Urban Airship是如何在美國大選日支持了25億條通知的

guchengren 8年前發布 | 31K 次閱讀 數據庫 軟件架構 NOSQL

大家都相信Urban Airship在手機上會有非常多的商業機會。Urban Ship是一家已有七年歷史的老SaaS公司,她也提供了免費增值商業模式,所以你可以試試免費體驗一下。現在每天Urban Airship都平均要推送十億以上條通知消息。本文關注Urban Airship在2016美國大選期間對通知消息的使用,講述一下系統架構——核心推送管道——是如何為消息發布者發送了幾十億條實時通知的。

2016年美國總統大選

在選舉日的24個小時中,Urban Airship共推送了25億條通知消息,這是它有史以來的單日最高推送量了。這大概相當于為每個美國人推送8條通知消息,或者為這世界上每一個在網的手機號碼都推送一條消息。由于Urban Airship在各個行業里共支持了約4.5萬個手機應用,對選舉日數據的分析表明有400多個媒體應用發送了這個消息量的60%,即在跟進選舉過程和報告結果的過程中,一天就發送了15億條通知消息。

當大選結果塵埃落定之后,通知量急劇上升并達到了頂峰。

在大選過程中,通過HTTPS注入到Urban Airship API 的消息量達到了約每秒7.5萬條的最高記錄。其中大多數都來自于Urban Airship SDK ,它們與Urban Airship API通信。

推送消息量也在快速劇增。最近推送量大增的主要事因有英國退出歐盟事件、奧林匹克運動會和美國大選。2016年十月的單月推送量與去年相比增加了150%。

核心推送管道架構概覽

核心推送管道(Core Delivery Pipeline,CDP)是在Urban Airship系統中負責從接收者選擇器中物化設備地址并向其推送通知的系統。不管發送什么樣的通知消息,我們都要追求低延遲,不管是要同時發送給幾百萬個用戶,或者要發送到多個復雜的子系統中,或者包含個性化內容,或者任意上述的組合,等等。下面是對我們架構的一個概述,以及演進過程中我們總結的一些經驗。

我們是怎么開始的

最初在2009年起步時只是一個Webapp,幾個子模塊慢慢演進成了面向服務的架構。當舊系統的幾個部分開始遇上擴展難題時,我們從中提煉出了一個或幾個新服務用于支持同樣的功能,但規模更大并且性能更好。許多舊的API和子模塊都是用Python寫的,我們把它們換成了高并發的Java服務。最早時我們把設備數據保存在一些Postgres分區中,可是我們增加新分區的速度始終跟不上我們的業務規模擴展速度,因此我們就移到了一個基于HBase和Cassandra的混合數據庫架構上來。

核心推送管道是一系列處理分片和推送通知消息的服務的集合。這些服務會為請求消息提供相同數據類型的回復消息,但每個服務的數據都是為了最佳性能而按不同方式索引的。比如,有個系統是負責處理廣播消息的,就是把相同的通知消息發送給注冊到相關應用上的所有手機。還有的服務是負責基于位置或其它用戶個人屬性而發送通知消息的,這些服務以及它們底層的數據存儲的設計彼此不同。

我們把所有持續長期運行的進程都叫做服務。為了方便部署和運營,在指標、配置和記錄日志等方面這些服務都使用了相同的模板。通常我們的服務都會屬于下面兩組之一:RPC服務或消費者服務。RPC服務對外提供一些命令,來與一些使用與GRPC很相近的內部庫的服務交互。而消費者服務會則會從Kafka流中讀出消息進行處理,并且依具體服務的不同而對這些消息進行處理。

(點擊放大圖像)

數據庫

為了滿足對性能和規模的需求,我們使用了非常大量的HBase和Cassandra來做數據存儲。盡管HBase和Cassandra都是列存儲型NoSQL數據庫,它們在細節的側重上還是各有不同,因此我們也會根據具體項目的需求選擇和使用不同的數據庫。

HBase非常適合用來做大吞吐量檢索,期望的響應消息的勢(cardinality)比較高,而Cassandra比較適合于低勢檢索,響應消息里應該只包含少量的數據結果。它們兩個都可以進行大數據量的寫入,這恰好是我們的需求之一,因為所有對用戶的手機號碼的更新操作都必須實時完成。

它們的缺點也互不相同。HBase在出現故障時支持一致性和分區容忍性,而Cassandra支持可用性和分區容忍性。每一種CDP服務都有非常獨特的用例,因此也都有著非常專門設計的模式來方便進行數據訪問,以及限制存儲空間。作為一種通用的規則,每個數據庫都只被一種單獨的服務訪問,并且通過一種比較通用的接口來為其它服務提供數據庫訪問。

強化這種一對一的服務和后臺數據庫之間的關系有一系列的好處。

  • 通過把服務的后臺數據存儲當作一種實現細節來處理,而不是一種共享的資源,我們就獲得了靈活性。
  • 我們可以改變一個服務的數據模型,只需要修改一個服務的代碼就可以。
  • 利用率跟蹤也是非常直白的,這就讓存儲空間計劃更容易做。
  • 調查問題很容易。有時候問題在于服務的代碼,有時候是后臺數據庫的問題。把服務和數據庫一起作為一個邏輯單元極大地簡化了調查問題的過程。我們不必再費心去想“還有誰也在訪問這個數據庫,才讓它成了現在這個樣子?”相反,我們只依賴這個服務相關的應用級別的指標就可以了,只需要考慮一種訪問模式。
  • 因為只有一個服務會訪問一個數據庫,所以我們幾乎不必付出任何停服時間就可以完成絕大部分的維護操作了。大型維護任務成了一些服務級別的考慮:數據修復、模式更改以及甚至遷移到一種完成不同的數據庫上都可以不需要中斷服務而完成。

的確,在把應用程序拆分成更小的服務時會帶來一些性能上的損耗。可是,從我們在完成高擴展性和高可用性需求的方面獲得的靈活性上來看,這樣做是完全值得的。

數據模型

我們大多數的服務都處理的是相同的數據,只是格式不同而已。所有數據都必須是一致的。要保持所有這些服務的數據都是最新的,我們主要是要靠Kafka。Kafka速度快,可靠性高。速度快也帶來了一些折衷。Kafka的消息只能保證是至少發送一次的可靠級別,而且并不保證會按順序到達。

我們怎么應對這種情況呢?我們設計讓所有的可能路徑都是可替換的:數據更改操作可以按任意順序應用,而最終都會保證相同的結果。它們這樣也是冪等的。這樣就帶來了一個非常好的特性,我們可以把Kafka流重放,用作一次性的數據修復任務、回填甚至遷移。

為實現這樣的功能我們利用了HBase和Cassandra共有的概念“Cell版本”。一般來說這是一個時間戳,但也可以是任意你喜歡的數字(有一些例外情況,比如,依你的HBase或Cassandra的版本不同以及你的模式處理刪除操作的方式不同,MAX_LONG有可能會導致一些奇怪的行為)。

對我們來說,對這些Cell的一般規則就是它們可以有多個版本,而且我們對版本進行排序的依據就是它們提供的時間戳。有了這個意識,我們就可以把輸入的消息拆分,存入一些具體的列中,也可以依據時間戳根據一些應用邏輯來把一些數據淘汰。這樣就可以在保持數據完整性的同時,允許對底層存儲的隨意寫入。

就這樣隨意寫入Cassandra和HBase也并不是沒有問題的。一個很典型的例子就是在重放時對相同數據的重復寫入。因為操作是有冪等性的,那寫入的數據就事實上不該改變,因此重復的數據就應該合并掉。在一些最極端的例子中,這些多余的記錄可能會導致極嚴重的合并延遲和備份問題。考慮到這個細節,我們會密切地監控合并時間以及隊列長度,因為由合并而導致延遲在Cassandra和HBase中都會導致非常嚴重的問題。

在保證流中的消息都遵守若干規則的前提下,以及設計消費者服務來處理亂序和重復的消息,我們可以保證許多獨立的服務數據同步延遲只有一兩秒。

服務設計

我們大多數服務的代碼都是用Java寫的,但具體風格卻五花八門。我們制定了一系列的通用指導規則,供大家在設計Java服務時參考:

只做一件事,把它做好——在設計一個服務的時候,它應該只承擔一個責任。可以由實現者來決定在這個責任里面加進些什么東西,但在代碼審查會議上他必須能夠說服大家這樣做是合理的。

沒有共享的操作狀態——在設計一個服務的時候,要假設至少這個服務有三個實例在運行。不需要任何外部的協調,這個服務的一個實例可以處理某個請求,那另一個實例必須也可以。那些熟悉Kafka的人會想起來Kafka消費者會在外部協調一個Topic:Group對的分區對應關系。這個規則提的是一些特定服務相關的外部協調機制,而不是指利用庫或客戶端來在外部隱式地協調。

要給隊列加限制——我們在各種服務中普遍使用了隊列,用于緩沖請求和將請求分散給多個處理進程以避免處理過程會阻塞對外部的響應,在這方面隊列是非常有效的。但所有的隊列都必須加以限制。的確,給隊列加限制會引發若干問題,可是:

  • 當隊列滿時生產者又會怎樣?它們該堵塞嗎?還是丟棄消息?
  • 隊列該有多大?要回答這個問題,可以試著假設隊列常常是滿的。
  • 我怎樣才能干干凈凈地關閉服務?
  • 每個服務都會對這些問題有自己的答案,因為它們的用例不同。

生成定制的線程池并注冊UncaughtExceptionHandler——如果最終我們需要自己創建線程池,我們會使用 Executors 提供的構造函數或幫助方法來提供一個ThreadFactory。有了這個ThreadFactory,我們就可以恰當地命名自己的線程,設置它們的守候運行狀態,并且注冊 UncaughtExceptionHandler 以便在意外發生時可以進行處理。這些都讓調試服務的過程變得更容易,也讓我們避免了很多次熬夜的經歷。

與可變的狀態相比,我們更喜歡不變的數據對象——在一個高并發的環境中,可變狀態是非常危險的。通常我們會把不可變的數據對象在內部子系統和隊列之間傳來傳去。使用不可變的數據對象成了在子系統之間通信的主要形式,這讓并發的設計變得更直白,也讓調查問題變得更容易。

下一步該往哪里去?

現在Urban Airship已經可以通過手機錢包發送消息,已經支持網頁通知和Apple新聞通知,也有開放式渠道可以把消息發往任意平臺、設備或市場渠道,我們期望消息的發送量可以獲得指數級地增長。要滿足這個需求,我們還在繼續向我們的核心推送管道架構、服務、數據庫和基礎設施加大投入。

來自:http://www.infoq.com/cn/articles/how-urban-airship-support-2-billion-500-million-notice

 

 本文由用戶 guchengren 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!