那些离大谱的诡异故障,其实源于错误的架构设计……

Thread Whisperer 2025-08-20 10:07:00
大多数灾难并非由一行坏代码引起。它们源自架构上的错误,这些错误只有在压力下才会显现出来。

 

你是否经历过这样的时刻:你亲手构建的系统突然开始跟你作对?

 

不是那些你预料到的Bug,而是一波无法解释的怪异故障:内存飙升、请求消失、指标数据撒谎……

 

起初,你以为只是犯了个小错误,但越深入挖掘,就越意识到这是架构层面的问题。

 

这不是一份检查清单,而是血泪史……

 

 

一、幻想延迟“自然就能扩展”

 

错误: 仅仅因为你的系统能处理测试负载,就相信它能处理10倍的流量。

 

你的预发布环境(staging)会说谎。小负载永远不会暴露你代码中最慢的路径。当流量洪峰来袭时,突然每个数据库调用、网络跳转(hop)和第三方服务都会暴露无遗。此时,你的“99百分位延迟”(99th percentile)才是唯一重要的东西。

 

基准测试(Go语言):

 

  •  
  •  
  •  
  •  
start := time.Now()db.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] <-- tightly coupled[Service C]---|

 

教训: 真正的微服务可以独立地消亡或变更。如果你无法在不部署另一个服务的情况下部署一个服务,那么你仍处于单体架构的领域。

 

三、依赖“最终一致性”却没有安全网

 

错误: 盲目相信最终一致性(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 []byteerror    Load(id string) ([]byteerror)}

 

程示意:

 

  •  
  •  
  •  
[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) < 10 {    panic("simulated failure")}

 

流程示意:

 

  •  
[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 cancel()client.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),区别在于你是否从上次的火灾(事故)中吸取了教训。

 

最好的架构师是那些承认自己哪里做错了的人——然后回去为下一代修复它。

 

如果这些错误中有任何一条让你感觉似曾相识,你并不孤单。

 

没有人第一次就能做对,但尽早面对这些残酷的现实,将在真实用户出现时为你节省数月的痛苦。

 

作者丨Thread Whisperer   编译丨Rio
来源丨网址:https://medium.com/@maahisoft20/10-deadly-sins-of-system-architecture-that-will-haunt-you-at-scale-841dabe54a44
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告