A full stack engineer never allocates from heap
寫了那麼多年代碼,得意的項目當然不少。不過今天想說的,是個很久遠的簡單小程序,久遠到我還是一個青蔥少年,久遠到我還在百度,久遠到我們剛發現百度需要一個基礎技術平台,組建了一個小小的團隊,開始憧憬詩和遠方。我在其中,折騰的是一個叫「前端技術方向」的不明覺厲的東西。
雖然當時我們連前端包括什麼要研究什麼問題都還沒爭論清楚,但是直接吐網頁的前端伺服器,肯定是包含其中吧,於是我開始看一個大產品的前端伺服器。彼時的百度,這種要頂大流量的東西顯然是用 C/C++(主要是 C, 呵呵)寫的,雖然已經「高性能」了,這個前端服務瓶頸還是在 CPU, 說是給模板引擎壓死了。
這個狀況相當合理,性能不是被IO線程模型之類壓死那好像沒啥好弄的。不過為了寫報告,我還是看了一下模板引擎的代碼。它大概就是把模板逐位元組掃一遍,看到變數或者控制語法就回調做操作填內容,完了,簡單直接。…… 等一下…… 模板是靜態文件很少更新,我可以給變數位置做個 index, 把變數語義做個簡單預解析吧?做上去,快了好象是 30%? 不錯。發了曬數據郵件準備收工時,突然呆住了:既然我都做 index 了,我知道每段模板文本的確切長度,我為毛還要 strcpy 逐個位元組拷我傻啊?
5 分鐘,全改成 memcpy, 順手去掉幾個重複內存分配和拷貝。然後就清凈了,模板引擎benchmark快了幾倍忘了,但綜合起來整個伺服器 QPS 穩定翻了兩三倍模板引擎再也不是瓶頸是有的。百度長期抗拒用腳本語言做前端服務,性能是個重要因素,我後來還對同樣語法做了個性能接近的 Python 實現,「C/C++ 就能高性能腳本語言搞不定」好像也沒那麼站得住腳了。
結果,我為這個「前端技術方向」做的第一個改進是個非常底層的優化。我為這個小改動得意,就是因為這是個房間里的大象:問題是這麼明顯卻沒人去動,改了大家都覺得理所當然就該這樣有什麼難的,但它效果就是好得讓你無話可說。
我後來挖了個墳,看為什麼這麼明顯的問題那麼久都沒人發現。跟百度很多代碼一樣,這個模板引擎源於大搜索。一開始,它只是一個比 printf 稍微好一點的庫函數,模板都是代碼字元串,還有根據後端返回動態拼接模板之類的操作,預處理之類的優化實際上是不可能的,老老實實逐位元組做就好了。到後來,模板管理越來越規範,剝離出了模板文件,模板語法也越來越強,但是這個核心循環就被遺忘在那裡。而且對大搜索而言,前端業務處理和演算法極複雜而頁面簡單,模板引擎開銷並不顯山露水,可是到了頁面複雜得多而業務演算法沒有這麼吃重的產品,大家卻都已經忘了模板引擎里有這麼一出了。
這種模式在我的職業生涯里常常出現,甚至後來在 Google 比搜索流量還大的 AdSense 前端服務上,我還碰到了幾乎一樣的狀況,而且這次提升的不僅是性能,還是真金白銀了。所以,我覺得工程師要養成刨根問底的習慣,不僅去了解底層系統如何工作,更重要的是探尋系統設計背後的前提假設 —— 不管是原始設計還是經年累月的改動,它們可能都基於當時的特定需求和限制做出了最好的選擇。但是,當年的設計前提在今天已不復存在,而實現依舊。再加上軟體系統是如此的複雜,我們做任何一個事情都要面對無數層把這些不再有效的實現隱藏起來的抽象,這個房子里其實已經遍地是人們視而不見的大象了。
而我眼中的「全棧工程師」,當然不僅要能從前端網頁寫到後台存儲(這個真的是和生活可以自理差不多),更是要能在這麼複雜的一個技術棧中找到瓶頸,而不管這瓶頸在技術棧的什麼地方,你都有能力和信心去解決它而不會為自己設限。一個全棧工程師總會感到自己不夠「全」,視野之外,總有新的天地。再說得高大上一點,就是 Larry Page 給 How Google Works 寫的前言里說的「Think from First principle」,我翻譯成「究本窮源,格物致知」,性能優化是如此,架構設計是如此,產品設計也一樣。
說到前端伺服器優化,Facebook 還有更直接的:現在的伺服器 CPU 都支持 SMP 和 NUMA 架構,一塊主板上的幾塊 CPU 能協作共享一塊大內存。但是,Facebook 前端 PHP 哪來的什麼共享內存,完全用不著,這些支持和協商電路完全是在白耗電,雙 CPU 共享內存匯流排更是性能殺手。於是他們跟 Intel 合作把這些多餘的電路去掉,把雙插槽架構改成更緊湊的單插槽。BOOM! 性能一下提上來了。很簡單很沒技術含量是不是,我超~愛這種事情的。
在這個點上,我想我得祭出這張 Latency Numbers Every Programmer Should Know 的表了:
Latency Comparison Numbersn--------------------------nL1 cache reference 0.5 nsnBranch mispredict 5 nsnL2 cache reference 7 ns 14x L1 cachenMutex lock/unlock 25 nsnMain memory reference 100 ns 20x L2 cache, 200x L1 cachenCompress 1K bytes with Zippy 3,000 ns 3 usnSend 1K bytes over 1 Gbps network 10,000 ns 10 usnRead 4K randomly from SSD* 150,000 ns 150 us ~1GB/sec SSDnRead 1 MB sequentially from memory 250,000 ns 250 usnRound trip within same datacenter 500,000 ns 500 usnRead 1 MB sequentially from SSD* 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memorynDisk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtripnRead 1 MB sequentially from disk 20,000,000 ns 20,000 us 20 ms 80x memory, 20X SSDnSend packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 ms n
這是大部分軟體程序員要面對的第一層抽象。真的,只要你還是在寫要給人用的程序,不管是在做前端網頁還是在做 RTB 策略優化,請記住這張表。看看 CPU cache 跟主存的速度區別,再去查查現在的伺服器 CPU 普遍的緩存大小,你就知道緩存命中率對性能的影響有多大,你在教科書上學到的各種大 O 複雜度很多時候都還沒有選對數據結構和內存布局提高緩存命中率重要。SSD 和機械硬碟,順序讀和隨機讀是老生長談了,再看看各級網路延遲,設計分散式架構時,設計前端應用時,也該會心有戚戚焉吧。
當然,跟這個技術棧上的所有東西一樣,這些數字,是會變的…… 多核改變了一切,SSD 改變了一切,就是因為他們改變了我們設計軟體時的基本假設。我剛入行的時候,多核還沒有那麼多,SSD 還沒能可靠地在伺服器上應用,這些觸及靈魂深處的改變,又是另一個故事了。
推薦閱讀: