Skip to content

程序员的 README

书籍链接

程序员的 README

思维导图














内容提要

  • 对于刚刚成为软件工程师的新手来说,知道如何编写代码只是成功了一半。你可能很快就会发现,学校并没有教授在现实世界中至关重要的技能和工作中必要的流程。
  • 本书恰恰填补了这一环节,它是作者十多年来在大型公司指导初级工程师工作的教程,涵盖软件工程的基础知识和最佳实践。

章节介绍:

  • 第 1 ~ 2 章讲解当你在公司开启你的职业生涯时会发生什么;
  • 第 3 ~ 11 章会扩展你的工作技能,教你如何使用现有代码库、解决和防止技术债、编写生产级软件、管理依赖关系、有效地测试、评审代码、交付软件、处理 On-Call 时的事故和构建可演进的架构等;
  • 剩余章节涵盖管理能力和职业阶梯的提升等相关内容,例如敏捷计划、与管理者合作以及成长为资深工程师的必经之路。
  • 本书中非常重要的一部分内容是教你如何应对糟糕的管理,以及如何调整自己的节奏。

  • 人类是具有习惯的生物。虽然软件工程师可能会被一些人视为特殊人群,但他们也可能会陷入习惯中。作为一名软件工程师,不断地学习和发展至关重要,不管当前的经验或职位怎样,这本书对每一名志在从事软件开发工作的人员都很有用。一名成熟的软件开发者的标志是打破固有的习惯,批判性地回顾旧代码,发现瑕疵,做到自省,且为没多做些什么而感到羞愧。

  • 这本书针对如何改进、如何学习、如何推进职业生涯发展,以及如何成为一名更好的开发者提供不同的方法和步骤。

  • 这本书包含适应团队的工作流程、处理会议、如期交付、善用学习工具和技术领域的最佳实践,并指导人们如何成为团队中有价值的成员。

前言

  • 我们将解释构建、测试和运行生产软件的现代实践,并阐释那些可以使团队更强大和使队友更默契的行为和方法。
  • 我们会给你一些实用的建议,诸如:
    • 如何获得帮助。
    • 如何撰写设计文档。
    • 如何维护旧代码。
    • 如何 On-Call(待命)。
    • 如何规划你的工作以及如何与你的管理者和团队互动。

第 1 章 前面的旅程

能力范围

技术知识

  • 掌握计算机科学的基础知识、知道如何使用集成开发环境(IDE)、构建系统、调试代码和测试框架。
  • 熟悉持续集成、系统指标和监控、配置和打包系统。
  • 积极主动地创建和改进测试代码。
  • 在做架构决策时,考虑到长期运维。

执行力

  • 通过用代码解决问题来创造价值,并且你了解你的工作和业务之间的联系。
  • 可以构建并部署中小型的特性。
  • 会编写、测试和评审代码。
  • 分担 On-Call 的职责,调试运维问题。
  • 积极主动并且可靠的。参加技术讲座、阅读小组、面谈和路演。

沟通能力

  • 能同时以书面和口头的形式进行清晰的沟通。
  • 能够有效地给予和接受反馈。在模棱两可的情况下,你会主动寻求帮助并得到明确的结果。
  • 能以建设性的方式提出问题和定义课题。在可能的情况下可以提供帮助,并开始影响同事。
  • 能够文档化你的工作。撰写清晰的设计文档并征求反馈意见。
  • 在与他人打交道时,富有耐心和同理心。

领导力

  • 能在指定的工作范围内独立地完成工作。
  • 能迅速地从错误中学习。能很好地处理变动和模糊的问题。
  • 积极参与到项目和季度的规划中。帮助新的成员融入团队。向管理者提供有意义的反馈。

工作路线

入职

  • 熟悉公司、团队,以及如何完成本职工作;
  • 参加入职会议;
  • 设置你的开发环境和系统权限,并弄清楚团队的常规流程和会议;
  • 阅读文档并与队友进行讨论。如果你在入职过程中发现了漏洞,你可以在文档中做出一些补充。
  • 如果你的公司没有安排新人入职培训,那需要你自己去向你的管理者要一份公司的“组织架构图”,了解清楚谁负责什么,谁向谁汇报,都有哪些不同的部门,以及它们之间的关系。记得做好笔记。

成长

  • 多提问,并经常让团队评审你的工作成果。
  • 了解如何编译、测试和部署代码。
  • 阅读那些提交代码的请求和代码评审意见。
  • 不要害怕询问更多的信息。
  • 多报名参加技术讲座、午餐会、阅读小组、导师计划,诸如此类。
  • 了解管理者的工作风格,理解他们的期望,并与他们谈谈你的目标。如果你的管理者有一对一面谈的习惯,那么你要期待有几场这样的谈话。管理者通常都希望跟踪事情的进展,所以问问他们后续如何沟通。
  • 参加项目总结会或全员会议。要了解一下路线图和开发计划的全貌。

贡献

  • 学习如何编写生产级别的代码,使这些代码对运维者友好。
  • 恰当地管理组件间的依赖关系,并进行完备的测试。
  • 参与到代码评审中去,做好队友会询问你的想法和反馈的准备。
  • 参与到团队的计划中去,并与你的管理者一同制定目标或 OKR(目标和关键成果)。

运维

  • 学习如何向客户交付代码。
  • 客户会因为软件的不稳定而受到影响。你需要使用监控指标、日志和跟踪工具来实时调试软件。
  • 这时你也可能需要参与轮流的 On-Call。接触运维工作会让你清楚地了解那些代码如何在客户的手中发挥作用。同时你也要学会保护你的软件。

胜任

  • 你的团队现在将依靠你来负责一个小项目。你需要撰写一份技术设计文档并帮助团队进行项目规划。设计软件将迫使你面临全新级别的复杂度。不要满足于你的第一版设计。反复斟酌,要随时做好准备,因为你的系统会随着时间的推移而不断变化。
  • 你在系统架构、编译环节、部署环节以及测试环境中都看到了不足之处。你要开始学习在必要的维护和重构中间寻找平衡。不要试图重构一切。
  • 你可能也对团队的工作流有见解。写下你的观察,哪些是有效的,哪些是无效的,然后与你的管理者一对一地谈谈你的想法。
  • 现在是设定长期目标和评估绩效的时候了。同管理者一起理解这个过程,并从同事那里获得反馈。同管理者谈谈职业上的志向、未来的工作、项目和想法。

坎宁安定律

“在互联网上获得正确答案的最好方法并不是提出问题,而是发布错误的答案。”

我们建议你在团队中用文档记录下会议的内容、入职流程和其他口口相传的东西。 你会得到很多的评论和纠正。不要把这些评论和纠正当作针对你个人的批评。
重点并不是要写一份完美的文档,而是要写得足够多,以引发讨论,充实细节。 这是坎宁安定律的一个应用。

自行车棚效应

过度集中在细枝末节上的讨论总是会很冗长。

第 2 章 步入自觉阶段

  • 本章的大部分内容将阐释如何自主学习和如何获得帮助。 校外学习是一种技能。我们会为如何养成独立自主的学习习惯提供一系列建议。
  • 我们还将准备一些提示,方便你在“万事都求人”和“独行侠”之间取得平衡。
  • 本章的最后将讨论冒充者综合征和邓宁-克鲁格效应,这可能会导致新工程师感到自信不足或自信爆棚,而这两种情况都会限制他们的成长。
  • 我们将解释如何自省并遏制这两种极端情况。在避免落入自我怀疑和过度自信的陷阱的同时,练习独立学习并提出有效的问题将会使你迅速地到达第三个阶段。

学习如何学习

  • 本节将列举各种各样的学习方法。切勿试图同时去做本章中列出的所有事情! 因为那样会让你感到倦怠。切记善用个人的时间——虽然说持续进步非常重要,但是把所有清醒的时间都花在工作上是不健康的。应该根据你的现实情况和自然倾向,从下面的方法中选择。
  • 在工作的前几个月里,你要学习一切如何运作。这将有助于你参与设计讨论、On-Call 轮换、解决运维问题和评审代码。
  • 在实践中学到的东西要比只坐在那里单纯地阅读学到的多出许多。你应该上手编写并且发布代码。尽你所能去理解你的工作会造成的影响,并以适当的谨慎程度行事。与变更高流量数据库上的索引相比,编写单元测试可以不那么谨慎,从而更快。
  • 错误是不可避免的。成为一名软件工程师的路途艰辛,我们有时会失败。这几乎是所有人都知道的事情。降低系统风险并使这些错误不那么致命是你的管理者和团队的工作。如果你失败了,也不要被击垮:写下经验教训,然后继续前行。
  • 运行实例代码可以真正地了解代码的工作原理。要注意,在复杂的情况下,特别是在多线程的应用程序中,输出调试信息可能会产生误导。因为操作系统会使用缓冲的方式将内容写到标准输出接口,这样会让你在控制台中看到的信息有延迟。而且多个线程都在向这个接口写入信息,这样输出的信息就会交织在一起。
  • 请每周都花一部分时间去阅读。可供阅读的内容有很多:团队文档、设计文档、代码、积压的任务票、书籍、论文和技术网站。
  • 去读源代码,因为它并不总是与设计文档相吻合!不要像阅读小说一样从前到后地通读代码:请利用你的 IDE 来浏览代码。为关键的操作绘制控制流和状态图。仔细研究代码的数据结构和算法。注意那些临界值的处理。留意那些惯用写法和风格,也就是去学习“本地方言”(local dialect)。
  • 在“任务票”(ticket)或“问题点”(issue)中跟踪未完成的工作。阅读团队的任务票,看看每个人都在做的事情以及即将发生的事情。
  • 你可以从一个优秀的讲座中学到许多东西。从过去录制的视频演示开始,包括公司内部演示和外部的 YouTube 视频。观看教程、技术讲座和阅读会议简报(conference presentations)。
  • 会议和聚会非常有利于建立联系和发现新的想法。它们值得偶尔参加,但不要过度。如果你开始觉得你不再有学习进展了,可以去当地大学看看。他们有大量向公众开放的项目。扩大你的圈子,接触新的想法。去上研究生是一个可选项。
  • 跟班学习是指在另一个人执行任务时跟着他。跟随者是一个积极的参与者:他做笔记并提出问题。跟随一名高级工程师是学习新技能的好方法。为了获得更大的收益,你应该在整个跟班学习过程的前后安排时间进行计划和回顾。
  • 从事副业项目会让你接触新的技术和想法。你也可以参与开源项目。不要根据你认为你需要学习的领域来选择项目。找到你有兴趣去解决的问题,并使用你想学习的工具来解决这些问题。公司一般会对外部的工作有规定,询问你公司的政策。不要使用公司资源(你的公司提供的笔记本计算机)来从事副业项目,不要在工作中从事副业项目,避免那些与你公司有竞争的副业项目。

提出问题

  • 有效地提出问题将帮助你快速地学习,而不会烦扰其他人。使用这 3 个步骤:做研究,提出明确的问题,并恰当地安排解决你的问题所需的时间
  • 不要只是在互联网上搜索。信息还存在于文档、内部论坛、自述文件(README)、源代码和错误跟踪器中。如果你的问题是关于代码的,试着把它变成一个可以演示的单元测试。
  • 限制你研究一个问题时预期花费的时间。 在你开始研究之前就应该设定好时间限制,这样可以鼓励你遵守这个限制,防止收益递减(研究最终会拖累生产性)。考虑你最终何时需要知道答案,然后留出足够的时间来提出问题,得到回答,并根据你学到的东西采取行动。
  • 在提出问题时描述你已经知道的情况。不要只是分享你的原始笔记。 简要地描述你所做的尝试和发现,这表明你已经花了很多时间去试图自己解决这个问题。 这样做也会给别人一个回答你的起点。
  • 就像你一样,其他人也在努力完成工作,他们需要专注。当他们进入状态时,不要打扰他们——即使问题很简单,即使你心里清楚他们知道答案,即使你的工作被卡住了。除非有重大问题发生,真的,请不要打扰他们。你需要找到一种异步的沟通方式。
  • 批量处理你的同步请求。
  • 知道如何学习以及如何提出问题还不够。你还必须避开那些会减缓你成长的障碍。一般有两个常见的障碍会影响许多工程师,即“冒充者综合征”和邓宁-克鲁格效应。如果你了解这些现象是什么,以及如何克服它们,你会成长得更快。

冒充者综合征

尽管有着杰出的学术和职业成就,经历着冒充者现象的女性仍然坚持认为她们真的不聪明,而且还愚弄了任何不这么想的人。众多的成就似乎并不影响冒充者的信念,而这些成就本身就是能充分证明智力水平卓越的客观证据。

  • 你可以用几种策略来推动事情的发展:觉知(awareness)、重塑(reframing)以及与同事交谈。
  • 有意识地打破每一个错误都会被看作能力匮乏的证明,而每一项成功都是优秀“冒充者”冒充的证据的循环。
  • 不要忽视赞美和成就。
  • 获得反馈也有助于缓解冒充者综合征。

邓宁-克鲁格效应

这是一种认知偏见,人们认为自己比实际情况更有能力。处于“无意识的无能力”阶段的工程师不知道自己不知道什么,所以他们不能准确地评估自己和他人的表现。

  • 有意识地培养好奇心;对犯错持开放态度;
  • 找到一位受人尊敬的工程师,询问他你做得怎么样,并真正地倾听;
  • 讨论设计决策,尤其是那些你不同意的决策,问问为什么会做出这样的决策;
  • 培养一种权衡利弊的心态,而不是非黑即白的心态。

行为准则

补充阅读

第 3 章 玩转代码

本章将告诉你 如何处理现有的代码。 我们将介绍混乱的根源,即“软件的熵”和“技术债”,然后给你一些视角。接着,我们将就如何安全地修改代码给出实用的指导。最后我们将给出一些提示来避免意外地造成代码混乱。

软件的熵

  • 混乱的代码是变化的自然副作用,不要把代码的不整洁归咎于开发者。这种走向无序的趋势被称为软件的熵(software entropy)。
  • 软件的熵可以被管理。代码风格bug 检测工具有助于保持代码的整洁(参见第 6 章),代码评审有助于传播知识和减少不一致(参见第 7 章),持续的重构可以减少熵(参见 3.3 节)。

技术债

  • 技术债是为了修复现有的代码不足而欠下的未来工作。

  • 与金融债务一样,技术债也有“本金”和“利息”。本金是那些需要修复的原始不足。利息是随着代码的发展没有解决的潜在不足,因为实施了越来越复杂的变通方法。随着变通办法的复制和巩固,利息就会增加。

  • 你不同意的技术决策并不是技术债,你不喜欢的代码也不是。要成为技术债,这个问题必须迫使团队“支付利息”,或者代码必须冒着触发严重问题的风险,因为严重问题需要紧急支付。

  • 技术债类型及判断

    1. 让我们先发布再处理:在代码的已知不足和交付速度之间进行务实的取舍。只要团队有规划地解决这个问题,这就是好的债务。
    2. 我们没有时间去设计:在团队面临交付压力的情况下产生的鲁莽的债务,尽量避免。
    3. 不知道自己不知道:你可以通过事前写下实施计划并获得反馈的方式,以及进行代码评审的方式来减轻这种债务的危险。持续学习也可以最大限度地减少这种无意的鲁莽行为。
    4. 现在我们知道了当时应该怎么做:这种类型的债务更像是在出问题的领域反思学习或作为软件架构师成长的必经之路,而不是未做功课这么简单。健康的团队使用诸如项目回顾等做法来发现无心之债,并讨论何时以及是否偿还。
  • 技术债总是不可避免的,因为你无法防止无意中的错误。技术债甚至可能是成功的标志:项目只有存活了足够长的时间,才会变得无序。

  • 不要等到世界都停转一个月了才去解决问题。相反地,要边做边解决,着手去做小幅的重构。在小幅的、独立的提交(commit)和拉动请求(pull request)中推动问题的修改。

  • 大型重构是一项重大的投入。在短期内,偿还技术债会拖慢交付特性的速度,而承担更多的技术债会加速交付。长期来看,情况正好相反:偿还技术债会加快交付的速度,而承担更多的债务则会减缓交付。正确的平衡点在很大程度上取决于环境。如果你有关于大规模重构或重写某些模块的建议,请先向你的团队说明情况。下面是讨论技术债的一个优秀的模板

    1. 按事实陈述情况;
    2. 描述技术债的风险和成本;
    3. 提出解决方案;
    4. 讨论备选方案(不采取行动也是备选方案);
    5. 权衡利弊。

    以书面形式提出你的建议。不要把你的呼吁建立在价值判断上(“这代码又老又难看”),将重点放在技术债的成本和修复它带来的好处上。要具体,如果有人要求你证明这种改动会带来哪些好处,不要感到惊讶。

