关于storm技术收集

原 顶 Storm【Storm0.9.3】- ACK 框架有意的补充
发表于1年前(2014-11-19 18:50) 阅读(152) | 评论(0) 0人收藏此文章, 我要收藏
赞0

摘要 Storm的Ack框架,在Storm之有意思的有2个地方:1 Ack机制,2 Trident的抽象

之前对于Storm的Acker机制进行了一些数学上的描述。

在这里,对于Storm的Ack机制 在源码实现上进行一些有意的补充。

1: 在Ack框架的设计之中,Storm发射出去的消息都会对应于一个随机的消息ID号。

2 : Spout发射消息之后。将像Acker Bolt发射一个消息,这个消息的内容为 《RootID,消息ID》 Acker Bolt将会为该消息创

建一条跟踪项

3: Bolt产生要发射的消息的过程之中,也会将该消息ID发射Acker Bolt,AckerBolt 对消息ID进行异或以后进行存储。

4: Bolt在对于输入的消息进行Ack的过程之中,也会将该消息ID发射到 AckerBolt,Acker Bolt对于消息ID进行亦或以后再存储,由于该消息在被发射之时,已经向Acker Bolt发射过了ID,之后在被Ack 时又再次发射该消息ID,一句 【与或】的语义,这相当于对于该消息的跟踪的结束。

5:Acker Bolt 在更新某一个消息的跟踪值时,若发现他的值变为0,那么就会像Spout节点发射消息,表名,Spout发射的这条消息已经被成功的处理 。

6:所有消息的消息ID 都将更新到最 根上( /root ) 树上的,根消息为Spout发射出去的消息,Bolt新产生的消息并不会被单独跟踪

7:如果你在发射的过程之中,没有指定用于消息跟踪的ID,那么系统就不对消息进行跟踪,若系统中不含有Ack bolt,消息也不会被跟踪。

8: Spout的每条消息以及该消息所演化出来的消息的跟踪 容量负载为:《16个字节》,8个字节的根消息ID,以及8个字节的消息跟踪值《AckValue》, 事实上,由于Storm中采用的是HashMap对其进行的跟踪,在32位的JVM之中,每条消息至少需要20个字节的额外负载,故有一条消息的跟踪需要 40个左右的负载。

具体的代码实现分析,可能需要 Cloujre的分析基础,如果您有兴趣,亲个参考本ID的其他系列:

 Storm【Storm0.9.3】- 源码分析:ACK 框架之 Acker Bolt的实现分析 

原 顶 Storm【Storm0.9.3】- 官方翻译 2: Serialization
发表于1年前(2014-10-31 10:55) 阅读(182) | 评论(0) 0人收藏此文章, 我要收藏
赞0

摘要 本小节将对于Storm的序列化的机行初步的阐述

目录[-]
Dynamic typing
Custom serialization
Java serialization
Component-specific serialization registrations
This page is about how the serialization system in Storm works for versions 0.6.0 and onwards. Storm used a different serialization system prior to 0.6.0 which is documented on Serialization (prior to 0.6.0).

本章的描述适应于Storm0.6.0 以及以后的一些版本采取了其他的序列化系统。

Tuples can be compressed of objects of any types. Since Storm is a distributed system, it needs to know how to serialize and deserialize objects when they’re passed between tasks.

Tuples能偶被压缩成为任意类型的对象,做为分布式的消息处理系统,它需要知道怎么样在Task之间去序列化和反序列化对象。

Storm uses Kryo for serialization. Kryo is a flexible and fast serialization library that produces small serializations.

Storm使用了Kryo作为序列化的手段,Kryo 是一个快速高效的Java对象图形序列化框架,主要特点是性能、高效和易用。该项目用来序列化对象到文件、数据库或者网络。

By default, Storm can serialize primitive types, strings, byte arrays, ArrayList, HashMap, HashSet, and the Clojure collection types. If you want to use another type in your tuples, you’ll need to register a custom serializer.

在默认的情况之下,Storm能够序列化以下的基本类型:String,字节数组,ArrayList,HashMap,HashSet,还包括了一些Clojure的集合类型,如果你想在Tuple的传递过程之中使用其他的类型,那么你需要手工的去注册一个Serializer。

Dynamic typing

动态的类型

对于元祖中的Field,是没有任何的类型声明的,你只需要向里面填充,并且Storm会自动的解析并且动态的序列化,在我们拿到这个序列化的接口之前,让我们在理解为什么Storn‘s tuples是一个动态的类型之上,在展开来谈。

There are no type declarations for fields in a Tuple. You put objects in fields and Storm figures out the serialization dynamically. Before we get to the interface for serialization, let’s spend a moment understanding why Storm’s tuples are dynamically typed.

一旦为tule的fileds增加了静态的类型,将会给Storm的API带来更多的复杂性,静态的类型她自身的keys和Values将会要有有大量的Annotation

Adding static typing to tuple fields would add large amount of complexity to Storm’s API. Hadoop, for example, statically types its keys and values but requires a huge amount of annotations on the part of the user. Hadoop’s API is a burden to use and the “type safety” isn’t worth it. Dynamic typing is simply easier to use.

除此以外,无论在哪个方面还没有找到一个合适的理由是这么做。支持一个Bolt订阅多个Stream,这些从不同流过来的Strems也可能有不同的类型,每单一个Bolt接收到一个Tuple在Execute方法,这个Tuple可能来自于任意的流,应此也可能有任意任意的类型,这可能在通过在每一个tuple Stream 设置特别的Reflection magic来区分,但是最简单,最直观的办法就死一个动态Dynamic 的类型。

