首页 文章 精选 留言 我的

精选列表

搜索[twitter],共2535篇文章
优秀的个人博客,低调大师

Twitter CEO Twitter账户被黑,盘古越狱 iOS 10 | 宅客周刊

1.一言不合就越狱!盘古团队越狱iOS 10 iOS 每一次大版本的升级,都会让安全性大大提升。但在真正的越狱大牛眼里,这不是什么问题。 在今天的 MOSEC 移动安全技术峰会上,声名赫赫的安全团队盘古毫无征兆地在演讲末尾加了一个“One More Thing”——越狱 iOS 10 Beta 1。 作为主办方,盘古团队最后一个登场,演示了这个视频,在视频中,仅仅数次点击,就成功安装了cydia。 盘古团队老牌“越狱达人”徐昊曾经是中国首位越狱 iOS 的黑客,他告诉雷锋网(公众号:雷锋网) 由于iOS增加了很多新的防御机制,这导致越狱的难度变大。但是,盘古手中掌握的几个重要漏洞,仍然可以使用。 这说明,至少苹果在 iOS 10 的 beta 1 版本中,还没有修复这些漏洞。徐昊表示,暂时不知道在 iOS 的后续版本中是否会封堵

优秀的个人博客,低调大师

第 135 章 Twitter Storm

135.1.单机版 操作系统环境:Ubuntu 13.04 KVM虚拟机 安装 storm 涉及到安装以下包:python、zookeeper、zeromq、jzmq、storm 过程135.1.Ubuntu + Storm 单机环境安装 安装 zookeeper $ sudo apt-get install zookeeper zookeeper-bin zookeeperd 安装 zeromq 与 jzmq $ sudo apt-get install openjdk-7-jdk $ sudo apt-get install libzmq-dev 设置 JAVA_HOME 环境变量 $ export JAVA_HOME="/usr/lib/jvm/java-7-openjdk-amd64" 编译安装 jzmq $ cd /usr/local/src/ $ git clone git://github.com/nathanmarz/jzmq.git $ cd jzmq $ ./autogen.sh $ ./configure --prefix=/srv/jzmq-2.1.0 $ javac src/org/zeromq/*.java $ touch src/classdist_noinst.stamp $ make && make install $ ls /srv/jzmq-2.1.0/ lib share 安装 Storm 下载地址:https://github.com/nathanmarz/storm/downloads $ sudo apt-get install unzip $ cd /usr/local/src/ $ $ wget https://github.com/downloads/nathanmarz/storm/storm-0.8.1.zip $ unzip storm-0.8.1.zip $ $ sudo mv storm-0.8.1 /srv/ $ sudo ln -s /srv/storm-0.8.1 /srv/storm cat >> ~/.bashrc <<'EOF' export JAVA_HOME="/usr/lib/jvm/java-7-openjdk-amd64" export ZOOKEEPER_HOME="/usr/lib/zookeeper" export STORM_HOME=/srv/storm export PATH=$PATH:$PATH:$ZOOKEEPER_HOME/bin:$STORM_HOME/bin EOF 修改 Storm CONF_DIR 目录位置 $ sed -i 's:CONF_DIR = os.path.expanduser("~/.storm"):CONF_DIR = "/srv/storm-0.8.1/conf":' /srv/storm/bin/storm 启动服务 启动 zookeeper 服务 $ sudo service zookeeper start 启动 storm 服务 /srv/storm/bin/storm nimbus & /srv/storm/bin/storm supervisor & /srv/storm/bin/storm ui & Storm UI http://192.168.6.2:8080/ 查看 Java 进程 $ jps 10382 Jps 10228 supervisor 10294 core 10173 nimbus 测试 Storm $ sudo apt-get install maven2 $ git clone git://github.com/nathanmarz/storm-starter $ cd storm-starter $ mvn -f m2-pom.xml package 先进入Storm UI http://192.168.6.2:8080/, 然后运行下面与命令 $ storm jar target/storm-starter-0.0.1-SNAPSHOT-jar-with-dependencies.jar storm.starter.WordCountTopology wordcountTop 在次进入 Storm UI 你会看到 wordcountTop 原文出处:Netkiller 系列 手札 本文作者:陈景峯 转载请与作者联系,同时请务必标明文章原始出处和作者信息及本声明。

优秀的个人博客,低调大师

twitter storm源码走读(二)

