背景
目前软件的应用越来越广泛,体量越来越大,随之而来是复杂度的急剧提升。
但是就像地球上的生物不能无限增长一样,其无时无刻不受到重力的约束;软件也一样,当其规模急剧扩大时,复杂度就相当于重力,会约束软件规模的无限制增长。
软件复杂度带来的可靠性骤降成为制约软件是否能用的重要维度。
从高可靠性的角度看,软件都可以从业务规则多少、动态行为丰富程度、高可靠性要求三方面来分类,其中:
可以看出,电信系统和云基础设施都属于业务规则复杂、动态行为极度丰富、高可靠性高的场景。这些场景可靠性带来不是好不好用的问题,而是能不能用的问题,所以必须进行充分的可靠性设计。
高可靠性设计的方法很多,其中兜底方案是最后一环,在前面的可靠性保护都失效的情况下,提供最后的保护。
思路
大型软件系统的功能是由一系列的模块组成,各模块的业务功能和技术特征也很可能大相径庭,不同模块进行可靠性设计的手段也随之变化。
对于系统高可靠性模块设计成本无疑是较高的,比如对于心脏起搏器里面的控制模块可靠性设计就需求投入巨大的人力物力。
但是并不是组成系统的每个模块都需要这么高的可靠性要求,这就需要可靠性设计器,先对系统从场景视图和运行视图的角度来进行模块化解耦;其次对不同的模块定义不同的可靠性等级,针对不同等级进行不同的可靠性设计。
比如航空航天等软件中模块,就可以划分为A-E五个等级。
对于高等级的模块,整体设计思路:
并联隔离+监控自愈
所谓并联是指模块在并发体(进程、线程、jvm、docker容器等)中运行,从而降低或消除波及影响和级联故障。
设计的具体要点:
如上图,高等级模块划分为工作进程、辅助进程(组)和监控进程。
1、进程级隔离
高等级模块必须运行在单独工作进程(以下统称工作进程)中,处理核心业务逻辑,防止受到其他进程错误的传播和波及影响。
2、不稳定因素隔离
高不可靠操作(比如操作文件、访问硬件等)、“复杂代码、脏代码”(密集计算,浮点运算)都需要从工作进程中隔离出去,使用单独进程(以下统称辅助线程)操作,辅助线程跟工作进程并联。
3、detector
高等级模块还需要增加基于独立进程的detector,其保留核心数据,并对工作进程进行状态监控,出现不可恢复等异常场景时进行外部干预,从而实现系统的高可靠性。
如果detector和工作进程合一,工作进程自己在极端异常场景下都很可能自己挂掉,也就进行不了兜底。
由于是独立进程,所以detector和工作进程、辅助进程也是并联关系。
4、确定性运行设计
系统无论运行多少次,都会按照同样的方式运行。这就要求系统解耦对全局变量和外部依赖的约束。
比如工作进程,辅助进程、detector无论多少次运行,都会得到相同的效果,这些进程多次启动,无论其中哪个进程启动顺序不一样,执行结果都一样。
5、error kernel
要点是系统设计时就要标识错误等级,即哪些错误必须要处理(比如工作进程间状态不一致),哪些错误可以不处理(比如日志操作失败)。
必须要处理的,放到工作进程。
上图基于进程并联隔离、监控自愈的可靠性设计示意图,其中R相当于各个进程或模块的可靠性概率。通过在工作进程、辅助进程和detector进程并联,进一步再把所有高等级模块并联,就可以提升总体系统的可靠性概率。
他山之石可以攻玉,这种进程间隔离实现高可靠性兜底的思路有很多成熟的案例,其中应用最广泛和充分经过时间检验的是操作系统设计思路,它对“并联隔离,监控自愈”实现的非常到位,是兜底设计的典范。
操作系统是基于进程隔离的,可以分为内核态和用户态。
其中用户态负责高复杂高风险的操作,内核态进行监控和兜底。
首先我们先来看看,如果只有内核态没有用户态,会发生什么?
如果整个操作系统没有用户态,所有进程都将运行在内核态,这会带来很多问题,比如对内核的需要谨慎执行的操作失去控制,或者进程间互相波及造成错误级联传播。
随便举几个具体的例子:
- 如果某个进程的代码写得有问题,运行了一段时间崩溃了,崩溃的同时可能会导致其他进程也崩溃,带来严重的级联问题。
- 所有进程将可以读写任意一个进程的内存的数据,安全性方面将受到极大的挑战
- 运行一段 shell 脚本,居然把一直在运行的服务进程搞挂了
- 运行一个用户密码处理的程序,另一个程序把所有用户密码全部读走
- 一个进程在写数据库,另外一个程序让这个写进程强行退出,造成数据不一致。
而有了内核态和用户态,可以有以下好处:
1、用户如果有内核相关需要谨慎执行的操作,首先它需要转换为内核态,操作系统会进行一些安全检查和波及影响检查(会不会影响其他进程或窃取其他进程数据);其次操作系统会统一择机调度这个命令,防止波及影响。
2、高风险高复杂操作(比如和外部系统交互、浮点运算、密集计算等)都在用户态执行,如果用户态进程崩掉,内核态就可以感知并把用户态进程杀掉,把资源回收。
从而实现了用户进程的兜底操作。
另外,内核态进程和用户态进程的内存区间也是隔离的。
用户态进程仅可以访问用户态内存块,而内核态可以访问所有内存块。用户态进程之间通过虚拟内存技术进行隔离。
小结
1、操作系统分为内核态和用户态,各自访问的内存区间也是相互隔离的,如果用户态如果崩溃,风险不会传播。
2、用户态如果进行内核态需要谨慎执行的操作,需要切换到内核态并接受审核,并由内核态择机执行(防止冲突),降低操作风险。
3、用户态进程执行相对高风险高复杂操作,内核态维护进程核心数据并订阅或定期检测用户态进程状态,如果用户态进程崩掉,内核态会回收用户态进程资源并清理现场。
综上,对系统高可靠性设计,我们可以借鉴操作系统的设计思路实现隔离:
1、把系统根据可靠性失效带来的损害分为不同等级相互解耦的模块。
2、高等级模块进行进程间隔离,分为工作进程、辅助进程和监控进程,其中监控进程使用独立的内存区,并且保存工作进程重启需要的核心数据。
3、高风险操作都移植到辅助进程,如果挂掉,杀掉重启即可。
4、监控进程订阅或定期侦测工作进程组的状态(进程的状态和核心数据是否有不符合预期的情况),及时干预或重启(使用期保存的工作进程的核心数据),通过服务降级实现快速恢复业务。