Further than that, it’s not possible to statically type Storm’s tuples in any reasonable way. Suppose a Bolt subscribes to multiple streams. The tuples from all those streams may have different types across the fields. When a Bolt receives a Tuple inexecute, that tuple could have come from any stream and so could have any combination of types. There might be some reflection magic you can do to declare a different method for every tuple stream a bolt subscribes to, but Storm opts for the simpler, straightforward approach of dynamic typing.

Finally, another reason for using dynamic typing is so Storm can be used in a straightforward manner from dynamically typed languages like Clojure and JRuby.

自定义序列化

Custom serialization

通常而言,Storm使用了Kryo的序列化,为了去实现自定义的序列化,你需要通过keyo去注册一个新的Serializer,如果有必要,请直接去访问Kryo’home page去明白怎杨去处理定制化。

As mentioned, Storm uses Kryo for serialization. To implement custom serializers, you need to register new serializers with Kryo. It’s highly recommended that you read over Kryo’s home page to understand how it handles custom serialization.

增加一个定制化的Serialzeers,你需要在你的topology配置之中新增。

Adding custom serializers is done through the “topology.kryo.register” property in your topology

他将花销一系列的注册器,每一个registraction能够采取以下的两种形式

config. It takes a list of registrations, where each registration can take one of two forms:

The name of a class to register. In this case, Storm will use Kryo’sFieldsSerializer to serialize the class. This may or may not be optimal for the class – see the Kryo docs for more details.

A map from the name of a class to register to an implementation ofcom.esotericsoftware.kryo.Serializer.

Let’s look at an example.

具体的释放如下

topology.kryo.register: - com.mycompany.CustomType1 - com.mycompany.CustomType2: com.mycompany.serializer.CustomType2Serializer - com.mycompany.CustomType3

com.mycompany.CustomType1 and com.mycompany.CustomType3 will use the FieldsSerializer, whereascom.mycompany.CustomType2 will use com.mycompany.serializer.CustomType2Serializer for serialization.

Storm也同样的提供了在topology Config之中注册序列化的helper,这个配置类有一个方法就叫

registerSreializaton

Storm provides helpers for registering serializers in a topology config. The Configclass has a method called registerSerialization that takes in a registration to add to the config.

相比之下,还会有更高级的Config配置被称为:Config.TOPOLOGY_SKIP_MISSING_KRYO_REGISTRATIONS.,

如果你把他摄制为true,Storn将会自动的ignore任何已经注册但是布恩那个在ClassPath之中找到代码的序列

There’s an advanced config called Config.TOPOLOGY_SKIP_MISSING_KRYO_REGISTRATIONS. If you set this to true, Storm will ignore any serializations that are registered but do not have their code available on the classpath.

//另外一方面,Storm将会抛出一个Error,每当你找不到这个Serialzation的时候,这是非常有必要的,如果你运许多topologirs在一个集群之中,并且每一个都有自身的序列化系统,那么你就必须在把每一个使用的序列化都配置到Storm.yarml文件之中

Otherwise, Storm will throw errors when it can’t find a serialization. This is useful if you run many topologies on a cluster that each have different serializations, but you want to declare all the serializations across all topologies in the storm.yaml files.

Java serialization

如果Storm遇到了一种类型,并且这个类型还没有被注册到Storm之中,他将会使用java的序列化,如果可能,如果连java的序列化都不能正常使用,那么Storm就会抛出一个异常。

If Storm encounters a type for which it doesn’t have a serialization registered, it will use Java serialization if possible. If the object can’t be serialized with Java serialization, then Storm will throw an error.

在这里必须清醒的意识到,java的序列化的开销是非常昂贵的。不管是在CPU的开销,还是序列化的对象的大小之上,强烈的建议你使用自定义的序列化在工业的生产环境之中

Beware that Java serialization is extremely expensive, both in terms of CPU cost as well as the size of the serialized object. It is highly recommended that you register custom serializers when you put the topology in production. The Java serialization behavior is there so that it’s easy to prototype new topologies.

You can turn off the behavior to fall back on Java serialization by setting theConfig.TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION config to false.

Component-specific serialization registrations

Storm 0.7.0 lets you set component-specific configurations (read more about this atConfiguration). Of course, if one component defines a serialization that serialization will need to be available to other bolts – otherwise they won’t be able to receive messages from that component!

When a topology is submitted, a single set of serializations is chosen to be used by all components in the topology for sending messages. This is done by merging the component-specific serializer registrations with the regular set of serialization registrations. If two components define serializers for the same class, one of the serializers is chosen arbitrarily.

To force a serializer for a particular class if there’s a conflict between two component-specific registrations, just define the serializer you want to use in the topology-specific configuration. The topology-specific configuration has precedence over component-specific configurations for serialization registrations.

原 顶 Storm【压力测试】- 系列1: 进行简单的压力测试
发表于1年前(2014-10-16 14:51) 阅读(174) | 评论(0) 0人收藏此文章, 我要收藏
赞0

摘要 在目前而言,由于业务的上的数据量远远小于Storm的设计阈值。以下提供一个简单的压力测试Class,通过UI来检测Strorm集群的近况。

代码比较简单,看图说话:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package storm.benchmark;

import backtype.storm.Config;
import backtype.storm.StormSubmitter;
import backtype.storm.spout.SpoutOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.BasicOutputCollector;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.TopologyBuilder;
import backtype.storm.topology.base.BaseBasicBolt;
import backtype.storm.topology.base.BaseRichSpout;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Tuple;
import backtype.storm.tuple.Values;
import java.util.Map;
import java.util.Random;

