微服务之可用性设计

302 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第25天,点击查看活动详情

可用性设计

3.1 可用性概述

3.1.1 可用性描述

可用性:是关于系统可以被使用的时间的描述,以丢失的时间为驱动。

可用性的衡量标准通常是以N个9来量化的。

可用性等级表:

3.1.2 是什么降低了可用性
  • 发布。当应用需要升级的时候,为了更好的用户体验,应用不能中断,如果需要迁移数据,会导致整个流程非常复杂。为了降低复杂度和成本,我们通常会暂时中断服务。
  • 故障。如内存溢出。
  • 压力。流量突然增大会造成系统宕机。
  • 外部强依赖。如果外部依赖的服务发生故障,则会导致调用异常。

3.2 逐步切换

3.2.1 影子测试

影子测试是一种常用的生产环境中通过流量复制、回放、对比的测试方法。先同步新老数据库内的数据,在不影响老服务的情况下,在负载均衡的位置记录请求日志,通过日志回放服务向新服务发送请求,新服务正常处理业务逻辑后入库,最后对比验证服务、两个库之间的数据差异。如果比对都正确,说明新服务和老服务逻辑上是等价的。

3.2.2 蓝绿部署

在生产环境中,除了正在运行的环境(蓝色环境),还需要冗余一份相同的环境(绿色环境)。如果要将服务由v1升级到v2,则先在绿色环境上部署,测试通过后将流量、路由指向绿色环境,一旦发生故障需要回退时,只需要切换到蓝色环境即可。

虽然听起来不错,但是需要注意以下细节:

  • 最好有自动化的基础设施支持
  • 全面的监控
  • 两套环境隔离风险,有互相影响的风险
  • 难点是数据结构发生变化时,如何同步数据,故障后如何回滚
  • 切换时需要优雅的终止,禁止直接kill进程
3.2.3 灰度发布、金丝雀发布

金丝雀发布和灰度发布很像。

金丝雀发布: (1)在负载均衡列表中摘掉一个节点,作为“金丝雀”服务器。
(2)在“金丝雀”服务器上部署新版本
(3)进行自动化测试
(4)将“金丝雀”节点添加到负载均衡列表上 (5)如果发生故障,则回滚
(6)如果没有问题,则逐步升级剩余的其他节点。

灰度发布的意义在于:

  • 减少波及的范围
  • 尽早得到用户的反馈

一般互联网公司还会有独立的灰度发布引擎,由运维人员设置规则,可以使内部员工用户先进入新版本进行测试。

流程如下: 内部员工-》外部1%用户-》5%用户-10%用户-》全网发布

3.3 容错设计

3.3.1 消除单点

多节点部署

3.3.2 特性开关

当某个功能有问题时,可以通过开关关掉。

3.3.3 服务分级

服务分级实际上就是服务的标签,表示服务的关键程度。

3.3.4 服务降级

降级是指为了保障核心功能,利用目前有限的资源,通过开关关闭非核心服务。

通常有哪些方式呢?

  • 关闭某个功能
  • 请求短路,直接返回缓存结果
  • 简化流程,直接放弃某个操作。如给用户发注册成功短信
  • 延迟执行,停止定时任务,如某些结算

降级的前提是进行分级,需要在设计阶段明确降级的条件,是吞吐量太大了,还是响应时间太长了,或者是某个依赖服务不可用了。

降级的方法如下:

  • 页面加开关,通过js控制是否隐藏
  • 关闭低级别服务前端页面。例如一些运营系统
  • 关闭定时任务。有一些非核心的定时任务可以延迟在跑
  • 预先定义降级逻辑。在配置中心配置一个变量,预先定义好变量的含义,例如变量值为3,则控制3级以下的服务不可调用
  • 降低精确度。例如在电商中库存可以显示为无货、有货,而不是具体的数量。

总之,降级是不得已而为之的,至少比宕机的用户体验好。

3.3.5 超时重试

需要考虑如下参数:

  • 超时时间
  • 重试总次数
  • 重试的间隔时间
  • 重试间隔时间的衰减度

方案:基于spring-retry进行重试

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.3.0</version>
</dependency>

方案:基于Guava-retrying进行重试

<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>
3.3.6 隔离策略

