Docker: Обмежте лог-спам біля джерела — патерни логування додатків, які рятують диски

Було корисно?

Інциденти через заповнений диск рідко оголошують про себе ввічливо. Вони проявляються як «не вдається створити файл», «база даних тільки для читання» або «нод не готовий», а потім ви виявляєте, що винуватець — контейнер, який поводився з stdout як з особистим щоденником.

Логування в Docker за дизайном просте: пишіть у stdout/stderr, а середовище виконання вже далі це обробить. Воно також сумлінно записує кожний байт — чи він корисний, зайвий або одна помилка, надрукована десять тисяч разів на хвилину. Якщо ви хочете зекономити місце на диску (і зберегти своє розуміння в нічні години), найбільший виграш досягається в джерелі: всередині додатку, в тому самому рядку коду, де виникає лог.

Чому «виправити в Docker» — недостатньо

Так, ви повинні налаштувати ротацію логів Docker. Це базова вимога. Але це також лише запобіжний захід. Якщо ваш додаток логуватиме як переляканий аукціоніст, ротація просто перетворить одну велику проблему на багато менших, які все одно заповнять диск, все одно споживатимуть CPU, все одно наситять I/O, все одно задушать корисний сигнал шумом і все одно нароблять витрат на централізований інгест логів.

Платформи контейнерів заохочують певний гріх: «просто виводьте все в stdout». Це шлях найменшого опору і шлях максимальної шкоди. Средовище виконання контейнера не знає вашого наміру. Воно не може розрізняти «платіжний чек-аут не вдався» і «debug: ітерація циклу 892341». Воно просто записує байти.

Обмеження на джерелі означає: генерується менше подій логування, і ті, що генеруються, краще стискаються, їх легше шукати і вони більш дієві. Тут інженери додатків і SRE зустрічаються в коридорі і погоджуються: менше, краще — кращe ніж більше, гірше.

Операційна істина: обсяг логів — це характеристика продуктивності. Трактуйте її як затримку. Вимірюйте її. Виділяйте бюджет. Регресії мають провалювати збірки.

Цитата, яку варто додати до кожного PR з логування:

Парафраз ідеї — Werner Vogels: ви будуєте — ви експлуатуєте; відповідальність включає те, що ваше програмне забезпечення робить у продакшні, у тому числі й його шум.

Факти та контекст, які пояснюють сучасний безлад з логами

Це не дрібниці. Це причини, чому ваші диски наповнюються ідеально збереженим нісенітництвом.

  1. Unix спочатку трактував логи як файли, потім як потоки. Syslog і текстові файли з’явилися раніше за «все в stdout». Контейнерне логування змінило транспорт за замовчуванням на потоки.
  2. Початковий драйвер логування Docker (json-file) пише один JSON-об’єкт на рядок. Це помірно дружньо для людей, зручно для машинного інгесту і небезпечно легко розростається без обмежень.
  3. «12-factor» популяризував логування в stdout. Чудово для переносимості. Але це не принесло дисципліни контролю обсягу; ця частина — на вас.
  4. Вендори агрегації логів беруть плату за обсяг інгесту. Ваш CFO тепер може постраждати через один logger.debug в гарячому циклі.
  5. Рання культура мікросервісів нормалізувала «логуй все; шукай пізніше». Це працювало при маленькому трафіку і невеликій кількості систем. На масштабі це як зберігати кожен натиск клавіші «на всякий випадок».
  6. Структуроване логування відродилося, бо grep перестав масштабуватись. JSON-логи чудові — поки ви не почнете еммітувати 20 полів на кожен запит і потроїти байти.
  7. Контейнери зробили збереження логів неоднозначним. У віртуальних машинах ви робили ротацію файлів. У контейнерах часто немає надійної записуваної файлової системи, тож люди скидують в stdout і сподіваються.
  8. Мітки й поля високої кардинальності стали тихим податком. Ідентифікатори трасування корисні; додавання унікального вводу користувача як поля в кожен рядок — це шлях до випадкового створення «озер даних».

План швидкої діагностики

Якщо ви на чергуванні і нода кричить, у вас немає часу на філософію. Потрібно швидко знайти вузьке місце і вирішити, чи це проблема логування, середовища виконання чи зберігання.

Перше: підтвердіть симптом і радіус ураження (диск vs I/O vs CPU)

  • Тиск на диск: df показує файлову систему майже на 100%.
  • I/O тиск: підвищені await-часи, висока кількість записів IOPS, повільні відповіді додатку.
  • Тиск на CPU: серіалізація логів і форматування JSON можуть спалювати CPU, особливо зі стектрейсами та великими об’єктами.