public class ThroughputTest {
public static class GenSpout extends BaseRichSpout {
private static final Character[] CHARS = new Character[] { ‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’, ‘g’, ‘h’};

    SpoutOutputCollector _collector;
    int _size;
    Random _rand;
    String _id;
    String _val;

    public GenSpout(int size) {
        _size = size;
    }

    @Override
    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
        _collector = collector;
        _rand = new Random();
        _id = randString(5);
        _val = randString(_size);
    }

    @Override
    public void nextTuple() {
        _collector.emit(new Values(_id, _val));

    }

    private String randString(int size) {
        StringBuffer buf = new StringBuffer();
        for(int i=0; i<size; i++) {
            buf.append(CHARS[_rand.nextInt(CHARS.length)]);
        }
        return buf.toString();
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("id", "item"));
    }        
}

public static class IdentityBolt extends BaseBasicBolt {
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("id", "item"));
    }

    @Override
    public void execute(Tuple tuple, BasicOutputCollector collector) {
        collector.emit(tuple.getValues());
    }        
}

public static class CountBolt extends BaseBasicBolt {
    int _count;

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("count"));
    }

    @Override
    public void execute(Tuple tuple, BasicOutputCollector collector) {
        _count+=1;
        collector.emit(new Values(_count));
    }        
}

public static class AckBolt extends BaseBasicBolt {
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    }

    @Override
    public void execute(Tuple tuple, BasicOutputCollector collector) {
    }       
}


//storm jar storm-benchmark-0.0.1-SNAPSHOT-standalone.jar storm.benchmark.ThroughputTest demo 100 8 8 8 10000
public static void main(String[] args) throws Exception {
    int size = Integer.parseInt(args[1]);
    int workers = Integer.parseInt(args[2]);
    int spout = Integer.parseInt(args[3]);
    int bolt = Integer.parseInt(args[4]);        
    int maxPending = Integer.parseInt(args[5]);

    TopologyBuilder builder = new TopologyBuilder();
    builder.setSpout("spout", new GenSpout(size), spout);

// builder.setBolt(“count”, new CountBolt(), bolt)
// .fieldsGrouping(“bolt”, new Fields(“id”));
// builder.setBolt(“bolt”, new IdentityBolt(), bolt)
// .shuffleGrouping(“spout”);
builder.setBolt(“bolt2”, new AckBolt(), bolt)
.shuffleGrouping(“spout”);
// builder.setBolt(“count2”, new CountBolt(), bolt)
// .fieldsGrouping(“bolt2”, new Fields(“id”));

    Config conf = new Config();
    conf.setNumWorkers(workers);
    //conf.setMaxSpoutPending(maxPending);
    conf.setNumAckers(0);
    conf.setStatsSampleRate(0.0001);
    //topology.executor.receive.buffer.size: 8192 #batched
    //topology.executor.send.buffer.size: 8192 #individual messages
    //topology.transfer.buffer.size: 1024 # batched

    //conf.put("topology.executor.send.buffer.size", 1024);
    //conf.put("topology.transfer.buffer.size", 8);
    //conf.put("topology.receiver.buffer.size", 8);
    //conf.put(Config.TOPOLOGY_WORKER_CHILDOPTS, "-Xdebug -Xrunjdwp:transport=dt_socket,address=1%ID%,server=y,suspend=n");

    StormSubmitter.submitTopology(args[0], conf, builder.createTopology());
}

}
顶 转 Storm 【Nimbus/Supervisor】本地目录结构
发表于2年前(2014-09-16 16:35) 阅读(180) | 评论(0) 0人收藏此文章, 我要收藏
赞0

摘要 特别感谢:Storm开发者之一的国人:徐明明。多谢他不断的翻译 Storm的最新进展与消息。 其博客地址为:http://xumingming.sinaapp.com/category/storm/

 阅读背景:确保您已经成功的安装了Storm,并且已经找到了storm-local 的本地文件夹

 阅读目的: 究竟在Storm的本地文件夹之有什么? 持有到zookeeper上的信息已被大家所熟知,那么

持有到本地的信息了?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/{storm-local-dir}
|
|-/nimbus
| |
| |-/inbox – 从nimbus客户端上传的jar包
| | | 会在这个目录里面
| | |
| | |-/stormjar-{uuid}.jar – 上传的jar包其中{uuid}表示
| | 生成的一个uuid
| |
| |-/stormdist
| |
| |-/{topology-id}
| |
| |-/stormjar.jar – 包含这个topology所有代码
| | 的jar包(从nimbus/inbox里
| | 面挪过来的)
| |
| |-/stormcode.ser – 这个topology对象的序列化
| |
| |-/stormconf.ser – 运行这个topology的配置
|
|-/supervisor
| |
| |-/stormdist
| | |
| | |-/{topology-id}
| | |
| | |-/resources – 这里保存的是topology的
| | | jar包里面的resources目录
| | | 下面的所有文件
| | |
| | |-/stormjar.jar – 从nimbus机器上下载来的
| | | topology的jar包
| | |
| | |-/stormcode.ser – 从nimbus机器上下载来的
| | | 这个topology对象的序列
| | | 化形式
| | |
| | |-/stormconf.ser – 从nimbus机器上下载来的
| | 运行这个topology的配置
| |
| |-/localstate – supervisor的localstate
| |
| |-/tmp – 临时目录,从Nimbus上下
| | 载的文件会先存在这个目
| | 录里面,然后做一些简单
| | 处理再copy到
| | stormdist/{topology-id}
| | 里面去
| |-/{uuid}
| |
| |-/stormjar.jar – 从Nimbus上面download下
| 来的工作jar包
|
|-/workers
|
|-/{worker-id}
|
|-/pids – 一个worker可能会起多个子
| | 进程所以可能会有多个pid
| |
| |-/{pid} – 运行这个worker的JVM的pid
|
|-/heartbeats – 这个supervisor机器上的
| worker的心跳信息
|
|-/{worker-id} – 这里面存的是一个worker
的心跳:主要包括心跳时
间和worker的id

