Skip to content

协程加入的原因和过程分享

思无邪 edited this page Feb 27, 2024 · 1 revision

feat:协程替代doElectionTicker和doHeartBeatTicker线程 by TiNnNnnn · Pull Request #29 · youngyangyang04/KVstorageBaseRaft-cpp 中本仓库完成了加入协程库,因为协程作为一个比较大的特性,所以在这里分享一下加入协程的前世今生,也希望得到大家的指点。

为何加入协程?

一言以蔽之,节约线程数量,减少无效的频繁的sleep。 raft中init中有会启动三个线程,一直循环执行,这三个函数(线程)分别是 leaderHearBeatTicker()electionTimeOutTicker()applierTicker(),这三个函数内部都是维护一个死循环,死循环相当于是一个不断的检查过程,搭配上定时的sleep达到 定时执行某个操作的目的,这三个函数分别的操作是:leader定时给follower节点发送AE、follower定时检查自己是否需要发起选举、向上层的kv存储引擎写入数据。 那么这三个不断循环的定时任务如果想要完成有几种方式呢?

  • 每个任务都启动一个thread,原来的写法。
  • 写一个定时器,添加定时任务,不断的执行即可。

简单分析一下这两种写法的利弊,

  • 每个任务都启动一个thread,原来的写法。
    • 好处:写法美妙,不用考虑其他任务的干扰。
    • 坏处:启动多个线程,而且反复的sleep,资源被浪费了。

而且原来的实现中electionTimeOutTicker()不会判断自己的身份,会狠狠的一直执行,哪怕是一直不断sleep也好。

  • 写一个定时器,添加定时任务,不断的执行即可。
    • 好处:节约资源了。
    • 坏处:写法是同步的写法,不够美妙;需要考虑不同任务之间的干扰,比如一个任务执行时间过长阻塞其他任务等等。

定时器还有一个缺点,因为原来已经采用了thread的写法,那么如果改成采用定时器,肯定就不能sleep了,那么就需要在sleep的时候改成在定时器里面添加定时任务,而且sleep的前后部分就必须额外封装成其他的函数(否则无法成task传给定时器去处理)。

经过分析,可以发现在考虑原有代码的基础上,改写成定时器的工作量还是比较大的,而且改写之后易读性就没那么强了(个人认为原来的代码性能很垃,但是简单,一看就懂)。

那么有没有美妙的一点的方式呢? 对了,协程嘛,我们看下协程加入之后的好处和坏处。 好处:基本不需要改动原有代码,只需要将创建thread改成加入协程调度即可,如:m_ioManager->scheduler([this]() -> void { this->leaderHearBeatTicker(); });,当然由于协程只能hook lib.c中的函数,因此std::this_thread::sleep_for就必须要改成sleep、usleep等lib.c中的函数,这个工作量不太大;节约资源:不仅减少了线程数量(减少线程数量自然减少了线程的上下文切换,减少了系统调用),而且避免了无效sleep带来的系统调用。 坏处:考虑多任务之间的干扰;协程不好写。

在C++标准库中直接hook std::this_thread::sleep_for是不可行的,因为C++标准库的实现通常是由编译器提供的,而且C++标准库的行为是由C++标准规定的 因此只有libc里面的这些函数才能正常的hook

感觉好处远远大于坏处,那么就动手吧。

code过程的分享

协程库的引入

协程库这里就不多介绍了,如果有问题的话可以在GitHub - 578223592/sylar-from-scratch-comment: 用于学习“从零开始重写sylar C++高性能分布式服务器框架“,添加了很多注解和文档方便学习的issue部分和我讨论。

后文中的协程库特指上面链接中的这个库,其他库的特性可能有不同的地方。

在引入的时候如上面所述,我们必须要考虑不同任务之间的影响,尤其是raft中对于定时时间要求是ms级别的。 这里主要考虑的就是:

  1. leaderHearBeatTicker()electionTimeOutTicker()可以加入协程管理,而applierTicker()不要加入协程管理,不加入的原因主要考虑如下:
    1. applierTicker()的作用的向上提交代码,而这个提交的时候可能会随着数据量的增大而发生改变,因此会带来一些隐藏的风险,导致其他任务收到影响,而且可能会极难debug。
  2. 考察函数执行时间,简单的代码运行时间分析之后发现leaderHearBeatTicker()electionTimeOutTicker()函数不考虑sleep的时间基本是不到1ms,发现时间很短,基本上不会相互阻塞。

