‘不立文字,直指人心’ — 六祖慧能

昨天正好在飞机上把原研哉的又一本关于设计的书看完了,书名就不提了,大概仍然是原研哉老师用他那种特有的平静的口吻描述关于设计的思考和社会的观察,引起了我对于软件设计审美的一些思考。熟悉我的朋友大概知道我一向对艺术和哲学有着偏爱,尤其是东亚的古典美学,看似离所从事的计算机行业很远,但是我一直认为在代码深处存在着相同的审美趣味。正好试着总结一下作为第二篇关于系统设计品味的文章,上次如果说是一篇关于‘术’的文章,这篇可能会更有‘道’的味道 :)。不过关于审美的事情通常是带着强烈的个人偏好,而且就像任何其他艺术形态一样,审美也是有流派的,如果对本文描述的东西不认同也实属正常,一笑了之就好。

东方美学的内核

在聊软件之前,我们先聊一下美学。仔细想想也蛮有意思的,长期以来我们都认为计算机科学是更加偏向工程学的范畴,也就是所谓的软件工程,但看看接近的学科,例如建筑,我们很自然的能够将泥瓦匠和建筑设计师看作是两种不同的职业,但是对于程序员和软件设计师很多时候是一体,于是对于系统设计品味的讨论通常会和工程学乃至管理学混在一起,即使有对于美学的讨论,也只是在一些 Geek 和 Hacker 的亚文化中,隐藏在一个个看似戏谑但是充满禅意的 Puns 和 Wordplay 里面,例如在 ESR 的《How To Become A Hacker》一文中关于 Hacker 生活方式中对禅宗和禅修的推崇可见一斑,我其实还蛮庆幸在我刚接触编程的年代(90年代中后期)赶上了黄金年代的尾巴,得以一窥,现在的年轻 Geek 们只能在计算机考古学中体验了 :)。

为了避免牵扯太多其他的内容,大概的范围会限定在禅宗和道家的审美,而且只会从‘表’上分析,内在的哲学和宗教的内容不会过多涉及。说到这个话题,大家不妨在脑子里想象一下典型的东方审美的例子:寥寥几笔加上大片留白的水墨画,雾气缭绕的山脉,日式枯山水庭院,Wabi-sabi,离日常生活近一点的,例如优衣库,无印良品,苹果(乔布斯时代)…如果总结几个关键字就是:崇尚简洁,没有攻击性,平静含蓄的表达,以小见大(通过小的细节隐喻出更大的意境)…值得注意的是,这并不是回避世界的复杂性,而是强调隐藏在复杂性的表面背后的根源是极简要素加上自洽的规则

其实有这种审美倾向丝毫不意外,禅宗的内核中有很浓的存在主义和本体论色彩,例如铃木大拙(当代最有名的禅学大师之一,在西方尤其受到推崇)关于意识以及一切可能性的根源,也就是他口中的 Cosmic Unconscious 概念,又如道家的老子在道德经中关于‘道’的描述:‘有物混成,先天地生…道生一,一生二,二生三,三生万物…',都是关于这个审美内核精彩诠释,当然严格来说,道家和禅宗在审美上还是有一定区别的,道家更加崇尚与自然的融合,顺其自然,用当下流行的话就是‘躺平了’,是一种出世的美学,但是禅宗更加严肃,只为了达到心物合一的顿悟。但在表面上,这类东方的古典审美其实倒是暗合了当代西方的包豪斯设计的一些理念:简单并不意味着简陋,而是舍弃掉一切无用的装饰,直指本源。只是对于包豪斯来说,本源大致是从实用主义角度出发,东方这边的含义更深且丰富而已。

复杂系统的简洁之美

