前不久,嗶哩嗶哩(一般常稱為 B 站)發(fā)布了一篇文章《2021.07.13 我們是這樣崩的》,詳細(xì)回顧了他們?cè)?2021.07.13 晚上全站崩潰約 3 小時(shí)的至暗時(shí)刻,以及萬(wàn)分緊張的故障定位與恢復(fù)過程。
創(chuàng)新互聯(lián)專業(yè)提供眉山服務(wù)器托管服務(wù),為用戶提供五星數(shù)據(jù)中心、電信、雙線接入解決方案,用戶可自行在線購(gòu)買眉山服務(wù)器托管服務(wù),并享受7*24小時(shí)金牌售后服務(wù)。
那篇文章將定位過程、問題分析、優(yōu)化改進(jìn)等方面寫得很詳細(xì),在我印象中,國(guó)內(nèi)互聯(lián)網(wǎng)大廠在發(fā)生類似事故后,能夠如此開誠(chéng)布公地“檢討”“還債”的并不多見。(值得送上一鍵三連~~~)
對(duì)于搞技術(shù)的同學(xué)來(lái)說,這篇文章是不錯(cuò)的學(xué)習(xí)材料。而我最為關(guān)注的內(nèi)容,其實(shí)是關(guān)于編程語(yǔ)言的特性,也就是在代碼層面上的細(xì)節(jié)問題。
在關(guān)于問題根因的分析中,我們看到了罪魁禍?zhǔn)椎?7 行代碼,它是用 Lua 語(yǔ)言寫的一個(gè)求最大公約數(shù)的函數(shù):
簡(jiǎn)單而言,這個(gè)函數(shù)預(yù)期接收的參數(shù)是兩個(gè)數(shù)字(普通的數(shù)字或者字符串類型的數(shù)字,即兩種類型都可以),然而,它的 if 語(yǔ)句卻只判斷了一種類型(普通數(shù)字),忽略了字符串類型的“0”。
在故障發(fā)生時(shí),它的第二個(gè)參數(shù)傳入的是字符串類型“0”而不是數(shù)字類型 0,導(dǎo)致 if 語(yǔ)句判斷失效!
由于 Lua 是動(dòng)態(tài)類型語(yǔ)言,只有在程序運(yùn)行時(shí)才知道傳入的參數(shù)是什么類型。這屬于是所有動(dòng)態(tài)類型語(yǔ)言的特色,在 Python、JavaScript、PHP、Ruby 等動(dòng)態(tài)類型語(yǔ)言中,也會(huì)有同樣的表現(xiàn)。這不是啥新鮮事物。
然而,真正該死的問題在于,Lua 還是一門弱類型語(yǔ)言,它不像 Python、Ruby、Java 等強(qiáng)類型語(yǔ)言那樣,它竟支持隱式類型轉(zhuǎn)換!
在 Lua 中,數(shù)字字符串在與普通數(shù)字作算術(shù)運(yùn)算時(shí),會(huì)將字符串類型隱式地轉(zhuǎn)換成數(shù)字類型,如上圖所示的“a % b”,如果 b 是字符串類型的數(shù)字,那它就會(huì)被轉(zhuǎn)換成數(shù)字類型!
而在 Python 這種強(qiáng)類型動(dòng)態(tài)類型語(yǔ)言中,這樣的轉(zhuǎn)換是不可思議的,數(shù)字與字符串作算術(shù)運(yùn)算,能得到的只會(huì)是報(bào)錯(cuò):TypeError: unsupported operand type(s) for %: 'int' and 'str'
Lua 語(yǔ)言的這種“字符串隱式變數(shù)字”的行為,即使在大意不察覺的情況下,似乎也不會(huì)造成太大問題。在 B 站代碼中,除了出事故時(shí)傳的字符串“0”以外,估計(jì)它一直接收的都是其它字符串?dāng)?shù)字,一直也沒出問題,顯然程序員是把這當(dāng)成一種便利手段了(因?yàn)椴恍枳黝愋娃D(zhuǎn)換)。
然而,不幸的是,Lua 中還有一個(gè)特殊的“nan”,它會(huì)進(jìn)一步將這一個(gè)“小小的錯(cuò)誤”傳遞下去,直至傳到了地老天荒不受控制的死循環(huán)里……
在大多數(shù)編程語(yǔ)言中,除零操作都是不可饒恕的錯(cuò)誤,這跟我們?cè)谛W(xué)數(shù)學(xué)課堂上就掌握的常識(shí)相吻合:數(shù)字零不允許作為除數(shù)!
掏出手機(jī),打開計(jì)算器,看看它是怎么說的:
看到了吧!不能除以0?。?!
繼續(xù)看看 Python 對(duì)于這種操作的反應(yīng):
ZeroDivisionError 除零錯(cuò)誤,這是在捍衛(wèi)我們根深蒂固的數(shù)學(xué)常識(shí)。
那么,Lua 語(yǔ)言在除零操作后得到的 nan 到底是個(gè)什么東西呢?
nan 一般也被稱為“NaN”,是“No a Number”的縮寫,表示“不是一個(gè)數(shù)”。它來(lái)頭不小,是在 1985 年的 IEEE 754 浮點(diǎn)數(shù)標(biāo)準(zhǔn)中首次引入的。
直白地講,它也是數(shù)字類型中的一個(gè)值,但是表示的是一個(gè)“不可表示的值”。也就是說,它表示的是一個(gè)非常抽象概念的數(shù)。
也許我們比較容易理解另一個(gè)抽象的數(shù)“無(wú)窮大”,因?yàn)樵谥袑W(xué)數(shù)學(xué)課上就經(jīng)常接觸到,而 nan 也是類似的一種特殊的數(shù),只不過它較為少用且更難以捉摸罷了。
Python 中也有這兩個(gè)數(shù)的存在,即 float('inf') 表示無(wú)窮大、float('nan') 表示非數(shù)。它們就像是兩個(gè)黑洞,會(huì)吞噬掉任何試圖前來(lái)“搭訕”的數(shù):
那么,當(dāng)這兩個(gè)黑洞相互靠近時(shí),誰(shuí)的引力更大些呢?請(qǐng)看示例:
看來(lái)還是 nan 的優(yōu)先級(jí)更高一籌啊。
然而,盡管 Python 中有 nan,但它并不因?yàn)檫@個(gè)數(shù)而拋棄前文提到的常識(shí)。而同為腳本語(yǔ)言的 Lua 卻拋棄了常識(shí), 在出現(xiàn)除零這種非法操作時(shí),它不是報(bào)錯(cuò),而是得到 nan 的結(jié)果。
這樣的特性簡(jiǎn)直是自由得過分,也許在某些時(shí)候會(huì)挺有用吧,但它也會(huì)埋下未知的隱患。
回到 B 站的問題代碼,弱類型的 Lua 語(yǔ)言由于太過自由,它放行了字符串?dāng)?shù)字與普通數(shù)字的運(yùn)算,又因?yàn)閷?duì) nan 過于自由的使用,它放行了數(shù)字除零的操作,兩次的放行,使得短短幾行代碼一路暢行不止,一路消耗服務(wù)器資源,直到 CPU 100%,直到牽動(dòng)服務(wù)集群故障,直到高可用的多活機(jī)房服務(wù)不可用,導(dǎo)致全站崩潰 3 小時(shí)的事故……
當(dāng)然了,如果當(dāng)初寫下這段代碼的程序員多加一個(gè)條件判斷,這一次的事故就完全可以避免。從另外的視角看,這就是程序員在遞歸程序的終止條件上處理不當(dāng),不能甩鍋給編程語(yǔ)言那兩項(xiàng)自由不羈的語(yǔ)言特性。
但是,我相信寫下那段代碼的程序員大概率是長(zhǎng)期使用其它編程語(yǔ)言,現(xiàn)學(xué)現(xiàn)賣上手寫 Lua,盡管知道 Lua 語(yǔ)言動(dòng)態(tài)弱類型的特點(diǎn),但思維習(xí)慣上仍深受其它語(yǔ)言影響,這才“一時(shí)失足、小河翻船”……程序員內(nèi)心有苦說不出?。?/p>
短短的 7 行代碼,說簡(jiǎn)單就簡(jiǎn)單,說不簡(jiǎn)單也不簡(jiǎn)單。本文就不展開說輾轉(zhuǎn)相除法求最大公約數(shù)了(說來(lái)話長(zhǎng)),單單是前面提及的隱式類型轉(zhuǎn)換加上除零得 nan 的細(xì)節(jié)問題,就足夠?qū)е乱粓?chǎng)大事故了。
從 7 行問題代碼中,作為吃瓜群眾的我們,能得到些什么收獲呢?到底是漲見識(shí)了,還是“又學(xué)廢了”呢?
人生苦短,不求無(wú) Bug,但求讀者老爺們賞個(gè)一鍵三連吧~~~