周刊(第8期):技术配图的一些心得

2022-03-04
7分钟阅读时长

引言:写过不少技术文章,以及给不少技术思路手绘示例配图之后,在这方面有了一些心得,本文权当个人的一些的总结,抛砖引玉。


技术配图的一些心得

我觉得我们理工科出身的,对于可以量化的事情,总是很容易根据量化差异来做出判断,比如一个程序性能优化之后,能比优化之前快出多少,都能有一个量化的数字来说明。

但是对于那些不能量化的东西,就很难说出具体好在哪里了。

本文主题要讨论的“技术配图”就属于这种很难量化的领域,很难有一个标准来量化说明两幅图之间差别在哪里。我也是画了很多图,以及看了别人的很多配图之后,才慢慢有一些心得,本文权当个人的一些的总结,抛砖引玉。

本文并不是一个画图工具的对比说明,尽管现在各种绘图工具已经很多,也各有自己的优缺点以及个人喜好,但是在这里并不讨论具体工具的使用,会把更多的文字放在配图的一些注意事项上。但是,也总有人问我文章的配图使用什么工具做的,在这里再回答一次:OmniGraffle,一款目前仅有Mac版本的工具软件。

一图胜千言

在开始交代具体的配图注意事项之前,有必要先说说配图的重要性。

绘图,某种程度也是辅助自己思考某个技术点的手段之一,以我个人的体会来说,有时候讲不清楚一个技术点的时候,就手绘图出来,比朴素的文字更容易说明问题。其中的原因,有可能是:图片可以有多维的信息,而文字通常只有一维,遇到文字表达能力不太好的人,这仅有的一维能力可能还不好发挥出来。

所以,在交代技术细节、沟通交流的时候,尽量多画图。反向的,图画多了,也自然慢慢会找到感觉,如何更好的通过图示表达思路。

顺便一提,还有比朴素的文字表达更差的技术沟通方式,就是简单粗暴的贴一大段代码上去。这种做法,其实更多时候是没有对作者的思路有太多个人的整理,想偷懒的方式,最后回头再看写过的文字,可能连自己都看不懂了。

个人的一个体会:如果产出某些输出的时候,能假设自己未来就是这些输出的读者、维护者,那么输出起来会更“友善”一些。比如写的代码、文章、甚至于提交代码时候的信息,如果能考虑是写给未来的自己看的,会更清晰、尽可能留下更多的信息。我最开始要在文章里大量配图,也是为了将来自己回看的时候能看懂。

扯远了,总之,尽可能多画图来表达技术思路。

下面开始正题,以下会以简单的几个原则及示例来说明。

区分、联系、组合

配图中,应该尽量将不同的模块、组件等区分开来,“区分”的方式有很多,常见的有:

  • 使用不同的颜色。
  • 使用不同的形状。
  • 使用箭头、曲线等表示数据的走向、趋势。

等等,所有的这些手段,概括起来就是尽量在图中,将不同的元素区分开来,“有区分”意味着至少有一个维度的不同,这样能给读者更加清晰的感觉。可以结合下面的例子来理解区分、联系和组合的绘图表达。

分组

一个模块里,可能由多个组件构成,可以把这些组件分组到一个更大的模块中。

分组是非常常见的一种手段,这里多举几个例子。

cpu

上图中,每个CPU Core中有L1、L2缓存,于是把这些组件合并在一起放在Core组件中,周围使用一个正方形包裹起来,同时这个正方形左上角有一个Core的说明文字,这样一目了然:Core模块,由L1、L2缓存构成。

meituan

上图出自Raft论文,整体上划分为了Client、Server这两大部分。而每个Server又有以下三部分组成:

  • 一致性算法模块。
  • 状态机。
  • 持久化的日志。

所以,图示中将这三部分合在一起放在同一个矩形里,表示一个Server有这三个组件。

另外还需注意的是,一般这种分组中外围的矩形,有这样的讲究:

  • 一般使用斜面矩形,即四个角是圆角的矩形,这样圆润一些的边角看起来会更舒服一些,如上图。
  • 如果这个组合,是一种逻辑上的组合,那么线的形状一般用虚线;否则就一般用的实线。

在分组时,有时候可以将相同类型的模块层叠起来,这样会更加简洁,如下图:

raft

上图是出自Raft论文中的状态机模型,其中想要表达的一个点是:

  • 有多个client向server发起请求。
  • server要达成一致,需要将日志在server之间同步。

但是上图中,并没有把这些同类型的组件分开表达,而是巧妙的使用层叠的方式,简洁得表达了有多个client、多个server的情况。

