因为花果山经常遇到ddos攻击,我们当前的BI统计平台无法提供实时的web日志。在无法更改现有框架的情况下,考虑加入实时计算平台。
在实时处理这块,相似的项目还是有 yahoo 的S4。Storm 和 S4 相比,除去工作流程不一样外,最大的不同在于 Storm 提供了消息追踪机制,能确保消息不被丢失。
Storm 依赖 Zookeeper 和 zeromq。它的框架相对简单。
配置分2部分,一部分是配置 zookeeper 集群;另外一部分对 storm 本身的配置。
Zookeeper 的配置:
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/home/datastream/cluster_data/zookeeper
clientPort=2181
server.2=download-1.popo.163.org:2181:3888
server.3=download-2.popo.163.org:2181:3888
server.4=download-3.popo.163.org:2181:3888
server.5=download-4.popo.163.org:2181:3888
server.6=download-5.popo.163.org:2181:3888
需要注意的是在dataDir目录中,不要忘记创建myid
,里面的内容需要和配置里面的保持一致。例如download-1.popo.163.org
上的myid
内容应该是2
。
Storm 的配置:
storm.zookeeper.servers:
- "download-1.popo.163.org"
- "download-2.popo.163.org"
- "download-3.popo.163.org"
- "download-4.popo.163.org"
- "download-5.popo.163.org"
nimbus.host: "download-1.popo.163.org"
# topology.serializations:
# - "org.mycompany.MyObjectSerialization"
# - "org.mycompany.MyOtherObjectSerialization"
drpc.servers:
- "download-1.popo.163.org"
整个配置文件一般是在~/.storm/storm.yaml
或者 $storm_home/conf/storm.yaml
./storm
Commands:
activate
classpath
deactivate
dev-zookeeper
drpc
help
jar
kill
list
localconfvalue
nimbus
rebalance
remoteconfvalue
repl
shell
supervisor
ui
version
Help:
help
help <command>
Documentation for the storm client can be found at https://github.com/nathanmarz/storm/wiki/Command-line-client
Storm 集群中有多个角色。
./storm supervisor
启动./storm nimbus
启动./storm ui
启动除去服务器上的 Storm 平台,开发者需要在本地搭建自己的开发环境。整个开发环境也需要storm.yaml,和线上的保持一致。
./storm jar all-my-code.jar backtype.storm.MyTopology arg1 arg2
./storm kill taskname
, taskname 基本就是 StormSubmitter.submitTopology
函数的第一个参数。Storm 支持 maven2 或者 lein 管理项目。
目录结构:
src/jvm #javacode
multilang/resources #other language
lib/ #jar depends
简单的lein配置(project.clj):
(defproject logstream "0.0.1-SNAPSHOT"
:java-source-path "src/jvm"
:javac-options {:debug "true" :fork "true"}
:jvm-opts ["-Djava.library.path=/usr/local/lib:/opt/local/lib:/usr/lib:/usr/local/diablo-jdk1.6.0/lib"]
:aot :all
:dependencies [[org.clojure/clojure "1.2.0"]
[org.clojure/clojure-contrib "1.2.0"]
[com.googlecode.json-simple/json-simple "1.1"]
]
:dev-dependencies [[storm "0.6.0"]
])
Storm 基于消息流处理,它称之为
stream
。主要由: spout 和 bolt 2部分组成。上面的图中的 Spout 和 Bolt 的关系被称为 topology。
在 Storm 的结构中 spout 承担了消息的生成功能。spout 有多个封装好的接口。
public interface ISpout extends Serializable {
void open(Map conf, TopologyContext context, SpoutOutputCollector collector);
void close();
void nextTuple();
void ack(Object msgId);
void fail(Object msgId);
}
public class WebLogSpout implements ISpout {
....
}
nextTuple
函数将在一个 while(ture) 中被调用。你可以使用IRichSpout
代替ISPout
,这样你可以设置isDistributed()
函数,让spout允许多个。 一个 spout 是 topology 的一个 stream source。 一个 Spout 可以设置为 reliable 或者 unreliable。通过设置declareOutputFields
函数,Spouts 可以 emit 多个 stream。 最组要的功能函数是nextTuple
,nextTuple
可以 emits 一个 tuple 到 topology 或者直接返回。需要注意的是nextTuple
一定是非阻塞的,因为所有的 spout 都是跑在同一个线程的。ack
和fail
的作用是处理消息在整个 topology 的完成情况。只有需要确保消息完整处理的时候才需要用到。
Bolt 是基本的消息处理单元。
public class SplitSentence implements IRichBolt {
OutputCollector _collector;
public void prepare(Map conf, TopologyContext context, OutputCollector collector) {
_collector = collector;
}
public void execute(Tuple tuple) {
String sentence = tuple.getString(0);
for(String word: sentence.split(" ")) {
_collector.emit(tuple, new Values(word));
}
_collector.ack(tuple);
}
public void cleanup() { }
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("word"));
}
}
Bolt 可以处理所有的逻辑操作。Bolt 必须在处理结束的时候显式的调用
ack
,这样 topology 可以知道哪些消息已经被处理完成了。反之如果在一个对消息完整性有要求的 topology 中,会触发一个消息的重发需求。
在 Storm 中,每个消息单元被称之为 tuple。因为 storm 使用内存追踪消息状态,每个 tuple 必须被
ack
或者fail
,否则很容易 OOM。如果在不需要确保消息完整性的 topology 里面,tuple 只是单纯的直接passdown,即使丢失也不会重发。
_collector.emit(new Values(word));
_collector.emit(new Values(word,count));
anchored,Multi-anchoring :
List<Tuple> anchors = new ArrayList<Tuple>();
anchors.add(tuple1);
anchors.add(tuple2);
_collector.emit(anchors, new Values(1, 2, 3));
当 emit 一个 tuple 的时候, spout生成的 tuple id会从被处理的 tuple的 anchors 里面复制到新的 tuple。如果一个 tuple 被ack了,它会发送一个消息到
acker
tasks,通知 tuple tree 改变了。例如: 如果tuples "D" and "E"是基于 tuple "C", 当 "C" 被 acked时:
既然 "C" 从 tree 中 被移除的同时, "D" 和 "E" 加入了 tree, 整棵树不能过早的完成。
一个 storm toplogy有一组特殊的
acker
任务,用于追踪每个 spout tuple生成的 tuple tree。一个 acker task 存储了一个 spout id到一对数值的映射。第一个数值是任务id,主要是spout的时候生成的,随机会被用于发送完整的消息。第二个值是一个64bit的数字,称为ack val
。整个 ack val用于表达整个tuple tree的状态。如果多少,整个数字只是单纯的xor所有已经创建的 tuple id。 当一个acker
任务看到一个ack val
变为0的时候,整个 tuple tree就算完成了。由于tuple id是一个随机的64bit的数字,ack val
刚好变成0的概率很低。通过计算,如果每秒ack 10k,要触发一个错误需要花费50,000,000年。而且只有当 tuple 刚好fail的时候才会触发数据丢失。
shuffle group tuple被随机的发送到bolt
fields group 按照field发送 tuple
all group 发送到所有的bolt
global group 整个stream进入到一个bolt中。
none group 整个只有当你不在乎group的时候使用。当前情况下这个和 shuffle group的效果一样。
direct group 指定特定的bolt处理消息,需要使用emitDirect
函数。
Fields主要的作用是对消息做分类处理。例如在bolt中:
public void execute(Tuple input) {
int val = input.getInteger(0);
_collector.emit(input, new Values(var, var2));
_collector.ack(input);
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("word","count"));
}
emit的数据和fields的个数要对应。
builder.setBolt("rank", new RankObjects(), n).fieldsGrouping("objects", new Fields("value"));
这个调用的作用是创建使用 RankObjects 一个叫 rack 的 bolt,同时从叫 objects 的bolt中订阅叫 value 的 fields。
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("words", new TestWordSpout(), 10);
builder.setBolt("exclaim1", new ExclamationBolt(), 3) .shuffleGrouping("words");
builder.setBolt("exclaim2", new ExclamationBolt(), 2) .shuffleGrouping("exclaim1");
上面这些语句就是创建了一个 topoloy。 一个 bolt 可以同时的订阅多个上游的消息来源,例如:
builder.setBolt("exclaim2", new ExclamationBolt(), 5) .shuffleGrouping("words") .shuffleGrouping("exclaim1");
Config conf = new Config();
conf.setDebug(true); //是否设置调试
conf.setNumWorkers(2); //设置最大worker数
if(args!=null && args.length > 0) {
conf.setNumWorkers(5);
StormSubmitter.submitTopology(args[0], conf, builder.createTopology()); //将topology提交到线上
} else {
conf.setMaxTaskParallelism(3);
LocalCluster cluster = new LocalCluster();
cluster.submitTopology("rolling-site", conf, builder.createTopology()); //本地调试
Thread.sleep(10000);
cluster.shutdown();
}
}
设置默认的消息超时和acker任务数:
Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS #set message timeout,default 30s
Config.TOPOLOGY_ACKERS #set acker number,default 1