Статьи

Реализация идентификаторов корреляции в Spring Boot (для распределенной трассировки в SOA / микросервисах)

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

Я видел преимущества идентификаторов корреляции в нескольких недавних проектах SOA, над которыми я работал, но, как Сэм упоминал в своих выступлениях, часто очень легко думать, что этот тип трассировки не понадобится при создании начальной версии приложения. , но тогда очень сложно внедрить приложение, когда вы понимаете преимущества (и необходимость!). Я еще не нашел идеальный способ реализовать корреляционные идентификаторы в приложении на основе Java / Spring, но после общения с Сэмом по электронной почте он сделал несколько предложений, которые я теперь превратил в простой проект с использованием Spring Boot, чтобы продемонстрировать, как это может быть реализованным.

Почему?

Во время обеих бесед Сэма с Geecon он упомянул, что в его опыте идентификаторы корреляции были очень полезны для диагностических целей. Идентификаторы корреляции — это, по сути, идентификатор, который генерируется и связывается с одним (обычно управляемым пользователем) запросом в приложении, которое передается через стек и на зависимые сервисы. В SOA или микросервисных платформах этот тип идентификатора очень полезен, так как запросы в приложение обычно «разветвляются» или обрабатываются множеством нисходящих сервисов, а идентификатор корреляции позволяет всем нисходящим запросам (от начальной точки запроса) быть сопоставлены или сгруппированы на основе идентификатора. Затем можно выполнить так называемую «распределенную трассировку» с использованием идентификаторов корреляции, объединив все журналы сервисов нисходящего потока и сопоставив требуемый идентификатор, чтобы увидеть трассировку запроса по всему стеку приложений (что очень легко, если вы используете централизованное ведение журнала рамки, такие как logstash ).

Крупные игроки в сервис-ориентированной области уже довольно давно говорят о необходимости распределенной трассировки и корреляции запросов, и поэтому Twitter создал свою среду с открытым исходным кодом Zipkin (которая часто подключается к их RPC-инфраструктуре Finagle ) и Netflix. открыла исходный код своей веб / микросервисной платформы Karyon, которая обеспечивает распределенную трассировку. Есть, конечно, коммерческие предложения в этой области, одним из таких продуктов является AppDynamics , который очень классный, но имеет довольно высокую цену.

Создание подтверждения концепции в Spring Boot

Как бы ни были Зипкин и Кэрион, они оба относительно инвазивны, так как вы должны строить свои сервисы поверх (часто самоуверенного) фреймворков. Это может быть хорошо для некоторых случаев использования, но не так много для других, особенно когда вы создаете микросервисы. В последнее время я с удовольствием экспериментировал с Spring Boot , и этот фреймворк основан на широко известном и любимом (по крайней мере, мне!) Фреймворке Spring, предоставляя множество предварительно настроенных разумных значений по умолчанию. Это позволяет очень быстро создавать микросервисы (особенно те, которые взаимодействуют через интерфейсы RESTful). В оставшейся части этого блога объясняется, как я реализовал (надеюсь) неинвазивный способ реализации корреляционных идентификаторов.

цели

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

Реализация

Я создал два проекта на GitHub, один из которых содержит реализацию, в которой все запросы обрабатываются в синхронном стиле (т. Е. Традиционный подход Spring для обработки всех запросов в одном потоке), а также один для асинхронных (неблокирующих) ) используется стиль связи (т. е. используется асинхронная поддержка Servlet 3 в сочетании с Spring DeferredResult и Java Futures / Callables). Большая часть этой статьи описывает асинхронную реализацию, так как это более интересно:

Основная работа в обеих базах кода выполняется CorrelationHeaderFilter, который является стандартным фильтром Java EE, который проверяет заголовок HttpServletRequest на наличие CorlationId. Если он найден, мы устанавливаем переменную ThreadLocal в классе RequestCorrelation (будет обсуждаться позже). Если идентификатор корреляции не найден, он генерируется и добавляется в класс RequestCorrelation:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CorrelationHeaderFilter implements Filter {
 
    //...
 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
 
        final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String currentCorrId = httpServletRequest.getHeader(RequestCorrelation.CORRELATION_ID_HEADER);
 
        if (!currentRequestIsAsyncDispatcher(httpServletRequest)) {
            if (currentCorrId == null) {
                currentCorrId = UUID.randomUUID().toString();
                LOGGER.info("No correlationId found in Header. Generated : " + currentCorrId);
            } else {
                LOGGER.info("Found correlationId in Header : " + currentCorrId);
            }
 
            RequestCorrelation.setId(currentCorrId);
        }
 
        filterChain.doFilter(httpServletRequest, servletResponse);
    }
 
 
    //...
 
    private boolean currentRequestIsAsyncDispatcher(HttpServletRequest httpServletRequest) {
        return httpServletRequest.getDispatcherType().equals(DispatcherType.ASYNC);
    }

Единственная вещь в этом коде, которая может быть не сразу очевидна, — это условная проверка currentRequestIsAsyncDispatcher (httpServletRequest) , но она предназначена для защиты от кода id корреляции, который выполняется, когда выполняется поток Async Dispatcher, чтобы вернуть результаты (это интересно обратите внимание, поскольку я изначально не ожидал, что Async Dispatcher снова запустит выполнение фильтра!).

Вот класс RequestCorrelation, который содержит простую статическую переменную ThreadLocal <String> для хранения идентификатора корреляции для текущего потока выполнения (установленного через CorrelationHeaderFilter выше):

01
02
03
04
05
06
07
08
09
10
11
public class RequestCorrelation {
 
