计算机的严谨性其实是个相对概念。从底层的浮点数误差、CPU乱序执行,到顶层的复杂业务逻辑与并发,每一个环节都可能引入不可控因素。深入探讨这些失控的根源,有助于理解为何软件工程充满了挑战,以及为何能稳定运行的系统都堪称奇迹。
智能速览
计算机底层并非绝对严谨,例如0.1+0.2不等于0.3。
CPU为提升性能会乱序执行,可能在多线程环境下引发问题。
并发编程是BUG的主要来源,很多问题在线上高并发时才暴露。
分布式系统存在网络不确定性,迫使在一致性和可用性间做抉择。
人的不确定性,包括产品、测试和用户,是最大的变量。
精华内容
要理解代码的脆弱性,必须先放下底层绝对可靠的幻想,从物理世界到抽象逻辑,层层剖析失控的根源。
底层的假象
计算机底层并非绝对严谨,更像一个会抄近道的老司机。根据IEEE 754标准,0.1 + 0.2在计算机中的结果并非0.3,而是0.30000000000000004,这是浮点数精度问题的正常表现。此外,为了提升效率,CPU会进行乱序执行,程序员编写的A→B→C顺序,实际执行可能是B→A→C。尽管CPU保证单线程下的最终结果一致,但在多线程环境中,这种“保证”变得极其微妙,极易引发状态错乱。
更离谱的是,宇宙射线也可能导致内存位翻转,将1变成0。Google统计显示,其数据中心每月会发生数千次内存错误。这些都说明,底层只是在大多数情况下表现得严谨,并非绝对可靠。
复杂性爆炸
人脑的认知能力有限,研究表明大概只能同时处理7±2个信息块。当项目规模膨胀时,这种局限性会带来巨大挑战。一个10行的函数逻辑清晰,不易出错;但当系统扩展到上百个模块、几十万行代码,并交织着历史遗留问题时,人类已无法完全掌握其全貌。
在这种规模下,修改一处代码,其连锁反应超出了单个人的预判范围。因此,在大型项目里,“改了A不知道B会炸”成为一种常态,并非意外。这是系统复杂度超越人类认知边界后的必然结果。
并发是万恶之源
单线程代码的执行顺序确定,状态变化可预测,堪称严谨。但引入多线程后,一切变得复杂。经典的例子是库存超卖:当库存只剩1时,两个线程同时读取到库存为1并执行扣减,最终库存变为-1。这种BUG在本地测试时很难复现,因为单机压力小,两个请求极难同时到达。而线上高QPS环境则使其发生率大幅提升。
还有一种更玄学的“Heisenbug”,即一观察就消失的BUG。例如生产环境偶发的空指针异常(NPE),当尝试添加日志定位问题时,BUG却因日志带来的微小延迟改变了线程时序而消失。这类并发问题极其隐蔽且难以调试。
分布式系统困境
分布式系统放大了不确定性。单机程序调用只有成功或失败两种状态,而分布式系统的网络调用引入了第三种状态:不知道。请求发出后,对方是否收到、是否处理、响应是否丢失,都是未知数。以支付为例,超时后用户的钱到底扣没扣?这是一个两难选择:告知失败,用户可能已付款但收不到货;告知成功,公司可能收不到款。引入重试机制,又必须考虑幂等性,防止重复扣款。
CAP定理揭示了这种困境的根源:在网络分区(P)客观存在的情况下,系统只能在一致性(C)和可用性(A)之间取舍。分布式系统天生就是不完美的,只能选择一种可以接受的不完美。
人是最大变量
再严谨的底层,也架不住上层的人为因素。产品经理认为“用户不会这么操作”,用户偏偏就这么操作;接口文档明确要求参数大于0,调用方却传入-1;测试覆盖了99%的场景,用户第一个请求就命中了那剩下的1%。从需求提出、代码编写到测试,每个环节都存在误解、遗漏和疏忽。
此外,代码还要面对物理世界的“脏”数据。用户输入的emoji可能导致字符编码错误,第三方接口返回一个多余的字段可能让程序反序列化失败,凌晨的数据库主从切换可能正好打断正在执行的定时任务。这些外部因素,代码本身无法防范,只能依靠监控、降级和容灾机制来兜底。
代码只是理想世界的蓝图,而真实世界充满了网络、硬件、并发与人的不确定性。一个能稳定运行的系统,是无数次与这些不可控因素博弈的结果。或许,程序员烧香拜佛的背后,是对这种复杂性的敬畏与妥协。
关键评论
测试覆盖了99%的场景,上线后用户第一个请求就命中那1%。
人是最大的不可控变量,思来想去没有一点招。
互联网火了后天天吹分布式,实际上单机优化好后,分布式问题会少很多。
程序员和代码其实是相对的:代码描述理想情况怎么跑,程序员处理不理想情况怎么跑。