<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://wzx140.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://wzx140.github.io//" rel="alternate" type="text/html" /><updated>2025-02-11T23:59:37+08:00</updated><id>https://wzx140.github.io//feed.xml</id><title type="html">WZX’s Blog</title><subtitle>学习是一种信仰</subtitle><author><name>wzx</name></author><entry><title type="html">Iceberg 存储结构</title><link href="https://wzx140.github.io//2024/01/27/1-iceberg/" rel="alternate" type="text/html" title="Iceberg 存储结构" /><published>2024-01-27T00:00:00+08:00</published><updated>2024-01-27T00:00:00+08:00</updated><id>https://wzx140.github.io//2024/01/27/1-iceberg</id><content type="html" xml:base="https://wzx140.github.io//2024/01/27/1-iceberg/"><![CDATA[<p>Iceberg 是数据湖三剑客之一，以下简要分析 Iceberg 的元数据、存储结构及读写流程</p>

<h1 id="背景">背景</h1>

<p>与 hudi 类似，面向海量数据分析场景的高效存储格式。支持 Full schema evolution，time travel 和事务性管理。以下简要分析 Iceberg 的元数据、存储结构和读写流程</p>

<h1 id="元数据">元数据</h1>

<p><img src="https://wzx140.github.io//assets/img/2024-1-27-1-1.jpeg" alt="" width="400" /></p>

<ul>
  <li>metadata: 提交级别的元数据，包含 snapshots, schema, statics</li>
  <li>snapshot: 某个时刻的 snapshot，每次提交都会产生新的 snapshot，包含一组 manifest
    <ul>
      <li>Sequence Nubmer：每次提交都会产生，相当于 version</li>
      <li>manifest: 每次提交的一组文件
        <ul>
          <li>data file: parquet</li>
          <li>delete file
            <ul>
              <li>Position deletes: 文件名, row offset</li>
              <li>Equality deletes: 匹配条件</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h1 id="读写流程">读写流程</h1>

<p>以下 SQL 为例，iceberg 两种表格式 COW 和 MOR 的读写流程</p>

<pre><code class="language-SQL">CREATE TABLE testhive.default.table (
  id INT, 
  dep STRING
) USING iceberg
TBLPROPERTIES (
  'write.format.default' = 'parquet',
  'write.delete.mode' = 'copy-on-write',
  'write.update.mode' = 'copy-on-write',
  'write.delete.mode' = 'merge-on-read',
  'write.update.mode' = 'merge-on-read',
  'format-version' = '2'
);

INSERT INTO TABLE testhive.default.table VALUES (1, 'software'), (2, 'hr'), (3, 'hehe');

UPDATE testhive.default.table SET id = -1 WHERE dep = 'hr';

DELETE FROM testhive.default.table WHERE id = 3;
</code></pre>

<h2 id="copy-on-write">Copy On Write</h2>

<p><img src="https://wzx140.github.io//assets/img/2024-1-27-1-2.png" alt="" /></p>

<ol>
  <li>第一次写入生成 snapshot0，新增了两个 parquet 文件</li>
  <li>第二次写入生成 snapshot1，全量读 snapshot1 根据 update filter 过滤后，重写 data0-1 为 data1-0</li>
  <li>第三次写入生成 snapshot2，全量读 snapshot2 根据 delete filter 过滤后，重写 data1-0 为 data2-0</li>
  <li>查询时读取 snapshot2 的 metafestList(manifest1-m0, manifest2-m0, manifest2-m1)，读取 data0-0 和 data2-0 两个 parquet 文件</li>
</ol>

<h2 id="merge-on-read">Merge On Read</h2>

<p><img src="https://wzx140.github.io//assets/img/2024-1-27-1-3.png" alt="" /></p>

<ol>
  <li>第一次写入生成 snapshot0，新增了两个 parquet 文件</li>
  <li>第二次写入生成 snapshot1，全量读 snapshot1 根据 update filter 过滤后，新增 pos-delete1-1 删除更新前的数据，新增 data1-0 保存更新后的数据</li>
  <li>第三次写入生成 snapshot2，全量读 snapshot2 根据 delete filter 过滤后，新增 pos-delete2-0 删除数据</li>
  <li>查询时读取 snapshot2 的 metafestList(manifest0-m0, manifest1-m0, manifest1-m1, manifest2-mo)，读取 data0-0 和 data1-0 两个 parquet 文件，合并 data0-1, pos-delete1-1, pos-delete2-0 三个 parquet 文件。<strong>合并时如果数据较少，在内存中构建 bitmap 根据位置进行过滤；数据较大，则使用 sortMerge 方式</strong></li>
</ol>

<h1 id="delete-file">Delete file</h1>

<h2 id="写入流程">写入流程</h2>

<p>Spark 的 MOR 表会写入 pos-delete，Flink 会缓存一个 batch 的数据，根据缓存判断写 pos-delete 还是 eq-delete</p>

<ol>
  <li>判断 rowkind 为 insert/update-after
    <ol>
      <li>放入 insertRowMap Map&lt;data, pos&gt;</li>
      <li>写 dataFile</li>
    </ol>
  </li>
  <li>判断 rowkind 为 delete/update-before
    <ol>
      <li>insertRowMap 中存在，写 pos-delete</li>
      <li>insertRowMap 中不存在，写 eq-delete</li>
    </ol>
  </li>
</ol>

<h2 id="读取流程">读取流程</h2>

<p>Position deletes 文件会进行以下合并</p>

<ol>
  <li>合并时如果数据较少，在内存中构建 bitmap 根据位置进行过滤</li>
  <li>数据较大，则使用 sortMerge 方式</li>
</ol>

<p>Equality deletes 文件会进行以下合并</p>

<ol>
  <li>找到所有 seq-num 小于 deleteFile 的 dataFile</li>
  <li>进行 anti-join</li>
</ol>

<h1 id="分区">分区</h1>

<p>DDL 中定义了普通列和分区列的映射关系</p>

<pre><code class="language-SQL">CREATE TABLE testhive.default.table (
    id bigint,
    date string)
USING iceberg
PARTITIONED BY (date, bucket(2, id));

INSERT INTO TABLE testhive.default.table VALUES 
  (0, '2022-01-01'), 
  (1, '2022-01-01'), 
  (2, '2022-01-01'), 
  (3, '2022-01-01'), 
  (0, '2022-01-02'), 
  (1, '2022-01-02');
</code></pre>

<p>实际存储上产生了类似 parition by date + clustered by (id) into 2 buckets 的效果</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── date=2022-01-01
│   ├── id_bucket=0
│   │   └── 00000-0-1f12309b-9c48-4fcf-916a-09094cbe52c0-00001.parquet
│   └── id_bucket=1
│       └── 00001-1-1df1c41b-1f28-4512-889f-9ed6b9a7b4b0-00001.parquet
└── date=2022-01-02
    └── id_bucket=0
        └── 00001-1-1df1c41b-1f28-4512-889f-9ed6b9a7b4b0-00002.parquet
</code></pre></div></div>]]></content><author><name>wzx</name></author><category term="LakeHouse" /><summary type="html"><![CDATA[Iceberg 是数据湖三剑客之一，以下简要分析 Iceberg 的元数据、存储结构及读写流程]]></summary></entry><entry><title type="html">数据湖和 Olap 引擎的存储结构对比</title><link href="https://wzx140.github.io//2024/01/27/1-merge/" rel="alternate" type="text/html" title="数据湖和 Olap 引擎的存储结构对比" /><published>2024-01-27T00:00:00+08:00</published><updated>2024-01-27T00:00:00+08:00</updated><id>https://wzx140.github.io//2024/01/27/1-merge</id><content type="html" xml:base="https://wzx140.github.io//2024/01/27/1-merge/"><![CDATA[<p>Olap 引擎或者数据湖基本都借鉴了 LSM 的思想来平衡读写性能，以及处理 update/delete</p>

<h1 id="clickhouse">ClickHouse</h1>

<h2 id="文件结构">文件结构</h2>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">my_first_table</span>
<span class="p">(</span>
    <span class="n">user_id</span>     <span class="n">UInt32</span><span class="p">,</span>
    <span class="n">message</span>     <span class="n">String</span><span class="p">,</span>
    <span class="nb">timestamp</span>   <span class="nb">DateTime</span><span class="p">,</span>
    <span class="n">metric</span>      <span class="n">Float32</span><span class="p">,</span>
    <span class="nb">date</span>        <span class="n">UInt32</span>
<span class="p">)</span>
<span class="n">ENGINE</span> <span class="o">=</span> <span class="n">MergeTree</span>
<span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="nb">timestamp</span><span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">date</span><span class="p">;</span>


<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">my_first_table</span> <span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="nb">timestamp</span><span class="p">,</span> <span class="n">metric</span><span class="p">,</span> <span class="nb">date</span><span class="p">)</span> <span class="k">VALUES</span>
    <span class="p">(</span><span class="mi">101</span><span class="p">,</span> <span class="s1">'Hello, ClickHouse!'</span><span class="p">,</span>                                 <span class="n">now</span><span class="p">(),</span>       <span class="o">-</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span>      <span class="mi">2</span><span class="p">),</span>
    <span class="p">(</span><span class="mi">102</span><span class="p">,</span> <span class="s1">'Insert a lot of rows per batch'</span><span class="p">,</span>                     <span class="n">yesterday</span><span class="p">(),</span> <span class="mi">1</span><span class="p">.</span><span class="mi">41421</span><span class="p">,</span>   <span class="mi">2</span><span class="p">),</span>
    <span class="p">(</span><span class="mi">102</span><span class="p">,</span> <span class="s1">'Sort your data based on your commonly-used queries'</span><span class="p">,</span> <span class="n">today</span><span class="p">(),</span>     <span class="mi">2</span><span class="p">.</span><span class="mi">718</span><span class="p">,</span>     <span class="mi">2</span><span class="p">),</span>
    <span class="p">(</span><span class="mi">101</span><span class="p">,</span> <span class="s1">'Granules are the smallest chunks of data read'</span><span class="p">,</span>      <span class="n">now</span><span class="p">()</span> <span class="o">+</span> <span class="mi">5</span><span class="p">,</span>   <span class="mi">3</span><span class="p">.</span><span class="mi">14159</span><span class="p">,</span>   <span class="mi">2</span><span class="p">);</span>

<span class="k">DELETE</span> <span class="k">FROM</span> <span class="n">my_first_table</span>
<span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="mi">101</span><span class="p">;</span>

<span class="n">OPTIMIZE</span> <span class="k">TABLE</span> <span class="n">my_first_table</span><span class="p">;</span>
</code></pre></div></div>

<p>执行以上 sql 可以得到以下目录结构</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- default
    - my_first_table
    		<span class="c"># 第一次 insert 写的 part 目录</span>
        - 2_1_1_0
        <span class="c"># 第二次 delete 写的 part 目录</span>
        - 2_1_1_0_2
        <span class="c"># 第三次执行完 compaction 的结果</span>
        - 2_1_1_1_2
</code></pre></div></div>

<p>每次对分区的写入都会产生一个新的目录称为 part，一个分区下包含多个 part。<strong>part 目录的命名规则</strong>为 {partitionId}_{min_block_num}_{max_block_num}_{level}_{version}</p>
<ul>
  <li>partitionId: 分区值</li>
  <li>min/max block num: 包含的最小、最大的 lsm layer block num。part 初始写入时这两个值相等</li>
  <li>level: lsm level。每次 compact 都会 level + 1</li>
  <li>version: 每次 update/delete 都会 version+1，<strong>用于合并时确定影响范围内的 part</strong>，类似 iceberg 中的 Sequence Nubmer</li>
</ul>

<p>以下文件都是 <strong>part 目录中包含的文件</strong></p>

<p><strong>元数据</strong></p>
<ul>
  <li>checksum.txt: 各文件的检验码</li>
  <li>serialization.json: 序列化信息</li>
  <li>default_compression_codec.txt: 压缩信息</li>
  <li>metadata_version.txt: 元数据版本信息</li>
  <li>columns.txt: schema</li>
  <li>count.txt: 行数</li>
</ul>

<p><strong>索引</strong></p>
<ul>
  <li>primary.cidx: 主键索引，用于存放主键与 granule 的映射关系</li>
  <li>minmax_date.idx: 分区列的 min-max 索引</li>
  <li>partition.dat: 从分区列计算出分区值的方法</li>
</ul>

