├── .gitignore ├── .idea └── workspace.xml ├── README.MD ├── bigdata-project.iml ├── design-pattern └── pom.xml ├── docs ├── clickhouse │ ├── ClickHouse.md │ ├── ClickHouse教程.md │ ├── ClickHouse数据结构.md │ ├── tableEngine.assets │ │ └── 640.png │ ├── tableEngine.md │ ├── 如何选择clickhouse表引擎.assets │ │ └── p88863.png │ └── 如何选择clickhouse表引擎.md ├── flink │ ├── DataFlow模型.md │ ├── Flink1.12nativekubernetes演进.assets │ │ ├── v2-899169586ff422599670187f97f74afd_1440w.jpg │ │ └── v2-975fc1b7cf5f31d647b7dacec219ea91_1440w.jpg │ ├── Flink1.12nativekubernetes演进.md │ ├── FlinkMemory.assets │ │ └── image-20200921183651039.png │ ├── FlinkMemory.md │ ├── FlinkStreamingJoins.assets │ │ ├── image-20201213005039152.png │ │ └── image-20201213005141832.png │ ├── FlinkStreamingJoins.md │ ├── FlinkTaskExecutor内存管理.assets │ │ ├── image-20200920223508792.png │ │ ├── image-20200920223853718.png │ │ ├── image-20200920230448872.png │ │ ├── image-20200921183651039.png │ │ ├── image-20200921222741670.png │ │ └── image-20200921222757296.png │ ├── FlinkTaskExecutor内存管理.md │ ├── Flink运行架构.assets │ │ ├── levels_of_abstraction.svg │ │ ├── stsy_0101.png │ │ ├── stsy_0108.png │ │ └── stsy_0209.png │ ├── TheDataflowModel.assets │ │ ├── 0f52d5e6a64a8a674ea6eb55682fb24f83b804b1.png │ │ ├── 2fe1c87966fcb64ed2cca1d1225dee783cc4f120.png │ │ ├── 3bbfa2fcb679ad0113d61a16ab01a76af3a93ae2.png │ │ ├── 5b0d71485571ac800df9f74f9b319aef806caadc.png │ │ ├── 705b075f87873b510f6cf756c56e4be60abe95e9.png │ │ ├── 878725ea9e43603b3397b4526e8aa09c1cab4176.png │ │ ├── 8aa1145584f91c77fb72fd4bc7258888c0a446a5.png │ │ ├── 8c97ec8a78996471d2a0ea641af155e14f350439.png │ │ ├── a0dda89c8f0a7215d5724eccd3f20e24721e47ae.png │ │ ├── a6bd33d762f58b36116e9cbb8625a824d53079cc.png │ │ ├── aacd4ced7363029902de9c084837de5176470d24.png │ │ ├── cf8214259842bea68979d4dc6129233bb20fccc3.png │ │ ├── d8fb8487d3588c7e51091ed1ae29f7add2585b93.png │ │ ├── dca2de195328bc1b812f1d9427c7eec6d12f2427.png │ │ ├── e50e00a21d07a779231ed8f4ab69ef13848daf38.png │ │ └── f35cf3a130d58de559fb03d13242323f79d24624.png │ ├── TheDataflowModel.md │ ├── Time与Watermark.assets │ │ ├── image-20201214180052040.png │ │ ├── image-20201214180808722.png │ │ ├── image-20201214180930910.png │ │ ├── image-20201214191539402.png │ │ ├── spaf_0309.png │ │ └── stsy_0209-20201214172457146.png │ ├── Time与Watermark.md │ ├── ValueState中存Map与MapState有什么区别.md │ ├── Window.assets │ │ ├── image-20201215114037836.png │ │ ├── image-20201215154505904.png │ │ ├── image-20201215172027772.png │ │ ├── image-20201215190623569.png │ │ ├── image-20201215190654537.png │ │ ├── image-20201215235014908.png │ │ ├── image-20201215235056407.png │ │ ├── image-20201215235435915.png │ │ ├── image-20201216000133111.png │ │ ├── image-20210103103848320.png │ │ ├── image-20210103103925904.png │ │ └── stsy_0108.png │ ├── Window.md │ ├── 流初级基础.assets │ │ ├── image-20201212200654705.png │ │ ├── image-20201212200700868.png │ │ ├── image-20201212200812245.png │ │ ├── image-20201212201051293.png │ │ ├── image-20201212201644857.png │ │ ├── image-20201212201733956.png │ │ ├── spaf_0206.png │ │ ├── spaf_0207.png │ │ ├── spaf_0208.png │ │ ├── spaf_0209.png │ │ ├── spaf_0210.png │ │ ├── spaf_0211.png │ │ ├── spaf_0212.png │ │ └── spaf_0213.png │ └── 流初级基础.md ├── scala │ ├── 函数式编程.md │ ├── 类型参数化.md │ ├── 隐式转换Implicit.md │ └── 面向对象的Scala.md ├── verseline │ ├── Untitled 1.md │ └── 对风说爱你.md └── 深度学习推荐系统 │ └── 03深度学习基础.md ├── flink-demo ├── pom.xml └── src │ └── main │ └── scala │ ├── Person.scala │ ├── Test.scala │ └── com │ └── bigdata │ └── flink │ └── datastream │ ├── step │ └── Check.scala │ ├── util │ ├── Constants.scala │ ├── DateUtil.scala │ └── UserEventCount.scala │ └── windowing │ └── EventTimeTriggerDemo.scala ├── pom.xml └── scala-demo ├── pom.xml └── src └── main └── scala └── com └── yit └── data ├── func ├── FunctionDemo01.scala ├── FunctionDemo02.scala ├── FunctionDemo03.scala └── PartialFunctionDemo01.scala ├── implicit_demo ├── Double2IntImplicitTest.scala ├── ExpandLibraryTest.scala ├── ImplicitParameter.scala ├── ImplicitParameter2.scala └── IntWrapper.scala └── variance └── Cell.scala /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Scala template 3 | *.class 4 | *.log 5 | 6 | .idea 7 | *.iml 8 | 9 | target/ 10 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # 大数据常用框架技术笔记 2 | 3 | ## Scala 4 | ### Scala基础 5 | [面向对象的Scala](docs/scala/面向对象的Scala.md) 6 | 7 | [函数式的Scala](docs/scala/函数式编程.md) 8 | 9 | [集合]() 10 | 11 | ### Scala高级 12 | [隐式转换Implicit](docs/scala/隐式转换Implicit.md) 13 | 14 | [类型参数化](docs/scala/类型参数化.md) 15 | 16 | ## Flink 17 | 18 | ### 基础 19 | [流处理基础](docs/flink/流初级基础.md) 20 | 21 | [DataFlow模型](docs/flink/DataFlow模型.md) 22 | 23 | [The Dataflow Model](docs/flink/TheDataflowModel.md) 24 | 25 | ### Flink DataStream API实践原理 26 | [1. Time与Watermark](docs/flink/Time与Watermark.md) 27 | 28 | [2. Window](docs/flink/Window.md) 29 | 30 | [3. Transform]() 31 | 32 | [4. Process Function]() 33 | 34 | [5. CEP]() 35 | 36 | ### 状态与一致性 37 | 38 | 39 | [Streaming Joins](docs/flink/FlinkStreamingJoins.md) 40 | 41 | [Flink TaskExecutor内存管理](docs/flink/FlinkTaskExecutor内存管理.md) 42 | 43 | ### 社区 44 | [Flink1.12 native kubernetes 演进](docs/flink/Flink1.12nativekubernetes演进.md) 45 | 46 | 47 | ## ClickHouse 48 | 49 | [1.ClickHouse简介及安装](docs/clickhouse/ClickHouse.md) 50 | 51 | [2.ClickHouse表引擎](docs/clickhouse/tableEngine.md) 52 | 53 | [3.ClickHouse的数据结构](docs/clickhouse/ClickHouse数据结构.md) 54 | 55 | [如何选择clickhouse表引擎](docs/clickhouse/如何选择clickhouse表引擎.md) 56 | 57 | - 企业实践 58 | 59 | [ClickHouse 在字节跳动的技术应用与实践-直播回放](https://www.ixigua.com/6853991019050959371/) 60 | 61 | [1.字节跳动ClickHouse在用户增长分析场景的应用](https://mp.weixin.qq.com/s/J0j0LM3ZDj4OP7SyYCWCEQ) 62 | 63 | [2.沙龙回顾|ClickHouse 在字节广告 DMP& CDP 的应用](https://mp.weixin.qq.com/s/lYjIfKS8k9ZHPrxBRYOBrw) 64 | 65 | [3.沙龙回顾|ClickHouse 在实时场景的应用和优化](https://mp.weixin.qq.com/s/hqUCFSr8cu3x3u8HCA6WYg) 66 | 67 | 68 | ## Streaming System 69 | 70 | - Streaming System 71 | 72 | [Streaming 101: The world beyond batch](https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-101) 73 | 74 | [Streaming 102: The world beyond batch](https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-102) 75 | 76 | [Streaming Systems](https://www.oreilly.com/library/view/streaming-systems/9781491983867/?_ga=2.214328721.704251868.1607675381-1314152331.1607675381) 77 | 78 | ## 数仓 79 | 80 | ## 实时数仓 81 | [ValueState中存Map与MapState有什么区别](docs/flink/ValueState中存Map与MapState有什么区别.md) 82 | 83 | [网易游戏基于 Flink 的流式 ETL 建设](https://mp.weixin.qq.com/s/R4dpdSGNzgE-uvlvo09ulA) 84 | 85 | ## 对酒当歌 86 | [对风说爱你](docs/verseline /对风说爱你.md) 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /bigdata-project.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /design-pattern/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | bigdata-project 7 | com.yit.data 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | design-pattern 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/clickhouse/ClickHouse.md: -------------------------------------------------------------------------------- 1 | * [ClickHouse简介及安装](#clickhouse简介及安装) 2 | * [ClickHouse简介](#clickhouse简介) 3 | * [基于Docker安装ClickHouse单机版](#基于docker安装clickhouse单机版) 4 | 5 | # ClickHouse简介及安装 6 | 7 | ## ClickHouse简介 8 | 9 | ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)。ClickHouse最初是一款名为Yandex.Metrica的产品,主要用于WEB流量分析。ClickHouse的全称是**Click Stream,Data WareHouse**,简称ClickHouse。 10 | 11 | 12 | 13 | ClickHouse非常适用于商业智能领域,除此之外,它也能够被广泛应用于广告流量、Web、App流量、电信、金融、电子商务、信息安全、网络游戏、物联网等众多其他领域。ClickHouse具有以下特点: 14 | 15 | - 支持完备的SQL操作 16 | 17 | - 列式存储与数据压缩 18 | 19 | - 向量化执行引擎 20 | 21 | - 关系型模型(与传统数据库类似) 22 | 23 | - 丰富的表引擎 24 | 25 | - 并行处理 26 | 27 | - 在线查询 28 | 29 | - 数据分片 30 | 31 | 32 | 33 | ClickHouse作为一款高性能OLAP数据库,存在以下不足。 34 | 35 | - 不支持事务。 36 | 37 | - 不擅长根据主键按行粒度进行查询(虽然支持),故不应该把ClickHouse当作Key-Value数据库使用。 38 | 39 | - 不擅长按行删除数据(虽然支持) 40 | 41 | 42 | 43 | ## 基于Docker安装ClickHouse单机版 44 | 45 | 1. **直接运行, docker会自动帮你拉取镜像**: 46 | 47 | ```shell 48 | docker run -d --name ch-server --ulimit nofile=262144:262144 -p 8123:8123 -p 9000:9000 -p 9009:9009 yandex/clickhouse-server 49 | ``` 50 | 51 | > -d 代表后台运行 --name 自定义ck的服务名称 -p:容器端口映射到当前主机端口 不指定默认http端口是8123,tcp端口是9000 52 | 53 | 2. **查看镜像** 54 | 55 | ```shell 56 | # docker images 57 | REPOSITORY TAG IMAGE ID CREATED SIZE 58 | docker.io/yandex/clickhouse-server latest c601d506867f 2 weeks ago 809 MB 59 | docker.io/yandex/clickhouse-client latest ba91f385ceea 2 weeks ago 485 MB 60 | docker.io/wurstmeister/kafka 2.11-0.11.0.3 2b1f874807ac 3 months ago 413 MB 61 | docker.io/mysql 5.6.39 079344ce5ebd 2 years ago 256 MB 62 | docker.io/wurstmeister/zookeeper 3.4.6 6fe5551964f5 4 years ago 451 MB 63 | docker.io/training/webapp latest 6fae60ef3446 5 years ago 349 MB 64 | ``` 65 | 66 | 67 | 68 | 3. **进入ClickHouse容器** 69 | 70 | ```shell 71 | docker exec -it d00724297352 /bin/bash 72 | ``` 73 | 74 | - 需要注意的是, 默认的容器是一个依赖包不完整的ubuntu虚拟机 75 | 76 | - 所以我们需要安装vim 77 | 78 | ```shell 79 | apt-get update 80 | apt-get install vim -y 81 | 82 | ``` 83 | 84 | - clickhouse-server目录并查看目录 85 | 86 | ```shell 87 | cd /etc/clickhouse-server 88 | 89 | # 查看目录 90 | root@d00724297352:/etc/clickhouse-server# ll 91 | total 52 92 | drwx------ 1 clickhouse clickhouse 4096 Dec 6 06:06 ./ 93 | drwxr-xr-x 1 root root 4096 Dec 6 05:24 ../ 94 | dr-x------ 1 clickhouse clickhouse 4096 Nov 20 15:29 config.d/ 95 | -r-------- 1 clickhouse clickhouse 38407 Nov 19 10:34 config.xml 96 | dr-x------ 2 clickhouse clickhouse 4096 Nov 20 15:29 users.d/ 97 | -r-------- 1 clickhouse clickhouse 5688 Dec 6 06:06 users.xml 98 | ``` 99 | 100 | - - 修改clickhouse的用户密码需要在users.xml中配置 101 | 102 | - - 需要注意的是: 密码必须为加密过的形式, 否则会一直连不上。 103 | 104 | - - 我们这次采用SHA256的方式加密 105 | 106 | ```shell 107 | PASSWORD=$(base64 < /dev/urandom | head -c8); echo "你的密码"; echo -n "你的密码" | sha256sum | tr -d '-' 108 | 109 | ``` 110 | 111 | - 执行以上命令后会在命令行打印密码明文和密码密文, 如下 112 | 113 | ```shell 114 | 123456 115 | 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 116 | ``` 117 | 118 | - - vim user.xml修改用户密码 119 | 120 | ```shell 121 | 122 | 123 | 124 | 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 125 | 126 | ::/0 127 | 128 | default 129 | default 130 | 131 | 132 | 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 133 | 134 | ::/0 135 | 136 | readonly 137 | default 138 | 139 | 140 | ``` 141 | 142 | 4. Clickhouse-client连接 143 | 144 | ```shell 145 | docker run -it --rm --link ch-server:clickhouse-server yandex/clickhouse-client --host clickhouse-server --password 123456 146 | 147 | ``` 148 | 149 | 因为客户端每次使用完不用一直常驻所以这里使用--rm 参数 在使用exit 命令退出liucf-clickhouse-client容器后就会直接删除这个容器,下次启动重新创建就可以了 150 | 151 | 注意:这里使用了 docker --link 命令,可以让一个容器和另一个容器很方便的连接起来,其参数部分的ch-server表示要连接的容器真实名称。 152 | 153 | - **配置clickhouse-client启动命令** 154 | 155 | 把上述命名放在一个文件中,比如start-ch-client.sh, chmod +x ,再放到/usr/local/bin下,就可以直接用了 156 | 157 | ```shell 158 | vim start-ch-client.sh 159 | 160 | docker run -it --rm --link ch-server:clickhouse-server yandex/clickhouse-client --host clickhouse-server --password 123456 161 | ``` 162 | 163 | 164 | 165 | 3. 测试 166 | 167 | - 查看数据库 168 | 169 | ```shell 170 | e38fc4946689 :) show databases 171 | 172 | SHOW DATABASES 173 | 174 | Query id: d1cb9abc-ef45-4d45-ac48-86db60f45001 175 | 176 | ┌─name───────────────────────────┐ 177 | │ _temporary_and_external_tables │ 178 | │ default │ 179 | │ system │ 180 | └────────────────────────────────┘ 181 | 182 | 3 rows in set. Elapsed: 0.003 sec. 183 | ``` 184 | 185 | - 创建表并插入一条数据 186 | 187 | ```shell 188 | CREATE TABLE test (FlightDate Date,Year UInt16) ENGINE = MergeTree(FlightDate, (Year, FlightDate), 8192); 189 | 190 | insert into test (FlightDate,Year) values('2020-06-05',2001); 191 | 192 | ``` 193 | 194 | - 查询数据 195 | 196 | ```shell 197 | SELECT * FROM test 198 | 199 | Query id: a729c322-0bf0-41d4-8c2d-b06fc14d1124 200 | 201 | ┌─FlightDate─┬─Year─┐ 202 | │ 2020-06-05 │ 2001 │ 203 | └────────────┴──────┘ 204 | 205 | 1 rows in set. Elapsed: 0.004 sec. 206 | ``` 207 | -------------------------------------------------------------------------------- /docs/clickhouse/ClickHouse教程.md: -------------------------------------------------------------------------------- 1 | # ClickHouse概述 2 | 3 | ## 什么是ClickHouse 4 | 5 | ClickHouse是俄罗斯**Yandex**于2016年开源的一个用于**联机分析(OLAP)**的**列式数据库管理系统**(DBMS),主要用于在线分析处理查询(OLAP),能够使用**SQL**查询实时生成分析数据报告。 6 | 7 | ClickHouse的全称是Click Stream Data WareHouse,简称ClickHouse。 8 | 9 | ## OLAP场景的特点 10 | 11 | ### 读多于写 12 | 13 | 不同于事务处理(OLTP)的场景,比如电商场景中加购物车、下单、支付等需要在原地进行大量insert、update、delete操作,数据分析(OLAP)场景通常是将数据批量导入后,进行任意维度的灵活探索、BI工具洞察、报表制作等。数据一次性写入后,分析师需要尝试从各个角度对数据做挖掘、分析,直到发现其中的商业价值、业务变化趋势等信息。这是一个需要反复试错、不断调整、持续优化的过程,其中数据的读取次数远多于写入次数。这就要求底层数据库为这个特点做专门设计,而不是盲目采用传统数据库的技术架构。 14 | 15 | ### 大宽表,读大量行但是少量列,结果集较小 16 | 17 | 在OLAP场景中,通常存在一张或是几张多列的大宽表,列数高达数百甚至数千列。对数据分析处理时,选择其中的少数几列作为维度列、其他少数几列作为指标列,然后对全表或某一个较大范围内的数据做聚合计算。这个过程会扫描大量的行数据,但是只用到了其中的少数列。而聚合计算的结果集相比于动辄数十亿的原始数据,也明显小得多。 18 | 19 | ### 数据批量写入,且数据不更新或少更新 20 | 21 | OLTP类业务对于延时(Latency)要求更高,要避免让客户等待造成业务损失;而OLAP类业务,由于数据量非常大,通常更加关注写入吞吐(Throughput),要求海量数据能够尽快导入完成。一旦导入完成,历史数据往往作为存档,不会再做更新、删除操作。 22 | 23 | ### 无需事务,数据一致性要求低 24 | 25 | OLAP类业务对于事务需求较少,通常是导入历史日志数据,或搭配一款事务型数据库并实时从事务型数据库中进行数据同步。多数OLAP系统都支持最终一致性。 26 | 27 | ### 灵活多变,不适合预先建模 28 | 29 | 分析场景下,随着业务变化要及时调整分析维度、挖掘方法,以尽快发现数据价值、更新业务指标。而数据仓库中通常存储着海量的历史数据,调整代价十分高昂。预先建模技术虽然可以在特定场景中加速计算,但是无法满足业务灵活多变的发展需求,维护成本过高。 30 | 31 | -------------------------------------------------------------------------------- /docs/clickhouse/ClickHouse数据结构.md: -------------------------------------------------------------------------------- 1 | * [ClickHouse的数据结构](#clickhouse的数据结构) 2 | * [数值类型](#数值类型) 3 | * [Int类型](#int类型) 4 | * [浮点类型](#浮点类型) 5 | * [Decimal类型](#decimal类型) 6 | * [字符串类型](#字符串类型) 7 | * [String](#string) 8 | * [FixedString](#fixedstring) 9 | * [UUID](#uuid) 10 | * [日期类型](#日期类型) 11 | * [Date类型](#date类型) 12 | * [DateTime类型](#datetime类型) 13 | * [布尔类型](#布尔类型) 14 | * [数组类型](#数组类型) 15 | * [枚举类型](#枚举类型) 16 | * [Tuple类型](#tuple类型) 17 | * [特殊数据类型](#特殊数据类型) 18 | * [Nullable](#nullable) 19 | * [Domain](#domain) 20 | 21 | 22 | # ClickHouse的数据结构 23 | 24 | ClickHouse提供了许多数据类型,它们可以划分为基础类型、复合类型和特殊类型。我们可以在`system.data_type_families`表中检查数据类型名称以及是否区分大小写。 25 | 26 | ``` 27 | SELECT * FROM system.data_type_families 28 | ``` 29 | 30 | 上面的系统表,存储了ClickHouse所支持的数据类型,注意不同版本的ClickHouse可能数据类型会有所不同,具体如下表所示: 31 | 32 | ```sql 33 | ┌─name────────────────────┬─case_insensitive─┬─alias_to────┐ 34 | │ IPv6 │ 0 │ │ 35 | │ IPv4 │ 0 │ │ 36 | │ LowCardinality │ 0 │ │ 37 | │ Decimal │ 1 │ │ 38 | │ String │ 0 │ │ 39 | │ Decimal64 │ 1 │ │ 40 | │ Decimal32 │ 1 │ │ 41 | │ Decimal128 │ 1 │ │ 42 | │ Float64 │ 0 │ │ 43 | │ Float32 │ 0 │ │ 44 | │ Int64 │ 0 │ │ 45 | │ SimpleAggregateFunction │ 0 │ │ 46 | │ Array │ 0 │ │ 47 | │ Nothing │ 0 │ │ 48 | │ UInt16 │ 0 │ │ 49 | │ Enum16 │ 0 │ │ 50 | │ UInt32 │ 0 │ │ 51 | │ Date │ 1 │ │ 52 | │ Int8 │ 0 │ │ 53 | │ Int32 │ 0 │ │ 54 | │ Enum8 │ 0 │ │ 55 | │ UInt64 │ 0 │ │ 56 | │ IntervalSecond │ 0 │ │ 57 | │ Int16 │ 0 │ │ 58 | │ FixedString │ 0 │ │ 59 | │ Nullable │ 0 │ │ 60 | │ AggregateFunction │ 0 │ │ 61 | │ DateTime │ 1 │ │ 62 | │ Enum │ 0 │ │ 63 | │ Tuple │ 0 │ │ 64 | │ IntervalMonth │ 0 │ │ 65 | │ Nested │ 0 │ │ 66 | │ IntervalMinute │ 0 │ │ 67 | │ IntervalHour │ 0 │ │ 68 | │ IntervalWeek │ 0 │ │ 69 | │ IntervalDay │ 0 │ │ 70 | │ UInt8 │ 0 │ │ 71 | │ IntervalQuarter │ 0 │ │ 72 | │ UUID │ 0 │ │ 73 | │ IntervalYear │ 0 │ │ 74 | │ LONGBLOB │ 1 │ String │ 75 | │ MEDIUMBLOB │ 1 │ String │ 76 | │ TINYBLOB │ 1 │ String │ 77 | │ BIGINT │ 1 │ Int64 │ 78 | │ SMALLINT │ 1 │ Int16 │ 79 | │ TIMESTAMP │ 1 │ DateTime │ 80 | │ INTEGER │ 1 │ Int32 │ 81 | │ INT │ 1 │ Int32 │ 82 | │ DOUBLE │ 1 │ Float64 │ 83 | │ MEDIUMTEXT │ 1 │ String │ 84 | │ TINYINT │ 1 │ Int8 │ 85 | │ DEC │ 1 │ Decimal │ 86 | │ BINARY │ 1 │ FixedString │ 87 | │ FLOAT │ 1 │ Float32 │ 88 | │ CHAR │ 1 │ String │ 89 | │ VARCHAR │ 1 │ String │ 90 | │ TEXT │ 1 │ String │ 91 | │ TINYTEXT │ 1 │ String │ 92 | │ LONGTEXT │ 1 │ String │ 93 | │ BLOB │ 1 │ String │ 94 | └─────────────────────────┴──────────────────┴─────────────┘ 95 | ``` 96 | 97 | ## 数值类型 98 | 99 | ### Int类型 100 | 101 | 固定长度的整数类型又包括有符号和无符号的整数类型。 102 | 103 | - 有符号整数类型 104 | 105 | | 类型 | 字节 | 范围 | 106 | | :----- | :--- | :----------------- | 107 | | Int8 | 1 | [-2^7 ~2^7-1] | 108 | | Int16 | 2 | [-2^15 ~ 2^15-1] | 109 | | Int32 | 4 | [-2^31 ~ 2^31-1] | 110 | | Int64 | 8 | [-2^63 ~ 2^63-1] | 111 | | Int128 | 16 | [-2^127 ~ 2^127-1] | 112 | | Int256 | 32 | [-2^255 ~ 2^255-1] | 113 | 114 | - 无符号类型 115 | 116 | | 类型 | 字节 | 范围 | 117 | | :------ | :--- | :------------ | 118 | | UInt8 | 1 | [0 ~2^8-1] | 119 | | UInt16 | 2 | [0 ~ 2^16-1] | 120 | | UInt32 | 4 | [0 ~ 2^32-1] | 121 | | UInt64 | 8 | [0 ~ 2^64-1] | 122 | | UInt256 | 32 | [0 ~ 2^256-1] | 123 | 124 | ### 浮点类型 125 | 126 | - 单精度浮点数 127 | 128 | Float32从小数点后第8位起会发生数据溢出 129 | 130 | | 类型 | 字节 | 精度 | 131 | | :------ | :--- | :--- | 132 | | Float32 | 4 | 7 | 133 | 134 | - 双精度浮点数 135 | 136 | Float32从小数点后第17位起会发生数据溢出 137 | 138 | | 类型 | 字节 | 精度 | 139 | | :------ | :--- | :--- | 140 | | Float64 | 8 | 16 | 141 | 142 | - 示例 143 | 144 | ``` 145 | -- Float32类型,从第8为开始产生数据溢出 146 | kms-1.apache.com :) select toFloat32(0.123456789); 147 | 148 | SELECT toFloat32(0.123456789) 149 | 150 | ┌─toFloat32(0.123456789)─┐ 151 | │ 0.12345679 │ 152 | └────────────────────────┘ 153 | -- Float64类型,从第17为开始产生数据溢出 154 | kms-1.apache.com :) select toFloat64(0.12345678901234567890); 155 | 156 | SELECT toFloat64(0.12345678901234568) 157 | 158 | ┌─toFloat64(0.12345678901234568)─┐ 159 | │ 0.12345678901234568 │ 160 | └────────────────────────────────┘ 161 | ``` 162 | 163 | ### Decimal类型 164 | 165 | 有符号的定点数,可在加、减和乘法运算过程中保持精度。ClickHouse提供了Decimal32、Decimal64和Decimal128三种精度的定点数,支持几种写法: 166 | 167 | - Decimal(P, S) 168 | 169 | - Decimal32(S) 170 | 171 | **数据范围:( -1 \* 10^(9 - S), 1 \* 10^(9 - S) )** 172 | 173 | - Decimal64(S) 174 | 175 | **数据范围:( -1 \* 10^(18 - S), 1 \* 10^(18 - S) )** 176 | 177 | - Decimal128(S) 178 | 179 | **数据范围:( -1 \* 10^(38 - S), 1 \* 10^(38 - S) )** 180 | 181 | - Decimal256(S) 182 | 183 | **数据范围:( -1 \* 10^(76 - S), 1 \* 10^(76 - S) )** 184 | 185 | 其中:**P**代表精度,决定总位数(整数部分+小数部分),取值范围是1~76 186 | 187 | **S**代表规模,决定小数位数,取值范围是0~P 188 | 189 | 根据**P**的范围,可以有如下的等同写法: 190 | 191 | | P 取值 | 原生写法示例 | 等同于 | 192 | | :---------- | :------------ | :------------ | 193 | | [ 1 : 9 ] | Decimal(9,2) | Decimal32(2) | 194 | | [ 10 : 18 ] | Decimal(18,2) | Decimal64(2) | 195 | | [ 19 : 38 ] | Decimal(38,2) | Decimal128(2) | 196 | | [ 39 : 76 ] | Decimal(76,2) | Decimal256(2) | 197 | 198 | **注意点**:不同精度的数据进行四则运算时,**精度(总位数)和规模(小数点位数)**会发生变化,具体规则如下: 199 | 200 | - 精度对应的规则 201 | 202 | **可以看出:两个不同精度的数据进行四则运算时,结果数据已最大精度为准** 203 | 204 | - - Decimal64(S1) `运算符` Decimal32(S2) -> Decimal64(S) 205 | - Decimal128(S1) `运算符` Decimal32(S2) -> Decimal128(S) 206 | - Decimal128(S1) `运算符` Decimal64(S2) -> Decimal128(S) 207 | - Decimal256(S1) `运算符` Decimal<32|64|128>(S2) -> Decimal256(S) 208 | 209 | - 规模(小数点位数)对应的规则 210 | 211 | - - 加法|减法:S = max(S1, S2),即以两个数据中小数点位数最多的为准 212 | - 乘法:S = S1 + S2(注意:S1精度 >= S2精度),即以两个数据的小数位相加为准 213 | 214 | - 除法:S = S1,即被除数的小数位为准 215 | 216 | ``` 217 | -- toDecimal32(value, S) 218 | -- 加法,S取两者最大的,P取两者最大的 219 | SELECT 220 | toDecimal64(2, 3) AS x, 221 | toTypeName(x) AS xtype, 222 | toDecimal32(2, 2) AS y, 223 | toTypeName(y) as ytype, 224 | x + y AS z, 225 | toTypeName(z) AS ztype; 226 | -- 结果输出 227 | ┌─────x─┬─xtype──────────┬────y─┬─ytype─────────┬─────z─┬─ztype──────────┐ 228 | │ 2.000 │ Decimal(18, 3) │ 2.00 │ Decimal(9, 2) │ 4.000 │ Decimal(18, 3) │ 229 | └───────┴────────────────┴──────┴───────────────┴───────┴────────────────┘ 230 | -- 乘法,比较特殊,与这两个数的顺序有关 231 | -- 如下:x类型是Decimal64,y类型是Decimal32,顺序是x*y,小数位S=S1+S2 232 | SELECT 233 | toDecimal64(2, 3) AS x, 234 | toTypeName(x) AS xtype, 235 | toDecimal32(2, 2) AS y, 236 | toTypeName(y) as ytype, 237 | x * y AS z, 238 | toTypeName(z) AS ztype; 239 | -- 结果输出 240 | ┌─────x─┬─xtype──────────┬────y─┬─ytype─────────┬───────z─┬─ztype──────────┐ 241 | │ 2.000 │ Decimal(18, 3) │ 2.00 │ Decimal(9, 2) │ 4.00000 │ Decimal(18, 5) │ 242 | └───────┴────────────────┴──────┴───────────────┴─────────┴────────────────┘ 243 | -- 交换相乘的顺序,y*x,小数位S=S1*S2 244 | SELECT 245 | toDecimal64(2, 3) AS x, 246 | toTypeName(x) AS xtype, 247 | toDecimal32(2, 2) AS y, 248 | toTypeName(y) as ytype, 249 | y * x AS z, 250 | toTypeName(z) AS ztype; 251 | -- 结果输出 252 | ┌─────x─┬─xtype──────────┬────y─┬─ytype─────────┬────────z─┬─ztype──────────┐ 253 | │ 2.000 │ Decimal(18, 3) │ 2.00 │ Decimal(9, 2) │ 0.400000 │ Decimal(18, 6) │ 254 | └───────┴────────────────┴──────┴───────────────┴──────────┴────────────────┘ 255 | -- 除法,小数位与被除数保持一致 256 | SELECT 257 | toDecimal64(2, 3) AS x, 258 | toTypeName(x) AS xtype, 259 | toDecimal32(2, 2) AS y, 260 | toTypeName(y) as ytype, 261 | x / y AS z, 262 | toTypeName(z) AS ztype; 263 | -- 结果输出 264 | ┌─────x─┬─xtype──────────┬────y─┬─ytype─────────┬─────z─┬─ztype──────────┐ 265 | │ 2.000 │ Decimal(18, 3) │ 2.00 │ Decimal(9, 2) │ 1.000 │ Decimal(18, 3) │ 266 | └───────┴────────────────┴──────┴───────────────┴───────┴────────────────┘ 267 | ``` 268 | 269 | ## 字符串类型 270 | 271 | ### String 272 | 273 | 字符串可以是任意长度的。它可以包含任意的字节集,包含空字节。因此,字符串类型可以代替其他 DBMSs 中的VARCHAR、BLOB、CLOB 等类型。 274 | 275 | ### FixedString 276 | 277 | 固定长度的`N`字节字符串,一般在在一些明确字符串长度的场景下使用,声明方式如下: 278 | 279 | ``` 280 | -- N表示字符串的长度 281 | FixedString(N) 282 | ``` 283 | 284 | 值得注意的是:FixedString使用null字节填充末尾字符。 285 | 286 | ``` 287 | -- 虽然hello只有5位,但最终字符串是6位 288 | select toFixedString('hello',6) as a,length(a) as alength; 289 | -- 结果输出 290 | 291 | ┌─a─────┬─alength─┐ 292 | │ hello │ 6 │ 293 | └───────┴─────────┘ 294 | -- 注意对于固定长度的字符串进行比较时,会出现不一样的结果 295 | -- 如下:由于a是6位字符,而hello是5位,所以两者不相等 296 | select toFixedString('hello',6) as a,a = 'hello' ,length(a) as alength; 297 | -- 结果输出 298 | ┌─a─────┬─equals(toFixedString('hello', 6), 'hello')─┬─alength─┐ 299 | │ hello │ 0 │ 6 │ 300 | └───────┴────────────────────────────────────────────┴─────────┘ 301 | 302 | -- 需要使用下面的方式 303 | select toFixedString('hello',6) as a,a = 'hello\0' ,length(a) as alength; 304 | -- 结果输出 305 | 306 | ┌─a─────┬─equals(toFixedString('hello', 6), 'hello\0')─┬─alength─┐ 307 | │ hello │ 1 │ 6 │ 308 | └───────┴──────────────────────────────────────────────┴─────────┘ 309 | ``` 310 | 311 | ### UUID 312 | 313 | UUID是一种数据库常见的主键类型,在ClickHouse中直接把它作为一种数据类型。UUID共有32位,它的格式为8-4-4-4-12,比如: 314 | 315 | ``` 316 | 61f0c404-5cb3-11e7-907b-a6006ad3dba0 317 | -- 当不指定uuid列的值时,填充为0 318 | 00000000-0000-0000-0000-000000000000 319 | ``` 320 | 321 | 使用示例如下: 322 | 323 | ``` 324 | -- 建表 325 | CREATE TABLE t_uuid (x UUID, y String) ENGINE=TinyLog; 326 | -- insert数据 327 | INSERT INTO t_uuid SELECT generateUUIDv4(), 'Example 1'; 328 | INSERT INTO t_uuid (y) VALUES ('Example 2'); 329 | SELECT * FROM t_uuid; 330 | -- 结果输出,默认被填充为0 331 | ┌────────────────────────────────────x─┬─y─────────┐ 332 | │ b6b019b5-ee5c-4967-9c4d-8ff95d332230 │ Example 1 │ 333 | │ 00000000-0000-0000-0000-000000000000 │ Example 2 │ 334 | └──────────────────────────────────────┴───────────┘ 335 | ``` 336 | 337 | ## 日期类型 338 | 339 | 时间类型分为DateTime、DateTime64和Date三类。需要注意的是ClickHouse目前没有时间戳类型,也就是说,时间类型最高的精度是秒,所以如果需要处理毫秒、微秒精度的时间,则只能借助UInt类型实现。 340 | 341 | ### Date类型 342 | 343 | 用两个字节存储,表示从 1970-01-01 (无符号) 到当前的日期值。日期中没有存储时区信息。 344 | 345 | ``` 346 | CREATE TABLE t_date (x date) ENGINE=TinyLog; 347 | INSERT INTO t_date VALUES('2020-10-01'); 348 | SELECT x,toTypeName(x) FROM t_date; 349 | ┌──────────x─┬─toTypeName(x)─┐ 350 | │ 2020-10-01 │ Date │ 351 | └────────────┴───────────────┘ 352 | ``` 353 | 354 | ### DateTime类型 355 | 356 | 用四个字节(无符号的)存储 Unix 时间戳。允许存储与日期类型相同的范围内的值。最小值为 0000-00-00 00:00:00。时间戳类型值精确到秒(没有闰秒)。时区使用启动客户端或服务器时的系统时区。 357 | 358 | ``` 359 | CREATE TABLE t_datetime(`timestamp` DateTime) ENGINE = TinyLog; 360 | INSERT INTO t_datetime Values('2020-10-01 00:00:00'); 361 | SELECT * FROM t_datetime; 362 | -- 结果输出 363 | ┌───────────timestamp─┐ 364 | │ 2020-10-01 00:00:00 │ 365 | └─────────────────────┘ 366 | -- 注意,DateTime类型是区分时区的 367 | SELECT 368 | toDateTime(timestamp, 'Asia/Shanghai') AS column, 369 | toTypeName(column) AS x 370 | FROM t_datetime; 371 | -- 结果输出 372 | ┌──────────────column─┬─x─────────────────────────┐ 373 | │ 2020-10-01 00:00:00 │ DateTime('Asia/Shanghai') │ 374 | └─────────────────────┴───────────────────────────┘ 375 | SELECT 376 | toDateTime(timestamp, 'Europe/Moscow') AS column, 377 | toTypeName(column) AS x 378 | FROM t_datetime; 379 | -- 结果输出 380 | ┌──────────────column─┬─x─────────────────────────┐ 381 | │ 2020-09-30 19:00:00 │ DateTime('Europe/Moscow') │ 382 | └─────────────────────┴───────────────────────────┘ 383 | ``` 384 | 385 | ## 布尔类型 386 | 387 | ClickHouse没有单独的类型来存储布尔值。可以使用UInt8 类型,取值限制为0或 1。 388 | 389 | ## 数组类型 390 | 391 | Array(T),由 T 类型元素组成的数组。T 可以是任意类型,包含数组类型。但不推荐使用多维数组,ClickHouse对多维数组的支持有限。例如,不能在MergeTree表中存储多维数组。 392 | 393 | ``` 394 | SELECT array(1, 2) AS x, toTypeName(x); 395 | -- 结果输出 396 | ┌─x─────┬─toTypeName(array(1, 2))─┐ 397 | │ [1,2] │ Array(UInt8) │ 398 | └───────┴─────────────────────────┘ 399 | SELECT [1, 2] AS x, toTypeName(x); 400 | -- 结果输出 401 | ┌─x─────┬─toTypeName([1, 2])─┐ 402 | │ [1,2] │ Array(UInt8) │ 403 | └───────┴────────────────────┘ 404 | ``` 405 | 406 | 需要注意的是,数组元素中如果存在Null值,则元素类型将变为Nullable。 407 | 408 | ``` 409 | SELECT array(1, 2, NULL) AS x, toTypeName(x); 410 | -- 结果输出 411 | ┌─x──────────┬─toTypeName(array(1, 2, NULL))─┐ 412 | │ [1,2,NULL] │ Array(Nullable(UInt8)) │ 413 | └────────────┴───────────────────────────────┘ 414 | ``` 415 | 416 | 另外,数组类型里面的元素必须具有相同的数据类型,否则会报异常 417 | 418 | ``` 419 | SELECT array(1, 'a') 420 | -- 报异常 421 | DB::Exception: There is no supertype for types UInt8, String because some of them are String/FixedString and some of them are not 422 | ``` 423 | 424 | ## 枚举类型 425 | 426 | 枚举类型通常在定义常量时使用,ClickHouse提供了Enum8和Enum16两种枚举类型。 427 | 428 | ``` 429 | -- 建表 430 | CREATE TABLE t_enum 431 | ( 432 | x Enum8('hello' = 1, 'world' = 2) 433 | ) 434 | ENGINE = TinyLog; 435 | -- INSERT数据 436 | INSERT INTO t_enum VALUES ('hello'), ('world'), ('hello'); 437 | -- 如果定义了枚举类型值之后,不能写入其他值的数据 438 | INSERT INTO t_enum values('a') 439 | -- 报异常:Unknown element 'a' for type Enum8('hello' = 1, 'world' = 2) 440 | ``` 441 | 442 | ## Tuple类型 443 | 444 | Tuple(T1, T2, ...),元组,与Array不同的是,Tuple中每个元素都有单独的类型,不能在表中存储元组(除了内存表)。它们可以用于临时列分组。在查询中,IN表达式和带特定参数的 lambda 函数可以来对临时列进行分组。 445 | 446 | ``` 447 | SELECT tuple(1,'a') AS x, toTypeName(x); 448 | --结果输出 449 | ┌─x───────┬─toTypeName(tuple(1, 'a'))─┐ 450 | │ (1,'a') │ Tuple(UInt8, String) │ 451 | └─────────┴───────────────────────────┘ 452 | -- 建表 453 | CREATE TABLE t_tuple( 454 | c1 Tuple(String,Int8) 455 | ) engine=TinyLog; 456 | -- INSERT数据 457 | INSERT INTO t_tuple VALUES(('jack',20)); 458 | --查询数据 459 | SELECT * FROM t_tuple; 460 | ┌─c1──────────┐ 461 | │ ('jack',20) │ 462 | └─────────────┘ 463 | -- 如果插入数据类型不匹配,会报异常 464 | INSERT INTO t_tuple VALUES(('tom','20')); 465 | -- Type mismatch in IN or VALUES section. Expected: Int8. Got: String 466 | ``` 467 | 468 | ## 特殊数据类型 469 | 470 | ### Nullable 471 | 472 | Nullable类型表示某个基础数据类型可以是Null值。其具体用法如下所示: 473 | 474 | ``` 475 | -- 建表 476 | CREATE TABLE t_null(x Int8, y Nullable(Int8)) ENGINE TinyLog 477 | -- 写入数据 478 | INSERT INTO t_null VALUES (1, NULL), (2, 3); 479 | SELECT x + y FROM t_null; 480 | -- 结果 481 | ┌─plus(x, y)─┐ 482 | │ ᴺᵁᴸᴸ │ 483 | │ 5 │ 484 | └────────────┘ 485 | ``` 486 | 487 | ### Domain 488 | 489 | Domain类型是特定实现的类型: 490 | 491 | IPv4是与UInt32类型保持二进制兼容的Domain类型,用于存储IPv4地址的值。它提供了更为紧凑的二进制存储的同时支持识别可读性更加友好的输入输出格式。 492 | 493 | IPv6是与FixedString(16)类型保持二进制兼容的Domain类型,用于存储IPv6地址的值。它提供了更为紧凑的二进制存储的同时支持识别可读性更加友好的输入输出格式。 494 | 495 | 注意低版本的ClickHouse不支持此类型。 496 | 497 | ``` 498 | -- 建表 499 | CREATE TABLE hits 500 | (url String, 501 | from IPv4 502 | ) ENGINE = MergeTree() 503 | ORDER BY from; 504 | -- 写入数据 505 | INSERT INTO hits (url, from) VALUES 506 | ('https://wikipedia.org', '116.253.40.133') 507 | ('https://clickhouse.tech', '183.247.232.58'); 508 | 509 | -- 查询 510 | SELECT * FROM hits; 511 | ┌─url─────────────────────┬───────────from─┐ 512 | │ https://wikipedia.org │ 116.253.40.133 │ 513 | │ https://clickhouse.tech │ 183.247.232.58 │ 514 | └─────────────────────────┴────────────────┘ 515 | ``` -------------------------------------------------------------------------------- /docs/clickhouse/tableEngine.assets/640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/clickhouse/tableEngine.assets/640.png -------------------------------------------------------------------------------- /docs/clickhouse/如何选择clickhouse表引擎.assets/p88863.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/clickhouse/如何选择clickhouse表引擎.assets/p88863.png -------------------------------------------------------------------------------- /docs/clickhouse/如何选择clickhouse表引擎.md: -------------------------------------------------------------------------------- 1 | * [如何选择clickhouse表引擎](#如何选择clickhouse表引擎) 2 | * [背景信息](#背景信息) 3 | * [ClickHouse表引擎概览](#clickhouse表引擎概览) 4 | * [Log系列](#log系列) 5 | * [Integration系列](#integration系列) 6 | * [Special系列](#special系列) 7 | * [MergeTree系列](#mergetree系列) 8 | * [总结](#总结) 9 | 10 | 11 | # 如何选择clickhouse表引擎 12 | 13 | ## 背景信息 14 | 15 | 表引擎在ClickHouse中的作用十分关键,直接决定了数据如何存储和读取、是否支持并发读写、是否支持index、支持的query种类、是否支持主备复制等。 16 | 17 | ClickHouse提供了大约28种表引擎,各有各的用途,比如有Lo系列用来做小表数据分析,MergeTree系列用来做大数据量分析,而Integration系列则多用于外表数据集成。再考虑复制表Replicated系列,分布式表Distributed等,纷繁复杂,新用户上手选择时常常感到迷惑。 18 | 19 | 本文尝试对ClickHouse的表引擎进行梳理,帮忙大家快速入门ClickHouse。 20 | 21 | ## ClickHouse表引擎概览 22 | 23 | 下图是ClickHouse提供的所有表引擎汇总。 24 | 25 | [![CK表引擎](如何选择clickhouse表引擎.assets/p88863.png)](https://static-aliyun-doc.oss-accelerate.aliyuncs.com/assets/img/zh-CN/1523283851/p88863.png)`%ClickHouse表引擎一共分为四个系列,分别是Log、MergeTree、Integration、Special。其中包含了两种特殊的表引擎Replicated、Distributed,功能上与其他表引擎正交,根据场景组合使用。;` 26 | 27 | ## Log系列 28 | 29 | Log系列表引擎功能相对简单,主要用于快速写入小表(1百万行左右的表),然后全部读出的场景。 30 | 31 | 几种Log表引擎的共性是: 32 | 33 | - 数据被顺序append写到磁盘上。 34 | - 不支持delete、update。 35 | - 不支持index。 36 | - 不支持原子性写。 37 | - insert会阻塞select操作。 38 | 39 | 它们彼此之间的区别是: 40 | 41 | - TinyLog:不支持并发读取数据文件,查询性能较差;格式简单,适合用来暂存中间数据。 42 | - StripLog:支持并发读取数据文件,查询性能比TinyLog好;将所有列存储在同一个大文件中,减少了文件个数。 43 | - Log:支持并发读取数据文件,查询性能比TinyLog好;每个列会单独存储在一个独立文件中。 44 | 45 | ## Integration系列 46 | 47 | 该系统表引擎主要用于将外部数据导入到ClickHouse中,或者在ClickHouse中直接操作外部数据源。 48 | 49 | - Kafka:将Kafka Topic中的数据直接导入到ClickHouse。 50 | - MySQL:将Mysql作为存储引擎,直接在ClickHouse中对MySQL表进行select等操作。 51 | - JDBC/ODBC:通过指定jdbc、odbc连接串读取数据源。 52 | - HDFS:直接读取HDFS上的特定格式的数据文件; 53 | 54 | ## Special系列 55 | 56 | Special系列的表引擎,大多是为了特定场景而定制的。这里也挑选几个简单介绍,不做详述。 57 | 58 | - Memory:将数据存储在内存中,重启后会导致数据丢失。查询性能极好,适合于对于数据持久性没有要求的1亿以下的小表。在ClickHouse中,通常用来做临时表。 59 | - Buffer:为目标表设置一个内存buffer,当buffer达到了一定条件之后会flush到磁盘。 60 | - File:直接将本地文件作为数据存储。 61 | - Null:写入数据被丢弃、读取数据为空。 62 | 63 | ## MergeTree系列 64 | 65 | Log、Special、Integration主要用于特殊用途,场景相对有限。MergeTree系列才是官方主推的存储引擎,支持几乎所有ClickHouse核心功能。 66 | 67 | 以下重点介绍MergeTree、ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree、SummingMergeTree、AggregatingMergeTree引擎。 68 | 69 | ***\*MergeTree\**** 70 | 71 | MergeTree表引擎主要用于海量数据分析,支持数据分区、存储有序、主键索引、稀疏索引、数据TTL等。MergeTree支持所有ClickHouse SQL语法,但是有些功能与MySQL并不一致,比如在MergeTree中主键并不用于去重,以下通过示例说明。 72 | 73 | 如下建表DDL所示,test_tbl的主键为(id, create_time),并且按照主键进行存储排序,按照create_time进行数据分区,数据保留最近一个月。 74 | 75 | ```sql 76 | CREATE TABLE test_tbl ( 77 | id UInt16, 78 | create_time Date, 79 | comment Nullable(String) 80 | ) ENGINE = MergeTree() 81 | PARTITION BY create_time 82 | ORDER BY (id, create_time) 83 | PRIMARY KEY (id, create_time) 84 | TTL create_time + INTERVAL 1 MONTH 85 | SETTINGS index_granularity=8192; 86 | ``` 87 | 88 | 写入数据:值得注意的是这里我们写入了几条primary key相同的数据。 89 | 90 | ```sql 91 | insert into test_tbl values(0, '2019-12-12', null); 92 | insert into test_tbl values(0, '2019-12-12', null); 93 | insert into test_tbl values(1, '2019-12-13', null); 94 | insert into test_tbl values(1, '2019-12-13', null); 95 | insert into test_tbl values(2, '2019-12-14', null); 96 | ``` 97 | 98 | 查询数据: 可以看到虽然主键id、create_time相同的数据只有3条数据,但是结果却有5行。 99 | 100 | ```sql 101 | select count(*) from test_tbl; 102 | ┌─count()─┐ 103 | │ 5 │ 104 | └─────────┘ 105 | 106 | select * from test_tbl; 107 | ┌─id─┬─create_time─┬─comment─┐ 108 | │ 2 │ 2019-12-14 │ ᴺᵁᴸᴸ │ 109 | └────┴─────────────┴─────────┘ 110 | ┌─id─┬─create_time─┬─comment─┐ 111 | │ 1 │ 2019-12-13 │ ᴺᵁᴸᴸ │ 112 | └────┴─────────────┴─────────┘ 113 | ┌─id─┬─create_time─┬─comment─┐ 114 | │ 0 │ 2019-12-12 │ ᴺᵁᴸᴸ │ 115 | └────┴─────────────┴─────────┘ 116 | ┌─id─┬─create_time─┬─comment─┐ 117 | │ 1 │ 2019-12-13 │ ᴺᵁᴸᴸ │ 118 | └────┴─────────────┴─────────┘ 119 | ┌─id─┬─create_time─┬─comment─┐ 120 | │ 0 │ 2019-12-12 │ ᴺᵁᴸᴸ │ 121 | └────┴─────────────┴─────────┘ 122 | ``` 123 | 124 | 由于MergeTree采用类似LSM tree的结构,很多存储层处理逻辑直到Compaction期间才会发生。因此强制后台compaction执行完毕,再次查询,发现仍旧有5条数据。 125 | 126 | ```sql 127 | optimize table test_tbl final; 128 | 129 | 130 | select count(*) from test_tbl; 131 | ┌─count()─┐ 132 | │ 5 │ 133 | └─────────┘ 134 | 135 | select * from test_tbl; 136 | ┌─id─┬─create_time─┬─comment─┐ 137 | │ 2 │ 2019-12-14 │ ᴺᵁᴸᴸ │ 138 | └────┴─────────────┴─────────┘ 139 | ┌─id─┬─create_time─┬─comment─┐ 140 | │ 0 │ 2019-12-12 │ ᴺᵁᴸᴸ │ 141 | │ 0 │ 2019-12-12 │ ᴺᵁᴸᴸ │ 142 | └────┴─────────────┴─────────┘ 143 | ┌─id─┬─create_time─┬─comment─┐ 144 | │ 1 │ 2019-12-13 │ ᴺᵁᴸᴸ │ 145 | │ 1 │ 2019-12-13 │ ᴺᵁᴸᴸ │ 146 | └────┴─────────────┴─────────┘ 147 | ``` 148 | 149 | 结合以上示例可以看到,MergeTree虽然有主键索引,但是其主要作用是加速查询,而不是类似MySQL等数据库用来保持记录唯一。即便在Compaction完成后,主键相同的数据行也仍旧共同存在。 150 | 151 | ***\*ReplacingMergeTree\**** 152 | 153 | 为了解决MergeTree相同主键无法去重的问题,ClickHouse提供了ReplacingMergeTree引擎,用来做去重。 154 | 155 | 示例如下: 156 | 157 | ```sql 158 | -- 建表 159 | CREATE TABLE test_tbl_replacing ( 160 | id UInt16, 161 | create_time Date, 162 | comment Nullable(String) 163 | ) ENGINE = ReplacingMergeTree() 164 | PARTITION BY create_time 165 | ORDER BY (id, create_time) 166 | PRIMARY KEY (id, create_time) 167 | TTL create_time + INTERVAL 1 MONTH 168 | SETTINGS index_granularity=8192; 169 | 170 | -- 写入主键重复的数据 171 | insert into test_tbl_replacing values(0, '2019-12-12', null); 172 | insert into test_tbl_replacing values(0, '2019-12-12', null); 173 | insert into test_tbl_replacing values(1, '2019-12-13', null); 174 | insert into test_tbl_replacing values(1, '2019-12-13', null); 175 | insert into test_tbl_replacing values(2, '2019-12-14', null); 176 | 177 | -- 查询,可以看到未compaction之前,主键重复的数据,仍旧存在。 178 | select count(*) from test_tbl_replacing; 179 | ┌─count()─┐ 180 | │ 5 │ 181 | └─────────┘ 182 | 183 | select * from test_tbl_replacing; 184 | ┌─id─┬─create_time─┬─comment─┐ 185 | │ 0 │ 2019-12-12 │ ᴺᵁᴸᴸ │ 186 | └────┴─────────────┴─────────┘ 187 | ┌─id─┬─create_time─┬─comment─┐ 188 | │ 0 │ 2019-12-12 │ ᴺᵁᴸᴸ │ 189 | └────┴─────────────┴─────────┘ 190 | ┌─id─┬─create_time─┬─comment─┐ 191 | │ 1 │ 2019-12-13 │ ᴺᵁᴸᴸ │ 192 | └────┴─────────────┴─────────┘ 193 | ┌─id─┬─create_time─┬─comment─┐ 194 | │ 1 │ 2019-12-13 │ ᴺᵁᴸᴸ │ 195 | └────┴─────────────┴─────────┘ 196 | ┌─id─┬─create_time─┬─comment─┐ 197 | │ 2 │ 2019-12-14 │ ᴺᵁᴸᴸ │ 198 | └────┴─────────────┴─────────┘ 199 | 200 | 201 | -- 强制后台compaction: 202 | optimize table test_tbl_replacing final; 203 | 204 | 205 | -- 再次查询:主键重复的数据已经消失。 206 | select count(*) from test_tbl_replacing; 207 | ┌─count()─┐ 208 | │ 3 │ 209 | └─────────┘ 210 | 211 | select * from test_tbl_replacing; 212 | ┌─id─┬─create_time─┬─comment─┐ 213 | │ 2 │ 2019-12-14 │ ᴺᵁᴸᴸ │ 214 | └────┴─────────────┴─────────┘ 215 | ┌─id─┬─create_time─┬─comment─┐ 216 | │ 1 │ 2019-12-13 │ ᴺᵁᴸᴸ │ 217 | └────┴─────────────┴─────────┘ 218 | ┌─id─┬─create_time─┬─comment─┐ 219 | │ 0 │ 2019-12-12 │ ᴺᵁᴸᴸ │ 220 | └────┴─────────────┴─────────┘ 221 | ``` 222 | 223 | 虽然ReplacingMergeTree提供了主键去重的能力,但是仍旧有以下限制: 224 | 225 | - 在没有彻底optimize之前,可能无法达到主键去重的效果,比如部分数据已经被去重,而另外一部分数据仍旧有主键重复。 226 | - 在分布式场景下,相同primary key的数据可能被sharding到不同节点上,不同shard间可能无法去重。 227 | - optimize是后台动作,无法预测具体执行时间点。 228 | - 手动执行optimize在海量数据场景下要消耗大量时间,无法满足业务即时查询的需求。 229 | 230 | 因此ReplacingMergeTree更多被用于确保数据最终被去重,而无法保证查询过程中主键不重复。 231 | 232 | ***\*CollapsingMergeTree\**** 233 | 234 | ClickHouse实现了CollapsingMergeTree来消除ReplacingMergeTree的功能限制。该引擎要求在建表语句中指定一个标记列Sign,后台Compaction时会将主键相同、Sign相反的行进行折叠,也即删除。 235 | 236 | CollapsingMergeTree将行按照Sign的值分为两类:Sign=1的行称之为状态行,Sign=-1的行称之为取消行。 237 | 238 | 每次需要新增状态时,写入一行状态行;需要删除状态时,则写入一行取消行。 239 | 240 | 在后台Compaction时,状态行与取消行会自动做折叠(删除)处理。而尚未进行Compaction的数据,状态行与取消行同时存在。 241 | 242 | 因此为了能够达到主键折叠(删除)的目的,需要业务层进行适当改造: 243 | 244 | - 执行删除操作需要写入取消行,而取消行中需要包含与原始状态行主键一样的数据(Sign列除外)。所以在应用层需要记录原始状态行的值,或者在执行删除操作前先查询数据库获取原始状态行。 245 | - 由于后台Compaction时机无法预测,在发起查询时,状态行和取消行可能尚未被折叠;另外,ClickHouse无法保证primary key相同的行落在同一个节点上,不在同一节点上的数据无法折叠。因此在进行count(*)、sum(col)等聚合计算时,可能会存在数据冗余的情况。为了获得正确结果,业务层需要改写SQL,将count()、sum(col)分别改写为sum(Sign)、sum(col * Sign)。 246 | 247 | 以下用示例说明: 248 | 249 | ```sql 250 | -- 建表 251 | CREATE TABLE UAct 252 | ( 253 | UserID UInt64, 254 | PageViews UInt8, 255 | Duration UInt8, 256 | Sign Int8 257 | ) 258 | ENGINE = CollapsingMergeTree(Sign) 259 | ORDER BY UserID; 260 | 261 | -- 插入状态行,注意sign一列的值为1 262 | INSERT INTO UAct VALUES (4324182021466249494, 5, 146, 1); 263 | 264 | -- 插入一行取消行,用于抵消上述状态行。注意sign一列的值为-1,其余值与状态行一致; 265 | -- 并且插入一行主键相同的新状态行,用来将PageViews从5更新至6,将Duration从146更新为185. 266 | INSERT INTO UAct VALUES (4324182021466249494, 5, 146, -1), (4324182021466249494, 6, 185, 1); 267 | 268 | -- 查询数据:可以看到未Compaction之前,状态行与取消行共存。 269 | SELECT * FROM UAct; 270 | ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 271 | │ 4324182021466249494 │ 5 │ 146 │ -1 │ 272 | │ 4324182021466249494 │ 6 │ 185 │ 1 │ 273 | └─────────────────────┴───────────┴──────────┴──────┘ 274 | ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 275 | │ 4324182021466249494 │ 5 │ 146 │ 1 │ 276 | └─────────────────────┴───────────┴──────────┴──────┘ 277 | 278 | -- 为了获取正确的sum值,需要改写SQL: 279 | -- sum(PageViews) => sum(PageViews * Sign)、 280 | -- sum(Duration) => sum(Duration * Sign) 281 | SELECT 282 | UserID, 283 | sum(PageViews * Sign) AS PageViews, 284 | sum(Duration * Sign) AS Duration 285 | FROM UAct 286 | GROUP BY UserID 287 | HAVING sum(Sign) > 0; 288 | ┌──────────────UserID─┬─PageViews─┬─Duration─┐ 289 | │ 4324182021466249494 │ 6 │ 185 │ 290 | └─────────────────────┴───────────┴──────────┘ 291 | 292 | 293 | -- 强制后台Compaction 294 | optimize table UAct final; 295 | 296 | -- 再次查询,可以看到状态行、取消行已经被折叠,只剩下最新的一行状态行。 297 | select * from UAct; 298 | ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 299 | │ 4324182021466249494 │ 6 │ 185 │ 1 │ 300 | └─────────────────────┴───────────┴──────────┴──────┘ 301 | ``` 302 | 303 | CollapsingMergeTree虽然解决了主键相同的数据即时删除的问题,但是状态持续变化且多线程并行写入情况下,状态行与取消行位置可能乱序,导致无法正常折叠。 304 | 305 | 如下面例子所示: 306 | 307 | ```sql 308 | -- 建表 309 | CREATE TABLE UAct_order 310 | ( 311 | UserID UInt64, 312 | PageViews UInt8, 313 | Duration UInt8, 314 | Sign Int8 315 | ) 316 | ENGINE = CollapsingMergeTree(Sign) 317 | ORDER BY UserID; 318 | 319 | -- 先插入取消行 320 | INSERT INTO UAct_order VALUES (4324182021466249495, 5, 146, -1); 321 | -- 后插入状态行 322 | INSERT INTO UAct_order VALUES (4324182021466249495, 5, 146, 1); 323 | 324 | -- 强制Compaction 325 | optimize table UAct_order final; 326 | 327 | -- 可以看到即便Compaction之后也无法进行主键折叠: 2行数据仍旧都存在。 328 | select * from UAct_order; 329 | ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 330 | │ 4324182021466249495 │ 5 │ 146 │ -1 │ 331 | │ 4324182021466249495 │ 5 │ 146 │ 1 │ 332 | └─────────────────────┴───────────┴──────────┴──────┘ 333 | ``` 334 | 335 | ***\*VersionedCollapsingMergeTree\**** 336 | 337 | 为了解决CollapsingMergeTree乱序写入情况下无法正常折叠问题,VersionedCollapsingMergeTree表引擎在建表语句中新增了一列Version,用于在乱序情况下记录状态行与取消行的对应关系。主键相同,且Version相同、Sign相反的行,在Compaction时会被删除。 338 | 339 | 与CollapsingMergeTree类似, 为了获得正确结果,业务层需要改写SQL,将count()、sum(col)分别改写为sum(Sign)、sum(col * Sign)。 340 | 341 | 示例如下: 342 | 343 | ```sql 344 | -- 建表 345 | CREATE TABLE UAct_version 346 | ( 347 | UserID UInt64, 348 | PageViews UInt8, 349 | Duration UInt8, 350 | Sign Int8, 351 | Version UInt8 352 | ) 353 | ENGINE = VersionedCollapsingMergeTree(Sign, Version) 354 | ORDER BY UserID; 355 | 356 | 357 | -- 先插入一行取消行,注意Signz=-1, Version=1 358 | INSERT INTO UAct_version VALUES (4324182021466249494, 5, 146, -1, 1); 359 | -- 后插入一行状态行,注意Sign=1, Version=1;及一行新的状态行注意Sign=1, Version=2,将PageViews从5更新至6,将Duration从146更新为185。 360 | INSERT INTO UAct_version VALUES (4324182021466249494, 5, 146, 1, 1),(4324182021466249494, 6, 185, 1, 2); 361 | 362 | 363 | -- 查询可以看到未compaction情况下,所有行都可见。 364 | SELECT * FROM UAct_version; 365 | ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 366 | │ 4324182021466249494 │ 5 │ 146 │ -1 │ 367 | │ 4324182021466249494 │ 6 │ 185 │ 1 │ 368 | └─────────────────────┴───────────┴──────────┴──────┘ 369 | ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 370 | │ 4324182021466249494 │ 5 │ 146 │ 1 │ 371 | └─────────────────────┴───────────┴──────────┴──────┘ 372 | 373 | 374 | -- 为了获取正确的sum值,需要改写SQL: 375 | -- sum(PageViews) => sum(PageViews * Sign)、 376 | -- sum(Duration) => sum(Duration * Sign) 377 | SELECT 378 | UserID, 379 | sum(PageViews * Sign) AS PageViews, 380 | sum(Duration * Sign) AS Duration 381 | FROM UAct_version 382 | GROUP BY UserID 383 | HAVING sum(Sign) > 0; 384 | ┌──────────────UserID─┬─PageViews─┬─Duration─┐ 385 | │ 4324182021466249494 │ 6 │ 185 │ 386 | └─────────────────────┴───────────┴──────────┘ 387 | 388 | 389 | -- 强制后台Compaction 390 | optimize table UAct_version final; 391 | 392 | 393 | -- 再次查询,可以看到即便取消行与状态行位置乱序,仍旧可以被正确折叠。 394 | select * from UAct_version; 395 | ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐ 396 | │ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 │ 397 | └─────────────────────┴───────────┴──────────┴──────┴─────────┘ 398 | ``` 399 | 400 | ***\*SummingMergeTree\**** 401 | 402 | ClickHouse通过SummingMergeTree来支持对主键列进行预先聚合。在后台Compaction时,会将主键相同的多行进行sum求和,然后使用一行数据取而代之,从而大幅度降低存储空间占用,提升聚合计算性能。 403 | 404 | 值得注意的是: 405 | 406 | - ClickHouse只在后台Compaction时才会进行数据的预先聚合,而compaction的执行时机无法预测,所以可能存在部分数据已经被预先聚合、部分数据尚未被聚合的情况。因此,在执行聚合计算时,SQL中仍需要使用GROUP BY子句。 407 | - 在预先聚合时,ClickHouse会对主键列之外的其他所有列进行预聚合。如果这些列是可聚合的(比如数值类型),则直接sum;如果不可聚合(比如String类型),则随机选择一个值。 408 | - 通常建议将SummingMergeTree与MergeTree配合使用,使用MergeTree来存储具体明细,使用SummingMergeTree来存储预先聚合的结果加速查询。 409 | 410 | 示例如下: 411 | 412 | ```sql 413 | -- 建表 414 | CREATE TABLE summtt 415 | ( 416 | key UInt32, 417 | value UInt32 418 | ) 419 | ENGINE = SummingMergeTree() 420 | ORDER BY key 421 | 422 | -- 插入数据 423 | INSERT INTO summtt Values(1,1),(1,2),(2,1) 424 | 425 | -- compaction前查询,仍存在多行 426 | select * from summtt; 427 | ┌─key─┬─value─┐ 428 | │ 1 │ 1 │ 429 | │ 1 │ 2 │ 430 | │ 2 │ 1 │ 431 | └─────┴───────┘ 432 | 433 | -- 通过GROUP BY进行聚合计算 434 | SELECT key, sum(value) FROM summtt GROUP BY key 435 | ┌─key─┬─sum(value)─┐ 436 | │ 2 │ 1 │ 437 | │ 1 │ 3 │ 438 | └─────┴────────────┘ 439 | 440 | -- 强制compaction 441 | optimize table summtt final; 442 | 443 | -- compaction后查询,可以看到数据已经被预先聚合 444 | select * from summtt; 445 | ┌─key─┬─value─┐ 446 | │ 1 │ 3 │ 447 | │ 2 │ 1 │ 448 | └─────┴───────┘ 449 | 450 | 451 | -- compaction后,仍旧需要通过GROUP BY进行聚合计算 452 | SELECT key, sum(value) FROM summtt GROUP BY key 453 | ┌─key─┬─sum(value)─┐ 454 | │ 2 │ 1 │ 455 | │ 1 │ 3 │ 456 | └─────┴────────────┘ 457 | ``` 458 | 459 | ***\*AggregatingMergeTree\**** 460 | 461 | AggregatingMergeTree也是预先聚合引擎的一种,用于提升聚合计算的性能。与SummingMergeTree的区别在于:SummingMergeTree对非主键列进行sum聚合,而AggregatingMergeTree则可以指定各种聚合函数。 462 | 463 | AggregatingMergeTree的语法比较复杂,需要结合物化视图或ClickHouse的特殊数据类型AggregateFunction一起使用。在insert和select时,也有独特的写法和要求:写入时需要使用-State语法,查询时使用-Merge语法。 464 | 465 | 以下通过示例进行介绍。 466 | 467 | 示例一:配合物化视图使用。 468 | 469 | ```sql 470 | -- 建立明细表 471 | CREATE TABLE visits 472 | ( 473 | UserID UInt64, 474 | CounterID UInt8, 475 | StartDate Date, 476 | Sign Int8 477 | ) 478 | ENGINE = CollapsingMergeTree(Sign) 479 | ORDER BY UserID; 480 | 481 | -- 对明细表建立物化视图,该物化视图对明细表进行预先聚合 482 | -- 注意:预先聚合使用的函数分别为: sumState, uniqState。对应于写入语法-State. 483 | CREATE MATERIALIZED VIEW visits_agg_view 484 | ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMM(StartDate) ORDER BY (CounterID, StartDate) 485 | AS SELECT 486 | CounterID, 487 | StartDate, 488 | sumState(Sign) AS Visits, 489 | uniqState(UserID) AS Users 490 | FROM visits 491 | GROUP BY CounterID, StartDate; 492 | 493 | -- 插入明细数据 494 | INSERT INTO visits VALUES(0, 0, '2019-11-11', 1); 495 | INSERT INTO visits VALUES(1, 1, '2019-11-12', 1); 496 | 497 | -- 对物化视图进行最终的聚合操作 498 | -- 注意:使用的聚合函数为 sumMerge, uniqMerge。对应于查询语法-Merge. 499 | SELECT 500 | StartDate, 501 | sumMerge(Visits) AS Visits, 502 | uniqMerge(Users) AS Users 503 | FROM visits_agg_view 504 | GROUP BY StartDate 505 | ORDER BY StartDate; 506 | 507 | -- 普通函数 sum, uniq不再可以使用 508 | -- 如下SQL会报错: Illegal type AggregateFunction(sum, Int8) of argument 509 | SELECT 510 | StartDate, 511 | sum(Visits), 512 | uniq(Users) 513 | FROM visits_agg_view 514 | GROUP BY StartDate 515 | ORDER BY StartDate; 516 | ``` 517 | 518 | 示例二:配合特殊数据类型AggregateFunction使用。 519 | 520 | ```sql 521 | -- 建立明细表 522 | CREATE TABLE detail_table 523 | ( CounterID UInt8, 524 | StartDate Date, 525 | UserID UInt64 526 | ) ENGINE = MergeTree() 527 | PARTITION BY toYYYYMM(StartDate) 528 | ORDER BY (CounterID, StartDate); 529 | 530 | -- 插入明细数据 531 | INSERT INTO detail_table VALUES(0, '2019-11-11', 1); 532 | INSERT INTO detail_table VALUES(1, '2019-11-12', 1); 533 | 534 | -- 建立预先聚合表, 535 | -- 注意:其中UserID一列的类型为:AggregateFunction(uniq, UInt64) 536 | CREATE TABLE agg_table 537 | ( CounterID UInt8, 538 | StartDate Date, 539 | UserID AggregateFunction(uniq, UInt64) 540 | ) ENGINE = AggregatingMergeTree() 541 | PARTITION BY toYYYYMM(StartDate) 542 | ORDER BY (CounterID, StartDate); 543 | 544 | -- 从明细表中读取数据,插入聚合表。 545 | -- 注意:子查询中使用的聚合函数为 uniqState, 对应于写入语法-State 546 | INSERT INTO agg_table 547 | select CounterID, StartDate, uniqState(UserID) 548 | from detail_table 549 | group by CounterID, StartDate 550 | 551 | -- 不能使用普通insert语句向AggregatingMergeTree中插入数据。 552 | -- 本SQL会报错:Cannot convert UInt64 to AggregateFunction(uniq, UInt64) 553 | INSERT INTO agg_table VALUES(1, '2019-11-12', 1); 554 | 555 | -- 从聚合表中查询。 556 | -- 注意:select中使用的聚合函数为uniqMerge,对应于查询语法-Merge 557 | SELECT uniqMerge(UserID) AS state 558 | FROM agg_table 559 | GROUP BY CounterID, StartDate; 560 | ``` 561 | 562 | ## 总结 563 | 564 | ClickHouse提供了丰富多样的表引擎,应对不同的业务需求。在这些表引擎之外,ClickHouse还提供了Replicated、Distributed等高级表引擎,需要结合集群部署方式和应用场景灵活组合使用。 -------------------------------------------------------------------------------- /docs/flink/DataFlow模型.md: -------------------------------------------------------------------------------- 1 | # DataFlow模型 2 | 3 | 毫无疑问,Apache Flink 和 Apache Spark (Structured Streaming)现在是实时流计算领域的两个最火热的话题了。那么为什么要介绍 Google Dataflow 呢?***Streaming Systems*** 这本书在分析 Flink 的火热原因的时候总结了下面两点: 4 | 5 | > “There were two main reasons for Flink’s rise to prominence: 6 | > 7 | > - Its rapid adoption of the Dataflow/Beam programming model, which put it in the position of being the most semantically capable fully open source streaming system on the planet at the time. 8 | > - Followed shortly thereafter by its highly efficient snapshotting implementation (derived from research in Chandy and Lamport’s original paper “Distributed Snapshots: Determining Global States of Distributed Systems” [Figure 10-29]), which gave it the strong consistency guarantees needed for correctness. ” 9 | > 10 | > 摘录来自: Tyler Akidau, Slava Chernyak, Reuven Lax. “Streaming Systems。” 11 | 12 | 简单来说一是实现了 Google Dataflow/Bean 的编程模型,二是使用分布式异步快照算法 Chandy-Lamport 的变体。 Chandy-Lamport 算法在本专栏的上一篇文章已经说过了。 13 | 14 | Apache Spark 的 2018 年的论文中也有提到 15 | 16 | > Structured Streaming combines elements of Google Dataflow [2], incremental queries [11, 29, 38] and Spark Streaming [37] to enable stream processing beneath the Spark SQL API. 17 | 18 | 所以说,称 Google Dataflow 为现代流式计算的基石,一点也不为过。我们这篇文章就来看一下 Google Dataflow 的具体内容,主要参考于 2015 年发表与 VLDB 的 Dataflow 论文:***The dataflow model: a practical approach to balancing correctness, latency, and cost in massive-scale, unbounded, out-of-order data processing***。 19 | 20 | ### Dataflow核心模型 21 | 22 | Google Dataflow 模型旨在提供一种统一批处理和流处理的系统,现在已经在 Google Could 使用。其内部使用 Flume 和 MillWheel 来作为底层实现,这里的 Flume 不是 Apache Flume,而是 MapReduce 的编排工具,也有人称之为 FlumeJava;MillWheel 是 Google 内部的流式系统,可以提供强大的无序数据计算能力。关于 Google Cloud 上面的 Dataflow 系统感兴趣的可以参考官网链接 [CLOUD DATAFLOW](https://cloud.google.com/dataflow/?hl=zh-cn)。我们这里重点看一下 Dataflow 模型。 23 | 24 | Dataflow 模型的核心点在于: 25 | 26 | - 对于无序的流式数据提供基于 event-time 的顺序处理、基于数据本身的特征进行窗口聚合处理的能力,以及平衡正确性、延迟、成本之间的相互关系。 27 | - 解构数据处理的四个维度,方便我们更好的分析问题: 28 | - **What** results are being computed. => Transformation 29 | - **Where** in event time they are been computed. => Window 30 | - **When** in processing time they are materialized. => Watermark and Trigger 31 | - **How** earlier results relate to later refinements. => Discarding, Accumulating, Accumulating & Retracting. 32 | 33 | - **Windowing Model**,支持非对齐的 event time 的窗口聚合 34 | - **Triggering Model**,提供强大和灵活的声明式 API 来描述 Trigger 语义,可以根据事件处理的运行时特征来决定输出次数。 35 | - **Incremental Processing Model**,将数据的更新整合到上面的 **Window** 模型和 **Trigger** 模型中。 36 | - **Scalable implementation**,基于 MillWheel 流式引擎和 Flume 批处理引擎实现的 Google Dataflow SDK,用户无需感知底层系统。 37 | - **Core Principle**,模型设计的核心原则。 38 | 39 | ### 核心概念 40 | 41 | #### Unbounded/Bounded vs Streaming/Batch 42 | 43 | 在 Dataflow 之前,对于有限/无限数据集合的描述,一般使用批/流 (Batch/Streaming),总有一种暗示底层两套引擎(批处理引擎和流处理引擎)。对于批处理和流处理,一般情况下是可以互相转化的,比如 Spark 用微批来模拟流。而 Dataflow 模型一般将有限/无限数据集合称为 Bounded/Unbounded Dataset,而 Streaming/Batch 用来特指执行引擎。可以认为批是流的一种特例。 44 | 45 | #### 时间语义 46 | 47 | 在流式处理中关于时间有两个概念需要注意: 48 | 49 | - **Event Time**,事件发生的时间。 50 | - **Processing TIme**,事件在系统中的处理时间。 51 | 52 | 这两个概念非常简单。比如在 IoT 中,传感器采集事件时对应的系统时间就是 Event Time,然后事件发送到流式系统进行处理,处理的时候对应的系统时间就是 Processing Time。虽然是两个很简单概念,但是在 Dataflow 模型之前,很多系统并没有显示区分,比如 Spark Streaming。 53 | 54 | 在现实中,由于通信延迟、调度延迟等,往往导致 Event Time 和 Processing Time 之间存在差值(skew),且动态变化。skew 一般使用 watermark 来进行可视化,如下图。 55 | 56 | ![img](Flink运行架构.assets/stsy_0209.png) 57 | 58 | 那条蜿蜒的红线本质上是水位线,随着处理时间的推移,它描述了事件时间完整性的进度。从概念上讲,可以将水印视为函数*F*(*P*)→ *E*,它在处理时间并返回事件时间点,事件时间*E的*那个点是系统认为事件时间小于*E的*所有输入都被观测到的时间点,换而言之,所有事件时间小于Watermark的数据都已经达到了,而任务在接收到的小于Watermark的数据可称为延迟数据。所以Watermark永远是单调递增的,因为`时光不会倒流`。 59 | 60 | #### window 61 | 62 | Window,也就是窗口,将一部分数据集合组合起操作。在处理无限数据集的时候有限操作需要窗口,比如 **aggregation**,**outer join**,**time-bounded** 操作。窗口大部分都是基于时间来划分,但是也有基于其他存在逻辑上有序关系的数据来划分的。窗口模型主要由三种:**Fixed Window**,**Sliding Window**,**Session Window**。 63 | 64 | img 65 | 66 | - Fixed Window 67 | 68 | Fixed Window ,有时候也叫 Tumbling Window。Tumble 的中文翻译有“翻筋斗”,我们可以将 Fixed Window 是特定的时间长度在无限数据集合上翻滚形成的,核心是每个 Window 没有重叠。比如小时窗口就是 12:00:00 ~ 13:00:00 一个窗口,13:00:00 ~ 14:00:00 一个窗口。从例子也可以看出来 Fixed Window 的另外一个特征:aligned,中文一般称为对齐。 69 | 70 | - Silding Window 71 | 72 | Sliding Window,中文可以叫滑动窗口,由两个参数确定,窗口大小和滑动间隔。比如每分钟开始一个小时窗口对应的就是窗口大小为一小时,滑动间隔为一分钟。滑动间隔一般小于窗口大小,也就是说窗口之间会有重叠。滑动窗口在很多情况下都比较有用,比如检测机器的半小时负载,每分钟检测一次。Fixed Window 是 Sliding Window 的一种特例:窗口大小等于滑动间隔。 73 | 74 | - Session Window 75 | 76 | Session Window,会话窗口, 会话由事件序列组成,这些事件序列以大于某个超时的不活动间隔终止(两边等),回话窗口不能事先定义,取决于数据流。一般用来捕捉一段时间内的行为,比如 Web 中一段时间内的登录行为为一个 Session,当长时间没有登录,则 Session 失效,再次登录重启一个 Session。Session Window 也是用超时时间来衡量,只要在超时时间内发生的事件都认为是一个 Session Window。 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /docs/flink/Flink1.12nativekubernetes演进.assets/v2-899169586ff422599670187f97f74afd_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Flink1.12nativekubernetes演进.assets/v2-899169586ff422599670187f97f74afd_1440w.jpg -------------------------------------------------------------------------------- /docs/flink/Flink1.12nativekubernetes演进.assets/v2-975fc1b7cf5f31d647b7dacec219ea91_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Flink1.12nativekubernetes演进.assets/v2-975fc1b7cf5f31d647b7dacec219ea91_1440w.jpg -------------------------------------------------------------------------------- /docs/flink/Flink1.12nativekubernetes演进.md: -------------------------------------------------------------------------------- 1 | * [Flink1\.12 native kubernetes 演进](#flink112-native-kubernetes-演进) 2 | * [Flink 1\.10](#flink-110) 3 | * [Flink 1\.11](#flink-111) 4 | * [Flink 1\.12](#flink-112) 5 | * [总结](#总结) 6 | 7 | # Flink1.12 native kubernetes 演进 8 | 9 | ![img](Flink1.12nativekubernetes演进.assets/v2-899169586ff422599670187f97f74afd_1440w.jpg) 10 | 11 | ## Flink 1.10 12 | 13 | Flink 1.10 开始支持将 native kubernetes 作为其资源管理器。在该版本中,你可以使用以下命令在你的 kubernetes 集群中创建一个flink session。 14 | 15 | ```shell 16 | ./bin/kubernetes-session.sh \ 17 | -Dkubernetes.cluster-id= \ 18 | -Dtaskmanager.memory.process.size=4096m \ 19 | -Dkubernetes.taskmanager.cpu=2 \ 20 | -Dtaskmanager.numberOfTaskSlots=4 \ 21 | -Dresourcemanager.taskmanager-timeout=3600000 22 | ``` 23 | 24 | 此时创建session 的 kubernetes 相关参数支持的比较少,只支持设置资源大小,容器镜像,命名空间等[基本参数](https://link.zhihu.com/?target=https%3A//ci.apache.org/projects/flink/flink-docs-release-1.10/ops/config.html%23kubernetes)。对于生产环境,暴露这些参数远远不够。 25 | 26 | 然后使用如下的命令,提交任务到我们刚刚创建的Session 中: 27 | 28 | ```shell 29 | ./bin/flink run -d -e kubernetes-session -Dkubernetes.cluster-id= examples/streaming/WindowJoin.jar 30 | ``` 31 | 32 | ## Flink 1.11 33 | 34 | Flink1.11, 首先创建 session 的kubernetes 相关参数支持增多了,支持了node-selector, tolerations等调度[相关参数](https://link.zhihu.com/?target=https%3A//ci.apache.org/projects/flink/flink-docs-release-1.11/ops/config.html%23kubernetes),并且支持设置保留JobManager端点的服务类型。 35 | 36 | 支持资源调度相关参数,对于生产环境非常重要。我们可以控制我们的 session 集群资源,主要是jobmanager 和 taskmanager 调度到指定的机器上,是实现资源隔离,安全,计费的前提。 37 | 38 | 另外flink1.11 新增了Application 模式(和Session 模式不同)。Application模式允许用户创建一个包含作业和Flink运行时的镜像,根据需要自动创建和销毁集群组件。 39 | 40 | 该模式就更加云原生了,可以充分发挥native k8s的弹性。根据提交的任务,来自动创建jobmanager 和 taskmanager ,待任务运行完成,则自动销毁jobmanager 和 taskmanager 。 41 | 42 | 可以使用如下命令,启动application: 43 | 44 | ```shell 45 | ./bin/flink run-application -p 8 -t kubernetes-application \ 46 | -Dkubernetes.cluster-id= \ 47 | -Dtaskmanager.memory.process.size=4096m \ 48 | -Dkubernetes.taskmanager.cpu=2 \ 49 | -Dtaskmanager.numberOfTaskSlots=4 \ 50 | -Dkubernetes.container.image= \ 51 | local:///opt/flink/usrlib/my-flink-job.jar 52 | ``` 53 | 54 | ## Flink 1.12 55 | 56 | ![img](Flink1.12nativekubernetes演进.assets/v2-975fc1b7cf5f31d647b7dacec219ea91_1440w.jpg) 57 | 58 | Flink 1.12 之前的版本中JobManager的 HA 是通过ZooKeeper 来实现的。 59 | 60 | 在1.12 版本中,Kubernetes提供了Flink可用于JobManager故障转移的内置功能,而不是依赖ZooKeeper。Kubernetes HA 服务与ZooKeeper实现基于相同的基本接口构建,并使用Kubernetes的ConfigMap对象处理从JobManager故障中恢复所需的所有元数据。 61 | 62 | 为了启动HA集群,您必须配置以下步骤: 63 | 64 | - [high-availability](https://link.zhihu.com/?target=https%3A//ci.apache.org/projects/flink/flink-docs-release-1.12/deployment/config.html%23high-availability-1)(required): `high-availability` 选项必须设置为KubernetesHaServicesFactory。 65 | 66 | ```text 67 | high-availability: org.apache.flink.kubernetes.highavailability.KubernetesHaServicesFactory 68 | ``` 69 | 70 | - [high-availability.storageDir](https://link.zhihu.com/?target=https%3A//ci.apache.org/projects/flink/flink-docs-release-1.12/deployment/config.html%23high-availability-storagedir)(required): JobManager元数据将持久保存在文件系统`high-availability.storageDir`中,并且仅指向此状态的指针存储在Kubernetes中。 71 | 72 | ```text 73 | high-availability.storageDir: s3:///flink/recovery 74 | ``` 75 | 76 | - [kubernetes.cluster-id](https://link.zhihu.com/?target=https%3A//ci.apache.org/projects/flink/flink-docs-release-1.12/deployment/config.html%23kubernetes-cluster-id)(required): 为了标识Flink集群,您必须指定kubernetes.cluster-id。 77 | 78 | ```text 79 | kubernetes.cluster-id: cluster1337 80 | ``` 81 | 82 | 此外flink 1.12 支持flink 中程序使用kubernetes 中的secrets。通过如下两种方式: 83 | 84 | - 将Secrets用作Pod中的文件; 85 | - 使用Secrets作为环境变量; 86 | 87 | 这样的话,我们可以将一些敏感凭证数据放到Secret中。在安全性上是一种增强。 88 | 89 | 最后Flink使用`Kubernetes OwnerReference`来清理所有集群组件。由Flink创建的所有资源,包括ConfigMap,Service和Pod,都将OwnerReference设置为`Deployment/`。删除部署后,所有相关资源将自动删除。 90 | 91 | ## 总结 92 | 93 | Flink 对于native kubernetes 的支持逐步增强,kubernets 相对于yarn等资源管理器,有着诸多的优势。 -------------------------------------------------------------------------------- /docs/flink/FlinkMemory.assets/image-20200921183651039.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/FlinkMemory.assets/image-20200921183651039.png -------------------------------------------------------------------------------- /docs/flink/FlinkMemory.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Flink TaskExecutor内存管理 4 | 5 | ## 一、 背景 6 | 7 | - Flink 1.9及以前版本的TaskExecutor内存模型 8 | 9 | - 逻辑复杂难懂 10 | - 实际内存大小不确定 11 | - 不同场景及模块计算结果不一致 12 | - 批、流配置无法兼容 13 | 14 | - Flink 1.10引入了FLIP-49,对TaskExecutor内存模型进行了统一的梳理和简化 15 | 16 | - Flink 1.10内存模型: 17 | 18 | image-20200920223508792 19 | 20 | 21 | 22 | ## 二、Flink内存类型及用途 23 | 24 | image-20200920223853718 25 | 26 | > 左图是Flink1.10设计文档的内存模型图,有图为Flink 官方用户文档内存模型,实际上语义是一样的。 27 | > 28 | > 1. Process Memory: 一个Flink TaskExecutor进程使用的所有内存包含在内, 尤其是在容器化的环境中。 29 | > 2. Flink Memory:抛出JVM自身的开销,留下的专门用于Flink应用程序的内存用量,通常用于standlone模式下。 30 | 31 | 32 | 33 | ### Framework 和 Task Memory 34 | 35 | - 区别:是否计入Slot资源 36 | 37 | - 总用量受限: 38 | 39 | - -Xmx = Framework Heap + Task Heap 40 | - -XX:MaxDirectMemorySize= Network + Framework Off-Heap + Task Off-Heap 41 | 42 | - 无隔离 43 | 44 | image-20200921222757296 45 | 46 | > 1. 设置JVM的参数,但我们并没有在Slot与Framework之间进行隔离,一个TaskExecutor是一个进程,不管TaskExecutor进行的是Slot也好,还是框架的其他任务也好,都是线程级别的任务。那对于JVM的内存特点来说,它是不会在线程级别对内存的使用量进行限制的。那为什么还要引入Framework与Task的区别呢?为后续版本的准备:动态切割Slot资源。 47 | > 2. 不建议对Framework的内存进行调整,保留它的默认值。在Flink 1.10发布之前,它的默认值是通过跑一个空的Cluster,上面不调度任何任务的情况下,通过测量、计算出来的Framework所需要的内存。 48 | 49 | 50 | 51 | ### Heap 和 Off-Heap Memory 52 | 53 | - Heap 54 | - Java的堆内存,适用于绝大多数Java对象 55 | - HeapStateBackend 56 | - Off-Heap 57 | - Direct: Java中的直接内存,但凡一下两种方式申请的内存,都属于Direct 58 | - DirectByteBuffer 59 | - MappedByteBuffer 60 | - Native 61 | - Native内存指的是像JNI、C/C++、Python、etc所用的不受Jvm进程管控的一些内存。 62 | 63 | > Flink的配置模型没有要求用户对Direct和Native进行区分的,统一叫做Off-Heap。 64 | 65 | 66 | 67 | ### NetWork Memory: 68 | 69 | - 用于 70 | - 数据传输缓冲 71 | - 特点 72 | - 同一个TaskExecutor之间没有隔离 73 | - 需要多少由作业拓扑决定,不足会导致作业运行失败 74 | 75 | > 1. 使用的是Direct Memory,只不过由于Network Memory在配置了指定大小之后,在集群启动的时候,它会去由已经配置的大小去把内存申请下来,并且在整个TaskExecutor Shutdown之前,都不会去释放内存。 76 | > 2. 当通常考虑Network Memory用了多少,没有用多少的时候,这部分Network Memory实际上是一个常量,所以把它从Off-Heap里面拆出来单独管理。 77 | 78 | 79 | 80 | ### Managed Memory 81 | 82 | - 用于 83 | - RocksDBStateBackend 84 | - Batch Operator 85 | - HeapStateBackend / 无状态应用,不需要Managed Memory,可以设置为0 86 | - 特点 87 | - 同一TaskExecutor之间的多个Slot严格隔离 88 | - 多点少点都能跑,与性能挂钩 89 | - RocksDB内存限制 90 | - state.backend.rocksdb.memory.m anaged (default: true) 91 | - 设置RocksDB实用的内存大小 Managed Memory 大小 92 | - 目的:避免容器内存超用,RocksDB的内存申请是在RocksDB内部完成的,Flink没有办法进行直接干预,但可以通过设置RocksDB的参数,让RocksDB去申请的内存大小刚好不超过Managed Memory。主要目的是为了防止state比较多(一个state一个列族),RocksDB的内存超用,造成容器被Yarn/K8s Kill掉。 93 | 94 | > 1. 本质上是用的Native Memory,并不会受到JVM的Heap、Direct大小的限制。不受JVM的掌控的,但是Flink会对它进行管理,Flink会严格控制到底申请了多少Managed Memory。 95 | > 2. RocksDB实用C++写成的一个数据库,会用到Native Memory。 96 | > 3. 一个Slot有多少Managed Memory,一定没有办法超用,这是其中一个特点;不管是RocksDB的用户,还是Batch Operator的用户,Task并没有一个严格要用多少大小的memory,可能会有一个最低限度,但一般会很小。 97 | 98 | 99 | 100 | ### JVM Metaspace 101 | 102 | - 存放JVM加载的类的元数据 103 | - 加载的类越多,需要空间越大 104 | - 以下情况需要增大JVM Metaspace 105 | - 作业需要加载大量第三方库 106 | - 多个不同作业的任务运行在同一TaskExecutor上 107 | 108 | 109 | 110 | ### JVM Overhead 111 | 112 | - Native Memory 113 | - 用于Jvm其他开销 114 | - code cache 115 | - Thread Stack 116 | 117 | 118 | 119 | ## 三、内存特性解读 120 | 121 | ### Java内存类型 122 | 123 | 很多时候都在说,Flink使用到的内存包括Heap、Direct、Native Memory等,那么Java到底有多少种类型的内存?Java的内存分类是一个比较复杂的问题,但归根究底只有两种内存:Heap与Off-Heap。所谓Heap就是Java堆内存,Off-Heap就是堆外内存。 124 | 125 | - Heap 126 | 127 | - 经过JVM虚拟化的内存。 128 | - 实际存储地址可能随GC变化,上层应用无感知。 129 | 130 | > 一个Java对象,或者一个应用程序拿到一个Java对象之后,它并不需要去关注内存实际上是存放在哪里的,实际上也没有一个固定的地址。 131 | 132 | - Off-Heap 133 | 134 | - 未经JVM虚拟化的内存 135 | - 直接映射到本地OS内存地址 136 | 137 | image-20200920230448872 138 | 139 | > 1. Java的Heap内存空间实际上也是切分为了Eden、S0、S1、Tenured这几部分,所谓的垃圾回收机制会让对象在Eden空间中,随着Eden空间满了之后会触发GC,把已经没有使用的内存释放掉;已经引用的对象会移动到Servivor空间,在S0和S1之间反复复制,最后会放在老年代内存空间中。 这是Java的GC机制,造成的问题是Java对象会在内存空间中频繁的进行复制、拷贝。 140 | > 2. 所谓的Off-Heap内存,是直接使用操作系统提供的内存,也就是说内存地址是操作系统来决定的。一方面避免了内存在频繁GC过程中拷贝的操作,另外如果涉及到对一些OS的映射或者网络的一些写出、文件的一些写出,它避免了在用户空间复制的一个成本,所以它的效率会相对更高。 141 | > 3. Heap的内存大小是有-Xmx决定的,而Off-Heap部分:有一些像Direct,它虽然不是在堆内,但是JVM会去对申请了多少Direct Memory进行计数、进行限制。如果设置了-XX:MaxDirectMemorySize的参数,当它达到限制的时候,就不会去继续申请新的内存;同样的对于metaspace也有同样的限制。 142 | > 4. 既不受到Direct限制,也不受到Native限制的,是Native Memory。 143 | 144 | - Question:什么是JVM内(外)的内存? 145 | 146 | - 经过JVM虚拟化 147 | - Heap 148 | 149 | - 受JVM管理 150 | - 用量上限、申请、释放(GC)等 151 | - Heap、Direct、Metaspace、even some Native(e.g., Thread Stack) 152 | 153 | ### Heap Memory特性 154 | 155 | 在Flink当中,Heap Memory 156 | 157 | - 包括 158 | - Framework Heap Memory 159 | - Task Heap Memory 160 | - 用量上限受JVM严格控制 161 | - -Xmx:Framework Heap + Task Heap 162 | - 达到上限后触发垃圾回收(GC) 163 | - GC后仍然空间不足,触发OOM异常并退出 164 | - OutOfMemoryError: Java heap space 165 | 166 | ### Direct Memory特性 167 | 168 | - 包括: 169 | - Framework Off-Heap Memory(部分) 170 | - Task Off-Heap Memory(部分) 171 | - Network Memory 172 | - 用量上限受JVM严格控制 173 | - -XX:MaxDirectMemorySize 174 | - Framework Off-Heap + Task Off-Heap + Network Memory 175 | - 达到上限时触发GC,GC后仍然空间不足触发OOM异常并退出 176 | - OutOfMemoryError: Direct buffer memory 177 | 178 | ### Metaspace特性 179 | 180 | - 用量上限受JVM严格控制 181 | - -XX:MaxMetaspaceSize 182 | - 达到上限时触发GC,GC后仍然空间不足触发OOM异常并退出 183 | - OutOfMemoryError: Metaspace 184 | 185 | ### Native Memory特性 186 | 187 | - 包括 188 | - Framework Off-Heap Memory(小部分) 189 | - Task Off-Heap Memory(小部分) 190 | - Managed Memory 191 | - JVM Overhead 192 | - 用量上限不受JVM严格控制 193 | - 其中Managed Memory用量上限受Flink严格控制 194 | 195 | 196 | 197 | ### Framework / Task Off-Heap Memory 198 | 199 | - 既包括Direct也包括Native 200 | - 用户无需理解Direct / Native内存的区别并分别配置 201 | - 无法严格控制Direct内存用量,可能导致超用 202 | - 绝大数情况下 203 | - Flink框架及用户代码不需要或只需要少量Native内存 204 | - Heap活动足够频繁,能够及时触发GC释放不需要的Direct内存 205 | 206 | ![image-20200921183651039](FlinkMemory.assets/image-20200921183651039.png) 207 | 208 | > 如果需要大量使用Native内存,可以考虑增大JVM Overhead 209 | 210 | 211 | 212 | ## 四、 配置方法 213 | 214 | ### Expilcit & Implicit 215 | 216 | - Expilcit 217 | - Size,Min/Max 218 | - 严格保证(包括默认值) 219 | - 配置不当可能引发冲突 220 | 221 | - Implicit 222 | - Fraction 223 | - 非严格保证 224 | - 存在冲突时优先保证Expilcit 225 | 226 | > 1. 所有的size,min/max都是严格保证的,所谓严格保证:如果配置了一个size=100m, 配置结果一定是100m。如果配置了min/max是100-200m, 最终配置的内存大小一定在这个范围内。 227 | > 1. 与之对应的Implicit,如果并不想配比如Managed Memory的内存大小,但是可以指定Managed Memory就是Flink Memory的一个比例。 228 | 229 | 230 | 231 | ### 如何避免配置冲突 232 | 233 | - 以下三项至少配置一项,不建议同时配置两项及以上 234 | - Total Process 235 | - Total Flink 236 | - Task Heap & Managed 237 | - No Explicit Default 238 | 239 | -------------------------------------------------------------------------------- /docs/flink/FlinkStreamingJoins.assets/image-20201213005039152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/FlinkStreamingJoins.assets/image-20201213005039152.png -------------------------------------------------------------------------------- /docs/flink/FlinkStreamingJoins.assets/image-20201213005141832.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/FlinkStreamingJoins.assets/image-20201213005141832.png -------------------------------------------------------------------------------- /docs/flink/FlinkStreamingJoins.md: -------------------------------------------------------------------------------- 1 | * [Streaming Joins](#streaming-joins) 2 | * [Window Join](#window-join) 3 | * [Inner Join](#inner-join) 4 | * [除了官方提供的API,也可以通过coGroup来实现](#除了官方提供的api也可以通过cogroup来实现) 5 | * [Left Join](#left-join) 6 | * [Right Join](#right-join) 7 | * [Interval Join](#interval-join) 8 | * [总结](#总结) 9 | 10 | # Streaming Joins 11 | 12 | ## Window Join 13 | 14 | 基于窗口的Join是将具有相同key并位于同一个窗口中的事件进行联结。 15 | 16 | 用法: 17 | 18 | ```scala 19 | stream.join(otherStream) 20 | .where() 21 | .equalTo() 22 | .window() 23 | .apply() 24 | ``` 25 | 26 | 官方案例: 27 | 28 | Tumbling Window Join的实现,关于其他的窗口,如滑动窗口、会话窗口等,原理是一致的。 29 | 30 | image-20201213005039152 31 | 32 | 如图所示,我们定义了一个大小为2毫秒的滚动窗口,该窗口的形式为[0,1], [2,3], ...。该图显示了每个窗口中所有元素的成对组合,这些元素将传递给JoinFunction。注意,在翻转窗口中[6,7]什么也不发射,因为在绿色流中不存在要与橙色元素⑥和joined连接的元素。 33 | 34 | ### Inner Join 35 | 36 | 与SQL中的Inner Join语义是一致的,将具有相同key并在同一个窗口中的事件Join,注意是key能完全匹配上才能Join上。 37 | 38 | Flink API中的stream.join(otherStream)正是实现的Inner Join。 39 | 40 | 以电商中的订单双流Join为例,定义两个输入流和一个Join后的输出流 41 | 42 | ```scala 43 | // 两个订单流,测试双流Join 44 | case class OrderLogEvent1(orderId:Long,amount:Double,timeStamp:Long) 45 | case class OrderLogEvent2(orderId:Long,itemId:Long,timeStamp:Long) 46 | case class OrderResultEvent(orderId:Long,amount:Double,itemId:Long) 47 | ``` 48 | 49 | 为了测试方便,直接读取两个集合中的数据,定义流事件 50 | 51 | ```scala 52 | val leftOrderStream = env.fromCollection(List( 53 | OrderLogEvent1(1L, 22.1, DateUtils.getTime("2020-04-29 13:01")), 54 | OrderLogEvent1(2L, 22.2, DateUtils.getTime("2020-04-29 13:03")), 55 | OrderLogEvent1(4L, 22.3, DateUtils.getTime("2020-04-29 13:04")), 56 | OrderLogEvent1(4L, 22.4, DateUtils.getTime("2020-04-29 13:05")), 57 | OrderLogEvent1(5L, 22.5, DateUtils.getTime("2020-04-29 13:07")), 58 | OrderLogEvent1(6L, 22.6, DateUtils.getTime("2020-04-29 13:09")) 59 | )) 60 | .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[OrderLogEvent1](Time.seconds(5)) { 61 | override def extractTimestamp(element: OrderLogEvent1): Long = element.timeStamp 62 | }) 63 | .keyBy(_.orderId) 64 | 65 | val rightOrderStream = env.fromCollection(List( 66 | OrderLogEvent2(1L, 121, DateUtils.getTime("2020-04-29 13:01")), 67 | OrderLogEvent2(2L, 122, DateUtils.getTime("2020-04-29 13:03")), 68 | OrderLogEvent2(3L, 123, DateUtils.getTime("2020-04-29 13:04")), 69 | OrderLogEvent2(4L, 124, DateUtils.getTime("2020-04-29 13:05")), 70 | OrderLogEvent2(5L, 125, DateUtils.getTime("2020-04-29 13:07")), 71 | OrderLogEvent2(7L, 126, DateUtils.getTime("2020-04-29 13:09")) 72 | )) 73 | .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[OrderLogEvent2](Time.seconds(5)) { 74 | override def extractTimestamp(element: OrderLogEvent2): Long = element.timeStamp 75 | }) 76 | .keyBy(_.orderId) 77 | ``` 78 | 79 | Join实现: 80 | 81 | ```scala 82 | leftOrderStream 83 | .join(rightOrderStream) 84 | .where(_.orderId) 85 | .equalTo(_.orderId) 86 | .window(TumblingEventTimeWindows.of(Time.minutes(5))) // 5min的时间滚动窗口 87 | .apply(new InnerWindowJoinFunction) 88 | .print() 89 | 90 | class InnerWindowJoinFunction extends JoinFunction[OrderLogEvent1, OrderLogEvent2, OrderResultEvent] { 91 | override def join(first: OrderLogEvent1, second: OrderLogEvent2): OrderResultEvent = { 92 | OrderResultEvent(first.orderId, first.amount, second.itemId) 93 | } 94 | } 95 | ``` 96 | 97 | 测试输出结果: 98 | 99 | ```scala 100 | OrderResultEvent(1,22.1,121) 101 | OrderResultEvent(2,22.2,122) 102 | OrderResultEvent(5,22.5,125) 103 | OrderResultEvent(4,22.4,124) 104 | ``` 105 | 106 | 可见实现了基于Window的Inner Join。 107 | 108 | #### 除了官方提供的API,也可以通过coGroup来实现 109 | 110 | **coGroup**: 111 | 112 | 该操作是将两个数据流/集合按照key进行group,然后将相同key的数据进行处理,但是它和join操作稍有区别,它在一个流/数据集中没有找到与另一个匹配的数据还是会输出。 113 | 114 | coGroup的用法类似于Join,不同的是在apply中传入的是一个CoGroupFunction,而不是JoinFunction 115 | 116 | ```scala 117 | val coGroupedStream = leftOrderStream 118 | .coGroup(rightOrderStream) 119 | .where(_.orderId) 120 | .equalTo(_.orderId) 121 | .window(TumblingEventTimeWindows.of(Time.minutes(5))) // 5min的时间滚动窗口 122 | ``` 123 | 124 | Inner Join实现 125 | 126 | ```scala 127 | coGroupedStream.apply(new InnerWindowJoinFunction).print() 128 | 129 | class InnerWindowJoinFunction extends CoGroupFunction[OrderLogEvent1,OrderLogEvent2,OrderResultEvent]{ 130 | override def coGroup(first: java.lang.Iterable[OrderLogEvent1], 131 | second: java.lang.Iterable[OrderLogEvent2], 132 | out: Collector[OrderResultEvent]): Unit = { 133 | /** 134 | * 将Java的Iterable对象转化为Scala的Iterable对象 135 | */ 136 | import scala.collection.JavaConverters._ 137 | val scalaT1 = first.asScala.toList 138 | val scalaT2 = second.asScala.toList 139 | 140 | // inner join要比较的是同一个key下,同一个时间窗口内的数据 141 | if (scalaT1.nonEmpty && scalaT1.nonEmpty){ 142 | for (left <- scalaT1) { 143 | for (right <- scalaT2) { 144 | out.collect(OrderResultEvent(left.orderId,left.amount,right.itemId)) 145 | } 146 | } 147 | } 148 | 149 | } 150 | ``` 151 | 152 | 输出结果和上面完全一致。 153 | 154 | ### Left Join 155 | 156 | left join与right join由于Flink官方并没有给出明确的方案,无法通过join来实现,但是可以用coGroup来实现。 157 | 158 | 参考代码: 159 | 160 | ```scala 161 | class LeftWindowJoinFunction extends CoGroupFunction[OrderLogEvent1,OrderLogEvent2,OrderResultEvent]{ 162 | override def coGroup(first: lang.Iterable[OrderLogEvent1], 163 | second: lang.Iterable[OrderLogEvent2], 164 | out: Collector[OrderResultEvent]): Unit = { 165 | /** 166 | * 将Java的Iterable对象转化为Scala的Iterable对象 167 | */ 168 | import scala.collection.JavaConverters._ 169 | val scalaT1 = first.asScala.toList 170 | val scalaT2 = second.asScala.toList 171 | 172 | for (left <- scalaT1) { 173 | var flag = false // 定义flag,left流中的key在right流中是否匹配 174 | for (right <- scalaT2) { 175 | out.collect(OrderResultEvent(left.orderId,left.amount,right.itemId)) 176 | flag = true; 177 | } 178 | if (!flag){ // left流中的key在right流中没有匹配到,则给itemId输出默认值0L 179 | out.collect(OrderResultEvent(left.orderId,left.amount,0L)) 180 | } 181 | } 182 | } 183 | } 184 | ``` 185 | 186 | 输出结果: 187 | 188 | ```scala 189 | OrderResultEvent(1,22.1,121) 190 | OrderResultEvent(2,22.2,122) 191 | OrderResultEvent(4,22.3,0) 192 | OrderResultEvent(5,22.5,125) 193 | OrderResultEvent(4,22.4,124) 194 | OrderResultEvent(6,22.6,0) 195 | ``` 196 | 197 | ### Right Join 198 | 199 | 和left join差不多,参考代码 200 | 201 | ```scala 202 | class RightWindowJoinFunction extends CoGroupFunction[OrderLogEvent1,OrderLogEvent2,OrderResultEvent]{ 203 | override def coGroup(first: lang.Iterable[OrderLogEvent1], 204 | second: lang.Iterable[OrderLogEvent2], 205 | out: Collector[OrderResultEvent]): Unit = { 206 | /** 207 | * 将Java的Iterable对象转化为Scala的Iterable对象 208 | */ 209 | import scala.collection.JavaConverters._ 210 | val scalaT1 = first.asScala.toList 211 | val scalaT2 = second.asScala.toList 212 | 213 | for (right <- scalaT2) { 214 | var flag = false // 定义flag,right流中的key在left流中是否匹配 215 | 216 | for (left <- scalaT1) { 217 | out.collect(OrderResultEvent(left.orderId,left.amount,right.itemId)) 218 | flag = true 219 | } 220 | 221 | if (!flag){ //没有匹配到的情况 222 | out.collect(OrderResultEvent(right.orderId,0.00,right.itemId)) 223 | } 224 | 225 | } 226 | } 227 | } 228 | ``` 229 | 230 | 输出结果,与预期一致 231 | 232 | ```scala 233 | OrderResultEvent(1,22.1,121) 234 | OrderResultEvent(2,22.2,122) 235 | OrderResultEvent(3,0.0,123) 236 | OrderResultEvent(5,22.5,125) 237 | OrderResultEvent(4,22.4,124) 238 | OrderResultEvent(7,0.0,126) 239 | ``` 240 | 241 | ## Interval Join 242 | 243 | 间隔Join表示A流Join B流,B流中事件时间戳在A流所界定的范围内的数据Join起来,实现的是Inner Join。 244 | 245 | Interval Join仅支持Event Time 246 | 247 | image-20201213005141832 248 | 249 | 在上面的示例中,我们将两个流“橙色”和“绿色”连接在一起,其下限为-2毫秒,上限为+1毫秒。 250 | 251 | 再次使用更正式的符号,这将转化为 252 | 253 | orangeElem.ts + lowerBound <= greenElem.ts <= orangeElem.ts + upperBound 254 | 255 | 思考:基于间隔的Join实现的是Inner Join语义,如图中时间戳为4的橙流没有join到任何数据。但如果想实现left join语义,应该怎么处理? 256 | 257 | Internal Join参考代码: 258 | 259 | ```scala 260 | leftOrderStream 261 | .intervalJoin(rightOrderStream) 262 | .between(Time.minutes(-2),Time.minutes(1)) 263 | .process(new IntervalJoinFunction) 264 | .print() 265 | 266 | 267 | class IntervalJoinFunction extends ProcessJoinFunction[OrderLogEvent1,OrderLogEvent2,OrderResultEvent]{ 268 | override def processElement(left: OrderLogEvent1, 269 | right: OrderLogEvent2, 270 | ctx: ProcessJoinFunction[OrderLogEvent1, OrderLogEvent2, OrderResultEvent]#Context, 271 | out: Collector[OrderResultEvent]): Unit = { 272 | out.collect(OrderResultEvent(left.orderId,left.amount,right.itemId)) 273 | } 274 | } 275 | ``` 276 | 277 | 输出结果: 278 | 279 | ```scala 280 | OrderResultEvent(1,22.1,121) 281 | OrderResultEvent(2,22.2,122) 282 | OrderResultEvent(4,22.3,124) 283 | OrderResultEvent(4,22.4,124) 284 | OrderResultEvent(5,22.5,125) 285 | ``` 286 | 287 | 288 | 289 | ## 总结 290 | 291 | Flink DataStream实现双流Join,一般来说,在生产情况下,如果满足业务需求只需要实现inner join,直接使用 292 | 293 | `interval join`即可,但需要注意的是,应该避免定义比较大的间隔时长,避免大的状态对服务性能造成影响,如果需要,可以使用如Redis等三方存储结合使用;如果业务需要实现`left join`或`right join`,可以使用sessionWindow结合coGroup实现。 294 | 295 | -------------------------------------------------------------------------------- /docs/flink/FlinkTaskExecutor内存管理.assets/image-20200920223508792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/FlinkTaskExecutor内存管理.assets/image-20200920223508792.png -------------------------------------------------------------------------------- /docs/flink/FlinkTaskExecutor内存管理.assets/image-20200920223853718.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/FlinkTaskExecutor内存管理.assets/image-20200920223853718.png -------------------------------------------------------------------------------- /docs/flink/FlinkTaskExecutor内存管理.assets/image-20200920230448872.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/FlinkTaskExecutor内存管理.assets/image-20200920230448872.png -------------------------------------------------------------------------------- /docs/flink/FlinkTaskExecutor内存管理.assets/image-20200921183651039.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/FlinkTaskExecutor内存管理.assets/image-20200921183651039.png -------------------------------------------------------------------------------- /docs/flink/FlinkTaskExecutor内存管理.assets/image-20200921222741670.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/FlinkTaskExecutor内存管理.assets/image-20200921222741670.png -------------------------------------------------------------------------------- /docs/flink/FlinkTaskExecutor内存管理.assets/image-20200921222757296.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/FlinkTaskExecutor内存管理.assets/image-20200921222757296.png -------------------------------------------------------------------------------- /docs/flink/FlinkTaskExecutor内存管理.md: -------------------------------------------------------------------------------- 1 | * [Flink TaskExecutor内存管理](#flink-taskexecutor内存管理) 2 | * [一、 背景](#一-背景) 3 | * [二、Flink内存类型及用途](#二flink内存类型及用途) 4 | * [Framework 和 Task Memory](#framework-和-task-memory) 5 | * [Heap 和 Off\-Heap Memory](#heap-和-off-heap-memory) 6 | * [NetWork Memory:](#network-memory) 7 | * [Managed Memory](#managed-memory) 8 | * [JVM Metaspace](#jvm-metaspace) 9 | * [JVM Overhead](#jvm-overhead) 10 | * [三、内存特性解读](#三内存特性解读) 11 | * [Java内存类型](#java内存类型) 12 | * [Heap Memory特性](#heap-memory特性) 13 | * [Direct Memory特性](#direct-memory特性) 14 | * [Metaspace特性](#metaspace特性) 15 | * [Native Memory特性](#native-memory特性) 16 | * [Framework / Task Off\-Heap Memory](#framework--task-off-heap-memory) 17 | * [四、 配置方法](#四-配置方法) 18 | * [Expilcit & Implicit](#expilcit--implicit) 19 | * [如何避免配置冲突](#如何避免配置冲突) 20 | 21 | # Flink TaskExecutor内存管理 22 | 23 | ## 一、 背景 24 | 25 | - Flink 1.9及以前版本的TaskExecutor内存模型 26 | 27 | - 逻辑复杂难懂 28 | - 实际内存大小不确定 29 | - 不同场景及模块计算结果不一致 30 | - 批、流配置无法兼容 31 | 32 | - Flink 1.10引入了FLIP-49,对TaskExecutor内存模型进行了统一的梳理和简化 33 | 34 | - Flink 1.10内存模型: 35 | 36 | image-20200920223508792 37 | 38 | 39 | 40 | ## 二、Flink内存类型及用途 41 | 42 | image-20200920223853718 43 | 44 | > 左图是Flink1.10设计文档的内存模型图,有图为Flink 官方用户文档内存模型,实际上语义是一样的。 45 | > 46 | > 1. Process Memory: 一个Flink TaskExecutor进程使用的所有内存包含在内, 尤其是在容器化的环境中。 47 | > 2. Flink Memory:抛出JVM自身的开销,留下的专门用于Flink应用程序的内存用量,通常用于standlone模式下。 48 | 49 | 50 | 51 | ### Framework 和 Task Memory 52 | 53 | - 区别:是否计入Slot资源 54 | 55 | - 总用量受限: 56 | 57 | - -Xmx = Framework Heap + Task Heap 58 | - -XX:MaxDirectMemorySize= Network + Framework Off-Heap + Task Off-Heap 59 | 60 | - 无隔离 61 | 62 | image-20200921222757296 63 | 64 | > 1. 设置JVM的参数,但我们并没有在Slot与Framework之间进行隔离,一个TaskExecutor是一个进程,不管TaskExecutor进行的是Slot也好,还是框架的其他任务也好,都是线程级别的任务。那对于JVM的内存特点来说,它是不会在线程级别对内存的使用量进行限制的。那为什么还要引入Framework与Task的区别呢?为后续版本的准备:动态切割Slot资源。 65 | > 2. 不建议对Framework的内存进行调整,保留它的默认值。在Flink 1.10发布之前,它的默认值是通过跑一个空的Cluster,上面不调度任何任务的情况下,通过测量、计算出来的Framework所需要的内存。 66 | 67 | 68 | 69 | ### Heap 和 Off-Heap Memory 70 | 71 | - Heap 72 | - Java的堆内存,适用于绝大多数Java对象 73 | - HeapStateBackend 74 | - Off-Heap 75 | - Direct: Java中的直接内存,但凡一下两种方式申请的内存,都属于Direct 76 | - DirectByteBuffer 77 | - MappedByteBuffer 78 | - Native 79 | - Native内存指的是像JNI、C/C++、Python、etc所用的不受Jvm进程管控的一些内存。 80 | 81 | > Flink的配置模型没有要求用户对Direct和Native进行区分的,统一叫做Off-Heap。 82 | 83 | 84 | 85 | ### NetWork Memory: 86 | 87 | - 用于 88 | - 数据传输缓冲 89 | - 特点 90 | - 同一个TaskExecutor之间没有隔离 91 | - 需要多少由作业拓扑决定,不足会导致作业运行失败 92 | 93 | > 1. 使用的是Direct Memory,只不过由于Network Memory在配置了指定大小之后,在集群启动的时候,它会去由已经配置的大小去把内存申请下来,并且在整个TaskExecutor Shutdown之前,都不会去释放内存。 94 | > 2. 当通常考虑Network Memory用了多少,没有用多少的时候,这部分Network Memory实际上是一个常量,所以把它从Off-Heap里面拆出来单独管理。 95 | 96 | 97 | 98 | ### Managed Memory 99 | 100 | - 用于 101 | - RocksDBStateBackend 102 | - Batch Operator 103 | - HeapStateBackend / 无状态应用,不需要Managed Memory,可以设置为0 104 | - 特点 105 | - 同一TaskExecutor之间的多个Slot严格隔离 106 | - 多点少点都能跑,与性能挂钩 107 | - RocksDB内存限制 108 | - state.backend.rocksdb.memory.m anaged (default: true) 109 | - 设置RocksDB实用的内存大小 Managed Memory 大小 110 | - 目的:避免容器内存超用,RocksDB的内存申请是在RocksDB内部完成的,Flink没有办法进行直接干预,但可以通过设置RocksDB的参数,让RocksDB去申请的内存大小刚好不超过Managed Memory。主要目的是为了防止state比较多(一个state一个列族),RocksDB的内存超用,造成容器被Yarn/K8s Kill掉。 111 | 112 | > 1. 本质上是用的Native Memory,并不会受到JVM的Heap、Direct大小的限制。不受JVM的掌控的,但是Flink会对它进行管理,Flink会严格控制到底申请了多少Managed Memory。 113 | > 2. RocksDB实用C++写成的一个数据库,会用到Native Memory。 114 | > 3. 一个Slot有多少Managed Memory,一定没有办法超用,这是其中一个特点;不管是RocksDB的用户,还是Batch Operator的用户,Task并没有一个严格要用多少大小的memory,可能会有一个最低限度,但一般会很小。 115 | 116 | 117 | 118 | ### JVM Metaspace 119 | 120 | - 存放JVM加载的类的元数据 121 | - 加载的类越多,需要空间越大 122 | - 以下情况需要增大JVM Metaspace 123 | - 作业需要加载大量第三方库 124 | - 多个不同作业的任务运行在同一TaskExecutor上 125 | 126 | 127 | 128 | ### JVM Overhead 129 | 130 | - Native Memory 131 | - 用于Jvm其他开销 132 | - code cache 133 | - Thread Stack 134 | 135 | 136 | 137 | ## 三、内存特性解读 138 | 139 | ### Java内存类型 140 | 141 | 很多时候都在说,Flink使用到的内存包括Heap、Direct、Native Memory等,那么Java到底有多少种类型的内存?Java的内存分类是一个比较复杂的问题,但归根究底只有两种内存:Heap与Off-Heap。所谓Heap就是Java堆内存,Off-Heap就是堆外内存。 142 | 143 | - Heap 144 | 145 | - 经过JVM虚拟化的内存。 146 | - 实际存储地址可能随GC变化,上层应用无感知。 147 | 148 | > 一个Java对象,或者一个应用程序拿到一个Java对象之后,它并不需要去关注内存实际上是存放在哪里的,实际上也没有一个固定的地址。 149 | 150 | - Off-Heap 151 | - 未经JVM虚拟化的内存 152 | - 直接映射到本地OS内存地址 153 | 154 | image-20200920230448872 155 | 156 | > 1. Java的Heap内存空间实际上也是切分为了Eden、S0、S1、Tenured这几部分,所谓的垃圾回收机制会让对象在Eden空间中,随着Eden空间满了之后会触发GC,把已经没有使用的内存释放掉;已经引用的对象会移动到Servivor空间,在S0和S1之间反复复制,最后会放在老年代内存空间中。 这是Java的GC机制,造成的问题是Java对象会在内存空间中频繁的进行复制、拷贝。 157 | > 2. 所谓的Off-Heap内存,是直接使用操作系统提供的内存,也就是说内存地址是操作系统来决定的。一方面避免了内存在频繁GC过程中拷贝的操作,另外如果涉及到对一些OS的映射或者网络的一些写出、文件的一些写出,它避免了在用户空间复制的一个成本,所以它的效率会相对更高。 158 | > 3. Heap的内存大小是有-Xmx决定的,而Off-Heap部分:有一些像Direct,它虽然不是在堆内,但是JVM会去对申请了多少Direct Memory进行计数、进行限制。如果设置了-XX:MaxDirectMemorySize的参数,当它达到限制的时候,就不会去继续申请新的内存;同样的对于metaspace也有同样的限制。 159 | > 4. 既不受到Direct限制,也不受到Native限制的,是Native Memory。 160 | 161 | - Question:什么是JVM内(外)的内存? 162 | 163 | - 经过JVM虚拟化 164 | - Heap 165 | 166 | - 受JVM管理 167 | - 用量上限、申请、释放(GC)等 168 | - Heap、Direct、Metaspace、even some Native(e.g., Thread Stack) 169 | 170 | ### Heap Memory特性 171 | 172 | 在Flink当中,Heap Memory 173 | 174 | - 包括 175 | - Framework Heap Memory 176 | - Task Heap Memory 177 | - 用量上限受JVM严格控制 178 | - -Xmx:Framework Heap + Task Heap 179 | - 达到上限后触发垃圾回收(GC) 180 | - GC后仍然空间不足,触发OOM异常并退出 181 | - OutOfMemoryError: Java heap space 182 | 183 | ### Direct Memory特性 184 | 185 | - 包括: 186 | - Framework Off-Heap Memory(部分) 187 | - Task Off-Heap Memory(部分) 188 | - Network Memory 189 | - 用量上限受JVM严格控制 190 | - -XX:MaxDirectMemorySize 191 | - Framework Off-Heap + Task Off-Heap + Network Memory 192 | - 达到上限时触发GC,GC后仍然空间不足触发OOM异常并退出 193 | - OutOfMemoryError: Direct buffer memory 194 | 195 | ### Metaspace特性 196 | 197 | - 用量上限受JVM严格控制 198 | - -XX:MaxMetaspaceSize 199 | - 达到上限时触发GC,GC后仍然空间不足触发OOM异常并退出 200 | - OutOfMemoryError: Metaspace 201 | 202 | ### Native Memory特性 203 | 204 | - 包括 205 | - Framework Off-Heap Memory(小部分) 206 | - Task Off-Heap Memory(小部分) 207 | - Managed Memory 208 | - JVM Overhead 209 | - 用量上限不受JVM严格控制 210 | - 其中Managed Memory用量上限受Flink严格控制 211 | 212 | 213 | 214 | ### Framework / Task Off-Heap Memory 215 | 216 | - 既包括Direct也包括Native 217 | - 用户无需理解Direct / Native内存的区别并分别配置 218 | - 无法严格控制Direct内存用量,可能导致超用 219 | - 绝大数情况下 220 | - Flink框架及用户代码不需要或只需要少量Native内存 221 | - Heap活动足够频繁,能够及时触发GC释放不需要的Direct内存 222 | 223 | ![image-20200921183651039](FlinkTaskExecutor内存管理.assets/image-20200921183651039.png) 224 | 225 | > 如果需要大量使用Native内存,可以考虑增大JVM Overhead 226 | 227 | 228 | 229 | ## 四、 配置方法 230 | 231 | ### Expilcit & Implicit 232 | 233 | - Expilcit 234 | - Size,Min/Max 235 | - 严格保证(包括默认值) 236 | - 配置不当可能引发冲突 237 | 238 | - Implicit 239 | - Fraction 240 | - 非严格保证 241 | - 存在冲突时优先保证Expilcit 242 | 243 | > 1. 所有的size,min/max都是严格保证的,所谓严格保证:如果配置了一个size=100m, 配置结果一定是100m。如果配置了min/max是100-200m, 最终配置的内存大小一定在这个范围内。 244 | > 1. 与之对应的Implicit,如果并不想配比如Managed Memory的内存大小,但是可以指定Managed Memory就是Flink Memory的一个比例。 245 | 246 | 247 | 248 | ### 如何避免配置冲突 249 | 250 | - 以下三项至少配置一项,不建议同时配置两项及以上 251 | - Total Process 252 | - Total Flink 253 | - Task Heap & Managed 254 | - No Explicit Default 255 | 256 | -------------------------------------------------------------------------------- /docs/flink/Flink运行架构.assets/levels_of_abstraction.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 31 | 33 | 35 | 36 | 38 | image/svg+xml 39 | 41 | 42 | 43 | 44 | 45 | 48 | 51 | 55 | 59 | Stateful 65 | Stream Processing 71 | 75 | 79 | DataStream 85 | / 91 | DataSet 97 | API 103 | 107 | 111 | Table API 117 | 121 | 125 | SQL 131 | Core 137 | APIs 143 | Declarative DSL 149 | High 155 | - 161 | level Language 167 | Low 173 | - 179 | level building block 185 | (streams, state, [event] time) 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /docs/flink/Flink运行架构.assets/stsy_0101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Flink运行架构.assets/stsy_0101.png -------------------------------------------------------------------------------- /docs/flink/Flink运行架构.assets/stsy_0108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Flink运行架构.assets/stsy_0108.png -------------------------------------------------------------------------------- /docs/flink/Flink运行架构.assets/stsy_0209.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Flink运行架构.assets/stsy_0209.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/0f52d5e6a64a8a674ea6eb55682fb24f83b804b1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/0f52d5e6a64a8a674ea6eb55682fb24f83b804b1.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/2fe1c87966fcb64ed2cca1d1225dee783cc4f120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/2fe1c87966fcb64ed2cca1d1225dee783cc4f120.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/3bbfa2fcb679ad0113d61a16ab01a76af3a93ae2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/3bbfa2fcb679ad0113d61a16ab01a76af3a93ae2.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/5b0d71485571ac800df9f74f9b319aef806caadc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/5b0d71485571ac800df9f74f9b319aef806caadc.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/705b075f87873b510f6cf756c56e4be60abe95e9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/705b075f87873b510f6cf756c56e4be60abe95e9.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/878725ea9e43603b3397b4526e8aa09c1cab4176.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/878725ea9e43603b3397b4526e8aa09c1cab4176.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/8aa1145584f91c77fb72fd4bc7258888c0a446a5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/8aa1145584f91c77fb72fd4bc7258888c0a446a5.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/8c97ec8a78996471d2a0ea641af155e14f350439.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/8c97ec8a78996471d2a0ea641af155e14f350439.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/a0dda89c8f0a7215d5724eccd3f20e24721e47ae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/a0dda89c8f0a7215d5724eccd3f20e24721e47ae.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/a6bd33d762f58b36116e9cbb8625a824d53079cc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/a6bd33d762f58b36116e9cbb8625a824d53079cc.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/aacd4ced7363029902de9c084837de5176470d24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/aacd4ced7363029902de9c084837de5176470d24.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/cf8214259842bea68979d4dc6129233bb20fccc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/cf8214259842bea68979d4dc6129233bb20fccc3.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/d8fb8487d3588c7e51091ed1ae29f7add2585b93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/d8fb8487d3588c7e51091ed1ae29f7add2585b93.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/dca2de195328bc1b812f1d9427c7eec6d12f2427.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/dca2de195328bc1b812f1d9427c7eec6d12f2427.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/e50e00a21d07a779231ed8f4ab69ef13848daf38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/e50e00a21d07a779231ed8f4ab69ef13848daf38.png -------------------------------------------------------------------------------- /docs/flink/TheDataflowModel.assets/f35cf3a130d58de559fb03d13242323f79d24624.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/TheDataflowModel.assets/f35cf3a130d58de559fb03d13242323f79d24624.png -------------------------------------------------------------------------------- /docs/flink/Time与Watermark.assets/image-20201214180052040.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Time与Watermark.assets/image-20201214180052040.png -------------------------------------------------------------------------------- /docs/flink/Time与Watermark.assets/image-20201214180808722.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Time与Watermark.assets/image-20201214180808722.png -------------------------------------------------------------------------------- /docs/flink/Time与Watermark.assets/image-20201214180930910.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Time与Watermark.assets/image-20201214180930910.png -------------------------------------------------------------------------------- /docs/flink/Time与Watermark.assets/image-20201214191539402.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Time与Watermark.assets/image-20201214191539402.png -------------------------------------------------------------------------------- /docs/flink/Time与Watermark.assets/spaf_0309.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Time与Watermark.assets/spaf_0309.png -------------------------------------------------------------------------------- /docs/flink/Time与Watermark.assets/stsy_0209-20201214172457146.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Time与Watermark.assets/stsy_0209-20201214172457146.png -------------------------------------------------------------------------------- /docs/flink/Time与Watermark.md: -------------------------------------------------------------------------------- 1 | * [Time与Watermark](#time与watermark) 2 | * [时间语义](#时间语义) 3 | * [Watermark](#watermark) 4 | * [Watermark的类型](#watermark的类型) 5 | * [完美式Watermark](#完美式watermark) 6 | * [启发式Watermark](#启发式watermark) 7 | * [Watermark的传递](#watermark的传递) 8 | 9 | # Time与Watermark 10 | 11 | Dataflow模型从流处理的角度来审视数据处理流程,将批和流处理的数据抽象成数据集的概念,并将数据集划分为无界数据集和有界数据集,认为流处理是批处理的超集。模型定义了时间域(time domain)的概念,将时间明确的区分为事件时间(`event-time`)和处理时间(`process-time`)。 12 | 13 | 在处理时间中的哪个时刻触发计算结果(When in processing time are results materialized)?=> `watermark` 14 | 15 | `Watermark`回答了`When` => 什么时候触发计算。 16 | 17 | 18 | 19 | ## 时间语义 20 | 21 | 在无界数据处理中,主要关注以下两种时间语义: 22 | 23 | - Event time: 事件实际发生的时间。 24 | - processing time: 在系统中观测到事件的时间, 也就是时间被处理的时间。 25 | 26 | 在Flink中定义时间语义: 27 | 28 | ```scala 29 | // alternatively: 30 | // env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime) 31 | // env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) 32 | ``` 33 | 34 | 35 | 36 | 在理想的情况下,事件时间和处理时间总是相等的,事件发生时立即处理它们。然而,实际情况并非如此,event time和processing time之间的偏差是变化的。所谓乱序,其实是指 Flink 接收到的事件的先后顺序并不是严格的按照事件的 Event Time 顺序排列的。 37 | 38 | 因此,绘制event time和processing time的一张图,通常会得到下图中的红线的结果。 39 | 40 | stsy_0209 41 | 42 | 可以理解为,这条红线本质上就是watermark,可以视为函数*F*(*P*)→ *E*,其中F代表处理时间,E代表事件时间,即我们能够在处理时间点P判定事件时间推进到了E,换而言之,所有事件时间小于E的数据都已经到达了。 43 | 44 | ## Watermark 45 | 46 | 先通过一个实际场景来理解watermark是如何工作的: 47 | 48 | 1. 从kafka消息队列中消费消息,数值代表event time,可以观测到数据是乱序的,W(4) 和 W(9) 代表水位线。flink消费kafka中消息,并定义了基于event time,窗口大小为4s的窗口。 49 | 50 | image-20201214180052040 51 | 52 | 2. 事件中event time为1、3、2 的数据进入了第一个窗口,event time为 7 会进入第二个窗口,接着 3 依旧会进入第一个窗口,此时系统watermark >= 第一个窗口entTime,触发窗口计算发出结果。 53 | 54 | image-20201214180808722 55 | 56 | 3. 接着的event time为 5 和 6的数据 会进入到第二个窗口里面,数据 9 会进入在第三个窗口里面。 当watermark更新时,触发第二个窗口的计算. 57 | 58 | image-20201214180930910 59 | 60 | ### Watermark的类型 61 | 62 | #### 完美式Watermark 63 | 64 | - 这是理想情况下的watermark,使用完美水位线创建的pipeline从不需要处理延迟数据。也就是说,数据数据源是严格保证有序的,不需要考虑乱序的问题。实际上,完美的watermark对数据源要求比较高,但并不是不存在的,比如canal监控mysql中的Binlog数据,此时Kafka中每个Partition中的数据就是严格有序的。 65 | 66 | - Flink中严格水位线的API表达方式: 67 | 68 | ```scala 69 | env.addSource(new Source).assignAscendingTimestamps(_.ts) 70 | ``` 71 | 72 | 73 | 74 | #### 启发式Watermark 75 | 76 | - 启发式watermark的概念出自于Dataflow,大部分情况下,数据源都是乱序的,此时watermark机制便是启发式的,它任务event time小于watermark的数据都已经到达,此后到达的event time小于watermark的数据则成为迟到数据。 77 | - 是一种平衡延迟与准确性的一种机制,也就意味着它可能是不准确的。 78 | 79 | - Watermark引入: 80 | 81 | ```scala 82 | env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) 83 | 84 | // 每隔5秒产生一个watermark 85 | env.getConfig.setAutoWatermarkInterval(5000) 86 | ``` 87 | 88 | 产生watermark的逻辑:每隔5秒钟,Flink会调用AssignerWithPeriodicWatermarks的getCurrentWatermark()方法。如果方法返回一个时间戳大于之前水位的时间戳,那么算子会发出一个新的水位线。这个检查保证了水位线是单调递增的。如果方法返回的时间戳小于等于之前水位的时间戳,则不会产生新的watermark。 89 | 90 | 从自定义Watermark可以看到,不断获取事件的最大时间戳,周期性调用getCurrentWatermark()返回watermark。 91 | 92 | ```scala 93 | class PeriodicAssigner extends AssignerWithPeriodicWatermarks[SensorReading] { 94 | val bound: Long = 60 * 1000 // 延时为1分钟 95 | var maxTs: Long = Long.MinValue // 观察到的最大时间戳 96 | 97 | override def getCurrentWatermark: Watermark = { 98 | new Watermark(maxTs - bound) 99 | } 100 | 101 | override def extractTimestamp(r: SensorReading, previousTS: Long) = { 102 | maxTs = maxTs.max(r.timestamp) 103 | r.timestamp 104 | } 105 | } 106 | ``` 107 | 108 | - Flink内置Watermark生成器 109 | 110 | ```scala 111 | env.addSource(FlinkUtils.getFlinkKafkaConsumer()) 112 | .assignTimestampsAndWatermarks( 113 | new BoundedOutOfOrdernessTimestampExtractor[UserBehavior](Time.seconds(2)) { //设置延时时长 114 | override def extractTimestamp(t: UserBehavior): Long = t.timestamp 115 | }) 116 | ``` 117 | 118 | 119 | 120 | ### Watermark的传递 121 | 122 | - Flink将数据流拆分为多个分区,并通过单独的算子任务并行地处理每个分区(数据并行)。每个分区都是一个流,里面包含了带着时间戳的数据和watermark。一个算子与它前置或后续算子的连接方式有多种情况,所以它对应的任务可以从一个或多个“输入分区”接收数据和watermark,同时也可以将数据和watermark发送到一个或多个“输出分区”。 123 | 124 | - 任务为每个输入分区维护一个分区水位线(watermark)。当从一个分区接收到watermark时,它会比较新接收到的值和当前水位值,然后将相应的分区watermark更新为两者的最大值(**`Watermark是单调递增的`**)。然后,任务会比较所有分区watermark的大小,将其事件时钟更新为所有分区watermark的最小值(**`Watermark对齐`**)。如果事件时间时钟前进了,任务就将处理所有被触发的定时器操作,并向所有连接的输出分区发送出相应的watermark,最终将新的事件时间广播给所有下游任务。 125 | 126 | image-20201214191539402 127 | 128 | - Flink的水位处理和传递算法,确保了算子任务发出的时间戳和watermark是“对齐”的。不过它依赖一个条件,**`那就是所有分区都会提供不断增长的watermark`**。一旦一个分区不再推进水位线的上升,或者完全处于空闲状态、不再发送任何数据和watermark,任务的事件时间时钟就将停滞不前,任务的定时器也就无法触发了。 129 | 130 | - 理解这一点非常重要,在生产环境中,消费Kafka中数据,一般来说,任务并行度会设置为与Kafka Partition数量一致,当分区数据倾斜严重时,会严重影响整个任务的计算;**`当某分区没有数据时,整个任务都不会触发计算`**,如果做双流Join,可能会遇到A流Watermark更新,但B流Watermark不更新的情况,导致整个任务停滞不前。所以应该尽量避免这种情况,良好的系统设计才是关键。 -------------------------------------------------------------------------------- /docs/flink/ValueState中存Map与MapState有什么区别.md: -------------------------------------------------------------------------------- 1 | * [ValueState中存Map与MapState有什么区别](#valuestate中存map与mapstate有什么区别) 2 | * [1、 结论](#1-结论) 3 | * [性能](#性能) 4 | * [TTL](#ttl) 5 | * [举一反三](#举一反三) 6 | * [2、 State 中要存储哪些数据](#2-state-中要存储哪些数据) 7 | * [Key](#key) 8 | * [Namespace](#namespace) 9 | * [Value、UserKey、UserValue](#valueuserkeyuservalue) 10 | * [3、 StateBackend 中是如何存储和读写 State 数据的](#3-statebackend-中是如何存储和读写-state-数据的) 11 | * [3\.1 Heap 模式 ValueState 和 MapState 是如何存储的](#31-heap-模式-valuestate-和-mapstate-是如何存储的) 12 | * [回到正题:Heap 模式下,ValueState 中存 Map 与 MapState 有什么区别?](#回到正题heap-模式下valuestate-中存-map-与-mapstate-有什么区别) 13 | * [3\.2 RocksDB 模式 ValueState 和 MapState 是如何存储的](#32-rocksdb-模式-valuestate-和-mapstate-是如何存储的) 14 | * [3\.2\.1 ValueState 如何映射成 RocksDB 的 kv](#321-valuestate-如何映射成-rocksdb-的-kv) 15 | * [3\.2\.2 MapState 如何映射成 RocksDB 的 kv](#322-mapstate-如何映射成-rocksdb-的-kv) 16 | * [3\.3 RocksDB 模式下,ValueState 中存 Map 与 MapState 有什么区别?](#33-rocksdb-模式下valuestate-中存-map-与-mapstate-有什么区别) 17 | * [3\.3\.1 假设 Map 集合有 100 个 KV 键值对,具体两种方案会如何存储数据?](#331-假设-map-集合有-100-个-kv-键值对具体两种方案会如何存储数据) 18 | * [3\.3\.2 修改 Map 中的一个 KV 键值对的流程](#332-修改-map-中的一个-kv-键值对的流程) 19 | * [3\.3\.3 结论](#333-结论) 20 | * [3\.4 直接拼接 key 和 namespace 可能导致 RocksDB 的 key 冲突](#34-直接拼接-key-和-namespace-可能导致-rocksdb-的-key-冲突) 21 | * [解决方案:](#解决方案) 22 | * [3\.5 RocksDB 的 key 中还会存储 KeyGroupId](#35-rocksdb-的-key-中还会存储-keygroupid) 23 | * [4\. State TTL 简述](#4-state-ttl-简述) 24 | * [ValueState 中存 Map 与 MapState 有什么区别?](#valuestate-中存-map-与-mapstate-有什么区别) 25 | * [5\. 总结](#5-总结) 26 | * [性能](#性能-1) 27 | * [TTL](#ttl-1) 28 | 29 | # ValueState中存Map与MapState有什么区别 30 | 31 | 本文主要讨论一个问题:ValueState 中存 Map 与 MapState 有什么区别? 32 | 33 | 如果不懂这两者的区别,而且使用 ValueState 中存大对象,生产环境很可能会出现以下问题: 34 | 35 | - CPU 被打满 36 | - 吞吐上不去 37 | 38 | ## 1、 结论 39 | 40 | 从性能和 TTL 两个维度来描述区别。 41 | 42 | #### 性能 43 | 44 | - RocksDB 场景,MapState 比 ValueState 中存 Map 性能高很多 45 | 46 | - - 生产环境强烈推荐使用 MapState,不推荐 ValueState 中存大对象 47 | - ValueState 中存大对象很容易使 CPU 打满 48 | 49 | - Heap State 场景,两者性能类似 50 | 51 | #### TTL 52 | 53 | Flink 中 State 支持设置 TTL 54 | 55 | - MapState 的 TTL 是基于 UK 级别的 56 | - ValueState 的 TTL 是基于整个 key 的 57 | 58 | ### 举一反三 59 | 60 | 能使用 ListState 的场景,不要使用 ValueState 中存 List。 61 | 62 | 大佬们已经把 MapState 和 ListState 性能都做了很多优化,高性能不香吗? 63 | 64 | **下文会详细分析 ValueState 和 MapState 底层的实现原理,通过分析原理得出上述结论。** 65 | 66 | ## 2、 State 中要存储哪些数据 67 | 68 | ValueState 会存储 key、namespace、value,缩写为 。 69 | 70 | MapState 会存储 key、namespace、userKey、userValue,缩写为 。 71 | 72 | 解释一下上述这些名词 73 | 74 | ### Key 75 | 76 | ValueState 和 MapState 都是 KeyedState,也就是 keyBy 后才能使用 ValueState 和 MapState。所以 State 中肯定要保存 key。 77 | 78 | 例如:按照 app 进行 keyBy,总共有两个 app,分别是:app1 和 app2。那么状态存储引擎中肯定要存储 app1 或 app2,用于区分当前的状态数据到底是 app1 的还是 app2 的。 79 | 80 | 这里的 app1、app2 也就是所说的 key。 81 | 82 | ### Namespace 83 | 84 | Namespace 用于区分窗口。 85 | 86 | 假设需要统计 app1 和 app2 每个小时的 pv 指标,则需要使用小时级别的窗口。状态引擎为了区分 app1 在 7 点和 8 点的 pv 值,就必须新增一个维度用来标识窗口。 87 | 88 | Flink 用 Namespace 来标识窗口,这样就可以在状态引擎中区分出 app1 在 7 点和 8 点的状态信息。 89 | 90 | ### Value、UserKey、UserValue 91 | 92 | ValueState 中存储具体的状态值。也就是上述例子中对应的 pv 值。 93 | 94 | MapState 类似于 Map 集合,存储的是一个个 KV 键值对。为了与 keyBy 的 key 进行区分,所以 Flink 中把 MapState 的 key、value 分别叫 UserKey、UserValue。 95 | 96 | 下面讲述状态引擎是如何存储这些数据的。 97 | 98 | ## 3、 StateBackend 中是如何存储和读写 State 数据的 99 | 100 | Flink 支持三种 StateBackend,分别是:MemoryStateBackend、FsStateBackend 和 RocksDBStateBackend。 101 | 102 | 其中 MemoryStateBackend、FsStateBackend 两种 StateBackend 在任务运行期间都会将 State 存储在内存中,两者在 Checkpoint 时将快照存储的位置不同。RocksDBStateBackend 在任务运行期间将 State 存储在本地的 RocksDB 数据库中。 103 | 104 | 所以下文将 MemoryStateBackend、FsStateBackend 统称为 heap 模式,RocksDBStateBackend 称为 RocksDB 模式。 105 | 106 | ### 3.1 Heap 模式 ValueState 和 MapState 是如何存储的 107 | 108 | Heap 模式表示所有的状态数据都存储在 TM 的堆内存中,所有的状态都存储的原始对象,不会做序列化和反序列化。(注:Checkpoint 的时候会涉及到序列化和反序列化,数据的正常读写并不会涉及,所以这里先不讨论。) 109 | 110 | Heap 模式下,无论是 ValueState 还是 MapState 都存储在 `CopyOnWriteStateMap` 中。 111 | 112 | key 、 Namespace 分别对应 CopyOnWriteStateMap 的 K、N。 113 | 114 | ValueState 的 value 对应 CopyOnWriteStateMap 的 V。 115 | 116 | MapState 将会把整个 Map 作为 CopyOnWriteStateMap 的 V,相当于 Flink 引擎创建了一个 HashMap 用于存储 MapState 的 KV 键值对。 117 | 118 | 具体 CopyOnWriteStateMap 是如何实现的,可以参考《[万字长文详解 Flink 中的 CopyOnWriteStateTable](https://mp.weixin.qq.com/s?__biz=MzkxOTE3MDU5MQ==&mid=2247484334&idx=1&sn=1aafab741bfd0e2e72652a4b459579c7&scene=21#wechat_redirect)》。 119 | 120 | #### 回到正题:Heap 模式下,ValueState 中存 Map 与 MapState 有什么区别? 121 | 122 | heap 模式下没有区别。 123 | 124 | ValueState 中存 Map,相当于用户手动创建了一个 HashMap 当做 V 放到了状态引擎中。 125 | 126 | 而 MapState 是 Flink 引擎帮用户创建了一个 HashMap 当做 V 放到了状态引擎中。 127 | 128 | 所以实质上 ValueState 中存 Map 与 MapState 都是一样的,存储结构都是 `CopyOnWriteStateMap` 。区别在于 ValueState 是用户手动创建 HashMap,MapState 是 Flink 引擎创建 HashMap。 129 | 130 | ### 3.2 RocksDB 模式 ValueState 和 MapState 是如何存储的 131 | 132 | RocksDB 模式表示所有的状态数据存储在 TM 本地的 RocksDB 数据库中。RocksDB 是一个 KV 数据库,且所有的 key 和 value 都是 byte 数组。所以无论是 ValueState 还是 MapState,存储到 RocksDB 中都必须将对象序列化成二进制当前 kv 存储在 RocksDB 中。 133 | 134 | #### 3.2.1 ValueState 如何映射成 RocksDB 的 kv 135 | 136 | ValueState 有 key、namespace、value 需要存储,所以最简单的思路: 137 | 138 | 1. 将 ValueState 的 key 序列化成 byte 数组 139 | 2. 将 ValueState 的 namespace 序列化成 byte 数组 140 | 3. 将两个 byte 数组拼接起来做为 RocksDB 的 key 141 | 4. 将 ValueState 的 value 序列化成 byte 数组做为 RocksDB 的 value 142 | 143 | 然后就可以写入到 RocksDB 中。 144 | 145 | 查询数据也用相同的逻辑:将 key 和 namespace 序列化后拼接起来作为 RocksDB 的 key,去 RocksDB 中进行查询,查询到的 byte 数组进行反序列化就得到了 ValueState 的 value。 146 | 147 | 这就是 RocksDB 模式下,ValueState 的读写流程。 148 | 149 | #### 3.2.2 MapState 如何映射成 RocksDB 的 kv 150 | 151 | MapState 有 key、namespace、userKey、userValue 需要存储,所以最简单的思路: 152 | 153 | 1. 将 MapState 的 key 序列化成 byte 数组 154 | 2. 将 MapState 的 namespace 序列化成 byte 数组 155 | 3. 将 MapState 的 userKey 序列化成 byte 数组 156 | 4. 将三个 byte 数组拼接起来做为 RocksDB 的 key 157 | 5. 将 MapState 的 value 序列化成 byte 数组做为 RocksDB 的 value 158 | 159 | 然后就可以写入到 RocksDB 中。 160 | 161 | 查询数据也用相同的逻辑:将 key、namespace、userKey 序列化后拼接起来作为 RocksDB 的 key,去 RocksDB 中进行查询,查询到的 byte 数组进行反序列化就得到了 MapState 的 userValue。 162 | 163 | 这就是 RocksDB 模式下,MapState 的读写流程。 164 | 165 | ### 3.3 RocksDB 模式下,ValueState 中存 Map 与 MapState 有什么区别? 166 | 167 | #### 3.3.1 假设 Map 集合有 100 个 KV 键值对,具体两种方案会如何存储数据? 168 | 169 | ValueState 中存 Map,Flink 引擎会把整个 Map 当做一个大 Value,存储在 RocksDB 中。对应到 RocksDB 中,100 个 KV 键值对的 Map 集合会序列化成一个 byte 数组当做 RocksDB 的 value,存储在 RocksDB 的 1 行数据中。 170 | 171 | MapState 会根据 userKey,将 100 个 KV 键值对分别存储在 RocksDB 的 100 行中。 172 | 173 | #### 3.3.2 修改 Map 中的一个 KV 键值对的流程 174 | 175 | ValueState 的情况,虽然要修改 Map 中的一个 KV 键值对,但需要将整个 Map 集合从 RocksDB 中读出来。具体流程如下: 176 | 177 | 1. 将 key、namespace 序列化成 byte 数组,生成 RocksDB 的 key 178 | 2. 从 RocksDB 读出 key 对应 value 的 byte 数组 179 | 3. 将 byte 数组反序列化成整个 Map 180 | 4. 堆内存中修改 Map 集合 181 | 5. 将 Map 集合写入到 RocksDB 中,需要将整个 Map 集合序列化成 byte 数组,再写入 182 | 183 | MapState 的情况,要修改 Map 中的一个 KV 键值对,根据 key、namespace、userKey 即可定位到要修改的那一个 KV 键值对。具体流程如下: 184 | 185 | 1. 将 key、namespace、userKey 序列化成 byte 数组,生成 RocksDB 的 key 186 | 2. 从 RocksDB 读出 key 对应 value 的 byte 数组 187 | 3. 将 byte 数组反序列化成 userValue 188 | 4. 堆内存中修改 userValue 的值 189 | 5. 将 userKey、userValue 写入到 RocksDB 中,需要先序列化,再写入 190 | 191 | #### 3.3.3 结论 192 | 193 | 要修改 Map 中的一个 KV 键值对: 194 | 195 | 如果使用 ValueState 中存 Map,则每次修改操作需要序列化反序列化整个 Map 集合,每次序列化反序列大对象会非常耗 CPU,很容易将 CPU 打满。 196 | 197 | 如果使用 MapState,每次修改操作只需要序列化反序列化 userKey 那一个 KV 键值对的数据,效率较高。 198 | 199 | 举一反三:其他使用 ValueState、value 是大对象且 value 频繁更新的场景,都容易将 CPU 打满。 200 | 201 | 例如:ValueState 中存储的位图,如果每条数据都需要更新位图,则可能导致 CPU 被打满。 202 | 203 | 为了便于理解,上述忽略了一些实现细节,下面补充一下: 204 | 205 | ### 3.4 直接拼接 key 和 namespace 可能导致 RocksDB 的 key 冲突 206 | 207 | 假设 ValueState 中有两个数据: 208 | 209 | key1 序列化后的二进制为 0x112233, namespace1 序列化后的二进制为0x4455 210 | 211 | key2 序列化后的二进制为 0x1122, namespace2 序列化后的二进制为0x334455 212 | 213 | 这两个数据对应的 RocksDB key 都是 0x1122334455,这样的话,两个不同的 key、namespace 映射到 RocksDB 中变成了相同的数据,无法做区分。 214 | 215 | ##### 解决方案: 216 | 217 | 在 key 和 namespace 中间写入 key 的 byte 数组长度,在 namespace 后写入 namespace 的 byte 长度。 218 | 219 | 写入这两个长度就不可能出现 key 冲突了,具体为什么,读者可以自行思考。 220 | 221 | ### 3.5 RocksDB 的 key 中还会存储 KeyGroupId 222 | 223 | 对 KeyGroup 不了解的同学可以参考《[Flink 源码:从 KeyGroup 到 Rescale](https://mp.weixin.qq.com/s?__biz=MzkxOTE3MDU5MQ==&mid=2247484339&idx=1&sn=c83b5fc85f6abadaa7ac94f08c571b31&scene=21#wechat_redirect)》,加上 KeyGroupId 也比较简单。只需要修改 RocksDB key 的拼接方式,在序列化 key 和 namespace 之前,先序列化 KeyGroupId 即可。 224 | 225 | ## 4. State TTL 简述 226 | 227 | Flink 中 TTL 的实现,都是将用户的 value 封装了一层,具体参考下面的 TtlValue 类: 228 | 229 | ```java 230 | public class TtlValue implements Serializable { 231 | @Nullable 232 | private final T userValue; 233 | private final long lastAccessTimestamp; 234 | } 235 | ``` 236 | 237 | TtlValue 类中有两个字段,封装了用户的 value 且有一个时间戳字段,这个时间戳记录了这条数据写入的时间。 238 | 239 | 如果开启了 TTL,则状态中存储的 value 就是 TtlValue 对象。时间戳字段也会保存到状态引擎中,之后查询数据时,就可以通过该时间戳判断数据是否过期。 240 | 241 | ValueState 将 value 封装为 TtlValue。 242 | 243 | MapState 将 userValue 封装成 TtlValue。 244 | 245 | ListState 将 element 封装成 TtlValue。 246 | 247 | ### ValueState 中存 Map 与 MapState 有什么区别? 248 | 249 | 如果 ValueState 中存 Map,则整个 Map 被当做 value,只维护一个时间戳。所以要么整个 Map 过期,要么都不过期。 250 | 251 | MapState 中如果存储了 100 个 KV 键值对,则 100 个 KV 键值对都会存储各自的时间戳。因此每个 KV 键值对的 TTL 是相互独立的。 252 | 253 | ## 5. 总结 254 | 255 | 本文从实现原理详细分析了 ValueState 中存 Map 与 MapState 有什么区别? 256 | 257 | 从性能和 TTL 两个维度来描述两者的区别。 258 | 259 | #### 性能 260 | 261 | - RocksDB 场景,MapState 比 ValueState 中存 Map 性能高很多,ValueState 中存大对象很容易使 CPU 打满 262 | - Heap State 场景,两者性能类似 263 | 264 | #### TTL 265 | 266 | Flink 中 State 支持设置 TTL,TTL 只是将时间戳与 userValue 封装起来。 267 | 268 | - MapState 的 TTL 是基于 UK 级别的 269 | - ValueState 的 TTL 是基于整个 key 的 270 | 271 | 扩展:其实 ListState 的数据映射到 RocksDB 比较复杂,用到了 RocksDB 的 merge 特性,比较有意思,有兴趣的同学可以阅读 RocksDB wiki《Merge Operator Implementation》,链接:https://github.com/facebook/rocksdb/wiki/Merge-Operator-Implementation -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20201215114037836.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20201215114037836.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20201215154505904.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20201215154505904.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20201215172027772.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20201215172027772.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20201215190623569.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20201215190623569.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20201215190654537.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20201215190654537.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20201215235014908.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20201215235014908.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20201215235056407.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20201215235056407.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20201215235435915.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20201215235435915.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20201216000133111.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20201216000133111.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20210103103848320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20210103103848320.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/image-20210103103925904.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/image-20210103103925904.png -------------------------------------------------------------------------------- /docs/flink/Window.assets/stsy_0108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/Window.assets/stsy_0108.png -------------------------------------------------------------------------------- /docs/flink/Window.md: -------------------------------------------------------------------------------- 1 | * [Window](#window) 2 | * [Window抽象概念](#window抽象概念) 3 | * [Window分类](#window分类) 4 | * [Time Window](#time-window) 5 | * [Fixed Window](#fixed-window) 6 | * [Silding Window](#silding-window) 7 | * [Session Window](#session-window) 8 | * [Count Window](#count-window) 9 | * [Tumbling count window](#tumbling-count-window) 10 | * [Sliding count window](#sliding-count-window) 11 | * [窗口函数](#窗口函数) 12 | * [RedeceFunction](#redecefunction) 13 | * [AggregateFunction](#aggregatefunction) 14 | * [ProcessWindowFunction](#processwindowfunction) 15 | * [增量聚合与ProcessWindowFunction](#增量聚合与processwindowfunction) 16 | * [Window Assigner](#window-assigner) 17 | * [Window Trigger](#window-trigger) 18 | * [TriggerResult](#triggerresult) 19 | * [Trigger接口](#trigger接口) 20 | * [Window Trigger触发机制](#window-trigger触发机制) 21 | * [EventTimeTrigger](#eventtimetrigger) 22 | * [ContinuousEventTimeTrigger](#continuouseventtimetrigger) 23 | * [DeltaTrigger](#deltatrigger) 24 | * [总结](#总结) 25 | * [Window Evictor](#window-evictor) 26 | * [总结](#总结-1) 27 | 28 | # Window 29 | 30 | 回到Dataflow的思想,从流处理的角度来审视数据处理过程。对于无边界数据的处理,**`Where:Where in event time are results calculated?`** 计算什么时间(event time)范围的数据,答案是:通过使用pipeline中的event time窗口。 31 | 32 | >事实上,Flink官网对Window的讲解以及使用已经足够详细了,总结这篇文章完全是多余,一定要阅读[**Flink官网-Window**](https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/stream/operators/windows.html)。 33 | 34 | ### Window抽象概念 35 | 36 | ![image-20201215154505904](Window.assets/image-20201215154505904.png) 37 | 38 | ## Window分类 39 | 40 | ### Time Window 41 | 42 | Window,也就是窗口,将一部分数据集合组合起操作。在处理无限数据集的时候有限操作需要窗口,比如 **aggregation**,**outer join**,**time-bounded** 操作。窗口大部分都是基于时间来划分,但是也有基于其他存在逻辑上有序关系的数据来划分的。窗口模型主要由三种:**Fixed Window**,**Sliding Window**,**Session Window**。 43 | 44 | ![stsy_0108](Window.assets/stsy_0108.png) 45 | 46 | Flink对于窗口的通用定义: 47 | 48 | - **Keyed Windows** 49 | 50 | ```java 51 | stream 52 | .keyBy(...) <- keyed versus non-keyed windows 53 | .window(...) <- required: "assigner" 54 | [.trigger(...)] <- optional: "trigger" (else default trigger) 55 | [.evictor(...)] <- optional: "evictor" (else no evictor) 56 | [.allowedLateness(...)] <- optional: "lateness" (else zero) 57 | [.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data) 58 | .reduce/aggregate/fold/apply() <- required: "function" 59 | [.getSideOutput(...)] <- optional: "output tag" 60 | ``` 61 | 62 | - **Non-Keyed Windows** 63 | 64 | ```scala 65 | stream 66 | .windowAll(...) <- required: "assigner" 67 | [.trigger(...)] <- optional: "trigger" (else default trigger) 68 | [.evictor(...)] <- optional: "evictor" (else no evictor) 69 | [.allowedLateness(...)] <- optional: "lateness" (else zero) 70 | [.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data) 71 | .reduce/aggregate/fold/apply() <- required: "function" 72 | [.getSideOutput(...)] <- optional: "output tag" 73 | ``` 74 | 75 | 76 | 77 | #### Fixed Window 78 | 79 | Fixed Window ,有时候也叫 Tumbling Window。Tumble 的中文翻译有“翻筋斗”,我们可以将 Fixed Window 是特定的时间长度在无限数据集合上翻滚形成的,核心是每个 Window 没有重叠。比如小时窗口就是 12:00:00 ~ 13:00:00 一个窗口,13:00:00 ~ 14:00:00 一个窗口。从例子也可以看出来 Fixed Window 的另外一个特征:aligned,中文一般称为对齐。 80 | 81 | 特点: 82 | 83 | - 将数据按照固定的窗口长度对数据进行切分。 84 | - 时间对齐,窗口长度固定,没有重叠。 85 | 86 | 以下代码展示如何在传感数据流上定义事件事件和处理时间滚动窗口: 87 | 88 | ```scala 89 | val sensorData: DataStream[SensorReading] = ... 90 | // 基于事件时间的滚动窗口 91 | val avgTemp = sensorData 92 | .keyBy(_.id) 93 | .window(TumblingEventTimeWindows.of(Time.seconds(1))) 94 | .process(new TemperatureAverager) 95 | 96 | // 基于处理时间的滚动窗口 97 | val avgTemp = sensorData 98 | .keyBy(_.id) 99 | .window(TumblingProcessingTimeWindows.of(Time.seconds(1))) 100 | .process(new TemperatureAverager) 101 | 102 | val avgTemp = sensorData 103 | .keyBy(_.id) 104 | // 以上写法的简写,具体调用哪个方法取决于配置的时间特性。 105 | // shortcut for window.(TumblingEventTimeWindows.of(size)) 106 | .timeWindow(Time.seconds(1)) 107 | .process(new TemperatureAverager) 108 | 109 | ``` 110 | 111 | 112 | 113 | #### Silding Window 114 | 115 | Sliding Window,中文可以叫滑动窗口,由两个参数确定,窗口大小和滑动间隔。比如每分钟开始一个小时窗口对应的就是窗口大小为一小时,滑动间隔为一分钟。滑动间隔一般小于窗口大小,也就是说窗口之间会有重叠。滑动窗口在很多情况下都比较有用,比如检测机器的半小时负载,每分钟检测一次。Fixed Window 是 Sliding Window 的一种特例:窗口大小等于滑动间隔。 116 | 117 | 特点: 118 | 119 | - 滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动间隔组成。 120 | 121 | - 窗口长度固定,可以有重叠,也是一种对齐窗口。 122 | 123 | - 处理时间滑动窗口分配器: 124 | 125 | ```scala 126 | // processing-time sliding windows assigner 127 | val slidingAvgTemp = sensorData 128 | .keyBy(_.id) 129 | // create 1h processing-time windows every 15 minutes 130 | .window(SlidingProcessingTimeWindows.of(Time.hours(1), 131 | Time.minutes(15))) 132 | .process(new TemperatureAverager) 133 | 134 | 135 | // 使用窗口分配器简写方法: 136 | // sliding windows assigner using a shortcut method 137 | val slidingAvgTemp = sensorData 138 | .keyBy(_.id) 139 | // shortcut for window.(SlidingEventTimeWindow.of(size, 140 | slide)) 141 | .timeWindow(Time.hours(1), Time(minutes(15))) 142 | .process(new TemperatureAverager) 143 | ``` 144 | 145 | 146 | 147 | #### Session Window 148 | 149 | Session Window,会话窗口, 会话由事件序列组成,这些事件序列以大于某个超时的不活动间隔终止(两边等),回话窗口不能事先定义,取决于数据流。一般用来捕捉一段时间内的行为,比如 Web 中一段时间内的登录行为为一个 Session,当长时间没有登录,则 Session 失效,再次登录重启一个 Session。Session Window 也是用超时时间来衡量,只要在超时时间内发生的事件都认为是一个 Session Window。 150 | 151 | 特点: 152 | 153 | - 是一种非对齐窗口, Window Size可变,根据Session gap切分不同的窗口 154 | 155 | - 使用场景,如:用户访问Session分析、基于Window的双流Join 156 | 157 | - 基于Session Window实现双流的left join: 158 | 159 | ```scala 160 | input1.coGroup(input2) 161 | .where(_.order_item_id) 162 | .equalTo(_.item_id) 163 | .window(EventTimeSessionWindows.withGap(Time.seconds(5))) 164 | .apply(new CoGroupFunction) 165 | ``` 166 | 167 | ### Count Window 168 | 169 | #### Tumbling count window 170 | 171 | ```scala 172 | keyedStream.countWindow(100) 173 | ``` 174 | 175 | #### Sliding count window 176 | 177 | ```scala 178 | keyedStream.countWindow(100, 10) 179 | ``` 180 | 181 | 如果对于DataStream,但并行度而言: 182 | 183 | ```scala 184 | stream.countWindowAll(20, 10) 185 | ``` 186 | 187 | ## 窗口函数 188 | 189 | 窗口函数定义了针对窗口内元素的计算逻辑。可用于窗口的函数类型有两种: 190 | 191 | - 增量聚合函数: 它的应用场景是窗口内以状态形式存储某个值且需要根据每个加入窗口的元素对该值进行更新。来一条计算一条,将结果保存为状态。 特点:节省空间且最终会将聚合值作为单个结果发出。 ReduceFunction和AggregateFunction属于增量聚合函数。 192 | - 全量窗口函数 收集窗口内所有元素,并在执行计算时对它们进行遍历。 通常占用更多空间,但支持更复杂的逻辑。 ProcessWindowFunction是全量窗口函数。 193 | 194 | ### RedeceFunction 195 | 196 | - keyedStream -> dataStream 197 | 198 | - 在被用到窗口内数据流时,会对窗口内元素进行增量聚合。 199 | 200 | - 将聚合结构保存为一个状态。 201 | 202 | - 要求输入、输出类型必须一致,所以仅用于一些简单聚合。 203 | 204 | 205 | 206 | 使用案例:在WindowedStream上应用reduce函数,计算每15秒的最低温度 207 | 208 | ```scala 209 | val minTempPerWindow: DataStream[(String, Double)] = sensonData 210 | .map(r => (r.id, r.temperature)) 211 | .keyBy(_._1) 212 | .timeWindow(Time.seconds(15)) 213 | .reduce((r1, r2) => (r1._1, r1._2.min(r2._2))) 214 | 215 | ``` 216 | 217 | 218 | 219 | ### AggregateFunction 220 | 221 | - 和RedeceFunction类似,状态也是一个值。 222 | 223 | - AggregateFunction接口: 224 | 225 | ```scala 226 | public interface AggregateFunction extends 227 | Function, Serializable { 228 | // create a new accumulator to start a new aggregate. 229 | ACC createAccumulator(); 230 | 231 | // add an input element to the accumulator and return the accumulator. 232 | ACC add(IN value, ACC accumulator); 233 | 234 | // compute the result from the accumulator and return it. 235 | OUT getResult(ACC accumulator); 236 | 237 | // merge two accumulators and return the result. 238 | ACC merge(ACC a, ACC b); 239 | } 240 | ``` 241 | 242 | - 使用AggregateFunction计算每个窗口内传感器读数的平均温度。累加器负责维护不断变化的温度总和和数量。 243 | 244 | ```scala 245 | // 2.使用AggregateFunction计算每个窗口内传感器读数的平均温度。 246 | sensonData 247 | .map(r => (r.id, r.temperature)) 248 | .keyBy(_._1) 249 | .timeWindow(Time.seconds(15)) 250 | .aggregate(new AvgAggregateFunction()) 251 | 252 | class AvgAggregateFunction() extends AggregateFunction[(String,Double),(String,Double,Int),(String,Double)]{ 253 | // 初始化累加器 254 | override def createAccumulator(): (String, Double, Int) = ("",0.0,0) 255 | 256 | // 每来一条数据执行的逻辑,注意数据并行执行 257 | override def add(value: (String, Double), accumulator: (String, Double, Int)): (String, Double, Int) = (value._1,accumulator._2 + value._2,accumulator._3 +1) 258 | 259 | override def getResult(accumulator: (String, Double, Int)): (String, Double) = (accumulator._1,accumulator._2 / accumulator._3) 260 | 261 | override def merge(a: (String, Double, Int), b: (String, Double, Int)): (String, Double, Int) = (a._1,a._2 + b._2,a._3 + b._3) 262 | } 263 | 264 | ``` 265 | 266 | ### ProcessWindowFunction 267 | 268 | - 是一个全量窗口函数。 269 | 270 | - 需要访问窗口内的所有元素来执行一些更加复杂的计算,例如计算窗口内数据的中值或出现频率最高的值。 271 | 272 | - ProcessWindowFunction接口: 273 | 274 | ```scala 275 | public abstract class ProcessWindowFunction 277 | extends AbstractRichFunction { 278 | // 对窗口执行计算 279 | void process( 280 | KEY key, Context ctx, Iterable vals, Collector 281 | out) throws Exception; 282 | 283 | // 在窗口清除时删除自定义的单个窗口状态 284 | public void clear(Context ctx) throws Exception { 285 | } 286 | 287 | // 保存窗口元数据的上下文 288 | public abstract class Context implements Serializable { 289 | // 返回窗口的元数据 290 | public abstract W window(); 291 | 292 | // 返回当前处理时间 293 | public abstract long currentProcessingTime(); 294 | 295 | // 返回当前事件时间水位线 296 | public abstract long currentWatermark(); 297 | 298 | // 用于单个窗口状态的访问器 299 | public abstract KeyedStateStore windowState(); 300 | 301 | // 用于每个键值全局状态的访问器 302 | public abstract KeyedStateStore globalState(); 303 | // 向OutputTag标识的副输出发送状态 304 | 305 | public abstract void output(OutputTag outputTag, X 306 | value); 307 | } 308 | } 309 | 310 | ``` 311 | 312 | - 使用ProcessWindowFunction计算每个传感器在每个窗口内的最低温和最高温 313 | 314 | ```scala 315 | // 3.计算每个传感器在每个窗口内的最低温和最高温 316 | sensorData 317 | .keyBy(_.id) 318 | .timeWindow(Time.seconds(5)) 319 | .process(new HighAndLowTempProcessFunction()) 320 | 321 | case class MinMaxTemp(id:String,min:Double,max:Double,endTs:Long) 322 | 323 | class HighAndLowTempProcessFunction 324 | extends ProcessWindowFunction[SensorReading, MinMaxTemp, String, TimeWindow] { 325 | 326 | override def process( 327 | key: String, 328 | ctx: Context, 329 | vals: Iterable[SensorReading], 330 | out: Collector[MinMaxTemp]): Unit = { 331 | 332 | val temps = vals.map(_.temperature) 333 | val windowEnd = ctx.window.getEnd 334 | 335 | out.collect(MinMaxTemp(key, temps.min, temps.max, windowEnd)) 336 | } 337 | } 338 | ``` 339 | 340 | - ProcessWindowFunction中的Context对象除了访问当前时间和事件时间、访问侧输出外,还提供了特有功能,如访问窗口的元数据,例如窗口中的开始时间和结束时间。 341 | 342 | - 在系统内部,窗口中的所有事件会存储在ListState中。通过对所有事件收集起来且提供对于窗口元数据的访问及其他一些特性的访问和使用,所以使用场景比增量聚合更广泛。但收集全部状态的窗口其状态要大得多。 343 | 344 | ### 增量聚合与ProcessWindowFunction 345 | 346 | - 增量集合函数计算逻辑,还需要访问窗口的元数据或状态。 347 | 348 | - 实现上述过程的途径是将ProcessWindowFunction作为reduce()或aggregate()方法的第二个参数 349 | 350 | - 前一个函数的输出即为后一个函数的输入即可。 351 | 352 | ```scala 353 | input 354 | .keyBy(...) 355 | .timeWindow(...) 356 | .reduce( 357 | incrAggregator: ReduceFunction[IN], 358 | function: ProcessWindowFunction[IN, OUT, K, W]) 359 | 360 | input 361 | .keyBy(...) 362 | .timeWindow(...) 363 | .aggregate( 364 | incrAggregator: AggregateFunction[IN, ACC, V], 365 | windowFunction: ProcessWindowFunction[V, OUT, K, W]) 366 | 367 | ``` 368 | 369 | - 示例: 计算每个传感器在每个窗口的温度的最大最小值。 370 | 371 | ```scala 372 | val minMaxTempPerWindow2: DataStream[MinMaxTemp] = sensorData 373 | .map(r => (r.id, r.temperature, r.temperature)) 374 | .keyBy(_._1) 375 | .timeWindow(Time.seconds(5)) 376 | .reduce( 377 | // incrementally compute min and max temperature 378 | (r1: (String, Double, Double), r2: (String, Double, Double)) => { 379 | (r1._1, r1._2.min(r2._2), r1._3.max(r2._3)) 380 | }, 381 | // finalize result in ProcessWindowFunction 382 | new AssignWindowEndProcessFunction() 383 | ) 384 | 385 | class AssignWindowEndProcessFunction 386 | extends ProcessWindowFunction[(String, Double, Double), MinMaxTemp, String, TimeWindow] { 387 | 388 | override def process( 389 | key: String, 390 | ctx: Context, 391 | minMaxIt: Iterable[(String, Double, Double)], 392 | out: Collector[MinMaxTemp]): Unit = { 393 | 394 | val minMax = minMaxIt.head 395 | val windowEnd = ctx.window.getEnd 396 | out.collect(MinMaxTemp(key, minMax._2, minMax._3, windowEnd)) 397 | } 398 | } 399 | 400 | ``` 401 | 402 | 403 | 404 | ## Window Assigner 405 | 406 | Flink 窗口的结构中有两个必须的两个操作: 407 | 408 | - 使用窗口分配器(WindowAssigner)将数据流中的元素分配到对应的窗口。 409 | 410 | - 当满足窗口触发条件后,对窗口内的数据使用窗口处理函数(Window Function)进行处理,常 411 | 412 | 用的 Window Function 有 reduce、aggregate、process。 413 | 414 | ![image-20201215114037836](Window.assets/image-20201215114037836.png) 415 | 416 | 对于KeyedStream,各种窗口分配器使用: 417 | 418 | - Tumbling time window 419 | 420 | ```scala 421 | keyedStream.timeWindow(Time.minutes(1)) 422 | ``` 423 | 424 | - Sliding time window 425 | 426 | ```scala 427 | keyedStream.timeWindow(Time.minutes(1), Time.seconds(10)) 428 | ``` 429 | 430 | - Tumbling count window 431 | 432 | ```scala 433 | keyedStream.countWindow(100) 434 | ``` 435 | 436 | - Sliding count window 437 | 438 | ```scala 439 | keyedStream.countWindow(100, 10) 440 | ``` 441 | 442 | - Session window 443 | 444 | ```scala 445 | keyedStream.window(EventTimeSessionWindows.withGap(Time. seconds(3)) 446 | ``` 447 | 448 | 449 | 450 | 对于DataStream,窗口分配器使用: 451 | 452 | ```scala 453 | stream.windowAll(…)… 454 | stream.timeWindowAll(Time.seconds(10))… 455 | stream.countWindowAll(20, 10)… 456 | ``` 457 | 458 | ## Window Trigger 459 | 460 | 触发器(Trigger)决定了何时启动 Window Function 来处理窗口中的数据以及何时将窗口内的数据清理。每个`WindowAssigner`都有一个默认`Trigger` 461 | 462 | | Flink 内置 Window Trigger | 触发频率 | 主要功能 | 463 | | ------------------------------- | -------- | ------------------------------------------------------------ | 464 | | ProcessingTimeTrigger | 一次触发 | 基于 ProcessingTime 触发,当机器时间大于窗口结束时间时触发 | 465 | | EventTimeTrigger | 一次触发 | 基于 EventTime,当 Watermark 大于窗口技术时间时触发 | 466 | | ContinuousProcessingTimeTrigger | 多次触发 | 基于 ProcessTime 的固定时间间隔触发 | 467 | | ContinuousEventTimeTrigger | 多次触发 | 基于 EventTime 的固定时间间隔触发 | 468 | | CountTrigger | 多次触发 | 基于 Element 的固定条数触发 | 469 | | DeltaTrigger | 多次触发 | 基于本次 Element和上次触发 Trigger 的 Element 做Delta 计算,超过指定 Threshold 后触发 | 470 | | PuringTrigger | | 对 Trigger 的封装实现,用于 Trigger 触发后额外清理中间状态数据 | 471 | 472 | - 整个Flink里面内置了非常多的Window Trigger,这里面包括基于 ProcessingTime 触发的ProcessingTimeTrigger以及基于 EventTime触发的EventTimeTrigger,对于这两种Window Trigger来说,基本上可以满足大部分的窗口的触发的逻辑。 473 | - 在上面的基础之上,又延伸出来两种,叫做ContinuousProcessingTimeTrigger和ContinuousEventTimeTrigger,他们的特点是多次触发,比如ContinuousEventTimeTrigger会基于Event time的**`固定时间间隔`**触发。 474 | - CountTrigger,是基于接入事件元素的固定条数,比如说每接入100条触发一次,那么Elements的固定条数,就是CountTrigger里面所需要依赖的条件。 475 | - DeltaTrigger,是基于我们本次数据元素和上次触发Trigger的数据元素之间做一个Delta的计算,Delta计算出来的结果会和一个指定的Threshold进行对比,如果超过了指定的Threshold指标,此时窗口触发计算。 476 | - PuringTrigger,是需要去基于前面提到的触发器来实现。 477 | 478 | 479 | 480 | ### TriggerResult 481 | 482 | 每次调用触发器都会生成一个TriggerResult,它用于决定窗口接下来的行为。 TriggerResult可以是以下值之一: 483 | 484 | - **`CONTINUE`** 什么都不做 485 | 486 | - **`FIRE`** 如果窗口算子配置了ProcessWindowFunction,就会调用该函数并发出结果;如果窗口只包含一个增量聚合函数,则直接发出当前聚合结果。窗口状态不会发出任何变化。 487 | 488 | - **`PURGE`** 完全清除窗口内容,并删除窗口自身及其元数据。同时调用ProcessWindowFunction.clear()来清理那些自定义的单个窗口状态。 489 | 490 | - **`FIRE_AND_PURGE`** 先进行窗口计算(FIRE),随后删除所有状态及元数据(PURGE)。 491 | 492 | 493 | 494 | ### Trigger接口 495 | 496 | ```scala 497 | public abstract class Trigger implements Serializable { 498 | 499 | private static final long serialVersionUID = -4104633972991191369L; 500 | 501 | // 每当有元素加入到窗口时都会调用 502 | public abstract TriggerResult onElement(T element, long timestamp, W window, TriggerContext ctx) throws Exception; 503 | 504 | // 在处理时间计时器触发时调用 505 | public abstract TriggerResult onProcessingTime(long time, W window, TriggerContext ctx) throws Exception; 506 | 507 | // 在事件时间计时器触发时调用 508 | public abstract TriggerResult onEventTime(long time, W window, TriggerContext ctx) throws Exception; 509 | 510 | // 如果计时器支持合并触发器状态则返回true 511 | public boolean canMerge() { 512 | return false; 513 | } 514 | 515 | // 当多个窗口合并为一个窗口 516 | // 且需要合并触发器状态时调用 517 | public void onMerge(W window, OnMergeContext ctx) throws Exception { 518 | throw new UnsupportedOperationException("This trigger does not support merging."); 519 | } 520 | 521 | // 在触发器中清除那些为给定窗口保存的状态 522 | // 该方法会在清除窗口时调用 523 | public abstract void clear(W window, TriggerContext ctx) throws Exception; 524 | 525 | // 用于触发器中方法的上下文对象,使其可以注册定时器回调并处理状态 526 | public interface TriggerContext { 527 | 528 | // 返回当前处理时间 529 | long getCurrentProcessingTime(); 530 | 531 | MetricGroup getMetricGroup(); 532 | 533 | //返回当前水位线时间 534 | long getCurrentWatermark(); 535 | 536 | //注册一个处理时间定时器 537 | void registerProcessingTimeTimer(long time); 538 | 539 | //注册一个事件事件定时器 540 | void registerEventTimeTimer(long time); 541 | 542 | //删除一个处理时间定时器 543 | void deleteProcessingTimeTimer(long time); 544 | 545 | //删除一个事件时间定时器 546 | void deleteEventTimeTimer(long time); 547 | 548 | //获取一个作用域为触发器键值和当前窗口的状态对象 549 | S getPartitionedState(StateDescriptor stateDescriptor); 550 | 551 | @Deprecated 552 | ValueState getKeyValueState(String name, Class stateType, S defaultState); 553 | 554 | @Deprecated 555 | ValueState getKeyValueState(String name, TypeInformation stateType, S defaultState); 556 | } 557 | 558 | //用于Trigger.onMerge()方法的TriggerContext扩展 559 | public interface OnMergeContext extends TriggerContext { 560 | > 561 | // 合并触发器中的单个窗口状态 562 | // 目标状态自身需要支持合并 563 | void mergePartitionedState(StateDescriptor stateDescriptor); 564 | } 565 | } 566 | 567 | ``` 568 | 569 | 570 | 571 | ### Window Trigger触发机制 572 | 573 | 先来看看在实际生产环境中最常用的Trigger:EventTimeTrigger是如何工作的 574 | 575 | #### EventTimeTrigger 576 | 577 | 以下案例中,左边是输入的数据,event time从12:00到12:10,它的指标有1、2、3、4、5,对应的Watermark的延迟限制是2分钟,这个时候会通过window assigner去分配对应的窗口,这里定义滚动窗口,并定义窗口大小是5分钟。 578 | 579 | - 当12:00,1这条数据进入第一个窗口时,窗口中有一个状态的维护,EventTimeTrigger会去控制窗口window function的计算以及结果的输出,包括Window Result的输出。 580 | 581 | - 一直到12:08,4的数据进入第二个窗口的时候,此时将watermark更新为12:06,大于第一个窗口的结束时间,触发窗口计算,发出结果。 582 | 583 | ![image-20201215172027772](Window.assets/image-20201215172027772.png) 584 | 585 | 代码实现,在这里AggregateFunction结合ProcessWindowFunction使用,增量聚合的同时发出窗口的元数据(窗口开始时间)。 586 | 587 | ```scala 588 | val list = List( 589 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:00"), 1), 590 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:04"), 2), 591 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:03"), 3), 592 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:08"), 4), 593 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:10"), 5) 594 | ) 595 | 596 | env.fromCollection(list) 597 | .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[UserEventCount](Time.minutes(2)) { 598 | override def extractTimestamp(t: UserEventCount): Long = t.timestamp 599 | }) 600 | .keyBy(_.id) 601 | .window(TumblingEventTimeWindows.of(Time.minutes(5))) 602 | .aggregate(new AggCountFunc, new WindowResult) 603 | .print() 604 | 605 | env.execute("job") 606 | } 607 | 608 | class AggCountFunc extends AggregateFunction[UserEventCount, (Int, Int), (Int, Int)] { 609 | override def createAccumulator(): (Int, Int) = (0, 0) 610 | 611 | override def add(in: UserEventCount, acc: (Int, Int)): (Int, Int) = (in.id, in.count + acc._2) 612 | 613 | override def getResult(acc: (Int, Int)): (Int, Int) = acc 614 | 615 | override def merge(acc: (Int, Int), acc1: (Int, Int)): (Int, Int) = (acc._1, acc._2 + acc1._2) 616 | } 617 | 618 | class WindowResult extends ProcessWindowFunction[(Int, Int), (String, Int, Int), Int, TimeWindow] { 619 | override def process(key: Int, context: Context, elements: Iterable[(Int, Int)], out: Collector[(String, Int, Int)]): Unit = { 620 | // 输出窗口开始时间 621 | val windowStart = DateUtil.formatTsToString(context.window.getStart) 622 | val userId = elements.head._1 623 | val count = elements.head._2 624 | out.collect((windowStart, userId, count)) 625 | } 626 | } 627 | 628 | 结果输出: 629 | (2020-12-15 12:00,1,6) 630 | (2020-12-15 12:05,1,4) 631 | (2020-12-15 12:10,1,5) 632 | ``` 633 | 634 | 635 | 636 | #### ContinuousEventTimeTrigger 637 | 638 | - 需求,每隔两分钟触发一次计算,发出结果: 639 | 640 | ![image-20201215190623569](Window.assets/image-20201215190623569.png) 641 | 642 | - 在上面的核心代码处,修改: 643 | 644 | ```scala 645 | DataStream(...) 646 | .keyBy(_.id) 647 | .window(TumblingEventTimeWindows.of(Time.minutes(5))) 648 | // 使用ContinuousEventTimeTrigger,基于EventTime每隔2分钟触发一次计算 649 | .trigger(ContinuousEventTimeTrigger.of(Time.minutes(2))) 650 | .aggregate(new AggCountFunc, new WindowResult) 651 | .print() 652 | ``` 653 | 654 | 655 | 656 | - 输出: 657 | 658 | ```scala 659 | (2020-12-15 12:00,1,6) 660 | (2020-12-15 12:00,1,6) 661 | (2020-12-15 12:00,1,6) 662 | (2020-12-15 12:05,1,4) 663 | (2020-12-15 12:10,1,5) 664 | (2020-12-15 12:10,1,5) 665 | (2020-12-15 12:10,1,5) 666 | ``` 667 | 668 | 可见,每两分钟触发一次计算,但每个窗口每次输出的结果是全量的窗口的结果。 669 | 670 | - 和上面的示例一样,唯一的不同是在 ContinuousEventTimeTrigger 外面包装了一个 PurgingTrigger,其作用是在 ContinuousEventTimeTrigger 触发窗口计算之后将窗口的 State 中的数据清除。具体作用为:如果被包装的trigger触发返回FIRE,则PurgingTrigger将返回修改为FIRE_AND_PURGE,其他的返回值不做处理。 671 | 672 | ```scala 673 | .trigger(PurgingTrigger.of(ContinuousEventTimeTrigger.of(Time.minutes(2)))) 674 | ``` 675 | 676 | 677 | 678 | #### DeltaTrigger 679 | 680 | DeltaTrigger具有一个DeltaFunction,该函数的逻辑需要用户自己定义。该函数比较上一次触发计算的元素,和目前到来的元素。比较结果为一个double类型阈值。如果阈值超过DeltaTrigger配置的阈值,会返回TriggerResult.FIRE 681 | 682 | 案例需求: 683 | 684 | - 需求:车辆区间测速 685 | - 描述:车辆每分钟上报位置与车速,没行进10公里,计算车间内最高车速。 686 | 687 | 以下是简单的代码实现: 688 | 689 | image-20201215235435915 690 | 691 | - 如何提取时间戳和生成水印,以及选择聚合维度就不赘述了。这个场景不是传统意义上的时间窗口或数量窗口,可以创建一个 **`GlobalWindow(默认情况下该窗口永远不会触发)`**,所有数据都在一个窗口中,我们通过定义一个 DeltaTrigger,并设定一个阈值,这里是10000(米)。每个元素和上次触发计算的元素比较是否达到设定的阈值,这里比较的是每个元素上报的位置,如果达到了10000(米),那么当前元素和上一个触发计算的元素之间的所有元素落在同一个窗口里计算,然后可以通过 Max 聚合计算出最大的车速。 692 | 693 | 694 | 695 | ### 总结 696 | 697 | 虽然Flink提供了以上各种内置实现的 Window Trigger,但其实大部分场景下都不会用到,一般来说默认的Trigger已经够用了;但如果需要自定义实现Trigger,只需要看一下Trigger在源码中的定义,自己实现就可以了。 698 | 699 | image-20201216000133111 700 | 701 | 702 | 703 | ## Window Evictor 704 | 705 | Evictor叫做数据清除器,是Flink窗口机制中的一个**`可选组件`**,可用于在窗口执行计算前后从窗口中删除元素。 706 | 707 | - 当数据接入到window之后,可以去调用相应的Evictor将不需要的数据删除,这个时候保证ProcessingFunction输入的数据的有效性。 708 | 709 | - 对应的,Evictor也可以用在ProcessingFunction之后,所以在ProcessingFunction之前和之后都可以使用Evictor进行相应的数据drop的操作。 710 | 711 | - Flink官方提供的一些Window Evictor 712 | 713 | | Evictor名称 | 功能描述 | 714 | | ------------ | ------------------------------------------------------------ | 715 | | CountEvictor | 保留一定数目的元素,多余的元素按照从前到后的顺序先后清理 | 716 | | TimeEvictor | 保留一个时间段的元素,早于这个时间段的元素会被清理 | 717 | | DeltaEvictor | 窗口计算时,最近一条Element和其他Element做Delta计算,仅保留Delta 结果在指定Threshold内Element | 718 | 719 | image-20210103103925904 720 | 721 | ```scala 722 | public interface Evictor extends Serializable { 723 | // 选择性移除元素,在窗口函数之前调用 724 | void evictBefore(Iterable> elements, 725 | int size, 726 | W window, 727 | Evictor.EvictorContext evictorContext); 728 | 729 | // 选择性移除元素,在窗口函数之后调用 730 | void evictAfter(Iterable> elements, 731 | int size, 732 | W window, 733 | Evictor.EvictorContext evictorContext); 734 | 735 | // 用于移除器内方法的上下文 736 | interface EvictorContext { 737 | // 返回当前处理时间 738 | long getCurrentProcessingTime(); 739 | 740 | MetricGroup getMetricGroup(); 741 | 742 | // 返回当前事件事件水位线 743 | long getCurrentWatermark(); 744 | } 745 | } 746 | 747 | ``` 748 | 749 | 750 | 751 | ## 总结 752 | 753 | -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/image-20201212200654705.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/image-20201212200654705.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/image-20201212200700868.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/image-20201212200700868.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/image-20201212200812245.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/image-20201212200812245.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/image-20201212201051293.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/image-20201212201051293.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/image-20201212201644857.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/image-20201212201644857.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/image-20201212201733956.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/image-20201212201733956.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/spaf_0206.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/spaf_0206.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/spaf_0207.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/spaf_0207.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/spaf_0208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/spaf_0208.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/spaf_0209.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/spaf_0209.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/spaf_0210.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/spaf_0210.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/spaf_0211.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/spaf_0211.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/spaf_0212.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/spaf_0212.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.assets/spaf_0213.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/flink/流初级基础.assets/spaf_0213.png -------------------------------------------------------------------------------- /docs/flink/流初级基础.md: -------------------------------------------------------------------------------- 1 | * [流处理基础](#流处理基础) 2 | * [数据流编程简介](#数据流编程简介) 3 | * [数据流图](#数据流图) 4 | * [数据并行和任务并行](#数据并行和任务并行) 5 | * [数据交换策略](#数据交换策略) 6 | * [并行处理流数据](#并行处理流数据) 7 | * [延迟和吞吐量](#延迟和吞吐量) 8 | * [延迟](#延迟) 9 | * [吞吐量](#吞吐量) 10 | * [延迟与吞吐量的对比](#延迟与吞吐量的对比) 11 | * [数据流上的操作](#数据流上的操作) 12 | * [数据摄入与数据吞吐量](#数据摄入与数据吞吐量) 13 | * [转换算子](#转换算子) 14 | * [滚动聚合](#滚动聚合) 15 | * [窗口计算](#窗口计算) 16 | * [时间语义](#时间语义) 17 | * [在流处理中一分钟代表什么](#在流处理中一分钟代表什么) 18 | * [处理时间](#处理时间) 19 | * [事件时间](#事件时间) 20 | * [水位线](#水位线) 21 | * [处理时间和事件时间](#处理时间和事件时间) 22 | * [状态和持久化模型](#状态和持久化模型) 23 | * [任务失败](#任务失败) 24 | * [什么是任务失败?](#什么是任务失败) 25 | * [结果的保证](#结果的保证) 26 | 27 | 28 | # 流处理基础 29 | 30 | 现存的系统Flink/Spark Structured Streaming的设计理念都是出自于谷歌大佬[Tyler Akidau](https://www.oreilly.com/people/tyler-akidau/)在O'really上发表的两篇博客,所以学习Flink,不妨读读Streaming System原文。以下文章主要总结于基于Apache Flink的流处理一书,后续会增加基于Streaming System的总结。 31 | 32 | 33 | 34 | ## 数据流编程简介 35 | 36 | 在我们深入研究流处理的基础知识之前,让我们来看看在数据流程编程的背景和使用的术语。 37 | 38 | ### 数据流图 39 | 40 | 顾名思义,数据流程序描述了数据如何在算子之间流动。数据流程序通常表示为有向图,其中节点称为算子,用来表示计算,边表示数据之间的依赖性。算子是数据流程序的基本功能单元。他们从输入消耗数据,对它们执行计算,并生成数据输出用于进一步处理。一个数据流图必须至少有一个数据源和一个数据接收器。 41 | 42 | image-20201212200700868 43 | 44 | 像图2-1中的数据流图被称为逻辑流图,因为它们表示了计算逻辑的高级视图。为了执行一个数据流程序,Flink会将逻辑流图转换为物理数据流图,详细说明程序的执行方式。例如,如果我们使用分布式处理引擎,每个算子在不同的物理机器可能有几个并行的任务运行。图2-2显示了图2-1逻辑图的物理数据流图。而在逻辑数据流图中节点表示算子,在物理数据流图中,节点是任务。“Extract hashtags”和“Count”算子有两个并行算子任务,每个算子任务对输入数据的子集执行计算。 45 | 46 | image-20201212200812245 47 | 48 | ### 数据并行和任务并行 49 | 50 | 我们可以以不同方式利用数据流图中的并行性。第一,我们可以对输入数据进行分区,并在数据的子集上并行执行具有相同算子的任务并行。这种类型的并行性被称为数据并行性。数据并行是有用的,因为它允许处理大量数据,并将计算分散到不同的计算节点上。第二,我们可以将不同的算子在相同或不同的数据上并行执行。这种并行性称为任务并行性。使用任务并行性,我们可以更好地利用计算资源。 51 | 52 | ### 数据交换策略 53 | 54 | 数据交换策略定义了在物理执行流图中如何将数据分配给任务。数据交换策略可以由执行引擎自动选择,具体取决于算子的语义或我们明确指定的语义。在这里,我们简要回顾一些常见的数据交换策略,如图2-3所示。 55 | 56 | image-20201212201051293 57 | 58 | - 前向策略将数据从一个任务发送到接收任务。如果两个任务都位于同一台物理计算机上(这通常由任务调度器确保),这种交换策略可以避免网络通信。 59 | - 广播策略将所有数据发送到算子的所有的并行任务上面去。因为这种策略会复制数据和涉及网络通信,所以代价相当昂贵。 60 | - 基于键控的策略通过Key值(键)对数据进行分区保证具有相同Key的数据将由同一任务处理。在图2-2中,输出“Extract hashtags”算子使用键来分区(hashtag),以便count算子的任务可以正确计算每个#标签的出现次数。 61 | - 随机策略统一将数据分配到算子的任务中去,以便均匀地将负载分配到不同的计算任务。 62 | 63 | 64 | 65 | ## 并行处理流数据 66 | 67 | 既然我们熟悉了数据流编程的基础知识,现在是时候看看这些概念如何应用于并行的处理数据流了。但首先,让我们定义术语数据流:数据流是一个可能无限的事件序列。 68 | 69 | 数据流中的事件可以表示监控数据,传感器测量数据,信用卡交易数据,气象站观测数据,在线用户交互数据,网络搜索数据等。在本节中,我们将学习如何并行处理无限流,使用数据流编程范式。 70 | 71 | 72 | 73 | ### 延迟和吞吐量 74 | 75 | 流处理程序不同与批处理程序。在评估性能时,要求也有所不同。对于批处理程序,我们通常关心一个作业的总的执行时间,或我们的处理引擎读取输入所需的时间,执行计算,并回写结果。由于流处理程序是连续运行的,输入可能是无界的,所以数据流处理中没有总执行时间的概念。 相反,流处理程序必须尽可能快的提供输入数据的计算结果。我们使用延迟和吞吐量来表征流处理的性能要求。 76 | 77 | 78 | 79 | ### 延迟 80 | 81 | 延迟表示处理事件所需的时间。它是接收事件和看到在输出中处理此事件的效果之间的时间间隔。要直观的理解延迟,考虑去咖啡店买咖啡。当你进入咖啡店时,可能还有其他顾客在里面。因此,你排队等候直到轮到你下订单。收银员收到你的付款并通知准备饮料的咖啡师。一旦你的咖啡准备好了,咖啡师会叫你的名字,你可以到柜台拿你的咖啡。服务延迟是从你进入咖啡店的那一刻起,直到你喝上第一口咖啡之间的时间间隔。 82 | 83 | 在数据流中,延迟是以时间为单位测量的,例如毫秒。根据应用程序,我们可能会关心平均延迟,最大延迟或百分位延迟。例如,平均延迟值为10ms意味着处理事件的平均时间在10毫秒内。或者,延迟值为95%,10ms表示95%的事件在10ms内处理完毕。平均值隐藏了处理延迟的真实分布,可能会让人难以发现问题。如果咖啡师在准备卡布奇诺之前用完了牛奶,你必须等到他们从供应室带来一些。虽然你可能会因为这么长时间的延迟而生气,但大多数其他客户仍然会感到高兴。 84 | 85 | 确保低延迟对于许多流应用程序来说至关重要,例如欺诈检测,系统警报,网络监控和提供具有严格服务水平协议的服务。低延迟是流处理的关键特性,它实现了我们所谓的实时应用程序。像Apache Flink这样的现代流处理器可以提供低至几毫秒的延迟。相比之下,传统批处理程序延迟通常从几分钟到几个小时不等。在批处理中,首先需要收集事件批次,然后才能处理它们。因此,延迟是受每个批次中最后一个事件的到达时间的限制。所以自然而然取决于批的大小。真正的流处理不会引入这样的人为延迟,因此可以实现真正的低延迟。真的流模型,事件一进入系统就可以得到处理。延迟更密切地反映了在每个事件上必须进行的实际工作。 86 | 87 | 88 | 89 | ### 吞吐量 90 | 91 | 吞吐量是衡量系统处理能力的指标,也就是处理速率。也就是说,吞吐量告诉我们每个时间单位系统可以处理多少事件。重温咖啡店的例子,如果商店营业时间为早上7点至晚上7点。当天为600个客户提供了服务,它的平均吞吐量将是每小时50个客户。虽然我们希望延迟尽可能低,但我们通常也需要吞吐量尽可能高。 92 | 93 | 吞吐量以每个时间单位系统所能处理的事件数量或操作数量来衡量。值得注意的是,事件处理速率取决于事件到达的速率,低吞吐量并不一定表示性能不佳。 在流式系统中,我们通常希望确保我们的系统可以处理最大的预期事件到达的速率。也就是说,我们主要的关注点在于确定的峰值吞吐量是多少,当系统处于最大负载时性能怎么样。为了更好地理解峰值吞吐量的概念,让我们考虑一个流处理 程序没有收到任何输入的数据,因此没有消耗任何系统资源。当第一个事件进来时,它会尽可能以最小延迟立即处理。例如,如果你是第一个出现在咖啡店的顾客,在早上开门后,你将立即获得服务。理想情况下,您希望此延迟保持不变 ,并且独立于传入事件的速率。但是,一旦我们达到使系统资源被完全使用的事件传入速率,我们将不得不开始缓冲事件。在咖啡店里 ,午餐后会看到这种情况发生。许多人出现在同一时间,必须排队等候。在此刻,咖啡店系统已达到其峰值吞吐量,进一步增加 事件传入的速率只会导致更糟糕的延迟。如果系统继续以可以处理的速率接收数据,缓冲区可能变为不可用,数据可能会丢失。这种情况是众所周知的 作为背压,有不同的策略来处理它。 94 | 95 | ### 延迟与吞吐量的对比 96 | 97 | 此时,应该清楚延迟和吞吐量不是独立指标。如果事件需要在处理流水线中待上很长时间,我们不能轻易确保高吞吐量。同样,如果系统容量很小,事件将被缓冲,而且必须等待才能得到处理。 98 | 99 | 让我们重温一下咖啡店的例子来阐明一下延迟和吞吐量如何相互影响。首先,应该清楚存在没有负载时的最佳延迟。也就是说,如果你是咖啡店的唯一客户,会很快得到咖啡。然而,在繁忙时期,客户将不得不排队等待,并且会有延迟增加。另一个影响延迟和吞吐量的因素是处理事件所花费的时间或为每个客户提供服务所花费的时间。想象一下,期间圣诞节假期,咖啡师不得不为每杯咖啡画圣诞老人。这意味着准备一杯咖啡需要的时间会增加,导致每个人花费 更多的时间在等待咖啡师画圣诞老人,从而降低整体吞吐量。 100 | 101 | 那么,你可以同时获得低延迟和高吞吐量吗?或者这是一个无望的努力?我们可以降低得到咖啡的延迟 ,方法是:聘请一位更熟练的咖啡师来准备咖啡。在高负载时,这种变化也会增加吞吐量,因为会在相同的时间内为更多的客户提供服务。 实现相同结果的另一种方法是雇用第二个咖啡师来利用并行性。这里的主要想法是降低延迟来增加吞吐量。当然,如果系统可以更快的执行操作,它可以在相同的时间内执行更多操作。 事实上,在流中利用并行性时也会发生这种情况。通过并行处理多个流,在同时处理更多事件的同时降低延迟。 102 | 103 | 104 | 105 | ## 数据流上的操作 106 | 107 | 流处理引擎通常提供一组内置操作:摄取(ingest),转换(transform)和输出流(output)。这些操作可以 结合到数据流图中来实现逻辑流处理程序。在本节中,我们描述最常见的流处理操作。 108 | 109 | 操作可以是无状态的或有状态的。无状态操作不保持任何内部状态。也就是说,事件的处理不依赖于过去看到的任何事件,也没有保留历史。 无状态操作很容易并行化,因为事件可以彼此独立地处理,也独立于事件到达的顺序(和事件到达顺序没有关系)。 而且,在失败的情况下,无状态操作可以是简单的重新启动并从中断处继续处理。相反, 有状态操作可能会维护之前收到的事件的信息。此状态可以通过传入事件更新,也可以用于未来事件的处理逻辑。有状态的流 处理应用程序更难以并行化和以容错的方式来运行,因为状态需要有效的进行分区和在发生故障的情况下可靠地恢复。 110 | 111 | 112 | 113 | ### 数据摄入与数据吞吐量 114 | 115 | 数据摄取和数据出口操作允许流处理程序与外部系统通信。数据摄取是操作从外部源获取原始数据并将其转换为其他格式(ETL)。实现数据提取逻辑的运算符被称为数据源。数据源可以从TCP Socket,文件,Kafka Topic或传感器数据接口中提取数据。数据出口是以适合消费的形式产出到外部系统。执行数据出口的运算符称为数据接收器,包括文件,数据库,消息队列和监控接口。 116 | 117 | 118 | 119 | ### 转换算子 120 | 121 | image-20201212201644857 122 | 123 | 转换算子是单遍处理算子,碰到一个事件处理一个事件。这些操作在使用后会消费一个事件,然后对事件数据做一些转换,产生一个新的输出流。转换逻辑可以集成在 操作符中或由UDF函数提供,如图所示图2-4。程序员编写实现自定义计算逻辑。 124 | 125 | 操作符可以接受多个输入流并产生多个输出流。他们还可以通过修改数据流图的结构要么将流分成多个流,要么将流合并为一条流。 126 | 127 | 128 | 129 | ### 滚动聚合 130 | 131 | 滚动聚合是一种聚合,例如sum,minimum和maximum,为每个输入事件不断更新。 聚合操作是有状态的,并将当前状态与传入事件一起计算以产生更新的聚合值。请注意能够有效地将当前状态与事件相结合 产生单个值,聚合函数必须是关联的和可交换的。否则,操作符必须存储完整的流数据历史。图2-5显示了最小滚动 聚合。操作符保持当前的最小值和相应地为每个传入的事件来更新最小值。 132 | 133 | image-20201212201733956 134 | 135 | ### 窗口计算 136 | 137 | 转换和滚动聚合一次处理一个事件产生输出事件并可能更新状态。但是,有些操作必须收集并缓冲数据以计算其结果。 例如,考虑不同流之间的连接或整体聚合这样的操作,例如中值函数。为了在无界流上高效运行这些操作符,我们需要限制 这些操作维护的数据量。在本节中,我们将讨论窗口操作,提供此服务。 138 | 139 | 窗口还可以在语义上实现关于流的比较复杂的查询。我们已经看到了滚动聚合的方式,以聚合值编码整个流的历史数据来为每个事件提供低延迟的结果。 但如果我们只对最近的数据感兴趣的话会怎样?考虑给司机提供实时交通信息的应用程序。这个程序可以使他们避免拥挤的路线。在这种场景下,你想知道某个位置在最近几分钟内是否有事故发生。 另一方面,了解所有发生过的事故在这个应用场景下并没有什么卵用。更重要的是,通过将流历史缩减为单一聚合值,我们将丢失这段时间内数据的变化。例如,我们可能想知道每5分钟有多少车辆穿过 某个路口。 140 | 141 | 窗口操作不断从无限事件流中创建有限的事件集,好让我们执行有限集的计算。通常会基于数据属性或基于时间的窗口来分配事件。 要正确定义窗口运算符语义,我们需要确定如何给窗口分配事件以及对窗口中的元素进行求值的频率是什么样的。 窗口的行为由一组策略定义。窗口策略决定何时创建新的窗口以及要分配的事件属于哪个窗口,以及何时对窗口中的元素进行求值。 而窗口的求值基于触发条件。一旦触发条件得到满足,窗口的内容将会被发送到求值函数,求值函数会将计算逻辑应用于窗口中的元素。 求值函数可以是sum或minimal或自定义的聚合函数。 求值策略可以根据时间或者数据属性计算(例如,在过去五秒内收到的事件或者最近的一百个事件等等)。 接下来,我们描述常见窗口类型的语义。 142 | 143 | - 滚动窗口是将事件分配到固定大小的不重叠的窗口中。当通过窗口的结尾时,全部事件被发送到求值函数进行处理。基于计数的滚动窗口定义了在触发求值之前需要收集多少事件。图2-6显示了一个基于计数的翻滚窗口,每四个元素一个窗口。基于时间的滚动窗口定义一个时间间隔,包含在此时间间隔内的事件。图2-7显示了基于时间的滚动窗口,将事件收集到窗口中每10分钟触发一次计算。 144 | 145 | img 146 | 147 | img 148 | 149 | - 滑动窗口将事件分配到固定大小的重叠的窗口中去。因此,事件可能属于多个桶。我们通过提供窗口的长度和滑动距离来定义滑动窗口。滑动距离定义了创建新窗口的间隔。基于滑动计数的窗口,图2-8的长度为四个事件,三个为滑动距离。 150 | 151 | img 152 | 153 | - 会话窗口在常见的真实场景中很有用,一些场景既不能使用滚动窗口也不能使用滑动窗口。考虑一个分析在线用户行为的应用程序。在应用程序里,我们想把源自同一时期的用户活动或会话事件分组在一起。会话由一系列相邻时间发生的事件组成,接下来有一段时间没有活动。例如,用户在App上浏览一系列的新闻,然后关掉App,那么浏览新闻这段时间的浏览事件就是一个会话。会话窗口事先没有定义窗口的长度,而是取决于数据的实际情况,滚动窗口和滑动窗口无法应用于这个场景。相反,我们需要将同一会话中的事件分配到同一个窗口中去,而不同的会话可能窗口长度不一样。会话窗口会定义一个间隙值来区分不同的会话。间隙值的意思是:用户一段时间内不活动,就认为用户的会话结束了。图2-9显示了一个会话窗口。 154 | 155 | img 156 | 157 | 到目前为止,所有窗口类型都是在整条流上去做窗口操作。但实际上你可能想要将一条流分流成多个逻辑流并定义并行窗口。 例如,如果我们正在接收来自不同传感器的测量结果,那么可能想要在做窗口计算之前按传感器ID对流进行分流操作。 在并行窗口中,每条流都独立于其他流,然后应用了窗口逻辑。图2-10显示了一个基于计数的长度为2的并行滚动窗口,根据事件颜色分流。 158 | 159 | img 160 | 161 | 在流处理中,窗口操作与两个主要概念密切相关:时间语义和状态管理。时间也许是流处理最重要的方面。即使低延迟是流处理的一个有吸引力的特性,它的真正价值不仅仅是快速分析。真实世界的系统,网络和通信渠道远非完美,流数据经常被推迟或无序(乱序)到达。理解如何在这种条件下提供准确和确定的结果是至关重要的。 更重要的是,流处理程序可以按原样处理事件制作的也应该能够处理相同的历史事件方式,从而实现离线分析甚至时间旅行分析。 当然,前提是我们的系统可以保存状态,因为可能有故障发生。到目前为止,我们看到的所有窗口类型在产生结果前都需要保存之前的数据。实际上,如果我们想计算任何指标,即使是简单的计数,我们也需要保存状态。考虑到流处理程序可能会运行几天,几个月甚至几年,我们需要确保状态可以在发生故障的情况下可靠地恢复。 并且即使程序崩溃,我们的系统也能保证计算出准确的结果。本章,我们将在流处理应用可能发生故障的语境下,深入探讨时间和状态的概念。 162 | 163 | 164 | 165 | ## 时间语义 166 | 167 | 在本节中,我们将介绍时间语义,并描述流中不同的时间概念。我们将讨论流处理器在乱序事件流的情况下如何提供准确的计算结果,以及我们如何处理历史事件流,如何在流中进行时间旅行。 168 | 169 | 170 | 171 | ### 在流处理中一分钟代表什么 172 | 173 | 在处理可能是无限的事件流(包含了连续到达的事件),时间成为流处理程序的核心方面。假设我们想要连续的计算结果,可能每分钟就要计算一次。在我们的流处理程序上下文中,一分钟的意思是什么? 174 | 175 | 考虑一个程序需要分析一款移动端的在线游戏的用户所产生的事件流。游戏中的用户分了组,而应用程序将收集每个小组的活动数据,基于小组中的成员多快达到了游戏设定的目标,然后在游戏中提供奖励。例如额外的生命和用户升级。例如,如果一个小组中的所有用户在一分钟之内都弹出了500个泡泡,他们将升一级。Alice是一个勤奋的玩家,她在每天早晨的通勤时间玩游戏。问题在于Alice住在柏林,并且乘地铁去上班。而柏林的地铁手机信号很差。我们设想一个这样的场景,Alice当她的手机连上网时,开始弹泡泡,然后游戏会将数据发送到我们编写的应用程序中,这时地铁突然进入了隧道,她的手机也断网了。Alice还在玩这个游戏,而产生的事件将会缓存在手机中。当地铁离开隧道,Alice的手机又在线了,而手机中缓存的游戏事件将发送到应用程序。我们的应用程序应该如何处理这些数据?在这个场景中一分钟的意思是什么?这个一分钟应该包含Alice离线的那段时间吗?下图展示了这个问题。 176 | 177 | img 178 | 179 | 在线手游是一个简单的场景,展示了应用程序的运算应该取决于事件实际发生的时间,而不是应用程序收到事件的时间。如果我们按照应用程序收到事件的时间来进行处理的话,最糟糕的后果就是,Alice和她的朋友们再也不玩这个游戏了。但是还有很多时间语义非常关键的应用程序,我们需要保证时间语义的正确性。如果我们只考虑我们在一分钟之内收到了多少数据,我们的结果会变化,因为结果取决于网络连接的速度或处理的速度。相反,定义一分钟之内的事件数量,这个一分钟应该是数据本身的时间。 180 | 181 | 在Alice的这个例子中,流处理程序可能会碰到两个不同的时间概念:处理时间和事件时间。我们将在接下来的部分,讨论这两个概念。 182 | 183 | 184 | 185 | ### 处理时间 186 | 187 | 处理时间是处理流的应用程序的机器的本地时钟的时间(墙上时钟)。处理时间的窗口包含了一个时间段内来到机器的所有事件。这个时间段指的是机器的墙上时钟。如下图所示,在Alice的这个例子中,处理时间窗口在Alice的手机离线的情况下,时间将会继续行走。但这个处理时间窗口将不会收集Alice的手机离线时产生的事件。 188 | 189 | img 190 | 191 | ### 事件时间 192 | 193 | 事件时间是流中的事件实际发生的时间。事件时间基于流中的事件所包含的时间戳。通常情况下,在事件进入流处理程序前,事件数据就已经包含了时间戳。下图展示了事件时间窗口将会正确的将事件分发到窗口中去。可以如实反应事情是怎么发生的。即使事件可能存在延迟。 194 | 195 | img 196 | 197 | 事件时间使得计算结果的过程不需要依赖处理数据的速度。基于事件时间的操作是可以预测的,而计算结果也是确定的。无论流处理程序处理流数据的速度快或是慢,无论事件到达流处理程序的速度快或是慢,事件时间窗口的计算结果都是一样的。 198 | 199 | 可以处理迟到的事件只是我们使用事件时间所克服的一个挑战而已。普遍存在的事件乱序问题可以使用事件时间得到解决。考虑和Alice玩同样游戏的Bob,他恰好和Alice在同一趟地铁上。Alice和Bob虽然玩的游戏一样,但他们的手机信号是不同的运营商提供的。当Alice的手机没信号时,Bob的手机依然有信号,游戏数据可以正常发送出去。 200 | 201 | 如果使用事件时间,即使碰到了事件乱序到达的情况,我们也可以保证结果的正确性。还有,当我们在处理可以重播的流数据时,由于时间戳的确定性,我们可以快进过去。也就是说,我们可以重播一条流,然后分析历史数据,就好像流中的事件是实时发生一样。另外,我们可以快进历史数据来使我们的应用程序追上现在的事件,然后应用程序仍然是一个实时处理程序,而且业务逻辑不需要改变。 202 | 203 | ### 水位线 204 | 205 | 在我们对事件时间窗口的讨论中,我们忽略了一个很重要的方面:我们应该怎样去决定何时触发事件时间窗口的计算?也就是说,在我们可以确定一个时间点之前的所有事件都已经到达之前,我们需要等待多久?我们如何知道事件是迟到的?在分布式系统无法准确预测行为的现实条件下,以及外部组件所引发的事件的延迟,以上问题并没有准确的答案。在本小节中,我们将会看到如何使用水位线来设置事件时间窗口的行为。 206 | 207 | 水位线是全局进度的度量标准。系统可以确信在一个时间点之后,不会有早于这个时间点发生的事件到来了。本质上,水位线提供了一个逻辑时钟,这个逻辑时钟告诉系统当前的事件时间。当一个运算符接收到含有时间T的水位线时,这个运算符会认为早于时间T的发生的事件已经全部都到达了。对于事件时间窗口和乱序事件的处理,水位线非常重要。运算符一旦接收到水位线,运算符会认为一段时间内发生的所有事件都已经观察到,可以触发针对这段时间内所有事件的计算了。 208 | 209 | 水位线提供了一种结果可信度和延时之间的妥协。激进的水位线设置可以保证低延迟,但结果的准确性不够。在这种情况下,迟到的事件有可能晚于水位线到达,我们需要编写一些代码来处理迟到事件。另一方面,如果水位线设置的过于宽松,计算的结果准确性会很高,但可能会增加流处理程序不必要的延时。 210 | 211 | 在很多真实世界的场景里面,系统无法获得足够的知识来完美的确定水位线。在手游这个场景中,我们无法得知一个用户离线时间会有多长,他们可能正在穿越一条隧道,可能正在乘飞机,可能永远不会再玩儿了。水位线无论是用户自定义的或者是自动生成的,在一个分布式系统中追踪全局的时间进度都不是很容易。所以仅仅依靠水位线可能并不是一个很好的主意。流处理系统还需要提供一些机制来处理迟到的元素(在水位线之后到达的事件)。根据应用场景,我们可能需要把迟到事件丢弃掉,或者写到日志里,或者使用迟到事件来更新之前已经计算好的结果。 212 | 213 | ### 处理时间和事件时间 214 | 215 | 大家可能会有疑问,既然事件时间已经可以解决我们的所有问题,为什么我们还要对比这两个时间概念?真相是,处理时间在很多情况下依然很有用。处理时间窗口将会带来理论上最低的延迟。因为我们不需要考虑迟到事件以及乱序事件,所以一个窗口只需要简单的缓存窗口内的数据即可,一旦机器时间超过指定的处理时间窗口的结束时间,就会触发窗口的计算。所以对于一些处理速度比结果准确性更重要的流处理程序,处理时间就派上用场了。另一个应用场景是,当我们需要在真实的时间场景下,周期性的报告结果时,同时不考虑结果的准确性。一个例子就是一个实时监控的仪表盘,负责显示当事件到达时立即聚合的结果。最后,处理时间窗口可以提供流本身数据的忠实表达,对于一些案例可能是很必要的特性。例如我们可能对观察流和对每分钟事件的计数(检测可能存在的停电状况)很感兴趣。简单的说,处理时间提供了低延迟,同时结果也取决于处理速度,并且也不能保证确定性。另一方面,事件时间保证了结果的确定性,同时还可以使我们能够处理迟到的或者乱序的事件流。 216 | 217 | 218 | 219 | ## 状态和持久化模型 220 | 221 | 我们现在转向另一个对于流处理程序非常重要的话题:状态。在数据处理中,状态是普遍存在的。任何稍微复杂一点的计算,都涉及到状态。为了产生计算结果,一个函数在一段时间内的一定数量的事件上来累加状态(例如,聚合计算或者模式匹配)。有状态的运算符使用输入的事件以及内部保存的状态来计算得到输出。例如,一个滚动聚合运算符需要输出这个运算符所观察到的所有事件的累加和。这个运算符将会在内部保存当前观察到的所有事件的累加和,同时每输入一个事件就更新一次累加和的计算结果。相似的,当一个运算符检测到一个“高温”事件紧接着十分钟以内检测到一个“烟雾”事件时,将会报警。直到运算符观察到一个“烟雾”事件或者十分钟的时间段已经过去,这个运算符需要在内部状态中一直保存着“高温”事件。 222 | 223 | 当我们考虑一下使用批处理系统来分析一个无界数据集时,会发现状态的重要性显而易见。在现代流处理器兴起之前,处理无界数据集的一个通常做法是将输入的事件攒成微批,然后交由批处理器来处理。当一个任务结束时,计算结果将被持久化,而所有的运算符状态就丢失了。一旦一个任务在计算下一个微批次的数据时,这个任务是无法访问上一个任务的状态的(都丢掉了)。这个问题通常使用将状态代理到外部系统(例如数据库)的方法来解决。相反,在一个连续不间断运行的流处理任务中,事件的状态是一直存在的,我们可以将状态暴露出来作为编程模型中的一等公民。当然,我们的确可以使用外部系统来管理流的状态,即使这个解决方案会带来额外的延迟。 224 | 225 | 由于流处理运算符默认处理的是无界数据流。所以我们必须要注意不要让内部状态无限的增长。为了限制状态的大小,运算符通常情况下会保存一些之前所观察到的事件流的总结或者概要。这个总结可能是一个计数值,一个累加和,或者事件流的采样,窗口的缓存操作,或者是一个自定义的数据结构,这个数据结构用来保存数据流中感兴趣的一些特性。 226 | 227 | 我们可以想象的到,支持有状态的运算符可能会碰到一些实现上的挑战: 228 | 229 | *状态管理* 230 | 231 | 系统需要高效的管理状态,并保证针对状态的并发更新,不会产生竞争条件(race condition)。 232 | 233 | *状态分区* 234 | 235 | 并行会带来复杂性。因为计算结果同时取决于已经保存的状态和输入的事件流。幸运的是,大多数情况下,我们可以使用Key来对状态进行分区,然后独立的管理每一个分区。例如,当我们处理一组传感器的测量事件流时,我们可以使用分区的运算符状态来针对不同的传感器独立的保存状态。 236 | 237 | *状态恢复* 238 | 239 | 第三个挑战是有状态的运算符如何保证状态可以恢复,即使出现任务失败的情况,计算也是正确的。 240 | 241 | 下一节,我们将讨论任务失败和计算结果的保证。 242 | 243 | 244 | 245 | ### 任务失败 246 | 247 | 流任务中的运算符状态是很宝贵的,也需要抵御任务失败带来的问题。如果在任务失败的情况下,状态丢失的话,在任务恢复以后计算的结果将是不正确的。流任务会连续不断的运行很长时间,而状态可能已经收集了几天甚至几个月。在失败的情况下,重新处理所有的输入并重新生成一个丢失的状态,将会很浪费时间,开销也很大。 248 | 249 | 在本章开始时,我们看到如何将流的编程建模成数据流模型。在执行之前,流程序将会被翻译成物理层数据流图,物理层数据流图由连接的并行任务组成,而一个并行任务运行一些运算符逻辑,消费输入流数据,并为其他任务产生输出流数据。真实场景下,可能有数百个这样的任务并行运行在很多的物理机器上。在长时间的运行中,流任务中的任意一个任务在任意时间点都有可能失败。我们如何保证任务的失败能被正确的处理,以使任务能继续的运行下去呢?事实上,我们可能希望我们的流处理器不仅能在任务失败的情况下继续处理数据,还能保证计算结果的正确性以及运算符状态的安全。我们在本小节来讨论这些问题。 250 | 251 | #### 什么是任务失败? 252 | 253 | 对于流中的每一个事件,一个处理任务分为以下步骤:(1)接收事件,并将事件存储在本地的缓存中;(2)可能会更新内部状态;(3)产生输出记录。这些步骤都能失败,而系统必须对于在失败的场景下如何处理有清晰的定义。如果任务在第一步就失败了,事件会丢失吗?如果当更新内部状态的时候任务失败,那么内部状态会在任务恢复以后更新吗?在以上这些场景中,输出是确定性的吗? 254 | 255 | 在批处理场景下,所有的问题都不是问题。因为我们可以很方便的重新计算。所以不会有事件丢失,状态也可以得到完全恢复。在流的世界里,处理失败不是一个小问题。流系统在失败的情况下需要保证结果的准确性。接下来,我们需要看一下现代流处理系统所提供的一些保障,以及实现这些保障的机制。 256 | 257 | #### 结果的保证 258 | 259 | 当我们讨论保证计算的结果时,我们的意思是流处理器的内部状态需要保证一致性。也就是说我们关心的是应用程序的代码在故障恢复以后看到的状态值是什么。要注意保证应用程序状态的一致性并不是保证应用程序的输出结果的一致性。一旦输出结果被持久化,结果的准确性就很难保证了。除非持久化系统支持事务。 260 | 261 | - AT-MOST-ONCE 262 | 263 | 当任务故障时,最简单的做法是什么都不干,既不恢复丢失的状态,也不重播丢失的事件。At-most-once语义的含义是最多处理一次事件。换句话说,事件可以被丢弃掉,也没有任何操作来保证结果的准确性。这种类型的保证也叫“没有保证”,因为一个丢弃掉所有事件的系统其实也提供了这样的保障。没有保障听起来是一个糟糕的主意,但如果我们能接受近似的结果,并且希望尽可能低的延迟,那么这样也挺好。 264 | 265 | - AT-LEAST-ONCE 266 | 267 | 在大多数的真实应用场景,我们希望不丢失事件。这种类型的保障成为at-least-once,意思是所有的事件都得到了处理,而且一些事件还可能被处理多次。如果结果的正确性仅仅依赖于数据的完整性,那么重复处理是可以接受的。例如,判断一个事件是否在流中出现过,at-least-once这样的保证完全可以正确的实现。在最坏的情况下,我们多次遇到了这个事件。而如果我们要对一个特定的事件进行计数,计算结果就可能是错误的了。 268 | 269 | 为了保证在at-least-once语义的保证下,计算结果也能正确。我们还需要另一套系统来从数据源或者缓存中重新播放数据。持久化的事件日志系统将会把所有的事件写入到持久化存储中。所以如果任务发生故障,这些数据可以重新播放。还有一种方法可以获得同等的效果,就是使用结果承认机制。这种方法将会把每一条数据都保存在缓存中,直到数据的处理等到所有的任务的承认。一旦得到所有任务的承认,数据将被丢弃。 270 | 271 | - *EXACTLY-ONCE* 272 | 273 | 恰好处理一次是最严格的保证,也是最难实现的。恰好处理一次语义不仅仅意味着没有事件丢失,还意味着针对每一个数据,内部状态仅仅更新一次。本质上,恰好处理一次语义意味着我们的应用程序可以提供准确的结果,就好像从未发生过故障。 274 | 275 | 提供恰好处理一次语义的保证必须有至少处理一次语义的保证才行,同时还需要数据重放机制。另外,流处理器还需要保证内部状态的一致性。也就是说,在故障恢复以后,流处理器应该知道一个事件有没有在状态中更新。事务更新是达到这个目标的一种方法,但可能引入很大的性能问题。Flink使用了一种轻量级快照机制来保证恰好处理一次语义。 276 | 277 | - *端到端恰好处理一次* 278 | 279 | 目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在Flink流处理器内部保证的。而在真实世界中,流处理应用除了流处理器以外还包含了数据源(例如Kafka)和持久化系统。端到端的一致性保证意味着结果的正确性贯穿了整个流处理应用的始终。每一个组件都保证了它自己的一致性。而整个端到端的一致性级别取决于所有组件中一致性最弱的组件。要注意的是,我们可以通过弱一致性来实现更强的一致性语义。例如,当任务的操作具有幂等性时,比如流的最大值或者最小值的计算。在这种场景下,我们可以通过最少处理一次这样的一致性来实现恰好处理一次这样的最高级别的一致性。 -------------------------------------------------------------------------------- /docs/scala/函数式编程.md: -------------------------------------------------------------------------------- 1 | \* [1 函数式编程简介](#1-函数式编程简介) 2 | 3 | \* [面向对象编程](#面向对象编程) 4 | 5 | \* [函数式编程](#函数式编程) 6 | 7 | \* [2 函数类型](#2-函数类型) 8 | 9 | \* [匿名函数](#匿名函数) 10 | 11 | \* [函数值](#函数值) 12 | 13 | \* [有名函数](#有名函数) 14 | 15 | \* [函数调用](#函数调用) 16 | 17 | \* [3 柯里化和闭包](#3-柯里化和闭包) 18 | 19 | \* [函数柯里化(多参数列表)](#函数柯里化多参数列表) 20 | 21 | \* [隐式(IMPLICIT)参数](#隐式implicit参数) 22 | 23 | \* [闭包](#闭包) 24 | 25 | \* [4 高阶函数](#4-高阶函数) 26 | 27 | \* [函数可以作为值进行传递](#函数可以作为值进行传递) 28 | 29 | \* [函数可以作为参数进行传递](#函数可以作为参数进行传递) 30 | 31 | \* [函数可以作为函数返回值返回](#函数可以作为函数返回值返回) 32 | 33 | \* [5 模式匹配](#5-模式匹配) 34 | 35 | \* [语法](#语法) 36 | 37 | \* [案例类(case classes)的匹配](#案例类case-classes的匹配) 38 | 39 | \* [模式守卫(Pattern gaurds)](#模式守卫pattern-gaurds) 40 | 41 | \* [仅匹配类型](#仅匹配类型) 42 | 43 | \* [密封类](#密封类) 44 | 45 | \* [备注](#备注) 46 | 47 | \* [6 偏函数](#6-偏函数) 48 | 49 | \* [偏函数定义](#偏函数定义) 50 | 51 | \* [偏函数原理](#偏函数原理) 52 | 53 | \* [偏函数使用](#偏函数使用) 54 | 55 | \* [案例需求](#案例需求) 56 | 57 | \* [7 嵌套函数](#7-嵌套函数) 58 | 59 | # 1 函数式编程简介 60 | 61 | ## 面向对象编程 62 | 63 | **解决问题,分解对象,行为,属性,然后通过对象的关系以及行为的调用来解决问题。** 64 | 65 | - 对象:用户 66 | 67 | - 行为:登录、连接JDBC、读取数据库 68 | 69 | - 属性:用户名、密码 70 | 71 | > Scala语言是一个完全面向对象编程语言。万物皆对象 72 | > 73 | > 对象的本质:对数据和行为的一个封装 74 | 75 | 76 | 77 | ## 函数式编程 78 | 79 | **解决问题时,将问题分解成一个一个的步骤,将每个步骤进行封装(函数),通过调用这些封装好的步骤,解决问题。** 80 | 81 | - 例如:请求->用户名、密码->连接JDBC->读取数据库 82 | 83 | > Scala语言是一个完全函数式编程语言。万物皆函数。 84 | > 85 | > 函数的本质:函数可以当做一个值进行传递 86 | 87 | 88 | 89 | # 2 函数类型 90 | 91 | - 在`Scala`中,函数可以使用`函数字面值(Function Literal)`直接定义。有时候,函数字面值也常常称为`函数`或`函数值`。 92 | 93 | - `在Scala中函数是一等公民`,当我们定义一个函数字面量时,实际上定义了一个包含apply方法的Scala对象。Scala对这个方法名有特别的规则,一个有apply方法的对象可以把它当成方法一样调用 94 | 95 | ```scala 96 | val f1 = (a: String) => a.toLowerCase 97 | ``` 98 | 99 | 100 | 101 | ## 匿名函数 102 | 103 | 事实上,「函数字面值」本质上是一个「匿名函数(Anonymous Function)」。在`Scala`里,函数被转换为`FunctionN`的实例。上例等价于: 104 | 105 | ```scala 106 | val f2 = new Function[String, String] { 107 | override def apply(v1: String): String = v1.toLowerCase 108 | } 109 | ``` 110 | 111 | 其中,`Function1[String, String]`可以简写为`String => String`,因此它又等价于: 112 | 113 | ```scala 114 | val f3 = new (String => String) { 115 | def apply(s: String): String = s.toLowerCase 116 | } 117 | ``` 118 | 119 | 也就是说,「函数字面值」可以看做「匿名函数对象」的一个「语法糖」。 120 | 121 | 122 | 123 | ## 函数值 124 | 125 | 综上述,函数实际上是`FunctionN[A1, A2, ..., An, R]`类型的一个实例而已。例如`(s: String) => s.toLowerCase`是`Function1[String, String]`类型的一个实例。 126 | 127 | 在函数式编程中,`函数做为一等公民,函数值可以被自由地传递和存储。`例如,它可以赋予一个变量`lower`。 128 | 129 | ```scala 130 | val lower: String => String = _.toLowerCase 131 | ``` 132 | 133 | 假如存在一个`map`的柯里化的函数。 134 | 135 | ```scala 136 | def map[A, B](a: A)(f: A => B): B = f(a) 137 | ``` 138 | 139 | 函数值可以作为参数传递给`map`函数。 140 | 141 | ```scala 142 | map("HORANCE") { _.toLowerCase } 143 | ``` 144 | 145 | ## 有名函数 146 | 147 | 相对于「匿名函数」,如果将「函数值」赋予`def`,或者`val`,此时函数常称为「有名函数」(Named Function)。 148 | 149 | 150 | 151 | ```scala 152 | val lower: String => String = _.toLowerCase 153 | def lower: String => String = _.toLowerCase 154 | ``` 155 | 156 | 两者之间存在微妙的差异。前者使用`val`定义的变量直接持有函数值,多次使用`lower`将返回同一个函数值;后者使用`def`定义函数,每次调用`lower`将得到不同的函数值。 157 | 158 | ## 函数调用 159 | 160 | 在`Scala`里,「函数调用」实际上等价于在`FunctionN`实例上调用`apply`方法。例如,存在一个有名函数`lower`。 161 | 162 | ```scala 163 | val lower: String => String = _.toLowerCase 164 | ``` 165 | 166 | 当发生如下函数调用时: 167 | 168 | ```scala 169 | lower("HORANCE") 170 | ``` 171 | 172 | 它等价于在`Function1[String, String]`类型的实例上调用`apply`方法。 173 | 174 | ```scala 175 | lower.apply("HORANCE") 176 | ``` 177 | 178 | 179 | 180 | # 3 柯里化和闭包 181 | 182 | ## 函数柯里化(多参数列表) 183 | 184 | 柯里化(Currying)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。 185 | 186 | 首先我们定义一个函数: 187 | 188 | ```scala 189 | def add(x:Int,y:Int)=x+y 190 | ``` 191 | 192 | 那么我们应用的时候,应该是这样用:add(1,2) 193 | 194 | 现在我们把这个函数变一下形: 195 | 196 | ```scala 197 | def add(x:Int)(y:Int) = x + y 198 | ``` 199 | 200 | 那么我们应用的时候,应该是这样用:add(1)(2),最后结果都一样是3,这种方式(过程)就叫柯里化。 201 | 202 | #### 隐式(IMPLICIT)参数 203 | 204 | 如果要指定参数列表中的某些参数为隐式(implicit),应该使用多参数列表。例如: 205 | 206 | ```scala 207 | def execute(arg: Int)(implicit ec: ExecutionContext) = ??? 208 | ``` 209 | 210 | ## 闭包 211 | 212 | **「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包** 213 | 214 | ```scala 215 | var factor = 3 216 | val adder = (i:Int) => i * factor 217 | ``` 218 | 219 | 可能会有同学有疑问:浪尖,这不对啊?我看网上说的闭包构造是: 220 | 闭包首先有函数嵌套,内部函数引用外部函数的变量,然后返回的是一个函数。 221 | 应该是这个样子的: 222 | 223 | ```text 224 | object closure { 225 | def main(args: Array[String]): Unit = { 226 | println(makeAdd()(1)) 227 | } 228 | def makeAdd() = { 229 | val more = 10 230 | (x: Int) => x + more 231 | } 232 | } 233 | ``` 234 | 235 | **为啥要用函数嵌套?** 236 | 需要外部函数的作用主要是隐藏变量,限制变量作用的范围。 237 | 有些人看到「闭包」这个名字,就一定觉得要用什么包起来才行。其实这是翻译问题,闭包的原文是 Closure,跟「包」没有任何关系。 238 | 所以函数套函数只是为了造出一个局部变量,跟闭包无关。 239 | 240 | **为啥要return函数呢?** 241 | 很明显,不return函数无法使用闭包~~ 242 | 那么现在换个脑子吧,我们将more 变成makeAdd的参数,那么就是下面的形式: 243 | def makeAdd(more : Int) = (x: Int) => x + more 244 | 245 | # 4 高阶函数 246 | 247 | 高阶函数通常来讲就是`函数的函数`,也就是说函数的`输出参数是函数`或者`函数的返回结果是函数`。在Scala中函数是一等公民。 248 | 249 | 我们看一下Scala集合类(collections)的高阶函数map: 250 | 251 | ```scala 252 | val salaries = Seq(20000, 70000, 40000) 253 | val doubleSalary = (x: Int) => x * 2 254 | val newSalaries = salaries.map(doubleSalary) // List(40000, 140000, 80000) 255 | ``` 256 | 257 | map接收一个函数为参数。所以map是一个高阶函数,map也可直接接收一个匿名函数,如下所示: 258 | 259 | ```scala 260 | val salaries = Seq(20000, 70000, 40000) 261 | val newSalaries = salaries.map(x => x * 2) // List(40000, 140000, 80000) 262 | ``` 263 | 264 | 在上面的例子中,我们并没有显示使用x:Int的形式,这是因为编译器可以通过类型推断推断出x的类型,对其更简化的形式是: 265 | 266 | ```scala 267 | val salaries = Seq(20000, 70000, 40000) 268 | val newSalaries = salaries.map(_ * 2) 269 | ``` 270 | 271 | 既然Scala编译器已经知道了参数的类型(一个单独的Int),你可以只给出函数的右半部分,不过需要使用_代替参数名(在上一个例子中是x) 272 | 273 | 274 | 275 | ## 函数可以作为值进行传递 276 | 277 | ```scala 278 | def main(args: Array[String]): Unit = { 279 | 280 | //(1)调用foo函数,把返回值给变量f 281 | //val f = foo() 282 | val f = foo 283 | println(f) // foo... 1 284 | 285 | //(2)在被调用函数foo后面加上 _,相当于把函数foo当成一个整体,传递给变量f1 286 | val f1 = foo _ 287 | 288 | foo() // foo... 289 | f1() // foo... 290 | //(3)如果明确变量类型,那么不使用下划线也可以将函数作为整体传递给变量 291 | var f2: () => Int = foo 292 | } 293 | 294 | def foo(): Int = { 295 | println("foo...") 296 | 1 297 | } 298 | ``` 299 | 300 | ## 函数可以作为参数进行传递 301 | 302 | ```scala 303 | def main(args: Array[String]): Unit = { 304 | 305 | // (1)定义一个函数,函数参数还是一个函数签名;f表示函数名称;(Int,Int)表示输入两个Int参数;Int表示函数返回值 306 | def f1(f: (Int, Int) => Int): Int = { 307 | f(2, 4) 308 | } 309 | 310 | // (2)定义一个函数,参数和返回值类型和f1的输入参数一致 311 | def add(a: Int, b: Int): Int = a + b 312 | 313 | // (3)将add函数作为参数传递给f1函数,如果能够推断出来不是调用,_可以省略 314 | println(f1(add)) 315 | println(f1(add _)) 316 | //可以传递匿名函数 317 | println(f1((a: Int, b: Int) => a + b)) 318 | } 319 | ``` 320 | 321 | ## 函数可以作为函数返回值返回 322 | 323 | ```scala 324 | def f1() = { 325 | def f2() = {} 326 | f2 _ 327 | } 328 | 329 | val f = f1() 330 | // 因为f1函数的返回值依然为函数,所以可以变量f可以作为函数继续调用 331 | f() 332 | // 上面的代码可以简化为 333 | f1()() 334 | ``` 335 | 336 | 有一些情况你希望生成一个函数, 比如: 337 | 338 | ```scala 339 | def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = { 340 | val schema = if (ssl) "https://" else "http://" 341 | (endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query" 342 | } 343 | 344 | val domainName = "www.example.com" 345 | def getURL = urlBuilder(ssl=true, domainName) 346 | val endpoint = "users" 347 | val query = "id=1" 348 | val url = getURL(endpoint, query) // "https://www.example.com/users?id=1": String 349 | ``` 350 | 351 | 注意urlBuilder的返回类型是`(String, String) => String`,这意味着返回的匿名函数有两个String参数,返回一个String。在这个例子中,返回的匿名函数是`(endpoint: String, query: String) => s"https://www.example.com/$endpoint?$query"`。 352 | 353 | # 5 模式匹配 354 | 355 | 模式匹配是检查某个值(value)是否匹配某一个模式的机制,一个成功的匹配同时会将匹配值解构为其组成部分。它是Java中的`switch`语句的升级版,同样可以用于替代一系列的 if/else 语句。 356 | 357 | ## 语法 358 | 359 | 一个模式匹配语句包括一个待匹配的值,`match`关键字,以及至少一个`case`语句。 360 | 361 | ```scala 362 | import scala.util.Random 363 | 364 | val x: Int = Random.nextInt(10) 365 | 366 | x match { 367 | case 0 => "zero" 368 | case 1 => "one" 369 | case 2 => "two" 370 | case _ => "other" 371 | } 372 | ``` 373 | 374 | 上述代码中的`val x`是一个0到10之间的随机整数,将它放在`match`运算符的左侧对其进行模式匹配,`match`的右侧是包含4条`case`的表达式,其中最后一个`case _`表示匹配其余所有情况,在这里就是其他可能的整型值。 375 | 376 | `match`表达式具有一个结果值 377 | 378 | ```scala 379 | def matchTest(x: Int): String = x match { 380 | case 1 => "one" 381 | case 2 => "two" 382 | case _ => "other" 383 | } 384 | matchTest(3) // other 385 | matchTest(1) // one 386 | ``` 387 | 388 | 这个`match`表达式是String类型的,因为所有的情况(case)均返回String,所以`matchTest`函数的返回值是String类型。 389 | 390 | ## 案例类(case classes)的匹配 391 | 392 | 案例类非常适合用于模式匹配。 393 | 394 | ```scala 395 | abstract class Notification 396 | 397 | case class Email(sender: String, title: String, body: String) extends Notification 398 | 399 | case class SMS(caller: String, message: String) extends Notification 400 | 401 | case class VoiceRecording(contactName: String, link: String) extends Notification 402 | ``` 403 | 404 | `Notification` 是一个虚基类,它有三个具体的子类`Email`, `SMS`和`VoiceRecording`,我们可以在这些案例类(Case Class)上像这样使用模式匹配: 405 | 406 | ```scala 407 | def showNotification(notification: Notification): String = { 408 | notification match { 409 | case Email(sender, title, _) => 410 | s"You got an email from $sender with title: $title" 411 | case SMS(number, message) => 412 | s"You got an SMS from $number! Message: $message" 413 | case VoiceRecording(name, link) => 414 | s"you received a Voice Recording from $name! Click the link to hear it: $link" 415 | } 416 | } 417 | val someSms = SMS("12345", "Are you there?") 418 | val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123") 419 | 420 | println(showNotification(someSms)) // prints You got an SMS from 12345! Message: Are you there? 421 | 422 | println(showNotification(someVoiceRecording)) // you received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123 423 | ``` 424 | 425 | `showNotification`函数接受一个抽象类`Notification`对象作为输入参数,然后匹配其具体类型。(也就是判断它是一个`Email`,`SMS`,还是`VoiceRecording`)。在`case Email(sender, title, _)`中,对象的`sender`和`title`属性在返回值中被使用,而`body`属性则被忽略,故使用`_`代替。 426 | 427 | 428 | 429 | 还有一个案例是Flink官方文档的demo中,要实现一个`DataStream[Tuple2[String, String]]`类型数据的计数计算,定义了`CountWithTimestamp`类型的状态,基于该状态实现累加计算。 430 | 431 | ```scala 432 | // the source data stream 433 | val stream: DataStream[Tuple2[String, String]] = ... 434 | 435 | // The data type stored in the state 436 | case class CountWithTimestamp(key: String, count: Long, lastModified: Long) 437 | 438 | // initialize or retrieve/update the state 439 | val current: CountWithTimestamp = state.value match { 440 | case null => 441 | CountWithTimestamp(value._1, 1, ctx.timestamp) 442 | case CountWithTimestamp(key, count, lastModified) => 443 | CountWithTimestamp(key, count + 1, ctx.timestamp) 444 | } 445 | 446 | ``` 447 | 448 | ## 模式守卫(Pattern gaurds) 449 | 450 | 为了让匹配更加具体,可以使用模式守卫,也就是在模式后面加上`if `。 451 | 452 | ```scala 453 | def showImportantNotification(notification: Notification, importantPeopleInfo: Seq[String]): String = { 454 | notification match { 455 | case Email(sender, _, _) if importantPeopleInfo.contains(sender) => 456 | "You got an email from special someone!" 457 | case SMS(number, _) if importantPeopleInfo.contains(number) => 458 | "You got an SMS from special someone!" 459 | case other => 460 | showNotification(other) // nothing special, delegate to our original showNotification function 461 | } 462 | } 463 | 464 | val importantPeopleInfo = Seq("867-5309", "jenny@gmail.com") 465 | 466 | val someSms = SMS("867-5309", "Are you there?") 467 | val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123") 468 | val importantEmail = Email("jenny@gmail.com", "Drinks tonight?", "I'm free after 5!") 469 | val importantSms = SMS("867-5309", "I'm here! Where are you?") 470 | 471 | println(showImportantNotification(someSms, importantPeopleInfo)) 472 | println(showImportantNotification(someVoiceRecording, importantPeopleInfo)) 473 | println(showImportantNotification(importantEmail, importantPeopleInfo)) 474 | println(showImportantNotification(importantSms, importantPeopleInfo)) 475 | ``` 476 | 477 | 在`case Email(sender, _, _) if importantPeopleInfo.contains(sender)`中,除了要求`notification`是`Email`类型外,还需要`sender`在重要人物列表`importantPeopleInfo`中,才会匹配到该模式。 478 | 479 | ## 仅匹配类型 480 | 481 | 也可以仅匹配类型,如下所示: 482 | 483 | ```scala 484 | abstract class Device 485 | case class Phone(model: String) extends Device { 486 | def screenOff = "Turning screen off" 487 | } 488 | case class Computer(model: String) extends Device { 489 | def screenSaverOn = "Turning screen saver on..." 490 | } 491 | 492 | def goIdle(device: Device) = device match { 493 | case p: Phone => p.screenOff 494 | case c: Computer => c.screenSaverOn 495 | } 496 | ``` 497 | 498 | 当不同类型对象需要调用不同方法时,仅匹配类型的模式非常有用,如上代码中`goIdle`函数对不同类型的`Device`有着不同的表现。一般使用类型的首字母作为`case`的标识符,例如上述代码中的`p`和`c`,这是一种惯例。 499 | 500 | ## 密封类 501 | 502 | 特质(trait)和类(class)可以用`sealed`标记为密封的,这意味着其所有子类都必须与之定义在相同文件中,从而保证所有子类型都是已知的。 503 | 504 | ```scala 505 | sealed abstract class Furniture 506 | case class Couch() extends Furniture 507 | case class Chair() extends Furniture 508 | 509 | def findPlaceToSit(piece: Furniture): String = piece match { 510 | case a: Couch => "Lie on the couch" 511 | case b: Chair => "Sit on the chair" 512 | } 513 | ``` 514 | 515 | 这对于模式匹配很有用,因为我们不再需要一个匹配其他任意情况的`case`。 516 | 517 | ## 备注 518 | 519 | Scala的模式匹配语句对于使用[案例类(case classes)](https://docs.scala-lang.org/zh-cn/tour/case-classes.html)表示的类型非常有用,同时也可以利用[提取器对象(extractor objects)](https://docs.scala-lang.org/zh-cn/tour/extractor-objects.html)中的`unapply`方法来定义非案例类对象的匹配。 520 | 521 | # 6 偏函数 522 | 523 | 偏函数也是函数的一种,通过偏函数我们可以方便的对输入参数做更精确的检查。例如该偏函数的输入类型为List[Int],而我们需要的是第一个元素是0的集合,这就是通过模式匹配实现的。 524 | 525 | ## 偏函数定义 526 | 527 | 实现一个偏函数的功能是返回输入的List集合的第二个元素 528 | 529 | ```scala 530 | val second: PartialFunction[List[Int], Option[Int]] = { 531 | case x :: y :: _ => Some(y) 532 | } 533 | ``` 534 | 535 | ## 偏函数原理 536 | 537 | 上述代码会被scala编译器翻译成以下代码,与普通函数相比,只是多了一个用于参数检查的函数——isDefinedAt,其返回值类型为Boolean。 538 | 539 | ```scala 540 | val second = new PartialFunction[List[Int], Option[Int]] { 541 | 542 | //检查输入参数是否合格 543 | override def isDefinedAt(list: List[Int]): Boolean = list match { 544 | case x :: y :: _ => true 545 | case _ => false 546 | } 547 | 548 | //执行函数逻辑 549 | override def apply(list: List[Int]): Option[Int] = list match { 550 | case x :: y :: _ => Some(y) 551 | } 552 | } 553 | ``` 554 | 555 | ## 偏函数使用 556 | 557 | 偏函数不能像second(List(1,2,3))这样直接使用,因为这样会直接调用apply方法,而应该调用applyOrElse方法,如下 558 | 559 | ```scala 560 | second.applyOrElse(List(1,2,3), (_: List[Int]) => None) 561 | ``` 562 | 563 | applyOrElse方法的逻辑为 : 564 | 565 | ```scala 566 | if (ifDefinedAt(list)) apply(list) else default 567 | ``` 568 | 569 | 如果输入参数满足条件,即isDefinedAt返回true,则执行apply方法,否则执行defalut方法,default方法为参数不满足要求的处理逻辑。 570 | 571 | ## 案例需求 572 | 573 | **将该List(1,2,3,4,5,6,"test")中的Int类型的元素加一,并去掉字符串。** 574 | 575 | 方法一: 576 | 577 | ```scala 578 | List(1,2,3,4,5,6,"test").filter(_.isInstanceOf[Int]).map(_.asInstanceOf[Int] + 1).foreach(println) 579 | ``` 580 | 581 | 方法二: 582 | 583 | ```scala 584 | List(1, 2, 3, 4, 5, 6, "test").collect { case x: Int => x + 1 }.foreach(println) 585 | ``` 586 | 587 | 588 | 589 | # 7 嵌套函数 590 | 591 | 在Scala中可以嵌套定义函数,定义在函数内的函数称之为局部函数。例如以下对象提供了一个`factorial`方法来计算给定数值的阶乘: 592 | 593 | ```scala 594 | def factorial(x: Int): Int = { 595 | def fact(x: Int, accumulator: Int): Int = { 596 | if (x <= 1) accumulator 597 | else fact(x - 1, x * accumulator) 598 | } 599 | fact(x, 1) 600 | } 601 | 602 | println("Factorial of 2: " + factorial(2)) 603 | println("Factorial of 3: " + factorial(3)) 604 | ``` 605 | 606 | 程序的输出为: 607 | 608 | ```scala 609 | Factorial of 2: 2 610 | Factorial of 3: 6 611 | ``` 612 | 613 | -------------------------------------------------------------------------------- /docs/scala/类型参数化.md: -------------------------------------------------------------------------------- 1 | # 类型参数化 2 | 3 | - **上界: [T <: C]** 4 | - **下界: [T >: C]** 5 | - **视图界定: [T % C]** 6 | - **上下文界定: [T : C]** 7 | - **协变:[+T]** 8 | - **逆变: [-T]** 9 | 10 | 11 | 12 | # 型变 13 | 14 | ## 术语表 15 | 16 | | **英语** | **中文** | **示例** | 17 | | ------------- | -------- | ------------------ | 18 | | Variance | 型变 | `Function[-T, +R]` | 19 | | Nonvariant | 不变 | `Array[A]` | 20 | | Covariant | 协变 | `Supplier[+A]` | 21 | | Contravariant | 逆变 | `Consumer[-A]` | 22 | | Immutable | 不可变的 | `String` | 23 | | Mutable | 可变的 | `StringBuilder` | 24 | 25 | 26 | 27 | ## 形式化 28 | 29 | **型变(Variance)」拥有三种基本形态:协变(Covariant), 逆变(Contravariant), 不变(Nonconviant)**,可以形式化地描述为: 30 | 31 | 一般地,假设类型`C[T]`持有类型参数`T`;给定两个类型`A`和`B`,如果满足`A <: B`,则`C[A]`与 `C[B]`之间存在三种关系: 32 | 33 | - 如果`C[A] <: C[B]`,那么`C`是协变的(Covariant); 34 | - 如果`C[A] :> C[B]`,那么`C`是逆变的(Contravariant); 35 | - 否则,`C`是不变的(Nonvariant)。 36 | 37 | 38 | 39 | ## scala表示 40 | 41 | ```scala 42 | trait C[+A] // C is covariant 43 | trait C[-A] // C is contravariant 44 | trait C[A] // C is nonvariant 45 | ``` 46 | 47 | - 假如我们定义一个class C[+A] {} ,这里A的类型参数是协变的,这就意味着在方法需要参数是C[AnyRef]的时候,我们可以是用C[String]来代替。 48 | - 同样的道理如果我们定义一个class C[-A] {}, 这里A的类型是逆变的,这就意味着在方法需要参数是C[String]的时候,我们可以用C[AnyRef]来代替。 49 | - 非转化类型不需要添加标记 50 | 51 | 52 | 53 | # 类型上下界 54 | 55 | ## 类型上界 56 | 57 | 在Scala中,[类型参数](https://docs.scala-lang.org/zh-cn/tour/generic-classes.html)和[抽象类型](https://docs.scala-lang.org/zh-cn/tour/abstract-type-members.html)都可以有一个类型边界约束。这种类型边界在限制类型变量实际取值的同时还能展露类型成员的更多信息。比如像`T <: A`这样声明的类型上界表示类型变量`T`应该是类型`A`的子类。下面的例子展示了类`PetContainer`的一个类型参数的类型上界。 58 | 59 | ```scala 60 | abstract class Animal { 61 | def name: String 62 | } 63 | 64 | abstract class Pet extends Animal {} 65 | 66 | class Cat extends Pet { 67 | override def name: String = "Cat" 68 | } 69 | 70 | class Dog extends Pet { 71 | override def name: String = "Dog" 72 | } 73 | 74 | class Lion extends Animal { 75 | override def name: String = "Lion" 76 | } 77 | 78 | class PetContainer[P <: Pet](p: P) { 79 | def pet: P = p 80 | } 81 | 82 | val dogContainer = new PetContainer[Dog](new Dog) 83 | val catContainer = new PetContainer[Cat](new Cat) 84 | ``` 85 | 86 | 类`PetContainer`接受一个必须是`Pet`子类的类型参数`P`。因为`Dog`和`Cat`都是`Pet`的子类,所以可以构造`PetContainer[Dog]`和`PetContainer[Cat]`。但在尝试构造`PetContainer[Lion]`的时候会得到下面的错误信息: 87 | 88 | ``` 89 | type arguments [Lion] do not conform to class PetContainer's type parameter bounds [P <: Pet] 90 | ``` 91 | 92 | 这是因为`Lion`并不是`Pet`的子类。 93 | 94 | 95 | 96 | ## 类型下界 97 | 98 | [类型上界](https://docs.scala-lang.org/zh-cn/tour/upper-type-bounds.html) 将类型限制为另一种类型的子类型,而 *类型下界* 将类型声明为另一种类型的超类型。 术语 `B >: A` 表示类型参数 `B` 或抽象类型 `B` 是类型 `A` 的超类型。 在大多数情况下,`A` 将是类的类型参数,而 `B` 将是方法的类型参数。 99 | 100 | 下面看一个适合用类型下界的例子: 101 | 102 | ```scala 103 | trait Node[+B] { 104 | def prepend(elem: B): Node[B] 105 | } 106 | 107 | case class ListNode[+B](h: B, t: Node[B]) extends Node[B] { 108 | def prepend(elem: B): ListNode[B] = ListNode(elem, this) 109 | def head: B = h 110 | def tail: Node[B] = t 111 | } 112 | 113 | case class Nil[+B]() extends Node[B] { 114 | def prepend(elem: B): ListNode[B] = ListNode(elem, this) 115 | } 116 | ``` 117 | 118 | 该程序实现了一个单链表。 `Nil` 表示空元素(即空列表)。 `class ListNode` 是一个节点,它包含一个类型为 `B` (`head`) 的元素和一个对列表其余部分的引用 (`tail`)。 `class Node` 及其子类型是协变的,因为我们定义了 `+B`。 119 | 120 | 但是,这个程序 *不能* 编译,因为方法 `prepend` 中的参数 `elem` 是*协*变的 `B` 类型。 这会出错,因为函数的参数类型是*逆*变的,而返回类型是*协*变的。 121 | 122 | 要解决这个问题,我们需要将方法 `prepend` 的参数 `elem` 的型变翻转。 我们通过引入一个新的类型参数 `U` 来实现这一点,该参数具有 `B` 作为类型下界。 123 | 124 | ```scala 125 | trait Node[+B] { 126 | def prepend[U >: B](elem: U): Node[U] 127 | } 128 | 129 | case class ListNode[+B](h: B, t: Node[B]) extends Node[B] { 130 | def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem, this) 131 | def head: B = h 132 | def tail: Node[B] = t 133 | } 134 | 135 | case class Nil[+B]() extends Node[B] { 136 | def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem, this) 137 | } 138 | ``` 139 | 140 | 现在我们像下面这么做: 141 | 142 | ``` 143 | trait Bird 144 | case class AfricanSwallow() extends Bird 145 | case class EuropeanSwallow() extends Bird 146 | 147 | 148 | val africanSwallowList= ListNode[AfricanSwallow](AfricanSwallow(), Nil()) 149 | val birdList: Node[Bird] = africanSwallowList 150 | birdList.prepend(EuropeanSwallow()) 151 | ``` 152 | 153 | 可以为 `Node[Bird]` 赋值 `africanSwallowList`,然后再加入一个 `EuropeanSwallow`。 154 | 155 | 156 | 157 | # 视图界定 158 | 159 | 在上一节,隐式参数的案例中,我们想要创建一个用于比较的函数, 通过隐式参数的隐式转换来实现 160 | 161 | ```scala 162 | object ImplicitParameter extends App { 163 | def compare[T](first:T, second:T)(implicit order:T => Ordered[T]): T = { 164 | if (first < second) first else second 165 | } 166 | } 167 | ``` 168 | 169 | 其实我们还可以换一种写法, 解决方法是使用“视图界定(view bound)”,就像这样: 170 | 171 | ```scala 172 | def compare[T <% Ordered[T]](first:T, second:T)(): T = { 173 | if (first < second) first else second 174 | } 175 | ``` 176 | 177 | `<% `关系意味着T 可以被隐士转换成Ordered[T]。 178 | 不过,Scala的视图界定即将推出历史舞台,如果你再编译时打开-future选项,使用视图界定将受到编译器的警告。可以用`隐式参数`的替换视图界定。 179 | 180 | 181 | 182 | # 上下文界定 183 | 184 | ## 语法 185 | 186 | ```scala 187 | def f[A : B](a: A) = println(a) //等同于def f[A](a:A)(implicit arg:B[A])=println(a) 188 | ``` 189 | 190 | ## 说明 191 | 192 | 上下文限定是将泛型和隐式转换的结合产物,以下两者功能相同,使用上下文限定[A : Ordering]之后,方法内无法使用隐式参数名调用隐式参数,需要通过***\*implicitly[Ordering[A]]\****获取隐式变量,如果此时无法查找到对应类型的隐式变量,会发生出错误。 193 | 194 | ```scala 195 | implicit val x = 1 196 | val y = implicitly[Int] 197 | ``` 198 | 199 | ## 案例 200 | 201 | ```scala 202 | def f[A:Ordering](a:A,b:A) =implicitly[Ordering[A]].compare(a,b) 203 | def f[A](a: A, b: A)(implicit ord: Ordering[A]) = ord.compare(a, b) 204 | ``` 205 | 206 | 207 | 208 | # 总结 209 | 210 | - 型变:协变与逆变一直不太好理解,参考[型变](https://docs.scala-lang.org/zh-cn/tour/variances.html)、[Scala教程之-深入理解协变和逆变](https://zhuanlan.zhihu.com/p/137365296) 211 | - 上下界: 限定类型参数子、父类的关系 212 | - 上下文界定: 泛型结合隐式转换 213 | 214 | -------------------------------------------------------------------------------- /docs/scala/隐式转换Implicit.md: -------------------------------------------------------------------------------- 1 | # 隐式转换简介 2 | 3 | 隐式转换是Scala中一种非常有特色的功能,是其他编程语言所不具有的,它允许你手动指定,将某种类型对象转换为另一种类型对象。通过这个功能可是实现 更加灵活强大的功能。 4 | 5 | Scala的隐式转换最为核心的就是定义隐式函数,即implicit conversion function,定了隐式函数,在程序的一定作用域中scala会 自动调用。**`Scala会根据隐式函数的签名,在程序中使用到隐式函数参数定义的类型对象时,会自动调用隐式函数将其传入隐式函数,转换为另一种类型对象并返回,这就是scala的“隐式转换”`**。 6 | 7 | **当编译器第一次编译失败的时候,会在当前的环境中查找能让代码编译通过的方法,用于将类型进行转换,实现二次编译** 8 | 9 | 10 | 11 | # 隐式函数 12 | 13 | ## 转换到一个预期的类型 14 | 15 | 隐式函数是使用最多的隐式转换,它的主要作用是将一定数据类型对象A转换为另一种类型对象B并返回,使对象A可以使用对象B的属性和函数。 16 | 17 | 我们来看一个案例,如果在scala中定义一下代码: 18 | 19 | ```scala 20 | val x:Int=3.5 21 | ``` 22 | 23 | 很显然"type mismatch",你不能把一个Double类型赋值到Int类型. 24 | 25 | 这个时候可以添加一个隐式函数后可以实现Double类型到Int类型的赋值: 26 | 27 | ```scala 28 | implicit def doubleToInt(a: Double) = a.toInt 29 | ``` 30 | 31 | 隐式函数的名称对结构没有影响,即`implicit def doubleToInt(a: Double) = a.toInt`t函数可以是任何名字,只不能采用doubleToInt这种方式函数的意思比较明确,阅读代码的人可以见名知义,增加代码的可读性。 32 | 33 | ## 扩展类库 34 | 35 | **隐式函数的另一个作用就是: 可以快速地扩展现有类库的功能** 36 | 37 | 需求:通过隐式转化为Int类型增加方法。 38 | 39 | ```scala 40 | object ExpandLibraryTest { 41 | 42 | // 使用implicit关键字声明的函数称之为隐式函数 43 | implicit def convert(arg: Int): MyRichInt = MyRichInt(arg) 44 | 45 | def main(args: Array[String]): Unit = { 46 | println(2.myMax(6)) 47 | } 48 | } 49 | 50 | case class MyRichInt(self: Int) { 51 | 52 | def myMax(i: Int): Int = { 53 | if (self < i) i else self 54 | } 55 | 56 | def myMin(i: Int): Int = { 57 | if (self < i) self else i 58 | } 59 | } 60 | ``` 61 | 62 | 以上代码中,为Int类型增加了**myMax**、**myMin**方法,同时将自身作为函数的来使用。 63 | 64 | 65 | 66 | 同样是扩展类库的功能,来看一个生产中的代码设计,Flink中实现双流Join是比较常见的需求,order流和item流实现双流join,其中order为主流,我们为order流扩展一个join item流的函数。 67 | 68 | **`隐式函数设计:`** 69 | 70 | ```scala 71 | implicit def joinSalesFlatOrderItem(source: DataStream[SalesFlatOrder]) 72 | : SalesFlatOrderAndSalesFlatOrderItemMerger = { 73 | SalesFlatOrderAndSalesFlatOrderItemMerger(source) 74 | } 75 | ``` 76 | 77 | 通过隐式函数定义,我们试图将DataStream[SalesFlatOrder]转换为SalesFlatOrderAndSalesFlatOrderItemMerger类型对象,同时本身,也就是我们所定义的`source`作为SalesFlatOrderAndSalesFlatOrderItemMerger类的形参传入使用。 78 | 79 | 那么order流与item流的具体实现我们就在SalesFlatOrderAndSalesFlatOrderItemMerger中实现了。 80 | 81 | **`双流join基类设计`**: 82 | 83 | 从程序设计的规范来看,可以对实现双流join的类定义一个基类,它的作用就是接受两种类型的流,输出为另一种类型的流: 84 | 85 | ```scala 86 | /** 87 | * DataStream[T1] and DataStream[T2] to DataStream[T] 88 | * @param source 89 | * @tparam T1 90 | * @tparam T2 91 | * @tparam T 92 | */ 93 | abstract class Merger[T1 <: Model,T2 <: Model, T <: Model](source: DataStream[T1]) extends Serializable { 94 | 95 | def joinStream(input: DataStream[T2]): DataStream[T] = { 96 | merge(source, input).uid(s"merge_${getName}") 97 | } 98 | 99 | def getName: String 100 | 101 | protected def merge(input1: DataStream[T1], input2: DataStream[T2]): DataStream[T] 102 | } 103 | ``` 104 | 105 | 以后所有要实现双流join的类只需要继承**Merger**类,在`merge函数`中去实现具体业务就行了。 106 | 107 | **`使用`** 108 | 109 | ```scala 110 | streamA 111 | .joinStream(streamB) 112 | .joinStream(streamC) 113 | .joinStream(streamD) 114 | ``` 115 | 116 | - streamA是一个DataStream,并没有joinStream()的方法,当streamA调用类中不存在的方法或成员时,会自动将对象进行隐式转换,也就是转换为了SalesFlatOrderAndSalesFlatOrderItemMerger类型。 117 | - SalesFlatOrderAndSalesFlatOrderItemMerger类继承于Merger类,基于多态的特性,就可以直接使用joinStream()方法并且将streamB当成参数传入了,也就是`streamA .joinStream(streamB)`的方式。 118 | 119 | ## 注意 120 | 121 | 如果转换存在二义性,则不会发生隐式转换 122 | 123 | - 对于隐式参数,如果查找范围内有两个该类型的变量,则编译报错。 124 | - 对于隐式转换,如果查找范围内有两个从A类型到B类型的隐式转换,则编译报错。 125 | 126 | 127 | 128 | # 隐式参数 129 | 130 | 普通方法或者函数中的参数可以通过**`implicit`**关键字声明为隐式参数,调用该方法时,就可以传入该参数,编译器会在相应的作用域寻找符合条件的隐式值。 131 | 132 | 133 | 134 | 隐式参数说明: 135 | 136 | - 同一个作用域中,相同类型的隐式值只能有一个 137 | - 编译器按照隐式参数的类型去寻找对应类型的隐式值,与隐式值的名称无关 138 | - 隐式参数优先于默认参数 139 | 140 | ```scala 141 | object TestImplicitParameter { 142 | 143 | implicit val str: String = "hello world!" 144 | 145 | def hello(implicit arg: String="good bey world!"): Unit = { 146 | println(arg) 147 | } 148 | 149 | def main(args: Array[String]): Unit = { 150 | hello 151 | } 152 | } 153 | ``` 154 | 155 | 156 | 157 | ## 隐式转换作为隐式参数 158 | 159 | 下面的代码给定的是一个普通的比较函数: 160 | 161 | ```scala 162 | object ImplicitParameter extends App { 163 | 164 | def compare[T](first:T, second:T): T = { 165 | if (first < second) first else second 166 | } 167 | 168 | } 169 | ``` 170 | 171 | 以上代码不能直接使用,这里面泛型T没有具体指定,它不能直接使用。如果想编译通过,可以将当前类型变量视图界定指定其上界为Ordered[T]。 172 | 173 | ```scala 174 | object ImplicitParameter extends App { 175 | def compare[T<: Ordered[T]](first:T, second:T): T = { 176 | if (first < second) first else second 177 | } 178 | } 179 | ``` 180 | 181 | 这是一种解决方案,我们还有一种解决方案就是通过隐式参数的隐式转换来实现,代码如下: 182 | 183 | ```scala 184 | object ImplicitParameter extends App { 185 | def compare[T](first:T, second:T)(implicit order:T => Ordered[T]): T = { 186 | if (first < second) first else second 187 | } 188 | } 189 | ``` 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /docs/scala/面向对象的Scala.md: -------------------------------------------------------------------------------- 1 | 鉴于[一切值都是对象](https://docs.scala-lang.org/zh-cn/tour/unified-types.html),可以说Scala是一门纯面向对象的语言。对象的类型和行为是由[类](https://docs.scala-lang.org/zh-cn/tour/classes.html)和[特质](https://docs.scala-lang.org/zh-cn/tour/traits.html)来描述的。类可以由子类化和一种灵活的、基于mixin的`组合机制`(它可作为多重继承的简单替代方案,后面学习设计模式的时候再详细说)来扩展。 2 | 3 | # 1 类与对象 4 | 5 | Classes就是类,和java中的类相似,它里面可以包含方法、常量、变量、类型、对象、特质、类等。 6 | 7 | 一个最简的类的定义就是关键字class+标识符,类名首字母应大写。如下所示: 8 | 9 | ```scala 10 | class Family 11 | 12 | val family = new Family 13 | ``` 14 | 15 | new关键字是用来创建类的实例。在上面的例子中,Family没有定义构造器,所以默认带有一个无参的默认的构造器。 16 | 17 | - 构造器 18 | 19 | 那么怎么给类加一个构造器呢? 20 | 21 | ```scala 22 | class Point(var x: Int, var y: Int) { 23 | 24 | override def toString: String = 25 | s"($x, $y)" 26 | } 27 | 28 | val point1 = new Point(2, 3) 29 | point1.x // 2 30 | println(point1) // prints (2, 3) 31 | ``` 32 | 33 | 和其他的编程语言不同的是,Scala的类构造器定义在类的签名中:(var x: Int, var y: Int)。 这里我们还重写了AnyRef里面的toString方法。 34 | 35 | 构造器也可以拥有默认值: 36 | 37 | ```scala 38 | class Point(var x: Int = 0, var y: Int = 0) 39 | 40 | val origin = new Point // x and y are both set to 0 41 | val point1 = new Point(1) 42 | println(point1.x) // prints 1 43 | ``` 44 | 45 | 主构造方法中带有val和var的参数是公有的。然而由于val是不可变的,所以不能像下面这样去使用。 46 | 47 | ```scala 48 | class Point(val x: Int, val y: Int) 49 | val point = new Point(1, 2) 50 | point.x = 3 // <-- does not compile 51 | ``` 52 | 53 | 不带val或var的参数是私有的,仅在类中可见。 54 | 55 | ```scala 56 | class Point(x: Int, y: Int) 57 | val point = new Point(1, 2) 58 | point.x // <-- does not compile 59 | ``` 60 | 61 | - 私有成员和Getter/Setter语法 62 | 63 | Scala的成员默认是public的。如果想让其变成私有的,可以加上private修饰符,Scala的getter和setter语法和java不太一样,下面我们来举个例子: 64 | 65 | ```scala 66 | class Point { 67 | 68 | private var _x = 0 69 | private var _y = 0 70 | private val bound = 100 71 | 72 | def x = _x 73 | def x_= (newValue: Int): Unit = { 74 | if (newValue < bound) _x = newValue else printWarning 75 | } 76 | 77 | def y = _y 78 | def y_= (newValue: Int): Unit = { 79 | if (newValue < bound) _y = newValue else printWarning 80 | } 81 | 82 | private def printWarning = println("WARNING: Out of bounds") 83 | } 84 | 85 | object Point { 86 | def main(args: Array[String]): Unit = { 87 | val point1 = new Point 88 | point1.x = 99 89 | point1.y = 101 // prints the warning 90 | } 91 | } 92 | ``` 93 | 94 | 我们定义了两个私有变量_x, _y, 同时还定义了他们的get方法x和y,那么相应的他们的set方法就是x_ 和y_, 在get方法后面加上下划线就可以了。 95 | 96 | 97 | 98 | # 2 特质 99 | 100 | 特质 (Traits) 用于在类 (Class)之间共享程序接口 (Interface)和字段 (Fields)。 它们类似于Java 8的接口。 类和对象 (Objects)可以扩展特质,但是特质不能被实例化,因此特质没有参数。 101 | 102 | ## 定义一个特质 103 | 104 | 最简化的特质就是关键字trait+标识符: 105 | 106 | ```scala 107 | trait HairColor 108 | ``` 109 | 110 | 特征作为泛型类型和抽象方法非常有用。 111 | 112 | ```scala 113 | trait Iterator[A] { 114 | def hasNext: Boolean 115 | def next(): A 116 | } 117 | ``` 118 | 119 | 扩展 `trait Iterator [A]` 需要一个类型 `A` 和实现方法`hasNext`和`next`。 120 | 121 | ## 使用特质 122 | 123 | 使用 `extends` 关键字来扩展特征。然后使用 `override` 关键字来实现trait里面的任何抽象成员: 124 | 125 | ```scala 126 | trait Iterator[A] { 127 | def hasNext: Boolean 128 | def next(): A 129 | } 130 | 131 | class IntIterator(to: Int) extends Iterator[Int] { 132 | private var current = 0 133 | override def hasNext: Boolean = current < to 134 | override def next(): Int = { 135 | if (hasNext) { 136 | val t = current 137 | current += 1 138 | t 139 | } else 0 140 | } 141 | } 142 | 143 | 144 | val iterator = new IntIterator(10) 145 | iterator.next() // returns 0 146 | iterator.next() // returns 1 147 | ``` 148 | 149 | 这个类 `IntIterator` 将参数 `to` 作为上限。它扩展了 `Iterator [Int]`,这意味着方法 `next` 必须返回一个Int。 150 | 151 | ## 子类型 152 | 153 | 凡是需要特质的地方,都可以由该特质的子类型来替换。 154 | 155 | ```scala 156 | import scala.collection.mutable.ArrayBuffer 157 | 158 | trait Pet { 159 | val name: String 160 | } 161 | 162 | class Cat(val name: String) extends Pet 163 | class Dog(val name: String) extends Pet 164 | 165 | val dog = new Dog("Harry") 166 | val cat = new Cat("Sally") 167 | 168 | val animals = ArrayBuffer.empty[Pet] 169 | animals.append(dog) 170 | animals.append(cat) 171 | animals.foreach(pet => println(pet.name)) // Prints Harry Sally 172 | ``` 173 | 174 | 在这里 `trait Pet` 有一个抽象字段 `name` ,`name` 由Cat和Dog的构造函数中实现。最后一行,我们能调用`pet.name`的前提是它必须在特质Pet的子类型中得到了实现。 175 | 176 | 177 | 178 | # 3 封装 179 | 180 | `封装`就是把抽象出的数据和对数据的操作封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(成员方法),才能对数据进行操作。**Java封装操作如下:** 181 | 182 | 1. **将属性进行私有化** 183 | 2. **提供一个公共的set方法,用于对属性赋值** 184 | 3. **提供一个公共的get方法,用于获取属性的值** 185 | 186 | 187 | 188 | Scala中的public属性,底层实际为private,并通过get方法`(obj.field())`和set方法`(obj.field_=(value))`对其进行操作。所以Scala并不推荐将属性设为private,再为其设置public的get和set方法的做法。但由于很多Java框架都利用反射调用getXXX和setXXX方法,有时候为了和这些框架兼容,也会为Scala的属性设置getXXX和setXXX方法(通过@BeanProperty注解实现) 189 | 190 | ## 访问权限 191 | 192 | 在Java中,访问权限分为:public,private,protected和默认。在Scala中,你可以通过类似的修饰符达到同样的效果。但是使用上有区别。 193 | 194 | 195 | 196 | git remote add origin https://github.com/fengchi66/bigdata-project.git 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /docs/verseline /Untitled 1.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengchi66/bigdata-project/e03d368581499a09ea279c04b3f0e4db35fd3014/docs/verseline /Untitled 1.md -------------------------------------------------------------------------------- /docs/verseline /对风说爱你.md: -------------------------------------------------------------------------------- 1 | > 2 | > 3 | >**`起风了 4 | >在六月的最后几天 5 | > 今夜你的黑头发 6 | >一直飘到我心上 7 | >在村庄的清晨 8 | >我该对你说些什么 9 | >你是静静生长的姑娘 10 | >在静静的情意中成长 11 | >即使我是一个粗枝大叶的人 12 | >也看到了黑麻花 红豹子 13 | >我感到魅惑 14 | >我把撕碎的诗稿和水打湿 15 | >当她还在北方草原采花的时候 16 | >我的耳畔驶过南方的清风 17 | >温柔得紧张起来 18 | >我年纪很轻 有点感伤 19 | >明晚的夜晚 20 | >我会为你念诗 21 | >和风一起抚摸你的所有`** 22 | 23 | -------------------------------------------------------------------------------- /docs/深度学习推荐系统/03深度学习基础.md: -------------------------------------------------------------------------------- 1 | # 03深度学习基础 2 | 3 | 上中学的时候,你肯定在生物课上学到过,神经元是我们神经系统的最基本单元,我们的大脑、小脑、脊髓都是由神经元组成的。比如,大脑大概包含了 1000 亿个神经元!正是这些小小的神经元之间互相连接合作,让大脑能够完成非常复杂的学习任务,这是一个多么神奇的过程! 4 | 5 | 下面这张图就是一个神经元的结构示意图,它大致由树突、细胞体(图中细胞核和周围的部分)、轴突、轴突末梢等几部分组成。树突是神经元的输入信号通道,它的功能是将其他神经元的动作电位(可以当作一种信号)传递至细胞体。 6 | 7 | -------------------------------------------------------------------------------- /flink-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | bigdata-project 7 | com.yit.data 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | flink-demo 13 | 14 | 15 | -------------------------------------------------------------------------------- /flink-demo/src/main/scala/Person.scala: -------------------------------------------------------------------------------- 1 | case class Person(a: Int) { 2 | val s = 10 3 | 4 | } 5 | -------------------------------------------------------------------------------- /flink-demo/src/main/scala/Test.scala: -------------------------------------------------------------------------------- 1 | import org.apache.flink.streaming.api.scala._ 2 | 3 | object Test { 4 | def main(args: Array[String]): Unit = { 5 | 6 | <<<<<<< HEAD 7 | val env = StreamExecutionEnvironment. getExecutionEnvironment 8 | 9 | env.fromCollection(List(1,2,3,4,5,6,7,8)).print() 10 | 11 | env.execute("Job") 12 | ======= 13 | env.addSource(new Source).assignAscendingTimestamps(_.ts) 14 | 15 | >>>>>>> 4aabcb9544e0851d3ac608f848641a3fd6a6b60f 16 | 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /flink-demo/src/main/scala/com/bigdata/flink/datastream/step/Check.scala: -------------------------------------------------------------------------------- 1 | package com.bigdata.flink.datastream.step 2 | 3 | import org.apache.flink.api.common.typeinfo.TypeInformation 4 | import org.apache.flink.streaming.api.scala.DataStream 5 | 6 | abstract class Check[T :TypeInformation] { 7 | 8 | // 请子类取一个有意义的方法名的方法来调用map方法 9 | def map(input: DataStream[T]): DataStream[T] = { 10 | check(input).uid(s"check_${getUid}").name(s"check_${getName}") 11 | } 12 | 13 | def getUid: String 14 | 15 | def getName: String 16 | 17 | protected def check(input: DataStream[T]): DataStream[T] 18 | } 19 | -------------------------------------------------------------------------------- /flink-demo/src/main/scala/com/bigdata/flink/datastream/util/Constants.scala: -------------------------------------------------------------------------------- 1 | package com.bigdata.flink.datastream.util 2 | 3 | object Constants { 4 | 5 | val USER_EVENT_LIST = List("") 6 | 7 | } 8 | -------------------------------------------------------------------------------- /flink-demo/src/main/scala/com/bigdata/flink/datastream/util/DateUtil.scala: -------------------------------------------------------------------------------- 1 | package com.bigdata.flink.datastream.util 2 | 3 | import java.time.Instant 4 | import java.time.LocalDateTime 5 | import java.time.ZoneId 6 | 7 | object DateUtil { 8 | 9 | import java.time.format.DateTimeFormatter 10 | 11 | val dtf: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") 12 | 13 | def formatTsToString(milliseconds: Long): String = { 14 | dtf.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(milliseconds), ZoneId.systemDefault())) 15 | } 16 | 17 | def formatStringToTs(time: String): Long = { 18 | val localDateTime1 = LocalDateTime.parse(time, dtf) 19 | LocalDateTime.from(localDateTime1).atZone(ZoneId.systemDefault).toInstant.toEpochMilli 20 | } 21 | 22 | def main(args: Array[String]): Unit = { 23 | println(formatStringToTs("2020-12-02 11:10")) 24 | 25 | println(formatTsToString(formatStringToTs("2020-12-02 11:10"))) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /flink-demo/src/main/scala/com/bigdata/flink/datastream/util/UserEventCount.scala: -------------------------------------------------------------------------------- 1 | package com.bigdata.flink.datastream.util 2 | 3 | case class UserEventCount(id: Int, timestamp: Long, count: Int) 4 | -------------------------------------------------------------------------------- /flink-demo/src/main/scala/com/bigdata/flink/datastream/windowing/EventTimeTriggerDemo.scala: -------------------------------------------------------------------------------- 1 | package com.bigdata.flink.datastream.windowing 2 | 3 | import com.bigdata.flink.datastream.util.{DateUtil, UserEventCount} 4 | import org.apache.flink.api.common.functions.AggregateFunction 5 | import org.apache.flink.streaming.api.TimeCharacteristic 6 | import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor 7 | import org.apache.flink.streaming.api.scala._ 8 | import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction 9 | import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows 10 | import org.apache.flink.streaming.api.windowing.time.Time 11 | import org.apache.flink.streaming.api.windowing.triggers.{ContinuousEventTimeTrigger, EventTimeTrigger, PurgingTrigger} 12 | import org.apache.flink.streaming.api.windowing.windows.TimeWindow 13 | import org.apache.flink.util.Collector 14 | 15 | 16 | /** 17 | * 测试EventTimeTrigger,触发时机以及窗口函数的简单使用 18 | */ 19 | object EventTimeTriggerDemo { 20 | def main(args: Array[String]): Unit = { 21 | val env = StreamExecutionEnvironment.getExecutionEnvironment 22 | env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) 23 | env.enableCheckpointing(120000) 24 | env.setParallelism(1) 25 | 26 | val list = List( 27 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:00"), 1), 28 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:04"), 2), 29 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:03"), 3), 30 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:08"), 4), 31 | UserEventCount(1, DateUtil.formatStringToTs("2020-12-15 12:10"), 5) 32 | ) 33 | 34 | env.fromCollection(list) 35 | .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[UserEventCount](Time.minutes(2)) { 36 | override def extractTimestamp(t: UserEventCount): Long = t.timestamp 37 | }) 38 | .keyBy(_.id) 39 | .window(TumblingEventTimeWindows.of(Time.minutes(5))) 40 | // .trigger(EventTimeTrigger.create()) 41 | // .trigger(ContinuousEventTimeTrigger.of(Time.minutes(2))) 42 | // .trigger(PurgingTrigger.of(ContinuousEventTimeTrigger.of(Time.minutes(2)))) 43 | .process(new WindowFunc) 44 | .print() 45 | 46 | env.execute("job") 47 | } 48 | 49 | class AggCountFunc extends AggregateFunction[UserEventCount, (Int, Int), (Int, Int)] { 50 | override def createAccumulator(): (Int, Int) = (0, 0) 51 | 52 | override def add(in: UserEventCount, acc: (Int, Int)): (Int, Int) = (in.id, in.count + acc._2) 53 | 54 | override def getResult(acc: (Int, Int)): (Int, Int) = acc 55 | 56 | override def merge(acc: (Int, Int), acc1: (Int, Int)): (Int, Int) = (acc._1, acc._2 + acc1._2) 57 | } 58 | 59 | class WindowResult extends ProcessWindowFunction[(Int, Int), (String, Int, Int), Int, TimeWindow] { 60 | override def process(key: Int, context: Context, elements: Iterable[(Int, Int)], out: Collector[(String, Int, Int)]): Unit = { 61 | // 输出窗口开始时间 62 | val windowStart = DateUtil.formatTsToString(context.window.getStart) 63 | val userId = elements.head._1 64 | val count = elements.head._2 65 | out.collect((windowStart, userId, count)) 66 | } 67 | } 68 | 69 | class WindowFunc extends ProcessWindowFunction[UserEventCount, (String, Int, Int), Int, TimeWindow] { 70 | override def process(key: Int, context: Context, elements: Iterable[UserEventCount], out: Collector[(String, Int, Int)]): Unit = { 71 | val count = elements.map(_.count).sum 72 | val windowStart = DateUtil.formatTsToString(context.window.getStart) 73 | out.collect((windowStart, key, count)) 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.yit.data 8 | bigdata-project 9 | pom 10 | 1.0-SNAPSHOT 11 | 12 | flink-demo 13 | design-pattern 14 | scala-demo 15 | 16 | 17 | 18 | UTF-8 19 | 1.10.2 20 | 2.11 21 | 2.11.12 22 | 23 | 24 | 25 | 26 | org.apache.flink 27 | flink-scala_${scala.binary.version} 28 | ${flink.version} 29 | 30 | 31 | scala-library 32 | org.scala-lang 33 | 34 | 35 | scala-parser-combinators_2.11 36 | org.scala-lang.modules 37 | 38 | 39 | 40 | 41 | org.apache.flink 42 | flink-streaming-scala_${scala.binary.version} 43 | ${flink.version} 44 | 45 | 46 | slf4j-api 47 | org.slf4j 48 | 49 | 50 | snappy-java 51 | org.xerial.snappy 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /scala-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | bigdata-project 7 | com.yit.data 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | scala-demo 13 | 14 | 15 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/func/FunctionDemo01.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.func 2 | 3 | object FunctionDemo01 extends App { 4 | 5 | // val aa = "AbC" 6 | // 7 | // /** 8 | // * 函数值 9 | // */ 10 | // val f1 = (a: String) => a.toLowerCase 11 | // 12 | // val f2 = new Function[String, String] { 13 | // override def apply(v1: String): String = v1.toLowerCase 14 | // } 15 | // 16 | // val f3 = new (String => String) { 17 | // def apply(s: String): String = s.toLowerCase 18 | // } 19 | // 20 | // val lower: String => String = _.toLowerCase 21 | // 22 | //// val add: (Int, Int) => Int = _ + _ 23 | // // 或者 24 | // def add(a: Int, b: Int): Int = a + b 25 | // 26 | // val salaries = Seq(20000, 70000, 40000) 27 | // val newSalaries = salaries.map(x => x * 2) // List(40000, 140000, 80000) 28 | // 29 | // def f4(f: (Int, Int) => Int): Int = { 30 | // f(2, 4) 31 | // } 32 | 33 | def f1() = { 34 | def f2() = {} 35 | f2 _ 36 | } 37 | 38 | val f = f1() 39 | // 因为f1函数的返回值依然为函数,所以可以变量f可以作为函数继续调用 40 | f() 41 | // 上面的代码可以简化为 42 | f1()() 43 | 44 | // println(f4(add)) 45 | 46 | 47 | // println(lower("sdKiFG")) 48 | // println(add(1, 2)) 49 | } 50 | 51 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/func/FunctionDemo02.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.func 2 | 3 | object FunctionDemo02 { 4 | 5 | def main(args: Array[String]): Unit = { 6 | 7 | //(1)调用foo函数,把返回值给变量f 8 | //val f = foo() 9 | val f = foo 10 | println(f) 11 | 12 | //(2)在被调用函数foo后面加上 _,相当于把函数foo当成一个整体,传递给变量f1 13 | val f1 = foo _ 14 | 15 | foo() 16 | f1() 17 | //(3)如果明确变量类型,那么不使用下划线也可以将函数作为整体传递给变量 18 | var f2: () => Int = foo 19 | } 20 | 21 | def foo(): Int = { 22 | println("foo...") 23 | 1 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/func/FunctionDemo03.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.func 2 | 3 | object FunctionDemo03 { 4 | 5 | def main(args: Array[String]): Unit = { 6 | 7 | // (1)定义一个函数,函数参数还是一个函数签名;f表示函数名称;(Int,Int)表示输入两个Int参数;Int表示函数返回值 8 | def f1(f: (Int, Int) => Int): Int = { 9 | f(2, 4) 10 | } 11 | 12 | // (2)定义一个函数,参数和返回值类型和f1的输入参数一致 13 | def add(a: Int, b: Int): Int = a + b 14 | 15 | // (3)将add函数作为参数传递给f1函数,如果能够推断出来不是调用,_可以省略 16 | println(f1(add)) 17 | println(f1(add _)) 18 | //可以传递匿名函数 19 | println(f1((a: Int, b: Int) => a + b)) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/func/PartialFunctionDemo01.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.func 2 | 3 | object PartialFunctionDemo01 { 4 | 5 | def main(args: Array[String]): Unit = { 6 | 7 | val list = List(1,2,3,4,5,6,"test") 8 | 9 | // 将该List(1,2,3,4,5,6,"test")中的Int类型的元素加一,并去掉字符串 10 | println(list.filter(_.isInstanceOf[Int]).map(_.asInstanceOf[Int] + 1)) 11 | 12 | list.collect{ 13 | case a: Int => a + 1 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/implicit_demo/Double2IntImplicitTest.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.implicit_demo 2 | 3 | object Double2IntImplicitTest { 4 | 5 | def main(args: Array[String]): Unit = { 6 | implicit def doubleToInt(a: Double) = a.toInt 7 | 8 | val a:Int = 1.2 9 | 10 | println(a) 11 | } 12 | 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/implicit_demo/ExpandLibraryTest.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.implicit_demo 2 | 3 | /** 4 | * 扩展类库 5 | */ 6 | object ExpandLibraryTest { 7 | 8 | // 使用implicit关键字声明的函数称之为隐式函数 9 | implicit def convert(arg: Int): MyRichInt = MyRichInt(arg) 10 | 11 | def main(args: Array[String]): Unit = { 12 | println(2.myMax(6)) 13 | } 14 | } 15 | 16 | case class MyRichInt(self: Int) { 17 | 18 | def myMax(i: Int): Int = { 19 | if (self < i) i else self 20 | } 21 | 22 | def myMin(i: Int): Int = { 23 | if (self < i) self else i 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/implicit_demo/ImplicitParameter.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.implicit_demo 2 | 3 | object ImplicitParameter extends App { 4 | def compare[T](first:T, second:T)(implicit order:T => Ordered[T]): T = { 5 | if (first < second) first else second 6 | } 7 | println(compare("A","B")) 8 | } 9 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/implicit_demo/ImplicitParameter2.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.implicit_demo 2 | 3 | object ImplicitParameter2 extends App { 4 | 5 | def compare[T <% Ordered[T]](first:T, second:T)(): T = { 6 | if (first < second) first else second 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/implicit_demo/IntWrapper.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.implicit_demo 2 | 3 | case class IntWrapper(i: Int) { 4 | 5 | private[IntWrapper] var l = List(i) 6 | 7 | def map(implicit m: Int => List[Int]): List[List[Int]] = { 8 | l.map(m) 9 | } 10 | } 11 | 12 | object IntWrapper { 13 | 14 | List 15 | 16 | implicit val f =(i : Int) => List(i + 1) 17 | def main(args : Array[String]) :Unit = { 18 | //map使用隐式参数,编译器在当前可见作用域中查找到 f 作为map的参数 19 | print(IntWrapper(3).map) // List(List(2)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scala-demo/src/main/scala/com/yit/data/variance/Cell.scala: -------------------------------------------------------------------------------- 1 | package com.yit.data.variance 2 | 3 | class Cell[T](init: T) { 4 | private[this] var current = init 5 | def get = current 6 | def set(x: T) = {current = x} 7 | 8 | } 9 | --------------------------------------------------------------------------------