意料之外的情况及其修复过程

在加入协程库之后发现原来稳定运行的kv(只发生一次选举),现在会不停的发生选举,而且比较规律。表现如下: image

1.一直没有AE的日志 2.一直都是两个节点的HOOK日志(三节点启动的raft)

红色部分表示hook成功,将usleep改位定时器任务。

发生选举再集合上面的日志表现,那么肯定是leader发送不成功。 原来使用线程的时候没问题,使用协程管理就出现了问题,然后随着猜测和其他尝试(比如只用协程管理一个任务又没问题),问题点逐渐明确了起来,那么肯定是两个任务相互干扰了,导致leader延迟唤醒了或者一直没有唤醒,为了打出对应的随眠与唤醒日志,采用了atomic变量来找对应,

20 leaderHearBeatTicker();函数设置睡眠时间为: 24 毫秒
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [func-Raft::sendAppendEntries-raft{1}] leader 向节点{2}发送AE rpc開始 , args->entries_size():{0}
[2024-2-22-20-39-29] [func-Raft::sendAppendEntries-raft{1}] leader 向节点{0}发送AE rpc成功
[2024-2-22-20-39-29] ---------------------------tmp------------------------- 節點{0}返回true,當前*appendNums{2}
[2024-2-22-20-39-29] [func-Raft::sendAppendEntries-raft{1}] leader 向节点{2}发送AE rpc成功
[2024-2-22-20-39-29] ---------------------------tmp------------------------- 節點{2}返回true,當前*appendNums{1}
 electionTimeOutTicker();函数设置睡眠时间为: 5 毫秒
 electionTimeOutTicker();函数实际睡眠时间为: 4.63221 毫秒
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
 electionTimeOutTicker();函数设置睡眠时间为: 320 毫秒
 electionTimeOutTicker();函数实际睡眠时间为: 321.094 毫秒
HOOK USLEEP REAL START
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
 electionTimeOutTicker();函数设置睡眠时间为: 445 毫秒
 electionTimeOutTicker();函数实际睡眠时间为: 445.421 毫秒
HOOK USLEEP REAL START
HOOK USLEEP REAL START
HOOK USLEEP REAL START
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
[2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
 electionTimeOutTicker();函数设置睡眠时间为: 72 毫秒
 electionTimeOutTicker();函数实际睡眠时间为: 73.5567 毫秒
[2024-2-22-20-39-30] [       ticker-func-rf(2)              ]  选举定时器到期且不是leader,开始选举 

[2024-2-22-20-39-30] [func-sendRequestVote rf{2}] 向server{2} 發送 RequestVote 開始
[2024-2-22-20-39-30] [func-sendRequestVote rf{2}] 向server{2} 發送 RequestVote 開始
HOOK USLEEP REAL START
[2024-2-22-20-39-30] [func-sendRequestVote rf{2}] 向server{2} 發送 RequestVote 完畢,耗時:{0} ms
[2024-2-22-20-39-30] [func-sendRequestVote rf{2}] elect success  ,current term:{2} ,lastLogIndex:{0}

[2024-2-22-20-39-30] [func-Raft::doHeartBeat()-Leader: {2}] Leader的心跳定时器触发了且拿到mutex,开始发送AE

[2024-2-22-20-39-30] [func-Raft::doHeartBeat()-Leader: {2}] Leader的心跳定时器触发了 index:{0}

[2024-2-22-20-39-30] [func-Raft::doHeartBeat()-Leader: {2}] Leader的心跳定时器触发了 index:{1}
[2024-2-22-20-39-30] [       ticker-func-rf(1)              ]  选举定时器到期且不是leader,开始选举 


[2024-2-22-20-39-30] [func-sendRequestVote rf{2}] 向server{2} 發送 RequestVote 完畢,耗時:{0} ms
[2024-2-22-20-39-30] [func-sendRequestVote rf{1}] 向server{3} 發送 RequestVote 開始
HOOK USLEEP REAL START
[2024-2-22-20-39-30] [func-Raft::sendAppendEntries-raft{2}] leader 向节点{0}发送AE rpc開始 , args->entries_size():{0}
[2024-2-22-20-39-30] [func-sendRequestVote rf{1}] 向server{3} 發送 RequestVote 開始
20 leaderHearBeatTicker();函数实际睡眠时间为: 347.828 毫秒

20 leaderHearBeatTicker();函数实际睡眠时间为: 347.828 毫秒中20是atomic的值,发现睡眠过久,排查一下。而且发现发起不同选举的时候leader的异常(过长)时间前面的atomic都很有规律。

查看electionTimeOutTicker()代码:

void Raft::electionTimeOutTicker() {
  // Check if a Leader election should be started.
  while (true) {

    std::chrono::duration<signed long int, std::ratio<1, 1000000000>> suitableSleepTime{};
    std::chrono::system_clock::time_point wakeTime{};
    {
      m_mtx.lock();
      wakeTime = now();
      suitableSleepTime = getRandomizedElectionTimeout() + m_lastResetElectionTime - wakeTime;
      m_mtx.unlock();
    }

    if (std::chrono::duration<double, std::milli>(suitableSleepTime).count() > 1) {
      usleep(std::chrono::duration_cast<std::chrono::microseconds>(suitableSleepTime).count());
    }
          if (std::chrono::duration<double, std::milli>(m_lastResetElectionTime - wakeTime).count() > 0) {
      //说明睡眠的这段时间有重置定时器,那么就没有超时,再次睡眠
      continue;
    }
    doElection();
  }
}

逻辑很简单,就是根据选举时间来睡眠,醒来之后判断这段时间选举超时定时器标志m_lastResetElectionTime是否被触发,触发就不发起选举,反之没触发就发起选举。 在这里问题就浮出水面了,对于leader来说,其m_lastResetElectionTime一直不会触发, electionTimeOutTicker会一直循环(当然 doElection();写了判断,leader不会发起选举),对于协程来说,压根没有切换时机呀。 解决方法就是判断如果是leader的话就睡眠一会,leader也不需要发起选举。 当然我的解决方法也是比较粗暴的,欢迎提出更优解。 修改后的electionTimeOutTicker() 如下:

void Raft::electionTimeOutTicker() {
  // Check if a Leader election should be started.
  while (true) {
    /**
     * 如果不睡眠,那么对于leader,这个函数会一直空转,浪费cpu。且加入协程之后,空转会导致其他协程无法运行,对于时间敏感的AE,会导致心跳无法正常发送导致异常
     */
    while (m_status == Leader) {
      usleep(
          HeartBeatTimeout);  //定时时间没有严谨设置,因为HeartBeatTimeout比选举超时一般小一个数量级,因此就设置为HeartBeatTimeout了
    }
    std::chrono::duration<signed long int, std::ratio<1, 1000000000>> suitableSleepTime{};
    std::chrono::system_clock::time_point wakeTime{};
    {
      m_mtx.lock();
      wakeTime = now();
      suitableSleepTime = getRandomizedElectionTimeout() + m_lastResetElectionTime - wakeTime;
      m_mtx.unlock();
    }

    if (std::chrono::duration<double, std::milli>(suitableSleepTime).count() > 1) {
      usleep(std::chrono::duration_cast<std::chrono::microseconds>(suitableSleepTime).count());
    }
          if (std::chrono::duration<double, std::milli>(m_lastResetElectionTime - wakeTime).count() > 0) {
      //说明睡眠的这段时间有重置定时器,那么就没有超时,再次睡眠
      continue;
    }
    doElection();
  }
}

协程使用注意以及其他收获

协程使用

协程库里面的定时器无法保证定时精度;虽然看起来是异步,但是实际还是同步,因此使用时还是要注意防止阻塞。

无法保证定时精度这点在学习协程的时候就感觉到了,但是真的踩坑了才理会到真意。 关于异步和同步,这里让我想到了线程池中的快慢线程分离的操作,本质上是防止慢命令线程阻塞快命令线程从而影响QPS。

其他收获

当前项目的启动方式是多进程的方式,使用主进程启动多个raft节点子节点,这种写法启动起来很美妙,一个命令即可,但是怎么debug呢? 至少用clion来是及其让人吐血,无法debug,只能打日志,也是深刻的感觉到了写代码的时候还是要考虑到debug 的时间,因为debug或者说后期维护的时间也算开发时间的呀。