<p><strong>数据</strong>: 根据写入数据大小使用 compact 和 wide 两种模式，<strong>compact 模式下所有列都会写入一个文件，wide 模式下每个列都会写一个文件</strong></p>
<ul>
  <li>data.bin: compact 数据文件，由压缩后的 bin block 组成</li>
  <li>bin block: 每个 bin block 控制在 64KB~1MB，相比于整个文件一起压缩，有以下优缺点
    <ol>
      <li>存在额外压缩计算开销，压缩比例不高</li>
      <li>bin block 为最小的 IO 单元，读时相比于加载整个文件，可以减少 IO 耗时</li>
    </ol>
  </li>
  <li>data.cmrk3: 标记文件，用于存放 <strong>granule 与 bin block 的映射关系</strong></li>
</ul>

<h2 id="索引">索引</h2>

<h3 id="主键索引">主键索引</h3>
<p>稀疏索引：<strong>索引条目间隔 index_granularity(默认 8192 条)条数据记录</strong>，保存至 primary.idx 文件内</p>
<ul>
  <li>索引和数据按照 PRIMARY KEY 排序</li>
  <li><strong>数据被索引切分成多个 granule 数据块，通过二分搜索可以快速定位 granule</strong></li>
  <li>索引数据常驻内存</li>
</ul>

<h3 id="二级索引">二级索引</h3>
<p><strong>二级索引保存多个 granule 的统计信息，用于跳过 granule，所以也称跳数索引</strong></p>
<ul>
  <li>minmax: 保存极值</li>
  <li>set: 保存索引列的 set。当唯一值数量超过 max_size 时，就不保存这个 granule 的 set</li>
  <li>bloom filter: 保存布隆过滤器
    <ul>
      <li>ngrambf_v1</li>
      <li>tokenbf_v1</li>
    </ul>
  </li>
</ul>

<h2 id="写入流程">写入流程</h2>
<p>每次写入一个新的 part 目录</p>

<h2 id="读取流程">读取流程</h2>
<ol>
  <li>part 目录下的 MinMax 索引，确定到 part 目录级别</li>
  <li>主键索引，确定到 granule 级别</li>
  <li>二级索引，对 granule 做进一步过滤</li>
  <li>读出过滤后的 granule 数据根据主键进行合并</li>
</ol>

<h2 id="优缺点">优缺点</h2>
<p>优点：单机 Olap 查询性能高效</p>

<p>缺点：</p>
<ul>
  <li>没有事务性保证</li>
  <li>通过分片来支持分布式存储，分布式 join 在两张表都很大的情况下效率很低</li>
</ul>

<h1 id="starrocks">StarRocks</h1>

<h2 id="文件结构-1">文件结构</h2>
<p>和 Hbase 比较类似，表数据被水平切分为多个 tablet 分别存储在 BE 上，包含以下几个部分</p>
<ul>
  <li>Rowset：一个 Tablet 被水平切分为多个 Rowset，<strong>每个 Rowset 以列存文件的形式存储</strong></li>
  <li>Meta：保存 Tablet 的版本历史以及每个版本的信息，比如包含哪些 Rowset 等。序列化后存储在 RocksDB 中，为了快速访问会缓存在内存中</li>
  <li>DelVector：<strong>记录每个 Rowset 中被标记为删除的行</strong>，同样保存在 RocksDB 中，也会缓存在内存中以便能够快速访问</li>
  <li>Primary Index：<strong>保存主键与该记录所在位置的映射。以 LSM tree 的形式分别存储在内存和磁盘上</strong></li>
</ul>

<h2 id="索引-1">索引</h2>
<p>由于 StarRocks 不要求 rowset 内有序，所以使用 HashMap 的方式保存哈希索引。整体类似 LSM Tree</p>

<p><img src="https://wzx140.github.io//assets/img/2024-2-5-1.png" alt="" /></p>

<ul>
  <li>L0 层存在 WAL 和一个内存中的 HashMap</li>
  <li>L1 层为多个从 L0 层 flush 的文件，每个文件大概 1M 左右大小</li>
</ul>

<h2 id="写入流程-1">写入流程</h2>
<ol>
  <li>Tablet 先将数据写到 MemTable，满了后触发 flush</li>
  <li>flush 前需要做以下操作
 a. <strong>Sort 先按主键对数据排序</strong>
 b. <strong>Merge 对主键相同的多条记录进行合并</strong>
 c. <strong>Split 把这批操作中的 Delete+Insert 和 Delete 操作拆分开来</strong></li>
  <li><strong>Delete 数据集会根据主键 index 生成 DelVector</strong></li>
  <li><strong>Delete+Insert 数据集除了生成 DelVector 还会写入新的 rowset</strong></li>
</ol>

<h2 id="优缺点-1">优缺点</h2>
<p>优点：读取效率高</p>
<ol>
  <li><strong>读取时不需要 merge</strong></li>
  <li><strong>非主键列 filter 也可以下推</strong></li>
  <li><strong>无需读取主键列</strong></li>
</ol>

<p>缺点：写入效率相对较低，除了写新增数据，还要更新历史的 DelVector</p>

<h1 id="总结">总结</h1>
<p>更多参考 <a href="/2021/04/19/hudi/">hudi</a>, <a href="/2024/01/27/1-iceberg/">iceberg</a></p>

<table>
  <thead>
    <tr>
      <th><strong>存储引擎</strong></th>
      <th><strong>更新方式</strong></th>
      <th><strong>存储方式</strong></th>
      <th><strong>filter 下推</strong></th>
      <th><strong>merge 时机</strong></th>
      <th><strong>读取性能</strong></th>
      <th><strong>写入性能</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ClickHouse</td>
      <td>merge on read</td>
      <td>有序列存</td>
      <td>不支持</td>
      <td>读</td>
      <td>较低</td>
      <td>高</td>
    </tr>
    <tr>
      <td>Hudi MOR</td>
      <td>merge on read</td>
      <td>存量列存，增量行存</td>
      <td>不支持</td>
      <td>读</td>
      <td>低</td>
      <td>高</td>
    </tr>
    <tr>
      <td>Hudi/Iceberg COW</td>
      <td>copy on write</td>
      <td>列存</td>
      <td>支持</td>
      <td>写</td>
      <td>高</td>
      <td>低</td>
    </tr>
    <tr>
      <td>Iceberg MOR</td>
      <td>delete and insert</td>
      <td>列存 + pos/eq delete</td>
      <td>支持</td>
      <td>写</td>
      <td>较高</td>
      <td>较低</td>
    </tr>
    <tr>
      <td>StarRock</td>
      <td>delete and insert</td>
      <td>有序列存 + delete vector</td>
      <td>支持</td>
      <td>写</td>
      <td>高</td>
      <td>较低</td>
    </tr>
  </tbody>
</table>

<h1 id="reference">Reference</h1>
<p>https://zhuanlan.zhihu.com/p/566219916</p>]]></content><author><name>wzx</name></author><category term="LakeHouse" /><summary type="html"><![CDATA[Olap 引擎或者数据湖基本都借鉴了 LSM 的思想来平衡读写性能，以及处理 update/delete]]></summary></entry><entry><title type="html">Flink 剖析(一) 基本概念与架构</title><link href="https://wzx140.github.io//2024/01/13/1-flink-overview/" rel="alternate" type="text/html" title="Flink 剖析(一) 基本概念与架构" /><published>2024-01-13T00:00:00+08:00</published><updated>2024-01-13T00:00:00+08:00</updated><id>https://wzx140.github.io//2024/01/13/1-flink-overview</id><content type="html" xml:base="https://wzx140.github.io//2024/01/13/1-flink-overview/"><![CDATA[<p>Flink 相对于 Spark Streaming 的微批处理架构，是真正意义上的流式处理架构。具备批流一体，高吞吐低延迟，状态机制，可靠的容错机制等优势。</p>

<h2 id="优势">优势</h2>

<p>Apache Flink 同时支持流式及批量分析应用，即<strong>批流一体</strong>，所以Flink在<strong>实时数仓和实时ETL</strong>中有天然的优势：</p>

<ul>
  <li><strong>状态管理</strong>，实时数仓里面会进行很多的聚合计算，这些都需要对于状态进行访问和管理，Flink 支持强大的状态管理</li>
  <li><strong>表义能力</strong>，Flink 提供极为丰富的多层次 API，包括Stream API、Table API 及 Flink SQL</li>
  <li><strong>生态完善</strong>，实时数仓的用途广泛，Flink 支持多种存储（HDFS、ES 等）</li>
  <li><strong>批流一体</strong>，Flink 已经在将流计算和批计算的 API 进行统一</li>
  <li><strong>丰富的窗口支持</strong>，Flink 支持包含滚动窗口、滑动窗口及其他窗口</li>
  <li><strong>多种时间语义</strong>，Flink 支持 Event Time、Processing Time 和 Ingestion Time</li>
</ul>

<p>相对于Spark Streaming，有以下优势</p>

<ul>
  <li>架构
    <ul>
      <li>Spark Streaming 的架构是基于Spark的，<strong>本质是微批处理，可以把 Spark Streaming 理解为时间维度上的 Spark DAG</strong></li>
      <li>Flink 会构建一张分布式的 DAG 图，每个子任务都流式处理</li>
    </ul>
  </li>
  <li>一致性
    <ul>
      <li><strong>Spark Streaming 通过配置 checkpoint 的方式实现 At least once 语义</strong>。当任务出现 failover 的时候，会从 checkpoint 重新加载，使得数据不丢失，但是会导致数据重复处理。</li>
      <li><strong>Flink 基于 2PC 实现了 Exactly once 语义</strong></li>
    </ul>
  </li>
</ul>

<h2 id="事件驱动">事件驱动</h2>

<p><strong>事件驱动型应用是一类具有状态的应用</strong>，它从一个或多个事件流提取数据，并根据到来的事件触发计算、状态更新或其他外部动作。在传统架构中，我们需要读写远程事务型数据库，比如MySQL。<strong>在事件驱动应用中数据和计算不会分离，状态保存在本地</strong>，所以具有更高的吞吐和更低的延迟。
<img src="https://wzx140.github.io//assets/img/2024-1-13-1.png" alt="" /></p>

<h2 id="架构">架构</h2>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-2.png" alt="" /></p>

<h3 id="部署模式">部署模式</h3>
<h4 id="调试模式">调试模式</h4>

<ul>
  <li>Local
    <ul>
      <li>模拟 cluster 集群，仅启动 JobManager 完成应用的运行</li>
    </ul>
  </li>
  <li>Standalone
    <ul>
      <li><strong>部署相对简单</strong>，可以支持小规模，少量的任务运行</li>
      <li>缺少系统层面对集群中 Job 的管理，<strong>容易遭成资源分配不均匀</strong></li>
      <li>资源隔离相对简单，<strong>任务之间资源竞争严重</strong></li>
    </ul>
  </li>
</ul>

<h4 id="生产模式">生产模式</h4>

<ul>
  <li>Yarn-session
    <ul>
      <li>所有任务提交到同一个 Flink runtime 集群</li>
      <li>Client 需要执行 main 方法并生成 JobGraph 传输到 JM</li>
      <li><strong>不同作业间共享资源，并且其中一个任务的失败会影响该 session 上的其他作业</strong></li>
      <li><strong>适用于启动延迟非常重要的短作业，例如交互式查询</strong></li>
    </ul>
  </li>
  <li>Yarn-per-job
    <ul>
      <li>每个任务提交到独占的 Flink runtime 集群</li>
      <li>Client 需要执行 main 方法并生成 JobGraph 传输到 JM</li>
      <li><strong>任务之间不会有影响，也不共享资源</strong></li>
      <li><strong>适用于长稳运行的普通流式作业</strong></li>
    </ul>
  </li>
  <li>Yarn-application
    <ul>
      <li>每个任务提交到独占的 Flink runtime 集群</li>
      <li>JM 执行 main 方法并生成 JobGraph</li>
      <li><strong>Client 不需要执行重操作，其资源不会成为瓶颈</strong></li>
    </ul>
  </li>
</ul>

<p>Client 提交作业分为 Detached 模式和 Attached 模式。Detached 模式下，Client 提交完作业后可以退出。 Attached 模式下，Client 提交完作业后，需要与集群之间维持连接</p>

<h3 id="runtime">Runtime</h3>
<p>Runtime 层以 JobGraph 的形式接收程序并执行</p>

<ul>
  <li>Client
    <ul>
      <li>Client 将 Flink 程序转化为数据流图，提交给 JobManager 去执行</li>
    </ul>
  </li>
  <li>JobManager
    <ul>
      <li>集群管理者，<strong>协调 JobGraph 的分布式执行</strong></li>
      <li>负责调度任务、协调 checkpoints、协调故障恢复、收集 Job 的状态信息，并管理 Flink 集群中的从节点 TaskManager</li>
    </ul>
  </li>
  <li>TaskManager
    <ul>
      <li>实际负责执行计算的 Worker，<strong>在其上执行 Flink Job 的一组Task</strong>。维护缓冲池，并负责网络连接以在算子之间交换数据流</li>
      <li>所在节点的管理员，<strong>负责把该节点上的服务器信息比如内存、磁盘、任务运行情况等向 JobManager 汇报</strong></li>
    </ul>
  </li>
