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

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

Topology

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

 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()来进行配置,

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, 就可以完成定时触发的功能

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, 以释放空间

public final class SlotBasedCounter<T> implements Serializable {

    private static final long serialVersionUID = 4858185737378394432L;

    private final Map<T, long[]> objToCounts = new HashMap<T, long[]>();
    private final int numSlots;     public SlotBasedCounter(int numSlots) {
        if (numSlots <= 0) {
            throw new IllegalArgumentException("Number of slots must be greater than zero (you requested " + numSlots
                + ")");
        }
        this.numSlots = numSlots;
    }     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]++;
    }     public long getCount(T obj, int slot) {
        long[] counts = objToCounts.get(obj);
        if (counts == null) {
            return 0;
        }
        else {
            return counts[slot];
        }
    }     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;
    }     private long computeTotalCount(T obj) {
        long[] curr = objToCounts.get(obj);
        long total = 0;
        for (long l : curr) {
            total += l;
        }
        return total;
    }     /**
     * 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);
        }
    }     private void resetSlotCountToZero(T obj, int slot) {
        long[] counts = objToCounts.get(obj);
        counts[slot] = 0;
    }     private boolean shouldBeRemovedFromCounter(T obj) {
        return computeTotalCount(obj) == 0;
    }     /**
     * 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的实现, 如何在数组实现循环的滑动窗口

public final class SlidingWindowCounter<T> implements Serializable {

    private static final long serialVersionUID = -2645063988768785810L;

    private SlotBasedCounter<T> objCounter;
    private int headSlot;
    private int tailSlot;
    private int windowLengthInSlots;     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);         this.headSlot = 0;
        this.tailSlot = slotAfter(headSlot);
    }     public void incrementCount(T obj) {
        objCounter.incrementCount(obj, headSlot);
    }     /**
     * 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;
    }     private void advanceHead() {
        headSlot = tailSlot;
        tailSlot = slotAfter(tailSlot);
    }     private int slotAfter(int slot) {
        return (slot + 1) % windowLengthInSlots;
    }
}
 

IntermediateRankingsBolt

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

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

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

Rankable

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

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

public class RankableObjectWithFields implements Rankable

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, 排序列表)

核心的操作是,

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对象

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逻辑

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

最终将整个rankings列表emit出去

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

TotalRankingsBolt

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

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. redis之 集群配置(主从复制)

    集群的作用: 集群的方式有二种: 第二种的好处是:master宕机以后可以直接切换到slave1 主从通信的过程 先通过dump导出大块的rdb 然后把aof缓冲过来,然后通过replicationf ...

  2. 四大组件之Activity Task任务栈4种启动模式

    1.启动模式 standard,创建一个新的Activity. singleTop,栈顶不是该类型的Activity,创建一个新的Activity.否则,onNewIntent. singleTask ...

  3. Js 中的false,零值,null,undefined和空字符串对象

    转自  http://www.imkevinyang.com/2009/07/javascript-中的false零值nullundefined和空字符串对象.html 在Javascript中,我们 ...

  4. PHP将字符串写入txt文件

    function writelog($str) { $open=fopen("e:\log.txt","a" ); fwrite($open,$str); fc ...

  5. java几个经典的算法题目----------查询子串和等于已知数字

    给出一个排序好的数组和一个数,求数组中连续元素的和等于所给数的子数组 public class testClockwiseOutput { public static void main(String ...

  6. session_start()导致history.go(-1)返回时无法保存表单数据的解决方法

    问题背景: 在填写完表单提交时,由于某个表单项可能填写的不合法,导致提交失败,返回表单页面.但返回后所有的表单都被清空了,重新填写比较麻烦,度娘解释说,是由于每个页面都调用了session_start ...

  7. Weblogic Maven

    从weblogic 10.3.4开始支持maven deploy部署   步骤如下: 1.构建weblogic-maven-plugin jar 在D:\oracle\Middleware\wlser ...

  8. 了解SSL必须要懂得密码技术

    要理解SSL就必须理解密码系统.消息摘要函数(单向或散列函数)和数字签名,这些技术是许多文献所讨论的主题(比如[AC96),提供了保密性.完整性和认证的基础. 密码系统 假设Alice想给她的银行发一 ...

  9. 如何用webgl(three.js)搭建不规则建筑模型,客流量热力图模拟

    本节课主要讲解如何用webgl(three.js)搭建一个建筑模型,客流量热力图模拟 使用技术说明: 这里主要用到了three.js,echart.js以及一些其它的js 与css技术,利用webso ...

  10. Vue 脱坑记 - 查漏补缺(汇总下群里高频询问的xxx及给出不靠谱的解决方案)

    前言 发现群里有些问题的提问重复率太高了,每次都去回答,回答的贼烦.这里做一个大体的汇总,废话不多说,直接开始给出方案,不是手把手..若是连问题和解决都看不懂的..应该去补充下基础知识 问题汇总 Q: ...