前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >我对软件分层设计的思考

我对软件分层设计的思考

作者头像
芋道源码
发布于 2021-08-05 05:04:36
发布于 2021-08-05 05:04:36
70600
代码可运行
举报
文章被收录于专栏:芋道源码1024芋道源码1024
运行总次数:0
代码可运行

在日常开发中,经常听到大家说一句话“任何需求都可以通过一个间接的的中间层来解决”。今天,通过几个 case 就“分层”话题梳理下自己的思考,其中,有些 case 比较直观,而有些不那么直观,甚至有些微妙,需要我们自己多品味。这意味着学习过程需要我们不断将新知识与旧知识进行关联,形成自己的知识体系,而非一个个知识孤岛。

1. 什么是分层设计?它有何好处?

图片

分层设计将软件划分成若干层,每一层只解决一部分问题,通过所有层的协作来完成整体目标。一个复杂问题通过分解成一个个系统子问题,这样就有效的降低了每个子问题的规模与复杂度。

分层设计带来的好处:

  • 降低了系统软件的复杂度,将一个复杂问题通过分解,分而治之
  • 功能的复用和封装

2. 计算机语言的发展

图片

机器语言

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0010000100100011

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

汇编语言

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

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

高级语言

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

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

计算机语言的发展就是不断的抽象,只有通过抽象,将一个复杂的的系统变成一层层的接口集合,让我们每次只需要考虑关注当前层集合内的逻辑,而不用去考虑当前层次以上或者以下的复杂度,才有可能让我们从复杂系统中解放出来,逐步理解以及构造一个复杂系统。

3. Linux 内核

内核功能层与内核硬件层

图片

操作系统内核,可以简化理解成三大层:

  • 内核接口层 :向上对用户态应用程序提供一套接口子集,开发者使用的系统调用 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 外设通信设计处处体现了分层 / 中间层的设计思想。

4. TCP/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 地址的转换。

5. Netty

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 的存在, 那么向乎所有的业务逻辑层都会去与具体的数据库存储强挷定. 耦合性就很高.

还有一个补充点:

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

7. Rails 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。

- END -


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 芋道源码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
我对软件分层设计的思考
在日常开发中,经常听到大家说一句话“任何需求都可以通过一个间接的的中间层来解决”。今天,通过几个 case 就“分层”话题梳理下自己的思考,其中,有些 case 比较直观,而有些不那么直观,甚至有些微妙,需要我们自己多品味。这意味着学习过程需要我们不断将新知识与旧知识进行关联,形成自己的知识体系,而非一个个知识孤岛。
用户2781897
2021/08/06
3940
我对软件分层设计的思考
大疆嵌入式一面问题集合
答:栈溢出发生的时候,栈顶指针(SP - Stack Pointer)一定会超出栈的范围,所以也可以在发生线程切换的时候,检测SP指向的地址是否超过了栈的内存限定。
Adam_chen
2022/09/01
1.1K0
牛客网论坛最具争议的Linux内核成神笔记,GitHub已下载量已过百万
原文地址:牛客网论坛最具争议的Linux内核成神笔记,GitHub已下载量已过百万
嵌入式Linux内核
2023/06/09
1K0
牛客网论坛最具争议的Linux内核成神笔记,GitHub已下载量已过百万
《深入浅出DPDK》&《DPDK应用基础》读书笔记
本文主要介绍了我在阅读《深入浅出DPDK》,《DPDK应用基础》这两本书中所划下的知识点
没有故事的陈师傅
2020/11/19
4.5K0
《深入浅出DPDK》&《DPDK应用基础》读书笔记
一文彻底搞懂“内存管理”
笔者面试过不少业务后台开发候选人,当问起内存管理的相关问题时,往往都会答出 JVM 的垃圾回收机制,并对 Serial、Parallel、CMS 等收集器如数家珍,侃侃而谈。
CloudBest
2021/07/13
7830
一文彻底搞懂“内存管理”
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决
这句话几乎概括了计算机软件体系结构的设计要点.整个体系从上到下都是按照严格的层级结构设计的.
一个会写诗的程序员
2019/08/22
6.1K1
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决
TCP/IP学习笔记1——协议分层
网络协议是分层的,分层的概念类似于函数封装,不断提供更高级更抽象的接口,最后提供给客户使用。对于分层协议而言,整个协议共同完成一件事情,每个层次基于本层或低层接口完成本层次的功能并对更高级的层次提供接口,即对于每个层次而言,有以下两个主要功能:
月见樽
2020/09/22
5420
TCP/IP学习笔记1——协议分层
Linux 内核系统架构
即使看了所有的Linux 内核文章,估计也还不是很明白,这时候,还是需要fucking the code.
刘盼
2019/09/17
5.3K0
Linux 内核系统架构
深入剖析虚拟内存工作原理
作者:allanpan,腾讯 IEG 后台开发工程师 导言 虚拟内存是当今计算机系统中最重要的抽象概念之一,它的提出是为了更加有效地管理内存并且降低内存出错的概率。虚拟内存影响着计算机的方方面面,包括硬件设计、文件系统、共享对象和进程/线程调度等等,每一个致力于编写高效且出错概率低的程序的程序员都应该深入学习虚拟内存。 本文全面而深入地剖析了虚拟内存的工作原理,帮助读者快速而深刻地理解这个重要的概念。 计算机存储器 存储器是计算机的核心部件之一,在完全理想的状态下,存储器应该要同时具备以下三种特性:
腾讯技术工程官方号
2021/05/08
3.3K0
【译】Linux概念架构的理解摘要一、Linux内核在整个计算机系统中的位置二、内核的作用三、Linux内核的整体架构四、高度模块化设计的系统,利于分工合作。五、系统中的数据结构六、子系统架构七、结论
声明:本文翻译自Conceptual Architecture of the Linux Kernel
阿杜
2018/08/06
1.2K0
【译】Linux概念架构的理解摘要一、Linux内核在整个计算机系统中的位置二、内核的作用三、Linux内核的整体架构四、高度模块化设计的系统,利于分工合作。五、系统中的数据结构六、子系统架构七、结论
QEMU架构浅析
QEMU是“Quick Emulator”的缩写,是一个用C语言编写的开源虚拟化软件。本文的目的是描述本人所理解的QEMU技术架构的见解,并以此抛砖引玉。众所周知,QEMU的源代码开发文档非常稀少,描述内部结构和工作机理的文档更是凤毛麟角,一般的开发人员想要从事QEMU的开发工作,通常只能从源代码入手。因此,对于技术人员来说,了解QEMU是一项艰巨的任务。
时间之外沉浮事
2019/10/15
9.7K0
QEMU架构浅析
深度:一文看懂Linux内核!Linux内核架构和工作原理详解
Linux内核的作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。目前支持模块的动态装卸(裁剪)。Linux内核就是基于这个策略实现的。
混说Linux
2022/11/18
3K0
深度:一文看懂Linux内核!Linux内核架构和工作原理详解
为什么操作系统需要虚拟内存
在计算机中,CPU执行程序之前,得先把程序的内容加载到内存中一段连续的空间里,这样CPU才能根据内存中排列好的指令顺序执行。
写bug的高哈哈
2024/05/15
1960
为什么操作系统需要虚拟内存
零拷贝(zero copy)技术你真的懂吗?什么时候需要用到内存映射?
Linux系统是虚拟内存系统,虚拟内存并不是真正的物理内存,而是虚拟的连续内存地址空间。虚拟内存又分为内核空间和用户空间,内核空间是内核程序运行的地方,用户空间是用户进程代码运行的地方,只有内核才能直接访问物理内存并为用户空间映射物理内存(MMU)。内核会为每个进程分配独立的连续的虚拟内存空间,并且在需要的时候映射物理内存,为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系,这个页表就是存在于MMU中;用户进程访问内存的时候,通过页表把虚拟内存地址转换为物理内存地址进而访问数据;其实对于用户进程而言,虚拟内存就是内存一般的存在(当作内存看待就好)。这样的设计可以把用户程序和系统程序分开,互不影响;内核可以对所有的用户程序进行管理,比如限制内存滥用等
IT大咖说
2020/04/21
1.7K0
如何设计真正高性能高并发分布式系统(万字长文)
“世间可称之为天经地义的事情没几样,复杂的互联网架构也是如此,万丈高楼平地起,架构都是演变而来,那么演变的本质是什么?”
玄姐谈AGI
2019/11/25
2.4K0
作为一个Linux新人,你必须知道的事
随着微服务的盛行、自动化运维技术的发展,我们测试管理测试环境的能力似乎在逐渐降低,而整个IT行业对于“W”型人才的需求确越来越高。作为一个有追求的测试,我们是时候补一补我们的运维知识~
TestOps
2022/04/07
3540
作为一个Linux新人,你必须知道的事
CSAPP 计算机系统漫游 笔记
CSAPP 最大的魅力在于,每章的前言会站在程序员的角度跟你分析,为什么要学这些知识,然后在正文里清晰的阐述这些问题的答案。
wywwzjj
2023/05/09
1740
好多网友都不知道怎么阅读Linux内核源码,这篇让你快速理解
Linux内核分为CPU调度、内存管理、网络和存储四大子系统,针对硬件的驱动成百上千。代码的数量更是大的惊人。
嵌入式Linux内核
2022/10/22
5.2K0
好多网友都不知道怎么阅读Linux内核源码,这篇让你快速理解
内存系列学习(一):万字长文带你搞定MMU&TLB&TWU
最近一直在学习内存管理,也知道MMU是管理内存的映射的逻辑IP,还知道里面有个TLB。
刘盼
2023/08/22
2.5K0
内存系列学习(一):万字长文带你搞定MMU&TLB&TWU
【linux学习指南】线程概念与控制
思考⼀下,如果在没有虚拟内存和分⻚机制的情况下,每⼀个⽤⼾程序在物理内存上所对应的空间必 须是连续的,如下图:
学习起来吧
2025/02/04
870
【linux学习指南】线程概念与控制
推荐阅读
相关推荐
我对软件分层设计的思考
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验