计算top N words的topology, 用于比如trending topics or trending images on Twitter.

实现了滑动窗口计数和TopN排序, 比较有意思, 具体分析一下代码

Topology

这是一个稍微复杂些的topology, 主要体现在使用不同的grouping方式, fieldsGrouping和globalGrouping

  1.  String spoutId = "wordGenerator";
     String counterId = "counter";
     String intermediateRankerId = "intermediateRanker";
     String totalRankerId = "finalRanker";
     builder.setSpout(spoutId, new TestWordSpout(), 5);
     builder.setBolt(counterId, new RollingCountBolt(9, 3), 4).fieldsGrouping(spoutId, new Fields("word"));
     builder.setBolt(intermediateRankerId, new IntermediateRankingsBolt(TOP_N), 4).fieldsGrouping(counterId, new Fields("obj"));
     builder.setBolt(totalRankerId, new TotalRankingsBolt TOP_N)).globalGrouping(intermediateRankerId);

RollingCountBolt

首先使用RollingCountBolt, 并且此处是按照word进行fieldsGrouping的, 所以相同的word会被发送到同一个bolt, 这个field id是在上一级的declareOutputFields时指定的

RollingCountBolt, 用于基于时间窗口的counting, 所以需要两个参数, the length of the sliding window in seconds和the emit frequency in seconds

new RollingCountBolt(9, 3), 意味着output the latest 9 minutes sliding window every 3 minutes

1. 创建SlidingWindowCounter(SlidingWindowCounter和SlotBasedCounter参考下面)     
counter = new SlidingWindowCounter(this.windowLengthInSeconds / this.windowUpdateFrequencyInSeconds);   
如何定义slot数? 对于9 min的时间窗口, 每3 min emit一次数据, 那么就需要9/3=3个slot   
那么在3 min以内, 不停的调用countObjAndAck(tuple)来递增所有对象该slot上的计数   
每3分钟会触发调用emitCurrentWindowCounts, 用于滑动窗口(通过getCountsThenAdvanceWindow), 并emit (Map<obj, 窗口内的计数和>, 实际使用时间)   
因为实际emit触发时间, 不可能刚好是3 min, 会有误差, 所以需要给出实际使用时间

2. TupleHelpers.isTickTuple(tuple), TickTuple

前面没有说的一点是, 如何触发emit? 这是比较值得说明的一点, 因为其使用Storm的TickTuple特性.   
这个功能挺有用, 比如数据库批量存储, 或者这里的时间窗口的统计等应用   
"__system" component会定时往task发送 "__tick" stream的tuple   
发送频率由TOPOLOGY_TICK_TUPLE_FREQ_SECS来配置, 可以在default.ymal里面配置   
也可以在代码里面通过getComponentConfiguration()来进行配置,

  1. public Map<String, Object> getComponentConfiguration() {
         Map<String, Object> conf = new HashMap<String, Object>();
         conf.put(Config.TOPOLOGY_TICK_TUPLE_FREQ_SECS, emitFrequencyInSeconds);
         return conf;

配置完成后, storm就会定期的往task发送ticktuple   
只需要通过isTickTuple来判断是否为tickTuple, 就可以完成定时触发的功能

  1. public static boolean isTickTuple(Tuple tuple) {
        return tuple.getSourceComponent().equals(Constants.SYSTEM_COMPONENT_ID) \\ SYSTEM_COMPONENT_ID == "__system"
            && tuple.getSourceStreamId().equals(Constants.SYSTEM_TICK_STREAM_ID); \\ SYSTEM_TICK_STREAM_ID == "__tick"
    }

最终, 这个blot的输出为, collector.emit(new Values(obj, count, actualWindowLengthInSeconds));   
obj, count(窗口内的计数和), 实际使用时间

SlotBasedCounter

基于slot的counter, 模板类, 可以指定被计数对象的类型T   
这个类其实很简单, 实现计数对象和一组slot(用long数组实现)的map, 并可以对任意slot做increment或reset等操作

关键结构为Map<T, long[]> objToCounts, 为每个obj都对应于一个大小为numSlots的long数组, 所以对每个obj可以计numSlots个数   
incrementCount, 递增某个obj的某个slot, 如果是第一次需要创建counts数组   
getCount, getCounts, 获取某obj的某slot值, 或某obj的所有slot值的和   
wipeSlot, resetSlotCountToZero, reset所有对象的某solt为0, reset某obj的某slot为0   
wipeZeros, 删除所有total count为0的obj, 以释放空间

  1. public final class SlotBasedCounter<T> implements Serializable {
  2.  
  3.     private static final long serialVersionUID = 4858185737378394432L;
  4.  
  5.     private final Map<T, long[]> objToCounts = new HashMap<T, long[]>();
        private final int numSlots;
  6.  
  7.     public SlotBasedCounter(int numSlots) {
            if (numSlots <= 0) {
                throw new IllegalArgumentException("Number of slots must be greater than zero (you requested " + numSlots
                    + ")");
            }
            this.numSlots = numSlots;
        }
  8.  
  9.     public void incrementCount(T obj, int slot) {
            long[] counts = objToCounts.get(obj);
            if (counts == null) {
                counts = new long[this.numSlots];
                objToCounts.put(obj, counts);
            }
            counts[slot]++;
        }
  10.  
  11.     public long getCount(T obj, int slot) {
            long[] counts = objToCounts.get(obj);
            if (counts == null) {
                return 0;
            }
            else {
                return counts[slot];
            }
        }
  12.  
  13.     public Map<T, Long> getCounts() {
            Map<T, Long> result = new HashMap<T, Long>();
            for (T obj : objToCounts.keySet()) {
                result.put(obj, computeTotalCount(obj));
            }
            return result;
        }
  14.  
  15.     private long computeTotalCount(T obj) {
            long[] curr = objToCounts.get(obj);
            long total = 0;
            for (long l : curr) {
                total += l;
            }
            return total;
        }
  16.  
  17.     /**
         * Reset the slot count of any tracked objects to zero for the given slot.
         * 
         * @param slot
         */
        public void wipeSlot(int slot) {
            for (T obj : objToCounts.keySet()) {
                resetSlotCountToZero(obj, slot);
            }
        }
  18.  
  19.     private void resetSlotCountToZero(T obj, int slot) {
            long[] counts = objToCounts.get(obj);
            counts[slot] = 0;
        }
  20.  
  21.     private boolean shouldBeRemovedFromCounter(T obj) {
            return computeTotalCount(obj) == 0;
        }
  22.  
  23.     /**
         * Remove any object from the counter whose total count is zero (to free up memory).
         */
        public void wipeZeros() {
            Set<T> objToBeRemoved = new HashSet<T>();
            for (T obj : objToCounts.keySet()) {
                if (shouldBeRemovedFromCounter(obj)) {
                    objToBeRemoved.add(obj);
                }
            }
            for (T obj : objToBeRemoved) {
                objToCounts.remove(obj);
            }
        }
    }

SlidingWindowCounter

SlidingWindowCounter只是对SlotBasedCounter做了进一步的封装, 通过headSlot和tailSlot提供sliding window的概念

incrementCount, 只能对headSlot进行increment, 其他slot作为窗口中的历史数据

核心的操作为, getCountsThenAdvanceWindow   
1. 取出Map<T, Long> counts, 对象和窗口内所有slots求和值的map   
2. 调用wipeZeros, 删除已经不被使用的obj, 释放空间   
3. 最重要的一步, 清除tailSlot, 并advanceHead, 以实现滑动窗口   
    advanceHead的实现, 如何在数组实现循环的滑动窗口

  1. public final class SlidingWindowCounter<T> implements Serializable {
  2.  
  3.     private static final long serialVersionUID = -2645063988768785810L;
  4.  
  5.     private SlotBasedCounter<T> objCounter;
        private int headSlot;
        private int tailSlot;
        private int windowLengthInSlots;
  6.  
  7.     public SlidingWindowCounter(int windowLengthInSlots) {
            if (windowLengthInSlots < 2) {
                throw new IllegalArgumentException("Window length in slots must be at least two (you requested "
                    + windowLengthInSlots + ")");
            }
            this.windowLengthInSlots = windowLengthInSlots;
            this.objCounter = new SlotBasedCounter<T>(this.windowLengthInSlots);
  8.  
  9.         this.headSlot = 0;
            this.tailSlot = slotAfter(headSlot);
        }
  10.  
  11.     public void incrementCount(T obj) {
            objCounter.incrementCount(obj, headSlot);
        }
  12.  
  13.     /**
         * Return the current (total) counts of all tracked objects, then advance the window.
         * 
         * Whenever this method is called, we consider the counts of the current sliding window to be available to and
         * successfully processed "upstream" (i.e. by the caller). Knowing this we will start counting any subsequent
         * objects within the next "chunk" of the sliding window.
         * 
         * @return
         */
        public Map<T, Long> getCountsThenAdvanceWindow() {
            Map<T, Long> counts = objCounter.getCounts();
            objCounter.wipeZeros();
            objCounter.wipeSlot(tailSlot);
            advanceHead();
            return counts;
        }
  14.  
  15.     private void advanceHead() {
            headSlot = tailSlot;
            tailSlot = slotAfter(tailSlot);
        }
  16.  
  17.     private int slotAfter(int slot) {
            return (slot + 1) % windowLengthInSlots;
        }
    }
  1.  

IntermediateRankingsBolt

这个bolt作用就是对于中间结果的排序, 为什么要增加这步, 应为数据量比较大, 如果直接全放到一个节点上排序, 会负载太重   
所以先通过IntermediateRankingsBolt, 过滤掉一些   
这里仍然使用, 对于obj进行fieldsGrouping, 保证对于同一个obj, 不同时间段emit的统计数据会被发送到同一个task

IntermediateRankingsBolt继承自AbstractRankerBolt(参考下面)   
并实现了updateRankingsWithTuple,

  1. void updateRankingsWithTuple(Tuple tuple) {
        Rankable rankable = RankableObjectWithFields.from(tuple);
        super.getRankings().updateWith(rankable);
    }
  1. 逻辑很简单, Tuple转化Rankable, 并更新Rankings列表
  1. 参考AbstractRankerBolt, bolt会定时将Ranking列表emit出去

Rankable

Rankable除了继承Comparable接口, 还增加getObject()和getCount()接口

  1. public interface Rankable extends Comparable<Rankable> {
        Object getObject();
        long getCount();
    }

RankableObjectWithFields

RankableObjectWithFields实现Rankable接口   
1. 提供将Tuple转化为RankableObject   
Tuple由若干field组成, 第一个field作为obj, 第二个field作为count, 其余的都放到List<Object> otherFields中

2. 实现Rankable定义的getObject()和getCount()接口

3. 实现Comparable接口, 包含compareTo, equals

  1. public class RankableObjectWithFields implements Rankable

  1. public static RankableObjectWithFields from(Tuple tuple) {
        List<Object> otherFields = Lists.newArrayList(tuple.getValues());
        Object obj = otherFields.remove(0);
        Long count = (Long) otherFields.remove(0);
        return new RankableObjectWithFields(obj, count, otherFields.toArray());
    }

Rankings

Rankings维护需要排序的List, 并提供对List相应的操作

核心的数据结构如下, 用来存储rankable对象的list   
List<Rankable> rankedItems = Lists.newArrayList();

提供一些简单的操作, 比如设置maxsize(list size), getRankings(返回rankedItems, 排序列表)

核心的操作是,

  1. public void updateWith(Rankable r) {
        addOrReplace(r);
        rerank();
        shrinkRankingsIfNeeded();
    }

上一级的blot会定期的发送某个时间窗口的(obj, count), 所以obj之间的排序是在不断变化的 
1. 替换已有的, 或新增rankable对象(包含obj, count) 
2. 从新排序(Collections.sort) 
3. 由于只需要topN, 所以大于maxsize的需要删除

AbstractRankerBolt

首先以TopN为参数, 创建Rankings对象

  1. private final Rankings rankings;
    public AbstractRankerBolt(int topN, int emitFrequencyInSeconds) {
        count = topN;
        this.emitFrequencyInSeconds = emitFrequencyInSeconds;
        rankings = new Rankings(count);
    }

在execute中, 也是定时触发emit, 同样是通过emitFrequencyInSeconds来配置tickTuple   
一般情况, 只是使用updateRankingsWithTuple不断更新Rankings   
这里updateRankingsWithTuple是abstract函数, 需要子类重写具体的update逻辑

  1. public final void execute(Tuple tuple, BasicOutputCollector collector) {
        if (TupleHelpers.isTickTuple(tuple)) {
            emitRankings(collector);
        }
        else {
            updateRankingsWithTuple(tuple);
        }
    }

最终将整个rankings列表emit出去

  1. private void emitRankings(BasicOutputCollector collector) {
        collector.emit(new Values(rankings));
        getLogger().info("Rankings: " + rankings);
    }

TotalRankingsBolt

该bolt会使用globalGrouping, 意味着所有的数据都会被发送到同一个task进行最终的排序.   
TotalRankingsBolt同样继承自AbstractRankerBolt

  1. void updateRankingsWithTuple(Tuple tuple) {
        Rankings rankingsToBeMerged = (Rankings) tuple.getValue(0);
        super.getRankings().updateWith(rankingsToBeMerged);
    }

唯一的不同是, 这里updateWith的参数是个rankable列表, 在Rankings里面的实现一样, 只是多了遍历

最终可以得到, 全局的TopN的Rankings列表

Storm 实现滑动窗口计数和TopN排序的更多相关文章

  1. 滑动窗口计数java实现

    滑动窗口计数有很多使用场景,比如说限流防止系统雪崩.相比计数实现,滑动窗口实现会更加平滑,能自动消除毛刺. 概念上可以参考TCP的滑窗算法,可以看一下这篇文章(http://go12345.iteye ...

  2. storm入门(二):关于storm中某一段时间内topN的计算入门

    刚刚接触storm 对于滑动窗口的topN复杂模型有一些不理解,通过阅读其他的博客发现有两篇关于topN的非滑动窗口的介绍.然后转载过来. 下面是第一种: Storm的另一种常见模式是对流式数据进行所 ...

  3. storm 1.0版本滑动窗口的实现及原理

    滑动窗口在监控和统计应用的场景比较广泛,比如每隔一段时间(10s)统计最近30s的请求量或者异常次数,根据请求或者异常次数采取相应措施.在storm1.0版本之前,没有提供关于滑动窗口的实现,需要开发 ...

  4. Storm Windowing storm滑动窗口简介

    Storm Windowing 简介 Storm可同时处理窗口内的所有tuple.窗口可以从时间或数量上来划分,由如下两个因素决定: 窗口的长度,可以是时间间隔或Tuple数量: 滑动间隔(slidi ...

  5. storm滑动窗口

    Window滑动方式: 没有数据不滑动windowLength:窗口的时间长度/tuple个数slidingInterval:滑动的时间间隔/tuple个数 withWindow(Duration w ...

  6. uva 1606 amphiphilic carbon molecules【把缩写写出来,有惊喜】(滑动窗口)——yhx

    Shanghai Hypercomputers, the world's largest computer chip manufacturer, has invented a new classof ...

  7. 57、Spark Streaming: window滑动窗口以及热点搜索词滑动统计案例

    一.window滑动窗口 1.概述 Spark Streaming提供了滑动窗口操作的支持,从而让我们可以对一个滑动窗口内的数据执行计算操作.每次掉落在窗口内的RDD的数据, 会被聚合起来执行计算操作 ...

  8. [LeetCode] Sliding Window Maximum 滑动窗口最大值

    Given an array nums, there is a sliding window of size k which is moving from the very left of the a ...

  9. [LeetCode] Sliding Window Median 滑动窗口中位数

    Median is the middle value in an ordered integer list. If the size of the list is even, there is no ...

随机推荐

  1. GitHub注册和Git安装

    一.注册GitHub GitHub官方地址:https://github.com. 在浏览器中打开GitHub网址,通过首页进行注册,如下图所示. 二.安装Git Git官方下载地址:http://g ...

  2. [问题解决]Fresco设置占位图不显示的问题

    [问题解决]Fresco设置占位图不显示的问题 /** * Created by diql on 2017/02/15. */ 问题说明 本来设置占位图是通过以下方法: public void set ...

  3. js继承的实现(es5)

    js对面向对象的支持很弱,所以在ES6之前实现继承会绕比较多的弯(类似于对面向对象支持弱,然后强行拼凑面向对象的特性) es5中实现继承的几种方式,父类定义为Super function Super( ...

  4. elixir中的truth和true

    在elixir中, true 就是true 或者是:true 是一个原子 atom, 在其他语言中的true,这里叫做truth, 只要你不是false,nil ,就是truth, 当然 false和 ...

  5. WPF Lambda

    lambda简介 lambda运算符:所有的lambda表达式都是用新的lambda运算符 " => ",可以叫他,“转到”或者 “成为”.运算符将表达式分为两部分,左边指定 ...

  6. maven子模块转化成project

    把maven多模块中的子模块单独形成一个eclipse的project,需要使用导入 选中子模块,右键选择Import... 在选择弹出框中选择Maven--Existing Maven Projec ...

  7. C#泛型设计的一个小陷阱.

    距离上次发表博客已经有几年了. 对于没能坚持更新博客,实在是感觉到甚是惭愧. 闲言少叙, 直接切入主题. 背景 最近一直在对于公司一个网络通信服务程序使用.net core 进行重构.重构的目的有两个 ...

  8. SQLite数据类型(学习必备)

    最近在学习android端的数据库,以下知识点作为备用- 一.存储种类和数据类型: SQLite将数据值的存储划分为以下几种存储类型:     NULL: 表示该值为NULL值.     INTEGE ...

  9. C#PrintDocument打印尺寸调整

    /// <summary> /// 打印的按钮 /// </summary> /// <param name="sender"></par ...

  10. GridFS使用及配合nginx实现文件服务

    Mongodb下GridFS使用及配合nginx实现文件服务 一.GridFS简介 GridFS是mongodb下用来存储文件的一种规范,所有官方支持的驱动均实现了GridFS规范. Mongodb本 ...