大数据入门--Flume详解

简介

概述

   Flume本身是由Cloudera公司开发的后来贡献给了Apache的一套针对日志进行收集(collecting)、汇聚(aggregating)和传输(moving)的分布式机制。

                                                  

图-1 Flume图标

Flume本身提供了较为简易的流式结构,使得开发者能够较为简易和方便的搭建Flume的流动模型。

图-2 Flume流动模型图

   目前,Flume提供了两大版本:Flume0.x和Flume1.x版本:

1)Flume0.X:又称之为Flume-og版本,依赖于Zookeeper,结构配置相对复杂,现在在市面上已经不怎么常见了。

2)Flume1.X:又称之为Flume-ng版本,不依赖于Zookeeper,结构配置相对简单,是现在比较常用的版本。

截止到目前为止(2023年7月20日),Flume的最新版本是Flume1.11.0版本。

基本概念

   在Flume中,有两个基本的概念:Event和Agent。

Event

   在Flume中,会将收集到的每一条日志都封装成一个Event对象,所以一个Event就是一条日志。

Event本质上是一个json串,固定的包含两部分:headers和body。即Flume会将收集到的每一条日志封装成一个json,这个json就是一个Event。

{"headers":{},"body":""}

Agent

Agent是Flume流动模型的基本组成部分,固定的包含三个部分:

1)Source:从数据源采集数据(collecting);

2)Channel:将数据进行临时的存储(aggregating);

3)Sink:将数据进行传输(moving)。

流动模型/拓扑结构

   Flume中,通过Agent之间相互组合,可以构成复杂的流动模型,包括:单级流动,多级流动,扇入流动,扇出流动以及复杂流动。

   单级流动(Agent Flow),顾名思义,即只有1个Agent组成,数据经过Source采集,通过Sink写入目的地,经过一次流动即可。

图-3 单级流动

   多级流动(Multi-agent Flow),又叫串联流动,是由2个及以上的Agent串联组成,数据需要流经多个Agent才能写到目的地。

图-4 多级流动

   扇入流动(Consolidation),又叫聚集流动,是将多个Agent的结果汇聚到一个Agent中,最终写入目的地。

图-5 扇入流动

   扇出流动(Multiplexing the flow),又叫复用流动,是将数据分别传输给多个Agent,写入多个目的地。

图-6 扇出流动

   复杂流动(Flexible Flow),按照项目需求,将上述流动模型进行组合,构成的就是复杂流动。

编译和安装

编译

   1)进入源码存放目录,上传或者下载Flume的源码包:

# 进入目录

cd /opt/presoftware/

# 官网下载地址

wget https://archive.apache.org/dist/flume/1.11.0/apache-flume-1.11.0-src.tar.gz

2)解压:

tar -xvf apache-flume-1.11.0-src.tar.gz -C /opt/source/

3)进入Flume源码目录:

cd /opt/source/apache-flume-1.11.0-src/

4)编译:

mvn -X package -Pdist,nativeN,docs -DskipTests -Dtar -Dmaven.skip.test=true -Dmaven.javadoc.skip=true -Denforcer.skip=true

5)编译好的安装包在flume-ng-dist/target目录下。

编译错误处理

如果编译过程中提示缺少了eigenbase,那么上传jar包之后,执行:

mvn install:install-file \

-Dfile=DynamoDBLocal-1.11.86.jar \

-DgroupId=com.amazonaws \

-DartifactId=DynamoDBLocal \

-Dversion=1.11.86 \

-Dpackaging=jar

缺少net.hydromatic:linq4j:jar:0.4:

mvn install:install-file \

-Dfile=linq4j-0.4.jar \

-DgroupId=net.hydromatic \

-DartifactId=linq4j \

-Dversion=0.4 \

-Dpackaging=jar

缺少net.hydromatic:quidem:jar:0.1:

mvn install:install-file \

-Dfile=quidem-0.1.1.jar \

-DgroupId=net.hydromatic \

-DartifactId=quidem \

-Dversion=0.1.1 \

-Dpackaging=jar

缺少org.pentaho:pentaho-aggdesigner-algorithm:jar:5.1.5-jhyde:

mvn install:install-file \

-Dfile=pentaho-aggdesigner-algorithm-5.1.5-jhyde.jar \

-DgroupId=org.pentaho \

-DartifactId=pentaho-aggdesigner-algorithm \

-Dversion=5.1.5-jhyde \

-Dpackaging=jar

安装

   Flume本身是使用Java开发的,所以要求服务器上首先安装了JDK1.8。

   1)进入软件预安装目录,上传或者下载安装包:

# 进入预安装目录

cd /opt/software/

# 官网下载地址

wget --no-check-certificate https://dlcdn.apache.org/flume/1.11.0/apache-flume-1.11.0-bin.tar.gz

   2)解压安装包:

tar -xvf apache-flume-1.11.0-bin.tar.gz -C /opt/software/

   3)进入Flume安装目录:

cd /opt/module/

   4)重命名目录:

mv apache-flume-1.11.0-bin/ flume-1.11.0

   5)配置环境变量:

# 编辑文件

vim /etc/profile.d/myenv.sh

# 在文件中添加

export FLUME_HOME=/opt/module/flume-1.11.0

export PATH=$PATH:$FLUME_HOME/bin

# 保存退出,生效

source /etc/profile.d/flumehome.sh

# 测试

flume-ng version

   6)解决Flume的连接池包和Hadoop的连接池包不一致问题:

# 进入Flume的lib目录

cd flume-1.11.0/lib/

# 删除原来的连接池包

rm -rf guava-11.0.2.jar

# 复制Hadoop的连接池包

cp /opt/module/hadoop-3.2.4/share/hadoop/common/lib/guava-27.0-jre.jar ./

入门案例

   1)进入Flume中,创建数据目录,用于存放格式文件:

# 进入Flume的安装目录

cd /opt/module/flume-1.11.0/

# 创建数据目录

mkdir data

# 进入目录

cd data

   2)创建格式文件:

vim basic.properties

   添加内容:

# 指定Agent的名字

# 指定Source的名字

# 如果要配置多个Source,那么Source之间用空格间隔

a1.sources = s1

# 指定Channel的名字

# 如果要配置多个Channel,那么Channel之间用空格间隔

a1.channels = c1

# 指定Sink的名字

# 如果要配置多个Sink,那么Sink之间用空格间隔

a1.sinks = k1

# 配置Source

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

# 配置Channel

a1.channels.c1.type = memory

# 配置Sink

a1.sinks.k1.type = logger

# 绑定Source和Channel

a1.sources.s1.channels = c1

# 绑定Sink和Channel

a1.sinks.k1.channel = c1

   3)启动Flume:

flume-ng agent --name a1 --conf $FLUME_HOME/conf --conf-file basic.properties -Dflume.root.logger=INFO,console

   上述命令可以简化为:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f basic.properties -Dflume.root.logger=INFO,console

   4)复制窗口,在新窗口中输入:

nc hadoop101 8090

# 回车之后发送数据

   5)停掉Flume之后,会发现在当前的data目录下生成了一个新文件flume.log,而我们输入的数据被封装成了Event形式写入到了这个新文件中。

控制台打印

需要注意的是,从Flume1.10.0版本开始,使用log4j2.x替换log4j1.x,使用log4j2.xml替换了log4j.properties,因此使用Logger Sink不再输出到控制台上,而是将结果输出到了flume.log文件中。如果想要将数据打印到控制台上,操作如下:

   1)进入Flume的配置文件目录:

cd /opt/module/flume-1.11.0/conf/

   2)编辑文件:

vim log4j2.xml

   修改文件内容如下:

<Loggers>

    <Logger name="org.apache.flume.lifecycle" level="info"/>

    <Logger name="org.jboss" level="WARN"/>

    <Logger name="org.apache.avro.ipc.netty.NettyTransceiver" level="WARN"/>

    <Logger name="org.apache.hadoop" level="INFO"/>

    <Logger name="org.apache.hadoop.hive" level="ERROR"/>

    <Root level="INFO">

       <AppenderRef ref="Console"/>

       <AppenderRef ref="LogFile" />

    </Root>

</Loggers>

   3)回到数据目录下:

cd ../data

   4)重新启动Flume,发送数据,会发现数据已经打印到了控制台上。

参数解释

   Flume启动过程中,需要用到多个参数,这多个参数的解释如下:

表-1 参数解释

完整写法

