Как стать автором
Обновить

Комментарии 8

Денис, спасибо за отличную статью!

В методе получения временного буфера написано, что если размер запрашиваемого буфера превышает некий порог, то кеш не используется. Вы не пробовали запретить кеширование буферов, установив "jdk.nio.maxCachedBufferSize" в ноль, интересно, насколько это было бы применимо для средних нагрузок?

public static ByteBuffer getTemporaryDirectBuffer(int size) {
    // If a buffer of this size is too large for the cache, there
    // should not be a buffer in the cache that is at least as
    // large. So we'll just create a new one. Also, we don't have
    // to remove the buffer from the cache (as this method does
    // below) given that we won't put the new buffer in the cache.
    if (isBufferTooLarge(size)) {
        return ByteBuffer.allocateDirect(size);
    }
  ....

   /**
   * Returns the max size allowed for a cached temp buffers, in
   * bytes. It defaults to Long.MAX_VALUE. It can be set with the
   * jdk.nio.maxCachedBufferSize property. Even though
   * ByteBuffer.capacity() returns an int, we're using a long here
   * for potential future-proofing.
   */
  private static long getMaxCachedBufferSize() {
      String s = java.security.AccessController.doPrivileged(
          new PrivilegedAction<String>() {
              @Override
              public String run() {
                  return System.getProperty("jdk.nio.maxCachedBufferSize");
              }
          });

Решение с установкой "jdk.nio.maxCachedBufferSize", конечно, рабочее. Причём рабочее без изменения кода вообще, благодаря free при возвращении буфера:

    /**
     * Releases a temporary buffer by returning to the cache or freeing it. If
     * returning to the cache then insert it at the start so that it is
     * likely to be returned by a subsequent call to getTemporaryDirectBuffer.
     */
    static void offerFirstTemporaryDirectBuffer(ByteBuffer buf) {
        // If the buffer is too large for the cache we don't have to
        // check the cache. We'll just free it.
        if (isBufferTooLarge(buf)) {
            free(buf);
            return;
        }

        assert buf != null;
        BufferCache cache = bufferCache.get();
        if (!cache.offerFirst(buf)) {
            // cache is full
            free(buf);
        }
    }

Однако, такое решение прилично снизит пропускную способность, так как в этом случае мы имеем и копирование памяти из HeapByteBuffer в DirectByteBuffer и аллокацию direct-памяти при создании DirectByteBuffer.

На мой взгляд, для сервисов с небольшими нагрузками проще сразу работать с DirectByteBuffer и самому его аллоцировать.

У меня немного другое видение ситуации, проекты с небольшими нагрузками не заметят проблемы с кешем, потому что буферы будут успевать освобождаться, и память новая аллоцироваться не будет. А если создают какой-нибудь концепт, то он нужен здесь и сейчас, и большинство предпочтут ByteBuffer.wrap(byte[] data), потому что это проще.

Со средними проектами немного иначе, из них до высоких нагрузок доходят гораздо меньше, чем завершают свой жизненный цикл или остаются в средняках. И в какой-то момент, "jdk.nio.maxCachedBufferSize" может стать настоящим спасением, чтоб проект мог дожить своё время, или дать отсрочку на чтение Вашей статьи.

Скажу даже больше: статья будет в помощь для довольно узкого круга проектов и большинству не понадобится даже "jdk.nio.maxCachedBufferSize", так как сейчас довольно небольшое число проектов имеют собственный сетевой стэк.

Не думаю, что для тех, кто пишет микро-сервисы при помощи аннотаций RestController, PostMapping и тому подобных, вообще интересно знание о том, как Spring работает с сетью.

Но, если кто-то написал свой сетевой клиент/сервер при помощи NIO, то вопрос производительности в этом случае, как правило, стоит остро. Искренне надеюсь, что для таких проектов статья будет полезна.

Если я вас правильно понял, то у вас поток бизнес-логики пытается писать в неблокирующий сокет, а если не смог, то bytebuffer отдаётся другому потоку, который это сделает. Можно было бы сразу отдавать bytebuffer в поток-писатель, это очень слабо сказалось бы на latency. Я бы ещё понял, если бы вам очень хотелось иметь синхронные вызовы записи, чтобы обрабатывать сетевые ошибки в том же потоке, который приготовил байты для отправки, но раз уж у вас есть и асинхронная запись, то ваше приложение должно уже быть готово к асинхронности.

Да, вы правильно поняли. Относительно асинхронности - все наши приложения полностью асинхронны с самого начала, как и шина. Есть, конечно, и синхронный API, но внутри он всё равно работает через асинхронный.

Запись с потока бизнес-логики уменьшает время отклика до определённого уровня нагрузки на данном железе. На принимающей стороне ответ также пытается уйти с потока бизнес-логики. У нас есть реальные сервисы, которые отвечают со временем отклика менее 1 мс. Многократное перекладывание с потока на поток ощутимо увеличивает время отклика при цепочке из нескольких вызовов.

Правильно ли понимаю, что используете нативный сокет из коробки, если да, то почему не netty?

Когда начиналась наша шина данных (во времена становления Glassfish и JBoss), проект netty существовал только как часть JBoss (вроде не ошибаюсь). В то время, при исследовании производительности веб-сервисов на серверах Glassfish и JBoss (ещё спецификация EJB 2.x на xml-файлах без аннотаций и асинхронности), стало понятно, что их производительности не достаточно для telecom-нагрузок. Это стало той самой точкой, с которой началось развитие нашей шины на Java (более 15 лет назад).

Зарегистрируйтесь на Хабре, чтобы оставить комментарий