搜 索

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

  • 31阅读
  • 2026年05月31日
  • 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 打标签,而不是只用 latestlatest 是飘的,出了问题你都不知道线上跑的到底是哪次提交。SHA 标签让每次部署可追溯、可回滚。latest 留着给"图省事手动拉取"用。
  • 测试不过就别构建go test 放在 docker build 前面,省 Actions 的构建分钟数——免费额度也是钱。
  • 多阶段 Dockerfile。Go 的优势就是能编译成静态二进制,最终镜像用 scratchdistroless,几十 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),是"长出来"而不是"推倒重来"。
完全体那篇讲的是"架构刚好够用、且能随业务生长"。这篇是它的另一半——在业务还没生长之前,先别让架构把你拖死

先活下来,再谈优雅。


评论区
暂无评论
avatar