歡迎訪問陳同學博客原文
本文以一個技術場景來學習 Hystrix 跨線程傳遞數據的知識。將先簡述ThreadLocal、InheritableThreadLocal跨父子線程傳遞數據,再進入主題。
基於Spring Boot 2.0.6.RELEASE, Spring Cloud Finchley.SR1。
技術場景
服務A 通過 Feign + Hystrix 調用服務B,服務間調用時需傳遞 JWT Token,希望在Feign發起的所有請求中都自動加上token以在各服務中傳遞環境信息。JWT Token 一是用於確保訪問的安全,二是存儲用戶context信息。
請求進入服務A時,首先通過 Filter 驗證 JWT token,接着把 token 存到當前線程(tomcat工作線程)。調用 Service B時,tomcat工作線程將把任務交給 Hystrix 線程池處理,這是本文主題:如何將token從tomcat工作線程傳遞到hystrix線程池線程?
Hystrix 線程池
先提一下Hystrix線程池。當 Hystrix 隔離策略爲線程池時,在上述場景中:
假設服務B的應用名爲 Service-B,在服務A中,Hystrix 會專門創建線程池用於執行對 Service-B的調用,上圖中的 hystrix-Service-B-n 就是其線程池中的線程。
通過這種方式,應用A對依賴進行了隔離。默認對每個依賴的服務分配10個線程。隔離的好處例舉幾個:
- 請求太多,10個線程處理不過來?=> 那就拒絕請求,保護應用
- 依賴服務調用出錯?失敗?=> 那就降級 或 快速失敗
- 依賴服務調用延時?=> 想耍流盲,佔用資源不放,那就利用超時監控機制,幹掉流氓任務
- …
父子線程如何傳遞數據
跨線程傳遞數據的場景有:父子線程和其他任意線程之間。
先看下ThreadLocal,定義兩個ThreadLocal類型的對象:TOKEN 和 USER,假定用1024、1025分別代表這兩個對象。
static final ThreadLocal<String> TOKEN = new ThreadLocal<>(); // 1024
static final ThreadLocal<String> USER = new ThreadLocal<>(); // 1025
在Thread1中執行: TOKEN.set(“1”); USER.set(“1”) ;
在Thread2中執行: TOKEN.set(“2”); USER.set(“2”) ;
數據存儲情況如下:
每個線程都有個ThreadLocalMap類型的屬性 threadLocals,它用於存儲線程私有數據,ThreadLocal對象只是充當檢索數據的Key,本身不存儲任何信息。
再看下 InheritableThreadLocal 怎麼在父子線程之間傳遞數據,如下例子:
static final ThreadLocal<String> TOKEN = new InheritableThreadLocal<>();
Thread有兩個屬性,threadLocals 和 inheritableThreadLocals,結構完全一樣。當使用 InheritableThreadLocal時,數據存儲在inheritableThreadLocals中,否則存儲在threadLocals。
ThreadLocalMap threadLocals = null;
ThreadLocalMap inheritableThreadLocals = null;
在創建子線程時,會將父線程的inheritableThreadLocals拷貝到子線程,從而達到跨線程傳遞數據的目的。
下面是Thread構造器上初始化的一段代碼:
private void init(ThreadGroup g, Runnable target, String name ...) {
...
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
Hystrix如何跨線程傳遞數據
上面的父子線程傳遞數據,子線程可以訪問父線程數據。但從tomcat線程向hystrix線程池線程傳遞數據情況有所不同,並不清楚任務最終由hystrix線程池中的哪個線程執行,而且兩種線程的關係八杆子打不着。
Hystrix提供瞭如下方案來解決上述問題(我將代碼全寫在了一起,看註釋結合下面的圖,否則容易暈):
// 用HystrixRequestVariableDefault做Key檢索數據, 類比ThreadLocal
static final HystrixRequestVariableDefault<String> TOKEN = new HystrixRequestVariableDefault<>();
在某個 ServletFilter 進行處理:
// 在當前線程初始化HystrixRequestContext, 並設置token
HystrixRequestContext context = null;
if (!HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.initializeContext();
}
try {
TOKEN.set("I am a token");
chain.doFilter(request, response);
} finally {
// 銷燬當前線程HystrixRequestContext
if (HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.getContextForCurrentThread().shutdown();
}
}
HystrixRequestContext 表示Request級別的context信息,在線程池環境下,用法和ThreadLocal差不多。
HystrixRequestContext中有兩個非常重要的屬性:
// requestVariables, 每個線程存儲自己的 HystrixRequestContext
private static ThreadLocal<HystrixRequestContext> requestVariables = new ThreadLocal<HystrixRequestContext>();
// state屬性,V是我自己簡化的, 表示存儲的值
ConcurrentHashMap<HystrixRequestVariableDefault<?>, V> state = new ...
畫個圖來翻譯下上面的代碼:
- 先創建靜態變量 HystrixRequestVariableDefault#1024 和 ThreadLocal#1025
static final HystrixRequestVariableDefault<String> TOKEN = new HystrixRequestVariableDefault<>();
private static ThreadLocal<HystrixRequestContext> requestVariables = new ThreadLocal<HystrixRequestContext>();
- 在當前線程(tomcat的線程)通過1025這個Key找到對應的HystrixRequestContext,然後往HystrixRequestContext的state屬性中put<1024, “I am a token”>
TOKEN.set("I am a token")
- 在執行 TOKEN.get() 時,先通過ThreadLocal對象作爲Key檢索到線程中存儲的HystrixRequestContext,然後通過HystrixRequestVariableDefault對象作爲Key從HystrixRequestContext.state中獲取對應的值。
上面其實沒解決跨線程傳遞數據問題,繞了一圈,用的還是ThreadLocal,並沒有線程間數據傳遞的過程。
調試後找到跨線程傳遞數據的地方,如下截圖,展示了從Feign發起調用,到任務被Hystrix線程執行的過程:
再看下圖,HystrixContexSchedulerAction。
它是在tomcat工作線程中創建的,因此可以拿到token,並將token存在了HystrixContexSchedulerAction對象中。
this.parentThreadState = HystrixRequestContext.getContextForCurrentThread();
當Hystrix線程執行這個任務時,任務本身就存儲了token,在執行任務前,利用下面的代碼把token存儲到hystrix工作線程。需要注意的是:parentThreadState是一個HystrixRequestContext類型的引用,也就是說tomcat工作線程在銷燬HystrixRequestContext時,Hystrix線程中存儲的數據同樣也就銷燬了。
HystrixRequestContext.setContextOnCurrentThread(parentThreadState);
上面整個過程比較麻煩,我在調試時也找了很久才找到跨線程傳遞數據的地方。
跨數據傳遞的簡單例子
通過Task對象本身來跨線程傳遞數據,Hystrix簡化後其實就是下面的樣子。
public class Demo {
private static ThreadLocal<String> TOKEN = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
TOKEN.set("1024");
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 在當前線程創建任務, 通過任務把token傳遞到其他線程
executorService.submit(new Task("1024"));
Thread.sleep(1000);
}
// 通過任務傳遞token
static class Task implements Runnable {
private String token;
public Task(String token) {
this.token = token;
}
@Override
public void run() {
// 從Task中獲取token並設置到當前線程
TOKEN.set(this.token);
}
}
}
小結
Runnable 和 Callable 都是可以被線程執行的Task,無論最終由哪個線程來執行,在各個線程間傳遞數據比較好的方式依然是通過任務本身。跨線程傳遞數據只是Hystrix中的一個小細節,實現過程也夾雜在複雜的Hystrix實現中,只是看上去比較複雜。
參考資料
- Hystrix系列之ThreadLocal跨線程傳遞問題 from 佔小狼
- Hystrix Isolation from Hystrix Github Wiki
歡迎關注陳同學的公衆號,一起學習,一起成長