Low-latency оптимизации

Что учесть в Java приложении для low-latency

Количество потоков

При проектировании приложения важно сопоставлять количество активных потоков с количеством ядер процессоров системы. Какие-то потоки будут CPU-bound, какие-то — IO-bound. IO-bound потоки часто блокируются при ожидании данных из сети, или при отправке данных в сеть. Большую часть своей «жизни» они проводят в ожидании и не занимают сильно процессор. CPU-bound потоки — это потоки делающие большие сложные вычисления, им нужны вычислительные мощности процессора в полной мере.

Если CPU-bound потоков больше, чем ядер процессора, какой-то поток будет «голодать», так как его постоянно будет вытеснять другой CPU-bound поток.

Учитываться должны не только потоки самого приложения, но и сервисные потоки самой JVM. Поэтому очень желательно иметь на своем сервере процессоры с большим количеством ядер.

Thread Affinity

Важно прикрепить критически важные потоки к определенному ядру процессора, запретить операционной системе переносить этот поток на другие ядра, а также запретить операционной системе переносить на это ядро другие потоки. То есть на конкретном ядре выполняется только критически важный поток и только он. В приложении таких потоков может быть несколько, поэтому опять-таки важно чтобы числа ядер процессора хватило на все критически важные потоки и плюс осталось еще ядер на некритически важные потоки. Affinity делается с помощью сторонних библиотек прямо в коде приложения. Мы используем OpenHFT/Java-Thread-Affinity.

Доступ к общей памяти

Минимальный доступ потоков к общей памяти и переменным в ней, тогда не требуется синхронизация и блокировки потоков, чтобы разрулить их совместный доступ к одной и той же переменной.

Lock-free

Использовать lock-free алгоритмы и структуры данных. Блокировки существенно повышают latency системы, вызывают переключения контекста ОС.

В случае lock-free структур данных часть кода будет загружать ядро процессора на 100%. Это нормально. Убедитесь только, что поток надежно привязан к этому ядру, иначе ОС будет пытаться его остановить, запарковать и исполнить на ядре другой поток (см. про Affinity выше).

Обращения к диску

Снизить до минимума записи в логи. Писать в логи только то, что совершенно необходимо для отслеживания работы системы. В low-latency программах записи на диск — это самые дорогие операции. Запись в лог должна осуществляться асинхронно: программа не должна ждать в коде, когда закончится запись на диск, прежде чем выполнить следующую команду: это приводит к блокировке потока, переключению контекста и затратам на запуск потока после того, как запись на диск завершена.

Сборка мусора

Снизить до минимума количество мусора, которое производит приложение. Чем быстрее heap заполнится мусором, тем скорее запустится сборщик мусора.

Хранить данные и объекты можно за пределами JVM heap в нативной памяти JVM процесса. Это делается либо с помощью недокументированного класса Unsafe.