目录
什么是软件架构
- 阐述系统的核心设计、关键模块、核心组件、及其相互关系,用以说明和指导软件系统的开发和协作。为了解决系统的复杂性而进行的一种设计
如何做软件架构
-
一般的关键步骤
- 需求分析
- 需求评审,对未达成一致的部分后续讨论解决。
- 系统要解决的业务问题是什么?系统如何帮助解决这些问题?
- 系统要完成什么,才能够实现业务目标?
- 业务未来1~2年内的业务预期/目标是什么?与之对应的系统需要达到什么要求?
- 方案设计
- 系统要完成的目标中隐含的约束和风险是什么?
- 结合业务的工程要求、技术的整体规划,根据目标进行系统设计。
- 合理设计系统、子系统、模块、核心逻辑流程、数据架构、资源要求.
- 选最合适的,不是选最好的
- 明确设计过程中已识别的风险及其应对方案,明确系统中需要进行验收或验证的部分
- 合理拆分任务,明确负责人,制定里程碑及工程协作方式、风险处理及责任
- 进行方案评审,拉相关项目开发者及测试、运维、产品
- 项目开发过程中
- 根据项目大小选择合适的沟通及进度同步方式,至少要对关键里程碑进行检查验收及问题处理
- 对系统的关键接口或模块功能在对的时间进行检查和验收
- 对有多人协作的界面在对的时间进行检查和验收
- 与核心开发及产品进行定期沟通和信息同步
- 测试
- 上线
- 运营
- 关键业务指标及技术指标的迭代、监控、提升
- 应急处理流程、应急处理方案的迭代及落地
- 需求分析
-
从0到1的架构
- 确定好边界和范围
- 功能、角色、如何实现
- 需求分析、方案设计
- 分工、迭代、预演、推断
- 开发、测试、上线、运营
- 功能、角色、如何实现
- 确定好边界和范围
如何设计系统
-
分维度思考解决问题的思路
- (整体)系统视图(架构图/功能图/模块图):系统逻辑上有哪些功能模块组成,各个模块要完成的功能是/接口能力是什么,各个层的能力和职责是什么
- (逻辑)核心模块逻辑/流程视图:关键模块的核心逻辑/流程是什么
- (数据)数据视图:核心数据是什么,他们的关系
- (部署)服务视图:系统由那些服务组成,调用关系是什么?
- 接口:各个核心模块/服务对外的接口定义
-
要始终持有工程及项目视角进行设计
- 要做到能够轻松让他人理解,并知道自己负责的模块的功能,知道与自己交互的模块/服务提供的接口或功能
- 要保证各个模块划分的合理性,并考虑其工程上的可落地性,要保证服务后续的可维护性和运维成本合理
- 对于技术难点、挑战、风险,要通过系统、流程进行合理的解决。其解决方案要进行合理、科学的调研和探讨、决策。
其他
- 五视图法
- DDD
- 微服务
- PMP
三高系统设计概要
- 系统的常见复杂性来源及分类
- 性能:
- 扩展性:
- 可用性:
- 性能及可用性的关系:
- 一般当系统的性能下降到某一个阈值以下时,其可用性也会随之受到影响
- 性能与可用性既不相同,也不被另一个概念覆盖
- 如何描述:
- 系统可用性:一般常用一年可用时间所占比例一年的比例描述,常用约束99.9%,99.99%
- 服务/接口可用性:可以用一定时间内业务调用成功比例描述(一般为1分钟、1小时、一天)
- TP99
- 需要保证在某一时间段内该方法所有调用的消耗时间至少有99%的值要小于此阀值,否则系统将会报警; 依次类推。
- TP99
- 性能及可用性的关系:
- 安全性:
- 成本:
- 协作成本
- 研发成本
- 开发成本
- 测试及验收成本
- 项目管理成本
- 软件及硬件成本
高性能架构设计
- 什么是高性能?如何衡量
- 业务类型和性能指标
- 需要与同类型的产品进行比对
- 1ms的系统响应速度和1s的系统响应速度哪个是高性能的系统?
- 我们是否应该以一个季度或半年为周期对系统的持续进行性能优化?
- 不一定,要看你的优化是否产生收益,避免没有意义的性能优化
- 业务类型和性能指标
常见优化思路
- 服务的分类:
- 实时接口服务
- 前端都在app上,后端被分为多个服务,每个功能模块逻辑都会比较简单,但是调用量很大,可能被多个其他服务或APP用户调用。
- 系统服务
- 一般由固定的用户群体使用,调用量较少,一般qps低于1。但该类服务可能会有需要大量数据计算的服务,或需要处理复杂业务逻辑的服务。
- 异步任务服务
- 用于进行某些数据的计算、同步或处理,不对外提供接口,不需要对数据做出实时响应,但可能要进行大规模数据计算。
- 实时处理服务
- 需要对接入的实时消息进行即时响应,并尽快完成处理
- 实时接口服务
-
常见中间件
- OLTP/DB:mysql
- OLAP:clickhose、es
- 检索中间件:
- KV:redis、tair、hbase
- 全文:es、slor
- sql:mysql
- 消息中间件:kafka、rocketmq
-
优化思路
- 实时接口服务
- 常见开销
- 高调用量造成的单机负载开销
- IO调用开销
- 调用其它服务导致的线程阻塞
- 数据检索开销
- 优化方案
- 接口通信模式
- Linux高并发情况下使用reactor模式(尽量选取epoll的边沿触发 + 非阻塞IO方式),可以用更小的负载解决更多的IO问题,可使用netty作为通信框架。
- Windows可以使用异步IO
- 异步调用
- 调用其它服务使用异步调用方式,优化IO等待阻塞时间
- 数据缓存
- 将DB或其他服务中的数据进行本地或KV缓存,优化数据存取时间
- 核心数据结构及算法优化
- 排序、MAP...
- 选择高性能、高可用的数据检索中间件
- 分担负载:
- 无状态服务
- 服务器端所能够处理的数据全部来自于请求所携带的信息,无状态服务对于客户端的单次请求的处理,不依赖于其他请求,处理一次请求的信息都包含在该请求里。
- 可以对服务进行扩容、压测(对线上流量进行录制,通过对非线上服务进行压测得到单机负载),以线上最高峰期流量的1.5倍计算需要多少机器
- 有状态服务
- 服务会存储请求上下文相关的数据信息,先后的请求是可以有关联的。
- 尽可能将状态拆分到中间件或单个服务上
- 若状态数据极大,可对数据服务进行合理分片,对同分片服务进行多副本部署,降低负载
- 无状态服务
- 接口通信模式
- 常见开销
- 系统服务:常见开销(DB开销、数据检索开销、计算开销)
- DB
- 读写分离,sql优化,减少大事务
- 数据缓存
- 合理优化及拆分计算任务
- 拆分大任务并合理并行执行(fork/join map/reduce)
- 核心数据结构及算法优化
- 优化多线程
- 无锁化:TLS、数据不变设计
- 锁粒度拆分:
- 合理选用CAS方式替换
- DB
- 异步任务服务
- 核心要保证计算资源的利用及关键任务的SLA
- 实时处理服务
- 核心要保证数据处理的流程无阻塞点,能实时响应
- 实时接口服务
单机优化方法
- 优化思路
- 自顶向下
- 对运行在特定负载下的应用进行监控,监控范围:OS、JVM、Java Web容器、应用性能测量统计指标
- 基于监控展开下一步工作
- JVM GC调优
- JVM 命令行选项调优
- 堆内存大小...
- OS调优
- 交换区、select IO...
- 应用程序性能分析 —> 应用程序优化
- 自底向上
- 关注的是在不更改应用前提下,改善CPU使用率。也可为修改应用提供建议
- 应用范围:
- 不同平台(CPU架构或数量不同)进行应用调优时
- 应用迁移到其他OS时
- 无法更改源代码时
- 需要收集和监控最底层的CPU性能统计数据。以下两项最常用:
- 执行特定任务所需CPU指令数
- 特定负载下CPU高速缓存未命中率
- 自顶向下
- 具体动作分类:
- 性能监控:非侵入式,作用在生产环境
- 性能分析:侵入式,会影响应用的吞吐量或响应性能。关注范围更集中。通常在性能监控之后的行动。一般在测试环境或开发环境中使用。
- 性能调优:为改善应用响应性或吞吐量而更改参数,源代码或属性配置的活动。
- 重要原则:
- 避免过早优化,首先应该编写清晰、易读、易理解的代码。
- 奥卡姆剃刀原则:性能问题最可能的原因应该是最容易解释的。
- 新代码比机器配置更可能引入性能问题
- 机器配置比 JVM 或者操作系统的 bug 更容易引入性能问题
- 隐藏的bug确实存在,但不应该把最可能引起性能问题的原因首先归咎于它,而只在测试用例通过某种方式触发了隐藏的bug时才关注
- 监控对象:
- cpu
- 使用率
- 用户态时间使用率
- CPU 执行应用代码时间占总CPU时间的百分比。
- 内核态时间使用率
- 内核态时间与应用相关,比如应用执行 I/O 操作,等底层系统资源的操作。
- 内核态CPU使用率高意味着共享资源有竞争或I/O设备间有大量交互。
- 理想情况下,应用达到高性能时,内核态CPU使用率为0%
- 内核态时间与应用相关,比如应用执行 I/O 操作,等底层系统资源的操作。
- 指标
- IPC和CPI是对计算密集型应用很重要的两个指标,但OS一般无自带监控工具
- IPC(Instructions Per Clock) 每时钟指令数
- CPI(Cycles Per Instructions) 指令时钟周期
- IPC和CPI是对计算密集型应用很重要的两个指标,但OS一般无自带监控工具
- 影响使用率的固有因素-停滞(Stall)
- CPU等待从内存中读取数据(也会被OS监控工具报告繁忙),一般发生在指令等待数据从内存载入寄存器的过程中
- 尽量命中缓存,减少停滞时间
- 优化目标
- 提升而不是降低(更短时间段内的)CPU使用率,以下是cpu空闲的常见原因
- 应用被同步原语阻塞,直至锁释放才能继续执行
- 应用在等待IO,例如数据库调用所返回的响应,可以通过异步调用优化
- 真的很闲
- 提升而不是降低(更短时间段内的)CPU使用率,以下是cpu空闲的常见原因
- 常用监控工具:top/vmstat/mpstat/dstat (us + sy + id = 100)
- 用户态时间使用率
- cpu运行队列(run queue)
- 队列中存放的是可监控可运行的线程/进程(没有被I/O阻塞,没有休眠)
- 运行队列的长度:
- 运行队列很长意味着系统负载可能已经饱和
- 当队列长度等于虚拟CPU个数时,基本无性能影响。若队列长度为 虚拟CPU个数的4倍以上 时,系统响应速度会下降的很明显。
- 若运行队列长度长时间超过虚拟CPU个数1倍时,就需要关注。若在3倍以上时就需要引起注意或采取行动。
- 运行队列很长意味着系统负载可能已经饱和
- 优化思路:
- 增加CPU或减少CPU负载量
- 定位分析系统中的具体应用,修改应用,改进CPU使用率
- 使用率
- 内存
- 使用率
- 影响使用率的常见因素-换页
- 换页
- 当内存应用运行所需内存 > 物理内存时,会发生换页。为了应对该情况,通常会为os配置swap空间,swap空间一般在一个独立的磁盘分区上,当应用的物理内存耗尽时,os应用会将一部分内存(最少运行那部分)置换到swap空间。当应用访问被置换出去的部分时,需要将这部分空间从磁盘中置换进内存。置换活动会对应用的响应性和吞吐量造成很大影响
- 优点
- 避免内存空间不够导致应用崩溃
- 缺点
- 换页时会对应用吞吐量和性能造成很大影响
- 对Java应用的影响
- JVM gc发生在os页面交换时,性能会很差
- 原因是gc回收不可达对象占用空间时需要访问大量内存。若java堆中一部分被置换出去,需要置换该部分空间才能扫描全部java堆。gc的stop the word时间将会延长。
- 置换过程中整个java应用是不可用的
- 一般推荐禁用swap
- JVM gc发生在os页面交换时,性能会很差
- 换页
- 查看堆存储快照,分析内存的占用情况
- 影响使用率的常见因素-换页
- 使用率
- 查看堆各区域的内存增长是否正常
- 查看是哪个区域导致的GC
- 查看GC后能否正常回收到内存
- 常用监控工具:free/top/vmstat
- cpu
- 线程
- 上下文切换
- 分类:
- 让步上下文切换
- 挂起和唤醒线程会导致os的让步上下文切换,代价通常可达到80000个时钟周期。
- 是应用发起的
- 加锁、sleep、IO...
- 抢占式上下文切换
- 因分配的时间片用尽或有更高优线程抢占。切换率高说明预备运行的线程数多于可用的虚拟CPU数,运行队列会很长。
- 让步上下文切换
- 消耗的计算:
- 让步上下文切换除以处理器数目即上下文切换浪费的时钟周期。
- 让步上下文切换*80000 / CPU每秒时钟周期 = 让步上下文切换时钟周期占比。
- 监控工具
- pidstat -w
- 对Java应用而言,若让步上下文切换占去它5%以上时钟周期,说明它遇到了锁竞争。当达到3%~5%阈值时已经值得一查。
- 分类:
- 线程迁移
- 待运行线程在处理器间的迁移
- 产生的原因
- 大多数情况,OS的调度程序会把待运行现场分配给上次运行他的虚拟CPU。但若该CPU忙,就会将该线程迁移到其他CPU
- 产生的消耗
- 新的虚拟CPU缓存中可能没有该线程运行所需数据和状态信息
- 优化思路
- 将Java应用绑定到特定处理器组上。一般在Java应用每秒迁移数量>=500次时,可以做此优化。
- 产生的原因
- 待运行线程在处理器间的迁移
- 上下文切换
- 网络I/O
- 使用率
- 若发送到系统网络接口硬件的消息数量超过了他的处理能力,消息就会进入os的缓冲区,从而导致应用延迟
- netstat可以查看每秒发送和接受的包数
- 一般瓶颈不会出现在这里,如果出现问题可以考虑更升级网卡
- 使用率
- 磁盘IO
- 可以用数据结构(b+树..)、page cache进行优化
- GC
- 查看每分钟GC时间是否正常
- 查看每分钟YGC次数是否正常
- GC日志常用 JVM 参数
- 查看对象的动态晋升年龄是否正常
- 查看单次GC各阶段详细耗时,找到耗时严重的阶段
- 查看单次FGC时间是否正常
- 查看FGC次数是否正常
// 打印GC的详细信息 -XX:+PrintGCDetails
// 打印GC的时间戳 -XX:+PrintGCDateStamps
// 在GC前后打印堆信息 -XX:+PrintHeapAtGC
// 打印Survivor区中各个年龄段的对象的分布信息 -XX:+PrintTenuringDistribution
// JVM启动时输出所有参数值,方便查看参数是否被覆盖 -XX:+PrintFlagsFinal
// 打印GC时应用程序的停止时间 -XX:+PrintGCApplicationStoppedTime
// 打印在GC期间处理引用对象的时间(仅在PrintGCDetails时启用) -XX:+PrintReferenceGC
- 首先表态如果使用合理的 JVM 参数配置,在大多数情况应该是不需要调优的
- 其次说明可能还是存在少量场景需要调优,我们可以对一些 JVM 核心指标配置监控告警,当出现波动时人为介入分析评估
高可用系统架构
-
可用性的度量
- MTBF(Mean Time Between Failure)
- 平均故障间隔时间,时间越长,系统稳定性越高
- MTTR(Mean Time To Repair)
- 平均故障恢复时间,值越小,故障对于用户的影响越小
- 可用性: MTBF / (MTBF + MTTR)
- 99% 年故障时间 3.65天
- 99.9% 年故障时间 8小时
- 99.99% 年故障时间 52分钟
- 99.999% 年故障时间 5分钟
故障体系
- 故障画像
- 本质上来说就是通过分析大量的故障案例,进行充分总结和高度抽象得出的故障根因(Root Cause)。常见的故障画像大概分为以下几类:
- 基础设施
- 中间件故障
- 外部服务
- 系统缺陷
- 流程问题
- 本质上来说就是通过分析大量的故障案例,进行充分总结和高度抽象得出的故障根因(Root Cause)。常见的故障画像大概分为以下几类:
- 故障预案
- 基础设施故障
- 服务器故障
- 宿主机(物理机)故障导致的一个或多个虚拟机实例宕机,或虚拟机发生计划外(不明原因)的重启
- 一个虚拟机实例宕机一般影响较小,确定负载均衡的自动健康检查已将流量转走,待虚拟机恢复后重新部署应用。注:正在调研服务器重启时自动触发Jenkins部署以减少人工运维
- 多个虚拟机实例宕机需评估影响范围,极端情况可能会造成流量击垮其他服务器,紧急情况需启用备用虚拟机。确定负载均衡的自动健康检查已将流量转走;
- 宿主机(物理机)故障导致的一个或多个虚拟机实例宕机,或虚拟机发生计划外(不明原因)的重启
- 网络故障
- 机房网络误操作(网络割接/板卡升级替换/打补丁/设备升级/配置优化/扩容/虚拟网络组件升级等),或部分网络设备故障,造成一台或多台服务器级别网络问题(内网不通、丢包、延迟等)
- 将问题反馈到网络组;一台服务器网络问题一般影响较小,确定负载均衡的自动健康检查已将流量转走,并关注网络恢复情况
- 多台服务器网络问题需评估影响范围,必要情况需要切流量。确定负载均衡的自动健康检查已将流量转走
- 机房核心交换机等大型故障,造成子网级别网络问题,或机房级别网络问题;
- 将问题反馈到网络组
- 若网络故障影响到负载均衡SLB,则通过控制台修改DNS配置,将流量导向其他正常SLB,并等待DNS缓存更新
- 若影响到转发前置机Nginx,通过SLB配置后台,将流量导向其他正常Nginx;【预案】若影响到服务器,通过Nginx配置将流量导向其他正常的服务器
- 运营商问题或网络流量过大,造成专线问题,或专线带宽打满;
- 将问题反馈到网络组
- 机房网络误操作(网络割接/板卡升级替换/打补丁/设备升级/配置优化/扩容/虚拟网络组件升级等),或部分网络设备故障,造成一台或多台服务器级别网络问题(内网不通、丢包、延迟等)
- 电力故障
- 机房例行检修(倒闸检修/柴油发电带载等)导致故障,或意外停电故障,造成机房无法正常运行;
- 若影响到负载均衡SLB,则通过控制台修改DNS配置,将流量导向其他正常SLB,并等待DNS缓存更新;【预案】若影响到Nginx,通过SLB配置将流量导向其他正常Nginx
- 机房例行检修(倒闸检修/柴油发电带载等)导致故障,或意外停电故障,造成机房无法正常运行;
- 接入层故障
- DNS故障,无法解析
- 公司层面报障,切换客户端DNS高可用配置,开启备用DNS域名
- SLB故障,如VIP无法访问,或丢包
- 通知SLB运维人员;
- SLB部署需要做到:外网:同一个运营商出口会配置2个不同机房SLB,内网:同一个内网域名会配置多个不同机房SLB用于冷备
- 通过控制台修改DNS将流量导向其他正常SLB,并等待DNS缓存更新
- Nginx故障;
- Nginx前置机部署时需要跨机房;【预案】通过SLB配置将流量导向其他正常Nginx;
- DNS故障,无法解析
- 服务器故障
- 中间件故障
- MySQL故障
- 无法访问或访问超时
- 要求部署时必须是Master-Slave架构且跨机房
- 遇到单实例故障或机房故障时,会自动进行Failover且切换主从,业务方会经历一段有损服务后自行恢复
- 无法访问或访问超时
- HBase故障
- 无法访问或访问超时
- 需要封装客户端代理SDK(内置配置中心开关),底层做双向数据复制;遇到集群故障时,通过配置中心切换到另一个集群
- 无法访问或访问超时
- Redis故障
- 无法访问或访问超时
- 要求部署必须是Master-Slave架构且跨机房
- 遇到到单实例故障时,会自动进行Failover且切换主从,业务方会经历一段有损服务后自行恢复
- 无法访问或访问超时
- Elasticsearch故障
- 无法访问或访问超时
- 通过客户端代理组件,将读写流量切换到备用Elasticsearch集群
- 无法访问或访问超时
- Kafka故障
- 无法访问或访问超时
- 通过客户端代理组件,将读写流量切换到备用Kafka集群
- 无法访问或访问超时
- Zookeeper故障
- 无法访问或访问超时
- 等待Zookeeper完成奔溃恢复和重新选举,密切观察监控指标变化
- 无法访问或访问超时
- MySQL故障
- 外部服务故障
- 外部服务出现调用超时或异常返回
- 在编码时务必处理好第三方调用的超时、重试、降级与熔断机制,做好监控告警与日志埋点,并对异常情况做好测试
- 遇到故障时,启动手动或自动降级熔断,使得调用快速失败和快速返回兜底数据,避免调用链雪崩
- 外部服务出现调用超时或异常返回
- 系统缺陷
- 流程问题
- 基础设施故障
- 故障演练
- 故障转移
- 故障恢复
可用性设计思路
- 接入层高可用
- Nginx 反向代理
- Nginx 反向代理的作用主要是提供限流、黑白名单、鉴权、流量转发、监控、日志等功能,也是众多 API 网关得以实现的基础组件(如 OpenResty、 Kong)。
- Nginx 反向代理
- 服务层高可用
- 单一服务器实例故障
- 一般是由服务器自身的故障点导致,这种问题的发生概率非常高,当集群规模达到 1000 以上的实例时,几乎每天都会随机出现 1~5 个实例故障。
- 当单一实例故障时,通常不需要采取任何手动措施,而是由上层的 Nginx 代理通过健康检查来自动摘除不健康实例。
- 多服务器实例故障
- 一般是虚拟机、容器对应的宿主机故障导致,当然也可能会由程序 BUG 触发或性能问题导致,如不合理的超时、重试设置导致的服务雪崩,流量突增导致的服务过载等,往往会将该服务集群直接压垮。
- 当多实例故障发生时,故障面相对较大,可能在借助 Nginx 自动健康检查摘除的同时,需要人工介入。
- 单一服务器实例故障
- 中间件高可用
- 很多中间件都提供了原生的集群部署能力,如互联网业务系统常见的面向 OLTP 的存储系统:MySQL、Redis、MongoDB、HBase、Couchbase、Cassandra、Elasticsearch 等;消息中间件如 RocketMQ、Kafka 等;分布式协调服务如 Chubby、Zookeeper 等。这里讨论的也是这种集群化架构的冗余能力,如果读者的生产系统中间件尚未具备多实例的集群部署,建议尽快对架构进行升级换代。
- 主从式架构
- 主从式集群通常以读写分离的方式为客户端提供服务,即所有应用节点写入 Master 节点,多个 Slave 组合并采用负载均衡(服务端代理或 DNS 域名)的方式分摊读请求。
- 当主从式集群的 Master 实例发生故障时,通常会由健康检查组件自动检测,执行重新选举,将集群的 Slave 节点提升为 Master。整个过程对客户端透明,切换时间视具体数据量和网络状况而变化,当数据量很小时,客户端几乎无感知。
- 当 Slave 实例发生故障时,处理流程相对简单,只需要将故障节点从负载均衡摘除即可。
- 去中心化架构
- 去中心话的架构简单高效,以 Cassandra 为例,任何节点均可支持读写以及请求转发,数据通过一致性哈希的方式进行分片,并且在其他节点上保存副本,客户端可随意调整一致性级别(Consistency Level)来在读写性能、强弱一致性语义之间达到平衡。
- 当节点发生故障时,集群会自动识别到故障节点,并进行摘除。数据会发生 Rehash,而客户端也会执行 Failover 操作,挑选其他健康的协调节点,重新建立连接。
- 服务(Worker)高可用
- 关键在于监控和冗余部署,当有合适的 HA 监视组件发现 Worker 不工作或状态不健康,并能及时执行 Failover,则问题就可以迎刃而解。
- 超时
- 如果服务间调用未设置超时,或超时设置不合理,则会带来级联故障风险。当某一系统发生故障时,服务不可用,上游就会出现调用延迟升高。如果没有合理的超时参数控制,则故障就会顺着调用链级联传播,最终导致关键路径甚至所有服务不可用,这种情况也称为服务雪崩。
- 警惕不合理的连接池超时设置
- 以基于 Spring Boot 开发的微服务为例,默认的 Tomcat 连接池大小为 200,也就是说默认单实例只允许同时存在 200 个并发请求。这在平时是毫无压力的,但雪崩发生时,所有的请求就会一直在等待,连接池迅速就被占满,从而导致其他请求无法进入,系统直接瘫痪。
- 以 HttpClient 连接池来说,通常大部分人都正确设置了创建连接超时(ConnectTimeout)和读取超时(SocketTimeout),但却忽略了连接池获取连接超时(ConnectionRequestTimeout)的设置。当目标服务超时发生,有时候会发现尽管设置了读取超时,系统还是会出现雪崩。原因就是连接池获取连接默认是无限等待,这就导致了当前请求被挂起,直到系统资源被耗尽。这里给出一种实际生产环境使用的配置。
- 动态超时与自适应超时
- 实际场景中,如果对所有的服务调用超时都搞“一刀切”,比如都设置为 500ms,则会过于死板,缺乏灵活性。比如有些服务调用量并不高,但是延迟偏高,而有些服务性能高,TP99 可能才不到 100ms,服务提供方也能承诺服务的性能可以长期得到保证。这种情况就可以考虑动态超时和自适应超时方案。
- 所谓动态超时,是指可以通过控制后台,人为在运行时干预超时参数。还是以 HttpClient 为例说明,可以通过配置中心,针对某些 API URL 动态调整参数。
- 超时的套娃原则
- 理想情况下,超时的参数配置应该是遵循“套娃原则”,即沿着调用链,超时的阈值设定应该是逐级减小的。
- 如果超时是逐级增大的,则会出现:在上游已超时返回的情况下,下游还在继续处理,造成不必要的资源浪费。如果反过来,上游还在等待,下游可能已超时并进行再次重试,这样当前请求的成功率就会得到提升。
- 重试
- 重试的位置无处不在,既可以是来自客户端的用户手动重试,也可以是服务端各组件之间的自动重试。当客户端出现全部或局部页面加载失败时,可以通过友好的文字、图片等方式来引导用户手动重试加载,如提示语“网络似乎开了点小差,请再次重试”。在服务端,接入层也可以增加重试能力,常见的就是 Nginx 的代理重试。
- 重试风暴
- 毋容置疑,重试会放大请求量。重试触发的条件一般是目标服务或中间件系统出现了不健康状态或不可用,在这种情况下,无论是来自客户端的用户重试或来自服务的组件间的重试,都会引起“读放大”效应。这会给原本就处于故障状态的组件雪上加霜,极端情况下会导致系统直接奔溃。
- 需要努力避免由于大量重试导致的瞬间流量放大造成的类似 DDoS 攻击效应,称为“重试风暴”。重试风暴是造成系统过载和奔溃的重要原因。
- 退避原则
- 为了减少重试风暴带来的危害,一种行之有效的方法是增加重试的时间间隔,通过退避(back-off)的方法来逐步增加等待时间。最常见的做法是指数退避(exponential back-off)原则。
- 举个例子,设置初始重试等待时间为 10ms,重试上限为 5 次。当目标系统出现故障时,调用方等待 10ms 后发起重试,如果请求依旧失败,则客户端等待 20ms 后再次发起重试。如果再次请求失败,则等待 40ms 后再次重试,以此类推,直到返回正确的结果或达到重试上限。
- 指数退避的特性会使得重试间隔时间按指数级别递增,在极端情况下,会导致单次请求的耗时被严重放大,从而导致系统资源被耗尽,进一步拖垮系统。需要引起格外注意。
- 建议减少初始等待时间,并且严格限制重试次数上限,比如只重试一次。
- 限流
- 限流的位置与粒度
- 常见的限流位置有客户端限流、接入层限流和服务层限流。客户端可以对单一用户的操进行限流,避免用户反复重试,比如可以在用户下单后,将按钮置灰,防止用户多次重试。接入层限流是服务端最有效的位置,可以通过令牌桶或漏桶算法,对远程 IP 进行限流,或对下游服务进行限流。应用层限流一般为单机限流,具体限流阈值通常以单机压测的结果为基准进行估算。
- 限流的粒度可以是全局限流,也可以是服务粒度,或者是 API 根目录,甚至可以设置为特定的 API URL。这些粒度应该可以支持在控制后台动态配置。
- 主动限流与被动限流
- 主动限流,是指服务的提供方主动评估服务的容量和峰值 QPS,提前配置限流阈值并说明限流后的服务表现。服务的使用方无需关心这些服务的性能和可靠性,只需要按照限流的标准进行容错处理即可。这种方式是一种比较友好的合作模式。
- 被动限流则恰恰相反,服务的提供方无法了解和评估自身的服务状态,上游流量突增后也不具备技术手段来应对。这种情况可以考虑服务调用方实施被动限流,即对服务的使用方、各种存储、中间件等资源增加限流约束机制,理想情况应该可以支持控制后台随时调整。
- 限流的位置与粒度
- 降级
- 降级的位置与粒度
- 应用层的降级相对更灵活,可以设置很多降级策略,如基于请求 QPS、请求的长尾延迟、超时率或错误率等,并且可以提前根据不同策略处理好降级数据。
- 降级的粒度可以是全局限流,也可以是服务粒度,或者是 API 根目录,甚至可以设置为特定的 API URL。这些粒度应该可以支持在控制后台动态配置。
- 主动降级与被动降级
- 主动降级是指直接将对外提供的服务(如 RESTful API)降级掉,输出降级错误提示,或一组缓存的业务数据,这些数据通常不是那么及时,甚至是“脏”数据。主动降级是一种粗粒度的降级。
- 被动降级是指将不健康的第三方服务或组件降级,这通常是一种细粒度的降级策略。降级的结果就是当前调用无法返回数据,或者返回缓存的旧数据,基于这些降级数据,服务端再处理本次请求事务。
- 强弱依赖
- 微服务架构的依赖非常多,比如服务间调用(HTTP/RPC)、数据库读取、缓存访问、消息中间件订阅与消费等等,这些依赖项构成了微服务系统的拓扑结构。
- 根据业务的不同,有些依赖是必不可少的,当出现故障时,整个大系统就面临着不可用的风险,我们把这种依赖称为“强依赖”。比如 Feed 业务中的推荐服务,就是一种典型的强依赖,当推荐服务不可用,整个 Feed 流就面临着无数据可用的困境。
- 有些依赖项在出现故障时,可以丢弃,业务上不会产生明显的影响,我们称之为“弱依赖”。比如在 Feed 业务中的用户服务和缓存,当出现问题时,完全可以抛弃,对用户的影响就是每条 Feed 的作者头像和昵称丢失,业务层面一般可以接受。
- 微服务架构设计的一个重要准则就是一切尽可能弱依赖,解除强依赖。这意味着在代码开发阶段,需要面向失败设计,做好依赖项失败的降级处理。
- 降级的位置与粒度
- 熔断
- 在问题累计到一定的程度后,直接断开依赖、快速失败,从而有效保护系统本身和依赖方,避免全量系统发生雪崩。
核心设计原则:Design for failure
- 开发部署阶段
- 容灾设计
- 集群部署,多机容灾:
- 预估业务负载,根据业务负载、系统类型,设计集群方案(设计按照预估峰值负载的10倍以上进行设计,部署按照预估峰值负载的1.5倍进行部署)
- 可以根据业务情况设计弹性伸缩方案,需要基于容器化部署之上(k8s)
- 异地多活,避免机房故障(你的业务非常有价值)
- 多机房容灾(同机房可以考虑多个机架部署)
- 异地容灾
- 集群部署,多机容灾:
- 链路容错设计
- 限流、熔断、降级
- 超时时间设置
- 根据业务对性能的需求及系统链路和负载的预估,对全链路服务进行超时设置
- 保证系统链路问题发生时能保证基本可用
- 分析依赖
- 非核心依赖故障可以使用熔断或降级处理
- 核心服务或核心依赖应当设计降级方案
- 分析依赖
- 根据业务及系统特点,设计制定合理的链路错误机制抛出机制及处理方案。
- 容灾设计
- 运维/运营阶段:
- 设计定期的故障演练方案,可以结合混沌工程,验证和迭代系统从方案设计、到线上问题发现、处理的能力
- 灰度发布及验证,保证系统部分可用
理论知识
- CAP理论:在一个分布式系统中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个
- 一致性:对某个指定的客户端来说,读操作保证能够返回最新的写操作结果
- 可用性:非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)
- 分区容错性:当出现网络分区后,系统能够继续履行职责
- BASE原理:分布式系统在出现不可预知故障的时候,允许损失部分可用性。
- Basically Available(基本可用)
- Soft state(软状态)
- Eventually consistent(最终一致性)
可扩展性架构
- 可伸缩性:系统负载变化可以通过集群伸缩解决
- 可扩展性:业务变化能够通过可扩展性解决
- 设计思路
- 归纳、演绎业务趋势导致的系统可能发生的变化,一般最多为系统做两年内的变化预测
- 设计能够应对变化的系统架构、框架、模块、组件
- 分析提炼出经常变化的部分和不变的部分,共性下沉(变化为架构、系统框架、核心模块、基本组件),特性上浮(尽可能抽象为接口)
- 注意避免过度设计,无该领域或行业的业务沉淀及积累时,往往预测会有很大偏差,导致一些设计根本没有启到作用,甚至要重新设计。
- 初次对某个领域或业务的系统进行设计时
- 根据业务参考相关架构及设计
- 可以实现先行,对于想清楚的部分可使用成熟的设计模式,后期再演进架构,对系统各个部分进行重构