</ul>

<p><img src="https://wzx140.github.io//assets/img/2020-4-21-3.png" alt="" /></p>

<h3 id="apilib层">API&amp;LIB层</h3>
<p><strong>DataSet 用于处理有限数据集即批处理，而 DataStream 用于处理无界数据流即流处理</strong>。DataStream API 和 DataSet API 均通过单独的编译过程生成有状态算子连接的数据流 DAG，交由 Runtime 去执行。</p>

<p>Flink自带了一些用于特定领域的库，这些库会生成 DataSet API 和 DataStream API 程序。</p>

<ul>
  <li>FlinkML 用于机器学习</li>
  <li>Gelly 用于图计算</li>
  <li>Table API 用于 SQL 操作</li>
</ul>

<h2 id="reference">REFERENCE</h2>

<ol>
  <li>CARBONE P, KATSIFODIMOS A, EWEN S, 等. Apache flink: Stream and batch processing in a single engine[J]. Bulletin of the IEEE Computer Society Technical Committee on Data Engineering, IEEE Computer Society, 2015, 36(4).</li>
  <li><a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/">flink官方文档</a></li>
</ol>]]></content><author><name>wzx</name></author><category term="Flink" /><summary type="html"><![CDATA[Flink 相对于 Spark Streaming 的微批处理架构，是真正意义上的流式处理架构。具备批流一体，高吞吐低延迟，状态机制，可靠的容错机制等优势。]]></summary></entry><entry><title type="html">Flink 剖析(二) 状态</title><link href="https://wzx140.github.io//2024/01/13/2-flink-state/" rel="alternate" type="text/html" title="Flink 剖析(二) 状态" /><published>2024-01-13T00:00:00+08:00</published><updated>2024-01-13T00:00:00+08:00</updated><id>https://wzx140.github.io//2024/01/13/2-flink-state</id><content type="html" xml:base="https://wzx140.github.io//2024/01/13/2-flink-state/"><![CDATA[<p>Flink 中高效而丰富的算子状态管理机制</p>

<h2 id="状态">状态</h2>

<p>Flink 的状态都是基于本地的，即每个算子子任务维护着这个算子子任务对应的状态存储，<strong>算子子任务之间的状态不能相互访问</strong>。</p>

<h3 id="keyedoperatorbroadcast-state">Keyed/Operator/Broadcast State</h3>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-2-1.png" alt="" /></p>

<p><strong>Keyed State 以键值对方式存储</strong>，并且<strong>与数据流一样严格分区</strong>。如图所示，只有在 keyBy 函数之后，才可以在对应 keyed stream 的子任务中访问到 Keyed State。</p>

<p>Operator State 绑定在子任务上，<strong>流入相同子任务的数据可以访问和共享Operator State</strong>。</p>

<p>Broadcast State 由 Broadcast Stream 生成，<strong>每个子任务保存全量的状态</strong>，但是 Broadcast state 的更新不能依赖于流中元素到达的顺序</p>

<h3 id="raw-and-managed-state">Raw and Managed State</h3>

<p><strong>Managed State 由 Flink 进行维护管理</strong>，如<code class="language-plaintext highlighter-rouge">ValueState</code>、<code class="language-plaintext highlighter-rouge">ListState</code>、<code class="language-plaintext highlighter-rouge">MapState</code> 等。Flink runtime 对 state 进行编码，然后将其写入检查点。</p>

<p><strong>Raw State 由算子维护</strong>，Flink 本身并不知道这些状态。快照时，仅将字节序列写入检查点。Raw State 的数据结构对 Flink 不透明，只能观察到原始的字节数组。<strong>一般不建议使用</strong>。</p>

<h2 id="存储">存储</h2>

<h3 id="memorystatebackend">MemoryStateBackend</h3>
<p><strong>MemoryStateBackend 将状态数据存储在本地内存中</strong>，一般用来进行本地调试用</p>

<h3 id="fsstatebackend">FsStateBackend</h3>
<p><strong>FsStateBackend 会把状态数据保存在 TaskManager 的内存中。CheckPoint 时，将状态快照写入到配置的 HDFS 中</strong>，少量的元数据信息存储到 JobManager 的内存中。<strong>适用于大作业、状态较大、全局高可用的那些任务</strong>。</p>

<h3 id="rocksdbstatebackend">RocksDBStateBackend</h3>
<p><strong>RocksDBStateBackend 将正在运行中的状态数据保存在 RocksDB 数据库中</strong>，RocksDB 数据库默认将数据存储在 TaskManager 运行节点的数据目录下。<strong>CheckPoint 时，将状态快照写入到配置 HDFS 中</strong>，<strong>是唯一支持增量快照的状态后端，适用于超大状态的场景</strong></p>

<h2 id="访问">访问</h2>

<p><strong>所有对 subTask 状态的并发操作都通过队列进行排队(Mailbox)，单线程(Mailbox 线程)依次处理达到线程安全</strong>。使用 StateDescriptor 从 StateBackend 中获取 State 实例</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Main</span> <span class="o">{</span>

  <span class="cm">/**
   * (1,4)
   * (1,6)
   */</span>
  <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="nc">StreamExecutionEnvironment</span> <span class="n">env</span> <span class="o">=</span> <span class="nc">StreamExecutionEnvironment</span><span class="o">.</span><span class="na">getExecutionEnvironment</span><span class="o">();</span>
    <span class="n">env</span><span class="o">.</span><span class="na">fromElements</span><span class="o">(</span><span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">3</span><span class="o">),</span> <span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">5</span><span class="o">),</span> <span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">7</span><span class="o">),</span> <span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">5</span><span class="o">),</span> <span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">))</span>
            <span class="o">.</span><span class="na">keyBy</span><span class="o">(</span><span class="n">x</span><span class="o">-&gt;</span><span class="n">x</span><span class="o">.</span><span class="na">f0</span><span class="o">)</span>
            <span class="o">.</span><span class="na">flatMap</span><span class="o">(</span><span class="k">new</span> <span class="nc">CountWindowAverage</span><span class="o">())</span>
            <span class="o">.</span><span class="na">print</span><span class="o">();</span>
    <span class="n">env</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="s">"test"</span><span class="o">);</span>
  <span class="o">}</span>

  <span class="kd">private</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">CountWindowAverage</span> <span class="kd">extends</span> <span class="nc">RichFlatMapFunction</span><span class="o">&lt;</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;,</span> <span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">ValueState</span><span class="o">&lt;</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="n">state</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">open</span><span class="o">(</span><span class="nc">Configuration</span> <span class="n">parameters</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">ValueStateDescriptor</span><span class="o">&lt;</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="n">descriptor</span> <span class="o">=</span>
              <span class="k">new</span> <span class="nc">ValueStateDescriptor</span><span class="o">&lt;&gt;(</span><span class="s">"average"</span><span class="o">,</span> <span class="nc">TypeInformation</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="k">new</span> <span class="nc">TypeHint</span><span class="o">&lt;</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;&gt;()</span> <span class="o">{</span>
              <span class="o">}));</span>

      <span class="c1">// 设置ttl</span>
      <span class="nc">StateTtlConfig</span> <span class="n">ttlConfig</span> <span class="o">=</span> <span class="nc">StateTtlConfig</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">(</span><span class="nc">Time</span><span class="o">.</span><span class="na">seconds</span><span class="o">(</span><span class="mi">10</span><span class="o">))</span>
              <span class="c1">// 何时更新过期状态</span>
              <span class="o">.</span><span class="na">setUpdateType</span><span class="o">(</span><span class="nc">StateTtlConfig</span><span class="o">.</span><span class="na">UpdateType</span><span class="o">.</span><span class="na">OnCreateAndWrite</span><span class="o">)</span>
              <span class="c1">// 是否可以访问过期数据</span>
              <span class="o">.</span><span class="na">setStateVisibility</span><span class="o">(</span><span class="nc">StateTtlConfig</span><span class="o">.</span><span class="na">StateVisibility</span><span class="o">.</span><span class="na">ReturnExpiredIfNotCleanedUp</span><span class="o">)</span>
              <span class="o">.</span><span class="na">build</span><span class="o">();</span>
      <span class="n">descriptor</span><span class="o">.</span><span class="na">enableTimeToLive</span><span class="o">(</span><span class="n">ttlConfig</span><span class="o">);</span>

      <span class="n">state</span> <span class="o">=</span> <span class="n">getRuntimeContext</span><span class="o">().</span><span class="na">getState</span><span class="o">(</span><span class="n">descriptor</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">flatMap</span><span class="o">(</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">value</span><span class="o">,</span> <span class="nc">Collector</span><span class="o">&lt;</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="n">out</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
      <span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">curSum</span> <span class="o">=</span> <span class="nc">Optional</span>
              <span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">state</span><span class="o">.</span><span class="na">value</span><span class="o">())</span>
              <span class="o">.</span><span class="na">orElse</span><span class="o">(</span><span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">));</span>

      <span class="n">curSum</span><span class="o">.</span><span class="na">f0</span> <span class="o">+=</span> <span class="mi">1</span><span class="o">;</span>
      <span class="n">curSum</span><span class="o">.</span><span class="na">f1</span> <span class="o">+=</span> <span class="n">value</span><span class="o">.</span><span class="na">f1</span><span class="o">;</span>
      <span class="n">state</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">curSum</span><span class="o">);</span>

      <span class="k">if</span> <span class="o">(</span><span class="n">curSum</span><span class="o">.</span><span class="na">f0</span> <span class="o">&gt;=</span> <span class="mi">2</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">out</span><span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">value</span><span class="o">.</span><span class="na">f0</span><span class="o">,</span> <span class="n">curSum</span><span class="o">.</span><span class="na">f1</span> <span class="o">/</span> <span class="n">curSum</span><span class="o">.</span><span class="na">f0</span><span class="o">));</span>
        <span class="n">state</span><span class="o">.</span><span class="na">clear</span><span class="o">();</span>
      <span class="o">}</span>
    <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="reference">REFERENCE</h2>

<ol>
  <li>CARBONE P, KATSIFODIMOS A, EWEN S, 等. Apache flink: Stream and batch processing in a single engine[J]. Bulletin of the IEEE Computer Society Technical Committee on Data Engineering, IEEE Computer Society, 2015, 36(4).</li>
  <li><a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/">flink官方文档</a></li>
</ol>]]></content><author><name>wzx</name></author><category term="Flink" /><summary type="html"><![CDATA[Flink 中高效而丰富的算子状态管理机制]]></summary></entry><entry><title type="html">Flink 剖析(三) 数据流</title><link href="https://wzx140.github.io//2024/01/13/3-flink-stream/" rel="alternate" type="text/html" title="Flink 剖析(三) 数据流" /><published>2024-01-13T00:00:00+08:00</published><updated>2024-01-13T00:00:00+08:00</updated><id>https://wzx140.github.io//2024/01/13/3-flink-stream</id><content type="html" xml:base="https://wzx140.github.io//2024/01/13/3-flink-stream/"><![CDATA[<p>Flink JM 将解析后的 StreamGraph 交由 TM 调度执行。从数据源读取数据开始，上游的数据处理完毕后 push 到下游继续处理，直到数据输出到外部存储中。</p>

<h2 id="数据流">数据流</h2>

<h3 id="算子">算子</h3>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-3-3.png" alt="" /></p>

<p>数据流由<strong>有<a href="/2024/01/13/2-flink-state/">状态</a>的算子和连接算子的 edge</strong> 组成。如图所示，数据流以并行化的方式执行，所以<strong>算子会根据并行度被并行化为多个 sub task</strong>，而 edge 被拆分为一个或多个<strong>流分区</strong>，流向一个 sub task。</p>

<h3 id="转化流程">转化流程</h3>

<h4 id="sql">SQL</h4>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-3-2.png" alt="" width="500" /></p>

<p>与 Spark Sql 非常类似，Flink Sql 使用 Calcite 进行语法生成抽象语法树，结合元数据解析为 Operation 树，并过优化规则优化为物理计划树，最后转化为 DataSet/DataStream 算子。</p>

<p>以下以祖传的 wordCount 为例</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">source_table</span> <span class="p">(</span>
  <span class="n">num</span>   <span class="nb">INT</span>
