你的眼里不能只有架构

Mew151
14 min readJan 17, 2023

--

身为技术人,在积累了一些经验之后,或多或少会参与到系统设计中,自然而然,会去看一些有关架构的知识,以便应用于业务。在主导过几次业务系统的设计与实现后,你可能被提升为组长,或者是 leader,此时能够感受到系统架构的重要性,于是在带团队时,可能会更看中大家的系统设计,而忽略了从设计到现实的落地过程。平时在看一些技术文章时,也只会关注诸如百万并发,几亿用户的系统设计这类内容。

诚然,架构设计的重要性不言而喻,没有它,做出来的系统可维护性、可扩展性都没办法保障,但与架构同样重要的,是对技术基础知识的熟练掌握。它和架构是相辅相成的,一个良好的系统是由两者共同构建而成的。这就好比盖楼,光有好的设计图纸不行,还必须保证每一块砖都砌的严丝合缝,这样盖出来的楼才能方方正正。

那上面所说的基础知识都包括哪些呢?其实很多都是你大学里学的那些课程:操作系统、网络协议、数据结构与算法、代码设计原则与设计模式、代码重构,甚至是高等数学和概率论与数理统计。

这些知识,很多人都只知道很重要,但不是能很深刻的体会到它们的重要性。我就来说说我的观点。

首先,我们都看到这些年新技术风起云涌,让人应接不暇,但静下来仔细想想,其实没有什么颠覆性的新东西,任何新技术、新架构的出现,都是在已有技术之上做的创新。计算机这些理论知识几十年都没变过,它们是计算机的底层逻辑,熟练掌握它们能让你快速吸收这些新技术,同时也让你的技术判断力与技术敏锐度保持在一个较高的水准。

其次,打牢这些基础,能让我们在读技术文章时,对一些大家熟知的东西,有不一样的理解和体会。比如下面举的这三个例子,看看你只是知道,还是对它们有比较全面的认识?

  • 数组使用的是连续的内存空间,链表在内存中是不连续的,因此相对于链表来说,数组对 CPU 缓存是友好的。那么问题来了:友好具体是什么意思?为什么连续就是友好的,不连续就不是友好的 [1]?
  • 我们都知道内存访问速度比磁盘要快,具体快多少 [2]?
  • 我们也知道,在写代码时,尽量用移位代替乘除,因为移位操作更快。那么为什么移位会更快,它比乘除快多少 [3]?

再次,我想说,任何技术、架构的底层逻辑都是相通的,我们能从这些共通的部分当中看到很多基础知识的影子,比如下面的这几个例子:

  • 我们知道,稍微有点儿体量的业务系统,在数据库前添加一层缓存已经是比较常见的操作了。但如果你熟悉操作系统,就会知道,计算机本身为了弥补 CPU 执行指令速度过快与内存访问速度过慢之间的差距,会在处理器内部存在三级缓存 L1 cache, L2 cache, L3 cache [4]。所以你看,在 10 多年前第一次数据库扛不住系统外部访问压力时,并不是某个人一拍脑袋,灵光一现就发现了添加缓存能够解决这个问题,而很大概率是他知道计算机本身就是这么做的,可以拿来借鉴到业务上。
  • 我们经常看到一句话:「几乎任何的软件问题都可以通过引入一个中间层来解决」。除了你在某些架构中看到过引入一些层是为了解决某个问题之外,如果你足够了解操作系统,你会发现这方面最经典的案例之一是引入了虚拟内存这个概念 [5]。
  • 我们经常讨论的线上数据如何平滑迁移,一种方案是系统升级后对写入数据的请求进行新旧库的数据双写,查询请求先查新库,如果没有再查旧库,这期间,后台同时进行旧库数据往新库的迁移,直至完成。而如果你对散列表这种数据结构掌握的很好,你就会马上领悟到,这不就是和散列表扩容中的均摊思想 [6] 有相似之处么?

所以,从以上这些例子你会发现,架构不是凭空出现的,它的一些设计思想和原理,很多时候是基于这些基础知识而来的,对基础知识的熟练掌握会让你对架构的体会与理解更为深刻。

最后,我想谈一下数学。你可能会疑惑,为什么把「高等数学」和「概率论与数理统计」也算在基础知识之内?它们貌似和计算机没有什么直接的关系。但我想说, 计算机的底层是数学 。我最初体会到数学的重要性是受到了吴军老师和左耳朵耗子一些观点 [7] 的启发,随后在日常学习与工作中也逐渐感受到了数学的重要性。比如在看交友应用的系统设计时,会提及邻近位置算法,用到了半正矢公式 [8],再比如当初看计算机网络这本书时,第二章一开始讲物理层的数据通信理论基础,提到了傅里叶分析 [9] ,看着那些陌生的数学符号,你是不是脑瓜子嗡嗡的,只想跳过?以及在我最近负责的某项新业务的技术攻坚中,看到的一些的概念,例如仿射变换,以及它的各种变换参数矩阵 [10] 是怎么来的,如何理解它们并在此基础之上得出业务想要的结果?这些都需要你具备相应的数学知识。

你可能觉得,看不懂也没多大关系,记公式就可以了。但我想说的是, 记公式永远只是下限 。理解公式,甚至能自己推导,会让你对看到的各种资料中的内容理解得更为透彻,对内容掌握的程度也会更好。而如果只是干记公式和结论,过不了多久就会忘掉。在实际的工作中,尤其是机器学习、人工智能等相关领域,一个数学基础不扎实的工程师,可能只会用一些公式套业务,在上面做一些参数调优,而一个真正掌握了这些数学知识的工程师,他处理业务问题的能力上限肯定要高得多。

关于数学我想说的就这些,另外,举一个数学在实际应用中的小例子:如何手算 1.04 ^ 2.02 [11],带你稍微感受一点儿数学的用处。

以上所述总结一下,我认为我们在关注架构设计的同时 也不要忘记在技术的基础知识上下功夫 ,因为这些才是计算机科学的基石,打牢它们才能让你在上层的架构设计中更加地从容。另外,我也想引用一下链家前董事长左晖先生常说的那句话:「做难而正确的事」。这些基础知识固然很枯燥,但如果你知道那是正确的方向之一,就要不畏困难,勇往直前。

[1] 这个问题的解释会涉及到计算机的以下概念:字(word)、CPU 缓存的块(cache block)、空间局部性原理(spatial locality)。综合来说,就是 CPU 并不是从内存中直接读取数据的,而是数据先被加载到缓存中,CPU 再从缓存中读。而数据被加载到缓存中并不是一个字节一个字节来加载的,而是以块(大小是 64 字节)为单位来加载的。这样就很好理解友好这个词的意思了,因为数组是连续的,那么每次都会一次性加载数组的一部分数据到 CPU 缓存中,由于大多数情况下会对数组做顺序访问,因此 CPU 在操作数组时缓存命中率就会很高;相反,由于链表存储空间是不连续的,那么对于顺序访问链表,链表数据会时不时的被加载到缓存,这样缓存命中率就会变得比较低。

Reference:

Running throughout the system is a collection of electrical conduits called buses that carry bytes of information back and forth between the components. Buses are typically designed to transfer fixed-size chunks of bytes known as words. The number of bytes in a word (the word size) is a fundamental system parameter that varies across systems. Most machines today have word sizes of either 4 bytes (32 bits) or 8 bytes (64 bits).

— Computer Systems: A Programmer’s Perspective (3rd Edition) P44

Locality is typically described as having two distinct forms: temporal locality and spatial locality. In a program with good temporal locality, a memory location that is referenced once is likely to be referenced again multiple times in the near future. In a program with good spatial locality, if a memory location is referenced once, then the program is likely to reference a nearby memory location in the near future.

— Computer Systems: A Programmer’s Perspective (3rd Edition) P640

The storage at level k + 1 is partitioned into contiguous chunks of data objects called blocks.

transfers between L1 and L0 typically use word-size blocks. Transfers between L2 and L1 (and L3 and L2, and L4 and L3) typically use blocks of tens of bytes.

— Computer Systems: A Programmer’s Perspective (3rd Edition) P647 & P648

[2] 这个问题其实涉及到了计算机时钟周期的概念。CPU 执行指令、读取数据是以时钟周期为单位来计量的。对于 1GHz 的 CPU,它的 1 个时钟周期是 1 ns。如果 CPU 从内存中读数据,大约需要数百个时钟周期;从磁盘中读数据,大约需要数千万个时钟周期(10 ms 左右),也就是说,内存的访问速度和磁盘的访问速度相差了 10 万倍。

Reference:

Given a 1 GHz cpu, the cycle time is 1 ns per clock cycle.

CSCI 463 Math — Clock speeds and cpu pipes.

If the data your program needs are stored in a CPU register, then they can be accessed in 0 cycles during the execution of the instruction. If stored in a cache, 4 to 75 cycles. If stored in main memory, hundreds of cycles. And if stored in disk, tens of millions of cycles!

— Computer Systems: A Programmer’s Perspective (3rd Edition) P616

[3] 这个问题需要你了解计算机的机器级指令集以及乘法、除法需要的时钟周期数。一般来讲,乘法操作需要 10 多个时钟周期,除法则需要 30 多个时钟周期,而移位只需要 1 个时钟周期,原因是移位操作有对应的机器级指令来直接支持。

Reference:

Historically, the integer multiply instruction on many machines was fairly slow, requiring 10 or more clock cycles, whereas other integer operations-such as addition, subtraction, bit-level operations, and shifting-required only 1 clock cycle.

Integer division on most machines is even slower than integer multiplication- requiring 30 or more clock cycles.

— Computer Systems: A Programmer’s Perspective (3rd Edition) P137 & P139

移位操作对应的机器级指令: SAL SHL SAR SHR

— Computer Systems: A Programmer's Perspective (3rd Edition) P228, Figure 3.10 Integer arithmetic operations.

[4] Computer Systems: A Programmer’s Perspective (3rd Edition) P646, Figure 6.21 The memory hierarchy.

[5] 虚拟内存很大一个作用是为所有进程提供了一个统一的内存视图,使不同进程之间不必直接操作物理内存地址,否则可能会出现多个进程同时操作同一个物理内存地址的情况。如果没有虚拟内存,那有些时候我们为了避免这种情况,在编码时只能 hardcode 某些物理内存地址了,可想而知,那是很愚蠢的。

[6] 散列表的扩容:一种方式是直接扩容。如果散列表当前大小为 1GB,要想扩容为原来的两倍大小,那就需要对 1GB 的数据重新计算哈希值,并且从原来的散列表搬移到新的散列表,听起来就很耗时,是不是?

另一种方式是使用均摊的思想,将扩容操作穿插在插入操作的过程中,分批完成。具体来讲,当有新数据要插入时,我们将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,我们都重复上面的过程。经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次性数据搬移,插入操作就都变得很快了。

[7] 吴军老师在他的《格局》一书中提到过他的一位恩师:

王作英教授是中国最早做语音识别的专家之一。和中国大部分工科出身的学者不同,王老师的数学特别好。他曾留学苏联,毕业于莫斯科国立鲍曼技术大学 — 这所大学在苏联相当于清华,而莫斯科大学相当于北大。在莫斯科期间,王老师除了在鲍曼技术大学做研究,还在莫斯科大学学了很多数学课。因此,他和许多从苏联学成归来的学者一样,理科基础非常扎实,这让他在解决各种未知问题的时候占据很大优势。

而当时国内大部分的工科学者,研究的专业领域很窄,虽然擅长技术,但缺乏理论功底。比如,在语音识别领域,大部分学者只会应用那些复杂的数学模型,不会改进。王老师则不同,很强的数学功底让他不仅能搞清楚复杂数学模型的本质,还能够根据汉语的特点做出修改,这一点非常难得。现在很多人抱怨中国搞技术的人工作时喜欢”山寨”,这其实是没有办法的事情,因为很多人一开始就用错了数学模型,只能”山寨”,难以创新。

我当时比较幸运,遇到一位有真才实学的导师。后来我在清华得过一个蛮大的数学奖项,再后来我在谷歌的机器学习和自然语言理解项目上做出了不少成就,这都要感谢王老师将我领进门,并且让我真正体会到数学的重要性。如果用一句话概括我那几年的收获,就是我学会了用数学的方法解决工程问题。

左耳朵耗子在他的博客中多次提到过数学:

打牢基础就可以突破瓶颈,不打牢基础没有办法突破瓶颈。在技术世界不要觉得量变会造成质变,这是不可能的。技术这个东西就像搞建筑砌砖头,砌砖头砌的再多也不可能让你能成为一个架构师的,因为你不懂原理,不懂科学方法,你就不可能成长上去的,就像学数学一样,当你掌握了微积分这种大杀器后,你解题的能力是无所披靡,而微积分这种方式绝对不是你能”量变”出来的。

程序员如何把控自己的职业 | 酷 壳 — CoolShell

学习不是为了找到答案,而是找到方法。就像数学一样,你学的是方法,是解题思路,是套路,会用方程式解题的和不会用方程式解题的在解题效率上不可比较,而在微积分面前,其它的解题方法都变成了渣渣。你可以看到,掌握高级方法的人比别人的优势有多大,学习的目的就是为了掌握更为高级的方法和解题思路。

如何超过大多数人 | 酷 壳 — CoolShell

[8] Haversine formula — Wikipedia

[9] 计算机网络(第5版) — 图书 — 豆瓣 P70

[10] 仿射变换及其变换矩阵的理解 — shine-lee — 博客园

[11] 这个小例子运用到了高等数学里全微分的知识,参见 高等数学·下册 — 图书 — 豆瓣 P76

如果你能看到这里,很感谢你的阅读,引用我从 Hacker News 上看到的一段话,分享给你:

现在的编程跟几十年前最大的不同是,以前是面对硬件编程,你可以在短短几周中了解计算机是如何工作的,今天有多少程序员知道,他们的笔记本电脑是怎么工作的?

原文 Knowledge of the entire machine’s activity. When I was a young teenager the home… | Hacker News

--

--

Mew151

十年经验的coolder,记录和分享自己对软件开发的一些心得和感悟