在 EKS Fargate 上做微服务蓝绿发布:把切换点放在 ALB 上

在 EKS Fargate 上做微服务蓝绿发布:把切换点放在 ALB 上

一套可直接运行的参考实现:Terraform 建平台,Kustomize 管切换。

源码仓库: github.com/geekchow/blue-green-eks-fargate-alb

------本文涉及的所有内容都在这个仓库里,clone 下来即可直接运行。English version:

blog/blue-green-eks-fargate-alb.en.md


缩写对照表

缩写 英文全称 中文
ACM AWS Certificate Manager AWS 证书管理器
ALB Application Load Balancer 应用负载均衡器
API Application Programming Interface 应用程序接口
ARN Amazon Resource Name 亚马逊资源名称
AZ Availability Zone 可用区
CIDR Classless Inter-Domain Routing 无类别域间路由
CRD Custom Resource Definition 自定义资源定义
DNS Domain Name System 域名系统
EKS Elastic Kubernetes Service 弹性 Kubernetes 服务
ENI Elastic Network Interface 弹性网络接口
IAM Identity and Access Management 身份与访问管理
IMDS Instance Metadata Service 实例元数据服务
IRSA IAM Roles for Service Accounts 服务账号的 IAM 角色
LP Live Proving 线上验证
NAT Network Address Translation 网络地址转换
NLB Network Load Balancer 网络负载均衡器
OIDC OpenID Connect 开放身份连接
SNS Simple Notification Service 简单通知服务
SSE Server-Sent Events 服务器发送事件
TG Target Group 目标组
TTL Time To Live 生存时间
VPC Virtual Private Cloud 虚拟私有云

一、问题背景

三个 API(Application Programming Interface,应用程序接口)跑在使用 Fargate 算力的

EKS(Elastic Kubernetes Service,弹性 Kubernetes 服务)集群上,通过

aws-load-balancer-controller 对外暴露。我们希望做蓝绿发布:新版本与老版本并存部署,用生产的基础设施

和生产的配置去验证它,确认无误后一次性把流量全部切过去;万一新版本在真实流量下出问题,能在几秒内撤回这个决定。

本文实现的思路是:

把新版本部署到 blue 命名空间,用一个只匹配 LP(Live Proving,线上验证)域名的 Ingress 暴露它,

并把 ingress order 设成较小的值,让它被优先匹配。通过 LP 域名对新版本做健康检查。确认没问题后,

解除主机名限制------此时这条低 order 的规则也会匹配生产流量,所有请求都落到 blue,green 退役。

如果真实用户进来之后才发现问题,就把主机名限制加回去,流量立刻回到 green。

这个设计是对的,本文大部分篇幅是在解释它为什么对。但要让它撑过第二次发布,还需要补两件事,下面都会讲到。


二、为什么在负载均衡器上切,而不是在 DNS 上切

最容易想到的替代方案,是让 api.example.com 指向当前的生产腿,切换时改 DNS(Domain Name System,域名系统)

记录。不要这样做。 对任何"可能需要撤回"的操作来说,DNS 都是一个糟糕的开关:

  • 回滚被 TTL(Time To Live,生存时间)卡住。 TTL 设 60 秒,就意味着你决定中止之后,至少还有一分钟的
    用户在继续访问坏掉的那条腿。
  • TTL 只是建议值。 企业解析器、设了 networkaddress.cache.ttl=-1 的 JVM、各种移动端客户端,
    都可能把过期结果缓存几个小时。你无法给"还有人留在旧腿上"设一个时间上界。
  • 集群内看不到这个状态。 你没法问 Kubernetes "现在哪条腿是生产的"。

改在 ALB(Application Load Balancer,应用负载均衡器)上路由,这些问题全部消失:两个域名永久 解析到

同一个 负载均衡器,发布过程中 DNS 从头到尾不变。切换只是改写监听器规则;而由于 ALB 是

按请求 、而不是按连接来匹配规则的,这个变更会作用在已经建立的连接上的下一个请求 ------

不需要重新解析,也不需要重连。

严格地说(后面所有结论都建立在这个细节上):规则变更确实需要数秒才能传播到 ALB 的所有节点,

所以这个切换是"很快",而不是字面意义上的"原子"。但那是以传播时间为上界的数秒

对比之下 DNS 是在你无法控制的各级缓存上无上界地等

第八节会完整推演一个持有 keep-alive 长连接的客户端到底会经历什么。

支撑这一点的机制是控制器的 IngressGroup:多个位于不同命名空间的 Ingress 对象共享同一个 ALB。

yaml 复制代码
alb.ingress.kubernetes.io/group.name: public-apis
alb.ingress.kubernetes.io/group.order: "1"     # 数值越小越先被匹配

每条腿向这个组贡献自己的 Ingress,group.order 决定这条腿的监听器规则的相对优先级。先匹配者胜出

所以 order 较小的腿挡在另一条腿前面时,可以遮蔽它------可以完全遮蔽,也可以只遮蔽特定的主机名。

