C/C++协程库libco:微信怎样漂亮地完成异步化改造


截至2020年第一季度,微信及WeChat的合并月活跃帐户数达12.025亿。

不可否认,当今的微信后台拥有着强大的并发能力。

不过,正如罗马非一日建成。微信的技术也曾经略显稚嫩。

微信诞生于2011年1月,当年的用户规模为0.1亿左右;到了2013年11月,微信月活跃用户数达到了3.55亿,一跃成为了亚洲地区拥有最大用户群体的移动终端即时通讯软件。

面对如此体量的提升,微信后台也曾遭遇棘手的窘境;令人赞叹的是技术人员及时做出了漂亮的应对。

这背后有着怎样的技术故事?

此时此刻,你在微信手机端发出的请求,是怎样在后台消化和处理的?

这次,我们聚焦在2013年作为腾讯六大开源项目之一的 libco 协程库上。现在,libco 是微信后台大规模使用的 c/c++ 协程库,它支持后台敏捷的同步风格编程模式,同步提高系统的高并发能力。

至今,微信后台绝大部分服务都已是多进程或多线程协程模型,并发能力相比之前有了质的提升,而libco也成为了微信后台框架的基石。


微信后端遇到了问题

早期微信后台因为业务需求复杂多变、产品要求快速迭代等需求,大部分模块都采用半同步半异步模型。接入层为异步模型,业务逻辑层则是同步的多进程或多线程模型,业务逻辑的并发能力只有几十到几百

随着微信业务的增长,直到2013年中,微信后台机器规模已经达到了1万多台,涉及数百个后台模块,RPC调用每分钟数亿次。在如此庞大复杂的系统模块下,每个模块很容易收到后端服务或者网络抖动的影响。因此我们急需对微信后台进行异步化的改造。


异步化改造方案的考量

当时他们面临着两种选择:

  • A 线程异步化:把所有服务改造成异步模型,等同于从框架到业务逻辑代码的彻底改造
  • B 协程异步化:对业务逻辑非侵入的异步化改造,即只修改少量框架代码

两者相比,工作量和风险系数的差异显而易见。虽然A方案服务器端多线程异步处理是常见做法,对提高并发能力这个原始目标非常奏效;但是对于微信后台如此复杂的系统,这过于耗时耗力且风险巨大。

无论是异步模型还是同步模型,都需要保存异步状态。所以两者在技术细节的相同点是,两个方案,都需要维护当前请求的状态。在A异步方案中,当请求需要被异步执行时,需要主动把请求相关数据保存起来,再等待状态机的下一次调度执行;而在B协程模型方案中,异步状态的保存和恢复是自动的,协程恢复执行的时候就是上一次退出时的上下文。

因此,B协程方案不需要显式维护异步状态:一方面在编程上可以更简单和直接;另一个方面只需要保存少量的寄存器。因此在复杂系统上,协程服务的性能可能比纯异步模型更优。

综合上述考虑,他们选择了B方案,通过协程的方式对微信后台上百个模块尽心了异步化改造。

但是使用协程会面临以下挑战:

  1. 业界协程在 c/c++ 环境下没有大规模应用的经验
  2. 如何控制协程调度
  3. 如何处理同步风格的 API 调用,如Socket、mysqlclient等
  4. 如何处理已有的全局变量、线程私有变量的使用

协程支持的特性

  • 无需侵入业务逻辑,把多进程、多线程服务改造成协程服务,并发能力得到百倍提升
  • 支持CGI框架,轻松构建web服务(New)
  • 支持 gethostbyname、mysqlclient、ssl等常用第三库(New)
  • 可选的共享栈模式,单机轻松接入千万连接(New)
  • 完善简洁的协程编程接口
    1.类 pthread 接口设计,通过 co_create、co_resume等简单清晰接口即可完成协程的创建和恢复
    2.类 __thread 的协程私有变量、协程间通信的协程信号量 so_signal (New)
    3.非语言级别的 lambda 实现,结合协程原地编写并执行后台异步任务(New)
    4.基于 epoll/kqueue 实现的小而轻的网络框架,基于时间轮盘实现的高性能定时器

libco 框架

libco 框架分为三层,分别是接口层、系统函数 Hook 层以及事件驱动层
在这里插入图片描述


接管历史遗留的同步风格API

方案敲定之后,接下来要做的就是实现异步化的同时尽可能少做代码修改。

通常而言,一个常规的网络后台服务需要connect、write、read等系列步骤,如果使用同步风格的 API 对网络进行调用,整个服务线程会因为等待网络交互而挂起,这就会造成等待并占用资源。原来的这种情况很明显地影响到了系统的并发性能,但是当初这样的选择是因为对应的同步编程风格具有其独特的优势:代码逻辑清晰、易于编写并且支持业务快速迭代敏捷开发

我们的改造方案需要消除同步风格API的缺点,但是同时还希望保持同步编程的优点。

最后不修改线上已有业务逻辑代码的情况下,我们的libco框架创新地接管了网络调用接口(Hook)。把协程的让出与恢复作为异步网络IO中的一次事件注册与回调。当业务处理遇到同步网络请求的时候,libco层会把本次网络请求注册为异步事件,当前的协程让出CPU控制权,CPU交给其它协程执行。在网络事件发生或者超时的时候,libco会自动地恢复协程执行。

大部分同步风格的API我们都通过 Hook 的方法来接管了,libco 会在恰当的时机调度协程恢复执行。


千万级协程支持

libco 默认是每一个协程独享一个运行栈,在协程创建的时候,从堆内存分配一个固定大小的内存作为该协程的运行栈。如果我们用一个协程处理前端的一个接入连接,那对于一个海量接入服务来说,我们的服务的并发上限就很容易受限于内存。为此,libco 也提供了 stackless 的协程共享栈模式,可以设置若干个协程共享同一个运行栈。同一个共享栈下的协程间切换的时候,需要把当前的运行栈内容拷贝到协程的私有内存中。为了减少这种内存拷贝次数,共享栈的内存拷贝只发生在不同协程间的切换。当共享栈的占用者一直没有改变的时候,则不需要拷贝运行栈。
在这里插入图片描述
libco 协程的共享协程模式使得单机很容易接入千万连接,只需创建足够多的协程即可。我们通过 libco 共享栈模式创建1千万的协程(E5-2670 v3 @ 2.30GHz * 2, 128G内存),每10万个协程共享使用128k内存,整个稳定 echo 服务的时候总内存消耗大概为 66G。


协程私有变量

多进程程序改造为多线程程序时候,我们可以用__thread 来对全局变量进行快速修改,而在协程环境下,我们创造了协程变量 ROUTINE_VAR,极大简化了协程的改造工作量。

因为协程实质上是线程内串行执行的,所以当我们定义了一个线程私有变量的时候,可能会有重入的问题。比如我们定义了一个 __thread 的线程私有变量,原本希望每一个执行逻辑独享这个变量的。但当我们的执行环境迁移到了协程之后,同一个线程私有变量,可能会有多个协程会操作它,这就导致了变量冲入的问题。为此,我们在做 libco 异步化改造的时候,把大部分的线程私有变量改成了协程级私有变量。协程私有变量具有这样的特性:当代码运行在多线程非协程环境下时,该变量是线程私有的;当代码运行在协程环境的时候,此变量是协程私有的。底层的协程私有变量会自动完成运行环境的判断并正确返回所需的值。

协程私有变量对于现有环境同步到异步化改造起了举足轻重的作用,同时我们定义了一个非常简单方便的方法定义协程私有变量,简单到只需一行声明代码即可。


gethostbyname 的 Hook 方法

对于现网服务,有可能需要通过系统的 gethostbyname API 接口去查询 DNS 获取真实地址。我们在协程化改造的时候,发现我们 hook 的 socket 族函数对 gethostbyname 不适用,当一个协程调用了 getbyhostname 时会同步等待结果,这就导致了同线程内的其它协程被延时执行。我们对 glibc 的 gethostbyname 源码进行了研究,发现 hook 不生效主要是由于 glibc 内部定义了 _poll 方法来等待事件,而不是通用的 poll 方法;同时 glibc 还定义了一个线程私有变量,不同协程的切换可能会重入导致数据不准确。最终 gethostbyname 协程异步化是通过 Hook __poll 方法以及定义协程私有变量解决的。

getbyhostname 是 glibc 提供的同步查询 DNS 接口,业界还有很多优秀的 gethostbyname 的异步化解决方案,但是这些实现都需要引入一个第三方库并且要求底层提供异步回调通知机制。libco 通过 hook 方法,在不修改 glibc 源码的前提下实现了 gethostbyname 的异步化。


协程信号量

在多线程环境下,我们会有线程间同步的需求,比如一个线程的执行需要等待另一个线程的信号,对于这种需求,我们通常是使用 pthread_signal 来解决的。在 libco 中,我们定义了协程信号量 co_signal 用于处理协程间的并发需求,一个协程可以通过 co_cond_signal 与 co_cond_broadcast 来决定通知一个等待的协程或者唤醒所有等待协程。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值