摘要:大多数灾难并非由一行坏代码引起。它们源自架构上的错误,这些错误只有在压力下才会显现出来。
大多数灾难并非由一行坏代码引起。它们源自架构上的错误,这些错误只有在压力下才会显现出来。
你是否经历过这样的时刻:你亲手构建的系统突然开始跟你作对?
不是那些你预料到的Bug,而是一波无法解释的怪异故障:内存飙升、请求消失、指标数据撒谎……
起初,你以为只是犯了个小错误,但越深入挖掘,就越意识到这是架构层面的问题。
这不是一份检查清单,而是血泪史……
一、幻想延迟“自然就能扩展”
错误: 仅仅因为你的系统能处理测试负载,就相信它能处理10倍的流量。
你的预发布环境(staging)会说谎。小负载永远不会暴露你代码中最慢的路径。当流量洪峰来袭时,突然每个数据库调用、网络跳转(hop)和第三方服务都会暴露无遗。此时,你的“99百分位延迟”(99th percentile)才是唯一重要的东西。
基准测试(Go语言):
start := time.Nowdb.Query("SELECT * FROM users WHERE id = ?", userID)elapsed := time.Since(start)fmt.Println("Query took", elapsed)流程示意:
[User]---(API Gateway)---[App Server]---[DB] | | [Cache] [3rd Party API]教训: 在上线前测量每一个慢速跳转。假设你未来的负载会比今天糟糕十倍。
二、在微服务世界中抱守单体架构思维
错误: 构建所谓的“模块化”代码,实际上却是一个隐藏在服务边界背后的巨型单体(monolith)。
你可以在图上画出任意多的框,但如果你的服务通过数据库模式(schema)或业务逻辑紧密耦合在一起,那你只是创建了一个分布式单体(distributed monolith)。
示例:
// Shared utility across "services" import "common/utils"流程示意:
[Service A]---|[Service B]---|--> [Shared Library]教训: 真正的微服务可以独立地消亡或变更。如果你无法在不部署另一个服务的情况下部署一个服务,那么你仍处于单体架构的领域。
三、依赖“最终一致性”却没有安全网
错误: 盲目相信最终一致性(eventually consistent),却没有数据核对(reconciliation)的计划。
最终一致性系统很棒——直到它出问题为止。用户会看到数据丢失。后台团队会追逐“幽灵记录”。
数据核对模式:
// Run a periodic job to fix mismatchesfor id := range orphanRecords { fixRecord(id)}流程示意:
[Source DB] ----> [Replication] ----> [Target DB] | | [Audit Job]教训: 每一个异步工作流都必须有一个审计器(auditor)或核对器(reconciler)。如果你不构建它,你将在凌晨3点手动运行SQL。
四、盲目崇拜单一数据库
错误: 把你的数据库当作解决一切问题的神奇盒子。
数据库不是你的瓶颈——直到它成为瓶颈。突然之间,锁(locking)、连接限制(connection limits)和查询执行计划(query plans)会让你的整个产品陷入瘫痪。
简单示例:
-- This lock will ruin your scaling dreamsUPDATE orders SET status='paid' WHERE id=42 FOR UPDATE;流程示意:
[App Server] | [DB Master] -- (locks) --> [Blocked Writes]教训: 尽早规划分片(sharding)、缓存(caching)或卸载(offloading)。单一数据库在真实规模下总会背叛你。
五、直到为时已晚才忽视可观测性
错误: 盲目构建。没有日志(logs)、没有指标(metrics)、没有追踪(traces)。
在第一次重大事故之前,你都是在盲目飞行。然后每个人都会手忙脚乱地添加日志,但为时已晚,无法捕捉到真正出错的原因。
代码:
log.Printf("Order created: %v", orderID)// Add latency metricmetrics.Observe("order.create.latency", elapsed)流程示意:
[Service] ---> [Logger] \-> [Metrics] \-> [Tracing]教训: 如果你看不见它,你就无法修复它。在用户接触你的系统之前,就把可观测性(observability)构建进去。
六、在需要之前过度设计
错误: 花数周时间在那些你“以后可能用到”的特性或抽象上。
每个系统都会增长,但过早的抽象是浪费精力。你会猜错,你的代码库会证明这一点。
示例:
// Too abstract for real use casestype Storage interface { Save(data byte) error Load(id string) (byte, error)}流程示意:
[Feature A] --\[Feature B] ---[Giant Abstract Layer]---[DB][Feature C] --/教训: 为当下的复杂性而构建。过度设计会制造出一个没人愿意清理的烂摊子。
七、把状态(State)到处推送
错误: 将有状态(stateful)逻辑混入你的无状态(stateless)服务。
有状态的代码使得扩展和恢复变得痛苦。如果你的进程崩溃,用户会丢失进度。如果你想运行更多副本(replicas),状态就会不同步。
无状态示例:
// Pure stateless handlerfunc handler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "pong")}流程示意:
[User]---(Load Balancer)---[Stateless Service]---[DB/Cache]教训: 尽可能将状态移出服务。对于任何必须持久化的东西,使用外部存储。
八、跳过故障注入
错误: 假设理想路径(happy path)是唯一的路径。
真实的系统会出故障。如果你从不注入故障(inject faults),你就不知道恢复过程会有多糟糕。
Go:
if rand.Intn(100)流程示意:
[Request] --> [App] --(simulate failure 10%)--> [Error Handler]教训: 混沌(Chaos)是你的朋友。故意在低级别环境(如测试环境)中搞坏东西,你在生产环境中就能睡得更安稳。
九、低估第三方风险
错误: 认为每个供应商或API都是100%可靠的。
你使用的每一个“企业级”平台都有糟糕的时候。速率限制(rate limits)、随机宕机(outages)或API变更会摧毁你的流程管道(pipeline)。
超时代码:
ctx, cancel := context.WithTimeout(context.Background, 2*time.Second)defer cancelclient.Do(req.WithContext(ctx))流程示意:
[App]---[Vendor API] (timeout/retry logic)教训: 总是用超时(timeouts)和重试(retries)包装第三方调用。信任,但要验证(Trust, but verify)——永远如此。
十、忽略人力成本
错误: 设计一个没人愿意维护的系统。
糟糕的文档、无法调试的流程、“聪明”的捷径——所有这些都会导致团队倦怠(burnout)和人员流失(turnover)。
示例(坏代码):
func mystery(x int) int { return x ^ 0x3F }流程示意:
[Old Dev] ---"Why did I do this?"---> [New Dev]教训: 未来的你也是系统的一部分。如果你的文档、测试和代码不能帮助你的团队,它们就是技术债务(technical debt)。
最后的思考
每一个大型系统都会在某个地方失败。
是成为经验教训(war story)还是成为噩梦(nightmare),区别在于你是否从上次的火灾(事故)中吸取了教训。
最好的架构师是那些承认自己哪里做错了的人——然后回去为下一代修复它。
如果这些错误中有任何一条让你感觉似曾相识,你并不孤单。
没有人第一次就能做对,但尽早面对这些残酷的现实,将在真实用户出现时为你节省数月的痛苦。
来源:dbaplus社群一点号