    public static final String CORRELATION_ID = "correlationId";
 
    private static final ThreadLocal<String> id = new ThreadLocal<String>();
 
 
    public static String getId() { return id.get(); }
 
    public static void setId(String correlationId) { id.set(correlationId); }
}

После сохранения идентификатора корреляции в классе RequestCorrelation его можно извлечь и добавить в нисходящие запросы на обслуживание (или доступ к хранилищу данных и т. Д.), Как требуется, путем вызова статического метода getId () в RequestCorrelation. Вероятно, хорошей идеей будет инкапсулировать это поведение в стороне от служб приложений, и вы можете увидеть пример того, как это сделать, в созданном мной классе RestClient, который составляет Spring RestTemplate и обрабатывает установку идентификатора корреляции в заголовке. прозрачно из вызывающего класса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
public class CorrelatingRestClient implements RestClient {
 
    private RestTemplate restTemplate = new RestTemplate();
 
    @Override
    public String getForString(String uri) {
        String correlationId = RequestCorrelation.getId();
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set(RequestCorrelation.CORRELATION_ID, correlationId);
 
        LOGGER.info("start REST request to {} with correlationId {}", uri, correlationId);
 
        //TODO: error-handling and fault-tolerance in production
        ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET,
                new HttpEntity<String>(httpHeaders), String.class);
 
        LOGGER.info("completed REST request to {} with correlationId {}", uri, correlationId);
 
        return response.getBody();
    }
}
 
 
//... calling Class
public String exampleMethod() {
        RestClient restClient = new CorrelatingRestClient();
        return restClient.getForString(URI_LOCATION); //correlation id handling completely abstracted to RestClient impl
}

Заставить это работать для асинхронных запросов …

Приведенный выше код прекрасно работает, когда вы обрабатываете все ваши запросы синхронно, но на платформе SOA / microservice часто неплохо обрабатывать запросы неблокирующим асинхронным образом. Весной этого можно достичь с помощью класса DeferredResult в сочетании с асинхронной поддержкой Servlet 3. Проблема с использованием переменных ThreadLocal в асинхронном подходе заключается в том, что поток, который первоначально обрабатывает запрос (и создает DeferredResult / Future), не будет потоком, выполняющим фактическую обработку.

Соответственно, требуется немного связующего кода, чтобы гарантировать, что идентификатор корреляции распространяется по потокам. Это может быть достигнуто путем расширения Callable с необходимой функциональностью: (не беспокойтесь, если пример кода вызывающего класса не выглядит интуитивно понятным — эта адаптация между DeferredResults и Futures — необходимое зло в Spring, а полный код, включая шаблон ListenableFutureAdapter, в моем репозитории GitHub):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CorrelationCallable<V> implements Callable<V> {
 
    private String correlationId;
    private Callable<V> callable;
 
    public CorrelationCallable(Callable<V> targetCallable) {
        correlationId = RequestCorrelation.getId();
        callable = targetCallable;
    }
 
    @Override
    public V call() throws Exception {
        RequestCorrelation.setId(correlationId);
        return callable.call();
    }
}
 
//... Calling Class
 
@RequestMapping("externalNews")
public DeferredResult<String> externalNews() {
    return new ListenableFutureAdapter<>(service.submit(new CorrelationCallable<>(externalNewsService::getNews)));
}

И у нас это есть — распространение идентификатора корреляции независимо от синхронного / асинхронного характера обработки!

Вы можете клонировать отчет Github, содержащий мой асинхронный пример, и запустить приложение, запустив mvn spring-boot: run в командной строке. Если вы откроете http: // localhost: 8080 / externalNews в своем браузере (или с помощью curl), вы увидите нечто похожее на следующее в консоли Spring Boot, которое четко демонстрирует идентификатор корреляции, сгенерированный при первоначальном запросе, а затем это передается через имитированный внешний вызов (загляните в класс ExternalNewsServiceRest, чтобы увидеть, как это было реализовано):

1
2
3
4
[nio-8080-exec-1] u.c.t.e.c.w.f.CorrelationHeaderFilter    : No correlationId found in Header. Generated : d205991b-c613-4acd-97b8-97112b2b2ad0
[pool-1-thread-1] u.c.t.e.c.w.c.CorrelatingRestClient      : start REST request to http://localhost:8080/news with correlationId d205991b-c613-4acd-97b8-97112b2b2ad0
[nio-8080-exec-2] u.c.t.e.c.w.f.CorrelationHeaderFilter    : Found correlationId in Header : d205991b-c613-4acd-97b8-97112b2b2ad0
[pool-1-thread-1] u.c.t.e.c.w.c.CorrelatingRestClient      : completed REST request to http://localhost:8080/news with correlationId d205991b-c613-4acd-97b8-97112b2b2ad0

Вывод

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

Я хотел бы выразить огромную благодарность Сэму не только за то, что он поделился своими знаниями на великих переговорах в Geecon, но и за то, что нашел время ответить на мои электронные письма. Если вы интересуетесь микросервисами и связанной с ними работой, я настоятельно рекомендую книгу Микросервиса Сэма, которая доступна в разделе «Ранний доступ» в O’Reilly . Я с удовольствием прочитал доступные на данный момент главы, и, реализовав довольно много SOA-проектов в последнее время, я могу сослаться на множество полезных советов, содержащихся в нем. Я буду следить за развитием этой книги с большим интересом!

Ресурсы

Я несколько раз использовал отличный блог Томаша Нуркевича, чтобы узнать, как лучше всего связать весь код DeferredResult / Future весной:

http://www.nurkiewicz.com/2013/03/deferredresult-asynchronous-processing.html