libco 是微信后盾大规模应用的 c /c++ 协程库,2013 年至今稳固运行在微信后盾的数万台机器上。libco 在 2013 年的时候作为腾讯六大开源我的项目首次开源,咱们最近做了一次较大的更新,同步更新在 https://github.com/tencent/libco(点击浏览原文间接拜访)上。libco 反对后盾麻利的同步格调编程模式,同时提供零碎的高并发能力。
libco 反对的个性
❏ 反对 CGI 框架,轻松构建 web 服务 (New);
❏ 反对 gethostbyname、mysqlclient、ssl 等罕用第三库 (New);
❏ 可选的共享栈模式,单机轻松接入千万连贯 (New);
❏ 欠缺简洁的协程编程接口:
▪ 类 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 框架
同步格调 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 作为微信后盾框架的基石施展了无足轻重的作用。
本文原创作者微信团队转自微信后盾团队如有侵权请分割咱们删除
OpenIMgithub 开源地址:
https://github.com/OpenIMSDK/…
OpenIM 官网:https://www.rentsoft.cn
OpenIM 官方论坛:https://forum.rentsoft.cn/
更多技术文章:
开源 OpenIM:高性能、可伸缩、易扩大的即时通讯架构
https://forum.rentsoft.cn/thr…
【OpenIM 原创】简略轻松入门 一文解说 WebRTC 实现 1 对 1 音视频通信原理
https://forum.rentsoft.cn/thr…
【OpenIM 原创】开源 OpenIM:轻量、高效、实时、牢靠、低成本的音讯模型
https://forum.rentsoft.cn/thr…