隔离是为了发生故障时,限制传播范围。

  • 线程池隔离
  • 进程隔离
  • 集群隔离
  • 用户隔离
  • 租户隔离

通过Hystrix实现隔离。

3.3.7 熔断器

可以配置超时时间、一个统计窗口内失败多少次熔断、熔断多少秒后去重新尝试、失败率达到多少熔断等。

3.4 流控设计

3.4.1 限流算法

固定窗口算法

对于限流来说,最简单的方法就是通过一个变量记录单位时间内的访问次数。例:如果一分钟内请求次数超过1000,那么拒绝1000以后发过来的请求,1分钟后次数归零。

但是这个算法有个漏洞,就是在次数清零的前后,是可能突然各涌入1000的请求的,就会有2000的请求并发。

漏桶算法

漏桶算法是控制数据注入网络的速率,平滑网络上的突发流量。

漏桶算法简单描述如下:

  • 水(请求)先进入漏桶(队列)
  • 漏桶(队列)以一定的速度出水(请求)
  • 水(请求)过大会直接溢出(丢弃数据包)

漏桶算法可以看作是一个先进先出的队列。当队列填满的时候,抛弃新请求,队列不保证某个时间点内请求一定会得到处理。往往当消费端处理能力有限时,通过消息队列削峰。例如秒杀场景:如果最后只秒杀一个商品,那么队列保留适当的请求,就能保证结果成功,不必处理所有请求。

令牌桶算法

令牌桶控制的是一个时间窗口内通过的数据量,通常以QPS、TPS衡量。

令牌桶简单描述如下:

  • 每秒会有x各令牌放入桶中
  • 桶中最多放n各令牌,如果桶满了,则新放入的令牌丢弃
  • 当一个m字节的数据包到达时,消耗m个令牌,然后发送数据包。
  • 如果桶中可用令牌小于m个,则该数据包将被缓存或丢弃。

令牌算法和漏桶算法不一样,令牌桶允许突发流量,只要有令牌就可以执行。

3.4.2 流控策略

在分布式系统中,每个环节都要考虑流控。限流一般是根据压测结果、生产环境上的表现对各个服务进行设定。

通常,下面几个重要的节点需要考虑。

  • 请求入口处。如Nginx
  • 业务服务入口处
  • 公共基础服务处
3.4.3 基于Guava限流
3.4.4 基于Nginx限流

连接数限流模块:ngx_http_limit_conn_module

请求限制模块:ngx_http_limit_req_module

3.5 容量预估

传统的压测方式是在测试环境下进行的,针对场景进行数据模拟,需要开发、测试人员根据线上的场景,评估可能出现的情况。但是这种做法非常不准确,很难模拟出接近生产环境的场景和数据。

互联网公司普通采用全链路压测的方式。全链路压测平台在请求入口进行真实的流量复制,为了加大压力,可以通过TCPCopy的参数调节。在数据库一侧通过影子表进行隔离,影子表和生产表建立相同的数据结构,通过后缀区分。

全链路压测需要注意以下几点:

  • 找到核心流程,全链路压测成本巨大,不可能全做,一个系统中核心的20%才是压测的目标
  • 选择隔离方式。一种是独立的环境进行压测,隔离效果好。另一种是和生产环境混合,通过参数识别,在框架里进行特殊处理
  • 缩小依赖服务范围。如果对A服务压测,那么A服务依赖的服务如何配合。

3.6 故障演练

我们害怕故障,做了很多应对策略,但是这些策略在没有测试的情况下谁也不敢轻易启用。

阿里也进行故障演练,他的工具叫MonkeyKing,可以模拟硬件故障、API故障、分布式故障、数据库故障。

3.7 数据迁移

因为大多数服务是从单体架构开始,伴随业务的发展开始拆分,所以会涉及到数据迁移。

3.7.1 逻辑分离,物理不分离

是指老服务和新服务放在同一个库里,建立不同的表名,从代码层面实现隔离、解耦。数据迁移可以通过触发器实现,或者双写的方式实现。

3.7.2 物理分离

新服务和老服务的数据通过不同的数据库物理隔离。可以使用相同的表名,数据同步需要额外的方案实现。

  • 利用数据库同步工具读取binlog
  • 业务应用上写两个库
  • 老系统在写数据库的同时,发消息到中间件实现同步