Русский
Русский
English
Статистика
Реклама

Логи

Использование journalctl для просмотра и анализа логов подробный гайд

19.12.2020 16:19:22 | Автор: admin


Journalctl отличный инструмент для анализа логов, обычно один из первых с которым знакомятся начинающие администраторы linux систем. Встроенные возможности ротации, богатые возможности фильтрации и возможность просматривать логи всех systemd unit-сервисов одним инструментом очень удобны и заметно облегчают работу системным администраторам.

Эта статья рассматривает основные возможности утилиты journalctl и различные варианты ее применения. С помощью journalctl можно просматривать логи системы, чтобы решить возникшие проблемы на рабочей станции или сервере использующие дистрибутив linux с демоном инициализации systemd, де-факто уже ставшим стандартом в современных Linux-системах, например: RHEL, CentOS, Fedora, Debian и многих других.

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

Systemd


Systemd состоит из трех основных компонентов:

  • systemd менеджер системы и сервисов
  • systemctl утилита для просмотра и управление статусом сервисов
  • systemd-analyze предоставляет статистику по процессу загрузки системы, проверяет корректность unit-файлов и так же имеет возможности отладки systemd


Journald


Journald системный демон журналов systemd. Systemd спроектирован так, чтобы централизованно управлять системными логами от процессов, приложений и т.д. Все такие события обрабатываются демоном journald, он собирает логи со всей системы и сохраняет их в бинарных файлах.

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

Файлы логов journald могут собирать тысячи событий и они обновляются с каждым новым событием, поэтому если ваша Linux-система работает достаточно долго размер файлов с логами может достигать несколько гигабайт и более. Поэтому анализ таких логов может происходить с задержками, в таком случае, при анализе логов можно фильтровать вывод, чтобы ускорить работу.

Файл конфигурации journald


Файл конфигурации можно найти по следующему пути: /etc/systemd/journald.conf, он содержит различные настройки journald, я бы не рекомендовал изменять этот файл, если вы точно не уверены в том, что делаете.

Каталог с журналом journald располагается /run/log/journal (в том случае, если не настроено постоянное хранение журналов, но об этом позже).
Файлы хранятся в бинарном формате, поэтому нормально их просмотреть с помощью cat или nano, как привыкли многие администраторы не получится.

Использование journalctl для просмотра и анализа логов


Основная команда для просмотра:

# journalctl



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

По умолчанию journalctl выводит время событий согласно настроенного в вашей системе часового пояса, также journalctl позволяет просмотреть логи с временем UTC (в этом стандарте времени сохраняются события внутри файлов journald), для этого можно использовать команду:

# journalctl --utc

Фильтрация событий по важности


Система записывает события с различными уровнями важности, какие-то события могут быть предупреждением, которое можно проигнорировать, какие-то могут быть критическими ошибками. Если мы хотим просмотреть только ошибки, игнорируя другие сообщения, введем команду с указанием кода важности:
# journalctl -p 0

Для уровней важности, приняты следующие обозначения:
  • 0: emergency (неработоспособность системы)
  • 1: alerts (предупреждения, требующие немедленного вмешательства)
  • 2: critical (критическое состояние)
  • 3: errors (ошибки)
  • 4: warning (предупреждения)
  • 5: notice (уведомления)
  • 6: info (информационные сообщения)
  • 7: debug (отладочные сообщения)


Когда вы указываете код важности, journalctl выведет все сообщения с этим кодом и выше. Например если мы укажем опцию -p 2, journalctl покажет все сообщения с уровнями 2, 1 и 0.

Настройка хранения журналов


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

Когда в конфигурационном файле /etc/systemd/journald.conf параметру Storage= задано значение auto) и каталога /var/log/journal/ не существует, журнал будет записываться в /run/log/journal без сохранения между перезагрузками, если /var/log/journal/ существует, журналы будут сохраняться в нем, на постоянной основе, но если каталог будет удален, systemd не пересоздаст его автоматически и вместо этого будет вести журнал снова в /run/systemd/journal без сохранения. Каталог может быть пересоздан в таком случае, если добавить Storage=persistent в journald.conf и перезапустить systemd-journald.service (или перезагрузиться).

Создадим каталог для хранения журналов, установим необходимые атрибуты и перезапустим службу:

# mkdir /var/log/journal# systemd-tmpfiles --create --prefix /var/log/journal# systemctl restart systemd-journald

Просмотр журналов загрузки


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

# journalctl --list-boots



Первый номер показывает номер журнала, который можно использовать для просмотра журнала определенной сессии. Второй номер boot ID так же можно использовать для просмотра отдельного журнала.

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

Например, чтобы просмотреть журнал начиная с текущего старта системы, можно использовать команду:

# journalctl -b 0

А для того, чтобы просмотреть журнал предыдущей загрузки:

# journalctl -b -1

Просмотр журнала за определенный период времени


Journalctl позволяет использовать такие служебные слова как yesterday (вчера), today (сегодня), tomorrow (завтра), или now (сейчас).
Поэтому мы можем использовать опцию "--since" (с начала какого периода выводить журнал).

С определенной даты и времени:

# journalctl --since "2020-12-18 06:00:00"

С определенной даты и по определенное дату и время:

# journalctl --since "2020-12-17" --until "2020-12-18 10:00:00

Со вчерашнего дня:

# journalctl --since yesterday

С 9 утра и до момента, час назад:

# journalctl --since 09:00 --until "1 hour ago"

Просмотр сообщений ядра


Чтобы просмотреть сообщения от ядра Linux за текущую загрузку, используйте команду с ключом -k:

# journalctl -k



Просмотр журнала логов для определенного сервиса systemd или приложения


Вы можете отфильтровать логи по определенному сервису systemd. Например, что бы просмотреть логи от NetworkManager, можно использовать следующую команду:



# journalctl -u NetworkManager.service


Если нужно найти название сервиса, используйте команду:

# systemctl list-units --type=service

Так же можно просмотреть лог приложения, указав его исполняемый файл, например чтобы просмотреть все сообщения от nginx за сегодня, мы можем использовать команду:

# journalctl /usr/sbin/nginx --since today

Или указав конкретный PID:

# journalctl _PID=1

Дополнительные опции просмотра


Следить за появлением новых сообщений (аналог tail -f):

# journalctl -f

Открыть журнал перемотав его к последней записи:

# journalctl -e

Если в каталоге с журналами очень много данных, то фильтрация вывода journalctl может занять некоторое время, процесс можно значительно ускорить с помощью опции --file, указав journalctl только нужный нам журнал, за которым мы хотим следить:

journalctl --file /var/log/journal/e02689e50bc240f0bb545dd5940ac213/system.journal -f

По умолчанию journalctl отсекает части строк, которые не вписываются в экран по ширине, хотя иногда перенос строк может оказаться более предпочтительным. Управление этой возможностью производится посредством переменной окружения SYSTEMD_LESS, в которой содержатся опции, передаваемые в less (программу постраничного просмотра, используемую по умолчанию). По умолчанию переменная имеет значение FRSXMK, если убрать опцию S, строки не будут обрезаться.

Например:

SYSTEMD_LESS=FRXMK journalctl

Ограничение размера журнала


Если journald настроен что бы сохранять журналы после перезагрузки, то по умолчанию размер журнала ограничен 10% от объема файлового раздела и максимально может занять 4 Гб дискового пространства.
Максимальный объем журнала можно скорректировать, раскомментировав и отредактировав следующий параметр в файле конфигурации journald:

SystemMaxUse=50M

Удаление журналов


Удалить файлы архивных журналов, можно вручную с помощью rm или использовав journalctl.

Удалить журналы, оставив только последние 100 Мб:

# journalctl --vacuum-size=100M

Удалить журналы, оставив журналы только за последние 7 дней:

# journalctl --vacuum-time=7d

Заключение


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



Подробнее..

Работа с событиями аудита Windows сбор, анализ, реагирование

18.09.2020 20:23:44 | Автор: admin

Уважаемые друзья, в предыдущих публикациях мы говорили об основах информационной безопасности, законодательстве по защите персональных данных и критической информационной инфраструктуры, безопасности в кредитно-финансовой сфере, а также провели анализ основных стандартов по управлению рисками информационной безопасности и обсудили системы класса IRP, предназначенные для автоматизации реагирования на инциденты ИБ. Как мы знаем, при обработке инцидентов детальный анализ событий безопасности с устройств является одним из ключевых этапов. В данной публикации мы рассмотрим настройку подсистемы аудита ОС Windows, принципы анализа и централизованного сбора журналов аудита с Windows-устройств и их пересылку в SIEM-систему IBM QRadar, а также покажем, как можно с помощью штатных средств Windows и утилиты Sysmon настроить простейшую систему реагирования на инциденты ИБ. Вперед!

Для решения задачи обработки инцидентов ИБ логично рассуждать, что чем больше данных (логов, событий безопасности) мы собираем, храним и анализируем, тем проще нам будет в дальнейшем не только оперативно среагировать на инцидент, но и расследовать обстоятельства произошедших атак для поиска причин их возникновения. При этом большое количество данных для обработки имеет и очевидный минус: нас может просто засыпать сообщениями, алертами, уведомлениями, поэтому необходимо выбрать самые значимые с точки зрения ИБ события и настроить соответствующие политики аудита. Microsoft предлагает использовать бесплатный набор утилит и рекомендаций (Baselines) в своем наборе Microsoft Security Compliance Toolkit, в котором в том числе приведены и рекомендуемые настройки аудита для контроллеров домена, рядовых серверов и рабочих станций. Кроме рекомендаций вендора можно обратиться еще к документам CIS Microsoft Windows Server Benchmark и CIS Microsoft Windows Desktop Benchmark, в которых, в числе прочего, указаны рекомендуемые экспертами политики аудита для, соответственно, серверных и десктопных версий ОС Windows. Однако зачастую выполнение абсолютно всех рекомендаций неэффективно именно по причине потенциального появления большого количества шумящих, малозначительных с точки зрения ИБ событий, поэтому в настоящей статье мы сначала приведем список наиболее полезных и эффективных (с нашей точки зрения) политик аудита безопасности и соответствующих типов событий безопасности ОС Windows.

Напомню, что в ОС Microsoft Windows, начиная с Microsoft Windows Server 2008 и Vista, используется достаточно продвинутая система аудита, настраиваемая при помощи конфигурирования расширенных политик аудита (Advanced Audit Policy Configuration). Не стоит забывать о том, что как только на устройствах будут включены политики расширенного аудита, по умолчанию старые классические политики аудита перестанут быть эффективными, хотя данное поведение может быть переопределено в групповой политике Аудит: принудительно переопределяет параметры категории политики аудита параметрами подкатегории политики аудита (Windows Vista или следующие версии)) (Audit: Force audit policy subcategory settings (Windows Vista or later) to override audit policy category settings).

Политики аудита Windows

Пройдем последовательно по настройкам, эффективным для решения задач аудита ИБ и выработки целостной политики аудита безопасности.

Категория аудита

Подкатегория аудита

События аудита

EventID

Комментарии

Вход учетной записи

Аудит проверки учетных данных

Успех, Отказ

4776

Целесообразно контролировать на домен-контроллерах при использовании NTLM-аутентификации.

Аудит службы проверки подлинности Kerberos

Успех, Отказ

4771

Неуспешная аутентификация учетной записи на контроллере домена с использованием Kerberos-аутентификации.

4768

Запрос билета Kerberos, при этом следует анализировать коды ответа сервера.

Примечание:

Данный тип аудита следует включать на контроллерах домена, при этом для детального изучения попыток подключения и получения IP-адреса подключающегося устройства на контроллере домена следует выполнить команду nltest /dbflag:2080ffff и проводить аудит текстового лог-файла %windir%\debug\netlogon.log

Управление учетными записями

Аудит управления учетными записями компьютеров

Успех

4741

Заведение устройства в домен Active Directory; может использоваться злоумышленниками, поскольку любой пользователь домена по умолчанию может завести в домен 10 устройств, на которых может быть установлено неконтролируемое компанией ПО, в том числе вредоносное.

Аудит управления группами безопасности

Успех, Отказ

4728

Добавление члена глобальной группы.

4732

Добавление члена локальной группы.

4756

Добавление члена универсальной группы.

Аудит управления учетными записями пользователей

Успех, Отказ

4720

Создание учетной записи.

4725

Отключение учетной записи.

4740

Блокировка учетной записи.

4723

Смена пароля.

4724

Сброс пароля.

Подробное отслеживание

Аудит создания процессов

Успех

4688

При создании процесса.

4689

При завершении процесса.

Примечание:

Чтобы для командного интерпретатора велась запись введенных команд, следует включить политику Конфигурация компьютера - Конфигурация Windows - Административные шаблоны - Система - Аудит создания процессов -> Включать командную строку в события создания процессов.

Примечание:

Чтобы велась запись выполняемых PowerShell-команд и загруженных PowerShell-модулей, следует включить в каталоге Конфигурация компьютера - Конфигурация Windows - Административные шаблоны - Компоненты Windows - Windows PowerShell политики Включить ведение журнала модулей (в настройках политики указать все модули символом *) и Включить регистрацию блоков сценариев PowerShell (в настройках политики отметить check-box Регистрация начала или остановки вызова блоков сценариев). Работа PowerShell-скриптов регистрируется с EventID=4104,4105,4106 в журнале Microsoft-Windows-PowerShell/Operational, а загрузка PowerShell-модулей регистрируется с EventID=800 в журнале Windows PowerShell.

Вход/выход

Аудит выхода из системы

Успех

4634

Для неинтерактивных сессий.

4647

Для интерактивных сессий и RDP-подключений.

Примечание:

При этом следует обращать внимание на код Logon Type, который показывает тип подключения (интерактивное, сетевое, с закэшированными учетными данными, с предоставлением учетных данных в открытом виде и т.д.).

Аудит входа в систему

Успех, Отказ

4624

При успешной попытке аутентификации, создается на локальном ПК и на домен-контроллере при использовании NTLM и Kerberos-аутентификации.

4625

При неуспешной попытке аутентификации, создается на локальном ПК и на домен-контроллере при использовании NTLM аутентификации; при Kerberos-аутентификации на контроллере домена создается EventID=4771.

4648

При попытке входа с явным указанием учетных данных, например, при выполнении команды runas, а также при работе хакерской утилиты Mimikatz.

Примечание:

При этом следует обращать внимание на код входа (Logon Type), который показывает тип подключения (интерактивное, сетевое, с закэшированными учетными данными, с предоставлением учетных данных в открытом виде и т.д.). Целесообразно также обращать внимание на код ошибки (Status/SubStatus), который также сохраняется в событии аудита и характеризует причину неуспешного входа - несуществующее имя учетной записи, недействительный пароль, попытка входа с заблокированной учетной записью и т.д.

Аудит других событий входа и выхода

Успех, Отказ

4778

RDP-подключение было установлено.

4779

RDP-подключение было разорвано.

Аудит специального входа

Успех

4672

При входе с административными полномочиями.

Доступ к объектам

Аудит сведений об общем файловом ресурсе

Успех, Отказ

5145

При доступе к системных сетевым ресурсам, таким как \\C$\ .

Данное событие будет создаваться при работе ransomware, нацеленного на горизонтальное перемещение по сети.

Аудит других событий доступа к объектам

Успех, Отказ

4698

При создании задания в Планировщике задач, что часто используется злоумышленниками как метод закрепления и скрытия активности в атакованной системе.

Изменение политики

Аудит изменения политики аудита

Успех

4719

Изменение политики аудита.

4906

Изменение настройки CrashOnAuditFail.

Примечание:

Изменить реакцию ОС на невозможность вести журнал аудита безопасности (настройка CrashOnAuditFail) можно в каталоге Конфигурация компьютера - Конфигурация Windows - Параметры безопасности - Локальные политики - Параметры безопасности в политике Аудит: немедленное отключение системы, если невозможно внести в журнал записи об аудите безопасности.

Система

Аудит расширения системы безопасности

Успех

4610

4614

4622

При появлении в системе новых пакетов аутентификации, что не должно происходить несанкционированно.

4697

При создании нового сервиса, что часто используется злоумышленниками как метод закрепления и скрытия активности в атакованной системе.

Кроме описанных выше настроек, имеет смысл также контролировать появление в журнале безопасности события с EventID=1102, которое формируется сразу после очистки журнала безопасности, что может говорить о вредоносной активности. Более того, разумно будет включить в каталоге Конфигурация компьютера - Конфигурация Windows - Параметры безопасности - Локальные политики - Параметры безопасности политику Сетевая безопасность: ограничения NTLM: исходящий трафик NTLM к удаленным серверам в значение Аудит всего. После этого EventID=8001 в журнале Microsoft-Windows-NTLM/Operational будет содержать информацию об автоматической аутентификации на веб-ресурсах с учетной записью пользователя. Следующим шагом станет allow list с перечнем веб-ресурсов, которые легитимно могут запрашивать учетные записи, а указанную политику можно будет перевести в режим блокировки. Это не позволит вредоносным ресурсам получать NTLM-хэши пользователей, которые кликнули на ссылку из фишингового письма.

Обратим внимание и на то, что подсистема журналирования Windows весьма гибка и позволяет настроить аудит произвольных папок и веток реестра - следует лишь выбрать критичные для ИТ-инфраструктуры объекты аудита и включить данные опции.

Настройка Windows Event Forwarding, интеграция с IBM QRadar

Настроив необходимые параметры аудита, перейдем к решению вопроса автоматизации сбора журналов аудита и их централизованного хранения и анализа. Штатный механизм Windows Event Forwarding, который работает из коробки с Microsoft Windows Server 2008 / Vista и старше, позволяет осуществлять централизованный сбор журналов аудита на устройстве-коллекторе (не ниже Windows Server 2008 и Vista, но все же рекомендуется использовать выделенный Windows Server 2012R2 и старше) с устройств-источников с применением функционала WinRM (Windows Remote Management, использует протокол WS-Management) и использованием т.н. подписок на определенные события (набор XPath-выражений, о которых мы поговорим далее, для выбора интересующих журналов и событий на источнике). События с удаленных устройств могут быть как запрошены коллектором (режим Pull/Collector initiated), так и отправлены самим источником (режим Push/Source computer initiated). Мы рекомендуем использовать последний режим, поскольку в режиме Push служба WinRM слушает входящие соединения только на коллекторе, а на клиентах-источниках WinRM не находится в режиме прослушивания и лишь периодически обращается к коллектору за инструкциями, что уменьшает поверхность потенциальной атаки на конечные устройства. По умолчанию для шифрования трафика от источников к коллектору, принадлежащих одному Windows-домену, используется Керберос-шифрование SOAP-данных, передаваемых через WinRM (режим HTTP-Kerberos-session-encrypted), при этом HTTP-заголовки и соответствующие метаданные передаются в открытом виде. Другой опцией является использование HTTPS с установкой SSL-сертификатов на приемнике и источнике, при этом они могут не принадлежать одному домену. При дальнейшем изложении будем считать, что мы работаем в одном домене и используем настройку по умолчанию.

Рассмотрев концепцию пересылки логов с Windows-устройств, перейдем непосредственно к настройке нашей связки: источник событий -> сервер-коллектор -> утилита IBM WinCollect -> SIEM-система IBM QRadar.

Для включения сервиса сбора логов следует выполнить нижеописанные шаги:

