揭秘:微信如何用 libco 支撑8亿用户?

作者:Leiffy

导语

正如 tony 所说"微信将会发展为数字社会的基础设置,做一个很好的产品,做一个很酷的产品,这个理念对于微信团队来说,已经不足够"。

在这里,微信的技术团队也希望更多的将我们的技术精神与积累分享出去。WeMobileDev 是一种尝试,tinker 是另外一种尝试。tinker 在一个月内获得 5000 的 star 是一个不错的开端,在未来微信终端将开源更多优秀的项目,例如 12 月底的跨平台组件 mars。

不仅仅微信终端,微信后台也开源了大量优秀的项目,phxsql、phxpaxos 以及文中的 libco。事实上,腾讯的开源也在大力发展中,当前也有超过 6 个项目正在审核的流程中。在不久的将来,我们可以在 https://github.com/tencent 发现更多优秀的项目。

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

libco 支持的特性

  • 无需侵入业务逻辑,把多进程、多线程服务改造成协程服务,并发能力得到百倍提升;
  • 支持 CGI 框架,轻松构建 web 服务(New);
  • 支持 gethostbyname、mysqlclient、ssl 等常用第三库(New);
  • 可选的共享栈模式,单机轻松接入千万连接(New);
  • 完善简洁的协程编程接口:
    • 类 _thread 接口设计,通过 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,qps 可达到 210W /s;

协程私有变量

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

因为协程实质上是线程内串行执行的,所以当我们定义了一个线程私有变量的时候,可能会有重入的问题。比如我们定义了一个_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 中,我们定义了协程信号量 cosignal 用于处理协程间的并发需求,一个协程可以通过 cocondsignal 与 cocondbroadcast 来决定通知一个等待的协程或者唤醒所有等待协程。

总结

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

本文来源于:WeMobileDev 微信公众号

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT开发技术与工作效率

服务设计

服务识别方法: 角色分析,场景梳理最小可用,用户服务,应用服务(横向按应用调用顺序,纵向按用户服务),系统能力分析,前置依赖优先后置依赖同版本

742
来自专栏Hadoop数据仓库

HAWQ与Hive查询性能对比测试

一、实验目的         本实验通过模拟一个典型的应用场景和实际数据量,测试并对比HAWQ内部表、外部表与Hive的查询性能。 二、硬件环境 1. 四台VM...

2156
来自专栏程序猿DD

来自95后的天池中间件大赛总结

天池中间件大赛的初赛在今早终于正式结束了,公众号停更了一个月,主要原因就是博主的空余时间几乎全花在这个比赛上,第一赛季结束,做下参赛总结,总的来说,收获不小。

2356
来自专栏用户2442861的专栏

2014 360校园招聘技术类笔试题

原文:http://blog.csdn.net/lanxuezaipiao/article/details/41892553

781
来自专栏ImportSource

Java9里将会出现的5个新功能

1. Java + REPL = jshell 下一个版本的Java将使用一个名为jshell的新命令行工具,它将添加本机支持(native support)并...

3286
来自专栏美团技术团队

美团点评广告实时索引的设计与实现

2383
来自专栏技术分享

.NET应用架构设计—重新认识分层架构(现代企业级应用分层架构核心设计要素)

阅读目录: 1.背景介绍 2.简要回顾下传统三层架构 3.企业级应用分层架构(现代分层架构的基本演变过程) 3.1.服务层中应用契约式设计来解决动态条件不...

2197
来自专栏软件成本造价评估

进行软件研发成本估算与测量时如何识别基本过程?

  我们在进行软件项目研发成本估算与测量过程中,功能点计数项分为数据功能(逻辑文件)和交易功能(基本过程)两类。那么,什么是基本过程呢? 所谓的基本过程...

391
来自专栏圣杰的专栏

ABP入门系列(18)—— 使用领域服务

源码路径:Github-LearningMpaAbp 1.引言 自上次更新有一个多月了,发现越往下写,越不知如何去写。特别是当遇到DDD中一些概念术语的时候...

18810
来自专栏美团技术团队

【美团技术团队博客】序列化和反序列化

摘要 序列化和反序列化几乎是工程师们每天都要面对的事情,但是要精确掌握这两个概念并不容易:一方面,它们往往作为框架的一部分出现而湮没在框架之中;另一方面,它们...

4189

扫码关注云+社区