NrOS: Effective Replication and Sharing in an Operating System
源自osdi2021
整體總結:
第一張圖是關於節點復制的,層次關系是每個NRkernel放在不同的NUMA節點上,每個NRkernel下又有多個cpu,這些cpu之間共享內存。不同的NRkernel之間因為在不同的NUMA節點上,所以內核之間的同步用NRLog來實現(主要減少了跨內核以及跨NUMA之間的消息交互)。
第二張圖是關於NROS設計,主要關注三個方面:虛擬內存、文件系統、進程調度
虛擬內存核心問題在1.修改內存映射時已有其他進程訪問了改內存映射,這一問題使用resolve操作來解決。2.對映射的修改會造成TLB表項不一致,這一點通過修改后發送IPI讓每個節點更新從而放棄失效的TLB來完成。
文件系統的核心問題在處理1.文件描述符問題(放入用戶空間,內核空間之間進行對地址的讀寫)2.大文件與日志大小的沖突(較大的文件在日志中進行索引)3.用戶空間緩沖區可能在所有內核同步之前被修改的問題(將緩沖區復制入內核)4.讀寫沖突和增加並發性(使用CNR)
進程調度主要涉及1.NR將線程分配給cpu內核層面只保留對應的映射表進行粗粒度操作2.需要寫的進程寫入部分內存分配問題(在創建進程前先分配好讀寫的內存,在使用時直接映射到預先分配好的內存即可)
這篇文章針對的主要問題是在多核狀態下,針對NUMA架構的內核設計問題。在多核並發的情況下,並且為了減少多核對總線資源的搶奪,便產生了NUMA架構。與此同時,不同內核之間的一致性問題以及內存讀寫鎖問題一直備受關注,目前針對NUMA架構設計的內核主要有兩種:一是單內核架構,核心思路是多個cpu使用同一個內核,例如linux,采用的思路是使用極其復雜的數據結構和讀寫鎖來提高整個系統的並發性,但是隨着內核的增多這使得整個系統的拓展性降低——增加cpu的代價是整個系統的冗雜和沉重的負載問題。二是多內核結構,對每個NUMA節點分配一個內核,這樣操作的結果是方便了整個系統的可拓展性但是使得不同的NUMA節點之間交流的效率變低使得整個系統的消息傳遞變得十分復雜。為了解決可拓展性和復雜性的矛盾,文章參考了分布式系統節點復制的想法,提出了NUMA節點之間進行內核復制的方法來同時滿足可拓展性和簡潔性,並設計了NROS被證明各種性能上優於linux。
核心思路
核心架構如圖,在此前多核設計的模式中,為了方便可擴展性,大多避免了共享內存的設計,但是NROS采用了共享內存的設計(其可擴展性將在內核復制中解決),NROS的每個內核是對內核節點復制,這減少了讀取內核狀態時復雜的消息傳遞。同時為了保持節點之間的一致性,所有NRkernel共享一個NRLog數據結構,每個節點對NRLog不采用實時更新的方式,只在必要時對其進行同步,並且對整體來說,整個系統的讀寫都采用單線程的數據結構,從而避免出現一致性錯誤。
整體來說,NROS的設計有以下特征和有點:
- 相對多內核來說,采用了內核復制的模式,簡化了NUMA節點之間消息傳遞的方式
- 在整體層面上使用單線程讀寫的數據結構,使得一致性模型簡單並且易於擴展
- 相對單內核來說,簡化了並行模型的復雜性,同時也提高了系統的擴展性
NR(Node Replication)
本文核心設計思路即為節點復制(Node Replication),即在宏觀層面上使得在同一時刻不同NUMA節點上的內核狀態是一致的,從而減少因內核狀態讀取造成的開銷。並且在此之上使用單線程的讀寫數據結構來保持一致性以及用共享內存來減少節點之間交換數據所造成的系統開銷。
一些核心概念:
NRLog:一個被各個內核節點共享的循環數據結構,其每一個條目為每個節點對內核的一次修改,並且NRLog保證操作的順序性。NRLog自身維護一個指針指向尾部,即最新的操作位置,每個NR節點維持一個自身指針指向NRLog的條目,表示本節點目前執行到日志的位置。每一個節點再寫入NRLog的內容后,其寫入的內容在所有節點都執行之前無法被重復使用(為了保證一致性),所以為了防止NRLog被條目占滿,每個節點都會有線程(進程)定期對自身進行更新從而保證與其他節點的一致性。
Flat combing:在NrOS中,flatcombing使得每個NUMA節點的多核中可以共享同一個NR節點(replic),這樣的好處在於同一個NUMA節點中的核可以共享最后一級的內核,同時也為日志的讀寫提供了方便。
日志更新(寫入)過程:當一個本地副本節點需要對NRLog進行修改時,向NRLog申請combiner,這個combiner有兩個作用:一是在Flat combing中所提到的對本地的線程進行加鎖,保證NR節點本地的一致性,二是作為對NRLog的一個訪問鎖,在將自己對NRLog寫入的條目更新完畢后,講NRLog指針放到尾部,並且將自己的指針也更新到尾部。這種對唯一NRLog加鎖的方式即上文提到的“單線程數據結構”,這種方法的好處是使用一個十分簡單可控並且可拓展的方法使各個節點之間保證一致性,但同時在某些極端情況下,這種方法會成為整個系統吞吐量的瓶頸,解決這一問題文章提出了CNR(Concurrent Node Replication,一致性節點復制)講在介紹完NR后詳細介紹。
日志更新(讀取)過程:對日志讀取的操作不需要新添NRLog條目,為了保證更新數據的實時性,讀取過程先回對比此時本地指針和NRLog維持指針的位置,並等待combiner,當所有需要寫入的NRNode使用完combiner后,需要讀取的NRNode獲得combiner講自身副本數據進行更新,講指針更新到NRLog尾部,返還combiner。這里特別需要注意一點,NRLog中日志的條目必然是線性順序的,並且在所有節點更新完之前,寫入的數據是不可重復使用的,這在保持各個節點之間的一致性過程中十分重要。
具體的讀寫過程以下圖為例:
初始過程:每個內核維持自己的對NRLog的指針,並且NRLog本身維持一個自身的尾部指針作為日志的最新狀態。
申請combiner:假設節點1的T2進程需要對內核更新,此時節點1申請到了combiner,節點1中T2執行寫入NRLog操作,其他進程阻塞。
寫入NRLog:節點1申請到combiner后,講需要寫入的內容加入NRLog中,並將自己維持的指針更新到日志尾部。
讀入操作:當節點二讀取到日志尾部在自己維持的指針之后時,申請combiner,並使自己內核的狀態更新到日志的最新位置。
關於NR內核的具體數據結構、文件系統、虛擬內核等放到下周說吧,接下來講一下CNR(Concurrent Node Replication,一致性節點復制)。
CNR(Concurrent Node Replication,一致性節點復制)
首先說明一下CNR出現的原因,因為在NR中,存在一個很大的問題:即所有節點都要共享一個NRLog,並且寫入日志的過程中單個combiner對日志操作減少了並行性。
為了解決這一問題,文章提出了CNR(Concurrent Node Replication,一致性節點復制)。
核心思路:
明確兩個概念:commutative & conflicting(可交換的和沖突的),我們可以將內核的操作指令分為可交換的以及沖突的,這里可交換指可線性化的,即兩條或多條指令並發執行時和其線性執行的結果一致,相反,互相沖突的指令是指交換其執行順序后可能造成其結果不一致的兩條或多條指令。
之后進入正題,CNR和NR的最大區別在於CNR將所有的操作指令根據其可交換性分組,每組指令對應一個NRLog,而CNR中每一個核都映射一個NRLog,並且將自己對內核的更新只記錄在自身的NRLog中。這就解決了以下的一致性問題:例如對內存的讀寫操作,當Node1執行Get(k)而Node2執行Write(k,buff)的時候,我們無法決定Node1和Node2誰先執行指令,從而造成的結果是我們無法確定從地址k讀出的數據是不是寫入的buff,而由於每個NRLog之間的指令是可交換的,所以並發執行時不會出現這樣的問題。
當然,如果放在線程層面來看,更准確的說法應該是相互沖突的指令被放在同一個combiner中來執行(在CNR中,combiner變成了保證線程與自己的本地log一致性的鎖)。這是一個很聰明的做法,將本應該立即保持一致的不同節點所需要保持一致性的時間要求推遲(總之就是雖然都是順序一致性但是CNR的要求要低一點該怎么表達我暫時也想不到)。但與此同時,當所有節點需要同步時,整個同步過程將會變得較為復雜。
有關多節點同步的問題:
首先考慮一些必須涉及到全局的操作:例如對整個文件系統或內存的掃描,抑或是文件改名等操作,這需要對整個內核進行掃描操作,這個時候我們將面臨兩個問題:1.我們需要保證對整個內核掃描的時候沒有正在更新的combiner 2.我們需要保證我們對內核的掃描均是最新的數據。
為了解決這個問題,文章巧妙設計了一種簡單的操作方法:當掃描開始時,對每個NRNode中加入掃描指令,並且加上scan-lock,保證此時只有掃描操作執行(這樣也避免爭奪log資源造成死鎖)即每一個需要更新的線程都等待掃描線程釋放scan-lock。關於節點更新問題,類似於全局操作,不過依舊需要說明一點的是,掃描操作有讀取和更新兩種操作,讀取操作只影響一個副本,而更新操作需要影響多個副本,這在設置讀寫鎖的問題上比較重要。
NROS具體設計
這周算是整體看完了NrOS的論文,首先是發現上周對作者的整體行文過程沒有搞清楚,以至於上周的一些地方(如CNR部分)是完全靠自己臆想推測出來的。在閱讀全文時發現了自己的問題,這里不再一一列舉,涉及到的問題會在下文遇到時提出來。
總體設計概要
NrOS的設計主要涉及以下幾個方面:
- 物理內存設計
- 虛擬內存設計
- 文件系統設計
- 進程分配管理
- 日志同步問題
這里先對NUMA節點和內核復制做一點簡單的說明,傳統的多核並發的架構需要不同內核以及不同NUMA節點之間進行消息傳遞,這使得高代價的消息傳遞(不同核之間以及不同NUMA之間)的時間復雜度與內核的數量之間是n方的關系。針對這件事,NrOS采用了不同的方法:對於同一個NUMA節點之間的不同內核采用共享內存的方式進行信息交互,並且根據NrLog進行節點之間的同步,這就減少了內存之間消息交換的頻率,並且使得消息交互的復雜度與NUMA數量相關而不是與內核數量相關。
說完了總體的內存設計架構之后,我們再來對上面幾點進行詳細的說明。
物理內存設計
物理內存設計主要考慮兩個方面的問題:物理框架(頁表)的設計以及動態內存分配的問題。
先說頁表,在NrOS中,物理頁表是獨立於節點復制的,因為如果將初始化物理頁表這一行為也下放給每個節點,會造成頁表不一致的問題。所以在內核復制之前會先生成一個整體的頁表,之后會將頁表的指針作為參數傳入內核復制的過程中,從而保證每個內核都擁有相同的頁表。
關於緩存:每個NUMA將自己內存分為4KiB和2MiB的塊,分配給每個核(類似於slap allocation的分法,這里不在細說)。
關於動態分配內存問題,與常規的多核系統將自己動態內存分配放在用戶空間不一樣的是,NrOS將動態內存分配放在內核空間,將4KiB和2MiB的塊組成鏈表自動為程序動態分配內存。如此設計有一個很大的問題是在自動內存分配的地方很難做到完美,可能會使后期出現大量的bug。為了解決這一問題,NrOS的內核部分使用Rust編寫,不同於C++,Rust的特點是安全性,即從語言設計層面避免了內存泄漏或溢出等問題的出現,這樣以編譯器為系統安全性做出保證,這使得NrOS將動態內存分配放入內核空間得以實現。
同時,動態內存分配仍需考慮一個問題——當內存不夠時,對內核的狀態復制導致不同節點不一致的問題。舉個例子,不同內核進行同步的時候,由於每個內核對log進行同步不是同時進行的,所以無法確定每個內核進行同步的時候有足夠的內存空間,這就會造成我們無法確定一個同步過程在每個節點中都同步成功。為了解決這一問題,我們在內核中設計了一個決定性內存分配器,在同步時,這個分配器會暫存每一個節點的內存分配結果,如果遇見分配出現錯誤的節點則向之前復制成功的節點返回錯誤。
虛擬內存
NrOS也使用虛擬內存來實現地址的獨立性與一致性,其物理地址的映射表為一個B樹,並且對於每一個節點,都擁有一個物理頁表(上面已經提過),以及一個地址映射表。在虛擬內存系統中,NrOS提供了四個操作:MapFrame(插入表項),Unmap(刪除表項),Adjust(調整表項權限)和Resolve(推進副本查詢地址空間狀態)。
考慮一個沖突問題:如果一個進程在復制體A的核心X上映射了一個頁面,而復制體B的核心Y在復制體B應用更新之前就在用戶空間訪問了該映射,這個時候就會產生沖突(主要原因在於硬件頁表不像日志那樣只涉及內核空間)。為了處理這一問題,便有了Resolve,一旦出現了頁面映射問題,便推動Resolve過程尋找沖突的映射,若找到沖突的映射則恢復進程此前的操作,若沒有找到,便認為此映射為此前進程無效的讀寫所遺留的映射。
對於Unmap和Adjust操作來說,有一個需要注意的地方,即在處理完映射表上的操作之后,不同節點之間映射表通過log來同步這一過程本身沒有問題,不過卻難以處理TLB中內存映射的同步。為了處理這一問題,文章通過發送處理器內部通斷來同步所有和修改塊相關的進程,使之與日志同步。例如,當一個進程對映射表已有的條目進行修改后,他會發送處理器內部中斷(IPI)使所有節點對日志進行同步,接着對每個核進行遍歷獲得其需要刷新TLB的區域,最后再使對應的TLB條目無效。
文件系統
有關文件系統的實現主要面臨三個問題,接下來一一說明。
首先,文件描述符問題,通常情況下,用戶空間使用文件描述符對文件進行讀取的過程會對內核產生修改(例如文件描述符的偏移),這回大幅度降低整個系統的並行性。為了解決這一問題,NrOS將文件描述符放在了用戶空間,並且在內核狀態下,只進行針對地址的讀寫,這樣保證了內核不必記錄每個文件描述符的偏移位置。
其次,大型文件的讀寫問題,我們的日志是一個循環的數據結構,當系統進行改變大型文件的時候,如果將所改變的內容全部放在日志條目里,可能會產生日志的內存不夠用的情況。為了解決這一問題,NrOS為大的文件在內存中分配緩沖區,並且將其索引放入日志中方便其他節點進行同步。
最后,仍需要考慮一個問題,即在用戶區的讀寫緩沖區在未被其他節點復制之前就被改變造成節點之間讀寫不一致問題。為了解決這一問題,NrOS將所有的讀寫緩沖區事先復制進入內核內存中,這樣便可以在同步的過程中進行固定地址的讀寫。
文件系統讀寫一致性問題
NrOS使用CNR來保證每個節點的讀寫一致性,CNR中有多個日志,根據沖突性對操作進行分組,把每組映射到不同的日志中(具體前面已經說過了)。之后針對文件改名等需要對整個文件系統進行掃描的,對整個系統進行操作(見第二節)。
進程分配管理
NrOS在內核分配層面采用粗粒度的方式為進程分配資源——NR調度器把cpu分配給進程的方式,進程通過系統調用來向內核申請cpu或釋放cpu。而一個cpu被分配給一個進程之后,就會生成一個執行器對象(相當於通常意義上的內核線程),來負責進程的系統調用,以及分配用戶空間和保存寄存器狀態,進程會將執行器保存起來以便重復使用。
NR調度器中維持一個hash表用來把內核進程映射在對應的分配器和對應的cpu,在這個表中用來生成、刪除進程以及分配調度器。
還需要注意一個問題即對進程內存分配的問題,當處理一個需要寫操作分配內存的進程時會造成沖突,同時也需要每個進程統一的虛擬地址。為了解決這一問題,在內存創建前,讀取程序,預先找到其需要寫的位置,為其分配好內存,隨后再創建進程,當需要分配需要寫的內存的時候,直接映射到預先分配好的內存,從而解決沖突問題。
日志同步
這個其實是從一開始就困擾的問題然后到最后文章才說明白,,,
NrOS日志是一個循環的數據結構造成的一個可能出現的問題是,當一個節點因為某些原因一直沒有和日志同步,以至於別的節點無法更新日志。為了解決這個問題,文章提出了兩個解決辦法:首先是無法進行更新的節點對沒有更新的節點發送IPI,使對應節點對日志進行同步,從而使自己可以更新日志。然而這種使用IPI的方法代價十分昂貴,於是大多數情況下采用另一種辦法:即當節點處於空閑狀態時,主動對日志進行同步,從而避免頻繁的IPI。
循環的數據結構還會造成一個問題,即日志中的表項只有在被覆蓋時才會銷毀掉,而一些表現可能有外部的數據引用,倘若在表項被覆蓋時才釋放這些引用就會造成大量的內存浪費。解決辦法則是對每個引用根據未被同步的節點數量設置一個計數器,當計數器清零的時候則釋放對應的資源。