以下是2012年一個公司內(nèi)部項目的技術(shù)總結(jié),涉及到的方面比較多比較雜,拿出來和大家分享下。如果有更好的方案或者想法請聯(lián)系我,謝謝~! 簡介TLog是一個分布式的,可靠的,對大量數(shù)據(jù)進行收集、分析、展現(xiàn)的的系統(tǒng)。主要應用場景是收集大量的運行時日志,分析并結(jié)構(gòu)化存儲,提供數(shù)據(jù)查詢和展現(xiàn)。 服務能力
技術(shù)選型一個海量數(shù)據(jù)收集的系統(tǒng),首先需要考慮的就是收集模型:推送(push),還是拉取(pull)。兩種模式都有各自的優(yōu)缺點。業(yè)界的很多系統(tǒng)都是push模型,比如facebook的scribe,而我們主要選擇的是pull模型(push模型后續(xù)支持),這個決策和我們所處的環(huán)境有關(guān): TLog集群可用資源非常有限選用push模型,就需要要求日志收集器的容量需要大于高峰期數(shù)據(jù)的生成量,否則主動推送過來的數(shù)據(jù)不能及時處理會帶來更多更復雜的問題:比如信息在收集器端如何先暫存慢慢處理,這又牽扯到收集器端是否有這么多的緩存空間(比如硬盤是否夠大來臨時保存洶涌而至的數(shù)據(jù),或者轉(zhuǎn)移到其他地方的網(wǎng)絡開銷等);如果在日志生成端臨時保存,則需要有一系列狀態(tài)的變化,比如收集器正常則直接發(fā)數(shù)據(jù),否則則保存本地硬盤,等到收集器恢復了再把硬盤數(shù)據(jù)發(fā)送,然后在恢復到直接發(fā)送模式等。 最初TLog集群只有6臺虛擬機,后期擴展到12臺。硬件處理能力的限制,決定了我們處理海量數(shù)據(jù)時壓力非常大,如果還選用push模型,在數(shù)據(jù)生成的高峰期,必然無法處理瞬間大量的日志。而選擇pull模型,控制權(quán)掌握在自己手里,收集器可以根據(jù)自己的節(jié)奏游刃有余的拉取日志,高峰期產(chǎn)生的日志會在接下來的時間慢慢的被消化(當然收集器的能力需要高于日志產(chǎn)生的平均值)。當然,這樣的缺陷是處理延遲增加了。 信息的時效性push模型能帶來很高的信息時效性,可以最快的收集、整理,并查詢出來。而我們的先期定位并不是特別在意這樣的實時性,因為接入的應用主要是使用這些數(shù)據(jù)做日報、周報等,能夠接受5~10分鐘甚至更張的數(shù)據(jù)延遲。而且有些環(huán)境的約束導致做到秒級別的準實時也沒有意義,比如HSF的哈勃日志,一個數(shù)據(jù)單元每2分鐘才輸出一次,從日志的輸出端就已經(jīng)造成了2分鐘的延遲了,后面在快也意義不大。所以選用pull模型,在數(shù)據(jù)高峰期,大量數(shù)據(jù)臨時擠壓,后期慢慢處理對我們來說是可以接受的。 可靠性可靠是必須的。眾多push模型的產(chǎn)品在保證可靠性做了很多事情,使得事情變得非常的復雜,比如:
而選擇pull模型,再借助哈勃Agent這個基礎(chǔ)設施,事情會變得非常簡單!這里不得不提一下:哈勃Agent是個很不錯的產(chǎn)品,簡單而有效!而且它的存在使得TLog設計和部署變得簡單很多:
當然,選擇pull模型也是有自己的問題:
技術(shù)挑戰(zhàn)TLog做的事情非常簡單,但是再海量數(shù)據(jù)的沖擊下,系統(tǒng)很容易變得千瘡百孔。 JVM內(nèi)存溢出TLog首先遇到的問題就是OOM。收集器所在的虛擬機,15MB/秒的數(shù)據(jù)流入10MB/秒的數(shù)據(jù)流出(這還是平常業(yè)務壓力不大的時候)。很容易想象,10+MB的數(shù)據(jù)解析成大量的對象,稍微處理不好就會導致大量的JVM堆內(nèi)存被占用,很容易OOM。結(jié)合應用自身的狀況,經(jīng)過很多嘗試,最終找到了解決辦法,這也讓我對很多東西有了新的認識: 線程池的大小線程池的大小對于TLog來說不是性能的問題,而是會不會死人的問題。線程池在TLog內(nèi)部主要是任務調(diào)度使用(Quarz),每一個日志收集任務啟動會占用一個線程,后續(xù)的所有動作都在這個線程完成:收集一批增量日志;使用不同的解析器把日志解析成結(jié)構(gòu)化對象;持久化(入HBase或者云梯或者消息中間件)。這樣的劃分方式使得線程之間沒有任何通信(也就沒有鎖的競爭),有因為整個處理任務的兩頭有大量的IO動作(拉取日志和持久化),中間過程是純CPU運算(解析),所以多個線程大家互補忙閑能做到很高的效率(CPU和IO雙忙……)。 但是線程池開多少?當初拍腦袋定了200,結(jié)果只要日志有積壓(業(yè)務高峰,或者TLog下線一段時間)TLog直接OOM。中間甚至使用過“延遲啟動任務”的方式,即收集器把任務以一定間隔(比如2秒)一個一個啟動,有一定效果,但還是很容易掛,而且一個收集器一般會有5k+個任務,兩秒啟動一個的話……這很顯然不靠譜。分析了狀況后,發(fā)現(xiàn)事情是這樣的:
原本很簡單的事情(拉日志,解析,入庫)變得無法穩(wěn)定運行,經(jīng)過一步步測試,最終把線程池大小控制在30(后續(xù)因為邏輯更加復雜,但任務占用內(nèi)存量又增加,而調(diào)整到25),之所以調(diào)整到這么小是因為如果再大,比如35個任務同時處理,就會導致內(nèi)存占用非常緊張(雖說有5G的堆內(nèi)存,old、eden、s1和s0分分就沒多少了),導致Full GC,但又沒有成果,GC完內(nèi)存一樣不夠用,就再Full GC,結(jié)果導致90%以上的時間都在做無用的GC。那還不如把內(nèi)存控制的留點余量,不至于頻繁觸發(fā)FGC,而留下大量的時間專心干活呢。當然除此之外還有很多其他的優(yōu)化,比如把先批量解析再批量保存改為邊解析邊保存,保存過后的對象就可以被GC了,降低對象的存活時間。另外一個很重要的點:通過哈勃Agent拉取的一批增量日志一下都被加載到內(nèi)存中,隨后慢慢的解析處理。在極端情況下只要這批日志沒有處理完,就會有10MB(哈勃Agent單次拉取日志的上限)的字節(jié)無法釋放。應該改為流式的處理,讀一部分處理一部分,然后再讀取下一部分。但因為這個改動對整個解析結(jié)構(gòu)會有很大的調(diào)整,所以就放到了后面遷移Storm時統(tǒng)一做了修改,整個jvm內(nèi)存占用量減少一半左右,不再成為系統(tǒng)的瓶頸。 經(jīng)過了一番改動后,TLog變得“壓不死”了,即使積壓了大量的數(shù)據(jù),啟動后網(wǎng)絡流量涌入30MB/每秒,cpu會穩(wěn)定再85%,系統(tǒng)load 40(夸張的時候有80),但是很穩(wěn)定,不到2秒一次YGC,10分鐘一次FGC,系統(tǒng)可以保持這樣的壓力運行6個小時,當積壓的所有數(shù)據(jù)都處理完后,機器的負載,cpu及內(nèi)存的占用自動恢復到正常水平。 JVM GC參數(shù)最初TLog使用的GC方式是CMS GC,花了不少經(jīng)歷調(diào)整eden和old的比例,以及觸發(fā)CMS GC的比率。但后來覺得這樣做沒有必要。因為CMS GC是為高相應系統(tǒng)設計的,使Stop-the-world時間盡量短,使得系統(tǒng)持續(xù)保持較高的相應速度,但付出的代價就是GC效率低。而TLog選用的是pull模型,不會有系統(tǒng)主動請求,所以不需要保證高相應,應該更加看中GC效率,所以后續(xù)改成了并行GC,提高GC效率,從而獲得更多的“工作時間”。 另外eden和old的比例對收集器也非常重要,從工作方式可以看到,收集器必定會大量的產(chǎn)生對象,和大量的銷毀對象,而且這些對象還是會在內(nèi)存中保留一定的時間,所以要求eden區(qū)稍微大些,以保證這些臨時對象在晉升到old之前就被回收掉,而相對穩(wěn)定的數(shù)據(jù)在收集器中比較少。所以eden空間設置的比old要大。 HBase寫效率隨著業(yè)務量的增長,馬上遇到了一個之前從沒有想象過的問題:HBase寫速度不夠。當初之所以選擇HBase為后端存儲,就是因為其寫入速度很高,能保證大量的數(shù)據(jù)快速的入庫。先前的測試也驗證了這一點:HBase單機扛住了我們每秒5萬條記錄的沖擊。所以我們樂觀的估計,有著30臺機器的集群抗100萬的量應該小case,但是隨著越來越多的應用使用HBase集群,以及整個集群數(shù)據(jù)量的增加及region數(shù)量的增多,HBase寫效率不斷下降,同樣保存1k條記錄的耗時從原來的不到100毫秒變成了將近1秒,有時甚至超過2秒。直到一個周六的晚上,整個集群寫入耗時忽然急劇上升,導致整個集群所有應用的寫入量被迫下降到10萬/秒左右,最后無奈關(guān)閉了Eagleeye的數(shù)據(jù)表,整個集群才恢復,原因很簡單:Eagleeye數(shù)據(jù)表的region數(shù)量將近1萬個,占整個集群region數(shù)量的80%,region server壓力過大。至此Eagleeye實時數(shù)據(jù)就暫停下來,全部轉(zhuǎn)為離線處理。 Eagleeye是TLog最大的一個接入方,其數(shù)據(jù)量占TLog所有業(yè)務的80%,每天日志量5T左右。HBase上的數(shù)據(jù)表被關(guān)閉,一部分原因是數(shù)據(jù)量的確太大,另外我覺得應該是我們使用HBase的方式不夠得當,還有優(yōu)化的空間。所以我開始尋找業(yè)界的解決方案,發(fā)現(xiàn)了OpenTSDB。 OpenTSDB"OpenTSDB is a distributed, scalable Time Series Database (TSDB) written on top of HBase."(官方說明)。學習了OpenTSDB的Schema設計,發(fā)現(xiàn)很多東西都值得學習和借鑒,根據(jù)多面對的場景,OpenTSDB對Schema的精巧設計,使得其記錄體積非常小,而且row的數(shù)量很少,這都能降低HBase和region server的壓力,從而提高數(shù)據(jù)庫的寫和讀的效率。將HSF的數(shù)據(jù)改為OpenTSDB的方式后,同樣的信息量,數(shù)據(jù)體積減少了80%以上(rowkey體積減少50%,value體積減少80%,數(shù)據(jù)條數(shù)減少66%),rowkey數(shù)量減少97%。直接效果就是寫入速度更快,吞吐量更高,而且HBase服務器的壓力更??!但這只適用于Time Series類型的數(shù)據(jù),比如HSF、精衛(wèi)等數(shù)值統(tǒng)計型的場景。對于Eagleeye和TAE這種日志記錄類型的不適用,但仍然又很多可以借鑒和改進之處。 流式處理的代價下半年,TLog的收集器遷移到Storm流式處理平臺。日志收集器兼顧了收集、解析、入庫的職責,而且解析期間經(jīng)常需要對信息進行分類,過濾,匯總等,非常適合使用流式處理框架完成這些工作。但遷移到Storm后一樣遇到了各式各樣的挑戰(zhàn): 額外的消耗在Storm中,每個處理節(jié)點可以認為是一個運算單元,數(shù)據(jù)在這些單元中流轉(zhuǎn),一級的輸出作為另一級的輸入。對于TLog的解析器來說,感覺理想的方式應該是這樣的:
上面的處理方式感覺非常清晰明了,但是卻產(chǎn)生了大量的“額外消耗------對象的序列化和網(wǎng)絡傳輸。數(shù)據(jù)在每個節(jié)點流轉(zhuǎn)都需要經(jīng)過序列化和反序列化操作(消耗CPU),還有網(wǎng)絡傳輸(消耗IO),而且根據(jù)上面的設計,幾乎從spout流出的數(shù)據(jù)會100%的跳轉(zhuǎn)多個節(jié)點,也就使得一份數(shù)據(jù)造成N倍的網(wǎng)絡傳輸,網(wǎng)絡消耗非常嚴重。所以我們制定了一個簡單的原則:只要沒有聚合需求,就在一個節(jié)點完成。因為集群數(shù)據(jù)的聚合使用普通方式比較難解決,而使用storm非常天然的處理掉。 對避免不了的數(shù)據(jù)流轉(zhuǎn),storm還是有辦法降低額外的消耗,比如:
可靠消息和非可靠消息的選擇Storm為了保證流轉(zhuǎn)消息的可靠性,引入第三視角的節(jié)點Acker,來跟蹤每一條消息,當下游處理失敗后能通知上游,上游可以有自己的策略進行處理(例如重發(fā)消息)。但是Acker的引入也必然有開銷(大量的Ack消息),導致業(yè)務可用的資源減少,而且會降低消息處理的性能。TLog處理器未啟用可靠消息時,每個節(jié)點處理消息的速度是11k/s,打開可靠消息后只能有3~4k/s,下降非常明顯。因為TLog處理的是大量的日志信息,處于從數(shù)據(jù)可靠的敏感程度,和資源限制的情況下,我們選擇了非可靠消息。 但事情并沒有這樣簡單的結(jié)束,我們的集群經(jīng)常出現(xiàn)個別進程內(nèi)存狂漲,消耗掉所有的內(nèi)存甚至swap分區(qū),然后操作系統(tǒng)啟動自我保護性的隨機kill進程,導致這個“異?!边M程被殺死;或者整個虛擬機掛掉。出現(xiàn)這個問題的原因是我們生產(chǎn)消息(Strom的spout端)的速度大于消費消息(Strom的bolt端)的速度,導致消息積壓在spout端的出口處,使得spout所在的進程內(nèi)存占用上升(順便提一下,Storm使用的消息組件是?MQ,非Java組件,所以消息的堆積無法從jvm堆體積中體現(xiàn)出來)。而Storm可以通過設置“topology.max.spout.pending”來設置積壓消息的最大值,但是這個特性只有在“可靠消息”時才有意義。所以對于非可靠消息,只能提高后續(xù)節(jié)點的處理能力(比如增加節(jié)點數(shù)量)來解決。 實時和離線相結(jié)合對于運行時數(shù)據(jù),一般情況下我們的場景如下:
所以對數(shù)據(jù)粒度的需求會隨著時間的流式而變粗(ps:你應該不會需要查看上個月3號上午10點~10點半,以分鐘為粒度展現(xiàn)一個服務的調(diào)用量。如果真的需要,這應該是一個特殊情況,相關(guān)的報警系統(tǒng)應該會沉淀該信息)。所以從“如何“打敗”CAP定理”一文得到的思路,我們使用實時和離線相結(jié)合的方法來解決一下需求: 實時部分準實時的處理最新的數(shù)據(jù),以小粒度保存(甚至可以直接緩存起來),方便查詢和檢索。但實時處理數(shù)據(jù)有一些問題:
所以我們的做法是在實時部分允許有這樣細小的問題,問題的修復由離線批量計算解決。 離線批量處理部分使用MapReduce來計算一段時間(前一小時或一天)匯總的數(shù)據(jù),很容易解決實時計算是出現(xiàn)的問題:
而離線批量處理的唯一問題----- 數(shù)據(jù)說話前面提到了很多系統(tǒng)優(yōu)化和調(diào)整的方式,但一定要記得“要進行優(yōu)化,先得找到性能瓶頸!”,根據(jù)Profiler的結(jié)果來確定優(yōu)化的方向。對于TLog收集器,使用了Java自帶的VisualVM,很快定位到幾個最大的cpu消耗點:
經(jīng)過一次次Profile和調(diào)整,最終基本只剩下無法避開的消耗,處理器容量提高4倍以上!遺憾的是當時前后的詳細對比數(shù)據(jù)沒有保留,無法列在這里提供參考。 其他下面是一些零碎的小心得。 HBase rowkey的唯一TLog收集到的一些信息不是Time Series類型的,不能做加和等處理,而是要根據(jù)日志內(nèi)容生成唯一的個體,比如操作日志,需要能根據(jù)時間和操作類型查詢操作的具體情況。對于這類需求有個細節(jié):HBase rowkey的生成該如何保證唯一。因為rowkey會由索引條件構(gòu)成,如日志類型、時間,但僅僅這樣的rowkey很容易重復,導致之前的記錄被覆蓋。當然可以在rowkey后面增加一個唯一后綴進行區(qū)分,比如下面幾種方式:
我們的處理方式:將日志原文進行CRC-32編碼,生成8位16進制的值,附加在rowkey的末尾,即保證了rowkey不會過度的膨脹(最多8個字符的長度),又保證了低重復率(CRC-32碰撞幾率相對較低),而且可以支持數(shù)據(jù)的重復導入(相同記錄計算的編碼一樣)。 |
|