From 944a3a78802fc9476b0513cee996d0069fd529e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E7=A5=A5?= <1366971433@qq.com> Date: Thu, 31 Oct 2019 11:01:51 +0800 Subject: [PATCH] flink sink --- .../java/com/heibaiying/CustomSinkJob.java | 4 +- .../com/heibaiying/KafkaStreamingJob.java | 6 +- ...linkToMySQL.java => FlinkToMySQLSink.java} | 4 +- notes/Flink_Data_Sink.md | 138 +++++++++++++++++- notes/Flink_Windows.md | 31 +++- pictures/flink-richsink.png | Bin 0 -> 8777 bytes 6 files changed, 170 insertions(+), 13 deletions(-) rename code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/sink/{FlinkToMySQL.java => FlinkToMySQLSink.java} (87%) create mode 100644 pictures/flink-richsink.png diff --git a/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/CustomSinkJob.java b/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/CustomSinkJob.java index 14861bc..56bd251 100644 --- a/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/CustomSinkJob.java +++ b/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/CustomSinkJob.java @@ -1,7 +1,7 @@ package com.heibaiying; import com.heibaiying.bean.Employee; -import com.heibaiying.sink.FlinkToMySQL; +import com.heibaiying.sink.FlinkToMySQLSink; import org.apache.flink.streaming.api.datastream.DataStreamSource; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; @@ -17,7 +17,7 @@ public class CustomSinkJob { new Employee("hei", 10, date), new Employee("bai", 20, date), new Employee("ying", 30, date)); - streamSource.addSink(new FlinkToMySQL()); + streamSource.addSink(new FlinkToMySQLSink()); env.execute(); } } diff --git a/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/KafkaStreamingJob.java b/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/KafkaStreamingJob.java index d26da5f..9977be2 100644 --- a/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/KafkaStreamingJob.java +++ b/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/KafkaStreamingJob.java @@ -20,7 +20,7 @@ public class KafkaStreamingJob { // 1.指定Kafka的相关配置属性 Properties properties = new Properties(); - properties.setProperty("bootstrap.servers", "192.168.200.229:9092"); + properties.setProperty("bootstrap.servers", "192.168.200.0:9092"); // 2.接收Kafka上的数据 DataStream stream = env @@ -35,7 +35,9 @@ public class KafkaStreamingJob { }; // 4. 定义Flink Kafka生产者 FlinkKafkaProducer kafkaProducer = new FlinkKafkaProducer<>("flink-stream-out-topic", - kafkaSerializationSchema, properties, FlinkKafkaProducer.Semantic.AT_LEAST_ONCE, 5); + kafkaSerializationSchema, + properties, + FlinkKafkaProducer.Semantic.AT_LEAST_ONCE, 5); // 5. 将接收到输入元素*2后写出到Kafka stream.map((MapFunction) value -> value + value).addSink(kafkaProducer); env.execute("Flink Streaming"); diff --git a/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/sink/FlinkToMySQL.java b/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/sink/FlinkToMySQLSink.java similarity index 87% rename from code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/sink/FlinkToMySQL.java rename to code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/sink/FlinkToMySQLSink.java index aa3e064..0a86e61 100644 --- a/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/sink/FlinkToMySQL.java +++ b/code/Flink/flink-kafka-integration/src/main/java/com/heibaiying/sink/FlinkToMySQLSink.java @@ -8,7 +8,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; -public class FlinkToMySQL extends RichSinkFunction { +public class FlinkToMySQLSink extends RichSinkFunction { private PreparedStatement stmt; private Connection conn; @@ -16,7 +16,7 @@ public class FlinkToMySQL extends RichSinkFunction { @Override public void open(Configuration parameters) throws Exception { Class.forName("com.mysql.cj.jdbc.Driver"); - conn = DriverManager.getConnection("jdbc:mysql://192.168.200.229:3306/employees?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false", "root", "123456"); + conn = DriverManager.getConnection("jdbc:mysql://192.168.0.229:3306/employees?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false", "root", "123456"); String sql = "insert into emp(name, age, birthday) values(?, ?, ?)"; stmt = conn.prepareStatement(sql); } diff --git a/notes/Flink_Data_Sink.md b/notes/Flink_Data_Sink.md index 939ce8a..218d2f7 100644 --- a/notes/Flink_Data_Sink.md +++ b/notes/Flink_Data_Sink.md @@ -1,4 +1,10 @@ # Flink Sink + ## 一、Data Sinks @@ -55,6 +61,8 @@ public DataStreamSink writeAsText(String path, WriteMode writeMode) { streamSource.writeToSocket("192.168.0.226", 9999, new SimpleStringSchema()); ``` + + ## 二、Streaming Connectors 除了上述 API 外,Flink 中还内置了系列的 Connectors 连接器,用于将计算结果输入到常用的存储系统或者消息中间件中,具体如下: @@ -77,14 +85,47 @@ streamSource.writeToSocket("192.168.0.226", 9999, new SimpleStringSchema()); 这里接着在 Data Sources 章节介绍的整合 Kafka Source 的基础上,将 Kafka Sink 也一并进行整合,具体步骤如下。 + + ## 三、整合 Kafka Sink ### 3.1 addSink +Flink 提供了 addSink 方法用来调用自定义的 Sink 或者第三方的连接器,想要将计算结果写出到 Kafka,需要使用该方法来调用 Kafka 的生产者 FlinkKafkaProducer,具体代码如下: + +```java +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + +// 1.指定Kafka的相关配置属性 +Properties properties = new Properties(); +properties.setProperty("bootstrap.servers", "192.168.200.0:9092"); + +// 2.接收Kafka上的数据 +DataStream stream = env + .addSource(new FlinkKafkaConsumer<>("flink-stream-in-topic", new SimpleStringSchema(), properties)); + +// 3.定义计算结果到 Kafka ProducerRecord 的转换 +KafkaSerializationSchema kafkaSerializationSchema = new KafkaSerializationSchema() { + @Override + public ProducerRecord serialize(String element, @Nullable Long timestamp) { + return new ProducerRecord<>("flink-stream-out-topic", element.getBytes()); + } +}; +// 4. 定义Flink Kafka生产者 +FlinkKafkaProducer kafkaProducer = new FlinkKafkaProducer<>("flink-stream-out-topic", + kafkaSerializationSchema, + properties, + FlinkKafkaProducer.Semantic.AT_LEAST_ONCE, 5); +// 5. 将接收到输入元素*2后写出到Kafka +stream.map((MapFunction) value -> value + value).addSink(kafkaProducer); +env.execute("Flink Streaming"); +``` + ### 3.2 创建输出主题 +创建用于输出测试的主题: + ```shell -# 创建用于测试的输出主题 bin/kafka-topics.sh --create \ --bootstrap-server hadoop001:9092 \ --replication-factor 1 \ @@ -97,23 +138,116 @@ bin/kafka-topics.sh --create \ ### 3.3 启动消费者 +启动一个 Kafka 消费者,用于查看 Flink 程序的输出情况: + ```java bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic flink-stream-out-topic ``` ### 3.4 测试结果 +在 Kafka 生产者上发送消息到 Flink 程序,观察 Flink 程序转换后的输出情况,具体如下: +
+ + +可以看到 Kafka 生成者发出的数据已经被 Flink 程序正常接收到,并经过转换后又输出到 Kafka 对应的 Topic 上。 ## 四、自定义 Sink +除了使用内置的第三方连接器外,Flink 还支持使用自定义的 Sink 来满足多样化的输出需求。想要实现自定义的 Sink ,需要直接或者间接实现 SinkFunction 接口。通常情况下,我们都是实现其抽象类 RichSinkFunction,相比于 SinkFunction ,其提供了更多的与生命周期相关的方法。两者间的关系如下: + +
+ + +这里我们以自定义一个 FlinkToMySQLSink 为例,将计算结果写出到 MySQL 数据库中,具体步骤如下: + ### 4.1 导入依赖 +首先需要导入 MySQL 相关的依赖: + +```xml + + mysql + mysql-connector-java + 8.0.16 + +``` + ### 4.2 自定义 Sink -### 4.3 测试结果 +继承自 RichSinkFunction,实现自定义的 Sink : + +```java +public class FlinkToMySQLSink extends RichSinkFunction { + + private PreparedStatement stmt; + private Connection conn; + + @Override + public void open(Configuration parameters) throws Exception { + Class.forName("com.mysql.cj.jdbc.Driver"); + conn = DriverManager.getConnection("jdbc:mysql://192.168.0.229:3306/employees" + + "?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false", + "root", + "123456"); + String sql = "insert into emp(name, age, birthday) values(?, ?, ?)"; + stmt = conn.prepareStatement(sql); + } + + @Override + public void invoke(Employee value, Context context) throws Exception { + stmt.setString(1, value.getName()); + stmt.setInt(2, value.getAge()); + stmt.setDate(3, value.getBirthday()); + stmt.executeUpdate(); + } + + @Override + public void close() throws Exception { + super.close(); + if (stmt != null) { + stmt.close(); + } + if (conn != null) { + conn.close(); + } + } + +} +``` + +### 4.3 使用自定义 Sink + +想要使用自定义的 Sink,同样是需要调用 addSink 方法,具体如下: + +```java +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); +Date date = new Date(System.currentTimeMillis()); +DataStreamSource streamSource = env.fromElements( + new Employee("hei", 10, date), + new Employee("bai", 20, date), + new Employee("ying", 30, date)); +streamSource.addSink(new FlinkToMySQLSink()); +env.execute(); +``` + +### 4.4 测试结果 + +启动程序,观察数据库写入情况: + +
+ + +数据库成功写入,代表自定义 Sink 整合成功。 + +> 以上所有用例的源码见本仓库:[flink-kafka-integration]( https://github.com/heibaiying/BigData-Notes/tree/master/code/Flink/flink-kafka-integration) +## 参考资料 +1. data-sinks: https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/datastream_api.html#data-sinks +2. Streaming Connectors:https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/index.html +3. Apache Kafka Connector: https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/kafka.html diff --git a/notes/Flink_Windows.md b/notes/Flink_Windows.md index 40deba1..022772b 100644 --- a/notes/Flink_Windows.md +++ b/notes/Flink_Windows.md @@ -1,4 +1,13 @@ # Flink Windows + ## 一、窗口概念 @@ -12,7 +21,9 @@ Time Windows 用于以时间为维度来进行数据聚合,具体分为以下 滚动窗口 (Tumbling Windows) 是指彼此之间没有重叠的窗口。例如:每隔1小时统计过去1小时内的商品点击量,那么 1 天就只能分为 24 个窗口,每个窗口彼此之间是不存在重叠的,具体如下: -![flink-tumbling-windows](D:\BigData-Notes\pictures\flink-tumbling-windows.png) +
+ + 这里我们以词频统计为例,给出一个具体的用例,代码如下: @@ -34,13 +45,19 @@ env.execute("Flink Streaming"); 测试结果如下: -![flink-window-word-count](D:\BigData-Notes\pictures\flink-window-word-count.png) +
+ + + + ### 2.2 Sliding Windows 滑动窗口用于滚动进行聚合分析,例如:每隔 6 分钟统计一次过去一小时内所有商品的点击量,那么统计窗口彼此之间就是存在重叠的,即 1天可以分为 240 个窗口。图示如下: -![flink-sliding-windows](D:\BigData-Notes\pictures\flink-sliding-windows.png) +
+ + 可以看到 window 1 - 4 这四个窗口彼此之间都存在着时间相等的重叠部分。想要实现滑动窗口,只需要在使用 timeWindow 方法时额外传递第二个参数作为滚动时间即可,具体如下: @@ -53,7 +70,9 @@ timeWindow(Time.minutes(1),Time.seconds(3)) 当用户在进行持续浏览时,可能每时每刻都会有点击数据,例如在活动区间内,用户可能频繁的将某类商品加入和移除购物车,而你只想知道用户本次浏览最终的购物车情况,此时就可以在用户持有的会话结束后再进行统计。想要实现这类统计,可以通过 Session Windows 来进行实现。 -![flink-session-windows](D:\BigData-Notes\pictures\flink-session-windows.png) +
+ + 具体的实现代码如下: @@ -68,7 +87,9 @@ window(EventTimeSessionWindows.withGap(Time.seconds(10))) 最后一个窗口是全局窗口, 全局窗口会将所有 key 相同的元素分配到同一个窗口中,其通常配合触发器 (trigger) 进行使用。如果没有相应触发器,则计算将不会被执行。 -![flink-non-windowed](D:\BigData-Notes\pictures\flink-non-windowed.png) +
+ + 这里继续以上面词频统计的案例为例,示例代码如下: diff --git a/pictures/flink-richsink.png b/pictures/flink-richsink.png new file mode 100644 index 0000000000000000000000000000000000000000..9ab26effc2077dd2cc7c8d6d70e209da8e304382 GIT binary patch literal 8777 zcmbW7cU%+ew)cY|3J7eGCS3?3QY{pbBArN6AfXdbI?|hz1Q8UG9;AziAT0z`I)Ow1 zsUn?FMVf>f=^6;R0o~{9d+t8(dGGz>`OM10ta-{Xv%c%MCi<3vHX}V3Jpce;1nFoP z0RU78N)taxOX*oulOaG>lh@Ot_(k(w&S_&3ADByhfJWUYdmZnZT&1oj zoVL@2ThtU_by62XLT|_C>v)}>dB!%?e){C(ak^)47+%c$_)h2y!;%c}X{bje9h#~3 zVj@BA{6mGejhq)GuHKMJjO1S?@Go1JH=@5(IpJorgOq`+Aprs3lqzO}x)-kGjI!2F zt|?Oiq;EbRIt~DAaH}2zNIw#w0;H=Y0swmFbO3;YKY##$@ktiI15p|}z|l?*A6 z>t1)ELfFpcX{kJC6cpVU5rLAL!#y6O-R7Llo91KUST59$vsk2eEp)0-<`nU#qM;Mt zE+d+3K6sr$Kj#`aE#$z}+hNp+Gkc7H8a!y1 zf}~Nm`~l>=64S}w$3VU%>pa#wRl`q2=M& z$%@lUtHEt@_Z#1ah%|1OtZUVOdpb<4Lb^xIjg}6dTB82+^<-1}^`NWy$=d3O{IYm4 z1&Y0F>aj>nu{<;>J#?F|2PVD2lf2)wLfK4y_{snAgcK4Ivi;=Cl#Pnlqp2}2o-GS7 z_!05p_FHy+NQ$^|omf$JR@U9*daj*fYqkyXo>Jxar!0V+zDOI%I8wu$c7p4NWM-t- z!Pbb|0AFUNG*n@g;IE6Ah;fXqFUyx3uMpso9G*}shCwTQd{)0n_CJq|o?K!S9gd@& zV^Mv0$#-@sO~HRFbY=HPPL%HwK4`L*IR9o-akul1aaG)-(KQxO_T5U^8hY!+g=6o+ z1wN!NlD0B~rxY@_s_@^ZB0RGPUu|e#^_)J%t9QwBFuR=a5&lBa;Z7&+-Rn}8oq08=>E&Xe$J9Uuv()SR zxc0MJg*%uv)1W-VC~8dmlOp0v*|Lt%vq~WiV*=O!xoQ_MxiXhokxRm|vf|=PebT0e zHe^-F^s=)?D$w24ZJ>3e9m(D}on+${Gnn>f#VcTkI`L-s`083k{mBvB(h!pEQ~~y_ zCFP+DzSFUyI#6A@ziXB9$j-#X1phvq8j=>Ss1`#rt2U5@t*EB~gx7wrgos)-Gn0}&R6N9~z z@UH9E-_;TYJcZGB5;}Lq0uQefm3KNwY?Lj;1Ou%b4;r^sp`qL8_cC3%{WEypW5GQ_ z$r~sr#TfXJFQwdcE#Qmn$Qjj#4apsc4%SBw&`=kljn8rzCG#|8qHdEP4v|66OE&KR z>1ji96~B3SA znj3r9pN{h!8t%xyvX6zN0g_L$oS0>n4;$I8c;_asz-Vpxe~2M}8RdT)kwm@FlQ0NJ zl2{D8#Jl=PZ?~nq#~v{D>iSsk;=DW|WY?^`Ufis7)jj^GI{Rhzj;2QpC`r4CnFP5y zE1oMnQN`Xo=hHmfK4~Bs+N<1fW#-n$-g}?4tniPa0VIa}KjYxY!8L@*%^Yt=B55oo zYe~U(=#}eN6P~Cy)NQmKooQG({BA-5lxK+l`$QYxvjve&VURoar^!MOT=~^*RP#ro zi*X+0W40kt4enm#ek;Ka_^GC!ZP1{{5oT*YoF0oQ_sY>8w?JSEe?>Jz=ML95{mwOE z4ct;~xzF~^Ru zf*v_4vD8i6s?nNIy0{31gUHLZ64)NIz>hrp#?%R-S% zaB-#(2dptpt>n+Y^7lA%b}Pq4eO*%23slkwx}B6-3O=J-)8kn zO;)raGnGhT@|c$0$HPt!Yuma5f6#jK%BRTzZRvn|)I5+A2f!^PD| zp#%M~kI&ZRpBWJr%4&n|;hTo0d5 zrI?hSn@ypMet}ltz^J+;*~S9})c3(fTSp{Meo2WM>;v(c+<@?+2I+l@m7AON^!7vg z9HtY;-iCR4mh;_aT-~NNg{Y_HfM9u$cY5<>F9SS0;`NtYM+yVL?G=g@#8PR#1ob@M zgke*Nt0qi3Kym->c01;q5(ysAW>DJ`ybFAO;~LXu@~j2$KS%QPl5S~*szB&1F`T&Z z?dH1JGNY#HJKYj`Q#IKJ{aMe=6H!DX9}X9TrWTLw$E?d3&V<%&?~!+!G7==yK@6|O z*z?9d&?B21df{vxZ$&SjZz64#Iv!Reqr&(X2_!fr>RV%d#8&qVc$LbzO|d)yFBWXf zODXMMy%xyGH;<_UIX1uO6el-sjmX|!Xdf9~faHpitRU@Kv$u>)1IzQ(y1Qqzam(cc zPl+!(L-m8--d`aXLU^L;rqkI$ghJfl-f%~Lxn`6#U+8i^dy2}dAOoUQ(%~Lpn{SVG^MAfFO{^jx|lA&#)-JMN7IM5uJh3h zJJ`&rsMm{co})j%!yhFZX6AX-$D8LH0>!VR;p2-XLxG@$ur!qu7gyTAvc?EjS90>l zl=tG}f$ny1=#o!$i^6mee(PJ{Np$nPKnE^TA0^Anij?C1G8qObG$1i?G9-N zWPIMW|5YD=zw|Rs1-I>J*|{;U=vQC_`6Npe=aKpF?bS*d7EL4&yWVlFrT6Hp z*w?wJqze|pKjQ?vADA4OoD9VlLboe#ZgAveQs=wIY4lzEV5T#4bTRBnmMmS|q5072 z=Ft(6^P}aSq_rFHDTK~P^ymnJ&vXQ2p#!g%_O7>-zf6~1$f}1=$3zB&Th(UG$oolT zWJr@F+w8ua)48`cqivdO0IgrYPQ-sLylchyd}(G;3{;3t{bbU!-bLwn7gr|ni!r-^ z%WG-}lvLR4v4;eO1Oa9R&K=@N-5czG0GTw?jYh%iVNuQJe52S7*$J@XSJpW zyn7_zeW9KeV9)S7Tedw*3mc(UJ@)?cDdL+G4{07aaBkT#(?qNHy?(I%K$U;z#{Y1f zx#cGcXlMVObN`DY9iU4Z-^@qZ=vTT_vs4kvE8+hb@lVH=T)Yt=QQ|kT{2uWij(}9V z-YBI%mvJ<^e|0>Z9YDbH&;0-Qh<`c)#G79-hoq->J(9AhypR?XMF)isw=G;T@b~jG zvu;ZFmdF4hTWEd81kG!`>?Apu&m2)04e-O%sg2tPmOzBx>R6Rk`7wa)xwWLfC=TMy zzCg-;0N$Jus-`T9%J%FFp|?kS_G9B#Nc+$4QAGLY>0><^i|415 zlYn3qaxJ9Ms-zIUU-X4E)JC68(j9Z?hOMXu!7oD*gF9J<{s$`yc_pHhtj{outE!*v z7cpF=)t8!B=&NSm+osW~@l0{EwScylGgdJ!U5btT38{ZLwm4eK9Q$cU^ZZ`<5wVr2 z4j_hXU-x;ae5JX2fP1qvZ$#RUE8HGJT^}PYohH>!S7K@psty}`(#rx3ReDwKG1=EM zb>D&#(Rc&Q{{%PkI`RrJ0h-;%Y-9FvuORj^H-(<8;JozpCYVxtzb;NKN{IimQx3j8 zl39lKTPAGdiVNNui7EOk0Lueb&6Eqaw~sG_jm^*Q?Un}iq(6Q#l*63Z)2gV0+5I$8 z+HGmeiieR!i2ED*F!;}cGMjz10Ve}qu=Qpt_-44!*f`hV^G=6&rCd&}!eilyCV0L| z=2LmbdyOf<)jlyo<*nHmQe6|}KH7qCkgr~a6A3-NQU@;PYPjKKu%z`$ecAhI1Mo0w zp;fEUCI(gL`VDET7T#&9QP6$=2VCj<{xM!9FGyOwhrN}dyj5xE%{0li>mvTU_=iZN z6k*1t#n>FAh7|Ec{R4wR=Cu;@dZm~8Y}CHmQ2q(`+5R=za$vC`wY9UR8)*z&X-ICY zx=A1kGA#bWhh^_hVafKhlplsz~q~?6WDRk_=>L^0=EWZQ_@_;x<+fQ%~ z{PP{lDuG8nDQ%DIb#Q_!vDq%oGb&Y9+kpLWab|d&l;S#!m_*)LSub%koU?C0UY5?E z3j3;IbZdIx`Xe~0slwb^czLk5mZA`xl}`$?P%0T^$p~BAc_1r`GA;@;Kh$7wrnzUf z`A`X1W;;%!$IDV%vP)ibJehyo>2p7MxMobtq}&RzIeO3Y(9grV@CG{}{Mny^&zUsO z0P?9C(r*=~T4xCg8E5LKFh`voT(H;Fbv%7u)^_oBDABC^OXz$J@ooFvVfLpzV_ z?a+~WW2$Z4*5HtmdFI~GiN~jx$Igy5vP~UZJfuH1SC4%ZtLf{D`V$t5#<#BLiKO~Y z)?Y}YpG?99@7zC81-Gz_i1=a&2IpdI8;d^x{sd3wIYPHRX?@SrnPddiVgB!^`J-+` zMRXe)xiT@qlf`;X`QltOy8EgX%%Jt~=Kb^q1`;B-b8y(FBt@=&$Ui?C`#ew#;cFw6 zWotGamcTStoXiyyax~_O&#S3ldPs`awV5K7c3_9JMJ4K;X5VxEF$p0MEcb;O$(P~v z4y^lfh_x+63Pej4rMRUQ7foAfZ9BP|~&(61c^35LR1dm^HNN#B5aD-che2xY1F zpT+4AXPLvUh;3>p?RL)eaTrthShBcRrnlYno_OW=tqh2p8ksXR6*rPJJ1GTY(p$*VmmOCIj3ZK-lz z?w#C=@;H}{v1!`G@zmiZN2brJRP zG=3kJhmgzh&ejnud}b4a!v%5P+sriwgVqrlmtqZ8VUy(>T9o`JG${nMt?F)-e9(CN z+m0b9bJeqtn{TnIS_ekg74YqXkG$TYT-G(zWLj>6vX$PB`-m?>xm*$Kfm>mNW6E{H z^T2;UU`1XX83ghwj zMvB^ZnE$(=A-o_rUTB!}Vi^noh(8-(@5s2~G< zwNU#7kspz-%YWb*BSvL=VnWE9^X>*I6MTShOu+&vCPn9t60^A6wZxKmoF=;YQ}8=T za)H4>e(}K7|E=D(k9dPacb2&zKcgNX?y$#8(O~gS82U+PI*s%xY|@Q zKTDG3uE*5i$Gc=PJK{s<@T`Oxd=d%lgH520oys6iOo*n<)0fBn1-<$US8hAde2MEJH8AYl9lz5v6IRnhYMdGcCth2wwo#QRy=vBIRn6z zSs#kKvRV|4?cZ0JI1d=~pWEY$`Bg50h~FD~{B{iv6nz2jgDL-867c$4LsbcGoup)8 zkS_eHp@e=8e(SAGj2LyI6a|xgly>AS*89(7DtTtYr?`x|urNVT3H-nt6?H8dXwh2MU;OUl7-cF75YOr_wti&qMle0re97!gX|Z6feTf9K7br%lr% zphLZ6%CeZ01l4NXrxETlSwoEN%DC08i(In|d*Mag5t4b>l6x%b`aC@JF?`*yi>1E$ zMXAr?9i0DhrofVS1@iNk`7(ChmB3e^$IsB4U<5C_w>IxChc9s8@!=KqgZ_6-svb2D zm2B7heIN&mYs{eG#Agj|!d49s#yA_c9Wvi4*V5DO+pjoCNoz>uM}9?RkiF}Si&iy~ zH!oUN7-)8Pl($A%l5gP5kLqCRsX1?8eUh&RH-T9y7&pn^(%uvX5j&jYN_Mt3wyMe0 zMUOX-#;?7J_1g<`#@X&G3QelCt57nt?EgKyF(K4E^O&?h+tKZcP{0ISQAnm)(hc>j z4Mu<22IDmvwU=iJ5uA?)RZgNNa2JL`Oa|^a{|Qv?5y7$Sjkgyd>UpM=If2mW{U|7+ zGt|wm=yRTXc0}3~o1CGz#vG2;FaVusgu=E-Kb}${ji)xab?Kl<9rd&^e=VJc%bv3@(x~%h_TH4QVL+VZ80?hk zG8y!e%tH=&)tdP-3{1_oJCh_VO$PeIG*`!SUw4mkXPe-+i*NNuv-+QJ9DQ+obV3DG z7jsp_?oLgx`cwbB4_};<quU>>Wygh6g2->T1ONk97)-T}0_bVoaN6oSM z6o3Og%FJri)UvSJR!#YtxpilKL}e?Zo0RTrPDK2uHV7-n9rwn4G$%` z5|eqI&4N1cYDsS^FLejBs=4r8B2>S-xq-P_6_-lQOHuydyYK`^!ewe#aSFeXQ@^i_f9=J51uNwU5<*r6^tg+LdLhbj%DM zho!pOB$dC`^%SG?suA1@nW6`cO5S<#MyN2t$>sv51k>v=$HoRn3J$e1kk;Q72+xix z7_a7xd|yhH7&B)pc1xkvIxGqz9LawX=QiW6SDStfK<|}G9;>Se-gZ46QS5n7OWR#P z>k=5@M_Q7=2p&%@DJtp>r#?|CNiB3Ere)fIe|d;f?B94D2hfTW2w(d`w>&c5fu*Jg z^iMlp{~uMXrtW|f>B_MBAz{FqOBO)cz;B<7V~=Ue<=K5_WF^3+dq ZNe{!rv+qBCpnL`b0BIU%l&RT1{4WO$`yv1U literal 0 HcmV?d00001