持续创作,加速成长!这是我参与「掘金日新计划 · 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
- 业务应用上写两个库
- 老系统在写数据库的同时,发消息到中间件实现同步