搜 索

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

  • 2阅读
  • 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
异步任务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 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 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 的端口。

为什么是 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 路由、走只读副本——那是增长期的事。

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

配置: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 包提出来
服务数量 > 10Kong + Backstage + OTel 三件套该上的都上

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


总结

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

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

具体落到这套方案上:

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

先活下来,再谈优雅。


评论区
暂无评论
avatar