Substrate 请求停放(Request Parking)演示实战:用 oversubscribed WorkerPool 让 503 变成 200
人工智能AI AgentAgent 沙箱云原生容器运行时零信任【免费下载链接】substrateAgent Substrate: the core system项目地址https://gitcode.com/GitHub_Trending/substrate7/substrate点击查看免费下载导读本文围绕 Agent Substrate 仓库中的demos/parking演示demos/parking/README.md完整讲解atenet 路由器的请求停放request parking特性当目标 Actor 因 WorkerPool 瞬时饱和而暂时无法被服务时路由器不再立刻返回503而是停放该请求并以指数退避重试 resume直到有 Worker 空闲或一个有界的停放预算耗尽。通过本文你将掌握该特性的设计动机、默认参数、基于kubectl ate的一整套可复现实验步骤含压测脚本load.sh的用法并从源码层面理解停放停车场parking lot、每 Actor 航班去重、预算语义与可观测性指标的底层实现。背景为什么需要请求停放Substrate 的核心前提是oversubscription——大量 Actor 复用少量 Worker。当请求到达一个处于SUSPENDED状态的 Actor 时路由器的调用链是Envoy --(ext_proc RequestHeaders)-- router.handleRequestHeaders -- ActorResumer.ResumeActor -- ateapi ResumeActor (gRPC)ateapi的AssignWorkerStep会从该 Actor 所属的WorkerPool中认领一个空闲 Worker。在流量突发时池子可能瞬间被占满AssignWorkerStep返回ResourceExhausted: no free workers available。过去路由器直接把它映射成 HTTP503并立刻失败请求——但这种饱和通常是瞬时的几毫秒后另一个 Actor 挂起suspend就会释放出 Worker。立即失败等于把一次亚秒级的抖动放大成了用户可见的错误。请求停放正是针对这个问题的答案把瞬时饱和翻译成有界等待而不是立即失败。完整的机制设计与语义细节见 docs/request-parking.md。演示前提条件Prerequisites在运行演示之前需要准备一个已安装 Agent Substrate 的 Kubernetes 集群安装方式为./hack/install-ate.sh --deploy-ate-system见 hack/install-ate.sh安装了ko用于构建镜像一个 GCS bucket用于存放快照通过环境变量BUCKET_NAME配置模板文件 demos/parking/parking-template.yaml.tmpl 中storageLocation: gs://${BUCKET_NAME}/ate-demo-parking/会被安装脚本注入该值。在 Agent Substrate 上运行演示1. 构建并部署[!NOTE] 不要手工编辑demos/parking/*.yaml.tmpl清单。安装脚本会在部署时自动把${BUCKET_NAME}环境变量注入模板。./hack/install-ate.sh --deploy-demo-parking这条命令会依次完成用ko构建counter工作负载镜像创建ate-demo-parking命名空间和2 副本的WorkerPool名为parking来自 demos/parking/parking.yaml.tmpl。该模板刻意把池子做得很小replicas: 2workerImage: ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor——所以同时活跃的 Actor 超过 2 个就会饱和池子从而触发停放路径创建ate-demo-parkingatespace并在其中创建parkingActor 模板demos/parking/parking-template.yaml.tmpl通过kubectl ate create actor-template应用。注意ActorTemplate 不是 CRD而是通过 ate API 创建的原型protojson 形态的ateapipb.ActorTemplate等待池子完成 rollout、模板的 golden snapshot 构建完成。值得说明的是模板中的容器 demos/parking/parking-template.yaml.tmpl 正是 counter 演示所用的同一个counter二进制ko://github.com/agent-substrate/substrate/demos/counter源码见 demos/counter/counter.go——它的响应体里包含 Worker Pod 的 IP因此你可以直观地看到哪个 Worker 服务了这次可能被停放的请求。快照配置snapshotsConfig在挂起/提交时都做全量快照沙箱类为SANDBOX_CLASS_GVISOR使用gvisor-default配置。2. 创建比 Worker 更多的 ActorActor 位于演示的 atespaceate-demo-parking中。--template指定模板名模板在 Actor 的 atespace 内解析# 如尚未安装 CLI先将其安装为 kubectl 插件 go install ./cmd/kubectl-ate # 4 个 Actor 共享一个 2-Worker 池 - oversubscribed for id in p1 p2 p3 p4; do kubectl ate create actor $id --atespace ate-demo-parking --template parking doneCLI 的入口实现位于 cmd/kubectl-ate/main.go内部实现可参考 cmd/kubectl-ate/internal。3. 端口转发 atenet 路由器kubectl port-forward -n ate-system svc/atenet-router 8000:80如何使用How to Use停放默认是开启的--parked-request-budget5s--parked-request-max1024所以刚部署的集群已经在停放请求了。下面每个 curl 请求都用ate-target-actor头选择目标 Actor注意改变 URL 或Host并不会切换到另一个 Actor。A. 观察一个 503 如何变成被服务的请求先用两个请求占满两个 Worker让 p1、p2 保持RUNNINGcurl -s -H ate-target-actor: ate-demo-parking/p1 http://localhost:8000 curl -s -H ate-target-actor: ate-demo-parking/p2 http://localhost:8000 kubectl ate get workers # 两个 worker 现在都绑定在 p1 和 p2 上 kubectl ate get actors # p1,p2 RUNNING; p3,p4 SUSPENDED现在带计时地请求p3。池子已满所以这次请求会停放——curl会挂起同时路由器在重试 resumecurl -s -w \n- HTTP %{http_code} in %{time_total}s\n \ -H ate-target-actor: ate-demo-parking/p3 http://localhost:8000在它挂起的期间打开第二个终端在 5s 停放预算内挂起 p1 以释放一个 Workerkubectl ate suspend actor p1 --atespace ate-demo-parking回到第一个终端被停放的请求此时以HTTP 200完成time_total会显示它等待 Worker 所花的时间。如果停放被禁用同样的请求会立刻返回503见第 D 节。B. 在负载下观察停放demos/parking/load.sh 为每个 Actor 驱动一个并发的请求→挂起循环。由于 Actor 多于 Worker池子始终保持饱和每个循环末尾的 suspend 会为竞争者释放一个 Worker模拟 Actor 进入空闲因为自动挂起-空闲尚未实现。统计结果展示了停放是如何吸收争用的./demos/parking/load.sh # 30s, actors p1 p2 p3 p4 # results # total requests : 142 # 200 OK : 142 # 503 unavailable: 0 # 200 latency : avg 0.43s, slowest 6.12s - parked requests sit here # 0 failures under saturation: parking absorbed the contention.上面的数字是演示文档记录的示例输出实际数值取决于环境。从 demos/parking/load.sh 的源码可以看到它的完整参数化能力./load.sh [-d duration_secs] [-r router_url] [-a atespace] [actor_id ...] # -d 负载时长秒默认 30 # -r 路由器 base URL默认 http://localhost:8000 # -a Actor 所在的 atespace默认 ate-demo-parking # 位置参数Actor ID 列表默认 p1 p2 p3 p4脚本的内部逻辑demos/parking/load.sh值得细看它预检并幂等地创建 atespace 和 Actor然后为每个 Actor 起一个worker()子进程——用curl -w %{http_code} %{time_total}记录每次请求的状态码与耗时紧接着kubectl ate suspend actor释放 Worker如此循环直到截止时间。最后汇总输出 total / 200 / 503 / other 以及 200 的平均与最慢延迟demos/parking/load.sh503为 0 时提示停放吸收了争用否则提示这些请求是被 503 丢弃的停放关闭、停车场已满或预算超时。C. 观察停放状态路由器的/statusz页面有一个Request Parking卡片。转发状态端口并读取在load.sh产生负载时运行能看到非零的activekubectl -n ate-system port-forward deployment/atenet-router 4040:4040 curl -s http://localhost:4040/statusz?formatjson | jq .parking # { enabled: true, active: 3, max_parked: 1024, max_wait: 5s }该 JSON 结构与源码中ParkingStatus的定义一一对应cmd/atenet/internal/router/ingress/parking.goenabled是否开启、active当前停放数、max_parked停车场容量上限、max_wait停放预算。状态端口4040与指标端口9090都定义在路由器 Deployment 清单 manifests/ate-install/atenet-router.yaml 中--status-port4040、containerPort: 9090并带有prometheus.io/port: 9090注解。停放指标同样通过路由器的 metrics 端点--metrics-listen-addr容器端口9090导出atenet.router.parking.active—— 当前停放中的请求数上下计数器atenet.router.parking.wait.duration—— 停放时长直方图按outcome标签区分atenet.router.parking.rejected—— 因停车场已满而被丢弃的请求计数。D. 与关闭停放的对比关闭停放观察旧的快速失败行为。在路由器容器的 args 里追加该 flagkubectl -n ate-system patch deployment atenet-router --typejson \ -p[{op:add,path:/spec/template/spec/containers/0/args/-,value:--parked-request-max0}] kubectl -n ate-system rollout status deployment/atenet-router重新运行压测——此时瞬时饱和会暴露成503./demos/parking/load.sh # 503 unavailable: 37 # 37 requests were shed with 503 (parking off, ...).再次启用停放只需移除该 flagkubectl -n ate-system rollout undo deployment/atenet-router[!TIP] 你也可以调参而不是完全关闭在同样的 args 列表里加--parked-request-budget10s或--parked-request-max512。卸载演示./hack/install-ate.sh --delete-demo-parking该命令会删除演示的 Actor先挂起正在运行的、模板、atespace、池子和命名空间。源码级原理停放到底是怎么工作的触发路径与可重试条件从 cmd/atenet/internal/router/ingress/resumer.go 的retryable判断可以看到停放的判定核心Aborted并发 resume 冲突总是重试ResourceExhausted无空闲 Worker、FailedPrecondition瞬时状态和Unavailable控制面抖动如 ateapi 滚动重启仅在停放开启时重试其余错误码NotFound、DeadlineExceeded、PermissionDenied等立即返回由 HTTP 边界按语义映射。完整映射表出自 docs/request-parking.mdResume 结果行为OK路由到 WorkerAborted并发 resume总是重试ResourceExhausted无空闲 Worker停放并重试启用时FailedPrecondition瞬时状态停放并重试启用时Unavailable控制面抖动停放并重试启用时NotFound快速失败 →404DeadlineExceeded快速失败 →504PermissionDenied/Unauthenticated快速失败 →403/401当停放关闭--parked-request-max0时路由器快速失败FailedPrecondition和Unavailable立即返回、无准入上限、只重试Aborted冲突且预算固定为15s源码中的failFastResumeBudget见 cmd/atenet/internal/router/ingress/resumer.go。预算语义per-flight而非 per-request被停放的请求由ActorResumer的 per-Actor航班注册表flight registry去重同一 Actor 的并发请求共享一个在途的ResumeActorgRPC 调用cmd/atenet/internal/router/ingress/resumer.go因此热点 Actor 会占用 N 个停放槽位却只发起一个控制面 RPC。预算的时钟从航班第一个调用者开始 resume 时启动context.WithTimeout(..., r.budget)见 cmd/atenet/internal/router/ingress/resumer.go之后加入的请求共享剩余的预算与结果。因此晚加入的请求可能在自身等待远不足一个完整预算时就看到budget_exhausted——这是把热点 Actor 的请求折叠进一次控制面调用的代价。预算耗尽 ≠ 取消在途 resume需要特别强调预算约束的是重试次数而不是一次已承诺的 resume。当预算耗尽时路由器停止发起新的 resume 尝试但已在途的尝试绝不会被取消——控制面已为它投入工作取消会丢弃一次进行中的恢复restore。路由器会等待该尝试的真实结果超出预算的恢复节点争用下的常态会延迟服务而不是失败迟到的可重试错误仍以容量503呈现。budgetExhaustedError包装了最后一个可重试错误cmd/atenet/internal/router/ingress/resumer.go这样 HTTP 边界仍能忠实映射底层 gRPC 状态带容量消息的503同时指标层可以把它记录为独立的budget_exhausted结果。停车场parking lot有界的准入与背压为了约束资源使用并提供背压被停放的请求进入一个固定容量的停车场--parked-request-max默认1024。源码实现见 cmd/atenet/internal/router/ingress/parking.goenter()是一个非阻塞的有界准入闸门一个请求只有在真正发生停放转换时其 resume 航班的第一次可重试失败才占用槽位——第一次尝试就成功Actor 已在运行的请求永远不会占用槽位。当停车场已满到达停放转换的请求被以503 actor id unavailable: router at capacity丢弃recordRejected代价恰好是暴露它需要等待的那一次 resume 尝试。与 Envoy 断路器的联动每个被停放的请求会在整个等待期间持有一条 ext_proc 流对 Envoy ext_proc 集群的一个 active 请求而普通请求只持有毫秒级的头部交换。因此该集群的断路器是并发停放请求的硬上限。默认情况下路由器推导它为--parked-request-max的两倍最小值1024见 cmd/atenet/internal/router/config.go 的extProcMaxRequests这样停车场总能装下并保留等量的快速路径余量——一个满载的停车场无法饿死发往已在运行 Actor 的请求。--extproc-max-requests可覆盖推导值显式值必须在启动时校验 --parked-request-maxcmd/atenet/internal/router/config.go因为低于停车场的断路器会静默截断停车场——溢出会被 Envoy 自己以 503 拒绝永远不会进入停车场也不会计入parking.rejected。完整配置参数表以下参数全部通过路由器容器的 args 配置默认值与语义来自 cmd/atenet/internal/router/ingress/parking.go 与 docs/request-parking.mdFlag默认值含义--parked-request-budget5s每个 resume航班的停放预算去重到同一在途 resume 的请求共享其剩余预算。--parked-request-max1024最大并发停放请求数槽位在停放转换时占用首次尝试查找永不占用超出者被丢弃503。0关闭停放。--parked-request-retry-interval100ms停放请求第一次 resume 重试前的延迟。--parked-request-retry-factor1.1每次尝试后重试延迟的倍率必须 1启动时校验见 cmd/atenet/internal/router/ingress/parking.go。--parked-request-retry-jitter0.1每次重试附加的随机抖动比例[0, 1)用于去同步被停放的请求。--extproc-max-requests0自动Envoy ext_proc 集群的断路器max_requests。0推导为--parked-request-max的两倍最小1024显式值必须 --parked-request-max启动时强制。超出部分是快速路径余量。关于重试退避源码特意不设上限、不设尝试次数上限resumeBackoff中Steps: math.MaxInt32、无 Cap见 cmd/atenet/internal/router/ingress/resumer.go——只有预算约束等待时间。默认参数下从 100ms 起步、倍率 1.1 的间隔在 5s 预算内只会增长到约 0.5s节奏温和。可观测性指标与状态页指标OpenTelemetrymeteratenet-router实现见 cmd/atenet/internal/router/ingress/metrics.goatenet.router.parking.active—— 上下计数器当前停放中的请求数atenet.router.parking.wait.duration—— 直方图秒停放耗时。每个停放请求恰好记录一次在其等待结束时首次尝试即成功的请求从未停放、被丢弃的请求只递增parking.rejected以及停放关闭时都不会记录。outcome标签说明停放如何结束outcome何时设置servedresume 成功请求被路由到其 Worker。budget_exhausted停放预算在 resume 仍被可重试条件阻塞时耗尽池饱和、并发操作占用 Actor、控制面不可用——这是容量是瓶颈而非故障的信号。canceled客户端在停放期间断开请求 context 被取消。timeout请求自身的 deadline 在停放期间到期区别于停放预算。errorresume 以不可重试错误失败NotFound、PermissionDenied等。atenet.router.parking.rejected—— 计数器因停车场已满而被丢弃的请求数。状态页/statusz上的Request Parking卡片会显示停放是否启用、当前停放数 vs 最大值、最大等待时长即上文第 C 节的 JSON。停机语义被停放的请求不会被重置一个在路由器 Pod 收到 SIGTERM 时处于停放状态的请求不会被重置关闭序列会保持 ext_proc 服务器并通过 preStop 握手保持 Envoy sidecar存活直到在途流结束ext_proc drain 截止时间--drain-timeout默认推导自--parked-request-budget并在启动时校验预算cmd/atenet/internal/router/config.go——所以即使处于终止过程中被停放的请求也总能拿到完整预算并获得正常裁决路由200或容量503。优雅关闭相关的--drain-delay、--drain-timeout参数见 manifests/ate-install/atenet-router.yaml。小结demos/parking演示把 request parking 从设计文档里的机制变成了一条命令可复现的实验2-Worker 池 4 个 Actor 的刻意 oversubscription配合load.sh的请求→挂起循环与/statusz/metrics 的实时观测你可以亲眼看到同一批流量在停放开启时全部以200略有延迟返回、关闭时出现成批503。而源码cmd/atenet/internal/router/ingress/parking.go、cmd/atenet/internal/router/ingress/resumer.go则揭示了其工程取舍per-flight 预算、绝不取消在途 resume、有界停车场与 Envoy 断路器的联动推导以及budget_exhausted与served等精确的结局分类——这些都是把瞬时饱和优雅地转化为有界等待的关键设计。赞分享人工智能AI AgentAgent 沙箱云原生容器运行时零信任【免费下载链接】substrateAgent Substrate: the core system项目地址https://gitcode.com/GitHub_Trending/substrate7/substrate点击查看免费下载相关推荐Hasura v3 pre-ndc-request 插件示例实战用 pre-ndc-request-plugin-example 在请求到达数据连接器前改写 NDC 请求Hasura v3 pre ndc request 插件示例实战用 pre ndc request plugin example 在请求到达数据连接器前改写后端API网关数据库GraphQLNext.js Middleware 请求头修改实战基于 modify-request-header 示例掌握请求头的增删改Next.js Middleware 请求头修改实战基于 modify request header 示例掌握请求头的增删改 导读 本篇文章围绕仓库 edge示例工程前端后端从503到200Traefik OpenTelemetry追踪实战指南从503到200Traefik OpenTelemetry追踪实战指南 你是否还在为微服务架构中的请求延迟问题头疼用户投诉页面加载缓慢日志里却只有零星的5后端API网关负载均衡微服务网络云原生创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考