趋势

如果不同的组件之间,有不同的趋势,可以在图中使用类似箭头这样的符号表达出来。

下图是描述不同层次存储的访问速度,于是用了两个方式来表达访问速度的变化趋势:

  • 左边的箭头表达速度和成本的变化。
  • 不同大小的多边形表达了这些存储空间的变化:越往上访问速度越快,但是对应的存储空间也更小。

cache

再比如,下图中,是说明sqlite中btree页面的数据组织的。其中的两部分内容,Cell地址数组以及Cell内容区为变长大小,前者从地址低位向高位生长,后者反之,于是在图中,就用箭头示例出地址的高低位区别,以及两者的增长方向:

页面内数组的组织

(出自sqlite3.36版本 btree实现(五)- Btree的实现 - codedump的网络日志

联系

用箭头等表示数据、状态等的走向,或者模块之间的联系。

这在涉及:

  • 状态切换。
  • 数据流向。

等场景下是非常常见的手段,比如经典的TCP状态机切换:

TCP状态机

以及TCP三次握手流程,也是典型的“状态切换”:

TCP三次握手

需要说明的是,以上的图示中:

  • 箭头代表的状态切换走向中,同时也配以文字说明是什么动作导致的状态切换,这样这个图示就更清晰了。
  • 箭头也分为实线和虚线,一般而言,虚线表示数据的走向,实线表示状态的走向。

禁止

需要禁止或者错误的行为,可以用特殊的符号,如带颜色的“×”符号示意出来;反之,可以用带颜色的“√”符号示意出来,而且表示禁止的时候,一般用红色会更显眼,下图就是一个示例:

meituan

(出自 Memory Barriers in .NET · Nadeem Afana’s Blog

说明

如果不好说明问题,可以在图示中搭配简短的说明文字。注意:这类型文字一定要足够的简短,否则可能会喧宾夺主。

比如下图中,有两部分蓝色注解的文字来说明不同的表类型:

数据库文件的rowid全量数据表和索引表

(出自sqlite3.36版本 btree实现(五)- Btree的实现 - codedump的网络日志

再比如下图中,使用注解文字来说明查找数据的两步流程:

查找key的流程

(出自sqlite3.36版本 btree实现(五)- Btree的实现 - codedump的网络日志

分类

有时候需要使用类似{这样的符号,对一类元素做一些说明,例如:

下图中,是说明sqlite中btree页面的数据组织的,最右边的以{包起来的文字,对每部分做了简要的说明。

页面内数组的组织

(出自sqlite3.36版本 btree实现(五)- Btree的实现 - codedump的网络日志

下图中,将页面划分为不同的部分,这些不同的组成部分,既使用了颜色进行区分,也使用了向下的{辅以文字说明。

空闲区链表

(出自sqlite3.36版本 btree实现(五)- Btree的实现 - codedump的网络日志

步骤

如果配图是需要讲解某个操作的步骤的,可以配以数字来辅助理解整个流程。

下图中,表达的是根据帧数查找页面编号的两个步骤:

添加帧数与页面编号对应关系的流程

(出自sqlite3.36版本 btree实现(四)- WAL的实现 - codedump的网络日志

下图中的步骤就更多了,并没有显得很乱,大概原因在于:

  • 最左边表达了每一步的步骤。
  • 每一步写入数据之后,显示WAL文件在写入之后的内容。
  • 最右边使用{表达修改之后的数据。

写事务修改WAL文件和WAL索引数据

(出自sqlite3.36版本 btree实现(四)- WAL的实现 - codedump的网络日志

展开

在讲解例如文件格式,或者协议格式等内容的时候,格式由多个部分划分组成,其中又可以针对其中的某些内容展开说明。

如下图中,是用于展示wal index索引文件格式的:

  • 左边示例每部分内容的大小,想说明的是,那个索引块大小为32KB,而第一块的头136字节为索引文件头。
  • 于是,在右边图中,将左边不同模块的具体格式继续展开说明。

WAL-Index索引文件结构图

(出自sqlite3.36版本 btree实现(四)- WAL的实现 - codedump的网络日志

总结

以上简单总结了一下个人技术配图的一些心得,总的大原则是:

  • 区分:将组件、流程、趋势等之间的”区分“尽可能在图示中通过各种手段(如不同的颜色、形状、箭头)表达出来。
  • 联系:组件之间的数据流动、状态切换等,都是它们之间的联系,也需要通过各种手段表达出来。
  • 说明:可能的话,要在图中加上一些说明文字,如步骤说明、分类说明,等等。