跳到正文
Hacker News· utiiiD·· 10 天前AI 评分22

现代软件架构从青铜时代水钟继承了什么

What modern software architecture owes to Bronze Age water clocks

AI 导读

现代软件中的漏桶限流机制可追溯至公元前1600年巴比伦人用底部小孔容器计时的水钟,公元前200年希腊人已将其改进为带浮子和齿轮的精确时钟,Ctesibius 用齿轮驱动指针显示季节。

正文 · AI 翻译

这里有一个看似简单的问题:这个函数应该执行吗?

有时,答案显而易见。用户登录了吗?他们的角色拥有所需的权限吗?

有时则不那么明显。有足够的可用资源吗?这项工作比其它所有事情优先级都高吗?

在软件中,用于阻止或允许操作的机制有很多名称。最常见的是被称为准入控制。也许你对漏桶实现比较熟悉。

漏桶简史

漏桶的工程应用早于计算机。大约在公元前1600年,巴比伦人、埃及人和波斯人都使用底部有小孔的容器作为原始时钟,通过容器以恒定速率排水时的水量来测量流逝的时间。

到公元前200年左右,希腊人已将漏桶改进为精确的时钟。一个浮动的调节器使水从一个桶稳定地流入第二个桶,在第二个桶中,上升的浮子带动指针显示小时。虹吸管在第二个桶满后将其排空。最值得注意的是,克特西比乌斯添加了齿轮,以定期推进时钟的每一次“滴答”,转动显示当前季节的指示器。

由漏桶驱动的机械运动诞生了,并在几个世纪里成为计时的黄金标准。

水钟被用作一种准入控制形式。在雅典法庭上,发言人为自己案件辩护的时间长短是通过水从容器中排出的时间来衡量的。倒入容器中的水量由案件的严重程度决定。

最后,在计算机时代,据说乔纳森·特纳在1986年首次使用了“漏桶”一词,为《IEEE通信杂志》撰写了《通信的新方向》。这篇文章描述了系统如何监管数据包在宽带连接上发送的速度。(如果你还没读过,剧透一下:它比你今天家里的连接慢不了多少。)

软件中的漏桶

软件中有两种漏桶,用于实现计量器或队列。

在两者中,每个“工作”单元都被分配一个值,该值与“桶”的当前容量进行核对。如果桶没有足够容量容纳新工作的大小,该工作就被拒绝。否则,工作被允许,其值加到桶的当前容量中。桶会排空(或者可以说泄漏),因此随着时间的推移,新工作的准入速度不能快于桶的排空速度。排空速率基于流逝的时间,而不是工作的完成。

当用作计量器时,桶定义了是否允许工作立即进行以及允许多少。作为队列,桶定义了可以等待进行的工作量,但工作只有在从桶中排空时才开始。

在这篇文章中,我们将重点关注作为计量器的漏桶。

通常,漏桶在软件中实现是为了保持系统健康。这是一种资源管理,以确保系统不会不堪重负。

不难想象这在数据库中很有用。想象一下,能够根据可用资源或并发限制,在查询执行之前阻止或允许查询。想想都美!

可以通过修改漏桶的容量和排空速率来调整它。

从本质上讲,漏桶会阻止工作,但它也迫使我们思考导致阻塞的工作总量、新容量变得可用的速度,以及在工作被拒绝时如何处理预期的失败状态。

API 设计中的漏桶

Shopify 的 GraphQL Admin API 使用有文档记录的漏桶来实现速率限制。每个商店上的每个应用都会获得一个具有固定大小和恢复速率的桶。

每个请求的成本不同,取决于查询请求的内容,而不是发出的请求数量。在查询运行之前,桶必须为其预估成本留出空间。一旦完成,桶会退还或收取预估成本与实际成本之间的差额。应用可以以快于恢复速率的突发速度运行,直到桶没有剩余空间,此时请求会被限流。

响应中包含桶的当前状态,因此应用可以在被限流之前放慢速度,并将限流视为预期结果,而不是意外。

作为准入控制的分类

现在让我们考虑 AI 时代。Jev,一个“System One”模型(也许更适合称为“决策模型”),最近发布了,使得通过推理进行分类变得廉价而快速。它可以根据一组预定义的选项对“这项工作是否应该继续”进行分类,并在数百毫秒内返回响应。

注意

有趣的首尾呼应事实:1907 年,法国科学家 Lapicque 将神经元建模为一种电荷,它会积累、泄漏,并在达到阈值时放电,后来被称为“漏积分-发放”模型。因此,类似漏桶的东西长期以来一直被用来描述神经元,使其成为神经网络起源故事的一部分,也因此成为 Jev 起源故事的一部分。

无论我们的决策门是机械的(漏桶)还是需要推理(Jev),要求都是一样的:回答“这应该继续吗?”

低成本限流

