Storm入门实例之单词计数

来源:互联网 发布:淘宝联盟怎么招代理 编辑:程序博客网 时间:2024/06/06 20:12

1 环境搭建

    关于Storm集群的搭建可以参考用两台物理机搭建storm集群。本文将统一使用apache-storm-0.9.5来运行拓扑,使用Maven + IDEA的方式进行开发(参考 Ubuntu下Maven安装和使用)。

2 项目源码

2.1 配置pom.xml

    有一个要注意的地方,就是storm-core.jar这个依赖在打包时的作用域必须是provided,因为集群环境会提供这个jar。但是在开发时要用到,在本地才能运行成功。

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>com.stormdemo</groupId>    <artifactId>StormDemo</artifactId>    <version>1.0-SNAPSHOT</version>    <packaging>jar</packaging>    <properties>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    </properties>    <repositories>        <!-- Repository where we can found the storm dependencies  -->        <repository>            <id>clojars.org</id>            <url>http://clojars.org/repo</url>        </repository>    </repositories>    <dependencies>        <dependency>            <groupId>org.apache.storm</groupId>            <artifactId>storm-core</artifactId>            <!-- 这里的版本号必须和集群中的storm版本保持一致-->            <version>0.9.5</version>            <!--              打包时不能把storm打进去,必须是provided。而在IDEA开发调试时必须将该行注释掉,因为IDEA存在bug。            -->            <scope>provided</scope>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-assembly-plugin</artifactId>                <version>2.5.5</version>                <configuration>                    <archive>                        <manifest>                            <mainClass>com.stormdemo.MyTopology</mainClass>                        </manifest>                    </archive>                    <descriptorRefs>                        <descriptorRef>jar-with-dependencies</descriptorRef>                    </descriptorRefs>                </configuration>                <!-- mvn package时,绑定运行mvn package assembly:single-->                <executions>                    <execution>                        <id>make-assembly</id>                        <phase>package</phase>                        <goals>                            <goal>single</goal>                        </goals>                    </execution>                </executions>            </plugin>        </plugins>    </build></project>

2.2 输出文本行ReadLineSpout

    这个类将是所有bolt数据的来源,这里模拟实际生产环境,将文本行保留在源码中,不断地随机输出一行一行的数据。当然也提供了输出一次完整数据,然后进入休眠的接口,来观察代码是否正确执行,具体哪种情况取决于传递的参数type。

package com.stormdemo;import backtype.storm.spout.SpoutOutputCollector;import backtype.storm.task.TopologyContext;import backtype.storm.topology.OutputFieldsDeclarer;import backtype.storm.topology.base.BaseRichSpout;import backtype.storm.tuple.Fields;import backtype.storm.tuple.Values;import backtype.storm.utils.Utils;import java.util.ArrayList;import java.util.Map;import java.util.Random;/** * 喷头,用于读取数据,并将数据往下传递,是数据的源头。 * Created by gzx on 17-2-4. */public class ReadLineSpout extends BaseRichSpout{    private SpoutOutputCollector collector;    private Map conf;    private boolean finished;    private int type;    private final String[] lines = {    "long long ago I like playing with cat",    "playing with cat make me happy",    "I feel happy to be with you",    "you give me courage",    "I like to be together with you",    "long long ago I like you"    };    private ArrayList<String> arrayList;    /**     * 相当于构造函数,做一些初始化工作。     * @param map  map可以获取配置信息,这些配置信息在TopologyBuilder时被设置好。     * @param topologyContext     * @param spoutOutputCollector   用于传递Tuple     */    public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {        this.collector = spoutOutputCollector;        this.conf = map;        this.type = Integer.parseInt(conf.get("type").toString());        this.arrayList = new ArrayList<String>();        for(String line : lines){            arrayList.add(line);        }    }    /**     * 这个函数或被不断调用,这里通过随机数的方式不断发送文本行,模拟流数据。     */    public void nextTuple() {        if(type == 1){            emitOnce();        }        else{            emitRandomAndContinue();        }    }    /*        type 1        只发送一次,且顺序发送     */    private void emitOnce(){        if(finished){            Utils.sleep(100);            return;        }        for(String line : lines){            this.collector.emit(new Values(line));        }        finished = true;    }    /*       type 2       不断随机发送    */    private void emitRandomAndContinue(){        Random random = new Random();        int id = random.nextInt();        id = id < 0? -id : id;        id = id % arrayList.size();        this.collector.emit(new Values(arrayList.get(id)));    }    /**     * 声明发送的元祖tuple,必须与Values中的元素个数对应,在下一个bolt中可以通过对应的名字或者所在数字顺序获得这个Tuple     * @param outputFieldsDeclarer     */    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {        outputFieldsDeclarer.declare(new Fields("line"));    }}