1. На сервере-коллекторе выполнить команду winrm qc, ответить согласием на оба последующих вопроса (включение службы WinRM и прослушивание порта TCP:5985 для входящих соединений от источников). Следует учесть, что выполнение команды winrm qc одновременно включает Windows Remote Shell (WinRS) и разрешает принимать входящие соединения для удаленного управления через функционал WinRS. Отключить WinRS можно либо через политику Конфигурация компьютера / Административные шаблоны / Компоненты Windows / Удаленная оболочка Windows / Разрешить доступ к удаленной оболочке -> Запретить (Computer Configuration / Administrative Templates / Windows Components / Windows Remote Shell / Allow Remote Shell Access -> Disabled), либо командой winrm set winrm/config/winrs @{AllowRemoteShellAccess="false"}

2. На сервере-коллекторе выполнить команду wecutil qc, согласиться на включение службы Сборщик событий Windows (Windows Event Collector). При этом в Windows Firewall создается разрешающее правило для входящих соединений на коллектор по TCP:5985.

3. На источниках событий следует включить службу WinRM: установить Тип запуска в значение Автостарт и запустить Службу удаленного управления Windows (Windows Remote Management (WS-Management)).

4. Проверить состояние службы WinRM на сервере-колекторе можно командой winrm enumerate winrm/config/listener, в результате выполнения которой отобразятся настройки порта и список локальных IP-адресов, на которых прослушиваются соединения по TCP:5985. Команда winrm get winrm/config покажет подробные настройки службы WinRM. Переконфигурировать настройки можно либо непосредственно через утилиту winrm, либо через групповые политики по пути Конфигурация компьютера / Административные шаблоны / Компоненты Windows / Удаленное управление Windows (Computer Configuration / Administrative Templates / Windows Components / Windows Remote Management).

5. На источниках событий требуется предоставить доступ к журналам аудита службе WinRM путем включения встроенной учетной записи NT AUTHORITY\NETWORK SERVICE (SID S-1-5-20) в локальную группу BUILTIN\Event Log Readers (Читатели журнала событий). После этого необходимо перезапустить Службу удаленного управления Windows (WinRM) и службу Журнал событий Windows (EventLog).

6. Затем следует создать и применить конфигурацию групповой политики для источников, в которой будет указана конфигурация и адрес сервера-коллектора. Требуется включить политику Конфигурация компьютера / Административные шаблоны / Компоненты Windows / Пересылка событий / Настроить адрес сервера... (Computer Configuration / Administrative Templates / Windows Components / Event Forwarding / Configure the server address...) и указать адрес сервера-коллектора в следующем формате:

Server=http://servername.domain.local:5985/wsman/SubscriptionManager/WEC,Refresh=60

где 60 частота обращения (в секундах) клиентов к серверу за новыми инструкциями по пересылке журналов. После применения данной настройки на устройствах-источниках следует сделать перезапуск службы WinRM.

7. Далее создаем и применяем конфигурацию подписки на сервере-коллекторе: открываем оснастку управления журналами аудита (eventvwr.msc) и находим внизу раздел Подписки (Subscriptions). Нажимаем правой кнопкой мыши и выбираем Создать подписку, задаем имя подписки. Далее выбираем опцию Инициировано исходным компьютером (Source Computer Initiated, это означает предпочтительный режим Push). Нажимаем на кнопку Выбрать группы компьютеров (Select Computer Groups), выбираем из Active Directory те устройства или их группы, которые должны будут присылать логи на коллектор. Далее, нажимаем Выбрать события (Select Events) и вводим XPath-запрос (пример для сбора журналов Security):

<QueryList>  <Query Id="0" Path="Security">    <Select Path="Security">*</Select>  </Query></QueryList>

8. В итоге, клиенты должны иметь активные сетевые соединения по TCP:5985 с сервером-коллектором. На сервере-коллекторе в eventvwr.msc в свойствах Подписки можно будет увидеть список клиентов-источников, а пересланные события будут находиться в разделе Журналы Windows Перенаправленные события (Windows Logs Forwarded Events) на сервере-коллекторе.

9. Далее решаем задачу пересылки собранных на сервере-коллекторе логов с источников в SIEM систему IBM QRadar. Для этого нам потребуется установить на сервере-коллекторе утилиту IBM WinCollect.

Рекомендуем использовать управляемый (Managed) режим работы WinCollect для упрощения его администрирования. Для того, чтобы отправляемые через WinCollect агрегированные события корректно обрабатывались в IBM QRadar, нам следует воспользоваться рекомендациями IBM и на сервере-коллекторе с установленной утилитой WinCollect перевести формат пересылаемых событий в RenderedText, а также сменить их локаль на EN-US командой wecutil ss SubscriptionName /cf:RenderedText /l:en-US (где SubscriptionName - имя подписки, заданное в п.7 выше). Кроме того, необходимо обеспечить сетевую доступность между сервером-коллектором с установленным WinCollect и нодами IBM QRadar по TCP:8413 и TCP/UDP:514.

10. После установки утилиты WinCollect на сервер-коллектор, в самой SIEM-системе IBM QRadar нужно будет добавить этот сервер в список источников (тип источника Microsoft Security Event Log, в поле Target Destination в выпадающем списке лучше выбрать вариант с TCP-syslog-подключением, отметить check-box Forwarded Events).

После применения указанных настроек новые события и устройства-источники, пересылающие Windows-логи на сервер-коллектор, появятся в консоли IBM QRadar автоматически. В итоге, после внедрения SIEM-системы данные в ней и регистрацию событий информационной безопасности можно будет легко обогатить журналами аудита Windows, собранными описанным способом с различных устройств в инфраструктуре компании.

Утилита Sysmon

Кроме задействования штатного функционала подсистемы журналирования, можно воспользоваться и официальной бесплатной утилитой Sysmon из пакета Microsoft Windows Sysinternals, которая существенно расширяет и дополняет возможности мониторинга ОС. Данная утилита дает возможность проводить аудит создания файлов, ключей реестра, процессов и потоков, а также осуществлять мониторинг загрузки драйверов и библиотек, сетевых подключений, WMI-событий и именованных каналов. Из особо полезных функций отметим возможность утилиты показывать родительский процесс и командную строку процесса, отображать значение хэш-сумм при событиях создания процесса и загрузки драйверов и библиотек с указанием наличия и действительности цифровой подписи. Несложным путем можно автоматизировать сравнение полученных хэш-сумм с индикаторами компрометации (IoCs, Indicator of Compromise) из данных фидов CyberThreat Intelligence, а также использовать приложение QVTI для IBM QRadar, с помощью которого хэши запускаемых файлов автоматически проверяются через сервис VirusTotal. Еще одной приятной опцией является возможность создания XML-конфигураций, в которых можно предельно четко указать объекты контроля и настройки работы Sysmon. Одними из наиболее продвинутых и детальных вариантов XML-конфигураций, с нашей точки зрения, являются конфиги https://github.com/ion-storm/sysmon-config и https://github.com/SwiftOnSecurity/sysmon-config .

Установка Sysmon предельно проста и также может быть легко автоматизирована:

1. Дистрибутив скачивается с https://docs.microsoft.com/en-us/sysinternals/downloads/sysmon

Все исполняемые файлы подписаны.

2. Создается или скачивается по приведенным выше ссылкам xml-файл с конфигурацией Sysmon.

3. Установка sysmon для x64 производится командой:

C:\folder\sysmon64.exe -accepteula -i C:\folder\sysmonconfig-export.xml , где sysmonconfig-export.xml файл конфигурации, sysmon64.exe файл-установщик.

Поддерживается запуск установки из сетевой папки.

4. После установки создается журнал Microsoft-Windows-Sysmon/Operational , размер которого мы сразу рекомендуем увеличить как минимум до 100 Мб.

Перезапуск устройства не требуется, Sysmon работает в виде сервиса, его исполняемый файл находится в C:\Windows\sysmon64.exe . По нашим подсчетам, footprint на конечной системе даже при использовании максимально детального конфига Sysmon не превышает 5-10% ЦПУ и около 100 Мб ОЗУ.

XPath-запросы

Наконец, выполнив необходимые настройки файлов журналов Windows, перейдем непосредственно к поиску интересующей информации. Заметим, что в случае включения всех рекомендованных политик аудита ИБ сами журналы событий становятся достаточно объемными, поэтому поиск по их содержимому может быть медленным (этих недостатков лишены специализированные решения, предназначенные в том числе для быстрого поиска информации - Log Management и SIEM-системы). Отметим также, что по умолчанию не все журналы Windows отображаются к графической оснастке (eventvwr.msc), поэтому в данной оснастке следует перейти в меню Вид и отметить check-box Отобразить аналитический и отладочный журналы.

Итак, поиск по журналам аудита будем осуществлять с помощью встроенного редактора запросов XPath (XPath queries). Открыв интересующий нас журнал, например, журнал безопасности Windows (вкладка Журналы Windows -> Безопасность / Security), нажатием правой кнопки мыши на имени журнала выберем пункт Фильтр текущего журнала. Нам откроется графический редактор поисковых запросов, при этом для наиболее продуктивной работы следует открыть вторую вкладку открывшегося окна с названием XML, отметив внизу check-box Изменить запрос вручную. Нам будет предложено изменить XML-текст (по сути, XPath запрос) в соответствии с нашими критериями поиска.

Результат запроса будет также представляться в различных формах, но для лучшего понимания и получения детального контента в конкретном событии рекомендуем переключиться на вкладку Подробности, а там выбрать radio-button Режим XML, в котором в формате ключ-значение будут представлены данные события безопасности.

Приведем несколько полезных XPath запросов с комментариями.

1. Поиск по имени учетной записи в журнале Security - возьмем для примера имя Username:

<QueryList><Query Id="0" Path="Security"><Select Path="Security">*[EventData[Data[@Name='TargetUserName']='Username']]</Select></Query></QueryList>

2. Поиск по значению конкретного свойства события в журнале Sysmon - возьмем для примера поиск событий, в которых фигурировал целевой порт 443:

<QueryList>  <Query Id="0" Path="Microsoft-Windows-Sysmon/Operational">    <Select Path="Microsoft-Windows-Sysmon/Operational">*[EventData[Data[@Name='DestinationPort'] = '443']]</Select>  </Query></QueryList>

3. Произведем поиск сразу по двум условиям - возьмем для примера событие входа с EventID=4624 и имя пользователя Username:

<QueryList>  <Query Id="0" Path="Security">    <Select Path="Security">*[System[(EventID=4624)]] and *[EventData[Data[@Name='TargetUserName']='Username']]</Select>  </Query></QueryList>

4. Поиск по трем условиям - дополнительно укажем Logon Type = 2, что соответствует интерактивному входу в ОС:

<QueryList>  <Query Id="0" Path="Security">    <Select Path="Security">*[System[(EventID=4624)]] and *[EventData[Data[@Name='TargetUserName']='Username']] and*[EventData[Data[@Name='LogonType']='2']]</Select>  </Query></QueryList>

5. Рассмотрим функционал исключения из выборки данных по определенным критериям - это осуществляется указанием оператора Suppress с условиями исключения. В данном примере мы исключим из результатов поиска по фактам успешного входа (EventID=4624) все события, которые имеют отношения к системным учетным записям (SID S-1-5-18/19/20) с нерелевантным для нас типам входа (Logon Type = 4/5), а также применим функционал задания условий поиска с логическим оператором ИЛИ, указав не интересующие нас имя процесса входа (Advapi) и методы аутентификации (Negotiate и NTLM):

<QueryList>  <Query Id="0" Path="Security">    <Select Path="Security">*[System[(EventID=4624)]]</Select><Suppress Path="Security">*[EventData[(Data[@Name='TargetUserSid'] and (Data='S-1-5-18' or Data='S-1-5-19' or Data='S-1-5-20') and Data[@Name='LogonType'] and (Data='4' or Data='5'))]]or*[EventData[(Data[@Name='LogonProcessName'] and (Data='Advapi') and Data[@Name='AuthenticationPackageName'] and (Data='Negotiate' or Data='NTLM'))]]</Suppress>  </Query></QueryList>

IRP-система штатными средствами Windows

Как мы увидели, встроенный функционал подсистемы журналирования Windows позволяет весьма гибко осуществлять поиск по зафиксированным событиям аудита ИБ, комбинируя различные условия поиска. Однако, у Windows есть еще одна интересная фишка, которая позволяет использовать сформированные описанным выше образом правила поиска событий - мы говорим про создание задач с определенным триггером в Планировщике заданий Windows, что также является штатным функционалом ОС.

Как мы знаем, задачи в ОС Windows могут выполнять совершенно разные функции, от запуска диагностических и системных утилит до обновления компонент прикладного ПО. В задаче можно не только указать исполняемый файл, который будет запущен при наступлении определенных условий и триггеров, но и задать пользовательский PowerShell/VBS/Batch-скрипт, который также будет передан на обработку. В контексте применения подсистемы журналирования интерес для нас представляет функционал гибкой настройки триггеров выполнения задач. Открыв Планировщик заданий (taskschd.msc), мы можем создать новую задачу, в свойствах которой на вкладке Триггеры мы увидим возможность создать свой триггер. При нажатии на кнопку Создать откроется новое окно, в котором в drop-down списке следует выбрать вариант При событии, а в открывшейся форме отображения установить radio-button Настраиваемое. После этих действий появится кнопка Создать фильтр события, нажав на которую, мы увидим знакомое меню фильтрации событий, на вкладке XML в котором мы сможем задать произвольное поисковое условие в синтаксисе XPath-запроса.

Например, если мы хотим выполнять некоторую команду или скрипт при каждом интерактивном входе в систему пользователя Username, мы можем задать в качестве триггера задачи следующее поисковое выражение, уже знакомое нам по примеру выше:

<QueryList>  <Query Id="0" Path="Security">    <Select Path="Security">*[System[(EventID=4624)]] and *[EventData[Data[@Name='TargetUserName']='Username']] and *[EventData[Data[@Name='LogonType']='2']]</Select>  </Query></QueryList>

Другой пример: оповещение администратора при подозрительном обращении к системному процессу lsass.exe, который хранит в своей памяти NTLM-хэши и Керберос-билеты пользователей Windows, что может говорить об использовании утилиты Mimikatz или аналогичных ей:

<QueryList>  <Query Id="0" Path="Microsoft-Windows-Sysmon/Operational">    <Select Path="Microsoft-Windows-Sysmon/Operational">*[System[(EventID=10)]] and *[EventData[Data[@Name='TargetImage']='C:\Windows\System32\lsass.exe']] and *[EventData[(Data[@Name='GrantedAccess'] and (Data='0x1010' or Data='0x1038'))]]</Select>  </Query></QueryList>

Таким образом, при условии работоспособности системы журналирования событий Windows можно не только детально и глубоко анализировать все произошедшее на устройстве, но и выполнять произвольные действия при появлении в журнале ОС событий, отвечающих условиям XPath-запроса, что позволяет выстроить целостную систему аудита ИБ и мониторинга событий безопасности штатными средствами ОС. Кроме того, объединив рекомендованные политики аудита информационной безопасности, утилиту Sysmon с детально проработанными конфигами, запрос данных из TI-фидов, функционал XPath-запросов, пересылку и централизацию событий с помощью Windows Event Forwarding, а также настраиваемые задачи с гибкими условиями выполнения скриптов, можно получить фактически бесплатную (по цене лицензии на ОС) систему защиты конечных точек и реагирования на киберинциденты, используя лишь штатный функционал Windows.

Подробнее..

Перевод Лучшие методики журналирования enterprise-приложений (с точки зрения инженера поддержки)

14.12.2020 20:04:49 | Автор: admin

Журналы приложений раскрывают информацию о внешних и внутренних событиях, которые видит приложение в ходе исполнения. Когда при деплое возникает баг, взлом или аномалия, журналы самое полезное и надёжное доказательство для проведения анализа причин инцидента.

Давайте разберемся, как писать в журнал полезные сообщения, которые всем понравятся.

1. Что журналировать


Входящие и исходящие сообщения


Если компоненты взаимодействуют друг с другом с помощью сообщений, то нужно записывать входящие и исходящие сообщения с указанием URLов конечных точек API, параметры запросов, исходные и промежуточные IP запросов, заголовки запросов, информацию об авторе, тела запросов и ответов, бизнес-контекст, временные метки и этапы внутренней обработки.

Очень важно, чтобы каждому сообщению был присвоен уникальный идентификатор (обычно генерируемый на уровне менеджера API или сервиса). Это нужно для отслеживания обмена сообщениями между сервисами в системе.

Вызов сервисов и функций


При вызове сервиса или функции желательно подробнее журналировать их контекст, в основном для отладки (используйте TRACE или DEBUG). Эти журналы помогут в расследовании проблем, связанных с бизнес-логикой, особенно при отсутствии привилегий по прикреплению отладчика к приложению (например, при развёртывании в тестовое, staging или pre-prod-окружение).

Действия пользователей и бизнес-статистика


У каждого приложения есть уникальные бизнес-сценарии и пользовательские маршруты (user journey), которые дают много информации профильным специалистам. Например, не слишком ли долго выполнялась определённая транзакция, или не застревают ли пользователи на какой-то функциональности всё это очень важные данные с точки зрения пользовательского опыта. Иная информация, относящаяся к бизнесу, количество транзакций и активных пользователей, а также их этапы важна для поиска причинно-следственных связей, и даже может применяться в бизнес-анализе.

Операции с данными (журнал аудита)


По соображениям безопасности и соблюдения требований регулятора в большинстве enterprise-приложений требуется вести отдельные журналы по операциям со всей важной информацией, такой как идентификаторы доступа (пользователей и систем), точные экземпляры сервисов и использованные привилегии ролей, временные метки, запросы на данные, слепки предыдущего и нового состояния изменённых данных (diff). Журнал аудита должен фиксировать все операции с данными (доступ, импорт, экспорт и т. д.), а также CRUD-операции (создание, чтение, обновление, удаление), выполненные пользователями, иными системами и сервисами.

Системные события


К ним относится информация о поведении (запусках, остановках, перезапусках и событиях, связанных с безопасностью), переходных режимах (холодный, разогрев, горячий), межсервисному взаимодействию (handshake, статусы установки соединения подключено, отключено, переподключено, повторные попытки), идентификаторы экземпляров сервисов, активно обслуживающие API, активно прослушивающие диапазоны IP и портов, загруженные конфигурации (первоначальные загрузки и динамические обновления), общее состояние сервисов, а также всё, что поможет понять поведение системы.

Статистика производительности


Усердие прекрасная характеристика вычислительных устройств, но они могут работать не идеально. В любое время могут возникнуть проблемы с производительностью или внезапные неожиданные ухудшения обслуживания (в основном из-за необработанных ошибок и повреждённых данных). Чтобы их определить, всегда рекомендуется публиковать статистику общего состояния и производительности системы. Она может содержать информацию вроде счётчиков вызовов API (успешно обслуженных и сбойных), сетевую задержку, среднюю длительность roundtripов, потребление памяти и прочую специфическую для приложения информацию (обычно определяется бизнес-контекстом).

Угрозы и уязвимости


Раскрытие угроз и уязвимостей с помощью runtimea приложения и журнала это искусство, которым должен овладеть любой разработчик enterprise-ПО. Обычно взломы и сбои не происходят внезапно. Чаще всего есть признаки, которые сначала никто не замечает. Поэтому нужно всегда журналировать подозрительную человеческую активность (например, ошибочные попытки аутентификации и верификации с приложением всей низкоуровневой информации вроде использованных сетей, источников запросов, пользовательских ролей и привилегий), а также поведение системы (например, рост пиков в паттернах потребления ресурсов, высокую нагрузку на веб-серверы, случайные сбои сервисов). Когда вы замечаете подозрительное событие, убедитесь, что журналы содержат всю связанную с ним информацию. В идеале, чтобы это была full-stack-трассировка со значениями параметров и дополнительной информацией, полученной из контекста приложения.

