您如何重现错误条件并查看应用程序执行时发生的情况?
您如何可视化应用程序的不同并发部分之间的交互?
根据我的经验,对这两个方面的回答如下:
分布式跟踪
分布式跟踪是一种技术,可捕获系统中每个并行并发组件的时序数据,并以图形格式显示给您。并发执行的表示始终是交错的,使您可以查看并行运行的内容和并行运行的内容。
分布式跟踪的起源是(当然)分布式系统,根据定义,该系统是异步的并且高度并发。具有分布式跟踪的分布式系统使人们能够:
a)确定重要的瓶颈,b)获得理想的应用程序“运行”的可视化表示,c)提供正在执行的并发行为的可视性,d)获取可用于评估您的更改之间差异的计时数据系统(如果您拥有强大的SLA,则非常重要)。
但是,分布式跟踪的后果是:
它增加了所有并发进程的开销,因为它转化为更多的代码以可能通过网络执行和提交。在某些情况下,这种开销非常重要-甚至Google都仅对所有请求的一小部分使用跟踪系统Dapper,以免破坏用户体验。
存在许多不同的工具,并非所有工具都可以互操作。OpenTracing之类的标准对此有所改善,但并未完全解决。
它没有告诉您共享资源及其当前状态的任何信息。您可能可以根据应用程序代码和所看到的图形向您猜测,但这在这方面不是有用的工具。
当前的工具假定您有足够的内存和存储空间。托管时间序列服务器可能并不便宜,具体取决于您的限制。
错误跟踪软件
我之所以链接到上面的Sentry,主要是因为它是其中使用最广泛的工具,并且有充分的理由-诸如Sentry的错误跟踪软件会劫持运行时执行程序,以将遇到的错误的堆栈跟踪同时转发到中央服务器。
这种专用软件在并发代码中的净收益:
- 重复的错误不会重复。换句话说,如果一个或多个并发系统遇到相同的异常,Sentry将增加事件报告,但不提交事件的两个副本。
这意味着您可以找出哪个并发系统遇到哪种错误,而不必查看无数同时发生的错误报告。如果您曾经遭受过来自分布式系统的电子邮件垃圾邮件,那么您将知道到底是什么感觉。
您甚至可以“标记”并发系统的不同方面(尽管这假定您没有在一个线程上进行交错工作,从技术上讲,这并不是并发的,因为线程只是在任务之间高效地跳转,但仍必须处理事件处理程序完成),然后按标记查看错误细目。
- 您可以修改此错误处理软件,以提供有关运行时异常的更多详细信息。该流程有哪些开放资源?此过程是否拥有共享资源?哪个用户遇到了此问题?
除了细致的堆栈跟踪(和源映射,如果必须提供文件的缩小版本)之外,这还使得很容易确定大部分时间出了问题。
- (特定于哨兵的)您可以拥有一个单独的Sentry报告仪表板,用于系统的测试运行,从而使您能够捕获测试中的错误。
这种软件的缺点包括:
像所有东西一样,它们增加了体积。例如,您可能不希望在嵌入式硬件上使用这样的系统。我强烈建议您试用这种软件,将一个简单的执行与不执行的情况进行比较,并在空闲的计算机上对数百次的运行进行采样。
并非所有语言都受到同样的支持,因为许多系统都依赖于隐式捕获异常,并且并非所有语言都具有强大的异常。话虽这么说,但有很多系统的客户。
由于其中许多系统本质上是开源的,因此可能会带来安全风险。在这种情况下,请对它们进行尽职调查,或者,如果愿意,可以自己进行研究。
他们可能不会总是为您提供所需的信息。所有尝试增加可见性的操作都有风险。
这些服务大多数都是为高度并发的Web应用程序设计的,因此,并非每种工具都适合您的用例。
总而言之:具有可见性是任何并发系统中最关键的部分。我上面描述的两种方法,结合有关硬件和数据的专用仪表板以在任何给定时间点获得系统的整体图景,在整个行业中被广泛使用,以解决该问题。
一些其他建议
对于那些试图以可怕的方式解决并发问题的人,我花了更多的时间在修理代码上。每次我发现以下情况可以极大改善开发人员体验(与用户体验同等重要)的情况:
一个好的链接测试会检查一个组件独立地与另一个组件通信时,是否收到的消息和发送的消息是否与您期望的相同。如果您有两个或多个组件依赖共享服务进行通信,请将它们全部旋转,让它们通过中央服务交换消息,然后查看它们是否最终都能达到您的期望。
将涉及很多组件的测试分解为组件本身的测试以及每个组件之间如何通信的测试,可以使您对代码的有效性更有信心。进行如此严格的测试可以使您在服务之间强制执行合同,并捕获在一次运行时发生的意外错误。
- 使用正确的算法来验证您的应用程序状态。我说的是简单的事情,例如,当您有一个主流程在等待所有工作人员完成一项任务,并且只想在所有工作人员都已完成时才移至下一步-这是检测全局问题的一个示例终止,存在诸如Safra算法的已知方法。
其中一些工具与语言捆绑在一起-例如,Rust保证您的代码在编译时不会出现竞争条件,而Go则具有一个内置的死锁检测器,该死锁检测器也在编译时运行。如果您能在问题投产之前就发现问题,那将是双赢。
一般经验法则:并发系统中的故障设计。预期公共服务将崩溃或中断。这甚至适用于不在计算机之间分发的代码-单台计算机上的并发代码可以依赖于外部依赖关系(例如共享日志文件,Redis服务器,该死的MySQL服务器),该依赖关系随时可能消失或被删除。 。
最好的方法是不时验证应用程序状态-对每个服务进行运行状况检查,并确保该服务的使用者收到不良运行状况的通知。诸如Docker之类的现代容器工具可以很好地做到这一点,应利用它来沙盒化。
您如何找出可以同时进行的事情和可以顺次进行的事情?
我在高度并发的系统上学习到的最大的经验教训之一是:您永远无法拥有足够的指标。指标绝对可以驱动应用程序中的所有内容-如果您不衡量所有内容,则您不是工程师。
没有指标,您将无法做一些非常重要的事情:
评估由于系统更改而造成的差异。如果您不知道调节旋钮A是否使指标B上升而指标C下降,那么当人们意外地将恶意代码推入系统时,您将不知道如何修复系统(他们会将代码推入系统) 。
了解下一步需要做些什么来改进它。在知道应用程序的内存不足之前,您无法确定是应该获得更多的内存还是为服务器购买更多的磁盘。
指标是如此关键和必要,以至于我在考虑系统需求之前就做出了有计划的计划。实际上,度量标准是如此重要,以至于我相信它们是该问题的正确答案:只有当您测量程序中的位在做什么时,您才知道可以使顺序或并发。正确的设计使用数字,而不是猜测。
话虽如此,当然有一些经验法则:
顺序意味着依赖。如果一个过程以某种方式依赖于另一个过程,则两个过程应该是顺序的。没有依赖关系的进程应该是并发的。但是,计划一种处理上游故障的方法,该方法不会阻止下游进程无限期地等待。
切勿将I / O绑定的任务与CPU绑定的任务混合在同一内核上。不要(例如)编写一个Web爬网程序,该爬网程序在同一线程中启动十个并发请求,在它们进入时立即将其抓取,并期望扩展到五百个-I / O请求并行进入队列,但是CPU仍然会顺序通过它们。(此单线程事件驱动模型是一种流行的模型,但由于这一方面而受到限制-人们不理解这一点,只是简单地绞尽脑汁,说Node无法扩展,为您提供示例)。
一个线程可以完成很多I / O工作。但是为了充分利用硬件的并发性,请使用一起占据所有内核的线程池。在上面的示例中,仅用于CPU工作的启动五个Python进程(每个进程可以使用六核计算机上的一个内核)和仅用于I / O工作的第六个Python线程将以比您想象的更快的速度扩展。
利用CPU并发性的唯一方法是通过专用线程池。一个线程通常足以应付许多I / O绑定工作。这就是为什么像Nginx这样的事件驱动的Web服务器可以更好地扩展(它们仅做I / O绑定工作)(将Apache I / O绑定工作与需要CPU并根据请求启动进程的对象进行扩展)的原因,但是为什么要使用Node来执行并行接收成千上万的GPU计算是一个糟糕的主意。