zoukankan      html  css  js  c++  java
  • 微服务架构

    微服务架构的简单实现-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

     

  • 相关阅读:
    184. Department Highest Salary【leetcode】sql,join on
    181. Employees Earning More Than Their Managers【leetcode】,sql,inner join ,where
    178. Rank Scores【leetcode】,sql
    177. Nth Highest Salary【leetcode】,第n高数值,sql,limit,offset
    176. Second Highest Salary【取表中第二高的值】,sql,limit,offset
    118. Pascal's Triangle【LeetCode】,java,算法,杨辉三角
    204. Count Primes【leetcode】java,算法,质数
    202. Happy Number【leetcode】java,hashSet,算法
    41. First Missing Positive【leetcode】寻找第一个丢失的整数,java,算法
    删除
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/6666925.html
Copyright © 2011-2022 走看看