跳到主要内容
查看文档索引

一人公司怎么搭生产环境?2 个 K3s 节点、4 层架构和 7 步发布法

从 GeekCode 当前的两节点 K3s 环境出发,讲清共享数据层、业务部署、跨区调度,以及一次可以回滚的发布应该怎么做。

搭建环境的第一步,不是安装 Kubernetes,也不是先写两百行 YAML。你先要回答一个更朴素的问题:一次发布失败后,数据会不会丢,服务能不能退回去?

如果这个问题还没有答案,那它就还不是一个可以托付业务的环境,只是一台能跑程序的机器。

这篇文章不准备复制一套“标准云架构”。我直接拆解 GeekCode 现在使用的生产环境:它怎么分层,数据放在哪里,业务怎么进入集群,为什么增加了 US 节点却没有把数据库搬过去。它不是唯一答案,但是一个真实跑着的样本。

本文的环境快照截止到 2026 年 8 月 31 日。架构会继续变,但分层、隔离、验证和回滚这四个原则不会轻易变。

先看全图:现在的环境分成四层

用一张简化图来看,用户的一次请求会穿过四层:

用户浏览器
    │ HTTPS
Cloudflare(DNS、边缘 TLS、缓存)
    │ HTTP 回源
NGINX Ingress(按域名和路径分流)
    ├─ 静态站点 → ExternalName Service → Cloudflare R2
    ├─ Web / API → ClusterIP Service → 无状态 Deployment
    └─ 运营与 AI 服务 → OpenLIT / LLM 网关
                                      │ 集群内 Service DNS
infra namespace
    ├─ PostgreSQL:持久业务数据
    ├─ Redis:缓存、会话、队列、锁和 Pub/Sub
    └─ ClickHouse:行情、事件、Trace 和分析数据

Cloudflare R2(集群外):静态发布物和对象存储

这四层分别解决四件事:边缘层处理域名与公网 TLS,入口层把请求送到对的项目,业务层运行可替换的程序,数据层保留不能随 Pod 一起消失的状态。

如果你刚开始,先记住这句话:业务容器可以随时删,数据不可以随它一起删。

两个节点,不等于两套数据

当前的 K3s 集群有两个 Ready 节点:

节点角色当前职责调度规则
HK 主节点控制面、Ingress、共享数据服务、大部分 Web 和 Worker没有地域标签,也没有业务污点
US 边缘节点边缘计算、文档/CDN 加速,以及 HK 节点无法直接请求的接口或能力标记 topology.kubernetes.io/region=us,并带有 dedicated=us:NoSchedule 污点

这个污点就像门口的门禁。普通工作负载不会被“随手”调度到 US 节点。只有同时声明地域要求和 toleration(容忍规则)的工作负载,才能进去。这样做比在配置里写死一个节点名更稳妥:将来换机器时,只要新节点具有相同的地域与污点即可。

US 节点的价值不是“再放一份相同业务”,而是补上 HK 节点不适合承担的网络位置和能力。适合放在这里的工作包括靠近目标用户执行的边缘计算、文档和静态资源的 CDN 加速,以及 HK 节点因为地域、网络或服务策略无法直接请求的接口。LLM 网关、特定数据源和投递任务都属于这一类。

但要特别注意:US 节点增加的是计算能力、边缘入口和出口路径,不是一套新的 PostgreSQL、Redis 和 ClickHouse。这些共享状态仍然在原有 PVC 所在的 HK 主节点。US Worker 通过集群网络回访它们,所以会承担跨区延迟,也依赖跨区网络。

多一个节点并不会自动带来高可用。这是最容易被误解的地方。

共享 Infra:三个数据服务,一套边界

PostgreSQL、Redis 和 ClickHouse 都放在 infra namespace。业务项目不再为自己启动一套数据库,也不会在项目里塞一份 Docker Compose 当“生产环境”。

服务现在存什么持久化集群内地址
PostgreSQL 15用户、配置、订单、任务元数据等持久业务状态5 GiB RWO PVCpostgres.infra.svc.cluster.local:5432
Redis 7缓存、会话、队列、分布式锁、限流和 Pub/Sub1 GiB RWO PVC,开启 AOFredis.infra.svc.cluster.local:6379
ClickHouse 25.8大量时序数据、行情、分析和 OpenTelemetry Trace10 GiB RWO PVCclickhouse.infra.svc.cluster.local:8123

每个服务现在都是单副本 Deployment,使用 Recreate 更新策略。原因很实际:它们挂载的是单写本地 PVC,同一块盘不能同时交给两个 Pod 写。所以这是一套成本可控、容量足够、但明确不是 HA(高可用)的基线。

这个选择并不丢人。独立开发最怕的不是“架构不够大”,而是系统明明只有一份数据,文档却把它写得像三地五中心。

服务地址和密码必须分开

业务需要知道数据库在哪里,但不需要拿到 Infra 管理员的全部权限。现在的做法把配置分成两类:

  • 可以共享的集成信息:服务名、端口、R2 endpoint 等,渲染到项目自己的 ConfigMap。
  • 不能共享的运行时信息:密码、带凭据的 URL、R2 key 等,只进入项目自己的 Secret。

每个项目都有自己的 PostgreSQL 数据库或 schema、ClickHouse 数据库与用户,以及 Redis key 前缀或独立数据库。业务用户不是 Infra 管理员,一个项目的密码也不能读另一个项目的数据。

数据库和 schema 由部署流程预先创建,而不是等第一个用户请求进来时,让应用“顺便”创建。请求路径不应该兼职当 DBA。

本地开发不能直接使用 Service DNS

postgres.infra.svc.cluster.local 这类地址只能在集群内解析。开发者电脑上的程序要临时访问数据库,就用已经登录集群的 kubectl port-forward,而且只绑定本机回环地址。简单说,它像一条用完就收起来的小水管,不是在公网墙上凿个洞。

kubectl -n infra port-forward service/postgres 15432:5432
kubectl -n infra port-forward service/redis 16379:6379
kubectl -n infra port-forward service/clickhouse 18123:8123 19000:9000

三个数据库 Service 都是 ClusterIP,没有 NodePort、LoadBalancer 或 Ingress,也不允许直接从公网连接。本地调试还要使用单独的项目账号,不复用生产应用 Secret,更不会为了省事又启动一套本地 PostgreSQL、Redis 或 ClickHouse。

业务怎么部署:先分类,再选载体

不是每个网站都需要一个 Pod。当前的业务大致分成四类:

类型当前实例部署方式
静态站点GeekCode 官网、TBTIAstro 构建结果上传到 R2 不可变发布目录;Kubernetes 里只有 namespace、ExternalName Service 和 Ingress,没有 Pod 和 PVC
动态 WebFortune、Strat Thread Web两个无状态副本,通过 ClusterIP Service 接收 Ingress 流量;业务数据全部进入共享 Infra
后台 WorkerStrat Thread 行情采集、量化计算、Agent、投递与事件日历按职责分成独立 Deployment;依赖特定地区接口或出口的任务只调度到 US 节点
边缘与平台能力文档/CDN 加速、OpenLIT、LLM 网关文档与静态资源可利用 US 节点补充边缘路径;OpenLIT 使用单副本 StatefulSet 和 1 GiB SQLite PVC,Trace 进共享 ClickHouse;LLM 网关使用两个 US 节点副本

这里有两个很值得复用的判断。

第一,静态站点没有必要为了“都在 Kubernetes 里”而常年跑一个 NGINX Pod。文件放 R2,Ingress 直接把请求代理到某个确定的发布目录,既少占资源,回滚也只需恢复路由。

第二,需要两个副本的 Web 和 Worker 必须无状态。用户会话、队列任务、执行锁和业务进度不能放在进程内存或容器文件里。否则 Kubernetes 重启的不是一个副本,而是用户的记忆。

为什么每个业务要有自己的 namespace

GeekCode 官网、TBTI、Fortune、Strat Thread 和 OpenLIT 各自拥有 namespace。这不是为了让 kubectl get pods -A 看起来更丰富,而是为了划清边界:

  • 项目只管自己的 Deployment、Service、Ingress、ConfigMap 和 Secret。
  • 凭据不会因为放在一个大 Secret 里而被所有应用共享。
  • 回滚和清理可以锁定一个项目,不碰 infra 的 PVC,也不碰别人的数据。

namespace 不是完整的安全边界,但它是一个很好的开始。真正的隔离还需要项目级数据库账号、Redis 前缀、Secret 和 NetworkPolicy 一起完成。

一次发布怎样才算结束

能运行不等于能发布。现在的发布流程有一条共同主线:

构建不可变产物
    → 运行 Infra 发布前健康检查
    → 保留当前工作负载或路由快照
    → 上传 R2 文件或导入不可变镜像
    → 应用新路由或新 Deployment
    → 验证应用内部、直连源站和公网路径
    → 再运行完整 Infra 健康检查
    → 接受发布

不可变产物,意思是同一个发布标识下的文件或镜像不会被覆盖。静态站点在 R2 中使用 project/releases/<release>/ 目录;动态应用使用可追溯的镜像标识。要回滚时,我们修改指向,而不是试图现场重建一份“应该和上一版差不多”的产物。

如果候选版健康检查失败,脚本会恢复上一份路由或 Kubernetes 资源快照,删除只属于这次失败发布的对象,然后重新跑健康门禁。回滚不删 PVC,也不倒回整个 Redis 数据。

这里最重要的不是脚本有多复杂,而是“发布完成”的定义变了:不是 kubectl apply 返回了 0,而是应用和共享依赖都已通过验证,而且回滚路径仍然可用。

如果你要搭第一个环境,按这个顺序来

不要一开始就复制现在的全部规模。这套环境也是随着真实需求长出来的,不是第一天就有两个地区、三种数据库和一排 Worker。

1. 先定义数据的归属

列出哪些数据必须持久、哪些可以丢失、哪些需要 TTL,然后再选 PostgreSQL、Redis、ClickHouse 或对象存储。不要因为手里有四种工具,就给一张用户表安排四个家。

2. 只部署一套共享数据基线

在这套仓库里,PostgreSQL、Redis 和 ClickHouse 只能通过 deploy/infra/scripts/apply-all.sh 统一渲染和应用。不为新项目复制第二套,也不在业务部署中夹带一个项目私有数据库。

先部署共享基线,再通过健康检查确认 Deployment、PVC、Service endpoint 和认证查询都正常。

./deploy/infra/scripts/apply-all.sh --context "$KUBE_CONTEXT"
./deploy/infra/scripts/healthcheck-all.sh --context "$KUBE_CONTEXT"

3. 为项目创建独立身份

先建 namespace,再由运维流程创建项目数据库、限权用户、ConfigMap 和 Secret。应用不使用 Infra 启动账号,也不把密码提交到项目 .env 文件。

4. 先把业务做成可替换的单副本

先准备不可变镜像、资源上限、非 root 运行、只读文件系统,以及 startup、readiness 和 liveness 检查。确认 Pod 被删后不会丢业务数据,再考虑扩展到两个副本。

5. 打通三条请求路径

依次检查 Pod 内部、Service/Ingress 直连源站和 Cloudflare 公网域名。只检查首页返回 200 还不够;还要确认核心依赖、迁移版本、读写路径和关键静态资源真的可用。

6. 在发布前写好回滚

保留上一份资源快照和不可变产物,明确失败时要恢复哪些对象、怎样保留 PVC、怎样再跑健康检查。回滚方案不应该等事故发生后才开始头脑风暴。

7. 只在有真实地域问题时增加节点

现在的 US 节点是为了解决明确的边缘计算、文档/CDN 加速和地域访问问题,不是为了让架构图多一个云朵。HK 节点无法直接请求的接口或能力,可以放到 US 节点承接;没有这类要求的核心业务仍留在 HK。新节点上线前,要先看标签、污点、延迟、出口、存储位置和故障域,再用 affinity、nodeSelector 和 toleration 表达要求。

这套环境现在的边界

把局限写清楚,比把它们藏在“云原生”三个字后面更有用:

  • 共享 PostgreSQL、Redis 和 ClickHouse 都是单副本,共享状态不具备跨区容灾能力。
  • PVC 使用现有存储位置。增加节点不会自动复制它们,也不能把单写盘盲目扩成两个副本。
  • 数据库只在集群内开放。临时调试走本机回环地址上的 kubectl port-forward,用完就关,不给公网留门。
  • 自建 Docker Registry 和 MinIO 已经退役。新镜像应使用审批过的外部 OCI Registry,新对象存储使用 Cloudflare R2。
  • OpenLIT 自己还有一块 SQLite PVC,所以它也是单实例运营工具,不能被误认为可随意水平扩展的无状态服务。

环境的价值,不在于用了多少个专有名词。它应该让你在凌晨两点也能答出四个问题:请求从哪里进,数据存在哪里,这一版怎么验证,失败了怎么回去。

当这四个问题都有明确答案时,你的第一个环境才算真正搭好。

完 / 继续实践