<span class="p">)</span> <span class="k">WITH</span> <span class="p">(</span>
  <span class="s1">'connector'</span><span class="o">=</span><span class="s1">'datagen'</span><span class="p">,</span> 
  <span class="s1">'fields.num.min'</span><span class="o">=</span><span class="s1">'0'</span><span class="p">,</span> 
  <span class="s1">'fields.num.max'</span><span class="o">=</span><span class="s1">'10'</span><span class="p">,</span> 
  <span class="s1">'rows-per-second'</span><span class="o">=</span><span class="s1">'1'</span>
<span class="p">);</span>

<span class="k">SELECT</span> <span class="n">num</span><span class="p">,</span> <span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">cnt</span> 
<span class="k">FROM</span> <span class="n">source_table</span> 
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">num</span><span class="p">;</span>
</code></pre></div></div>

<p>任务以回撤流输出</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+----+-------------+----------------------+
| op |         num |                  cnt |
+----+-------------+----------------------+
| +I |           7 |                    1 |
| +I |           5 |                    1 |
| +I |           4 |                    1 |
| +I |          10 |                    1 |
| -U |           4 |                    1 |
| +U |           4 |                    2 |
| -U |          10 |                    1 |
</code></pre></div></div>

<p>explain 后的结果为</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== Abstract Syntax Tree ==
LogicalAggregate(group=[{0}], cnt=[COUNT()])
+- LogicalTableScan(table=[[default_catalog, default_database, source_table]])

== Optimized Physical Plan ==
GroupAggregate(groupBy=[num], select=[num, COUNT(*) AS cnt])
+- Exchange(distribution=[hash[num]])
   +- TableSourceScan(table=[[default_catalog, default_database, source_table]], fields=[num])

== Optimized Execution Plan ==
GroupAggregate(groupBy=[num], select=[num, COUNT(*) AS cnt])
+- Exchange(distribution=[hash[num]])
   +- TableSourceScan(table=[[default_catalog, default_database, source_table]], fields=[num])
</code></pre></div></div>

<p>转化后的 JobGraph 如下所示</p>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-3-7.png" alt="" /></p>

<h4 id="datastream">DataStream</h4>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-3-1.png" alt="" width="450" /></p>

<p>在 per-job 和 session 模式下，client 负责转化 StreamGraph 和 JobGraph</p>

<ol>
  <li>
    <p>DataStream 算子会先转化为 StreamGraph，<strong>表达计算过程的逻辑</strong></p>
  </li>
  <li>转化为 JobGraph，在 web ui 上看到的就是这个。<strong>在 StreamGraph 基础上进行 OperatorChain 优化</strong></li>
  <li>转化为 ExecutionGraph，<strong>包含了作业中所有并行执行的 Task 信息、Task 之间的关联关系、数据流转关系</strong></li>
</ol>

<p>以祖传的 wordCount 代码来看 StreamGraph -&gt; JobGraph -&gt; ExecutionGraph 的流程</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">WindowWordCount</span> <span class="o">{</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>

    <span class="nc">Configuration</span> <span class="n">conf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Configuration</span><span class="o">();</span>
    <span class="n">conf</span><span class="o">.</span><span class="na">setString</span><span class="o">(</span><span class="nc">RestOptions</span><span class="o">.</span><span class="na">BIND_PORT</span><span class="o">,</span> <span class="s">"8081"</span><span class="o">);</span>
    <span class="nc">StreamExecutionEnvironment</span> <span class="n">env</span> <span class="o">=</span> <span class="nc">StreamExecutionEnvironment</span><span class="o">.</span><span class="na">getExecutionEnvironment</span><span class="o">(</span><span class="n">conf</span><span class="o">);</span>

    <span class="n">env</span><span class="o">.</span><span class="na">socketTextStream</span><span class="o">(</span><span class="s">"localhost"</span><span class="o">,</span> <span class="mi">9999</span><span class="o">)</span>
        <span class="o">.</span><span class="na">flatMap</span><span class="o">(</span><span class="k">new</span> <span class="nc">Splitter</span><span class="o">())</span>
        <span class="o">.</span><span class="na">keyBy</span><span class="o">(</span><span class="n">value</span> <span class="o">-&gt;</span> <span class="n">value</span><span class="o">.</span><span class="na">f0</span><span class="o">)</span>
        <span class="o">.</span><span class="na">window</span><span class="o">(</span><span class="nc">TumblingProcessingTimeWindows</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="nc">Time</span><span class="o">.</span><span class="na">seconds</span><span class="o">(</span><span class="mi">5</span><span class="o">)))</span>
        <span class="o">.</span><span class="na">sum</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
        <span class="o">.</span><span class="na">print</span><span class="o">();</span>

    <span class="n">env</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="s">"Window WordCount"</span><span class="o">);</span>
  <span class="o">}</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Splitter</span> <span class="kd">implements</span> <span class="nc">FlatMapFunction</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">flatMap</span><span class="o">(</span><span class="nc">String</span> <span class="n">sentence</span><span class="o">,</span> <span class="nc">Collector</span><span class="o">&lt;</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;&gt;</span> <span class="n">out</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">word</span> <span class="o">:</span> <span class="n">sentence</span><span class="o">.</span><span class="na">split</span><span class="o">(</span><span class="s">" "</span><span class="o">))</span> <span class="o">{</span>
        <span class="n">out</span><span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="k">new</span> <span class="nc">Tuple2</span><span class="o">&lt;&gt;(</span><span class="n">word</span><span class="o">,</span> <span class="mi">1</span><span class="o">));</span>
      <span class="o">}</span>
    <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="streamgraph">StreamGraph</h3>

<p>每个 DataStream 算子都会先转化为一个 Transformation，然后再转化为 <strong>StreamNode 和 StreamEdge</strong></p>

<p>Transformation 如下所示</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[LegacySourceTransformation{id=1, name='Socket Stream', outputType=String, parallelism=1}]
[OneInputTransformation{id=2, name='Flat Map', outputType=Java Tuple2&lt;String, Integer&gt;, parallelism=12}]
[PartitionTransformation{id=3, name='Partition', outputType=Java Tuple2&lt;String, Integer&gt;, parallelism=12}]
[OneInputTransformation{id=4, name='TumblingProcessingTimeWindows', outputType=Java Tuple2&lt;String, Integer&gt;, parallelism=12}]
[LegacySinkTransformation{id=5, name='Print to Std. Out', outputType=Java Tuple2&lt;String, Integer&gt;, parallelism=12}]
</code></pre></div></div>

<p>对应的 StreamGraph 如下所示</p>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-3-8.png" alt="" /></p>

<ul>
  <li><strong>实体 StreamNode 表示执行计算的算子</strong>，由 Transformation 转化而成，可以有多个输入和输出</li>
  <li><strong>虚拟 StreamNode 表示 StreamEdge 的属性</strong>，由 Transformation 转化而成，会附着到 StreamEdge 中。如 PartitionTransformation</li>
  <li><strong>StreamEdge 用来连接两个 StreamNode</strong>，包含了旁路输出 tag、partitioner 等的信息</li>
</ul>

<h3 id="jobgraph">JobGraph</h3>

<p>如下所示，web ui 中显示的 DAG 图就是 JobGraph</p>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-3-9.png" alt="" /></p>

<ul>
  <li><strong>JobVertex 由 OperatorChain 融合优化后的多个 StreamNode 组合而成</strong>，输入是 JobEdge，输出是 IntermediateDataSet</li>
  <li>JobEdge 是连接 IntermediateDatSet 和 JobVertex 的边，<strong>其数据分发模式会直接影响执行时 Task 之间的数据连接关系</strong>，是点对点连接还是全连接</li>
  <li><strong>IntermediateDataSet 表示 JobVertex 中包含的算子会产生的数据集</strong>。其个数与 JobVertext 对应的 StreamNode 的出边数量相同，可以是一个或者多个</li>
</ul>

<h3 id="executiongraph">ExecutionGraph</h3>

<ul>
  <li><strong>ExecutionJobVertex 和 JobVertex 一一对应</strong>。包含一组 ExecutionVertex
    <ul>
      <li><strong>ExecutionVertex 表示 ExecutionJobVertex 的并发执行实例</strong>，与 JobVertex 的并发数一致
        <ul>
          <li><strong>Execution 表示 ExecutionVertex 一次尝试</strong>。在发生故障或者数据需要重算的情况下，ExecutionVertex 可能会有多个 ExecutionAttemptID 来标识 Execution</li>
        </ul>
      </li>
      <li><strong>ExecutionEdge 表示 ExecutionVertex 的输入</strong>，连接到上游产生的 IntermediateResultPartition</li>
    </ul>
  </li>
  <li><strong>IntermediateResult 表示 ExecutionJobVertex 的逻辑输出结果</strong>，和 IntermediateDataSet 一一对应。包含一组 IntermediateResultPartition
    <ul>
      <li><strong>IntermediateResultPartition 表示 ExecutionVertex 的逻辑输出结果</strong></li>
    </ul>
  </li>
</ul>

<h2 id="资源和资源组">资源和资源组</h2>

<p>在 Flink 集群中，一个 TaskManger 就是一个 JVM 进程，并且会用独立的线程来执行 Task。如下图所示，<strong>每个 TaskManger 中含有固定的 Slot，作为为运行Task的容器，起到了内存隔离的作用，并且共享 TCP 连接，减少网络传输</strong>。
<img src="https://wzx140.github.io//assets/img/2024-1-13-3-4.png" alt="" /></p>

<h3 id="operatorchain">OperatorChain</h3>

<p><strong>在 StreamGraph -&gt; JobGraph 阶段</strong>，类似 Spark 中窄依赖，让没有 shuffle 的算子在同一个线程中执行。</p>

<ul>
  <li><strong>减少线程间上下文的切换</strong></li>
  <li><strong>减少序列化的资源消耗</strong></li>
  <li><strong>减少网络传输的资源消耗</strong></li>
</ul>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-3-5.png" alt="" /></p>

<p>符合以下条件的算子将合并为算子链</p>

<ul>
  <li>
    <p>下游算子的输入 StreamEdge 只能有一个</p>
  </li>
  <li>上下游算子实例处于同一个 SlotSharingGroup 中，默认都是 default</li>
  <li>下游节点的连接策略为 ALWAYS (表示可以与上下游链接，map、flatmap、filter 等默认是 ALWAYS)</li>
  <li>上游节点的连接策略为 ALWAYS 或 HEAD(表示只能与下游链接，Source 默认是 HEAD)</li>
  <li>上下游算子的并行度相同</li>
  <li>StreamEdge 的分区类型为 ForwardPartitioner</li>
  <li>没有禁用算子链</li>
</ul>

<h3 id="slotsharing">SlotSharing</h3>

<p><strong>在调度 ExecutionGraph 阶段，将不能形成算子链的两个 Execution 放在一个 Slot 中执行</strong>。一些简单的 map 和 filter 算子所需要的资源不多，但是有些算子比如window、group by 则需要更多的计算资源才能满足计算所需。<strong>资源需求大的算子可以共用其他的 Slot，提高整个集群的资源利用率</strong>。在一个 Slot 中执行多个线程，类似 Spark vcore 功能。比如下图中的 flatmap 和 key&amp;sink 放在一个 slot 里执行以达到资源共享的目的。</p>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-3-6.png" alt="" /></p>

<h2 id="数据交换">数据交换</h2>

<h3 id="push-模式">Push 模式</h3>

<p><strong>Flink 的 Stream 计算模型采用的是 PUSH(Pipeline) 模式</strong>，上游主动向下游推送数据，下游收到数据触发计算，没有数据则进入等待状态</p>

<table>
  <thead>
    <tr>
      <th>对比点</th>
      <th>Pull</th>
      <th>Push</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>延迟</td>
      <td>延迟高(需要等待上游所有计算完毕)</td>
      <td>延迟低(上游边计算边向下游输出)</td>
    </tr>
    <tr>
      <td>下游状态</td>
      <td>有状态，需要知道拉取时机和拉取点</td>
      <td>无状态</td>
    </tr>
    <tr>
      <td>上游状态</td>
      <td>无状态</td>
      <td>有状态，需要知道下游的推送点</td>
    </tr>
    <tr>
      <td>连接状态</td>
      <td>短链接</td>
      <td>长链接</td>
    </tr>
  </tbody>
</table>

<h3 id="传递方式">传递方式</h3>

<p>数据要交给下一个算子或者 Task 进行计算， 有以下三种情形</p>

<ol>
  <li>OperatorChain内部的数据传递，发生在本地线程内</li>
  <li>同一个 TM 的不同 Task 之间传递数据，发生在同一个 JVM的不同线程之间</li>
  <li>不同 TM 的 Task 之间传递数据，即跨 JVM 的数据传递，需要使用跨网络的通信</li>
</ol>

<h3 id="反压">反压</h3>

<h2 id="reference">REFERENCE</h2>

<ol>
  <li>CARBONE P, KATSIFODIMOS A, EWEN S, 等. Apache flink: Stream and batch processing in a single engine[J]. Bulletin of the IEEE Computer Society Technical Committee on Data Engineering, IEEE Computer Society, 2015, 36(4).</li>
  <li>flink官方文档](https://ci.apache.org/projects/flink/flink-docs-release-1.10/)</li>
</ol>]]></content><author><name>wzx</name></author><category term="Flink" /><summary type="html"><![CDATA[Flink JM 将解析后的 StreamGraph 交由 TM 调度执行。从数据源读取数据开始，上游的数据处理完毕后 push 到下游继续处理，直到数据输出到外部存储中。]]></summary></entry><entry><title type="html">Flink 剖析(四) Window、Time、Watermark</title><link href="https://wzx140.github.io//2024/01/13/4-flink-time/" rel="alternate" type="text/html" title="Flink 剖析(四) Window、Time、Watermark" /><published>2024-01-13T00:00:00+08:00</published><updated>2024-01-13T00:00:00+08:00</updated><id>https://wzx140.github.io//2024/01/13/4-flink-time</id><content type="html" xml:base="https://wzx140.github.io//2024/01/13/4-flink-time/"><![CDATA[<p>Flink 通过 Window、Time、Watermark 完成乱序事件处理，定义窗口以及维护和更新用户定义状态等</p>

<h2 id="window">Window</h2>
<p>流式计算最终的目的是去统计数据产生汇总结果的，而<strong>在无界数据集上，如果做一个全局的窗口统计是不现实的，所以只能去划定一定大小的窗口范围去汇总</strong></p>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-4-1.png" alt="" /></p>

<ul>
  <li>滚动窗口(Tumbling Window): 窗口数据有固定的大小(时间，计数)，窗口不会重叠</li>
  <li>滑动窗口(Sliding Window): 窗口数据有固定的大小(时间)，有生成间隔，窗口会重叠</li>
  <li>会话窗口(Session Window): 窗口数据没有固定的大小，根据会话参数划分，窗口不会重叠</li>
</ul>

<p>窗口是时间驱动(如每30s)或者事件驱动(如每100个元素)的。触发流程如下所示</p>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-4-2.png" alt="" /></p>

<ul>
  <li>WindowAssigner：根据 record 的维度进行 keyBy，并创建 key 为 window 的 keyed state</li>
  <li>Trigger：触发窗口计算的条件</li>
  <li>evictor：触发计算后，先过滤一部分 record</li>
</ul>

<p>Flink 对一些聚合类的窗口计算(如 sum 和 min)做了优化，因为聚合类的计算不需要将窗口中的所有数据都保存下来，只需要保存一个中间结果值就可以了。</p>