简写

解释

flume-ng

Flume启动命令

agent

表示启动Flume的Agent组件

--name

-n

指定要启动的Agent的名字,和格式文件中Agent的名字对应

--conf

-c

指定Flume的配置文件所在的位置,注意,这儿的配置文件是Flume自己的配置文件,而不是我们自己编写的格式文件/数据文件!默认情况下,Flume的配置文件在Flume安装目录的conf目录下

--conf-file

-f

指定要执行的格式文件的位置

-D

指定其他运行的参数

flume.root.logger

指定日志的打印级别和打印位置,可以指定级别包含INFO,WARN和ERROR,可以指定的打印位置包含console和logfile

Source

AVRO Source

   AVRO Source会监听指定的端口,接收其他节点传过来的被AVRO序列化之后的数据。AVRO Source结合AVRO Sink可以实现更多的流动模型/拓扑结构,例如多级流动、扇入流动、扇出流动等。

   AVRO Source中配置的选项有:

表-2 配置选项

选项

备注

解释

type

required

Source的类型,此处必须是avro

bind

required

要监听的服务器的主机名或者IP

port

required

要监听的端口

channels

required

Source需要绑定的Channel

   案例:

   1)编辑文件:

vim avrosource.properties

   添加如下配置:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置AVRO Source

# 类型必须是avro

a1.sources.s1.type = avro

# 绑定要监听的主机

a1.sources.s1.bind = 0.0.0.0

# 绑定要监听的端口

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   2)启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f avrosource.properties -Dflume.root.logger=INFO,console

   3)在新窗口中,创建一个文件,添加内容。

   4)启动Flume的avro-client组件,将文件进行AVRO序列化之后发送给Avro Source:

flume-ng avro-client -H hadoop101 -p 8090 -F a.txt

   avro-client组件中的参数解释:

表-3 参数解释

完整写法

简写

解释

avro-client

指定启动Flume的avro-client组件

--host

-H

指定数据要发送的主机名或者IP

--port

-p

指定数据要发送的端口

--filename

-F

指定要发送的文件

Exec Source

   Exec Source会运行指定的命令,然后收集这个命令的执行结果。可以利用这个Source来完成部分场景的监控,例如对方是否有返回,文件是否有新增等。

   Exec Source中配置的选项有:

表-4 配置选项

选项

备注

解释

type

required

Source的类型,此处必须是exec

command

required

要执行的命令

channels

required

Source需要绑定的Channel

shell

optional

指定命令的类型

   案例:监听指定的文件,如果文件中新添了数据,那么自动收集新添的数据。

   1)编辑文件:

vim execsource.properties

   添加如下内容:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置Exec Source

# 类型必须是exec

a1.sources.s1.type = exec

# 指定命令

a1.sources.s1.command = tail -F /opt/module/flume-1.11.0/data/a.txt

# 指定Shell类型,用于在 Bash shell 中执行一个命令字符串

a1.sources.s1.shell = /bin/bash -c

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   2)启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f execsource.properties -Dflume.root.logger=INFO,console

   3)新窗口中,向文件中追加数据:

echo 'append' >> /opt/module/flume-1.11.0/data/a.txt

echo 'exec' >> /opt/module/flume-1.11.0/data/a.txt

   回到Flume界面,发现新添加的数据被Flume自动收集了。

Spooling Directory Source

   Spooling Directory Source用于监听指定的目录,如果目录中有新的文件,那么会自动收集新文件中的数据。注意,被收集过的文件会自动的添加一个后缀。

   Spooling Directory Source配置的选项包含:

表-5 配置选项

选项

备注

解释

type

required

Source的类型,此处必须是spooldir

spoolDir

required

要监听的目录

channels

required

Source需要绑定的Channel

fileSuffix

optional

被收集过的文件添加的后缀,默认是.COMPLETED

   案例:监听指定的目录,自动收集新文件的内容。

   1)编辑文件:

vim spoolsource.properties

   添加如下配置:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置Spooling Directory Source

# 类型必须是spooldir

a1.sources.s1.type = spooldir

# 绑定要监听的目录

a1.sources.s1.spoolDir = /opt/module/flumedata

# 指定收集后的文件的后缀

a1.sources.s1.fileSuffix = .testing

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   2)启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f spoolsource.properties -Dflume.root.logger=INFO,console

   3)编辑新文件,添加数据,然后移动到指定的目录中:

# 编辑文件

vim a.txt

# 在文件中添加数据

# 添加之后,保存退出,然后将文件挪到指定目录下

mv a.txt /opt/module/flumedata

   会发现,新添加的文件中的内容被自动收集了。

Taildir Source

   Taildir Source可以用于监听多个文件(一组文件)的变化,如果这一组文件中的某个文件中新添了数据,那么可以自动收集这些新添的数据。不同于之前案例的地方在于,Taildir Source可以监听多个文件中数据的新添,Exec Source是利用命令监听一个文件中数据的新添,Spooling Directory Source是监听目录下文件的新添而不是某一个文件中数据的变化!

   Taildir Source支持断点续传,本身依靠索引文件来记录每一个文件上一次的读取位置。

   需要注意的是,Taildir Source本身不支持Windows系统中使用。

   Taildir Source配置选项包含:

表-6 配置选项

选项

备注

解释

type

required

Source的类型,此处必须是TAILDIR

filegroups

required

给要监听的文件组起名,如果要指定多个文件组,那么彼此之间用空格隔开

filegroups.<filegroupName>

required

指定文件组中要监听的文件,支持正则表达式

channels

required

Source需要绑定的Channel

positionFile

optional

该Source会用json形式记录每一个文件上一次的读取位置(索引文件)。如果不指定,默认是~/.flume/taildir_position.json

   案例:监听所有的log文件,如果log文件中有新的数据,则自动收集。

   1)编辑文件:

vim tailsource.properties

   在文件中添加如下配置:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置Taildir Source

# 类型必须是TAILDIR

a1.sources.s1.type = TAILDIR

# 指定文件组的名字

a1.sources.s1.filegroups = f1

# 指定文件组要监听的目录

a1.sources.s1.filegroups.f1 = /opt/module/flumedata/.*log.*

# 指定索引文件的存放位置

a1.sources.s1.positionFile = /opt/module/flumedata/taildir_position.json

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1 

2)启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f tailsource.properties -Dflume.root.logger=INFO,console

   3) 编辑新文件,添加数据,然后移动到指定的目录中:

# 编辑文件

vim a.log

# 在文件中添加数据

# 添加之后,保存退出,然后将文件挪到指定目录下

mv a.log /opt/module/flumedata

   会发现,新添加的文件中的内容被自动收集了。继续测试:

# 进入flumedata目录:

cd /opt/module/flumedata

# 创建新文件

touch b.log

# 追加数据

echo 'from a' >> a.log

echo 'hi b' >> b.log

   会发现,无论是新文件还是追加到文件中的数据,都会被自动收集。

NetCat Source

   NetCat Source分为NetCat TCP Source和NetCat UDP Source,都是用于监听指定的主机和端口,从该端口接收TCP请求或者UDP请求的数据。

   可以配置的选项:

表-7 配置选项

选项

备注

解释

type

required

Source的类型,如果监听TCP请求,则类型为netcat;如果监听UDP请求,则类型为netcatudp

bind

required

要监听的主机IP或者主机名

port

required

要监听的端口

channels

required

Source需要绑定的Channel

max-line-length

optional

NetCat TCP Source支持,NetCat UDP Source不支持该选项,表示每一次能接收的最大数据量,单位是字节,默认值是512

   如果需要发送TCP请求,使用nc命令;如果需要发送UDP请求,使用nc -u命令。

Sequence Generator Source

   Sequence Generator Source本质上是一个序列产生器,会从0开始依次递增。实际过程中,经常使用这个Souce来测试流动模型是否搭建成功。

   可以配置的选项包含:

表-8 配置选项

选项

备注

解释

type

required

Source的类型,该选项的值必须是seq

channels

required

Source需要绑定的Channel

totalEvents

optional

递增的最大值。默认是Long.MAV_VALUE

   案例配置文件内容如下:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置Sequence Generator Source

# 类型必须是seq

a1.sources.s1.type = seq

# 指定递增的最大范围

a1.sources.s1.totalEvents = 100

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

HTTP Source

   HTTP Source用于监听和接收HTTP请求,但是需要注意的是,该Source只能监听GET请求和POST请求。其中,官网指出,GET请求的监听最好只用于实验环境,所以实际生产环境中只用这个Source来监听POST请求。

   可以配置的选项:

表-9 配置选项

选项

备注

解释

type

required

Source的类型,该选项的值必须是http

port

required

要监听的端口

channels

required

Source需要绑定的Channel

   案例:

1)编辑文件:

vim httpsource.properties

   2)配置文件内容如下:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置Http Source

# 类型必须是http

a1.sources.s1.type = http

# 指定要监听的端口

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   3)启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f httpsource.properties -Dflume.root.logger=INFO,console

   4)发送POST请求:

curl -X POST -d '[{"headers":{"kind":"test","class":"test"},"body":"testing"}]' http://hadoop101:8090

Custom Source

概述

   在Flume中,支持自定义Source。如果Flume没有提供对应的数据源,那么可以通过自定义Source来实现数据的采集过程。

   Flume针对Source提供了顶级接口Source,但是实际过程中,一般不是直接实现Source接口,而是实现其子接口EventDrivenSource或者PollableSource:

   1)EventDrivenSource:事件驱动Source,本身是一个被动型Source,需要自己定义线程来获取数据并封装数据。

   2)PollableSource:拉取Source,本身是一个主动型Source,提供了线程来获取数据,用户只需要考虑怎么封装数据即可。

   除了实现上述接口之外,一般自定义的Source还需要实现Configurable接口,覆盖这个接口中的configure方法,通过这个configure方法来获取格式文件中的属性值。

自定义EventDrivenSource

   1)新建Maven项目,在POM文件中添加如下依赖:

<dependencies>

    <!--Flume核心包-->

    <dependency>

       <groupId>org.apache.flume</groupId>

       <artifactId>flume-ng-core</artifactId>

       <version>1.11.0</version>

    </dependency>

    <!--Flume开发包-->

    <dependency>

       <groupId>org.apache.flume</groupId>

       <artifactId>flume-ng-sdk</artifactId>

       <version>1.11.0</version>

    </dependency>

    <!--Flume配置包-->

    <dependency>

       <groupId>org.apache.flume</groupId>

       <artifactId>flume-ng-configuration</artifactId>

       <version>1.11.0</version>

    </dependency>

</dependencies>

   2)定义类,继承AbstractSource,实现EventDrivenSource和Configurable接口,覆盖其中的configure、start和stop方法:

package com.flume.auth.source;

import org.apache.flume.Context;

import org.apache.flume.EventDrivenSource;

import org.apache.flume.channel.ChannelProcessor;

import org.apache.flume.conf.Configurable;

import org.apache.flume.source.AbstractSource;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

// 模拟Sequence Generator Source

public class AuthDrivenSource extends AbstractSource implements EventDrivenSource, Configurable {

    // 递增的最大范围

    private long max;

    // 递增的起始值

    private long min;

    // 递增步长

    private long step;

    // 线程池

    private ExecutorService es;

    // 获取配置属性

    @Override

    public void configure(Context context) {

        // 获取max的属性值,如果用户没有指定,那么最大递增到Long.MAX_VALUE

        this.max = context.getLong("max", Long.MAX_VALUE);

        // 获取min的属性值,如果用户没有指定,那么从0开始递增

        this.min = context.getLong("min", 0L);

        // 判断起始值是否超过了最大值

        if (min > max) throw new IllegalArgumentException("起始值" + min + "超过了递增范围" + max);

        // 获取step的属性值,如果没有指定,那么默认递增步长为1

        this.step = context.getLong("step", 1L);

        // 判断步长是否合理

        if (step < 1) throw new IllegalArgumentException("递增步长" + step + "不合理!");

    }

    // 启动Source,完成初始化过程

    @Override

    public synchronized void start() {

        // 获取ChannelProcessor

        ChannelProcessor cp = this.getChannelProcessor();

        // 构建线程池

        es = Executors.newCachedThreadPool();

        // 提交任务

        es.submit(new Add(min, max, step, cp));

    }

    // 同步结束Source,用完就关掉,保证平滑关闭Executor,回收资源

    @Override

    public synchronized void stop() {

        if (es != null) es.shutdown();

    }

}

   3)定义递增线程类:

package com.flume.auth.source;

import org.apache.flume.Event;

import org.apache.flume.channel.ChannelProcessor;

import org.apache.flume.event.EventBuilder;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

public class Add implements Runnable {

    private final long min;

    private final long max;

    private final long step;

    private final ChannelProcessor cp;

//这个构造函数,我们可以在创建 Add 类的对象时,将 min、max、step 和 cp 的值传入,并自动初始化对象的成员变量

    public Add(long min, long max, long step, ChannelProcessor cp) {

        this.min = min;

        this.max = max;

        this.step = step;

        this.cp = cp;

    }

    @Override

    public void run() {

        // 循环,从min~max

        for (long i = min; i < max; i += step) {

            // Flume会将收集的数据封装成Event

            // 封装headers,注意键和值的类型必须是String

            Map<String, String> headers = new ConcurrentHashMap<>();

            headers.put("time", String.valueOf(System.currentTimeMillis()));

            // 封装body

            byte[] body = String.valueOf(i).getBytes();

            // 构建Event对象

            Event e = EventBuilder.withBody(body, headers);

            // 通过ChannelProcessor,推送给Channel

            cp.processEvent(e);

        }

    }

}

   4)将定义好的Source打成jar包。

   5)将jar包上传到Flume安装目录的lib目录下:

# 进入Flume安装目录的lib目录下:

cd /opt/module/flume-1.11.0/lib/

# 选择jar包,上传

rz

   6)回到数据目录,编辑配置文件:

# 回到数据目录

cd ../data/

# 编辑文件

vim authdrivensource.properties

   在文件中添加:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置自定义Source

# 类型必须是类的全路径名

a1.sources.s1.type = chapter1.AuthDrivenSource

# 指定递增起始值

a1.sources.s1.min = 0

# 指定递增的最大范围

a1.sources.s1.max = 100

# 指定递增的步长

a1.sources.s1.step = 5

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   7)启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f authdrivensource.properties -Dflume.root.logger=INFO,console

自定义PollableSource

   1) 定义类,继承AbstractSource,实现PollableSource和Configurable接口,覆盖其中的configure,process,getBackOffSleepIncrement和getMaxBackOffSleepInterval方法。

package com.flume.auth.source;

import org.apache.flume.Context;

import org.apache.flume.Event;

import org.apache.flume.PollableSource;

import org.apache.flume.channel.ChannelProcessor;

import org.apache.flume.conf.Configurable;

import org.apache.flume.event.EventBuilder;

import org.apache.flume.source.AbstractSource;

import java.util.HashMap;

import java.util.Map;

public class AuthPollableSource extends AbstractSource implements PollableSource, Configurable {

    // 起始值

    private long min;

    // 最大范围

    private long max;

    // 递增步长

    private long step;

    // 获取配置

    @Override

    public void configure(Context context) {

        // 获取起始值,如果没有指定,那么就从0开始递增

        min = context.getLong("min", 0L);

        // 获取最大值,如果没有指定,那么就递增到Long.MAX_VALUE

        max = context.getLong("max", Long.MAX_VALUE);

        // 判断起始值是否大于最大值

        if (min > max) throw new IllegalArgumentException("起始值" + min + "超过了递增范围" + max);

        // 获取步长,如果没有指定,那么步长为1

        step = context.getLong("step", 1L);

        // 判断步长

        if (step < 1) throw new IllegalArgumentException("递增步长" + step + "不合理!");

    }

    // 封装处理数据:因为是多线程调用process方法,所以会是死循环,而EventDrivenSource中有stop方法,所以会自动停止

    @Override

    public Status process() {

        // 获取通道处理器

        ChannelProcessor cp = this.getChannelProcessor();

        // 遍历,递增

        for (long i = min; i < max; i += step) {

            // 封装headers

            Map<String, String> headers = new HashMap<>();

            headers.put("time", String.valueOf(System.currentTimeMillis()));

            // 封装body

            byte[] body = String.valueOf(i).getBytes();

            // 封装Event

            Event e = EventBuilder.withBody(body, headers);

            // 交给ChannelProcessor推送给通道

            cp.processEvent(e);

        }

        return Status.READY;

    }

    // PollableSource是通过PollableRunner线程池来启动线程获取数据

    // 而如果没有数据获取,那么PollableSource会暂时休眠

// 该方法是用于控制休眠的时间增量

//每次重新尝试时,睡眠时间都会增加 1000 毫秒

    @Override

    public long getBackOffSleepIncrement() {

        return 1000;

    }

//重新尝试操作的睡眠时间不会超过 10000 毫秒

    @Override

    public long getMaxBackOffSleepInterval() {

        return 10000;

    }

}

   2)将定义好的Source打成jar包。

   3)将jar包传到Flume安装目录的lib目录下:

# 进入Flume的lib目录下

cd /opt/software/flume-1.11.0/lib/

# 选择对应的jar包上传

rz

   4)回到数据目录下,编辑配置文件:

# 回到数据目录下

cd ../data/

# 编辑文件

vim authpollablesource.properties

   在文件中添加:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置自定义Source

# 类型必须是类的全路径名

a1.sources.s1.type = com.flume.auth.source.AuthPollableSource

# 指定递增起始值

a1.sources.s1.min = 0

# 指定递增的最大范围

a1.sources.s1.max = 100

# 指定递增的步长

a1.sources.s1.step = 5

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   5)启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f authpollablesource.properties -Dflume.root.logger=INFO,console

Channel

Memory Channel

   Memory Channel是将收集来的数据临时存储到内存队列中,如果不指定,那么该队列默认大小是100,即最多允许在队列中存储100条数据。如果队列被占满,那么后来的数据就会被阻塞(即Source收集到的数据就无法放入队列中),直到队列中有位置被空出。实际过程中,这个值一般会调大,一般会调节为10W~30W,如果数据量较大,那么也可以考虑调节为50W。

   Memory Channel可以批量接收Source发来的数据,也可以批量的将数据发送给Sink,而默认情况下,每一批数据是100条。实际过程中,一般会将这个值调节为1000~3000,如果Channel的容量为50W,那么这个值可以考虑调节为5000。

   需要注意的是,Memory Channel是将数据临时存储在内存中,所以是不可靠的,但是数据的读写速度相对较快,因此适用于要求速度但是不要求可靠性的场景。

   Memory Channel可配置的选项如下:

表-10 配置选项

选项

备注

解释

type

required

Channel的类型,此处必须是memory

capacity

optional

队列的容量,默认为100

transactionCapacity

optional

指定命令的类型,最好指定这个属性

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

# 配置Memory Channel

# 类型必须是memory

a1.channels.c1.type = memory

# 指定队列的容量

a1.channels.c1.capacity = 10000

# 指定批容量

a1.channels.c1.transactionCapacity = 1000

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

  File Channel

   File Channel是将数据临时存储到本地磁盘上,因此数据不会丢失,但是读写速度相对较慢,适用于要求可靠性但是不要求速度的场景。

   在File Channel中,如果不指定,那么默认情况下File Channel会将数据临时存储到~/.flume/file-channel/data目录下。例如是tom用户登录,那么就存储到/home/tom/.flume/file-channel/data目录下;bob用户登录那么就存储到/home/bob/.flume/file-channel/data目录下;如果是root用户登录,那么就存储到/root/.flume/file-channel/data目录下。

   为了防止File Channel占用磁盘过多,所以在默认情况下,最多允许File Channel在磁盘上存储100W条数据,或者是2146435071B(大约2GB)的数据。

   File Channel可配置的选项如下:

表-11 配置选项

选项

备注

解释

type

required

Channel的类型,此处必须是file

checkpointDir

optional

检查点的存储位置,默认是~/.flume/file-channel/checkpoint

dataDirs

optional

临时存储数据的目录,如果指定了多个目录,目录之间用逗号间隔。默认是~/.flume/file-channel/data

transactionCapacity

optional

批容量的大小,默认是10000

maxFileSize

optional

临时文件的大小,默认是2146435071,单位是字节

capacity

optional

临时文件的容量,即队列的容量,默认是1000000

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

# 配置File Channel

# 类型必须是file

a1.channels.c1.type = file

# 指定临时文件的存储位置

a1.channels.c1.dataDirs = /opt/module/flumedata/data

# 指定检查点文件的存储位置

a1.channels.c1.checkpointDir = /opt/module/flumedata/checkpoint

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

其他Channel

JDBC Channel

   JDBC Channel是将收集到的数据临时存储到数据库中,因此理论上而言,JDBC Channel的效率略高于File channel但是低于Memory Channel。

   到目前为止,JDBC Channel只支持Derby数据库,而基于Derby数据库的特性(微型数据库,最多能存储几百条数据;单连接,只允许一个用户连接操作),所以实际过程中很少使用这个数据库,因此实际生产过程中,几乎弃用JDBC Channel。

Spillable Memory Channel

   Spillable Memory Channel可以看作是对Memory Channel的一个扩展。在接收到数据的时候,Spillable Memory Channel会先试图将数据临时存储到内存队列中,不同于Memory Channel的地方在于,如果内存队列被占满,那么Spillable Memory Channel不会阻塞,而是会将数据临时存储到本地磁盘上。所以,Spillable Memory Channel可以看作是Memory Channel和File Channel的结合体。

   但是到目前为止,Spillable Memory Channel一直处于试验阶段,不推荐在生产环境中使用。

Sink

Logger Sink

   Logger Sink是将收集到的数据以日志形式打印。需要注意的是,从Flume1.10.0版本开始,使用log4j2.x替换log4j1.x,使用log4j2.xml替换了log4j.properties,因此使用Logger Sink不再输出到控制台上,而是将结果输出到了flume.log文件中。

   在打印的时候,为了防止过多的数据占满屏幕,所以要求数据body的部分不能超过16个字节,超过16个字节的部分不打印。另外,Logger Sink对中文打印支持的不好。

   Logger Sink可以配置的选项有:

表-12 配置选项

选项

备注

解释

type

required

Sink的类型,此处必须是logger

channel

required

Sink要绑定的Channel

maxBytesToLog

optional

body部分打印的字节个数,默认是16,单位是字节

案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

# 配置Logger Sink

# 类型必须是logger

a1.sinks.k1.type = logger

# 指定可打印的字节个数

a1.sinks.k1.maxBytesToLog = 20

# 绑定Source和Channel

a1.sources.s1.channels = c1

# 绑定Sink和Channel

a1.sinks.k1.channel = c1

HDFS Sink

   HDFS Sink,顾名思义,是将收集到的数据写到HDFS中。由于HDFS是以文件形式来存储数据,所以在往HDFS上写的时候,需要指定文件类型。HDFS Sink支持三种文件类型:DataStream(文本类型),SequenceFile(序列类型)以及CompressedStream(压缩类型)。如果不指定,那么HDFS Sink默认使用的是序列类型。

   同样,HDFS Sink在写数据的时候,数据对应的存储文件会定时的滚动。如果不指定,那么默认是每隔30s滚动一次,生成一个小文件,所以此时会生成大量的小文件,因此在实际过程中一般需要调节这个属性。

   HDFS Sink可以配置的选项有:

表-13 配置选项

选项

备注

解释

type

required

Sink的类型,此处必须是hdfs

hdfs.path

required

文件在HDFS上的存储位置

channel

required

Sink要绑定的Channel

hdfs.filePrefix

optional

文件的前缀,默认是FlumeData

hdfs.fileSuffix

optional

文件的后缀,默认文件没有后缀

hdfs.rollInterval

optional

文件的滚动间隔时间,默认是30,单位是秒。如果指定为0,则表示不滚动

hdfs.rollSize

optional

文件的滚动大小,默认是1024,单位是字节。如果指定为0,则表示不滚动

hdfs.rollCount

optional

文件的滚动条数,默认是10。如果指定为0,则表示不滚动

hdfs.fileType

optional

文件在HDFS上的存储类型,支持DataStream,SequenceFile以及CompressedStream三种类型。默认是SequenceFile类型

hdfs.codeC

optional

如果指定了文件类型为CompressedStream,那么需要指定压缩编码。支持gzip,bzip2,lzo,lzop和snappy共五种编码格式

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 类型必须是spooldir

a1.sources.s1.type = spooldir

# 绑定要监听的目录

a1.sources.s1.spoolDir = /opt/module/flumedata

# 指定收集后的文件的后缀

a1.sources.s1.fileSuffix = .testing

a1.channels.c1.type = memory

# 配置HDFS Sink

# 类型必须是hdfs

a1.sinks.k1.type = hdfs

# 指定文件在HDFS上的存储位置

a1.sinks.k1.hdfs.path = hdfs://hadoop101:8020/flumedata/

# 指定文件前缀

a1.sinks.k1.hdfs.filePrefix = test

# 指定文件后缀

a1.sinks.k1.hdfs.fileSuffix = .txt

# 指定文件滚动间隔时间

a1.sinks.k1.hdfs.rollInterval = 3600

# 指定文件滚动大小

a1.sinks.k1.hdfs.rollSize = 134217728

# 指定文件滚动条数

a1.sinks.k1.hdfs.rollCount = 1000000

# 指定文件类型

a1.sinks.k1.hdfs.fileType = DataStream

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   启动Flume:

flume-ng agent --name a1 --conf $FLUME_HOME/conf --conf-file hdfs.properties -Dflume.root.logger=INFO,console

File Roll Sink

   File Roll Sink是将收集到的数据写到磁盘的指定位置上。同HDFS Sink一样,File Roll Sink在往磁盘上写的时候,也有一个滚动时间,默认同样是30s间隔,因此在磁盘上同样会形成大量的小文件。

   File Roll Sink可以配置的选项有:

表-14 配置选项

选项

备注

解释

type

required

Sink的类型,此处必须是file_roll

sink.directory

required

文件在磁盘上的存储位置

channel

required

Sink要绑定的Channel

sink.rollInterval

optional

文件的滚动间隔时间。默认是30,单位是秒

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

# 配置File Roll Sink

# 类型必须是file_roll

a1.sinks.k1.type = file_roll

# 指定文件在磁盘上的存储位置

a1.sinks.k1.sink.directory = /opt/flumedata

# 指定文件的滚动间隔时间

a1.sinks.k1.sink.rollInterval = 3600

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

AVRO Sink

概述

   AVRO Sink会将数据利用AVRO序列化之后再写出到指定节点的指定端口上,结合之前学习的AVRO Source可以是现实数据的多级、扇入、扇出等流动效果。

   AVRO Sink可以配置的选项有:

表-15 配置选项

选项

备注

解释

type

required

Sink的类型,此处必须是avro

hostname

required

要发送的主机

port

required

要发送的端口

多级流动

   第一个节点的格式文件内容:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

# 类型是avro

a1.sinks.k1.type = avro

# 要发送的节点的主机名或者ip

a1.sinks.k1.hostname = hadoop102

# 要发送的端口

a1.sinks.k1.port = 8090

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   第二个节点的格式文件内容:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置为AVRO Source

a1.sources.s1.type = avro

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

# 类型是avro

a1.sinks.k1.type = avro

# 要发送的节点的主机名或者ip

a1.sinks.k1.hostname = hadoop103

# 要发送的端口

a1.sinks.k1.port = 8090

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   第三个节点的格式文件内容如下:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

# 配置为AVRO Source

a1.sources.s1.type = avro

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

# 类型是logger

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   注意,启动的时候,是先启动接收节点,在启动发送节点,所以启动顺序是:hadoop103,hadoop102,hadoop101。

   启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f multilevel.properties -Dflume.root.logger=INFO,console

通过在hadoop101新启动窗口输入数据:

nc hadoop101 8090

扇入流动

   第一个和第二个节点的格式文件内容如下:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

a1.sinks.k1.type = avro

a1.sinks.k1.hostname = hadoop103

a1.sinks.k1.port = 8090

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   第三个节点的格式文件内容如下:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = avro

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

  

启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f fanin.properties -Dflume.root.logger=INFO,console

通过在hadoop101、hadoop102新启动窗口输入数据:

nc hadoop101 8090

扇出流动

   第一个节点的格式文件内容如下:

a1.sources = s1

a1.channels = c1 c2

a1.sinks = k1 k2

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

a1.channels.c2.type = memory

a1.sinks.k1.type = avro

a1.sinks.k1.hostname = hadoop102

a1.sinks.k1.port = 8090

a1.sinks.k2.type = avro

a1.sinks.k2.hostname = hadoop103

a1.sinks.k2.port = 8090

a1.sources.s1.channels = c1 c2

a1.sinks.k1.channel = c1

a1.sinks.k2.channel = c2

   第二个和第三个节点的格式文件内容如下:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = avro

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f fanout.properties -Dflume.root.logger=INFO,console

通过在hadoop101新启动窗口输入数据:

nc hadoop101 8090

Custom Sink

   如果写入的目的地在Flume中找不到对应的Sink,那么Flume同样支持自定义Sink。在Flume中,Sink的顶级接口就是Sink,而考虑到还需要获取配置,所以一般还需要实现Configurable接口。

   1)定义类,继承AbstractSink,实现Sink接口和Configurable接口,覆盖其中的configure,start,process和stop方法。

package com.flume.sink;

import org.apache.flume.*;

import org.apache.flume.conf.Configurable;

import org.apache.flume.sink.AbstractSink;

import java.io.FileNotFoundException;

import java.io.PrintStream;

import java.util.Map;

public class AuthSink extends AbstractSink implements Sink, Configurable {

    // 文件的存储路径

    private String path;

    // 输出流

    private PrintStream out;

    @Override

    public void configure(Context context) {

        // 获取指定路径,如果没有指定,那么默认放在/flume_data目录下

        path = context.getString("path", "/flume_data");

        // 判断路径是否合法

        if (path.equals("")) throw new IllegalArgumentException("路径不合法!");

        else if (!path.startsWith("/")) path = "/" + path;

    }

    // 获取输出流,用于写入数据

    @Override

    public synchronized void start() {

        // 创建输入流

        try {

            out = new PrintStream(path + "/" + System.currentTimeMillis());

        } catch (FileNotFoundException e) {

            throw new RuntimeException(e);

        }

    }

    @Override

    public Status process() {

        // 获取Channel对象

        Channel c = this.getChannel();

        // 获取事务

        Transaction t = c.getTransaction();

        // 开启事务

        t.begin();

        // 获取数据

        Event e;

        try {

            while ((e = c.take()) != null) {

                // 写出headers部分

                out.println("headers:");

                Map<String, String> headers = e.getHeaders();

                for (Map.Entry<String, String> entry : headers.entrySet()) {

                    out.println("\t" + entry.getKey() + "=" + entry.getValue());

                }

                // 写出body部分

                out.println("body:");

                out.println("\t" + new String(e.getBody()));

            }

            // 提交事务

            t.commit();

            // 返回状态

            return Status.READY;

        } catch (Exception exception) {

            // 事务回滚

            t.rollback();

            return Status.BACKOFF;

        } finally {

            // 无论如何,都要关闭事务

            t.close();

        }

    }

    // 关流

    @Override

    public synchronized void stop() {

        if (out != null) out.close();

    }

}

   2)将定义好的Sink打成jar包。

   3)将jar包上传到Flume安装目录的lib目录下:

# 进入Flume安装目录的lib目录下

cd /opt/module/flume-1.11.0/lib/

# 选择jar包,上传

rz

   4)回到数据目录下,编写格式文件:

# 回到数据目录

cd ../data

# 编辑文件

vim authsink.properties

   在文件中添加如下内容:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = http

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

# 配置自定义Sink

# 类型必须是类的全路径名

a1.sinks.k1.type = chapter1.AuthSink

# 指定存储路径

a1.sinks.k1.path = /opt/module/flumedata

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   5)启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f authsink.properties -Dflume.root.logger=INFO,console

   6)发送HTTP请求:

curl -X POST -d '[{"headers":{"class":"big data","kind":"test"},"body":"hello custom source"}]' http://hadoop101:8090

Channel Selector

概述

   Channel Selector本身是Source的子组件,决定了将数据的分发方式以及数据分发给哪一个Channel。

   Channel Selector提供了三种模式:

   1)replicating:复制模式。节点收入数据之后,会将数据复制之后发送给每一个节点,所以此时每一个节点收到的数据都是完全相同的。

   2)multiplexing:路由/多路复用模式。节点收入数据之后,会根据数据中headers中的指定字段的值来决定将数据分发给哪一个Channel,所以此时各个节点的数据是不同的。

   3)load_balancing:负载均衡模式。这是Flume1.10.0版本开始提供的一种新的分发模式。提供了两种均衡方式:round_robin(轮询)模式和random(随机)模式。默认是round_robin模式。需要注意的是,这种模式并不好用!

   在Channel Selector中,如果不指定,那么默认使用的是replicating模式。

