記錄一次線上組件崩潰的解決過程



馬上就要離職了,想想工作中有些東西還是需要沉淀下來的,不僅僅要沉淀到心里,因為年紀大了_,很容易忘記,不是有句話么,好記性不如爛筆頭。

分析這個bug之前先說點別的。

解決bug的大致思路

我覺的解bug和醫生看病是一樣的,中醫看病講究望聞問切。軟件出了毛病也按這個套路來,但是不需要聞。

  • 望。觀察表面現象,server端出了問題還是client端?現象是什么?log里記錄了什么?
  • 問。詢問客戶最近做了什么操作?哪些是重現問題的必要步驟?
  • 切。為軟件的code把把脈吧,由表及里,看一下軟件的哪些機能出現了問題,小問題還是大問題?表層應用代碼有問題?還是底層代碼庫有問題?只是邏輯的問題?還是性能問題?或者設計架構缺陷?

我們開發的產品運行在windows server 平台上,幾個月之前fix過一個線上發現的bug。對於有經驗的開發人員來說,需要解決的bug分為兩種:能穩定重現的和不能穩定重現的。只要能夠穩定重現,從客戶提供的種種數據中總能順藤摸瓜,找到問題根源。在我們的軟件中,這些數據包括以下幾種:

  • Windows Event Log,這也分為兩個不同的級別(只說和我們相關的)
    1. windows logs,這是操作系統級別的log,若遇到組件崩潰,windows logs下面的Application會打印log記錄。
    2. Application and Services Logs,我們的產品自定義的log放在這個目錄下面,這些log這是最直接的信息,如插在軟件中的一個個索引,報錯之后能夠很快定位代碼位置,找到問題突破口。
  • 網絡包(wireshark包),這些是實時動態的數據,可以作為review靜態代碼時的輔助,查看數據交互是否出現了問題。是不是漏發數據或者多發了等等。
  • trace log,這是更加詳細的log,如果windows log是CT片,那么trace log就是核磁共振片了。這些log的打印會影響性能,因此是有開關的,平時處於關閉狀態,如果需要把開關打開后,log就會被打印出來。這些log首先被寫入windows Message Queue中,然后使用工具進行顯示或重定向到log文件。
  • 數據庫備份。在實驗室里重現問題使用客戶的數據庫可能會更容易些。
  • dump文件。抓取dump文件是一種在遇到代碼崩潰時行之有效的獲取相關信息的方法,它是出錯時內存的一份快照。我們會在下面介紹如何在windows環境下設置注冊表,在出現崩潰時自動抓取dump。

根據bug的難易程度,找問題出現的根源有三種手段,可以層層遞進或者同時進行:

  • 開發人員review靜態代碼,這可能需要結合前面提到的兩種log或者網絡包來review。有可能問題的根源很簡單,一眼就能看出來。下面的步驟就省了。
  • 如果review代碼發現不了問題或者問題不好重現,測試可以嘗試去重現問題,尋找規律,如果自己的制作的數據庫重現不了可以使用客戶數據庫。問題可以穩定重現后,開發人員可以在remote debug來進一步跟蹤代碼。但是release環境下的debug因為有編譯優化存在,有些內存數據更本顯示不出來或者是錯誤的,代碼執行順序有時候也很怪異。這點需要小心。
  • 鑒於debug release環境的難處,開發人員可以自己搭建一個debug版的環境進行重現、調試。但是有一點,還是因為release和debug環境的差異,有時候只有在release環境下才能重現問題,這種情況下這種方法就失效了。

還有一種特殊情況就是出現組件崩潰時,這種情況下我們會抓取到dump文件,把dump文件導入到visual studio中,就能一步步查看call stack來尋找問題的出錯點。

好了,前面將我們解決bug時需要的信息以及解決方法做了一個簡單的總結,下面就具體說一下博主一次解決組件崩潰的經歷。很早之前的事了,dump文件和環境都沒有了,主要從以下三點介紹:

  • 問題是如何重現的;
  • 如何抓取dump文件;
  • 問題根源在哪里

問題描述

客戶發現有一個server組件一周會出現至少一次崩潰,windows的application log報出類似如下錯誤:

根據客戶的描述,這個server組件大約有100多個socket連接,這些連接每隔幾分鍾就會有一些斷掉然后重連(有可能是網絡問題導致的),他們推斷這和組件崩潰有一定的關系。

因為問題不好重現,測試團隊先行重現客戶的這個bug。使用客戶數據庫,實現腳本模擬客戶的socket斷開和重連。

前面也說過,bug分為容易重新和不容易重現的,這個bug的第一個難點是如何重現,測試為了增加壓力,將連接數增加至300,socket的斷開時間間隔逐漸改小等等。

如何抓取dump文件

在重現問題之前,需要配置windows在組件崩潰時自動抓取dump文件,如何配置,很簡單,將下面的注冊表項導入注冊表即可,注意要把DumpComponentName.exe替換為你的需要抓dump的組件。

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps]
"DumpType"=dword:00000002
"DumpFolder"=hex(2):43,00,3a,00,5c,00,43,00,72,00,61,00,73,00,68,00,44,00,75,\
  00,6d,00,70,00,00,00
"DumpCount"=dword:000000ff

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\DumpComponentName.exe]
"DumpType"=dword:00000002
"DumpCount"=dword:000000ff
"DumpFolder"=hex(2):43,00,3a,00,5c,00,43,00,72,00,61,00,73,00,68,00,44,00,75,\
  00,6d,00,70,00,5c,00,44,00,75,00,6d,00,70,00,43,00,6f,00,6d,00,70,00,6f,00,\
  6e,00,65,00,6e,00,74,00,4e,00,61,00,6d,00,65,00,00,00

