Flink State Processor API 读写/修复 Savepoint,把“状态”当成可查询的数据

1. State Processor API 能解决什么问题

典型用法(都是真实生产会遇到的):

  • 状态审计/验收:对线上作业打一个 savepoint,用批作业读出来做一致性校验

  • 状态修复:修掉异常 key、纠正不一致 entries、清理脏数据

  • 状态引导(bootstrap):从离线历史数据构造 state,写成 savepoint,给流作业冷启动

  • 作业演进不丢状态

    • 修改 state 的数据类型(兼容/迁移)
    • 调整算子最大并行度(maxParallelism)
    • 拆分/合并 operator state
    • 重分配 operator UID(或从 UID hash 迁移到 UID)

依赖(Flink 2.2.0 示例):

xml 复制代码
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-state-processor-api</artifactId>
  <version>2.2.0</version>
</dependency>

2. 心智模型:Savepoint 就是一座"数据库"

理解它,你基本就通了。

  • 一个 Flink Job 由多个 operator 组成(Src/Proc/Snk)

  • 每个 operator 可能有两类状态:

    • Operator State:算子级别,按 subtask 组织,常见 ListState/UnionListState/BroadcastState
    • Keyed State:按 key 分区的状态,像分布式 KV(ValueState/ListState/MapState/AggregatingState...)

State Processor API 会把一个 savepoint 映射成"数据库":

  • 每个 operator(用 UID 标识)是一个 namespace
  • 每个 operator state 映射成单列表(所有 subtasks 的 list entry 汇总)
  • 同一个 operator 的所有 keyed states 合并到一张表:
    key 一列 + 每个 keyed state 一个列(同 key 的不同 state 并在一行)

这也解释了为什么 Table/SQL 很适合做状态分析:它天然就是在查表。

3. 先把"算子识别"做好:UID 优先,hash 兜底

State Processor API 通过 OperatorIdentifier 定位算子:

  • 最推荐:OperatorIdentifier.forUid("my-uid")
  • UID 不可用时(历史作业没设置 UID):OperatorIdentifier.forUidHash("...")

工程建议:生产作业务必显式 .uid("xxx"),否则后面迁移/修状态会非常痛苦。

4. DataStream API 读状态:SavepointReader

读取的第一步:给出 savepoint/checkpoint 路径 + 与原作业一致的 StateBackend(兼容性规则与正常恢复一致)。

java 复制代码
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
SavepointReader sp = SavepointReader.read(env, "hdfs://path/", new HashMapStateBackend());

4.1 读 Operator List State

对应作业里 getListState(new ListStateDescriptor<>("list-state", ...)) 写出的状态:

java 复制代码
DataStream<Integer> listState = sp.readListState(
  OperatorIdentifier.forUid("my-uid"),
  "list-state",
  Types.INT
);

4.2 读 Union List State

对应 getUnionListState:读取时会返回"等价于并行度 1 的单份状态"。

java 复制代码
DataStream<Integer> unionState = sp.readUnionState(
  OperatorIdentifier.forUid("my-uid"),
  "union-state",
  Types.INT
);

4.3 读 Broadcast State

BroadcastState 读取同样是"单份副本"语义:

java 复制代码
DataStream<Tuple2<Integer, Integer>> bc = sp.readBroadcastState(
  OperatorIdentifier.forUid("my-uid"),
  "broadcast-state",
  Types.INT,
  Types.INT
);

4.4 自定义序列化器

如果原 StateDescriptor 用了自定义 TypeSerializer,读取也要对应指定:

java 复制代码
DataStream<Integer> listState = sp.readListState(
  OperatorIdentifier.forUid("uid"),
  "list-state",
  Types.INT,
  new MyCustomIntSerializer()
);

5. DataStream API 读 Keyed State:KeyedStateReaderFunction(最强也最容易踩坑)

Keyed State 的读取入口是:

java 复制代码
DataStream<Out> ds = sp.readKeyedState(
  OperatorIdentifier.forUid("my-uid"),
  new MyReaderFunction()
);

你需要实现 KeyedStateReaderFunction<KeyType, OutType>,在 open() 里注册你要读的各种 state descriptor,然后在 readKey() 中针对每个 key 输出一条(或多条)结果。

示例:读取 ValueState<Integer> + ListState<Long>

java 复制代码
public static class KeyedState {
  public int key;
  public int value;
  public List<Long> times;
}

public static class ReaderFunction extends KeyedStateReaderFunction<Integer, KeyedState> {

  private ValueState<Integer> state;
  private ListState<Long> updateTimes;

  @Override
  public void open(OpenContext openContext) {
    state = getRuntimeContext().getState(
      new ValueStateDescriptor<>("state", Types.INT)
    );
    updateTimes = getRuntimeContext().getListState(
      new ListStateDescriptor<>("times", Types.LONG)
    );
  }

  @Override
  public void readKey(Integer key, Context ctx, Collector<KeyedState> out) throws Exception {
    KeyedState data = new KeyedState();
    data.key = key;
    data.value = state.value();
    data.times = StreamSupport
      .stream(updateTimes.get().spliterator(), false)
      .collect(Collectors.toList());
    out.collect(data);
  }
}

关键坑点(非常重要):

  • 所有 state descriptor 必须在 open() 里"提前注册"
    文档明确说:在 readKey() 里再调用 getRuntimeContext().get*State 会直接抛 RuntimeException
  • Context 还能访问该 key 的元数据:event time / processing time timers(适合做诊断)

6. 读 Window State:读窗口聚合结果 + 定时器

State Processor API 支持读取窗口算子状态,适合排查"窗口聚合对不对、定时器是否异常"等。

使用方式:指定 window assigner + 聚合函数 + 可选 WindowReaderFunction 进行"富化输出"。

示例:每分钟按 userId 统计点击数的窗口聚合,读出 countwindowtrigger timers

java 复制代码
savepoint
  .window(TumblingEventTimeWindows.of(Duration.ofMinutes(1)))
  .aggregate(
    "click-window",
    new ClickCounter(),
    new ClickReader(),
    Types.STRING, Types.INT, Types.INT
  )
  .print();

并且在 WindowReaderFunction 的 context 中还能读 trigger state(CountTrigger 或自定义 trigger 的状态)。

7. 写 Savepoint:SavepointWriter + BootstrapTransformation(用离线数据"造状态")

写 savepoint 的核心用途:bootstrap。比如你要让新作业上线时直接带着历史累计值,而不是从 0 开始。

注意:写 savepoint 的程序必须是 BATCH 执行

7.1 基本写法:newSavepoint + withOperator

java 复制代码
int maxParallelism = 128;

SavepointWriter
  .newSavepoint(env, new HashMapStateBackend(), maxParallelism)
  .withOperator(OperatorIdentifier.forUid("uid1"), transformation1)
  .withOperator(OperatorIdentifier.forUid("uid2"), transformation2)
  .write(savepointPath);

这里最关键的是:uid1/uid2 必须和未来要恢复的 DataStream 作业中算子的 .uid("...") 一一对应,否则恢复不了。

7.2 写 Operator State:StateBootstrapFunction

适合 CheckpointedFunction 里用的 operator list state。

java 复制代码
public class SimpleBootstrapFunction extends StateBootstrapFunction<Integer> {
  private ListState<Integer> state;

  @Override
  public void initializeState(FunctionInitializationContext context) throws Exception {
    state = context.getOperatorState()
      .getListState(new ListStateDescriptor<>("state", Types.INT));
  }

  @Override
  public void processElement(Integer value, Context ctx) throws Exception {
    state.add(value);
  }
}

构造 transformation:

java 复制代码
StateBootstrapTransformation t = OperatorTransformation
  .bootstrapWith(env.fromElements(1,2,3))
  .transform(new SimpleBootstrapFunction());

7.3 写 Broadcast State:BroadcastStateBootstrapFunction

Broadcast state 要求"全量能放进内存",和流作业的广播语义一致。

java 复制代码
public class CurrencyBootstrapFunction extends BroadcastStateBootstrapFunction<CurrencyRate> {
  public static final MapStateDescriptor<String, Double> descriptor =
    new MapStateDescriptor<>("currency-rates", Types.STRING, Types.DOUBLE);

  @Override
  public void processElement(CurrencyRate v, Context ctx) throws Exception {
    ctx.getBroadcastState(descriptor).put(v.currency, v.rate);
  }
}

7.4 写 Keyed State:KeyedStateBootstrapFunction(还能设置 timers)

java 复制代码
public class AccountBootstrapper extends KeyedStateBootstrapFunction<Integer, Account> {
  private ValueState<Double> total;

  @Override
  public void open(OpenContext openContext) {
    total = getRuntimeContext().getState(new ValueStateDescriptor<>("total", Types.DOUBLE));
  }

  @Override
  public void processElement(Account value, Context ctx) throws Exception {
    total.update(value.amount);
  }
}

组装 transformation:

java 复制代码
StateBootstrapTransformation<Account> t = OperatorTransformation
  .bootstrapWith(accountDataSet)
  .keyBy(acc -> acc.id)
  .transform(new AccountBootstrapper());

定时器注意点:

  • bootstrap 函数里设置的 timers 不会在 bootstrap 过程中触发
  • 恢复到流作业后才会激活
  • 如果设置了 processing time timer,但恢复时刻已晚于触发时间,则会在作业启动后立刻触发
  • 文档强调:如果 bootstrap 创建 timers,恢复端必须用 process 类型算子(process function family)

7.5 写 Window State:必须严格匹配原窗口配置

写窗口状态时,bootstrap 侧的窗口 assigner/trigger/evictor/聚合逻辑要与未来流作业完全一致,否则恢复语义会对不上。

8. 基于已有 Savepoint 修改:fromExistingSavepoint(增量补状态)

常见场景:老作业有 savepoint,你只想给新加的算子补一份 state,不动别的。

java 复制代码
SavepointWriter
  .fromExistingSavepoint(env, oldPath, new HashMapStateBackend())
  .withOperator(OperatorIdentifier.forUid("uid"), transformation)
  .write(newPath);