原 顶 Storm实践3【TopN封装版】 - 1: 简要介绍
发表于2年前(2014-09-13 13:41) 阅读(231) | 评论(1) 1人收藏此文章, 我要收藏
赞0

摘要 Storm实践的第三个系列:TopN的封装版,Storm实践2的基础之上,再封装。

阅读前提:在阅读之前务必请先参看本ID的 Storm实践系列2

阅读目的 : 优化之前的封装

最新版本的Storm TOPN 整体的构造如下:

依然包含如下的几个package:

        1 Spout: 模拟发射数据

        2 bolt : 处理数据

        3 topology: 启动的topo类

        4:utils 工具类 



这里做一个简要的介绍: SImple Tiples, 从下篇博文开始将 依次来解读每一个 java。

原 荐 顶 Storm 和JStorm
发表于2年前(2014-08-30 23:33) 阅读(5514) | 评论(1) 54人收藏此文章, 我要收藏
赞2

摘要 由于storm的内核是clojure编写的,目前阿里巴巴公司已经有开源的Copy版本Jstorn,以下本ID为你带来其中相关区别

关于流处理框架,在先前的文章汇总已经介绍过Strom,今天学习的是来自阿里的的流处理框架JStorm。简单的概述Storm就是:JStorm 比Storm更稳定,更强大,更快,Storm上跑的程序,一行代码不变可以运行在JStorm上。直白的将JStorm是阿里巴巴的团队基于Storm的二次开发产物,相当于他们的Tengine是基于Ngix开发的一样。

jstorm

阿里拥有自己的实时计算引擎

类似于hadoop 中的MR

开源storm响应太慢

开源社区的速度完全跟不上Ali的需求

降低未来运维成本

提供更多技术支持,加快内部业务响应速度

现有Storm无法满足一些需求

现有storm调度太简单粗暴,无法定制化

Storm 任务分配不平衡

RPC OOM一直没有解决

监控太简单

对ZK 访问频繁

JStorm相比Storm更稳定

Nimbus 实现HA:当一台nimbus挂了,自动热切到备份nimbus

原生Storm RPC:Zeromq 使用堆外内存,导致OS 内存不够,Netty 导致OOM;JStorm底层RPC 采用netty + disruptor保证发送速度和接受速度是匹配的

新上线的任务不会冲击老的任务:新调度从cpu,memory,disk,net 四个角度对任务进行分配,已经分配好的新任务,无需去抢占老任务的cpu,memory,disk和net

Supervisor主线

Spout/Bolt 的open/prepar

所有IO, 序列化,反序列化

减少对ZK的访问量:去掉大量无用的watch;task的心跳时间延长一倍;Task心跳检测无需全ZK扫描。

JStorm相比Storm调度更强大

彻底解决了storm 任务分配不均衡问题

从4个维度进行任务分配:CPU、Memory、Disk、Net

默认一个task,一个cpu slot。当task消耗更多的cpu时,可以申请更多cpu slot

默认一个task,一个memory slot。当task需要更多内存时,可以申请更多内存slot

默认task,不申请disk slot。当task 磁盘IO较重时,可以申请disk slot

可以强制某个component的task 运行在不同的节点上

可以强制topology运行在单独一个节点上

可以自定义任务分配,提前预约任务分配到哪台机器上,哪个端口,多少个cpu slot,多少内存,是否申请磁盘

可以预约上一次成功运行时的任务分配,上次task分配了什么资源,这次还是使用这些资源

JStorm相比Storm性能更好

JStorm 0.9.0 性能非常的好,使用netty时单worker 发送最大速度为11万QPS,使用zeromq时,最大速度为12万QPS。

JStorm 0.9.0 在使用Netty的情况下,比Storm 0.9.0 使用netty情况下,快10%, 并且JStorm netty是稳定的而Storm 的Netty是不稳定的

在使用ZeroMQ的情况下, JStorm 0.9.0 比Storm 0.9.0 快30%

性能提升的原因:

Zeromq 减少一次内存拷贝

增加反序列化线程

重写采样代码,大幅减少采样影响

优化ack代码

优化缓冲map性能

Java 比clojure更底层

JStorm的其他优化点

资源隔离。不同部门,使用不同的组名,每个组有自己的Quato;不同组的资源隔离;采用cgroups 硬隔离

Classloader。解决应用的类和Jstorm的类发生冲突,应用的类在自己的类空间中

Task 内部异步化。Worker 内部全流水线模式,Spout nextTuple和ack/fail运行在不同线程

具体如何实现,请参考本ID的的博文系列 【jstorm-源码解析】

顶 Storm【实践系列-如何写一个爬虫- Metric 系列】1
发表于2年前(2014-08-21 16:19) 阅读(399) | 评论(0) 0人收藏此文章, 我要收藏
赞0

摘要 进入Storm开发的另外一个主题: Metric

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package com.digitalpebble.storm.crawler;

import backtype.storm.Config;
import backtype.storm.metric.MetricsConsumerBolt;
import backtype.storm.metric.api.IMetricsConsumer;
import backtype.storm.task.IErrorReporter;
import backtype.storm.task.OutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.tuple.Tuple;
import backtype.storm.utils.Utils;
import com.google.common.base.Joiner;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ObjectWriter;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.ServletHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

