天天看點

複盤報告:心跳資料丢失,從發現到解決曆經一年多的bug

bug複盤報告:zmq心跳資料丢失,一個從發現到解決曆經一年多的bug。

時間線

  1. 大約在2020年10月,内網測試服服務端更新,發現程序A重新開機後,與其他程序之間的心跳協定不通,不能正常的提供服務。重新開機後,就正常了。
  2. 這個情況持續了很長時間。隻在重新開機時才會出現,且發生機率很低,一個月一次的機率。是以這個問題就被擱置了。
  3. 現網部署後,發現更新時,也出現了這個情況,但是由于重新開機就可以解決,這個問題被歸屬為:重要但不緊急的範疇。
  4. 大約2021年5月,花了一些時間嘗試解決這個問題。由于沒有找到重制的步驟,是以等待出現問題時觀察現場。後面有一次出現了,進行排查:

看日志,沒有異常。重點看網絡。

netstat發現程序A與程序B的tcp是連接配接的。

tcpdump發現程序A發出的心跳資料,程序B的tcp層接收資料了,但是程序B的業務層沒有收到資料。由于A與B之間的心跳資料沒有閉環,是以程序B認為程序A不存在,故程序A雖然啟動成功了,但是沒能正常對外提供服務。

由于内網不能一直儲存現場,需要對外提供服務,重新開機了程序A,問題的原因還是沒有找到。

  1. 2021年12月,做了一個一鍵編譯釋出的工具,便于服務端更新,提高工作效率。發現使用一鍵釋出後,這個問題又出現了。經過多次重試,發現使用一鍵釋出,這個問題必現。經過排查,排除了工具本身的問題。

程序B是listen端,程序A是connect端,A主動連B。底層通信元件是ZMQ。按照ZMQ設計,底層是會自動重連的。

初步懷疑是ZMQ的bug,于是把ZMQ更新到最新版本,發現問題仍然存在,确定是業務代碼有問題。

通過tcpdump,netstat等工具,發現現象和之前是一樣的:程序A發出的心跳資料,程序B的tcp層接收資料了,但是程序B的業務層沒有收到資料。

  1. 這個問題确定是必現問題,就好解決了,繼續推進。通過多次重試,發現了一個規律:

程序B是listen端,程序A是connect端,A主動連B。隻要程序B重新開機後,程序A在大約3秒内去連B,就會出現這個問題。如果是3秒後,A再連B,就不會出現這個問題。

期間也懷疑代碼多線程的問題,通過測試也排除了。

  1. 有了這個3秒的規律,于是看代碼,看哪些地方會和這個3有關系。果然代碼有一處跟這個相關:
    複盤報告:心跳資料丢失,從發現到解決曆經一年多的bug

    把3改為5、10來測試,發現問題和這個數字強相關。

    于是把這行代碼删除。

  2. 修改代碼後,重新釋出,多次測試後,結論:問題解決。原因是什麼呢?後面講。

問題如何發現的

因為有可視化監控。如果沒有可視化監控,這個問題是很難發現的。

複盤報告:心跳資料丢失,從發現到解決曆經一年多的bug

這個圖展現的是程序之間的心跳資訊。

綠色:正常

紅色:失活,失活了多長時間,可以倒推出最後一次心跳正常的時間。

問題産生原因

  1. 最開始之是以偶現,是因為更新時,手速有快慢,快的時候就撞到了這個問題。
  2. 開發了一鍵釋出工具,重新開機程序就很快,故而偶現問題就找到了必現方法。
  3. 代碼上的bug:執行zmq_bind() 後,套接字進入靜音狀态,在router模式下是丢棄資料的。需要bind後,立即調用recv,跳過靜音階段。删除代碼中的sleep,問題即解決。

參考:

zmq_connect(3) - 0MQ Api

複盤報告:心跳資料丢失,從發現到解決曆經一年多的bug

ZeroMQ史上最全的知識總結(一):常見的通訊模式

複盤報告:心跳資料丢失,從發現到解決曆經一年多的bug
  1. 繼續分析。執行zmq_bind() 後,套接字進入靜音狀态,sleep結束後調用recv,結束了靜音狀态,後續應該可以正常接收消息的,但是并沒有。
在連接配接建立的過程中,當TCP三向交握後,StreamEngine會再一次和另一端握手,這是ZMQ應用層自己的握手協定,握手成功後,就進行msg的傳遞了。

原因是:靜音狀态下,router丢棄了dealer的握手協定。後面的資料也就不接收了。

正常的流程:tcp三次握手成功--zmq層握手成功--業務層心跳激活--業務層資料傳遞。

複盤沉澱

  • 得益于可視化,對發現問題提供了很大的幫助。
  • 找bug類似于找規律,多試試有好處。
  • 線上文檔持續跟蹤,即使時間跨度很長,也能接續起來。
  • 遇到問題時,第一時間去檢視官方文檔。
zmq