前言

线程池和工作队列其实是密不可分的,从ceph的代码中也可以看出来。让任务推入工作队列,而线程池中的线程负责从工作队列中取出任务进行处理。这种处理任务的模式任何一个有C编程经验的人都不会陌生。当年我在ZTE和Trend工作的时候,都写过或者维护过类似的代码。

工作队列和线程池的关系,类似于狡兔和走狗的关系,正是因为有任务,所以才需要雇佣线程来完成任务,没有了狡兔,走狗也就失去了存在的意义。而线程必须要可以从工作队列中认领任务并完成,这就类似于猎狗要有追捕狡兔的功能。正因为两个数据结构拥有如此紧密的关系,因此,Ceph中他们的相关函数都位于WorkQueue.cc和WorkQueue.h中。

线程池线程个数调整

线程池的关键在于线程的主函数做的事情。首先是工作线程.

线程池中会有很多的WorkThread,它的基类就是Thread。线程的主函数为pool->worker,即ThreadPool::worker函数。

  struct WorkThread : public Thread {
    ThreadPool *pool;
    // cppcheck-suppress noExplicitConstructor
    WorkThread(ThreadPool *p) : pool(p) {}
    void *entry() {
      pool->worker(this);
      return 0;
    }
  };

    void ThreadPool::worker(WorkThread *wt)
    {
    }

线程池和heartbeat是交织在一起的,这里略过。 线程池是支持动态调整线程个数的。所谓调整,有两种可能性,一种是线程个数增加,一种线程个数减少。我们知道,当添加OSD的时候,数据会重分布,恢复的速度可以调节,其中一个重要的参数为osd-recovery-threads,默认值为1,该值修改可以实时生效。

ceph tell osd.* injectargs '--osd-recovery-threads 8'

该值之所以可以实时生效,说到底,不过是因为OSD类中的recovery_tp就是一种普通的ThreadPool而已

OSD.h
class OSD {
private:

  ThreadPool osd_tp;
  ShardedThreadPool osd_op_tp;
  ThreadPool recovery_tp;
  ThreadPool disk_tp;
  ThreadPool command_tp;
    }

线程个数减少

线程个数减少相对比较简单,线程自杀即可