2.3 对文本行进行分割SplitLineBolt

    这个Bolt将ReadLineSpout传递过来的文本行进行分割,得到一个个单词,并将单词往下传送。

package com.stormdemo;import backtype.storm.task.OutputCollector;import backtype.storm.task.TopologyContext;import backtype.storm.topology.OutputFieldsDeclarer;import backtype.storm.topology.base.BaseRichBolt;import backtype.storm.tuple.Fields;import backtype.storm.tuple.Tuple;import backtype.storm.tuple.Values;import java.util.Map;/** * Created by gzx on 17-2-4. */public class SplitLineBolt extends BaseRichBolt{    private OutputCollector outputCollector;    /**     * 初始化函数,相当于spout中的open     * @param map     * @param topologyContext     * @param outputCollector     */    public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {        this.outputCollector = outputCollector;    }    /**     * 这个函数也会被不断执行,但它的数据来自于上游。     * 这里将文本行分割为单词,并发送     * @param tuple     */    public void execute(Tuple tuple) {        String line = tuple.getStringByField("line");        String[] words = line.split(" ");        for(String word : words){            word = word.trim();            if(word.equals("")){                continue;            }            this.outputCollector.emit(new Values(word));        }    }    /**     * 声明发送的数据项,与上面的函数对应。     * @param outputFieldsDeclarer     */    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {        outputFieldsDeclarer.declare(new Fields("word"));    }}

2.4 对单词进行计数CountWordBolt

    这个Bolt对SplitLineBolt传递过来的单词进行计数,并保存在HashMap中,并将该单词及其对应的次数往下传递。

