全球控制面服务经常面对一个难题:系统既要在跨地域网络中保持强一致,又要尽量避免某个中心节点故障导致写入停摆。Cloudflare 最近介绍了内部服务 Meerkat,它基于 QuePaxa 共识算法,在保持强一致性的同时支持无领导者写入,为全球协调服务提供了不同于 Raft 的设计路径。
Raft 的领导者模型为何会限制全球写入
Raft 的工程优势很明确:模型容易理解,日志复制、任期和领导者选举都有成熟实现。客户端通常把写请求发送给当前领导者,由领导者把日志复制到多数节点后提交。
但在全球部署中,领导者模型会带来几个现实问题:
- 写请求可能需要跨越较高延迟的网络才能到达领导者。
- 领导者所在区域出现故障或网络隔离时,写入需要等待重新选举。
- 即使其他区域仍然健康,客户端也可能因为无法访问领导者而暂时无法提交变更。
- 多个地域的控制面操作最终都要经过同一个写入入口,扩展方式受到限制。
这并不意味着 Raft 不适合全球系统。对于单地域或延迟要求可控的服务,Raft 仍然是很实用的选择。问题在于,全球控制面通常更重视跨地域可用性,以及在故障期间继续处理协调请求的能力。
Meerkat 的核心:保持一致性,但不把写入绑定到单一领导者
根据 Cloudflare 对 Meerkat 的介绍,它使用 QuePaxa 共识算法构建全球一致的控制面服务。与 Raft 的典型领导者写入模型不同,QuePaxa 支持 leaderless writes,也就是写入不必先经过一个固定的领导者。
这里的“无领导者”并不等于“写入后最终会一致”。如果系统需要强一致,写请求仍然必须经过足够严格的排序、确认和提交流程。变化在于:
- 写入请求可以由不同地域的节点接收。
- 共识协议负责确定操作的全局顺序和提交条件。
- 客户端可以减少对单一领导者可达性的依赖。
- 某个地域发生故障时,其他健康节点仍有机会继续承担协调工作。
因此,Meerkat 的重点不是删除共识,而是改变共识协议对写入入口的约束。这种设计适合配置发布、路由协调、策略变更、元数据更新等控制面场景:这些操作数量通常不如业务数据写入大,但必须可靠、可审计,并且在全球范围内看到一致结果。
一个可改造的客户端模型
Meerkat 是 Cloudflare 的内部服务,公开资料并不提供可直接调用的公共 API。下面的示例因此是一个假设的 HTTP 接口,用于展示应用侧如何表达强一致写入、幂等键和版本条件。实际接入时,应以目标服务提供的 API、认证方式和错误码为准。
假设服务提供以下接口:
PUT /v1/configs/{name}:提交配置变更。If-Match:要求服务端只在客户端已知版本仍然有效时提交。Idempotency-Key:避免客户端重试造成重复操作。- 返回的
version:表示已经提交的全局版本。
可以先用 curl 验证调用流程:
export COORDINATION_ENDPOINT="https://coordination.example.internal"
export TOKEN="replace-with-service-token"
export REQUEST_ID="deploy-2025-03-08-001"
curl --fail-with-body --request PUT \
--url "$COORDINATION_ENDPOINT/v1/configs/routing-policy" \
--header "Authorization: Bearer $TOKEN" \
--header "Content-Type: application/json" \
--header "If-Match: \"version-41\"" \
--header "Idempotency-Key: $REQUEST_ID" \
--data '{
"rules": [
{"prefix": "api.example.com", "target": "region-us"},
{"prefix": "static.example.com", "target": "region-eu"}
]
}'
应用代码应把冲突和临时不可用区分开来。版本冲突通常意味着需要重新读取最新配置后重新计算变更,而网络错误则可能适合使用带上限的重试。下面是一个可以直接运行和改造的 Python 客户端骨架:
from __future__ import annotations
import time
import uuid
from typing import Any
import requests
class VersionConflict(Exception):
pass
class CoordinationClient:
def __init__(self, endpoint: str, token: str, timeout: float = 5.0):
self.endpoint = endpoint.rstrip("/")
self.session = requests.Session()
self.session.headers.update({"Authorization": f"Bearer {token}"})
self.timeout = timeout
def update_config(
self,
name: str,
document: dict[str, Any],
expected_version: str,
retries: int = 3,
) -> dict[str, Any]:
request_id = str(uuid.uuid4())
for attempt in range(retries):
try:
response = self.session.put(
f"{self.endpoint}/v1/configs/{name}",
json=document,
headers={
"Content-Type": "application/json",
"If-Match": f'"{expected_version}"',
"Idempotency-Key": request_id,
},
timeout=self.timeout,
)
except requests.RequestException:
if attempt == retries - 1:
raise
time.sleep(0.25 * (2**attempt))
continue
if response.status_code == 409:
raise VersionConflict(
f"configuration changed before commit: {response.text}"
)
response.raise_for_status()
return response.json()
raise RuntimeError("update did not complete")
if __name__ == "__main__":
client = CoordinationClient(
endpoint="https://coordination.example.internal",
token="replace-with-service-token",
)
result = client.update_config(
name="routing-policy",
expected_version="version-41",
document={"rules": [{"prefix": "api.example.com", "target": "region-us"}]},
)
print("committed version:", result["version"])
这个客户端示例有三个值得保留的边界:重试使用固定的幂等键,避免同一次逻辑操作被提交多次;版本冲突不自动覆盖别人的更新;提交成功后记录服务端返回的全局版本,而不是把本地时间戳当作一致性依据。
强一致性和可用性之间仍然存在成本
无领导者写入可以降低对单一领导者的依赖,但它不会消除分布式系统的基本约束。跨地域共识仍然需要通信、排序和失败处理,网络分区期间系统仍必须在可用性和一致性之间做出明确选择。
采用类似设计时,团队需要特别确认以下问题:
- 提交延迟:一次写入需要等待哪些节点确认?最远地域的网络延迟是否会进入用户请求路径?
- 冲突语义:两个地域同时更新同一对象时,系统是按全局顺序提交、拒绝冲突,还是提供更高层的合并规则?
- 重试语义:客户端能否安全重试?幂等键的保存时长和作用范围是什么?
- 读取保证:读取是线性一致、单调读取,还是只保证最终一致?不同 API 是否有不同级别?
- 故障恢复:节点恢复后如何追赶状态?控制面是否会因为积压更新而影响数据面?
- 审计能力:配置是谁、在什么时间、以哪个版本提交的?回滚是否也是一个经过共识提交的操作?
什么时候值得考虑这种架构
如果服务只部署在一个地域,写入延迟低,且领导者故障可以接受短暂的重新选举,那么成熟的 Raft 实现往往更简单,运维和排障成本也更低。
如果系统需要在多个地域接收控制面写入,并且不能依赖某个固定区域持续可达,那么 QuePaxa 这类支持无领导者写入的共识模型值得评估。评估时不要只比较吞吐量,应使用真实的跨地域拓扑测试:模拟单地域故障、链路分区、重复请求、并发更新和节点恢复,再检查提交结果、读取顺序与审计记录。
可以把落地检查表压缩为四项:
- 明确定义每个操作需要的读取和写入一致性。
- 为每个变更设计幂等键、版本条件和冲突处理。
- 用故障注入测试跨地域网络异常,而不是只测试节点进程崩溃。
- 记录全局版本和完整审计事件,让回滚成为可验证的控制面操作。
Meerkat 展示了一条重要思路:全球强一致系统不一定只能通过一个领导者接收写入。真正的工程问题仍然是如何定义提交、排序、冲突和故障恢复,而共识算法的选择,应围绕这些控制面语义和实际网络条件展开。