/**
* @author Enno Shioji (enno.shioji@peerindex.com)
*/
public class DebugMetricConsumer implements IMetricsConsumer {
private static final Logger log = LoggerFactory
.getLogger(DebugMetricConsumer.class);
private IErrorReporter errorReporter;
private Server server;

// Make visible to servlet threads
private volatile TopologyContext context;
private volatile ConcurrentMap<String, Number> metrics;
private volatile ConcurrentMap<String, Map<String, Object>> metrics_metadata;

public void prepare(Map stormConf, Object registrationArgument,
        TopologyContext context, IErrorReporter errorReporter) {
    this.context = context;
    this.errorReporter = errorReporter;
    this.metrics = new ConcurrentHashMap<String, Number>();
    this.metrics_metadata = new ConcurrentHashMap<String, Map<String, Object>>();

    try {
        // TODO Config file not tested
        final String PORT_CONFIG_STRING = "topology.metrics.consumers.debug.servlet.port";
        Integer port = (Integer) stormConf.get(PORT_CONFIG_STRING);
        if (port == null) {
            log.warn("Metrics debug servlet's port not specified, defaulting to 7070. You can specify it via "
                    + PORT_CONFIG_STRING + " in storm.yaml");
            port = 7070;
        }
        server = startServlet(port);
    } catch (Exception e) {
        log.error("Failed to start metrics server", e);
        throw new AssertionError(e);
    }
}

private static final Joiner ON_COLONS = Joiner.on("::");

public void handleDataPoints(TaskInfo taskInfo,
        Collection<DataPoint> dataPoints) {
    // In order
    String componentId = taskInfo.srcComponentId;
    Integer taskId = taskInfo.srcTaskId;
    Integer updateInterval = taskInfo.updateIntervalSecs;
    Long timestamp = taskInfo.timestamp;
    for (DataPoint point : dataPoints) {
        String metric_name = point.name;
        try {
            Map<String, Number> metric = (Map<String, Number>) point.value;
            for (Map.Entry<String, Number> entry : metric.entrySet()) {
                String metricId = ON_COLONS.join(componentId, taskId,
                        metric_name, entry.getKey());
                Number val = entry.getValue();
                metrics.put(metricId, val);
                metrics_metadata.put(metricId, ImmutableMap
                        .<String, Object> of("updateInterval",
                                updateInterval, "lastreported", timestamp));
            }
        } catch (RuntimeException e) {
            // One can easily send something else than a Map<String,Number>
            // down the __metrics stream and make this part break.
            // If you ask me either the message should carry type
            // information or there should be different stream per message
            // type
            // This is one of the reasons why I want to write a further
            // abstraction on this facility
            errorReporter.reportError(e);
            metrics_metadata
                    .putIfAbsent("ERROR_METRIC_CONSUMER_"
                            + e.getClass().getSimpleName(), ImmutableMap
                            .of("offending_message_sample", point.value));
        }
    }
}

private static final ObjectMapper OM = new ObjectMapper();

private Server startServlet(int serverPort) throws Exception {
    // Setup HTTP server
    Server server = new Server(serverPort);
    Context root = new Context(server, "/");
    server.start();

    HttpServlet servlet = new HttpServlet() {
        @Override
        protected void doGet(HttpServletRequest req,
                HttpServletResponse resp) throws ServletException,
                IOException {
            SortedMap<String, Number> metrics = ImmutableSortedMap
                    .copyOf(DebugMetricConsumer.this.metrics);
            SortedMap<String, Map<String, Object>> metrics_metadata = ImmutableSortedMap
                    .copyOf(DebugMetricConsumer.this.metrics_metadata);

            Map<String, Object> toplevel = ImmutableMap
                    .of("retrieved",
                            new Date(),

                            // TODO this call fails with mysterious
                            // exception
                            // "java.lang.IllegalArgumentException: Could not find component common for __metrics"
                            // Mailing list suggests it's a library version
                            // issue but couldn't find anything suspicious
                            // Need to eventually investigate
                            // "sources",
                            // context.getThisSources().toString(),

                            "metrics", metrics, "metric_metadata",
                            metrics_metadata);

            ObjectWriter prettyPrinter = OM
                    .writerWithDefaultPrettyPrinter();
            prettyPrinter.writeValue(resp.getWriter(), toplevel);
        }
    };

    root.addServlet(new ServletHolder(servlet), "/metrics");

    log.info("Started metric server...");
    return server;

}

public void cleanup() {
    try {
        server.stop();
    } catch (Exception e) {
        throw new AssertionError(e);
    }
}

}

前提说明:

      storm从0.9.0开始,增加了指标统计框架,用来收集应用程序的特定指标,并将其输出到外部系统。

       一般来说,您只需要去实现 LoggingMetricsConsumer,统计将指标值输出到metric.log日志文件之中。

当然,您也可以自定义一个监听的类:只需要去实现IMetricsConsumer接口就可以了。这些类可以在代码里注册(registerMetricsConsumer),也可以在 storm.yaml配置文件中注册:

原 顶 Storm【实践系列】 - 如何部署您的工程项目带Storm集群
发表于2年前(2014-08-03 12:04) 阅读(187) | 评论(0) 1人收藏此文章, 我要收藏
赞0

摘要 关键:如何从eclipse导出Storm可以运行的jar包

本章主题:

            详细的描述如何在生产环境之中提交您的代码到Storm平台运行。

1 : 首先确保您的工程代码,完美无错。能够在LocalMode 下运行

2 : 导出您的Storm jar包,如图所示:

