當點擊CAT的查看LogViews時出現Sorry, the message is not there. It could be missing or archived.
這時候出現這種問題會一頭霧水,去github上查看貌似也沒有給出明確答覆。
這裏根據自己的猜想以及源碼角度的查看來定位問題。
首先諮詢CAT維護相關人員得到一個非常重要的信息就是消息編號中的小時數不同
項目名-ac13bd78-430207-91
項目名-ac13bd78-430208-91
第二段代表的是IP,第三段代表小時數,第四段代表當前自增數
舉例:
從LogViews中的消息編號列表中發現當前小時如果屬於430207的話,是可以打開的,非430207的話就是消息丟失。
然後從CAT的state報表中發現有一列消息丟失數據:
兩臺機器時鐘不準導致消息存儲丟失 這個場景用於Pigeon,服務端id是由客戶端產生,客戶端和服務端時鐘差2小時,會導致存儲丟失
猜想這一列是不是統計了我剛好消息丟失的數據。
有了這些線索,我們開始假設和驗證!
從客戶端編號的消息編號生成開始入手。
我的客戶端版本是 2.0
源碼查看
- 發送消息的時候會判斷該消息是否是EVENT消息,如果有則放入m_atomicTrees對象中
TcpSocketSender.java
public void send(MessageTree tree) {
if (isAtomicMessage(tree)) {
boolean result = m_atomicTrees.offer(tree, m_manager.getSample());
if (!result) {
logQueueFullInfo(tree);
}
} else {
boolean result = m_queue.offer(tree, m_manager.getSample());
if (!result) {
logQueueFullInfo(tree);
}
}
}
- 一旦放入m_atomicTrees對象時,則會被一個單獨監控的線程檢測到
public class MergeAtomicTask implements Task {
@Override
public String getName() {
return "merge-atomic-task";
}
@Override
public void run() {
while (true) {
// 時刻監控這個隊列,一旦有並且是當前小時的消息則會滿足
if (shouldMerge(m_atomicTrees)) {
MessageTree tree = mergeTree(m_atomicTrees);
boolean result = m_queue.offer(tree);
if (!result) {
logQueueFullInfo(tree);
}
} else {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
break;
}
}
}
}
@Override
public void shutdown() {
}
}
mergeTree這個方法有點重要,其實就是將當前的消息進行合併,爲什麼需要合併?
我猜的話應該是在同一個消息樹中,每條消息都對應了一個消息編號,但是對於消息樹的編號來說只要第一條消息的消息編號就能夠定位到,而第二條往後走的消息這個編號根本就沒用了,但是不想浪費了,放入消息編號隊列中,爲下一條消息樹的編號生成所用。
private MessageTree mergeTree(MessageQueue trees) {
int max = MAX_CHILD_NUMBER;
DefaultTransaction t = new DefaultTransaction("_CatMergeTree", "_CatMergeTree", null);
// 先獲取消息樹中的第一條消息
MessageTree first = trees.poll();
t.setStatus(Transaction.SUCCESS);
t.setCompleted(true);
t.addChild(first.getMessage());
t.setTimestamp(first.getMessage().getTimestamp());
long lastTimestamp = 0;
long lastDuration = 0;
while (max >= 0) {
// 注意這裏是從第二條開始
MessageTree tree = trees.poll();
if (tree == null) {
t.setDurationInMillis(lastTimestamp - t.getTimestamp() + lastDuration);
break;
}
lastTimestamp = tree.getMessage().getTimestamp();
if (tree.getMessage() instanceof DefaultTransaction) {
lastDuration = ((DefaultTransaction) tree.getMessage()).getDurationInMillis();
} else {
lastDuration = 0;
}
t.addChild(tree.getMessage());
// 這裏非常關鍵,會將本次產生的id編號重新放入生成的隊列中。
m_factory.reuse(tree.getMessageId());
max--;
}
((DefaultMessageTree) first).setMessage(t);
return first;
}
// D:\lib\maven\com\dianping\cat\cat-client\2.0.0\cat-client-2.0.0-sources.jar!\com\dianping\cat\message\internal\MessageIdFactory.java
// m_factory.reuse(tree.getMessageId()); 對應的實現
public void reuse(String id) {
m_reusedIds.offer(id);
}
接下來我們只要看它是如何拿id的
MessageIdFactory.java
public String getNextId() {
// 先從隊列裏面拿到,這裏和上面生成的相關,如果有直接返回。
String id = m_reusedIds.poll();
if (id != null) {
return id;
} else {
long timestamp = getTimestamp();
if (timestamp != m_timestamp) {
m_index = new AtomicInteger(0);
m_timestamp = timestamp;
}
int index = m_index.getAndIncrement();
StringBuilder sb = new StringBuilder(m_domain.length() + 32);
sb.append(m_domain);
sb.append('-');
sb.append(m_ipAddress);
sb.append('-');
sb.append(timestamp);
sb.append('-');
sb.append(index);
return sb.toString();
}
}
這裏就會出現一個小問題,如果當前小時的id沒有拿完,下一個小時來拿id的時候發現還有,則會從隊列裏面繼續獲取生成的id編號,生成的消息樹發送到服務端,但問題在於,該編號卻是上個小時殘留的。
這時候服務端是以小時爲key存儲的,存儲的時候會發現這個編號是上一個小時的,則會直接丟棄。從state中的
兩臺機器時鐘不準導致消息存儲丟失 | 這個場景用於Pigeon,服務端id是由客戶端產生,客戶端和服務端時鐘差2小時,會導致存儲丟失
中查看到!
這部分的源碼在 : TcpSocketReceiver.MessageDecoder.decode()
中體現出來,最終的實現類是RealtimeConsumer.consume
方法
public void consume(MessageTree tree) {
long timestamp = tree.getMessage().getTimestamp();
Period period = m_periodManager.findPeriod(timestamp);
if (period != null) {
// 將消息樹交給桶處理 , 然後這裏會放到隊列然後交給另一個PeriodTask線程去處理
period.distribute(tree);
} else {
m_serverStateManager.addNetworkTimeError(1);
}
}
PeriodTask.run -> AbstractMessageAnalyzer.analyze -> DumpAnalyzer.process -> processWithStorage 處理
DumpAnalyzer.java - 關鍵代碼
public void process(MessageTree tree) {
try {
// 根據消息編號做解析
MessageId messageId = MessageId.parse(tree.getMessageId());
if (!shouldDiscard(messageId)) {
// 這裏會獲取消息編號的第三段作爲參數messageId.getHour()
processWithStorage(tree, messageId, messageId.getHour());
}
} catch (Exception ignored) {
}
}
private void processWithStorage(MessageTree tree, MessageId messageId, int hour) {
// 這個桶是以當前小時爲單位 也就是消息編號的第三段
MessageDumper dumper = m_dumperManager.find(hour);
tree.setFormatMessageId(messageId);
// 這裏根據消息編號發現消息編號匹配不到
if (dumper != null) {
dumper.process(tree);
} else {
// 然後state那裏就會多一條數據
m_serverStateManager.addPigeonTimeError(1);
}
}
ServerStatistic.Statistic.m_pigeonTimeError
但這個時候是誤導了用戶,實際上是消息隊列中殘留了上個小時的消息生成的編號導致的。
解決方案:
客戶端升級到3.0吧。它已經把隊列去掉了,每次獲取當前時間戳,來生成編號。