项目中协程加入的原因和过程分享

嚴華  金牌会员 | 2024-5-10 16:30:35 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 921|帖子 921|积分 2763

原文已经发到项目wiki页面:https://github.com/youngyangyang04/KVstorageBaseRaft-cpp/wiki/协程加入的原因和过程分享
欢迎大家给项目来个star哈哈哈。

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级别的。
这里主要考虑的就是:

  • leaderHearBeatTicker()、electionTimeOutTicker()可以加入协程管理,而applierTicker()不要加入协程管理,不加入的原因主要考虑如下:

    • applierTicker()的作用的向上提交代码,而这个提交的时候可能会随着数据量的增大而发生改变,因此会带来一些隐藏的风险,导致其他任务收到影响,而且可能会极难debug。

  • 考察函数执行时间,简单的代码运行时间分析之后发现leaderHearBeatTicker()、electionTimeOutTicker()函数不考虑sleep的时间基本是不到1ms,发现时间很短,基本上不会相互阻塞。
意料之外的情况及其修复过程

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

1.一直没有AE的日志
2.一直都是两个节点的HOOK日志(三节点启动的raft)
红色部分表示hook成功,将usleep改位定时器任务。
发生选举再集合上面的日志表现,那么肯定是leader发送不成功。
原来使用线程的时候没问题,使用协程管理就出现了问题,然后随着猜测和其他尝试(比如只用协程管理一个任务又没问题),问题点逐渐明确了起来,那么肯定是两个任务相互干扰了,导致leader延迟唤醒了或者一直没有唤醒,为了打出对应的随眠与唤醒日志,采用了atomic变量来找对应,
  1. 20 leaderHearBeatTicker();函数设置睡眠时间为: 24 毫秒
  2. HOOK USLEEP REAL START
  3. [2024-2-22-20-39-29] [func-Raft::sendAppendEntries-raft{1}] leader 向节点{2}发送AE rpc開始 , args->entries_size():{0}
  4. [2024-2-22-20-39-29] [func-Raft::sendAppendEntries-raft{1}] leader 向节点{0}发送AE rpc成功
  5. [2024-2-22-20-39-29] ---------------------------tmp------------------------- 節點{0}返回true,當前*appendNums{2}
  6. [2024-2-22-20-39-29] [func-Raft::sendAppendEntries-raft{1}] leader 向节点{2}发送AE rpc成功
  7. [2024-2-22-20-39-29] ---------------------------tmp------------------------- 節點{2}返回true,當前*appendNums{1}
  8. electionTimeOutTicker();函数设置睡眠时间为: 5 毫秒
  9. electionTimeOutTicker();函数实际睡眠时间为: 4.63221 毫秒
  10. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  11. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  12. HOOK USLEEP REAL START
  13. HOOK USLEEP REAL START
  14. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  15. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  16. HOOK USLEEP REAL START
  17. HOOK USLEEP REAL START
  18. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  19. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  20. HOOK USLEEP REAL START
  21. HOOK USLEEP REAL START
  22. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  23. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  24. HOOK USLEEP REAL START
  25. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  26. HOOK USLEEP REAL START
  27. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  28. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  29. HOOK USLEEP REAL START
  30. HOOK USLEEP REAL START
  31. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  32. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  33. HOOK USLEEP REAL START
  34. HOOK USLEEP REAL START
  35. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  36. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  37. HOOK USLEEP REAL START
  38. HOOK USLEEP REAL START
  39. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  40. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  41. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  42. HOOK USLEEP REAL START
  43. HOOK USLEEP REAL START
  44. [2024-2-22-20-39-29] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  45. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  46. HOOK USLEEP REAL START
  47. HOOK USLEEP REAL START
  48. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  49. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  50. HOOK USLEEP REAL START
  51. HOOK USLEEP REAL START
  52. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  53. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  54. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  55. electionTimeOutTicker();函数设置睡眠时间为: 320 毫秒
  56. electionTimeOutTicker();函数实际睡眠时间为: 321.094 毫秒
  57. HOOK USLEEP REAL START
  58. HOOK USLEEP REAL START
  59. HOOK USLEEP REAL START
  60. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  61. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  62. HOOK USLEEP REAL START
  63. HOOK USLEEP REAL START
  64. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  65. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  66. electionTimeOutTicker();函数设置睡眠时间为: 445 毫秒
  67. electionTimeOutTicker();函数实际睡眠时间为: 445.421 毫秒
  68. HOOK USLEEP REAL START
  69. HOOK USLEEP REAL START
  70. HOOK USLEEP REAL START
  71. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  72. [2024-2-22-20-39-30] [Raft::applierTicker() - raft{1}]  m_lastApplied{0}   m_commitIndex{0}
  73. electionTimeOutTicker();函数设置睡眠时间为: 72 毫秒
  74. electionTimeOutTicker();函数实际睡眠时间为: 73.5567 毫秒
  75. [2024-2-22-20-39-30] [       ticker-func-rf(2)              ]  选举定时器到期且不是leader,开始选举
  76. [2024-2-22-20-39-30] [func-sendRequestVote rf{2}] 向server{2} 發送 RequestVote 開始
  77. [2024-2-22-20-39-30] [func-sendRequestVote rf{2}] 向server{2} 發送 RequestVote 開始
  78. HOOK USLEEP REAL START
  79. [2024-2-22-20-39-30] [func-sendRequestVote rf{2}] 向server{2} 發送 RequestVote 完畢,耗時:{0} ms
  80. [2024-2-22-20-39-30] [func-sendRequestVote rf{2}] elect success  ,current term:{2} ,lastLogIndex:{0}
  81. [2024-2-22-20-39-30] [func-Raft::doHeartBeat()-Leader: {2}] Leader的心跳定时器触发了且拿到mutex,开始发送AE
  82. [2024-2-22-20-39-30] [func-Raft::doHeartBeat()-Leader: {2}] Leader的心跳定时器触发了 index:{0}
  83. [2024-2-22-20-39-30] [func-Raft::doHeartBeat()-Leader: {2}] Leader的心跳定时器触发了 index:{1}
  84. [2024-2-22-20-39-30] [       ticker-func-rf(1)              ]  选举定时器到期且不是leader,开始选举
  85. [2024-2-22-20-39-30] [func-sendRequestVote rf{2}] 向server{2} 發送 RequestVote 完畢,耗時:{0} ms
  86. [2024-2-22-20-39-30] [func-sendRequestVote rf{1}] 向server{3} 發送 RequestVote 開始
  87. HOOK USLEEP REAL START
  88. [2024-2-22-20-39-30] [func-Raft::sendAppendEntries-raft{2}] leader 向节点{0}发送AE rpc開始 , args->entries_size():{0}
  89. [2024-2-22-20-39-30] [func-sendRequestVote rf{1}] 向server{3} 發送 RequestVote 開始
  90. 20 leaderHearBeatTicker();函数实际睡眠时间为: 347.828 毫秒
