日郵件發送量千萬級,談京東的EDM平臺優化之路

jjxg6932 8年前發布 | 12K 次閱讀 Redis 數據庫

EDM 是 Email Direct Marketing 的縮寫,即郵件營銷。它是利用電子郵件(Email)與受眾客戶進行商業交流的一種直銷方式,郵件營銷的對于企業的價值主要體現在三個方面:開拓新客戶、維護老客戶,以及品牌建設。

京東的EDM平臺每日發送量千萬級別,峰值數據2T(每日),如何能在峰值到來時保證系統的穩定,以及從大量的郵件發送任務中篩選出高優先級發送任務并及時發送是構建這個平臺的難點之一。

老平臺日益突出的問題

原有的老平臺已經是5年前的產物了,受限于當時研發人員對EDM的研發經驗和當時的技術棧,老平臺的幾個問題在日后的發展中越來越突出,慢慢的讓新架構變成了一件不可不做的事情。

老平臺存在的問題如下:

1、技術棧、實現思路混亂

受限于業務,老平臺分成了生產郵件和促銷郵件兩個子平臺。對于郵件任務的發送,老促銷郵件平臺引入了Thrift框架,而老生產郵件平臺則通過搶占式更新數據庫的方式實現。

渲染郵件模板時,老生產使用的是velocity,而老促銷已經改為由上層(郵件模板裝修系統)用handlebars提前渲染好后直接發送。

2、SQL Server的改造需求。

老平臺的數據庫使用的是SQL Server,從降低公司成本的角度出發,有盡快遷移到MySQL的需求。

3、歷史需求包袱很重。

很多業務已經下線,但是代碼還留在線上(包括各種關聯系統)。

新平臺:業務架構的確定

在這樣的情況下,我們決定拋棄老的平臺,構建新一代的EDM平臺。那其中第一步就是確認業務架構。

新平臺將由生產平臺、發送平臺和統一管理平臺組成。生產平臺負責生產郵件發送任務,發送平臺則部署在主流運營商的網絡上,冗余部署一定量的發送節點,保證發送成功率。因為郵箱服務提供商通常會對郵件的發送方按IP做流控限制,并且對各種網絡運營商投遞的郵件的接受程度也不一樣。

同時,為了保證在主流的網絡運營商渠道上都有發送節點,避免因某個網段或某個運營商網絡故障引起郵件發送成功率的波動。所以將發送和生產的邏輯區分開,有利于應用區分部署和擴容,平臺間的職責也更加清晰。

老平臺也是生產和發送分成兩個平臺,只不過職責沒有劃分得非常清晰,比如各自的平臺上都集成了管理功能,發送節點只能調節自己的發送策略,不利于發送策略的批量調整。

生產平臺和發送平臺間使用Redis隊列傳遞郵件任務。設計期間考慮過MQ,Kafka類組件,沒有使用的原因是以上組件對于傳遞郵件的任務都“過重”,引入新的組件意味著新的風險,新組件的穩定性也會影響平臺的穩定性。同時Redis對隊列有原生的支持,作為當前最常使用的組件,起簡單易用的特點正好符合新架構對這個組件的要求。再加上公司已經對Redis實現了集群及自動伸縮方案,可用率大大提高,學習零成本,都是Redis的優勢,也是這次架構升級選用Redis的理由。

建立統一管理平臺,實現對生成和發送情況的統一調度。要求是實現對整個生產和發送平臺的調度管理,包含指定高優節點,屏蔽成功率較低節點,降級開關等一系列措施。

經過多次梳理和方案推導后,整個EDM平臺的業務架構如下:

生產平臺對接用戶組,訂單組,會員等系統的MQ消息或遠程調用生成郵件發送任務,并按類型劃分1-10個優先級,數字越大,優先級越高。

Redis按優先級和目標郵箱兩個維度拆分成多個Redis隊列,負責將郵件任務傳遞到發送平臺。

發送平臺聚焦在發送業務的處理上,包括優先級隊列、發送降頻、郵件模板渲染等事務。

管理平臺主要負責配置數據的維護、降級開關的推送和必要時人工對生產、發送平臺的調度。

方案細節

1、Redis優先級隊列

首先,業務上決定了郵件任務的緊急程度是不一樣的,例如用戶對賬戶密碼找回的郵件就比優惠券到賬的及時性敏感度更高,顯然密碼找回的郵件需要以最快速度投遞到用戶郵箱中。這和某些在極端場景才出現的“優先級”不一樣,是一直持續存在的高優任務,最簡單的辦法就是區別對待,按優先級設置隊列,從生產平臺開始到Redis隊列再到發送平臺都一直是一個或多個特殊的隊列,方便系統對這些高優發送任務做處理。

同樣,高優先級隊列長度的報警閥值比較小,一旦積壓研發同學會第一時間收到報警,必要時可以人工接入。而發送平臺總是最先拉取高優先級發送任務,保證其第一時間被處理。值得注意的是,從客觀規律上看高優的郵件往往量是比較小,這使得發送平臺總是優先處理高優的郵件并不會讓優先級低的發送任務沒有機會被拉取。

當業務量較大,發送較為頻繁時觸發郵件服務商的流控,或網絡不穩定,出口IP異常時都會引起部分發送平臺的郵件投遞成功率的下降,這時需要讓成功率將低的節點暫時甚至永久的不再向郵件服務商投遞郵件,解決方案之一是在隊列的拆分維度除了優先級以外再增加一個目標郵箱,一旦出現上述問題后,可以直接讓發送節點不再拉取該郵箱的所有隊列來實現故障隔離。

同時,用優先級和目標郵箱拆分Redis隊列還有一個好處是,如果使用的是分布式的Redis,隊列的元素總是在一個分片中的,如果隊列過少,會導致有可能大量元素都集中在同一個分片中形成熱點分片。將Redis隊列拆分后可以讓分個分片的讀寫相對更均衡,分片的利用率更高。實際上,Redis隊列還設置了一個最大長度,防止隊列無限制的增長。

2、投遞降頻

投遞郵件時如果投遞被拒絕郵件服務提供商一般都會返回一個錯誤碼,發送平臺上有一個錯誤映射表,發送錯誤后將錯誤碼和錯誤映射表比較,如果觸發了流控則降低郵件發送任務的拉取頻率,直到投遞成功率恢復后再逐步提升發送能力。

3、Checker

Checker是生產平臺掃描郵件發送任務的定時任務的總稱,按職責不同,Checker被具體分為UnsuccessChcker、InQueueChecker、ExpireChecker等等。職責是將各個狀態的郵件發送任務更新為下一個流程需要的狀態,比如將入庫成功的狀態更新為Redis隊列中,Redis隊列中更新為發送平臺發送中,發送錯誤的任務更新為重新投遞等等。

重新投遞是非常重要的一個功能,因為某個出口IP因為各種原因可能會常常被郵件服務商拒收,重試相當于有較大幾率更換出口IP再做發送嘗試,有利于投遞成功率的提高。因為郵件發送任務常常被各種Checker更新,為了保證數據的一致性和狀態按正確流程流轉,郵件發送任務被加上了版本號,每次更新后自增,更新時使用樂觀鎖更新。

性能優化

初期平臺上線時的第一版架構如下:

上線后出現了一系列的性能問題,總結起來主要為兩類。

第一類問題:寫庫CPU 100%,影響遠程調用接口的性能,引發上游團隊關注。

引發寫庫CPU 100%的原因最主要的還是數據庫同一時間讀寫請求太多和索引利用率不高導致的。因此第一步想將數據庫的讀寫壓力分開,對數據庫做了讀寫分離,所有的讀請求全部調整到了從庫上去,以此降低主庫壓力。這里有一個前提是經過評估,從庫讀取臟數據并不會對業務產生困擾,因為郵件發送任務本身有版本號,即使數據庫主從同步有延遲引起從庫讀到“臟數據”使用樂觀鎖更新時也會失敗,不會引起業務錯誤。

第二步將查詢條件歸一再后建立索引,索引不是越多越好,歸一時可以現將查詢任務列出來,觀察哪些查詢條件是相似的,有沒有特殊的業務導致了不一樣的查詢條件,這些業務有沒有辦法從其他角度去支持,逐步歸納后再建索引。上述步驟多次使用后,通常情況必須要建立的索引就不會太多了。目前郵件發送任務表(分表后單表千萬級)只有一個主鍵索引和一個組合索引(三個字段),所有查詢條件全部先利用索引查詢數據。

調整完索引后發現Checker的掃描還是過于頻繁,讀庫CPU利用率還是不夠理想(過高),梳理業務后發現整個平臺的發送能力取決于郵件服務商的接受量和一定程度的發送平臺量(出口IP量)。兩者不變的情況下,整個發送平臺的發送量不會提升,Redis隊列的吞吐能力也不變,Checker大部分時候運行的結果只是Push了個別元素,甚至沒有Push。Checker完全可以改成Redis隊列小于一定閥值,例如最大長度的1/2再做一次掃描,一次掃描盡量將隊列填滿。調整Checker策略和索引后數據庫的QPS大約降了2/3,load也穩定在5以下。

還有一個讓數據庫CPU較高的原因是Checker引起數據庫死鎖。

例如有Transaction1需要對ABC記錄加鎖,已經對A,B記錄加了X鎖,此刻正嘗試對C記錄加鎖。同時此前Transaction2已經對C記錄加了獨占鎖,此刻需要對B記錄加X鎖,就會產生數據庫的死鎖。

盡管MySQL做了優化,比如增加超時時間:innodb_lock_wait_timeout,超時后會自動釋放,釋放的結果是Transaction1和Transaction2全部Rollback(死鎖問題并沒有解決,如果不幸,下次執行還會重現)。

如果每個Transaction都是Update數萬,數十萬的記錄(我們的業務就是),那事務的回滾代價就非常高,還會引起數據庫的性能波動。

解決辦法很多,比如先查詢出數據后再做逐條做寫操作,或者寫操作加上一個limit限制每次的更新次數,同時避免兩個Transaction并發執行等等。最終在調整了Checker的運行周期后選擇了逐條更新的方案,因為業務對于時間上要求并不高,更新不及時并不會引起業務上的錯誤。

經過以上優化后,壓測表明整個平臺3倍峰值流量下,數據庫CPU利用率10%以下,load5以下,95%的遠程調用只有一次數據庫的Insert操作,遠程接口TP999在20ms內。

第二類問題是因為代碼編寫不當,引發JVM假死和CPU 100%。

出于減少遠程接口同步邏輯的需要,研發同學將大部分操作改為異步方式,比如郵件的推薦商品服務。因為郵件發送任務在生產平臺到發送平臺間流轉需要一定的時間,將推薦商品服務異步化后,生產郵件發送任務的同步邏輯會減少,遠程接口調用或MQ消息消費線程可以更早返回,對推薦接口的性能波動容忍度也會變高,只需要保證在發送平臺渲染郵件模板前能夠拿到推薦商品的數據即可。

異步改造時,研發同學使用了線程池的無界隊列,并因為一個低級BUG導致上線后無界隊列的消費線程只有5個,生產和消費的速率嚴重不匹配,導致了短時間內JVM內存占用過高,JVM頻繁GC,JVM頻繁處于“stop the world”階段,呈現出“假死”狀態,最終再次影響到遠程接口的調用和MQ消息的消費。這次的經驗說明,實例宕機或許并不是最難處理的,更難處理的是實例處于可以提供服務,但是沒有服務能力的狀態。

我們的解決方案是使用有界隊列,防止超長隊列的產生。設置隊列的拒絕策略,隊列無空閑位置時,放棄入隊操作。此時會導致部分郵件缺少推薦商品模塊,可視作推薦商品模塊的處理能力達到上限后的一種降級方式。

Redis隊列中元素的大小大于10K時,入隊和出隊的效率會嚴重下降,出于這這個原因,Redis隊列中只存放有郵件發送任務的原始數據,渲染工作是在發送節點上完成的。發送高峰期時,發送平臺的CPU利用率整體在80%甚至90%以上,發送能力無法再提升。經過一系列排查后發現CPU利用率較高的源頭來自于Handlebars模板渲染模塊。

抽樣查看部分線上機器的線程占用率時發現渲染線程大部分時間一直在做郵件模板的語法解析,參考相應文檔后發現語法解析是模板渲染中最耗時的流程,為了提高效率無論是Velocity還是Handlbars都會對模板語法解析的結果做緩存,下次渲染時直接使用解析結果渲染。但緩存是基于VelocityEngine或Handlebar實例的,如果JVM中存在多個VelocityEngine或Handlebar實例,緩存就無法有效利用,結果是每次渲染模板時都要做語法解析,如果并發解析的線程達到數十、數百個的話,就會引起實例的CPU 100%。

解決方案:

  • 保證全局只有一個Handlebar實例,方便共享緩存結果。

  • 容器啟動時,渲染線程依次啟動并等待一段時間在啟動下一個渲染線程,避免并發啟動多個線程時再次出現并發解析模板的情況。

除去以上問題的解決,上線后研發團隊還做了幾次全流程的優化,優化包括黑名單、退訂數據緩存化,Redis隊列Push方式異步化、批量化,發送平臺的拉取合并、郵件模板本地化等。

優化后平臺的應用架構如下:

容災方案

容災方案中,優先考慮的就是多網絡運營商覆蓋的問題,防止某一網絡運營商網絡故障影響郵件發送的能力。目前的方案是單一機房配置單一網絡運營商的出口IP及反解析域名,每個機房部署相應的生產平臺,Redis隊列和發送平臺,彼此之間相互獨立運行,底層使用同一個數據庫,生產平臺提供的遠程接口為同一個別名服務,MQ消息也是消費的同一個Topic下的內容,多個Redis之間存在少量數據的同步,比如去重數據。整體架構如下:

我們將緊急情況分為內部接口、服務故障和外部服務故障。針對內部故障,比如:

  • Redis集群故障。如果是單分片故障,Redis集群提供了主從分片,可以通過切換分片的方式解決。如果是集群整體故障,可以啟用備用Redis集群,在這里不存在集群數據為空的問題,因為生產平臺有Checker存在,如果切換集群,Checker可以感知到Redis隊列數據量不夠,會重新將待發送的郵件任務Push到Redis隊列中。

針對外部故障,比如:

  • 機房出口網絡故障。可以停止故障機房的發送平臺,因數據庫共享,數據入庫后對端機房的Checker會將數據重新Push到對端機房的Redis隊列中,從對端機房發送郵件任務。這里還有一種方案是修改故障機房的Redis集群配置,故障機房的生產平臺生成郵件發送任務后直接將數據Push到對端機房的Redis集群中,省略Checker掃描的這一步,會大大減少數據庫的讀壓力。

  • 郵件服務提供商對部分出口IP降頻。發送節點上內置了降頻處理措施,可以解決該問題。

  • 郵件服務商屏蔽部分出口IP。通過自研的配置推送與服務監控框架,可用管理平臺將被屏蔽的IP地址推送到發送平臺上,發送平臺通過比對如果發現自身已被屏蔽,將不再從Redis隊列中Pull相應的郵件發送任務。

總結

EDM平臺上層對接了精準營銷平臺和生產郵件業務,如何用一套通用的解決方案解決兩個業務的不同需求是建設該平臺的難點,需要在兩種業務形態間找到共性并滿足各自業務對及時性,發送量方面的要求。尤其是生產郵件業務,對發送的及時性,穩定性的要求都較高,非常容易引起上層業務團隊的關注。在做方案時需要更多的關注到架構本身對性能、容災、業務和研發同學的友好性,架構越容易讓人接受,更簡單的解決現有問題,才有可能在以后的發展中不斷往好的方向進化,容納更多復雜的業務需求,支持業務的長久發展。

 

 

來自:http://www.infoq.com/cn/articles/the-optimization-road-of-jingdong-edm-platform

 

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