1. 从镜像到 Pod:one-api 在 k8s 里到底卡在哪
上一篇我们把 one-api 打包成了镜像,这一步真正把它推进 k8s 集群时,问题往往不在镜像本身,而在三件事:密钥怎么注入、端口怎么对齐、健康探针怎么判断“真的活了”。one-api 是一个开源的 OpenAI 接口聚合与分发网关,它能把多个上游渠道统一成一套 Key 对外提供服务,适合自建 AI 网关、团队内部统一计费、给多个应用共享一个出口的场景。把它容器化部署到 k8s,本质就是让这套网关以 Deployment 的形式常驻,用 Secret 托管数据库、Redis、会话密钥,用探针保证滚动更新时不把流量打到半成品 Pod 上。
我试过直接拿明文环境变量跑,结果一次kubectl describe pod里SQL_DSN明晃晃躺在 Events 里,排查完赶紧改成 Secret。所以这篇的重点不是“能不能跑”,而是“跑得干净、跑得可复制”。下面会交付一份可直接套用的deployment.yaml、Secret和config.toml骨架,配合 TaoToken 统一 Key 通道完成密钥注入与服务暴露,最后用kubectl apply和就绪验证动作一次性跑通。
需要先明确一点:one-api 自身监听端口默认是 3000,但很多团队会改成 5175 之类避免和集群里其他服务撞车。端口一旦改,探针、Service、args 三处必须同步,否则 Pod 会一直CrashLoopBackOff或者永远不 Ready。这是本篇最容易踩的坑,后面会单独拆开讲。
2. 前置准备:TaoToken 统一 Key 通道与集群侧检查
在写 YAML 之前,先把“密钥从哪来”这件事定下来。one-api 需要连接 MySQL 存渠道和额度、连接 Redis 做缓存和会话,还需要一个SESSION_SECRET给会话签名。这些都属于敏感信息,不该写进镜像,也不该写进 ConfigMap。TaoToken 在这里扮演的是统一 Key/API 通道的角色:你可以在它的控制台里集中管理上游模型的访问凭证,one-api 通过配置引用这套通道,避免把多个上游 Key 散落在各个环境变量里。
具体操作上,先到 TaoToken 控制台创建 API Key,地址是https://taotoken.net/api-keys,这个页面会生成后续 one-api 渠道配置要用的凭证。如果你还没决定用哪种接入方式,可以先看接入文档https://taotoken.net/doc确认 base_url 和鉴权头的写法。对于长期跑编码类 Agent 或需要稳定额度的场景,Coding Plan 页面https://taotoken.net/coding-plan里有对应的套餐说明,按需选择即可。想先验证模型通不通,用模型对话页https://taotoken.net/chat发一条测试请求最直接。
集群侧要确认三件事:一是kubectl能正常连上目标 namespace;二是集群里已经有可用的 MySQL 和 Redis(可以是集群内的 Service,也可以是外部地址);三是镜像已经推到集群能拉取的仓库。可以用下面几条命令快速自检:
kubectl config current-context kubectl get ns kubectl get svc -A | grep -E 'mysql|redis'如果 MySQL 和 Redis 是集群内的,记下它们的 Service 名和端口,比如mysql.default.svc.cluster.local:3306、redis.default.svc.cluster.local:6379。这两个地址会写进 Secret 的SQL_DSN和REDIS_CONN_STRING。注意 DSN 的格式是用户:密码@tcp(主机:端口)/库名,Redis 是redis://主机:端口,格式写错 Pod 起不来但日志不一定直白,后面排障章节会讲怎么定位。
3. 可复制骨架:Secret、config.toml 与 deployment.yaml
这一节是全文核心,三份骨架按顺序创建即可。先建 Secret,再建 ConfigMap 挂 config.toml,最后 apply Deployment。顺序反了 Pod 会因为找不到 Secret 卡在CreateContainerConfigError。
3.1 Secret:把敏感信息从 YAML 里赶出去
Secret 用stringData写明文,kubectl 会自动做 base64 编码,比手动echo -n | base64省事且不容易出错。把下面内容存成one-api-secret.yaml:
apiVersion: v1 kind: Secret metadata: name: one-api-conf namespace: default type: Opaque stringData: sql-dsn: "oneapi:YourStrongPass@tcp(mysql.default.svc.cluster.local:3306)/one-api" redis-conn-string: "redis://redis.default.svc.cluster.local:6379" session-secret: "replace-with-a-long-random-string"三个 key 分别对应数据库连接、Redis 连接、会话密钥。session-secret建议用openssl rand -hex 32生成,别用random_string这种。生产环境如果 MySQL 有独立账号,把oneapi:YourStrongPass换成实际账号密码。
3.2 config.toml:端口与日志路径的集中声明
one-api 支持用配置文件覆盖部分启动参数。把下面内容存成one-api-configmap.yaml,挂到容器里作为/data/config.toml:
apiVersion: v1 kind: ConfigMap metadata: name: one-api-config namespace: default data: config.toml: | port = 5175 log-dir = "/data/logs"这里把端口定成 5175,日志目录定成/data/logs。端口这个值必须和 Deployment 里的args、探针、Service 三处完全一致。日志目录建议挂一个 emptyDir 或 PVC,否则 Pod 重建日志就没了;排查阶段用 emptyDir 够用,长期运行换 PVC。
3.3 deployment.yaml:探针、args 与 Secret 引用
这是最完整的一份,直接存成one-api-deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: one-api namespace: default labels: app: one-api spec: replicas: 1 selector: matchLabels: app: one-api template: metadata: labels: app: one-api spec: containers: - name: one-api image: your-registry/one-api:1.0.0 args: - '--port' - '5175' ports: - containerPort: 5175 env: - name: SQL_DSN valueFrom: secretKeyRef: name: one-api-conf key: sql-dsn - name: REDIS_CONN_STRING valueFrom: secretKeyRef: name: one-api-conf key: redis-conn-string - name: SESSION_SECRET valueFrom: secretKeyRef: name: one-api-conf key: session-secret volumeMounts: - name: config mountPath: /data/config.toml subPath: config.toml - name: logs mountPath: /data/logs startupProbe: exec: command: - sh - -c - "wget -q -O - http://127.0.0.1:5175/api/status | grep -o '\"success\":\\s*true'" initialDelaySeconds: 15 periodSeconds: 10 failureThreshold: 22 readinessProbe: exec: command: - sh - -c - "wget -q -O - http://127.0.0.1:5175/api/status | grep -o '\"success\":\\s*true'" initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 livenessProbe: exec: command: - sh - -c - "wget -q -O - http://127.0.0.1:5175/api/status | grep -o '\"success\":\\s*true'" initialDelaySeconds: 30 periodSeconds: 20 timeoutSeconds: 3 failureThreshold: 3 volumes: - name: config configMap: name: one-api-config - name: logs emptyDir: {}几个关键点解释一下。args里的--port 5175是程序启动参数,和 config.toml 里的port保持一致,双保险。探针用wget请求/api/status,这个接口返回的 JSON 里有"success":true,用grep -o抓出来判断,比单纯看 HTTP 200 更准,因为 one-api 在数据库没连上时也可能返回 200 但 success 为 false。startupProbe的failureThreshold给到 22,是因为首次启动要跑数据库迁移,慢的时候几十秒很正常,给足时间避免被误杀。readinessProbe和livenessProbe分开,前者控制流量接入,后者控制重启,职责不混。
3.4 Service:把 5175 暴露出去
存成one-api-service.yaml:
apiVersion: v1 kind: Service metadata: name: one-api namespace: default spec: selector: app: one-api ports: - name: http port: 80 targetPort: 5175 type: ClusterIP集群内访问用http://one-api.default.svc.cluster.local,对外暴露再按需加 Ingress 或改type: NodePort。targetPort必须等于容器实际监听端口 5175。
4. 验证请求:apply 之后怎么确认真的跑通
四份 YAML 准备好后,按 Secret → ConfigMap → Deployment → Service 的顺序 apply:
kubectl apply -f one-api-secret.yaml kubectl apply -f one-api-configmap.yaml kubectl apply -f one-api-deployment.yaml kubectl apply -f one-api-service.yaml然后盯 Pod 状态:
kubectl get pods -l app=one-api -w正常会看到0/1 Running一段时间后变成1/1 Running。如果一直0/1,说明 readinessProbe 没过,先别急着删,用下面命令看探针失败原因:
kubectl describe pod -l app=one-api | tail -30 kubectl logs -l app=one-api --tail=100Pod Ready 之后,直接在集群内验证接口。起一个临时 Pod 发请求:
kubectl run curl-test --rm -it --image=curlimages/curl -- \ curl -s http://one-api.default.svc.cluster.local/api/status返回的 JSON 里应该能看到"success":true,同时system_name是One API,version有值。这一步通了,说明数据库连接、Redis 连接、会话密钥三样都生效了。接着验证端口转发到本地:
kubectl port-forward svc/one-api 8080:80浏览器打开http://localhost:8080,能看到 one-api 的登录页就说明服务暴露没问题。首次登录用默认管理员账号,进去后到渠道页配置 TaoToken 的 API Key 和 base_url,具体字段参考接入文档https://taotoken.net/doc。配好渠道后,在模型对话页https://taotoken.net/chat发一条测试消息,确认整条链路从 one-api 到上游是通的。
如果你打算把 one-api 作为长期编码 Agent 的统一出口,建议在 TaoToken 的 Coding Plan 页面https://taotoken.net/coding-plan看一下额度方案,避免高峰期被限流。控制台https://taotoken.net/console里能看调用量和余额,方便和 one-api 的计费对账。
5. 本篇常见错排查:从 CrashLoop 到探针误杀
部署 one-api 到 k8s,报错基本集中在下面几类,按出现频率排。
第一类是CreateContainerConfigError,Pod 起不来,describe里写secret "one-api-conf" not found。原因是 Secret 没建或者 namespace 对不上。确认kubectl get secret -n default里有one-api-conf,且 Deployment 的 namespace 和 Secret 一致。如果 Secret 建在别的 namespace,要么改 Deployment 的 namespace,要么在secretKeyRef里没法跨 namespace 引用,只能重建。
第二类是CrashLoopBackOff,日志里出现dial tcp: lookup mysql on 10.96.0.10:53: no such host。这是SQL_DSN里的主机名解析不了。集群内 Service 的完整域名是服务名.namespace.svc.cluster.local,只写mysql在部分 DNS 配置下能解析,但跨 namespace 就不行。统一写全限定名最稳。如果 MySQL 在集群外,直接写 IP 或外部域名。
第三类是探针一直失败但日志显示程序正常。常见原因是端口不一致:args写 5175,探针却请求 3000,或者反过来。三处端口必须完全一致——args、containerPort、探针 URL、Service 的targetPort。改端口时用grep -rn 5175把四份 YAML 全扫一遍,别漏。
第四类是startupProbe误杀。首次启动数据库迁移慢,如果failureThreshold给太小,比如 3,Pod 会在迁移完成前被重启,然后陷入循环。给到 20 以上,periodSeconds10 秒,等于给 200 秒以上启动窗口,足够。等稳定运行后可以适当调小。
第五类是日志目录权限问题。如果挂载的 PVC 权限不对,one-api 写/data/logs失败会退出。排查阶段先用emptyDir,确认程序能跑再换 PVC。emptyDir的日志在 Pod 重建后丢失,但排查够用。
第六类是 Redis 连接串格式错。redis://redis.default.svc.cluster.local:6379是对的,写成redis://redis:6379/0带库号也行,但写成redis:6379缺协议头会解析失败。日志里会报invalid redis URL,对着改即可。
6. 后续动作:把 Key 通道和部署串成一条线
到这里,one-api 在 k8s 里的 Deployment、Secret、config.toml 骨架已经跑通,Pod 就绪、接口返回success:true、端口转发能打开登录页。接下来要做的是把 TaoToken 的 Key 通道真正接进 one-api 的渠道配置里,让上游调用走统一出口。这一步在 one-api 后台的渠道管理页完成,填入 TaoToken 控制台生成的 API Key 和接入文档里给的 base_url,保存后发一条测试请求验证。
如果后续要扩副本,注意 one-api 的会话和额度依赖 Redis 和 MySQL,多副本时SESSION_SECRET必须所有副本一致,否则会话会串。滚动更新时readinessProbe保证新 Pod 就绪才接流量,startupProbe保证慢启动不被误杀,这两条已经写在骨架里,扩副本直接改replicas即可。密钥轮换时只改 Secret 再kubectl rollout restart deployment one-api,不用动镜像。需要长期稳定额度或跑编码 Agent 的话,Coding Plan 页面https://taotoken.net/coding-plan有对应方案,控制台https://taotoken.net/console可以随时看用量。