复制代码
20 leaderHearBeatTicker();函数实际睡眠时间为: 347.828 毫秒中20是atomic的值,发现睡眠过久,排查一下。而且发现发起不同选举的时候leader的异常(过长)时间前面的atomic都很有规律。
查看electionTimeOutTicker()代码:
  1. void Raft::electionTimeOutTicker() {
  2.   // Check if a Leader election should be started.
  3.   while (true) {
  4.     std::chrono::duration<signed long int, std::ratio<1, 1000000000>> suitableSleepTime{};
  5.     std::chrono::system_clock::time_point wakeTime{};
  6.     {
  7.       m_mtx.lock();
  8.       wakeTime = now();
  9.       suitableSleepTime = getRandomizedElectionTimeout() + m_lastResetElectionTime - wakeTime;
  10.       m_mtx.unlock();
  11.     }
  12.     if (std::chrono::duration<double, std::milli>(suitableSleepTime).count() > 1) {
  13.       usleep(std::chrono::duration_cast<std::chrono::microseconds>(suitableSleepTime).count());
  14.     }
  15.           if (std::chrono::duration<double, std::milli>(m_lastResetElectionTime - wakeTime).count() > 0) {
  16.       //说明睡眠的这段时间有重置定时器,那么就没有超时,再次睡眠
  17.       continue;
  18.     }
  19.     doElection();
  20.   }
  21. }
复制代码
逻辑很简单,就是根据选举时间来睡眠,醒来之后判断这段时间选举超时定时器标志m_lastResetElectionTime是否被触发,触发就不发起选举,反之没触发就发起选举。
在这里问题就浮出水面了,对于leader来说,其m_lastResetElectionTime一直不会触发, electionTimeOutTicker会一直循环(当然 doElection();写了判断,leader不会发起选举),对于协程来说,压根没有切换时机呀。
解决方法就是判断如果是leader的话就睡眠一会,leader也不需要发起选举。
当然我的解决方法也是比较粗暴的,欢迎提出更优解。
修改后的electionTimeOutTicker() 如下:
  1. void Raft::electionTimeOutTicker() {
  2.   // Check if a Leader election should be started.
  3.   while (true) {
  4.     /**
  5.      * 如果不睡眠,那么对于leader,这个函数会一直空转,浪费cpu。且加入协程之后,空转会导致其他协程无法运行,对于时间敏感的AE,会导致心跳无法正常发送导致异常
  6.      */
  7.     while (m_status == Leader) {
  8.       usleep(
  9.           HeartBeatTimeout);  //定时时间没有严谨设置,因为HeartBeatTimeout比选举超时一般小一个数量级,因此就设置为HeartBeatTimeout了
  10.     }
  11.     std::chrono::duration<signed long int, std::ratio<1, 1000000000>> suitableSleepTime{};
  12.     std::chrono::system_clock::time_point wakeTime{};
  13.     {
  14.       m_mtx.lock();
  15.       wakeTime = now();
  16.       suitableSleepTime = getRandomizedElectionTimeout() + m_lastResetElectionTime - wakeTime;
  17.       m_mtx.unlock();
  18.     }
  19.     if (std::chrono::duration<double, std::milli>(suitableSleepTime).count() > 1) {
  20.       usleep(std::chrono::duration_cast<std::chrono::microseconds>(suitableSleepTime).count());
  21.     }
  22.           if (std::chrono::duration<double, std::milli>(m_lastResetElectionTime - wakeTime).count() > 0) {
  23.       //说明睡眠的这段时间有重置定时器,那么就没有超时,再次睡眠
  24.       continue;
  25.     }
  26.     doElection();
  27.   }
  28. }
复制代码
协程使用注意以及其他收获

协程使用

协程库里面的定时器无法保证定时精度;虽然看起来是异步,但是实际还是同步,因此使用时还是要注意防止阻塞。
无法保证定时精度这点在学习协程的时候就感觉到了,但是真的踩坑了才理会到真意。
关于异步和同步,这里让我想到了线程池中的快慢线程分离的操作,本质上是防止慢命令线程阻塞快命令线程从而影响QPS。
其他收获

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

嚴華

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表