<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Jark&#39;s Blog</title>
  <icon>https://www.gravatar.com/avatar/77efb4eed4c1292d514ac18a6e31b19c</icon>
  <subtitle>当你的才华还撑不起你的野心时，你就应该静下心来学习。</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="http://wuchong.me/"/>
  <updated>2023-02-28T17:56:09.743Z</updated>
  <id>http://wuchong.me/</id>
  
  <author>
    <name>WuChong</name>
    <email>imjark@gmail.com</email>
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Flink 1.16：Hive SQL 如何平迁到 Flink SQL</title>
    <link href="http://wuchong.me/blog/2022/12/14/migrate-from-hive-sql-to-flink-sql/"/>
    <id>http://wuchong.me/blog/2022/12/14/migrate-from-hive-sql-to-flink-sql/</id>
    <published>2022-12-13T16:56:52.000Z</published>
    <updated>2023-02-28T17:56:09.743Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Hive-SQL-迁移的动机"><a href="#Hive-SQL-迁移的动机" class="headerlink" title="Hive SQL 迁移的动机"></a>Hive SQL 迁移的动机</h2><p>Flink 已经是流计算的事实标准，当前国内外做实时计算或流计算一般都会选择 Flink 和 Flink SQL。另外，Flink 也是是家喻户晓的流批一体大数据计算引擎。</p><p>然而，目前 Flink 也面临着挑战。比如虽然现在大规模应用都以流计算为主，但 Flink 批计算的应用并不广泛，想要进一步推动真正意义上的流批一体落地，需要推动业界更多地落地 Flink 批计算，需要更积极地拥抱现有的离线生态。当前业界离线生态主要以 Hive 为主，因此我们在过去版本中做了很多与 Hive 相关的集成，包括 Hive Catalog、Hive 语法兼容、Hive UDF 兼容、流式写入 Hive 等。在 Flink 1.16 版本中，我们进一步提升了 HiveSQL 的兼容度，还支持了 HiveServer2 的协议兼容。</p><p>所以，为什么 Flink 要去支持 Hive SQL 的迁移？一方面，我们希望吸引更多的 Hive 离线数仓用户，通过用户来不断打磨批计算引擎，对齐主流批计算引擎。另一方面，通过兼容 Hive SQL，来降低现有离线用户使用 Flink 开发离线业务的门槛。除此之外，另外，生态是开源产品的最大门槛。Flink 已经拥有非常丰富的实时生态工具，但离线生态依然较为欠缺。通过兼容 Hive 生态可以快速融入 Hive 离线生态工具和平台，降低用户接入的成本。最后，这也是实现流批一体的重要一环，我们希望推动业界尝试统一的流计算和批计算引擎，再统一流计算和批计算 SQL。</p><p>从用户角度来看，Hive SQL 为什么要迁移到 Flink SQL 上？</p><p>对于平台方而言，统一流批计算引擎，只需维护一套 Flink 引擎，可以降低维护成本，提升团队研发效率。另外，可以利用 Flink + Gateway+ HiveSQL 兼容，快速建设一套 OLAP 系统。Flink 的另一优势是拥有丰富的 connector 生态，可以借助 Flink 丰富的数据源实现强大的联邦查询。比如不仅可以在 Hive 数仓里做 ad-hoc 查询，也可以将 Hive 表数据与 MySQL、HBase、Iceberg、Hudi 等数据源做联邦查询等。</p><p>对于离线数仓用户而言，可以用 Hive SQL 写流计算作业，极大降低实时化改造成本。使用的依然是以前的 HiveSQL 语法，但是可以运行在 streaming 模式下。在此基础之上也可以进一步探索流批一体 SQL 层以及流批一体数仓层的建设。</p><h2 id="Hive-SQL-迁移的挑战"><a href="#Hive-SQL-迁移的挑战" class="headerlink" title="Hive SQL 迁移的挑战"></a>Hive SQL 迁移的挑战</h2><p>但是 Flink 支持 HiveSQL 的迁移面临着很多挑战，主要有以下三个方面：</p><ul><li>兼容：包括离线数仓作业和Hive平台工具的兼容。主要对应用户层的兼容和平台方的兼容。</li><li>稳定性：迁移后的作业首先要保证生产的稳定性。我们在1.16中也做了很多这方面的工作，包括FLIP-168 预测执行和Adaptive Hash Join。后续我们会发表更多的文章来介绍这方面的工作。</li><li>性能：最后性能也是很重要的，在1.16中我们也做了很多这方面的工作，包括Dynamic Partition Pruning（DPP）、元数据访问加速等，后续也会发表更多文章来介绍这方面的工作。</li></ul><p>接下来我们重点讲解下 Hive 兼容相关的工作。</p><p><img src="https://user-images.githubusercontent.com/5378924/221926784-7cf6845d-506b-426c-a354-3e9c9d9c4625.png" alt></p><p>Hive 语法的兼容并没有完全造出一套新的 SQL 引擎，而是复用了 Flink SQL 的很多核心流程和代码。我们抽象出了可插拔的 parser 层来支持和扩展不同的语法。Flink SQL 会经过 Flink Parser 转换成 Flink RelNode，再经过 Logical Plan 优化为 Physical Plan，最后转换为 Job Graph 提交执行。为了支持 Hive 语法兼容，我们引入了 Hive Parser 组件，来将 Hive SQL 转化成 Flink RelNode。这个过程中，复用了大部分 Hive 现有的 SQL 解析逻辑，保证语法层的兼容（均基于 Calcite）。之后 RelNode 复用同样的流程和代码转化成 LogicalPlan、Physical Plan、JobGraph，最后提交执行。</p><p><img src="https://user-images.githubusercontent.com/5378924/221926916-26c86af6-cfba-4712-90e8-f5ed9733b71f.png" alt></p><p>从架构上看，Hive 语法兼容并不复杂，但这是一个“魔鬼在细节”的工作。上图为部分 Flink1.16 版本里 Flink Hive 兼容相关的 issue，涉及 query 兼容、类型系统、语义、行为、DDL、DML、辅助查询命令等非常多语法功能。累计完成的 issue 数达近百个。</p><p>Flink1.16 版本将 Hive 兼容度从 85% 提升至 94.1%。兼容度测试主要依靠 Hive qtest 测试集，其中包含 12,000 多个测试 case，覆盖了 Hive 目前所有主流语法功能。没有兼容的一部分包括 ACID 功能（业界使用较少），如果除去 ACID 功能，兼容度已达 97%以上。</p><p><img src="https://user-images.githubusercontent.com/5378924/221926974-009f6dda-0ec4-4899-a281-0470b1c92bc9.png" alt></p><p>SQLGateway 是 Flink SQL 的 server 层组件，是单独的进程，对标 HiveServer2 组件。从 Flink 整体架构上看，SQLGateway 处于中间位置。</p><p>向下，封装了用户 API 的 Flink SQL 和 Hive SQL。不管是 Flink SQL 还是 Hive SQL，都使用 Flink 流批一体的 Runtime 来执行，可以运行在批模式，也可以运行在流模式。Flink 的资源也可以部署运行在 YARN、K8S、Flink standalone 集群上。</p><p>向上，SQLGateway 提供了可插拔协议层 Endpoint，目前提供了 HiveServer2 和 REST 两种协议实现。通过 HiveServer2 Endpoint，用户可以将 Hive 生态的很多工具和组件（Zeppelin、Superset、Beeline、DBeaver 等）连接到 SQL Gateway，提供流批统一的 SQL 服务并兼容 Hive SQL。通过 REST 协议可以使用 Postman、curl 命令或自己通过 Python、Java 编程来访问，提供完善和灵活的流计算服务。将来，Endpoint 能力也会继续扩展，比如可以提供更高性能的 gRPC 协议或兼容 PG 协议。</p><h2 id="Hive-SQL-迁移的实践"><a href="#Hive-SQL-迁移的实践" class="headerlink" title="Hive SQL 迁移的实践"></a>Hive SQL 迁移的实践</h2><p><img src="https://user-images.githubusercontent.com/5378924/221927225-597cea6a-159c-4c7a-85d5-3342d449b45a.png" alt></p><p>目前快手正在与 Flink 社区紧密合作，推进流批一体的落地。目前快手迁移 Hive SQL 作业到 Flink SQL 作业已经取得了初步的进展，已有上千个作业完成了迁移。快手的迁移主要策略为双跑平台，已有业务继续运行，双跑平台有智能路由组件，可以通过指定规则或 pattern 识别出作业，投递到 MapReduce、Spark 或 Flink 上运行。初期的运行较为谨慎，会通过白名单机制指定某些作业先运行在 Flink，观察其稳定性与性能，对比其结果一致性，后续逐步通过规则来放量。更多的实践经验与细节可以关注 Flink Forward Asia 2022 上分享的《Hive SQL 迁移到 Flink SQL 在快手的实践》。</p><h2 id="Hive-SQL-迁移的演示"><a href="#Hive-SQL-迁移的演示" class="headerlink" title="Hive SQL 迁移的演示"></a>Hive SQL 迁移的演示</h2><p><strong>Demo：Hive SQL 如何迁移到 Flink SQL</strong></p><p><img src="https://user-images.githubusercontent.com/5378924/221927826-91155d1e-f213-47aa-8965-b6738e7df2d4.png" alt></p><p>接下来演示一下 Hive SQL 如何迁移到 Flink SQL。我们已经搭建好一个 YARN 集群，以及 Hive 相关组件，包括 HiveServer2 的服务。我们使用 Zeppelin 做数据可视化和 SQL 查询。我们将演示 Hive SQL 迁移到 Flink SQL 只需改一行地址，Zeppelin 体验并无二致，SQL 也无需修改。完整的 Demo 视频请观看完整的演讲视频：<a href="https://www.bilibili.com/video/BV1BV4y1T7d4" target="_blank" rel="noopener">https://www.bilibili.com/video/BV1BV4y1T7d4</a></p><h2 id="未来规划"><a href="#未来规划" class="headerlink" title="未来规划"></a>未来规划</h2><p>未来，Flink 将在以下三个方面持续演进：</p><ul><li>第一，持续在 batch 上做更多尝试和投入，提升 batch 的稳定性和性能，目标是短期内能够追齐主流的批计算引擎。</li><li>第二，完善数据湖的分析，比如更高效的批式数据湖读写、查询优化下推、列存上的读写优化，Iceberg、Hudi 以及 Flink Table Store 的支持等。另外，也会提供丰富的湖上数据查询和管理功能，比如查询快照版本的能力、查询元数据、更丰富的 DML 语法（UPDATE、DELETE、MERGE INTO）以及管理湖上数据 CALL 命令等。</li><li>第三，Flink Batch 生态建设，包括进一步完善 Remote Shuffle Service、血缘管理等。</li></ul>]]></content>
    
    <summary type="html">
    
      本文整理自我在 9 月 24 日 Apache Flink Meetup 北京站的演讲。主要内容包括：Hive SQL 迁移的动机、挑战、实践、演示和未来规划。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Hive" scheme="http://wuchong.me/tags/Hive/"/>
    
  </entry>
  
  <entry>
    <title>Flink CDC 如何简化实时数据入湖入仓</title>
    <link href="http://wuchong.me/blog/2022/01/10/how-flink-cdc-simplifies-real-time-data-into-lakes-and-warehouses/"/>
    <id>http://wuchong.me/blog/2022/01/10/how-flink-cdc-simplifies-real-time-data-into-lakes-and-warehouses/</id>
    <published>2022-01-09T17:35:43.000Z</published>
    <updated>2023-02-28T17:55:59.768Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、Flink-CDC-介绍"><a href="#一、Flink-CDC-介绍" class="headerlink" title="一、Flink CDC 介绍"></a>一、Flink CDC 介绍</h2><p>从广义的概念上讲，能够捕获数据变更的技术, 我们都可以称为 CDC 技术。通常我们说的 CDC 技术是一种用于捕获数据库中数据变更的技术。CDC 技术应用场景也非常广泛，包括：</p><ul><li><strong>数据分发</strong>，将一个数据源分发给多个下游，常用于业务解耦、微服务。</li><li><strong>数据集成</strong>，将分散异构的数据源集成到数据仓库中，消除数据孤岛，便于后续的分析。</li><li><strong>数据迁移</strong>，常用于数据库备份、容灾等。</li></ul><p><img src="https://user-images.githubusercontent.com/5378924/221934643-e6cee2ef-e4ba-4cb1-b85a-2f8a2e7940bc.png" alt></p><p>Flink CDC 基于数据库日志的 Change Data Caputre 技术，实现了全量和增量的一体化读取能力，并借助 Flink 优秀的管道能力和丰富的上下游生态，支持捕获多种数据库的变更，并将这些变更实时同步到下游存储。</p><p>目前，Flink CDC 的上游已经支持了 MySQL、MariaDB、PG、Oracle、MongoDB 等丰富的数据源，对 Oceanbase、TiDB、SQLServer 等数据库的支持也已经在社区的规划中。 </p><p>Flink CDC 的下游则更加丰富，支持写入 Kafka、Pulsar 消息队列，也支持写入 Hudi、Iceberg 等数据湖，还支持写入各种数据仓库。</p><p>同时，通过 Flink SQL 原生支持的 Changelog 机制，可以让 CDC 数据的加工变得非常简单。用户可以通过 SQL 便能实现数据库全量和增量数据的清洗、打宽、聚合等操作，极大地降低了用户门槛。 此外， Flink DataStream API 支持用户编写代码实现自定义逻辑，给用户提供了深度定制业务的自由度。</p><p><img src="https://user-images.githubusercontent.com/5378924/221934717-2d4de44c-7eb3-47a0-ac7e-a95c2afba0d6.png" alt></p><p>Flink CDC 技术的核心是支持将表中的全量数据和增量数据做实时一致性的同步与加工，让用户可以方便地获每张表的实时一致性快照。比如一张表中有历史的全量业务数据，也有增量的业务数据在源源不断写入，更新。Flink CDC 会实时抓取增量的更新记录，实时提供与数据库中一致性的快照，如果是更新记录，会更新已有数据。如果是插入记录，则会追加到已有数据，整个过程中，Flink CDC 提供了一致性保障，即不重不丢。</p><p>那么 Flink CDC 技术能给现有的数据入仓入湖架构带来什么样的改变呢？我们可以先来看看传统数据入仓的架构。</p><p><img src="https://user-images.githubusercontent.com/5378924/221934773-0366ae9a-180c-41ad-956b-add6254772f2.png" alt></p><p>在早期的数据入仓架构中，一般会每天 SELECT 全量数据导入数仓后再做离线分析。这种架构有几个明显的缺点：</p><ul><li>每天查询全量的业务表会影响业务自身稳定性。</li><li>离线天级别调度的方式，天级别的产出时效性差。</li><li>基于查询方式，随着数据量的不断增长，对数据库的压力也会不断增加，架构性能瓶颈明显。</li></ul><p><img src="https://user-images.githubusercontent.com/5378924/221934809-1c4fdbe9-14e8-495e-9629-0f6d30d9208a.png" alt></p><p>到了数据仓库的 2.0 时代，数据入仓进化到了 Lambda 架构，增加了实时同步导入增量的链路。整体来说，Lambda 架构的扩展性更好，也不再影响业务的稳定性，但仍然存在一些问题：</p><ul><li>依赖离线的定时合并，只能做到小时级产出，延时还是较大；</li><li>全量和增量是割裂的两条链路； </li><li>整个架构链路长，需要维护的组件比较多，该架构的全量链路需要维护 DataX 或 Sqoop 组件，增量链路要维护 Canal 和 Kafka - 组件，同时还要维护全量和增量的定时合并链路。</li></ul><p><img src="https://user-images.githubusercontent.com/5378924/221934838-24bc9edf-2c68-4dfe-8b40-002d101b28cf.png" alt></p><p>对于传统数据入仓架构存在的问题，Flink CDC 的出现为数据入湖架构提供了一些新思路。借助 Flink CDC 技术的全增量一体化实时同步能力，结合数据湖提供的更新能力，整个架构变得非常简洁。我们可以直接使用 Flink CDC 读取 MySQL 的全量和增量数据，并直接写入和更新到 Hudi 中。</p><p>这种简洁的架构有着明显的优势。首先，不会影响业务稳定性。其次，提供分钟级产出，满足近实时业务的需求。同时，全量和增量的链路完成了统一，实现了一体化同步。最后，该架构的链路更短，需要维护的组件更少。</p><h2 id="二、Flink-CDC-的核心特性"><a href="#二、Flink-CDC-的核心特性" class="headerlink" title="二、Flink CDC 的核心特性"></a>二、Flink CDC 的核心特性</h2><p><img src="https://user-images.githubusercontent.com/5378924/221934889-6df96fc4-db4e-4ef9-b5f3-0587937929ac.png" alt></p><p>Flink CDC 的核心特性可以分成四个部分：</p><ul><li>一是通过增量快照读取算法，实现了无锁读取，并发读取，断点续传等功能。</li><li>二是设计上对入湖友好，提升了 CDC 数据入湖的稳定性。</li><li>三是支持异构数据源的融合，能方便地做 Streaming ETL的加工。</li><li>四是支持分库分表合并入湖。接下来我们会分别介绍下这几个特性。</li></ul><p><img src="https://user-images.githubusercontent.com/5378924/221934930-ab4365be-f221-49d6-84c5-42a543658e1f.png" alt></p><p>在 Flink CDC 1.x 版本时，MySQL CDC 存在三大痛点，影响了生产可用性。</p><ul><li>一是 MySQL CDC 需要通过全局锁去保证全量和增量数据的一致性，而 MySQL 的全局锁会影响线上业务。</li><li>二是只支持单并发读取，大表读取非常耗时。</li><li>三是在全量同步阶段，作业失败后只能重新同步，稳定性较差。针对这些问题，Flink CDC 社区提出了 “增量快照读取算法”，同时实现了无锁读取、并行读取、断点续传等能力，一并解决了上述痛点。</li></ul><p><img src="https://user-images.githubusercontent.com/5378924/221934963-fc0ee42f-8a06-425e-99f5-39b517dcb117.png" alt></p><p>简单来说，增量快照读取算法的核心思路就是在全量读取阶段把表分成一个个 chunk 进行并发读取，在进入增量阶段后只需要一个 task 进行单并发读取 binlog 日志，在全量和增量自动切换时，通过无锁算法保障一致性。这种设计在提高读取效率的同时，进一步节约了资源。实现了全增量一体化的数据同步。这也是流批一体道路上一个非常重要的落地。 </p><p><img src="https://user-images.githubusercontent.com/5378924/221935011-e02add2d-d3b9-4493-8bed-b40a597b16ab.png" alt></p><p>Flink CDC 是一个流式入湖友好的框架。在早期版本的 Flink CDC 设计中，没有考虑数据湖场景，全量阶段不支持 Checkpoint，全量数据会在一个 Checkpoint 中处理，这对依靠 Checkpoint 提交数据的数据湖很不友好。Flink CDC 2.0 设计之初考虑了数据湖场景，是一种流式入湖友好的设计。设计上将全量数据进行分片，Flink CDC 可以将 checkpoint 粒度从表粒度优化到 chunk 粒度，大大减少了数据湖写入时的 Buffer 使用，对数据湖写入更加友好。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935048-73e9bda1-03d3-4300-9137-5b74e0ce06c2.png" alt></p><p>Flink CDC 区别于其他数据集成框架的一个核心点，就是在于 Flink 提供的流批一体计算能力。这使得 Flink CDC 成为了一个完整的 ETL 工具，不仅仅拥有出色的 E 和 L 的能力，还拥有强大的 Transformation 能力。因此我们可以轻松实现基于异构数据源的数据湖构建。</p><p>在上图左侧的 SQL 中，我们可以将 MySQL 中的实时产品表、实时订单表和 PostgreSQL 中的实时物流信息表进行实时关联，即 Streaming Join，关联后的结果实时更新到 Hudi 中，非常轻松地完成异构数据源的数据湖构建。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935142-0b288405-9b50-416b-8b83-3644d23b1ebf.png" alt></p><p>在 OLTP 系统中，为了解决单表数据量大的问题，通常采用分库分表的方式将单个大表进行拆分以提高系统的吞吐量。但是为了方便数据分析，通常需要将分库分表拆分出的表在同步到数据仓库、数据湖时，再合并成一个大表。Flink CDC 可以轻松完成这个任务。</p><p>在上图左侧的 SQL 中，我们声明了一张 user_source 表去捕获所有 user 分库分表的数据，我们通过表的配置项 database-name、table-name 使用正则表达式来匹配这些表。并且，user_source 表也定义了两个 metadata 列来区分数据是来自哪个库和表。在 Hudi 表的声明中，我们将库名、表名和原表的主键声明成 Hudi 中的联合主键。在声明完两张表后，一条简单的 INSERT INTO 语句就可以将所有分库分表的数据合并写入 Hudi 的一张表中，完成基于分库分表的数据湖构建，方便后续在湖上的统一分析。</p><h2 id="三、Flink-CDC-的开源生态"><a href="#三、Flink-CDC-的开源生态" class="headerlink" title="三、Flink CDC 的开源生态"></a>三、Flink CDC 的开源生态</h2><p><img src="https://user-images.githubusercontent.com/5378924/221935195-71903539-60ea-4aba-bd2f-c67f7d5f72f5.png" alt></p><p>Flink CDC 是一个独立的开源项目，项目代码托管在 <a href="https://github.com/ververica/flink-cdc-connectors" target="_blank" rel="noopener">GitHub</a> 上。采取小步快跑的发布节奏，今年社区已经发布了 5 个版本。1.x 系列的三个版本推出了一些小功能；2.0 版本 MySQL CDC 支持了无锁读取、并发读取、断点续传等高级功能，commits 达到了 91 个，贡献者达到了 15 人；2.1 版本则支持了 Oracle、MongoDB 数据库，commits 达到了115个，贡献者达到了28人。社区的 commits 和 贡献者增长非常明显。</p><p>文档和帮助手册也是开源社区非常重要的一部分，为了更好地帮助用户，Flink CDC 社区推出了版本化的文档网站，如 <a href="https://ververica.github.io/flink-cdc-connectors/release-2.1/" target="_blank" rel="noopener">2.1 版本的文档</a> 。文档中还提供了很多快速入门的教程，用户只要有个 Docker 环境就能上手 Flink CDC。此外，还提供了 <a href="https://github.com/ververica/flink-cdc-connectors/wiki/FAQ(ZH" target="_blank" rel="noopener">FAQ 指导手册</a>)，快速解决用户遇到的常见问题。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935245-3e7cd218-6b7d-4dc9-a675-3af6f70dc93d.png" alt></p><p>在过去的 2021 年，Flink CDC 社区取得了迅速的发展，GitHub 的 PR 和 issue 相当活跃，GitHub Star 更是年度同比增长 330%。</p><h2 id="四、Flink-CDC-在阿里巴巴的实践和改进"><a href="#四、Flink-CDC-在阿里巴巴的实践和改进" class="headerlink" title="四、Flink CDC 在阿里巴巴的实践和改进"></a>四、Flink CDC 在阿里巴巴的实践和改进</h2><p>Flink CDC 入湖入仓在阿里巴巴也有大规模的实践和落地，过程中也遇到了一些痛点和挑战。我们会介绍下我们是如何改进和解决的。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935292-6f35e6d7-f16f-4f2d-a002-5384c5b09009.png" alt></p><p>我们先来看下 CDC 入湖遇到的一些痛点和挑战。这是某个用户原有的 CDC 数据入湖架构，分为两个链路：</p><ul><li>有一个全量同步作业做一次性的全量数据拉取；</li><li>还有一个增量作业通过 Canal 和处理引擎将 Binlog 数据准实时地同步到 Hudi 表中。</li></ul><p>这个架构虽然利用了 Hudi 的更新能力，无需周期性地调度全量合并任务，能做到分钟级延迟。但是全量和增量仍是割裂的两个作业，全量和增量的切换仍需要人工的介入，并且需要指定一个准确的增量启动位点，否则的话就会有丢失数据的风险。可以看到这种架构是流批割裂的，并不是一个统一的整体。刚刚雪尽也介绍了 Flink CDC 最大的一个优势之一就是全增量的自动切换，所以我们用 Flink CDC 替换了用户原有的入湖架构。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935344-0c55c457-efa2-43a9-89b9-8dfa03811f4d.png" alt></p><p>但是用户用了 Flink CDC 后，<strong>遇到的第一个痛点</strong>就是需要将 MySQL 的 DDL 手工映射成 Flink 的 DDL。手工映射表结构是比较繁琐的，尤其是当表和字段数非常多的时候。而且手工映射也容易出错，比如 说 MySQL 的 BIGINT UNSINGED，它不能映射成 Flink 的 BIGINT，而是要映射成 DECIMAL(20)。 如果系统能自动帮助用户自动去映射表结构就会简单安全很多。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935361-936fc8f9-5668-4829-9041-d6986d05bec8.png" alt></p><p>用户遇到的<strong>另一个痛点</strong>是表结构的变更导致入湖链路难以维护。例如用户有一张表，原先有 id 和 name 两列，突然增加了一列 Address。新增的这一列数据可能就无法同步到数据湖中，甚至导致入湖链路的挂掉，影响稳定性。除了加列的变更，还可能会有删列、类型变更等等。国外的 Fivetran 做过一个<a href="https://fivetran.com/blog/analyst-survey" target="_blank" rel="noopener">调研报告</a> ，发现 60% 的公司，schema 每个月都会变化，30% 每周都会变化。这说明基本每个公司都会面临 schema 变更带来的数据集成上的挑战。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935387-1b7f0ce4-cfb7-4c33-bee4-26534c2b8d6e.png" alt></p><p><strong>最后一个是整库入湖的挑战</strong>。因为用户主要使用 SQL，这就需要为每个表的数据同步链路定义一个 INSERT INTO 语句。有些用户的 MySQL 实例中甚至有上千张的业务表，用户就要写上千个 INSERT INTO 语句。更令人望而生却的是，每一个 INSERT INTO 任务都会创建至少一个数据库连接，读取一次 Binlog 数据。千表入湖的话就需要上千个连接，上千次的 Binlog 重复读取。这就会对 MySQL 和网络造成很大的压力。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935471-c4c1578f-738a-4e81-83c8-ceab33bfee82.png" alt></p><p>刚刚我们介绍了 CDC 数据入湖的很多痛点和挑战，我们可以站在用户的角度想一想，数据库入湖这个场景用户到底想要的是什么呢？我们可以先把中间的数据集成系统看成一个黑盒，用户会期望这个黑盒提供什么样的能力来简化入湖的工作呢？</p><p><img src="https://user-images.githubusercontent.com/5378924/221935553-7ba2fc2e-3ef5-4226-8dc1-8b315d353c90.png" alt></p><ul><li>首先，用户肯定想把数据库中全量和增量的数据都同步过去，这就需要这个系统具有全增量一体化、全增量自动切换的能力，而不是割裂的全量链路 + 增量链路。</li><li>其次，用户肯定不想为每个表去手动映射 schema，这就需要系统具有元信息自动发现的能力，省去用户在 Flink 中创建 DDL 的过程，甚至帮用户自动在 Hudi 中创建目标表。</li><li>另外，用户还希望源端表结构的变更也能自动同步过去，不管是加列减列和改列，还是加表减表和改表，都能够实时的自动的同步到目标端，从而不丢失任何在源端发生的新增数据，自动化地构建与源端数据库保持数据一致的 ODS 层。</li><li>最后，还需要有具备生产可用的整库同步能力，不能对源端造成太大压力，影响在线业务。</li></ul><p>这四个核心功能基本组成了用户理想中所期待的数据集成系统，而这一切如果只需要一行 SQL，一个Job就能完成的话，那就更完美了。我们把中间的这个系统称为 “全自动化数据集成”，因为它全自动地完成了数据库的入湖，解决了目前遇到的几个核心痛点。而且目前看来，Flink 是实现这一目标非常适合的引擎。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935585-7bb95c9a-5a06-4017-9fce-4b9ac6131c74.png" alt></p><p>所以我们花了很多精力，基于 Flink 去打造这个 “全自动化数据集成”。主要就是围绕刚刚说的这四点。</p><ul><li>首先 Flink CDC 已经具备了全增量自动切换的能力，这也是 Flink CDC 的亮点之一。</li><li>在元信息的自动发现上，可以通过 Flink 的 Catalog 接口无缝对接上，我们开发了 MySQL Catalog 来自动发现 MySQL 中的表和 schema，还开发了 Hudi Catalog 自动地去 Hudi 中创建目标表的元信息。</li><li>在表结构变更的自动同步方面，我们引入了一个 Schema Evolution 的内核，使得 Flink Job 无需依赖外部服务就能实时同步 schema 变更。</li><li>在整库同步方面，我们引入了 CDAS 语法，一行 SQL 语句就能完成整库同步作业的定义，并且引入了 source 合并的优化，减轻对源端数据库的压力。</li></ul><p><img src="https://user-images.githubusercontent.com/5378924/221935687-4685de73-c3ba-4263-a061-7425fe017144.png" alt></p><p>为了支持整库同步，我们还引入了 CDAS 和 CTAS 的数据同步语法。它的语法非常简单，CDAS 语法就是 create database as database，主要用于整库同步，像这里展示的这行语句就完成了从 MySQL 的 tpc_ds 库，整库同步至 Hudi 的 ods 库中。与之类似的，我们还有一个 CTAS 语法，可以方便的用来支持表级别的同步，还可以通过正则表达式指定库名和表名，来完成分库分表合并同步。像这里就完成了 MySQL 的 user 分库分表合并到了 Hudi 的 users 表中。CDAS CTAS 的语法，会自动地去目标端创建目标表，然后启动一个 Flink Job 自动同步全量 + 增量的数据，并且也会实时同步表结构变更。</p><p><img src="https://user-images.githubusercontent.com/5378924/221935727-9959b785-ea60-40e8-9db7-2e5bc78993b3.png" alt></p><p>之前提到千表入湖时，建立的数据库连接过多，Binlog 重复读取会造成源库的巨大压力。为了解决这个问题，我们引入了 source 合并的优化，我们会尝试合并同一作业中的 source，如果都是读的同一数据源，则会被合并成一个 source 节点，这时数据库只需要建立一个连接，binlog 也只需读取一次，实现了整库的读取，降低了对数据库的压力。</p><p>为了更直观地了解我们是如何简化数据入湖入仓的工作，我们还额外提供了一个 Demo 视频，感兴趣的朋友可以在 Flink Forward Asia 2021 大会上，观看《Flink CDC 如何简化实时数据入湖入仓》的分享。</p><h2 id="五、Flink-CDC-的未来规划"><a href="#五、Flink-CDC-的未来规划" class="headerlink" title="五、Flink CDC 的未来规划"></a>五、Flink CDC 的未来规划</h2><p><img src="https://user-images.githubusercontent.com/5378924/221935772-38974f81-1425-4489-a183-059098b6119f.png" alt></p><p>最后关于 Flink CDC 的未来主要有三个方面的规划。</p><ul><li>第一，我们会继续完善 CDAS 和 CTAS 的语法和接口，打磨 Schema Evolution 的内核，为开源做准备。</li><li>第二，我们会扩展更多的 CDC 数据源，包括 TiDB、OceanBase、SQLServer 这些都已经规划中了。</li><li>第三，我们会将目前的增量快照读取算法抽象成通用框架，使得有更多的数据库能通过简单几个接口就对接到这个框架上，具备全增量一体化的能力。</li></ul><p>也希望有更多的志同道合之士能加入到 Flink CDC 开源社区的建设和贡献中，一起打造新一代的数据集成框架！</p>]]></content>
    
    <summary type="html">
    
      本文整理自我在 Flink Forward Asia 2021 的分享，该分享以 5 个章节详细介绍如何使用 Flink CDC 来简化实时数据的入湖入仓, 文章的主要内容如下：Flink CDC 介绍、Flink CDC 的核心特性、Flink CDC 的开源生态、Flink CDC 在阿里巴巴的实践与改进、Flink CDC 的未来规划。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink CDC" scheme="http://wuchong.me/tags/Flink-CDC/"/>
    
  </entry>
  
  <entry>
    <title>基于 Flink SQL 构建流批一体的 ETL 数据集成</title>
    <link href="http://wuchong.me/blog/2021/02/25/build-stream-batch-unified-ETL-based-on-flink-sql/"/>
    <id>http://wuchong.me/blog/2021/02/25/build-stream-batch-unified-ETL-based-on-flink-sql/</id>
    <published>2021-02-24T17:17:05.000Z</published>
    <updated>2023-02-28T17:34:04.363Z</updated>
    
    <content type="html"><![CDATA[<h2 id="数据仓库与数据集成"><a href="#数据仓库与数据集成" class="headerlink" title="数据仓库与数据集成"></a>数据仓库与数据集成</h2><p>数据仓库是一个集成的（Integrated），面向主题的（Subject-Oriented），随时间变化的（Time-Variant），不可修改的（Nonvolatile）数据集合，用于支持管理决策。这是数据仓库之父 Bill Inmon 在 1990 年提出的数据仓库概念。该概念里最重要的一点就是“集成的”，其余特性都是一些方法论的东西。因为数据仓库首先要解决的问题，就是数据集成，就是将多个分散的、异构的数据源整合在一起，消除数据孤岛，便于后续的分析。这个不仅适用于传统的离线数仓，也同样适用于实时数仓，或者是现在火热的数据湖。首先要解决的就是数据集成的问题。如果说业务的数据都在一个数据库中，并且这个数据库还能提供非常高效的查询分析能力，那其实也用不着数据仓库和数据湖上场了。</p><p>数据集成就是我们常称作 ETL 的过程，分别是数据接入(Extract)、数据清洗转换打宽(Transformation)、以及数据的入仓入湖(Load)，分别对应三个英文单词的首字母，所以叫 ETL。ETL 的过程也是数仓搭建中最具工作量的环节。那么 Flink 是如何改善这个 ETL 的过程的呢？我们先来看看传统的数据仓库的架构。</p><p><img src="https://user-images.githubusercontent.com/5378924/221929207-2577a6e0-a50c-4bae-8bbd-9facb6acd05d.png" alt></p><p>传统的数据仓库，实时和离线数仓是比较割裂的两套链路，比如实时链路通过 Flume和 Canal 实时同步日志和数据库数据到 Kafka 中，然后在 Kafka 中做数据清理和打宽。离线链路通过 Flume 和 Sqoop 定期同步日志和数据库数据到 HDFS 和 Hive。然后在 Hive 里做数据清理和打宽。</p><p>这里我们主要关注的是数仓的前半段的构建，也就是到 ODS、DWD 层，我们把这一块看成是广义的 ETL 数据集成的范围。那么在这一块，传统的架构主要存在的问题就是这种割裂的数仓搭建这会造成很多重复工作，重复的资源消耗，并且实时、离线底层数据模型不一致，会导致数据一致性和质量难以保障。同时两个链路的数据是孤立的，数据没有实现打通和共享。</p><p>那么 Flink 能给这个架构带来什么改变呢？</p><p><img src="https://user-images.githubusercontent.com/5378924/221929291-2e53a311-da81-4831-b2a6-a602045d1c83.png" alt></p><p>基于 Flink SQL 我们现在可以方便地构建流批一体的 ETL 数据集成，与传统数仓架构的核心区别主要是这几点：</p><ol><li>Flink SQL 原生支持了 CDC 所以现在可以方便地同步数据库数据，不管是直连数据库，还是对接常见的 CDC工具。</li><li>Flink SQL 在最近的版本中持续强化了维表 join 的能力，不仅可以实时关联数据库中的维表数据，现在还能关联 Hive 和 Kafka 中的维表数据，能灵活满足不同工作负载和时效性的需求。</li><li>基于 Flink 强大的流式 ETL 的能力，我们可以统一在实时层做数据接入和数据转换，然后将明细层的数据回流到离线数仓中。</li><li>现在 Flink 流式写入 Hive，已经支持了自动合并小文件的功能，解决了小文件的痛苦。</li></ol><p>所以基于流批一体的架构，我们能获得的收益：</p><ul><li>统一了基础公共数据</li><li>保障了流批结果的一致性</li><li>提升了离线数仓的时效性</li><li>减少了组件和链路的维护成本</li></ul><p>接下来我们会针对这个架构中的各个部分,结合场景案例展开进行介绍，包括数据接入，数据入仓入湖，数据打宽。</p><h2 id="数据接入"><a href="#数据接入" class="headerlink" title="数据接入"></a>数据接入</h2><p>现在数据仓库典型的数据来源主要来自日志和数据库，日志接入现阶段已经非常成熟了，也有非常丰富的开源产品可供选择，包括 Flume，Filebeat，Logstash 等等都能很方便地采集日志到 Kafka 。这里我们就不作过多展开。</p><p>数据库接入会复杂很多，常见的几种 CDC 同步工具包括 Canal，Debezium，Maxwell。Flink 通过 CDC format 与这些同步工具做了很好的集成，可以直接消费这些同步工具产生的数据。同时 Flink 还推出了原生的 CDC connector，直连数据库，降低接入门槛，简化数据同步流程。</p><p><img src="https://user-images.githubusercontent.com/5378924/221932127-cf723bf0-4193-4290-8cdd-ab9b944c21d6.png" alt></p><p>我们先来看一个使用 CDC format 的例子。现在常见的方案是通过 Debezium 或者  Canal 去实时采集 MySQL 数据库的 binlog，并将行级的变更事件同步到 Kafka 中供 Flink 分析处理。在 Flink 推出 CDC format 之前，用户要去消费这种数据会非常麻烦，用户需要了解 CDC 工具的数据格式，将 before，after 等字段都声明出来，然后用 ROW_NUMBER 做个去重，来保证实时保留最后一行的语义。但这样使用成本很高，而且也不支持 DELETE 事件。</p><p>现在 Flink 支持了 CDC format，比如这里我们在 with 参数中可以直接指定 format = ‘debezium-json’，然后 schema 部分只需要填数据库中表的 schema 即可。Flink 能自动识别 Debezium 的 INSERT/UPDATE/DELETE 事件，并转成 Flink 内部的 INSERT/UPDATE/DELETE 消息。之后用户可以在该表上直接做聚合、join 等操作，就跟操作一个 MySQL 实时物化视图一样，非常方便。</p><p><img src="https://user-images.githubusercontent.com/5378924/221929849-6eca7025-1a11-4c61-919e-c011b0a2caed.png" alt></p><p>在 Flink 1.12 版本中，Flink 已经原生支持了大部分常见的 CDC format，比如 Canal json、Debezium json、Debezium avro、Maxwell 等等。同时 Flink 也开放了 CDC format 的接口，用户可以实现自己的 CDC format 插件来对接自己公司的同步工具。</p><p><img src="https://user-images.githubusercontent.com/5378924/221929945-8a2ba2aa-1c96-424d-a074-aeaf1450a36e.png" alt></p><p>除此之外，Flink 内部原生支持了 CDC 的语义，所以可以很自然地直接去读取  MySQL 的 binlog 数据并转成 Flink 内部的变更消息。所以我们推出了 MySQL CDC connector，你只需要在 with 参数中指定 connector=mysql-cdc，然后 select 这张表就能实时读取 MySQL 中的全量 +CDC 增量数据，无需部署其他组件和服务。你可以把 Flink 中定义的这张表理解成是 MySQL 的实时物化视图，所以在这张表上的聚合、join 等结果，跟实时在 MySQL 中运行出来的结果是一致的。相比于刚刚介绍的 Debezium，Canal 的架构，CDC connector 在使用上更加简单易用了，不用再去学习和维护额外组件，数据不需要经过 Kafka 落地，减少了端到端延迟。而且支持先读取全量数据，并无缝切换到 CDC 增量读取上，也就是我们说的是流批一体，流批融合的架构。</p><p><img src="https://user-images.githubusercontent.com/5378924/221930031-62bd1cf9-f442-4d24-b8f7-715ef677c955.png" alt></p><p>我们发现 MySQL CDC connector 非常受用户的欢迎，尤其是结合 OLAP 引擎，可以快速构建实时 OLAP 架构。实时 OLAP 架构的一个特点就是将数据库数据同步到  OLAP 中做即席查询，这样就无需离线数仓了。</p><p>以前是怎么做的呢？</p><p>之前用户一般先用 datax 做个全量同步，然后用 canal 同步实时增量到 Kafka，然后从 Kafka 同步到 OLAP，这种架构比较复杂，链路也很长。现在很多公司都在用 Flink+ClickHouse 来快速构建实时 OLAP 架构。我们只需要在 Flink 中定义一个 mysql-cdc source，一个 ClickHouse sink，然后提交一个 insert into query 就完成了从 MySQL 到 ClickHouse 的实时同步工作，非常方便。而且，ClickHouse 有一个痛点就是 join 比较慢，所以一般我们会把 MySQL 数据打成一张大的明细宽表数据，再写入 ClickHouse。这个在 Flink 中一个 join 操作就完成了。而在 Flink 提供 MySQL CDC connector 之前，要在全量+增量的实时同步过程中做 join 是非常麻烦的。</p><p><img src="https://user-images.githubusercontent.com/5378924/221930151-6363a1a0-4117-4717-a7b4-64091d414abc.png" alt></p><p>当然，这里我们也可以把 ClickHouse 替换成其他常见的 OLAP 引擎，比如阿里云的 Hologres。我们发现在阿里云上有很多的用户都采用了这套链路和架构，因为它可以省掉数据同步服务和消息中间件的成本，对于很多中小公司来说，在如今的疫情时代，控制成本是非常重要的。当然，这里也可以使用其他 OLAP 引擎，比如 TiDB。TiDB 官方也在最近发过一篇文章介绍这种 Flink+TiDB 的实时 OLAP架构。</p><h2 id="数据入仓湖"><a href="#数据入仓湖" class="headerlink" title="数据入仓湖"></a>数据入仓湖</h2><p>刚刚我们介绍了基于 Flink SQL 可以非常方便地做数据接入，也就是 ETL 的 Extract 的部分。接下来，我们介绍一下 Flink SQL 在数据入仓入湖方面的能力，也就是 Load 的部分。</p><p><img src="https://user-images.githubusercontent.com/5378924/221930400-6d8536e7-4acb-493a-84a8-948137009645.png" alt></p><p>我们回顾下刚刚的流批一体的架构图，其中最核心的部分就是 Kafka 数据的流式入仓，正是这一流程打通了实时和离线数仓，统一了数仓的基础公共数据，提升了离线数仓的时效性，所以我们针对这一块展开讲一讲。</p><p><img src="https://user-images.githubusercontent.com/5378924/221930510-2eb1c118-20a7-4405-8744-823b2909e129.png" alt></p><p>使用 Flink SQL 做流式数据入仓，非常的方便，而且 1.12 版本已经支持了小文件的自动合并，解决了小文件的痛点。可以看下右边这段代码，先在 Flink SQL 中使用  Hive dialect 创建一张 Hive 的结果表，然后通过 select from kafka 表 insert into Hive 表这样一个简单 query，就可以提交任务实时将 Kafka 数据流式写入 Hive。</p><p>如果要开启小文件合并，只需要在 Hive 表参数中加上 auto-compaction = true，那么在流式写入这张 Hive 表的时候就会自动做小文件的 compaction。小文件合并的原理，是 Flink 的 streaming sink 会起一个小拓扑，里面 temp writer 节点负责不断将收到的数据写入临时文件中，当收到 checkpoint 时，通知 compact coordinator 开始做小文件合并，compact coordinator 会将 compaction 任务分发给多个 compact operator 并发地去做小文件合并。当 compaction 完成的时候，再通知 partition committer 提交整个分区文件可见。整个过程利用了 Flink 自身的 checkpoint 机制完成 compaction 的自动化，无需起另外的 compaction 服务。这也是 Flink 流式入仓对比于其他入仓工具的一个核心优势。</p><p><img src="https://user-images.githubusercontent.com/5378924/221930624-eb933cc3-454a-4d5f-af14-e635c2856230.png" alt></p><p>除了流式入仓，Flink 现在也支持流式入湖。以 Iceberg 举例，基于 Iceberg 0.10，现在可以在 Flink SQL 里面直接 create 一个 Iceberg catalog，在 Iceberg catalog 下可以 create table 直接创建 Iceberg表。然后提交 insert into query 就可以将流式数据导入到 Iceberg 中。然后在 Flink 中可以用 batch 模式读取这张 Iceberg  表，做离线分析。不过 Iceberg 的小文件自动合并功能目前还没有发布，还在支持中。</p><p><img src="https://user-images.githubusercontent.com/5378924/221930696-59fe1228-421b-4745-871d-3a84fb0fa537.png" alt></p><p>刚刚介绍的是纯 append 数据流式入仓入湖的能力，接下来介绍 CDC 数据流式入仓入湖的能力。我们先介绍 CDC 数据入 Kafka 实时数仓。其实这个需求在实时数仓的搭建中是非常常见的，比如同步数据库 binlog 数据到 Kafka 中，又比如 join，聚合的结果是个更新流，用户想把这个更新流写到 Kafka 作为中间数据供下游消费。</p><p>这在以前做起来会非常的麻烦，在 Flink 1.12 版本中，Flink 引入了一个新的 connector ，叫做 upsert-kafka，原生地支持了 Kafka 作为一个高效的 CDC 流式存储。</p><p>为什么说是高效的，因为存储的形式是与 Kafka log compaction 机制高度集成的，Kafka 会对 compacted topic 数据做自动清理，且 Flink 读取清理后的数据，仍能保证语义的一致性。而且像 Canal, Debezium 会存储 before,op_type 等很多无用的元数据信息，upsert-kafka 只会存储数据本身的内容，节省大量的存储成本。使用上的话，只需要在 DDL 中声明 connector = upsert-kafka，并定义 PK 即可。</p><p>比如我们这里定义了 MySQL CDC 的直播间表，以及一个 upsert-kafka 的结果表，将直播间的数据库同步到 Kafka 中。那么写入 Kafka 的 INSERT 和 UPDATE 都是一个带 key 的普通数据，DELETE 是一个带 key 的 NULL 数据。Flink 读取这个 upsert-kafka 中的数据时，能自动识别出 INSERT/UPDATE/DELETE 消息，消费这张 upsert-kafka 表与消费 MySQL CDC 表的语义一致。并且当 Kafka 对 topic 数据做了 compaction 清理后，Flink 读取清理后的数据，仍能保证语义的一致性。</p><p><img src="https://user-images.githubusercontent.com/5378924/221930850-671b3aaf-61cf-4b25-b3d5-f54682096b35.png" alt></p><p>CDC 数据入 Hive 数仓会麻烦一些，因为 Hive 本身不支持 CDC 的语义，现在的一种常见方式是先将 CDC 数据以 changelog-json 格式流式写入到 HDFS。然后起个  batch 任务周期性地将 HDFS 上的 CDC 数据按照 op 类型分为 INSERT, UPDATE, DELETE 三张表，然后做个 batch merge。</p><h2 id="数据打宽"><a href="#数据打宽" class="headerlink" title="数据打宽"></a>数据打宽</h2><p>前面介绍了基于 Flink SQL 的 ETL 流程的 Extract 和 Load，接下来介绍 Transformation 中最常见的数据打宽操作。</p><p><img src="https://user-images.githubusercontent.com/5378924/221930958-4ae09da4-c7ca-42b7-a5dd-15b6fbc4c535.png" alt></p><p>数据打宽是数据集成中最为常见的业务加工场景，数据打宽最主要的手段就是 Join，Flink SQL 提供了丰富的 Join 支持，包括 Regular Join、Interval Join、Temporal Join。</p><p><img src="https://user-images.githubusercontent.com/5378924/221931029-6ce5b66d-e953-4595-8df8-8a1797562db9.png" alt></p><p>Regular Join 就是大家熟知的双流 Join，语法上就是普通的 JOIN 语法。图中案例是通过广告曝光流关联广告点击流将广告数据打宽，打宽后可以进一步计算广告费用。从图中可以看出，曝光流和点击流都会存入 join 节点的 state，join 算子通过关联曝光流和点击流的 state 实现数据打宽。Regular Join 的特点是，任意一侧流都会触发结果的更新，比如案例中的曝光流和点击流。同时 Regular Join 的语法与传统批 SQL 一致，用户学习门槛低。但需要注意的是，Regular join 通过 state 来存储双流已经到达的数据，state 默认永久保留，所以 Regular join 的一个问题是默认情况下 state 会持续增长，一般我们会结合 state TTL 使用。</p><p><img src="https://user-images.githubusercontent.com/5378924/221931103-0a7ce51f-e0f7-41f6-a755-91ea3e0d0a0d.png" alt></p><p>Interval Join 是一条流上需要有时间区间的 join，比如刚刚的广告计费案例中，它有一个非常典型的业务特点在里面，就是点击一般发生在曝光之后的 10 分钟内。因此相对于 Regular Join，我们其实只需要关联这10分钟内的曝光数据，所以 state 不用存储全量的曝光数据，它是在 Regular Join 之上的一种优化。要转成一个 Interval Join，需要在两个流上都定义时间属性字段（如图中的 click_time 和 show_time）。并在 join 条件中定义左右流的时间区间，比如这里我们增加了一个条件：点击时间需要大于等于曝光时间，同时小于等于曝光后 10 分钟。与 Regular Join 相同， Interval Join 任意一条流都会触发结果更新，但相比 Regular Join，Interval Join 最大的优点是 state 可以自动清理，根据时间区间保留数据，state 占用大幅减少。Interval Join 适用于业务有明确的时间区间，比如曝光流关联点击流，点击流关联下单流，下单流关联成交流。</p><p><img src="https://user-images.githubusercontent.com/5378924/221931131-22367343-ca0a-4f49-84c5-944d96ba19f6.png" alt></p><p>Temporal join (时态表关联) 是最常用的数据打宽方式，它常用来做我们熟知的维表  Join。在语法上，它需要一个显式的 FOR SYSTEM_TIME AS OF 语句。它与 Regular Join 以及 Interval Join 最大的区别就是，维度数据的变化不会触发结果更新，所以主流关联上的维度数据不会再改变。Flink 支持非常丰富的 Temporal join 功能，包括关联 lookup DB，关联 changelog，关联 Hive 表。在以前，大家熟知的维表 join 一般都是关联一个可以查询的数据库，因为维度数据在数据库里面，但实际上维度数据可能有多种物理形态，比如 binlog 形式，或者定期同步到 Hive 中变成了 Hive 分区表的形式。在 Flink 1.12 中，现在已经支持关联这两种新的维表形态。 </p><p><img src="https://user-images.githubusercontent.com/5378924/221931164-1563c63a-feba-4d1f-8443-a5f3ddbfec2a.png" alt></p><p>Temporal Join Lookup DB 是最常见的维表 Join 方式，比如在用户点击流关联用户画像的案例中，用户点击流在 Kafka 中，用户实时画像存放在 HBase 数据库中，每个点击事件通过查询并关联 HBase 中的用户实时画像完成数据打宽。Temporal Join Lookup DB 的特点是，维表的更新不会触发结果的更新，维度数据存放在数据库中，适用于实时性要求较高的场景，使用时我们一般会开启 Async IO 和内存 cache 提升查询效率。</p><p><img src="https://user-images.githubusercontent.com/5378924/221931207-4ce616d5-3044-448c-89d8-03e6de56b53d.png" alt></p><p>在介绍 Temporal Join Changelog 前，我们再看一个 Lookup DB 的例子，这是一个直播互动数据关联直播间维度的案例。这个案例中直播互动数据（比如点赞、评论）存放在 Kafka 中，直播间实时的维度数据（比如主播、直播间标题）存放在 MySQL 中，直播互动的数据量是非常大的，为了加速访问，常用的方案是加个高速缓存，比如把直播间的维度数据通过 CDC 同步，再存入 Redis 中，再做维表关联。这种方案的问题是，直播的业务数据比较特殊，直播间的创建和直播互动数据基本是同时产生的，因此互动数据可能早早地到达了 Kafka 被 Flink 消费，但是直播间的创建消息经过了 Canal, Kafka，Redis, 这个链路比较长，数据延迟比较大，可能导致互动数据查询 Redis 时，直播间数据还未同步完成，导致关联不上直播间数据，造成下游统计分析的偏差。</p><p><img src="https://user-images.githubusercontent.com/5378924/221931228-eeb97e82-ba0f-4efe-b80f-137ae49d4479.png" alt></p><p>针对这类场景，Flink 1.12  支持了 Temporal Join Changelog，通过从 changelog在 Flink state 中物化出维表来实现维表关联。刚刚的场景有了更简洁的解决方案，我们可以通过 Flink CDC connector 把直播间数据库表的 changelog 同步到 Kafka 中，注意我们看下右边这段 SQL，我们用了 upsert-kafka connector 来将 MySQL binlog 写入了 Kafka，也就是 Kafka 中存放了直播间变更数据的 upsert 流。然后我们将互动数据 temporal join 这个直播间 upsert 流，便实现了直播数据打宽的功能。</p><p>注意我们这里 FOR SYSTEM_TIME AS OF 不是跟一个 processing time，而是左流的 event time，它的含义是去关联这个 event time 时刻的直播间数据，同时我们在直播间 upsert 流上也定义了 watermark，所以 temporal join changelog 在执行上会做 watermark 等待和对齐，保证关联上精确版本的结果，从而解决先前方案中关联不上的问题。</p><p><img src="https://user-images.githubusercontent.com/5378924/221931261-f3301383-7fd2-428e-8307-055a1b9d1c84.png" alt></p><p>我们详细解释下 temporal join changelog 的过程，左流是互动流数据，右流是直播间 changelog。直播间 changelog 会物化到右流的维表 state 中，state 相当于一个多版本的数据库镜像， 主流互动数据会暂时缓存在左流的 state 中，等到 watermark 到达对齐后再去查维表 state 中的数据。比如现在互动流和直播流的 watermark 都到了10:01分，互动流的这条 10：01 分评论数据就会去查询维表 state，并关联上 103 房间的信息。当 10：05 这条评论数据到来时，它不会马上输出，不然就会关联上空的房间信息。它会一直等待，等到左右两流的 watermark 都到 10：05 后，才会去关联维表 state 中的数据并输出。这个时候，它能关联上准确的 104 房间信息。</p><p>总结下，Temporal Join Changelog 的特点是实时性高，因为是按照 event time 做的版本关联，所以能关联上精确版本的信息，且维表会做 watermark 对齐等待，使得用户可以通过 watermark 控制迟到的维表数。Temporal Join Changelog 中的维表数据都是存放在 temporal join 节点的 state 中，读取非常高效，就像是一个本地的 Redis 一样，用户不再需要维护额外的 Redis 组件。 </p><p><img src="https://user-images.githubusercontent.com/5378924/221931378-2d7cafa3-410a-48ac-9807-49d865898f35.png" alt></p><p>在数仓场景中，Hive 的使用是非常广泛的，Flink 与 Hive 的集成非常友好，现在已经支持 Temporal Join Hive 分区表和非分区表。我们举个典型的关联 Hive 分区表的案例：订单流关联店铺数据。店铺数据一般是变化比较缓慢的，所以业务方一般会按天全量同步店铺表到 Hive 分区中，每天会产生一个新分区，每个分区是当天全量的店铺数据。</p><p>为了关联这种 Hive 数据，只需我们在创建 Hive 分区表时指定右侧这两个红圈中的参数，便能实现自动关联 Hive 最新分区功能，partition.include = latestb 表示只读取 Hive 最新分区，partition-name 表示选择最新分区时按分区名的字母序排序。到 10 月 3 号的时候，Hive 中已经产生了 10 月 2 号的新分区, Flink 监控到新分区后，就会重新加载10月2号的数据到 cache 中并替换掉10月1号的数据作为最新的维表。之后的订单流数据关联上的都是 cache 10 月 2 号分区的数据。Temporal join Hive 的特点是可以自动关联 Hive 最新分区，适用于维表缓慢更新，高吞吐的业务场景。</p><p><img src="https://user-images.githubusercontent.com/5378924/221931408-0c40e80f-ddc9-40d6-8292-a54a71d4151c.png" alt></p><p>总结一下我们刚刚介绍的几种在数据打宽中使用的 join：</p><ul><li>Regular Join 的实效性非常高，吞吐一般，因为 state 会保留所有到达的数据，适用于双流关联场景；</li><li>Interval Jon 的时效性非常好，吞吐较好，因为 state 只保留时间区间内的数据，适用于有业务时间区间的双流关联场景；</li><li>Temporal Join Lookup DB 的时效性比较好，吞吐较差，因为每条数据都需要查询外部系统，会有 IO 开销，适用于维表在数据库中的场景；</li><li>Temporal Join Changelog 的时效性很好，吞吐也比较好，因为它没有 IO 开销，适用于需要维表等待，或者关联准确版本的场景；</li><li>Temporal Join Hive 的时效性一般，但吞吐非常好，因为维表的数据存放在cache 中，适用于维表缓慢更新的场景，高吞吐的场景。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><img src="https://user-images.githubusercontent.com/5378924/221931462-8a65a3e1-1b63-4b3a-b227-de657649a11f.png" alt></p><p>最后我们来总结下 Flink 在 ETL 数据集成上的能力。这是目前 Flink 数据集成的能力矩阵，我们将现有的外部存储系统分为了关系型数据库、KV 数据库、消息队列、数据湖、数据仓库 5 种类型，可以从图中看出 Flink 有非常丰富的生态，并且对每种存储引擎都有非常强大的集成能力。</p><p>横向上我们定义了 6 种能力，分别是：</p><ul><li>3 种数据接入能力：全量读取、流式读取、CDC 流式读取。</li><li>1 种数据打宽能力：维度关联。</li><li>2 种入仓/入湖能力：流式写入、CDC 写入。</li></ul><p>可以看到 Flink 对各个系统的数据接入能力、维度打宽能力、入仓/入湖能力都已经非常完善了。在 CDC 流式读取上，Flink 已经支持了主流的数据库和 Kafka 消息队列。在数据湖方向，Flink 对 Iceberg 的流式读取和 CDC 写入的功能也即将在接下来的 Iceberg 版本中发布。从这个能力矩阵可以看出，Flink 的数据集成能力是非常全面的。</p><p>在未来的版本中，我们也将持续优化 Flink 数据集成的能力，扩展上下游生态。也非常欢迎大家使用和反馈。</p>]]></content>
    
    <summary type="html">
    
      本文整理自我在 Flink Forward Asia 2020 的分享，该分享以 4 个章节来详细介绍如何利用 Flink SQL 构建流批一体的 ETL 数据集成, 文章的主要内容包括：数据仓库与数据集成、数据接入(E)、数据入仓/湖(L)、数据打宽(T)。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="ETL" scheme="http://wuchong.me/tags/ETL/"/>
    
      <category term="数据集成" scheme="http://wuchong.me/tags/%E6%95%B0%E6%8D%AE%E9%9B%86%E6%88%90/"/>
    
  </entry>
  
  <entry>
    <title>Nexmark: 如何设计一个流计算基准测试？</title>
    <link href="http://wuchong.me/blog/2020/09/16/nexmark-how-to-design-a-benchmark-for-stream-processing/"/>
    <id>http://wuchong.me/blog/2020/09/16/nexmark-how-to-design-a-benchmark-for-stream-processing/</id>
    <published>2020-09-16T15:26:59.000Z</published>
    <updated>2023-03-19T15:45:15.771Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h2><p>随着数据时效性对企业的精细化运营越来越重要，“实时即未来”、“实时数仓”、“数据湖” 成为了近几年炙手可热的词。流计算领域的格局也在这几年发生了巨大的变化，Apache Flink 在流批一体的方向上不断深耕，Apache Spark 的近实时处理有着一定的受众，Apache Kafka 也有了 ksqlDB 高调地进军流计算，而 Apache Storm 却开始逐渐地退出历史的舞台。</p><p>每一种引擎有其优势的地方，如何选择适合自己业务的流计算引擎成了一个由来已久的话题。除了比较各个引擎提供的不同的功能矩阵之外，性能是一个无法绕开的评估因素。基准测试（benchmark）就是用来评估系统性能的一个重要和常见的过程。</p><p>本文将探讨流计算基准测试设计上的难点，分享我们是如何设计一个流计算基准测试框架 —— <a href="https://github.com/nexmark/nexmark" target="_blank" rel="noopener">Nexmark</a>，以及将来的规划。</p><h2 id="二、现有流计算基准测试的问题"><a href="#二、现有流计算基准测试的问题" class="headerlink" title="二、现有流计算基准测试的问题"></a>二、现有流计算基准测试的问题</h2><p>目前在流计算领域中，还没有一个行业标准的基准测试。目前业界较为人知的流计算 benchmark 是五年前雅虎 Storm 团队发布的 <a href="https://github.com/yahoo/streaming-benchmarks" target="_blank" rel="noopener">Yahoo Streaming Benchmarks</a>。雅虎的原意是因为业界缺少反映真实场景的 benchmark，模拟了一个简单的广告场景来比较各个流计算框架，后来被广泛引用。具体场景是从 Kafka 消费的广告的点击流，关联 Redis 中的广告所属的 campaign 信息，然后做时间窗口聚合计数。</p><p>然而，正是因为雅虎团队太过于追求还原真实的生产环境，导致这些外部系统服务（Kafka, Redis）成为了作业的瓶颈。<a href="https://www.ververica.com/blog/extending-the-yahoo-streaming-benchmark" target="_blank" rel="noopener">Ververica 曾在这篇文章</a>中做过一个扩展实验，将数据源从 Kafka 替换成了一个内置的 datagen source，性能提升了 37 倍！由此可见，引入的 Kafka 组件导致了无法准确反映引擎真实的性能。更重要的一个问题是，Yahoo Benchmark 只包含一个非常简单的，类似 “Word Count” 的作业，它无法全面地反映当今复杂的流计算系统和业务。试想，谁会用一个简单的 “Word Count” 去衡量比较各个数据库之间的性能差异呢？正是这些原因使得 Yahoo Benchmark 无法成为一个行业标准的基准测试。这也正是我们想要解决的问题。</p><p>因此，我们认为一个行业标准的基准测试应该具备以下几个特点：</p><p><strong>可复现性</strong></p><p>可复现性是使得 benchmark 被信任的一个重要条件。许多 benchmark 的结果是难以重现的。有的是因为只摆了个 benchmark 结果图，用于生成这些结果的代码并没有公开。有的是因为用于 benchmark 的硬件不容易被别人获取到。有的是因为 benchmark 依赖的服务太多，致使测试结果不稳定。</p><p><strong>能代表和覆盖行业真实的业务场景（ query 量）</strong></p><p>例如数据库领域非常著名的 TPC-H、TPC-DS 涵盖了大量的 query 集合，来捕获查询引擎之间细微的差别。而且这些 query 集合都立于真实业务场景之上（商品零售行业），数据规模大，因此也很受一些大数据系统的青睐。</p><p><strong>能调整作业的负载（数据量、数据分布）</strong></p><p>在大数据领域，不同的数据规模对于引擎来说可能会是完全不同的事情。例如 Yahoo Benchmark 中使用的 campaign id 只有 100 个，使得状态非常小，内存都可以装的下。这样使得同步 IO 和 checkpoint 等的影响可以忽略不计。而真实的场景往往要面对大状态，面临的挑战要复杂困难的多。像 TPC-DS 的数据生成工具会提供 scalar factor 的参数来控制数据量。其次在数据分布上最好也能贴近真实世界的数据，如有数据倾斜，及调整倾斜比例。从而能全面、综合地反映业务场景和引擎之间地差异。</p><p><strong>有统一的性能衡量指标和采集汇总工具</strong></p><p>基准测试的性能指标的定义需要清晰、一致，且能适用于各种计算引擎。然而流计算的性能指标要比传统批处理的更难定义、更难采集。是流计算 benchmark 最具挑战性的一个问题，这也会在下文展开描述。</p><p>我们也研究了很多其他的流计算相关的基准测试，包括：StreamBench、HiBench、BigDataBench，但是它们都在上述几个基本面有所欠缺。基准测试的行业标杆无疑是 TPC 发布的一系列 benchmark，如 TPC-H，TPC-DS。然而这些 benchmark 是面向传统数据库、传统数仓而设计的，并不适用于今天的流计算系统。例如 benchmark 中没有考虑事件时间、数据的乱序、窗口等流计算中常见的场景。因此我们不得不考虑重新设计并开源一个流计算基准测试框架——Nexmark。</p><p>地址：<a href="https://github.com/nexmark/nexmark" target="_blank" rel="noopener">https://github.com/nexmark/nexmark</a></p><h2 id="三、Nexmark-基准测试框架的设计"><a href="#三、Nexmark-基准测试框架的设计" class="headerlink" title="三、Nexmark 基准测试框架的设计"></a>三、Nexmark 基准测试框架的设计</h2><p>为了提供一个满足以上几个基本面的流计算基准测试，我们设计和开发了 Nexmark 基准测试框架，并努力让其成为流计算领域的标准 benchmark 。</p><p>Nexmark 基准测试框架来源于 <a href="https://web.archive.org/web/20100620010601/http://datalab.cs.pdx.edu/niagaraST/NEXMark/" target="_blank" rel="noopener">NEXMark 研究论文</a>，以及 <a href="https://beam.apache.org/documentation/sdks/java/testing/nexmark/" target="_blank" rel="noopener">Apache Beam Nexmark Suite</a>，并在其之上进行了扩展和完善。Nexmark 基准测试框架不依赖任何第三方服务，只需要部署好引擎和 Nexmark，通过脚本 <code>nexmark/bin/run_query.sh all</code> 即可等待并获得所有 query 下的 benchmark 结果。下面我们将探讨 Nexmark 基准测试在设计上的一些决策。</p><h3 id="1-移除外部-source、sink-依赖"><a href="#1-移除外部-source、sink-依赖" class="headerlink" title="1. 移除外部 source、sink 依赖"></a>1. 移除外部 source、sink 依赖</h3><p>如上所述，Yahoo Benchmark 使用了 Kafka 数据源，却使得最终结果无法准确反映引擎的真实性能。此外，我们还发现，在 benchmark 快慢流双流 JOIN 的场景时，如果使用了 Kafka 数据源，慢流会超前消费（快流易被反压），导致 JOIN 节点的状态会缓存大量超前的数据。这其实不能反映真实的场景，因为在真实的场景下，慢流是无法被超前消费的（数据还未产生）。所以我们在 Nexmark 中使用了 datagen source，数据直接在内存中生成，数据不落地，直接向下游节点发送。多个事件流都由单一的数据生成器生成，所以当快流被反压时，也能抑制慢流的生成，较好地反映了真实场景。</p><p>与之类似的，我们也移除了外部 sink 的依赖，不再输出到 Kafka/Redis，而是输出到一个空 sink 中，即 sink 会丢弃收到的所有数据。</p><p>通过这种方式，我们保证了瓶颈只会在引擎自身，从而能精确地测量出引擎之间细微的差异。</p><h3 id="2-Metrics"><a href="#2-Metrics" class="headerlink" title="2. Metrics"></a>2. Metrics</h3><p>批处理系统 benchmark 的 metric 通常采用总体耗时来衡量。然而流计算系统处理的数据是源源不断的，无法统计 query 耗时。因此，我们提出三个主要的 metric：吞吐、延迟、CPU。Nexmark 测试框架会自动帮我们采集 metric，并做汇总，不需要部署任何第三方的 metric 服务。</p><p><strong>吞吐</strong></p><p>吞吐（throughput）也常被称作 TPS，描述流计算系统每秒能处理多少条数据。由于我们有多个事件流，所有事件流都由一个数据生成器生成，为了统一观测角度，我们采用数据生成器的 TPS，而非单一事件流的 TPS。我们将一个 query 能达到的最大吞吐，作为其吞吐指标。例如，针对 Flink 引擎，我们通过 Flink REST API 暴露的 <code>&lt;source_operator_name&gt;.numRecordsOutPerSecond</code> metric 来获取当前吞吐量。</p><p><strong>延迟</strong></p><p>延迟（Latency）描述了从数据进入流计算系统，到它的结果被输出的时间间隔。对于窗口聚合，Yahoo Benchmark 中使用 <code>output_system_time - window_end</code>作为延迟指标，这其实并没有考虑数据在窗口输出前的等待时间，这种计算结果也会极大地受到反压的影响，所以其计算结果是不准确的。一种更准确的计算方式应为 <code>output_system_time - max(ingest_time)</code>。然而在非窗口聚合，或双流 JOIN 中，延迟又会有不同的计算方式。</p><p>所以延迟的定义和采集在流计算系统中有很多现实存在的问题，需要根据具体 query 具体分析，这在<a href="https://arxiv.org/pdf/1802.08496.pdf" target="_blank" rel="noopener">《Benchmarking Distributed Stream Data Processing Systems》</a>中有详细的讨论，这也是我们目前还未在 Nexmark 中实现延迟 metric 的原因。</p><p><strong>CPU</strong></p><p>资源使用率是很多流计算 benchmark 中忽视的一个指标。由于在真实生产环境，我们并不会限制流计算引擎所能使用的核数，从而给系统更大的弹性。所以我们引入了 CPU 使用率，作为辅助指标，即作业一共消耗了多少核。通过<code>吞吐/cores</code>，可以计算出平均每个核对于吞吐的贡献。对于进程的 CPU 使用率的采集，我们没有使用 JVM CPU load，而是借鉴了 YARN 中的实现，通过采样 <code>/proc/&lt;pid&gt;/stat</code> 并计算获得，该方式可以获得较为真实的进程 CPU 使用率。因此我们的 Nexmark 测试框架需要在测试开始前，先在每台机器上部署 CPU 采集进程。</p><h3 id="3-Query-与-Schema"><a href="#3-Query-与-Schema" class="headerlink" title="3. Query 与 Schema"></a>3. Query 与 Schema</h3><p>Nexmark 的业务模型基于一个真实的在线拍卖系统。所有的 query 都基于相同的三个数据流，三个数据流会有一个数据生成器生成，来控制他们之间的比例、数据偏斜、关联关系等等。这三个数据流分别是：</p><ul><li>用户（Person）：代表一个提交拍卖，或参与竞标的用户。</li><li>拍卖（Auction）：代表一个拍卖品。</li><li>竞标（Bid）：代表一个对拍卖品的出价。</li></ul><p>我们一共定义了 16 个 query，所有的 query 都使用 ANSI SQL 标准语法。基于 SQL ，我们可以更容易地扩展 query 测试集，支持更多的引擎。然而，由于 Spark 在流计算功能上的限制，大部分的 query 都无法通过 Structured Streaming 来实现。因此我们目前只支持测试 Flink SQL 引擎。</p><p><img src="https://user-images.githubusercontent.com/5378924/226186674-604f1312-3fc6-4efa-ba8e-70b22481bc52.png" alt></p><h3 id="4-作业负载的配置化"><a href="#4-作业负载的配置化" class="headerlink" title="4. 作业负载的配置化"></a>4. 作业负载的配置化</h3><p>我们也支持配置调整作业的负载，包括数据生成器的吞吐量以及吞吐曲线、各个数据流之间的数据量比例、每个数据流的数据平均大小以及数据倾斜比例等等。具体的可以参考 Source DDL 参数。</p><h2 id="四、实验结果"><a href="#四、实验结果" class="headerlink" title="四、实验结果"></a>四、实验结果</h2><p>我们在阿里云的三台机器上进行了 Nexmark 针对 Flink 的基准测试。每台机器均为 ecs.i2g.2xlarge 规格，配有 Xeon 2.5 GHz CPU (8 vCores) 以及 32 GB 内存，800 GB SSD 本地磁盘。机器之间的带宽为 2 Gbps。</p><p>测试了 flink-1.11 版本，我们在这 3 台机器上部署了 Flink standalone 集群，由 1 个 JobManager，8 个 TaskManager （每个只有 1 slot）组成，都是 4 GB内存。集群默认并行度为 8。开启 checkpoint 以及 exactly once 模式，checkpoint 间隔 3 分钟。使用 RocksDB 状态后端。测试发现，对于有状态的 query，每次 checkpoint 的大小在 GB 级以上，所以有效地测试的大状态的场景。</p><p>Datagen source 保持 1000 万每秒的速率生成数据，三个数据流的数据比例分别是 Bid: 92%，Auction: 6%，Person: 2%。每个 query 都先运行 3 分钟热身，之后 3 分钟采集性能指标。</p><p>运行 <code>nexmark/bin/run_query.sh all</code> 后，打印测试结果如下：</p><p><img src="https://user-images.githubusercontent.com/5378924/226186721-3787bd1c-2dce-42a6-972c-66006babce50.png" alt></p><h2 id="五、总结"><a href="#五、总结" class="headerlink" title="五、总结"></a>五、总结</h2><p>我们开发和设计 Nexmark 的初衷是为了推出一套标准的流计算 benchmark 测试集，以及测试流程。虽然目前仅支持了 Flink 引擎，但在当前也具有一定的意义，例如：</p><ul><li>推动流计算 benchmark 的发展和标准化。</li><li>作为 Flink 引擎版本迭代之间的性能测试工具，甚至是日常回归工具，及时发现性能回退的问题。</li><li>在开发 Flink 性能优化的功能时，可以用来验证性能优化的效果。</li><li>部分公司可能会有 Flink 的内部版本，可以用作内部版本与开源版本之间的性能对比工具。</li></ul><p>当然，我们也计划持续改进和完善 Nexmark 测试框架，例如支持 Latency metric，支持更多的引擎，如 Spark Structured Streaming, Spark Streaming, ksqlDB, Flink DataStream 等等。也欢迎有志之士一起加入贡献和扩展。</p>]]></content>
    
    <summary type="html">
    
      如何选择适合自己业务的流计算引擎？除了比较各自的功能矩阵外，基准测试（benchmark）便是用来评估系统性能的一个重要和常见的方法。然而在流计算领域，目前还没有一个行业标准的基准测试。本文将探讨流计算基准测试设计上的难点，分享我们是如何设计一个流计算基准测试框架的 —— Nexmark，以及将来的规划。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="nexmark" scheme="http://wuchong.me/tags/nexmark/"/>
    
  </entry>
  
  <entry>
    <title>Demo：基于 Flink SQL 构建流式应用</title>
    <link href="http://wuchong.me/blog/2020/02/25/demo-building-real-time-application-with-flink-sql/"/>
    <id>http://wuchong.me/blog/2020/02/25/demo-building-real-time-application-with-flink-sql/</id>
    <published>2020-02-25T09:15:55.000Z</published>
    <updated>2022-08-03T06:46:44.501Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>上周四在 Flink 中文社区钉钉群中直播分享了《Demo：基于 Flink SQL 构建流式应用》，直播内容偏向实战演示。这篇文章是对直播内容的一个总结，并且改善了部分内容，比如除 Flink 外其他组件全部采用 Docker Compose 安装，简化准备流程。读者也可以结合视频和本文一起学习。完整分享可以观看视频回顾：<a href="https://www.bilibili.com/video/av90560012" target="_blank" rel="noopener">https://www.bilibili.com/video/av90560012</a></p></blockquote><p>Flink 1.10.0 于近期刚发布，释放了许多令人激动的新特性。尤其是 Flink SQL 模块，发展速度非常快，因此本文特意从实践的角度出发，带领大家一起探索使用 Flink SQL 如何快速构建流式应用。</p><p>本文将基于 Kafka, MySQL, Elasticsearch, Kibana，使用 Flink SQL 构建一个电商用户行为的实时分析应用。本文所有的实战演练都将在 Flink SQL CLI 上执行，全程只涉及 SQL 纯文本，无需一行 Java/Scala 代码，无需安装 IDE。本实战演练的最终效果图：</p><p><img src="https://img.alicdn.com/tfs/TB1xc2ewlr0gK0jSZFnXXbRRXXa-3104-1978.png" alt></p><a id="more"></a><h2 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h2><p>一台装有 Docker 和 Java8 的 Linux 或 MacOS 计算机。</p><h3 id="使用-Docker-Compose-启动容器"><a href="#使用-Docker-Compose-启动容器" class="headerlink" title="使用 Docker Compose 启动容器"></a>使用 Docker Compose 启动容器</h3><p>本实战演示所依赖的组件全都编排到了容器中，因此可以通过 <code>docker-compose</code> 一键启动。你可以通过 <code>wget</code> 命令自动下载该 <code>docker-compose.yml</code> 文件，也可以手动下载。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">mkdir flink-demo; <span class="built_in">cd</span> flink-demo;</span><br><span class="line">wget https://raw.githubusercontent.com/wuchong/flink-sql-demo/master/docker-compose.yml</span><br></pre></td></tr></table></figure><p>该 Docker Compose 中包含的容器有：</p><ul><li><strong>DataGen:</strong> 数据生成器。容器启动后会自动开始生成用户行为数据，并发送到 Kafka 集群中。默认每秒生成 1000 条数据，持续生成约 3 小时。也可以更改 <code>docker-compose.yml</code> 中 datagen 的 <code>speedup</code> 参数来调整生成速率（重启 docker compose 才能生效）。</li><li><strong>MySQL:</strong> 集成了 MySQL 5.7 ，以及预先创建好了类目表（<code>category</code>），预先填入了子类目与顶级类目的映射关系，后续作为维表使用。</li><li><strong>Kafka:</strong> 主要用作数据源。DataGen 组件会自动将数据灌入这个容器中。</li><li><strong>Zookeeper:</strong> Kafka 容器依赖。</li><li><strong>Elasticsearch:</strong> 主要存储 Flink SQL 产出的数据。</li><li><strong>Kibana:</strong> 可视化 Elasticsearch 中的数据。</li></ul><p>在启动容器前，建议修改 Docker 的配置，将资源调整到 4GB 以及 4核。启动所有的容器，只需要在 <code>docker-compose.yml</code> 所在目录下运行如下命令。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker-compose up -d</span><br></pre></td></tr></table></figure><p>该命令会以 detached 模式自动启动 Docker Compose 配置中定义的所有容器。你可以通过 <code>docker ps</code> 来观察上述的五个容器是否正常启动了。 也可以访问 <a href="http://localhost:5601/" target="_blank" rel="noopener">http://localhost:5601/</a> 来查看 Kibana 是否运行正常。</p><p>另外可以通过如下命令停止所有的容器：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker-compose down</span><br></pre></td></tr></table></figure><h3 id="下载安装-Flink-本地集群"><a href="#下载安装-Flink-本地集群" class="headerlink" title="下载安装 Flink 本地集群"></a>下载安装 Flink 本地集群</h3><p>我们推荐用户手动下载安装 Flink，而不是通过 Docker 自动启动 Flink。因为这样可以更直观地理解 Flink 的各个组件、依赖、和脚本。</p><ol><li>下载 Flink 1.10.0 安装包并解压（解压目录 <code>flink-1.10.0</code>）：<a href="https://www.apache.org/dist/flink/flink-1.10.0/flink-1.10.0-bin-scala_2.11.tgz" target="_blank" rel="noopener">https://www.apache.org/dist/flink/flink-1.10.0/flink-1.10.0-bin-scala_2.11.tgz</a></li><li>进入 flink-1.10.0 目录：<code>cd flink-1.10.0</code></li><li><p>通过如下命令下载依赖 jar 包，并拷贝到 <code>lib/</code> 目录下，也可手动下载和拷贝。因为我们运行时需要依赖各个 connector 实现。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">wget -P ./lib/ https://repo1.maven.org/maven2/org/apache/flink/flink-json/1.10.0/flink-json-1.10.0.jar | \</span><br><span class="line">    wget -P ./lib/ https://repo1.maven.org/maven2/org/apache/flink/flink-sql-connector-kafka_2.11/1.10.0/flink-sql-connector-kafka_2.11-1.10.0.jar | \</span><br><span class="line">    wget -P ./lib/ https://repo1.maven.org/maven2/org/apache/flink/flink-sql-connector-elasticsearch6_2.11/1.10.0/flink-sql-connector-elasticsearch6_2.11-1.10.0.jar | \</span><br><span class="line">    wget -P ./lib/ https://repo1.maven.org/maven2/org/apache/flink/flink-jdbc_2.11/1.10.0/flink-jdbc_2.11-1.10.0.jar | \</span><br><span class="line">    wget -P ./lib/ https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.48/mysql-connector-java-5.1.48.jar</span><br></pre></td></tr></table></figure></li><li><p>将 <code>conf/flink-conf.yaml</code> 中的 <code>taskmanager.numberOfTaskSlots</code> 修改成 10，因为我们会同时运行多个任务。</p></li><li>执行 <code>./bin/start-cluster.sh</code>，启动集群。<br>运行成功的话，可以在 <a href="http://localhost:8081" target="_blank" rel="noopener">http://localhost:8081</a> 访问到 Flink Web UI。并且可以看到可用 Slots 数为 10 个。</li></ol><p><img src="https://img.alicdn.com/tfs/TB1U_q.wkY2gK0jSZFgXXc5OFXa-2878-896.png" alt></p><ol start="6"><li>执行 <code>bin/sql-client.sh embedded</code> 启动 SQL CLI。便会看到如下的松鼠欢迎界面。</li></ol><p><img src="https://img.alicdn.com/tfs/TB1H1bewlr0gK0jSZFnXXbRRXXa-1696-1512.png" alt></p><h2 id="使用-DDL-创建-Kafka-表"><a href="#使用-DDL-创建-Kafka-表" class="headerlink" title="使用 DDL 创建 Kafka 表"></a>使用 DDL 创建 Kafka 表</h2><p>Datagen 容器在启动后会往 Kafka 的 <code>user_behavior</code> topic 中持续不断地写入数据。数据包含了2017年11月27日一天的用户行为（行为包括点击、购买、加购、喜欢），每一行表示一条用户行为，以 JSON 的格式由用户ID、商品ID、商品类目ID、行为类型和时间组成。该原始数据集来自<a href="https://tianchi.aliyun.com/dataset/dataDetail?dataId=649" target="_blank" rel="noopener">阿里云天池公开数据集</a>，特此鸣谢。</p><p>我们可以在 <code>docker-compose.yml</code> 所在目录下运行如下命令，查看 Kafka 集群中生成的前10条数据。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker-compose <span class="built_in">exec</span> kafka bash -c <span class="string">'kafka-console-consumer.sh --topic user_behavior --bootstrap-server kafka:9094 --from-beginning --max-messages 10'</span></span><br></pre></td></tr></table></figure><figure class="highlight"><table><tr><td class="code"><pre><span class="line">&#123;<span class="attr">"user_id"</span>: <span class="string">"952483"</span>, <span class="attr">"item_id"</span>:<span class="string">"310884"</span>, <span class="attr">"category_id"</span>: <span class="string">"4580532"</span>, <span class="attr">"behavior"</span>: <span class="string">"pv"</span>, <span class="attr">"ts"</span>: <span class="string">"2017-11-27T00:00:00Z"</span>&#125;</span><br><span class="line">&#123;<span class="attr">"user_id"</span>: <span class="string">"794777"</span>, <span class="attr">"item_id"</span>:<span class="string">"5119439"</span>, <span class="attr">"category_id"</span>: <span class="string">"982926"</span>, <span class="attr">"behavior"</span>: <span class="string">"pv"</span>, <span class="attr">"ts"</span>: <span class="string">"2017-11-27T00:00:00Z"</span>&#125;</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>有了数据源后，我们就可以用 DDL 去创建并连接这个 Kafka 中的 topic 了。在 Flink SQL CLI 中执行该 DDL。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> user_behavior (</span><br><span class="line">    user_id <span class="built_in">BIGINT</span>,</span><br><span class="line">    item_id <span class="built_in">BIGINT</span>,</span><br><span class="line">    category_id <span class="built_in">BIGINT</span>,</span><br><span class="line">    behavior <span class="keyword">STRING</span>,</span><br><span class="line">    ts <span class="built_in">TIMESTAMP</span>(<span class="number">3</span>),</span><br><span class="line">    proctime <span class="keyword">as</span> PROCTIME(),   <span class="comment">-- 通过计算列产生一个处理时间列</span></span><br><span class="line">    WATERMARK <span class="keyword">FOR</span> ts <span class="keyword">as</span> ts - <span class="built_in">INTERVAL</span> <span class="string">'5'</span> <span class="keyword">SECOND</span>  <span class="comment">-- 在ts上定义watermark，ts成为事件时间列</span></span><br><span class="line">) <span class="keyword">WITH</span> (</span><br><span class="line">    <span class="string">'connector.type'</span> = <span class="string">'kafka'</span>,  <span class="comment">-- 使用 kafka connector</span></span><br><span class="line">    <span class="string">'connector.version'</span> = <span class="string">'universal'</span>,  <span class="comment">-- kafka 版本，universal 支持 0.11 以上的版本</span></span><br><span class="line">    <span class="string">'connector.topic'</span> = <span class="string">'user_behavior'</span>,  <span class="comment">-- kafka topic</span></span><br><span class="line">    <span class="string">'connector.startup-mode'</span> = <span class="string">'earliest-offset'</span>,  <span class="comment">-- 从起始 offset 开始读取</span></span><br><span class="line">    <span class="string">'connector.properties.zookeeper.connect'</span> = <span class="string">'localhost:2181'</span>,  <span class="comment">-- zookeeper 地址</span></span><br><span class="line">    <span class="string">'connector.properties.bootstrap.servers'</span> = <span class="string">'localhost:9092'</span>,  <span class="comment">-- kafka broker 地址</span></span><br><span class="line">    <span class="string">'format.type'</span> = <span class="string">'json'</span>  <span class="comment">-- 数据源格式为 json</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>如上我们按照数据的格式声明了 5 个字段，除此之外，我们还通过计算列语法和 <code>PROCTIME()</code> 内置函数声明了一个产生处理时间的虚拟列。我们还通过 WATERMARK 语法，在 ts 字段上声明了 watermark 策略（容忍5秒乱序）， ts 字段因此也成了事件时间列。关于时间属性以及 DDL 语法可以阅读官方文档了解更多：</p><ul><li>时间属性：<br> <a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/streaming/time_attributes.html" target="_blank" rel="noopener">https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/streaming/time_attributes.html</a></li><li>DDL：<br> <a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/sql/create.html#create-table" target="_blank" rel="noopener">https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/sql/create.html#create-table</a></li></ul><p>在 SQL CLI 中成功创建 Kafka 表后，可以通过 <code>show tables;</code> 和 <code>describe user_behavior;</code> 来查看目前已注册的表，以及表的详细信息。我们也可以直接在 SQL CLI 中运行 <code>SELECT * FROM user_behavior;</code> 预览下数据（按<code>q</code>退出）。</p><p>接下来，我们会通过三个实战场景来更深入地了解 Flink SQL 。</p><h2 id="统计每小时的成交量"><a href="#统计每小时的成交量" class="headerlink" title="统计每小时的成交量"></a>统计每小时的成交量</h2><h3 id="使用-DDL-创建-Elasticsearch-表"><a href="#使用-DDL-创建-Elasticsearch-表" class="headerlink" title="使用 DDL 创建 Elasticsearch 表"></a>使用 DDL 创建 Elasticsearch 表</h3><p>我们先在 SQL CLI 中创建一个 ES 结果表，根据场景需求主要需要保存两个数据：小时、成交量。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> buy_cnt_per_hour ( </span><br><span class="line">    hour_of_day <span class="built_in">BIGINT</span>,</span><br><span class="line">    buy_cnt <span class="built_in">BIGINT</span></span><br><span class="line">) <span class="keyword">WITH</span> (</span><br><span class="line">    <span class="string">'connector.type'</span> = <span class="string">'elasticsearch'</span>, <span class="comment">-- 使用 elasticsearch connector</span></span><br><span class="line">    <span class="string">'connector.version'</span> = <span class="string">'6'</span>,  <span class="comment">-- elasticsearch 版本，6 能支持 es 6+ 以及 7+ 的版本</span></span><br><span class="line">    <span class="string">'connector.hosts'</span> = <span class="string">'http://localhost:9200'</span>,  <span class="comment">-- elasticsearch 地址</span></span><br><span class="line">    <span class="string">'connector.index'</span> = <span class="string">'buy_cnt_per_hour'</span>,  <span class="comment">-- elasticsearch 索引名，相当于数据库的表名</span></span><br><span class="line">    <span class="string">'connector.document-type'</span> = <span class="string">'user_behavior'</span>, <span class="comment">-- elasticsearch 的 type，相当于数据库的库名</span></span><br><span class="line">    <span class="string">'connector.bulk-flush.max-actions'</span> = <span class="string">'1'</span>,  <span class="comment">-- 每条数据都刷新</span></span><br><span class="line">    <span class="string">'format.type'</span> = <span class="string">'json'</span>,  <span class="comment">-- 输出数据格式 json</span></span><br><span class="line">    <span class="string">'update-mode'</span> = <span class="string">'append'</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>我们不需要在 Elasticsearch 中事先创建 <code>buy_cnt_per_hour</code> 索引，Flink Job 会自动创建该索引。</p><h3 id="提交-Query"><a href="#提交-Query" class="headerlink" title="提交 Query"></a>提交 Query</h3><p>统计每小时的成交量就是每小时共有多少 “buy” 的用户行为。因此会需要用到 TUMBLE 窗口函数，按照一小时切窗。然后每个窗口分别统计 “buy” 的个数，这可以通过先过滤出 “buy” 的数据，然后 <code>COUNT(*)</code> 实现。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> buy_cnt_per_hour</span><br><span class="line"><span class="keyword">SELECT</span> <span class="keyword">HOUR</span>(TUMBLE_START(ts, <span class="built_in">INTERVAL</span> <span class="string">'1'</span> <span class="keyword">HOUR</span>)), <span class="keyword">COUNT</span>(*)</span><br><span class="line"><span class="keyword">FROM</span> user_behavior</span><br><span class="line"><span class="keyword">WHERE</span> behavior = <span class="string">'buy'</span></span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> TUMBLE(ts, <span class="built_in">INTERVAL</span> <span class="string">'1'</span> <span class="keyword">HOUR</span>);</span><br></pre></td></tr></table></figure><p>这里我们使用 <code>HOUR</code> 内置函数，从一个 TIMESTAMP 列中提取出一天中第几个小时的值。使用了 <code>INSERT INTO</code>将 query 的结果持续不断地插入到上文定义的 es 结果表中（可以将 es 结果表理解成 query 的物化视图）。另外可以阅读该文档了解更多关于窗口聚合的内容：<a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/sql/queries.html#group-windows" target="_blank" rel="noopener">https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/sql/queries.html#group-windows</a></p><p>在 Flink SQL CLI 中运行上述查询后，在 Flink Web UI 中就能看到提交的任务，该任务是一个流式任务，因此会一直运行。</p><p><img src="https://img.alicdn.com/tfs/TB1DU7ovubviK0jSZFNXXaApXXa-2878-1310.png" alt></p><p>可以看到凌晨是一天中成交量的低谷。</p><h3 id="使用-Kibana-可视化结果"><a href="#使用-Kibana-可视化结果" class="headerlink" title="使用 Kibana 可视化结果"></a>使用 Kibana 可视化结果</h3><p>我们已经通过 Docker Compose 启动了 Kibana 容器，可以通过 <a href="http://localhost:5601" target="_blank" rel="noopener">http://localhost:5601</a> 访问 Kibana。首先我们需要先配置一个 index pattern。点击左侧工具栏的 “Management”，就能找到 “Index Patterns”。点击 “Create Index Pattern”，然后通过输入完整的索引名 “buy_cnt_per_hour” 创建 index pattern。创建完成后， Kibana 就知道了我们的索引，我们就可以开始探索数据了。  </p><p>先点击左侧工具栏的”Discovery”按钮，Kibana 就会列出刚刚创建的索引中的内容。</p><p><img src="https://img.alicdn.com/tfs/TB1xDYawbY1gK0jSZTEXXXDQVXa-2878-946.png" alt></p><p>接下来，我们先创建一个 Dashboard 用来展示各个可视化的视图。点击页面左侧的”Dashboard”，创建一个名为 ”用户行为日志分析“ 的Dashboard。然后点击 “Create New” 创建一个新的视图，选择 “Area” 面积图，选择 “buy_cnt_per_hour” 索引，按照如下截图中的配置（左侧）画出成交量面积图，并保存为”每小时成交量“。</p><p><img src="https://img.alicdn.com/tfs/TB19ae.woT1gK0jSZFhXXaAtVXa-2874-1596.png" alt></p><h2 id="统计一天每10分钟累计独立用户数"><a href="#统计一天每10分钟累计独立用户数" class="headerlink" title="统计一天每10分钟累计独立用户数"></a>统计一天每10分钟累计独立用户数</h2><p>另一个有意思的可视化是统计一天中每一刻的累计独立用户数（uv），也就是每一刻的 uv 数都代表从0点到当前时刻为止的总计 uv 数，因此该曲线肯定是单调递增的。</p><p>我们仍然先在 SQL CLI 中创建一个 Elasticsearch 表，用于存储结果汇总数据。主要有两个字段：时间和累积 uv 数。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> cumulative_uv (</span><br><span class="line">    time_str <span class="keyword">STRING</span>,</span><br><span class="line">    uv <span class="built_in">BIGINT</span></span><br><span class="line">) <span class="keyword">WITH</span> (</span><br><span class="line">    <span class="string">'connector.type'</span> = <span class="string">'elasticsearch'</span>,</span><br><span class="line">    <span class="string">'connector.version'</span> = <span class="string">'6'</span>,</span><br><span class="line">    <span class="string">'connector.hosts'</span> = <span class="string">'http://localhost:9200'</span>,</span><br><span class="line">    <span class="string">'connector.index'</span> = <span class="string">'cumulative_uv'</span>,</span><br><span class="line">    <span class="string">'connector.document-type'</span> = <span class="string">'user_behavior'</span>,</span><br><span class="line">    <span class="string">'format.type'</span> = <span class="string">'json'</span>,</span><br><span class="line">    <span class="string">'update-mode'</span> = <span class="string">'upsert'</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>为了实现该曲线，我们可以先通过 OVER WINDOW 计算出每条数据的当前分钟，以及当前累计 uv（从0点开始到当前行为止的独立用户数）。 uv 的统计我们通过内置的 <code>COUNT(DISTINCT user_id)</code>来完成，Flink SQL 内部对 COUNT DISTINCT 做了非常多的优化，因此可以放心使用。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">VIEW</span> uv_per_10min <span class="keyword">AS</span></span><br><span class="line"><span class="keyword">SELECT</span> </span><br><span class="line">  <span class="keyword">MAX</span>(<span class="keyword">SUBSTR</span>(<span class="keyword">DATE_FORMAT</span>(ts, <span class="string">'HH:mm'</span>),<span class="number">1</span>,<span class="number">4</span>) || <span class="string">'0'</span>) <span class="keyword">OVER</span> w <span class="keyword">AS</span> time_str, </span><br><span class="line">  <span class="keyword">COUNT</span>(<span class="keyword">DISTINCT</span> user_id) <span class="keyword">OVER</span> w <span class="keyword">AS</span> uv</span><br><span class="line"><span class="keyword">FROM</span> user_behavior</span><br><span class="line"><span class="keyword">WINDOW</span> w <span class="keyword">AS</span> (<span class="keyword">ORDER</span> <span class="keyword">BY</span> proctime <span class="keyword">ROWS</span> <span class="keyword">BETWEEN</span> <span class="keyword">UNBOUNDED</span> <span class="keyword">PRECEDING</span> <span class="keyword">AND</span> <span class="keyword">CURRENT</span> <span class="keyword">ROW</span>);</span><br></pre></td></tr></table></figure><p>这里我们使用 <code>SUBSTR</code> 和  <code>DATE_FORMAT</code> 还有 <code>||</code> 内置函数，将一个 TIMESTAMP 字段转换成了 10分钟单位的时间字符串，如: <code>12:10</code>, <code>12:20</code>。关于 OVER WINDOW 的更多内容可以参考文档：<a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/sql/queries.html#aggregations" target="_blank" rel="noopener">https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/sql/queries.html#aggregations</a></p><p>我们还使用了 CREATE VIEW 语法将 query 注册成了一个逻辑视图，可以方便地在后续查询中对该 query 进行引用，这有利于拆解复杂 query。注意，创建逻辑视图不会触发作业的执行，视图的结果也不会落地，因此使用起来非常轻量，没有额外开销。由于 <code>uv_per_10min</code> 每条输入数据都产生一条输出数据，因此对于存储压力较大。我们可以基于 <code>uv_per_10min</code> 再根据分钟时间进行一次聚合，这样每10分钟只有一个点会存储在 Elasticsearch 中，对于 Elasticsearch 和 Kibana 可视化渲染的压力会小很多。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> cumulative_uv</span><br><span class="line"><span class="keyword">SELECT</span> time_str, <span class="keyword">MAX</span>(uv)</span><br><span class="line"><span class="keyword">FROM</span> uv_per_10min</span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> time_str;</span><br></pre></td></tr></table></figure><p>提交上述查询后，在 Kibana 中创建 <code>cumulative_uv</code> 的 index pattern，然后在 Dashboard 中创建一个”Line”折线图，选择 <code>cumulative_uv</code> 索引，按照如下截图中的配置（左侧）画出累计独立用户数曲线，并保存。</p><p><img src="https://img.alicdn.com/tfs/TB1xU5.wkY2gK0jSZFgXXc5OFXa-2878-1598.png" alt></p><h2 id="顶级类目排行榜"><a href="#顶级类目排行榜" class="headerlink" title="顶级类目排行榜"></a>顶级类目排行榜</h2><p>最后一个有意思的可视化是类目排行榜，从而了解哪些类目是支柱类目。不过由于源数据中的类目分类太细（约5000个类目），对于排行榜意义不大，因此我们希望能将其归约到顶级类目。所以笔者在 mysql 容器中预先准备了子类目与顶级类目的映射数据，用作维表。</p><p>在 SQL CLI 中创建 MySQL 表，后续用作维表查询。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> category_dim (</span><br><span class="line">    sub_category_id <span class="built_in">BIGINT</span>,  <span class="comment">-- 子类目</span></span><br><span class="line">    parent_category_id <span class="built_in">BIGINT</span> <span class="comment">-- 顶级类目</span></span><br><span class="line">) <span class="keyword">WITH</span> (</span><br><span class="line">    <span class="string">'connector.type'</span> = <span class="string">'jdbc'</span>,</span><br><span class="line">    <span class="string">'connector.url'</span> = <span class="string">'jdbc:mysql://localhost:3306/flink'</span>,</span><br><span class="line">    <span class="string">'connector.table'</span> = <span class="string">'category'</span>,</span><br><span class="line">    <span class="string">'connector.driver'</span> = <span class="string">'com.mysql.jdbc.Driver'</span>,</span><br><span class="line">    <span class="string">'connector.username'</span> = <span class="string">'root'</span>,</span><br><span class="line">    <span class="string">'connector.password'</span> = <span class="string">'123456'</span>,</span><br><span class="line">    <span class="string">'connector.lookup.cache.max-rows'</span> = <span class="string">'5000'</span>,</span><br><span class="line">    <span class="string">'connector.lookup.cache.ttl'</span> = <span class="string">'10min'</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>同时我们再创建一个 Elasticsearch 表，用于存储类目统计结果。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> top_category (</span><br><span class="line">    category_name <span class="keyword">STRING</span>,  <span class="comment">-- 类目名称</span></span><br><span class="line">    buy_cnt <span class="built_in">BIGINT</span>  <span class="comment">-- 销量</span></span><br><span class="line">) <span class="keyword">WITH</span> (</span><br><span class="line">    <span class="string">'connector.type'</span> = <span class="string">'elasticsearch'</span>,</span><br><span class="line">    <span class="string">'connector.version'</span> = <span class="string">'6'</span>,</span><br><span class="line">    <span class="string">'connector.hosts'</span> = <span class="string">'http://localhost:9200'</span>,</span><br><span class="line">    <span class="string">'connector.index'</span> = <span class="string">'top_category'</span>,</span><br><span class="line">    <span class="string">'connector.document-type'</span> = <span class="string">'user_behavior'</span>,</span><br><span class="line">    <span class="string">'format.type'</span> = <span class="string">'json'</span>,</span><br><span class="line">    <span class="string">'update-mode'</span> = <span class="string">'upsert'</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>第一步我们通过维表关联，补全类目名称。我们仍然使用 CREATE VIEW 将该查询注册成一个视图，简化逻辑。维表关联使用 temporal join 语法，可以查看文档了解更多：<a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/streaming/joins.html#join-with-a-temporal-table" target="_blank" rel="noopener">https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/streaming/joins.html#join-with-a-temporal-table</a></p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">VIEW</span> rich_user_behavior <span class="keyword">AS</span></span><br><span class="line"><span class="keyword">SELECT</span> U.user_id, U.item_id, U.behavior, </span><br><span class="line">  <span class="keyword">CASE</span> C.parent_category_id</span><br><span class="line">    <span class="keyword">WHEN</span> <span class="number">1</span> <span class="keyword">THEN</span> <span class="string">'服饰鞋包'</span></span><br><span class="line">    <span class="keyword">WHEN</span> <span class="number">2</span> <span class="keyword">THEN</span> <span class="string">'家装家饰'</span></span><br><span class="line">    <span class="keyword">WHEN</span> <span class="number">3</span> <span class="keyword">THEN</span> <span class="string">'家电'</span></span><br><span class="line">    <span class="keyword">WHEN</span> <span class="number">4</span> <span class="keyword">THEN</span> <span class="string">'美妆'</span></span><br><span class="line">    <span class="keyword">WHEN</span> <span class="number">5</span> <span class="keyword">THEN</span> <span class="string">'母婴'</span></span><br><span class="line">    <span class="keyword">WHEN</span> <span class="number">6</span> <span class="keyword">THEN</span> <span class="string">'3C数码'</span></span><br><span class="line">    <span class="keyword">WHEN</span> <span class="number">7</span> <span class="keyword">THEN</span> <span class="string">'运动户外'</span></span><br><span class="line">    <span class="keyword">WHEN</span> <span class="number">8</span> <span class="keyword">THEN</span> <span class="string">'食品'</span></span><br><span class="line">    <span class="keyword">ELSE</span> <span class="string">'其他'</span></span><br><span class="line">  <span class="keyword">END</span> <span class="keyword">AS</span> category_name</span><br><span class="line"><span class="keyword">FROM</span> user_behavior <span class="keyword">AS</span> U <span class="keyword">LEFT</span> <span class="keyword">JOIN</span> category_dim <span class="keyword">FOR</span> SYSTEM_TIME <span class="keyword">AS</span> <span class="keyword">OF</span> U.proctime <span class="keyword">AS</span> C</span><br><span class="line"><span class="keyword">ON</span> U.category_id = C.sub_category_id;</span><br></pre></td></tr></table></figure><p>最后根据 类目名称分组，统计出 <code>buy</code> 的事件数，并写入 Elasticsearch 中。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> top_category</span><br><span class="line"><span class="keyword">SELECT</span> category_name, <span class="keyword">COUNT</span>(*) buy_cnt</span><br><span class="line"><span class="keyword">FROM</span> rich_user_behavior</span><br><span class="line"><span class="keyword">WHERE</span> behavior = <span class="string">'buy'</span></span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> category_name;</span><br></pre></td></tr></table></figure><p>提交上述查询后，在 Kibana 中创建 <code>top_category</code> 的 index pattern，然后在 Dashboard 中创建一个”Horizontal Bar”条形图，选择 <code>top_category</code> 索引，按照如下截图中的配置（左侧）画出类目排行榜，并保存。</p><p><img src="https://img.alicdn.com/tfs/TB13HW9weL2gK0jSZPhXXahvXXa-2874-1596.png" alt></p><p>可以看到服饰鞋包的成交量远远领先其他类目。</p><p>Kibana 还提供了非常丰富的图形和可视化选项，感兴趣的用户可以用 Flink SQL 对数据进行更多维度的分析，并使用 Kibana 展示出可视化图，并观测图形数据的实时变化。</p><h2 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h2><p>在本文中，我们展示了如何使用 Flink SQL 集成 Kafka, MySQL, Elasticsearch 以及 Kibana 来快速搭建一个实时分析应用。整个过程无需一行 Java/Scala 代码，使用 SQL 纯文本即可完成。期望通过本文，可以让读者了解到 Flink SQL 的易用和强大，包括轻松连接各种外部系统、对事件时间和乱序数据处理的原生支持、维表关联、丰富的内置函数等等。希望你能喜欢我们的实战演练，并从中获得乐趣和知识！</p>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;上周四在 Flink 中文社区钉钉群中直播分享了《Demo：基于 Flink SQL 构建流式应用》，直播内容偏向实战演示。这篇文章是对直播内容的一个总结，并且改善了部分内容，比如除 Flink 外其他组件全部采用 Docker Compose 安装，简化准备流程。读者也可以结合视频和本文一起学习。完整分享可以观看视频回顾：&lt;a href=&quot;https://www.bilibili.com/video/av90560012&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.bilibili.com/video/av90560012&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Flink 1.10.0 于近期刚发布，释放了许多令人激动的新特性。尤其是 Flink SQL 模块，发展速度非常快，因此本文特意从实践的角度出发，带领大家一起探索使用 Flink SQL 如何快速构建流式应用。&lt;/p&gt;
&lt;p&gt;本文将基于 Kafka, MySQL, Elasticsearch, Kibana，使用 Flink SQL 构建一个电商用户行为的实时分析应用。本文所有的实战演练都将在 Flink SQL CLI 上执行，全程只涉及 SQL 纯文本，无需一行 Java/Scala 代码，无需安装 IDE。本实战演练的最终效果图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.alicdn.com/tfs/TB1xc2ewlr0gK0jSZFnXXbRRXXa-3104-1978.png&quot; alt&gt;&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
  </entry>
  
  <entry>
    <title>Flink 1.9 实战：使用 SQL 读取  Kafka 并写入 MySQL</title>
    <link href="http://wuchong.me/blog/2019/09/02/flink-sql-1-9-read-from-kafka-write-into-mysql/"/>
    <id>http://wuchong.me/blog/2019/09/02/flink-sql-1-9-read-from-kafka-write-into-mysql/</id>
    <published>2019-09-02T15:05:45.000Z</published>
    <updated>2022-08-03T06:46:44.505Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>上周六在深圳分享了《Flink SQL 1.9.0 技术内幕和最佳实践》，会后许多小伙伴对最后演示环节的 Demo 代码非常感兴趣，迫不及待地想尝试下，所以写了这篇文章分享下这份代码。希望对于 Flink SQL 的初学者能有所帮助。完整分享可以观看 Meetup 视频回顾 ：<a href="https://developer.aliyun.com/live/1416" target="_blank" rel="noopener">https://developer.aliyun.com/live/1416</a></p></blockquote><p>演示代码已经开源到了 GitHub 上：<a href="https://github.com/wuchong/flink-sql-submit" target="_blank" rel="noopener">https://github.com/wuchong/flink-sql-submit</a> 。</p><p>这份代码主要由两部分组成：1) 能用来提交 SQL 文件的 SqlSubmit 实现。2） 用于演示的 SQL 示例、Kafka 启动停止脚本、 一份测试数据集、Kafka 数据源生成器。</p><p>通过本实战，你将学到：</p><ol><li>如何使用 Blink Planner</li><li>一个简单的 SqlSubmit 是如何实现的</li><li>如何用 DDL 创建一个 Kafka 源表和 MySQL 结果表</li><li>运行一个从 Kafka 读取数据，计算 PVUV，并写入 MySQL 的作业</li><li>设置调优参数，观察对作业的影响</li></ol><a id="more"></a><h2 id="SqlSubmit-的实现"><a href="#SqlSubmit-的实现" class="headerlink" title="SqlSubmit 的实现"></a>SqlSubmit 的实现</h2><p>笔者一开始是想用 <a href="https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/table/sqlClient.html" target="_blank" rel="noopener">SQL Client</a> 来贯穿整个演示环节，但可惜 1.9 版本 SQL CLI 还不支持处理 CREATE TABLE 语句。所以笔者就只好自己写了个简单的提交脚本。后来想想，也挺好的，可以让听众同时了解如何通过 SQL 的方式，和编程的方式使用 Flink SQL。</p><p>SqlSubmit 的主要任务是执行和提交一个 SQL 文件，实现非常简单，就是通过正则表达式匹配每个语句块。如果是 <code>CREATE TABLE</code> 或 <code>INSERT INTO</code> 开头，则会调用 <code>tEnv.sqlUpdate(...)</code>。如果是 <code>SET</code> 开头，则会将配置设置到 <code>TableConfig</code> 上。其核心代码主要如下所示：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">EnvironmentSettings settings = EnvironmentSettings.newInstance()</span><br><span class="line">        .useBlinkPlanner()</span><br><span class="line">        .inStreamingMode()</span><br><span class="line">        .build();</span><br><span class="line"><span class="comment">// 创建一个使用 Blink Planner 的 TableEnvironment, 并工作在流模式</span></span><br><span class="line">TableEnvironment tEnv = TableEnvironment.create(settings);</span><br><span class="line"><span class="comment">// 读取 SQL 文件</span></span><br><span class="line">List&lt;String&gt; sql = Files.readAllLines(path);</span><br><span class="line"><span class="comment">// 通过正则表达式匹配前缀，来区分不同的 SQL 语句</span></span><br><span class="line">List&lt;SqlCommandCall&gt; calls = SqlCommandParser.parse(sql);</span><br><span class="line"><span class="comment">// 根据不同的 SQL 语句，调用 TableEnvironment 执行</span></span><br><span class="line"><span class="keyword">for</span> (SqlCommandCall call : calls) &#123;</span><br><span class="line">    <span class="keyword">switch</span> (call.command) &#123;</span><br><span class="line">        <span class="keyword">case</span> SET:</span><br><span class="line">            String key = call.operands[<span class="number">0</span>];</span><br><span class="line">            String value = call.operands[<span class="number">1</span>];</span><br><span class="line">            <span class="comment">// 设置参数</span></span><br><span class="line">            tEnv.getConfig().getConfiguration().setString(key, value);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> CREATE_TABLE:</span><br><span class="line">            String ddl = call.operands[<span class="number">0</span>];</span><br><span class="line">            tEnv.sqlUpdate(ddl);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> INSERT_INTO:</span><br><span class="line">            String dml = call.operands[<span class="number">0</span>];</span><br><span class="line">            tEnv.sqlUpdate(dml);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"Unsupported command: "</span> + call.command);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 提交作业</span></span><br><span class="line">tEnv.execute(<span class="string">"SQL Job"</span>);</span><br></pre></td></tr></table></figure><h2 id="使用-DDL-连接-Kafka-源表"><a href="#使用-DDL-连接-Kafka-源表" class="headerlink" title="使用 DDL 连接 Kafka 源表"></a>使用 DDL 连接 Kafka 源表</h2><p>在 flink-sql-submit 项目中，我们准备了一份测试数据集（来自<a href="https://tianchi.aliyun.com/datalab/index.htm" target="_blank" rel="noopener">阿里云天池公开数据集</a>，特别鸣谢），位于 <code>src/main/resources/user_behavior.log</code>。数据以 JSON 格式编码，大概长这个样子：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line">&#123;<span class="attr">"user_id"</span>: <span class="string">"543462"</span>, <span class="attr">"item_id"</span>:<span class="string">"1715"</span>, <span class="attr">"category_id"</span>: <span class="string">"1464116"</span>, <span class="attr">"behavior"</span>: <span class="string">"pv"</span>, <span class="attr">"ts"</span>: <span class="string">"2017-11-26T01:00:00Z"</span>&#125;</span><br><span class="line">&#123;<span class="attr">"user_id"</span>: <span class="string">"662867"</span>, <span class="attr">"item_id"</span>:<span class="string">"2244074"</span>, <span class="attr">"category_id"</span>: <span class="string">"1575622"</span>, <span class="attr">"behavior"</span>: <span class="string">"pv"</span>, <span class="attr">"ts"</span>: <span class="string">"2017-11-26T01:00:00Z"</span>&#125;</span><br></pre></td></tr></table></figure><p>为了模拟真实的 Kafka 数据源，笔者还特地写了一个 <code>source-generator.sh</code> 脚本（感兴趣的可以看下源码），会自动读取 user_behavior.log 的数据并以默认每毫秒1条的速率灌到 Kafka 的 <code>user_behavior</code> topic 中。</p><p>有了数据源后，我们就可以用 DDL 去创建并连接这个 Kafka 中的 topic（详见 <code>src/main/resources/q1.sql</code>）。 </p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> user_log (</span><br><span class="line">    user_id <span class="built_in">VARCHAR</span>,</span><br><span class="line">    item_id <span class="built_in">VARCHAR</span>,</span><br><span class="line">    category_id <span class="built_in">VARCHAR</span>,</span><br><span class="line">    behavior <span class="built_in">VARCHAR</span>,</span><br><span class="line">    ts <span class="built_in">TIMESTAMP</span></span><br><span class="line">) <span class="keyword">WITH</span> (</span><br><span class="line">    <span class="string">'connector.type'</span> = <span class="string">'kafka'</span>, <span class="comment">-- 使用 kafka connector</span></span><br><span class="line">    <span class="string">'connector.version'</span> = <span class="string">'universal'</span>,  <span class="comment">-- kafka 版本，universal 支持 0.11 以上的版本</span></span><br><span class="line">    <span class="string">'connector.topic'</span> = <span class="string">'user_behavior'</span>,  <span class="comment">-- kafka topic</span></span><br><span class="line">    <span class="string">'connector.startup-mode'</span> = <span class="string">'earliest-offset'</span>, <span class="comment">-- 从起始 offset 开始读取</span></span><br><span class="line">    <span class="string">'connector.properties.0.key'</span> = <span class="string">'zookeeper.connect'</span>,  <span class="comment">-- 连接信息</span></span><br><span class="line">    <span class="string">'connector.properties.0.value'</span> = <span class="string">'localhost:2181'</span>, </span><br><span class="line">    <span class="string">'connector.properties.1.key'</span> = <span class="string">'bootstrap.servers'</span>,</span><br><span class="line">    <span class="string">'connector.properties.1.value'</span> = <span class="string">'localhost:9092'</span>, </span><br><span class="line">    <span class="string">'update-mode'</span> = <span class="string">'append'</span>,</span><br><span class="line">    <span class="string">'format.type'</span> = <span class="string">'json'</span>,  <span class="comment">-- 数据源格式为 json</span></span><br><span class="line">    <span class="string">'format.derive-schema'</span> = <span class="string">'true'</span> <span class="comment">-- 从 DDL schema 确定 json 解析规则</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><blockquote><p>注：可能有用户会觉得其中的 <code>connector.properties.0.key</code> 等参数比较奇怪，社区计划将在下一个版本中改进并简化 connector 的参数配置。</p></blockquote><h2 id="使用-DDL-连接-MySQL-结果表"><a href="#使用-DDL-连接-MySQL-结果表" class="headerlink" title="使用 DDL 连接 MySQL 结果表"></a>使用 DDL 连接 MySQL 结果表</h2><p>连接 MySQL 可以使用 Flink 提供的 JDBC connector。例如</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> pvuv_sink (</span><br><span class="line">    dt <span class="built_in">VARCHAR</span>,</span><br><span class="line">    pv <span class="built_in">BIGINT</span>,</span><br><span class="line">    uv <span class="built_in">BIGINT</span></span><br><span class="line">) <span class="keyword">WITH</span> (</span><br><span class="line">    <span class="string">'connector.type'</span> = <span class="string">'jdbc'</span>, <span class="comment">-- 使用 jdbc connector</span></span><br><span class="line">    <span class="string">'connector.url'</span> = <span class="string">'jdbc:mysql://localhost:3306/flink-test'</span>, <span class="comment">-- jdbc url</span></span><br><span class="line">    <span class="string">'connector.table'</span> = <span class="string">'pvuv_sink'</span>, <span class="comment">-- 表名</span></span><br><span class="line">    <span class="string">'connector.username'</span> = <span class="string">'root'</span>, <span class="comment">-- 用户名</span></span><br><span class="line">    <span class="string">'connector.password'</span> = <span class="string">'123456'</span>, <span class="comment">-- 密码</span></span><br><span class="line">    <span class="string">'connector.write.flush.max-rows'</span> = <span class="string">'1'</span> <span class="comment">-- 默认5000条，为了演示改为1条</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><h2 id="PV-UV-计算"><a href="#PV-UV-计算" class="headerlink" title="PV UV 计算"></a>PV UV 计算</h2><p>假设我们的需求是计算每小时全网的用户访问量，和独立用户数。很多用户可能会想到使用滚动窗口来计算。但这里我们介绍另一种方式。即 Group Aggregation 的方式。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> pvuv_sink</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">  <span class="keyword">DATE_FORMAT</span>(ts, <span class="string">'yyyy-MM-dd HH:00'</span>) dt,</span><br><span class="line">  <span class="keyword">COUNT</span>(*) <span class="keyword">AS</span> pv,</span><br><span class="line">  <span class="keyword">COUNT</span>(<span class="keyword">DISTINCT</span> user_id) <span class="keyword">AS</span> uv</span><br><span class="line"><span class="keyword">FROM</span> user_log</span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> <span class="keyword">DATE_FORMAT</span>(ts, <span class="string">'yyyy-MM-dd HH:00'</span>)</span><br></pre></td></tr></table></figure><p>它使用 <code>DATE_FORMAT</code> 这个内置函数，将日志时间归一化成“年月日小时”的字符串格式，并根据这个字符串进行分组，即根据每小时分组，然后通过 <code>COUNT(*)</code> 计算用户访问量（PV），通过 <code>COUNT(DISTINCT user_id)</code> 计算独立用户数（UV）。这种方式的执行模式是每收到一条数据，便会进行基于之前计算的值做增量计算（如+1），然后将最新结果输出。所以实时性很高，但输出量也大。</p><p>我们将这个查询的结果，通过 <code>INSERT INTO</code> 语句，写到了之前定义的 <code>pvuv_sink</code> MySQL 表中。</p><blockquote><p>注：在深圳 Meetup 中，我们有对这种查询的性能调优做了深度的介绍。</p></blockquote><h2 id="实战演示"><a href="#实战演示" class="headerlink" title="实战演示"></a>实战演示</h2><h3 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h3><p>本实战演示环节需要安装一些必须的服务，包括：</p><ul><li>Flink 本地集群：用来运行 Flink SQL 任务。</li><li>Kafka 本地集群：用来作为数据源。</li><li>MySQL 数据库：用来作为结果表。</li></ul><h4 id="Flink-本地集群安装"><a href="#Flink-本地集群安装" class="headerlink" title="Flink 本地集群安装"></a>Flink 本地集群安装</h4><ol><li>下载 Flink 1.9.0 安装包并解压（解压目录 <code>flink-1.9.0</code>）：<a href="https://www.apache.org/dist/flink/flink-1.9.0/flink-1.9.0-bin-scala_2.11.tgz" target="_blank" rel="noopener">https://www.apache.org/dist/flink/flink-1.9.0/flink-1.9.0-bin-scala_2.11.tgz</a></li><li>下载以下依赖 jar 包，并拷贝到 <code>flink-1.9.0/lib/</code> 目录下。因为我们运行时需要依赖各个 connector 实现。<ul><li><a href="http://central.maven.org/maven2/org/apache/flink/flink-sql-connector-kafka_2.11/1.9.0/flink-sql-connector-kafka_2.11-1.9.0.jar" target="_blank" rel="noopener">flink-sql-connector-kafka_2.11-1.9.0.jar</a> </li><li><a href="http://central.maven.org/maven2/org/apache/flink/flink-json/1.9.0/flink-json-1.9.0-sql-jar.jar" target="_blank" rel="noopener">flink-json-1.9.0-sql-jar.jar</a></li><li><a href="http://central.maven.org/maven2/org/apache/flink/flink-jdbc_2.11/1.9.0/flink-jdbc_2.11-1.9.0.jar" target="_blank" rel="noopener">flink-jdbc_2.11-1.9.0.jar</a></li><li><a href="https://dev.mysql.com/downloads/connector/j/5.1.html" target="_blank" rel="noopener">mysql-connector-java-5.1.48.jar</a></li></ul></li><li>将 <code>flink-1.9.0/conf/flink-conf.yaml</code> 中的 <code>taskmanager.numberOfTaskSlots</code> 修改成 10，因为我们的演示任务可能会消耗多于1个的 slot。</li><li>执行 <code>flink-1.9.0/bin/start-cluster.sh</code>，启动集群。</li></ol><p>运行成功的话，可以在 <a href="http://localhost:8081" target="_blank" rel="noopener">http://localhost:8081</a> 访问到 Flink Web UI。</p><p><img src="https://img.alicdn.com/tfs/TB1AeFyeYr1gK0jSZR0XXbP8XXa-3818-942.png" alt></p><p>另外，还需要将 Flink 的安装路径填到 flink-sql-submit 项目的 <code>env.sh</code> 中，用于后面提交 SQL 任务，如我的路径是</p><figure class="highlight awk"><table><tr><td class="code"><pre><span class="line">FLINK_DIR=<span class="regexp">/Users/</span>wuchong<span class="regexp">/dev/i</span>nstall<span class="regexp">/flink-1.9.0</span></span><br></pre></td></tr></table></figure><h4 id="Kafka-本地集群安装"><a href="#Kafka-本地集群安装" class="headerlink" title="Kafka 本地集群安装"></a>Kafka 本地集群安装</h4><ol><li>下载 Kafka 2.2.0 安装包并解压：<a href="https://www.apache.org/dist/kafka/2.2.0/kafka_2.11-2.2.0.tgz" target="_blank" rel="noopener">https://www.apache.org/dist/kafka/2.2.0/kafka_2.11-2.2.0.tgz</a></li><li><p>将安装路径填到 flink-sql-submit 项目的 <code>env.sh</code> 中，如我的路径是</p> <figure class="highlight lsl"><table><tr><td class="code"><pre><span class="line">KAFKA_DIR=/Users/wuchong/dev/install/kafka_2<span class="number">.11</span><span class="number">-2.2</span><span class="number">.0</span></span><br></pre></td></tr></table></figure></li><li><p>在 <code>flink-sql-submit</code> 目录下运行 <code>./start-kafka.sh</code> 启动 Kafka 集群。</p></li><li>在命令行执行 <code>jps</code>，如果看到 <code>Kafka</code> 进程和 <code>QuorumPeerMain</code> 进程即表明启动成功。</li></ol><h4 id="MySQL-安装"><a href="#MySQL-安装" class="headerlink" title="MySQL 安装"></a>MySQL 安装</h4><ul><li>可以在<a href="https://dev.mysql.com/downloads/mysql/" target="_blank" rel="noopener">官方页面</a>下载 MySQL 并安装。</li><li>如果有 Docker 环境的话，也可以直接通过 <a href="https://hub.docker.com/_/mysql" target="_blank" rel="noopener">Docker 安装</a>。</li></ul><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">$ docker pull mysql</span><br><span class="line">$ docker <span class="builtin-name">run</span> --name mysqldb -p 3306:3306 -e <span class="attribute">MYSQL_ROOT_PASSWORD</span>=123456 -d mysql</span><br></pre></td></tr></table></figure><p> 在 MySQL 中创建一个 <code>flink-test</code> 的数据库，并按照上文的 schema 创建 pvuv_sink 表。</p><h3 id="提交-SQL-任务"><a href="#提交-SQL-任务" class="headerlink" title="提交 SQL 任务"></a>提交 SQL 任务</h3><ol><li>在 <code>flink-sql-submit</code> 目录下运行 <code>./source-generator.sh</code>，会自动创建 <code>user_behavior</code> topic，并实时往里灌入数据。</li></ol><p><img src="https://img.alicdn.com/tfs/TB1Z6dBeW61gK0jSZFlXXXDKFXa-2650-528.png" alt></p><ol start="2"><li>在 <code>flink-sql-submit</code> 目录下运行 <code>./run.sh q1</code>， 提交成功后，可以在 Web UI 中看到拓扑。</li></ol><p><img src="https://img.alicdn.com/tfs/TB1FxFye7Y2gK0jSZFgXXc5OFXa-2870-2148.png" alt></p><p>在 MySQL 客户端，我们也可以实时地看到每个小时的 pv uv 值在不断地变化。</p><p><img src="https://img.alicdn.com/tfs/TB11fVze8r0gK0jSZFnXXbRRXXa-1824-1178.png" alt></p><h2 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h2><p>本文带大家搭建基础集群环境，并使用 SqlSubmit 提交纯 SQL 任务来学习了解如何连接外部系统。<code>flink-sql-submit/src/main/resources/q1.sql</code> 中还有一些注释掉的调优参数，感兴趣的同学可以将参数打开，观察对作业的影响。关于这些调优参数的原理，可以看下我在<a href="https://mp.weixin.qq.com/s/ncaJdv3XcuLSBo6WrCKWfg" target="_blank" rel="noopener">深圳 Meetup</a> 上的分享《Flink SQL 1.9.0 技术内幕和最佳实践》。</p>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;上周六在深圳分享了《Flink SQL 1.9.0 技术内幕和最佳实践》，会后许多小伙伴对最后演示环节的 Demo 代码非常感兴趣，迫不及待地想尝试下，所以写了这篇文章分享下这份代码。希望对于 Flink SQL 的初学者能有所帮助。完整分享可以观看 Meetup 视频回顾 ：&lt;a href=&quot;https://developer.aliyun.com/live/1416&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.aliyun.com/live/1416&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;演示代码已经开源到了 GitHub 上：&lt;a href=&quot;https://github.com/wuchong/flink-sql-submit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/wuchong/flink-sql-submit&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;这份代码主要由两部分组成：1) 能用来提交 SQL 文件的 SqlSubmit 实现。2） 用于演示的 SQL 示例、Kafka 启动停止脚本、 一份测试数据集、Kafka 数据源生成器。&lt;/p&gt;
&lt;p&gt;通过本实战，你将学到：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如何使用 Blink Planner&lt;/li&gt;
&lt;li&gt;一个简单的 SqlSubmit 是如何实现的&lt;/li&gt;
&lt;li&gt;如何用 DDL 创建一个 Kafka 源表和 MySQL 结果表&lt;/li&gt;
&lt;li&gt;运行一个从 Kafka 读取数据，计算 PVUV，并写入 MySQL 的作业&lt;/li&gt;
&lt;li&gt;设置调优参数，观察对作业的影响&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
  </entry>
  
  <entry>
    <title>Flink SQL 编程实践</title>
    <link href="http://wuchong.me/blog/2019/08/20/flink-sql-training/"/>
    <id>http://wuchong.me/blog/2019/08/20/flink-sql-training/</id>
    <published>2019-08-20T08:16:22.000Z</published>
    <updated>2022-08-03T06:46:44.505Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>注： 本教程实践基于 Ververica 开源的 <a href="https://github.com/ververica/sql-training" target="_blank" rel="noopener">sql-training</a> 项目。基于 Flink 1.7.2 。</p></blockquote><h2 id="通过本课你能学到什么？"><a href="#通过本课你能学到什么？" class="headerlink" title="通过本课你能学到什么？"></a>通过本课你能学到什么？</h2><p>本文将通过五个实例来贯穿 Flink SQL 的编程实践，主要会涵盖以下几个方面的内容。</p><ol><li>如何使用 SQL CLI 客户端</li><li>如何在流上运行 SQL 查询</li><li>运行 window aggregate 与 non-window aggregate，理解其区别</li><li>如何用 SQL 消费 Kafka 数据</li><li>如何用 SQL 将结果写入 Kafka 和 ElasticSearch</li></ol><p>本文假定您已具备基础的 SQL 知识。</p><a id="more"></a><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>本文教程是基于 Docker 进行的，因此你只需要安装了 <a href="https://www.docker.com/" target="_blank" rel="noopener">Docker</a> 即可。不需要依赖 Java、Scala 环境、或是IDE。</p><p>注意：Docker 默认配置的资源可能不太够，会导致运行 Flink Job 时卡死。因此推荐配置 Docker 资源到 3-4 GB，3-4 CPUs。</p><p><img src="https://img.alicdn.com/tfs/TB1giprai_1gK0jSZFqXXcpaXXa-1224-1146.png" alt></p><p>本次教程的环境使用 Docker Compose 来安装，包含了所需的各种服务的容器，包括：</p><ul><li>Flink SQL Client：用来提交query，以及可视化结果</li><li>Flink JobManager 和 TaskManager：用来运行 Flink SQL 任务。</li><li>Apache Kafka：用来生成输入流和写入结果流。</li><li>Apache Zookeeper：Kafka 的依赖项</li><li>ElasticSearch：用来写入结果</li></ul><p>我们已经提供好了Docker Compose 配置文件，可以直接下载 <a href="https://raw.githubusercontent.com/ververica/sql-training/master/docker-compose.yml" target="_blank" rel="noopener">docker-compose.yml</a> 文件。</p><p>然后打开命令行窗口，进入存放 <code>docker-compose.yml</code> 文件的目录，然后运行以下命令：</p><ul><li><strong>Linux &amp; MacOS</strong></li></ul><figure class="highlight ebnf"><table><tr><td class="code"><pre><span class="line"><span class="attribute">docker-compose up -d</span></span><br></pre></td></tr></table></figure><ul><li><strong>Windows</strong></li></ul><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="builtin-name">set</span> <span class="attribute">COMPOSE_CONVERT_WINDOWS_PATHS</span>=1</span><br><span class="line">docker-compose up -d</span><br></pre></td></tr></table></figure><p><code>docker-compose</code> 命令会启动所有所需的容器。第一次运行的时候，Docker 会自动地从 Docker Hub 下载镜像，这可能会需要一段时间（将近 2.3GB）。之后运行的话，几秒钟就能启动起来了。运行成功的话，会在命令行中看到以下输出，并且也可以在 <a href="http://localhost:8081" target="_blank" rel="noopener">http://localhost:8081</a> 访问到 Flink Web UI。</p><p><img src="https://img.alicdn.com/tfs/TB19WduabH1gK0jSZFwXXc7aXXa-1034-292.png" alt></p><h2 id="运行-Flink-SQL-CLI-客户端"><a href="#运行-Flink-SQL-CLI-客户端" class="headerlink" title="运行 Flink SQL CLI 客户端"></a>运行 Flink SQL CLI 客户端</h2><p>运行下面命令进入 Flink SQL CLI 。</p><figure class="highlight axapta"><table><tr><td class="code"><pre><span class="line">docker-compose exec sql-<span class="keyword">client</span> ./sql-<span class="keyword">client</span>.sh</span><br></pre></td></tr></table></figure><p>该命令会在容器中启动 Flink SQL CLI 客户端。然后你会看到如下的欢迎界面。</p><p><img src="https://img.alicdn.com/tfs/TB1.f0wahn1gK0jSZKPXXXvUXXa-3104-1978.png" alt></p><h2 id="数据介绍"><a href="#数据介绍" class="headerlink" title="数据介绍"></a>数据介绍</h2><p>Docker Compose 中已经预先注册了一些表和数据，可以运行 <code>SHOW TABLES;</code> 来查看。本文会用到的数据是 <code>Rides</code> 表，这是一张出租车的行车记录数据流，包含了时间和位置信息，运行 <code>DESCRIBE Rides;</code> 可以查看表结构。</p><figure class="highlight groovy"><table><tr><td class="code"><pre><span class="line">Flink SQL&gt; DESCRIBE Rides;</span><br><span class="line">root</span><br><span class="line"> |-- <span class="string">rideId:</span> Long           <span class="comment">// 行为ID (包含两条记录，一条入一条出）</span></span><br><span class="line"> |-- <span class="string">taxiId:</span> Long           <span class="comment">// 出租车ID </span></span><br><span class="line"> |-- <span class="string">isStart:</span> Boolean       <span class="comment">// 开始 or 结束</span></span><br><span class="line"> |-- <span class="string">lon:</span> Float             <span class="comment">// 经度</span></span><br><span class="line"> |-- <span class="string">lat:</span> Float             <span class="comment">// 纬度</span></span><br><span class="line"> |-- <span class="string">rideTime:</span> TimeIndicatorTypeInfo(rowtime)     <span class="comment">// 时间</span></span><br><span class="line"> |-- <span class="string">psgCnt:</span> Integer        <span class="comment">// 乘客数</span></span><br></pre></td></tr></table></figure><p>Rides 表的详细定义见 <a href="https://github.com/ververica/sql-training/blob/master/build-image/training-config.yaml" target="_blank" rel="noopener">training-config.yaml</a>。</p><h2 id="实例1：过滤"><a href="#实例1：过滤" class="headerlink" title="实例1：过滤"></a>实例1：过滤</h2><p>例如我们现在只想查看<strong>发生在纽约的行车记录</strong>。</p><p>注：Docker 环境中已经预定义了一些内置函数，如 <code>isInNYC(lon, lat)</code> 可以确定一个经纬度是否在纽约，<code>toAreaId(lon, lat)</code> 可以将经纬度转换成区块。</p><p>因此，此处我们可以使用 <code>isInNYC</code> 来快速过滤出纽约的行车记录。在 SQL CLI 中运行如下 Query：</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> * <span class="keyword">FROM</span> Rides <span class="keyword">WHERE</span> isInNYC(lon, lat);</span><br></pre></td></tr></table></figure><p>SQL CLI 便会提交一个 SQL 任务到 Docker 集群中，从数据源（Rides 流存储在Kafka中）不断拉取数据，并通过 <code>isInNYC</code> 过滤出所需的数据。SQL CLI 也会进入可视化模式，并不断刷新展示过滤后的结果：</p><p><img src="https://img.alicdn.com/tfs/TB1pSBsaXY7gK0jSZKzXXaikpXa-3104-1978.png" alt></p><p>也可以到 <a href="http://localhost:8081" target="_blank" rel="noopener">http://localhost:8081</a> 查看 Flink 作业的运行情况。</p><h2 id="实例2：Group-Aggregate"><a href="#实例2：Group-Aggregate" class="headerlink" title="实例2：Group Aggregate"></a>实例2：Group Aggregate</h2><p>我们的另一个需求是计算<strong>搭载每种乘客数量的行车事件数</strong>。也就是搭载1个乘客的行车数、搭载2个乘客的行车… 当然，我们仍然只关心纽约的行车事件。</p><p>因此，我们可以按照乘客数<code>psgCnt</code>做分组，使用 <code>COUNT(*)</code> 计算出每个分组的事件数，注意在分组前需要先过滤出<code>isInNYC</code>的数据。在 SQL CLI 中运行如下 Query：</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> psgCnt, <span class="keyword">COUNT</span>(*) <span class="keyword">AS</span> cnt </span><br><span class="line"><span class="keyword">FROM</span> Rides </span><br><span class="line"><span class="keyword">WHERE</span> isInNYC(lon, lat)</span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> psgCnt;</span><br></pre></td></tr></table></figure><p>SQL CLI 的可视化结果如下所示，结果每秒都在发生变化。不过最大的乘客数不会超过 6 人。</p><p><img src="https://img.alicdn.com/tfs/TB1aKxAabr1gK0jSZR0XXbP8XXa-3104-1250.png" alt></p><h2 id="实例3：Window-Aggregate"><a href="#实例3：Window-Aggregate" class="headerlink" title="实例3：Window Aggregate"></a>实例3：Window Aggregate</h2><p>为了持续地监测纽约的交通流量，需要计算出<strong>每个区块每5分钟的进入的车辆数</strong>。我们只关心至少有5辆车子进入的区块。</p><p>此处需要涉及到窗口计算（每5分钟），所以需要用到 Tumbling Window 的语法。“每个区块” 所以还要按照 <code>toAreaId</code> 进行分组计算。“进入的车辆数” 所以在分组前需要根据 <code>isStart</code> 字段过滤出进入的行车记录，并使用 <code>COUNT(*)</code> 统计车辆数。最后还有一个 “至少有5辆车子的区块” 的条件，这是一个基于统计值的过滤条件，所以可以用 SQL HAVING 子句来完成。</p><p>最后的 Query 如下所示：</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> </span><br><span class="line">  toAreaId(lon, lat) <span class="keyword">AS</span> area, </span><br><span class="line">  TUMBLE_END(rideTime, <span class="built_in">INTERVAL</span> <span class="string">'5'</span> <span class="keyword">MINUTE</span>) <span class="keyword">AS</span> window_end, </span><br><span class="line">  <span class="keyword">COUNT</span>(*) <span class="keyword">AS</span> cnt </span><br><span class="line"><span class="keyword">FROM</span> Rides </span><br><span class="line"><span class="keyword">WHERE</span> isInNYC(lon, lat) <span class="keyword">and</span> isStart</span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> </span><br><span class="line">  toAreaId(lon, lat), </span><br><span class="line">  TUMBLE(rideTime, <span class="built_in">INTERVAL</span> <span class="string">'5'</span> <span class="keyword">MINUTE</span>) </span><br><span class="line"><span class="keyword">HAVING</span> <span class="keyword">COUNT</span>(*) &gt;= <span class="number">5</span>;</span><br></pre></td></tr></table></figure><p>在 SQL CLI 中运行后，其可视化结果如下所示，每个 area + window_end 的结果输出后就不会再发生变化，但是会每隔 5 分钟会输出一批新窗口的结果。因为 Docker 环境中的source我们做了10倍的加速读取（相对于原始速度），所以演示的时候，大概每隔30秒就会输出一批新窗口。</p><p><img src="https://img.alicdn.com/tfs/TB1F0XCaoD1gK0jSZFGXXbd3FXa-3104-1214.png" alt></p><h2 id="Window-Aggregate-与-Group-Aggregate-的区别"><a href="#Window-Aggregate-与-Group-Aggregate-的区别" class="headerlink" title="Window Aggregate 与 Group Aggregate 的区别"></a>Window Aggregate 与 Group Aggregate 的区别</h2><p>从实例2和实例3的结果显示上，可以体验出来 Window Aggregate 与 Group Aggregate 是有一些明显的区别的。其主要的区别是，Window Aggregate 是当window结束时才输出，其输出的结果是最终值，不会再进行修改，其输出流是一个 <strong>Append 流</strong>。而 Group Aggregate 是每处理一条数据，就输出最新的结果，其结果是在不断更新的，就好像数据库中的数据一样，其输出流是一个 <strong>Update 流</strong>。</p><p>另外一个区别是，window 由于有 watermark ，可以精确知道哪些窗口已经过期了，所以可以及时清理过期状态，保证状态维持在稳定的大小。而 Group Aggregate 因为不知道哪些数据是过期的，所以状态会无限增长，这对于生产作业来说不是很稳定，所以建议对 Group Aggregate 的作业配上 State TTL 的配置。</p><p><img src="https://img.alicdn.com/tfs/TB1Ku0zahD1gK0jSZFsXXbldVXa-2566-1266.png" alt></p><p>例如统计每个店铺每天的实时PV，那么就可以将 TTL 配置成 24+ 小时，因为一天前的状态一般来说就用不到了。</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">SELECT  DATE_FORMAT(ts, <span class="string">'yyyy-MM-dd'</span>), shop_id, COUNT(*) as pv</span><br><span class="line"><span class="keyword">FROM</span> T</span><br><span class="line">GROUP BY DATE_FORMAT(ts, <span class="string">'yyyy-MM-dd'</span>), shop_id</span><br></pre></td></tr></table></figure><p>当然，如果 TTL 配置地太小，可能会清除掉一些有用的状态和数据，从而导致数据精确性地问题。这也是用户需要权衡地一个参数。</p><h2 id="实例4：将-Append-流写入-Kafka"><a href="#实例4：将-Append-流写入-Kafka" class="headerlink" title="实例4：将 Append 流写入 Kafka"></a>实例4：将 Append 流写入 Kafka</h2><p>上一小节介绍了 Window Aggregate 和 Group Aggregate 的区别，以及 Append 流和 Update 流的区别。在 Flink 中，目前 Update 流只能写入支持更新的外部存储，如 MySQL, HBase, ElasticSearch。Append 流可以写入任意地存储，不过一般写入日志类型的系统，如 Kafka。</p><p>这里我们希望将<strong>“每10分钟的搭乘的乘客数”</strong>写入Kafka。</p><p>我们已经预定义了一张 Kafka 的结果表 <code>Sink_TenMinPsgCnts</code>（<a href="https://github.com/ververica/sql-training/blob/master/build-image/training-config.yaml" target="_blank" rel="noopener">training-config.yaml</a> 中有完整的表定义）。</p><p>在执行 Query 前，我们先运行如下命令，来监控写入到 <code>TenMinPsgCnts</code> topic 中的数据：</p><figure class="highlight jboss-cli"><table><tr><td class="code"><pre><span class="line">docker-compose exec sql-client <span class="string">/opt/kafka-client/bin/kafka-console-consumer.sh</span> <span class="params">--bootstrap-server</span> kafka<span class="function">:9092</span> <span class="params">--topic</span> TenMinPsgCnts <span class="params">--from-beginning</span></span><br></pre></td></tr></table></figure><p>每10分钟的搭乘的乘客数可以使用 Tumbling Window 来描述，我们使用 <code>INSERT INTO Sink_TenMinPsgCnts</code> 来直接将 Query 结果写入到结果表。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> Sink_TenMinPsgCnts </span><br><span class="line"><span class="keyword">SELECT</span> </span><br><span class="line">  TUMBLE_START(rideTime, <span class="built_in">INTERVAL</span> <span class="string">'10'</span> <span class="keyword">MINUTE</span>) <span class="keyword">AS</span> cntStart,  </span><br><span class="line">  TUMBLE_END(rideTime, <span class="built_in">INTERVAL</span> <span class="string">'10'</span> <span class="keyword">MINUTE</span>) <span class="keyword">AS</span> cntEnd,</span><br><span class="line">  <span class="keyword">CAST</span>(<span class="keyword">SUM</span>(psgCnt) <span class="keyword">AS</span> <span class="built_in">BIGINT</span>) <span class="keyword">AS</span> cnt </span><br><span class="line"><span class="keyword">FROM</span> Rides </span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> TUMBLE(rideTime, <span class="built_in">INTERVAL</span> <span class="string">'10'</span> <span class="keyword">MINUTE</span>);</span><br></pre></td></tr></table></figure><p>我们可以监控到 <code>TenMinPsgCnts</code>  topic 的数据以 JSON 的形式写入到了 Kafka 中：</p><p><img src="https://img.alicdn.com/tfs/TB1.GRFakY2gK0jSZFgXXc5OFXa-1908-1502.png" alt></p><h2 id="实例5：将-Update-流写入-ElasticSearch"><a href="#实例5：将-Update-流写入-ElasticSearch" class="headerlink" title="实例5：将 Update 流写入 ElasticSearch"></a>实例5：将 Update 流写入 ElasticSearch</h2><p>最后我们实践一下将一个持续更新的 Update 流写入 ElasticSearch 中。我们希望将<strong>“每个区域出发的行车数”</strong>，写入到 ES 中。</p><p>我们也已经预定义好了一张 <code>Sink_AreaCnts</code> 的 ElasticSearch 结果表（<a href="https://github.com/ververica/sql-training/blob/master/build-image/training-config.yaml" target="_blank" rel="noopener">training-config.yaml</a> 中有完整的表定义）。该表中只有两个字段 <code>areaId</code> 和 <code>cnt</code>。</p><p>同样的，我们也使用 <code>INSERT INTO</code> 将 Query 结果直接写入到 <code>Sink_AreaCnts</code> 表中。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> Sink_AreaCnts </span><br><span class="line"><span class="keyword">SELECT</span> toAreaId(lon, lat) <span class="keyword">AS</span> areaId, <span class="keyword">COUNT</span>(*) <span class="keyword">AS</span> cnt </span><br><span class="line"><span class="keyword">FROM</span> Rides </span><br><span class="line"><span class="keyword">WHERE</span> isStart</span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> toAreaId(lon, lat);</span><br></pre></td></tr></table></figure><p>在 SQL CLI 中执行上述 Query 后，Elasticsearch 会自动地创建 <code>area-cnts</code> 索引。Elasticsearch 提供了一个 REST API 。我们可以访问 </p><ul><li>查看<code>area-cnts</code>索引的详细信息： <a href="http://localhost:9200/area-cnts" target="_blank" rel="noopener">http://localhost:9200/area-cnts</a> </li><li>查看<code>area-cnts</code>索引的统计信息： <a href="http://localhost:9200/area-cnts/_stats" target="_blank" rel="noopener">http://localhost:9200/area-cnts/_stats</a></li><li>返回<code>area-cnts</code>索引的内容：<a href="http://localhost:9200/area-cnts/_search" target="_blank" rel="noopener">http://localhost:9200/area-cnts/_search</a></li><li>显示 区块 49791 的行车数：<a href="http://localhost:9200/area-cnts/_search?q=areaId:49791" target="_blank" rel="noopener">http://localhost:9200/area-cnts/_search?q=areaId:49791</a></li></ul><p>随着 Query 的一直运行，你也可以观察到一些统计值（<code>_all.primaries.docs.count</code>, <code>_all.primaries.docs.deleted</code>）在不断的增长：<a href="http://localhost:9200/area-cnts/_stats" target="_blank" rel="noopener">http://localhost:9200/area-cnts/_stats</a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文带大家使用 Docker Compose 快速上手 Flink SQL 的编程，并对比 Window Aggregate 和 Group Aggregate 的区别，以及这两种类型的作业如何写入到 外部系统中。感兴趣的同学，可以基于这个 Docker 环境更加深入地去实践，例如运行自己写的 UDF , UDTF, UDAF。查询内置地其他源表等等。</p>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;注： 本教程实践基于 Ververica 开源的 &lt;a href=&quot;https://github.com/ververica/sql-training&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;sql-training&lt;/a&gt; 项目。基于 Flink 1.7.2 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;通过本课你能学到什么？&quot;&gt;&lt;a href=&quot;#通过本课你能学到什么？&quot; class=&quot;headerlink&quot; title=&quot;通过本课你能学到什么？&quot;&gt;&lt;/a&gt;通过本课你能学到什么？&lt;/h2&gt;&lt;p&gt;本文将通过五个实例来贯穿 Flink SQL 的编程实践，主要会涵盖以下几个方面的内容。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如何使用 SQL CLI 客户端&lt;/li&gt;
&lt;li&gt;如何在流上运行 SQL 查询&lt;/li&gt;
&lt;li&gt;运行 window aggregate 与 non-window aggregate，理解其区别&lt;/li&gt;
&lt;li&gt;如何用 SQL 消费 Kafka 数据&lt;/li&gt;
&lt;li&gt;如何用 SQL 将结果写入 Kafka 和 ElasticSearch&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本文假定您已具备基础的 SQL 知识。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="flink" scheme="http://wuchong.me/tags/flink/"/>
    
  </entry>
  
  <entry>
    <title>如何从小白成长为 Apache Committer?</title>
    <link href="http://wuchong.me/blog/2019/02/12/how-to-become-apache-committer/"/>
    <id>http://wuchong.me/blog/2019/02/12/how-to-become-apache-committer/</id>
    <published>2019-02-12T07:20:16.000Z</published>
    <updated>2022-08-03T06:46:44.507Z</updated>
    
    <content type="html"><![CDATA[<p>过去三年，我一直在为 Apache Flink 开源项目贡献，也在两年前成为了 Flink Committer。我在 Flink 社区成长的过程中受到过社区大神的很多指导，如今也有很多人在向我咨询如何能参与到开源社区中，如何能成为 Committer。这也是本文写作的初衷，希望能帮助更多人参与到开源社区中。</p><p>本文将以 Apache Flink 为例，介绍如何参与社区贡献，如何成为 Apache Committer。</p><a id="more"></a><p>我们先来了解下一个小白在 Apache 社区中的成长路线是什么样的。</p><h2 id="Apache-社区的成长路线"><a href="#Apache-社区的成长路线" class="headerlink" title="Apache 社区的成长路线"></a>Apache 社区的成长路线</h2><p>Apache 软件基金会（Apache Software Foundation，ASF）在开源软件界大名鼎鼎。ASF 能保证旗下 200 多个项目的社区活动运转良好，得益于其独特的组织架构和良好的制度。</p><p><img src="https://img.alicdn.com/tfs/TB1cRcCF7voK1RjSZFNXXcxMVXa-594-184.png" alt></p><p><strong>用户 (User):</strong> 通过使用社区的项目构建自己的业务架构的开发者都是Apache的用户。</p><p><strong>贡献者 (Contributor):</strong> 帮助解答用户的问题，贡献代码或文档，在邮件列表中参与讨论设计和方案的都是 Contributor。</p><p><strong>提交者 (Committer):</strong> 贡献多了以后，就有可能经过 PMC 的提议和投票，邀请你成为 Committer。成为 Committer 也就意味着正式加入 Apache了，不但拥有相应项目的写入权限还有 apache.org 的专属邮箱。成为 Committer 的一个福利是可以免费使用 JetBrains 家的全套付费产品，包括全宇宙最好用的 IntelliJ IDEA （这是笔者当初成为 Committer 的最大动力之一）。</p><p><strong>PMC:</strong> Committer 再往上走就是 PMC，这个必须由现有 PMC 成员提名。PMC 主要负责保证开源项目的社区活动都能运转良好，包括 Roadmap 的制定，版本的发布，Committer 的提拔。</p><p><strong>ASF Member</strong> 相当于是基金会的“股东”，有董事会选举的投票权，也可以参与董事会竞选。ASF Member 也有权利决定是否接受一个新项目，主要关注 Apache 基金会本身的发展。ASF Member 通常要从 Contributor, Committer 等这些角色起步，逐步通过行动证明自己后，才可能被接受成为ASF Member。</p><p>Apache 社区的成员分类，权限由低到高，像极了我们在公司的晋升路线，一步步往上走。</p><h2 id="如何成为-Committer"><a href="#如何成为-Committer" class="headerlink" title="如何成为 Committer"></a>如何成为 Committer</h2><p>成为 Apache Committer 并没有一个确切的标准，但是 Committer 的候选人一般都是长期活跃的贡献者。成为 Committer 并没有要求必须有巨大的架构改进贡献，或者多少行的代码贡献。贡献文档、参与邮件列表的讨论、帮助回答问题都是很重要的增加贡献，提升影响力的方式。</p><p>所以如何成为 Committer 的问题归根结底还是如何参与贡献，以及如何开始贡献的问题。</p><p>成为 Committer 的关键在于持之以恒。不同项目，项目所处的不同阶段，成为 Committer 的难度都不太一样，笔者之前也持续贡献了近一年才有幸成为了 Committer。但是只要能坚持，保持活跃，持续贡献，为项目做的贡献被大家认可后，成为 Committer 也只是时间问题了。</p><h2 id="如何参与贡献"><a href="#如何参与贡献" class="headerlink" title="如何参与贡献"></a>如何参与贡献</h2><p>参与贡献 Apache 项目有许多途径，包括提Bug，提需求，参与讨论，贡献代码和文档等等。</p><ol><li>订阅开发者邮件列表：<a href="mailto:dev@flink.apache.org" target="_blank" rel="noopener">dev@flink.apache.org</a>。关注社区动向，参与设计和方案的讨论，大胆地提出你的想法！</li><li>订阅用户邮件列表：<a href="mailto:user@flink.apache.org" target="_blank" rel="noopener">user@flink.apache.org</a>, <a href="mailto:user-zh@flink.apache.org" target="_blank" rel="noopener">user-zh@flink.apache.org</a>。帮助解答用户问题。</li><li>提Bug和提需求：Flink 使用 JIRA 来管理issue。打开 <a href="http://issues.apache.org/jira/browse/FLINK" target="_blank" rel="noopener">Flink JIRA</a>  并登录，点击菜单栏中的红色 “<strong>Create</strong>“ 按钮，创建一个issue。</li><li>贡献代码：可以在  <a href="http://issues.apache.org/jira/browse/FLINK" target="_blank" rel="noopener">Flink JIRA</a> 中寻找自己感兴趣的 issue，并提交一个 Pull Request（下文会介绍提交一个 PR 的全过程）。如果是新手，建议从 <a href="https://issues.apache.org/jira/issues/?jql=project%20%3D%20FLINK%20AND%20resolution%20%3D%20Unresolved%20AND%20labels%20%3D%20starter%20ORDER%20BY%20priority%20DESC" target="_blank" rel="noopener">“starter” 标记的 issue</a> 入手。笔者在 Flink 项目的第一个 issue 就是修复了打印日志中的错别字，非常适合于熟悉贡献流程，而且当天就 merge 了，成就感满满。当熟悉了流程之后，建议专注贡献某个模块（如 SQL, DataStream, Runtime)，有利于积累影响力。</li><li>贡献文档：文档是一个项目很重要的部分，可以在 JIRA 中寻找并解决文档类的 issue。熟悉中英文的同学可以参与贡献中文翻译，可以搜索 <a href="https://issues.apache.org/jira/browse/FLINK-11567?jql=project%20%3D%20FLINK%20AND%20resolution%20%3D%20Unresolved%20AND%20component%20%20%3D%20chinese-translation%20%20ORDER%20BY%20createdDate%20%20ASC" target="_blank" rel="noopener">“chinese-translation” 的 issue</a>。</li><li>代码审查：Flink 每天都会在 GitHub 上收到很多 <a href="https://github.com/apache/flink/pulls" target="_blank" rel="noopener">Pull Request</a> 。帮助 review 代码也是对社区很重要的贡献。</li><li>还有很多参与贡献的方式，比如帮助测试RC版本，写Flink相关的博客等等。</li></ol><h2 id="如何提交第一个-Pull-Request"><a href="#如何提交第一个-Pull-Request" class="headerlink" title="如何提交第一个 Pull Request"></a>如何提交第一个 Pull Request</h2><h3 id="1-订阅-dev-邮件列表"><a href="#1-订阅-dev-邮件列表" class="headerlink" title="1. 订阅 dev 邮件列表"></a>1. 订阅 dev 邮件列表</h3><p>1.用自己的邮箱给 <a href="mailto:dev-subscribe@flink.apache.org" target="_blank" rel="noopener">dev-subscribe@flink.apache.org</a> 发送任意邮件。<br>2.收到官方确认邮件。<br>3.回复该邮件，内容随意，表示确认即可。<br>4.确认后，会收到一封欢迎邮件，表示订阅成功。</p><p>注：自2019年7月开始，经过社区讨论，将开始执行新的 JIRA workflow，不再需要去 dev 邮件列表申请 contributor 权限。</p><h3 id="2-在-JIRA-中挑选-issue"><a href="#2-在-JIRA-中挑选-issue" class="headerlink" title="2. 在 JIRA 中挑选 issue"></a>2. 在 JIRA 中挑选 issue</h3><p>如果有感兴趣的 JIRA，可以直接在 JIRA 下面留言，对于复杂的 issue，需要先阐明实现方案。然后会有 Committer/PMC assign issue 给你。</p><p>推荐从简单的开始做起。例如<a href="https://issues.apache.org/jira/browse/FLINK-11567?jql=project%20%3D%20FLINK%20AND%20resolution%20%3D%20Unresolved%20AND%20component%20%20%3D%20chinese-translation%20%20ORDER%20BY%20createdDate%20%20ASC" target="_blank" rel="noopener">中文翻译的issue</a>。</p><h3 id="3-本地开发代码"><a href="#3-本地开发代码" class="headerlink" title="3. 本地开发代码"></a>3. 本地开发代码</h3><p>认领了 issue 后建议尽快开始开发，本地的开发环境建议使用 IntelliJ IDEA。在开发过程中有几个注意点：</p><ul><li><strong>分支开发。</strong> 从最新的 master 分支切出一个开发分支用于 issue 开发。</li><li><strong>单 PR 单改动。</strong> 不要在 PR 中混入不相关的改动，不做无关的代码优化，不做无关的代码格式化。如果真有必要，可以另开 JIRA 解决。</li><li><strong>保证新代码能被单元测试覆盖到。</strong>如果原本的测试用例，无法覆盖到，则需要自己编写对应的单元测试。</li></ul><h3 id="4-创建-pull-request"><a href="#4-创建-pull-request" class="headerlink" title="4. 创建 pull request"></a>4. 创建 pull request</h3><p>在提交之前，先更新 master 分支，并通过 <code>git rebase -i master</code> 命令，将自己的提交置顶（也可以通过 IDEA &gt; VCS &gt; Git &gt; Rebase 可视化界面来做 rebase）。同时保证自己的提交信息中只有一个 commit，commit message 遵循规范格式。Commit 格式是 “[FLINK-XXX] [YYY] ZZZ”，其中 XXX 是 JIRA ID，YYY 是 component 名字，ZZZ 是 JIRA title。例如 <em>[FLINK-5385] [core] Add a helper method to create Row object</em>。</p><p>要创建一个 pull request，需要将这个开发分支推到自己 fork 的 Flink 仓库中。并在 fork 仓库页面（<code>https://github.com/&lt;your-user-name&gt;/flink</code>）点击 “Compare &amp; pull request” 或者 “New pull request” 按钮，开始创建一个 PR。确保 base 是 <code>apache/flink master</code>，head 是刚刚的开发分支。另外在编辑框中按提示提供尽可能丰富的PR描述，然后点击 “Create pull request”。</p><p><img src="https://img.alicdn.com/tfs/TB1tv.zFVzqK1RjSZSgXXcpAVXa-2874-1472.png" alt></p><h3 id="5-解决-code-review-反馈的问题和建议"><a href="#5-解决-code-review-反馈的问题和建议" class="headerlink" title="5. 解决 code review 反馈的问题和建议"></a>5. 解决 code review 反馈的问题和建议</h3><p>提交 PR 后会收到修改建议，只需要为这些修改 <strong>追加commit</strong> 就行，commit message 随意。<em>注意不要 rebase/squash commits。</em>追加 commit 能方便地看出距离上次的改动，而 rebase/squash 会导致 reviewer 不得不从头到尾重新看一遍 diff。</p><h3 id="6-Committer-merge-PR"><a href="#6-Committer-merge-PR" class="headerlink" title="6. Committer merge PR"></a>6. Committer merge PR</h3><p>当 PR 获得 Committer 的 +1 认可后，就可以等待被 merge 到主干分支了。merge 的工作会由 Committer 来完成，Committer 会将你的分支再次 rebase 到最新的master 之上，并将多个 commits 合并成一个，完善 commit 信息，做最后的测试检查，最后会 merge 到 master 。 </p><p>此时在 Flink 仓库的 commit 历史中就能看到自己的提交信息了。恭喜你成为了 code contributor！</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在我看来，成为 Apache Committer 的小窍门有几点：</p><ol start="0"><li>把项目看成自己的事情，自发地，有激情地去做贡献</li><li>保持活跃，持续贡献，耐心和平常心都很重要</li><li>专注一个模块，吃透该模块的源码和原理，成为某个模块的专家</li><li>提升个人的代码品位和质量，让他人信任你的代码</li><li>勇敢地在邮件列表中参与讨论</li></ol><p>希望通过本文能让大家了解到，成为 Contributor 并没有想象中那么难，成为 Committer 也不是不可能，只要怀有开源的热情，找到自己感兴趣的项目，在开源贡献中成长，持之以恒，付出总会有回报的。</p><p>成为 Apache Committer 不仅仅是一种光环和荣誉，更多的是一种责任，代表着社区的信任，期盼着你能为社区做更多的贡献。所以成为 Committer 远不是终点，而是一个更高起点，毕竟 Committer 之上还有 PMC 呢 ;-）。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;过去三年，我一直在为 Apache Flink 开源项目贡献，也在两年前成为了 Flink Committer。我在 Flink 社区成长的过程中受到过社区大神的很多指导，如今也有很多人在向我咨询如何能参与到开源社区中，如何能成为 Committer。这也是本文写作的初衷，希望能帮助更多人参与到开源社区中。&lt;/p&gt;
&lt;p&gt;本文将以 Apache Flink 为例，介绍如何参与社区贡献，如何成为 Apache Committer。&lt;/p&gt;
    
    </summary>
    
      <category term="个人成长" scheme="http://wuchong.me/categories/%E4%B8%AA%E4%BA%BA%E6%88%90%E9%95%BF/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Committer" scheme="http://wuchong.me/tags/Committer/"/>
    
  </entry>
  
  <entry>
    <title>聊聊Blink开源和Flink社区近况</title>
    <link href="http://wuchong.me/blog/2019/01/31/blink-opensource-and-flink-community/"/>
    <id>http://wuchong.me/blog/2019/01/31/blink-opensource-and-flink-community/</id>
    <published>2019-01-31T08:34:15.000Z</published>
    <updated>2022-08-03T06:46:44.501Z</updated>
    
    <content type="html"><![CDATA[<p>前几天 Blink 开源的消息刷了朋友圈，因为笔者一直关注着社区的发展。所以今天想从我个人的角度来聊聊 Blink 开源，社区 merge Blink 的计划，以及最近发生的一些很有意义的事情。</p><h2 id="Blink-开源"><a href="#Blink-开源" class="headerlink" title="Blink 开源"></a>Blink 开源</h2><p>这次 Blink 开源的主要目的是让社区的开发者们能尽早地尝试一些他们感兴趣的功能与改进。我觉得最核心的贡献包括：</p><ul><li><strong>Stream SQL 的新功能和性能优化：</strong>如维表Join、TopN、支持迟到数据和提早输出的Window、有效解决Agg数据倾斜问题等。</li><li><strong>完整且高性能的 Batch SQL：</strong>跑通全部 TPC-H/TPC-DS （比当前 Flink Batch SQL 快 10x 以上），能够读取 Hive meta 和 data 。</li><li><strong>易用性提升：</strong>焕然一新的Web UI，与 Zeppelin 的集成，交互式编程的支持。</li></ul><p>Stream SQL 上的功能补齐和性能优化经阿里内部多年千锤百炼打磨而来，毫无疑问是社区用户们最为翘首以盼的功能，笔者认为这部分的开源和回馈能迅速将 Flink SQL 的流式计算能力提升到高度成熟级别。</p><p>同时，Blink 对Batch SQL 上的完善和优化弥补了 Flink 长久以来在批处理能力上的不足，这也为未来Flink深度统一批流大业打下了更为坚实的基础。</p><p>另外值得一提的是这次Blink开源的新UI，是由 NG-ZORRO 的作者 vthinkxie 亲自操刀，一改以往 Apache 项目 Web UI 略为朴素的特点。新UI的简洁美观程度比起商业化产品可谓有过之而无不及，其易用性更是得到阿里内部的深度检验与高度认可。</p><p><img src="https://img.alicdn.com/tfs/TB1vmdhEMHqK1RjSZFgXXa7JXXa-1080-661.png" alt></p><p>回到 Blink 开源本身，要谈论其意义首先要清楚 Blink 与 Flink 的关系。从阿里多篇公开的报导中我们也了解到 “Blink 永远不会成为另外一个项目，如果后续进入 Apache 一定是成为 Flink 的一部分”，这点从 Blink 最后是以 Flink 的分支方式开源进一步得到了印证。另外 Blink 开源的核心改进将来都有希望合并进 Flink，这必将极大加速 Flink 的发展，同时也会为 Flink 打开更广阔的舞台。对于 Flink 用户来说，无疑也是个好消息，用户不仅能享受到更好用的产品，同时也拥有了更多想象的空间。相信未来几年的大数据领域，也会因此而变得格外精彩。</p><h2 id="Blink-merge-的计划"><a href="#Blink-merge-的计划" class="headerlink" title="Blink merge 的计划"></a>Blink merge 的计划</h2><p>接下来聊一个大家都格外关心的话题，Blink 的特性将如何合并到 Flink 中</p><p>合并 Blink 的方案是在社区公开讨论的，目前方案已经基本确定，从社区大神 Stephan 和 Timo 发起的方案讨论邮件中我们可以了解到合并的大致方案。</p><p>首先总体方针：尽可能地与目前的 Table API 保持兼容，使用户无感知地升级。</p><p>改动最大的两个模块会是：(1) SQL/Table Query Processor，(2) batch scheduling/failover/shuffle 。</p><p>对于 Query Processor（下文简称 QP），社区计划逐渐地构建起一个基于Blink的 QP，默认仍使用 Flink 原生的 QP，Blink QP 会以插件的方式存在，用户可以使用时可以配置成用 Blink QP 来执行（这非常像 Beam 的 runner 架构）。直到 Blink QP 完全合并进来了且稳定了，会作为默认的 QP 实现，而 Flink QP 最终会被移除。为了完成这个方案，flink-table 模块需要做一些模块的拆解和重构，为了尽快拆解完模块和重构，社区可能会短期内暂缓一些 Table API/SQL 的功能开发和贡献。更详细的方案可以看下 <a href="http://apache-flink-mailing-list-archive.1008284.n3.nabble.com/DISCUSS-FLIP-32-Restructure-flink-table-for-future-contributions-td26543.html" target="_blank" rel="noopener">FLIP-32</a>（见文末链接[3]）。目前这块相关的开发工作已经在进行中了。 </p><p>对于 batch scheduling &amp; failover 来说，目前已经有一个在进行中的调度重构方案了 FLINK-10429。当这个完成后，Blink 的实现可以以插件的方式作为新的 scheduler 和 failover handler 加入。在测试完备后可以作为默认的策略。对于 Shuffle Service，目前也已经有一个在讨论中的方案了：<a href="http://apache-flink-mailing-list-archive.1008284.n3.nabble.com/DISCUSS-Proposal-of-external-shuffle-service-td23988.html" target="_blank" rel="noopener">见文末链接[4]</a>。</p><h2 id="Flink-社区的近况"><a href="#Flink-社区的近况" class="headerlink" title="Flink 社区的近况"></a>Flink 社区的近况</h2><p>就个人的感受而言，最近 Flink 社区正在发生两件重要的事情。一是上文说的 Blink 开源，另一件就是社区对中文用户的日益重视。</p><h3 id="建立-Apache-Flink-中文邮件列表"><a href="#建立-Apache-Flink-中文邮件列表" class="headerlink" title="建立 Apache Flink 中文邮件列表"></a>建立 Apache Flink 中文邮件列表</h3><p>Apache Flink 社区最近为中文用户建立了官方的中文邮件列表，提供了一个官方的渠道给中文用户做问题交流。起因是社区在最近的统计中发现，过去三个月内访问 flink.apache.org 官方网站的用户中有 30% 来自中国。</p><p>感兴趣的用户可以通过下面的流程来订阅：</p><p>1、发送任意邮件到 <a href="mailto:user-zh-subscribe@flink.apache.org" target="_blank" rel="noopener">user-zh-subscribe@flink.apache.org</a><br>2、收到官方确认邮件<br>3、回复该邮件 confirm 即可订阅</p><p>订阅成功后将收到 Flink 官方的中文邮件列表的消息，就可以用中文在上面问问题和帮助别人解答问题了。</p><p>这件事对笔者触动比较大，因为 Apache 以往一向推崇使用英文交流，肯为中文用户单独开设中文邮件列表的顶级项目实为罕见，据我所知 Flink 是第一个。相信这个举动也是社区做的一次较大的创新和尝试，同时这也从侧面反映了来自中国的开源力量在世界的舞台上获得了越来越多的关注与尊重。同时也期待着可以有更多 Flink 中文社区志愿者参与到这个项目中来，共建社区生态，为中国的开源事业贡献一份力量。</p><h3 id="中文文档支持计划"><a href="#中文文档支持计划" class="headerlink" title="中文文档支持计划"></a>中文文档支持计划</h3><p>之前由中文社区的小伙伴们共同翻译的中文文档（托管在 <a href="https://flink-china.org/" target="_blank" rel="noopener">https://flink-china.org/</a> ），现在计划将贡献给 Apache Flink，并已在邮件列表中发起讨论。讨论的内容包括如何在 Flink 主干分支中同时维护中英文文档，如何做中英文文档同步和翻译，中文文档的地址和链接等等。更详细的支持计划可以看下 <a href="http://apache-flink-mailing-list-archive.1008284.n3.nabble.com/DISCUSS-Contributing-Chinese-website-and-docs-to-Apache-Flink-td26603.html" target="_blank" rel="noopener">链接[6]</a>。</p><p>文档是一个开源项目重要的组成部分，也是参与开源贡献、融入开源社区的一种方式。感兴趣的同学可以关注下这个邮件的动态，参与到后续的翻译贡献中去。</p><h3 id="全面拥抱中文用户"><a href="#全面拥抱中文用户" class="headerlink" title="全面拥抱中文用户"></a>全面拥抱中文用户</h3><p>从社区里发生的和正在发生的一些事情中可以看出，Apache Flink 社区正在拥抱中文用户，能将中文用户以如此高的优先级来支持的社区真不多。这对于国内用户来说无疑是一个积极的信号，同时这一系列举动也很可能会影响接下来几年国内大数据生态圈的变化。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="http://apache-flink-mailing-list-archive.1008284.n3.nabble.com/ANNOUNCE-Contributing-Alibaba-s-Blink-td26429.html" target="_blank" rel="noopener">[ANNOUNCE] Contributing Alibaba’s Blink</a></li><li><a href="http://apache-flink-mailing-list-archive.1008284.n3.nabble.com/DISCUSS-A-strategy-for-merging-the-Blink-enhancements-td26446.html" target="_blank" rel="noopener">[DISCUSS] A strategy for merging the Blink enhancements</a></li><li><a href="http://apache-flink-mailing-list-archive.1008284.n3.nabble.com/DISCUSS-FLIP-32-Restructure-flink-table-for-future-contributions-td26543.html" target="_blank" rel="noopener">[DISCUSS] FLIP-32: Restructure flink-table for future contributions</a></li><li><a href="http://apache-flink-mailing-list-archive.1008284.n3.nabble.com/DISCUSS-Proposal-of-external-shuffle-service-td23988.html" target="_blank" rel="noopener">[DISCUSS] Proposal of external shuffle service</a></li><li><a href="http://apache-flink-mailing-list-archive.1008284.n3.nabble.com/DISCUSS-Start-a-user-zh-flink-apache-org-mailing-list-for-the-Chinese-speaking-community-td26531.html" target="_blank" rel="noopener">[DISCUSS] Start a user-zh@flink.apache.org mailing list for the Chinese-speaking community?</a></li><li><a href="http://apache-flink-mailing-list-archive.1008284.n3.nabble.com/DISCUSS-Contributing-Chinese-website-and-docs-to-Apache-Flink-td26603.html" target="_blank" rel="noopener">[DISCUSS] Contributing Chinese website and docs to Apache Flink</a></li></ol>]]></content>
    
    <summary type="html">
    
      前几天 Blink 开源的消息刷了朋友圈，因为笔者一直关注着社区的发展。所以今天想从我个人的角度来聊聊 Blink 开源，社区 merge Blink 的计划，以及最近发生的一些很有意义的事情。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
  </entry>
  
  <entry>
    <title>Flink 小贴士 (7): 4个步骤，让 Flink 应用达到生产状态</title>
    <link href="http://wuchong.me/blog/2018/12/03/flink-tips-4-steps-flink-application-production-ready/"/>
    <id>http://wuchong.me/blog/2018/12/03/flink-tips-4-steps-flink-application-production-ready/</id>
    <published>2018-12-03T13:36:21.000Z</published>
    <updated>2022-08-03T06:46:44.505Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文：<a href="https://data-artisans.com/blog/4-steps-flink-application-production-ready" target="_blank" rel="noopener">https://data-artisans.com/blog/4-steps-flink-application-production-ready</a><br>作者：Nico Kruber, Markos Sfikas<br>译者：云邪（Jark）</p></blockquote><p>本文阐述了使 Flink 应用达到生产就绪状态所需要的一些配置步骤。在以下部分中，我们概述了重要的配置参数，这些参数是技术领导、DevOps、工程师们在将 Flink 应用程序上线生产之前都需要仔细考虑的。Apache Flink 为大多数配置都提供了开箱即用的默认选项，在许多情况下，它们是POC阶段（概念验证）或探索 Flink 不同 API 和抽象的很好的起点。</p><p>然而，将 Flink 应用程序投入生产还需要额外的配置，这些配置可以高效地调整应用的规模，使其达到生产就绪状态，并能与不同系统之间保持兼容，以保证未来迭代升级的需求。</p><p>下面几点是我们收集的需要在 Flink 应用上线前做的检查：</p><p><img src="https://img.alicdn.com/tfs/TB1qClwtNjaK1RjSZFAXXbdLFXa-1168-1125.png" alt></p><h2 id="1-明确定义-Flink-算子的最大并发度"><a href="#1-明确定义-Flink-算子的最大并发度" class="headerlink" title="1. 明确定义 Flink 算子的最大并发度"></a>1. 明确定义 Flink 算子的最大并发度</h2><p>Flink 的 keyed state 是由 <a href="https://ci.apache.org/projects/flink/flink-docs-stable/dev/stream/state/state.html#keyed-state" target="_blank" rel="noopener">key group</a> 进行组织的，并分布在 Flink 算子（operator）的各个并发实例上。Key group 是用来分布和影响 Flink 应用程序可扩展性的最小原子单元，每个算子上的 key group 个数即为最大并发数（maxParallelism），可以手动配置也可以直接使用默认配置。默认值粗略地使用 <code>operatorParallelism * 1.5</code> ，下限 128，上限 32768 。可以通过 <a href="https://ci.apache.org/projects/flink/flink-docs-release-1.6/dev/parallel.html#setting-the-maximum-parallelism" target="_blank" rel="noopener"><code>setMaxParallelism(int maxParallelism)</code></a> 来手动地设定作业或具体算子的最大并发。</p><p>任何进入生产的作业都应该指定最大并发数。但是，一定要仔细考虑后再决定该值的大小。因为一旦设置了最大并发度（<strong>无论是手动设置，还是默认设置</strong>），之后就无法再对该值做更新。想要改变一个作业的最大并发度，就只能将作业从全新的状态重新开始执行。目前还无法在更改最大并发度后，从上一个 checkpoint 或 savepoint 恢复。</p><p>最大并发度的取值建议设定一个足够高的值以满足应用未来的可扩展性和可用性，同时，又要选一个相对较低的值以避免影响应用程序整体的性能。这是由于一个很高的最大并发度会导致 Flink 需要维护大量的元数据（用于扩缩容），这可能会增加 Flink 应用程序的整体状态大小。</p><h2 id="2-为-Flink-算子指定唯一用户ID（UUID）"><a href="#2-为-Flink-算子指定唯一用户ID（UUID）" class="headerlink" title="2. 为 Flink 算子指定唯一用户ID（UUID）"></a>2. 为 Flink 算子指定唯一用户ID（UUID）</h2><p>对于有状态的 Flink 应用，推荐给每个算子都<a href="https://ci.apache.org/projects/flink/flink-docs-release-1.6/ops/state/savepoints.html#should-i-assign-ids-to-all-operators-in-my-job" target="_blank" rel="noopener">指定唯一用户ID（UUID）</a>。 严格地说，仅需要给有状态的算子设置就足够了。但是因为 Flink 的某些内置算子（如 window）是有状态的，而有些是无状态的，可能用户不是很清楚哪些内置算子是有状态的，哪些不是。所以从实践经验上来说，我们建议每个算子都指定上 UUID。</p><p>Flink 算子的 UUID 可以通过 <code>uid(String uid)</code> 方法指定。算子 UUID 使得 Flink 有效地将算子的状态从 savepoint 映射到作业修改后（拓扑图可能也有改变）的正确的算子上，这是 <a href="https://data-artisans.com/blog/turning-back-time-savepoints" target="_blank" rel="noopener">savepoint</a> 在 Flink 应用中正常工作的一个基本要素。</p><h2 id="3-充分考虑-Flink-程序的状态后端"><a href="#3-充分考虑-Flink-程序的状态后端" class="headerlink" title="3. 充分考虑 Flink 程序的状态后端"></a>3. 充分考虑 Flink 程序的状态后端</h2><p>当前 Flink 还不支持状态后端之间的互换功能，也就是当我们用内存状态后端做了一个 savepoint，我们无法把作业改成 RocksDB 状态后端然后恢复。所以，开发人员和工程负责人在将作业投向生产之前要仔细考虑好该 Flink 应用的最合适的状态后端类型。</p><p>关于 Flink 当前支持的三种不同的状态后端类型，可以阅读我们的上一篇文章：<a href="http://wuchong.me/blog/2018/11/21/flink-tips-how-to-choose-state-backends/">《Flink 小贴士 (4): 如何选择状态后端》</a></p><p>对于生产用例来说，强烈建议使用 RocksDB 状态后端，因为这是目前唯一一种支持大型状态和异步操作（如快照过程）的状态后端，异步操作能使 Flink 不阻塞正常数据流的处理的情况下做快照操作。另一方面，使用 RocksDB 状态后端可能存在性能折衷，因为所有状态访问和检索都需要序列化（和反序列化）来跨越 JNI 边界，这与内存状态后端相比可能会影响应用程序的吞吐量。</p><h2 id="4-配置-JobManager-的高可用性（HA）"><a href="#4-配置-JobManager-的高可用性（HA）" class="headerlink" title="4. 配置 JobManager 的高可用性（HA）"></a>4. 配置 JobManager 的高可用性（HA）</h2><p>高可用性（HA）配置确保了 Flink 应用中 JobManager 组件发生潜在故障后的自动恢复，从而将停机时间降到最低。JobManager 的主要职责是协调 Flink 的部署，例如调度和适当的资源分配。</p><p>默认情况下，Flink 为每个集群设置一个 JobManager 实例。这会导致单点故障（SPOF）：如果 JobManager 崩溃了，则无法提交新的作业，而且正在运行的程序也会失败。因此，强烈建议为生产用例<a href="https://ci.apache.org/projects/flink/flink-docs-stable/ops/config.html#high-availability-ha" target="_blank" rel="noopener">配置高可用性（HA）</a>。</p><p>上述 4 个步骤总结自社区的最佳实践，使得 Flink 应用能够保持状态的同时任意地扩缩容，处理更大规模的数据和状态，并提高系统的可用性。我们强烈建议您在将应用投入生产之前，仔细阅读上述步骤。</p>]]></content>
    
    <summary type="html">
    
      本文阐述了使 Flink 应用达到生产就绪状态所需要的一些配置步骤。在以下部分中，我们概述了重要的配置参数，这些参数是技术领导、DevOps、工程师们在将 Flink 应用程序上线生产之前都需要仔细考虑的。Apache Flink 为大多数配置都提供了开箱即用的默认选项，在许多情况下，它们是POC阶段（概念验证）或探索 Flink 不同 API 和抽象的很好的起点。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink小贴士" scheme="http://wuchong.me/tags/Flink%E5%B0%8F%E8%B4%B4%E5%A3%AB/"/>
    
  </entry>
  
  <entry>
    <title>Flink 小贴士 (6): 使用 Broadcast State 的 4 个注意事项</title>
    <link href="http://wuchong.me/blog/2018/11/28/flink-tips-broadcast-state-pattern-flink-considerations/"/>
    <id>http://wuchong.me/blog/2018/11/28/flink-tips-broadcast-state-pattern-flink-considerations/</id>
    <published>2018-11-28T00:21:40.000Z</published>
    <updated>2022-08-03T06:46:44.505Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文：<a href="https://data-artisans.com/blog/broadcast-state-pattern-flink-considerations" target="_blank" rel="noopener">https://data-artisans.com/blog/broadcast-state-pattern-flink-considerations</a><br>作者：Markos Sfikas<br>译者：云邪（Jark）</p></blockquote><p>在 Apache Flink 1.5.0 中引入了广播状态（Broadcast State）。本文将描述什么是广播状态模式（Broadcast State Pattern），广播状态与其他的 Operator State 有什么区别，最后，我们在 Flink 中使用该功能时需要考虑的一些重要的注意事项。</p><h2 id="什么是广播状态模式"><a href="#什么是广播状态模式" class="headerlink" title="什么是广播状态模式"></a>什么是广播状态模式</h2><p>广播状态模式指的一种流应用程序，其中低吞吐量的事件流（例如，包含一组规则）被广播到某个 operator 的所有并发实例中，然后针对来自另一条原始数据流中的数据（例如金融或信用卡交易）进行计算。 广播状态模式的一些典型应用案例如下：</p><ul><li>动态规则：例如，有一个规则：当某个交易超过100万美元时需要发一个警报。我们将这个规则广播到计算交易的算子的所有并发实例中。</li><li>数据丰富：例如，将用户的详细信息作业广播状态进行广播，对包含用户ID的交易数据流进行数据丰富。</li></ul><p>为了实现这样的应用，关键组件是广播状态，我们将在下文详细描述。</p><h2 id="什么是广播状态？"><a href="#什么是广播状态？" class="headerlink" title="什么是广播状态？"></a>什么是广播状态？</h2><p>广播状态是 Apache Flink 中支持的第三种类型的 operator state。广播状态使得 Flink 用户能够以容错、一致、可扩缩容地将来自广播的低吞吐的事件流数据存储下来。来自另一条数据流的事件可以流经同一 operator 的各个并发实例，并与广播状态中的数据一起处理。有关其他类型的状态，以及如何使用请访问 <a href="https://ci.apache.org/projects/flink/flink-docs-stable/dev/stream/state/state.html" target="_blank" rel="noopener">Flink 官方文档</a>。</p><p>广播状态与其他 operator state 之间有三个主要区别。与其余的 operator state 相反，广播状态：</p><ul><li>Map 的格式</li><li>有一条广播的输入流</li><li>operator 可以有多个不同名字的广播状态</li></ul><p>可以查阅我们之前的博客文章，探索 <a href="https://data-artisans.com/blog/a-practical-guide-to-broadcast-state-in-apache-flink" target="_blank" rel="noopener">Apache Flink 中使用广播状态的实践指南</a>。</p><h2 id="重要注意事项"><a href="#重要注意事项" class="headerlink" title="重要注意事项"></a>重要注意事项</h2><p>对于急切开始使用广播状态的 Flink 用户，Apache Flink 官方文档提供了有关 API 的详细指南，以及在应用程序中如何使用该功能。在使用广播状态时要记住以下4个重要事项：</p><ul><li><p>使用广播状态，operator task 之间不会相互通信</p><p>这也是为什么<code>(Keyed)-BroadcastProcessFunction</code>上只有广播的一边可以修改广播状态的内容。用户必须保证所有 operator 并发实例上对广播状态的修改行为都是一致的。或者说，如果不同的并发实例拥有不同的广播状态内容，将导致不一致的结果。</p></li><li><p>广播状态中事件的顺序在各个并发实例中可能不尽相同</p><p>虽然广播流的元素保证了将所有元素（最终）都发给下游所有的并发实例，但是元素的到达的顺序可能在并发实例之间并不相同。因此，对广播状态的修改不能依赖于输入数据的顺序。</p></li><li><p>所有 operator task 都会快照下他们的广播状态</p><p>在 checkpoint 时，所有的 task 都会 checkpoint 下他们的广播状态，并不仅仅是其中一个，即使所有 task 在广播状态中存储的元素是一模一样的。这是一个设计倾向，为了避免在恢复期间从单个文件读取而造成热点。然而，随着并发度的增加，checkpoint 的大小也会随之增加，这里会存在一个并发因子 p 的权衡。Flink 保证了在恢复/扩缩容时不会出现重复数据和少数据。在以相同或更小并行度恢复时，每个 task 会读取其对应的检查点状态。在已更大并行度恢复时，每个 task 读取自己的状态，剩余的 task （p_new-p_old）会以循环方式（round-robin）读取检查点的状态。</p></li><li><p>RocksDB 状态后端目前还不支持广播状态</p><p>广播状态目前在运行时保存在内存中。因为当前，RocksDB 状态后端还不适用于 operator state。Flink 用户应该相应地为其应用程序配置足够的内存。</p></li></ul><p><img src="https://img.alicdn.com/tfs/TB1zqRar5LaK1RjSZFxXXamPFXa-818-579.png" alt></p>]]></content>
    
    <summary type="html">
    
      在 Apache Flink 1.5.0 中引入了广播状态（Broadcast State）。本文将描述什么是广播状态模式（Broadcast State Pattern），广播状态与其他的 Operator State 有什么区别，最后，我们在 Flink 中使用该功能时需要考虑的一些重要的注意事项。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink小贴士" scheme="http://wuchong.me/tags/Flink%E5%B0%8F%E8%B4%B4%E5%A3%AB/"/>
    
  </entry>
  
  <entry>
    <title>Flink 小贴士 (5): Savepoint 和 Checkpoint 的 3 个不同点</title>
    <link href="http://wuchong.me/blog/2018/11/25/flink-tips-differences-between-savepoints-and-checkpoints/"/>
    <id>http://wuchong.me/blog/2018/11/25/flink-tips-differences-between-savepoints-and-checkpoints/</id>
    <published>2018-11-25T14:21:40.000Z</published>
    <updated>2022-08-03T06:46:44.506Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文：<a href="https://data-artisans.com/blog/differences-between-savepoints-and-checkpoints-in-flink" target="_blank" rel="noopener">https://data-artisans.com/blog/differences-between-savepoints-and-checkpoints-in-flink</a><br>作者：Stefan Richter, Dawid Wysakowicz, Markos Sfikas<br>译者：云邪（Jark）</p></blockquote><p>在本文中，我们将阐述 Savepoint 和 Checkpoint 是什么，它们主要用在什么时候，以及对比它们的主要区别。</p><h2 id="什么是-Savepoint-和-Checkpoint"><a href="#什么是-Savepoint-和-Checkpoint" class="headerlink" title="什么是 Savepoint 和 Checkpoint"></a>什么是 Savepoint 和 Checkpoint</h2><p>Savepoint 是用来为整个流应用程序在某个“时间点”（point-in-time）的生成快照的功能。该快照包含了输入源的位置信息，数据源读取到的偏移量（offset），以及整个应用的状态。借助 Chandy-Lamport 算法的变体，我们可以无需停止应用程序而得到一致的快照。Savepoint 包含了两个主要元素：</p><ol><li>首先，Savepoint 包含了一个目录，其中包含（通常很大的）二进制文件，这些文件表示了整个流应用在 Checkpoint/Savepoint 时的状态。</li><li>以及一个（相对较小的）元数据文件，包含了指向 Savapoint 各个文件的指针，并存储在所选的分布式文件系统或数据存储中。</li></ol><p>上述有关 Savepoint 的介绍听起来和之前文章中介绍的 Checkpoint 很像。Checkpoint 是 Flink 用来从故障中恢复的机制，快照下了整个应用程序的状态，当然也包括输入源读取到的位点。如果发生故障，Flink 将通过从 Checkpoint 加载应用程序状态并从恢复的读取位点继续应用程序的处理，就像什么事情都没发生一样。</p><p>可以阅读之前 Flink 小贴士的一篇关于 <a href="http://wuchong.me/blog/2018/11/04/how-apache-flink-manages-kafka-consumer-offsets/">Flink 如何管理 Kafka 消费位点</a>的文章。</p><h2 id="Savepoint-和-Checkpoint-的-3-个不同点"><a href="#Savepoint-和-Checkpoint-的-3-个不同点" class="headerlink" title="Savepoint 和 Checkpoint 的 3 个不同点"></a>Savepoint 和 Checkpoint 的 3 个不同点</h2><p>Savepoint 和 Checkpoint 是 Apache Flink 作为流处理框架非常独特的两个特性。Savepoint 和 Checkpoint 在实现中看起来也很相似，但是，这两个功能主要有以下3个不同点：</p><p><img src="https://img.alicdn.com/tfs/TB1mvoErMHqK1RjSZFkXXX.WFXa-881-1060.png" alt></p><p><strong>目标：</strong>从概念上讲，Flink 的 Savepoint 和 Checkpoint 的不同之处很像传统数据库中备份与恢复日志之间的区别。Checkpoint 的主要目标是充当 Flink 中的恢复机制，确保能从潜在的故障中恢复。相反，Savepoint 的主要目标是充当手动备份、恢复暂停作业的方法。</p><p><strong>实现：</strong>Checkpoint 和 Savepoint 在实现上也有不同。Checkpoint 被设计成轻量和快速的机制。它们可能（但不一定必须）利用底层状态后端的不同功能尽可能快速地恢复数据。例如，基于 RocksDB 状态后端的增量检查点，能够加速 RocksDB 的 checkpoint 过程，这使得 checkpoint 机制变得更加轻量。相反，Savepoint 旨在更多地关注数据的可移植性，并支持对作业做任何更改而状态能保持兼容，这使得生成和恢复的成本更高。</p><p><strong>生命周期：</strong>Checkpoint 是自动和定期的，它们由 Flink 自动地周期性地创建和删除，无需用户的交互。相反，Savepoint 是由用户手动地管理（调度、创建、删除）的。</p><h2 id="何时使用-Savepoint"><a href="#何时使用-Savepoint" class="headerlink" title="何时使用 Savepoint ?"></a>何时使用 Savepoint ?</h2><p>虽然流式应用程序处理的数据是持续地生成的（“运动中”的数据），但是存在着想要重新处理之前已经处理过的数据的情况。Savepoint 可以在以下情况下使用：</p><ul><li>部署流应用的一个新版本，包括新功能、BUG 修复、或者一个更好的机器学习模型</li><li>引入 A/B 测试，使用相同的源数据测试程序的不同版本，从同一时间点开始测试而不牺牲先前的状态</li><li>在需要更多资源时扩容应用程序</li><li>迁移流应用程序到 Flink 的新版本上，或者迁移到另一个集群</li></ul><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>Checkpoint 和 Savepoint 是 Flink 中两个不同的功能，它们满足了不同的需求，以确保一致性、容错性，和满足作业升级、BUG 修复、迁移、A/B测试等。这两个功能相结合，可以确保应用程序的状态在不同的场景和环境中保持不变。</p>]]></content>
    
    <summary type="html">
    
      在本文中，我们将阐述 Savepoint 和 Checkpoint 是什么，它们主要用在什么时候，以及对比它们的主要区别。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink小贴士" scheme="http://wuchong.me/tags/Flink%E5%B0%8F%E8%B4%B4%E5%A3%AB/"/>
    
  </entry>
  
  <entry>
    <title>Flink 小贴士 (4): 如何选择状态后端</title>
    <link href="http://wuchong.me/blog/2018/11/21/flink-tips-how-to-choose-state-backends/"/>
    <id>http://wuchong.me/blog/2018/11/21/flink-tips-how-to-choose-state-backends/</id>
    <published>2018-11-21T04:35:20.000Z</published>
    <updated>2022-08-03T06:46:44.506Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文：<a href="https://data-artisans.com/blog/stateful-stream-processing-apache-flink-state-backends" target="_blank" rel="noopener">https://data-artisans.com/blog/stateful-stream-processing-apache-flink-state-backends</a><br>作者：Seth Wiesman, Markos Sfikas<br>译者：云邪（Jark）</p></blockquote><p>本文我们将深入探讨有状态的流处理，更确切地说是 Apache Flink 中不同的状态后端（state backend）。在以下部分，我们将介绍 Apache Flink 的 3 种状态后端，它们的局限性以及根据具体案例需求选择最合适的状态后端。</p><p>在有状态的流处理中，当开发人员启用了 Flink 中的 checkpoint 机制，那么状态将会持久化以防止数据的丢失并确保发生故障时能够完全恢复。选择何种状态后端，将决定状态持久化的方式和位置。</p><p>Flink 提供了三种可用的状态后端：<code>MemoryStateBackend</code>，<code>FsStateBackend</code>，和<code>RocksDBStateBackend</code>。</p><p><img src="https://img.alicdn.com/tfs/TB19tVmqwHqK1RjSZFkXXX.WFXa-1158-1112.png" alt></p><a id="more"></a><h2 id="MemoryStateBackend"><a href="#MemoryStateBackend" class="headerlink" title="MemoryStateBackend"></a>MemoryStateBackend</h2><p><code>MemoryStateBackend</code> 是将状态维护在 Java 堆上的一个内部状态后端。键值状态和窗口算子使用哈希表来存储数据（values）和定时器（timers）。当应用程序 checkpoint 时，此后端会在将状态发给 JobManager 之前快照下状态，JobManager 也将状态存储在 Java 堆上。默认情况下，MemoryStateBackend 配置成支持异步快照。异步快照可以避免阻塞数据流的处理，从而避免反压的发生。</p><p>使用 MemoryStateBackend 时的注意点：</p><ul><li>默认情况下，每一个状态的大小限制为 5 MB。可以通过 MemoryStateBackend 的构造函数增加这个大小。</li><li>状态大小受到 akka 帧大小的限制，所以无论怎么调整状态大小配置，都不能大于 akka 的帧大小。也可以通过 <code>akka.framesize</code> 调整 akka 帧大小（通过<a href="https://ci.apache.org/projects/flink/flink-docs-release-1.7/ops/config.html" target="_blank" rel="noopener">配置文档</a>了解更多）。</li><li>状态的总大小不能超过 JobManager 的内存。</li></ul><p>何时使用 MemoryStateBackend：</p><ul><li>本地开发或调试时建议使用 MemoryStateBackend，因为这种场景的状态大小的是有限的。</li><li>MemoryStateBackend 最适合小状态的应用场景。例如 <a href="http://wuchong.me/blog/2018/11/04/how-apache-flink-manages-kafka-consumer-offsets/">Kafka consumer</a>，或者一次仅一记录的函数 （Map, FlatMap，或 Filter）。</li></ul><h2 id="FsStateBackend"><a href="#FsStateBackend" class="headerlink" title="FsStateBackend"></a>FsStateBackend</h2><p>FsStateBackend 需要配置的主要是文件系统，如 URL（类型，地址，路径）。举个例子，比如可以是：</p><ul><li>“hdfs://namenode:40010/flink/checkpoints” 或</li><li>“s3://flink/checkpoints”</li></ul><p>当选择使用 FsStateBackend 时，正在进行的数据会被存在 TaskManager 的内存中。在 checkpoint 时，此后端会将状态快照写入配置的文件系统和目录的文件中，同时会在 JobManager 的内存中（在高可用场景下会存在 Zookeeper 中）存储极少的元数据。</p><p>默认情况下，FsStateBackend 配置成提供异步快照，以避免在状态 checkpoint 时阻塞数据流的处理。该特性可以实例化 FsStateBackend 时传入 false 的布尔标志来禁用掉，例如：</p><figure class="highlight haxe"><table><tr><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="type">FsStateBackend</span>(path, <span class="literal">false</span>);</span><br></pre></td></tr></table></figure><p>使用 FsStateBackend 时的注意点：</p><ul><li>当前的状态仍然会先存在 TaskManager 中，所以状态的大小不能超过 TaskManager 的内存。</li></ul><p>何时使用 FsStateBackend：</p><ul><li>FsStateBackend 适用于处理大状态，长窗口，或大键值状态的有状态处理任务。</li><li>FsStateBackend 非常适合用于高可用方案。</li></ul><h2 id="RocksDBStateBackend"><a href="#RocksDBStateBackend" class="headerlink" title="RocksDBStateBackend"></a>RocksDBStateBackend</h2><p>RocksDBStateBackend 的配置也需要一个文件系统（类型，地址，路径），如下所示：</p><ul><li>“hdfs://namenode:40010/flink/checkpoints” 或</li><li>“s3://flink/checkpoints”</li></ul><p>RocksDB 是一种嵌入式的本地数据库。RocksDBStateBackend 将处理中的数据使用 RocksDB 存储在本地磁盘上。在 checkpoint 时，整个 RocksDB 数据库会被存储到配置的文件系统中，或者在超大状态作业时可以将增量的数据存储到配置的文件系统中。同时 Flink 会将极少的元数据存储在 JobManager 的内存中，或者在 Zookeeper 中（对于高可用的情况）。RocksDB 默认也是配置成异步快照的模式。</p><p>使用 RocksDBStateBackend 时的注意点：</p><ul><li>RocksDB 支持的单 key 和单 value 的大小最大为每个 2^31 字节。这是因为 RocksDB 的 JNI API 是基于 byte[] 的。</li><li>我们需要强调的是，对于使用具有合并操作的状态的应用程序，例如 ListState，随着时间可能会累积到超过 2^31 字节大小，这将会导致在接下来的查询中失败。</li></ul><p>何时使用 RocksDBStateBackend：</p><ul><li>RocksDBStateBackend 最适合用于处理大状态，长窗口，或大键值状态的有状态处理任务。</li><li>RocksDBStateBackend 非常适合用于高可用方案。</li><li>RocksDBStateBackend 是目前唯一支持增量 checkpoint 的后端。增量 checkpoint 非常使用于超大状态的场景。</li></ul><p>当使用 RocksDB 时，状态大小只受限于磁盘可用空间的大小。这也使得 RocksDBStateBackend 成为管理超大状态的最佳选择。使用 RocksDB 的权衡点在于所有的状态相关的操作都需要序列化（或反序列化）才能跨越 JNI 边界。与上面提到的堆上后端相比，这可能会影响应用程序的吞吐量。</p><p>不同状态后端满足不同场景的需求，在开始开发应用程序之前应该仔细考虑和规划后选择。这可确保选择了正确的状态后端以最好地满足应用程序和业务需求。</p>]]></content>
    
    <summary type="html">
    
      本文我们将深入探讨有状态的流处理，更确切地说是 Apache Flink 中不同的状态后端（state backend）。在以下部分，我们将介绍 Apache Flink 的 3 种状态后端，它们的局限性以及根据具体案例需求选择最合适的状态后端。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink小贴士" scheme="http://wuchong.me/tags/Flink%E5%B0%8F%E8%B4%B4%E5%A3%AB/"/>
    
      <category term="StateBackend" scheme="http://wuchong.me/tags/StateBackend/"/>
    
  </entry>
  
  <entry>
    <title>Flink 小贴士 (3): 轻松理解 Watermark</title>
    <link href="http://wuchong.me/blog/2018/11/18/flink-tips-watermarks-in-apache-flink-made-easy/"/>
    <id>http://wuchong.me/blog/2018/11/18/flink-tips-watermarks-in-apache-flink-made-easy/</id>
    <published>2018-11-18T13:03:49.000Z</published>
    <updated>2022-08-03T06:46:44.506Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文：<a href="https://data-artisans.com/blog/watermarks-in-apache-flink-made-easy" target="_blank" rel="noopener">https://data-artisans.com/blog/watermarks-in-apache-flink-made-easy</a><br>作者：David Anderson<br>译者：云邪（Jark）</p></blockquote><p>当人们第一次使用 Flink 时，经常会对 watermark 感到困惑。但其实 watermark 并不复杂。让我们通过一个简单的例子来说明为什么我们需要 watermark，以及它的工作机制是什么样的。</p><h2 id="在-Apache-Flink-中使用-watermark-的-4-个理解"><a href="#在-Apache-Flink-中使用-watermark-的-4-个理解" class="headerlink" title="在 Apache Flink 中使用 watermark 的 4 个理解"></a>在 Apache Flink 中使用 watermark 的 4 个理解</h2><p>在下文中的例子中，我们有一个带有时间戳的事件流，但是由于某种原因它们并不是按顺序到达的。图中的数字代表事件发生的时间戳。第一个到达的事件发生在时间 4，然后它后面跟着的是发生在更早时间（时间 2）的事件，以此类推：</p><p><img src="https://img.alicdn.com/tfs/TB1W0UdqcfpK1RjSZFOXXa6nFXa-684-133.png" alt></p><p>注意这是一个按照事件时间处理的例子，这意味着时间戳反映的是事件发生的时间，而不是处理事件的时间。事件时间（Event-Time）处理的强大之处在于，无论是在处理实时的数据还是重新处理历史的数据，基于事件时间创建的流计算应用都能保证结果是一样的。</p><p><em>注：可以访问<a href="https://ci.apache.org/projects/flink/flink-docs-stable/dev/event_time.html" target="_blank" rel="noopener"> Apache Flink 文档</a>，了解更多有关时间的概念，如 event-time, processing-time, ingestion-time。</em></p><p>现在假设我们正在尝试创建一个流计算排序算子。也就是处理一个乱序到达的事件流，并按照事件时间的顺序输出事件。</p><h2 id="理解-1"><a href="#理解-1" class="headerlink" title="理解 #1:"></a>理解 #1:</h2><p>数据流中的第一个元素的时间是 4，但是我们不能直接将它作为排序后数据流的第一个元素并输出它。因为数据是乱序到达的，也许有一个更早发生的数据还没有到达。事实上，我们能预见一些这个流的未来，也就是我们的排序算子至少要等到 2 这条数据的到达再输出结果。</p><p><strong><em>有缓存，就必然有延迟。</em></strong></p><h2 id="理解-2"><a href="#理解-2" class="headerlink" title="理解 #2:"></a>理解 #2:</h2><p>如果我们做错了，我们可能会永远等待下去。首先，我们的应用程序从看到时间 4 的数据，然后看到时间 2 的数据。是否会有一个比时间 2 更早的数据到达呢？也许会，也许不会。我们可以一直等下去，但可能永远看不到 1 。</p><p><strong><em>最终，我们必须勇敢地输出 2 作为排序流的第一个结果。</em></strong></p><h2 id="理解-3"><a href="#理解-3" class="headerlink" title="理解 #3:"></a>理解 #3:</h2><p>我们需要的是某种策略，它定义了对于任何带时间戳的事件流，何时停止等待更早数据的到来。</p><p><strong><em>这正是 watermark 的作用，他们定义了何时不再等待更早的数据。</em></strong></p><p>Flink 中的事件时间处理依赖于一种特殊的带时间戳的元素，成为 watermark，它们会由数据源或是 watermark 生成器插入数据流中。具有时间戳 <code>t</code> 的 watermark 可以被理解为断言了所有时间戳<strong>小于或等于</strong> <code>t</code> 的事件都（在某种合理的概率上）已经到达了。</p><blockquote><p>译注：此处原文是“小于”，译者认为应该是 “小于或等于”，因为 Flink 源码中采用的是 “小于或等于” 的机制。</p></blockquote><p>何时我们的排序算子应该停止等待，然后将事件 2 作为首个元素输出？答案是当收到时间戳为 2（或更大）的 watermark 时。</p><h2 id="理解-4"><a href="#理解-4" class="headerlink" title="理解 #4:"></a>理解 #4:</h2><p><strong><em>我们可以设想不同的策略来生成 watermark。</em></strong></p><p>我们知道每个事件都会延迟一段时间才到达，而这些延迟差异会比较大，所以有些事件会比其他事件延迟更多。一种简单的方法是假设这些延迟不会超过某个最大值。Flink 把这种策略称作 “有界无序生成策略”（bounded-out-of-orderness）。当然也有很多更复杂的方式去生成 watermark，但是对于大多数应用来说，固定延迟的方式已经足够了。</p><p>如果想要构建一个类似排序的流应用，可以使用 Flink 的 <code>ProcessFunction</code>。它提供了对事件时间计时器（基于 watermark 触发回调）的访问，还提供了可以用来缓存数据的托管状态接口。</p><p>如果想要了解更多有关 Apache Flink 的 <code>ProcessFunction</code> 的实践案例，可以访问我的上一篇文章<a href="http://wuchong.me/blog/2018/11/07/use-flink-calculate-hot-items/">《Flink 零基础实战教程：如何计算实时热门商品》</a> 了解如何使用 <code>ProcessFunction</code> 实现 TopN 功能。</p>]]></content>
    
    <summary type="html">
    
      当人们第一次使用 Flink 时，经常会对 watermark 感到困惑。但其实 watermark 并不复杂。让我们通过一个简单的例子来说明为什么我们需要 watermark，以及它的工作机制是什么样的。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink小贴士" scheme="http://wuchong.me/tags/Flink%E5%B0%8F%E8%B4%B4%E5%A3%AB/"/>
    
      <category term="watermark" scheme="http://wuchong.me/tags/watermark/"/>
    
  </entry>
  
  <entry>
    <title>一文了解 Apache Flink 核心技术</title>
    <link href="http://wuchong.me/blog/2018/11/09/flink-tech-evolution-introduction/"/>
    <id>http://wuchong.me/blog/2018/11/09/flink-tech-evolution-introduction/</id>
    <published>2018-11-09T07:41:49.000Z</published>
    <updated>2022-08-03T06:46:44.505Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>作者：云邪（Jark）<br>原文链接：<a href="http://wuchong.me/blog/2018/11/09/flink-tech-evolution-introduction/">http://wuchong.me/blog/2018/11/09/flink-tech-evolution-introduction/</a></p></blockquote><h2 id="Apache-Flink-介绍"><a href="#Apache-Flink-介绍" class="headerlink" title="Apache Flink 介绍"></a>Apache Flink 介绍</h2><p>Apache Flink 是近年来越来越流行的一款开源大数据计算引擎，它同时支持了批处理和流处理，也能用来做一些基于事件的应用。使用官网的一句话来介绍 Flink 就是 <strong>“Stateful Computations Over Streams”</strong>。</p><p><strong>首先</strong> Flink 是一个<strong>纯流式的计算引擎</strong>，它的基本数据模型是数据流。流可以是无边界的无限流，即一般意义上的流处理。也可以是有边界的有限流，这样就是批处理。因此 Flink 用一套架构同时支持了流处理和批处理。<strong>其次</strong>，Flink 的一个优势是支持<strong>有状态的计算</strong>。如果处理一个事件（或一条数据）的结果只跟事件本身的内容有关，称为无状态处理；反之结果还和之前处理过的事件有关，称为有状态处理。稍微复杂一点的数据处理，比如说基本的聚合，数据流之间的关联都是有状态处理。</p><p><img src="https://img.alicdn.com/tfs/TB1Ta88oyLaK1RjSZFxXXamPFXa-1627-845.png" alt></p><a id="more"></a><h2 id="Apache-Flink-基石"><a href="#Apache-Flink-基石" class="headerlink" title="Apache Flink 基石"></a>Apache Flink 基石</h2><p>Apache Flink 之所以能越来越受欢迎，我们认为离不开它最重要的四个基石：Checkpoint、State、Time、Window。</p><p>首先是Checkpoint机制，这是 Flink 最重要的一个特性。Flink 基于 Chandy-Lamport 算法实现了分布式一致性的快照，从而提供了 exactly-once 的语义。在 Flink 之前的流计算系统（如 Strom，Samza）都没有很好地解决 exactly-once 的问题。提供了一致性的语义之后，Flink 为了让用户在编程时能够更轻松、更容易地去管理状态，引入了托管状态（managed state）并提供了 API 接口，让用户使用起来感觉就像在用 Java 的集合类一样。除此之外，Flink 还实现了 watermark 的机制，解决了基于事件时间处理时的数据乱序和数据迟到的问题。最后，流计算中的计算一般都会基于窗口来计算，所以 Flink 提供了一套开箱即用的窗口操作，包括滚动窗口、滑动窗口、会话窗口，还支持非常灵活的自定义窗口以满足特殊业务的需求。</p><h2 id="Flink-API-历史变迁"><a href="#Flink-API-历史变迁" class="headerlink" title="Flink API 历史变迁"></a>Flink API 历史变迁</h2><p><img src="https://img.alicdn.com/tfs/TB1ksgRokvoK1RjSZFNXXcxMVXa-1481-566.png" alt></p><p><strong>在 Flink 1.0.0 时期</strong>，加入了 State API，即 ValueState、ReducingState、ListState 等等。State API 可以认为是 Flink 里程碑式的创新，它能够让用户像使用 Java 集合一样地使用 Flink State，却能够自动享受到状态的一致性保证，不会因为故障而丢失状态。包括后来 Apache Beam 的 State API 也从中借鉴了很多。</p><p><strong>在 Flink 1.1.0 时期</strong>，支持了 Session Window 以及迟到数据容忍的功能。</p><p><strong>在 Flink 1.2.0 时期</strong>，提供了 ProcessFunction，这是一个 Lower-level 的API，用于实现更高级更复杂的功能。它除了能够注册各种类型的 State 外，还支持注册定时器（支持 EventTime 和 ProcessingTime），常用于开发一些基于事件、基于时间的应用程序。 </p><p><strong>在 Flink 1.3.0 时期</strong>，提供了 Side Output 功能。算子的输出一般只有一种输出类型，但是有些时候可能需要输出另外的类型，比如除了输出主流外，还希望把一些异常数据、迟到数据以侧边流的形式进行输出，并分别交给下游不同节点进行处理。简而言之，Side Output 支持了多路输出的功能。</p><p><strong>在 Flink 1.5.0 时期</strong>，加入了BroadcastState。BroadcastState是对 State API 的一个扩展。它用来存储上游被广播过来的数据，这个 operator 的每个并发上存的BroadcastState里面的数据都是一模一样的，因为它是从上游广播而来的。基于这种State可以比较好地去解决 CEP 中的动态规则的功能，以及 SQL 中不等值Join的场景。</p><p><strong>在 Flink 1.6.0 时期</strong>，提供了State TTL功能、DataStream Interval Join功能。State TTL实现了在申请某个State时候可以在指定一个生命周期参数（TTL），指定该state过了多久之后需要被系统自动清除。在这个版本之前，如果用户想要实现这种状态清理操作需要使用ProcessFunction注册一个Timer，然后利用Timer的回调手动把这个State清除。从该版本开始，Flink框架可以基于TTL原生地解决这件事情。另外 DataStream Interval Join 功能也叫做 <strong>区间Join</strong>。例如左流的每一条数据去Join右流前后<strong>5分钟之内</strong>的数据，这种就是5分钟的区间Join。</p><h2 id="Flink-High-Level-API-历史变迁"><a href="#Flink-High-Level-API-历史变迁" class="headerlink" title="Flink High-Level API 历史变迁"></a>Flink High-Level API 历史变迁</h2><p><img src="https://img.alicdn.com/tfs/TB1vZZYohjaK1RjSZFAXXbdLFXa-1470-575.png" alt></p><p><strong>在 Flink 1.0.0 时期</strong>，Table API （结构化数据处理API）和 CEP（复杂事件处理API）这两个框架被首次加入到仓库中。Table API 是一种结构化的高级 API，支持 Java 语言和 Scala 语言，类似于 Spark 的 DataFrame API。但是当时社区对于 SQL 的需求很大，而 SQL 和 Table API 非常相近，他们都是一种处理结构化数据的语言，实现上可以共用很多内容。所以在 <strong>Flink 1.1.0</strong> 里面，社区基于Apache Calcite对整个 Table 模块做了重构，使得同时支持了 Table API 和 SQL 并共用了大部分代码。</p><p><strong>在 Flink 1.2.0 时期</strong>，社区在Table API和SQL上支持丰富的内置窗口操作，包括Tumbling Window、Sliding Window、Session Window。</p><p><strong>在 Flink 1.3.0 时期</strong>，社区首次提出了Dynamic Table这个概念，借助Dynamic Table，流和批之间可以相互进行转换。流可以是一张表，表也可以是一张流，这是流批统一的基础之一。其中Retraction机制是实现Dynamic Table的基础，基于Retraction才能够正确地实现多级Aggregate、多级Join，才能够保证流式 SQL 的语义与结果的正确性。另外，在该版本中还支持了 CEP 算子的可伸缩容（即改变并发）。</p><p><strong>在 Flink 1.5.0 时期</strong>，在 Table API 和 SQL 上支持了Join操作，包括无限流的 Join 和带窗口的 Join。还添加了 SQL CLI 支持。SQL CLI 提供了一个类似Shell命令的对话框，可以交互式执行查询。</p><h2 id="Flink-Checkpoint-amp-Recovery-历史变迁"><a href="#Flink-Checkpoint-amp-Recovery-历史变迁" class="headerlink" title="Flink Checkpoint &amp; Recovery 历史变迁"></a>Flink Checkpoint &amp; Recovery 历史变迁</h2><p><img src="https://img.alicdn.com/tfs/TB1xds3ohYaK1RjSZFnXXa80pXa-1470-575.png" alt></p><p>Checkpoint机制在Flink很早期的时候就已经支持，是Flink一个很核心的功能，Flink 社区也一直努力提升 Checkpoint 和 Recovery 的效率。</p><p><strong>在 Flink 1.0.0 时期</strong>，提供了 RocksDB 状态后端的支持，在这个版本之前所有的状态数据只能存在进程的内存里面，JVM 内存是固定大小的，随着数据越来越多总会发生 FullGC 和 OOM 的问题，所以在生产环境中很难应用起来。如果想要存更多数据、更大的State就要用到 RocksDB。RocksDB是一款基于文件的嵌入式数据库，它会把数据存到磁盘，同时又提供高效的读写性能。所以使用RocksDB不会发生OOM这种事情。</p><p><strong>在 Flink 1.1.0 时期</strong>，支持了 RocksDB Snapshot 的异步化。在之前的版本，RocksDB 的 Snapshot 过程是同步的，它会阻塞主数据流的处理，很影响吞吐量。在支持异步化之后，吞吐量得到了极大的提升。</p><p><strong>在 Flink 1.2.0 时期</strong>，通过引入KeyGroup的机制，支持了 KeyedState 和 OperatorState 的可扩缩容。也就是支持了对带状态的流计算任务改变并发的功能。</p><p><strong>在 Flink 1.3.0 时期</strong>，支持了 Incremental Checkpoint （增量检查点）机制。Incemental Checkpoint 的支持标志着 Flink 流计算任务正式达到了生产就绪状态。增量检查点是每次只将本次 checkpoint 期间新增的状态快照并持久化存储起来。一般流计算任务，GB 级别的状态，甚至 TB 级别的状态是非常常见的，如果每次都把全量的状态都刷到分布式存储中，这个效率和网络代价是很大的。如果每次只刷新增的数据，效率就会高很多。在这个版本里面还引入了细粒度的recovery的功能，细粒度的recovery在做恢复的时候，只需要恢复失败节点的联通子图，不用对整个 Job 进行恢复，这样便能够提高恢复效率。</p><p><strong>在 Flink 1.5.0 时期</strong>，引入了本地状态恢复的机制。因为基于checkpoint机制，会把State持久化地存储到某个分布式存储，比如HDFS，当发生 failover 的时候需要重新把数据从远程HDFS再下载下来，如果这个状态特别大那么下载耗时就会较长，failover 恢复所花的时间也会拉长。本地状态恢复机制会提前将状态文件在本地也备份一份，当Job发生failover之后，恢复时可以在本地直接恢复，不需从远程HDFS重新下载状态文件，从而提升了恢复的效率。</p><h2 id="Flink-Runtime-历史变迁"><a href="#Flink-Runtime-历史变迁" class="headerlink" title="Flink Runtime 历史变迁"></a>Flink Runtime 历史变迁</h2><p><img src="https://img.alicdn.com/tfs/TB1T.sRoa6qK1RjSZFmXXX0PFXa-1470-616.png" alt></p><p><strong>在 Flink 1.2.0 时期</strong>，提供了Async I/O功能。Async I/O 是阿里巴巴贡献给社区的一个呼声非常高的特性，主要目的是为了解决与外部系统交互时网络延迟成为了系统瓶颈的问题。例如，为了关联某些字段需要查询外部 HBase 表，同步的方式是每次查询的操作都是阻塞的，数据流会被频繁的I/O请求卡住。当使用异步I/O之后就可以同时地发起N个异步查询的请求，不会阻塞主数据流，这样便提升了整个job的吞吐量，提升CPU利用率。</p><p><strong>在 Flink 1.3.0 时期</strong>，引入了HistoryServer的模块。HistoryServer主要功能是当job结束以后，会把job的状态以及信息都进行归档，方便后续开发人员做一些深入排查。</p><p><strong>在 Flink 1.4.0 时期</strong>，提供了端到端的 exactly-once 的语义保证。Exactly-once 是指每条输入的数据只会作用在最终结果上有且只有一次，即使发生软件或硬件的故障，不会有丢数据或者重复计算发生。而在该版本之前，exactly-once 保证的范围只是 Flink 应用本身，并不包括输出给外部系统的部分。在 failover 时，这就有可能写了重复的数据到外部系统，所以一般会使用幂等的外部系统来解决这个问题。在 Flink 1.4 的版本中，Flink 基于两阶段提交协议，实现了端到端的 exactly-once 语义保证。内置支持了 Kafka 的端到端保证，并提供了 <code>TwoPhaseCommitSinkFunction</code> 供用于实现自定义外部存储的端到端 exactly-once 保证。</p><p><strong>在 Flink 1.5.0 时期</strong>，Flink 发布了新的部署模型和处理模型（FLIP6）。新部署模型的开发工作已经持续了很久，该模型的实现对Flink核心代码改动特别大，可以说是自 Flink 项目创建以来，Runtime 改动最大的一次。简而言之，新的模型可以在YARN, MESOS调度系统上更好地动态分配资源、动态释放资源，并实现更高的资源利用率，还有提供更好的作业之间的隔离。</p><p>除了 FLIP6 的改进，在该版本中，还对网站栈做了重构。重构的原因是在老版本中，上下游多个 task 之间的通信会共享同一个 TCP connection，导致某一个 task 发生反压时，所有共享该连接的 task 都会被阻塞，反压的粒度是 TCP connection 级别的。为了改进反压机制，Flink应用了在解决网络拥塞时一种经典的流控方法——基于Credit的流量控制。使得流控的粒度精细到具体某个 task 级别，有效缓解了反压对吞吐量的影响。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Flink 同时支持了流处理和批处理，目前流计算的模型已经相对比较成熟和领先，也经历了各个公司大规模生产的验证。社区在接下来将继续加强流计算方面的性能和功能，包括对 Flink SQL 扩展更丰富的功能和引入更多的优化。另一方面也将加大力量提升批处理、机器学习等生态上的能力。</p>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;作者：云邪（Jark）&lt;br&gt;原文链接：&lt;a href=&quot;http://wuchong.me/blog/2018/11/09/flink-tech-evolution-introduction/&quot;&gt;http://wuchong.me/blog/2018/11/09/flink-tech-evolution-introduction/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;Apache-Flink-介绍&quot;&gt;&lt;a href=&quot;#Apache-Flink-介绍&quot; class=&quot;headerlink&quot; title=&quot;Apache Flink 介绍&quot;&gt;&lt;/a&gt;Apache Flink 介绍&lt;/h2&gt;&lt;p&gt;Apache Flink 是近年来越来越流行的一款开源大数据计算引擎，它同时支持了批处理和流处理，也能用来做一些基于事件的应用。使用官网的一句话来介绍 Flink 就是 &lt;strong&gt;“Stateful Computations Over Streams”&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首先&lt;/strong&gt; Flink 是一个&lt;strong&gt;纯流式的计算引擎&lt;/strong&gt;，它的基本数据模型是数据流。流可以是无边界的无限流，即一般意义上的流处理。也可以是有边界的有限流，这样就是批处理。因此 Flink 用一套架构同时支持了流处理和批处理。&lt;strong&gt;其次&lt;/strong&gt;，Flink 的一个优势是支持&lt;strong&gt;有状态的计算&lt;/strong&gt;。如果处理一个事件（或一条数据）的结果只跟事件本身的内容有关，称为无状态处理；反之结果还和之前处理过的事件有关，称为有状态处理。稍微复杂一点的数据处理，比如说基本的聚合，数据流之间的关联都是有状态处理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.alicdn.com/tfs/TB1Ta88oyLaK1RjSZFxXXamPFXa-1627-845.png&quot; alt&gt;&lt;/p&gt;
    
    </summary>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
  </entry>
  
  <entry>
    <title>Flink 零基础实战教程：如何计算实时热门商品</title>
    <link href="http://wuchong.me/blog/2018/11/07/use-flink-calculate-hot-items/"/>
    <id>http://wuchong.me/blog/2018/11/07/use-flink-calculate-hot-items/</id>
    <published>2018-11-07T15:13:24.000Z</published>
    <updated>2022-08-03T06:46:44.513Z</updated>
    
    <content type="html"><![CDATA[<p>在<a href="http://wuchong.me/blog/2018/11/07/5-minutes-build-first-flink-application/">上一篇入门教程</a>中，我们已经能够快速构建一个基础的 Flink 程序了。本文会一步步地带领你实现一个更复杂的 Flink 应用程序：实时热门商品。在开始本文前我们建议你先实践一遍上篇文章，因为本文会沿用上文的<code>my-flink-project</code>项目框架。</p><p>通过本文你将学到：</p><ol><li>如何基于 EventTime 处理，如何指定 Watermark</li><li>如何使用 Flink 灵活的 Window API</li><li>何时需要用到 State，以及如何使用</li><li>如何使用 ProcessFunction 实现 TopN 功能</li></ol><a id="more"></a><h2 id="实战案例介绍"><a href="#实战案例介绍" class="headerlink" title="实战案例介绍"></a>实战案例介绍</h2><p>本案例将实现一个“实时热门商品”的需求，我们可以将“实时热门商品”翻译成程序员更好理解的需求：每隔5分钟输出最近一小时内点击量最多的前 N 个商品。将这个需求进行分解我们大概要做这么几件事情：</p><ul><li>抽取出业务时间戳，告诉 Flink 框架基于业务时间做窗口</li><li>过滤出点击行为数据</li><li>按一小时的窗口大小，每5分钟统计一次，做滑动窗口聚合（Sliding Window）</li><li>按每个窗口聚合，输出每个窗口中点击量前N名的商品</li></ul><h2 id="数据准备"><a href="#数据准备" class="headerlink" title="数据准备"></a>数据准备</h2><p>这里我们准备了一份淘宝用户行为数据集（来自<a href="https://tianchi.aliyun.com/datalab/index.htm" target="_blank" rel="noopener">阿里云天池公开数据集</a>，特别感谢）。本数据集包含了淘宝上某一天随机一百万用户的所有行为（包括点击、购买、加购、收藏）。数据集的组织形式和MovieLens-20M类似，即数据集的每一行表示一条用户行为，由用户ID、商品ID、商品类目ID、行为类型和时间戳组成，并以逗号分隔。关于数据集中每一列的详细描述如下：</p><table><thead><tr><th>列名称</th><th>说明</th></tr></thead><tbody><tr><td>用户ID</td><td>整数类型，加密后的用户ID</td></tr><tr><td>商品ID</td><td>整数类型，加密后的商品ID</td></tr><tr><td>商品类目ID</td><td>整数类型，加密后的商品所属类目ID</td></tr><tr><td>行为类型</td><td>字符串，枚举类型，包括(‘pv’, ‘buy’, ‘cart’, ‘fav’)</td></tr><tr><td>时间戳</td><td>行为发生的时间戳，单位秒</td></tr></tbody></table><p>你可以通过下面的命令下载数据集到项目的 <code>resources</code> 目录下：</p><figure class="highlight elixir"><table><tr><td class="code"><pre><span class="line"><span class="variable">$ </span>cd my-flink-project/src/main/resources</span><br><span class="line"><span class="variable">$ </span>curl <span class="symbol">https:</span>/<span class="regexp">/raw.githubusercontent.com/wuchong</span><span class="regexp">/my-flink-project/master</span><span class="regexp">/src/main</span><span class="regexp">/resources/</span>UserBehavior.csv &gt; UserBehavior.csv</span><br></pre></td></tr></table></figure><p>这里是否使用 curl 命令下载数据并不重要，你也可以使用 wget 命令或者直接访问链接下载数据。关键是，<strong>将数据文件保存到项目的 <code>resources</code> 目录下</strong>，方便应用程序访问。</p><h2 id="编写程序"><a href="#编写程序" class="headerlink" title="编写程序"></a>编写程序</h2><p>在 <code>src/main/java/myflink</code> 下创建 <code>HotItems.java</code> 文件：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> myflink;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">HotItems</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    </span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>与上文一样，我们会一步步往里面填充代码。第一步仍然是创建一个 <code>StreamExecutionEnvironment</code>，我们把它添加到 main 函数中。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();</span><br><span class="line"><span class="comment">// 为了打印到控制台的结果不乱序，我们配置全局的并发为1，这里改变并发对结果正确性没有影响</span></span><br><span class="line">env.setParallelism(<span class="number">1</span>);</span><br></pre></td></tr></table></figure><h3 id="创建模拟数据源"><a href="#创建模拟数据源" class="headerlink" title="创建模拟数据源"></a>创建模拟数据源</h3><p>在数据准备章节，我们已经将测试的数据集下载到本地了。由于是一个csv文件，我们将使用 <code>CsvInputFormat</code> 创建模拟数据源。</p><blockquote><p>注：虽然一个流式应用应该是一个一直运行着的程序，需要消费一个无限数据源。但是在本案例教程中，为了省去构建真实数据源的繁琐，我们使用了文件来模拟真实数据源，这并不影响下文要介绍的知识点。这也是一种本地验证 Flink 应用程序正确性的常用方式。</p></blockquote><p>我们先创建一个 <code>UserBehavior</code> 的 POJO 类（所有成员变量声明成<code>public</code>便是POJO类），强类型化后能方便后续的处理。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/** 用户行为数据结构 **/</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">UserBehavior</span> </span>&#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">long</span> userId;         <span class="comment">// 用户ID</span></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">long</span> itemId;         <span class="comment">// 商品ID</span></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">int</span> categoryId;      <span class="comment">// 商品类目ID</span></span><br><span class="line">  <span class="keyword">public</span> String behavior;     <span class="comment">// 用户行为, 包括("pv", "buy", "cart", "fav")</span></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">long</span> timestamp;      <span class="comment">// 行为发生的时间戳，单位秒</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来我们就可以创建一个 <code>PojoCsvInputFormat</code> 了， 这是一个读取 csv 文件并将每一行转成指定 POJO<br>类型（在我们案例中是 <code>UserBehavior</code>）的输入器。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// UserBehavior.csv 的本地文件路径</span></span><br><span class="line">URL fileUrl = HotItems2.class.getClassLoader().getResource("UserBehavior.csv");</span><br><span class="line">Path filePath = Path.fromLocalFile(<span class="keyword">new</span> File(fileUrl.toURI()));</span><br><span class="line"><span class="comment">// 抽取 UserBehavior 的 TypeInformation，是一个 PojoTypeInfo</span></span><br><span class="line">PojoTypeInfo&lt;UserBehavior&gt; pojoType = (PojoTypeInfo&lt;UserBehavior&gt;) TypeExtractor.createTypeInfo(UserBehavior<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"><span class="comment">// 由于 Java 反射抽取出的字段顺序是不确定的，需要显式指定下文件中字段的顺序</span></span><br><span class="line">String[] fieldOrder = <span class="keyword">new</span> String[]&#123;<span class="string">"userId"</span>, <span class="string">"itemId"</span>, <span class="string">"categoryId"</span>, <span class="string">"behavior"</span>, <span class="string">"timestamp"</span>&#125;;</span><br><span class="line"><span class="comment">// 创建 PojoCsvInputFormat</span></span><br><span class="line">PojoCsvInputFormat&lt;UserBehavior&gt; csvInput = <span class="keyword">new</span> PojoCsvInputFormat&lt;&gt;(filePath, pojoType, fieldOrder);</span><br></pre></td></tr></table></figure><p>下一步我们用 <code>PojoCsvInputFormat</code> 创建输入源。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream&lt;UserBehavior&gt; dataSource = env.createInput(csvInput, pojoType);</span><br></pre></td></tr></table></figure><p>这就创建了一个 <code>UserBehavior</code> 类型的 <code>DataStream</code>。</p><h3 id="EventTime-与-Watermark"><a href="#EventTime-与-Watermark" class="headerlink" title="EventTime 与 Watermark"></a>EventTime 与 Watermark</h3><p>当我们说“统计过去一小时内点击量”，这里的“一小时”是指什么呢？ 在 Flink 中它可以是指 ProcessingTime ，也可以是 EventTime，由用户决定。</p><ul><li>ProcessingTime：事件被处理的时间。也就是由机器的系统时间来决定。</li><li>EventTime：事件发生的时间。一般就是数据本身携带的时间。</li></ul><p>在本案例中，我们需要统计业务时间上的每小时的点击量，所以要基于 EventTime 来处理。那么如果让 Flink 按照我们想要的业务时间来处理呢？这里主要有两件事情要做。</p><p>第一件是告诉 Flink 我们现在按照 EventTime 模式进行处理，Flink 默认使用 ProcessingTime 处理，所以我们要显式设置下。</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">env</span><span class="selector-class">.setStreamTimeCharacteristic</span>(<span class="selector-tag">TimeCharacteristic</span><span class="selector-class">.EventTime</span>);</span><br></pre></td></tr></table></figure><p>第二件事情是指定如何获得业务时间，以及生成 Watermark。Watermark 是用来追踪业务事件的概念，可以理解成 EventTime 世界中的时钟，用来指示当前处理到什么时刻的数据了。由于我们的数据源的数据已经经过整理，没有乱序，即事件的时间戳是单调递增的，所以可以将每条数据的业务时间就当做 Watermark。这里我们用 <code>AscendingTimestampExtractor</code> 来实现时间戳的抽取和 Watermark 的生成。</p><blockquote><p>注：真实业务场景一般都是存在乱序的，所以一般使用 <code>BoundedOutOfOrdernessTimestampExtractor</code>。</p></blockquote><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream&lt;UserBehavior&gt; timedData = dataSource</span><br><span class="line">    .assignTimestampsAndWatermarks(<span class="keyword">new</span> AscendingTimestampExtractor&lt;UserBehavior&gt;() &#123;</span><br><span class="line">      <span class="meta">@Override</span></span><br><span class="line">      <span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">extractAscendingTimestamp</span><span class="params">(UserBehavior userBehavior)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 原始数据单位秒，将其转成毫秒</span></span><br><span class="line">        <span class="keyword">return</span> userBehavior.timestamp * <span class="number">1000</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br></pre></td></tr></table></figure><p>这样我们就得到了一个带有时间标记的数据流了，后面就能做一些窗口的操作。</p><h3 id="过滤出点击事件"><a href="#过滤出点击事件" class="headerlink" title="过滤出点击事件"></a>过滤出点击事件</h3><p>在开始窗口操作之前，先回顾下需求“每隔5分钟输出过去一小时内<strong>点击量</strong>最多的前 N 个商品”。由于原始数据中存在点击、加购、购买、收藏各种行为的数据，但是我们只需要统计点击量，所以先使用 <code>FilterFunction</code> 将点击行为数据过滤出来。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream&lt;UserBehavior&gt; pvData = timedData</span><br><span class="line">    .filter(<span class="keyword">new</span> FilterFunction&lt;UserBehavior&gt;() &#123;</span><br><span class="line">      <span class="meta">@Override</span></span><br><span class="line">      <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">filter</span><span class="params">(UserBehavior userBehavior)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">// 过滤出只有点击的数据</span></span><br><span class="line">        <span class="keyword">return</span> userBehavior.behavior.equals(<span class="string">"pv"</span>);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br></pre></td></tr></table></figure><h3 id="窗口统计点击量"><a href="#窗口统计点击量" class="headerlink" title="窗口统计点击量"></a>窗口统计点击量</h3><p>由于要每隔5分钟统计一次最近一小时每个商品的点击量，所以窗口大小是一小时，每隔5分钟滑动一次。即分别要统计 [09:00, 10:00), [09:05, 10:05), [09:10, 10:10)… 等窗口的商品点击量。是一个常见的滑动窗口需求（Sliding Window）。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream&lt;ItemViewCount&gt; windowedData = pvData</span><br><span class="line">    .keyBy(<span class="string">"itemId"</span>)</span><br><span class="line">    .timeWindow(Time.minutes(<span class="number">60</span>), Time.minutes(<span class="number">5</span>))</span><br><span class="line">    .aggregate(<span class="keyword">new</span> CountAgg(), <span class="keyword">new</span> WindowResultFunction());</span><br></pre></td></tr></table></figure><p>我们使用<code>.keyBy(&quot;itemId&quot;)</code>对商品进行分组，使用<code>.timeWindow(Time size, Time slide)</code>对每个商品做滑动窗口（1小时窗口，5分钟滑动一次）。然后我们使用 <code>.aggregate(AggregateFunction af, WindowFunction wf)</code> 做增量的聚合操作，它能使用<code>AggregateFunction</code>提前聚合掉数据，减少 state 的存储压力。较之<code>.apply(WindowFunction wf)</code>会将窗口中的数据都存储下来，最后一起计算要高效地多。<code>aggregate()</code>方法的第一个参数用于</p><p>这里的<code>CountAgg</code>实现了<code>AggregateFunction</code>接口，功能是统计窗口中的条数，即遇到一条数据就加一。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/** COUNT 统计的聚合函数实现，每出现一条记录加一 */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">CountAgg</span> <span class="keyword">implements</span> <span class="title">AggregateFunction</span>&lt;<span class="title">UserBehavior</span>, <span class="title">Long</span>, <span class="title">Long</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Long <span class="title">createAccumulator</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0L</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Long <span class="title">add</span><span class="params">(UserBehavior userBehavior, Long acc)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> acc + <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Long <span class="title">getResult</span><span class="params">(Long acc)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> acc;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Long <span class="title">merge</span><span class="params">(Long acc1, Long acc2)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> acc1 + acc2;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>.aggregate(AggregateFunction af, WindowFunction wf)</code> 的第二个参数<code>WindowFunction</code>将每个 key每个窗口聚合后的结果带上其他信息进行输出。我们这里实现的<code>WindowResultFunction</code>将主键商品ID，窗口，点击量封装成了<code>ItemViewCount</code>进行输出。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/** 用于输出窗口的结果 */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">WindowResultFunction</span> <span class="keyword">implements</span> <span class="title">WindowFunction</span>&lt;<span class="title">Long</span>, <span class="title">ItemViewCount</span>, <span class="title">Tuple</span>, <span class="title">TimeWindow</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">apply</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">      Tuple key,  // 窗口的主键，即 itemId</span></span></span><br><span class="line"><span class="function"><span class="params">      TimeWindow window,  // 窗口</span></span></span><br><span class="line"><span class="function"><span class="params">      Iterable&lt;Long&gt; aggregateResult, // 聚合函数的结果，即 count 值</span></span></span><br><span class="line"><span class="function"><span class="params">      Collector&lt;ItemViewCount&gt; collector  // 输出类型为 ItemViewCount</span></span></span><br><span class="line"><span class="function"><span class="params">  )</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    Long itemId = ((Tuple1&lt;Long&gt;) key).f0;</span><br><span class="line">    Long count = aggregateResult.iterator().next();</span><br><span class="line">    collector.collect(ItemViewCount.of(itemId, window.getEnd(), count));</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/** 商品点击量(窗口操作的输出类型) */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">ItemViewCount</span> </span>&#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">long</span> itemId;     <span class="comment">// 商品ID</span></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">long</span> windowEnd;  <span class="comment">// 窗口结束时间戳</span></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">long</span> viewCount;  <span class="comment">// 商品的点击量</span></span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ItemViewCount <span class="title">of</span><span class="params">(<span class="keyword">long</span> itemId, <span class="keyword">long</span> windowEnd, <span class="keyword">long</span> viewCount)</span> </span>&#123;</span><br><span class="line">    ItemViewCount result = <span class="keyword">new</span> ItemViewCount();</span><br><span class="line">    result.itemId = itemId;</span><br><span class="line">    result.windowEnd = windowEnd;</span><br><span class="line">    result.viewCount = viewCount;</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在我们得到了每个商品在每个窗口的点击量的数据流。</p><h3 id="TopN-计算最热门商品"><a href="#TopN-计算最热门商品" class="headerlink" title="TopN 计算最热门商品"></a>TopN 计算最热门商品</h3><p>为了统计每个窗口下最热门的商品，我们需要再次按窗口进行分组，这里根据<code>ItemViewCount</code>中的<code>windowEnd</code>进行<code>keyBy()</code>操作。然后使用 <code>ProcessFunction</code> 实现一个自定义的 TopN 函数 <code>TopNHotItems</code> 来计算点击量排名前3名的商品，并将排名结果格式化成字符串，便于后续输出。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream&lt;String&gt; topItems = windowedData</span><br><span class="line">    .keyBy(<span class="string">"windowEnd"</span>)</span><br><span class="line">    .process(<span class="keyword">new</span> TopNHotItems(<span class="number">3</span>));  <span class="comment">// 求点击量前3名的商品</span></span><br></pre></td></tr></table></figure><p><code>ProcessFunction</code> 是 Flink 提供的一个 low-level API，用于实现更高级的功能。它主要提供了定时器 timer 的功能（支持EventTime或ProcessingTime）。本案例中我们将利用 timer 来判断何时<strong>收齐</strong>了某个 window 下所有商品的点击量数据。由于 Watermark 的进度是全局的，</p><p>在 <code>processElement</code> 方法中，每当收到一条数据（<code>ItemViewCount</code>），我们就注册一个 <code>windowEnd+1</code> 的定时器（Flink 框架会自动忽略同一时间的重复注册）。<code>windowEnd+1</code> 的定时器被触发时，意味着收到了<code>windowEnd+1</code>的 Watermark，即收齐了该<code>windowEnd</code>下的所有商品窗口统计值。我们在 <code>onTimer()</code> 中处理将收集的所有商品及点击量进行排序，选出 TopN，并将排名信息格式化成字符串后进行输出。</p><p>这里我们还使用了 <code>ListState&lt;ItemViewCount&gt;</code> 来存储收到的每条 <code>ItemViewCount</code> 消息，保证在发生故障时，状态数据的不丢失和一致性。<code>ListState</code> 是 Flink 提供的类似 Java <code>List</code> 接口的 State API，它集成了框架的 checkpoint 机制，自动做到了 exactly-once 的语义保证。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/** 求某个窗口中前 N 名的热门点击商品，key 为窗口时间戳，输出为 TopN 的结果字符串 */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">TopNHotItems</span> <span class="keyword">extends</span> <span class="title">KeyedProcessFunction</span>&lt;<span class="title">Tuple</span>, <span class="title">ItemViewCount</span>, <span class="title">String</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">int</span> topSize;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">TopNHotItems</span><span class="params">(<span class="keyword">int</span> topSize)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.topSize = topSize;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 用于存储商品与点击数的状态，待收齐同一个窗口的数据后，再触发 TopN 计算</span></span><br><span class="line">  <span class="keyword">private</span> ListState&lt;ItemViewCount&gt; itemState;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">open</span><span class="params">(Configuration parameters)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>.open(parameters);</span><br><span class="line">    <span class="comment">// 状态的注册</span></span><br><span class="line">    ListStateDescriptor&lt;ItemViewCount&gt; itemsStateDesc = <span class="keyword">new</span> ListStateDescriptor&lt;&gt;(</span><br><span class="line">        <span class="string">"itemState-state"</span>,</span><br><span class="line">        ItemViewCount<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    itemState = getRuntimeContext().getListState(itemsStateDesc);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">processElement</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">      ItemViewCount input,</span></span></span><br><span class="line"><span class="function"><span class="params">      Context context,</span></span></span><br><span class="line"><span class="function"><span class="params">      Collector&lt;String&gt; collector)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 每条数据都保存到状态中</span></span><br><span class="line">    itemState.add(input);</span><br><span class="line">    <span class="comment">// 注册 windowEnd+1 的 EventTime Timer, 当触发时，说明收齐了属于windowEnd窗口的所有商品数据</span></span><br><span class="line">    context.timerService().registerEventTimeTimer(input.windowEnd + <span class="number">1</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onTimer</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">      <span class="keyword">long</span> timestamp, OnTimerContext ctx, Collector&lt;String&gt; out)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    <span class="comment">// 获取收到的所有商品点击量</span></span><br><span class="line">    List&lt;ItemViewCount&gt; allItems = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span> (ItemViewCount item : itemState.get()) &#123;</span><br><span class="line">      allItems.add(item);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 提前清除状态中的数据，释放空间</span></span><br><span class="line">    itemState.clear();</span><br><span class="line">    <span class="comment">// 按照点击量从大到小排序</span></span><br><span class="line">    allItems.sort(<span class="keyword">new</span> Comparator&lt;ItemViewCount&gt;() &#123;</span><br><span class="line">      <span class="meta">@Override</span></span><br><span class="line">      <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">compare</span><span class="params">(ItemViewCount o1, ItemViewCount o2)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> (<span class="keyword">int</span>) (o2.viewCount - o1.viewCount);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="comment">// 将排名信息格式化成 String, 便于打印</span></span><br><span class="line">    StringBuilder result = <span class="keyword">new</span> StringBuilder();</span><br><span class="line">    result.append(<span class="string">"====================================\n"</span>);</span><br><span class="line">    result.append(<span class="string">"时间: "</span>).append(<span class="keyword">new</span> Timestamp(timestamp-<span class="number">1</span>)).append(<span class="string">"\n"</span>);</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i=<span class="number">0</span>;i&lt;topSize;i++) &#123;</span><br><span class="line">      ItemViewCount currentItem = allItems.get(i);</span><br><span class="line">      <span class="comment">// No1:  商品ID=12224  浏览量=2413</span></span><br><span class="line">      result.append(<span class="string">"No"</span>).append(i).append(<span class="string">":"</span>)</span><br><span class="line">            .append(<span class="string">"  商品ID="</span>).append(currentItem.itemId)</span><br><span class="line">            .append(<span class="string">"  浏览量="</span>).append(currentItem.viewCount)</span><br><span class="line">            .append(<span class="string">"\n"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    result.append(<span class="string">"====================================\n\n"</span>);</span><br><span class="line"></span><br><span class="line">    out.collect(result.toString());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="打印输出"><a href="#打印输出" class="headerlink" title="打印输出"></a>打印输出</h3><p>最后一步我们将结果打印输出到控制台，并调用<code>env.execute</code>执行任务。</p><figure class="highlight abnf"><table><tr><td class="code"><pre><span class="line">topItems.print()<span class="comment">;</span></span><br><span class="line">env.execute(<span class="string">"Hot Items Job"</span>)<span class="comment">;</span></span><br></pre></td></tr></table></figure><h2 id="运行程序"><a href="#运行程序" class="headerlink" title="运行程序"></a>运行程序</h2><p>直接运行 main 函数，就能看到不断输出的每个时间点的热门商品ID。</p><p><img src="https://img.alicdn.com/tfs/TB1o_fIn3TqK1RjSZPhXXXfOFXa-1534-1270.png" alt></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文的完整代码可以通过 <a href="https://github.com/wuchong/my-flink-project/blob/master/src/main/java/myflink/HotItems.java" target="_blank" rel="noopener">GitHub</a> 访问到。本文通过实现一个“实时热门商品”的案例，学习和实践了 Flink 的多个核心概念和 API 用法。包括 EventTime、Watermark 的使用，State 的使用，Window API 的使用，以及 TopN 的实现。希望本文能加深大家对 Flink 的理解，帮助大家解决实战上遇到的问题。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在&lt;a href=&quot;http://wuchong.me/blog/2018/11/07/5-minutes-build-first-flink-application/&quot;&gt;上一篇入门教程&lt;/a&gt;中，我们已经能够快速构建一个基础的 Flink 程序了。本文会一步步地带领你实现一个更复杂的 Flink 应用程序：实时热门商品。在开始本文前我们建议你先实践一遍上篇文章，因为本文会沿用上文的&lt;code&gt;my-flink-project&lt;/code&gt;项目框架。&lt;/p&gt;
&lt;p&gt;通过本文你将学到：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如何基于 EventTime 处理，如何指定 Watermark&lt;/li&gt;
&lt;li&gt;如何使用 Flink 灵活的 Window API&lt;/li&gt;
&lt;li&gt;何时需要用到 State，以及如何使用&lt;/li&gt;
&lt;li&gt;如何使用 ProcessFunction 实现 TopN 功能&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink入门" scheme="http://wuchong.me/tags/Flink%E5%85%A5%E9%97%A8/"/>
    
  </entry>
  
  <entry>
    <title>5分钟从零构建第一个 Flink 应用</title>
    <link href="http://wuchong.me/blog/2018/11/07/5-minutes-build-first-flink-application/"/>
    <id>http://wuchong.me/blog/2018/11/07/5-minutes-build-first-flink-application/</id>
    <published>2018-11-07T00:40:26.000Z</published>
    <updated>2022-08-03T06:46:44.500Z</updated>
    
    <content type="html"><![CDATA[<p>在本文中，我们将从零开始，教您如何构建第一个 Flink 应用程序。</p><a id="more"></a><h2 id="开发环境准备"><a href="#开发环境准备" class="headerlink" title="开发环境准备"></a>开发环境准备</h2><p>Flink 可以运行在 Linux, Max OS X, 或者是 Windows 上。为了开发 Flink 应用程序，在本地机器上需要有 <strong>Java 8.x</strong> 和 <strong>maven</strong> 环境。</p><p>如果有 Java 8 环境，运行下面的命令会输出如下版本信息：</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">$ java -version</span><br><span class="line">java version <span class="string">"1.8.0_65"</span></span><br><span class="line">Java(TM) SE Runtime Environment (build 1.8.0_65-b17)</span><br><span class="line">Java HotSpot(TM) 64-Bit<span class="built_in"> Server </span>VM (build 25.65-b01, mixed mode)</span><br></pre></td></tr></table></figure><p>如果有 maven 环境，运行下面的命令会输出如下版本信息：</p><figure class="highlight groovy"><table><tr><td class="code"><pre><span class="line">$ mvn -version</span><br><span class="line">Apache Maven <span class="number">3.5</span><span class="number">.4</span> (<span class="number">1</span>edded0938998edf8bf061f1ceb3cfdeccf443fe; <span class="number">2018</span><span class="number">-06</span><span class="number">-18</span><span class="string">T02:</span><span class="number">33</span>:<span class="number">14</span>+<span class="number">08</span>:<span class="number">00</span>)</span><br><span class="line">Maven <span class="string">home:</span> <span class="regexp">/Users/</span>wuchong<span class="regexp">/dev/</span>maven</span><br><span class="line">Java <span class="string">version:</span> <span class="number">1.8</span><span class="number">.0</span>_65, <span class="string">vendor:</span> Oracle Corporation, <span class="string">runtime:</span> <span class="regexp">/Library/</span>Java<span class="regexp">/JavaVirtualMachines/</span>jdk1<span class="number">.8</span><span class="number">.0</span>_65.jdk<span class="regexp">/Contents/</span>Home/jre</span><br><span class="line">Default <span class="string">locale:</span> zh_CN, platform <span class="string">encoding:</span> UTF<span class="number">-8</span></span><br><span class="line">OS <span class="string">name:</span> <span class="string">"mac os x"</span>, <span class="string">version:</span> <span class="string">"10.13.6"</span>, <span class="string">arch:</span> <span class="string">"x86_64"</span>, <span class="string">family:</span> <span class="string">"mac"</span></span><br></pre></td></tr></table></figure><p>另外我们推荐使用 ItelliJ IDEA （社区免费版已够用）作为 Flink 应用程序的开发 IDE。Eclipse 虽然也可以，但是 Eclipse 在 Scala 和 Java 混合型项目下会有些已知问题，所以不太推荐 Eclipse。下一章节，我们会介绍如何创建一个 Flink 工程并将其导入 ItelliJ IDEA。</p><h2 id="创建-Maven-项目"><a href="#创建-Maven-项目" class="headerlink" title="创建 Maven 项目"></a>创建 Maven 项目</h2><p>我们将使用 Flink Maven Archetype 来创建我们的项目结构和一些初始的默认依赖。在你的工作目录下，运行如下命令来创建项目：</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">mvn archetype:generate \</span><br><span class="line">    <span class="attribute">-DarchetypeGroupId</span>=org.apache.flink \</span><br><span class="line">    <span class="attribute">-DarchetypeArtifactId</span>=flink-quickstart-java \</span><br><span class="line">    <span class="attribute">-DarchetypeVersion</span>=1.6.1 \</span><br><span class="line">    <span class="attribute">-DgroupId</span>=my-flink-project \</span><br><span class="line">    <span class="attribute">-DartifactId</span>=my-flink-project \</span><br><span class="line">    <span class="attribute">-Dversion</span>=0.1 \</span><br><span class="line">    <span class="attribute">-Dpackage</span>=myflink \</span><br><span class="line">    <span class="attribute">-DinteractiveMode</span>=<span class="literal">false</span></span><br></pre></td></tr></table></figure><p>你可以编辑上面的 groupId, artifactId, package 成你喜欢的路径。使用上面的参数，Maven 将自动为你创建如下所示的项目结构：</p><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line">$ tree my-flink-project</span><br><span class="line">my-flink-project</span><br><span class="line">├── pom.xml</span><br><span class="line">└── src</span><br><span class="line">    └── main</span><br><span class="line">        ├── java</span><br><span class="line">        │   └── myflink</span><br><span class="line">        │       ├── BatchJob.java</span><br><span class="line">        │       └── StreamingJob.java</span><br><span class="line">        └── resources</span><br><span class="line">            └── log4j.properties</span><br></pre></td></tr></table></figure><p>我们的 pom.xml 文件已经包含了所需的 Flink 依赖，并且在 src/main/java 下有几个示例程序框架。接下来我们将开始编写第一个 Flink 程序。</p><h2 id="编写-Flink-程序"><a href="#编写-Flink-程序" class="headerlink" title="编写 Flink 程序"></a>编写 Flink 程序</h2><p>启动 IntelliJ IDEA，选择 “Import Project”（导入项目），选择 my-flink-project 根目录下的 pom.xml。根据引导，完成项目导入。</p><p>在 src/main/java/myflink 下创建 <code>SocketWindowWordCount.java</code> 文件：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> myflink;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SocketWindowWordCount</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在这程序还很基础，我们会一步步往里面填代码。注意下文中我们不会将 import 语句也写出来，因为 IDE 会自动将他们添加上去。在本节末尾，我会将完整的代码展示出来，如果你想跳过下面的步骤，可以直接将最后的完整代码粘到编辑器中。</p><p>Flink 程序的第一步是创建一个 <code>StreamExecutionEnvironment</code> 。这是一个入口类，可以用来设置参数和创建数据源以及提交任务。所以让我们把它添加到 main 函数中：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">StreamExecutionEnvironment see = StreamExecutionEnvironment.getExecutionEnvironment();</span><br></pre></td></tr></table></figure><p>下一步我们将创建一个从本地端口号 9000 的 socket 中读取数据的数据源：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream&lt;String&gt; text = env.socketTextStream(<span class="string">"localhost"</span>, <span class="number">9000</span>, <span class="string">"\n"</span>);</span><br></pre></td></tr></table></figure><p>这创建了一个字符串类型的 <code>DataStream</code>。<code>DataStream</code> 是 Flink 中做流处理的核心 API，上面定义了非常多常见的操作（如，过滤、转换、聚合、窗口、关联等）。在本示例中，我们感兴趣的是每个单词在特定时间窗口中出现的次数，比如说5秒窗口。为此，我们首先要将字符串数据解析成单词和次数（使用<code>Tuple2&lt;String, Integer&gt;</code>表示），第一个字段是单词，第二个字段是次数，次数初始值都设置成了1。我们实现了一个 <code>flatmap</code> 来做解析的工作，因为一行数据中可能有多个单词。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream&lt;Tuple2&lt;String, Integer&gt;&gt; wordCounts = text</span><br><span class="line">        .flatMap(<span class="keyword">new</span> FlatMapFunction&lt;String, Tuple2&lt;String, Integer&gt;&gt;() &#123;</span><br><span class="line">          <span class="meta">@Override</span></span><br><span class="line">          <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">flatMap</span><span class="params">(String value, Collector&lt;Tuple2&lt;String, Integer&gt;&gt; out)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">for</span> (String word : value.split(<span class="string">"\\s"</span>)) &#123;</span><br><span class="line">              out.collect(Tuple2.of(word, <span class="number">1</span>));</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;);</span><br></pre></td></tr></table></figure><p>接着我们将数据流按照单词字段（即0号索引字段）做分组，这里可以简单地使用 <code>keyBy(int index)</code> 方法，得到一个以单词为 key 的<code>Tuple2&lt;String, Integer&gt;</code>数据流。然后我们可以在流上指定想要的窗口，并根据窗口中的数据计算结果。在我们的例子中，我们想要每5秒聚合一次单词数，每个窗口都是从零开始统计的：。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream&lt;Tuple2&lt;String, Integer&gt;&gt; windowCounts = wordCounts</span><br><span class="line">        .keyBy(<span class="number">0</span>)</span><br><span class="line">        .timeWindow(Time.seconds(<span class="number">5</span>))</span><br><span class="line">        .sum(<span class="number">1</span>);</span><br></pre></td></tr></table></figure><p>第二个调用的 <code>.timeWindow()</code> 指定我们想要5秒的翻滚窗口（Tumble）。第三个调用为每个key每个窗口指定了<code>sum</code>聚合函数，在我们的例子中是按照次数字段（即1号索引字段）相加。得到的结果数据流，将每5秒输出一次这5秒内每个单词出现的次数。</p><p>最后一件事就是将数据流打印到控制台，并开始执行：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">windowCounts.print().setParallelism(<span class="number">1</span>);</span><br><span class="line">env.execute(<span class="string">"Socket Window WordCount"</span>);</span><br></pre></td></tr></table></figure><p>最后的 <code>env.execute</code> 调用是启动实际Flink作业所必需的。所有算子操作（例如创建源、聚合、打印）只是构建了内部算子操作的图形。只有在<code>execute()</code>被调用时才会在提交到集群上或本地计算机上执行。</p><p>下面是完整的代码，部分代码经过简化（代码在 <a href="https://github.com/wuchong/my-flink-project/blob/master/src/main/java/myflink/SocketWindowWordCount.java" target="_blank" rel="noopener">GitHub</a> 上也能访问到）：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> myflink;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.flink.api.common.functions.FlatMapFunction;</span><br><span class="line"><span class="keyword">import</span> org.apache.flink.api.java.tuple.Tuple2;</span><br><span class="line"><span class="keyword">import</span> org.apache.flink.streaming.api.datastream.DataStream;</span><br><span class="line"><span class="keyword">import</span> org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;</span><br><span class="line"><span class="keyword">import</span> org.apache.flink.streaming.api.windowing.time.Time;</span><br><span class="line"><span class="keyword">import</span> org.apache.flink.util.Collector;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SocketWindowWordCount</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建 execution environment</span></span><br><span class="line">    <span class="keyword">final</span> StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 通过连接 socket 获取输入数据，这里连接到本地9000端口，如果9000端口已被占用，请换一个端口</span></span><br><span class="line">    DataStream&lt;String&gt; text = env.socketTextStream(<span class="string">"localhost"</span>, <span class="number">9000</span>, <span class="string">"\n"</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 解析数据，按 word 分组，开窗，聚合</span></span><br><span class="line">    DataStream&lt;Tuple2&lt;String, Integer&gt;&gt; windowCounts = text</span><br><span class="line">        .flatMap(<span class="keyword">new</span> FlatMapFunction&lt;String, Tuple2&lt;String, Integer&gt;&gt;() &#123;</span><br><span class="line">          <span class="meta">@Override</span></span><br><span class="line">          <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">flatMap</span><span class="params">(String value, Collector&lt;Tuple2&lt;String, Integer&gt;&gt; out)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">for</span> (String word : value.split(<span class="string">"\\s"</span>)) &#123;</span><br><span class="line">              out.collect(Tuple2.of(word, <span class="number">1</span>));</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;)</span><br><span class="line">        .keyBy(<span class="number">0</span>)</span><br><span class="line">        .timeWindow(Time.seconds(<span class="number">5</span>))</span><br><span class="line">        .sum(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 将结果打印到控制台，注意这里使用的是单线程打印，而非多线程</span></span><br><span class="line">    windowCounts.print().setParallelism(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    env.execute(<span class="string">"Socket Window WordCount"</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="运行程序"><a href="#运行程序" class="headerlink" title="运行程序"></a>运行程序</h2><p>要运行示例程序，首先我们在终端启动 netcat 获得输入流：</p><figure class="highlight lsl"><table><tr><td class="code"><pre><span class="line">nc -lk <span class="number">9000</span></span><br></pre></td></tr></table></figure><p>如果是 Windows 平台，可以通过 <a href="https://nmap.org/ncat/" target="_blank" rel="noopener">https://nmap.org/ncat/</a> 安装 ncat 然后运行：</p><figure class="highlight lsl"><table><tr><td class="code"><pre><span class="line">ncat -lk <span class="number">9000</span></span><br></pre></td></tr></table></figure><p>然后直接运行<code>SocketWindowWordCount</code>的 main 方法。</p><p>只需要在 netcat 控制台输入单词，就能在 <code>SocketWindowWordCount</code> 的输出控制台看到每个单词的词频统计。如果想看到大于1的计数，请在5秒内反复键入相同的单词。</p><p><img src="https://img.alicdn.com/tfs/TB1L_9VnMTqK1RjSZPhXXXfOFXa-1836-682.png" alt></p><p>Cheers! 🎉</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在本文中，我们将从零开始，教您如何构建第一个 Flink 应用程序。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink入门" scheme="http://wuchong.me/tags/Flink%E5%85%A5%E9%97%A8/"/>
    
  </entry>
  
  <entry>
    <title>Flink 小贴士 (2)：Flink 如何管理 Kafka 消费位点</title>
    <link href="http://wuchong.me/blog/2018/11/04/how-apache-flink-manages-kafka-consumer-offsets/"/>
    <id>http://wuchong.me/blog/2018/11/04/how-apache-flink-manages-kafka-consumer-offsets/</id>
    <published>2018-11-04T13:34:38.000Z</published>
    <updated>2022-08-03T06:46:44.507Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文：<a href="https://data-artisans.com/blog/how-apache-flink-manages-kafka-consumer-offsets" target="_blank" rel="noopener">https://data-artisans.com/blog/how-apache-flink-manages-kafka-consumer-offsets</a><br>作者：Fabian Hueske, Markos Sfikas<br>译者：云邪（Jark）</p></blockquote><p>在本周的《Flink Friday Tip》中，我们将结合例子逐步讲解 Apache Flink 是如何与 Apache Kafka 协同工作并确保来自 Kafka topic 的消息以 exactly-once 的语义被处理。</p><p>检查点（Checkpoint）是使 Apache Flink 能从故障恢复的一种内部机制。检查点是 Flink 应用状态的一个一致性副本，包括了输入的读取位点。在发生故障时，Flink 通过从检查点加载应用程序状态来恢复，并从恢复的读取位点继续处理，就好像什么事情都没发生一样。你可以把检查点想象成电脑游戏的存档一样。如果你在游戏中发生了什么事情，你可以随时读档重来一次。</p><p>检查点使得 Apache Flink 具有容错能力，并确保了即时发生故障也能保证流应用程序的语义。检查点是以固定的间隔来触发的，该间隔可以在应用中配置。</p><p>Apache Flink 中实现的 Kafka 消费者是一个有状态的算子（operator），它集成了 Flink 的检查点机制，它的状态是所有 Kafka 分区的读取偏移量。当一个检查点被触发时，每一个分区的偏移量都被存到了这个检查点中。Flink 的检查点机制保证了所有 operator task 的存储状态都是一致的。这里的“一致的”是什么意思呢？意思是它们存储的状态都是<strong>基于相同的输入数据</strong>。当所有的 operator task 成功存储了它们的状态，一个检查点才算完成。因此，当从潜在的系统故障中恢复时，系统提供了 excatly-once 的状态更新语义。</p><a id="more"></a><p>下面我们将一步步地介绍 Apache Flink 中的 Kafka 消费位点是如何做检查点的。在本文的例子中，数据被存在了 Flink 的 JobMaster 中。值得注意的是，在 POC 或生产用例下，这些数据最好是能存到一个外部文件系统（如HDFS或S3）中。</p><h3 id="第一步："><a href="#第一步：" class="headerlink" title="第一步："></a>第一步：</h3><p>如下所示，一个 Kafka topic，有两个partition，每个partition都含有 “A”, “B”, “C”, ”D”, “E” 5条消息。我们将两个partition的偏移量（offset）都设置为0.</p><p><img src="https://img.alicdn.com/tfs/TB1QQ91nhTpK1RjSZR0XXbEwXXa-842-415.png" alt></p><h3 id="第二步："><a href="#第二步：" class="headerlink" title="第二步："></a>第二步：</h3><p>Kafka comsumer（消费者）开始从 partition 0 读取消息。消息“A”正在被处理，第一个 consumer 的 offset 变成了1。</p><p><img src="https://img.alicdn.com/tfs/TB1jBS2nb2pK1RjSZFsXXaNlXXa-842-420.png" alt></p><h3 id="第三步："><a href="#第三步：" class="headerlink" title="第三步："></a>第三步：</h3><p>消息“A”到达了 Flink Map Task。两个 consumer 都开始读取他们下一条消息（partition 0 读取“B”，partition 1 读取“A”）。各自将 offset 更新成 2 和 1 。同时，Flink 的 JobMaster 开始在 source 触发了一个检查点。</p><p><img src="https://img.alicdn.com/tfs/TB1ZNS8nkvoK1RjSZFNXXcxMVXa-842-423.png" alt></p><h3 id="第四步："><a href="#第四步：" class="headerlink" title="第四步："></a>第四步：</h3><p>接下来，由于 source 触发了检查点，Kafka consumer 创建了它们状态的第一个快照（”offset = 2, 1”），并将快照存到了 Flink 的 JobMaster 中。Source 在消息“B”和“A”从partition 0 和 1 发出后，发了一个 checkpoint barrier。Checkopint barrier 用于各个 operator task 之间对齐检查点，保证了整个检查点的一致性。消息“A”到达了 Flink Map Task，而上面的 consumer 继续读取下一条消息（消息“C”）。</p><p><img src="https://img.alicdn.com/tfs/TB1o4TbnkzoK1RjSZFlXXai4VXa-842-447.png" alt></p><h3 id="第五步："><a href="#第五步：" class="headerlink" title="第五步："></a>第五步：</h3><p>Flink Map Task 收齐了同一版本的全部 checkpoint barrier 后，那么就会将它自己的状态也存储到 JobMaster。同时，consumer 会继续从 Kafka 读取消息。</p><p><img src="https://img.alicdn.com/tfs/TB1EI2XngHqK1RjSZFkXXX.WFXa-842-439.png" alt></p><h3 id="第六步："><a href="#第六步：" class="headerlink" title="第六步："></a>第六步：</h3><p>Flink Map Task 完成了它自己状态的快照流程后，会向 Flink JobMaster 汇报它已经完成了这个 checkpoint。当所有的 task 都报告完成了它们的状态 checkpoint 后，JobMaster 就会将这个 checkpoint 标记为成功。从此刻开始，这个 checkpoint 就可以用于故障恢复了。值得一提的是，Flink 并不依赖 Kafka offset 从系统故障中恢复。 </p><p><img src="https://img.alicdn.com/tfs/TB1huHtnhnaK1RjSZFBXXcW7VXa-842-417.png" alt></p><h3 id="故障恢复"><a href="#故障恢复" class="headerlink" title="故障恢复"></a>故障恢复</h3><p>在发生故障时（比如，某个 worker 挂了），所有的 operator task 会被重启，而他们的状态会被重置到最近一次成功的 checkpoint。Kafka source 分别从 offset 2 和 1 重新开始读取消息（因为这是完成的 checkpoint 中存的 offset）。当作业重启后，我们可以期待正常的系统操作，就好像之前没有发生故障一样。如下图所示：</p><p><img src="https://img.alicdn.com/tfs/TB1o8zXnmzqK1RjSZFjXXblCFXa-842-411.png" alt></p><p>如果想了解更多有关如何最佳地使用 Apache Flink 与 Apache Kafka，以及一些常见问题，可以访问我们这篇文章 <a href="https://data-artisans.com/blog/kafka-flink-a-practical-how-to" target="_blank" rel="noopener">Kafka + Flink: A Practical, How-To Guide</a>。</p>]]></content>
    
    <summary type="html">
    
      在本周的《Flink Friday Tip》中，我们将结合例子逐步讲解 Apache Flink 是如何与 Apache Kafka 协同工作并确保来自 Kafka topic 的消息以 exactly-once 的语义被处理。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink小贴士" scheme="http://wuchong.me/tags/Flink%E5%B0%8F%E8%B4%B4%E5%A3%AB/"/>
    
      <category term="Kafka" scheme="http://wuchong.me/tags/Kafka/"/>
    
  </entry>
  
  <entry>
    <title>Flink小贴士 (1)：确定Flink作业所需资源大小时要考虑的6件事</title>
    <link href="http://wuchong.me/blog/2018/10/30/6-things-to-consider-when-defining-your-apache-flink-cluster-size/"/>
    <id>http://wuchong.me/blog/2018/10/30/6-things-to-consider-when-defining-your-apache-flink-cluster-size/</id>
    <published>2018-10-30T02:33:46.000Z</published>
    <updated>2022-08-03T06:46:44.500Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文：<a href="https://data-artisans.com/blog/6-things-to-consider-when-defining-your-apache-flink-cluster-size" target="_blank" rel="noopener">https://data-artisans.com/blog/6-things-to-consider-when-defining-your-apache-flink-cluster-size</a><br>作者：Fabian Hueske<br>译者：云邪（Jark）<br>译注：原文标题是“6 things to consider when defining your Apache Flink cluster size”，其实在确定作业所需资源时要考虑的事情是一样的，而作业所需资源的问题是用户更经常遇到的问题，所以这里将标题修改了下。</p></blockquote><p>本文是我们博客新开系列《Flink Friday Tip》的首篇文章。该系列主要涵盖了易理解的最佳实践、如何提高Flink 性能的建议、以及如何最佳地使用 Flink 的各种功能。</p><blockquote><p>译注：这也是我为什么打算开始翻译这个系列的原因，不过我可能做不到每周五同步更新，但是我会尽量做到每周更新，所以翻译过来的系列名叫做《Flink小贴士》。</p></blockquote><p>在 Apache Flink 社区中我们被经常问及的一件事是：如何规划和计算一个 Flink 集群的大小（或者说如何确定一个 Flink 作业所需的资源）。确定集群的大小很显然是决定于多种因素的，例如应用场景，应用的规模，以及特定的服务等级协议（SLA）。另外应用程序中的 checkpoint 类型（增量 vs 全量）和 Flink 作业处理是连续还是突发的也都会影响到 Flink 集群的大小。</p><p>以下6个方面是确定 Flink 集群大小时最先要考虑的一些因素：</p><p><strong>1. 记录数和每条记录的大小</strong></p><p>确定集群大小的首要事情就是估算预期进入流计算系统的每秒记录数（也就是我们常说的吞吐量），以及每条记录的大小。不同的记录类型会有不同的大小，这将最终影响 Flink 应用程序平稳运行所需的资源。</p><p><strong>2. 不同 key 的数量和每个 key 存储的 state 大小</strong></p><p>应用程序中不同 key 的数量和每个 key 所需要存储的 state 大小，都将影响到 Flink 应用程序所需的资源，从而能高效地运行，避免任何反压。</p><p><strong>3. 状态的更新频率和状态后端的访问模式</strong></p><p>第三个考虑因素是状态的更新频率，因为状态的更新通常是一个高消耗的动作。而不同的状态后端（如 RocksDB，Java Heap）的访问模式差异很大，RocksDB 的每次读取和更新都会涉及序列化和反序列化以及 JNI 操作，而 Java Heap 的状态后端不支持增量 checkpoint，导致大状态场景需要每次持久化的数据量较大。这些因素都会显著地影响集群的大小和 Flink 作业所需的资源。</p><p><strong>4. 网络容量</strong></p><p>网络容量不仅仅会收到 Flink 应用程序本身的影响，也会受到可能正在交互的 Kafka、HDFS 等外部服务的影响。这些外部服务可能会导致额外的网络流量。例如，启用 replication 可能会在网络的消息 broker 之间产生额外的流量。</p><p><strong>5. 磁盘带宽</strong></p><p>如果你的应用程序依赖了基于磁盘的状态后端，如 RocksDB，或者考虑使用 Kafka 或 HDFS，那么磁盘的带宽也需要纳入考虑。</p><p><strong>6. 机器数量及其可用 CPU 和内存</strong></p><p>最后但并非最不重要的，在开始应用部署前，你需要考虑集群中可用机器的数量及其可用的 CPU 和内存。这最终确保了在将应用程序投入生产之后，集群有充足的处理能力。</p><p>更多需要考虑的特定方面是你或者你的组织能接受的SLA（服务等级协议）。例如，考虑你的组织能接受的宕机时间，能接受的延迟和最大吞吐。这些 SLA 都会对 Flink 集群大小产生影响。</p><p>在确定 Flink 作业所需资源数目时，上述所有的因素应该能起到良好的指示作用，另外这也算是提供了 Flink 作业如何正常运行的指引。你也需要始终考虑增加一些资源的缓冲用于作业恢复时的追赶和处理一些负载高峰。例如，当你的 Flink 发生故障时，系统会需要额外的资源来做恢复工作以及从 Kafka topic 或其他消息客户端追上最新的数据。</p><p>如果你对这个话题感兴趣，可以访问我们早期的博客了解更多细节。下图总结展示了上文讨论的6点考虑项，你可以下载保存以备不时之需。</p><p><img src="https://img.alicdn.com/tfs/TB1mCHflNnaK1RjSZFBXXcW7VXa-1064-1064.png" width="600px"></p>]]></content>
    
    <summary type="html">
    
      在 Apache Flink 社区中我们被经常问及的一件事是：如何规划和计算一个 Flink 集群的大小（或者说如何确定一个 Flink 作业所需的资源）。确定集群的大小很显然是决定于多种因素的，例如应用场景，应用的规模，以及特定的服务等级协议（SLA）。另外应用程序中的 checkpoint 类型（增量 vs 全量）和 Flink 作业处理是连续还是突发的也都会影响到 Flink 集群的大小。主要有6个方面是确定 Flink 集群大小时最先要考虑的一些因素。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink小贴士" scheme="http://wuchong.me/tags/Flink%E5%B0%8F%E8%B4%B4%E5%A3%AB/"/>
    
  </entry>
  
  <entry>
    <title>Flink在美团的实践与应用</title>
    <link href="http://wuchong.me/blog/2018/08/25/flink-in-meituan-practice/"/>
    <id>http://wuchong.me/blog/2018/08/25/flink-in-meituan-practice/</id>
    <published>2018-08-25T03:38:58.000Z</published>
    <updated>2022-08-03T06:46:44.502Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>作者：刘迪珊<br>整理：上海-星辰（Flink China社区志愿者）<br>本文整理自8月11日在北京举行的Flink<br>Meetup，分享嘉宾刘迪珊(2015年加入美团数据平台。致力于打造高效、易用的实时计算平台，探索不同场景下实时应用的企业级解决方案及统⼀化服务)。</p></blockquote><h2 id="美团实时计算平台现状和背景"><a href="#美团实时计算平台现状和背景" class="headerlink" title="美团实时计算平台现状和背景"></a>美团实时计算平台现状和背景</h2><h3 id="实时平台架构"><a href="#实时平台架构" class="headerlink" title="实时平台架构"></a>实时平台架构</h3><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fultlwgxz7j21fu0rowj0.jpg" alt></p><p>上图呈现的是当前美团实时计算平台的简要架构。最底层是数据缓存层，可以看到美团测的所有日志类的数据，都是通过统一的日志收集系统收集到Kafka。Kafka作为最大的数据中转层，支撑了美团线上的大量业务，包括离线拉取，以及部分实时处理业务等。在数据缓存层之上，是一个引擎层，这一层的左侧是我们目前提供的实时计算引擎，包括Storm和Flink。Storm在此之前是 standalone 模式的部署方式，Flink由于其现在运行的环境，美团选择的是On YARN模式，除了计算引擎之外，我们还提供一些实时存储功能，用于存储计算的中间状态、计算的结果、以及维度数据等，目前这一类存储包含Hbase、Redis以及ES。在计算引擎之上，是趋于五花八门的一层，这一层主要面向数据开发的同学。实时数据开发面临诸多问题，例如在程序的调试调优方面就要比普通的程序开发困难很多。在数据平台这一层，美团面向用户提供的实时计算平台，不仅可以托管作业，还可以实现调优诊断以及监控报警，此外还有实时数据的检索以及权限管理等功能。除了提供面向数据开发同学的实时计算平台，美团现在正在做的事情还包括构建元数据中心。这也是未来我们想做SQL的一个前提，元数据中心是承载实时流系统的一个重要环节，我们可以把它理解为实时系统中的大脑，它可以存储数据的Schema，Meta。架构的最顶层就是我们现在实时计算平台支撑的业务，不仅包含线上业务日志的实时查询和检索，还涵盖当下十分热门的实时机器学习。机器学习经常会涉及到搜索和推荐场景，这两个场景最显著特点：一、会产生海量实时数据；二、流量的QPS相当高。此时就需要实时计算平台承载部分实时特征的提取工作，实现应用的搜索推荐服务。还有一类是比较常见的场景，包括实时的特征聚合，斑马Watcher（可以认为是一个监控类的服务），实时数仓等。</p><p>以上就是美团目前实时计算平台的简要架构。</p><h3 id="实时平台现状"><a href="#实时平台现状" class="headerlink" title="实时平台现状"></a>实时平台现状</h3><p>美团实时计算平台的现状是作业量现在已经达到了近万，集群的节点的规模是千级别的，天级消息量已经达到了万亿级，高峰期的消息量能够达到千万条每秒。</p><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fultsobawtj21dc09gjtd.jpg" alt></p><h3 id="痛点和问题"><a href="#痛点和问题" class="headerlink" title="痛点和问题"></a>痛点和问题</h3><p>美团在调研使用Flink之前遇到了一些痛点和问题:</p><ul><li><p>实时计算精确性问题：在调研使用Flink之前美团很大规模的作业是基于Storm去开发的，Storm主要的计算语义是At-Least-Once，这种语义在保证正确性上实际上是有一些问题的，在Trident之前Storm是无状态的处理。虽然Storm<br>Trident提供了一个维护状态的精确的开发，但是它是基于串行的Batch提交的，那么遇到问题在处理性能上可能会有一点瓶颈。并且Trident是基于微批的处理，在延迟上没有达到比较高的要求，所以不能满足一些对延迟比较高需求的业务。</p></li><li><p>流处理中的状态管理问题：基于之前的流处理过程中状态管理的问题是非常大的一类问题。状态管理除了会影响到比如说计算状态的一致性，还会影响到实时计算处理的性能以及故障恢复时候的能力。而Flink最突出的一个优势就是状态管理。</p></li><li><p>实时计算表义能力的局限性：在实时计算之前很多公司大部分的数据开发还是面向离线的场景，近几年实时的场景也慢慢火热起来了。那与离线的处理不同的是，实时的场景下，数据处理的表意能力可能有一定的限制，比如说他要进行精确计算以及时间窗口都是需要在此之上去开发很多功能性的东西。</p></li><li><p>开发调试成本高：近千结点的集群上已经跑了近万的作业，分布式的处理的引擎，手工写代码的方式，给数据开发的同学也带来了很高开发和调试的成本，再去维护的时候，运维成本也比较高。</p></li></ul><h3 id="Flink探索关注点"><a href="#Flink探索关注点" class="headerlink" title="Flink探索关注点"></a>Flink探索关注点</h3><p>在上面这些痛点和问题的背景下，美团从去年开始进行Flink的探索，关注点主要有以下4个方面：</p><ul><li><p>ExactlyOnce计算能力</p></li><li><p>状态管理能力</p></li><li><p>窗口/Join/时间处理等等</p></li><li><p>SQL/TableAPI</p></li></ul><h2 id="Flink在美团的实践"><a href="#Flink在美团的实践" class="headerlink" title="Flink在美团的实践"></a>Flink在美团的实践</h2><p>下面带大家来看一下，美团从去年投入生产过程中都遇到了哪些问题，以及一些解决方案，分为下面三个部分：</p><h3 id="稳定性实践"><a href="#稳定性实践" class="headerlink" title="稳定性实践"></a>稳定性实践</h3><h4 id="稳定性实践-资源隔离"><a href="#稳定性实践-资源隔离" class="headerlink" title="稳定性实践-资源隔离"></a>稳定性实践-资源隔离</h4><ul><li><p>资源隔离的考虑：分场景、按业务</p><ol><li>高峰期不同，运维时间不同；</li><li>可靠性、延迟需求不同；</li><li>应用场景，重要性不同</li></ol></li><li><p>资源隔离的策略：</p><ol><li>YARN打标签，节点物理隔离；</li><li>离线DataNode与实时计算节点的隔离</li></ol></li></ul><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fultv3strfj20qu0bqq3y.jpg" alt></p><h4 id="稳定性实践-智能调度"><a href="#稳定性实践-智能调度" class="headerlink" title="稳定性实践-智能调度"></a>稳定性实践-智能调度</h4><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fultytaxupj20ng0mijte.jpg" alt></p><p>智能调度目的也是为了解决资源不均的问题，现在普通的调度策略就是基于CPU，基于内存去调度的。除此之外，在生产过程中也发现了一些其他的问题，比如说Flink是会依赖本地磁盘，进行依赖本地磁盘做本地的状态的存储，所以磁盘IO，还有磁盘的容量，也是一类考虑的问题点，除此之外还包括网卡流量，因为每个业务的流量的状态是不一样的，分配进来会导致流量的高峰，把某一个网卡打满，从而影响其他业务，所以期望的话是说做一些智能调度化的事情。目前暂时能做到的是从cpu和内存两方面，未来会从其他方面做一些更优的调度策略。</p><h4 id="稳定性实践-故障容错"><a href="#稳定性实践-故障容错" class="headerlink" title="稳定性实践-故障容错"></a>稳定性实践-故障容错</h4><ul><li><p>节点/网络故障</p><ul><li><p>JobManagerHA</p></li><li><p>自动拉起</p></li></ul></li></ul><p>与Storm不同的是，知道Storm在遇到异常的时候是非常简单粗暴的，比如说有发生了异常，可能用户没有在代码中进行比较规范的异常处理，但是没有关系，因为worker会重启作业还会继续执行，并且他保证的是At-Least-Once这样的语义，比如说一个网络超时的异常对他而言影响可能并没有那么大，但是Flink不同的是他对异常的容忍度是非常的苛刻的，那时候就考虑的是比如说会发生节点或者是网络的故障，那JobManager单点问题可能就是一个瓶颈，JobManager那个如果挂掉的话，那么可能对整个作业的影响就是不可回复的，所以考虑了做HA，另外一个就是会去考虑一些由于运维的因素而导致的那作业，还有除此之外，可能有一些用户作业是没有开启CheckPoint，但如果是因为节点或者是网络故障导致挂掉，希望会在平台内层做一些自动拉起的策略，去保证作业运行的稳定性。</p><ul><li><p>上下游容错</p><ul><li>FlinkKafka08异常重试</li></ul></li></ul><p>我们的数据源主要是Kafka，读写Kafka是一类非常常见的实时流处理避不开的一个内容，而Kafka本身的集群规模是非常比较大的，因此节点的故障出现是一个常态问题，在此基础上我们对节点故障进行了一些容错，比如说节点挂掉或者是数据均衡的时候，Leader会切换，那本身Flink的读写对Leader的切换容忍度没有那么高，在此基础上我们对一些特定场景的，以及一些特有的异常做的一些优化，进行了一些重试。</p><ul><li><p>容灾</p><ul><li><p>多机房</p></li><li><p>流热备</p></li></ul></li></ul><p>容灾可能大家对考虑的并不多，比如说有没有可能一个机房的所有的节点都挂掉了，或者是无法访问了，虽然它是一个小概率的事件，但它也是会发生的。所以现在也会考虑做多机房的一些部署，包括还有Kafka的一些热备。</p><h3 id="Flink平台化"><a href="#Flink平台化" class="headerlink" title="Flink平台化"></a>Flink平台化</h3><h4 id="Flink平台化-作业管理"><a href="#Flink平台化-作业管理" class="headerlink" title="Flink平台化-作业管理"></a>Flink平台化-作业管理</h4><p>在实践过程中，为了解决作业管理的一些问题，减少用户开发的一些成本，我们做了一些平台化的工作，下图是一个作业提交的界面展示，包括作业的配置，作业生命周期的管理，报警的一些配置，延迟的展示，都是集成在实时计算平台的。</p><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fultzv8jrhj21760ma446.jpg" alt></p><h4 id="Flink平台化-监控报警"><a href="#Flink平台化-监控报警" class="headerlink" title="Flink平台化-监控报警"></a>Flink平台化-监控报警</h4><p>在监控上我们也做了一些事情，对于实时作业来讲，对监控的要求会更高，比如说在作业延迟的时候对业务的影响也比较大，所以做了一些延迟的报警，包括作业状态的报警，比如说作业存活的状态，以及作业运行的状态，还有未来会做一些自定义Metrics的报警。自定义Metrics是未来会考虑基于作业处理本身的内容性，做一些可配置化的一些报警。</p><h4 id="Flink平台化-调优诊断"><a href="#Flink平台化-调优诊断" class="headerlink" title="Flink平台化-调优诊断"></a>Flink平台化-调优诊断</h4><ul><li><p>实时计算引擎提供统一日志和Metrics方案</p></li><li><p>为业务提供按条件过滤的日志检索</p></li><li><p>为业务提供自定义时间跨度的指标查询</p></li><li><p>基于日志和指标，为业务提供可配置的报警</p></li></ul><p>另外就是刚刚提到说在开发实时作业的时候，调优和诊断是一个比较难的痛点，就是用户不是很难去查看分布式的日志，所以也提供了一套统一的解决方案。这套解决方案主要是针对日志和Metrics，会在针对引擎那一层做一些日志和Metrics的上报，那么它会通过统一的日志收集系统，将这些原始的日志，还有Metrics汇集到Kafka那一层。今后Kafka这一层大家可以发现它有两个下游，一方面是做日志到ES的数据同步，目的的话是说能够进入日志中心去做一些日志的检索，另外一方面是通过一些聚合处理流转到写入到OpenTSDB把数据做依赖，这份聚合后的数据会做一些查询，一方面是Metrics的查询展示，另外一方面就是包括实做的一些相关的报警。</p><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fulu0py17bj20tc0ysadu.jpg" alt></p><p>下图是当前某一个作业的一个可支持跨天维度的Metrics的一个查询的页面。可以看到说如果是能够通过纵向的对比，可以发现除了作业在某一个时间点是因为什么情况导致的？比如说延迟啊这样容易帮用户判断一些他的做作业的一些问题。除了作业的运行状态之外，也会先就是采集一些节点的基本信息作为横向的对比</p><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fulu1nzne4j20xi0xqgv2.jpg" alt></p><p>下图是当前的日志的一些查询，它记录了，因为作业在挂掉之后，每一个ApplicationID可能会变化，那么基于作业唯一的唯一的主键作业名去搜集了所有的作业，从创建之初到当前运行的日志，那么可以允许用户的跨Application的日志查询。</p><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fulu2b199ej21ju0wgqhl.jpg" alt></p><h4 id="生态建设"><a href="#生态建设" class="headerlink" title="生态建设"></a>生态建设</h4><p>为了适配这两类MQ做了不同的事情，对于线上的MQ，期望去做一次同步多次消费，目的是避免对线上的业务造成影响，对于的生产类的Kafka就是线下的Kafka，做了一些地址的地址的屏蔽，还有基础基础的一些配置，包括一些权限的管理，还有指标的采集。</p><h3 id="Flink在美团的应用"><a href="#Flink在美团的应用" class="headerlink" title="Flink在美团的应用"></a>Flink在美团的应用</h3><p>下面会给大家讲两个Flink在美团的真实使用的案例。第一个是Petra，Petra其实是一个实时指标的一个聚合的系统，它其实是面向公司的一个统一化的解决方案。它主要面向的业务场景就是基于业务的时间去统计，还有计算一些实时的指标，要求的话是低时延，他还有一个就是说，因为它是面向的是通用的业务，由于业务可能是各自会有各自不同的维度，每一个业务可能包含了包括应用通道机房，还有其他的各自应用各个业务特有的一些维度，而且这些维度可能涉及到比较多，另外一个就是说它可能是就是业务需要去做一些复合的指标的计算，比如说最常见的交易成功率，他可能需要去计算支付的成功数，还有和下单数的比例。另外一个就是说统一化的指标聚合可能面向的还是一个系统，比如说是一些B端或者是R段的一些监控类的系统，那么系统对于指标系统的诉求，就是说我希望指标聚合能够最真最实时最精确的能够产生一些结果，数据保证说它的下游系统能够真实的监控到当前的信息。右边图是我当一个Metrics展示的一个事例。可以看到其他其实跟刚刚讲也是比较类似的，就是说包含了业务的不同维度的一些指标汇聚的结果。</p><h4 id="Petra实时指标聚合"><a href="#Petra实时指标聚合" class="headerlink" title="Petra实时指标聚合"></a>Petra实时指标聚合</h4><ul><li><p>业务场景：</p><ul><li><p>基于业务时间（事件时间）</p></li><li><p>多业务维度：如应用、通道、机房等</p></li><li><p>复合指标计算：如交易成功率=支付成功数/下单数</p></li><li><p>低延迟：秒级结果输出</p></li></ul></li></ul><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fulu3p0o28j216i0o8wwi.jpg" alt></p><ul><li><p>Exactlyonce的精确性保障</p><ul><li>Flinkcheckpoint机制</li></ul></li><li><p>维度计算中数据倾斜</p><ul><li>热点key散列</li></ul></li><li><p>对晚到数据的容忍能力</p><ul><li>窗口的设置与资源的权衡</li></ul></li></ul><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fulu5f77kbj20s40wgq6p.jpg" alt></p><p>在用Flink去做实时指标复核的系统的时候，着重从这几方面去考虑了。第一个方面是说精确的计算，包括使用了FLink和CheckPoint的机制去保证说我能做到不丢不重的计算，第一个首先是由统一化的Metrics流入到一个预聚合的模块，预聚合的模块主要去做一些初始化的一些聚合，其中的为什么会分预聚合和全量聚合主要的解决一类问题，包括就刚刚那位同学问的一个问题，就是数据倾斜的问题，比如说在热点K发生的时候，当前的解决方案也是通过预聚合的方式去做一些缓冲，让尽量把K去打散，再聚合全量聚合模块去做汇聚。那其实也是只能解决一部分问题，所以后面也考虑说在性能的优化上包括去探索状态存储的性能。下面的话还是包含晚到数据的容忍能力，因为指标汇聚可能刚刚也提到说要包含一些复合的指标，那么符合的指标所依赖的数据可能来自于不同的流，即便来自于同一个流，可能每一个数据上报的时候，可能也会有晚到的情况发生，那时候需要去对数据关联做晚到的容忍，容忍的一方面是说可以设置晚到的Lateness的延迟，另一方面是可以设置窗口的长度，但是其实在现实的应用场景上，其实还有一方面考虑就是说除了去尽量的去拉长时间，还要考虑真正的计算成本，所以在这方面也做了一些权衡，那么指标基本就是经过全量聚合之后，聚合结果会回写Kafka，经过数据同步的模块写到OpenTSDB去做，最后去grafana那做指标的展示，另一方面可能去应用到通过Facebook包同步的模块去同步到报警的系统里面去做一些指标，基于指标的报警。</p><p>下图是现在提供的产品化的Petra的一个展示的机示意图，可以看到目前的话就是定义了某一些常用的算子，以及维度的配置，允许用户进行配置话的处理，直接去能够获取到他期望要的指标的一个展示和汇聚的结果。目前还在探索说为Petra基于Sql做一些事情，因为很多用户也比较就是在就是习惯上也可以倾向于说我要去写Sql去完成这样的统计，所以也会基于此说依赖Flink的本身的对SQl还有TableAPI的支持，也会在Sql的场景上进行一些探索。</p><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fulu5waxu9j21be0twwng.jpg" alt></p><h3 id="MLX机器学习平台"><a href="#MLX机器学习平台" class="headerlink" title="MLX机器学习平台"></a>MLX机器学习平台</h3><hr><p>第二类应用就是机器学习的一个场景，机器学习的场景可能会依赖离线的特征数据以及实时的特征数据。一个是基于现有的离线场景下的特征提取，经过了批处理，流转到了离线的集群。另外一个就是近线模式，近线模式出的数据就是现有的从日志收集系统流转过来的统一的日志，经过Flink的处理，就是包括流的关联以及特征的提取，再做模型的训练，流转到最终的训练的集群，训练的集群会产出P的特征，还有都是Delta的特征，最终将这些特征影响到线上的线上的特征的一个训练的一个服务上。这是一个比较常见的，比如说比较就是通用的也是比较通用的一个场景，目前的话主要应用的方可能包含了搜索还有推荐，以及一些其他的业务。</p><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fulu6o90adj21bg0x8n5m.jpg" alt></p><h2 id="未来展望"><a href="#未来展望" class="headerlink" title="未来展望"></a>未来展望</h2><p>未来的话可能也是通过也是期望在这三方面进行做一些更多的事情，刚刚也提到了包括状态的管理，第一个是状态的统一的，比如说Sql化的统一的管理，希望有统一的配置，帮用户去选择一些期望的回滚点。另外一个就是大状态的性能优化，因为比如说像做一些流量数据的双流的关联的时候，现在也遇到了一些性能瓶颈的问题，对于说啊基于内存型的状态，基于内存型的数据的处理，以及基于RocksDB的状态的处理，做过性能的比较，发现其实性能的差异还是有一些大的，所以希望说在基于RocksDBBackend的上面能够去尽量去更多的做一些优化，从而提升作业处理的性能。第二方面就是Sql，Sql的话应该是每一个位就是当前可能各个公司都在做的一个方向，因为之前也有对Sql做一些探索，包括提供了基于Storm的一些Sql的表示，但是可能对于之前的话对于与语义的表达可能会有一些欠缺，所以希望说在基于Flink可去解决这些方面的事情，以及包括Sql的并发度的一些配置的优化，包括Sql的查询的一些优化，都希望说在Flink未来能够去优化更多的东西，去真正能使Sql应用到生产的环境。</p><p>另外一方面的话就是会进行新的场景的也在做新的场景的一些探索，期望是比如说包括刚刚也提到说除了流式的处理，也期望说把离线的场景下的数据进行一些合并，通过统一的Sql的API去提供给业务做更多的服务，包括流处理，还有批处理的结合。</p>]]></content>
    
    <summary type="html">
    
      本文整理自8月11日在北京举行的Flink Meetup。美团目前主要用到的实时计算引擎是 Flink 和 Storm。Storm 采用的是 standalone 模式部署，Flink 采用的是 ON YARN 的模式不是。由于 Flink 很多设计上的优越性，美团现在大量的业务正在基于 Flink 搭建。
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
  </entry>
  
  <entry>
    <title>我在阿里的这两年</title>
    <link href="http://wuchong.me/blog/2017/07/16/two-years-in-alibaba/"/>
    <id>http://wuchong.me/blog/2017/07/16/two-years-in-alibaba/</id>
    <published>2017-07-16T12:43:38.000Z</published>
    <updated>2022-08-03T06:46:44.512Z</updated>
    
    <content type="html"><![CDATA[<p>这两天，团队来了不少新人，有实习生也有刚毕业的大学生。看着他们稚气的面庞，文静而腼腆的样子，跟我两年前刚进公司的时候好像。今天内网提示我这是我在阿里的第732天，两周年快乐。突然觉得，我应该写点什么东西，一方面给自己一个总结，分享一下自己成长的故事。另一方面，希望能帮助刚毕业的大学生们消除一些成长路上的疑惑。</p><p><img src="http://ww1.sinaimg.cn/large/81b78497gy1fhljtdhofaj21ue0s6x6q.jpg" alt></p><a id="more"></a><h2 id="JStorm"><a href="#JStorm" class="headerlink" title="JStorm"></a>JStorm</h2><p>我是2015年7月毕业后加入的公司，当时进入的是中间件-实时计算 JStorm 团队。JStorm 是用 Java 语言代替 Clojure 语言重写了 Apache Storm，并在原有的基础上做了诸多性能和功能优化。JStorm 是阿里巴巴开源的几个明星产品之一，在国内的用户非常多。很多国内做流计算，实时计算的应该都知道 JStorm。</p><p>但是我当时并不知道 JStorm 或是 Storm，我当时只知道 Spark，也不懂什么是实时计算，流计算。所以<br>都是入职之后现学，看文档，看源码，学习 Clojure。差不多11月份的时候，阿里正式向 Apache 基金会捐赠了 JStorm。这是阿里捐赠给 Apache 的第一个项目，后面还有 <a href="https://rocketmq.incubator.apache.org/" target="_blank" rel="noopener">Apache RocketMQ</a>, <a href="https://weex.incubator.apache.org/cn/" target="_blank" rel="noopener">Apache Weex</a>。JStorm 火了一把，当时有很多报道转载这件事情，还放了一张我们团队油头垢面、屌丝气十足的合照。</p><p>在第一个半年里，我重写了 JStorm 的开源 UI，参与了管控平台的开发，做了 JStorm 的一些开发工作。到了半年 Review 的时候，我觉得我做的并不好（虽然老板一直鼓励我做的挺好…）。其实作为一个新人，半年时间是很难做出成绩的。但是从一开始我对自己的期望就比较高，所以失望也比较大。而且看到同期的应届生半年就有非常出色的成果，相比之下就相形见绌了。那段时间，自己的心情也比较低落，比较迷茫。我觉得我脑子不笨，学习能力也不差，工作也很努力，为什么就做不出什么“成果”呢？</p><p>在阿里中间件，有很多非常耀眼的新人，有的在我还在熟悉工作的时候人家已经成为了项目 owner，有的已经成为了 Apache 项目的 PMC，有的一年就晋升到了 P6。他们有很多地方是值得学习的，聪明，拼搏，机遇，是我觉得最重要的几个关键词。但是工作后你就会发现，哪有那么多的机会在等你呢。不过，关于个人成长和晋升，阿里有句老话叫做“没有坑，就让自己先成为萝卜”，我非常认同，是萝卜的话，那个坑是迟早的事情。</p><h2 id="Flink"><a href="#Flink" class="headerlink" title="Flink"></a>Flink</h2><p>2015 年是流计算百花齐放的时代，各个流计算框架层出不穷。Storm, JStorm, Heron, Flink, Spark Streaming, Google Dataflow (后来的 Beam) 等等。其中 Flink 的一致性语义和最接近 Dataflow 模型的开源实现，使其成为流计算框架中最耀眼的一颗。我也是看中了 Flink 在流计算上的先进性，所以在 2016 年春节过后回来上班的第一天，我跟老板提议，希望能去研究 Flink，我们团队需要有个人透彻了解 Flink 的原理（我希望成为 Flink 的萝卜，事实证明这为我之后带来了很多机会）。Boss 同意了，并且给了我一个 KPI：一年内成为 Flink Committer。</p><p>后来我才知道搜索部门已经研究 Flink 有一年了，并且有了个内部版本，名叫“Blink”。所以之后，我们便与 Blink 开始共建 Table API &amp; SQL。我也是在那个时候进入 Flink 社区工作。那个时候在社区工作是非常孤独，非常艰难的，因为很多时候没有人可以一起讨论，一个人在社区推进事情也很困难。不过，当你推进的提议被社区所接受并落地的那种心情是非常有成就感的。</p><p>记得刚进入社区工作的时候，我也只能挑一些非常简单的修复 Bug 的任务，学习社区的工作流程，用我非常蹩脚的英文在 GitHub 上与他们交流。有时候，每写一句话都要粘贴到 Google 翻译中翻译一遍，确保自己语句没有问题。我对学习英语没什么天赋，大学考英语四级考了三次才过。但是孰能生巧，现在我也可以流畅地在 GitHub 、邮件列表上与他们交流，能洋洋洒洒地写几千单词的英文设计文档与社区讨论，能与 dataArtisans 电话会议讨论设计细节。英语是 IT 人士非常重要的软实力，我觉得至少要做到读英文技术文章不吃力，其次要做到能用英语流畅交流技术。我现在已经越来越体会到英语的重要性，我觉得英语在某些方面甚至决定了你的潜力，为此我还买了一个半年的英语课程，每天练习自己的听力和口语，现在已经坚持了一个半月了，希望能坚持到底。</p><p>在社区工作了将近一年，终于在 2017 年春节的时候，收到了社区邀请我成为 Flink Committer 的邮件，这是对我过去一年工作的肯定，也算是踩点完成了 KPI … 我很荣幸是阿里第一位成为 Flink Committer 的。截止到目前，阿里已经发展了 5 位 Flink Committer，当然都在我们大团队 😉。</p><h2 id="Blink"><a href="#Blink" class="headerlink" title="Blink"></a>Blink</h2><p>阿里还有一句老话叫“拥抱变化”。大概五月份的时候，为了打造世界级的流计算引擎和服务，我们团队和 Blink 都加入了新成立的计算平台事业部。在新的事业部，我迎来了自己的第一次晋升。晋升的过程非常愉快。令我印象深刻的是，在这么一个P5到P6的晋升面试上居然要出动1个P10，1个P9，3个P8，1个HRG的阵容。公司也真是舍得下成本啊。</p><p>虽然“拥抱变化”了，不过在新事业部我做的事情没什么改变，仍然是 Flink/Blink SQL 相关的工作。从一开始我们就意识 SQL 在抽象和统一用户业务逻辑上的强大之处。而且，流和批的计算可以自然而然的在传统SQL这一层统一。SQL 可以把一个非常复杂的计算用简单的抽象表达出来。这是我认为我现在工作有意思有挑战的地方。在社区方面，Flink SQL 总共有 5 位 Committer，我们团队占据了其中三位。我们与社区的合作是非常紧密的，平均每个星期都有与 dataArtisans （Flink 背后的公司） 的视频会议，讨论每周的技术设计细节。可以说，流计算 SQL 在开源范围内我们是比较领先的，甚至是在定义流计算SQL的标准。dataArtisans 的 CTO Stephan 说，阿里巴巴是 Flink 现在的最大的贡献者。确实如此，不仅在 Flink SQL，在 Runtime 方面，我们帮助社区贡献了若干从大规模部署到性能，再到容错方面的优化。这些优化使得 Flink 的易用性和性能得到了大大的提升。</p><p>哈哈，是的，这里不免落俗地发个招聘广告。阿里实时计算团队（杭州，北京，美国）诚邀各种牛人（存储，计算，分布式，大数据，甚至前端）加入，有感兴趣的可以直接联系我：imjark#gmail.com 。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;这两天，团队来了不少新人，有实习生也有刚毕业的大学生。看着他们稚气的面庞，文静而腼腆的样子，跟我两年前刚进公司的时候好像。今天内网提示我这是我在阿里的第732天，两周年快乐。突然觉得，我应该写点什么东西，一方面给自己一个总结，分享一下自己成长的故事。另一方面，希望能帮助刚毕业的大学生们消除一些成长路上的疑惑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ww1.sinaimg.cn/large/81b78497gy1fhljtdhofaj21ue0s6x6q.jpg&quot; alt&gt;&lt;/p&gt;
    
    </summary>
    
      <category term="职场生涯" scheme="http://wuchong.me/categories/%E8%81%8C%E5%9C%BA%E7%94%9F%E6%B6%AF/"/>
    
    
      <category term="职场生涯" scheme="http://wuchong.me/tags/%E8%81%8C%E5%9C%BA%E7%94%9F%E6%B6%AF/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：Aysnc I/O</title>
    <link href="http://wuchong.me/blog/2017/05/17/flink-internals-async-io/"/>
    <id>http://wuchong.me/blog/2017/05/17/flink-internals-async-io/</id>
    <published>2017-05-17T12:53:38.000Z</published>
    <updated>2022-08-03T06:46:44.503Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>Async I/O 是阿里巴巴贡献给社区的一个呼声非常高的特性，于1.2版本引入。主要目的是为了解决与外部系统交互时网络延迟成为了系统瓶颈的问题。</p><a id="more"></a><p>流计算系统中经常需要与外部系统进行交互，比如需要查询外部数据库以关联上用户的额外信息。通常，我们的实现方式是向数据库发送用户<code>a</code>的查询请求，然后等待结果返回，在这之前，我们无法发送用户<code>b</code>的查询请求。这是一种同步访问的模式，如下图左边所示。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB11fwcRXXXXXXwXpXXXXXXXXXX" alt="图片来自官方文档"><span class="caption">图片来自官方文档</span></p><p>图中棕色的长条表示等待时间，可以发现网络等待时间极大地阻碍了吞吐和延迟。为了解决同步访问的问题，异步模式可以并发地处理多个请求和回复。也就是说，你可以连续地向数据库发送用户<code>a</code>、<code>b</code>、<code>c</code>等的请求，与此同时，哪个请求的回复先返回了就处理哪个回复，从而连续的请求之间不需要阻塞等待，如上图右边所示。这也正是 Async I/O 的实现原理。</p><h2 id="Async-I-O"><a href="#Async-I-O" class="headerlink" title="Async I/O"></a>Async I/O</h2><p>使用 Async I/O 的前提是需要一个支持异步请求的客户端。当然，没有异步请求客户端的话也可以将同步客户端丢到线程池中执行作为异步客户端。Flink 提供了非常简洁的API，让用户只需要关注业务逻辑，一些脏活累活比如消息顺序性和一致性保证都由框架处理了，多么棒的事情！</p><p>使用方式如下方代码片段所示（来自官网文档）：</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="comment">/** 'AsyncFunction' 的一个实现，向数据库发送异步请求并设置回调 */</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AsyncDatabaseRequest</span> <span class="keyword">extends</span> <span class="title">AsyncFunction</span>[<span class="type">String</span>, (<span class="type">String</span>, <span class="type">String</span>)] </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/** 可以异步请求的特定数据库的客户端 */</span></span><br><span class="line">    <span class="keyword">lazy</span> <span class="keyword">val</span> client: <span class="type">DatabaseClient</span> = <span class="keyword">new</span> <span class="type">DatabaseClient</span>(host, post, credentials)</span><br><span class="line"></span><br><span class="line">    <span class="comment">/** future 的回调的执行上下文（当前线程） */</span></span><br><span class="line">    <span class="keyword">implicit</span> <span class="keyword">lazy</span> <span class="keyword">val</span> executor: <span class="type">ExecutionContext</span> = <span class="type">ExecutionContext</span>.fromExecutor(<span class="type">Executors</span>.directExecutor())</span><br><span class="line"></span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">def</span> <span class="title">asyncInvoke</span></span>(str: <span class="type">String</span>, asyncCollector: <span class="type">AsyncCollector</span>[(<span class="type">String</span>, <span class="type">String</span>)]): <span class="type">Unit</span> = &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 发起一个异步请求，返回结果的 future</span></span><br><span class="line">        <span class="keyword">val</span> resultFuture: <span class="type">Future</span>[<span class="type">String</span>] = client.query(str)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置请求完成时的回调: 将结果传递给 collector</span></span><br><span class="line">        resultFuture.onSuccess &#123;</span><br><span class="line">            <span class="keyword">case</span> result: <span class="type">String</span> =&gt; asyncCollector.collect(<span class="type">Iterable</span>((str, result)));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建一个原始的流</span></span><br><span class="line"><span class="keyword">val</span> stream: <span class="type">DataStream</span>[<span class="type">String</span>] = ...</span><br><span class="line"></span><br><span class="line"><span class="comment">// 添加一个 async I/O 的转换</span></span><br><span class="line"><span class="keyword">val</span> resultStream: <span class="type">DataStream</span>[(<span class="type">String</span>, <span class="type">String</span>)] =</span><br><span class="line">    <span class="type">AsyncDataStream</span>.(un)orderedWait(</span><br><span class="line">      stream, <span class="keyword">new</span> <span class="type">AsyncDatabaseRequest</span>(),</span><br><span class="line">      <span class="number">1000</span>, <span class="type">TimeUnit</span>.<span class="type">MILLISECONDS</span>, <span class="comment">// 超时时间</span></span><br><span class="line">      <span class="number">100</span>)  <span class="comment">// 进行中的异步请求的最大数量</span></span><br></pre></td></tr></table></figure><p><code>AsyncDataStream</code> 有两个静态方法，<code>orderedWait</code> 和 <code>unorderedWait</code>，对应了两种输出模式：有序和无序。</p><ul><li>有序：消息的发送顺序与接受到的顺序相同（包括 watermark ），也就是先进先出。</li><li>无序：<ul><li>在 ProcessingTime 的情况下，完全无序，先返回的结果先发送。</li><li>在 EventTime 的情况下，watermark 不能超越消息，消息也不能超越 watermark，也就是说 watermark 定义的顺序的边界。在两个 watermark 之间的消息的发送是无序的，但是在watermark之后的消息不能先于该watermark之前的消息发送。</li></ul></li></ul><h2 id="原理实现"><a href="#原理实现" class="headerlink" title="原理实现"></a>原理实现</h2><p><code>AsyncDataStream.(un)orderedWait</code> 的主要工作就是创建了一个 <code>AsyncWaitOperator</code>。<code>AsyncWaitOperator</code> 是支持异步 IO 访问的算子实现，该算子会运行 <code>AsyncFunction</code> 并处理异步返回的结果，其内部原理如下图所示。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1tBPQRXXXXXX2aXXXXXXXXXXX" alt></p><p>如图所示，<code>AsyncWaitOperator</code> 主要由两部分组成：<code>StreamElementQueue</code> 和 <code>Emitter</code>。StreamElementQueue 是一个 Promise 队列，所谓 Promise 是一种异步抽象表示将来会有一个值（参考 <a href="http://docs.scala-lang.org/zh-cn/overviews/core/futures.html#promises" target="_blank" rel="noopener">Scala Promise</a> 了解更多），这个队列是未完成的 Promise 队列，也就是进行中的请求队列。Emitter 是一个单独的线程，负责发送消息（收到的异步回复）给下游。</p><p>图中<code>E5</code>表示进入该算子的第五个元素（”Element-5”），在执行过程中首先会将其包装成一个 “Promise” <code>P5</code>，然后将<code>P5</code>放入队列。最后调用 <code>AsyncFunction</code> 的 <code>ayncInvoke</code> 方法，该方法会向外部服务发起一个异步的请求，并注册回调。该回调会在异步请求成功返回时调用 <code>AsyncCollector.collect</code> 方法将返回的结果交给框架处理。实际上 <code>AsyncCollector</code> 是一个 Promise ，也就是 <code>P5</code>，在调用 <code>collect</code> 的时候会标记 Promise 为完成状态，并通知 Emitter 线程有完成的消息可以发送了。Emitter 就会从队列中拉取完成的 Promise ，并从 Promise 中取出消息发送给下游。</p><h3 id="消息的顺序性"><a href="#消息的顺序性" class="headerlink" title="消息的顺序性"></a>消息的顺序性</h3><p>上文提到 Async I/O 提供了两种输出模式。其实细分有三种模式: 有序，ProcessingTime 无序，EventTime 无序。Flink 使用队列来实现不同的输出模式，并抽象出一个队列的接口（<code>StreamElementQueue</code>），这种分层设计使得<code>AsyncWaitOperator</code>和<code>Emitter</code>不用关心消息的顺序问题。<code>StreamElementQueue</code>有两种具体实现，分别是 <code>OrderedStreamElementQueue</code> 和 <code>UnorderedStreamElementQueue</code>。<code>UnorderedStreamElementQueue</code> 比较有意思，它使用了一套逻辑巧妙地实现完全无序和 EventTime 无序。</p><h4 id="有序"><a href="#有序" class="headerlink" title="有序"></a>有序</h4><p>有序比较简单，使用一个队列就能实现。所有新进入该算子的元素（包括 watermark），都会包装成 Promise 并按到达顺序放入该队列。如下图所示，尽管<code>P4</code>的结果先返回，但并不会发送，只有 <code>P1</code> （队首）的结果返回了才会触发 Emitter 拉取队首元素进行发送。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB16DYTRXXXXXbWXVXXXXXXXXXX" alt></p><h4 id="ProcessingTime-无序"><a href="#ProcessingTime-无序" class="headerlink" title="ProcessingTime 无序"></a>ProcessingTime 无序</h4><p>ProcessingTime 无序也比较简单，因为没有 watermark，不需要协调 watermark 与消息的顺序性，所以使用两个队列就能实现，一个 <code>uncompletedQueue</code> 一个 <code>completedQueue</code>。所有新进入该算子的元素，同样的包装成 Promise 并放入 <code>uncompletedQueue</code> 队列，当<code>uncompletedQueue</code>队列中任意的Promise返回了数据，则将该 Promise 移到 <code>completedQueue</code> 队列中，并通知 Emitter 消费。如下图所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1zzsnRXXXXXbNXXXXXXXXXXXX" alt></p><h4 id="EventTime-无序"><a href="#EventTime-无序" class="headerlink" title="EventTime 无序"></a>EventTime 无序</h4><p>EventTime 无序类似于有序与 ProcessingTime 无序的结合体。因为有 watermark，需要协调 watermark 与消息之间的顺序性，所以<code>uncompletedQueue</code>中存放的元素从原先的 Promise 变成了 Promise 集合。如果进入算子的是消息元素，则会包装成 Promise 放入队尾的集合中。如果进入算子的是 watermark，也会包装成 Promise 并放到一个独立的集合中，再将该集合加入到 <code>uncompletedQueue</code> 队尾，最后再创建一个空集合加到 <code>uncompletedQueue</code> 队尾。这样，watermark 就成了消息顺序的边界。只有处在队首的集合中的 Promise 返回了数据，才能将该 Promise 移到 <code>completedQueue</code> 队列中，由 Emitter 消费发往下游。只有队首集合空了，才能处理第二个集合。这样就保证了当且仅当某个 watermark 之前所有的消息都已经被发送了，该 watermark 才能被发送。过程如下图所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1TnrsRXXXXXaKaVXXXXXXXXXX" alt></p><h3 id="快照与恢复"><a href="#快照与恢复" class="headerlink" title="快照与恢复"></a>快照与恢复</h3><p>分布式快照机制是为了保证状态的一致性。我们需要分析哪些状态是需要快照的，哪些是不需要的。首先，已经完成回调并且已经发往下游的元素是不需要快照的。否则，会导致重发，那就不是 exactly-once 了。而已经完成回调且未发往下游的元素，加上未完成回调的元素，就是上述队列中的所有元素。</p><p>所以快照的逻辑也非常简单，(1)清空原有的状态存储，(2)遍历队列中的所有 Promise，从中取出 <code>StreamElement</code>（消息或 watermark）并放入状态存储中，(3)执行快照操作。 </p><p>恢复的时候，从快照中读取所有的元素全部再处理一次，当然包括之前已完成回调的元素。所以在失败恢复后，会有元素重复请求外部服务，但是每个回调的结果只会被发往下游一次。</p><blockquote><p>本文的原理和实现分析基于 Flink 1.3 版本。</p></blockquote><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://ci.apache.org/projects/flink/flink-docs-release-1.3/dev/stream/asyncio.html" target="_blank" rel="noopener">Asynchronous I/O for External Data Access</a></li><li><a href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=65870673" target="_blank" rel="noopener">FLIP-12: Asynchronous I/O Design and Implementation</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h2&gt;&lt;p&gt;Async I/O 是阿里巴巴贡献给社区的一个呼声非常高的特性，于1.2版本引入。主要目的是为了解决与外部系统交互时网络延迟成为了系统瓶颈的问题。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：Table &amp; SQL API</title>
    <link href="http://wuchong.me/blog/2017/03/30/flink-internals-table-and-sql-api/"/>
    <id>http://wuchong.me/blog/2017/03/30/flink-internals-table-and-sql-api/</id>
    <published>2017-03-30T15:51:16.000Z</published>
    <updated>2022-08-03T06:46:44.504Z</updated>
    
    <content type="html"><![CDATA[<p>Flink 已经拥有了强大的 DataStream/DataSet API，可以基本满足流计算和批计算中的所有需求。为什么还需要 Table &amp; SQL API 呢？</p><p>首先 Table API 是一种关系型API，类 SQL 的API，用户可以像操作表一样地操作数据，非常的直观和方便。用户只需要说需要什么东西，系统就会自动地帮你决定如何最高效地计算它，而不需要像 DataStream 一样写一大堆 Function，优化还得纯靠手工调优。另外，SQL 作为一个“人所皆知”的语言，如果一个引擎提供 SQL，它将很容易被人们接受。这已经是业界很常见的现象了。值得学习的是，Flink 的 Table API 与 SQL API 的实现，有 80% 的代码是共用的。所以当我们讨论 Table API 时，常常是指 Table &amp; SQL API。</p><p>Table &amp; SQL API 还有另一个职责，就是流处理和批处理统一的API层。Flink 在runtime层是统一的，因为Flink将批任务看做流的一种特例来执行，这也是 Flink 向外鼓吹的一点。然而在编程模型上，Flink 却为批和流提供了两套API （DataSet 和 DataStream）。为什么 runtime 统一，而编程模型不统一呢？ 在我看来，这是本末倒置的事情。用户才不管你 runtime 层是否统一，用户更关心的是写一套代码。这也是为什么现在 Apache Beam 能这么火的原因。所以 Table &amp; SQL API 就扛起了统一API的大旗，批上的查询会随着输入数据的结束而结束并生成有限结果集，流上的查询会一直运行并生成结果流。Table &amp; SQL API 做到了批与流上的查询具有同样的语法，因此不用改代码就能同时在批和流上跑。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB15GhOQpXXXXaYapXXXXXXXXXX" width="450px"></p><a id="more"></a><h2 id="聊聊历史"><a href="#聊聊历史" class="headerlink" title="聊聊历史"></a>聊聊历史</h2><p>Table API 始于 Flink 0.9，Flink 0.9 是一个类库百花齐放的版本，众所周知的 Table API, Gelly, FlinkML 都是在这个版本加进去的。Flink 0.9 大概是在2015年6月正式发布的，在 Flink 0.9 发布之前，社区对 SQL 展开过好几次争论，不过当时社区认为应该首先完善 Table API 的功能，再去搞SQL，如果两头一起搞很容易什么都做不好。而且在整个Hadoop生态圈中已经有大量的所谓 “SQL-on-Hadoop” 的解决方案，譬如 <a href="https://hive.apache.org/" target="_blank" rel="noopener">Apache Hive</a>, <a href="https://drill.apache.org/" target="_blank" rel="noopener">Apache Drill</a>, <a href="http://impala.io/" target="_blank" rel="noopener">Apache Impala</a>。”SQL-on-Flink”的事情也可以像 Hadoop 一样丢给其他社区去搞。</p><p>不过，随着 Flink 0.9 的发布，意味着抽象语法树、代码生成、运行时函数等都已经成熟，这为SQL的集成铺好了前进道路。另一方面，用户对 SQL 的呼声越来越高。2015年下半年，Timo 大神也加入了 dataArtisans，于是对Table API的改造开始了。2016 年初的时候，改造基本上完成了。我们也是在这个时间点发现了 Table API 的潜力，并加入了社区。经过这一年的努力，Flink 已经发展成 Apache 中最火热的项目之一，而 Flink 中最活跃的类库目前非 Table API 莫属。这其中离不开国内公司的支持，Table API 的贡献者绝大多数都来自于阿里巴巴和华为，并且主导着 Table API 的发展方向，这是非常令国人自豪的。而我在社区贡献了一年后，幸运地成为了 Flink Committer。</p><h2 id="Table-API-amp-SQL-长什么样？"><a href="#Table-API-amp-SQL-长什么样？" class="headerlink" title="Table API &amp; SQL 长什么样？"></a>Table API &amp; SQL 长什么样？</h2><p>这里不会详细介绍 Table API &amp; SQL 的使用，只是做一个展示。更多使用细节方面的问题请访问<a href="https://ci.apache.org/projects/flink/flink-docs-release-1.3/dev/table_api.html" target="_blank" rel="noopener">官网文档</a>。</p><p>下面这个例子展示了如何用 Table API 处理温度传感器数据。计算每天每个以<code>room</code>开头的location的平均温度。例子中涉及了如何使用window，event-time等。</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> sensorData: <span class="type">DataStream</span>[(<span class="type">String</span>, <span class="type">Long</span>, <span class="type">Double</span>)] = ???</span><br><span class="line"></span><br><span class="line"><span class="comment">// convert DataSet into Table</span></span><br><span class="line"><span class="keyword">val</span> sensorTable: <span class="type">Table</span> = sensorData</span><br><span class="line">  .toTable(tableEnv, <span class="symbol">'location</span>, <span class="symbol">'time</span>, <span class="symbol">'tempF</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// define query on Table</span></span><br><span class="line"><span class="keyword">val</span> avgTempCTable: <span class="type">Table</span> = sensorTable </span><br><span class="line">  .window(<span class="type">Tumble</span> over <span class="number">1.</span>day on <span class="symbol">'rowtime</span> as <span class="symbol">'w</span>) </span><br><span class="line">  .groupBy(<span class="symbol">'location</span>, <span class="symbol">'w</span>)</span><br><span class="line">  .select(<span class="symbol">'w</span>.start as <span class="symbol">'day</span>, <span class="symbol">'location</span>, ((<span class="symbol">'tempF</span>.avg - <span class="number">32</span>) * <span class="number">0.556</span>) as <span class="symbol">'avgTempC</span>)</span><br><span class="line">  .where(<span class="symbol">'location</span> like <span class="string">"room%"</span>)</span><br></pre></td></tr></table></figure><p>下面的例子是展示了如何用 SQL 来实现。</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> sensorData: <span class="type">DataStream</span>[(<span class="type">String</span>, <span class="type">Long</span>, <span class="type">Double</span>)] = ???</span><br><span class="line"></span><br><span class="line"><span class="comment">// register DataStream</span></span><br><span class="line">tableEnv.registerDataStream(<span class="string">"sensorData"</span>, sensorData, <span class="symbol">'location</span>, ’time, <span class="symbol">'tempF</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// query registered Table</span></span><br><span class="line"><span class="keyword">val</span> avgTempCTable: <span class="type">Table</span> = tableEnv.sql(<span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">  SELECT FLOOR(rowtime() TO DAY) AS day, location, </span></span><br><span class="line"><span class="string">    AVG((tempF - 32) * 0.556) AS  avgTempC</span></span><br><span class="line"><span class="string">  FROM sensorData</span></span><br><span class="line"><span class="string">  WHERE location LIKE 'room%'</span></span><br><span class="line"><span class="string">  GROUP BY location, FLOOR(rowtime() TO DAY) "</span><span class="string">""</span>)</span><br></pre></td></tr></table></figure><h2 id="Table-API-amp-SQL-原理"><a href="#Table-API-amp-SQL-原理" class="headerlink" title="Table API &amp; SQL 原理"></a>Table API &amp; SQL 原理</h2><p>Flink 非常明智，没有像Spark那样重复造轮子(Spark Catalyst)，而是将 SQL 校验、SQL 解析以及 SQL 优化交给了 <a href="https://calcite.apache.org/" target="_blank" rel="noopener">Apache Calcite</a>。Calcite 在其他很多开源项目里也都应用到了，譬如Apache Hive, Apache Drill, Apache Kylin, Cascading。Calcite 在新的架构中处于核心的地位，如下图所示。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1mENHQpXXXXacaFXXXXXXXXXX" alt></p><p>新的架构中，构建抽象语法树的事情全部交给了 Calcite 去做。SQL query 会经过 Calcite 解析器转变成 SQL 节点树，通过验证后构建成 Calcite 的抽象语法树（也就是图中的 Logical Plan）。另一边，Table API 上的调用会构建成 Table API 的抽象语法树，并通过 Calcite 提供的 RelBuilder 转变成 Calcite 的抽象语法树。</p><p>以上面的温度计代码为样例，Table API 和 SQL 的转换流程如下，绿色的节点代表 Flink Table Nodes，蓝色的节点代表 Calcite Logical Nodes。最终都转化成了相同的 Logical Plan 表现形式。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1cr1qQpXXXXaMXpXXXXXXXXXX" alt></p><p>之后会进入优化器，Calcite 会基于优化规则来优化这些 Logical Plan，根据运行环境的不同会应用不同的优化规则（Flink提供了批的优化规则，和流的优化规则）。这里的优化规则分为两类，一类是Calcite提供的内置优化规则（如条件下推，剪枝等），另一类是是将Logical Node转变成 Flink Node 的规则。这两类规则的应用体现为下图中的①和②步骤，这两步骤都属于 Calcite 的优化阶段。得到的 DataStream Plan 封装了如何将节点翻译成对应 DataStream/DataSet 程序的逻辑。步骤③就是将不同的 DataStream/DataSet Node 通过代码生成（CodeGen）翻译成最终可执行的 DataStream/DataSet 程序。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1EW5eQpXXXXamXFXXXXXXXXXX" alt></p><p>代码生成是 Table API &amp; SQL 中最核心的一块内容。表达式、条件、内置函数等等是需要CodeGen出具体的Function 代码的，这部分跟Spark SQL的结构很相似。CodeGen 出的Function以字符串的形式存在。在提交任务后会分发到各个 TaskManager 中运行，在运行时会使用 <a href="http://janino-compiler.github.io/janino/" target="_blank" rel="noopener">Janino</a> 编译器编译代码后运行。</p><h2 id="Table-API-amp-SQL-现状"><a href="#Table-API-amp-SQL-现状" class="headerlink" title="Table API &amp; SQL 现状"></a>Table API &amp; SQL 现状</h2><p>目前 Table API 对于批和流都已经支持了基本的Selection, Projection, Union，以及 Window 操作（包括固定窗口、滑动窗口、会话窗口）。SQL 的话由于 Calcite 在最近的版本中才支持 Window 语法，所以目前 Flink SQL 还不支持 Window 的语法。并且 Table API 和 SQL 都支持了UDF,UDTF,UDAF(开发中)。</p><h2 id="Table-API-amp-SQL-未来"><a href="#Table-API-amp-SQL-未来" class="headerlink" title="Table API &amp; SQL 未来"></a>Table API &amp; SQL 未来</h2><ol><li><p>Dynamic Tables</p><p>Dynamic Table 就是传统意义上的表，只不过表中的数据是会变化更新的。Flink 提出 Stream <--> Dynamic Table 之间是可以等价转换的。不过这需要引入Retraction机制。有机会的话，我会专门写一篇文章来介绍。</--></p></li><li><p>Joins</p><p>包括了支持流与流的 Join，以及流与表的 Join。</p></li><li><p>SQL 客户端</p><p>目前 SQL 是需要内嵌到 Java/Scala 代码中运行的，不是纯 SQL 的使用方式。未来需要支持 SQL 客户端执行提交 SQL 纯文本运行任务。</p></li><li><p>并行度设置</p><p>目前 Table API &amp; SQL 是无法设置并行度的，这使得 Table API 看起来仍像个玩具。</p></li></ol><p>在我看来，Flink 的 Table &amp; SQL API 是走在时代前沿的，在很多方面在做着定义业界标准的事情，比如 SQL 上Window的表达，时间语义的表达，流和批语义的统一等。在我看来，SQL 拥有更天然的流与批统一的特性，并且能够自动帮用户做很多SQL优化（下推、剪枝等），这是 Beam 所做不到的地方。当然，未来如果 Table &amp; SQL API 发展成熟的话，剥离出来作为业界标准的流与批统一的API也不是不可能（叫BeamTable，BeamSQL ？），哈哈。这也是我非常看好 Table &amp; SQL API，认为其大有潜力的一个原因。当然就目前来说，需要走的路还很长，Table API 现在还只是个玩具。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ul><li><a href="http://flink.apache.org/news/2016/05/24/stream-sql.html" target="_blank" rel="noopener">Stream Processing for Everyone with SQL and Apache Flink</a></li><li><a href="http://flink.apache.org/news/2017/03/29/table-sql-api-update.html" target="_blank" rel="noopener">From Streams to Tables and Back Again: An Update on Flink’s Table &amp; SQL API</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Flink 已经拥有了强大的 DataStream/DataSet API，可以基本满足流计算和批计算中的所有需求。为什么还需要 Table &amp;amp; SQL API 呢？&lt;/p&gt;
&lt;p&gt;首先 Table API 是一种关系型API，类 SQL 的API，用户可以像操作表一样地操作数据，非常的直观和方便。用户只需要说需要什么东西，系统就会自动地帮你决定如何最高效地计算它，而不需要像 DataStream 一样写一大堆 Function，优化还得纯靠手工调优。另外，SQL 作为一个“人所皆知”的语言，如果一个引擎提供 SQL，它将很容易被人们接受。这已经是业界很常见的现象了。值得学习的是，Flink 的 Table API 与 SQL API 的实现，有 80% 的代码是共用的。所以当我们讨论 Table API 时，常常是指 Table &amp;amp; SQL API。&lt;/p&gt;
&lt;p&gt;Table &amp;amp; SQL API 还有另一个职责，就是流处理和批处理统一的API层。Flink 在runtime层是统一的，因为Flink将批任务看做流的一种特例来执行，这也是 Flink 向外鼓吹的一点。然而在编程模型上，Flink 却为批和流提供了两套API （DataSet 和 DataStream）。为什么 runtime 统一，而编程模型不统一呢？ 在我看来，这是本末倒置的事情。用户才不管你 runtime 层是否统一，用户更关心的是写一套代码。这也是为什么现在 Apache Beam 能这么火的原因。所以 Table &amp;amp; SQL API 就扛起了统一API的大旗，批上的查询会随着输入数据的结束而结束并生成有限结果集，流上的查询会一直运行并生成结果流。Table &amp;amp; SQL API 做到了批与流上的查询具有同样的语法，因此不用改代码就能同时在批和流上跑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://img3.tbcdn.cn/5476e8b07b923/TB15GhOQpXXXXaYapXXXXXXXXXX&quot; width=&quot;450px&quot;&gt;&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Table" scheme="http://wuchong.me/tags/Table/"/>
    
      <category term="SQL" scheme="http://wuchong.me/tags/SQL/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：Session Window</title>
    <link href="http://wuchong.me/blog/2016/06/06/flink-internals-session-window/"/>
    <id>http://wuchong.me/blog/2016/06/06/flink-internals-session-window/</id>
    <published>2016-06-06T03:48:45.000Z</published>
    <updated>2022-08-03T06:46:44.504Z</updated>
    
    <content type="html"><![CDATA[<p>在<a href="http://wuchong.me/blog/2016/05/25/flink-internals-window-mechanism/">上一篇文章：Window机制</a>中，我们介绍了窗口的概念和底层实现，以及 Flink 一些内建的窗口，包括滑动窗口、翻滚窗口。本文将深入讲解一种较为特殊的窗口：会话窗口（session window）。建议您在阅读完上一篇文章的基础上再阅读本文。</p><p>当我们需要分析用户的一段交互的行为事件时，通常的想法是将用户的事件流按照“session”来分组。session 是指一段持续活跃的期间，由活跃间隙分隔开。通俗一点说，消息之间的间隔小于超时阈值（sessionGap）的，则被分配到同一个窗口，间隔大于阈值的，则被分配到不同的窗口。目前开源领域大部分的流计算引擎都有窗口的概念，但是没有对 session window 的支持，要实现 session window，需要用户自己去做完大部分事情。而当 Flink 1.1.0 版本正式发布时，Flink 将会是开源流计算领域第一个内建支持 session window 的引擎。</p><a id="more"></a><p>在 Flink 1.1.0 之前，Flink 也可以通过自定义的window assigner和trigger来实现一个基本能用的session window。<code>release-1.0</code> 版本中提供了一个实现 session window 的 example：<a href="https://github.com/apache/flink/blob/release-1.0/flink-examples/flink-examples-streaming/src/main/java/org/apache/flink/streaming/examples/windowing/SessionWindowing.java" target="_blank" rel="noopener">SessionWindowing</a>。这个session window范例的实现原理是，基于GlobleWindow这个window assigner，将所有元素都分配到同一个窗口中，然后指定一个自定义的trigger来触发执行窗口。这个trigger的触发机制是，对于每个到达的元素都会根据其时间戳（timestamp）注册一个会话超时的定时器（timestamp+sessionTimeout），并移除上一次注册的定时器。最新一个元素到达后，如果超过 sessionTimeout 的时间还没有新元素到达，那么trigger就会触发，当前窗口就会是一个session window。处理完窗口后，窗口中的数据会清空，用来缓存下一个session window的数据。</p><p>但是这种session window的实现是非常弱的，无法应用到实际生产环境中的。因为它无法处理乱序 event time 的消息。  而在即将到来的 Flink 1.1.0 版本中，Flink 提供了对 session window 的直接支持，用户可以通过<code>SessionWindows.withGap()</code>来轻松地定义 session widnow，而且能够处理乱序消息。Flink 对 session window 的支持主要借鉴自 Google 的 DataFlow 。</p><h2 id="Session-Window-in-Flink"><a href="#Session-Window-in-Flink" class="headerlink" title="Session Window in Flink"></a>Session Window in Flink</h2><p>假设有这么个场景，用户点开手机淘宝后会进行一系列的操作（点击、浏览、搜索、购买、切换tab等），这些操作以及对应发生的时间都会发送到服务器上进行用户行为分析。那么用户的操作行为流的样例可能会长下面这样：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1rvs8KXXXXXXiXVXXXXXXXXXX" alt></p><p>通过上图，我们可以很直观地观察到，用户的行为是一段一段的，每一段内的行为都是连续紧凑的，段内行为的关联度要远大于段之间行为的关联度。我们把每一段用户行为称之为“session”，段之间的空档我们称之为“session gap”。所以，理所当然地，我们应该按照 session window 对用户的行为流进行切分，并计算每个session的结果。如下图所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1jB3_KXXXXXcgXFXXXXXXXXXX" alt></p><p>为了定义上述的窗口切分规则，我们可以使用 Flink 提供的 <code>SessionWindows</code> 这个 widnow assigner API。如果你用过 <code>SlidingEventTimeWindows</code>、<code>TumlingProcessingTimeWindows</code>等，你会对这个很熟悉。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream input = …</span><br><span class="line">DataStream result = input</span><br><span class="line">  .keyBy(&lt;key selector&gt;)</span><br><span class="line">  .window(SessionWindows.withGap(Time.seconds(&lt;seconds&gt;))</span><br><span class="line">  .apply(&lt;window function&gt;) <span class="comment">// or reduce() or fold()</span></span><br></pre></td></tr></table></figure><p>这样，Flink 就会基于元素的时间戳，自动地将元素放到不同的session window中。如果两个元素的时间戳间隔小于 session gap，则会在同一个session中。如果两个元素之间的间隔大于session gap，且没有元素能够填补上这个gap，那么它们会被放到不同的session中。</p><h2 id="底层实现"><a href="#底层实现" class="headerlink" title="底层实现"></a>底层实现</h2><p>为了实现 session window，我们需要扩展 Flink 中的窗口机制，使得能够支持窗口合并。要理解其原因，我们需要先了解窗口的现状。在上一篇文章中，我们谈到了 Flink 中 WindowAssigner 负责将元素分配到哪个/哪些窗口中去，Trigger 决定了一个窗口何时能够被计算或清除。当元素被分配到窗口之后，这些窗口是固定的不会改变的，而且窗口之间不会相互作用。</p><p>对于session window来说，我们需要窗口变得更灵活。基本的思想是这样的：<code>SessionWindows</code> assigner 会为每个进入的元素分配一个窗口，该窗口以元素的时间戳作为起始点，时间戳加会话超时时间为结束点，也就是该窗口为<code>[timestamp, timestamp+sessionGap)</code>。比如我们现在到了两个元素，它们被分配到两个独立的窗口中，两个窗口目前不相交，如图：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1174pKpXXXXbVXXXXXXXXXXXX" alt></p><p>当第三个元素进入时，分配到的窗口与现有的两个窗口发生了叠加，情况变成了这样：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB19.g5KXXXXXX.XVXXXXXXXXXX" alt></p><p>由于我们支持了窗口的合并，<code>WindowAssigner</code>可以合并这些窗口。它会遍历现有的窗口，并告诉系统哪些窗口需要合并成新的窗口。Flink 会将这些窗口进行合并，合并的主要内容有两部分：</p><ol><li>需要合并的窗口的底层状态的合并（也就是窗口中缓存的数据，或者对于聚合窗口来说是一个聚合值）</li><li>需要合并的窗口的Trigger的合并（比如对于EventTime来说，会删除旧窗口注册的定时器，并注册新窗口的定时器）</li></ol><p>总之，结果是三个元素现在在同一个窗口中了：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1iFw0KXXXXXcoXVXXXXXXXXXX" alt></p><p>需要注意的是，对于每一个新进入的元素，都会分配一个属于该元素的窗口，都会检查并合并现有的窗口。在触发窗口计算之前，每一次都会检查该窗口是否可以和其他窗口合并，直到trigger触发后，会将该窗口从窗口列表中移除。对于 event time 来说，窗口的触发是要等到大于窗口结束时间的 watermark 到达，当watermark没有到，窗口会一直缓存着。所以基于这种机制，可以做到对乱序消息的支持。</p><p>这里有一个优化点可以做，因为每一个新进入的元素都会创建属于该元素的窗口，然后合并。如果新元素连续不断地进来，并且新元素的窗口一直都是可以和之前的窗口重叠合并的，那么其实这里多了很多不必要的创建窗口、合并窗口的操作，我们可以直接将新元素放到那个已存在的窗口，然后扩展该窗口的大小，看起来就像和新元素的窗口合并了一样。</p><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><p><a href="https://issues.apache.org/jira/browse/FLINK-3174" target="_blank" rel="noopener">FLINK-3174</a> 这个JIRA中有对 Flink 如何支持 session window 的详细说明，以及代码更新。建议可以结合该 <a href="https://github.com/apache/flink/pull/1460" target="_blank" rel="noopener">PR</a> 的代码来理解本文讨论的实现原理。</p><p>为了扩展 Flink 中的窗口机制，使得能够支持窗口合并，首先 window assigner 要能合并现有的窗口，Flink 增加了一个新的抽象类 <code>MergingWindowAssigner</code> 继承自 <code>WindowAssigner</code>，这里面主要多了一个 <code>mergeWindows</code> 的方法，用来决定哪些窗口是可以合并的。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">MergingWindowAssigner</span>&lt;<span class="title">T</span>, <span class="title">W</span> <span class="keyword">extends</span> <span class="title">Window</span>&gt; <span class="keyword">extends</span> <span class="title">WindowAssigner</span>&lt;<span class="title">T</span>, <span class="title">W</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">long</span> serialVersionUID = <span class="number">1L</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 决定哪些窗口需要被合并。对于每组需要合并的窗口, 都会调用 callback.merge(toBeMerged, mergeResult)</span></span><br><span class="line"><span class="comment">   *</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> windows 现存的窗口集合 The window candidates.</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> callback 需要被合并的窗口会回调 callback.merge 方法</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title">mergeWindows</span><span class="params">(Collection&lt;W&gt; windows, MergeCallback&lt;W&gt; callback)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">MergeCallback</span>&lt;<span class="title">W</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用来声明合并窗口的具体动作（合并窗口底层状态、合并窗口trigger等）。</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> toBeMerged  需要被合并的窗口列表</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> mergeResult 合并后的窗口</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">merge</span><span class="params">(Collection&lt;W&gt; toBeMerged, W mergeResult)</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所有已经存在的 assigner 都继承自 <code>WindowAssigner</code>，只有新加入的 session window assigner 继承自 <code>MergingWindowAssigner</code>，如：<code>ProcessingTimeSessionWindows</code>和<code>EventTimeSessionWindows</code>。</p><p>另外，Trigger 也需要能支持对合并窗口后的响应，所以 Trigger 添加了一个新的接口 <code>onMerge(W window, OnMergeContext ctx)</code>，用来响应发生窗口合并之后对trigger的相关动作，比如根据合并后的窗口注册新的 event time 定时器。</p><p>OK，接下来我们看下最核心的代码，也就是对于每个进入的元素的处理，代码位于<code>WindowOperator.processElement</code>方法中，如下所示：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">processElement</span><span class="params">(StreamRecord&lt;IN&gt; element)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">  Collection&lt;W&gt; elementWindows = windowAssigner.assignWindows(element.getValue(), element.getTimestamp());</span><br><span class="line">  <span class="keyword">final</span> K key = (K) getStateBackend().getCurrentKey();</span><br><span class="line">  <span class="keyword">if</span> (windowAssigner <span class="keyword">instanceof</span> MergingWindowAssigner) &#123;</span><br><span class="line">    <span class="comment">// 对于session window 的特殊处理，我们只关注该条件块内的代码</span></span><br><span class="line">    MergingWindowSet&lt;W&gt; mergingWindows = getMergingWindowSet();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (W window: elementWindows) &#123;</span><br><span class="line">      <span class="keyword">final</span> Tuple1&lt;TriggerResult&gt; mergeTriggerResult = <span class="keyword">new</span> Tuple1&lt;&gt;(TriggerResult.CONTINUE);</span><br><span class="line">      </span><br><span class="line">      <span class="comment">// 加入新窗口, 如果没有合并发生,那么actualWindow就是新加入的窗口</span></span><br><span class="line">      <span class="comment">// 如果有合并发生, 那么返回的actualWindow即为合并后的窗口,</span></span><br><span class="line">      <span class="comment">// 并且会调用 MergeFunction.merge 方法, 这里方法中的内容主要是更新trigger, 合并旧窗口中的状态到新窗口中</span></span><br><span class="line">      W actualWindow = mergingWindows.addWindow(window, <span class="keyword">new</span> MergingWindowSet.MergeFunction&lt;W&gt;() &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">merge</span><span class="params">(W mergeResult,</span></span></span><br><span class="line"><span class="function"><span class="params">            Collection&lt;W&gt; mergedWindows, W stateWindowResult,</span></span></span><br><span class="line"><span class="function"><span class="params">            Collection&lt;W&gt; mergedStateWindows)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">          context.key = key;</span><br><span class="line">          context.window = mergeResult;</span><br><span class="line"></span><br><span class="line">          <span class="comment">// 这里面会根据新窗口的结束时间注册新的定时器</span></span><br><span class="line">          mergeTriggerResult.f0 = context.onMerge(mergedWindows);</span><br><span class="line"></span><br><span class="line">          <span class="comment">// 删除旧窗口注册的定时器</span></span><br><span class="line">          <span class="keyword">for</span> (W m: mergedWindows) &#123;</span><br><span class="line">            context.window = m;</span><br><span class="line">            context.clear();</span><br><span class="line">          &#125;</span><br><span class="line"></span><br><span class="line">          <span class="comment">// 合并旧窗口(mergedStateWindows)中的状态到新窗口（stateWindowResult）中</span></span><br><span class="line">          getStateBackend().mergePartitionedStates(stateWindowResult,</span><br><span class="line">              mergedStateWindows,</span><br><span class="line">              windowSerializer,</span><br><span class="line">              (StateDescriptor&lt;? extends MergingState&lt;?,?&gt;, ?&gt;) windowStateDescriptor);</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 取 actualWindow 对应的用来存状态的窗口</span></span><br><span class="line">      W stateWindow = mergingWindows.getStateWindow(actualWindow);</span><br><span class="line">      <span class="comment">// 从状态后端拿出对应的状态 </span></span><br><span class="line">      AppendingState&lt;IN, ACC&gt; windowState = getPartitionedState(stateWindow, windowSerializer, windowStateDescriptor);</span><br><span class="line">      <span class="comment">// 将新进入的元素数据加入到新窗口（或者说合并后的窗口）中对应的状态中</span></span><br><span class="line">      windowState.add(element.getValue());</span><br><span class="line"></span><br><span class="line">      context.key = key;</span><br><span class="line">      context.window = actualWindow;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 检查是否需要fire or purge </span></span><br><span class="line">      TriggerResult triggerResult = context.onElement(element);</span><br><span class="line"></span><br><span class="line">      TriggerResult combinedTriggerResult = TriggerResult.merge(triggerResult, mergeTriggerResult.f0);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 根据trigger结果决定怎么处理窗口中的数据</span></span><br><span class="line">      processTriggerResult(combinedTriggerResult, actualWindow);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 对于普通window assigner的处理， 这里我们不关注</span></span><br><span class="line">    <span class="keyword">for</span> (W window: elementWindows) &#123;</span><br><span class="line"></span><br><span class="line">      AppendingState&lt;IN, ACC&gt; windowState = getPartitionedState(window, windowSerializer,</span><br><span class="line">          windowStateDescriptor);</span><br><span class="line"></span><br><span class="line">      windowState.add(element.getValue());</span><br><span class="line"></span><br><span class="line">      context.key = key;</span><br><span class="line">      context.window = window;</span><br><span class="line">      TriggerResult triggerResult = context.onElement(element);</span><br><span class="line"></span><br><span class="line">      processTriggerResult(triggerResult, window);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其实这段代码写的并不是很clean，并且不是很好理解。在第六行中有用到<code>MergingWindowSet</code>，这个类很重要所以我们先介绍它。这是一个用来跟踪窗口合并的类。比如我们有A、B、C三个窗口需要合并，合并后的窗口为D窗口。这三个窗口在底层都有对应的状态集合，为了避免代价高昂的状态替换（创建新状态是很昂贵的），我们保持其中一个窗口作为原始的状态窗口，其他几个窗口的数据合并到该状态窗口中去，比如随机选择A作为状态窗口，那么B和C窗口中的数据需要合并到A窗口中去。这样就没有新状态产生了，但是我们需要额外维护窗口与状态窗口之间的映射关系（D-&gt;A），这就是<code>MergingWindowSet</code>负责的工作。这个映射关系需要在失败重启后能够恢复，所以<code>MergingWindowSet</code>内部也是对该映射关系做了容错。状态合并的工作示意图如下所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB15U4lKpXXXXc9XXXXXXXXXXXX" alt></p><p>然后我们来解释下processElement的代码，首先根据window assigner为新进入的元素分配窗口集合。接着进入第一个条件块，取出当前的<code>MergingWindowSet</code>。对于每个分配到的窗口，我们就会将其加入到<code>MergingWindowSet</code>中（<code>addWindow</code>方法），由<code>MergingWindowSet</code>维护窗口与状态窗口之间的关系，并在需要窗口合并的时候，合并状态和trigger。然后根据映射关系，取出结果窗口对应的状态窗口，根据状态窗口取出对应的状态。将新进入的元素数据加入到该状态中。最后，根据trigger结果来对窗口数据进行处理，对于session window来说，这里都是不进行任何处理的。真正对窗口处理是由定时器超时后对完成的窗口调用<code>processTriggerResult</code>。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文在<a href="http://wuchong.me/blog/2016/05/25/flink-internals-window-mechanism/">上一篇文章：Window机制</a>的基础上，深入讲解了 Flink 是如何支持 session window 的，核心的原理是窗口的合并。Flink 对于 session window 的支持很大程度上受到了 Google DataFlow 的启发，所以也建议阅读下 DataFlow 的论文。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="http://blog.madhukaraphatak.com/introduction-to-flink-streaming-part-7" target="_blank" rel="noopener">Introduction to Flink Streaming - Part 7 : Implementing Session Windows using Custom Trigger</a></li><li><a href="http://data-artisans.com/session-windowing-in-flink/" target="_blank" rel="noopener">How Apache Flink Enables New Streaming Applications<br>Part III: Session Windowing in Flink</a></li><li><a href="http://www.vldb.org/pvldb/vol8/p1792-Akidau.pdf" target="_blank" rel="noopener">Google DataFlow paper</a></li><li><a href="https://cloud.google.com/dataflow/model/windowing#session-windows" target="_blank" rel="noopener">Google DataFlow Document</a></li><li><a href="https://issues.apache.org/jira/browse/FLINK-3174" target="_blank" rel="noopener">FLINK-3174: Add merging WindowAssigner</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在&lt;a href=&quot;http://wuchong.me/blog/2016/05/25/flink-internals-window-mechanism/&quot;&gt;上一篇文章：Window机制&lt;/a&gt;中，我们介绍了窗口的概念和底层实现，以及 Flink 一些内建的窗口，包括滑动窗口、翻滚窗口。本文将深入讲解一种较为特殊的窗口：会话窗口（session window）。建议您在阅读完上一篇文章的基础上再阅读本文。&lt;/p&gt;
&lt;p&gt;当我们需要分析用户的一段交互的行为事件时，通常的想法是将用户的事件流按照“session”来分组。session 是指一段持续活跃的期间，由活跃间隙分隔开。通俗一点说，消息之间的间隔小于超时阈值（sessionGap）的，则被分配到同一个窗口，间隔大于阈值的，则被分配到不同的窗口。目前开源领域大部分的流计算引擎都有窗口的概念，但是没有对 session window 的支持，要实现 session window，需要用户自己去做完大部分事情。而当 Flink 1.1.0 版本正式发布时，Flink 将会是开源流计算领域第一个内建支持 session window 的引擎。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：Window 机制</title>
    <link href="http://wuchong.me/blog/2016/05/25/flink-internals-window-mechanism/"/>
    <id>http://wuchong.me/blog/2016/05/25/flink-internals-window-mechanism/</id>
    <published>2016-05-25T05:56:19.000Z</published>
    <updated>2022-08-03T06:46:44.505Z</updated>
    
    <content type="html"><![CDATA[<p>Flink 认为 Batch 是 Streaming 的一个特例，所以 Flink 底层引擎是一个流式引擎，在上面实现了流处理和批处理。而窗口（window）就是从 Streaming 到 Batch 的一个桥梁。Flink 提供了非常完善的窗口机制，这是我认为的 Flink 最大的亮点之一（其他的亮点包括消息乱序处理，和 checkpoint 机制）。本文我们将介绍流式处理中的窗口概念，介绍 Flink 内建的一些窗口和 Window API，最后讨论下窗口在底层是如何实现的。</p><h2 id="什么是-Window"><a href="#什么是-Window" class="headerlink" title="什么是 Window"></a>什么是 Window</h2><p>在流处理应用中，数据是连续不断的，因此我们不可能等到所有数据都到了才开始处理。当然我们可以每来一个消息就处理一次，但是有时我们需要做一些聚合类的处理，例如：在过去的1分钟内有多少用户点击了我们的网页。在这种情况下，我们必须定义一个窗口，用来收集最近一分钟内的数据，并对这个窗口内的数据进行计算。</p><p>窗口可以是时间驱动的（Time Window，例如：每30秒钟），也可以是数据驱动的（Count Window，例如：每一百个元素）。一种经典的窗口分类可以分成：翻滚窗口（Tumbling Window，无重叠），滚动窗口（Sliding Window，有重叠），和会话窗口（Session Window，活动间隙）。</p><a id="more"></a><p>我们举个具体的场景来形象地理解不同窗口的概念。假设，淘宝网会记录每个用户每次购买的商品个数，我们要做的是统计不同窗口中用户购买商品的总数。下图给出了几种经典的窗口切分概述图：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1bwsTJVXXXXaBaXXXXXXXXXXX" alt></p><p>上图中，raw data stream 代表用户的购买行为流，圈中的数字代表该用户本次购买的商品个数，事件是按时间分布的，所以可以看出事件之间是有time gap的。Flink 提供了上图中所有的窗口类型，下面我们会逐一进行介绍。</p><h3 id="Time-Window"><a href="#Time-Window" class="headerlink" title="Time Window"></a>Time Window</h3><p>就如名字所说的，Time Window 是根据时间对数据流进行分组的。这里我们涉及到了流处理中的时间问题，时间问题和消息乱序问题是紧密关联的，这是流处理中现存的难题之一，我们将在后续的 <a href="#">EventTime 和消息乱序处理</a> 中对这部分问题进行深入探讨。这里我们只需要知道 Flink 提出了三种时间的概念，分别是event time（事件时间：事件发生时的时间），ingestion time（摄取时间：事件进入流处理系统的时间），processing time（处理时间：消息被计算处理的时间）。Flink 中窗口机制和时间类型是完全解耦的，也就是说当需要改变时间类型时不需要更改窗口逻辑相关的代码。</p><ul><li><p><strong>Tumbling Time Window</strong><br>如上图，我们需要统计每一分钟中用户购买的商品的总数，需要将用户的行为事件按每一分钟进行切分，这种切分被成为翻滚时间窗口（Tumbling Time Window）。翻滚窗口能将数据流切分成不重叠的窗口，每一个事件只能属于一个窗口。通过使用 DataStream API，我们可以这样实现：</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Stream of (userId, buyCnt)</span></span><br><span class="line"><span class="keyword">val</span> buyCnts: <span class="type">DataStream</span>[(<span class="type">Int</span>, <span class="type">Int</span>)] = ...</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> tumblingCnts: <span class="type">DataStream</span>[(<span class="type">Int</span>, <span class="type">Int</span>)] = buyCnts</span><br><span class="line">  <span class="comment">// key stream by userId</span></span><br><span class="line">  .keyBy(<span class="number">0</span>) </span><br><span class="line">  <span class="comment">// tumbling time window of 1 minute length</span></span><br><span class="line">  .timeWindow(<span class="type">Time</span>.minutes(<span class="number">1</span>))</span><br><span class="line">  <span class="comment">// compute sum over buyCnt</span></span><br><span class="line">  .sum(<span class="number">1</span>)</span><br></pre></td></tr></table></figure></li><li><p><strong>Sliding Time Window</strong><br>但是对于某些应用，它们需要的窗口是不间断的，需要平滑地进行窗口聚合。比如，我们可以每30秒计算一次最近一分钟用户购买的商品总数。这种窗口我们称为滑动时间窗口（Sliding Time Window）。在滑窗中，一个元素可以对应多个窗口。通过使用 DataStream API，我们可以这样实现：</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> slidingCnts: <span class="type">DataStream</span>[(<span class="type">Int</span>, <span class="type">Int</span>)] = buyCnts</span><br><span class="line">  .keyBy(<span class="number">0</span>) </span><br><span class="line">  <span class="comment">// sliding time window of 1 minute length and 30 secs trigger interval</span></span><br><span class="line">  .timeWindow(<span class="type">Time</span>.minutes(<span class="number">1</span>), <span class="type">Time</span>.seconds(<span class="number">30</span>))</span><br><span class="line">  .sum(<span class="number">1</span>)</span><br></pre></td></tr></table></figure></li></ul><h3 id="Count-Window"><a href="#Count-Window" class="headerlink" title="Count Window"></a>Count Window</h3><p>Count Window 是根据元素个数对数据流进行分组的。</p><ul><li><p><strong>Tumbling Count Window</strong><br>当我们想要每100个用户购买行为事件统计购买总数，那么每当窗口中填满100个元素了，就会对窗口进行计算，这种窗口我们称之为翻滚计数窗口（Tumbling Count Window），上图所示窗口大小为3个。通过使用 DataStream API，我们可以这样实现：</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Stream of (userId, buyCnts)</span></span><br><span class="line"><span class="keyword">val</span> buyCnts: <span class="type">DataStream</span>[(<span class="type">Int</span>, <span class="type">Int</span>)] = ...</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> tumblingCnts: <span class="type">DataStream</span>[(<span class="type">Int</span>, <span class="type">Int</span>)] = buyCnts</span><br><span class="line">  <span class="comment">// key stream by sensorId</span></span><br><span class="line">  .keyBy(<span class="number">0</span>)</span><br><span class="line">  <span class="comment">// tumbling count window of 100 elements size</span></span><br><span class="line">  .countWindow(<span class="number">100</span>)</span><br><span class="line">  <span class="comment">// compute the buyCnt sum </span></span><br><span class="line">  .sum(<span class="number">1</span>)</span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>Sliding Count Window</strong><br>当然Count Window 也支持 Sliding Window，虽在上图中未描述出来，但和Sliding Time Window含义是类似的，例如计算每10个元素计算一次最近100个元素的总和，代码示例如下。</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> slidingCnts: <span class="type">DataStream</span>[(<span class="type">Int</span>, <span class="type">Int</span>)] = vehicleCnts</span><br><span class="line">  .keyBy(<span class="number">0</span>)</span><br><span class="line">  <span class="comment">// sliding count window of 100 elements size and 10 elements trigger interval</span></span><br><span class="line">  .countWindow(<span class="number">100</span>, <span class="number">10</span>)</span><br><span class="line">  .sum(<span class="number">1</span>)</span><br></pre></td></tr></table></figure></li></ul><h3 id="Session-Window"><a href="#Session-Window" class="headerlink" title="Session Window"></a>Session Window</h3><p>在这种用户交互事件流中，我们首先想到的是将事件聚合到会话窗口中（一段用户持续活跃的周期），由非活跃的间隙分隔开。如上图所示，就是需要计算每个用户在活跃期间总共购买的商品数量，如果用户30秒没有活动则视为会话断开（假设raw data stream是单个用户的购买行为流）。Session Window 的示例代码如下：</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Stream of (userId, buyCnts)</span></span><br><span class="line"><span class="keyword">val</span> buyCnts: <span class="type">DataStream</span>[(<span class="type">Int</span>, <span class="type">Int</span>)] = ...</span><br><span class="line">  </span><br><span class="line"><span class="keyword">val</span> sessionCnts: <span class="type">DataStream</span>[(<span class="type">Int</span>, <span class="type">Int</span>)] = vehicleCnts</span><br><span class="line">  .keyBy(<span class="number">0</span>)</span><br><span class="line">  <span class="comment">// session window based on a 30 seconds session gap interval </span></span><br><span class="line">  .window(<span class="type">ProcessingTimeSessionWindows</span>.withGap(<span class="type">Time</span>.seconds(<span class="number">30</span>)))</span><br><span class="line">  .sum(<span class="number">1</span>)</span><br></pre></td></tr></table></figure><p>一般而言，window 是在无限的流上定义了一个有限的元素集合。这个集合可以是基于时间的，元素个数的，时间和个数结合的，会话间隙的，或者是自定义的。Flink 的 DataStream API 提供了简洁的算子来满足常用的窗口操作，同时提供了通用的窗口机制来允许用户自己定义窗口分配逻辑。下面我们会对 Flink 窗口相关的 API 进行剖析。</p><h2 id="剖析-Window-API"><a href="#剖析-Window-API" class="headerlink" title="剖析 Window API"></a>剖析 Window API</h2><p>得益于 Flink Window API 松耦合设计，我们可以非常灵活地定义符合特定业务的窗口。Flink 中定义一个窗口主要需要以下三个组件。</p><ul><li><p><strong>Window Assigner：</strong>用来决定某个元素被分配到哪个/哪些窗口中去。</p><p>如下类图展示了目前内置实现的 Window Assigners：<br><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1plkxJVXXXXXqXpXXXXXXXXXX" alt></p></li><li><p><strong>Trigger：</strong>触发器。决定了一个窗口何时能够被计算或清除，每个窗口都会拥有一个自己的Trigger。</p><p>如下类图展示了目前内置实现的 Triggers：<br><img src="http://img3.tbcdn.cn/5476e8b07b923/TB15yMeJVXXXXbbXFXXXXXXXXXX" alt></p></li><li><p><strong>Evictor：</strong>可以译为“驱逐者”。在Trigger触发之后，在窗口被处理之前，Evictor（如果有Evictor的话）会用来剔除窗口中不需要的元素，相当于一个filter。</p><p>如下类图展示了目前内置实现的 Evictors：<br><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1OCT6JVXXXXcjXVXXXXXXXXXX" alt></p></li></ul><p>上述三个组件的不同实现的不同组合，可以定义出非常复杂的窗口。Flink 中内置的窗口也都是基于这三个组件构成的，当然内置窗口有时候无法解决用户特殊的需求，所以 Flink 也暴露了这些窗口机制的内部接口供用户实现自定义的窗口。下面我们将基于这三者探讨窗口的实现机制。</p><h2 id="Window-的实现"><a href="#Window-的实现" class="headerlink" title="Window 的实现"></a>Window 的实现</h2><p>下图描述了 Flink 的窗口机制以及各组件之间是如何相互工作的。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1swNgKXXXXXc4XpXXXXXXXXXX" alt></p><p>首先上图中的组件都位于一个算子（window operator）中，数据流源源不断地进入算子，每一个到达的元素都会被交给 WindowAssigner。WindowAssigner 会决定元素被放到哪个或哪些窗口（window），可能会创建新窗口。因为一个元素可以被放入多个窗口中，所以同时存在多个窗口是可能的。注意，<code>Window</code>本身只是一个ID标识符，其内部可能存储了一些元数据，如<code>TimeWindow</code>中有开始和结束时间，但是并不会存储窗口中的元素。窗口中的元素实际存储在 Key/Value State 中，key为<code>Window</code>，value为元素集合（或聚合值）。为了保证窗口的容错性，该实现依赖了 Flink 的 State 机制（参见 <a href="https://ci.apache.org/projects/flink/flink-docs-master/apis/streaming/state.html" target="_blank" rel="noopener">state 文档</a>）。</p><p>每一个窗口都拥有一个属于自己的 Trigger，Trigger上会有定时器，用来决定一个窗口何时能够被计算或清除。每当有元素加入到该窗口，或者之前注册的定时器超时了，那么Trigger都会被调用。Trigger的返回结果可以是 continue（不做任何操作），fire（处理窗口数据），purge（移除窗口和窗口中的数据），或者 fire + purge。一个Trigger的调用结果只是fire的话，那么会计算窗口并保留窗口原样，也就是说窗口中的数据仍然保留不变，等待下次Trigger fire的时候再次执行计算。一个窗口可以被重复计算多次知道它被 purge 了。在purge之前，窗口会一直占用着内存。</p><p>当Trigger fire了，窗口中的元素集合就会交给<code>Evictor</code>（如果指定了的话）。Evictor 主要用来遍历窗口中的元素列表，并决定最先进入窗口的多少个元素需要被移除。剩余的元素会交给用户指定的函数进行窗口的计算。如果没有 Evictor 的话，窗口中的所有元素会一起交给函数进行计算。</p><p>计算函数收到了窗口的元素（可能经过了 Evictor 的过滤），并计算出窗口的结果值，并发送给下游。窗口的结果值可以是一个也可以是多个。DataStream API 上可以接收不同类型的计算函数，包括预定义的<code>sum()</code>,<code>min()</code>,<code>max()</code>，还有 <code>ReduceFunction</code>，<code>FoldFunction</code>，还有<code>WindowFunction</code>。WindowFunction 是最通用的计算函数，其他的预定义的函数基本都是基于该函数实现的。</p><p>Flink 对于一些聚合类的窗口计算（如sum,min）做了优化，因为聚合类的计算不需要将窗口中的所有数据都保存下来，只需要保存一个result值就可以了。每个进入窗口的元素都会执行一次聚合函数并修改result值。这样可以大大降低内存的消耗并提升性能。但是如果用户定义了 Evictor，则不会启用对聚合窗口的优化，因为 Evictor 需要遍历窗口中的所有元素，必须要将窗口中所有元素都存下来。</p><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><p>上述的三个组件构成了 Flink 的窗口机制。为了更清楚地描述窗口机制，以及解开一些疑惑（比如 purge 和 Evictor 的区别和用途），我们将一步步地解释 Flink 内置的一些窗口（Time Window，Count Window，Session Window）是如何实现的。</p><h3 id="Count-Window-实现"><a href="#Count-Window-实现" class="headerlink" title="Count Window 实现"></a>Count Window 实现</h3><p>Count Window 是使用三组件的典范，我们可以在 <code>KeyedStream</code> 上创建 Count Window，其源码如下所示：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// tumbling count window</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> WindowedStream&lt;T, KEY, GlobalWindow&gt; <span class="title">countWindow</span><span class="params">(<span class="keyword">long</span> size)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> window(GlobalWindows.create())  <span class="comment">// create window stream using GlobalWindows</span></span><br><span class="line">      .trigger(PurgingTrigger.of(CountTrigger.of(size))); <span class="comment">// trigger is window size</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// sliding count window</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> WindowedStream&lt;T, KEY, GlobalWindow&gt; <span class="title">countWindow</span><span class="params">(<span class="keyword">long</span> size, <span class="keyword">long</span> slide)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> window(GlobalWindows.create())</span><br><span class="line">    .evictor(CountEvictor.of(size))  <span class="comment">// evictor is window size</span></span><br><span class="line">    .trigger(CountTrigger.of(slide)); <span class="comment">// trigger is slide size</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>第一个函数是申请翻滚计数窗口，参数为窗口大小。第二个函数是申请滑动计数窗口，参数分别为窗口大小和滑动大小。它们都是基于 <code>GlobalWindows</code> 这个 WindowAssigner 来创建的窗口，该assigner会将所有元素都分配到同一个global window中，所有<code>GlobalWindows</code>的返回值一直是 <code>GlobalWindow</code> 单例。基本上自定义的窗口都会基于该assigner实现。</p><p>翻滚计数窗口并不带evictor，只注册了一个trigger。该trigger是带purge功能的 CountTrigger。也就是说每当窗口中的元素数量达到了 window-size，trigger就会返回fire+purge，窗口就会执行计算并清空窗口中的所有元素，再接着储备新的元素。从而实现了tumbling的窗口之间无重叠。</p><p>滑动计数窗口的各窗口之间是有重叠的，但我们用的 GlobalWindows assinger 从始至终只有一个窗口，不像 sliding time assigner 可以同时存在多个窗口。所以trigger结果不能带purge，也就是说计算完窗口后窗口中的数据要保留下来（供下个滑窗使用）。另外，trigger的间隔是slide-size，evictor的保留的元素个数是window-size。也就是说，每个滑动间隔就触发一次窗口计算，并保留下最新进入窗口的window-size个元素，剔除旧元素。</p><p>假设有一个滑动计数窗口，每2个元素计算一次最近4个元素的总和，那么窗口工作示意图如下所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB15vcUJVXXXXcGXVXXXXXXXXXX" alt></p><p>图中所示的各个窗口逻辑上是不同的窗口，但在物理上是同一个窗口。该滑动计数窗口，trigger的触发条件是元素个数达到2个（每进入2个元素就会触发一次），evictor保留的元素个数是4个，每次计算完窗口总和后会保留剩余的元素。所以第一次触发trigger是当元素5进入，第三次触发trigger是当元素2进入，并驱逐5和2，计算剩余的4个元素的总和（22）并发送出去，保留下2,4,9,7元素供下个逻辑窗口使用。</p><h3 id="Time-Window-实现"><a href="#Time-Window-实现" class="headerlink" title="Time Window 实现"></a>Time Window 实现</h3><p>同样的，我们也可以在 <code>KeyedStream</code> 上申请 Time Window，其源码如下所示：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// tumbling time window</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> WindowedStream&lt;T, KEY, TimeWindow&gt; <span class="title">timeWindow</span><span class="params">(Time size)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) &#123;</span><br><span class="line">    <span class="keyword">return</span> window(TumblingProcessingTimeWindows.of(size));</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> window(TumblingEventTimeWindows.of(size));</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// sliding time window</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> WindowedStream&lt;T, KEY, TimeWindow&gt; <span class="title">timeWindow</span><span class="params">(Time size, Time slide)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) &#123;</span><br><span class="line">    <span class="keyword">return</span> window(SlidingProcessingTimeWindows.of(size, slide));</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> window(SlidingEventTimeWindows.of(size, slide));</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在方法体内部会根据当前环境注册的时间类型，使用不同的WindowAssigner创建window。可以看到，EventTime和IngestTime都使用了<code>XXXEventTimeWindows</code>这个assigner，因为EventTime和IngestTime在底层的实现上只是在Source处为Record打时间戳的实现不同，在window operator中的处理逻辑是一样的。</p><p>这里我们主要分析sliding process time window，如下是相关源码：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SlidingProcessingTimeWindows</span> <span class="keyword">extends</span> <span class="title">WindowAssigner</span>&lt;<span class="title">Object</span>, <span class="title">TimeWindow</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">long</span> serialVersionUID = <span class="number">1L</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">long</span> size;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">long</span> slide;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">private</span> <span class="title">SlidingProcessingTimeWindows</span><span class="params">(<span class="keyword">long</span> size, <span class="keyword">long</span> slide)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.size = size;</span><br><span class="line">    <span class="keyword">this</span>.slide = slide;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Collection&lt;TimeWindow&gt; <span class="title">assignWindows</span><span class="params">(Object element, <span class="keyword">long</span> timestamp)</span> </span>&#123;</span><br><span class="line">    timestamp = System.currentTimeMillis();</span><br><span class="line">    List&lt;TimeWindow&gt; windows = <span class="keyword">new</span> ArrayList&lt;&gt;((<span class="keyword">int</span>) (size / slide));</span><br><span class="line">    <span class="comment">// 对齐时间戳</span></span><br><span class="line">    <span class="keyword">long</span> lastStart = timestamp - timestamp % slide;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">long</span> start = lastStart;</span><br><span class="line">      start &gt; timestamp - size;</span><br><span class="line">      start -= slide) &#123;</span><br><span class="line">      <span class="comment">// 当前时间戳对应了多个window</span></span><br><span class="line">      windows.add(<span class="keyword">new</span> TimeWindow(start, start + size));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> windows;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProcessingTimeTrigger</span> <span class="keyword">extends</span> <span class="title">Trigger</span>&lt;<span class="title">Object</span>, <span class="title">TimeWindow</span>&gt; </span>&#123;</span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="comment">// 每个元素进入窗口都会调用该方法</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> TriggerResult <span class="title">onElement</span><span class="params">(Object element, <span class="keyword">long</span> timestamp, TimeWindow window, TriggerContext ctx)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 注册定时器，当系统时间到达window end timestamp时会回调该trigger的onProcessingTime方法</span></span><br><span class="line">    ctx.registerProcessingTimeTimer(window.getEnd());</span><br><span class="line">    <span class="keyword">return</span> TriggerResult.CONTINUE;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="comment">// 返回结果表示执行窗口计算并清空窗口</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> TriggerResult <span class="title">onProcessingTime</span><span class="params">(<span class="keyword">long</span> time, TimeWindow window, TriggerContext ctx)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> TriggerResult.FIRE_AND_PURGE;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先，<code>SlidingProcessingTimeWindows</code>会对每个进入窗口的元素根据系统时间分配到<code>(size / slide)</code>个不同的窗口，并会在每个窗口上根据窗口结束时间注册一个定时器（相同窗口只会注册一份），当定时器超时时意味着该窗口完成了，这时会回调对应窗口的Trigger的<code>onProcessingTime</code>方法，返回FIRE_AND_PURGE，也就是会执行窗口计算并清空窗口。整个过程示意图如下：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1iihcKXXXXXavXFXXXXXXXXXX" alt></p><p>如上图所示横轴代表时间戳（为简化问题，时间戳从0开始），第一条record会被分配到[-5,5)和[0,10)两个窗口中，当系统时间到5时，就会计算[-5,5)窗口中的数据，并将结果发送出去，最后清空窗口中的数据，释放该窗口资源。</p><h3 id="Session-Window-实现"><a href="#Session-Window-实现" class="headerlink" title="Session Window 实现"></a>Session Window 实现</h3><p>Session Window 是一个需求很强烈的窗口机制，但Session也比之前的Window更复杂，所以 Flink 也是在即将到来的 1.1.0 版本中才支持了该功能。由于篇幅问题，我们将在后续的 <a href="#">Session Window 的实现</a> 中深入探讨 Session Window 的实现。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://ci.apache.org/projects/flink/flink-docs-release-1.0/concepts/concepts.html#time-and-windows" target="_blank" rel="noopener">Flink Concepts</a></li><li><a href="https://flink.apache.org/news/2015/12/04/Introducing-windows.html" target="_blank" rel="noopener">Introducing Stream Windows in Apache Flink</a></li><li><a href="https://cwiki.apache.org/confluence/display/FLINK/Streaming+Window+Join+Rework" target="_blank" rel="noopener">Streaming Window Join Rework</a></li><li><a href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=60624830" target="_blank" rel="noopener">Window Semantics (and Implementation)</a></li><li><a href="http://blog.madhukaraphatak.com/introduction-to-flink-streaming-part-6" target="_blank" rel="noopener">Introduction to Flink Streaming - Part 6 : Anatomy of Window API</a></li><li><a href="http://blog.madhukaraphatak.com/introduction-to-flink-streaming-part-5" target="_blank" rel="noopener">Introduction to Flink Streaming - Part 5 : Window API in Flink</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Flink 认为 Batch 是 Streaming 的一个特例，所以 Flink 底层引擎是一个流式引擎，在上面实现了流处理和批处理。而窗口（window）就是从 Streaming 到 Batch 的一个桥梁。Flink 提供了非常完善的窗口机制，这是我认为的 Flink 最大的亮点之一（其他的亮点包括消息乱序处理，和 checkpoint 机制）。本文我们将介绍流式处理中的窗口概念，介绍 Flink 内建的一些窗口和 Window API，最后讨论下窗口在底层是如何实现的。&lt;/p&gt;
&lt;h2 id=&quot;什么是-Window&quot;&gt;&lt;a href=&quot;#什么是-Window&quot; class=&quot;headerlink&quot; title=&quot;什么是 Window&quot;&gt;&lt;/a&gt;什么是 Window&lt;/h2&gt;&lt;p&gt;在流处理应用中，数据是连续不断的，因此我们不可能等到所有数据都到了才开始处理。当然我们可以每来一个消息就处理一次，但是有时我们需要做一些聚合类的处理，例如：在过去的1分钟内有多少用户点击了我们的网页。在这种情况下，我们必须定义一个窗口，用来收集最近一分钟内的数据，并对这个窗口内的数据进行计算。&lt;/p&gt;
&lt;p&gt;窗口可以是时间驱动的（Time Window，例如：每30秒钟），也可以是数据驱动的（Count Window，例如：每一百个元素）。一种经典的窗口分类可以分成：翻滚窗口（Tumbling Window，无重叠），滚动窗口（Sliding Window，有重叠），和会话窗口（Session Window，活动间隙）。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：数据流上的类型和操作</title>
    <link href="http://wuchong.me/blog/2016/05/20/flink-internals-streams-and-operations-on-streams/"/>
    <id>http://wuchong.me/blog/2016/05/20/flink-internals-streams-and-operations-on-streams/</id>
    <published>2016-05-20T15:56:48.000Z</published>
    <updated>2022-08-03T06:46:44.504Z</updated>
    
    <content type="html"><![CDATA[<p>Flink 为流处理和批处理分别提供了 DataStream API 和 DataSet API。正是这种高层的抽象和 flunent API 极大地便利了用户编写大数据应用。不过很多初学者在看到<a href>官方 Streaming 文档</a>中那一大坨的转换时，常常会蒙了圈，文档中那些只言片语也很难讲清它们之间的关系。所以本文将介绍几种关键的数据流类型，它们之间是如何通过转换关联起来的。下图展示了 Flink 中目前支持的主要几种流的类型，以及它们之间的转换关系。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1BWrvJVXXXXXbXVXXXXXXXXXX" alt></p><a id="more"></a><h2 id="DataStream"><a href="#DataStream" class="headerlink" title="DataStream"></a>DataStream</h2><p><code>DataStream</code> 是 Flink 流处理 API 中最核心的数据结构。它代表了一个运行在多个分区上的并行流。一个 <code>DataStream</code> 可以从 <code>StreamExecutionEnvironment</code> 通过<code>env.addSource(SourceFunction)</code> 获得。</p><p>DataStream 上的转换操作都是逐条的，比如 <code>map()</code>，<code>flatMap()</code>，<code>filter()</code>。DataStream 也可以执行 <code>rebalance</code>（再平衡，用来减轻数据倾斜）和 <code>broadcaseted</code>（广播）等分区转换。</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> stream: <span class="type">DataStream</span>[<span class="type">MyType</span>] = env.addSource(<span class="keyword">new</span> <span class="type">FlinkKafkaConsumer08</span>[<span class="type">String</span>](...))</span><br><span class="line"><span class="keyword">val</span> str1: <span class="type">DataStream</span>[(<span class="type">String</span>, <span class="type">MyType</span>)] = stream.flatMap &#123; ... &#125;</span><br><span class="line"><span class="keyword">val</span> str2: <span class="type">DataStream</span>[(<span class="type">String</span>, <span class="type">MyType</span>)] = stream.rebalance()</span><br><span class="line"><span class="keyword">val</span> str3: <span class="type">DataStream</span>[<span class="type">AnotherType</span>] = stream.map &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>上述 DataStream 上的转换在运行时会转换成如下的执行图：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1aqPSJVXXXXbqXXXXXXXXXXXX" width="450px"></p><p>如上图的执行图所示，DataStream 各个算子会并行运行，算子之间是数据流分区。如 Source 的第一个并行实例（S1）和 flatMap() 的第一个并行实例（m1）之间就是一个数据流分区。而在 flatMap() 和 map() 之间由于加了 rebalance()，它们之间的数据流分区就有3个子分区（m1的数据流向3个map()实例）。这与 Apache Kafka 是很类似的，把流想象成 Kafka Topic，而一个流分区就表示一个 Topic Partition，流的目标并行算子实例就是 Kafka Consumers。</p><h2 id="KeyedStream"><a href="#KeyedStream" class="headerlink" title="KeyedStream"></a>KeyedStream</h2><p><code>KeyedStream</code>用来表示根据指定的key进行分组的数据流。一个<code>KeyedStream</code>可以通过调用<code>DataStream.keyBy()</code>来获得。而在<code>KeyedStream</code>上进行任何transformation都将转变回<code>DataStream</code>。在实现中，<code>KeyedStream</code>是把key的信息写入到了transformation中。每条记录只能访问所属key的状态，其上的聚合函数可以方便地操作和保存对应key的状态。</p><h2 id="WindowedStream-amp-AllWindowedStream"><a href="#WindowedStream-amp-AllWindowedStream" class="headerlink" title="WindowedStream &amp; AllWindowedStream"></a>WindowedStream &amp; AllWindowedStream</h2><p><code>WindowedStream</code>代表了根据key分组，并且基于<code>WindowAssigner</code>切分窗口的数据流。所以<code>WindowedStream</code>都是从<code>KeyedStream</code>衍生而来的。而在<code>WindowedStream</code>上进行任何transformation也都将转变回<code>DataStream</code>。</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> stream: <span class="type">DataStream</span>[<span class="type">MyType</span>] = ...</span><br><span class="line"><span class="keyword">val</span> windowed: <span class="type">WindowedDataStream</span>[<span class="type">MyType</span>] = stream</span><br><span class="line">        .keyBy(<span class="string">"userId"</span>)</span><br><span class="line">        .window(<span class="type">TumblingEventTimeWindows</span>.of(<span class="type">Time</span>.seconds(<span class="number">5</span>))) <span class="comment">// Last 5 seconds of data</span></span><br><span class="line"><span class="keyword">val</span> result: <span class="type">DataStream</span>[<span class="type">ResultType</span>] = windowed.reduce(myReducer)</span><br></pre></td></tr></table></figure><p>上述 WindowedStream 的样例代码在运行时会转换成如下的执行图：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1G2HqJVXXXXb4aXXXXXXXXXXX" width="500px"></p><p>Flink 的窗口实现中会将到达的数据缓存在对应的窗口buffer中（一个数据可能会对应多个窗口）。当到达窗口发送的条件时（由Trigger控制），Flink 会对整个窗口中的数据进行处理。Flink 在聚合类窗口有一定的优化，即不会保存窗口中的所有值，而是每到一个元素执行一次聚合函数，最终只保存一份数据即可。</p><p>在key分组的流上进行窗口切分是比较常用的场景，也能够很好地并行化（不同的key上的窗口聚合可以分配到不同的task去处理）。不过有时候我们也需要在普通流上进行窗口的操作，这就是 <code>AllWindowedStream</code>。<code>AllWindowedStream</code>是直接在<code>DataStream</code>上进行<code>windowAll(...)</code>操作。AllWindowedStream 的实现是基于 WindowedStream 的（Flink 1.1.x 开始）。Flink 不推荐使用<code>AllWindowedStream</code>，因为在普通流上进行窗口操作，就势必需要将所有分区的流都汇集到单个的Task中，而这个单个的Task很显然就会成为整个Job的瓶颈。</p><h2 id="JoinedStreams-amp-CoGroupedStreams"><a href="#JoinedStreams-amp-CoGroupedStreams" class="headerlink" title="JoinedStreams &amp; CoGroupedStreams"></a>JoinedStreams &amp; CoGroupedStreams</h2><p>双流 Join 也是一个非常常见的应用场景。深入源码你可以发现，JoinedStreams 和 CoGroupedStreams 的代码实现有80%是一模一样的，JoinedStreams 在底层又调用了 CoGroupedStreams 来实现 Join 功能。除了名字不一样，一开始很难将它们区分开来，而且为什么要提供两个功能类似的接口呢？？</p><p>实际上这两者还是很点区别的。首先 co-group 侧重的是group，是对同一个key上的两组集合进行操作，而 join 侧重的是pair，是对同一个key上的每对元素进行操作。co-group 比 join 更通用一些，因为 join 只是 co-group 的一个特例，所以 join 是可以基于 co-group 来实现的（当然有优化的空间）。而在 co-group 之外又提供了 join 接口是因为用户更熟悉 join（源于数据库吧），而且能够跟 DataSet API 保持一致，降低用户的学习成本。</p><p>JoinedStreams 和 CoGroupedStreams 是基于 Window 上实现的，所以 CoGroupedStreams 最终又调用了 WindowedStream 来实现。</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> firstInput: <span class="type">DataStream</span>[<span class="type">MyType</span>] = ...</span><br><span class="line"><span class="keyword">val</span> secondInput: <span class="type">DataStream</span>[<span class="type">AnotherType</span>] = ...</span><br><span class="line"> </span><br><span class="line"><span class="keyword">val</span> result: <span class="type">DataStream</span>[(<span class="type">MyType</span>, <span class="type">AnotherType</span>)] = firstInput.join(secondInput)</span><br><span class="line">    .where(<span class="string">"userId"</span>).equalTo(<span class="string">"id"</span>)</span><br><span class="line">    .window(<span class="type">TumblingEventTimeWindows</span>.of(<span class="type">Time</span>.seconds(<span class="number">3</span>)))</span><br><span class="line">    .apply (<span class="keyword">new</span> <span class="type">JoinFunction</span> () &#123;...&#125;)</span><br></pre></td></tr></table></figure><p>上述 JoinedStreams 的样例代码在运行时会转换成如下的执行图：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1mW_zJVXXXXcfXVXXXXXXXXXX" width="500px"></p><p>双流上的数据在同一个key的会被分别分配到同一个window窗口的左右两个篮子里，当window结束的时候，会对左右篮子进行笛卡尔积从而得到每一对pair，对每一对pair应用 JoinFunction。不过目前（Flink 1.1.x）JoinedStreams 只是简单地实现了流上的join操作而已，距离真正的生产使用还是有些距离。因为目前 join 窗口的双流数据都是被缓存在内存中的，也就是说如果某个key上的窗口数据太多就会导致 JVM OOM（然而数据倾斜是常态）。双流join的难点也正是在这里，这也是社区后面对 join 操作的优化方向，例如可以借鉴Flink在批处理join中的优化方案，也可以用ManagedMemory来管理窗口中的数据，并当数据超过阈值时能spill到硬盘。</p><h2 id="ConnectedStreams"><a href="#ConnectedStreams" class="headerlink" title="ConnectedStreams"></a>ConnectedStreams</h2><p>在 DataStream 上有一个 union 的转换 <code>dataStream.union(otherStream1, otherStream2, ...)</code>，用来合并多个流，新的流会包含所有流中的数据。union 有一个限制，就是所有合并的流的类型必须是一致的。<code>ConnectedStreams</code> 提供了和 union 类似的功能，用来连接<strong>两个</strong>流，但是与 union 转换有以下几个区别：</p><ol><li>ConnectedStreams 只能连接两个流，而 union 可以连接多于两个流。</li><li>ConnectedStreams 连接的两个流类型可以不一致，而 union 连接的流的类型必须一致。</li><li>ConnectedStreams 会对两个流的数据应用不同的处理方法，并且双流之间可以共享状态。这在第一个流的输入会影响第二个流时, 会非常有用。</li></ol><p>如下 ConnectedStreams 的样例，连接 <code>input</code> 和 <code>other</code> 流，并在<code>input</code>流上应用<code>map1</code>方法，在<code>other</code>上应用<code>map2</code>方法，双流可以共享状态（比如计数）。</p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> input: <span class="type">DataStream</span>[<span class="type">MyType</span>] = ...</span><br><span class="line"><span class="keyword">val</span> other: <span class="type">DataStream</span>[<span class="type">AnotherType</span>] = ...</span><br><span class="line"> </span><br><span class="line"><span class="keyword">val</span> connected: <span class="type">ConnectedStreams</span>[<span class="type">MyType</span>, <span class="type">AnotherType</span>] = input.connect(other)</span><br><span class="line"> </span><br><span class="line"><span class="keyword">val</span> result: <span class="type">DataStream</span>[<span class="type">ResultType</span>] = </span><br><span class="line">        connected.map(<span class="keyword">new</span> <span class="type">CoMapFunction</span>[<span class="type">MyType</span>, <span class="type">AnotherType</span>, <span class="type">ResultType</span>]() &#123;</span><br><span class="line">            <span class="keyword">override</span> <span class="function"><span class="keyword">def</span> <span class="title">map1</span></span>(value: <span class="type">MyType</span>): <span class="type">ResultType</span> = &#123; ... &#125;</span><br><span class="line">            <span class="keyword">override</span> <span class="function"><span class="keyword">def</span> <span class="title">map2</span></span>(value: <span class="type">AnotherType</span>): <span class="type">ResultType</span> = &#123; ... &#125;</span><br><span class="line">        &#125;)</span><br></pre></td></tr></table></figure><p>当并行度为2时，其执行图如下所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1WEjuJVXXXXcHXFXXXXXXXXXX" alt></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文介绍通过不同数据流类型的转换图来解释每一种数据流的含义、转换关系。后面的文章会深入讲解 Window 机制的实现，双流 Join 的实现等。 </p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Flink 为流处理和批处理分别提供了 DataStream API 和 DataSet API。正是这种高层的抽象和 flunent API 极大地便利了用户编写大数据应用。不过很多初学者在看到&lt;a href&gt;官方 Streaming 文档&lt;/a&gt;中那一大坨的转换时，常常会蒙了圈，文档中那些只言片语也很难讲清它们之间的关系。所以本文将介绍几种关键的数据流类型，它们之间是如何通过转换关联起来的。下图展示了 Flink 中目前支持的主要几种流的类型，以及它们之间的转换关系。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://img3.tbcdn.cn/5476e8b07b923/TB1BWrvJVXXXXXbXVXXXXXXXXXX&quot; alt&gt;&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：如何生成 JobGraph</title>
    <link href="http://wuchong.me/blog/2016/05/10/flink-internals-how-to-build-jobgraph/"/>
    <id>http://wuchong.me/blog/2016/05/10/flink-internals-how-to-build-jobgraph/</id>
    <published>2016-05-10T05:25:53.000Z</published>
    <updated>2022-08-03T06:46:44.503Z</updated>
    
    <content type="html"><![CDATA[<p>继前文<a href="http://wuchong.me/blog/2016/05/03/flink-internals-overview/">Flink 原理与实现：架构和拓扑概览</a>中介绍了Flink的四层执行图模型，本文将主要介绍 Flink 是如何将 StreamGraph 转换成 JobGraph 的。根据用户用Stream API编写的程序，构造出一个代表拓扑结构的StreamGraph的。以 WordCount 为例，转换图如下图所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1DzYXJFXXXXXJXVXXXXXXXXXX" alt></p><p>StreamGraph 和 JobGraph 都是在 Client 端生成的，也就是说我们可以在 IDE 中通过断点调试观察 StreamGraph 和 JobGraph 的生成过程。</p><a id="more"></a><p>JobGraph 的相关数据结构主要在 <code>org.apache.flink.runtime.jobgraph</code> 包中。构造 JobGraph 的代码主要集中在 <code>StreamingJobGraphGenerator</code> 类中，入口函数是 <code>StreamingJobGraphGenerator.createJobGraph()</code>。我们首先来看下<code>StreamingJobGraphGenerator</code>的核心源码：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">StreamingJobGraphGenerator</span> </span>&#123;</span><br><span class="line">  <span class="keyword">private</span> StreamGraph streamGraph;</span><br><span class="line">  <span class="keyword">private</span> JobGraph jobGraph;</span><br><span class="line">  <span class="comment">// id -&gt; JobVertex</span></span><br><span class="line">  <span class="keyword">private</span> Map&lt;Integer, JobVertex&gt; jobVertices;</span><br><span class="line">  <span class="comment">// 已经构建的JobVertex的id集合</span></span><br><span class="line">  <span class="keyword">private</span> Collection&lt;Integer&gt; builtVertices;</span><br><span class="line">  <span class="comment">// 物理边集合（排除了chain内部的边）, 按创建顺序排序</span></span><br><span class="line">  <span class="keyword">private</span> List&lt;StreamEdge&gt; physicalEdgesInOrder;</span><br><span class="line">  <span class="comment">// 保存chain信息，部署时用来构建 OperatorChain，startNodeId -&gt; (currentNodeId -&gt; StreamConfig)</span></span><br><span class="line">  <span class="keyword">private</span> Map&lt;Integer, Map&lt;Integer, StreamConfig&gt;&gt; chainedConfigs;</span><br><span class="line">  <span class="comment">// 所有节点的配置信息，id -&gt; StreamConfig</span></span><br><span class="line">  <span class="keyword">private</span> Map&lt;Integer, StreamConfig&gt; vertexConfigs;</span><br><span class="line">  <span class="comment">// 保存每个节点的名字，id -&gt; chainedName</span></span><br><span class="line">  <span class="keyword">private</span> Map&lt;Integer, String&gt; chainedNames;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 构造函数，入参只有 StreamGraph</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">StreamingJobGraphGenerator</span><span class="params">(StreamGraph streamGraph)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.streamGraph = streamGraph;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 根据 StreamGraph，生成 JobGraph</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> JobGraph <span class="title">createJobGraph</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    jobGraph = <span class="keyword">new</span> JobGraph(streamGraph.getJobName());</span><br><span class="line">  </span><br><span class="line">    <span class="comment">// streaming 模式下，调度模式是所有节点（vertices）一起启动</span></span><br><span class="line">    jobGraph.setScheduleMode(ScheduleMode.ALL);</span><br><span class="line">    <span class="comment">// 初始化成员变量</span></span><br><span class="line">    init();</span><br><span class="line">  </span><br><span class="line">    <span class="comment">// 广度优先遍历 StreamGraph 并且为每个SteamNode生成hash id，</span></span><br><span class="line">    <span class="comment">// 保证如果提交的拓扑没有改变，则每次生成的hash都是一样的</span></span><br><span class="line">    Map&lt;Integer, <span class="keyword">byte</span>[]&gt; hashes = traverseStreamGraphAndGenerateHashes();</span><br><span class="line">  </span><br><span class="line">    <span class="comment">// 最重要的函数，生成JobVertex，JobEdge等，并尽可能地将多个节点chain在一起</span></span><br><span class="line">    setChaining(hashes);</span><br><span class="line">  </span><br><span class="line">    <span class="comment">// 将每个JobVertex的入边集合也序列化到该JobVertex的StreamConfig中</span></span><br><span class="line">    <span class="comment">// (出边集合已经在setChaining的时候写入了)</span></span><br><span class="line">    setPhysicalEdges();</span><br><span class="line">  </span><br><span class="line">    <span class="comment">// 根据group name，为每个 JobVertex 指定所属的 SlotSharingGroup </span></span><br><span class="line">    <span class="comment">// 以及针对 Iteration的头尾设置  CoLocationGroup</span></span><br><span class="line">    setSlotSharing();</span><br><span class="line">    <span class="comment">// 配置checkpoint</span></span><br><span class="line">    configureCheckpointing();</span><br><span class="line">    <span class="comment">// 配置重启策略（不重启，还是固定延迟重启）</span></span><br><span class="line">    configureRestartStrategy();</span><br><span class="line">  </span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="comment">// 将 StreamGraph 的 ExecutionConfig 序列化到 JobGraph 的配置中</span></span><br><span class="line">      InstantiationUtil.writeObjectToConfig(<span class="keyword">this</span>.streamGraph.getExecutionConfig(), <span class="keyword">this</span>.jobGraph.getJobConfiguration(), ExecutionConfig.CONFIG_KEY);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"Config object could not be written to Job Configuration: "</span>, e);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> jobGraph;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>StreamingJobGraphGenerator</code>的成员变量都是为了辅助生成最终的JobGraph。<code>createJobGraph()</code>函数的逻辑也很清晰，首先为所有节点生成一个唯一的hash id，如果节点在多次提交中没有改变（包括并发度、上下游等），那么这个id就不会改变，这主要用于故障恢复。这里我们不能用 <code>StreamNode.id</code>来代替，因为这是一个从1开始的静态计数变量，同样的Job可能会得到不一样的id，如下代码示例的两个job是完全一样的，但是source的id却不一样了。然后就是最关键的chaining处理，和生成JobVetex、JobEdge等。之后就是写入各种配置相关的信息。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 范例1：A.id=1  B.id=2</span></span><br><span class="line">DataStream&lt;String&gt; A = ...</span><br><span class="line">DataStream&lt;String&gt; B = ...</span><br><span class="line">A.union(B).print();</span><br><span class="line"><span class="comment">// 范例2：A.id=2  B.id=1</span></span><br><span class="line">DataStream&lt;String&gt; B = ...</span><br><span class="line">DataStream&lt;String&gt; A = ...</span><br><span class="line">A.union(B).print();</span><br></pre></td></tr></table></figure><p>下面具体分析下关键函数 <code>setChaining</code> 的实现：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 从source开始建立 node chains</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">setChaining</span><span class="params">(Map&lt;Integer, <span class="keyword">byte</span>[]&gt; hashes)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">for</span> (Integer sourceNodeId : streamGraph.getSourceIDs()) &#123;</span><br><span class="line">    createChain(sourceNodeId, sourceNodeId, hashes);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 构建node chains，返回当前节点的物理出边</span></span><br><span class="line"><span class="comment">// startNodeId != currentNodeId 时,说明currentNode是chain中的子节点</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> List&lt;StreamEdge&gt; <span class="title">createChain</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">    Integer startNodeId,</span></span></span><br><span class="line"><span class="function"><span class="params">    Integer currentNodeId,</span></span></span><br><span class="line"><span class="function"><span class="params">    Map&lt;Integer, <span class="keyword">byte</span>[]&gt; hashes)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!builtVertices.contains(startNodeId)) &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 过渡用的出边集合, 用来生成最终的 JobEdge, 注意不包括 chain 内部的边</span></span><br><span class="line">    List&lt;StreamEdge&gt; transitiveOutEdges = <span class="keyword">new</span> ArrayList&lt;StreamEdge&gt;();</span><br><span class="line"></span><br><span class="line">    List&lt;StreamEdge&gt; chainableOutputs = <span class="keyword">new</span> ArrayList&lt;StreamEdge&gt;();</span><br><span class="line">    List&lt;StreamEdge&gt; nonChainableOutputs = <span class="keyword">new</span> ArrayList&lt;StreamEdge&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 将当前节点的出边分成 chainable 和 nonChainable 两类</span></span><br><span class="line">    <span class="keyword">for</span> (StreamEdge outEdge : streamGraph.getStreamNode(currentNodeId).getOutEdges()) &#123;</span><br><span class="line">      <span class="keyword">if</span> (isChainable(outEdge)) &#123;</span><br><span class="line">        chainableOutputs.add(outEdge);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        nonChainableOutputs.add(outEdge);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//==&gt; 递归调用</span></span><br><span class="line">    <span class="keyword">for</span> (StreamEdge chainable : chainableOutputs) &#123;</span><br><span class="line">      transitiveOutEdges.addAll(createChain(startNodeId, chainable.getTargetId(), hashes));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (StreamEdge nonChainable : nonChainableOutputs) &#123;</span><br><span class="line">      transitiveOutEdges.add(nonChainable);</span><br><span class="line">      createChain(nonChainable.getTargetId(), nonChainable.getTargetId(), hashes);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 生成当前节点的显示名，如："Keyed Aggregation -&gt; Sink: Unnamed"</span></span><br><span class="line">    chainedNames.put(currentNodeId, createChainedName(currentNodeId, chainableOutputs));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 如果当前节点是起始节点, 则直接创建 JobVertex 并返回 StreamConfig, 否则先创建一个空的 StreamConfig</span></span><br><span class="line">    <span class="comment">// createJobVertex 函数就是根据 StreamNode 创建对应的 JobVertex, 并返回了空的 StreamConfig</span></span><br><span class="line">    StreamConfig config = currentNodeId.equals(startNodeId)</span><br><span class="line">        ? createJobVertex(startNodeId, hashes)</span><br><span class="line">        : <span class="keyword">new</span> StreamConfig(<span class="keyword">new</span> Configuration());</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 设置 JobVertex 的 StreamConfig, 基本上是序列化 StreamNode 中的配置到 StreamConfig 中.</span></span><br><span class="line">    <span class="comment">// 其中包括 序列化器, StreamOperator, Checkpoint 等相关配置</span></span><br><span class="line">    setVertexConfig(currentNodeId, config, chainableOutputs, nonChainableOutputs);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (currentNodeId.equals(startNodeId)) &#123;</span><br><span class="line">      <span class="comment">// 如果是chain的起始节点。（不是chain中的节点，也会被标记成 chain start）</span></span><br><span class="line">      config.setChainStart();</span><br><span class="line">      <span class="comment">// 我们也会把物理出边写入配置, 部署时会用到</span></span><br><span class="line">      config.setOutEdgesInOrder(transitiveOutEdges);</span><br><span class="line">      config.setOutEdges(streamGraph.getStreamNode(currentNodeId).getOutEdges());</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 将当前节点(headOfChain)与所有出边相连</span></span><br><span class="line">      <span class="keyword">for</span> (StreamEdge edge : transitiveOutEdges) &#123;</span><br><span class="line">        <span class="comment">// 通过StreamEdge构建出JobEdge，创建IntermediateDataSet，用来将JobVertex和JobEdge相连</span></span><br><span class="line">        connect(startNodeId, edge);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 将chain中所有子节点的StreamConfig写入到 headOfChain 节点的 CHAINED_TASK_CONFIG 配置中</span></span><br><span class="line">      config.setTransitiveChainedTaskConfigs(chainedConfigs.get(startNodeId));</span><br><span class="line"></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// 如果是 chain 中的子节点</span></span><br><span class="line">      </span><br><span class="line">      Map&lt;Integer, StreamConfig&gt; chainedConfs = chainedConfigs.get(startNodeId);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (chainedConfs == <span class="keyword">null</span>) &#123;</span><br><span class="line">        chainedConfigs.put(startNodeId, <span class="keyword">new</span> HashMap&lt;Integer, StreamConfig&gt;());</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// 将当前节点的StreamConfig添加到该chain的config集合中</span></span><br><span class="line">      chainedConfigs.get(startNodeId).put(currentNodeId, config);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 返回连往chain外部的出边集合</span></span><br><span class="line">    <span class="keyword">return</span> transitiveOutEdges;</span><br><span class="line"></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每个 JobVertex 都会对应一个可序列化的 StreamConfig, 用来发送给 JobManager 和 TaskManager。最后在 TaskManager 中起 Task 时,需要从这里面反序列化出所需要的配置信息, 其中就包括了含有用户代码的StreamOperator。</p><p><code>setChaining</code>会对source调用<code>createChain</code>方法，该方法会递归调用下游节点，从而构建出node chains。<code>createChain</code>会分析当前节点的出边，根据<a href="http://wuchong.me/blog/2016/05/09/flink-internals-understanding-execution-resources/#Operator-Chains">Operator Chains</a>中的chainable条件，将出边分成chainalbe和noChainable两类，并分别递归调用自身方法。之后会将StreamNode中的配置信息序列化到StreamConfig中。如果当前不是chain中的子节点，则会构建 JobVertex 和 JobEdge相连。如果是chain中的子节点，则会将StreamConfig添加到该chain的config集合中。一个node chains，除了 headOfChain node会生成对应的 JobVertex，其余的nodes都是以序列化的形式写入到StreamConfig中，并保存到headOfChain的 <code>CHAINED_TASK_CONFIG</code> 配置项中。直到部署时，才会取出并生成对应的ChainOperators，具体过程请见<a href="http://wuchong.me/blog/2016/05/09/flink-internals-understanding-execution-resources/#Operator-Chains">理解 Operator Chains</a>。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要对 Flink 中将 StreamGraph 转变成 JobGraph 的核心源码进行了分析。思想还是很简单的，StreamNode 转成 JobVertex，StreamEdge 转成 JobEdge，JobEdge 和 JobVertex 之间创建 IntermediateDataSet 来连接。关键点在于将多个 SteamNode chain 成一个 JobVertex的过程，这部分源码比较绕，有兴趣的同学可以结合源码单步调试分析。下一章将会介绍 JobGraph 提交到 JobManager 后是如何转换成分布式化的 ExecutionGraph 的。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;继前文&lt;a href=&quot;http://wuchong.me/blog/2016/05/03/flink-internals-overview/&quot;&gt;Flink 原理与实现：架构和拓扑概览&lt;/a&gt;中介绍了Flink的四层执行图模型，本文将主要介绍 Flink 是如何将 StreamGraph 转换成 JobGraph 的。根据用户用Stream API编写的程序，构造出一个代表拓扑结构的StreamGraph的。以 WordCount 为例，转换图如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://img3.tbcdn.cn/5476e8b07b923/TB1DzYXJFXXXXXJXVXXXXXXXXXX&quot; alt&gt;&lt;/p&gt;
&lt;p&gt;StreamGraph 和 JobGraph 都是在 Client 端生成的，也就是说我们可以在 IDE 中通过断点调试观察 StreamGraph 和 JobGraph 的生成过程。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
      <category term="源码分析" scheme="http://wuchong.me/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：理解 Flink 中的计算资源</title>
    <link href="http://wuchong.me/blog/2016/05/09/flink-internals-understanding-execution-resources/"/>
    <id>http://wuchong.me/blog/2016/05/09/flink-internals-understanding-execution-resources/</id>
    <published>2016-05-09T09:19:31.000Z</published>
    <updated>2022-08-03T06:46:44.504Z</updated>
    
    <content type="html"><![CDATA[<p>本文所讨论的计算资源是指用来执行 Task 的资源，是一个逻辑概念。本文会介绍 Flink 计算资源相关的一些核心概念，如：Slot、SlotSharingGroup、CoLocationGroup、Chain等。并会着重讨论 Flink 如何对计算资源进行管理和隔离，如何将计算资源利用率最大化等等。理解 Flink 中的计算资源对于理解 Job 如何在集群中运行的有很大的帮助，也有利于我们更透彻地理解 Flink 原理，更快速地定位问题。</p><h2 id="Operator-Chains"><a href="#Operator-Chains" class="headerlink" title="Operator Chains"></a>Operator Chains</h2><p>为了更高效地分布式执行，Flink会尽可能地将operator的subtask链接（chain）在一起形成task。每个task在一个线程中执行。将operators链接成task是非常有效的优化：它能减少线程之间的切换，减少消息的序列化/反序列化，减少数据在缓冲区的交换，减少了延迟的同时提高整体的吞吐量。</p><a id="more"></a><p>我们仍以经典的 WordCount 为例（参考<a href="http://wuchong.me/blog/2016/05/03/flink-internals-overview/#Job-例子">前文Job例子</a>），下面这幅图，展示了Source并行度为1，FlatMap、KeyAggregation、Sink并行度均为2，最终以5个并行的线程来执行的优化过程。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB18Gv5JFXXXXcDXXXXXXXXXXXX" alt></p><p>上图中将KeyAggregation和Sink两个operator进行了合并，因为这两个合并后并不会改变整体的拓扑结构。但是，并不是任意两个 operator 就能 chain 一起的。其条件还是很苛刻的：</p><ol><li>上下游的并行度一致</li><li>下游节点的入度为1 （也就是说下游节点没有来自其他节点的输入）</li><li>上下游节点都在同一个 slot group 中（下面会解释 slot group）</li><li>下游节点的 chain 策略为 ALWAYS（可以与上下游链接，map、flatmap、filter等默认是ALWAYS）</li><li>上游节点的 chain 策略为 ALWAYS 或 HEAD（只能与下游链接，不能与上游链接，Source默认是HEAD）</li><li>两个节点间数据分区方式是 forward（参考<a href="#">理解数据流的分区</a>）</li><li>用户没有禁用 chain</li></ol><p>Operator chain的行为可以通过编程API中进行指定。可以通过在DataStream的operator后面（如<code>someStream.map(..)</code>)调用<code>startNewChain()</code>来指示从该operator开始一个新的chain（与前面截断，不会被chain到前面）。或者调用<code>disableChaining()</code>来指示该operator不参与chaining（不会与前后的operator chain一起）。在底层，这两个方法都是通过调整operator的 chain 策略（HEAD、NEVER）来实现的。另外，也可以通过调用<code>StreamExecutionEnvironment.disableOperatorChaining()</code>来全局禁用chaining。</p><h3 id="原理与实现"><a href="#原理与实现" class="headerlink" title="原理与实现"></a>原理与实现</h3><p>那么 Flink 是如何将多个 operators chain在一起的呢？chain在一起的operators是如何作为一个整体被执行的呢？它们之间的数据流又是如何避免了序列化/反序列化以及网络传输的呢？下图展示了operators chain的内部实现：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1cFbJJFXXXXaIXVXXXXXXXXXX" alt></p><p>如上图所示，Flink内部是通过<code>OperatorChain</code>这个类来将多个operator链在一起形成一个新的operator。<code>OperatorChain</code>形成的框框就像一个黑盒，Flink 无需知道黑盒中有多少个ChainOperator、数据在chain内部是怎么流动的，只需要将input数据交给 HeadOperator 就可以了，这就使得<code>OperatorChain</code>在行为上与普通的operator无差别，上面的OperaotrChain就可以看做是一个入度为1，出度为2的operator。所以在实现中，对外可见的只有HeadOperator，以及与外部连通的实线输出，这些输出对应了JobGraph中的JobEdge，在底层通过<code>RecordWriterOutput</code>来实现。另外，框中的虚线是operator chain内部的数据流，这个流内的数据不会经过序列化/反序列化、网络传输，而是直接将消息对象传递给下游的 ChainOperator 处理，这是性能提升的关键点，在底层是通过 <code>ChainingOutput</code> 实现的，源码如下方所示，</p><p><em>注：HeadOperator和ChainOperator并不是具体的数据结构，前者指代chain中的第一个operator，后者指代chain中其余的operator，它们实际上都是<code>StreamOperator</code>。</em></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">ChainingOutput</span>&lt;<span class="title">T</span>&gt; <span class="keyword">implements</span> <span class="title">Output</span>&lt;<span class="title">StreamRecord</span>&lt;<span class="title">T</span>&gt;&gt; </span>&#123;</span><br><span class="line">  <span class="comment">// 注册的下游operator</span></span><br><span class="line">  <span class="keyword">protected</span> <span class="keyword">final</span> OneInputStreamOperator&lt;T, ?&gt; operator;</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">ChainingOutput</span><span class="params">(OneInputStreamOperator&lt;T, ?&gt; operator)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.operator = operator;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="comment">// 发送消息方法的实现，直接将消息对象传递给operator处理，不经过序列化/反序列化、网络传输</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">collect</span><span class="params">(StreamRecord&lt;T&gt; record)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      operator.setKeyContextElement1(record);</span><br><span class="line">      <span class="comment">// 下游operator直接处理消息对象</span></span><br><span class="line">      operator.processElement(record);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> ExceptionInChainedOperatorException(e);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Task-Slot"><a href="#Task-Slot" class="headerlink" title="Task Slot"></a>Task Slot</h2><p>在<a href="http://wuchong.me/blog/2016/05/03/flink-internals-overview/">架构概览</a>中我们介绍了 TaskManager 是一个 JVM 进程，并会以独立的线程来执行一个task或多个subtask。为了控制一个 TaskManager 能接受多少个 task，Flink 提出了 <em>Task Slot</em> 的概念。</p><p>Flink 中的计算资源通过 <em>Task Slot</em> 来定义。每个 task slot 代表了 TaskManager 的一个固定大小的资源子集。例如，一个拥有3个slot的 TaskManager，会将其管理的内存平均分成三分分给各个 slot。将资源 slot 化意味着来自不同job的task不会为了内存而竞争，而是每个task都拥有一定数量的内存储备。需要注意的是，这里不会涉及到CPU的隔离，slot目前仅仅用来隔离task的内存。</p><p>通过调整 task slot 的数量，用户可以定义task之间是如何相互隔离的。每个 TaskManager 有一个slot，也就意味着每个task运行在独立的 JVM 中。每个 TaskManager 有多个slot的话，也就是说多个task运行在同一个JVM中。而在同一个JVM进程中的task，可以共享TCP连接（基于多路复用）和心跳消息，可以减少数据的网络传输。也能共享一些数据结构，一定程度上减少了每个task的消耗。</p><p>每一个 TaskManager 会拥有一个或多个的 task slot，每个 slot 都能跑由多个连续 task 组成的一个 pipeline，比如 MapFunction 的第n个并行实例和 ReduceFunction 的第n个并行实例可以组成一个 pipeline。</p><p>如上文所述的 WordCount 例子，5个Task可能会在TaskManager的slots中如下图分布，2个TaskManager，每个有3个slot：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1Q4zUJFXXXXXnXVXXXXXXXXXX" alt></p><h2 id="SlotSharingGroup-与-CoLocationGroup"><a href="#SlotSharingGroup-与-CoLocationGroup" class="headerlink" title="SlotSharingGroup 与 CoLocationGroup"></a>SlotSharingGroup 与 CoLocationGroup</h2><p>默认情况下，Flink 允许subtasks共享slot，条件是它们都来自同一个Job的不同task的subtask。结果可能一个slot持有该job的整个pipeline。允许slot共享有以下两点好处：</p><ol><li>Flink 集群所需的task slots数与job中最高的并行度一致。也就是说我们不需要再去计算一个程序总共会起多少个task了。</li><li>更容易获得更充分的资源利用。如果没有slot共享，那么非密集型操作source/flatmap就会占用同密集型操作 keyAggregation/sink 一样多的资源。如果有slot共享，将基线的2个并行度增加到6个，能充分利用slot资源，同时保证每个TaskManager能平均分配到重的subtasks。</li></ol><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1VTj4JFXXXXX8XFXXXXXXXXXX" alt></p><p>我们将 WordCount 的并行度从之前的2个增加到6个（Source并行度仍为1），并开启slot共享（所有operator都在default共享组），将得到如上图所示的slot分布图。首先，我们不用去计算这个job会其多少个task，总之该任务最终会占用6个slots（最高并行度为6）。其次，我们可以看到密集型操作 keyAggregation/sink 被平均地分配到各个 TaskManager。</p><p><code>SlotSharingGroup</code>是Flink中用来实现slot共享的类，它尽可能地让subtasks共享一个slot。相应的，还有一个 <code>CoLocationGroup</code> 类用来强制将 subtasks 放到同一个 slot 中。<code>CoLocationGroup</code>主要用于<a href="https://ci.apache.org/projects/flink/flink-docs-master/apis/streaming/index.html#iterations" target="_blank" rel="noopener">迭代流</a>中，用来保证迭代头与迭代尾的第i个subtask能被调度到同一个TaskManager上。这里我们不会详细讨论<code>CoLocationGroup</code>的实现细节。</p><p>怎么判断operator属于哪个 slot 共享组呢？默认情况下，所有的operator都属于默认的共享组<code>default</code>，也就是说默认情况下所有的operator都是可以共享一个slot的。而当所有input operators具有相同的slot共享组时，该operator会继承这个共享组。最后，为了防止不合理的共享，用户也能通过API来强制指定operator的共享组，比如：<code>someStream.filter(...).slotSharingGroup(&quot;group1&quot;);</code>就强制指定了filter的slot共享组为<code>group1</code>。</p><h3 id="原理与实现-1"><a href="#原理与实现-1" class="headerlink" title="原理与实现"></a>原理与实现</h3><p>那么多个tasks（或者说operators）是如何共享slot的呢？</p><p>我们先来看一下用来定义计算资源的slot的类图：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1NxjUJFXXXXXoaXXXXXXXXXXX" alt></p><p>抽象类<code>Slot</code>定义了该槽位属于哪个TaskManager（<code>instance</code>）的第几个槽位（<code>slotNumber</code>），属于哪个Job（<code>jobID</code>）等信息。最简单的情况下，一个slot只持有一个task，也就是<code>SimpleSlot</code>的实现。复杂点的情况，一个slot能共享给多个task使用，也就是<code>SharedSlot</code>的实现。SharedSlot能包含其他的SharedSlot，也能包含SimpleSlot。所以一个SharedSlot能定义出一棵slots树。</p><p>接下来我们来看看 Flink 为subtask分配slot的过程。关于Flink调度，有两个非常重要的原则我们必须知道：（1）同一个operator的各个subtask是不能呆在同一个SharedSlot中的，例如<code>FlatMap[1]</code>和<code>FlatMap[2]</code>是不能在同一个SharedSlot中的。（2）Flink是按照拓扑顺序从Source一个个调度到Sink的。例如WordCount（Source并行度为1，其他并行度为2），那么调度的顺序依次是：<code>Source</code> -&gt; <code>FlatMap[1]</code> -&gt; <code>FlatMap[2]</code> -&gt; <code>KeyAgg-&gt;Sink[1]</code> -&gt; <code>KeyAgg-&gt;Sink[2]</code>。假设现在有2个TaskManager，每个只有1个slot（为简化问题），那么分配slot的过程如图所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1TM_3JFXXXXb8XVXXXXXXXXXX" alt></p><p><em>注：图中 SharedSlot 与 SimpleSlot 后带的括号中的数字代表槽位号（slotNumber）</em></p><ol><li>为<code>Source</code>分配slot。首先，我们从TaskManager1中分配出一个SharedSlot。并从SharedSlot中为<code>Source</code>分配出一个SimpleSlot。如上图中的①和②。</li><li>为<code>FlatMap[1]</code>分配slot。目前已经有一个SharedSlot，则从该SharedSlot中分配出一个SimpleSlot用来部署<code>FlatMap[1]</code>。如上图中的③。</li><li>为<code>FlatMap[2]</code>分配slot。由于TaskManager1的SharedSlot中已经有同operator的<code>FlatMap[1]</code>了，我们只能分配到其他SharedSlot中去。从TaskManager2中分配出一个SharedSlot，并从该SharedSlot中为<code>FlatMap[2]</code>分配出一个SimpleSlot。如上图的④和⑤。</li><li>为<code>Key-&gt;Sink[1]</code>分配slot。目前两个SharedSlot都符合条件，从TaskManager1的SharedSlot中分配出一个SimpleSlot用来部署<code>Key-&gt;Sink[1]</code>。如上图中的⑥。</li><li>为<code>Key-&gt;Sink[2]</code>分配slot。TaskManager1的SharedSlot中已经有同operator的<code>Key-&gt;Sink[1]</code>了，则只能选择另一个SharedSlot中分配出一个SimpleSlot用来部署<code>Key-&gt;Sink[2]</code>。如上图中的⑦。</li></ol><p>最后<code>Source</code>、<code>FlatMap[1]</code>、<code>Key-&gt;Sink[1]</code>这些subtask都会部署到TaskManager1的唯一一个slot中，并启动对应的线程。<code>FlatMap[2]</code>、<code>Key-&gt;Sink[2]</code>这些subtask都会被部署到TaskManager2的唯一一个slot中，并启动对应的线程。从而实现了slot共享。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要介绍了Flink中计算资源的相关概念以及原理实现。最核心的是 Task Slot，每个slot能运行一个或多个task。为了拓扑更高效地运行，Flink提出了Chaining，尽可能地将operators chain在一起作为一个task来处理。为了资源更充分的利用，Flink又提出了SlotSharingGroup，尽可能地让多个task共享一个slot。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://ci.apache.org/projects/flink/flink-docs-master/internals/job_scheduling.html" target="_blank" rel="noopener">Flink: Jobs and Scheduling</a></li><li><a href="https://ci.apache.org/projects/flink/flink-docs-master/concepts/concepts.html" target="_blank" rel="noopener">Flink Concepts</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文所讨论的计算资源是指用来执行 Task 的资源，是一个逻辑概念。本文会介绍 Flink 计算资源相关的一些核心概念，如：Slot、SlotSharingGroup、CoLocationGroup、Chain等。并会着重讨论 Flink 如何对计算资源进行管理和隔离，如何将计算资源利用率最大化等等。理解 Flink 中的计算资源对于理解 Job 如何在集群中运行的有很大的帮助，也有利于我们更透彻地理解 Flink 原理，更快速地定位问题。&lt;/p&gt;
&lt;h2 id=&quot;Operator-Chains&quot;&gt;&lt;a href=&quot;#Operator-Chains&quot; class=&quot;headerlink&quot; title=&quot;Operator Chains&quot;&gt;&lt;/a&gt;Operator Chains&lt;/h2&gt;&lt;p&gt;为了更高效地分布式执行，Flink会尽可能地将operator的subtask链接（chain）在一起形成task。每个task在一个线程中执行。将operators链接成task是非常有效的优化：它能减少线程之间的切换，减少消息的序列化/反序列化，减少数据在缓冲区的交换，减少了延迟的同时提高整体的吞吐量。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：如何生成 StreamGraph</title>
    <link href="http://wuchong.me/blog/2016/05/04/flink-internal-how-to-build-streamgraph/"/>
    <id>http://wuchong.me/blog/2016/05/04/flink-internal-how-to-build-streamgraph/</id>
    <published>2016-05-04T15:56:27.000Z</published>
    <updated>2022-08-03T06:46:44.503Z</updated>
    
    <content type="html"><![CDATA[<p>继上文<a href="http://wuchong.me/blog/2016/05/03/flink-internals-overview/">Flink 原理与实现：架构和拓扑概览</a>中介绍了Flink的四层执行图模型，本文将主要介绍 Flink 是如何根据用户用Stream API编写的程序，构造出一个代表拓扑结构的StreamGraph的。</p><p><em>注：本文比较偏源码分析，所有代码都是基于 flink-1.0.x 版本，建议在阅读本文前先对Stream API有个了解，详见<a href="https://ci.apache.org/projects/flink/flink-docs-master/apis/streaming/index.html" target="_blank" rel="noopener">官方文档</a>。</em></p><p>StreamGraph 相关的代码主要在 <code>org.apache.flink.streaming.api.graph</code> 包中。构造StreamGraph的入口函数是 <code>StreamGraphGenerator.generate(env, transformations)</code>。该函数会由触发程序执行的方法<code>StreamExecutionEnvironment.execute()</code>调用到。也就是说 StreamGraph 是在 Client 端构造的，这也意味着我们可以在本地通过调试观察 StreamGraph 的构造过程。</p><a id="more"></a><h2 id="Transformation"><a href="#Transformation" class="headerlink" title="Transformation"></a>Transformation</h2><p><code>StreamGraphGenerator.generate</code> 的一个关键的参数是 <code>List&lt;StreamTransformation&lt;?&gt;&gt;</code>。<code>StreamTransformation</code>代表了从一个或多个<code>DataStream</code>生成新<code>DataStream</code>的操作。<code>DataStream</code>的底层其实就是一个 <code>StreamTransformation</code>，描述了这个<code>DataStream</code>是怎么来的。</p><p>StreamTransformation的类图如下图所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1yQmNJFXXXXXnXpXXXXXXXXXX" alt></p><p>DataStream 上常见的 transformation 有 map、flatmap、filter等（见<a href="https://ci.apache.org/projects/flink/flink-docs-master/apis/streaming/index.html#datastream-transformations" target="_blank" rel="noopener">DataStream Transformation</a>了解更多）。这些transformation会构造出一棵 StreamTransformation 树，通过这棵树转换成 StreamGraph。比如 <code>DataStream.map</code>源码如下，其中<code>SingleOutputStreamOperator</code>为DataStream的子类：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;R&gt; <span class="function">SingleOutputStreamOperator&lt;R&gt; <span class="title">map</span><span class="params">(MapFunction&lt;T, R&gt; mapper)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 通过java reflection抽出mapper的返回值类型</span></span><br><span class="line">  TypeInformation&lt;R&gt; outType = TypeExtractor.getMapReturnTypes(clean(mapper), getType(),</span><br><span class="line">      Utils.getCallLocationName(), <span class="keyword">true</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 返回一个新的DataStream，SteramMap 为 StreamOperator 的实现类</span></span><br><span class="line">  <span class="keyword">return</span> transform(<span class="string">"Map"</span>, outType, <span class="keyword">new</span> StreamMap&lt;&gt;(clean(mapper)));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> &lt;R&gt; <span class="function">SingleOutputStreamOperator&lt;R&gt; <span class="title">transform</span><span class="params">(String operatorName, TypeInformation&lt;R&gt; outTypeInfo, OneInputStreamOperator&lt;T, R&gt; operator)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// read the output type of the input Transform to coax out errors about MissingTypeInfo</span></span><br><span class="line">  transformation.getOutputType();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 新的transformation会连接上当前DataStream中的transformation，从而构建成一棵树</span></span><br><span class="line">  OneInputTransformation&lt;T, R&gt; resultTransform = <span class="keyword">new</span> OneInputTransformation&lt;&gt;(</span><br><span class="line">      <span class="keyword">this</span>.transformation,</span><br><span class="line">      operatorName,</span><br><span class="line">      operator,</span><br><span class="line">      outTypeInfo,</span><br><span class="line">      environment.getParallelism());</span><br><span class="line"></span><br><span class="line">  <span class="meta">@SuppressWarnings</span>(&#123; <span class="string">"unchecked"</span>, <span class="string">"rawtypes"</span> &#125;)</span><br><span class="line">  SingleOutputStreamOperator&lt;R&gt; returnStream = <span class="keyword">new</span> SingleOutputStreamOperator(environment, resultTransform);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 所有的transformation都会存到 env 中，调用execute时遍历该list生成StreamGraph</span></span><br><span class="line">  getExecutionEnvironment().addOperator(resultTransform);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> returnStream;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从上方代码可以了解到，map转换将用户自定义的函数<code>MapFunction</code>包装到<code>StreamMap</code>这个Operator中，再将<code>StreamMap</code>包装到<code>OneInputTransformation</code>，最后该transformation存到env中，当调用<code>env.execute</code>时，遍历其中的transformation集合构造出StreamGraph。其分层实现如下图所示：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB12u5yJFXXXXXhaXXXXXXXXXXX" alt></p><p>另外，并不是每一个 StreamTransformation 都会转换成 runtime 层中物理操作。有一些只是逻辑概念，比如 union、split/select、partition等。如下图所示的转换树，在运行时会优化成下方的操作图。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1XgmOJFXXXXaYXpXXXXXXXXXX" alt></p><p>union、split/select、partition中的信息会被写入到 Source –&gt; Map 的边中。通过源码也可以发现，<code>UnionTransformation</code>,<code>SplitTransformation</code>,<code>SelectTransformation</code>,<code>PartitionTransformation</code>由于不包含具体的操作所以都没有StreamOperator成员变量，而其他StreamTransformation的子类基本上都有。</p><h2 id="StreamOperator"><a href="#StreamOperator" class="headerlink" title="StreamOperator"></a>StreamOperator</h2><p>DataStream 上的每一个 Transformation 都对应了一个 StreamOperator，StreamOperator是运行时的具体实现，会决定UDF(User-Defined Funtion)的调用方式。下图所示为 StreamOperator 的类图（点击查看大图）：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1l9aYJFXXXXbAXXXXXXXXXXXX" alt></p><p>可以发现，所有实现类都继承了<code>AbstractStreamOperator</code>。另外除了 project 操作，其他所有可以执行UDF代码的实现类都继承自<code>AbstractUdfStreamOperator</code>，该类是封装了UDF的StreamOperator。UDF就是实现了<code>Function</code>接口的类，如<code>MapFunction</code>,<code>FilterFunction</code>。</p><h2 id="生成-StreamGraph-的源码分析"><a href="#生成-StreamGraph-的源码分析" class="headerlink" title="生成 StreamGraph 的源码分析"></a>生成 StreamGraph 的源码分析</h2><p>我们通过在DataStream上做了一系列的转换（map、filter等）得到了StreamTransformation集合，然后通过<code>StreamGraphGenerator.generate</code>获得StreamGraph，该方法的源码如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 构造 StreamGraph 入口函数</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> StreamGraph <span class="title">generate</span><span class="params">(StreamExecutionEnvironment env, List&lt;StreamTransformation&lt;?&gt;&gt; transformations)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> StreamGraphGenerator(env).generateInternal(transformations);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 自底向上（sink-&gt;source）对转换树的每个transformation进行转换。</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> StreamGraph <span class="title">generateInternal</span><span class="params">(List&lt;StreamTransformation&lt;?&gt;&gt; transformations)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">for</span> (StreamTransformation&lt;?&gt; transformation: transformations) &#123;</span><br><span class="line">    transform(transformation);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> streamGraph;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 对具体的一个transformation进行转换，转换成 StreamGraph 中的 StreamNode 和 StreamEdge</span></span><br><span class="line"><span class="comment">// 返回值为该transform的id集合，通常大小为1个（除FeedbackTransformation）</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> Collection&lt;Integer&gt; <span class="title">transform</span><span class="params">(StreamTransformation&lt;?&gt; transform)</span> </span>&#123;  </span><br><span class="line">  <span class="comment">// 跳过已经转换过的transformation</span></span><br><span class="line">  <span class="keyword">if</span> (alreadyTransformed.containsKey(transform)) &#123;</span><br><span class="line">    <span class="keyword">return</span> alreadyTransformed.get(transform);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  LOG.debug(<span class="string">"Transforming "</span> + transform);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 为了触发 MissingTypeInfo 的异常</span></span><br><span class="line">  transform.getOutputType();</span><br><span class="line"></span><br><span class="line">  Collection&lt;Integer&gt; transformedIds;</span><br><span class="line">  <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> OneInputTransformation&lt;?, ?&gt;) &#123;</span><br><span class="line">    transformedIds = transformOnInputTransform((OneInputTransformation&lt;?, ?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> TwoInputTransformation&lt;?, ?, ?&gt;) &#123;</span><br><span class="line">    transformedIds = transformTwoInputTransform((TwoInputTransformation&lt;?, ?, ?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> SourceTransformation&lt;?&gt;) &#123;</span><br><span class="line">    transformedIds = transformSource((SourceTransformation&lt;?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> SinkTransformation&lt;?&gt;) &#123;</span><br><span class="line">    transformedIds = transformSink((SinkTransformation&lt;?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> UnionTransformation&lt;?&gt;) &#123;</span><br><span class="line">    transformedIds = transformUnion((UnionTransformation&lt;?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> SplitTransformation&lt;?&gt;) &#123;</span><br><span class="line">    transformedIds = transformSplit((SplitTransformation&lt;?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> SelectTransformation&lt;?&gt;) &#123;</span><br><span class="line">    transformedIds = transformSelect((SelectTransformation&lt;?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> FeedbackTransformation&lt;?&gt;) &#123;</span><br><span class="line">    transformedIds = transformFeedback((FeedbackTransformation&lt;?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> CoFeedbackTransformation&lt;?&gt;) &#123;</span><br><span class="line">    transformedIds = transformCoFeedback((CoFeedbackTransformation&lt;?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (transform <span class="keyword">instanceof</span> PartitionTransformation&lt;?&gt;) &#123;</span><br><span class="line">    transformedIds = transformPartition((PartitionTransformation&lt;?&gt;) transform);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"Unknown transformation: "</span> + transform);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// need this check because the iterate transformation adds itself before</span></span><br><span class="line">  <span class="comment">// transforming the feedback edges</span></span><br><span class="line">  <span class="keyword">if</span> (!alreadyTransformed.containsKey(transform)) &#123;</span><br><span class="line">    alreadyTransformed.put(transform, transformedIds);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (transform.getBufferTimeout() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    streamGraph.setBufferTimeout(transform.getId(), transform.getBufferTimeout());</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (transform.getUid() != <span class="keyword">null</span>) &#123;</span><br><span class="line">    streamGraph.setTransformationId(transform.getId(), transform.getUid());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> transformedIds;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最终都会调用 <code>transformXXX</code> 来对具体的StreamTransformation进行转换。我们可以看下<code>transformOnInputTransform(transform)</code>的实现：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> &lt;IN, OUT&gt; <span class="function">Collection&lt;Integer&gt; <span class="title">transformOnInputTransform</span><span class="params">(OneInputTransformation&lt;IN, OUT&gt; transform)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 递归对该transform的直接上游transform进行转换，获得直接上游id集合</span></span><br><span class="line">  Collection&lt;Integer&gt; inputIds = transform(transform.getInput());</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 递归调用可能已经处理过该transform了</span></span><br><span class="line">  <span class="keyword">if</span> (alreadyTransformed.containsKey(transform)) &#123;</span><br><span class="line">    <span class="keyword">return</span> alreadyTransformed.get(transform);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  String slotSharingGroup = determineSlotSharingGroup(transform.getSlotSharingGroup(), inputIds);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 添加 StreamNode</span></span><br><span class="line">  streamGraph.addOperator(transform.getId(),</span><br><span class="line">      slotSharingGroup,</span><br><span class="line">      transform.getOperator(),</span><br><span class="line">      transform.getInputType(),</span><br><span class="line">      transform.getOutputType(),</span><br><span class="line">      transform.getName());</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (transform.getStateKeySelector() != <span class="keyword">null</span>) &#123;</span><br><span class="line">    TypeSerializer&lt;?&gt; keySerializer = transform.getStateKeyType().createSerializer(env.getConfig());</span><br><span class="line">    streamGraph.setOneInputStateKey(transform.getId(), transform.getStateKeySelector(), keySerializer);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  streamGraph.setParallelism(transform.getId(), transform.getParallelism());</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 添加 StreamEdge</span></span><br><span class="line">  <span class="keyword">for</span> (Integer inputId: inputIds) &#123;</span><br><span class="line">    streamGraph.addEdge(inputId, transform.getId(), <span class="number">0</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> Collections.singleton(transform.getId());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该函数首先会对该transform的上游transform进行递归转换，确保上游的都已经完成了转化。然后通过transform构造出StreamNode，最后与上游的transform进行连接，构造出StreamNode。</p><p>最后再来看下对逻辑转换（partition、union等）的处理，如下是<code>transformPartition</code>函数的源码：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> &lt;T&gt; <span class="function">Collection&lt;Integer&gt; <span class="title">transformPartition</span><span class="params">(PartitionTransformation&lt;T&gt; partition)</span> </span>&#123;</span><br><span class="line">  StreamTransformation&lt;T&gt; input = partition.getInput();</span><br><span class="line">  List&lt;Integer&gt; resultIds = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 直接上游的id</span></span><br><span class="line">  Collection&lt;Integer&gt; transformedIds = transform(input);</span><br><span class="line">  <span class="keyword">for</span> (Integer transformedId: transformedIds) &#123;</span><br><span class="line">    <span class="comment">// 生成一个新的虚拟id</span></span><br><span class="line">    <span class="keyword">int</span> virtualId = StreamTransformation.getNewNodeId();</span><br><span class="line">    <span class="comment">// 添加一个虚拟分区节点，不会生成 StreamNode</span></span><br><span class="line">    streamGraph.addVirtualPartitionNode(transformedId, virtualId, partition.getPartitioner());</span><br><span class="line">    resultIds.add(virtualId);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> resultIds;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对partition的转换没有生成具体的StreamNode和StreamEdge，而是添加一个虚节点。当partition的下游transform（如map）添加edge时（调用<code>StreamGraph.addEdge</code>），会把partition信息写入到edge中。如<code>StreamGraph.addEdgeInternal</code>所示：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addEdge</span><span class="params">(Integer upStreamVertexID, Integer downStreamVertexID, <span class="keyword">int</span> typeNumber)</span> </span>&#123;</span><br><span class="line">  addEdgeInternal(upStreamVertexID, downStreamVertexID, typeNumber, <span class="keyword">null</span>, <span class="keyword">new</span> ArrayList&lt;String&gt;());</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">addEdgeInternal</span><span class="params">(Integer upStreamVertexID,</span></span></span><br><span class="line"><span class="function"><span class="params">    Integer downStreamVertexID,</span></span></span><br><span class="line"><span class="function"><span class="params">    <span class="keyword">int</span> typeNumber,</span></span></span><br><span class="line"><span class="function"><span class="params">    StreamPartitioner&lt;?&gt; partitioner,</span></span></span><br><span class="line"><span class="function"><span class="params">    List&lt;String&gt; outputNames)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 当上游是select时，递归调用，并传入select信息</span></span><br><span class="line">  <span class="keyword">if</span> (virtualSelectNodes.containsKey(upStreamVertexID)) &#123;</span><br><span class="line">    <span class="keyword">int</span> virtualId = upStreamVertexID;</span><br><span class="line">    <span class="comment">// select上游的节点id</span></span><br><span class="line">    upStreamVertexID = virtualSelectNodes.get(virtualId).f0;</span><br><span class="line">    <span class="keyword">if</span> (outputNames.isEmpty()) &#123;</span><br><span class="line">      <span class="comment">// selections that happen downstream override earlier selections</span></span><br><span class="line">      outputNames = virtualSelectNodes.get(virtualId).f1;</span><br><span class="line">    &#125;</span><br><span class="line">    addEdgeInternal(upStreamVertexID, downStreamVertexID, typeNumber, partitioner, outputNames);</span><br><span class="line">  &#125; </span><br><span class="line">  <span class="comment">// 当上游是partition时，递归调用，并传入partitioner信息</span></span><br><span class="line">  <span class="keyword">else</span> <span class="keyword">if</span> (virtuaPartitionNodes.containsKey(upStreamVertexID)) &#123;</span><br><span class="line">    <span class="keyword">int</span> virtualId = upStreamVertexID;</span><br><span class="line">    <span class="comment">// partition上游的节点id</span></span><br><span class="line">    upStreamVertexID = virtuaPartitionNodes.get(virtualId).f0;</span><br><span class="line">    <span class="keyword">if</span> (partitioner == <span class="keyword">null</span>) &#123;</span><br><span class="line">      partitioner = virtuaPartitionNodes.get(virtualId).f1;</span><br><span class="line">    &#125;</span><br><span class="line">    addEdgeInternal(upStreamVertexID, downStreamVertexID, typeNumber, partitioner, outputNames);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 真正构建StreamEdge</span></span><br><span class="line">    StreamNode upstreamNode = getStreamNode(upStreamVertexID);</span><br><span class="line">    StreamNode downstreamNode = getStreamNode(downStreamVertexID);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 未指定partitioner的话，会为其选择 forward 或 rebalance 分区。</span></span><br><span class="line">    <span class="keyword">if</span> (partitioner == <span class="keyword">null</span> &amp;&amp; upstreamNode.getParallelism() == downstreamNode.getParallelism()) &#123;</span><br><span class="line">      partitioner = <span class="keyword">new</span> ForwardPartitioner&lt;Object&gt;();</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (partitioner == <span class="keyword">null</span>) &#123;</span><br><span class="line">      partitioner = <span class="keyword">new</span> RebalancePartitioner&lt;Object&gt;();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 健康检查， forward 分区必须要上下游的并发度一致</span></span><br><span class="line">    <span class="keyword">if</span> (partitioner <span class="keyword">instanceof</span> ForwardPartitioner) &#123;</span><br><span class="line">      <span class="keyword">if</span> (upstreamNode.getParallelism() != downstreamNode.getParallelism()) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> UnsupportedOperationException(<span class="string">"Forward partitioning does not allow "</span> +</span><br><span class="line">            <span class="string">"change of parallelism. Upstream operation: "</span> + upstreamNode + <span class="string">" parallelism: "</span> + upstreamNode.getParallelism() +</span><br><span class="line">            <span class="string">", downstream operation: "</span> + downstreamNode + <span class="string">" parallelism: "</span> + downstreamNode.getParallelism() +</span><br><span class="line">            <span class="string">" You must use another partitioning strategy, such as broadcast, rebalance, shuffle or global."</span>);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 创建 StreamEdge</span></span><br><span class="line">    StreamEdge edge = <span class="keyword">new</span> StreamEdge(upstreamNode, downstreamNode, typeNumber, outputNames, partitioner);</span><br><span class="line">    <span class="comment">// 将该 StreamEdge 添加到上游的输出，下游的输入</span></span><br><span class="line">    getStreamNode(edge.getSourceId()).addOutEdge(edge);</span><br><span class="line">    getStreamNode(edge.getTargetId()).addInEdge(edge);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="实例讲解"><a href="#实例讲解" class="headerlink" title="实例讲解"></a>实例讲解</h2><p>如下程序，是一个从 Source 中按行切分成单词并过滤输出的简单流程序，其中包含了逻辑转换：随机分区shuffle。我们会分析该程序是如何生成StreamGraph的。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">DataStream&lt;String&gt; text = env.socketTextStream(hostName, port);</span><br><span class="line">text.flatMap(<span class="keyword">new</span> LineSplitter()).shuffle().filter(<span class="keyword">new</span> HelloFilter()).print();</span><br></pre></td></tr></table></figure><p>首先会在env中生成一棵transformation树，用<code>List&lt;StreamTransformation&lt;?&gt;&gt;</code>保存。其结构图如下：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1w3SQJFXXXXalXVXXXXXXXXXX" alt></p><p>其中符号<code>*</code>为input指针，指向上游的transformation，从而形成了一棵transformation树。然后，通过调用<code>StreamGraphGenerator.generate(env, transformations)</code>来生成StreamGraph。自底向上递归调用每一个transformation，也就是说处理顺序是Source-&gt;FlatMap-&gt;Shuffle-&gt;Filter-&gt;Sink。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1s7SpJFXXXXXjaXXXXXXXXXXX" alt></p><p>如上图所示：</p><ol><li>首先处理的Source，生成了Source的StreamNode。</li><li>然后处理的FlatMap，生成了FlatMap的StreamNode，并生成StreamEdge连接上游Source和FlatMap。由于上下游的并发度不一样（1:4），所以此处是Rebalance分区。</li><li>然后处理的Shuffle，由于是逻辑转换，并不会生成实际的节点。将partitioner信息暂存在<code>virtuaPartitionNodes</code>中。</li><li>在处理Filter时，生成了Filter的StreamNode。发现上游是shuffle，找到shuffle的上游FlatMap，创建StreamEdge与Filter相连。并把ShufflePartitioner的信息写到StreamEdge中。</li><li>最后处理Sink，创建Sink的StreamNode，并生成StreamEdge与上游Filter相连。由于上下游并发度一样（4:4），所以此处选择 Forward 分区。</li></ol><p>最后可以通过 <a href="http://flink.apache.org/visualizer/" target="_blank" rel="noopener">UI可视化</a> 来观察得到的 StreamGraph。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1y_1FJFXXXXapaXXXXXXXXXXX" alt></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要介绍了 Stream API 中 Transformation 和 Operator 的概念，以及如何根据Stream API编写的程序，构造出一个代表拓扑结构的StreamGraph的。本文的源码分析涉及到较多代码，如果有兴趣建议结合完整源码进行学习。下一篇文章将介绍 StreamGraph 如何转换成 JobGraph 的，其中设计到了图优化的技巧。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;继上文&lt;a href=&quot;http://wuchong.me/blog/2016/05/03/flink-internals-overview/&quot;&gt;Flink 原理与实现：架构和拓扑概览&lt;/a&gt;中介绍了Flink的四层执行图模型，本文将主要介绍 Flink 是如何根据用户用Stream API编写的程序，构造出一个代表拓扑结构的StreamGraph的。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注：本文比较偏源码分析，所有代码都是基于 flink-1.0.x 版本，建议在阅读本文前先对Stream API有个了解，详见&lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-master/apis/streaming/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;官方文档&lt;/a&gt;。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;StreamGraph 相关的代码主要在 &lt;code&gt;org.apache.flink.streaming.api.graph&lt;/code&gt; 包中。构造StreamGraph的入口函数是 &lt;code&gt;StreamGraphGenerator.generate(env, transformations)&lt;/code&gt;。该函数会由触发程序执行的方法&lt;code&gt;StreamExecutionEnvironment.execute()&lt;/code&gt;调用到。也就是说 StreamGraph 是在 Client 端构造的，这也意味着我们可以在本地通过调试观察 StreamGraph 的构造过程。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
      <category term="源码分析" scheme="http://wuchong.me/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：架构和拓扑概览</title>
    <link href="http://wuchong.me/blog/2016/05/03/flink-internals-overview/"/>
    <id>http://wuchong.me/blog/2016/05/03/flink-internals-overview/</id>
    <published>2016-05-03T15:41:40.000Z</published>
    <updated>2022-08-03T06:46:44.504Z</updated>
    
    <content type="html"><![CDATA[<h2 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h2><p>要了解一个系统，一般都是从架构开始。我们关心的问题是：系统部署成功后各个节点都启动了哪些服务，各个服务之间又是怎么交互和协调的。下方是 Flink 集群启动后架构图。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1ObBnJFXXXXXtXVXXXXXXXXXX" alt></p><p>当 Flink 集群启动后，首先会启动一个 JobManger 和一个或多个的 TaskManager。由 Client 提交任务给 JobManager，JobManager 再调度任务到各个 TaskManager 去执行，然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。</p><a id="more"></a><ul><li><strong>Client</strong> 为提交 Job 的客户端，可以是运行在任何机器上（与 JobManager 环境连通即可）。提交 Job 后，Client 可以结束进程（Streaming的任务），也可以不结束并等待结果返回。</li><li><strong>JobManager</strong> 主要负责调度 Job 并协调 Task 做 checkpoint，职责上很像 Storm 的 Nimbus。从 Client 处接收到 Job 和 JAR 包等资源后，会生成优化后的执行计划，并以 Task 的单元调度到各个 TaskManager 去执行。</li><li><strong>TaskManager</strong> 在启动的时候就设置好了槽位数（Slot），每个 slot 能启动一个 Task，Task 为线程。从 JobManager 处接收需要部署的 Task，部署启动后，与自己的上游建立 Netty 连接，接收数据并处理。</li></ul><p>可以看到 Flink 的任务调度是多线程模型，并且不同Job/Task混合在一个 TaskManager 进程中。虽然这种方式可以有效提高 CPU 利用率，但是个人不太喜欢这种设计，因为不仅缺乏资源隔离机制，同时也不方便调试。类似 Storm 的进程模型，一个JVM 中只跑该 Job 的 Tasks 实际应用中更为合理。</p><h2 id="Job-例子"><a href="#Job-例子" class="headerlink" title="Job 例子"></a>Job 例子</h2><blockquote><p>本文所示例子为 flink-1.0.x 版本</p></blockquote><p>我们使用 Flink 自带的 examples 包中的 <code>SocketTextStreamWordCount</code>，这是一个从 socket 流中统计单词出现次数的例子。</p><ul><li><p>首先，使用 <strong>netcat</strong> 启动本地服务器：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ nc -l 9000</span><br></pre></td></tr></table></figure></li></ul><ul><li><p>然后提交 Flink 程序</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ bin/flink run examples/streaming/SocketTextStreamWordCount.jar \</span><br><span class="line">  --hostname 10.218.130.9 \</span><br><span class="line">  --port 9000</span><br></pre></td></tr></table></figure></li></ul><p>在netcat端输入单词并监控 taskmanager 的输出可以看到单词统计的结果。</p><p><code>SocketTextStreamWordCount</code> 的具体代码如下： </p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">  <span class="comment">// 检查输入</span></span><br><span class="line">  <span class="keyword">final</span> ParameterTool params = ParameterTool.fromArgs(args);</span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  <span class="comment">// set up the execution environment</span></span><br><span class="line">  <span class="keyword">final</span> StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// get input data</span></span><br><span class="line">  DataStream&lt;String&gt; text =</span><br><span class="line">      env.socketTextStream(params.get(<span class="string">"hostname"</span>), params.getInt(<span class="string">"port"</span>), <span class="string">'\n'</span>, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  DataStream&lt;Tuple2&lt;String, Integer&gt;&gt; counts =</span><br><span class="line">      <span class="comment">// split up the lines in pairs (2-tuples) containing: (word,1)</span></span><br><span class="line">      text.flatMap(<span class="keyword">new</span> Tokenizer())</span><br><span class="line">          <span class="comment">// group by the tuple field "0" and sum up tuple field "1"</span></span><br><span class="line">          .keyBy(<span class="number">0</span>)</span><br><span class="line">          .sum(<span class="number">1</span>);</span><br><span class="line">  counts.print();</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// execute program</span></span><br><span class="line">  env.execute(<span class="string">"WordCount from SocketTextStream Example"</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们将最后一行代码 <code>env.execute</code> 替换成 <code>System.out.println(env.getExecutionPlan());</code> 并在本地运行该代码（并发度设为2），可以得到该拓扑的逻辑执行计划图的 JSON 串，将该 JSON 串粘贴到 <a href="http://flink.apache.org/visualizer/" target="_blank" rel="noopener">http://flink.apache.org/visualizer/</a> 中，能可视化该执行图。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1vB1uJFXXXXbaXpXXXXXXXXXX" alt></p><p>但这并不是最终在 Flink 中运行的执行图，只是一个表示拓扑节点关系的计划图，在 Flink 中对应了 SteramGraph。另外，提交拓扑后（并发度设为2）还能在 UI 中看到另一张执行计划图，如下所示，该图对应了 Flink 中的 JobGraph。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1QKR2JFXXXXbyaXXXXXXXXXXX" alt></p><h2 id="Graph"><a href="#Graph" class="headerlink" title="Graph"></a>Graph</h2><p>看起来有点乱，怎么有这么多不一样的图。实际上，还有更多的图。Flink 中的执行图可以分成四层：StreamGraph -&gt; JobGraph -&gt; ExecutionGraph -&gt; 物理执行图。</p><ul><li><strong>StreamGraph：</strong>是根据用户通过 Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。</li><li><strong>JobGraph：</strong>StreamGraph经过优化后生成了 JobGraph，提交给 JobManager 的数据结构。主要的优化为，将多个符合条件的节点 chain 在一起作为一个节点，这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。</li><li><strong>ExecutionGraph：</strong>JobManager 根据 JobGraph 生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本，是调度层最核心的数据结构。</li><li><strong>物理执行图：</strong>JobManager 根据 ExecutionGraph 对 Job 进行调度后，在各个TaskManager 上部署 Task 后形成的“图”，并不是一个具体的数据结构。</li></ul><p>例如上文中的2个并发度（Source为1个并发度）的 <code>SocketTextStreamWordCount</code> 四层执行图的演变过程如下图所示（点击查看大图）：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1tA_GJFXXXXapXFXXXXXXXXXX" alt></p><p>这里对一些名词进行简单的解释。</p><ul><li><strong>StreamGraph：</strong>根据用户通过 Stream API 编写的代码生成的最初的图。 <ul><li>StreamNode：用来代表 operator 的类，并具有所有相关的属性，如并发度、入边和出边等。</li><li>StreamEdge：表示连接两个StreamNode的边。</li></ul></li><li><strong>JobGraph：</strong>StreamGraph经过优化后生成了 JobGraph，提交给 JobManager 的数据结构。<ul><li>JobVertex：经过优化后符合条件的多个StreamNode可能会chain在一起生成一个JobVertex，即一个JobVertex包含一个或多个operator，JobVertex的输入是JobEdge，输出是IntermediateDataSet。</li><li>IntermediateDataSet：表示JobVertex的输出，即经过operator处理产生的数据集。producer是JobVertex，consumer是JobEdge。</li><li>JobEdge：代表了job graph中的一条数据传输通道。source 是 IntermediateDataSet，target 是 JobVertex。即数据通过JobEdge由IntermediateDataSet传递给目标JobVertex。</li></ul></li><li><strong>ExecutionGraph：</strong>JobManager 根据 JobGraph 生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本，是调度层最核心的数据结构。<ul><li>ExecutionJobVertex：和JobGraph中的JobVertex一一对应。每一个ExecutionJobVertex都有和并发度一样多的 ExecutionVertex。</li><li>ExecutionVertex：表示ExecutionJobVertex的其中一个并发子任务，输入是ExecutionEdge，输出是IntermediateResultPartition。</li><li>IntermediateResult：和JobGraph中的IntermediateDataSet一一对应。一个IntermediateResult包含多个IntermediateResultPartition，其个数等于该operator的并发度。</li><li>IntermediateResultPartition：表示ExecutionVertex的一个输出分区，producer是ExecutionVertex，consumer是若干个ExecutionEdge。</li><li>ExecutionEdge：表示ExecutionVertex的输入，source是IntermediateResultPartition，target是ExecutionVertex。source和target都只能是一个。</li><li>Execution：是执行一个 ExecutionVertex 的一次尝试。当发生故障或者数据需要重算的情况下 ExecutionVertex 可能会有多个 ExecutionAttemptID。一个 Execution 通过 ExecutionAttemptID 来唯一标识。JM和TM之间关于 task 的部署和 task status 的更新都是通过 ExecutionAttemptID 来确定消息接受者。</li></ul></li><li><strong>物理执行图：</strong>JobManager 根据 ExecutionGraph 对 Job 进行调度后，在各个TaskManager 上部署 Task 后形成的“图”，并不是一个具体的数据结构。<ul><li>Task：Execution被调度后在分配的 TaskManager 中启动对应的 Task。Task 包裹了具有用户执行逻辑的 operator。</li><li>ResultPartition：代表由一个Task的生成的数据，和ExecutionGraph中的IntermediateResultPartition一一对应。</li><li>ResultSubpartition：是ResultPartition的一个子分区。每个ResultPartition包含多个ResultSubpartition，其数目要由下游消费 Task 数和 DistributionPattern 来决定。</li><li>InputGate：代表Task的输入封装，和JobGraph中JobEdge一一对应。每个InputGate消费了一个或多个的ResultPartition。</li><li>InputChannel：每个InputGate会包含一个以上的InputChannel，和ExecutionGraph中的ExecutionEdge一一对应，也和ResultSubpartition一对一地相连，即一个InputChannel接收一个ResultSubpartition的输出。</li></ul></li></ul><p>那么 Flink 为什么要设计这4张图呢，其目的是什么呢？Spark 中也有多张图，数据依赖图以及物理执行的DAG。其目的都是一样的，就是解耦，每张图各司其职，每张图对应了 Job 不同的阶段，更方便做该阶段的事情。我们给出更完整的 Flink Graph 的层次图。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1qmtpJVXXXXagXXXXXXXXXXXX" alt></p><p>首先我们看到，JobGraph 之上除了 StreamGraph 还有 OptimizedPlan。OptimizedPlan 是由 Batch API 转换而来的。StreamGraph 是由 Stream API 转换而来的。为什么 API 不直接转换成 JobGraph？因为，Batch 和 Stream 的图结构和优化方法有很大的区别，比如 Batch 有很多执行前的预分析用来优化图的执行，而这种优化并不普适于 Stream，所以通过 OptimizedPlan 来做 Batch 的优化会更方便和清晰，也不会影响 Stream。JobGraph 的责任就是统一 Batch 和 Stream 的图，用来描述清楚一个拓扑图的结构，并且做了 chaining 的优化，chaining 是普适于 Batch 和 Stream 的，所以在这一层做掉。ExecutionGraph 的责任是方便调度和各个 tasks 状态的监控和跟踪，所以 ExecutionGraph 是并行化的 JobGraph。而“物理执行图”就是最终分布式在各个机器上运行着的tasks了。所以可以看到，这种解耦方式极大地方便了我们在各个层所做的工作，各个层之间是相互隔离的。</p><p>后续的文章，将会详细介绍 Flink 是如何生成这些执行图的。由于我目前关注 Flink 的流处理功能，所以主要有以下内容：</p><ol><li><a href="http://wuchong.me/blog/2016/05/04/flink-internal-how-to-build-streamgraph/">如何生成 StreamGraph</a></li><li><a href="http://wuchong.me/blog/2016/05/10/flink-internals-how-to-build-jobgraph/">如何生成 JobGraph</a></li><li>如何生成 ExecutionGraph</li><li>如何进行调度（如何生成物理执行图）</li></ol>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;架构&quot;&gt;&lt;a href=&quot;#架构&quot; class=&quot;headerlink&quot; title=&quot;架构&quot;&gt;&lt;/a&gt;架构&lt;/h2&gt;&lt;p&gt;要了解一个系统，一般都是从架构开始。我们关心的问题是：系统部署成功后各个节点都启动了哪些服务，各个服务之间又是怎么交互和协调的。下方是 Flink 集群启动后架构图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://img3.tbcdn.cn/5476e8b07b923/TB1ObBnJFXXXXXtXVXXXXXXXXXX&quot; alt&gt;&lt;/p&gt;
&lt;p&gt;当 Flink 集群启动后，首先会启动一个 JobManger 和一个或多个的 TaskManager。由 Client 提交任务给 JobManager，JobManager 再调度任务到各个 TaskManager 去执行，然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：内存管理</title>
    <link href="http://wuchong.me/blog/2016/04/29/flink-internals-memory-manage/"/>
    <id>http://wuchong.me/blog/2016/04/29/flink-internals-memory-manage/</id>
    <published>2016-04-29T06:44:51.000Z</published>
    <updated>2022-08-03T06:46:44.503Z</updated>
    
    <content type="html"><![CDATA[<p>如今，大数据领域的开源框架（Hadoop，Spark，Storm）都使用的 JVM，当然也包括 Flink。基于 JVM 的数据分析引擎都需要面对将大量数据存到内存中，这就不得不面对 JVM 存在的几个问题：</p><ol><li>Java 对象存储密度低。一个只包含 boolean 属性的对象占用了16个字节内存：对象头占了8个，boolean 属性占了1个，对齐填充占了7个。而实际上只需要一个bit（1/8字节）就够了。</li><li>Full GC 会极大地影响性能，尤其是为了处理更大数据而开了很大内存空间的JVM来说，GC 会达到秒级甚至分钟级。</li><li>OOM 问题影响稳定性。OutOfMemoryError是分布式计算框架经常会遇到的问题，当JVM中所有对象大小超过分配给JVM的内存大小时，就会发生OutOfMemoryError错误，导致JVM崩溃，分布式框架的健壮性和性能都会受到影响。</li></ol><p>所以目前，越来越多的大数据项目开始自己管理JVM内存了，像 Spark、Flink、HBase，为的就是获得像 C 一样的性能以及避免 OOM 的发生。本文将会讨论 Flink 是如何解决上面的问题的，主要内容包括内存管理、定制的序列化工具、缓存友好的数据结构和算法、堆外内存、JIT编译优化等。</p><a id="more"></a><h2 id="积极的内存管理"><a href="#积极的内存管理" class="headerlink" title="积极的内存管理"></a>积极的内存管理</h2><p>Flink 并不是将大量对象存在堆上，而是将对象都序列化到一个预分配的内存块上，这个内存块叫做 <code>MemorySegment</code>，它代表了一段固定长度的内存（默认大小为 32KB），也是 Flink 中最小的内存分配单元，并且提供了非常高效的读写方法。你可以把 MemorySegment 想象成是为 Flink 定制的 <code>java.nio.ByteBuffer</code>。它的底层可以是一个普通的 Java 字节数组（<code>byte[]</code>），也可以是一个申请在堆外的 <code>ByteBuffer</code>。每条记录都会以序列化的形式存储在一个或多个<code>MemorySegment</code>中。</p><p>Flink 中的 Worker 名叫 TaskManager，是用来运行用户代码的 JVM 进程。TaskManager 的堆内存主要被分成了三个部分：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB17qs5JpXXXXXhXpXXXXXXXXXX" width="300px"></p><ul><li><strong>Network Buffers:</strong> 一定数量的32KB大小的 buffer，主要用于数据的网络传输。在 TaskManager 启动的时候就会分配。默认数量是 2048 个，可以通过 <code>taskmanager.network.numberOfBuffers</code> 来配置。（阅读<a href="http://wuchong.me/blog/2016/04/26/flink-internals-how-to-handle-backpressure/#网络传输中的内存管理">这篇文章</a>了解更多Network Buffer的管理）</li><li><strong>Memory Manager Pool:</strong> 这是一个由 <code>MemoryManager</code> 管理的，由众多<code>MemorySegment</code>组成的超大集合。Flink 中的算法（如 sort/shuffle/join）会向这个内存池申请 MemorySegment，将序列化后的数据存于其中，使用完后释放回内存池。默认情况下，池子占了堆内存的 70% 的大小。</li><li><strong>Remaining (Free) Heap:</strong> 这部分的内存是留给用户代码以及 TaskManager 的数据结构使用的。因为这些数据结构一般都很小，所以基本上这些内存都是给用户代码使用的。从GC的角度来看，可以把这里看成的新生代，也就是说这里主要都是由用户代码生成的短期对象。</li></ul><p><strong><em>注意：Memory Manager Pool 主要在Batch模式下使用。在Steaming模式下，该池子不会预分配内存，也不会向该池子请求内存块。也就是说该部分的内存都是可以给用户代码使用的。不过社区是打算在 Streaming 模式下也能将该池子利用起来。</em></strong></p><p>Flink 采用类似 DBMS 的 sort 和 join 算法，直接操作二进制数据，从而使序列化/反序列化带来的开销达到最小。所以 Flink 的内部实现更像 C/C++ 而非 Java。如果需要处理的数据超出了内存限制，则会将部分数据存储到硬盘上。如果要操作多块MemorySegment就像操作一块大的连续内存一样，Flink会使用逻辑视图（<code>AbstractPagedInputView</code>）来方便操作。下图描述了 Flink 如何存储序列化后的数据到内存块中，以及在需要的时候如何将数据存储到磁盘上。</p><p>从上面我们能够得出 Flink 积极的内存管理以及直接操作二进制数据有以下几点好处：</p><ol><li><strong>减少GC压力。</strong>显而易见，因为所有常驻型数据都以二进制的形式存在 Flink 的<code>MemoryManager</code>中，这些<code>MemorySegment</code>一直呆在老年代而不会被GC回收。其他的数据对象基本上是由用户代码生成的短生命周期对象，这部分对象可以被 Minor GC 快速回收。只要用户不去创建大量类似缓存的常驻型对象，那么老年代的大小是不会变的，Major GC也就永远不会发生。从而有效地降低了垃圾回收的压力。另外，这里的内存块还可以是堆外内存，这可以使得 JVM 内存更小，从而加速垃圾回收。</li><li><strong>避免了OOM。</strong>所有的运行时数据结构和算法只能通过内存池申请内存，保证了其使用的内存大小是固定的，不会因为运行时数据结构和算法而发生OOM。在内存吃紧的情况下，算法（sort/join等）会高效地将一大批内存块写到磁盘，之后再读回来。因此，<code>OutOfMemoryErrors</code>可以有效地被避免。</li><li><strong>节省内存空间。</strong>Java 对象在存储上有很多额外的消耗（如上一节所谈）。如果只存储实际数据的二进制内容，就可以避免这部分消耗。</li><li><strong>高效的二进制操作 &amp; 缓存友好的计算。</strong>二进制数据以定义好的格式存储，可以高效地比较与操作。另外，该二进制形式可以把相关的值，以及hash值，键值和指针等相邻地放进内存中。这使得数据结构可以对高速缓存更友好，可以从 L1/L2/L3 缓存获得性能的提升（下文会详细解释）。</li></ol><h2 id="为-Flink-量身定制的序列化框架"><a href="#为-Flink-量身定制的序列化框架" class="headerlink" title="为 Flink 量身定制的序列化框架"></a>为 Flink 量身定制的序列化框架</h2><p>目前 Java 生态圈提供了众多的序列化框架：Java serialization, Kryo, Apache Avro 等等。但是 Flink 实现了自己的序列化框架。因为在 Flink 中处理的数据流通常是同一类型，由于数据集对象的类型固定，对于数据集可以只保存一份对象Schema信息，节省大量的存储空间。同时，对于固定大小的类型，也可通过固定的偏移位置存取。当我们需要访问某个对象成员变量的时候，通过定制的序列化工具，并不需要反序列化整个Java对象，而是可以直接通过偏移量，只是反序列化特定的对象成员变量。如果对象的成员变量较多时，能够大大减少Java对象的创建开销，以及内存数据的拷贝大小。</p><p>Flink支持任意的Java或是Scala类型。Flink 在数据类型上有很大的进步，不需要实现一个特定的接口（像Hadoop中的<code>org.apache.hadoop.io.Writable</code>），Flink 能够自动识别数据类型。Flink 通过 Java Reflection 框架分析基于 Java 的 Flink 程序 UDF (User Define Function)的返回类型的类型信息，通过 Scala Compiler 分析基于 Scala 的 Flink 程序 UDF 的返回类型的类型信息。类型信息由 <code>TypeInformation</code> 类表示，TypeInformation 支持以下几种类型：</p><ul><li><code>BasicTypeInfo</code>: 任意Java 基本类型（装箱的）或 String 类型。</li><li><code>BasicArrayTypeInfo</code>: 任意Java基本类型数组（装箱的）或 String 数组。</li><li><code>WritableTypeInfo</code>: 任意 Hadoop Writable 接口的实现类。</li><li><code>TupleTypeInfo</code>: 任意的 Flink Tuple 类型(支持Tuple1 to Tuple25)。Flink tuples 是固定长度固定类型的Java Tuple实现。</li><li><code>CaseClassTypeInfo</code>: 任意的 Scala CaseClass(包括 Scala tuples)。</li><li><code>PojoTypeInfo</code>: 任意的 POJO (Java or Scala)，例如，Java对象的所有成员变量，要么是 public 修饰符定义，要么有 getter/setter 方法。</li><li><code>GenericTypeInfo</code>: 任意无法匹配之前几种类型的类。</li></ul><p>前六种数据类型基本上可以满足绝大部分的Flink程序，针对前六种类型数据集，Flink皆可以自动生成对应的TypeSerializer，能非常高效地对数据集进行序列化和反序列化。对于最后一种数据类型，Flink会使用Kryo进行序列化和反序列化。每个TypeInformation中，都包含了serializer，类型会自动通过serializer进行序列化，然后用Java Unsafe接口写入MemorySegments。对于可以用作key的数据类型，Flink还同时自动生成TypeComparator，用来辅助直接对序列化后的二进制数据进行compare、hash等操作。对于 Tuple、CaseClass、POJO 等组合类型，其TypeSerializer和TypeComparator也是组合的，序列化和比较时会委托给对应的serializers和comparators。如下图展示 一个内嵌型的Tuple3&lt;Integer,Double,Person&gt; 对象的序列化过程。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1lvdbJFXXXXa9XVXXXXXXXXXX" alt></p><p>可以看出这种序列化方式存储密度是相当紧凑的。其中 int 占4字节，double 占8字节，POJO多个一个字节的header，PojoSerializer只负责将header序列化进去，并委托每个字段对应的serializer对字段进行序列化。</p><p>Flink 的类型系统可以很轻松地扩展出自定义的TypeInformation、Serializer以及Comparator，来提升数据类型在序列化和比较时的性能。</p><h2 id="Flink-如何直接操作二进制数据"><a href="#Flink-如何直接操作二进制数据" class="headerlink" title="Flink 如何直接操作二进制数据"></a>Flink 如何直接操作二进制数据</h2><p>Flink 提供了如 group、sort、join 等操作，这些操作都需要访问海量数据。这里，我们以sort为例，这是一个在 Flink 中使用非常频繁的操作。</p><p>首先，Flink 会从 MemoryManager 中申请一批 MemorySegment，我们把这批 MemorySegment 称作 sort buffer，用来存放排序的数据。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1_hhgJFXXXXc2XFXXXXXXXXXX" alt></p><p>我们会把 sort buffer 分成两块区域。一个区域是用来存放所有对象完整的二进制数据。另一个区域用来存放指向完整二进制数据的指针以及定长的序列化后的key（key+pointer）。如果需要序列化的key是个变长类型，如String，则会取其前缀序列化。如上图所示，当一个对象要加到 sort buffer 中时，它的二进制数据会被加到第一个区域，指针（可能还有key）会被加到第二个区域。</p><p>将实际的数据和指针加定长key分开存放有两个目的。第一，交换定长块（key+pointer）更高效，不用交换真实的数据也不用移动其他key和pointer。第二，这样做是缓存友好的，因为key都是连续存储在内存中的，可以大大减少 cache miss（后面会详细解释）。</p><p>排序的关键是比大小和交换。Flink 中，会先用 key 比大小，这样就可以直接用二进制的key比较而不需要反序列化出整个对象。因为key是定长的，所以如果key相同（或者没有提供二进制key），那就必须将真实的二进制数据反序列化出来，然后再做比较。之后，只需要交换key+pointer就可以达到排序的效果，真实的数据不用移动。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1f6BnJFXXXXbnXFXXXXXXXXXX" alt></p><p>最后，访问排序后的数据，可以沿着排好序的key+pointer区域顺序访问，通过pointer找到对应的真实数据，并写到内存或外部（更多细节可以看这篇文章 <a href="http://flink.apache.org/news/2015/03/13/peeking-into-Apache-Flinks-Engine-Room.html" target="_blank" rel="noopener">Joins in Flink</a>）。</p><h2 id="缓存友好的数据结构和算法"><a href="#缓存友好的数据结构和算法" class="headerlink" title="缓存友好的数据结构和算法"></a>缓存友好的数据结构和算法</h2><p>随着磁盘IO和网络IO越来越快，CPU逐渐成为了大数据领域的瓶颈。从 L1/L2/L3 缓存读取数据的速度比从主内存读取数据的速度快好几个量级。通过性能分析可以发现，CPU时间中的很大一部分都是浪费在等待数据从主内存过来上。如果这些数据可以从 L1/L2/L3 缓存过来，那么这些等待时间可以极大地降低，并且所有的算法会因此而受益。</p><p>在上面讨论中我们谈到的，Flink 通过定制的序列化框架将算法中需要操作的数据（如sort中的key）连续存储，而完整数据存储在其他地方。因为对于完整的数据来说，key+pointer更容易装进缓存，这大大提高了缓存命中率，从而提高了基础算法的效率。这对于上层应用是完全透明的，可以充分享受缓存友好带来的性能提升。</p><h2 id="走向堆外内存"><a href="#走向堆外内存" class="headerlink" title="走向堆外内存"></a>走向堆外内存</h2><p>Flink 基于堆内存的内存管理机制已经可以解决很多JVM现存问题了，为什么还要引入堆外内存？</p><ol><li>启动超大内存（上百GB）的JVM需要很长时间，GC停留时间也会很长（分钟级）。使用堆外内存的话，可以极大地减小堆内存（只需要分配Remaining Heap那一块），使得 TaskManager 扩展到上百GB内存不是问题。</li><li>高效的 IO 操作。堆外内存在写磁盘或网络传输时是 zero-copy，而堆内存的话，至少需要 copy 一次。</li><li>堆外内存是进程间共享的。也就是说，即使JVM进程崩溃也不会丢失数据。这可以用来做故障恢复（Flink暂时没有利用起这个，不过未来很可能会去做）。</li></ol><p>但是强大的东西总是会有其负面的一面，不然为何大家不都用堆外内存呢。</p><ol><li>堆内存的使用、监控、调试都要简单很多。堆外内存意味着更复杂更麻烦。</li><li>Flink 有时需要分配短生命周期的 <code>MemorySegment</code>，这个申请在堆上会更廉价。</li><li>有些操作在堆内存上会快一点点。</li></ol><p>Flink用通过<code>ByteBuffer.allocateDirect(numBytes)</code>来申请堆外内存，用 <code>sun.misc.Unsafe</code> 来操作堆外内存。</p><p>基于 Flink 优秀的设计，实现堆外内存是很方便的。Flink 将原来的 <code>MemorySegment</code> 变成了抽象类，并生成了两个子类。<code>HeapMemorySegment</code> 和 <code>HybridMemorySegment</code>。从字面意思上也很容易理解，前者是用来分配堆内存的，后者是用来分配堆外内存<strong>和堆内存</strong>的。是的，你没有看错，后者既可以分配堆外内存又可以分配堆内存。为什么要这样设计呢？</p><p>首先假设<code>HybridMemorySegment</code>只提供分配堆外内存。在上述堆外内存的不足中的第二点谈到，Flink 有时需要分配短生命周期的 buffer，这些buffer用<code>HeapMemorySegment</code>会更高效。那么当使用堆外内存时，为了也满足堆内存的需求，我们需要同时加载两个子类。这就涉及到了 JIT 编译优化的问题。因为以前 <code>MemorySegment</code> 是一个单独的 final 类，没有子类。JIT 编译时，所有要调用的方法都是确定的，所有的方法调用都可以被去虚化（de-virtualized）和内联（inlined），这可以极大地提高性能（MemroySegment的使用相当频繁）。然而如果同时加载两个子类，那么 JIT 编译器就只能在真正运行到的时候才知道是哪个子类，这样就无法提前做优化。实际测试的性能差距在 2.7 被左右。</p><p>Flink 使用了两种方案：</p><p><strong>方案1：只能有一种 MemorySegment 实现被加载</strong></p><p>代码中所有的短生命周期和长生命周期的MemorySegment都实例化其中一个子类，另一个子类根本没有实例化过（使用工厂模式来控制）。那么运行一段时间后，JIT 会意识到所有调用的方法都是确定的，然后会做优化。</p><p><strong>方案2：提供一种实现能同时处理堆内存和堆外内存</strong></p><p>这就是 <code>HybridMemorySegment</code> 了，能同时处理堆与堆外内存，这样就不需要子类了。这里 Flink 优雅地实现了一份代码能同时操作堆和堆外内存。这主要归功于 <code>sun.misc.Unsafe</code>提供的一系列方法，如getLong方法：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">sun.misc.Unsafe.getLong(Object reference, <span class="keyword">long</span> offset)</span><br></pre></td></tr></table></figure><ul><li>如果reference不为空，则会取该对象的地址，加上后面的offset，从相对地址处取出8字节并得到 long。这对应了堆内存的场景。</li><li>如果reference为空，则offset就是要操作的绝对地址，从该地址处取出数据。这对应了堆外内存的场景。</li></ul><p>这里我们看下 <code>MemorySegment</code> 及其子类的实现。</p><figure class="highlight axapta"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">MemorySegment</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 堆内存引用</span></span><br><span class="line">  <span class="keyword">protected</span> <span class="keyword">final</span> <span class="keyword">byte</span>[] heapMemory;</span><br><span class="line">  <span class="comment">// 堆外内存地址</span></span><br><span class="line">  <span class="keyword">protected</span> <span class="keyword">long</span> address;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">//堆内存的初始化</span></span><br><span class="line">  MemorySegment(<span class="keyword">byte</span>[] buffer, Object owner) &#123;</span><br><span class="line">    <span class="comment">//一些先验检查</span></span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">this</span>.heapMemory = buffer;</span><br><span class="line">    <span class="keyword">this</span>.address = BYTE_ARRAY_BASE_OFFSET;</span><br><span class="line">    ...</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//堆外内存的初始化</span></span><br><span class="line">  MemorySegment(<span class="keyword">long</span> offHeapAddress, <span class="keyword">int</span> size, Object owner) &#123;</span><br><span class="line">    <span class="comment">//一些先验检查</span></span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">this</span>.heapMemory = <span class="keyword">null</span>;</span><br><span class="line">    <span class="keyword">this</span>.address = offHeapAddress;</span><br><span class="line">    ...</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">long</span> getLong(<span class="keyword">int</span> <span class="keyword">index</span>) &#123;</span><br><span class="line">    <span class="keyword">final</span> <span class="keyword">long</span> pos = address + <span class="keyword">index</span>;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">index</span> &gt;= <span class="number">0</span> &amp;&amp; pos &lt;= addressLimit - <span class="number">8</span>) &#123;</span><br><span class="line">      <span class="comment">// 这是我们关注的地方，使用 Unsafe 来操作 on-heap &amp; off-heap</span></span><br><span class="line">      <span class="keyword">return</span> UNSAFE.getLong(heapMemory, pos);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (address &gt; addressLimit) &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"segment has been freed"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// index is in fact invalid</span></span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> IndexOutOfBoundsException();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">HeapMemorySegment</span> <span class="keyword">extends</span> <span class="title">MemorySegment</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 指向heapMemory的额外引用，用来如数组越界的检查</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">byte</span>[] memory;</span><br><span class="line">  <span class="comment">// 只能初始化堆内存</span></span><br><span class="line">  HeapMemorySegment(<span class="keyword">byte</span>[] memory, Object owner) &#123;</span><br><span class="line">    <span class="keyword">super</span>(Objects.requireNonNull(memory), owner);</span><br><span class="line">    <span class="keyword">this</span>.memory = memory;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">HybridMemorySegment</span> <span class="keyword">extends</span> <span class="title">MemorySegment</span> </span>&#123;</span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">final</span> ByteBuffer offHeapBuffer;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 堆外内存初始化</span></span><br><span class="line">  HybridMemorySegment(ByteBuffer buffer, Object owner) &#123;</span><br><span class="line">    <span class="keyword">super</span>(checkBufferAndGetAddress(buffer), buffer.capacity(), owner);</span><br><span class="line">    <span class="keyword">this</span>.offHeapBuffer = buffer;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 堆内存初始化</span></span><br><span class="line">  HybridMemorySegment(<span class="keyword">byte</span>[] buffer, Object owner) &#123;</span><br><span class="line">    <span class="keyword">super</span>(buffer, owner);</span><br><span class="line">    <span class="keyword">this</span>.offHeapBuffer = <span class="keyword">null</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以发现，HybridMemorySegment 中的很多方法其实都下沉到了父类去实现。包括堆内堆外内存的初始化。<code>MemorySegment</code> 中的 <code>getXXX</code>/<code>putXXX</code> 方法都是调用了 unsafe 方法，可以说<code>MemorySegment</code>已经具有了些 Hybrid 的意思了。<code>HeapMemorySegment</code>只调用了父类的<code>MemorySegment(byte[] buffer, Object owner)</code>方法，也就只能申请堆内存。另外，阅读代码你会发现，许多方法（大量的 getXXX/putXXX）都被标记成了 final，两个子类也是 final 类型，为的也是优化 JIT 编译器，会提醒 JIT 这个方法是可以被去虚化和内联的。</p><p>对于堆外内存，使用 <code>HybridMemorySegment</code> 能同时用来代表堆和堆外内存。这样只需要一个类就能代表长生命周期的堆外内存和短生命周期的堆内存。既然<code>HybridMemorySegment</code>已经这么全能，为什么还要方案1呢？因为我们需要工厂模式来保证只有一个子类被加载（为了更高的性能），而且HeapMemorySegment比heap模式的HybridMemorySegment要快。</p><p>下方是一些性能测试数据，更详细的数据请参考<a href="http://flink.apache.org/news/2015/09/16/off-heap-memory.html#appendix-detailed-micro-benchmarks" target="_blank" rel="noopener">这篇文章</a>。</p><table><thead><tr><th>Segment</th><th>Time</th></tr></thead><tbody><tr><td>HeapMemorySegment, exclusive</td><td>1,441 msecs</td></tr><tr><td>HeapMemorySegment, mixed</td><td>3,841 msecs</td></tr><tr><td>HybridMemorySegment, heap, exclusive</td><td>1,626 msecs</td></tr><tr><td>HybridMemorySegment, off-heap, exclusive</td><td>1,628 msecs</td></tr><tr><td>HybridMemorySegment, heap, mixed</td><td>3,848 msecs</td></tr><tr><td>HybridMemorySegment, off-heap, mixed</td><td>3,847 msecs</td></tr></tbody></table><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要总结了 Flink 面对 JVM 存在的问题，而在内存管理的道路上越走越深。从自己管理内存，到序列化框架，再到堆外内存。其实纵观大数据生态圈，其实会发现各个开源项目都有同样的趋势。比如最近炒的很火热的 Spark Tungsten 项目，与 Flink 在内存管理上的思想是及其相似的。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://flink.apache.org/news/2015/09/16/off-heap-memory.html" target="_blank" rel="noopener">Off-heap Memory in Apache Flink and the curious JIT compiler</a></li><li><a href="https://flink.apache.org/news/2015/05/11/Juggling-with-Bits-and-Bytes.html" target="_blank" rel="noopener">Juggling with Bits and Bytes</a></li><li><a href="https://flink.apache.org/news/2015/03/13/peeking-into-Apache-Flinks-Engine-Room.html" target="_blank" rel="noopener">Peeking into Apache Flink’s Engine Room</a></li><li><a href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=53741525" target="_blank" rel="noopener">Flink: Memory Management </a></li><li><a href="http://www.bigsynapse.com/addressing-big-data-performance" target="_blank" rel="noopener">Big Data Performance Engineering</a></li><li><a href="http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/" target="_blank" rel="noopener">sun.misc.misc.Unsafe usage for C style memory management</a></li><li><a href="http://howtodoinjava.com/core-java/related-concepts/usage-of-class-sun-misc-unsafe/" target="_blank" rel="noopener">sun.misc.misc.Unsafe usage for C style memory management - How to do it.</a></li><li><a href="http://www.javamex.com/tutorials/memory/object_memory_usage.shtml" target="_blank" rel="noopener">Memory usage of Java objects: general guide</a></li><li><a href="http://www.36dsj.com/archives/33650" target="_blank" rel="noopener">脱离JVM？ Hadoop生态圈的挣扎与演化</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;如今，大数据领域的开源框架（Hadoop，Spark，Storm）都使用的 JVM，当然也包括 Flink。基于 JVM 的数据分析引擎都需要面对将大量数据存到内存中，这就不得不面对 JVM 存在的几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Java 对象存储密度低。一个只包含 boolean 属性的对象占用了16个字节内存：对象头占了8个，boolean 属性占了1个，对齐填充占了7个。而实际上只需要一个bit（1/8字节）就够了。&lt;/li&gt;
&lt;li&gt;Full GC 会极大地影响性能，尤其是为了处理更大数据而开了很大内存空间的JVM来说，GC 会达到秒级甚至分钟级。&lt;/li&gt;
&lt;li&gt;OOM 问题影响稳定性。OutOfMemoryError是分布式计算框架经常会遇到的问题，当JVM中所有对象大小超过分配给JVM的内存大小时，就会发生OutOfMemoryError错误，导致JVM崩溃，分布式框架的健壮性和性能都会受到影响。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以目前，越来越多的大数据项目开始自己管理JVM内存了，像 Spark、Flink、HBase，为的就是获得像 C 一样的性能以及避免 OOM 的发生。本文将会讨论 Flink 是如何解决上面的问题的，主要内容包括内存管理、定制的序列化工具、缓存友好的数据结构和算法、堆外内存、JIT编译优化等。&lt;/p&gt;
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
      <category term="内存管理" scheme="http://wuchong.me/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>Flink 原理与实现：如何处理反压问题</title>
    <link href="http://wuchong.me/blog/2016/04/26/flink-internals-how-to-handle-backpressure/"/>
    <id>http://wuchong.me/blog/2016/04/26/flink-internals-how-to-handle-backpressure/</id>
    <published>2016-04-26T11:18:26.000Z</published>
    <updated>2022-08-03T06:46:44.503Z</updated>
    
    <content type="html"><![CDATA[<p>流处理系统需要能优雅地处理反压（backpressure）问题。反压通常产生于这样的场景：短时负载高峰导致系统接收数据的速率远高于它处理数据的速率。许多日常问题都会导致反压，例如，垃圾回收停顿可能会导致流入的数据快速堆积，或者遇到大促或秒杀活动导致流量陡增。反压如果不能得到正确的处理，可能会导致资源耗尽甚至系统崩溃。</p><p>目前主流的流处理系统 Storm/JStorm/Spark Streaming/Flink 都已经提供了反压机制，不过其实现各不相同。</p><p>Storm 是通过监控 Bolt 中的接收队列负载情况，如果超过高水位值就会将反压信息写到 Zookeeper ，Zookeeper 上的 watch 会通知该拓扑的所有 Worker 都进入反压状态，最后 Spout 停止发送 tuple。具体实现可以看这个 JIRA <a href="https://github.com/apache/storm/pull/700" target="_blank" rel="noopener">STORM-886</a>。</p><p>JStorm 认为直接停止 Spout 的发送太过暴力，存在大量问题。当下游出现阻塞时，上游停止发送，下游消除阻塞后，上游又开闸放水，过了一会儿，下游又阻塞，上游又限流，如此反复，整个数据流会一直处在一个颠簸状态。所以 JStorm 是通过逐级降速来进行反压的，效果会较 Storm 更为稳定，但算法也更复杂。另外 JStorm 没有引入 Zookeeper 而是通过 TopologyMaster 来协调拓扑进入反压状态，这降低了 Zookeeper 的负载。</p><a id="more"></a><h2 id="Flink-中的反压"><a href="#Flink-中的反压" class="headerlink" title="Flink 中的反压"></a>Flink 中的反压</h2><p>那么 Flink 是怎么处理反压的呢？答案非常简单：Flink 没有使用任何复杂的机制来解决反压问题，因为根本不需要那样的方案！它利用自身作为纯数据流引擎的优势来优雅地响应反压问题。下面我们会深入分析 Flink 是如何在 Task 之间传输数据的，以及数据流如何实现自然降速的。</p><p>Flink 在运行时主要由 <strong>operators</strong> 和 <strong>streams</strong> 两大组件构成。每个 operator 会消费中间态的流，并在流上进行转换，然后生成新的流。对于 Flink 的网络机制一种形象的类比是，Flink 使用了高效有界的分布式阻塞队列，就像 Java 通用的阻塞队列（BlockingQueue）一样。还记得经典的线程间通信案例：生产者消费者模型吗？使用 BlockingQueue 的话，一个较慢的接受者会降低发送者的发送速率，因为一旦队列满了（有界队列）发送者会被阻塞。Flink 解决反压的方案就是这种感觉。</p><p>在 Flink 中，这些分布式阻塞队列就是这些逻辑流，而队列容量是通过缓冲池（<code>LocalBufferPool</code>）来实现的。每个被生产和被消费的流都会被分配一个缓冲池。缓冲池管理着一组缓冲(<code>Buffer</code>)，缓冲在被消费后可以被回收循环利用。这很好理解：你从池子中拿走一个缓冲，填上数据，在数据消费完之后，又把缓冲还给池子，之后你可以再次使用它。</p><p>在解释 Flink 的反压原理之前，我们必须先对 Flink 中网络传输的内存管理有个了解。</p><h3 id="网络传输中的内存管理"><a href="#网络传输中的内存管理" class="headerlink" title="网络传输中的内存管理"></a>网络传输中的内存管理</h3><p>如下图所示展示了 Flink 在网络传输场景下的内存管理。网络上传输的数据会写到 Task 的 InputGate（IG） 中，经过 Task 的处理后，再由 Task 写到 ResultPartition（RS） 中。每个 Task 都包括了输入和输入，输入和输出的数据存在 <code>Buffer</code> 中（都是字节数据）。Buffer 是 MemorySegment 的包装类。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB14fLsHVXXXXXWXFXXXXXXXXXX" alt></p><ol><li><p>TaskManager（TM）在启动时，会先初始化<code>NetworkEnvironment</code>对象，TM 中所有与网络相关的东西都由该类来管理（如 Netty 连接），其中就包括<code>NetworkBufferPool</code>。根据配置，Flink 会在 NetworkBufferPool 中生成一定数量（默认2048）的内存块 MemorySegment（关于 Flink 的内存管理，<a href="http://wuchong.me/blog/2016/04/29/flink-internals-memory-manage/">后续文章</a>会详细谈到），内存块的总数量就代表了网络传输中所有可用的内存。NetworkEnvironment 和 NetworkBufferPool 是 Task 之间共享的，每个 TM 只会实例化一个。</p></li><li><p>Task 线程启动时，会向 NetworkEnvironment 注册，NetworkEnvironment 会为 Task 的 InputGate（IG）和 ResultPartition（RP） 分别创建一个 LocalBufferPool（缓冲池）并设置可申请的 MemorySegment（内存块）数量。IG 对应的缓冲池初始的内存块数量与 IG 中 InputChannel 数量一致，RP 对应的缓冲池初始的内存块数量与 RP 中的 ResultSubpartition 数量一致。不过，每当创建或销毁缓冲池时，NetworkBufferPool 会计算剩余空闲的内存块数量，并平均分配给已创建的缓冲池。注意，这个过程只是指定了缓冲池所能使用的内存块数量，并没有真正分配内存块，只有当需要时才分配。为什么要动态地为缓冲池扩容呢？因为内存越多，意味着系统可以更轻松地应对瞬时压力（如GC），不会频繁地进入反压状态，所以我们要利用起那部分闲置的内存块。</p></li><li><p>在 Task 线程执行过程中，当 Netty 接收端收到数据时，为了将 Netty 中的数据拷贝到 Task 中，InputChannel（实际是 RemoteInputChannel）会向其对应的缓冲池申请内存块（上图中的①）。如果缓冲池中也没有可用的内存块且已申请的数量还没到池子上限，则会向 NetworkBufferPool 申请内存块（上图中的②）并交给 InputChannel 填上数据（上图中的③和④）。如果缓冲池已申请的数量达到上限了呢？或者 NetworkBufferPool 也没有可用内存块了呢？这时候，Task 的 Netty Channel 会暂停读取，上游的发送端会立即响应停止发送，拓扑会进入反压状态。当 Task 线程写数据到 ResultPartition 时，也会向缓冲池请求内存块，如果没有可用内存块时，会阻塞在请求内存块的地方，达到暂停写入的目的。</p></li><li><p>当一个内存块被消费完成之后（在输入端是指内存块中的字节被反序列化成对象了，在输出端是指内存块中的字节写入到 Netty Channel 了），会调用 <code>Buffer.recycle()</code> 方法，会将内存块还给 LocalBufferPool （上图中的⑤）。如果LocalBufferPool中当前申请的数量超过了池子容量（由于上文提到的动态容量，由于新注册的 Task 导致该池子容量变小），则LocalBufferPool会将该内存块回收给 NetworkBufferPool（上图中的⑥）。如果没超过池子容量，则会继续留在池子中，减少反复申请的开销。</p></li></ol><h3 id="反压的过程"><a href="#反压的过程" class="headerlink" title="反压的过程"></a>反压的过程</h3><p>下面这张图简单展示了两个 Task 之间的数据传输以及 Flink 如何感知到反压的：</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1rCIvJpXXXXcKXXXXXXXXXXXX" alt></p><ol><li>记录“A”进入了 Flink 并且被 Task 1 处理。（这里省略了 Netty 接收、反序列化等过程）</li><li>记录被序列化到 buffer 中。</li><li>该 buffer 被发送到 Task 2，然后 Task 2 从这个 buffer 中读出记录。</li></ol><p><strong>不要忘了：记录能被 Flink 处理的前提是，必须有空闲可用的 Buffer。</strong></p><p>结合上面两张图看：Task 1 在输出端有一个相关联的 LocalBufferPool（称缓冲池1），Task 2 在输入端也有一个相关联的 LocalBufferPool（称缓冲池2）。如果缓冲池1中有空闲可用的 buffer 来序列化记录 “A”，我们就序列化并发送该 buffer。</p><p>这里我们需要注意两个场景：</p><ul><li>本地传输：如果 Task 1 和 Task 2 运行在同一个 worker 节点（TaskManager），该 buffer 可以直接交给下一个 Task。一旦 Task 2 消费了该 buffer，则该 buffer 会被缓冲池1回收。如果 Task 2 的速度比 1 慢，那么 buffer 回收的速度就会赶不上 Task 1 取 buffer 的速度，导致缓冲池1无可用的 buffer，Task 1 等待在可用的 buffer 上。最终形成 Task 1 的降速。</li><li>远程传输：如果 Task 1 和 Task 2 运行在不同的 worker 节点上，那么 buffer 会在发送到网络（TCP Channel）后被回收。在接收端，会从 LocalBufferPool 中申请 buffer，然后拷贝网络中的数据到 buffer 中。如果没有可用的 buffer，会停止从 TCP 连接中读取数据。在输出端，通过 Netty 的水位值机制来保证不往网络中写入太多数据（后面会说）。如果网络中的数据（Netty输出缓冲中的字节数）超过了高水位值，我们会等到其降到低水位值以下才继续写入数据。这保证了网络中不会有太多的数据。如果接收端停止消费网络中的数据（由于接收端缓冲池没有可用 buffer），网络中的缓冲数据就会堆积，那么发送端也会暂停发送。另外，这会使得发送端的缓冲池得不到回收，writer 阻塞在向 LocalBufferPool  请求 buffer，阻塞了 writer 往 ResultSubPartition 写数据。</li></ul><p>这种固定大小缓冲池就像阻塞队列一样，保证了 Flink 有一套健壮的反压机制，使得 Task 生产数据的速度不会快于消费的速度。我们上面描述的这个方案可以从两个 Task 之间的数据传输自然地扩展到更复杂的 pipeline 中，保证反压机制可以扩散到整个 pipeline。</p><h3 id="Netty-水位值机制"><a href="#Netty-水位值机制" class="headerlink" title="Netty 水位值机制"></a>Netty 水位值机制</h3><p>下方的代码是初始化 NettyServer 时配置的水位值参数。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 默认高水位值为2个buffer大小, 当接收端消费速度跟不上，发送端会立即感知到</span></span><br><span class="line">bootstrap.childOption(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, config.getMemorySegmentSize() + <span class="number">1</span>);</span><br><span class="line">bootstrap.childOption(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, <span class="number">2</span> * config.getMemorySegmentSize());</span><br></pre></td></tr></table></figure><p>当输出缓冲中的字节数超过了高水位值, 则 Channel.isWritable() 会返回false。当输出缓存中的字节数又掉到了低水位值以下, 则 Channel.isWritable() 会重新返回true。Flink 中发送数据的核心代码在 <code>PartitionRequestQueue</code> 中，该类是 server channel pipeline 的最后一层。发送数据关键代码如下所示。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">writeAndFlushNextMessageIfPossible</span><span class="params">(<span class="keyword">final</span> Channel channel)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (fatalError) &#123;</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  Buffer buffer = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="comment">// channel.isWritable() 配合 WRITE_BUFFER_LOW_WATER_MARK </span></span><br><span class="line">    <span class="comment">// 和 WRITE_BUFFER_HIGH_WATER_MARK 实现发送端的流量控制</span></span><br><span class="line">    <span class="keyword">if</span> (channel.isWritable()) &#123;</span><br><span class="line">      <span class="comment">// 注意: 一个while循环也就最多只发送一个BufferResponse, 连续发送BufferResponse是通过writeListener回调实现的</span></span><br><span class="line">      <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (currentPartitionQueue == <span class="keyword">null</span> &amp;&amp; (currentPartitionQueue = queue.poll()) == <span class="keyword">null</span>) &#123;</span><br><span class="line">          <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        buffer = currentPartitionQueue.getNextBuffer();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (buffer == <span class="keyword">null</span>) &#123;</span><br><span class="line">          <span class="comment">// 跳过这部分代码</span></span><br><span class="line">          ...</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> &#123;</span><br><span class="line">          <span class="comment">// 构造一个response返回给客户端</span></span><br><span class="line">          BufferResponse resp = <span class="keyword">new</span> BufferResponse(buffer, currentPartitionQueue.getSequenceNumber(), currentPartitionQueue.getReceiverId());</span><br><span class="line"></span><br><span class="line">          <span class="keyword">if</span> (!buffer.isBuffer() &amp;&amp;</span><br><span class="line">              EventSerializer.fromBuffer(buffer, getClass().getClassLoader()).getClass() == EndOfPartitionEvent<span class="class">.<span class="keyword">class</span>) </span>&#123;</span><br><span class="line">            <span class="comment">// 跳过这部分代码。batch 模式中 subpartition 的数据准备就绪，通知下游消费者。</span></span><br><span class="line">            ...</span><br><span class="line">          &#125;</span><br><span class="line"></span><br><span class="line">          <span class="comment">// 将该response发到netty channel, 当写成功后, </span></span><br><span class="line">          <span class="comment">// 通过注册的writeListener又会回调进来, 从而不断地消费 queue 中的请求</span></span><br><span class="line">          channel.writeAndFlush(resp).addListener(writeListener);</span><br><span class="line"></span><br><span class="line">          <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">catch</span> (Throwable t) &#123;</span><br><span class="line">    <span class="keyword">if</span> (buffer != <span class="keyword">null</span>) &#123;</span><br><span class="line">      buffer.recycle();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> IOException(t.getMessage(), t);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 当水位值降下来后（channel 再次可写），会重新触发发送函数</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">channelWritabilityChanged</span><span class="params">(ChannelHandlerContext ctx)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">  writeAndFlushNextMessageIfPossible(ctx.channel());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>核心发送方法中如果channel不可写，则会跳过发送。当channel再次可写后，Netty 会调用该Handle的 <code>channelWritabilityChanged</code> 方法，从而重新触发发送函数。</p><h3 id="反压实验"><a href="#反压实验" class="headerlink" title="反压实验"></a>反压实验</h3><p>另外，<a href="http://data-artisans.com/how-flink-handles-backpressure/" target="_blank" rel="noopener">官方博客</a>中为了展示反压的效果，给出了一个简单的实验。下面这张图显示了：随着时间的改变，生产者（黄色线）和消费者（绿色线）每5秒的平均吞吐与最大吞吐（在单一JVM中每秒达到8百万条记录）的百分比。我们通过衡量task每5秒钟处理的记录数来衡量平均吞吐。该实验运行在单 JVM 中，不过使用了完整的 Flink 功能栈。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1geTaHVXXXXcXXVXXXXXXXXXX" alt></p><p>首先，我们运行生产task到它最大生产速度的60%（我们通过Thread.sleep()来模拟降速）。消费者以同样的速度处理数据。然后，我们将消费task的速度降至其最高速度的30%。你就会看到背压问题产生了，正如我们所见，生产者的速度也自然降至其最高速度的30%。接着，停止消费task的人为降速，之后生产者和消费者task都达到了其最大的吞吐。接下来，我们再次将消费者的速度降至30%，pipeline给出了立即响应：生产者的速度也被自动降至30%。最后，我们再次停止限速，两个task也再次恢复100%的速度。总而言之，我们可以看到：生产者和消费者在 pipeline 中的处理都在跟随彼此的吞吐而进行适当的调整，这就是我们希望看到的反压的效果。</p><h2 id="反压监控"><a href="#反压监控" class="headerlink" title="反压监控"></a>反压监控</h2><p>在 Storm/JStorm 中，只要监控到队列满了，就可以记录下拓扑进入反压了。但是 Flink 的反压太过于天然了，导致我们无法简单地通过监控队列来监控反压状态。Flink 在这里使用了一个 trick 来实现对反压的监控。如果一个 Task 因为反压而降速了，那么它会卡在向 <code>LocalBufferPool</code> 申请内存块上。那么这时候，该 Task 的 stack trace 就会长下面这样：</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">java</span><span class="selector-class">.lang</span><span class="selector-class">.Object</span><span class="selector-class">.wait</span>(<span class="selector-tag">Native</span> <span class="selector-tag">Method</span>)</span><br><span class="line"><span class="selector-tag">o</span><span class="selector-class">.a</span><span class="selector-class">.f</span>.<span class="selector-attr">[...]</span><span class="selector-class">.LocalBufferPool</span><span class="selector-class">.requestBuffer</span>(<span class="selector-tag">LocalBufferPool</span><span class="selector-class">.java</span><span class="selector-pseudo">:163)</span></span><br><span class="line"><span class="selector-tag">o</span><span class="selector-class">.a</span><span class="selector-class">.f</span>.<span class="selector-attr">[...]</span><span class="selector-class">.LocalBufferPool</span><span class="selector-class">.requestBufferBlocking</span>(<span class="selector-tag">LocalBufferPool</span><span class="selector-class">.java</span><span class="selector-pseudo">:133)</span> &lt;<span class="selector-tag">---</span> <span class="selector-tag">BLOCKING</span> <span class="selector-tag">request</span></span><br><span class="line"><span class="selector-attr">[...]</span></span><br></pre></td></tr></table></figure><p>那么事情就简单了。通过不断地采样每个 task 的 stack trace 就可以实现反压监控。</p><p><img src="http://img3.tbcdn.cn/5476e8b07b923/TB1T3cJJpXXXXXLXpXXXXXXXXXX" alt></p><p>Flink 的实现中，只有当 Web 页面切换到某个 Job 的 Backpressure 页面，才会对这个 Job 触发反压检测，因为反压检测还是挺昂贵的。JobManager 会通过 Akka 给每个 TaskManager 发送<code>TriggerStackTraceSample</code>消息。默认情况下，TaskManager 会触发100次 stack trace 采样，每次间隔 50ms（也就是说一次反压检测至少要等待5秒钟）。并将这 100 次采样的结果返回给 JobManager，由 JobManager 来计算反压比率（反压出现的次数/采样的次数），最终展现在 UI 上。UI 刷新的默认周期是一分钟，目的是不对 TaskManager 造成太大的负担。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Flink 不需要一种特殊的机制来处理反压，因为 Flink 中的数据传输相当于已经提供了应对反压的机制。因此，Flink 所能获得的最大吞吐量由其 pipeline 中最慢的组件决定。相对于 Storm/JStorm 的实现，Flink 的实现更为简洁优雅，源码中也看不见与反压相关的代码，无需 Zookeeper/TopologyMaster 的参与也降低了系统的负载，也利于对反压更迅速的响应。</p>]]></content>
    
    <summary type="html">
    
      流处理系统需要能优雅地处理反压（backpressure）问题。反压通常产生于这样的场景：短时负载高峰导致系统接收数据的速率远高于它处理数据的速率。许多日常问题都会导致反压，例如，垃圾回收停顿可能会导致流入的数据快速堆积，或者遇到大促或秒杀活动导致流量陡增。反压如果不能得到正确的处理，可能会导致资源耗尽甚至系统崩溃。那么 Flink 是怎么处理反压的呢？
    
    </summary>
    
      <category term="Flink" scheme="http://wuchong.me/categories/Flink/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink原理与实现" scheme="http://wuchong.me/tags/Flink%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    
      <category term="反压" scheme="http://wuchong.me/tags/%E5%8F%8D%E5%8E%8B/"/>
    
  </entry>
  
  <entry>
    <title>Flink官方文档翻译：安装部署（集群模式）</title>
    <link href="http://wuchong.me/blog/2016/02/26/flink-docs-setup-cluster/"/>
    <id>http://wuchong.me/blog/2016/02/26/flink-docs-setup-cluster/</id>
    <published>2016-02-26T08:03:35.000Z</published>
    <updated>2022-08-03T06:46:44.502Z</updated>
    
    <content type="html"><![CDATA[<p>本文主要介绍如何将Flink以分布式模式运行在集群上（可能是异构的）。</p><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>Flink 运行在所有类 UNIX 环境上，例如 Linux、Mac OS X 和 Cygwin（对于Windows），而且要求集群由<strong>一个master节点</strong>和<strong>一个或多个worker节点</strong>组成。在安装系统之前，确保每台机器上都已经安装了下面的软件：</p><ul><li><strong>Java 1.7.x</strong>或更高版本</li><li><strong>ssh</strong>（Flink的脚本会用到sshd来管理远程组件）</li></ul><p>如果你的集群还没有完全装好这些软件，你需要安装/升级它们。例如，在 Ubuntu Linux 上， 你可以执行下面的命令安装 ssh 和 Java ：</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">sudo apt-<span class="builtin-name">get</span> install ssh </span><br><span class="line">sudo apt-<span class="builtin-name">get</span> install openjdk-7-jre</span><br></pre></td></tr></table></figure><h3 id="SSH免密码登录"><a href="#SSH免密码登录" class="headerlink" title="SSH免密码登录"></a>SSH免密码登录</h3><p>*译注：安装过Hadoop、Spark集群的用户应该对这段很熟悉，如果已经了解，可跳过。**</p><p>为了能够启动/停止远程主机上的进程，master节点需要能免密登录所有worker节点。最方便的方式就是使用ssh的公钥验证了。要安装公钥验证，首先以最终会运行Flink的用户登录master节点。<strong>所有的worker节点上也必须要有同样的用户（例如：使用相同用户名的用户）</strong>。本文会以 flink 用户为例。非常不建议使用 root 账户，这会有很多的安全问题。</p><p>当你用需要的用户登录了master节点，你就可以生成一对新的公钥/私钥。下面这段命令会在 ~/.ssh 目录下生成一对新的公钥/私钥。</p><figure class="highlight awk"><table><tr><td class="code"><pre><span class="line">ssh-keygen -b <span class="number">2048</span> -P <span class="string">''</span> -f ~<span class="regexp">/.ssh/i</span>d_rsa</span><br></pre></td></tr></table></figure><p>接下来，将公钥添加到用于认证的<code>authorized_keys</code>文件中：</p><figure class="highlight awk"><table><tr><td class="code"><pre><span class="line">cat ~<span class="regexp">/.ssh/i</span>d_rsa.pub &gt;&gt; ~<span class="regexp">/.ssh/</span>authorized_keys</span><br></pre></td></tr></table></figure><p>最后，将<code>authorized_keys</code>文件分发给集群中所有的worker节点，你可以重复地执行下面这段命令：</p><figure class="highlight elixir"><table><tr><td class="code"><pre><span class="line">scp ~<span class="regexp">/.ssh/authorized</span>_keys &lt;worker&gt;<span class="symbol">:~/</span>.ssh/</span><br></pre></td></tr></table></figure><p>将上面的<code>&lt;worker&gt;</code>替代成相应worker节点的IP/Hostname。完成了上述拷贝的工作，你应该就可以从master上免密登录其他机器了。</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line">ssh <span class="tag">&lt;<span class="name">worker</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="配置JAVA-HOME"><a href="#配置JAVA-HOME" class="headerlink" title="配置JAVA_HOME"></a>配置JAVA_HOME</h3><p>Flink 需要master和worker节点都配置了<code>JAVA_HOME</code>环境变量。有两种方式可以配置。</p><p>一种是，你可以在<code>conf/flink-conf.yaml</code>中设置<code>env.java.home</code>配置项为Java的安装路径。</p><p>另一种是，<code>sudo vi /etc/profile</code>，在其中添加<code>JAVA_HOME</code>：</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">JAVA_HOME</span>=/path/to/java_home/</span><br></pre></td></tr></table></figure><p>然后使环境变量生效，并验证 Java 是否安装成功</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">$ source /etc<span class="built_in">/profile </span>  #生效环境变量</span><br><span class="line">$ java -version         #如果打印出版本信息，则说明安装成功</span><br><span class="line">java version <span class="string">"1.7.0_75"</span></span><br><span class="line">Java(TM) SE Runtime Environment (build 1.7.0_75-b13)</span><br><span class="line">Java HotSpot(TM) 64-Bit<span class="built_in"> Server </span>VM (build 24.75-b04, mixed mode)</span><br></pre></td></tr></table></figure><h2 id="安装-Flink"><a href="#安装-Flink" class="headerlink" title="安装 Flink"></a>安装 Flink</h2><p>进入<a href="http://flink.apache.org/downloads.html" target="_blank" rel="noopener">下载页面</a>。请选择一个与你的Hadoop版本相匹配的Flink包。如果你不打算使用Hadoop，选择任何版本都可以。</p><p>在下载了最新的发布包后，拷贝到master节点上，并解压：</p><figure class="highlight jboss-cli"><table><tr><td class="code"><pre><span class="line">tar xzf flink-*<span class="string">.tgz</span></span><br><span class="line"><span class="keyword">cd</span> flink-*</span><br></pre></td></tr></table></figure><h2 id="配置-Flink"><a href="#配置-Flink" class="headerlink" title="配置 Flink"></a>配置 Flink</h2><p>在解压完之后，你需要编辑<code>conf/flink-conf.yaml</code>配置Flink。</p><p>设置<code>jobmanager.rpc.address</code>配置项为你的master节点地址。另外为了明确 JVM 在每个节点上所能分配的最大内存，我们需要配置<code>jobmanager.heap.mb</code>和<code>taskmanager.heap.mb</code>，值的单位是 MB。如果对于某些worker节点，你想要分配更多的内存给Flink系统，你可以在相应节点上设置<code>FLINK_TM_HEAP</code>环境变量来覆盖默认的配置。</p><p>最后，你需要提供一个集群中worker节点的列表。因此，就像配置HDFS，编辑<em>conf/slaves</em>文件，然后输入每个worker节点的 IP/Hostname。每一个worker结点之后都会运行一个 TaskManager。</p><p>每一条记录占一行，就像下面展示的一样：</p><figure class="highlight accesslog"><table><tr><td class="code"><pre><span class="line"><span class="number">192.168.0.100</span></span><br><span class="line"><span class="number">192.168.0.101</span></span><br><span class="line">.</span><br><span class="line">.</span><br><span class="line">.</span><br><span class="line"><span class="number">192.168.0.150</span></span><br></pre></td></tr></table></figure><p><em>译注：conf/master文件是用来做<a href="https://ci.apache.org/projects/flink/flink-docs-master/setup/jobmanager_high_availability.html" target="_blank" rel="noopener">JobManager HA</a>的，在这里不需要配置</em></p><p>每一个worker节点上的 Flink 路径必须一致。你可以使用共享的 NSF 目录，或者拷贝整个 Flink 目录到各个worker节点。</p><figure class="highlight elixir"><table><tr><td class="code"><pre><span class="line">scp -r /path/to/flink &lt;worker&gt;<span class="symbol">:/path/to/</span></span><br></pre></td></tr></table></figure><p>请查阅<a href="https://ci.apache.org/projects/flink/flink-docs-master/setup/config.html" target="_blank" rel="noopener">配置页面</a>了解更多关于Flink的配置。</p><p>特别的，这几个</p><ul><li>TaskManager 总共能使用的内存大小（<code>taskmanager.heap.mb</code>）</li><li>每一台机器上能使用的 CPU 个数（<code>taskmanager.numberOfTaskSlots</code>）</li><li>集群中的总 CPU 个数（<code>parallelism.default</code>）</li><li>临时目录（<code>taskmanager.tmp.dirs</code>）</li></ul><p>是非常重要的配置项。</p><h2 id="启动-Flink"><a href="#启动-Flink" class="headerlink" title="启动 Flink"></a>启动 Flink</h2><p>下面的脚本会在本地节点启动一个 JobManager，然后通过 SSH 连接所有的worker节点（<em>slaves</em>文件中所列的节点），并在每个节点上运行 TaskManager。现在你的 Flink 系统已经启动并运行了。跑在本地节点上的 JobManager 现在会在配置的 RPC 端口上监听并接收任务。</p><p>假定你在master节点上，并在Flink目录中：</p><figure class="highlight stata"><table><tr><td class="code"><pre><span class="line">bin/start-<span class="keyword">cluster</span>.<span class="keyword">sh</span></span><br></pre></td></tr></table></figure><p>要停止Flink，也有一个 <em>stop-cluster.sh</em> 脚本。</p><h2 id="添加-JobManager-TaskManager-实例到集群中"><a href="#添加-JobManager-TaskManager-实例到集群中" class="headerlink" title="添加 JobManager/TaskManager 实例到集群中"></a>添加 JobManager/TaskManager 实例到集群中</h2><p>你可以使用 <em>bin/jobmanager.sh</em> 和 <em>bin/taskmanager</em> 脚本来添加 JobManager 和 TaskManager 实例到你正在运行的集群中。</p><h3 id="添加一个-JobManager"><a href="#添加一个-JobManager" class="headerlink" title="添加一个 JobManager"></a>添加一个 JobManager</h3><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">bin/jobmanager.sh (<span class="keyword">start</span> cluster)|<span class="keyword">stop</span>|<span class="keyword">stop</span>-<span class="keyword">all</span></span><br></pre></td></tr></table></figure><h3 id="添加一个-TaskManager"><a href="#添加一个-TaskManager" class="headerlink" title="添加一个 TaskManager"></a>添加一个 TaskManager</h3><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">bin/taskmanager.sh <span class="keyword">start</span>|<span class="keyword">stop</span>|<span class="keyword">stop</span>-<span class="keyword">all</span></span><br></pre></td></tr></table></figure><p>确保你是在需要启动/停止相应实例的节点上运行的这些脚本。</p>]]></content>
    
    <summary type="html">
    
      本文主要介绍如何将Flink以分布式模式运行在集群上（可能是异构的）。
    
    </summary>
    
      <category term="分布式系统" scheme="http://wuchong.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink官方文档翻译" scheme="http://wuchong.me/tags/Flink%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E7%BF%BB%E8%AF%91/"/>
    
      <category term="部署" scheme="http://wuchong.me/tags/%E9%83%A8%E7%BD%B2/"/>
    
  </entry>
  
  <entry>
    <title>Flink官方文档翻译：安装部署（本地模式）</title>
    <link href="http://wuchong.me/blog/2016/02/26/flink-docs-setup-local/"/>
    <id>http://wuchong.me/blog/2016/02/26/flink-docs-setup-local/</id>
    <published>2016-02-26T08:03:23.000Z</published>
    <updated>2022-08-03T06:46:44.502Z</updated>
    
    <content type="html"><![CDATA[<p>本文主要介绍如何将Flink以本地模式运行在单机上。</p><h2 id="下载"><a href="#下载" class="headerlink" title="下载"></a>下载</h2><p>进入<a href="http://flink.apache.org/downloads.html" target="_blank" rel="noopener">下载页面</a>。如果你想让Flink与Hadoop进行交互（如HDFS或者HBase），请选择一个与你的Hadoop版本相匹配的Flink包。当你不确定或者只是想运行在本地文件系统上，请选择Hadoop 1.2.x对应的包。</p><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>Flink 可以运行在 Linux、Mac OS X 和 Windows 上。本地模式的安装唯一需要的只是 Java 1.7.x或更高版本。接下来的指南假定是类Unix环境，Windows用户请参考 <a href="#flink-on-windows">Flink on Windows</a>。</p><p>你可以执行下面的命令来查看是否已经正确安装了Java了。</p><figure class="highlight applescript"><table><tr><td class="code"><pre><span class="line">java -<span class="built_in">version</span></span><br></pre></td></tr></table></figure><p>这条命令会输出类似于下面的信息：</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">java version <span class="string">"1.8.0_51"</span></span><br><span class="line">Java(TM) SE Runtime Environment (build 1.8.0_51-b16)</span><br><span class="line">Java HotSpot(TM) 64-Bit<span class="built_in"> Server </span>VM (build 25.51-b03, mixed mode)</span><br></pre></td></tr></table></figure><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p><strong>对于本地模式，Flink是可以开箱即用的，你不用去更改任何的默认配置。</strong></p><p>开箱即用的配置会使用默认的Java环境。如果你想更改Java的运行环境，你可以手动地设置环境变量<code>JAVA_HOME</code>或者<code>conf/flink-conf.yaml</code>中的配置项<code>env.java.home</code>。你可以查阅<a href="https://ci.apache.org/projects/flink/flink-docs-master/setup/config.html" target="_blank" rel="noopener">配置页面</a>了解更多关于Flink的配置。</p><h2 id="启动Flink"><a href="#启动Flink" class="headerlink" title="启动Flink"></a>启动Flink</h2><p>你现在就可以开始运行Flink了。解压已经下载的压缩包，然后进入新创建的flink目录。在那里，你就可以本地模式运行Flink了：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="variable">$</span> tar xzf flink-*.tgz</span><br><span class="line"><span class="variable">$</span> cd flink-*</span><br><span class="line"><span class="variable">$</span> bin/<span class="built_in">start-local</span>.sh</span><br><span class="line">Starting job manager</span><br></pre></td></tr></table></figure><p>你可以通过观察logs目录下的日志文件来检查系统是否正在运行了：</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">$ tail log/flink-*-jobmanager-*.log</span><br><span class="line"><span class="builtin-name">INFO</span> <span class="built_in">..</span>. - Initializing memory manager with 409 megabytes of memory</span><br><span class="line"><span class="builtin-name">INFO</span> <span class="built_in">..</span>. - Trying <span class="keyword">to</span> load org.apache.flinknephele.jobmanager.scheduler.local.LocalScheduler as scheduler</span><br><span class="line"><span class="builtin-name">INFO</span> <span class="built_in">..</span>. - Setting up web <span class="builtin-name">info</span> server, using web-root directory <span class="built_in">..</span>.</span><br><span class="line"><span class="builtin-name">INFO</span> <span class="built_in">..</span>. - Web <span class="builtin-name">info</span><span class="built_in"> server </span>will display information about nephele job-manager on localhost,<span class="built_in"> port </span>8081.</span><br><span class="line"><span class="builtin-name">INFO</span> <span class="built_in">..</span>. - Starting web <span class="builtin-name">info</span><span class="built_in"> server </span><span class="keyword">for</span> JobManager on<span class="built_in"> port </span>8081</span><br></pre></td></tr></table></figure><p>JobManager 同时会在8081端口上启动一个web前端，你可以通过 <a href="http://localhost:8081" target="_blank" rel="noopener">http://localhost:8081</a> 来访问。</p><h2 id="Flink-on-Windows"><a href="#Flink-on-Windows" class="headerlink" title="Flink on Windows"></a>Flink on Windows</h2><p>如果你想要在 Windows 上运行 Flink，你需要如上文所述地下载、解压、配置 Flink 压缩包。之后，你可以使用使用 Windows 批处理文件（.bat文件）或者使用 <strong>Cygwin</strong> 运行 Flink 的 JobMnager。</p><h3 id="使用-Windows-批处理文件启动"><a href="#使用-Windows-批处理文件启动" class="headerlink" title="使用 Windows 批处理文件启动"></a>使用 Windows 批处理文件启动</h3><p>使用 Windows 批处理文件本地模式启动Flink，首先打开命令行窗口，进入 Flink 的 bin/ 目录，然后运行 start-local.bat 。</p><p>注意：Java运行环境必须已经加到了 Windows 的<code>%PATH%</code>环境变量中。按照<a href="http://www.java.com/en/download/help/path.xml" target="_blank" rel="noopener">本指南</a>添加 Java 到<code>%PATH%</code>环境变量中。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">$ cd flink</span><br><span class="line">$ cd bin</span><br><span class="line">$ <span class="keyword">start</span>-local.bat</span><br><span class="line"><span class="keyword">Starting</span> Flink job manager. Webinterface <span class="keyword">by</span> <span class="keyword">default</span> <span class="keyword">on</span> <span class="keyword">http</span>://localhost:<span class="number">8081</span>/.</span><br><span class="line"><span class="keyword">Do</span> <span class="keyword">not</span> <span class="keyword">close</span> this batch window. <span class="keyword">Stop</span> job manager <span class="keyword">by</span> pressing Ctrl+C.</span><br></pre></td></tr></table></figure><p>之后，你需要打开新的命令行窗口，并运行<code>flink.bat</code>。</p><h3 id="使用-Cygwin-和-Unix-脚本启动"><a href="#使用-Cygwin-和-Unix-脚本启动" class="headerlink" title="使用 Cygwin 和 Unix 脚本启动"></a>使用 Cygwin 和 Unix 脚本启动</h3><p>使用 Cygwin 你需要打开 Cygwin 的命令行，进入 Flink 目录，然后运行<code>start-local.sh</code>脚本：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="variable">$</span> cd flink</span><br><span class="line"><span class="variable">$</span> bin/<span class="built_in">start-local</span>.sh</span><br><span class="line">Starting Nephele job manager</span><br></pre></td></tr></table></figure><h3 id="从-Git-安装-Flink"><a href="#从-Git-安装-Flink" class="headerlink" title="从 Git 安装 Flink"></a>从 Git 安装 Flink</h3><p>如果你是从 git 安装的 Flink，而且使用的 Windows git shell，Cygwin会产生一个类似于下面的错误：</p><figure class="highlight livecodeserver"><table><tr><td class="code"><pre><span class="line">c:/flink/bin/<span class="built_in">start</span>-<span class="built_in">local</span>.sh: <span class="built_in">line</span> <span class="number">30</span>: $<span class="string">'\r'</span>: <span class="keyword">command</span> <span class="title">not</span> <span class="title">found</span></span><br></pre></td></tr></table></figure><p>这个错误的产生是因为 git 运行在 Windows 上时，会自动地将 UNIX 换行转换成 Windows 换行。问题是，Cygwin 只认 Unix 换行。解决方案是调整 Cygwin 配置来正确处理换行。步骤如下：</p><ol><li>打开 Cygwin 命令行</li><li><p>确定 home 目录，通过输入</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span>;<span class="built_in">pwd</span></span><br></pre></td></tr></table></figure></li></ol><p>  它会返回 Cygwin 根目录下的一个路径。</p><ol start="3"><li><p>在home目录下，使用 NotePad, WordPad 或者其他编辑器打开<code>.bash_profile</code>文件，然后添加如下内容到文件末尾：（如果文件不存在，你需要创建它）</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="builtin-name">export</span> SHELLOPTS</span><br><span class="line"><span class="builtin-name">set</span> -o igncr</span><br></pre></td></tr></table></figure></li></ol><p>  保存文件，然后打开一个新的bash窗口。</p>]]></content>
    
    <summary type="html">
    
      本文主要介绍如何将Flink以本地模式运行在单机上。
    
    </summary>
    
      <category term="分布式系统" scheme="http://wuchong.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
    
      <category term="Flink" scheme="http://wuchong.me/tags/Flink/"/>
    
      <category term="Flink官方文档翻译" scheme="http://wuchong.me/tags/Flink%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E7%BF%BB%E8%AF%91/"/>
    
      <category term="部署" scheme="http://wuchong.me/tags/%E9%83%A8%E7%BD%B2/"/>
    
  </entry>
  
  <entry>
    <title>迟到的2015年终总结</title>
    <link href="http://wuchong.me/blog/2016/01/10/2015-summary/"/>
    <id>http://wuchong.me/blog/2016/01/10/2015-summary/</id>
    <published>2016-01-10T14:55:02.000Z</published>
    <updated>2022-08-03T06:46:44.500Z</updated>
    
    <content type="html"><![CDATA[<p>这是一篇迟到了很久很久的2015年终总结……</p><p>这一年，是我人生的又一个转折点。因为，我终于走出了校园，迈进了社会。阿里是一座大学，让我很快就适应了这里的生活。尤其是园区的五个食堂可以每天都吃不同的食堂，午饭有补贴、晚饭免费、夜宵免费，害的我半年就胖了10斤…斤…</p><p>言归正传，今年主要就分为两个阶段，前半年是在校的毕业期，后半年是入职的菜鸟期。</p><a id="more"></a><h2 id="在校的最后半年"><a href="#在校的最后半年" class="headerlink" title="在校的最后半年"></a>在校的最后半年</h2><p>这半年主要是与毕设各种斗智斗勇以及和好友们胡吃海塞中度过。还有趁离开北京前，把还没去过的地方逛了下。</p><p>有时看着实验室群里学弟学妹们的聊天，他们也正在经历着我半年前的经历。正巧，收到了汤老师的来信，问我工作的近况。突然，好想念老师们，好想念在北京的兄弟们。所以计划今年在学弟们毕业前，回一趟学校见见老师同学们。</p><p>在毕业之后，我把shadowsocks免费服务和码农圈关掉了。经过半年的运营，码农圈的用户数已经将近2000了。可是非常遗憾，毕业之后没有精力去维护它们，犹豫了良久，还是关停了。</p><h2 id="入职的半年"><a href="#入职的半年" class="headerlink" title="入职的半年"></a>入职的半年</h2><p>很幸运地能进入阿里，很幸运地能进入牛人云集的中间件团队，更幸运的是我进入的团队做的正是我喜欢的东西（大数据、流处理、JStorm/Storm）。在这里，我遇到了我的中国好老板，中国好师兄。刚来到团队时，主要负责jstorm的监控系统和管控平台。作为一名后端开发，在中间件中前端能力居然成了我的强项，虽然我对前端也有兴趣，但我还是更想做核心后端。</p><p>所以，需要学的东西好多，Clojure、Netty、Thrift、ZooKeeper、高并发、Spark、Flink…有学不完的东西，每天都很充实，却也每天都觉得时间不够用。以前在学校，一周七天都能拿来学这些东西，现在工作忙了，只能等到下班后或是周末，然而周末又会有好多琐碎的事情。一周能坐下来持续学习的时间真的好少。不过我们团队的好处是，在工作不忙时可以在工作时间去学习、去阅读源码，Boss也支持，简直就是领着工资在学习。</p><p>这一年，团队发生的大事是 <a href="http://mp.weixin.qq.com/s?__biz=MzA4NjI4MzM4MQ==&amp;mid=403824466&amp;idx=1&amp;sn=cfbbe7c802f15b089a9e15dee2126f3d&amp;scene=2&amp;srcid=11190nGh0JBVclikaTSe3bqM&amp;from=timeline&amp;isappinstalled=0#wechat_redirect" target="_blank" rel="noopener">JStorm 进入了 Apache 基金会</a>。社区有一个JStorm Merge的规划，也就是Storm 2.0会基于JStorm将Clojure核心代码替换成Java。最近Storm马上就会发布1.0版本，之后会冻结核心相关的feature，开始全力做jstorm merge。希望届时，自己能为社区做尽量多的贡献。</p><h2 id="开源社区"><a href="#开源社区" class="headerlink" title="开源社区"></a>开源社区</h2><p>这一年，我在Github上开源的项目总共收到了450多个star。进入JStorm团队后，也有更多的机会活跃在JStorm社区和Storm社区。</p><p>这一年，开源社区对整个工业界的影响很大。开源社区对我的影响也很大。深刻地体会到，这是个卧虎藏龙的地方，这里有取之不尽的知识，这里就像大学的象牙塔一样引无数人攀登。</p><p>大型的开源项目有很多值得学习的东西，优秀的架构设计，漂亮整洁的代码，就像文档一样的代码注释，以及测试、code review、文档、流程规范等等。就算是BAT绝大多数团队都没有做到。所以，参与开源项目不仅是一件“镀金”的经验，更是能大幅提升自己的技术能力。</p><h2 id="2016年"><a href="#2016年" class="headerlink" title="2016年"></a>2016年</h2><p>这是我工作的第一年，起初刚毕业时心情比较浮躁，现在慢慢心沉下来了。目标清晰，只需低头拉车踏踏实实做研发。这些今年的TODO List。</p><ul><li>努力成为Storm Commiter</li><li>熟悉Storm、Spark、Flink等流处理框架的原理和代码</li><li>自己能从零写一个分布式系统，即时是玩具级别的或是重复造轮子</li><li>成为一名人像摄影狮，摄影后期入门</li><li>摇号中奖</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;这是一篇迟到了很久很久的2015年终总结……&lt;/p&gt;
&lt;p&gt;这一年，是我人生的又一个转折点。因为，我终于走出了校园，迈进了社会。阿里是一座大学，让我很快就适应了这里的生活。尤其是园区的五个食堂可以每天都吃不同的食堂，午饭有补贴、晚饭免费、夜宵免费，害的我半年就胖了10斤…斤…&lt;/p&gt;
&lt;p&gt;言归正传，今年主要就分为两个阶段，前半年是在校的毕业期，后半年是入职的菜鸟期。&lt;/p&gt;
    
    </summary>
    
      <category term="随笔生活" scheme="http://wuchong.me/categories/%E9%9A%8F%E7%AC%94%E7%94%9F%E6%B4%BB/"/>
    
    
      <category term="生活" scheme="http://wuchong.me/tags/%E7%94%9F%E6%B4%BB/"/>
    
  </entry>
  
  <entry>
    <title>高效Macbook开发之道(工具篇)</title>
    <link href="http://wuchong.me/blog/2015/11/21/macbook-productive-tools/"/>
    <id>http://wuchong.me/blog/2015/11/21/macbook-productive-tools/</id>
    <published>2015-11-21T13:09:06.000Z</published>
    <updated>2022-08-03T06:46:44.510Z</updated>
    
    <content type="html"><![CDATA[<p>程序员就像工匠，若想高效地编写出漂亮的代码，就得要有一把好”锤子”——好的开发工具。就像老罗提出匠心与情怀，程序员对于手中的工具也是饱含工匠情怀的。所以，本文就讲讲那些我用出情怀的高效工具们。</p><a id="more"></a><h2 id="Macbook"><a href="#Macbook" class="headerlink" title="Macbook"></a>Macbook</h2><p>毋庸置疑，首先你得有台Macbook，这是脱离鼠标提升效率的第一步。所以本文基本上都是推荐Mac上的工具。</p><h2 id="笔记-amp-编辑器"><a href="#笔记-amp-编辑器" class="headerlink" title="笔记&amp;编辑器"></a>笔记&amp;编辑器</h2><ul><li><p><strong><a href="http://zh.mweb.im/" target="_blank" rel="noopener">MWeb</a></strong></p><p>我的笔记需求很简单，1. 支持Markdown与预览 2. 支持笔记分类管理 3. 简洁美观。哦，要是能直接在Markdown中粘贴图片就更好了。MWeb是我目前用过这么多产品里唯一全符合这些要求的。已购。</p><p>EvenNote不支持Markdown，太重。Mou缺少文档管理。Cmd Markdown，离线版还有待改进。</p><p><img src="http://ww2.sinaimg.cn/large/81b78497gw1ey8g8xmqb9j212o0oljyv.jpg" alt></p></li><li><p><strong><a href="http://www.sublimetext.com/" target="_blank" rel="noopener">Sublime</a></strong></p><p>Sublime是一款具有丰富扩展功能的编辑器。作为前端开发者，完全可以用如此轻量的工具作为前端IDE。</p><p><img src="http://ww2.sinaimg.cn/large/81b78497gw1ey8g1n19emj20qt0hzn22.jpg" alt></p></li><li><p><strong><a href="https://atom.io/" target="_blank" rel="noopener">Atom</a></strong></p><p>Atom的推出就是要取代Sublime的。两者功能差不多，可以说Atom深受Sublime哲学的影响。Atom对于包管理更加方便，代码补全也是出色的功能之一。优秀的界面设计，让我这视觉动物忍不住就用上了。就是相对Sublime而言，做的有些重了。</p><p><img src="http://ww1.sinaimg.cn/large/81b78497gw1ey8g3ijl21j21bs0lo458.jpg" alt></p></li></ul><h2 id="开发工具"><a href="#开发工具" class="headerlink" title="开发工具"></a>开发工具</h2><ul><li><p><strong><a href="https://www.jetbrains.com/idea/" target="_blank" rel="noopener">IntelliJ IDEA</a></strong></p><p>Java IDE的不二之选。强大，强大，强大，记得一定要上Ultimate版，资金充足就付费，不充足就先用破解，记得靠IDEA赚到钱了得回来补上。用惯后会极大提高开发速度。重复代码自动检查、代码规范提示等功能还能帮你纠正编码规范。快捷键尽量用默认的，不要用Eclipse快捷键，虽然一开始会有点难以适应，但是用久了会发现爽的飞起。IDEA是可以为之单独写篇文章安利的产品，此处不再多说。另外Jetbrains家族的产品都很良心，RubyMine、Pycharm、WebStorm都是不错的IDE。</p><p><img src="http://ww3.sinaimg.cn/large/81b78497gw1ey8hba8qylj21kw119arz.jpg" alt></p></li><li><p><strong><a href="https://kapeli.com/dash" target="_blank" rel="noopener">Dash</a></strong></p><p>Dash是一个API文档浏览器，以及代码片段管理工具。作为一名程序员，每天必不可少的动作就是查各种API文档，为了搜一个函数打开好几个web窗口是很常见的事。Dash可以提高我们的效率，尤其是我为它绑定了<code>shift+space</code>的快捷键之后，在全屏IDE中我可以直接呼出dash查询想要的类/函数。已购。</p><p><img src="http://ww3.sinaimg.cn/large/81b78497gw1ey8hcd4mg7j20t40k80xv.jpg" alt></p></li><li><p><strong><a href="http://iterm2.com/" target="_blank" rel="noopener">iTerm 2</a></strong></p><p>自带的Terminal其实也还行，不过有很多理由让我们用iTerm 2。例如设置主题、各种快捷键、方便的复制查找。再配合上<a href="http://ohmyz.sh/" target="_blank" rel="noopener">Oh My Zsh</a> ，简直爽到爆！</p></li></ul><h2 id="绿色上网"><a href="#绿色上网" class="headerlink" title="绿色上网"></a>绿色上网</h2><ul><li><p><strong><a href="https://github.com/shadowsocks/shadowsocks-iOS/releases/" target="_blank" rel="noopener">ShadowSocks X</a></strong></p><p>Shadowsocks在Mac上的客户端。[蜡烛]</p></li><li><p><strong><a href="https://itunes.apple.com/us/app/surge-web-developer-tool-proxy/id1040100637?ls=1&amp;mt=8" target="_blank" rel="noopener">Surge.app</a></strong></p><p>iOS 9的一个神级API，以及给力的app开发者，终于带给iOS用户们一个安全、低成本、最大网络速度、无连接状态、国内外分流的完美解决方案。终于可以在碎片时间获取国外的最新资讯了。</p></li></ul><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><ul><li><p><strong><a href="http://www.eudic.net/eudic/mac_dictionary.aspx" target="_blank" rel="noopener">欧陆词典</a></strong></p><p>Mac自带的字典其实已经很方便了，三指轻按在阅读英文文档时非常方便，但不能满足查单词的需求。而Mac上的词典确实比较少，也就这款用的比较顺手，我绑定了<code>option+space</code>快捷键，可以轻松从顶部呼出搜索栏。</p></li><li><p><strong><a href="https://www.wunderlist.com/zh/" target="_blank" rel="noopener">奇妙清单</a></strong></p><p>奇妙清单是一款任务管理工具，可用于记事提醒、工作安排、待办清单、项目管理等工作，重点的是它免费且跨平台支持 iOS、Android、Windows、Mac、网页版等。虽然同类优秀的TODO产品众多，不过这款产品清一色的五星好评值得你拥有。目前我一直用它来管理工作、生活、学习上的事项，用的很顺。支持国产免费软件。</p></li><li><p><strong><a href="https://www.omnigroup.com/omnigraffle/" target="_blank" rel="noopener">OminiGraffle</a></strong></p><p>作为程序员，免不了要画些流程图什么的。OmniGraffle绝对是Mac上最好用的流程图软件，画出来的图颜值爆表。当然，这是收费的。</p></li><li><p><strong>Chrome插件</strong></p><p>关于Chrome插件我这里只推荐两个吧。一个是<a href="https://chrome.google.com/webstore/detail/%E5%9B%B4%E8%84%96%E6%98%AF%E4%B8%AA%E5%A5%BD%E5%9B%BE%E5%BA%8A/pngmcllbdfgmhdgnnpfaciaolgbjplhe" target="_blank" rel="noopener">围脖是个好图床</a>，可以方便的通过粘贴、拖拽将图片上传到新浪微博图床，并拿到链接。另一个是<a href="https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif" target="_blank" rel="noopener">Proxy SwitchyOmega</a>，SwitchySharp的升级版，搭配ss能代理工具使用。</p></li></ul>]]></content>
    
    <summary type="html">
    
      程序员就像工匠，若想高效地编写出漂亮的代码，就得要有一把好&quot;锤子&quot;——好的开发工具。就像老罗提出匠心与情怀，程序员对于手中的工具也是饱含工匠情怀的。所以，本文就讲讲那些我用出情怀的高效工具们。
    
    </summary>
    
      <category term="杂项资源" scheme="http://wuchong.me/categories/%E6%9D%82%E9%A1%B9%E8%B5%84%E6%BA%90/"/>
    
    
      <category term="工具" scheme="http://wuchong.me/tags/%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>Clojure学习笔记（三）：并发与引用</title>
    <link href="http://wuchong.me/blog/2015/11/06/learn-clojure-3-concurrent/"/>
    <id>http://wuchong.me/blog/2015/11/06/learn-clojure-3-concurrent/</id>
    <published>2015-11-06T06:45:03.000Z</published>
    <updated>2022-08-03T06:46:44.509Z</updated>
    
    <content type="html"><![CDATA[<p>很多人是为了更好地进行并发编程而选择了Clojure，但Clojure所有的数据都是只读的，除非你使用引用类型（Vars、Ref、Atom、Agent）来标明它们是可以修改的。Clojure处理并发的思路与众不同，采用的是<a href="http://en.wikipedia.org/wiki/Software_transactional_memory" target="_blank" rel="noopener">Software Transactional Memory</a> (STM)来实现的，即软事务内存。你可以将STM想象成数据库，只不过是内存型的，它只支持事务的ACI，也就是原子性、一致性、隔离性，但是不包括持久性，因为状态的保存都在内存里。引用类型是一种可变引用指向不可变数据的一种机制。Clojure的并发API分为四种模型：</p><ol><li>管理Thread local变量的Var。</li><li>管理协作式、同步修改可变状态的Ref</li><li>管理非协作式、同步修改可变状态的Atom</li><li>管理异步修改可变状态的Agent</li></ol><a id="more"></a><h2 id="Vars"><a href="#Vars" class="headerlink" title="Vars"></a>Vars</h2><p>Vars 是一种引用类型，它可以有一个被所有线程共享的root binding，并且每个线程还能拥有自己(thread-local)的值。</p><ul><li><p><strong>def</strong><br><code>def</code> 定义, 将会影响全局定义。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> v <span class="number">2</span>) <span class="comment">; -&gt; 2</span></span><br></pre></td></tr></table></figure></li><li><p><strong>let</strong><br><code>let</code> 定义, 将会影响自身生命周期内,以及自己作用域内,如果超出自己的作用域,则无效。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> name <span class="string">"jark"</span>)</span><br><span class="line">(<span class="name"><span class="builtin-name">let</span></span> [name <span class="string">"wuchong"</span>] </span><br><span class="line">  (<span class="name">println</span> name)) <span class="comment">; -&gt; 输出 wuchong</span></span><br><span class="line">(<span class="name">println</span> name) <span class="comment">; -&gt; 输出 jark</span></span><br></pre></td></tr></table></figure></li><li><p><strong>binding</strong><br><code>binding</code>, 将会影响自身生命周期以及自己作用域内，即使超出自身作用域,也都有效。</p><p>这个例子演示了binding和set!一起使用，用set!来修改一个由binding bind的Var的线程本地的值。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> ^<span class="symbol">:dynamic</span> v <span class="number">1</span>) <span class="comment">; 需要声明成"dynamic"，v才能用binding来改变值。</span></span><br><span class="line"></span><br><span class="line">(<span class="name"><span class="builtin-name">defn</span></span> change-it []</span><br><span class="line">  (<span class="name">println</span> <span class="string">"2) v ="</span> v) <span class="comment">; -&gt; 1</span></span><br><span class="line"></span><br><span class="line">  (<span class="name"><span class="builtin-name">def</span></span> v <span class="number">2</span>) <span class="comment">; changes root value</span></span><br><span class="line">  (<span class="name">println</span> <span class="string">"3) v ="</span> v) <span class="comment">; -&gt; 2</span></span><br><span class="line"></span><br><span class="line">  (<span class="name">binding</span> [v <span class="number">3</span>] <span class="comment">; binds a thread-local value</span></span><br><span class="line">    (<span class="name">println</span> <span class="string">"4) v ="</span> v) <span class="comment">; -&gt; 3</span></span><br><span class="line"></span><br><span class="line">    (<span class="name"><span class="builtin-name">set!</span></span> v <span class="number">4</span>) <span class="comment">; changes thread-local value</span></span><br><span class="line">    (<span class="name">println</span> <span class="string">"5) v ="</span> v)) <span class="comment">; -&gt; 4</span></span><br><span class="line"></span><br><span class="line">  (<span class="name">println</span> <span class="string">"6) v ="</span> v)) <span class="comment">; thread-local value is gone now -&gt; 2</span></span><br><span class="line"></span><br><span class="line">(<span class="name">println</span> <span class="string">"1) v ="</span> v) <span class="comment">; -&gt; 1</span></span><br><span class="line"></span><br><span class="line">(<span class="name"><span class="builtin-name">let</span></span> [thread (<span class="name">Thread.</span> #(<span class="name">change-it</span>))] <span class="comment">; 启动一个本地线程</span></span><br><span class="line">  (<span class="name">.start</span> thread)</span><br><span class="line">  (<span class="name">.join</span> thread)) <span class="comment">; 等待线程结束</span></span><br><span class="line"></span><br><span class="line">(<span class="name">println</span> <span class="string">"7) v ="</span> v) <span class="comment">; -&gt; 2</span></span><br></pre></td></tr></table></figure></li></ul><p>好吧，说了这么多，其实Clojure不鼓励我们使用Vars，因为Vars在线程间不能很好地协作。</p><h2 id="Refs"><a href="#Refs" class="headerlink" title="Refs"></a>Refs</h2><p>Refs是用来协调对于一个或者多个binding的并发修改的。</p><ul><li><p><strong>ref</strong></p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line"><span class="comment">; 用ref函数创建一个可变的引用(reference)，一个空的歌曲集合</span></span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> song (<span class="name"><span class="builtin-name">ref</span></span> #&#123;&#125;))</span><br><span class="line">(<span class="name">println</span> @song) <span class="comment">; -&gt; #&#123;&#125;  用@来读取ref值</span></span><br></pre></td></tr></table></figure></li><li><p><strong>validator</strong><br>类似数据库，可以为ref添加“约束”，在数据更新的时候需要通过validator函数的验证，如果验证不通过，整个事务将回滚。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> validate-song</span><br><span class="line">     (<span class="name"><span class="builtin-name">partial</span></span> every? #(<span class="name"><span class="builtin-name">not</span></span> (<span class="name"><span class="builtin-name">nil?</span></span> %)))) <span class="comment">; 定义了歌名不能为空的validator</span></span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> song (<span class="name"><span class="builtin-name">ref</span></span> #&#123;&#125; <span class="symbol">:validator</span> validate-song))</span><br></pre></td></tr></table></figure></li><li><p><strong>dosync &amp; ref-set</strong></p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line"><span class="comment">; 改变引用指向的内容，使用ref-set函数</span></span><br><span class="line">(<span class="name"><span class="builtin-name">ref-set</span></span> song #&#123;<span class="string">"Dangerous"</span>&#125;) </span><br><span class="line"><span class="comment">; -&gt; IllegalStateException: No transaction running</span></span><br><span class="line"><span class="comment">; 会报错，因为引用是可变的，对状态的更新需要用事务进行保护，使用dosync</span></span><br><span class="line">(<span class="name"><span class="builtin-name">dosync</span></span> (<span class="name"><span class="builtin-name">ref-set</span></span> song #&#123;<span class="string">"Dangerous"</span>&#125;))</span><br><span class="line"></span><br><span class="line"><span class="comment">; 因为我们加了不能为空的validator，加入空会报错</span></span><br><span class="line">(<span class="name"><span class="builtin-name">dosync</span></span> (<span class="name"><span class="builtin-name">ref-set</span></span> song #&#123;&#125;)) <span class="comment">; -&gt; IllegalStateException Invalid reference state</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; 可以对多个ref的更新放在一个事务里</span></span><br><span class="line">(<span class="name"><span class="builtin-name">dosync</span></span> (<span class="name"><span class="builtin-name">ref-set</span></span> song #&#123;<span class="string">"Dangerous"</span>&#125;)</span><br><span class="line">             (<span class="name"><span class="builtin-name">ref-set</span></span> singer #&#123;<span class="string">"MJ"</span>&#125;) )</span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>alter &amp; commute</strong><br>更改引用有点暴力也比较少见，更常见的更新是根据当前状态更新，比如加一首歌进去。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line"><span class="comment">; 先查询集合内容，然后往集合里添加歌曲，然后更新整个集合</span></span><br><span class="line">(<span class="name"><span class="builtin-name">dosync</span></span> (<span class="name"><span class="builtin-name">ref-set</span></span> song (<span class="name"><span class="builtin-name">conj</span></span> @song <span class="string">"heal the world"</span>)))</span><br><span class="line"></span><br><span class="line"><span class="comment">; 查询并更新的操作可以合成一步，这是通过alter函数实现</span></span><br><span class="line">(<span class="name"><span class="builtin-name">dosync</span></span> (<span class="name"><span class="builtin-name">alter</span></span> song conj <span class="string">"heal the world"</span>))</span><br><span class="line"></span><br><span class="line">(<span class="name">println</span> @song) <span class="comment">; -&gt; #&#123;heal the world&#125;</span></span><br></pre></td></tr></table></figure></li></ul><p>  注意alter后跟的函数会把ref值当做第一个参数，所以这里使用cons就不行了，因为cons要求第一个参数是加入的元素。</p><p>  commute函数是是对alter的优化，commute可以同时进行修改（并不影响ref最终的值）。通常情况下，一般优先使用alter，除非在遇到明显的性能瓶颈并且对顺序不是那么关心的时候，可以考虑用commute替换。</p><h2 id="Atoms"><a href="#Atoms" class="headerlink" title="Atoms"></a>Atoms</h2><p>Atoms 提供了比使用Refs&amp;STM更简单的更新当个值的方法。它不受事务的影响。有点像Java的原子类(Atomic)。</p><p>有三个函数可以修改一个Atom的值：<code>reset!</code>,<code>compare-and-set!</code> 和<code>swap!</code>。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> counter (<span class="name"><span class="builtin-name">atom</span></span> <span class="number">1</span>))  <span class="comment">; 指定 counter 为Atom类型</span></span><br><span class="line">(<span class="name"><span class="builtin-name">reset!</span></span> counter <span class="number">2</span>)   <span class="comment">; 更新原子的值</span></span><br><span class="line">(<span class="name">println</span> @counter) <span class="comment">; -&gt; 2 deref，用`@`读取atom的值</span></span><br><span class="line">(<span class="name"><span class="builtin-name">compare-and-set!</span></span> counter <span class="number">2</span> <span class="number">3</span>) <span class="comment">; -&gt; true 执行成功</span></span><br><span class="line">(<span class="name"><span class="builtin-name">swap!</span></span> counter inc) <span class="comment">; 第二个参数是计算Atom新值的函数，可带参数。会一直执行直到成功为止。</span></span><br></pre></td></tr></table></figure><h2 id="Agents"><a href="#Agents" class="headerlink" title="Agents"></a>Agents</h2><p>Agents 是用来把一些事情放到另外一个线程来做（一般不需要事务控制的），用来控制状态的异步更新。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> counter (<span class="name"><span class="builtin-name">agent</span></span> <span class="number">0</span>)) <span class="comment">; 使用agent函数定义一个初始值为0的agent</span></span><br><span class="line">(<span class="name">println</span> @counter) <span class="comment">; -&gt; 0 同样的使用@读取值</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; 更新agent，通过send函数给agent发送任务去更新agent</span></span><br><span class="line">(<span class="name"><span class="builtin-name">send</span></span> counter inc) <span class="comment">; -&gt; #&lt;Agent@9444d1: 0&gt; 此处一般是0，因为更新是异步的</span></span><br><span class="line">(<span class="name">println</span> @counter) <span class="comment">; -&gt; 1  这里获取的肯定是1了，已经更新了</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; 还有个方法，send-off，它的作用于send类似：</span></span><br><span class="line">(<span class="name"><span class="builtin-name">send-off</span></span> counter inc)</span><br><span class="line">(<span class="name">println</span> @counter) <span class="comment">; -&gt; 2</span></span><br></pre></td></tr></table></figure><p>send和send-off的区别在于，send是将任务交给一个固定大小的线程池执行(默认大小是CPU核数+2)。<strong>因此send执行的任务最好不要有阻塞的操作</strong>。而send-off则使用没有大小限制（取决于内存）的线程池。因此，<strong>send-off比较适合任务有阻塞的操作</strong>，如IO读写之类。注意，所有的agent是共用这些线程池。</p><h2 id="扩展阅读"><a href="#扩展阅读" class="headerlink" title="扩展阅读"></a>扩展阅读</h2><p>这篇笔记原先是想放在一篇文章里的，谁知太长了，只好分成了三篇。以下是我觉得学习Clojure不错的网上资源，需者自取。</p><ul><li><a href="http://learnxinyminutes.com/docs/zh-cn/clojure-cn/" target="_blank" rel="noopener">Learn Clojure in Y Minutes</a>, 快速入门</li><li><a href="http://java.ociweb.com/mark/clojure/article.htm" target="_blank" rel="noopener">Clojure - Function Programming for JVM</a>, 更系统地学习。</li><li><a href="http://clojure.github.io/clojure/clojure.core-api.html" target="_blank" rel="noopener">Clojure API</a>，遇到不懂的就查一下，不过还是推荐用Mac上的<a href="https://kapeli.com/" target="_blank" rel="noopener">Dash</a>工具，真的是很好用很方便！</li><li><a href="http://clojure-api-cn.readthedocs.org/en/latest/" target="_blank" rel="noopener">Clojure 中文 API</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;很多人是为了更好地进行并发编程而选择了Clojure，但Clojure所有的数据都是只读的，除非你使用引用类型（Vars、Ref、Atom、Agent）来标明它们是可以修改的。Clojure处理并发的思路与众不同，采用的是&lt;a href=&quot;http://en.wikipedia.org/wiki/Software_transactional_memory&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Software Transactional Memory&lt;/a&gt; (STM)来实现的，即软事务内存。你可以将STM想象成数据库，只不过是内存型的，它只支持事务的ACI，也就是原子性、一致性、隔离性，但是不包括持久性，因为状态的保存都在内存里。引用类型是一种可变引用指向不可变数据的一种机制。Clojure的并发API分为四种模型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;管理Thread local变量的Var。&lt;/li&gt;
&lt;li&gt;管理协作式、同步修改可变状态的Ref&lt;/li&gt;
&lt;li&gt;管理非协作式、同步修改可变状态的Atom&lt;/li&gt;
&lt;li&gt;管理异步修改可变状态的Agent&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
      <category term="编程语言" scheme="http://wuchong.me/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    
      <category term="Clojure" scheme="http://wuchong.me/tags/Clojure/"/>
    
      <category term="笔记" scheme="http://wuchong.me/tags/%E7%AC%94%E8%AE%B0/"/>
    
  </entry>
  
  <entry>
    <title>Clojure学习笔记（二）：语法</title>
    <link href="http://wuchong.me/blog/2015/11/06/learn-clojure-2-syntax/"/>
    <id>http://wuchong.me/blog/2015/11/06/learn-clojure-2-syntax/</id>
    <published>2015-11-06T06:36:15.000Z</published>
    <updated>2022-08-03T06:46:44.508Z</updated>
    
    <content type="html"><![CDATA[<h2 id="定义函数"><a href="#定义函数" class="headerlink" title="定义函数"></a>定义函数</h2><ul><li><p><strong>匿名函数</strong><br>匿名函数<code>(fn) (fn [x y] (+ x y))</code> 创建一个匿名函数, fn 和 lambda 类似，fn还有一个简写形式 <code>#(+ %1 %2)</code>。如果只有一个参数,那么可以用 <code>%</code> 代替 <code>%1</code></p></li><li><p><strong>def</strong><br><code>def</code>可以将一个匿名函数绑定到一个name上。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> my-add (<span class="name"><span class="builtin-name">fn</span></span> [x y] (<span class="name"><span class="builtin-name">+</span></span> x y)))</span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>defn</strong></p><p>defn 是 def 与 fn 的简写。一般我们也都是用defn。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">defn</span></span> my-add</span><br><span class="line">  <span class="string">"this is a comment"</span></span><br><span class="line">  [x y]</span><br><span class="line">  (<span class="name"><span class="builtin-name">+</span></span> x y))</span><br><span class="line">(<span class="name">println</span> (<span class="name">my-add</span> <span class="number">3</span> <span class="number">4</span>)) <span class="comment">; -&gt; 7</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; 函数的重载</span></span><br><span class="line">(<span class="name"><span class="builtin-name">defn</span></span> my-add</span><br><span class="line">  ([x y]</span><br><span class="line">    (<span class="name"><span class="builtin-name">+</span></span> x y))</span><br><span class="line">  ([x y z]</span><br><span class="line">    (<span class="name"><span class="builtin-name">+</span></span> x y z)))</span><br><span class="line">(<span class="name">println</span> (<span class="name">my-add</span> <span class="number">3</span> <span class="number">4</span>)) <span class="comment">; -&gt; 7</span></span><br><span class="line">(<span class="name">println</span> (<span class="name">my-add</span> <span class="number">3</span> <span class="number">4</span> <span class="number">5</span>)) <span class="comment">; -&gt; 12</span></span><br></pre></td></tr></table></figure></li></ul><a id="more"></a><ul><li><p><strong>declare</strong><br>函数定义必须在函数调用前,如果想在定义前使用函数,必须声明它<code>(declare function-names)</code></p></li><li><p><strong>defn- &amp; defmacro-</strong><br>defn- 定义私有函数,他们仅在自己的 namespace 可见</p></li><li><p><strong>参数的解构</strong><br>解构可以用在一个函数或者宏的参数里面来把一个集合里面的一个或者几个元素抽取到一些本地binding里面去。它可以用<code>let</code>,<code>&amp;</code>,<code>:as</code>等。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">defn</span></span> my-add [numbers]</span><br><span class="line">  (<span class="name"><span class="builtin-name">let</span></span> [n1 (<span class="name"><span class="builtin-name">first</span></span> numbers)</span><br><span class="line">         n3 (<span class="name"><span class="builtin-name">nth</span></span> numbers <span class="number">2</span>)]</span><br><span class="line">     (<span class="name"><span class="builtin-name">+</span></span> n1 n3)))</span><br><span class="line">(<span class="name">my-add</span> [<span class="number">1</span> <span class="number">2</span> <span class="number">3</span> <span class="number">4</span>])  <span class="comment">; -&gt; 4</span></span><br></pre></td></tr></table></figure></li></ul><p>  <code>&amp;</code>符号可以在解构里面用来获取集合里面剩下的元素。<code>:as</code>关键字可以用来获取对于整个被解构的集合的访问。</p><h2 id="调用Java"><a href="#调用Java" class="headerlink" title="调用Java"></a>调用Java</h2><ul><li><p><strong>导入</strong></p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">import</span></span></span><br><span class="line">  '(<span class="name">java.util</span> Calendar GregorianCalendar)</span><br><span class="line">  '(<span class="name">javax.swing</span> JFrame JLabel))</span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>创建对象</strong></p><p>使用new关键字创建对象<code>(new class-name args)</code>，也可以使用语法糖简化<code>(class-name. args)</code>。</p><p>例如：<code>(new String &quot;abc&quot;)</code> 或者 <code>(String. &quot;abc&quot;)</code>。</p></li><li><p><strong>方法调用</strong></p><p>使用<code>.</code>，<code>(. class-or-instance method-name args)</code> 或者 <code>(.method-name class-or-instance args)</code></p><p>例如：</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> s (<span class="name"><span class="builtin-name">new</span></span> String <span class="string">"abc"</span>))</span><br><span class="line">(<span class="name"><span class="builtin-name">.</span></span> s charAt <span class="number">0</span>) <span class="comment">; -&gt; \a</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>连续调用方法</strong><br><code>(.. class-or-object (method1 args) (method2 args) ...)</code> 前一个函数的返回值，作为后一个函数的第一个参数。</p><p>例如：</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">..</span></span> s (<span class="name">toString</span>) (<span class="name">charAt</span> <span class="number">1</span>)) <span class="comment">; -&gt; \b</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><strong>set!</strong><br>set! : (set! (. target name) value)</li></ul><ul><li><p><strong>静态变量/静态方法</strong></p><p>对于静态方法和静态变量，可以使用<code>ClassName/field</code>的方式来调用。例如：</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">Integer/MIN_VALUE ; -&gt; -2147483648</span><br><span class="line">(<span class="name">Integer/parseInt</span> <span class="string">"101"</span>)  <span class="comment">; -&gt; 101</span></span><br></pre></td></tr></table></figure></li></ul><h2 id="条件控制"><a href="#条件控制" class="headerlink" title="条件控制"></a>条件控制</h2><ul><li><p><strong>if</strong><br>if的语法是<code>(if condition then-expr else-expr)</code>，第一个参数是条件，第二个表达式是条件成立时执行，第三个表达式可选，在条件不成立时执行。如果需要执行多个表达式，包在<code>do</code>里。</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">if</span></span> is-weekend</span><br><span class="line">    (<span class="name">println</span> <span class="string">"play"</span>)</span><br><span class="line">    (<span class="name"><span class="builtin-name">do</span></span> (<span class="name">println</span> <span class="string">"work"</span>)</span><br><span class="line">        (println "sleep"))))</span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>when &amp; whennot</strong><br>宏when 和when-not 提供和if类似的功能，用它们可以更方便地写inline condition。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">when</span></span> is-weekend (<span class="name">println</span> <span class="string">"play"</span>))</span><br><span class="line">(<span class="name"><span class="builtin-name">when-not</span></span> is-weekend (<span class="name">println</span> <span class="string">"work"</span>) (<span class="name">println</span> <span class="string">"sleep"</span>))</span><br></pre></td></tr></table></figure></li><li><p><strong>if-let &amp; when-let</strong><br><code>if-let</code>给一个变量绑定一个值,如果这个值为 true,则选择一个语句来执行,否则选择另一个。<br><code>when-let</code>跟<code>if-let</code>类似，不同之处还是在于没有else分支。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">defn</span></span> process-next [waiting-line]</span><br><span class="line">  (<span class="name"><span class="builtin-name">if-let</span></span> [name (<span class="name"><span class="builtin-name">first</span></span> waiting-line)]</span><br><span class="line">    (<span class="name">println</span> name <span class="string">"is next"</span>)</span><br><span class="line">    (<span class="name">println</span> <span class="string">"no waiting"</span>)))</span><br><span class="line"> </span><br><span class="line">(<span class="name">process-next</span> '(<span class="string">"Jeremy"</span> <span class="string">"Amanda"</span> <span class="string">"Tami"</span>)) <span class="comment">; -&gt; Jeremy is next</span></span><br><span class="line">(<span class="name">process-next</span> '()) <span class="comment">; -&gt; no waiting</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>condp</strong><br>condp 宏跟其他语言里面的switch/case语句差不多。它接受两个参数，一个谓词参数 (通常是= 或者instance?) 以及一个表达式作为第二个参数。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">defn</span></span> choose [value]</span><br><span class="line">  (<span class="name"><span class="builtin-name">condp</span></span> = value</span><br><span class="line">      <span class="number">1</span> <span class="string">"one"</span></span><br><span class="line">      <span class="number">2</span> <span class="string">"two"</span></span><br><span class="line">      <span class="number">3</span> <span class="string">"three"</span></span><br><span class="line">      (<span class="name"><span class="builtin-name">str</span></span> <span class="string">"unexpected value, "</span> value )))</span><br><span class="line"></span><br><span class="line">(<span class="name">choose</span> <span class="number">2</span>) <span class="comment">; -&gt; "two"</span></span><br><span class="line">(<span class="name">choose</span> <span class="number">8</span>) <span class="comment">; -&gt; "unexpected value, 8"</span></span><br></pre></td></tr></table></figure></li><li><p><strong>cond</strong><br>cond 宏接受任意个 谓词/结果表达式 的组合。按照顺序测试所有谓词，直到有一个为true，返回其结果。有点像一直<code>else if</code>。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">defn</span></span> chosse [t]</span><br><span class="line">  (<span class="name">println</span></span><br><span class="line">    (<span class="name"><span class="builtin-name">cond</span></span></span><br><span class="line">       (<span class="name"><span class="builtin-name">instance?</span></span> String t) <span class="string">"invalid temperature"</span></span><br><span class="line">       (<span class="name"><span class="builtin-name">&lt;=</span></span> t <span class="number">0</span>) <span class="string">"freezing"</span></span><br><span class="line">       (<span class="name"><span class="builtin-name">&gt;=</span></span> t <span class="number">100</span>) <span class="string">"boiling"</span></span><br><span class="line">       <span class="literal">true</span> <span class="string">"neither"</span>)))</span><br></pre></td></tr></table></figure></li></ul><h2 id="迭代"><a href="#迭代" class="headerlink" title="迭代"></a>迭代</h2><ul><li><p><strong>dotimes</strong><br><code>dotimes</code>执行给定的表达式一定次数, 一个本地binding会被给定值：从0到一个给定的数值。如果这个本地binding是不需要的，也可以用<code>_</code>来代替。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">dotimes</span></span> [card-number <span class="number">3</span>]</span><br><span class="line">  (<span class="name">println</span> <span class="string">"deal card number"</span> card-number))</span><br></pre></td></tr></table></figure></li></ul><p>  输出为 </p>  <figure class="highlight applescript"><table><tr><td class="code"><pre><span class="line">deal card <span class="built_in">number</span> <span class="number">0</span></span><br><span class="line">deal card <span class="built_in">number</span> <span class="number">1</span></span><br><span class="line">deal card <span class="built_in">number</span> <span class="number">2</span></span><br></pre></td></tr></table></figure><ul><li><p><strong>while</strong><br><code>while</code> 会一直执行一个表达式只要指定的条件为true。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> c <span class="number">3</span>)</span><br><span class="line">(<span class="name"><span class="builtin-name">while</span></span> (<span class="name"><span class="builtin-name">&gt;</span></span> c <span class="number">0</span>)</span><br><span class="line">  (<span class="name">print</span> <span class="string">"."</span>)</span><br><span class="line">  (<span class="name"><span class="builtin-name">def</span></span> c (<span class="name"><span class="builtin-name">dec</span></span> c)))</span><br><span class="line"></span><br><span class="line"><span class="comment">; -&gt; ...</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>dorun+for &amp; doseq</strong><br><code>doseq</code>用来遍历集合在上面已经讲过了。</p><p><code>for</code>的执行主体只可以采用单行语句，而<code>doseq</code>可以采用多行语句，并且<code>for</code>产生 lazy sequence。同时用<code>:when</code>和<code>:while</code>还可以做一些过滤，它们的区别在于<code>:when</code>会迭代所有bindings并执行满足条件的主体，<code>:while</code>会迭代所有bindings并执行满足条件的主体 <strong><em>直到</em></strong> 条件为false后跳出。</p></li></ul>  <figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> cols <span class="string">"ABCD"</span>)</span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> rows (<span class="name"><span class="builtin-name">range</span></span> <span class="number">1</span> <span class="number">4</span>)) <span class="comment">; purposely larger than needed to demonstrate :while</span></span><br><span class="line"></span><br><span class="line">(<span class="name">println</span> <span class="string">"for demo"</span>)</span><br><span class="line">(<span class="name"><span class="builtin-name">dorun</span></span></span><br><span class="line">  (<span class="name"><span class="builtin-name">for</span></span> [col cols <span class="symbol">:when</span> (<span class="name"><span class="builtin-name">not=</span></span> col \B)</span><br><span class="line">        row rows <span class="symbol">:while</span> (<span class="name"><span class="builtin-name">&lt;</span></span> row <span class="number">3</span>)]</span><br><span class="line">    (<span class="name">println</span> (<span class="name"><span class="builtin-name">str</span></span> col row)))) <span class="comment">; -&gt; 只可单行</span></span><br><span class="line"></span><br><span class="line">(<span class="name">println</span> <span class="string">"\ndoseq demo"</span>)</span><br><span class="line">(<span class="name"><span class="builtin-name">doseq</span></span> [col cols <span class="symbol">:when</span> (<span class="name"><span class="builtin-name">not=</span></span> col \B)</span><br><span class="line">        row rows <span class="symbol">:while</span> (<span class="name"><span class="builtin-name">&lt;</span></span> row <span class="number">3</span>)]</span><br><span class="line">  (<span class="name">print</span> col)      <span class="comment">; 可以</span></span><br><span class="line">  (<span class="name">println</span> row))   <span class="comment">; 多行</span></span><br></pre></td></tr></table></figure><p>  执行结果如下，两段代码的结果是一致的。</p>  <figure class="highlight mipsasm"><table><tr><td class="code"><pre><span class="line">for demo</span><br><span class="line"><span class="built_in">A1</span></span><br><span class="line"><span class="built_in">A2</span></span><br><span class="line">C1</span><br><span class="line">C2</span><br><span class="line">D1</span><br><span class="line">D2</span><br><span class="line"></span><br><span class="line">doseq demo</span><br><span class="line"><span class="built_in">A1</span></span><br><span class="line"><span class="built_in">A2</span></span><br><span class="line">C1</span><br><span class="line">C2</span><br><span class="line">D1</span><br><span class="line">D2</span><br></pre></td></tr></table></figure><h2 id="递归"><a href="#递归" class="headerlink" title="递归"></a>递归</h2><p>由于jvm的关系,clojure不会自动进行尾递归优化(<a href="https://en.wikipedia.org/wiki/Tail_call" target="_blank" rel="noopener">tail call optimization</a>),在尾递归的地方,你应该明确的使用 <code>recur</code> 这个关键词,而不是函数名。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">defn</span></span> my-add [x y]</span><br><span class="line">  (<span class="name"><span class="builtin-name">if</span></span> (<span class="name"><span class="builtin-name">zero?</span></span> x)</span><br><span class="line">    y</span><br><span class="line">    (<span class="name">my-add</span> (<span class="name"><span class="builtin-name">dec</span></span> x) (<span class="name"><span class="builtin-name">inc</span></span> y))))</span><br><span class="line"></span><br><span class="line">(<span class="name"><span class="builtin-name">defn</span></span> my-add [x y]</span><br><span class="line">  (<span class="name"><span class="builtin-name">if</span></span> (<span class="name"><span class="builtin-name">zero?</span></span> x)</span><br><span class="line">    y</span><br><span class="line">    (<span class="name"><span class="builtin-name">recur</span></span> (<span class="name"><span class="builtin-name">dec</span></span> x) (<span class="name"><span class="builtin-name">inc</span></span> y))))</span><br></pre></td></tr></table></figure><p>第一个不会进行尾递归优化,第二个会进行尾递归优化。</p><p><code>loop</code>和<code>recur</code>配合可以实现和循环类似的效果。</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">loop</span></span> [n number factorial <span class="number">1</span>]</span><br><span class="line">    (<span class="name"><span class="builtin-name">if</span></span> (<span class="name"><span class="builtin-name">zero?</span></span> n)</span><br><span class="line">      factorial</span><br><span class="line">      (recur (dec n) (* factorial n)))))</span><br></pre></td></tr></table></figure><p>注意，<code>recur</code>只能出现在special form的最后一行。</p><h2 id="谓词"><a href="#谓词" class="headerlink" title="谓词"></a>谓词</h2><p>false 和 nil 解释为 false<br>true 和其他任意值,包括 0 都被认为是 true</p><ul><li><p>测试两值关系<br>&lt;,&lt;=,=,not=,==,&gt;,&gt;=,compare,distinct? 以及identical?.</p></li><li><p>测试逻辑关系<br>and,or,not,true?,false? 和nil?</p></li><li><p>测试集合<br>empty?,not-empty,every?,not-every?,some? 以及not-any?.</p></li><li><p>测试数字<br>even?,neg?,odd?,pos? 以及zero?.</p></li><li><p>测试对象类型<br>class?,coll?,decimal?,delay?,float?,fn?,instance?,integer?,isa?,keyword?,list?,macro?,map?,number?,seq?,set?,string? 以及vector?</p></li></ul><h2 id="命名空间-namespace"><a href="#命名空间-namespace" class="headerlink" title="命名空间(namespace)"></a>命名空间(namespace)</h2><ul><li><p><strong>require</strong><br>导入clojure库。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name">require</span> 'clojure.string) <span class="comment">; 注意前面的单引号</span></span><br></pre></td></tr></table></figure></li></ul><p>  clojure里面命名空间和方法名之间的分隔符是/而不是java里面使用的</p>  <figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name">clojure.string/join</span> <span class="string">"$"</span> [<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>]) <span class="comment">; -&gt; "1$2$3"</span></span><br></pre></td></tr></table></figure><ul><li><p><strong>alias</strong><br>alias 函数给一个命名空间指定一个别名来简化操作。以及处理类名冲突问题。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">alias</span></span> 'su 'clojure.string)</span><br><span class="line">(<span class="name">su/join</span> <span class="string">"$"</span> [<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>]) <span class="comment">; -&gt; "1$2$3"</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>refer</strong><br><code>refer</code>可以使指定的命名空间里的函数在当前命名空间里可以不需要包名前缀就能访问。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">refer</span></span> 'clojure.string)</span><br><span class="line"><span class="comment">; 上面的代码可以写成</span></span><br><span class="line">(<span class="name">join</span> <span class="string">"$"</span> [<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>]) <span class="comment">; -&gt; "1$2$3"</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>use</strong><br>我们通常把require 和refer 结合使用, 所以clojure提供了一个use ， 它相当于require和refer的简洁形式。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">use</span></span> 'clojure.string)</span><br></pre></td></tr></table></figure></li></ul><ul><li><p><strong>ns</strong><br>ns宏可以改变当前的默认名字空间。它支持这些指令：<code>:require</code>,<code>:use</code>和<code>:import</code>，后面的参数不需要quote。这些其实是它们对应的函数的另外一种方式，一般建议使用这些指令而不是函数。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">ns</span></span> com.example.library</span><br><span class="line">(<span class="symbol">:require</span> [clojure.contrib.sql <span class="symbol">:as</span> sql])</span><br><span class="line">(<span class="symbol">:use</span> (<span class="name">com.example</span> one two))</span><br><span class="line">(<span class="symbol">:import</span> (<span class="name">java.util</span> Date Calendar)</span><br><span class="line">      (<span class="name">java.io</span> File FileInputStream)))</span><br></pre></td></tr></table></figure></li></ul><h2 id="宏-Macro"><a href="#宏-Macro" class="headerlink" title="宏(Macro)"></a>宏(Macro)</h2><p>宏是在读入期（而不是编译期）就进行实际代码替换的一个机制。</p><ul><li><p>反引号(`)<br>防止宏体内的任何一个表达式被evaluate。这意味着宏体里面的代码会原封不动地替换到使用这个宏的所有的地方 – 除了以波浪号开始的那些表达式。</p></li><li><p><code>~</code>和<code>~@</code><br>当一个名字前面被加了一个波浪号，并且还在反引号里面，它的值会被替换的。如果这个名字代表的是一个序列，那么我们可以用 <code>~@</code> 来替换序列里面的某个具体元素。</p></li><li><p><code>id#</code><br>在一个标识符背后加上 # 意味着生成一个唯一的symbol, 比如 foo# 实际可 能就是 foo_004 可以看作是let与gensym的等价物,这在避免符号捕捉时很有用。</p></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;定义函数&quot;&gt;&lt;a href=&quot;#定义函数&quot; class=&quot;headerlink&quot; title=&quot;定义函数&quot;&gt;&lt;/a&gt;定义函数&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;匿名函数&lt;/strong&gt;&lt;br&gt;匿名函数&lt;code&gt;(fn) (fn [x y] (+ x y))&lt;/code&gt; 创建一个匿名函数, fn 和 lambda 类似，fn还有一个简写形式 &lt;code&gt;#(+ %1 %2)&lt;/code&gt;。如果只有一个参数,那么可以用 &lt;code&gt;%&lt;/code&gt; 代替 &lt;code&gt;%1&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;def&lt;/strong&gt;&lt;br&gt;&lt;code&gt;def&lt;/code&gt;可以将一个匿名函数绑定到一个name上。&lt;/p&gt;
&lt;figure class=&quot;highlight clojure&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;(&lt;span class=&quot;name&quot;&gt;&lt;span class=&quot;builtin-name&quot;&gt;def&lt;/span&gt;&lt;/span&gt; my-add (&lt;span class=&quot;name&quot;&gt;&lt;span class=&quot;builtin-name&quot;&gt;fn&lt;/span&gt;&lt;/span&gt; [x y] (&lt;span class=&quot;name&quot;&gt;&lt;span class=&quot;builtin-name&quot;&gt;+&lt;/span&gt;&lt;/span&gt; x y)))&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;defn&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;defn 是 def 与 fn 的简写。一般我们也都是用defn。&lt;/p&gt;
&lt;figure class=&quot;highlight clojure&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;(&lt;span class=&quot;name&quot;&gt;&lt;span class=&quot;builtin-name&quot;&gt;defn&lt;/span&gt;&lt;/span&gt; my-add&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;string&quot;&gt;&quot;this is a comment&quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  [x y]&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  (&lt;span class=&quot;name&quot;&gt;&lt;span class=&quot;builtin-name&quot;&gt;+&lt;/span&gt;&lt;/span&gt; x y))&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;(&lt;span class=&quot;name&quot;&gt;println&lt;/span&gt; (&lt;span class=&quot;name&quot;&gt;my-add&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;)) &lt;span class=&quot;comment&quot;&gt;; -&amp;gt; 7&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;; 函数的重载&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;(&lt;span class=&quot;name&quot;&gt;&lt;span class=&quot;builtin-name&quot;&gt;defn&lt;/span&gt;&lt;/span&gt; my-add&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  ([x y]&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    (&lt;span class=&quot;name&quot;&gt;&lt;span class=&quot;builtin-name&quot;&gt;+&lt;/span&gt;&lt;/span&gt; x y))&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  ([x y z]&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    (&lt;span class=&quot;name&quot;&gt;&lt;span class=&quot;builtin-name&quot;&gt;+&lt;/span&gt;&lt;/span&gt; x y z)))&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;(&lt;span class=&quot;name&quot;&gt;println&lt;/span&gt; (&lt;span class=&quot;name&quot;&gt;my-add&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;)) &lt;span class=&quot;comment&quot;&gt;; -&amp;gt; 7&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;(&lt;span class=&quot;name&quot;&gt;println&lt;/span&gt; (&lt;span class=&quot;name&quot;&gt;my-add&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;5&lt;/span&gt;)) &lt;span class=&quot;comment&quot;&gt;; -&amp;gt; 12&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
    
    </summary>
    
      <category term="编程语言" scheme="http://wuchong.me/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    
      <category term="Clojure" scheme="http://wuchong.me/tags/Clojure/"/>
    
      <category term="笔记" scheme="http://wuchong.me/tags/%E7%AC%94%E8%AE%B0/"/>
    
  </entry>
  
  <entry>
    <title>Clojure学习笔记（一）：数据结构</title>
    <link href="http://wuchong.me/blog/2015/11/06/learn-clojure-1-datastruct/"/>
    <id>http://wuchong.me/blog/2015/11/06/learn-clojure-1-datastruct/</id>
    <published>2015-11-06T06:12:38.000Z</published>
    <updated>2022-08-03T06:46:44.508Z</updated>
    
    <content type="html"><![CDATA[<p>最近学习了Clojure，好记性不如烂笔头，把一些知识点记录了下来。原本想放在一篇文章里的，谁知太长了，只好分成了三篇，本文是第一篇。由于是笔记，比较杂乱，建议阅读前先系统地学习Clojure（比如 <a href="http://java.ociweb.com/mark/clojure/article.htm" target="_blank" rel="noopener">Clojure - Function Programming for JVM</a>），然后用本笔记知识梳理。</p><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>Clojure是一个动态类型的，运行在JVM(JDK5.0以上），并且可以和java代码互操作的函数式语言。这个语言的主要目标之一是使得编写一个有多个线程并发访问数据的程序变得简单。</p><p>Clojure的发音和单词closure是一样的。Clojure之父是这样解释Clojure名字来历的</p><blockquote><p>“我想把这就几个元素包含在里面： C (C#), L (Lisp) and J (Java). 所以我想到了 Clojure, 而且从这个名字还能想到closure;它的域名又没有被占用;而且对于搜索引擎来说也是个很不错的关键词，所以就有了它了.”</p></blockquote><a id="more"></a><h2 id="基本类型"><a href="#基本类型" class="headerlink" title="基本类型"></a>基本类型</h2><h3 id="Boolean"><a href="#Boolean" class="headerlink" title="Boolean"></a>Boolean</h3><p><code>true</code>, <code>false</code></p><p>常用函数:</p><ol><li><code>not</code></li><li><code>and</code></li><li><code>or</code></li></ol><h3 id="Nil"><a href="#Nil" class="headerlink" title="Nil"></a>Nil</h3><p><code>nil</code>，只有false与nil会被计算为false,其它的都为true</p><p>常用函数：</p><ol><li><code>nil?</code></li></ol><h3 id="Character"><a href="#Character" class="headerlink" title="Character"></a>Character</h3><p>字符的表示要在前面加反斜杠 <code>\a</code> <code>\b</code> <code>\c</code> …</p><h3 id="Number"><a href="#Number" class="headerlink" title="Number"></a>Number</h3><p>数字<code>1</code>, <code>2</code> …</p><p>常用函数：</p><ol><li><code>+</code>, <code>-</code>, <code>*</code></li><li><code>/</code>(分数形式),<code>quot</code>(商),<code>rem</code>(余数)</li><li><code>inc</code>,<code>dec</code></li><li><code>min</code>, <code>max</code></li><li><code>=</code>,<code>&lt;</code>,<code>&lt;</code>,<code>&gt;</code>,<code>&gt;=</code></li><li><code>zero?</code>,<code>pos?</code>,<code>neg?</code>,<code>number?</code></li></ol><h3 id="String"><a href="#String" class="headerlink" title="String"></a>String</h3><p>字符串，如 “hello”</p><p>常用函数</p><ol><li><code>str</code>: 拼接多个字符串</li><li><code>subs</code>: 子字符串(0为开始下标)</li><li><code>string?</code></li></ol><h3 id="KeyWord"><a href="#KeyWord" class="headerlink" title="KeyWord"></a>KeyWord</h3><p>关键字，常用于Map中的key，<code>:tag</code>，<code>:doc</code></p><h3 id="Symbol"><a href="#Symbol" class="headerlink" title="Symbol"></a>Symbol</h3><p>变量名，一个命名空间中唯一。如<code>(def x 1)</code>，就申请了一个<code>&#39;user/x</code>的Symbol。</p><h2 id="集合"><a href="#集合" class="headerlink" title="集合"></a>集合</h2><p>list, vector, set, map</p><p>clojure中的任何变量都是不变的，当对一个变量进行修改时，都会产生一个新的变量（clojure会使用共享内存的方式，只会消耗很小的内存代价）。有点像scala中的val哦。</p><ul><li><p>count<br>返回集合里面的元素个数</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">count</span></span> [<span class="number">19</span> <span class="string">"yellow"</span> <span class="literal">true</span>]) <span class="comment">; -&gt; 3</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p>reverse<br>把集合里面的元素反转</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">reverse</span></span> [<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>]) <span class="comment">; -&gt; (3 2 1)</span></span><br></pre></td></tr></table></figure></li><li><p>map<br>对一个集合内的每个元素都调用一个指定的方法。每个元素调用方法后的返回值再构成一个新的集合返回。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line"><span class="comment">; 使用匿名函数对每个元素加3</span></span><br><span class="line">(<span class="name"><span class="builtin-name">map</span></span> #(<span class="name"><span class="builtin-name">+</span></span> % <span class="number">3</span>) [<span class="number">2</span> <span class="number">4</span> <span class="number">7</span>]) <span class="comment">; -&gt; (5 7 10)</span></span><br><span class="line"><span class="comment">; 函数可以有多个参数，则指定多个集合，执行次数取决于个数最少的集合长度</span></span><br><span class="line">(<span class="name"><span class="builtin-name">map</span></span> + [<span class="number">2</span> <span class="number">4</span> <span class="number">7</span>] [<span class="number">5</span> <span class="number">6</span>] [<span class="number">1</span> <span class="number">2</span> <span class="number">3</span> <span class="number">4</span>]) <span class="comment">; -&gt; (8 12)</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p>apply<br>把集合里的所有元素都作为函数参数做一次调用，并返回这个函数的返回值。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">apply</span></span> + [<span class="number">2</span> <span class="number">4</span> <span class="number">7</span>]) <span class="comment">; -&gt; 13</span></span><br></pre></td></tr></table></figure></li><li><p>conj &amp; cons<br>conj (conjoin), 添加一个元素到集合里面去。如果是list类型，则新元素插到队头；如果是vector类型，则新元素插到队尾。</p><p>cons (construct),也是添加一个元素到集合里面去。只不过新元素都会在队头。返回类型都会变成seq。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line"><span class="comment">; cons用以向列表或向量的起始位置添加元素</span></span><br><span class="line">(<span class="name"><span class="builtin-name">cons</span></span> <span class="number">4</span> [<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>]) <span class="comment">; =&gt; (4 1 2 3)</span></span><br><span class="line">(<span class="name"><span class="builtin-name">cons</span></span> <span class="number">4</span> '(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>)) <span class="comment">; =&gt; (4 1 2 3)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; conj将以最高效的方式向集合中添加元素。</span></span><br><span class="line"><span class="comment">; 对于列表，数据会在起始位置插入，而对于向量，则在末尾位置插入。</span></span><br><span class="line"><span class="comment">; 注意：第一个参数是集合，第二个是要插入的元素，可以有多个元素</span></span><br><span class="line">(<span class="name"><span class="builtin-name">conj</span></span> [<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>] <span class="number">4</span>) <span class="comment">; -&gt; [1 2 3 4]</span></span><br><span class="line">(<span class="name"><span class="builtin-name">conj</span></span> [<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>] <span class="number">4</span> <span class="number">5</span>) <span class="comment">; -&gt; [1 2 3 4 5]</span></span><br><span class="line">(<span class="name"><span class="builtin-name">conj</span></span> '(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>) <span class="number">4</span> <span class="number">5</span> <span class="number">6</span>) <span class="comment">; -&gt; (6 5 4 1 2 3)</span></span><br></pre></td></tr></table></figure></li></ul><p>常用的集合操作</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line"><span class="comment">;从集合中取一个元素</span></span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> stooges [<span class="string">"Moe"</span> <span class="string">"Larry"</span> <span class="string">"Curly"</span> <span class="string">"Shemp"</span>]) (<span class="name"><span class="builtin-name">first</span></span> stooges) <span class="comment">; -&gt; "Moe"</span></span><br><span class="line">(<span class="name"><span class="builtin-name">second</span></span> stooges) <span class="comment">; -&gt; "Larry"</span></span><br><span class="line">(<span class="name"><span class="builtin-name">last</span></span> stooges) <span class="comment">; -&gt; "Shemp"</span></span><br><span class="line"></span><br><span class="line"><span class="comment">;从集合中取多个元素</span></span><br><span class="line">(<span class="name"><span class="builtin-name">nth</span></span> stooges <span class="number">2</span>) <span class="comment">; indexes start at 0 -&gt; "Curly" (next stooges) ; -&gt; ("Larry" "Curly" "Shemp") (butlast stooges) ; -&gt; ("Moe" "Larry" "Curly") (drop-last 2 stooges) ; -&gt; ("Moe" "Larry")</span></span><br><span class="line"><span class="comment">; 得到包含多于3个字符的名字.</span></span><br><span class="line">(<span class="name"><span class="builtin-name">filter</span></span> #(<span class="name"><span class="builtin-name">&gt;</span></span> (<span class="name"><span class="builtin-name">count</span></span> %) <span class="number">3</span>) stooges) <span class="comment">; -&gt; ("Larry" "Curly" "Shemp") </span></span><br><span class="line">(<span class="name"><span class="builtin-name">nthnext</span></span> stooges <span class="number">2</span>) <span class="comment">; -&gt; ("Curly" "Shemp")</span></span><br><span class="line"></span><br><span class="line"><span class="comment">;一些谓词操作</span></span><br><span class="line">(<span class="name">every?</span> #(<span class="name"><span class="builtin-name">instance?</span></span> String %) stooges) <span class="comment">; -&gt; true</span></span><br><span class="line">(<span class="name"><span class="builtin-name">not-every?</span></span> #(<span class="name"><span class="builtin-name">instance?</span></span> String %) stooges) <span class="comment">; -&gt; false</span></span><br><span class="line">(<span class="name">some</span> #(<span class="name"><span class="builtin-name">instance?</span></span> Number %) stooges) <span class="comment">; -&gt; nil</span></span><br><span class="line">(<span class="name"><span class="builtin-name">not-any?</span></span> #(<span class="name"><span class="builtin-name">instance?</span></span> Number %) stooges) <span class="comment">; -&gt; true</span></span><br></pre></td></tr></table></figure><h3 id="Lists"><a href="#Lists" class="headerlink" title="Lists"></a>Lists</h3><p>Lists 相当于Java中的LinkedList。</p><ul><li><p>创建</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> stooges (<span class="name"><span class="builtin-name">list</span></span> <span class="string">"Moe"</span> <span class="string">"Larry"</span> <span class="string">"Curly"</span>))</span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> stooges (<span class="name"><span class="builtin-name">quote</span></span> (<span class="string">"Moe"</span> <span class="string">"Larry"</span> <span class="string">"Curly"</span>)))</span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> stooges '(<span class="string">"Moe"</span> <span class="string">"Larry"</span> <span class="string">"Curly"</span>))</span><br></pre></td></tr></table></figure></li></ul><ul><li><p>some<br>检测一个集合中是否包含某个元素，需要跟一个谓词函数和一个集合。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name">some</span> #(<span class="name"><span class="builtin-name">=</span></span> % <span class="string">"Moe"</span>) stooges) <span class="comment">; -&gt; true</span></span><br><span class="line">(<span class="name">some</span> #(<span class="name"><span class="builtin-name">=</span></span> % <span class="string">"Mark"</span>) stooges) <span class="comment">; -&gt; nil</span></span><br><span class="line"><span class="comment">;在Lists搜索一个元素是线性低效的，在set里搜索就容易且高效多了</span></span><br><span class="line">(<span class="name"><span class="builtin-name">contains?</span></span> (<span class="name">set</span> stooges) <span class="string">"Moe"</span>) <span class="comment">; -&gt; true</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p>into<br>把两个集合里面的元素合并成一个新的大list</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">into</span></span> [<span class="number">1</span> <span class="number">2</span>] [<span class="number">3</span> <span class="number">4</span>]) <span class="comment">; -&gt; [1 2 3 4]</span></span><br><span class="line">(<span class="name"><span class="builtin-name">into</span></span> '(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>) '(<span class="number">4</span> <span class="number">5</span> <span class="number">6</span>)) <span class="comment">; -&gt; (6 5 4 1 2 3)</span></span><br></pre></td></tr></table></figure></li></ul><h3 id="Vectors"><a href="#Vectors" class="headerlink" title="Vectors"></a>Vectors</h3><p>类似于数组。这种集合对于从最后面删除一个元素，或者获取最后面一个元素是非常高效的(O(1))。这意味着对于向vector里面添加元素使用conj被使用cons更高效。  </p><ul><li><p>创建</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> stooges (<span class="name"><span class="builtin-name">vector</span></span> <span class="string">"Moe"</span> <span class="string">"Larry"</span> <span class="string">"Curly"</span>))</span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> stooges [<span class="string">"Moe"</span> <span class="string">"Larry"</span> <span class="string">"Curly"</span>])</span><br></pre></td></tr></table></figure></li></ul><ul><li><p>get<br>获取vector里面指定索引的元素，从map中取value也是用的get。get 和 nth 有点类似。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line"><span class="comment">; Usage: (get map key) , (get map key not-found)</span></span><br><span class="line"> (<span class="name"><span class="builtin-name">get</span></span> stooges <span class="number">1</span> <span class="string">"unknown"</span>) <span class="comment">; -&gt; "Larry"</span></span><br><span class="line">(<span class="name"><span class="builtin-name">get</span></span> stooges <span class="number">3</span> <span class="string">"unknown"</span>) <span class="comment">; -&gt; "unknown"</span></span><br></pre></td></tr></table></figure></li><li><p>assoc<br>可以对 vectors 和 maps进行操作。替换指定索引的元素。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">assoc</span></span> stooges <span class="number">2</span> <span class="string">"Shemp"</span>) <span class="comment">; -&gt; ["Moe" "Larry" "Shemp"]</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p>subvec<br>获取一个给定vector的子vector。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">  <span class="comment">; Usage: (subvec v start) , (subvec v start end)</span></span><br><span class="line">  (<span class="name"><span class="builtin-name">subvec</span></span> stooges <span class="number">1</span>) <span class="comment">; -&gt; ["Larry" "Curly"]</span></span><br><span class="line">  (<span class="name"><span class="builtin-name">subvec</span></span> stooges <span class="number">1</span> <span class="number">2</span>) <span class="comment">; -&gt; ["Larry"]</span></span><br><span class="line">  ``` </span><br><span class="line"></span><br><span class="line">### Sets</span><br><span class="line"></span><br><span class="line">Sets 是一个包含不重复元素的集合。Clojure 支持两种不同的set： 排序的(sorted-set)和不排序的(hash-set)。</span><br><span class="line"></span><br><span class="line">- 创建</span><br><span class="line">  </span><br><span class="line">  ```clojure</span><br><span class="line">  (<span class="name"><span class="builtin-name">def</span></span> stooges (<span class="name"><span class="builtin-name">hash-set</span></span> <span class="string">"Moe"</span> <span class="string">"Larry"</span> <span class="string">"Curly"</span>)) <span class="comment">; not sorted</span></span><br><span class="line">  (<span class="name"><span class="builtin-name">def</span></span> stooges #&#123;<span class="string">"Moe"</span> <span class="string">"Larry"</span> <span class="string">"Curly"</span>&#125;) <span class="comment">; same as previous</span></span><br><span class="line">  (<span class="name"><span class="builtin-name">def</span></span> stooges (<span class="name"><span class="builtin-name">sorted-set</span></span> <span class="string">"Moe"</span> <span class="string">"Larry"</span> <span class="string">"Curly"</span>))</span><br></pre></td></tr></table></figure></li></ul><ul><li><p>contains?<br>是否包含某元素。可以操作在set和map上。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">contains?</span></span> stooges <span class="string">"Moe"</span>) <span class="comment">; -&gt; true</span></span><br><span class="line">(<span class="name"><span class="builtin-name">contains?</span></span> stooges <span class="string">"Mark"</span>) <span class="comment">; -&gt; false</span></span><br></pre></td></tr></table></figure></li></ul><p>  Sets 自己也可以作为一个函数。当以这种方式来用的时候，返回值要么是这个元素，要么是nil。 这个比起contains？函数来说更简洁。</p>  <figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name">stooges</span> <span class="string">"Moe"</span>) <span class="comment">; -&gt; "Moe"</span></span><br><span class="line">(<span class="name">stooges</span> <span class="string">"Mark"</span>) <span class="comment">; -&gt; nil</span></span><br><span class="line">(<span class="name">println</span> (<span class="name"><span class="builtin-name">if</span></span> (<span class="name">stooges</span> person) <span class="string">"stooge"</span> <span class="string">"regular person"</span>))</span><br></pre></td></tr></table></figure><ul><li><p>disj<br>去掉给定的set里面的一些元素，返回一个新的set。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> more-stooges (<span class="name"><span class="builtin-name">conj</span></span> stooges <span class="string">"Shemp"</span>)) <span class="comment">; -&gt; #&#123;"Moe" "Larry" "Curly" "Shemp"&#125;</span></span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> less-stooges (<span class="name"><span class="builtin-name">disj</span></span> more-stooges <span class="string">"Curly"</span>)) <span class="comment">; -&gt; #&#123;"Moe" "Larry" "Shemp"&#125;</span></span><br></pre></td></tr></table></figure></li></ul><h3 id="Maps"><a href="#Maps" class="headerlink" title="Maps"></a>Maps</h3><p>Maps 保存从key到value的a对应关系 — key和value都可以是任意对象。和set类似，map也分为排序的(sorted-hash)和不排序的(hash-map)。</p><ul><li><p>创建</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> colors (<span class="name"><span class="builtin-name">hash-map</span></span> <span class="symbol">:red</span> <span class="number">1</span>, <span class="symbol">:yellow</span> <span class="number">9</span>, <span class="symbol">:blue</span> <span class="number">4</span>, <span class="symbol">:black</span> <span class="number">3</span>)) </span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> colors &#123;<span class="symbol">:red</span> <span class="number">1</span>, <span class="symbol">:yellow</span> <span class="number">9</span>, <span class="symbol">:blue</span> <span class="number">4</span>, <span class="symbol">:black</span> <span class="number">3</span>&#125;) </span><br><span class="line"><span class="comment">; -&gt; &#123;:red 1, :yellow 9, :blue 4, :black 3&#125; 语法糖，和上面的是一样的</span></span><br><span class="line">(<span class="name"><span class="builtin-name">def</span></span> colors_sort (<span class="name"><span class="builtin-name">sorted-map</span></span> <span class="symbol">:red</span> <span class="number">1</span>, <span class="symbol">:yellow</span> <span class="number">9</span>, <span class="symbol">:blue</span> <span class="number">4</span>, <span class="symbol">:black</span> <span class="number">3</span>))</span><br><span class="line"><span class="comment">; -&gt; &#123;:black 3, :blue 4, :red 1, :yellow 9&#125; 可以看到这是排序了的</span></span><br></pre></td></tr></table></figure><p>Map可以作为它的key的函数，同时如果key是keyword的话，那么key也可以作为map的函数。下面是三种获取<code>:red</code>所对应的值的方法。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">get</span></span> colors <span class="symbol">:red</span>)</span><br><span class="line">(<span class="name">colors</span> <span class="symbol">:green</span>)</span><br><span class="line">(<span class="symbol">:red</span> colors)</span><br></pre></td></tr></table></figure></li><li><p>contains? &amp; keys &amp; vals<br>contains? 用来检测某个key存不存在。keys 和  vals 可以获得map中的键集合和值集合。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">contains?</span></span> colos <span class="symbol">:red</span>) <span class="comment">; -&gt; true</span></span><br><span class="line">(<span class="name"><span class="builtin-name">keys</span></span> colors) <span class="comment">; -&gt; (:yellow :red :blue :black)</span></span><br><span class="line">(<span class="name"><span class="builtin-name">vals</span></span> colors) <span class="comment">; -&gt; (9 1 4 3)</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p>assoc &amp; dissoc<br>assoc 会创建一个新的map，同时添加任意对新的key-value对, 如果某个给定的key已经存在了，那么它的值会被更新。</p><p>dissoc 会创建一个新的map，同时去掉了给定的那些key。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">assoc</span></span> colors <span class="symbol">:red</span> <span class="string">"red"</span> <span class="symbol">:white</span> <span class="string">"white"</span>)</span><br><span class="line"><span class="comment">; -&gt; &#123;:white "white", :yellow 9, :red "red", :blue 4, :black 3&#125;</span></span><br><span class="line">(<span class="name"><span class="builtin-name">dissoc</span></span> colors <span class="symbol">:red</span> <span class="symbol">:white</span> )</span><br><span class="line"><span class="comment">; -&gt; &#123;:yellow 9, :blue 4, :black 3&#125;</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p>遍历<br>遍历map可以使用宏<code>doseq</code>，把key bind到color， 把value bind到v。name函数返回一个keyword的字符串名字。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">doseq</span></span> [[color v] colors]</span><br><span class="line">  (<span class="name">println</span> (<span class="name"><span class="builtin-name">str</span></span> <span class="string">"The value of "</span> (<span class="name"><span class="builtin-name">name</span></span> color) <span class="string">" is "</span> v <span class="string">"."</span>)))</span><br></pre></td></tr></table></figure></li></ul><p>  输出为：</p>  <figure class="highlight ceylon"><table><tr><td class="code"><pre><span class="line">The <span class="keyword">value</span> <span class="keyword">of</span> yellow <span class="keyword">is</span> <span class="number">9</span>.</span><br><span class="line">The <span class="keyword">value</span> <span class="keyword">of</span> red <span class="keyword">is</span> <span class="number">1</span>.</span><br><span class="line">The <span class="keyword">value</span> <span class="keyword">of</span> blue <span class="keyword">is</span> <span class="number">4</span>.</span><br><span class="line">The <span class="keyword">value</span> <span class="keyword">of</span> black <span class="keyword">is</span> <span class="number">3</span>.</span><br></pre></td></tr></table></figure><p>  或者使用<a href="https://gist.github.com/john2x/e1dca953548bfdfb9844" target="_blank" rel="noopener">Destructing</a>。</p><ul><li><p>内嵌<br>map的值也可以是一个map。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> person &#123;</span><br><span class="line">  <span class="symbol">:name</span> <span class="string">"Mark Volkmann"</span></span><br><span class="line">  <span class="symbol">:address</span> &#123;</span><br><span class="line">    <span class="symbol">:street</span> <span class="string">"644 Glen Summit"</span></span><br><span class="line">    <span class="symbol">:city</span> <span class="string">"St. Charles"</span></span><br><span class="line">    <span class="symbol">:state</span> <span class="string">"Missouri"</span></span><br><span class="line">    <span class="symbol">:zip</span> <span class="number">63304</span>&#125;</span><br><span class="line">  <span class="symbol">:employer</span> &#123;</span><br><span class="line">    <span class="symbol">:name</span> <span class="string">"Object Computing, Inc."</span></span><br><span class="line">    <span class="symbol">:address</span> &#123;</span><br><span class="line">      <span class="symbol">:street</span> <span class="string">"12140 Woodcrest Executive Drive, Suite 250"</span></span><br><span class="line">      <span class="symbol">:city</span> <span class="string">"Creve Coeur"</span></span><br><span class="line">      <span class="symbol">:state</span> <span class="string">"Missouri"</span></span><br><span class="line">      <span class="symbol">:zip</span> <span class="number">63141</span>&#125;&#125;&#125;)</span><br></pre></td></tr></table></figure></li></ul><p>  为了获取这个人的employer的address的city的值，我们一般有三种方法。</p>  <figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line"><span class="comment">; 使用get-in函数</span></span><br><span class="line">(<span class="name"><span class="builtin-name">get-in</span></span> person [<span class="symbol">:employer</span> <span class="symbol">:address</span> <span class="symbol">:city</span>])</span><br><span class="line"><span class="comment">; 宏 -&gt; 本质上是调用一系列的函数，前一个函数的返回值作为后一个函数的参数. </span></span><br><span class="line">(<span class="name"><span class="builtin-name">-&gt;</span></span> person <span class="symbol">:employer</span> <span class="symbol">:address</span> <span class="symbol">:city</span>) </span><br><span class="line"><span class="comment">; reduce 函数接收一个需要两个参数的函数, 一个可选的value以及一个集合。</span></span><br><span class="line">(<span class="name"><span class="builtin-name">reduce</span></span> get person [<span class="symbol">:employer</span> <span class="symbol">:address</span> <span class="symbol">:city</span>])</span><br></pre></td></tr></table></figure><p>  修改一个内嵌的key值，我们可以使用<code>assoc-in</code>或者<code>update-in</code>，不同之处在于<code>update-in</code>的新值是通过一个给定的函数来计算出来的。</p>  <figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name">assoc-in</span> person [<span class="symbol">:employer</span> <span class="symbol">:address</span> <span class="symbol">:city</span>] <span class="string">"Clayton"</span>)</span><br><span class="line">(<span class="name"><span class="builtin-name">update-in</span></span> person [<span class="symbol">:employer</span> <span class="symbol">:address</span> <span class="symbol">:zip</span>] str <span class="string">"-1234"</span>) <span class="comment">; :zip "63141-1234"</span></span><br></pre></td></tr></table></figure><h2 id="StructMaps"><a href="#StructMaps" class="headerlink" title="StructMaps"></a>StructMaps</h2><p>StructMaps 和普通map类似，它的作用其实是用来模拟java里面的javabean。</p><ul><li><p>定义</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> vehicle-struct (<span class="name">create-struct</span> <span class="symbol">:make</span> <span class="symbol">:model</span> <span class="symbol">:year</span> <span class="symbol">:color</span>))</span><br><span class="line">(<span class="name"><span class="builtin-name">defstruct</span></span> vehicle-struct <span class="symbol">:make</span> <span class="symbol">:model</span> <span class="symbol">:year</span> <span class="symbol">:color</span>) <span class="comment">; 简洁的写法</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p>实例化</p><p>使用<code>struct</code>函数实例化一个StructMap对象，相当于java里面的new关键字。提供给struct的参数的顺序必须和定义时提供的keyword的顺序一致，后面的参数可以忽略。如果忽略，那么对应key的值就是nil。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> vehicle (<span class="name">struct</span> vehicle-struct <span class="string">"Toyota"</span> <span class="string">"Prius"</span> <span class="number">2009</span>))</span><br></pre></td></tr></table></figure></li></ul><ul><li><p>accessor<br>accessor 函数可以创建一个类似java里面的getXXX的方法， 它的好处是可以避免hash查找， 它比普通的hash查找要快。</p><figure class="highlight clojure"><table><tr><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">def</span></span> make (<span class="name">accessor</span> vehicle-struct <span class="symbol">:make</span>)) <span class="comment">; 注意使用的是def而不是defn</span></span><br><span class="line">(<span class="name">make</span> vehicle) <span class="comment">; -&gt; "Toyota"</span></span><br><span class="line">(<span class="name">vehicle</span> <span class="symbol">:make</span>) <span class="comment">; 效果一样只是慢些</span></span><br><span class="line">(<span class="symbol">:make</span> vehicle) <span class="comment">; 效果一样只是慢些</span></span><br></pre></td></tr></table></figure></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近学习了Clojure，好记性不如烂笔头，把一些知识点记录了下来。原本想放在一篇文章里的，谁知太长了，只好分成了三篇，本文是第一篇。由于是笔记，比较杂乱，建议阅读前先系统地学习Clojure（比如 &lt;a href=&quot;http://java.ociweb.com/mark/clojure/article.htm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Clojure - Function Programming for JVM&lt;/a&gt;），然后用本笔记知识梳理。&lt;/p&gt;
&lt;h2 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h2&gt;&lt;p&gt;Clojure是一个动态类型的，运行在JVM(JDK5.0以上），并且可以和java代码互操作的函数式语言。这个语言的主要目标之一是使得编写一个有多个线程并发访问数据的程序变得简单。&lt;/p&gt;
&lt;p&gt;Clojure的发音和单词closure是一样的。Clojure之父是这样解释Clojure名字来历的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“我想把这就几个元素包含在里面： C (C#), L (Lisp) and J (Java). 所以我想到了 Clojure, 而且从这个名字还能想到closure;它的域名又没有被占用;而且对于搜索引擎来说也是个很不错的关键词，所以就有了它了.”&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="编程语言" scheme="http://wuchong.me/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    
      <category term="Clojure" scheme="http://wuchong.me/tags/Clojure/"/>
    
      <category term="笔记" scheme="http://wuchong.me/tags/%E7%AC%94%E8%AE%B0/"/>
    
  </entry>
  
  <entry>
    <title>Thrift 实践</title>
    <link href="http://wuchong.me/blog/2015/10/07/thrift-practice/"/>
    <id>http://wuchong.me/blog/2015/10/07/thrift-practice/</id>
    <published>2015-10-07T12:22:35.000Z</published>
    <updated>2022-08-03T06:46:44.512Z</updated>
    
    <content type="html"><![CDATA[<p><a href="/blog/2015/10/07/thrift-induction">上一篇文章</a>我们了解了thrift的概念以及类型系统，本文我们通过一个简单的实例来更深入地了解thrift的使用。我们的实例非常简单，就是实现一个登录注册功能，其用户名密码缓存在内存中。</p><h2 id="编写thrift文件"><a href="#编写thrift文件" class="headerlink" title="编写thrift文件"></a>编写thrift文件</h2><p>我们编写一个<code>account.thrift</code>的文件。</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">namespace</span> java me.wuchong.thrift.generated</span><br><span class="line"></span><br><span class="line"><span class="keyword">enum</span> Operation&#123;</span><br><span class="line">  LOGIN = <span class="number">1</span>,</span><br><span class="line">  REGISTER = <span class="number">2</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">struct Request&#123;</span><br><span class="line">  <span class="number">1</span>: <span class="built_in">string</span> name,</span><br><span class="line">  <span class="number">2</span>: <span class="built_in">string</span> password,</span><br><span class="line">  <span class="number">3</span>: Operation op</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">exception InvalidOperation&#123;</span><br><span class="line">  <span class="number">1</span>: i32 code,</span><br><span class="line">  <span class="number">2</span>: <span class="built_in">string</span> reason</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">service Account&#123;</span><br><span class="line">  <span class="built_in">string</span> doAction(<span class="number">1</span>: Request request) throws (<span class="number">1</span>: InvalidOperation e);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><a id="more"></a><p>然后在命令行下运行如下命令：<br><figure class="highlight ada"><table><tr><td class="code"><pre><span class="line">thrift <span class="comment">--gen java account.thrift</span></span><br></pre></td></tr></table></figure></p><p>则会在当前目录生成<code>gen-java</code>目录，该目录下会按照namespace定义的路径名一次一层层生成文件夹，如下图所示，在指定的包路径下生成了4个类。<br><img src="http://ww1.sinaimg.cn/mw690/81b78497jw1ewsui5oaftj20he058t92.jpg" alt>￼</p><h2 id="服务实现"><a href="#服务实现" class="headerlink" title="服务实现"></a>服务实现</h2><p>到此为止，thrift已经完成了其工作。接下来我们需要做的就是实现<code>Account</code>接口里的具体逻辑。我们创建一个<code>AccountService</code>类，实现<code>Account.Iface</code>接口。逻辑非常简单，将用户账户信息缓存在内存中，实现登录注册的功能，并且对一些非法输入状况抛出异常。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> me.wuchong.thrift.impl;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> me.wuchong.thrift.generated.Account;</span><br><span class="line"><span class="keyword">import</span> me.wuchong.thrift.generated.InvalidOperation;</span><br><span class="line"><span class="keyword">import</span> me.wuchong.thrift.generated.Operation;</span><br><span class="line"><span class="keyword">import</span> me.wuchong.thrift.generated.Request;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.HashMap;</span><br><span class="line"><span class="keyword">import</span> java.util.Map;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by wuchong on 15/10/7.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AccountService</span> <span class="keyword">implements</span> <span class="title">Account</span>.<span class="title">Iface</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Map&lt;String, String&gt; accounts = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">doAction</span><span class="params">(Request request)</span> <span class="keyword">throws</span> InvalidOperation </span>&#123;</span><br><span class="line">        String name = request.getName();</span><br><span class="line">        String pass = request.getPassword();</span><br><span class="line">        Operation op = request.getOp();</span><br><span class="line"></span><br><span class="line">        System.out.println(String.format(<span class="string">"Get request[name:%s, pass:%s, op:%d]"</span>, name, pass, op.getValue()));</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (name == <span class="keyword">null</span> || name.length() == <span class="number">0</span>)&#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> InvalidOperation(<span class="number">100</span>, <span class="string">"param name should not be empty"</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (op == Operation.LOGIN) &#123;</span><br><span class="line">            String password = accounts.get(name);</span><br><span class="line">            <span class="keyword">if</span> (password != <span class="keyword">null</span> &amp;&amp; password.equals(pass)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="string">"Login success!! Hello "</span> + name;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="string">"Login failed!! please check your username and password"</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (op == Operation.REGISTER) &#123;</span><br><span class="line">            <span class="keyword">if</span> (accounts.containsKey(name)) &#123;</span><br><span class="line">                <span class="keyword">return</span> String.format(<span class="string">"The username '%s' has been registered, please change one."</span>, name);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                accounts.put(name, pass);</span><br><span class="line">                <span class="keyword">return</span> <span class="string">"Register success!! Hello "</span> + name;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> InvalidOperation(<span class="number">101</span>, <span class="string">"unknown operation: "</span> + op.getValue());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="启动服务端和客户端"><a href="#启动服务端和客户端" class="headerlink" title="启动服务端和客户端"></a>启动服务端和客户端</h2><p>我们实现了服务的具体逻辑，接下来需要启动该服务。这里我们需要用到thrift的依赖包。在pom.xml中加入对thrift的依赖。</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.thrift<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>libthrift<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>0.9.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p><em>注：如果你的依赖中没有加入slf4j的实现，则需要加上slf4j-log4j12或者logback的依赖，因为thrift有用到slf4j</em></p><p>启动服务的实现如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> me.wuchong.thrift.impl;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> me.wuchong.thrift.generated.Account;</span><br><span class="line"><span class="keyword">import</span> org.apache.thrift.server.TServer;</span><br><span class="line"><span class="keyword">import</span> org.apache.thrift.server.TSimpleServer;</span><br><span class="line"><span class="keyword">import</span> org.apache.thrift.transport.TServerSocket;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by wuchong on 15/10/7.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AccountServer</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        TServerSocket socket = <span class="keyword">new</span> TServerSocket(<span class="number">9999</span>);</span><br><span class="line">        Account.Processor processor = <span class="keyword">new</span> Account.Processor&lt;&gt;(<span class="keyword">new</span> AccountService());</span><br><span class="line">        TServer server = <span class="keyword">new</span> TSimpleServer(<span class="keyword">new</span> TServer.Args(socket).processor(processor));</span><br><span class="line">        System.out.println(<span class="string">"Starting the Account server..."</span>);</span><br><span class="line">        server.serve();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行之后，可以在控制台看到输出：<br><figure class="highlight gams"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="title">Starting</span></span> the Account server...</span><br></pre></td></tr></table></figure></p><p>目前服务已经启动，则在客户端就可以进行RPC调用了。启动客户端的代码如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> me.wuchong.thrift.impl;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> me.wuchong.thrift.generated.Account;</span><br><span class="line"><span class="keyword">import</span> me.wuchong.thrift.generated.InvalidOperation;</span><br><span class="line"><span class="keyword">import</span> me.wuchong.thrift.generated.Operation;</span><br><span class="line"><span class="keyword">import</span> me.wuchong.thrift.generated.Request;</span><br><span class="line"><span class="keyword">import</span> org.apache.thrift.TException;</span><br><span class="line"><span class="keyword">import</span> org.apache.thrift.protocol.TBinaryProtocol;</span><br><span class="line"><span class="keyword">import</span> org.apache.thrift.protocol.TProtocol;</span><br><span class="line"><span class="keyword">import</span> org.apache.thrift.transport.TSocket;</span><br><span class="line"><span class="keyword">import</span> org.apache.thrift.transport.TTransport;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by wuchong on 15/10/7.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AccountClient</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> TException </span>&#123;</span><br><span class="line">        TTransport transport = <span class="keyword">new</span> TSocket(<span class="string">"localhost"</span>, <span class="number">9999</span>);</span><br><span class="line">        transport.open();   <span class="comment">//建立连接</span></span><br><span class="line"></span><br><span class="line">        TProtocol protocol = <span class="keyword">new</span> TBinaryProtocol(transport);</span><br><span class="line">        Account.Client client = <span class="keyword">new</span> Account.Client(protocol);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//第一个请求， 登录 wuchong 帐号</span></span><br><span class="line">        Request req = <span class="keyword">new</span> Request(<span class="string">"wuchong"</span>, <span class="string">"1234"</span>, Operation.LOGIN);</span><br><span class="line">        request(client, req);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//第二个请求， 注册 wuchong 帐号</span></span><br><span class="line">        req.setOp(Operation.REGISTER);</span><br><span class="line">        request(client, req);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//第三个请求， 登录 wuchong 帐号</span></span><br><span class="line">        req.setOp(Operation.LOGIN);</span><br><span class="line">        request(client, req);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//第四个请求， name 为空的请求</span></span><br><span class="line">        req.setName(<span class="string">""</span>);</span><br><span class="line">        request(client, req);</span><br><span class="line"></span><br><span class="line">        transport.close();  <span class="comment">//关闭连接</span></span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">request</span><span class="params">(Account.Client client, Request req)</span> <span class="keyword">throws</span> TException</span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            String result = client.doAction(req);</span><br><span class="line">            System.out.println(result);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InvalidOperation e) &#123;</span><br><span class="line">            System.out.println(e.reason);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行客户端，其结果如下所示。</p><figure class="highlight smali"><table><tr><td class="code"><pre><span class="line">Login failed!! please<span class="built_in"> check </span>your username<span class="built_in"> and </span>password</span><br><span class="line">Register success!! Hello wuchong</span><br><span class="line">Login success!! Hello wuchong</span><br><span class="line">param name should<span class="built_in"> not </span>be empty</span><br></pre></td></tr></table></figure><p>而此时，服务端会打印出收到的请求信息。</p><figure class="highlight groovy"><table><tr><td class="code"><pre><span class="line">Starting the Account server...</span><br><span class="line">Get request[<span class="string">name:</span>wuchong, <span class="string">pass:</span><span class="number">1234</span>, <span class="string">op:</span><span class="number">1</span>]</span><br><span class="line">Get request[<span class="string">name:</span>wuchong, <span class="string">pass:</span><span class="number">1234</span>, <span class="string">op:</span><span class="number">2</span>]</span><br><span class="line">Get request[<span class="string">name:</span>wuchong, <span class="string">pass:</span><span class="number">1234</span>, <span class="string">op:</span><span class="number">1</span>]</span><br><span class="line">Get request[<span class="string">name:</span>, <span class="string">pass:</span><span class="number">1234</span>, <span class="string">op:</span><span class="number">1</span>]</span><br></pre></td></tr></table></figure><p>你可以发现，只需要几行代码，我们就实现了高效的RPC通信。</p><p>##参考资料</p><ul><li><a href="https://diwakergupta.github.io/thrift-missing-guide/" target="_blank" rel="noopener">Thrift: The Missing Guide</a></li><li><a href="http://thrift-tutorial.readthedocs.org/en/latest/index.html" target="_blank" rel="noopener">Thrift Tutorial</a></li><li><a href="http://www.jianshu.com/p/0f4113d6ec4b" target="_blank" rel="noopener">thrift入门教程</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;a href=&quot;/blog/2015/10/07/thrift-induction&quot;&gt;上一篇文章&lt;/a&gt;我们了解了thrift的概念以及类型系统，本文我们通过一个简单的实例来更深入地了解thrift的使用。我们的实例非常简单，就是实现一个登录注册功能，其用户名密码缓存在内存中。&lt;/p&gt;
&lt;h2 id=&quot;编写thrift文件&quot;&gt;&lt;a href=&quot;#编写thrift文件&quot; class=&quot;headerlink&quot; title=&quot;编写thrift文件&quot;&gt;&lt;/a&gt;编写thrift文件&lt;/h2&gt;&lt;p&gt;我们编写一个&lt;code&gt;account.thrift&lt;/code&gt;的文件。&lt;/p&gt;
&lt;figure class=&quot;highlight cpp&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;namespace&lt;/span&gt; java me.wuchong.thrift.generated&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;enum&lt;/span&gt; Operation&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  LOGIN = &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  REGISTER = &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;struct Request&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;: &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt; name,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;: &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt; password,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;number&quot;&gt;3&lt;/span&gt;: Operation op&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;exception InvalidOperation&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;: i32 code,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;: &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt; reason&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;service Account&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt; doAction(&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;: Request request) throws (&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;: InvalidOperation e);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
    
    </summary>
    
      <category term="程序设计" scheme="http://wuchong.me/categories/%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/"/>
    
    
      <category term="教程" scheme="http://wuchong.me/tags/%E6%95%99%E7%A8%8B/"/>
    
      <category term="thrift" scheme="http://wuchong.me/tags/thrift/"/>
    
  </entry>
  
  <entry>
    <title>Thrift 入门</title>
    <link href="http://wuchong.me/blog/2015/10/07/thrift-induction/"/>
    <id>http://wuchong.me/blog/2015/10/07/thrift-induction/</id>
    <published>2015-10-07T12:08:35.000Z</published>
    <updated>2022-08-03T06:46:44.512Z</updated>
    
    <content type="html"><![CDATA[<h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>Thrift 最初由Facebook开发，而后捐献给Apache，目前已广泛应用于业界。Thrift 正如其<a href="https://thrift.apache.org/" target="_blank" rel="noopener">官方主页</a>介绍的，“是一种可扩展、跨语言的服务开发框架”。简而言之，它主要用于各个服务之间的RPC通信，其服务端和客户端可以用不同的语言来开发。只需要依照IDL（Interface Description Language）定义一次接口，Thrift工具就能自动生成 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml等语言的代码。</p><a id="more"></a><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Thrift的安装还是有些繁琐的，跟着<a href="https://thrift.apache.org/docs/install/" target="_blank" rel="noopener">官方文档</a>的走就可以。如果你是Mac OS X， 这里有更方便的方法。</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">brew install boost</span><br><span class="line">brew install libevent</span><br><span class="line">brew install thrift</span><br><span class="line">gem install thrift -- --with-cppflags='-D_FORTIFY_SOURCE=0'</span><br></pre></td></tr></table></figure><p>不过注意上述方法默认安装的最新版。</p><h2 id="Thrift-类型系统"><a href="#Thrift-类型系统" class="headerlink" title="Thrift 类型系统"></a>Thrift 类型系统</h2><p>thrfit的类型系统包括了基本类型，比如bool, byte, double, string和int。也提供了特殊类型如binary，提供了structs（等同于无继承的class），还提供了容器类型（list,set,map）。</p><h3 id="基本类型-basic-types"><a href="#基本类型-basic-types" class="headerlink" title="基本类型(basic types)"></a>基本类型(basic types)</h3><ul><li>bool: 布尔变量</li><li>byte: 8位有符号整数</li><li>i16: 16位有符号整数</li><li>i32: 32位有符号整数</li><li>i64: 64位有符号整数</li><li>double: 64位浮点数</li><li>string: 字符串</li></ul><p><em>注：thrift不支持无符号整数类型，因为很多编程语言不存在无符号类型，比如java</em></p><h3 id="特殊类型-special-types"><a href="#特殊类型-special-types" class="headerlink" title="特殊类型(special types)"></a>特殊类型(special types)</h3><p>binary: 未编码的字节序列</p><h3 id="枚举-enum"><a href="#枚举-enum" class="headerlink" title="枚举(enum)"></a>枚举(enum)</h3><p>枚举的定义形式和Java的Enum定义差不多，例如：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> Sex &#123;</span><br><span class="line">    MALE,</span><br><span class="line">    FEMALE</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="容器类型-container"><a href="#容器类型-container" class="headerlink" title="容器类型(container)"></a>容器类型(container)</h3><p>集合中的元素可以是除了service之外的任何类型，包括exception。</p><ul><li>list<t>: 一系列由T类型的数据组成的有序列表，元素可以重复</t></li><li>set<t>: 一系列由T类型的数据组成的无序集合，元素不可重复</t></li><li>map&lt;K, V&gt;: 一个字典结构，key为K类型，value为V类型</li></ul><h3 id="结构体-struct"><a href="#结构体-struct" class="headerlink" title="结构体(struct)"></a>结构体(struct)</h3><p>结构体中包含一系列的强类型域，等同于无继承的class。可以看出struct写法很类似C语言的结构体。</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Example</span> &#123;</span></span><br><span class="line">  <span class="number">1</span>:i32 number=<span class="number">10</span>,</span><br><span class="line">  <span class="number">2</span>:i64 bigNumber,</span><br><span class="line">  <span class="number">3</span>:<span class="built_in">list</span>&lt;<span class="keyword">double</span>&gt; decimals,</span><br><span class="line">  <span class="number">4</span>:<span class="built_in">string</span> name=<span class="string">"thrifty"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="可选与必选"><a href="#可选与必选" class="headerlink" title="可选与必选"></a>可选与必选</h3><p>thrift提供两个关键字<code>required</code>，<code>optional</code>，分别用于表示对应的字段时必填的还是可选的。例如：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">People</span> &#123;</span></span><br><span class="line">    <span class="number">1</span>: required <span class="built_in">string</span> name;</span><br><span class="line">    <span class="number">2</span>: optional i32 age;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>表示name是必填的，age是可选的。</p><h3 id="联合-union"><a href="#联合-union" class="headerlink" title="联合(union)"></a>联合(union)</h3><p>当一个结构体中，field之间的关系是互斥的，即只能有一个field被使用被赋值。我们可以用union来声明这个结构体，而不是一堆堆optional的field，语意上也更明确了。例如：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">union</span> JavaObjectArg &#123;</span><br><span class="line">  <span class="number">1</span>: i32 int_arg;</span><br><span class="line">  <span class="number">2</span>: i64 long_arg;</span><br><span class="line">  <span class="number">3</span>: <span class="built_in">string</span> string_arg;</span><br><span class="line">  <span class="number">4</span>: <span class="keyword">bool</span> bool_arg;</span><br><span class="line">  <span class="number">5</span>: binary binary_arg;</span><br><span class="line">  <span class="number">6</span>: <span class="keyword">double</span> double_arg;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="异常-Exceptions"><a href="#异常-Exceptions" class="headerlink" title="异常(Exceptions)"></a>异常(Exceptions)</h3><p>可以自定义异常类型，所定义的异常会继承对应语言的异常基类，例如java，就会继承 <code>java.lang.Exception</code>。</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">exception InvalidOperation &#123;</span><br><span class="line">  <span class="number">1</span>: i32 what,</span><br><span class="line">  <span class="number">2</span>: <span class="built_in">string</span> why</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="服务-service"><a href="#服务-service" class="headerlink" title="服务(service)"></a>服务(service)</h3><p>thrift定义服务相当于Java中创建Interface一样，创建的service经过代码生成命令之后就会生成客户端和服务端的框架代码。定义形式如下：</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">service StringCache &#123;</span><br><span class="line">  void set(1:i32 key, 2:string value),</span><br><span class="line">  string get(1:i32 key) throws (1:KeyNotFound knf),</span><br><span class="line">  <span class="function"><span class="keyword">void</span> <span class="title">delete</span><span class="params">(<span class="number">1</span>:i32 key)</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="命名空间-namespace"><a href="#命名空间-namespace" class="headerlink" title="命名空间(namespace)"></a>命名空间(namespace)</h3><p>thrift的命名空间相当于Java中的package的意思，主要目的是组织代码。thrift使用关键字namespace定义命名空间，例如：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">namespace</span> java me.wuchong.thrift</span><br></pre></td></tr></table></figure><p>注意末尾不能有分号，由此生成的代码，其包路径结构为<code>me.wuchong.thrift</code></p><p><a href="/blog/2015/10/07/thrift-practice">下一篇文章</a>，将通过一个简单的实例来了解thrift的使用。</p><p>##参考资料</p><ul><li><a href="https://diwakergupta.github.io/thrift-missing-guide/" target="_blank" rel="noopener">Thrift: The Missing Guide</a></li><li><a href="http://thrift-tutorial.readthedocs.org/en/latest/index.html" target="_blank" rel="noopener">Thrift Tutorial</a></li><li><a href="http://www.jianshu.com/p/0f4113d6ec4b" target="_blank" rel="noopener">thrift入门教程</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;介绍&quot;&gt;&lt;a href=&quot;#介绍&quot; class=&quot;headerlink&quot; title=&quot;介绍&quot;&gt;&lt;/a&gt;介绍&lt;/h2&gt;&lt;p&gt;Thrift 最初由Facebook开发，而后捐献给Apache，目前已广泛应用于业界。Thrift 正如其&lt;a href=&quot;https://thrift.apache.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;官方主页&lt;/a&gt;介绍的，“是一种可扩展、跨语言的服务开发框架”。简而言之，它主要用于各个服务之间的RPC通信，其服务端和客户端可以用不同的语言来开发。只需要依照IDL（Interface Description Language）定义一次接口，Thrift工具就能自动生成 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml等语言的代码。&lt;/p&gt;
    
    </summary>
    
      <category term="程序设计" scheme="http://wuchong.me/categories/%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/"/>
    
    
      <category term="教程" scheme="http://wuchong.me/tags/%E6%95%99%E7%A8%8B/"/>
    
      <category term="thrift" scheme="http://wuchong.me/tags/thrift/"/>
    
  </entry>
  
  <entry>
    <title>Metrics 是个什么鬼 之入门教程</title>
    <link href="http://wuchong.me/blog/2015/08/01/getting-started-with-metrics/"/>
    <id>http://wuchong.me/blog/2015/08/01/getting-started-with-metrics/</id>
    <published>2015-08-01T08:51:52.000Z</published>
    <updated>2022-08-03T06:46:44.506Z</updated>
    
    <content type="html"><![CDATA[<p>Metrics，谷歌翻译就是度量的意思。当我们需要为某个系统某个服务做监控、做统计，就需要用到Metrics。</p><p>举个栗子，一个图片压缩服务：</p><ol><li>每秒钟的请求数是多少（TPS）？</li><li>平均每个请求处理的时间？</li><li>请求处理的最长耗时？</li><li>等待处理的请求队列长度？</li></ol><p>又或者一个缓存服务：</p><ol><li>缓存的命中率？</li><li>平均查询缓存的时间？</li></ol><p>基本上每一个服务、应用都需要做一个监控系统，这需要尽量以少量的代码，实现统计某类数据的功能。</p><p>以 Java 为例，目前最为流行的 metrics 库是来自 Coda Hale 的 <a href="https://github.com/dropwizard/metrics" target="_blank" rel="noopener">dropwizard/metrics</a>，该库被广泛地应用于各个知名的开源项目中。例如 Hadoop，Kafka，Spark，JStorm 中。 </p><p>本文就结合范例来主要介绍下 dropwizard/metrics 的概念和用法。</p><h2 id="Maven-配置"><a href="#Maven-配置" class="headerlink" title="Maven 配置"></a>Maven 配置</h2><p>我们需要在<code>pom.xml</code>中依赖 <code>metrics-core</code> 包：<br><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.dropwizard.metrics<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>metrics-core<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;metrics.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p><em>注：在POM文件中需要声明 <code>${metrics.version}</code> 的具体版本号，如 3.1.0</em></p><h2 id="Metric-Registries"><a href="#Metric-Registries" class="headerlink" title="Metric Registries"></a>Metric Registries</h2><p><code>MetricRegistry</code>类是Metrics的核心，它是存放应用中所有metrics的容器。也是我们使用 Metrics 库的起点。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">MetricRegistry registry = <span class="keyword">new</span> MetricRegistry();</span><br></pre></td></tr></table></figure><p>每一个 metric 都有它独一无二的名字，Metrics 中使用句点名字，如 com.example.Queue.size。当你在 com.example.Queue 下有两个 metric 实例，可以指定地更具体：com.example.Queue.requests.size 和 com.example.Queue.response.size 。使用<code>MetricRegistry</code>类，可以非常方便地生成名字。</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">MetricRegistry.name(Queue.class, "requests", "size")</span><br><span class="line">MetricRegistry.name(Queue.class, "responses", "size")</span><br></pre></td></tr></table></figure><h2 id="Metrics-数据展示"><a href="#Metrics-数据展示" class="headerlink" title="Metrics 数据展示"></a>Metrics 数据展示</h2><p>Metircs 提供了 Report 接口，用于展示 metrics 获取到的统计数据。<code>metrics-core</code>中主要实现了四种 reporter： <a href="http://metrics.dropwizard.io/3.1.0/manual/core/#man-core-reporters-jmx" target="_blank" rel="noopener">JMX</a>, <a href="http://metrics.dropwizard.io/3.1.0/manual/core/#man-core-reporters-console" target="_blank" rel="noopener">console</a>, <a href="http://metrics.dropwizard.io/3.1.0/manual/core/#man-core-reporters-slf4j" target="_blank" rel="noopener">SLF4J</a>, 和 <a href="http://metrics.dropwizard.io/3.1.0/manual/core/#man-core-reporters-csv" target="_blank" rel="noopener">CSV</a>。 在本文的例子中，我们使用 ConsoleReporter 。</p><h2 id="五种-Metrics-类型"><a href="#五种-Metrics-类型" class="headerlink" title="五种 Metrics 类型"></a>五种 Metrics 类型</h2><h3 id="Gauges"><a href="#Gauges" class="headerlink" title="Gauges"></a>Gauges</h3><p>最简单的度量指标，只有一个简单的返回值，例如，我们想衡量一个待处理队列中任务的个数，代码如下：<br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">GaugeTest</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Queue&lt;String&gt; q = <span class="keyword">new</span> LinkedList&lt;String&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        MetricRegistry registry = <span class="keyword">new</span> MetricRegistry();</span><br><span class="line">        ConsoleReporter reporter = ConsoleReporter.forRegistry(registry).build();</span><br><span class="line">        reporter.start(<span class="number">1</span>, TimeUnit.SECONDS);</span><br><span class="line"></span><br><span class="line">        registry.register(MetricRegistry.name(GaugeTest.class, "queue", "size"), </span><br><span class="line">        <span class="keyword">new</span> Gauge&lt;Integer&gt;() &#123;</span><br><span class="line"></span><br><span class="line">            <span class="function"><span class="keyword">public</span> Integer <span class="title">getValue</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> q.size();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span>(<span class="keyword">true</span>)&#123;</span><br><span class="line">            Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">            q.add(<span class="string">"Job-xxx"</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>运行之后的结果如下：<br><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line">-- Gauges ------------------------------------------------</span><br><span class="line">com<span class="selector-class">.alibaba</span><span class="selector-class">.wuchong</span><span class="selector-class">.metrics</span><span class="selector-class">.GaugeTest</span><span class="selector-class">.queue</span>.size</span><br><span class="line">             value = <span class="number">6</span></span><br></pre></td></tr></table></figure></p><p>其中第7行和第8行添加了ConsoleReporter，可以每秒钟将度量指标打印在屏幕上，理解起来会更清楚。</p><p>但是对于大多数队列数据结构，我们并不想简单地返回<code>queue.size()</code>，因为<code>java.util</code>和<code>java.util.concurrent</code>中实现的<code>#size()</code>方法很多都是 <strong>O(n)</strong> 的复杂度，这会影响 Gauge 的性能。</p><h3 id="Counters"><a href="#Counters" class="headerlink" title="Counters"></a>Counters</h3><p>Counter 就是计数器，Counter 只是用 Gauge 封装了 <code>AtomicLong</code> 。我们可以使用如下的方法，使得获得队列大小更加高效。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CounterTest</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Queue&lt;String&gt; q = <span class="keyword">new</span> LinkedBlockingQueue&lt;String&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Counter pendingJobs;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Random random = <span class="keyword">new</span> Random();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">addJob</span><span class="params">(String job)</span> </span>&#123;</span><br><span class="line">        pendingJobs.inc();</span><br><span class="line">        q.offer(job);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">takeJob</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        pendingJobs.dec();</span><br><span class="line">        <span class="keyword">return</span> q.poll();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        MetricRegistry registry = <span class="keyword">new</span> MetricRegistry();</span><br><span class="line">        ConsoleReporter reporter = ConsoleReporter.forRegistry(registry).build();</span><br><span class="line">        reporter.start(<span class="number">1</span>, TimeUnit.SECONDS);</span><br><span class="line"></span><br><span class="line">        pendingJobs = registry.counter(MetricRegistry.name(Queue.class,"pending-jobs","size"));</span><br><span class="line"></span><br><span class="line">        <span class="keyword">int</span> num = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">while</span>(<span class="keyword">true</span>)&#123;</span><br><span class="line">            Thread.sleep(<span class="number">200</span>);</span><br><span class="line">            <span class="keyword">if</span> (random.nextDouble() &gt; <span class="number">0.7</span>)&#123;</span><br><span class="line">                String job = takeJob();</span><br><span class="line">                System.out.println(<span class="string">"take job : "</span>+job);</span><br><span class="line">            &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">                String job = <span class="string">"Job-"</span>+num;</span><br><span class="line">                addJob(job);</span><br><span class="line">                System.out.println(<span class="string">"add job : "</span>+job);</span><br><span class="line">            &#125;</span><br><span class="line">            num++;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行之后的结果大致如下：<br><figure class="highlight mipsasm"><table><tr><td class="code"><pre><span class="line"><span class="keyword">add </span><span class="keyword">job </span>: <span class="keyword">Job-15</span></span><br><span class="line"><span class="keyword">add </span><span class="keyword">job </span>: <span class="keyword">Job-16</span></span><br><span class="line"><span class="keyword">take </span><span class="keyword">job </span>: <span class="keyword">Job-8</span></span><br><span class="line"><span class="keyword">take </span><span class="keyword">job </span>: <span class="keyword">Job-10</span></span><br><span class="line"><span class="keyword">add </span><span class="keyword">job </span>: <span class="keyword">Job-19</span></span><br><span class="line"><span class="keyword">15-8-1 </span><span class="number">16</span>:<span class="number">11</span>:<span class="number">31</span> ============================================</span><br><span class="line">-- Counters ----------------------------------------------</span><br><span class="line"><span class="keyword">java.util.Queue.pending-jobs.size</span></span><br><span class="line"><span class="keyword"> </span>            <span class="built_in">count</span> = <span class="number">5</span></span><br></pre></td></tr></table></figure></p><h3 id="Meters"><a href="#Meters" class="headerlink" title="Meters"></a>Meters</h3><p>Meter度量一系列事件发生的速率(rate)，例如TPS。Meters会统计最近1分钟，5分钟，15分钟，还有全部时间的速率。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MeterTest</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Random random = <span class="keyword">new</span> Random();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">request</span><span class="params">(Meter meter)</span></span>&#123;</span><br><span class="line">        System.out.println(<span class="string">"request"</span>);</span><br><span class="line">        meter.mark();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">request</span><span class="params">(Meter meter, <span class="keyword">int</span> n)</span></span>&#123;</span><br><span class="line">        <span class="keyword">while</span>(n &gt; <span class="number">0</span>)&#123;</span><br><span class="line">            request(meter);</span><br><span class="line">            n--;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        MetricRegistry registry = <span class="keyword">new</span> MetricRegistry();</span><br><span class="line">        ConsoleReporter reporter = ConsoleReporter.forRegistry(registry).build();</span><br><span class="line">        reporter.start(<span class="number">1</span>, TimeUnit.SECONDS);</span><br><span class="line"></span><br><span class="line">        Meter meterTps = registry.meter(MetricRegistry.name(MeterTest.class,"request","tps"));</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span>(<span class="keyword">true</span>)&#123;</span><br><span class="line">            request(meterTps,random.nextInt(<span class="number">5</span>));</span><br><span class="line">            Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行结果大致如下：<br><figure class="highlight excel"><table><tr><td class="code"><pre><span class="line">request</span><br><span class="line"><span class="number">15</span>-<span class="number">8</span>-<span class="number">1</span> <span class="symbol">16:23</span><span class="symbol">:25</span> ============================================</span><br><span class="line"></span><br><span class="line">-- Meters ------------------------------------------------</span><br><span class="line">com.alibaba.wuchong.metrics.MeterTest.request.tps</span><br><span class="line">             <span class="built_in">count</span> = <span class="number">134</span></span><br><span class="line">         mean <span class="built_in">rate</span> = <span class="number">2.13</span> events/<span class="built_in">second</span></span><br><span class="line">     <span class="number">1</span>-<span class="built_in">minute</span> <span class="built_in">rate</span> = <span class="number">2.52</span> events/<span class="built_in">second</span></span><br><span class="line">     <span class="number">5</span>-<span class="built_in">minute</span> <span class="built_in">rate</span> = <span class="number">3.16</span> events/<span class="built_in">second</span></span><br><span class="line">    <span class="number">15</span>-<span class="built_in">minute</span> <span class="built_in">rate</span> = <span class="number">3.32</span> events/<span class="built_in">second</span></span><br></pre></td></tr></table></figure></p><p><em>注：非常像 Unix 系统中 uptime 和 top 中的 load。</em></p><h3 id="Histograms"><a href="#Histograms" class="headerlink" title="Histograms"></a>Histograms</h3><p>Histogram统计数据的分布情况。比如最小值，最大值，中间值，还有中位数，75百分位, 90百分位, 95百分位, 98百分位, 99百分位, 和 99.9百分位的值(percentiles)。</p><p>比如request的大小的分布：<br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">HistogramTest</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Random random = <span class="keyword">new</span> Random();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        MetricRegistry registry = <span class="keyword">new</span> MetricRegistry();</span><br><span class="line">        ConsoleReporter reporter = ConsoleReporter.forRegistry(registry).build();</span><br><span class="line">        reporter.start(<span class="number">1</span>, TimeUnit.SECONDS);</span><br><span class="line"></span><br><span class="line">        Histogram histogram = <span class="keyword">new</span> Histogram(<span class="keyword">new</span> ExponentiallyDecayingReservoir());</span><br><span class="line">        registry.register(MetricRegistry.name(HistogramTest.class, "request", "histogram"), histogram);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">while</span>(<span class="keyword">true</span>)&#123;</span><br><span class="line">            Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">            histogram.update(random.nextInt(<span class="number">100000</span>));</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>运行之后结果大致如下：<br><figure class="highlight lsl"><table><tr><td class="code"><pre><span class="line">-- Histograms --------------------------------------------</span><br><span class="line">java.util.Queue.queue.histogram</span><br><span class="line">             count = <span class="number">56</span></span><br><span class="line">               min = <span class="number">1122</span></span><br><span class="line">               max = <span class="number">99650</span></span><br><span class="line">              mean = <span class="number">48735.12</span></span><br><span class="line">            stddev = <span class="number">28609.02</span></span><br><span class="line">            median = <span class="number">49493.00</span></span><br><span class="line">              <span class="number">75</span>% &lt;= <span class="number">72323.00</span></span><br><span class="line">              <span class="number">95</span>% &lt;= <span class="number">90773.00</span></span><br><span class="line">              <span class="number">98</span>% &lt;= <span class="number">94011.00</span></span><br><span class="line">              <span class="number">99</span>% &lt;= <span class="number">99650.00</span></span><br><span class="line">            <span class="number">99.9</span>% &lt;= <span class="number">99650.00</span></span><br></pre></td></tr></table></figure></p><h3 id="Timers"><a href="#Timers" class="headerlink" title="Timers"></a>Timers</h3><p>Timer其实是 Histogram 和 Meter 的结合， histogram 某部分代码/调用的耗时， meter统计TPS。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TimerTest</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Random random = <span class="keyword">new</span> Random();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        MetricRegistry registry = <span class="keyword">new</span> MetricRegistry();</span><br><span class="line">        ConsoleReporter reporter = ConsoleReporter.forRegistry(registry).build();</span><br><span class="line">        reporter.start(<span class="number">1</span>, TimeUnit.SECONDS);</span><br><span class="line"></span><br><span class="line">        Timer timer = registry.timer(MetricRegistry.name(TimerTest.class,"get-latency"));</span><br><span class="line"></span><br><span class="line">        Timer.Context ctx;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span>(<span class="keyword">true</span>)&#123;</span><br><span class="line">            ctx = timer.time();</span><br><span class="line">            Thread.sleep(random.nextInt(<span class="number">1000</span>));</span><br><span class="line">            ctx.stop();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行之后结果如下：<br><figure class="highlight lsl"><table><tr><td class="code"><pre><span class="line">-- Timers ------------------------------------------------</span><br><span class="line">com.alibaba.wuchong.metrics.TimerTest.get-latency</span><br><span class="line">             count = <span class="number">38</span></span><br><span class="line">         mean rate = <span class="number">1.90</span> calls/second</span><br><span class="line">     <span class="number">1</span>-minute rate = <span class="number">1.66</span> calls/second</span><br><span class="line">     <span class="number">5</span>-minute rate = <span class="number">1.61</span> calls/second</span><br><span class="line">    <span class="number">15</span>-minute rate = <span class="number">1.60</span> calls/second</span><br><span class="line">               min = <span class="number">13.90</span> milliseconds</span><br><span class="line">               max = <span class="number">988.71</span> milliseconds</span><br><span class="line">              mean = <span class="number">519.21</span> milliseconds</span><br><span class="line">            stddev = <span class="number">286.23</span> milliseconds</span><br><span class="line">            median = <span class="number">553.84</span> milliseconds</span><br><span class="line">              <span class="number">75</span>% &lt;= <span class="number">763.64</span> milliseconds</span><br><span class="line">              <span class="number">95</span>% &lt;= <span class="number">943.27</span> milliseconds</span><br><span class="line">              <span class="number">98</span>% &lt;= <span class="number">988.71</span> milliseconds</span><br><span class="line">              <span class="number">99</span>% &lt;= <span class="number">988.71</span> milliseconds</span><br><span class="line">            <span class="number">99.9</span>% &lt;= <span class="number">988.71</span> milliseconds</span><br></pre></td></tr></table></figure></p><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>初次之外，Metrics还提供了 <a href="http://metrics.dropwizard.io/3.1.0/manual/healthchecks/" target="_blank" rel="noopener">HealthCheck</a> 用来检测某个某个系统是否健康，例如数据库连接是否正常。还有<a href="https://dropwizard.github.io/metrics/3.1.0/apidocs/com/codahale/metrics/annotation/package-tree.html" target="_blank" rel="noopener">Metrics Annotation</a>，可以很方便地实现统计某个方法，某个值的数据。感兴趣的可以点进链接看看。</p><h2 id="使用经验总结"><a href="#使用经验总结" class="headerlink" title="使用经验总结"></a>使用经验总结</h2><p>一般情况下，当我们需要统计某个函数被调用的频率（TPS），会使用Meters。当我们需要统计某个函数的执行耗时时，会使用Histograms。当我们既要统计TPS又要统计耗时时，我们会使用Timers。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="http://metrics.dropwizard.io/3.1.0/manual/core/" target="_blank" rel="noopener">Metrics Core</a></li><li><a href="http://metrics.dropwizard.io/3.1.0/getting-started/" target="_blank" rel="noopener">Metrics Getting Started</a></li></ul><p>本文代码已上传至<a href="https://github.com/wuchong/metrics-demo" target="_blank" rel="noopener">GitHub</a>。</p>]]></content>
    
    <summary type="html">
    
      Metrics，谷歌翻译就是度量的意思。当我们需要为某个系统某个服务做监控、做统计，就需要用到Metrics。
    
    </summary>
    
      <category term="程序设计" scheme="http://wuchong.me/categories/%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/"/>
    
    
      <category term="metrics" scheme="http://wuchong.me/tags/metrics/"/>
    
      <category term="教程" scheme="http://wuchong.me/tags/%E6%95%99%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>使用Redis和SQLAlchemy对Scrapy Item去重并存储</title>
    <link href="http://wuchong.me/blog/2015/05/22/using-redis-and-sqlalchemy-to-checkd-dup-and-store-scrapy-item/"/>
    <id>http://wuchong.me/blog/2015/05/22/using-redis-and-sqlalchemy-to-checkd-dup-and-store-scrapy-item/</id>
    <published>2015-05-22T15:08:53.000Z</published>
    <updated>2022-08-03T06:46:44.513Z</updated>
    
    <content type="html"><![CDATA[<p>在上篇<a href="/blog/2015/05/22/running-scrapy-dynamic-and-configurable">博客</a>中，我们讲解了如何通过维护多个网站的爬取规则来抓取各个网站的数据。本文将简要地谈谈如何使用Scrapy的<a href="https://scrapy-chs.readthedocs.org/zh_CN/0.24/topics/item-pipeline.html" target="_blank" rel="noopener">Item Pipline</a>将爬取的数据去重并存储到数据库中。</p><p>Scrapy框架的高度灵活性得益于其数据管道的架构设计，开发者可以通过简单的配置就能轻松地添加新特性。我们可以通过如下的方式添加一个pipline。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">settings.set(<span class="string">"ITEM_PIPELINES"</span>, &#123;<span class="string">'pipelines.DataBasePipeline'</span>: <span class="number">300</span>&#125;)</span><br></pre></td></tr></table></figure><p>这里<code>ITEM_PIPELINES</code>是一个Python字典，其中key保存的pipline类在项目中的位置，value为整型值，确定了他们运行的顺序，item按数字从低到高的顺序，通过pipeline，通常将这些数字定义在0-1000范围内。<br><a id="more"></a></p><h2 id="存储到数据库"><a href="#存储到数据库" class="headerlink" title="存储到数据库"></a>存储到数据库</h2><p>在上一篇<a href="/blog/2015/05/22/running-scrapy-dynamic-and-configurable">博客</a>中，我们已经介绍了使用<a href="http://www.sqlalchemy.org/" target="_blank" rel="noopener">SQLAlchemy</a> 作为我们的ORM。同样的，为了将爬取的文章保存到数据库，我们先要有一个<code>Article</code>模型，包含了 URL，标题，正文等字段。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> sqlalchemy <span class="keyword">import</span> Column, String , DateTime, Integer</span><br><span class="line"><span class="keyword">from</span> sqlalchemy.ext.declarative <span class="keyword">import</span> declarative_base</span><br><span class="line"></span><br><span class="line">Base = declarative_base()</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Article</span><span class="params">(Base)</span>:</span></span><br><span class="line">    __tablename__ = <span class="string">'articles'</span></span><br><span class="line"></span><br><span class="line">    id = Column(Integer, primary_key=<span class="literal">True</span>)</span><br><span class="line">    title = Column(String)</span><br><span class="line">    url = Column(String)</span><br><span class="line">    body = Column(String)</span><br><span class="line">    publish_time = Column(DateTime)</span><br><span class="line">    source_site = Column(String)</span><br></pre></td></tr></table></figure><p>之后在<code>DataBasePipeline</code>中，我们需要生成<code>Aticle</code>对象，并将item中对应的字段赋给<code>Aticle</code>对象，最后通过SQLAlchemy将文章插入到数据库中。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> model.config <span class="keyword">import</span> DBSession</span><br><span class="line"><span class="keyword">from</span> model.article <span class="keyword">import</span> Article</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">DataBasePipeline</span><span class="params">(object)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">open_spider</span><span class="params">(self, spider)</span>:</span></span><br><span class="line">        self.session = DBSession()</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">process_item</span><span class="params">(self, item, spider)</span>:</span></span><br><span class="line">        a = Article(title=item[<span class="string">"title"</span>].encode(<span class="string">"utf-8"</span>),</span><br><span class="line">                    url=item[<span class="string">"url"</span>],</span><br><span class="line">                    body=item[<span class="string">"body"</span>].encode(<span class="string">"utf-8"</span>),</span><br><span class="line">                    publish_time=item[<span class="string">"publish_time"</span>].encode(<span class="string">"utf-8"</span>),</span><br><span class="line">                    source_site=item[<span class="string">"source_site"</span>].encode(<span class="string">"utf-8"</span>))</span><br><span class="line">        self.session.add(a)</span><br><span class="line">        self.session.commit()</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">close_spider</span><span class="params">(self,spider)</span>:</span></span><br><span class="line">        self.session.close()</span><br></pre></td></tr></table></figure><h2 id="使用Redis去重"><a href="#使用Redis去重" class="headerlink" title="使用Redis去重"></a>使用Redis去重</h2><p>为了防止同一个网页爬取两遍，我们使用Redis来去重，因为 Redis 作为Key/Value数据库在这个场景是非常适合的。我们认为一个URL能唯一代表一个网页。所以使用URL作为键值存储。</p><p>我们希望在存储之前就进行去重操作，所以需要更改下<code>ITEM_PIPELINES</code>的配置。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">settings.set(<span class="string">"ITEM_PIPELINES"</span> , &#123;</span><br><span class="line">    <span class="string">'pipelines.DuplicatesPipeline'</span>: <span class="number">200</span>,</span><br><span class="line">    <span class="string">'pipelines.DataBasePipeline'</span>: <span class="number">300</span>,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><code>DuplicatesPipeline</code>长这个样子。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> scrapy.exceptions <span class="keyword">import</span> DropItem</span><br><span class="line"><span class="keyword">from</span> model.config <span class="keyword">import</span> Redis</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">DuplicatesPipeline</span><span class="params">(object)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">process_item</span><span class="params">(self, item, spider)</span>:</span></span><br><span class="line">        <span class="keyword">if</span> Redis.exists(<span class="string">'url:%s'</span> % item[<span class="string">'url'</span>]):</span><br><span class="line">            <span class="keyword">raise</span> DropItem(<span class="string">"Duplicate item found: %s"</span> % item)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            Redis.set(<span class="string">'url:%s'</span> % item[<span class="string">'url'</span>],<span class="number">1</span>)</span><br><span class="line">            <span class="keyword">return</span> item</span><br></pre></td></tr></table></figure><p>当检测到Item已经存在，会抛出DropItem 异常，被丢弃的item将不会被之后的pipeline组件所处理。</p><p>最后，运行脚本，你能看到我们的程序欢快地跑起来了。</p><figure class="highlight dockerfile"><table><tr><td class="code"><pre><span class="line">python <span class="keyword">run</span>.<span class="bash">py</span></span><br></pre></td></tr></table></figure><p>你可以在 <a href="https://github.com/wuchong/scrapy-dynamic-configurable/tree/scrapy-0.24" target="_blank" rel="noopener">GitHub</a> 上看到本文的完整项目。</p><p><em>注：本文使用的 Scrapy 版本是 0.24，<a href="https://github.com/wuchong/scrapy-dynamic-configurable" target="_blank" rel="noopener">GitHub</a> 上的master分支已支持 Scrapy 1.0</em></p><p><strong>本系列的三篇文章</strong></p><ol><li><a href="/blog/2015/05/22/running-scrapy-programmatically">编程方式下运行 Scrapy spider</a></li><li><a href="/blog/2015/05/22/running-scrapy-dynamic-and-configurable">使用Scrapy定制可动态配置的爬虫</a></li><li><a href="/blog/2015/05/22/using-redis-and-sqlalchemy-to-checkd-dup-and-store-scrapy-item">使用Redis和SQLAlchemy对Scrapy Item去重并存储</a></li></ol>]]></content>
    
    <summary type="html">
    
      本文将简要地谈谈如何使用Scrapy的Item Pipline，基于Redis和SQLALchemy将爬取的数据去重并存储到数据库中。
    
    </summary>
    
      <category term="程序设计" scheme="http://wuchong.me/categories/%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/"/>
    
    
      <category term="爬虫" scheme="http://wuchong.me/tags/%E7%88%AC%E8%99%AB/"/>
    
      <category term="scrapy" scheme="http://wuchong.me/tags/scrapy/"/>
    
      <category term="redis" scheme="http://wuchong.me/tags/redis/"/>
    
  </entry>
  
  <entry>
    <title>使用Scrapy定制可动态配置的爬虫</title>
    <link href="http://wuchong.me/blog/2015/05/22/running-scrapy-dynamic-and-configurable/"/>
    <id>http://wuchong.me/blog/2015/05/22/running-scrapy-dynamic-and-configurable/</id>
    <published>2015-05-22T14:22:20.000Z</published>
    <updated>2022-08-03T06:46:44.511Z</updated>
    
    <content type="html"><![CDATA[<p>本文紧接上篇<a href="/blog/2015/05/22/running-scrapy-programmatically">博客</a>，在上一篇博客中我们讲解了如何使用编程的方式运行Scrapy spider。本文将讲解如何通过维护多个网站的爬取规则来抓取各个网站的数据。</p><p>具体要实现的目标是这样的，有一张<code>Rule</code>表用来存储各个网站的爬取规则，Scrapy获取<code>Rule</code>表中的记录后，针对每一条rule自动生成一个spider，每个spider去爬它们各自网站的数据。这样我们只需要维护Rule表中的规则（可以写个Web程序来维护），而不用针对上千个网站写上千个spider文件了。</p><p>我们使用 <a href="http://www.sqlalchemy.org/" target="_blank" rel="noopener">SQLAlchemy</a> 来映射数据库，Rule表的结构如下：<br><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> sqlalchemy <span class="keyword">import</span> Column, String , DateTime, Integer</span><br><span class="line"><span class="keyword">from</span> sqlalchemy.ext.declarative <span class="keyword">import</span> declarative_base</span><br><span class="line"></span><br><span class="line">Base = declarative_base()</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Rule</span><span class="params">(Base)</span>:</span></span><br><span class="line">    __tablename__ = <span class="string">'rules'</span></span><br><span class="line"></span><br><span class="line">    id = Column(Integer, primary_key=<span class="literal">True</span>)</span><br><span class="line">    name = Column(String)</span><br><span class="line">    allow_domains = Column(String)</span><br><span class="line">    start_urls = Column(String)</span><br><span class="line">    next_page = Column(String)</span><br><span class="line">    allow_url = Column(String)</span><br><span class="line">    extract_from = Column(String)</span><br><span class="line">    title_xpath = Column(String)</span><br><span class="line">    body_xpath = Column(String)</span><br><span class="line">    publish_time_xpath = Column(String)</span><br><span class="line">    source_site_xpath = Column(String)</span><br><span class="line">    enable = Column(Integer)</span><br></pre></td></tr></table></figure></p><a id="more"></a><p>接下来我们要重新定制我们的spider，命名为<code>DeepSpider</code>，让他能够通过rule参数初始化。我们令<code>DeepSpider</code>继承自 <a href="https://scrapy-chs.readthedocs.org/zh_CN/0.24/topics/spiders.html#crawlspider" target="_blank" rel="noopener"><code>CrawlSpider</code></a>，一个提供了更多强大的规则(rule)来提供跟进link功能的类。<code>deep_spider.py</code>长这个样子：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># -*- coding: utf-8 -*-</span></span><br><span class="line"><span class="keyword">import</span> scrapy</span><br><span class="line"><span class="keyword">from</span> scrapy.contrib.spiders <span class="keyword">import</span> CrawlSpider, Rule</span><br><span class="line"><span class="keyword">from</span> scrapy.contrib.linkextractors <span class="keyword">import</span> LinkExtractor</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Article</span><span class="params">(scrapy.Item)</span>:</span></span><br><span class="line">    title = scrapy.Field()</span><br><span class="line">    url = scrapy.Field()</span><br><span class="line">    body = scrapy.Field()</span><br><span class="line">    publish_time = scrapy.Field()</span><br><span class="line">    source_site = scrapy.Field()</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">DeepSpider</span><span class="params">(CrawlSpider)</span>:</span></span><br><span class="line">    name = <span class="string">"Deep"</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self,rule)</span>:</span></span><br><span class="line">        self.rule = rule</span><br><span class="line">        self.name = rule.name</span><br><span class="line">        self.allowed_domains = rule.allow_domains.split(<span class="string">","</span>)</span><br><span class="line">        self.start_urls = rule.start_urls.split(<span class="string">","</span>)</span><br><span class="line">        rule_list = []</span><br><span class="line">        <span class="comment">#添加`下一页`的规则</span></span><br><span class="line">        <span class="keyword">if</span> rule.next_page:</span><br><span class="line">            rule_list.append(Rule(LinkExtractor(restrict_xpaths = rule.next_page)))</span><br><span class="line">        <span class="comment">#添加抽取文章链接的规则</span></span><br><span class="line">        rule_list.append(Rule(LinkExtractor(</span><br><span class="line">            allow=[rule.allow_url],</span><br><span class="line">            restrict_xpaths = [rule.extract_from]),</span><br><span class="line">            callback=<span class="string">'parse_item'</span>))</span><br><span class="line">        self.rules = tuple(rule_list)</span><br><span class="line">        super(DeepSpider, self).__init__()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">parse_item</span><span class="params">(self, response)</span>:</span></span><br><span class="line">        self.log(<span class="string">'Hi, this is an article page! %s'</span> % response.url)</span><br><span class="line"></span><br><span class="line">        article = Article()</span><br><span class="line"></span><br><span class="line">        article[<span class="string">"url"</span>] = response.url</span><br><span class="line"></span><br><span class="line">        title = response.xpath(self.rule.title_xpath).extract()</span><br><span class="line">        article[<span class="string">"title"</span>] = title[<span class="number">0</span>] <span class="keyword">if</span> title <span class="keyword">else</span> <span class="string">""</span></span><br><span class="line"></span><br><span class="line">        body = response.xpath(self.rule.body_xpath).extract()</span><br><span class="line">        article[<span class="string">"body"</span>] =  <span class="string">'\n'</span>.join(body) <span class="keyword">if</span> body <span class="keyword">else</span> <span class="string">""</span></span><br><span class="line"></span><br><span class="line">        publish_time = response.xpath(self.rule.publish_time_xpath).extract()</span><br><span class="line">        article[<span class="string">"publish_time"</span>] = publish_time[<span class="number">0</span>] <span class="keyword">if</span> publish_time <span class="keyword">else</span> <span class="string">""</span></span><br><span class="line"></span><br><span class="line">        source_site = response.xpath(self.rule.source_site_xpath).extract()</span><br><span class="line">        article[<span class="string">"source_site"</span>] = source_site[<span class="number">0</span>] <span class="keyword">if</span> source_site <span class="keyword">else</span> <span class="string">""</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> article</span><br></pre></td></tr></table></figure><p>要注意的是<code>start_urls</code>，<code>rules</code>等都初始化成了对象的属性，都由传入的<code>rule</code>对象初始化，<code>parse_item</code>方法中的抽取规则也都有<code>rule</code>对象提供。</p><p>为了同时运行多个spider，我们需要稍稍修改上节中的运行脚本<code>run.py</code>，如下所示：</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="comment"># -*- coding: utf-8 -*-</span></span><br><span class="line"><span class="keyword">from</span> spiders.deep_spider import DeepSpider</span><br><span class="line"><span class="keyword">from</span> model.config import DBSession</span><br><span class="line"><span class="keyword">from</span> model.rule import Rule</span><br><span class="line"></span><br><span class="line"><span class="comment"># scrapy api</span></span><br><span class="line"><span class="keyword">from</span> scrapy import signals, log</span><br><span class="line"><span class="keyword">from</span> twisted.internet import reactor</span><br><span class="line"><span class="keyword">from</span> scrapy.crawler import Crawler</span><br><span class="line"><span class="keyword">from</span> scrapy.settings import Settings</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">RUNNING_CRAWLERS = []</span><br><span class="line"></span><br><span class="line">def spider_closing(spider):</span><br><span class="line">    <span class="string">""</span><span class="string">"Activates on spider closed signal"</span><span class="string">""</span></span><br><span class="line">    log.msg(<span class="string">"Spider closed: %s"</span> % spider, <span class="attribute">level</span>=log.INFO)</span><br><span class="line">    RUNNING_CRAWLERS.<span class="builtin-name">remove</span>(spider)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> RUNNING_CRAWLERS:</span><br><span class="line">        reactor.stop()</span><br><span class="line"></span><br><span class="line">log.start(<span class="attribute">loglevel</span>=log.DEBUG)</span><br><span class="line"></span><br><span class="line">settings = Settings()</span><br><span class="line"></span><br><span class="line"><span class="comment"># crawl settings</span></span><br><span class="line">settings.<span class="builtin-name">set</span>(<span class="string">"USER_AGENT"</span>, <span class="string">"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36"</span>)</span><br><span class="line"></span><br><span class="line">db = DBSession()</span><br><span class="line">rules = db.query(Rule).filter(Rule.<span class="builtin-name">enable</span> == 1)</span><br><span class="line"><span class="keyword">for</span> rule <span class="keyword">in</span> rules:</span><br><span class="line">    crawler = Crawler(settings)</span><br><span class="line">    spider = DeepSpider(rule)  # instantiate every spider using rule</span><br><span class="line">    RUNNING_CRAWLERS.append(spider)</span><br><span class="line"></span><br><span class="line">    # stop reactor when spider closes</span><br><span class="line">    crawler.signals.connect(spider_closing, <span class="attribute">signal</span>=signals.spider_closed)</span><br><span class="line">    crawler.configure()</span><br><span class="line">    crawler.crawl(spider)</span><br><span class="line">    crawler.start()</span><br><span class="line"></span><br><span class="line"><span class="comment"># blocks process so always keep as the last statement</span></span><br><span class="line">reactor.<span class="builtin-name">run</span>()</span><br></pre></td></tr></table></figure><p>我们从数据库中查出启用的rules，并对于rules中每一个规则实例化一个<code>DeepSpider</code>对象。这儿的一个小技巧是建立了一个<code>RUNNING_CRAWLERS</code>列表，新建立的<code>DeepSpider</code>对象 spider 都会加入这个队列。在 spider 运行完毕时会调用<code>spider_closing</code>方法，并将该spider从<code>RUNNING_CRAWLERS</code>移除。最终，<code>RUNNING_CRAWLERS</code>中没有任何spider了，我们会停止脚本。</p><p>运行<code>run.py</code>后，就能对Rule表中网站进行爬取了，但是我们现在还没有对爬下来的结果进行存储，所以看不到结果。下一篇<a href="/blog/2015/05/22/using-redis-and-sqlalchemy-to-checkd-dup-and-store-scrapy-item">博客</a>，我们将使用 Scrapy 提供的强大的 Pipline 对数据进行保存并去重。</p><p>现在我们可以往Rule表中加入成百上千个网站的规则，而不用添加一行代码，就可以对这成百上千个网站进行爬取。当然你完全可以做一个Web前端来完成维护Rule表的任务。当然Rule规则也可以放在除了数据库的任何地方，比如配置文件。</p><p><em>由于本人刚接触 Scrapy 不久，如有理解不当之处或是更好的解决方案，还请不吝赐教 :)</em></p><p>你可以在 <a href="https://github.com/wuchong/scrapy-dynamic-configurable/tree/scrapy-0.24" target="_blank" rel="noopener">GitHub</a> 上看到本文的完整项目。</p><p><em>注：本文使用的 Scrapy 版本是 0.24，<a href="https://github.com/wuchong/scrapy-dynamic-configurable" target="_blank" rel="noopener">GitHub</a> 上的master分支已支持 Scrapy 1.0</em></p><p><strong>本系列的三篇文章</strong></p><ol><li><a href="/blog/2015/05/22/running-scrapy-programmatically">编程方式下运行 Scrapy spider</a></li><li><a href="/blog/2015/05/22/running-scrapy-dynamic-and-configurable">使用Scrapy定制可动态配置的爬虫</a></li><li><a href="/blog/2015/05/22/using-redis-and-sqlalchemy-to-checkd-dup-and-store-scrapy-item">使用Redis和SQLAlchemy对Scrapy Item去重并存储</a></li></ol><p>###参考资料</p><ul><li><a href="http://kirankoduru.github.io/python/multiple-scrapy-spiders.html" target="_blank" rel="noopener">Running multiple scrapy spiders programmatically</a></li></ul>]]></content>
    
    <summary type="html">
    
      在上一篇博客中我们讲解了如何使用编程的方式运行Scrapy spider。本文将讲解如何通过维护多个网站的爬取规则来抓取各个网站的数据。本文将讲解如何通过维护多个网站的爬取规则来抓取各个网站的数据
    
    </summary>
    
      <category term="程序设计" scheme="http://wuchong.me/categories/%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/"/>
    
    
      <category term="爬虫" scheme="http://wuchong.me/tags/%E7%88%AC%E8%99%AB/"/>
    
      <category term="scrapy" scheme="http://wuchong.me/tags/scrapy/"/>
    
  </entry>
  
  <entry>
    <title>编程方式下运行 Scrapy spider</title>
    <link href="http://wuchong.me/blog/2015/05/22/running-scrapy-programmatically/"/>
    <id>http://wuchong.me/blog/2015/05/22/running-scrapy-programmatically/</id>
    <published>2015-05-22T14:08:03.000Z</published>
    <updated>2022-08-03T06:46:44.511Z</updated>
    
    <content type="html"><![CDATA[<p>最近实验室的项目中有一个需求是这样的，需要爬取若干个（数目不小）网站发布的文章元数据（标题、时间、正文等）。问题是这些网站都很老旧和小众，当然也不可能遵守 <a href="http://en.wikipedia.org/wiki/Microdata_(HTML)" target="_blank" rel="noopener">Microdata</a> 这类标准。这时候所有网页共用一套默认规则无法保证正确抓取到信息，而每个网页写一份spider代码也不切实际。</p><p><strong>这时候，我迫切地希望能有一个框架可以通过只写一份spider代码和维护多个网站的爬取规则，就能自动抓取这些网站的信息，很庆幸 <a href="http://scrapy.org" target="_blank" rel="noopener">Scrapy</a> 可以做到这点</strong>。鉴于国内外关于这方面资料太少，所以我将这段时间来的经验和代码分享成了本文。</p><p>为了讲清楚这件事，我分成了三篇文章来叙述：</p><ol><li><a href="/blog/2015/05/22/running-scrapy-programmatically">编程方式下运行 Scrapy spider</a></li><li><a href="/blog/2015/05/22/running-scrapy-dynamic-and-configurable">使用Scrapy定制可动态配置的爬虫</a></li><li><a href="/blog/2015/05/22/using-redis-and-sqlalchemy-to-checkd-dup-and-store-scrapy-item">使用Redis和SQLAlchemy对Scrapy Item去重并存储</a></li></ol><p>本篇文章主要介绍如何使用编程的方式运行Scrapy爬虫。</p><p>在开始本文之前，你需要对 Scrapy 有所熟悉，知道 Items、Spider、Pipline、Selector 的概念。如果你是 Scrapy 新手，想了解如何用Scrapy开始爬取一个网站，推荐你先看看<a href="https://scrapy-chs.readthedocs.org/zh_CN/0.24/intro/tutorial.html" target="_blank" rel="noopener">官方的教程</a>。</p><p>运行一个Scrapy爬虫可以通过命令行的方式（<code>scrapy runspider myspider.py</code>）启动，也可以使用<a href="https://scrapy-chs.readthedocs.org/zh_CN/0.24/topics/api.html" target="_blank" rel="noopener">核心API</a>通过编程的方式启动。为了获得更高的定制性和灵活性，我们主要使用后者的方式。</p><a id="more"></a><p>我们使用官方教程中的 Dmoz 例子来帮助我们理解使用编程方式启动spider。我们的 spider 文件 <code>dmoz_spider.py</code> 长这个样子：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> scrapy</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">DmozItem</span><span class="params">(scrapy.Item)</span>:</span></span><br><span class="line">    title = scrapy.Field()</span><br><span class="line">    link = scrapy.Field()</span><br><span class="line">    desc = scrapy.Field()</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">DmozSpider</span><span class="params">(scrapy.Spider)</span>:</span></span><br><span class="line">    name = <span class="string">"dmoz"</span></span><br><span class="line">    allowed_domains = [<span class="string">"dmoz.org"</span>]</span><br><span class="line">    start_urls = [</span><br><span class="line">        <span class="string">"http://www.dmoz.org/Computers/Programming/Languages/Python/Books/"</span>,</span><br><span class="line">        <span class="string">"http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"</span></span><br><span class="line">    ]</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">parse</span><span class="params">(self, response)</span>:</span></span><br><span class="line">        <span class="keyword">for</span> sel <span class="keyword">in</span> response.xpath(<span class="string">'//ul/li'</span>):</span><br><span class="line">            item = DmozItem()</span><br><span class="line">            item[<span class="string">'title'</span>] = sel.xpath(<span class="string">'a/text()'</span>).extract()</span><br><span class="line">            item[<span class="string">'link'</span>] = sel.xpath(<span class="string">'a/@href'</span>).extract()</span><br><span class="line">            item[<span class="string">'desc'</span>] = sel.xpath(<span class="string">'text()'</span>).extract()</span><br><span class="line">            <span class="keyword">yield</span> item</span><br></pre></td></tr></table></figure><p>接下来我们需要写一个脚本<code>run.py</code>，来运行DmozSpider：</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> dmoz_spider import DmozSpider</span><br><span class="line"></span><br><span class="line"><span class="comment"># scrapy api</span></span><br><span class="line"><span class="keyword">from</span> scrapy import signals, log</span><br><span class="line"><span class="keyword">from</span> twisted.internet import reactor</span><br><span class="line"><span class="keyword">from</span> scrapy.crawler import Crawler</span><br><span class="line"><span class="keyword">from</span> scrapy.settings import Settings</span><br><span class="line"></span><br><span class="line">def spider_closing(spider):</span><br><span class="line">    <span class="string">""</span><span class="string">"Activates on spider closed signal"</span><span class="string">""</span></span><br><span class="line">    log.msg(<span class="string">"Closing reactor"</span>, <span class="attribute">level</span>=log.INFO)</span><br><span class="line">    reactor.stop()</span><br><span class="line"></span><br><span class="line">log.start(<span class="attribute">loglevel</span>=log.DEBUG)</span><br><span class="line">settings = Settings()</span><br><span class="line"></span><br><span class="line"><span class="comment"># crawl responsibly</span></span><br><span class="line">settings.<span class="builtin-name">set</span>(<span class="string">"USER_AGENT"</span>, <span class="string">"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36"</span>)</span><br><span class="line">crawler = Crawler(settings)</span><br><span class="line"></span><br><span class="line"><span class="comment"># stop reactor when spider closes</span></span><br><span class="line">crawler.signals.connect(spider_closing, <span class="attribute">signal</span>=signals.spider_closed)</span><br><span class="line"></span><br><span class="line">crawler.configure()</span><br><span class="line">crawler.crawl(DmozSpider())</span><br><span class="line">crawler.start()</span><br><span class="line">reactor.<span class="builtin-name">run</span>()</span><br></pre></td></tr></table></figure><p>然后运行<code>python run.py</code>就启动了我们的爬虫了，但是由于我们这里没有对爬下来的结果进行任何的存储操作，所以看不到结果。你可以写一个 item pipline 用来将数据存储到数据库，使用<code>settings.set</code>接口将这个 pipline 配置到<code>ITEMS_PIPLINE</code>，我们将在<a href="/blog/2015/05/22/using-redis-and-sqlalchemy-to-checkd-dup-and-store-scrapy-item">第三篇文章</a>中具体讲解这部分内容。<a href="/blog/2015/05/22/running-scrapy-dynamic-and-configurable">下一篇博客</a>将会介绍如何通过维护多个网站的爬取规则来抓取各个网站的数据。</p><p>你可以在 <a href="https://github.com/wuchong/scrapy-dynamic-configurable/tree/scrapy-0.24" target="_blank" rel="noopener">GitHub</a> 上看到本文的完整项目。</p><p><em>注：本文使用的 Scrapy 版本是 0.24，<a href="https://github.com/wuchong/scrapy-dynamic-configurable" target="_blank" rel="noopener">GitHub</a> 上的master分支已支持 Scrapy 1.0</em></p><h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul><li><a href="http://kirankoduru.github.io/python/running-scrapy-programmatically.html" target="_blank" rel="noopener">Running scrapy spider programmatically</a></li></ul>]]></content>
    
    <summary type="html">
    
      本篇文章主要介绍如何使用编程的方式运行Scrapy爬虫。
    
    </summary>
    
      <category term="程序设计" scheme="http://wuchong.me/categories/%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/"/>
    
    
      <category term="爬虫" scheme="http://wuchong.me/tags/%E7%88%AC%E8%99%AB/"/>
    
      <category term="scrapy" scheme="http://wuchong.me/tags/scrapy/"/>
    
  </entry>
  
  <entry>
    <title>读《程序员必读的职业规划书》</title>
    <link href="http://wuchong.me/blog/2015/05/15/read-programmer-career-planning/"/>
    <id>http://wuchong.me/blog/2015/05/15/read-programmer-career-planning/</id>
    <published>2015-05-15T14:48:56.000Z</published>
    <updated>2022-08-03T06:46:44.510Z</updated>
    
    <content type="html"><![CDATA[<p>半年前我就有幸拜读了<a href="http://weibo.com/easy" target="_blank" rel="noopener">@Easy</a>的<a href="https://selfstore.io/products/190" target="_blank" rel="noopener">《程序员跳槽全攻略》</a>电子书，当时趁着限免机智地入手了。当晚一口气读完后感觉收获颇丰，随手就发了篇<a href="http://weibo.com/2176287895/BAQPPgAmA?from=page_1005052176287895_profile&amp;wvr=6&amp;mod=weibotime" target="_blank" rel="noopener">微博</a>正能量了出去。在前几天的下午，又看到Easy为庆祝纸书上架发布了<a href="http://weibo.com/p/1001603839169511193437" target="_blank" rel="noopener">百人试读活动</a>。以我的性格当然果断就报名啦。</p><p>刚拿到纸书的第一印象是“哇，好薄啊！”，不过浓缩的都是精华。这更像是一本迷你武林秘籍，在你练功遇到瓶颈时，拿出这本小册子读一读，说不定就找到了突破的方式。纸书与电子书在内容上的差别不大，主要是调整了目录的结构，加了些插图和tips。虽然是第二次读这本书，也有一些新的收获，所以就写了篇文章记录下。</p><a id="more"></a><h2 id="职业规划"><a href="#职业规划" class="headerlink" title="职业规划"></a>职业规划</h2><p>电子书的书名叫《程序员跳槽全攻略》，纸书的书名叫《程序员必读的职业规划书》。从「跳槽攻略」到「职业规划」的改变，一方面是措辞上更加严肃和严谨了，另一方面是这本书在定位上不仅面向在职程序员，还面向了在校学生们。</p><p>作为一名即将离开大学校园的应届毕业生，我深深认为在校生们应该看看这本书。私以为毕业后的第一份工作对个人的成长和影响是非常重要的，正确地选择人生的第一份工作是职业规划中的重要一课。而许多在校生对自己的职业没有很清晰的规划，大多数不知道该往什么技术方向发展。应聘PHP，可能只是PHP用最熟练，谈不上喜欢，谈不上规划。看完这本书后，你可能对于要选择哪条技术道路更加清晰。</p><p>职业规划说白了就是为了实现人生目标而做的规划。比如我的理想是升职、加薪、迎娶白富美、当上CTO。为了当上CTO的终极目标，必须规划好当前一步。精通一门语言、积累高并发系统的开发经验、做好几个开源项目、让自己的博客UV过千，每一件事都是为了实现终极目标而做出的努力。有了人生目标，做每一件事都会变得有意义有动力，做成每件事的成就感又会让下件事更有动力。</p><h2 id="调整定位"><a href="#调整定位" class="headerlink" title="调整定位"></a>调整定位</h2><blockquote><p>站在风口不一定能飞起来，但站在冰山上必然会沉下去。</p></blockquote><p>互联网技术变化非常快，新技术层出不穷，但是并不是所有技术都有平等的待遇，相反总是有些技术突然之间变得炙手可热，有些技术不温不火逐渐没落。在调整个人定位上本书给了两个建议，（1）学会观察技术趋势（2）投资新兴市场和细分市场。</p><p>学会观察技术趋势真是说的容易做到难。未来总是难以预测的，在没有足够的技术敏感性的时候，就看看技术大牛们都在用什么吧。对于应届生来说，书中提到「可以选择一些得到大量投资的行业，通常而言，他们代表了未来的发展方向，比如云计算、大数据、移动互联网、智能硬件、共享经济、互联网金融等」。</p><p>投资新兴市场和细分市场方面书中讲了几个例子，有个例子是如果应聘了乌云平台PHP开发工程师，那么「在乌云工作几个月以后，你就能写出来可能是国内最安全的PHP代码…这就是细分市场，比你懂安全的没你懂PHP、比你懂PHP的没你懂安全」。</p><h2 id="树立个人品牌"><a href="#树立个人品牌" class="headerlink" title="树立个人品牌"></a>树立个人品牌</h2><p>长辈总是劝戒我们要低调做人，但是程序员应该高调树立个人品牌。原因我就不说了，看书去吧。关键是如何树立个人品牌？</p><p>书中列了以下几个建议：</p><ul><li>GitHub帐号，不解释</li><li>技术博客</li><li>微博，最好能加V，用于业内交流</li><li>技术社区帐号，比如StackOverflow</li><li>开源项目</li></ul><p>其实，关键就是「分享」二字。「平时遇到的大小问题可以零星发在微博上。相对大量的内容，可以写成文章发在博客上。比较系统的内容，可以在相应文章的基础上整理成迷你书」。个人觉得不错的内容可以提交到<a href="http://toutiao.io/" target="_blank" rel="noopener">开发者头条</a>和<a href="http://geek.csdn.net/" target="_blank" rel="noopener">CSDN极客头条</a>，借助平台来推广。博客的内容质量是最重要的，只要你持续分享高质量的干货，就不愁没有读者。</p><p>开源项目是重磅杀器。很多人认为开始开源项目很难，其实只是不敢迈出第一步而已。找一些自己在在项目时遇到的费时费事的小细节做好，然后开源就可以了。或者用自己新学的语言造个自己感兴趣的轮子，然后开源。或者用开源的形式做一个应用。我自己最近也在做一个开源应用，贵在实践。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本书主要分为原理篇、准备篇、操作篇。推荐好好读读准备篇，会有很多收获。对于正在找工作的同学，操作篇也是非常实用的。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;半年前我就有幸拜读了&lt;a href=&quot;http://weibo.com/easy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@Easy&lt;/a&gt;的&lt;a href=&quot;https://selfstore.io/products/190&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;《程序员跳槽全攻略》&lt;/a&gt;电子书，当时趁着限免机智地入手了。当晚一口气读完后感觉收获颇丰，随手就发了篇&lt;a href=&quot;http://weibo.com/2176287895/BAQPPgAmA?from=page_1005052176287895_profile&amp;amp;wvr=6&amp;amp;mod=weibotime&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;微博&lt;/a&gt;正能量了出去。在前几天的下午，又看到Easy为庆祝纸书上架发布了&lt;a href=&quot;http://weibo.com/p/1001603839169511193437&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;百人试读活动&lt;/a&gt;。以我的性格当然果断就报名啦。&lt;/p&gt;
&lt;p&gt;刚拿到纸书的第一印象是“哇，好薄啊！”，不过浓缩的都是精华。这更像是一本迷你武林秘籍，在你练功遇到瓶颈时，拿出这本小册子读一读，说不定就找到了突破的方式。纸书与电子书在内容上的差别不大，主要是调整了目录的结构，加了些插图和tips。虽然是第二次读这本书，也有一些新的收获，所以就写了篇文章记录下。&lt;/p&gt;
    
    </summary>
    
      <category term="读书笔记" scheme="http://wuchong.me/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="读书" scheme="http://wuchong.me/tags/%E8%AF%BB%E4%B9%A6/"/>
    
  </entry>
  
  <entry>
    <title>Spark 下操作 HBase（1.0.0 新 API）</title>
    <link href="http://wuchong.me/blog/2015/04/06/spark-on-hbase-new-api/"/>
    <id>http://wuchong.me/blog/2015/04/06/spark-on-hbase-new-api/</id>
    <published>2015-04-06T15:36:32.000Z</published>
    <updated>2022-08-03T06:46:44.511Z</updated>
    
    <content type="html"><![CDATA[<p>HBase经过七年发展，终于在今年2月底，发布了 1.0.0 版本。这个版本提供了一些让人激动的功能，并且，在不牺牲稳定性的前提下，引入了新的API。虽然 1.0.0 兼容旧版本的 API，不过还是应该尽早地来熟悉下新版API。并且了解下如何与当下正红的 Spark 结合，进行数据的写入与读取。鉴于国内外有关 HBase 1.0.0 新 API 的资料甚少，故作此文。</p><p>本文将分两部分介绍，第一部分讲解使用 HBase 新版 API 进行 CRUD 基本操作；第二部分讲解如何将 Spark 内的 RDDs 写入 HBase 的表中，反之，HBase 中的表又是如何以 RDDs 形式加载进 Spark 内的。</p><a id="more"></a><h2 id="环境配置"><a href="#环境配置" class="headerlink" title="环境配置"></a>环境配置</h2><p>为了避免版本不一致带来不必要的麻烦，API 和 HBase环境都是 1.0.0 版本。HBase 为单机模式，分布式模式的使用方法类似，只需要修改<code>HBaseConfiguration</code>的配置即可。</p><p>开发环境中使用 SBT 加载依赖项<br><figure class="highlight makefile"><table><tr><td class="code"><pre><span class="line">name := <span class="string">"SparkLearn"</span></span><br><span class="line"></span><br><span class="line">version := <span class="string">"1.0"</span></span><br><span class="line"></span><br><span class="line">scalaVersion := <span class="string">"2.10.4"</span></span><br><span class="line"></span><br><span class="line">libraryDependencies += <span class="string">"org.apache.spark"</span> %% <span class="string">"spark-core"</span> % <span class="string">"1.3.0"</span></span><br><span class="line"></span><br><span class="line">libraryDependencies += <span class="string">"org.apache.hbase"</span> % <span class="string">"hbase-client"</span> % <span class="string">"1.0.0"</span></span><br><span class="line"></span><br><span class="line">libraryDependencies += <span class="string">"org.apache.hbase"</span> % <span class="string">"hbase-common"</span> % <span class="string">"1.0.0"</span></span><br><span class="line"></span><br><span class="line">libraryDependencies += <span class="string">"org.apache.hbase"</span> % <span class="string">"hbase-server"</span> % <span class="string">"1.0.0"</span></span><br></pre></td></tr></table></figure></p><h2 id="HBase-的-CRUD-操作"><a href="#HBase-的-CRUD-操作" class="headerlink" title="HBase 的 CRUD 操作"></a>HBase 的 CRUD 操作</h2><p>新版 API 中加入了 <code>Connection</code>，<code>HAdmin</code>成了<code>Admin</code>，<code>HTable</code>成了<code>Table</code>，而<code>Admin</code>和<code>Table</code>只能通过<code>Connection</code>获得。<code>Connection</code>的创建是个重量级的操作，由于<code>Connection</code>是线程安全的，所以推荐使用单例，其工厂方法需要一个<code>HBaseConfiguration</code>。<br><figure class="highlight stata"><table><tr><td class="code"><pre><span class="line">val <span class="keyword">conf</span> = HBaseConfiguration.create()</span><br><span class="line"><span class="keyword">conf</span>.<span class="keyword">set</span>(<span class="string">"hbase.zookeeper.property.clientPort"</span>, <span class="string">"2181"</span>)</span><br><span class="line"><span class="keyword">conf</span>.<span class="keyword">set</span>(<span class="string">"hbase.zookeeper.quorum"</span>, <span class="string">"master"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">//Connection 的创建是个重量级的工作，线程安全，是操作hbase的入口</span></span><br><span class="line">val conn = ConnectionFactory.createConnection(<span class="keyword">conf</span>)</span><br></pre></td></tr></table></figure></p><h3 id="创建表"><a href="#创建表" class="headerlink" title="创建表"></a>创建表</h3><p>使用<code>Admin</code>创建和删除表<br><figure class="highlight pf"><table><tr><td class="code"><pre><span class="line">val <span class="keyword">user</span>Table = TableName.valueOf(<span class="string">"user"</span>)</span><br><span class="line"></span><br><span class="line">//创建 <span class="keyword">user</span> 表</span><br><span class="line">val <span class="built_in">table</span>Descr = new HTableDescriptor(<span class="keyword">user</span>Table)</span><br><span class="line"><span class="built_in">table</span>Descr.addFamily(new HColumnDescriptor(<span class="string">"basic"</span>.getBytes))</span><br><span class="line">println(<span class="string">"Creating table `user`. "</span>)</span><br><span class="line">if (admin.<span class="built_in">table</span>Exists(<span class="keyword">user</span>Table)) &#123;</span><br><span class="line">  admin.disableTable(<span class="keyword">user</span>Table)</span><br><span class="line">  admin.deleteTable(<span class="keyword">user</span>Table)</span><br><span class="line">&#125;</span><br><span class="line">admin.createTable(<span class="built_in">table</span>Descr)</span><br><span class="line">println(<span class="string">"Done!"</span>)</span><br></pre></td></tr></table></figure></p><h3 id="插入、查询、扫描、删除操作"><a href="#插入、查询、扫描、删除操作" class="headerlink" title="插入、查询、扫描、删除操作"></a>插入、查询、扫描、删除操作</h3><p>HBase 上的操作都需要先创建一个操作对象<code>Put</code>,<code>Get</code>,<code>Delete</code>等，然后调用<code>Table</code>上的相对应的方法<br><figure class="highlight fsharp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">try</span>&#123;</span><br><span class="line">  <span class="comment">//获取 user 表</span></span><br><span class="line">  <span class="keyword">val</span> table = conn.getTable(userTable)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">try</span>&#123;</span><br><span class="line">    <span class="comment">//准备插入一条 key 为 id001 的数据</span></span><br><span class="line">    <span class="keyword">val</span> p = <span class="keyword">new</span> Put(<span class="string">"id001"</span>.getBytes)</span><br><span class="line">    <span class="comment">//为put操作指定 column 和 value （以前的 put.add 方法被弃用了）</span></span><br><span class="line">    p.addColumn(<span class="string">"basic"</span>.getBytes,<span class="string">"name"</span>.getBytes, <span class="string">"wuchong"</span>.getBytes)</span><br><span class="line">    <span class="comment">//提交</span></span><br><span class="line">    table.put(p)</span><br><span class="line"></span><br><span class="line">    <span class="comment">//查询某条数据</span></span><br><span class="line">    <span class="keyword">val</span> g = <span class="keyword">new</span> Get(<span class="string">"id001"</span>.getBytes)</span><br><span class="line">    <span class="keyword">val</span> result = table.get(g)</span><br><span class="line">    <span class="keyword">val</span> value = Bytes.toString(result.getValue(<span class="string">"basic"</span>.getBytes,<span class="string">"name"</span>.getBytes))</span><br><span class="line">    println(<span class="string">"GET id001 :"</span>+value)</span><br><span class="line"></span><br><span class="line">    <span class="comment">//扫描数据</span></span><br><span class="line">    <span class="keyword">val</span> s = <span class="keyword">new</span> Scan()</span><br><span class="line">    s.addColumn(<span class="string">"basic"</span>.getBytes,<span class="string">"name"</span>.getBytes)</span><br><span class="line">    <span class="keyword">val</span> scanner = table.getScanner(s)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>&#123;</span><br><span class="line">      <span class="keyword">for</span>(r &lt;- scanner)&#123;</span><br><span class="line">        println(<span class="string">"Found row: "</span>+r)</span><br><span class="line">        println(<span class="string">"Found value: "</span>+Bytes.toString(</span><br><span class="line">          r.getValue(<span class="string">"basic"</span>.getBytes,<span class="string">"name"</span>.getBytes)))</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">      <span class="comment">//确保scanner关闭</span></span><br><span class="line">      scanner.close()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//删除某条数据,操作方式与 Put 类似</span></span><br><span class="line">    <span class="keyword">val</span> d = <span class="keyword">new</span> Delete(<span class="string">"id001"</span>.getBytes)</span><br><span class="line">    d.addColumn(<span class="string">"basic"</span>.getBytes,<span class="string">"name"</span>.getBytes)</span><br><span class="line">    table.delete(d)</span><br><span class="line"></span><br><span class="line">  &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="keyword">if</span>(table != <span class="keyword">null</span>) table.close()</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">&#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">  conn.close()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="Spark-操作-HBase"><a href="#Spark-操作-HBase" class="headerlink" title="Spark 操作 HBase"></a>Spark 操作 HBase</h2><h3 id="写入-HBase"><a href="#写入-HBase" class="headerlink" title="写入 HBase"></a>写入 HBase</h3><p>首先要向 HBase 写入数据，我们需要用到<code>PairRDDFunctions.saveAsHadoopDataset</code>。因为 HBase 不是一个文件系统，所以<code>saveAsHadoopFile</code>方法没用。</p><blockquote><p><code>def saveAsHadoopDataset(conf: JobConf): Unit</code><br>Output the RDD to any Hadoop-supported storage system, using a Hadoop JobConf object for that storage system</p></blockquote><p>这个方法需要一个 JobConf 作为参数，类似于一个配置项，主要需要指定输出的格式和输出的表名。</p><p><strong>Step 1：</strong>我们需要先创建一个 JobConf。<br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="comment">//定义 HBase 的配置</span></span><br><span class="line"><span class="keyword">val</span> conf = HBaseConfiguration.create()</span><br><span class="line">conf.<span class="keyword">set</span>(<span class="string">"hbase.zookeeper.property.clientPort"</span>, <span class="string">"2181"</span>)</span><br><span class="line">conf.<span class="keyword">set</span>(<span class="string">"hbase.zookeeper.quorum"</span>, <span class="string">"master"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">//指定输出格式和输出表名</span></span><br><span class="line"><span class="keyword">val</span> jobConf = new JobConf(conf,<span class="keyword">this</span>.getClass)</span><br><span class="line">jobConf.setOutputFormat(classOf[TableOutputFormat])</span><br><span class="line">jobConf.<span class="keyword">set</span>(TableOutputFormat.OUTPUT_TABLE,<span class="string">"user"</span>)</span><br></pre></td></tr></table></figure></p><p><strong>Step 2：</strong> RDD 到表模式的映射<br>在 HBase 中的表 schema 一般是这样的：</p><pre><code>row     cf:col_1    cf:col_2</code></pre><p>而在Spark中，我们操作的是RDD元组，比如<code>(1,&quot;lilei&quot;,14)</code>, <code>(2,&quot;hanmei&quot;,18)</code>。我们需要将 <code>RDD[(uid:Int, name:String, age:Int)]</code> 转换成 <code>RDD[(ImmutableBytesWritable, Put)]</code>。所以，我们定义一个 convert 函数做这个转换工作</p><figure class="highlight lasso"><table><tr><td class="code"><pre><span class="line">def convert(triple: (Int, <span class="built_in">String</span>, Int)) = &#123;</span><br><span class="line">      val p = <span class="literal">new</span> Put(<span class="built_in">Bytes</span>.toBytes(triple._1))</span><br><span class="line">      p.addColumn(<span class="built_in">Bytes</span>.toBytes(<span class="string">"basic"</span>),<span class="built_in">Bytes</span>.toBytes(<span class="string">"name"</span>),<span class="built_in">Bytes</span>.toBytes(triple._2))</span><br><span class="line">      p.addColumn(<span class="built_in">Bytes</span>.toBytes(<span class="string">"basic"</span>),<span class="built_in">Bytes</span>.toBytes(<span class="string">"age"</span>),<span class="built_in">Bytes</span>.toBytes(triple._3))</span><br><span class="line">      (<span class="literal">new</span> ImmutableBytesWritable, p)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Step 3：</strong> 读取RDD并转换<br><figure class="highlight lsl"><table><tr><td class="code"><pre><span class="line"><span class="comment">//read RDD data from somewhere and convert</span></span><br><span class="line">val rawData = List((<span class="number">1</span>,<span class="string">"lilei"</span>,<span class="number">14</span>), (<span class="number">2</span>,<span class="string">"hanmei"</span>,<span class="number">18</span>), (<span class="number">3</span>,<span class="string">"someone"</span>,<span class="number">38</span>))</span><br><span class="line">val localData = sc.parallelize(rawData).map(convert)</span><br></pre></td></tr></table></figure></p><p><strong>Step 4：</strong> 使用<code>saveAsHadoopDataset</code>方法写入HBase<br><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">localData</span><span class="selector-class">.saveAsHadoopDataset</span>(<span class="selector-tag">jobConf</span>)</span><br></pre></td></tr></table></figure></p><h3 id="读取-HBase"><a href="#读取-HBase" class="headerlink" title="读取 HBase"></a>读取 HBase</h3><p>Spark读取HBase，我们主要使用<code>SparkContext</code> 提供的<code>newAPIHadoopRDD</code>API将表的内容以 RDDs 的形式加载到 Spark 中。 </p><figure class="highlight scala"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> conf = <span class="type">HBaseConfiguration</span>.create()</span><br><span class="line">conf.set(<span class="string">"hbase.zookeeper.property.clientPort"</span>, <span class="string">"2181"</span>)</span><br><span class="line">conf.set(<span class="string">"hbase.zookeeper.quorum"</span>, <span class="string">"master"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">//设置查询的表名</span></span><br><span class="line">conf.set(<span class="type">TableInputFormat</span>.<span class="type">INPUT_TABLE</span>, <span class="string">"user"</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> usersRDD = sc.newAPIHadoopRDD(conf, classOf[<span class="type">TableInputFormat</span>],</span><br><span class="line">  classOf[org.apache.hadoop.hbase.io.<span class="type">ImmutableBytesWritable</span>],</span><br><span class="line">  classOf[org.apache.hadoop.hbase.client.<span class="type">Result</span>])</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> count = usersRDD.count()</span><br><span class="line">println(<span class="string">"Users RDD Count:"</span> + count)</span><br><span class="line">usersRDD.cache()</span><br><span class="line"></span><br><span class="line"><span class="comment">//遍历输出</span></span><br><span class="line">usersRDD.foreach&#123; <span class="keyword">case</span> (_,result) =&gt;</span><br><span class="line">  <span class="keyword">val</span> key = <span class="type">Bytes</span>.toInt(result.getRow)</span><br><span class="line">  <span class="keyword">val</span> name = <span class="type">Bytes</span>.toString(result.getValue(<span class="string">"basic"</span>.getBytes,<span class="string">"name"</span>.getBytes))</span><br><span class="line">  <span class="keyword">val</span> age = <span class="type">Bytes</span>.toInt(result.getValue(<span class="string">"basic"</span>.getBytes,<span class="string">"age"</span>.getBytes))</span><br><span class="line">  println(<span class="string">"Row key:"</span>+key+<span class="string">" Name:"</span>+name+<span class="string">" Age:"</span>+age)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><p>更完整的代码已上传到 Gist 。</p><ul><li><a href="https://gist.github.com/wuchong/95630f80966d07d7453b#file-hbasenewapi-scala" target="_blank" rel="noopener">HBaseNewAPI.scala</a>  HBase 的 CRUD 操作</li><li><a href="https://gist.github.com/wuchong/95630f80966d07d7453b#file-sparkonhbase-scala" target="_blank" rel="noopener">SparkOnHBase.scala</a>  Spark 操作 HBase</li></ul>]]></content>
    
    <summary type="html">
    
      本文将分两部分介绍，第一部分讲解使用 HBase 新版 API 进行 CRUD 基本操作；第二部分讲解如何将 Spark 内的 RDDs 写入 HBase 的表中，反之，HBase 中的表又是如何以 RDDs 形式加载进 Spark 内的。
    
    </summary>
    
      <category term="分布式系统" scheme="http://wuchong.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
    
      <category term="Spark" scheme="http://wuchong.me/tags/Spark/"/>
    
      <category term="HBase" scheme="http://wuchong.me/tags/HBase/"/>
    
  </entry>
  
  <entry>
    <title>HBase 集群安装部署</title>
    <link href="http://wuchong.me/blog/2015/04/05/hbase-cluster-deploy/"/>
    <id>http://wuchong.me/blog/2015/04/05/hbase-cluster-deploy/</id>
    <published>2015-04-05T15:40:09.000Z</published>
    <updated>2022-08-03T06:46:44.506Z</updated>
    
    <content type="html"><![CDATA[<p>软件环境</p><blockquote><p>OS: Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-32-generic x86_64)<br>Java: jdk1.7.0_75<br>Hadoop: hadoop-2.6.0<br>Hbase: hbase-1.0.0</p></blockquote><p>集群机器：</p><table><thead><tr><th>IP</th><th>HostName</th><th>Mater</th><th>RegionServer</th></tr></thead><tbody><tr><td>10.4.20.30</td><td>master</td><td>yes</td><td>no</td></tr><tr><td>10.4.20.31</td><td>slave1</td><td>no</td><td>yes</td></tr><tr><td>10.4.20.32</td><td>slave2</td><td>no</td><td>yes</td></tr></tbody></table><h2 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h2><p>假设你已经安装部署好了 Hadoop 集群和 Java，可以参考 <a href="/blog/2015/04/04/spark-on-yarn-cluster-deploy/">Spark on YARN 集群部署手册</a> 这篇文章。</p><a id="more"></a><h2 id="下载解压"><a href="#下载解压" class="headerlink" title="下载解压"></a>下载解压</h2><p>可以从<a href="http://www.apache.org/dyn/closer.cgi/hbase/" target="_blank" rel="noopener">官方下载地址</a>下载 HBase 最新版本，推荐 stable 目录下的二进制版本。我下载的是 hbase-1.0.0-bin.tar.gz 。确保你下载的版本与你现存的 Hadoop 版本兼容（<a href="http://hbase.apache.org/book.html#hadoop" target="_blank" rel="noopener">兼容列表</a>）以及支持的JDK版本（HBase 1.0.x 已经不支持 JDK 6 了）。</p><p>解压缩<br><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">tar</span> <span class="selector-tag">-zxvf</span> <span class="selector-tag">hbase-1</span><span class="selector-class">.0</span><span class="selector-class">.0-bin</span><span class="selector-class">.tar</span><span class="selector-class">.gz</span></span><br><span class="line"><span class="selector-tag">cd</span> <span class="selector-tag">hbase-1</span><span class="selector-class">.0</span><span class="selector-class">.0</span></span><br></pre></td></tr></table></figure></p><h2 id="配置-HBase"><a href="#配置-HBase" class="headerlink" title="配置 HBase"></a>配置 HBase</h2><p>编辑<code>hbase-env.sh</code>文件，修改 JAVA_HOME 为你的路径。<br><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="comment"># The java implementation to use.  Java 1.7+ required.</span></span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">JAVA_HOME</span>=/home/spark/workspace/jdk1.7.0_75</span><br></pre></td></tr></table></figure></p><p>编辑<code>conf/hbase-site.xml</code>文件：<br><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>hbase.rootdir<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>hdfs://master:9000/hbase<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>hbase.cluster.distributed<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>true<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">property</span>&gt;</span>   </span><br><span class="line">  <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>hbase.zookeeper.quorum<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>master,slave1,slave2<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>hbase.zookeeper.property.dataDir<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>/home/spark/workspace/zookeeper/data<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>其中第一个属性指定本机的hbase的存储目录，必须与Hadoop集群的<code>core-site.xml</code>文件配置保持一致；第二个属性指定hbase的运行模式，true代表全分布模式；第三个属性指定 Zookeeper 管理的机器，一般为奇数个；第四个属性是数据存放的路径。这里我使用的默认的 HBase 自带的 Zookeeper。</p><p>配置regionservers，在regionservers文件中添加如下内容：<br><figure class="highlight"><table><tr><td class="code"><pre><span class="line">slave1</span><br><span class="line">slave2</span><br></pre></td></tr></table></figure></p><p><code>regionservers</code>文件列出了所有运行hbase的机器（即HRegionServer)。此文件的配置和Hadoop中的slaves文件十分相似，每行指定一台机器的主机名。当HBase启动的时候，会将此文件中列出的所有机器启动。关闭时亦如此。我们的配置意为在 slave1, slave2, slave3 上都将启动 RegionServer。</p><p>将配置好的 hbase 文件分发给各个 slave<br><figure class="highlight elixir"><table><tr><td class="code"><pre><span class="line">scp -r hbase<span class="number">-1.0</span>.0 spark<span class="variable">@slave1</span><span class="symbol">:~/workspace/</span></span><br><span class="line">scp -r hbase<span class="number">-1.0</span>.0 spark<span class="variable">@slave2</span><span class="symbol">:~/workspace/</span></span><br></pre></td></tr></table></figure></p><h2 id="修改-ulimit-限制"><a href="#修改-ulimit-限制" class="headerlink" title="修改 ulimit 限制"></a>修改 ulimit 限制</h2><p>HBase 会在同一时间打开大量的文件句柄和进程，超过 Linux 的默认限制，导致可能会出现如下错误。<br><figure class="highlight css"><table><tr><td class="code"><pre><span class="line">2010<span class="selector-tag">-04-06</span> 03<span class="selector-pseudo">:04</span><span class="selector-pseudo">:37</span>,542 <span class="selector-tag">INFO</span> <span class="selector-tag">org</span><span class="selector-class">.apache</span><span class="selector-class">.hadoop</span><span class="selector-class">.hdfs</span><span class="selector-class">.DFSClient</span>: <span class="selector-tag">Exception</span> <span class="selector-tag">increateBlockOutputStream</span> <span class="selector-tag">java</span><span class="selector-class">.io</span><span class="selector-class">.EOFException</span></span><br><span class="line">2010<span class="selector-tag">-04-06</span> 03<span class="selector-pseudo">:04</span><span class="selector-pseudo">:37</span>,542 <span class="selector-tag">INFO</span> <span class="selector-tag">org</span><span class="selector-class">.apache</span><span class="selector-class">.hadoop</span><span class="selector-class">.hdfs</span><span class="selector-class">.DFSClient</span>: <span class="selector-tag">Abandoning</span> <span class="selector-tag">block</span> <span class="selector-tag">blk_-6935524980745310745_1391901</span></span><br></pre></td></tr></table></figure></p><p>所以编辑<code>/etc/security/limits.conf</code>文件，添加以下两行，提高能打开的句柄数量和进程数量。注意将<code>spark</code>改成你运行 HBase 的用户名。</p><figure class="highlight lsl"><table><tr><td class="code"><pre><span class="line">spark  -       nofile  <span class="number">32768</span></span><br><span class="line">spark  -       nproc   <span class="number">32000</span></span><br></pre></td></tr></table></figure><p>还需要在 <code>/etc/pam.d/common-session</code> 加上这一行:<br><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">session</span> <span class="selector-tag">required</span> <span class="selector-tag">pam_limits</span><span class="selector-class">.so</span></span><br></pre></td></tr></table></figure></p><p>否则在<code>/etc/security/limits.conf</code>上的配置不会生效。</p><p>最后还要注销（<code>logout</code>或者<code>exit</code>）后再登录，这些配置才能生效！使用<code>ulimit -n -u</code>命令查看最大文件和进程数量是否改变了。记得在每台安装 HBase 的机器上运行哦。</p><h2 id="运行-HBase"><a href="#运行-HBase" class="headerlink" title="运行 HBase"></a>运行 HBase</h2><p>在master上运行<br><figure class="highlight dos"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/workspace/hbase-<span class="number">1</span>.<span class="number">0</span>.<span class="number">0</span></span><br><span class="line">bin/<span class="built_in">start</span>-hbase.sh</span><br></pre></td></tr></table></figure></p><h2 id="验证-HBase-成功安装"><a href="#验证-HBase-成功安装" class="headerlink" title="验证 HBase 成功安装"></a>验证 HBase 成功安装</h2><p>在 master 运行 <code>jps</code> 应该会有<code>HMaster</code>进程。在各个 slave 上运行<code>jps</code> 应该会有<code>HQuorumPeer</code>,<code>HRegionServer</code>两个进程。</p><p>在浏览器中输入 <a href="http://master:16010" target="_blank" rel="noopener">http://master:16010</a> 可以看到 HBase Web UI 。</p>]]></content>
    
    <summary type="html">
    
      HBase 1.0.0 集群安装部署手册指南
    
    </summary>
    
      <category term="分布式系统" scheme="http://wuchong.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
    
      <category term="Spark" scheme="http://wuchong.me/tags/Spark/"/>
    
      <category term="HBase" scheme="http://wuchong.me/tags/HBase/"/>
    
  </entry>
  
  <entry>
    <title>Spark On YARN 集群安装部署</title>
    <link href="http://wuchong.me/blog/2015/04/04/spark-on-yarn-cluster-deploy/"/>
    <id>http://wuchong.me/blog/2015/04/04/spark-on-yarn-cluster-deploy/</id>
    <published>2015-04-04T15:45:34.000Z</published>
    <updated>2022-08-03T06:46:44.511Z</updated>
    
    <content type="html"><![CDATA[<p>最近毕设需要用到 Spark 集群，所以就记录下了部署的过程。我们知道 Spark 官方提供了三种集群部署方案： Standalone, Mesos, YARN。其中 Standalone 最为方便，本文主要讲述结合 YARN 的部署方案。</p><p>软件环境：</p><blockquote><p>Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-32-generic x86_64)<br>Hadoop: 2.6.0<br>Spark: 1.3.0</p></blockquote><h2 id="0-写在前面"><a href="#0-写在前面" class="headerlink" title="0 写在前面"></a>0 写在前面</h2><p>本例中的演示均为非 root 权限，所以有些命令行需要加 sudo，如果你是 root 身份运行，请忽略 sudo。下载安装的软件建议都放在 home 目录之上，比如<code>~/workspace</code>中，这样比较方便，以免权限问题带来不必要的麻烦。</p><a id="more"></a><h2 id="1-环境准备"><a href="#1-环境准备" class="headerlink" title="1. 环境准备"></a>1. 环境准备</h2><h3 id="修改主机名"><a href="#修改主机名" class="headerlink" title="修改主机名"></a>修改主机名</h3><p>我们将搭建1个master，2个slave的集群方案。首先修改主机名<code>vi /etc/hostname</code>，在master上修改为<code>master</code>，其中一个slave上修改为<code>slave1</code>，另一个同理。</p><h3 id="配置hosts"><a href="#配置hosts" class="headerlink" title="配置hosts"></a>配置hosts</h3><p>在每台主机上修改host文件<br><figure class="highlight accesslog"><table><tr><td class="code"><pre><span class="line">vi /etc/hosts</span><br><span class="line"></span><br><span class="line"><span class="number">10.1.1.107</span>      master</span><br><span class="line"><span class="number">10.1.1.108</span>      slave1</span><br><span class="line"><span class="number">10.1.1.109</span>      slave2</span><br></pre></td></tr></table></figure></p><p>配置之后ping一下用户名看是否生效<br><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">ping slave1</span><br><span class="line">ping slave2</span><br></pre></td></tr></table></figure></p><h3 id="SSH-免密码登录"><a href="#SSH-免密码登录" class="headerlink" title="SSH 免密码登录"></a>SSH 免密码登录</h3><p>安装Openssh server<br><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">sudo apt-<span class="builtin-name">get</span> install openssh-server</span><br></pre></td></tr></table></figure></p><p>在所有机器上都生成私钥和公钥<br><figure class="highlight 1c"><table><tr><td class="code"><pre><span class="line">ssh-keygen -t rsa   <span class="meta">#一路回车</span></span><br></pre></td></tr></table></figure></p><p>需要让机器间都能相互访问，就把每个机子上的<code>id_rsa.pub</code>发给master节点，传输公钥可以用scp来传输。<br><figure class="highlight elixir"><table><tr><td class="code"><pre><span class="line">scp ~<span class="regexp">/.ssh/id</span>_rsa.pub spark<span class="variable">@master</span><span class="symbol">:~/</span>.ssh/id_rsa.pub.slave1</span><br></pre></td></tr></table></figure></p><p>在master上，将所有公钥加到用于认证的公钥文件<code>authorized_keys</code>中<br><figure class="highlight awk"><table><tr><td class="code"><pre><span class="line">cat ~<span class="regexp">/.ssh/i</span>d_rsa.pub* &gt;&gt; ~<span class="regexp">/.ssh/</span>authorized_keys</span><br></pre></td></tr></table></figure></p><p>将公钥文件<code>authorized_keys</code>分发给每台slave<br><figure class="highlight elixir"><table><tr><td class="code"><pre><span class="line">scp ~<span class="regexp">/.ssh/authorized</span>_keys spark<span class="variable">@slave1</span><span class="symbol">:~/</span>.ssh/</span><br></pre></td></tr></table></figure></p><p>在每台机子上验证SSH无密码通信<br><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line">ssh <span class="literal">master</span></span><br><span class="line">ssh slave1</span><br><span class="line">ssh slave2</span><br></pre></td></tr></table></figure></p><p>如果登陆测试不成功，则可能需要修改文件authorized_keys的权限（权限的设置非常重要，因为不安全的设置安全设置,会让你不能使用RSA功能 ）<br><figure class="highlight awk"><table><tr><td class="code"><pre><span class="line">chmod <span class="number">600</span> ~<span class="regexp">/.ssh/</span>authorized_keys</span><br></pre></td></tr></table></figure></p><h2 id="安装-Java"><a href="#安装-Java" class="headerlink" title="安装 Java"></a>安装 Java</h2><p>从<a href="http://www.oracle.com/technetwork/java/javase/overview/index.html" target="_blank" rel="noopener">官网</a>下载最新版 Java 就可以，Spark官方说明 Java 只要是6以上的版本都可以，我下的是 jdk-7u75-linux-x64.gz<br>在<code>~/workspace</code>目录下直接解压<br><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">tar</span> <span class="selector-tag">-zxvf</span> <span class="selector-tag">jdk-7u75-linux-x64</span><span class="selector-class">.gz</span></span><br></pre></td></tr></table></figure></p><p>修改环境变量<code>sudo vi /etc/profile</code>，添加下列内容，<strong>注意将home路径替换成你的</strong>：<br><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">WORK_SPACE</span>=/home/spark/workspace/</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">JAVA_HOME</span>=<span class="variable">$WORK_SPACE</span>/jdk1.7.0_75</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">JRE_HOME</span>=/home/spark/work/jdk1.7.0_75/jre</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">PATH</span>=<span class="variable">$JAVA_HOME</span>/bin:$JAVA_HOME/jre/bin:$PATH</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">CLASSPATH</span>=<span class="variable">$CLASSPATH</span>:.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib</span><br></pre></td></tr></table></figure></p><p>然后使环境变量生效，并验证 Java 是否安装成功<br><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">$ source /etc<span class="built_in">/profile </span>  #生效环境变量</span><br><span class="line">$ java -version         #如果打印出如下版本信息，则说明安装成功</span><br><span class="line">java version <span class="string">"1.7.0_75"</span></span><br><span class="line">Java(TM) SE Runtime Environment (build 1.7.0_75-b13)</span><br><span class="line">Java HotSpot(TM) 64-Bit<span class="built_in"> Server </span>VM (build 24.75-b04, mixed mode)</span><br></pre></td></tr></table></figure></p><h2 id="安装-Scala"><a href="#安装-Scala" class="headerlink" title="安装 Scala"></a>安装 Scala</h2><p>Spark官方要求 Scala 版本为 2.10.x，注意不要下错版本，我这里下了 2.10.4，<a href="http://www.scala-lang.org/download/2.10.4.html" target="_blank" rel="noopener">官方下载地址</a>（可恶的天朝大局域网下载 Scala 龟速一般）。</p><p>同样我们在<code>~/workspace</code>中解压<br><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">tar</span> <span class="selector-tag">-zxvf</span> <span class="selector-tag">scala-2</span><span class="selector-class">.10</span><span class="selector-class">.4</span><span class="selector-class">.tgz</span></span><br></pre></td></tr></table></figure></p><p>再次修改环境变量<code>sudo vi /etc/profile</code>，添加以下内容：<br><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">SCALA_HOME</span>=<span class="variable">$WORK_SPACE</span>/scala-2.10.4</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">PATH</span>=<span class="variable">$PATH</span>:$SCALA_HOME/bin</span><br></pre></td></tr></table></figure></p><p>同样的方法使环境变量生效，并验证 scala 是否安装成功<br><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">$ source /etc<span class="built_in">/profile </span>  #生效环境变量</span><br><span class="line">$ scala -version        #如果打印出如下版本信息，则说明安装成功</span><br><span class="line">Scala code runner version 2.10.4 -- Copyright 2002-2013, LAMP/EPFL</span><br></pre></td></tr></table></figure></p><h2 id="安装配置-Hadoop-YARN"><a href="#安装配置-Hadoop-YARN" class="headerlink" title="安装配置 Hadoop YARN"></a>安装配置 Hadoop YARN</h2><h3 id="下载解压"><a href="#下载解压" class="headerlink" title="下载解压"></a>下载解压</h3><p>从<a href="http://hadoop.apache.org/releases.html#Download" target="_blank" rel="noopener">官网</a>下载 hadoop2.6.0 版本，这里给个我们学校的<a href="http://mirror.bit.edu.cn/apache/hadoop/common/hadoop-2.6.0/hadoop-2.6.0.tar.gz" target="_blank" rel="noopener">镜像下载地址</a>。</p><p>同样我们在<code>~/workspace</code>中解压<br><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">tar</span> <span class="selector-tag">-zxvf</span> <span class="selector-tag">hadoop-2</span><span class="selector-class">.6</span><span class="selector-class">.0</span><span class="selector-class">.tar</span><span class="selector-class">.gz</span></span><br></pre></td></tr></table></figure></p><h3 id="配置-Hadoop"><a href="#配置-Hadoop" class="headerlink" title="配置 Hadoop"></a>配置 Hadoop</h3><p><code>cd ~/workspace/hadoop-2.6.0/etc/hadoop</code>进入hadoop配置目录，需要配置有以下7个文件：<code>hadoop-env.sh</code>，<code>yarn-env.sh</code>，<code>slaves</code>，<code>core-site.xml</code>，<code>hdfs-site.xml</code>，<code>maprd-site.xml</code>，<code>yarn-site.xml</code></p><ol><li><p>在<code>hadoop-env.sh</code>中配置JAVA_HOME</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="comment"># The java implementation to use.</span></span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">JAVA_HOME</span>=/home/spark/workspace/jdk1.7.0_75</span><br></pre></td></tr></table></figure></li><li><p>在<code>yarn-env.sh</code>中配置JAVA_HOME</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="comment"># some Java parameters</span></span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">JAVA_HOME</span>=/home/spark/workspace/jdk1.7.0_75</span><br></pre></td></tr></table></figure></li><li><p>在<code>slaves</code>中配置slave节点的ip或者host，</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">slave1</span><br><span class="line">slave2</span><br></pre></td></tr></table></figure></li><li><p>修改<code>core-site.xml</code></p><figure class="highlight dts"><table><tr><td class="code"><pre><span class="line"><span class="params">&lt;configuration&gt;</span></span><br><span class="line">    <span class="params">&lt;property&gt;</span></span><br><span class="line">        <span class="params">&lt;name&gt;</span>fs.defaultFS<span class="params">&lt;/name&gt;</span></span><br><span class="line">        <span class="params">&lt;value&gt;</span>hdfs:<span class="comment">//master:9000/&lt;/value&gt;</span></span><br><span class="line">    <span class="params">&lt;/property&gt;</span></span><br><span class="line">    <span class="params">&lt;property&gt;</span></span><br><span class="line">         <span class="params">&lt;name&gt;</span>hadoop.tmp.dir<span class="params">&lt;/name&gt;</span></span><br><span class="line">         <span class="params">&lt;value&gt;</span>file:<span class="meta-keyword">/home/</span>spark<span class="meta-keyword">/workspace/</span>hadoop<span class="number">-2.6</span><span class="number">.0</span>/tmp<span class="params">&lt;/value&gt;</span></span><br><span class="line">    <span class="params">&lt;/property&gt;</span></span><br><span class="line"><span class="params">&lt;/configuration&gt;</span></span><br></pre></td></tr></table></figure></li><li><p>修改<code>hdfs-site.xml</code></p><figure class="highlight dts"><table><tr><td class="code"><pre><span class="line"><span class="params">&lt;configuration&gt;</span></span><br><span class="line">    <span class="params">&lt;property&gt;</span></span><br><span class="line">        <span class="params">&lt;name&gt;</span>dfs.namenode.secondary.http-address<span class="params">&lt;/name&gt;</span></span><br><span class="line">        <span class="params">&lt;value&gt;</span>master:<span class="number">9001</span><span class="params">&lt;/value&gt;</span></span><br><span class="line">    <span class="params">&lt;/property&gt;</span></span><br><span class="line">    <span class="params">&lt;property&gt;</span></span><br><span class="line">        <span class="params">&lt;name&gt;</span>dfs.namenode.name.dir<span class="params">&lt;/name&gt;</span></span><br><span class="line">        <span class="params">&lt;value&gt;</span>file:<span class="meta-keyword">/home/</span>spark<span class="meta-keyword">/workspace/</span>hadoop<span class="number">-2.6</span><span class="number">.0</span><span class="meta-keyword">/dfs/</span>name<span class="params">&lt;/value&gt;</span></span><br><span class="line">    <span class="params">&lt;/property&gt;</span></span><br><span class="line">    <span class="params">&lt;property&gt;</span></span><br><span class="line">        <span class="params">&lt;name&gt;</span>dfs.datanode.data.dir<span class="params">&lt;/name&gt;</span></span><br><span class="line">        <span class="params">&lt;value&gt;</span>file:<span class="meta-keyword">/home/</span>spark<span class="meta-keyword">/workspace/</span>hadoop<span class="number">-2.6</span><span class="number">.0</span><span class="meta-keyword">/dfs/</span>data<span class="params">&lt;/value&gt;</span></span><br><span class="line">    <span class="params">&lt;/property&gt;</span></span><br><span class="line">    <span class="params">&lt;property&gt;</span></span><br><span class="line">        <span class="params">&lt;name&gt;</span>dfs.replication<span class="params">&lt;/name&gt;</span></span><br><span class="line">        <span class="params">&lt;value&gt;</span><span class="number">3</span><span class="params">&lt;/value&gt;</span></span><br><span class="line">    <span class="params">&lt;/property&gt;</span></span><br><span class="line"><span class="params">&lt;/configuration&gt;</span></span><br></pre></td></tr></table></figure></li><li><p>修改<code>mapred-site.xml</code></p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>mapreduce.framework.name<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>yarn<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br></pre></td></tr></table></figure></li><li><p>修改<code>yarn-site.xml</code></p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.nodemanager.aux-services<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>mapreduce_shuffle<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.nodemanager.aux-services.mapreduce.shuffle.class<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>org.apache.hadoop.mapred.ShuffleHandler<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.resourcemanager.address<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>master:8032<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.resourcemanager.scheduler.address<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>master:8030<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.resourcemanager.resource-tracker.address<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>master:8035<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.resourcemanager.admin.address<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>master:8033<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.resourcemanager.webapp.address<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>master:8088<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br></pre></td></tr></table></figure></li></ol><p>将配置好的<code>hadoop-2.6.0</code>文件夹分发给所有slaves吧<br><figure class="highlight elixir"><table><tr><td class="code"><pre><span class="line">scp -r ~<span class="regexp">/workspace/hadoop</span><span class="number">-2.6</span>.0 spark<span class="variable">@slave1</span><span class="symbol">:~/workspace/</span></span><br></pre></td></tr></table></figure></p><h3 id="启动-Hadoop"><a href="#启动-Hadoop" class="headerlink" title="启动 Hadoop"></a>启动 Hadoop</h3><p>在 master 上执行以下操作，就可以启动 hadoop 了。<br><figure class="highlight livecodeserver"><table><tr><td class="code"><pre><span class="line">cd ~/workspace/hadoop<span class="number">-2.6</span><span class="number">.0</span>     <span class="comment">#进入hadoop目录</span></span><br><span class="line">bin/hadoop namenode -<span class="built_in">format</span>     <span class="comment">#格式化namenode</span></span><br><span class="line">sbin/<span class="built_in">start</span>-dfs.sh               <span class="comment">#启动dfs </span></span><br><span class="line">sbin/<span class="built_in">start</span>-yarn.sh              <span class="comment">#启动yarn</span></span><br></pre></td></tr></table></figure></p><h3 id="验证-Hadoop-是否安装成功"><a href="#验证-Hadoop-是否安装成功" class="headerlink" title="验证 Hadoop 是否安装成功"></a>验证 Hadoop 是否安装成功</h3><p>可以通过<code>jps</code>命令查看各个节点启动的进程是否正常。在 master 上应该有以下几个进程：<br><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="string">$</span> <span class="string">jps</span>  <span class="comment">#run on master</span></span><br><span class="line"><span class="number">3407</span> <span class="string">SecondaryNameNode</span></span><br><span class="line"><span class="number">3218</span> <span class="string">NameNode</span></span><br><span class="line"><span class="number">3552</span> <span class="string">ResourceManager</span></span><br><span class="line"><span class="number">3910</span> <span class="string">Jps</span></span><br></pre></td></tr></table></figure></p><p>在每个slave上应该有以下几个进程：<br><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="string">$</span> <span class="string">jps</span>   <span class="comment">#run on slaves</span></span><br><span class="line"><span class="number">2072</span> <span class="string">NodeManager</span></span><br><span class="line"><span class="number">2213</span> <span class="string">Jps</span></span><br><span class="line"><span class="number">1962</span> <span class="string">DataNode</span></span><br></pre></td></tr></table></figure></p><p>或者在浏览器中输入 <a href="http://master:8088" target="_blank" rel="noopener">http://master:8088</a> ，应该有 hadoop 的管理界面出来了，并能看到 slave1 和 slave2 节点。</p><h2 id="Spark安装"><a href="#Spark安装" class="headerlink" title="Spark安装"></a>Spark安装</h2><h3 id="下载解压-1"><a href="#下载解压-1" class="headerlink" title="下载解压"></a>下载解压</h3><p>进入<a href="http://spark.apache.org/downloads.html" target="_blank" rel="noopener">官方下载地址</a>下载最新版 Spark。我下载的是 <a href="http://mirror.bit.edu.cn/apache/spark/spark-1.3.0/spark-1.3.0.tgz" target="_blank" rel="noopener">spark-1.3.0-bin-hadoop2.4.tgz</a>。</p><p>在<code>~/workspace</code>目录下解压<br><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">tar</span> <span class="selector-tag">-zxvf</span> <span class="selector-tag">spark-1</span><span class="selector-class">.3</span><span class="selector-class">.0-bin-hadoop2</span><span class="selector-class">.4</span><span class="selector-class">.tgz</span></span><br><span class="line"><span class="selector-tag">mv</span> <span class="selector-tag">spark-1</span><span class="selector-class">.3</span><span class="selector-class">.0-bin-hadoop2</span><span class="selector-class">.4</span> <span class="selector-tag">spark-1</span><span class="selector-class">.3</span><span class="selector-class">.0</span>    #原来的文件名太长了，修改下</span><br></pre></td></tr></table></figure></p><h3 id="配置-Spark"><a href="#配置-Spark" class="headerlink" title="配置 Spark"></a>配置 Spark</h3><figure class="highlight vim"><table><tr><td class="code"><pre><span class="line"><span class="keyword">cd</span> ~/workspace/spark-<span class="number">1.3</span>.<span class="number">0</span>/<span class="keyword">conf</span>    #进入spark配置目录</span><br><span class="line"><span class="keyword">cp</span> spark-env.<span class="keyword">sh</span>.template spark-env.<span class="keyword">sh</span>   #从配置模板复制</span><br><span class="line"><span class="keyword">vi</span> spark-env.<span class="keyword">sh</span>     #添加配置内容</span><br></pre></td></tr></table></figure><p>在<code>spark-env.sh</code>末尾添加以下内容（这是我的配置，你可以自行修改）：<br><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">SCALA_HOME</span>=/home/spark/workspace/scala-2.10.4</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">JAVA_HOME</span>=/home/spark/workspace/jdk1.7.0_75</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">HADOOP_HOME</span>=/home/spark/workspace/hadoop-2.6.0</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">HADOOP_CONF_DIR</span>=<span class="variable">$HADOOP_HOME</span>/etc/hadoop</span><br><span class="line"><span class="attribute">SPARK_MASTER_IP</span>=master</span><br><span class="line"><span class="attribute">SPARK_LOCAL_DIRS</span>=/home/spark/workspace/spark-1.3.0</span><br><span class="line"><span class="attribute">SPARK_DRIVER_MEMORY</span>=1G</span><br></pre></td></tr></table></figure></p><p>注：在设置Worker进程的CPU个数和内存大小，要注意机器的实际硬件条件，如果配置的超过当前Worker节点的硬件条件，Worker进程会启动失败。</p><p><code>vi slaves</code>在slaves文件下填上slave主机名：<br><figure class="highlight"><table><tr><td class="code"><pre><span class="line">slave1</span><br><span class="line">slave2</span><br></pre></td></tr></table></figure></p><p>将配置好的<code>spark-1.3.0</code>文件夹分发给所有slaves吧<br><figure class="highlight elixir"><table><tr><td class="code"><pre><span class="line">scp -r ~<span class="regexp">/workspace/spark</span><span class="number">-1.3</span>.0 spark<span class="variable">@slave1</span><span class="symbol">:~/workspace/</span></span><br></pre></td></tr></table></figure></p><h3 id="启动Spark"><a href="#启动Spark" class="headerlink" title="启动Spark"></a>启动Spark</h3><figure class="highlight vim"><table><tr><td class="code"><pre><span class="line">sbin/start-<span class="keyword">all</span>.<span class="keyword">sh</span></span><br></pre></td></tr></table></figure><h3 id="验证-Spark-是否安装成功"><a href="#验证-Spark-是否安装成功" class="headerlink" title="验证 Spark 是否安装成功"></a>验证 Spark 是否安装成功</h3><p>用<code>jps</code>检查，在 master 上应该有以下几个进程：<br><figure class="highlight lsl"><table><tr><td class="code"><pre><span class="line">$ jps</span><br><span class="line"><span class="number">7949</span> Jps</span><br><span class="line"><span class="number">7328</span> SecondaryNameNode</span><br><span class="line"><span class="number">7805</span> Master</span><br><span class="line"><span class="number">7137</span> NameNode</span><br><span class="line"><span class="number">7475</span> ResourceManager</span><br></pre></td></tr></table></figure></p><p>在 slave  上应该有以下几个进程：<br><figure class="highlight lsl"><table><tr><td class="code"><pre><span class="line">$jps</span><br><span class="line"><span class="number">3132</span> DataNode</span><br><span class="line"><span class="number">3759</span> Worker</span><br><span class="line"><span class="number">3858</span> Jps</span><br><span class="line"><span class="number">3231</span> NodeManager</span><br></pre></td></tr></table></figure></p><p>进入Spark的Web管理页面： <a href="http://master:8080" target="_blank" rel="noopener">http://master:8080</a><br><img src="http://ww4.sinaimg.cn/mw690/81b78497jw1eqwa7uqndoj20za0p8afa.jpg" alt></p><h3 id="运行示例"><a href="#运行示例" class="headerlink" title="运行示例"></a>运行示例</h3><figure class="highlight crystal"><table><tr><td class="code"><pre><span class="line"><span class="comment">#本地模式两线程运行</span></span><br><span class="line">./bin/run-example SparkPi <span class="number">10</span> --master local[<span class="number">2</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">#Spark Standalone 集群模式运行</span></span><br><span class="line">./bin/spark-submit \</span><br><span class="line">  --<span class="class"><span class="keyword">class</span> <span class="title">org</span>.<span class="title">apache</span>.<span class="title">spark</span>.<span class="title">examples</span>.<span class="title">SparkPi</span> \</span></span><br><span class="line">  --master <span class="symbol">spark:</span>/<span class="regexp">/master:7077 \</span></span><br><span class="line"><span class="regexp">  lib/spark</span>-examples-<span class="number">1.3</span>.<span class="number">0</span>-hadoop2.<span class="number">4.0</span>.jar \</span><br><span class="line">  <span class="number">100</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#Spark on YARN 集群上 yarn-cluster 模式运行</span></span><br><span class="line">./bin/spark-submit \</span><br><span class="line">    --<span class="class"><span class="keyword">class</span> <span class="title">org</span>.<span class="title">apache</span>.<span class="title">spark</span>.<span class="title">examples</span>.<span class="title">SparkPi</span> \</span></span><br><span class="line">    --master yarn-cluster \  <span class="comment"># can also be `yarn-client`</span></span><br><span class="line">    <span class="class"><span class="keyword">lib</span>/<span class="title">spark</span>-<span class="title">examples</span>*.<span class="title">jar</span> \</span></span><br><span class="line">    <span class="number">10</span></span><br></pre></td></tr></table></figure><p>注意 Spark on YARN 支持两种运行模式，分别为<code>yarn-cluster</code>和<code>yarn-client</code>，具体的区别可以看<a href="http://www.iteblog.com/archives/1223" target="_blank" rel="noopener">这篇博文</a>，从广义上讲，yarn-cluster适用于生产环境；而yarn-client适用于交互和调试，也就是希望快速地看到application的输出。</p>]]></content>
    
    <summary type="html">
    
      最近毕设需要用到 Spark 集群，所以就记录下了部署的过程。我们知道 Spark 官方提供了三种集群部署方案： Standalone, Mesos, YARN。其中 Standalone 最为方便，本文主要讲述结合 YARN 的部署方案。
    
    </summary>
    
      <category term="分布式系统" scheme="http://wuchong.me/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
    
      <category term="Spark" scheme="http://wuchong.me/tags/Spark/"/>
    
  </entry>
  
  <entry>
    <title>Git 常用技能</title>
    <link href="http://wuchong.me/blog/2015/03/30/git-useful-skills/"/>
    <id>http://wuchong.me/blog/2015/03/30/git-useful-skills/</id>
    <published>2015-03-30T02:39:39.000Z</published>
    <updated>2022-08-03T06:46:44.506Z</updated>
    
    <content type="html"><![CDATA[<p>学习使用 Git 已经一年有余，一些常用技能也用的炉火纯青了，但偶尔碰到一些生僻的技能，总是需要去 Google，第二次用时又忘了。所以这是一篇我自认为比较重要的 Git 技能表，主要供自己查阅使用，反复查阅能够加深印象，提升技能熟练度。如果你是还不知道 Git 是什么，建议先阅读 <a href="http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000" target="_blank" rel="noopener">廖雪峰的Git教程</a>。</p><h2 id="工作流"><a href="#工作流" class="headerlink" title="工作流"></a>工作流</h2><p>Git 最核心的一个概念就是工作流。工作区(Workspace)是电脑中实际的目录；暂存区(Index)像个缓存区域，临时保存你的改动；最后是版本库(Repository)，分为本地仓库和远程仓库。下图真是一图胜千言啊，就无耻盗图了。</p><p><img src="http://ww4.sinaimg.cn/mw690/81b78497jw1eqnk1bkyaij20e40bpjsm.jpg" alt></p><a id="more"></a><h2 id="远程仓库"><a href="#远程仓库" class="headerlink" title="远程仓库"></a>远程仓库</h2><h3 id="添加远程仓库"><a href="#添加远程仓库" class="headerlink" title="添加远程仓库"></a>添加远程仓库</h3><figure class="highlight applescript"><table><tr><td class="code"><pre><span class="line">git remote add origin git@server-<span class="built_in">name</span>:path/repo-<span class="built_in">name</span>.git  <span class="comment">#添加一个远程库</span></span><br></pre></td></tr></table></figure><h3 id="查看远程仓库"><a href="#查看远程仓库" class="headerlink" title="查看远程仓库"></a>查看远程仓库</h3><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">git</span> remote      <span class="comment">#要查看远程库的信息</span></span><br><span class="line">git remote -v   <span class="comment">#显示更详细的信息</span></span><br></pre></td></tr></table></figure><h3 id="推送分支"><a href="#推送分支" class="headerlink" title="推送分支"></a>推送分支</h3><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line">git push origin <span class="keyword">master</span>    <span class="title">#推送到远程master</span>分支</span><br></pre></td></tr></table></figure><h3 id="抓取分支"><a href="#抓取分支" class="headerlink" title="抓取分支"></a>抓取分支</h3><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line">git <span class="keyword">clone</span> <span class="title">git</span>@server-name:path/repo-name.git   <span class="comment">#克隆远程仓库到本地(能看到master分支)</span></span><br><span class="line">git checkout -b dev origin/dev  <span class="comment">#创建远程origin的dev分支到本地，并命名为dev</span></span><br><span class="line">git checkout origin/dev --track <span class="comment">#与上面效果一样</span></span><br><span class="line">git pull origin <span class="keyword">master</span>          <span class="title">#从远程分支进行更新 </span></span><br><span class="line"><span class="title">git</span> fetch origin <span class="keyword">master</span>         <span class="title">#获取远程分支上的数据</span></span><br></pre></td></tr></table></figure><h3 id="抓取GitHub上某个pull-request到本地"><a href="#抓取GitHub上某个pull-request到本地" class="headerlink" title="抓取GitHub上某个pull request到本地"></a>抓取GitHub上某个pull request到本地</h3><figure class="highlight groovy"><table><tr><td class="code"><pre><span class="line">git fetch origin pull<span class="regexp">/ID/</span><span class="string">head:</span>BRANCHNAME</span><br><span class="line">git checkout BRANCHNAME</span><br></pre></td></tr></table></figure><p><code>$ git branch --set-upstream branch-name origin/branch-name</code>，可以建立起本地分支和远程分支的关联，之后可以直接<code>git pull</code>从远程抓取分支。</p><p>另外，<code>git pull</code> = <code>git fetch</code> + <code>merge</code> to local</p><h3 id="删除远程分支"><a href="#删除远程分支" class="headerlink" title="删除远程分支"></a>删除远程分支</h3><figure class="highlight armasm"><table><tr><td class="code"><pre><span class="line">$ git <span class="keyword">push </span>origin --delete <span class="keyword">bugfix</span></span><br><span class="line"><span class="keyword">To </span>https://github.com/wuchong/jacman</span><br><span class="line"> - [deleted]         <span class="keyword">bugfix</span></span><br><span class="line"><span class="keyword"># </span>或者直接<span class="keyword">push一个空分支</span></span><br><span class="line"><span class="keyword">$ </span>git <span class="keyword">push </span>origin :<span class="keyword">bugfix</span></span><br><span class="line"><span class="keyword">To </span>https://github.com/wuchong/jacman</span><br><span class="line"> - [deleted]         <span class="keyword">bugfix</span></span><br></pre></td></tr></table></figure><h3 id="更新远程分支信息"><a href="#更新远程分支信息" class="headerlink" title="更新远程分支信息"></a>更新远程分支信息</h3><p>项目往前推进的过程中，远程仓库上经常会增加一些分支、删除一些分支。 所以有时需要与远程同步下分支信息。</p><figure class="highlight ebnf"><table><tr><td class="code"><pre><span class="line"><span class="attribute">git fetch -p</span></span><br></pre></td></tr></table></figure><p><code>-p</code>就是修剪的意思。它在fetch之后删除掉没有与远程分支对应的本地分支，并且同步一些远程新创建的分支和tag。</p><h2 id="历史管理"><a href="#历史管理" class="headerlink" title="历史管理"></a>历史管理</h2><h3 id="查看历史"><a href="#查看历史" class="headerlink" title="查看历史"></a>查看历史</h3><figure class="highlight applescript"><table><tr><td class="code"><pre><span class="line">git <span class="built_in">log</span> <span class="comment">--pretty=oneline filename #一行显示</span></span><br><span class="line">git <span class="built_in">log</span> -p <span class="number">-2</span>      <span class="comment">#显示最近2次提交内容的差异</span></span><br><span class="line">git show cb926e7   <span class="comment">#查看某次修改</span></span><br></pre></td></tr></table></figure><h3 id="版本回退"><a href="#版本回退" class="headerlink" title="版本回退"></a>版本回退</h3><figure class="highlight dsconfig"><table><tr><td class="code"><pre><span class="line"><span class="string">git </span><span class="string">reset </span><span class="built_in">--hard</span> <span class="string">HEAD^</span>    <span class="comment">#回退到上一个版本</span></span><br><span class="line"><span class="string">git </span><span class="string">reset </span><span class="built_in">--hard</span> <span class="string">cb926e7 </span> <span class="comment">#回退到具体某个版</span></span><br><span class="line"><span class="string">git </span><span class="string">reflog </span>               <span class="comment">#查看命令历史,常用于帮助找回丢失掉的commit</span></span><br></pre></td></tr></table></figure><p>用HEAD表示当前版本，上一个版本就是<code>HEAD^</code>，上上一个版本就是<code>HEAD^^</code>，<code>HEAD~100</code>就是上100个版本。</p><h2 id="管理修改"><a href="#管理修改" class="headerlink" title="管理修改"></a>管理修改</h2><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">git status              <span class="comment">#查看工作区、暂存区的状态</span></span><br><span class="line">git checkout <span class="comment">-- &lt;file&gt;  #丢弃工作区上某个文件的修改</span></span><br><span class="line">git <span class="keyword">reset</span> <span class="keyword">HEAD</span> &lt;<span class="keyword">file</span>&gt;   <span class="comment">#丢弃暂存区上某个文件的修改，重新放回工作区</span></span><br></pre></td></tr></table></figure><h3 id="查看差异"><a href="#查看差异" class="headerlink" title="查看差异"></a>查看差异</h3><figure class="highlight mipsasm"><table><tr><td class="code"><pre><span class="line">git <span class="keyword">diff </span>             <span class="comment">#查看未暂存的文件更新 </span></span><br><span class="line">git <span class="keyword">diff </span>--<span class="keyword">cached </span>    <span class="comment">#查看已暂存文件的更新 </span></span><br><span class="line">git <span class="keyword">diff </span>HEAD -- readme.txt  <span class="comment">#查看工作区和版本库里面最新版本的区别</span></span><br><span class="line">git <span class="keyword">diff </span>&lt;source_branch&gt; &lt;target_branch&gt;  <span class="comment">#在合并改动之前，预览两个分支的差异</span></span><br></pre></td></tr></table></figure><p>使用内建的图形化git：<code>gitk</code>，可以更方便清晰地查看差异。当然 Github 客户端也不错。</p><h3 id="删除文件"><a href="#删除文件" class="headerlink" title="删除文件"></a>删除文件</h3><figure class="highlight cmake"><table><tr><td class="code"><pre><span class="line">git rm &lt;<span class="keyword">file</span>&gt;           <span class="comment">#直接删除文件</span></span><br><span class="line">git rm --cached &lt;<span class="keyword">file</span>&gt;  <span class="comment">#删除文件暂存状态</span></span><br></pre></td></tr></table></figure><h3 id="储藏和恢复"><a href="#储藏和恢复" class="headerlink" title="储藏和恢复"></a>储藏和恢复</h3><figure class="highlight applescript"><table><tr><td class="code"><pre><span class="line">git stash           <span class="comment">#储藏当前工作</span></span><br><span class="line">git stash <span class="built_in">list</span>      <span class="comment">#查看储藏的工作现场</span></span><br><span class="line">git stash apply     <span class="comment">#恢复工作现场，stash内容并不删除</span></span><br><span class="line">git stash pop       <span class="comment">#恢复工作现场，并删除stash内容</span></span><br></pre></td></tr></table></figure><h2 id="分支管理"><a href="#分支管理" class="headerlink" title="分支管理"></a>分支管理</h2><h3 id="创建分支"><a href="#创建分支" class="headerlink" title="创建分支"></a>创建分支</h3><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line">git branch develop              <span class="comment">#只创建分支</span></span><br><span class="line">git checkout -b <span class="keyword">master</span> <span class="title">develop</span>  <span class="comment">#创建并切换到 develop 分支</span></span><br></pre></td></tr></table></figure><h3 id="合并分支"><a href="#合并分支" class="headerlink" title="合并分支"></a>合并分支</h3><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line">git checkout <span class="keyword">master</span>         <span class="title">#切换到主分支</span></span><br><span class="line"><span class="title">git</span> merge --no-ff develop   <span class="comment">#把 develop 合并到 master 分支，no-ff 选项的作用是保留原分支记录</span></span><br><span class="line">git branch -d develop       <span class="comment">#删除 develop 分支</span></span><br></pre></td></tr></table></figure><h2 id="标签"><a href="#标签" class="headerlink" title="标签"></a>标签</h2><h3 id="显示标签"><a href="#显示标签" class="headerlink" title="显示标签"></a>显示标签</h3><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line">git <span class="keyword">tag</span>         <span class="title">#列出现有标签 </span></span><br><span class="line"><span class="title">git</span> show <span class="tag">&lt;tagname&gt;</span>  <span class="comment">#显示标签信息</span></span><br></pre></td></tr></table></figure><p>###创建标签<br><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line">git <span class="keyword">tag</span> <span class="title">v0</span>.<span class="number">1</span>    <span class="comment">#新建标签，默认位 HEAD</span></span><br><span class="line">git <span class="keyword">tag</span> <span class="title">v0</span>.<span class="number">1</span> cb926e7  <span class="comment">#对指定的 commit id 打标签</span></span><br><span class="line">git <span class="keyword">tag</span> <span class="title">-a</span> v0.<span class="number">1</span> -m '<span class="keyword">version</span> <span class="number">0.1</span> released'   <span class="comment">#新建带注释标签</span></span><br></pre></td></tr></table></figure></p><h3 id="操作标签"><a href="#操作标签" class="headerlink" title="操作标签"></a>操作标签</h3><figure class="highlight elixir"><table><tr><td class="code"><pre><span class="line">git checkout &lt;tagname&gt;        <span class="comment">#切换到标签</span></span><br><span class="line"></span><br><span class="line">git push origin &lt;tagname&gt;     <span class="comment">#推送分支到源上</span></span><br><span class="line">git push origin --tags        <span class="comment">#一次性推送全部尚未推送到远程的本地标签</span></span><br><span class="line"></span><br><span class="line">git tag -d &lt;tagname&gt;          <span class="comment">#删除标签</span></span><br><span class="line">git push origin <span class="symbol">:refs/tags/&lt;tagname&gt;</span>      <span class="comment">#删除远程标签</span></span><br></pre></td></tr></table></figure><h2 id="Git-设置"><a href="#Git-设置" class="headerlink" title="Git 设置"></a>Git 设置</h2><p>设置 commit 的用户和邮箱</p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">git<span class="built_in"> config </span>user.name <span class="string">"xx"</span>               #设置 commit 的用户</span><br><span class="line">git<span class="built_in"> config </span>user.email <span class="string">"xx@xx.com"</span>       #设置 commit 的邮箱</span><br><span class="line">git commit --amend --author <span class="string">"Jark Wu &lt;imjark@gmail.com&gt;"</span>    #修改上次提交的用户信息</span><br><span class="line">git<span class="built_in"> config </span>format.pretty oneline        #显示历史记录时，每个提交的信息只显示一行</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      学习使用 Git 已经一年有余，一些常用技能也用的炉火纯青了，但偶尔碰到一些生僻的技能，总是需要去 Google，第二次用时又忘了。所以这是一篇我自认为比较重要的 Git 技能表，主要供自己查阅使用，反复查阅能够加深印象，提升技能熟练度。
    
    </summary>
    
      <category term="杂项资源" scheme="http://wuchong.me/categories/%E6%9D%82%E9%A1%B9%E8%B5%84%E6%BA%90/"/>
    
    
      <category term="Git" scheme="http://wuchong.me/tags/Git/"/>
    
  </entry>
  
</feed>
