# :-: 分布式日志鏈路跟蹤
## 一、背景
開發排查系統問題用得最多的手段就是查看系統日志,在分布式環境中一般使用`ELK`來統一收集日志,但是在并發大時使用日志定位問題還是比較麻煩,由于大量的其他用戶/其他線程的日志也一起輸出穿行其中導致很難篩選出指定請求的全部相關日志,以及下游線程/服務對應的日志。
## 二、解決思路
* 每個請求都使用一個`唯一標識`來追蹤全部的鏈路顯示在日志中,并且不修改原有的打印方式(代碼無入侵)
* 使用Logback的`MDC`機制日志模板中加入`traceId`標識,取值方式為`%X{traceId}`
>[danger] MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日志的功能。MDC 可以看成是一個與當前線程綁定的Map,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當需要記錄日志時,只需要從 MDC 中獲取所需的信息即可。MDC 的內容則由程序在適當的時候保存進去。對于一個 Web 應用來說,通常是在請求被處理的最開始保存這些數據。
## 三、方案實現
由于`MDC`內部使用的是`ThreadLocal`所以只有本線程才有效,子線程和下游的服務`MDC`里的值會丟失;所以方案主要的難點是解決**值的傳遞問題**,主要包括以幾下部分:
* API網關中的`MDC`數據如何傳遞給下游服務
* 服務如何接收數據,并且調用其他遠程服務時如何繼續傳遞
* 異步的情況下(線程池)如何傳給子線程
### 3.1. 修改日志模板
logback配置文件日志格式添加該標識

~~~
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 設置字符集 -->
</encoder>
~~~
### 3.2. 網關添加過濾器
生成`traceId`并通過`header`傳遞給下游服務

上面代碼有個MDC是屬于org.slf4j.MDC中的,下面就是常量的值:
~~~text
/**
* 日志鏈路追蹤id信息頭
*/
String TRACE_ID_HEADER = "x-traceId-header";
/**
* 日志鏈路追蹤id日志標志
*/
String LOG_TRACE_ID = "traceId";
~~~
### 3.3. 下游服務增加spring攔截器
接收并保存`traceId`的值**攔截器**
**注冊攔截器**

### 3.4. 下游服務增加feign攔截器
繼續把當前服務的`traceId`值傳遞給下游服務

### 3.5. 解決父子線程傳遞問題
主要針對業務會使用線程池(異步、并行處理),并且`spring`自己也有`@Async`注解來使用線程池,要解決這個問題需要以下兩個步驟
#### 3.5.1. 重寫logback的`LogbackMDCAdapter`
由于logback的`MDC`實現內部使用的是`ThreadLocal`不能傳遞子線程,所以需要重寫替換為阿里的`TransmittableThreadLocal`
>[info] **TransmittableThreadLocal** 是Alibaba開源的、用于解決 **“在使用線程池等會緩存線程的組件情況下傳遞ThreadLocal”** 問題的 InheritableThreadLocal 擴展。若希望 TransmittableThreadLocal 在線程池與主線程間傳遞,需配合 **TtlRunnable** 和 **TtlCallable** 使用。
**TtlMDCAdapter類**
~~~
package org.slf4j;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.spi.MDCAdapter;
public class TtlMDCAdapter implements MDCAdapter {
/**
* 此處是關鍵
*/
private final ThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new TransmittableThreadLocal<>();
private static TtlMDCAdapter mtcMDCAdapter;
static {
mtcMDCAdapter = new TtlMDCAdapter();
MDC.mdcAdapter = mtcMDCAdapter;
}
public static MDCAdapter getInstance() {
return mtcMDCAdapter;
}
~~~
>[success] 其他代碼與**ch.qos.logback.classic.util.LogbackMDCAdapter**一樣,只需改為調用`copyOnInheritThreadLocal`變量
**TtlMDCAdapterInitializer類**用于程序啟動時加載自己的mdcAdapter實現
~~~
public class TtlMDCAdapterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
//加載TtlMDCAdapter實例
TtlMDCAdapter.getInstance();
}
}
~~~
#### 3.5.2. 擴展線程池實現
增加`TtlRunnable`和`TtlCallable`擴展
~~~
public class CustomThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public void execute(Runnable runnable) {
Runnable ttlRunnable = TtlRunnable.get(runnable);
super.execute(ttlRunnable);
}
@Override
public <T> Future<T> submit(Callable<T> task) {
Callable ttlCallable = TtlCallable.get(task);
return super.submit(ttlCallable);
}
@Override
public Future<?> submit(Runnable task) {
Runnable ttlRunnable = TtlRunnable.get(task);
return super.submit(ttlRunnable);
}
@Override
public ListenableFuture<?> submitListenable(Runnable task) {
Runnable ttlRunnable = TtlRunnable.get(task);
return super.submitListenable(ttlRunnable);
}
@Override
public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
Callable ttlCallable = TtlCallable.get(task);
return super.submitListenable(ttlCallable);
}
}
~~~
## 四、場景測試
### 4.1. 測試代碼如下

