返回 登录
5

这位拥有三十多年从业经验的微软程序员,是如何修炼到专家级别的?

原文Education of a Programmer
作者:Terry Crowley
翻译:雁惊寒

译者注: 本文作者在微软工作了将近21年,并拥有35年的行业经验。本文总结了作者这些年的所学所想,包括:对技术的态度、端到端、复杂性、异步性、分层与模块、性能、管理等多个方面。以下是译文,篇幅比较长。

要成为一名专家级的程序员,你需要精通大量的知识,包括:语言、API、算法、数据结构、系统和工具等等。这些知识的发展日新月异,新的语言和编程环境不停地在涌现,并且似乎这些新东西还很热门,人们都在使用它们。对程序员来说,保持知识不落伍以及对知识做到精通是非常重要的。一个木匠需要知道如何选择合适的锤子和钉子,并要能够把钉子笔直地敲进去。

同时,我发现十几年前的一些观念和策略在当前的很多情形下仍然适用。我们知道底层设备的性能或容量发生了数量级的变化,但是有关系统设计的某些思路一直有效。这些抽象的观念比具体的系统实现更为重要。了解一些常见的思维方式对复杂系统的分析和设计非常有帮助。

谦卑与自负

不仅仅是在编程方面,在像计算机这样一个不断发展的领域里,我们需要在谦卑和自负之间寻找一个平衡点。如果你愿意去学习,总会有学不完的东西,也总有一个人可以帮助你学习。一个人既需要谦卑地承认你所不知道的东西,也要有信心去精通一个新的领域并应用已经掌握的知识。我曾经遇到过这么一个人,他在一个独立的领域工作了很长很长时间之后,“忘记了”如何去学习新的事物。 最好的学习是撸起袖子亲自去建立一些东西,哪怕它只是一个原型或者小技巧。我认为,一个优秀的程序员需要对技术有着广泛的了解,同时也要花一些时间去深入研究某些技术,并最终成为专家。当你真正遇到困难的时候,就是你开始深入学习的时候。

端到端的观点

早在1981年,Jerry Saltzer、Dave Reed和Dave Clark就在互联网和分布式系统方面做了早期的研究,并写下了他们对端到端观点的经典论述。现在在互联网上有很多关于这方面的内容,但是有很多都是错误的,所以你应该去阅读原始的论文。他们谦虚地认为端到端的观点并不是一个发明。因为从他们的角度来讲,这只是一个普通的工程策略,除了通讯领域之外,还可以应用在很多领域。他们只是把这些观点写了下来并收集了相关的例子。这里有一小段解释:

在实现系统某些功能的时候,只有在掌握了系统性的知识并亲自动手的前提下才可能正确的完成任务。在某些情况下,系统中某些内部组件的部分实现可能是影响性能的重要因素。

这在SRC论文中被称为“观点”,尽管它已经在维基百科和其他一些地方被提升为“法则”。事实上,还是把它看作是一个观点比较好,在论文中提到,对于系统设计师来说,最难的一个问题就是如何在系统组件之间划分职责。这个问题最终会成为一场争论,因为在切分功能的时候需要权衡利弊以及隔离复杂性,并想办法设计一个可以灵活应对不断变化的需求的可靠而又高性能的系统。世上没有简单的规则可供遵循。

互联网上的大部分讨论都集中在通信系统上,但是端到端的观点适用于更广泛的情形。有一个分布式系统的例子,就是“最终一致性”这个观点。最终一致的系统可以通过让系统的各个组件进入临时性的不一致状态来进行优化和简化,因为有端到端的流程可以解决这些不一致。我比较喜欢可横向扩展的订单系统例子(例如亚马逊所用的)。这种订单系统并不要求每个请求都必须通过中央库存控制节点。中央控制点的缺失可能会导致出现两个终端同时出售某本书的最后一本的情形,但在任何情况下都需要有某种类型的决策系统,例如,通知客户该书需要延期交货。当然,最后的那本书也有可能会在处理订单时被仓库里的铲车碾过而不能出售。一旦你意识到端到端的决策系统是必需的并且在当前系统中已经建立起来,那么可以充分利用这个决策系统,让系统的内部设计得以优化。

系统在对外提供服务以及进行不断性能优化的过程中,会体现出设计的灵活性,而这种灵活性正说明了端到端方法的强大。端到端的方法能促使系统内部运行的灵活性,并促使整个系统更加强大。这说明端到端的方法是“反脆弱”的,并且随着时间的推移,彼此之间互相促进。