<p>以下代码描述了建立一个<code class="language-plaintext highlighter-rouge">GlobalWindow</code>，当窗口积累了1000个事件时，保留最新的100个并触发计算。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">stream</span>
  <span class="o">.</span><span class="na">window</span><span class="o">(</span><span class="nc">GlobalWindow</span><span class="o">.</span><span class="na">create</span><span class="o">())</span>
  <span class="o">.</span><span class="na">trigger</span><span class="o">(</span><span class="nc">Count</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1000</span><span class="o">))</span>
  <span class="o">.</span><span class="na">evict</span><span class="o">(</span><span class="nc">Count</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">100</span><span class="o">))</span>
</code></pre></div></div>

<h2 id="time">Time</h2>

<p>通过<code class="language-plaintext highlighter-rouge">env.setStreamTimeCharacteristic()</code>可以设置使用时间</p>

<ul>
  <li><strong>事件时间(Event Time)</strong>: 事件实际发生的时间，由数据生产方标记
    <ul>
      <li>需要对每条记录都保存事件时间，成本较高</li>
      <li>能处理无序事件时性能和延迟受到影响</li>
    </ul>
  </li>
  <li><strong>摄入时间(Ingestion Time)</strong>: 事件进入流处理框架的时间。
    <ul>
      <li>处于 Event Time 和 Processing Time之间，<strong>性能和准确度的折中方案</strong></li>
      <li>比起 Event Time，Ingestion Time可以<strong>不需要设置复杂的 Watermark</strong>，因此也不需要太多缓存，延迟较低。<strong>不能保证生产者到 source 的这段线路的有序性</strong>，因此不能处理无序事件和延迟数据</li>
      <li>相对于 Processing Time，能<strong>保证执行图内部有序</strong>，计算结果相对准确</li>
    </ul>
  </li>
  <li><strong>处理时间(Processing Time)</strong>: 事件被处理的时间
    <ul>
      <li>只依赖当前执行机器的系统时钟，<strong>无需缓存，最佳的性能和最低的延迟</strong></li>
      <li>容易受到各种因素影响(event 产生的速度、在算子之间传输速度等)</li>
    </ul>
  </li>
</ul>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-4-3.png" alt="" /></p>

<h2 id="watermark">WaterMark</h2>
<p>WaterMark 本质上是一个时间戳，是 DataStream 中一个带有时间戳的元素，<strong>一般结合事件时间使用，解决实时计算中的数据乱序问题</strong>。</p>

<ul>
  <li>WaterMark 是判断迟到数据的标准，同时也是窗口触发的标记。如果出现了一个 WaterMark(T)，那么就意味着 EventTime &lt; T 的数据都已经到达</li>
  <li>在程序并行度大于 1 的情况下，会有多个流产生 WaterMark 和窗口，这时候 <strong>Flink 会选取时间戳最小的 WaterMark</strong></li>
  <li><strong>WaterMark 可以加在 source 算子或者非 source 算子，建议加在source算子上</strong></li>
</ul>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-4-4.png" alt="" /></p>

<p>如上图所示，对于有序的数据流来说，WaterMark 周期性的出现在数据流中；对于无序的数据流来说，WaterMark 表示该点之前，所有到特定事件时间的事件都应该到达。一旦 WaterMark 到达算子，算子就可以将其内部事件时钟提前到 WaterMark 的值。</p>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-4-5.png" alt="" /></p>

<p>如图所示，WaterMark 由 source 直接生成，<strong>每个算子的 subtask 都维护自己的 WaterMark</strong>。当算子接收到 WaterMark，它会将内部事件时钟提前到WaterMark 的时间，并且为后序算子生成新的 WaterMark。<strong>当算子从多个输入流获得 WaterMark (如 keyBy, partition 等算子)时，算子会选择 WaterMark 中的最小值更新时间</strong></p>

<h3 id="watermarkstrategy">WatermarkStrategy</h3>
<p>如果不是 Kafka 或者是 Kinesis，那么需要用 <code class="language-plaintext highlighter-rouge">WatermarkStrategy</code> 来指定 <code class="language-plaintext highlighter-rouge">TimestampAssigner</code></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">WatermarkStrategy</span><span class="o">.</span><span class="na">forMonotonousTimestamps</span><span class="o">()</span>
    <span class="o">.</span><span class="na">withTimestampAssigner</span><span class="o">((</span><span class="n">event</span><span class="o">,</span> <span class="n">timestamp</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">event</span><span class="o">.</span><span class="na">time</span><span class="o">);</span>
</code></pre></div></div>

<p>Flink中已经实现 <code class="language-plaintext highlighter-rouge">WatermarkGenerator</code> 接口的预制周期性 Watermark 生成器</p>
<ul>
  <li><strong>单调增加的生成策略</strong>: <code class="language-plaintext highlighter-rouge">WatermarkStrategy.forMonotonousTimestamps()</code></li>
  <li><strong>固定延迟的生成策略(基于事件时间)</strong>: <code class="language-plaintext highlighter-rouge">WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10))</code></li>
</ul>

<h3 id="idle-watermark">Idle WaterMark</h3>

<p>如果 source 的一个分区在某段时间内没有数据，那么这个分区产生的WaterMark 值将停滞，由于 Flink 取时间戳最小的 WaterMark，那么会<strong>导致下游整体的 WaterMark 停滞</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">WatermarkStrategy</span>
    <span class="o">.</span><span class="na">forBoundedOutOfOrderness</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">20</span><span class="o">))</span>
    <span class="c1">// 超过1min没有数据, 生成的Watermark会携带idle标记, 不会阻碍下游的运行</span>
    <span class="o">.</span><span class="na">withIdleness</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">1</span><span class="o">));</span>
</code></pre></div></div>

<h3 id="watermarkgenerators">WatermarkGenerators</h3>

<p>如果需要自定义 <code class="language-plaintext highlighter-rouge">WatermarkStrategy</code>，需要继承 <code class="language-plaintext highlighter-rouge">WatermarkStrategy</code> 并实现 <code class="language-plaintext highlighter-rouge">createWatermarkGenerator()</code> 方法，<code class="language-plaintext highlighter-rouge">TimestampAssigner</code> 对象可以在构建时传递，所以不需要实现 <code class="language-plaintext highlighter-rouge">createTimestampAssigner()</code> 方法。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">WatermarkStrategy</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="kd">extends</span> <span class="nc">TimestampAssignerSupplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;,</span> <span class="nc">WatermarkGeneratorSupplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;{</span>

    <span class="cm">/**
     * Instantiates a {@link TimestampAssigner} for assigning timestamps according to this
     * strategy.
     */</span>
    <span class="nd">@Override</span>
    <span class="nc">TimestampAssigner</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="nf">createTimestampAssigner</span><span class="o">(</span><span class="nc">TimestampAssignerSupplier</span><span class="o">.</span><span class="na">Context</span> <span class="n">context</span><span class="o">);</span>

    <span class="cm">/**
     * Instantiates a WatermarkGenerator that generates watermarks according to this strategy.
     */</span>
    <span class="nd">@Override</span>
    <span class="nc">WatermarkGenerator</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="nf">createWatermarkGenerator</span><span class="o">(</span><span class="nc">WatermarkGeneratorSupplier</span><span class="o">.</span><span class="na">Context</span> <span class="n">context</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>对于 <code class="language-plaintext highlighter-rouge">WatermarkGenerators</code> 来说，需要继承并实现以下方法</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Public</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">WatermarkGenerator</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="cm">/**
     * 为每个事件调用，用于检查并记住事件时间戳，或基于事件本身产生Watermark
     */</span>
    <span class="kt">void</span> <span class="nf">onEvent</span><span class="o">(</span><span class="no">T</span> <span class="n">event</span><span class="o">,</span> <span class="kt">long</span> <span class="n">eventTimestamp</span><span class="o">,</span> <span class="nc">WatermarkOutput</span> <span class="n">output</span><span class="o">);</span>

    <span class="cm">/**
     * 由ExecutionConfig.getAutoWatermarkInterval()时间间隔调用，产生Watermark
     */</span>
    <span class="kt">void</span> <span class="nf">onPeriodicEmit</span><span class="o">(</span><span class="nc">WatermarkOutput</span> <span class="n">output</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="周期性-watermarkgenerator">周期性 WatermarkGenerator</h4>
<p>通过 <code class="language-plaintext highlighter-rouge">ExecutionConfig.setAutoWatermarkInterval()</code> 设置时间间隔</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * 固定延迟的生成器(基于事件时间)
 */</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">BoundedOutOfOrdernessGenerator</span> <span class="kd">implements</span> <span class="nc">WatermarkGenerator</span><span class="o">&lt;</span><span class="nc">MyEvent</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">maxOutOfOrderness</span> <span class="o">=</span> <span class="mi">3500</span><span class="o">;</span> <span class="c1">// 3.5 seconds</span>

    <span class="kd">private</span> <span class="kt">long</span> <span class="n">currentMaxTimestamp</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onEvent</span><span class="o">(</span><span class="nc">MyEvent</span> <span class="n">event</span><span class="o">,</span> <span class="kt">long</span> <span class="n">eventTimestamp</span><span class="o">,</span> <span class="nc">WatermarkOutput</span> <span class="n">output</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">currentMaxTimestamp</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="n">currentMaxTimestamp</span><span class="o">,</span> <span class="n">eventTimestamp</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onPeriodicEmit</span><span class="o">(</span><span class="nc">WatermarkOutput</span> <span class="n">output</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// emit the watermark as current highest timestamp minus the out-of-orderness bound</span>
        <span class="n">output</span><span class="o">.</span><span class="na">emitWatermark</span><span class="o">(</span><span class="k">new</span> <span class="nc">Watermark</span><span class="o">(</span><span class="n">currentMaxTimestamp</span> <span class="o">-</span> <span class="n">maxOutOfOrderness</span> <span class="o">-</span> <span class="mi">1</span><span class="o">));</span>
    <span class="o">}</span>

<span class="o">}</span>

<span class="cm">/**
 * 固定延迟的生成器(基于系统时间)
 */</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TimeLagWatermarkGenerator</span> <span class="kd">implements</span> <span class="nc">WatermarkGenerator</span><span class="o">&lt;</span><span class="nc">MyEvent</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">maxTimeLag</span> <span class="o">=</span> <span class="mi">5000</span><span class="o">;</span> <span class="c1">// 5 seconds</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onEvent</span><span class="o">(</span><span class="nc">MyEvent</span> <span class="n">event</span><span class="o">,</span> <span class="kt">long</span> <span class="n">eventTimestamp</span><span class="o">,</span> <span class="nc">WatermarkOutput</span> <span class="n">output</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// don't need to do anything because we work on processing time</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onPeriodicEmit</span><span class="o">(</span><span class="nc">WatermarkOutput</span> <span class="n">output</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">output</span><span class="o">.</span><span class="na">emitWatermark</span><span class="o">(</span><span class="k">new</span> <span class="nc">Watermark</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">-</span> <span class="n">maxTimeLag</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="punctuated-watermarkgenerator">Punctuated WatermarkGenerator</h4>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">PunctuatedAssigner</span> <span class="kd">implements</span> <span class="nc">WatermarkGenerator</span><span class="o">&lt;</span><span class="nc">MyEvent</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onEvent</span><span class="o">(</span><span class="nc">MyEvent</span> <span class="n">event</span><span class="o">,</span> <span class="kt">long</span> <span class="n">eventTimestamp</span><span class="o">,</span> <span class="nc">WatermarkOutput</span> <span class="n">output</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">hasWatermarkMarker</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">output</span><span class="o">.</span><span class="na">emitWatermark</span><span class="o">(</span><span class="k">new</span> <span class="nc">Watermark</span><span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getWatermarkTimestamp</span><span class="o">()));</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onPeriodicEmit</span><span class="o">(</span><span class="nc">WatermarkOutput</span> <span class="n">output</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// don't need to do anything because we emit in reaction to events above</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="reference">REFERENCE</h2>
<ol>
  <li>CARBONE P, KATSIFODIMOS A, EWEN S, 等. Apache flink: Stream and batch processing in a single engine[J]. Bulletin of the IEEE Computer Society Technical Committee on Data Engineering, IEEE Computer Society, 2015, 36(4).</li>
  <li><a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/">flink官方文档</a></li>
</ol>]]></content><author><name>wzx</name></author><category term="Flink" /><summary type="html"><![CDATA[Flink 通过 Window、Time、Watermark 完成乱序事件处理，定义窗口以及维护和更新用户定义状态等]]></summary></entry><entry><title type="html">Flink 剖析(五) checkpoint</title><link href="https://wzx140.github.io//2024/01/13/5-flink-recover/" rel="alternate" type="text/html" title="Flink 剖析(五) checkpoint" /><published>2024-01-13T00:00:00+08:00</published><updated>2024-01-13T00:00:00+08:00</updated><id>https://wzx140.github.io//2024/01/13/5-flink-recover</id><content type="html" xml:base="https://wzx140.github.io//2024/01/13/5-flink-recover/"><![CDATA[<p>Flink 中精准一次的容错机制</p>

<h2 id="一致性">一致性</h2>

<h3 id="一致性语义">一致性语义</h3>

<ul>
  <li><strong>最多一次(At-most-Once)</strong>: 用户的数据只会被处理一次，不管成功还是失败，不会重试也不会重发。</li>
  <li><strong>至少一次(At-least-Once)</strong>: 这种语义下，系统会保证数据或事件至少被处理一次。如果中间发生错误或者丢失，那么会从源头重新发送一条然后进入处理系统，所以同一个事件或者消息会被处理多次。</li>
  <li><strong>精确一次(Exactly-Once)</strong>: 每一条数据只会被精确地处理一次，不会多也不会少。</li>
</ul>

<h3 id="2pc">2PC</h3>

<p>通过 2PC 保证 source 和 sink 的精确一次，需要实现以下四个方法</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">beginTransaction()</code>: 开始一个事务，返回事务信息的句柄</li>
  <li><code class="language-plaintext highlighter-rouge">preCommit()</code>: 刷写数据，但是不提交，数据不可见</li>
  <li><code class="language-plaintext highlighter-rouge">commit()</code>: 原子性地提交 preCommit 阶段刷写的数据，数据可见</li>
  <li><code class="language-plaintext highlighter-rouge">abort()</code>: 清理 preCommit 刷写的数据</li>
</ul>

<p>检查点开始时，触发 2PC 算子的 preCommit 操作，所有算子都完成 snapshot 时，触发 2PC 算子的 commit 操作。</p>

<h2 id="轻量级异步分布式状态快照">轻量级异步分布式状态快照</h2>

<p>Flink 通过<strong>严格只处理一次的一致性保证</strong>和<strong>检查点与分区重新执行</strong>来保证执行的可靠性。因为数据源是持久并可以重新获得的，如文件，持久的消息队列等，非持久数据源通过日志实现持久化。</p>

<p>如果发生程序故障，Flink 将停止分布式流数据流。然后，系统重新启动算子，并将他们重置为最近检查点的状态。输入流也用状态快照重置，并保证作为重新启动的并行数据流中的的任何 record 都在所恢复检查点之后。</p>

<p>基于以下假设，Flink 使用轻量级异步分布式状态快照</p>

<ol>
  <li><strong>作业异常和失败极少发生</strong>，因为一旦发生异常，作业回滚到上 一个状态的成本很高</li>
  <li>为了<strong>低延迟</strong>，快照需要很快就能完成</li>
  <li><strong>Task 与 TaskManager 之间的关系是静态的</strong>，即分配完成之后， 在作业运行过程中不会改变(至少是一个快照周期内)，除非手动改变并行度</li>
</ol>

<h3 id="checkpoint">CheckPoint</h3>

<p><strong>容错机制会绘制分布式数据流和算子的状态的一致快照，包含所有算子的状态和一定时间间隔内的输入数据流。</strong></p>

<h3 id="barrier">Barrier</h3>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-5-1.png" alt="" /></p>

<p><strong>checkpoint barrier 插入到数据流中，并和 record 一起流动</strong>。barrier 严格地按照直线流动并且不会超过 record。如图所示，<strong>barrier 带有一个 ID，并且将数据流中的 record 分为当前快照的 recordSet(右部)和下一个快照的 recordSet(左部)</strong>。barrier 不会中断数据流的流动，因此非常轻量级。来自不同快照的多个 barrier 可以同时出现在数据流中，这意味着可以同时进行多个快照。</p>

<p>算子接收到 barrier 时进入<strong>对齐阶段，确保接收到每个输入流中的 barrier</strong>，才会向输出流中传递 barrier。当 sink接收到 barrier n 时，它会向检查点协调器确认快照 n。当所有的 sink 都接收到 barrier 时，快照 n 完成。</p>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-5-2.png" alt="" /></p>

<p>如图所示，当算子接收多个输入流时，需要对 barrier 进行对齐：</p>

<ol>
  <li>当算子接收到某个输入流的 barrier n 后，它就<strong>不能继续处理该数据流的后续 record</strong>，直到算子接收到其余输入流的 barrier n。否则快照 n 和快照 n+1 的 record 将会混淆。</li>
  <li>算子<strong>将不能处理的 record 放到 input buffer 中</strong></li>
  <li>当算子接收到最后一个输入流中的 barrier n 时，算子会<strong>向后传递所有等待的输出 record 以及 barrier n</strong></li>
  <li>经过以上步骤，算子恢复所有输入流数据的处理，优先处理 input buffer 中的 record</li>
</ol>

<h3 id="barrier-对齐">Barrier 对齐</h3>

<p><strong>不跳过对齐是 Exatly once 语义，但是对齐阶段可能会导致流处理的延迟</strong>。通常，这种额外的延迟大约是几毫秒，但是出现过异常的延迟显着增加的情况。</p>

<p><strong>跳过对齐是 At least once 语义</strong>。当算子接收到一个 barrier，就立即进行状态快照。算子在检查点 n 创建之前，会继续处理属于检查点 n+1 的 record，这就导致检查点 n 与检查点 n+1 之间存在数据重叠。</p>

<h3 id="state">State</h3>

<p>所有类型的<a href="/2024/01/13/2-flink-state/">状态</a>都是快照的一部分。</p>

<ul>
  <li><strong>用户定义状态</strong>：由 transformation 函数直接创建或者修改的状态</li>
  <li><strong>系统状态</strong>：这种状态指的是数据缓存，是算子计算的一部分。例如窗口，其中缓存一定数量的 record，直到计算完成为止。</li>
</ul>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-5-3.png" alt="" /></p>

<p>如图所示，算子在从输入流接收到所有 barrier 之后，向输出流发出 barrier 之前，对其状态进行快照。在这个时间点，barrier 之前的 record 进行的所有状态更新已经完成，并且没有依赖于 barrier 之后的 record。由于快照状态占用空间可能很大，因此将其存储在可配置的<a href="/2024/01/13/2-flink-state/#存储">后端存储系统</a>中。默认情况下，使用 JobManager 的内存，但<strong>对于生产用途，应配置分布式可靠存储(例如HDFS)</strong>。状态存储后，算子确认检查点，将 barrier 发送到输出流，然后继续处理数据。</p>

<p>快照包含：</p>

<ul>
  <li>对于并行输入数据源：快照创建时数据流中的位置偏移</li>
  <li>对于算子：存储在快照中的状态的指针</li>
</ul>

<h2 id="恢复策略">恢复策略</h2>

<p>一旦遇到故障，Flink 选择最近一个完成的检查点 k，重置所有算子的状态到检查点 k，数据源被置为从检查点 k 位置读取。如果是增量快照，算子需要从最新的全量快照恢复，然后对此状态进行一系列增量更新。</p>

<p>Flink 支持了不同级别的故障恢复策略，<code class="language-plaintext highlighter-rouge">jobmanager.execution.failover-strategy</code> 的可配置项有两种：<strong>full 和 region</strong>。</p>

<ul>
  <li>Full：Task 发生故障，那么<strong>该 Job 的所有 Task 都会发生重启</strong>。恢复成本高，但是恢复作业一致性的最安全策略</li>
  <li>Region：当某一个 Task 发生故障时，只需重启最小 Region</li>
</ul>

<h2 id="重启策略">重启策略</h2>

<p>如果用户配置了checkpoint，但没有设置重启策略，那么会按照固定延迟重启策略模式进行重启；如果用户没有配置checkpoint，那么默认不会重启</p>

<h3 id="无重启策略">无重启策略</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>restart-strategy: none
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="nc">ExecutionEnvironment</span> <span class="n">env</span> <span class="o">=</span> <span class="nc">ExecutionEnvironment</span><span class="o">.</span><span class="na">getExecutionEnvironment</span><span class="o">();</span>
<span class="n">env</span><span class="o">.</span><span class="na">setRestartStrategy</span><span class="o">(</span><span class="nc">RestartStrategies</span><span class="o">.</span><span class="na">noRestart</span><span class="o">());</span>
</code></pre></div></div>

<h3 id="固定延迟重启策略">固定延迟重启策略</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>restart-strategy: fixed-delay
# 每个task重试
restart-strategy.fixed-delay.attempts: 3
# 每次重试间隔
restart-strategy.fixed-delay.delay: 5 s
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">env</span><span class="o">.</span><span class="na">setRestartStrategy</span><span class="o">(</span><span class="nc">RestartStrategies</span><span class="o">.</span><span class="na">fixedDelayRestart</span><span class="o">(</span>
        <span class="mi">3</span><span class="o">,</span> <span class="c1">// 重启次数</span>
        <span class="nc">Time</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">5</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">SECONDS</span><span class="o">)</span> <span class="c1">// 时间间隔</span>
<span class="o">));</span>
</code></pre></div></div>

<h3 id="失败率重启策略">失败率重启策略</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>restart-strategy: failure-rate
# 时间范围内最大失败次数
restart-strategy.failure-rate.max-failures-per-interval: 3
# 时间范围
restart-strategy.failure-rate.failure-rate-interval: 5 min
# 重试间隔时间
restart-strategy.failure-rate.delay: 5 s
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">env</span><span class="o">.</span><span class="na">setRestartStrategy</span><span class="o">(</span><span class="nc">RestartStrategies</span><span class="o">.</span><span class="na">failureRateRestart</span><span class="o">(</span>
        <span class="mi">3</span><span class="o">,</span> <span class="c1">// 每个时间间隔的最大故障次数</span>
        <span class="nc">Time</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">5</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">MINUTES</span><span class="o">),</span> <span class="c1">// 测量故障率的时间间隔</span>
        <span class="nc">Time</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">5</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">SECONDS</span><span class="o">)</span> <span class="c1">//  每次任务失败时间间隔</span>
