|
|
' C3 \8 C v3 h' V
<h4 id="flink系列文章">Flink系列文章</h4>
8 O+ F% P6 f1 z# {<ol>
1 x9 l# m9 ?" A" n, N<li><a href="https://www.ikeguang.com/?p=1976">第01讲:Flink 的应用场景和架构模型</a></li>
+ g- l+ R/ d6 t6 h0 T9 p<li><a href="https://www.ikeguang.com/?p=1977">第02讲:Flink 入门程序 WordCount 和 SQL 实现</a></li>7 G& h7 z' r6 M9 W3 T7 O7 y& e
<li><a href="https://www.ikeguang.com/?p=1978">第03讲:Flink 的编程模型与其他框架比较</a></li>
, b0 X9 @ Q% s- D& l5 p. e, w<li><a href="https://www.ikeguang.com/?p=1982">第04讲:Flink 常用的 DataSet 和 DataStream API</a></li>; f' H! Q' q. G& F+ ]$ L5 Y
<li><a href="https://www.ikeguang.com/?p=1983">第05讲:Flink SQL & Table 编程和案例</a></li>% V0 j: ^' {8 D* m8 e
<li><a href="https://www.ikeguang.com/?p=1985">第06讲:Flink 集群安装部署和 HA 配置</a></li>, C P4 U; S" a
<li><a href="https://www.ikeguang.com/?p=1986">第07讲:Flink 常见核心概念分析</a></li>
2 h% `5 }# [& u- P" Z3 |; G7 ~<li><a href="https://www.ikeguang.com/?p=1987">第08讲:Flink 窗口、时间和水印</a></li>3 ^. @5 r3 @# _$ ?% K! r, p* ]
<li><a href="https://www.ikeguang.com/?p=1988">第09讲:Flink 状态与容错</a></li>
# F/ X1 J! y0 o- |$ v# `6 X# M</ol>
7 U( Q* [- _1 z# p) f& d A, T0 w<blockquote>
/ Q" k2 O. [0 l! F$ f* t. s) }5 S+ |<p>关注公众号:<code>大数据技术派</code>,回复<code>资料</code>,领取<code>1024G</code>资料。</p>
( x3 s8 t7 j% E. i5 @</blockquote>1 n V5 @6 E: }7 h$ M+ y! W7 Q
<p>这一课时将介绍 Flink 中提供的一个很重要的功能:旁路分流器。</p>
( ~9 X: Y; U5 n3 a<h3 id="分流场景">分流场景</h3>4 h. K9 u/ X; z% `4 Z X' k6 A
<p>我们在生产实践中经常会遇到这样的场景,需把输入源按照需要进行拆分,比如我期望把订单流按照金额大小进行拆分,或者把用户访问日志按照访问者的地理位置进行拆分等。面对这样的需求该如何操作呢?</p>
& ^5 ]" g8 e4 L1 }7 O' N' c<h3 id="分流的方法">分流的方法</h3>% v4 J- l" ]2 O3 O: ~. r
<p>通常来说针对不同的场景,有以下三种办法进行流的拆分。</p>8 s* r0 I4 S# y' y
<h4 id="filter-分流">Filter 分流</h4>; i2 N) W# Z5 `( l, \7 H/ p
<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/CgqCHl7CAy6ADUaXAACSFUbdpuA911-20210223084827182.png" ></p>: b* ]+ O8 S$ O$ \% ]# q
<p>Filter 方法我们在第 04 课时中(Flink 常用的 DataSet 和 DataStream API)讲过,这个算子用来根据用户输入的条件进行过滤,每个元素都会被 filter() 函数处理,如果 filter() 函数返回 true 则保留,否则丢弃。那么用在分流的场景,我们可以做多次 filter,把我们需要的不同数据生成不同的流。</p>
5 p, Q# c: Z$ g6 H3 _3 U<p>来看下面的例子:</p>9 s' Y3 i, E" C% F& F
<p>复制代码</p>
9 c1 k3 m( u1 _0 i<pre><code class="language-java">public static void main(String[] args) throws Exception {0 \2 @9 v9 ~7 B' |
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
" [5 G+ g0 N+ H, R2 H //获取数据源
5 j1 v$ x* [, K List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();
- H- W) Y# f7 K. @% q data.add(new Tuple3<>(0,1,0));
: y0 [/ @) @8 ?0 @3 P* k: _ data.add(new Tuple3<>(0,1,1));
3 b9 B# V* [: } data.add(new Tuple3<>(0,2,2));
. b) d: ] |, W3 g; A, q data.add(new Tuple3<>(0,1,3));
: ?+ g- B$ n' e data.add(new Tuple3<>(1,2,5));/ i. v. E. q, P" p
data.add(new Tuple3<>(1,2,9));6 ^! p# C% Z+ M& z+ q
data.add(new Tuple3<>(1,2,11));
) d" Y9 B& b E* D- X data.add(new Tuple3<>(1,2,13));/ J! R7 }' \5 @+ r }. d4 j
! L" q" d9 R/ [, k: D$ X
DataStreamSource<Tuple3<Integer,Integer,Integer>> items = env.fromCollection(data);
/ X# x! A% x9 f8 _2 \
: f' |3 `0 a1 L. a' y. @2 f
- t* ]: v! E3 O! X8 F6 b! M9 u! |$ X3 G8 w
SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> zeroStream = items.filter((FilterFunction<Tuple3<Integer, Integer, Integer>>) value -> value.f0 == 0);
4 z+ D+ v, P, j0 R* z: a# _8 X* o) D
SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> oneStream = items.filter((FilterFunction<Tuple3<Integer, Integer, Integer>>) value -> value.f0 == 1); y! d! t2 ~3 {" k# @
6 {' L2 G/ Q0 o. y" l+ f2 o# S
1 }4 J" y: b* m% O. M. ]5 z" \: Q. M0 C6 w& }& N% r
zeroStream.print();
0 z; n* C- Y: J+ K- i( @$ S" w2 ^8 }# @% z5 |
oneStream.printToErr();0 G* X, w$ U+ ^* A/ h0 z
- s: ~$ y- y/ A" o9 v! [& O
: j: y3 v( Y5 }6 H% Q3 f
v% p$ S7 f- W2 b6 Y
% \: X7 x: W8 a9 v% i6 D
. k* f0 w$ h) l. D( T2 I, \2 x6 [ //打印结果
% E+ ] x/ F9 e' G$ G; N4 O+ H4 r. F1 Y# n" Y6 `- X4 s4 Z
String jobName = "user defined streaming source";
/ j# I# r) s$ c; f
+ O; b7 x& [& |% q P+ M: @ env.execute(jobName);4 r) Z( {5 b$ P( P/ q) u
) m* G' y/ M- A, U9 t: r" P# k}
) C8 T: W- d0 B9 z1 O</code></pre>
0 t' K8 ~4 Q" l* F: [, d<p>在上面的例子中我们使用 filter 算子将原始流进行了拆分,输入数据第一个元素为 0 的数据和第一个元素为 1 的数据分别被写入到了 zeroStream 和 oneStream 中,然后把两个流进行了打印。</p>' G7 W* }' n* X& ?: S* S9 I
<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/Ciqc1F7CA2WAYbshAAKj494h86s723-20210223084827296.png" ></p>$ e$ V+ v6 {0 p* q
<p>可以看到 zeroStream 和 oneStream 分别被打印出来。</p>: u; U/ }( H- E7 q9 `" T
<p>Filter 的弊端是显而易见的,为了得到我们需要的流数据,需要多次遍历原始流,这样无形中浪费了我们集群的资源。</p>
. p H( H8 l/ G1 v<h4 id="split-分流">Split 分流</h4>( X# A0 M/ S" ]" s* B2 q v
<p>Split 也是 Flink 提供给我们将流进行切分的方法,需要在 split 算子中定义 OutputSelector,然后重写其中的 select 方法,将不同类型的数据进行标记,最后对返回的 SplitStream 使用 select 方法将对应的数据选择出来。</p>
4 ^ t7 G5 q' u% }+ N- l2 {. Z1 {<p>我们来看下面的例子:</p>
. X/ S8 ~* m& S7 k8 v- }<p>复制代码</p>
0 d/ A) q! U/ E* e y<pre><code class="language-java">public static void main(String[] args) throws Exception {* {: ~1 w$ v) @ j6 Y
. z4 D# H. d# d+ c0 w9 E- k9 z/ Q! U' A2 r
, `& x. p' H+ c* z( ]
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
9 C2 b) H* F, n8 v$ x: a% p0 e' w+ C* N% d
//获取数据源
" w6 }3 D" K0 w9 Z! x
, ?1 Q/ z8 `- C- E+ t List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();
9 W8 \+ ~+ k! }1 G) c2 m
" X- ?3 _4 V5 c" ^, J+ ? data.add(new Tuple3<>(0,1,0));6 ?) D5 O: `" j7 E: I# X M
+ H" K% [- P# X G9 r5 x, _2 ~ data.add(new Tuple3<>(0,1,1));
- w4 X2 `5 |+ X2 y3 B0 Y5 y- Q
( R" f3 i. Q6 x3 R data.add(new Tuple3<>(0,2,2));
6 W* {; Z2 f" i" y5 X. j* i! f% F$ W/ P& [+ ~+ F7 p2 ~; E
data.add(new Tuple3<>(0,1,3));0 `4 ~' @+ R4 w. Y8 ^4 k; w, Z
& u# k& Z0 Z0 ?- J
data.add(new Tuple3<>(1,2,5));
- d9 j; j4 }+ y% P1 a! C
2 j9 t/ e) V' B data.add(new Tuple3<>(1,2,9));
( t7 e' D0 p: Z7 N+ u9 m1 h _! b
, _2 f9 g4 H; R4 H; z data.add(new Tuple3<>(1,2,11));% a5 y+ M) [) e, ]! p
* s& B! L7 @: v- a4 p% V data.add(new Tuple3<>(1,2,13));
( C4 Z! l1 w0 {5 _ A( e) k' z8 x3 X. X4 Y9 {
+ G# E1 R0 G3 w( w# J* R! o) {6 L# E! H* w: _( `
) R) r9 k# D& m8 N4 Q
2 j' N/ s# L2 A
DataStreamSource<Tuple3<Integer,Integer,Integer>> items = env.fromCollection(data);. _0 y" h% k6 x/ F+ H/ v' v
4 X1 V/ Y2 G5 j8 [: t
9 |0 V+ o4 ]( |" f) v* }
7 c' K. n6 g9 e4 p2 g% s; A+ S
0 S) B1 a f' O7 Y& X3 O1 I
' x, j- Z. [ t* k- d1 K" w5 m6 n SplitStream<Tuple3<Integer, Integer, Integer>> splitStream = items.split(new OutputSelector<Tuple3<Integer, Integer, Integer>>() {
" ?. M4 p f ^. _) j/ i% Y! b5 P1 s$ a5 q# s2 f5 G
@Override
8 V: `5 C5 [/ x. s- O+ m
2 d0 n# C: n* _0 Y. D public Iterable<String> select(Tuple3<Integer, Integer, Integer> value) {
@6 @" X7 z9 p7 o7 ^4 L- Z! U4 R, }/ e# S: N5 J# o% F" A
List<String> tags = new ArrayList<>();
* V2 A$ S6 ]# i. y$ U, e
7 H( D4 j1 X) v) K y) f if (value.f0 == 0) {7 Z$ y1 p3 l, A( R l
' l$ a% ?; J! r; d* e2 }1 q% B tags.add("zeroStream");
! ?& F+ h, U1 [2 n5 C' g# X3 N3 k! s" H" Z# c/ a3 ^
} else if (value.f0 == 1) {. |: r, \% e/ ]. w7 m/ D3 ~
7 @1 U- G+ m; F$ _$ h) a
tags.add("oneStream");- S8 `0 s' V4 y5 T+ x! z
6 M# x/ Z, v9 {4 v) z
}
: H& F9 e2 h' ?0 X! E0 z9 X0 A
. B7 ~3 J% }/ K1 g! Y4 I return tags;4 O/ [/ b% O* m
0 q. h1 {' V& s% H {% R) M
}! {7 S3 L; r' l* W0 h. T7 X
/ }6 U; ^& Q: s; F# f4 z$ H
});
; B0 \6 ^, K# f4 O$ ]
& X) i2 G& L2 c' d# L% k$ U: i$ C: S$ R' A' s$ _
+ i9 ~/ s/ `+ f8 v splitStream.select("zeroStream").print();# M3 ?3 A' w. h. r# [
: |& P6 J. J/ [- Q4 H! ^
splitStream.select("oneStream").printToErr();
* X7 i7 u; \* y2 D( K. }; V& F. S! Z
' T' P5 B" U, g+ {. @4 ]5 \! V6 C$ V1 N% B/ h e
//打印结果9 H+ U0 O6 Q; S- P
+ T% r N! w' g* F
String jobName = "user defined streaming source";
( I5 j2 |. p1 { E8 }% n0 A- A% ?3 F: d* C/ }$ o3 D$ O5 M
env.execute(jobName);
! S1 b# ~ Z2 m8 w. ` n3 I U7 g% Z4 B$ N' _3 |6 @) }
}
3 L w# X4 O, m</code></pre>
7 {" V) W1 U2 q8 A<p>同样,我们把来源的数据使用 split 算子进行了切分,并且打印出结果。</p>6 o: R7 N" t9 h( @
<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/CgqCHl7CA4aAbUSJAAG1LWNB3qw627-20210223084827377.png" ></p>* P9 h ^* s' a# ~# [; C
<p>但是要注意,使用 split 算子切分过的流,是不能进行二次切分的,假如把上述切分出来的 zeroStream 和 oneStream 流再次调用 split 切分,控制台会抛出以下异常。</p>
# [ u( @. R4 ^ ]# G<p>复制代码</p>5 u& e, T' a& g# e' P M* f1 V% B
<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.$ c& Z- E% x. I2 D( X$ n
</code></pre>
7 N. U& u0 a, v, L$ t( M8 I2 |<p>这是什么原因呢?我们在源码中可以看到注释,该方式已经废弃并且建议使用最新的 SideOutPut 进行分流操作。</p>! a; |$ y. Z: t: G( Q) {6 C
<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/CgqCHl7CA6OAJ-JDAAIrh1JSAEo033-20210223084827602.png" ></p>( ^. U) F; X. |
<h4 id="sideoutput-分流">SideOutPut 分流</h4>
+ i% ]& h a. e( s- u2 [<p>SideOutPut 是 Flink 框架为我们提供的最新的也是最为推荐的分流方法,在使用 SideOutPut 时,需要按照以下步骤进行:</p>! R0 N) v* C% D$ A5 d k
<ul>) j6 J0 c: W& ~+ Y' r) Z/ j) Y% Z( U
<li>定义 OutputTag</li>% F* s+ ^% W. @$ g( h' y* }$ M
<li>调用特定函数进行数据拆分
; k( W7 C: m# q% Q<ul>
& b6 x% {# A! M4 t! Q<li>ProcessFunction</li>
( ?, k }6 d+ E! y' O0 }( t<li>KeyedProcessFunction</li>
9 `8 w4 y7 W2 d/ [% c% b<li>CoProcessFunction</li>
7 d, j5 d+ @# [% C4 B3 I<li>KeyedCoProcessFunction</li>
, [! @# t; |& O1 r) X% K<li>ProcessWindowFunction</li>
% ]1 o# m! Z$ V( ~& k<li>ProcessAllWindowFunction</li>1 S2 I/ X2 V( z" }' p
</ul>. v# R2 U: w% F: X. [$ c0 T
</li># u) L3 v- |1 z4 r/ J
</ul># V4 u Z' T Y$ v( c; w$ D) r
<p>在这里我们使用 ProcessFunction 来讲解如何使用 SideOutPut:</p> @6 `7 a* s( ~; b( d+ C b, f, z
<p>复制代码</p>5 ^3 F+ K0 N# `6 I
<pre><code class="language-java">public static void main(String[] args) throws Exception {
& H3 A/ J6 \4 M6 z. C5 }( @) {9 e: H& H3 r! {- j9 K: S# z
. ~+ n1 Z3 A2 r" f% @
# _6 `8 d; s& j+ X# y StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();% I$ n% W; R d6 U8 G W o
9 M; O% d8 }3 u0 b2 ~0 y. F
//获取数据源3 G$ H0 ~! s1 O# W/ |/ X
5 L% @0 y4 K# D3 C* \3 W: g8 e1 \# u List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();, i6 I6 V/ p2 g- w9 o
: k$ j3 l+ C. x1 c3 w2 b. ^ data.add(new Tuple3<>(0,1,0));
/ X" x D ~ s* }! \8 E- S
% g/ e) a4 n. ]( j1 A# c2 } data.add(new Tuple3<>(0,1,1));% h h: O& L: ?! ]+ a
% {, o p+ [. u' l7 }$ ~ data.add(new Tuple3<>(0,2,2));; X) f* H4 K4 \, l6 T2 }
7 c% U# d5 s$ x data.add(new Tuple3<>(0,1,3));9 p# H* }# ^2 U0 n
% n' L6 k! g8 W9 M* e8 ]
data.add(new Tuple3<>(1,2,5));- {) E$ x- [( {2 e
; t: m4 |1 [; W# g9 s+ l
data.add(new Tuple3<>(1,2,9));
' q$ N( S7 n/ X8 Y) Y8 d9 B6 R4 h
data.add(new Tuple3<>(1,2,11));
$ X: n ?* x2 z- r$ G0 m0 v9 b. Z5 o5 j
data.add(new Tuple3<>(1,2,13));7 ?8 H" S8 n9 I
, b) ^' T8 P- z$ t
- {( m3 {& S! o
2 _3 ^! {1 O* L- S
& H4 I! `% A/ r `0 x9 x/ r
% o4 w T9 ]/ e3 a5 k' T! ? DataStreamSource<Tuple3<Integer,Integer,Integer>> items = env.fromCollection(data);
5 v; v7 i. {$ p0 |
8 G2 u% h, e" Z* \, W9 W3 P; z4 e! h; Y
/ `( t9 J5 I" K5 F* i
OutputTag<Tuple3<Integer,Integer,Integer>> zeroStream = new OutputTag<Tuple3<Integer,Integer,Integer>>("zeroStream") {};$ ^9 z8 h" s6 v* ^: J
2 r: o% V! m# [# I3 ^ OutputTag<Tuple3<Integer,Integer,Integer>> oneStream = new OutputTag<Tuple3<Integer,Integer,Integer>>("oneStream") {};
9 q1 b; K0 N- S* G8 t$ R+ {! y
1 n7 j6 @; H: ~" a% C* [- L# F$ k2 I' M' J$ D* E
- Y0 J# J! U# a" m4 M( f$ d8 D# f
! E+ M+ O$ m7 q+ V3 z& p; I0 r w
SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> processStream= items.process(new ProcessFunction<Tuple3<Integer, Integer, Integer>, Tuple3<Integer, Integer, Integer>>() {
3 O Z$ M. @, Z/ A$ g$ u3 h+ t, Q1 P* |4 W Z" h' P, a
@Override
8 H9 H5 _+ }7 V0 J' O% u# R. @ M) p: U/ q9 e9 M
public void processElement(Tuple3<Integer, Integer, Integer> value, Context ctx, Collector<Tuple3<Integer, Integer, Integer>> out) throws Exception {/ l( L* c$ ?& G2 J0 D( {
: x* y4 f3 h& {+ W: r
2 Z, r+ N6 ~4 N/ t- _& J
' N' a# }, q5 N& t; B* ^! w if (value.f0 == 0) {3 K5 @& h# ?! G4 U" _3 B4 A8 V
k0 X$ n& A+ _; H1 J2 K7 N+ Z$ r ctx.output(zeroStream, value);: t1 {5 P2 h7 ~* ?; u' l* d
% l" W! w5 Q' Z# H } else if (value.f0 == 1) {
; l! q2 c4 n, s: @/ N" Q% O1 A/ o0 X6 P& F1 P: T+ n
ctx.output(oneStream, value);
$ ^& W. q" A6 o$ j: K4 i
8 L+ p# O; Q6 m }
' Z% n- n" q& |. V! ?, H6 S* {4 p. M" `, }4 J8 A
}+ U+ e: a5 d0 e1 K; [( w
0 T3 B1 _' m$ y2 g4 F5 [/ b
});# a J; ^5 z4 ?: Y
+ }& N+ A/ F x) M! R1 g( s' Z6 x! K" _% E, L
& g* ^, r4 a& i! p DataStream<Tuple3<Integer, Integer, Integer>> zeroSideOutput = processStream.getSideOutput(zeroStream);
$ ^ d: u2 B8 G/ a+ j" c# u/ A% n# z$ a; X- n
DataStream<Tuple3<Integer, Integer, Integer>> oneSideOutput = processStream.getSideOutput(oneStream);# r, a0 {* G5 _) j! |( F
; A% D0 j' _: D$ t' j8 B
2 l4 c5 S+ X% X+ ?" E
J" j- e: |. P' q7 ? zeroSideOutput.print();
( i8 q5 \# H, k& H, E& I5 A5 {
U% y" ?4 L! V# R oneSideOutput.printToErr();
1 h- I V4 b# C4 N4 y6 N" k2 d% `9 o( ?9 h4 G1 p
: S1 }7 `2 }0 c8 P& c* ]3 L
0 ?8 B7 n6 ~/ n' F
) v* n8 a6 F7 e, D" Y9 m; r) K% j- g" T3 G& P; C
//打印结果9 t1 }) Y( q* X' q
3 m, z/ ?$ g0 t( R String jobName = "user defined streaming source";
# p2 @: b6 C& O9 R; H
' }4 @% B4 D# G/ b4 N: P; E env.execute(jobName);* }# G1 y! K. w& W
. i8 X% F) ], |% I1 F8 q' L
}
6 G1 w- X. Y1 e" k: ^' w4 h</code></pre>2 i& i) r3 n% v. }
<p>可以看到,我们将流进行了拆分,并且成功打印出了结果。这里要注意,Flink 最新提供的 SideOutPut 方式拆分流是<strong>可以多次进行拆分</strong>的,无需担心会爆出异常。</p>- v- I3 Z) A2 D3 R+ ?2 [
<p><img src="https://kingcall.oss-cn-hangzhou.aliyuncs.com/blog/img/CgqCHl7CBMKAGHoUAAM-5UL5geg132-20210223084827698.png" ></p>/ f; U! @ v* \& c; ]
<h3 id="总结">总结</h3>0 j: f% ~0 P2 I/ }" d! V b
<p>这一课时我们讲解了 Flink 的一个小的知识点,是我们生产实践中经常遇到的场景,Flink 在最新的版本中也推荐我们使用 SideOutPut 进行流的拆分。</p>+ t3 n# x# h5 I" L- `/ l8 [
<blockquote>
# U$ ?. M5 C. i<p>关注公众号:<code>大数据技术派</code>,回复<code>资料</code>,领取<code>1024G</code>资料。</p># r6 ~# C$ Q7 Q: I$ d* N% B! M
</blockquote>, @( l6 a3 Q. r4 d7 |
6 e) V+ U' F* Y1 q. ?( _) ?
|
|