一、背景
master broker.log中出現如下日誌:
2019-12-03 17:42:06 INFO BrokerControllerScheduledThread1 - Slave fall behind master: -6868909625984868736 bytes
slave同步master的數據落後的是負數,難以理解!
二、分析
1 上面的日誌對應DefaultMessageStore如下代碼:
public long slaveFallBehindMuch() {
return this.commitLog.getMaxOffset() - this.haService.getPush2SlaveMaxOffset().get();
}
其中this.commitLog.getMaxOffset()這個是master的commit log的最大offset,這個不太可能出錯,如果出錯,那消息存儲麻煩就大了,commit log設計可以參考。
那麼就是this.haService.getPush2SlaveMaxOffset().get()這個有問題了。
2 push2SlaveMaxOffset來自於HAService中的屬性,顧名思義:推送給slave的最大偏移量,初始值爲0。
那麼它是什麼時候更新的,參考如下代碼:
public void notifyTransferSome(final long offset) {
for (long value = this.push2SlaveMaxOffset.get(); offset > value; ) {
boolean ok = this.push2SlaveMaxOffset.compareAndSet(value, offset);
if (ok) {
this.groupTransferService.notifyTransferSome();
break;
} else {
value = this.push2SlaveMaxOffset.get();
}
}
}
push2SlaveMaxOffset來自於offset的值,offset是什麼?
- 如果slave未同步過master的數據,offset將使用master的commit文件組中最新的一個文件的最小offset,即:同步最新的一個commit文件給slave。
- 如果slave已經同步過master數據,那麼offset將使用slave commit文件組的最大的offset,其通過心跳包的方式上報給master。
很明顯,對於第一種情況(slave未同步過master的數據),offset值不可能大於master的最大offset。
對於第二種情況,是有可能的。
3 實驗
模擬slave上報給master的offset過大,很容易聯想到如下步驟:
- 啓動一個新的master。
- 增加一個新的slave。
- 發送一些消息。
- 關閉master,並重新部署一個新的同樣brokerName的master。
此時,slave自動變成新master的slave了,但是其commit log的offset是之前master的,肯定大於新master的。
結論:如果重部master,一定要部署新的slave,老的slave重啓沒用。
另外,如果已經發生這樣的情況,移除舊的slave,再部署新的slave並不起作用,因爲master已經持有舊的slave上報上來的offset,notifyTransferSome(final long offset)中的for (… offset > value; )會阻止push2SlaveMaxOffset更新。
4 影響
- 上面的情況影響的是同步雙寫模式的master,因爲此模式的master會通過push2SlaveMaxOffset檢測是否寫入到slave了。
- 其次,發送錯誤偏移量的slave將無法正確同步數據。
- storeerror.log出現大量錯誤日誌,broker.log和store.log出現詭異的日誌,影響主從數據同步情況的判斷。
三、解決方案
- broker遷移
由於存儲文件巨大,一般broker遷移時,採取的方案是下掉舊的broker,部署新的broker的方案。那麼slave一定也要部署新的,即使slave的機器不用遷移,也不能用舊的slave。 - 已經存在的broker
- 執行停寫。
- 下掉slave。
- 停止master。
- 啓動master。
- 部署新的slave。
注意以上順序,master必須重啓,否則此問題依然存在。
四、其餘情況
導致這樣的情況的還有一種可能,就是非rocketmq slave程序和master的slave端口進行了通信,進行如下測試(假設master在127.0.0.1的10916監聽slave請求,在127.0.0.2進行測試):
- telnet 127.0.0.1 10916
- 發送999999999
- 觀察master日誌:
2019-12-04 11:35:12 INFO AcceptSocketService - HAService receive new connection, /127.0.0.2:38824
2019-12-04 11:35:12 INFO ReadSocketService - ReadSocketService service started
2019-12-04 11:35:12 INFO WriteSocketService - WriteSocketService service started
2019-12-04 11:35:16 INFO ReadSocketService - slave[/127.0.0.2:38824] request offset 4123389851770370361
2019-12-04 11:35:16 INFO WriteSocketService - master transfer data from 4123389851770370361 to slave[/127.0.0.2:38824], and slave request 4123389851770370361
2019-12-04 11:35:36 WARN ReadSocketService - ha housekeeping, found this connection[/127.0.0.2:38824] expired, 20020
2019-12-04 11:35:36 INFO ReadSocketService - makestop thread ReadSocketService
2019-12-04 11:35:36 INFO ReadSocketService - makestop thread WriteSocketService
2019-12-04 11:35:36 INFO ReadSocketService - ReadSocketService service end
2019-12-04 11:35:36 INFO WriteSocketService - makestop thread WriteSocketService
2019-12-04 11:35:36 INFO WriteSocketService - makestop thread ReadSocketService
2019-12-04 11:35:36 INFO WriteSocketService - WriteSocketService service end
2019-12-04 11:35:53 INFO BrokerControllerScheduledThread1 - slave fall behind master: 27539373 - 4123389851770370361 = -4123389851742830988 bytes
由於rocketmq的同步機制並未進行安全校驗,所以任何程序都有可能鏈接此端口進行通信,導致問題發生。
另附rocketmq解析通信數據的代碼:
if ((this.byteBufferRead.position() - this.processPostion) >= 8) {
int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % 8);
long readOffset = this.byteBufferRead.getLong(pos - 8);
this.processPostion = pos;
HAConnection.this.slaveAckOffset = readOffset;
if (HAConnection.this.slaveRequestOffset < 0) {
HAConnection.this.slaveRequestOffset = readOffset;
log.info("slave[" + HAConnection.this.clientAddr + "] request offset " + readOffset);
}
HAConnection.this.haService.notifyTransferSome(HAConnection.this.slaveAckOffset);
}
即,只要連接master監聽的slave通信端口,發送數據大於8個字節,就可能導致該問題的產生。