TiFlash 初期存在一个棘手的问题:对于复杂的小查询,无论增加多少并发,TiFlash 的整机 CPU 使用率都远远不能打满。如下图:

对 TiFlash 和问题本身经过一段时间的了解后,认为方向应该在“公共组件”(全局锁、底层存储、上层服务等)上。在这个方向上做“地毯式”排查后, 终于定位到问题的一个重要原因:高并发下频繁的线程创建和释放, 这会引发线程在创建/释放过程出现排队和阻塞现象。
由于 TiFlash 的工作模式依赖于启动大量临时新线程去做一些局部计算或者其他的事情, 大量线程创建/释放过程出现了排队和阻塞现象,导致应用的计算工作也被阻塞了。而且并发越多, 这个问题越严重, 所以 CPU 使用率不再随着并发增加而增加。 具体的排查过程, 因为篇幅有限, 本篇就不多赘述了。首先我们可以构造个简单实验来复现这个问题:
实验复现、验证
定义
首先定义三种工作模式: wait、 work 、 workOnNewThread
wait: while 循环,等待 condition_variable
work: while 循环,每次 memcpy20次(每次 memcpycopy 1000000 bytes)。
workOnNewThread: while循环,每次申请新的 thread,新 thread 内 memcpy20次, join等待线程结束,重复这个过程。
接下来按不同的工作模式组合去做实验。
各实验
实验 1:40 个 work 线程
实验 2:1000 个 wait 线程, 40 个 work 线程
实验 3:40 个 workOnNewThread 线程
实验 4:120 个 workOnNewThread 线程
实验 5:500 个 workOnNewThread 线程
具体实验结果
各实验 CPU 使用率如下:

结果分析
实验 1 和 2 表明, 即使实验 2 比实验 1 多了 1000 个 wait 线程,并不会因为 wait 线程数非常多而导致 CPU 打不满。过多的 wait 线程数并不会让 CPU 打不满。从原因上来讲,wait 类型的线程不参与调度,后面会讲到。另外,linux 采用的是 cfs 调度器,时间复杂度是 O(lgn),所以理论上大规模可调度线程数目也并不会给调度增加明显的压力。
实验 3、4、5 表明, 如果大量工作线程的工作模式是频繁申请和释放线程, 可以导致cpu打不满的情况。
接下来带大家一起分析下, 为什么线程的频繁创建和释放会带来排队和阻塞现象,代价如此之高?
多并发下,线程创建和释放会发生什么?
GDB上看到的阻塞现象
使用 GDB 查看线程的频繁创建和释放场景下的程序,可以看到线程创建和释放过程被 lll_lock_wait_private的锁阻塞掉。如图:
#0 _lll_lock_wait_private () at ../nptl/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:95
#1 0x00007fbc55f60d80 in _L_lock_3443 () from /lib64/libpthread.so.0
#2 0x00007fbc55f60200 in get_cached_stack (memp=<synthetic pointer>, sizep=<synthetic pointer>)
at allocatestack.c:175
#3 allocate_stack (stack=<synthetic pointer>, pdp=<synthetic pointer>,
attr=0x7fbc56173400 <__default_pthread_attr>) at allocatestack.c:474
#4 __pthread_create_2_1 (newthread=0x7fb8f6c234a8, attr=0x0,
start_routine=0x88835a0 <std::execute_native_thread_routine(void*)>, arg=0x7fbb8bd10cc0)
at pthread_create.c:447
#5 0x0000000008883865 in __gthread_create (__args=<optimized out>
__func=0x88835a0 <std::execute_native_thread_routine(void*)>,
__threadid=_threadid@entry=0x7fb8f6c234a8)
at /root/XXX/gcc-7.3.0/x86_64-pc-linux-gnu/libstdc++-v3/include/x86_64-pc-linux-gnu/b...
#6 std::thread::_M_start_thread (this=this@entry=0x7fb8f6c234a8,state=...)
at ../../../../-/libstdc++-v3/src/c++11/thread.cc:163
Figure 1:线程申请阻塞时堆栈
#0 _lll_lock_wait_private () at ../nptl/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:95
#1 0x00007fbc55f60e59 in _L_lock_4600 () from /lib64/libpthread.so.0
#2 0x00007fbc55f6089f in allocate_stack (stack=<synthetic pointer>, pdp=<synthetic pointer>
attr=0x7fbc56173400 <__default_pthread_attr>) at allocatestack.c:552
#3 __pthread_create_2_1 (newthread=0x7fb5f1a5e8b0, attr=0x0,
start_routine=0x88835a0 <std::execute_native_thread_routine(void*)>, arg=0x7fbb8bcd6500)
at pthread_create.c:447
#4 0x0000000008883865 in __gthread_create (__args=<optimized out>,
__func=0x88835a0 <std::execute_native_thread_routine(void*)>,
__threadid=__threadid@entry=0x7fb5f1a5e8b0)
at /root/XXX/gcc-7.3.0/x86_64-pc-linux-gnu/libstdc++-v3/include/...
#5 std::thread::_M_start_thread (this=this@entry=0x7fb5f1a5e8b0, state=...)
at ../../../.././libstdc++-v3/src/c++11/thread.cc:163
Figure 2:线程申请阻塞时堆栈
#0 __lll_lock_wait_private () at ../nptl/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:95
#1 0x00007fbc55f60b71 in _L_lock_244 () from /lib64/libpthread.so.0
#2 0x00007fbc55f5ef3c in _deallocate_stack (pd=0x7fbc56173320 <stack_cache_lock>, pd@entry=0x7fb378912700) at allocatestack.c:704
#3 0x00007fbc55f60109 in __free_tcb (pd=pd@entry=0x7fb378912700) at pthread_create.c:223
#4 0x00007fbc55f61053 in pthread_join (threadid=140408798652160, thread_return=0x0) at pthread_join.c:111
#5 0x0000000008883803 in __gthread_join (__value_ptr=0x0, __threadid=<optimized out>)
at /root/XXX/gcc-7.3.0/x86_64-pc-linux-gnu/libstdc++-v3/include/x86_64-pc-linux-gnu/bits/gth

本文围绕TiFlash复杂小查询时CPU使用率无法打满的问题展开。经排查,高并发下频繁的线程创建和释放会引发排队阻塞,导致计算工作受阻。通过实验复现问题,并深入分析线程创建释放过程、锁机制、系统调用代价等,最后给出多线程开发的经验总结。
最低0.47元/天 解锁文章
847

被折叠的 条评论
为什么被折叠?



