libco是微信背景大規模使用的c/c++協程庫,2013年至今穩定運作在微信背景的數萬台機器上。libco在2013年的時候作為騰訊六大開源項目首次開源,最近做了一次較大的更新。libco支援背景靈活的同步風格程式設計模式,同時提供系統的高并發能力。
libco支援的特性
支援cgi架構,輕松建構web服務(new);
支援gethostbyname、mysqlclient、ssl等常用第三庫(new);
可選的共享棧模式,單機輕松接入千萬連接配接(new);
完善簡潔的協程程式設計接口:
libco産生的背景
早期微信背景因為業務需求複雜多變、産品要求快速疊代等需求,大部分子產品都采用了半同步半異步模型。接入層為異步模型,業務邏輯層則是同步的多程序或多線程模型,業務邏輯的并發能力隻有幾十到幾百。随着微信業務的增長,系統規模變得越來越龐大,每個子產品很容易受到後端服務/網絡抖動的影響。
異步化改造的選擇
為了提升微信背景的并發能力,一般的做法是把現網的所有服務改成異步模型。這種做法工程量巨大,從架構到業務邏輯代碼均需要做一次徹底的改造,耗時耗力而且風險巨大。于是開始考慮使用協程。
但使用協程會面臨以下挑戰:
最終通過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來決定通知一個等待的協程或者喚醒所有等待協程。