这种有选择的遮蔽,就是蓝绿开关本身。


三、路由全景

#mermaid-svg-flYPjH9kzKTj9c6M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-flYPjH9kzKTj9c6M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-flYPjH9kzKTj9c6M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-flYPjH9kzKTj9c6M .error-icon{fill:#552222;}#mermaid-svg-flYPjH9kzKTj9c6M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-flYPjH9kzKTj9c6M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-flYPjH9kzKTj9c6M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-flYPjH9kzKTj9c6M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-flYPjH9kzKTj9c6M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-flYPjH9kzKTj9c6M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-flYPjH9kzKTj9c6M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-flYPjH9kzKTj9c6M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-flYPjH9kzKTj9c6M .marker.cross{stroke:#333333;}#mermaid-svg-flYPjH9kzKTj9c6M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-flYPjH9kzKTj9c6M p{margin:0;}#mermaid-svg-flYPjH9kzKTj9c6M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-flYPjH9kzKTj9c6M .cluster-label text{fill:#333;}#mermaid-svg-flYPjH9kzKTj9c6M .cluster-label span{color:#333;}#mermaid-svg-flYPjH9kzKTj9c6M .cluster-label span p{background-color:transparent;}#mermaid-svg-flYPjH9kzKTj9c6M .label text,#mermaid-svg-flYPjH9kzKTj9c6M span{fill:#333;color:#333;}#mermaid-svg-flYPjH9kzKTj9c6M .node rect,#mermaid-svg-flYPjH9kzKTj9c6M .node circle,#mermaid-svg-flYPjH9kzKTj9c6M .node ellipse,#mermaid-svg-flYPjH9kzKTj9c6M .node polygon,#mermaid-svg-flYPjH9kzKTj9c6M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-flYPjH9kzKTj9c6M .rough-node .label text,#mermaid-svg-flYPjH9kzKTj9c6M .node .label text,#mermaid-svg-flYPjH9kzKTj9c6M .image-shape .label,#mermaid-svg-flYPjH9kzKTj9c6M .icon-shape .label{text-anchor:middle;}#mermaid-svg-flYPjH9kzKTj9c6M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-flYPjH9kzKTj9c6M .rough-node .label,#mermaid-svg-flYPjH9kzKTj9c6M .node .label,#mermaid-svg-flYPjH9kzKTj9c6M .image-shape .label,#mermaid-svg-flYPjH9kzKTj9c6M .icon-shape .label{text-align:center;}#mermaid-svg-flYPjH9kzKTj9c6M .node.clickable{cursor:pointer;}#mermaid-svg-flYPjH9kzKTj9c6M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-flYPjH9kzKTj9c6M .arrowheadPath{fill:#333333;}#mermaid-svg-flYPjH9kzKTj9c6M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-flYPjH9kzKTj9c6M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-flYPjH9kzKTj9c6M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-flYPjH9kzKTj9c6M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-flYPjH9kzKTj9c6M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-flYPjH9kzKTj9c6M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-flYPjH9kzKTj9c6M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-flYPjH9kzKTj9c6M .cluster text{fill:#333;}#mermaid-svg-flYPjH9kzKTj9c6M .cluster span{color:#333;}#mermaid-svg-flYPjH9kzKTj9c6M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-flYPjH9kzKTj9c6M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-flYPjH9kzKTj9c6M rect.text{fill:none;stroke-width:0;}#mermaid-svg-flYPjH9kzKTj9c6M .icon-shape,#mermaid-svg-flYPjH9kzKTj9c6M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-flYPjH9kzKTj9c6M .icon-shape p,#mermaid-svg-flYPjH9kzKTj9c6M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-flYPjH9kzKTj9c6M .icon-shape .label rect,#mermaid-svg-flYPjH9kzKTj9c6M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-flYPjH9kzKTj9c6M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-flYPjH9kzKTj9c6M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-flYPjH9kzKTj9c6M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Route 53 --- 两条记录永远指向同一个 ALB
不匹配
匹配
匹配
命名空间 green --- 在位腿,v1.9.0
orders-api
payments-api
catalog-api
命名空间 blue --- 候选腿,v2.0.0
orders-api
payments-api
catalog-api
api.example.com

生产域名
lp-api.example.com

线上验证域名
ALB · IngressGroup public-apis · HTTPS:443
优先级 1 --- 来自 blue 的 Ingress

host == lp-api.example.com ?
优先级 10 --- 来自 green 的 Ingress

host == api.example.com ?

按顺序读这两条规则,整个设计就自然浮现出来了:

  • 访问 lp-api.example.com 的请求命中优先级 1,进入 blue
  • 访问 api.example.com 的请求 命中优先级 1,穿透到优先级 10,进入 green

这两件事在同一时刻 成立,走的是同一个 负载均衡器、同一张 证书、同一个 监听器。

这正是线上验证有意义的原因:你验证的不是一个近似的预发环境,而是真实的入口链路、真实的 TLS 终止,

唯一的差别只有主机名。

切换时,blue 的规则不再限制在 LP 域名上,它现在也匹配 api.example.com------而因为它被优先匹配,

green 的规则再也不会被走到。green 没有被修改、没有重启、没有缩容,只是不再被问到而已。


四、状态机:为什么两个状态不够

原始设计里 blue 是 order 1、green 是 order 10。对第一次发布,这完全没问题。但看看第二次。

blue 赢了,blue 现在在 order 1。下一个版本得找地方放,于是放到 green------而 green 在 order 10,

排在 blue 后面 。green 在那个位置永远遮蔽不了 blue,也就永远无法被提升。

前排的槽位被上一次发布的赢家占住了,而且再也没被腾出来。

解法是:不要把 order 当作颜色的属性,而要当作角色的属性。 两个槽位,三种角色:

角色 group.order Ingress 规则 承接的流量
candidate(候选) 1(前排) 仅 LP 域名 LP 流量;生产流量穿透给在位腿
promoted(已提升) 1(前排) 生产 + LP 全部流量------它遮蔽了在位腿
incumbent(在位) 10(后排) 生产 + LP 只要前面没人挡着,就是生产流量

静息状态下只存在一条腿,它是在位腿。一次发布会在前排临时创建一条候选腿;这条候选腿要么被提升,要么被丢弃。

提升并观察(soak)一段时间之后,finalize(收尾) 步骤会让老腿退役,并把赢家从 promoted

轮转到 incumbent,从而腾出前排槽位给下一次发布。
#mermaid-svg-B697F4XYIeKtwgih{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-B697F4XYIeKtwgih .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-B697F4XYIeKtwgih .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-B697F4XYIeKtwgih .error-icon{fill:#552222;}#mermaid-svg-B697F4XYIeKtwgih .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-B697F4XYIeKtwgih .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-B697F4XYIeKtwgih .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-B697F4XYIeKtwgih .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-B697F4XYIeKtwgih .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-B697F4XYIeKtwgih .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-B697F4XYIeKtwgih .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-B697F4XYIeKtwgih .marker{fill:#333333;stroke:#333333;}#mermaid-svg-B697F4XYIeKtwgih .marker.cross{stroke:#333333;}#mermaid-svg-B697F4XYIeKtwgih svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-B697F4XYIeKtwgih p{margin:0;}#mermaid-svg-B697F4XYIeKtwgih defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-B697F4XYIeKtwgih g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-B697F4XYIeKtwgih g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-B697F4XYIeKtwgih g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-B697F4XYIeKtwgih g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-B697F4XYIeKtwgih g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-B697F4XYIeKtwgih .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-B697F4XYIeKtwgih .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-B697F4XYIeKtwgih .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-B697F4XYIeKtwgih .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-B697F4XYIeKtwgih .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-B697F4XYIeKtwgih .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-B697F4XYIeKtwgih .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-B697F4XYIeKtwgih .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-B697F4XYIeKtwgih .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-B697F4XYIeKtwgih .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-B697F4XYIeKtwgih .edgeLabel .label text{fill:#333;}#mermaid-svg-B697F4XYIeKtwgih .label div .edgeLabel{color:#333;}#mermaid-svg-B697F4XYIeKtwgih .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-B697F4XYIeKtwgih .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-B697F4XYIeKtwgih .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-B697F4XYIeKtwgih .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-B697F4XYIeKtwgih .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-B697F4XYIeKtwgih .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-B697F4XYIeKtwgih .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-B697F4XYIeKtwgih #statediagram-barbEnd{fill:#333333;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-B697F4XYIeKtwgih .cluster-label,#mermaid-svg-B697F4XYIeKtwgih .nodeLabel{color:#131300;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-B697F4XYIeKtwgih .note-edge{stroke-dasharray:5;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-note text{fill:black;}#mermaid-svg-B697F4XYIeKtwgih .statediagram-note .nodeLabel{color:black;}#mermaid-svg-B697F4XYIeKtwgih .statediagram .edgeLabel{color:red;}#mermaid-svg-B697F4XYIeKtwgih #dependencyStart,#mermaid-svg-B697F4XYIeKtwgih #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-B697F4XYIeKtwgih .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-B697F4XYIeKtwgih :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 静息态,green 承接生产流量
把 v2 部署到 blue(order 1,仅 LP)
验证失败 --- 删掉 blue,生产流量全程无感
验证通过 --- 解除主机名限制
回滚 --- 把限制加回去
收尾 --- green 退役,blue 从 1 轮转到 10
下一次发布,两种颜色互换角色
GreenIncumbent
BlueCandidate
BluePromoted
BlueIncumbent
GreenCandidate

注意回滚这条边指向哪里:指回 candidate,而不是某个独立的抢修流程。回滚就是重新进入五分钟前你已经待过的状态

