一、系统设计的一些原则
海恩法则
事故的发生是量积累的结果
再好的技术、在完美的规章,在实际操作层面也无法取代人自身的素质和责任心
墨菲定律
任何事情都没有表面看起来那么简单
所有事情的发展都会比你预计的时间长
会出错的事总会出错
如果你担心某种情况发生,那么它更有可能发生
二、软件架构中的高可用设计
2.1、什么是高可用
假设一个系统一直可以提供服务,那么这个系统的可用性是100%。
大部分公司的高可用目标是99.99%。也就是一年的停机时间为53分钟。
2.2、可用性度量和考核
现在业界常用N个9来量化可用性
描述 | 通俗叫法 | 可用性级别 | 年度停机时间 |
基本可用性 | 2个9 | 99% | 87.6小时 |
较高可用性 | 3个9 | 99.9% | 8.8小时 |
具有故障自动恢复的可用性 | 4个9 | 99.99% | 53分钟 |
极高可用性 | 5个9 | 99.999% | 5分钟 |
故障的度量与考核:
类别 | 描述 | 权重 |
高危S级事故故障 | 一旦出现故障,可能会导致服务整体不可用 | 100 |
严重A级故障 | 客户明显感知服务异常:错误的回答 | 20 |
中级B级故障 | 客户能够感知服务异常:响应比较慢 | 5 |
一般C级故障 | 服务出现短时间内抖动 | 1 |
2.3、如何保障系统的高可用
- 系统设计过程中避免使用单点
- 高可用保证的原则是“集群化”,或者叫“冗余”
- 通过“自动故障转移”来实现系统的高可用
解决高可用问题具体方案:
- 负载均衡
- 限流
- 降级
- 隔离
- 超时与重试
- 回滚
- 压测与预案
三、负载均衡
3.1、DNS&nginx负载均衡
保证服务集群可以进行故障转移。
当服务宕机后,负载请求进行转移,来达到高可用。
其他的一些负载均衡:
- 服务和服务RPC --- RPC 框架 提供负载方案 (DUBBO,SpringCloud)
- 数据集群需要负载均衡(mycat,haproxy)
3.2 upstream配置
再nginx中配置upstream
upstream backend{
server 192.168.1.101:8080 weight=1;
server 192.168.1.102:8080 weight=2;
}
复制代码
proxy_pass来处理用户请求
location / {
proxy_pass http://backend;
}
复制代码
3.3、负载均衡算法
3.3.1、round-robin
轮询,默认的负载均衡算法,以轮询的方式将请求转发到上游服务器,配合weight配置可以实现基于权重的轮询
3.3.2、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;
}
复制代码
3.3.3、hash key [consistent]
对某一个key进行哈希或者使用一致性哈希算法进行负载均衡。
Hash算法存在的问题是,若添加或者删除一台服务器的时候,会导致很多key被重新负载均衡到不同的服务器,而引起后端的问题。
若是使用一致性哈希算法,当添加/删除一台服务器时,只有少数key将被重新负载均衡到不同的服务器。
哈希算法:
upstream backend{
hash $url;
server 192.168.1.101:8080 weight=1;
server 192.168.1.102:8080 weight=2;
}
复制代码
一致性哈希算法: consistent_key动态指定。
upstream backend{
hash $consistent_key consistent;
server 192.168.1.101:8080 weight=1;
server 192.168.1.102:8080 weight=2;
}
复制代码
3.4、失败重试
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
秒之后会再次将服务器加入到存活列表进行重试。
3.5、健康检查
时刻关注服务的健康状态,若是服务不可用了,将会把请求转发到其他存活的服务上,以提高可用性。
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: 当上游服务器返回匹配的响应状态码时,则认为上游服务器存活。 请勿将检查时间设置过短,以防心跳检查包过多影响上游服务器。
3.6、其他配置
- 备份服务器
upstream backend{
server 192.168.1.101:8080 weight=1;
server 192.168.1.102:8080 weight=2 backup;
}
复制代码
此时102服务器为备服务器,当所有的主服务器不可用时,请求将转发备被服务器。
- 不可用服务器
upstream backend{
server 192.168.1.101:8080 weight=1;
server 192.168.1.102:8080 weight=2 down;
}
复制代码
此时102服务器永久不可用,当测试或者机器出现问题时,可通过此配置摘掉机器。
四、隔离术
4.1、线程隔离
线程隔离指的是线程池隔离,一个请求出现问题不会影响到其他线程池。
4.2、进程隔离
把项目拆分成一个一个的子项目,互相物理隔离,不进行相互调用。
4.3、集群隔离
将集群隔离开,使互相不影响。
4.4、机房隔离
分不同的机房进行部署,杭州机房;北京机房;上海机房;
4.5、读写隔离
互联网项目中大多是读多写少,读写分离,扩展读的能力,提高性能,提高可用性。
4.6、动静隔离
将静态资源放入nginx,CDN,从而达到动静隔离,防止页面加载大量静态资源
4.7、热点隔离
将热点业务独立成系统或服务进行隔离,如秒杀,抢购。
读热点一般使用多级缓存
写热点一般使用缓存加消息队列的方式
五、限流
若是不做限流,当突发大流量,服务可能会被冲垮。
5.1、限流算法
5.1.1、漏桶算法
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
5.1.2、令牌桶算法
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
5.2、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,如果请求处理量一直远远大于最大线程数,则会引起响应变慢甚至会僵死。
5.3、接口限流
限制某个接口的请求频率
long limit = 1000;
while(true){
//得到当前秒
long currentSeconds = System.currentTimeMillis()/10;
if (counter.get(currentSeconds).incrementAndGet()>limit) {
//限流了
continue;
}
//业务处理
}
复制代码
5.4、redis限流
实际是使用lua脚本设置参数做限流。
5.5、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是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。
六、降级
当访问量剧增、服务出现问题(如响应时间长或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。
6.1、降级预案
在降级前需要对系统进行梳理,判断系统是否可以丢丢卒保帅,从而整理出那些可以降级,那些不能降级。
-
一般
: 比如,有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级。 -
警告
: 有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警。 -
错误
: 比如,可用率低于90%,或者数据库连接池用完了,或者访问量突然猛增到系统能承受的最大阈值,此时,可以根据情况自动降级或者人工降级。 -
严重错误
: 比如,因为特殊原因数据出现错误,此时,需要紧急人工降级。
降级按照是否自动化可分为:自动开关降级和人工开关降级。
降级按照功能可分为:读服务降级和写服务降级。
降级按照处于的系统层次可分为:多级降级。
降级的功能点主要从服务器端链路考虑,即根据用户访问的服务调用链路来梳理哪里需要降级。
6.2、页面降级
在大型促销或者抢购活动时,某些页面占用了一些稀缺服务资源,在紧急情况下可以对其整个降级。
6.3、页面片段降级
比如,商品详情页中的商家部分因为数据错误,此时,需要对其进行降级。
6.4、页面异步请求降级
比如,商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,则可以进行降级。
6.5、服务功能降级
比如,渲染商品详情页时,需要调用一些不太重要的服务(相关分类、热销榜等),而这些服务在异常情况下直接不获取,即降级即可。
6.6、读降级
比如,多级缓存模式,如果后端服务有问题,则可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。
6.7、写降级
比如,秒杀抢购,我们可以只进行Cache的更新,然后异步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
6.8、自动降级
当服务中错误出现次数到达阀值(99.99%),对服务进行降级,发出警告。
七、超时与重试
在访问服务之后,由于网络或其他原因迟迟没有响应而超时,此时为了用户体验度,可以默认发起第二次请求,进行尝试。
- 代理层超时与重试: nginx
- web容器超时与重试
- 中间件和服务之间超时与重试
- 数据库连接超时与重试
- nosql超时与重试
- 业务超时与重试
- 前端浏览器ajax请求超时与重试
八、压测与预案
8.1、系统压测
压测一般指性能压力测试,用来评估系统的稳定性和性能,通过压测数据进行系统容量评估,从而决定是否需要进行扩容或缩容。
线下压测:
通过如JMeter、Apache ab压测系统的某个接口(如查询库存接口)或者某个组件(如数据库连接池),然后进行调优(如调整JVM参数、优化代码),实现单个接口或组件的性能最优。
线下压测的环境(比如,服务器、网络、数据量等)和线上的完全不一样,仿真度不高,很难进行全链路压测,适合组件级的压测,数据只能作为参考。
线上压测:
线上压测的方式非常多,按读写分为读压测、写压测和混合压测,按数据仿真度分为仿真压测和引流压测,按是否给用户提供服务分为隔离集群压测和线上集群压测。
读压测是压测系统的读流量,比如,压测商品价格服务。写压测是压测系统的写流量,比如下单。写压测时,要注意把压测写的数据和真实数据分离,在压测完成后,删除压测数据。只进行读或写压测有时是不能发现系统瓶颈的,因为有时读和写是会相互影响的,因此,这种情况下要进行混合压测。
仿真压测是通过模拟请求进行系统压测,模拟请求的数据可以是使用程序构造、人工构造(如提前准备一些用户和商品),或者使用Nginx访问日志,如果压测的数据量有限,则会形成请求热点。而更好的方式可以考虑引流压测,比如使用TCPCopy复制
8.2、系统优化和容灾
拿到压测报告后,接下来会分析报告,然后进行一些有针对性的优化,如硬件升级、系统扩容、参数调优、代码优化(如代码同步改异步)、架构优化(如加缓存、读写分离、历史数据归档)等。
不要直接复用别人的案列,一定要根据压测结果合理调整自己的案例。
在进行系统优化时,要进行代码走查,发现不合理的参数配置,如超时时间、降级策略、缓存时间等。在系统压测中进行慢查询排查,包括Redis、MySQL等,通过优化查询解决慢查询问题。
在应用系统扩容方面,可以根据去年流量、与运营业务方沟通促销力度、最近一段时间的流量来评估出是否需要进行扩容,需要扩容多少倍,比如,预计GMV增长100%,那么可以考虑扩容2~3倍容量。
8.3、应急预案
在系统压测之后会发现一些系统瓶颈,在系统优化之后会提升系统吞吐量并降低响应时间,容灾之后的系统可用性得以保障,但还是会存在一些风险,如网络抖动、某台机器负载过高、某个服务变慢、数据库Load值过高等,为了防止因为这些问题而出现系统雪崩,需要针对这些情况制定应急预案,从而在出现突发情况时,有相应的措施来解决掉这些问题。
应急预案可按照如下几步进行:首先进行系统分级,然后进行全链路分析、配置监控报警,最后制定应急预案。