在《基於Flume的美團日誌收集系統(一)架構和設計》中,咱們詳述了基於Flume的美團日誌收集系統的架構設計,以及爲何作這樣的設計。在本節中,咱們將會講述在實際部署和使用過程當中遇到的問題,對Flume的功能改進和對系統作的優化。web
在Flume的使用過程當中,遇到的主要問題以下:正則表達式
a. Channel「水土不服」:使用固定大小的MemoryChannel在日誌高峯時常報隊列大小不夠的異常;使用FileChannel又致使IO繁忙的問題;緩存
b. HdfsSink的性能問題:使用HdfsSink向Hdfs寫日誌,在高峯時間速度較慢;服務器
c. 系統的管理問題:配置升級,模塊重啓等;架構
從上面的問題中能夠看到,有一些需求是原生Flume沒法知足的,所以,基於開源的Flume咱們增長了許多功能,修改了一些Bug,而且進行一些調優。下面將對一些主要的方面作一些說明。app
一方面,Flume自己提供了http, ganglia的監控服務,而咱們目前主要使用zabbix作監控。所以,咱們爲Flume添加了zabbix監控模塊,和sa的監控服務無縫融合。分佈式
另外一方面,淨化Flume的metrics。只將咱們須要的metrics發送給zabbix,避免 zabbix server形成壓力。目前咱們最爲關心的是Flume可否及時把應用端發送過來的日誌寫到Hdfs上, 對應關注的metrics爲:ide
首先,咱們的HdfsSink寫到hadoop的文件採用lzo壓縮存儲。 HdfsSink能夠讀取hadoop配置文件中提供的編碼類列表,而後經過配置的方式獲取使用何種壓縮編碼,咱們目前使用lzo壓縮數據。採用lzo壓縮而非bz2壓縮,是基於如下測試數據:oop
event大小(Byte) | sink.batch-size | hdfs.batchSize | 壓縮格式 | 總數據大小(G) | 耗時(s) | 平均events/s | 壓縮後大小(G) |
---|---|---|---|---|---|---|---|
544 | 300 | 10000 | bz2 | 9.1 | 2448 | 6833 | 1.36 |
544 | 300 | 10000 | lzo | 9.1 | 612 | 27333 | 3.49 |
其次,咱們的HdfsSink增長了建立lzo文件後自動建立index功能。Hadoop提供了對lzo建立索引,使得壓縮文件是可切分的,這樣Hadoop Job能夠並行處理數據文件。HdfsSink自己lzo壓縮,但寫完lzo文件並不會建索引,咱們在close文件以後添加了建索引功能。性能
/** * Rename bucketPath file from .tmp to permanent location. */ private void renameBucket() throws IOException, InterruptedException { if(bucketPath.equals(targetPath)) { return; } final Path srcPath = new Path(bucketPath); final Path dstPath = new Path(targetPath); callWithTimeout(new CallRunner<Object>() { @Override public Object call() throws Exception { if(fileSystem.exists(srcPath)) { // could block LOG.info("Renaming " + srcPath + " to " + dstPath); fileSystem.rename(srcPath, dstPath); // could block //index the dstPath lzo file if (codeC != null && ".lzo".equals(codeC.getDefaultExtension()) ) { LzoIndexer lzoIndexer = new LzoIndexer(new Configuration()); lzoIndexer.index(dstPath); } } return null; } }); }
咱們在HdfsSink和DualChannel中增長開關,當開關打開的狀況下,HdfsSink再也不往Hdfs上寫數據,而且數據只寫向DualChannel中的FileChannel。以此策略來防止Hdfs的正常停機維護。
Flume自己提供了MemoryChannel和FileChannel。MemoryChannel處理速度快,但緩存大小有限,且沒有持久化;FileChannel則恰好相反。咱們但願利用二者的優點,在Sink處理速度夠快,Channel沒有緩存過多日誌的時候,就使用MemoryChannel,當Sink處理速度跟不上,又須要Channel可以緩存下應用端發送過來的日誌時,就使用FileChannel,由此咱們開發了DualChannel,可以智能的在兩個Channel之間切換。
其具體的邏輯以下:
/*** * putToMemChannel indicate put event to memChannel or fileChannel * takeFromMemChannel indicate take event from memChannel or fileChannel * */ private AtomicBoolean putToMemChannel = new AtomicBoolean(true); private AtomicBoolean takeFromMemChannel = new AtomicBoolean(true); void doPut(Event event) { if (switchon && putToMemChannel.get()) { //往memChannel中寫數據 memTransaction.put(event); if ( memChannel.isFull() || fileChannel.getQueueSize() > 100) { putToMemChannel.set(false); } } else { //往fileChannel中寫數據 fileTransaction.put(event); } } Event doTake() { Event event = null; if ( takeFromMemChannel.get() ) { //從memChannel中取數據 event = memTransaction.take(); if (event == null) { takeFromMemChannel.set(false); } } else { //從fileChannel中取數據 event = fileTransaction.take(); if (event == null) { takeFromMemChannel.set(true); putToMemChannel.set(true); } } return event; }
Flume提供了NullSink,能夠把不須要的日誌經過NullSink直接丟棄,不進行存儲。然而,Source須要先將events存放到Channel中,NullSink再將events取出扔掉。爲了提高性能,咱們把這一步移到了Channel裏面作,因此開發了NullChannel。
爲支持向Storm提供實時數據流,咱們增長了KafkaSink用來向Kafka寫實時數據流。其基本的邏輯以下:
public class KafkaSink extends AbstractSink implements Configurable { private String zkConnect; private Integer zkTimeout; private Integer batchSize; private Integer queueSize; private String serializerClass; private String producerType; private String topicPrefix; private Producer<String, String> producer; public void configure(Context context) { //讀取配置,並檢查配置 } @Override public synchronized void start() { //初始化producer } @Override public synchronized void stop() { //關閉producer } @Override public Status process() throws EventDeliveryException { Status status = Status.READY; Channel channel = getChannel(); Transaction tx = channel.getTransaction(); try { tx.begin(); //將日誌按category分隊列存放 Map<String, List<String>> topic2EventList = new HashMap<String, List<String>>(); //從channel中取batchSize大小的日誌,從header中獲取category,生成topic,並存放於上述的Map中; //將Map中的數據經過producer發送給kafka tx.commit(); } catch (Exception e) { tx.rollback(); throw new EventDeliveryException(e); } finally { tx.close(); } return status; } }
Scribed在經過ScribeSource發送數據包給Flume時,大於4096字節的包,會先發送一個Dummy包檢查服務器的反應,而Flume的ScribeSource對於logentry.size()=0的包返回TRY_LATER,此時Scribed就認爲出錯,斷開鏈接。這樣循環反覆嘗試,沒法真正發送數據。如今在ScribeSource的Thrift接口中,對size爲0的狀況返回OK,保證後續正常發送數據。
lc.sinks.sink_hdfs.serializer.appendNewline = false
調大MemoryChannel的capacity,儘可能利用MemoryChannel快速的處理能力;
調大HdfsSink的batchSize,增長吞吐量,減小hdfs的flush次數;
適當調大HdfsSink的callTimeout,避免沒必要要的超時錯誤;
HdfsSink的path參數指明瞭日誌被寫到Hdfs的位置,該參數中能夠引用格式化的參數,將日誌寫到一個動態的目錄中。這方便了日誌的管理。例如咱們能夠將日誌寫到category分類的目錄,而且按天和按小時存放:
lc.sinks.sink_hdfs.hdfs.path = /user/hive/work/orglog.db/%{category}/dt=%Y%m%d/hour=%H
HdfsS ink中處理每條event時,都要根據配置獲取此event應該寫入的Hdfs path和filename,默認的獲取方法是經過正則表達式替換配置中的變量,獲取真實的path和filename。由於此過程是每條event都要作的操做,耗時很長。經過咱們的測試,20萬條日誌,這個操做要耗時6-8s左右。
因爲咱們目前的path和filename有固定的模式,能夠經過字符串拼接得到。然後者比正則匹配快幾十倍。拼接定符串的方式,20萬條日誌的操做只須要幾百毫秒。
在咱們初始的設計中,全部的日誌都經過一個Channel和一個HdfsSink寫到Hdfs上。咱們來看一看這樣作有什麼問題。
首先,咱們來看一下HdfsSink在發送數據的邏輯:
//從Channel中取batchSize大小的events for (txnEventCount = 0; txnEventCount < batchSize; txnEventCount++) { //對每條日誌根據category append到相應的bucketWriter上; bucketWriter.append(event); } for (BucketWriter bucketWriter : writers) { //而後對每個bucketWriter調用相應的flush方法將數據flush到Hdfs上 bucketWriter.flush(); }
假設咱們的系統中有100個category,batchSize大小設置爲20萬。則每20萬條數據,就須要對100個文件進行append或者flush操做。
其次,對於咱們的日誌來講,基本符合80/20原則。即20%的category產生了系統80%的日誌量。這樣對大部分日誌來講,每20萬條可能只包含幾條日誌,也須要往Hdfs上flush一次。
上述的狀況會致使HdfsSink寫Hdfs的效率極差。下圖是單Channel的狀況下每小時的發送量和寫hdfs的時間趨勢圖。
鑑於這種實際應用場景,咱們把日誌進行了大小歸類,分爲big, middle和small三類,這樣能夠有效的避免小日誌跟着大日誌一塊兒頻繁的flush,提高效果明顯。下圖是分隊列後big隊列的每小時的發送量和寫hdfs的時間趨勢圖。
目前,Flume日誌收集系統提供了一個高可用,高可靠,可擴展的分佈式服務,已經有效地支持了美團的日誌數據收集工做。
後續,咱們將在以下方面繼續研究:
日誌管理系統:圖形化的展現和控制日誌收集系統;
跟進社區發展:跟進Flume 1.5的進展,同時回饋社區;