導語
ibco是微信後台大規模使用的c/c++協程庫,2013年至今穩定運作在微信後台的數萬台機器上。 libco在2013年的時候作為騰訊六大開源專案首次開源,我們最近做了一次較大的更新,同步更新在https://github.com/tencent/libco 上。 libco支援後台敏捷的同步風格程式設計模式,同時提供系統的高並發能力。
無需侵入業務邏輯,把多進程、多執行緒服務改造成協程服務,並發能力得到百倍提升;
支援CGI框架,輕鬆建立web服務(Newget); 、mysqlclient、ssl等常用第三庫(New);
可選的共享堆疊模式,單機輕鬆接入千萬連接(New);
完善簡潔的協程編程接口
– 類pthread – 類透過co_create、co_resume等簡單清晰介面即可完成協程的建立與復原; – 類別__thread的協程私有變數、協程間通訊的協程訊號量co_signal (New); – 非語言層級的lambda實現,結合協程原地編寫並執行後台非同步任務(New); – 基於epoll/kqueue實現的小而輕的網路框架,基於時間輪盤實現的高效能定時器;
libco產生的背景
『早期微信後台因為業務需求複雜多變、產品需求快速迭代等需求,大部分模組都採用了半同步半非同步模型。存取層為非同步模型,業務邏輯層則是同步的多進程或多執行緒模型,業務邏輯的並發能力只有幾十到幾百。隨著微信業務的成長,系統規模變得越來越龐大,每個模組很容易受到後端服務/網路抖動的影響。
非同步化改造的選擇
為了提升微信後台的並發能力,一般的做法是把現網的所有服務改成非同步模型。這種做法工程量龐大,從框架到業務邏輯程式碼均需要做一次徹底的改造,耗時耗力且風險巨大。於是我們開始考慮使用協程。
但使用協程會面臨以下挑戰:
業界協程在c/c++環境下沒有大規模應用的經驗;
產業調度;
『 mysqlclient等;
如何處理已有全域變數、執行緒私有變數的使用;
最終我們透過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來對全域變數進行快速修改,而在協程環境下,我們創造了協程變量協程的改造工作量。
因為協程實質上是線程內串行執行的,所以當我們定義了一個線程私有變數的時候,可能會有重入的問題。例如我們定義了一個__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作為微信後台框架的基石發揮了舉足輕重的作用。