回到计算机的世界,复杂总是不可回避的,人类总是会在不停尝试创造超出自己理解边界的事物,正所谓创新。而且无穷的复杂性用有穷的代码来表示,本身就是一件非常充满禅学审美的事情。就像我经常举的一个例子:大家有没有想过,TiDB 核心大概百来万行代码,运行在 3 台机器上和运行在 3000 台机器上一样,都是这百来万行代码,但是 3 台机器的复杂性和 3000 台机器复杂性确是千差万别,人肉管理 3 台机器很轻松,但是管理好 3000 台机器,让它们高效稳定的协作这件事情,肯定超出了人作为个体的能力范围,等一下后面我会再聊到 TiDB 的例子。 同样的例子还有很多,例如大家比较熟悉的 Unix 哲学,已经有无数人阐述过,我也不追溯,后边我会用几个比较新鲜的例子,用来阐述复杂系统的简单之美,来让大家体会一下其中妙处。

元胞自动机

第一个例子,我想聊聊元胞自动机(Cellular Automata),元胞自动机大概是 50 年代由冯诺依曼提出的一个自动机模型,听名字很高级,但事实上非常非常简单,简单到小学生都能马上理解:想象一个 n 维度的无限网格(最好理解的是 2 维,大概就是类似棋盘的样子),每个格子会处于有限的状态,t 作为离散的时间概念,每一个 t 时刻的网格中每一个格子的状态是由 t - 1 时刻这个的格子的邻居格子( 2 维里面就是上下左右的几个)的状态决定,注意所有的格子都是平等的,受到同一种规则的支配。实现这么一个自动机,也就是百来行代码,最有名的实现是 Conway 的 Game of Life,生命游戏是一个二维的 CA 实现,想象每个格子是一个细胞,规则很简单(来自 Wikipedia):

  • 每个细胞有两种状态 - 存活或死亡,每个细胞与以自身为中心的周围八个格子的细胞产生互动

  • 当前细胞为存活状态时,当周围的存活细胞低于2个时(不包含2个),该细胞变成死亡状态。(模拟生命数量稀少)

  • 当前细胞为存活状态时,当周围有2个或3个存活细胞时,该细胞保持原样。

  • 当前细胞为存活状态时,当周围有超过3个存活细胞时,该细胞变成死亡状态。(模拟生命数量过多)

  • 当前细胞为死亡状态时,当周围有3个存活细胞时,该细胞变成存活状态。(模拟繁殖)

可能你会问:就这么一个看上去如此弱智的游戏,能玩出什么花样来?

那我接下来要说的可能会让你大吃一惊,已经被证明,生命游戏中可以构造出通用图灵机,也就是说,通过这个游戏,你可以实现在计算机上能够实现的一切,只要你愿意,你甚至可以用生命游戏模拟出一台计算机,在这个表格上实现一个操作系统和文本编辑器,然后构造出我写这篇文字需要的一切软件,或者实现一个深度学习系统,例如 实现一个 AlphaGo 在围棋上胜过人类最强的棋手,没错就是在这么一个二维的表格上加上 5 条规则。

image-20211203123520678