multiplexing模式

   multiplexing模式本身是基于扇出结构来实现,根据数据headers中指定字段的指定值来决定将数据分配给哪一个Channel,从而实现数据的分类/分组。

   multiplexing模式需要配置的选项:

表-16 配置选项

选项

备注

解释

selector.type

required

Selector的类型,此处必须是multiplexing

selector.header

required

要监听的字段

selector.mapping.*

required

字段对应的值

selector.default

optional

如果没有对应的值,那么要分发到哪一个channel上

   案例:

a1.sources = s1

a1.channels = c1 c2

a1.sinks = k1 k2

a1.sources.s1.type = http

a1.sources.s1.port = 8090

# Selector的类型必须是multiplexing

a1.sources.s1.selector.type = multiplexing

# 要监听的字段

a1.sources.s1.selector.header = kind

# 字段的值

a1.sources.s1.selector.mapping.video = c1

a1.sources.s1.selector.mapping.music = c2

# 如果值不对应,那么数据分发给哪一个通道

a1.sources.s1.selector.default = c2

a1.channels.c1.type = memory

a1.channels.c2.type = memory

a1.sinks.k1.type = avro

a1.sinks.k1.hostname = hadoop102

a1.sinks.k1.port = 8090

a1.sinks.k2.type = avro

a1.sinks.k2.hostname = hadoop103

a1.sinks.k2.port = 8090

a1.sources.s1.channels = c1 c2

a1.sinks.k1.channel = c1

a1.sinks.k2.channel = c2

load_balancing模式

   load_balancing模式是Flume1.10.0开始提供的一种新模式,能够实现数据在Channel之间的负载均衡。目前官网提供了两种均衡模式:round_robin(轮询)和random(随机)模式。需要注意的是,这种模式并不好用!(官网提供了这种模式,但是没有在lib包中集成这种方式!!!)

   load_balancing配置的选项包含:

表-17 配置选项

选项

备注

解释

selector.type

required

Selector的类型,此处必须是load_balancing

selector.policy

optional

均衡策略,默认是round_robin,支持round_robin和random两种模式

   案例:

a1.sources = s1

a1.channels = c1 c2

a1.sinks = k1 k2

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

# Selector的类型必须是load_balancing

a1.sources.s1.selector.type = load_balancing

# 指定均衡策略

a1.sources.s1.selector.policy = round_robin

a1.channels.c1.type = memory

a1.channels.c2.type = memory

a1.sinks.k1.type = avro

a1.sinks.k1.hostname = hadoop102

a1.sinks.k1.port = 8090

a1.sinks.k2.type = avro

a1.sinks.k2.hostname = hadoop103

a1.sinks.k2.port = 8090

a1.sources.s1.channels = c1 c2

a1.sinks.k1.channel = c1

a1.sinks.k2.channel = c2

Sink Processor

概述

   Sink Processor是将一个或者多个Sink绑定到一个组中,针对一个组中的Sink来进行整体的操作。目前官网支持三种模式:

   1)default:默认模式。该模式下,一个Sink就是一个Sink组,有几个Sink就对应了几个Sink组。

   2)failover:崩溃恢复模式。该模式下,要求将多个Sink绑定到一个组中,给这个组中的每一个Sink指定优先级,数据优先发送给高优先级的Sink;如果高优先级的Sink宕机,那么才会发送给低优先级的Sink。

# 定义了一个名为g1的sink group

a1.sinkgroups = g1

# sinkgroup g1 中有两个sinks,分别为 k1 和 k2

a1.sinkgroups.g1.sinks = k1 k2

# 使用了failover类型的processor

a1.sinkgroups.g1.processor.type = failover

# 配置定义了两个sink的优先级。

# k1和k2是唯一的标识符,可以根据实际情况进行命名。在failover模式下,优先级较高的sink会被优先选择。

a1.sinkgroups.g1.processor.priority.k1 = 5

a1.sinkgroups.g1.processor.priority.k2 = 10

# 定义了在切换sink时的最大延迟。如果一个sink在切换时超过了这个延迟,将被视为不可用,并尝试切换到下一个可用的sink。

a1.sinkgroups.g1.processor.maxpenalty = 10000

   3)load_balancing:负载均衡模式。该模式同样要求将多个Sink绑定到一个组中,数据在组中进行均衡。Flume提供了两种均衡方式:round_robin和random。同样需要注意的是,Flume原生提供的load_balancing模式同样不好用!

failover模式

  failover(崩溃恢复)模式,在使用的时候要求将多个Sink绑定到一个组中,然后给每一个Sink指定优先级,数据优先发送给高优先级的Sink;如果高优先级的Sink宕机,那么才会将数据发送给低优先级的Sink。如果同一个组中的Sink没有指定优先级,那么就按照绑定顺序发送,即数据会优先发送给最先绑定的Sink。

   failover模式可以配置的选项有:

表-18 配置选项

选项

备注

解释

sinks

required

组中要绑定的Sink,Sink之间用空格间隔

processor.type

required

Processor的类型,此处必须是failover

processor.priority.<sinkName>

required

Sink的优先级

processor.maxpenalty

optional

崩溃恢复的时间,默认是30000,单位是毫秒

   案例:

a1.sources = s1

a1.channels = c1 c2

a1.sinks = k1 k2

# 给Sinkgroup起名

a1.sinkgroups = g1

# 绑定Sink

a1.sinkgroups.g1.sinks = k1 k2

# 指定Sinkgroup的类型

a1.sinkgroups.g1.processor.type = failover

# 给每一个Sink指定优先级

a1.sinkgroups.g1.processor.priority.k1 = 5

a1.sinkgroups.g1.processor.priority.k2 = 2

# 定义了在切换sink时的最大延迟。如果一个sink在切换时超过了这个延迟,将被视为不可用,并尝试切换到下一个可用的sink

a1.sinkgroups.g1.processor.maxpenalty = 10000

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

a1.channels.c2.type = memory

a1.sinks.k1.type = avro

a1.sinks.k1.hostname = hadoop102

a1.sinks.k1.port = 8090

a1.sinks.k2.type = avro

a1.sinks.k2.hostname = hadoop103

a1.sinks.k2.port = 8090

a1.sources.s1.channels = c1 c2

a1.sinks.k1.channel = c1

a1.sinks.k2.channel = c2

load_balancing模式

   load_balancing(负载均衡)模式,该模式同样要求将多个Sink绑定到一个组中,数据在组中进行均衡。Flume提供了两种均衡方式:round_robin和random。同样需要注意的是,Flume原生提供的load_balancing模式同样不好用!

   load_balancing模式可以配置的选项有:

表-19 配置选项

选项

备注

解释

sinks

required

组中要绑定的Sink,Sink之间用空格间隔

processor.type

required

Processor的类型,此处必须是load_balance

processor.selector

optional

负载均衡模式,支持round_robin和random,默认是round_robin

   案例:

a1.sources = s1

a1.channels = c1 c2

a1.sinks = k1 k2

# 给Sinkgroup起名

a1.sinkgroups = g1

# 绑定Sink

a1.sinkgroups.g1.sinks = k1 k2

# 指定Sinkgroup的类型

a1.sinkgroups.g1.processor.type = load_balance

# 指定均衡类型

a1.sinkgroups.g1.processor.selector = round_robin

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.channels.c1.type = memory

a1.channels.c2.type = memory

a1.sinks.k1.type = avro

a1.sinks.k1.hostname = hadoop102

a1.sinks.k1.port = 8090

a1.sinks.k2.type = avro

a1.sinks.k2.hostname = hadoop103

a1.sinks.k2.port = 8090

a1.sources.s1.channels = c1 c2

a1.sinks.k1.channel = c1

a1.sinks.k2.channel = c2

Interceptor

概述

   Interceptor(拦截器)本身是Source的子组件之一,可以对数据进行拦截、过滤、替换等操作。不同于Selector的地方在于,一个Source上可以配置多个拦截器,构成拦截器链。需要注意的是,后一个拦截器不能和前一个拦截器相悖!

Timestamp Interceptor

   时间戳拦截器,本质上不是拦截数据,而是在数据的headers字段中添加一个时间戳,可以用于标记数据被收集的时间。

   Timestamp Interceptor可以配置的选项有:

表-20 配置选项

选项

备注

解释

type

required

拦截器类型,此处必须是timestamp

headerName

optional

在headers中添加的字段值,默认是timestamp

preserveExisting

optional

如果时间戳已经存在,是否替换。默认是false

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

# 给Interceptor起名

# 如果需要配置多个拦截器,那么拦截器之间用空格隔开

a1.sources.s1.interceptors = i1

# 配置Timestamp Interceptor

a1.sources.s1.interceptors.i1.type = timestamp

# 指定headers字段的名称

a1.sources.s1.interceptors.i1.headerName = time

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   结合之前学习的HDFS Sink,可以实现数据的按小时/天/月/年存放:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.sources.s1.interceptors = i1

a1.sources.s1.interceptors.i1.type = timestamp

a1.channels.c1.type = memory

a1.sinks.k1.type = hdfs

a1.sinks.k1.hdfs.path = hdfs://hadoop101:8020/flumedata/reportDate=%Y-%m-%d

a1.sinks.k1.hdfs.filePrefix = test

a1.sinks.k1.hdfs.fileSuffix = .txt

a1.sinks.k1.hdfs.rollInterval = 3600

a1.sinks.k1.hdfs.rollSize = 134217728

a1.sinks.k1.hdfs.rollCount = 1000000

a1.sinks.k1.fileType = DataStream

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   符号解析如下:

表-21 符号解析

符号

解释

%t

毫秒

%a

星期简写,例如Mon,Tue等

%A

星期全称,例如Monday,Tuesday等

%b

月份简称,例如Jan,Feb等

%B

月份全称,例如January,February等

%c

当前日期和时间,例如Mon Jul 24 17:02:05 2023

%d

月份,例如01,02,03等

%e

无填充的月份,例如1,2,3等

%D

日期,等价于%m/%d/%y

%H

小时,00~23

%I

小时,0~12

%j

一年中的第几天,1~366

%k

无填充的小时,0~23

%m

月份,01~12

%n

无填充的月份,1~12

%M

分钟,00~59

%p

am或者pm

%s

从1970-01-01 00:00:00到现在的秒数

%S

秒,00~59

%y

两位数的年,00~99

%Y

四位数的年,例如2023

Host Interceptor

   主机拦截器,本质上不是拦截数据,而是在数据的headers中添加一个host字段,可以用于标记数据来源(被收集)的主机。

   Host Interceptor可以配置的选项有:

表-22 配置选项

选项

备注

解释

type

required

拦截器类型,此处必须是host

preserveExisting

optional

如果host字段已经存在,是否替换。默认是false

useIP

optional

是否使用IP。如果为true,则显示为IP;如果为false,则显示为主机名。默认为true

hostHeader

optional

显示的字段,默认是host

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.sources.s1.interceptors = i1

# 配置Host Interceptor

a1.sources.s1.interceptors.i1.type = host

# 是否使用IP

a1.sources.s1.interceptors.i1.useIP = false

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

Static Interceptor

   静态拦截器,本质上不是拦截器,而是在数据的headers中添加一个指定的字段。

Static Interceptor可以配置的选项有:

表-23 配置选项

选项

备注

解释

type

required

拦截器类型,此处必须是static

preserveExisting

optional

如果指定字段已经存在,是否替换。默认是false

key

optional

指定的键。默认是key

value

optional

指定的值,默认是value

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.sources.s1.interceptors = i1

# 配置Static Interceptor

a1.sources.s1.interceptors.i1.type = static

# 指定键

a1.sources.s1.interceptors.i1.key = kind

# 指定值

a1.sources.s1.interceptors.i1.value = test

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

UUID Interceptor

   UUID拦截器,本质上也不是一个拦截器,而是在数据的headers中添加一个id字段,可以用于标记数据的唯一性。

   UUID Interceptor可以配置的选项包含:

表-24 配置选项

选项

备注

解释

type

required

拦截器类型,此处必须是org.apache.flume.sink.solr.morphline.UUIDInterceptor$Builder

headerName

optional

headers中添加的字段名,默认是id

preserveExisting

optional

如果headers中已经存在id字段,是否替换。默认是true

prefix

optional

在生成的id之前添加前缀

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.sources.s1.interceptors = i1

# 配置UUID Interceptor

a1.sources.s1.interceptors.i1.type = org.apache.flume.sink.solr.morphline.UUIDInterceptor$Builder

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

Search And Replace Interceptor

   搜索和替换拦截器,在使用的时候需要指定正则表达式,会根据正则表达式指定的规则,对Event中body部分的数据进行替换。注意,只替换body部分的数据,而不会影响headers中的数据。正则表达式的使用规则和Java中正则表达式的规则是一致的。

   Search And Replace Interceptor中可以配置的选项包含:

表-25 配置选项

选项

备注

解释

type

required

拦截器类型,此处必须是search_replace

searchPattern

optional

搜索的正则表达式形式

replaceString

optional

替换的形式

charset

optional

body部分的字符集编码,默认是UTF-8

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.sources.s1.interceptors = i1

# 配置Search And Replace Interceptor

a1.sources.s1.interceptors.i1.type = search_replace

# 指定正则表达式

a1.sources.s1.interceptors.i1.searchPattern = [a-z]

# 指定替换后的形式

a1.sources.s1.interceptors.i1.replaceString = *

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

Regex Filtering Interceptor

   正则过滤拦截器,在使用的时候需要指定一个正则表达式,然后根据属性excludeEvents的值来确定过滤方式。如果excludeEvents的值为true,则表示过滤掉符合正则表达式形式的数据,其他数据留下来;如果excludeEvents的值为false,则表示过滤掉不符合正则表达式形式的数据,符合形式的数据留下来。默认情况下,excludeEvents的值为false。

   Regex Filtering Interceptor中可以配置的选项包含:

表-26 配置选项

选项

备注

解释

type

required

拦截器类型,此处必须是regex_filter

regex

optional

正则表达式

excludeEvents

optional

替换规则,默认为false

   案例:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.sources.s1.interceptors = i1

# 配置Regex Filtering Interceptor

a1.sources.s1.interceptors.i1.type = regex_filter

# 指定正则表达式

a1.sources.s1.interceptors.i1.regex = .*[0-9].*

# 指定替换规则

a1.sources.s1.interceptors.i1.excludeEvents = true

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

Custom Interceptor

   在实际开发过程中,如果Flume提供的拦截器无法满足业务需求,那么Flume同样支持自定义拦截器。但是不同于其他组件的地方在于,Flume中的拦截器在启动的时候,是通过内部类Builder来启动,所以拦截器中需要覆盖其内部类。

   步骤:

   1)定义类实现Interceptor接口,覆盖其中的intercept方法,其他方法可以忽略掉;同时需要定义内部类实现Interceptor.Builder接口,覆盖其中的build方法;如果需要获取配置,那么可以配置configure方法。

package com.flume.interceptor;

import org.apache.flume.Context;

import org.apache.flume.Event;

import org.apache.flume.interceptor.Interceptor;

import java.util.ArrayList;

import java.util.List;

import java.util.Map;

public class AuthInterceptor implements Interceptor {

    @Override

    public void initialize() {

    }

    @Override

    public Event intercept(Event event) {

        // 获取headers部分

        Map<String, String> headers = event.getHeaders();

        // 判断headers是否包含time或者timestamp字段

        if (headers.containsKey("time") || headers.containsKey("timestamp")) return event;

        // 如果没有,则添加当前的时间戳

        headers.put("timestamp", String.valueOf(System.currentTimeMillis()));

        return event;

    }

    @Override

    public List<Event> intercept(List<Event> events) {

        // 定义新的集合

        List<Event> es = new ArrayList<>();

        // 遍历

        for (Event event : events) {

            es.add(intercept(event));

        }

        return es;

    }

    @Override

    public void close() {

    }

    public static class Builder implements Interceptor.Builder {

    //用于构建并返回一个实现 Interceptor 接口的对象

        @Override

        public Interceptor build() {

            return new AuthInterceptor();

        }

    //用于配置拦截器的属性和行为

        @Override

        public void configure(Context context) {

        }

    }

}

   2)将定义好的Interceptor打成jar包。

   3)将jar包上传到Flume安装目录的lib目录下:

# 进入Flume的lib目录

cd /opt/software/flume-1.11.0/lib/

# 选择jar包,上传

rz

   4)回到数据目录,编辑文件:

# 回到数据目录

cd ../data

# 编辑文件

vim authin.properties

   在文件中添加:

a1.sources = s1

a1.channels = c1

