高并发是为了让系统“有效率”,高并发是让系统“更可靠”。
高可用是架构设计中必须考虑的,也是技术面试时经常问到的。
下面整理了一些常用的高可用策略,包括:
(1)多副本
(2)隔离
(3)限流
(4)熔断
(5)降级
(6)灰度发布与回滚
(7)监控体系
(8)日志报警
避免单点,不把鸡蛋放到一个篮子里。
例如网关、应用服务器、缓存服务器、数据库……,通常都会做多副本。
像网关、应用服务器这类无状态的,多副本比较好做,但像数据库、缓存这类有状态的,多副本时就必然涉及到数据同步的问题。
例如消息队列的发布订阅机制就是一个常用的方式。
还有像 Redis cluster、MySQL 提供了 master-slave 复制机制。
需要注意,做数据同步就会涉及数据一致性问题,而数据一致性与可用性又存在矛盾。
例如 MySQL 集群使用非同步复制机制,复制存在延时,当master宕机后,有一小部分数据还没来得及同步,如果把slave切换为master,那部分数据就丢了。
这时,是保护现场,尽快修复master,还是立即切换?
通常都会牺牲一定的数据一致性来保证可用性,因为大部分数据都是可以修复的,例如人工操作、补偿机制,问题不大。但如果一个规模较大的系统有10分钟不可用,那影响就很糟糕了。
隔离是指将系统资源分隔开,在系统发生故障时可以限定影响范围,不会出现雪球效应。
主要形式:
(1)数据隔离
对数据分开存储,例如核心数据与非核心数据在物理上彻底分离。
(2)机器隔离
类似VIP服务,比如一个服务有很多调用者,其中几个调用者是大户,调用量很大,可以单独用一组机器专门服务这几个调用者。
(3)线程池隔离
比如使用Tomcat,开了500个线程,最多同时处理500个请求。
后面有多个服务,其中某个服务在某段时间访问量非常大,把500个线程都耗尽了,并且服务的响应又比较慢,导致整个服务器都卡死了。
可以使用线程池隔离,为每个服务开一个线程池,而不是共用一个线程池,互不影响。
(4)信号量隔离
比如有10个并发请求去调用一个服务,都要获取一个信号量才能真正去调用。
信号量有限,比如有5个,那么就有5个请求需要进入队列排队。
队列也有上限,如果队列也满了,那么就有请求会走fallback流程,从而达到限流和防止雪崩的目的。
限流在生活中也很常见,比如景点限流、地铁限流。
(1)技术层面限流
例如限制并发数,根据系统最大资源量进行限制,如数据库连接池、线程池、Nginx的limit_conn模块。
还有限制速率,如 Guava 的 RateLimiter、Nginx 的 limit_req 模块。比如通过测试知道某接口的QPS为2000,就可以限流在这个量,当并发量超出时,直接拒绝提供服务,保证不被压垮。
(2)业务层面限流
例如秒杀活动,一共有100件商品,但有好几万人参与,可以只放500个进来抢,后面的直接通知秒杀结束即可。
当电路出现问题(如短路、温度过高),可能烧毁整个电路时,保险丝会自动熔断,保护电路。
系统设计时也要有保险丝的思路。
(1)根据请求失败率做熔断
如果某个服务在短时间内频繁的超时或者报错,就开启熔断,也就是不调用它了。
过一定时间后再次调用,如果还是有问题,继续熔断。
(2)根据请求响应时间做熔断
统计服务的平均响应时间,超出阈值后就开启熔断。
限流和熔断的区别,限流是服务端对自己的保护,熔断是客户端对自己的保护。
比如电商系统,核心是购买流程,像个性推荐系统等,在系统压力很大时,就可以停止服务,这就是降级,把非核心的服务暂停,以保障核心业务。
(1)新功能上线的灰度
上线新功能时,可以先让一小部分人看到,如果没有问题,再逐步开放。
比如根据user_id划分流量,或者根据用户标签划分。
(2)旧系统重构的灰度
对旧系统重构后,通常不是马上全部切换为新系统,新旧系统会并存一段时间。
比如开始时10%的用户使用新系统,90%使用旧系统,新系统有问题就及时修改,影响范围不大。
新系统越来越成熟稳定,在此过程中逐渐增加新系统用户的占比,最终完成切换。
(3)回滚
如果发现新功能新系统有比较严重的问题,可以回滚为旧系统。
一种是整体回滚,直接把整个系统回滚到上个版本。
另一种是功能回滚,在开发新功能时做了开关,可以通过开切换新旧功能。
系统现在可用吗?我们得经常看一看,检查一下。
监控系统就是帮我们全方位的观察系统状态。
(1)资源监控
例如 CPU、内存、磁盘、网络 ……
(2)系统监控
例如某些URL访问是否失败、接口调用是否正常、接口平均响应时间、JVM回收情况 ……
(3)业务监控
例如订单系统有个关键的业务指标:订单支付成功率。
这个指标是否异常?可以与历史数据进行比较,知道了历史分布曲线,如果今天发生了剧烈波动,就可能出问题了。
日志可以帮我们快速定位问题,但这是被动的,更应该通过日志进行主动报警、主动解决。
写代码时,对可预见的问题提前写好日志。例如,通过 ASSERT 语句可以断定异常,异常的原因可能是bug、上游传入了脏数据、下游返回了脏数据……
针对有问题的地方提前写好错误日志,然后对日志进行监控,实现主动报警。