Друге: визначте головного «розмовника» (контейнер, процес або агент хоста)

  • Шукайте найбільші файли логів Docker і контейнери, з якими вони пов’язані.
  • Перевірте, чи лог-шифтер (Fluent Bit, Filebeat тощо) не посилює проблему циклами повторних спроб/зворотним тиском.
  • Підтвердьте, чи додаток повторює те саме повідомлення; якщо так — застосуйте обмеження частоти або дедуплікацію на боці додатку.

Третє: вирішіть найшвидшу безпечну міру

  • Аварійне пом’якшення: зупиніть/перезапустіть найгіршого порушника, встановіть ротацію логів якщо її немає, зменшіть рівень логування через конфіг, або тимчасово застосуйте семплювання логів.
  • Післяінцидентне виправлення: змініть патерни логування так, щоб інцидент не міг повторитися з одного шляху коду.

Жарт №1: Якщо ваш диск заповнився логами — вітаю — ви винайшли дуже дорогий, дуже повільний базовий сховище без індексів.

Практичні завдання: команди, виводи, рішення

Це ті перевірки, які ви робите на реальному хості о 02:13. Кожне завдання включає команду, що означає її вивід і рішення, яке ви з нього приймаєте.

Завдання 1: Підтвердьте тиск на диск і яка файлова система уражена

cr0x@server:~$ df -h
Filesystem                         Size  Used Avail Use% Mounted on
/dev/nvme0n1p2                      220G  214G  2.9G  99% /
tmpfs                               32G     0   32G   0% /dev/shm

Значення: Коренева файлова система фактично заповнена. Контейнери та їхні логи часто розташовані під /var/lib/docker на /.

Рішення: Не запускайте «скрипти очищення» без розбору. Спочатку ідентифікуйте, що споживає простір; уникайте видалення стану рантайму, якщо ви не готові до даунтайму.

Завдання 2: Знайдіть найбільші каталоги у сховищі Docker

cr0x@server:~$ sudo du -xhd1 /var/lib/docker | sort -h
1.2G    /var/lib/docker/containers
8.4G    /var/lib/docker/image
12G     /var/lib/docker/overlay2
22G     /var/lib/docker

Значення: containers достатньо великий, щоб підозрювати ріст логів. overlay2 також може бути великим через записувані шари.

Рішення: Заглибтесь у /var/lib/docker/containers, щоб знайти великі файли логів і співставити їх із контейнерами.

Завдання 3: Знайдіть найбільші файли логів контейнерів

cr0x@server:~$ sudo find /var/lib/docker/containers -name "*-json.log" -printf "%s %p\n" | sort -n | tail -5
2147483648 /var/lib/docker/containers/2c1c3e.../2c1c3e...-json.log
3221225472 /var/lib/docker/containers/7a8b9c.../7a8b9c...-json.log
4294967296 /var/lib/docker/containers/aa0bb1.../aa0bb1...-json.log

Значення: У вас багатогігабайтні JSON-файли логів. Це не «трохи дебагу», це фонтан логів.

Рішення: Ідентифікуйте контейнери за цими ID та перевірте, що вони емiтять.

Завдання 4: Зіставте ID контейнера з іменем та образом

cr0x@server:~$ docker ps --no-trunc --format "table {{.ID}}\t{{.Names}}\t{{.Image}}" | grep aa0bb1
aa0bb1d3f0e9c1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4  payments-api  registry.local/payments-api:3.14.2

Значення: Контейнер payments API генерує величезний файл логів.

Рішення: Проінспектуйте останні логи і шукайте патерни повторення (той самий рядок, однаковий стектрейс, однаковий шлях запиту).

Завдання 5: Вибірковий перегляд останніх логів без вивантаження всього файлу

cr0x@server:~$ docker logs --tail 50 payments-api
{"level":"error","msg":"db timeout","tenant":"blue","path":"/charge","retry":1}
{"level":"error","msg":"db timeout","tenant":"blue","path":"/charge","retry":1}
{"level":"error","msg":"db timeout","tenant":"blue","path":"/charge","retry":1}

Значення: Повторювані ідентичні помилки. Ймовірно, цикл повторних спроб логуватиме кожну спробу.

Рішення: Пом’якште зараз, зменшивши рівень логів або застосувавши обмеження частоти для цього конкретного повідомлення. Потім виправте код: логувати раз на вікно невдач, а не при кожній спробі.

Завдання 6: Перевірте драйвер логування контейнера та опції

cr0x@server:~$ docker inspect -f '{{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}' payments-api
json-file {"max-file":"1","max-size":"0"}

