前言

一轉眼來到了 ALPHA Camp A+ 計畫的第六週,也是個人專案準備收尾的時刻(DEMO 相關請到文章最下方的 GitHub )。個人專案之所以選擇做電商網站,是因為內容已經涵蓋業界常見基本需求,例如:會員系統、session 機制、商品資料結構設計、金流串接等等。在 ALPHA Camp 學期三經過兩個月的密集訓練後,後端 CRUD 的熟悉度其實已經有一定水準。在思考主題時,剛好知名健身網紅 — — 館長的電商網站因為粉絲搶購商品造成網站掛掉,當時網路上也引起一波熱烈討論,其中也不乏資深軟體技術人員提出各種解法與觀點,因此希望能夠透過個人專案,接觸更多後端架構面的技術。

上圖是這個專案的時間線,本文會著重在介紹最後階段後端架構的實作,前面基本的電商網站架構、購買流程及金流串接就不贅述了。

商品秒殺場景會遇到的主要問題

商品秒殺場景可以想像成 Apple 發表新款 iPhone 之後,官網在開賣的那一瞬間,會湧入大量使用者,也就是官網在那一瞬間會收到大量的請求(concurrent requests),而官網要能夠處理這些請求,維持原有的服務讓使用者能順利購物。

上圖為這個專案的後端架構圖,為的是要探討並嘗試解決下列問題:

  1. 如何讓 server 能夠承受大量請求?
  2. 如何優化 response 的時間與品質?
  3. 如何處理 race condition 確保資料庫寫入的資料是正確的?

解決問題的方法

MySQL 調校

如何處理 race condition 確保資料庫寫入的資料是正確的?

  1. 使用 transaction 處理 race condition,isolationLevel 設為最嚴格的 SERIALIZABLE,以確保資料正確性
  2. 設定 connection pool 提高資料庫連線效率。

redis 快取 & 購買流程優化

  1. 伺服器啟動時 redis 會存取 MySQL 的產品資訊作為快取。
  2. 新增獨立的產品頁面,讓使用者在此頁面搶購,只需讀取一個產品,可降低伺服器及資料庫的負載。

NGINX 調校

調整 NGINX 相關設定,讓 Server 可以承受更高的請求數量,並提高 I/O 效率。

測試指標

資料正確性

商品庫存、訂單數量、訂單付款狀態資料無異常,商品不會超賣,也不會沒賣完。下圖是我新增一支 API 可以顯示結果。

API 錯誤率(%)

JMeter 送出請求後得到錯誤回應的百分比。

Apdex

Apdex (Application Performance Index) 是一個評估應用程式效能的標準,將響應時間的表現,轉為使用者對於效能的滿意度評價,並量化為 0 ~ 1 之間的數字。

服務優化過程

以上 case 都是在 1 秒內產生 user(ramp-up time),1 個 user 會對伺服器發出 10 個請求來完成 1 次完整的購買流程(loop count),以模擬大量使用者「同時」發出請求。以下是服務優化的三個階段:

階段一:確保資料正確性

case1: 在基本架構下,同時間有 5 個 user 操作,雖然 API 錯誤率是 0 %,但是資料庫正確性未通過,可見同時寫入資料時已經發生資料庫資源排擠的現象。

case 2:
user 數量增加了 10 到 50 個,為了解決資料庫同時寫入資料的問題,將寫入資料的部分加上 transaction,確保每一筆資料在寫入時不會彼此影響。
👉 資料庫正確性通過,錯誤率 0%,Apdex 為 0.72。

階段二:增加 user 數量並維持伺服器及資料庫效能

case 3: user 數量增加 3 倍到 150 個,需要多設定 connection pool,才不會跳出 Too many connections;NGINX 調整設定,以維持伺服器響應速度。
👉 資料庫正確性通過,API 錯誤率從 0 % 躍升至 1.14 %,Apdex 降低至 0.691。

case 4: user 數量維持 150 個,將 connection pool 的 min 調整到上限值,讓資料庫準備好連線數量,請求進來時可以直接連線。
👉 資料庫正確性通過,API 錯誤率從 1.14% 降低至 0.09 %,Apdex 微升至 0.698。

階段三:服務崩潰

case 5: user 數量提升到 300 個,測試開始執行便不斷跳出 Too many connections。
👉 資料庫正確性未通過,API 錯誤率從 0.09 % 躍升至 2.04 %,Apdex 降低至 0.652。

總結

case 4 為現有架構下的極限,與一開始的 case 1 相比:

  1. user 數量增加了 30 倍 (5 -> 150)
  2. API 錯誤率為 0.09%
  3. Apdex 僅下降 3% (0.72 -> 0.698)

因此這個專案希望解決的三個問題有了解答!

如何讓 server 能夠承受大量請求?

如何優化 response 的時間與品質?

如何處理 race condition 確保資料庫寫入的資料是正確的?

在現有架構下,若要面對更多的 user,例如 case 5 的 300 個 user,可以使用的解法是:

  1. 訊息佇列 RabbitMQ / Kafka
  2. 增加機器數量 (AWS auto-scaling)
  3. 限制進入網站的使用者數量 (e.g. QUEUE IT)

最後

心得

一開始的 Web App 基本程式碼沒寫 transaction,跑 5 個 user 就會出問題了,加上 transaction 之後可以確保資料正確,再加上 connection pool 之後可以跑小規模的 user 數量,這時候才算是真正開始測試。

在本地測試時 MySQL connection pool 可以隨便我開,但是到 AWS 測試時,免費版 t2.micro 的 max_connection 是 65,user 多一點就會報 Too many connections 了,這是最初沒有預想到的。

建立測試環境、規劃執行測試和單純寫 code 的感覺很不同,需要嘗試或摸索的時間更長、次數更多,過程中常會不小心搞混本地跟遠端,藉由將專案部署 EC2 也大致學會操作 Ubuntu Linux 了。

很高興透過學習新的技術,最後真的解決了現實生活中會發生的問題,這就是學習程式最有趣的地方,當然這個專案還有很多可以改善的地方,相信隨著自己技術層次逐漸提升,再次回頭看這些問題時,會有更多不同的想法。

秒殺場景優化方向

  1. 訊息佇列 RabbitMQ / Kafka
  2. 增加 server 數量 & NGINX 負載平衡
  3. Linux 在高併發下有可能受限於 kernel, TCP 參數
  4. 將更多資料庫操作放到 redis
  5. express 讀寫分離
  6. 嘗試使用 NoSQL

開發及測試優化方向

  1. CI / CD 持續整合與自動化佈署,才能更有效率地執行測試
  2. 更嚴謹的測試手法,從壓力(Stress)、負載(Load)、效能(Performance) 三個面向切入
  3. JMeter Distributed Testing
    最後得出 JMeter 測試 300 個 user 以上就會出現錯誤 java.lang.OutOfMemoryError: Java heap space,這大概是 test server (我的電腦)的極限了,如果要同時發出更多請求數量,要再研究有沒有其它設定可以調整,或是考慮使用 Distributed Testing 的架構,以多台 test server 同時發送請求來進行測試。

--

--