而且出问题的那条腿依然挂在 LP 域名上,你可以在生产已经安全的前提下,用真实请求去调试它。


五、用 Kustomize 表达这个状态机

切换动作应该是一个小的、可评审、可回退的 diff,而不是一段去改动线上对象的脚本。

所以状态机被建模成一个 颜色 × 角色 的 overlay 矩阵

复制代码
k8s/
├── base/                       三个 API + 一个同时带两条 host 规则的 Ingress
│   ├── apis/{orders,payments,catalog}/{deployment,service}.yaml
│   ├── nginx/api.conf.template
│   └── ingress.yaml
├── legs/
│   ├── blue/kustomization.yaml    namespace: blue,  leg=blue,  APP_VERSION
│   └── green/kustomization.yaml   namespace: green, leg=green, APP_VERSION
├── components/
│   ├── candidate/     order 1,删掉生产 host 规则
│   ├── promoted/      order 1,保留两条 host 规则
│   └── incumbent/     order 10,保留两条 host 规则
└── overlays/
    ├── blue-candidate/   blue-promoted/   blue-incumbent/
    └── green-candidate/  green-promoted/  green-incumbent/

legs/ 负责身份,components/ 负责角色,六个 overlay 各自只有六行,把两者组合起来:

yaml 复制代码
resources:
  - ../../legs/blue
components:
  - ../../components/candidate

开关本身

base 里的 Ingress 同时列出两个主机名,生产的排在前面:

yaml 复制代码
rules:
  - host: api.example.com        # rules[0] --- 生产
    http: { paths: [...] }
  - host: lp-api.example.com     # rules[1] --- 线上验证
    http: { paths: [...] }

candidate 组件把生产那条删掉:

yaml 复制代码
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
patches:
  - target: { group: networking.k8s.io, version: v1, kind: Ingress, name: apis }
    patch: |-
      - op: remove
        path: /spec/rules/0
      - op: replace
        path: /metadata/annotations/alb.ingress.kubernetes.io~1group.order
        value: "1"

这就是全部的"主机名限制"。promoted 就是同一段补丁去掉 remove 那一步。

提升和回滚,就是这两个文件之间的差别。

~1 不是笔误。 在 JSON Pointer(RFC 6901)里,键名中的字面 / 要转义成 ~1~ 转义成 ~0)。

注解的 key 里全是斜杠,所以每一个针对注解的 JSON 6902 补丁都需要这个转义。

不转义的话,报错会是含糊的 "missing value",而不是明显的"路径写错了"。

离线校验这个矩阵

scripts/render-check.sh 会渲染全部六个 overlay 并逐格断言,不需要集群,也不需要 AWS 账号,

适合直接放进持续集成:

复制代码
OVERLAY            NS      ORDER  HOSTS                                RESULT
blue-candidate     blue    1      lp-api.example.com                   ok
blue-promoted      blue    1      api.example.com,lp-api.example.com   ok
blue-incumbent     blue    10     api.example.com,lp-api.example.com   ok
green-candidate    green   1      lp-api.example.com                   ok
green-promoted     green   1      api.example.com,lp-api.example.com   ok
green-incumbent    green   10     api.example.com,lp-api.example.com   ok

它还会检查一件手改时很容易搞砸的事:IngressGroup 里所有成员的 ALB 级注解必须逐字节一致。见第九节。


六、一次完整的发布

green (v1.9.0) blue (v2.0.0) ALB LB 控制器 EKS API 运维 green (v1.9.0) blue (v2.0.0) ALB LB 控制器 EKS API 运维 #mermaid-svg-J1WsRm3hgSfjyf9C{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J1WsRm3hgSfjyf9C .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J1WsRm3hgSfjyf9C .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J1WsRm3hgSfjyf9C .error-icon{fill:#552222;}#mermaid-svg-J1WsRm3hgSfjyf9C .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J1WsRm3hgSfjyf9C .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J1WsRm3hgSfjyf9C .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J1WsRm3hgSfjyf9C .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J1WsRm3hgSfjyf9C .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J1WsRm3hgSfjyf9C .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J1WsRm3hgSfjyf9C .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J1WsRm3hgSfjyf9C .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J1WsRm3hgSfjyf9C .marker.cross{stroke:#333333;}#mermaid-svg-J1WsRm3hgSfjyf9C svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J1WsRm3hgSfjyf9C p{margin:0;}#mermaid-svg-J1WsRm3hgSfjyf9C .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-J1WsRm3hgSfjyf9C text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-J1WsRm3hgSfjyf9C .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-J1WsRm3hgSfjyf9C .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-J1WsRm3hgSfjyf9C .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-J1WsRm3hgSfjyf9C .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-J1WsRm3hgSfjyf9C #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-J1WsRm3hgSfjyf9C .sequenceNumber{fill:white;}#mermaid-svg-J1WsRm3hgSfjyf9C #sequencenumber{fill:#333;}#mermaid-svg-J1WsRm3hgSfjyf9C #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-J1WsRm3hgSfjyf9C .messageText{fill:#333;stroke:none;}#mermaid-svg-J1WsRm3hgSfjyf9C .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-J1WsRm3hgSfjyf9C .labelText,#mermaid-svg-J1WsRm3hgSfjyf9C .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-J1WsRm3hgSfjyf9C .loopText,#mermaid-svg-J1WsRm3hgSfjyf9C .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-J1WsRm3hgSfjyf9C .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-J1WsRm3hgSfjyf9C .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-J1WsRm3hgSfjyf9C .noteText,#mermaid-svg-J1WsRm3hgSfjyf9C .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-J1WsRm3hgSfjyf9C .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-J1WsRm3hgSfjyf9C .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-J1WsRm3hgSfjyf9C .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-J1WsRm3hgSfjyf9C .actorPopupMenu{position:absolute;}#mermaid-svg-J1WsRm3hgSfjyf9C .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-J1WsRm3hgSfjyf9C .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-J1WsRm3hgSfjyf9C .actor-man circle,#mermaid-svg-J1WsRm3hgSfjyf9C line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-J1WsRm3hgSfjyf9C :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 静息态 --- green 在位,order 10 LP ->> blue,生产 ->> green,同时成立 green 在 order 10 原封不动 --- 热的回滚目标 观察期,盯指标 apply -k overlays/blue-candidate 1 创建 Pod(注入就绪门控) 2 注册 blue 目标,新增规则 @1,host 仅 LP 3 目标健康 4 就绪门控 ->> True 5 GET https://lp-api/...(线上验证) 6 转发 7 200, X-Leg: blue 8 apply -k overlays/blue-promoted 9 规则 @1 现在也匹配生产 host 10 全部生产流量 11 收尾:先删 green,再把 blue 从 1 轮转到 10 12

对应的脚本:

bash 复制代码
./scripts/status.sh                    # 当前真实状态
./scripts/promote.sh blue              # 部署、LP 验证、暂停确认、切换
./scripts/rollback.sh blue             # 立即撤回
./scripts/finalize.sh blue             # 观察期门禁、green 退役、槽位轮转

promote.sh 在三个条件同时满足之前不会切换:所有 Deployment 完成滚动、每个 Pod 的 ALB 就绪门控都是 True

三个 API 在 LP 域名上都返回 200X-Leg: blue。然后它会停下来征求确认。

在提示之前它会打印那个"分叉"状态------这是整套机制里最有说服力的一段输出:

复制代码
  live  https://api.example.com/orders    -> green
  lp    https://lp-api.example.com/orders -> blue

同一时刻、同一个负载均衡器、同一张证书、同一个监听器,两个不同的答案。


七、回滚

bash 复制代码
./scripts/rollback.sh blue

它重新应用 blue-candidate,把"仅 LP"的限制恢复回去。blue 的 order 1 规则不再匹配 api.example.com

请求穿透到 green 的 order 10 规则;而 green 全程都在运行、健康、已注册,于是下一个请求就恢复由它服务。

它之所以快,关键在于没有发生什么 :没有 DNS 变更、没有拉镜像、没有起 Pod、没有扩容、没有目标注册、

没有健康检查爬坡。唯一的变更是一条监听器规则的匹配条件。脚本花在验证回滚上的时间,远多于执行回滚本身。

blue 故意不删除 。它依然可以通过 LP 域名访问,你可以在生产已经安全之后,

对着真实的入口链路复现这个故障。

唯一会失去这个性质的时刻是 finalize.sh ,它会让老腿退役。在那之后,恢复手段就只剩向前修复,

或者从头重新部署上一个版本。脚本在执行前会明确提示这一点。选观察期长度时请把这一条放在心上 ------

多让两条腿并行跑一小时的成本,远小于"想撤撤不回来"的成本。


八、连接复用:切换对一条正在使用的 keep-alive 连接做了什么

任何认真想过 HTTP 的人,都应该会提出这个质疑:

HTTP/1.1 默认保持连接不断开,HTTP/2 更是把所有请求复用在一条连接上。这些连接会跨过切换点继续存在。

那用户不就会一直卡在老腿上,直到连接超时为止吗?而且一个浏览器同时开着好几条连接,

岂不是会在新旧版本之间来回跳?

如果连接是路由的单位,这个推论完全正确。 在工作于四层的 NLB(Network Load Balancer,网络负载均衡器)

上它就是对的:被均衡的对象就是连接本身,已经建立的连接会继续流向它原本流向的地方。

但 ALB 不是这样工作的。

ALB 是一个完整的代理,它按请求路由

这里有两个互相独立的连接池,彼此解耦:

复制代码
客户端  <--keep-alive-->  ALB  <--连接池-->  目标 Pod
        (前端连接)              (后端连接)

客户端的 TCP 与 TLS 连接终止在 ALB 上 ,它根本到不了 Pod。对于这条连接上到达的每一个请求

