предисловие
В традиционных системах, если можно обеспечить вывод журнала, он может в основном удовлетворить требования. Но как только система разделена на две или более систем, в сочетании с балансировкой нагрузки и т. д., связь вызова становится сложной.
Особенно в направлении дальнейшей эволюции к микросервисам, если нет разумного планирования и отслеживания ссылок журналов, устранить неполадки журналов будет крайне сложно.
Например, в системах A, B и C ссылка вызова A -> B -> C. Если каждый набор услуг является активным-активным, путь вызова имеет 2-кубическую возможность. Если будет больше систем и больше услуг, цепочка вызовов будет расти в геометрической прогрессии.
Поэтому, будь то несколько простых вызовов внутренних сервисов или сложная микросервисная система, необходим механизм для реализации отслеживания ссылок журналов. Пусть вывод журнала вашей системы будет красивым по форме, как стихотворение, и будет иметь гармоничный ритм.
На самом деле существует множество готовых фреймворков для отслеживания логов, таких как Sleuth, Zipkin и другие компоненты. Но это не является предметом нашего обсуждения.Эта статья посвящена реализации простой функции отслеживания ссылок вызовов журнала вручную на основе Spring Boot и LogBack. Основываясь на этом режиме реализации, вы можете реализовать более мелкие детали.
Интеграция Logback в Spring Boot
Сам Spring Boot имеет встроенную функцию логирования, здесь используется фреймворк логбэка, а выходные результаты форматируются. Давайте взглянем на встроенную интеграцию Logback в SpringBoot, Зависимости следующие. Когда проект представляет:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Косвенно представлено в spring-boot-starter-web:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
spring-boot-starter снова представляет стартер ведения журнала:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
Требуемый пакет logback фактически вводится в ведение журнала:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
</dependency>
Поэтому, когда мы его используем, нам нужно только поместить файл конфигурации logback-spring.xml в каталог ресурсов. Теоретически файл конфигурации с именем logback.xml также поддерживается, но официальное веб-сайт Spring Boot рекомендует имя: logback-spring.xml.
Затем настройте вывод журнала в logback-spring.xml. Я не буду публиковать здесь весь код, только соответствующую часть формата вывода журнала, взяв в качестве примера вывод консоли:
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n"/>
В выражение атрибута value мы добавили значение пользовательской переменной requestId, которое отображается в виде «[%X{requestId}]».
Этот requestId — уникальный идентификатор, который мы используем для отслеживания журналов. Если запрос использует один и тот же requestId от начала до конца, вся цепочка запросов может быть объединена. Если система также собирает журналы единообразно на основе инструментов сбора журналов, таких как EKL, более удобно просматривать ссылку вызова всего журнала.
Итак, откуда взялась эта переменная requestId и где она хранится? Чтобы понять это, мы должны сначала понять функциональные возможности MDC, предоставляемые структурой ведения журналов.
Что такое МДК?
MDC (Mapped Diagnostic Contexts) — это потокобезопасный контейнер для хранения журналов диагностики. MDC — это класс инструментов, предоставляемый slf4j для адаптации к другим конкретным пакетам реализации журналов.В настоящее время эту функцию поддерживают только logback и log4j.
MDC является потоконезависимым и потокобезопасным.Обычно запросы HTTP и RPC выполняются в отдельных потоках, что хорошо согласуется с механизмом MDC.
При использовании функции MDC мы в основном используем метод put, который косвенно вызывает метод put интерфейса MDCAdapter.
Взгляните на код в одной из реализаций интерфейса MDCAdapter, BasicMDCAdapter:
public class BasicMDCAdapter implements MDCAdapter {
private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() {
@Override
protected Map<String, String> childValue(Map<String, String> parentValue) {
if (parentValue == null) {
return null;
}
return new HashMap<String, String>(parentValue);
}
};
public void put(String key, String val) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
Map<String, String> map = inheritableThreadLocal.get();
if (map == null) {
map = new HashMap<String, String>();
inheritableThreadLocal.set(map);
}
map.put(key, val);
}
// ...
}
Из исходного кода видно, что экземпляр InheritableThreadLocal хранится внутри, а данные контекста в этом экземпляре сохраняются через HashMap.
Кроме того, MDC предоставляет несколько основных интерфейсов, таких как put/get/clear, для управления данными, хранящимися в ThreadLocal. В logback.xml данные, хранящиеся в MDC, можно получить, объявив «%X{requestId}» в макете, и эту информацию можно распечатать.
На основе этих характеристик MDC часто используется для отслеживания ссылок на журналы, динамической настройки пользовательской информации (например, requestId, sessionId и т. д.) и других сценариев.
реальное боевое применение
Изучив некоторые основные принципы выше, давайте посмотрим, как реализовать отслеживание журналов на основе функции MDC платформы журналов.
подготовка инструмента
Сначала определите некоторые классы инструментов.Настоятельно рекомендуется реализовать некоторые операции в виде классов инструментов.Это часть написания элегантного кода, а также позволяет избежать необходимости менять каждое место в более поздних модификациях.
Класс генерации TraceID (имя параметра мы определяем как requestId), здесь для генерации используется UUID, конечно, он может быть сгенерирован другими методами по вашему сценарию и потребностям.
public class TraceIdUtils {
/**
* 生成traceId
*
* @return TraceId 基于UUID
*/
public static String getTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
}
Класс рабочего инструмента TraceIdContext для контента Context:
public class TraceIdContext {
public static final String TRACE_ID_KEY = "requestId";
public static void setTraceId(String traceId) {
if (StringLocalUtil.isNotEmpty(traceId)) {
MDC.put(TRACE_ID_KEY, traceId);
}
}
public static String getTraceId() {
String traceId = MDC.get(TRACE_ID_KEY);
return traceId == null ? "" : traceId;
}
public static void removeTraceId() {
MDC.remove(TRACE_ID_KEY);
}
public static void clearTraceId() {
MDC.clear();
}
}
Благодаря классу инструмента удобно использовать все сервисы единообразно.Например, requestId можно определить единообразно, чтобы везде избежать различий. Здесь предоставляется не только метод установки, но также методы удаления и очистки.
Следует отметить, что использование метода MDC.clear(). Если все потоки создаются с помощью нового метода Thread, после того, как поток умирает, хранимые данные также умирают, что нормально. Однако, если используется пул потоков, поток можно использовать повторно.Если содержимое MDC предыдущего потока не очищается, а поток снова получен из пула потоков, предыдущие данные (грязные данные) будут удалены, что приведет к некоторым непредсказуемым ошибкам, поэтому его необходимо очистить после завершения текущего потока.
Перехват фильтра
Поскольку мы хотим отслеживать ссылку на журнал, наиболее интуитивно понятной идеей является создание идентификатора запроса в источнике посещения, а затем передача его до тех пор, пока запрос не будет выполнен. Взяв в качестве примера Http, запрос перехватывается фильтром, а данные сохраняются и передаются через заголовок Http. Когда задействован вызов между системами, вызывающий устанавливает requestId в заголовке, и вызываемый может взять его из заголовка.
Определение фильтра:
public class TraceIdRequestLoggingFilter extends AbstractRequestLoggingFilter {
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
String requestId = request.getHeader(TraceIdContext.TRACE_ID_KEY);
if (StringLocalUtil.isNotEmpty(requestId)) {
TraceIdContext.setTraceId(requestId);
} else {
TraceIdContext.setTraceId(TraceIdUtils.getTraceId());
}
}
@Override
protected void afterRequest(HttpServletRequest request, String message) {
TraceIdContext.removeTraceId();
}
}
В методе beforeRequest из заголовка получается requestId. Если его невозможно получить, он считается «источником», генерируется requestId и устанавливается в MDC. Когда запрос завершен, set requestId удаляется, чтобы предотвратить упомянутую выше проблему с пулом потоков. Каждая услуга в системе может быть реализована вышеописанным образом, и вся цепочка запросов связана.
Конечно, фильтр, определенный выше, необходимо инициализировать.Метод создания экземпляра в Spring Boot выглядит следующим образом:
@Configuration
public class TraceIdConfig {
@Bean
public TraceIdRequestLoggingFilter traceIdRequestLoggingFilter() {
return new TraceIdRequestLoggingFilter();
}
}
Для обычных системных вызовов вышеперечисленные методы в принципе могут быть удовлетворены, и на практике вы можете расширяться на этой основе в соответствии со своими потребностями. Здесь используется фильтр, который также можно реализовать через перехватчики, АОП Spring и т.д.
Притворяться в микросервисах
Если ваша система основана на компонентах Feign в Spring Cloud, вы можете добавить requestId, внедрив перехватчик RequestInterceptor. Конкретная реализация выглядит следующим образом:
@Configuration
public class FeignConfig implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(TraceIdContext.TRACE_ID_KEY, TraceIdContext.getTraceId());
}
}
Проверка результата
Когда вышеуказанные операции будут завершены, будет сделан запрос к контроллеру, и будет напечатан следующий журнал:
2021-04-13 10:58:31.092 cloud-sevice-consumer-demo [http-nio-7199-exec-1] INFO [ef76526ca96242bc8e646cdef3ab31e6] c.b.demo.controller.CityController - getCity
2021-04-13 10:58:31.185 cloud-sevice-consumer-demo [http-nio-7199-exec-1] WARN [ef76526ca96242bc8e646cdef3ab31e6] o.s.c.o.l.FeignBlockingLoadBalancerClient -
Вы можете видеть, что requestID был успешно добавлен. Когда мы проверяем журнал, нам нужно только найти информацию о ключе запроса, а затем мы можем объединить весь журнал в соответствии со значением requestId в журнале информации о ключе.
резюме
Наконец, давайте рассмотрим весь процесс отслеживания логов: когда запрос доходит до первого сервера, сервис проверяет, существует ли requestId, если его нет, он создает его и помещает в MDC, когда сервис вызывает другие сервисы, requestId передается через заголовок для прохождения и вывод requestId конфигурации журналирования каждой службы. Чтобы добиться эффекта конкатенации логов от начала до конца.
Изучая эту статью, если вы узнали только об отслеживании журналов, это потеря, потому что статья также включает в себя интеграцию SpringBoot с журналированием, базовую реализацию и ямы MDC, использование фильтров и перехватчик запросов Feign. Если вам интересно, каждый может разойтись и узнать больше очков знаний.