01 · 架构心法:决策框架与造轮子的时机
“架构不是画框图,是在约束下做决策。“
开场:一次”正确”的失败
小林:我设计了一个系统,用了微服务、消息队列、分布式缓存、分库分表。技术选型都是主流方案。但上线半年后,维护成本极高,团队天天救火。我哪里错了?
老陈:你的系统有多少用户?
小林:……日活 5000。
老陈:团队几个人?
小林:4 个。
老陈:这就是错误所在。
小林:可是这些技术都是”最佳实践”啊。
老陈:脱离约束的’最佳实践’就是灾难。 我问你:
- 日活 5000,需要分库分表吗?不需要。单表几百万行,MySQL 轻松搞定。
- 4 个人的团队,能维护 20 个微服务吗?不能。
- 你需要消息队列吗?如果 QPS 只有几十,不需要。
你引入的每一个组件,都要付出:学习成本、运维成本、调试成本、故障概率。
日活 5000 的系统,一个单体 + 一个 MySQL + 一个 Redis,就够了。
小林:那我什么时候该用微服务?
老陈:这就是今天的核心问题。我们要建立一个判断框架,而不是记住”该用什么”。
第一部分:架构的本质
架构 = 决策 + 后果
架构师的核心工作不是"设计系统",而是"做决策"
一个决策包含三部分:
① 约束(我面对什么)
② 选择(我选什么)
③ 理由(为什么这么选,以及我放弃了什么)
★ 判断一个人是否真的懂架构,看他能不能说出”我放弃了什么”。
只会说”我用微服务因为它好”的人,不懂架构。 能说”我用微服务,代价是运维复杂度和调试难度上升,但我们团队有 20 个人,能扛住”的人,才懂。
架构的三个层次
┌──────────────────────────────────────────┐
│ 业务架构 │
│ · 业务域怎么划分 │
│ · 领域模型是什么 │
│ · 业务流程怎么串 │
│ ★ 决定"系统长什么样" │
├──────────────────────────────────────────┤
│ 应用架构 │
│ · 服务怎么拆分 │
│ · 服务怎么通信 │
│ · 数据怎么流转 │
│ ★ 决定"代码怎么写" │
├──────────────────────────────────────────┤
│ 技术架构 │
│ · 用什么中间件 │
│ · 用什么存储 │
│ · 怎么部署 │
│ ★ 决定"用什么工具" │
└──────────────────────────────────────────┘
★ 顺序很重要:业务架构 → 应用架构 → 技术架构
★ 反过来做(先选技术,再套业务)是本末倒置
老陈:很多人一上来就问”用什么数据库”、“用不用 Kafka”。
这些是技术架构的问题,应该最后才决定。
先搞清楚业务:
- 有哪些实体?它们的关系是什么?
- 核心流程是什么?
- 哪些地方是瓶颈?
- 数据的一致性要求是什么?
答案清楚了,技术选型几乎是唯一的。
架构的质量标准
不是”用了多少先进技术”,而是:
| 标准 | 含义 | 反面 |
|---|---|---|
| 可理解性 | 新人多久能看懂 | 过度抽象、“魔法”太多 |
| 可修改性 | 改一个需求要动多少地方 | 牵一发动全身 |
| 可测试性 | 能不能独立测试 | 必须起整个环境 |
| 可运维性 | 出问题多久能定位 | 日志缺失、无监控 |
| 可扩展性 | 加功能/加机器容易吗 | 需要重构 |
★ 最重要的:可理解性。
一个只有你能维护的系统,是最糟糕的架构。
—— 哪怕它”技术上先进”。
第二部分:造轮子的时机判断
★ 核心判断框架
这是本章最重要的内容。
问自己四个问题,按顺序:
问题 1:现成方案能满足需求吗?
调研所有现成的方案,问:
· 功能满足吗?
· 性能满足吗?
· 能满足未来的需求吗?
★ 如果答案是"能" → 不要造
★ 即使它不是"最优"的
关键:接受”不完美”。
现成方案能满足 90% 的需求
自研能满足 100%,但要 3 个月
★ 除非那 10% 是核心业务,否则不值得
问题 2:这是你的核心竞争力吗?
判断标准:
如果这个能力做到极致,能带来商业价值吗?
例子:
· 电商的推荐算法 → ★ 核心竞争力,值得自研
· 电商的 Web 框架 → 不是,用 gin 就好
· 数据库的存储引擎 → 看情况(如果是做数据库的公司,就是)
· 日志库 → 不是,用 zap
★ 判断标准:这个能力是”业务差异化的来源”吗?
问题 3:你有能力长期维护吗?
自研不是"写出来"就完了,是"维护到系统下线"。
问:
· 团队有人懂这个领域吗?
· 有人能 24 小时 oncall 处理故障吗?
· 半年后新人能接手吗?
· 有测试覆盖吗?
· 有文档吗?
★ 如果答案是"不确定" → 不要造
这是最常被忽略的问题。 很多人造轮子时很兴奋,半年后没人维护,变成”技术债”。
问题 4:成本收益比合理吗?
成本:
· 开发时间(N 人月 × 人力成本)
· 维护成本(长期)
· 机会成本(这些时间可以用来做什么)
收益:
· 性能提升?
· 成本降低(比如省下 License 费用)?
· 能力提升(团队技术积累)?
· 业务价值?
★ 只有当收益明显大于成本时才值得
决策树
要引入一个新技术 / 造一个轮子
│
├─ Q1: 有现成方案吗?
│ ├─ 没有 → 考虑造(但先确认需求真实存在)
│ └─ 有 ↓
│
├─ Q2: 现成方案满足需求吗?
│ ├─ 满足 → ★ 用现成的,结束
│ └─ 不满足 ↓
│
├─ Q3: 差距在哪?(性能?功能?成本?)
│ ├─ 可以通过配置/扩展解决 → 用现成的 + 扩展
│ └─ 不能 ↓
│
├─ Q4: 这是核心竞争力吗?
│ ├─ 不是 → ★ 考虑"换业务方案"而不是"造技术轮子"
│ └─ 是 ↓
│
├─ Q5: 有能力维护吗?
│ ├─ 没有 → ★ 不要造
│ └─ 有 ↓
│
└─ Q6: 成本收益比合理吗?
├─ 不合理 → 不要造
└─ 合理 → 造,但要有 Plan B
案例分析:什么时候该自己写存储引擎
场景 A:做日志收集系统
需求:写多读少,数据按时间查询
现成方案:Elasticsearch
· 功能满足 ✓
· 性能满足 ✓
· 但资源占用大(ES 很吃内存)
★ 结论:用 ES。除非你的日志量大到 ES 成本不可接受
(比如每天 100TB,那可能要自研或者用 ClickHouse)
场景 B:做时序数据库产品
需求:高性能时序数据存储
现成方案:
· InfluxDB(开源版不支持集群)
· TimescaleDB(基于 PG,性能有限)
· 但你做的是"时序数据库产品"
★ 结论:自研。因为存储引擎就是你的产品本身
场景 C:内部监控系统的存储
需求:存监控指标,保留 30 天
现成方案:Prometheus + Thanos / VictoriaMetrics
· 功能满足 ✓
· 性能好 ✓
· 开源免费 ✓
★ 结论:用现成的。
自研一个时序数据库至少 6 人月,而且做得还不如 Prometheus
第三部分:架构演进
三个阶段
阶段 1: 单体 (Monolith)
┌─────────────────┐
│ 所有功能 │
│ ┌───┬───┐ │
│ │用户│订单│ ... │ │
│ └───┴───┘ │
├─────────────────┤
│ 单个数据库 │
└─────────────────┘
优点: 简单、开发快、易测试、易部署
缺点: 耦合、无法独立扩展、技术栈单一
适用: 团队 < 10 人,用户 < 100 万
阶段 2: 服务化 (SOA / 微服务)
┌──────┐ ┌──────┐ ┌──────┐
│用户服务│ │订单服务│ │支付服务│
└───┬──┘ └───┬──┘ └───┬──┘
└────────┼────────┘
┌────▼────┐
│ 服务治理 │
└─────────┘
优点: 独立开发/部署/扩展、技术栈自由
缺点: 分布式复杂度、调试难、运维重
适用: 团队 > 20 人,用户 > 1000 万
阶段 3: 中台 / 平台化
业务前台(快速试错)
↓
业务中台(复用能力)
↓
技术中台(基础设施)
优点: 复用、快速创新
缺点: 中台本身可能变成瓶颈("中台病")
适用: 大公司,多条业务线
★ 演进的原则:不要跳步
错误路径:
创业公司 → 直接上微服务 → 团队 5 个人维护 20 个服务
→ 天天救火 → 产品做不出来 → 公司倒闭
正确路径:
阶段 1: 单体(但要"模块化",为拆分做准备)
↓ 业务增长,团队扩大
阶段 2: 拆分(先拆最独立的、最需要扩展的)
↓ 多条业务线
阶段 3: 平台化
★ 关键:单体时要”模块化”
❌ 糟糕的单体:
所有代码互相调用,没有边界
→ 拆不动
✅ 好的单体("模块化单体" Modular Monolith):
┌─────────────────────────┐
│ module A │ module B │ C │
│ ↓ 只能通过接口调用 ↓ │
└─────────────────────────┘
· 模块间通过明确的接口通信
· 模块内高内聚
· 模块间低耦合
→ 需要时能拆成独立服务
模块化单体的实践:
// 内部目录结构
internal/
├── user/ # 用户模块
│ ├── api.go # 对外接口
│ ├── service.go # 业务逻辑
│ └── repo.go # 数据访问
├── order/ # 订单模块
│ ├── api.go
│ └── ...
└── payment/
└── ...
// 模块间只能通过 api.go 调用
// ★ 禁止直接访问其他模块的 repo 或内部结构第四部分:技术选型的框架
四个维度
① 成熟度 (Maturity)
· 有多少公司在用?
· 社区活跃吗?
· 文档完善吗?
· ★ 出问题能搜到答案吗?
② 匹配度 (Fit)
· 功能匹配需求吗?
· 性能匹配吗?
· 团队技能匹配吗?
③ 成本 (Cost)
· License 费用?
· 硬件成本?
· 学习成本?
· 运维成本?
④ 风险 (Risk)
· 供应商锁定?
· 技术会不会过时?
· 出了问题有人支持吗?
选型矩阵
以”消息队列”为例:
| 方案 | 成熟度 | 匹配度 | 成本 | 风险 | 适用 |
|---|---|---|---|---|---|
| Kafka | ⭐⭐⭐⭐⭐ | 高吞吐、持久化 | 中(运维重) | 低 | ★ 日志、流处理、大数据 |
| RabbitMQ | ⭐⭐⭐⭐⭐ | 功能丰富、灵活路由 | 中 | 低 | 业务消息、任务队列 |
| RocketMQ | ⭐⭐⭐⭐ | 事务消息、顺序消息 | 中 | 中(国内生态好) | 电商、金融 |
| Pulsar | ⭐⭐⭐ | 存算分离、多租户 | 中 | 中(较新) | 多租户、云原生 |
| Redis Stream | ⭐⭐⭐⭐ | 轻量 | 低 | 低 | ★ 小规模、简单场景 |
| 自研 | - | 完全匹配 | 高 | 高 | ★ 几乎不要 |
★ 我的默认建议:
小流量(QPS < 1万): Redis Stream 或 RabbitMQ
简单,运维成本低
中大规模(QPS 1万-100万): Kafka
生态最好,招人容易
需要事务消息: RocketMQ
★ 几乎不要自研消息队列
除非你就是做消息队列的
第五部分:许式伟的架构观
许式伟(七牛云创始人,Go 圈早期的布道者,《Go 语言编程》作者)的架构课有几个核心观点,值得单独讲。
观点 1:架构的第一步是”需求分析”
他的说法:
"很多人做架构,上来就说'我要做一个分布式系统'。
但你问他:你的数据规模多大?峰值 QPS 多少?
他答不出来。
★ 架构的第一步不是画框图,是把需求数字化。"
需求要数字化到这个程度:
✗ "用户量很大"
✓ "日活 100 万,峰值 QPS 2 万,每秒写入 5000 条"
✗ "数据量很大"
✓ "每天新增 500GB,保留 6 个月,总共 90TB"
✗ "要求高可用"
✓ "可用性 99.95%,允许每月 22 分钟不可用"
✗ "要求低延迟"
✓ "P99 < 100ms,P999 < 500ms"
★ 有了这些数字,架构几乎是唯一确定的。
观点 2:少谈”战略”,多谈”问题”
他的说法:
"架构师容易犯的错:一上来就讲'我们的架构要支持未来三年'。
但你连明年的业务是什么都不知道。
★ 好的架构不是'预测未来',而是'拥抱变化'——
让系统容易改,而不是试图一次设计对。"
这是”演进式架构”的思想:
❌ 过度设计:
"未来可能需要多租户,现在就加上"
"未来可能要换数据库,现在就抽象一层"
→ 90% 的"未来"不会发生
→ 但你付出了 100% 的复杂度
✅ 演进式设计:
先解决当前问题,但保持"可修改性"
· 模块化
· 接口清晰
· 测试覆盖
→ 需求变了,能改得动
观点 3:从”文件”到”分布式”的信息观
许式伟有一个很有意思的视角:
他把整个计算机体系看作"信息的组织和流动":
单机时代: 信息存在"文件"里
→ 核心抽象是文件、目录、流式读写
网络时代: 信息存在"服务端"里
→ 核心抽象是请求-响应、数据库
分布式时代: 信息存在"分布式系统"里
→ 核心抽象是一致性、分区、副本
云计算时代: 信息存在"云"上
→ 核心抽象是弹性、按需、多租户
★ 每一次范式转移,都是"信息的组织方式"变了
★ 架构师要理解当前范式,并预判下一个
思考题 ·【创造层】
你现在负责一个创业公司的技术架构。业务是在线协作文档(类似 Google Docs / 飞书文档)。
- 团队 8 个工程师(5 后端 + 2 前端 + 1 运维)
- 预期 1 年内日活 10 万,同时在线编辑 5000 人
- 核心功能:多人实时协同编辑、历史版本、权限管理
- 预算有限
请设计架构,说明:
- 服务怎么拆分(或不拆分)
- 实时协同用什么方案(OT 还是 CRDT?自研还是用库?)
- 存储怎么设计
- 你会避免哪些”看起来很美”的陷阱
参考答案
一、先算需求和约束
需求数字化
日活 10 万,假设:
· 每人每天编辑 2 个文档
· 每个文档平均编辑 30 分钟
· 编辑期间每 3 秒产生一次操作
同时在线编辑峰值 5000 人:
· 操作频率: 5000 / 3 ≈ 1700 ops/s
· 峰值可能 3 倍: 5000 ops/s
数据量:
· 假设每个文档平均 100KB
· 每个用户 20 个文档 = 2MB
· 10 万用户 = 200GB
★ 数据量不大
实时性要求:
· 本地编辑响应: < 50ms(本地立即生效)
· 其他协作者看到: < 500ms
· ★ 注意这两个是不同的
约束
团队: 8 人(5 后端)
时间: 产品要尽快上线验证
预算: 有限
★ 关键约束:团队小、时间紧、预算少
这决定了架构必须"简单"
二、服务拆分:不拆(或最小化拆分)
我的建议:模块化单体
❌ 一开始就拆微服务:
用户服务、文档服务、协同服务、权限服务、
版本服务、通知服务、搜索服务...
→ 8 个团队维护 7 个服务
→ 分布式调试、链路追踪、服务发现全都要
→ ★ 团队会死在基础设施上
✅ 模块化单体(起步):
┌────────────────────────────────────┐
│ 单个 Go 服务(模块化) │
│ ┌────────┬────────┬────────┐ │
│ │ 文档模块 │ 协同模块 │ 权限模块 │ │
│ └────────┴────────┴────────┘ │
│ ┌────────┬────────┐ │
│ │ 版本模块 │ 用户模块 │ │
│ └────────┴────────┘ │
├────────────────────────────────────┤
│ PostgreSQL + Redis │
└────────────────────────────────────┘
为什么是单体:
| 理由 | 说明 |
|---|---|
| 团队小 | 5 个后端维护 1 个服务 vs 7 个服务,效率差 3-5 倍 |
| 数据量小 | 200GB,单库完全够(甚至几年内都不用分库) |
| 迭代速度 | 单体部署快、调试快、测试快 |
| 业务未验证 | 产品方向可能变,微服务会锁死架构 |
但必须模块化(为未来拆分做准备):
// 目录结构
internal/
├── document/ # 文档模块
│ ├── api.go # 对外接口(其他模块只能调这个)
│ ├── service.go
│ └── repo.go
├── collab/ # 协同模块
│ ├── api.go
│ ├── ot.go # OT 算法
│ └── hub.go # WebSocket 连接管理
├── permission/
├── version/
└── user/
// ★ 规则:
// 1. 模块间只能通过 api.go 调用
// 2. 不允许直接访问其他模块的 repo
// 3. 模块间不共享数据库表(每个模块自己的表)什么时候拆?
信号:
① 某个模块需要独立扩展(比如协同服务需要更多机器)
② 某个模块的变更频率远高于其他(独立部署能加快迭代)
③ 团队规模扩大到 15+ 人
④ 某个模块需要不同的技术栈
★ 在出现这些信号之前,不要拆
但有一个例外:协同服务(WebSocket)可以独立
理由:
· WebSocket 是长连接,跟 HTTP 请求的资源模型完全不同
· 长连接服务需要单独的水平扩展策略
· 长连接的内存占用模式不同(每连接占用内存)
· 重启策略不同(HTTP 服务可以随时重启,长连接要考虑重连)
★ 所以:拆成 2 个服务
① API 服务(HTTP,处理文档 CRUD、权限、版本)
② 协同服务(WebSocket,处理实时编辑)
这是"按技术特性拆分",不是"按业务域拆分"
★ 这种拆分是合理的,因为拆分理由是"技术需求",不是"组织需求"
三、实时协同:OT 还是 CRDT?用库还是自研?
OT vs CRDT
| OT (Operational Transformation) | CRDT | |
|---|---|---|
| 原理 | 操作在服务端变换后应用 | 数据结构本身保证收敛 |
| 服务端 | 需要中心化的变换逻辑 | 可以 P2P |
| 实现复杂度 | 中等(变换函数难写对) | 高(数据结构复杂) |
| 成熟度 | ★ 高(Google Docs 用了 20 年) | 中(较新) |
| 生态 | 丰富 | 较少 |
| 离线编辑 | 困难 | ★ 天然支持 |
| 性能(大文档) | 好 | 可能膨胀 |
我的建议:用 OT,且用现成的库
理由:
① 你的场景是"在线协作",不是"离线优先"
→ OT 完全够用,CRDT 的优势(离线)用不上
② OT 生态成熟
· Go: 有几个开源实现
· JS: ot.js、sharedb 都很成熟
③ CRDT 的实现复杂度高得多
· 8 个人团队自研 CRDT,风险太大
· 而且 CRDT 的"自动合并"在某些场景会产生奇怪的结果
★ 绝对不要自研 OT 算法。
OT 的变换函数是出了名的难写对:
· 两个并发操作,变换后要满足 TP1、TP2 性质
· 边界情况极多(删除+插入+格式化的各种组合)
· ★ 这是学术界研究了 30 年的问题
自研的代价:
· 至少 3-6 人月
· 而且你做的不如成熟的库
· 出 bug 了难查(并发时序问题)
★ 用现成的:
· 前端: ot.js / Yjs(Yjs 是 CRDT,但很成熟)
· 后端: 找个 Go 的 OT 库,或者用 Node 的 sharedb
如果一定要自研,建议用 Yjs(CRDT)
Yjs 是目前最成熟的协同库:
· 支持离线编辑
· 性能极好
· 有 CRDT 的理论保证(不会出错)
· 生态丰富(有 WebSocket provider、数据库持久化)
★ 用 Yjs 的话,后端只需要:
· y-websocket server(Node.js,官方就有)
· 或者用 Go 实现一个 Yjs 的 WebSocket 服务
★ 这样你不用实现协同算法,只需要"转发消息 + 持久化"
实时协同的架构
┌─────────┐ WebSocket ┌──────────────┐
│ 浏览器 A │◄────────────►│ │
└─────────┘ │ 协同服务 │
┌─────────┐ WebSocket │ │
│ 浏览器 B │◄────────────►│ ┌────────┐ │
└─────────┘ │ │房间管理 │ │
│ ├────────┤ │
│ │OT 变换 │ │
│ ├────────┤ │
│ │持久化 │ │
│ └────────┘ │
└──────┬───────┘
│
┌──────▼───────┐
│ Redis Pub/Sub│ ← 多实例间广播
└──────────────┘
│
┌──────▼───────┐
│ PostgreSQL │ ← 定期保存快照
└──────────────┘
关键设计:
① 房间(Room)模型
一个文档 = 一个房间
房间里维护:
· 当前在线的用户列表
· 文档的当前内容(内存)
· 操作历史(用于 OT 变换)
· 版本号
② 多实例广播
协同服务会有多个实例(水平扩展)
用户 A 在实例 1,用户 B 在实例 2
→ 实例 1 收到操作,要广播给实例 2
→ ★ 用 Redis Pub/Sub
③ 持久化策略
不要每个操作都写数据库(太频繁)
策略:
· 每 N 个操作 或 每 M 秒,保存一次快照到 PostgreSQL
· 操作日志写 Redis(用于崩溃恢复)
· ★ 权衡:崩溃时丢失多少操作?
保存频率越高,丢失越少,但数据库压力越大
建议:每 5 秒 或 每 100 个操作,取先到者
四、存储设计
数据模型
-- 用户
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 文档(元数据)
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500),
owner_id BIGINT REFERENCES users(id),
-- ★ 当前内容(快照)
content JSONB, -- 或者存 Yjs 的二进制格式
version BIGINT DEFAULT 0,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
-- 版本历史(★ 关键:不要存全量,存增量)
CREATE TABLE document_versions (
id BIGSERIAL PRIMARY KEY,
document_id BIGINT REFERENCES documents(id),
version BIGINT,
-- 增量(操作序列)
operations JSONB,
-- 或者:定期存全量快照 + 中间存增量
is_snapshot BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ,
created_by BIGINT
);
CREATE INDEX idx_doc_versions ON document_versions(document_id, version DESC);
-- 权限
CREATE TABLE document_permissions (
document_id BIGINT,
user_id BIGINT,
role VARCHAR(20), -- owner / editor / viewer
PRIMARY KEY (document_id, user_id)
);
CREATE INDEX idx_perm_user ON document_permissions(user_id);为什么用 PostgreSQL 而不是 MongoDB
很多人觉得"文档是 JSON,用 MongoDB 更合适"
★ 这是错的
理由:
① 协作文档有强一致性要求(权限、版本不能出错)
→ PostgreSQL 的 ACID 更可靠
② 你需要事务(比如"创建文档 + 设置权限"要原子)
→ MongoDB 的事务支持较晚、较弱
③ JSONB 类型足够灵活
→ 文档内容可以存 JSONB,还能建索引、做查询
④ 团队熟悉 SQL
→ 招人容易,运维简单
★ MongoDB 的优势(灵活 schema、水平扩展)在这个场景用不上
Redis 的用途
① 协同服务的 Pub/Sub(多实例广播)
② 文档内容的缓存(热文档)
③ 在线用户状态
④ 操作日志(崩溃恢复用,短期)
五、要避免的”看起来很美”的陷阱
陷阱 1:一开始就搞微服务
❌ "我们要支持未来百万用户,现在就上微服务"
→ 8 人团队维护 10 个服务
→ 80% 的时间在搞基础设施
✅ 模块化单体,需要时再拆
陷阱 2:自研协同算法
❌ "OT 算法看起来不难,我们自己写"
→ 3-6 人月
→ 边界情况永远修不完
→ ★ 而且这是学术界研究了 30 年的问题
✅ 用 Yjs 或成熟的 OT 库
陷阱 3:为每个操作写数据库
❌ "保证不丢失每一个操作"
→ 5000 ops/s 写数据库 → 数据库崩溃
✅ 批量保存快照 + Redis 存操作日志
→ 崩溃最多丢失 5 秒的操作(用户重连时会重新同步)
陷阱 4:用 Kubernetes(起步阶段)
❌ "K8s 是云原生标准,必须用"
→ 1 个运维维护 K8s 集群
→ 学习曲线陡峭
→ 出问题难排查
✅ 起步: Docker Compose 或者云厂商的托管服务
→ 等业务稳定、团队扩大后再上 K8s
★ 或者直接用云厂商的容器服务(比如阿里云 ACK Serverless)
省去运维 K8s 本身的成本
陷阱 5:过度设计权限系统
❌ "我们要支持 RBAC + ABAC + 自定义角色 + 权限继承"
→ 设计 3 个月
✅ 起步: owner / editor / viewer 三个角色
→ 90% 的场景够用
→ 需要时再扩展
陷阱 6:忽略”文档很大”的情况
场景: 有人把 10MB 的文本粘贴进文档
❌ 没有限制 → 协同服务内存爆炸 → 所有用户受影响
✅ 限制:
· 单文档大小上限(比如 5MB)
· 单次操作大小上限
· ★ 超过限制要有明确的错误提示
六、我的最终建议
架构总结
┌─────────────────────────────────────────────┐
│ 前端 │
│ React + Yjs (CRDT) │
└──────────────────┬──────────────────────────┘
│ WSS
┌──────────────────▼──────────────────────────┐
│ 协同服务 (Go) │
│ · WebSocket 连接管理 │
│ · 房间管理 │
│ · Yjs 消息转发 + 持久化 │
│ · ★ 不实现协同算法,用 Yjs │
│ ┌──────────────────┐ │
│ │ Redis Pub/Sub │ 多实例广播 │
│ └──────────────────┘ │
└──────────────────┬──────────────────────────┘
│ HTTP + WebSocket
┌──────────────────▼──────────────────────────┐
│ API 服务 (Go 单体) │
│ ┌────────┬────────┬────────┬────────┐ │
│ │用户模块 │文档模块 │权限模块 │版本模块 │ │
│ └────────┴────────┴────────┴────────┘ │
└──────────────────┬──────────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ PostgreSQL │ │ Redis │
│ · 用户 │ │ · Pub/Sub │
│ · 文档元数据 │ │ · 缓存 │
│ · 版本历史 │ │ · 在线状态 │
│ · 权限 │ │ │
└──────────────┘ └──────────────┘
技术选型
| 组件 | 选择 | 理由 |
|---|---|---|
| 后端语言 | Go | 团队熟悉、并发好、部署简单 |
| HTTP 框架 | gin 或 chi | 简单够用 |
| 数据库 | PostgreSQL | ACID、JSONB、团队熟悉 |
| 缓存 | Redis | 够用、简单 |
| 协同 | Yjs(前端)+ Go 转发 | ★ 不自研算法 |
| ORM | sqlx(不是 GORM) | 简单、可控、性能可见 |
| 部署 | Docker Compose(起步) | 简单;后续可上云厂商 K8s |
| 监控 | Prometheus + Grafana | 开源、标准 |
演进路径
第 1-3 月: MVP
· 单体服务
· 基础协同(Yjs + 内存)
· 简单的版本历史
第 4-6 月: 稳定性
· 持久化优化(快照策略)
· 权限细化
· 监控告警
第 7-12 月: 扩展
· 协同服务独立部署(如果需要)
· 搜索功能(Elasticsearch)
· 性能优化
★ 什么时候拆微服务?
团队 > 15 人,或 某个模块需要独立扩展时
一句话总结
这个场景的核心矛盾是”野心”和”资源”。
- 野心:实时协同、百万用户、完整功能
- 资源:8 个人、有限预算、时间紧
解法是”聚焦”:
- 只拆该拆的(协同服务因为技术特性需要独立)
- 不自研不该自研的(协同算法用 Yjs)
- 不做不必要的基础设施(不上 K8s、不搞微服务全家桶)
把有限的资源,投入到”产品差异化”上,而不是”技术炫技”上。
这是架构师最重要的判断力——知道什么不该做,比知道该做什么更难。
小结:这一节你应该带走的东西
-
架构 = 在约束下做决策。判断一个人是否懂架构,看他能不能说出”我放弃了什么”。
-
★ 造轮子的四个判断:有现成方案吗?是核心竞争力吗?有能力维护吗?成本收益合理吗?
-
架构演进不要跳步:模块化单体 → 按需拆分 → 平台化。单体时要模块化,为拆分做准备。
-
技术选型的四个维度:成熟度、匹配度、成本、风险。
-
需求必须数字化——有了数字,架构几乎是唯一确定的。
-
好的架构不是”预测未来”,而是”拥抱变化”——保持可修改性,而不是一次设计对。
-
架构师最重要的判断力:知道什么不该做。
下一节
最后一节。我们要把前面 7 章的所有知识,用在一个最”吃基础设施”的系统上。
老陈的预告:为什么直播延迟总是 3-10 秒,而不是 0.3 秒?
这不是技术做不到,是权衡的结果。 下一节你会理解这个权衡,并能自己设计一个直播系统。