在上面的截图之中值包含:

                    2.1 : src 目录

                     2.2:.project 目录  请不要勾选 .classapth位置路径



注意:  

        在我们的lib目录下面,有一个子目录为NoStorm,也就是说我们的整个工程除了包含有Strom的jar包意外,还包含了其他的jar包。 请务必将您所需要的的三方jar包放入集群之中Storm_home/lib的目录之下。



 补充:

        1 : 请不要使用Export 到处Runnable jar,Eclipse中导出Runnbalejar  会自动打包两份配置文件,造成集群本身和Jar包两者配置文件的冲突



        2:一旦您的Export jar包出现了  no zip,亦或是unpackage  zip 不能解析jar的错误,请您务必



                2.1:检查您的jar包是否错误

                2.2:检查您的jar包在传输的过程之中是够出现错误

                2.3:检查您的Storm 服务器环境是否有问题

顶 Storm 【系统设计】-提交过程

分享到:
原 顶 Storm【实践】系列 - 电子商务系统的小demo
发表于2年前(2014-06-30 17:15) 阅读(501) | 评论(0) 0人收藏此文章, 我要收藏
赞0

摘要 电商云项目,实战Storm。

1 项目背景 :

        做为电子商务的实时统计分析系统,如何对实时产生的日志进行统计和分析,将是目前 云商的一大热点。

2 系统流程:

        如下表:





各个类型的的数据日志,包括,点击,点赞,购买,评论等消息生成来源,通过kafka日志收集工具,不断的将数据发送到Storm的计算集群之中,经由Storm处理以后,将处理完成的结果写回redis,亦或是mysql。

3: 日志的介绍:

        对于所要分析的日志~,由于需要屏蔽掉真实项目的授权问题,我仅仅在此提供一些最低级别的日志字段:

请参看如下表:

请参看如上表格:

       目前的情况 

               First : 我们的统计在维度上如下:

                    1 地域

                    2 时间    

                    3 各个系统的版本号

                    4 来源类型



                Second:我们在统计粒度上如下

                    1: 地域,我们需要保证三级的分类   【省,市,县】

                    2 :时间上,【年,月,日,小是,分】

                    3 :时间上,我们还需要附加的时间额度: 星期,季度

也就是说,在一个基本的范畴之类,我们的统计为:

             实时的查看,某一天【时间】,在某一个地点【地域】, 在某一个系统版本【版本】下的 pv,uv

顶 Storm 【技术博客】- 图解Storm的各个流程
发表于2年前(2014-06-26 14:39) 阅读(297) | 评论(0) 0人收藏此文章, 我要收藏
赞0

分享到:
顶 Storm[技术博客] Storm数据流模型的分析以及讨论
发表于2年前(2014-06-24 17:59) 阅读(278) | 评论(0) 0人收藏此文章, 我要收藏
赞0

摘要 Storm 数据模型的局限以及适应的范畴

Storm 是一个开源的实时计算系统,它提供了一系列的基本元素用于进行计算:

1 Topology

2 Stream

3 spout

4 bolt

在我们提交我们的topology的时候,一旦你提交了你的topology到你的集群之中后,除非你显示的去停止任务

集群中间的topology会一直的在运行

计算任务Topology是由不同的Spouts 和 bolts,通过数据流 Stream连接起来的图,下面是一个Topology的结构示意图

其中包括了

1 : Spout: Strom 中的消息源头,用于为Topology来生产消息(数据),一般是从外部的数据源开始读取数据,在我们的真实环境之中,我们采用的是 kafka-Storm 流式对接的接口,所以我们 使用的Spout为 :kafkaSpout

2 Bolt, Storm中的消息处理者,用于为Topology 进行消息的处理,Bolt,可以执行如下的几种操作:

     2.1 :过滤

     2.2: 聚合

     2.3: 查询数据库

              等几种操作,并且可以一级一级的进行处理,最终Topology会被提交到Storm集群中运行,也可以通过命令停止topology的运行,并且将占用的资源归还给Storm集群。

Storm 数据流模型

数据流的模型是Storm中对数据进行的抽象,它是时间上无界的tuple的元祖,在topology之中,Spout是bolt的源头,

bolt是对于Spout的消费者,负责Topology从特定数据源发射Stream,bolt可以接受任意多个Stream输入,然后进行数据的加工处理工作,如果需要,bolt还可以发射出新的Stream给下一级Bolt进行处理

下面是一个Topology内部Spout和Bolt之间的数据流关系:

topology中每一个计算组件(Spout和bolt) 都有一个并行度来控制,在创建Topology时可以进行指定,Storm在集群内分配对应并行度个数的线程来同时执行这一个组件

那么,有一个这样的问题: 既然对于一个Spout,或Bolt,都会有多个task线程来运行,那么如何在两个组件之间发送tuple 元祖了?

Storm 提供了好几种数据流的分发策略用来解决这一个问题,在Topology定义的时,需要为每一个bolt指定接受什么样的Stream作为它输入

目前Storm中提供了一下的7种Stream Grouping

Shuffle Grouping、

Fields Grouping、

All Grouping、

Global Grouping、

Non Grouping、

Direct Grouping、

Local or shuffle grouping

一种Storm不能支持的场景

如果您阅读到这里,那么您可以细细的回想起来,当我们每一个业务逻辑都被一个Topolo持有的时候,

只能在Topology内按照 “发布-订阅”方式在不同的计算组件(spout/bolt)之间进行数据的处理,而Stream在

Topology之间是无法流动的。

很多时候,开始需要把你所有的业务逻辑写到你的一个Topology之中,请不要忘记:Stream在topology之间是无法流动的

