這是釘釘第一次對外揭秘 DTIM(DingTalk IM,釘釘即時消息服務)。我們從設計原理到技術架構、從最底層存儲模型到跨地域的單元化,全方位地展現了 DTIM 在實際生產中遇到各種挑戰與解決方案,期望為企業級 IM 的建設貢獻一臂之力。
釘釘已經有 2100 萬 + 組織、5 億 + 注冊用戶在使用。DTIM 為釘釘用戶提供即時消息服務,用于組織內外的溝通,這些組織包括公司、政府、學校等,規模從幾人到百萬人不等。DTIM 有著豐富的功能,單聊、各種類型的群聊、消息已讀、文字表情、多端同步、動態卡片、專屬安全和存儲等等。同時釘釘內部很多業務模塊,比如文檔、釘閃會、Teambition、音視頻、考勤、審批等,每個業務都在使用 DTIM,用于實現業務流程通知、運營消息推送、業務信令下發等。每個業務模塊對于 DTIM 調用的流量峰值模型各有差別,對可用性要求也不盡相同。DTIM 需要能夠面對這些復雜的場景,保持良好的可用性和體驗,同時兼顧性能與成本。
通用的即時消息系統對消息發送的成功率、時延、到達率有很高的要求,企業 IM 由于 ToB 的特性,在數據安全可靠、系統可用性、多終端體驗、開放定制等多個方面有著極致的要求。構建穩定高效的企業 IM 服務,DTIM 主要面臨的挑戰是:
DTIM 在系統設計上,為了實現消息收發體驗、性能和成本的平衡,設計了高效的讀寫擴散模型和同步服務,以及定制化的 NoSQL 存儲。通過對 DTIM 服務流量的分析,對于大群消息、單賬號大量的消息熱點以及消息更新熱點的場景進行了合并、削峰填谷等處理;同時核心鏈路的應用中間件的依賴做容災處理,實現了單一中間件失敗不影響核心消息收發,保障基礎的用戶體驗。
在消息存儲過程中,一旦出現存儲系統寫入異常,系統會回旋緩沖重做,并且在服務恢復時,數據能主動向端上同步。隨著用戶數不斷增長,單一地域已無法承載 DTIM 的容量和容災需求,DTIM 實現了異地多單元的云原生的彈性架構。在分層上遵從的原則為重云輕端:業務數據計算、存儲、同步等復雜操作盡量后移到云端處理,客戶端只做終態數據的接收、展示,通過降低客戶端業務實現的復雜度,最大化地提升客戶端迭代速度,讓端上開發可以專注于提升用戶的交互體驗,所有的功能需求和系統架構都圍繞著該原則做設計和擴展。
以下我們將對 DTIM 做更加詳細的介紹。在第 1 章,我們介紹了 DTIM 的核心模型設計;第 2 章介紹了針對 IM 特點將計算下沉和與之對應的優化點;在第 3 章中介紹了同步服務,實現 IM 和其他業務的多端同步能力;在第 4 章主要介紹高可用的設計,首先是系統自我防護,之后是系統的彈性擴展能力與異地容災設計。
低延遲、高觸達、高可用一直是 DTIM 設計的第一原則,依據這個原則在架構上 DTIM 將系統拆分為三個服務做能力的承載:
Fig.1:DTIM architecture
同步服務和通知服務除了服務于消息服務,也面向其他釘釘業務比如音視頻、直播、Ding、文檔等多端 (多設備) 數據同步。上圖展示了 DTIM 系統架構,接下來詳細介紹消息收發鏈路。
消息發送:消息發送接口由 Receiver 提供,釘釘統一接入層將用戶從客戶端發送的消息請求轉發到 Receiver 模塊,Receiver 校驗消息的合法性(文字圖片等安全審核、群禁言功能是否開啟或者是否觸發會話消息限流規則等)以及成員關系的有效性(單聊校驗二者聊天、群聊校驗發送者在群聊成員列表中),校驗通過后為該消息生成一個全局唯一的 MessageId 隨消息體以及接收者列表打包成消息數據包投遞給異步隊列,由下游 Processor 處理。消息投遞成功之后,Receiver 返回消息發送成功的回執給客戶端。
消息處理 :Processor 消費到 IM 發送事件首先做按接收者的地域分布(DTIM 支持跨域部署, Geography,Geo)做消息事件分流,將本域用戶的消息做本地存儲入庫(消息體、接收者維度、已讀狀態、個人會話列表紅點更新),最后將消息體以及本域接收者列表打包為 IM 同步事件通過異步隊列轉發給同步服務。
消息接收 :同步服務按接收者維度寫入各自的同步隊列,同時查取當前用戶設備在線狀態,當用戶在線時撈取隊列中未同步的消息,通過接入層長連接推送到各端。當用戶離線時,打包消息數據以及離線用戶狀態列表為 IM 通知事件,轉發給通知服務的 PNS 模塊,PNS 查詢離線設備做三方廠商通道推送,至此一條消息的推送流程結束。
Fig. 2: DTIM message processing architecture
了解 IM 服務最快的途徑就是掌握它的存儲模型。業界主流 IM 服務對于消息、會話、會話與消息的組織關系雖然不盡相同,但是歸納起來主要是兩種形式:寫擴散讀聚合、讀擴散寫聚合,所謂讀寫擴散其實是定義消息在群組會話中的存儲形式,以下圖所示:
Fig. 3: Read model & Write model
DTIM 對 IM 消息的及時性、前后端存儲狀態一致性要求異常嚴格,特別對于歷史消息漫游的訴求十分強烈,當前業界 IM 產品對于消息長時間存儲和客戶端歷史消息多端漫游都做得不盡如人意,主要是存儲成本過高。因此在產品體驗與投入成本之間需要找到一個平衡點。
采用讀擴散,在個性化的消息擴展及實現層面有很大的約束。采用寫擴散帶來的問題也很明顯:一個群成員為 N 的會話一旦產生消息就會擴散 N 條消息記錄,如果在消息發送和擴散量較少的場景,這樣的實現相比于讀擴散落地更為簡單,存儲成本也不是問題,但是 DTIM 會話活躍度超高,一條消息的平均擴散比可以達到 1:30,超大群又是企業 IM 最核心的溝通場景,如果采用完全寫擴散所帶來存儲成本問題勢必制約釘釘業務發展。
所以,在 DTIM 的存儲實現上,釘釘采取了混合的方案,將讀擴散和寫擴散針對不同場景做了適配,最終在用戶視角系統會做統一合并,如下圖所示:
Fig. 4: DTIM Read-Write hybrid model
作為讀擴散、寫擴散方案的混合形式存在,用戶維度的消息分別從 conversation_message 和 message_inbox 表中獲取,在應用側按消息發送時間做排序合并,conversation_message 表中記錄了該會話面向所有群成員接收的普通消息 N(Normal),而 message_inbox 表在以下兩種場景下被寫入:
第一種是定向消息 P(Private,私有消息),群會話中發送的消息指定了接收者范圍,那么會直接寫入到接收者的 message_inbox 表中,比如紅包的領取狀態消息只能被紅包發送者可見,那么這種消息即被定義為定向消息。
第二種是歸屬于會話消息的狀態轉換 NP(Normal to Private,普通消息轉私有消息),當會話消息通過某種行為使得某些消息接收者的消息狀態發生轉換時,該狀態會寫入到 message_inbox 表中,比如用戶在接收側刪除了消息,那么消息的刪除狀態會寫入到 message_inbox 中,在用戶拉取時會通過 message_inbox 的刪除狀態將 conversation_message 中的消息做覆蓋,最終用戶拉取不到自己已刪除的消息。
當用戶在客戶端發起某個會話的歷史消息漫游請求時,服務端根據用戶上傳的消息截止時間(message_create_time)分別從 conversation_message 表和 message_inbox 表拉取消息列表,在應用層做狀態的合并,最終返回給用戶合并之后的數據,N、P、NP 三種類型消息在消息個性化處理和存儲成本之間取得了很好的平衡。
用戶在會話中發出的消息和消息狀態變更等事件是如何同步到端上呢?業界關于消息的同步模型的實現方案大致有三種:客戶端拉取、服務端推送、服務端推送位點之后客戶端拉取的推拉結合方案。
三種方案各有優劣,在此簡短總結:
在 DTIM 的場景中,如上文所述,DTIM 相對傳統 toC 的場景,有較明顯的區別:
第一是對實時性的要求。在企業服務中,比如員工聊天消息、各種系統報警,又比如音視頻中的共享畫板,無不要求實時事件同步,因此需要一種低延時的同步方案。
第二是弱網接入的能力。在 DTIM 服務的對象中,上千萬的企業組織涉及各行各業,從大城市 5G 的高速到偏遠的山區弱網,都需要 DTIM 的消息能發送、能觸達。對于復雜的網絡環境,需要服務端能判斷接入環境,并依據不同的環境條件調整同步數據的策略。
第三是功耗可控成本可控。在 DTIM 的企業場景中,消息收發頻率比傳統的 IM 多出一個數量級,在這么大的消息收發場景怎么保障 DTIM 的功耗可控,特別是移動端的功耗可控,是 DTIM 必須面對的問題。在這種要求下,就需要 DTIM 盡量降低 IO 次數,并基于不同的消息優先級進行合并同步,既能要保障實時性不被破壞,又要做到低功耗。
從以上三點可知,主動推的模型更適合 DTIM 場景,服務端主動推送,首先可以做到極低的延時,保障推送耗時在毫秒級別;其次是服務端能通過用戶接入信息判斷用戶接入環境好壞,進行對應的分包優化,保障弱網鏈路下的成功率;最后是主動推送相對于推拉結合來說,可以降低一次 IO,對 DTIM 這種每分鐘過億消息服務來說,能極大的降低設備功耗,同時配合消息優先級合并包的優化,進一步降低端的功耗。
雖說主動推送有諸多優勢,但是客戶端會離線,甚至客戶端處理速度無法跟上服務端的速度,必然導致消息堆積,DTIM 為了協調服務端和客戶端處理能力不一致的問題,支持 Rebase 的能力,當服務端消息堆積的條數達到一定閾值,觸發 Rebase,客戶端會從 DTIM 拉取最新的消息,同時服務端跳過這部分消息從最新的位點開始推送消息。DTIM 稱這個同步模型為推優先模型(Preferentially-Push Model,PPM)。
在基于 PPM 的推送方案下,為了保障消息的可靠達到,DTIM 還做一系列優化。
第一,支持消息可重入,服務端可能會針對某條消息做重復推送,客戶端需要根據 msgId 做去重處理,避免端上消息重復展示。
第二,支持消息排序,服務端推送消息特別是群比較活躍的場景,某條消息由于推送鏈路或者端側網絡抖動,推送失敗,而新的消息正常推送到端側,如果端上不做消息排序的話,消息列表就會發生亂序,所以服務端會為每條消息分配一個時間戳,客戶端每次進入消息列表就是根據時間戳做排序再投遞給 UI 層做消息展示。
第三,支持缺失數據回補,在某個極端情況下客戶端群消息事件比群創建事件更早到達端上,此時端上沒有群的基本信息消息也就無法展現,所以需要客戶端主動向服務端拉取群信息同步到本地,再做消息的透出。
多端數據一致性問題一直是多端同步最核心的問題,單個用戶可以同時在 PC、Pad 以及 Mobile 登錄,消息、會話紅點等狀態需要在多端保持一致,并且用戶更換設備情況下消息可以做全量的回溯。
基于上面的業務訴求以及系統層面面臨的諸多挑戰,釘釘自研了同步服務來解決一致性問題,同步服務的設計理念和原則如下:
上面介紹了 DTIM 的存儲模型以及同步模型的設計與思考,在存儲優化中,存儲會基于 DTIM 消息特點,進行深度優化,并會對其中原理以及實現細節做深入分析與介紹;在同步機制中,會進一步介紹多端同步機制是如何保障消息必達以及各端消息一致性。
DTIM 底層使用了表格存儲作為消息系統的核心存儲系統,表格存儲是一個典型 LSM 存儲架構,讀寫放大是此類系統的典型問題。LSM 系統通過 Major Compaction 來降低數據的 Level 高度,降低讀取數據放大的影響。在 DTIM 的場景中,實時消息寫入超過百萬 TPS,系統需要劃歸非常多的計算資源用于 Compaction 處理,但是在線消息讀取延遲毛刺依舊嚴重。
在存儲的性能分析中,我們發現如下幾個特點:
從上面的四個表現來看,我們能得出如下結論:
為了解決此類問題,我們采用分而治之方法,將高頻用戶和低頻用戶的消息區別對待。我們借鑒了 WiscKey KV 分離技術的思想,就是將到達一定閾值的 Value 分離出來,降低這類消息在文件中的占比進而有效的降低寫放大的問題。
但是 WiscKey KV 分離僅考慮單 Key 的情況,在 DITM 的場景下,Key 之間的大小差距不大,直接采用這種 KV 分離技術并不能解決以上問題。因此我們在原有 KV 分離的基礎上,改進了 KV 分離,將相同前綴的多個 Key 聚合判斷,如果多個 Key 的 Value 超過閾值,那么將這些 Key 的 Value 打包了 value-block 分離出去,寫入到 value 文件。
數據顯示,上述方法能夠在 Minor Compaction 階段將 MemTable 中 70% 的消息放入 value 文件,大幅降低 key 文件大小,帶來更低的寫放大;同時,Major Compaction 可以更快降低 key 文件數,使讀放大更低。高頻用戶發送消息更多,因此其數據行更容易被分離到 value 文件。讀取時,高頻用戶一般把最近消息全部讀出來,考慮到 DTIM 消息 ID 是遞增生成,消息讀取的 key 是連續的,同一個 value-block 內部的數據會被順序讀取,基于此,通過 IO 預取技術提前讀取 value-block,可以進一步提高性能。對于低頻用戶,其發送消息較少,K-V 分離不生效,從而減少讀取時候 value 文件帶來的額外 IO。
Fig. 5: Key value separation and re-pack
DTIM 面向辦公場景,和面向普通用戶的產品在服務端到客戶端的數據同步上最大的區別是消息量巨大、變更事件復雜、對多端同步有著強烈的訴求。DTIM 基于同步服務構建了一套完整同步流程。同步服務是一個服務端到客戶端的數據同步服務,是一套統一的數據下行平臺,支撐釘釘多個應用服務。
Fig. 6: SyncServices architecture
同步服務是一套多端的數據同步服務,由兩部分組成:部署于服務端的同步服務和由客戶端 APP 集成的同步服務 SDK。工作原理類似于消息隊列,用戶 ID 近似消息隊列中的 Topic,用戶設備近似消息隊列中的 Consumer Group,每個用戶設備作為一個消費者能夠按需獲得這個用戶一份數據拷貝,實現了多端同步訴求。
當業務需要同步一個變更數據到指定的用戶或設備時,業務調用數據同步接口,服務端會將業務需要同步的數據持久化到存儲系統中,然后當客戶端在線的時候把數據推送給客戶端。每一條數據入庫時都會原子的生成一個按用戶維度單調遞增的位點,服務端會按照位點從小到大的順序把每一條數據都推送至客戶端。
客戶端應答接收成功后,更新推送數據最大的位點到位點管理存儲中,下次推送從這個新的位點開始推送。同步服務 SDK 內部負責接收同步服務數據,接收后寫入本地數據庫,然后再把數據異步分發到客戶端業務模塊,業務模塊處理成功后刪除本地存儲對應的內容。
在上文章節中,已經初步介紹同步服務推送模型和多端一致性的考慮,本章主要是介紹 DTIM 是如何做存儲設計、在多端同步如何實現數據一致性、最后再介紹服務端消息數據堆積技術方案 Rebase。
在同步服務中,采用以用戶為中心,將所有要推送給此用戶的消息匯聚在一起,并為每個消息分配唯一且遞增的 PTS(位點, Point To Sequence),服務端保存每個設備推送的位點。
通過兩個用戶 Bob 和 Alice,來實際展示消息在存儲系統中存儲的邏輯形態。例如,Bob 給 Alice 發送了一個消息”Hi! Alice“,Alice 回復了 Bob 消息”Hi! Bob“。
當 Bob 發送第一條消息給 Alice 時,接收方分別是 Bob 和 Alice,系統會在 Bob 和 Alice 的存儲區域末尾分別添加一條消息,存儲系統在入庫成功時,會分別為這兩行分配一個唯一且遞增的位點(Bob 的位點是 10005,Alice 的位點是 23001);入庫成功之后,觸發推送。比如 Bob 的 PC 端上一次下推的位點是 10000,Alice 移動端的推送位點是 23000,在推送流程發起之后,會有兩個推送任務,第一是 Bob 的推送任務,推送任務從上一次位點(10000) + 1 開始查詢數據,將獲取到 10005 位置的”Hi“消息,將此消息推送給 Bob 的設備,推送成功之后,存儲推送位點(10005)。Alice 推送流程也是同理。Alice 收到 Bob 消息之后,Alice 回復 Bob,類似上面的流程,入庫成功并分配位點(Bob 的位點是 10009,Alice 的位點是 23003)。
Fig. 7: Storage design of SyncServices
多端同步是 DTIM 的典型特點,如何保持多端的數據及時觸達和解決一致性是 DTIM 同步服務最大的挑戰。上文中已經介紹了同步服務的事件存儲模型,將需要推送的消息按照用戶聚合。當用戶有多個設備時,將設備的位點保存在位點管理系統之中,Key 是用戶 + 設備 ID,Value 是上一次推送的位點。如果是設備第一次登錄,位點默認為 0。由此可知,每個設備都有單獨的位點,數據在服務端只有一份按照用戶維度的數據,推送到客戶端的消息是服務端對應位點下的快照,從而保障了每個端的數據都是一致的。
比如此時 Bob 登錄了手機(該設備之前登錄過釘釘),同步服務會獲取到設備登錄的事件,事件中有此設備上次接收數據的位點(比如 10000),同步服務會從 10000 + 1(位點)開始查詢數據,獲取到五條消息(10005~10017),將消息推送給此臺手機并更新服務端位點。此時,Bob 手機和 PC 上的消息一致,當 Alice 再次發送消息時,同步服務會給 Bob 的兩臺設備推送消息,始終保持 Bob 兩個設備之間消息數據的一致性。
正如上文所述,我們采用了推優先的模型下推數據以保障事件的實時性,采用位點管理實現多端同步,但是實際情況卻遠比上面的情況復雜。最常見的問題就是設備離線重新登錄,期間該用戶可能會累計大量未接收的消息數據,比如幾萬條。如果按照上面的方案,服務端在短時間會給客戶端推送大量的消息,客戶端 CPU 資源極有可能耗盡導致整個設備假死。
其實對于 IM 這種場景來說,幾天甚至幾小時之前的數據,再推送給用戶已經喪失即時消息的意義,反而會消耗客戶移動設備的電量,得不償失。又或者節假日大群中各種活動,都會有大量的消息產生。對于以上情況,同步服務提供 Rebase 的方案,當要推送的消息累計到一定閾值時,同步服務會向客戶端發送 Rebase 事件,客戶端收到事件之后,會從消息服務中獲取到最新的消息(Lastmsg)。這樣可以跳過中間大量的消息,當用戶需要查看歷史消息,可以基于 Lastmsg 向上回溯,即省電也能提升用戶體驗。
還是以 Bob 為例,Bob 登錄了 Pad 設備(一臺全新的設備),同步服務收到 Pad 登錄的事件,發現登錄的位點為 0,查詢從 0 開始到當前,已經累計 1 萬條消息,累計量大于同步服務的閾值,同步服務發送 Rebase 事件給客戶端,客戶端從消息服務中獲取到最新的一條消息”Tks !!!“,同時客戶端從同步服務中獲取最新的位點為 10017,并告訴同步服務后續從 10017 這個位置之后開始推送。當 Bob 進入到和 Alice 的會話之后,客戶端只要從 Lastmsg 向上回溯幾條歷史消息填滿聊天框即可。
DTIM 對外提供 99.995% 的可用性 SLA,有上百萬的組織將釘釘作為自身數字化辦公的基礎設施,由于其極廣的覆蓋面,DTIM 些許抖動都會影響大量企業、機構、學校等組織,進而可能形成社會性事件。因此,DTIM 面臨著極大的穩定性挑戰。
高可用是 DTIM 的核心能力。對于高可用,需要分兩個維度來看,首先是服務自我防護,在遇到流量洪峰、黑客攻擊、業務異常時,要有流量管控、容錯等能力,保障服務在極端流量場景下還有基本服務的能力。其次是服務擴展能力,比如常見的計算資源的擴展、存儲資源的擴展等,資源伴隨流量增長和縮減,提供優質的服務能力并與成本取得較好的平衡。
DTIM 經常會面臨各種突發大流量,比如萬人大群紅包大戰、早高峰打卡提醒、春節除夕紅包等等都會在短時間內產生大量的聊天消息,給系統帶來很大的沖擊,基于此我們采用了多種措施。
首先是流量管控,限流是保護系統最簡單有效的方式。DTIM 服務通過各種維度的限流來保護自身以及下游,最重要的是保護下游的存儲。在分布式系統中存儲都是分片的,最容易出現的是單個分片的熱點問題,DTIM 里面有兩個維度的數據:用戶、會話 (消息屬于會話), 分片也是這兩個維度,所以限流采用了會話、用戶維度的限流,這樣既可以保護下游存儲單個分區,又可以一定程度上限制整體的流量。要控制系統的整體流量, 前面兩個維度還不夠,DTIM 還采用了客戶端類型、應用 (服務端 IM 上游業務) 兩個維度的限流,來避免整體的流量上漲對系統帶來的沖擊。
其次是削峰平谷。限流簡單有效,但是對用戶的影響比較大。在 DTIM 服務中有很多消息對于實時性要求不高,比如運營推送等。對于這些場景中的消息可以充分利用消息系統異步性的特點,使用異步消息隊列進行削峰平谷,這樣一方面減少了對用戶的影響,另一方面也減輕對下游系統的瞬時壓力。DTIM 可以根據業務類型 (比如運營推送)、消息類型 (比如點贊消息) 等多種維度對消息進行分級,對于低優先級的消息保證在一定時間 (比如 1 個小時) 內處理完成。
最后是熱點優化。DTIM 服務中面臨著各種熱點問題,對于這些熱點問題僅僅靠限流是不夠的。比如通過釘釘小秘書給大量用戶推送升級提醒,由于是一個賬號與大量賬號建立會話,因此會存在 conversation_inbox 的熱點問題,如果通過限速來解決,會導致推送速度過慢、影響業務。對于這類問題需要從架構上來解決。
總的來說,主要是兩類問題:大賬號和大群導致的熱點問題。對于大賬戶問題,由于 conversation_inbox 采用用戶維度做分區,會導致系統賬號的請求都落到某個分區,從而導致熱點。解決方案為做熱點拆分,既將 conversation_inbox 數據合并到 conversation_member 中 (按照會話做分區),將用戶維度的操作轉換為會話維度的操作,這樣就可以將系統賬號的請求打散到所有分區上, 從而實現消除熱點。對于大群問題,壓力來自大量發消息、消息已讀和貼表情互動,大量的接收者帶來極大的擴散量。所以我們針對以上三個場景,分而治之。
對于消息發送,一般的消息對于群里面所有人都是一樣的,所以可以采用讀擴散的方式,即不管多大的群,發一條消息就存儲一份。另一方面,由于每個人在每個會話上都有紅點數和 Lastmsg, 一般情況下每次發消息都需要更新紅點和 Lastmsg,但是在大群場景下會存在大量擴散,對系統帶來巨大的壓力。我們的解決方案為,對于大群的紅點和 Lastmsg,在發消息時不更新,在拉首屏時實時算,由于拉首屏是低頻操作且每個人只有一到兩個大群,實時計算壓力很小,這樣高峰期可以減少 99.99 % 的存儲操作, 從而解決了大群發消息對 DTIM 帶來的沖擊。
在大群發消息的場景中,如果用戶都在線,瞬時就會有大量已讀請求,如果每個已讀請求都處理,則會產生 M*N(M 消息條數,N 群成員數) 的擴散,這個擴散是十分恐怖的。
DTIM 的解決方案是客戶端將一個會話中的多次已讀進行合并,一次性發送給服務端,服務端對于每條消息的已讀請求進行合并處理,比如 1 分鐘的所有請求合并為 1 次請求。在大群中,進行消息點贊時,短時間會對消息產生大量更新,再加上需要擴散到群里面的所有人,系統整體的擴散量十分巨大。我們發現,對于消息多次更新的場景,可以將一段時間里面多次更新合并,可以大大減少擴散量,從實際優化之后的數據來看,高峰期系統的擴散量同比減少 96%。
即使完全做到以上幾點,也很難提供當前承諾的 SLA,除了防止自身服務出現問題以外,還必須實現對依賴組件的容災。我們整體采用了冗余異構存儲和異步隊列與 RPC 相結合的方案,當任意一類 DTIM 依賴的產品出現問題時, DTIM 都能正常工作,由于篇幅問題,此處不再展開。
對于服務的彈性擴展能力,也需要分兩個維度來看。首先,服務內部的彈性擴展,比如計算資源的擴展、存儲資源的擴展等,是我們通常構建彈性擴展能力關注的重點方向;其次是跨地域維度的擴展,服務能根據自身需要,在其他區域擴展一個服務集群,新的服務集群承接部分流量,在跨地域層面形成一個邏輯統一的分布式服務,這種分布式服務我們稱之為單元化。
對于 DTIM 的擴展性,因為構建和生長于云上,在彈性擴展能力建設擁有了更多云的特點和選擇。對于計算節點,應用具備橫向擴展的能力,系統能在短時間之內感知流量突增進而進行快速擴容,對于上文提到的各種活動引起的流量上漲,能做到輕松應對。同時,系統支持定時擴容和縮容,在系統彈性能力和成本之間取得較好的平衡。
對于存儲,DTIM 底層選擇了可以水平擴展的 Serverless 存儲服務,存儲服務內部基于讀寫流量的大小進行動態調度,應用上層完全無感知。對于服務自身的擴展性,我們還實施了不可變基礎設施、應用無狀態、去單點、松耦合、負載均衡等設計,使 DTIM 構建出了一套高效的彈性應用架構。
在應用內部實現了高效彈性之后,伴隨著業務流量的增長,單個地域已經無法滿足 DTIM 億級別 DUA 的彈性擴展的需求。由于 DTIM 特點,所有用戶都可以在添加好友之后進行聊天,這就意味著不能簡單換個地域搭建一套孤島式的 DTIM。為了解決這種規模下的彈性能力,我們基于云上的多 Region 架構,在一個 Geo 地域內,構建了一套異地多活、邏輯上是一體的彈性架構,我們稱之為單元化。下圖是 DTIM 的單元化架構。
Fig. 8: DTIM Unit group architecture to process message by RoutingService.
對于單元化的彈性擴展架構,其中最核心的內容是流量動態調度、數據單地域的自封閉性和單元整體降級。
流量路由決定了數據流向,我們可以依托這個能力,將大群流量調度到新的單元來承接急速增長的業務流量,同時實現流量按照企業維度匯聚,提供就近部署能力,進而提供優質的 RT 服務。
業界現在主流的單元化調度方案主要是基于用戶維度的靜態路由分段,這種方案算法簡單可靠,但是很難實現動態路由調度,一旦用戶路由固定,無法調整服務單元。比如在 DTIM 的場景中,企業(用戶)規模是隨著時間增長、用戶業務規模增長之后,單地域無法有效支撐多個大型企業用戶時,傳統靜態方案很難將企業彈性擴展到其他單元,強行遷移會付出極高的運維代價。因此傳統的路由方案不具備彈性調度能力。
DTIM 提供一套全局一致性的高可用路由服務系統 (RoutingService)。服務中存儲了用戶會話所在單元,消息服務基于路由服務,將流量路由到不同的單元。應用更新路由數據之后,隨之路由信息也發生變化。與此同時,路由服務發起數據訂正事件,將會話的歷史消息數據進行遷移,遷移完成之后正式切換路由。路由服務底層依賴存儲的 GlobalTable 能力,路由信息更新完成之后,保障跨地域的一致性。
數據的單元自封閉是將 DTIM 最重要且規模最大的數據:“消息數據”的接收、處理、持久化、推送等過程封閉在當前單元中,解除了對其他單元依賴,進而能高效地擴充單元,實現跨地域級別高效彈性能力。
要做到業務數據在單元內自封閉,最關鍵是要識別清楚要解決哪種數據的彈性擴展能力。在 DTIM 的場景下,用戶 Profile、會話數據、消息數據都是 DTIM 最核心的資產,其中消息數據的規模遠超其他數據,彈性擴展能力也是圍繞消息數據的處理在建設。怎么將消息按照單元數據合理的劃分成為單元自封閉的關鍵維度。
在 IM 的場景中,消息數據來自于人與人之間的聊天,可以按照人去劃分數據,但是如果聊天的兩個人在不同的單元之間,消息數據必然要在兩個單元拷貝或者冗余,因此按照人劃分數據并不是很好的維度。
DTIM 采用了會話維度劃分,因為人和會話都是元數據,數據規模有限,消息數據近乎無限,消息歸屬于會話,會話與會話之間并無交集,消息處理時并沒有跨單元的調用。因此,將不同的會話拆分到不同的單元,保障了消息數據僅在一個單元處理和持久化,不會產生跨單元的請求調用,進而實現了單元自封閉。
在單元化的架構中,為了支持服務級別的橫向擴展能力,多單元是基本形態。單元的異常流量亦或者是服務版本維護的影響都會放大影響面,進而影響 DTIM 整體服務。因此,DTIM 重點打造了單元降級的能力,單一單元失去服務能力之后,DTIM 會將業務流量切換到新的單元,新消息會從正常的單元下推,釘釘客戶端在數據渲染時也不會受到故障單元的影響,做到了單元故障切換用戶無感知。
本文通過模型設計、存儲優化、同步機制以及高可用等維度,本文全方位地展示了當代企業級 IM 設計的核心。上文是對 DTIM 過去一段時間的技術總結,隨著用戶數的持續增長,DTIM 也在與時俱進、持續迭代和優化,比如支持條件索引進而實現索引加速和成本可控、實現消息位點的連續累加、實現消息按需拉取和高效的完整性校驗、提供多種上下行通道,進一步提升弱網下的成功率和體驗等。
假日,對于我們每個人來說都息息相關。特別是國家法定節假日的安排,大家都希望清楚知道并合理安排好。因為,節假日是國務院統一安排的,我們就為此編寫了這樣一個節假日查詢API接口,供大家方便查詢。
節假日API接口正廣泛應用于各行各業系統中。人們都希望可以在國家法定的時間放假休息及娛樂,畢竟是帶薪休息是大家都樂意的事情,而且國家對于法定節假日是有嚴格要求的,公司企業必須要遵守放假規定。因此企業對接節假日信息查詢有著非常廣泛的需求,我們開發的API接口也就運應而生。
節假日API接口:
指定日期,返回是否國家法定節假日或者法定工作日。同時返回當天是一年中的第幾天、第幾周。每年根據國務院放假安排進行同步更新。
國家對于每年節假日的安排是統一的,企業在設置上班時間打卡的系統中就要是自己手動進行設置,就像每天什么時間需要上班打卡,每個月的周末是幾號需要放幾天假;工作日時幾號到多少號,系統設置需要每隔多少天停止,持續的是多少天,那么就按照這樣的規律進行循環。在接入API節假日接口之后就可以手動增加某天或者是某幾天是什么節假日,需要進行休息放假。
當然在使用節假日信息查詢API接口時,節假日有些是不能根據計算得來的就必須要自己進行維護,因此在選擇接口時就要注意接口平臺進行測試接口數據是否與給出的協議一致,在自己實際使用的時候是否可以對系統進行修改,這些都是在簽訂協議前要注意的。
子接口:
返回格式:json,xml
請求方式:GET,POST
POST 請求需要設置Header頭:Content-Type: application/x-www-form-urlencoded;charset=utf-8
請求說明:
名稱 | 必填 | 類型 | 說明 | 示例 參數另存 |
appid | 是 | String | 應用ID,在后臺我的應用查看或者添加 | 1 |
date | 否 | String | 日期 | 2020-10-07 |
format | 否 | String | 返回數據格式類型,每個接口已經說明支持返回格式:json,xml | json |
sign | 是 | String | 1.使用Md5方式驗證,參數按一定規則md5后返回的字符串,詳情點擊這里閱讀 | 52a32be274a5c537bbf7a53e2d66c09f |
返回參數說明:
名稱 | 必填 | 類型 | 說明 | 示例 參數另存 |
codeid | 否 | Integer | 狀態碼,返回10000狀態都會進行計費。具體說明可查看狀態碼說明 | 10000 |
curtime | 否 | String | 當前服務器時間戳 | 1672219972 |
dau_event | 否 | String | 日期狀態描述 | 中秋節、國慶節放假 |
is_work_day | 否 | Integer | 0正常周末休息,1正常工作日,3法定假日,4法定工作日(調班) | 3 |
leap_year | 否 | String | 是否為閏年 | 是 |
message | 否 | String | 請求狀態說明 | 操作成功! |
retdata | 否 | Array | 回數據集合,可能是數據、對象或者字符串 | |
week | 否 | String | 中文星期幾 | 星期三 |
week_abbr | 否 | String | 星期的英文縮寫 | Wed |
week_english | 否 | String | 星期的英文全稱 | Wednesday |
year_day | 否 | String | 年份中的第幾天 0 到 365 | 280 |
year_week | 否 | String | ISO-8601 格式年份中的第幾周,每周從星期一開始 例如:41(當年的第 41 周) | 41 |
狀態碼說明:
狀態碼 | 說明 |
10000 | 返回成功 |
10001 | appid必須指定,可以我的應用里面查看 |
10002 | sign值必須指定,加密規則請前往幫助中心查看 |
10003 | sign值驗證不通過,加密規則請前往幫助中心查看 |
10004 | 時差不能超過10分鐘,可以不傳遞這個參數,注意時間戳單位是秒 |
10005 | appid錯誤,請檢查appid值,前往會員中心->我的應用查看或添加 |
10006 | 當前IP地址未授權,請前往用戶中心->我的應用添加ip{@info} |
10007 | 應用被禁用,請聯系客服處理 |
10008 | 應用內沒有該接口,請到我的應用里面添加這個接口 |
10009 | api接口不存在 |
10010 | 您沒有添加該api接口 |
10011 | api已經到期 |
10012 | 沒有訂購任何api,請前往購買后再操作 |
10013 | 該接口已經暫停使用 |
10014 | 未知的錯誤,可以聯系客服處理 |
10015 | 參數個數錯誤 |
10019 | {@info} |
10017 | time必須是整型 |
10018 | 次數不足 |
10020 | 子接口不存在,可能已經被關閉 |
10021 | 服務器發生錯誤 |
10022 | 帳戶余額不足,請充值! |
10023 | 訂單提交成功,等待回調結果 |
10024 | 調試模式數據 |
10025 | 查無數據 |
請求示例:
$method='GET'; //請求方式 GET,POST
$secretType='MD5'; //驗證方式MD5,Hash 通過后臺 我的應用去修改
$api_url='https://登錄后顯示/api/186/360';
$appid='應用id';// 在后臺我的應用查看;
$secret='應用密鑰';// 在后臺我的應用查看;
$data=array(
'appid'=> '1',
'date'=> '2020-10-07',
'format'=> 'json',
);
$data['appid']=$appid;
$data['time']=time();//當前服務器時間
if('MD5'==$secretType){
ksort($data); //按照鍵名對數組排序,為數組值保留原來的鍵。
$md5String='';
foreach($data as $key=>$val){
if(strlen($val)>0){ //過濾空值
$md5String.=$key.$val;
}
}
$secret=md5($md5String.$secret);
}
$data['sign']=$secret;
if('GET'==$method){
$sendUrl=$api_url.'?'.http_build_query($data); //把數據轉換成url參數形式,a=b&c=d&e=f
$result=file_get_contents($sendUrl);
}else{
$header=['Content-Type: application/x-www-form-urlencoded;charset=utf-8'];
$ch=curl_init();
if(is_array($data))$data=http_build_query($data);
curl_setopt($ch, CURLOPT_URL, $api_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POST, true);//POST
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_ENCODING,'gzip,deflate');
$result=curl_exec($ch);
}
$result=json_decode($result,true);
print_r($result);
在選擇接口平臺時就一定要選經驗豐富的正規平臺,挖數據平臺是一家從事API接口服務多年的專業API平臺,受到很多企業的認可,大家可以放心應用。