轉載地址 http://devopstarter.info/-fan-yi-salt-xin-tong-xin-jia-gou-salt-raet/
By 譯者
Saltstack官方在salt 2014 介紹視頻中引入了salt raet概念,salt raet是繼Salt-Zeromq, Salt-Ssh之後的第三套通信體系,全名爲Reliable Asynchronous Event Transport,即基於事件的可靠異步傳輸協議。
原文地址:https://github.com/saltstack/raet
正文
爲什麼要研發Salt Raet?
現代大規模的分佈式應用架構,其組件均是分佈在互聯網上的多個主機和多個CPU內核,往往是基於一個消息或事件總線,允許不同的分佈式組件之間相互異步通信。通常情況下,消息總線是某種形式的消息隊列服務,如AMQP或ZeroMQ。消息總線支持通常被稱爲發佈\訂閱模式的信息交互方式。
一個具備完整功能的消息隊列服務擁有很多的優點,然而,其存在的缺陷之一便是大規模應用環境下的性能問題。
一個消息隊列服務完成兩個獨立而互補的功能。
第一個是在互聯網上消息的異步傳輸。
第二個是消息隊列的管理,這便是通過隊列實現的諸如消息的識別、跟蹤、存儲,以及發佈者和訂閱者相互之間的消息分發。
對於衆多應用程序而言,一個消息隊列服務的優勢之一便是通過API能很好的隱藏服務背後的細節,也就是其來自各個客戶端的消息隊列管理的複雜性。
但是,消息隊列服務存在的主要缺陷便是大規模應用環境下的擴展性,其消息的容量、消息的定時以及內存、網絡和CPU處理能力相關的需求往往成爲關鍵,而客戶端對於服務性能的調整往往顯得無能爲力。MQ服務在分佈式的應用環境下通常會成爲瓶頸所在,而更復雜的MQ服務,如AMQP,則會在高負載下變得不可靠。
異步事件的網絡傳輸和消息隊列管理之間功能的分離使得每個功能模塊可以獨立的調整各自大規模環境下的性能。
絕大部分的MQ服務是基於TCP/IP網絡傳輸協議。TCP/IP對於網絡通信有顯著的延遲效應,因而不太適合用於分佈式事件驅動式應用程序交互的異步特性。這其中主要的原因便是在於TCP/IP爲了支持流傳輸針對連接的建立和連接的關閉以及失敗連接的處理。從根本上來說,TCP/IP是基於大規模的連續數據流作出了諸多優化,而不太適用於大量小規模異步事件或消息的傳輸。其在小規模應用系統下不成問題,而一旦達到一定規模,相關的通信特徵的差異問題將會凸顯。
UDP/IP 低延遲和無連接的特性註定它更適用於許多小規模異步消息的傳輸。UDP/IP本身的缺點在於它是不可靠的傳輸協議。
這裏所需要的便是一個適配的針對UDP/IP協議增添可靠性而同時無損其低延遲及擴展性的傳輸協議。
一個事務型協議,比流協議更適用於爲異步事件提供可靠傳輸。
進一步來說,由於大部分的MQ服務是基於TCP/IP協議,他們也更傾向於使用HTTP或者保證安全通信的TLS/SSL。雖然使用HTTP能夠輕鬆的提供基於Web的集成系統,但是長遠來說它也會成爲高性能系統的瓶頸所在,TLS對於一個安全系統而言,其性能和漏洞兩方面也同樣存在問題。
橢圓曲線加密,另一方面來說,在相對於其他實現方法更低的性能需求的前提下增強了系統的安全性。LibSodium提供了一個開源的橢圓曲線加密庫,用於驗證和加密支持。CurveCP協議基於LibSodium提供了一個引導安全網絡信息交互的握手協議。
最後,在分佈式併發事件驅動的應用環境下管理和協調處理器資源(CPU、內存、網絡)的一個最佳途徑便是使用一種稱爲微線程的東西。一個微線程應該說是一個程序語言級的特性,它在不比函數調用消耗更多資源的情況下實現了代碼邏輯上的併發。
微線程使用協同工作的多任務處理來取代線程和/或進程,其避免了許多諸如資源競爭,上下文切換,和進程間通信的複雜性,同時提供了更高的性能。
由於所有協同工作的微線程均運行於一個進程,也就造成一個簡單的微線程應用的資源調用被限制在一個CPU核心。爲了使得所有的CPU核心均得到充分的利用,應用程序需要能夠爲每個CPU核心運行至少一個進程。這就需要同一臺主機的進程間通信。但是不同於傳統的多進程處理方式,即一個進程完成一個邏輯併發的功能,一個基於微線程的多進程程序不再使用一個微線程處理一個邏輯併發功能的模式,而微線程的總數是取決於總的進程的最小數目的限制,並且其不超過CPU的核心數量。這將優化CPU的處理能力,同時最大限度地減少進程上下文切換的開銷。
一個使用這樣的微線程-多進程架構平臺的典型例子便是Erlang。事實上,Erlang模式的成功爲RAET方案的可行性提供了強有力的支持。 進一步來說,一個潛在的問題可能是:我們爲什麼不使用Erlang呢? 很不幸的是,Erlang生態系統跟Python相比較而言有些時候顯得有些侷限性,而它本身則使用了一種不太友好的語法結構。RAET的設計實現目標之一便是充分集成現有的Python生態系統豐富的類庫和知識集,同時又能夠方便的開發一個基於微線程-多進程架構模型的分佈式應用程序。我們的終極目標便是想兩全其美。
RAET設計用於藉助微線程-多進程應用框架提供在互聯網上安全可靠的、可擴展的異步消息\事件傳輸,其使用UDP協議來完成主機間的通信,LibSodium來完成認證、加密,CurveCP握手協議來達成安全引導。
相應的隊列管理和微線程應用程序的支持由Ioflo提供。RAET可以算作是Ioflo的一個互補項目,它使得多個Ioflo程序可以通過網絡結合在一起,作爲一個分佈式程序的一部分協同工作。
導致開發RAET的一個主要驅動因素便是使得Saltstack具備更好的擴展性的需求。Saltstack是一個使用Python編寫的遠程執行和配置管理平臺。Saltstack使用Zeromq作爲它的消息總線或者說消息隊列服務。Zeromq是基於TCP/IP傳輸協議實現的,因而也存在上述相應的TCP/IP基礎架構下的延遲及非同步性問題。此外,因爲Zeromq是通過一個特殊的“套接字”將隊列管理和傳輸集成在一起,其大規模應用環境下的隊列的獨立傳輸性能也成爲問題所在,甚至於跟蹤Bug也存在一定的困難。
安裝
當前的RAET提供Pypi安裝方式,在類Unix系統下通過pip指定如下命令來完成安裝:
# pip install raet
介紹
當前的Raet支持兩種通信方式:
主機間通過UDP\IP協議套接字通信;
通過Unix域(UXD)套接字實現的同一主機進程間通信。
一個基於Raet的應用程序架構如下圖所示:
組件的隱喻命名
下列組件的形象命名僅僅是爲了保持一致性而設計,與Ioflo並不衝突。
Road,Estates,Main Estate
UPD通道便是一個“Road”;
一個Road的成員便是“Estates”(就像現實中房屋前方的道路);
每個Estate擁有一個唯一的UDP主機端口地址“ha”,一個唯一的字符串“name”和一個唯一的數字ID“eid”;
一個Road上的Estate便稱爲Main Estate;
Main Estate允許其他Estates通過Join方法(key交換)加入到Road上並且允許(CurveCP)相互之間的信息交易;
Main Estate同樣負責擔任其他Estates的消息路由。
Lane,Yards,Main Yard
每個Estate都可以是一個“Lane”,這便是一個UXD通道;
一個Lane的成員便是“Yard”(簡單來說便是一個Estate的細分點);
一個Lane的每個Yard成員都有一個唯一的UXD文件名稱、主機地址‘ha’和一個唯一的字符串“name”。這個類通常也有一個Yard的數字ID“yid”用於生成Yard name但是它不是Yard實例的屬性之一;
Lane name和Yard name結合起來便可以形成一個唯一的文件名,它是UXD的主機地址‘ha’;
一個Lane上的Yard便是一個Main Yard;
Main Yard負責形成Lane並且允許其他Yards加入到該lane;在此之前這還沒有一個正式的處理過程。當前這會設置一個由Main yard維護的標誌,其將會把任何事先未在Yards列表裏的Yard發送過來的數據包Drop掉。另外,文件權限的設置可以用來阻止僞Yards與Main Yard交互。
Main Yard還負責其他Lane上的Yards之間的消息路由。
運行IoFlo
每個Estate UDP接口運行於一個在單個IoFlo House上下文中工作的RoadStack(UDP套接字)(因此可以把運行UDP Stack的House看作是Estate組成的莊園別墅);
每個Yard UXD接口運行於一個在單個IoFlo House上下文中工作的 LaneStack(Unix域套接字)(因此可以把運行UXD Stack的House看作是Estate的附屬Houses、Tents或Shacks);
“莊園”House的特殊之處在於其可以運行在針對Estate的UDP Stack和針對Main Yard的UXD Stack兩套環境下;
運行Main Estate UDP Stack的House可以被認爲是Mayor's House;
在上下文中,一個House便是一個數據存儲。共享的數據存儲可以被以一個點號分隔路徑的唯一共享名稱定位。
路由
鑑於上面所描述的Ioflo運行架構,其依照如下方式完成路由:
爲了定位一個特定的Estate,Estate name需要相應的指定;
爲了定位一個Estate下的特定Yard,Yard name需要相應的指定;
爲了定位一個House下的特定Queue,Share name需要相應的指定。
UDP stack 將Estate name映射到UDP主機地址和Estate ID,而UXD stack 將Yard name映射到UXD主機地址,任意IoFlo行爲的存儲將Share name映射到共享引用。
因此,路由是從:在一個源Share、源Yard或是一個源Estate中的隊列定義好的一個源節點到:一個源Share、源Yard或是一個源Estate中的隊列定義好的目的節點。
這裏需要兩個三選一的節點,一個是源節點,另一個則是目標節點
源節點(Estate name,Yard name,Share name)。
目標節點(Estate name,Yard name,Share name)。
如果三合一中的任一元素均是None或者空,那麼便會使用默認參數值。如下便是一個路由本身信息的消息正文樣例。
estate = 'minion1'stack0 = stacking.StackUxd(name='lord', lanename='cherry', yid=0) stack1 = stacking.StackUxd(name='serf', lanename='cherry', yid=1) yard = yarding.Yard( name=stack0.yard.name, prefix='cherry') stack1.addRemoteYard(yard) src = (estate, stack1.yard.name, None) dst = (estate, stack0.yard.name, None) route = odict(src=src, dst=dst) msg = odict(route=route, stuff="Serf to my lord. Feed me!") stack1.transmit(msg=msg) timer = Timer(duration=0.5) timer.restart()while not timer.expired: stack0.serviceAll() stack1.serviceAll() lord Received Message { 'route': { 'src': ['minion1', 'yard1', None], 'dst': ['minion1', 'yard0', None] }, 'stuff': 'Serf to my lord. Feed me!'}
UDP/IP Raet協議的具體細節
UDP Raet協議便是基於一個預編碼的隱喻命名約定,換句話說,就是estates 關聯到一個Road。核心的對象便是由如下包提供:raet.road
Road Raet 生產環境UDP/IP端口
Manor Estate端口爲4505,而其他Estates端口爲4510
數據包格式
Raet使用一個帶有數個字段的排序好的字典數據來初始化一個數據包的數據,而其實大多數的字段都共享在下面頭部數據格式,因此僅僅有少量唯一的字段會顯示在這裏。
唯一的數據包字段
sh: 源主機IP地址 (ipv4)sp: 源主機IP端口dh: 目的主機IP地址(ipv4)dp: 目的主機IP端口
數據包的包頭格式
數據包頭部的.data部分是一個排序好的字典數據,其常常用於創建一個準備傳送的數據包或是收取一個數據包的相應字段。什麼字段應該被包含到數據包頭依賴於數據包頭的類型。
Header的編碼
當前Raet支持三種類型的頭部編碼格式。
RAET 本地格式。 這便是一個在可讀性和大小方面做過權衡和優化的精簡的ASCII文本格式。該模式作爲RAET通信的默認選項。JSON 數據格式。 這是一個最可視化的格式而且具備一些兼容性上的優勢。 二進制 數據格式。 這個方案在當前還沒能完全實現。一旦協議達到一個更成熟的階段並且保證沒有任何的頭部變化(或者只是少量), 那麼我們將會提供一個精簡後的二進制數據格式。
當頭部的類型爲json = 0時,某些優化措施將會用於精簡頭部的長度。
頭部的字段key爲兩字節(bytes)長。 如果一個頭部的字段值爲其默認值,那麼它的字段將不包括那些編碼爲十六進制的字符串值。 標誌位在字段‘fg’中編碼爲雙字符的十六進制字符串。
包頭的數據字段
ri: raet id Default 'RAET'vn: Version (Version) Default 0pk: Packet Kind (PcktKind)pl: Packet Length (PcktLen)hk: Header kind (HeadKind) Default 0hl: Header length (HeadLen) Default 0se: Source Estate ID (SEID)de: Destination Estate ID (DEID)cf: Correspondent Flag (CrdtFlag) Default 0bf: BroadCast Flag (BcstFlag) Default 0si: Session ID (SID) Default 0ti: Transaction ID (TID) Default 0tk: Transaction Kind (TrnsKind)dt: Datetime Stamp (Datetime) Default 0oi: Order index (OrdrIndx) Default 0wf: Waiting Ack Flag (WaitFlag) Default 0Next segment or ordered packet is waiting for ack to this packet ml: Message Length (MsgLen) Default 0Length of message only (unsegmented) sn: Segment Number (SgmtNum) Default 0sc: Segment Count (SgmtCnt) Default 1sf: Segment Flag (SgmtFlag) Default 0This packet is part of a segmented message af: All Flag (AllFlag) Default 0Resend all segments not just one bk: Body kind (BodyKind) Default 0ck: Coat kind (CoatKind) Default 0fk: Footer kind (FootKind) Default 0fl: Footer length (FootLen) Default 0fg: flags packed (Flags) Default '00' hs2 char Hex string with bits (0, 0, af, sf, 0, wf, bf, cf) Zeros are TBD flags
正文的數據格式
正文的.data部分是一個使用JSON或MSGPACK序列化好的映射。
數據包的組成成分
每個數據包有4個組成部分,而其中一些可能爲空,它們分別是:
Head Body Coat Tail
頭部是必要的部分,其提供數據包處理所需的各種頭部字段。
尾部提供用於識別數據包源的認證簽名,從而表明它的內容沒有被篡改過。
正文部分是整個數據包的內容上下文,一些諸如ACKs和NACKs的數據包一般不需要正文部分。一般來說,正文是已經序列化並排序好的Python字典數據,正文中排序好的數據字段能夠讓解析和調試擁有一個一致的視圖。
表層部分是正文的加密版本,其加密類型是基於CurveCP實現的。如果一個數據包存在Coat部分,那麼其正文將會被封裝在數據包的表層。
包頭的具體細節
Json編碼格式
包頭是一個ASCII安全的JSON編碼格式的有組織的Python字典。包頭的末端是一個用以兩對回車換行符開頭的空行。
/r/n/r/n 10 13 10 13 ADAD 1010 1101 1010 1101
回車換行符及換行字符一般不能出現在JSON編碼中,除非它們用反斜槓分隔開,因此在一個合法的JSON格式數據中不能出現4字節的組合,因爲他們沒有多字節的unicode字符使其成爲一個唯一的數據包頭終止符。
這就意味着包頭必須是ASCII安全編碼以至於其不允許出現多字節的utf-8格式的字符串。
RAET本地編碼格式
RAET本地編碼格式的數據包頭由換行分隔的數據行組成。而包頭的每一行都包含一個兩字符的字段標識符,緊接着是一個空格跟這個字段的ASCII十六進制編碼的二進制數據,後面再跟一個換行符。包頭的末尾用一個空行表示,換句話說,就是一對換行符。
二進制編碼格式
其包頭由一個預定義好的固定長度的字段集合組成。
會話
會話控制對於一個系統的安全性至關重要。Raet希望一個會話打開後,諸多的消息事務均在會話中完成。
會話ID SID稱爲si
快速開啓一個會話
分層:
OSI 層次模型
7: 應用層:
格式: 數據 (應用接口數據緩衝棧等)
6: 表示層:
格式: 數據 (加密-解密成獨立的主機數據格式)
5: 會話層:
格式: 數據 (主機間通信,身份驗證,組)
4: 傳輸層:
格式: 數據段 (消息的可靠傳輸, 事務, 分段, 錯誤檢測)
3: 網絡層:
格式: 數據包/數據流 (定位和路由)
2: 鏈路層:
格式: 數據幀 (保證每一幀的通信連接的可靠性,介質訪問控制層)
1: 物理層: 數據位 (通信連接的數據傳輸並不可靠)
數據鏈路層對於Raet來說是透明的;
網絡層包含主機的IP地址和UDP端口信息;
Raet在傳輸層通過事務、數據包認證和尾部簽名的相互搭配提供了可靠的數據傳輸;
會話層便是會話ID爲完成簽名而進行的密鑰交互認證,分組便形成了Road;
表示層完成對數據包正文的加密-解密及序列化-反序列化工作。
應用層便是正文的數據字典。
數據包的簽名和認證技術實現上來說可以在傳輸層或者會話層完成。
UXD消息
RAET UXD 消息(每個分段)被限制在與RAET UDP消息同等尺寸大小(大概16Mb)。
UXD消息可以有如下的數據包頭格式並緊接着一個序列化好的消息正文字典,然而當前僅有JSON數據的格式得以實現。
1) JSON 頭部: “RAET\njson\n\n” 緊接着一個統一JSON格式的消息正文的數據字典;
2) msgpack 頭部: “RAET\npack\n\n”緊接着一個統一的MSGPACK格式的消息正文的數據字典。