ALB 都会解析该请求、拿它去匹配监听器规则、再选出目标组。host-header 条件是每个请求都重新求值一次 的,

而不是在建连时求值一次就固定下来。

所以在同一条没有断开的连接上跨越切换点:

复制代码
第 N 个请求    在连接 C 上   ->  规则:lp-only@1, live@10  ->  green
        <<< 提升:blue 的规则现在也匹配生产 host >>>
第 N+1 个请求  在连接 C 上   ->  规则:live+lp@1           ->  blue

同一条 TCP 连接、同一个 TLS 会话,不重连、不握手,客户端感知不到任何事件。

keep-alive 连接与路由无关,因为路由从来就不是它的属性。

后端一侧同样是独立的:ALB 自己维护着通往 blue 和 green 目标组的连接池。切换之后,

它只是不再从 green 的池里取连接而已。那些连接会闲置下来并自然老化关闭,没有任何请求被中途掐断。

一个有用的推论:切换过程完全不涉及目标摘除。 第九节里的 deregistration_delaypreStop sleep

是为 Pod 滚动更新准备的;切换本身只改了一个规则条件,因此不会发生摘流,也不需要摘流。

这个担心在哪些情况下确实成立

三种情况,其中两种是真实存在的,值得精确地知道。

1. 规则传播不是原子的。 一个 ALB 不是一台机器,而是一组节点(每个可用区至少一个,并会随负载扩展)。

规则变更需要在很短的时间内(通常是数秒)传播到所有节点。客户端通过 DNS 被分散到不同节点上,

而浏览器会开好几条连接、可能落在不同节点上。在这个窗口期内,同一个用户确实可能一部分响应来自 blue、
一部分来自 green。

所以"来回跳"是真的存在的------但它的边界是规则传播时间(秒级)

不是 连接寿命或 idle_timeout。AWS 并没有对这个时间给出硬性上界。

实际的应对不是"等连接都断掉",而是:不要在几秒之内连续做提升和回滚;

以及确保"短暂一两秒的混合响应"是可以承受的------对向后兼容的 API 版本来说,它是可以承受的。

2. WebSocket 与 SSE(Server-Sent Events)是被钉死的。 连接一旦完成 Upgrade,

它就不再是一串可以逐个路由的请求,而变成了通往某个特定目标的隧道。这些连接会一直留在老腿上,

直到自己断开、客户端重连为止。这是原始质疑完全成立的地方。

如果你有长连接流式业务,那么切换对你的请求/响应流量是"按请求"的,对你的流式连接却是"按连接寿命"的

------这两者要分开规划。

3. 两条腿共用同一个 group.order------这才是真正会导致抖动的 bug。

如果两个 Ingress 最终都声明了同一个 order,它们之间的 ALB 规则优先级是未定义的,

而且可能在不同节点上不一致。这会产生真实的、持续的、不可预测的、永远不会收敛的 来回抖动,

看起来恰好就是那个质疑描述的故障模式。这正是 finalize.sh先删掉退役的腿、再 把赢家从

order 1 轮转到 order 10 的原因,也是 promote.sh 在两条腿都已在前排时拒绝启动的原因。

真正会遗留下来的问题:版本错配(version skew)

把传输层的问题澄清掉,并不会让底层的担忧消失------只是把它挪了个位置。

真正要担心的不是连接,而是会话。

一个浏览器会话在 green v1.9 上开始,之后它发出的请求会在会话进行中、页面没有刷新的情况下

落到 blue v2.0 上。如果你回滚,同一个会话又会回到 v1.9。

按请求路由不只是允许 这种情况发生,它是保证这种情况会发生。

要用的纪律和数据库迁移(第十一节)是同一套:

  • API 契约至少要向后兼容一个版本。
  • 如果某个客户端确实必须锁定版本,就在路径或请求头里显式协商 ------
    不要指望连接亲和性,你并没有这个东西。
  • 单页应用的静态资源用 CloudFront/S3 提供、并使用内容哈希文件名,不要从腿里出,
    这样会话中途的切换才不会让前端 bundle 和后端 API 脱节。
  • 把"现在提升、一小时后回滚"当成一个对用户可见的决定,而不是一次免费的撤销。

如果对你来说会话一致性比即时回滚更重要 ,那这套设计形状就不对。

你要的是加权目标组配合粘性 Cookie,代价是撤销过程会从"立即"变成"渐进"。

亲眼验证一下

curl 对同一 host 的重复 URL 会复用同一条连接,所以你可以直接在单条连接 上观察这次翻转。

在做提升的过程中运行:

bash 复制代码
curl -sv -o /dev/null -D - \
  $(printf 'https://api.example.com/orders %.0s' $(seq 1 300)) 2>&1 \
  | grep -iE 'Re-using existing connection|^x-leg'

你会看到全程都在打印 Re-using existing connection,而 X-Leg 的值在列表中间某处从 green

变成了 blue一条连接,两条腿------这正是整件事的关键。