package com.stormdemo;import backtype.storm.task.OutputCollector;import backtype.storm.task.TopologyContext;import backtype.storm.topology.OutputFieldsDeclarer;import backtype.storm.topology.base.BaseRichBolt;import backtype.storm.tuple.Fields;import backtype.storm.tuple.Tuple;import backtype.storm.tuple.Values;import java.util.HashMap;import java.util.Map;/** * Created by gzx on 17-2-4. */public class CountWordBolt extends BaseRichBolt {    private OutputCollector outputCollector;    // 保存数据,生产环境下应保存到数据库中    private HashMap<String, Integer> countWords;    public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {        this.outputCollector = outputCollector;        this.countWords = new HashMap<String, Integer>();    }    /**     * 获取上游的word,并对应增加计数,最后输出展示,实际应当更新数据库     * @param tuple     */    public void execute(Tuple tuple) {        String word = tuple.getStringByField("word");        int cnt = 1;        if(countWords.containsKey(word)){            cnt = countWords.get(word) + 1;        }        countWords.put(word, cnt);        this.outputCollector.emit(new Values(word, cnt));    }    /*        传递两个数据项     */    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {        outputFieldsDeclarer.declare(new Fields("word", "cnt"));    }}

2.5 输出单词及其次数ReportWordBolt

    这个Bolt不断对CountWordBolt传递过来的单词及其次数进行输出。

package com.stormdemo;import backtype.storm.task.OutputCollector;import backtype.storm.task.TopologyContext;import backtype.storm.topology.OutputFieldsDeclarer;import backtype.storm.topology.base.BaseRichBolt;import backtype.storm.tuple.Tuple;import java.util.Map;/** * Created by gzx on 17-2-6. */public class ReportWordBolt extends BaseRichBolt {    public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {    }    /**     * 打印单词及其出现次数     * @param tuple     */    public void execute(Tuple tuple) {        String word = tuple.getStringByField("word");        int cnt = tuple.getIntegerByField("cnt");        System.out.printf("%s\t%d\n", word, cnt);    }    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {    }}

2.6 搭建拓扑MyTopology

    这里将前面所有的Spout和Bolt连接起来,形成一个拓扑,并运行本地提交和远程提交。具体的拓扑是:ReadLineSpout ->SplitLineBolt -> CountWordBolt -> ReportWordBolt。

package com.stormdemo;import backtype.storm.Config;import backtype.storm.LocalCluster;import backtype.storm.StormSubmitter;import backtype.storm.generated.AlreadyAliveException;import backtype.storm.generated.InvalidTopologyException;import backtype.storm.topology.TopologyBuilder;import backtype.storm.tuple.Fields;import backtype.storm.utils.Utils;/** * 这个类组建拓扑,并提交拓扑 * Created by gzx on 17-2-4. */public class MyTopology {    public static void main(String[] args) throws AlreadyAliveException, InvalidTopologyException{        // 组建拓扑,并使用流分组        TopologyBuilder builder = new TopologyBuilder();        // 使用一个线程表示一个喷头,多个线程将使得单词出现次数翻倍        builder.setSpout("ReadLineSpout", new ReadLineSpout(), 1);        // 接收来自ReadLineSpout的数据,连边,随机分组        builder.setBolt("SplitLineBolt", new SplitLineBolt(), 2).shuffleGrouping("ReadLineSpout");        // 接收来自SplitLineBolt的word数据,相同的单词分发到相同的线程        builder.setBolt("CountWordBolt", new CountWordBolt(), 2).fieldsGrouping("SplitLineBolt", new Fields("word"));        // 将所有单词及其次数进行汇总输出        builder.setBolt("ReportWordBolt", new ReportWordBolt(), 2).globalGrouping("CountWordBolt");        // 保存配置信息        Config config = new Config();        // 关闭debug模式        config.setDebug(false);        // 默认type = 1,将输出一次完整数据。type = 2不断随机输出文本行        config.put("type", "1");        // 通过是否传递任意参数,来决定是使用远程提交集群,还是提交本地集群。远程集群可以决定spout使用哪种发送方式,一般是2。        // 有参数,远程集群提交        if(args.length > 0){            // 拓扑名称            StormSubmitter.submitTopology("topology-produce", config, builder.createTopology());            config.put("type", args[0]);        }        else{// 没参数本地提交,只能输出一次完整文本行            LocalCluster localCluster = new LocalCluster();            localCluster.submitTopology("topology-devlopment", config, builder.createTopology());            // 休眠4秒,Utils.sleep不会抛出受干扰异常,尤其在Spout休眠时,集群突然shutdown            Utils.sleep(4000);            // 手动关闭本地集群            localCluster.shutdown();        }    }}

3 本地运行

    调到根目录下,运行如下命令,这里不指定参数,使用本地提交,且只输出一次完整文本行来统计,就进入休眠,4秒过后手动关闭本地集群。

mvn exec:java -Dexec.mainClass='com.stormdemo.MyTopology'

    部分运行结果:
    zookeeper和nimbus启动:
这里写图片描述

    supervisor启动:
这里写图片描述

    可以看出,nimbus收到提交的拓扑,然后supervisor启动一个worker进程来执行该拓扑,该拓扑有8个executor。
这里写图片描述

    开始执行代码,并输出结果,可以看到拓扑是以executor为单位来执行的。
这里写图片描述这里写图片描述

    shutdown整个本地集群:
这里写图片描述

    所以,本地提交的话,会模拟整个集群的运行,包括在单机上启动zookeeper、nimbus、supervisor守护进程,启动worker进程,启动exectuor来执行spout/bolt。在项目的根目录下生成一个storm-local目录,记录节点的信息。最后必须手动进行shutdown。

4 在两台物理机下运行

4.1 对项目进行打包

    关于集群的建立可以参考用两台物理机搭建storm集群。在命令行下切到项目的根目录进行打包:

mvn package

4.2 启动集群

    在默认配置storm的机器上启动zookeeper和nimbus:

zkServer.sh startstorm nimbus &storm ui &

    如下:
这里写图片描述

这里写图片描述

    在另一台机器上运行supervisor:
这里写图片描述
    最后通过访问http://192.168.43.209:8080/index.html,可以观察到有一个supervisor启动了。
这里写图片描述

4.3 上传拓扑

    需要指定main入口和传递的参数。这里传递参数2,表示使用远程集群,并不断输出。

 storm jar target/StormDemo-1.0-SNAPSHOT-jar-with-dependencies.jar 'com.stormdemo.MyTopology' 2

    上传结果:
这里写图片描述

    UI页面:
这里写图片描述

    点击拓扑名字topology-produce:
这里写图片描述

这里写图片描述

    点击show visualization:
这里写图片描述

    在supervisor这台机器的$STORM_HOME/logs/worker-6700.log日志有输出单词及其次数(在nimbus的logs下有nimbus.log和ui.log):
这里写图片描述

    最后终止拓扑的运行,不然下次还是存在这个拓扑。

storm kill topology-produce

这里写图片描述

    在/var/zookeeper/下有version-2目录和zookeeper_server.pid文件,其中文件保存的是当前zookeeper的进程ID。在nimbus的storm数据目录/var/stormData下有nimbus目录。在supervisor目录下有supervisors和workers两个目录。后面这两个都有相应的上传jar包。

附录
项目源码下载

0 0
原创粉丝点击