前言
之前使用Gearman的时候,遇到过一个卡顿的问题。今天微博上又有人问我是否遇到过此类问题。这个问题,当时是伯诚老师解决的。我把他的文章搬过来。希望能给遇到此类问题的人一点参考。
问题
使用Gearman作为异步消息处理中间件是却没有想象中的顺利。我们多次发现Gearmand进程会将PHP的请求Hold住,不做任何响应,即便PHP在GearmanClient发起连接时设置了连接超时时间,也不会超时。这对于php的工作方式来说,是很危险的。
对于这个问题,我们问了Google许多次,自己也在服务器上跟踪了许久,终于将问题定位为在Gearman的worker进程在通过 addFunction 方法注册任务时,如果使用了timeout参数,那么就会复现这个问题,但这个问题的复现是随机发生的。
首先,Gearmand进程正常工作时,通过pstack查看其工作线程状态如下:
Thread 7 (Thread 0x40bc2940 (LWP 8627)):
#0 0x00007fec366d1e46 in poll () from /lib64/libc.so.6
#1 0x00007fec386be7a7 in current_epoch_handler () from /proc/8626/exe
#2 0x00007fec37dc54a7 in start_thread () from /lib64/libpthread.so.0
#3 0x00007fec366dac2d in clone () from /lib64/libc.so.6
Thread 6 (Thread 0x415c3940 (LWP 8628)):
#0 0x00007fec366db018 in epoll_wait () from /lib64/libc.so.6
#1 0x00007fec37767ff0 in epoll_dispatch ()
#2 0x00007fec3775b3e1 in event_base_loop ()
#3 0x00007fec386b7848 in _thread () from /proc/8626/exe
#4 0x00007fec37dc54a7 in start_thread () from /lib64/libpthread.so.0
#5 0x00007fec366dac2d in clone () from /lib64/libc.so.6
Thread 5 (Thread 0x41fc4940 (LWP 8629)):
#0 0x00007fec37dc9b99 in pthread_cond_wait@@GLIBC_2.3.2 ()
#1 0x00007fec386b6269 in _proc () from /proc/8626/exe
#2 0x00007fec37dc54a7 in start_thread () from /lib64/libpthread.so.0
#3 0x00007fec366dac2d in clone () from /lib64/libc.so.6
Thread 4 (Thread 0x429c5940 (LWP 8630)):
#0 0x00007fec366db018 in epoll_wait () from /lib64/libc.so.6
#1 0x00007fec37767ff0 in epoll_dispatch ()
#2 0x00007fec3775b3e1 in event_base_loop ()
#3 0x00007fec386b7848 in _thread () from /proc/8626/exe
#4 0x00007fec37dc54a7 in start_thread () from /lib64/libpthread.so.0
#5 0x00007fec366dac2d in clone () from /lib64/libc.so.6
Thread 3 (Thread 0x433c6940 (LWP 8631)):
#0 0x00007fec366db018 in epoll_wait () from /lib64/libc.so.6
#1 0x00007fec37767ff0 in epoll_dispatch ()
#2 0x00007fec3775b3e1 in event_base_loop ()
#3 0x00007fec386b7848 in _thread () from /proc/8626/exe
#4 0x00007fec37dc54a7 in start_thread () from /lib64/libpthread.so.0
#5 0x00007fec366dac2d in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x43dc7940 (LWP 8632)):
#0 0x00007fec366db018 in epoll_wait () from /lib64/libc.so.6
#1 0x00007fec37767ff0 in epoll_dispatch ()
#2 0x00007fec3775b3e1 in event_base_loop ()
#3 0x00007fec386b7848 in _thread () from /proc/8626/exe
#4 0x00007fec37dc54a7 in start_thread () from /lib64/libpthread.so.0
#5 0x00007fec366dac2d in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7fec3863e6f0 (LWP 8626)):
#0 0x00007fec366db018 in epoll_wait () from /lib64/libc.so.6
#1 0x00007fec37767ff0 in epoll_dispatch ()
#2 0x00007fec3775b3e1 in event_base_loop ()
#3 0x00007fec386b4d5d in gearmand_run () from /proc/8626/exe
#4 0x00007fec386a3307 in main () from /proc/8626/exe
可以看到,正常工作时,Gearmand进程中会有7个工作线程。而当工作不正常时,也就是PHP请求被Hold住时,可以看到这里面的线程数是少于7个的。
这个问题在起官方网站的issue中也有人提起。
原因与解决方法
使用的Gearmand的版本是1.1.8依然有这个问题。追溯到0.32版本发现无法重现这个问题,而0.33至1.1.8都存在此问题。后来阅读了源码以及查看了相关Changelog发现0.33之后的版本增加了对worker的timeout处理。而从上面官网的一些用户的反馈来看,也确实和addFunction的timeout参数有关。于是我们先将addFunction时的timeout参数设置为0。问题果然不在重现,那么这个timeout到底为什么会导致这个问题呢?这需要进一步分析Gearmand的源码,分析的重点如下:
首先是线程数为什么会减少?
libgearman-server/gearmand_thread.cc: _thread方法中
static void *_thread(void *data)
{
gearmand_thread_st *thread= (gearmand_thread_st *)data;
char buffer[BUFSIZ];
int length= snprintf(buffer, sizeof(buffer), "[%6u ]", thread->count);
if (length <= 0 or sizeof(length) >= sizeof(buffer))
{
assert(0);
buffer[0]= 0;
}
(void)gearmand_initialize_thread_logging(buffer);
gearmand_debug("Entering thread event loop");
ex s;
int r ;
if ( (r = event_base_loop(thread->base, 0)) == -1)
{
gearmand_fatal("event_base_loop(-1)");
cout<<"event_base_loop(-1)"<<endl;
Gearmand()->ret= GEARMAND_EVENT;
}
cout<<"r="<<r<<endl;
cout<<"Exiting thread event loop"<<endl;
gearmand_debug("Exiting thread event loop");
return NULL;
}
在问题发生的时候,会导致event_base_loop执行完毕,导致退出。查看了libevent的相关资料,可以看到。libevent 1.4.x是非线程安全的,不能跨线程执行event_add。而libevent 2.0.x通过线程锁做到了线程安全,可以通过执行evthread_use_pthreads跨线程执行event_add。而Gearmand的代码中没有使用evthread_use_pthreads。而Gearmand的官方代码中,应该就是对于timeout特性进行处理时发生的问题。我们可以看到如下源码中
libgearman-server/connection.cc:gearman_server_con_add_job_timeout 方法中
if (con->timeout_event == NULL)
{
gearmand_con_st *dcon= con->con.context;
con->timeout_event= (struct event *)malloc(sizeof(struct event)); // libevent POD
if (con->timeout_event == NULL)
{
return gearmand_merror("malloc(sizeof(struct event)", struct event, 1);
}
timeout_set(con->timeout_event, _server_job_timeout, job);
event_base_set(dcon->thread->base, con->timeout_event);
}
gearman_server_con_add_job_timeout 这个方法,是在main thread中执行的,而下面这条命令
event_base_set(dcon->thread->base, con->timeout_event);操作的是dcon->thread 对象上的event base对象,因此属于跨线程操作了event_base对象。
我们随后对gearman_server_con_add_job_timeout这个方法中的event_base_set调用进行了修改。将dcon->thread->base对象改到了_global_gearmand->base。