- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
前不久,哔哩哔哩(一般常称为 B 站)发布了一篇文章《2021.07.13 我们是这样崩的》,详细回顾了他们在 2021.07.13 晚上全站崩溃约 3 小时的至暗时刻,以及万分紧张的故障定位与恢复过程。
那篇文章将定位过程、问题分析、优化改进等方面写得很详细,在我印象中,国内互联网大厂在发生类似事故后,能够如此开诚布公地“检讨”“还债”的并不多见。(值得送上一键三连~~~)
对于搞技术的同学来说,这篇文章是不错的学习材料。而我最为关注的内容,其实是关于编程语言的特性,也就是在代码层面上的细节问题。
在关于问题根因的分析中,我们看到了罪魁祸首的 7 行代码,它是用 Lua 语言写的一个求最大公约数的函数:
简单而言,这个函数预期接收的参数是两个数字(普通的数字或者字符串类型的数字,即两种类型都可以),然而,它的 if 语句却只判断了一种类型(普通数字),忽略了字符串类型的“0”。
在故障发生时,它的第二个参数传入的是字符串类型“0”而不是数字类型 0,导致 if 语句判断失效!
由于 Lua 是动态类型语言,只有在程序运行时才知道传入的参数是什么类型。这属于是所有动态类型语言的特色,在 Python、JavaScript、PHP、Ruby 等动态类型语言中,也会有同样的表现。这不是啥新鲜事物。
然而,真正该死的问题在于,Lua 还是一门弱类型语言,它不像 Python、Ruby、Java 等强类型语言那样,它竟支持隐式类型转换!
在 Lua 中,数字字符串在与普通数字作算术运算时,会将字符串类型隐式地转换成数字类型,如上图所示的“a % b”,如果 b 是字符串类型的数字,那它就会被转换成数字类型!
而在 Python 这种强类型动态类型语言中,这样的转换是不可思议的,数字与字符串作算术运算,能得到的只会是报错:TypeError: unsupported operand type(s) for %: 'int' and 'str'
Lua 语言的这种“字符串隐式变数字”的行为,即使在大意不察觉的情况下,似乎也不会造成太大问题。在 B 站代码中,除了出事故时传的字符串“0”以外,估计它一直接收的都是其它字符串数字,一直也没出问题,显然程序员是把这当成一种便利手段了(因为不需作类型转换)。
然而,不幸的是,Lua 中还有一个特殊的“nan”,它会进一步将这一个“小小的错误”传递下去,直至传到了地老天荒不受控制的死循环里……
在大多数编程语言中,除零操作都是不可饶恕的错误,这跟我们在小学数学课堂上就掌握的常识相吻合:数字零不允许作为除数!
掏出手机,打开计算器,看看它是怎么说的:
看到了吧!不能除以0!!!
继续看看 Python 对于这种操作的反应:
ZeroDivisionError 除零错误,这是在捍卫我们根深蒂固的数学常识。
那么,Lua 语言在除零操作后得到的 nan 到底是个什么东西呢?
nan 一般也被称为“NaN”,是“No a Number”的缩写,表示“不是一个数”。它来头不小,是在 1985 年的 IEEE 754 浮点数标准中首次引入的。
直白地讲,它也是数字类型中的一个值,但是表示的是一个“不可表示的值”。也就是说,它表示的是一个非常抽象概念的数。
也许我们比较容易理解另一个抽象的数“无穷大”,因为在中学数学课上就经常接触到,而 nan 也是类似的一种特殊的数,只不过它较为少用且更难以捉摸罢了。
Python 中也有这两个数的存在,即 float('inf') 表示无穷大、float('nan') 表示非数。它们就像是两个黑洞,会吞噬掉任何试图前来“搭讪”的数:
那么,当这两个黑洞相互靠近时,谁的引力更大些呢?请看示例:
看来还是 nan 的优先级更高一筹啊。
然而,尽管 Python 中有 nan,但它并不因为这个数而抛弃前文提到的常识。而同为脚本语言的 Lua 却抛弃了常识, 在出现除零这种非法操作时,它不是报错,而是得到 nan 的结果。
这样的特性简直是自由得过分,也许在某些时候会挺有用吧,但它也会埋下未知的隐患。
回到 B 站的问题代码,弱类型的 Lua 语言由于太过自由,它放行了字符串数字与普通数字的运算,又因为对 nan 过于自由的使用,它放行了数字除零的操作,两次的放行,使得短短几行代码一路畅行不止,一路消耗服务器资源,直到 CPU 100%,直到牵动服务集群故障,直到高可用的多活机房服务不可用,导致全站崩溃 3 小时的事故……
当然了,如果当初写下这段代码的程序员多加一个条件判断,这一次的事故就完全可以避免。从另外的视角看,这就是程序员在递归程序的终止条件上处理不当,不能甩锅给编程语言那两项自由不羁的语言特性。
但是,我相信写下那段代码的程序员大概率是长期使用其它编程语言,现学现卖上手写 Lua,尽管知道 Lua 语言动态弱类型的特点,但思维习惯上仍深受其它语言影响,这才“一时失足、小河翻船”……程序员内心有苦说不出!!
短短的 7 行代码,说简单就简单,说不简单也不简单。本文就不展开说辗转相除法求最大公约数了(说来话长),单单是前面提及的隐式类型转换加上除零得 nan 的细节问题,就足够导致一场大事故了。
从 7 行问题代码中,作为吃瓜群众的我们,能得到些什么收获呢?到底是涨见识了,还是“又学废了”呢?
人生苦短,不求无 Bug,但求读者老爷们赏个一键三连吧~~~
我对cassandra并使用1.2.10非常陌生。我有一个时间戳数据类型的主键列。现在,我正在尝试检索日期范围的数据。由于我们知道不能在cassandra中使用,因此我使用的是大于()来获取日期范围。
我正在尝试进行有条件的转场。但我得到: Terminating app due to uncaught exception 'NSInvalidArgumentException', reas
我有一个游戏项目,在调试和发布模式下在设备上运行得非常好。我有两个版本。旧版本和新版本具有更多(后来我添加了)功能,并且两者的 bundle ID、版本相同。当我构建旧版本时,之前没有安装“myGam
这个问题已经有答案了: 奥 git _a (2 个回答) 已关闭 5 年前。 我正在获取 ClassCastException 。这两个类来自不同的 jar,但是JettyContinuationPr
以下代码行抛出异常: HttpResponse response = client.execute(request); // actual HTTP request 我能够捕获它并打印: Log
就目前情况而言,这个问题不太适合我们的问答形式。我们希望答案得到事实、引用资料或专业知识的支持,但这个问题可能会引发辩论、争论、民意调查或扩展讨论。如果您觉得这个问题可以改进并可能重新开放,visit
public class TwoThreads { private static Object resource = new Object(); private static void
当我输入 6 (int) 作为值时,运行此命令会出现段错误 (gcc filename.c -lm)。请帮助我解决这个问题。预期的功能尚未实现,但我需要知道为什么我已经陷入段错误。 谢谢! #incl
所以,过去一周半我一直在研究这个 .OBJ/.MTL 网格解析器。在这段时间里,我一直在追踪/修复很多错误、清理代码、记录代码等等。 问题是,每修复一个错误,仍然会出现这个问题,而且一张图片胜过一千个
我正在运行一个代码,它基本上围绕 3 个维度旋转一个大数据数组(5000 万行)。但是,我遇到了一个奇怪的问题,我已将其缩小到如何评估旋转矩阵。基本上,对于除绕 x 轴以外的任何旋转,python 代
就在你说这是重复之前,我已经看到了其他问题,但我仍然想发布这个。 所以我正在阅读 Thinking in Java -Bruce Eckel 这篇文章是关于小写命名约定的: In Java 1.0 a
我想在我的应用程序中使用 REST API。它为我从这个应用程序发出的所有请求抛出 SocketTimeoutException。 Logcat 输出:(您也可以在此处看到带有漂亮格式的输出:http
我知道 raise ... from None 并已阅读 How can I more easily suppress previous exceptions when I raise my own
在未能找到各种Unix工具(例如xargs和whatnot)的最新独立二进制文件(this version很好,但需要外部DLL)后,我承担了自己进行编译的挑战。 ...这是痛苦的。 最终,尽管如此,
我有一个用PHP编写的流套接字服务器。 为了查看一次可以处理多少个连接,我用C语言编写了一个模拟器来创建1000个不同的客户端以连接到服务器。 stream_socket_accept几次返回fals
我的Android Studio昨天运行良好,但是今天当我启动Android Studio并想在移动设备上运行应用程序时,发生了以下错误, 我在互联网和stackoverflow上进行了搜索,但没有解
默认情况下,grails似乎为Java域对象的toString()返回:。那当然不是我想要的,所以我尝试@Override toString()返回我想要的。当我尝试grails generate-a
尝试通过LDAP通过LDAP对用户进行身份验证时,出现以下错误。 Reason: Cannot pass null or empty values to constructor. 谁能告诉我做错了什么
我正在尝试使用应用程序附带的 Houdini Python 模块,该模块是 Houdini 安装文件夹的一部分,位于标准 Python 路径之外。按照安装说明操作后,运行 Houdini Termin
简单地说,我正在为基本数据库编写单链表的原始实现。当用户请求打印索引下列出的元素高于数据库中当前记录数量时,我不断出现段错误,但仅当差值为 1 时。对于更高的数字,它只会触发我在那里编写的错误系统。
我是一名优秀的程序员,十分优秀!