软件中漏桶的一大好处是它们非常廉价。我们所描述的缓慢排空的桶并不是一个使用 CPU 来“排空”的长期运行进程。

简而言之,桶是一个有状态对象,保存两个值:

  1. 有多满,即上次有人查看时的状态
  2. 何时查看的

根据这些值,你可以算出桶现在有多满:

level_now = max(0, level_then - drain_rate × (now - then))

任何被桶拒绝或接纳的工作都会更新其当前状态。通过这种方式,桶是惰性排空的(即仅在检查时排空),空桶不保存任何值。

准入控制与数据库

就像硬件、操作系统、应用程序和古希腊人一样,数据库也能从漏桶中受益。

然而,像 Postgres 和 MySQL 这样的开源数据库并没有内置此功能。

生产环境中数据库最常见的故障模式之一是无法故意拒绝它们无法完成的工作。向典型的关​​系数据库发送十亿个查询,它会尽力执行它们。成功不太可能。某些硬件、配置或连接限制会无意中扮演准入控制系统的角色,并以某种未处理、不受欢迎的方式开始阻塞工作。

一些专有数据库具有某种形式的资源管理,以便对工作负载进行限流(但不拒绝)。例如,1999 年 Oracle Database Resource Manager 为会话添加了针对用户、服务、客户端等的映射规则,随后可用于对资源使用和并发等施加限制。

2008 年,SQL Server 添加了 SQL Server Resource Governor,它为会话添加了分类器函数,随后可对该会话中的查询应用规则。

Postgres 有一些配置选项可以控制已经消耗了资源的昂贵查询,例如 statement_timeout,但无法在昂贵工作发生之前加以阻止。于是在 2026 年,PlanetScale 通过 Database Traffic Control® 将决策门引入了 Postgres。

Database Traffic Control

你的 Postgres 数据库的健康状况完全暴露在应用程序的摆布之下。一场意外的连接风暴或查询计划器的故障,都可能对你的数据库提出超出其合理承受能力的请求。

为了保护你的 Postgres 数据库免受应用程序(有时也免受其自身)的影响,我们构建了 Traffic Control,即 Postgres 的准入控制。在其中,资源预算决定一个或多个查询可以消耗多少资源或多少并发。

每个预算会准入或阻止匹配指定标签集的查询。这些标签可以在查询中指定为注释,也可以指代发送该查询的用户或应用程序。如果资源预算判定该查询不应继续,它将记录一条警告或阻止该查询,具体取决于你的资源预算配置。

创建资源预算会让你进入一种预期的失败状态,因此它迫使你思考当查询被阻止时应用程序应表现出何种行为。出于这个原因,我们建议先以警告模式创建一个新预算,监控有多少流量受到影响,让应用程序为被阻止的查询做好准备,然后再更新模式。

准入控制,但针对流量

你在资源预算上配置三个主要设置。

服务器份额和突发限制,即令牌*桶。服务器份额决定桶以多快的速度补充令牌,以服务器资源的百分比表示。突发限制设置桶的最大容量,以整台服务器秒数表示。

由于数据库在执行前并不知道查询的成本,Traffic Control 会根据查询计划以及具有该形态的查询的历史行为来估算其成本。如果预算有足够的容量,查询就会继续。执行后,会计算查询的成本,任何差额要么退还,要么从令牌桶中扣除。

为什么补充速率以服务器容量而非秒数来衡量?因为执行查询所花费的时间并非都是等价的。一个占用 8 个核心一秒钟的查询,与一个占用 1 个核心一秒钟的查询是不同的。还要考虑不同的服务器规格:25% 的服务器份额意味着你的服务器容量的四分之一每秒被消耗并补充回来,无论该服务器有 2 个核心还是 64 个核心。

注意

*等等,令牌桶?

令牌桶的工作方式与漏桶相同,但隐喻相反。桶一开始就装满了“令牌”,每项工作都被分配一个“成本”。只有当剩余令牌足以支付该工作的成本时,工作才会被接纳。令牌会随时间重新填充。用桶中剩余令牌来衡量资源预算的剩余容量,比用当前填充水平来推理要容易一些。

命名这件事,对吧?

每查询限制为单个查询可消耗的最大容量设定了上限。这直接对单个查询做出准入控制决策,而不是根据桶中的可用容量来衡量它。

最大并发工作线程数同样控制单个查询可消耗的资源,但它是按可用工作线程数而不是执行时间来衡量的。

一个查询可能受多个资源预算的约束,必须满足所有预算才能继续执行。

关于资源预算在哪些具体场景中有帮助,以及如何在代码库中为查询打标签,请参阅Postgres 流量控制模式。

总结

准入控制迫使你决定当工作无法继续时应该发生什么,而漏桶长期以来一直是一种低成本的决策方式。Traffic Control 会在昂贵的查询运行之前为 Postgres 做出这个决定,而不是之后。

开始使用数据库流量控制

来源:Hacker News · planetscale.com