現代硬件上的高性能C+異步框架-SeaStar


 

Seastar簡介

概括來說,Seastar 是一個開源,基於c++ 11/14 feature,支持高並發和低延遲的異步編程高性能庫。要想深入學習Seastar,需要掌握新的C++ features,這些features包括:

Auto/decltype

Tuple

Variadic Template可變參數的模板

Move copy/Assignment 移動拷貝/移動賦值

Metaprogramming 模板元編程

Lambda函數

Smart pointers智能指針

Future/promise

這些新的特性很復雜,背后都有深厚的學術和業界工程實踐的積累,學習起來有一定難度。實際上Seastar代碼里充斥着幾乎所有新的C++ features和新的c++庫中的APIs (包括STD 和boost庫)。而且為了自己的需要,Seastar擴展了C++標准中的部分特性。

例如STD中的future/promise就不能滿足Seastar復雜的異步編程的需要,為此,Seastar實現了廣泛的future/promise接口。同時,Seastar重新實現了一些有別於標准庫中的數據結構,因為像thread safe這樣的要求是無需考慮的。所以,閱讀Seastar這樣的匠心之作肯定能帶來別樣的收獲。

異步編程

1)為什么需要異步編程?

隨着硬件的高速發展(SSD, 10G/40G network, NUMA),軟件開發人員的技能還停留在傳統的開發模式上,這樣寫出來的程序就限制了新硬件性能的發揮:

當硬件發展到了一定階段,單核的性能很難再飛速增長

多核的數量在增加,但應用很難有效利用他們

鎖的開銷很大。在傳統的多進程/線程的編程中,鎖是保證數據安全的重要手段。由於資源(file, memory)的競爭, 進程/線程不得不阻塞等待。據實驗測試,一個高並發的應用,20%~70%的時間可能耗在無謂的鎖等待上。

數據分配在一個核上,可能復制和使用在別的核上例如一個網卡的中斷程序運行在一個core上,而后續的數據包的處理可能遷移到別的core上,這樣CPU的cache line頻繁的miss,造成性能的penalty。

用戶態/內核態,進程線程/中斷上下文切換的開銷

2)異步編程的起源

同步和異步區別是什么?同步意味着一個操作必須等待它調用的其他操作完成后才能繼續進行下一步的操作。而異步則是無需等待,當一個操作調用一個會阻塞的操作時,它會接着做別的事情,等那個阻塞的操作完成時,會發一個event通知它,那么它接着處理這個阻塞的操作。

我們看看傳統的一個network server 是如何處理並發的網絡連接:當一個client連接到來時,一個新的process或者thread被fork 出來,即使某個連接調用一個阻塞(blocking)的系統調用(syscall)也沒有關系,因為可以有的其他process或者thread來處理新的連接。為什么說這種編程是同步的呢?因為代碼的執行就如程序員所寫一樣是線性的,嚴格按照一行一行代碼的邏輯順序執行。

盡管從操作系統角度來說,本質上系統是並發執行的:當一個process/thread 調用一個阻塞的系統調用時,OS會自動切換到另一個合適的process/thread去執行。同步編程是有開銷的:首先,fork一個新的process或者thread很慢,再加上如果大量的操作被阻塞,隨之發生的頻繁進程/線程上下文切換的代價也很大。除此之外,每個process/thread需要有自己獨立的棧空間,如果系統中process/thread很多,棧的內存開銷也會很大。

最近一些年,異步編程開始流行起來。各種語言從Python,JaveScript,Go到C++紛紛開始支持異步編程。一個server是異步的,那么它本質上是事件驅動的(event-driven)。通常只有一個thread,這個thread就是一個迭代循環執行。每次迭代它都要輪詢(poll)有沒有新的事件(event)要處理,如果有,可以調用相應的已經注冊好的具體事件處理函數。

事件的處理是run-to-completion,也即意味着main thread不會處理下一個even,除非上一個event已經處理完成。這些event可以對應網絡socket連接,存儲disk I/O和計時器等。這樣整個系統沒有thread休眠和鎖造成的休眠或者忙等待,所有的組成構件都在不停地運轉,系統性能達到最優。當然,對於Seastar來說,是每個core一個這樣thread,它稱之為engine。

3)異步編程的挑戰

復雜性:代碼不再像之前同步編程時可讀性那么好。許許多多從簡單到復雜的回調函數嵌入(遞歸嵌入)到各種代碼分支中,大部分時候,程序員閱讀所見的代碼並不一定能在邏輯上順序執行到,執行的時機取決於一些狀態值。

要妥善安排這些代碼之間的關系(event 和event hander)並非易事。為了減少這種異步帶來的復雜性,Seastar實現了future/promise對象,用來管理這些異步操作。這樣基於回調函數的編程就變成了基於future/promise的編程。

非阻塞模式:因為每個core上只有一個thread在運行,這個thread不能被阻塞。所以它不能直接或者間接調用任何可能阻塞的系統調用,也不能調用鎖接口以防止死鎖。

Seastar異步編程基石

Future

Future代表一個值可能未定的計算結果,這個結果可能現在不能馬上得到,需要等待到將來某個時間點。這種future可以是網絡傳輸的一個緩存,定時器的到期,磁盤寫的完成等,它可以是任意一個需要等待的結果。一般我們把一個異步函數的返回值作為一個future,這個future最終向調用者提供結果。

