首页 » 开源分析 » Gearmand异步处理就安全了吗?不!

Gearmand异步处理就安全了吗?不!

 

前言

之前使用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。

技术交流

原文链接:Gearmand异步处理就安全了吗?不!,转载请注明来源!

原文链接:Gearmand异步处理就安全了吗?不!,转载请注明来源!

8