淘寶:TLog的設計

簡介

TLog是一個分佈式的,可靠的,對大量數據進行收集、分析、展現的的系統。主要應用場景是收集大量的運行時日誌,分析並結構化存儲,提供數據查詢和展現。

服務能力

  • 收集淘寶線上8000+臺機器的日誌,每天日誌量5T+
  • 一般情況下,數據從產生到TLog最終入庫有10秒以下的延遲。
  • 整個TLog集羣有12臺機器(虛擬機,5核,8G內存),10臺負責日誌的收集和解析,2臺提供數據的查詢和展現。

技術選型

一個海量數據收集的系統,首先需要考慮的就是收集模型:推送(push),還是拉取(pull)。兩種模式都有各自的優缺點。業界的很多系統都是push模型,比如facebookscribe,而我們主要選擇的是pull模型(push模型後續支持),這個決策和我們所處的環境有關:

TLog集羣可用資源非常有限

選用push模型,就需要要求日誌收集器的容量需要大於高峯期數據的生成量,否則主動推送過來的數據不能及時處理會帶來更多更復雜的問題:比如信息在收集器端如何先暫存慢慢處理,這又牽扯到收集器端是否有這麼多的緩存空間(比如硬盤是否夠大來臨時保存洶涌而至的數據,或者轉移到其他地方的網絡開銷等);如果在日誌生成端臨時保存,則需要有一系列狀態的變化,比如收集器正常則直接發數據,否則則保存本地硬盤,等到收集器恢復了再把硬盤數據發送,然後在恢復到直接發送模式等。

最初TLog集羣只有6臺虛擬機,後期擴展到12臺。硬件處理能力的限制,決定了我們處理海量數據時壓力非常大,如果還選用push模型,在數據生成的高峯期,必然無法處理瞬間大量的日誌。而選擇pull模型,控制權掌握在自己手裏,收集器可以根據自己的節奏遊刃有餘的拉取日誌,高峯期產生的日誌會在接下來的時間慢慢的被消化(當然收集器的能力需要高於日誌產生的平均值)。當然,這樣的缺陷是處理延遲增加了。

信息的時效性

push模型能帶來很高的信息時效性,可以最快的收集、整理,並查詢出來。而我們的先期定位並不是特別在意這樣的實時性,因爲接入的應用主要是使用這些數據做日報、週報等,能夠接受5~10分鐘甚至更張的數據延遲。而且有些環境的約束導致做到秒級別的準實時也沒有意義,比如HSF的哈勃日誌,一個數據單元每2分鐘才輸出一次,從日誌的輸出端就已經造成了2分鐘的延遲了,後面在快也意義不大。所以選用pull模型,在數據高峯期,大量數據臨時擠壓,後期慢慢處理對我們來說是可以接受的。

可靠性

可靠是必須的。衆多push模型的產品在保證可靠性做了很多事情,使得事情變得非常的複雜,比如:

  • 收集器出現異常:正常情況下直接推送消息,如果收集器異常則本地先緩存,待收集器恢復後再把緩存的信息發送,然後再恢復爲正常模式
  • 收集器的選擇:當有多個收集器時,消息推送給誰?是否要負載均衡,是否要對收集器上下線很快的感知,勢必需要引入ConfigServer或者ZK這樣的產品
  • 需要嵌入應用:如果要做到運行時信息不落盤直接發送,需要發送的邏輯嵌入應用,涉及到應用的大面積升級,而且上面提到了發送的邏輯在收集器的問題上變得不是那麼簡單,所以這對應用是很重的負擔。

而選擇pull模型,再借助哈勃Agent這個基礎設施,事情會變得非常簡單!這裏不得不提一下:哈勃Agent是個很不錯的產品,簡單而有效!而且它的存在使得TLog設計和部署變得簡單很多:

  • 收集器無狀態:這一點很重要,保證了TLog的可靠性和簡單的設計。試想如果收集器需要記錄每一個日誌收集的狀態(目標機器地址、日誌文件、日誌抓取偏移量),則收集器在重啓或者掛掉時需要做很多狀態持久化的工作,有時甚至沒有時間來做而直接掛掉,所以又需要又更復雜的方式來近乎實時的記錄狀態;如果任務分配不均,或者其他原因向調動一些任務到其他的機器,又涉及到任務狀態的遷移等等。而狀態記錄在哈勃Agent,收集器只需要通過相關標識請求哈勃Agent,即可獲取增量日誌,如果讓任務調動或者某個收集器掛掉,別的收集器接替其工作,只需要使用同樣的標識即可獲取後續的增量日誌,這樣的結構使得對單個收集器的穩定性要求大大降低,只要整個集羣持續有足夠的資源,即可保證系統的可靠性。而且哈勃Agent足夠的簡單,使其很容易做到非常穩定。所以,不存在push模型的的問題一和問題二。
  • 天然的分佈式存儲:開玩笑的說,TLog有個強大的分佈式存儲,即目標服務器本身的硬盤空間,每個服務器生成的日誌直接落盤,而且硬盤空間又足夠保持一定時間內的日誌,收集器在之後的某個時刻讀取這些信息,如果處理失敗還可以根據上次日誌抓取的偏移量再抓取一次或者保存再某處做特殊處理。這比所有日誌都堆積到十幾臺收集器的硬盤上要可靠的多,也簡單的多。
  • 部署和依賴簡單:幾乎淘寶所有服務器都有部署哈勃Agent,即使沒有也很方便的能安裝。應用不需要嵌入發送日誌信息的邏輯,只需要簡單的記錄日誌即可,對應用幾乎零侵入。所以,對push模型的問題三解決了。

當然,選擇pull模型也是有自己的問題:

  • 日誌收集任務的管理:因爲信息不會主動推送過來,所以需要自己記得去哪裏取。收集任務的管理是個不小的問題:比如有些任務鏈接會失敗(比如哈勃Agent沒有部署,需要找PE解決);有些任務會忙不過來,需要增加處理器節點(如某個任務的收集器負荷重導致收集頻率降低,長時間沒有抓取動作);新的應用接入需要配置相關的抓取任務,應用的服務器變更後相關的任務也需要變更等,雖然很多都做了自動化處理(比如定時同步Armory來獲取應用和ip的映射關係等),但不得不承認任務的管理是個不小的負擔。

技術挑戰

TLog做的事情非常簡單,但是再海量數據的衝擊下,系統很容易變得千瘡百孔。

JVM內存溢出

TLog首先遇到的問題就是OOM。收集器所在的虛擬機,15MB/秒的數據流入10MB/秒的數據流出(這還是平常業務壓力不大的時候)。很容易想象,10+MB的數據解析成大量的對象,稍微處理不好就會導致大量的JVM堆內存被佔用,很容易OOM。結合應用自身的狀況,經過很多嘗試,最終找到了解決辦法,這也讓我對很多東西有了新的認識:

線程池的大小

線程池的大小對於TLog來說不是性能的問題,而是會不會死人的問題。線程池在TLog內部主要是任務調度使用(Quarz),每一個日誌收集任務啓動會佔用一個線程,後續的所有動作都在這個線程完成:收集一批增量日誌;使用不同的解析器把日誌解析成結構化對象;持久化(入HBase或者雲梯或者消息中間件)。這樣的劃分方式使得線程之間沒有任何通信(也就沒有鎖的競爭),有因爲整個處理任務的兩頭有大量的IO動作(拉取日誌和持久化),中間過程是純CPU運算(解析),所以多個線程大家互補忙閒能做到很高的效率(CPUIO雙忙……)。

但是線程池開多少?當初拍腦袋定了200,結果只要日誌有積壓(業務高峯,或者TLog下線一段時間)TLog直接OOM。中間甚至使用過“延遲啓動任務”的方式,即收集器把任務以一定間隔(比如2秒)一個一個啓動,有一定效果,但還是很容易掛,而且一個收集器一般會有5k+個任務,兩秒啓動一個的話……這很顯然不靠譜。分析了狀況後,發現事情是這樣的:

  1. 收集器開啓一個任務抓取日誌,返回的量很大(比如10MB),拿到內存裏開始解析,解析完開始保存。
  1. 於此同時,其他的任務也啓動起來,做同樣的動作,拉日誌,解析,保存。
  2. 因爲線程池有200個線程,意味着同時允許200個任務在運行,而我後續dump內存發現,一個線程運行的時候,會佔用20~40MB不等的JVM內存。而總共JVM堆有5G,還沒到200個線程運行內存就不夠了,然後開始GC,但是GC掉的內存不多,因爲很多線程剛剛處於數據保存階段,數據在最終入庫前是不可能GC的,所以港GC完內存又瞬間用光然後再次GC,或者就乾脆OOM了。

