【腾讯Bugly干货分享】揭秘:微信是如何用libco支撑8亿用户的

本文来自于腾讯bugly开发者社区,未经做者赞成,请勿转载,原文地址:http://dev.qq.com/topic/58203...前端

做者:Leiffymysql

导语

ibco是微信后台大规模使用的c/c++协程库,2013年至今稳定运行在微信后台的数万台机器上。libco在2013年的时候做为腾讯六大开源项目首次开源,咱们最近作了一次较大的更新,同步更新在https://github.com/tencent/libco 上。libco支持后台敏捷的同步风格编程模式,同时提供系统的高并发能力。c++

libco支持的特性

  • 无需侵入业务逻辑,把多进程、多线程服务改形成协程服务,并发能力获得百倍提高;git

  • 支持CGI框架,轻松构建web服务(New);github

  • 支持gethostbyname、mysqlclient、ssl等经常使用第三库(New);web

  • 可选的共享栈模式,单机轻松接入千万链接(New);sql

  • 完善简洁的协程编程接口编程

-- 类pthread接口设计,经过co_create、co_resume等简单清晰接口便可完成协程的建立与恢复;
-- 类__thread的协程私有变量、协程间通讯的协程信号量co_signal (New);
-- 非语言级别的lambda实现,结合协程原地编写并执行后台异步任务 (New);
-- 基于epoll/kqueue实现的小而轻的网络框架,基于时间轮盘实现的高性能定时器;后端

libco产生的背景

早期微信后台由于业务需求复杂多变、产品要求快速迭代等需求,大部分模块都采用了半同步半异步模型。接入层为异步模型,业务逻辑层则是同步的多进程或多线程模型,业务逻辑的并发能力只有几十到几百。随着微信业务的增加,系统规模变得愈来愈庞大,每一个模块很容易受到后端服务/网络抖动的影响。微信

异步化改造的选择

为了提高微信后台的并发能力,通常的作法是把现网的全部服务改为异步模型。这种作法工程量巨大,从框架到业务逻辑代码均须要作一次完全的改造,耗时耗力并且风险巨大。因而咱们开始考虑使用协程。

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

  1. 业界协程在c/c++环境下没有大规模应用的经验;

  2. 如何控制协程调度;

  3. 如何处理同步风格的API调用,如Socket、mysqlclient等;

  4. 如何处理已有全局变量、线程私有变量的使用;

最终咱们经过libco解决了上述的全部问题,实现了对业务逻辑非侵入的异步化改造。咱们使用libco对微信后台上百个模块进行了协程异步化改造,改造过程当中业务逻辑代码基本无修改。至今,微信后台绝大部分服务都已经是多进程或多线程协程模型,并发能力相比以前有了质的提高,而libco也成为了微信后台框架的基石。

libco框架

libco在框架分为三层,分别是接口层、系统函数Hook层以及事件驱动层。

同步风格API的处理

对于同步风格的API,主要是同步的网络调用,libco的首要任务是消除这些等待对资源的占用,提升系统的并发性能。一个常规的网络后台服务,咱们可能会经历connect、write、read等步骤,完成一次完整的网络交互。当同步的调用这些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不适用,当一个协程调用了gethostbyname时会同步等待结果,这就致使了同线程内的其它协程被延时执行。咱们对glibc的gethostbyname源码进行了研究,发现hook不生效主要是因为glibc内部是定义了__poll方法来等待事件,而不是通用的poll方法;同时glibc还定义了一个线程私有变量,不一样协程的切换可能会重入致使数据不许确。最终gethostbyname协程异步化是经过Hook __poll方法以及定义协程私有变量解决的。

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

协程信号量

在多线程环境下,咱们会有线程间同步的需求,好比一个线程的执行须要等待另外一个线程的信号,对于这种需求,咱们一般是使用pthread_signal 来解决的。在libco中,咱们定义了协程信号量co_signal用于处理协程间的并发需求,一个协程能够经过co_cond_signal与co_cond_broadcast来决定通知一个等待的协程或者唤醒全部等待协程。

总结

libco是一个高效的c/c++协程库,提供了完善的协程编程接口、经常使用的Socket族函数Hook等,使得业务可用同步编程模型快速迭代开发。随着几年来的稳定运行,libco做为微信后台框架的基石发挥了举足轻重的做用。


更多精彩内容欢迎关注bugly的微信公众帐号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!

相关文章
相关标签/搜索