也就是意味着一个业务逻辑的过程,不能够和另外的一个业务过程进行通信

我们假设现在有这样的一个Topology1,在整个Topology的过程之中,通过初步的 filter,join bolt,Business1

Bolt,其中,Filter Bolt用于对数据的过滤,join Bolt用于数据流的聚合,如下图所示:

目前这个Topology已经被提交到集群了,那么,如果我们需要一个新的业务逻辑,而

这个Topology的特点是和Topology1 公用的数据源,而且前期的预处理过程是一样的

那么这时候Storm 怎么满足这一需求?

1 第一: kill掉原先的topology,然后实现bussiness Bolt的计算逻辑,并且重新打包形成一个新的

topology计算任务的jar 包后,提交到Storm集群之中重新运行,那么目前,我们的结构图如下所示:

这样的过程之中,来自于不同数据源的处理过程,经过处理以后,经过join以后,被发送到两个业务逻辑的处理Bolt之中。

第一种方式的缺陷:

Topology 需要重新来部署,并且状态会丢失。而且需要修改你自身的topology结构,失去了稳定性的保证

2:第二种方式:

同一份的数据源被被两份处理流程所消费。无疑增加了External Data Source的负载压力,而且会导致我们的发射数据在集群之中被传输两份,一旦数据重复读取的因子超过2,那么对Storm 的计算Slot的浪费很严重

3 第三种方式

ok,看了以上两种方式以后,也许你会提出下面的解决方案,通过kafka这样的消息中间件,来实现不同Topology的

Spout 共享数据源头,而且这样可以做到

                        3.1:【消息可靠传输】    

                        3.2: 【消息rewind回传等】

 有关kafka-Storm的接入组件,请参考 【至静】所写的其他kafka有关的博文

对于消息中间件的引入,一方面减少了对减少对External Data Source的重复访问压力,而且通过消息中间件,我们屏蔽了External Data Sourcede 的重复访问压力

总结: 到目前为止,我们的流式系统还不能够在各个 Topology之间拥有Stream与Stream 之间的数据转发。

个人觉得,Storm有必要实现不同Topology之间Stream的共享,这个至少可以在不损失Storm现有功能的前提下,使得Storm在处理实际生产环境下的一些应用场景时更加从容应对。

至于如何在现有Storm的基础上实现这一需求,可能的方式很多。一种简单的方式是通过Zookeeper来集中存储、动态感知Topology之间Stream的“发布-订阅”关系,同时在Storm的消息分发过程中对这种情况加以处理。

以上观点,如有欠缺不足之处,还请指出。

Storm【Storm-MongoDB接口】- 1: 简要介绍
发表于1年前(2014-10-16 14:30) 阅读(323) | 评论(1) 1人收藏此文章, 我要收藏
赞1

摘要 Storm-MongoDB的接口,是用来从MongoDB之中load数据到Storm之中计算,并且通过Storm计算以后,将数据写回到MongoDB

阅读前提:

其一:    您需要对于MongoDB有一个初步的了解。

其二:    您需要对Storm本身有所了解

阅读建议:

            由于整个Storm接口系列包含了围绕Storm实时处理的框架的一系列接口,在一系列的接口文档之中,请对比Storm-hbase接口的博文

整体的Storn接口分为以下的几个class

1:MongoBolt.java

2 : MongoSpout.java

3 : MongoTailableCursorTopology.java

4 : SimpleMongoBolt.java

看代码说话:

1

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package storm.mongo;

import java.util.Map;

import backtype.storm.task.OutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.base.BaseRichBolt;
import backtype.storm.tuple.Tuple;

import com.mongodb.DB;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.MongoException;
import com.mongodb.WriteConcern;

/**
*
* 注意在这里,没有实现批处理的调用,并且只是一个抽象类,对于Mongo的Storm交互做了一次封装
*
* @author Adrian Petrescu apetresc@gmail.com
*
*/
public abstract class MongoBolt extends BaseRichBolt {
private OutputCollector collector;

// MOngDB的DB对象
private DB mongoDB;


