Принцип и внедрение MDC Global Link отслеживание ссылок

Java

задний план

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

MDC (отображенные диагностические контексты) Введение

MDC — важный инструмент для реализации распределенной многопоточной передачи данных журнала в системе журнала, подобной Slf4J.Пользователи могут использовать MDC для печати некоторых данных контекста времени выполнения.В настоящее время только log4j и logback обеспечивают встроенную поддержку MDC.

Анализ исходного кода

MDC поддерживает контекст потокаMap<String,String>Свойства, можно понимать как контейнеры на уровне нитей в обратном плане,%X{key}Получить значение в контексте MDCИнтерфейс предоставлен MDC

public interface MDCAdapter {
    // 获取当前线程MDC上下文中指定key的值
    void put(String var1, String var2);
	// 往当前线程MDC上下文中
    String get(String var1);
	// 移除当前线程MDC上下文中指定key的键值
    void remove(String var1);
	// 清空MDC上下文
    void clear();
	// 获取MDC上下文
    Map<String, String> getCopyOfContextMap();
	// 设置MDC上下文呢
    void setContextMap(Map<String, String> var1);
}

Реализация логбэка LogbackMDCAdapter

public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();
    private static final int WRITE_OPERATION = 1;
    private static final int MAP_COPY_OPERATION = 2;
    final ThreadLocal<Integer> lastOperation = new ThreadLocal();

    public LogbackMDCAdapter() {
    }
    ...

Вы можете видеть, что LogbackMDC объявляет карту типа ThreadLocal. ThreadLocal предоставляет экземпляры локальных переменных потока. Разница между ним и обычными переменными заключается в том, что каждый поток, использующий переменную, будет инициализировать полностью независимую копию экземпляра, а это означает, что переменные ThreadLocal изолированы между потоками и могут совместно использоваться методами или классами.

put() в LogbackMDCAdapter

  public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        } else {
            Map<String, String> oldMap = (Map)this.copyOnThreadLocal.get();
            Integer lastOp = this.getAndSetLastOperation(1);
            if (!this.wasLastOpReadOrNull(lastOp) && oldMap != null) {
                oldMap.put(key, val);
            } else {
                Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
                newMap.put(key, val);
            }

        }
    }

Проверьте и установите значение ключа для контейнера ThreadLocal.

duplicateAndInsertNewMap()

 private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap());
        if (oldMap != null) {
            synchronized(oldMap) {
                newMap.putAll(oldMap);
            }
        }

        this.copyOnThreadLocal.set(newMap);
        return newMap;
    }

Создайте потокобезопасный HashMap в качестве контейнера и поместите его в ThreadLocal.

Схема и реализация

Объявить фильтр для перехвата запросов

@Configuration
public class FilterConfig {
 @Bean
 public FilterRegistrationBean registFilter() {
     FilterRegistrationBean registration = new FilterRegistrationBean();
     registration.setFilter(new DyeFilter());
     registration.addUrlPatterns("/*");
     registration.setName("DyeFilter");
     registration.setOrder(1);
     return registration;
 }
}

Создайте объект контекста и назначьте его MDC

@WebFilter(filterName = "DyeFilter")
public class DyeFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest request = (HttpServletRequest) req;
        initGlobalContext(request);
        try {
            chain.doFilter(req, resp);
        } finally {
            ContextUtil.clearContext();
        }
    }

    private static void initGlobalContext(HttpServletRequest servletRequest) {
        GlobalContext context = new GlobalContext();
        String contextStr = servletRequest.getHeader(ContextConstant.REQUEST_CONTEXT);
        if (!StringUtils.isEmpty(contextStr)){
            context = JSON.parseObject(contextStr,GlobalContext.class);
        }else{
            context.setTraceId(UUID.randomUUID().toString());
            context.setClientIp(IpUtil.getClientAddress(servletRequest));
        }
        ContextUtil.setCurrentContext(context);
    }

    @Override
    public void init(FilterConfig config){
    }

    @Override
    public void destroy() {
    }
}

Создайте GlobalContext в бизнес-входе, и в последующей цепочке вызовов построенный GlobalContext будет получен из заголовка запроса.

public class ContextUtil {
    private static ThreadLocal<GlobalContext> currentThreadLocal = ThreadLocal.withInitial(GlobalContext::new);

    public static void setCurrentContext(GlobalContext context) {
        currentThreadLocal.set(context);
        String traceId = context.getTraceId();
        if (traceId != null && traceId.length() > 0 && MDC.get(ContextConstant.TRACK_ID) == null) {
            MDC.put(ContextConstant.TRACK_ID, traceId);
        }
    }

    public static GlobalContext getCurrentContext() {
       return currentThreadLocal.get();
    }

    public static void clearContext() {
        MDC.clear();
        currentThreadLocal.remove();
    }
}

Получите GlobalContext и передайте его через заголовок запроса

public class FeignConfig {
 @Bean
 public RequestInterceptor header(){
     return this::setRequestHeader;
 }
 private void setRequestHeader(RequestTemplate requestTemplate){
     GlobalContext context = ContextUtil.getCurrentContext();
     if (context!=null){
         requestTemplate.header(ContextConstant.REQUEST_CONTEXT, JSONParser.quote(JSON.toJSONString(context)));
     }
 }
}

Вот только пример звонка через Feign.Типов звонков между сервисами много.Главная идея в том,чтобы прозрачно передать GlobalContext на следующий сервис.

Переменные конфигурации контейнера MDC в обратном плане файла конфигурации%X{trackId}

 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %X{trackId} [%15.15t] %class{39}.%method[%L] : %m%n</pattern>
            <!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
            <charset>UTF-8</charset>f
        </encoder>
    </appender>l

ДЕМО адрес:GitHub.com/Chen Xuan код…