端到端的方法提到,在添加系统层次和功能的时候,你应该非常小心,因为这会影响到系统整体性能(或其影响到其他方面,但性能往往是很重要的)。如果你知道当前正在创建的那个层的原始性能,端到端的方法可以帮助你优化这个性能以满足某些特定的需求。如果你破坏了性能,那么即使提供了重要的附加功能,你也可能牺牲了设计的灵活性。

当你有一个足够大而又复杂的系统,并且需要安排整个团队的人员去实现其内部功能的时候,端到端的观点将与组织设计融合在一起。团队在扩展系统功能的时候,通常以牺牲设计灵活性为代价来提供端到端的功能。

在使用端到端方法的时候,可能会遇到的难点之一就是确定“端”到底在哪里。“小跳蚤生出了更小的跳蚤……如此往复……”。

关注复杂性

编码是一种非常精确的艺术,程序的正常运行需要每一行代码的正确执行。但这是有误导性的。程序整体的复杂性与组件交互的复杂性这两者之间并不等同。程序为了隔离复杂性,需要让其中的重要部分看起来简单直观,同时要简化系统中各个组件之间的交互。复杂性的隐藏可以与其他设计方法(如信息隐藏和数据抽象)融合,但如果你真正专注于寻找复杂性在哪里以及如何隔离它,你会参透另外一种设计感悟。

在我的著作中曾多次提到一个关于屏幕重绘算法的例子,这是早期字符视频终端编辑器的屏幕重绘算法,在VI和EMACS中有使用到。早期的视频终端实现了绘制字符核心动作的控制序列以及附加显示功能,并以此来优化重显示,比如上下滚动当前行、插入新行,或在行内移动字符。这里每一个命令的成本都各不相同,并且这些成本在不同制造商的设备上也并不相同。像文本编辑器这样的全屏应用程序希望尽可能快地更新屏幕,因此需要优化其使用的控制序列,使得屏幕能够从一个状态快速地转换为另一个状态。

这些应用程序在设计的时候隐藏了潜在的复杂性。负责修改文本缓冲区的子系统完全不知道如何将缓冲区的修改更新到屏幕上去。在系统设计中,性能分析是寻找复杂性以及隐藏复杂性的关键部分,这是系统设计的常见模式。屏幕更新过程与底层文本缓冲区中的修改,这两者之间是异步的,并且可以与缓冲区实际的修改顺序也是无关的。缓冲区中的内容如何改变并不重要,重要的是改变了什么。

隐藏复杂性是否成功,不是由隐藏的组件决定,而是由组件的消费者决定的。这就是为什么复杂性对组件提供商至关重要的其中一个原因,组件提供商要对组件的端到端使用负责。他们需要清楚地知道组件是如何与系统的其他部分进行交互的,需要知道复杂性是否存在扩散,以及如何扩散的。复杂性的扩散通常表现为“这个组件很难使用”这样的反馈,这意味着组件提供商没有有效地隐藏好内部的复杂性,或者没有确定好功能边界来隐藏复杂性。

分层和组件化

系统设计师的基本作用是将系统分解成不同的组件和层次,并决定构建什么以及从其他地方获得什么。在“构建与购买”的决策中,使用开源代码能节约资金的使用,但从动态来讲,这两种选择是一样的。在规模比较大的工程时,我们要了解这些决定如何随着时间的推移而发挥作用的。我们程序员所能做的就是“改变”,所以这些设计选择的好坏不只在当下要进行评估,而且还要随着产品的不断发展而反复评估。

这里有一些关于系统分层的内容,其中还包含了大量的时间因素,因此往往需要花费更长的时间去学习和领会。

  • 层会泄露。层(或抽象)从根本上来说是会泄露的。这种泄露可能会立即产生后果,也可能会随着时间的推移而产生后果。一种后果是,层的特性会发生渗透并遍布到更多的层中。第二个后果是,层实际上比看起来的更加依赖于内部行为,所以如果你打算要修改层,那么后果和挑战可能比你想象的要大得多。

  • 层包含的功能太多。你打算要实现的功能比实际需要的功能更多,这几乎就是一个真理。在某些情况下,你需要某个功能是因为将来可能会用到它。你特意采纳了这个功能,因为你想“搭上火车”。但这也可能会产生一些后果。 1)组件功能在做出取舍的时候会因为你实际不需要的功能而出现偏差。 2)由于存在你不需要的功能,组件会增加复杂性和约束,这些约束将阻碍该组件在未来的发展。我们发现,对于我们建立的任何一个层,我们最终只是在系统的某一部分里深入地探索其功能。虽然这似乎是积极的,但不是所有的用途都是同等重要的。所以我们最终要花费大量的成本来从一个层面转移到另一层。 3)额外的功能创造了复杂性,并增加了使用错误的可能。如果XML模式定义被指定为XML树一部分的话,我们使用XML校验API会动态地下载模式定义。在我们地文件解析代码中,一旦这个功能被错误地打开,就会导致w3c.org Web服务器的性能急剧下降,就跟受到了DDOS攻击一样(俗称“地雷”API)。

  • 层会被取代。随着需求发生变化,系统也跟着发展,而组件却可能会被放弃。外部组件依赖关系以及内部组件依赖关系也是如此。

  • 构建与购买的决定会发生改变,这是必然结果。但这并不意味着构建或购买的决定是错误的。通常,在一开始的时候,你没有合适的组件,但是后来却有可能会遇到。或者,你使用的组件最终发现它并不符合不断变化的需求。

  • 层会变厚。一旦定义了一个层,我们就要开始为它增加功能。厚的层所带来的问题在于,它往往会降低你持续创新的能力。在某种意义上说,这就是为什么开发操作系统的公司讨厌在其核心功能之上建立厚的层,这样会让创新放缓。避免这种情况发生的一个方法就是禁止在适配器层中有任何额外的状态存储。 MFC就是采用这样的方法来构建在Win32之上的。理解了这一点,系统设计人员就会去寻找机会来分解和简化组件,而不是在其中增加越来越多的功能。

爱因斯坦宇宙

几十年来,我一直在设计异步分布式系统,但还是被一位SQL架构师Pat Helland在微软内部提到的一句话所触动:“我们生活在爱因斯坦宇宙中,没有同时发生的事情”。在构建分布式系统时,实际上几乎所有构建的都是分布式系统,你无法隐藏系统的分布式特性。这使我一直感觉到,RPC,特别是“透明”RPC,显式地试图隐藏交互的分布式性质,从根本上说这是错误的。你需要拥抱系统的分布式特性。

拥抱系统的分布式特性需要认识到以下几点:

  • 你从一开始就要考虑到用户体验,而不是事后去尝试在错误处理、取消和状态报告上打补丁。

  • 使用异步技术来连接组件。同步连接是做不到这一点的。如果某些系统出现同步,那是因为某些内部层在试图隐藏异步,并且这样做掩盖了系统运行时行为的基本特征。

  • 认识并明确地设计交互式状态机,这些状态表示强大的长期存在的内部系统状态。

  • 失败是可预期的。在分布式系统中检测故障的唯一保险的方法是简单地确定你已经等待了“太长”的时间。系统的一些层需要确定它已经等待得太久,并要取消交互。取消操作只是重新建立本地状态和回收本地资源,因为没有一种可靠得方式来通知系统做了取消。为了优化性能,可能会有一个低成本的、不可靠的方式来尝试通知取消。

  • 取消不是回滚,因为它只是回收本地资源和状态。如果需要回滚,则需要当成使端到端的功能。

  • 你永远不会真正知道分布式组件的状态。一旦你知道了分布式组件的状态,它可能已经发生了改变。当你发送操作指令时,它可能会在传输中丢失,可能会被处理,但响应丢失,或者可能需要大量时间来处理,使得远程状态最终会在将来某个时间上发生转换。这需要有强大而有效地重新发现远程状态的能力,而不是期望分布式组件能够可靠地跟踪状态。

我喜欢说,你应该“着迷于异步”。你应该接受它并为它设计,而不是试图隐藏它。

性能

Don Knuth一定会对大家误解他曾说过的“过早优化是一切邪恶的根源”这段话感到震惊。事实上,性能已经持续超过六十多年以令人难以置信的指数级的速度得到提升,这是行业内所有惊人创新的基础,并且创新和变革就像“软件吞噬世界”那样影响着经济。

系统的所有组件都经历了指数级的变化,但是各个组件的变化速度是不同的。硬盘容量的增长速度与内存容量、CPU速度、内存和CPU之间的延迟的变化速度各不相同。即使性能的变化趋势是由相同的底层技术驱动的,其变化速度也会不同。延迟的改进从根本上依赖带宽的改进。在短期内,指数级的变化趋势看起来似乎是线性的,但随着时间的推移,性能变化所带来的影响是势不可挡的。系统部件在性能方面的变化迫使设计决策者定期重新评估。

这样的结果是,曾经看来很明智的设计决策在几年之后变得不再明智。或者在某些情况下,二十年前的一个聪明做法再一次看起来是一个很好的权衡。现代内存映射的特征看起来更像是早期分时(time-sharing)的过程交换,而不是像按需调页(demand paging)那样。

当这些指数级的变化超越了对人类的约束时,重要的转变就会发生。比如从2的16次方个字符的限制(一个用户可以在几个小时内输入)变成到2的32次方个字符的限制(这超出了一个人可以输入的内容)。或者,你能够以比人眼可以察觉的更高的分辨率来捕获数字图像。或者,,放进口袋里。实时流传输音频使得可以将音频存储在一个地方而不是在数千个本地硬盘上重复“记录”。

Jon Devaan曾经说过“设计数据,而不是设计代码”。这也意味着在查看系统的结构时,我对于查看代码如何交互并不感兴趣,我想看的是数据如何交互和流动。如果有人尝试通过描述代码结构来解释系统,而不了解数据流,那么他们就不是真正的了解系统。

内存分层结构也意味着缓存的存在,即使某些系统层在试图隐藏它。缓存是基础,但也是危险的。缓存利用代码的运行时行为来改变系统中不同组件之间的交互模式。如果模型不好或者随着行为的变化而变差,缓存就不会按预期的那样运行。因此必须对缓存进行检测。每个工作了很长时间的程序员都可能经历过有关缓存的恐怖故事。你可以将整个音乐集存储在非常小的硬盘上

幸运的是,我早期的职业生涯是在互联网发源地BBN度过的。我很自然地就把异步组件之间的通信看成是系统连接的基本方式。流量控制和排队理论是通信系统的基础,并且是任何异步系统更为一般的运行方式。流量控制本质上是资源管理(管理渠道的能力),但资源管理是更为根本的事情。流量控制本质上是一个端到端的能力,所以以端到端的方式思考异步系统是一件非常自然的事情。

“光速”这个概念是我所发现的在分析任何系统都有用的概念。光速分析不是针对系统当前的性能,它问的是“这个设计可以达到最佳的理论性能是什么?”,“正在传输的真实信息内容是什么以及以什么速度改变?”,“组件之间的潜在延迟和带宽是多少?”。光速分析迫使设计师深入了解他们的方法是否能够满足性能目标,或者需要重新考虑其他方法。它让设计师深入了解性能消耗在哪里,以及这是否是固有的问题或可能是一些不当行为导致的。从建设性的角度来看,它迫使系统设计师了解其构建模块实际的性能特征,而不是专注于其他功能特征。

我花了很多的时间来构建图形应用程序。人类视觉和神经系统没有经历指数级的变化。系统有其固有一些限制,这意味着系统设计者可以充分利用这些约束,例如,通过虚拟化或通过限制屏幕刷新速率来提升人类视觉系统的感知限制。

复杂性的本质

我在整个职业生涯中都在跟复杂性做斗争。为什么系统和应用程序会变复杂?随着基础设施变得更强大,应用程序领域的开发为什么不会随着时间的推移而变得更加容易,而是越来越困难,并且限制越来越多。事实上,我们管理复杂性的关键方法之一是“走开”,开始新的尝试。通常新的工具或语言迫使我们从头开始,这意味着开发人员最终会将工具的好处与干净的开始的好处结合起来。干净的开始是根本。这并不是说一些新的工具、平台或语言可能不是一件好事,但我可以保证不会解决复杂性增长的问题。控制复杂性增长的最简单的方法是用较少的开发人员构建一个较小的系统。

当然,在许多情况下,“走开”并不是一种选择,因为办公室业务建立在非常有价值和复杂的资产之上。随着OneNote的诞生,Office从Word的复杂性“走了出去”,到不同的维度进行创新。 Sway是另外一个例子,其中Office认为我们需要摆脱限制,以便真正利用重要的环境变化,并有机会采取本质上不同的设计方法。

我曾受到Fred Brook关于软件开发中的事故和本质的“无银弹”论文的影响。我刚刚又重新阅读了这篇文章,并惊讶的看到,他讨论了两个趋势中最能影响未来开发人员生产力的是在“构建与购买”决策中强调“购买”,这是开源和云基础设施的出现的征兆。另一种趋势是转向更为“有机”或“生物”增量方式,而不是单纯的构成方式。现代读者把这个看成是敏捷和持续的发展过程。这是在1986年啊!

我一直很关注Stuart Kauffman关于复杂性本质的研究。Kauffman从一个简单的布尔网络模型(“NK模型”)建立起来,探究基础数学结构的应用,诸如交互分子系统、遗传网络、生态系统、经济系统和计算机系统,并领会突发命令行为和混沌行为之间的关系的数学基础。在高度连接的系统中,本质上具有的冲突约束,使得难以让系统向前发展。控制这种复杂性的一个基本方法是将系统转化为一个个独立的元素,并限制元素之间的互连(从根本上减少NK模型中的“N”和“K”)。当然,系统设计人员会很自然地实现复杂性隐藏、信息隐藏和数据抽象技术以及使用松散的异步耦合来限制组件之间的交互。

我们一直面临的挑战是,我们改进系统的方法涉及到了各个方面。实时协同著作是Office应用程序目前的一个非常具体(复杂)的例子。

设计用户体验的一个挑战是,我们需要将一组有限的手势映射到底层数据模型状态空间中。增加状态空间的维度不可避免地为用户手势产生歧义。这“只是数学”上的,这意味着确保系统保持“易于使用”的最根本的方法是限制底层数据模型。

管理

我从高中开始就担任领导职务(学生会理事长),并且总是很自然地认为自己应当承担更大的责任。同时,我一直很自豪,我在每一个管理阶段仍然是一名全职的程序员。办公室开发副总裁这个职务终于把我推到了边缘,远离了日常的编程。过去一年,当我离开这个工作后,我已经喜欢上了重新回到编程的日子,这是一个令人难以置信的富有创意而又充实的活动(有时你可能会为了解决“最后一个”bug而觉得沮丧)。

尽管我到微软十多年来一直都是“经理”,但我直到1996年才真正认识到管理。微软强调“工程领导是技术领导”。这符合我的观点,并帮助我既接受又逐渐适应更大的管理责任。

最令我得到共鸣的事情是办公室中的“透明”文化。经理的工作是设计和使用透明的流程来推动项目。透明度需要被设计到系统中。最好的透明度是要能够跟踪进度,以单个工程师日常活动(工作项目完成,错误打开和修复,方案完成)这样的粒度进行输出。

我在Beyond Software的时候,我真正认识到拥有一个有能力的领导者对项目的重要性。当工程经理离开后,其他的四个领导都不愿意担当这个角色,主要的原因是我们不知道我们要坚持多久。我们都是技术敏锐型的,并且相处得很好,所以我们决定平等地推动项目前进。但这简直就是一团糟。一个明显的问题是,我们没有分配资源的战略想法,这是管理上的最高职责之一!我们没有任何领导来统一目标和制定约束。

有一件事我一直记得很清楚,那是我第一次意识到倾听领导的重要性。那时我刚刚担任了Word,OneNote,Publisher和Text Services的集团开发经理。对于如何组织文本服务团队有一个很大的争议,我需要去了解每一个主要参与者的想法,倾听他们的意见,然后整理我所听到的一切。当我向其中一位主要参与者展示了这份报告时,他的反应是“哇,你真的听到了我说的话”!作为一个经理,我遇到的最大的问题是需要仔细聆听所有参与者的声音。倾听是一个积极主动的过程,涉及到理解观点,然后写下我所学到的并测试它来验证我的理解。

我曾经当过FrontPage开发经理,在那时我内化了根据部分信息进行决策所固有的“操作困境”的问题。等待的时间越长,你做出决定所获得的信息就越多。但是等待的时间越长,实现起来的灵活性就越低。在某些时候你只需要打电话。

在你刚进入管理领域时,你会发现你和你的新同事不会突然变得更聪明,因此你现在有着更大的责任。这进一步说明了一个整体的组织比拥有一个聪明的领导者更重要。授权每一个级别的参与者可以自己做决定是很关键的方法。倾听,以及对组织解释你所做的决策背后的原因是另一个关键策略。担心自己做出愚蠢的决定这种想法可能是也非常有用,这能确保你清楚地表达你自己的理由,并确保你听到了所有的声音。

结论

在我接受大学毕业后的第一份工作面试时,招聘人员问我是否对“系统”或“应用程序”的工作感兴趣。当时我没有真正理解这个问题。有趣的问题会出现在软件栈的每一个层次,我很乐于探究这些问题。 请保持不断学习的态度吧。

评论