2. Что не нужно журналировать


Информацию личного порядка


Почти все законы, регулирующие вопросы приватности (например, GDPR, CCPA) прямо рекомендуют разработчикам не журналировать информацию, позволяющую установить личность. Сюда относятся ФИО, ники, пол, день рождения, почтовый адрес, электронную почту, телефонные номера, номер социального страхования и номера кредитных карт.

Названия компаний и контактную информацию


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

Финансовые данные (банковские счета, реквизиты банковских карт, пересылаемые суммы и т. д.)


По закону все финансовые данные должны быть полностью убраны или замаскированы. Раскрытие такой информации в журналах легко может привести к серьёзному судебному иску (вплоть до уголовной ответственности). Избегайте этого всеми способами.

Пароли, ключи безопасности и секреты, токены аутентификации


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

Примечание: вам легко будет определять, какую информацию нужно скрыть от журналов, если вы добавите в каждое поле атрибут, определяющий уровень видимости (например, show, mask, hide, encrypt). Если у вас есть такой механизм, то вы сможете менять видимость полей, просто обновляя свойства в конфигурации. Это хорошее решение в тех случаях, когда нужно журналировать какую-нибудь пользовательскую информацию в небоевых окружениях, особенно для тестирования и отладки. Или можно написать парсеры, которые фильтруют журналы и обрабатывают конфиденциальные поля в соответствии с заранее прописанными для этого окружения инструкциями.

3. Лучшие методики


Знайте, когда нужно использовать тот или иной уровень журналирования


Уровень журналирования используется для обозначения серьёзности каждого элемента системы. В большинстве фреймворков для журналирования есть такие уровни:

  • FATAL: очень серьёзные ошибки, которые наверняка приводят к прерыванию приложения. Обычно это заканчивается серьезными сбоями.
  • ERROR: ошибки, с которыми приложение ещё может продолжить работу, но с ухудшением определённых возможностей.
  • WARN: менее опасные события по сравнению с ошибками. Обычно не приводят к ухудшению возможностей или полному сбою приложения. Но это всё ещё важные события, которые необходимо расследовать.
  • INFO: важные баннеры событий и информационные сообщения в поведении приложения.
  • DEBUG: специфическая и подробная информация, обычно используемая в отладке. Эти журналы помогают нам путешествовать по коду.
  • TRACE: самые низкоуровневые данные, вроде трассировок стека, которые содержат больше всего информации об определённом событии или контексте. Эти журналы помогают исследовать значения переменных и полные стеки ошибок.

В Linux Syslog есть и более серьёзные уровни, такие как Emergency, Alert, Critical, Error, Warning, Notice, Informational и Debug.

Вне зависимости от сложности и глубины каждого уровня журналирования, мы должны корректно настраивать их в своём коде, чтобы предоставлять оптимальное количество информации в каждом сценарии. Например, все данные, используемые разработчиками для отладки и технического анализа, должны идти на уровнях DEBUG или TRACE, а баннеры с системными данными опускаются ниже INFO.

Используйте английский язык


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

2020-11-11 13:52:18 DEBUG  App:36 - Loading adapters..2020-11-11 13:52:21 DEBUG  Adapters:10 - Loading adapter docs/v12020-11-11 13:52:22 DEBUG  Adapters:16 - Loading adapter mongo/v12020-11-11 13:52:26 DEBUG  Docs:38 - docs adapter initialized2020-11-11 13:52:27 DEBUG  Mongo:38 - mongo adapter initialized2020-11-11 13:52:22 DEBUG  Adapters:20 - Successfully loaded all

Добавляйте удобные для разработчиков сообщения (краткие и содержательные)


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

Для оптимизации сообщений вам необходимо чёткое понимание функциональных и нефункциональных ожиданий от системы и планирование нужного качества и количества сообщений. Каждый журнал должен быть содержательным и соответствующим контексту: всегда пишите кратко и по делу.

2020-11-11 13:52:27 DEBUG  Users:18 - Successfully created new user (RecordID: 5f5a0d594d17e22b848ee570)2020-11-11 13:52:27 ERROR  Users:34 - Failed to update DB (E: inactive user, RecordID: 5f5a0d594d17e22b848ee570)

Создайте справочные идентификаторы, псевдонимы и упрощённые шаблоны для часто используемых и длинных сообщений


Вместо того, чтобы каждый раз записывать полное описание, попробуйте создать справочные идентификаторы или упрощённые шаблоны, чтобы представлять в журнале длинные повторяющиеся описания. Это уменьшает общее количество и длину сообщений, а также повышает гибкость в сокрытии определённой информации. Например, вместо того, чтобы описывать в текстовом журнале описание уязвимости, лучше использовать псевдоним или идентификатор, чтобы только профильные специалисты могли разобраться в актуальном сценарии.

2020-11-11 13:52:27 ERROR  DB:22 - Failed to write (E:ORA-01017)// ORA-01017 denotes "invalid username/password; logon denied" error message

Используйте корректные временные метки


Временные метки позволяют понять последовательность событий, они необходимы для отладки и анализа. При фиксировании времени рекомендуется использовать наиболее подробные значения (например, на уровне милли- или микросекунд), чтобы легче было определять смежные события. Также убедитесь, что временные метки стоят в начале сообщения в формате yyyy-mm-dd HH:mm:ss. Всегда указывайте часовой пояс, если не используете на сервере время по умолчанию (UTC).

// with default server time (UTC)2020-11-11 13:52:12 INFO  XYZ Integration API Manager v2.0.0// with timezone (e.g. PST - Pacific Standard Time)2020-11-11 13:52:12PST INFO  XYZ Integration API Manager v2.0.0

Указывайте источник или происхождение журнальных данных (для DEBUG, TRACE, ERROR)


Это очень полезно для чёткого определения места, которое привело к соответствующему сообщению. Некоторые фреймворки для журналирования позволяют указывать источники на самом подробном уровне (вплоть до название файлов с номерами строк), но чаще всего достаточно упоминания только класса, функции или названия файла.

2020-11-11 13:52:12 INFO  app - XYZ Integration API Manager v2.0.02020-11-11 13:52:15 INFO  app - Loading configurations..2020-11-11 13:52:18 INFO  app - *** InstanceID APIM_V2_I022020-11-11 13:52:18 INFO  app  *** BaseURL http://10.244.2.168:30002020-11-11 13:52:19 INFO  app - *** LogLevel 05 (DEBUG)2020-11-11 13:52:12 DEBUG  App:22 - Initializing Swagger UI..2020-11-11 13:52:14 DEBUG  App:28 - Generating schemata..2020-11-11 13:52:14 DEBUG  App:30 - Initializing REST services..2020-11-11 13:52:15 DEBUG  App:32 - Generating documentation..2020-11-11 13:52:18 DEBUG  App:36 - Loading adapters..2020-11-11 13:52:21 DEBUG  Adapters:10 - Loading adapter docs/v12020-11-11 13:52:22 DEBUG  Adapters:16 - Loading adapter mongo/v12020-11-11 13:52:26 DEBUG  Docs:38 - docs adapter initialized2020-11-11 13:52:27 DEBUG  Mongo:38 - mongo adapter initialized2020-11-11 13:52:22 DEBUG  Adapters:20 - Successfully loaded all2020-11-11 13:52:31 INFO  app - Started listening...

Каждый журнал должен быть уникален в рамках системы


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

2020-11-11 13:52:18 DEBUG  App:36 - Loading adapters..2020-11-11 13:52:21 DEBUG  Adapters:10 - Loading adapter docs/v12020-11-11 13:52:22 DEBUG  Adapters:16 - Loading adapter mongo/v12020-11-11 13:52:26 DEBUG  Docs:38 - docs adapter initialized2020-11-11 13:52:27 DEBUG  Mongo:38 - mongo adapter initialized2020-11-11 13:52:22 DEBUG  Adapters:20 - Successfully loaded all

Добавьте в сообщение отслеживаемый идентификатор или токен сообщения


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