注意,生命游戏的广度并不局限于实现图灵机,是否蕴含更多关于计算模型的可能性?没有答案。只是如此深邃复杂的宇宙就是由如此简单的规则构成,本身就是一件很美的事情,对吧?(另外关于在生命游戏的一个 Fun Fact 是,在游戏中构造逻辑门的生成模式叫做:滑翔机,这个图案也常被作为 Hacker 的 Logo,https://www.bilibili.com/video/BV1T541157s7/?spm_id_from=333.788.recommend_more_video.2)。

λ-Calculus 和 Y-Combinator

第二个例子仍然是一个关于计算理论的例子: λ 演算。这个被作为计算之美的一个具体的体现,虽然它和图灵机是等价的,但是在爱好者圈子里通常对它的美学价值更加的推崇(我感觉?),我猜背后的原因大概是图灵机的抽象充满了一股机械主义的气息,而 λ 演算从名字就看得出来就带着纯理论那种的高级感吧 😂(当然图灵机的抽象也很简洁)。λ 演算大概就是数学中的函数换个写法再加几个规则, 例如 f(x) = x , 这么一个函数,通过 λ 演算的书写方式,就是 λx.x。λ 演算的核心规则就两条(注:可能有朋友会问 η 归约哪去了?其实 η 归约在完备性上不是必要的)分别叫:α 转换和 β 归约,听起来很高级,但是实际是简单的吓人:所谓 α 转换就是一个函数的自变量名字叫做 x 还是 y 还是 z 无所谓;所谓 β 归约,就是函数的变量可以带入一个值进行计算,相当于 Apply 函数的过程,表达方式是一个括号。具体 λ 演算能做什么我就不赘述了,和上面一样,图灵机能做的都能做。 提到美,在 λ 演算中经常被拉出来溜溜的大概就是 Y-Combinator(下面简称 YC),没错,就是 YC 的那个 YC,因为这个东西过于优雅,以至于 Paul Graham 搞基金的时候直接拿过去当名字了。

下面我试一下尽可能用人话把 YC 讲明白(不想看的跳过这段也没有任何问题):我们已经知道大概 λ 演算是和我们在数学里面的函数接近的例如 f(x) = x, 但是注意到:λ 演算的定义都是匿名函数(如:λx.x)并没有一个叫做 f 的名字,如果我们希望实现递归函数怎么办呢?也就是自己调用自己?这里插播介绍一个概念:不动点,也很简单,所有满足 f(x) = x 的 x 都叫做 f 的不动点。想象一下,如果 x 作为一个函数带入,输出一个函数,这个函数和传入的 x 是等价的函数,我们不就可以把自己给‘算’出来了吗?所以把 YC 想象成就是一个传入参数是 f 返回 f 的不动点函数的函数(有点绕)。当然看不懂也没关系,静静的观赏一下 YC 吧:

image-20211203123520678

一切这些优雅的东西都是构建在上面几条简单演算规则之上。

Actor 模式

说了两个纯理论的例子,其实这样纯粹的抽象带来的美学在更软件工程的味道的领域也不少见,让我们看看分布式系统里面的一个常用的设计模式:Actor 模式。Actor 模式大约在上世纪 70 年代由 Carl Hewitt 等人作为研究并行计算理论的工具被提出,后来在 Erlang OTP 和 Scala Akka 被发扬光大。我之前的一篇文章提到可观测性的时候略微提及了周期的概念,程序的逻辑其实就是一个又一个的‘循环’嵌套起来,Actor 模式就是利用了这一点:万物皆是 Actor,每一个 Actor 都是一个无限的循环,这个循环对不同的事件的到来,可以做出以下几种反馈:

  • 向其他(有限个)Actor 发送事件(包括自己)
  • 产生(有限个)新 Actor
  • 根据收到事件,决定下一条事件的内容

不管多么复杂的业务逻辑,都能归约到 Actor 上,篇幅有限举一个特别简单的例子,由 Actor 模型构建的 Web Service 服务(假设需要访问数据库),大概分为 2 类 Actor:处理 HTTP 请求的 Actor,以及处理 DAO(Data Access Object)访问数据库的 Actor。对于第一类 Actor 来说是一个不停接收客户端请求事件,持久化请求,当收到连接可读写的事件后读取并对 HTTP 请求的内容进行处理,如果是需要访问数据库的事件,发送给负责数据库处理的 Actor,当收到数据库处理的 Actor 的回应事件后,返回客户端;对于 DAO Actor,收到的数据库访问请求的时候访问数据库,然后向请求的 Actor 返回包含结果的消息事件。与 Actor 类似的思想还有 Tony Hoare 的 CSP (communicating sequential processes) 模型,甚至在 Go 语言中 Goroutine / Channel 的模式也是一点 Actor 和 CSP 的影子。

image-20211203123520678

Actor 模型优雅的地方不仅仅在于通过一套简单统一的范式来表达复杂的逻辑,更重要的是从消除了单机和集群的区别,大家仔细想,Actor 并没有规定与其通信的其他的 Actor 在哪,与之对应的 Mutex 和信号量通常带着单机色彩(Spinlock 本质上是通过控制锁对象的所有权实现互斥),Actor 就没有这个问题,而且很天然的对负载均衡友好,不过上面的第三条原则实际上暗示了 Actor 要处理自身状态持久化的问题,虽然实际的工程中,完全纯粹的 Actor 模型的系统也不多见,也经常被人诟病性能问题和 Actor 的拆分粒度问题(太细了冗余增加通信的复杂度,太粗了形成单点则失去了 Actor 的意义),但是作为一种思考分布式系统的角度,Actor 是受启发于现实的物理世界,我们和世界发生交互的方式正是通过语言(消息)和异步的,从某种程度上来说 Actor 是对我们自己的建模。

The Zen of TiKV

刚才提到了的 Actor 的时候为状态留下了一个伏笔,每个 Actor 需要关注自己的状态,但是状态一向是优雅系统的大敌,尤其是叠加上分布式/Failover/一致性之类的要求之后,这里每一个要求都不简单,叠加在一起就变成了复杂性爆炸的状态,TiKV 作为 TiDB 的存储层的核心,就是为了解决这个问题。就像前面提到的,东方审美的精髓是用简单对抗复杂。TiKV 是一个支持跨行事务的分布式的 Key-Value Database,这个世界上的 KV 数据库已经那么多了,Why another?如果说从设计的优雅程度来说,TiKV 一定是在不错的那一批,所以这个数据库的优雅在哪?在分析之前,我先放一首当时自己写的诗(为了致敬 Python 的 import this)TiKV 是构建在下面这 10 条规则:

《The Zen of TiKV》

Everything is KV pair

Every KV pair belongs to a Region, but a Region contains multiple KV pairs

Every Region belongs to a Host, but a Host contains multiple Regions

Region comes from nothingness, only specifying the beginning and the end of a KV range

Initial Region is (-∞, +∞)

When Region is too big, it splits

When Regions are too small, they merge

Region can copy itself (to other hosts)

Region can also destroy itself

Regions live and prosper

如果读者读完后,脑中能联想到细胞的分裂和繁殖,那就对了,TiKV 诞生在一个大的假设上:系统应该能调整自己去适应环境的变化,这个过程人和业务的领域知识只是在给系统指引方向,让系统朝着这个方向演进,而并不是反过来,为了一个具体特定的场景定制。 我经常开玩笑说,从技术的角度来说,TiDB 是我们努力让 TiKV 长成了一个关系型数据库的样子,事实上因为这种类似‘细胞’的设计,让 TiKV 能长出来的东西很多,例如 TiFlash 就是一个绝佳的例子,TiFlash 是 TiDB 的列式存储加速拓展,是之所以用拓展(Extension)这个词是因为 TiFlash 的原理就是 TiKV 这些一个个存储数据的‘细胞’上,根据上面那首小诗的第八行,创建出来的一批列式存储副本,这些副本除了是列式存储格式之外,和普通的‘细胞’别无二致,于是就很平滑的继承了整个系统的灵活性。试想一下,TiFlash 只是这个能力的一个应用,未来还能发展出多少可能性?另外这个设计带来的一个好处是,对于一致性和 Failover 之类的比较‘脏’的问题,被限定在了 Region 的单位上,也就是我只需要保证 Region 在复制/销毁/分裂/合并的正确性以及可用性,就很自然能在一个更大的集群上大致保证整体的正确性,更详细的一些介绍,我在 2016 年的一篇 Blog 里有阐述(https://pingcap.com/zh/blog/building-distributed-db-with-raft)。

Plan9

最后一个例子来自我特别喜欢的一个操作系统:Plan9。

这一小节我想了很久,竟然不知道该如何下笔,因为 Plan9 的设计实在超前时代太多,这种感觉就像看到一个非常震撼的艺术品,你的第一反应其实会是愣住,有千言万语没法说出来,不过拖了好几个礼拜觉得总这么拖着也不好,就硬着头皮写一下。

Plan9 大概要从它的历史说起,Plan9 脱胎于贝尔实验室,没错 ,就是那个诞生了 UNIX 和 C 语言的地方,而且 Plan9 的设计团队的领导者正是 Rob Pike 和 Ken Thompson,诞生的定位就是一个’更好的’ UNIX。主要是为了解决 UNIX 的一个问题,其哲学里面虽然有:一切皆是文件,但是随着时间的发展,新的模块不停的加入,对这条原则开始没有那么的坚守,一个例子:TCP/IP 协议栈和 BSD-Style Socket,回想当初学网络编程的时候,学习到 socket 的时候就觉得有点别扭,socket 的 API 长得有点像 UNIX 的文件系列的 API,但是名字和行为却有点不一样,而且 socket 很显然不是一个文件,我相信很多开发者有过同样的困惑。其实 Plan9 就是希望将这条哲学带回来并严格的贯彻,例如刚才网络的例子,我们看看 Plan9 是如何处理的:

image-20211203123520678

(在 Plan9 中完全用 shell 操作模拟 HTTP 的过程,全程都是用文件操作的命令)

Plan9 另外一个超前的设计是:这是一个面向纯分布式环境设计的操作系统,我一直觉得分布式应该是一个和操作系统抽象无关的特性,一个很好的例子就是 plan9 的文件系统:9pfs。9pfs 的设计是可以作为一个分布式文件系统,也就是你可以 mount 一块远程的文件夹,分布式存储细节全部也隐藏在了文件系统的抽象之下;更绝的是,我们现在经常提到的「计算存储分离」,在 plan9 中已经都落地了(90年代初),plan9 把机器分成几种类型:cpu servers(计算密集型),file servers(IO 密集),terminal servers (终端);通过 rcpu 在本机里面会启动一个新的 term,类似 ssh,然后将本地的文件系统挂载给远端的 cpu server 上,相当于给本地的机器换了个 CPU,这是我见过最简洁和最完整的计算存储分离。

其实 Plan9 从某种意义上来说间接影响了我们现代的分布式系统设计,原因是其实这帮大牛作者(Rob Pike,Ken 等人)从贝尔实验室离开后加入了 Google 搞 Infra,其实仔细看看 Borg / GFS(Colossus),以及 Google 内部很多服务都是一股浓浓的 plan9 的味道:通过文件系统作为抽象屏蔽掉分布式的细节。Google 的这些系统通过一篇篇论文影响着整个行业,从这个角度看,plan9 也算成功了。

写在最后

一个优秀的软件,在日常使用或者阅读源码或者学习它的设计的时候,总是能隐约感受到其设计者清晰的主线,或者说是‘世界观’或者‘道’,不分大小,大的如同 Plan9 ‘一切皆是文件’ 的宣言,小的如同 Lua 中的 table (用过 Lua 的朋友一定知道我在说什么)。一个好的主线应该是清晰,简洁,优雅的,你可能看不见它,但又感觉无处不在。在这个焦虑作为底色的时代,一切都变化得太快,我们因为自身如何适应这个不断变化的世界而焦虑,我们以及我们所造的系统都在不停的追求迭代和进化,而进化密码可能是隐藏在这些亘古不变的古老智慧里面。懂得欣赏这些‘不变’的美,会让我们更清晰的了解自己,了解自己应该做出什么样子的软件,也可以更从容的面对这个不停变化的世界。

其实本来写到这里就应该结束了,我写完上一句愣了一下,因为事实上,这个世界并不是美的总是最后的胜利者,美学角度的优雅只是一个优秀软件的很多优秀方面中的一小部分(甚至不是最重要的),所以最后也是想提醒一下各位,美的东西通常会让人沉迷,这在某种程度上会让人看不到的全局。所以按照惯例,我想分享一段话,作为这篇文章的结尾和大家共勉,这篇文章是 Eric Raymond 在一篇 Blog 中总结 Plan9 的失败时写道的 :

Compared to Plan 9, Unix creaks and clanks and has obvious rust spots, but it gets the job done well enough to hold its position. There is a lesson here for ambitious system architects: the most dangerous enemy of a better solution is an existing codebase that is just good enough.