文章目录
- 系统设计的一些原则
- 海恩法则
- 墨菲定律
- 软件架构中的高可用设计
- 什么是高可用
- 故障的度量与考核
- 解决高可用问题具体方案
- 集群化部署
- 负载均衡
- 负载均衡实现
- 内部服务
- 外部服务
- 数据库
- 负载均衡算法
- round-robin
- ip_hash
- hash key
- 失败重试
- 健康检查
- TCP
- HTTP
- 隔离
- 线程隔离
- 进程隔离
- 集群隔离
- 机房隔离
- 读写隔离
- 动静隔离
- 热点隔离
- 限流
- 限流算法
- 漏桶算法
- 令牌桶算法
- nginx限流
- ngx_http_limit_conn_module
- ngx_http_limit_req_module
- Tomcat限流
- 接口限流
- 降级
- 降级预案
- 页面降级
- 页面片段降级
- 页面异步请求降级
- 服务功能降级
- 读降级
- 写降级
- 自动降级
- 熔断
- 超时与重试
- 压测与预案
- 系统压测
- 线下压测
- 线上压测
- 系统优化
- 应急预案
- 监控
- 指标
- 链路追踪
- 日志
- 参考文献
系统设计的一些原则
海恩法则
- 事故的发生是量积累的结果
- 再好的技术、在完美的规章,在实际操作层面也无法取代人自身的素质和责任心
墨菲定律
- 任何事情都没有表面看起来那么简单
- 所有事情的发展都会比你预计的时间长
- 会出错的事总会出错
- 如果你担心某种情况发生,那么它更有可能发生
软件架构中的高可用设计
什么是高可用
首先我们来了解下如何来定义一个系统可用,业界常用N个9来定义一个系统的可用情况。比如可用性为99.99%,这个99.99%就是代表的该系统在一年内可用的时间为99.99%。那么可用性为99.99%的系统不可用时间呢?一年有365天,一天有24小时,一小时有60分钟,一分钟有60秒,那么可用性为99.99%的系统一年内不可用时间大致为52.56分钟(3652460*(1-0.9999))。
高可用就是可用性指标大于等于4个9的系统。
故障的度量与考核
该评定标准一般会有SRE和各个业务方的技术负责人一起协商输出统一标准。对于出现各个等级的故障后都会进行总结,作为年底的技术输出。
解决高可用问题具体方案
- 集群化部署
- 负载均衡
- 熔断
- 限流
- 降级
- 隔离
- 超时与重试
- 回滚
- 压测与预案
集群化部署
由于单点部署的话一旦服务挂了就直接不可用,所以需要采用集群部署,避免单点故障。
负载均衡
负载均衡实现
内部服务
保证服务集群可以进行故障转移。当服务宕机后,负载均衡进行转移,来达到高可用。对于内部调用的服务,通过RPC提供负载均衡。
外部服务
对于调用的外部服务,需要外部服务保障自身的高可用部署。同时调用方需要做重试(对于请求的节点不可用时切换其他节点)以及对第三方服务进行不可用、异常的监控。
数据库
对于服务内的数据库需保障高可用部署,以及采用的高可用方案的不适配问题,例如某些高可用方案在进行故障转移时会出现短暂不可用,这种情况也需要考虑进去。最后需要对数据库进行监控(异常、慢请求、CPU、磁盘、内存、线程池、IO)
负载均衡算法
round-robin
轮询,默认的负载均衡算法,以轮询的方式将请求转发到上游服务器,配合weight配置可以实现基于权重的轮询
ip_hash
根据客户IP进行负载均衡,享同的IP将负载均衡到同一个upstream server
upstream backend{
ip_hash;
server 192.168.1.101:8080 weight=1;
server 192.168.1.102:8080 weight=2;
}
hash key
对某一个key进行哈希或者使用一致性哈希算法进行负载均衡。
Hash算法存在的问题是,若添加或者删除一台服务器的时候,会导致很多key被重新负载均衡到不同的服务器,而引起后端的问题。
若是使用一致性哈希算法,当添加/删除一台服务器时,只有少数key将被重新负载均衡到不同的服务器。
失败重试
upstream backend{
server 192.168.1.101:8080 max_fails=2 fail_timeout=10s weight=1;
server 192.168.1.102:8080 max_fails=2 fail_timeout=10s weight=2;
}
若是fail_timeout秒内失败了max_fails次,则认为上游服务器不可用|不存活,将摘掉上游服务器,fail_timeout秒之后会再次将服务器加入到存活列表进行重试。
健康检查
时刻关注服务的健康状态,若是服务不可用了,将会把请求转发到其他存活的服务上,以提高可用性。
Nginx可以集成nginx_upstream_check_module模块来进行主动健康检查。支持TCP心跳和HTTP心跳。
TCP
upstream backend{
server 192.168.1.101:8080 weight=1;
server 192.168.1.102:8080 weight=2;
check interval=3000 rise=1 fall=3 timeout=2000 type=tcp;
}
- interval: 检测间隔时间,此处配置了每隔3s检测一次。
- fall: 检测失败多少次后,上游服务器被标识为不存活。
- rise: 检测成功多少次后,上游服务器被标识为存活,并可以处理请求。
HTTP
upstream backend{
server 192.168.1.101:8080 weight=1;
server 192.168.1.102:8080 weight=2;
check interval=3000 rise=1 fall=3 timeout=2000 type=tcp;
check_http_send ”HEAD /status HTTP/1.0\r\n\r\n“
check_http_expect_alive http_2xx http_3xx;
}
- check_http_send: 即检查时发的HTTP请求内容。
- check_http_expect_alive: 当上游服务器返回匹配的响应状态码时,则认为上游服务器存活。 请勿将检查时间设置过短,以防心跳检查包过多影响上游服
所以我们可以看到某些厂在自己的容器平台上会有 【健康检查】的功能就是根据nginx来做的改造。需要用户配置健康检查的path、频率、首次探测等待时间、每次探测超时时间、端口、探测失败阈值。
隔离
线程隔离
线程隔离指的是线程池隔离,一个请求出现问题不会影响到其他线程池。
进程隔离
把项目拆分成一个一个的子项目,互相物理隔离(部署在不同的机器上)。
集群隔离
将集群隔离开,使互相不影响。
机房隔离
分不同的机房进行部署,杭州机房;北京机房;上海机房,因为可能出现某机房网络问题,这样可以防止某机房出现网络问题导致整个系统无法使用。
读写隔离
互联网项目中大多是读多写少,读写分离,扩展读的能力,提高性能,提高可用性。常见的mysql读写分离就是这个原理。 但是在使用读写分离时还要考虑数据的延迟问题,事务失效问题。
动静隔离
将静态资源放入nginx,CDN,从而达到动静隔离,防止页面加载大量静态资源。
热点隔离
-
将热点业务独立成系统或服务进行隔离,如秒杀,抢购。
-
读热点一般使用多级缓存。同时需要考虑缓存与数据库一致性问题。
-
写热点一般使用缓存加消息队列的方式。同时需要考虑数据延时问题是否会对业务有影响。
限流
限流主要是针对有突发流量的场景,如秒杀、抢购。若是不做限流,当突发大流量,服务可能会被冲垮。
限流算法
漏桶算法
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
令牌桶算法
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
nginx限流
Nginx接入层限流可以使用Nginx自带的两个模块:
-
连接数限流模块ngx_http_limit_conn_module
-
漏桶算法实现的请求限流模块ngx_http_limit_req_module
ngx_http_limit_conn_module
针对某个key对应的总的网络连接数进行限流
可以按照IP来限制IP维度的总连接数,或者按照服务域名来限制某个域名的总连接数。
http{
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_log_level error;
limit_conn_status 503;
...
server{
location /limit{
limit_conn addr 1;
}
}
...
}
-
limit_conn: 要配置存放key和计数器的共享内存区域和指定key的最大连接数。此处指定的最大连接数是1,表示Nginx最多同时并发处理1个连接。
-
limit_conn_zone: 用来配置限流key及存放key对应信息的共享内存区域大小。此处的key是binaryremoteaddr”,表示IP地址,也可以使用binary_remote_addr”,表示IP地址,也可以使用binaryremoteaddr”,表示IP地址,也可以使用server_name作为key来限制域名级别的最大连接数。
-
limit_conn_status: 配置被限流后返回的状态码,默认返回503。
-
limit_conn_log_level: 配置记录被限流后的日志级别,默认error级别。
ngx_http_limit_req_module
漏桶算法实现,用于对指定key对应的请求进行限流,比如,按照IP维度限制请求速率。配置示例如下
limit_conn_log_level error;
limit_conn_status 503;
...
server{
location /limit{
limit_req zone=one burst=5 nodelay;
}
}
-
limit_req: 配置限流区域、桶容量(突发容量,默认为0)、是否延迟模式(默认延迟)。
-
limit_req_zone: 配置限流key、存放key对应信息的共享内存区域大小、固定请求速率。此处指定的key是“$binary_remote_addr”,表示IP地址。固定请求速率使用rate参数配置,支持10r/s和60r/m,即每秒10个请求和每分钟60个请求。不过,最终都会转换为每秒的固定请求速率(10r/s为每100毫秒处理一个请求,60r/m为每1000毫秒处理一个请求)。
-
limit_conn_status: 配置被限流后返回的状态码,默认返回503。
-
limit_conn_log_level: 配置记录被限流后的日志级别,默认级别为error。
Tomcat限流
对于一个应用系统来说,一定会有极限并发/请求数,即总有一个TPS/QPS阈值,如果超了阈值,则系统就会不响应用户请求或响应得非常慢,因此我们最好进行过载保护,以防止大量请求涌入击垮系统。对于这些参数的配置需要经过压力测试后才能得出一个较好的数据,并且每次有重大功能上线时还需要再次压测看是否需要调整参数。这不是一件简单的事情。
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" maxThreads="800" maxConnections="2000" acceptCount="1000"/>
-
acceptCount:等待队列,如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接;默认值为100
-
maxConnections:可以创建的瞬时最大连接数,超出的会排队等待;
-
maxThreads:Tomcat能启动用来处理请求的最大线程数,即同时处理的任务个数,默认值为200,如果请求处理量一直远远大于最大线程数,则会引起响应变慢甚至会僵死。
接口限流
接口的限流主要是针对些核心、QPS高的接口进行限流,限制某个接口的请求频率,以此来保护整个服务不被压垮。可以设置每个实例接口的QPS也可以设置所有实例总和的QPS。
降级
当访问量剧增、服务出现问题(如响应时间长或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。
降级预案
在降级前需要对系统进行梳理,判断系统是否可以丢卒保帅,从而整理出哪些可以降级,哪些不能降级。
-
一般: 比如,有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级。
-
警告: 有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警。
-
错误: 比如,可用率低于90%,或者数据库连接池用完了,或者访问量突然猛增到系统能承受的最大阈值,此时,可以根据情况自动降级或者人工降级。
-
严重错误: 比如,因为特殊原因数据出现错误,此时,需要紧急人工降级。
降级按照是否自动化可分为:自动开关降级和人工开关降级。
降级按照功能可分为:读服务降级和写服务降级。
降级按照处于的系统层次可分为:多级降级。
降级的功能点主要从服务器端链路考虑,即根据用户访问的服务调用链路来梳理哪里需要降级。
页面降级
在大型促销或者抢购活动时,某些页面占用了一些稀缺服务资源,在紧急情况下可以对其整个降级。
页面片段降级
比如,商品详情页中的商家部分因为数据错误,此时,需要对其进行降级。
页面异步请求降级
比如,商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,则可以进行降级。
服务功能降级
比如,渲染商品详情页时,需要调用一些不太重要的服务(相关分类、热销榜等),而这些服务在异常情况下直接不获取,即降级即可。
读降级
比如,多级缓存模式,如果后端服务有问题,则可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。
写降级
比如,秒杀抢购,我们可以只进行Cache的更新,然后异步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
自动降级
当服务中错误出现次数到达阀值(99.99%),对服务进行降级,发出警告。
熔断
熔断机制也是很重要的。 熔断机制说的是系统自动收集所依赖服务的资源使用情况和性能指标,当所依赖的服务不可用或者调用失败次数达到某个阈值的时候就迅速失败,让当前系统立即切换依赖其他备用服务。 比较常用的是流量控制和熔断降级框架是 Netflix 的 Hystrix 和 alibaba 的 Sentinel。
超时与重试
一旦用户请求超过某个时间的得不到响应,就抛出异常。这个是非常重要的,很多线上系统故障都是因为没有进行超时设置或者超时设置的方式不对导致的。我们在读取第三方服务的时候,尤其适合设置超时和重试机制。如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法在处理请求。重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合,因为可能产生重复请求,而服务端又没做幂等处理就会产生脏数据)。常见的重试如下:
-
代理层超时与重试: nginx
-
web容器超时与重试
-
中间件|服务间超时与重试
-
数据库连接超时与重试
-
nosql超时与重试
-
业务超时与重试
-
前端浏览器ajax请求超时与重试
压测与预案
系统压测
压测一般指性能压力测试,用来评估系统的稳定性和性能,通过压测数据进行系统容量评估,从而决定是否需要进行扩容或缩容。
线下压测
通过如JMeter、Apache ab压测系统的某个接口(如查询库存接口)或者某个组件(如数据库连接池),然后进行调优(如调整JVM参数、优化代码),实现单个接口或组件的性能最优。
线下压测的环境(比如,服务器、网络、数据量等)和线上的完全不一样,仿真度不高,很难进行全链路压测,适合组件级的压测,数据只能作为参考。
线上压测
线上压测的方式非常多,按读写分为读压测、写压测和混合压测,按数据仿真度分为仿真压测和引流压测,按是否给用户提供服务分为隔离集群压测和线上集群压测。
读压测是压测系统的读流量,比如,压测商品价格服务。写压测是压测系统的写流量,比如下单。写压测时,要注意把压测写的数据和真实数据分离,在压测完成后,删除压测数据。只进行读或写压测有时是不能发现系统瓶颈的,因为有时读和写是会相互影响的,因此,这种情况下要进行混合压测。
仿真压测是通过模拟请求进行系统压测,模拟请求的数据可以是使用程序构造、人工构造(如提前准备一些用户和商品),或者使用Nginx访问日志,如果压测的数据量有限,则会形成请求热点。而更好的方式可以考虑引流压测,比如使用TCPCopy复制;也可以考虑使用流量拷贝来拷贝线上流量,然后测试环境构造出和线上一样的环境(硬件、数据、示例等),最后将拷贝的流量来对测试环境进行压测,这是个很复杂的事,需要单独的压测平台来支持。
系统优化
拿到压测报告后,接下来会分析报告,然后进行一些有针对性的优化,如硬件升级、系统扩容、参数调优、代码优化(如代码同步改异步)、架构优化(如加缓存、读写分离、历史数据归档)等。
不要直接复用别人的案列,一定要根据压测结果合理调整自己的案例。
在进行系统优化时,要进行代码走查,发现不合理的参数配置,如超时时间、降级策略、缓存时间等。在系统压测中进行慢查询排查,包括Redis、MySQL等,通过优化查询解决慢查询问题。
在应用系统扩容方面,可以根据去年流量、与运营业务方沟通促销力度、最近一段时间的流量来评估出是否需要进行扩容,需要扩容多少倍,比如,预计GMV增长100%,那么可以考虑扩容2~3倍容量。
应急预案
在系统压测之后会发现一些系统瓶颈,在系统优化之后会提升系统吞吐量并降低响应时间,容灾之后的系统可用性得以保障,但还是会存在一些风险,如网络抖动、某台机器负载过高、某个服务变慢、数据库Load值过高等,为了防止因为这些问题而出现系统雪崩,需要针对这些情况制定应急预案,从而在出现突发情况时,有相应的措施来解决掉这些问题。
应急预案可按照如下几步进行:首先进行系统分级,然后进行全链路分析、配置监控报警,最后制定应急预案。
监控
首先这部分不属于高可用系统架构的部分,它应该是监控体系的一部分,而之所以放在这里主要还是因为软件的生命周期中维护的时间更长,所以在系统前期的架构设计中还是要考虑开发、维护中的监控。同时这里对于监控不会介绍很多,只会基于监控的三大数据进行简单概括。
从上面的步骤我们知道如果没有监控,那么系统维护人员面对系统就像个瞎子一样,不知道系统的情况。即使前期做了再完备的设计、开发人员的技术水平多高,但是在软件的维护过程中依然会存在问题 。所以详细、全面、合理的监控是必不可少的。它可以帮助维护人员知道异常的出现,可以帮助维护人员知道系统的变化。一个全面、完善的监控体系是业务保障的关键。
没有什么系统的一步到位的,所以更加需要有监控来帮助系统完善自身的问题。
指标
我们需要对系统的业务、性能进行指标暴露、采集、监控。我们可以根据指标变化分析出系统某个东西的变化情况,同时可以根据指标值发出告警。
指标大体可以分为容器(tomcat)、中间件、数据库、http、池(数据库连接池、http连接池)等,每项指标又可以有QPS、响应时间、异常率、慢请求率等。这些都是些基础的指标数据,在系统的不断维护、迭代中会增加更多的指标数据以及报表展示。这是一个不断优化的过程。
链路追踪
链路追踪主要用来分析一个执行过程的耗时以及具体性能问题出现在哪个地方。同时链路ID(这块需要链路追踪系统进行改造兼容)也可以作为串联日志的中间层来把日志和链路进行串联。由于链路的数据非常的完整,所以链路数据是非常有价值的数据,可以进行链路分析来帮助业务更加轻松的维护系统。
日志
日志作为开发人员常用的排查问题数据存在一个非常难受的点就是无法将一个执行的全部日志一次性查出来,所以可以在日志中添加traceId来把日志数据进行串联。好的、完善的日志是排查问题的关键。所以日志的重构存在于软件维护的整个过程。日志的级别设置非常关键,因为我们经常会对日志进行分析,将error级别的日志进行告警通知相应服务的维护人员。
参考文献
高可用设计原则