通用后台系统架构设计(MVP 版):3 个应用,把成本压到地板上

claw 12 分钟 4407 字 0 0

前言

上一篇我把通用后台的"完全体"架构画了出来:Kong、Go-zero、Kafka、MongoDB、ES、Python、Rust、Backstage……一套下来技术栈二十多个组件,看着很爽,部署起来就是另一回事了——光是把这些东西在云上拉起来,每个月账单先把我劝退。

这篇是它的"冷启动版本",也就是真正落地一个 MVP 时我会怎么砍。

砍的原则只有一条:MVP 阶段,每多一个需要单独部署、单独运维、单独花钱的组件,都要被审判一次。能不上的坚决不上,能合并的坚决合并。

最终结论很激进:整个系统只在 Sealos 上跑 3 个应用,外加一个白嫖 Vercel 的管理后台。


技术栈全景(MVP 版)

分类完全体方案MVP 选型备注
部署平台K8s 自建Sealos按量计费,托管中间件
网关Kong砍掉Gin 中间件顶上
开发语言GoGo不变
Web 框架GinGin不变
RPCGo-zero砍掉单体,函数调用即"RPC"
ORMGORMGORM不变
后台管理RefineRefine发布到 Vercel
CI/CD(未提)GitHub Actions测试 + 构建镜像
镜像仓库(未提)GHCR 私有仓库免费私有,跟代码同源
异步任务Kafka + AsynqAsynq一个就够
关系型数据库PostgreSQLPostgreSQL不变
连接池PgBouncerPgBouncer跟 PG 打包成一个应用
缓存 / 锁 / 队列RedisRedis一鱼三吃
NoSQLMongoDB砍掉PG 的 JSONB 顶上
全文搜索Elasticsearch砍掉PG 全文检索顶上
配置中心Etcd砍掉环境变量 + YAML
可观测性OTel+Jaeger+Prometheus+Grafana砍掉Sealos 自带监控 + Zap 日志
开发者中心Backstage砍掉一个 README 解决一切
多语言扩展Python + Rust砍掉等真有 AI 需求再说

3 个 Sealos 应用 + 1 个 Vercel 部署,就这。


整体架构

graph TB subgraph 客户端 AppClient[移动 App / Web 前端] AdminClient[运营后台浏览器] end subgraph CICD[CI/CD] GH[GitHub 仓库] GHA[GitHub Actions 测试 + 构建] GHCR["GHCR 私有镜像仓库"] GH --> GHA --> GHCR end subgraph Vercel[Vercel 免费托管] Refine[Refine 管理后台 React CRUD / 静态构建] end subgraph Sealos[Sealos 三应用] App["应用端 App 1 Go + Gin + GORM 内嵌 Asynq Worker"] PG["App 2 PostgreSQL + PgBouncer"] Redis["App 3 Redis 缓存 / 锁 / 任务队列"] end GH -->|前端子目录| Vercel GHCR -->|imagePullSecret 拉取| App AppClient -->|HTTPS| App AdminClient -->|HTTPS| Refine Refine -->|REST API| App App -->|连接池| PG App -->|缓存 / 锁 / Asynq| Redis

整张图就这么大。

注意几个关键决策:

  1. 管理后台不占 Sealos 名额。Refine 本质是个 React 静态站点,扔 Vercel 上免费托管,构建产物走 CDN,它只是通过 REST 调用 Sealos 上那个 Go 应用而已。
  2. Asynq Worker 不单独部署。它和 Gin API 跑在同一个 Go 二进制里,启动时多开一个 goroutine 消费队列。这样"应用端"始终是 1 个应用。
  3. PgBouncer 和 PostgreSQL 打包成一个应用。Sealos 的数据库托管模板通常自带连接池,或者用一个 Pod 跑两个容器(PG + PgBouncer sidecar),对外只暴露 PgBouncer 的端口。
  4. 代码到线上走 GHCR 中转。Go 应用由 GitHub Actions 构建成镜像、推到私有 GHCR,Sealos 凭 imagePullSecret 拉取部署;Refine 那条线交给 Vercel 全自动。详见后文流水线一节。

为什么是 Sealos

选 Sealos 的核心理由,就俩字:省钱。

更准确地说,是省"心智成本"和"现金成本":

  • 按量计费。MVP 阶段没流量,半夜没人访问的时候你不希望还在为闲置的虚拟机付费。Sealos 按实际用量结算,跑得少花得少。
  • 托管中间件开箱即用。PostgreSQL、Redis 在 Sealos 上是模板,点几下就起来了,不用自己装、自己调参、自己搞高可用脚本。MVP 阶段我没空当 DBA。
  • 本质是 K8s,但不用懂 K8s。这点很重要——等业务长大要回到完全体架构时,底层还是 Kubernetes,迁移路径平滑,不会推倒重来。
一句话:Sealos 让你"先用 PaaS 的姿势开发,将来有 K8s 的退路"。

应用端:Go + Gin + GORM + Asynq 的单体

完全体里那条 Kong → Gin → Go-zero RPC 的链路,在 MVP 里被压扁成一个单体进程。

graph TB subgraph 单一 Go 二进制 Router[Gin 路由] subgraph 中间件链 替代 Kong CORS[CORS] Auth[JWT 鉴权] RateLimit[限流 Redis 计数] Recovery[Recovery + Zap 日志] end Handler[Handler 参数绑定 + 校验] Service[Service 纯业务逻辑] Repo[Repository GORM] AsynqWorker[Asynq Worker 同进程 goroutine] end Router --> CORS --> Auth --> RateLimit --> Recovery --> Handler Handler --> Service --> Repo Service -.->|入队| AsynqWorker AsynqWorker --> Service

砍掉 Kong,谁来鉴权限流?

Gin 中间件。完全体里把鉴权、限流、CORS 放网关是为了"关注点分离",但那是服务多了之后的奢侈品。MVP 就一个应用,多写几个中间件,比维护一个 Kong 集群划算太多了。限流直接用 Redis 做计数器(INCR + EXPIRE),几十行代码搞定。

砍掉 Go-zero RPC,业务怎么拆?

不拆。MVP 阶段服务拆分是负优化——你还没搞清楚业务边界在哪,拆出来的服务大概率会被推翻重画。单体里用清晰的 Handler / Service / Repository 分层,把边界划在包(package)层面而不是进程层面。将来要拆,把某个 Service 包提出来套上 Go-zero 就是了,业务代码几乎不动。

Asynq Worker 为什么塞进同一个进程?

为了守住"3 个应用"这条线。生产环境里 Worker 和 API 分开部署是对的(互不影响、独立扩缩容),但 MVP 阶段:

func main() {
    // 同一个二进制,两条腿走路
    go startAsynqWorker(redisOpt)  // 后台消费
    startGinServer(addr)           // 前台 HTTP
}

等哪天异步任务把 CPU 吃满影响到 API 响应了,再把 startAsynqWorker 拎出来单独部署——那时候说明你有这个"幸福的烦恼"了,恭喜。


存储层:一个 PG 顶三个组件

完全体里 PostgreSQL、MongoDB、Elasticsearch 各司其职。MVP 里,PostgreSQL 一个人把这活全干了。

graph TB subgraph PostgreSQL 全能选手 Relational["结构化数据 用户 / 任务 / 配置 标准表 + 外键"] JSONB["半结构化数据 JSONB 字段 替代 MongoDB"] FullText["全文检索 tsvector + GIN 索引 替代 ES"] end App[Go 应用] -->|PgBouncer| Relational App -->|PgBouncer| JSONB App -->|PgBouncer| FullText
  • 替代 MongoDB:检测结果、行为事件这类"Schema 灵活"的数据,丢进 PG 的 JSONB 字段。配合 GIN 索引,查询性能在 MVP 的数据量下完全够用,还白送了事务能力。
  • 替代 Elasticsearch:PG 自带的 tsvector + GIN 全文索引,应付中小数据量的搜索绰绰有余。ES 那套分词、集群、JVM 调优,MVP 阶段碰都别碰。
  • PgBouncer 依然保留:这是少数我不肯砍的东西。PG 的连接是重资源,哪怕 MVP,一个写得不好的循环里反复建连,也能把 max_connections 打满。PgBouncer 用 Transaction 模式兜底,成本几乎为零。
⚠️ PgBouncer 的 Transaction 模式不支持会话级 SET 语句,用之前确认 GORM 没有依赖会话状态的行为(比如自定义的会话级参数),否则会踩坑。

Redis:一鱼三吃

Redis 在 MVP 里身兼三职,这是它性价比最高的用法:

graph LR subgraph 单实例 Redis Cache[缓存 热点数据 / 会话 Token] Lock[分布式锁 SET NX EX] Queue[任务队列 Asynq 底层存储] end GinAPI[Gin API] --> Cache GinAPI --> Lock Worker[Asynq Worker] --> Queue

缓存、分布式锁、Asynq 的任务队列,全压在同一个 Redis 上。MVP 阶段数据量小、隔离需求低,没必要分多个实例。

唯一要留个心眼的:Asynq 的队列和你的业务缓存共享内存,如果将来任务积压严重,注意别把内存挤爆导致缓存被驱逐。真到那一步,再拆 Redis 实例不迟。


异步任务:只留 Asynq

完全体里我纠结过 Kafka 和 Asynq 的边界——事件驱动、多消费者、数据管道用 Kafka;延迟、重试、定时用 Asynq。

MVP 阶段这个纠结直接消失:Kafka 不上,全用 Asynq。

graph LR subgraph Asynq 一把梭 Delay[延迟任务 30 分钟后提醒] Retry[可重试任务 发推送 / 邮件] Schedule[定时任务 每日报告] Pseudo[伪事件驱动 任务完成后入队下一个任务] end Redis -->|底层| Delay Redis -->|底层| Retry Redis -->|底层| Schedule Redis -->|底层| Pseudo

Kafka 的价值在于高吞吐、多消费者、数据回放——这些是日活百万级的需求。日活万级以下,那套运维成本(ZooKeeper/KRaft、分区、消费组、Rebalance)纯属自我折磨。

需要"事件驱动"的地方,用 Asynq 任务链模拟即可:任务 A 完成后,在 Worker 里直接 client.Enqueue(taskB)。够用。


管理后台:Refine 白嫖 Vercel

这是省钱的点睛之笔:管理后台一分钱不花,还不占 Sealos 应用名额。

graph LR Dev[git push] --> Vercel subgraph Vercel 免费层 Build[自动构建 Refine / React] CDN[全球 CDN 静态产物] end Vercel --> Build --> CDN Browser[运营浏览器] --> CDN CDN -.->|REST 调用| GoAPI["Sealos 上的 Go API 同一套接口"]

完全体里我设计了独立的 Admin BFF,让运营后台和用户 API 走两条链路,避免运营的慢查询拖垮用户侧。MVP 阶段这个担忧暂时不存在——没那么多并发,也没那么复杂的报表。

所以直接让 Refine 复用应用端那套 REST 接口,通过 RBAC 中间件区分管理员权限即可。Refine 本身是纯前端 React CRUD 框架,vercel deploy 一把梭,免费层的构建额度和 CDN 流量对 MVP 完全够用。

等运营报表真的开始拖慢用户响应了,再在 Go 应用里分出一组 /admin 路由、走只读副本——那是增长期的事。

发布流水线:GitHub Actions + 私有 GHCR

前面把"系统长什么样"讲清楚了,但还差一环:代码是怎么从 git push 变成线上运行的容器的。这一段补上完整的 CI/CD 流程。

整条流水线分两条线,左边 Go 应用,右边 Refine 后台:

graph TB Dev[开发者 git push / PR merge] --> GH[GitHub 仓库] subgraph GoLine[应用端流水线] direction TB GHA[GitHub Actions workflow 触发] Test[go test + go vet + lint] Build[docker build 多阶段构建] Push[推送镜像] GHCR["GHCR 私有仓库 ghcr.io/OWNER/app:sha"] GHA --> Test --> Build --> Push --> GHCR end subgraph AdminLine[后台流水线] direction TB VercelCI[Vercel 自带 CI 监听同仓库 / 子目录] VBuild[Refine 构建 + CDN 发布] VercelCI --> VBuild end GH --> GHA GH --> VercelCI GHCR -->|imagePullSecret 拉取| Sealos["Sealos 应用端 滚动更新"] VBuild --> CDN[Vercel CDN]

为什么镜像要放 GHCR,而不是 Docker Hub?

三个理由,都跟 MVP 的"省"和"稳"对齐:

  1. 私有免费。GHCR 对私有镜像不收费(在 GitHub 免费额度内),而 Docker Hub 的私有仓库数量有限、还有匿名拉取限流。MVP 阶段镜像里多少藏着点不想公开的东西(内部依赖、配置模板),私有是默认选项。
  2. 跟代码同源。镜像和源码在同一个 GitHub 账户/组织下,权限用一套 GitHub Token 管理,不用再去 Docker Hub 单独维护一套凭据。
  3. Actions 原生集成。在 GitHub Actions 里推 GHCR,认证直接用工作流自带的 GITHUB_TOKEN,不用手动配 secret,这是体验上最丝滑的一点。

Go 应用的 workflow

一个最小可用的 .github/workflows/deploy.yml 长这样(省略了细节,重点看流程):

name: build-and-push

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write        # 推 GHCR 必须
    steps:
      - uses: actions/checkout@v4

      # 测试先行,挂了就不浪费构建时间
      - uses: actions/setup-go@v5
        with: { go-version: '1.22' }
      - run: go vet ./... && go test ./...

      # 用工作流自带 token 登录 GHCR,零配置
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # 多阶段构建 + 用 commit sha 打标签
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.sha }}
            ghcr.io/${{ github.repository }}:latest

几个实践要点:

  • 用 commit SHA 打标签,而不是只用 latest。latest 是飘的,出了问题你都不知道线上跑的到底是哪次提交。SHA 标签让每次部署可追溯、可回滚。latest 留着给"图省事手动拉取"用。
  • 测试不过就别构建。go test 放在 docker build 前面,省 Actions 的构建分钟数——免费额度也是钱。
  • 多阶段 Dockerfile。Go 的优势就是能编译成静态二进制,最终镜像用 scratch 或 distroless,几十 MB,拉取快、攻击面小。

Sealos 怎么拉私有镜像

GHCR 是私有的,Sealos 默认拉不到,需要配一个 imagePullSecret:

sequenceDiagram participant GHA as GitHub Actions participant GHCR as GHCR 私有仓库 participant Sealos as Sealos participant PAT as GitHub PAT (read:packages) GHA->>GHCR: push ghcr.io/owner/app:sha Note over Sealos,PAT: 一次性配置:用 PAT 创建 docker-registry secret Sealos->>Sealos: 应用引用 imagePullSecret Sealos->>GHCR: 携带凭据拉取镜像 GHCR-->>Sealos: 返回镜像层 Sealos->>Sealos: 滚动更新,新 Pod 起来旧 Pod 下线

操作上就是在 Sealos 集群里建一个 docker-registry 类型的 secret:

kubectl create secret docker-registry ghcr-cred \
  --docker-server=ghcr.io \
  --docker-username=<你的 GitHub 用户名> \
  --docker-password=<一个 read:packages 权限的 PAT> \
  --namespace=<你的 Sealos 命名空间>

然后在应用的部署配置里引用这个 secret 即可。

⚠️ 这里的 PAT 要单独签发、只给 read:packages 这一个权限,别图省事用一个 full access 的 token。Sealos 这边只需要"拉镜像"的能力,给多了纯属给自己埋雷。

触发更新:MVP 阶段别整太复杂

镜像推到 GHCR 之后,怎么让 Sealos 用上新镜像?按"折腾程度"从低到高有三档:

方式折腾程度适合阶段
Sealos 控制台手动改镜像 tag、点重启⭐ 最省事MVP 第一周,一天发不了几次
Actions 里加一步 kubectl set image 远程触发⭐⭐发布频繁起来之后
上 Argo CD / Flux 做 GitOps 自动同步⭐⭐⭐增长期,回到完全体时

MVP 阶段我建议直接用第一档或第二档。GitOps 那套(Argo CD)很香,但它本身又是一个要部署、要运维、要学习的组件——这跟本文"把成本压到地板"的主旨是冲突的。等服务多了、发布频繁了再上不迟。

一句话总结这条流水线:push 代码 → Actions 测试+构建 → 推私有 GHCR → Sealos 拉取滚动更新。Refine 那条线交给 Vercel 全自动,你只管 git push。

配置与可观测性:能少则少

配置:Etcd 砍掉。静态配置走 YAML,敏感信息(DB 密码、JWT 密钥)走 Sealos 的环境变量 / Secret 注入。功能开关这种"动态配置",MVP 阶段重启一下服务也能接受,没必要为了热更新引入配置中心。

可观测性:OTel + Jaeger + Prometheus + Grafana 这套"三件套"全砍。MVP 阶段:

  • 日志:Zap 结构化日志,输出到 stdout,Sealos 控制台直接看。
  • 指标:用 Sealos 自带的应用监控面板(CPU / 内存 / 请求),够用。
  • 链路追踪:单体应用,一次请求基本不跨进程,日志里带个 request_id 串起来就行,不需要 Jaeger。
真正需要分布式追踪,是服务拆成一堆微服务之后的事。MVP 是个单体,追踪个啥。

典型场景走读

场景:用户记录一条任务

sequenceDiagram participant App as 客户端 participant Gin as Go 应用 (Gin) participant Redis as Redis participant PG as PostgreSQL participant Worker as Asynq Worker (同进程) App->>Gin: POST /api/tasks {title, duration} Gin->>Gin: 中间件: JWT 鉴权 + Redis 限流 Gin->>PG: GORM 写入任务记录 Gin->>Redis: 更新今日统计缓存 Gin->>Redis: Asynq 入队 "成就检查" 任务 Gin-->>App: 200 OK {taskId} Note over Worker: 同进程异步消费 Worker->>Redis: 取出任务 Worker->>PG: 检查并更新成就 Worker->>Redis: Asynq 入队 "推送通知" 任务

整个链路在一个进程内完成,没有跨服务调用,没有网关跳转。延迟低,排查问题就看一份日志。这就是单体在 MVP 阶段的爽点。


它怎么长大?

MVP 不是终点,是起点。这套最简架构留好了回到完全体的路:

触发信号加什么从哪长出来
异步任务拖慢 APIAsynq Worker 独立部署把同进程的 goroutine 拎成第 4 个应用
运营查询拖慢用户侧Admin BFF / 只读副本Gin 里分出 /admin 路由组
多消费者 / 数据回放需求Kafka替换 Asynq 的"伪事件驱动"部分
全文搜索性能不够Elasticsearch从 PG 的 tsvector 迁出去
服务边界清晰且团队变大Go-zero RPC 拆服务把 Service 包提出来
发布频繁 / 想要自动同步Argo CD / Flux(GitOps)在现有 GHCR 镜像基础上接 GitOps
服务数量 > 10Kong + Backstage + OTel 三件套该上的都上

每一步都是"被业务推着走",而不是"提前设计好"。


总结

MVP 架构的核心思路,和完全体正好相反:

完全体追求"边界清晰",MVP 追求"成本最低"。

具体落到这套方案上:

  • 3 个 Sealos 应用(Go App / PG+PgBouncer / Redis)+ 1 个免费 Vercel(Refine),把现金成本压到地板。
  • 发布流水线全免费:GitHub Actions 测试构建、私有 GHCR 存镜像、Sealos 拉取部署,git push 即上线,一条龙不花钱。
  • 能合并的全合并:Worker 进主进程、PgBouncer 跟 PG 打包、Redis 一鱼三吃、PG 顶替 Mongo 和 ES。
  • 能砍的全砍:Kong、Go-zero、Kafka、Etcd、可观测性三件套、Backstage、多语言扩展——这些都是"长大以后"的事。
  • 但留好退路:底层是 Sealos(K8s),分层是 Handler/Service/Repo,镜像和源码同源在 GitHub,将来要回到完全体(甚至上 GitOps),是"长出来"而不是"推倒重来"。
完全体那篇讲的是"架构刚好够用、且能随业务生长"。这篇是它的另一半——在业务还没生长之前,先别让架构把你拖死。

先活下来,再谈优雅。


评论

还没有评论,来说点什么吧。

发表评论

Ctrl + Enter 快速提交
Enter 搜索Esc 关闭