Lazy loaded image
网关轮子项目09丨从本地可跑到可交付
字数 3208阅读时长 9 分钟
2025-11-22
type
Post
status
Published
date
Nov 22, 2025
slug
gateway009
summary
tags
gateway
category
icon
password
摘要:我怎么补上 Docker / Kubernetes 交付能力
这篇我想单独写出来,是因为前面几篇文章把接入、治理、fallback、观测和真实上游都收了一遍,但还有一个问题一直没单独讲清楚:本地 go run 能跑,不等于别人能稳定把它拉起来。代码能跑,通常只说明“我自己知道怎么起”;交付能成立,才说明“别人按固定方式也能起,而且起完以后知道怎么核对它是不是健康”。
我一开始也把这件事想轻了。仓库里有 cmd/gateway,有 cmd/devinit,本地 MySQL 和 Redis 也能起,文档里再给几行命令,看起来像是差不多了。再往下看我才发现,最容易散掉的不是某一个命令,而是启动顺序、依赖、配置入口、健康检查,还有“别人起完以后到底怎么核对”。这些东西不一起收进去,项目还是更像“我的本地工程”。可交付性从来不只是把程序跑起来。
这篇我只围着一句话写:本地能跑,不等于别人能稳定拉起来;真正有交付价值的,是把启动、依赖、配置、健康检查和最小验证方式一起收进去。后面所有内容都只服务这句。先把交付这件事里该补的东西说清楚,再谈 Docker 和 Kubernetes。

一、本地能跑和可交付差在什么地方

本地 go run 的价值当然有,它能让我先把 handler、service、上游选路、数据库和 Redis 这些东西接通。但这条路径默认有一个前提:我自己知道先起什么、后起什么,也知道失败以后去哪里看。别人拿到仓库时,没有义务先猜出“要先起 MySQL,再起 Redis,再跑 devinit,最后才起 gateway”;也没有义务自己发明一套健康检查和验收方法。本地能跑解决的是“我会跑”,可交付解决的是“别人也能按固定方式跑”。
如果这一步没收好,最容易出现两种假成功。第一种是假成功:进程起来了,但库还没初始化完;第二种是假成功:对象都创建出来了,但你还没核对它到底能不能接流量。代码还在,服务也在,看起来像是起了,实际上别人还是得靠猜。交付这件事最怕的不是报错,而是“看起来像成功”。

二、我最后把交付收成了五块责任

它们都不花哨,但缺了哪一块,项目都会从“可交付”退回“本地项目”。这篇最重要的不是技术名词,而是这五块责任有没有一起成立。
责任
仓库里的落点
现在能回答什么
启动顺序和等待动作
Compose depends_on、K8s job.yaml 加上 wait / rollout 命令
初始化和 gateway 启动有固定检查点
依赖和配置入口
Compose environment、K8s 里的 ConfigMap + Secret + envFrom、docs/deployment/configuration.md
MySQL、Redis、provider、OTLP 这些入口是固定的
健康状态表达
/healthz、/readyz、/metrics、/debug/providers
进程活着、能不能接流量、当前上游状态可以分开看
最小验证动作
scripts/gateway-smoke-check.sh
拉起来以后怎么确认它真的工作被收成固定动作
静态 / 运行时交付检查
scripts/stage7-verify.sh、scripts/validate-deployments.rb、scripts/k8s-production-cluster-check.sh
不只是看文件在不在,还会继续看这条交付路径能不能站住
这张表对我最有价值的地方,是它把“交付能力”这个很虚的词,压成了几件能在仓库里直接核对的东西。这样后面再看 Compose、K8s、smoke 和 verify,主线就不会散。我关心的是“交付这几块有没有真的补进去”。

三、Docker Compose 先把本地顺序写进仓库