[87d4s-a98d7-9a8742jsd] Request Body: { "request_id": "87d4s-a98d7-9a8742jsd", "app_id": "TX001", "option_val": "IBM", "req_type_id": "0013", "data": {...........}[87d4s-a98d7-9a8742jsd] Sending request to RefData: href="http://personeltest.ru/away/10.244.2.168:8280/v1

Указывайте соответствие идентификаторов в точках перехода


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

[1000508] ***** Incoming request from 10.244.2.168:3000 *****[1000508] Origin Id: 87d4s-a98d7-9a8742jsd -> System ID: 1000508[1000508] Transaction successfully added to Rabbit Queue

Указывайте идентификаторы всех экземпляров сервиса


Большинство enterprise-систем являются распределёнными вычислительными платформами, в которых присутствует много экземпляров одних и тех же сервисов с разнообразными конфигурациями приложения, ресурсами, версиями и сетевыми свойствами. Рекомендуется присвоить экземплярам идентификаторы и использовать их при межсервисном взаимодействии.

2020-11-11 13:52:12 INFO  app - XYZ Integration API Manager v2.0.02020-11-11 13:52:15 INFO  app - Loading configurations..2020-11-11 13:52:18 INFO  app - *** InstanceID APIM_V2_I022020-11-11 13:52:18 INFO  app  *** BaseURL http://10.244.2.168:30002020-11-11 13:52:19 INFO  app - *** LogLevel 05 (DEBUG)

Настройте активный уровень журналирования


Активный уровень журналирования нужно менять в зависимости от окружения развёртывания. Для production рекомендуется выводить журналы вплоть до уровня INFO. В других окружениях журналы выводятся до уровня DEBUG или TRACE, в зависимости от степени подробности, которая нужна командам разработки и эксплуатации.

// Active Log Level = DEBUG2020-11-11 13:52:12 INFO  app - XYZ Integration API Manager v2.0.02020-11-11 13:52:15 INFO  app - Loading configurations..2020-11-11 13:52:18 INFO  app - *** InstanceID APIM_V2_I022020-11-11 13:52:18 INFO  app  *** BaseURL http://10.244.2.168:30002020-11-11 13:52:19 INFO  app - *** LogLevel 05 (DEBUG)2020-11-11 13:52:12 DEBUG  App:22 - Initializing Swagger UI..2020-11-11 13:52:14 DEBUG  App:28 - Generating schemata..2020-11-11 13:52:14 DEBUG  App:30 - Initializing REST services..2020-11-11 13:52:15 DEBUG  App:32 - Generating documentation..2020-11-11 13:52:18 DEBUG  App:36 - Loading adapters..2020-11-11 13:52:21 DEBUG  Adapters:10 - Loading adapter docs/v12020-11-11 13:52:22 DEBUG  Adapters:16 - Loading adapter mongo/v12020-11-11 13:52:26 DEBUG  Docs:38 - docs adapter initialized2020-11-11 13:52:27 DEBUG  Mongo:38 - mongo adapter initialized2020-11-11 13:52:22 DEBUG  Adapters:20 - Successfully loaded all2020-11-11 13:52:31 INFO  app - Started listening...// Active Log Level = INFO2020-11-11 13:52:12 INFO  app - XYZ Integration API Manager v2.0.02020-11-11 13:52:15 INFO  app - Loading configurations..2020-11-11 13:52:18 INFO  app - *** InstanceID API_V2_I022020-11-11 13:52:18 INFO  app  *** BaseURL http://10.244.2.168:30002020-11-11 13:52:19 INFO  app - *** LogLevel 04 (INFO)2020-11-11 13:52:31 INFO  app - Started listening...

Предоставляйте достаточный контекст для ошибок и сбоев


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

[1000508] ***** Incoming request from 10.244.2.168:3000 *****[1000508] Origin Id: 87d4s-a98d7-9a8742jsd -> System ID: 1000508[1000508] Failed to validate msg body (E: Uncaught ReferenceError: missing params - option_val)[1000508] Failed Message: { "request_id": "87d4s-a98d7-9a8742jsd", "app_id": "TX001", "req_type_id": "0013", "data": {...........}}

Подтверждайте доказательствами операции с данными (не предполагайте!)


При каждой операции с данными не принимайте на веру успешность её выполнения. Всегда проверяйте конечное состояние с помощью доказательств. Например, когда вы создаёте, обновляете или удаляете запись в базе данных, она возвращает счётчик изменённых записей и саму обновлённую запись. Всегда выполняйте проверку ожидаемого счётчика или результата. Другой пример: когда вы вставляете запись в структуру данных (например, в очередь), то проверяйте, увеличился ли её размер. Предположим, в вашей системе используются конкурентные операции, но очередь их не поддерживает. Тогда вы можете терять какие-то записи, и единственный способ обнаружить такие скрытые ошибки в коде это проверка длины.

DEBUG  BatchWriter:28 - Successfully connected to DB. Trying to insert 24 accounts...DEBUG  BatchWriter:35 - Successfully inserted 24 accounts. Total DB rows affected: 24

Шифруйте или маскируйте конфиденциальные данные


Обычно закон требует маскировать и/или шифровать конфиденциальную информацию пользователей и внутренних систем. В зависимости от вашей отрасли и места работы могут меняться и требования регуляторов. Выясните все нюансы и реализуйте в приложении корректные процедуры. В некоторых случаях перед началом эксплуатации вам может потребоваться представить стратегию журналирования службе безопасности и получить их утверждение или сертификат.

[1000508] ***** Incoming request from 10.244.2.168:3000 *****[1000508] Origin Id: 87d4s-a98d7-9a8742jsd -> System ID: 1000508[1000508] Request Body: {user_id:XXXXXXXXXX,personal_details:{  firstName:XXXXXXXXXX,  lastName:XXXXXXXXXX,  DOB:XXXXXXXXXX,  gender:Male,  proffessional:Software Architect,  industry:IT,  SSN:XXXXXXXXXX},address_history:[  {streetAdress:Street No 1,zipcode:XXXXX,state:CA},  {streetAdress:Street No 2,zipcode:XXXXX,state:NY},    {streetAdress:Street No 2,zipcode:XXXXX,state:AL}],card_info:[  {type:amex,card_number:XXXXXXXXX,credit_limit:XXXXX},  {type:visa,card_number:XXXXXXXXX,credit_limit:XXXXX}]}

Настройте именование файлов журналов, политики ротации, размер хранилищ и процедуры резервного копирования


Если вы пишете журналы в файлы, то убедитесь, что они хранятся на отдельном диске, который никак не влияет на работающее приложение (например, можно выделить том на удалённом сервере и прикрепить его к серверу приложений). Также выясните частоту журналирования и характер увеличения файлов. Убедитесь, что у вас есть политика ротации журналов с корректными соглашениями по наименованиям (например, при создании к названию добавляется временная метка), чтобы поддерживать идеальный размер и количество файлов. Также у вас должен быть механизм резервного копирования старых журналов в безопасное место, и механизм регулярной очистки хранилищ. В зависимости от вашей отрасли можно настроить резервное копирование по времени (обычно каждые несколько месяцев или лет), с уничтожением всех предыдущих файлов в конце периода.

[ec2-user@ip-XXX-XX-X-XXX logs]$ ls..APIM_V2_I02-2020-11-20_04:38:43.logAPIM_V2_I02-2020-11-23_02:05:35.logAPIM_V2_I02-2020-11-24_04:38:17.logAPIM_V2_I02-2020-11-27_03:28:37.logAPIM_V2_I02-2020-11-27_12:06:45.log...

4. Дополнительные рекомендации


Используйте центральный агрегатор журналов


Очень распространённая практика среди разработчиков enterprise-приложений создание центрального доступного сервера или места для сбора журналов. Обычно такие агрегаторы отслеживают журналы не только приложений, но также устройств и операционных систем (например, Linux Syslog), сети, сетевых экранов, баз данных и т. д. К тому же они отделяют файлы журналов от серверов приложений и позволяют хранить журналы в более защищённых, упорядоченных и эффективных форматах более длительное время. В некоторых отраслях (например, банковской и финансовой) необходимо хранить журналы как в локальном, так и в центральном хранилище, чтобы злоумышленники не могли получить доступ к обоим местам и одновременно удалить доказательства своей деятельности. Так что избыточность данных и их несоответствие между двумя хранилищами могут помочь заметить проникновения.

Пишите парсеры и предусмотрительно отслеживайте журналы


Возможность написания парсеров и фильтров встроена в большинство инструментов для мониторинга журналов так называемая SIEM-интеграция (security information and event management). Парсеры помогают сохранять журналы в более упорядоченных форматах, а запрашивать данные становится гораздо проще и быстрее. Также правильно организованные данные можно передавать системам мониторинга и поиска аномалий для профилактического мониторинга и прогнозирования будущих событий. Эти инструменты обладают очень широкими возможностями по графическому отображению данных на основе временных последовательностей и в реальном времени.

Настройте оповещения и push-уведомления для критических инцидентов


Почти все инструменты мониторинга журналов позволяют задавать определённым уровням свои пороговые значения. Когда система достигает таких значений, инструмент заранее обнаруживает их, помогает журналировать данные и уведомляет сисадминов с помощью оповещений, API push-уведомлений (например, Slack Audit Logs API), электронных писем и т. д. Также их можно заранее настроить на инициирование автоматических процессов вроде динамического масштабирования, резервного копирования системы, переброски нагрузки и т. д. Но если вы вкладываетесь в коммерческое ПО для мониторинга журналов, внимательно его изучите, потому что такие инструменты могут быть избыточны для небольших и средних программных систем.

5. Рекомендуемые инструменты


Фреймворки для журналирования


JavaScript/TypeScript: Log4js / pino
Java: Log4j
Golang: Logrus
Serverless-функции: aws-lambda-powertools-python / Самописное

Изучение, агрегирование и мониторинг журналов


CLI-инструменты: less, vim
Облачные инструменты: Fluentd, AWS CloudWatch
SIEM-инструменты: SolarWinds, Splunk, McAfee ESM, DataDog, IBM QRadar
Прочие: ELK Stack (ElasticSearch, Logstash, Kibana, Beats), Loggly
Подробнее..

Где порешать аналитические задачи от команд Яндекса? Контест и разбор

21.09.2020 18:14:10 | Автор: admin
Сегодня начинается пробный раунд чемпионата по программированию Yandex Cup. Это означает, что можно с помощью системы Яндекс.Контест решать задачи, подобные тем, которые будут в квалификационном раунде. Пока результат ни на что влияет.

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

A. Посчитать лгунов в стране

Решить в Контесте

В государстве живёт 10 000 человек. Они делятся на правдолюбов и лгунов. Правдолюбы говорят правду с вероятностью 80%, а лгуны с вероятностью 40%. Государство решило подсчитать правдолюбов и лгунов на основе опроса 100 жителей. Каждый раз случайно выбранного человека спрашивают: Вы лгун? и записывают ответ. Однако один человек может поучаствовать в опросе несколько раз. Если житель уже участвовал в опросе он отвечает то же самое, что и в первый раз. Мы знаем, что правдолюбов 70%, а лгунов 30%. Какая вероятность того, что государство недооценит количество лгунов, т. е. опрос покажет, что лгунов меньше 30%? Дайте ответ в процентах с точкой в качестве разделителя, результат округлите до сотых (пример ввода: 00.00).

Решение
1. Посчитаем вероятность получить ответ Да на вопрос Вы лгун?.

На каждом шаге вероятность получить ответ Да, я лгун складывается из вероятности получить ответ Да:

От правдолюбов, которых не спрашивали до этого: 0,2 * доля правдолюбов, которых не спрашивали.
От лгунов, которых не спрашивали до этого: 0,4 * доля лгунов, которых не спрашивали.
От правдолюбов, которых уже спрашивали до этого и которые ответили Да: 1,0 * доля правдолюбов, которых уже спрашивали и которые ответили Да.
От лгунов, которых уже спрашивали до этого и которые ответили Да: 1,0 * доля лгунов, которых уже спрашивали и которые ответили Да.

Посчитаем по шагам вероятность получить ответ Да от правдолюбов:

1. 0,2 * % правдолюбов.
2. 0,2 * (% правдолюбов % опрошенных правдолюбов) + 0,2 * (% опрошенных правдолюбов) = 0,2 * % правдолюбов.
3. Аналогично шагу 2.

То есть на каждом шаге вероятность получить ответ Да, я лгун от правдолюбов составляет 0,2 и не зависит от того, сколько правдолюбов опросили до этого. Точно так же для лгунов.

Таким образом, вероятность получить ответ Да от правдолюбов и лгунов: 0,2 * 0,7 + 0,4 * 0,3 = 0,26.

2. Посчитаем вероятность недооценить количество лгунов.

Количество лгунов, которое получит государство по результатам опроса, это биномиальное распределение с параметрами n = 100, p = 0,26.

Количеством успехов в нашем случае будет 30 (30% от 100 опрошенных). Если мы посмотрим на функцию распределения в этой точке, то получим P (x < 30) = 0,789458. Посчитать можно вот тут: stattrek.com/online-calculator/binomial.aspx.

Ответ в процентах, округлённых до сотых: 78,95.

B. Театральный сезон и телефоны

Решить в Контесте

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

При покупке билета пользователь указывает номер своего телефона. Необходимо найти спектакль с наибольшим числом уникальных телефонных номеров. И посчитать количество соответствующих уникальных телефонных номеров.

Формат ввода

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

Формат вывода

Число уникальных номеров.

Решение
Технические особенности данных

Подробный вариант решения лежит в main.py.

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

Каждый номер может получить одного и более двойников из следующих вариантов:

1. 8-(801)-111-11-11
2. 8-801-111-11-11
3. 8801-111-11-11
4. 8-8011111111
5. +88011111111
6. 8-801-flowers, вместо цифр буквы (распространено в США)

Как предполагается обнаружить эти особенности:

1. Форматы в пунктах 14 видны при первом взгляде на данные и удаляются стандартными методами вроде replace.
2. Формат 5 легко отфильтровать, проверив число символов в телефонах после форматирования пункта 1. Во всех номерах будет 11 символов, кроме этого формата.
3. Пункт 6 самый неочевидный, надо догадаться проверить наличие нечисловых символов в номере телефона. Надеюсь, что смысл этих букв участник быстро найдёт в интернете.

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

Код. Как генерировались данные

Этот раздел для тех, кому надо разобраться в устройстве кода или изменить сгенерированные логи в ticket_logs.csv. Все действия сложены в logs_generator.py. Как запустить:

python logs_generator.py

На выходе получается файл ticket_logs.csv.

Конфигурационный файл config.yaml

В файле собраны все параметры, которые влияют на создание файла ticket_logs.csv:

  • zones коды зон, которые используются в генерируемых телефонных номерах.
  • seven_letter_words слова, которые используются для создания телефонных номеров с буквами.
  • letters_to_numbers_dict словарь соответствия цифр на клавиатуре телефона и алфавита. Вряд ли он изменится.
  • performances список спектаклей и их весов. Чем выше вес, тем чаще спектакль будет в логах ticket_logs.csv.

Полезные константы в файле logs_generator.py:

USERS_COUNT = 1000  # количество пользователей (можно сверять в решении main.py результат)RESULT_FILE_LOCATION = 'ticket_logs.csv'  # куда сохранять созданные логи

Как формируются телефонные номера

Весь процесс создания номеров сложен в классе PhonesGenerator. Для создания случайного номера (и вариаций его написания) вызовите метод generate_number:

from yaml import load, FullLoaderfrom phone_numbers.phone_numbers_generator import PhonesGeneratorwith open('config.yaml') as f:    config = load(f, Loader=FullLoader)PhonesGenerator(config).generate_number()

Метод вернёт словарь с набором телефонных номеров. Пример:

{
'base': '8804academy', 'case_1': '8-(804)-aca-de-my', 'case_2': '8-804-aca-de-my',
'case_3': '8804-aca-de-my', 'case_4': '+8804academy', 'case_5': '8-804-academy',
'case_6': '8-804-2223369'
}


При многократном вызове метода generate_number в первую очередь отдаются номера с буквами. Слова в случайном порядке берутся из файла config.yaml, ключ seven_letter_words. Когда слова заканчиваются, то отдаются только числовые номера. Но можно и сразу генерировать числовые, для этого достаточно указать параметр generate_number(with_letters=False):

{
'base': '88062214016', 'case_1': '8-(806)-221-40-16', 'case_2': '8-806-221-40-16',
'case_3': '8806-221-40-16', 'case_4': '+88062214016', 'case_5': '8-806-2214016',
'case_6': '8-806-2214016'
}


В logs_generator.py из этого набора случайно выбирается от одного до некоторого набора вариантов. Подходящие варианты для числовых номеров задаёт константа PHONE_CASES, для буквенных PHONE_CASES_WITH_LETTERS в файле logs_generator.py. Сами форматы определяют методы build_case_1_number, ..., build_case_6_number в классе PhonesGenerator. Они же добавляются в конце метода generate_number.

Как генерируются названия спектаклей

Список спектаклей и их весов сложен в файле config.yaml. Чем выше вес, тем чаще спектакль будет в логах ticket_logs.csv. Этот процесс заложен в функции random_performance в logs_generator.py. Состав спектаклей:

  • Оперы: Севильский цирюльник, Волшебная флейта, Норма, Травиата, Евгений Онегин, Аида, Кармен, Свадьба Фигаро, Риголетто.
  • Балеты: Жизель, Лебединое озеро, Щелкунчик, Спящая красавица, Ромео и Джульетта, Дон Кихот, Баядерка, Спартак.
  • Мюзиклы: Вестсайдская история, TODD, Юнона и Авось, Ночь перед Рождеством, Чикаго, Ла-Ла Ленд, Нотр-Дам де Пари, Кошки.

Недостатки

Код класса PhonesGenerator слишком завязан на число символов в номере это можно улучшить.

C. Рассчитать pFound

Решить в Контесте

В архиве содержится три текстовых файла:

  • qid_query.tsv id запроса и текст запроса, разделённые табуляцией;
  • qid_url_rating.tsv id запроса, URL документа, релевантность документа запросу;
  • hostid_url.tsv id хоста и URL документа.

Нужно вывести текст запроса с максимальным значением метрики pFound, посчитанной по топ-10 документов. Выдача по запросу формируется по следующим правилам:
  • С одного хоста может быть только один документ на выдаче. Если для запроса есть несколько документов с одним и тем же id хоста берется максимально релевантный документ (а если несколько документов максимально релевантны, берется любой).
  • Документы по запросу сортируются по убыванию релевантности.
  • Если у нескольких документов с разных хостов релевантность одинакова, их порядок может быть произвольным.

Формула для расчёта pFound:

pFound = $\sum_{i=1}^{10}$pLook[i] pRel[i]
pLook[1] = 1
pLook[i] = pLook[i 1] (1 pRel[i 1]) (1 pBreak)
pBreak = 0,15

Формат вывода

Текст запроса с максимальным значением метрики. Например, для open_task.zip правильный ответ:
гугл переводчик

Решение
Все вводные даны в условии. Что-то дополнительное придумывать не нужно достаточно аккуратно реализовать вычисление pFound в коде и не забыть взять максимум по хосту. Для решения очень удобно использовать библиотеку pandas с помощью неё легко группировать по запросам и хостам и вычислять агрегации.

import pandas as pd# считываем данныеqid_query = pd.read_csv("hidden_task/qid_query.tsv", sep="\t", names=["qid", "query"])qid_url_rating = pd.read_csv("hidden_task/qid_url_rating.tsv", sep="\t", names=["qid", "url", "rating"])hostid_url = pd.read_csv("hidden_task/hostid_url.tsv", sep="\t", names=["hostid", "url"])# делаем join двух таблиц, чтобы было просто брать url с максимальным рейтингомqid_url_rating_hostid = pd.merge(qid_url_rating, hostid_url, on="url")def plook(ind, rels): if ind == 0: return 1    return plook(ind-1, rels)*(1-rels[ind-1])*(1-0.15)def pfound(group): max_by_host = group.groupby("hostid")["rating"].max() # максимальный рейтинг хоста top10 = max_by_host.sort_values(ascending=False)[:10] # берем топ-10 урлов с наивысшим рейтингом pfound = 0    for ind, val in enumerate(top10): pfound += val*plook(ind, top10.values) return pfoundqid_pfound = qid_url_rating_hostid.groupby('qid').apply(pfound) # группируем по qid и вычисляем pfoundqid_max = qid_pfound.idxmax() # берем qid с максимальным pfoundqid_query[qid_query["qid"] == qid_max]

D. Спортивный турнир

Решить в Контесте
Ограничение по времени на тест 2 с
Ограничение по памяти на тест 256 МБ
Ввод стандартный ввод или input.txt
Вывод стандартный вывод или output.txt
Пока Маша была в отпуске, её коллеги организовали турнир по шахматам по олимпийской системе. За отдыхом Маша не обращала особого внимания на эту затею, так что она еле может вспомнить, кто с кем играл (про порядок игр даже речи не идёт). Внезапно Маше пришла в голову мысль, что неплохо бы привезти из отпуска сувенир победителю турнира. Маша не знает, кто победил в финальной игре, но сможет без труда вычислить, кто в нём играл, если только она правильно запомнила играющие пары. Помогите ей проверить, так ли это, и определить возможных кандидатов в победители.

Формат ввода

В первой строке находится целое число 3n2161,n=2k1 количество прошедших игр. В последующих n строках по две фамилии игроков (латинскими заглавными буквами) через пробел. Фамилии игроков различны. Все фамилии уникальны, однофамильцев среди коллег нет.

Формат ввода

Выведите NO SOLUTION (без кавычек), если Маша неправильно запомнила игры, и по этой сетке нельзя получить турнир по олимпийской системе. Если турнирная сетка возможна, выведите две фамилии в одной строке фамилии кандидатов на первое место (порядок не важен).

Пример 1
Ввод Вывод
7
GORBOVSKII ABALKIN
SIKORSKI KAMMERER
SIKORSKI GORBOVSKII
BYKOV IURKOVSKII
PRIVALOV BYKOV
GORBOVSKII IURKOVSKII
IURKOVSKII KIVRIN
IURKOVSKII GORBOVSKII
Пример 2
Ввод Вывод
3
IVANOV PETROV
PETROV BOSHIROV
BOSHIROV IVANOV
NO SOLUTION
Примечания

Олимпийская система, также известная как плей-офф система организации соревнований, при которой участник выбывает из турнира после первого же проигрыша. Подробнее про олимпийскую систему можно почитать на Википедии.

Схема первого теста из условия:



Решение
Из количества игрn = 2^k 1легко получить количество раундов турнираk.Обозначим количество игр, которые сыгралi-й участник, черезn_i.Очевидно, что финалисты сыграли максимальное количество раз (они единственные играли во всехkраундах).Теперь научимся проверять, что данный нам набор встреч между участниками возможен в турнире по олимпийской системе.Заметим, что игра между участникамиiиjмогла произойти только в раундеmin(n_i, n_j),поскольку этот раунд был последним для кого-то из них (раунды для удобства нумеруются с единицы).Назовём псевдораундом номерrмножество игр(i, j), для которыхmin(n_i, n_j) = r. Проверку корректности будем делать в соответствии с таким утверждением:

Утверждение.Набор из2^k 1игр задаёт турнир по олимпийской системе тогда и только тогда,когда:

1. В каждом псевдораунде все участники различны.
2. Количество игр в псевдораунде r равно 2^{k r}.

Доказательство.Необходимость этих двух условий очевидна: псевдораунды соответствуют настоящим раундам турнира,а для настоящих раундов условия верны.Достаточность докажем индукцией поk.Приk=1есть одна играс двумя различными участниками это корректный олимпийский турнир.Проверим переходk1 -> k.

Во-первых, докажем, что каждый участник турнира играл в первом псевдораунде.Рассмотрим произвольного игрока,пусть он участвовал вqиграх.В каждом псевдораунде он мог сыграть не более одного раза,причём в псевдораундах послеq-го он не мог играть ни разу.Значит, он должен был сыгратьпо одному разу в каждом из псевдораундов1, 2, ..., q.Это, в частности, означает, что все люди сыграли в первомпсевдораунде, а всего игроков2^k.Теперь докажем, что в каждой из2^{k1}игр первого псевдораунда был ровно один участниксn_i = 1.Как минимум один такой участник в каждой игре должен быть по определению псевдораунда.

С другой стороны,есть не менее2^{k1}человек сn_i > 1 это участники следующего псевдораунда.Следовательно, людей сn_i = 1было ровно2^{k1}, по одному на каждую игру.Теперь легко понять, как должен выглядеть первый раундискомого турнира: назначим в каждой игре первого псевдораунда проигравшим участника сn_i = 1,а победителем участника сn_i > 1.Множество игр между победителями удовлетворяет условиюдляk1(после выбрасывания игр из первого псевдораунда всеn_iуменьшились на 1).Следовательно, этомумножеству соответствует турнир по олимпийской системе.

import sysimport collectionsdef solve(fname):    games = []    for it, line in enumerate(open(fname)):        line = line.strip()        if not line:            continue        if it == 0:            n_games = int(line)            n_rounds = n_games.bit_length()        else:            games.append(line.split())    gamer2games_cnt = collections.Counter()    rounds = [[] for _ in range(n_rounds + 1)]    for game in games:        gamer_1, gamer_2 = game        gamer2games_cnt[gamer_1] += 1        gamer2games_cnt[gamer_2] += 1    ok = True    for game in games:        gamer_1, gamer_2 = game        game_round = min(gamer2games_cnt[gamer_1], gamer2games_cnt[gamer_2])        if game_round > n_rounds:            ok = False            break        rounds[game_round].append(game)    finalists = list((gamer for gamer, games_cnt in gamer2games_cnt.items() if games_cnt == n_rounds))    for cur_round in range(1, n_rounds):        if len(rounds[cur_round]) != pow(2, n_rounds - cur_round):            ok = False            break        cur_round_gamers = set()        for gamer_1, gamer_2 in rounds[cur_round]:            if gamer_1 in cur_round_gamers or gamer_2 in cur_round_gamers:                ok = False                break            cur_round_gamers.add(gamer_1)            cur_round_gamers.add(gamer_2)    print ' '.join(finalists) if ok else 'NO SOLUTION'def main():    solve('input.txt')if name == '__main__':    main()



Чтобы порешать задачи других треков чемпионата, нужно зарегистрироваться здесь.
Подробнее..

Забываете передавать аргументы в функцию? Вам поможет contextvars

05.11.2020 12:07:06 | Автор: admin


Мы в Яндекс.Такси любим писать логи. Ещё больше мы любим, когда логи помогают нам расследовать проблемы в продакшене. При нагрузке в десятки тысяч RPS просто набора лог-записей мало. Хочется уметь фильтровать логи по пользователю, видеть последовательность вызовов клиентского API, а также углубляться в логи запроса.


Для реализации такого интерфейса каждая лог-запись в обработчике сопровождается метаинформацией: id заказа, пользователя, запроса. Однако иногда разработчики забывают добавить метаинформацию при логировании.



Из логов хорошо понятно, что именно пошло не так


Проблема


Представим себе такой обработчик:


@route('/feedback')async def feedback_handler(request):    log_extra = {        'handler': 'some-handler',        'user_id': request.user_id,    }    ...    order = await db.feedbacks.find_one(order_id=request.order_id)    if order is None:        logger.warning(f'user has no active orders in db', extra=log_extra)        return process_without_order(request)# output:# text="user has no active orders in db" handler=some-handler user_id=some-guid

Хендлер запроса POST /feedback?order_id=d34db33f пытается найти указанный в query-параметре заказ, если заказа в БД нет рапортует об этом в логе. Вызов логера сопровождается явной передачей аргумента extra. В нём содержится метаинформация, которую нужно вывести вместе с текстом лога. У такого подхода есть проблемы:


  • Аргумент extra необязателен для функций-логеров.
  • Отсутствие extra никак не влияет на результат тестов (тесты на логи мы обычно не пишем).
  • Проблемы с extra ловятся на ревью часто, но далеко не всегда.
  • Однажды созданный extra нужно тащить сквозь все сигнатуры, хотя фактически он не влияет на логику функции:

# handler.pyasync def handler(request):    log_extra = derive_extra(request)    # ...    await foo.some_logic1(arg, kwarg=kwarg, log_extra=log_extra)    # ...# foo.pyasync def some_logic1(..., log_extra=None):    # ...    await bar.some_logic2(..., log_extra=log_extra)    # ...# bar.pyasync def some_logic2(..., log_extra=None):    # ...    await qux.some_logic3(..., log_extra=log_extra)    # ...# qux.pyasync def some_logic3(..., log_extra=None):    # ...    if something:        logger.warning('Something has happened', extra=log_extra)    # ...

На практике разработчики периодически забывают добавлять метаинформацию к логированию, из-за чего поведение некоторых хендлеров тяжело объяснить с точки зрения логов.



Из логов не понять причину 404 кода


Решения


Пропатчить логер


Зададимся целью сделать параметр extra обязательным для логирующих методов. Стандартная библиотека предоставляет возможность задать кастомный класс логера для потребителей функции logging.getLogger(). Воспользуемся этим и реализуем класс, для логинг-методов которого параметр extra будет обязательным.


Например, так
import loggingclass MyLogger(logging.getLoggerClass()):    sentinel = object()    def __init__(self, name):        logging.Logger.__init__(self, name=name)    def _log(self, *args, extra=sentinel, **kwargs):        if extra == self.sentinel:            raise ValueError('Extra missed in logger call')        super()._log(*args, **kwargs)logging.setLoggerClass(MyLogger)logger = logging.getLogger(__name__)logger.error('With extra', extra={'meta': 'data'})logger.error('Explicit no extra', extra=None)logger.error('Error message without extra')# output:# With extra# Explicit no extra# Traceback (most recent call last):# ...# ValueError: Extra missed in logger call

При запуске тестов приложение будет падать на каждом вызове логера без extra.


Такая реализация выглядит опасно. Если мы не покроем тестами все строчки с логингом, мы рискуем получить исключение при логировании в продакшене. Да, неприятно попадать в непокрытую тестами строчку на бою, однако вряд ли бизнес согласен пятисотить при непереданном extra. Попробуем развить идею форсирования далее.


Статический анализ


Попробуем автоматически отслеживать наличие extra в вызовах логера до запуска приложения, основываясь на исходном коде. Кажется, что такое делается едва ли не грепом, но раз уж мы пишем на питоне, воспользуемся модулем ast. За несколько минут можно сделать наколеночный скрипт, и он в простейшем случае обнаружит все вызовы объекта logger, которым не был передан параметр extra.


Пример простейшей реализации
# linter.pyimport ast, syscode = open(sys.argv[1]).read()for node in ast.walk(ast.parse(code)):    # remember: not production-ready, just proof of concept    if isinstance(node, ast.Call) and not isinstance(node.func, ast.Name):        is_logger = node.func.value.id == 'logger'        if is_logger and 'extra' not in [k.arg for k in node.keywords]:            print(f'Missing extra at line {node.lineno}')# lint_example.pydef foobar():    log_extra = {'meta': 'data'}    bar()    logger.warning('Has no extra')    baz()    logger.warning('Has extra', extra=log_extra)    logger.warning('No extra again')# $ python3.7 linter.py lint_example.py# Missing extra at line 4# Missing extra at line 7

Вложив в него ещё несколько часов разработки, можно получить приемлемое для внедрения в CI решение. Однако у такого подхода есть два недостатка. Во-первых, существуют лог-записи без метаинформации (например, логи фреймворка до начала обработки запроса). Для таких вызовов разработчику придётся либо явно передавать extra=None, либо добавлять ignore-строку. Во-вторых, разработчики всё ещё вынуждены тащить log_extra сквозь сигнатуры всех методов.


Итог: вариант с линтером приемлем, но не идеален.


Автоматическое добавление extra


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


Синхронное однопоточное приложение


Первое решение, которое приходит в голову, глобальная переменная, мы сохраняем в неё контекст в начале обработки запроса и достаём его при необходимости что-то залогировать.


LOG_EXTRA = {}def set_log_extra(extra):    global LOG_EXTRA    LOG_EXTRA = extradef get_log_extra():    return LOG_EXTRAdef log(text):    extra_tskv = '\t'.join(f'{k}={v}' for k, v in get_log_extra().items())    msg = f'text="{text}"\t{extra_tskv}'    print(msg)@app.route('/feedback')def feedback_handler_sync_singlethreaded(request):    set_log_extra({ ... })    order = db.feedbacks.find_one(order_id=request.order_id)    if order is None:        log('user has no active orders in db')        return process_without_order(request)# output:# text="user has no active orders in db" handler=some-handler user_id=some-guid

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


Представим следующую ситуацию
  1. Начинаем обрабатывать запрос 1, с помощью set_log_extra сохраняем в глобальную переменную контекст запроса.


  2. Не находим заказ в базе наступает время логировать ошибку.


  3. В этот момент соседний поток начал выполнять запрос 2 и вызвал set_log_extra со своим контекстом.


  4. Поскольку память у потоков общая, set_log_extra обновит значение глобальной переменной для всех тредов.


  5. В результате обработчик запроса 1 залогирует ошибку с контекстом из запроса 2.



Получается, что всего лишь глобальной переменной недостаточно для многопоточных серверов.


Многопоточное приложение


Проблема с гонками имеет тривиальное решение: будем хранить глобальную переменную не в общей для потоков памяти, а в локальной памяти каждого треда. В UNIX для этого есть готовый механизм thread-local storage, а Python предоставляет аналогичную по смыслу функциональность через threading.local:


import threadingLOG_EXTRA = threading.local()LOG_EXTRA.data = {}def set_log_extra(extra):    global LOG_EXTRA    LOG_EXTRA.data = extradef get_log_extra():    return LOG_EXTRA.data# ...

В таком случае каждый поток будет работать со своим собственным значением поля LOG_EXTRA.data.


Однако в последнее время большинство новых бэкендов пишутся на асинхронных фреймворках вроде aiohttp. В мире event-loopов все запросы обрабатываются в разных корутинах, но в рамках одного потока. Это значит, что для асинхронных приложений threading.local не решит проблему: с точки зрения корутин мы продолжаем использовать единую глобальную переменную.


Асинхронное приложение


А если бы у нас был асинхронный аналог threading.local, то есть нечто, что может хранить состояние выполняемой в данном контексте задачи. В случае с asyncio некоторым аналогом тредов являются таски инстансы asyncio.Task, исполняющие корутины поверх event-loopа.


В используемом нами фреймворке aiohttp при выполнении конкретного запроса запускается много корутин, но по умолчанию все они будут выполняться в рамках одной asyncio.Task (если мы, конечно, не породим новые таски самостоятельно).


Кажется, что этим фактом можно воспользоваться: asyncio предоставляет интерфейс для получения текущей таски через asyncio.current_task, и мы можем сохранить контекст выполнения нашего запроса в ней через определение собственного атрибута:


import asyncio_log_extra_key = '_log_extra'def set_log_extra(log_extra, task=None):    if task is None:        task = asyncio.current_task()    setattr(task, _log_extra_key, log_extra)def get_log_extra(task=None):    if task is None:        task = asyncio.current_task()    return getattr(task, _log_extra_key, None)def log(text):    extra_tskv = '\t'.join(f'{k}={v}' for k, v in get_log_extra().items())    msg = f'text=""{text}""\t{extra_tskv}'    print(msg)async def feedback_handler_example():    set_log_extra({'handler': 'some-handler', 'user_id': 'some-guid'})    log(f'no explicit extra')asyncio.run(feedback_handler_async())# output:# text="no explicit extra" handler=some-handler user_id=some-guid

Однако такой подход не будет работать, если при выполнении запроса мы запустим ещё одну таску. В качестве примера попробуем параллельно сделать запрос в два микросервиса:


# ...async def call_foo():    # ...    log(f'service_foo')async def call_bar():    # ...    log(f'service_bar')async def feedback_handler_example():    set_log_extra({'handler': 'some-handler', 'user_id': 'some-guid'})    await asyncio.gather(        call_foo(),        call_bar(),    )    log(f'no explicit extra')asyncio.run(feedback_handler_example())# output:# text="service_foo"# text="service_bar"# text="no explicit extra" handler=some-handler user_id=some-guid

Дело в том, что вызов asyncio.gather порождает новую таску, для которой никто не вызывал set_log_extra, и, как следствие, у неё нет атрибута, хранящего extra-информацию. К счастью, asyncio позволяет кастомизировать механизм создания тасок для event-loopа при помощи loop.set_task_factory. Если при создании новой таски мы будем присваивать ей атрибут от родительской задачи, мы решим проблему теряющегося extra:


# ...def log_extra_factory():    default_task_factory = asyncio.Task    @functools.wraps(default_task_factory)    def custom_task_factory(loop, coro):        child_task = default_task_factory(coro, loop=loop)        if not loop.is_running():            return child_task        current_extra = get_log_extra()        set_log_extra(current_extra, task=child_task)        return child_task    return custom_task_factoryasync def feedback_handler_example():    set_log_extra({'handler': 'some-handler', 'user_id': 'some-guid'})    await asyncio.gather(        call_foo(),        call_bar(),    )    log(f'no explicit extra')async def main():    asyncio.get_event_loop().set_task_factory(log_extra_factory())    await feedback_handler_example()asyncio.run(main())# output:# text="fetch_data_from_db" handler=some-handler user_id=some-guid# text="fetch_data_from_service" handler=some-handler user_id=some-guid# text="no explicit extra" handler=some-handler user_id=some-guid

В целом мы уже получили достаточно интересное решение. Однако благодаря модулю contextvars, который появился в стандартной библиотеке Python 3.7, больше не нужно вручную задавать атрибуты тасок: желаемого результата можно добиться и без потенциально опасной работы с атрибутами.


Contextvars


В каком-то смысле контекстные переменные похожи на threadlocal-переменные: значение threading.local-переменной зависит от потока, а значение contextvars.ContextVar от активного сейчас контекста (инстанса contextvars.Context), коих может быть несколько в рамках одного потока:


from contextvars import ContextVar, Contextctx_var = ContextVar('ctx_var')def get_name():    # значение ctx_var будет зависеть от контекста    return ctx_var.get()for name in ['Alice', 'Bob']:    ctx = Context()    # ctx.run() запускает некоторый callable внутри контекста ctx    # устанавливаем значение ctx_var внутри контекста ctx    ctx.run(ctx_var.set, name)    # запускаем get_name внутри ctx    print(ctx.run(get_name))# output:# Alice# Bob

У contextvars есть два важных свойства, которые помогут нам решить задачу с log_extra:


Встроенная интеграция с asyncio


Начиная с Python 3.7 методы Loop.call_{at,later,soon} (а также Future.add_done_callback), ответственные за выполнение корутин в рамках event-loopа, принимают опциональный параметр context, в рамках которого потом будет выполняться корутина:


# asyncio pseudocodedef call_soon(self, callback, *args, context=None):    if context is None:        # use copy of current context by default        context = contextvars.copy_context()    # ... later    context.run(callback, *args)

Кроме того, теперь каждая таска по умолчанию выполняет все корутины в своём контексте, который по умолчанию равен копии текущего контекста:


# asyncio pseudocodeclass Task:    def __init__(self, coro):        ...        # Get the current context snapshot.        self._context = contextvars.copy_context()        self._loop.call_soon(self._step, context=self._context)    def _step(self, exc=None):        ...        # Every advance of the wrapped coroutine is done in        # the task's context.        self._loop.call_soon(self._step, context=self._context)        ...

Другими словами:


  • Все корутины в рамках таски выполняются в едином контексте.
  • Дочерние таски выполняются в контексте-копии текущей таски. Дети имеют доступ ко всем контекстным переменным родителя, однако модификация происходит исключительно в рамках локального контекста, без влияния на соседей.

Вот как это выглядит на практике:


import asyncio, contextvarsctx_var = contextvars.ContextVar('ctx_var')async def main():    ctx_var.set('aaa')    print(f'ctx_var in main: {ctx_var.get()}')    await same_task()    print(f'ctx_var in main after same_task call: {ctx_var.get()}')    await asyncio.Task(other_task())    print(f'ctx_var in main after asyncio.Task(other_task()) call: {ctx_var.get()}')async def same_task():    print(f'ctx_var in same_task: {ctx_var.get()}')    ctx_var.set('bbb')    print(f'ctx_var in same_task after set: {ctx_var.get()}')async def other_task():    print(f'ctx_var in other_task before set: {ctx_var.get()}')    ctx_var.set('ccc')    print(f'ctx_var in other_task after set: {ctx_var.get()}')asyncio.run(main())# output:# ctx_var in main: aaa# ctx_var in same_task: aaa# ctx_var in same_task after set: bbb# ctx_var in main after same_task call: bbb  # (1)# ctx_var in other_task before set: bbb  # (2)# ctx_var in other_task after set: ccc# ctx_var in main after asyncio.Task(other_task()) call: bbb  # (3)

Обратите внимание на три важных аспекта:


  1. Корутина same_task, выполняющаяся в одной таске с main, повлияла на значение ctx_var для них обеих, так как они выполняются в одном контексте.
  2. Корутина other_task, выполняющаяся в дочерней таске от main, унаследовала значение ctx_var, установленное в same_task.
  3. Однако модификация ctx_var внутри other_task не повлияла на значение внутри main, так как дочерняя таска копирует родительский контекст, что делает их независимыми.

Копирование контекста за O(1)


Поскольку каждое создание таски теперь приводит к копированию контекста, важно сделать копирование легковесным. Для этого модуль contextvars хранит элементы с помощью immutable-структуры данных HAMT (hash array mapped trie), для которой асимптотическая сложность копирования не зависит от размера контекста:


import contextvars, timeclass Timer:    def __enter__(self):        self.start = time.perf_counter()    def __exit__(self, *args):        print(time.perf_counter() - self.start)LOOP_COUNT = 1000CTXVAR_COUNT = 42000print(f'Context with {len(contextvars.copy_context())} variables')with Timer():    for _ in range(LOOP_COUNT):        contextvars.copy_context()cvars = [contextvars.ContextVar(f'cvar_{i}', default=i) for i in range(CTXVAR_COUNT)]for v in cvars:    v.set('somevalue')print(f'Context with {len(contextvars.copy_context())} variables')with Timer():    for _ in range(LOOP_COUNT):        contextvars.copy_context()# output# Context with 0 variables# 0.00015302500000000108# Context with 42000 variables# 0.00015314099999999835

Применяем contextvars к extra


Поскольку нам больше не нужно вручную задавать атрибуты для тасок, мы можем избавиться от кастомной фактори функции get_log_extra и set_log_extra становятся обёртками над методами контекстной переменной:


import asyncioimport functoolsimport contextvars_log_extra_data = contextvars.ContextVar('_log_extra_data')def set_log_extra(log_extra):    _log_extra_data.set(log_extra)def get_log_extra():    return _log_extra_data.get()def log(text):    extra_tskv = '\t'.join(f'{k}={v}' for k, v in get_log_extra().items())    msg = f'text="{text}"\t{extra_tskv}'    print(msg)async def call_foo():    log(f'service_foo')async def call_bar():    log(f'service_bar')async def feedback_handler_example():    set_log_extra({'handler': 'some-handler', 'user_id': 'some-guid'})    await asyncio.gather(        call_foo(),        call_bar(),    )    log(f'no explicit extra')async def main():    await feedback_handler_example()asyncio.run(main())# output# text="service_foo"      handler=some-handler    user_id=some-guid# text="service_bar"      handler=some-handler    user_id=some-guid# text="no explicit extra"        handler=some-handler    user_id=some-guid

Выглядит сильно лучше. Однако перед внедрением в продакшен необходима пара финальных штрихов.


Изменить extra для конкретной таски


Важно помнить, что копирование контекста не влечёт за собой глубокого копирования объектов. Это значит, что изменение контекстных переменных мутабельных типов будет влиять на родительский контекст. Представим, что после обращения к стороннему сервису мы хотим дополнительно логировать response_id в качестве extra-информации:


async def external_call():    # ...    get_log_extra()['response_id'] = 'some_external_response_id'    log('log from external call')async def feedback_handler_example():    set_log_extra({'handler': 'some-handler', 'user_id': 'some-guid'})    await asyncio.Task(external_call())    log('log from parent task ')async def main():    await feedback_handler_example()asyncio.run(main())# output# text="log from external call"  handler=some-handler    user_id=some-guid       response_id=some_external_response_id# text="log from parent task"     handler=some-handler    user_id=some-guid       response_id=some_external_response_id

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


import copy# ...def get_log_extra(should_copy=False):    extra = _log_extra_data.get()    if should_copy:        return copy.deepcopy(extra)    return extradef update_log_extra(update):    current_extra = get_log_extra(should_copy=True)    set_log_extra({**current_extra, **update})async def external_call():    # ...    update_log_extra({'response_id': 'some_external_response_id'})    log('log from external call')async def feedback_handler_example():    set_log_extra({'handler': 'some-handler', 'user_id': 'some-guid'})    await asyncio.Task(external_call())    log(' log from parent task)async def main():    await feedback_handler_example()asyncio.run(main())# output# text="log from external call"  handler=some-handler    user_id=some-guid       response_id=some_external_response_id# text="log from parent task"     handler=some-handler    user_id=some-guid

Контекстные переменные и тредпул


Периодически при написании асинхронных приложений требуется отдать какую-то функцию на выполнение в тредпул (например, из-за отсутствия асинхронного аналога у библиотеки). Здесь нас ждёт неприятный сюрприз: потоки из пула по умолчанию работают в собственном контексте, в котором может и не быть созданных в основном потоке переменных:


import asyncioimport functoolsimport contextvars_log_extra_data = contextvars.ContextVar('_log_extra_data')# ...def log(text):    extra_tskv = '\t'.join(f'{k}={v}' for k, v in get_log_extra().items())    msg = f'text="{text}"\t{extra_tskv}'    print(msg)async def feedback_handler_example():    set_log_extra({'handler': 'some-handler', 'user_id': 'some-guid'})    await asyncio.get_event_loop().run_in_executor(None, log, 'some-info')    log(f'no explicit extra')async def main():    await feedback_handler_example()asyncio.run(main())# output:# Traceback (most recent call last):# ...#   File "contextvars_threadpool.py", line 11, in get_log_extra#     return _log_extra_data.get()# LookupError: <ContextVar name='_log_extra_data' at 0x1101a63b0>

В потоке из пула переменной _log_extra_data ни разу не присваивалось значение, из-за чего метод get() поднимает исключение LookupError. Для нашей задачи такое поведение кажется сомнительным: по умолчанию хотелось бы, чтобы потоки в пуле вели себя так же, как и дочерние таски, т. е. наследовали контекст родителя. Определим свой собственный пул, выполняющий target-функцию в копии текущего контекста, и зададим его пулом по умолчанию для event-loopа:


import asyncioimport concurrent.futuresimport contextvarsimport functoolsimport typingclass CtxCopyThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):    def submit(            self, fn: typing.Callable, *args, **kwargs,    ) -> concurrent.futures.Future:        ctx = contextvars.copy_context()        return super().submit(            ctx.run,            functools.partial(fn, *args, **kwargs),        )# ...async def feedback_handler_example():    set_log_extra({'handler': 'some-handler', 'user_id': 'some-guid'})    await asyncio.get_event_loop().run_in_executor(None, log, 'some-info')    log(f'no explicit extra')async def main():    asyncio.get_event_loop().set_default_executor(CtxCopyThreadPoolExecutor())    await feedback_handler_example()asyncio.run(main())# text="from threadpool"        handler=some-handler    user_id=some-guid# text="no explicit extra"      handler=some-handler    user_id=some-guid

