Подумайте о границах системы и событиях для наблюдаемости

Эта статья изначально была опубликована в моем блоге - https://kislayverma.com/programming/publish-events-not-logs/

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

Хотя много было сказано о структурированном ведении журнала вместо типичного ведения журнала сообщений (например, Вызов службы A с идентификатором B… или Произошла ошибка при выполнении X: не удалось найти Y), я хочу продвинуть эту идею дальше, чтобы ответить на вопрос - что регистрировать? Я предлагаю подходить к ведению журнала с точки зрения публикации событий, происходящих между системами и подсистемами. Я предполагаю, что не существует таких вещей, как журналы, есть просто события, которые происходят между двумя взаимодействующими компонентами, и анализ событий дает нам состояние распределенной системы.

Неизбежный марш к структурированному ведению журнала

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

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

Но теперь начинают появляться другие проблемы. Как мы объединяем работу, выполненную в нескольких системах, для одного действия пользователя. Как мы отслеживаем взаимодействия, осуществляемые по асинхронным каналам, таким как Kafka? Что, если программное обеспечение работает в двух центрах обработки данных - как определить, что и где происходит? Наиболее распространенное решение для всего этого - добавлять больше метаданных в каждое сообщение журнала (часто через стандартную библиотеку ведения журнала). Итак, теперь у нас есть идентификатор центра обработки данных, идентификатор корреляции, трассировки / промежутки и другие подобные точки данных, добавляемые к сообщению журнала. Разработчики все еще думают о таких строках.

2020-09-23 17:08:13+0530 INFO  [thread=main] [uId=user54672] [reqId=avsk4ghaioernkva] [cId=5] o.e.j.s.Server  - Started @7217ms
2020-09-23 17:13:12+0530 INFO  [tid=Timer-0] [uId=user54672]  [reqId=avsk4ghaioernkva] [cId=5] c.c.g.t.LocationServiceTimerTask  - Updating cities and countries

Фактически, сообщение журнала фактически стало этим.

{
    “thread” : “main”,
    “uId” : “user54672”,
    “reqId” : “avsk4ghaioernkva”,
    “cId” : 5,
    “class” : “c.c.g.t.LocationServiceTimerTask”,
    “Message” : “Updating cities and countries”
}

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

Что мы ищем?

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

  1. Возникающие ошибки
  2. Состояние системы при возникновении ошибки
  3. Оценка успехов / неудач или различных действий
  4. Операции происходят с разными сущностями.

Этот небольшой набор требований к образцам настолько разнообразен, что многие люди говорят: «Регистрируйте все». Это не является ни необходимым, ни возможным.

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

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

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

Границы - это места, где происходят события

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

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

Где границы?

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

public class InvoiceService {
   public Invoice generateInvoice(Order order) {
       if (order.ItemCount() > 0) {
           double amount = 0;
           long amountCalcStartTime = new Date().getTime();
           for (Item item : order.getItems()) {
               amount += item.getValue();
               int taxAmount = computeTax(); //Some complicated tax logic
               Amount += taxAmount;
           }
           long amountCalcEndTime = new Date().getTime();
           LOG.info("Amount calculation took {} ms", amountCalcEndTime - amountCalcStartTime);
           order.setValue(amount);
           Invoice invoice = convertToInvoice(order);
           long dbWriteStartTime = new Date().getTime();
           InvoiceDao dao = new InvoiceDao();
           Invoice persistedInvoice = dao.saveInvoice(invoice);
           long dbWriteEndTime = new Date().getTime();
           LOG.info("Persisting invoice to DB took {} ms", dbWriteEndTime - dbWriteStartTime);
           return persistedInvoice;
       }
       LOG.error("Persisting invoice to DB took {} ms", dbWriteEndTime - dbWriteStartTime);
       throw new IllegalArgumentException("no items in this order");
   }
}

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

Давайте реорганизуем этот код во что-то вроде этого.

public class InvoiceServiceNew {
   public Invoice generateInvoice(Order order) {
       AbstractAmountCalculator calculator = new ItemAndTaxBasedCalculator();
       order.setAmount(calculator.calculate(order));
       Invoice invoice = convertToInvoice(order);
       long dbWriteStartTime = new Date().getTime();
       InvoiceDao dao = new InvoiceDao();
       Invoice persistedInvoice = dao.saveInvoice(invoice);
       long dbWriteEndTime = new Date().getTime();
       LOG.info("Persisting invoice to DB took {} ms", dbWriteEndTime - dbWriteStartTime);
       return persistedInvoice;
   }
}
public abstract class AbstractAmountCalculator {
   public Double calculate(Order order)  {
       try {
           long amountCalcStartTime = new Date().getTime();
           Double amount = doCalculate(order);
           long amountCalcEndTime = new Date().getTime();
           LOG.info("Amount calculation took {} ms", amountCalcEndTime - amountCalcStartTime);
           LOG.info("Invoice amount for order id {} is {}", order.getId() - amount);
           return amount;
       } catch (Exception e) {
           Log.error(e.getMessage());
           throw e;
       }
   }
   protected abstract Double doCalculate(Order order);
}
public class ItemAndTaxBasedCalculator extends AbstractAmountCalculator {
   @Override
   protected Double doCalculate(Order order) {
       if (order.ItemCount() > 0) {
           for (Item item : order.getItems()) {
               amount += item.getValue();
               int taxAmount = computeTax(item); // Some complicated tax logic
               LOG.info("Tax for item {} is {}", item.getId() - taxAmount);
               amount+= taxAmount;
           }
           return amount;
       }
       throw new IllegalArgumentException("no items in this order");
   }
}

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

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

Структурированные журналы - это всего лишь наполовину оцененное событие

Независимо от того, думаем мы о них так или нет, сообщения журнала - это плохо продуманные события.

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

{
    “eventName” : “INVOICE_AMOUNT_CALACULATION”
    “Type” : SUCCESS,
    “eventTime” : 12344847339485,
    “Metadata” : {
    “thread” : “main”,
    “uId” : “user54672”,
    “reqId” : “avsk4ghaioernkva”,
    “cId” : 5,
    “class” : “c.c.g.t.ItemAndTaxBasedCalculator”,
    “Message” : “Invoice amount is 1000”
    “OrdeId” : 1,
    “InvoiceAmount” : 1000
    }
}

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

Единый режим наблюдения

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

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

Читать далее: другие статьи о распределенных системах.

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