問題根源分析

因為dump文件和環境都不在了,不能介紹分析dump文件的過程了。只能分析最后的結果,發現了兩個問題:

問題一:有關STL容器的線程安全問題

有關這個問題建議先拜讀一下Scott Meyers的effective STL 條款12。

引用其中的話:

在STL容器(和大多數廠商的願望)里對多線程支持的黃金規則已經由SGI定義,並且在它們的STL網站[21]上
發布。大體上說,你能從實現里確定的最多是下列內容:

  • 多個讀取者是安全的。多線程可能同時讀取一個容器的內容,這將正確地執行。當然,在讀取時不能
    有任何寫入者操作這個容器。
  • 對不同容器的多個寫入者是安全的。多線程可以同時寫不同的容器。

也就是STL容器只能夠在兩種情況下保證多線程安全:

  • 同時讀取一個不會發生變化的容器。
  • 同時寫多個不同的容器。

僅僅這兩種情況,看看下面的發現有問題的code,你能看出問題在哪里么?

template<typename T>
ResourceResult getValue(ResourceKey key, T value)
{
	ResourceResult result = ResourceFailure;
	resourcevalues_.guard();//lock
	Resources::iterator resource = resourcevalues_.resources().find(key);
	if (resource == resourcevalues_.resources().end()) // not found
	{		
		resourcevalues_.addResource(key, resourceValueEntry);
		resourcevalues_.unGuard();//unlock
	}
	else
	{
		// item cached			
		resourcevalues_.unGuard();//unlock
	    if ((*resource).second.getValue(value))
		{
			result = ResourceSuccess;
		}
		else
		{
			result = ResourceConversionError;
		}	
	}	
	return result;
}

這是典型的有問題的SLT多線程編程,寫這段代碼的coder可能是這么認為的:

  • 多線程不能同時寫一個容器,因此加了lock
  • 多線程可以同時讀一個容器。

第一條沒有問題,但是第二條是有前提的,同時讀一個容器需要在容器不發生變化的情況下。如果恰巧在讀取容器元素時另外一個線程對此容器進行寫操作,讀線程的iterator會失效,接下來的行為是未定義的。

如何修改?對,讀也需要加鎖。

template<typename T>
ResourceResult getValue(ResourceKey key, T value)
{
	ResourceResult result = ResourceFailure;
	resourcevalues_.guard();//lock
	Resources::iterator resource = resourcevalues_.resources().find(key);
	if (resource == resourcevalues_.resources().end()) // not found
	{		
		resourcevalues_.addResource(key, resourceValueEntry);		
	}
	else
	{
		// item cached			
	    if ((*resource).second.getValue(value))
		{
			result = ResourceSuccess;
		}
		else
		{
			result = ResourceConversionError;
		}	
	}	
	resourcevalues_.unGuard();//unlock
	return result;
}

這就沒有問題了。

問題二 : 一個線程句柄釋放問題

GeneratorThread::~GeneratorThread()
{
	if (thread_)
	{
	    stop();
	    thread_->wait(5000);	
	    delete thread_;
	}
    delete generator_;
}

上面代碼的背景是一個controller thread(簡稱CT)管理着多個Generator Thread(GT),CT會監控每個GT的狀態,如果有的狀態不是Active,那么就delete掉它的資源,調用這個析構函數的時候會釋放掉GT所擁有的線程句柄(thread_)和線程使用的相關資源(generator_)(socket和應用層協議的相關資源),delete thread_之前需要先給這個thread發送一個stop()信號:

void
GeneratorThread::stop()
{
    (void) stopRequest_.set();
}

然后調用thread_->wait(5000)確保線程收到這個stop信號之后,采取delete thead_,為的是防止打斷thread有可能正在進行的其他工作。通過抓到的dump文件,發現在這里跳出了exception。我們懷疑5秒的等待時間是不是太短了,於是改成了下面的代碼:

GeneratorThread::~GeneratorThread()
{
	if (thread_)
	{
	    stop();
	    thread_->wait(INFINITE);//wait forever	
	    delete thread_;
	}
    delete generator_;
}

問題依然得不到解決,繼續研究,發現thread_包含的線程的真正句柄(thread_對象所屬類的成員變量,windows線程函數_beginthreadex的返回值),此時已經變成了無效句柄。問題有眉目了,竟然沒有注意到野指針問題,我們在刪除一個指針之后,編譯器只會釋放該指針所指向的內存空間,而不會刪除這個指針本身,因此,如果delete兩次GeneratorThread,析構函數會進入兩次,第二次執行下面這個句子的時候,雖然thread_已經被delete過了,但仍然能夠進入

if(thread_)

這時候一個無效句柄調用wait函數,跳出了exception。如何修改就很簡單了。

GeneratorThread::~GeneratorThread()
{
	if (thread_)
	{
	    stop();
	    thread_->wait(5000);
	    delete thread_;
	    thread_=nullptr;
	}
    delete generator_;
    generator_=nullptr;
}

因此,看似復雜的問題往往是由一些低級錯誤造成的。反過來,如果寫代碼的時候如果不小心,一些小錯誤往往也會釀成大禍。

好了,最后是出補丁,release...

這篇博文結束了。

后續如果時間充足,會有工作中的其他總結,包括但不限於:

  • 如何用OOP實現我們的應用層協議——協議狀態機
  • 設計模式實戰——我們的產品中用到的其它設計模式
  • google C++ mock測試框架在我們產品中是如何使用的
  • 談談開發眼中的敏捷——我們的敏捷團隊是如何運作的
  • 記錄其他疑難bug的解決過程
  • 。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM