微服務架構,一個當下比較火的概念了。以前也只是了解過這方面的概念,沒有嘗試過。想找找.NET生態下面是否有現成的實現,可是沒找到,就花了大半個月的閑暇時間,遵循着易用和簡單,實現了一個微服務框架,我叫它Stardust(星塵),Stardust有三個項目組成:
Stardust.Server是服務端組件,Stardust.Client是客戶端組件,Stardust.ConfigCenterWeb是配置中心,是個MVC web站點。
本文目錄:
在Stardust里,只用了兩個模型,ServerNode和NodeEvent。
ServerNode表示一個服務節點,包括唯一Id,服務名稱,節點地址,服務版本,節點狀態,權重,動態權重,最后心跳時間。
動態權重這個屬性是在實現負載的時候加上去的,並不是設計的時候就想到的,微服務架構中,服務節點應該是可動態的。
節點狀態有三個,Normal(正常),Disconnect(斷開),Disabled(禁用)。
這里要確定兩個概念的,服務節點是指一個服務實例,比如IIS上的一個站點,是個具體的東西;服務則是一個抽象的分組概念,可以為一個服務部署多個服務節點,ServerNode中的服務名稱,就是說的這個概念。
NodeEvent表示服務節點的一個變動事件,包括事件Id(自增,這個后面是有用的),變更的服務節點,事件類型。
事件類型有四個,Register(注冊),Logout(下線),Update(修改),Delete(刪除),這些事件都什么情況會觸發,下面再交代。
基礎組件也是不多的。
任務調度器:是之前寫過的一個組件TaskScheduler,不想多一個引用,就把源碼放進來了。
序列化:用的ServiceStack.Text,這是引用的唯一一個外部類庫。
HTTP通信:本來想自己封裝或引用第三方的,沒想到ServiceStack.Text里有個HttpUtils,寫了好多擴展方法應用到String上,正好!
基礎的模型和組件就這么多了。
.net版的服務端,是要架設到web服務器(IIS)上的。是需要一個web站點,mvc也好,webfrom也好,asp.net空web項目也行,都可以用Stardust.Server成為一個服務節點。
Stardust.Server中提供一個ServiceRouteHttpModule的HTTP模塊,用這個模塊從IIS接管對Stardust服務的請求,所以第一步要在web.config里添加模塊
<system.webServer> <modules> <add name="StardustServiceRoute" type="Stardust.Server.ServiceRouteHttpModule"/> </modules> </system.webServer>
Stardust.Server中提供了一個空接口IStardustService,我們提供的服務類繼承這個接口,寫服務方法實現就行了:
public class User { public string Name { get; set; } } //[StardustName("User")] //默認是類名,如果類名以Service結尾,會把Service去掉 public class UserService : IStardustService { //[StardustName("hello")] //默認是方法名,可以StardustNameAttribute來自定義 public string Hello(string name, int count = 1) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < count; i++) { sb.AppendFormat("Hello,{0}!{1}", name, Environment.NewLine); } return sb.ToString(); } public Task<string> HelloAsync() { return new Task<string>(() => { return "Hello World"; }); } public List<User> UpdateUsers(List<User> list) { foreach (var user in list) { user.Name = "Updated:" + user.Name; } return list; } }
服務方法是指那些本類聲明(基類不算)的、公開的、有返回值的實例方法。
方法的參數有些要求:
可以無參,如:FunName()
可以是多個簡單類型,如:FunName(int id, string name, DateTime dt)
可以是一個復雜類型,如:FunName(SomeParamsObj ps)
ref和out等不做考慮......
服務的注冊放在Global里是個好地方:
注冊的時候,有個版本號,這個版本號在配置中心是可以改的,根目錄默認是"",如果在站點下面添加應用程序就要指定應用程序的根目錄了。
服務節點的實現,編碼方面就是上面三步:1添加Http模塊,2繼承IStardustService寫服務方法, 3注冊服務。
當服務節點啟動了之后,就會和配置中心互動了:
1.當應用程序啟動的時候,會向配置中心注冊服務節點,配置中心根據服務名稱和地址判斷是否是新節點,如果是新的,會添加到數據庫,如果不是,會修改節點信息,兩種情況都會產生一條注冊事件。
2.啟動之后,服務節點定時(5s)向配置中心發送心跳請求,配置中心會更新節點的最后心跳時間;
3.當服務節點關閉的時候,可以向配置中心發送下線請求,比如在Application_End中,但是這個並不靠譜,下線的代碼可有可無,所以再求他法;
4.配置中心定時(10s)檢測那些狀態是正常但是最后心跳時間已經大於8s的節點,如果服務節點返回約定的值,就說明節點是活着,配置中心更新節點的心跳時間,否則會修改結點的狀態為斷開,同時生產一條下線事件。
5.配置中心定時(15s)檢測那些狀態是斷開(只是斷開的,禁用的不檢測),最近15s都不心跳的服務節點,試圖把服務節點拉起,如果拉起成功,就會馬上生成節點注冊事件(當應用程序啟動了也會生成這個事件,可能會重復,不過沒關系,客戶端會處理好的)
經過這么你來我往的交互,服務節點在配置中心就活起來了:
上面都是自動觸發的事件,在配置中心里的操作,也是有事件產生的:
1.如果一個節點不存在,可以手動先添加,這個時候是沒有事件的,新加后節點的狀態是斷開的,這個節點將來可能會被上面第5點說的由配置中心拉起來,也可能應用程序啟動自己注冊。
2.對一個已存在的節點,可以修改地址、版本、狀態、和權重,修改完成會產生一條修改事件。
3.刪除會產生刪除事件。
這些自動或手動生成的事件,是為客戶端獲取最新服務節點狀態使用的。
Stardust是沒有路由的,是客戶端直接調用服務的,所以客戶端有發現和選擇服務節點的能力。
由於服務信息都在配置中心,所以客戶端在調用服務之前,要設置一下配置中心的地址:
StardustClient.SetConfigCenterUrl("http://localhost:85");
一個客戶端可能會調用多個服務。
在客戶端,維護着一個字典,key是服務名稱,value的結構如下:
{ "MaxEventId":287, //最新服務節點事件Id "LastInvokeTime":"2017-4-1 02:05:08", //客戶端最后調用時間 "Nodes":[ { "Id":1, "ServiceName":"server1", //服務名 "Address":"127.0.0.1:8001", //服務節點地址 "Version":"1.25", //版本 "Status":1, //狀態 "Weight":0, //權重 "DynamicWeight":0 //動態計算出來 } ] //節點列表 }
當調用一個服務的時候,先看看是不是已經獲取了該服務的信息,如果沒有,會從配置中心拉取過來這個服務下面所有正常的服務節點信息,然后存起來。這些信息也包含當前服務的節點事件的最大Id。
當調用一個服務的時候,客戶端會在本地更新LastInvokeTime,紀錄最后調用時間。
客戶端會定時(6s)檢測那些在1天內有調用過的服務,然后從配置中心拉去這些服務下面的節點事件(從本地MaxEventId開始),如果有事件的話,就把這些事件依次應用到對應的節點上,同時更新MaxEventId。
應用事件的邏輯:
var localNode = group.Nodes.FirstOrDefault(x => x.Id == evt.ServerNodeId); if (localNode != null) { switch (evt.EventType) { case Common.Enum.NodeEventType.Logout: case Common.Enum.NodeEventType.Delete: localNode.Status = Common.Enum.ServerNodeStatus.Disabled; break; case Common.Enum.NodeEventType.Update: case Common.Enum.NodeEventType.Register: localNode.Status = evt.ServerNode.Status; localNode.Address = evt.ServerNode.Address; localNode.Version = evt.ServerNode.Version; localNode.Weight = evt.ServerNode.Weight; break; default: break; } } else { if (evt.EventType == Common.Enum.NodeEventType.Register || evt.EventType == Common.Enum.NodeEventType.Update) { group.Nodes.Add(evt.ServerNode); } }
當一個服務正好下線了,狀態還沒有同步過來,這個時候客戶端調用了就會有異常的,當在遠程主機主動拒絕連接的時候,會在本地修改節點為禁用狀態,這樣就不會反復調用了,如果那個節點后來又好了,狀態也是會通過事件同步過來的,然后這個節點就又可用了。
客戶端獲得了所調用的服務的節點信息,就可以直接調用服務了。
var client = new StardustClient("server1", "1.1"); var str = client.Invoke<string>("user", "hello", new { name = "Jack", count = 2 }); //var task=client.InvokeAsync<string>("user", "hello", new { name = "Jack", count = 2 }); // 或者異步調用
服務節點注冊的版本是固定的,但是客戶端的選擇應該是靈活的。
基於這個的考慮,我把版本分成兩部分 x.y ,x和y都是整數,x表示不兼容版本,y表示可兼容版本。
如果一個服務有以下節點:
node_a 2.23
node_b 2.23
node_c 2.21
node_d 2.20
node_e 1.24
在客戶端實例化的時候,版本號可以如上面那樣"1.1"指定版本號,更靈活的是在可兼容版本y可以是*,可以在y后面帶上+,-,>,<這四個符號:
2.* :會選擇x等於2,兼容版本里面最高一組版本,[ node_a 2.23 , node_b 2.23 ]
2.21+ :會選擇x等於2,y大於等於21的一組兼容版本,[node_a 2.23 , node_b 2.23 , node_c 2.21]
2.21- : 會選擇x等於2,y小於等於21的一組兼容版本,[node_c 2.21 , node_d 2.20]
1.24< : 會選擇x等於1,y小於24的兼容版本,列表中沒有符合的節點,[]
1.20> : 會選擇x等於1,y大於20的兼容版本,[node_e 1.24]
我們根據版本號篩選出了可用節點列表,下一步是根據權重確定具體的調用節點。
如果可用節點列表為空,就拋出異常;如果只有一個節點,那就是它了;如果不止一個,就要先計算他們的權重。
假設有三個節點,默認權重都是0,這個時候每個節點的動態權重都是1/3,所以選擇的概率是相等的。
如果其中一個節點權重是2,另外兩個是0,那么先算出為全部為零的平均權重1/3,他們總的動態權重是: sum=2+ 1/3 + 1/3,他們的動態權重則分別是 : 2/sum,(1/3)/sum,(1/3)/sum。
獲取到動態權重,經過隨機數定位區間,就可以確定具體的節點了。
每次實例化客戶端的時候,都會通過版本號篩選和計算動態權重,這樣在增刪改服務節點之后,就反映到客戶端了。
起始於2017.3.16凌晨4點左右,突然醒來畫了個圖,上面所說的實現,大都是那1個小時整理的思路:
附源碼地址 http://git.oschina.net/loogn/Stardust
更新日志:
2017-4-13日更新:
java版客戶和服務端:http://git.oschina.net/loogn/stardust4j
2017-4-17日更新:
.net版去掉TaskScheduler ,改用自己封裝的簡單的Timer
2017-4-24日更新:
.net版添加上傳服務契約功能
2017-4-27更新:
2017-4-28更新:
1,服務節點加入平台信息;
2,優化契約,並可以調用測試