core真的那麼難以追蹤嗎?

本週遇到了好幾個core都很有典型性。在這裡和大家分享下。

相信有過Linux編程經驗的人,肯定都遇到過。感覺周圍人很多對core有天然的恐懼感,尤其對剛入行不久的同學來說。當然了,也有工作好幾年看到core也束手無策的。今天就分析一下,core,其實大部分都是很容易解決的。如果一個core很難以復現,那麼說明還是很複雜的,算是Corner case,可能需要很長時間,腦子裡要有很好的運行時狀態才可以(閱讀源碼,學習的是邏輯;將源碼對應到運行時的狀態,分析一些狀態機的轉換,再去分析可能會發生的情況)。相信前幾篇文章會對這種Corner case的分析與解決打下比較好的基礎。

相反,那種每次必現,或者復現比率非常高的case,是非常容易解決的。

多線程必然出core?

如果是你新加入的代碼引入的core,實際上非常容易解決的,簡單的對比一下修改的diff,然後看一下是否有比較低級的錯誤。如果發現不了,看是否是多線程的問題?單線程如果沒有出core,改成多線程就出core,那麼就說明多線程競爭某些變量了:同時修改某些變量導致出問題。這個時候你可能第一反應會加鎖。我本人非常反感加鎖;即使你加鎖的粒度很小,作用域也夠小,但是隻要是加鎖,就代表有阻塞,就代表維護起來會很麻煩。這些共享變量真的那麼值得加鎖嗎?可否換成局部變量?如果他是一塊動態內存,為了調用某個接口時不要頻繁申請釋放內存(比如這個接口每秒幾千次的調用),那麼初始化時候申請一塊內存是絕對合理的:請把它設置為線程變量吧。每個線程初始化時候申請這塊內存。

當然了你如果實現的是一個框架或者架構調用的接口,這個接口要做到線程安全的。那麼看起來你並不能控制這個線程什麼時候啟動;線程數目會是多少個,那麼就沒有辦法了嗎?

實際上,方法有很多,比如,你可以在一個map中維護一個“線程”變量的對應關係

__gnu_cxx::hash_map< pid_t, void *> thread_data_map;
void * thread_buffer;
std::map<pid_t, void *>::iterator it;
lock
it = thread_data_map.find(pid);
if (it == thread_data_map.end()) {
    //init "thread data"
    thread_data_map[pid] = create_buffer();
} else {
    thread_buffer = it->second;
}
unlock

這裡不得不使用了一個鎖。實際上由於線程數是有限的,因此這個效率還好。我本週實現了一個qps可以達到2000+的在線應用,基本上鎖的代價在整個的call stack中可以忽略不計。

當然了,比較好的框架可能會提供OnThreadInit這種接口,那麼在這邊申請線程變量吧:

int pthread_setspecific (pthread_key_t key, const void *value);

在實現邏輯的函數獲取該變量即可:

void *pthread_getspecific (pthread_key_t key);

什麼時候要使用線程變量?看多線程下是否對該變量有寫操作,如果有就要申請線程變量(或者加鎖),否則必然出core

不要給自己埋下一個坑:

今天一個同學的core看起來是做了一個“優化”,節省了申請變量的時間。

void init() {
    my_struct * some_var;
    ...
    some_var->res = new some_res;
    some_var->res->set_value1(some_common_value1);
    some_var->res->set_value2(some_common_value2);
}

void * thread_func(my_struct * some_var) {

    some_var->res->set_value3(value_3);
   ...
}

set_value3就是一個出core的原因。這個也是一個典型的多線程必然出corecase。實際上,res沒必要提前申請吧。把它改成一個局部變量,性能幾乎沒有的損耗。當然了,如果這個資源很大,那麼就當成線程變量吧。

那麼如何分析core?

實際上,上述的場景出現的core如果僅從core本身,可能一時不好排查問題出在哪裡;而且core的位置可能還不一樣,不可避免的出現替罪羊;比如調用第三方的模塊,實際上是自己的全局變量導致到了第三方的調用棧時出core了。因此一定要排查自己的處理是否正確。

確定調用的接口是否線程安全;如果是你的同事,你得確定他說的是對的。就像今天又另外的一個core,調用者堅稱他寫的是線程安全的,彷彿不是線程安全的就像是代碼寫的不好似得;最後排查出他寫的代碼不是線程安全時,他問你什麼是線程安全的。

那麼回答一下,如何分析core?

首先了解清楚core出現的應用場景,比如長句子處理會有core;多線程處理會有core;特殊字符會有core;這個QA會給你一個比較清楚的說明。然後通過corecall stack去大概確定大概的源碼位置。

源碼面前,了無祕密。

你讀源碼,它展現的是邏輯;但是你腦中,要有個runtime的運行時調用棧,多線程的調度;天馬行空。

當然了,還是解決不了,開始從core得到更詳細的信息吧!看看調用棧的參數是什麼,切換一個線程看看其他的幾個線程的frame是什麼。

還解決不了?

那麼它是Corner case,只要把應用在core後立即啟動就行了。記得EMC有個bug的選項是unable to root cause, 形容的很貼切。同時不要忘記自我安慰:碼的碼多了,肯定有bug,誰能保證服務100%呢?