18 May 2014

這一二週都在打泥巴戰,搞得滿身混泥。最近的需求是這樣的,一個服務由三台伺服器串連完成,要快也要能 scale,更不能中斷服務。架構上大致是這樣:

                                                                                       
 end user     +----------+          +----------+         +---------------+
  browser     |          |          |          |         |               |
    +-------> |          | +------> |          | +-----> |               |
              |          |          |          |         |               |
              |   Web A  |          |   Web B  |         |     Web C     |
              |          |          |          |         |  (3rd party)  |
small html    |          |          |          |         |               |
    <-------+ |          | <------+ |          | <-----+ |               |
              |          |          |          |         |               |
              +----------+          +----------+         +---------------+ 
                                                                                       

用戶的請求來自瀏覽器,然後經過 ABC 三台伺服器,中間的傳輸都是 http json,最後再吐一個小的 html 給用戶。C 伺服器可能是自家的,也可能是外部廠商,所以沒有什麼控制權。不過為了測試的需要,還是寫了個 dummy C 伺服器供測試。由於這個服務量可能很大,未來也有可能有更多的伺服器介入,所以這次我準備做先期的壓力測試,來幫助確認合適的架構。

計畫初期
  1. 使用基本組合 tomcat + postgresql + spring mvc + freemarker
  2. 利用基本組合開發軟體的原型 (prototype)
  3. 壓力測試原型軟體後得到數據
  4. 換用新的組合:netty (非同步), cassandra, jsp
  5. 用新組合壓測原型軟體得到新數據
  6. 比較各種組合的數據,取得最佳組合
正式開發期
  • 利用最佳組合,開發完整個軟體
  • 最佳化整個軟體,做正式壓測

一開始呢,我們先拿自家工具箱裡上層的工具組:tomcat + postgresql + spring mvc + freemarker,來開發原型。隨著原型逐漸可以運作後,我就開始進行初步的壓力測試,因為我心知這組工具可能無法負荷這次的需求。我最擔心的還是資料庫,所以上一篇裡就針對 postgresql 的 insert 做了測驗。資料庫的測驗只是其中一環,接下來就是換上我工具箱底層裡的壓箱寶:netty, cassandra, jsp。然後嘗試搭配不同的組合,試圖取得最好的效能。

第一輪是 template engine 的較勁,jsp 對戰 freemarker:

       jsp:  22000 req/s
freemarker:  29000 req/s
plain html:  30000 req/s (request per second)

我原本以為 jsp 會是最快的 (不用任何 taglib),因為它是先轉 servlet 再編譯成 bytecode。但實際的測驗是 freemarker 壓倒性的勝利,相當接近純 html 檔的速度。我不能說 freemarker 樣樣都贏 jsp ,也許輸出的 html 結構不一樣,結果又會不同。這個測驗是拿實際成果的 html 來測 (需求是產生一個小 html),既然 freemarker 大勝了,就沒必要換 jsp,而且它還接近純 html 檔的效能,這樣連其他的 template engine 都不用測了。

這個數據也會拿來做為空白對照組,因為這個測試只經過 A 伺服器,純產生一個 html 而已。現在我大概知道我的測試環境最理想可以得到約每秒 3 萬的請求。

第二輪是 postgresql 和 cassandra insert 的較勁 (硬碟 SSD):

  cassandra:  21681 TPS 
 postgresql:  16876 TPS (transaction per second)

上一篇文章內,postgresql 經過調整後得到不錯的成績,這一次追加 cassandra 的測試,結果當然是 cassandra 勝利,達到每秒 2 萬個交易,但贏的並不多。這讓我蠻驚訝的,我驚訝的是 postgresql 調校後 insert 效能也非常接近 NoSQL,我會認為這算是同等量級的效能了。但差別不大讓決策更困難,到底選哪個好?

第三輪是整體的壓力測試,測試 http 請求實際經過 ABC 伺服器,也包含寫資料庫和運算。而為求逼近真實情況,Web C 這個 3rd party 伺服器上加上了人造的 delay (~25ms)。

測試的組合大致分兩大組,第一組就是上面提到的基本組合,採同步 (synchronous) 的架構

同步架構:
  • tomcat (Blocking IO)
  • Apache http client (用在伺服器間的溝通)
  • spring mvc
  • freemarker
  • postgresql

第二組是用 netty http 模組做為 ABC 伺服器間溝通,採用非同步 (asynchronous) 架構

非同步架構:
  • netty server (NIO, http piplining)
  • netty client (NIO, http piplining) (伺服器間的溝通)
  • freemarker
  • postgresql

測試結果如下:

  同步組:  1200 req/s
非同步組:  3300 req/s

這是調了 thread, connection 一堆變數得到最好的數據了。好了,跟空白測試相比,無論同步與否,吞吐量都降了一級。非同步架構並沒有很神的讓我得到萬級的吞吐量。另外同步的 latency 平均而言比非同步還要好一些。而且非同步的 latency 在 95% percentile 常常很差。

我對這樣的結果當然是很沮喪,這個整體測試我們其實做了非常多組合,比方說 tomcat 也換過 NIO,也用過 AsyncServlet,也試過 Apache async http client。最後跟同事討論後,才發現在系統內同步與非同步兩者互搭是行不通的,要採用非同步架構,得全程三個伺服器都採用才行,這才有了第二組的成果,我們也才得到了最高的吞吐量 3300 req/s。第二組組合我還開了外掛,特別加上 http pipelining,有點做弊 (C 伺服器是外部廠商,可能不支援 http pipelining 啊)。

當然吞吐量下降一級的成因可能很多,可能是原型軟體某個地方太差了,也有可能在單機上跑這個整體測試無法推到極限,也有可能經過三台伺服器,本身就會帶來一個無法突破的屏障。時間足夠的話當然是可以在繼續調校,但其實這個軟體還在原型階段,做最佳化是沒有意義的。做這個壓力測試的目的還是讓我們可以得到基準值,還有整體架構的方向,因此這輪的實驗就此停止了,雖然如此,也花了整週的時間打這場混戰。

What I learn

我本來以為我的壓箱寶可以得到神級結果,但數據並不是這樣說,那也只能接受:

  • 非同步的 latency 比同步的還要高
  • 同步架構在吞吐量上不會差非同步太多
  • freemarker 超快
  • postgresql 的寫入逼近 cassandra (SSD)

Netty in Action 裡有提到:如果你的服務需要低的 latency ,那麼你應該選 Blocking IO。除非你的連線數大到某個量級,你才需要切換到 NIO,用 latency 去換 scalability。這裡的 某個量級 是千個連線數。

在這次的實驗中直接證實了這個論點。我想 ABC 三台伺服器的 Web A 未來還是會用 NIO,畢竟它是對外,會面對成千上萬的用戶連線。而 Tomcat 也能用 NIO,所以應該 ok。

因此,現階段的組合決定為 tomcat + spring mvc + postgresql + cassandra。資料庫變成兩者都用,各取所需。template engine 則還是選老牌子 freemarker,雖然我個人很討厭它的語法,但測試數據太好,無法捨棄。而非同步的架構以及 netty,這個組合在開發上比較複雜,然後效果又不顯注,就先放棄了。

測試結果總是讓人意外,沒測真的不知道

如果是平常的軟體需求,我用基本組合就打死了,不會想這麼多,反正 Java 本來就超快,而且就算拿來實驗一些新奇的工具組也是 ok 的。這次的需求比較特別,所以進行了一輪前導壓力測試。雖然我的工具箱有不少東西,可以讓我試很多組合,但這次的經驗讓我覺得在初期要選出好的架構和組合並不容易,很多時候你會無法分辨是原型軟體的問題,還是工具組本身的問題。除非有很完整的測驗和評估啦,但時間並不是無限的。完整的分析與最佳化要到開發最後階段再做才合算。

不過,花時間測試不同架構的組合也帶來一些 副作用:程式被迫要更加模組化,才能抽換組合進行測試。即使先行測試結束了,模組化的成果還是保留著,這算是意外的好處吧。這也意謂未來還是有機會抽換掉組合的。

後記:這篇文章還真難寫,一半是挺難整理的,另一半是測試的細節太多太雜。不過還是本著 blog (web log) 的精神將這段奮戰記錄下來。測試的環境和數據我故意草草帶過,因為它們對各位沒有絕對的參考價值,各自的軟體還是得自己測試取得數據才準。


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>