|
|
5 y* f* E {$ L7 r' s! p<h4 id="flink系列文章">Flink系列文章</h4>' ]1 H2 L& ]# w b' M: Z
<ol>
5 N) F) U* ]/ j9 p<li><a href="https://www.ikeguang.com/?p=1976">第01讲:Flink 的应用场景和架构模型</a></li>
! l% m4 j: y, L) }<li><a href="https://www.ikeguang.com/?p=1977">第02讲:Flink 入门程序 WordCount 和 SQL 实现</a></li>- C: _& O* n0 H& p; K% D
<li><a href="https://www.ikeguang.com/?p=1978">第03讲:Flink 的编程模型与其他框架比较</a></li>1 _+ _. r. S' T0 ~' T
<li><a href="https://www.ikeguang.com/?p=1982">第04讲:Flink 常用的 DataSet 和 DataStream API</a></li>
! [( @. _5 d4 m4 o; I; S7 ~<li><a href="https://www.ikeguang.com/?p=1983">第05讲:Flink SQL & Table 编程和案例</a></li>
: c* F5 B3 }; Z' i+ Q* ?5 w t8 _<li><a href="https://www.ikeguang.com/?p=1985">第06讲:Flink 集群安装部署和 HA 配置</a></li>! {: B. k7 S& a5 k9 Q: x% w
<li><a href="https://www.ikeguang.com/?p=1986">第07讲:Flink 常见核心概念分析</a></li>4 d5 a5 H. n! a
<li><a href="https://www.ikeguang.com/?p=1987">第08讲:Flink 窗口、时间和水印</a></li>- `5 s1 u$ x4 U* j% m4 e) V, t5 T
<li><a href="https://www.ikeguang.com/?p=1988">第09讲:Flink 状态与容错</a></li>& _: ]" v* P# J2 x7 M
</ol>3 ^$ @0 g( S, u' R( i2 q$ t
<blockquote>0 t2 I6 ~7 g7 N2 |8 m k' T7 n
<p>关注公众号:<code>大数据技术派</code>,回复<code>资料</code>,领取<code>1024G</code>资料。</p>' W7 g0 q3 D, Z" J( W/ q2 Y f
</blockquote>
- G0 L4 N/ k* y( q* e6 ^& I<p>这一课时将介绍 Flink 中提供的一个很重要的功能:旁路分流器。</p>( S P0 _. c# o0 Y
<h3 id="分流场景">分流场景</h3>
; |0 [9 g, M( D* u9 w4 z: g<p>我们在生产实践中经常会遇到这样的场景,需把输入源按照需要进行拆分,比如我期望把订单流按照金额大小进行拆分,或者把用户访问日志按照访问者的地理位置进行拆分等。面对这样的需求该如何操作呢?</p>
+ T3 u% d. }! @* E<h3 id="分流的方法">分流的方法</h3>
- f v$ k7 G5 M' ~2 E<p>通常来说针对不同的场景,有以下三种办法进行流的拆分。</p>
! r6 {, ^% f' x* D5 @2 W6 u" ]<h4 id="filter-分流">Filter 分流</h4>
+ q3 d: E$ L1 ` m+ B* v<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/CgqCHl7CAy6ADUaXAACSFUbdpuA911-20210223084827182.png" ></p>
! h! }9 x7 i D0 X<p>Filter 方法我们在第 04 课时中(Flink 常用的 DataSet 和 DataStream API)讲过,这个算子用来根据用户输入的条件进行过滤,每个元素都会被 filter() 函数处理,如果 filter() 函数返回 true 则保留,否则丢弃。那么用在分流的场景,我们可以做多次 filter,把我们需要的不同数据生成不同的流。</p>% _0 @$ X% P; j' U- G* I. D1 X+ m
<p>来看下面的例子:</p>
4 X% x& o$ g$ W: W* V6 p# b<p>复制代码</p>4 [8 b9 ?& Y& v' @" l/ Z
<pre><code class="language-java">public static void main(String[] args) throws Exception {
; E1 [' b0 n" O" _. }5 r# I StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
" `. L- p% j: ^5 g0 ]5 f //获取数据源+ Q* _+ V$ a4 [5 W1 s& U" L
List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();
# {# X" R! d% X& O' H0 C! z data.add(new Tuple3<>(0,1,0));8 E: l5 x( p" M/ B) A
data.add(new Tuple3<>(0,1,1));
3 I7 V1 E! V/ ]+ G! G' n9 ]7 n& E data.add(new Tuple3<>(0,2,2));3 Y1 }* v9 \8 h! ?9 ?' x Y: V5 o
data.add(new Tuple3<>(0,1,3));: O) h7 N& D' r7 Y: k$ i" \; j
data.add(new Tuple3<>(1,2,5));
5 b9 \+ Z' d' r0 N8 C data.add(new Tuple3<>(1,2,9));
- g0 z1 _7 x, E0 Y q* e data.add(new Tuple3<>(1,2,11));( {3 x x( u" n7 ?3 F# b8 A/ ^
data.add(new Tuple3<>(1,2,13));: ~0 v+ q1 k7 J( f6 r$ l: S( K
( n- k/ D/ Y. d6 l: ?" ^7 | DataStreamSource<Tuple3<Integer,Integer,Integer>> items = env.fromCollection(data);
8 y' T% j4 u( b& ]; `% \" w0 r4 ^" A/ A" A7 U. f' Q
! z: g: }. x' a0 ~( }( I! a- D2 O: T) P
8 c2 F6 {1 c# V1 c
SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> zeroStream = items.filter((FilterFunction<Tuple3<Integer, Integer, Integer>>) value -> value.f0 == 0);
( {9 L" N( S! W* }: r
% V/ \* Z. G5 j' e: y5 O# C SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> oneStream = items.filter((FilterFunction<Tuple3<Integer, Integer, Integer>>) value -> value.f0 == 1);
2 V* X$ c/ Q1 x* Q# R. w( o9 n
; w% I! K0 y9 E! @! V( w9 a6 q# _) \. m7 \, f
# U( s8 V' @. g# L; x
zeroStream.print();
$ P9 ~6 V, ~! p# x: ]# S3 c6 E9 {+ J) R/ c# h2 S0 e9 y
oneStream.printToErr();) Y) ?% z% L. D# J8 E+ v9 x
P+ d- p# q% S' ^) Y1 Q& \5 s
s9 ^; U7 s& [3 w) S) N0 [# }
7 u8 r6 _1 a: ^$ [& J' P; }+ Y# v+ ~, j3 z7 n- F6 q
# T4 R# K @$ }- M
//打印结果
0 y6 k8 q( t3 p* x" q! J7 }
$ Y/ X- o6 }) T" Y2 i$ G& X String jobName = "user defined streaming source";
3 a0 l7 w2 ]1 L5 d/ c7 s9 ?& Y% Z; G2 _/ i( z
env.execute(jobName);& e3 J+ g1 O( o; U V: ~
/ z) k& `; a1 Z- p
}
/ ` R0 T- |& p% q1 p6 F: N0 _</code></pre>9 E7 X" ?4 c4 x' h/ I
<p>在上面的例子中我们使用 filter 算子将原始流进行了拆分,输入数据第一个元素为 0 的数据和第一个元素为 1 的数据分别被写入到了 zeroStream 和 oneStream 中,然后把两个流进行了打印。</p>9 j% I$ P. K) U2 B( U$ l
<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/Ciqc1F7CA2WAYbshAAKj494h86s723-20210223084827296.png" ></p>
! X/ P+ i4 K! \; E* @$ `* D: ]<p>可以看到 zeroStream 和 oneStream 分别被打印出来。</p>
# h5 b8 O0 z+ H1 t+ r: f<p>Filter 的弊端是显而易见的,为了得到我们需要的流数据,需要多次遍历原始流,这样无形中浪费了我们集群的资源。</p>
' T+ `: ~! [( y# q1 X4 ^ N1 N<h4 id="split-分流">Split 分流</h4>
: B ~: p; d; N1 g9 k- D' R7 }8 p<p>Split 也是 Flink 提供给我们将流进行切分的方法,需要在 split 算子中定义 OutputSelector,然后重写其中的 select 方法,将不同类型的数据进行标记,最后对返回的 SplitStream 使用 select 方法将对应的数据选择出来。</p>
. Q2 z* {% a1 H2 ^# u% P& V<p>我们来看下面的例子:</p>9 X% p9 G2 R- P7 k- j7 Z, }. f
<p>复制代码</p>
3 u- J4 w2 U, S<pre><code class="language-java">public static void main(String[] args) throws Exception {
, i/ N9 s1 S j4 V7 ]3 y! a. o. Z
C- D4 L5 M: V9 F' e/ s i5 n) W1 V) j3 r
$ v) M, C) d9 Z2 G/ r5 r- Q: N1 v
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
* I1 S( a! B# `; X$ G+ J7 Z2 E- x1 _( ?
//获取数据源( a& H7 y& W5 L5 x+ U8 B- z S
1 l0 B3 o) x" f6 k I3 u List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();' G2 n4 o8 @! ? T. N% a
+ l3 a( K, }5 h& z. u data.add(new Tuple3<>(0,1,0));9 Z H# ^6 b; g! g
& e! ] s* ~' ^
data.add(new Tuple3<>(0,1,1));
2 n# H- J2 P0 r# ~/ i9 C( c" _6 f U3 L2 u$ y, F2 X! }) F f
data.add(new Tuple3<>(0,2,2));- u1 }5 D% I. L
! f- u! C# F2 }7 A; Q/ p
data.add(new Tuple3<>(0,1,3));
' S- `& B" G0 @; A$ c' |: b0 ^
data.add(new Tuple3<>(1,2,5));; N* d+ | i& T# @% I, n2 K
) n/ i; X2 ~6 g8 Y0 c ] data.add(new Tuple3<>(1,2,9));1 }* k: x+ ~, h9 D8 f* T
7 E6 R6 p/ A1 M/ G9 m- X* |
data.add(new Tuple3<>(1,2,11));
- h) D4 }: e# Q7 ]4 V+ E9 q5 J9 R4 x0 x( P
data.add(new Tuple3<>(1,2,13));$ Y5 g$ r- V% {& q
" T2 A! ?' c0 u ?( A2 L: x
, |5 e& X& A$ ]8 H8 ?7 K' {5 \7 s- h" t6 g+ [" P, r
: |# `: {. K3 i: {1 T; ?) S# e! r. R: s2 ? o' `/ j" u
DataStreamSource<Tuple3<Integer,Integer,Integer>> items = env.fromCollection(data); _% O* @3 \# p
6 ?3 M( k1 K1 T8 o2 z
! A. e$ b, a# H, y+ Y. h: R, c% r
/ j8 Q& E5 t5 A0 X; G; F
- A4 \8 Z0 {( [: ?5 }3 b1 w1 M1 Q& ~
SplitStream<Tuple3<Integer, Integer, Integer>> splitStream = items.split(new OutputSelector<Tuple3<Integer, Integer, Integer>>() {3 ]6 b* K1 g" R2 R7 D! F6 j
! \% M+ j/ y: X1 \/ H+ H, P. q
@Override
% r9 k" k, B; _4 ]* q
+ n+ d! r, b2 w# s; K2 r6 [ public Iterable<String> select(Tuple3<Integer, Integer, Integer> value) {* I K6 ^/ j. [* |+ z
9 m% i- O* |9 y( ~' Z List<String> tags = new ArrayList<>();
$ s, Q1 ?: y# q, S) W8 u' d) B( s* T y
if (value.f0 == 0) {
, c+ ^( T0 |% ] J
! ~2 J* D; `; x0 |$ e2 d tags.add("zeroStream");! w/ i* p2 F3 W8 ^
$ s9 J. A$ ]9 a1 [
} else if (value.f0 == 1) {$ P! d3 S# p3 G5 e7 X- \5 I z7 s
3 q/ }) w. \3 s1 d2 I7 d; p( u tags.add("oneStream");
+ d; A% e9 I G9 H; b/ b8 U' X9 |; p1 q9 ]( u
}
" u: C+ Q5 @, q% H. Y% d9 n+ S) |
1 F: R" b( d. j% [) ` return tags;
/ O- D' S' `( V0 j8 P. ?: c1 T, |4 O1 Y% o7 X, b8 u
}
% Y9 C( o6 F8 ?( T# ~
! S' N' L% x! H8 a* q1 X });
$ R/ [7 z) I. Y$ F& T; h! Z$ v6 ?6 I) @ B
# x0 B" W! R0 {. g
. p0 ~$ a! b% X* {& ~
splitStream.select("zeroStream").print();
& n! F* l; V \8 E, n/ G
3 s4 {2 M& T) S( f3 u splitStream.select("oneStream").printToErr();
: k8 G$ e$ F. r3 ?+ R; S, \3 x6 a& ^: e
1 |7 q# h) K+ S( d+ K
$ y u* K3 w# H. C I2 X //打印结果
, i6 T1 }$ A9 d
8 d& C; D0 s* Y9 [1 Q- G5 x1 n String jobName = "user defined streaming source";" J* u+ G* }+ Z6 I8 ?- z
' I% a0 W. v9 b6 x' M+ W0 P
env.execute(jobName);% A, H# ?# a9 s1 H7 R8 d8 D
; X5 X. V0 R" K. R$ x) x/ I. i& Y) U
}
5 o$ R" s5 n3 S. S. n</code></pre>$ A, P# H4 Y8 `
<p>同样,我们把来源的数据使用 split 算子进行了切分,并且打印出结果。</p>1 \3 M5 E2 \9 x& J" F
<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/CgqCHl7CA4aAbUSJAAG1LWNB3qw627-20210223084827377.png" ></p># t5 O+ c% y. Z, [0 s+ W' M
<p>但是要注意,使用 split 算子切分过的流,是不能进行二次切分的,假如把上述切分出来的 zeroStream 和 oneStream 流再次调用 split 切分,控制台会抛出以下异常。</p>. w6 s0 Y+ X3 P( n: O! L7 s8 b
<p>复制代码</p>0 A! k' g! k5 Z+ e- g c
<pre><code class="language-java">Exception in thread "main" java.lang.IllegalStateException: Consecutive multiple splits are not supported. Splits are deprecated. Please use side-outputs./ z) p1 X3 f. x+ e9 Y
</code></pre>
* p! a. ?2 b, ?2 m2 N<p>这是什么原因呢?我们在源码中可以看到注释,该方式已经废弃并且建议使用最新的 SideOutPut 进行分流操作。</p>0 ]& V7 i7 C. [! f# z) c) @5 X
<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/CgqCHl7CA6OAJ-JDAAIrh1JSAEo033-20210223084827602.png" ></p>; c t7 |' I8 } M
<h4 id="sideoutput-分流">SideOutPut 分流</h4>5 H# ~1 K" `4 d
<p>SideOutPut 是 Flink 框架为我们提供的最新的也是最为推荐的分流方法,在使用 SideOutPut 时,需要按照以下步骤进行:</p>' g9 ~$ r2 z) A
<ul>
4 D$ C3 T7 M- ~4 C& \% Z" X<li>定义 OutputTag</li>
. R$ s% e! {: c/ ^<li>调用特定函数进行数据拆分7 ~" K5 |, C! K: l
<ul>1 Y# e* k3 X5 H
<li>ProcessFunction</li>: v* I! }* X$ |
<li>KeyedProcessFunction</li>
1 {& N. j* k5 p5 W2 d<li>CoProcessFunction</li>( k, \9 Q" g' m6 l ?
<li>KeyedCoProcessFunction</li>% @% b5 ]; `9 @" ~+ q
<li>ProcessWindowFunction</li>. F( c. C9 }) _; O( \5 D& b9 E
<li>ProcessAllWindowFunction</li>4 W2 Z6 [1 H0 ~' M8 N. {% g
</ul>- _4 W( { i- z1 j9 ?7 F R
</li>" l$ v/ t* n2 o; d) U/ |( w' V
</ul>
; {* Z m) E5 b& a3 _$ K8 R<p>在这里我们使用 ProcessFunction 来讲解如何使用 SideOutPut:</p>
* \' ]3 c7 b8 J' K<p>复制代码</p>- \9 T+ n- S1 O- Q9 n3 K" ]
<pre><code class="language-java">public static void main(String[] args) throws Exception {
/ l2 H; @5 o8 I. e
5 ?0 ?9 m7 f. A& ?9 [
, G! d2 y. ~; _, P8 ~9 c2 P
% x8 g$ H7 l R. c/ S StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
8 w: t! W1 h J$ w1 Q8 \
4 {/ S" a$ P8 r7 z2 R //获取数据源) t5 B4 h K! u3 f
# m% ~/ ?& N; j- o' Y
List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();
5 M- I# J) M# v" H
0 u; j7 F- ~3 y0 L- x' [ data.add(new Tuple3<>(0,1,0));
5 G5 e* W' ~- S
& s* J: L7 t& V9 x- [: m data.add(new Tuple3<>(0,1,1));$ M/ m- X" g. R# |: h5 u- O
) N& O, O r' B" @ data.add(new Tuple3<>(0,2,2));
. ~4 q v3 f: ^" w1 f. A* A% f$ r. a1 p' r6 r$ T& k" h8 V1 S% X! G6 Y
data.add(new Tuple3<>(0,1,3));5 C" t, b5 [$ S5 J3 L8 X% L
4 |7 }# I- B" B, `, f2 S data.add(new Tuple3<>(1,2,5));
5 |! J, u6 g. R5 I- A& E# ?
9 N% l' e) W$ a7 P, N data.add(new Tuple3<>(1,2,9)); b; N& j$ F) U
; z4 [- v- N6 }: D* Y
data.add(new Tuple3<>(1,2,11));
' M; q1 }: J5 }* R, i) N7 z
7 e% J0 j3 F9 \' o' A2 N: q4 `9 Q data.add(new Tuple3<>(1,2,13));
* e7 H5 B! J' |. m3 Y
- m! D8 n5 p5 l' K5 d: \. ]0 P+ P7 t+ J6 [. U7 i/ f! b
3 K$ \* M" ]: G6 T! G' R
& t9 _8 _( g2 M1 H
) x& e" Z, U+ M, i DataStreamSource<Tuple3<Integer,Integer,Integer>> items = env.fromCollection(data);' m$ Z1 C0 l1 G/ r9 E ?% e \
* f) N7 x8 z% D D/ [
" ?; ?; M; x8 ~, k3 e# n. B; i" T/ T) J! r& J; y% M9 X. W7 F
OutputTag<Tuple3<Integer,Integer,Integer>> zeroStream = new OutputTag<Tuple3<Integer,Integer,Integer>>("zeroStream") {};
7 }7 a7 B" z1 C. T
( Y1 I3 h& ]3 o OutputTag<Tuple3<Integer,Integer,Integer>> oneStream = new OutputTag<Tuple3<Integer,Integer,Integer>>("oneStream") {};; F2 Y @$ [/ s9 }
2 X# J, J, m( x6 s5 _! e) g8 B2 E
. c3 d( K0 `$ q: R6 }' ~) m# L- ^8 |" ^( W+ q& P) Q* a5 |
( W) j9 ^& z7 ]1 c, j
; k$ w3 t, E# [6 Y/ @1 v2 P) V/ d+ Z- y SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> processStream= items.process(new ProcessFunction<Tuple3<Integer, Integer, Integer>, Tuple3<Integer, Integer, Integer>>() {; e0 w% ?8 m# O
: T2 N' }; G* r, R. U- H+ L$ s U @Override$ V( d- ^1 i& D4 N% ?- m+ c
& l2 |3 \- i3 y0 A3 N/ x, D @ public void processElement(Tuple3<Integer, Integer, Integer> value, Context ctx, Collector<Tuple3<Integer, Integer, Integer>> out) throws Exception {& D, o/ |( B) R1 P2 T* q$ q0 W w
U) m( N6 G* _+ H. ?# _0 `
9 k. m- M" T' z2 O
9 u4 |% o3 G% b- ` Q$ k if (value.f0 == 0) {" y( E* `5 o/ W! R* w. ~$ [; W
5 J& `) `7 D, y ctx.output(zeroStream, value);$ b+ B3 }9 X) m0 Y7 q
; t# V4 g5 @8 B1 ~, @3 E* K } else if (value.f0 == 1) {' W# `! x" g! f) g( i8 p% b3 _' J
0 E; A: y& L: z' c" e; T" H
ctx.output(oneStream, value);
* v4 F' K" {5 n. R1 s7 ]. v+ [: h! F! N& `( k( V k, s
}+ T7 ]6 Z% N, a9 a, _7 ]/ @4 L
, w( R: {3 J P. w) I$ S7 C. k }
) G0 `6 ~% y' c
5 _0 V2 M( i. T- x' n });
7 Y. K6 q/ p! Q$ [/ {. b2 q- @5 `+ d8 r1 |4 j \( h
* |% k) o# G/ b# u/ ], G3 x
. u$ v' a4 ^3 m( v DataStream<Tuple3<Integer, Integer, Integer>> zeroSideOutput = processStream.getSideOutput(zeroStream);: _( s3 S. N; x; J# I
|2 I0 f+ C- O1 j$ s
DataStream<Tuple3<Integer, Integer, Integer>> oneSideOutput = processStream.getSideOutput(oneStream);% p( L! v; j! T) U! C
' |2 F. s' |' [& J- [
, |& C/ G6 J# E2 ^' ]+ e a) Q# N4 f% d3 ~4 [
zeroSideOutput.print();
1 N# c' z/ n ^7 K q. ?- u
; B% O# x# F/ P: p% h oneSideOutput.printToErr();
# P: p S/ [5 V8 t+ ?) l
" g4 N1 n" _5 H6 e+ ~3 v5 @3 [0 @
3 h+ f5 s* c8 h" s0 j( g
! @0 F7 o7 a* L4 k/ N$ Q( h7 R; P
& L; u4 L0 t2 r! k1 {7 }4 t //打印结果$ C `( B& \0 W z* l5 f: C
$ O. H/ a5 w. O- X* g String jobName = "user defined streaming source";* C |( X+ ~% R9 W
# j) `: t- n. X0 j) v' L- [ env.execute(jobName);1 r) n' ]" Q- m& B6 z$ n: H
* k* C# S, l* G) A0 U. @# E
}& h7 E9 J: F* I: n
</code></pre>8 d1 ^# D. A/ s% z `
<p>可以看到,我们将流进行了拆分,并且成功打印出了结果。这里要注意,Flink 最新提供的 SideOutPut 方式拆分流是<strong>可以多次进行拆分</strong>的,无需担心会爆出异常。</p>+ S u$ k# n# ^( s5 i
<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/CgqCHl7CBMKAGHoUAAM-5UL5geg132-20210223084827698.png" ></p>
1 Z: h9 L* j5 l& S; x<h3 id="总结">总结</h3>% J1 H) z3 V3 v3 `6 s/ Q$ T7 D2 T
<p>这一课时我们讲解了 Flink 的一个小的知识点,是我们生产实践中经常遇到的场景,Flink 在最新的版本中也推荐我们使用 SideOutPut 进行流的拆分。</p>
7 h1 F& h' G+ M5 |/ _<blockquote>+ r7 P: e% {) b" i( ^8 v
<p>关注公众号:<code>大数据技术派</code>,回复<code>资料</code>,领取<code>1024G</code>资料。</p>+ @5 _' _' B) b$ f/ z
</blockquote>6 ~7 A4 P; ]* P4 y
6 ~2 o' u; Y1 ?8 m7 R |
|