Freewheel执行官技师:

译者 | 刘凯

Damazan | 罗燕珊

在日常生活合作开发中,时常听见我们说一句话任何人市场需求都能透过两个间接地的的第二层来化解。那时,透过两个 case 就多层热门话题剖析下他们的思索,当中,很多 case 较为简单,而很多不所以简单,即使很多错综繁杂,须要他们他们多品位。这意味著自学操作过程须要他们急速将新科学知识与旧科学知识展开关连,逐步形成他们的科学知识管理体系,而非无数个科学知识行尸。

1甚么是多层结构设计?它答益处?

多层结构设计将应用软件分割成若干层,每几层只化解一小部分难题,透过大部份层的协同来顺利完成总体最终目标。两个繁杂难题透过还原成无数个控制系统子难题,这种就有效率的减少了每一子难题的体量与维数。

多层结构设计带来的益处:

  • 减少了控制系统应用软件的维数,将两个繁杂难题透过分解,分而治之
  • 功能的复用和封装

2计算机语言的发展

机器语言

早期,应用软件合作开发是机器语言,直接用二进制 0 和 1 表示机器能识别的指令和数据,看起来像这种:

0010000100100011

这就是计算机 CPU 唯一能理解的语言。对人类为说,二进制的程序是不可读的。

汇编语言

为了化解语言可读性的难题,汇编程序诞生了。汇编程序是人类可读的机器代码。它又被称为符号语言,使用助记符来代替机器的操作码。

汇编语言是二进制的文本形式,与 CPU 的指令是一一对应的关系。而他们不同的 CPU 管理体系结构(比如 PC 的 X86、嵌入式的 ARM) 是不同的,面向机器的语言带来的难题就是:对于不同的 CPU 管理体系架构,就须要不同的汇编语言。

高级语言

为了化解语言对机器的无关性,高级语言诞生了。一条高级语言通常由若干条机器语言实现的,并且不具有对应性。

高级语言让合作开发者不须要关注底层 CPU 管理体系结构与指令,只关注业务即可。

计算机语言的发展就是急速的抽象,只有透过抽象,将两个繁杂的的控制系统变成几层层的接口集合,让他们每次只须要考虑关注当前层集合内的逻辑,而不用去考虑当前层次以上或者以下的维数,才有可能让他们从繁杂控制系统中解放出来,逐步理解以及构造两个繁杂控制系统。

3Linux 内核

内核功能层与内核硬件层

操作控制系统内核,能简化理解成三大层:

  • 内核接口层:向上对用户态应用程序提供一套接口子集,合作开发者使用的控制系统调用 APIs。
  • 内核功能层:这几层顺利完成各种实际的功能,他们知道 OS 主要负责资源管理、内存、进程这些资源,物理内存如何申请、释放,进程如何调度。具体来说进程管理、内存管理、中断管理、设备管理。
  • 内核硬件层:分离硬件的相关性,他们知道两个 OS 能运行不同的指令集,也就是运行在不同的硬件平台。

不管是 ARM 管理体系结构,还是 X86,选择两个进程调度的算法是能相同的,须要改变的进程切换相关代码,因为不同的硬件平台的上下文是不同的,CPU 的寄存器也不同。这时候最好的结构设计是多层,当操作控制系统运行在不同的硬件平台时,就只须要修改硬件平台相关层代码,实现操作控制系统的高可移植性。

操作控制系统有两个关键结构设计:

  • 内核接口层区分用户态与内核态,来保护硬件资源受限访问。
  • 内核硬件层分离多种硬件平台相关性。这种多层的架构,极大提升了控制系统的稳定性和扩展性。

MMU 抽象层

操作控制系统负责管理物理内存,而用户进程使用虚拟内存。操作控制系统呈现给用户进程的是连续的虚拟空间,但不一定是连续的物理空间。因为物理内存被整个 OS 共享。

甚么是 MMU 呢?它是硬件,即内存管理单元,它对 CPU 发出的访存地址展开映射与检查,能让处理器发出的访存地址访问不同的物理内存单元。

如果将计算机上有限的物理内存分配给多个应用程序使用,如果让应用程序直接访问物理内存,如果没有 MMU 这层抽象呢?带来的难题是每一应用程序地址空间不隔离,内存使用率低,程序运行地址也无法固定。

