基于Flume的日志收集系統(二)改進和優化

轉自http://www.aboutyun.com/thread-8318-1-1.html

問題導讀:

1.Flume的存在些什么問題?

2.基于開源的Flume美團增加了哪些功能?

3.Flume系統如何調優?

在《基于Flume的日志收集系統(一)架構和設計》中,我們詳述了基于Flume的美團日志收集系統的架構設計,以及為什么做這樣的設計。在本節中,我們將會講述在實際部署和使用過程中遇到的問題,對Flume的功能改進和對系統做的優化。

1 Flume的問題總結

在Flume的使用過程中,遇到的主要問題如下:

a. Channel“水土不服”:使用固定大小的MemoryChannel在日志高峰時常報隊列大小不夠的異常;使用FileChannel又導致IO繁忙的問題;

b. HdfsSink的性能問題:使用HdfsSink向Hdfs寫日志,在高峰時間速度較慢;

c. 系統的管理問題:配置升級,模塊重啟等;

2 Flume的功能改進和優化點

從上面的問題中可以看到,有一些需求是原生Flume無法滿足的,因此,基于開源的Flume我們增加了許多功能,修改了一些Bug,并且進行一些調優。下面將對一些主要的方面做一些說明。

2.1 增加Zabbix monitor服務

一方面,Flume本身提供了http, ganglia的監控服務,而我們目前主要使用zabbix做監控。因此,我們為Flume添加了zabbix監控模塊,和sa的監控服務無縫融合。

另一方面,凈化Flume的metrics。只將我們需要的metrics發送給zabbix,避免 zabbix server造成壓力。目前我們最為關心的是Flume能否及時把應用端發送過來的日志寫到Hdfs上, 對應關注的metrics為:

Source : 接收的event數和處理的event數

Channel : Channel中擁堵的event數

Sink : 已經處理的event數

2.2 為HdfsSink增加自動創建index功能

首先,我們的HdfsSink寫到hadoop的文件采用lzo壓縮存儲。 HdfsSink可以讀取hadoop配置文件中提供的編碼類列表,然后通過配置的方式獲取使用何種壓縮編碼,我們目前使用lzo壓縮數據。采用lzo壓縮而非bz2壓縮,是基于以下測試數據:

event大小(Byte)sink.batch-sizehdfs.batchSize壓縮格式總數據大小(G)耗時(s)平均events/s壓縮后大小(G)

54430010000bz29.1244868331.36

54430010000lzo9.1612273333.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() {

@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;

}

});

}

復制代碼

2.3 增加HdfsSink的開關

我們在HdfsSink和DualChannel中增加開關,當開關打開的情況下,HdfsSink不再往Hdfs上寫數據,并且數據只寫向DualChannel中的FileChannel。以此策略來防止Hdfs的正常停機維護。

2.4 增加DualChannel

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;

}

復制代碼

2.5 增加NullChannel

Flume提供了NullSink,可以把不需要的日志通過NullSink直接丟棄,不進行存儲。然而,Source需要先將events存放到Channel中,NullSink再將events取出扔掉。為了提升性能,我們把這一步移到了Channel里面做,所以開發了NullChannel。

2.6 增加KafkaSink

為支持向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 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> topic2EventList = new HashMap>();

//從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;

}

}

復制代碼

2.7 修復和scribe的兼容問題

Scribed在通過ScribeSource發送數據包給Flume時,大于4096字節的包,會先發送一個Dummy包檢查服務器的反應,而Flume的ScribeSource對于logentry.size()=0的包返回TRY_LATER,此時Scribed就認為出錯,斷開連接。這樣循環反復嘗試,無法真正發送數據。現在在ScribeSource的Thrift接口中,對size為0的情況返回OK,保證后續正常發送數據。

3. Flume系統調優經驗總結3.1 基礎參數調優經驗

HdfsSink中默認的serializer會每寫一行在行尾添加一個換行符,我們日志本身帶有換行符,這樣會導致每條日志后面多一個空行,修改配置不要自動添加換行符;

lc.sinks.sink_hdfs.serializer.appendNewline = false

復制代碼

調大MemoryChannel的capacity,盡量利用MemoryChannel快速的處理能力;

調大HdfsSink的batchSize,增加吞吐量,減少hdfs的flush次數;

適當調大HdfsSink的callTimeout,避免不必要的超時錯誤;

3.2 HdfsSink獲取Filename的優化

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萬條日志的操作只需要幾百毫秒。

3.3 HdfsSink的b/m/s優化

在我們初始的設計中,所有的日志都通過一個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的時間趨勢圖。


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容