Значення: max-size = 0 (фактично без обмежень), тоді як max-file не має значення.

Рішення: Виправте конфіг середовища виконання (compose/systemd/daemon.json), але не зупиняйтесь на цьому; додаток усе ще емiтує надто багато.

Завдання 7: Перевірте системні значення за замовчуванням для демона

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Значення: Якщо це присутнє, нові контейнери повинні мати ротацію. Існуючі контейнери могли бути створені до встановлення цих дефолтів або мати перезаписані налаштування на контейнер.

Рішення: Стандартизувати шляхи створення. Переконатися, що compose-стеки або специфікації оркестратора не перезаписують ліміти.

Завдання 8: Визначте високу активність запису I/O, спричинену логуванням

cr0x@server:~$ iostat -xz 1 3
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.31    0.00    6.44   38.27    0.00   42.98

Device            r/s     rkB/s   rrqm/s  %rrqm   r_await  w/s     wkB/s   w_await aqu-sz  %util
nvme0n1          2.1      86.3     0.0     0.0     3.21   912.4  18432.0  42.10   39.2   98.7

Значення: Дуже висока завантаженість запису і високий w_await. Запис логів на диск може домінувати час пристрою.

Рішення: Зменшіть обсяг логів негайно. Якщо ви продовжите писати з такою швидкістю, диск стане вузьким місцем для всього.

Завдання 9: Підтвердьте, які процеси інтенсивно записують

cr0x@server:~$ sudo pidstat -d 1 3
Linux 6.5.0 (server)  01/03/2026  _x86_64_  (16 CPU)

01:12:11      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
01:12:12        0      2471      0.00  18240.00      0.00  dockerd
01:12:12        0     19382      0.00   3100.00      0.00  fluent-bit

Значення: Демон Docker пише величезні обсяги (логи контейнерів). Шипер також активно записує/обробляє багато даних.

Рішення: Спочатку розберіться з джерелом — контейнером; потім налаштуйте буферизацію/поведінку шипера, щоб уникнути зворотних циклів.

Завдання 10: Перевірте, чи додаток повторно логуватиме стектрейси

cr0x@server:~$ docker logs --tail 200 payments-api | grep -c "Traceback\|Exception\|stack"
147

Значення: Часті стектрейси дорогі по байтах і CPU. Часто це одна й та сама повторювана помилка.

Рішення: Логувати один стектрейс на унікальну помилку за вікно часу; для решти емiтувати лічильники/метрики.

Завдання 11: Визначте швидкість подій логів (рядків на секунду) з сирого файлу логів

cr0x@server:~$ cid=$(docker inspect -f '{{.Id}}' payments-api); sudo sh -c "tail -n 20000 /var/lib/docker/containers/$cid/${cid}-json.log | wc -l"
20000

Значення: Це 20k рядків у недавньому сегменті tail. Якщо цей сегмент — «кілька секунд», ви фонтануєте. Якщо це «хвилини», все одно занадто гучно.

Рішення: Встановіть бюджет: наприклад, у стійкому стані < 50 рядків/сек на екземпляр; вибухи дозволені лише з семплюванням і лімітами.

Завдання 12: Виміряйте, як швидко росте файл логів

cr0x@server:~$ cid=$(docker inspect -f '{{.Id}}' payments-api); sudo sh -c "stat -c '%s %y' /var/lib/docker/containers/$cid/${cid}-json.log; sleep 5; stat -c '%s %y' /var/lib/docker/containers/$cid/${cid}-json.log"
4294967296 2026-01-03 01:12:41.000000000 +0000
4311744512 2026-01-03 01:12:46.000000000 +0000

Значення: ~16 MB за 5 секунд (~3.2 MB/s). Це швидко заповнить диски і задушить I/O.

Рішення: Негайна міра: зменшити рівень, відключити галузь, перезапустити з змінною середовища. У довгому строку: імплементувати тротлінг/дедуплікацію.

Завдання 13: Перевірте, чи контейнер перезапускається через crash-loop і при цьому генерує гучні стартові логи

cr0x@server:~$ docker inspect -f '{{.State.Status}} {{.RestartCount}}' payments-api
running 47

Значення: Багато перезапусків. Кожен перезапуск може повторно вивести великі банери/дампи конфігів, множачи шум.

Рішення: Виправити корінну причину крашів і подавляти гучні стартові виводи; зробіть «одноразову інформацію при старті» дійсно одноразовою.

Завдання 14: Підтвердіть, чи лог-шифтери перебувають під зворотним тиском і повторюють спроби (що посилює шум)

cr0x@server:~$ docker logs --tail 50 fluent-bit
[warn] [output:es:es.0] HTTP status=429 URI=/_bulk
[warn] [engine] failed to flush chunk '1-173587...' retry in 8 seconds

Значення: Нижня ланка обмежує швидкість. Ваші логи не лише заповнюють диск; вони також спричиняють шторм повторів і буферизацію в пам’яті/на диску.

Рішення: Зменшіть обсяг логів у додатку, потім налаштуйте буферизацію шипера і розгляньте відкидання малоцінних логів під навантаженням.

Завдання 15: Проінспектуйте частоту підозрілих повідомлень (топ повторюваних рядків)

cr0x@server:~$ docker logs --tail 5000 payments-api | jq -r '.msg' | sort | uniq -c | sort -nr | head
4821 db timeout
112 cache miss
45 payment authorized

Значення: Одне повідомлення домінує. Це ідеальна ціль для дедуплікації і обмеження частоти.

Рішення: Замініть пер-евентні помилкові логи на: (a) періодичний підсумок, (b) лічильник-метрику, (c) один семпльний екземпляр з контекстом.

Патерни логування в додатках, що реально зменшують обсяг

Це основне: патерни кодування й операційні контракти, що запобігають лог-спаму. Ви можете реалізувати їх у будь-якій мові; принципи не залежать від фреймворку.

1) Перестаньте логувати під час кожної спроби повтору; логуйтесь на результат протягом вікна

Повтори — нормальні. Логувати кожну спробу — ні. Якщо залежність впаде, цикл повторів може створити ідеальний підсилювач логів: помилка викликає повтори, повтори викликають логи, логи створюють I/O тиск, I/O тиск викликає ще таймаути, таймаути викликають ще помилок.

Робіть так: зафіксуйте першу невдачу з контекстом; потім обмежуйте частоту повторних повідомлень; потім емiтуйте підсумок кожні N секунд: «db timeout триває; приховано 4,821 схожих помилок».

Хороший патерн:

  • Один «екземпляр» помилки зі стектрейсом та метаданими запиту (але див. зауваження про приватність нижче).
  • Лічильник-метрика для кожної події невдачі.
  • Періодичний підсумок по залежності, по інстансу.

2) Задайте бюджет логів для стійкого стану і дотримуйтеся його

Більшість команд сперечаються про формати логів. Краще сперечатися про бюджет швидкості логів. Наприклад:

  • На екземпляр сервісу в стійкому стані: < 1 KB/s середнього трафіку логів.
  • Дозволений сплеск: до 50 KB/s протягом 60 секунд під час інцидентів.
  • Понад сплеск: семплювання на 1%, збереження екземплярів помилок, відкидання debug/info.

Це дає SRE чіткий SLO-подібний поріг і дає командам додатків мету для тестування. Додайте CI-перевірку, яка виконує синтетичне навантаження і провалює тест, якщо логи перевищують бюджет.

3) За замовчуванням — структуровані логи, але не перестарайтеся

JSON-логи — стандарт у контейнерному середовищі. Їх також легко зловживати. Кожне додаткове поле коштує байтів. Деякі поля також коштують грошей на індексацію.

Залишайте: timestamp, level, message, service name, instance ID, request ID/trace ID, latency, status code, dependency name, error class.

Уникайте: повних тіл запитів, необмежених масивів, сирих SQL-рядків і міток високої кардинальності, скопійованих у кожен рядок.

4) Не логувати в тісних циклах без тротлінгу

Цикли зустрічаються всюди: опитування, споживання черг, сканування директорій, повтори блокувань, перевірки стану з’єднання. Якщо ви логуватимете в циклі, ви створили майбутній інцидент. Не можливість — запланована подія.

Правило: будь-який лог, який може виконуватись більше одного разу на секунду у стійкому стані, має бути за лімітером частоти, або за воротами зміни стану, або і тим, і тим.

5) Логувати зміни стану, а не підтвердження стану

«Все ще підключено» кожні 5 секунд — марно. «З’єднання відновлено через 42 секунди, приховано 500 невдач» — корисно. Людям потрібні переходи. Машинам потрібні лічильники.

Реалізуйте просту кінцеву машину станів для здоров’я залежності (UP → DEGRADED → DOWN) і емiтуйте логи лише на переходах та у періодичних підсумках.

6) Використовуйте «dedupe keys» для повторюваних помилок

Повторювані помилки часто мають підпис: той самий тип виключення, та сама залежність, той самий endpoint. Обчислюйте ключ дедуплікації, наприклад:

  • dedupe_key = hash(error_class + dependency + path + error_code)

Потім тримайте невеликий in-memory мап у процесі: last-seen timestamp, suppressed count і один екземпляр payload. Емiтуйте:

  • Перша поява: логувати як зазвичай.
  • У межах вікна: інкрементувати лічильник прихованих, можливо емiтувати семпл в debug.
  • Наприкінці вікна: логувати підсумок з кількістю прихованих і ID одного екземпляра.

7) Семплуйте інформаційні логи; ніколи не семплуйте метрики

Семпліювання — це скальпель. Використовуйте його для подій з високим обсягом і низькою цінністю: access-логи на кожен запит, «cache miss», «job started». Помилки тримайте переважно несемплованими, але можна семплувати повторювані ідентичні помилки після першого екземпляра.

Метрики потрібні для підрахунку. Лічильник — дешевий і точний. Не замінюйте метрики логами; це як замінити термометр на інтерпретативний танець.

8) Зробіть «debug mode» запобіжником, а не просто рівнем

Debug-логи в продакшні мають бути тимчасовими, таргетованими і зворотними без деплойменту. Найбезпечніший підхід:

  • Debug-логи існують, але за замовчуванням — вимкнені.
  • Увімкнення debug для конкретного request ID, user ID (хешованого) або tenant на обмежений час.
  • Автовимкнення після TTL.

Це запобігає класичній помилці: «увімкнули debug для розслідування, забули і платили за нього тиждень».

9) Перестаньте логувати «очікувані помилки» як ERROR

Якщо клієнт скасовує запит — це не помилка сервера; це буденна подія. Якщо користувач вводить неправильний пароль — це не серверна помилка; це бізнес-логіка. Якщо ви логуватимете це як error, ви навчите on-call ігнорувати ERROR. Так можна пропустити реальний збій.

Патерн:

  • Використовуйте info або warn для помилок, спричинених клієнтом.
  • Використовуйте error для серверних відмов, що потребують уваги.
  • Використовуйте fatal рідко, і тільки коли процес завершиться.

10) Обрізайте payload-и; логувати покажчики

Логування повних тіл запитів/відповідей пожирає диск і створює ризики приватності. Натомість:

  • Логируйте розмір payload у байтах.
  • Логируйте хеш вмісту (щоб корелювати повтори без зберігання вмісту).
  • Логируйте ID об’єкта, який можна отримати з безпечного сховища за потреби.

11) Зробіть стектрейси опцією і з обмеженнями

Стектрейси можуть бути корисними. Водночас вони можуть бути 200 рядків шуму, повторюваного 10,000 разів. Обмежуйте їх:

  • Включайте стектрейс для першої появи dedupe-ключа за вікно.
  • Обрізайте глибину стектрейсу, де це підтримується.
  • Для повторів надавайте тип виключення + повідомлення + верхні кадри стека.

12) Використовуйте «once» логер для стартової конфігурації

Стартові логи часто друкують повну конфігурацію, змінні оточення, feature-флаги та списки залежностей. Це нормально одного разу. Це хаос, коли процес у циклі падінь і друкує це 50 разів.

Патерн: логувати стиснутий стартовий підсумок і хеш конфігурації. Детальну конфігурацію зберігати в іншому місці (або віддавати через захищений ендпоінт), а не в логах.

13) Ставтеся до логування як до залежності з бэкпрешером

Більшість бібліотек логування претендують, що запис — безкоштовний. Це не так. Коли вихід блокується (повільний диск, заблокований stdout pipe, тиск драйвера логів), ваш додаток може застопоритися.

Робіть так:

  • Віддавайте перевагу асинхронному логуванню з обмеженими чергами.
  • Коли черга заповнена — відкидайте логи низького пріоритету першими.
  • Експонуйте метрики: відкинуті логи, глибина черги, час логування.

14) Робіть логи такими, що легше стискаються

Якщо ви не можете достатньо зменшити обсяг, бодай зробіть його добре стискуваним. Повторюваність стискається. Випадковість — ні. Хороші логи:

  • Використовують стабільні шаблони повідомлень: "db timeout" замість "db timeout after 123ms on host a1b2" в самій текстовій частині.
  • Розміщають змінні дані у полях, а не в повідомленні.
  • Уникають друку випадкових UUID у кожному рядку, якщо вони не потрібні для кореляції.

15) Додайте «лог-ф’юз» для аварійних ситуацій

Іноді потрібен вимикач: «Якщо логи перевищують X рядків/сек протягом Y секунд, автоматично підвищити семплювання і придушити повторювані INFO/WARN». Це не красиво, але краще, ніж відмова через заповнений диск.

Реалізуйте це з локальним лічильником і ковзним вікном. При спрацьовуванні емiтуйте один гучний лог: «лог-ф’юз спрацював; семплювання тепер 1%; приховано N рядків».