topology提交过程分析 概要 storm cluster可以想像成为一个工厂,nimbus主要负责从外部接收订单和任务分配。除了从外部接单,nimbus还要将这些外部订单转换成为内部工作分配,这个时候nimbus充当了调度室的角色。supervisor作为中层干部,职责就是生产车间的主任,他的日常工作就是时刻等待着调度到给他下达新的工作。作为车间主任,supervisor领到的活是不用自己亲力亲为去作的,他手下有着一班的普通工人。supervisor对这些工人只会喊两句话,开工,收工。注意,讲收工的时候并不意味着worker手上的活已经干完了,只是进入休息状态而已。 topology的提交过程涉及到以下角色。 storm client 负责将用户创建的topology提交到nimbus nimbus 通过thrift接口接收用户提交的topology supervisor根据zk接口上提示的消息下载最新的任务安排,并负责启动worker workerworker内可以运行task,这些task要么属于bolt类型,要么属于spout类型 executorexecutor是一个个运行的线程,同一个executor内可以运行同一种类型的task,即一个线程中的task要么全部是bolt类型,要么全部是spout类型 一个worker等同于一个进程,一个executor等同于一个线程,同一个线程中能够运行一或多个tasks。在0.8.0版之前,一个task是对应于一个线程的,在0.8.0版本中引入了executor概念,变化引入之后,task与thread之间的一一对应关系就取消了,同时在zookeeper server中原本存在的tasks-subtree也消失了,有关这个变化,可以参考http://storm-project.net/2012/08/02/storm080-released.html storm client storm client需要执行下面这句指令将要提交的topology提交给storm cluster 假设jar文件名为storm-starter-0.0.1-snapshot-standalone.jar,启动程序为 storm.starter.ExclamationTopology,给这个topology起的名称为exclamationTopology. #./storm jar $HOME/working/storm-starter/target/storm-starter-0.0.1-SNAPSHOT-standalone.jar storm.starter.ExclamationTopology exclamationTopology 这么短短的一句话对于storm client来说,究竟意味着什么呢? 源码面前是没有任何秘密可言的,那好打开storm client的源码文件 def jar(jarfile, klass, *args): """Syntax: [storm jar topology-jar-path class ...] Runs the main method of class with the specified arguments. The storm jars and configs in ~/.storm are put on the classpath. The process is configured so that StormSubmitter (http://nathanmarz.github.com/storm/doc/backtype/storm/StormSubmitter.html) will upload the jar at topology-jar-path when the topology is submitted. """ exec_storm_class( klass, jvmtype="-client", extrajars=[jarfile, USER_CONF_DIR, STORM_DIR + "/bin"], args=args, jvmopts=["-Dstorm.jar=" + jarfile]) def exec_storm_class(klass, jvmtype="-server", jvmopts=[], extrajars=[], args=[], fork=False): global CONFFILE all_args = [ "java", jvmtype, get_config_opts(), "-Dstorm.home=" + STORM_DIR, "-Djava.library.path=" + confvalue("java.library.path", extrajars), "-Dstorm.conf.file=" + CONFFILE, "-cp", get_classpath(extrajars), ] + jvmopts + [klass] + list(args) print "Running: " + " ".join(all_args) if fork: os.spawnvp(os.P_WAIT, "java", all_args) else: os.execvp("java", all_args) # replaces the current process and never returns exec_storm_class说白了就是要运行传进来了的WordCountTopology类中main函数,再看看main函数的实现 public static void main(String[] args) throws Exception { TopologyBuilder builder = new TopologyBuilder(); builder.setSpout("spout", new RandomSentenceSpout(), 5); builder.setBolt("split", new SplitSentence(), 8).shuffleGrouping("spout"); builder.setBolt("count", new WordCount(), 12).fieldsGrouping("split", new Fields("word")); Config conf = new Config(); conf.setDebug(true); if (args != null && args.length > 0) { conf.setNumWorkers(3); StormSubmitter.submitTopology(args[0], conf, builder.createTopology()); } } 对于storm client侧来说,最主要的函数StormSubmitter露出了真面目,submitTopology才是我们真正要研究的重点。 public static void submitTopology(String name, Map stormConf, StormTopology topology, SubmitOptions opts) throws AlreadyAliveException, InvalidTopologyException { if(!Utils.isValidConf(stormConf)) { throw new IllegalArgumentException("Storm conf is not valid. Must be json-serializable"); } stormConf = new HashMap(stormConf); stormConf.putAll(Utils.readCommandLineOpts()); Map conf = Utils.readStormConfig(); conf.putAll(stormConf); try { String serConf = JSONValue.toJSONString(stormConf); if(localNimbus!=null) { LOG.info("Submitting topology " + name + " in local mode"); localNimbus.submitTopology(name, null, serConf, topology); } else { NimbusClient client = NimbusClient.getConfiguredClient(conf); if(topologyNameExists(conf, name)) { throw new RuntimeException("Topology with name `" + name + "` already exists on cluster"); } submitJar(conf); try { LOG.info("Submitting topology " + name + " in distributed mode with conf " + serConf); if(opts!=null) { client.getClient().submitTopologyWithOpts(name, submittedJar, serConf, topology, opts); } else { // this is for backwards compatibility client.getClient().submitTopology(name, submittedJar, serConf, topology); } } catch(InvalidTopologyException e) { LOG.warn("Topology submission exception", e); throw e; } catch(AlreadyAliveException e) { LOG.warn("Topology already alive exception", e); throw e; } finally { client.close(); } } LOG.info("Finished submitting topology: " + name); } catch(TException e) { throw new RuntimeException(e); } } submitTopology函数其实主要就干两件事,一上传jar文件到storm cluster,另一件事通知storm cluster文件已经上传完毕,你可以执行某某某topology了. 先看上传jar文件对应的函数submitJar,其调用关系如下图所示 再看第二步中的调用关系,图是我用tikz/pgf写的,生成的是pdf格式。 在上述两幅调用关系图中,处于子树位置的函数都曾在storm.thrift中声明,如果此刻已经忘记了的点话,可以翻看一下前面1.3节中有关storm.thrift的描述。client侧的这些函数都是由thrift自动生成的。 由于篇幅和时间的关系,在storm client侧submit topology的时候,非常重要的函数还有TopologyBuilder.java中的源码。 nimbus storm client侧通过thrift接口向nimbus发送了了jar并且通过预先定义好的submitTopologyWithOpts来处理上传的topology,那么nimbus是如何一步步的进行文件接收并将其任务细化最终下达给supervisor的呢。 submitTopologyWithOpts 一切还是要从thrift说起,supervisor.clj中的service-handler具体实现了thrift定义的Nimbus接口,代码这里就不罗列了,太占篇幅。主要看其是如何实现submitTopologyWithOpts (^void submitTopologyWithOpts [this ^String storm-name ^String uploadedJarLocation ^String serializedConf ^StormTopology topology ^SubmitOptions submitOptions] (try (assert (not-nil? submitOptions)) (validate-topology-name! storm-name) (check-storm-active! nimbus storm-name false) (.validate ^backtype.storm.nimbus.ITopologyValidator (:validator nimbus) storm-name (from-json serializedConf) topology) (swap! (:submitted-count nimbus) inc) (let [storm-id (str storm-name "-" @(:submitted-count nimbus) "-" (current-time-secs)) storm-conf (normalize-conf conf (-> serializedConf from-json (assoc STORM-ID storm-id) (assoc TOPOLOGY-NAME storm-name)) topology) total-storm-conf (merge conf storm-conf) topology (normalize-topology total-storm-conf topology) topology (if (total-storm-conf TOPOLOGY-OPTIMIZE) (optimize-topology topology) topology) storm-cluster-state (:storm-cluster-state nimbus)] (system-topology! total-storm-conf topology) ;; this validates the structure of the topology (log-message "Received topology submission for " storm-name " with conf " storm-conf) ;; lock protects against multiple topologies being submitted at once and ;; cleanup thread killing topology in b/w assignment and starting the topology (locking (:submit-lock nimbus) (setup-storm-code conf storm-id uploadedJarLocation storm-conf topology) (.setup-heartbeats! storm-cluster-state storm-id) (let [thrift-status->kw-status {TopologyInitialStatus/INACTIVE :inactive TopologyInitialStatus/ACTIVE :active}] (start-storm nimbus storm-name storm-id (thrift-status->kw-status (.get_initial_status submitOptions)))) (mk-assignments nimbus))) (catch Throwable e (log-warn-error e "Topology submission exception. (topology name='" storm-name "')") (throw e)))) storm cluster在zookeeper server上创建的目录结构。目录结构相关的源文件是config.clj. 白话一下上面这个函数的执行逻辑,对上传的topology作必要的检测,包括名字,文件内容及格式,好比你进一家公司上班之前做的体检。这些工作都完成之后进入关键区域,是进入关键区域所以上锁,呵呵。 normalize-topology (defn all-components [^StormTopology topology] (apply merge {} (for [f thrift/STORM-TOPOLOGY-FIELDS] (.getFieldValue topology f) ))) 一旦列出所有的components,就可以读出这些component的配置信息。 mk-assignments 在这关键区域内执行的重点就是函数mk-assignments,mk-assignment有两个主要任务,第一是计算出有多少task,即有多少个spout,多少个bolt,第二就是在刚才的计算基础上通过调用zookeeper应用接口,写入assignment,以便supervisor感知到有新的任务需要认领。 先说第二点,因为逻辑简单。在mk-assignment中执行如下代码在zookeeper中设定相应的数据以便supervisor能够感知到有新的任务产生 (doseq [[topology-id assignment] new-assignments :let [existing-assignment (get existing-assignments topology-id) topology-details (.getById topologies topology-id)]] (if (= existing-assignment assignment) (log-debug "Assignment for " topology-id " hasn't changed") (do (log-message "Setting new assignment for topology id " topology-id ": " (pr-str assignment)) (.set-assignment! storm-cluster-state topology-id assignment) ))) 调用关系如下图所示 而第一点涉及到的计算相对繁杂,需要一一仔细道来。其实第一点中非常重要的课题就是如何进行任务的分发,即scheduling. 也许你已经注意到目录src/clj/backtype/storm/scheduler,或者注意到storm.yaml中与scheduler相关的配置项。那么这个scheduler到底是在什么时候起作用的呢。mk-assignments会间接调用到这么一个名字看起来奇怪异常的函数。compute-new-topology->executor->node+por,也就是在这么很奇怪的函数内,scheduler被调用 _ (.schedule (:scheduler nimbus) topologies cluster) new-scheduler-assignments (.getAssignments cluster) ;; add more information to convert SchedulerAssignment to Assignment new-topology->executor->node+port (compute-topology->executor->node+port new-scheduler-assignments)] schedule计算出来的assignments保存于Cluster.java中,这也是为什么new-scheduler-assignment要从其中读取数据的缘由所在。有了assignment,就可以计算出相应的node和port,其实就是这个任务应该交由哪个supervisor上的worker来执行。 storm在zookeeper server上创建的目录结构如下图所示 有了这个目录结构,现在要解答的问题是在topology在提交的时候要写哪几个目录?assignments目录下会新创建一个新提交的topology的目录,在这个topology中需要写的数据,其数据结构是什么样子? supervisor 一旦有新的assignment被写入到zookeeper中,supervisor中的回调函数mk-synchronize-supervisor立马被唤醒执行 主要执行逻辑就是读入zookeeper server中新的assignments全集与已经运行与本机上的assignments作比较,区别出哪些是新增的。在sync-processes函数中将运行具体task的worker拉起。 要想讲清楚topology提交过程中,supervisor需要做哪些动作,最主要的是去理解下面两个函数的处理逻辑。 mk-synchronize-supervisor当在zookeeper server的assignments子目录内容有所变化时,supervisor收到相应的notification, 处理这个notification的回调函数即为mk-synchronize-supervisor,mk-sychronize-supervisor读取所有的assignments即便它不是由自己处理,并将所有assignment的具体信息读出。尔后判断分析出哪些assignment是分配给自己处理的,在这些分配的assignment中,哪些是新增的。知道了新增的assignment之后,从nimbus的相应目录下载jar文件,用户自己的处理逻辑代码并没有上传到zookeeper server而是在nimbus所在的机器硬盘上。 sync-processesmk-synchronize-supervisor预处理过完与assignment相关的操作后,将真正启动worker的动作交给event-manager, event-manager运行在另一个独立的线程中,这个线程中进行处理的一个主要函数即sync-processes. sync-processes会将当前运行着的worker全部kill,然后指定新的运行参数,重新拉起worker. (defn mk-synchronize-supervisor [supervisor sync-processes event-manager processes-event-manager] (fn this [] (let [conf (:conf supervisor) storm-cluster-state (:storm-cluster-state supervisor) ^ISupervisor isupervisor (:isupervisor supervisor) ^LocalState local-state (:local-state supervisor) sync-callback (fn [& ignored] (.add event-manager this)) assignments-snapshot (assignments-snapshot storm-cluster-state sync-callback) storm-code-map (read-storm-code-locations assignments-snapshot) downloaded-storm-ids (set (read-downloaded-storm-ids conf)) ;;read assignments from zookeeper all-assignment (read-assignments assignments-snapshot (:assignment-id supervisor)) new-assignment (->> all-assignment (filter-key #(.confirmAssigned isupervisor %))) ;;task在assignment中 assigned-storm-ids (assigned-storm-ids-from-port-assignments new-assignment) existing-assignment (.get local-state LS-LOCAL-ASSIGNMENTS)] (log-debug "Synchronizing supervisor") (log-debug "Storm code map: " storm-code-map) (log-debug "Downloaded storm ids: " downloaded-storm-ids) (log-debug "All assignment: " all-assignment) (log-debug "New assignment: " new-assignment) ;; download code first ;; This might take awhile ;; - should this be done separately from usual monitoring? ;; should we only download when topology is assigned to this supervisor? (doseq [[storm-id master-code-dir] storm-code-map] (when (and (not (downloaded-storm-ids storm-id)) (assigned-storm-ids storm-id)) (log-message "Downloading code for storm id " storm-id " from " master-code-

