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應用層測試代碼
#include
#include
#include
#include
#include
#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代碼了,將就著看看吧
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節點的申請,間接影響可創建的線程數
本文永久更新鏈接地址:
http://xxxxxx/Linuxjc/1145188.html TechArticle