第一部分 概述
1. 交易型系统设计的一些原则
1.1 高并发原则
- 无状态
- 拆分(系统维度、功能维度、读写维度、AOP 维度、模块维度)
- 服务化(进程内服务 - 单机远程服务 - 集群手动注册服务 - 自动注册和发现服务 - 服务的分组/隔离/路由 - 服务治理如限流/黑白名单)
- 消息队列
- 数据异构(数据异构、数据闭环)
- 缓存银弹(浏览器缓存、App 客户端缓存、CDN 缓存、接入层缓存、应用层缓存、分布式缓存)
- 并发化
2. 高可用原则
3. 业务设计原则
- 防重设计(防重 Key、防重表)
- 幂等设计(消息中间件)
- 流程可定义
- 状态与状态机
- 后台操作系统可反馈
- 后台系统审批化(操作日志记录,保证操作可追溯、可审计)
- 文档和注释
- 备份
第二部分 高可用
2. 负载均衡与反向代理
对于一般应用来说,有 Nginx 就可以了,但 Nginx 一般用于七层负载均衡,吞吐量有一定限制,为了提升整体吞吐量,会在 DNS 和 Nginx 之间引入接入层,如使用 LVS(软件负载均衡器)、F5(硬负载均衡器)可以做四层负载均衡,即首先 DNS 解析到 LVS/F5,然后 LVS/F5转发给 Nginx,再由 Nginx 转发给后端 Server。
负载均衡主要关注几个方面:
- 上游服务器配置:使用 upstream server 配置上游服务器
- 负载均衡算法:配置多个上游服务器时的负载均衡机制
- 失败重试机制
- 服务器心跳检查
负载均衡算法 :
- round-robin:轮询算法,默认 LB 算法,配合 weight 配置可以实现基于权重的轮询
- ip_hash:根据 IP 进行 LB,相同的 IP 将 LB 到同一个upstream server
- hash key:对某一个 key 进行 hash或者使用一致性 hash 算法进行 LB
- least_conn:将请求 LB 到最少活跃连接的 upstream server
- least_time:Nginx 商业版功能,基于最小平均响应时间进行 LB
健康检查:
3. 隔离术
隔离是指将系统或资源分割开,系统隔离是为了在系统发生故障时,能限定传播范围和影响范围,即发生故障后不会出现滚雪球效应,从而保证只有出问题的服务不可用,其他服务还是可用的。资源隔离通过隔离来减少资源竞争,保障服务间的相互不影响和可用性。出现系统问题时,可以考虑 LB 路由、自动/手动切换分组或者降级等手段来保障可用性。
- 线程隔离(不同的线程池)
- 进程隔离(单实例到多子系统)
- 集群隔离(不同的集群)
- 机房隔离(不同的机房)
- 读写隔离(主从模式等)
- 快慢隔离
- 动静隔离(CDN)
- 爬虫隔离
- 热点隔离(秒杀、抢购做成独立系统或服务隔离,对于读热点,可以使用多级缓存,对于写热点,可以使用缓存+队列模式削峰,)
- 资源隔离
- 环境隔离(测试环境、预发布环境、灰度环境、正式环境)
- 压测隔离(真实数据、压测数据)
- AB 测试
- 缓存隔离
- 查询隔离(简单、批量、复杂条件查询分别路由到不同集群)
- 使用 Hystrix 隔离
- 基于 Servlet 3 实现请求隔离
4. 限流
一般幵发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、 限制瞬时并发数(如Nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间 窗口内的平均速率(如Guava的RateLimiter、Nginx的limit_req模块,用来限制每秒的 平均速率),以及限制远程接口调用速率、限制MQ的消费速率等。另外,还可以根据 网络连接数、网络流量、CPU或内存负载等来限流。
限流算法:
应用级限流:
- 限流总并发、连接、请求数(限制 TPS、QPS)
- 限流总资源数(池化,如数据库连接池、线程池)
- 限流某个接口的总并发、请求数(使用AtomicLong或者Semaphore进行限流,Hystrix)
- 限流某个接口的时间窗请求数(如使用Guava Cache来存储计数器)
- 平滑限流某个接口的请求数(Guava RateLimiter)
分布式限流
分布式限流最关键的是要将限流服务做成原子化,解决方案可以使用 Redis+Lua 或者 Nginx+Lua 技术进行实现
接入层限流
接入层通常指请求流量的入口,主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、降级、限流、A/B测试、服务质量监控等。
Nginx接入层限流可以使用 Nginx 自带的两个模块:连接数限流模块 ngx_http_limit_conn_module 和漏桶算法实现的请求限流模块 ngx_http_limit_req_module,或者 OpenResty 提供的 Lua 限流模块 lua-resty-limit-traffic
节流
防止多个相同事件连续重复执行,主要有throttleFirst、throttleLast、throttleWithTimeout
5. 降级特技
降级的最终目的是保证核心服务可用,即使是有损的。
降级需要根据系统的吞吐量、响应时间、可用率等条件进行手工降级或自动降级。
5.1 降级预案
自动开关降级/人工开关降级,读服务降级/写服务降级,多级降级,页面降级/页面片段降级/页面异步请求降级/服务功能降级/读降级/写降级/爬虫降级/风控降级
5.2 自动开关降级
5.3 人工开关降级
5.4 读服务降级
5.5 写服务降级
5.6 多级降级
- 页面 js 降级开关
- 接入层降级开关
- 应用层降级开关
5.7. 配置中心(通过配置方式动态开启/关闭降级开关)
- 应用层 API 封装
- 使用配置文件实现开关配置
- 使用配置中心实现开关配置
5.8. 使用 Hystrix 实现降级
5.9. 使用 Hystrix 实现熔断
6. 超时与重试机制
代理层超时与重试
如Haproxy/Nginx/Twemproxy,需设置代理与后端真实服务器之间的网络连接/读/写超时时间
Web 容器超时
提供HTTP服务运行环境的,如Tomcat/Jetty,需设置客户端与容器之间的网络连接/读/写超时时间,和在此容器中默认 socket 网络连接/读/写超时时间
中间件客户端超时与重试
如 Dubbo、MQ、HttpClient 等,需设置客户的网络连接/读/写超时时间与失败重试机制
数据库客户端超时
如MySQL/Oracle/PG,需要分别设置JDBC Connection、Statement的网络连接/读/写超时时间,事务超时时间,获取连接池等待时间
NOSQL客户端超时
如Mongo、Redis,需要设置其网络连接/读/写超时时间,获取连接池等待时间
业务超时
如订单取消任务、超时活动关闭,还有如Future#get(timeout, unix)限制某个接口的超时时间
前端Ajax超时
浏览器通过Ajax访问时的网络连接/读/写超时时间
总结:
最重要的是网络相关的超时设置。
超时后应该有相应的处理策略,如重试(稍后再试、尝试其它分组服务、尝试其它机房服务)、摘掉不存活节点(负载均衡/分布式缓存场景下)、托底(返回历史数据/静态数据/缓存数据)、等待页或错误页。
对于非幂等写服务应避免重试,可以提前生成唯一流水号保证写服务操作通过流水号来实现幂等操作。
在进行DB/缓存服务器操作时,需经常检查慢查询,同时在超时严重时,可以直接将该服务降级。
对于有负载均衡的中间件,考虑配置心跳/存活检查。
7. 回滚机制
回滚是指当程序或数据出错时,将程序和数据恢复到最近的一个正确版本的行为。常见的如事务回滚、代码库回滚、部署版本回滚、数据版本回滚、静态资源版本回滚等。
7.1 事务回滚
单库事务回滚直接使用相关 SQL,分布式数据库可以使用分布式事务,如两阶段提交、三阶段提交协议。另外可以考虑入事务表、消息队列、补偿机制(执行/回滚)、TCC 模式(预占/确认/取消)、Sagas模式(拆分事务+补偿机制)等实现最终一致性。
7.2 代码库回滚
Git/SVN
7.3 部署版本回滚
部署版本化、小版本增量发布、大版本灰度发布、架构升级并发发布
7.4 数据版本回滚
设计版本化数据结构时,有全量和增量两种思路
7.5 静态资源版本回滚
全量新版本保证版本可追溯。清理CDN 缓存,在新 URL上添加随机数并清理浏览器缓存。请求参数加上版本号
8. 压测与预案
在大促来临之前,研发人员需要对现有系统进行梳理,发现系统瓶颈和问题,然后 进行系统调优来提升系统的健壮性和处理能力。一般通过系统压测来发现系统瓶颈和问 题,然后进行系统优化和容灾(如系统参数调优、单机房容灾、多机房容灾等)。即使 己经把系统优化和容灾做得非常好了,但也存在一些不稳定因素,如网络、依赖服务的 SLA不稳定等,这就需要我们制定应急预案,在出现这些因素后进行路由切换或降级处 理。在大促之前需要进行预案演习,确保预案的有效性。
8.1 系统压测
一般指性能压力测试,评估系统的稳定性和性能,通过压测数据进行系统容量评估,决定是否需要扩容和缩容。压测要有压测方案 {如压测接口、并发量、压测策略(突发、逐步加压、并发量)、压测指标(机器负载、QPS/TPS、响应时间)},之后产出压测报告{压测方案、机器负载、QPSTPS、响应时间(avg、min、max)、成功率、相关参数(JVM参数、压缩参数)等},根据压测报告分析的结果进行系统优化和容灾。
- 线下压测(使用Jmeter、Apache ab 压测某个接口或某个组件(如DB 连接池),然后进行调优,实现单个接口或组件性能最优。线下压测环境和线上不同,适合组件级压测,数据只能参考)
- 线上压测(按读写分为读压测、写压测、混合压测,按数据仿真度分为仿真压测和引流压测(如TCPCopy),按是否给用户提供服务分为隔离集群压测和线上集群压测。注意离散压测(选择的数据应该是分散的或长尾的)和全链路压测)
8.2 系统优化和容灾
拿到压测报告后,接下来分析报告,然后进行有针对性的优化,如硬件升级、系统扩容、参数调优、代码优化、架构优化(如加缓存、读写分离、历史数据归档)等,根据压测数据因地制宜地解决。在系统优化时,要进行代码走查,发现不合理的参数配置,如超时时间、降级策略、缓存时间等。在系统压测时进行慢查询排查,如Redis、MySQL等。在应用系统扩容方面,根据往年流量和运营业务方沟通,评估是否需要扩容及扩容容量。扩容时考虑系统容灾,如分组部署、跨机房部署等,保证高可用。
8.3 应急预案
在系统压测后会发现一些系统瓶颈,在系统优化之后会提升系统吞吐量并降低响应时间,容灾之后的系统可用性得以保障,但还存在一些风险,如网络抖动、某台机器负载过高、某个服务变慢、DB load值过高等,为了防止因为这些问题导致系统雪崩,需要制定应急预案。
应急预案可分几步执行:
- 系统分级(划分交易核心系统和交易支撑系统,对不同级别的系统实施不同的质量保障)
- 全链路分析(从用户入口到后端存储,梳理出关键路径,进行评估和预案,防止问题的级联效应和雪崩效应)
- 配置监控报警
- 制定应急预案
最后,要对关联路径实施监控报警,包括服务器监控(CPU使用率、磁盘使用率、网络带宽等)、系统监控(系统存活、URL 状态/内容监控、端口存活等)、JVM 监控(堆内存、GC 次数、线程数等)、接口监控(接口调用量『每秒/每分钟』、接口性能『TOP50/TOP99/TOP999』、接口可用率等)。然后配置报警策略,如监控时间段、报警阈值、通知方式等。在报警后要观察系统状态、监控数据或者日志来查看系统是否真的存在故障,如果确实是故障,则应即及时执行相关预案处理,避免故障扩散。
第三部分 高并发
9. 应用级缓存
9.1 缓存简介
让数据更接近于使用者,目的是让访问速度更快。
9.2 缓存命中率
从缓存中读取数据的次数与总读取次数的比率,命中率越高越好。
缓存命中率 = 从缓存中读取次数/总读取次数(从缓存中读取次数+从慢速设备上读取次数)
9.3 缓存回收策略
- 基于空间:设置缓存存储空间,超过空间上限时回收
- 基于容量:设置缓存最大数量大小
- 基于时间:TTL(Time To Live 存活期),TTI(Time To Idle 空闲期)
- 基于 Java 对象引用:软引用,弱引用
- 回收算法:
- FIFO(First In First out 先进先出算法)
- LRU(Least Recently Used 最近最少使用算法)
- LFU(Least Frequently Used 最不常用算法)
- 实际应用中基于 LRU 的缓存居多,如 Guava Cache、Ehcache 等
9.4 Java 缓存类型
堆缓存:使用Java堆内存来存储缓存对象,好处是没有序列号/反序列化,速度最快。缺点是缓存数据量很大时,GC 暂停时间会变长,存储容量受限于堆空间大小,一般通过软引用/弱引用来存储缓存对象,这样堆内存不足时可以强制回收这部分内存。一般存储较热的数据。
堆外缓存:存储在堆外内存,可以减少 GC 暂停时间,可以支持更大的缓存空间,但读取数据需要序列化/反序列化,速度较慢。
磁盘缓存:缓存存在磁盘上,在 JVM 重启时缓存还在,而堆缓存/堆外缓存会丢失,需重新加载。
分布式缓存:与上面的进程内缓存和磁盘缓存不同,多 JVM 实例。可以使用Redis/Ehcache-clustered等实现。
两种模式:
* 单机时:存储最热的数据到堆缓存,相对热的数据到堆外缓存,不热的数据到磁盘缓存
* 集群时:存储最热的数据到堆缓存,相对热的数据到堆外缓存,全量数据到分布式缓存
9.5 缓存使用模式
主要分两大类:Cache-Aside 和 Cache-As-SoR(Read-through、Write-through、Write-behind)
三个名词
- SoR(system-of-record):记录系统,又叫数据源,即实际存储原始数据的系统
- Cache:缓存,是 SoR 的快照数据,访问速度比 SoR快,放入 Cache 目的是提升访问速度,减少回源到 SoR 的次数
- 回源:即回到数据源头获取数据,Cache 没有命中时,需要从 SoR 读取数据。
缓存使用模式:
- Cache-Aside:业务代码围绕着 Cache 写,是由业务代码直接维护缓存。读场景,先从缓存获取数据,如果没有命中,则回源到 SoR 并将源数据放入缓存供下次读取使用。写场景,先将数据写入 SoR,写入成功后立即将数据同步写入缓存。适合使用 AOP 模式实现。
- Cache-As-SoR:把 Cache 看作为 SoR,所有操作都是对 Cache 进行,然后 Cache 委托给 SoR 进行真实的读写。业务代码中只看到 Cache 的操作,看不到关于 SoR 相关的代码。
- Read-Through:业务代码首先调用 Cache,如果不命中由Cache回源到 SoR。需要配置一个 CacheLoader 组件用来回源到 SoR 加载源数据。
- Write-Through:穿透写模式/直写模式,业务代码首先调用 Cache 写数据,然后由 Cache 负责写缓存和写 SoR,而不是由业务代码。需要配置一个 CacheWriter 组件用来回写 SoR。
- Write-Behind:也叫 Write-Back,回写模式。不同于Write-Through是同步写 SoR 和 Cache,它是异步写,异步之后可以实现批量写、合并写、延时和限流。
- Copy pattern:有 Copy-On-Read(在读时复制) 和 Copy-On-Write(在写时复制)两种。
9.6 性能测试
使用 JMH 进行基准性能测试。首先进行 JVM 预热,然后进行度量,产生测试结果。
10. HTTP 缓存
10.1 HTTP 缓存简介
当用浏览器访问网页或 HTTP 服务时,根据服务器端返回的缓存设置响应头将相应内容缓存到浏览器,下次可以直接使用缓存内容或者仅需要去服务器端验证内容是否过期即可。减少浏览器与服务器端之间来回传输的数据量,节省带宽以提升性能。
10.2 HTTP 缓存
- 服务器端响应的 Last-Modified 会在下次请求时,将 If-Modified-Since 请求头带到服务器端进行文档是否修改的验证,如果没有修改就返回304,浏览器可以直接使用缓存内容
- Cache-Control:max-age和 Expires 用户决定浏览器端内容缓存多久,即多久过期,过期后则删除缓存重新从服务器端获取最新的。
- HTTP/1.1规范定义的 Cache-Control 优先级高于 HTTP/1.0规范定义的 Expires
- 一般情况下 Expires=当前系统时间+缓存时间(Cache-Control:max-age)
- HTTP/1.1规范定义 ETag 为“被请求变量的实体值”,可简单理解为文档内容摘要,ETag 可用来判断页面内容是否已经被修改过了。
10.3 一些经验
- 只缓存200状态码的响应,像302等要根据实际场景决定,比如当系统出错时,自动302到错误页面,此时缓存302就不对了
- 有些也没不需要强一致,可以进行几秒的缓存。比如商品详情页展示的库存,可以缓存几秒钟。短时间的不一致对于用户来说没有影响的
- JS/CSS/image等一些内容缓存时间可以设置为很久,比如1月甚至1年,通过在页面修改版本来控制过期
- 假设商品详情页异步加载的一些数据,使用 last-modified 进行过期控制,而服务器端做了逻辑修改,但内容是没有修改的,即内容的最后修改时间没变。若果想过期这些异步加载的数据,则可以考虑在商品详情页添加异步加载数据的版本号,通过添加版本号来加载最新的数据,或者将 last-modified 时间加1来解决,但这种情况下使用 ETag 是更好的选择
- 商品详情页异步加载的一些数据,可以考虑更长时间的缓存,比如1个月而不是几分钟。可以通过 MQ 将修改时间推送到商品详情页,从而实现按需过期数据
- 服务器端考虑使用 tmpfs 内存文件系统缓存、ssd 缓存,使用服务器端负载均衡算法一致性哈希来提升缓存命中率
- 缓存 key 要合理设计。比如去掉某些参数或排序参数,以保证代理层的缓存命中率;要有清理缓存的工具,出问题时能快速清理掉问题 key。
- AB 测试/个性化需求时,要禁用掉浏览器缓存,但要考虑服务器端缓存
- 为了便于查找问题一般会在响应头中添加源服务器信息,如访问京东商品详情页会看到 ser 响应头,此投存储了源服务器 IP,以便出现问题时,知道哪台服务器有问题
11. 多级缓存
缓存相关的问题有很多,如缓存算法、热点数据与更新缓存、更新缓存与原子性、缓存崩溃与快速恢复等。
多级缓存是指在整个系统架构的不同系统层级进行数据缓存,以提升访问效率。
典型的应用整体架构和流程如图:
![Pasted Graphic 1.png](/Users/amon/Library/Application Support/typora-user-images/E9FDE38F-8D19-4587-8308-6629648903FE/Pasted%20Graphic%201.png)