九、Fargate 与 ALB 上真正会咬人的细节

下面每一条,第一次遇到时都会以令人困惑的方式失败。

target-type: ip 是必须的。 Fargate 没有工作节点,也就没有 NodePort 可供转发,

ALB 必须直接注册 Pod 的 ENI(Elastic Network Interface,弹性网络接口)地址。

子网发现标签。 公有子网上没有 kubernetes.io/role/elb = 1(私有子网上没有

kubernetes.io/role/internal-elb = 1),控制器就无法判断把 ALB 放在哪里,报错 "unable to discover subnets"。

配置在 terraform/vpc.tf

控制器必须显式传入 regionvpcId 在 EC2 节点上它从 IMDS(Instance Metadata Service,实例元数据服务)

读这两个值。Fargate 没有 IMDS,缺了这两个 Helm 值,控制器会以

failed to introspect vpcID from EC2Metadata 反复崩溃。配置在 terraform/lb-controller.tf

CoreDNS 调度不起来。 官方的 CoreDNS Deployment 带着 eks.amazonaws.com/compute-type: ec2 注解,

在纯 Fargate 集群上这个 addon 会永远停在 DEGRADED。要在 addon 的配置里传 computeType: Fargate

并确保 kube-system 的 Fargate profile 先存在。配置在 terraform/addons.tf

Pod 就绪门控,否则你的 rollout status 是假的。 默认情况下 Pod 的探针通过就算 Ready

而这跟 ALB 有没有注册它毫无关系。给命名空间打标签:

yaml 复制代码
labels:
  elbv2.k8s.aws/pod-readiness-gate-inject: enabled

控制器的 webhook 就会注入就绪门控,Pod 只有在其目标组报告健康之后才算 Ready

这样 kubectl rollout status 才真正是一句关于流量的陈述------而 promote.sh 正是依赖这一点。

门控是在 Pod 创建时 注入的。事后再给命名空间打标签,对已经存在的 Pod 毫无作用------

这正是 terraform/namespaces.tf 直接带标签创建两个命名空间、而不交给 Kustomize 的原因。

优雅摘流。 三个数字的大小关系必须正确,否则每次滚动都会丢在途请求:

复制代码
preStop sleep (15s)  <  deregistration_delay (30s)  <  terminationGracePeriodSeconds (60s)

Pod 在 preStop 期间继续服务,同时 ALB 把它摘掉;宽限期必须比前两者都长。

ALB 级注解在组内必须一致。 group.nameschemelisten-portscertificate-arn

load-balancer-attributes 描述的是共享的 那个负载均衡器。blue 和 green 只要有一项不一致,

控制器就会拒绝整个组 ------包括那条本来跑得好好的腿。本仓库里两条腿都渲染自同一个 base/ingress.yaml

从结构上就不可能漂移,render-check.sh 还会再断言一次。

绝不允许两条腿共用同一个 order。 group.order 相同时,ALB 规则优先级是未定义的,流量会不可预测地分裂。

这就是为什么 finalize.sh先删掉退役的腿、再 把赢家从 1 轮转到 10,

也是为什么 promote.sh 在发现两条腿都已经在前排时会拒绝启动。


十、可观测性门禁

刻意分成两层。

常设告警terraform/observability.tf)监控整个负载均衡器:HTTPCode_Target_5XX_Count

HTTPCode_ELB_5XX_Count、p95 TargetResponseTimeUnHealthyHostCount

可选接到 SNS(Simple Notification Service,简单通知服务)。

这里有个顺序上的小疙瘩,值得直说:ALB 不是 Terraform 创建的 ,是控制器响应第一个 Ingress 时创建的。

所以在全新环境的首次 apply 时它还不存在,告警也就无从引用,因此 enable_alarms 默认是 false

等 Ingress 起来之后再做第二次 apply 打开它。这是有意为之的两阶段 apply,不是疏漏。

按腿对比 放在 finalize.sh 里,因为 ALB 整体的指标无法告诉你新版本 本身是否有问题。

脚本通过控制器在该命名空间创建的 TargetGroupBinding CRD(Custom Resource Definition,自定义资源定义)

找到已提升那条腿的目标组,再按目标组粒度拉取观察期内的指标:

bash 复制代码
./scripts/finalize.sh blue --soak-minutes 30 --max-error-rate 0.1 --max-p95 0.8
复制代码
  TARGET GROUP                        REQUESTS      5XX   p95(s)
  k8s-blue-ordersap-a1b2c3d4e5           14032        3    0.211
  k8s-blue-payments-f6g7h8i9j0            8814        0    0.402
  k8s-blue-catalogap-k1l2m3n4o5          21190        1    0.147

==> requests=44036  5xx=4  error_rate=0.0091%  worst p95=0.402s
  ok soak gate passed

门禁不通过时,脚本拒绝让老腿退役,并提示你回滚------因为老腿还热着,这个建议是可执行的。