例如我們可以用future read來表示讀取磁盤文件的結果,這個結果是一個int值,這個read函數沒有任何等待,立馬返回給我們一個future. 調用者調用future.available()檢查值是否可用,一旦可用,就用future.get獲取相應的值。

Promise

Promise顧名思義承諾,代表一個異步函數,這個異步函數返回future,並承諾在將來的某個時間點給future賦值。接口promise.get_Future獲取對應的future,promise.set_value(T)給對應的future賦值。

Continuation

Continuation代表一段計算,最常用的就是Lambda函數。這些continuations通過future的then函數綁定到future上,then函數的輸入參數就是綁定的future對象。當future 的值available時,這些綁定的continuations就會自動執行。

Continuation最大的威力在於:一個continuation可以返回一個新的future,這個future又可以綁定新的continuation。這樣就可以實現異步操作級聯執行future.then().then().then()…。

高階接口

Seastar在f-p-c基礎上還實現了更高級的接口:

異步操作的並行執行parallel_for_each

異步操作的循環執行repeat

同步等待異步操作的執行when_all

對於map reduce支持

Semaphore,gate和pipe等接口。

從這里可以看出,Seastar是一個完備的支持異步編程的框架。

Seastar架構

Seastar是一個基於分片的異步編程框架: 它能夠實現復雜的服務器邏輯,保證網絡和存儲操作,多核之間操作的異步性,以達到高性能和低延遲的目標。圖四可以清楚地看出Seastar相對於傳統數據棧的優勢。

 

內存shard

Seastar運行后會保留(--reserve-memory)一小部分內存給操作系統或者預留(-m)一定數量的內存給自己。

這樣,Seastar對分配給自己的物理內存也進行了分片(shard),每個core有自己的內存空間,有自己的memory allocator(log-structured)對內存區域進行分配和釋放管理,無需考慮thread safe和內存碎片化(定期compact,移動object,合並memory holes)。下圖對比了基於JVM和Seastar的內存管理:

 

網絡shard

所有的網絡連接在cores之間分片(shard),每個core只負責處理自己那部分數據連接。

但是對於Posix network stack,盡管在Seastar這一層是shar-nothing設計,由於Seastar需要和下方OS network stack進行交互,這樣就可能有鎖,原子操作,CPU 緩存的miss,性能不可避免地受到損失。所以要想獲得最佳性能,推薦配置Seastar native network stack。

用戶態task調度

下圖對比了Seastar任務調度和傳統線程調度:

 

每個core上都有一個task scheduler,相對於內核中的thread,每個task都是一個輕量級的任務。Seastar中有兩種類型task:

Non-threaded context task 這種task一般很短,沒有自己的棧,主要由Lambda函數組成,event-loop 主線程循環調度task隊列中的Lambda函數。當然如果這種task不能立即執行,是需要內存空間來保存相應的狀態,不過比起thread棧來說,開銷很小。

Threaded context task 這種task的實現實際上就是用戶態協程:它們有自己的棧,只會主動讓出(yield) CPU,用setjmp/longjmp進行用戶態的上下文切換,有相應的調度policy(例如基於時間片的調度去保證公平性)。

所有這些用戶態task不能調用blocking系統調用,因為整個core只有一個系統thread。有時候又不得不調用blocking系統調用,那只能另起一個Posixthread。所以大部分時間,整個core只有一個Posix thread在運行,有時候會是兩個。

用戶態disk I/O的調度

Seastar利用操作系統libaio提供的io_submit去提交磁盤操作和io_getevent來收集操作結果,從而實現磁盤I/O操作的異步性。但Linux對於aio支持的並不是很好,並不是所有文件系統都支持aio,即使有的支持,也有很多問題。

最新的xfs對於aio支持的很好,所以對於disk IO,只推薦采用xfs。由於在內核中,從文件系統(file system)到塊設備層(block level)再到具體的存儲設備層,每個層都有I/O隊列,對I/O進行了自己的管理。一旦storage I/O出現擁塞,不太容易判斷哪層出現問題,也不好采取措施進行調控。

因此,Seastar在用戶態實現了I/O scheduler,對磁盤I/O進行精確的分級控制和調優。Seastar有自己的I/O queue來緩存I/O,並實現了各種I/O priority class,從而保證各種I/O調度的公平性。下圖示意了Seastar實現的用戶態I/O調度器:

 

用戶態原生網絡棧(Native network stack)

下圖描繪了Seastar的網絡棧,即包含Posix stack,也包含原生網絡棧:

 

除了支持常規的Posix network stack,Seastar還支持基於DPDK的native network stack。大家知道,DPDK是Intel推出的一個高性能用戶態網絡管理包,其核心思想和Seastar是一致的:用戶態輪詢(poll)模式的網卡驅動,沒有中斷,沒有上下文切換,沒有內存拷貝,無鎖,share-nothing,自己的內存管理,利用NUMA等等。

DPDK主要提供L2數據包處理功能,需要上層應用提供L3及以上的網絡管理。Seastar實現了TCP/IP協議:每個core綁定到物理網卡的一個接受隊列和發送隊列,這樣所有的數據連接也被分片(shard),每個core自始至終只負責自己那部分數據連接。對於native network stack,沒有syscall調用,沒有多余的數據復制,沒有鎖,性能當然最好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM