微服務架構的簡單實現-Stardust


微服務架構,一個當下比較火的概念了。以前也只是了解過這方面的概念,沒有嘗試過。想找找.NET生態下面是否有現成的實現,可是沒找到,就花了大半個月的閑暇時間,遵循着易用和簡單,實現了一個微服務框架,我叫它Stardust(星塵),Stardust有三個項目組成:

Stardust.Server是服務端組件,Stardust.Client是客戶端組件,Stardust.ConfigCenterWeb是配置中心,是個MVC web站點。

本文目錄:

  1. 基礎模型和組件
  2. 服務節點與配置中心
  3. 客戶端與配置中心
  4. 客戶端選擇節點(版本和負載)
  5. 結束

 

一、基礎模型和組件


 在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更新:

1,java版參數上傳契約;
2,java版服務方法定義修改為強類型;
3,java版客戶端調用可直接返回強類型;

 2017-4-28更新:

1,服務節點加入平台信息;

2,優化契約,並可以調用測試

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM