一、系统设计的一些原则
海恩法则

事故的发生是量积累的结果
再好的技术、在完美的规章,在实际操作层面也无法取代人自身的素质和责任心

墨菲定律

任何事情都没有表面看起来那么简单
所有事情的发展都会比你预计的时间长
会出错的事总会出错
如果你担心某种情况发生,那么它更有可能发生

二、软件架构中的高可用设计

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、如何保障系统的高可用

  • 系统设计过程中避免使用单点
  • 高可用保证的原则是“集群化”,或者叫“冗余”
  • 通过“自动故障转移”来实现系统的高可用

解决高可用问题具体方案:

  1. 负载均衡
  2. 限流
  3. 降级
  4. 隔离
  5. 超时与重试
  6. 回滚
  7. 压测与预案

三、负载均衡

3.1、DNS&nginx负载均衡

保证服务集群可以进行故障转移。

当服务宕机后,负载请求进行转移,来达到高可用。

其他的一些负载均衡:

  1. 服务和服务RPC --- RPC 框架 提供负载方案 (DUBBO,SpringCloud)
  2. 数据集群需要负载均衡(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、其他配置

  1. 备份服务器
upstream backend{
	server 192.168.1.101:8080 weight=1;
	server 192.168.1.102:8080 weight=2 backup;
}
复制代码

此时102服务器为备服务器,当所有的主服务器不可用时,请求将转发备被服务器。

  1. 不可用服务器
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、令牌桶算法

有高可用架构的系统 高可用系统设计_有高可用架构的系统_02

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

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自带的两个模块:

  1. 连接数限流模块ngx_http_limit_conn_module
  2. 漏桶算法实现的请求限流模块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是有高可用架构的系统 高可用系统设计_限流_03server_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%),对服务进行降级,发出警告。

七、超时与重试

在访问服务之后,由于网络或其他原因迟迟没有响应而超时,此时为了用户体验度,可以默认发起第二次请求,进行尝试。

  1. 代理层超时与重试: nginx
  2. web容器超时与重试
  3. 中间件和服务之间超时与重试
  4. 数据库连接超时与重试
  5. nosql超时与重试
  6. 业务超时与重试
  7. 前端浏览器ajax请求超时与重试

八、压测与预案

8.1、系统压测

压测一般指性能压力测试,用来评估系统的稳定性性能,通过压测数据进行系统容量评估,从而决定是否需要进行扩容缩容

线下压测
通过如JMeter、Apache ab压测系统的某个接口(如查询库存接口)或者某个组件(如数据库连接池),然后进行调优(如调整JVM参数、优化代码),实现单个接口或组件的性能最优。
线下压测的环境(比如,服务器、网络、数据量等)和线上的完全不一样,仿真度不高,很难进行全链路压测,适合组件级的压测,数据只能作为参考。

线上压测:
线上压测的方式非常多,按读写分为读压测、写压测和混合压测,按数据仿真度分为仿真压测和引流压测,按是否给用户提供服务分为隔离集群压测和线上集群压测。
读压测是压测系统的读流量,比如,压测商品价格服务。写压测是压测系统的写流量,比如下单。写压测时,要注意把压测写的数据和真实数据分离,在压测完成后,删除压测数据。只进行读或写压测有时是不能发现系统瓶颈的,因为有时读和写是会相互影响的,因此,这种情况下要进行混合压测。
仿真压测是通过模拟请求进行系统压测,模拟请求的数据可以是使用程序构造、人工构造(如提前准备一些用户和商品),或者使用Nginx访问日志,如果压测的数据量有限,则会形成请求热点。而更好的方式可以考虑引流压测,比如使用TCPCopy复制

8.2、系统优化和容灾

拿到压测报告后,接下来会分析报告,然后进行一些有针对性的优化,如硬件升级、系统扩容、参数调优、代码优化(如代码同步改异步)、架构优化(如加缓存、读写分离、历史数据归档)等。

不要直接复用别人的案列,一定要根据压测结果合理调整自己的案例。

在进行系统优化时,要进行代码走查,发现不合理的参数配置,如超时时间、降级策略、缓存时间等。在系统压测中进行慢查询排查,包括Redis、MySQL等,通过优化查询解决慢查询问题。

在应用系统扩容方面,可以根据去年流量、与运营业务方沟通促销力度、最近一段时间的流量来评估出是否需要进行扩容,需要扩容多少倍,比如,预计GMV增长100%,那么可以考虑扩容2~3倍容量。

8.3、应急预案

在系统压测之后会发现一些系统瓶颈,在系统优化之后会提升系统吞吐量并降低响应时间,容灾之后的系统可用性得以保障,但还是会存在一些风险,如网络抖动、某台机器负载过高、某个服务变慢、数据库Load值过高等,为了防止因为这些问题而出现系统雪崩,需要针对这些情况制定应急预案,从而在出现突发情况时,有相应的措施来解决掉这些问题。

应急预案可按照如下几步进行:首先进行系统分级,然后进行全链路分析、配置监控报警,最后制定应急预案。