Итоги


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


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


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

Подробнее..

Разрабатываем самый удобный в мире интерфейс для просмотра логов

22.07.2020 18:16:54 | Автор: admin

Если Вам приходилось когда-нибудь пользоваться веб-интерфейсами для просмотра логов, то Вы наверняка замечали, насколько, как правило, эти интерфейсы громоздки и (зачастую) не слишком-то удобны и отзывчивы. К некоторым можно привыкнуть, некоторые совсем ужасны, но, как мне кажется, причина всех проблем заключается в том, что мы неправильно подходим к задаче просмотра логов: мы пытаемся создать веб-интерфейс там, где лучше работает CLI (интерфейс командной строки). Мне лично очень комфортно работать с tail, grep, awk и прочими, и поэтому для меня идеальным интерфейсом для работы с логами было бы что-то аналогичное tail и grep, но которое при этом можно было использовать для чтения логов, которые пришли с множества серверов. То есть, очевидно, читать их из ClickHouse :).


*по личному мнению хабрапользователя youROCK


Встречайте logscli


Я не придумал названия своему интерфейсу, и, сказать честно, он скорее существует в виде прототипа, но если Вы хотите сразу посмотреть исходники, то добро пожаловать: https://github.com/YuriyNasretdinov/logscli (350 строк отборного кода на Go).


Возможности


Я ставил перед собой цель сделать интерфейс, который будет казаться знакомым тем, кто привык к tail/grep, то есть поддерживать следующие вещи:


  1. Просмотр всех логов, без фильтрации.
  2. Оставить строки, в которых содержится фиксированная подстрока (флаг -F у grep).
  3. Оставить строки, подходящие под регулярное выражение (флаг -E у grep).
  4. По умолчанию просмотр в обратном хронологическом порядке, поскольку обычно в первую очередь интересны самые свежие логи.
  5. Показ контекста возле каждой строки (параметры -A, -B и -C у grep, печатающие N строк до, после, и вокруг каждой совпадающей строки соответственно).
  6. Просмотр поступающих логов в режиме реального времени, с фильтрацией и без (по сути tail -f | grep).
  7. Интерфейс должен быть совместим с less, head, tail и прочими по умолчанию должны возвращаться результаты без ограничений по их количеству; строки печатаются потоково до тех пор, пока пользователь заинтересован в их получении; сигнал SIGPIPE должен молча прерывать стриминг логов, также, как это делают tail, grep и остальные UNIX-утилиты.

