在学习Storm过程中,遇到了很多新的概念,最初的时候拼拼剪剪的运行了演示实例。经过一段时间的学习后,再回头看一下Storm究竟是什么。
一、Storm 框架的特点
Storm定义了一批实时计算的原语。如同hadoop大大简化了并行批量数据处理,storm的这些原语大大简化了并行实时数据处理。storm的一些关键特性如下:
1、适用场景广泛
storm可以用来处理消息和更新数据库(消息流处理), 对一个数据量进行持续的查询并返回客户端(持续计算), 对一个耗资源的查询作实时并行化的处理(分布式方法调用), storm的这些基础原语可以满足大量的场景。
2、可伸缩性高
Storm的可伸缩性可以让storm每秒可以处理的消息量达到很高。为了扩展一个实时计算任务,你所需要做的就是加机器并且提高这个计算任务的并行度设置(parallelism setting)。作为Storm可伸缩性的一个例证, 一个Storm应用在一个10个节点的集群上每秒处理1000000个消息 — 包括每秒一百多次的数据库调用。Storm使用ZooKeeper来协调集群内的各种配置使得Storm的集群可以很容易的扩展很大。
3、证无数据丢失
实时系统必须保证所有的数据被成功的处理。 那些会丢失数据的系统的适用场景非常窄, 而storm保证每一条消息都会被处理, 这一点和S4相比有巨大的反差。
4、异常健壮
不像Hadoop — 出了名的难管理, storm集群非常容易管理。容易管理是storm的设计目标之一。
5、容错性好
如果在消息处理过程中出了一些异常, storm会重新安排这个出问题的处理逻辑。 storm保证一个处理逻辑永远运行 — 除非你显式杀掉这个处理逻辑。
6、语言无关性
健壮性和可伸缩性不应该局限于一个平台。Storm的topology和消息处理组件可以用任何语言来定义, 这一点使得任何人都可以使用storm.
二、Storm 框架的关键概念
storm的一些关键概念如下:Topologies、Streams、Spouts、Bolts、Stream groupings、Reliability、Tasks、Workers、Configuration,示意图如下:
1、计算拓补: Topologies
一个实时计算应用程序的逻辑在storm里面被封装到topology对象里面, 我把它叫做计算拓补。Storm里面的topology相当于Hadoop里面的一个MapReduce Job, 它们的关键区别是:一个MapReduce Job最终总是会结束的, 然而一个storm的topoloy会一直运行,除非你显式的杀死它。 一个Topology是Spouts和Bolts组成的图状结构, 而链接Spouts和Bolts的则是Stream groupings。
2、消息源: Spouts
消息源Spouts是storm里面一个topology里面的消息生产者。一般来说消息源会从一个外部源读取数据并且向topology里面发出消息: tuple。 消息源Spouts可以是可靠的也可以是不可靠的。一个可靠的消息源可以重新发射一个tuple如果这个tuple没有被storm成功的处理, 但是一个不可靠的消息源Spouts一旦发出一个tuple就把它彻底忘了 — 也就不可能再发了。
消息源可以发射多条消息流stream。要达到这样的效果, 使用OutFieldsDeclarer.declareStream来定义多个stream, 然后使用SpoutOutputCollector来发射指定的sream。
Spout类里面最重要的方法是nextTuple要么发射一个新的tuple到topology里面或者简单的返回如果已经没有新的tuple了。要注意的是nextTuple方法不能block Spout的实现, 因为storm在同一个线程上面调用所有消息源Spout的方法。
另外两个比较重要的Spout方法是ack和fail。storm在检测到一个tuple被整个topology成功处理的时候调用ack, 否则调用fail。storm只对可靠的spout调用ack和fail。
3、消息处理者: Bolts
所有的消息处理逻辑被封装在bolts里面。 Bolts可以做很多事情: 过滤, 聚合, 查询数据库等等。
Bolts可以简单的做消息流的传递。复杂的消息流处理往往需要很多步骤, 从而也就需要经过很多Bolts。比如算出一堆图片里面被转发最多的图片就至少需要两步: 第一步算出每个图片的转发数量。第二步找出转发最多的前10个图片。
Bolts可以发射多条消息流, 使用OutputFieldsDeclarer.declareStream定义stream, 使用OutputCollector.emit来选择要发射的stream。
Bolts的主要方法是execute, 它以一个tuple作为输入,Bolts使用OutputCollector来发射tuple, Bolts必须要为它处理的每一个tuple调用OutputCollector的ack方法,以通知storm这个tuple被处理完成了。– 从而我们通知这个tuple的发射者Spouts。 一般的流程是: Bolts处理一个输入tuple, 发射0个或者多个tuple, 然后调用ack通知storm自己已经处理过这个tuple了。storm提供了一个IBasicBolt会自动调用ack。
4、消息分发策略:Stream Grouping
定义一个Topology的其中一步是定义每个bolt接受什么样的流作为输入。stream grouping就是用来定义一个stream应该分配给Bolts上面的多个Tasks。
storm里面有6种类型的stream grouping:
(1)Shuffle Grouping: 随机分组, 随机派发stream里面的tuple, 保证每个bolt接收到的tuple数目相同。
(2)Fields Grouping:按字段分组, 比如按userid来分组, 具有同样userid的tuple会被分到相同的Bolts, 而不同的userid则会被分配到不同的Bolts。
(3)All Grouping: 广播发送, 对于每一个tuple, 所有的Bolts都会收到。
(4)Global Grouping: 全局分组, 这个tuple被分配到storm中的一个bolt的其中一个task。再具体一点就是分配给id值最低的那个task。
(5)Non Grouping: 不分组, 这个分组的意思是说stream不关心到底谁会收到它的tuple。目前这种分组和Shuffle grouping是一样的效果, 有一点不同的是storm会把这个bolt放到这个bolt的订阅者同一个线程里面去执行。
(6)Direct Grouping: 直接分组, 这是一种比较特别的分组方法,用这种分组意味着消息的发送者指定由消息接收者的哪个task处理这个消息。 只有被声明为Direct Stream的消息流可以声明这种分组方法。而且这种消息tuple必须使用emitDirect方法来发射。消息处理者可以通过TopologyContext来获取处理它的消息的taskid (OutputCollector.emit方法也会返回taskid)
5、消息流: Streams
消息流是storm里面的最关键的抽象。一个消息流是一个没有边界的tuple序列, 而这些tuples会被以一种分布式的方式并行地创建和处理。 对消息流的定义主要是对消息流里面的tuple的定义, 我们会给tuple里的每个字段一个名字。 并且不同tuple的对应字段的类型必须一样。 也就是说: 两个tuple的第一个字段的类型必须一样, 第二个字段的类型必须一样, 但是第一个字段和第二个字段可以有不同的类型。在默认的情况下, tuple的字段类型可以是: integer, long, short, byte, string, double, float, boolean和byte array。你还可以自定义类型 ,只要你实现对应的序列化器。
6、任务:Task
每个Spout是一个Task,每个Bolt也是一个Task。每一个Spout和Bolt会被当作很多task在整个集群里面执行。每一个task对应到一个线程,而stream grouping则是定义怎么从一堆task发射tuple到另外一堆task。你可以调用TopologyBuilder.setSpout()和TopBuilder.setBolt来设置并行度 — 也就是有多少个task。
7、计算元组:Tuple
Tuple是组成消息流的元素。
三、Storm 编程入门
1、Topology构成
和同样是计算框架的Mapreduce相比,Mapreduce集群上运行的是Job,而Storm集群上运行的是Topology。但是Job在运行结束之后会自行结束,Topology却只能被手动的kill掉,否则会一直运行下去。
Storm集群中有两种节点,一种是控制节点(Nimbus节点),另一种是工作节点(Supervisor节点)。所有Topology任务的提交必须在Storm客户端节点上进行(需要配置~/.storm/storm.yaml文件),由Nimbus节点分配给其他Supervisor节点进行处理。Nimbus节点首先将提交的Topology进行分片,分成一个个的Task,并将Task和Supervisor相关的信息提交到zookeeper集群上,Supervisor会去zookeeper集群上认领自己的Task,通知自己的Worker进程进行Task的处理。总体的Topology处理流程图为:
每个Topology都由Spout和Bolt组成,在Spout和Bolt传递信息的基本单位叫做Tuple,由Spout发出的连续不断的Tuple及其在相应Bolt上处理的子Tuple连起来称为一个Steam,每个Stream的命名是在其首个Tuple被Spout发出的时候,此时Storm会利用内部的Ackor机制保证每个Tuple可靠的被处理。
而Tuple可以理解成键值对,其中,键就是在定义在declareStream方法中的Fields字段,而值就是在emit方法中发送的Values字段。
2、Configuration配置
在运行Topology之前,可以通过一些参数的配置来调节运行时的状态,参数的配置是通过Storm框架部署目录下的conf/storm.yaml文件来完成的。在次文件中可以配置运行时的Storm本地目录路径、运行时Worker的数目等。
在代码中,也可以设置Config的一些参数,但是优先级是不同的,不同位置配置Config参数的优先级顺序为:
default.yaml < storm.yaml < topology内部的configuration(代码中设定)
在storm.yaml中常用的几个选项为:
配置选项名称 | 配置选项作用 |
topology.max.task.parallelism | 每个Topology运行时最大的executor数目 |
topology.workers | 每个Topology运行时的worker的默认数目,若在代码中设置,则此选项值被覆盖 |
storm.zookeeper.servers | zookeeper集群的节点列表 |
storm.local.dir | Storm用于存储jar包和临时文件的本地存储目录 |
storm.zookeeper.root | Storm在zookeeper集群中的根目录,默认是“/” |
ui.port | Storm集群的UI地址端口号,默认是8080 |
nimbus.host: | Nimbus节点的host |
supervisor.slots.ports | Supervisor节点的worker占位槽,集群中的所有Topology公用这些槽位数,即使提交时设置了较大数值的槽位数,系统也会按照当前集群中实际剩余的槽位数来进行分配,当所有的槽位数都分配完时,新提交的Topology只能等待,系统会一直监测是否有空余的槽位空出来,如果有,就再次给新提交的Topology分配 |
supervisor.worker.timeout.secs | Worker的超时时间,单位为秒,超时后,Storm认为当前worker进程死掉,会重新分配其运行着的task任务 |
drpc.servers | 在使用drpc服务时,drpc server的服务器列表 |
drpc.port | 在使用drpc服务时,drpc server的服务端口
|
在代码中设定的配置有如下一些:
(1)setDebug:是否开启调试模式
(2)Config.NIMBUS_HOST: 指定nimbus主机
(3)Config.NIMBUS_THRIFT_PORT: 指定nimbus的通信端口
(4)Config.STORM_ZOOKEEPER_PORT:指定zookeeper通信端口
(5)Config.STORM_ZOOKEEPER_SERVERS:zookeeper集群IP
(6)setNumWorkers:设置工作进程,最大值为Storm集群的Slots的总数,是实际运行Topology的进程数。在前面所说的并行度则是线程数。如果设置工作进程为12,执行的并行度(executor)为48,则每个进程执行4个线程。
(7)Config.TOPOLOGY_TASKS:设置topology的任务数(task),如果不设置这个参数,则默认每隔线程执行一个任务。
(8)setNumAckers:生成多少个线程来执行acker,acker负责跟踪任务是否执行完成。如果不设置,系统默认生成一个线程来跟踪任务。
(9)setMaxSpoutPending:设置任务在发出后,但还没处理完成的中间状态任务的最大数量。
(10)setMessageTimeoutSecs:设置任务在多久之内没处理完成,就任务这个任务处理失败。
3、Spouts 数据发射源
Spout是Stream的消息产生源, Spout组件的实现可以通过继承BaseRichSpout类或者其他*Spout类来完成,也可以通过实现IRichSpout接口来实现。
需要根据情况实现Spout类中重要的几个方法有:
(1)open()方法:程序在进入Spout时需要执行的方法。
/** * We will create the file and get the collector object */ public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) { try { this.fileReader = new FileReader(conf.get("wordsFile").toString()); } catch (FileNotFoundException e) { throw new RuntimeException("Error reading file ["+conf.get("wordFile")+"]"); } this.collector = collector; }
(2)nextTuple()方法:任务执行的主要方法,其特征是永远不会结束,一直循环执行。
/** * The only thing that the methods will do It is emit each * file line */ public void nextTuple() { /** * The nextuple it is called forever, so if we have been readed the file * we will wait and then return */ if(completed){ try { Thread.sleep(1000); } catch (InterruptedException e) { //Do nothing } return; } String str; //Open the reader BufferedReader reader = new BufferedReader(fileReader); try{ //Read all lines while((str = reader.readLine()) != null){ /** * By each line emmit a new value with the line as a their */ this.collector.emit(new Values(str),str); } }catch(Exception e){ throw new RuntimeException("Error reading tuple",e); }finally{ completed = true; } }
关键代码:this.collector.emit(new Values(str),str);
这里指明发送的是字段str,后一个参数是messageId,指明这个任务的唯一id,用于acker跟踪任务的完成情况。如果省略这个参数,则系统任务这个任务是不可靠任务,发送出去后就不再跟踪,成功与否都不管。
(3)declareOutputFields()方法:声明发送到下一个Bolt的字段,如:
/** * Declare the output field "word" */ public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("line")); }
(4)ack()方法:每个tuple执行完成后调用,根据messageId跟踪tuple执行情况。
(5)fail()方法:每个tuple执行失败后调用。
4、Bolts 数据处理类
Bolt类接收由Spout或者其他上游Bolt类发来的Tuple,对其进行处理。Bolt组件的实现可以通过继承BasicRichBolt类或者IRichBolt接口来完成。
Bolt类需要实现的主要方法有:
(1)prepare()方法:程序在执行之前执行该方法,用于做以些准备工作。
/** * On create */ @Override public void prepare(Map stormConf, TopologyContext context) { this.counters = new HashMap<String, Integer>(); this.name = context.getThisComponentId(); this.id = context.getThisTaskId(); }
(2)execute()方法:主要执行的方法,所有的业务均在此处理,接受从spout传送过来的数据:
@Override public void execute(Tuple input, BasicOutputCollector collector) { String str = input.getString(0); /** * If the word dosn't exist in the map we will create * this, if not We will add 1 */ if(!counters.containsKey(str)){ counters.put(str, 1); }else{ Integer c = counters.get(str) + 1; counters.put(str, c); } }
(3)cleanup()方法:bolt执行完成后调用的方法,用于清理操作,但是仅在单机版环境下有效。
/** * At the end of the spout (when the cluster is shutdown * We will show the word counters */ @Override public void cleanup() { System.out.println("-- Word Counter ["+name+"-"+id+"] --"); for(Map.Entry<String, Integer> entry : counters.entrySet()){ System.out.println(entry.getKey()+": "+entry.getValue()); } }
(4)declareOutputFields()方法:与Spout中的方法相同,定义下一个Bolts接收的参数名称
5、Topology的构建
Topology是Storm的运行任务,构建Topology就是通知Storm怎样运行程序。在此过程中,可以定义哪些类作为Spout,哪些类作为Bolt,并定义分组策略和执行的并行度,所有这些,都定义在TopologyBuilder对象上。典型配置如下:
// Topology definition TopologyBuilder builder = new TopologyBuilder(); builder.setSpout("word-reader", new WordReader()); builder.setBolt("word-normalizer", new WordNormalizer(),12).shuffleGrouping("word-reader"); builder.setBolt("word-counter", new WordCounter()).fieldsGrouping("word-reader", new Fields("line"));
上面的代码中,把WordReader()类作为Spout,并设置其id标志为"word-reader",由于没有定义并行度,系统默认并行度为1;同时把WordNormalizer()和WordCounter()设置为Bolt,WordNormalizer()的分组策略为随机分组, 保证每个bolt接收到的tuple数目相同,并行设置并行度为12,这样Storm就会为这个Topology分配12个线程进行处理。WordCounter()的分组策略为按照字段分组,每一个line相同的tuple都会发送到同一个Bolts中处理。
分组策略重述如下:
(1)ShuffleGrouping:随机分组,随机分发Stream中的tuple,保证每个Bolt的Task接收Tuple数量大致一致;
(2)FieldsGrouping:按照字段分组,保证相同字段的Tuple分配到同一个Task中;
(3)AllGrouping:广播发送,每一个Task都会受到所有的Tuple;
(4)GlobalGrouping:全局分组,所有的Tuple都发送到同一个Task中,此时一般将当前Component的并发数目设置为1;
(5)NonGrouping:不分组,和ShuffleGrouping类似,当前Task的执行会和它的被订阅者在同一个线程中执行;
(6)DirectGrouping:直接分组,直接指定由某个Task来执行Tuple的处理,而且,此时必须有emitDirect方法来发送;
(7) localOrShuffleGrouping:和ShuffleGrouping类似,若Bolt有多个Task在同一个进程中,Tuple会随机发给这些Task。
不同的的Grouping,需要根据不同的场景来具体设定,不一而论。
6、Topology的运行
在集群中运行的代码都需要打包提交到master机器中,Storm也不例外。这里需要一个第三方提供的打包工具EJob:
EJob.java 完整代码如下:
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; public class EJob { private static ArrayList<URL> classPath = new ArrayList<URL>(); /** Unpack a jar file into a directory. */ public static void unJar(File jarFile, File toDir) throws IOException { JarFile jar = new JarFile(jarFile); try { Enumeration entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = (JarEntry) entries.nextElement(); if (!entry.isDirectory()) { InputStream in = jar.getInputStream(entry); try { File file = new File(toDir, entry.getName()); if (!file.getParentFile().mkdirs()) { if (!file.getParentFile().isDirectory()) { throw new IOException("Mkdirs failed to create " + file.getParentFile().toString()); } } OutputStream out = new FileOutputStream(file); try { byte[] buffer = new byte[8192]; int i; while ((i = in.read(buffer)) != -1) { out.write(buffer, 0, i); } } finally { out.close(); } } finally { in.close(); } } } } finally { jar.close(); } } /** * Run a Hadoop job jar. If the main class is not in the jar's manifest, then * it must be provided on the command line. */ public static void runJar(String[] args) throws Throwable { String usage = "jarFile [mainClass] args..."; if (args.length < 1) { System.err.println(usage); System.exit(-1); } int firstArg = 0; String fileName = args[firstArg++]; File file = new File(fileName); String mainClassName = null; JarFile jarFile; try { jarFile = new JarFile(fileName); } catch (IOException io) { throw new IOException("Error opening job jar: " + fileName).initCause(io); } Manifest manifest = jarFile.getManifest(); if (manifest != null) { mainClassName = manifest.getMainAttributes().getValue("Main-Class"); } jarFile.close(); if (mainClassName == null) { if (args.length < 2) { System.err.println(usage); System.exit(-1); } mainClassName = args[firstArg++]; } mainClassName = mainClassName.replaceAll("/", "."); File tmpDir = new File(System.getProperty("java.io.tmpdir")); tmpDir.mkdirs(); if (!tmpDir.isDirectory()) { System.err.println("Mkdirs failed to create " + tmpDir); System.exit(-1); } final File workDir = File.createTempFile("hadoop-unjar", "", tmpDir); workDir.delete(); workDir.mkdirs(); if (!workDir.isDirectory()) { System.err.println("Mkdirs failed to create " + workDir); System.exit(-1); } Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { try { fullyDelete(workDir); } catch (IOException e) { } } }); unJar(file, workDir); classPath.add(new File(workDir + "/").toURL()); classPath.add(file.toURL()); classPath.add(new File(workDir, "classes/").toURL()); File[] libs = new File(workDir, "lib").listFiles(); if (libs != null) { for (int i = 0; i < libs.length; i++) { classPath.add(libs[i].toURL()); } } ClassLoader loader = new URLClassLoader(classPath.toArray(new URL[0])); Thread.currentThread().setContextClassLoader(loader); Class<?> mainClass = Class.forName(mainClassName, true, loader); Method main = mainClass.getMethod("main", new Class[] { Array.newInstance( String.class, 0).getClass() }); String[] newArgs = Arrays.asList(args).subList(firstArg, args.length) .toArray(new String[0]); try { main.invoke(null, new Object[] { newArgs }); } catch (InvocationTargetException e) { throw e.getTargetException(); } } /** * Delete a directory and all its contents. If we return false, the directory * may be partially-deleted. */ public static boolean fullyDelete(File dir) throws IOException { File contents[] = dir.listFiles(); if (contents != null) { for (int i = 0; i < contents.length; i++) { if (contents[i].isFile()) { if (!contents[i].delete()) { return false; } } else { // try deleting the directory // this might be a symlink boolean b = false; b = contents[i].delete(); if (b) { // this was indeed a symlink or an empty directory continue; } // if not an empty directory or symlink let // fullydelete handle it. if (!fullyDelete(contents[i])) { return false; } } } } return dir.delete(); } /** * Add a directory or file to classpath. * * @param component */ public static void addClasspath(String component) { if ((component != null) && (component.length() > 0)) { try { File f = new File(component); if (f.exists()) { URL key = f.getCanonicalFile().toURL(); if (!classPath.contains(key)) { classPath.add(key); } } } catch (IOException e) { } } } /** * Add default classpath listed in bin/hadoop bash. * * @param hadoopHome */ public static void addDefaultClasspath(String hadoopHome) { // Classpath initially contains conf dir. addClasspath(hadoopHome + "/conf"); // For developers, add Hadoop classes to classpath. addClasspath(hadoopHome + "/build/classes"); if (new File(hadoopHome + "/build/webapps").exists()) { addClasspath(hadoopHome + "/build"); } addClasspath(hadoopHome + "/build/test/classes"); addClasspath(hadoopHome + "/build/tools"); // For releases, add core hadoop jar & webapps to classpath. if (new File(hadoopHome + "/webapps").exists()) { addClasspath(hadoopHome); } addJarsInDir(hadoopHome); addJarsInDir(hadoopHome + "/build"); // Add libs to classpath. addJarsInDir(hadoopHome + "/lib"); addJarsInDir(hadoopHome + "/lib/jsp-2.1"); addJarsInDir(hadoopHome + "/build/ivy/lib/Hadoop/common"); } /** * Add all jars in directory to classpath, sub-directory is excluded. * * @param dirPath */ public static void addJarsInDir(String dirPath) { File dir = new File(dirPath); if (!dir.exists()) { return; } File[] files = dir.listFiles(); if (files == null) { return; } for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { continue; } else { addClasspath(files[i].getAbsolutePath()); } } } /** * Create a temp jar file in "java.io.tmpdir". * * @param root * @return * @throws IOException */ public static File createTempJar(String root) throws IOException { if (!new File(root).exists()) { return null; } Manifest manifest = new Manifest(); manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); final File jarFile = File.createTempFile("EJob-", ".jar", new File(System .getProperty("java.io.tmpdir"))); Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { jarFile.delete(); } }); JarOutputStream out = new JarOutputStream(new FileOutputStream(jarFile), manifest); createTempJarInner(out, new File(root), ""); out.flush(); out.close(); return jarFile; } private static void createTempJarInner(JarOutputStream out, File f, String base) throws IOException { if (f.isDirectory()) { File[] fl = f.listFiles(); if (base.length() > 0) { base = base + "/"; } for (int i = 0; i < fl.length; i++) { createTempJarInner(out, fl[i], base + fl[i].getName()); } } else { out.putNextEntry(new JarEntry(base)); FileInputStream in = new FileInputStream(f); byte[] buffer = new byte[1024]; int n = in.read(buffer); while (n != -1) { out.write(buffer, 0, n); n = in.read(buffer); } in.close(); } } /** * Return a classloader based on user-specified classpath and parent * classloader. * * @return */ public static ClassLoader getClassLoader() { ClassLoader parent = Thread.currentThread().getContextClassLoader(); if (parent == null) { parent = EJob.class.getClassLoader(); } if (parent == null) { parent = ClassLoader.getSystemClassLoader(); } return new URLClassLoader(classPath.toArray(new URL[0]), parent); } }
Storm可以在 本地模式 和 集群模式 运行,以下代码包含了两种模式的提交代码
import backtype.storm.Config; import backtype.storm.LocalCluster; import backtype.storm.topology.TopologyBuilder; import backtype.storm.tuple.Fields; import com.bolts.WordCounter; import com.spouts.WordReader; import com.wordcount.bolts.WordNormalizer; public class TopologyMain { private static String numbus_host = "192.168.100.1"; private static String storm_zookeeper_servers = "192.168.100.1,192.168.100.2,192.168.100.3"; public static void main(String[] args) throws Exception { String res = ""; // Topology definition TopologyBuilder builder = new TopologyBuilder(); builder.setSpout("word-reader", new WordReader()); builder.setBolt("word-normalizer", new WordNormalizer()).shuffleGrouping("word-reader"); builder.setBolt("word-counter", new WordCounter()).fieldsGrouping("word-reader", new Fields("line")); // Configuration Config conf = new Config(); conf.put("file", res); conf.setDebug(false); conf.put(Config.NIMBUS_HOST, numbus_host); conf.put(Config.NIMBUS_THRIFT_PORT, 6627); conf.put(Config.STORM_ZOOKEEPER_PORT, 2181); conf.put(Config.STORM_ZOOKEEPER_SERVERS, storm_zookeeper_servers); conf.setNumWorkers(25); conf.setNumAckers(50); conf.setMaxSpoutPending(5000); conf.setMessageTimeoutSecs(120); // 在集群中运行storm File jarFile = EJob.createTempJar(TopologyMain.class.getClassLoader().getResource("").getPath()); ClassLoader classLoader = EJob.getClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); System.setProperty("storm.jar", jarFile.toString()); StormSubmitter.submitTopology("test", conf, builder.createTopology()); // 在本地运行storm // LocalCluster cluster = new LocalCluster(); // cluster.submitTopology("wordCount", conf, builder.createTopology()); // Thread.sleep(1000 * 60); // cluster.shutdown(); } }
6.1、Topology的运行流程
(1)Storm提交后,会把代码首先存放到Nimbus节点的inbox目录下,之后,会把当前Storm运行的配置生成一个stormconf.ser文件放到Nimbus节点的stormdist目录中,在此目录中同时还有序列化之后的Topology代码文件;
(2)在设定Topology所关联的Spouts和Bolts时,可以同时设置当前Spout和Bolt的executor数目和task数目,默认情况下,一个Topology的task的总和是和executor的总和一致的。之后,系统根据worker的数目,尽量平均的分配这些task的执行。worker在哪个supervisor节点上运行是由storm本身决定的;
(3)任务分配好之后,Nimbes节点会将任务的信息提交到zookeeper集群,同时在zookeeper集群中会有workerbeats节点,这里存储了当前Topology的所有worker进程的心跳信息;
(4)Supervisor节点会不断的轮询zookeeper集群,在zookeeper的assignments节点中保存了所有Topology的任务分配信息、代码存储目录、任务之间的关联关系等,Supervisor通过轮询此节点的内容,来领取自己的任务,启动worker进程运行;
(5)一个Topology运行之后,就会不断的通过Spouts来发送Stream流,通过Bolts来不断的处理接收到的Stream流,Stream流是无界的。
最后一步会不间断的执行,除非手动结束Topology。
6.2、Topology的方法调用流程
Topology中的Stream处理时的方法调用过程如下:
有几点需要说明的地方:
(1)每个组件(Spout或者Bolt)的构造方法和declareOutputFields方法都只被调用一次。
(2)open方法、prepare方法的调用是多次的。入口函数中设定的setSpout或者setBolt里的并行度参数指的是executor的数目,是负责运行组件中的task的线程的数目,此数目是多少,上述的两个方法就会被调用多少次,在每个executor运行的时候调用一次。相当于一个线程的构造方法。
(3)nextTuple方法、execute方法是一直被运行的,nextTuple方法不断的发射Tuple,Bolt的execute不断的接收Tuple进行处理。只有这样不断地运行,才会产生无界的Tuple流,体现实时性。相当于线程的run方法。
(4)在提交了一个topology之后,Storm就会创建spout/bolt实例并进行序列化。之后,将序列化的component发送给所有的任务所在的机器(即Supervisor节点),在每一个任务上反序列化component。
(5)Spout和Bolt之间、Bolt和Bolt之间的通信,是通过zeroMQ的消息队列实现的。
(6)上图没有列出ack方法和fail方法,在一个Tuple被成功处理之后,需要调用ack方法来标记成功,否则调用fail方法标记失败,重新处理这个Tuple。
6.3、Topology并行度设置
在Topology的执行单元里,有几个和并行度相关的概念。
(1)worker:每个worker都属于一个特定的Topology,每个Supervisor节点的worker可以有多个,每个worker使用一个单独的端口,它对Topology中的每个component运行一个或者多个executor线程来提供task的运行服务。
(2)executor:executor是产生于worker进程内部的线程,会执行同一个component的一个或者多个task。
(3)task:实际的数据处理由task完成,在Topology的生命周期中,每个组件的task数目是不会发生变化的,而executor的数目却不一定。executor数目小于等于task的数目,默认情况下,二者是相等的。
在运行一个Topology时,可以根据具体的情况来设置不同数量的worker、task、executor,而设置的位置也可以在多个地方。
(1)worker设置:
(1.1)可以通过设置yaml中的topology.workers属性
(1.2)在代码中通过Config的setNumWorkers方法设定
(2)executor设置:
通过在Topology的入口类中setBolt、setSpout方法的最后一个参数指定,不指定的话,默认为1;
(3)task设置:
(3.1) 默认情况下,和executor数目一致;
(3.2)在代码中通过TopologyBuilder的setNumTasks方法设定具体某个组件的task数目。
6.4、Topology任务的结束
通过在Nimbus节点利用如下命令来终止一个Topology的运行:
storm kill topologyName
kill之后,可以通过UI界面查看topology状态,会首先变成KILLED状态,在清理完本地目录和zookeeper集群中的和当前Topology相关的信息之后,此Topology就会彻底消失了。
6.5、Topology任务的监控
Topology提交后,可以在Nimbus节点的web界面查看,默认的地址是http://NimbusIp:8080。
7、Storm应用场景
上面给出了如何编写Storm框架任务Topology的方法,那么在哪些场景下能够使用Storm框架呢?下面介绍Storm框架的几个典型的应用场景。
(1)利用Storm框架的DRPC进行大量的函数并行调用,即实现分布式的RPC;
(2)利用Storm框架的Transaction Topology,可以进行实时性的批量更新或者查询数据库操作或者应用需要同一批内的消息以及批与批之间的消息并行处理这样的场景,此时Topology中只能有一个TrasactionalSpout;
(3)利用滑动窗口的逻辑结合Storm框架来计算得出某段时间内的售出量最多的产品、购买者最多的TopN地区等;
(4)精确的广告推送,在用户浏览产品的时候,将浏览记录实时性的搜集,发送到Bolt,由Bolt来根据用户的账户信息(如果有的话)完成产品的分类统计,产品的相关性查询等逻辑计算之后,将计算结果推送给用户;
(5)实时日志的处理,Storm可以和一个分布式存储结合起来,实时性的从多个数据源发送数据到处理逻辑Bolts,Bolts完成一些逻辑处理之后,交给分布式存储框架进行存储,此时,Spout可以是多个;
(6)实时性的监控舆论热点,比如针对某个关键词,在用户查询的时候,产生数据源Spout,结合语义分析等,由Bolt来完成查询关键词的统计分析,汇总当前的舆论热点;
(7)数据流的实时聚合操作。
参考资料:
http://www.cnblogs.com/Scott007/p/3320938.html
http://xumingming.sinaapp.com/138/twitter-storm%E5%85%A5%E9%97%A8/