背景
今天收到業務團隊反饋線上有個應用往 Pulsar 中傳送訊息失敗了,經過日誌檢視得知是傳送訊息時候丟擲了 java.lang.InterruptedException
異常。
和業務溝通後得知是在一個 gRPC
介面中觸發的訊息傳送,大約持續了半個小時的異常後便恢復正常了,這是整個問題的背景。
前置排查
拿到該問題後首先排查下是否是共性問題,檢視了其他的應用沒有發現類似的異常;同時也檢視了 Pulsar broker 的監控大盤,在這個時間段依然沒有波動和異常;
這樣可以初步排除是 Pulsar 服務端的問題。
接著便是檢視應用那段時間的負載情況,從應用 QPS 到 JVM 的各個記憶體情況依然沒發現有什麼明顯的變化。
Pulsar 原始碼排查
既然看起來應用本身和 Pulsar broker 都沒有問題的話那就只能從異常本身來排查了。
首先第一步要得知具體使用的是 Pulsar-client
是版本是多少,因為業務使用的是內部基於官方 SDK 封裝 springboot starter
所以第一步還得排查這個 starter
是否有影響。
透過檢視原始碼基本排除了 starter
的嫌疑,裡面只是簡單的封裝了 SDK
的功能而已。
org.apache.pulsar.client.api.PulsarClientException: java.util.concurrent.ExecutionException: org.apache.pulsar.client.api.PulsarClientException: java.lang.InterruptedException at org.apache.pulsar.client.api.PulsarClientException.unwrap(PulsarClientException.java:1027) at org.apache.pulsar.client.impl.TypedMessageBuilderImpl.send(TypedMessageBuilderImpl.java:91) at
java.base/java.lang.Thread.run(Thread.java:834) Caused by: java.util.concurrent.ExecutionException: org.apache.pulsar.client.api.PulsarClientException: java.lang.InterruptedException at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:395)
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1999)
at org.apache.pulsar.client.impl.TypedMessageBuilderImpl.send(TypedMessageBuilderImpl.java:89) ... 49 common frames omitted Caused by: org.apache.pulsar.client.api.PulsarClientException: java.lang.InterruptedException
at org.apache.pulsar.client.impl.ProducerImpl.canEnqueueRequest(ProducerImpl.java:775)
at org.apache.pulsar.client.impl.ProducerImpl.sendAsync$original$BWm7PPlZ(ProducerImpl.java:393)
at org.apache.pulsar.client.impl.ProducerImpl.sendAsync$original$BWm7PPlZ$accessor$i7NYMN6i(ProducerImpl.java)
at org.apache.pulsar.client.impl.ProducerImpl$auxiliary$EfuVvJLT.call(Unknown Source)
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
at org.apache.pulsar.client.impl.ProducerImpl.sendAsync(ProducerImpl.java)
at org.apache.pulsar.client.impl.ProducerImpl.internalSendAsync(ProducerImpl.java:292)
at org.apache.pulsar.client.impl.ProducerImpl.internalSendWithTxnAsync(ProducerImpl.java:363)
at org.apache.pulsar.client.impl.PartitionedProducerImpl.internalSendWithTxnAsync(PartitionedProducerImpl.java:191)
at org.apache.pulsar.client.impl.PartitionedProducerImpl.internalSendAsync(PartitionedProducerImpl.java:167)
at org.apache.pulsar.client.impl.TypedMessageBuilderImpl.sendAsync(TypedMessageBuilderImpl.java:103)
at org.apache.pulsar.client.impl.TypedMessageBuilderImpl.send(TypedMessageBuilderImpl.java:82) ... 49 common frames omitted Caused by: java.lang.InterruptedException: null
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1343)
at java.base/java.util.concurrent.Semaphore.acquire(Semaphore.java:318)
at org.apache.pulsar.client.impl.ProducerImpl.canEnqueueRequest(ProducerImpl.java:758)
接下來便只能是分析堆疊了,因為 Pulsar-client 的部分實現原始碼是沒有直接打包到依賴中的,反編譯的話許多程式碼行數對不上,所以需要將官方的原始碼拉到本地,切換到對於的分支進行檢視。
這一步稍微有點麻煩,首先是程式碼庫還挺大的,加上之前如果沒有準備好 Pulsar 的開發環境的話估計會勸退一部分人;但其實大部分問題都是網路造成的,只要配置一些 Maven 映象多試幾次總會編譯成功。
我這裡直接將分支切換到 branch-2.8
。
從堆疊的頂部開始排查 TypedMessageBuilderImpl.java:91
:
看起來是內部非同步傳送訊息的時候拋了異常。
接著往下看到這裡:
java.lang.InterruptedException
at org.apache.pulsar.client.impl.ProducerImpl.canEnqueueRequest(ProducerImpl.java:775) at
看起來是這裡沒錯,但是程式碼行數明顯不對;因為 2.8 這個分支也是修復過幾個版本,所以中間有修改導致程式碼行數與最新程式碼對不上也正常。
semaphore.get().acquire();
不過初步來看應該是這行程式碼丟擲的執行緒終端異常,這裡看起來只有他最有可能了。
為了確認是否是真的是這行程式碼,這個檔案再往前翻了幾個版本最終確認了就是這行程式碼沒錯了。
我們點開java.util.concurrent.Semaphore#acquire()
的原始碼,
/**
* <li>has its interrupted status set on entry to this method; or
* <li>is {@linkplain Thread#interrupt interrupted} while waiting
* for a permit,
* </ul>
* then {@link InterruptedException} is thrown and the current thread's
* interrupted status is cleared.
*
* @throws InterruptedException if the current thread is interrupted
*/
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted() ||
(tryAcquireShared(arg) < 0 &&
acquire(null, arg, true, true, false, 0L) < 0))
throw new InterruptedException();
}
透過原始碼會發現 acquire()
函式確實會響應中斷,一旦檢測到當前執行緒被中斷後便會丟擲 InterruptedException
異常。
定位問題
所以問題的原因基本確定了,就是在 Pulsar 的傳送訊息執行緒被中斷了導致的,但為啥會被中斷還需要繼續排查。
我們知道執行緒中斷是需要呼叫 Thread.currentThread().interrupt();
API的,首先猜測是否 Pulsar 客戶端內部有個執行緒中斷了這個傳送執行緒。
於是我在 pulsar-client
這個模組中搜尋了相關程式碼:
排除掉和 producer 不相關的地方,其餘所有中斷執行緒的程式碼都是在有了該異常之後繼續傳遞而已;所以初步來看 pulsar-client 內部沒有主動中斷的操作。
既然 Pulsar 自己沒有做,那就只可能是業務做的了?
於是我在業務程式碼中搜尋了一下:
果然在業務程式碼中搜到了唯一一處中斷的地方,而且透過呼叫關係得知這段程式碼是在訊息傳送前執行的,並且和 Pulsar 傳送函式處於同一執行緒。
大概的虛擬碼如下:
List.of(1, 2, 3).stream().map(e -> {
return CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
return e;
});
}
).collect(Collectors.toList()).forEach(f -> {
try {
Integer integer = f.get();
log.info("====" + integer);
if (integer==3){
TimeUnit.SECONDS.sleep(10);
Thread.currentThread().interrupt();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
});
MessageId send = producer.newMessage().value(msg.getBytes()).send();
執行這段程式碼可以完全復現同樣的堆疊。
幸好中斷這裡還打得有日誌:
透過日誌搜尋發現異常的時間和這個中斷的日誌時間點完全重合,這樣也就知道根本原因了。
因為業務執行緒和訊息傳送執行緒是同一個,在某些情況下會執行 Thread.currentThread().interrupt();
,其實單純執行這行函式並不會發生什麼,只要沒有去響應這個中斷,也就是 Semaphore
原始碼中的判斷了執行緒中斷的標記:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted() ||
(tryAcquireShared(arg) < 0 &&
acquire(null, arg, true, true, false, 0L) < 0))
throw new InterruptedException();
}
但恰好這裡業務中斷後自己並沒有去判斷這個標記,導致 Pulsar 內部去判斷了,最終丟擲了這個異常。
總結
所以歸根結底還是這裡的程式碼不合理導致的,首先是自己中斷了執行緒但也沒使用,從而導致有被其他基礎庫使用的可能,所以會造成了一些不可預知的後果。
再一個是不建議在業務程式碼中使用 Thread.currentThread().interrupt();
這類程式碼,第一眼根本不知道是要幹啥,也不易維護。
其實本質上執行緒中斷也是執行緒間通訊的一種手段,有這類需求完全可以換為內建的 BlockQueue
這類函式來實現。