在日常开发中,经常听到大家说一句话“任何需求都可以通过一个间接的的中间层来解决”。今天,通过几个 case 就“分层”话题梳理下自己的思考,其中,有些 case 比较直观,而有些不那么直观,甚至有些微妙,需要我们自己多品味。这意味着学习过程需要我们不断将新知识与旧知识进行关联,形成自己的知识体系,而非一个个知识孤岛。
图片
分层设计将软件划分成若干层,每一层只解决一部分问题,通过所有层的协作来完成整体目标。一个复杂问题通过分解成一个个系统子问题,这样就有效的降低了每个子问题的规模与复杂度。
分层设计带来的好处:
图片
早期,软件开发是机器语言,直接用二进制 0 和 1 表示机器可以识别的指令和数据,看起来像这样:
0010000100100011
这就是计算机 CPU 唯一可以理解的语言。对人类为说,二进制的程序是不可读的。
为了解决语言可读性的问题,汇编程序诞生了。汇编程序是人类可读的机器代码。它又被称为“符号语言”,使用助记符来代替机器的操作码。
汇编语言是二进制的文本形式,与 CPU 的指令是一一对应的关系。而我们不同的 CPU 体系结构(比如 PC 的 X86、嵌入式的 ARM) 是不同的,面向机器的语言带来的问题就是:对于不同的 CPU 体系架构,就需要不同的汇编语言。
为了解决语言对机器的无关性,高级语言诞生了。一条高级语言通常由若干条机器语言实现的,并且不具有对应性。
高级语言让开发者不需要关注底层 CPU 体系结构与指令,只关注业务即可。
计算机语言的发展就是不断的抽象,只有通过抽象,将一个复杂的的系统变成一层层的接口集合,让我们每次只需要考虑关注当前层集合内的逻辑,而不用去考虑当前层次以上或者以下的复杂度,才有可能让我们从复杂系统中解放出来,逐步理解以及构造一个复杂系统。
图片
操作系统内核,可以简化理解成三大层:
不管是 ARM 体系结构,还是 X86,选择一个进程调度的算法是可以相同的,需要改变的进程切换相关代码,因为不同的硬件平台的上下文是不同的,CPU 的寄存器也不同。这时候最好的设计是分层,当操作系统运行在不同的硬件平台时,就只需要修改硬件平台相关层代码,实现操作系统的高可移植性。
操作系统有两个关键设计:
操作系统负责管理物理内存,而用户进程使用虚拟内存。操作系统呈现给用户进程的是连续的虚拟空间,但不一定是连续的物理空间。因为物理内存被整个 OS 共享。
什么是 MMU 呢?它是硬件,即内存管理单元,它对 CPU 发出的访存地址进行映射与检查,可以让处理器发出的访存地址访问不同的物理内存单元。
如果将计算机上有限的物理内存分配给多个应用程序使用,如果让应用程序直接访问物理内存,如果没有 MMU 这层抽象呢?带来的问题是每个应用程序地址空间不隔离,内存使用率低,程序运行地址也无法固定。
图片
解决的问题:虚拟内存 VA 与物理内存 PA 的映射——通过在 CPU 与内存之间加入 MMU 抽象层,让 CPU 在运行指令时发出的 VA 虚拟地址通过 MMU 转换后变成 PA 物理地址,然后再去访问物理内存。
图片
MMU 引入带来的好处:
这是我认为最经典、最本质、最受启发的中间抽象层的设计。
CPU 访问外设有两种方法;
图片
外设接口中的 IO 寄存器(即 IO 端口)与主存单元一样看待,每个端口占用一个存储单元的地址,将主存的一部分划分出来用作 IO 的地址空间。
把外设的寄存器当做是一个内存地址,从而 CPU 以类似访问内存相同的方式来操作外设。
对 IO 外设的端口映射到一个物理内存单元地址,在 CPU 与外设之间的“内存”抽象层,带来好处是访问内存一样去访问外设。
Linux 中的内核硬件层设计、MMU、CPU 与 IO 外设通信设计处处体现了分层 / 中间层的设计思想。
从最底层的物理链路层层层向上封装抽象,解决了复杂的网络通信的问题。同样的,任何复杂的问题,通过分层最终总能够回归最本质、最简单。这个分层架构,对所有开发者而言,再熟悉不过,它的引入是想与后续介绍的 Netty 形成对比。这里先卖个关子,后面解开谜底。
图片
举例说明::
来自杭州西湖区某个小区的商务人士来京出差后,被确诊新冠肺炎,实施在京隔离措施,同时北京将此报告先发给浙江省,接着浙江省发给杭州市政府,然后市政府再向西湖区发送,最后到达某小区。这个发送报告过程也是分层报告思想。
图片
DNS (domain name system) 是域名系统,是用来将主机转换为 IP 地址的服务。我们有至少三种方式在互联网上标识一台主机、主机名、IP 地址以及 MAC 地址。为什么有引入 DNS 中间抽象层呢? 主要是主机名便于记忆,而 IP 地址方便于在计算机网络设备的处理,因此需要设计出一个 DNS 协议 (中间层) 来做主机名到 IP 地址的转换。
图片
ARP(address resolution protocol) 是地址解析协议,它根据 IP 地址来获取物理地址。上面也谈到,MAC 与 IP 都可以用来标识一台主机。那二者区别是什么?
同一个局域网中的一台主机和另一台主机通信的时候,需要通过 MAC 地址进行定位,之后才能进行数据包的传送。
而在网络层和传输层中,主机之间是通过 IP 地址来定位的,对应的数据包中必须携带目标主机的 IP 地址, 而没有 MAC 地址。
因此,ARP 协议 (中间层) 用来实现从 IP 到 MAC 地址的转换。
Netty 提供了异步的,基于事件驱动的网络应用程序框架。目前分布式搜索引擎,Spark 框架底层是扩展使用 Netty 框架。Netty 本身的架构理解有些曲线,为了讲清楚,我还是希望循序渐进方式,通过它的发展历史来一步步介绍。先铺垫再介绍,大家需要一些耐心。
图片
思路:
问题:
图片
思路:
问题:
主 React 处理所有 socket 连接事件的监听和响应,而从 React 处理所有 socket 的读写事件的监听与响应。主从 React 都在多线程中运行。
图片
Netty 主要基于主从 Reactor 多线程模型发展出来的。
图片
前面 Netty 的发展阶段都是铺垫,Nettty 逻辑架构为典型网络分层架构设计,从下到上分别为网络通信层、事件调度层、服务编排层。
图片
网络通信层 :它执行网络 I/O 操作,核心组件包含 BootStrap、ServerBootStrap、Channel。——Channel 通道,提供了基础的 API 用于操作网络 IO,比如 bind、connect、read、write、flush 等等。它以 JDK NIO Channel 为基础,提供了更高层次的抽象,同时屏蔽了底层 Socket 的复杂性。Channel 有多种状态,比如连接建立、数据读写、连接断开。随着状态的变化,Channel 处于不同的生命周期,背后绑定相应的事件回调函数。
事件调度层 :它的核心组件包含 EventLoopGroup、EventLoop。——EventLoop 本质是一个线程池,主要负责接收 Socket I/O 请求,并分配事件循环器来处理连接生命周期中所发生的各种事件。
服务编排层 :它的职责实现网络事件的动态编排和有序传播——ChannelPipeline 基于责任链模式,方便业务逻辑的拦截和扩展;本质上它是一个双向链表将不同的 ChannelHandler 链接在一块,当 I/O 读写事件发生时, 会依次调用 ChannelHandler 对 Channel(Socket) 读取的数据进行处理。
图片
前面铺垫这么久,就是为了自然过渡到上面的图,请务必与 TCP/IP 协议栈进行对比。
socket。read 经过 TCP/IP 协议栈后,进入 netty 的网络通信层,事件调度层,最后来到服务编排层。而服务编排层的 channelPipeline 的设计也是一个 upstream/downstream 的 stack,一进一出的二个 pipeline。负责处理流入 / 流出的数据包。
上面的 stack 就非常类似 TCP/IP 协议栈。根据公司组织的需要可以定制分层的私有协议栈,比如从 authentication-handler、message-validation-handler、message-encode-handler、message-decoder-handler。
图片
grpc-gateway ——它是一个开源框架, 读取 protobuf 接口定义并生成一个反向代理服务器, 此服务器时一步将 restful http API 转换成 grpc 服务.
middleware ——实现鉴权功能, 比如哪些 URL 需要权限检验
handler 通用处理层 ——参数检验: handler 层负责执行与客户端约定参数的检验, 检验通过后再组装成后端服务需要的数据结构发往后端;接口聚合 / 组合服务: handler 层可以根据业务需要, 调用多个后端服务的 endpoint 来组合实现一个新的接口, 同时将下层返回的数据进行聚合处理.
service/model 业务逻辑层 ——对业务逻辑的封装, 负责将多个 DAO 数据结构转换和封装成一个有逻辑意义的模型;可以引入缓存策略, 优化数据存取效率.
DAO 层 ——数据访问层, 主要负责操作 DB 中某张表并映射到内存中某个 DAO 模型;与数据表结构一一对应, 通过 DAO 内存模型向上层传递数据源的对象.
数据访问层 DAL ——对底层的数据源做统一的抽象, 屏蔽数据库. 如果没有 DAL 的存在, 那么向乎所有的业务逻辑层都会去与具体的数据库存储强挷定. 耦合性就很高.
还有一个补充点:
业务逻辑层中的服务在实际场景中不可避免的会出现互相调用的场景,这种情况往往需要将耦合 / 公共的功能进行下沉,比如数据请求下沉为数据访问层服务,而业务下沉为稳定的通用业务服务,被其它服务稳定依赖。
熟悉 Ruby On Rails Web 应用框架的开发者,肯定知道 Rack 是如何成为应用容器 (webserver) 和应用框架之间的桥梁的。
图片
Rack 在 webserver 和应用框架之间提供了一套最小的 API 接口,如果 webserver 都遵循 Rack 提供的这套规则,那么所有的框架都能通过协议任意地改变底层使用 webserver。
图片
Rack 分层设计非常类似 Decorate Pattern 或者 Chain of Responsibility Pattern。
本文作者结合自身工作经验, 总结一些典型分层设计案例
这些案例充分说明了计算机系统本身就是通过一层一层抽象构造出来的。
杨敏,Freewheel 首席工程师,负责 SFX 团队的整体工作。目前从事服务化框架、容器化平台相关。关注与感兴趣的技术主要有 Python/Java 虚拟机、Golang、K8s、分布式数据库、分布式搜索引擎 ElasticSearch。
- END -