    //记录我们的主机,端口,和MongoDB的数据DB民粹
private final String mongoHost;
private final int mongoPort;
private final String mongoDbName;

/**
 * @param mongoHost The host on which Mongo is running.
 * @param mongoPort The port on which Mongo is running.
 * @param mongoDbName The Mongo database containing all collections being
 * written to.
 */
protected MongoBolt(String mongoHost, int mongoPort, String mongoDbName) {
    this.mongoHost = mongoHost;
    this.mongoPort = mongoPort;
    this.mongoDbName = mongoDbName;
}

@Override
public void prepare(
        @SuppressWarnings("rawtypes") Map stormConf, TopologyContext context, OutputCollector collector) {

    this.collector = collector;
    try {

            //prepare方法目前在初始化的过程之中得到了一个Mongo的连接
        this.mongoDB = new MongoClient(mongoHost, mongoPort).getDB(mongoDbName);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

@Override
public void execute(Tuple input) {


        //注意我们在这里还有一个判断,判断当前是否该发射

    if (shouldActOnInput(input)) {
        String collectionName = getMongoCollectionForInput(input);
        DBObject dbObject = getDBObjectForInput(input);
        if (dbObject != null) {
            try {
                mongoDB.getCollection(collectionName).save(dbObject, new WriteConcern(1));
                collector.ack(input);
            } catch (MongoException me) {
                collector.fail(input);
            }
        }
    } else {
        collector.ack(input);
    }
}

/**
 * Decide whether or not this input tuple should trigger a Mongo write.
 *
 * @param input the input tuple under consideration
 * @return {@code true} iff this input tuple should trigger a Mongo write
 */
public abstract boolean shouldActOnInput(Tuple input);

/**
 * Returns the Mongo collection which the input tuple should be written to.
 *
 * @param input the input tuple under consideration
 * @return the Mongo collection which the input tuple should be written to
 */
public abstract String getMongoCollectionForInput(Tuple input);

/**
 * Returns the DBObject to store in Mongo for the specified input tuple.
 * 

 拿到DBObject的一个抽象类


 * @param input the input tuple under consideration
 * @return the DBObject to be written to Mongo
 */
public abstract DBObject getDBObjectForInput(Tuple input);


//注意这里随着计算的终结被关闭了。
@Override
public void cleanup() {
    this.mongoDB.getMongo().close();
}

}

2 :

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package storm.mongo;

import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;

import backtype.storm.spout.SpoutOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichSpout;
import backtype.storm.utils.Utils;

import com.mongodb.BasicDBObject;
import com.mongodb.Bytes;
import com.mongodb.DB;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.MongoException;

/**
* A Spout which consumes documents from a Mongodb tailable cursor.
*
* Subclasses should simply override two methods:
*


  • *
  • {@link #declareOutputFields(OutputFieldsDeclarer) declareOutputFields}
    *
  • {@link #dbObjectToStormTuple(DBObject) dbObjectToStormTuple}, which turns
    * a Mongo document into a Storm tuple matching the declared output fields.
    *

*
**


* WARNING: You can only use tailable cursors on capped collections.
*
* @author Dan Beaulieu danjacob.beaulieu@gmail.com
*
*/

// 在这里,抽象的过程中,依旧保持了第一层的Spout为一个抽象类,MongoSpout为abstract的一个抽象类,子类在继承这// 个类的过程之中实现特定的方法即可
// 这里还有一个类似Cursor的操作。

public abstract class MongoSpout extends BaseRichSpout {

private SpoutOutputCollector collector;

private LinkedBlockingQueue<DBObject> queue;
private final AtomicBoolean opened = new AtomicBoolean(false);

private DB mongoDB;
private final DBObject query;

private final String mongoHost;
private final int mongoPort;
private final String mongoDbName;
private final String mongoCollectionName;


public MongoSpout(String mongoHost, int mongoPort, String mongoDbName, String mongoCollectionName, DBObject query) {

    this.mongoHost = mongoHost;
    this.mongoPort = mongoPort;
    this.mongoDbName = mongoDbName;
    this.mongoCollectionName = mongoCollectionName;
    this.query = query;
}

class TailableCursorThread extends Thread {


    // 内部类 TailableCursorThread线程


    //注意在其中我们使用了LinkedBlockingQueue的对象,有关java高并发的集合类,请参考本ID的【Java集合类型的博文】博文。
    LinkedBlockingQueue<DBObject> queue;
    String mongoCollectionName;
    DB mongoDB;
    DBObject query;

    public TailableCursorThread(LinkedBlockingQueue<DBObject> queue, DB mongoDB, String mongoCollectionName, DBObject query) {

        this.queue = queue;
        this.mongoDB = mongoDB;
        this.mongoCollectionName = mongoCollectionName;
        this.query = query;
    }

    public void run() {

        while(opened.get()) {
            try {
                // create the cursor
                mongoDB.requestStart();
                final DBCursor cursor = mongoDB.getCollection(mongoCollectionName)
                                            .find(query)
                                            .sort(new BasicDBObject("$natural", 1))
                                            .addOption(Bytes.QUERYOPTION_TAILABLE)
                                            .addOption(Bytes.QUERYOPTION_AWAITDATA);
                try {
                    while (opened.get() && cursor.hasNext()) {
                        final DBObject doc = cursor.next();

                        if (doc == null) break;

                        queue.put(doc);
                    }
                } finally {
                    try { 
                        if (cursor != null) cursor.close(); 
                    } catch (final Throwable t) { }
                    try { 
                        mongoDB.requestDone(); 
                        } catch (final Throwable t) { }
                }

                Utils.sleep(500);
            } catch (final MongoException.CursorNotFound cnf) {
                // rethrow only if something went wrong while we expect the cursor to be open.
                if (opened.get()) {
                    throw cnf;
                }
            } catch (InterruptedException e) { break; }
        }
    };
}

@Override
public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {

    this.collector = collector;
    this.queue = new LinkedBlockingQueue<DBObject>(1000);
    try {
        this.mongoDB = new MongoClient(this.mongoHost, this.mongoPort).getDB(this.mongoDbName);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

    TailableCursorThread listener = new TailableCursorThread(this.queue, this.mongoDB, this.mongoCollectionName, this.query);
    this.opened.set(true);
    listener.start();
}

@Override
public void close() {
    this.opened.set(false);
}

@Override
public void nextTuple() {

    DBObject dbo = this.queue.poll();
    if(dbo == null) {
        Utils.sleep(50);
    } else {
        this.collector.emit(dbObjectToStormTuple(dbo));
    }
}

@Override
public void ack(Object msgId) {
    // TODO Auto-generated method stub  
}

@Override
public void fail(Object msgId) {
    // TODO Auto-generated method stub  
}

public abstract List<Object> dbObjectToStormTuple(DBObject message);

}

<script type="text/javascript"> $(function () { $('pre.prettyprint code').each(function () { var lines = $(this).text().split('\n').length; var $numbering = $('<ul/>').addClass('pre-numbering').hide(); $(this).addClass('has-numbering').parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($('<li/>').text(i)); }; $numbering.fadeIn(1700); }); }); </script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值