实验
- nginx的tcp负载均衡
- consul+consul-template
- consul实现配置中心
- 一个系统不是一下子就能设计完美的
- 在有限的资源下,优先解决最核心问题
一、原则
- 高并发
- 无状态
- 拆分
- 服务化
- 消息队列(异步,)
- 大流量缓存(先入redis,再同步到db)
- 数据校对
- 数据异构(类似数据冗余)
- 数据异构(类似数据冗余,来提升读取效率,例如分表)
- 数据闭环,对于需要多次查询数据的接口,可以把所有数据缓存一次,提升读的速度,然后各个数据修改后,都来改这个缓存
- 缓存,
- 浏览器缓存
- app客户端缓存
- cdn缓存
- 接入层缓存(Nginx,缓存整个接口)
- 应用层缓存
- java的线程共享
- redis缓存
- 分布式缓存
- redis缓存
- 缓存有多个层级,例如
- 接入层(Nginx)
- Redis
- java的缓存(Python没有)
- 回源(数据库,或者调API)
- 并发化(如果需要多个IO,可以使用并发获取,而不是串行)
- 高可用
- 降级(提供有损服务)
- 集中开关管理(也就是一个统一的配置后台,设置降级后,所有服务都能识别)
- 可降级的多级读服务(对于上面的缓存层级,可以设置最终会去到哪一层。例如Redis压力很大,就可以设置接入层获取不到缓存,就直接返回了,而不继续往下找)
- 这这里,返回什么也是个问题,可以返回空列表(会不会被吐槽?),如果是状态,可以返回个默认值,例如有货
- 业务降级
- 屏蔽次要功能,例如双十一,淘宝查历史账单,只会查近1个月的
- 次要流程改为异步,例如微信抢红包,抢到红包是重要功能,红包金额的入账就是次要功能
- 限流(限制恶意流量,防止流量超过峰值)
- 主动拒绝,返回友好点的文案
- 切流量(机器挂了,切流量到其他正常的机器)
- 可回滚(代码版本可回滚)
- 降级(提供有损服务)
业务设计原则
- 幂等
- 流程可定义(也就是有流水表)
- 状态修改(使用CAS)
- 管理后台审计
- 文档和注释
- 备份(代码和人员)
二、负载均衡和反向代理
接入层、反向代理、负载均衡,一般都是指Nginx
- 负载均衡(Nginx的upstream)
- 算法
- 轮询(weight来指定权重)
- ip hash 同一个ip去同一台机
- 其他hash
- 失败重试
- 在fail_timeout时间内如果失败max_fails次,认为不可用,在fail_timeout后,重新检测
- 健康检查
- 默认是惰性的(应该是请求来才去检测的意思)
- nginx_upstream_check_module(插件,每n秒请求上游服务,返回2xx或者3xx,表示存活,所以上游服务要做好对接)
- 算法
- 反向代理
- 缓存(存放在tmpfs)
动态负载均衡
如果修改upstream,比较麻烦,需要重启nginx。动态负载均衡就是可以自动发现上游服务器,然后通过管理后台,快速注册或者摘取上游服务器
方案一:consul+consul-template
流程
- 上游服务向consul_server注册节点
- 管理后台向consul_server注册或者摘除节点
- consul_template长轮询监听consul_server的配置变化
- 有变化后,生成nginx配置,修改nginx配置,重启nginx
好处是节点的注册和摘除,可以在管理后台完成,不需要手动操作nginx配置和重启。
缺点是上游服务需要有consul功能,不知道python有没有,java有
方案二:consul+openResty
流程
- 上游服务向consul_server注册节点
- 管理后台向consul_server注册或者摘除节点
- nginx启动后调用init_by_lua,想consul_server获取配置
- 然后nginx定期去consul_server拉取配置,然后reload
缺点是只能定期,不能长连接,所以有延迟,解决方法是nginx暴露一个http api,开发一个agent,长轮询监听consul_server的配置变化,然后调用http api实时更新nginx的配置
感觉上面两个方案都有点蛋疼。。。。如果有msalt,可以自己做个管理后台,修改配置后,发送msalt任务,msalt自己修改nginx配置,然后reload
四层负载均衡
上面的都是http的负载均衡,那tcp连接,就要用四层负载均衡(7层网络模型,tcp在第4层)
三、隔离
隔离是发生故障后,将故障服务和正常服务隔离,避免故障服务影响正常服务,造成滚雪球。
- 线程隔离
- 系统有两个线程池,将核心业务请求导向线程池A,非核心的导向线程池B
- 这样非核心业务的故障不会影响核心业务
- 进程隔离
- 跟线程类似
- 集群隔离
- 对于一些压力比较大的业务,例如秒杀,用一个单独的集群来实现,避免影响到其他业务
- 机房隔离
- 当一个机房发生故障,把流量切到另一个机房,实现高可用
- 读写隔离
- 例如redis或者mysql,读请求走一个集群,写请求走另一个集群
- 动静隔离
- 动是动态资源,静是静态资源。静态资源尽量放CDN
- 爬虫隔离
- 识别爬虫请求,导到单独的集群,避免影响正常请求
- 热点隔离
- 例如秒杀,可以用单独集群来实现
- 资源隔离
19.资源是指硬件,例如CPU,磁盘,内存。 重要进程,单独分配CPU资源,保证重要进程可用
Hystrix和Servlet3
都是java的组件,
隔离的思路是
- 一台机有多个线程池
- 业务区分核心业务和非核心业务,分流到不同的线程池
- 达到非核心业务过载或者一次,不会影响核心业务
四、限流
-
限流算法
- 令牌桶法
- 进程A往桶里塞令牌,例如速度是1s10个令牌,桶的容量有个上限,假如是100,溢出就丢弃
- 请求来了,从桶里获取令牌(可以根据请求的不同设置不同的令牌数,例如耗时的业务需要2个令牌,简单的业务只需要1个)
- 能获取足够的令牌,就处理请求
- 不能,就丢弃请求,或者等待
- 漏桶算法(有错误,需要重新整理)
- 进程A以速率A流入水滴到桶里
- 如果溢出,就丢弃
- 按照常量速率流出水滴
- 感觉两个算法都很类似,都是类似生产消费的模式,来控制消费的速率
- 令牌法允许突发流量,例如1s内把所有令牌都获取完
- 漏桶法不允许特发流量,而且配置的流出速率不能小于流入,不然就没什么意义了
- 令牌桶法
-
应用级限流(也就是在单台机上面限流)
- 限流总并发/连接/请求数 (例如某个时刻,如果并发数超过阈值,就丢弃请求)
- 限制总资源数(资源一般指数据库连接等)
- 限制单个接口的总并发数/请求数(这个粒度就小一点)
- 限制窗口时间的并发数,例如1s内,并发数不能大于N
- 实现
- 组件的限流功能,例如gevent的最大连接数
- 自己用redis实现
- 总并发数。key是接口名+机器ip,通过incr方法,如果小于N,执行,大于N丢弃,最后执行incr -1),然后设置超时时间
- 窗口时间内总并发数。key是接口名+机器ip,通过incr方法,如果小于N,执行,大于N丢弃,然后设置超时时间(例如1s),这里要考虑设置超时时间失败,导致永远释放不了的问题,所以key最好带上时间,
- 可以写成一个装饰器
-
分布式限流(相对于单机器,主要的难点是原子性)
- 上面的redis也可以实现分布式限流
- redis+lua 利用redis的incr实现原子性
- Nginx+lua 利用lua的锁来实现原子性(底层应该是信号量)
-
接入层限流(Nginx的限流)
- ngx_http_limit_conn_module
limit_conn_zone $binary_remote_addr zone=addr:10m; # 定义限流模块addr 以ip地址作为key;10m表示使用10m内存来进行ip传输;除了binary_remote_addr 表示ip外,server_name表示域名
limit_conn_log_level error; #限流日志,触发限流会打error日志
limit_conn_status 503; #限流时返回的http code
server {
location /limit{
limit_conn addr 1; #定义addr模块限流1
}
}
} -
- ngx_http_limit_conn_module
* ngx_http_limit_req_module(使用漏桶算法来实现)[官方文档](http://nginx.org/en/docs/http/ngx_http_limit_req_module.html)
* ```
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s
location /limit{
limit_req zone=one burst=5 nodelay;
}
```
* 其他配置和limit_conn一样
* 这里有三个参数:
* 1r/s 表示每秒处理2个请求
* burst=5 表示桶的容量=5
* nodelay 非延迟,不加这个配置就是默认延迟
* 情况:
* 情况1 burst=0
* 2r/s 表示500毫秒内,只能处理一个请求,多出的请求返回503。时间窗口是500毫秒。(时间窗口从什么时候开始,这个是nginx决定的,而且不是非常准确,但是这个影响不大)。例如时间窗口是05.000秒到05.500秒。如果在05.300处理了一个请求,下一个请求要到05.500秒,300-500内的请求会被拒绝
* 情况2 burst大于0,例如1,
* 2r/s 表示500毫秒内,只能处理一个请求,超出的请求会放进桶里,最多1个请求。超出桶容量的请求依然返回503。到下一个时间窗口,nginx会从桶里取一个请求出来处理.所以假设3个请求在同一个时间窗口内到来,第一个会被正常处理,第二个会等待500毫秒再处理,第三个返回503
* 情况2 burst大于0,例如1,nodelay
* 和情况2一样,不同的是,放进桶里的请求会被立刻处理,而不是等下一个时间窗口。假设3个请求在同一个时间窗口内到来,第一个会被正常处理,第二个会放进桶里,但是是立刻处理,不会等待,第三个返回503
* 更高级的限流
* 如果要更加复杂,更加了灵活的限流策略,就要用OpenResty,也就是用lua语言,写限流策略,然后在Nginx上面执行
* 节流
* 节流是指在特定的时间窗口,对相同的请求,只处理一次。或者限制多个时间的执行间隔
* 应用:
* 例如防止用户一直刷新页面
* 策略
* throttleFirst,对于相同的请求,只处理第一个
* throttleLast,对于相同的请求,只处理最后一个
* throttleWithTimeout,限制两个请求的间隔不能小于某个时间
* 实现:
* RxJava
## 五、降级
###1.降级的分类
* 从服务端链路考虑,可以进行降级的地方有:
* 页面降级
* 页面片段降级。例如商品详情页,不重要的信息不请求,例如商家信息
* 读降级
* 写降级
* 爬虫降级
* 风控降级。识别用户是否机器人,如果是,进行降级
* 从读和写服务分类
* 读降级(包括上面的读降级,页面降级,页面片段降级等)
* 降级方案:
1. 返回缓存 。返回缓存里的数据,或者返回上一次成功读取的数据。这些数据不会很实时,所以适用于对数据一致性要求不高的场景。
2. 返回默认值。例如读取库存的接口,默认返回没货。例如列表接口,默认返回空列表
3. 返回兜底数据,例如列表接口返回静态的几个item,这些item一般写在代码里面或者静态文件里面
* 写降级
* 降级方案
1. 异步。
2. **先写redis,异步写DB**。适用于需要判断状态的操作。例如下单,需要在redis判断是否有缓存,如果有,写入Redis,返回下单成功,如果没有,返回失败。如果成功,异步同步Redis到DB。
3. **异步执行全部操作**。适用于不需要判断状态的操作。例如写评论,直接返回写入成功。异步执行评论逻辑
###2.降级的触发
触发分为**降级**(打开降级开关)和降级后的**恢复**(关闭降级开噶)
* 人工触发。当开发人员通过监控或者告警,意识到系统需要降级时,手工打开降级开关,进行降级。当系统恢复时,手工关闭开关进行恢复
* 自动降级。当系统通过某些指标,判断系统需要降级时,自动打开降级开关。当系统判断系统负载降低后,自动恢复
* 降级指标有:
1. 超时。当系统执行某个操作超时时,自动降级
2. 失败次数。当系统执行某个操作失败次数超过N时,自动降级
3. 故障。当系统执行某个操作失败时,自动降级
4. 限流。当某个服务触发限流时,自动降级
* 恢复
* 时间窗口重试。当降级后,每个时间窗口执行一次降级前的操作,如果成功,关闭降级开关。例如每1s执行一次操作。
###3.降级的配置
降级开关,其实也就是一个配置,这个配置的实现可以:
* 代码变量。也就是写死在代码里面,修改配置需要修改代码,然后重启服务。
* 配置中心。通过页面就可以修改配置,修改后可以同步到所有机器
* 开源方案:Zookeeper,Consul等
* 实现方案,配置中心的难点是修改配置后,怎么同步到多台机器的多个进程里面(一个进程里面的多个线程或者协程,会共享一份配置)
1. 定时更新。例如进程里面每隔1s或者每100次读取配置,就主动去配置中心更新最新的配置
2. 监听。当配置修改,通过IO多路复用机制通知进程去更新。类似于消息队列,配置中心是生产者,进程是消费者。
###4.降级的实现
书中介绍了使用Hystrix来实现降级
具体做法是
1. 定义run和getFallback两个函数,这个是不同业务不一样的
2. 当请求尽量,先执行run函数,如果成功,就返回
3. 如果失败或者超时,进行降级,返回getFallback函数的内容
自己项目的做法
* 对于每一个接口,定义两个函数,一个是未降级的逻辑run,另一个是已降级的逻辑getFallback。
* 封装一层降级逻辑:
* 如果降级开关关闭,执行run
* 如果降级开关打开,执行getFallback
* 如果run超时或者失败,决定是否自动降级
* 降级后 每个时间窗口执行一次run,如果成功,决定是否恢复
* getFallback可以默认不定义
## 六、重试
### 代理层
代理层主要就是Nginx了
Nginx有有很多超时,或者重试的配置
下面的time是时间,例如可以是`5s`
* 客户端超时配置
* client_header_timeout time; nginx接收客户端请求头的超时时间
* client_body_timeout time nginx接收客户端请求体的超时时间
* send_timeout time; 发送响应到客户端的超时时间
* keepalive_timeout timeout [header_timeout] 长连接的超时时间,header_timeout是返回给客户端的,例如如果设置了,返回的响应头就会有:`Keep-Alive:timeout=10`
* 默认http1.1是打开长连接的,1.0不会,可以通过wireshark来看看是否真的没有3次握手
* keepalive_requests 100 表示长连接可以处理100次请求
* 代理超时设置(代理就是上游的服务)
* 连接超时:
* proxy_connect_time time; 建立连接超时时间
* proxy_read_timeout time ;从上游服务读取响应的超时时间。注意不是读取的超时时间,是发送请求后,到可以读取的超时时间
* proxy_send_time time;建立连接后,发送请求,到上游开始接受请求的超时时间,注意不是开始接收请求到接收完请求的时间。
* 失败重试机制
* proxy_next_upstream ;这个配置可以是多个下面的选项
* timeout 超时,包括建立连接,写请求,读响应头的超时
* invalid_header 上游服务返回错误响应头
* http_xxx,例如http_500,表示上游返回指定的httpcode
* non_idempotent 非幂等请求(idempotent 是幂等的意思)。POST、LOCK、PATCH都是非幂等的请求。默认幂等的请求都是允许重试的。
* off 关闭重试
* proxy_next_upstream_tries number;失败重试次数,包含第一次请求,也就是1表示不重试。0表示不限制。
* proxy_next_upstream_timeout time;在此时间内执行重试,超过后就不重试了。0表示不限制
### 应用层
* 超时
* 设置好超时时间
* 例如A调用B调用C,超时时间一定是A>B,不然会导致一直重试
* 超时时间不能大于用户的容忍时间,不然用户自己会不断重试
*
* 超时后的策略是重试或者降级
* 重试
* 超时后一般的策略是重试
* 非幂等的操作不能重试,最好设置所有操作都是幂等。
*
### 应用层上游
例如redis,mysql的连接都要设置好超时时间
## 七、回滚
* 事务回滚
* 如果是单机数据库,执行rollback回滚就可以了
* 如果是分布式事务
* 补偿机制
* 回滚事务。例如扣优惠券成功了,但是下单事务失败了,就把扣优惠券的事务回滚。这里有有个问题就是万一回滚前,进程挂了,就不能回滚了,所以要有个定时扫描机制,把未回滚的事务进行回滚
* 重试。通过定时扫描机制,把失败的事务进行重试。例如上面的下单事务
* TCC事务。每个事务分3步,Try-Confirm-Cancel。
1. 扣优惠券,下单,都执行Try。例如优惠券的Try就是把要减的优惠券冻结
2. Confirm。等Try都执行成功,执行Confirm,例如优惠券就是把冻结的优惠券扣掉
3. Cancel。如果其中一个Try失败,就执行Cancel,也就是回滚,例如把冻结的优惠券恢复
* 代码库回滚,这些Git和Svn都很成熟了
* 部署版本回滚。例如上线了新版本,但是有问题,需要回滚到旧版本
* 部署前备份旧版本,做到可以快速回滚
* 灰度。
##八、压测
系统雪崩:整个系统全部不可用
雪崩效应:由于一个小问题,导致整个系统不可用。例如整个系统都依赖于一个非核心业务,但是没有做好降级,导致一旦这个业务不可用,导致所有核心业务都不可用。
###压测
* 系统压测,用来评估系统的稳定性和性能。常用指标有:
* QPS/TPS T是事务
* 响应时间,也就是时延
* 机器负载
* 压测方式:
* 线下压测。也就是在非线上环境测试,例如测试环境。优点是不用考虑正常用户,缺点的压测结果不够真实
* 线上压测。在线上环境测试,这时要注意不要影响正常用户,包括请求和数据。
* 仿真压测。模仿真实环境的访问情况(可以通过access日志来达到)。可以对访问量翻倍的方式增加压力
* 隔离集群压测,从线上集群中摘除一台机器,用来压测
* 导流压测,把集群的所有流量导到一台机,风险比较大
* 单机压测,只在一台机上面压测,得到单机的并发能力
* 离散压测,也就是不要只访问热点数据,因为热点数据一般有缓存
###系统优化和容灾
压测后,就知道系统的性能,就能根据预期负载来决定是否需要优化性能或者增加机器
###应急预案
当上面两步都做了,项目上线后,还是会有一些突发情况,对于这些突发情况,需要做好预案。
一个预案需要有
* 预案名称
* 问题描述(也就是遇到了什么突发情况)
* 执行操作(遇到突发情况怎么处理)
* 相关人员
例如
| 预案名称| 问题描述 | 执行操作| 相关人员|
| :-------- | --------:| :------: | :------: |
|机房故障 | 机房网络不可用| DNS配置中摘掉该机房的IP| 小A|
#高并发
## 九、缓存、HTTP缓存、多级缓存
主要讲java的进程内缓存。但是现在基本都用redis缓存了,感觉需要用进程内缓存的场景不多了。
浏览器中Ctrl+F5可以强制刷新缓存
使用缓存能大幅提升系统的QPS,但是要注意下面几点:
* 缓存命中率,缓存设置了,但是大部分请求还是去DB了
* 缓存一致性,DB改了,缓存还是旧数据
* 缓存雪崩,多个缓存KEY一起过期,导致请求都去DB了
* 缓存穿透,缓存设置了,但是永远用不了
* 缓存更新。
* 过期更新。缓存设置过期时间,如果过期,就回源。这里有个坑是如果并发较大,而且回源的速度很慢,会导致多个请求同时回源,弄挂回源的服务。所以碎玉回源速度慢的业务,适用下面的定时更新
* 定时任务更新。设置定时任务,定时回源,更新缓存。
多级缓存有(从用户端到后端),缓存离用户越近越好:
* 浏览器缓存
* CDN
* 接入层缓存(Nginx+Lua+Redis)
* 应用层缓存,基本是Redis
## 十二、连接池,线程池
池化技术用于建设一些消耗,来提升性能。例如避免TCP连接和端口,避免线程创建和消耗
一般有指标:
* 最小数量,当系统空闲时,最小维护的连接数量。数量太大会导致资源占用较多,太小会起不到连接池的作用。一般这个数量乘以进程数,就是总的连接数。
* 最大数量,当系统繁忙时,最大支持的连接数量,超过就等待,用来保护上游服务。
坑
* 上游服务主动关闭连接,例如Mysql一般8小时后会主动断开空闲连接。所以业务端获取连接池里面的连接后,需要进行reconnect操作。可以再获取连接对象就检查连接是否可用,也可以在需要传输数据,也就是执行命令时,检查是否可用。Redis是后者。
* 等待超时时间,不知道是什么意思,但是好像会遇到,也就是有大量TIMED_WAIT连接
##十三、异步
书上说的异步是指处理多个IO请求的并行问题,所以这个叫并行合适点。
解决方法是从串行改为并行,但是就算改为并行,还是需要阻塞一个线程,在java中,是有线程池的, 但是没有协程,所以还是会造成一个线程的浪费。
##十四、扩容
* 垂直扩容,例如换CPU,加内存,加硬盘等
* 水平扩容,加机器
* 应用拆分,把一个大系统拆分为多个小系统,也就是微服务化。带来的问题是分布式事务,join查询等。
* 分库分表
* 分表
* 当一个表太大,导致容量和磁盘/带宽IO瓶颈(不是很明白)
* 分库
* 当一台机的性能不够的时候,分为多个库。这里的分库是不同表分不分的库。
* 分库分表
* 分库分表是原来的一张表,拆分为多张子表,放在不同的库。
* 带来的问题:
* 查询问题,由于分表是按特定的一个字段(例如用户ID)分的,如果需用用其他维度来查询,例如商品ID,就需要
* 用合并表,或者另一组分表
* ES等搜索引擎
* 上面两种方法相当于数据冗余,必然存在数据不一致请求,解决方法是1.消息队列,通知对应的业务方更新数据 2. 监听binlog日志
* 分布式事务问题,这个上面有说
##十五、队列术
队列的作用:
* 异步处理,同步任务发送消息都队列,另一个进程从队列获取消息,处理异步任务。好处是:
* 提升响应速度
* 流量削峰
* 系统解耦,系统1触发一个事件,通过发消息队列的方式,通知多个其他系统。
* 数据同步。系统1修改了数据,通过发消息队列的方式,通知多个其他系统。
## 十六、案例
主要讲京东几个业务的实现架构