微服务高可用秘诀 - 隔离
何谓“隔离”
微服务化最大的两个问题是可用性的问题和数据一致性的问题。
我们把项目从一个单体拆分为微服务,项目复杂度上升,出问题的概率自然提高了。
并且从数学与统计角度而言,由于服务数量变多了,假设单个服务的故障率不变,那么整体微服务系统的故障率则会提高。
如果我们不做任何预防手段,微服务中若有一个服务宕机,可能会连锁反应导致整个服务不可用。
所以我们采用“隔离”的手段,让影响范围可控。事实上“隔离”这种手段在工程学中很常见,比如造船行业对船舱进水风险的隔离方法:将船舱分块隔离起来,一部分船舱进水不会导致整条船完蛋。
隔离,本质上是对系统或资源进行分隔,从而实现当系统发生故障时能限定传播范围和影响范围,即发生故障后只有出问题的服务不可用,保证其他的服务仍然可用。
隔离可以分为 3 大类,7 个套路:
- 服务隔离:动静分离、读写分离
- 轻重隔离:核心业务分离、快慢隔离、热点隔离
- 物理隔离:线程(进程)隔离、机房隔离
服务隔离
动静分离
动静分离在计算机系统设计中很常见,比如:
- CPU 的 CacheLine False Sharing
- Mysql 表设计中隔离动静表从而避免 bufferPool 频繁过期
- 架构设计中对图片等静态资源做 CDN 加速。
我们拿 CDN 场景举例,静态资源做 CDN 加速好处多多,比如:
- 降低应用服务器负载,静态文件访问负载全部通过CDN。
- 对象存储存储费用最低。
- 海量存储空间,无需考虑存储架构升级。
- 静态 CDN 带宽加速,延迟低。
此外,在 Mysql 表设计中,把不怎么更新的字段和经常更新的字段分成两个表也体现了动静分离思想。
比如 archive 表和 archive_stats 表,archive_stats 存储统计信息,比如点赞数据(经常更新),把这些经常更新的数据与不怎么更新的数据隔离开以提高 archive 表缓存的命中率,优化 MySql 性能。
读写分离
读写分离就很好理解了,主从数据库、CQRS 等等都是针对读写分离的实践。
想了解CQRS可以看看这篇: DDD 中的那些模式 — CQRS
轻重隔离
- 业务根据 Level 分配资源,重要业务多给资源。
可以考虑重要业务资源独占,比如重要业务的 Pod 固定部署在某些物理机上,避免服务之间相互影响。
- 日志采集,如果用 Kafka 做中间件采集日志,那么建议把 Info 级别日志和 Error 级别日志使用两个单独的 Topic 采集,以防止 Info 级别日志量过大,导致最为重要的 Error 级别日志无法采集。
快慢隔离
我们可以把服务的吞吐想象为一个池,当洪流(一个耗时请求)突然进来时,池子需要一定时间才能排放完,这时候就会对其他小请求产生影响。 比如一个视频转码服务被超大视频攻击,本来用户上传一个 4K 视频,服务器会自动将其转码,以满足不同分辨率观看的需要,结果有一个坏蛋,恶意大量上传超大的 4K 视频,导致其他用户视频上传转码服务异常。 这时我们可以将视频大小分类,大中小视频分别走不同的通道,这样大视频转码有延迟不影响其他种类的视频。
热点隔离
将一些热点数据从 RemoteCache 提升为 LocalCache(App),这种操作可以手动触发,也可以定时任务监控触发。
物理隔离
线程隔离
主要通过线程池进行隔离,这也是实现服务隔离的基础。把业务进行分类并交给不同的线程池进行处理,当某个线程池处理一种业务请求发生问题时,不会将故障扩散和影响到其他线程池,保证服务可用。
Java 做线程池隔离:
对于 Go 来说,所有 IO 都是 Non- Blocking 的,且托管给了 Runtime,阻塞只会阻塞 Goroutine 不会阻塞线程。我们只要保证 Goroutine 数量不爆炸,那么线程就不会被耗尽,程序就不会 OOM。
如何控制 Goroutine 数量?利用超时控制、自适应保护等机制可以做到,后面会讲。
集群、机房隔离
这点很好懂,不细说,仅分享一个事故:入口 Nginx 故障,影响全机房流量入口故障。 Nginx 转发流量到下游服务,因下游服务故障且没有做超时控制,导致 Nginx 堆积大量请求,最终导致 Nginx 故障进而导致网络交换机故障,所有网络请求都进不来。 如果 Nginx 把异常流量隔离到部分集群里,事故影响范围将大大缩小。