電商專案—商品秒殺場景實作
我有新的部落格了,歡迎來逛逛 https://blog.jhdev.pro/
前言
一轉眼來到了 ALPHA Camp A+ 計畫的第六週,也是個人專案準備收尾的時刻(DEMO 相關請到文章最下方的 GitHub )。個人專案之所以選擇做電商網站,是因為內容已經涵蓋業界常見基本需求,例如:會員系統、session 機制、商品資料結構設計、金流串接等等。在 ALPHA Camp 學期三經過兩個月的密集訓練後,後端 CRUD 的熟悉度其實已經有一定水準。在思考主題時,剛好知名健身網紅 — — 館長的電商網站因為粉絲搶購商品造成網站掛掉,當時網路上也引起一波熱烈討論,其中也不乏資深軟體技術人員提出各種解法與觀點,因此希望能夠透過個人專案,接觸更多後端架構面的技術。
上圖是這個專案的時間線,本文會著重在介紹最後階段後端架構的實作,前面基本的電商網站架構、購買流程及金流串接就不贅述了。
商品秒殺場景會遇到的主要問題
商品秒殺場景可以想像成 Apple 發表新款 iPhone 之後,官網在開賣的那一瞬間,會湧入大量使用者,也就是官網在那一瞬間會收到大量的請求(concurrent requests),而官網要能夠處理這些請求,維持原有的服務讓使用者能順利購物。
上圖為這個專案的後端架構圖,為的是要探討並嘗試解決下列問題:
- 如何讓 server 能夠承受大量請求?
- 如何優化 response 的時間與品質?
- 如何處理 race condition 確保資料庫寫入的資料是正確的?
解決問題的方法
MySQL 調校
如何處理 race condition 確保資料庫寫入的資料是正確的?
- 使用 transaction 處理 race condition,isolationLevel 設為最嚴格的 SERIALIZABLE,以確保資料正確性
- 設定 connection pool 提高資料庫連線效率。
redis 快取 & 購買流程優化
- 伺服器啟動時 redis 會存取 MySQL 的產品資訊作為快取。
- 新增獨立的產品頁面,讓使用者在此頁面搶購,只需讀取一個產品,可降低伺服器及資料庫的負載。
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 相比:
- user 數量增加了 30 倍 (5 -> 150)
- API 錯誤率為 0.09%
- Apdex 僅下降 3% (0.72 -> 0.698)
因此這個專案希望解決的三個問題有了解答!
如何讓 server 能夠承受大量請求?
如何優化 response 的時間與品質?
如何處理 race condition 確保資料庫寫入的資料是正確的?
在現有架構下,若要面對更多的 user,例如 case 5 的 300 個 user,可以使用的解法是:
- 訊息佇列 RabbitMQ / Kafka
- 增加機器數量 (AWS auto-scaling)
- 限制進入網站的使用者數量 (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 了。
很高興透過學習新的技術,最後真的解決了現實生活中會發生的問題,這就是學習程式最有趣的地方,當然這個專案還有很多可以改善的地方,相信隨著自己技術層次逐漸提升,再次回頭看這些問題時,會有更多不同的想法。
秒殺場景優化方向
- 訊息佇列 RabbitMQ / Kafka
- 增加 server 數量 & NGINX 負載平衡
- Linux 在高併發下有可能受限於 kernel, TCP 參數
- 將更多資料庫操作放到 redis
- express 讀寫分離
- 嘗試使用 NoSQL
開發及測試優化方向
- CI / CD 持續整合與自動化佈署,才能更有效率地執行測試
- 更嚴謹的測試手法,從壓力(Stress)、負載(Load)、效能(Performance) 三個面向切入
- JMeter Distributed Testing
最後得出 JMeter 測試 300 個 user 以上就會出現錯誤 java.lang.OutOfMemoryError: Java heap space,這大概是 test server (我的電腦)的極限了,如果要同時發出更多請求數量,要再研究有沒有其它設定可以調整,或是考慮使用 Distributed Testing 的架構,以多台 test server 同時發送請求來進行測試。