为什么?因为这条路径先回答“别人在一台机器上能不能把整个依赖链稳定拉起来”。
这个仓库的 Compose 不是只有一个 gateway 容器。deployments/docker/docker-compose.yml 里实际收了四个服务:mysql、redis、devinit、gateway。这里最有价值的地方,是启动顺序已经写进配置里了:gateway 要等健康的 mysql、健康的 redis,还要等 devinit 成功跑完以后才启动;devinit 自己也要先等 mysql healthy。Compose 这一步先把“起服务的顺序靠人记”改成了“顺序由仓库自己表达”。
docker compose -f deployments/docker/docker-compose.yml up -d --build docker compose -f deployments/docker/docker-compose.yml ps docker compose -f deployments/docker/docker-compose.yml logs devinit
别人按这组命令拉起来以后,我关注的是几个固定信号:mysql healthy、redis healthy、devinit 退出码是 0,然后 gateway 再进入 healthy。Compose 不只是把容器起起来,它还把“起成了没有”一起说清楚了。
mysql ─┐ ├─> devinit ─┐ redis ──┘ ├─> gateway mysql ───────────────┘ redis ───────────────┘
仓库根目录的 Dockerfile 也不只是打一个二进制。它用多阶段构建一起产出了 /app/gateway 和 /app/devinit,然后让 Compose 里的 devinit 服务直接跑 /app/devinit,gateway 服务再跑 /app/gateway。这样镜像里带的就不只是 gateway 本身,初始化入口也一起带上了。这里补的是交付时真正要用到的两个入口。
Compose 这条线还把配置入口一起收进来了。gateway 的 APP_MYSQL_DSN、APP_REDIS_ADDRESS、APP_SERVER_ADDRESS 都是固定的,devinit 的 MySQL DSN 也是固定的。用户按文档直接 docker compose up -d --build,就能起出一个带 MySQL、Redis 和 gateway 的最小环境。Compose 这条线已经把项目从“我自己会跑”推进到了“别人也能在一台机器上重复跑”。

四、Kubernetes 这一步最容易想当然

为什么?因为很多人看到 Job 和 Deployment 都写了,就会顺手把“顺序也成立了”一起想当然带过去。
deployments/k8s/ 这套基础部署文件很小,但最基本那几样东西已经摆全了:namespace.yaml、configmap.yaml、secret.example.yaml、job.yaml、deployment.yaml、service.yaml。配置放在 ConfigMap,敏感信息放在 Secret,devinit 作为单独的 Job 跑一次,gateway 再作为 Deployment 对外提供服务。到这里,部署对象已经被写成固定文件了。K8s 这一步先把“这个服务该怎么发出去”写成了固定对象。
但我再往下看才发现,最容易想当然的地方也在这里。Compose 里可以直接把等待关系写进 depends_on,K8s 里把 Job 和 Deployment 都写出来,并不代表顺序就天然成立。你如果只是 kubectl apply -k ...,对象当然会被创建出来,可 devinit 还没跑完时,gateway 也可能已经起来了。K8s 这条线最容易出现的假成功,是对象创建出来了,但初始化和就绪顺序还没收住。
所以这个仓库没有把“交付成功”收在 apply 这一步,而是继续收在后面的部署流程里:先 kubectl apply -k deployments/k8s-overlays/production,再 wait --for=condition=complete job/llm-access-gateway-devinit,最后再看 rollout status deployment/llm-access-gateway。顺序不是 K8s 天生替我保证的,是仓库把这几步固定成了流程。
kubectl kustomize deployments/k8s-overlays/production kubectl apply -k deployments/k8s-overlays/production kubectl -n llm-access-gateway wait --for=condition=complete job/llm-access-gateway-devinit --timeout=120s kubectl -n llm-access-gateway rollout status deployment/llm-access-gateway --timeout=180s
deployment.yaml 里我关注的还有探针。它把 readinessProbe 指到 /readyz,把 livenessProbe 指到 /healthz。这和 internal/api/handlers/health.go 的语义是对上的:/healthz 只回答进程活着没有,/readyz 会继续看当前还有没有可用上游。这不是唯一正确的做法,但对这个网关是合适的。好处是所有上游都不可用时,服务不会继续装作自己 ready;代价是上游波动会直接影响 readiness。/healthz、/readyz、/metrics 在这里已经不是附属接口,它们就是交付面的一部分。
deployments/k8s-overlays/production/ 又往前补了一层:副本数从 1 提到 2,滚动更新策略、PodDisruptionBudget、Prometheus 抓指标的注解、Ingress、NetworkPolicy、pod security 默认值都加进来了,production-hpa 还把 HPA 单独做成了可选层。但这里我也不想把话写过头,因为 MySQL、Redis、TLS、镜像仓库、OTLP collector、provider key、NetworkPolicy 这些环境里的东西,还是要由外部环境自己补。这里成立的是“最小交付闭环已经有了”,还不是“已经能直接上生产”。

五、smoke / verify 脚本把验收方式固定了下来

交付最容易散掉的是,部署完以后每个人都用不同办法判断它有没有真的起来。
scripts/gateway-smoke-check.sh 这条脚本比它看起来更重要。它不是随手写的 curl 集合,而是把最低限度的验收动作固定下来了:/healthz、/metrics、/v1/models、/v1/usage、非流式 chat、流式 chat、还有内置 loadtest。ASSERT=true 时,它还会把 HTTP 200、X-Trace-Id、object、[DONE]、以及 load test 的失败计数一起核对掉。smoke script 的价值不只是自动化,而是把最小验收动作固定成了仓库自己的一套检查。
仓库里连“最小通过长什么样”都留了出来。比如 docs/local-development.md 里直接把 smoke 之后应该看到的信号写成了很短的原始输出:/healthz 要回 HTTP/1.1 200 OK,/metrics 里要能看到 lag_http_requests_total 和 lag_http_request_duration_milliseconds_count。这几行不长,但已经够用,因为它告诉我,起完以后不只是看见进程活着,还要看见最小运行信号出来。这里已经有一条很轻但很实的运行时证据。
HTTP/1.1 200 OK X-Trace-Id: ... lag_http_requests_total ... lag_http_request_duration_milliseconds_count ...
scripts/stage7-verify.sh 又把这件事往前收了一步。它把交付检查拆成了两条:static 会跑 go test ./...、go vet ./...、scripts/validate-deployments.rb、dashboard JSON 校验和必需资产清单;runtime 则直接跑 ASSERT=true ./scripts/gateway-smoke-check.sh。这样一来,仓库里最后收住的就不只是“有几条脚本”,而是“一道静态检查”和“一道运行时检查”。到这里,“怎么验收它真的拉起来了”已经不再靠人临场发挥。
ASSERT=true ./scripts/gateway-smoke-check.sh ./scripts/stage7-verify.sh static ./scripts/stage7-verify.sh runtime
scripts/validate-deployments.rb 和 scripts/k8s-production-cluster-check.sh 又把这条线拉得更稳。前者会同时检查 Compose 展开后的结果、K8s 部署文件、probe 路径、Service 端口、overlay 渲染结果;后者会在有真实集群的时候再去做 kubectl kustomize、server-side dry-run、metrics API 检查和 apply 前 checklist。这样仓库自己也把两件事分开了:一件是“交付文件写全了没有”,另一件是“具体环境里能不能真的发出去”。所以我想说的是,这一步让交付检查能落地,也没有把结论写过头。

六、到这里,这个项目多了什么

到这里,这个项目多出来的,不只是 Compose 文件、Dockerfile、K8s 部署文件和几条脚本。更准确地说,是仓库已经把“怎么把依赖拉起来、怎么等初始化完成、怎么把配置放到统一入口、怎么分开看活着和 ready、怎么做最小验收”都收成了固定动作。
这一步对整条路线也很重要。前面写治理、fallback、观测、真实上游验证,更多是在说明“我知道系统内部怎么工作”;补上 Docker、K8s、smoke 和 verify 以后,文章开始还能说明“我知道怎么把它交出去”。这篇想说的是,我不只会做功能,我也会把它交付出去。

总结

本地能跑,不等于别人能稳定拉起来;真正有交付价值的,是把启动、依赖、配置、健康检查和最小验证方式一起收进去。对这个网关来说,Compose 把本地顺序写进了仓库,K8s 把部署对象和等待动作收成了流程,smoke / verify 又把“怎么确认它真的起来了”固定成了动作。交付能力是一整条别人也能重复执行的启动和验收路径。
回到首页