变更代码

  1. 你必须在不破坏现有行为的情况下进行修改。
  2. 你必须理解其他开发者的想法,坚持原有的代码风格和设计模式。
  3. 而且,你必须在工作中温和地改进代码库。

所谓重构,是指在不改变软件行为的情况下改进内部代码结构。它经常发生在添加新特性的时候,因为它使新特性可以更容易地被添加。而在修复 bug 的过程中,则经常删除代码。

  • 如何安全地在现有代码库中修改代码参考步骤:

    1. 定义变更点;
    2. 寻找测试点;
    3. 打破依赖关系;
    4. 编写测试;
    5. 进行修改和重构。
  • 当你重构和打破依赖关系时,应该添加新的测试来验证旧的行为。在迭代过程中要频繁地运行测试套件,包括新的和旧的测试用例。考虑使用自动测试工具来生成捕获现有行为的测试用例。

  • 在不影响整个项目持续运转的情况下要持续地重构工程,这样重构的成本就会平摊在多次的版本更迭中。

  • 当你修复错误或增加新的特性时,只清理有关联性的代码。不要不顾一切地去找“脏”代码,要“随缘”一些。尽量将清理代码的提交和改变行为的提交各自分开。 分开提交可以让你在不会丢失针对代码清理的提交的基础上,更容易地去恢复代码变更。较小的提交也更容易针对变更的部分进行评审。

  • 通过代码质量检查工具优化那些不一定是 bug,但采用了已知会导致问题的代码模式。

  • 在你着手重构的时候,要得到你的团队的支持。因为你正在修改你的团队的代码,他们也应该参与进来。

  • 重构的成本也可能超过其价值。正在被替换的旧的、废弃的代码不需要被重构,同理,低风险或很少被触及的代码也不需要。在重构的时候要务实。

  • IDE 在重构时特别有帮助。它们拥有可以方便地重命名和移动代码、提取方法和字段、更新方法签名,以及进行其他常见的操作的工具。

  • 代码变更都应该被提交到版本控制系统(VCS),如 Git。

  • 版本控制系统提交规范建议:

    1. 用一个空行将标题与正文分开。
    2. 标题行限制在 50 个字符以内。
    3. 标题行要大写。
    4. 不要以句号结束标题行。
    5. 在标题行中使用命令式语气。
    6. 将正文限制在 72 个字符之内。
    7. 用正文解释修改的内容和原因,而不解释如何修改。
  • 任何技术创业公司必须做的主要的事情是建立一个产品,这个产品在做某件事情时至少要比目前流行的方式好十倍。两倍或三倍的改进不足以让人们快速或大量地转向新事物。 如果你想重构代码或重定义标准,你的改进就必须是一个数量级层面的改进。重构、打破惯例或在技术栈中添加新技术时要谨慎。把重构代码的机会留给高价值的情况。在可能的情况下使用保守一些的技术。不要忽视惯例,即便你不同意它,也要避免对代码硬分叉。

避“坑”指南

  • 技术选型前可以问自己以下的问题:(对这些问题的回答与语言本身的特点一样重要。
    1. 围绕一种新语言的生态系统的成熟度尤其关键。构建和打包系统是否考虑周全?
    2. IDE 的支持情况如何?
    3. 重要的类库是否由经验丰富的开发者维护?
    4. 测试框架是否可用?
    5. 如果你需要技术支持,你能支付费用吗?你能雇用到具有相关技能的工程师吗?
    6. 该语言是否容易掌握?
    7. 该语言的性能如何?
    8. 该语言的生态系统是否可以与公司现有的工具集成在一起?
  • 你的喜好可能真的更好,但特立独行仍然不是一个好主意。在短期内,大家要做一样的事情。试着去理解标准做法的理由,它有可能是在解决一个不显眼的问题。如果你不能找出一个好的理由,就去四处打听一下。如果你仍然找不到答案,就与你的管理者和负责该技术的团队交流一下。
  • 在改变标准时,会有许多方面需要考虑:优先权、所有权、成本和实施细节。说服一个团队终结他们自己的东西并不容易,一定会有很多意见。你需要实事求是。
  • 不要只分叉而不向上游提交修改。
  • 只有在收益大于成本的情况下才应该进行重构。重构是有风险的,成本也很高。工程师们总是会低估重构花费的时间,尤其是迁移花费的时间往往很可怕。数据需要被转移,上游和下游的系统都需要同步更新,这可能需要几年甚至几十年。

第二系统综合征

第一个系统的范围是有限的,因为它的创造者并不了解可能会出问题的地方。这个系统完成了它的工作,但它是笨拙的和有限的。现在有经验的开发者清楚地看到了他们的问题所在,他们开始用他们的一切聪明才智来开发第二个系统。

新系统是为灵活性而设计的,所有东西都是可配置和可注入的。可悲的是,第二个系统通常是一个臃肿的烂摊子。

如果你要着手重构一个系统,要小心过度扩张。

行为准则

补充阅读

第 4 章 编写可维护的代码

本章描述了一些最佳实践,它们将使你的代码更容易在生产环境中运行。 本章要涵盖的内容很多,所以行文比较紧凑。到最后,你将熟悉那些可以使你的软件具有可操作性的关键概念和工具。此外,与可操作性相关的评审意见在代码评审环节中很常见,这些信息将帮助你给予和接受更好的反馈。

防御式编程

  1. 避免空值:通过检查变量是否为空,通过使用空对象模式(null object pattern),或通过可选类型(option type)来避免空指针异常。
  2. 保持变量不可变:作为奖励,使用不可变的变量可以使并发编程变得更简单,而且当编译器或运行环境知道变量不会改变时就可以运转得更有效率。
  3. 使用类型提示和静态类型检查器:限制变量将确保意外的值会立即失效(甚至可能无法编译),而不是任由其引发潜在的 bug。在定义变量时,尽可能使用最具体的类型。
  4. 验证输入:通过校验输入的正确性去保护你的代码,可以使用先决条件、校验和以及校验数据合法性,套用安全领域中的最佳实践以及使用工具等方法来发现常见的错误。尽可能地提早拒绝不良输入。使用前置条件和后置条件的方式来校验方法中输入的变量。
  5. 限制具体的提交文件类型:如果你需要强大的耐久性保证,使用校验和的方式来检查数据没有意外的变化。也不要忽视安全问题,外部输入是危险的。恶意用户可能试图在输入中注入代码或 SQL,或撑爆缓冲区以获得对你的应用程序的控制权限。使用成熟的类库和框架来防止跨站脚本攻击,总是强制转义输入的字符来防止 SQL 注入攻击。在使用 strcpy(特别是 strncpy)等命令操作内存时,明确地设置缓冲区的大小,以防止缓冲区溢出。使用广泛采用的安全与密码类库或协议,而不是自己去编写这样的类库或协议。熟悉开放式 Web 应用程序安全项目(open Web application security project,OWASP)的十大安全报告以快速建立你的安全知识体系。
  6. 善用异常:异常(Error 类)可以比 null 或 −1 携带更多的信息,它们可以被命名,并有堆栈跟踪、行号和错误消息。
  7. 异常要有精确含义:使用异常处理来应对故障,而不是控制应用程序的运行逻辑。尽可能地使用内置的异常,避免创建通用的异常。
  8. 早抛晚捕:早抛”意味着在尽可能接近错误的地方引发异常,这样开发人员就能迅速地定位相关的代码。“晚捕”意味着在调用的堆栈上传播这个异常,直到你到达能够处理异常的程序的层级。
  9. 智能重试:在实践中,决定何时重试以及重试的频率都需要一些技巧。
    • “退避”策略:非线性地增加休眠时间(通常使用指数退避,如(retry number)^2)。如果你使用这种方法,请确保将退避时间限定在某个最大值内,这样它就不会变得太大。会导致“惊群效应”。可以在退避策略中加入抖动。有了抖动,客户端就会给退避增加一个随机的、有限制的时间。引入随机性可以分散请求,降低发生“踩踏”的可能性。
    • “快速失败”:不要盲目地重试所有失败的调用,尤其是那些写入数据或可能触发一些业务流程的调用。让应用程序在遇到其在设计时没有预想到的错误时崩溃。
  10. 构建幂等系统:一个幂等的操作是可以被进行多次并且仍然产生相同结果的操作。通过允许客户端单独为每个请求提供一个唯一 ID 的方式,远程 API 就可以变为幂等 API。当客户端重试时,它提供的唯一 ID 与失败时的相同。如果该请求已经被处理过了,服务器可以移除重复的请求。让你的所有操作都成为幂等操作,这可大大简化系统的交互,同时也可消除一大类潜在的错误。
  11. 及时释放资源:当故障发生后,要确保清理所有的资源,释放你不再需要的内存、数据结构、网络套接字和文件句柄。所谓网络套接字泄露,是指在使用后没有关闭它们。网络套接字泄露会使无用的连接一直存在,从而填满连接池。如果你的编程语言不支持自动关闭,请将你的代码包裹在一个 try/ finally 代码块中,这样即使发生了异常也能安全地关闭文件句柄。

关于日志的使用

输出日志信息对理解代码或调试一个小程序来说既简单又方便。对于复杂的应用程序,编程语言有精良的日志类库,让运维人员对要记录的内容和时间有更多的控制。运维人员可以通过修改日志级别来调节输出日志的总量,并控制日志格式。日志框架还可以注入上下文信息,诸如线程名、主机名、ID,你可以在调试的时候使用这些信息。日志框架与日志管理系统可以很好地配合,这种系统可以聚集日志信息,所以运维人员可以过滤并搜索它们。

  • 日志框架设有日志级别,它可以让运维人员根据重要性过滤消息。日志级别通常可以通过全局配置和对包或类级别的覆写来控制。日志级别可以让运维人员根据特定的情况来调整日志量,从极其详细的调试日志到正常操作的稳定的背景常规输出。
  • 日志常见分级参考:
    1. TRACE:一个极其精细的日志级别,只对特定的包或类开放,在开发阶段之外很少使用这个级别。如果你需要逐行的日志或数据结构临时信息,那么可以使用这个级别。
    2. DEBUG:这个日志级别多用于那些只在调查产品出故障时有用,但在正常操作中没有用的日志。
    3. INFO:这个日志级别一般用于输出应用程序运转良好的日志,不应该用于输出任何问题的指示。INFO 级别的日志应该在正常操作中告诉我们一些有用的信息。
    4. WARN:这个日志级别一般用于提示那些潜在问题。每当你记录一个 WARN 时,应该对应一个你希望看到这个日志的人去采取的具体行动。
    5. ERROR:这个日志级别表明正在发生需要注意的错误。常需要一个 ERROR 日志。ERROR 日志应该足够详细,以便诊断问题。记录明确的细节,包括相关的堆栈信息和软件正在执行的操作。
    6. FATAL:这属于“最后一搏”类型的日志信息。如果程序遇到非常严重的情况,必须立即退出,就可以在 FATAL 级别上记录关于问题原因的信息。应包括该程序状态的上下文内容,恢复或诊断相关数据的位置也应该被记录下来。
  • 原子日志:就是指在一行消息中包含所有相关的信息。原子日志与日志聚合器搭配使用更方便。
    • 不要假设日志会按照特定的顺序被看到,许多操作工具会重新排序,甚至弃用一些消息。
    • 不要依赖系统的时间戳来排序,系统时钟可能被重置或来自不同的主机,从而造成日志信息难以理解。
    • 避免在日志信息中使用折行,许多日志聚合器会把每一个新行当作一串单独的消息。要特别确保堆栈跟踪被记录在一条消息中,因为它们在输出时经常包含折行。
    • 如果日志信息不能以原子化的方式输出,可以在消息中放置唯一的 ID,这样日志信息就可以在后续的处理中被拼接起来。
  • 过度的日志记录会损害性能
    • 在写入日志前,要记得处理好字符串的拼接和格式化。用参数化的日志输入及异步附加器来保持快速记录日志。
    • 你会发现字符串的拼接效率非常低,在性能敏感的循环中甚至可能产生“毁灭性”的影响。
    • 默认的日志附加器通常在调用者的线程中操作,与调用 print 的方式相同。异步附加器在写日志信息时不会阻塞执行线程,这提高了性能,因为应用程序代码不需要等待日志被写入之后再执行。分批写入式附加器在日志被写入磁盘之前会在内存中缓冲日志信息,从而提高写入吞吐量。操作系统的分页缓存也可以通过充当缓冲器的方式来提高日志的吞吐量。虽然异步附加器和分批写入附加器提高了性能,但如果应用程序崩溃,它们也可能会丢失日志信息,因为并不是所有的日志都能保证被释放到磁盘上。
    • 请注意,改变日志的冗余度和配置可以消除竞争条件和 bug,因为它降低了应用程序的速度。如果你启用冗余的日志等级来调试一个问题,并发现一个 bug 消失了,日志等级的变化本身可能就是原因。
  • 日志信息不应该包括任何私人数据,如密码、安全令牌、信用卡号码或电子邮件地址。

系统监控

用各种系统指标来监控你的应用程序,看看它在做什么。系统指标相当于日志的数值,它们能反映出应用程序的行为。

  • 常见的三种系统指标类型:
    1. 计数器测量的是某个事件发生的次数,通过使用计数器获得缓存命中数和请求总数,你就可以计算出缓存命中率。计数器只在进程重新启动时增加数值或被重置为 0(它们是单向递增的)。
    2. 仪表盘是一个基于时间点的测量值,它既可以上升又可以下降。仪表盘揭示了诸如队列大小、堆栈长短或 map 中键值对的总数等统计数据。
    3. 直方图根据事件的大小幅度分成不同的范围。每一个范围都会有一个计数器,每当某事件的值落入其范围时,计数器就会递增。直方图通常用来测量请求所需的时间或数据有效负载的长度。
  • 系统性能通常以阈值百分比的形式来衡量,例如,从 0%到 99%,被称为 P99。一个所谓 P99 耗时 2 毫秒级别的系统需要 2 毫秒或更少的时间来响应它所收到的 99%的请求。百分数是由直方图得出的。为了减少需要跟踪的数据,一些系统会要求你去配置你真正关心的响应比例。如果一个系统默认跟踪 P95,但你有一个 P99 的服务等级目标(service level objective,SLO),确保可以修改相应的系统设置。
  • 应用程序的系统指标可以被汇总到一个集中式可视化系统中,如 Datadog、LogicMonitor 或 Prometheus。可视化是控制论中的一个概念,即通过观察一个系统的输出结果来确定其状态的难易程度。可视化系统可以在聚合指标之上提供面板和监控工具,这样可以更容易确定一个正在运行的应用程序的状态。面板向运维人员展示了系统中正在发生的事情,而监控工具则可以根据指标值触发警告。
  • 系统指标也被用来自动地进行系统扩容或缩容。系统资源的自动伸缩在提供动态资源分配的环境中很常见。
  • 为了跟踪 SLO,你可以使用可视化系统,同时也要利用自动伸缩的特性,所以你必须监控一切。使用标准的系统指标库来跟踪这些值,大多数应用程序框架都会提供这些系统指标。作为一名开发者,你的工作是确保重要的指标可以被可视化系统收集与呈现。
  • 使用标准的监控组件。使用框架自带的系统指标库是一个免费获得大量系统指标的好方法,因为你只需配置框架,然后把结果输出到你的可视化系统中。另外,你的代码也会更干净,因为所有指标的计算都发生在底层。
  • 测量一切:监测的性能开销很低,你应该广泛地使用这些监测数据。监测以下所有的数据结构、操作和行为:
    • 资源池:使用仪表盘来监测资源池的大小,要特别注意线程池和连接池。资源池使用过大表明系统此刻的响应很卡顿或无法跟上需求速度。
    • 缓存:计算高速缓存的命中数和失误数,两者比率的变化会影响应用程序的性能。
    • 数据结构:用仪表盘监测关键数据结构的大小,数据结构大小的异样表明正在发生一些奇怪的事情。
    • CPU 密集型操作:为 CPU 密集型操作计时。要特别注意数据的序列化操作,它的性能开销高得令人吃惊。一个简单的数据结构的 JSON-encode 往往是代码中开销最高的操作。
    • I/O 密集型操作:磁盘和网络 I/O 操作是缓慢和不可预知的,使用计时器来监测它们所需的时间。
    • 数据大小:监测你的代码所处理的数据的大小,跟踪远程过程调用(remote procedure call,RPC)有效载荷的大小变化。可以使用直方图(类似于计时器)去表现 I/O 产生的数据的大小,这样你就可以看到 P99 的性能指标了。大体量的数据对内存占用、I/O 速度和磁盘使用都有影响。
    • 异常和错误:计算异常、错误响应代码和不良输入的次数,监测错误的出现频率可以在出错时很容易触发警报。
    • 远程请求和响应:监测任何提交至你的应用程序的请求,高到不正常或低到不正常的请求数都是信号,表明有什么地方不对劲儿。用户希望你的系统能够快速响应,所以你需要监测系统延迟的程度。对所有的响应进行计时,以便你知道你的系统什么时候会变慢。
    • 花点儿时间了解你的系统指标库是如何工作的。某个类库如何计算一个指标并不总是显而易见的,因为许多类库会对测量进行抽样。抽样可以保持快速的性能,减少磁盘和内存的使用,但它也会使测量的准确性降低。

跟踪器

  • 对上游 API 的一次调用可能会导致对下游的数百次不同服务的 RPC 调用。分布式调用跟踪将所有这些下游调用连接成一个图。分布式跟踪对于调试错误、监测性能、理解依赖关系和分析系统成本(哪些 API 的服务成本最高、哪些消费者线程成本最高等)都很有用。

配置相关注意事项

  • 应用程序和服务应该暴露出配置信息,并允许开发人员或网站稳定性工程师(site reliability engineers,SRE)配置运行时的行为。应用配置的最佳实践将使你的代码更容易运行。不要太有创意,要使用标准的配置格式,提供合理的默认值,校验配置的输入值,并尽可能地避免动态配置。
  • 配置的表达方式:
    • 普通的、对人友好的格式的文件,如 INI、JSON 或 YAML;
    • 环境变量;
    • 命令行参数;
    • 定制的领域特定语言(DSL);
    • 应用程序所使用的语言。
  • 理想状态应该是单一标准格式的静态配置文件。
  • 动态配置通常存储在一个专门的配置服务中,当某些值发生变化时,配置服务应该被轮询或主动推送。或者,动态配置是通过定期检查本地配置文件的更新来刷新自己的。有一些常见的情况确实需要动态配置,如日志的分级经常是动态配置,当有奇怪的事情发生时,运维人员可以将日志级别改为更高的配置,如 DEBUG。当某个进程出现奇怪的行为时,重新启动这个进程可能会改变你要观察的那个奇怪行为。改动某个正在运行的进程的日志级别,可以让你在不重新启动的情况下对它的行为一探究竟。
  • 在程序启动时立即记录所有(非秘密的)配置,以显示应用程序正在获取哪些值。
  • 始终在加载配置的值时对其进行校验。只做一次校验,并且尽可能早地进行(就在配置加载完之后)。确保配置的值都被设置成了适当的类型,例如端口应该为整数,并检查这些值是否有逻辑意义,比如检查边界、字符串长度、有效的枚举值等。−200 是一个整数,但不是一个有效的端口。利用拥有强大数据类型的配置系统来表现可接受的配置值。
  • 如果用户不得不配置大量的参数,你的系统将很难运行起来。提供良好的默认值,这样你的应用程序对大多数用户来说开箱即用。
  • 应用程序配置很容易变得难以管理,特别是不支持嵌套语法的键值格式。可以使用像 YAML 这样允许嵌套的标准格式。将相关属性分组,这样就更容易组织和维护配置信息。
  • 为了保证配置变化的安全,配置应该被版本控制、评审、测试、构建和发布。
  • 干净、清爽的配置对其他人来说更容易理解和改变。删除不使用的配置,使用标准的格式和间距,不要盲目地从其他文件中复制配置(一个被称为船货崇拜的例子:在没有真正理解它们的作用或原理的情况下就复制东西)。当你处于快速迭代的阶段,很难维护整洁的配置,但错误的配置会导致生产环境的被迫中断。
  • 避免在特定的某台计算机上手动编辑配置。配置的一次性修改会在随后的部署中被覆盖,不清楚是谁做的修改,而且配置相似的计算机最终会出现分歧。如果你在生产事故中手动编辑配置,请确保所做的更改随后会被提交到真正的源(如 VCS)。

工具集

  • 可维护的系统通常会带有可以帮助运维人员去运行应用程序的工具。运维人员可能需要批量地加载数据、运行恢复、重置数据库状态、触发集群选举,或将分区分配从一台计算机转到另一台计算机。系统应该配备工具,帮助运维人员处理常见的操作。
  • 编写工具是协作性的。在某些情况下,你将被期望编写和提供运维工具。拥有强大的 SRE 团队的组织也可能为你的系统编写工具。不管怎么样,与你的运维团队合作,了解他们需要什么。
  • SRE 通常会喜欢基于命令行界面(command line interface,CLI)的工具和自描述的 API,因为它们很容易脚本化,脚本化的工具很容易实现自动化。如果你打算构建一个基于用户界面的工具,那就把逻辑抽象成一个共享库或服务,这样基于 CLI 的工具也可以使用。把你的系统工具当作代码一样对待:遵循干净整洁的编码规范,并进行严格的测试。
  • 拥有统一管理控制台的公司会期望所有的工具都能与之集成。如果你的公司已经拥有基于 CLI 的工具,问问将你的工具与之整合是否有价值。每个人都习惯于现有的工具界面,与它们集成将使你的工具更容易使用。

行为准则

扩展阅读

第 5 章 依赖管理

在这一章中,我们将介绍依赖管理的基础知识,并谈论几乎每个工程师的“噩梦”:相依性“地狱”。

依赖管理基础知识

  • 相依性是指你的代码所依赖的代码。在编译、测试或运行期间,所有需要依赖关系的时间周期被称为依赖范围。
  • 一个好的版本管理方案,其版本都具有以下特点:
    1. 唯一性(unique):版本不应该被重复使用。构件会被分发、缓存,并被自动化工作流拉取。永远不要在现有版本下重新发布更改的代码。
    2. 可比性(comparable):版本应该帮助人们和工具对版本的优先顺序进行推断。当一个构建依赖于同一构件的多个版本时,可以使用优先顺序来解决冲突。
    3. 信息性(informative):版本信息区分了预先发布的代码和已发布的代码,将构建流水号与构件相关联,并设置了稳定性和兼容性的合理预期。
  • 语义化版本:主版本号.次版本号.补丁版本号
    • 主版本号为 0 被认为是“预发布”,是为了快速迭代,不做任何兼容性保证。
    • 主版本号从 1 开始后,一个项目应该保证以下内容:
      • 补丁版本号是递增的,用于修复 bug,并且可以向下兼容。
      • 对于向下兼容的特性,次版本号是递增的。
      • 对于无法向下兼容的变化,主版本号会被递增。
    • 语义版本管理还通过在补丁版本号后添加一个“-”来定义预发布版本。
      • 小数点分隔的字母和数字的序列被用作预发布版本的标识符(2.13.7-alpha.2)。
      • 许多项目使用候选发布版(release candidate,RC)构建。早期采用者可以在正式版本发布之前发现 RC 中的错误。RC 预发布版本有递增的标识符,如 3.0.0-rc.1。然后,最终的 RC 被提升为正式发布版,重新发布的版本没有 RC 的后缀。所有的预发布版本都会被最终版本(在我们的例子中是 3.0.0)所取代。
      • 构建流水号被附加在版本号和预发布元数据之后,如 2.13.7-alpha.2+1942。包含构建流水号有助于开发者和工具找到任何版本被编译时的构建日志。
    • 语义版本管理的方案还允许使用通配符来标记版本范围(2.13.*)。由于语义版本管理承诺跨小版本和补丁版本的兼容性,即使有更新版本被自动拉取,比如修复 bug 和新特性,此时构建工作也应该继续进行。
  • 软件包管理或构建文件都揭示了项目的直接依赖关系,但直接依赖关系只是构建或打包系统实际使用的子集。依赖关系通常也依赖于其他类库,这就造成了依赖传递。依赖关系报告可以展示出完全解决的依赖关系树(或依赖关系图)。依赖关系树展示了构建系统在编译项目时实际使用的依赖关系。该报告会说明当前依赖的深度。了解依赖传递是依赖管理的一个关键部分。增加一个依赖关系看起来似乎是一个小变化,但如果相关类库依赖于其他 100 个类库,你的代码现在就依赖于 101 个类库。任何依赖关系的变化都会影响你的程序。确保你知道如何获得准确的依赖关系树的信息,以便你能解决依赖冲突。

相依性地狱

  • 比较常见的相依性地狱的罪魁祸首是循环依赖、钻石依赖和版本冲突。

    • 钻石依赖:一个项目下依赖一个库的不同版本。

    • 循环依赖:一个库间接性地依赖它自己。

避免相依性地狱

是否应当往一个项目中添加依赖项的参考标准:

  1. 你真的需要这些特性吗?
  2. 依赖关系的维护情况如何?
  3. 如果出了问题,你修复这个依赖有多容易?
  4. 依赖项的成熟度如何?
  5. 引用依赖后向下兼容的变化频率如何?
  6. 你自己、你的团队和你的组织对该依赖的理解程度如何?
  7. 自己写代码有多容易?
  8. 代码采用什么样的许可协议?
  9. 在依赖中,你使用的代码与你不使用的代码的比例是多少?

添加依赖性的优化方式:

  1. 依赖相关的代码也可以被复制、被制作成供应商代码或被遮蔽。将代码复制到你的项目中,用依赖管理自动化换取更多的隔离(稳定性)。你将能够准确地挑选你使用的代码,但你必须管理代码的复制。
  2. 遮蔽依赖也能达到隔离依赖项的目的。遮蔽依赖会自动将一个依赖关系重新定位到不同的命名空间,以避免冲突,比如将 some.package.space 变成 shaded.some.package.space。这是一种可以防止库将其依赖关系强加给应用程序的友好方式。
  3. 建议只有在你创建一个被广泛使用的依赖项时,才需要对依赖关系进行遮蔽处理,因为这些依赖项可能会产生冲突。
  4. 将你使用的所有类库显式声明为依赖项。不要使用来自横向依赖的方法和类,即使它看起来很有效。
  5. 不要只靠 IDE 来进行依赖管理,在构建文件中明确声明你的依赖项。
  6. 明确设定每个依赖项的版本号,这种做法称为版本指定(version pinning)。未被指定的那些将由构建系统或软件包管理系统为你指定版本。把你的命运交给构建系统是个坏主意,当依赖版本在连续构建过程中发生变化时,你的代码就会不稳定。
  7. 对每个依赖项使用尽可能精确的依赖范围。依赖范围有一个层次:编译时的依赖项在运行时使用,但运行时的依赖项不会用于编译代码,只会用于运行代码。测试依赖项只会在测试执行时被拉取。
  8. 不要引入循环依赖。循环依赖会导致构建系统的奇怪行为和部署顺序问题。构建系统会出现构建先正常进行,然后突然失败,应用程序会出现难以捉摸的零星 bug。使用构建工具保护自己。许多构建系统都有内置的循环依赖检测的特性,当检测到循环依赖时就会提醒你。如果你的构建系统不能防止循环依赖,通常有插件可以提供帮助。

行为准则

扩展阅读

  • SemVer 的官方主页:关于语义化版本管理的紧凑而可读的规范。
  • 帕累托法则:又称 80/20 规则,是一种经验法则,指出大约 80%的结果来自约 20%的原因。

第 6 章 测试

本章将教你如何有效地进行测试。我们将讨论测试目的、不同的测试类型、不同的测试工具、如何进行负责任的测试,以及如何处理测试中的不确定性。

测试的多种用途

  • 测试可以检查代码是否正常工作。

  • 保护代码不会被将来那些无意中的修改所影响、鼓励清爽的代码、强迫开发者试用他们自己的 API、记录组件之间如何交互,以及将其作为一个实验的“游乐场”。

  • 最重要的是,测试本身就可以验证软件的行为是否符合预期。

  • 迫使开发人员思考他们程序的接口和实现过程。

  • 迫使开发人员分别通过改进关注点分离和降低紧耦合的方式来确保他们的代码拥有良好的构造。

  • 测试其实是另一种形式的文档,它说明了代码是如何被交互的。它是一名有经验的程序员开始阅读并了解一个新的代码库的首选入口。

  • 测试套件是一个伟大的游乐场。开发人员通过调试器来运行测试,并进行逐行调试。当发现 bug 或出现关于软件行为的问题时,可以通过添加新的测试来了解它们。

测试类型

  • 比较常见的几种类型——单元测试、集成测试、系统测试、性能测试和验收测试。
    1. 单元测试:通常指某个单一的方法或行为。单元测试应该快速、简短且集中。
    2. 集成测试:验证多个组件集成在一起之后是否还能正常工作。
    3. 系统测试:验证整个系统的整体运行情况。端到端(end-to- end,e2e)的工作流程是为了模拟在预生产环境中系统与真实用户的互动。
    4. 性能测试:(如负载测试和压力测试)监控不同配置下的系统性能。负载测试可以监控不同负载水平下的性能:例如,系统的性能在 10 个、100 个或 1000 个用户同时访问时究竟如何。压力测试将系统负载推高到崩溃的程度。压力测试可暴露系统的负载能力究竟有多大,以及在过度负载下会发生什么状况。这些测试对于容量规划和服务等级目标定义非常有用。
    5. 验收测试:由客户或其代理人进行的,以验证交付的软件是否符合验收标准的测试。

测试工具

  • 测试工具分为几类:
    • 测试用例的编写工具:如模拟库,可以帮助你编写干净和高效的测试。
    • 测试框架:测试框架通过模拟测试的生命周期,从 setup 到 teardown,帮助测试的运行;测试框架还可以保存测试结果,与构建系统集成,并提供其他的辅助功能。
    • 代码质量工具:用来分析代码覆盖率和代码复杂性,通过静态分析来寻找 bug,并检查代码风格错误。
  • 在你能证明引入相关工具的复杂性带来的利弊之前,请避免使用外部工具,即使新引入的工具利大于弊,也要确保你的团队可以接受它。
  • 对那些未能通过质量检查的代码库要有务实精神。

自己动手编写测试

  • 许多公司都有正式的质量保证(QA)团队,尽管其职责各不相同,但都包括以下内容。

    • 编写黑盒或白盒测试。

    • 编写性能测试。

    • 进行集成测试、用户验收测试或系统测试。

    • 提供和维护测试工具。

    • 维护测试环境和基础设施。

    • 定义正式的测试认证和发布流程。

  • 如果你所在的公司有正式的 QA 团队,请了解他们负责什么,以及如何与他们接触。如果他们被嵌入你的团队中,他们很可能会参加 Scrum 和冲刺计划会议(关于敏捷开发的更多信息,请参见第 12 章)。如果他们是一个集中的组织,想要得到他们的帮助可能需要填写任务票或提交一些正式的请求。

  • 在测试中要采用良好的编程实践。记录测试如何生效,如何运行,以及为什么写这些测试。避免硬编码的值,不要重复代码。使用设计的最佳实践来保持关注点的分离,并保持测试的内聚性和解耦性。

  • 专注于测试基本功能而不是实现细节,这有助于代码库的重构,因为测试代码在重构后仍然可以运行。

  • 将测试的依赖项与常规代码的依赖项分开。如果一项测试需要某个类库来运行,不要强迫整个代码库都依赖这个类库。

  • 不要淹没在编写测试的这项工作中,这样很容易跟丢那些值得投入精力去编写测试的地方。要编写那些在测试失败的时候有意义的测试,不要为了提高代码覆盖率而去提高代码覆盖率。测试数据库包装器、第三方类库或基本的变量赋值,即使它们能提高覆盖率指标,也是毫无价值的。要专注于那些对代码风险有最大影响的测试。

  • 不要为自动生成的代码手动编写测试,如 Web 框架、脚手架或 OpenAPI 客户端。

  • 把精力集中在最高价值的测试上。测试需要时间来编写和维护。专注于高价值的测试,可以产生最大的收益。

  • 测试可以降低代码风险,因为测试越多,失败发生的可能性就越低。首先应该关注代码中的高风险的区域;而那些低风险或被废弃的代码,诚如其概念所言,并不值得测试。

测试中的确定性

非确定性测试是一个困扰许多项目的问题。重要的是需要了解为什么非确定性测试是糟糕的、如何去修复它们,以及如何避免编写它们。

  • 间歇性失败的测试应该被禁用或立即修复。
  • 通过下面几种手段可以避免出现非确定性,比如使用具有确定性的时间类型和随机数并在测试后进行清理,以及避免网络调用。
    • 种子随机数生成器:可用一个常数作为随机数生成器的种子,迫使它每次运行时都能确定地生成相同的序列。
    • 不要在单元测试中调用远程系统:你可以通过使用模拟库或重构代码来剔除单元测试中的远程系统调用,从而使远程系统仅在集成测试中被需要。
    • 采用注入式时间戳:注入式时间戳将让你使用模拟来精确控制测试中的时间流逝。
    • 避免使用休眠和超时:如果你发现自己想在测试中设置休眠或超时,看看你是否能重组测试步骤,进而确认一切能否以确定的方式来执行。如果不能,那也没关系,但要做出真诚的努力。在测试并发或异步的代码时,并不总能提供确定性。
    • 记得关闭网络套接字和文件句柄:对于在局部使用的资源可以利用标准的资源管理技巧,如 try- with-resource 或 block。在测试中共享的资源应使用 setup 和 teardown 方法进行关闭。
    • 绑定到 0 端口:将网络套接字都绑定到 0 端口,这将使操作系统需要自动去选择一个开放的端口。测试可以检索被选中的端口,并在该项测试的剩余部分使用这个端口。
    • 生成唯一的文件路径和数据库位置:应该动态地生成唯一的文件名、目录路径以及数据库或表名。
    • 隔离并清理剩余的测试状态:无论你的测试是否通过,你都必须重置状态,不要让失败的测试“留下残渣”。使用 setup 和 teardown 方法来删除测试文件,清理数据库,并在每次执行之前重置内存中的测试状态。在测试套件运行的间隙重建环境,这样可以消除测试机的遗留状态。像容器或虚拟化这样的工具可以很容易地废弃整个环境并开启一个新的环境。然而,废弃和开启新的虚拟机要比运行 setup 和 teardown 方法慢,所以这样的工具最好用于大型的测试分组。
    • 不要依赖测试顺序:使用 setup 和 teardown 方法,在测试之间共享逻辑。在 setup 方法中为每项测试提供数据,并在 teardown 中清理数据。在每次运行之间重置状态,将防止测试在状态发生突变时相互破坏。

行为准则

扩展阅读

第 7 章 代码评审

本章将解释为什么代码评审是有用的,以及如何成为一名优秀的被评审者和评审者。 我们将告诉你如何让你的代码得到他人的评审,以及当你得到反馈时如何回应。然后,我们将翻转角色,告诉你如何成为一名好的评审者。

我们为什么需要评审代码?

代码评审对你的团队来说是一种教学和学习工具,你可以从别人评审你的代码给予的反馈中学习,评审者会指出那些你可能不知道的有用的类库和编码实践。你也可以阅读更资深的队友的代码评审请求,以了解代码库,并学习如何编写生产级别的代码。代码评审也是了解你的团队的编码风格的一种简单方法。

评审整个代码库的变更可以确保不止一个人熟悉生产环境中代码的每一行,对代码库的共同理解有助于团队更有凝聚力地扩展代码。让别人知道你在改什么,意味着一旦出现了问题,你不是团队中唯一可以仰仗的人。On-Call 工程师会追加什么时候哪些代码被修改了的背景信息,这种共享的知识意味着你可以在休假时不必担心还要必须对你的代码做支持。

代码评审可以作为实现决策的档案,有旧的代码评审作为参考,可以为开发人员提供一份书面的历史记录。

安全性和合规性政策通常规定了代码评审作为一项防范措施来防止任何一名开发人员恶意修改代码库。

只有当所有的参与者能够在一个“高度信任”的环境中工作时,代码评审的这些好处才会适用,在这个环境中,评审者有意提供有用的反馈,被评审者也愿意接受意见。

执行不力的代码评审会成为一种有害的阻碍。轻率的反馈不提供任何价值,还会拖慢开发人员的速度。缓慢的周转时间会使代码的变化停滞不前。如果没有正确的评审文化,开发人员可能会陷入反复拉锯扯皮的分歧中,这可能会毁掉一个团队。评审不是一个证明你有多聪明的机会,也不是一个橡皮图章式的官僚主义障碍。

当你的代码被评审时

  • 代码修改由准备、提交、评审、最后批准和合并这几个环节组成。
  • 不要执着于那些你提交评审的代码,要期待它在评审过程中发生变化,有时甚至是重大的变化。
  • 记得附加一个标题和描述,添加评审者,并链接到你的评审请求所要解决的问题。 标题和描述与提交信息不完全一样,评审请求的标题和描述应该包括相应修改需要如何被测试的附加背景,与其他资源的链接,以及关于未解决的问题或实现细节的标注。
  • 代码修改的草案是一种思考和提出相应修改的很棒的方式,这种方式不需要投入那么多时间来编写测试、打磨代码和添加文档。你可以通过提交评审草案来检查你正在做的事情:一项非正式的评审请求,旨在从队友那里获得快速和低成本的反馈,这可大大降低你在错误道路上走得太远的风险。
  • 为了避免混淆,要清楚代码评审的时候是草案还是正在进行的工作(work-in-progress,WIP)。许多团队都有关于草案的惯例,通常会在代码评审的标题前添加“DRAFT”或“WIP”作为区分。 一些代码评审平台对此有内置支持,例如,GitHub 有“草案拉动请求”。一旦你的草案看起来像在正确的轨道上,你就可以通过完成代码实现、测试和文档,并增加润色,将其从“草案”的状态中迁移出来。
  • 通过提交代码评审来触发执行测试的方式是一种浪费。
  • 在做大体量的修改时,要进行代码层面上的预排会议(walk- through)。预排会议是一种面对面的会议,开发人员在会上共享他们的屏幕,并引导队友了解正在进行的修改内容。预排会议是启发想法和让你的团队适应代码修改的好方法。提前分发相关的设计文档和代码,并要求你的团队成员在预排会议之前简单地浏览。给他们足够的时间,不要把预排会议安排在一个小时之后。在预排会议开始的时候,要介绍有关修改的背景,可能需要快速回顾一下设计文档。然后,分享你的屏幕,并在你的 IDE 中浏览代码。最好的预排方法是通过浏览代码的运行步骤来完成,从最开始的页面加载、API 调用或应用程序启动,一直到执行结束。解释任何新模型或抽象背后的主要概念、它们是如何被使用的,以及它们是如何与整个应用程序进行整合的。不要试图让你的队友在预排会议中实际地进行代码评审,参加者应该把他们的评论留到未来真正的代码评审环节。预排会议的目的是帮助你的团队理解为什么要提出修改,并给他们一个良好的心理模型,以便他们可以自行去进行详细的代码评审。
  • 从你的代码上得到的那些批评性的评论可能让你很难接受。切记应该保持一些情感上的距离——这些评审意见是针对代码的,而不是针对你个人的,而且这甚至都不算是你的代码,将来整个团队会拥有这些代码。
  • 每个人的沟通方式各有不同,但不应该容忍粗鲁。请记住,一个人的“简短且命中要害”可能对于其他人意味着“粗暴无礼”。应该允许评审者怀疑,但如果他们的评论似乎偏离了中心或粗鲁无礼,请明确地告知他们。如果讨论总拖拖拉拉或让人感觉“哪里不太对劲”,那么试着去面对面地交流,这样可以扫清沟通中的障碍并找到解决办法。如果你觉得不舒服,可以和你的管理者谈谈。如果你不同意某项建议,试着解决分歧。首先审视你自己的反应,你本能地保护你的代码只是因为你编写了它们,还是因为你的方式事实上更好?清楚地解释你的观点,如果你们还是不能达成一致,咨询一下你的管理者下一步该怎么做。 团队处理代码评审冲突的方式各不相同,有的服从提交者,有的服从技术负责人,还有的服从小组的法定人数。应该遵循团队惯例。
  • 不要羞于要求别人评审你的代码。评审者经常被代码评审和任务票通知淹没,所以在高速推进的项目中可能会漏掉某些评审请求。如果你没有得到任何反馈,请向团队报告(但不要催促)。当你收到评论时,要有所回应。 你不希望你的代码评审要拖上几个星期。每个人的记忆都会“消失”,你回应得越快,你得到他人回应的速度就越快。
  • 在你收到批准后请及时合并你的修改。
  • 好的评审者将评审请求分成几个阶段。
    1. 首先分流评审请求,以确定其紧急度和复杂度,并预留出时间来评审代码的修改。
    2. 开始评审时,阅读代码并提出问题,以了解变化的背景。
    3. 然后,给出反馈意见,在评审工作中推动决断。将这一流程与一些最佳实践相结合,将大大改善你在评审他人代码时的表现。
  • 当你收到评审请求的通知时,你作为评审者的工作就开始了。首先要对评审请求进行分流。有些代码修改很关键,需要立即评审。然而,大多数的修改是不那么紧急的。如果紧急度不明确,请询问提交者。修改的规模和复杂度也需要考虑在内。如果一项修改是小且简单明了的,快速的评审将有助于你的队友扫清前进的路障。大型修改的评审则需要更多的时间。高速推进的团队会产生大量的代码评审需求。你不需要评审每一项代码修改,要专注于那些你可以从中学习的修改和你熟悉的代码。
  • 在你的日历上划出代码评审时间。 预定的评审时间会使你很容易继续你的其他任务,因为你知道你以后会有集中的时间段进行代码评审。这也会使你的评审保持高质量——当你有专门的时间时,你就不会对需要切换回其他任务而感到有那么大的压力。大型的代码评审可能需要进行额外的计划。如果你收到的评审请求可能需要花费一两个小时以上的时间,请创建一张任务票来跟踪代码评审本身。与你的管理者合作,在冲刺计划中分配专门的时间。
  • 不要一上来就以提交评论的方式开始你的评审工作,首先要阅读并提出问题。 如果评审者真的花时间去理解拟议的代码修改,那么代码评审是较有价值的事情。争取理解为什么要进行这项修改,代码过去的表现是什么样的,以及改变后的代码表现是怎么样的。考虑 API 设计、数据结构和其他关键决策的长期影响。了解修改的动机将解释具体实现的决策,你可能会发现某些修改甚至是不需要的。比较修改前后的代码也会帮助你检查正确性,并启发其他的实现想法。
  • 你需要对代码修改的正确性、可实施性、可维护性、可读性和安全性提供反馈,指出那些违反代码风格手册、难以阅读或令人困惑的代码,阅读测试用例并寻找 bug 以验证代码的正确性。问问你自己,你将如何实现这些改动,以引发关于替代方案的想法,并权衡各个方案的利弊。如果公共的 API 被改变了,想想这可能会影响到兼容性和计划中的展开。考虑未来的程序员可能会误用或误解这段代码的使用方式,以及如何修改代码以防止这种情况发生。思考有哪些类库和服务可以帮助这项修改。建议采用第 11 章中讨论的模式来保持代码的可维护性。寻找 OWASP 十大违规行为,如 SQL 注入攻击、敏感数据泄露和跨站脚本攻击的漏洞。写评论时不要过于简短——请按照你们坐在一起评审代码时的说话方式来写评论。评论应该是有礼貌的,并且包括“什么”和“为什么”。
  • 如果你从阅读代码中学到了一些新的东西,请明确地传达给作者。如果一次重构清理了代码中的问题区域,或者新的测试看起来会降低未来修改的风险,那么请用积极的、鼓励性的评论来嘉许这些内容。 即使是一项令你讨厌的修改,你也可以对它说些好话——如果没有别的原因,就承认它的意图和努力。
  • 并非所有的评审意见都有相同的重要性。重大问题需要比中性的建议和肤浅的挑剔投入更多的关注。不要回避文体方面的反馈,但要清楚地表明你是在吹毛求疵。在评论前添加一个“Nit”作为前缀是惯例。 如果你发现自己经常对代码风格挑挑拣拣,请询问该项目是否设置了足够的代码检查工具。理想情况下,应该是工具为你做这项工作。如果你发现你的评审意见中大多是挑剔的内容,很少有实质性的评论,那就放慢速度,做更深入的阅读。指出有用的代码美观的问题是评审的一部分,但它不是主要目标。
  • 把那些对你来说更好但并不需要批准的建议指出来,在反馈前加上“可选”(optional)、“接受或不接受”(take it or leave it)或“非必须”(nonblocking)的字样。 将你提出的建议与你真正希望看到的修改区分开来,否则,提交者就不一定清楚了。
  • 要抵制那种用草率批准的方式快速给评审盖上橡皮图章的诱惑,橡皮图章式的评审是有害的。团队成员会认为你已经知道了这项修改是什么、为什么要这么改,你可能会在以后被追究责任。提交者会认为你已经浏览并批准了他们的工作。如果你不能充分地确定评审的优先次序,那就根本不要评审相应代码修改。给某项请求盖上橡皮图章的诱惑可能是一个信号,表明代码的变化对一个单独的请求来说太大了。不要害怕要求你的团队成员将大型的代码评审分割成较小的部分分批进行。对开发者来说,很容易就会产生一项数千行的改动。期望一次性就能充分评审一项巨大的代码改动是不合理的。如果你觉得代码预排会议可能更有效率,你也可以要求这样做。
  • 代码评审通常在一个专门的 UI 中处理,比如 GitHub 中的拉取请求界面。不要忘记,代码评审本身也只是代码而已。你仍然可以迁出或下载那些拟议的修改,并在本地处理它们。在本地迁出代码可以让你在你的 IDE 中检查、建议那些拟议的修改。大型的改动在网页界面中很难浏览,集成开发环境和桌面的评审工具可以让你更容易地浏览这些变更。本地代码也是可以运行的,你可以创建你自己的测试来验证事情是否如预期般进行。调试器可以被附加到正在运行的代码上,这样你就可以更好地了解事情是如何表现的。你甚至可以触发失败的场景,以更好地说明你的评审意见。
  • 评审者经常会忽略测试代码,特别是当变更比较大的时候。测试代码应该像代码的其他部分一样被评审。通过阅读测试代码来开始评审工作通常是有用的,它们说明了代码是如何被使用的,以及预期会发生什么。一定要检查测试代码的可维护性和清洁度。寻找糟糕的测试模式:依赖执行顺序、缺乏隔离和远程系统调用。
  • 不要成为促成“夭折”的原因,要帮助提交者评审以迅速批准他们的代码。不要坚持要求完美,不要扩大修改的范围,要清楚地描述哪些评审意见是关键的,不要让分歧发酵。坚持质量,但不要成为不可逾越的障碍。
  • 尊重正在进行的修改的范围。在你阅读的过程中,你会发现改进相邻代码的方法,并产生一些关于新特性的想法,不要坚持将这些修改作为现有评审的一部分来进行。 另开一张任务票来改进代码,把工作留到以后。确定严格的范围将提高速度并保持增量更改。你可以通过将本项评审标记为“尚需修改”(request changes)或“批准”(approved)来做出决断。 如果你留下了很多评审意见,撰写评审摘要会很有帮助。如果你要求修改,请明确说明需要哪些修改才能使你批准。
  • 如果对代码修改有重大分歧,而你和作者又不能解决分歧的话,请主动提出把这个问题移交给其他专家,他们可以帮助解决相关分歧。

行为准则

扩展阅读

第 8 章 软件交付

本章将介绍向客户交付软件所涉及的不同阶段、源代码控制的分支策略(这会影响软件的发布方式)以及当前的最佳实践。

软件交付流程

  1. 软件首先必须被构建成软件包。软件包应该是不可变的,并且被标记了版本。
  2. 然后,软件包必须被发布。发行说明和变更日志都会被更新,同时软件包会被发行到一个集中的存储库。
  3. 已发行的发布级的构件必须被部署到预生产和生产环境中。部署的软件还不能被用户访问——它只是被安装了而已。
  4. 一旦部署,软件就会通过将用户转移到新的软件上而进行展开。一旦展开完成,就意味着完成了交付。
  5. 交付过程是更大的产品开发周期中的一部分。展开阶段之后,收集反馈意见,发现 bug 并收集新的产品需求。特性开发重新开始,并最终启动下一轮的构建流程。

分支策略

分支策略决定了代码变更的提交位置以及发布代码的维护方式。正确的分支策略将使软件交付变得简单和可预测,而错误的策略将使交付变成与流程本身的缠斗。

基于主分支的开发模式

只有当各分支可以快速合并到主分支时,基于主分支的开发模式的效果才是最好的,如果不是在几小时内,也应该在几天内合并到主分支,并且不在开发人员之间共享。 频繁地合并被称为持续集成(CI)。CI 可降低风险,因为代码上的变化会迅速传递给所有的开发人员,使他们彼此之间不太可能有很大的分歧。让开发人员的代码库保持同步,可以防止潜在的最后一分钟的集成障碍,并尽早暴露出错误和不兼容的情况。作为一种代价,主分支中的 bug 会拖累所有的开发者。为了防止代码破损,在一个分支被合并到主分支上之前,要运行快速的自动化测试来验证其是否可以通过。团队通常有明确的流程来应对破损的主分支,一般的期望是主分支应该总是可以发布的,而且发布往往相当频繁。

Gitflow 基于特性分支的开发模式

Gitflow 使用开发分支、热修复分支和发布分支。开发分支被用作主分支,特性分支与之合并和变基。

  1. 在准备发布时,发布分支会从开发分支中被切分出来。
  2. 在版本已稳定的期间内,开发工作在特性分支上继续进行。
  3. 发行版稳定后会合并到主分支。
  4. 主分支总被认为是可以随时部署到生产环境的,因为它只包含稳定的版本。如果主分支是不稳定的,因为它包含了严重的 bug,则会立即采用热修复的方式来解决这些 bug,而不是等待正常的发布周期。
  5. 热修复分支用于修复在主分支上发现的错误。热修复分支会被拉取到开发分支之中,因此特性分支也可以将其拉取进来。

理解并遵循团队的分支策略。分支策略定义了代码的变动何时被推出,设置了测试预期,定义了你的错误修复选项,并确定了你的代码变动必须被移植到的版本数量。许多公司开发了内部工具来帮助管理他们的 VCS 工作流,这些脚本会自动为你进行分支、合并和标记。

除非你真的需要那种长期存续的特性分支,否则请坚持使用基于主分支的分支策略。管理特性分支会变得很复杂。事实上,德里森已经修改了他最初关于 Gitflow 的博文,不再鼓励将 Gitflow 用于可持续集成和交付的软件。

构建环节

  • 构建软件包需要很多步骤:解决和连接依赖项、运行 linter、编译、测试,最后是打包软件。

  • 软件包的内容和结构各不相同。软件包可以包含二进制包或源代码、依赖关系、配置、发行说明、文档、媒体文件、许可证、校验和,甚至是虚拟机镜像。

  • 打包决定了什么软件会被发布。糟糕的打包会使软件难以部署和调试。为了避免出现令人头痛的问题,应该总是对软件包进行版本管理,并按资源类型分割软件包。

  • 软件包也应该被纳入版本管理,并且被分配唯一的标识符。唯一的标识符帮助运维人员和开发人员将运行中的应用程序与特定的源代码、特性集合以及文档联系起来。如果没有版本号,你就不知道这个包会有什么样的表现。如果你不确定要使用哪种版本策略,语义化版本是一个安全的选择。大多数软件包都遵循某种形式的语义版本管理。

  • 不同的资源应该被分开单独地打包,这样它们就可以被修改而不需要重新构建整个软件包。分开打包让每种类型资源都有自己的发布周期,可以独立向前和向后滚动。

发布环节

  • 你应该确保你的代码在测试环境中运行良好,跟踪发布时间表,理解那些可用的选项,并为你的应用程序选择正确的配置。
  • 软件包通常会被发布到一个存储资源库中,或者简单地标记一下并存放在像 Git 这样的 VCS 中。虽然这两种做法都可以,但我们鼓励你把发布包发布到一个专门的存储资源库。存储资源库会保证已发布的构件(另一种说法是可部署的包)可用于部署。存储资源库也可以当作档案库——以往发布的构件可用于调试、回滚和分阶段部署。包的内容和元数据都有索引,可以浏览。支持搜索可以使其很容易地找到依赖关系、版本信息和发布日期——这些都是在排除故障时的宝贵信息。存储资源库也是为了满足部署的需求而建立的,可以应对成千上万的用户在同时下载某一个新版本。
  • 一旦发布了,就永远不要改变或覆盖这个已发布的包。
  • 你应该尽可能频繁地发布。较长的发布周期给人一种错误的安全感:两次发布之间的漫长周期感觉像是有充足的时间来测试变化。具有自动发布和部署特性的软件应该可以在每次提交时都触发发布流程。对于较难部署的大型软件,要平衡发布的频率与发布、部署、维护的成本以及用户的覆盖率。
  • 无论采用哪种发布方式,都要明确发布时间表。公开时间表并在新版本发布时通知用户。
  • 变更日志列出了在该版本中被修复或提交的每一张任务票的内容。为了自动生成变更日志的内容,可以跟踪提交信息或问题票中的标签。发行说明是对一个版本中包含的新特性和修复的 bug 的汇总。变更日志主要会被支持团队和开发团队阅读,而发行说明是专门给用户看的。

部署环节

  • 使用脚本而不是手动步骤来部署软件。
  • 高度发展的自动化催生了持续交付。通过持续交付,人力被完全从部署环节中移除。打包、测试、发布、部署,甚至展开环节都是自动化的。部署可以根据需要的频率来进行——每天、每小时或持续不断。通过持续交付,团队可以快速地向用户交付特性,并从他们那里获得反馈。成功的持续交付需要对自动化测试(、自动化工具以及能够吸收快速变化的客户群体做出承诺。
  • 使部署原子化的最简单方法之一是在与旧版本不同的位置上安装软件,不要覆盖任何东西。一旦软件包被安装了,一个快捷方式或软链接就可以被原子化地翻转。在新的位置安装软件包还有一个好处,那就是回滚将变得更加容易——只需再次指向旧的版本。在某些情况下,同一软件的不同版本可以在同一台计算机上同时运行。
  • 构建可独立部署的应用程序。不依赖顺序部署的软件必须向后和向前兼容。当依赖关系不可避免时,在部署时暂时先封住你的变化内容,并在之后以特定的顺序解开它们,这比执行按顺序的部署更快捷、更简单。

展开环节

  • 在展开环节有许多策略:特性开关、熔断器、“摸黑启动”、“金丝雀部署”和“蓝绿部署”。
    • 特性开关:允许你控制收到一个代码路径的用户与收到另一个代码路径的用户的比例。
    • 熔断器:用来防止性能下降。如果超过了延迟的阈值,某些特性可以被自动禁用或限制速率。同样,如果日志显示出异常行为——程序异常或日志详细程度的飙升,也可以触发熔断。熔断器还可以防止永久性损坏。采取不可逆行动的应用程序,如发送电子邮件或从银行账户中转账,在不清楚是否可以继续进行的情况下可以使用熔断器。数据库也可以通过切换到只读模式来保护自己。许多数据库和文件系统在检测到磁盘损坏时,就会自动这样做。
    • 金丝雀部署:用于处理高流量并会部署到大量实例的服务。一个新的应用程序版本被首先部署到一组受限的计算机上,全部用户中的一个小的子集会被路由到这个金丝雀版本。金丝雀部署的是新应用版本的早期预警系统。运转不良的金丝雀版本只会影响一小部分用户,当遇到错误时,他们可以被迅速地返回旧版本。
    • 蓝绿部署:指的是运行两个不同版本的应用程序:一个是主动的,一个是被动的。新版本被部署到被动环境中,当它准备好时,流量被切换到新版本,它就变成了主动的,而以前的版本则变成了被动的。当流量不容易被划出子集或者无法并行运行不同的版本时,蓝绿部署就派上了用场。与金丝雀部署不同的是,每个环境必须能够处理 100%的用户流量。在灾难场景中,所有的用户都需要从一个有问题的系统中迁移出来,有能力快速启动并运行一个并行的环境善莫大焉。
    • 摸黑启动(有时被称为影子流量):将新的代码暴露在真实的流量中,而不使其对终端用户可见,即使代码是坏的,也没有用户受到影响。摸黑启动的软件其实仍然启用了,代码也被调用了,只是结果被丢掉了。摸黑启动可帮助开发者和运维人员在生产环境中了解他们的软件,对用户的影响最小。每当你发布特别复杂的变化时,就可以利用摸黑启动的优势。这种模式对于验证系统的迁移特别有效。在摸黑启动模式下,应用程序的代理位于实时流量和应用程序之间。该代理重复向影子系统发出请求,对不同系统根据相同请求做出的响应进行比较,并记录差异。只有生产环境下的系统响应被发送到用户手中。这种做法允许运维人员在不影响用户的情况下观察他们在真实流量下的服务。当只有读取流量被发送到系统,而没有数据被修改时,系统被称为处于“暗中读取”模式。某个系统在暗中读取模式下运行时,可能使用与生产系统相同的数据存储。当写入请求也被发送到系统中,并且使用一个完全独立的数据存储时,它被认为处于“暗中写入”模式。由于同一请求有两次操作,一次在生产系统,一次在影子系统,你应该注意避免与重复相关的错误。影子系统的流量应该从用户分析中被排除,而且必须避免双重计费等副作用。可以通过修改头信息来标记需要被排除的请求,以突出影子流量。一些服务网格(如 Istio)和 API 网关(如 Gloo)都对这些操作有内置支持。
  • 当新代码被激活后,监测健康指标,诸如错误率、响应时间和资源消耗,监测可以手动或自动完成。先进的发布管道会自动向更多的用户展开系统的变化,或者根据观察到的统计数字将变化回滚。即使在一个完全自动化的过程中,人们也应该密切关注统计数据和展开的进展。更常见的是,增加或减少的决定仍然是由人们参考日志和系统指标而做出的。
  • 你需要提前指定一般健康指标是什么标准。服务水平指标(service level indicators,SLI)指表明服务健康状况的指标,在第 9 章有更多的讨论。注意这些指标是否有退化的迹象,想一想你期望在指标或日志中看到什么才真正代表刚部署的内容正在正常运行着,验证你所期待的事情是否真正发生了。

行为准则

扩展阅读

第 9 章 On-Call

本章包括你参与 On-Call 工作、事故处理和支持工作所需的基本知识和技能。

On-Call 的工作方式

  • On-Call 人员的大部分时间用来处理临时性的支持请求,如 bug 报告、关于他们团队的软件如何运行以及使用的问题。On-Call 人员对这些请求进行分流,并对最紧急的请求做出响应。
  • 然而,大概每名 On-Call 人员最终都会遇到一起运维事故(生产软件的关键问题)。事故是由自动监控系统发出的警报或由支持工程师观察到问题并报告给值班人员的。On-Call 的开发人员必须对事故分流、缓解症状和最终解决。
  • 所有的 On-Call 轮换的工作都应该以交接开始和结束。上一名 On-Call 的开发人员总结当前的所有运维事故,并为下一名 On-Call 的开发人员提供任何未解决任务的背景。如果你已经很好地跟踪了你的工作,交接工作就不是什么大事了。

On-Call 技能包

  • On-Call 人员的工作是对请求和警报做出回应。不要忽视请求或试图隐瞒。你需要期待被打断,并自我预期你在 On-Call 时不能做那么多深入工作。
  • 与值班工作相关的信息会通过许多渠道传递过来:聊天软件、电子邮件、电话、短信、任务票、日志、系统指标、监控工具甚至是会议。注意这些渠道,这样你在调试和排除故障时就会有背景信息。
  • 创建一个单独的“On-Call”书签文件夹,并保持更新,这会很方便。与团队分享你的清单,以便其他人可以使用和改进它。
  • 首先处理优先级最高的任务。随着任务的完成或受阻,你可以依次从最高优先级到最低优先级展开工作。如果你不认可请求者对于某个问题的优先级次序的看法,请与你的管理者讨论一下。
  • 谷歌云的支持优先级梯队提供了一个如何定义优先级的例子
    • P1:严重影响(critical impact)——服务在生产环境中无法使用。
    • P2:高影响(high impact)——服务的使用受到严重损害。
    • P3:中等影响(medium impact)——服务的使用部分受损。
    • P4:低影响(low impact)——服务完全可用。
  • 服务水平指标、服务水平目标和服务水平协议也都有助于确定运维工作的优先次序。
    • 服务水平指标(SLI):如错误率、请求延迟和每秒请求数,是了解一个应用程序是否健康的最简单的方法之一。
    • 服务水平目标(service level objective,SLO):为健康的应用程序行为定义了 SLI 的目标。如果错误率是某个应用程序的 SLI,SLO 可能是请求错误率低于 0.001%的。
    • 服务水平协议(service level agreement,SLA):是关于越过 SLO 范围时将会发生什么的协议。(违反 SLA 的公司通常需要返还资金,甚至可能面临合同终止。)了解你的应用程序的 SLI、SLO 和 SLA,SLI 将为你指出最重要的指标,SLO 和 SLA 将帮助你确定事故的优先次序。
  • 在处理运维任务时,清晰的沟通至关重要。事情发生得很快,沟通不畅会造成重大问题。为了可以清晰地进行沟通,要有礼貌、直接、反应迅速,并且彻底。
  • 定期发布状态更新。更新内容应该包括自上次更新以来你的新发现以及你的下一步计划。每次更新时,提供一个新的时间预估。
  • 在工作过程中,通过在每个任务票中写下更新内容来跟踪进度。在任务票中包含缓解或解决该问题的最后步骤,这样,如果该问题再次出现,你就会有解决方案的记录。当你在工作中断后回到任务票上时,跟踪进度可以提醒你回想起你在离开时的状态。下一名 On-Call 人员可以通过阅读你的任务票看到正在进行的工作状态,而那些被你寻求过帮助的人都可以阅读日志来追赶最新进度。记录下来的问题和事故还可以创建一个可供搜索的知识库,供未来的 On-Call 人员参考。

事故处理

大多数开发人员认为处理事故是为了解决生产问题。解决问题确实很重要,但在关键事故中,第一个目标是减轻问题的影响并恢复服务。第二个目标是捕捉信息,以便以后分析问题是如何发生以及为什么发生的。确定事故的原因,证明它是罪魁祸首,并解决根本问题——只是你的第三个目标。

  • 事故响应分为以下 5 个阶段:
    1. 分流(triage):工程师必须找到问题,确定其严重性,并确定谁能修复它。
    2. 协同(coordination):团队(以及潜在的用户)必须得到这个问题的通知。如果 On-Call 人员自己不能解决这个问题,他们必须提醒那些能解决的人。
    3. 应急方案(mitigation):工程师必须尽快让事情稳定下来。缓解并不是长期的修复,你只是在试图“止血”。问题可以通过回滚一个版本、将故障转移到另一个环境、关闭有问题的特性或增加硬件资源来缓解。
    4. 解决方案(resolution):在问题得到缓解后,工程师有一些时间来喘口气、深入思考,并为解决问题而努力。工程师将继续调查问题,以确定和解决潜在的问题。一旦眼前的问题得到解决,事故也就得到了解决。
    5. 后续行动(follow-up):对事故的根本原因——为什么会发生,进行调查。如果事故很严重,就会进行正式的事后调查,或进行回顾性调查。建立后续任务,以防止那个(或那些)根本原因的再次出现。团队要寻找流程、工具或文档中的任何漏洞。在所有的后续任务完成之前,相应事故的处理不应该被认为已经结束了。

提供支持

支持请求遵循一个相当标准的流程。

  • 当一个请求进来时,你应该承认你已经看到了它,并提出问题以确保你了解该请求。
  • 一旦你掌握了该请求的问题所在,给出下一次更新反馈的预估时间:“我将在下午 5 点前给你答复。
  • ”接下来,开始调查,并在调查过程中向请求者提供最新信息。遵循我们前面概述的相同的应急方案和解决方案的策略。
  • 当你认为问题已经解决时,拜托请求者进行确认。最后,关闭该请求。

不要逞英雄

对于一些工程师来说,随着他们经验的增加,跳入“救火”模式成为一种条件反射。有天赋的“救火”工程师可以成为一个团队的“万金油”:每个人都知道,当事情变得棘手时,他们只需要去问“救火队员”,就能解决这个问题。依赖“救火队员”是不健康的。“救火队员”如果被拉入每一个问题,实际上就成了一名长期的 On-Call 人员。长时间和高风险将导致倦怠。“救火”工程师也会在编程或设计工作中“步履蹒跚”,因为他们不断地被打断。而依赖“救火队员”的团队不会拓展自己的专业知识和提高排除故障的能力。“救火队员”的英雄主义也会导致那种修复严重的潜在问题的工作被置于次要地位,因为“救火队员”总在旁边修修补补。

如果你觉得你是唯一能解决问题的人,或者你在不 On-Call 的情况下经常参与“救火”工作,你可能正在成为一名“英雄”。与你的管理者或技术负责人讨论如何找到更好的平衡,让更多的人接受培训并可以介入“救火”。如果你的团队中有一名英雄,看看你是否可以向他学习,挑起一些重担。当你想挑战一下自己的时候,请让他知道。

行为准则

扩展阅读

第 10 章 技术设计流程

本章将描述一个扩展版的设计流程,适用于大型的变更。 这些流程可能看起来进展很缓慢,而且令人生畏。一些工程师会因为重量级的设计流程出错而受到伤害。对于较小的变更,将事情缩小是可以的。你要解决的问题可能只需要 3 句话,而不是一篇滔滔不绝的论文。设计模板的部分可能是不相关的,多轮的反馈可能是不必要的,而其他团队的评审可能也是不需要的。你会对一个问题的正确投入与合作程度形成一种感觉。在开始的时候,要谨慎行事:向你的技术领导或管理者寻求指导,并广泛分享你的设计。正确地完成、参与和领导技术设计工作是很有意义并且有价值的。

技术设计的 V 型结构

  1. 你将从圆锥体的底部开始。在此处,你不清楚问题空间(problem space)、需求和可能的解决方案。所以在这个过程的早期,你不可能拥有一份令你有信心的解决方案。
  2. 当你在研究时,你会在独立工作和与一小组队友或你所研究领域的专家的讨论之间来回跳动。你要进行头脑风暴和实验。目标是学习,以提高确定性和清晰度。
  3. 最终,你的研究、实验和头脑风暴使你找到了一个理想的设计。和那些与你一起工作的人进行理智的检查之后,你撰写了一份设计文档。当你在撰写文档的过程中,你发现了更多的未知因素。你创建了一些小的原型来验证你的设计、回答问题,并帮助你在可行的替代方案中做出选择。你进行更多的研究,并请专家提供意见。你充实了设计文档的草稿。
  4. 圆锥体中的箭头进一步螺旋式上升。你现在更确定你理解了问题空间。你的原型为你的解决方案提供了越来越多的信心。你做出了一份设计方案,并准备将它分发出去。你与你的团队分享它,并得到更多的反馈。你研究、讨论并更新你的设计文档。
  5. 你现在处于圆锥体的顶部。你在你的设计中已经投入了大量的工作,而且对你的方案充满信心。你将设计方案在整个组织内传阅。安全、运维、相关的团队和架构师都需要了解你所承诺的变更,这不仅仅是为了提供反馈,也是为了更新他们对整个系统工作方式的心理模型。
  6. 在你的设计被批准后,就需要着手实施了,但设计还没有结束。实施的过程中会出现更多的意外状况。如果你和你的团队在编码时出现了任何重大的偏差,你必须更新你的设计文档。

关于设计的思考

  • 定义问题:你需要了解问题的边界,以便知道如何解决它,并避免构建错误的东西。你甚至可能会发现没有问题,或者问题并不值得被解决。
    • 首先,询问利益相关者他们认为问题究竟是什么。这些利益相关者可能是你的管理者、队友、产品经理或技术负责人。并不是每个人都会以同样的方式来看待问题。
    • 用你自己的语言向利益相关者重述问题,询问你的理解是否与他们一致。如果有一个以上的问题,询问哪些问题是最优先的。
    • “如果我们不解决这个问题会怎么样?”这是一个强有力的提问。当利益相关者回答时,问其结果是否可以接受。你会发现许多问题实际上并不需要被解决。
    • 一旦你从不同的利益相关者那里收集到关于问题的笔记,试着把反馈意见整合成一份清晰的问题陈述。不要对问题的描述信以为真,批判性地思考你被告知的内容。要特别注意问题的范围——哪些已被包含进去,哪些应该被包含但并没有被包含进去。不要把利益相关者的所有问题结合起来,这样会变得很麻烦。不要害怕修剪低优先级的变更。撰写并分发问题陈述——包括范围内和范围外的内容,以验证你的理解并获得反馈。完善的问题描述将导致一份与原来截然不同的解决方案,工程师可聚焦在问题上并列出优先事项。
  • 着手调查:不要从问题定义直接就过渡到“最终”设计。考虑相关的研究、替代的解决方案,以及权衡各方案的利弊。你提出的设计不应该是你的第一个想法,而应该是你若干想法中最好的那个。
    • 网上有大量的资源——看看别人是如何解决类似问题的。
    • 与你正在探索的问题领域的专家交流。向你公司的专家征求意见,但不要局限于你的同事。你会发现,许多博客和论文作者以及演讲者都渴望谈论他们的工作,只是要注意与外人交流时不要泄露公司的专有信息。
    • 最后,你应该批判性地思考。不是所有你在网上读到的东西都是好主意,一个特别常见的错误做法是将一个与你的问题相似但不完全相同的解决方案全盘复制。你的问题不是谷歌的问题(即使你在为谷歌工作),尽管它们看起来很相似。
  • 进行实验:通过编写代码草稿和运行测试来对你的想法进行实验。编写 API 草案和部分实现,运行性能测试,甚至 A/B 用户测试,以了解系统和用户的行为。
    • 实验会让你对自己的想法增长信心、暴露出设计上的权衡,并澄清问题空间。你也会感受到你的代码将被如何使用。在你的团队中传阅你的原型以获得反馈。
    • 不要迷恋你的实验性代码。概念验证类的代码是为了说明一个想法,然后被扔掉或重构,把你的精力集中在说明或测试你的想法上。不要写测试,也不要花时间打磨代码。你要尽可能快地学习到更多的东西。
  • 给些时间:好的设计需要创造力。不要指望坐下来一次就能完成一份设计。要给自己大块的时间,休息一下,换换环境,耐心一点儿。
    • 弄清楚你什么时候最能保持深度集中,并在你的日历上划出时间。找到适合你的时间段并保护它。
    • 外界干扰是深度工作的“杀手”。避免所有的交流方式:关闭聊天、关闭电子邮件、禁用电话通知,也许可以换个地方坐。确保你的手边有你需要的工具——白板、笔记本、纸张——如果你换了地方的话。
    • 你不会在你锁定的整段时间内进行“设计”。你的大脑需要时间来放松:休息一下,给自己一个呼吸的空间;允许你的思想放松和游荡;去散步、泡茶、阅读、写作、画画。
    • 设计是一种每天 24 小时都在进行的工作,所以要有耐心。你的大脑总在酝酿着各种想法,创意想法会在一天内随机出现(甚至在你睡觉的时候)。
    • 轻松的设计方法并不意味着你可以永远这样做。你有交付日期需要满足。设计尖峰(design spike)是管理创造性工作和可预测的交付之间的紧张关系的一个好方法。尖峰是极限编程(extreme programming,XP)的术语,指有时间限制的调查。在冲刺阶段分配一个尖峰任务,可以给你空间以进行深入的思考,而不用担心其他任务的问题。

撰写设计文档

设计文档以一种可扩展的方式来清楚地传达你的想法。写作的过程会使你的思维结构化,并凸显出薄弱的环节。记录你的想法并不总是水到渠成的。为了创建有用的设计文档,请把注意力集中在最重要的变更上,牢记目标和受众,练习写作,并保证你的文档是最新的。

  • 并非每一项变更都需要设计文档,更不用说正式的设计评审过程了。你的组织可能有自己的指导方针。如果没有指导方针,就用这 3 个标准来决定是否需要设计文档。

    • 该项目将需要至少一个月的工程时间。

    • 这一变更将对软件的扩展和维护产生长期的影响。

    • 该变更将显著影响其他团队。

  • 从表面上看,设计文档是告诉别人某个软件组件是如何工作的。但是,设计文档的用途超越了简单的文档。设计文档是一种工具,可以帮助你思考、获得反馈、让你的团队了解情况、培养新的工程师,并推动项目规划。

  • 以目标受众的视角重读你所写的内容:你是否理解并不重要,重要的是他们是否能理解。文档要简明扼要。为了帮助你获得读者的视角,你需要去阅读别人写的东西。想一想你会如何编辑他们的文章:哪些是多余的,哪些还需要补充。在你的公司里寻找优秀的文档作者,并征求他们对你所写内容的反馈。

  • 不要让语言障碍使你对撰写设计文档望而却步,不要担心语法是否完美,重要的是清楚地表达你的想法。

  • 务必保证你的文档是最新的。如果你的设计提案和设计文档是两个独立的东西(比如 Python PEP 和 Python 文档),你需要使文档与已实现的方案保持同步,确保有其他人在你进行代码评审之后同步更新文档。

  • 你需要对你的设计文档进行版本控制,一个优秀的技巧是将设计文档与代码放在同一个库中进行版本控制。然后,代码评审也可以作为设计内容的评审意见,这些文档也可以随着代码的发展而更新。

使用设计文档模板

设计文档应该描述当前的代码设计、变更的动机、潜在的解决方案,以及建议的解决方案。该文档应该包括建议的解决方案的细节:架构图、重要的算法细节、公共 API、模式、与替代方案的利弊比较、假设和依赖项。

参考模板:

  • 概要:介绍正在解决的问题,并说明为什么它值得被解决。提供约一段关于拟议变化的总结,并提供一些指导,将不同类型的读者——安全工程师、运维工程师、数据科学家,指向相关章节。
  • 现状与背景:描述正在修改的结构并定义专有名词,解释那些名字不显眼的系统是干什么的:“Feedler 是我们的用户注册系统。它建立在 Rouft 之上,是提供有状态的工作流处理的基础设施。”谈谈目前解决相应问题的方法。是否有正在采用的变通方法?它们的缺点是什么?
  • 变更的目的:软件团队往往拥有超过他们能同时应对的极限的项目。为什么这个特别的问题值得去解决,而且是要现在解决?描述这项工作将带来的好处,把这些好处与业务需求联系起来。“我们可以减少 50%的内存占用”不如“通过减少 50%的内存需求,我们可以解决安装我们的软件时最常见的拒绝理由,从而提高安装率”来得有力。但是,请注意不要过度承诺!
  • 需求:列出一个可接受的解决方案必须满足的需求,这些需求可以分成以下几个部分。
    • 面向用户的需求:这部分内容通常在需求中占很大比重,它们从用户的角度定义了变更的性质。
    • 技术需求:这部分内容包括解决方案必须满足的硬性需求。技术需求通常是由互操作性问题或严格的内部准则引起的,如“存储层必须支持 MySQL”或“必须提供 OpenAPI 规格以与我们的应用网关相配合”。服务水平目标也可以定义在这里。
    • 安全性与合规性需求:虽然这些可能被视为面向用户或技术需求,但它们通常被分开,以确保安全性的需求得到明确的讨论。数据保留和访问政策通常包括在这里。
    • 其他:这可能包括关键的截止期限、预算和其他重要的考量因素。
  • 潜在的解决方案:解决一个问题通常可以有多种方案可以采用。撰写这部分内容对你和读者来说都是一种工具,它的目的是迫使你不仅要思考你的第一个想法,还要思考其他的想法和它们之间的利弊。描述合理的替代方案,以及你为什么拒绝它们。描述潜在的解决方案将预先解决“为什么不做 ××?”的评论。如果你因为错误的原因而否定了某个解决方案,评论者就有机会发现其中的过错,甚至可以找出你没有考虑过的替代方案。
  • 建议的解决方案:描述你所确定采用的解决方案。这个描述要比概要中简短的描述更加详细,并且可能包含突出变化的图示。在这里和下面的章节中,如果你的建议包括多个阶段,请解释该解决方案是如何从一个阶段发展到另一个阶段的。
  • 设计与架构:设计与架构的内容通常占据文档中很大的比例。所有值得讨论的技术细节都在这里。突出令人感兴趣的实施细节,如利用的关键类库和框架、实施模式,以及任何与公司常见做法不同的地方。设计与架构应该包括组件的示意图、调用流和数据流、用户界面、代码、API 和模式模拟。
  • 系统构成图:该部分包括展示主要组件和它们如何互动的图例。通过突出显示新的和已更改的组件来解释正在发生的变化,通过展示某组件在创建之前和之后的图例变化也可以办到这一点。该图应配有简要的说明,引导读者了解这些变化。
  • UI/UX 变更点:如果你的项目改变了用户界面,请创建原型,用原型的方式来预演用户的动作流。如果你的变更没有可视化的组件,这部分可以讨论开发者对于你正在创建的类库的使用体验,或者描述用户使用你的命令行工具的方式。我们的目标是要多考虑与你的变更进行交互的人的体验。
  • 代码变更点:描述你的具体实现的过程。要高亮出现有的代码需要变更的内容、方式和时间。当引入任何新的抽象概念时,也需要描述出来。
  • API 变更点:记录所有对现有的 API 和任何新增的 API 的改变。讨论向前或向后兼容性与版本问题。要记得在你的 API 提案中包含错误处理:当遇到不规则输入、违反约束条件、意外的内部错误或异常时,它应该以有用的消息来响应。
  • 持久层变更点:解释正在引入或已经修改的存储技术。讨论新的数据库、文件和文件系统架构、检索的索引和数据传输管道,包括所有模式的变化并对其向后兼容性进行说明。
  • 测试计划:不要事先定义每项测试,而是去解释你计划如何去验证你的变更。讨论测试数据的来源或生成方法,强调需要覆盖的用例,讨论你期望使用的类库和测试策略,并解释你将如何验证安全性的需求是否得到满足。
  • 发布计划:描述你将使用的策略,以避免复杂的部署顺序需求。记录你需要设置的特性开关,以控制新版本的展开,以及你是否会使用第 8 章中的部署模式。想一想你将如何发现你发布的变更是否生效,以及在发现问题时你将如何回滚。
  • 遗留的问题:明确地列出设计中尚未回答的紧迫问题。这是征求读者意见的一种好方法,并说明你的“已知的未知”。
  • 附录:在附录中加入额外的令人感兴趣的细节。这也是添加相关工作参考和进一步阅读资料的好地方。

协同设计

与你的团队进行建设性的合作将产生更好的设计,但合作并不总是容易的。开发人员是一群有主见的人,解释反馈并将其精简为一份有意义的设计并不那么容易。通过融入团队的设计流程,提早沟通和经常沟通以避免意外,以及利用设计讨论来进行头脑风暴,以进行设计上的协作。

  • 架构评审是更正式的、重量级的过程。设计必须得到外部利益相关者的批准,如运维人员和安全保障人员。设计文档是必需的,而且可能会有多轮会议或演示。由于时间成本很高,只有大型或有风险的变更才用得上架构评审。不要等到最后的批准才开始写代码,要花时间实现原型和概念验证的“尖峰”,以增加对设计的信心,并给你一条更短的生产路径。但不要超越概念验证的工作,你可能需要根据设计反馈来改变你的代码。
  • 我们把另一种设计评审过程称为“请求裁定”,即 RFD(不要与互联网协会的“征求意见稿”的过程即 RFC 相混淆)。RFD 这个词并不常见,它的模式是:RFD 是快速的团队内部评审,以快速达成需要一些讨论但不需要全面评审的决定。一名请求做出裁定的工程师会分发一份精要的书面材料,来描述将要做出的决定——一份轻量级的设计文档。然后团队成员在白板上讨论他们的选项,提供意见,并做出决定。
  • 找出谁必须被告知或签署承认你的设计工作,以及谁被授权做决定。
  • 随着你工作的深入,让人们了解到最新的情况。在状态会议和站会上提供最新信息,继续进行随意的对话。注意你提议的变更可能会产生的二阶效应以及可能影响到的人,将即将发生的变更通知受影响的团队。这尤其适用于支持、QA 和运维团队。要有包容性——把人们拉进头脑风暴会议,倾听他们的想法。
  • 设计讨论可帮助你理解问题空间、分享知识、讨论利弊,并巩固设计。这些头脑风暴会议是非正式的,谈话是自由流动的,白板上写满了讨论时留下的油墨。讨论发生在设计的早期,也就是当问题被合理地理解,但设计尚未决定之时,应该有一版设计文档的草案,但它可能仍然有很多空白和开放的问题。将头脑风暴分成多个会议,由不同的参与者参加,集中讨论设计的不同方面。
  • 在头脑风暴会议之前,制定一个松散的议程,包括问题、范围和某项(或某些)拟议的设计,以及潜在的权衡及开放性问题。与会者应该事先阅读议程,所以议程要保持简短。目的是提供足够的信息,方便大家开始自由讨论。
  • 在头脑风暴会议期间,做笔记可能会分散注意力。有些团队会指定一名正式的会议记录员。确保这个角色由所有团队成员平均分担,否则一直担任记录员的人将无法做出贡献。白板也是一个“记录员”,你可以在讨论过程中拍照,如果使用虚拟板,则可以保存中间的状态。会议结束后,以白板上的图片为指导,根据你的回忆写一份总结。把这些笔记发给与会者和其他相关的队友。
  • 你应该为你团队的设计工作贡献力量,而不仅仅是你自己的。和代码评审一样,对设计的贡献可能会让人感到不舒服。你可能认为你对更高级的开发者的设计没有什么贡献,阅读设计文档和参加头脑风暴会议可能会让人觉得是在分散精力,但还是要做。你的参与将改善你的团队的设计并帮助你学习。
  • 当你参与设计工作时,应提出建议和问题。运用我们为代码评审提供的相同的指导,对设计进行全面的思考,考虑到安全性、可维护性、性能、规模等,要特别注意设计是如何影响你的专业领域的。沟通要清晰,要尊重他人。
  • 提出问题和给予建议一样重要,问题会帮助你成长。就像在课堂上一样,你可能不是唯一对某项设计决定感到疑惑的人,所以你的问题也会帮助其他人成长。此外,你的问题可能会引发新的想法,或者暴露出设计中没有考虑到的缺失。

行为准则

扩展阅读

第 11 章 构建可演进的架构

本章将教你一些技术,这些技术可以使你的软件变得更简单,从而更容易演进。 矛盾的是,在软件中实现简洁性会很困难。如果缺乏有意识的努力,代码会变得纠结和复杂。我们将首先描述复杂性以及它如何导致代码库走向僵化和混乱。然后,我们将向你展示降低复杂性的设计原则。最后,我们将把这些设计原则转化为具体的 API 和数据层最佳实践。

理解复杂性

复杂系统有两个特点:高依赖性和高隐蔽性。我们要再加上第三个:高惯性。

  • 高依赖性:导致软件依赖于其他的 API 或代码行为。
  • 高隐蔽性:使得程序员很难预测某项变更的副作用、代码的行为方式,以及需要修改的地方。
  • 高惯性:是指软件保持之前的使用习惯。

可演进的设计

  • 避免过早优化。
  • 避免不必要的灵活抽象模型。
  • 避免最小可行产品(minimum viable product,MVP)所不需要的产品特性——你需要那些可以获得用户反馈的最低限度的功能集。
  • 通过基于业务领域的软件分类来封装领域内的知识——会计、计费、运输等。将软件组件映射到业务领域,将使代码的变化保持专注和干净。
  • 识别领域边界和封装领域知识既是一门科学又是一门艺术。有一种完整的架构方法叫作领域驱动设计(domain-driven design,DDD),它定义了一套广泛的概念和实践,将商业概念映射到软件上。只有在最复杂的情况下才需要全覆盖的 DDD。不过,熟悉 DDD 将有助于你做出更好的设计决策。

最小惊讶原则

不要让用户感到惊讶,构建特性表现得要像用户最初期望的那样,具有上扬的学习曲线或奇怪表现的特性会使用户感到沮丧;同样地,不要让开发者感到惊讶,令人惊讶的代码通常晦涩难懂,这会导致复杂性。你可以通过保持代码的针对性、避免隐性知识,以及使用标准类库和模式来消除惊讶。

凡是开发者在调用 API 时需要知道的但又不属于 API 本身的不明显的知识,都被视为隐性知识。需要隐性知识的 API 会让开发者感到惊讶,造成 bug 和上扬的学习曲线。两种常见的隐性知识违规行为是隐藏的排序需求和隐藏的参数需求。

可演进的 API

  • 小巧的 API 更易于理解和演进。
    • 当创建一个 API 数据模型时,只添加你当时需要的方法。
    • 当使用一个框架或生成器工具来引导生成你的 API 时,清理掉你不使用的字段或方法。
    • 带有许多字段的 API 方法应该有合理的默认值。开发人员可以只专注于和自己相关的字段,因为它们会继承其他字段的默认值。默认值可使大型 API 在感觉上很小巧。
  • 切记使用标准工具来定义服务端 API。一项定义良好的服务将声明其模式、请求和响应的方法以及异常。定义良好的服务端 API 使编译时的验证更为容易,并使客户端、服务端和文档保持同步。
  • 如果你的公司已经选择了某个 API 定义框架,那么就去使用它,选择一个“更好的”框架将需要太多的互操作工作。如果你的公司仍然在手动生成 REST API,并且 JSON 接口是在没有正式规范的情况下从代码中演进而来的,那么你最好的选择之一就是 OpenAPI,因为它可以在预先存在的 REST 服务上进行改造,并且不需要进行重大迁移即可采用。
  • 使 API 变更保持兼容性可以让客户端和服务端版本独立发展。你需要考虑两种形式的兼容性:向前兼容和向后兼容。
    • 向前兼容的变更允许客户端在调用旧版的服务时使用新版的 API。
    • 向后兼容的变更则恰恰相反:新版本的库或服务不需要改变旧的客户端代码。
  • 版本化你的 API 意味着你在做出改变时将引入一个新的版本。老用户可以继续使用旧的 API 版本,跟踪版本也有助于你与你的用户沟通——他们可以告诉你他们正在使用的版本,你也可以用新的版本推销新的特性。
  • 将文档与你的 API 一起保持版本化。
  • 当客户端代码难以变更时,API 的版本管理最有价值。通常你对外部(用户的)的客户端控制较少,所以面向客户的 API 是非常重要的版本。如果你的团队同时控制着服务端和客户端,你也许可以不需要进行内部的 API 版本管理。

可持续的数据管理

  • 隔离数据库和使用明确的 schema 将使数据演进更易于管理。
  • 采用数据库 schema 管理工具使数据库的变更操作不那么容易出错。自动工具为你做了两件事情:它迫使你跟踪 schema 的整个历史,并为你提供工具,将 schema 从一个版本迁移到另一个版本;跟踪 schema 变化,使用自动化数据库工具,并与你的数据库团队合作管理 schema 演变。
  • 你也可以通过导出将内部模式与下游用户显式解耦的数据产品来保护内部 schema。数据产品将内部 schema 映射到独立的面向用户的 schema,开发团队同时拥有生产环境的数据库和供发布的数据产品。独立的数据产品,可能只是数据库视图,允许团队与数据的消费者保持兼容,而不必冻结其内部的数据库 schema。

行为准则

扩展阅读

第 12 章 敏捷计划

本章将向你介绍敏捷计划的基础知识和 Scrum(一种被普遍采用的敏捷框架)的关键实践,这样你就可以马上着手实践了。

敏捷宣言

  • 个人和互动高于流程和工具
  • 工作的软件高于详尽的文档
  • 客户合作高于合同谈判
  • 响应变化高于遵循计划

敏捷实践的重点是与团队成员和客户的合作;认识、接受并消化变更;注重迭代改进而不是大爆炸式的开发发布。敏捷开发模型通常与瀑布流模型形成对比,瀑布流模型是指在项目开始时就进行详尽的计划,这是一种过时的做法。

人们迷恋于“做敏捷”的“正确”方法,而往往损害了第一个原则:“个人和互动高于流程和工具”。

敏捷计划的框架

Scrum 和看板是两个最常见的敏捷计划框架。

最流行的是 Scrum,它鼓励短期迭代,并经常设有检查点来调整计划。开发工作被分成几个冲刺阶段。冲刺的周期各不相同,最常见的是两个星期。在冲刺开始时,每个团队都会安排一场冲刺计划会议来分配工作,这些工作被记录在用户故事或任务池中。规划之后,开发人员便开始工作,工作进展在任务票或问题系统中被跟踪。每天都设有一个简短的站会,以分享最新情况并指出问题。在每个冲刺阶段结束后,团队会进行一次回顾总结,回顾已经完成的工作、讨论新的发现、查看关键指标,并对执行过程进行微调。回顾会可以为下一轮冲刺的计划会提供信息,创建一个从计划到开发到回顾会再到计划的闭环。

看板不像 Scrum 那样使用固定周期冲刺。相反,看板定义了工作流程中的各个阶段,所有的工作条目都要经历这些阶段(例如,待着手、计划中、实施中、测试中、部署、展开)。团队经常定制看板阶段以适应他们自己的需求。看板通过限制每个阶段的任务数量来限制正在进行中的工作(WIP)。通过限制任务票的数量,团队被迫在承担新工作之前要完成现有的任务。看板相当于为每个工作流程阶段设置了垂直列的仪表盘,由标题框代表的任务,随着状态的变化在各列之间移动。看板将进行中的工作可视化,并识别出诸如工作在某一阶段中形成了积压之类的问题。例如,当看板显示大量的工作停留在测试阶段时,团队可能会做出调整,将一些开发的工作暂时转入“待着手”的工作中,并派工程师来帮助测试。看板对像支持工程师和 SRE 这样的团队来说效果极好,他们要处理大量传入的请求,而不是长期的项目。

团队其实很少实施 Scrum 或看板的“柏拉图式的理想”,他们从中挑选一些来进行实践,改变或忽略其他的。无论你的组织是采用 Scrum、看板,还是两者的混合体 Scrumban(这确实是一个真实存在的东西!),或者敏捷的其他变体,规划过程应该服务于提供有用的软件给客户。将注意力集中在目标上,而不是机制上。实验并测量结果,只保留有效的东西,放弃其他的。

Scrum 框架

  • 用户故事是一种特殊的任务票,它从用户的角度定义了特性的需求,格式是“作为一名<用户>,我<想><这样>”。用户故事通常在其标题和描述旁边设有属性。最常见的两个属性是预估工数和验收标准。
  • 单一的用户故事可能需要被分解成更小的任务,以预估它需要多长时间才能完成,用来给多名开发人员分配工作,并跟踪实施进度。分解任务的技巧之一是写出非常详细的描述。仔细阅读描述,找出所有的任务。
  • 团队的工作能力是以故事点来衡量的,这是一个约定好的尺度单位(以小时、天或“复杂性”来度量)。一次冲刺迭代的能力是以开发人员的数量乘每名开发人员的故事点来计算的。许多团队使用基于时间的任务分配策略,一个故事点相当于一个工作日。基于工作日的估计通常需要考虑到非任务工作——会议、中断、代码评审等,请将一个工作日定义为 4 个小时。
  • 积压分流或梳理(从修剪树木的意义上讲)通常在计划会议之前进行。所谓积压是指候选的用户故事列表,分流是为了保持它的新鲜度、相关性和优先级。产品经理与工程经理一起阅读积压的用户故事,有时还有开发人员的参与。新的故事被添加进来,过时的故事被关闭,不完整的故事被更新,高优先级的工作被转移到积压列表的顶部。一份已经梳理好的积压列表将更方便地在计划会议中进行讨论。
  • 一旦前期工作完成,就会召开冲刺计划会议。计划会议是协作性的,工程团队与产品经理一起决定要做什么。讨论高优先级的用户故事,工程师与产品经理一起决定什么适合于描述冲刺迭代的能力。冲刺迭代的能力是通过查看以前的冲刺中完成了多少任务来确定的。在冲刺计划期间,随着团队成员的加入或离开、休假、On-Call 轮换,每次冲刺迭代的能力都会被进一步完善。冲刺最重要的特点是周期短,通常为两周。短暂的冲刺使工作的推进变得可行,因为工作最多只需要推进一到两周的时间。小型的冲刺将迫使团队将大型任务分解成小型任务。小型任务更好,因为它们更容易被理解和预估。将工作分解成小型任务,也允许一名以上的开发人员同时在一个项目上工作。较短的开发周期和频繁的接触点——站会和评审——意味着问题将更早地浮现。
  • 一旦完成了冲刺计划,当前冲刺就被认为进入了已锁定的状态。在冲刺期间出现的新工作不应该被拉进来,它应该被推到工作的积压列表中,并计划到未来的冲刺中。锁定冲刺让开发人员专注于他们的工作并带来可预测性。当计划外的工作被拉进来时,团队应该在回顾阶段调查原因,以便在将来减少计划外的工作。严格遵守冲刺计划的做法并不常见,大多数团队会选择他们要做的事情。有些团队在冲刺计划会议上做预着手的工作,有些团队没有产品经理——开发人员定义所有工作。许多团队不使用用户故事,而是选择格式更开放的任务票或 bug 票。你要预想到各团队之间会有差异。

站会

  • 站会通常是在每天早上安排 15 分钟的会议(快到可以站着完成,不过实际上可以选择是否一定要站着)。在会议上,队友们围成一圈,介绍自上一次站会以来他们所做的工作,他们计划在未来做什么,以及他们是否发现了任何可能拖延或破坏冲刺进程的问题。虽然面对面的站会比较常见,但有些团队也采取了异步的形式。在异步站会中,同样的更新被提交到一个聊天工具或团体电子邮件中,每天都有。
  • 站会是一种定期的系统检查——看一眼你的汽车仪表盘,以确保你有汽油,而且其神秘的“检查引擎”灯并没有亮起。状态应该被快速更新,这并不是一个排除故障的地方。尽量将你对进展的评论限制在最基本的范围内,并提出你有的任何问题。也要宣布你的发现:你发现的 bug、软件的意外行为等。可以在稍后的停车场讨论中谈谈你的发现(当然不是在真正的停车场)。
  • 如果你的团队举行同步站会,应该尽你所能准时参加。如果你的站会涉及更新任务票或问题票的状态,请尽量提前更新那些分配给你的票。在阅读或聆听他人的更新时,你要寻找机会来帮助降低完成冲刺的风险:当有人说某张任务票实际需要的时间比预期的要长,如果你有空闲时间,就自愿去帮忙。
  • 停车场讨论发生在站会之后。这是一种让站会保持简短的方法,并确保讨论与每个与会者相关。当有人说“留到停车场”时,他们的意思是当下停止讨论,有兴趣的人在站会之后继续讨论。
  • 当出现日程安排冲突时,跳过站会是可以接受的。如果你需要错过站会,请询问你的管理者如何提供和获得最新信息。在异步站会的情况下,错过的次数就会比较少。
  • 有许多站会和 Scrum 会议的变化。你可能会听到一些名词,如 Scrum of Scrums 或 Scrumban。Scrum of Scrums 是一种模式,即从每个单独的 Scrum 会议中选出一名领导者去参加第二次 Scrum,所有的团队聚在一起报告他们的团队进展,并指出彼此之间的相互依赖关系。Scrum of Scrums 在运维中很常见,每个团队派一名工程师(通常是 On-Call 人员)去参加运维的 Scrum 会议,以了解运维问题的情况。Scrumban 是 Scrum 和看板的混合体。所有这些的重要之处在于理解你的团队和组织是如何配合的,并在这个框架内工作。

评审机制

评审发生在某两轮冲刺之间。评审通常分为两个环节:演示和项目评审。在演示环节中,团队中的每个人都会展示他们在本轮冲刺中取得的进展。之后,根据目标对当前的冲刺进行评审。成功的冲刺将完成他们的目标,并有较高的用户故事完成率。

标准的做法是,每个冲刺周的评审时间不应超过一小时——两周的冲刺迭代将有两小时的冲刺评审。每个人都聚集在办公桌前或会议室里进行演示,团队成员轮流展示他们所做的工作,会议保持非正式。之后,对冲刺目标进行评审,并对完成情况进行评估。

不要为冲刺评审做过度的准备。花几分钟时间弄清楚你要展示的东西,并确保你的任务票状态是准确的。演示是非正式的,所以要避免正式的演讲或发言。

评审是为了庆祝团队的胜利、创造团结、提供反馈的机会,并使团队对进展保持诚实。在一个团队中,并不是所有的开发人员都在做同样的项目,所以评审可以帮助团队成员了解其他人正在做的事情。评审让队友保持同步,让每个人都有机会提供反馈,并认可杰出的工作,可创造凝聚力。项目状态评审也可以帮助团队就什么是真正的“完成”以及他们如何朝着目标前进达成一致,发现的问题可以在冲刺回顾会上讨论。

回顾会

  • 在回顾会中,团队聚在一起讨论自上次回顾会以来有哪些进展,哪些不足。会议通常分为 3 个阶段:分享、确定优先级和解决问题。

  • 领导者(或敏捷专家)将通过要求每个人分享上个冲刺阶段的成功案例和失败经验来召开回顾会。每个人都要参与进来,而敏捷专家会在白板上或共享文件中保留一份清单。然后,团队成员讨论效果不好的条目的优先级——哪些条目造成了最大的痛苦?最后,团队集思广益,讨论如何解决最高优先级的问题。

  • 不要害怕改变事情。敏捷开发实践就是需要具有可塑性,这一点在宣言中有所体现。“个人和互动高于流程和工具”。在每次回顾会之前,花几分钟时间思考什么会让你的团队变得更好。在会议上分享你的想法。

  • 回顾会和评审会经常被混淆。评审会的重点是在某个冲刺阶段完成的工作,而回顾会的重点是流程和工具。回顾会通常发生在冲刺之间,通常是在评审会之后。许多团队在每轮冲刺开始时都会把评审会、回顾会和冲刺计划合并为一个会议。只要每个步骤——评审、回顾和计划——都能得到单独的解决,把这几个会议合并起来就没有问题。

  • 回顾会也是造成敏捷实践有如此众多的风格的原因之一。我们鼓励团队经常重新评估和调整他们的流程,不断地调整意味着不存在两个团队以相同的方式来实践敏捷开发模型。

路线图

以两周为周期的冲刺迭代是完成中小型工作的好方法,但更庞大的项目需要更先进的规划。客户有开发人员需要遵守的交付日期,企业需要知道哪些团队需要更多的工程师,而大型技术项目需要分解、规划和协调。

路线图应该鼓励每个人对团队正在构建的东西进行长期思考,它并不是要成为关于团队 9 个月后将构建的东西的静态和不可变的文档。更远的地方应该更模糊,而更近的地方应该更准确。不要自欺欺人地认为任何一个季度都是百分之百准确的。

与被锁定的冲刺迭代不同,路线图是要不断发展的。客户需求会改变,新的技术问题会出现。这就是冲刺计划、评审和回顾的作用,它们可以让你根据新的信息调整你的计划。在改变路线图时,沟通是至关重要的。相关联的团队应该尽早地得到通知,告诉他们工作将被重新安排或放弃。

许多公司都要经历年度计划周期,管理者们在每年的最后一个季度试图为下一年的 4 个季度的工作进行规划。年度规划几乎都是一个充斥着讨价还价的交易场。尽管如此,年度计划周期往往会推动“资源分配”或“人头数”(head count)——公司的说法是指新雇用的工程师最终会去哪里。年度计划通常集中在占团队时间很大比例的大型项目上。如果一个你很感兴趣的项目没有被提及,不要紧张,在规划过程结束时,问问你的管理者该项目情况。

行为准则

扩展阅读

第 13 章 与管理者合作

本章将帮助你与你的管理者建立有效的关系。我们将给你一份关于管理职业的简短概述:你的管理者是做什么的以及他们如何做。然后我们将讨论常见的管理过程。工程师们经常会遇到像 1∶1、PPP 和 OKR 这样的缩写,以及像绩效评估这样的术语,但却不知道它们是什么,为什么而存在,或者它们是如何运转的。我们将教你入门的知识,并告诉你如何从中获得最大收益。然后,我们将给出“向上管理”的提示,并有一段关于如何应对糟糕的管理者的内容。最后,你将拥有一个工具包来建立一段富有成效的关系。

管理者是做什么的

工程经理的工作是关于人、产品和流程的。管理者们构建团队、指导和培养工程师,并进行人际关系的动态管理,工程经理还计划和协调产品的开发。他们也可能参与产品开发的技术方面如代码评审和技术架构,但好的工程经理很少写代码。最后,管理者们对团队流程进行迭代,以保持其有效性。管理者们通过与高管或董事(“向上”)合作、与其他管理者(“横向”)合作以及与他们的团队(“向下”)合作来“管理”所有的这些事务。

管理者通过与高管的关系和沟通进行向上管理。管理者是普通工程师和负责商业决策的高管之间的沟通渠道,向上管理对于获得资源(资金和工程师)以及确保你的团队可以得到认可、赞赏和倾听至关重要。

管理者通过与其他管理者合作来进行横向管理。一名管理者有两个团队:他们所管理的所有人和管理者的同行们。管理者同行们一起配合,使团队在共同目标上保持一致。关系的维护、清晰的沟通以及合作的规划,可以确保团队有效地合作。管理者通过跟踪正在进行的项目的进展来进行管理,设定期望值并给予反馈,明确提出相对优先的事项,雇用员工并在必要时解雇,以及保持团队的士气。

沟通、目标和成长

用于维护你与管理者关系的流程:

  • 一对一面谈(1∶1)和进展、计划与问题(progress-plans- problems,PPP)报告用于沟通和更新项目状态。
  • 目标和关键结果(OKR)以及绩效评估则管理目标和成长。

一对一面谈

  • 一对一面谈是你和你的管理者专属的时间,可以用来讨论关键问题、解决大局观上的偏差,并建立富有成效的长期关系。一对一面谈是一种众所周知的做法,但它们往往被用作工作状态检查或故障排除会议而没有发挥很大作用。
  • 你应该制定议程,并在一对一面谈中承担大部分的谈话。在面谈之前,与你的管理者分享一份议程摘要。保存一份包含过去议程和笔记的面谈文档,与你的管理者分享你的文档,并在每次一对一面谈之前和之后更新它。如果你的管理者有某些话题要讨论,他们也可以添加自己的条目,但管理者的议程应该排在你的议程之后。

PPP

  • PPP 是一种常用的更新工作状态的格式。更新工作状态并不是为了计算你的时间,它是为了帮助你的管理者发现问题,找到你需要背景信息的领域,以及提供将你与正确的人联系起来的机会。在工作状态的更新中也会浮现出一对一面谈的主题,并帮助你反思你已经到达的地方、你要去的地方以及什么阻碍了你的发展。
  • 顾名思义,PPP 中的每个 P(进展、计划与问题)都有自己的小节。每个小节应该有 3 到 5 个要点,每个要点应该很简短,只有 1 到 3 个句子。下面是一个例子。
    • 进展
    • 计划
    • 问题

OKR

  • OKR 框架是公司定义目标和衡量其是否成功的一种方式。在 OKR 框架中,公司、团队和个人都定义了目标(目的),并为每个目标附上衡量标准(关键结果)。每个目标都附有 3 到 5 个关键结果,它们是标志着目标达成的具体指标。
  • OKR 通常是按季度设定和评估的。与你的管理者合作,了解公司和团队的目标,使用更高阶的目标来定义你的 OKR。尽量减少 OKR 的数量,这将使你保持专注。每个季度有 1 到 3 个 OKR 是一个合理的数值。如果超过 5 个,你就会把自己搞得过于疲惫。
  • OKR 通常被设定得比合理值略高,以创造“达成”或“延伸”目标的条件。这种理念意味着你不应该百分之百地达成目标的 OKR,这是一个表明你设定的目标还不够高的迹象。大多数 OKR 的实施以 60%到 80%的成功率为预期目标,这意味着只有 60%到 80%的目标应被实现。

绩效考核

  • 绩效考核会使用一个工具或像下面这样的模板来进行
    • 你今年做了什么?
    • 今年有什么事情进展顺利?
    • 今年有什么事情可以做得更好?
    • 你在职业生涯中想得到什么?你认为自己在 3 到 5 年内会到达什么样的高度?
  • 你也可能被要求参加“360 度考评”(如“全方位考核法”),员工从各个方向的同事那里征求反馈意见:向上(管理者)、向下(下属)和横向(同行)。同事们回答诸如“我可以做得更好吗?”和“人们害怕告诉我什么?”以及“我做得好的是什么?”等问题。最终,360 度考评鼓励诚实的反馈,给员工一个机会告诉管理者他们做得如何。请认真对待 360 度考评,并给出深思熟虑的说明。

向上管理

  • 不要听信表面上的反馈。你的管理者仅仅是视角之一(尽管是一个重要的视角),试着把管理者的反馈纳入你的观点,而不是直接采用管理者的反馈。问问你自己,你和你的管理者在观点上有什么差异,他们的反馈如何才能与你吻合,他们知道什么而你不知道,诸如此类。
  • 对别人的反馈意见也要给予反馈。如果不这么做,可能会让别人觉得自己的反馈掉入了黑洞。当管理者的反馈得到了响应时,就告诉他们。
  • 在提供反馈时,使用情况、行为和影响(situation-behavior- impact,SBI)框架。首先,描述情况。然后,描述行为:你认为值得表扬的或有问题的具体行为。最后,解释影响:该行为的影响以及它的重要性。
  • 不要指望你的管理者知道你对自己职业的要求。你需要清楚地阐述你的目标和愿望,以便你的管理者可以帮助你实现这些目标。正式的绩效考核环节是进行这种对话的好时机。如果你知道你想做什么,就让你的管理者也知道,并与他们合作,把你的工作引向你的目的地。管理者工作的一部分是使你的兴趣与公司的需求相一致,他们越了解你的兴趣,他们就越能将正确的机会引导到你前进的道路上。在讨论完你的目标后要保持耐心,可用的机会只有这么多,最终要看你如何充分利用你所得到的机会。请认识到机会以多种形式而存在:新项目、新挑战、要指导的实习生、演讲机会、要写的博客文章、培训,或要合作的团队。在正确的视角下,你做的每件事都是成长的机会。
  • 给予反馈、处理某些状况,甚至就连知道什么是正常的而什么是不正常的,都可能是困难的。在你的组织内部和外部,受信任的同行团体是检验事情是否合理的好去处。对于代表性不足的群体的成员来说,这种好处会翻倍。寻找像 PyLadies、/dev/color 和其他社区的组织,他们可以讨论你的情况并分享他们的故事和经验。
  • 如果你觉得合适,可以使用 SBI 框架与你的管理者交谈。如果你不愿意与你的管理者交谈,可以与人力资源(human resource,HR)部门、你管理者的上级或其他导师交谈。你所追求的方向取决于你与每一方的关系。如果你觉得没有什么好的选项,就去人力资源部门寻求帮助。
  • 失职的管理令人沮丧、徒增压力,甚至会阻碍你的职业发展。不是每名管理者都是优秀的,也不是每名优秀的管理者都适合你。如果你已经给出了反馈意见,并保持了耐心,但事情仍然没有进展,那就起身离开。

行为准则

扩展阅读

第 14 章 职业生涯规划

前面的章节着重于具体的工程活动,而在这一章中,我们将眼光放长远来看看未来的发展,给出职业生涯的建议,并分享一些结尾寄语。

迈向资深之路

  • 各家公司的职级数量不尽相同,但通常有两个过渡表明资历发生了重大转变:从初级工程师或软件工程师到资深工程师,以及从资深工程师到主任工程师或首席工程师。
  • 初级工程师实现特性和完成任务。
  • 资深工程师要处理更多的不确定性和模糊性。他们帮助确定工作内容、应对更大或更关键的项目,并且需要更少的指导。
  • 主任工程师承担了更广泛的职责,甚至超出了他们团队的范畴。他们要对工程战略、季度规划、系统架构做出贡献,并且要确保工程流程的运转和政策的实施。主任工程师仍然在编写代码(而且编码很多),但要达到这个水平,仅仅是一名优秀的程序员还不够:你必须理解大局,并做出具有深远影响的决策。

职业生涯建议

T 型人才

  • 请从构建你的基本盘开始。构建基本盘会让你接触到不同的子领域,这样你就能找到自己的激情所在。寻找涉及其他团队的项目,诸如数据科学、运维、前端等。使用其他团队的代码,并询问是否可以在代码变更时贡献补丁或结对编程。当你遇到激起你兴趣的主题和问题时,深入研究以获得深度。
  • 一个好的团队会有一个坚实的 T 型人才的组合。产品开发团队的成员有可能拥有不同的深度领域,而基础设施团队的成员则更有可能拥有共同的专长。
  • 随着公司的发展,他们越来越多地为每个领域雇用专家,这将推动已经在公司工作的每个人也走向专业化(因为通才会发现自己可操作的领域越来越小)。这个过程可能会促使你走向专业化。或者,如果你已经有了 T 型的技能组合,请帮助自己去优雅地适应,因为你可以更多地依靠自己的专业。你甚至可能发现,一些加入公司的专家都没有你的广度(他们不是 T 型的),所以你可以帮助他们了解周围的环境以提高工作效率。

参加工程师训练营

  • 许多公司都设有工程师训练营以鼓励学习、开发和共享的企业文化。招聘、面试、午餐会、研讨会、聚会、阅读小组、开源项目、学徒和导师计划都是可以参与的机会。
  • 寻找并加入你最感兴趣的项目,你可以通过参加项目或领导项目来参与训练营。如果你发现你的公司没有组织良好的工程师训练营,那就创建一个吧。与管理层讨论你的想法,并寻找到愿意帮忙的热情的队友。
  • 参与工程师训练营并做出贡献将有助于你的职业生涯。你将会建立关系,提高你在整个组织中的知名度,学习到新技能,并帮助影响公司文化。

主导你自己的晋升

  • 了解晋升的流程,确保你的工作是有价值的和可见的,当你认为自己接近下一个级别时,要大声说出来。
  • 为了获得晋升,你需要知道如何评价你自己,以及晋升流程是什么样的。找到你公司的职业发展阶梯,以确定你在下一个级别所需的技能。与你的管理者讨论一下晋升流程。晋升是每年进行的吗?谁负责评估潜在的晋升?你是否需要一名导师、发起人或晋升资料袋(promotion packet)?
  • 一旦你了解了评估标准和晋升流程,就进行自我评估,并获得他人的反馈。写一份简要的文件,列出你在职业发展阶梯中每一类取得的成就;找出你需要发展的领域;征求你的管理者、同行和导师的反馈意见。告诉人们你为什么要征求反馈意见,这样他们就知道你不只是在寻求安慰。你要挖掘出具体细节。
  • 在你自我评估并收到反馈后,与你的管理者一起回顾所有的内容,并制订一个计划来填补差距。请期待收到一些有关加入工程师训练营或可以锻炼技能的项目和实践的建议。
  • 当人们因为错误的原因期待晋升时,往往会感到失望。一个有前途但未完成的项目还不够,管理者们希望看到结果。技术技能是必要的,但还不够:你必须与他人愉快地合作,为团队目标做出贡献,并帮助到组织。晋升不是时间的线性函数:无论你在你的工作中是 1 年还是 5 年,影响力才是最重要的。当你遇到下一个级别时,期望在晋升前的 3 到 6 个月就达到那个高度,这样你就能证明你能始终如一地满足标准。
  • 说到晋升谈话,时机很重要。在你认为你已经准备好晋升之前,大约在你达到一半的时候,就开始这些谈话。尽早参与,使你和你的管理者都有时间进行协调并解决差距问题。如果你已经拖到了你认为你应该得到晋升,但你的管理者并不同意的时候,晋升谈话就会变成如何解决冲突,而不是提出一个计划。
  • 最后,要注意职业发展阶梯反映的是常见的模式,并不是对每个人都合适。日常工作需要广泛的影响力和“粘合剂”(协调、流程改进、文档、沟通等)。在资深工程师及以下的职级,职级需求往往更多的是以纯编码能力来进行描述的。这就给初级工程师带来了难题,他们虽然承担了一些必不可少的非编码工作,但这些工作并没有附带 Git 提交。这就导致这些工程师花在编码上的时间较少,所以他们的晋升就被推迟了,或者他们被推到了一个不同的角色上,如项目管理。塔尼娅·赖利的演讲和博文建议,如果你的管理者不认为你贡献的价值是晋升的途径,你就不要再做胶水工作了——即使它会在短期内伤害团队。这让人如鲠在喉,而且可能看起来不公平,但是让事情公平的责任在管理层,而不在你。

自我调节

  • 软件领域的工作并不是没有压力的。工作可能很忙碌,竞争很激烈,技术发展又很快,而且总是有更多的东西需要学习。你可能会觉得有太多的事情发生得太快了。新工程师的反应往往是更加努力、工作时间更长,但这是造成倦怠的关键因素。休息一下,短暂脱离,不要让自己过度劳累。
  • 即便你有一张健康的工作时间表,每月工作的劳累也会让你疲惫不堪。利用年假和学术休假来短暂脱离。一些工程师喜欢在年终时休一个长假,而一些人则每季度休一次假以避免疲劳。找到适合你自己的方式,但不要让假白白浪费。大多数公司都规定了你可以累积的最多的年假天数,一些公司还提供学术休假,通常是 1 到 3 个月的延长休息时间,以供你探索和更新。