處理非阻塞上下文警告中的阻塞方法
1. 概述
在本文中,我們將探討警告: “Possibly blocking call in non-blocking context could lead to thread starvation”
。首先,我們將透過一個簡單的範例重新建立警告,並探討如何在它與我們的案例無關時抑制它。
然後,我們將討論忽視它的風險,並探索兩種有效解決問題的方法。
2. 非阻塞情境中的阻塞方法
如果我們嘗試在反應式上下文中使用阻塞操作,IntelliJ IDEA 將提示「 Possibly blocking call in non-blocking context could lead to thread starvation
」警告。
假設我們正在使用 Spring WebFlux 和 Netty 伺服器開發反應式 Web 應用程式。如果我們在處理應保持非阻塞的 HTTP 請求時引入阻塞操作,我們將遇到此警告:
此警告源自於IntelliJ IDEA的靜態分析。如果我們確信它不會影響我們的應用程序,我們可以使用“ BlockingMethodInNonBlockingContext
”檢查名稱輕鬆抑制警告:
@SuppressWarnings("BlockingMethodInNonBlockingContext")
@GetMapping("/warning")
Mono<String> warning() {
// ...
}
然而,了解根本問題並評估其影響至關重要。在某些情況下,這可能會導致阻塞負責處理 HTTP 請求的線程,從而造成嚴重影響。
3. 理解警告
讓我們展示一個場景,忽略此警告可能會導致執行緒匱乏並阻止傳入的 HTTP 流量。對於此範例,我們將新增另一個端點,並使用Thread.sleep()
故意阻塞執行緒兩秒鐘,儘管處於反應式上下文中:
@GetMapping("/blocking")
Mono<String> getBlocking() {
return Mono.fromCallable(() -> {
try {
Thread.sleep(2_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "foo";
});
}
在這種情況下,處理傳入 HTTP 請求的 Netty 事件循環執行緒可能會很快被阻塞,導致無回應。例如,如果我們發送 200 個並發請求,即使不涉及任何計算,應用程式也將花費 32 秒來回應所有請求。此外,這也會影響其他端點—即使它們不需要阻塞操作。
出現這種延遲是因為 Netty HTTP 執行緒池的大小為 12,因此它一次只能處理 12 個請求。如果我們檢查 IntelliJ 分析器,我們可以預期看到執行緒大部分時間都被阻塞,並且在整個測試過程中 CPU 使用率非常低:
4. 解決問題
理想情況下,我們應該切換到響應式 API 來解決這個問題。但是,當這不可行時,我們應該使用單獨的執行緒池來執行此類操作,以避免阻塞 HTTP 執行緒。
4.1.使用響應式替代方案
首先,我們應該盡可能採取被動的方法。這意味著尋找阻塞操作的反應性替代方案。
例如,我們可以嘗試將響應式資料庫驅動程式與 Spring Data Reactive Repositories 或響應式 HTTP 用戶端(如 WebClient)結合使用。在我們的簡單範例中,我們可以使用Mono 的 API將回應延遲兩秒,而不是依賴阻塞的Thread.sleep()
:
@GetMapping("/non-blocking")
Mono<String> getNonBlocking() {
return Mono.just("bar")
.delayElement(Duration.ofSeconds(2));
}
透過這種方法,應用程式可以處理數百個並發請求,並在我們引入的兩秒延遲後發送所有回應。
4.2.使用專用調度程序進行阻塞操作
另一方面,在某些情況下我們無法選擇使用響應式 API。一個常見的場景是使用非響應式驅動程式查詢資料庫時,這將導致阻塞操作:
@GetMapping("/blocking-with-scheduler")
Mono<String> getBlockingWithDedicatedScheduler() {
String data = fetchDataBlocking();
return Mono.just("retrieved data: " + data);
}
在這些情況下,我們可以將阻塞操作包裝在Mono
中,並使用subscribeOn()
指定其執行的調度程序。這為我們提供了一個Mono<String>
,稍後可以將其映射到我們所需的回應格式:
@GetMapping("/blocking-with-scheduler")
Mono<String> getBlockingWithDedicatedScheduler() {
return Mono.fromCallable(this::fetchDataBlocking)
.subscribeOn(Schedulers.boundedElastic())
.map(data -> "retrieved data: " + data);
}
5. 結論
在本教程中,我們介紹了由 IntelliJ 的靜態分析器產生的“Possibly blocking call in non-blocking context could lead to thread starvation”
警告。透過程式碼範例,我們示範了忽略此警告如何阻止 Netty 處理傳入 HTTP 請求的線程池,從而導致應用程式無回應。
之後,我們看到了盡可能支援響應式 API 如何幫助我們解決這個問題。此外,我們了解到,只要沒有反應性替代方案,就應該使用單獨的線程池來執行阻塞操作。
與往常一樣,本教學的源代碼可在 GitHub 上取得。