恩,這是名副其實的杯具(很抱歉我用這個詞,shitstorm)。長話短說,如今MelonCard已經被TechCrunch使用,然而所有事情都可能突然出現問題。過去幾天裡,我們對MelonCard進行了巨大的改進,使用NodeJS長輪詢機制以及平滑的KnockoutJS動態jQuery Templates前端。在完成站點無縫升級,在達成“所有功能都是最新”目標的同時,成為外觀更美、體驗更好的產品。為了避免可能發生的不良影響,我們進行了手工測試和單元測試,併為Node結合使用了一整套Vows。完成所有的系統測試,朝著目標全速前進,對嗎?事實並沒有那麼快。
譯註:
長輪詢(long polling):是Comet的一種實現方式,也是Facebook,Plurk實現動態更新內容的方法,具體原理是傳送一個長時間等待的request,當伺服器有資料response的時候立刻斷掉,接著再傳送一個新的request。
Vows:Node.js的非同步行為開發框架
我們的系統用的是NodeJS,根據使用者輸入決定他的當前狀態,例如使用者輸入“我正在等待更新這兩條記錄”,伺服器(基於時間戳檢查)會返回“您的記錄已經是最新的”或者“記錄xxx已更新為yyy。”(實際的過程會比這個更加複雜,我們使用了Redis共享變數和會話,並對Rails、mySQL、Redis和Node之間的介面進行安全檢查)。(這個過程)看上去非常簡單,但當事情不如預期的那樣時,即使是簡單的NodeJS 程式碼也會成為噩夢。今天噩夢發生了。
處理完今天的日常工作之後,我們遇到了一個正常的新使用者註冊高峰(一個小時內有50-100個新使用者註冊)。突然之間,所有事情都開始出現問題,沒有一個頁面可以正常工作。我們的郵箱開始被“你的產品掛掉了”類似的郵件塞滿了。我抓起一杯咖啡準備戰鬥。
我的第一個反應是:NodeJS能夠很好地處理負載,這是眾所周知的。50或100個使用者不可能讓系統崩潰。在得到Ryan Dahl的幫助之後,我們知道這不是Node本身的問題(後來的結果也驗證了這一點)。伺服器開始返回異常結果,使用者輸入“我的記錄是a、b和c”然而服務的回答是“你這個蠢貨,刪掉x、y和z這裡的記錄是a、b和c。”即使能夠確定問題的範圍並可以重複錯誤,也幾乎不可能通過Node可憐的錯誤處理和除錯功能解決。採用的方法就是下面的unix命令(是的,我對production進行查詢)
1 NODE_ENV=’production’ node/privacy.js | grep “Returned results”
你能夠想象對這些結果進行分類是件多麼恐怖的事。結果是,所有的分段測試和單元測試都正常,我已經束手無策了。最為重要的是,我們的系統再一次執行了大量的(安全性)會話檢查。例如,如果使用者在(瀏覽器)不同的標籤頁之間進行登入和退出時,會有大量的“Unauthorized (未授權)”錯誤彈出(這讓企圖瞭解真正問題的我們更加糊塗)。當我列出錯誤的時候,看上去像這樣:
1 Trace: at EventEmitter.<anonymous> (/—/node/privacy.js:118:11) at EventEmitter.emit (events.js:81:20)
錯誤出現的那一行(或唯一返回給我的Node)如下:
1 process.on(‘uncaughtException’, function (err) { console.log([‘Caught exception’, err]); console.trace(); });
雖然程式還沒有崩潰,但我還是沒有找到線索。在這裡最佳實踐也沒有辦法捕捉那一行的錯誤(手動前端測試,單元測試,錯誤處理,等等)。的確,我應當做負載測試,但是即使(測試)到達了競爭條件同樣不能讓我感到安全。
4個小時以後(當我翻到第503頁“ Temporarily Unavailable”,這時我的聯合創始人對每一位失望或好奇的使用者回覆了致歉的郵件),我意識到問題出在伺服器將我的請求輸入(請求引數)錯誤地當成隨機使用者請求引數。確切的說,伺服器的設計只會對你的請求返回你的相關資訊,但是對你的請求產生了錯誤的理解。比如你說“我喜歡蘋果和甜瓜”,但是服務告訴你“不要傻了,你喜歡的是芒果。”所以,雖然(從安全的角度來說)一切都是安全的,但是結果是錯的。為什麼我的ExpressJS伺服器不能理解我的請求呢。我繼續追蹤發現了下面的程式碼:
1234567 app.all(‘/apps/:user_id/status’, function(req, res, next) {// …initial = extractVariables(req.body);});
看上去很糟糕是吧?這是個該死的策略。我不是個JavaScript專家,但請允許我盡我所能為你解釋(或者也刻意看看這裡)。在JavaScript裡,你可以宣告變數為函式(區域性)變數或者全域性變數(在變數宣告/引用的作用域連結串列上為其增加複雜性,直到最後確定為全域性變數)。當我沒有使用var來宣告一個全域性變數initial時,它會遍歷作用域連結串列直到全域性範圍,最後會建立一個全域性變數initial。當下一個請求來到時,它會遍歷同樣的連結串列並重寫該變數(另一個請求仍然希望使用這個變數)。每個請求都會重複這一過程。當伺服器試圖在接下來的處理中回覆每個請求時,它會讀到不斷被截斷的變數,並返回的結果不正常,甚至可以說是荒唐。因此我應當這麼修改:
1 var initial = extractVariables(req.body);
這樣會在我的匿名函式作用域內建立這個變數,因此接下來的請求就不會截斷這個值。這是新手才會犯的錯誤,但是在我做的所有除錯或測試中都完全不會帶來競爭/併發問題。
現在我要對你說的是:你應該用過 CoffeeScript並且可能對TameJS很在行。你可能是正確的。我想理解第一次執行NodeJS呼叫了哪些函式,但這對於我的公司是一個不必要的損失。在其他情況下,它可能帶來更嚴重的問題(假如我在會話中錯誤的使用了變數會如何?)。最重要的是,缺乏真正好的錯誤處理(在Rails,我們通過backtrace跟蹤錯誤並把錯誤傳送給開發團隊)以及真正的除錯(依靠的是grep和less命令)讓我覺得我們離好的開發差距很遠。或者也許我應當更加小心。
在4個小時當機並令上百個使用者失望而歸之後,我找到了問題的癥結並修復了產品。風雨之後,撥雲見日。我們開始對受到影響的使用者致歉,盤點這次事故的損失並繼續工作。但是,缺少一個關鍵字帶來這樣的損失還是讓我感到難以釋懷。少寫了一個var會讓我成為壞人嗎?
【如需轉載,請標註並保留原文連結、譯文連結和譯者等資訊,謝謝合作!】