Жарт №2: Логування — як кава: невеликі порції покращують продуктивність, але занадто багато перетворюють систему на нервову істоту, яка не перестає базікати.

Три корпоративні міні-історії (анонімізовані, правдоподібні та технічно точні)

Міні-історія 1: Інцидент через хибне припущення

Вони припустили, що Docker ротує логи за замовчуванням. Команда перейшла з VM-оточення, де logrotate був скрізь, і поводилася так, ніби рантайм контейнера має розумні налаштування за замовчуванням.

Під час інтеграційного тесту з партнером сервіс почав провалювати аутентифікацію. Сервіс мав полісі повторів з експоненційним бекафоффом і джитером. Але розробник додав error-лог всередині циклу повторів, щоб «зробити видимим». Він був видимим. І він був безжалісним.

Перший знак проблеми не був алармом про диск. Це була база даних, що скаржилася на повільні запити. Хост з гучним контейнером мав кореневий диск майже заповнений, і латентність I/O вийшла з-під контролю. Драйвер логів писав JSON-рядки як метроном.

On-call робили те, що зазвичай роблять люди: перезапускали сервіс. Це тимчасово зменшувало обсяг логів, бо давало кілька секунд перед тим, як повтори знову наростуть. Перезапускали знову. Те саме. Тим часом лог-шифтер повторював інгест через те, що нижній шар обмежував швидкість, що додавало ще один шар запису.

Виправлення було смішно простим: додати ротацію логів на рівні Docker і змінити додаток, щоб логував лише першу невдачу за вікно, потім підсумовував. Урок був гостріший: «припущені значення за замовчуванням» — не стратегія надійності.

Міні-історія 2: Оптимізація, що обернулась проти

Інша організація хотіла «ідеальну спостережуваність». Вони додали структуроване логування всюди, що добре. Потім вирішили, що кожен рядок логу має містити повний контекст запиту для легшого дебагу: заголовки, query params і шматок тіла.

В стадії це працювало чудово. У продакшні це стало вогнем на рахунках інгесту. Гірше того, це стало проблемою продуктивності: JSON-серіалізація великих об’єктів для кожного запиту з’їдала CPU, а рантайм контейнера сумлінно писав великі рядки. Латентність зросла, що створило більше таймаутів, що створило більше помилок, що створило ще більші стектрейси. Класичний зворотний цикл.

Симптом на чергуванні виглядав як проблема пропускної здатності: «нам потрібні більші ноди». Але реальне вузьке місце було самостійно спричинене I/O і CPU тиском від логів. Коли вони зменшили логування payload-ів і перейшли на «лог-показчики» (request ID, хеш payload, розмір), система стабілізувалася без зміни розмірів інстансів.

Оптимізація була «зменшити час дебагу, логуючи все». Відкат — «збільшити кількість інцидентів і витрати, логуючи все». Щасливий кінець: вони зберегли структуроване логування — просто без частин, що мали належати в захищеному трасувальному сховищі.

Міні-історія 3: Нудна, але правильна практика, яка врятувала день

Фінансовий сервіс обробляв періодичні батч-джоби. Нічого надзвичайного. Команда мала практику, що здавалася майже старомодною: кожен сервіс мав письмовий бюджет логів і тест, що вимірював пропускну здатність логів під навантаженням. Якщо зміна збільшувала логи понад бюджет — збірка провалювалася, поки інженер не обґрунтував це.

Одної п’ятниці залежність почала повертати періодичні 500. Сервіс повторював, але патерни логування вже були обмежені частотою і дедупліковані. Вони емiтували один екземпляр помилки з trace ID, потім один підсумок кожні 30 секунд: «помилки залежності тривають; приховано N». Метрики стрибнули, аларми спрацювали, але диски залишилися спокійними.

Поки інші команди боролися з тиском на диск і тонули в повторюваних стектрейсах, цей сервіс залишався читабельним. On-call міг бачити, що змінилося (поведінка залежності), кількісно оцінити (метрики) і скорелювати (trace IDs). Інцидент був дратівливим, але він не переріс у відмову рівня ноди.

Після цього ніхто не писав великого внутрішнього поста про «героїзм». Це було нудно. У цьому і вся суть. Нудні практики надійності працюють довго.

Типові помилки: симптом → корінна причина → виправлення

1) Симптом: Логи Docker ростуть без меж

Корінна причина: драйвер json-file без max-size/max-file, або контейнери створені до встановлення дефолтів.

Виправлення: Задайте дефолти демона і змусьте дотримуватися конфігур за сервісом. Пересоздайте контейнери, щоб підхопити ліміти. І все одно виправте додаток, щоб він не генерував сміття.

2) Симптом: Диск заповнився після відмови залежності

Корінна причина: цикл повторів логував кожну спробу (часто зі стектрейсами).

Виправлення: логувати першу невдачу + підсумок; рахувати повтори як метрики; обмежувати частоту логів за dedupe-ключем; додати circuit breakers.

3) Симптом: On-call ігнорує ERROR, бо він завжди шумний

Корінна причина: очікувані клієнтські події логуються як error (скасування, 4xx, валідація).

Виправлення: виправити мапінг рівнів і правила алертингу; резервувати ERROR для серверних відмов, що потребують уваги.

4) Симптом: Високий CPU без очевидного росту бізнес-навантеження

Корінна причина: дорогі операції форматування логів (інтерполяція рядків, серіалізація великих об’єктів) на гарячих шляхах.

Виправлення: lazy-логування (форматувати тільки якщо ввімкнено), уникати серіалізації повних об’єктів, препроцесувати шаблони повідомлень, семплювати малоцінні логи.

5) Симптом: Лог-шифтер показує повтори, ріст пам’яті або відкинуті чанки

Корінна причина: обмеження інгесту вниз по ланцюжку плюс високий upstream-обсяг; буферизація шипера посилює використання диску.

Виправлення: зменшити обсяг логів додатку; налаштувати backpressure і політики відкидання в шипері; пріоритезувати екземпляри помилок і підсумки.

6) Симптом: «Не можемо знайти потрібні рядки» під час інциденту

Корінна причина: відсутні поля контексту (request ID, версія сервісу, ім’я залежності) і забагато повторюваного шуму.

Виправлення: додати необхідні поля контексту; дедуплікувати повтори; логувати переходи станів; робити повідомлення послідовними.

7) Симптом: У логах з’явилися чутливі дані

Корінна причина: логування тіл запитів/відповідей, дампи заголовків або повідомлення виключень з секретами.

Виправлення: редагувати на джерелі, припинити логування payload-ів, додати allowlist для полів, автоматично аудиторити логи, ставитися до логів як до продакшн-даних.

8) Симптом: «Виправлення» — збільшити розмір диска, але проблема повертається

Корінна причина: тимчасове рішення за рахунок потужності; без зміни патернів емісії.

Виправлення: впровадити бюджети логів, застосувати обмеження частоти і додати регресійні тести для обсягу логів.

Чеклісти / покроковий план

Покроково: припиніть кровотечу під час активного інциденту

  1. Підтвердьте використання диска: df -h. Якщо корінь > 95%, вважайте це терміновим.
  2. Знайдіть найбільші файли логів: find /var/lib/docker/containers -name "*-json.log" відсортовані за розміром.
  3. Зіставте файл → контейнер: docker ps --no-trunc і docker inspect.
  4. Виявте повтори: вибірково перегляньте останні логи; перевірте найпоширеніші повторювані повідомлення.
  5. Швидке пом’якшення: тимчасово зменшіть рівень логів, увімкніть семплювання або відключіть гучний компонент. Якщо потрібно — перезапустіть контейнер з безпечними налаштуваннями.
  6. Відновіть запас місця: після зупинки емісії видаліть або обріжте лише найгірший файл логів, якщо ви погоджуєтесь втратити ці логи. Віддавайте перевагу ротації і контрольованим перезапускам над ручним видаленням.
  7. Підтвердьте відновлення I/O: iostat і латентність сервісів мають нормалізуватися.

Покроково: запобігти повторенню (що робити після інциденту)

  1. Задайте дефолти Docker: max-size і max-file у /etc/docker/daemon.json.
  2. Аудит per-service override-ів: файли compose, systemd-юнити, специфікації оркестратора.
  3. Інструментуйте обсяг логів: відстежуйте рядки/сек і байти/сек на інстанс сервісу.
  4. Впровадьте дедуплікацію + ліміти: за сигнатурою помилки, за залежністю.
  5. Замініть спам на підсумки: періодичні зведення плюс екземпляри.
  6. Перенесіть великий контекст у трейси: тримайте логи легкими; використовуйте request ID для переходу до деталей.
  7. Додайте CI-страхувалку: навантажувальний тест, що провалює збірку при регресії бюджету логів.
  8. Проведіть огляд приватності: редагування, allowlist і перевірка просочування секретів.

Операційний чекліст: як має виглядати «добре»

  • ERROR-логи рідкісні, дієві і не домінуються одним повторюваним рядком.
  • Info-логи семплуються або обмежуються на гарячих шляхах (запити, споживачі черг).
  • Кожен сервіс має бюджет логів і відомий середній рівень рядків/сек.
  • Кожна повторювана помилка має dedupe-ключ, вікно пригнічення і підсумковий рядок.
  • Логи містять контекст для кореляції (trace/request ID, версія), а не дані, які не слід зберігати.
  • Коли інгест обмежений, система деградує плавно (спочатку відкидаються малоцінні логи).

FAQ

1) Чи достатньо просто змінити драйвер логування Docker, щоб виправити це?

Ні. Зміна драйвера може допомогти з ротацією, доставкою або характеристиками продуктивності, але це не виправить додаток, який емiтує сміття. Спочатку виправляйте емісію; потім обирайте драйвер відповідно до операційних потреб.

2) Чи завжди коректно логувати в stdout у контейнерах?

Це стандартний підхід, але не автоматично правильний. Stdout підходить, якщо ви ставитесь до нього як до обмеженого каналу з бюджетами, семплюванням і лімітами. Якщо вам потрібні надійні локальні логи — використайте volume і керуйте ротацією, але це має бути свідоме рішення, а не випадковість.

3) На якому рівні має працювати продакшн?

Зазвичай info або warn, з таргетованими debug-переключеннями. Якщо вам постійно потрібен debug для роботи — ймовірно, бракує метрик, трасування або структурованого контексту.

4) Як переконати команди перестати логувати тіла запитів?

Скажіть їм правду: це ризик для надійності й безпеки. Запропонуйте альтернативу: логувати request ID, розмір payload, хеш, і зберігати деталі у захищеному сховищі з контролем доступу, якщо дійсно потрібно.

5) Який найпростіший спосіб обмеження частоти в додатку?

Часове вікно на повідомлення (або на dedupe-ключ): логувати першу появу, потім придушувати протягом N секунд, при цьому рахувати приховані випадки, і періодично емiтувати підсумок.

6) Хіба семплювання не ускладнить дебаг?

Семплювання робить дебаг можливим, коли альтернатива — потонути в шумі. Зберігайте екземпляри (перша поява, унікальні сигнатури) і тримайте метрики для повноти. Ви не зможете дебагувати те, що не можете прочитати.

7) Як виявити лог-спам до того, як він виведе ноду з ладу?

Алармуйте за швидкістю росту логів (байти/сек) і за раптовими змінами у топ-повторюваних повідомленнях. Якщо ви алармуєте лише за «диск > 90%», ви дізнаєтесь занадто пізно.

8) Чому повторювані стектрейси так болісні?

Вони великі, повільні в форматуванні і часто ідентичні. Вони марнують CPU і диск, і руйнують сигнал пошуку. Тримайте один екземпляр за вікно; рахуючи решту.

9) Чи безпечно видаляти великий файл *-json.log щоб відновити простір?

Іноді так, але це гострий інструмент. Видалення файлу, який ще відкритий процесом, може не звільнити простір, поки дескриптор не закриють. Віддавайте перевагу ротації, перезапуску контейнера або контрольованому усіченню під час інциденту — а потім виправте джерело емісії.

10) Як зберегти логи корисними, водночас зменшуючи обсяг?

Робіть логи подієвими: переходи станів, підсумки, екземпляри. Переносьте деталі з високим обсягом у метрики (підрахунки) і трейси (багатий контекст на запит). Логи мають пояснювати інциденти, а не їх відтворювати.

Наступні кроки, які ви можете виконати цього тижня

Якщо ви зробите лише одну річ, зробіть цю: видаліть логування з циклів повторів і замініть його на дедупльовані екземпляри з періодичними підсумками. Цей один патерн запобігає цілій категорії інцидентів з заповненням диска та I/O-трясінням.

Потім:

  1. Задайте дефолти ротації логів Docker і перевірте, що кожен контейнер їх успадковує.
  2. Визначте бюджет логів на сервіс і вимірюйте рядки/сек та байти/сек під навантаженням.
  3. Реалізуйте обмеження частоти і dedupe-ключі для повторюваних помилок і гарячих інформаційних логів.
  4. Припиніть логувати payload-и; логируйте покажчики та хеші замість них.
  5. Додайте «лог-ф’юз», щоб один поганий реліз не зміг вивести ноду з ладу через надмірне базікання.

Ви не здобуваєте надійності, записуючи більше логів. Ви здобуваєте її, роблячи ті логи, що залишаються, вартими байтів, які вони займають.

← Попередня
Основи ZFS iostat: перетворення цифр на вузьке місце
Наступна →
Docker: I/O wait з пекла — обмежте контейнер, який вбиває ваш хост

Залишити коментар