【騰訊Bugly乾貨分享】揭祕:微信是如何用libco支撐8億用戶的

本文來自於騰訊bugly開發者社區,未經做者贊成,請勿轉載,原文地址:http://dev.qq.com/topic/58203cfcd149ba305c5ccf85前端

做者:Leiffymysql

導語

libco是微信後臺大規模使用的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 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!

相關文章
相關標籤/搜索