<b>本文講的是從 Node.js 到 Golang 的遷徙之路,</b>
<b></b>
我在大學時期就開始涉獵 JavaScript 并會随便寫一些網頁。我把 JS 當作寫 C 語言和 Java 時候的一種小憩,并且我認為它是一種相當受限制的語言,它一直在鼓吹能夠實作一些令使用者歎為觀止的特效和動畫。我第一次教人程式設計就是用的 JS,因為它簡單易學、能快速給開發者以可見的結果。将它與 HTML 和 CSS 代碼寫到一起,就能得到一個網頁,初學者對此愛不釋手。
然後意想不到的事情發生了。兩年前,我還在一個研究性質的崗位上從事服務端程式設計和安卓應用原型的開發的時候,Node.js 突然躍入了我的視野。後端的 JavaScript?誰會拿它當回事兒呢?充其量也就是嘗試讓服務端性能、擴充等方面的開發容易些罷了,但随之而來的是運作和擴充性能的下滑等等。或許那僅僅是我根深蒂固的開發者的懷疑論,當讀到一些有關快速、簡單和高産的東西的時候,我就總是會那樣想。

然後是接踵而至的研究、報告、教程和附加項目,六個月之後我才意識到自從第一次讀到 Node 以來,我一直在心無旁骛地研究它。它太簡單了,尤其是當我每兩個月就要開發新的創意的時候我更加意識到它的友善。但是 Node 并不僅僅是為應用原型和小項目而生的,甚至很多像 Netflix 這樣成熟的公司也将業務分了一杯羹給 Node。霎時間,我手裡拿着金剛鑽看到了這世界充滿了瓷器活兒。
我們問題重重的 Node 伺服器承擔着相當直接的使命。Digg 使用亞馬遜的 S3 的雲存儲服務,S3 很出色但是不支援批量 GET 操作。為了不将所有的負荷都加到我們的 Python 伺服器上,不讓 Python 伺服器每次都從 S3 請求超過 100 個 key,我們決定利用 Node 簡單的異步代碼模式和大并發處理來完成。于是 S3 的内容擷取服務 Octo 誕生了。
Node Octo 除了偶爾的掉鍊子之外性能都很好。某天它需要處理一個網絡峰值,每分鐘的請求數量從 50 躍升至 200+,與此同時每個請求中 Octo 基本都要從 S3 擷取大概 10-100 個 key,也就是說它可能每分鐘有 20,000 次 S3 的 GET 請求。日志表明,網絡峰值的時候伺服器的性能會大大下降,但是問題在于它并不總是能恢複。就這樣,每隔一周在 Octo 卡住并且失靈後,我們也卡在恢複 EC2 執行個體當中。
伺服器請求有着嚴格的逾時時間,接收到請求後的幾毫秒時刻内,Octo 應該将從 S3 成功擷取的資訊傳回給用戶端并繼續工作。然而,即使設定逾時時間為最大值 1200 毫秒,Octo 在最壞的情況下還是會出現請求處理時間達到 10 秒之久。
Octo 的代碼非常的不同步并且我們擷取 S3 的 key 和 value 的方式非常激進,并且它還和兩個中型的 EC2 執行個體交叉運作,後來我們增加到四個。
我将代碼重寫過三次,每次都更深層次地挖掘 Node 的優化、填坑并在性能上锱铢必較。我檢視了流行的 Node 網站伺服器架構的性能評估,比如 Express 和 Hapi,并和 Node 内置的 HTTP 子產品做了比較。我移除了所有第三方的子產品,盡管它們很好用但是會拖慢代碼的執行,結果三次都遭遇了相同的問題。無論我多努力,我還是不能使得 Octo 走上正軌,也不能減少請求峰值時性能的下降。
Node 的 “event loop” 是處理高傳送率方案的核心。那裡充滿了神迹和天馬行空,也正是因為它才使得 Node 雖然是單線程卻還能夠允許背景處理任意數量的操作。
并沒有多麼神奇的 Event Loop 阻塞(X軸:時間/毫秒)
你能看到在我們對服務進行彈性恢複之後原本丢失的性能又回來了。
即使發現了 event loop 阻塞是罪魁禍首,那也隻是說明了在一開始的時候性能滞後的原因。
大多數的開發人員都聽過 Node 的非阻塞 I/O 模型,那非常棒因為它意味着所有的請求在異步處理的時候不會造成執行阻塞,也不會産生任何多餘開銷(像線程和程序)并且作為開發人員你能很幸福地不用管背景發生的事。然而,你要牢記 Node 是單線程的,那意味着沒有并行執行的代碼。I/O 或許不會阻塞伺服器,但是你的代碼會啊。如果我在代碼中調用休眠 5 秒鐘,那麼伺服器在這段時間将不會有任何響應。
在一個循環中,隊列輪詢下一個消息(每個輪詢被稱為一個“tick”),當遇到一個消息時,執行該消息的回調函數。這個回調函數的調用作為調用堆棧中的初始幀,并且因為 JavaScript 是單線程的,堆棧中所有調用的傳回之前會停止進一步的消息輪詢。并發的(同步的)函數調用會在堆棧中增加新的調用幀……
如果我們的 Node 服務隻是需要傳回觸手可得的資料,那它處理接收的請求綽綽有餘。但是相反,它一直等待着許多嵌套的回調函數,這完全依賴于 S3 的響應(而這有時會超級慢)。請求逾時之後,事件和與其相關的回調函數會被置于超載消息隊列中。然而,逾時事件可能在 1 秒的時候發生,隻有等目前隊列的消息和其回調函數都執行完(這可能需要幾秒鐘)該事件的回調函數才會被處理。我能想象請求峰值時堆棧的狀态,但事實上,我并不需要想象,隻需一點點 CPU 的運作切面就能展示給我們相當生動的狀态圖像。對以上的長篇累牍我表示抱歉。
失敗情況下的火焰圖
如果這個标題沒有足夠懸念的話[原标題是:使用 Golang 每分鐘處理百萬請求。譯者注],其主題是建立服務向 S3 發送 PUT 請求(有人遇到過同樣的問題麼?)。這已經不是第一次我們談論要使用 Golang 了,而現在我們有了一個絕佳的測試對象。
良好的不溫不火的狀态
我們的服務平均響應時間幾乎縮減了一半,我們的逾時設定(S3 響應太慢,是以會有逾時)也能夠按部就班,并且網絡峰值也隻對服務造成了微小的影響而已。
藍色的是 Node.js Octo | 綠色的是 Golang Octo
用 Golang 更新之後,我們很容易地就能每分鐘處理 200 個請求,每天處理 150 萬個 S3 内容擷取。我們一開始運作在 Octo 上的那四台負載均衡執行個體怎樣了?我們現在又所縮減到了兩個。
我并不是為了說明 Golang 是解決我們疑難雜症的靈丹妙藥。我們再三考慮了我們每項服務的需求,作為一個公司,我們努力地站在新技術的前沿并且會反躬自省,我們能做得更好嗎?這将是一個持續進步的過程,我們将會再三調研并認真計劃。
我可以很自豪地說,我們的 Octo 服務已經非常成功地運作了幾個月(修複了一些 bug 除外),結局皆大歡喜,Digg 将繼續前行。
<b>原文釋出時間為:2016年12月07日</b>
<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>