我一年前开始在彭博社全职工作。从那以后我就一直在想这篇文章。我想象自己能够在时机成熟时将自己的想法倾诉在纸上。但就在最近一个月,我才意识到这并不容易:随着工作的推进,我忘记了很多刚刚学到的东西。这些事情的迅速内化使我的大脑开始欺骗自己,让自己认为我已经掌握了我记得如此清楚的知识,或者我从未听说过实际上被遗忘的事情。因此,我开始保留自己的日志。每当遇到有趣的情况时,我都会记录下来。感谢坐在我旁边的资深软件工程师,我可以仔细观察他们在做什么,和我有什么不同。我们会经常结对编程,这样可以大大降低工作难度。此外,在我们的团队文化中,“窥探”别人的编码过程并不丢人。每当我觉得有什么有趣的事情即将发生时,我总是迅速转身查看。这种敏感度让我能够很快弄清楚事情的来龙去脉。下面是我在一名高级软件工程师身边工作了一年所学到的一些重要经验教训。如何编写代码命名我在工作中遇到的第一个任务是开发ReactUI。当时我们有一个主要组件来容纳所有其他组件。我喜欢为我的代码添加一点幽默感,所以我将它命名为GodComponent。但正是在代码审查期间,我才意识到为什么命名如此重要又如此困难。计算机科学中存在两大问题:缓存失效、命名和缓冲区溢出错误。---LeonBambrick我命名的每一段代码都包含一个隐藏的含义。神组件?这个组件的意思就是我会把所有不知道放哪里的组件放在这里。它拥有一切。如果我将它命名为LayoutComponent,我稍后会意识到它所做的是布局赋值,它不包含任何状态。我发现的另一个经验是:如果太大了,像这里说的LayoutComponent里面包含了很多业务逻辑,那我就会意识到是时候重构了,因为从名字上可以看出业务逻辑不属于这里。但是使用GodComponent这个名字,我们无法判断业务逻辑出现在这里是否正常。如何命名集群?最好在服务运行后对集群进行命名,然后随着运行内容的变化重新调整名称。最后我们用自己的团队名完成了集群命名。函数命名也是如此。doEverything()是个坏名字,它会带来严重的后果。如果函数完成了所有事情,那么将很难测试函数的某些部分。而且不管这个功能有多大,我们都会觉得很正常,毕竟它的名字叫“一切”。所以,最好的办法当然是改名重构。然而,还有另一种类型的问题,我们在命名时也必须考虑到。如果名称的含义过于具体而忽略了一些细微差别怎么办?例如,在SQLAlchemy中调用session.close()时,关闭会话不会关闭底层数据库连接。(我应该在手册之外处理这个错误,这在调试部分有进一步解释。)在这种情况下,我们可以考虑像x、y、z这样的名称,而不是count()、close()、insertIntoDB(),从而避免赋予它隐含的含义。过于具体会迫使我们在后续的维护中费力地检查这些功能是用来干什么的。最后,当时我并没有想到,命名是一件值得一提的大事。遗留代码和下一个开发者你是否曾经面对一段代码而感到困惑?他们为什么这样写?这根本没有意义。我很“幸运”地接管了遗留代码库。其中,有类似“与穆罕默德确认情况后,取消评论”的说明。谁说的?穆罕默德是谁?对此,我们不妨来个角色互换——考虑下一个接手我写的代码的开发人员。他们也会觉得我的代码很奇怪。同行评审可以很好地解决这个问题。这让我想到了环境原则,即:了解团队开展工作的实际环境。如果我跑出去做其他事情,然后再回来,我也可能无法重新建立这个上下文。我坐下来说,“我在想什么?这没有意义……哦等等,我就是这么做的。”文档和代码注释对于此提醒非常重要。文档和代码注释文档和代码注释的要点是维护上下文和共享知识。正如李在《如何构建好软件》中所说,“软件的主要价值不在于生成的代码,而在于开发人员在生成代码的过程中积累的知识。”“软件的主要价值不在于生成的代码,而在于开发者在生成代码的过程中积累的知识。-Li我们有一组似乎从未使用过的API端点的随机客户端。那么我们应该删除它吗?这毕竟也是技术债。但如果我告诉你,每年在一个特定的国家/地区,有10个记者会向这个端点发送新闻,那又怎样?我们是如何测试的?没有文档(实际上没有),我们找不到答案。所以我们删除了端点,并在相应时间点发现了问题——10名记者无法发送10个重要故事,因为端点不再存在。知道产品的成员离开了团队,现在端点只能通过代码中的注释来解释从这件事中,我意识到文档是每个团队都在努力解决的问题,但很难有效.除了代码文档,与代码相关的流程也有类似的情况。今天,我们还没有找到完美的解决方案。如果原子提交一定要回滚(回滚的需求迟早会出现,我们会在测试部分讨论),这个提交有意义吗?删除垃圾代码时,放心删除Crap或过时的代码总是让我不舒服。我总觉得过去的工作有一些神圣不可侵犯的东西。我当时想,“他们在写的时候一定已经考虑过了。“这是一种传统的理解方式,它与第一性原理相冲突。出于类似的原因,我每年在做代码审查和清理时也遇到了很多麻烦。这个坏习惯让我吃了不少苦头。苦。我已经试过tweakingcodeissues,一些老会员习惯绕过这些代码。但是delete,delete听起来更严重。一个永远不会用到的if语句,一个永远不会用到的函数,会在我的完全不完善一条命令就消失了。因此,我更有可能在上面覆盖自己的功能。这并没有减少技术债务,反而增加了代码的复杂性和误导性。这样一来,后继者会更难以有意义的方式将各个部分拼凑起来。我现在采取的方式是:总会有我们不理解的代码,总会有我们永远不会使用的代码。删除我们永远不会使用的代码,但要警惕你看不懂的代码CodereviewCodereviewisan学习的重要组成部分。review的过程就是从写代码到理解如何更好地写代码的反馈循环。我们自己的编码思想,您如何看待与其他人不同的编码?我在每次代码审查时问自己,“他们为什么要这样做?”如果我找不到合理的答案,我会与他们面对面交谈。过渡的第一个月后,我开始疯狂地在同事的代码中查找错误(当然,他们不会放过我)。这真的很疯狂,它让判断变得有趣——或者说是一个游戏,一个提高我们编码技能的小游戏。我的要点:在理解代码的作用之前不要轻易断言。测试我真的很喜欢测试这份工作,事实上,我什至不会在没有测试的情况下直接在代码库中编写代码。如果你的整个应用程序只需要执行一个任务(我在学校的实验项目就是这种情况),那么手动测试就可以了,我一直习惯于这种方式。但是当应用程序包含数百个函数时会发生什么?我不想花很多时间一个一个测试,我也知道我肯定会忘记一些需要测试的部分。这绝对会是一场噩梦。这时候,我们就应该要求一个测试自动化的解决方案。在我看来,测试几乎就像记录文档一样。测试的过程就是记录我对代码的假设是否正确的过程。这些测试告诉我我(或编写代码的开发人员)如何期望代码运行以及我认为它可能在哪里出错。所以现在,在编写测试时,我牢记两点:演示如何使用我正在测试的类/函数/系统。显示我认为可能出错的部分。第一个相信很多朋友都懂。毕竟在大多数情况下,我们需要测试的其实是行为,而不是实现。但我个人总是忽略第2个,这是bug可能出现的地方。因此,每当我发现错误时,我都会确保代码修复记录了相应测试(也称为回归测试)中错误可能出错的其他方式。当然,编写这类测试本身并不能提供代码质量,只有实际编写代码才能真正影响质量。但是我从阅读测试结果中获得的见解确实帮助我编写了更好的代码。这就是测试的宏观意义。此外,测试还肩负着另一个重要使命:确定部署环境。你可能有完善的单元测试,但如果你没有系统测试,就会出现以下情况:锁是好是坏?对于经过良好测试的代码也是如此:如果您的机器上没有它需要的库,您的代码就会出错。您开发的机器环境。(“在我的机器上一切正常!”)您测试的机器环境。(可能是你开发的同一台机器。)最后,你部署的机器环境。(一定要换另一台机器。)如果测试机器和部署机器之间的环境不匹配,通常是出了问题。而这正是部署环境的全部内容。我们在自己的机器上使用docker构建本地开发环境。这个开发环境安装了一套库(和开发工具),我们以此为基础安装已经写好的代码。与其他依赖系统相关的所有测试都在这里完成。然后是betatesting/staging环境,它与生产环境完全一样。最后是生产环境,也就是负责运行代码和服务实际客户的机器。基本思想是尝试捕获单元和系统测试中未出现的错误。例如,请求和响应系统之间的API不匹配问题。我想个人项目或小型企业的情况可能会有所不同,毕竟不是每个人都有资源来建立自己的一套基础设施。不过,如果你愿意使用AWS、Azure等云服务,这里提到的方法还是适合你的。您可以为开发和生产环境设置单独的集群。AWSECS使用docker镜像部署,环境相对一致。棘手的部分是如何与其他AWS服务顺利集成。例如,我们是否从正确的环境中调用了正确的端点?您甚至可以更进一步:为其他AWS服务下载备用容器镜像,并使用docker-compose命令设置完整的本地环境。这加快了反馈循环。这样,当我的副项目启动并运行时,我可以获得更多经验。去风险去风险是在代码部署期间尽可能降低风险级别的艺术。那么,我们可以做些什么来防止灾难性后果呢?如果我们要推出breakingchange,如果出现问题,如何尽可能保证业务不受到严重影响?“我们不需要在整个系统范围内推出所有新变化!”哦是的...对不起,我没有想到这一点。设计很多朋友可能会问,写完代码测试完了为什么还要放设计呢?嗯,在实际过程中设计可能是超前的,但是如果不在当前环境下进行编码和测试,我很难设计出一个能够完美适应特定环境的系统。在设计系统时,我们需要考虑很多问题,包括:资源使用情况是什么?有多少用户?预计用户增长速度有多快?(这将直接决定以后有多少数据库行)以后可能会遇到哪些坑?我需要将这些转换成一个名为“需求摘要”的列表。目前,我还没有积累足够的相关经验。按照计划,我明年的工作内容将着重解决这个问题。这个过程有点违背敏捷原则——在开始实施之前我们可以做出多少设计判断?这是一个权衡的问题,我们需要选择在什么时间点做什么。我们什么时候应该深入挖掘,什么时候应该退后一步进行计划?当然,这里收集的需求不需要也不可能是真正全面的。我认为将开发过程纳入设计考虑因素也是完全可能的,例如:本地开发将如何进行?我们如何打包和部署?我们如何进行端到端测试?我们如何对这项新服务进行压力测试?我们如何管理机密信息?我们如何实现CI/CD集成?我们最近为BNEF开发了一个新的搜索系统,这项工作也给了我们很大的启发。我们必须设计本地开发流程,考虑DPKG方法(打包和部署),并对敏感信息保密。那么为什么将机密信息引入生产环境会导致问题呢?我们不能直接在代码中添加,否则任何人都可以直接看。它应该是12因素应用程序所要求的环境变量吗?这确实是个好主意,但我们如何实现呢?(每次机器启动填充环境变量时都要攻击生产机器,这绝对是一件令人头疼的事情。)将它部署为一个秘密文件?那么这个文件是从哪里来的呢?又该如何填写?最后,整个过程当然不可能靠人工来实现。总而言之,我们使用具有角色访问控制的数据库(只有我们的机器和我们自己可以与数据库通信)。我们的代码在启动时从这个数据库中获取秘密。这部分信息可以在开发、公测、生产环境之间顺利复制,并各自保存在对应的数据库中。这里还要提一点,AWS等各个云服务商提供的具体解决方案可能不尽相同。您不必太担心机密信息。获取一个角色帐户,在UI中输入秘密,并确保您的代码在需要时获取其内容。这些服务可以显着简化整个过程,但之前的探索并没有白费——我很高兴我能真正理解和欣赏这个简单的解决方案。考虑到维护需求来设计系统是令人兴奋的,但是维护呢?恐怕一点成就感都没有。在维护系统的过程中,我想到了这样一个问题:为什么要对系统进行降级,如何实现系统降级?第一部分的答案是因为总有一些人不喜欢丢弃旧的部分并添加新的部分。恐古蔑今,至少我有这样的毛病。至于第二部分,答案是我们在设计系统的时候提出的最终目标,以后可能就不再适用了。随着系统的演进,它的使用方式很可能会与设计假设发生冲突,这意味着我们最初提出的任何预期要求都不再有效。这时候,我们就需要退一步,把那些不再适用的部分剥掉。目前,我知道至少三种降低降级率的方法。保持业务逻辑和基础设施彼此分离:一般来说,需要降级的是基础设施的部分——例如增加的使用率、过时的框架、零日漏洞等。围绕维护需求设计流程。新代码以与旧代码相同的方式更新,以防止新旧代码之间的差异并确保代码作为一个整体保持“现代”。始终坚持删除所有不必要/过时的代码。部署我更喜欢将功能捆绑在一起还是一个一个地部署它们?这取决于现有流程,但如果答案是捆绑部署,那么这可能会导致后续问题。我们需要在这里回答的问题是,为什么我们要捆绑部署功能?是因为部署太花时间了吗?是因为codereview比较难吗?不管是什么原因,我们需要解决瓶颈本身而不是妥协部署方法。捆绑方式至少会带来以下两个缺点。如果其中一个函数发生错误,它将阻止另一个函数的执行。这会增加风险水平,或出现问题的可能性。接下来,不管你选择哪种部署方式,你一定希望你的机器能像牛一样勤快,而不是像宠物一样勤快。机器必须努力工作,我们知道每台机器上运行的是什么,以及当它出现故障时如何恢复。如果发生中断,我们不会感到沮丧-只是开始一个新的。这些设备应该像放牧的牛羊,而不是需要细心照料的小猫小狗。当出现问题时以及出现问题时-迟早会出现问题-我们的黄金法则是尽量减少对客户的影响。当出现问题时,我的第一直觉是修复它。但事实证明,这并不是最有效的处理方式。相反,哪怕只是一个小问题,最高效的方式就是选择回滚。返回到以前的工作状态会缩短客户无法使用该服务的时间窗口。只有这样,我们才能找到错误并安心修复。对于集群中的“故障”机器,我们应该首先将其脱机并将其标记为不可用,然后再尝试找出它的问题所在。我发现这确实违反直觉,我的直觉总是让我远离最佳解决方案。我认为正是这种本能驱使我走上了修复错误的漫长道路。有时问题的根本原因是我写的代码有问题,我会深入我写的第一行代码。这有点像深度优先搜索的过程。如果结果是配置更改,而我没有及时调整功能本身,我会非常生气。因为这个错误太低级了,所以不应该发生。从那以后,我的经验就是先做一轮广度优先搜索,再做深度优先搜索,暂时不去触碰最上面的节点。我可以利用手头的资源确定哪些问题?机器还在运转吗?安装的代码是否正确?配置到位了吗?代码是否正确使用特定配置,例如代码中的路由?架构版本是否正确?最后看一下代码内容。我们原本以为机器上没有正确安装nginx。但事实证明,只有配置文件被设置为false。*当然,大多数情况下没必要这么麻烦。有时仅错误消息就足以帮助我快速找到有问题的代码。当我找不到问题时,我会尝试通过对代码的更改来逐步查找可能的根本原因。更改的次数越少,您发现真正问题的速度就越快。总之,请尽量让推理过程有迹可循,跳太多只会漏掉蛛丝马迹。我现在还记得我花了一个多小时解决了几个错误:问题是什么?一般是一些低级的东西忘记检查了,比如设置路由,确保schema版本和服务版本匹配等等。这只是说明我对自己使用的技术栈还不够熟悉,所以我需要通过犯错来获得经验。最后,我可以凭直觉告诉我为什么代码不起作用。战争故事一方面是关于调整参数和查看统计数据,另一方面是解决潜在问题的根本原因。如果没有战争故事(令人难忘的经历,通常涉及危险、困难或风险的元素),这篇文章将如何完成?我喜欢回顾这些经历,分享会马上开始。这是一个关于搜索和SQLAlchemy的故事。在BNEF,我们处理大量由分析师撰写的研究报告。每次发布报告时,我们都会收到一条消息;一旦我们得到消息,我们就通过SQLAlchemy去数据库,获取我们需要的所有信息,转换它,并将结果发送到一个solr实例进行索引。但是这时候,我们发现了一个奇怪的AFbug。每天早上,连接数据库的操作都会失败,提示“MYSQL服务器不存在”。有时甚至在下午也会发生这种情况。我先检查了一下,因为下午时间使用率最高。没问题,机器上一切正常。我们全天向数据库发出数千次请求,没有失败。那么,为什么这么低的负载强度会出现问题呢?哦,也许我们在交易结束后不关闭会话?所以失败实际上是来自同一个会话,但是下一个请求在很长一段时间后才出现,这会导致超时——因为这次服务器宕机了。快速浏览一下代码,我们通过上下文管理器检查每次在exit()上调用session.close()的读取。经过一整天的排查,没有发现任何问题。第二天早上,我又遇到了同样的情况。错误后一秒,其他三个索引请求成功。这显然是会话未正确关闭的典型情况。好吧,我相信每个人都可以编出下面的完整故事。SQLAlchemymysql语言中的Session.close()无法关闭底层数据库连接,除非使用NullPool。是的,这就是解决方法。这个错误的原因很简单,我们不在晚上和午餐时间发布研究报告。此外,我们还学到了另一个教训——大多数堆栈溢出问题的答案(我从谷歌上查过)是bug本身会调整会话的超时时间,或者控制每条SQL语句可以发送的数据量参数。这些对我来说都没有意义,因为它们与问题的根源无关。我检查了查询大小是否在限制范围内,并且由于会话本身正在关闭,因此没有超时。我们当然可以通过将超时从1小时增加到8小时来快速“修复”此错误。但这显然不能解决问题,而且到了第二天早上,又会有研究报告导致的错误再次出现在我们面前。一方面是调整参数和查看统计数据,另一方面是修复潜在问题的根本原因。这就是我们的日常生活。监视我从没想过监视会在我自己的控制之下。坦率地说,在接受全职编码职位之前,我从不关心系统维护。我只是搭建系统,用了一个星期,然后换了一个系统。现在,我每天都使用两个系统,一个监控机制很好,另一个监控机制很差。通过实际体验,我感受到了监控的重要性。毕竟,如果我知道问题,我怎么能解决它呢?最坏的情况,连客户都发现了bug,我还蒙在鼓里。“我这是在做什么?!我自己的系统出了问题都不知道?”我认为监控机制主要由三大部分组成——日志记录、指标和警报。日志记录以代码的形式存在,类似于人类的记录,是一个增量的过程。我们可以找到需要监控的内容,记录下来,同时运行系统。随着时间的推移,我们可能会发现自己遗漏了解决错误所需的一些信息。现在是调整日志记录的好时机——我们忘记记录了哪些重要的东西?在我看来,最重要的是直观地理解什么是值得记录的。作为我的观察对象,他(标题中的高级软件工程师)和我对录音服务的看法截然不同。我以为记录请求-响应就足够了,但他反而列出了很多指标,比如查询执行时间、代码中的一些特定内部调用、何时轮换日志等。显然,没有日志记录作为参考,我们几乎不可能做任何调试工作——如果不知道系统当前的状态,重建系统自然是无稽之谈。指标可以从日志中提取或在代码中单独创建。(例如将事件发送到AWSCloudWatch和Grafana)。您可以设置自己的指示器并在代码运行时发出相应的数字。警报是将所有内容结合在一个良好的监控系统中的重要粘合剂。如果一个指标代表当前生产中的机器数量,那么下降到50%是一个严重的危险信号——出现了严重的错误。失败计数超过阈值?会有新的警报提醒我们。这样我就可以安心睡觉了,因为我知道即使出了什么问题,系统也会第一时间提醒我~对了……而且这中间还隐藏着一个重要的习惯。修复BUG,不能只关注如何解决问题,为什么不早点发现呢?报警是否及时提醒?如何更好地设置监控以防止出现此类问题?我仍然没有弄清楚如何监视用户界面。目前的组件选项还不能理解问题出在哪里。此外,还有相当多的客户报告的问题——这里肯定有改进的余地。经过一年的总结,收获颇丰。当我开始写这篇文章时,我很高兴接受了这份新工作。在写作的过程中,我也深刻体会到自己的成长。希望你也能从本文中得到一些启发!我很幸运能成为一个伟大团队的一员——我们做了很多编码,我们每天都玩得很开心,我们从头开始设计系统,并且我们与许多其他团队合作。今年,我身边又多了一位高级开发人员。我期待学习更多重要的课程。谢谢你,我的团队!优秀的工程师设计的系统更健壮,更易于他人理解。这将产生乘数效应,帮助同事更快、更可靠地构建他们的工作。-*如何构建好的软件