void ThreadPool::worker(WorkThread *wt)
{  
  _lock.Lock();
  ldout(cct,10) << "worker start" << dendl;

  std::stringstream ss;
  ss << name << " thread " << (void*)pthread_self();
  heartbeat_handle_d *hb = cct->get_heartbeat_map()->add_worker(ss.str());

  while (!_stop) {

    // manage dynamic thread pool
    join_old_threads(); //这里会判断是否存在自杀的线程,如果存在的话,执行join,并清除(自杀)。
    if (_threads.size() > _num_threads) { //如果超出线程最大数,自杀,即把线程放到_old_threads list中,跳出循环;
      ldout(cct,1) << " worker shutting down; too many threads (" << _threads.size() << " > " << _num_threads << ")" << dendl;
      _threads.erase(wt);
      _old_threads.push_back(wt);
      break;
    }

线程本身是一个loop,不停地处理WorkQueue中的任务,在一个loop的开头,线程个数是否超出了配置的个数,如果超出了,就需要自杀,所谓自杀即将自身推送到_old_threads中,然后跳出loop,直接返回了。线程池中的其他兄弟在busy-loop开头的join_old_threads函数会判断是否存在自杀的兄弟,如果存在的话,执行join,为兄弟收尸。

void ThreadPool::join_old_threads()
{
  assert(_lock.is_locked());
  while (!_old_threads.empty()) {
    ldout(cct, 10) << "join_old_threads joining and deleting " << _old_threads.front() << dendl;
    _old_threads.front()->join(); // thread 的join方法,是等待该线程执行完,才能继续往下执行。
    delete _old_threads.front();
    _old_threads.pop_front();
  }
}

线程个数增加

线程池的线程个数如果不够用,也可以动态的增加,通过配置的变化来做到:

md_config_t::injectargs()--->md_config_t::_apply_changes()--->ThreadPool::handle_conf_change

void ThreadPool::handle_conf_change(const struct md_config_t *conf,
                    const std::set  &changed)
{
  if (changed.count(_thread_num_option)) {
    char *buf;
    int r = conf->get_val(_thread_num_option.c_str(), &buf, -1);// 拿到动态传过来的线程数
    assert(r >= 0);
    int v = atoi(buf);
    free(buf);
    if (v > 0) {
      _lock.Lock();
      _num_threads = v; //_num_threads 此时是最新的线程数
      start_threads();
      _cond.SignalAll();
      _lock.Unlock();
    }
  }
}

void ThreadPool::start_threads()
{
  assert(_lock.is_locked());
  while (_threads.size() < _num_threads) {
    WorkThread *wt = new WorkThread(this);
    ldout(cct, 10) << "start_threads creating and starting " << wt << dendl;
    _threads.insert(wt);

    int r = wt->set_ioprio(ioprio_class, ioprio_priority);
    if (r < 0)
      lderr(cct) << " set_ioprio got " << cpp_strerror(r) << dendl;

    wt->create(thread_name.c_str());
  }
}

start_threads函数不仅仅可以用在初始化时启动所有工作线程,而且可以用于动态增加,它会根据配置要求的线程数_num_threads和当前线程池中线程的个数,来创建WorkThread,当然了,他会调整线程的io优先级。

工作线程的暂停执行和恢复执行

线程池的工作线程,绝大部分时间内,自然是busy-loop中处理工作队列上的任务,但是有一种场景是,需要让工作暂时停下来,停止工作,不要处理WorkQueue中的任务。

线程池提供了一个标志为_pause,只要_pause不等于0,那么线程池中线程就在loop中就不会处理工作队列中的任务,而是空转。为了能够及时的醒来,也不是sleep,而是通过条件等待,等待执行的时间。

void ThreadPool::pause()
{
  ldout(cct,10) << "pause" << dendl;
  _lock.Lock();
  _pause++;
  while (processing)
    _wait_cond.Wait(_lock);
  _lock.Unlock();
  ldout(cct,15) << "paused" << dendl;
}

void ThreadPool::pause_new()
{
  ldout(cct,10) << "pause_new" << dendl;
  _lock.Lock();
  _pause++;
  _lock.Unlock();
}

void ThreadPool::worker(WorkThread *wt)
{
  while (!_stop) {

    // manage dynamic thread pool
    join_old_threads();
    if (_threads.size() > _num_threads) {
      ldout(cct,1) << " worker shutting down; too many threads (" << _threads.size() << " > " << _num_threads << ")" << dendl;
      _threads.erase(wt);
      _old_threads.push_back(wt);
      break;
    }

    if (!_pause && !work_queues.empty()) {
      ...
            工作线程的工作内容,见下
            ...
    }

    ldout(cct,20) << "worker waiting" << dendl;

    /*重新设置timeout时间,放置误判timeout,自杀超时设置为0,即线程不会自杀*/
    cct->get_heartbeat_map()->reset_timeout(
      hb,
      cct->_conf->threadpool_default_timeout,/*60*/
      0);

    /*此处相当于sleep,但是条件变量的存在,一旦情况有变(比如调用了unpause函数)能及时醒来*/
    _cond.WaitInterval(cct, _lock,
      utime_t(
                cct->_conf->threadpool_empty_queue_max_wait, 0));//2s 默认设置工作队列为空2s超时
                            //default wait time for an empty queue before pinging the hb timeout
  }

  ...
}

那么处理pause_new和pause 函数做的事情差不多,两者有什么区别呢?关键在于

 while (processing)
    _wait_cond.Wait(_lock);
当下达pause指令的时候,很可能线程池中的某几个线程正在处理工作队列中的任务,这种情况下并不是立刻就能停下的,只有处理完手头的任务,在下一轮loop中检查_pause标志位才能真正地停下。那么pause指令就面临选择,要不要等工作线程WorkThread处理完手头的任务。pause函数是等,pauser_new函数并不等,pause_new函数只负责设置标志位,当其返回的时候,某几个线程可能仍然在处理工作队列中的任务。

工作线程的工作内容

讲了这么多,基本都是旁枝,而不是工作线程的主干,主干部分是处理工作队列中的任务:

if (!_pause && !work_queues.empty()) {
      WorkQueue_* wq;
      int tries = work_queues.size();
      bool did = false;
      while (tries--) {
           last_work_queue++;
           last_work_queue %= work_queues.size();
           wq = work_queues[last_work_queue];

           void *item = wq->_void_dequeue();
           if (item) {
               processing++;
               ldout(cct,12) << "worker wq " << wq->name << " start processing " << item
                   << " (" << processing << " active)" << dendl;
               TPHandle tp_handle(cct, hb, wq->timeout_interval, wq->suicide_interval);
               tp_handle.reset_tp_timeout();// //这里会设置超时时间,并check是否超时,如果超时,会输出对应信息;
               _lock.Unlock();
               wq->_void_process(item, tp_handle);// 工作队列中item真正处理的函数
               _lock.Lock();
               wq->_void_process_finish(item);
               processing--;
               ldout(cct,15) << "worker wq " << wq->name << " done processing " << item
                        << " (" << processing << " active)" << dendl;
                     if (_pause || _draining)
                         _wait_cond.Signal();
                     did = true;
                     break;
             }
      }
     if (did)
        continue;
}

比如,写入部分数据,输出日志:
 2020-04-29 04:43:28.878510 7f2413d75700 12 FileStore::op_tp worker wq FileStore::OpWQ start processing 0x7f242ec382c0 (1 active)
2020-04-29 04:43:28.878512 7f2413d75700 20 heartbeat_map reset_timeout 'FileStore::op_tp thread 0x7f2413d75700' grace 60 suicide 180
2020-04-29 04:43:28.878519 7f2413d75700 20 heartbeat_map reset_timeout 'FileStore::op_tp thread 0x7f2413d75700' grace 60 suicide 180
2020-04-29 04:43:28.878608 7f2413d75700 20 heartbeat_map reset_timeout 'FileStore::op_tp thread 0x7f2413d75700' grace 60 suicide 180
2020-04-29 04:43:28.878618 7f2413d75700 20 heartbeat_map reset_timeout 'FileStore::op_tp thread 0x7f2413d75700' grace 60 suicide 180
2020-04-29 04:43:28.878653 7f2413d75700 20 heartbeat_map reset_timeout 'FileStore::op_tp thread 0x7f2413d75700' grace 60 suicide 180
2020-04-29 04:43:28.878725 7f2413d75700 20 heartbeat_map reset_timeout 'FileStore::op_tp thread 0x7f2413d75700' grace 60 suicide 180
2020-04-29 04:43:28.878768 7f2413d75700 15 FileStore::op_tp worker wq FileStore::OpWQ done processing 0x7f242ec382c0 (0 active)

其中_void_process和_void_process_finish是WorkQueue基类中定义的函数,真正定义工作队列的时候,可以继承该基类,定义自己的_process函数和_process_finish 函数,来雇用线程完成特定的任务。

template
  class WorkQueue : public WorkQueue_ {
    ThreadPool *pool;

    /// Add a work item to the queue.
    virtual bool _enqueue(T *) = 0;
    /// Dequeue a previously submitted work item.
    virtual void _dequeue(T *) = 0;
    /// Dequeue a work item and return the original submitted pointer.
    virtual T *_dequeue() = 0;
    virtual void _process_finish(T *) {}

    // implementation of virtual methods from WorkQueue_
    void *_void_dequeue() {
      return (void *)_dequeue();
    }
    void _void_process(void *p, TPHandle &handle) {
      _process(static_cast(p), handle);
    }
    void _void_process_finish(void *p) {
      _process_finish(static_cast(p));
    }

  protected:
    /// Process a work item. Called from the worker threads.
    virtual void _process(T *t, TPHandle &) = 0;

  public:
    WorkQueue(string n, time_t ti, time_t sti, ThreadPool* p) : WorkQueue_(n, ti, sti), pool(p) {
      pool->add_work_queue(this);
    }
    ~WorkQueue() {
      pool->remove_work_queue(this);
    }

我们不妨以FileStore的op_tp和op_wq为例查看下该线程池中工作线程的日常任务。

ThreadPool op_tp;
  struct OpWQ : public ThreadPool::WorkQueue {
    FileStore *store;
    OpWQ(FileStore *fs, time_t timeout, time_t suicide_timeout, ThreadPool *tp)
      : ThreadPool::WorkQueue("FileStore::OpWQ", timeout, suicide_timeout, tp), store(fs) {}

    bool _enqueue(OpSequencer *osr) {
      store->op_queue.push_back(osr);
      return true;
    }
    void _dequeue(OpSequencer *o) {
      assert(0);
    }
    bool _empty() {
      return store->op_queue.empty();
    }
    OpSequencer *_dequeue() {
      if (store->op_queue.empty())
           return NULL;
      OpSequencer *osr = store->op_queue.front();
      store->op_queue.pop_front();
      return osr;
    }
    void _process(OpSequencer *osr, ThreadPool::TPHandle &handle) override {
      store->_do_op(osr, handle);
    }
    void _process_finish(OpSequencer *osr) {
      store->_finish_op(osr);
    }
    void _clear() {
      assert(store->op_queue.empty());
    }
  } op_wq;

工作队列和线程池建立合作关系

好像至今也没有介绍如何建立起合作关系,还是以FileStore的op_tp和op_wq为例:

 op_tp(g_ceph_context, "FileStore::op_tp", "tp_fstore_op", g_conf->filestore_op_threads/*2*/, "filestore_op_threads"),

  op_wq(this, g_conf->filestore_op_thread_timeout/*60*/,
    g_conf->filestore_op_thread_suicide_timeout/*180*/, &op_tp),

FileStore实例化的时候,op_tp作为参数传递给了op_wq的构造函数。

我们不妨看看WorkQueue的构造函数和析构函数:

public:
    WorkQueue(string n, time_t ti, time_t sti, ThreadPool* p) : WorkQueue_(n, ti, sti), pool(p) {
      pool->add_work_queue(this);//分配一个wq到这个线程池,建立联系
    }
    ~WorkQueue() {
      pool->remove_work_queue(this);
    }

而ThreadPool中的add_work_queue和remove_work_queue就是用来建立和移除与WorkQueue关联的函数

 /// assign a work queue to this thread pool
  void add_work_queue(WorkQueue_* wq) {
    Mutex::Locker l(_lock);
    work_queues.push_back(wq);
  }
  /// remove a work queue from this thread pool
  void remove_work_queue(WorkQueue_* wq) {
    Mutex::Locker l(_lock);
    unsigned i = 0;
    while (work_queues[i] != wq)
      i++;
    for (i++; i < work_queues.size(); i++) 
      work_queues[i-1] = work_queues[i];
    assert(i == work_queues.size());
    work_queues.resize(i-1);
  }

因此建立狼狈为奸的关系,需要先创建线程池,然后创建WorkQueue的时候,将线程池作为参数传递给WorkQueue,就能建立关系。

线程超时

部分超时参数

OPTION(osd_op_thread_timeout, OPT_INT, 15)  线程超时时间
OPTION(osd_op_thread_suicide_timeout, OPT_INT, 150) 线程自杀时间
OPTION(filestore_op_thread_timeout, OPT_INT, 60)
OPTION(filestore_op_thread_suicide_timeout, OPT_INT, 180)
OPTION(osd_recovery_thread_timeout, OPT_INT, 30)
OPTION(osd_recovery_thread_suicide_timeout, OPT_INT, 300)
OPTION(threadpool_default_timeout, OPT_INT, 60)

osd超时导致down过程分析
假设故障osd为osd-A,osd-A处理心跳时(OSD::handle_osd_ping),会检查工作线程(OSD:osd_op_tp)状态,如果由工作线程处理任务时间超时(默认超时时间osd_op_thread_timeout 为15s),就不处理心跳消息,其它osd收不到osd-A的心跳,会向mon上报osd-A down掉,满足条件osd down 条件,mon会把osd-A置为down,但osd-A还活着,超时的osd_op处理完后会重新up,除非超过了osd_op_thread_suicide_timeout时间,默认150s,这个osd-A进程会自杀,最终真正down掉。