成为软件工程师的基本知识(我从资深软件工程师学到的避坑大法)

选自neilkakkar

作者:Neil Kakkar机器之心编译参与:陈韵莹、一鸣

本文是彭博社的一位开发者所写的文章,介绍了从一位资深工程师同事的身上学到的一些开发经验。

过去一年中,我坐在一位资深的软件工程师旁边,可以仔细地观察他是怎么工作的。我们两人经常共同编程,使得这项观察更为容易。此外,在团队文化中,从背后窥探写代码的人并不令人反感。以下是我所学到的:

编写代码

如何命名

我首先着手的是 React UI。我们有一个主要组件来放置其他所有的组件。我喜欢在代码里加点幽默感,因此我想要将它命名为 GodComponent。当进入代码审查环境的时候,我才明白为什么命名这么难。

在计算机科学里有两个难题:内存不足、命名、以及差一(off-by-one)错误。——Leon Bambrick

我每个命名的代码段都有隐藏含义在里面。GodComponent 是所有我不必费心去寻找合适位置来存放那些垃圾的地方,它可以容纳所有东西。如果我早早把它命名为 LayoutComponent,之后的我就会发现它所做的就是分配 layout,没有状态。

测试

我非常喜欢测试,以至于如果没有测试就将代码写入代码库我会感到非常不舒服。

如果整个应用程序只做一件事(就像我所有的学校项目),那么手动测试是可以的。但是如果该应用程序可完成 100 种不同的功能,那该怎么办呢?我不想花半个小时来测试所有的功能,何况有时候还会忘记一些需要测试的地方。

所以就出现了自动化测试。

我认为测试是一种文档,是对代码假设的文档。测试会告诉我(或我之前的人)他们预想代码是如何工作的,以及他们预期哪里会出错。

所以,当写测试时,我会记住:

  1. 记录如何使用测试时用到的类/函数/系统。
  2. 记录我所想到的会出错的地方。

在大多数情况下,以上的结论是在我在测试而不是实现的过程中想到的。

以下是我在 Google 卫生间小休时学到的例子:

  • 我在 #2 中遗漏了一些东西,那里是 bug 出现的地方;
  • 所以每当发现 bug 时,确保修复 bug 的代码也有相应的测试(称为回归测试),用于记录信息:这里可能出现另一种错误。

仅仅编写这些测试并不能提高我代码的质量,而编写代码却可以。但是我从阅读测试代码中获得了写更好代码的直觉。

但是,并不只有这一种测试,这就是为什么有部署环境测试的原因。

你可以有完美的测试单元,但是如果没有系统测试,就会出现以下的情况:

成为软件工程师的基本知识(我从资深软件工程师学到的避坑大法)(1)

这同样适用于已经测试好的代码:如果你机器上没有你需要的库,你会崩溃。

为了测试你需要:

  • 有一台你用于开发的机器;
  • 有一台你用于测试的机器;
  • 最后,有一台你部署的机器(请不要用与开发程序使用同一台)。

如果测试和部署机器之间的环境不匹配,你就遇到麻烦了。所以这里就出现了部署环境。

  • 我们先有本地开发环境,在我的机器上是 docker;
  • 然后有服务器上的开发环境,机器上安装了一系列的库(和开发工具),我们在安装了代码的机器上进行开发。其他相关依赖的测试都可以在这里进行;
  • 接下来是 beta/stage 环境,它与生产环境完全一样;
  • 最后是生产环境,它是代码运行和服务于实际客户的机器上的环境。

这里的想法是尝试捕获单元和系统测试无法捕获的错误。例如,请求系统和响应系统之间的 API 不匹配。个人项目与小公司的情况大不一样。不是每个人都有资源来搭建自己的设备。然而,这个想法仍适用于像 AWS 和 AZURE 这样的云供应商。

你可以为开发和生产设置分开的集群。AWS ECS 使用 docker 镜像来部署,所以即使跨环境事情也会相对平稳。棘手的一点是其他 AWS 服务之间的集成。你是否可以在正确的环境中调用正确的终端呢?

你甚至可以更进一步:下载其他 AWS 服务的备用容器镜像并使用 docker-compose 来配置本地完整的环境。它会加速反馈循环。

设计

为什么我要将设计放到写代码和测试的后面呢?设计本应该在第一位,但是如果我没有在环境中写代码和测试,我可能会不擅长设计一个遵循环境特性的系统。

在设计系统时,有很多事情需要考虑:

  • 使用编号是多少?
  • 有多少用户?预期增长是多少?(即需要使用多少数据行)
  • 未来可能出现的问题是什么?

我需要把它转成一个名为「需求收集」的合理清单。这个过程有点与灵活性的原则相悖——在开始系统开发之前,你可以设计多少部分呢?但是这是一种平衡——你需要选择什么时候做什么。当然仅仅收集需求并不是所有需要考虑的事情。我认为,在设计中包含了开发的过程也是值得去做的。例如:

  • 本地开发如何运作?
  • 怎么打包和部署?
  • 如何进行端对端的测试?
  • 怎么对新的服务进行压力测试?
  • 怎么管理机密信息?
  • CI/CD 集成?

我们最近为 BNEF 开发了一个新的搜索系统。做这件事真的很棒。我开始设计本地开发,学习 DPKG(打包和部署)和试图解决部署机密信息的问题。

谁会想到对产品中的机密信息进行部署会变得如此棘手呢?

  1. 你不能将这些信息存到代码中,因为这样任何人都能看得到。
  2. 把它们作为环境变量?这是一个好主意。但你怎么把它们放在那里?(每次机器启动时访问 PROD 机器来填充环境变量是一件痛苦的事情)
  3. 部署为机密文件?文件从哪里来呢?怎么进行填充呢?

而且我们不想进行手动操作。

最后我们使用了一个有角色访问控制的数据库(只有我们的机器可以与数据库对话)。我们的代码在启动时从这个数据库中获取秘密数据。这个能在开发、测试和产品之间很好地复制——在各自的数据库中都有机密。

同样的,对于像 AWS 这样的云供应商,这可能非常不同。你不必考虑太多机密。获取你角色账户,在用户界面中输入机密数据,在需要的时候你的代码会找到它们。它简化了很多时间,这非常酷,而我很高兴有经验领会这种简易性。

设计时考虑维护需求

设计系统是件令人兴奋的事。维护系统呢?就没那么有趣了。

我在维护过程中遇到了这个问题:系统为什么会降级,以及如何降级?

有两个原因可以解答为什么系统也会有降级的时候:

首先,系统不应当舍弃旧的东西,而是在已有的基础上增加更多功能。系统更新倾向于增加而不是删除。

其次,带着最终目标来设计。一个进化到做不该做的事情的系统和一个从零来设计做同样事情的系统一样,没有用。这是一种系统的倒退。因此需要对系统进行降级。

现在我知道至少三种降低降级机率的方法:

  • 将业务逻辑和基础设施分开:通常是对基础设施降级——当使用量增加、框架过时、出现零日漏洞等情况下;
  • 围绕系统维护建立流程。对旧的和新的组件都使用相同的更新。这可以防止组件之间出现差异,保持整个代码「现代化」;
  • 确保一直修剪你不想要的/旧的东西。

部署

将功能进行捆绑部署还是逐个部署呢?如果答案是将功能捆绑在一起,则会出现问题。

接下来要问的问题是:为什么想要把功能进行捆绑呢?

  • 部署是否花费过多时间?
  • 代码审查是否容易进行?

不管是什么原因,这是需要修复的流程瓶颈。

捆绑功能部署至少有两个问题

  1. 如果一个功能中有 bug,将妨碍另一个功能执行;
  2. 增加整体出错的风险。

然后,无论你选择什么部署过程,你总是希望你的机器像一头牛而不是像宠物一样。它们并不珍贵。你知道每台机器上运行的是什么,以及如何在死机的情况下重新创建它们。当一台机器死机时,你不会心烦意乱,你只需要启动一台新机器。你像牛一样放养它们,而不是像宠物一样养着他们。

程序出错的时候

当事情出错时,而且一定会有出问题的时候,黄金法则是将对客户的影响最小化。

当事情出了差错,我自然倾向于赶快解决 bug。事实证明,这并不是最理想的解决方案。与其修复哪里错了,即使只是「修改一行」,所做的第一件事应该是回滚版本。回到之前的工作状态,这是让客户恢复工作最快的方法。

过了这个时候,才应该看看哪里出了问题并修复那些 bug。

在你的集群中出现一台「垮掉」的机器也应当是同样的做法——在试图找出机器出了什么问题之前,先把它停了,并标记它不可用。

首先找 bug 这种本能会引导我走上解决 bug 的漫长旅途,反而偏离了让客户先恢复工作这一理想的目标状态。有时候,我觉得它没有工作的原因是因为写的代码有问题,而仔细阅读每一行代码后会陷入混乱,像是一种深度优先搜索。

之后,我的启发是,首先开始广度优先搜索,然后再深度优先搜索,去除最顶端的节点。能否用已有的资源确认:

  • 机器启动了吗?
  • 是否安装了正确的代码?
  • 配置是否正确?
  • <代码特定配置>,像代码中的路由是否正确?
  • 模式版本是否正确?
  • 然后进入代码。

在某次出错的问题上,我们以为机器上没有正确安装 nginx,但结果是配置被设置为了 false。

当然,我不需要总是这样做。有时候错误信息已经足以减少需要搜索代码的区域。而且当我无法解决这个问题时,我尝试并持续修改代码以将问题降到最低。修改的次数越少,我就能越快地处理实际问题。

但是我现在还是会记录花了 1 个多小时来解决的 bug:遗漏了什么?这通常是一些我忘记检查的愚蠢错误,比如像设置路由、确保模式版本和服务版本匹配等。这是熟悉使用的技术堆栈的另一步,而且只有经验会告诉我为什么系统无法运行。

监控

这是我以前从未想过去做的事。说句公道话,在全职编码之前,我从没维护过系统。我只是搭建它们,使用 1 个星期后然后进行下一项工作。

有两个系统,一个有良好的监控,另一个并不那么好。我逐渐非常喜欢监控。如果我不知道 bug 在哪我就不能修改错误。其中一种最糟糕的感觉是从客户那里知道有 bug。

「我做了什么?!我甚至不知道我的系统出了什么问题?」

我认为监控由 3 个部分组成——日志、衡量标准和警报。

日志

以代码中进行日记记录就像人写日志一样,是一个进化的过程。

你要找到你可能需要监控的东西,日志记录下来,运行系统。一段时间后,你会发现你没有足够信息来解决的 bug。这是增强日志记录的好时机——你的代码少了些什么?

我想你会凭直觉地知道什么东西很重要需要记录,但是在我们的服务器中我和资深软件工程师所记录的东西有很多不同。我认为只要请求-相应日志就足够了,但是他会有更多的记录内容,比如查询执行时间、代码进行的一些特定的内部调用,以及何时转储日志。一切都已经解决了。

几乎不可能在没有日志的情况下进行调试——如果你不知道系统的状态,你怎么重新创建它呢?

衡量标准和惊爆

衡量标准可以源于日志,也可以独立于日志(例如向 AWS CloudWatch 和 Grafana 发送时间)。你可以决定你的衡量指标并在代码运行时发送数字。

警报是把所有东西整合到一个的强大监控系统的粘合剂。如果一个衡量标准是当前产品中运行的机器数量,当这个数字降到 50% 时,这是一个很好的警报——你知道有什么出错了。

失败计数高于某个阈值时?是的,又一个警报。

这里暗示了另一个需要养成的习惯。当你修复 bug 时,你不仅仅关注如何修复 bug,而是你为什么不早点发现它呢?是否有布置警报?如何能够更好地监控来避免类似的问题?

我还不知道如何监控 UI。即使吧组件测试到位,也还不足以了解出错的情况。这些错误通常是由客户来告诉我们的——这看起来不太对劲。

总结

在过去的一年里,我学到了很多东西。当我对这篇文章进行回顾时,我能够更好地体会到我的成长。希望你也可以从这里得到一些东西!

原文链接:https://neilkakkar.com/things-I-learnt-from-a-senior-dev.html#when-things-go-wrong

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页