微服务实战(八):落地微服务架构到直销系统(服务高可用性)

在微服务架构风格的系统中,如果单个微服务垮掉或地址不可访问,虽然对系统的影响是有限的,但我们也必须采取一定的手段来保证每个微服务尽量可用;并且在大并发的情况下,虽然可以通过EDA消息队列处理的方式提高吞吐量,但仍然需要WebApi能够更加高效的侦听用户请求,处理消息,即使在某个服务短暂不可用的情况下。本篇文章主要来详细讲一讲要保证微服务的高可用性,可以通过哪些手段来实现。

 

一、保证微服务负载可用

这里的问题指的是当某个微服务或者微服务依赖的持久化存储出现不可访问时,会造成此块服务不可用,我们需要有一定的手段能够尽量避免这个问题;为了达到这个目标,通常可以从4个方面来解决。

1.数据库高可用

现代的关系型数据库系统或NoSql通常是作为微服务的持久化存储机制的。当数据库所在的服务器、数据服务或数据库故障或不可用时,会造成业务中断;所以我们应该利用数据库产品本身的高可用机制来解决这个问题,这里以SQL Server 2016关系型数据库为例。

SQL Server 2016数据库服务提供了一种SQL AlwaysOn的高可用机制。SQL AlwaysOn是将多台SQL Server组合成一个虚拟的SQL Server,然后通过SQL AlwaysOn的功能将需要能够自动转移故障的数据库同步到多台SQL Server上。当WebApi连接数据库服务时,连接的是虚拟IP和端口,然后SQL AlwaysOn会自动将数据访问请求定向到主物理SQL Server上;当主服务器垮掉时,会自动转移数据服务到一台从数据库服务器上,从数据库服务器自动成为新的主数据库服务器,后续的WebApi连接虚拟IP和端口时,会自动连接到新的主数据库服务器上,这个阶段对WebApi来说是完全透明的。在SQL Server 2016中,AlwaysOn的管理界面大致如下,作为开发人员或架构师,了解即可,通常这是由运维团队管理的。

 

2.微服务高可用

通常我们会将某个微服务WebApi部署到物理主机、虚拟机或其他形态的主机(比如docker)的Web Server服务上。这里通常会有两个方面的原因造成微服务无法访问,一是微服务所在的Web Server或主机停止响应或关机、二是微服务并发访问量太大,造成资源大量占用,无法响应用户请求。

除了前面系列文章讲解的软件架构解决外,我们还需要配合另一个机制能够尽量保证微服务高可用,这个机制就是NLB(网络负载均衡)。

如果你的WebApi主机在内网,可以通过F5等硬件设备提供NLB支持,如果你的WebApi部署在云端,可以使用云端供应商提供的NLB相关服务提供NLB支持。NLB是将多台Web服务器组合成一个虚拟的Web服务器,当然还要通过端口组织。通过文件复制功能,比如Windows Server自带的DFS的功能将多台Web服务器承载相同的WebApi保持WebApi内容一致。当前端调用WebApi服务时,连接的是NLB上配置的虚拟IP和端口,然后根据NLB的配置(有根据Web服务器负载情况路由到请求少的主机上;有根据每个请求自动轮询每个主机;有根据某个会话总是请求到特定主机),将前端的请求路由到合适的WebApi主机上。在阿里云上,NLB的管理界面大致如下,作为开发人员或架构师,了解即可,通常也是由运维团队管理的。

 

3.重试策略

无论是数据库还是WebApi,因为网络或服务等原因,可能会出现瞬间故障,也就是在很短的时间内,临时不可访问。如果出现这种情况,我们就应该有重试机制,无论是数据库连接的重试,还是调用WebApi的重试。

a.数据连接的重试

在一些第三方的数据访问库或ORM框架中,通常都提供了数据连接重试的功能,这些功能通常都能实现如果数据访问不可用,要重试连接几次,每次重试的间隔是多长。示例代码如下:

protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder)
        {           

            optionBuilder.UseSqlServer("Server=localhost;Database=DDD1OrderDB;User ID=DDD1user;Password=password",
                sqlServerOptionsAction:p=> {
                    p.EnableRetryOnFailure(
                        maxRetryCount:5,
                        maxRetryDelay:TimeSpan.FromSeconds(1),
                        errorNumbersToAdd:null
                        );
                });           
        }

b.调用WebApi的重试

无论是前端框架还是后端框架,通常都提供了一些库和方法可以使用http的方式调用WebApi。我们可以按照需求扩展这些库,能够在调用WebApi不可用时,重试几次。后端代码调用WebApi重试代码:

public interface IHttpClient
    {
        Task<HttpResponseMessage> GetAsync(string requesturi);
        Task<HttpResponseMessage> PostAsync(string requesturi, HttpContent content);
    }
    public class ReHttpClient : IHttpClient
    {
        private HttpClient client;
        private PolicyWrap policywrap;
        public ReHttpClient(Policy[] policies)
        {
            client = new HttpClient();
            policywrap = Policy.WrapAsync(policies);
        }
        private Task<T> HttpInvoker<T>(Func<Task<T>> action)
        {
            return policywrap.ExecuteAsync(() => action());
        }
        public Task<HttpResponseMessage> GetAsync(string requesturi)
        {
            return HttpInvoker(async () =>
            {
                return await client.GetAsync(requesturi);
            });
        }
        public Task<HttpResponseMessage> PostAsync(string requesturi, HttpContent content)
        {
            return HttpInvoker(async () =>
            {
                return await client.PostAsync(requesturi, content);
            });
        }
    }
 public class ReHttpClientFactory
    {
        public ReHttpClient CreateReHttpClient() =>
            new ReHttpClient(CreatePolicies());

        private Policy[] CreatePolicies() => new Policy[]
        {
            Policy.Handle<HttpRequestException>()
            .WaitAndRetryAsync(6,retry=>TimeSpan.FromSeconds(1)+TimeSpan.FromMilliseconds(new Random().Next(0,100))),
            Policy.Handle<Exception>()
            .WaitAndRetryAsync(6,retry=>TimeSpan.FromSeconds(1)),
            Policy.Handle<HttpRequestException>().            
        };
    }

 

4.断路器模式

当某些故障是非瞬间故障时,一直重试通常是无意义的,而且也消耗资源。当重试到达一定的次数时,可以判断为非瞬间故障,断路器被触发,则不再重试;断路器恢复后,则可以重试。

CircuitBreakerAsync(5,TimeSpan.FromMinutes(1))

 

二、保证微服务地址可用

前端通常通过域名或IP地址作为前缀来访问特定的微服务WebApi的接口。在IT运维调整的情况下,微服务所在的域名或IP地址可能会发生变化,这样前端用户在拿到新的域名或IP地址前,将无法正常调用服务。

为了解决这个问题,我们就需要将微服务通过一个API网关组织起来。API网关会手工或自动配置它所管理的微服务的具体地址,当前端直接调用的API网关的服务时,API网关会根据配置来正确路由请求到特定域名或IP地址的服务。

 

1.API网关手工配置所路由的WebApi

这种情况需要在API网关手工添加某个服务请求应该路由到哪个特定的域名或IP地址的WebApi接口。手工配置的Json配置文件内容如下:

这里的Upstream指的就是前端调用API网关的特定服务时,Downstream指的就是路由到哪个特定的WebApi。有了配置文件后,就可以使用相关的API网关库加载配置文件到API网关的WebApi中。

 

2.WebApi自动注册地址信息

如果总是通过手工配置映射信息,还是比较麻烦。我们可以让WebApi自己将信息注册到一个服务中心中,然后API网关利用这个服务中心的信息实现请求的自动路由。

a.服务中心提供注册功能

public static class AppBuilderExtension
    {
        public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app,
            IApplicationLifetime lifetime,ServiceEntity serviceEntity)
        {
            var consulClient = new ConsulClient(x => x.Address = 
            new Uri($"http://{serviceEntity.ConsulIP}:{serviceEntity.ConsulPort}"));
            var httpCheck = new AgentServiceCheck()
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),
                Interval = TimeSpan.FromSeconds(10),
                HTTP = $"http://{serviceEntity.IP}:{serviceEntity.Port}/api/health",
                Timeout = TimeSpan.FromSeconds(5)
            };
            var registration = new AgentServiceRegistration()
            {
                Checks = new[] { httpCheck },
                ID = Guid.NewGuid().ToString(),
                Name = serviceEntity.ServiceName,
                Address = serviceEntity.IP,
                Port = serviceEntity.Port,
                Tags = new[] { $"urlprefix-/{serviceEntity.ServiceName}" }
            };
            consulClient.Agent.ServiceRegister(registration).Wait();
            lifetime.ApplicationStopped.Register(() =>
            {
                consulClient.Agent.ServiceDeregister(registration.ID).Wait();
            });
            return app;
        }
    }

b.WebApi注册到服务中心

ServiceEntity serviceEntity = new ServiceEntity
            {
                IP = Configuration["Service:Address"],
                Port = Convert.ToInt32(Configuration["Service:Port"]),
                ServiceName=Configuration["Service:Name"],
                ConsulIP = Configuration["Consul:IP"],
                ConsulPort = Convert.ToInt32(Configuration["Consul:Port"])
            };
            app.RegisterConsul(lifetime, serviceEntity);

c.API网关利用服务中心信息自动路由

好了,本篇文章关于微服务的高可用性就介绍到这里。

QQ讨论群:309287205 

微服务实战视频请关注微信公众号: