幂等性:分布式系统的稳定与高效关键

幂等性这个东西,很多人没怎么注意它,但它其实挺重要的。

在分布式系统里,幂等性能让系统既稳定又高效。如果你像我们一样,正在做那种要求反应特别快的API,而且一旦达不到标准就得挨罚,那你可能得好好看看这篇文章了。

啥是幂等性?
幂等性就是说,不管你重复做多少次,结果都跟只做一次一样。对于API来说,就是如果一个HTTP请求被处理了好几次,最后的结果还是跟只处理了一次一样。

举个例子,像计数器增加这种操作就不是幂等的,因为每增加一次,结果就变了。

这有啥大不了的?
在分布式系统里,各种意外是家常便饭,比如网络不稳定、资源不够用,这些都可能导致延迟增加,客户端等不及就超时了(当然,你已经设好了超时时间)。

不管你是提前预防还是事后补救,要想保证系统的高可用性,就得有办法应对这些突发情况。

重试是解决问题的常用方法,一有问题大家首先想到的就是重试。

想象一下:网络交换机突然缓冲区满了,你的响应数据包丢了,或者处理你请求的服务器CPU突然飙高(这种情况肯定得报个Jira)。这些都是临时性的错误,重试一下很可能就成功了。

关键点:在分布式系统里,任何流程如果不考虑重试,那都是不完善的。
但在你到处重试之前,得先问个问题:这样安全吗?

安全重试的前提是,你得确保不会有意外的副作用——最重要的是,操作不会重复执行。简单说,你重试的端点必须是幂等的。

我们怎么设计幂等的端点?
设计幂等端点挺有挑战的,所以得提前想好。比如,存下每个请求的外部状态可以检测到之前有没有处理过,但这可能会影响系统性能。

我们用Apache Cassandra的经验
Cassandra是我们的主要数据库,写入速度超快,低到几毫秒。保持这种速度是必须的,所以我们得找到既能保证幂等性又不拖慢速度的办法。

我们常用的几种方法:

1、客户端提供标识符
问题:服务器生成的ID每次重试都会变,可能导致重复插入和数据混乱。
解决方案:让客户端提供标识符。在Cassandra里,客户端还得提供主键的所有数据。对于非主键数据,重试时重新生成标识符一般没问题。

2、客户端提供时间戳
问题:重试的请求可能会覆盖后来的更新,导致数据错误。
解决方案:在Cassandra查询里用客户端提供的时间戳。
Cassandra用最后写入的数据为准,通过用客户端提供的时间戳,所有重试都能幂等地进行。

3、校准计数器
如果不提计数器幂等性,这篇文章就不完整。Cassandra有个计数器类型,线程安全,可以正常增减。

问题:如果超时了,你不知道计数器有没有变。

解决方案一:支持重试并定期校准计数器。
我们的系统能容忍计数器短时间内有点偏差。我们有个校准机制,把计数器值和另一个表里的数据对比,每分钟校准一次,适合我们的需求。

解决方案二:用幂等的数据结构。
如果你不能容忍计数器偏差,那就得换个数据结构。Cassandra有Set类型,添加/删除操作是幂等的(用客户端提供的时间戳)。然后你可以读取集合计算元素数量。这对Cassandra来说负担更重,但为了一致性,得做出权衡。

另一个好处:用对冲请求减少延迟

我们已经看到幂等性对系统稳定性的好处,但它对延迟也有帮助。
根据Jeff Dean在《The Tail at Scale》里的说法,减少延迟波动的一个有效方法是对冲请求,就是同时发给多个副本。这和超时重试很像,只是更主动,通常在比超时时间短得多的时候就开始对冲。

对冲也需要幂等性,因为多个副本可能同时处理请求。

减少延迟波动的一个简单方法是同时发请求给多个副本,用最先响应的那个。

在我们的WebFlux系统里,我们用大约15行代码实现了请求对冲,对于最关键的流程,它把我们的p999延迟从1200毫秒降到了450毫秒。

我们选择根据p99延迟来对冲(就是在p99延迟过去后发第一个对冲请求),但如果资源够用且延迟是主要目标,你可以更主动!

提示:同样的策略可以用在查询上,Cassandra DataStax客户端驱动原生支持推测执行。

容错库
您可能不想花时间在系统中构建容错原语,考虑到可用的工具,这是可以理解的。如果您能够找到一个符合系统性能要求的强大库,这可能是最好的第一步。

在研究这篇文章时,我碰到了对各种有助于持久执行的库和产品的引用,包括TemporalAzure Durable FunctionsDBOS

例如,DBOS 提供了一个注释驱动的 API 来构建弹性工作流,支持从任何中断状态自动恢复。它有 TypeScript 和 Python 库,并使用 Postgres 作为后端状态存储。

DBOS 还在此处发布了针对 AWS Step Functions 的基准测试,结果显示速度提高了 25 倍。这对于持久执行领域来说是一个激动人心的时刻!

这些库遵循外部状态管理模式,由于额外的持久性步骤,给已检测的工作流带来了一些开销。虽然它们简化了编排和弹性,但仍适用相同的幂等性要求。它们保证至少执行一次,这意味着您仍必须确保重试不会在您的工作流中造成意外的副作用。