9. 改 UID 或 UID hash:救命技能

当历史作业没显式 UID 时,你可能只能从日志里拿到 uid hash,此时可以先把 hash 映射成可控 UID:

java 复制代码
savepointWriter.changeOperatorIdentifier(
  OperatorIdentifier.forUidHash("2feb7f8bcc404c3ac8a981959780bd78"),
  OperatorIdentifier.forUid("new-uid")
);

或者直接替换旧 UID 为新 UID(算子重命名/重构时很常见):

java 复制代码
savepointWriter.changeOperatorIdentifier(
  OperatorIdentifier.forUid("old-uid"),
  OperatorIdentifier.forUid("new-uid")
);

10. Table/SQL 读状态:把 Savepoint 当表查(只支持 keyed state)

如果你更喜欢 SQL(或者想给运维/数据同学一个可读的排查方式),State Table API 很香。

重要限制:State Table API 只支持 keyed state

10.1 读元信息:savepoint_metadata

sql 复制代码
LOAD MODULE state;
SELECT * FROM savepoint_metadata('/root/dir/of/checkpoint-data/chk-1');

它会告诉你 checkpoint id、operator uid、uid hash、并行度、max parallelism、各类 state size 等信息,定位问题非常快。

10.2 建表读取 keyed state:savepoint connector

sql 复制代码
CREATE TABLE state_table (
  k INTEGER,
  MyValueState INTEGER,
  MyAccountValueState ROW<id INTEGER, amount DOUBLE>,
  MyListState ARRAY<INTEGER>,
  MyMapState MAP<INTEGER, INTEGER>,
  MyAvroState ROW<longData BIGINT>,
  PRIMARY KEY (k) NOT ENFORCED
) WITH (
  'connector' = 'savepoint',
  'state.backend.type' = 'rocksdb',
  'state.path' = '/root/dir/of/checkpoint-data/chk-1',
  'operator.uid' = 'my-uid',
  'fields.MyAvroState.value-type-factory' = 'org.apache.flink.state.table.AvroSavepointTypeInformationFactory'
);

然后你就可以:

sql 复制代码
SELECT k, MyValueState FROM state_table WHERE k = 42;
SELECT COUNT(*) FROM state_table;
SELECT k, CARDINALITY(MyListState) AS list_len FROM state_table;

几个实战点:

  • state.backend.type 必须与原作业一致(hashmap/rocksdb)
  • operator.uidoperator.uid.hash 二选一
  • state name 不符合列名时,用 fields.#.state-name 覆盖
  • MapState 的 key/value 类型推断不准时,用 fields.#.key-class/value-class 或 type factory 明确指定
  • Avro 这类复杂类型,常需要 value-type-factory(文档给了 SavepointTypeInformationFactory 的套路)

10.3 默认类型映射与"快捷查看"

SQL connector 会对基础类型做默认映射。还有个很实用的"快捷方式":

  • 如果你把一个复杂 Java 类映射成 STRING 列,那么 SQL 读出来就是该对象 toString() 的结果
    适合"先看个大概、快速解释性查询",排障很省事。

11. 生产最佳实践清单

  1. 作业里给关键算子统一规划 UID(强烈建议)
  • Source/关键处理算子/Sink 都显式 .uid("...")
  1. 定义 state descriptor 名称要稳定
  • State Processor 读写都靠 state name,改名等于断档
  1. RocksDB 作业读状态也尽量用 RocksDB backend
  • backend 不一致会直接导致读取/恢复不兼容
  1. 写 savepoint 的 bootstrap 作业一定是 BATCH
  • 别拿 streaming env 去写,否则会踩执行模式问题
  1. 大状态先做"元信息盘点"
  • savepoint_metadata 看 size、并行度、max parallelism,再决定怎么读/怎么修
相关推荐
木风小助理2 小时前
Elasticsearch生产环境最佳实践指南
大数据·elasticsearch·搜索引擎
hg01182 小时前
筑梦非洲:中国电建以实干绘就中非合作新图景
大数据
WZGL12302 小时前
智慧养老方兴未艾,“AI+养老”让银龄老人晚年更美好
大数据·人工智能·物联网·生活·智能家居
檐下翻书1733 小时前
PC端免费跨职能流程图模板大全 中文
大数据·人工智能·架构·流程图·论文笔记
一只专注api接口开发的技术猿3 小时前
如何处理淘宝 API 的请求限流与数据缓存策略
java·大数据·开发语言·数据库·spring
程途拾光1583 小时前
中文界面跨职能泳道图制作教程 PC
大数据·论文阅读·人工智能·信息可视化·流程图
CORNERSTONE3653 小时前
智能制造为什么要实现EMS和MES的集成
大数据·人工智能·制造
yumgpkpm5 小时前
Cloudera CDH、CDP、Hadoop大数据+决策模型及其案例
大数据·hive·hadoop·分布式·spark·kafka·cloudera
sld1685 小时前
以S2B2C平台重构快消品生态:效率升级与价值共生
大数据·人工智能·重构