导读:作为一名Java程序员,相信同学们都听说过微内核架构设计,也有自己的理解。那么微内核是如何被提出来的?微内核在操作系统内核的设计中又有什么作用?本文从插件化(Plug-in)架构的角度来诠释微内核架构设计,通过微内核架构和微服务架构的对比,分享其对微服务设计的参考意义。
关于微内核架构设计现在比较热,听起来好像是操作系统内核相关的,作为Java程序员,操作系统内核那么遥远的事情,好像和我们没有什么关系。但是如果我说微内核其实就是插件化(Plug-in)架构,你一定会一脸疑惑,“你居然向Java程序员解释什么是插件化架构?我每天都在用啊,Eclipse、IntelliJ IDEA、OSGi、Spring Plugin、SPI等,哪个不是插件化架构。我的一些项目也是采用插件化设计的,如使用插件实现流程控制定制等等”。但是别着急,即便是我们每天都在使用的技术,而且大多数人也都知道,如果我们能将其阐述得更清楚,并且能从中发现一些问题,做出一些优化有助于以后的架构设计,那么大多数人在日常的设计和开发中都能受益,岂不是更好。现在我们就来聊一聊微内核架构设计。
一 微内核设计之操作系统内核
微内核设计其实就是插件体系。我们都知道,操作系统内核诞生得比较早,所以插件化最早被用在内核设计上,于是就有了微内核设计这一称呼。
微内核是这样一种内核:它只完成内核不得不完成的功能,包括时钟中断、进程创建与销毁、进程调度、进程间通信,而其他的诸如文件系统、内存管理、设备驱动等都被作为系统进程放到了用户态空间。说白了,微内核是相对于宏内核而言的,像Linux就是典型的宏内核,它除了时钟中断、进程创建与销毁、进程调度、进程间通信外,其他的文件系统、内存管理、输入输出、设备驱动管理都需要内核完成。
也就是说,微内核是相对宏内核而言的,宏内核是一个包含非常多功能的底层程序,也就是我们现在讲的Monolith。它干的事情非常多,而且不是可插拔的,修改一些小的功能,都会涉及到整个程序的重新编译等,比如一个功能出现了一个小bug,可能导致整个内核都出问题。这也是很多人将Linux称为monolithic OS的原因。而微内核只负责最核心的功能,其他功能都是通过用户态独立进程以插件方式加入进来,然后微内核负责进程的管理、调度和进程之间通讯,从而完成整个内核需要的功能。基本一个功能出现问题,但是该功能是以独立进程方式存在的,不会对其他进程有什么影响从而导致内核不可用,最多就是内核某一功能现在不可用而已。
微内核就是一个运行在最高级别的程序片段,它能完成用户态程序不能完成的一些功能。微内核通过进程间通信来协调各个系统进程间的合作,这就需要系统调用,而系统调用需要切换堆栈以及保护进程现场,比较耗费时间;而宏内核则是通过简单的函数调用来完成各个模块之间的合作,所以理论上宏内核效率要比微内核高。这个和微服务的架构设计一样,我们将Monolith应用划分为多个小应用后,系统的设计就变得比较复杂了,之前都是应用内部函数调用,现在要涉及网络通讯、超时等问题,同时响应时间会被拉长。
聊到这里,相信大家对微内核和宏内核已经有了一个大致的了解,看起来各有千秋。但是宏内核有一个最大的问题就是定制和维护陈本。现在的移动设备和IoT设备越来越多,如果要把一个庞大复杂的内核适配到某一设备上,是一件非常复杂的事情,如果很简单的话,那么把Linux内核适配到Android内核,甚至到Tesla等车载系统,基本上人人都可以做了。
因此我们更需要一个微内核的架构设计,方便定制,而且非常小,可以实现功能的热替换或者在线更新等,这就是微内核被提出来的核心需求。但是微内核有一个运行的效率问题,所以在微内核和宏内核之间,又有了Hybrid内核,主要是想拥有微内核的灵活性,同时在关键点上有宏内核的性能。微内核设计在理论上确实有效率问题,但是随着芯片设计、硬件性能提升等,这方面或许已经有了非常大的提升,已经不再是最关键的问题。
总体下来,内核设计有三个形式,如下:
二 插件化(Plug-in)架构设计
上面聊了微内核在操作系统内核设计中的作用,接下来我们就开始讨论更通用的插件化架构设计,毕竟这个词大家都明白。
插件化架构非常简单,就两个核心组件:系统核心(Core System)和插件化组件(Plug-in component)。Core System负责管理各种插件,当然Core System也会包含一些重要功能,如插件注册管理、插件生命周期管理、插件之间的通讯、插件动态替换等。整体结构如下:
插件化架构对微服务架构设计帮助非常大,考虑到隔离性,插件可能是以独立进程方式运行的,那么这些进程如果扩展到网络上,分布在众多的服务器上,这个就是微服务架构的原型,所以了解微内核的同学都不屑于和你讨论微服务架构,相信你也明白了,除了IT传统的鄙视链因素,原理上确实就是这么回事。
回到微服务架构设计场景,我们将Plug-in component重新命名为服务(Service),这个和微内核设计中的服务也差不多,这个时候微服务和微内核就差不多了,都涉及到服务注册、管理和服务之间的通讯等。那我们看一下微内核是如何解决服务之间的通讯问题的?以下摘自维基百科:
因为所有服务行程都各自在不同地址空间运行,因此在微核心架构下,不能像宏内核一样直接进行函数调用。在微核心架构下,要创建一个进程间通信机制,通过消息传递的机制来让服务进程间相互交换消息,调用彼此的服务,以及完成同步。采用主从式架构,使得它在分布式系统中有特别的优势,因为远程系统与本地进程间,可以采用同一套进程间通信机制。
也就是说,采取的是基于消息的进程间通讯机制。消息最简单,就两个接口:send和receive,消息发送出去,然后等着收消息,处理后再发消息就可以了,这里大家应该也知道了,这个是异步的。回到插件化架构设计中,Plug-in组件设计包含交互规范,也就是和外界相互通讯的接口,如果是基于消息通讯的话,就是send和receive接口,可以说是非常简单的。
但是这里还有一个问题,那就是进程间通讯。你可能会问,这个有什么好疑问的,就是两个进程之间相互发消息呗。但是这里有一个最大的疑问,那就是进程间通讯是否有第三者介入?如下图:
当然在操作系统的内核设计中,一定是通过内核进行转发的,就是我们理解的总线架构,内核负责协调各个进程间的通讯。这个大家也能理解,如果进程A直接发给另外一个进程B,必然要了解对应的内存地址,微内核中的服务是可以被随时替换的,如果服务不可用或者被替换,这个时候要通知和其通讯的其他进程,是不是太复杂?刚才已经提到,只有send和receive接口,没有其他通知下线、服务不可用的接口。在微内核的设计中,一定是通过总线结构,进程向Kernel发送消息,然后kernel再发送给对应的进程,这样的一个总线设计。实际上很多应用内部在做Plug-in组件解耦时,都会使用EventBus的结构,其实就是总线的设计机制。
为何婆婆妈妈说这些?因为非常关键。分布式的进程通讯是微服务的核心,我们理解的服务到服务的通讯,就是服务A启动监听端口,服务B会和服务A建立连接,然后两者通讯即可。这个方式和微内核设计中内核负责消息接收和转发的总线架构设计是不一样的。如采用HTTP,HSF等通讯协议时,相当于kernel告知通讯的双方各自的地址,然后它们之间就可以通讯了。然后就没有Kernel什么事情了,也不会用到什么总线的结构设计,这个就是传统的服务发现机制。
但是还有一种模式,就是完全透明的插件化通讯机制,如下图:
Plug-in组件,也就是微服务架构中的服务,是不能直接通讯的,而是需要Core System进行转发。这样做的好处和微内核架构一样,插件相互之间无直接联系,彼此之间非常透明,例如服务A下线后,完全不需要通知其他服务;服务A被替换,也不需要通知其他服务;服务A从数据中心1到数据中心2,也不用通知其他服务;即便服务N和服务A之间网络不互通,两者之间也能通讯。
这里有个问题:性能问题。我们都知道,两点之间,直线段最短。为何要多绕一下到Core System呢?这就是微内核和宏内核之间的争论之处,使用函数调用非常快,而进程间的消息通讯则是非常慢的,但是这种通过中介进行通讯机制的好处也是非常明显的。那么如何提升这种基于总线的通讯性能呢?当然有,比如选择高性能的二进制协议,HTTP 1.1这种文本协议就不需要了;采用Zero Copy机制,可以快速进行网络包转发;好的网络硬件,如RDMA;好的协议,如基于UDP的QUIC等。总结下来,和微内核一样,这种微服务通讯的性能是可以提升的。当然如果实在受不了这种性能,在关键场景,你可以采用Hybrid模式,混入一些服务之间直接通讯的设计,但只能在性能极致的场景中使用。
此外,插件化架构中的插件组件是各种各样的,通讯的机制也各不一样,一些是RPC的,一些是Pub/Sub的,一些是无需ACK的(如Beacon接口),还有一些是双向通讯的等等。当然你可以选择不同的通讯协议,但是这里有一个问题,就是Core System需要理解这个协议,然后才能进行消息路由。这个时候Core System需要编写大量的Adapter来解析这些协议,例如Envoy包含各种filter来支持不同的协议,如HTTP、MySQL、ZooKeeper等,但是因此Core System就会变得非常复杂且不稳定。
另外可以选一种通用的协议,Core System只支持这一种协议,各个插件之间都基于该协议通讯,至于服务和其他外部系统如何通讯,如数据库、github集成等,这些Core System并不关心,那只是Service内部的事情。目前比较通用的协议是gRPC,如K8s内部都会采用该协议,另外Dapr也采用gRPC协议做服务集成,因为gRPC提供的通讯模型基本可以满足大多数的通讯场景。当然另外一个就是RSocket,提供更丰富的通讯模型,也适用于Core System这种服务间通讯场景。对比gRPC,RSocket可以运行在各种传输层上,如TCP、UDP、WebSocket、RDMA等,相反的,gRPC目前只能运行在HTTP 2之上。
三 服务通讯的延伸
前面说到,最好由插件化架构设计的Core System作为服务之间消息通讯的路由,如果是这样的话,就会产生一种Broker模式,当然也有可能是Agent。这里大家一定会想到Service Mesh,没错。当然你可以选择Agent Sidecar模式,也可以选择中心化的Broker模式,这两者的功能都是一样的,只是处理的方式不一样而已。Agent基于服务注册和发现机制,然后找到对方服务的Agent,再进行两个Agent之间的通讯,只是省掉服务之间的调用的开销。但是Broker是集中式的,大家都向Broker发送和接收消息,不涉及服务注册发现机制,不涉及服务元信息推送,就是总线结构。
我现在做的就是基于这种Broker的总线的架构设计,在RSocket Broker中,也是采用微内核架构设计,当然未必做得最好 。RSocket Broker核心就是管理注册的服务、路由管理、数据采集等,而不会添加过多的功能,和Core System的设计理念一样,只添加必须的功能。如果你要扩展整个系统更多的功能,如发短信、发邮件、对接云存储服务等,需要编写一个Service ,然后和Broker对接一下,再从broker那里收消息(receive),处理完毕后再发送(send)给Broker就可以了。总体结构如下:
有不少同学会问,当服务实例的负载太高的时候,Broker如何实现动态扩容呢?Broker会给你提供数据,如一个服务实例QPS,至于是否扩展,你只需要写一个服务,从Broker上采集数据,分析后,调用K8s API进行扩容即可,Broker并不负载这些业务功能,它只会添加非常必要的功能,这个和Core System设计是一样的。
回到插件化架构的灵活性上,如果系统中有一个KV存储的插件,你只要遵循消息格式或者通讯接口,就可以保存KV数据。但是你并不太关心是Redis存储的,还是Tair存储的,或者是云端的KV服务,这就为服务标准化和可替换性提供了很好的基础,这对应用上云或云原生化帮助非常大,整个系统有非常大的灵活性。
四 总结
其实有非常多的书有关于微内核的介绍,操作系统的图书就不用说了,另外两本书也非常不错,对通用架构设计帮助也非常大,尤其是微服务的场景,我也是参考这两本书写这篇文章的。
微内核架构设计对微服务设计有非常好的参考意义,但是微服务有一个非常大的问题就是服务边界的划分,对比操作系统,已经发展几十年,而且非常稳定,功能划分非常容易。而微服务架构是为业务服务的,虽然面对的业务可能已经存在上百年,但是软件化、数字化和流程化并没有多少年,加上现实业务的复杂性,还有各种妥协,个人认为微服务架构会更复杂一些。