Реализация


Я буду предполагать, что вы уже каким-то образом умеете доставлять логи до ClickHouse. Если нет, то рекомендую попробовать lsd и kittenhouse, а также эту статью про доставку логов.


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


То есть, нам нужна примерно следующая схема таблиц:


CREATE TABLE logs(    category LowCardinality(String), -- категория логов (опционально)    time DateTime, -- время события    millis UInt16, -- миллисекунды (могут быть и микросекунды, и т.д.): рекомендуется хранить, если событий много, чтобы было легче различать события между собой    ..., -- ваши собственные поля, например имя сервера, уровень логирования, и так далее    message String -- текст сообщения) ENGINE=MergeTree()ORDER BY (category, time, millis)

К сожалению, я не смог сразу найти каких-либо открытых источников с реалистичными логами, которые можно было бы взять и скачать, поэтому я взял вместо этого для примера отзывы от товарах из Amazon до 2015 года. Безусловно, их структура не совсем такая же, как у текстовых логов, но для иллюстрации это не принципиально.


инструкция по заливке отзывов Amazon в ClickHouse

Создадим таблицу:


CREATE TABLE amazon(   review_date Date,   time DateTime DEFAULT toDateTime(toUInt32(review_date) * 86400 + rand() % 86400),   millis UInt16 DEFAULT rand() % 1000,   marketplace LowCardinality(String),   customer_id Int64,   review_id String,   product_id LowCardinality(String),   product_parent Int64,   product_title String,   product_category LowCardinality(String),   star_rating UInt8,   helpful_votes UInt32,   total_votes UInt32,   vine FixedString(1),   verified_purchase FixedString(1),   review_headline String,   review_body String)ENGINE=MergeTree()ORDER BY (time, millis)SETTINGS index_granularity=8192

В датасете Амазона есть только дата для отзыва, но нет точного времени, поэтому заполним эти данные рандоном.


Можно не скачивать все tsv-файлы и ограничиться первыми ~10-20, чтобы уже получить достаточно большой набор данных, который не влезет в 16 Гб оперативной памяти. Для заливки TSV-файлов я использовал следующую команду:


for i in *.tsv; do    echo $i;    tail -n +2 $i | pv |    clickhouse-client --input_format_allow_errors_ratio 0.5 --query='INSERT INTO amazon(marketplace,customer_id,review_id,product_id,product_parent,product_title,product_category,star_rating,helpful_votes,total_votes,vine,verified_purchase,review_headline,review_body,review_date) FORMAT TabSeparated'done

На стандартном Persistent Disk (который HDD) в Google Cloud размером в 1000 Гб (такой размер я взял в основном для того, чтобы скорость была чуть выше, хотя, возможно SSD нужного объема вышел бы дешевле :)) скорость заливки составила примерно ~75 Мб/сек на 4 ядрах.


  • должен оговориться, что я работаю в Google, но я использовал личный аккаунт и эта статья к моей работе в компании отношения не имеет

Все иллюстрации я буду производить именно с этим датасетом, поскольку это всё, что у меня было под рукой :)).


Показ прогресса сканирования данных


Поскольку в ClickHouse мы будем использовать full scan по таблице с логами, а эта операция может занимать существенное время и долго не отдавать никаких результатов, если совпадений найдено мало, желательно уметь показывать прогресс выполнения запроса до получения первых строк с результатом. Для этого в HTTP-интерфейсе есть параметр, позволяющий отдавать прогресс в заголовках HTTP: send_progress_in_http_headers=1. К сожалению, стандартная библиотека Go не умеет читать заголовки по мере их получения, но интерфейс HTTP 1.0 (не путайте с 1.1!) поддерживается ClickHouse, поэтому можно открыть сырое TCP-соединение с ClickHouse, послать туда GET /?query=... HTTP/1.0\n\n и получить в ответ заголовки и тело ответа без какого-либо экранирования и шифрования, так что нам в этом случае стандартную библиотеку использовать даже не требуется.


Стриминг логов из ClickHouse


В ClickHouse уже относительно давно (с 2019 года?) есть оптимизация для запросов с ORDER BY, так что запрос вида


SELECT time, millis, messageFROM logsWHERE message LIKE '%something%'ORDER BY time DESC, millis DESC

Начнет сразу возвращать строки, у которых в message есть подстрока "something", не дожидаясь окончания сканирования.


Также, было бы очень удобно, если бы ClickHouse сам отменял запрос, когда с ним закрыли соединение, но это не является поведением по умолчанию. Автоматическую отмену запроса можно включить опцией cancel_http_readonly_queries_on_client_close=1.


Корректная обработка SIGPIPE в Go