<span class="o">));</span>
</code></pre></div></div>

<h2 id="reference">REFERENCE</h2>

<ol>
  <li>CARBONE P, KATSIFODIMOS A, EWEN S, 等. Apache flink: Stream and batch processing in a single engine[J]. Bulletin of the IEEE Computer Society Technical Committee on Data Engineering, IEEE Computer Society, 2015, 36(4).</li>
  <li><a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/">flink官方文档</a></li>
</ol>]]></content><author><name>wzx</name></author><category term="Flink" /><summary type="html"><![CDATA[Flink 中精准一次的容错机制]]></summary></entry><entry><title type="html">Flink 剖析(六) 内存管理与类型系统</title><link href="https://wzx140.github.io//2024/01/13/6-flink-memory/" rel="alternate" type="text/html" title="Flink 剖析(六) 内存管理与类型系统" /><published>2024-01-13T00:00:00+08:00</published><updated>2024-01-13T00:00:00+08:00</updated><id>https://wzx140.github.io//2024/01/13/6-flink-memory</id><content type="html" xml:base="https://wzx140.github.io//2024/01/13/6-flink-memory/"><![CDATA[<p>Flink 中内存管理与类型系统</p>

<h2 id="内存管理">内存管理</h2>

<h3 id="内存模型">内存模型</h3>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-6-3.png" alt="" width="200" /></p>

<ul>
  <li>堆内内存
    <ul>
      <li>框架内存：TM 本身所占用的堆内内存，不记入 Slot 的资源中</li>
      <li>Task 内存：Task 执行用户代码时所使用的堆内内存</li>
    </ul>
  </li>
  <li>堆外内存
    <ul>
      <li>直接内存
        <ul>
          <li>框架内存：TM 本身所占用的堆外内存，不记入 Slot 的资源中</li>
          <li>Task 内存：Task 执行用户代码时所使用的堆外内存</li>
          <li>网络缓冲内存：Task 间网络传输使用到的缓存</li>
        </ul>
      </li>
      <li>托管内存：Flink 管理的堆外内存。在批处理中用在排序、Hash 表和中间结果的缓存中，在流计算中作为 RocksDBStateBackend 的内存</li>
    </ul>
  </li>
</ul>

<h3 id="内存数据结构">内存数据结构</h3>

<h4 id="memorysegment">MemorySegment</h4>

<p><strong>MemorySegment 是 Flink 的内存抽象的最小分配单元</strong>。默认情况下，一个MemorySegment 对应着一个 32 KB 大小的内存块。这块内存既可以是堆内内存(Java 的 byte 数组)，也可以是堆外内存(基于 Netty 的 DirectByteBuffer)</p>

<h4 id="dataview">DataView</h4>

<p>MemorySegment 之上存储数据访问视图，读取抽象为 DataInputView，数据写入抽象为 DataOutputView。有了这一层，上层使用者无须关心  MemorySegment 的细节，<strong>该层会自动处理跨 MemorySegment 的读取和写入</strong></p>

<h4 id="networkbuffer">NetworkBuffer</h4>

<p>NetworkBuffer 包装了 MemorySegment，用来<strong>处理各个 TM 之间的数据传递</strong></p>

<h4 id="bufferpool">BufferPool</h4>

<p>BufferPool 用来管理 Buffer 的申请、释放、销毁、可用 Buffer 通知等。</p>

<p>每个 TM 只有一个 NetworkBufferPool，同一个 TM 上的 Task 共享NetworkBufferPool，在 TM 启动的时候，就会创建 NetworkBufferPool，为其分配内存</p>

<h4 id="memorymanager">MemoryManager</h4>

<p>MemoryManager 用来理堆外的托管内存。在批处理中用在排序、Hash表和中间结果的缓存 中，在流计算中作为RocksDBStateBackend的内存</p>

<h2 id="类型系统">类型系统</h2>

<h3 id="逻辑类型">逻辑类型</h3>

<ul>
  <li><strong>物理类型系统面向开发者</strong>，指的是 Java Class</li>
  <li>逻辑类型系统是描述物理类型的类型系统，能够<strong>对物理类型就行序列化/反序列化</strong>。对于常见的物理类型，Flink 提供了内置的序列化方法，其他类型默认交给 kryo 进行序列化，也可以自定义。
    <ul>
      <li>针对 DataStream/DataSet API 的 TypeInformation 类型系统</li>
      <li>针对 SQL 的 LogicalTypes 类型系统</li>
    </ul>
  </li>
</ul>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-6-1.png" alt="" /></p>

<h3 id="类型推断">类型推断</h3>

<p>Flink 应用使用反射机制获取 Function 的输入和输出类型。对于一些带有泛型的类型， <strong>Java 泛型的类型擦除机制会导致 Flink 在处理 Lambda 表达式的类型推断时不能保证一定能提取到类型</strong>。</p>

<p>在开发某些带有泛型返回值的 transformation 时，需要传入 TypeHint 来获取泛型的类型信息。</p>

<h2 id="sql-row">SQL Row</h2>

<p>接口 BaseRow 定义了针对表中一行数据的处理方法 getter(index)/setter(index) 等，有以下实现类</p>

<ul>
  <li>BinaryRow: 二进制行式存储，分为定长部分和不定长部分，定长部分只能在一个 MemorySegment 内，来提升读写字段的速度</li>
  <li>NestedRow: 与 BinaryRow 的内存存储结构一样，区别在于 NestedRow的定长部分可以跨 MemorySegment</li>
  <li>UpdatableRow: 该类型的Row比较特别，其保存了该行所有字 段的数据，更新字段数据的时候不修改原始数据，而是使用一个数组 记录被修改字段的最新值。读取数据的时候，首先判断数据是否被更 新过，如果更新过则读取最新值，如果没有则读取原始值</li>
  <li>ObjectArrayRow: 使用对象数组保存一整行的数据
    <ul>
      <li>GenericRow: 存储的数据类型是原始类型</li>
      <li>BoxedWrapperRow: 存储的数据类型是可序列化和可比较大小的对象类型</li>
    </ul>
  </li>
  <li>JoinedRow: 逻辑上合并的两个 BaseRow，物理上没有合并</li>
</ul>

<p>重点介绍以下 BinaryRow 的存储结构</p>

<p><img src="https://wzx140.github.io//assets/img/2024-1-13-6-2.png" alt="" /></p>

<ul>
  <li>定长部分
    <ul>
      <li>头信息区：1 byte</li>
      <li>空值索引(Null Bit Set)：8 bytes 对齐，标记 Null 的位置</li>
      <li>定长字段值：保存基本类型和 8 bytes 长度以内字段的值</li>
      <li>变长字段索引：保存变长类型和大于 8 bytes 长度字段的 offset 和 length</li>
    </ul>
  </li>
  <li>变长部分
    <ul>
      <li>变长字段值：保存变长类型和大于 8 bytes 长度字段的值</li>
    </ul>
  </li>
</ul>

<h2 id="reference">REFERENCE</h2>

<ol>
  <li><a href="https://ci.apache.org/projects/flink/flink-docs-release-1.10/">flink官方文档</a></li>
</ol>]]></content><author><name>wzx</name></author><category term="Flink" /><summary type="html"><![CDATA[Flink 中内存管理与类型系统]]></summary></entry><entry><title type="html">你不是真正的快乐 ——读《心流》、《欲罢不能》</title><link href="https://wzx140.github.io//2023/03/05/addiction-and-happyness/" rel="alternate" type="text/html" title="你不是真正的快乐 ——读《心流》、《欲罢不能》" /><published>2023-03-05T00:00:00+08:00</published><updated>2023-03-05T00:00:00+08:00</updated><id>https://wzx140.github.io//2023/03/05/addiction-and-happyness</id><content type="html" xml:base="https://wzx140.github.io//2023/03/05/addiction-and-happyness/"><![CDATA[<p>全人类加起来，我们每年浪费了数以百万年计的人类意识，这么大的能量本来可以用来完成更复杂的目标，带动乐趣横生的成长，现在却浪费在模拟现实的刺激追求上。</p>

<h1 id="行为上瘾">行为上瘾</h1>

<p><strong>上瘾是一种短期内可解决心理需求，但长期却会造成严重伤害的行为</strong>。其此刻带来的奖励最终因为其破坏性后果而抵消。行为上瘾的特征为有害性，呼吸并不是上瘾，因为哪怕难于抵挡，也没有坏处。上瘾是对难于戒除的有害体验的深度依恋。</p>

<p><strong>上瘾行为形成的原因是<a href="/2022/04/22/dopamine/">多巴胺产生的愉悦感</a></strong>。大脑把多巴胺喷涌转换成愉悦感，初期的有利方面明显大于不利方面。但很快，大脑会把这种喷涌解释为错误，产生的多巴胺越来越少。要达到最初的高峰，唯一方法是增加更多的行为，比如用更多的钱赌博，吸更多的可卡因，花更多时间玩更投入的电子游戏和手机短视频。随着大脑产生耐受性，多巴胺生成区域开始进入静待状态，每一轮高峰之间的低谷变得更低。这些区域不再生成健康剂量的多巴胺，而是转入休眠状态，静待下一轮过度刺激的降临。只要我们继续拼命去获取上瘾源，这样的循环就会持续下去，每一轮刺激过后，大脑产生的多巴胺也越来越少。</p>

<p><strong>行为上瘾形成的必要条件是容易获取的上瘾源</strong>。如果你想驱使人们采取行动，要把大目标切割成便于管理的具体刚好可以一次完成的小目标。如果已经玩了游戏几分钟甚至几个小时，你可能已经打通了很多关卡，你最不愿意做的就是认输了。胜利即将来临的感觉让你欲罢不能。最初的数百次尝试都以失败告终，但是一直感觉，只要鼠标按钮稍微一滑，就能在时间限制内通过这一关，像这样的差一点儿就赢了的感觉往往比真正的胜利更让人上瘾。</p>

<p>上瘾源的积极反馈让人不可抗拒，不确定性成为了人们追求的目标。每次滑动小视频都有可能带来更好看的内容（由于推荐算法的作用，它确实是这样的），于是你在刷视频中消耗了时间。毕竟这是大脑认为获取快乐的低成本方式了。</p>

<h1 id="远离上瘾">远离上瘾</h1>

<p>意志力是看着美味的巧克力饼干又拒绝它们。而良好的习惯是一开始就让你身边没有这些美味的巧克力饼干。意志力是稀缺的消耗品，如果你为了达成某种目的而使用了意志力，那么你必须等待意志力恢复后才能强制自己做另外的事情。</p>

<p><strong>根据黄金法则，习惯由3个部分构成：线索（任何能促使行为的东西）；惯例（行为本身）；奖励（训练大脑在将来重复该习惯的回报）</strong>。克服不良习惯或上瘾的最佳方法是，在改变惯例的同时保留线索和奖励，即只用分心之事来改变原有行为。</p>

<ol>
  <li>
    <p>任何放在你身边的东西都比离得远的东西对你的精神生活有着更大的影响。身边包围着诱惑，你就会受到诱惑；把诱惑放到手拿不到的地方，你会发现无形的意志力储备。你无法完全避免诱惑，但是可以削弱上瘾体验的心理迫切性。</p>
  </li>
  <li>
    <p>奖励让大脑记住该习惯回路，以备将来之用。提供奖赏将提供正加强，有利于人对执行该行为产生渴求感，促进习惯的养成。</p>
  </li>
</ol>

<h1 id="本我自我超我">本我、自我、超我</h1>

<p>1923年，弗洛伊德提出相关概念，以解释意识和潜意识的形成和相互关系。本我（完全潜意识）代表欲望，受意识遏抑；自我负责处理现实世界的事情；超我是良知或内在的道德判断。<strong>自我是永久存在的，而超我和本我又几乎是永久对立的，为了协调本我和超我之间的矛盾，自我需要进行调节</strong>。</p>

<p>本我代表着<a href="/2019/07/18/gene/">基因的奴隶</a>，它只会追求最原始的快乐以保证基因的生存和繁衍。</p>

