1. 程式人生 > 其它 >記一次線上問題定位過程

記一次線上問題定位過程

  1. 出現問題,但不能快速修復

    1. 系統在高峰期突然出現了大面積的core dump,通過gdbcore檔案發現,是core在傳送資料到另一個服務的地方,開啟堆疊對應的程式碼,是公司的一個基礎庫檔案,只是簡單的宣告一個protobufmessage物件,但這地方一般不太可能出現core啊,不然程式到處都有類似的做法,為啥不在其他地方core了?難道是服務的其他執行緒導致的?用gdb的thread apply all bt檢視各個執行緒堆疊,大部分執行緒處於sleep或者等待條件訊號中(線上服務等待處理請求吧),不太像是有問題啊,這就有點無解了。
    2. gdb開啟其他機器例項的core檔案看看,發現執行緒1還是一樣的堆疊資訊,顯示core在protobufmessage物件中。。。稍微瞄幾眼protobuf宣告物件的過程,無非就是申請記憶體,沒什麼值得懷疑的,如果這裡有問題,服務中用到pb物件的地方很多,為什麼偏偏就core在同一個地方?
  2. 降級止損

    1. 百思不得其解,查查服務的日誌,發現有大量的請求下游服務超時,檢視這個下游服務的負載情況,發現負載很高,難道是這個下游服務導致的?仔細一想,既然大量例項在同一時間core了,也會觸發線上監控程序把服務重新拉起來,這樣短時間內會有大量請求集中請求到下游某個服務的ip(下游服務有多個例項,但由於服務本身在資料還沒準備好之前,不能向註冊中心註冊,不然會暴露名字給上游,所以這時候要訪問下游只能通過ip來訪問),所以下游服務負載高不是導致core的原因,採取了批量重啟+請求降級處理。但很不幸,重啟後進程服務了短暫的時間後又core了。
  3. 降級沒用,繼續找原因止損

    1. 看來還是得先把core的原因找出來。既然都是線上程1的同樣的地方core了,這地方應該是有問題的,可以從這裡入手。檢視當前請求中的資料,發現不同core檔案中有個欄位的值是一致,這是否可以猜測是某種特殊的請求才會導致core?馬上打電話找產品運營人員,先把這種流量遮蔽了,10分鐘過去了,發現還真的不再core了,觀察了半個小時,服務正常,這時已經是凌晨4點了。。目前只能先這樣止損,等第二天找出core原因。
  4. 找到潛在原因,並修復

    1. 第二天上班仔細定位問題,還是跟昨晚一樣,開啟core檔案,檢視堆疊資訊,對比原始碼,發現core的地方的原始碼的下面幾行呼叫了一個序列化函式,而且這個函式是個inline的,因為inline函式在編譯器編譯的時候,只是把內容嵌入進去,不做函式呼叫,所以如果在這裡core了,也不會有堆疊資訊。為了驗證這個猜測,gdb除錯core檔案的時候,我故意列印當前inline函式裡面的變數,確實能打印出來。。(反證:如果程式還沒執行到這裡就core了,不可能能打印出變數資訊),所以斷定是在序列化的過程core了。那就把注意力聚焦在序列化上。序列化的實現是公司2005年的程式碼了,到現在也有16年的時間了,不太可能會出現問題吧?序列化初看很複雜,涉及到記憶體設計,序列化編碼過程。仔細一看,大概意思就是把C++各個基礎型別寫入佇列中,迭代器就先寫元素個數,再寫元素,沒毛病啊?繼續深挖,發現了一個關鍵的資訊:序列化字串的時候會判斷大小,如果超出一定大小,會拋異常。。。對比了這個特殊流量的某個字串長度,確實很長,問題應該就是出在這裡了。查了相關資料,查到是gcc的低版本中,如果底層有拋異常,會被捕捉後直接退出,也不列印堆疊了,後來有人上報了這個bug並在gcc8中修復了。這就難怪我看core檔案一直沒看出core的真正地方了。針對本次core的案例,解決辦法也很簡單,修改那份陳年老舊的程式碼(當然如果通用一點,直接hook __cxa_throw,把堆疊打印出來),至此,問題已經定位到,並找到了解決方案。
    2. 在__cxa_throw中,底層是捕捉到異常後就退出:
      1. void * execute_native_thread_routine(){
            try {
             ...   
            }catch(...){
                std::terminate();
            }
        }
        View Code
      2. core dump之後,檢視core檔案是這樣的
  5. 方案

    1. hook__cxa_throw()讓每次生成的 coredump 都帶上堆疊
    2. #include <iostream>
      #include <stdexcept>
      #include <thread>
      
      extern "C" { //加這3行程式碼,通過 hook __cxa_throw,直接 abort,可以避免 stack unwind。
      void __cxa_throw(void* ex, void* info, void (*dest)(void*)) { ::abort(); }
      }
      
      void func(){
          throw std::runtime_error("die");
      }
      
      int main() {
          std::thread t(func);
          t.join();
          return 0;
      }
      View Code
    3. 效果
  6. 程式碼

    1. https://github.com/longbozhan/sample/tree/master/hook_exception
  7. 參考

    1. https://byronhe.com/post/cpp-throw-coredump-with-backtrace/
    2. https://abcdabcd987.com/libstdc++-bug/