a1.sinks = k1

a1.sources.s1.type = netcat

a1.sources.s1.bind = 0.0.0.0

a1.sources.s1.port = 8090

a1.sources.s1.interceptors = i1

# 配置Custom Interceptor

a1.sources.s1.interceptors.i1.type = com.flume.interceptor.AuthInterceptor$Builder

a1.channels.c1.type = memory

a1.sinks.k1.type = logger

a1.sources.s1.channels = c1

a1.sinks.k1.channel = c1

   5)启动Flume:

flume-ng agent -n a1 -c $FLUME_HOME/conf -f authin.properties -Dflume.root.logger=INFO,console

其他

事务

   在Flume中,也存在事务的问题:

图-7 Flume事务

流程如下:

1)Source进行doPut将数据写到临时的缓冲区PutList中;

2)PutList会推送数据给Channel,如果Channel中有足够的位置,则数据推送成功(doCommit),如果Channel中没有位置,则推送失败,进行回滚(doRollback);

3)Channel进行doTake操作将数据写到临时缓冲区TakeList中;

4)将TakeList中的数据通过Sink批量写往目的地;

5)如果写成功了,则执行doCommit操作;如果写失败了,则执行doRollback操作。

执行流程

   Flume执行流程如下图所示:

图-8 Flume执行流程

   流程如下:

   1)Source会先采集数据,然后将数据发送给ChannelProcessor(用于处理数据事件并将其传递到通道(Channel)中)进行处理;

2)ChannelProcessor收到数据处理之后,会将数据交给Interceptor来处理,注意,在Flume允许存在多个Interceptor来构成拦截器链;

3)Interceptor处理完成之后,会交给Channel Selector处理,Selector存在三种模式:replicating、multiplexing和load_balancing。Selector收到数据之后会根据对应的模式将数据交给对应的Channel来处理;

4)Channel处理之后会交给SinkProcessor。SinkProcessor本质上是一个Sinkgroup,包含了三种方式:default,failover和load_balance。SinkProcessor收到数据之后会根据对应的方式将数据交给Sink来处理;

5)Sink收到数据之后,会将数据写到指定的目的地。

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在这个科技高速发展的时代,经历了PC时代几乎人手一台电脑,随之衍生出站长这个概念;移动互联网时代几乎人手一部智能手机,智能手机一般都会安装很多应用,目前应用呈爆发式的增长;随着产业的不断深入发展,小程序的发展也日益壮大,应用涵盖各个领域;如今一个公司就可能有多个软件应用,对于软件开发商来说,急需一套分析系统帮助软件运营,如果单独开发一个分析系统去针对一个软件进行分析的话,成本会非常的大,这个成本包含开发成本以及以后的维护成本。为了解决了上述的问题,我们开发出了一套云产品:亿级动态数据统计分析系统,本系统可以支持所有的终端  (Web端、移动端、小程序端等 )数据统计,只要简单的使用sdk就可以接入我们的系统,软件开发商可以很轻松的对软件使用的情况进行监控,及时辅助公司对该软件的运营。该产品历经2年的实践,商业价值极高。本套案例是完全基于真实的产品进行开发和讲解的,同时对架构进行全面的升级,采用了全新的 Flink 架构+Node.js+Vue.js等,完全符合目前企业级的使用标准。对于本套课程在企业级应用的问题,可以提供全面的指导。Flink作为第四代大数据计算引擎,越来越多的企业在往Flink转换。Flink在功能性、容错性、性能方面都远远超过其他计算框架,兼顾高吞吐和低延时。Flink能够基于同一个Flink运行时,提供支持流处理和批处理两种类型应用的功能。也就是说同时支持流处理和批处理。Flink将流处理和批处理统一起来,也就是说作为流处理看待时输入数据流是无界的;批处理被作为一种特殊的流处理,只是它的输入数据流被定义为有界的。Flink技术特点1. 流处理特性支持高吞吐、低延迟、高性能的流处理支持带有事件时间的窗口(Window)操作支持有状态计算的Exactly-once语义支持高度灵活的窗口(Window)操作,支持基于time、count、session,以及data-driven的窗口操作支持具有Backpressure功能的持续流模型支持基于轻量级分布式快照(Snapshot)实现的容错一个运行时同时支持Batch on Streaming处理和Streaming处理Flink在JVM内部实现了自己的内存管理支持迭代计算支持程序自动优化:避免特定情况下Shuffle、排序等昂贵操作,中间结果有必要进行缓存2. API支持对Streaming数据类应用,提供DataStream API对批处理类应用,提供DataSet API(支持Java/Scala)3. Libraries支持支持机器学习(FlinkML)支持图分析(Gelly)支持关系数据处理(Table)支持复杂事件处理(CEP)4. 整合支持支持Flink on YARN支持HDFS支持来自Kafka的输入数据支持Apache HBase支持Hadoop程序支持Tachyon支持ElasticSearch支持RabbitMQ支持Apache Storm支持S3支持XtreemFS课程所涵盖的知识点包括:Flink、 Node.js、 Vue.js、 Kafka、Flume、Spring、SpringMVC、Dubbo、HDFS、Hbase、Highcharts等等  企业一线架构师讲授,代码在老师指导下可以复用,提供企业解决方案。  版权归作者所有,盗版将进行法律维权。   
上层应用业务对实时数据的需求,主要包含两部分内容:1、 整体数据的实时分析。2、 AB实验效果的实时监控。这几部分数据需求,都需要进行的下钻分析支持,我们希望能够建立统一的实时OLAP数据仓库,并提供一套安全、可靠的、灵活的实时数据服务。目前每日新增的曝光日志达到几亿条记录,再细拆到AB实验更细维度时,数据量则多达上百亿记录,多维数据组合下的聚合查询要求秒级响应时间,这样的数据量也给团队带来了不小的挑战。OLAP层的技术选型,需要满足以下几点:1:数据延迟在分钟级,查询响应时间在秒级2:标准SQL交互引擎,降低使用成本3:支持join操作,方便维度增加属性信息4:流量数据可以近似去重,但订单行要精准去重5:高吞吐,每分钟数据量在千W级记录,每天数百亿条新增记录6:前端业务较多,查询并发度不能太低通过对比开源的几款实时OLAP引擎,可以发现Doris和ClickHouse能够满足上面的需求,但是ClickHouse的并发度太低是个潜在的风险,而且ClickHouse的数据导入没有事务支持,无法实现exactly once语义,对标准SQL的支持也是有限的。所以针对以上需求Doris完全能解决我们的问题,DorisDB是一个性能非常高的分布式、面向交互式查询的分布式数据库,非常的强大,随着互联网发展,数据量会越来越大,实时查询需求也会要求越来越高,DorisDB人才需求也会越来越大,越早掌握DorisDB,以后就会有更大的机遇。本课程基于真实热门的互联网电商业务场景为案例讲解,具体分析指标包含:AB版本分析,下砖分析,营销分析,订单分析,终端分析等,能承载海量数据的实时分析,数据分析涵盖全端(PC、移动、小程序)应用。整个课程,会带大家实践一个完整系统,大家可以根据自己的公司业务修改,既可以用到项目中去,价值是非常高的。本课程包含的技术:开发工具为:IDEA、WebStormFlink1.9.0DorisDBHadoop2.7.5Hbase2.2.6Kafka2.1.0Hive2.2.0HDFS、MapReduceFlume、ZookeeperBinlog、Canal、MySQLSpringBoot2.0.8.RELEASESpringCloud Finchley.SR2Vue.js、Nodejs、Highcharts、ElementUILinux Shell编程等课程亮点:1.与企业接轨、真实工业界产品2.DorisDB高性能分布式数据库3.大数据热门技术Flink4.支持ABtest版本实时监控分析5.支持下砖分析6.数据分析涵盖全端(PC、移动、小程序)应用7.主流微服务后端系统8.天级别与小时级别多时间方位分析9.数据库实时同步解决方案10.涵盖主流前端技术VUE+jQuery+Ajax+NodeJS+ElementUI11.集成SpringCloud实现统一整合方案12.互联网大数据企业热门技术栈13.支持海量数据的实时分析14.支持全端实时数据分析15.全程代码实操,提供全部代码和资料16.提供答疑和提供企业技术方案咨询企业一线架构师讲授,代码在老师的指导下企业可以复用,提供企业解决方案。  版权归作者所有,盗版将进行法律维权。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值