化解的难题:虚拟内存 VA 与物理内存 PA 的映射——透过在 CPU 与内存之间加入 MMU 抽象层,让 CPU 在运行指令时发出的 VA 虚拟地址透过 MMU 转换后变成 PA 物理地址,然后再去访问物理内存。

MMU 引入带来的益处:

  • 权限控制。能对一些虚拟地址展开访问控制,较为代码段为只读,用户程序代可写。
  • 提升内存使用率:物理内存按需申请。fork 子进程的对应的物理空间是能过写时复制才展开真正的物理内存分配。
  • 不同进程之间能使用相同的虚拟内存地址空间,而进程的物理内存又能隔离。
  • 控制系统运行多个进程,所分配的内存之和能大于实际物理内存大小。

这是我认为最经典、最本质、最受启发的中间抽象层的结构设计。

CPU 与外设的通信

CPU 访问外设有两种方法;

  • IO 与内存统一编址
  • IO 与内存的独立编址

外设接口中的 IO 寄存器(即 IO 端口)与主存单元一样看待,每一端口占用两个存储单元的地址,将主存的一小部分分割出来用作 IO 的地址空间。

把外设的寄存器当做是两个内存地址,从而 CPU 以类似访问内存相同的方式来操作外设。

对 IO 外设的端口映射到两个物理内存单元地址,在 CPU 与外设之间的内存抽象层,带来益处是访问内存一样去访问外设。

小结

Linux 中的内核硬件层结构设计、MMU、CPU 与 IO 外设通信结构设计处处体现了多层 / 第二层的结构设计思想。

4TCP/IP 网络协议堆栈

从最底层的物理链路层层层向上封装抽象,化解了繁杂的网络通信的难题。同样的,任何人繁杂的难题,透过多层最终总能够回归最本质、最简单。这个多层架构,对大部份合作开发者而言,再熟悉不过,它的引入是想与后续介绍的 Netty 逐步形成对比。这里先卖个关子,后面解开谜底。

举例说明::

来自杭州西湖区某个小区的商务人士来京出差后,被确诊新冠肺炎,实施在京隔离措施,同时北京将此报告先发给浙江省,接着浙江省发给杭州市政府,然后市政府再向西湖区发送,最后到达某小区。这个发送报告操作过程也是多层报告思想。

DNS 第二层

DNS (domain name system) 是域名控制系统,是用来将主机转换为 IP 地址的服务。他们有至少三种方式在互联网上标识一台主机、主机名、IP 地址以及 MAC 地址。为甚么有引入 DNS 中间抽象层呢? 主要是主机名便于记忆,而 IP 地址方便于在计算机网络设备的处理,因此须要结构设计出两个 DNS 协议 (第二层) 来做主机名到 IP 地址的转换。

ARP 第二层

ARP(address resolution protocol) 是地址解析协议,它根据 IP 地址来获取物理地址。上面也谈到,MAC 与 IP 都能用来标识一台主机。那二者区别是甚么?

同两个局域网中的一台主机和另一台主机通信的时候,须要透过 MAC 地址展开定位,之后才能展开数据包的传送。

而在网络层和传输层中,主机之间是透过 IP 地址来定位的,对应的数据包中必须携带最终目标主机的 IP 地址, 而没有 MAC 地址。

因此,ARP 协议 (第二层) 用来实现从 IP 到 MAC 地址的转换。

5Netty

Netty 提供了异步的,基于事件驱动的网络应用程序框架。目前分布式搜索引擎,Spark 框架底层是扩展使用 Netty 框架。Netty 本身的架构理解很多曲线,为了讲清楚,我还是希望循序渐进方式,透过它的发展历史来一步步介绍。先铺垫再介绍,我们须要一些耐心。

传统阻塞 IO 服务模型

思路:

  • 采用阻塞 IO 模式获取输入数据
  • 每一连接都须要独立的线程顺利完成数据的输入,业务的处理和数据返回

难题:

  • 当并发数很大时,就会创建大量的线程,占用了很大的控制系统资源。
  • 连接创建后,如果当前线程没有数据可读,这个线程会阻塞在 read 方法上,造成资源浪费。

单 Reactor 单线程

思路:

  • 透过引入 selector 事件选择器来监听多路连接的请求。
  • Reactor 对象透过 selector 监控客户端请求事件后,透过 Dispatch 展开分发。
  • 如果建立连接请求事件,则由 Acceptor 负责建立两个连接,然后创建两个 Handler 对象处理连接顺利完成后的业务处理。

难题:

  • 模型简单,没有多线程,资源竞争的难题。所以工作在两个线程顺利完成。
  • 性能难题,两个线程,无法发挥多核 CPU 的性能。
  • 可靠性难题,线程 crash,会导致整个控制系统不可用。

主从 Reactor 多线程

主 React 处理大部份 socket 连接事件的监听和响应,而从 React 处理大部份 socket 的读写事件的监听与响应。主从 React 都在多线程中运行。

Netty 模型

Netty 主要基于主从 Reactor 多线程模型发展出来的。

Netty 逻辑架构

前面 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) 读取的数据展开处理。

ChannelPipeline 私有协议栈 vs. TCP/IP 协议栈

前面铺垫这么久,就是为了自然过渡到上面的图,请务必与 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。

6微服务多层

grpc-gateway——它是两个开源框架, 读取 protobuf 接口定义并生成两个反向代理服务器, 此服务器时一步将 restful http API 转换成 grpc 服务.

middleware——实现鉴权功能, 比如哪些 URL 须要权限检验

handler 通用处理层——参数检验: handler 层负责执行与客户端约定参数的检验, 检验透过后再组装成后端服务须要的数据结构发往后端;接口聚合 / 组合服务: handler 层能根据业务须要, 调用多个后端服务的 endpoint 来组合实现两个新的接口, 同时将下层返回的数据展开聚合处理.

service/model 业务逻辑层——对业务逻辑的封装, 负责将多个 DAO 数据结构转换和封装成两个有逻辑意义的模型;能引入缓存策略, 优化数据存取效率.

DAO 层——数据访问层, 主要负责操作 DB 中某张表并映射到内存中某个 DAO 模型;与数据表结构一一对应, 透过 DAO 内存模型向上层传递数据源的对象.

数据访问层 DAL——对底层的数据源做统一的抽象, 屏蔽数据库. 如果没有 DAL 的存在, 所以向乎大部份的业务逻辑层都会去与具体的数据库存储强挷定. 耦合性就很高.

还有两个补充点:

业务逻辑层中的服务在实际场景中不可避免的会出现互相调用的场景,这种情况往往须要将耦合 / 公共的功能展开下沉,比如数据请求下沉为数据访问层服务,而业务下沉为稳定的通用业务服务,被其它服务稳定依赖。

7Rails On Rack

熟悉 Ruby On Rails Web 应用框架的合作开发者,肯定知道 Rack 是如何成为应用容器 (webserver) 和应用框架之间的桥梁的。

Rack 在 webserver 和应用框架之间提供了一套最小的 API 接口,如果 webserver 都遵循 Rack 提供的这套规则,所以大部份的框架都能透过协议任意地改变底层使用 webserver。

Rack 多层结构设计非常类似 Decorate Pattern 或者 Chain of Responsibility Pattern。

8总结

本文译者结合自身工作经验, 总结一些典型多层结构设计案例

  • 计算机语言的发展
  • Linux 内核结构设计 (内核功能层与内核硬件层,MMU 抽象层,CPU 与外设的通信)
  • TCP/IP 网络协议堆栈 (DNS 和 ARP 协议)
  • Netty 框架发展以及多层私有协议栈分析
  • 微服务多层
  • 应用框架 Rails On Rack

这些案例充分说明了计算机控制系统本身就是透过几层几层抽象构造出来的。

  • 硬件方面是从无数个小的晶体管,抽象成无数个门电路,再到 CPU 器件,最后抽象组成计算机。
  • 应用软件结构设计也是两个层次两个层次功能完善叠加的,无论是自顶向下还是自底向上。

关于译者

刘凯,Freewheel 执行官技师,负责 SFX 团队的总体工作。目前从事服务化框架、容器化平台相关。关注与感兴趣的技术主要有 Python/Java 虚拟机、Golang、K8s、分布式数据库、分布式搜索引擎 ElasticSearch。