首页 > 其他分享 >项目中协程加入的原因和过程分享

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

时间:2024-02-27 20:14:56浏览次数:21  
标签:2024 39 20 22 加入 中协程 raft Raft 分享

原文已经发到项目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级别的。
这里主要考虑的就是:

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

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

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

file

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或者说后期维护的时间也算开发时间的呀。

本文由博客一文多发平台 OpenWrite 发布!

标签:2024,39,20,22,加入,中协程,raft,Raft,分享
From: https://www.cnblogs.com/swx123/p/18037780

相关文章

  • facebook, twitter, linkedin等的分享功能
    1.facebook分享方法一:传入参数,此时标题获取的是页面title标签中的内容<!DOCTYPEhtml><htmllang="en"><head><title>Document</title></head><body><ahref="https://www.facebook.com/sharer.php?u=https://www.go......
  • 设备管理器-网络适配器-Remote NDiS-based Internet Sharing Device(基于远程NDIS的互
    RemoteNDiS-basedInternetSharingDevice(基于远程NDIS的互联网共享设备)是一种网络接口遥控分享装置。这种设备允许通过USB连接将智能手机等设备连接到电脑,从而充当无线网卡的作用,使电脑能够连接到互联网。具体功能作用如下:充当无线网卡:当手机连接到互联网后,通过USB绑定,这个......
  • 【干货分享】uniapp做的App如何加固
    摘要本文介绍了在2023年,使用uniapp开发APP的盛行趋势,以及随之而来的加固需求。通过对uniapp制作的APP进行代码混淆、加固资源文件、防止调试和反调试、加密敏感数据以及防止篡改等多个方面的加固措施,来保障APP的安全性。引言在当下,uniapp开发APP的兴起让越来越多的开发者受益于......
  • ipmitool是很常见的物理机管理工具,这里分享一些ipmitool经常用到的一些命令
    ipmitool-Ilanplus-H$oob_ip-Uroot-P密码poweroff(硬关机,直接切断电源)ipmitool-Ilanplus-H$oob_ip-Uroot-P密码powersoft(软关机,即如同轻按一下开机按钮)ipmitool-Ilanplus-H$oob_ip-Uroot-P密码poweron(硬开机)ipmitool-Ilanplus-H$oo......
  • 基于RK3588的NPU案例分享!6T是真的强!
    RK3588 NPU简介作为瑞芯微新一代旗舰工业处理器,RK3588NPU性能可谓十分强大,6TOPS设计能够实现高效的神经网络推理计算。这使得RK3588在图像识别、语音识别、自然语言处理等人工智能领域有着极高的性能表现。 此外,RK3588的NPU还支持多种学习框架,包括TensorFlow、PyTorch、Caf......
  • 三屏异显案例分享,基于全国产RK3568J工业平台!
    在工业领域中,能否更灵活、更高效地在主屏幕进行主要任务,并在其他副屏幕上进行其他次要任务(例如查看参考资料、监控其他应用程序),一直都是许多工业领域客户面临的刚需,而“多屏异显”功能便为此而生。在过去,由于性能、成本、技术等诸多问题,许多工业处理器并不支持多屏异显。但随着工......
  • 【专题】2023年金融、保险、银行行业报告汇总PDF合集分享(附原数据表)
    原文链接:https://tecdat.cn/?p=35149原文出处:拓端数据部落公众号自中国提出双碳目标以来,可持续金融市场呈现出蓬勃发展的态势。这一发展趋势在多年来得到可持续金融战略咨询团队的支持和推动。同时,数字化转型的深入推进推动了新客户的增长,而中国的碳金融创新也成为市场关注的焦......
  • 【机器学习科学库】全md文档笔记:Matplotlib详细使用方法(已分享,附代码)
    本系列文章md笔记(已分享)主要讨论人工智能相关知识。主要内容包括,了解机器学习定义以及应用场景,掌握机器学习基础环境的安装和使用,掌握利用常用的科学计算库对数据进行展示、分析,学会使用jupyternotebook平台完成代码编写运行,应用Matplotlib的基本功能实现图形显示,应用Matplotlib......
  • 在K8S中,worke节点如何加入K8S高可用集群?
    在Kubernetes(K8S)中,将一个Worker节点加入到高可用集群的过程与加入单Master集群大体相似,但需要注意的是,高可用集群中的Master通常是通过负载均衡器暴露服务端点的,这样无论哪个Master节点宕机,Worker节点都可以连接到活跃的Master节点。以下是加入高可用Kubernetes集群的具体步骤:1.......
  • 【机器学习算法】KNN鸢尾花种类预测案例和特征预处理。全md文档笔记(已分享,附代码)
    本系列文章md笔记(已分享)主要讨论机器学习算法相关知识。机器学习算法文章笔记以算法、案例为驱动的学习,伴随浅显易懂的数学知识,让大家掌握机器学习常见算法原理,应用Scikit-learn实现机器学习算法的应用,结合场景解决实际问题。包括K-近邻算法,线性回归,逻辑回归,决策树算法,集成学习,聚......