Приложение в Docker зависает при записи логов: проверяем logging backpressure

Сервис выполняет запрос и в конце пишет строку в stdout. После перевода логов на внешний сборщик иногда растёт задержка ответа. Сами вычисления укладываются в обычное время, а проблема чаще появляется, когда сборщик принимает сообщения медленно. Проверьте путь от записи приложения до драйвера логирования. Если приложение пишет синхронно, эта запись может оказаться частью времени ответа пользователю.

Узнайте, куда идут stdout и stderr

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

docker inspect --format '{{.HostConfig.LogConfig.Type}}' report-api
docker inspect --format '{{index .HostConfig.LogConfig.Config "mode"}} {{index .HostConfig.LogConfig.Config "max-buffer-size"}}' report-api

Вывод только этих полей позволяет проверить нужный параметр без печати всех настроек драйвера. Если mode не указан, Docker по умолчанию использует блокирующую доставку. Логи из контейнера идут напрямую драйверу. Когда тот долго обрабатывает сообщения, давление может дойти до приложения: буферы на пути исчерпываются, синхронная запись ждёт освобождения места. Если используется асинхронный логгер в приложении, проверьте и его очередь. Она может перенести задержку на другой поток или проявить её позже, когда заполнится собственный буфер.

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

Настройте буфер, если приложение может терять сообщения

В неблокирующем режиме Docker помещает сообщения в промежуточный буфер каждого контейнера, а драйвер читает их из буфера в своём темпе. Когда буфер заполнен, новые сообщения отбрасываются. Поэтому такой режим подходит, если в момент перегрузки важнее сохранить работу приложения, а отдельные строки служебного журнала допустимо потерять.

Ниже фрагмент Compose для стенда с уже выбранным драйвером fluentd. Здесь приёмник доступен Docker-демону на 127.0.0.1:24224. Для своего стенда укажите адрес приёмника со стороны демона и сохраните остальные параметры драйвера. Если приёмник недоступен на старте, обычный синхронный запуск драйвера Fluentd может не дать создать рабочий контейнер, поэтому проверку замедления проводите после успешного запуска.

services:
  app:
    image: report-api:lab
    logging:
      driver: fluentd
      options:
        fluentd-address: "127.0.0.1:24224"
        mode: "non-blocking"
        max-buffer-size: "4m"

Размер буфера имеет смысл выбирать вместе с допустимой длительностью всплеска. Если приложение выдаёт около 500 кБ логов в секунду и драйвер временно совсем перестал читать, 4 МБ соответствуют приблизительно восьми секундам входящего потока при пустом буфере. Реальная вместимость и темп зависят от сообщений и состояния драйвера. Увеличение буфера позволяет пережить более длинный кратковременный всплеск за счёт памяти. При постоянном превышении скорости приёма он всё равно заполнится.

Измерьте задержки и пропуски одновременно

Поднимите два одинаковых стенда с разным режимом доставки и одинаковой нагрузкой. На каждом пронумеруйте тестовые сообщения, запишите число запросов и замедлите только тестовый приёмник на известное время. Сравните задержки запросов и номера, которые дошли до сборщика. Если неблокирующий режим удерживает время ответа, но теряет обязательные события, переносите запись этих событий в канал с подходящими гарантиями доставки. Для обычной диагностики сохраните метрики ошибок и задержек, чтобы видеть сбой даже при выпадении части строк.

Посмотрите программу курса Docker и Docker Compose. В локальной практике вы будете сопоставлять сбои приложения с логами и настройками контейнера, а затем соберёте воспроизводимый Compose-проект с PostgreSQL. Базовый запуск можно пройти в пробном уроке о первом контейнере.

Продолжить на практике

  • Docker и Docker Compose: контейнеризация приложений ↗

    Практический курс Docker для разработчиков: упакуйте своё приложение, подключите PostgreSQL и запускайте окружение через Compose. Научитесь находить поломки, обновлять сервис и восстанавливать данные. 6 Dockerfile-задач с автопроверкой и 42 локальные лабораторные.