標籤:
org.apache.flume.source.SpoolDirectorySource是flume的一個經常使用的source,這個源支援從磁碟中某目錄擷取檔案資料。不同於其它非同步源,這個源可以避免重新啟動或者發送失敗後資料丟失。flume可以監控目錄,當出現新檔案時會讀取該檔案並擷取資料。當一個給定的檔案被所有讀入到通道中時,該檔案會被重新命名以標誌已經完畢。同一時候,該源須要一個清理進程來定期移除完畢的檔案。
通道可選地將一個完畢路徑的原始檔案插入到每一個事件的hearder域中。在讀取檔案時,source快取檔案資料到記憶體中。同一時候,須要確定設定了bufferMaxLineLength選項,以確保該資料遠大於輸入資料中資料最長的某一行。
注意!!!channel僅僅接收spooling directory中唯一命名的檔案。假設檔案名稱反覆或檔案在讀取過程中被改動,則會有讀取失敗返回異常資訊。這樣的情境下,同名的檔案拷貝到這個檔案夾時建議帶唯一標示,比方時間戳記。
一、configure(Context context)方法。代碼例如以下:
public void configure(Context context) { spoolDirectory = context.getString(SPOOL_DIRECTORY); Preconditions.checkState(spoolDirectory != null, "Configuration must specify a spooling directory"); completedSuffix = context.getString(SPOOLED_FILE_SUFFIX, DEFAULT_SPOOLED_FILE_SUFFIX); deletePolicy = context.getString(DELETE_POLICY, DEFAULT_DELETE_POLICY); fileHeader = context.getBoolean(FILENAME_HEADER, DEFAULT_FILE_HEADER); fileHeaderKey = context.getString(FILENAME_HEADER_KEY, DEFAULT_FILENAME_HEADER_KEY); batchSize = context.getInteger(BATCH_SIZE, DEFAULT_BATCH_SIZE); inputCharset = context.getString(INPUT_CHARSET, DEFAULT_INPUT_CHARSET); ignorePattern = context.getString(IGNORE_PAT, DEFAULT_IGNORE_PAT); trackerDirPath = context.getString(TRACKER_DIR, DEFAULT_TRACKER_DIR); deserializerType = context.getString(DESERIALIZER, DEFAULT_DESERIALIZER); deserializerContext = new Context(context.getSubProperties(DESERIALIZER + ".")); // "Hack" to support backwards compatibility with previous generation of // spooling directory source, which did not support deserializers Integer bufferMaxLineLength = context.getInteger(BUFFER_MAX_LINE_LENGTH); if (bufferMaxLineLength != null && deserializerType != null && deserializerType.equals(DEFAULT_DESERIALIZER)) { deserializerContext.put(LineDeserializer.MAXLINE_KEY, bufferMaxLineLength.toString()); } }
1、spoolDirectory是監控檔案夾,不可為空,沒有預設值。這個source不具有監控子檔案夾的功能,也就是不能遞迴監控。假設須要,這須要自己去實現,http://blog.csdn.net/yangbutao/article/details/8835563 這裡有遞迴檢測的實現;
2、completedSuffix是檔案讀取完畢後給完畢檔案加入的標記尾碼,預設是".COMPLETED";
3、deletePolicy這是是否刪除讀取完成的檔案,預設是"never",就是不刪除,眼下僅僅支援"never"和“IMMEDIATE”;
4、fileHeader是否在event的Header中加入檔案名稱,boolean類型
5、fileHeaderKey這是event的Header中的key,value是檔案名稱
6、batchSize這個是一次處理的記錄數,預設是100;
7、inputCharset編碼方式,預設是"UTF-8";
8、ignorePattern忽略合格檔案名稱
9、trackerDirPath被處理檔案中繼資料的隱藏檔夾,預設".flumespool"
10、deserializerType將檔案裡的資料序列化成event的方式,預設是“LINE”---org.apache.flume.serialization.LineDeserializer
11、deserializerContext這個主要用在Deserializer中設定編碼方式outputCharset和檔案每行最大長度maxLineLength。
二、start()方法。代碼例如以下:
public void start() { logger.info("SpoolDirectorySource source starting with directory: {}", spoolDirectory); ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); counterGroup = new CounterGroup(); File directory = new File(spoolDirectory); try { reader = new ReliableSpoolingFileEventReader.Builder() .spoolDirectory(directory) .completedSuffix(completedSuffix) .ignorePattern(ignorePattern) .trackerDirPath(trackerDirPath) .annotateFileName(fileHeader) .fileNameHeader(fileHeaderKey) .deserializerType(deserializerType) .deserializerContext(deserializerContext) .deletePolicy(deletePolicy) .inputCharset(inputCharset) .build(); } catch (IOException ioe) { throw new FlumeException("Error instantiating spooling event parser", ioe); } Runnable runner = new SpoolDirectoryRunnable(reader, counterGroup); executor.scheduleWithFixedDelay( runner, 0, POLL_DELAY_MS, TimeUnit.MILLISECONDS); super.start(); logger.debug("SpoolDirectorySource source started"); }
1、構建了一個org.apache.flume.client.avro.ReliableSpoolingFileEventReader的對象reader;
2、啟動了一個每隔POLL_DELAY_MS(預設500,單位ms)運行一次SpoolDirectoryRunnable的進程;
三、讀取並發送event進程。代碼例如以下:
private class SpoolDirectoryRunnable implements Runnable { private ReliableSpoolingFileEventReader reader; private CounterGroup counterGroup; public SpoolDirectoryRunnable(ReliableSpoolingFileEventReader reader, CounterGroup counterGroup) { this.reader = reader; this.counterGroup = counterGroup; } @Override public void run() { try { while (true) { List<Event> events = reader.readEvents(batchSize); //讀取batchSize個記錄 if (events.isEmpty()) { break; } counterGroup.addAndGet("spooler.events.read", (long) events.size()); getChannelProcessor().processEventBatch(events); //將events批量發送到channel reader.commit(); } } catch (Throwable t) { logger.error("Uncaught exception in Runnable", t); if (t instanceof Error) { throw (Error) t; } } } }
該進程實現了批量讀取reader所指向的檔案的資料,並發送到channel。
四、org.apache.flume.client.avro.ReliableSpoolingFileEventReader的構造方法首先是先嘗試對spoolDirectory是否有建立檔案、讀、寫、刪除等許可權;然後在構造"$spoolDirectory/.flumespool/.flumespool-main.meta"中繼資料檔案
五、上面SpoolDirectoryRunnable.run方法中的List<Event> events = reader.readEvents(batchSize),是org.apache.flume.client.avro.ReliableSpoolingFileEventReader.readEvents(batchSize):
public List<Event> readEvents(int numEvents) throws IOException { if (!committed) { if (!currentFile.isPresent()) {//為空白,假設Optional包括非null的引用(引用存在),返回true throw new IllegalStateException("File should not roll when " + "commit is outstanding."); } logger.info("Last read was never committed - resetting mark position."); currentFile.get().getDeserializer().reset(); } else {//已經committed成功 // Check if new files have arrived since last call //Returns true if this holder contains a (non-null) instance if (!currentFile.isPresent()) {//為空白,擷取下一個檔案,初次調用 currentFile = getNextFile(); } // Return empty list if no new files if (!currentFile.isPresent()) {//為空白,已經沒有可讀的檔案了 return Collections.emptyList(); } //其他的說明是currentFile眼下還在讀 } EventDeserializer des = currentFile.get().getDeserializer(); List<Event> events = des.readEvents(numEvents);//加入event的body /* It's possible that the last read took us just up to a file boundary. * If so, try to roll to the next file, if there is one. */ if (events.isEmpty()) { retireCurrentFile(); //改名字 currentFile = getNextFile();//換下一個檔案 if (!currentFile.isPresent()) { return Collections.emptyList(); } events = currentFile.get().getDeserializer().readEvents(numEvents);//繼續讀,加入event的body } if (annotateFileName) { String filename = currentFile.get().getFile().getAbsolutePath(); for (Event event : events) { event.getHeaders().put(fileNameHeader, filename);//加入header } } committed = false; lastFileRead = currentFile; return events; }
1,committed初始化時是true,所以第一次執行就是通過getNextFile()擷取當前要去讀的檔案。假設是空就返回空值了。
2,使用deserializer(預設是org.apache.flume.serialization.LineDeserializer)的readEvents(numEvents)去批量讀資料封裝成event。
3,如擷取的批量events為空白,說明這個檔案讀完了,須要對這個讀完的檔案做個“刪除”(retireCurrentFile()方法,在這也會刪除中繼資料檔案),就是依據deletePolicy(刪除還是加入去讀完成尾碼completedSuffix);可是這個本方法是有返回值的就是events,所以須要擷取下一個檔案,即再次執行getNextFile(),並events = currentFile.get().getDeserializer().readEvents(numEvents)
4,是否要對這些events的Header中加入檔案名稱
5,committed = false; lastFileRead = currentFile; 並返回events。
這種方法還有幾點須要解釋:
其一、就是committed參數,此參數關係到這一批量的event是否已經正確處理完成。能夠看到上面的5中所講,每調用一次ReliableSpoolingFileEventReader.readEvents(batchSize)均會在最後將committed設定為false,可是在SpoolDirectoryRunnable.run()方法中也能夠看出在調用readEvents方法後還會調用ReliableSpoolingFileEventReader.commit()方法,代碼例如以下:
/** Commit the last lines which were read. */ @Override public void commit() throws IOException { if (!committed && currentFile.isPresent()) { currentFile.get().getDeserializer().mark(); committed = true; } }
這種方法說明滿足兩個條件就能夠:一、向trackerFile寫入讀到的記錄位置,mark()方法會將syncPosition寫入trackerFile,而ResettableFileInputStream中的position用來暫存位置添加的,待到何時會syncPosition=position,這樣是為了防止出現異常時用於恢複丟失的資料;二、將committed = true。兩個條件:一個是committed=false,這個運行完readEvents最後會置為false;二、currentFile“非空”,代表有正在讀的檔案。假設committed在readEvents中開始時為false,說明:一、event提交到channel時出現了問題,沒有運行reader.commit;二、currentFile已經“為空白”,說明沒有能夠讀的檔案。這兩點也體如今readEvents開始部分,committed=false時,假設沒有可讀檔案就會拋出異常File should not roll when commit is outstanding.";假設是在提交到channel時出問題會通過currentFile.get().getDeserializer().reset()又一次撤回到上次正確提交channel的位置,這樣能夠使得不遺失資料。
其二、就是getNextFile()方法。這種方法會首先過濾檢測檔案夾的子檔案夾(也就是不能遞迴)、隱藏檔案(以"."開頭的檔案)、已經讀完的檔案(有completedSuffix尾碼的)、符合ignorePattern的檔案;然後將過濾後的檔案按時間的先後順序排序,再建立一個新的相應的中繼資料檔案;構造一個讀取檔案的輸入資料流ResettableFileInputStream,並將此輸入資料流作為參數傳遞給deserializer,終於返回一個Optional.of(new FileInfo(nextFile, deserializer));
其三、就是LineDeserializer)的readEvents(numEvents)方法。這種方法會多次(numEvents)調用LineDeserializer(預設)的readLine()擷取一行資料封裝成event。readLine()會通過org.apache.flume.serialization.ResettableFileInputStream.readChar()不斷的去擷取資料,讀完正行後推斷每行的長度是否超過規定值maxLineLength。readChar()方法除了不斷讀取一個字元外,還會記下字元的位置,等待將位置寫入中繼資料檔案裡(通過deserializer.mark()寫入)
【Java】【Fulme】Flume-NG原始碼閱讀之SpoolDirectorySource