### 4.2. api網關打印的日志
網關生成`traceId`值為`13d9800c8c7944c78a06ce28c36de670`

### 4.4. ELK聚合日志通過`traceId`查詢整條鏈路日志
當系統出現異常時,可直接通過該異常日志的`traceId`?的值,在日志中心中詢該請求的所有日志信息

- 項目介紹
- 項目聲明
- 項目簡介
- 架構設計
- 項目亮點功能介紹
- 技術棧介紹
- 核心功能
- 運行環境
- 項目更新日志
- 文檔更新日志
- F&Q
- 部署教程
- 環境準備
- JDK安裝
- JDK1.8,17共存
- maven
- 分布式緩存Redis
- 單機版
- 集群
- 注冊&配置中心alibaba/nacos
- 介紹
- Nacos安裝
- Nacos配置中心
- Nacos注冊發現
- Nacos生產部署方案
- 服務監控-BootAdmin
- 基本介紹
- 如何使用
- 整合Admin-Ui
- 客戶端配置
- 鏈路追蹤
- 基本介紹
- SkyWalking-1
- Skywalking-1
- 消息隊列
- Kafka
- docker安裝kafka
- Linux集群
- Maven私服
- nexus安裝部署
- nexus使用介紹
- 全文搜索elasticsearch
- windows集群搭建
- docker安裝es
- ElasticHD
- linux集群部署
- 統一日志解決方案
- 日志解決方案設計
- 介紹與相關資料
- ELK安裝部署
- elasticsearch 7.5
- logstash-7.5
- kibana-7.5
- filebeat
- 服務監控-Prometheus
- Prometheus安裝配置
- Prometheus介紹
- grafana
- 持續集成部署CICD
- 自動化部署Jenkins
- 安裝部署win
- 打包發布遠程執行
- 安裝部署linux
- jenkins+gitlab+docker容器化工程自動化部署
- Git
- CICD說明
- 阿里云效
- CentOS_MYSQL安裝
- docker
- 安裝
- Docker安裝Nginx
- Docker部署啟動springboot
- dockerCompose
- harbor
- Docker私有鏡像倉庫
- Portainer
- Docker遠程連接設置
- 打包工程
- 必要啟動模塊
- 核心模塊
- 登錄認證
- 緩存功能
- 日志模塊
- 分布式鎖
- 消息隊列
- 異常處理
- 系統接口
- 參數驗證
- es檢索
- 數據導出
- 系統設計
- 系統總體架構
- 擴展模塊(可選)
- 限流熔斷alibaba/sentinel
- 使用Sentinel實現gateway網關及服務接口限流
- Sentinel使用Nacos存儲規則及同步
- 服務調用Feign
- Feign基本介紹
- 如何使用
- 負載均衡
- 請求超時
- 請求攔截器
- 分布式任務調度
- XXL-JOB
- 分布式事務
- TX-LCN
- Seata
- Seata原理解析
- 數據庫分庫分表
- swagger文檔
- 分布式ID生成器解決方案
- 服務網關CloudGateway
- 基本介紹
- 使用網關
- 路由配置
- 全局過濾器
- 服務認證授權架構設計
- 認證服務流程
- 授權服務流程
- 系統冪等性設計與實踐
- 分布式日志鏈路跟蹤
- 實時搜索系統設計
- 應用性能
- 壓力測試工具
- Apache JMeter介紹和安裝
- ApacheJMeter使用
- JVM
- JVM性能調優
- 常見JVM內存錯誤及解決方案
- JVM 分析工具詳解
- Spring Cloud性能調優
- Linux運維
- Linux 常用命令
- Linux開啟端口