原本打算將storm直接與flume直連,發現相應組件支持比較弱,topology任務對應的supervisor也不一定在哪個節點上,只能採用統一的分佈式消息服務Kafka。
原本打算將結構設置爲:
最後結構更改爲:
集成Kafka
storm中已經寫好了KafkaSpout用來接收Kafka中間件上的消息,併發射到Bolt中,只需要依賴 storm-kafka即可:
<dependency>
<groupId>org.apache.storm</groupId>
<artifactId>storm-kafka</artifactId>
<version>${storm.version}</version>
</dependency>
調用org.apache.storm.kafka.KafkaSpout, 需要傳遞一個SpoutConfig用來配置kafka對應的zookeeper以及topic:
String zks = "192.168.1.1xx:2181,192.168.1.1xx:2181,192.168.1.1xx:2181/kafka";
String topic = "log-storm";
BrokerHosts brokerHosts = new ZkHosts(zks);
SpoutConfig spoutConfig = new SpoutConfig(brokerHosts, topic, "/"+ topic, UUID.randomUUID().toString());
spoutConfig.scheme = new SchemeAsMultiScheme(new StringScheme());
spoutConfig.zkServers = Arrays.asList("192.168.1.1xx","192.168.1.1xx","192.168.1.1xx");
spoutConfig.zkPort = 2181;
建立KafkaSpout(spoutConfig)即可。
需要注意的是,我們在Bolt中需要對收到的消息進行主動ack/fail,否則會出現消息重複發送的情況,一般情況下Bolt的寫法類似下面,在prepare中緩存collector,executor中通過try/catch塊決定是否確認消息(以通知Spout是否需要對消息進行重發),declareOutputFields中聲明需要輸出的字段。
private OutputCollector collector;
@Override
public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
this.collector = collector;
}
@Override
public void execute(Tuple input) {
try {
String msgBody = input.getString(0);
int traceIndex = msgBody.indexOf(TRACE_CONST);
if (traceIndex >= 0) {
String completeLog = msgBody.substring(traceIndex + TRACE_CONST.length());
collector.emit(new Values(completeLog));
}
collector.ack(input);
} catch (Exception e) {
collector.fail(input);
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("log"));
}
Storm中需要有一個main函數,用於構建和啓動topology,以便將spout,bolt等組件連接起來,代碼類似下面:
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("kafka-reader", new KafkaSpoutNoMetrics(spoutConfig), 3);
builder.setBolt("log-extractor", new LogExtractorBolt(), 2).shuffleGrouping("kafka-reader");
builder.setBolt("log-splitter", new LogSplitterBolt(), 2).shuffleGrouping("log-extractor");
builder.setBolt("memcached-store", new MemcachedBolt()).fieldsGrouping("log-splitter", new Fields("md"));
Config config = new Config();
String name = "LogStormProcessor";
config.setNumWorkers(1);
StormSubmitter.submitTopologyWithProgressBar(name, config, builder.createTopology());
使用Storm Trident
Trident在Storm上進行了高級抽象,例如事務處理和狀態管理的細節,可以讓一批tuple進行離散的事務處理,還允許topology在數據上執行函數功能、過濾和聚合操作。
使用Trident,我們需要使用TridentTopology替換原有的TopologyBuilder構造Storm的拓撲圖。在Trident中的spout引入了數據批次(batch)的概念,不像Strom中的spout,Trident Spout必須成批地發送tuple。
在Trident中,spout並沒有真正發射tuple,而是把這項工作分解給了BatchCoordinator和Emitter方法,Emitter方法負責發送tuple,BatchCoordinator負責管理批次和元數據,Emitter需要依靠元數據來恰當地進行批次的數據重放。
首先,需要根據ITridentSpout新建一個數據流,
Stream stream = tridentTopology.newStream("event", kafkaSpout);
在使用KafkaSpout作爲TridentSpout時,其默認的輸出字段名稱爲str,
Exception in thread "main" java.lang.IllegalArgumentException: Trying to select non-existent field: 'event' from stream containing fields fields: <[str]>
at org.apache.storm.trident.Stream.projectionValidation(Stream.java:853)
at org.apache.storm.trident.Stream.each(Stream.java:320)
at com.zhen.log.processor.trident.Main.main(Main.java:48)
我們原來使用的KafkaSpout,雖然可以將其直接用於newStream方法,但是運行時會出現錯誤:
原因就在於進行適配的過程中,註冊方法registerMetric只能夠被宰ISpout::open()方法中被調用,雖然可以進行合理地改造(由於有一些包訪問控制權限的相關依賴,新建一個同名package,並將其中的registerMetric方法刪除),但是其事務性不能得到保證,在本人測試的過程中,Kafka的消息不能被正常消費,每次重啓服務都會讀到完整的所有數據。
但是這並不是推薦的用法,storm-kafka中存在另外一個實現:org.apache.storm.kafka.trident.OpaqueTridentKafkaSpout,可以滿足要求:
TridentKafkaConfig kafkaConfig = new TridentKafkaConfig(brokerHosts, topic);
OpaqueTridentKafkaSpout kafkaSpout = new OpaqueTridentKafkaSpout(kafkaConfig);
TridentTopology tridentTopology = new TridentTopology();
Stream stream = tridentTopology.newStream("event", kafkaSpout);
而使用OpaqueTridentKafkaSpout時,默認的輸出名稱爲“bytes”,其輸出格式也並不是String字符串而是byte[],需要根據編碼格式將其轉換爲String。
在Spout編寫完成後,就可以加入後續的運算操作,trident處理是通過創建Stream的各種operation並連接來進行處理的,比較常用的兩種運算:filter和function,例如我們下面處理流的方式,每次返回Stream都可以繼續用來創建新的數據流:
Stream logStream = stream.each(new Fields("bytes"), new LogExtractorFunction(), new Fields("log"))
.each(new Fields("log"), new LogSplitterFunction(), new Fields("logObject"))
.each(new Fields("logObject"), new LogTypeFilter("TRACE"));
在filter中,繼承自BaseFilter,唯一的isKeep方法會根據tuple的屬性進行相應過濾操作,需要指定對應輸入的Field,filter沒有額外輸出的多餘字段。注意filter中不能改變tuple,如果既想要過濾又想添加字段時必須使用function。
在function中,繼承自BaseFunction,通過execute方法來對所有的數據增加額外的字段,並不會刪除或者變更已有的字段。使用function需要指定多餘輸出的Fields,function中發射的字段數要與聲明的fields字段數據保持一致。
和function比較類似,aggregator允許topology組合tuple,不同的是,它會替換tuple字段和值,有三種聚合器可以被使用:CombinerAggregator,ReducerAggregator和Aggregator。這裏,我們使用的是CombinerAggregator。
CombinerAggregator用來將一個集合的tuple組合到一個單獨到一個單獨的字段中,定義如下:
public interface CombinerAggregator<T> extends Serializable {
T init(TridentTuple tuple);
T combine(T val1, T val2);
T zero();
}
Storm會對每個tuple調用init方法,然後重複調用combiner方法指導一個分片的數據處理完成,傳遞給combine方法的兩個參數是局部聚合的結果,以及調用了init返回的值,如果沒有聚合結果,會直接調用zero方法返回一個自定義空值。
聚合一般需要首先對數據流進行groupBy操作後,在GroupedStream流上進行實際操作,一般情況下,首先根據前面的流輸出一個用於分組的鍵值用於groupBy,然後進行persistentAggregate,根據分組將數據歸併計算合併結果。
logStream
.each(new Fields("logObject"), new LogGroupFunction(), new Fields("key")).groupBy(new Fields("key"))
.persistentAggregate(MemcachedState.nonTransactional(servers), new Fields("logObject"), new LogCombinerAggregator(),
new Fields("statistic"))
注意,使用Trident時也是可以分成多個流的,只需要在特定的節點上,保存本地變量,就可以在其上執行多次each,分出多條路徑流進行獨立處理(也可以對多條輸入流進行合併,這裏沒有使用到這樣高級的功能)。
Stream logStream = stream.each(new Fields("bytes"), new LogExtractorFunction(), new Fields("log"))
.each(new Fields("log"), new LogSplitterFunction(), new Fields("logObject"))
.each(new Fields("logObject"), new LogTypeFilter("TRACE"));
logStream.each(new Fields("log"), new LocalFileSaveFunction(), new Fields());
logStream
.each(new Fields("logObject"), new LogGroupFunction(), new Fields("key")).groupBy(new Fields("key"))
.persistentAggregate(MemcachedState.nonTransactional(servers), new Fields("logObject"), new LogCombinerAggregator(),
new Fields("statistic"))
;
在使用任何function,aggregator時,都可以通過聲明Fields的方式來設置使用到的字段名稱,Combiner中可以不使用任何定義的Fields,此時傳遞給Trident的Tuple中將不會包含任何字段(一般代碼示例中如此)。使用聚合時,還需要持續存儲聚合的Trident狀態,持久化操作從狀態管理開始,Trident對狀態有底層的操作原語,可以參考State接口的方法。
Storm中用State來持久化存儲信息,有三種狀態類型:非事務型,重複事務型以及不透明事務型,在分佈式環境下,數據可能被重放,爲了支持計數和狀態更新,Trident將狀態更新操作進行序列化,使用不同的狀態更新模式對重放和錯誤數據進行容錯。
我們存儲中間數據狀態使用了memcached作爲媒介,關於trident與memcached進行事務處理相關代碼,可以參考工程(storm創建者編寫)
其中調用了twitter中定義的所以使用改造過的客戶端:
<dependency>
<groupId>com.twitter</groupId>
<artifactId>finagle-memcached_2.9.2</artifactId>
<version>6.20.0</version>
</dependency>
但是將源碼copy到工程中並添加對應的maven依賴(多數是twitter相關的依賴),其中的twitter依賴始終找不到:
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project log-storm-processor: Could not resolve dependencies for project com.zhen:log-storm-processor:jar:1.0.0-SNAPSHOT: The following artifacts could not be resolved: com.twitter.common.zookeeper:server-set:jar:1.0.83, com.twitter.common.zookeeper:client:jar:0.0.60, com.twitter.common.zookeeper:group:jar:0.0.78: Failure to find com.twitter.common.zookeeper:server-set:jar:1.0.83 in http://192.168.1.14:8081/nexus/content/repositories/releases/ was cached in the local repository, resolution will not be reattempted until the update interval of nexus-releases has elapsed or updates are forced -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
因此只能將其進行改造,使用com.whalin對應的memcached客戶端jar包,以滿足從Storm存儲到memcached的需求。
storm trident的結構: