在dotnet core下去中心化访问HTTP服务集群

一般应用服务都会部署到多台服务器之上,一、可以通过硬件得到更多的并发处理能力;二、可以避免单点太故障的出现,从而确保服务7X24有效运作。当访问这些HTTP服务的情况一般都是经过反向代理服务进行统一处理,这样的好处就访问透明化,统一管理和控制。但存在的问题就是服务处理延时加大,还有就是对小团或公司来说可能没有专门的技术人来规划和管理这些代理服务。接下一来讲一下在.net core下更轻更的一种处理方案,这种方案通过Client自身的功能实现集群化的HTTP服务访问,通过故障迁移和权重分配达到一个无中心化灵活的HTTP集群服务访问(通过它还能访问所有非.net core的HTTP服务)

场景定义

首先有以下一个asp.net core mvc服务

    public class HomeController : Controller
    {
        [HttpPost]
        public int EmployeeAdd([FromBody]List<Employee> items)
        {
            return items == null ? 0 : items.Count;
        }

        [HttpPost]
        public Employee EmployeeEdit(int id, [FromBody]Employee emp)
        {
            Employee record = DataHelper.Employees.Find(e => e.EmployeeID == id);
            if (record != null)
            {
                record.City = emp.City;
                record.Address = emp.Address;
                record.Title = emp.Title;
                record.HomePhone = emp.HomePhone;
                return record;
            }
            return null;
        }

        public object EmployeesGetName()
        {
            return from e in DataHelper.Employees select new { ID = e.EmployeeID, Name = e.FirstName + " " + e.LastName };
        }

        public object Customers(int count)
        {
            List<Customer> result = new List<Customer>();
            if (count > DataHelper.Customers.Count)
                count = DataHelper.Customers.Count;
            for (int i = 0; i < count; i++)
            {
                result.Add(DataHelper.Customers[i]);
            }
            return result;
        }

        public object CustomersGetName()
        {
            return from c in DataHelper.Customers select new { ID = c.CustomerID, Name = c.CompanyName };
        }

    }

代码简化了一下,正常API的服务都部署在多台服务器构建应用集群,一般情况都是通过nginx或其他反向代理服务器接管Client的请求,然后针对负载配置进行转发处理。但接下来需要讲解的是通过开源组件实现无中心化的集群负载调用。

引用组件

首先net core的http client组件并不具这一功能,所以需要引用第三方的一个开源组件BeetleX(组件暂只支持net core 2.1或更高版本) 

定义访问接口

组件支持通过接口的方式来描述HTTP接口服务,接口的访问方式对使用和维护都具有着极大的便利性,以下是针对以上服务描述的接口

    [JsonFormater]
    [Controller(BaseUrl = "Home")]
    public interface IDataService
    {
        [Get]
        Task<DateTime> GetTime();
        [Get]
        Task<string> Hello(string name);
        [Get]
        Task<bool> Login(string name, string pwd);
        [Get]
        Task<List<Employee>> Employees();
        [Get]
        Task<Employee> EmployeeGet(int id);
        [Post]
        Task<int> EmployeeAdd(params Employee[] items);
        [Post]
        Task<Employee> EmployeeEdit([CQuery]int id, Employee emp);
        [Get]
        Task<List<EmployeeName>> EmployeesGetName();
        [Get]
        Task<List<Customer>> Customers(int count);
        [Get]
        Task<List<CustomerName>> CustomersGetName();
        [Get]
        Task<List<Order>> Orders(int? employeeid, string customerid, int index, int size);
    }

组件支持Task和非Task返回值的方法定义,由于基础网络访问是基于异步,所以最好还是定义Task返回值的方法。如果定义了非Task返回值访问组件内部会同步等待完成返回,在并发服务下这不利于线程资源的利用。

集群化访问定义

接口定义好之后,就可以通过接口来调用并把请求调用负载到不同服务器的服务上。这样做首先需要定义一个集群化服务访问对象:

private static HttpClusterApi HttpClusterApi = new HttpClusterApi();

HttpClusterApi对象是线程安全的,所以定义成静态即可;也可以根据服务分类来定义不同的HttpClusterApi(之于内部的工作原理这里就不详细解说了,可以到GitHub上过一步了解)。创建了集群接口对象之后就可以用它来创建接口实例。

private static IDataService DataService;

