1 标准协议
通信协议优先使用 HTTP、gRPC、WebSocket、MQTT 这些标准协议,自研协议的维护成本远高于第一版开发成本。
数据格式优先使用 JSON、Protobuf、MessagePack、CSV,不要随便搞 xxx.json2、xxx.bin 这种只有自己认识的格式。
加密解密直接使用 TLS、AES、ChaCha20、SHA-256、Ed25519 等成熟方案,不要自己发明加密算法、Hash、签名算法。
各领域基础设施已经有非常多成熟的方案,例如:Nginx、Redis、MySQL、MinIO等等。
2 简单
不要为了未来设计不存在的需求。
YAGNI, You Aren’t Gonna Need It.
例如,现在只有
user_id
name
不要因为“以后可能需要多租户、国际化、ABAC、组织树”,第一版就设计成:
tenant_id
organization_id
region_id
locale
namespace
scope
policy
...
未来真的需要多话,再演进。
不要过度抽象。
例如,当前
func CreateUser()
func CreateOrder()
func CreateProduct()
不要立刻设计
AbstractEntityService
GenericDomainManager
BaseBusinessProcessor
UniversalCRUDService
重复不一定是坏事,错误的抽象才是。
不要过早微服务化。
如果一个单体代码量可能就那么 1000 行代码,不要设计成多个微服务运行。微服务解决的是组织规模、部署隔离、扩展性等问题,不是代码多了就拆。
3 优先降低耦合
模块之间尽量依赖接口,而不是具体实现。替换实现、测试和演进都会容易很多。
不要让业务层依赖基础设施细节。业务真正关心的是缓存数据,而不是我必须对接Redis。
不要让模块共享太多内部状态。避免使用全局变量,不然任何一个模块修改状态都会产生蝴蝶效应。
4 显式
不要依赖魔法
例如:
config.Get("xxx")
到处偷偷读取全局变量。
更推荐:
NewService(cfg)
依赖是什么,一眼就能看到。
不要让函数产生隐藏副作用。
例如:
func LoadConfig() {
...
}
执行的时候:至于加载了配置写到哪里去了,还得看内部细节。
不要依赖隐式顺序。
不要假设
A 一定先启动
B 一定后启动
C 一定已经初始化
系统应该尽可能明确表达依赖关系。
这在 Kubernetes、systemd、微服务中尤其重要。
5 可观测
任何关键操作都应该能够回答。
发生了什么?
什么时候发生?
谁发起的?
处理了多久?
为什么失败?
影响了谁?
所以系统设计阶段就应该考虑:
- 日志
- Metrics
- Trace
- Request ID
- Audit Log
- Error Code
而不是上线以后才补。
错误不要只返回“失败”
不推荐:
ERROR
至少应该能够区分:
INVALID_ARGUMENT
NOT_FOUND
TIMEOUT
PERMISSION_DENIED
CONFLICT
INTERNAL_ERROR
错误是系统 API 的一部分。
6 可演进
API 一旦发布,就把它当成长期契约
例如:
/api/v1/users
不要轻易修改:
{
"name": "张三"
}
变成:
{
"username": "张三"
}
然后要求所有客户端同时升级。
更好的方式是:
V1
↓
V2
↓
逐步迁移
↓
废弃 V1
数据库 Schema 要考虑向后兼容
特别是线上系统。
推荐:
增加字段
↓
发布代码
↓
新代码开始使用
↓
旧字段逐渐废弃
↓
最后删除
而不是:
ALTER TABLE
删除字段
↓
然后发现旧版本还在访问
↓
生产事故
不要让一个版本升级必须“全世界同时升级”
理想状态:
Client V1 ───┐
├── Server V2
Client V2 ───┘
而不是:
Client V1 × Server V2
Client V2 × Server V1
这叫做兼容性设计,也是为什么协议设计非常重要。
7 失败可控
系统一定会失败,要设计失败模式。
不要问:
“怎么保证 Redis 永远不会挂?”
而应该问:
“Redis 挂了以后系统怎么办?”
例如:
Redis 挂掉
↓
缓存失效
↓
是否降级?
↓
数据库是否被打爆?
↓
是否限流?
↓
是否熔断?
不要让局部故障扩散成全局故障
这是分布式系统非常重要的原则:
服务 A 故障
↓
服务 B 等待
↓
服务 C 等待
↓
线程池耗尽
↓
整个系统雪崩
应该设计:
Timeout
Retry
Circuit Breaker
Bulkhead
Rate Limit
Fallback
重试必须谨慎
很多人认为:
请求失败 → retry
但如果:
请求支付
↓
服务端已经扣款
↓
响应丢失
↓
客户端 retry
↓
再次扣款
就出事故了。
所以:
重试 + 幂等,是一对必须一起设计的东西。
8 数据设计
不要让多个系统同时成为同一份数据的“主人”
例如:
A → user 表
B → user 表
C → user 表
最后:
到底谁说了算?
更好的设计是明确:
User Service
↓
User 数据唯一事实来源
即 Single Source of Truth。
不要为了性能过早引入缓存
先:
Database
发现:
QPS 不够
再:
Database
↑
Cache
否则你马上获得一个新问题:
数据库里的数据和缓存里的数据到底哪个是真的?
不要把一致性当成免费的
你选择:
强一致
通常意味着:
性能 ↓
可用性 ↓
复杂度 ↑
选择:
最终一致
则意味着:
性能 ↑
可用性 ↑
业务处理复杂度 ↑
一致性是架构决策,不是数据库默认行为。
9 总结
| 原则 | 核心思想 |
|---|---|
| Don’t reinvent the wheel | 不重复造轮子 |
| YAGNI | 不为不存在的需求设计 |
| KISS | 能简单就不要复杂 |
| Avoid premature optimization | 不要过早优化 |
| Prefer standard | 优先标准方案 |
| Explicit over implicit | 显式优于隐式 |
| Loose coupling | 降低耦合 |
| Backward compatibility | 保证向后兼容 |
| Fail gracefully | 优雅处理失败 |
| Observability first | 系统必须可观测 |
但如果只让我选三个最高级的原则,我会选:
① 不要重新解决已经被解决的问题。
② 不要为尚不存在的问题增加复杂度。
③ 永远假设依赖会失败、数据会异常、需求会变化。