Напишите собственную структуру отслеживания распределенных ссылок. 1. Генерация и передача TraceId/SpanId/ParentSpanId.

задняя часть Архитектура

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

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

Адрес источника

Сначала перечислите свои потребности.

Заявление о потребностях

  1. На основе синхронного HTTP-вызова он может осуществлять передачу TraceId, генерацию и передачу SpanId и получение ParentSpanId.
  2. Прикладной уровень не имеет восприятия, и запрос на обслуживание не должен отображать информацию о канале передачи.
  3. Поддержка Spring Framework

Реализация и анализ кода

Имя проекта называется lite-tracer-agent.

1. Генерация и хранение TraceId, SpanId и ParentSpanId

Поскольку мы хотим реализовать передачу TraceId, генерацию и передачу SpanId, получение ParentSpanId и другие функции, мы в первую очередь думаем о перехватчике Spring.
Данные собираются и сохраняются в методе preHandle().

preHandle(..)方法返回boolean值,可用此方法停止或继续处理过程。
如果此方法返回true,则处理过程继续,
如果此方法返回false,DispatcherServlet认为该截断器接手处理请求 (比如,渲染一个视图),
所以就不再执行别的截断器和处理器。

Код следующий, расположение в проекте: lite-tracer/lite-tracer-agent/src/main/java/com.snowalker.tracer.interceptor.TraceInterceptor

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    LOGGER.info("[TraceInterceptor]进入Trace拦截器[preHandle()]");
    String traceId = request.getHeader(Constants.HTTP_HEADER_TRACE_ID);
    String lastSpanId = request.getHeader(Constants.HTTP_HEADER_SPAN_ID);
    String parentSpanId = "";
    /**当前SpanId*/
    String spanId = "";
    traceId = validateTraceId(traceId);
    parentSpanId = getParentSpanId(lastSpanId);
    spanId = getSpanId();
    /**将Trace信息写入线程局部变量*/
    saveTraceInfoToRequestContext(traceId, parentSpanId, spanId);
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("[TraceInterceptor]当前请求中,TraceId={},SpanId={},ParentSpanId={}", traceId, spanId, parentSpanId);
    }
    return true;
}

Поскольку мы передаем TraceId, SpanId и parentSpanId в протоколе прикладного уровня, заголовок Http является лучшим местом для передачи параметров. Здесь осуществляется только передача уровня протокола HTTP.
Чтобы реализовать передачу в протоколе RPC, нам нужно добавить настраиваемые поля в протокол сериализации RPC для передачи TraceId и SpanId.

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

/**每一次的SpanId都是新的,直接分配即可*/
private String getSpanId() {
    String spanId;
    spanId = IdGenerator.getInstance().nextSpanId();
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("[TraceInterceptor]当前请求的SpanId={}", spanId);
    }
    return spanId;
}

Для ParentSpanId при выполнении первого запроса родительский диапазон не существует, поэтому значение по умолчанию равно -1.При выполнении последующего анализа, если родительский диапазон узла трассировки равен -1, это означает, что этот запрос
Это первый запрос, который является корневым узлом в структуре дерева трассировки, упомянутой в статье Даппера.

/**
 * 当前请求的parentSpanId就是上个spanId
 * 如果上一个spanId为空,则表明当前没有父Span,则父Span为-1
 */
private String getParentSpanId(String lastSpanId) {
    String parentSpanId;
    if (StringUtils.isEmpty(lastSpanId)) {
        parentSpanId = "-1";
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("[TraceInterceptor]首次请求获取到的ParentSpanId不存在,默认分配ParentSpanId={}", parentSpanId);
        }
    } else {
        parentSpanId = lastSpanId;
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("[TraceInterceptor]当前请求获取到的ParentSpanId={}", parentSpanId);
        }
    }
    return parentSpanId;
}

Для самого важного TraceId первый запрос пустой, если TraceId полученный из Header пустой, то будет присвоена порция информации, если не пустая, то это означает, что текущий запрос является одной из ссылок запроса.
Запросите, просто передайте это значение напрямую.

/**TraceId默认第一个为空,如果没值则分配一个*/
private String validateTraceId(String traceId) {
    if (StringUtils.isEmpty(traceId)) {
        traceId = IdGenerator.getInstance().nextTraceId();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("[TraceInterceptor]首次请求未分配TraceId,生成首次TraceId={}", traceId);
        }
    }
    return traceId;
}

После получения TraceId, SpanId и ParentSpanId их необходимо сохранить и передать, причем сохранить непосредственно в ThreadLocal, поскольку каждый запрос в JavaWeb представляет собой
нить для обработки.