DataService = HttpClusterApi.Create<IDataService>();

同样接口实例也是线程安的,只需要创建一个即可在不同线程和方法里同时调用。其实这样创建接口后还没能正常使用,因为没有定义相应服务地址,可以通过HttpClusterApi添加不同服务地址:

HttpClusterApi.AddHost("*", Host25, Host29);

以上是所有请求都负载到Host25和Host29,一般情况都不会直这样定义;*的优先级是最低的,只有没有匹配到其他url描述的情况才会匹配*

平均负载

平均负载是一种最常用的方式,主要是把并发请求平均到不同的服务器上;以下是针对employee相关请求的地址负载到5个服务上.

HttpClusterApi.AddHost("employee.*", Host26, Host27, Host28, Host29, Host30);

接下简单地测试一下

            System.Threading.ThreadPool.QueueUserWorkItem(async (o) =>
            {
                while (true)
                {
                    try
                    {
                        await DataService.EmployeeGet(2);
                    }
                    catch (Exception e_)
                    {
                        Console.WriteLine(e_.Message);
                    }
                }
            });

测试结果:

从测试打印信息来看,基本上把请求平均到不同的服务器上,5台服务器的权重值都是10.

权重定义

现实中由于服务器配置不同,服务器运行的服务也有所差异,所以一般情况都不会平均化负载;这时候权重定义就起到一个非常重要的作用,针对不同地址的服务制定不同的重权值这样可以让服务资源得到更好地利用!接下来还是针对以上测试代码,针对不同的服务器设置不同的权重

HttpClusterApi.GetUrlNode("employee.*").Add(new string[] { Host26, Host27, Host28 }).Add(Host29,5).Add(Host30,5);

以上代码分别把Host29和Host30的权重设置为5其他均为10.

定义备份节点

所谓的备份节点,其实就是不参与负载处理,但所有可用点都故障的情况,备份节点就生效。其实在组件中没有所谓的备份节点,不过可以通过0权重这个值来实现。当存在其他重权的节点可用时,0权重的节点是不会存在于重权表中的,只有当所有节点不可用的情况下,0权重的节点才会移入到权重表;当有效节点恢复后0权重的节点也会被移出权重表。

故障迁移和恢复

组件会自动把存在N次连接错的服务地址在权重表中排除出去,所以当服务不可用的情况会短暂有部分请求会引发访问异常;接下来组件会每隔一段时间去检测有问题的服务地址,直到服务连接可用的情况才会把服务地址重新添加回重权表里接受负载轮循处理。

自定义集群信息源

如果在代码中把负载设置写死了,那到增加节点的时候就非常麻烦需要重新发布程序;组件为了解决这一问题制定了一个接口方便使用者制定自己的集群信息来源配置。通过实现INodeSourceHandler并设置到HttpClusterApi即可以动态更新节点信息便于更改和管理。以下是针对负载源实现的一个HTTP获取负载源信息实现

    public class HTTPRemoteSourceHandler : INodeSourceHandler
    {
        private HttpClusterApi mHttpClusterApi = new HttpClusterApi();

        private IHttpSourceApi mRemoteSourceApi;

        public HTTPRemoteSourceHandler(string name, params string[] hosts)
        {
            mHttpClusterApi.AddHost("*", hosts);
            mRemoteSourceApi = mHttpClusterApi.Create<IHttpSourceApi>();
            Name = name;
        }

        public string Name { get; set; }

        public int UpdateTime { get; set; } = 5;

        public Task<ApiClusterInfo> Load()
        {
            return mRemoteSourceApi._GetCluster(Name);
        }
    }

实现是每5秒钟检则一下信息源,集成到HttpClusterApi如下:

     HTTPRemoteSourceHandler remoteSourceHandler = new HTTPRemoteSourceHandler("default", "http://localhost:8080");
     HttpClusterApi.NodeSourceHandler = remoteSourceHandler;
     var result = await HttpClusterApi.LoadNodeSource();

设置INodeSourceHandler后组件就会检配置信息,检测错误或版本没有变化的情况就不会更新配置。以下是针对测编写的一个HTTP配置服务:

如果想得到更详细的代码可以访问 https://github.com/IKende/FastHttpApi
posted @ 2018-12-13 16:52  beetlex  阅读(2912)  评论(13编辑  收藏  举报