在进入本文内容之前,请查看上周推出的 my new book 。
几年前,我维护了一个数据库驱动的系统,并遇到了一个怪异的生产错误。我正在阅读的列的值是无效的,但这在代码中不允许,没有任何位置可以为null。该数据库腐败的方式很差,我们没有任何事情要继续前进。是的,有日志。但是由于隐私问题,您也可以记录所有内容,即使我们可以,我们如何知道要寻找什么?
程序失败,这是不可避免的。我们努力减少失败,但失败会发生。我们也有另一项努力,并且引起了人们的关注:失败分析。有一些最佳实践和常见方法,最著名的记录。我经常说,在日志之前是认知的调试,但是我们如何创建一个更容易调试的应用程序?
我们如何构建系统,以便当它失败时,我们将有一个问题的线索?
一个常见的军事公理会变得困难训练使战斗变得容易。假设开发阶段是训练,我们在这里所做的任何工作都将变得更加困难,因为我们不知道我们在生产中可能面临的错误。但是,当我们到达生产准备时,这项工作很有价值。
此准备超出了测试和质量检查。这意味着在发生问题的那一刻准备我们的代码和基础架构。这是测试和质量检查使我们失败的地方。根据定义,这是为意外准备的准备。
定义失败
我们首先需要定义故障范围。当我谈论生产失败时,人们会自动假设崩溃,网站下降和灾难级事件。实际上,这些很少见。这些案件中的绝大多数是由OP和系统工程师处理的。
当我要求开发人员描述他们遇到的最后一个生产问题时,他们经常跌跌撞撞并可以回忆起。然后在讨论和查询时,似乎在生产中,客户确实报告了他们最近处理的一个错误。他们必须以某种方式在本地复制它或查看信息以修复它。我们不认为这样的错误是生产错误,但它们是。重现现实世界中已经发生的失败的需求使我们的工作变得更加艰难。
如果我们只能通过观察其失败的方式,就可以理解问题,该怎么办?
简单
简单性的规则很普遍且显而易见,但人们用它来争论双方。简单是主观的。这个代码块很简单吗?
return obj.method(val).compare(otherObj.method(otherVal));
还是这个块很简单?
var resultA = obj.method(val);
var resultB = otherObj.method(otherVal);
return resultA.compare(resultB);
就代码行而言,第一个示例似乎更简单,实际上许多开发人员会更喜欢这一点。这可能是一个错误。请注意,第一个示例包括单行中的多个故障点。对象可能无效。有三种可能失败的方法。如果发生故障,则可能不清楚哪个部分失败。
此外,我们可以正确记录结果。我们可以轻松调试代码,因为我们需要介入单个方法。如果在方法中发生故障,即使在第一个示例中,堆栈跟踪也应将我们带到正确的位置。那足够了吗?
想象一下我们在那里调用的方法是否改变了状态。 obj.method(val)
是否在otherObj.method(otherVal)
?
在第二个示例中,这是立即可见的,很难错过。此外,可以检查中间状态并记录为结果和结果的值。
让我们检查一个常见的例子:
var result = list.stream()
.map(MyClass::convert)
.collect(Collectors.toList());
这是一个非常常见的代码,与此代码相似:
var result = new ArrayList<OtherType>();
for(MyClass c: list) {
result.add(c.convert());
}
在辩论性方面,两种方法都有优势,我们的决定可能会对长期质量产生重大影响。第一个示例中的一个微妙的变化是返回列表无法解码的事实。这是一个福音和问题。当我们试图更改它们时,在运行时列表失败了,这是失败的潜在风险。但是,失败很明显。我们知道什么失败了。
更改第二列表的结果可能会造成级联问题,但也可能只是解决了一个问题而不会失败。
我们应该选择哪个?
仅阅读列表是一个主要优势。它促进了失败原则,这是我们想调试生产问题时的主要优势。快速失败时,我们降低了级联故障的可能性。这些是我们在生产中可能遇到的最严重的失败,因为它们需要对生产中复杂的应用状态有深刻的了解。
构建大型应用程序时,“鲁棒”一词经常被抛弃。系统应该很健壮,但是他们应该在您的代码之外提供的系统应快速失败。
一致性
在我谈论记录最佳实践的讨论中,我提到了一个事实,即我从事过的每家公司都有一个时尚指南,或者至少与众所周知的风格保持一致。很少有人有一个guide for logging,我们应该在哪里登录,我们应该记录什么等。这是一种悲伤的状态。
我们需要比代码格式更深的一致性。调试时,我们需要知道会发生什么。如果禁止使用特定的软件包,我希望这适用于整个代码库。如果不建议使用特定的编码实践,我希望这是普遍的。
值得庆幸的是,使用CI,这些一致性规则很容易执行,而不会负担我们的审查过程。自动化工具(例如Sonarqube)是可插入的,可以使用自定义检测代码扩展。我们可以调整这些工具来强制执行一套一致的规则,以将使用限制为代码的特定子集或需要适当的记录。
每个规则都有一个例外,我们不应该限于过于严格的规则。这就是为什么要覆盖此类工具并将更改与开发人员审查合并的能力很重要。
双重验证
调试是我们圈出错误区域时验证假设的过程。通常,这很快就会发生。我们看到了什么破坏,验证和修复它。但是有时我们会花费大量时间跟踪错误。尤其是一个难以生产的错误或仅在生产中表现出来的错误。
由于错误变得难以捉摸,因此重要的是要退后一步,这意味着我们的假设之一是错误的。在这种情况下,这可能意味着我们验证假设的方式是错误的。双重验证的重点是测试以下假设,该假设使用不同的方法确保结果正确。
。通常我们要验证错误的两侧,例如让我假设我在后端有问题。它将通过数据不正确的前端表达自己。为了缩小错误,我最初做出了两个假设:
-
前端从后端正确显示数据
-
数据库查询返回正确的数据
要验证这些假设,我可以打开浏览器并查看数据。我可以使用Web开发人员工具检查响应,以确保显示的数据是服务器查询返回的内容。对于后端,我可以直接针对数据库发布查询,并查看值是否正确。
但这只是验证这些数据的一种方法。理想情况下,我们想要第二种方式。如果缓存返回错误的结果怎么办?如果SQL做错了假设怎么办?
理想情况下,第二种方法应该足够不同,因此它不会仅仅重复第一种方式的失败。对于前端代码,我们的膝盖反应是尝试使用像卷发这样的工具。那很好,我们可能应该尝试一下。但更好的方法可能是查看服务器上的已记录数据或调用前端的网络服务。
同样,对于后端,我们希望看到从应用程序内返回的数据。这是可观察性的核心概念。可观察的系统是我们可以表达问题并获得答案的系统。在开发过程中,我们应该以两种不同的方式将可观察性水平针对回答问题。
为什么不验证三种方法?
我们不想要两种多种方式,因为这意味着我们观察到太多了,因此,在绩效下降时,我们的成本可能会上升。我们需要将收集的信息限制为合理的金额。特别是考虑到保留个人信息的风险,这是要记住的重要方面!
通常通过其工具,支柱或类似的表面积特征来定义可观察性。这是个错误。可观察性应通过其提供的访问来定义。我们决定要记录什么以及要监视什么。我们决定迹线的跨度。我们决定信息的粒度,我们决定是否希望部署开发人员可观察性工具。
我们需要确保正确观察我们的生产系统。为此,我们需要运行故障场景,甚至可能是混乱的游戏日。在运行此类方案时,我们需要考虑解决出现的问题的过程。我们对系统有什么样的问题,我们如何回答这样的问题?
例如。当发生特定问题时,我们通常想知道有多少用户正在积极修改系统中的数据。结果,我们可以为该信息添加一个指标。
使用功能标志验证
我们可以使用可观察性工具来验证一个假设,但我们也可以使用更具创造性的验证工具。一个意外的工具是功能标志系统。特征标志解决方案通常可以用非常细的粒度来操纵,我们只能为特定用户禁用或修改功能,等等。
。这非常强大,如果特定代码包裹在标志中,我们可以切换一个功能,该功能可以为我们提供特定行为的验证。我不建议在整个代码上散布功能标志,但是拉动杠杆和更改系统中的系统的能力是一种强大的调试工具,通常不足。
虫子汇报
在90年代,我开发了飞行模拟器,并与许多战斗机飞行员合作。他们向我灌输了一种汇报文化。直到那时,我才想到这些事情只是为了讨论失败,但战斗机飞行员在飞行后立即进行汇报,无论是成功还是失败的任务。
我们需要在这里学习一些要点:
-
即时 - 我们需要新鲜的信息。如果我们等待一些事情丢失,我们的回忆会发生重大变化。
-
关于成功和失败 - 每个任务都对与错。我们需要了解出了什么问题和正确的事情。特别是在成功的情况下。
当我们修复一个错误时,我们只想回家,我们通常不想再讨论了。即使我们确实想炫耀,这通常是我们对跟踪过程的破碎回忆。通过公开讨论我们做对与错的事情没有判断。我们可以对我们当前的状态建立了解。然后,这些信息可以用于在跟踪问题时改善我们的结果。
这种汇报可以指出我们的可观察性数据,不一致和有问题的过程中的空白。在此过程中,许多团队中的一个常见问题确实是在此过程中。当提出问题时,通常是:
-
客户遇到的
-
报告支持
-
由OPS检查
-
传递到R&D
如果您在研发中,您将距离客户四步之遥,并收到可能不包括所需信息的问题。完善这些过程不是代码的一部分,但是我们可以在代码中包括工具,以使我们更容易找到问题。一个常见的技巧是在每个异常对象中添加一个唯一的密钥。在失败的情况下,这将一直传播到UI。
当客户报告问题时,很有可能会在日志中包含R&D可以在日志中找到的错误密钥。这些是经常通过此类汇报上升的过程改进的类型。
查看成功的日志和仪表板
等待失败是一个有问题的概念。我们需要定期检查日志,仪表板等。这两者都会跟踪表现出来的潜在错误,但也要了解基线。健康的仪表板或原木看起来像
我们在正常日志中有错误,如果在搜寻错误期间,我们花时间查看良性错误,那么我们会浪费时间。理想情况下,我们希望将这些错误的数量最小化,因为它们使日志更难读取。服务器开发的现实是,我们总是可以做到这一点。但是,我们可以通过熟悉和正确的源代码注释来最大程度地减少在这上花费的时间。
我在记录最佳实践帖子和talk中更详细介绍。
最后一句话
建立代号几年后,我们的Google App Engine Bill突然跳至几天之内触发破产的水平。由于其后端的变化,这是突然的回归。
这是由于数据未经间隔而引起的,但是由于应用引擎在当时的工作方式,因此无法知道触发问题的代码的特定区域。没有能力调试问题,检查问题是否解决问题的唯一方法是部署服务器更新并等待很多
我们通过愚蠢的运气解决了这一问题。缓存我们在每个地方都能想到的一切。直到今天
我所知道的是:
当我决定选择应用程序引擎时,我犯了一个错误。它没有提供适当的可观察性,也没有主要的盲点。如果我花了一些时间在部署之前审查了我会知道的可观察性功能。我们很幸运,但是如果我们做好了更多的准备,我可以尽早节省很多现金。