Когда Вы выполняете, скажем, команду some_cmd | head -n 10, каким именно образом команда some_cmd прекращает свое исполнение, когда head вычитал 10 строк? Ответ прост: когда head завершается, pipe закрывается, и stdout команды some_cmd начинает указывать, условно, вникуда. Когда some_cmd пытается записать в закрытый pipe, [https://www.quora.com/What-are-SIGPIPEs](ей приходит сигнал SIGPIPE, который по умолчанию молча завершает программу).


В Go по умолчанию это тоже происходит, но обработчик сигнала SIGPIPE в конце также печатает "signal: SIGPIPE" или похожее сообщение, и чтобы это сообщение убрать, нужно просто самому обработать SIGPIPE так, как мы хотим, то есть просто молча выйти:


ch := make(chan os.Signal)signal.Notify(ch, syscall.SIGPIPE)go func() {    <-ch    os.Exit(0)}()

Показ контекста сообщения


Часто хочется увидеть контекст, в котором произошла какая-то ошибка (например, какой запрос вызвал панику, или какие сопутствующие проблемы были видны до падения), и в grep для этого служат опции -A, -B и -C, которые показывают указанное число строк после, до, и вокруг сообщения соответственно.


К сожалению, я не нашел простого способа сделать то же самое в ClickHouse, поэтому, для отображения контекста, на каждую строчку результата посылается дополнительный запрос примерно следующего вида (детали зависят от сортировки и от того, показывается контекст до или после):


SELECT time,millis,review_body FROM amazonWHERE (time = 'ВРЕМЯ_СОБТИЯ' AND millis < МИЛЛИСЕКУНД_СОБТИЯ) OR (time < 'ВРЕМЯ_СОБТИЯ')ORDER BY time DESC, millis DESCLIMIT КОЛИЧЕСТВО_СТРОК_КОНТЕКСТАSETTINGS max_threads=1

Поскольку запрос посылается почти сразу после того, как ClickHouse вернул соответствующую строку, она попадает в кеш и в целом запрос выполняется достаточно быстро и тратит немножко CPU (обычно запрос занимает около ~6 мс на моей виртуалке).


Показ новых сообщений в режиме реального времени


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


Примеры команд


Как выглядят типичные команды logscli на практике?


Если Вы загрузили датасет Amazon, который я упоминал в начале статьи, то сможете выполнить следующие команды:


# Показать строки, где встречается слово walmart$ logscli -F 'walmart' | less# Показать самые свежие 10 строк, где встречается "terrible"$ logscli -F terrible -limit 10# То же самое без -limit:$ logscli -F terrible | head -n 10# Показать все строки, подходящие под /times [0-9]/, написанные для vine и у которых высокий рейтинг$ logscli -E 'times [0-9]' -where="vine='Y' AND star_rating>4" | less# Показать все строки со словом "panic" и 3 строки контекста вокруг$ logscli -F 'panic' -C 3 | less# Непрерывно показывать новые строки со словом "5-star"$ logscli -F '5-star' -tailf

Ссылки


Код утилиты (без документации) доступен на github по адресу https://github.com/YuriyNasretdinov/logscli. Буду рад услышать Ваши мысли по поводу моей идеи для консольного интерфейса для просмотра логов на основе ClickHouse.

Подробнее..

Требования к разработке приложения в Kubernetes

06.08.2020 16:13:59 | Автор: admin

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


Эта лекция в рамках Вечерней школы Слёрма по Кубернетес. Вы можете просмотреть открытые теоретические лекции Вечерней Школы на Youtube, сгруппированные в плейлист. Для тех же, кому удобнее текст, а не видео, мы подготовили эту статью.


Зовут меня Павел Селиванов, на текущий момент я являюсь ведущим DevOps инженером компании Mail.ru Cloud Solutions, мы делаем облака, мы делаем мэнедж-кубернетисы и так далее. В мои задачи сейчас как раз-таки входит помощь в разработке, раскатывание эти облаков, раскатывание приложения, которые мы пишем и непосредственно разработка инструментария, который мы предоставляем для наших пользователей.



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


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


Если говорить по плану того, о чем я буду рассказывать, это выглядит вот так, в скобочках написано (TL;DR) too long; dont read. Моя сегодняшняя презентация будет представлять из себя бесконечные списки.



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


Потому что по большому счету эта информация это ctrl+c, ctrl+v, из, в том числе нашей Вики в разделе DevOps, где у нас написаны требования к разработчикам: ребята, чтобы ваше приложение мы запустили в Kubernetes, оно должно быть вот таким.


Поэтому получилась презентация таким большим списком. Извините. Постараюсь рассказывать максимально, чтобы было нескучно по возможности.


Что мы с вами сейчас будем разбирать:


  • это, во-первых, логи (журналы приложения?), что с ними делать в Kubernetes, как с ними быть, какими они должны быть;
  • что делать с конфигурациями в Kubernetes, какие для Kubernetes лучшие-худшие способы конфигурировать приложение;
  • поговорим о том, что такое проверки доступности вообще, как они должны выглядеть;
  • поговорим о том, что такое graceful shutdown;
  • поговорим еще раз про ресурсы;
  • еще раз затронем тему хранения данных;
  • и в конце я расскажу, что такое термин этот загадочный cloud-native приложение. Cloudnativeness, как прилагательное от этого термина.

Логи


Я предлагаю начать с логов с того, куда эти логи нужно в Kubernetes пихать. Вот вы запустили в Kubernetes приложение. По классике, раньше приложения всегда писали логи куда-нибудь в файлик. Плохие приложения писали логи в файлик в домашней директории разработчика, который запустил это приложение. Хорошие приложения писали логи в файлик куда-нибудь в /var/log.



Соответственно, дальше, у хороших админов в инфраструктурах были настроены какие-нибудь штуки, которые эти логи могут ротировать тот же самый rsyslog элементарно, который смотрит на эти логи и когда с ними что-то происходит, их становится много, он создает резервные копии, складывает туда логи, удаляет старые файлики, больше, чем за неделю, за полгода и еще за сколько-нибудь. По идее, у нас должно быть предусмотрено, чтобы просто тупо из-за того, что приложение пишет логи, место на серверах продакшна (боевых серверах?) не кончалось. И, соответственно, весь продакшн не останавливался из-за логов.


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


Оказывается, если мы будем говорить про Kubernetes, что правильным местом для того, чтобы из докер-контейнера куда-то написать логи, это просто писать их из приложения в так называемые Stdout/Stderr, то есть потоки стандартного вывода операционной системы, стандартного вывода ошибок. Это самый правильный, самый простой и самый логичный способ, куда девать логи в принципе в докере и конкретно в Кубернетисе. Потому что, если ваше приложение пишет логи в Stdout/Stderr, то дальше это уже задача докера и надстройки над ним Kubernetes, что с этими логами делать. Докер по умолчанию будет складывать свои специальные файлики в JSON формате.


Тут уже возникает вопрос, что вы с этими логами будете делать дальше. Самый простой способ, понятно, у нас есть возможность сделать kubectl logs и посмотреть эти логи этих подов. Но, наверное, это не очень хороший вариант с логами нужно что-то дальше делать.


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


Нам нужен какой-то инструмент, по-хорошему, который эти логи, которые у нас докер складывает в свои файлики, возьмет и куда-то их отправит. По большому счету, обычно мы внутри Kubernetes запускаем в виде DaemonSet какой-нибудь агент сборщик логов, которому просто сказано, где лежат логи, которые складывает докер. И этот агент-сборщик их просто берет, возможно, даже по пути как-то парсит, возможно обогащает какой-то дополнительной метаинформацией и, в итоге, отправляет на хранение куда-то. Там уже возможны вариации. Самое распространенное, наверное, Elasticsearch, где можно хранить логи, их можно оттуда удобно доставать. Потом с помощью запроса, с помощью Kibana, например, строить по ним графики, строить по ним алерты и так далее.


Самая главная мысль, еще раз ее хочу повторить, о том, что внутри докера, в частности, внутри Kubernetes, складывать ваши логи в файлик это очень плохая идея.


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


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


Следующий момент, тут я хочу опять же об этом поговорить раз уж мы затрагиваем тему логов, то хорошо бы поговорить и о том, как логи должны выглядеть, для того, чтобы с ними было удобно работать. Как я говорил, тема не относится напрямую к Kubernetes, но зато очень хорошо относится к теме DevOps. К теме культуры разработки и дружбы двух этих разных отделов Dev и Ops, чтобы всем было удобно.


Значит в идеале, на сегодняшний день, логи нужно писать в JSON формате. Если у вас какое-нибудь непонятное приложение ваше самописное, которое пишет логи в непонятных форматах, потому что вы там вставляете какой-нибудь print или что-то типа того, то самое время уже нагуглить какой-нибудь фрэймворк, какую-нибудь обертку, которая позволяет реализовывать нормальное логирование; включить там параметры логирования в JSON, потому что JSON простой формат, его парсить просто элементарно.


Если у вас JSON не прокатывает по каким-нибудь критериям, неизвестно каким, то хотя бы пишите логи в том формате, который можно парсить. Тут, скорее, стоит задумываться о том, что, например, если у вас запущена куча каких-нибудь контейнеров или просто процессов с nginx, и у каждого свои настройки логирования, то, наверное, кажется, вам парсить их будет очень неудобно. Потому что на каждую новый nginx instance вам нужно написать собственный парсер, потому что они пишут логи по-разному. Опять же, наверное, тут стоило задуматься о том, чтобы все эти nginx instance имели одну и ту же конфигурацию логирования, и абсолютно единообразно писали все свои логи. То же самое касается абсолютно всех приложений.


Я в итоге еще хочу масла в огонь добавить, о том, что в идеале, логов многострочного формата стоит избегать. Тут какая штука, если вы когда-то работали со сборщиками логов, то, скорее всего, вы видели, что они вам обещают, что они умеют работать с многострочными логами, умеют их собирать и так далее. На самом деле, нормально, полноценно и без ошибок собирать многострочные логи, не умеет, по-моему, на сегодняшний день ни один сборщик. По-человечески, чтобы это было удобно и без ошибок.



Но stack trace это всегда многострочные логи и как их избегать. Тут вопрос в том, что лог это запись о событии, а стактрэйс фактически логом не является. Если мы логи собираем и складываем их куда-нибудь в Elasticsearch и по ним потом рисуем графики, строим какие-нибудь отчеты работы пользователей на вашем сайте, то когда у вас вылазит stack trace это значит, что у вас происходит какая-то непредвиденная, необработанная ситуация в вашем приложении. И stack trace имеет смысл закидывать автоматически куда-нибудь в систему, которая их умеет трэкать.


Это то ПО (то же Sentry), которое сделано специально для того, чтобы работать со stack trace. Оно может создавать сразу же автоматизированно задачи, назначать на кого-то, аллертить, когда стактрейсы происходят, группировать эти стактрэйсы по одному типу и так далее. В принципе, не имеет особого смысла говорить о стактрэйсах, когда мы говорим о логах, потому что это, все-таки, разные вещи с разным предназначением.


Конфигурация


Дальше у нас по поводу конфигурации в Kubernetes: что с этим делать и как приложения внутри Kubernetes должны конфигурироваться. Вообще, я обычно говорю о том, что докер он не про контейнеры. Все знают, что докер это контейнеры, даже те, кто особо с докером не работал. Повторюсь, Докер это не про контейнеры.


Докер, по моему мнению, это про стандарты. И там стандарты всего практически: стандарты сборки вашего приложения, стандарты установки вашего приложения.



И эта штука мы ей пользовались и раньше, просто с приходом контейнеров это стало особо популярно эта штука называется ENV (environment) переменные, то есть переменные окружения, которые есть в вашей операционной системе. Это вообще идеальный способ конфигурировать ваше приложение, потому что, если у вас есть приложения на JAVA, Python, Go, Perl не дай бог, и они все могут читать переменные database host, database user, database password, то идеально. У вас приложения на четырех разных языках конфигурируются в плане базы данных одним и тем же способом. Нет больше никаких разных конфигов.


Все можно сконфигурировать с помощью ENV переменных. Когда мы говорим про Kubernetes, там есть прекрасный способ объявлять ENV переменные прямо внутри Deployment. Соотвественно, если мы говорим про секретные данные, то секретные данные из ENV переменных (пароли к базам данных и т.д.), мы можем сразу запихать в секрет, создать секрет кластер и в описании ENV в Deployment указать, что мы не непосредственно объявляем значение этой переменной, а значение этой переменной database password будем читать из секрета. Это стандартное поведение Kubernetes. И это самый идеальный вариант конфигурировать ваши приложения. Просто на уровне кода, опять же к разработчикам это относится. Если вы DevOps, можно попросить: Ребята, пожалуйста, научите ваше приложение читать переменные окружения. И будет нам счастье всем.


Если еще и все будут читать одинаково названные переменные окружения в компании, то это вообще шикарно. Чтобы не было такого, что одни ждут postgres database, другие database name, третьи database еще что-нибудь, четвертые dbn какой-нибудь там, чтобы, соответственно, единообразие было.


Проблема наступает, когда у вас переменных окружения становится так много, что просто открываешь Deployment а там пятьсот строчек переменных окружения. В таком случае, переменные окружения вы уже просто переросли и дальше уже не надо себя мучить. В таком случае, имело бы смысл начать пользоваться конфигами. То есть, обучить ваше приложение использовать конфиги.


Только вопрос в том, что конфиги это не то, что вы думаете. Config.pi это не тот конфиг, которым удобно пользоваться. Или какой-нибудь конфиг в вашем собственном формате, альтернативно одаренном это тоже не тот конфиг, который я подразумеваю.


То, о чем я говорю, это конфигурация в приемлемых форматах, то есть, на сегодняшний день самым популярным стандартом является стандарт .yaml. Его понятно, как читать, он человекочитаемый, его понятно, как читать из приложения.


Соответственно, помимо YAML, можно еще, например, попользоваться JSON, парсить примерно так же удобно, как YAML в плане чтения оттуда конфигурации приложения. Читать людьми заметно неудобнее. Можно попробовать формат, а-ля ini. Его читать прям совсем удобно, с точки зрения человека, но его может быть неудобно автоматизированно обрабатывать, в том плане, если вы когда-то захотите генерить свои конфиги, вот ini формат уже может быть неудобно генерить.


Но в любом случае, какой бы формат вы ни выбрали, суть в том, что с точки зрения Kubernetes это очень удобно. Вы весь свой конфиг можете положить внутри Kubernetes, в ConfigMap. И потом этот configmap взять и попросить внутри вашего пода замонтировать в какую-нибудь определенную директорию, где ваше приложение будет конфигурацию из этого configmap читать так, как будто бы это просто файлик. Это, собственно, то, что хорошо делать, когда у вас в приложении становится конфигурационных опций много. Или просто структура какая-нибудь сложная, вложенность есть.


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


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


Health check


Следующий момент это о такая штука, которая называется Health check. Вообще, Health check это просто проверка того, что ваше приложение работает. При этом мы чаще всего говорим о неких веб-приложениях, у которых, соответственно, с точки зрения хэлсчека(лучше не переводить здесь и далее) это будет какой-нибудь специальный URL, который они обрабатывают стандартно, обычно делают /health.


При обращении на этот URL, соответственно, наше приложение говорит или да, окей, у меня все хорошо 200 или нет, у меня всё не хорошо, какой-нибудь 500. Соответственно, если у нас приложение не http, не веб-приложение, мы сейчас говорим какой-нибудь демон, мы можем придумать, как делать хэлсчеки. То есть, необязательно, если приложение не http, то все работает без хэлсчека и сделать это никак нельзя. Можно обновлять периодически информацию в файлике какую-нибудь, можно придумать к вашему daemon специальную какую-нибудь команду, типа, daemon status, которая будет говорить да, все нормально, daemon работает, он живой.


Для чего это нужно? Первое самое очевидное, наверное, для чего нужен хэлсчек для того, чтобы понимать, что приложение работает. То есть, просто тупо, когда оно сейчас поднято, оно выглядит как рабочее, чтобы точно быть уверенным, что оно работает. А то получается так, что под с приложением запущен, контейнер работает, инстенс работает, все нормально а там пользователи уже оборвали все телефоны у техподдержки и говорят вы что там, ..., заснули, ничего не работает.


Вот хэлсчек это как раз-таки такой способ увидеть с точки зрения пользователя, что оно работает. Один из способов. Давайте говорить так. С точки зрения Kubernetes, это еще и способ понимать, когда приложение запускается, потому что мы понимаем, что есть разница между тем, когда запустился контейнер, создался и стартанул и когда непосредственно в этом контейнере запустилось приложение. Потому что если мы возьмем какое-нибудь среднестатистическое java-приложение и попробуем его запустить в доке, то секунд сорок, а то и минуту, а то и десять, оно может прекрасно стартовать. При этом можно хоть обстучаться в его порты, оно там не ответит, то есть оно еще не готово принимать трафик.


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



То, о чем я говорю сейчас, называется Readiness/Liveness пробы в рамках Kubernetes, соответственно, readiness пробы у нас как раз-таки отвечают за доступность приложения в балансировке. То есть если readiness пробы выполняются в приложении, то значит все окей, на приложение идет клиентский трафик. Если readiness пробы не выполняются, то приложение просто не участвует, конкретно этот instance, не участвует в балансировке, оно убирается с балансировки, клиентский трафик не идет. Соответственно, Liveness пробы в рамках Kubernetes нужны для того, чтобы в случае, если приложение залипло, его рестартануть. Если liveness проба не работает у приложения, которое объявлено в Kubernetes, то приложение не просто убирается с балансировки, оно именно рестартится.


И тут такой важный момент, о котором хотелось бы сказать, с точки зрения практики, обычно все-таки чаще используется и чаще нужна readiness проба, чем liveness проба. То есть просто бездумно объявлять и readiness, и liveness пробы, потому что Kubernetes так умеет, а давайте использовать всё, что он умеет не очень хорошая идея. Объясню почему. Потому что есть пунктик номер два в пробах это о том, что неплохо было бы проверять нижележащий сервис в ваших хэлсчеках. Это значит, что если у вас есть веб-приложение, отдающее какую-то информацию, которую в свою очередь оно, естественно, должно где-то взять. В базе данных, например. Ну, и сохраняет в эту же базу данных информацию, которая в это REST API поступает. То соответственно, если у вас хэлсчек отвечает просто типа обратились на слешхэлс, приложение говорит 200, окей, все хорошо, а при этом у вас у приложения база данных недоступна, а приложение на хэлсчек говорит 200, окей, все хорошо это плохой хэлсчек. Так не должно работать.


То есть, ваше приложение, когда к нему приходит запрос на /health, оно не просто отвечает, 200, ок, оно сначала сначала идет, например, в базу данных, пробует подключиться к ней, делает там что-нибудь совсем элементарное, типа селект один, просто проверяет, что в базе данных коннект есть и в базу данных можно выполнить запрос. Если это все прошло успешно, то отвечает 200, ок. Если не прошло успешно, то говорит, что ошибка, база данных недоступна.


Поэтому, в связи с этим, опять же возвращаюсь к Readiness/Liveness пробам почему readiness проба скорее всего вам нужна, а liveness проба под вопросом. Потому что, если вы будете описывать хэлсчеки именно так, как я сейчас сказал, то получится у нас недоступна в части instanceв или со всех instanceв база данных, к примеру. Когда у вас объявлена readiness проба, у нас хэлсчеки начали фэйлиться, и приложения соответственно все, с которых недоступна база данных, они выключаются просто из балансировки и фактически висят просто в запущенном состоянии и ждут, пока у них базы данных заработают.


Если у нас объявлена liveness проба, то представьте, у нас сломалась база данных, а у вас в Kubernetes половина всего начинает рестартовать, потому что liveness-проба падает. Это значит, нужно рестартовать. Это совсем не то, что вы хотите, у меня даже был в практике личный опыт. У нас было приложение, которое было чатом и которое было написано в JS и входило в базу данных Mongo. И была как раз-таки проблема в том, что это было на заре моей работы с Kubernetes, мы описывали readiness, liveness пробы по принципу того, что Kubernetes умеет значит будем использовать. Соответственно, в какой-то момент Mongo немножко затупила и проба начала фэйлиться. Соответственно, по ливнесс пробе поды начали убиваться.


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


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


Поэтому, readiness и liveness пробы это разное, даже более того, можно теоретически делать разные хэлсчеки, один типа рэди, один типа лив, например, и проверять разные вещи. На readiness пробах проверять бэкенды ваши. А на liveness пробе, например, не проверять, с точки зрения того, что liveness пробы это вообще просто приложение отвечает, если вообще оно в состоянии ответить.


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


По поводу того, чем нужно отвечать, когда у вас происходит проба, когда вы делаете хэлсчеки. Просто это боль на самом деле. Те, кто знаком с этим, наверное, будут смеяться но серьезно, я видел сервисы в своей жизни, которые в ответ на запросы в стопроцентных случаях отвечают 200. То есть, который успешен. Но при этом в теле ответа они пишут ошибка такая-то.


То есть статус ответа вам приходит все успешно. Но при этом вы должны пропарсить тело, потому что в теле написано извините, запрос завершился с ошибкой и это прям реальность. Это я видел в реальной жизни.


И чтобы кому-то не было смешно, а кому-то очень больно, все-таки стоит придерживаться простого правила. В хэлсчеках, да и в принципе в работе с веб-приложениями.


Если все прошло успешно, то отвечайте двухсотым ответом. В принципе, любой двухсотый ответ устроит. Если вы очень хорошо читали рэгси и знаете, что одни статусы ответа отличаются от других, отвечайте подходящим: 204, 5, 10, 15, что угодно. Если не очень хорошо, то просто два ноль ноль. Если все пошло плохо и хэлсчек не отвечает, то отвечаете любым пятисотым. Опять же, если вы понимаете, как нужно отвечать, чем отличаются разные статусы ответа друг от друга. Если не понимаете, то 502 ваш вариант отвечать на хэлсчеки, если что-то пошло не так.


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


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


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


Дальше у нас тоже одна из больных тем при запуске приложений.


На самом деле, это не только Kubernetes касается по большому счету, просто так совпало, что культура какая-то разработки массово и DevOps в частности начала распространяться примерно в то же время, что и Kubernetes. Поэтому по большому счету, что оказывается нужно корректно (graceful) завершать работу вашего приложения и без Kubernetes. И до Kubernetes люди так делали, но просто с приходом Kubernetes мы про это начали массово говорить.


Graceful Shutdown


В общем, что такое Graceful Shutdown и вообще для чего это нужно. Это о том, что когда ваше приложение по какой-то причине завершает свою работу, вам нужно выполнить app stop или вы получаете, например, сигнал операционной системы, ваше приложение его должно понимать и что-то с этим делать. Самый плохой вариант, конечно, это когда ваше приложение получает SIGTERM и такой типа SIGTERM, висим дальше, работаем, ничего не делаем. Это прям откровенно плохой вариант.



Практически настолько же плохой вариант, это когда ваше приложение получает SIGTERM и такой типа сказали же сегтерм, все значит завершаемся, не видел, не знаю никаких пользовательских запросов, не знаю что у меня там за запросы сейчас в работе, сказали SIGTERM, значит завершаемся. Это тоже плохой вариант.


А какой вариант хороший? Первым пунктом, учитываем завершение операций. Хороший вариант это когда ваш сервер все-таки учитывает то, что он делает, если ему приходит SIGTERM.


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


С точки зрения Kubernetes, как это выглядит. Когда мы говорим поду, который работает в кластере Kubernetes, пожалуйста, остановись, удались или у рестарт нас происходит, или обновление, когда Kubernetes пересоздает поды Kubernetes присылает в под сообщение как раз-таки SIGTERM, ждет какое-то время, причем, вот это время, которое он ждет, оно тоже конфигурируется, есть такой специальный параметр в диплойментах и он называется Graceful ShutdownTimeout. Как вы понимаете, неспроста он так называется и неспроста мы об этом сейчас говорим.


Там можно конкретно сказать, сколько нужно подождать между тем, когда мы послали в приложение SIGTERM и когда мы поймем, что приложение кажется от чего-то офигело или, залипло и не собирается завершаться и нужно послать ему SIGKILL, то есть, жестко завершить его работу. То есть, соответственно, у нас работает какой-то демон, он обрабатывает операции. Мы понимаем, что в среднем у нас операции, над которыми работает демон, не длятся больше 30 секунд за раз. Соответственно, когда приходит SIGTERM, мы понимаем, что наш демон максимум может после SIGTERM дорабатывать 30 секунд. Мы его пишем, например, 45 секунд на всякий случай и говорим, что SIGTERM. После этого 45 секунд ждем. По идее, за это время демон должен был завершить свою работу и завершиться сам. Но если вдруг он не смог, значит, он, скорее всего, залип он больше не обрабатывает наши запросы нормально. И его можно через 45 секунд смело, собственно, прибить.


Причем тут, на самом деле, даже 2 аспекта можно учитывать. Во-первых, понимать, что, если у вас запрос пришел, вы начали с ним как-то работать и не дали ответ пользователю, а вам пришел SIGTERM, например. То имеет смысл доработать и дать ответ пользователю. Это пункт номер раз в этом плане. Пункт номер два тут будет то, что если вы пишите свое приложение, вообще архитектуру строите таким образом, что у вас пришел запрос на ваше приложение, дальше вы запустили какую-то работу, файлики начали откуда-то качать, базу данных перекачивать и еще что-то. В общем, ваш пользователь, ваш запрос висит полчаса и ждет, пока вы ему ответите то, скорее всего, вам нужно над архитектурой поработать. То есть просто учитывать даже здравый смысл, что если у вас операции короткие, то имеет смысл SIGTERM игнорировать и дорабатывать. Если у вас операции длинные, то не имеет смысла в данном случае игнорировать SIGTERM. Имеет смысл переработать архитектуру так, чтобы таких длинных операций избежать. Чтобы пользователи просто не висели, не ждали. Не знаю, сделать там какой-нибудь websocket, сделать обратные хуки, которые ваш сервер уже будет отправлять клиенту, что угодно другое, но не заставлять пользователя полчаса висеть и ждать просто сессию, пока вы ему ответите. Потому что непредсказуемо, где она может порваться.


Когда ваше приложение завершается, следует отдавать какой-нибудь адекватный exit-код. То есть если ваше приложение попросили закрыться, остановиться, и оно нормально само смогло остановиться, то не нужно возвращать какой-нибудь там типа exit-код 1,5,255 и так далее. Все что не нулевой код, по крайней мере, в Linux системах, я в этом точно уверен, считается неуспешным. То есть считается, что ваше приложение в таком случае завершилось с ошибкой. Соответственно, по-хорошему, если ваше приложение завершилось без ошибки, вы говорите 0 на выходе. Если ваше приложение завершилось с ошибкой по каким-то там поводам, вы говорите не 0 на выходе. И с этой с информацией можно работать.


И последний вариант. Плохо, когда у вас пользователь кидает запрос и полчаса висит, пока вы его обработаете. Но в целом еще хочется сказать о том, что вообще стоит со стороны клиента. Неважно у вас мобильное приложение, фронтенд и так далее. Необходимо учитывать, что вообще сессия пользователя может оборваться, может произойти, что угодно. Может быть запрос послан, например, недообработаться и не вернуться ответ. Ваш фронтенд или ваше мобильное приложение вообще любой фронтенд, давайте говорить так должны это учитывать. Если вы работаете с websocket`ами, это вообще самая страшная боль, которая была в моей практике.


Когда разработчики каких-нибудь очередных чатов не знают, что, оказывается, websocket может порваться. У них когда там элементарно на проксе что-нибудь происходит, мы просто конфиг меняем, и она делает reload. Естественно, все долгоживущие сессии рвутся при этом. К нам прибегают разработчики и говорят: Ребята, вы чего, у нас там у всех клиентов чат поотваливался!. Мы им говорим: Вы чего? Ваши клиенты не могут переподключаться?. Они говорят: Нет, нам надо, чтобы сессии не рвались. Короче говоря, это бред на самом деле. Нужно учитывать сторону клиента. Особенно, я же говорю, с долгоживущими сессиями типа websocket, что она может порваться и незаметно для пользователя нужно уметь такие сессии переустановить. И тогда вообще все идеально.


Ресурсы


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


Ресурсы в данном случае, я имею в виду, какие-то реквесты, лимиты, которые вы можете ставить на поды в ваших Kubernetes кластерах. Самое смешное, что я слышал от разработчика Как-то один из коллег разработчиков на предыдущем месте работы сказал: У меня приложение в кластере не запускается. Я посмотрел, что оно не запускается, а там оно то ли не влазило по ресурсам, то ли они очень маленькие лимиты поставили. Короче, приложение не может запуститься из-за ресурсов. Я говорю: Из-за ресурсов не запускается, вы определитесь сколько вам нужно и поставьте адекватное значение. Он говорит: Что за ресурсы?. Я ему начал объяснять, что вот Kubernetes, лимиты в реквестах и бла, бла, бла их нужно ставить. Человек слушал пять минут, кивал и говорит: Я пришел сюда работать разработчиком, не хочу знать ничего ни про какие ресурсы. Я пришел типо сюда писать код и все. Это грустно. Это очень грустная концепция с точки зрения разработчика. Особенно в современном мире, так сказать, прогрессирующего девопса.


Для чего вообще нужны ресурсы? Ресурсы в Kubernetes бывают 2 типов. Одни называются реквестами, вторые лимитами. Под ресурсами мы будем понимать, в основном всегда всего два ограничения базовых. То есть ограничения по процессорному времени и ограничения по оперативной памяти для контейнера, который запущен в Kubernetes.


Лимит ограничивает верхнюю границу для использования ресурса в вашем приложении. То есть, соответственно, если вы говорите 1Гб ОП в лимитах, то больше 1Гб ОП ваше приложение использовать не сможет. А если вдруг захочет и попытается это сделать, то придет процесс, который называется oom killer, out of memory то есть, и убьет ваше приложение то есть оно просто рестартанет. По CPU приложения не рестартятся. По CPU, если приложение пытается использовать сильно много, больше, чем указано в лимитах, CPU будет просто жёстко отбираться. К рестартам это не приводит. Это вот лимит это верхняя граница.


А есть реквест. Реквест это то, благодаря чему Kubernetes понимает, как ноды в вашем кластере Kubernetes заполнены приложениями. То есть реквест это такой некий commit вашего приложения. Оно говорит, что я хочу использовать: Я хотело бы, чтобы ты под меня зарезервировал вот столько CPU и вот столько памяти. Такая простая аналогия. Что если у нас есть node, которой всего доступно, не знаю, 8 CPU. И туда приезжает под, у которого в реквестах написано 1 CPU, значит у нода осталось 7 CPU. То есть, соответственно, как только на эту ноду приедет 8 подов, у которых у каждого в реквестах стоит под 1 цпу на ноде, как бы с точки зрения Kubernetes, CPU кончилось и больше подов с реквестами на эту ноду запустить нельзя. Если на всех нодах CPU кончится, то Kubernetes начнет говорить, что в кластере нет подходящих нод, для того чтобы запустить ваши поды, потому что кончилось CPU.


Для чего нужны реквесты и почему без реквестов, я думаю, что в Kubernetes запускать ничего не нужно? Давайте представим, себе гипотетическую ситуацию. Вы запускаете без реквестов ваше приложениие, Kubernetes не знает, сколько у вас чего, на какие ноды можно запихивать. Ну и пихает, пихает, пихает на ноды. В какой-то момент у вас на ваше приложение начнет приходить трафик. И одно из приложений начинает внезапно вдруг использовать ресурсы до лимитов, которые по лимитам у него есть. Оказывается, что рядом еще одно приложение и ему тоже нужны ресурсы. На ноде начинает реально физически кончаться ресурсы, например, ОП. Когда у нода кончается ОП, у вас нода в первую очередь докер, потом кублет, потом ОС. Они просто уйдут в несознанку и работать у вас все перестанет точно. То есть это приведет к тому, что у вас залипнет нода, ее нужно будет рестартануть. Короче, ситуация не очень хорошая.


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


Хранение данных


Следующий пунктик у нас это про хранение данных. Что с ними делать и вообще, как быть с persistence в Kubernetes?


Я думаю, опять же у нас, в рамках Вечерней Школы, тема про базу данных в Kubernetes была. И мне кажется, что я даже примерно знаю, что сказали вам коллеги на вопрос: Можно ли запускать в Kubernetes базу данных?. Мне почему-то, кажется, что коллеги вам должны были сказать, что, если вы задаете вопрос, можно ли в Kubernetes запускать базу данных, значит нельзя.


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


Что делать с данными, которые наше приложение хотело бы хранить, какие-нибудь картинки, которые загружают пользователи, какие-нибудь штуки, которые в процессе работы генерирует наше приложение, при старте, например? Что с ними в Kubernetes делать?


Вообще в идеале, да, конечно, Kubernetes очень хорошо предусмотрен и вообще изначально задумывался под stateless приложения. То есть под те приложения, которые вообще не хранят информацию. Это идеальный вариант.


Но, естественно, идеальный вариант, он как бы существует далеко не всегда. Поэтому ну что? Пункт первый и самый простой возьмите какой-нибудь S3, только не самопальное, которое тоже непонятно как работает, а от какого-нибудь провайдера. Хорошего нормального провайдера и научите ваше приложение пользоваться S3. То есть когда у вас пользователь хочет загрузить файлик, скажите вот сюда, пожалуйста, в S3 его загрузи. Когда он захочет его получить, скажите: Вот тебе ссылочка на S3 бэк и вот отсюда его возьми. Это идеальный вариант.


Если вдруг по каким-то причинам такой идеальный вариант не подходит, у вас приложение, которое не вы пишите, не вы разрабатываете, или оно какое-нибудь жуткое легаси, оно не может по S3 протоколу, но должно работать с локальными директориями в локальных папочках. Возьмите что-нибудь более-менее простое, разверните это дело Kubernetes. То есть сразу городить Ceph под какие-то минимальные задачи, мне кажется, идея плохая. Потому что Ceph, конечно, хорошо и модно. Но если вы не очень понимаете, что делаете, то сложив что-то на Ceph, можно очень легко и просто это оттуда потом больше никогда не достать. Потому что, как известно, Ceph хранит данные у себя в кластере в бинарном виде, а не в виде простых файликов. Поэтому если вдруг кластер Ceph сломается, то есть полная и большая вероятность оттуда данные свои больше никогда не достать.


У нас будет курс по Ceph, можете ознакомиться с программой и оставить заявку.


Потому лучше что-нибудь простое типа на NFS сервера. Kubernetes умеет с ними работать, вы можете подмонтировать директорию под NFS сервер ваше приложение просто как локальную директорию. При этом, естественно, нужно понимать, что опять же, с вашим NFS нужно что-то делать, нужно понимать, что иногда, возможно, оно станет недоступным и предусмотреть вопрос, что вы будете делать в таком случае. Возможно, его стоит бэкапить куда-то на отдельный машину.


Следующий момент, о котором я говорил, это что делать, если в процессе работы ваше приложение генерирует какие-нибудь файлики. К примеру, оно, запускаясь, генерирует какой-нибудь статический файлик, который основан на какой-нибудь информации, которое приложение получает только в момент запуска. Тут какой момент. Если таких данных немного, то можно вообще не заморачиваться, просто под себя это приложение положить и работать. Только тут вопрос какой, смотрите. Очень часто легаси всякие системы, типа вордпресса и прочее, особенно с доработанными хитроумными какими-нибудь плагинами, хитроумными пхп-разработчиками, они часто умеют делать так, что они генерят какой-нибудь файлик под себя. Соответственно, один генерирует один файлик, второй генерит второй файлик. Они разные. Балансировка происходит в кластере Kubernetes клиентов просто случайно. Соответственно, вместе получается в instance работать не умеют. Один отдает одну информацию, другой отдает пользователю другую информацию. Вот такого стоит избегать. То есть в Kubernetes все, что вы запускаете, гарантировано должно уметь работать в несколько instances. Потому что Kubernetes штука подвижная. Соответственно, подвинуть он может что угодно, когда угодно, ни у кого не спрашивая в целом. Поэтому на это нужно рассчитывать. Все запущенное в один instance рано или поздно когда-нибудь упадет. Чем больше, соответственно, у вас резервирования, тем лучше. Но опять же, я говорю, если у вас таких файликов немного, то их можно прям под себя положить, они небольшой объем весят. Если их становится чуть больше, внутрь контейнера их, наверное, пихать не стоит.


Я бы посоветовал, есть в Kubernetes такая прекрасная штука, вы можете использовать volume. В частности, есть volume типа empty dir. То есть это просто Kubernetes автоматически на том сервере, где вы запустились, создаст директорию у себя в служебных директориях. И даст вам ее, чтобы вы ей пользовались. Тут только один важный момент. То есть ваши данные будут храниться не внутри контейнера, а как бы на хосте, на котором вы запущены. Kubernetes более того такие empty dirs при нормальной настройке умеет контролировать и умеет контролировать их максимальный размер и не давать его превышать. Единственный момент то, что у вас записано в empty dir, при рестартах пода не теряется. То есть если ваш под по ошибке упадет и поднимется снова, информация в empty dir никуда не денется. Под ею снова при новом старте может воспользоваться и это хорошо. Если ваш под куда-то уедет, то естественно, он уедет без данных. То есть, как только под с нода, где он был запущен с empty dir, пропадает, empty dir удаляется.


Чем еще хорош empty dir? Например, им можно пользоваться как кэшем. Давайте представим, что у нас приложение генеририт что-то на лету, отдает пользователям и это делает долго. Поэтому приложение, например, генерит и отдает пользователям, и заодно складывает под себя куда-нибудь, чтобы в следующий раз, когда пользователь за тем же самым придет, отдать это сразу сгенерированное быстрее. Empty dir можно попросить Kubernetes создать в памяти. И таким образом, у вас кэши вообще могут просто молниеносно работать в плане скорости обращения к диску. То есть у вас empty dir в памяти, в ОС он хранится в памяти, а при этом для вас, для пользователя внутри пода это выглядит, как просто локальная директория. Вам не нужно приложение специально учить какой-то магии. Вы просто прям берете и кладете в директорию ваш файлик, но, на самом деле, в память на ОС. Это тоже очень удобная фишка в плане Kubernetes.


Какие проблемы у Minio существуют? Главная проблема Minio это в том, что для того, чтобы эта штука работала, ей нужно где-то быть запущенной, и по ней должна быть какая-нибудь файловая система, то есть хранилище. А тут мы встречаемся с теми же самыми проблемами, которые есть у Ceph. То есть Minio где-то должно хранить свои файлы. Оно просто HTTP интерфейс к вашим файлам. Причем с функционалом явно победнее, чем у амазоновской S3. Раньше оно не умело нормально авторизовывать пользователя. Сейчас оно, насколько я знаю, оно уже умеет делать buckets с разной авторизацией, но опять же, мне кажется, главная проблема это, так сказать, нижележащая система хранения под минимум.


Empty dir в памяти как влияет на лимиты? Никак не влияет на лимиты. Он лежит в памяти, получается, хоста, а не в памяти вашего контейнера. То есть ваш контейнер не видит empty dir в памяти, как часть занятой своей памяти. Это видит хост. Соответственно, да, с точки зрения kubernetes, когда вы начинаете такое использовать, хорошо бы понимать, что вы часть своей памяти отдаете под empty dir. И соответственно, понимать, что память может кончиться не только из-за приложений, но и из-за того, кто-то в эти empty dir пишет.


Cloudnativeness


И заключительная подтема, что такое Cloudnative. Зачем оно нужно. Cloudnativeness и так далее.


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



Давайте возьмем для примера просто Kubernetes. У вас приложение запущено в Kubernetes. У вас всегда ваше приложение может, точнее админы для вашего приложения, всегда могут сделать сервис-аккаунт. То есть учетку для авторизации в самом Kubernetes в его сервере. Накрутить туда какие-нибудь права, которые нам нужны. И вы можете обращаться в Kubernetes из вашего приложения. Что таким образом можно делать? Например, из приложения получать данные о том, где находятся другие ваши приложения, другие такие же инстансы и вместе как-нибудь кластеризоваться поверх Kubernetes, если такая необходимость есть.


Опять же, недавно буквально у нас был кейс. У нас есть один контролер, который занимается тем, что мониторит очередь. И когда в этой очереди появляются какие-то новые задачи, он идет в Kubernetes и внутри Kubernetes создает новый под. Дает этому поду какую-то новую задачу и в рамках этого пода, под выполняет задачу, отправляет ответ в сам контролер, и контролер потом с этой информацией что-то делает. Базу данных, к примеру, складывает. То есть это опять же плюс того, что наше приложение работает в Kubernetes. Мы можем использовать сам встроенный функционал Kubernetes для того, чтобы как-то расширить и сделать более удобным функционал нашего приложения. То есть не городить какую-то магию того, как запускать приложение, как запускать воркер. В Kubernetes просто закинуть запрос в аппе, если приложение написано на питоне.


То же самое касается, если мы выйдем за пределы Kubernetes. У нас где-то наш Kubernetes запущен хорошо, если в каком-то облаке. Опять же, мы можем использовать, и даже должны, я считаю, использовать возможности самого облака, где мы запущены. Из элементарного, что нам облако предоставляет. Балансировка, то есть мы можем создавать балансировщики облачные, их использовать. Это прям преимущество того, чем мы можем пользоваться. Потому что облачная балансировка, во-первых, просто тупо снимает ответственность с нас за то, как оно работает, как оно настраивается. Плюс очень удобно, потому что обычный Kubernetes с облаками умеет интегрироваться.


Тоже самое касается масштабирования. Обычный Kubernetes умеет интегрироваться с облачными провайдерами. Умеет понимать, что если в кластере закончились ноды, то есть место нода закончилось, то нужно добавить сам Kubernetes добавит вам новых нод в свой кластер и начнет на них запускать поды. То есть когда у вас нагрузка приходит, у вас подов начинает количество увеличиваться. Когда ноды в кластере заканчиваются для этих подов, то Kubernetes запускает новые ноды и, соответственно, количество подов еще может увеличиваться. И это очень удобно. Это прям возможность на лету масштабировать кластер. Не очень быстро, в плане того, что это не секунда, это скорее минута, для того чтобы добавлять новые ноды.


Но из моего опять же опыта, самое такое клевое, что я видел. Когда на основе времени суток кластер Cloudnative скейлился. Это был такой бэкенд сервис, которым пользовались люди в бэкофисе. То есть они приходят на работу в 9 утра, начинают заходить в систему, соответственно, кластер Cloudnative, где это все запущено, начинает пухнуть, запускать новые поды, для того чтобы все, кто пришел на работу, могли с приложением работать. Когда они в 8 вечера или в 6 вечера уходят с работы, кластеры Kubernetes замечают, что больше никто не пользуется приложением и начинают уменьшаться. Экономия прям процентов 30 гарантированная. Это тогда работало в Амазоне, в России на тот момент не было вообще никого, кто бы так круто умел делать.


Я прям говорю, экономия процентов 30 просто из-за того, что мы пользуемся Kubernetes, пользуемся возможностями облака. Сейчас такое можно делать в России. Я не буду никого рекламировать, конечно, но скажем так, есть провайдеры, которые такое умеют делать, предоставлять прямо с коробки по кнопке.


Последний момент, на который тоже хочу обратить внимание. Для того, чтобы ваше приложение, ваша инфраструктура была Cloudnative, имеет смысл уже начать наконец-то адаптировать подход, который называется Infrastructure as a Code.То есть это о том, что ваше приложение, точнее вашу инфраструктуру нужно точно так же, как и код вашего приложения, вашей бизнес логики описывать в виде кода. И работать с ним как с кодом, то есть его тестировать, раскатывать, хранить в git, CI\CD к нему применять.


И это прям то, что позволяет вам, во-первых, всегда иметь контроль над вашей инфраструктурой, всегда понимать в каком она состоянии. Во-вторых, избегать ручных операций, которые вызывают ошибки. В-третьих, избегать просто того, что называется текучкой, когда вам постоянно нужно выполнять одни и те же ручные операции. В-четвертых, это позволяет гораздо быстрее восстанавливаться в случае сбоя. Вот в России каждый раз, когда я об этом говорю, находятся обязательно огромное количество человек, которые говорят: Ага, понятно, да у вас подходы, короче, ничего чинить не надо. Но эта правда. Если у вас что-то сломалось в инфраструктуре, то с точки зрения Cloudnative подхода и с точки зрения Infrastructure as a Code, чем это починить, пойти на сервер, разобраться, что сломалось и починить, проще сервер удалить и создать заново. И у меня все это восстановится.


Более подробно все эти вопросы рассматриваются на видеокурсах по Kubernetes: Джуниор, Базовый, Мега. По ссылке вы можете ознакомиться с программой и условиями. Удобно то, что можете освоить Kubernetes, занимаясь из дома или с работы по 1-2 часа в день.

Подробнее..

XCResult как и зачем читать

04.03.2021 16:23:55 | Автор: admin


В 2018 году Apple в очередной (третий) раз обновили формат, в котором выдаётся информация о прогоне тестов. Если раньше это был plist файл, который представлял из себя большой xml, то теперь это большой файл с расширением xcresult, который открывается через Xcode и содержит в себе кучу полезной информации, начиная c результатов тестов с логами, скриншотами и заканчивая покрытием таргетов, диагностической информацией о сборке и многим другим. Большинство разработчиков не работает каждый день с этим, но инфраструктурщики в данной статье могут найти что-то полезное.

Разложим по полочкам плюсы и минусы обновления формата


В чем минусы обновления формата?
Много весит, а это значит обмен такими файлами с CI сервером может оказаться долгим.
Если нет Xcode, то его не открыть (сомнительно, что у тестировщика или разработчика не будет Xcode, но все же).
Возможная поломка существующих инструментов интеграции. Снова учиться работать с чем-то новым.

Чем удобен новый xcresult?
Открывается нативными средствами через Xcode.
Можно передавать коллегам из QA и разработки, даже если у них нет локально проекта. Все откроется и покажет нужную информацию.
Содержит исчерпывающую информацию о прогоне тестов.
Можно читать не только через Xcode.

Вот о последнем пункте мы и будем говорить в этой статье.

Зачем читать XCResult не через Xcode?


Если у вас в компании настроены процессы CI&CD, то наверняка вы собираете метрики по сборкам проекта, по стабильности и количеству тестов, и, конечно, данные по тестовому покрытию. Скорее всего, где-нибудь на Bamboo, Jenkins, Github у вас рисуются упавшие тесты или статус CI, или процент покрытия. Такие операции принято автоматизировать и отдавать на откуп бездушным машинам. Какие инструменты есть у нас для этого?
Apple, вместе с релизом нового формата, выпустили и инструменты xcresulttool и xccov, с которыми можно работать из терминала.

Что мы можем достать, используя xccov?


xcrun xccov view --report --json /path/to/your/TestScheme.xcresult

Запрос вернёт исчерпывающую информацию о том, каким покрытием обладают все таргеты, какие методы и каких классов покрыты, сколько раз они были выполнены и какие строчки выполнялись. Объекты обладают схожей структурой. Всего там 4 уровня: корень, таргет, файл, функция. Все уровни, кроме корневого, имеют поле name. Во всех уровнях есть поля coveredLines и lineCoverage. Важно отметить, что объекты имеют какой-то собственный контекст. Всю структуру можно описать в несколько протоколов.



Помимо протоколов выделим следующие структуры: CoverageReport агрегатор всего и корень. Он содержит в себе массив объектов Target. Каждый Target содержит в себе массив File, которые, в свою очередь, содержат массив Function. Эти объекты будут реализовывать протоколы, которые описаны выше.
Нас интересует поле lineCoverage. Для составления красивого отчета (как в fastlane) обратимся к полю lineCoverage и пройдем по всем объектам нехитрой функцией:



Получим что-то похожее на:

Coverage Report Summary:

Utils.framework: 51,04 %

NavigationAssistantKit.framework: 0,0 %

NavigationKit.framework: 35,85 %

Logger.framework: 20,32 %

FTCCardData.framework: 78,21 %

FTCFeeSDK.framework: 25,25 %

ErrorPresenter.framework: 2,8 %

MTUIKit.framework: 0,24 %

AnalyticsKit.framework: 47,52 %

EdaSDK.framework: 1,18 %

Alerts.framework: 85,19 %

Resources.framework: 39,16 %

QpayApiTests.xctest: 88,37 %

FTCFeeSDKTests.xctest: 97,91 %


P.S. Для того, чтобы coverage собирался, необходимо добавить в вашу команду тестирования параметр -enableCodeCoverage YES или включить в настройках схемы в Xcode.

Какие возможности даст xcresulttool?


На самом деле xcresulttool имеет не сильно большой интерфейс, но из него можно извлечь поистине много информации, если знать структуру xcresult. А это целая база данных, к которой можно делать запросы.

Для начала неплохо ознакомиться с самим интерфейсом:

xcrun xcresulttool --help

OVERVIEW: Xcode Result Bundle Tool (version 16015)

USAGE: xcresulttool subcommand [options] ...

SUBCOMMANDS:

export Export File or Directory from Result Bundle

formatDescription Result Bundle Format Description

get Get Result Bundle Object

graph Print Result Bundle Object Graph

merge Merge Result Bundles

metadata Result Bundle Metadata

version XCResultKit Version


Чтобы прочитать структуру, нам достаточно вызвать команду:

xcrun xcresulttool get --path /path/to/your/res.xcresult --format json

Здесь мы получим оглавление для нашего xcresult бандла. Что собиралось, какие тесты прогонялись, сколько времени заняло, где лежат скриншоты и логи, и какие были предупреждения компилятора. Для нас главное достать идентификаторы файлов, в которых лежит информация о тестах.

xcrun xcresulttool get --path /path/to/your/res.xcresult --format json --id {id}

Тогда мы получим объекты с тест-таргетами, типом тестов, которые разбиты по тест-классам и test suits с отчётами с логами, скриншотами, временем выполнения и прочей информацией по каждому тесту.
К сожалению, причину падения красных тестов не получится вытащить просто для этого придётся делать ещё один запрос на каждый упавший тест (а на самом деле даже не один! Если тест крэшнул, то крэшлоги вместе со стректрейсом лежат в другом месте и это ещё один запрос!

Для Failure Summary используется тот же запрос:

xcrun xcresulttool get --path /path/to/your/res.xcresult --format json --id {id}

А вот для крэшлогов нужно убрать --format json из запроса, т.к. там просто строка и при передаче форматтера инструмент выдаст ошибку.

Что делать с этими справочными знаниями дальше?


Автоматизировать, конечно же! Если вы попробуете выполнить эти команды, то увидите, что ответы гигантские и их тяжело читать. Как автоматизировать? Ruby, Python Или Swift?
Конечно же, swift. Его знает любой современный iOS разработчик. Проект открывается в Xcode, доступна отладка, подсветка синтаксиса, строгая типизация. Короче, мечта! Особенно при появлении Swift package manager.
Ни для кого не секрет, что с помощью swift мы легко можем запускать процессы, слушать ошибки и получать выходные данные. В самом простом случае мы можем обойтись такой конструкцией:



Нам остается теперь только исследовать формат XCResult через уже знакомые нам xcrun xcov и xcrun xcresulttool. Например, чтобы прочитать покрытие тестами, мы используем:



А чтоб получить оглавление XCResult нам нужно выполнить:



Но как нам получить наши заветные структуры CoverageReport и XCResult?
Получаем строку из Data, которую вернет нам первая Shell команда и помещаем содержимое сюда: quicktype.io.
Сервис сгенерирует нам что-то похожее на нужные свифтовые структуры. Правда использовать результат как есть не получится. Придётся пристальнее изучать структуру ответа и выбрасывать дубли. Тем не менее такая работа не составляет большого труда. Можно отбрасывать ненужные части, а можно заняться исследованием и выделить несколько основных кирпичиков:



На основании этого описать уже остальные структуры, например:



или даже такие сведения о компьютерах, на которых совершался прогон:



Ну а этим-то как пользоваться?


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



Второй путь использовать этот проект как библиотеку. У нас есть большой CI проект, который отвечает за сборку, тестирование и доставку нашего продукта KoronaPay. Например, мы можем по результатам прохождения тестов извлекать все assertion failures и крэши в тестах приблизительно так:



Или получать красные тесты, анализировать флаки и перезапускать только их.
А как анализировать? Всё просто и непросто одновременно. Чтобы достать детали причины падения теста, надо сделать дополнительный запрос к xcresult по идентификатору failure summary. А затем из failure summary вытаскивать информацию. На сегодняшний момент мы научились искать крэши в тестах и lost connection случаи, а также вытаскивать причины. Понять, что произошел крэш несложно. Надо лишь найти в failureSummaries заветные слова crashed in.



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



В методе reflectProperties нет ничего магического, это простенький extension для Mirror:



Еще одна категория красных тестов ассерты. В отличие от крэшей здесь не получится просто поискать строку crashed in. Такие тесты могут маскироваться под lost connection случаи. Чтобы докопаться до причины, придется пройтись по нескольким массивам внутри объекта TestCase примерно так:



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



Вместо заключения


Как и все существующие в этой области решения, наш скраппер не является исчерпывающим инструментом для анализа xcresult. Чтобы получить всю информацию и посмотреть скриншоты, все еще надо открывать xcresult через Xcode. Однако если у вас настроен CI и вы хотите видеть результаты тестов быстро, то, скорее всего, сможете оценить связку junit и нашего xcscrapper по достоинству.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru