Shopify如何使用Ruby實現每小時銷售1億美元?

banq發表於2021-08-27

在 2021 年網路黑色星期五 (BFCM) 期間,Shopify 商家的銷售額超過 50 億美元,峰值銷售額超過每小時 1 億美元。在如此大規模的情況下,高可用性和快速響應時間至關重要。但即使對於較小的應用程式,可用性和響應時間對於出色的使用者體驗也很重要。

高可用性通常與高伺服器正常執行時間混為一談。但是,伺服器沒有崩潰或關閉是不夠的。在 Shopify 的情況下,我們的商家需要能夠進行銷售。因此,買方需要能夠與應用程式進行互動。一條訊息說“稍後回覆結果”是不夠的,一次只服務一個買家也不夠好。要考慮可用的應用程式,使用者社群需要與應用程式進行有意義的互動。如果在使用者需要時可以進行這些互動,則可以認為可用性很高。
 

遷移工作量非同步處理
為了可用,應用程式需要能夠接受傳入的請求。如果應用程式的面向外部的部分(應用程式伺服器)也在執行處理請求所需的繁重工作,它會很快變得不堪重負,無法接收新的傳入請求。為了避免這種情況,我們可以將一些繁重的工作解除安裝到系統的不同部分,將其移到主請求響應週期之外,以免影響應用伺服器接受和服務傳入請求的可用性。這也縮短了響應時間,提供了更好的使用者體驗。
常見的解除安裝任務包括:

  • 傳送電子郵件
  • 處理影像和影片
  • 觸發 webhook 或發出第三方請求
  • 重建搜尋索引
  • 匯入大資料集
  • 清理陳舊資料

如果任務很慢、消耗大量資源或容易出錯,那麼解除安裝任務的好處就特別大。
例如,當新使用者註冊 Web 應用程式時,該應用程式通常會建立一個新帳戶並向他們傳送歡迎電子郵件。帳戶可用不需要傳送電子郵件,而且使用者也不會立即收到電子郵件。所以在請求響應週期內傳送電子郵件是沒有意義的。使用者不必等待傳送電子郵件,他們應該能夠立即開始使用應用程式,並且應用程式伺服器不應該承擔傳送電子郵件的任務。
在向呼叫者提供響應之前不需要完成的任何任務都是解除安裝的候選物件。將影像上傳到 Web 應用程式時,需要處理影像檔案,應用程式可能希望建立不同大小的縮圖。使用者通常不需要成功的影像處理,因此通常可以解除安裝此任務。但是,伺服器無法再響應說“影像已成功處理”或“發生影像處理錯誤”。現在,它只要回應“影像已成功上傳“即可,如果一切按計劃進行,圖片將在稍後出現在網站上。 考慮到影像處理非常耗時的性質,考慮到響應時間的大幅改進和它提供的可用性優勢,這種權衡通常是值得的。
 

後臺工作
後臺作業是一種解除安裝工作的方法。後臺作業是稍後要在請求響應週期之外完成的任務。應用程式伺服器將任務(例如影像處理)委託給工作程式,該程式甚至可能執行在完全不同的機器上。應用程式伺服器需要將任務傳達給工作者worker。工作人員可能很忙,無法立即接受任務,但應用程式伺服器不應該等待工作者的響應。在應用程式伺服器和工作者之間放置一個訊息佇列解決了這個難題,使它們的通訊非同步。訊息的傳送者和接收者可以在不同的時間點獨立地與佇列互動。應用伺服器將訊息放入佇列並繼續前進,立即可以接受更多傳入請求。訊息是工作者要完成的任務,這就是為什麼這樣的訊息佇列常被稱為任務佇列。工作者可以以自己的速度處理來自佇列的訊息。
後臺作業後端本質上是一些任務佇列以及一些用於管理工作者worker的代理程式碼。
Shopify 每秒對數以萬計的作業進行排隊以利用各種功能。下面是優點:

  • 響應時間

使用後臺作業允許我們將面向外部的請求(由應用程式伺服器提供)與任何耗時的後端任務(由工作人員執行)分離。從而提高響應時間。提高單個請求的響應時間也提高了系統的整體可用性。
  • 尖峰能力

如果耗時的影像處理是由後臺作業完成的,那麼突然激增的影像上傳不會造成傷害。應用程式伺服器的可用性和響應時間受到它排隊影像處理作業的速度的限制。但是排隊更多作業的速度不受處理它們的速度的限制。如果工作人員無法跟上增加的影像處理任務量,佇列就會增長。但是佇列充當了工作者和應用程式伺服器之間的緩衝區,以便使用者可以像往常一樣繼續上傳影像。隨著 Shopify 面臨每秒高達 17 萬個請求的流量高峰,儘管流量不可預測,但後臺作業對於保持高可用性至關重要。
  • 重試和冗餘

當工作者在執行作業時遇到錯誤時,作業將重新排隊並稍後重試。由於所有這些都發生在後面,因此不會影響面向外部的應用程式伺服器的可用性或響應時間。它使後臺作業非常適合容易出錯的任務,例如向不可靠的第三方發出請求。
  • 並行化

多個工作者可能會處理來自同一佇列的訊息,從而允許同時處理多個任務。這是分配工作量。我們還可以將一個大任務拆分為幾個較小的任務,並將它們作為單獨的後臺作業進行排隊,以便同時處理其中的幾個子任務。
  • 優先排序

大多數後臺作業後端允許對作業進行優先順序排序。他們可能會使用不遵循先進先出方法的優先順序佇列,這樣高優先順序的作業最終就會被切斷。或者他們為不同優先順序的作業設定單獨的佇列,並配置工作者從較高優先順序佇列中優先處理作業。沒有工作者需要完全專注於高優先順序工作,因此只要佇列中沒有高優先順序工作,工作者就會處理較低優先順序的工作。這是足智多謀的,顯著減少了工作者的空閒時間。
  • 基於事件和基於時間的排程

後臺作業並不總是由應用程式伺服器排隊。處理作業的工作人員也可以將另一個作業排隊。雖然他們根據使用者互動或一些變異資料等事件對作業進行排隊,但排程程式可能會根據時間對作業進行排隊(例如,每日備份)。
  1. 程式碼簡單

後臺作業後端封裝了請求作業的客戶端和執行作業的工作者之間的非同步通訊。將這種複雜性置於單獨的抽象層中,可以使具體的作業類保持簡單。具體的作業類只實現要完成的任務(例如,傳送歡迎電子郵件或處理影像)。它不知道將來會執行,在幾個工作程式之一上執行,或者在錯誤後重試。
 

挑戰
非同步通訊帶來了一些挑戰,這些挑戰不會因為封裝了它的一些複雜性而消失。後臺工作沒有什麼不同。

  • 作業引數的重大更改

排隊作業的客戶端和處理它的工作者並不總是執行相同的軟體版本。其中之一可能已經部署了較新的版本。這種情況可能會持續很長時間,尤其是在練習金絲雀部署時。如果作業已使用一組特定引數排入佇列,則作業引數的更改可能會破壞應用程式,但處理作業的工作者需要不同的引數集。對作業引數的重大更改需要透過保持向後相容性的一系列更改進行,直到佇列中的所有舊作業都已處理完畢。
  • 無法實現恰好一次交付

當工作人員完成一項工作時,它會返回報告說現在可以安全地從佇列中刪除該工作。但是如果處理工作的工人保持沉默怎麼辦?我們可以讓其他工作者接手這樣的工作並執行它。這確保即使第一個工作人員崩潰,作業也能執行。但是如果第一個工人只是比預期慢一點,我們的工作就會執行兩次。另一方面,如果我們不允許其他工作人員接手工作,那麼如果第一個工作人員崩潰,工作將根本無法執行。所以我們必須決定什麼更糟:根本不執行作業,或者執行它兩次。換句話說,我們必須在至少和最多一次交付之間進行選擇。
例如,不向客戶收費並不理想,但對某些企業而言,向他們收取兩次費用可能更糟。在這種情況下,最多一次交付聽起來是對的。但是,如果仔細跟蹤每次收費並且作業在嘗試收費之前檢查這些狀態,則第二次執行該作業不會導致第二次充費。這項工作是冪等的,允許我們安全地選擇至少一次交付。
  • 非事務性排隊

作業佇列通常位於單獨的資料儲存中。Redis 是佇列的常見選擇,而許多 Web 應用程式將其運算元據儲存在 MySQL 或 PostgreSQL 中。當用於寫入運算元據的事務開啟時,排隊作業將不是此封閉事務的一部分 - 將作業寫入 Redis 不是 MySQL 或 PostgreSQL 事務的一部分。作業會立即排隊,並且可能會在封閉事務提交(或者即使它回滾)之前被處理。
當接受來自使用者互動的外部輸入時,通常會以最少的處理寫入一些運算元據,並將執行額外步驟處理該資料的作業排入佇列。除非我們在提交寫入運算元據的事務後將其排隊,否則此作業可能找不到它需要的資料。但是,系統可能會在提交事務之後和排隊作業之前崩潰。作業永遠不會執行,不會執行處理資料的附加步驟,使系統處於不一致的狀態。
發件箱模式 可用於建立事務性暫存作業佇列。不是立即將作業排隊,而是將作業引數寫入運算元據儲存中的臨時表中。這可以是寫入運算元據的資料庫事務的一部分。排程程式可以定期檢查暫存表,將作業排隊,並在作業成功排隊時更新暫存表。由於即使作業已排隊,對臨時表的此更新也可能失敗,因此作業至少排隊一次並且應該是冪等的。
根據作業量,事務性暫存作業佇列可能會給資料庫帶來相當大的負載。雖然這種方法可以保證作業的排隊,但不能保證它們會成功執行。
  • 本地事務

業務流程可能涉及來自為請求提供服務的應用程式伺服器和執行多個作業的工作者的資料庫寫入。這就產生了本地資料庫事務的問題。當最後一個本地事務提交時達到最終一致性。但是,如果其中一項作業未能提交其資料,則系統將再次處於不一致狀態。SAGA 模式可用於保證最終一致性。除了以事務方式對作業進行排隊之外,作業在成功時還會向臨時表報告。排程程式可以檢查此表並發現不一致之處。這會導致資料庫負載比單獨的事務暫存作業佇列更高。
  • 順序排隊到無序交付

作業以預定義的順序離開佇列,但它們最終可能會出現在不同的工作人員上,並且無法預測哪個工作完成得更快。如果作業失敗並重新排隊,它甚至會在稍後處理。因此,如果我們立即將多個作業排隊,它們可能會出現故障。如果臨時表也用於維護作業順序,則 SAGA 模式可以確保作業以正確的順序執行。
如果不考慮一致性保證,則可以使用更輕量級的替代方案。作業完成其任務後,它可以將另一個作業排隊作為後續作業。這可確保作業按預定義的順序執行。該方法快速且易於實現,因為它不需要臨時表或排程程式,並且不會對資料庫產生任何額外負載。但由此產生的系統可能會變得難以除錯和維護,因為它會將其所有複雜性推向下一個潛在的長作業鏈,將其他作業排隊,並且幾乎無法觀察到底哪裡出了問題。
  • 長時間執行的作業

作業不必像面向使用者的請求一樣快,但長時間執行的作業可能會導致問題。例如,流行的 ruby​​ 後臺作業後端Resque可防止工作程式在作業執行時被關閉。但是無法部署它,它也不是非常適合雲端計算,因為資源需要在很長一段時間內連續可用。
另一個流行的 ruby​​ 後臺作業後端,Sidekiq, 在開始關閉工作程式時中止並重新排隊作業。但是,下次作業執行時,它會從頭開始,因此它可能會在完成之前再次中止。如果部署發生得比作業完成的速度快,作業就沒有機會成功。Shopify的核心每天部署大約40次,這不是學術討論而是我們需要解決的實際問題。
幸運的是,許多長期執行的作業在本質上是相似的:它們迭代龐大的資料集。Shopify 開發並開源了 Ruby on Rails 的 Active Job 框架的擴充套件,使這種工作可中斷和可恢復。它在每次迭代後設定一個檢查點並重新排隊作業。下次處理作業時,檢查點將繼續工作,從而可以安全輕鬆地中斷作業。透過可中斷和可恢復的工作,工作人員可以隨時關閉,這使他們對雲更加友好,並允許頻繁部署。可以限制或停止作業以進行災難預防,例如,如果資料庫上有大量負載。中斷作業還允許在資料庫分片之間安全地移動資料。
 

分散式後臺作業
Ruby 中的ResqueSidekiq等後臺作業後端通常透過將序列化物件放入佇列(具體作業類的例項)來將作業排隊。這意味著排隊作業的客戶端和處理它的工作人員都需要能夠使用此物件並具有此類的實現。這在客戶端和工作人員執行相同程式碼庫的單體架構中非常有效。但是,如果我們想將影像處理提取到專用的影像處理微服務中,甚至可能是用不同的語言編寫的,我們需要一種不同的通訊方法。

  • 選擇Redis:可以將 Sidekiq 與單獨的服務一起使用,但工作人員仍然需要用 Ruby 編寫,並且客戶端必須為作業選擇正確的 redis 佇列。所以這種方式不太容易應用到大規模微服務架構中,而是避免了增加像RabbitMQ這樣的訊息代理的開銷。
  • RabbitMQ這樣的面向訊息的中介軟體在訊息的生產者和消費者之間放置了一個純粹基於資料的介面,例如 JSON 負載。訊息代理可以充當分散式後臺作業後端,客戶端可以在其中將工作解除安裝到執行完全不同程式碼庫的工作器上。利用任務佇列的主題新增了強大的路由,而不是簡單的點對點佇列。與 HTTP 相比,這種路由不限於 1:1。除了委派特定任務之外,只要需要微服務之間的通訊,就可以將訊息傳遞用於不同的事件訊息。在處理後刪除訊息後,就無法重放訊息流,也沒有系統範圍狀態的真實來源。
  • Kafka這樣的事件流有一種完全不同的方法:將事件寫入僅附加事件日誌中。所有消費者共享同一個日誌,可以隨時閱讀。經紀人本身是無狀態的;它不跟蹤事件消耗。事件被分組到主題中,這提供了一些釋出訂閱功能,可用於將工作解除安裝到不同的服務。這些主題不基於佇列,並且不會刪除事件。由於事件日誌可以重播,因此它可以用作,例如,事件溯源的真實來源。藉助無狀態代理和僅追加寫入,吞吐量非常高,非常適合實時應用程式和資料流。

後臺作業允許面向使用者的應用程式伺服器將任務委派給工作人員。由於工作量減少,應用伺服器可以更快地處理面向使用者的請求並保持更高的可用性,即使在面臨不可預測的流量高峰或處理耗時且容易出錯的後端任務時也是如此。後臺工作將客戶端和工作者之間非同步通訊的複雜性封裝到一個單獨的抽象層中,使得具體的程式碼保持簡單和可維護。
 
高可用性和快速響應時間對於提供出色的使用者體驗是必不可少的,無論應用程式的規模如何,後臺作業都成為不可或缺的工具。
本文作者Kerstin 是shopify一名高階開發人員,她將 Shopify 的大量 Rails 程式碼庫轉變為更加模組化的整體,這種重構是基於她之前在分散式微服務架構方面的經驗為基礎。

#Shopify

相關文章