В этом коде мы получаем TraceId и SpanId из заголовка запроса и сохраняем их в ThreadLoacl.Детальный процесс сохранения выглядит следующим образом.

private void saveTraceInfoToRequestContext(String traceId, String parentSpanId, String spanId) {
    RequestContext.addTraceId(traceId);
    RequestContext.addSpanId(spanId);
    RequestContext.addParentSpanId(parentSpanId);
}

RequestContext инкапсулирует операции, соответствующие ThreadLocal.

public class RequestContext {

    private final static ThreadLocal<String> traceIdThreadLocal = new ThreadLocal<>();
    private final static ThreadLocal<String> spanIdThreadLocal = new ThreadLocal<>();
    private final static ThreadLocal<String> parentSpanIdThreadLocal = new ThreadLocal<>();

    public static void addTraceId(String id) {
        traceIdThreadLocal.set(id);
    }

    public static String getTraceId() {
        return traceIdThreadLocal.get();
    }

    public static void removeTraceId() {
        traceIdThreadLocal.remove();
    }

    public static void addSpanId(String id) {
        spanIdThreadLocal.set(id);
    }

    public static String getSpanId() {
        return spanIdThreadLocal.get();
    }

    public static void removeSpanId() {
        spanIdThreadLocal.remove();
    }

    public static void addParentSpanId(String id) {
        parentSpanIdThreadLocal.set(id);
    }

    public static String getParentSpanId() {
        return parentSpanIdThreadLocal.get();
    }

    public static void removeParentSpanId() {
        parentSpanIdThreadLocal.remove();
    }
}

Целью этого является получение соответствующих данных ссылки в любое время в процессе.

2. Очистка TraceId, SpanId и ParentSpanId

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

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    RequestContext.removeTraceId();
    RequestContext.removeSpanId();
    RequestContext.removeParentSpanId();
    LOGGER.info("[TraceInterceptor]进入Trace拦截器[afterCompletion]清理本次请求的trace信息完成");
    return;
}


方法afterCompletion:该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。
顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。
这个方法的主要作用是用于进行资源清理工作的。

Мы просто вызываем метод remove() класса ThreadLocal, чтобы очистить временно сохраненное значение, чтобы его можно было обновить при следующем поступлении запроса.

3. Инкапсулируйте RestTemplate для передачи информации о трассировке

Приемная часть информации о трассировке в основном завершена, нам еще нужно передать эту часть информации, здесь нам нужно переписать HTTP-клиент и записать информацию о трассировке в HTTP-заголовок.

Здесь я оборачиваю его на основе RestTemplate и инкапсулирую логику передачи части Trace-информации.

@Component
public class RestTemplateWrapper {
    /**
     * 获取包装trace信息之后的RestTemplate
     * @return
     */
    public RestTemplate getRestTemlate() {
        // 使用拦截器包装http header
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(new ArrayList<ClientHttpRequestInterceptor>() {
            {
                add((request, body, execution) -> {
                    String traceId = RequestContext.getTraceId();
                    String spanId = RequestContext.getSpanId();
                    String parentSpanId = RequestContext.getParentSpanId();

                    if (StringUtils.isNotEmpty(traceId)) {
                        request.getHeaders().add(Constants.HTTP_HEADER_TRACE_ID, traceId);
                    }
                    if (StringUtils.isNotEmpty(spanId)) {
                        request.getHeaders().add(Constants.HTTP_HEADER_SPAN_ID, spanId);
                    }
                    if (StringUtils.isNotEmpty(parentSpanId)) {
                        request.getHeaders().add(Constants.HTTP_HEADER_PARENT_SPAN_ID, parentSpanId);
                    }
                    return execution.execute(request, body);
                });
            }
        });
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        // 注意此处需开启缓存,否则会报getBodyInternal方法“getBody not supported”错误
        factory.setBufferRequestBody(true);
        restTemplate.setRequestFactory(factory);
        return restTemplate;
    }
}

Для краткого пояснения мы добавили фильтр ClientHttpRequestInterceptor в нативный RestTemplate, а TraceId, полученный из контекста, добавили в заголовок запроса,
SpanId, ParentSpanId и другая информация. Таким образом, при использовании упакованного RestTemplate для инициирования запроса Http информация о трассировке может быть неявно передана в заголовке Http.

Поскольку используется HttpComponentsClientHttpRequestFactory, необходимо добавить пакет Httpclient, а зависимости следующие:

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.3</version>
</dependency>

боевое испытание

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

Для удобства я создал здесь два веб-проекта, оба из которых являются проектами Springboot, с именами tracer-demo-A, tracer-demo-B.

Тестовый сценарий заключается в том, что запрос отправляется на интерфейс demoA, а интерфейс demoA отправит другой запрос на demoB после обработки запроса и проанализирует, пришел ли запрос или нет в соответствии с журналом.
Информация о трассировке генерируется во время demoA и успешно передается в demoB в соответствии с правилами.

основной код tracer-demo-A

Здесь я показываю только основной код demoA, DemoControllerA.java.

@RestController
public class DemoControllerA {

    private static final Logger LOGGER = LoggerFactory.getLogger(DemoController.class);

    @Autowired
    RestTemplateWrapper restTemplateWrapper;

    @RequestMapping(value = "trace")
    public @ResponseBody String trace(HttpServletRequest request, HttpServletResponse response) {
        LOGGER.info("[trace]--traceId={},spanId={},parantSpanId={}",
                RequestContext.getTraceId(),
                RequestContext.getSpanId(),
                RequestContext.getParentSpanId());
        String result =
                restTemplateWrapper.getRestTemlate().getForObject("http://localhost:8081/trace", String.class);

        return "Send Message To B,result= " + result;
    }
}

Мы отправляем запрос наПосле запроса http://localhost:8080/trace вывести строку лога и перенаправить запрос на адрес http://localhost:8081/trace
и вывести возвращаемое значение интерфейса demoB в теле ответа интерфейса.

Основной код tracer-demo-B

@RestController
public class DemoControllerB {

    private static final Logger LOGGER = LoggerFactory.getLogger(DemoController.class);

    @RequestMapping(value = "trace")
    public String trace(HttpServletRequest request, HttpServletResponse response) {
        LOGGER.info("[trace]--traceId={},spanId={},parantSpanId={}",
                RequestContext.getTraceId(),
                RequestContext.getSpanId(),
                RequestContext.getParentSpanId());
        return "[trace]--traceId="+ RequestContext.getTraceId() +
                ",spanId=" + RequestContext.getSpanId() +
                ",parantSpanId={}" + RequestContext.getParentSpanId() +
                "\nrequestUrl=" + request.getRequestURL();
    }

}

Логика demoB очень проста, то есть после получения запроса он печатает лог и возвращает строку.

результат операции

Мы запускаем две демонстрации с портами 8080 и 8081 соответственно. Для всестороннего анализа включите уровень журнала для отладки.

отправить запрос наhttp://localhost:8080/trace, вы можете получить возвращаемую строку как:

Send Message To B,result= [trace]--traceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,spanId=SPAN-e87d82079daa4ea792a782a083451250,parantSpanId={}SPAN-de63425a1dc445e5a93d1d2369cafc3b
requestUrl=http://localhost:8081/trace

Возврат demo B успешно завершен запросом demo A. Давайте посмотрим на результат печати журнала.

1. демо журнал

Сначала посмотрите на журнал demoA.

2018-09-03 14:14:28.514 -INFO  [http-nio-8080-exec-5] 
com.snowalker.tracer.interceptor.TraceInterceptor [26] -
[TraceInterceptor]进入Trace拦截器[preHandle()]
2018-09-03 14:14:28.514 -DEBUG [http-nio-8080-exec-5] 
com.snowalker.tracer.interceptor.TraceInterceptor [84] -
[TraceInterceptor]首次请求未分配TraceId,生成首次TraceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0
2018-09-03 14:14:28.515 -DEBUG [http-nio-8080-exec-5] 
com.snowalker.tracer.interceptor.TraceInterceptor [68] -
[TraceInterceptor]首次请求获取到的ParentSpanId不存在,默认分配ParentSpanId=-1
2018-09-03 14:14:28.515 -DEBUG [http-nio-8080-exec-5] 
com.snowalker.tracer.interceptor.TraceInterceptor [54] -
[TraceInterceptor]当前请求的SpanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b
2018-09-03 14:14:28.515 -DEBUG [http-nio-8080-exec-5] 
com.snowalker.tracer.interceptor.TraceInterceptor [38] -
[TraceInterceptor]当前请求中,TraceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,SpanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b,ParentSpanId=-1
2018-09-03 14:14:28.515 -INFO  [http-nio-8080-exec-5] 
com.snowalker.tracer.aop.TraceHandler [35] 
-traceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,spanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b,parentSpanId=-1,方法执行[开始],methodName=trace
2018-09-03 14:14:28.515 -INFO  [http-nio-8080-exec-5] 
com.snowalker.tracer.demoA.controller.DemoController [35] -[trace]
--traceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,spanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b,parantSpanId=-1
2018-09-03 14:14:28.869 -INFO  [http-nio-8080-exec-5] 
com.snowalker.tracer.aop.TraceHandler [40] 
-traceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,spanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b,parentSpanId=-1,方法执行[结束],methodName=trace
2018-09-03 14:14:28.884 -DEBUG [http-nio-8080-exec-5] 
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor [249] 
-Written [Send Message To B,result= [trace]
--traceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,spanId=SPAN-e87d82079daa4ea792a782a083451250,parantSpanId={}SPAN-de63425a1dc445e5a93d1d2369cafc3b
2018-09-03 14:14:28.884 -INFO  [http-nio-8080-exec-5] 
com.snowalker.tracer.interceptor.TraceInterceptor [96] 
-[TraceInterceptor]进入Trace拦截器[afterCompletion]清理本次请求的trace信息完成
2018-09-03 14:14:28.884 -DEBUG [http-nio-8080-exec-5] 
org.springframework.web.servlet.DispatcherServlet [1000] 
-Successfully completed request

lite-tracer-agent считает, что это первый запрос, поэтому генерирует traceId и SpanId. Так как это первый раз, родительский SpanId не существует, поэтому ему присваивается значение -1. Здесь все соответствует нашему дизайну.

Затем посмотрите журнал demoB.

2018-09-03 14:14:28.714 -INFO  [http-nio-8081-exec-10] 
com.snowalker.tracer.interceptor.TraceInterceptor [26] -
[TraceInterceptor]进入Trace拦截器[preHandle()]
2018-09-03 14:14:28.718 -DEBUG [http-nio-8081-exec-10] 
com.snowalker.tracer.interceptor.TraceInterceptor [73] -
[TraceInterceptor]当前请求获取到的ParentSpanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b
2018-09-03 14:14:28.720 -DEBUG [http-nio-8081-exec-10] 
com.snowalker.tracer.interceptor.TraceInterceptor [54] -
[TraceInterceptor]当前请求的SpanId=SPAN-e87d82079daa4ea792a782a083451250
2018-09-03 14:14:28.720 -DEBUG [http-nio-8081-exec-10] 
com.snowalker.tracer.interceptor.TraceInterceptor [38] -
[TraceInterceptor]当前请求中,TraceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,SpanId=SPAN-e87d82079daa4ea792a782a083451250,ParentSpanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b
2018-09-03 14:14:28.737 -INFO  [http-nio-8081-exec-10] 
com.snowalker.tracer.aop.TraceHandler [35] 
-traceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,spanId=SPAN-e87d82079daa4ea792a782a083451250,parentSpanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b,方法执行[开始],methodName=trace
2018-09-03 14:14:28.746 -INFO  [http-nio-8081-exec-10] 
com.snowalker.tracer.demoB.controller.DemoController [24] 
-[trace]--traceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,spanId=SPAN-e87d82079daa4ea792a782a083451250,parantSpanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b
2018-09-03 14:14:28.746 -INFO  [http-nio-8081-exec-10] 
com.snowalker.tracer.aop.TraceHandler [40] 
-traceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,spanId=SPAN-e87d82079daa4ea792a782a083451250,parentSpanId=SPAN-de63425a1dc445e5a93d1d2369cafc3b,方法执行[结束],methodName=trace
2018-09-03 14:14:28.798 -DEBUG [http-nio-8081-exec-10] 
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor [249] 
-Written [[trace]--traceId=TRACE-3b63c42d0a1244b690b77e3e035a7cc0,spanId=SPAN-e87d82079daa4ea792a782a083451250,parantSpanId={}SPAN-de63425a1dc445e5a93d1d2369cafc3b
2018-09-03 14:14:28.799 -INFO  [http-nio-8081-exec-10] 
com.snowalker.tracer.interceptor.TraceInterceptor [96]
 -[TraceInterceptor]进入Trace拦截器[afterCompletion]清理本次请求的trace信息完成
2018-09-03 14:14:28.799 -DEBUG [http-nio-8081-exec-10] 
org.springframework.web.servlet.DispatcherServlet [1000] 
-Successfully completed request

Запрос отправляется из службы demoA в службу demoB, перехватывается перехватчиком lite-tracer-agent, анализирует TraceId и устанавливает в него значение ThreadLocal, соответствующее demoB,
Сгенерируйте SpanId, соответствующий demoB, и установите SpanId для demoA равным ParentSpanId текущего запроса.

Идеальная работа~

Просто шучу, на данный момент мы успешно достигли передачи информации об звесе в службах HTTP и моделирование базовой модели Dapper. Конечно, этот проект по-прежнему несовершенны, например

  1. Трассировка пулов потоков не реализована
  2. Трассировка очередей не реализована
  3. Такие операции, как анализ информации о ссылках, не реализованы

Эти функции будут постепенно улучшаться в будущем.

резюме

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

использованная литература

Google dapper paper китайский перевод