<p>超我则代表彻底社会化的人，只追求周遭他认定应该期望的东西。他可能会经历许多难能可贵的事，但因这些事与他的欲望不符，他就会完全忽略它们。他在意的并非现在拥有的，而是满足别人的要求后能获得的，沦为社会控制的奴隶，只知道周而复始地追逐一到手就化为泡影的奖赏。我们不能全盘接受社会的奖赏，试着以自己所能控制的奖赏取而代之，我们要在别人用以诱惑我们的目标之外，另行建立一套自己的目标。外界事物令你痛苦并不是因为它们打扰你，而是由于你对它们的判断，而你有能力立刻消除那种判断。</p>

<p>自我代表一个人在现实环境中真正的需要，控制意识才能控制体验的品质，才能体验到真正的幸福，任何在这方面最起码的进步都足以提升生活的品质，使生活更快乐、更有意义。</p>

<h1 id="心流与幸福">心流与幸福</h1>

<p>心流描述人们一种完全沉浸和完全投入于活动本身的心智状态的振奋状态。比如当你阅读书籍、操作实验时，当你参加体育训练、音乐学习时，你全神贯注，不畏艰辛，只为获得新知新能。<strong>此时的你因心无旁骛而感受到巨大的幸福</strong>。体验过心流的人都知道，那份深沉的快乐是严格的自律、集中注意力换来的。控制意识最明显的指标就是能随心所欲地集中注意力，不因任何事情而分心。若能做到这一点，就能在日常生活中找到乐趣。</p>

<p>构成心流体验的必要因素是：</p>

<ol>
  <li>这种体验的事情必须要在我们的能力范围之内</li>
  <li>我们必须能够全神贯注于这件事情</li>
  <li>这件事情有明确的目标</li>
  <li>这件事情有即时的反馈</li>
  <li>我们能深入而毫不牵强地投入到行动之中，日常生活的忧虑和沮丧都因此一扫而空。</li>
  <li>充满乐趣的体验使人觉得能自由控制自己的行动</li>
  <li>进入“忘我”状态，但心流体验告一段落后，自我感觉又会变得强烈</li>
  <li>时间感会改变——几小时犹如几分钟，几分钟也可能变得像几小时那么漫长。这些元素结合成一种深刻的愉悦感，带来无比的报偿，并扩展成极大的能量，仅是感觉它的存在就已值回“票价”了</li>
</ol>

<p><strong>真正给人带来乐趣的并不是事情本身，而是在艰难状况下行使控制权的感觉</strong>。只有在个人力量能左右结果时，才能确知自己握有控制权。</p>

<p>天下熙熙，皆为利来；天下嚷嚷，皆为利往。完全超我社会化，只能让我们追逐一到手就化为泡影的奖赏。完全本我物质化，只能让基因赋予我们快乐。利用自我去控制注意力去得到心流的感觉固然能体验到幸福，但一定得先改善实际物质条件，然后心流才能对生活品质有所裨益。换句话说，心流只是锦上添花，健康与财富才是根本。主观能动力不能超越物质基础。<strong>如果以为能控制意识的人，不论发生什么事仍然会快乐，那就未免太天真、太理想主义了。一个人所能承受的痛苦、饥饿、剥夺，都有一定的限度</strong>。我们不能过分的追求自我、本我、超我的单一方面的诉求，而是让他们达到某种稳定的平衡。</p>]]></content><author><name>wzx</name></author><category term="BookReview" /><summary type="html"><![CDATA[全人类加起来，我们每年浪费了数以百万年计的人类意识，这么大的能量本来可以用来完成更复杂的目标，带动乐趣横生的成长，现在却浪费在模拟现实的刺激追求上。]]></summary></entry><entry><title type="html">身处寒夜 把握星光 ——读《贪婪的多巴胺》</title><link href="https://wzx140.github.io//2022/04/22/dopamine/" rel="alternate" type="text/html" title="身处寒夜 把握星光 ——读《贪婪的多巴胺》" /><published>2022-04-22T00:00:00+08:00</published><updated>2022-04-22T00:00:00+08:00</updated><id>https://wzx140.github.io//2022/04/22/dopamine</id><content type="html" xml:base="https://wzx140.github.io//2022/04/22/dopamine/"><![CDATA[<p>虽然只有 0.0005% 的脑细胞可以产生多巴胺，但这些细胞却能对行为产生巨大的影响。多巴胺是“快乐分子”，大脑中产生多巴胺的途径为“奖赏回路”。当人们产生多巴胺时，他们能体验到快乐的感觉，因此会不遗余力地激活这些稀有的细胞，这是一种是无法抗拒的诱惑。</p>

<h1 id="预期">预期</h1>

<p>奖赏预测误差表现为从新奇事物中得到的快感，正是这种让人快乐的误差激活了奖赏回路。<strong>这种快乐不是源于时间或金钱本身，而是预期之外的奖励带来的兴奋感。</strong></p>

<p>从多巴胺的角度来说，拥有不会带来快乐，更多的获取才会带来快乐。如果你睡在桥洞，多巴胺会让你想获得一顶帐篷。如果你生活在帐篷里，多巴胺就会让你想获得一栋房子。多巴胺不管现在的事物已有多完美，追求也永无止境。所以说人是一种永远不知道满足的生物，野心总是不断膨胀，当然这也是人类能持续发展的原因之一。</p>

<h1 id="成瘾">成瘾</h1>

<p>对于一个生物来说，未来最重要的目标就是活到那个时候。<strong>在发现了有潜在价值的东西(多巴胺触发器)后，多巴胺系统会通过传递“想要”的感觉来触发人们进一步的行为，以此达到趋利避害的作用</strong>。这种感觉并不是你主动选择的，而是你对所遇之事的反应。</p>

<p>毒品、酒精、尼古丁之所以会成瘾，是因为它触发了欲望回路中的多巴胺。毒品从根本上不同于天然的多巴胺触发器，它没有适当的检查和平衡机制来保持稳定，因此不会产生一种饱足感，瘾君子想要的只有更多的毒品。</p>

<h1 id="未来与当下">未来与当下</h1>

<p>多巴胺具有两种完全不同的功能。<strong>欲望回路是让我们产生“想要”的感觉，甚至对某些事物成瘾。多巴胺控制回路给予我们制定计划的能力，创造新的工具和抽象概念来最大限度地利用未来的资源，通过理性的思考去追求和实现自己的目标</strong>。它是非常理性的，感觉不到任何情绪，因为情绪是一种当下的现象。</p>

<p>额叶负责多巴胺控制回路，它是大脑中发育得最晚的区域，直到一个人结束青春期进入成年期之后，它才与大脑的其他部分完成连接。<strong>当控制回路很弱时，人们会去追求他们想要的东西而不考虑长期的后果</strong>，这也是小孩子自控能力差，需要成年人监护的原因。</p>

<p>当下分子包括血清素、催产素、内啡肽和内源性大麻素，与多巴胺所产生的对于预期的愉悦相反，这些化学物质会<strong>给我们带来由当下的感觉和情感引发的愉悦，让我们更加关注身边具体的人或事</strong>。同时<strong>当下分子让我们拥有同理心来理解别人的想法，这是社交互动的基本技能。</strong></p>

<p>多巴胺控制回路与当下分子回路是相互对立的。<strong>多巴胺代表着痴迷与渴望，而当下分子则意味着当下的幸福</strong>。设想这样一个问题，道路上两辆自动驾驶汽车不可避免地会发生致命的撞车事故，那么自动驾驶汽车的程序应该遵守什么原则呢？它应该转向一个方向来保护它主人的生命，还是应该转向相反的方向牺牲它的主人，以保护另一辆车上更多的人呢？多巴胺控制回路会告诉它应该保护大多数人，而当下分子回路则会告诉它要保护眼下的主人。</p>

<p>多巴胺控制回路与当下分子回路也是相互补充的。<strong>过多的多巴胺会导致精力旺盛的痛苦（如工作狂式的主管），而过多的当下分子则会导致快乐的懒惰（如躺在地下室抽大麻的人）</strong>。平衡的多巴胺与当下分子才能获得健康的 Work-Life-Balance。</p>

<h1 id="多巴胺能人格">多巴胺能人格</h1>

<p><strong>多巴胺能人格会产生高水平的多巴胺，从而抑制当下分子的功能，所以这类人往往更具有抽象与理性的思维但在人际关系的处理上往往表现很差</strong>。爱因斯坦曾说过：“我对社会正义和社会责任充满热情，但我与其他人直接接触时却表现冷淡，这实在是一种奇怪的对比”。处理社会正义和人性的抽象概念对他来说很容易，但与一个具体的人碰面对他来说太难了。这类人沉迷于让未来更有价值，而牺牲了现在能够体验到的快乐。</p>

<p>虽然多巴胺和当下分子在进化中学会了通力合作，但是在许多情况尤其是现代社会下，多巴胺和当下分子会失去平衡，往多巴胺这一边倾斜。</p>

<h1 id="生活焦虑">生活焦虑</h1>

<p><strong>快节奏的社会文化让我们做任何事都追求极致的效率</strong>。在我们的生活中，那些时长较短，要素密集且浅显的视频变成了主流；总结浓缩的剧集解说占据了我们的生活。这个娱乐至死的时代劫持了多巴胺的欲望回路，我们没有耐心摸索学习一项业余技能，仔细读完一本书，甚至认真看完一整部电影，我们逐渐失去了深入思考的能力，转而去寻求更快更强烈的刺激。</p>

<p>我在阅读一本书时，会记录一些笔记以及提炼一些知识点，我认为读书的过程并不重要，重要的是读完后你记住了哪些抽象的知识点。我的朋友这样对我说，不是所有书都要当做教科书去阅读，而需要靠内心感悟书籍，你读懂了一本书，心中自然会领悟他的主旨，心态自然会受其影响。</p>

<h1 id="工作焦虑">工作焦虑</h1>

<p>当快节奏成为了社会的主旋律，效率成为了大厂的制胜法宝时，快速迭代以及边际效益成为了工作的考量，大家比以往更关注ROI，目标也变得易变，复杂以及不确定。这一切都加剧了我们的高多巴胺能。这是适应目前市场环境的产物，能给企业带来最大的收益。但是对于个人生活而言，增加了我们对“更多”的痴迷，忽略了我们已经拥有的东西。<strong>多巴胺达到了它的目标，到达了它渴望的顶峰，挤压了所有可用资源的最后一滴，可代价是……</strong></p>

<p>在工作中，我是一个很焦虑的人，焦虑来源于对未完成任务以及未来可能发生事故的担忧。用多巴胺回路来定义自我，会让我们陷入一个充满猜测和可能性的世界。此时此地的有形世界被忽视，甚至让我们感到畏惧，因为我们无法控制它。即使并不是紧急的工作，我也会工作到很晚，工作时也没有心情看其他无关的消息，因为未完成的任务始终会在我心中形成强大的压迫感。项目上线前总是我最紧张的时刻，这种未知的恐惧让我精神时刻处于紧绷状态，工作群的任何消息都会让我心跳加速。</p>

<p>我会找一段放空的时间去缓解这种焦虑感。运动便是一种很好的方法，在沉浸式接触大自然也是一种很好的调剂方法。最重要还是心态的改变，工作与生活可以有部分交集，但生活决不能被工作绑架。我们需要调整自己的心态，适当走得慢一些，让心灵跟得上前进的步伐。<strong>我们无法预测未来，对未来的过分担忧是毫无意义的，我们更应该做的是把握当下。</strong></p>

<h1 id="把握星光">把握星光</h1>

<p>在这个快节奏的时代，<strong>我们被裹挟着快速前进，逐渐失去了感受世界的乐趣</strong>。我们不去欣赏花朵的美丽，只去想象它在厨房桌子上的花瓶里会是什么样子；我们不去闻早晨的空气、抬头看看天空，而是弯下脖子，在智能手机上查询天气，对周围的世界视而不见。</p>

<p><strong>我们不能被多巴胺绑架生活，而是应该更多地关注眼前之人，享受眼下的生活。</strong>社交孤立（即使在没有孤独感的情况下）会导致50%~90%的早逝风险。这个比例和吸烟差不多，比肥胖或缺乏锻炼还要高。哪怕只是为了能活下去，我们的大脑也需要同理心与亲和关系。如果我们忽视了同理心，我们就失去了让别人感到快乐的能力。如果我们忽视了亲和关系，我们很可能会失去获得快乐的能力。</p>]]></content><author><name>wzx</name></author><category term="BookReview" /><summary type="html"><![CDATA[虽然只有 0.0005% 的脑细胞可以产生多巴胺，但这些细胞却能对行为产生巨大的影响。多巴胺是“快乐分子”，大脑中产生多巴胺的途径为“奖赏回路”。当人们产生多巴胺时，他们能体验到快乐的感觉，因此会不遗余力地激活这些稀有的细胞，这是一种是无法抗拒的诱惑。]]></summary></entry></feed>