Для чего нужна журнализация в базах данных
Журнализация изменений
В простейшем случае журнализация изменений заключается в последовательной записи во внешнюю память всех изменений, выполняемых в базе данных. Записывается следующая информация:
Формируемая таким образом информация называется журнал изменений базы данных. Журнал содержит отметки начала и завершения транзакции, и отметки принятия контрольной точки (см. ниже).
В СУБД с отложенной записью блоки данных внешней памяти снабжаются отметкой порядкового номера последнего изменения, которое было выполнено над этим блоком данных. В случае сбоя системы эта отметка позволяет узнать какая версия блока данных успела достичь внешней памяти.
СУБД с отложенной записью периодически выполняет контрольные точки. Во время выполнения этого процесса все незаписанные данные переносятся на внешнюю память, а в журнал пишется отметка принятия контрольной точки. После этого содержимое журнала, записанное до контрольной точки может быть удалено.
Журнал изменений может не записываться непосредственно во внешнюю память, а аккумулироваться в оперативной. В случае подтверждения транзакции СУБД дожидается записи оставшейся части журнала на внешнюю память. Таким образом гарантируется, что все данные, внесённые после сигнала подтверждения, будут перенесены во внешнюю память, не дожидаясь переписи всех измененных блоков из дискового кэша. СУБД дожидается записи оставшейся части журнала так же при выполнении контрольной точки.
В случае логического отказа или сигнала отката одной транзакции журнал сканируется в обратном направлении, и все записи отменяемой транзакции извлекаются из журнала вплоть до отметки начала транзакции. Согласно извлеченной информации выполняются действия, отменяющие действия транзакции, а в журнал записываются компенсирующие записи. Этот процесс называется откат (rollback).
В случае физического отказа, если ни журнал, ни сама база данных не повреждена, то выполняется процесс прогонки (rollforward). Журнал сканируется в прямом направлении, начиная от предыдущей контрольной точки. Все записи извлекаются из журнала вплоть до конца журнала. Извлеченная из журнала информация вносится в блоки данных внешней памяти, у которых отметка номера изменений меньше, чем записанная в журнале. Если в процессе прогонки снова возникает сбой, то сканирование журнала вновь начнется сначала, но фактически восстановление продолжиться с той точки, откуда оно прервалось.
Содержание
Мультиплексирование
Для увеличения отказоустойчивости СУБД может записывать одновременно несколько идентичных копий журнала изменений. Если в случае отказа одна из копий журнала окажется недоступной, СУБД восстановит базу данных используя любую из доступных копий. Такая стратегия называется мультиплексированием журнала изменений.
Архивирование
Как правило, журнал изменений перезаписывается сначала, как только заканчивается пространство внешней памяти, распределенное под него. Это позволяет восстановить базу данных до актуального и консистентного состояния, но только в том случае, если сама база данных не потеряна, пусть даже и не в актуальном состоянии.
Однако в некоторых информационных системах восстановление должно быть гарантировано, даже если вся база данных потеряна. В таких системах периодически выполняются резервное копирование базы данных, а журнал изменений разделяется на последовательные отрезки и архивируется. Перед началом резервного копирования выполняется контрольная точка и журнал разделяется на отрезки, записанные до и после начала резервного копирования. По завершении процесса резервного копирования весь журнал изменений записанный до начала резервного копирования удаляется. Таким образом, при наличии резервной копии и архивированных журналов изменений, база данных может быть восстановлена до актуального состояния, даже если все блоки данных были потеряны.
Реализации
Не все реальные СУБД следуют классической схеме реализации журнала изменений, в частности по соображениям эффективности.
Oracle
Informix
Для восстановления в случае отказа используется т.н. физический журнал, в который копируются образы страниц перед их изменением. В случае сбоя сервера неподтвержденные данные будут восстановлены во время запуска.
Журнализация транзакций
Журнализация изменений — функция СУБД, которая сохраняет информацию, необходимую для восстановления базы данных в предыдущее консистентное состояние в случае логических или физических отказов.
В простейшем случае журнализация изменений заключается в последовательной записи во внешнюю память всех изменений, выполняемых в базе данных. Записывается следующая информация:
Формируемая таким образом информация называется журнал изменений базы данных. Журнал содержит отметки начала и завершения транзакции, и отметки принятия контрольной точки (см. ниже).
В СУБД с отложенной записью блоки данных внешней памяти снабжаются отметкой порядкового номера последнего изменения, которое было выполнено над этим блоком данных. В случае сбоя системы эта отметка позволяет узнать какая версия блока данных успела достичь внешней памяти.
СУБД с отложенной записью периодически выполняет контрольные точки. Во время выполнения этого процесса все незаписанные данные переносятся на внешнюю память, а в журнал пишется отметка принятия контрольной точки. После этого содержимое журнала, записанное до контрольной точки может быть удалено.
Журнал изменений может не записываться непосредственно во внешнюю память, а аккумулироваться в оперативной. В случае подтверждения транзакции СУБД дожидается записи оставшейся части журнала на внешнюю память. Таким образом гарантируется, что все данные, внесённые после сигнала подтверждения, будут перенесены во внешнюю память, не дожидаясь переписи всех измененных блоков из дискового кэша. СУБД дожидается записи оставшейся части журнала так же при выполнении контрольной точки.
В случае логического отказа или сигнала отката одной транзакции журнал сканируется в обратном направлении, и все записи отменяемой транзакции извлекаются из журнала вплоть до отметки начала транзакции. Согласно извлеченной информации выполняются действия, отменяющие действия транзакции, а в журнал записываются компенсирующие записи. Этот процесс называется откат (rollback).
В случае физического отказа, если ни журнал, ни сама база данных не повреждена, то выполняется процесс прогонки (rollforward). Журнал сканируется в прямом направлении, начиная от предыдущей контрольной точки. Все записи извлекаются из журнала вплоть до конца журнала. Извлеченная из журнала информация вносится в блоки данных внешней памяти, у которых отметка номера изменений меньше, чем записанная в журнале. Если в процессе прогонки снова возникает сбой, то сканирование журнала вновь начнется сначала, но фактически восстановление продолжится с той точки, откуда оно прервалось.
Содержание
Мультиплексирование
Для увеличения отказоустойчивости СУБД может записывать одновременно несколько идентичных копий журнала изменений. Если в случае отказа одна из копий журнала окажется недоступной, СУБД восстановит базу данных используя любую из доступных копий. Такая стратегия называется мультиплексированием журнала изменений.
Архивирование
Как правило, журнал изменений перезаписывается сначала, как только заканчивается пространство внешней памяти, распределенное под него. Это позволяет восстановить базу данных до актуального и консистентного состояния, но только в том случае, если сама база данных не потеряна, пусть даже и не в актуальном состоянии.
Однако в некоторых информационных системах восстановление должно быть гарантировано, даже если вся база данных потеряна. В таких системах периодически выполняются резервное копирование базы данных, а журнал изменений разделяется на последовательные отрезки и архивируется. Перед началом резервного копирования выполняется контрольная точка и журнал разделяется на отрезки, записанные до и после начала резервного копирования. По завершении процесса резервного копирования весь журнал изменений записанный до начала резервного копирования удаляется. Таким образом, при наличии резервной копии и архивированных журналов изменений, база данных может быть восстановлена до актуального состояния, даже если все блоки данных были потеряны.
Реализации
Не все реальные СУБД следуют классической схеме реализации журнала изменений, в частности по соображениям эффективности.
Oracle
Информация для отката (журнал отката) группируется в сегменты отката и также используется для поддержания целостности по чтению. В случае подтверждения транзакции информация о старых данных уничтожается, а в случае отката — используется для восстановления отменяемой транзакции.
Informix
В СУБД Informix журнал изменений представляет из себя дисковое пространство, разделенное на части называемые файлами журнала транзакций (эти файлы не имеют ничего общего с файлами на файловой системе) или логическим журналом. Запись изменений в этот журнал зависит от того в каком режиме находится база данных — без журналирования, с буферизованным журналированием или с небуферизованным. Все изменения сначала попадают в буфера логического журнала, а дальнейший сброс их в журнал транзакций зависит от режима журналирования базы данных.
Для восстановления в случае отказа используется т. н. физический журнал, в который копируются образы страниц перед их изменением. В случае сбоя сервера неподтвержденные данные будут восстановлены во время запуска.
Лучшие методики журналирования enterprise-приложений (с точки зрения инженера поддержки)
Журналы приложений раскрывают информацию о внешних и внутренних событиях, которые видит приложение в ходе исполнения. Когда при деплое возникает баг, взлом или аномалия, журналы — самое полезное и надёжное доказательство для проведения анализа причин инцидента.
Давайте разберемся, как писать в журнал полезные сообщения, которые всем понравятся.
1. Что журналировать
Входящие и исходящие сообщения
Если компоненты взаимодействуют друг с другом с помощью сообщений, то нужно записывать входящие и исходящие сообщения с указанием URL’ов конечных точек API, параметры запросов, исходные и промежуточные IP запросов, заголовки запросов, информацию об авторе, тела запросов и ответов, бизнес-контекст, временные метки и этапы внутренней обработки.
Очень важно, чтобы каждому сообщению был присвоен уникальный идентификатор (обычно генерируемый на уровне менеджера API или сервиса). Это нужно для отслеживания обмена сообщениями между сервисами в системе.
Вызов сервисов и функций
При вызове сервиса или функции желательно подробнее журналировать их контекст, в основном для отладки (используйте TRACE или DEBUG). Эти журналы помогут в расследовании проблем, связанных с бизнес-логикой, особенно при отсутствии привилегий по прикреплению отладчика к приложению (например, при развёртывании в тестовое, staging или pre-prod-окружение).
Действия пользователей и бизнес-статистика
У каждого приложения есть уникальные бизнес-сценарии и пользовательские маршруты (user journey), которые дают много информации профильным специалистам. Например, не слишком ли долго выполнялась определённая транзакция, или не застревают ли пользователи на какой-то функциональности — всё это очень важные данные с точки зрения пользовательского опыта. Иная информация, относящаяся к бизнесу, — количество транзакций и активных пользователей, а также их этапы — важна для поиска причинно-следственных связей, и даже может применяться в бизнес-анализе.
Операции с данными (журнал аудита)
По соображениям безопасности и соблюдения требований регулятора в большинстве enterprise-приложений требуется вести отдельные журналы по операциям со всей важной информацией, такой как идентификаторы доступа (пользователей и систем), точные экземпляры сервисов и использованные привилегии ролей, временные метки, запросы на данные, слепки предыдущего и нового состояния изменённых данных (diff). Журнал аудита должен фиксировать все операции с данными (доступ, импорт, экспорт и т. д.), а также CRUD-операции (создание, чтение, обновление, удаление), выполненные пользователями, иными системами и сервисами.
Системные события
К ним относится информация о поведении (запусках, остановках, перезапусках и событиях, связанных с безопасностью), переходных режимах (холодный, разогрев, горячий), межсервисному взаимодействию (handshake, статусы установки соединения — подключено, отключено, переподключено, повторные попытки), идентификаторы экземпляров сервисов, активно обслуживающие API, активно прослушивающие диапазоны IP и портов, загруженные конфигурации (первоначальные загрузки и динамические обновления), общее состояние сервисов, а также всё, что поможет понять поведение системы.
Статистика производительности
Усердие — прекрасная характеристика вычислительных устройств, но они могут работать не идеально. В любое время могут возникнуть проблемы с производительностью или внезапные неожиданные ухудшения обслуживания (в основном из-за необработанных ошибок и повреждённых данных). Чтобы их определить, всегда рекомендуется публиковать статистику общего состояния и производительности системы. Она может содержать информацию вроде счётчиков вызовов API (успешно обслуженных и сбойных), сетевую задержку, среднюю длительность roundtrip’ов, потребление памяти и прочую специфическую для приложения информацию (обычно определяется бизнес-контекстом).
Угрозы и уязвимости
Раскрытие угроз и уязвимостей с помощью runtime’a приложения и журнала — это искусство, которым должен овладеть любой разработчик enterprise-ПО. Обычно взломы и сбои не происходят внезапно. Чаще всего есть признаки, которые сначала никто не замечает. Поэтому нужно всегда журналировать подозрительную человеческую активность (например, ошибочные попытки аутентификации и верификации с приложением всей низкоуровневой информации вроде использованных сетей, источников запросов, пользовательских ролей и привилегий), а также поведение системы (например, рост пиков в паттернах потребления ресурсов, высокую нагрузку на веб-серверы, случайные сбои сервисов). Когда вы замечаете подозрительное событие, убедитесь, что журналы содержат всю связанную с ним информацию. В идеале, чтобы это была full-stack-трассировка со значениями параметров и дополнительной информацией, полученной из контекста приложения.
2. Что не нужно журналировать
Информацию личного порядка
Почти все законы, регулирующие вопросы приватности (например, GDPR, CCPA) прямо рекомендуют разработчикам не журналировать информацию, позволяющую установить личность. Сюда относятся ФИО, ники, пол, день рождения, почтовый адрес, электронную почту, телефонные номера, номер социального страхования и номера кредитных карт.
Названия компаний и контактную информацию
Убедитесь, что вы не записываете имена компаний, информацию о сотрудниках, клиентах, поставщиках, а также контактную информацию компании и отдельных людей. Журнал никогда не должен раскрывать деловые взаимосвязи и операций с третьими сторонами. Для отслеживания конкретных транзакций используйте вместо настоящих названий идентификаторы событий, сгенерированные системой, и передавайте их другим сервисам.
Финансовые данные (банковские счета, реквизиты банковских карт, пересылаемые суммы и т. д.)
По закону все финансовые данные должны быть полностью убраны или замаскированы. Раскрытие такой информации в журналах легко может привести к серьёзному судебному иску (вплоть до уголовной ответственности). Избегайте этого всеми способами.
Пароли, ключи безопасности и секреты, токены аутентификации
Учётные данные и токены аутентификации считаются конфиденциальной информацией, поэтому их наличие в журналах поможет злоумышленникам легко найти бреши в системе. Поэтому не допускайте эти данные в журналы.
Примечание: вам легко будет определять, какую информацию нужно скрыть от журналов, если вы добавите в каждое поле атрибут, определяющий уровень видимости (например, show, mask, hide, encrypt). Если у вас есть такой механизм, то вы сможете менять видимость полей, просто обновляя свойства в конфигурации. Это хорошее решение в тех случаях, когда нужно журналировать какую-нибудь пользовательскую информацию в небоевых окружениях, особенно для тестирования и отладки. Или можно написать парсеры, которые фильтруют журналы и обрабатывают конфиденциальные поля в соответствии с заранее прописанными для этого окружения инструкциями.
3. Лучшие методики
Знайте, когда нужно использовать тот или иной уровень журналирования
Уровень журналирования используется для обозначения серьёзности каждого элемента системы. В большинстве фреймворков для журналирования есть такие уровни:
Вне зависимости от сложности и глубины каждого уровня журналирования, мы должны корректно настраивать их в своём коде, чтобы предоставлять оптимальное количество информации в каждом сценарии. Например, все данные, используемые разработчиками для отладки и технического анализа, должны идти на уровнях DEBUG или TRACE, а баннеры с системными данными опускаются ниже INFO.
Используйте английский язык
Некоторые инструменты и консоли не поддерживают вывод и хранение журналов с определёнными Unicode-символами. Поэтому локализация и прочие улучшения могут доставить трудности. Придерживайтесь английского языка и всегда используйте для записи сообщений широко поддерживаемый набор символов.
Добавляйте удобные для разработчиков сообщения (краткие и содержательные)
Если журналировать слишком мало, но мы не сможем получить нужную информацию для воссоздания контекста каждого важного события. А если журналировать слишком много, то это ухудшит производительность: запись огромного файла журнала увеличит операции ввода-вывода и потребление ресурсов дискового хранилища. Если файловая система не поддерживает его, это снизит общую производительность системы.
Для оптимизации сообщений вам необходимо чёткое понимание функциональных и нефункциональных ожиданий от системы и планирование нужного качества и количества сообщений. Каждый журнал должен быть содержательным и соответствующим контексту: всегда пишите кратко и по делу.
Создайте справочные идентификаторы, псевдонимы и упрощённые шаблоны для часто используемых и длинных сообщений
Вместо того, чтобы каждый раз записывать полное описание, попробуйте создать справочные идентификаторы или упрощённые шаблоны, чтобы представлять в журнале длинные повторяющиеся описания. Это уменьшает общее количество и длину сообщений, а также повышает гибкость в сокрытии определённой информации. Например, вместо того, чтобы описывать в текстовом журнале описание уязвимости, лучше использовать псевдоним или идентификатор, чтобы только профильные специалисты могли разобраться в актуальном сценарии.
Используйте корректные временные метки
Временные метки позволяют понять последовательность событий, они необходимы для отладки и анализа. При фиксировании времени рекомендуется использовать наиболее подробные значения (например, на уровне милли- или микросекунд), чтобы легче было определять смежные события. Также убедитесь, что временные метки стоят в начале сообщения в формате yyyy-mm-dd HH:mm:ss. Всегда указывайте часовой пояс, если не используете на сервере время по умолчанию (UTC).
Указывайте источник или происхождение журнальных данных (для DEBUG, TRACE, ERROR)
Это очень полезно для чёткого определения места, которое привело к соответствующему сообщению. Некоторые фреймворки для журналирования позволяют указывать источники на самом подробном уровне (вплоть до название файлов с номерами строк), но чаще всего достаточно упоминания только класса, функции или названия файла.
Каждый журнал должен быть уникален в рамках системы
Большинство новичков совершают одну и ту же ошибку — копипастят образец сообщения в разных файлах, собирая финальный журнал из одинаковых строк, приходящих из разных частей системы. В этом случае трудно отследить конкретное место, которое вызвало это событие. Если набор слов нельзя менять, то хотя бы упомяните в сообщении источник, чтобы строки в финальном файле отличались друг от друга. Кроме того, если журналирование обрабатывается родительским классом, то отправляйте при инициализации идентификатор и применяйте его для записи сообщений о поведении дочерних классов.
Добавьте в сообщение отслеживаемый идентификатор или токен сообщения
Когда событие или сообщение попадает в систему, ему обычно присваивается уникальный идентификатор. Его можно передавать на другие этапы обработки, чтобы отслеживать движение события по системе, это полезно для отладки, конкурентной обработки и операций, связанных с данными. В идеале, в системе должен быть уникальный идентификатор события в рамках всех модулей и сервисов.
Указывайте соответствие идентификаторов в точках перехода
В определённых случаях, особенно при передаче события из одной системы в другую, идентификаторы меняются в соответствии с принятым в другой системе соглашением. В таких точках перехода нужно отдельным сообщением явно указывать соответствие старого и нового идентификатора, с добавлением необходимого контекста.
Указывайте идентификаторы всех экземпляров сервиса
Большинство enterprise-систем являются распределёнными вычислительными платформами, в которых присутствует много экземпляров одних и тех же сервисов с разнообразными конфигурациями приложения, ресурсами, версиями и сетевыми свойствами. Рекомендуется присвоить экземплярам идентификаторы и использовать их при межсервисном взаимодействии.
Настройте активный уровень журналирования
Активный уровень журналирования нужно менять в зависимости от окружения развёртывания. Для production рекомендуется выводить журналы вплоть до уровня INFO. В других окружениях журналы выводятся до уровня DEBUG или TRACE, в зависимости от степени подробности, которая нужна командам разработки и эксплуатации.
Предоставляйте достаточный контекст для ошибок и сбоев
Ошибки и сбои требуют самого тщательного расследования. Для этого приложение должно предоставлять профильным специалистам необходимую информацию, а также технологический и бизнес-контекст. Например, если запрос или сообщение не удалось обработать, то в дополнение к ошибке очень полезно журналировать и тело сбойного запроса.
Подтверждайте доказательствами операции с данными (не предполагайте!)
При каждой операции с данными не принимайте на веру успешность её выполнения. Всегда проверяйте конечное состояние с помощью доказательств. Например, когда вы создаёте, обновляете или удаляете запись в базе данных, она возвращает счётчик изменённых записей и саму обновлённую запись. Всегда выполняйте проверку ожидаемого счётчика или результата. Другой пример: когда вы вставляете запись в структуру данных (например, в очередь), то проверяйте, увеличился ли её размер. Предположим, в вашей системе используются конкурентные операции, но очередь их не поддерживает. Тогда вы можете терять какие-то записи, и единственный способ обнаружить такие скрытые ошибки в коде — это проверка длины.
Шифруйте или маскируйте конфиденциальные данные
Обычно закон требует маскировать и/или шифровать конфиденциальную информацию пользователей и внутренних систем. В зависимости от вашей отрасли и места работы могут меняться и требования регуляторов. Выясните все нюансы и реализуйте в приложении корректные процедуры. В некоторых случаях перед началом эксплуатации вам может потребоваться представить стратегию журналирования службе безопасности и получить их утверждение или сертификат.
Настройте именование файлов журналов, политики ротации, размер хранилищ и процедуры резервного копирования
Если вы пишете журналы в файлы, то убедитесь, что они хранятся на отдельном диске, который никак не влияет на работающее приложение (например, можно выделить том на удалённом сервере и прикрепить его к серверу приложений). Также выясните частоту журналирования и характер увеличения файлов. Убедитесь, что у вас есть политика ротации журналов с корректными соглашениями по наименованиям (например, при создании к названию добавляется временная метка), чтобы поддерживать идеальный размер и количество файлов. Также у вас должен быть механизм резервного копирования старых журналов в безопасное место, и механизм регулярной очистки хранилищ. В зависимости от вашей отрасли можно настроить резервное копирование по времени (обычно каждые несколько месяцев или лет), с уничтожением всех предыдущих файлов в конце периода.
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 / Самописное
