來源:https://blog.csdn.net/bluishglc/article/details/79277455
引言
在大數據的生態系統里,時間序列數據(Time Series Data,簡稱TSD)是很常見也是所占比例最大的一類數據,幾乎出現在科學和工程的各個領域,一些常見的時間序列數據有:描述服務器運行狀況的Metrics數據、各種IoT系統的終端數據、腦電圖、匯率、股價、氣象和天文數據等等,時序數據在數據特征和處理方式上有很大的共性,因此也催生了一些面向面向時序數據的特定工具,比如時序數據庫和時序數據可視化工具等等,在雲平台上也開始出現面向時序數據的SaaS/PaaS服務,例如微軟最近剛剛發布的Azure Time Series Insight。本文會介紹一個時間序列數據處理平台案例,探討這類大數據平台在架構、選型和設計上的一些實踐經驗以供參考。
業務場景
本文介紹的案例是一個面向大型企業IT系統運維的監控平台,數據來源於多種監控終端產生的時序數據,涉及的數據源涵蓋了SCOM、AppDynamics、Website Pulse、Piwik以及AWS Cloud Watch等多種主流的第三方監控工具,基於組織內部的IT規范,所有應用系統都安裝了上述一種或多種監控工具,這為建立一個統一的多維度的監控平台提供了保證,該平台基於多種監控數據,對同一應用/服務系統進行綜合的健康評估,在發生故障時會根據不同的數據源進行交叉驗證,從而幫助運維人員快速和准確地定位故障原因。
架構設計
完整的大數據系統往往包涵數據采集,消息對列,實時流處理,離線批處理,數據存儲和數據展示等多個組件,為了滿足業務上對實時監控和歷史數據匯總分析的需求,系統遵循了Lambda架構,將實時流處理與離線批處理進行了分離。此外,鑒於平台處理的所有數據均為時序數據,在架構上針對這個特點着重進行了調整和優化,其中重要的一環是引入“時間序列數據庫”作為核心的數據存儲與查詢引擎。
系統完整的數據流如下:首先,數據被數據采集組件從外部系統采集並來放入消息隊列,接着,流處理組件從隊列中取出數據進行流式計算,消息隊列從中的起到的作用是平衡“生產者”——數據采集組件和“消費者”——流處理組件在消息處理上的速率差,提升系統的穩定性和可靠性。數據在流處理組件中會經歷清洗、過濾、轉換、業務處理等諸多環節,之后按TSD引擎規定的標准TSD格式推送到TSD引擎,由TSD引擎最終寫入后端數據庫。
實時流處理部分要求數據從采集到最后的展示控制在秒級延遲,嚴格來說,這是一套近實時系統,但其實時性已經足夠滿足業務上的需求,為了保證處理速率,實時鏈條上的數據大多數時間是駐留在內存中的,好在實時部分只關注近兩周的數據,所以總的內存消耗處在可控的范圍之內。
在批處理數據線上,利用數據庫的同步機制將實時部分落地的數據持續同步到批處理的數據庫上,這個庫存儲着數據全集,所有批處理相關的查詢都在這個庫上執行,與實時部分的組件完全隔離。批處理會保存過去三年的數據,分析尺度多為日,周,月甚至年。不同於一般離線分析系統選型Hive一類的數據倉庫,我們希望在離線分析時繼續充分利用時序數據庫帶來的種種好處,比如經過特殊優化的時序數據查詢,開箱即用的查詢接口等等,所以在離線部分我們依然配備TSD引擎,批處理組件在實現業務需求時可以深度利用TSD引擎對時序數據進行聚合運算,在聚合之后的結果上再進行更加復雜的分析並寫回數據庫,同時也可以在普通查詢無法實現需求時越過TSD引擎直接對底層數據文件進行MR計算。
最后,數據展示組件會從TSD引擎中提取數據,以各種形式的圖表展示給用戶。在實際的開發中我們發現TSD引擎對數據格式有諸多的限制,有的TSD需要進行某些轉換和適配才能展示,因此我們在TSD引擎和數據展示組件中間引入了一個輕量的驅動程序來透明地解決這些問題。
基於上述分析和實際的原型驗證,在多輪迭代之后,我們最終成形的系統架構如下:
接下去我們會對每個組件逐一進行介紹。
組件與選型
數據采集
平台的數據來源非常多,涉及到的協議類型自然就多,並且伴隨着以后的持續建設,會有越來越多新的數據源和傳輸協議需要被支持,因此我們希望選定的組件能夠支持非常豐富的協議類型,同時盡可能地通過配置去集成數據源並采集數據,避免編寫大量的代碼。目前業界較為主流的數據采集工具有Flume、Logstash以及Kafka Connect等等,這些工具各有各的特點和擅長領域,但是在支持協議的豐富性和可配置性上,與我們的需求有一定的差距。
其實有一個一直被人忽視但卻是非常理想的數據采集組件——Apache Camel,它主要應用於企業應用集成領域,也被一些系統作為ESB(企業服務總線)使用,其作用是在應用系統林立的企業IT環境中扮演一個“萬向接頭”的角色,讓數據和信息在各種不同的系統間平滑地交換和流轉,經過多年的積累,Camel已經支持近200種協議或數據源,並且可以完全基於配置實現,這恰好滿足了我們數據采集的需求,經過原型驗證,也證明了我們的選擇是明智的。
最后,作為一個非大數據組件,對於Camel的性能和吞吐量我們是有清晰認識的,通過對數據源進行分組,使用多個Camel實例分區采集數據,我們從架構上輕松地解決了這些問題。
消息隊列
在消息隊列的選擇上沒有可以討論的,Kafka幾乎是不二的選擇,我們也不例外。
流處理
流處理和批處理都是業務邏輯最集中的地方,也是系統的核心。目前用於流處理的主流技術是Storm和Spark Streaming,對兩者進行比較的文章很多,通常認為Storm具有更高的實時性,可以做到最低亞秒級的延遲,相比之下Spark Streaming的實時性要差一些,因為它以”micro batch”的方式進行流處理的,但是依托Spark這個大平台,從統一技術堆棧和與其他Spark組件交互的角度考慮,Spark Streaming變得越來越流行,鑒於在業務上秒級延遲已經可以滿足需求,我們最終選擇了后者。
批處理
傳統大數據的離線處理多選擇以Hive為代表的數據倉庫進行建模和分析,這在很多項目上被證明是可靠的解決方案。后來隨着Spark的不斷壯大,Spark SQL的使用越來越廣泛,並且Spark SQL完全兼容Hive,這使得遷移工作幾乎沒有任何障礙。對於復雜的非結構化數據,Hadoop平台上通過MR編程去處理,Spark是通過Spark Core的RDD編程實現。如今Spark在大數據處理的很多方面已經取代Hadoop成為大數據的首選技術平台,我們在批處理的選型上也沒有過多的討論,使用Spark Core + Spark SQL是一個自然而然的決定。
但是考慮到系統處理的是TSD數據,如前文所屬,在批處理的數據鏈條上,TSD引擎依然是一個必不可少的角色,我們設計的策略是:
所有TSD引擎可以直接支持的查詢交由TSD引擎直接處理
復雜的業務處理可以通過TSD引擎進行預處理,將預處理結果交給Spark Core進行深度分析並將結果寫回數據庫
針對TSD引擎無法完成的分析邏輯,由Spark Core或Spark SQL繞過TSD引擎,直連后端的HBase進行分析處理,結果同樣直接寫到HBase上
為提升性能,對分析中使用到的以日/周/月/年為單位的中間表進行預生成計算。
主數據管理
主數據是指來自數據源的核心業務對象,對於我們這個以監控為核心的平台,主數據包括:服務器、系統拓撲結構、站點、網絡設施等等,主數據往往都跨越多種不同的數據源,並且經常發生變更,需要對其進行定期維護。
為此,我們構建了一個統一的主數據管理組件,並通過Web Service的方式向外提供主數據,由於平台在流處理和批處理過程中需要頻繁地使用主數據,而主數據的體量並不大,所以我們會讓流處理和批處理組件一次性地將主數據加載到內存中,同時為它們加入命令行和Restful API接口,允許它們在主數據發生變更時重新加載主數據。
主數據管理模塊是一個傳統的Web應用,基於Spring-Boot構建,使用MySQL存儲導入的主數據,對外通過Restful API提供主數據供給服務,它還有一個管理頁面方便管理員維護主數據。
TSD引擎與數據存儲
TSD引擎負責TSD的寫入和查詢,很多TSD數據庫會利用一個成熟的NoSQL數據庫進行數據存儲,而TSD引擎則專注在TSD數據的處理上。這兩部分密不可分,因此我們放在一起討論。
我們對時間序列數據庫的選型主要是在目前業界最主流的兩個產品InfluxDB和OpenTSDB之間展開的。 前者使用GO語言編寫,后端存儲先后使用過LevelDB和BoltDB,現在使用的則是InfluxDB自己實現的Time Structured Merge Tree引擎,OpenTSDB使用Java編寫,后端存儲使用HBase。在單機性能上,多種對比測試顯示InfluxDB具有更高的性能,但我們最終選擇的是OpenTSDB,主要原因是考慮到在集群和水平伸縮方面,背靠HBase的OpenTSDB有明顯的優勢,相比之下InfluxDB只在收費的企業版提供集群功能,同時在集群規模和支撐的數據量上沒有公開詳實的參考數據,而HBase早已在眾多實際項目特別是國內一些知名互聯網公司中廣泛使用並得到了驗證。另一方面,OpenTSDB和HBase都使用Java編寫,這對於我們整個大數據技術團隊來說在維護和修復一些底層Bug上也相對容易一些。
TSD引擎驅動
這是一個定制開發的組件,其作用是對TSD數據進行轉換和包裹,以便於更好地進行數據展示,當數據查詢請求到達時,它會根據請求的內容和時間跨度把請求路由到實時庫或批處理庫,當請求返回時,它同樣會過濾響應內容,對某些字段和值進行映射和轉碼,如前所述,因為時間序列數據庫對存儲的TSD有很多形式上的限制,某些數據不可以直接存儲,它們在入庫前已經做了相應的格式化處理,在提取展示時需要進行相應的反處理。
TSD引擎驅動本質上是一個Web Service,從某種意義上說,這個Web Service像是TSD引擎的一個反向代理,它能靈活和透明地解決一些定制化需求以及非標准數據的適配工作,從而避免對TSD引擎和前端展示進行侵入性的修改。
在技術選型上,所有支持Web Service的框架都可以勝任這個工作,考慮到我們整個大平台的技術堆棧都以sbt-native-packager/Java為主,我們實驗性地選擇了Akka-Http,通過利用Akka-Http的HTTP DSL和sbt-native-packager的模式匹配,我們用很少的代碼就實現了既定目標,效果非常好。
數據展示
最后,在數據展示上,Grafana是我們最佳的選擇。它是一個專門的時序數據展示工具,可以直連OpenTSDB,圖表的制作都是通過拖放完成的,它還有一個異常強大的“模版”機制,可以通過一次設定生成多張圖表。如果既有插件無法滿足展示需求,團隊還以開發自定義插件。
綜上所述,整個系統的技術堆棧如下所示:
物理架構
對於平台的物理架構我們不打算進行過多的介紹,因為Hadoop/Spark集群都大同小異,我們這里要討論的是這個平台在物理架構上的一個顯著的特點,就是我們構建了兩個獨立的Hadoop/Spark集群,一個負責流處理,另一個負責批處理,這也是踐行Lambda架構在物理層面上的一個自然的結果,兩個集群的數據交互依靠HBase的Replication機制透明地實現。其他的非Hadoop/Spark組件會部署在離散的服務器上。
實時處理集群和批處理集群除了分工上的不同,在集群結構和節點配置上也有很大的區別,特別是在計算資源和存儲資源的分配上。通常,Hadoop集群的計算服務和存儲服務是共生在一起的,即HDFS的DataNode和YARN的NodeManager總是collocate的, 這樣做的目的是讓分布式計算盡可能地從本地讀取數據進行處理,減少網絡IO,提升性能。我們的批處理集群就是按這樣的模式進行資源配置的:基於Spark的批處理程序跑在Yarn的NodeManager上,盡量讀寫本地DataNode上的數據,對於HBase也是同樣的邏輯,讓NodeManager也與DataNode共生在一起。
在實時處理集群上情況則大不相同。首先,在流處理過程中數據是不落地的,因此在流計算的節點上只會分配NodeManager,而不會有DataNode, 到了數據存儲環節才會讓HBase的NodeManager與DataNode共生。所以說NodeManager和DataNode總是collocate的說法太絕對,一切還是要根據實際情況靈活處理。
平台建設
從前面介紹的技術架構和選型上不難看出這個系統的復雜性,在建設過程中我們遇到了很多困難,也積累了一些寶貴的經驗,限於篇幅,我們選取了一些有價值的話題和大家進行分享。
圍繞主數據進行領域建模
“沒有領域模型的設計都是耍流氓”,這句看似調侃的話表達的卻是對軟件設計的一種嚴肅態度,領域模型在任何類型的系統里都起着核心作用,大數據系統也不例外,你可以不去設計它,但這並不表示它不存在,一個不能如實反映業務邏輯的模型注定會導致整個系統的失敗。在我們這個面向時序數據的大數據平台上,所有的TSD都出自於或描述了某一類主數據的狀態或行為,或者說它們都是主數據所代表的業務實體的產物,比如服務器的Metrics數據,這是典型的TSD,它們描述的就是業務對象:”服務器”的狀態。從OO建模的角度來思考這個問題,如果監控系統需要建立針對這個服務器的一整套監控和報警規則,那么所有相應的邏輯必然會追加到“服務器”以及一些和它相關聯的實體上,這就是我們所說的“圍繞主數據進行領域建模”。
這一點非常重要且有效,因為它是對所有業務邏輯的一種自然的梳理和划分,最能夠反映領域的本來面目,越是復雜的業務場景越能體現優越性。所有這些思考和傾向性都在引導我們漸漸地向“領域驅動設計”(Domain Driven Design)的方向前進,這是一個非常豐富並且具有實際意義的話題,令人感慨的是我們在大數據平台上讓“領域驅動設計”再一次煥發了生機,以領域模型為核心驅動業務處理和數據分析是一個非常明智的選擇,盡管這對團隊整體的素質有更高的要求,實施難度也更大,但是回饋也是巨大的。
我們有一個生動的實例:在平台建設的早期,限於每個數據源的格式和處理邏輯上的差異,每個數據源都自己的業務處理代碼和獨立的業務規則表,這種處理方式非常類似於傳統企業應用架構中的“Transaction Script”模式(關於Transaction Script請參考Martin Fowler的《Patterns of Enterprise Application Architecture》一書的第9章),伴隨着數據源的不斷引入,我們發現應用在很多不同數據源上的監控和報警邏輯都非常類似,並且針對的業務對象也都是一樣的,例如不同的數據源都會面向某台服務器或某個站點產生報警消息,而我們對這些報警消息的處理有着很大的相似性,這促使我們以主數據為對象進行了領域建模,把邏輯進行了統一梳理,在不一致的地方運用適配器、修飾器和策略等模式進行對接,最終將原來離散的代碼和配置統一在了一個領域模型上,大大簡化了編程和維護成本,在處理新加入的數據源時變得更加簡便快捷。
最后,補充一點認識,在傳統企業級應用里進行領域驅動設計有諸多的困難,其中一個比較突出的問題就是領域對象的持久化,由於數據存放在關系型數據庫中,領域對象的寫入和加載都存在一個“對象關系映射”的問題,盡管有很多成熟的ORM框架能在一定程度上緩解這個問題,但是在傳統企業級應用里落地一個純正的領域模型依然是一個不小的挑戰,而大數據平台為領域驅動設計提供了一個更加自由的空間,比如大數據的計算節點可以提供足夠的內存將領域對象一次性全部加載,免去了ORM中對關聯對象加載策略的糾結,而領域對象會在大數據處理過程中反復使用,客觀上也需要直接把它們加載到內存中使用,再比如,在業務處理和分析階段,幾乎所有領域對象都是只讀的,它們只會在同步主數據時被更新,這天然地形成了讀寫分離,更加適合CQRS架構。
流處理的工程結構
很多團隊在初次使用流計算框架構建項目時往往會在如何組織工程結構上感到迷茫,不同於傳統企業級應用經過多年積累形成的“套路”,流處理項目的工程結構並沒有一個約定俗成的最佳實踐,我們在這里分享我們的工程結構作為一個參考,希望對你有所啟發。
也許你會覺得這個工程結構非常面熟,是的,我們充分借鑒了傳統企業級應用的分層結構,每一個色塊都代表着一類組件,映射到工程上就是一個package,讓我們逐一介紹一下:
Stream: 系統中的每一個流都會封裝在一個類中,我們把這些類統一按“XxxStream”形式進行命名,放在stream包里,Stream類里出現的多是與Spark Streaming相關的API,在涉及實際的業務處理時,會調用相應的Service方法,這種設計反映了我們對流處理的一個基本認識,那就是流計算中的API是一個“門面”(Facade),厚重的業務處理不應在這些API上直接以Lambda表達式的方式編寫,而應該封裝到專門的Service里。這與Web應用中Action和Service的關系極為類似。
Service: 與業務相關的處理邏輯會封裝到Service類里,這是很傳統的做法,但是由於我們深度地應用了領域驅動設計,所以絕大部分的業務邏輯已經自然地委派到了領域對象的方法上了,因此Service也變成了很薄的一層封裝。有個值得一提的細節,我們把所有的Service都做成了object(sbt-native-packager中的object對象),也就是單態, 這樣做的主要的動機是讓所有的Executor節點在本地加載全局唯一的Service實例,避免Service實例從Driver端到Executor端做無謂的序列化與反序列化操作。
Repository:在相對簡單的系統里,你可以利用Repository直接讀取存放於數據庫中的主數據和配置信息,如果你的平台有多處組件都需要使用主數據,我們建議你務必建立統一的主數據和配置信息讀寫組件,如果是這樣,則專屬於流處理的Repository將不復存在。
Domain:領域模型涉及的實體和值對象都會放在這個包里,業務處理和分析的邏輯會按照面向對象的設計理念分散到領域對象的業務方法上。同樣的,如果建立了統一的主數據和配置信息的讀寫組件,則Domain也將不復存在
DTO: 流處理中的DTO並不是為傳輸領域對象而設計的,它是外部采集的原生數據經過結構化處理之后在流上的數據對象。
項目構建:Sbt vs. Maven
由於我們的平台技術堆棧以Spark為核心,我們的幾個核心組件都是使用scala編寫的,在項目構建上也積累了一些寶貴的經驗,早期我們使用的是scala的默認構建工具sbt, 作為新一代的構建工具,sbt吸收了眾多前輩的優點,簡單易用,能夠滿足基本的應用場景,但在實際的項目構建中,當面臨一些相對復雜的場景時,年青的sbt會顯得比較無力,其中最為我們不能接受的是面向多環境的構建。盡管社區提出過一些解決方案,例如http://stackoverflow.com/questions/17193795/how-to-add-environment-profile-config-to-sbt , 但是這個方案的缺陷在於對於每一套環境都要提供全套的配置,即使它們在多數據環境中的值是一樣的。實際上這個問題的本質原因是sbt尚沒有類似Maven那樣在構建時基於某個配置文件對一些變量進行過濾和替換的Resource+Profile功能,這是很重要的一個需求。
在打包方面,我指的是構建一個包含命令行工具、配置文件和各種lib的安裝包,sbt的sbt-native-packager確實非常強大,令人印象深刻。同樣,在面向不同環境的前提下,打包不同用途的package時,sbt-native-packager的靈活性還有待檢驗。例如,基於我們過去的最佳實踐,面向每一種環境,我們嘗嘗會利用sbt-native-packager構建兩種package,一種是包含全部產出物的標准部署包,一種是僅僅包含每次構建都有可能發生變化的文件,例如項目自身的jar包和一些配置文件,我們把這種包稱為最小化的package,這種package會用於日常持續集成的部署,它的體積很小,在網絡帶寬有限的環境里,它會大大節約部署時間。
回到Maven,在過去數年的開發工作中,Maven滿足了我們各式各樣的構建需求,從沒有讓我們失望過,它的約定大於配置的思想和豐富的周邊插件真正實踐了:”Make simple things simple, complex things possible!”從實際效果看,使用Maven構建sbt-native-packager項目沒有任何障礙,它成熟而強大的各項功能可以解決實際項目上各式各樣的需求,這一切讓我們最終回歸了Maven。
但是這並不代表我們會在Maven上停滯不前,實際上我們對sbt依然抱有期望,只是它需要時間變得更加強大。在未來某個合適的時機,我想我們會遷移到sbt。
數據采集的痛點和應對策略
數據采集往往是大數據平台上的臟活、累活,除了解決技術上的問題,團隊還需要進行大量協調和溝通工作,因為外部數據源都由其他團隊管理,需要從更高的組織層面進行疏通,並且很多數據源需要同時為多個外部系統供給數據,為了確保數據源的可用性,會對外部的數據采集作業進行控制,比如限制采集頻率等。我們下面會討論兩個棘手的問題,並分享我們的解決方案。
數據采集作業超時
在我們采集的外部數據源中,有一個數據庫在某些時刻因為需要同時處理多個外圍系統疊加的查詢請求而經常響應緩慢,進而導致了我們的數據采集作業超時,而這個Job原來的設計是每分鍾執行一次,每次執行時會從目標數據庫中查詢最近1分鍾內的數據,這個查詢請求通常在1秒以內就可以返回,但是當數據庫響應緩慢時,一個Job的耗時往往要超過1分鍾,而后續啟動的Job仍然按啟動時的時間點向前1分鍾作為時間窗口進行查詢,這就出現了數據丟失。
應對這個問題的一個簡單方案是將Job的執行變為異步非阻塞模式,每一個Job被觸發后都在一個獨立的線程中運行。但是這個方案不適用於我們的系統,因為這樣采集到的數據不能保證時間上的有序性,而這對一個時序數據系統至關重要。所以這一方案被否決。
經過仔細的思考,我們認為必須要將這個Job切分成兩個子的Job:第一個Job負責制定周期性的計划,准確地說是周期性地生成時間窗口參數,第二個Job負責讀取時間窗口參數執行查詢,這一部分的工作並不是周期性的,原則上,只要有時間參數生成就應該立即執行,如果執行超時,在超時期間,我們需要緩存第一個Job生成的時間參數,而當所有的查詢都及時完成沒有待執行的查詢計划時,第二個Job需要等待新的查詢參數到達,是的,這實際上是一個生產者-消費者模型,只是生產者是在“有節奏”地生產,在這個模式里,第三個參與者:倉庫,或者說傳送帶,起到了關鍵的調節作用,而一個現成的實現就是JDK自帶的BlockingQueue!於是我們的落地方案是:
第一個Job由定時器周期性觸發,每次觸發時會把當前時間放到一個BlockingQueue的隊尾。
第二個Job循環執行,每次執行的工作就是從BlockingQueue的隊頭取出時間參數,組裝SQL並執行。當隊列為空時,由BlockingQueue來阻塞當前線程,等待時間參數進入隊列。
當第二個Job執行完一次時,如果隊列中還有時間參數,會繼續執行步驟2,發生此類情況時就說明前一次的執行超過了1分鍾。
數據延遲就緒
我們一直為降低平台的數據延遲做着各種努力,但最讓人感到無力的是外部數據源本身在數據寫入時發生了延遲。舉個例子,還是前面提到的數據庫,每次采集數據設定的時間區間是從當前時間到前一分鍾,假定當前時間是00:10,則執行的SQL中時間窗口參數是(00:09,00:10],此時你可能會查詢到1000條數據,但如果你在00:11以同樣的參數(00:09,00:10]再次執行這條SQL, 返回的數據條目就可能變成了1200條,這說明數據庫中的數據從它在業務系統中生成到最后寫入數據庫的過程中發生了延遲,造成這種情況的原因有很多,比如系統存在性能問題等等,總之現狀就是:數據就緒發生了延遲,而對於數據采集方這完全不可控。
面對這種問題,我們的應對策略是:如果數據及時地就緒了,我們要保證能及時的捕獲,如果數據延遲就緒,我們要保證至少不會丟掉它。基於這樣的考慮,我們把同一個數據源的數據采集分成了兩到三個“波次”進行,第一波次的采集緊緊貼近當前時間,並且保持極高的頻率,這一波次是要保證最早最快地采集到當前的新生數據,第二波次采集的是過去某個時間區間上的數據,時間偏移可能在十幾秒到幾分鍾不等,這取決於目標數據源的數據延遲程度,第二波次是一個明顯的“補償”操作,用於采集在第一波次進行時還未在數據庫中就緒的數據,第三波次則是最后的“托底”操作,它的時間偏移會更大,目的是最后一次補錄數據,保證數據的完整性。
多波次采集的方案會導致出現重復數據,因此需要進行去重操作,我們把這個工作交給了流處理組件,利用Spark Streaming的checkpoint機制,我們會在流上cache住近一段時間內的數據作為去重時的比對數據,當超過設定的TTL(Time-To-Live)時,數據會從流上移除。
---------------------
作者:bluishglc
來源:CSDN
原文:https://blog.csdn.net/bluishglc/article/details/79277455
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!