1.問題來源
公司線上環境出現MQ不能接受訊息的異常,運維和開發人員臨時切換另一臺伺服器的MQ後恢復。同時運維人員反饋在出現問題的伺服器上很多基本的命令都不能執行,出現如下錯誤:
2. 初步原因分析和解決
讓運維的兄弟在服務上檢視記憶體、CPU、網路、IO等基本資訊都正常。於是自己到運維的伺服器上看了一下,下面是slabtop –s c的執行結果,問題初步原因貌似出現了:
如果看到這個截圖你看不出什麼異常的話,下面的內容你可能不感興趣,哈哈。。。
task_struct是核心對程式的管理單位,通過slub(slab的升級版,如果你對slub不瞭解也不影響下面的內容,只要瞭解slab就行了)進行節點的管理,正常負載的服務不應該出現task_struct的slub結構體佔用記憶體最大的情況,這說明這臺伺服器上開啟了大量的程式(Linux核心態對程式和執行緒都是一個單位,不要糾結這個,後面可能會程式、執行緒混用)。
通過這個資訊,兄弟們發現這臺伺服器上有近3萬個執行緒,同時也定位到出問題的網元(一個新同學的程式碼沒有Review直接上線,裡面有一個BUG觸發了異常建立大量執行緒)。
問題貌似到這裡就結束了,但是作為一個有情懷的程式設計師,這只是一個開始(哥的情懷白天都被繁瑣的工作磨沒了,只能在這深夜獨享了。。。)
3. Linux執行緒數的限制
3.1 應用層測試程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#define MEMSIZE (1024 * 1024 * 256) void thread(void) { sleep(100); return; } int main() { pthread_t id; int ret; int num = 0; while (1) { ret = pthread_create(&id, NULL, (void*)thread, NULL); ++num; if (ret != 0) break; } printf("pthread_create fail with ret=%d, total num=%d\n", ret, num); sleep(100); return 0; } |
通過strace跟蹤,發現問題出現在copy_process函式,那剩下的工作就是分析copy_process返回異常的原因了。
3.2 逆向分析
這個時候逆向分析最簡單直接,可以直接定位到問題原因。
首先通過strace分析,查詢出問題的系統呼叫是clone函式。
SYS_clone—>do_fork—>copy_process。核心態函式的分析工具這次試用了systemtap,下面就是沒有任何美感的stap程式碼了,將就著看看吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
probe kernel.statement("*@kernel/fork.c:1184") { printf("In kernel/fork.c 1184\n"); } probe kernel.statement("*@kernel/fork.c:1197") { printf("In kernel/fork.c 1197\n"); } probe kernel.statement("*@kernel/fork.c:1206") { printf("In kernel/fork.c 1206\n"); } probe kernel.statement("*@kernel/fork.c:1338") { printf("In kernel/fork.c 1338\n"); } probe kernel.statement("*@kernel/fork.c:1342") { printf("In kernel/fork.c 1342\n"); } probe kernel.statement("*@kernel/fork.c:1363") { printf("In kernel/fork.c 1363\n"); } probe kernel.statement("*@kernel/fork.c:1369") { printf("In kernel/fork.c 1369\n"); } probe kernel.statement("*@kernel/fork.c:1373") { printf("In kernel/fork.c 1373\n"); } probe kernel.function("copy_process").return { printf("copy_process return %d\n", $return) } function check_null_pid:long(addr:long) { struct pid *p; p = (struct pid*)THIS->l_addr; if (p == NULL) THIS->__retvalue = 0; else THIS->__retvalue = 1; } probe kernel.function("alloc_pid") { printf("alloc_pid init\n"); } probe kernel.statement("*@kernel/pid.c:301") { printf("alloc_pid 301\n"); } probe kernel.statement("*@kernel/pid.c:312") { printf("alloc_pid 312\n"); } probe kernel.function("alloc_pid").return { printf("alloc_pid return %ld\n", check_null_pid($return)); } |
發現問題出在alloc_pid失敗,分析核心程式碼,這個受限於kernel.pid_max引數。
將引數調大到100000後,再次執行。
繼續通過strace跟蹤,這次發現問題出在了mprotect函式
這個問題是由於當個執行緒的mmap個數限制,受限於vm.max_map_count引數。
將引數調大到100000後,再次執行,執行緒數明顯增加了。
其實這裡面還有一個引數kernel.threads-max限制,由於系統預設將這個引數設定為800000,非常大,所以這個引數的影響一直沒有保留出來。
後面又犯賤把相關的引數都設定成800000,結果記憶體耗盡,系統直接沒響應了。。。。
3.3 正向分析
直接分析copy_process程式碼
copy_process
3.3.1 記憶體限制
dup_task_struct–>alloc_task_struct_node/alloc_thread_info_node/arch_dup_task_struct–>kmme_cache_alloc_node(slub.c)–>slab_alloc_node–>
“CONFIG_MEMCG_KMEM” //這裡也是一個坑,docker這種基於cgroup的也會影響,可能會因為分配給slub的記憶體不夠用出現執行緒限制
具體函式:
alloc_pages—->__memcg_kmem_newpage_charge–>memcg_charge_kmem–>__res_counter_charge–>res_counter_charge_locked
3.3.2 Threads-max引數限制
if (nr_threads >= max_threads) // threads-max 引數影響
3.3.3 Pid_max引數限制
alloc_pid–>alloc_pidmap //pid_max引數影響
3.3.4 單程式記憶體限制
單個程式的執行緒數,受限於vm.max_map_count限制
4. 總結
/proc/sys/kernel/pid_max #作業系統執行緒數限制
/proc/sys/kernel/thread-max #作業系統執行緒數
max_user_process(ulimit -u) #系統限制某使用者下最多可以執行多少程式或執行緒
/proc/sys/vm/max_map_count #單程式mmap的限制會影響當個程式可建立的執行緒數
/sys/fs/cgroup/memory/${cgroup}/memory.kmem #單個docker 核心記憶體的限制,可以影響task_struct等slab節點的申請,間接影響可建立的執行緒數