微服务可用性设计之隔离
隔离
隔离,本质上是对系统或资源进行分割,从而实现当系统发生故障时,能限定传播范围和影响范围。(也就是当一个资源不可用,则将这个无法使用的资源隔离出系统之外)
即发生故障后只有出问题的服务不可用,保证其他服务仍然可用。
服务隔离的方式
- 动静分离:通过
CDN
进行静态资源加速 - 读写分离:
CQRS
,一个进程负责写入,另外一个进程负责写入,时间差几秒钟可以接受的情况。
轻重隔离
- 核心:
- 快慢:
- 热点:
物理隔离
- 线程:
- 进程:
- 集群:
- 机房:
服务隔离
动静隔离
小到 CPU
的 cacheline false sharing
、数据库 mysql
表设计中避免 bufferpool
频繁过期,隔离动静表(缓存小表,增加 buffer pool
命中率。),大到架构设计中的图片、静态资源等缓存加速。本质上都是体现的一样的思路,即加速、缓存访问变换频次小的。
“缓存行伪共享”(cacheline false sharing)是指在多线程编程中,当多个线程同时访问位于同一缓存行(cacheline)中的不同变量时,由于缓存一致性协议的机制,可能导致额外的性能开销。尽管这些变量彼此之间没有直接的依赖关系,但由于它们位于同一缓存行中,当一个线程修改其中一个变量时,会导致整个缓存行无效,从而使其他线程需要重新从主内存中读取自己所需的变量。
这种现象会降低程序的性能,因为频繁的缓存行无效和内存访问会导致额外的延迟。为了减少缓存行伪共享的影响,可以采取措施,如通过调整变量的布局,使得不同的变量位于不同的缓存行,或者使用填充字节(padding)来确保变量之间有足够的间隔,避免它们位于同一缓存行中。这样可以减少缓存行无效的次数,提高程序的性能。
比如 CND
场景中,将静态资源和动态资源 API
分离,也是体现了隔离的思路:
- 降低应用服务器负载,静态文件访问负载全部通过
CDN
- 对象存储存储费用最低
- 海量存储空间,无需考虑存储架构升级
- 静态
CDN
带宽加速,延迟低
又例如稿件表:
archive
:稿件表,存储稿件的名称、作者、分类、tag、状态等信息,表示稿件的基本信息。在一个投稿流程中,一旦稿件创建改动的频率比较低
archive_stat
:稿件统计表,表示稿件的播放、点赞、收藏、投币数量,比较高频的更新。随着稿件获取流量,稿件被用户所消费,各类计数信息更新比较频繁
MySQL BufferPool
是用于缓存 DataPage
的,DataPage
可以理解为缓存了表的行,那么如果频繁更新 DataPage
可以理解为缓存了表的行,那么如果频繁更新, DataPage
不断会置换,会导致命中率下降的问题。所以在表设计中,仍然可以沿用类似的思路,其主表基本更新,在上游 Cache
未命中,透穿到 MySQL
,仍然有 BufferPool
的缓存。
读写分离
- 主从:对一致性要求不高的,从库通过
binlog
实现更新,读取请求 Replicaset
:微服务多副本机制,增加读的性能,增加冗余CQRS
:读写在进程上分离,针对读写是两种角色的操作,而进行的分发。例如草稿库的设计,草稿库同步到先上库的时候,同时刷新缓存,保证缓存一直是最新的,而且不必设置过期时间,或者不同的库、相同库不同的表,保证读和写是不同的对象。
轻重隔离
核心是根据资源的重要性,将重要资源和不重要资源隔离开来,同时重要资源之间也相互隔离,以保证一些资源崩溃的时候,另外一些资源不会受到影响。资源一般就是服务和数据。而判断是否需要隔离的标准则很多,可以是业务的业务价值,服务 QPS,数据的价值,数据被访问 QPS 等,由此引申出来了核心隔离、快慢隔离、热点隔离。
核心隔离
业务按照 Level 进行资源池划分(L0/L1/L2)
- 核心/非核心的故障域的差异隔离(机器资源、依赖资源)
- 多集群,通过冗余资源来提升吞吐和容灾能力
快慢隔离
如果将服务的吞吐想象为一个池,当突然洪流进来时(大量的请求,或者小请求但是请求吃很多资源),池子需要一定时间才能排放完(处理某一个请求,导致其他请求无法处理),这时候其他支流在池子里待的时间取决于前面的排放能力,耗时就会增高,对小请求产生影响。
日志传输体系的架构设计中,整个流都会投放到一个 kafka topic
中(早期设计目的:更好的顺序 IO ),流内会区分不同的 logid
,logid
会有不同的 sink
端,他们之前会出现差速,比如 HDFS
抖动吞吐下降, ES
正常水位,全局数据就会整体反压。
- 按照各种维度隔离:
sink
、部门、业务、logid
、重要性(S/A/B/C)
业务日志也属于某个 logid
,日志等级就可以作为隔离通道。 (变成 logid
之后隔离性更好)
热点隔离
热点即经常访问的数据。
很多时候我们希望统计某个热点数据中访问频次最高的 Top K
数据,并对其访问进行缓存。比如:
小表广播(被动预热):从
remotecache
提升为localcache
,app
定时更新,甚至可以让运营平台支持广播刷新localcache
。使用atomic.Value
主动预热:比如直播房间页高在线情况下
bypass
旁路监控主动防御。
物理隔离
比较多的是指进程隔离、线程隔离和集群隔离。
线程隔离在 Java
这种语言比较常见,一般是线程池隔离。
go
的框架在这方面使用得比较少,主要是因为 goroutine
非常轻量。在一些特别复杂的框架里面也会有应用 goroutine 池,但是总体来说不是主流,复杂性太多而收益太小。
目前来说,大规模采用 docker
之类的容器技术应该算是最为典型的。
线程隔离
主要通过线程池进行隔离,也是实现服务隔离的基础。把业务进行分类并交给不同的线程池进行处理,当某个线程池处理一种业务请求发生问题时,不会将故障扩散和影响到其他线程池,保证服务可用。
例如 Tomcat
的线程池,需要同时处理用户服务、推荐服务、积分服务。
当推荐服务无可用线程导致调用失败,有效的线程隔离可以不影响用户服务和积分服务。
对于 Go 来说,所有 IO
都是 Nonblocking
,且托管给了 Runtime
,只会阻塞 Goroutine
,不阻塞 M
,所以只需要考虑 Goroutine
总量的控制,不需要线程模型语言的线程隔离。
在 Java
中,除了线程池隔离,也有基于信号量的做法。
当信号量达到 maxConcurrentRequests
后,再请求会触发 fallback
。
而线程池则是当线程池达到 maxSize
后,再请求会触发 fallback
接口进行熔断。
进程隔离
容器化(docker
),容器编排引擎(k8s
)。比如早期在 KVM
上部署服务,到 Docker Swarm
;再到 Kubernetes
,去全部托管在线应用;迁移到弹性公有云;离线Yarn
和在线 k8s
做离线混部(错峰使用),之后计划弹性公有云配合自建 IDC
做到离线的混合云架构。
集群隔离
例如 gRPC
,可以使用多集群方案,在 metadata
中通过租户实现隔离,以及多集群方案,逻辑上虽然是一个应用,但是物理上部署多套应用,通过 cluster
区分。
多活减少完毕后,应用可以划分为:region.zone.cluster.appid
Case Stduy
早期转码集群被超大视频攻击,导致转码大量延迟;
- 进行集群隔离,大视频和小视频隔离
- 进行用户标签隔离,将用户的垃圾视频隔离
入口
Nginx(SLB)
故障,影响全机房流量入口故障- 将异常流量切换到其他的
SLB
上,避免影响核心交换,进而影响全局SLB
- 将异常流量切换到其他的
缩略图服务,被大图实时缩略吃完所有
CPU
,导致正常的小图缩略被丢弃,大量 503- 与转码一样,可以通过大图片和小图片隔离
数据库实例
cgroup
未隔离,导致大SQL
引起的集群故障通过
cgroup
限制CPU
和内存INFO
日志量过大,导致异常ERROR
日志采集延迟
推荐阅读
阿里微服务之殇及分布式链路追踪技术原理