在「大厂」工作的体验就像当一颗螺丝钉?读过很多软件工程的书却仍会犯下里面的典型错误?感到自己工作效率低下却不知如何改进?要成为一名高效的软件工程师,在理论和实践之间是存在不小的距离需要我们通过广泛学习、勇敢试错和不断反思去跨越的。在 Grow @ Google 这个系列中,我会结合自己在 Google 的工作经历来分享一些经验和教训,帮助大家少走一些弯路。系列中的其他文章请见:
我在 Google 参与的第一个项目主要是修改和运行一个 C++ pipeline 来 backfill1 几年的用户数据。过程如下:
许多人将作为 entry level 工程师在 Google 工作的体验形容为当一颗螺丝钉,而我的感受则是自己所扮演的角色更接近于润滑油一般的存在。一言以蔽之,我所发挥的是「在一个巨大的组织里促成一件事情」的作用。作为一个新人,我需要频繁地借助来自他人的反馈。
为了验证 pipeline 生成的数据是否正确,我们将其与其它数据源进行比较。由于之前存在过的一些 bug 的影响,它们之间并不是 100% 一致的。我们投入了大量的时间和精力去调查数据的不一致,最终找到了超过 10 个 bug,并确认了大约 99.5% 的数据是一致的。
由于部分不一致的数据是对用户可见的,我们需要得到来自 PM 的 approval。这里我们犯的错误是没有及时跟 PM 沟通。后来我们了解到,他们所关心的是在更高的颗粒度上数据之间的差异有多大,并且更侧重于那些用户频繁查询的数据。也就是说,我们闷头所做的调查有很大一部分是无用的。
一个项目往往牵扯到承担不同职能的 stakeholder:工程师、产品经理、销售、客服等等。不同角色的思维模式相去甚远,所关心的问题也往往有比较大的出入。及时 involve 相关的 stakeholder 并明确共识,对个人和整个团队的生产力都有帮助。
《The Effective Engineer》2提到,高效的工程师们有一个共性是会强迫症般地投入时间来缩短 feedback loop,从而达到更快的迭代速度。但一个常见的误区是,在身上已经背满工作时,我们最不想做的一件事情就是给自己再创造更多的工作。如果已经存在一个可用的 workflow,即使中间有可以优化的部分,很多人都会倾向于不加改进地继续用下去。而我也犯了同样的错误。
这个项目是我中途接手的。当时的 pipeline 有一些单元测试,但 SQL query 的部分没有被覆盖。每次对 query 进行修改,我都要手动验证改动前后的 query 返回的结果是否一致,同时还要将 pipeline 提交到 Borg(Google 的集群管理系统)上运行作为 end-to-end test。
这样的重复性劳动不仅占用我的有效工作时间,也更容易出错。而当时的我却觉得为 query 的部分设计自动化测试并不容易,项目本身也是不上生产环境的一次性工作,于是在多次感受到痛点并且被 TL 提了建议以后仍然没有去改进测试流程。事后回想起来,在测试和调试上由于 feedback loop 过长而浪费的时间还是比较可观的。
Google 对于 entry level 工程师的要求中有一条是 recognize and clarify important assumptions before starting to code。持有错误的假设会严重影响工作效率。像前文所说那样建立高效的反馈渠道就有助于及时验证假设是否正确,与此同时我们也要留意思维上可能出现的偏差。
锚定效应是认知偏差的一种,指的是人们在做决策时倾向于过度依赖最初接触到的信息。
我接手项目后提交的第一份代码就是对 query 进行修改。由于在开发早期没有将 pipeline 在大规模的数据上进行测试,我没有意识到自己的改动使得 query 的执行效率下降了几十倍。直到项目的收尾阶段,我们才开始意识到 pipeline 不能很好地 scale,从而开始探索是否有优化的空间。
在这个过程中,我两度尝试优化 query 未果。为什么呢?在我看来,自提交第一份代码以来 pipeline 的性能并没有什么变化,从而完全忽视了第一次改动就是罪魁祸首的可能性。幸运的是,我最终意识到了自己可能已经过于 biased 而不适合继续承担调优这项工作。于是我向 mentor 寻求了帮助,而他只花了一天时间就定位了瓶颈,将 query 提速了 65 倍。
像这样的认知偏差是很难避免的。我们所能做的就是意识到自己的想法可能有偏差,并向他人寻求帮助。
除了 query 不够高效,pipeline 难以 scale 的另一个原因是这个项目所需要处理的数据量本身就过于巨大。最初的设计文档写道,选择使用这个 pipeline 的原因是另一个 team 在一两年前完成了类似的 backfill,因此有许多经验和代码可以重用。然而一个被忽略的事实是:当时的数据量比现在低了两个数量级。
仅仅从项目性质相似和代码重用出发决定使用相同的 pipeline,本质上是基于表面的相似性3而做出草率的决策。具体到这个项目中,虽然事实上的确没有其他替代方案,但一个好的设计应该指出两个应用场景下的关键差异——数据规模。如果从最初就有这样的意识,我们就可以将未知数较多的 scaling 尽早开始探索和实验,而不是在快收尾时才开始投入精力。
Ownership 意味着你要对整个项目的最终交付负责,这在任何一个稍具规模的组织里,复杂程度都是远远超出纯粹的技术/工程因素的。
任何尝试过项目管理的人恐怕都会同意:时间估计是非常困难的,在项目的实际执行中永远会出现意料之外的情况。而我在这个项目中犯的错误,则是在多次遇到意外的情况下依然对剩余所需时间做出了过于乐观的估计。
如果经验告诉你,当你预计一个项目需要 X 天来完成时,实际却往往要花上 1.5X 天,那么下次在做出 Y 天的估计之后就应该将 1.5Y - 2Y 天作为正式的估计数字与其他人进行沟通。
绝大多数时候,stakeholder 关心的并不是你能否在实际可能交付的日期之前交付,而是你能否在他们的心理预期之前交付。如果有足够的能力和精力,按时甚至提前交付所有的项目自然很好。但有时交付日期并不合理,或是突发的客观因素耽误了进度,又或者你想拥有更好的 work-life balance 来避免 burnout。这时候管理他人的心理预期就是一门很有用的学问。
首先,采取比较保守的立场来估计时间。如果保守估计得到的数字无法赶上交付日期,我们需要向 stakeholder 沟通这些信息:
如果能够提供合理和充足的信息,我们通常能说服 stakeholder 接受全部或者部分妥协的方案。这些信息同时也能帮助他们确立清晰的心理预期和做出对应的决策。如果最终成功按时交付,他们也会更为满意。
在成功优化 query 之前,我们一度认为无法赶上交付日期。在 PM 表示不能延期的前提下,我们提出的折中方案是优先为在过去六个月或是一年间活跃的用户进行 backfill,并给出了不同方案所需的时间估计。虽然最终我们在原定的交付日期之前完成了所有用户的 backfill,但这个折中方案也得到了 PM 的认可。
绝大部分工程师都理解定期 prioritize 待办事项的重要性。在一个项目中,如何 prioritize 技术/工程上的 task 是相对简单的,而另外一些 task 则不那么显然或是容易被忽视。比如:
我们应该总是优先做那些可以缩短 feedback loop 或是可以验证假设的事项。对于那些不为我们所控制的事项,也需要尽可能早地发起并定期跟进。
在这个项目中,我们需要不定期地清空临时表。这个改动需要另一个 team 的 approval。第一次我犯了一个错误,在清空的前置事项都做完了以后才发起了改动,结果等了几天才得到 approval。第二次我就吸取了教训,在前置事项完成之前就得到了 approval,从而把浪费的时间减到最低。