在微服务架构风格的系统中, 如果单个微服务垮掉或地址不可访问, 虽然对系统的影响是有限的, 但我们也必须采取一定的手段来保证每个微服务尽量可用; 并且在大并发的情况下, 虽然可以通过 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 网关利用服务中心信息自动路由
好了, 本篇文章关于微服务的高可用性就介绍到这里.
来源: https://www.cnblogs.com/malaoko/p/9760322.html