【老万】Exchange Server用户新年发不出邮件怪谁
#旧文重发
这篇文章今年元旦发过了。之所以急着重发,是因为我的朋友周%认真查了资料,认为我文中引用的 “Premature optimization is the root of all evil” 并非 Tony Hoare 原创,也不是高德纳的。他甚至不记得跟我讨论过这个问题。那么到底是谁说的?Tony 自己倾向于是民间创作。如此一来,我在原文中把这个金句归于 Tony 老师就误导读者了。因为微信公众号文章一旦发出只能改几个字,只好把原文删了重发改正版。大家见谅。
~~ 微软出事 ~~
程序员最怕的就是过年过节。这不是迷信。过节的时候用户一般比较活跃,明星容易并发出轨。怨女也爱在这个时候手撕渣男:因为围观群众无所事事,闲着也是闲着,不如吃瓜,这时撕传播效果好,事半功倍。平时只能撕一个,过节撕十个不在话下。总之,节假日流量大、系统忙、容易挂。
再有就是机器里都有表示当前时间的各种变量,跨年的时候容易出问题。比如说我们熟悉的 2000 年(Y2K)问题,就是因为上个世纪一些程序员抽风,决定用两位数表示年份,比如 98 就是 1998。这样 2000 年就成了 00,会被一些措不及防的代码认成 1900,各种故障。
这首歌有 bug!
这首歌的作者是个优秀的程序员。
Y2K 过去 20 多年了,大家可能觉得这样的 bug 早该绝迹了吧。不,萧伯纳老师曰过:我们从历史中学到的就是我们从历史中啥也学不到。程序员多了,总有一些个掉链子的。比如,前几天谷歌的 Chrome 浏览器升级到第 100 版出问题了。不是谷歌自己的问题,而是有些第三方软件在比较浏览器版本号时图省事只看前两位,把100当成了10。再比如,2022 元旦刚过,微软 Exchange Server 的用户就收到了一份大礼:他们的邮件都发不出去了。
Exchange Server 是微软给企业用户设计的邮件和日历处理服务器。它的邮件过滤系统里面,有一个地方用整型数表示时间(实际用来做这个系统的版本号)。出于某种原因(估计是为了优化),程序员一拍脑袋决定把时间编码成十进制 yymmddHHMM 的格式(用两位数分别表示年、月、日、时、分)用 32-bit 有符号整数表示。比如 1908231256 就表示2019年8月23日12点56分。
问题是每种数据类型都有它能表示的数值范围。32-bit 有符号整数只能表示 -2147483648 到 2147483647。2021 年的最后一夜(12月31日23点59分)还可以用这种形式表示为 2112312359,没有问题。但是,翻过年就不成了。随着 2022 年的第一场雪,2201010000 超过了32-bit 有符号整数的上限,系统就挂了。
因为这个 bug 是 2022 年引发的,所以简称 Y2K22(Year 2022)。
时代进步了,微软这次没有把涉事程序员祭天,而是紧急集合了一批无辜程序员抢修。一顿操作猛如虎,誓把漏洞连夜补,终于推出了一个临时措施,可以把邮件过滤系统关闭,让邮件正常发送。但是彻底的解决措施还需要时间。因为 Exchange Server 是部署到用户的机器上的,不在微软云上统一管理,到时候还得要所有用户分别下载安装微软的补丁,好不麻烦。
~~ 为啥出事 ~~
很多大厂都有这样的制度:出了生产事故后,写验尸报告(postmortem)复盘分析:事故发生的经过和根本原因,如何快速治标,如何彻底治本,如何避免类似的故障再发生。虽然微软不给我发工资,但是这一事件很有代表性,所以我义务劳动,也来写一写这个报告,给大家一个参考,让微软之外的朋友也能从中得到启发,未雨绸缪。
故障是如何被引入的?我们知道和程序员试图优化有关系。如果不硬把时间戳挤进一个 32-bit 的整数,就不会出现这个问题。我们常用的 64-bit 时间戳,即便是精确到微秒(百万分之一秒),也可以表示 58 万多年以后的时间。不出意外的话,那时候人类已经把自己作死了,可以使用鸵鸟算法完美解决时间戳溢出的问题。
要是只要求精确到秒,64-bit 可以表示 5800 亿年之后的时间,而科学界公认的宇宙年龄是130亿年,还不到这个范围的零头。所以,64-bit 给三体人用都足够了。
程序员应该都听过“过早的优化是一切罪恶之源。”(“Premature optimization is the root of all evil.”)在不需要优化的地方优化,往往是画蛇添足,增加了系统的复杂度,降低了可靠性,得不偿失。很难想象,我们已经从半部《论语》治天下发展到了一部 iPhone 装1000套大英百科全书,还有人会为了 32-bit 还是 64-bit 纠结。
我们看,这个作者选用了一个奇怪的精确到分的时间格式,这是非常罕见的。通常我们用的时间戳都是精确到秒甚至微秒。这个程序员这么做,大概是非要把时间戳弄成32-bit不可。他知道,要是精确到秒,32-bit 就不够用了。但是,他没有老老实实地改用 64-bit,而是耍小聪明,搞出了一个两位数年份的非标准格式,反误了卿卿性命。
他采取这种格式,肯定是做了一定的分析,知道 32-bit 整数有限制,4位数年份搁不下。可惜的是他没有把分析做到极致。发现问题之后,提出一个解决方案就觉得 OK 了,没有认真去论证方案是否可靠。
程序员都知道面向对象编程 OOP(Object-Oriented Programming)。老万觉得,比找对象更重要的是 POP(Proof-Oriented Programming,面向证明编程)。
啥意思?程序员在设计算法和写代码的时候要化身为数学家,每一步都要有据可依,不能大概齐。我在别的文章中也提到过,好的代码应该是明显正确的。用杨振宁先生的话说,秋水文章不染尘。我读代码时,一边读,一边会在脑子里构建这段代码100%正确的证明。如果这个证明在哪一步卡住了,说明这个代码要么是错的,要么还不够好懂。这两种情况我都会要求作者修改。
高手的代码,反映了作者清晰的思维、明确的意图。每一步要做什么,为什么这么做是正确的,让人一看就明白。这是程序员的基本素质。不幸的是学校里不教这个,公司培训也没有把这个划为重点,大家各自摸着石头过河,有的人摸着摸着就被冲到下游了,成了下流的,啊,我是想说二流的,程序员。但是功夫不到家,迟早要体现出来,出来混总是要还的。就像这位写出Y2K22 bug 的程序员一样,躲过初一躲过十五但是躲不过十六(按我们程序员常用的十六进制,16 就是十进制的 22)。
根据木桶理论,一只木桶能装多少水是由最短的一块板子决定的,一条链子有多结实取决于最弱的一环。所以,每个模块的负责人都要把自己的模块当成系统中最关键的一个,高标准严要求,用做证明题的态度来做应用题。
这个 bug 是用什么编程语言实现的呢?新闻里没说,我来推测一下。
我们知道,微软开发软件基本上使用两种语言:C++ 和 C#。据报道,这次的出错信息是
由此可见,这个数据类型是 long,从而可以排除 C#,因为 C# 里面 long 是 64-bit 的。所以最大可能是 C++。
推到这里,我不由得出了一身冷汗。为什么?因为 C++ 是一个功能无比强大但遍布陨石坑的语言,用好了可以强身健体,用得不好分分钟走火入魔。就好像一部《九阴真经》,练好了可以独步天下登上人生巅峰,练糟了就会神志失常频发羊癫疯。
具体地说,这 C++ 里面有符号的整型数溢出是无定义行为(undefined behavior),也就是我们说的程序跑飞了。如果程序员写出了这种 bug,可能引发任意后果,因为编译器在做优化的时候可以假设这种情况永远不会发生。学过逻辑的同学都知道,从一个错误的前提可以推出任何结论。一个无定义行为可以导致严重灾难,比如把用户服务器上面的数据删除,造成无可挽回的损失。
从错误信息看,这位程序员虽然犯了一个严重的设计错误,但他至少还知道防溢出(假定这段代码也是他写的),没有造成真正的无定义行为,可喜可贺。如果没有加这个溢出检查,后果可能就不仅仅是不能发送邮件这么简单了。
~~ 避坑指南 ~~
其实在这个 bug 爆雷之前是有很多机会防止的,遗憾的是这些机会都没有被抓住。下面我们就来看一看究竟有哪些机会可以在以后写软件的时候绕开这些坑。
权宜之计
32-bit 有符号整数只能表示 -2147483648 到 2147483647。但事实上在这个场景里我们没有使用负数的需求,所以一个最简单的改动是换成 32-bit 无符号整数,可以表示 0 到 4294967295。这样系统可以用到 2042 年底,让微软有 21 年的时间彻底修正这个错误,不失为一个权宜之计。
采用无符号整数的另一个好处是:即便溢出了也不是无定义行为(C++对无符号整数的溢出行为有严格的规定,虽然结果不合预期,起码程序不会马上跑飞),所以更安全。
代码审查
在软件行业的早期,程序员各自为营,手艺好的写出来的代码靠谱,水平差的出来的就是一包草。
随着行业的发展和系统规模的扩大,终于有一天单打独斗的模式行不通了,现代软件绝大多数都是团队合作的结果。一个好的软件公司,会通过一套制度和工具,行之有效地控制软件质量和可维护性,而不是把希望寄托在程序员的个人水平上。
所以大公司肯定有代码审查制度,不能谁都可以随便提交代码。张三丰写的代码必须要李四光检查了才能提交,反之亦然,这叫权力制衡。当然,这个要起作用,审查人必须认真负责,不能是橡皮图章。
不是随便一个阿猫阿狗都可以审查代码。不同的模块,有不同的负责人,改动必须经过负责人同意才能提交。模块负责人想把自己的系统搞好,审查的时候自然会比较负责。
试想这个程序员和审查者要是用了老万的 POP 方法,很快就会发现没法证明这个程序的正确性,这个 bug 就藏不住了。可惜他们不看《老万故事会》,白瞎了。
考虑到一个模块的负责人不一定是某个编程语言的专家,谷歌搞了一个制度,叫“可读性资格”:每个工程师入职后,要通过一定的培训,确认掌握了某种编程语言的高阶技能,能写出可读性好、正确性高、稳固可靠的代码,才能得到这门语言的可读性资格。如果一段代码的作者和审查者都没有这个语言的可读性资格,那么对不起,还得再找一个有资格的程序员再审查一次。
这个制度对保证谷歌代码的可靠性和可读性起了非常好的作用。你问我支持不支持?我当然说支持。要是微软还没有这个制度,欢迎萨提亚同学私信我,老万一定倾囊相助。
测试覆盖
只要是用户关心的行为,必须要有测试来覆盖。试想,要是这个邮件过滤系统的开发者会用老万主持开发的 GoogleTest,写了单元测试考察这个系统在各种奇葩年份的行为,肯定能在代码提交前就发现错误。所以说不重视测试也是造成这一大 bug 的原因之一。
程序员不是生来就喜欢写测试的,需要培训引导。越是有经验的程序员越重视测试,因为这是他们血淋淋的教训。16 年前,谷歌内部经历了一次工程师文化革命,一批志愿者通过草根运动,帮助大多数工程师接受了测试是代码的一部分的观点。
不过光靠热情还不够,一来总有些人写测试的积极性不高,二来有的时候不好判断还需要添加哪方面的测试案例。有两类工具可以帮助大家在写测试时有的放矢,值得推荐。
一类是测试覆盖率(test coverage)工具。它们可以检测出哪些代码还没有被测试覆盖。注意这不是一个完全可靠的指标。比如,可能有的测试案例把一段代码执行了一遍但是并没有仔细验证它的结果。这种情况覆盖率工具会错误以为这段代码已经被覆盖了,给你虚假的安全感。但是,有这个指标总比没有强:如果一段代码根本没有被任何测试覆盖,显然是有问题的。
还有一类工具是模糊测试(fuzzy test)。它的思想很简单:随机找一行程序改一改,比如说把 + 改成 - ,把 true 改成 false,再来跑一跑已知的测试案例,看有没有失败的。如果有,说明 OK。如果没有,说明还需要补充新的测试案例。
模糊测试工具的运行成本比代码覆盖率工具要高,但是可以发现后者发现不了的问题,在实际工作中可以配合服用。要是 Exchange Server 组用了这两类工具,就有比较大的可能在测试中抓住这一个大 bug。很可惜,这没有发生。
测试未来
如果我们有需要处理时间戳的重要系统,可以搭建一个特殊的测试环境,这个环境里的机器时钟故意调到未来,比如说10年后。这不就可以提前发现故障,有充裕的时间修正错误了吗?有了至少十年的提前量,程序员不用再流泪。
汪老师现身说法支持老万。
自动检测
我现在负责的项目是半路接手的有十多年历史的老系统,很多地方还采取了 32-bit 的表示方式。最近几年,这些遗留问题接二连三地爆了。痛定思痛,我们决定在大多数情况下禁用 32-bit,改用 64-bit。为此,我们加了一个自动检测系统,在提交代码的时候运行:如果代码里使用了 32-bit 整型数就报错。有了这样的系统,就可以防止这个 bug 进入产品。
慎选语言
象 C++ 这样遍地是坑的编程语言一定要慎用,因为很难找到足够多的优秀 C++ 程序员。大多数 C++ 程序员都是一知半解,会写出似是而非的代码,埋下定时炸弹。我日常工作之一就是扫雷 - 排除前面的程序员留下的隐患。
很多人选 C++ 是因为它能充分发挥硬件性能。可是,硬件每年都在大幅度进步。在绝大多数应用场景下,程序的执行效率不再是最主要的问题,很多时候你就是慢上50%也没事。相比之下,系统的安全性和可靠性要重要得多。何况,写得不好的 C++ 效率可能还没有 C# 高。
象 C# 这种更高级的语言,因为对用户有更多的限制,编译器有更多的自由去优化。比如 .NET 的运行环境可以通过消除内存碎片化来提高机器指令的执行效率。而 C++ 受编程模型的制约,是无法在内存里随便移动对象的。所以,某些类型的问题,用 C# 解决可能效率更高。一开始就假定需要 C++ 的性能,其实是某种形式的过早优化。
如果 Exchange Server 用 C# 实现,很可能就不会出现这个 Y2K22 bug。我大胆预测一下,如果五年之后 Exchange Server 还没有被淘汰的话,微软会用 C# 之类的基于 .NET 的语言重写之。
青云直上
前面说过,Exchange Server 是部署在用户自己的机器上,没有拥抱云计算。这种模式出了故障修改起来很麻烦。像前段时间沸沸扬扬的 log4j 安全漏洞,耗费了很多公司大量人力物力去修正,就是因为出事的是一个库(library)不是一个 service,所有用到这个库的项目都必须重新构建。
试想,要是用户没有用 Exchange Server,而是选择基于云的邮件解决方案(比如谷歌的 Workspace),一旦出了问题,服务商可以快速在云端发布新的版本解决,系统的出错时间会短得多。不是说云计算没有问题,但绝大多数情况下它是比本地部署更经济可靠的解决方案。
微软 Exchange Server 这个瓜咱们今天就吃到这里,希望大家有所收获。如果你喜欢,欢迎订阅、转发、评论、点赞。谢谢!
~~~~~~~~~~
猜你会喜欢:
程序员护发秘籍 - 掌握这些工作技巧,包你不脱发
程序员的核心技能 - 以脱口秀的方式讲解程序员最重要的技能
程序员要学好语文 - 写作很重要,哪怕是写给自己看
如何做出保鲜十年的软件 - 老码农冒死披露行业内幕系列
我在谷歌弄啥咧之十四 - 拿奖到手软
2021老万故事汇 - 《老万故事会》2021全年文章汇总,供你按图索骥
~~~~~~~~~~
关注老万故事会公众号:
本公众号不开赞赏不放广告。如果喜欢这篇文章,点个在看,转发给朋友就是对老万的最大支持。谢谢大家🙏