优秀的个人博客,低调大师

twitter storm源码走读(三)

worker进程中线程的分类及用途 本文重点分析storm的worker进程在正常启动之后有哪些类型的线程,针对每种类型的线程,剖析其用途及消息的接收与发送流程。 概述 worker进程启动过程中最重要的两个函数是mk-worker和worker-data,代码就不一一列出了。worker顺利启动之后会拥有如下图所示的各类线程。 接收和发送线程 worker在启动的时候会生成进程级别的消息接收和消息发送线程,它们视具体配置而定,可以是基于zmq,也可以基于netty,这个没有太多好说的。socket connection的建立过程可以在tuple消息传递一文中找到说明。 zk client worker需要定期的向zk server发送心跳消息,与zk server之间的连接处理就落到zk client这个线程身上了。具体代码见函数do-heartbeat及do-executor-heartbeats。 定时器线程 worker进程需要定期的做些事情,比如发送心跳消息,刷新socket连接,这些定时器归为如下几类,每类定时器运行在各自的线程。 :heartbeat-timer worker :refresh-connections-timer worker :refresh-active-timer worker :executor-heartbeat-timer worker :user-timer worker 上述定时器分类见于worker的shutdown函数,有时候在分析代码的时候,如果从入口看不清楚的话,不妨试试从退出的处理逻辑哪里找找答案。 SystemBolt 在topology提交的时候曾经见过函数system-topology!,这个函数会创建SystemBolt,每个worker内有且只有一个SystemBolt,可以见SystemBolt.java中注释的说明或参考github上storm对该改变的说明,https://github.com/nathanmarz/storm/pull/517。 SystemBolt主要进行进程相关的统计功能,比如内存使用情况,网络包的吞吐量,具体可见SystemBolt.java。SystemBolt是不接收tuple,只有出度,没有入度。 Metrics Bolt线程 MetricsBolt主要也是处理统计工作,与systembolt不同的是,metricsbolt主要处理executor级别的,如果用户在配置文件中定义了相关的MetricsConsumer类,那么这些类会在此被执行。 与之相关的配置内容, ## Metrics Consumers # topology.metrics.consumer.register: # - class: "backtype.storm.metrics.LoggingMetricsConsumer" # parallelism.hint: 1 # - class: "org.mycompany.MyMetricsConsumer" # parallelism.hint: 1 # argument: # - endpoint: "metrics-collector.mycompany.org" Shared Executor 这个是在storm 0.8中引入的,其用途可在0.8的release notes中找到,创建共享线程池,具体用途没太搞清楚,:). Metrics的执行流程 metrics所做的计量工作是在什么时候被唤醒的呢,也就是说如何一步步的触发直到MetricsConsumeBolt的execute函数被调用。 下图勾勒出与metrics相关的线程间的消息传递过程。 简要说明如下 worker在启动的时候,会往:user-timer中注册metrics timer(见setup-metrics!函数). 一旦metrics timer超时,会发送一个stream-id为metrics-tick-stream-id的tuple到非metrics类型的bolt,如user/acker/system bolt. 接收到tuple之后,会调用metrics-tick函数发送task-data给MetricsConsumerBolt, stream-id为metrics-stream-id MetricsConsumerBolt接收到stream-id为metrics-stream-id的tuple后,会执行execute 注:在worker内部还有另一套计量api,定义于builtin-metrics.clj中,与MetricsConsumerBolt的区别在于,builtin-metrics是在处理外部进程发送过来的tuple时进行计量统计,而MetricsConsumerBolt是定时触发。 worker进程内部消息传递处理和数据结构分析 本文从外部消息在worker进程内部的转化,传递及处理过程入手,一步步分析在worker-data中的数据项存在的原因和意义。试图从代码实现的角度来回答,如果是从头开始实现worker的话,该如何来定义消息接口,如何实现各自接口上的消息处理。 Topology到Worker的映射关系 Topology由Spout,Bolt组成,其逻辑关系大体如下图所示。 无论是Spout或Bolt的处理逻辑都需要在进程或线程内执行,那么它们与进程及线程间的映射关系又是如何呢。有关这个问题,Understanding the Parallelism of a Storm Topology一文作了很好的总结,现重复一下其要点。 worker是进程,executor对应于线程,spout或bolt是一个个的task 同一个worker只会执行同一个topology相关的task 在同一个executor中可以执行多个同类型的task, 即在同一个executor中,要么全部是bolt类的task,要么全部是spout类的task 运行的时候,spout和bolt需要被包装成一个又一个task worker,executor, task三者之间的关系可以用下图表示 小结一下,Worker=Process, Executor=Thread, Task=Spout or Bolt. 每一个executor使用的是actor pattern,high level的处理逻辑如下图所示 外部消息的接收和处理 在源码走读之四一文中总结了worker进程内的各种类型的thread,也即executor,这个等同于定义了进程内部和进程间的接口类型。那么这些接口上的消息在具体流传和处理过程中需要定义哪些数据结构,针对这些数据结构,又要做哪些必要的处理呢? 换句话说,就是为什么在worker.clj中有哪些数据和函数存在,不这样做,可以不? 先图示一下,外部消息处理的大概流程。 注:圈起来的数字表示消息转换和处理的序列。 步骤一 监听端口准备就绪,接收线程在收到外部的消息后,面临的问题就是如何确定由哪个task来处理该消息。接收到的tuple中含有task-id,根据task-id可以知道运行该task的executor,executor中有receive-message-queue即(incoming queue)来存放外部的tuple. 定义的数据结构需要反映这个转换过程task-id->executor->receive-queue-map. 那么在worker-data中哪些数据项与这个过程相关呢 :port :executor-receive-queue-map :short-executor-receive-queue-map :task->short-executor :transfer-local-fn transfer-local-fn将数据从接收线程发送到spout或bolt所在的executor线程。 步骤二 接下来数据会被传递到executor,于是又牵涉到executor的数据结构问题。executor-data由函数mk-executor-data创建,其内容与worker-data比较起来相对较少。 executor收到tuple之后,第一步需要进行反序列化,storm中使用kyro来进行序列化和反序列化,这也是为什么在executor中有该数据项的原因。 executor中与步骤2相关的数据项 :type executor-type :receive-queue :deserializer (executor-data中的数据项) 步骤三: 步骤2处理结束,会产生相应的tuple发送到外部。这个过程需要多解释一下,首先tuple不是直接发送给worker的transfer-thread(负责向其它进程发送消息),而是发送给send-handler线程,每一个executor在创建的时候最起码会有两个线程被创建,一个用于运行bolt或spout的处理逻辑,另一个用以负责缓存bolt或spout产生的对外发送的tuple。 一旦snd-hander中的tuple数量达到阀值,这些被缓存的tuple会一次性发送给worker级别的transfer-thread. executor中与步骤3相关的数据项 :transfer-fn (mk-executor-transfer-fn batch-transfer->worker) :batch-transfer-queue 在步骤3中生成outgoing的tuple,tuple生成的时候需要回答两个基本问题 tuple中含有哪些字段 -- 该问题的解答由spout或bolt中的declareOutFields来解决 由哪个node+port来接收该tuple -- 由grouping来解决,这个时候就可以看出为什么需要task这一层的逻辑抽象了,有关grouping的详细解释,请参考fxjwind撰写的Storm-源码分析-Streaming Grouping (backtype.storm.daemon.executor) 步骤四: 处理逻辑很简单,先将数据缓存,然后在达到阀值之后,一起传送给transfer-thread. start-batch-transfer->worker-handler (defn start-batch-transfer->worker-handler! [worker executor-data] (let [worker-transfer-fn (:transfer-fn worker) cached-emit (MutableObject. (ArrayList.)) storm-conf (:storm-conf executor-data) serializer (KryoTupleSerializer. storm-conf (:worker-context executor-data)) ] (disruptor/consume-loop* (:batch-transfer-queue executor-data) (disruptor/handler [o seq-id batch-end?] (let [^ArrayList alist (.getObject cached-emit)] (.add alist o) (when batch-end? (worker-transfer-fn serializer alist) (.setObject cached-emit (ArrayList.)) ))) :kill-fn (:report-error-and-die executor-data)))) worker-transfer-fn是worker中的transfer-fn,由mk-transfer-fn生成。 (defn mk-transfer-fn [worker] (let [local-tasks (-> worker :task-ids set) local-transfer (:transfer-local-fn worker) ^DisruptorQueue transfer-queue (:transfer-queue worker)] (fn [^KryoTupleSerializer serializer tuple-batch] (let [local (ArrayList.) remote (ArrayList.)] (fast-list-iter [[task tuple :as pair] tuple-batch] (if (local-tasks task) (.add local pair) (.add remote pair) )) (local-transfer local) ;; not using map because the lazy seq shows up in perf profiles (let [serialized-pairs (fast-list-for [[task ^TupleImpl tuple] remote] [task (.serialize serializer tuple)])] (disruptor/publish transfer-queue serialized-pairs) ))))) 步骤五: 处理函数mk-transfer-tuples-handler,主要进行序列化,将序列化后的数据发送给目的地址。 (defn mk-transfer-tuples-handler [worker] (let [^DisruptorQueue transfer-queue (:transfer-queue worker) drainer (ArrayList.) node+port->socket (:cached-node+port->socket worker) task->node+port (:cached-task->node+port worker) endpoint-socket-lock (:endpoint-socket-lock worker) ] (disruptor/clojure-handler (fn [packets _ batch-end?] (.addAll drainer packets) (when batch-end? (read-locked endpoint-socket-lock (let [node+port->socket @node+port->socket task->node+port @task->node+port] ;; consider doing some automatic batching here (would need to not be serialized at this point to remove per-tuple overhead) ;; try using multipart messages ... first sort the tuples by the target node (without changing the local ordering) (fast-list-iter [[task ser-tuple] drainer] ;; TODO: consider write a batch of tuples here to every target worker ;; group by node+port, do multipart send (let [node-port (get task->node+port task)] (when node-port (.send ^IConnection (get node+port->socket node-port) task ser-tuple)) )))) (.clear drainer)))))) tuple发送的时候需要用到connection,但目前只知道task-id,所以在worker中需要保存task-id到node+port的映射,node+port与outgoing connections之间的映射。 worker中与步骤5相关的数据项: :cached-node+port->socket :cached-task->node+port :component->stream->fields :component->sorted-tasks :endpoint-socket-lock :transfer-queue (线程内部的消息队列) :task->component 其它的数据项 上述五个步骤并没有涵盖worker-data所有的数据项,那么其它的数据项归一归类,大体如下 timer相关,timer相关的数据项包括timer及其对应的处理句柄 :heartbeat-timer :refresh-connection-timer :refresh-active-timer :executor-heartbeat-timer :user-timer zk相关 :storm-cluster-state :storm-active-atom :cluster-state 配置相关 :conf :mq-context 在transport layer是使用zmq还是netty Assignment相关 :storm-id :assigment-id :worker-id :executors :task-ids :storm-conf :topology :system-topology 进程关闭相关 :suicide-fn 其它的其它 :uptime 运行时间,统计用 :default-shared-resources 线程池 :user-shared-resources 未启用 小结 设计的时候,一定是先画出一个大概的蓝图,然后逐步的细化并加以实现。具体来说,步骤如下 manifest 定义主要的功能 draw skeleton 画出实现草图,定义主要的接口 discussion 与团队讨论 data structures 数据结构 function 函数实现 testing 测试

优秀的个人博客,低调大师

twitter storm源码走读(四)

Trident Topology执行过程分析 TridentTopology是storm提供的高层使用接口,常见的一些SQL中的操作在tridenttopology提供的api中都有类似的影射。关于TridentTopology的使用及运行原理,当前进行详细分析的文章不多。 从TridentTopology到vanilla topology(普通的topology)由三个层次组成: 面向最终用户的概念stream, operation 利用planner将tridenttopology转换成vanilla topology 执行vanilla topology 本文尝试TridentTopology是如何先一步步转换成普通的storm Topology(即vanila topology), 转换后的topology的执行中有哪些区别? 概述 从TridentTopology到基本的Topology有三层,下图给出一个全局的视图。 创建TridentTopology 下面的代码摘自StormStarter中的TridentWordCount.java TridentTopology topology = new TridentTopology(); topology.newStream("spout1", spout).parallelismHint(16).each(new Fields("sentence"), new Split(), new Fields("word")).groupBy(new Fields("word")).persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count")).parallelismHint(16); return topology.build(); 上述代码的newStream一行,分两大部分,一是使用newStream来创建一个stream对象,然后针对该Stream进行各种操作,each/shuffle/persistentAggregate等就是各种operation. 用户在使用TridentTopology的时候,只需要熟悉Stream和TridentTopology中的API函数即可。 转换TridentTopology为Vanilla Topology 上一节创建了Stream,但是如何将其与原有的Spout及Bolt联系起来呢?问题的关键就在TridentTopology::build函数和TridentTopologyBuilder::buildTopology TridentTopology::build newStream及其后的函数调用创建了一个含有三大类节点的List,利用该List创建了一个有向非循环图(DAG)。这三类节点分别是operation, partition, spout,在build函数将节点分类分别加入到boltNodes或spoutNodes,注意此处的spout或bolt不能等同于普通的spout和bolt. TridentTopologyBuilder::buildTopology 利用在build函数中创建的boltNodes,spoutNodes及生成的graph来创建vanilla topology所需要的bolt及spout. 在buildTopology中会看到类似的代码片段。 builder.setBolt(spoutCoordinator(id), new TridentSpoutCoordinator(c.commitStateId, (ITridentSpout) c.spout)) .globalGrouping(masterCoordinator(c.batchGroupId), MasterBatchCoordinator.BATCH_STREAM_ID) .globalGrouping(masterCoordinator(c.batchGroupId), MasterBatchCoordinator.SUCCESS_STREAM_ID); builder.setSpout(masterCoordinator(batch), new MasterBatchCoordinator(commitIds, batchesToSpouts.get(batch))); for(String b: c.committerBatches) { specs.get(b).commitStream = new GlobalStreamId(masterCoordinator(b), MasterBatchCoordinator.COMMIT_STREAM_ID); } BoltDeclarer d = builder.setBolt(id, new TridentBoltExecutor(c.bolt, batchIdsForBolts, specs), c.parallelism); 最终生成的普通Topology,与普通Topology中的Spout相对应的是MasterBatchCoordinator,而在创建TridentTopology使用的spout则成了Bolt,使用于Stream上的各种Operation也存在于多个普通Bolt中。 TridentTopology的执行 TridentTopology被转换为普通的Topology(vanilla Topology)之后提交到nimbus,它的具体执行过程有什么不同呢? 主要有几点: MasterBatchCoordinator通过Batch_stream_id来发送通知给TridentSpoutExecutor TridentSpoutExecutor收到通知发送成批的tuple给下一跳的Bolt 下一跳的Bolt收到tuple之后,使用TridentBoltExecutor来进行处理 TridentBoltExecutor调用SubtopologyBolt::execute InitialReceiver::execute被调用 TridentProcessor::execute被调用 MasterBatchCoordinator收到ack之后,会发送success消息给Spout MasterBatchCoordinator在commit的时候,会发送commit消息给Spout,让Spout将缓存的消息删除 trident topology可靠性分析 本文详细分析TridentTopology的可靠性实现, TridentTopology通过transactional spout与transactional state相结合,能够做到tuple“只被处理一次,不多也不少”。也就是做到事务性处理exactly-once,要么成功,要么失败。 而一般的storm topology是无法保证eactly-once的处理的,它们要么是at-least-once(至少被处理一次,有可能被处理多次);要么是at-most-once(最多被处理一次,这样就存在遗漏的可能). TridentTopology在设计中借鉴和保留了目前已经过期的transactional topology的设计思想。 Storm Topology的ack机制 在进行TridentTopology的可靠性分析之前,我们先回顾一下在storm topology中的ack机制。ack bolt是在提交到storm cluster中,由系统自动产生的,一般来说一个topology只有一个ack bolt(当然可以通过配置参数指定多个)。 当bolt处理并下发完tuple给下一跳的bolt时,会发送一个ack给ack bolt。ack bolt通过简单的异或原理(即同一个数与自己异或结果为零)来判定从spout发出的某一个bolt是否已经被完全处理完毕。如果结果为真,ack bolt发送消息给spout,spout中的ack函数被调用并执行。如果超时,则发送fail消息给spout,spout中的fail函数被调用并执行,spout中的ack和fail的处理逻辑由用户自行填写。 如在github上的kerstel spout就能做到只有当某一个tuple被成功处理之后,它才会从缓存中移除,否则继续放入到处理队列再次进行处理。 TridentTopology的可靠性机制 在“走读之6”一文中分析了一个tridenttopology是如何转换成storm topology的,我想用上面这幅图再次阐述一下转变后的结果。 一个tridenttopoloy会至少引入一个MasterBatchCoordinator,这个MBC就类似于storm topology中的spout newStream时使用的入参spout会裂变成两个bolt,一是TridentSpoutCoordinator,另一个是TridentSpoutExecutor 针对stream的各种操作则被分散到各个Bolt中,它们的执行上下文是TridentBoltExecutor 可以看出使用TridentTopology Api进行操作时,所有的东西其实都运行在bolt context中,而真正的spout是在调用TridentTopologyBuilder.buildTopology()的时候被添加的。 MasterBatchCoordinator使用batch_stream发送一个类似于seeder tuple的东西给tridentspoutcoordinator,tridentspoutcoordinator将该信号继续下发给TridentSpoutExecutor, TridentSpout是如何一步步被调用到的呢。 TridentBoltExecutor::execute TridentSpoutExecutor::execute BatchSpoutExecutor::execute ITridentSpout::emitBatch emitBatch是产生真正需要被处理的tuple的,这些tuple会被各个Operation所在的bolt所接收。它们的调用顺序是 TridentBoltExecutor::execute SubtopologyBolt::execute InitialReceiver::receive TridentProcessor::execute 处理结束的判断依据 在TridentSpout中是如何判断所有的tuple都已经被处理的呢。 在每跳中认为自己处理完毕的时候,它都会告诉下一跳,即下游,我给你发送了多少tuple,如果下游将上游发送过来的确认消息与自身确实已经处理的消息比对一致的话,则认为处理都完成,于是发送ack. 问题的关键变成每一个bolt是如何判断自己已经处理完毕的呢,请看步骤3 总有一个bolt是没有上游的,即TridentSpoutExecutor,它只会收到启动指令,但不接收真正的业务数据,于是它会告诉下一跳,我发了多少tuple给你。 STREAM 在MasterBatchCoordinator中定义了三种不同的stream,这三种stream分别是 BATCH_STREAM COMMIT_STREAM SUCCESS_STREAM 这些stream分别在什么时候被使用呢,下图给出一个大概的时序 简要说明: masterbatchcoordinator通过batch_stream发送seeder tuple给tridentspoutcoordinator tridentspoutcoordinator给tridentspoutexecutor继续传递该指令 TridentSpoutExecutor在收到启动指令后,调用ITridentSpout接口的实现类进行emitBatch TridentSpoutExecutor在发送完一批batch后,finishBatch被调用,通过emitDirect会给下一跳通过coord_stream发送trackedinfo,即我已经发送了多少消息给你 TridentSpoutExecutor紧接着还会给ack bolt发送ack消息,ack bolt将其传达到MasterBatchCoordinator MasterBatchCoordinator在收到第一个ack后,将状态置为processed 当MasterBatchCoordinator再次收到ack后,会将状态转为committing,同时通过commit_stream发送tuple给TridentSpoutExecutor 收到commit_stream上传来的tuple后,TridentSpoutExecutor会调用ITridentSpout中的emmitter, emmitter::commit()被执行,TridentSpoutExecutor会再次ack收到tuple MasterBatchCoordinator在收到这个tuple之后,会认为针对某一个seeder tuple的处理已经完全实现,于是通过SUCCESS_STREM告知TridentSpoutCoordinator,所有的活都已经都完成了,收工。 收到Success_stream上传来的信号后,ITridentSpout中的内嵌子类Emmit和Coordinator中相应的success方法会被调用执行。 注意: 为了描述方便,将TridentTopology进行了简化,认为其在转换成真正的storm topology时,只有一个TridentProcessor所在的bolt。真实的情况可能比这复杂,但消息的传递路径还是差不多的。 注意在TridentTopology中ack会被多次反复调用,这不同于普通的storm topology 状态机 在MasterBatchCoordinator中,针对每一个seeder tuple,其状态机如下图所示。注意这些状态是会被保存到zookeeper server中的,使用的api定义在TransactionalState中。 总结 通过上面的分析可以看出,TridentTopology实现了一个比较好的框架,但真正要做到exactly-once的处理,还需要用户自己去实现ITridentSpout中的两个重要内嵌类,Emmitter和Coordinator。 具体如何实现该接口,可以查看storm-core/src/jvm/storm/trident/testing目录下的FixedBatchSpout.java和FeederCommitterBatchSpout.java

