一、問題概述
客戶的生產環境突然在近期間歇式的收到了Kafka CRC的相關異常,異常內容如下
Record batch for partition skywalking-traces-0 at offset 292107075 is invalid, cause: Record is corrupt (stored crc = 1016021496, compute crc = 1981017560)
報錯沒有規律性,有可能半天都不出現一次,也有可能一小時出現2、3次
而這個報錯會導致Kafka的Consumer hang死,即無法繼續消費後續的訊息,只能手動重啟Consumer才能繼續,是非常嚴厲的報錯,導致生產不可用
簡單解釋一下這個異常的原因,Kafka會在每個Batch的header中儲存持久化下來的訊息體的CRC,所謂CRC可以簡單理解為TCP的checkSum,而後Consumer在收到這條訊息後,將Batch header中儲存的CRC取出來,然後再根據統一的CRC演算法計算收到的訊息體的CRC值,而後拿上這兩個值做一下比對,如果一樣,說明訊息沒有被篡改,如果不一樣,就會扔出CRC異常
由於是公司的重保客戶,又出現如此嚴重的驚天bug,一時間公司的高層領導均高度重視此事,每天晚上的夕會(均是領導)也都要單獨詢問解決進度,一時間,壓力山大
二、分析定位
2.1、問題分析
在公司另外一個專案中(簡稱A客戶,我們當前要處理的簡稱B客戶),這個報錯並不陌生,且報錯的內容一模一樣,那這兩者會不會是同一個bug呢?答案是否定的
- A客戶:確實會報CRC異常,但穩定復現,只要出現了,不論Consumer重啟多少次,也不論多少個Consumer來消費,均是會報CRC異常(最終定位是A客戶的磁碟出現的壞點,導致資料異常,更換磁碟後,問題消失)
- B客戶:問題只會出現一次,如果手動重啟Consumer,該問題便會消失,如果換個Consumer來消費,問題也會消失,好像它只是曇花一現,且再也無法復現,如同鬼魅
那這個問題不是常規問題,就比較棘手了
2.2、Kafka自身bug ?
筆者長期活躍在Kafka社群,是Kafka社群的Contributer,且在阿里的公有云工作多年,但這個報錯在之前卻是一次沒有遇到過
在我印象中,CRC錯誤已經好久沒見到了,而後我排查了社群所有CRC有關的issue list,發現只有在0.10之前的低版本中出現過類似異常,而我們的B客戶生產環境使用的是2.8.2,這是整個2版本中的最後一個版本,而Kafka 2.0以後的版本卻很少出現這個異常
而Kafka自身計算CRC的邏輯卻非常簡潔,只有幾十行java程式碼,綜上,筆者認為Kafka自身出bug的機率相對較小,不能把重要精力放在這塊
2.3、報錯傾向分析
接下來我們便對這個異常的報錯傾向開始分析,經過各種排查,它具備以下特質:
- 間歇性、機率性、不確定性;有時候可能半天都不出現一次,也有可能一小時出現2、3次
- 無法復現;出現問題的訊息,重新消費,CRC異常會消失
- B客戶現場有8套環境,現只有一套環境存在此問題,對比其他7套環境,粗略看在磁碟型號、CPU型號等硬體裝置是保持一致的
- 檢視出問題的Broker,其不具備統一性,也沒有落在同一臺宿主機上
- 懷疑是記憶體條錯誤,排查對應的日誌沒有發現異常資訊
- 與作業系統的同學確認,咱們現在雖然使用的是我們自研的OS,但TCP協議這塊卻沒有改動過,使用的是Linux原生的
- 在家裡的環境模擬網路丟包、錯包的場景,沒有復現CRC異常
至此,好像均一切正常,粗略排查後,沒有發現有價值的線索。因此我們便將重心轉移至“網路”+“磁碟”這兩塊看似不太可能出現問題的基礎能力上
三、網路排查
3.1、埋點
進行網路排查的話,我們就要執行TCP抓包,即執行tcpdump命令,但抓包也不是簡單在機器上執行一個命令那麼簡單,當前的部署環境是3臺Broker、3臺Consumer,異常會暴露在Consumer中,但單臺Consumer可能會與多臺Broker建連
因此抓包的思路是在3臺Broker上均建立監聽,且在某一臺consumer也建立監聽,而後觀察TCP傳送出去的資料與接收到的資料是否一致
首先在3臺Broker上執行dump命令,由於出現問題的機率不高,可能導致網路dump包非常大,因此在命令中將TCP包拆成500M大小的檔案,且只保留最近的5個檔案,3臺Broker的IP如下:
- 10.0.0.70
- 10.0.0.71
- 10.0.0.72
而3臺Consumer的IP為:
- 10.0.0.18
- 10.0.0.19
- 10.0.0.20
首先在1臺consumer上執行如下命令:
sudo nuhup tcpdump -i vethfe2axxxx -nve host 10.0.0.70 or host 10.0.0.71 or host 10.0.0.72 -w broker_18.pcap -C 500 -W 5 -Z ccadmin &
sudo nuhup tcpdump -i vethfe2axxxx -nve host 10.0.0.70 or host 10.0.0.71 or host 10.0.0.72 -w broker_19.pcap -C 500 -W 5 -Z ccadmin &
sudo nuhup tcpdump -i vethfe2axxxx -nve host 10.0.0.70 or host 10.0.0.71 or host 10.0.0.72 -w broker_20.pcap -C 500 -W 5 -Z ccadmin &
其次在3臺Broker上分別執行如下:
sudo nuhup tcpdump -i xxxxxxxxx1 -nve host 10.0.0.18 -w broker_18.pcap -C 500 -W 5 -Z ccadmin &
sudo nuhup tcpdump -i xxxxxxxxx2 -nve host 10.0.0.19 -w broker_19.pcap -C 500 -W 5 -Z ccadmin &
sudo nuhup tcpdump -i xxxxxxxxx3 -nve host 10.0.0.20 -w broker_20.pcap -C 500 -W 5 -Z ccadmin &
簡單解釋一下命令含義:
- -i vethfe2axxxx 後面新增的是網路卡名稱
- -nve host 需要新增的是需要監聽的IP
- -w broker_18.pcap 會將記錄生成到broker_18.pcap的檔案中
- -C 500 每隔500M生成一個檔案
- -W 5 保留最近生成的5個檔案
- -Z ccadmin 指定使用者名稱
3.2、異常浮現
而後我們在螢幕前等待了漫長的近2個小時後,終於捕獲到了一條異常資訊,因此馬不停蹄地將資料下載下來,進行對照分析。首先看到的便是Wireshark
幫助解析生成的前後報文
報文總長度為2712個位元組,約3K的資料,但我們驚訝地發現,其中有15個位元組的資料出現了偏差
至此,定位可能就是網路傳輸出現了問題,不過我們還需要做更多的校驗工作。Wireshark
工具雖然好用,但卻不是萬能的,它只能解析Kafka的傳輸協議,無法對內容進行校驗與還原
(我們在樣本中隨機取了3個正常互動的訊息進行對比,發現在網路傳送前與傳送後的資料完全一致)
3.3、Kafka協議解析
Kafka的協議分為傳輸協議及儲存協議,筆者對它們相對比較熟悉,因此開始直接對前後傳輸的報文進行了解析,首先是Request
import org.apache.kafka.common.requests.FetchRequest;
import org.apache.kafka.common.requests.RequestHeader;
import javax.xml.bind.DatatypeConverter;
import java.nio.ByteBuffer;
public class MyTest {
public static void main(String[] args) {
// 08-30 15:06:40.820028
String hexString =
"0e 1f 48 2d 7e 32 06 82 25 e9 a3 d9 08 00 45 00 " +
"00 ab 3f b3 40 00 40 06 35 ed 62 02 00 55 62 02 " +
"00 54 eb 2e 23 85 68 57 32 37 b5 08 3f b2 80 18 " +
"7d 2c c5 4a 00 00 01 01 08 0a b7 d1 e6 5c 26 30 " +
"b9 11 00 00 00 73 00 01 00 0c 11 85 6f bf 00 15 " +
"62 72 6f 6b 65 72 2d 31 30 30 32 2d 66 65 74 63 " +
"68 65 72 2d 30 00 00 00 03 ea 00 00 01 f4 00 00 " +
"00 01 00 a0 00 00 00 72 52 11 e0 11 85 6f bf 02 " +
"13 5f 5f 63 6f 6e 73 75 6d 65 72 5f 6f 66 66 73 " +
"65 74 73 02 00 00 00 15 00 00 00 03 00 00 00 00 " +
"13 36 67 3a 00 00 00 03 00 00 00 00 00 00 00 00 " +
"00 10 00 00 00 00 01 01 00";
// FetchRequestData(clusterId=null, replicaId=1002, maxWaitMs=500, minBytes=1, maxBytes=10485760,
// isolationLevel=0, sessionId=1917981152, sessionEpoch=293957567,
// topics=[FetchTopic(topic='__consumer_offsets', partitions=[FetchPartition(partition=21, currentLeaderEpoch=3,
// fetchOffset=322332474, lastFetchedEpoch=3, logStartOffset=0, partitionMaxBytes=1048576)])], forgottenTopicsData=[], rackId='')
StringBuilder sb = new StringBuilder();
char[] charArray = hexString.toCharArray();
for (char c : charArray) {
if (c != ' ') {
sb.append(c);
}
}
hexString = sb.toString();
byte[] byteArray = DatatypeConverter.parseHexBinary(hexString);
ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray);
System.out.println("len is : " + byteBuffer.array().length);
byteBuffer.get(new byte[66]);
byteBuffer.getInt();
RequestHeader header = RequestHeader.parse(byteBuffer);
System.out.println(header);
FetchRequest fetchRequest = FetchRequest.parse(byteBuffer, (short) 12);
System.out.println(fetchRequest);
}
}
最終輸出如下
len is : 185
RequestHeader(apiKey=FETCH, apiVersion=12, clientId=broker-1002-fetcher-0, correlationId=293957567)
FetchRequestData(clusterId=null, replicaId=1002, maxWaitMs=500, minBytes=1, maxBytes=10485760, isolationLevel=0, sessionId=1917981152, sessionEpoch=293957567, topics=[FetchTopic(topic='__consumer_offsets', partitions=[FetchPartition(partition=21, currentLeaderEpoch=3, fetchOffset=322332474, lastFetchedEpoch=3, logStartOffset=0, partitionMaxBytes=1048576)])], forgottenTopicsData=[], rackId='')
且TCP傳輸前後的內容一致,因此問題沒有出現在Request上
接下來就需要進行Response的二進位制協議解析,程式碼如下
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.record.MemoryRecords;
import org.apache.kafka.common.requests.FetchResponse;
import org.apache.kafka.common.requests.ResponseHeader;
import javax.xml.bind.DatatypeConverter;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.Map;
public class MyTest2 {
public static void main(String[] args) {
08-30 15:06:41.853611
String hexString =
"0e 1f 48 2d 7e 32 06 82 25 e9 a3 d9 08 00 45 00 " +
"00 ab 3f b3 40 00 40 06 35 ed 62 02 00 55 62 02 " +
"00 54 eb 2e 23 85 68 57 32 37 b5 08 3f b2 80 18 " +
"7d 2c c5 4a 00 00 01 01 08 0a b7 d1 e6 5c 26 30 " +
"b9 11 00 00 00 f9 11 85 6f c1 00 00 00 00 00 00 " +
"00 72 52 11 e0 02 13 5f 5f 63 6f 6e 73 75 6d 65 " +
"72 5f 6f 66 66 73 65 74 73 03 00 00 00 03 00 00 " +
"00 00 00 00 15 09 0c 9c 00 00 00 00 15 09 0c 9c " +
"00 00 00 00 00 00 00 00 00 ff ff ff ff 89 01 " +
" 00 00 00 00 15 09 0c 9c 00 00 00 7c 00 00 " +
"00 03 02 08 16 d8 10 00 00 00 00 00 00 00 00 01 " +
"91 a2 1b 8f 3c 00 00 01 91 a2 1b 8f 3c ff ff ff " +
"ff ff ff ff ff ff ff 00 00 00 00 00 00 00 01 92 " +
"01 00 00 00 56 00 01 00 0b 43 43 4d 2d 65 6e 63 " +
"72 79 70 74 00 16 43 53 53 2d 49 49 53 2d 53 69 " +
"74 75 61 74 69 6f 6e 51 75 65 72 79 00 00 00 00 " +
"30 00 03 00 00 00 00 00 00 00 00 ff ff ff ff 00 " +
"00 00 00 01 91 a2 1b 8f 3b 00 " +
" 00 00 00 00 15 00 00 00 00 00 00 13 36 67 " +
"47 00 00 00 00 13 36 67 47 00 00 00 00 00 00 00 " +
"00 00 ff ff ff ff 01 00 00 00 ";
// String hexString =
// "0e 1f 48 2d 7e 32 06 82 25 e9 a3 d9 08 00 45 00 " +
// "00 ab 3f b3 40 00 40 06 35 ed 62 02 00 55 62 02 " +
// "00 54 eb 2e 23 85 68 57 32 37 b5 08 3f b2 80 18 " +
// "7d 2c c5 4a 00 00 01 01 08 0a b7 d1 e6 5c 26 30 " +
// "b9 11 00 00 04 65 11 85 6f c0 00 00 00 00 00 00 " +
// "00 72 52 11 e0 03 13 5f 5f 63 6f 6e 73 75 6d 65 " +
// "72 5f 6f 66 66 73 65 74 73 02 00 00 00 15 00 00 " +
// "00 00 00 00 13 36 67 3b 00 00 00 00 13 36 67 3b " +
// "00 00 00 00 00 00 00 00 00 ff ff ff ff da 07 " +
//
// " 00 00 00 00 13 36 67 3b 00 00 03 cd 00 00 " +
// "00 03 02 5c 37 79 85 00 00 00 00 00 0b 00 00 01 " +
// "91 a2 1b 8f 3c 00 00 01 91 a2 1b 8f 37 ff ff ff " +
// "ff ff ff ff ff ff ff 00 00 00 00 00 00 00 0c 96 " +
// "01 00 00 00 5a 00 01 00 0b 43 43 4d 2d 65 6e 63 " +
// "72 79 70 74 00 16 43 53 53 2d 49 49 53 2d 53 69 " +
// "74 75 61 74 69 6f 6e 51 75 65 72 79 00 00 00 00 " +
// "30 00 03 00 00 00 00 00 00 00 00 ff ff ff ff 00 " +
// "00 00 00 01 91 a2 1b 8f 3b 00 " +
//
//
//
// " 00 00 00 00 15 00 00 00 00 00 00 13 36 67 " +
// "47 00 00 00 00 13 36 67 47 00 00 00 00 00 00 00 " +
// "00 00 ff ff ff ff 01 00 00 00 ";
StringBuilder sb = new StringBuilder();
char[] charArray = hexString.toCharArray();
for (char c : charArray) {
if (c != ' ') {
sb.append(c);
}
}
hexString = sb.toString();
byte[] byteArray = DatatypeConverter.parseHexBinary(hexString);
ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray);
System.out.println("len is : " + byteBuffer.array().length);
byteBuffer.get(new byte[66]);
byteBuffer.getInt();
ResponseHeader responseHeader = ResponseHeader.parse(byteBuffer, (short) 0);
System.out.println("responseHeader is " + responseHeader);
FetchResponse<MemoryRecords> fetchResponse = FetchResponse.parse(byteBuffer, (short) 11);
System.out.println(fetchResponse);
LinkedHashMap<TopicPartition, FetchResponse.PartitionData<MemoryRecords>> map = fetchResponse.responseData();
System.out.println("map size is : " + map.size());
System.out.println("map is : " + map);
for (Map.Entry<TopicPartition, FetchResponse.PartitionData<MemoryRecords>> entry : map.entrySet()) {
System.out.println();
System.out.println();
System.out.println();
System.out.println();
System.out.println("TP is : " + entry.getKey());
FetchResponse.PartitionData<MemoryRecords> value = entry.getValue();
MemoryRecords records = value.records();
records.batches().forEach(batch -> {
System.out.println("isValid: " + batch.isValid());
System.out.println("crc : " + batch.checksum());
System.out.println("baseOffset : " + batch.baseOffset());
});
}
}
}
其中關鍵的部分是檢視2個CRC的邏輯:
- 一塊是從協議體中獲取到的,即Broker計算的CRC內容,這個對比傳輸前後均一致
- 一塊是根據訊息內容動態計算的,訊息傳輸前,動態計算的CRC內容能與自身儲存的對齊,但訊息接收後,動態計算的CRC便發生了偏差
下圖是真實生產業務日誌中爆出的異常
其中發現計算出來的CRC是2481280076。而後將我們dump下來的二進位制進行協議解碼,執行上述程式碼後發現結果與生產環境的一致
四、磁碟排查
後續我們對傳送出去的內容與磁碟的內容做了對比,內容一致,因此bug不是因磁碟導致
五、結論
由於出現問題的訊息在TCP傳送前後出現了diff,而正常收到的訊息,在TCP傳送前後均一致,因此
定位為網路出現了問題
網路的同學也認為這個現象不符合預期,已經開始介入了排查
總結:其實根據這些年處理Kafka異常的經驗,這個bug第一時間拿到的時候,就感覺不像是Kafka自己的問題,關鍵是異常報錯在Kafka的Broker端,我們需要不斷提升自證清白的能力