非同步編程框架Seastar介紹
4 人贊了文章
教程翻譯自Seastar官方文檔,參考:
http://docs.seastar-project.org/master/md_doc_tutorial.html
使用Seastar進行非同步(Asynchronout)編程
介紹
我們在本文中介紹的Seastar,是一個用於在現代多核機器上,編寫高效複雜的伺服器應用程序的C++庫。
傳統上,用於編寫伺服器應用程序的編程語言庫和框架已經分為兩個不同的陣營:那些注重效率的陣營和側重於複雜性的陣營。一些框架是非常高效的,但是只允許構建簡單的應用程序(例如,DPDK允許單獨處理數據包的應用程序),而其他框架允許構建極其複雜的應用程序,代價是運行時效率。 Seastar是我們努力獲得兩全其美的方法:創建一個允許構建高度複雜的伺服器應用程序,但實現最佳性能的庫。
Scylladb第一個使用了Seastar,它是對Apache Cassandra的重寫。 Cassandra是一個非常複雜的應用程序,然而,通過Seastar,我們能夠讓吞吐量提高10倍,同時顯著的降低一致性的延遲。
Seastar提供了一個完整的非同步編程框架,它使用了 futrues 和 continuations 的概念,來統一表示和處理每種類型的非同步事件,包括網路IO,磁碟IO以及其他事件的複雜事件。
由於現代多核和多插槽機器在核心之間共享數據(原子指令,高速緩存行反彈和內存隔離)具有很高的代價,Seastar程序使用無共享編程模型,即可用內存隔離,每個核心都在自己的內存部分進行數據處理,內核之間的通信通過顯式的消息傳遞(當然這發生在使用SMP的共享內存硬體的情況下)
非同步編程
用於網路協議的伺服器(如經典的HTTP(Web)或SMTP(電子郵件)伺服器)處理是並行的:多個客戶端並行發送請求,在完成處理一個請求之前,我們無法開始處理下一個請求:因為各種原因,一個請求可能並經常需要阻塞磁碟IO,例如一個完整的TCP窗口(即一個慢速連接),甚至是持有非活躍連接的客戶端。伺服器需要處理其他連接也是如此。
經典網路伺服器(如Inetd,Apache Httpd和Sendmail)處理這種並行連接所採用的最直接的方法是對每個連接使用單獨的操作系統進程。這種技術經過多年的發展,以提高其性能:剛開始,使用一個新的進程來處理每個新的連接;後來,使用進程池,每一個新的連接都被分配到池中的一進程。最後,進程被線程所取代。但是,所有這些實現背後的共同思想是,在每一個時刻,每個進程只處理一個連接。因此,伺服器代碼可以自由使用帶阻塞的系統調用,例如讀取或寫入連接,或者從磁碟讀取數據,如果這個過程阻塞了,也沒關係,因為我們有許多額外的進程可以處理其他連接。
對每個連接使用一個進程(或一個線程)的伺服器編程稱為同步編程,因為代碼是線性執行的,在上一行完成後,下一行代碼開始運行。例如,代碼可能會從套接字讀取請求,解析請求,然後零碎地從磁碟讀取文件並將其寫回套接字。這樣的代碼很容易編寫,就像傳統的非並行程序一樣。事實上,甚至可以運行一個外部的非並行程序來處理每個請求 - 例如Apache HTTPd如何運行「CGI」程序,生成第一個動態Web頁面。
>注意:儘管同步伺服器應用程序是以線性,非並行的方式編寫的,但內核可以幫助確保所有事情都是並行發生的,並且可以充分利用機器的資源(CPU,磁碟和網路)。除了進程並行(我們有多個進程並行處理多個連接)之外,內核甚至可以並行化一個單獨連接的工作 - 例如處理一個未完成的磁碟請求(例如,從磁碟文件中讀取)並行處理網路連接(發送緩衝區中尚未發送的數據,並緩衝新接收的數據,直到應用程序準備好讀取它)。
但是同步的,每個連接的過程,伺服器編程並不是沒有缺點或成本的。伺服器作者慢慢地但是確定地意識到,開始一個新的進程是緩慢的,上下文切換很慢,每個進程都伴隨著大量的開銷,最顯著的是它的堆棧大小。伺服器和內核作者努力減輕這些開銷:他們從進程切換到線程,從創建新線程到線程池,降低了每個線程的默認堆棧大小,增加了虛擬內存大小以允許部分利用的堆棧。但是,同步設計的伺服器的性能並不理想,隨著並發連接數量的增長,伺服器的性能也不盡人意。 1999年,Dan Kigel普及了「C10K問題」,需要一台伺服器來高效地處理10,000個並發連接,其中大部分是緩慢甚至不活動的。
該解決方案在接下來的十年中變得流行,它放棄了舒適但低效的同步伺服器設計,轉而採用新型伺服器設計 - 非同步伺服器或事件驅動伺服器。事件驅動的伺服器只有一個線程,或者更準確地說,每個CPU有一個線程。這個單線程運行一個緊密的循環,在每次迭代時,使用poll()(或更高效的epoll)檢查許多打開文件描述符(例如套接字)上的新事件。例如,一個事件可以是一個套接字變得可讀(新的數據已經從遠端到達)或變得可寫(我們可以在這個連接上發送更多的數據)。應用程序通過做一些非阻塞操作來處理這個事件,修改一個或多個文件描述符,並且保持它對這個連接狀態的知識。
然而,非同步伺服器應用程序的作者面臨著今天仍面臨的兩大挑戰:
* 複雜性:編寫簡單的非同步伺服器非常簡單。但是編寫一個複雜的非同步伺服器是非常困難的。單個連接的處理,而不是一個簡單易讀的函數調用,現在涉及大量的小型回調函數和一個複雜的狀態機,以記住每個事件發生時需要調用哪個函數。
* 非阻塞:每個內核只有一個線程,對於伺服器應用程序性能很重要,因為上下文切換很慢。但是,如果我們每個內核只有一個線程,則事件處理函數不能阻塞,否則內核將保持空閑狀態。但是一些現有的編程語言和框架讓伺服器作者別無選擇,只能使用阻塞函數,因此也不能使用多線程。例如,Cassandra被編寫為一個非同步伺服器應用程序;但是因為磁碟I/O是用mmaped文件實現的,所以在訪問時可以不受控制地阻塞整個線程,它們被迫在每個CPU上運行多個線程。
而且,當需要儘可能好的性能時,伺服器應用程序及其編程框架不得不考慮以下因素:
* 現代機器:現代機器與十年前的機器非常不同。它們具有許多內核和深層內存層次結構(從L1緩存到NUMA),這些有利於某些編程實踐,卻帶來一些問題:不可擴展的編程實踐(如鎖定)會破壞許多內核的性能;共享內存和無鎖同步原語是可用的(即,原子操作和內存排序的屏障),但是比僅涉及單個內核的緩存中的數據的操作慢得多,並且也阻止應用程序擴展到多個內核。
* 編程語言:諸如Java,Javascript和類似的「現代」語言的高級語言是很方便的,但是每一種語言都有自己的一套假設,與上面列出的要求相衝突。這些旨在可移植的語言也使編程人員無法控制關鍵代碼的性能。為了獲得最佳的性能,我們需要一種編程語言,使程序員能夠完全控制,零運行時間的開銷,另一方面, 編譯時代碼的複雜性和優化。
Seastar是一個用於編寫非同步伺服器應用程序的框架,旨在解決上述所有四個難題:這是一個用於編寫複雜的非同步應用程序(包括網路和磁碟I / O)的框架。該框架的高效運行途徑完全是單線程(每核心),並可擴展到多個內核,並最小化在內核之間使用昂貴的內存共享。它是一個C ++ 14庫,為用戶提供了複雜的編譯時功能和對性能的全面控制,而無需運行時間的開銷。
Seastar
Seastar是一個事件驅動的框架,允許您以相對直接的方式編寫非阻塞的非同步代碼。它的API基於 futures(c++11新特性)。 Seastar利用以下概念來實現卓越的性能:
* Cooperative micro-task scheduler(合作的微任務調度器):每個核心運行一個合作任務調度器,而不是運行線程。每個任務通常都是非常輕量級的,只要處理最後一次I/O操作的結果並提交一個新的結果就可以運行。
* Share-nothing SMP architecture(無共享SMP架構):每個內核獨立於SMP系統中的其他的內核運行。內存,數據結構和CPU時間不共享;相反,核心間通信使用顯示的消息傳遞。 Seastar core 通常被稱為 shard。
TODO:更多在資料訪問 scylladb/seastar
* Future based APIs(基於future的API):futures允許您提交I/O操作,並鏈接完成I/O操作時要執行的任務。這是很容易並行運行多個I/O操作。例如,在響應TCP連接請求時,可以發出多個磁碟I/O請求,或發送相同的系統上的消息給其他核,或者發送請求到集群中的其他節點,等待一些或全部結果完成,匯總結果並發送響應。
* Share-nothing TCP stack(無共享TCP堆棧):雖然Seastar可以使用主機操作系統的TCP堆棧,但它還提供了自己的高性能TCP/IP堆棧,該堆棧構建在任務調度器和無共享架構之上。堆棧在兩個方向上提供零拷貝:您可以直接從TCP堆棧的緩衝區處理數據,並將您自己的數據結構的內容作為消息的一部分發送,而不會產生副本。
* DMA-based storage APIs(基於DMA的存儲API):與網路堆棧一樣,Seastar提供零拷貝存儲API,允許您將數據存入存儲設備。
本教程面向已經熟悉C ++語言的開發人員,並將介紹如何使用Seastar創建新的應用程序。
參考鏈接:
scylladb/seastar
scylladb/seastar
推薦閱讀:
※常規操作之非同步
※生成器進化到協程 Part 1
※一個關於事件循環的題目
TAG:非同步 |