Внедрение полного журнала отслеживания ссылок Dubbo

Java

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

Теперь с микросервисной архитектурой dubbo в качестве фона возьмем каштан:

A -> B -> C

Нам нужно распечатать журналы между A/B/C/тремя микросервисами в цепочке.Все мы знаем, что RpcContext Dubbo может использовать один и тот же RpcContext только с потребителями и поставщиками, например, A->B, тогда A и Оба B могут получить RpcContext одного и того же содержимого, но когда B->C, A и C не могут совместно использовать RpcContext одного и того же содержимого, то есть журнал цепной печати не может быть выполнен.

Итак, как мы это делаем?

Мы можем решить эту проблему, поменяв левую руку на правую.Предположим, что левая рука — это ThreadLocal потока, а правая рука — это RpcContext, тогда перед обменом мы сначала сохраняем необходимую информацию журнала в ThreadLocal.

Микросервисы в нашем проекте грубо разделены на два типа микросервисов, один из которых — контейнер Dubbo, который характеризуется использованием только контейнера spring для запуска, затем использованием dubbo для предоставления сервиса, а затем регистрации сервиса в zookeeper, предоставляя сервисы. для потребителей; другой — контейнер SpringMVC, который является нашим общим WEB-контейнером, который является единственным контейнером, который может открывать интерфейсы для внешнего мира в нашем проекте, а также действует как функция шлюза проекта.

Узнав о контейнере микросервиса, мы теперь знаем, что первый слой цепочки вызовов должен быть в слое контейнера SpringMVC, тогда мы можем написать кастомный перехватчик прямо в этом слое.

Например, демо-код, код предварительного перехвата общедоступного перехватчика выглядит следующим образом:

public class CommonInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler)
        throws Exception {

        // ...

        // 初始化全局的Context容器
        Request request = initRequest(httpServletRequest);
        // 新建一个全局唯一的请求traceId,并set进request中
        request.setTraceId(JrnGenerator.genTraceId());
        // 将初始化的请求信息放进ThreadLocal中
        Context.initialLocal(request);

        // ...

        return true;
    }
    
    // ...
    
}

Объект внутреннего контекста системы:

public class Context {
    
    // ...
    
    private static final ThreadLocal<Request> REQUEST_LOCAL = new ThreadLocal<>();
    
    public final static void initialLocal(Request request) {
        if (null == request) {
            return;
        }
        REQUEST_LOCAL.set(request);
    }
    
    public static Request getCurrentRequest() {
        return REQUEST_LOCAL.get();
    }
    
    // ...
}

Перехватчик реализованorg.springframework.web.servlet.HandlerInterceptorИнтерфейс, его основная функция - перехват и обработка запросов, вы можете выполнять некоторые операции, такие как ведение журнала и проверка разрешений в слое MVC, что эквивалентно АОП слоя MVC, то есть все функции, отвечающие сквозным задачам можно поставить в перехватчик выполнить.

здесьinitRequest(httpServletRequest);Это объект запроса, который инкапсулирует информацию о запросе в системное содержимое.Request, и инициализируйте глобально уникальный traceId в запросе, а затем поместите его в поле ThreadLocal внутреннего контекста системы.

Далее я расскажу о том, как поместить содержимое ThreadLocal в RpcContext. Прежде чем говорить, позвольте мне рассказать о Dubbo, основанном на механизме расширения spi. Официальный документ объясняет расширение перехватчика следующим образом:

Перехват процесса вызова поставщика услуг и потребителя услуг. Большинство собственных функций Dubbo реализованы на основе этой точки расширения. Каждый раз, когда выполняется удаленный метод, будет выполняться перехват. Обратите внимание на влияние на производительность.

Другими словами, прежде чем мы выполним удаленный вызов службы, перехватчик перехватит вызов, поэтому с ним легко справиться. Прежде чем потребитель вызовет удаленную службу, мы можем тайно поместить содержимое ThreadLocal в контейнер RpcContext. Механизм расширяет два перехватчика, один действует на стороне потребителя, а другой действует на стороне поставщика:

Добавьте файл com.alibaba.dubbo.rpc.Filter в META-INF, содержимое будет следующим:

provider=com.objcoding.dubbo.filter.ProviderFilter
consumer=com.objcoding.dubbo.filter.ConsumerFilter

Обработка перехвата на стороне потребителя:


public class ConsumerFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) 
        throws RpcException {

        //1.从ThreadLocal获取请求信息
        Request request = Context.getCurrentRequest();
        //2.将Context参数放到RpcContext
        RpcContext rpcCTX = RpcContext.getContext();
        // 将初始化的请求信息放进ThreadLocal中
        Context.initialLocal(request);

        // ...

    }   
}

Context.getCurrentRequest();Это получение содержимого запроса запроса от ThreadLocal,contextToDubboContext(request);Поместите содержимое запроса в контейнер RpcContext текущего потока.

Легко представить провайдера, который извлекает содержимое из RpcContext и помещает его в ThreadLocal:

public class ProviderFilter extends AbstractDubboFilter implements Filter{
     @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) 
        throws RpcException {
        // 1.获取RPC远程调用上下文
        RpcContext rpcCTX = RpcContext.getContext();
        // 2.初始化请求信息
        Request request = dubboContextToContext(rpcCTX);
        // 3.将初始化的请求信息放进ThreadLocal中
        Context.initialLocal(request);

        // ...

    }   
}

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

Мы находимся в объекте контекста ContextinitialLocal(Request request)Добавьте информацию о traceId в контексте log4j2 в методе:

public class Context {
    
    // ...

    final public static String TRACEID = "_traceid";

    public final static void initialLocal(Request request) {
        if (null == request) {
            return;
        }
        // 在log4j2的上下文中添加traceId
        ThreadContext.put(TRACEID, request.getTraceId());
        REQUEST_LOCAL.set(request);
    }
    
    // ...
}

Реализовать следующийorg.apache.logging.log4j.core.appender.rewrite.RewritePolicy:

@Plugin(name = "Rewrite", category = "Core", elementType = "rewritePolicy", printObject = true)
public final class MyRewritePolicy implements RewritePolicy {

    // ...
    
    @Override
    public LogEvent rewrite(final LogEvent source) {
        HashMap<String, String> contextMap = Maps.newHashMap(source.getContextMap());
        contextMap.put(Context.TRACEID, contextMap.containsKey(Context.TRACEID) ? contextMap.get(Context.TRACEID) : NULL);
        return new Log4jLogEvent.Builder(source).setContextMap(contextMap).build();
    }
    
    // ...
}

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

Настройте log4j2.xml:

<Configuration status="warn">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout
                pattern="[%d{yyyy/MM/dd HH:mm:ss,SSS}][${ctx:_traceid}]%m%n" />
        </Console>
        
        <!--定义一个Rewrite-->
        <Rewrite name="Rewrite">
            <MyRewritePolicy/>
            <!--引用输出模板-->
            <AppenderRef ref="Console"/>
        </Rewrite>
    </Appenders>
    <Loggers>
       
        <!--使用日志模板-->
        <Logger name="com.objcoding.MyLogger" level="debug" additivity="false">
            <!--引用Rewrite-->
            <AppenderRef ref="Rewrite"/>
        </Logger>
    </Loggers>
</Configuration>

Пользовательский класс журнала:

public class MyLogger {
    private static final Logger logger = LoggerFactory.getLogger(MyLogger.class);
    
     public static void info(String msg, Object... args) {
        if (canLog() == 1 && logger.isInfoEnabled()) {
            logger.info(msg, args);
        }
    }
    
    public static void debug(String message, Object... args) {
        if (canLog() == 1 && logger.isDebugEnabled()) {
            logger.debug(message, args);
        }
    }
    
    // ..
}

Для получения более интересных статей, пожалуйста, обратите внимание на официальный аккаунт «Back-end Advanced», поддерживаемый автором, который является официальным аккаунтом, посвященным технологиям, связанным с сервером.

Подпишитесь на официальную учетную запись и ответьте на «Backend», чтобы бесплатно получать электронные книги, связанные с бэкендом.

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

公众号「后端进阶」,专注后端技术分享!