Kafka實時流資料經Storm至Hdfs

滄南發表於2017-03-14

目前HDFS上日誌一部分由MR清洗生成&二次計算,一部分直接從伺服器離線上傳,但在私有云環境下,離線日誌的壓縮上傳可能會對服務造成效能影響,而且在很多日誌已經實時傳輸到Kafka叢集的情況下,考慮Kafka->Hdfs也不失為一條合理的路徑。

1. Kafka-Flume-Hdfs

這種方法直接通過Flume-ng的Hdfs-Sink往Hdfs導資料,Hdfs-Sink用來將資料寫入Hadoop分散式檔案系統(HDFS)中。支援建立text和sequence檔案及這2種檔案型別的壓縮;支援檔案週期性滾動(就是關閉當前檔案在建立一個新的),滾動可以基於時間、資料大小、事件數量;也支援通過event hearder屬性timestamp或host分割資料。HDFS目錄路徑或檔名支援格式化封裝,相應的封裝串在Hdfs-Sink生成目錄或檔案時被恰當的替換。使用HDFSSink需要首先安裝hadoop,Hdfs-Sink是通過hadoop jar和HDFS叢集通訊的。注意Hadoop版本需要支援sync()。具體配置類似:

dataAgent.channels.kafka-piwikGlobal.kafka.producer.type=sync
dataAgent.channels.kafka-piwikGlobal.topic=app_piwik
dataAgent.channels.kafka-piwikGlobal.groupId=AutoCollect-piwikGlobal-1
dataAgent.channels.kafka-piwikGlobal.zookeeperConnect=192.168.1.10:2181,192.168.1.11:2181
dataAgent.channels.kafka-piwikGlobal.brokerList=192.168.1.10:9092,192.168.1.11:9092
dataAgent.channels.kafka-piwikGlobal.is_avro_event=false
dataAgent.channels.kafka-piwikGlobal.transactionCapacity=100000
dataAgent.channels.kafka-piwikGlobal.capacity=6000000
dataAgent.channels.kafka-piwikGlobal.type=org.apache.flume.channel.kafka.KafkaChannel
dataAgent.channels.kafka-piwikGlobal.parseAsFlumeEvent=false

dataAgent.sinks.hdfs-piwikGlobal.channel=kafka-piwikGlobal
dataAgent.sinks.hdfs-piwikGlobal.type=hdfs
#使用gzip壓縮演算法
dataAgent.sinks.hdfs-piwikGlobal.hdfs.codeC=gzip
dataAgent.sinks.hdfs-piwikGlobal.hdfs.fileType=CompressedStream
#日誌儲存路徑,這裡按小時存放
dataAgent.sinks.hdfs-piwikGlobal.hdfs.path=hdfs://argo/data/logs/autoCollect/piwikGlobal/%Y-%m-%d/%H
#檔案字首,也可以使用封裝串
dataAgent.sinks.hdfs-piwikGlobal.hdfs.filePrefix=piwikGlobal
#不按時間滾動
dataAgent.sinks.hdfs-piwikGlobal.hdfs.rollInterval=0
#不根據檔案大小滾動
dataAgent.sinks.hdfs-piwikGlobal.hdfs.rollSize=0
#按事件條數滾動
dataAgent.sinks.hdfs-piwikGlobal.hdfs.rollCount=1000000
#hadoop叢集響應時間較長時需要配置
dataAgent.sinks.hdfs-piwikGlobal.hdfs.callTimeout=40000
#100秒後這個檔案還沒有被寫入資料,就會關閉它然後去掉.tmp,後續的events會新開一個.tmp檔案來接收
dataAgent.sinks.hdfs-piwikGlobal.hdfs.idleTimeout=100
dataAgent.sinks.hdfs-piwikGlobal.hdfs.useLocalTimeStamp=true

這種方式在日誌量大的情況下,需要啟動多個Hdfs-Sink或多個Flume程式,甚至需要部署在多臺機器上,不好管理,並且在特定需求下,還需要做定製開發。

2.Kafka-Storm-Hdfs

這種方法通過storm往hdfs寫資料,可以做定製開發,可以根據日誌量調整併發度,上下線方便,可根據Storm REST Api做監控報警。

這裡寫圖片描述

官方原始碼:https://github.com/apache/storm/tree/master/external/storm-hdfs

主要的類為HdfsBolt和SequenceFileBolt,都在org.apache.storm.hdfs.bolt包中。HdfsBolt用來寫text資料, SequenceFileBolt用來寫二進位制資料。

HdfsBolt的配置引數:

1、RecordFormat:定義欄位分隔符,你可以使用換行符\n或者製表符\t;

2、SyncPolicy:定義每次寫入的tuple的數量;

3、FileRotationPolicy:定義寫入的hdfs檔案的輪轉策略,你可以以時間輪轉(TimedRotationPolicy)、大小輪轉(FileSizeRotationPolicy)、不輪轉(NoRotationPolicy);

4、FileNameFormat:定義寫入檔案的路徑(withPath)和檔名的前字尾(withPrefix、withExtension);

5、withFsUrl:定義hdfs的地址。

示例:

RecordFormat format = new DelimitedRecordFormat().withFieldDelimiter("|");

SyncPolicy syncPolicy = new CountSyncPolicy(1000);

FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(5.0f, Units.MB);

FileNameFormat fileNameFormat = new DefaultFileNameFormat()
    .withPath("/data/logs");

HdfsBolt bolt = new HdfsBolt()
    .withFsUrl("hdfs://localhost:8020")
    .withFileNameFormat(fileNameFormat)
    .withRecordFormat(format)
    .withRotationPolicy(rotationPolicy)
    .withSyncPolicy(syncPolicy);

如果要連線開啟了HA的Hadoop叢集,可以改為withFsURL(“hdfs://nameserviceID”)。

nameserviceID可以在hdfs-site.xml中查到。

<property>
  <name>dfs.nameservices</name>
  <value>nameserviceID</value> 
</property>

這裡存在的問題是,一個執行緒只會寫一個檔案,不支援壓縮儲存,無法分目錄,因此需要做一些修改。

1)Gzip壓縮儲存

this.fs = FileSystem.get(URI.create(this.fsUrl), hdfsConfig);
CompressionCodecFactory compressionCodecFactory = new CompressionCodecFactory(new Configuration());
CompressionCodec compressionCodec = compressionCodecFactory.getCodecByClassName("org.apache.hadoop.io.compress.GzipCodec");
FSDataOutputStream out = this.fs.create(new Path(parentPath, new Path(childStrPath)));
CompressionOutputStream compressionOutput = compressionCodec.createOutputStream(out, compressionCodec.createCompressor());
#寫資料
compressionOutput.write(bulkStr.toString().getBytes());

Flush操作也需要做些修改,太過頻繁會影響寫入效能:

try {
    compressionOutput.flush();
    if (out instanceof HdfsDataOutputStream) {
        ((HdfsDataOutputStream) out).hsync(EnumSet.of(HdfsDataOutputStream.SyncFlag.UPDATE_LENGTH));
    } else {
         out.hsync();
    }
} catch (IOException e) {
    LOG.error("flush error:{}",e.getMessage());
}

如果worker異常終止,造成gzip檔案非正常關閉,通過hdfs -text命令是可以正常檢視的,但一般MR程式無法讀取此類檔案,指標不治本的方法,可以簡單設定mapred.max.map.failures.percent來跳過異常檔案,或者自己實現InputStream類。

2)分目錄寫入

比如對於接收到的每一條日誌,需要解析時間或型別,按/type/day/hour的方式儲存,這就會導致一個hdfsBolt執行緒需要開啟多個不同目錄下的檔案進行寫入。

#每個目錄對應一個Path物件,以防重複建立
private Map<String, Path> parentPathObjMap = Maps.newHashMap();

#每個目錄對應一個CompressionOutputStream物件,判斷日誌需要寫入哪一個目錄,則獲取相應物件寫入
private Map<String, CompressionOutputStream> pathToCompWriter = Maps.newHashMap();

#每個目錄對應一個StringBuilder物件,積攢一批日誌寫入,以提高效能
private Map<String, StringBuilder> pathToCache = Maps.newHashMap();

#每個目錄對應一個Long物件,判斷積攢日誌量是否滿足寫入閾值
private Map<String, Long> pathToCacheLineNum = Maps.newHashMap();

#每個目錄對應一個檔案輪轉物件
private Map<String, FileRotationPolicy> fileRotationMap = Maps.newHashMap();

#每個目錄寫入的日誌位元組數,用來判斷是否輪轉
private Map<String, Long> offsetMap = Maps.newHashMap();

#每個目錄上次寫入的時間,超過一定時間沒有資料寫入,則關閉檔案
private Map<String, Long> lastFlushTimeMap = Maps.newHashMap();

因為一個執行緒在一個目錄下只會往一個檔案寫,因此這些Map的key值都為目錄路徑。

在程式執行過程本來將日誌解析單獨作為一個bolt,後來將其融入HdfsBolt,以配置正規表示式的方式,減少網路傳輸開銷,來提高效能。

相關文章