The Kollected Kode Vicious

Kode Vicious - @kode_vicious

  下载本文的PDF版本 PDF

延迟与活锁

一位态度鲜明的程序员,KV 回答你的问题。他可不是曼纳斯小姐。

有时数据传输速度不如预期。有时程序看起来运行良好,但实际上在幕后悄悄地失败。如果你遇到过这些问题,你可能挣扎了一段时间,然后变得困惑和/或疲惫。Kode Vicious 理解你的挫败感,本月他提供了一些关于如何处理这两个恼人问题的指导性建议。是否因其他难题而感到疲惫或困惑?将你的问题通过电子邮件发送至 [email protected]

尊敬的 KV:

我公司有一个非常庞大的数据库,其中包含我们所有的客户信息。该数据库被复制到世界各地的多个地点,以提高本地性能,以便亚洲的客户在查看他们的数据时,不必等待数据从我公司总部所在的美国传输过来。

几个月前,公司升级了软件,这需要更新客户数据库中的所有记录。当我们测试升级程序时,更新大量记录仅需几分钟,但是当我们必须更新亚洲客户时,这个过程必须在亚洲运行,并且会访问美国的数据,这个过程开始花费更长的时间。由于公司拥有连接美国和亚洲办公室的非常快速的网络,因此很难理解距离怎么会产生影响。一定有其他原因导致程序运行时间过长。

带宽困惑者

尊敬的困惑者:

拥有大的管道和知道如何使用它之间有很大的区别。网络中有两件事很重要:带宽和延迟。不幸的是,大多数人只考虑前者,而不是后者。延迟是消息从 A 点到 B 点(例如,客户端和服务器)所需的时间。如果我猜测,我会认为你在本地网络上测试了转换程序,可能是 100-Mbit 以太网,那里的延迟通常小于 1 毫秒。然后你通过非常快速的网络远程运行该程序,发现它运行得慢得多。

我敢打赌它慢了 100 倍。我是怎么得出 100 倍的?

很简单,跨太平洋的平均往返时间约为 100 毫秒。你忘记了一个非常重要的常数,然后你忘记了网络是如何工作的。

你所说的非常快速的网络可能是按每秒比特数出售的,所以你可能在日本和美国之间有 1-Gbps 的链路,但这衡量的是带宽,而不是延迟。在你的情况下,延迟才是重要的。为什么?因为你的转换过程很可能一次处理一条记录,将其打包,向服务器发出请求,然后等待响应。

当底层网络具有非常低的延迟时,例如本地网络,这种打包单个请求和等待响应的过程几乎不会被注意到;但是,如果你转移到更高延迟的网络,它会像铁蹄一样碾压你的系统性能。

你忘记的东西是 c,光速。假设你在东京运行转换过程,它与加利福尼亚州的数据库通信。从东京到加利福尼亚州大约 5,000 英里,光速是每秒 186,000 英里,因此光束应该能够在约 0.027 秒内从东京到达加利福尼亚州。这是两点之间绝对最快的时间 27 毫秒,往返时间为 5.4 毫秒。这已经比你的局域网慢了五倍。

当然,数据包不是点对点传输的。它们在旅程中的各个点被存储和转发——这就是互联网的工作方式。每个中途点(技术术语是路由器)都会给数据包的旅程引入自己的延迟,直到我们得到日本和加利福尼亚州之间平均单程 50 毫秒。现在,你的局域网和实际网络之间存在 100 倍的差异。现实是残酷的,在这种情况下,它狠狠地咬了你一口。如果本地转换一些记录需要五分钟,那么远程转换将需要 500 分钟(8 小时 20 分钟)。如果我是你,我会带本书上班,或者像其他同事一样下载一部电影,然后再开始任何数据库转换。

有一些方法可以缓解这些问题,尽管绕过光速已经困扰了更聪明的人很长一段时间。第一种方法是在本地运行所有转换;第二种方法是编写转换程序,使其批量处理请求。这可以更有效地利用你公司可能花费大量资金租用的高带宽网络。如果服务器端的数据库转换可以在比跨链路移动所有这些记录所需的时间更短的时间内处理一批记录,那么你将节省一些时间;但是,如果每条记录都必须串行处理,那么你将始终会因长距离链路上的缓慢性能而受苦。

KV

尊敬的 KV:

我一直在尝试调试网络服务器中的一个问题。在负载下,服务器似乎锁死了,但是当我查看系统上的进程状态时,状态始终为 RUN,这意味着该进程没有被阻塞。该程序没有陷入无限循环,因为每次我附加调试器时,代码都位于其处理循环的不同部分,但是始终没有任何输出或进展。程序如何在运行,没有陷入无限循环,却又没有产生任何输出?

寻找死锁的疲惫者

尊敬的疲惫者:

你所看到的——实际上是摆在你面前的——是活锁,而不是死锁。尽管大多数人在学习计算机科学时都会学习死锁,但他们很少在遇到活锁之前了解它。活锁在代码的一般流程是:读取、处理、写入、读取、处理、写入的软件中很常见。

在活锁情况下,代码被传入的工作负载压倒。许多系统——包括,看起来,你的软件——都有一个安全阀,以便在不堪重负时,它们可以丢弃未完成的工作。问题是安全阀可能不够好。

如果服务器因为时间、内存或其他资源耗尽而丢弃一个请求,那么它将陷入一个循环,在这个循环中,它读取一个请求,尝试处理它,然后失败,然后返回并读取下一个请求,尝试处理它,然后失败。就所有意图和目的而言,代码看起来像是在运行,而且它确实在运行,但是它没有成功地完成它应该做的事情。实际上,它是在徒劳地挣扎和失败。

克服活锁的一种方法是购买更多资源,例如更快的处理器或更多内存。这种“用钱解决问题”的方案在智力上是懦弱的,应该作为最后的手段。在许多情况下,根本不可能获得更多的原始硬件能力,因为许多公司已经使用最快的处理器和最大量的内存来运行其服务器。通常,公司这样做是因为他们聘请了错误的人来编写他们的软件(即,不考虑他们的代码正在使用哪些资源的工程师——你知道,白痴)。对于非白痴,还有几种其他方法可以处理活锁。

一个常见的技巧是在系统中构建代码,允许某人调整处理循环。调整非常简单:它表示系统每单位时间仅处理一定数量的工作。最初,旋钮设置为无限大,直到系统陷入活锁,然后将旋钮调低到略低于导致活锁的请求数量。是的,这意味着系统将丢弃工作请求,但它仍然能够完成工作,而不是被淹没而无法完成任何工作。使用此技巧需要你的软件知道它每单位时间正在处理多少个请求,因此你需要一些代码来计数。

要实现一个可以调整请求数量的系统,你需要设计代码,使其可以在处理循环中尽可能早地丢弃请求,甚至可以追溯到读取阶段。如果系统知道它已过载,则不应浪费额外的资源来引入更多工作。

通常,活锁情况下的问题与处理时间有关,即处理时间不足。如果你认为时间是你的问题,那么你需要分析你的软件,看看它在哪里浪费,呃,花费时间。如果可以加快处理速度,那么显然你的代码将能够更长时间地避免陷入活锁。

调整软件是一个难题,并且之前在 Queue 中已经介绍过(2006 年 2 月)。因为我确信你保留了所有过刊,所以你应该可以轻松地回顾并参考其中的优秀建议。

KV

acmqueue

最初发表于 Queue 第 6 卷,第 2 期
数字图书馆 中评论本文





更多相关文章

Ethan Miller, Achilles Benetopoulos, George Neville-Neil, Pankaj Mehra, Daniel Bittman - 远内存中的指针
有效利用新兴的远内存技术需要考虑在父进程上下文之外操作富连接数据。正在开发中的操作系统技术通过公开诸如内存对象和全局不变指针之类的抽象来提供帮助,这些抽象可以被设备和新实例化的计算遍历。这些想法将允许在具有分离内存节点的未来异构分布式系统上运行的应用程序利用近内存处理来获得更高的性能,并独立扩展其内存和计算资源以降低成本。


Simson Garfinkel, Jon Stewart - 磨砺你的工具
本文介绍了我们在最初发布十年后更新高性能数字取证工具 BE (bulk_extractor) 的经验。在 2018 年至 2022 年之间,我们将该程序从 C++98 更新为 C++17。我们还进行了完整的代码重构,并采用了单元测试框架。DF 工具必须经常更新,以跟上其使用方式的变化。对 bulk_extractor 工具更新的描述可以作为可以而且应该做什么的示例。


Pat Helland - 自主计算
自主计算是一种使用协作来连接领地及其使者的业务工作模式。这种模式基于纸质表格,已经使用了几个世纪。在这里,我们解释了领地、协作和使者。我们研究了使者如何在自主边界之外工作,并且在保持局外人身份的同时很方便。并且我们研究了如何跨不同领地启动、长时间运行并最终完成工作。


Archie L. Cobbs - 持久性编程
几年前,我的团队正在为一个增强型 911 (E911) 紧急呼叫中心的商业 Java 开发项目工作。我们对尝试使用传统的 Java over SQL 数据库模型来满足该项目的数据存储要求感到沮丧。在对项目的特定要求(和非要求)进行一些反思之后,我们深吸一口气,并决定从头开始创建我们自己的自定义持久层。





© 保留所有权利。

© . All rights reserved.