Shopify如何處理大促?

對於電商來說,大促是很重要的,無論是中文圈的1111或外語區的黑色星期五,這些大促事件基本上可以為店家帶來鉅額收益,甚至一天就佔一年GMV的兩成以上。

當然,大促期間也會伴隨巨大的流量,並為電商網站帶來嚴重挑戰。因此,這篇文章我會就我看到的資訊來介紹Shopify如何應對大促事件。

在開始談論技術細節前,我們先來看一些Shopify在2022黑色星期五的數據

  • 在每分鐘3 TB的egress流量下維持99.999+%的uptime。
  • MySQL的尖峰QPS超過14M,平均也超過8.5M。
  • 尖峰時刻每秒索引16G的log。
  • 為了監控擴充性和可用性,每分鐘收集20B以上的運維指標,並且每秒儲存27G的指標資料。
  • 全站QPS為1.27M。
  • 透過Resque處理超過32B個非同步任務。
  • 傳送超過24B個webhook。
  • 批次的資料管線總共寫入1.1T個訊息。
  • 尖峰時刻Kafka每秒處理20M個訊息。

很驚人的數據。

接下來讓我們看看,他們能做到這種成績的其中一個機制:分片平衡(Shard Balancing)

Shopify Shard Balancing

Shopify是一個提供網店服務的SaaS公司,商家可以在Shopify輕易的建立一個屬於自己的網店。而Shopify為了應對來自各個商家的流量,因此將商家分群並為各群建立一個獨立的環境,稱為Pod(與K8s的Pod不同)。

在理解Shopify的分片平衡前,首先要先知道Shopify的分片組成。

根據Shopify在2018釋出來的技術文件,我們可以知道他們將網店所需要用到的基礎架構都放進一個Pod裡,包含Cron、Redis、Memcache和MySQL。

當店家存取自己的網店時,透過NGINX將流量轉移至正確的Pod。

當所有店家流量都差不多時,這樣的策略可以有效的攤提全站流量,並為每個店家提供差不多的使用體驗,但若是同一個Pod內的某間店擁有超高流量,那就會產生Noisy Neighbor

因此Shopify會監控每個Pod的用量,並且根據分片策略進行再平衡(Rebalancing)。

但這有兩個問題要解決:

  1. 哪間店應該被放到哪個Pod?
  2. 如何在盡可能沒有downtime的情況下搬遷?

哪間店應該被放到哪個Pod?

單純用Pod內的網店數是不夠的,因為網店的流量不是一致的。Shopify會參考許多量測指標來擬定假設。

  • 用量
  • 店家規模
  • GMV
  • 上次的移動時間
  • 限時搶購
  • …等

我們可以知道,要搬遷網店不是件簡單的事,因此需要考量的因素很多,Shopify也沒有透漏他們的營業機密。唯一可以知道的是,衡量完眾多指標後,Shopify還會跑預測模型,確定有效之後才會真的定案。

如何在盡可能沒有downtime的情況下搬遷?

當決定要搬遷網店,以下幾點必須要確保:

  • 可用性:店家絕對不希望看到後端的再平衡導致網店前台無法使用。
  • 資料一致性:搬遷過程中,絕不可資料遺失毀損,並且搬遷過程中的操作不能阻塞也不能失效。
  • 吞吐量:即便是搬遷過程依然要保持合理的效能,也不能對底層造成不必要的壓力。

為了更好解釋搬遷過程,用一個實際例子比較容易理解。

我們希望把XIAOLI’s XYLOPHONES從Pod 1遷移至Pod 2。

整個搬遷過程會經過三個步驟。

  1. 拷貝MySQL。
  2. 原Pod斷流
  3. 更新路由、重新服務、清除過期數據

步驟1:拷貝MySQL

要執行MySQL的拷貝,Shopify使用一個自研的開源工具:Ghostferry

拷貝MySQL分成兩個動作,批次拷貝和拼接binlog。

首先是批次拷貝,目的是為了快速讓目標資料庫能夠跟上源資料庫。Ghostferry可以提供跨資料庫的交易(transaction),其中,為了避免在拷貝的過程中源資料又被修改導致資料遺失或毀損,Ghostferry會使用MySQL的互斥鎖(SELECT ... FOR UPDATE)來阻擋更新。

同時,拷貝的過程Ghostferry也會追蹤源資料庫的binlog並在目標資料庫重放,以確保不會有任何更新被遺漏。

為了提昇效能,Ghostferry可以平行執行以便一次拷貝數張表,並且Ghostferry是背景執行因此不會影響網店的線上服務。

步驟2:原Pod斷流

當批次拷貝結束,依然會持續複製binlog,使目標資料庫與源資料庫可以同步。

當Ghostferry確定兩邊數據已經足夠接近,切換不會導致太大延遲的時候,就會讓源資料庫不再接受新的請求同時啟動斷流,這個網店的請求以及非同步任務無法再寫資料進源資料庫。

會有這樣的限制是因為要完全避免資料遺失,所以必須得讓所有的寫入暫停下來。作法是在應用程式端設計一個讀寫鎖,只要寫鎖沒有上,讀鎖就可以無限拿,一旦寫鎖上了,讀鎖就會要不到。而寫鎖必須要等所有讀鎖都結束才能上。

每個請求和非同步任務要執行時都會去要一個讀鎖,而遷移這個動作會去要寫鎖;只要拿到寫鎖,那所有請求和非同步任務都會暫停。若是時限內要不到寫鎖,那該次遷移就宣告失敗。

步驟3:更新路由、重新服務、清除過期數據

當寫鎖要到,那麼已經可以確認所有請求都暫停了,而目的端資料庫也很快就會完全同步完成,那就可以更新路由表,讓網店的流量轉移至新的Pod。

路由表更新完成後就可以把寫鎖釋放,所有的功能都會回歸正常,只是是作用在新Pod。整個遷移過程只有極短的downtime會發生在這期間。

在這個時間點,舊資料依然會儲存在源資料庫上,Shopify會啟動遷移的驗證流程,確保遷移後的網店功能正常。那麼,就可以把這些舊資料刪除。

結論

Shopify要能承受大促的極大流量,必須要兼顧擴充性以及彈性(Elasticity),除了分片平衡之外還有許多工程解決方案,之後會再介紹其他的方案。

如果不知道擴充性以及彈性怎麼區分,我建議參閱我之前寫過的文章。

Shopify的工程部落格我覺得是一個很好的學習資源,一方面是因為電商這個領域本身涵蓋的議題就包山包海,另一方面,Shopify很認真在經營自己的品牌,因此從這些技術文章中都可以看出Shopify的技術深度。

未來我還會再從中挑選一些與我專業有關的技術文章出來分享。