Orleans—一些概念
這是Orleans系列文章中的一篇.首篇文章在此
這個文章聊一聊Orleans的概念.以下文章大部分翻譯自官方教程,還有一些結合實際的應用經驗,並對以前文章留下的坑進行填平.如果有哪個坑沒有填,還請告訴我.
Grain的生命周期:
一個Grain在邏輯上是永遠存在的,並在邏輯上擁有一個不變的標識.程序的代碼永遠不會去創造或者銷毀一個Grain,你可以認為Grain永遠存在於內存中,就等着響應你的請求.當然在物理上,按照需求由Orleans運行時自動的激活一個Grain,如果這個Grain在某一個時間段空閑,它也由Orleans自動的反激活,並從內存中移除.一個Grain的激活與反激活從程序代碼上捕捉不到的,程序代碼只可以從Grain類本身提供的兩個方法參與其中,它們是OnActivateAsync , OnDeactivateAsync.除此之外Grain的一生就處於反激活或者激活狀態,但是不管何時,你都可以假定Grain在silo里存在,放心大膽的激活它. 激活一個Grain,只能通過GetGrain方法,這個方法只有在GrainClient和Grain類內部提供.通過這個方法,我們只能得到一個grain的引用,這個引用不是Grain的物理地址,而是Grain在Orleans體系中的一個邏輯地址.這個引用對於特定的實例是唯一不變的.並且不會兩兩相同.
消息
我們調用Grain引用的方法,實質上是給一個特定的grain實例發送消息,默認Orleans體系保證消息的到達目的地是"至多一次".也就是說如果一個消息因為網絡問題發送失敗了,Orleans不會重新發送.這時候需要在程序代碼中做額外的判斷與處理.
單線程機制
一個消息到達了一個特定的grain實例,這個grain實例把這個消息翻譯成一個task,然后會調用自己內部的TaskScheduler(任務排程器),把這個消息放入到特定的隊列.等待執行.Orleans的內部其實是使用了一個自定義的TPL,這個TPL本質上也是通過標准的TaskScheduler來工作的.從宏觀上說,Orleans體系內的TaskScheduler有兩類,一類是全局的TaskScheduler,一類是Grain實例內部的TaskScheduler.
一個Task,在Orleans里也稱作是workItem,有時候會在日志中看到WorkItem XXX等等內容,可以這樣簡單的認為,消息 task 和workItem,是一個消息在不同的階段的不同的名字.
在類實例內部的TaskScheduler里,workItem排隊等待被執行.由於一個隊列中WorkItem也許會排多個,所以它們這多個又被稱為WorkItemGroup.
這樣每個Grain實例中過的WorkItemGroup,又會在全局中的TaskScheduler里排隊.這個全局的TaskScheduler管理着這些WorkItemGroup組成的隊列(即隊列的隊列).
同時全局的TaskScheduler還管理着一組線程.並給每一個WorkItemGroup安排一個線程執行WorkItemgroup里載明的WorkItem.在統一時間段,一個WorkItemGroup只安排一個線程.
這樣構成了一個"單線程機制".
一個Task,永遠會被安排到正確的隊列中.Orleans永遠不會在執行Task的中途創造另一個Task,所以要求程序代碼在執行Task的中途,不要開辟多線程.不然會報錯或者會破壞單線程機制.如果真的有需要創造額外的task.可以使用一個變量,獲取當前的TaskScheduler.在啟動這個額外的task時,指定這個變量作為參數.其他時候,最好永遠不要再grain內部開辟新的線程.其實用久了你會發現,大部分時候根本不需要線程...啥是線程?能吃嗎?啥是鎖,能做我女朋友嗎?雖然這是嘻話,但是,了解鎖和了解線程對Orleans的編程還是很有幫助的,比如怎么給Orleans破處,破除它的單線程機制(雖然平時用不到)等等奇淫巧技,對於有女朋友的碼農們還是很容易做到的.所以還是早點找到女朋友.
跑遠了,上文說到 消息,task,以及workitem可以認為是消息在不同階段的不同表現形式.那么Orleans是如何做到Orleans客戶端與Orleans服務端通信的呢?Orleans底層是用的tcp協議.Orleans在編譯階段會產生額外的代碼,同時在運行階段,也會產生一些代碼,這些代碼的作用是為了支持發送消息,包括消息的序列化等等一些復雜的概念.前一段時間有新聞說程序員寫了一個程序,這個程序是可以自動寫代碼的…現在我們也用上了,咩哈哈哈…還依然是碼農.
Grain目錄服務
消息一旦到達silo,就會觸發一個Grain的激活(如果之前沒有激活過.)這個激活的動作,是在集群內部隨機發生的.也就是說,Grain實例產生的物理位置是隨機的.Orleans系統維持了一個變量,名字叫做ActivationCountPlacement,它使用這個變量嘗試在集群的各個silo之間平衡激活發生的次數.Orleans還使用了分布式hash表的技術,構建了3. Grain Directory service(Grain目錄服務),如果你不是很優雅的關閉silo,會在日志中看到這個目錄.如下圖
這就要求我們在關閉silo的時候調用正確的函數,特別是在需要存儲grain狀態的情況下.
我一般是使用以下方式關閉一個silo
以前的文章說過允許Grain在接收消息的時候稍微富有彈性,不是剛性的遵循"單線程機制".這個富有彈性的機制就是[reentrant]特性,上文已經說過了.但是這個特性帶來的好處的同時(好處就是稍微防止了一下消息死鎖),也帶來的壞處,壞處就是在實現EventSourcing的時候,它會稍微的破壞一致性.這個在這里只是一提.不做展開.在合適的時候會做說明.
狀態無關的Grain
關於Grain,在Orleans中還有一個概念就是StatelessWorker特性,這個特性標志一個Grain是無狀態的(但是可以有字段保存需要的信息).意思就是啟動StatelessWorker任意多個,都不會感到它們之間有何區別.大家都看過黑客帝國吧.哪個特工史密斯在大雨中復制了自己好多個..沒錯,就是那種感覺.這樣的Grain無論多少個,都他X的是克隆體,所以Orleans處理它的時候與普通的Grain不太一樣.(這個StatelessWorker特性也標明了,這種grain就是工人,純干活的,就是工具類,與狀態無關的),區別是1.可以在集群的多個silo里,每一個silo都創造一個相同標識的Grain.2.針對此類grain的請求,都只在第一個接受到請求的silo里執行.(即請求不會跳轉,在一個集群里,如果你請求一個普通的GrainA,假設標識是123,這個請求的消息也許發往了siloA,但是這個123的實例已經在另一個的siloB里已經被創建了,這樣針對123的請求必須跳轉到此siloB.)3.Orleans會在所有此類grain忙的時候,自動增加一個...實際應用中,會利用特性2,加快請求的處理速度.也可以利用特性1,讓StatelessWorker在自己的字段里攜帶一些常用的數據,在整個集群中擴散.
注意狀態無關並不是無狀態值.
Observers通知
經過了以上的介紹,也許早就有人想到了.所有的消息是從client到silo.如何讓消息從silo到client?Orleans提供了兩個辦法(當然你也可以利用TCP等協議自己實現一個),一個簡單點,一個復雜點.當然這是我自己的觀點.
簡單點的辦法就是使用observer,見多識廣的人已經猜到了Rx這種反應式通知機制.我就認識一個真Rx高手.對Rx的理解非常之深刻,不過Orleans只是借用了它的思想和部分方法.observer是消息從silo到client的一種異步機制.並且是單向的.(即silo不期待任何返回值),官方的例子中有兩個例子展示了如何使用observer.其中有個例子是chirper.可以去觀摩觀摩.
還有個復雜點的通知機制就是stream,所謂流.或許下一步我應該把流加入我的話題列表內.Orleans真是一個嚴肅的框架,它替我們完成了幾乎所有底層的工作,而且設計巧妙,讓我們只需要關注生產實際.而之前介紹的文章中,並沒有真正詳細介紹如何構建一個可以橫向擴展的集群.雖然已經說過一個主從模式的silo集群,但是哪個集群太過於脆弱.不是真正的服務自我發現式的,靈活的集群..下面一個文章我就說說,如何構建一個Orleans的silo集群.這個集群滿足以下特性:
-
服務自我發現.
-
后加入的silo可以被集群接納
-
離線的silo可以被集群排除
-
Orleans客戶端可以充分利用集群內在線的silo
如果能夠滿足以上特點,可以認為這個集群能夠應用於生產實際當中.
今天說的內容,沒有什么實用性的內容.但是這些概念每個都值得好好研究,我這里只是引路.