Логи в Kubernetes (і не тільки) сьогодні: очікування та реальність
Йшов 2019 рік, а ми все ще не маємо стандартного рішення для агрегації логів у Kubernetes. У цій статті ми хотіли б, використовуючи приклади з реальної практики, поділитися своїми пошуками, проблемами та їхніми рішеннями.
Однак для початку обмовлюся, що різні замовники під збиранням логів розуміють дуже різне:
хтось хоче бачити security-і audit-логи;
хтось централізоване логування всієї інфраструктури;
а комусь достатньо збирати лише логи програми, виключивши, наприклад, балансувальники.
Про те, як ми реалізовували різні «хотілки» і з якими труднощами зіткнулися, — під катом.
Теорія: про інструменти для лігів
Передісторія про компоненти системи логування
Логування пройшло довгий шлях, в результаті якого виробилися методології збирання та аналізу логів, що ми й застосовуємо сьогодні. Ще в 1950-х роках у Fortran з'явився аналог стандартних потоків введення-виводу, які допомагали програмісту у налагодженні його програми. Це були перші комп'ютерні логи, які полегшували життя програмістам тих часів. На сьогодні ми в них бачимо перший компонент системи логування. джерело або "виробник" (producer) логів.
Комп'ютерна наука не стояла дома: з'явилися комп'ютерні мережі, перші кластери… Почали працювати складні системи, які з кількох комп'ютерів. Тепер системні адміністратори змушені були збирати логи з кількох машин, а в особливих випадках могли додавати повідомлення ядра ОС на випадок, якщо потрібно розслідувати системний збій. Щоб описати системи централізованого збирання логів, на початку 2000-х виходить RFC 3164який стандартизував remote_syslog. Так виник ще один важливий компонент: колектор (складальник) логів та їх сховище.
Зі збільшенням обсягу логів та повсюдним впровадженням веб-технологій постало питання про те, що логи потрібно зручно показати користувачам. На зміну простим консольним інструментам (awk/sed/grep) прийшли більш просунуті переглядачі логів - Третій компонент.
У зв'язку зі збільшенням обсягу ліг стало ясно й інше: логи потрібні, але не всі. А ще різні логи вимагають різного рівня безпеки: одні можна втратити через день, а інші треба зберігати 5 років. Так до системи логування додався компонент фільтрації та маршрутизації потоків даних - назвемо його фільтром.
Сховища теж зробили серйозний стрибок: зі звичайних файлів перейшли на реляційні бази даних, та був і документоориентированные сховища (наприклад, Elasticsearch). Так від колектора відокремилося сховище.
Зрештою, саме поняття лога розширилося до абстрактного потоку подій, які ми хочемо зберігати для історії. А точніше — на той випадок, коли потрібно буде провести розслідування або скласти аналітичний звіт…
У результаті, за порівняно невеликий проміжок часу, збір логів розвинувся у важливу підсистему, яку можна назвати одним із підрозділів у Big Data.
Якщо колись звичайних print'ів могло бути достатньо «системи логування», то тепер ситуація сильно змінилася.
Kubernetes та логи
Коли в інфраструктуру прийшов Kubernetes, проблема збору логів, що існувала і без того, не обійшла стороною і його. У певному сенсі вона стала навіть болючішою: управління інфраструктурною платформою було не тільки спрощене, а й одночасно ускладнене. Багато старих сервісів розпочали міграцію на мікросервісні рейки. У контексті логів це виявилося у зростаючій кількості джерел логів, їх особливому життєвому циклі, і необхідності відстежувати через логи взаємозв'язку всіх компонентів системи.
Забігаючи вперед, можу констатувати, що зараз, на жаль, немає стандартизованого варіанта логування для Kubernetes, який вигідно відрізнявся б від усіх інших. Найбільш популярні у співтоваристві схеми зводяться до наступних:
Однак не зупинятимуся на інструкціях щодо їх встановлення та конфігурації. Натомість, сфокусуюсь на їхніх недоліках і більш глобальних висновках щодо ситуації з логами в цілому.
Практика з логами у K8s
«Повсякденні логи», скільки ж вас?
Централізований збір логів із досить великої інфраструктури вимагає чималих ресурсів, які підуть на збирання, зберігання та обробку логів. У ході експлуатації різних проектів ми зіткнулися з різними вимогами і проблемами експлуатації, що виникають через них.
Спробуємо ClickHouse
Давайте розглянемо централізоване сховище на проекті із додатком, який досить активно генерує логи: понад 5000 рядків на секунду. Почнемо роботу з його логами, складаючи їх у ClickHouse.
Як тільки буде потрібний максимальний realtime, 4-ядерний сервер з ClickHouse вже буде перевантажений по дисковій підсистемі:
Подібний тип завантаження пов'язаний з тим, що ми намагаємося максимально швидко писати ClickHouse. І на це БД реагує підвищеним дисковим навантаженням, через що може видавати такі помилки:
DB::Exception: Too many parts (300). Merges are processing significantly slower than inserts
Справа в тому, що MergeTree-таблиці У ClickHouse (в них лежать дані логів) мають свої складнощі під час операцій запису. Дані, що вставляються в них, генерують тимчасову партицію, яка потім зливається з основною таблицею. В результаті запис виходить дуже вимогливим до диска, а також на нього поширюється обмеження, повідомлення про яке ми і отримали вище: в 1 секунду можуть зливатися не більше 300 субпартицій (фактично це 300 insert'ів в секунду).
Щоб уникнути подібної поведінки, слід писати в ClickHouse якомога більшими шматками і не частіше 1 разу на 2 секунди. Однак запис великими пачками передбачає, що ми повинні рідше писати в ClickHouse. Це, у свою чергу, може призвести до переповнення буфера та втрати логів. Рішення збільшити буфер Fluentd, але тоді збільшиться і споживання пам'яті.
Примітка: Інша проблемна сторона нашого рішення з ClickHouse була пов'язана з тим, що партикування в нашому випадку (loghouse) реалізовано через зовнішні таблиці, пов'язані Merge-таблицею. Це призводить до того, що при вибірці великих часових інтервалів потрібна зайва оперативна пам'ять, оскільки метатаблиця перебирає всі партиції — навіть ті, які не містять потрібні дані. Втім, зараз такий підхід можна оголосити застарілим для актуальних версій ClickHouse (c 18.16).
У результаті стає зрозуміло, що для збирання логів у реальному часі в ClickHouse вистачить ресурсів далеко не кожного проекту (точніше, їх розподіл не буде доцільним). Крім того, потрібно використовувати акумулятор, До якого ми ще повернемося. Випад, що описується вище, — реальний. І на той момент ми не змогли запропонувати надійне та стабільне рішення, яке влаштовувало б замовника і дозволило б збирати логи з мінімальною затримкою…
А Elasticsearch?
Відомо, що Elasticsearch справляється з великими навантаженнями. Спробуємо його у тому ж проекті. Тепер навантаження виглядає так:
Elasticsearch зумів перетравити потік даних, проте запис подібних обсягів у нього сильно утилізує CPU. Це вирішується організацією кластера. Чисто технічно це не проблема, однак вийде, що тільки для роботи системи збирання логів ми вже використовуємо близько 8 ядер і маємо додатковий високонавантажений компонент у системі.
Підсумок: такий варіант може бути виправданий, але тільки в тому випадку, якщо проект великий та його керівництво готове витратити помітні ресурси на систему централізованого логування.
Тоді виникає закономірне питання:
Які логи справді потрібні?
Спробуємо змінити сам підхід: логи мають одночасно бути інформативними, і не покривати кожне подія у системі.
Припустимо, у нас є процвітаючий інтернет-магазин. Які логі важливі? Збирати максимум інформації, наприклад, із платіжного шлюзу — чудова ідея. А ось від сервісу нарізки зображень у каталозі продуктів нам критичні не всі логи: вистачить лише помилок та розширеного моніторингу (наприклад, на відсоток 500 помилок, які генерує цей компонент).
Ось ми й дійшли того, що централізоване логування виправдане далеко не завжди. Дуже часто клієнт хоче зібрати всі логи в одному місці, хоча насправді з усього лога потрібно лише умовні 5% повідомлень, які є критичними для бізнесу:
Іноді досить налаштувати, скажімо, лише розмір лога контейнера та збирач помилок (наприклад, Sentry).
Для розслідування інцидентів найчастіше може вистачити оповіщення про помилку та власне великого локального лога.
У нас були проекти, які обходилися виключно функціональними тестами і системами збору помилок. Розробнику не були потрібні логи як такі - вони все бачили по трейс помилок.
Ілюстрація з життя
Хорошим прикладом може бути інша історія. До нас надійшов запит від команди безпечників одного з клієнтів, у якого вже використовувалося комерційне рішення, розроблене задовго до впровадження Kubernetes.
Потрібно було «подружити» систему централізованого збирання логів з корпоративним сенсором виявлення проблем — QRadar. Ця система вміє приймати логи за протоколом Syslog, забирати з FTP. Однак інтегрувати її з плагіном remote_syslog для fluentd відразу не вдалося (як виявилося, ми не одні такі). Проблеми з налаштуванням QRadar опинилися на стороні команди клієнтів.
В результаті частина логів, критичних для бізнесу, вивантажувалася на FTP QRadar, а інша частина — перенаправлялася через remote syslog безпосередньо з вузлів. Для цього ми навіть написали простий chart — можливо, він допоможе комусь вирішити аналогічне завдання... Завдяки схемі, що вийшла, сам клієнт отримував і аналізував критичні логи (за допомогою свого улюбленого інструментарію), а ми змогли знизити витрати на систему логування, зберігаючи лише останній місяць.
Ще один приклад досить показовий у тому, як робити не слід. Один із наших клієнтів на обробку кожного події, що надходить від користувача, робив багаторядковий неструктурований висновок інформації у балку. Як легко здогадатися, подібні логи було вкрай незручно читати і зберігати.
Критерії для логів
Подібні приклади підводять до висновку, що крім вибору системи збирання логів треба спроектувати ще й самі логи! Які тут вимоги?
Логи повинні бути в машиночитаному форматі (наприклад, JSON).
Логи мають бути компактними та з можливістю зміни ступеня логування, щоб налагодити можливі проблеми. При цьому в production-оточеннях слід запускати системи з рівнем логування на кшталт попередження або помилка.
Логи повинні бути нормалізованими, тобто в об'єкті лога усі рядки повинні мати однаковий тип поля.
Неструктуровані логи можуть призвести до проблем із завантаженням логів у сховище та повною зупинкою їх обробки. Як ілюстрація - приклад з помилкою 400, з якою багато хто точно стикався в логах fluentd:
2019-10-29 13:10:43 +0000 [warn]: dump an error event: error_class=Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchError error="400 - Rejected by Elasticsearch"
Помилка означає, що ви відправляєте в індекс із готовим mapping'ом поле, тип якого нестабільний. Найпростіший приклад - поле в лозі nginx зі змінною $upstream_status. У ньому може бути як число, і рядок. Наприклад:
У логах видно, що сервер 10.100.0.10 відповів 404 помилкою і запит пішов на інше сховище контенту. У результаті в логах значення стало таким:
"upstream_response_time": "0.001, 0.007"
Ця ситуація настільки поширена, що удостоїлася навіть окремого згадки у документації.
А що з надійністю?
Бувають випадки, коли життєво потрібні всі логи без винятку. І з цим типові схеми збору логів для K8s, запропонованих/розглядаються вище, мають проблеми.
Наприклад, fluentd не може зібрати логи з короткоживучих контейнерів. В одному з наших проектів контейнер з міграцією баз даних жив менше 4-х секунд, а потім видалявся відповідно до відповідної анотації:
"helm.sh/hook-delete-policy": hook-succeeded
Через це лог виконання міграції не потрапляв у сховище. Допомогти в цьому випадку може політика before-hook-creation.
Інший приклад – ротація логів Docker. Припустимо, є програма, яка активно пише в логи. У звичайних умовах ми встигаємо обробити всі логи, але як тільки з'являється проблема - наприклад, як була описана вище з неправильним форматом, обробка зупиняється, а Docker ротує файл. Підсумок можуть бути втрачені критичні для бізнесу логи.
Саме тому важливо розділяти потоки логів, вбудовуючи відправку найбільш цінних безпосередньо в додаток, щоб забезпечити їх збереження. Крім того, не буде зайвим створення якогось «акумулятора» логів, який може пережити коротку недоступність сховища при збереженні критичних повідомлень.
Зрештою, не треба забувати, що будь-яку підсистему важливо якісно моніторити. Інакше легко зіткнутися з ситуацією, в якій fluentd перебуває в стані CrashLoopBackOff і нічого не відправляє, а це обіцяє втратою важливої інформації.
Висновки
У цій статті ми не розглядаємо SaaS-рішення на кшталт Datadog. Багато з описаних тут проблем так чи інакше вже вирішено комерційними компаніями, що спеціалізуються на збиранні логів, але не всі можуть використовувати SaaS з різних причин (основні - це вартість та дотримання 152-ФЗ).
Централізований збір ліг спочатку виглядає простим завданням, але зовсім такою не є. Важливо пам'ятати, що:
Логувати докладно варто лише критичні компоненти, а інших систем можна налаштувати моніторинг і збирання помилок.
Логи у production варто робити мінімальними, щоб не давати зайвого навантаження.
Логи повинні бути машиночитаними, нормалізованими, мати строгий формат.
Справді критичні логи варто відправляти окремим потоком, який має бути відокремлений від основних.
Варто продумати акумулятор логів, який може врятувати від сплесків високого навантаження і зробить навантаження більш рівномірним.
Ці прості правила, якщо їх застосовувати скрізь, дозволили б працювати і описаним вище схемам - навіть незважаючи на те, що в них не вистачає важливих компонентів (акумулятора). Якщо ж не дотримуватись таких принципів, завдання з легкістю приведе вас та інфраструктуру до ще одного високонавантаженого (і водночас малоефективного) компонента системи.