原本很簡單的事情(拉日誌,解析,入庫)變得無法穩定運行,經過一步步測試,最終把線程池大小控制在30(後續因爲邏輯更加複雜,但任務佔用內存量又增加,而調整到25),之所以調整到這麼小是因爲如果再大,比如35個任務同時處理,就會導致內存佔用非常緊張(雖說有5G的堆內存,oldedens1s0分分就沒多少了),導致Full GC,但又沒有成果,GC完內存一樣不夠用,就再Full GC,結果導致90%以上的時間都在做無用的GC。那還不如把內存控制的留點餘量,不至於頻繁觸發FGC,而留下大量的時間專心幹活呢。當然除此之外還有很多其他的優化,比如把先批量解析再批量保存改爲邊解析邊保存,保存過後的對象就可以被GC了,降低對象的存活時間。另外一個很重要的點:通過哈勃Agent拉取的一批增量日誌一下都被加載到內存中,隨後慢慢的解析處理。在極端情況下只要這批日誌沒有處理完,就會有10MB(哈勃Agent單次拉取日誌的上限)的字節無法釋放。應該改爲流式的處理,讀一部分處理一部分,然後再讀取下一部分。但因爲這個改動對整個解析結構會有很大的調整,所以就放到了後面遷移Storm時統一做了修改,整個jvm內存佔用量減少一半左右,不再成爲系統的瓶頸。

經過了一番改動後,TLog變得“壓不死”了,即使積壓了大量的數據,啓動後網絡流量涌入30MB/每秒,cpu會穩定再85%,系統load 40(誇張的時候有80),但是很穩定,不到2秒一次YGC10分鐘一次FGC,系統可以保持這樣的壓力運行6個小時,當積壓的所有數據都處理完後,機器的負載,cpu及內存的佔用自動恢復到正常水平。

JVM GC參數

最初TLog使用的GC方式是CMS GC,花了不少經歷調整edenold的比例,以及觸發CMS GC的比率。但後來覺得這樣做沒有必要。因爲CMS GC是爲高相應系統設計的,使Stop-the-world時間儘量短,使得系統持續保持較高的相應速度,但付出的代價就是GC效率低。而TLog選用的是pull模型,不會有系統主動請求,所以不需要保證高相應,應該更加看中GC效率,所以後續改成了並行GC,提高GC效率,從而獲得更多的“工作時間”。

另外edenold的比例對收集器也非常重要,從工作方式可以看到,收集器必定會大量的產生對象,和大量的銷燬對象,而且這些對象還是會在內存中保留一定的時間,所以要求eden區稍微大些,以保證這些臨時對象在晉升到old之前就被回收掉,而相對穩定的數據在收集器中比較少。所以eden空間設置的比old要大。

HBase寫效率

隨着業務量的增長,馬上遇到了一個之前從沒有想象過的問題:HBase寫速度不夠。當初之所以選擇HBase爲後端存儲,就是因爲其寫入速度很高,能保證大量的數據快速的入庫。先前的測試也驗證了這一點:HBase單機扛住了我們每秒5萬條記錄的衝擊。所以我們樂觀的估計,有着30臺機器的集羣抗100萬的量應該小case,但是隨着越來越多的應用使用HBase集羣,以及整個集羣數據量的增加及region數量的增多,HBase寫效率不斷下降,同樣保存1k條記錄的耗時從原來的不到100毫秒變成了將近1秒,有時甚至超過2秒。直到一個週六的晚上,整個集羣寫入耗時忽然急劇上升,導致整個集羣所有應用的寫入量被迫下降到10/秒左右,最後無奈關閉了Eagleeye的數據表,整個集羣才恢復,原因很簡單:Eagleeye數據表的region數量將近1萬個,佔整個集羣region數量的80%region server壓力過大。至此Eagleeye實時數據就暫停下來,全部轉爲離線處理。

EagleeyeTLog最大的一個接入方,其數據量佔TLog所有業務的80%,每天日誌量5T左右。HBase上的數據表被關閉,一部分原因是數據量的確太大,另外我覺得應該是我們使用HBase的方式不夠得當,還有優化的空間。所以我開始尋找業界的解決方案,發現了OpenTSDB

OpenTSDB

"OpenTSDB is a distributed, scalable Time SeriesDatabase (TSDB) written on top of HBase."(官方說明)。學習了OpenTSDBSchema設計,發現很多東西都值得學習和借鑑,根據多面對的場景,OpenTSDBSchema的精巧設計,使得其記錄體積非常小,而且row的數量很少,這都能降低HBaseregion server的壓力,從而提高數據庫的寫和讀的效率。將HSF的數據改爲OpenTSDB的方式後,同樣的信息量,數據體積減少了80%以上(rowkey體積減少50%value體積減少80%,數據條數減少66%),rowkey數量減少97%。直接效果就是寫入速度更快,吞吐量更高,而且HBase服務器的壓力更小!但這隻適用於Time Series類型的數據,比如HSF、精衛等數值統計型的場景。對於EagleeyeTAE這種日誌記錄類型的不適用,但仍然又很多可以借鑑和改進之處。

流式處理的代價

下半年,TLog的收集器遷移到Storm流式處理平臺。日誌收集器兼顧了收集、解析、入庫的職責,而且解析期間經常需要對信息進行分類,過濾,彙總等,非常適合使用流式處理框架完成這些工作。但遷移到Storm後一樣遇到了各式各樣的挑戰:

額外的消耗

Storm中,每個處理節點可以認爲是一個運算單元,數據在這些單元中流轉,一級的輸出作爲另一級的輸入。對於TLog的解析器來說,感覺理想的方式應該是這樣的:

  1. Spout節點(Storm中的數據源節點)拉取日誌內容,然後發射出去
  2. 下游節點接收日誌內容,進行解紛並結構化成對象,然後發射出去
    1. 下游節點接收結構化對象,進行相關存儲(HBase或者雲梯)
    1. 另一類下游節點接收結構化對象進行相關的聚合,比如根據某種類型進行累加

上面的處理方式感覺非常清晰明瞭,但是卻產生了大量的“額外消耗------對象的序列化和網絡傳輸。數據在每個節點流轉都需要經過序列化和反序列化操作(消耗CPU),還有網絡傳輸(消耗IO),而且根據上面的設計,幾乎從spout流出的數據會100%的跳轉多個節點,也就使得一份數據造成N倍的網絡傳輸,網絡消耗非常嚴重。所以我們制定了一個簡單的原則:只要沒有聚合需求,就在一個節點完成。因爲集羣數據的聚合使用普通方式比較難解決,而使用storm非常天然的處理掉。

對避免不了的數據流轉,storm還是有辦法降低額外的消耗,比如:

  • 優化序列化方式storm可以使用kryo的序列化方式,cpu消耗和序列化後的體積會比java自身的序列化好很多,可以參考這裏的比較。
  • 進程內流轉不做序列化和網絡傳輸處理storm 0.7.2版本做了優化,如果數據是在一個進程內流轉,則跳過序列化和網絡傳輸步驟,這樣能極大的減輕額外的消耗。但是這需要使得多個節點在一個進程,會使得進程的龐大,導致機器storm worker進程數減少,可能造成負載不均衡的情況,所以一臺機器開多少個worker需要根據機器的配置,以及任務的複雜度,以及任務數量來權衡。

可靠消息和非可靠消息的選擇

Storm爲了保證流轉消息的可靠性,引入第三視角的節點Acker,來跟蹤每一條消息,當下遊處理失敗後能通知上游,上游可以有自己的策略進行處理(例如重發消息)。但是Acker的引入也必然有開銷(大量的Ack消息),導致業務可用的資源減少,而且會降低消息處理的性能。TLog處理器未啓用可靠消息時,每個節點處理消息的速度是11k/s,打開可靠消息後只能有3~4k/s,下降非常明顯。因爲TLog處理的是大量的日誌信息,處於從數據可靠的敏感程度,和資源限制的情況下,我們選擇了非可靠消息。

但事情並沒有這樣簡單的結束,我們的集羣經常出現個別進程內存狂漲,消耗掉所有的內存甚至swap分區,然後操作系統啓動自我保護性的隨機kill進程,導致這個“異常”進程被殺死;或者整個虛擬機掛掉。出現這個問題的原因是我們生產消息(Stromspout端)的速度大於消費消息(Strombolt端)的速度,導致消息積壓在spout端的出口處,使得spout所在的進程內存佔用上升(順便提一下,Storm使用的消息組件是ØMQ,非java組件,所以消息的堆積無法從jvm堆體積中體現出來)。而Storm可以通過設置“topology.max.spout.pending”來設置積壓消息的最大值,但是這個特性只有在“可靠消息”時纔有意義。所以對於非可靠消息,只能提高後續節點的處理能力(比如增加節點數量)來解決。

實時和離線相結合

對於運行時數據,一般情況下我們的場景如下:

  • 近期的實時數據:對於這部分數據,我們的需求是查詢時間跨度小(近1小時或近10分鐘),時間粒度細(每分鐘甚至幾十秒一個數據單元),能夠準實時的展現(數據從產生到最終展現的延遲可能只有幾秒或十幾秒)。
  • 過往的歷史數據:對於歷史數據,我們的查詢的時間跨度一般比較大(一週、一個月等),但時間粒度較粗(一小時甚至一天一個數據單元),不在乎實時性。

所以對數據粒度的需求會隨着時間的流式而變粗(ps:你應該不會需要查看上個月3號上午10點~10點半,以分鐘爲粒度展現一個服務的調用量。如果真的需要,這應該是一個特殊情況,相關的報警系統應該會沉澱該信息)。所以從“如何“打敗”CAP定理一文得到的思路,我們使用實時和離線相結合的方法來解決一下需求:

實時部分

準實時的處理最新的數據,以小粒度保存(甚至可以直接緩存起來),方便查詢和檢索。但實時處理數據有一些問題:

  • 易錯:實時處理一般使用流式處理,大量數據批量涌入又快速的處理輸出,很容易出現不確定性或錯誤。如數據因爲收集的不同步,導致加和時上一分鐘的日誌被加和到下一分鐘;或者因爲短暫的暫停服務導致數據出現缺口;或者一個新上的算法有缺陷導致計算錯誤等。
  • 無法重複計算:因爲數據快速的流動,如果有消息重發機制就意味着一定有個池子來緩存,以保證下游處理失敗而重發。緩存池的存在又增加了資源、性能、複雜度等的極大提高。
  • 數據量太大:因爲數據的時間粒度太細,使得數據量非常大,存儲和查詢代價很高。

所以我們的做法是在實時部分允許有這樣細小的問題,問題的修復由離線批量計算解決。

離線批量處理部分

使用MapReduce來計算一段時間(前一小時或一天)彙總的數據,很容易解決實時計算是出現的問題:

  • 大時間跨度的合併:單條數據彙總時間跨度較大,極大減小了數據的體積,對存儲和大時間跨度查詢友好。
  • 替代“過時”的實時數據:實時計算的結果會被批量計算覆蓋或替代,當時產生的細小錯誤也自然消失。
  • 容錯性高:因爲原始數據已經保存,使用MR可以重複計算,而且計算結果穩定。即使出現算法錯誤,修復後仍然可以重新計算。

而離線批量處理的唯一問題-----實時性-----也被實時計算彌補。

數據說話

前面提到了很多系統優化和調整的方式,但一定要記得“要進行優化,先得找到性能瓶頸!”,根據Profiler的結果來確定優化的方向。對於TLog收集器,使用了Java自帶的VisualVM,很快定位到幾個最大的cpu消耗點:

  • 反射構造對象:爲了方便構造存儲HBase的結構化對象,我們開發了一套註解,通過在Model對象屬性上標記註解,可以自動轉換成需要的HBase對象,使用起來非常方便,但是轉換過程沒有緩存Model的類結構,導致大量的使用反射。這樣生成對象的速度比直接代碼構造要慢了一個數量級。
  • 正則表達式匹配:收集器中有大量的日誌匹配和解析,所以當時用了很多正則表達式,雖然緩存了pattern,但是還是非常的消耗cpu,最後把一些非常規範的日誌都使用StringUtils.split()方法進行分割然後處理,也能提大幅度提高解析速度。
  • 切分字符串Java Stringsplit方法內部使用了曾則表達式,如果切分字符不需要正則匹配,建議使用Apache commons langStringUtils。當然,這個的調整帶來的提高遠不如前兩個大。

經過一次次Profile和調整,最終基本只剩下無法避開的消耗,處理器容量提高4倍以上!遺憾的是當時前後的詳細對比數據沒有保留,無法列在這裏提供參考。

其他

下面是一些零碎的小心得。

HBase rowkey的唯一

TLog收集到的一些信息不是TimeSeries類型的,不能做加和等處理,而是要根據日誌內容生成唯一的個體,比如操作日誌,需要能根據時間和操作類型查詢操作的具體情況。對於這類需求有個細節:HBase rowkey的生成該如何保證唯一。因爲rowkey會由索引條件構成,如日誌類型、時間,但僅僅這樣的rowkey很容易重複,導致之前的記錄被覆蓋。當然可以在rowkey後面增加一個唯一後綴進行區分,比如下面幾種方式:

  • 增加相關區別標識:比如增加日誌生成機器ip,或者其他什麼比較容易區別的業務字段信息來進行去重,但是會發現後綴元素加的少很難達到效果,加的多又使得rowkey變得很龐大,甚至只有把所有日誌內容都放在rowkey裏才能達到去重目的。
  • 增加遞增量:增加一個自增的變量,但是這樣的缺陷是數據無法補救,即如果因爲某種原因,昨天一天的數據想重新導入一邊,昨天已有的覆蓋掉,沒有的補上,你會發現自增量的存在導致你沒辦法做這個事情。

我們的處理方式:將日誌原文進行CRC-32編碼,生成816進制的值,附加在rowkey的末尾,即保證了rowkey不會過度的膨脹(最多8個字符的長度),又保證了低重複率(CRC-32碰撞機率相對較低),而且可以支持數據的重複導入(相同記錄計算的編碼一樣)。

發佈了15 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章