另外,零流量也会被判不通过:一个没有任何请求的观察窗口什么都证明不了,却是最容易自欺的方式。


十一、这套设计不能给你什么

把边界说清楚:

  • 它不是金丝雀。 流量是 0% → 100% 一步到位。ALB 的加权目标组可以做百分比切分,
    但那是另一套机制、另一组失败模式,而且与"回滚只是改一个规则条件"这个性质不兼容。
  • 两条腿都要付钱。 从部署到收尾之间,你在跑双份容量。在 Fargate 上这是按秒计费、能被量化的成本,
    这也是"观察期要有纪律、而不是无限期"的一个很好的理由。
  • 数据库迁移依然是你自己的问题。 blue 和 green 共用同一个数据存储。迁移必须至少向后兼容一个版本
    ------采用 expand/contract(先扩后收),绝不要把破坏性变更和依赖它的代码放在同一次发布里。
    否则回滚 Pod 就等于回滚到一个已经不存在的表结构上。
  • 有状态与长连接切不干净。 已经建立在老腿上的 WebSocket、SSE 连接会一直留在那里,直到自己断开。
  • group.order 是一个很小的整数空间 (−1000 到 1000),并且被组内所有成员共享。
    两个槽位对蓝绿来说绰绰有余,但它不是一个通用的路由分层机制。

十二、怎么跑起来

源码:https://github.com/geekchow/blue-green-eks-fargate-alb

bash 复制代码
git clone https://github.com/geekchow/blue-green-eks-fargate-alb.git
cd blue-green-eks-fargate-alb

# 1. 平台
cd terraform
cp terraform.tfvars.example terraform.tfvars   # 填 hosted_zone_name、live_host、lp_host
terraform init && terraform apply
eval "$(terraform output -raw configure_kubectl)"

# 2. 把清单指向真实的证书与主机名
#    (terraform output certificate_arn / live_host / lp_host -> k8s/base/ingress.yaml)
./scripts/render-check.sh

# 3. 建立在位腿
kubectl apply -k k8s/overlays/green-incumbent
curl -s https://api.example.com/orders     # {"api":"orders","leg":"green",...}

# 4. ALB 已存在,打开告警
terraform apply -var enable_alarms=true

# 5. 发布
./scripts/promote.sh blue --stop-after-proving   # 手工确认那个"分叉"状态
./scripts/promote.sh blue                        # 切换
./scripts/rollback.sh blue                       # 需要时------立即生效
./scripts/finalize.sh blue                       # green 退役,槽位轮转

# 6. 验证这个循环可以重复
#    修改 k8s/legs/green/kustomization.yaml 里的 APP_VERSION
./scripts/promote.sh green

在把这套东西用到生产之前,第 6 步是最值得先跑一遍的------它恰恰是两状态版本做不到的那一步。


十三、小结

最初的直觉------把新腿限制在验证域名上、用规则优先级把它挡在前面、然后解除限制------完全正确。

它之所以成立,是因为它把开关放在了唯一一个既即时生效可被观测的地方:ALB 的监听器规则表。

这份实现补上的,是让它从"一次性事件"变成"一个可持续流程"的部分:

第三个角色,让两种颜色可以无限次互换槽位;一个明确宣告"你将失去回滚目标"的收尾步骤;

就绪门控,让"已滚动完成"真的等于"正在承接流量";以及一个读取按腿指标、而不是盲信绿色滚动状态的观察期门禁。

本文提到的所有内容都在同一个仓库里

------https://github.com/geekchow/blue-green-eks-fargate-alb ------

terraform/ 是平台,k8s/ 是状态机,scripts/ 是操作手册,

render-check.sh 负责在持续集成里让六个 overlay 保持诚实。

相关推荐
林间码客2 小时前
从DevOps到DevSecOps:构建现代软件交付的基石与护城河
大数据·运维·devsecops·devops
歪歪歪比巴卜3 小时前
服务商批量接手客户跨平台社媒账号的技术落地:体检体系与重启架构
矩阵·架构·aigc
BerryS3N3 小时前
2026年AI前沿技术全景深度解析:大模型推理范式变革、Agentic AI工程架构与底层基础设施演进
人工智能·架构
神王宝宝 王者小学3 小时前
面向领域驱动架构的查询实现方式
前端·python·架构
晨米酱4 小时前
Matt Pocock Skills v1.2:控制从主流程深入到每一步
面试·架构·agent
大卫陈5 小时前
PCB 拼版系统近期迭代复盘:大小拼跃迁、横直料重构与引擎打磨
后端·架构
谢白羽5 小时前
SGLang源码剖析-2-sglang双层体系架构全景
分布式·架构·llm·vllm·sglang
茨球是只猫6 小时前
A 股 AI 量化全链路系统技术拆解:分层架构、双引擎验证与低换手实盘闭环
人工智能·机器学习·架构·量化交易
用户7754963581586 小时前
从 Socket 到网卡:数据包在 Linux 内核里的全生命周期
架构