用戶態操作系統之一 Seastar簡介

用戶態操作系統之一 Seastar簡介

來自專欄 Zero Switch8 人贊了文章

引言

有一句老生常談的話:CPU不是瓶頸,網路才是。在此前提下,服務端開發使用什麼語言關係並不大,因為CPU還沒用用滿的情況下,網路就已經滿了。

然而,世殊事異,這句話的真實性已經大大下降。近幾年,存儲硬體與網路硬體高速發展,設備IOPS與帶寬都是一漲再漲,其延遲甚至已經達到了數us的級別,一次進程切換也不過如此。相比之下,CPU在速度上幾乎毫無發展。這就促使著開發者們開始研究各種節約CPU的技術:

  • 只有單個任務對CPU的需求下降了,CPU才能處理更多的任務,以享受硬體在帶寬發展上的紅利。
  • 節約CPU,意味著單個任務的延遲更加短。硬體延遲已經很低了,如果不能降低軟體延遲,則無法充分發揮硬體延遲上的紅利。

如何節約CPU

想要節約CPU,我們首先需要知道現代CPU的使用方式,以及,哪些操作會佔用大量的CPU時間。

現代CPU已經處於多核時代,我們的程序也是並發的,從而可以充分發揮多核CPU的優勢。那麼,我們的CPU究竟消耗在了什麼地方?

  • 複製:在進行網路讀寫時,內核網路棧會從網卡中將數據複製到內核緩衝區中,我們的應用又會從內核網路棧中將數據複製到應用緩衝區中。反之亦然。硬體操作也有類型的情況發生。
  • 進程切換:一方面,上下文切換的過程本身會消耗CPU;另一方面,正在工作的進程被切換出去再切換回來,會嚴重增加正在處理任務的延遲。
  • 系統調用:上下文切換的開銷。任務延遲增加。
  • 中斷處理:上下文切換的開銷。任務延遲增加。
  • 數據多核同步(緩存同步)
  • 鎖的開銷:鎖會引起多核緩存同步開銷,嚴重時需要內核參與,造成上下文切換。

可以參考以下文章:

吳乎:微優化之一 Zero Allocation?

zhuanlan.zhihu.com圖標吳乎:微優化之二 Zero Branch misprediction?

zhuanlan.zhihu.com圖標吳乎:微優化之三 Zero Switch?

zhuanlan.zhihu.com圖標吳乎:微優化之四 Zero Synchronization?

zhuanlan.zhihu.com圖標吳乎:微優化之五 Zero Copy?

zhuanlan.zhihu.com圖標

我們可以上將述問題大致分成三類:

  • 複製開銷:如果整個IO棧不能一體化開發,這個問題很難避免。
  • 切換開銷:操作系統進程抽象固有開銷,無法避免,只能儘力減少。
  • 並發開銷:緩存同步與切換開銷。

對於前兩類問題,其根本來源都是內核抽象的開銷,想要優化掉它,我們必然減少對內核的依賴,即,將內核提供的抽象,上移到應用層中,在應用層中實現這些抽象。典型的例子如:

  • 用戶態TCP/IP棧
  • 用戶態PageCache與Disk IO調度
  • 用戶態線程/用戶態調度器
  • 用戶態內存管理

對於並發開銷,其優化思路則非常多:

  • 能用原子變數,盡量使用原子變數
  • 鎖分段
  • 鎖分解
  • 降低鎖的頻率
  • 讀寫鎖
  • 自旋鎖
  • 順序鎖
  • RCU

是不是眼花繚亂?Linux自從進入SMP時代後,從最開始的Big kernel lock,到後來的spinlock,再到後來的RCU,基本每一行代碼都要細斟慢酌,以求降低臨界區大小,降低鎖開銷。那麼,問題來了,一個工具使用起來如此複雜,是不是我們從一開始就使用了錯誤的工具,才把整個事情變得那麼複雜?

並發範式 Share nothing

並發編程難在哪裡?難在共享數據,正是因為共享數據的存在,我們的才需要用各種手段保護它們,才需要在多個核之間同步數據。如果我們完全禁止共享數據呢?核與核之間只通過消息進程通信,會怎麼樣?

這樣的編程範式其實已經存在許多,如:

  • Actor:每個Actor是一個獨立運行的執行流,多個Actor之間只能通過發送消息來通信,禁止共享數據。
  • CSP:大家就理解為是golang提供的並發抽象吧。每個goroutine是一個獨立運行的執行流,goroutine之間通過具名的channel進行通信。

這類並發範式的核心都是Share nothing。此系列文章的主人公Seastar,採用了類似的思想,但是與上面又有很大的不同。Seastar引入了一層非常薄的抽象:它會在每個核上創建一個線程,並將此線程綁定在其上。不同核(線程)之間禁共享數據,只能通過消息隊列來傳遞數據。

比起Actor與golang,seastar引入的抽象非常薄,它使得程序員可以完整地推斷程序的行為,並且沒有過重的runtime抽象開銷。

用戶態操作系統 Seastar

Seastar是一個應用框架,它幾乎將操作系統所提供的抽象完整地搬移到了用戶態中,以減少操作系統的抽象開銷,實現軟硬體一體化。

scylladb/seastar?

github.com圖標http://seastar.io/shared-nothing/?

seastar.io

Seastar之執行流抽象

可以將Seastar想像成一個支持多核的操作系統,每個核上運行著許多的「進程」,我們之後會使用執行流這個詞,以避免混淆。但是與操作系統不同的是,這些執行流有固定的宿主核,每個執行流從頭到尾只能在一個核上運行,並且,位於不同核上的執行流之間,只能通過跨核消息來通信。

可以這樣理解,Seastar將每個核抽象成一台單核計算機,每個單核計算機上運行著許多執行流,一個單核計算機上的多個執行流可以共享數據,不同單核計算機上的執行流只能通過消息來共享數據。

現在考慮Seastar如何提供執行流抽象的。執行流的本質是一系列微任務被鏈接起來後形成的一個微任務鏈。為什麼是微任務鏈,而不是一個連續的宏任務?因為,我們的任務通常涉及到IO,而IO並不總是可用的,比如,讀一個socket時,其內還沒有數據。此時,我們顯然需要讓出CPU,等待socket可讀,才進行接下來的步驟。於是,我們的一個完整的宏任務被拆成了很多微任務,這些微任務被鏈接起來後,即是我們的宏任務,也即是我們的執行流。

當然,複雜的微任務甚至可以構造一個有向無環圖。這個有向無環圖會由Seastar抽象的用戶態CPU按照拓撲序來調度執行。

那麼,Seastar是如何將這些微任務鏈接起來的?一種簡單的方法是回調函數,寫過js的人應該都理解。一種複雜的方法是提供用戶態線程的抽象,即所謂協程,給用戶提供一個單協程佔用整個CPU的抽象。Seastar提供了另一種抽象,FPC,即future-promise-continuation。FPC使得構造有向無環圖更加方便,使用協程的話,還需要提供latch/conditional variable等同步原語,才能構造更加複雜的執行流。

Seastar在每個用戶態CPU上運行一個調度器,來調度一系列的微任務。

Seastar之內存抽象

Share-nothing的用戶態執行流的抽象降低了切換開銷以及同步開銷,然而,同一進程內,內存是共享的,分配與釋放內存時,依然會有同步的存在。為了避免此問題,Seastar在應用啟動時,將整個虛擬地址空間按照CPU核數等分為若干塊,每個CPU使用自己的內存塊進行內存分配與釋放,從而避免同步。

Seastar之文件抽象

Seastar是一個非同步框架,任何一個核阻塞都會造成核上的待調度的微任務嚴重超時。然而,令人無奈的是,傳統文件系統操作是同步阻塞的。好在AIO的存在解決了這一問題(雖然現在AIO還是一堆坑)。AIO有一些固有的限制,它必須以O_DIRECT方式打開文件,導致不能使用pagecache以及讀寫必須對齊。為了解決AIO的問題,Seastar維護用戶態PageCache,從而實現了Zero copy的文件操作。並且,它維護自己的IO調度策略,從而更好地使用磁碟。

Seastar之用戶態網路棧

Seastar支持多種形式的網路操作,一是傳統的epoll方式,這種方式已經非常成熟,並且在業內有廣泛應用。另一種是用戶態網路棧+DPDK,從而實現Zero copy與Zero switch的網路操作,進一步提高了網路的性能。


這是一篇引言性質的文章,介紹的Seastar背後的思想以及簡單構成。在後面的章節中,我將進一步剖析Seastar的源碼,並且編寫一個簡化版本的Seastar實現。


推薦閱讀:

伺服器維護應該注意要點有哪些?
保護 Web 伺服器的安全
什麼是 Linux 伺服器,你的業務為什麼需要它?
自己家裡搭建NAS伺服器有什麼好方案?(轉載)
Confluence 6 配置 MySQL 伺服器

TAG:高並發 | 高性能 | 伺服器 |