优秀的个人博客,低调大师

twitter storm源码走读(五)

TridentTopology创建过程详解 从用户层面来看TridentTopology,有两个重要的概念一是Stream,另一个是作用于Stream上的各种Operation。在实现层面来看,无论是stream,还是后续的operation都会转变成为各个Node,这些Node之间的关系通过重要的数据结构图来维护。具体到TridentTopology,实现图的各种操作的组件是jgrapht。 说到图,两个基本的概念会闪现出来,一是结点,二是描述结点之间关系的边。要想很好的理解TridentTopology就需要紧盯图中结点和边的变化。 TridentTopology在转换成为普通的StormTopology时,需要将原始的图分成各个group,每个group将运行于一个独立的bolt中。TridentTopology又是如何知道哪些node应该在同一个group,哪些应该处在另一个group中的呢;如何来确定每个group的并发度(parallismHint)的呢。这些问题的解决都与jgrapht分不开。 关于jgrapht的更多信息,请参考其官方网站http://jgrapht.org 概要 在TridentTopology中向图中添加结点的api有三种: addNode addSourcedNode addSourcedStateNode 其中addNode在创建stream是使用,addSourcedStateNode在partitionPersist时使用到,其它的operation使用到的是addSourcedNode. addNode与其它两个方法的一个重要区别还在于,addNode是不需要添加边(Edge),而其它两个API需要往图中添加edge,以确定该node的源是哪个。 TridentTopology 1 2 3 4 public TridentTopology() { _graph = new DefaultDirectedGraph( new ErrorEdgeFactory()); _gen = new UniqueIdGen(); } 在TridentTopology的构造函数中,创建了DAG(有向无环图)。利用这个_graph来作为容器以存储后续过程中创建的各个node及它们之间的关系。 newStream newStream会为DAG(有向无环图)中创建源结点,其调用关系如下所示。 newStream addNode registerNode 1 protected void registerNode(Node n) { 2 _graph.addVertex(n); 3 if(n.stateInfo!=null) { 4 String id = n.stateInfo.id; 5 if(!_colocate.containsKey(id)) { 6 _colocate.put(id, new ArrayList()); 7 } 8 _colocate.get(id).add(n); 9 } 10 } each 作用于stream上的Operation有很多,以each为例来看新的operation是如何转换成为node添加到_graph中的。 //Stream.javapublic Stream each(Fields inputFields, Function function, Fields functionFields) { projectionValidation(inputFields); return _topology.addSourcedNode(this, new ProcessorNode(_topology.getUniqueStreamId(), _name, TridentUtils.fieldsConcat(getOutputFields(), functionFields), functionFields, new EachProcessor(inputFields, function))); } 调用关系描述如下 Stream::each TridentTopology::addSourcedNode TridentTopology::registerSourcedNode registerSourcedNode的实现如下 protected void registerSourcedNode(List<Stream> sources, Node newNode) { registerNode(newNode); int streamIndex = 0; for(Stream s: sources) { _graph.addEdge(s._node, newNode, new IndexedEdge(s._node, newNode, streamIndex)); streamIndex++; } } 注意此处添加edge是,是有索引的,这样可以区别处理的先后顺序。 在Stream中含有成员变量_node,表示stream最近停泊的node,有了该变量添加edge才成为了可能。 partitionPersist public TridentState partitionPersist(StateSpec stateSpec, Fields inputFields, StateUpdater updater, Fields functionFields) { projectionValidation(inputFields); String id = _topology.getUniqueStateId(); ProcessorNode n = new ProcessorNode(_topology.getUniqueStreamId(), _name, functionFields, functionFields, new PartitionPersistProcessor(id, inputFields, updater)); n.committer = true; n.stateInfo = new NodeStateInfo(id, stateSpec); return _topology.addSourcedStateNode(this, n); } 调用关系 Stream::partitionPersist TridentTopology::addSourcedStateNode TridentTopology::registerSourcedNode 与addNode及addSourcedNode不同的是,addSourcedStateNode返回的是TridentState而非Stream。 既然谈到了TridentState就不得不谈到其另一面Stream::stateQuery, public Stream stateQuery(TridentState state, Fields inputFields, QueryFunction function, Fields functionFields) { projectionValidation(inputFields); String stateId = state._node.stateInfo.id; Node n = new ProcessorNode(_topology.getUniqueStreamId(), _name, TridentUtils.fieldsConcat(getOutputFields(), functionFields), functionFields, new StateQueryProcessor(stateId, inputFields, function)); _topology._colocate.get(stateId).add(n); return _topology.addSourcedNode(this, n); } 从此处可以看出stateQueryNode最起码有两个inputStream,一是从TridentState而来表示状态已经改变,另一个是处于drpcStream这个方面的上一跳结点。 build TridentTopology::build是将TridentTopology转变为StormTopology的过程,这一过程中最重要的一环就是将_graph中含有的node进行分组。 grouping 算法逻辑概述 将boltNodes中的每个boltNode作为一个group加入全部加入initialGroups 以graph和initialGroups作为入参创建GraphGrouper 分组的过程其实就是进行合并的过程,详见GraphGrouper::mergeFully() 如果从当前group1的输出目的地都是属于group2,则将group1,group2合并 如果当前group1的所有输入源都是来自于group2,则将group1,group2合并 将需要合并的group1,group2作为入参创建新的group,同时将group1,group2从已有的集合出移除 public void mergeFully() { boolean somethingHappened = true; while(somethingHappened) { somethingHappened = false; for(Group g: currGroups) { Collection<Group> outgoingGroups = outgoingGroups(g); if(outgoingGroups.size()==1) { Group out = outgoingGroups.iterator().next(); if(out!=null) { merge(g, out); somethingHappened = true; break; } } Collection<Group> incomingGroups = incomingGroups(g); if(incomingGroups.size()==1) { Group in = incomingGroups.iterator().next(); if(in!=null) { merge(g, in); somethingHappened = true; break; } } } } } GraphGrouper::merge() private void merge(Group g1, Group g2) { Group newGroup = new Group(g1, g2); currGroups.remove(g1); currGroups.remove(g2); currGroups.add(newGroup); for(Node n: newGroup.nodes) { groupIndex.put(n, newGroup); } } 在group之间添加partitionNode // add identity partitions between groups for(IndexedEdge<Node> e: new HashSet<IndexedEdge>(graph.edgeSet())) { if(!(e.source instanceof PartitionNode) && !(e.target instanceof PartitionNode)) { Group g1 = grouper.nodeGroup(e.source); Group g2 = grouper.nodeGroup(e.target); // g1 being null means the source is a spout node if(g1==null && !(e.source instanceof SpoutNode)) throw new RuntimeException("Planner exception: Null source group must indicate a spout node at this phase of planning"); if(g1==null || !g1.equals(g2)) { graph.removeEdge(e); PartitionNode pNode = makeIdentityPartition(e.source); graph.addVertex(pNode); graph.addEdge(e.source, pNode, new IndexedEdge(e.source, pNode, 0)); graph.addEdge(pNode, e.target, new IndexedEdge(pNode, e.target, e.index)); } } } _graph中所有的node在变换过后,变成两组元素,一是spoutNodes,另一个是合并后的mergedGroup. spoutNodes中的每个元素作为spout添加到TridentTopologyBuilder的_spouts数组中,mergedGroup中的每个group添加到TridentTopologyBuilder的_bolt数组中。在TridentTopologyBuilder::build()中最主要的事情是为每个_spouts和_bolts数组中的成员添加grouping关系。 小结 到目前为止,通过两篇文章分析了TridentTopology的创建过程及其运行时在每个TridentBoltExecutor中的消息传递情况。接下来会分析TridentTopology提供的API实现及其作用场景。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册