直接在本地跑一个流处理任务,和把任务放到云上处理,完全是两个世界。本地你只需要关心数据和逻辑,上了云,你要面对的是权限、网络、配额、限流,还有一堆你本地根本遇不到的“环境问题”。这篇文章就是围绕 Flink 和 AWS Kinesis 的集成来写的,解决的核心问题很简单:怎么把 Kinesis 里的实时数据流,用 Flink 稳定地消费、处理、再写回去。我会从选型思路一直讲到实操配置和排坑,适合正在做实时数仓、数据管道迁移,或者刚准备把 Flink 任务部署到 AWS 上的团队参考。
1. 整体设计与思路拆解
1.1 为什么是 Flink 和 Kinesis 这个组合
先聊一个很多团队都会纠结的问题:实时数据处理,消息中间件选型一大堆,Kafka、Pulsar、RabbitMQ,为什么偏偏要把 Kinesis 和 Flink 凑到一起?
Kinesis 是 AWS 上的托管流式数据服务,它的定位类似云上的 Kafka,但不是一个东西。Kinesis Data Streams 的核心概念是 shard,一个 shard 提供 1MB/s 的写入速度和 2MB/s 的读取速度。你用多少 shard,就决定了管道能扛多大的吞吐。这个设计和 Kafka 的 partition 很像,但 Kinesis 最大的优势是你不用自己运维集群——没有 broker 要管,没有磁盘要担心,没有副本同步要操心。你用 Flink 消费 Kinesis,本质上是让 Flink 成为 Kinesis 的消费者,像读本地文件一样读云上的数据流。
而 Flink 的优势在于,它是真正意义上的流处理引擎,不是微批。Kinesis Data Analytics(KDA)底层其实也是 Flink,但如果你自己有 Flink 集群,或者想用开源版本的功能(比如更灵活的 Window 操作、CEP 复杂事件处理、与 Flink CDC 的联动),直接在自建的 Flink 集群里加一个 Kinesis Source 和 Sink,反而是更可控、更灵活的做法。
我在实际项目中见过不少团队,最开始用 Lambda 去消费 Kinesis,业务逻辑复杂之后就撑不住了。比如你要做 5 分钟窗口的实时聚合,或者要关联多个流做 join,Lambda 的编程模型做这种事非常痛苦,加上冷启动和并发限制,吞吐一上来就各种超时。Flink 就没有这个问题,它有完善的 state 管理、checkpoint 机制、窗口计算,是干实时计算的正经工具。
1.2 方案选型:官方连接器与第三方连接器怎么取舍
现在往 Flink 里接 Kinesis,主流的做法是直接用官方维护的 flink-connector-kinesis。这个连接器在 Flink 1.x 各版本里都有对应的发行版,比如 Flink 1.17 就有对应的快照版本。少部分团队会用 KCL(Kinesis Client Library)原生库去写自定义 Source,但这样会和 Flink 的 checkpoint 机制脱节,一旦任务重启,offset 管理就得自己处理,非常麻烦。所以除非有特别诡异的需求,否则直接用官方连接器是性价比最高的选择。
选型时还要考虑一个细节:你需要的是 Kinesis Data Streams 还是 Kinesis Data Firehose。前者是原始数据流,Flink 可以直接作为消费者去读取;后者是把数据批量投递到 S3、Redshift 等目标的托管服务,不需要你写消费者逻辑。用 Firehose 的时候,Flink 一般是作为上游把数据写进 Firehose,再由 Firehose 负责投递。这两个服务的定位不同,设计管道前得先想清楚。
官方连接器还支持一个很关键的特性:消费模式可以选择 EFO(Enhanced Fan-Out)。普通模式下,一个 shard 的所有消费者共享 2MB/s 的读取带宽,如果你开了多个 Flink 任务同时消费同一个流,很容易互相抢带宽;EFO 模式给每个消费者开独立带宽,2MB/s 是消费者独享的,代价是每 shard 要额外收费。这个选型关系到成本和稳定性,不能拍脑袋决定。
1.3 架构图里的角色划分
一个典型的架构是这样的:Kinesis Data Streams 接收上游数据,比如埋点日志、订单事件、IoT 设备上报数据,Flink 集群运行实时计算任务,从 Kinesis 读取数据,做清洗、聚合、关联,然后把结果写到下游(或者直接写回另一个 Kinesis Stream,作为下一级数据管道的输入)。
为什么结果要写回 Kinesis Stream 而不是直接写数据库?因为解耦。下游可能还有别的消费者,比如实时大屏、告警系统、推荐引擎,都从同一个流里读取各自关心的数据。如果你把结果直接写数据库,下游想实时消费就只能去数据库里轮询,性能差且浪费。让 Flink 把结果作为事件写回 Kinesis,下游各自维护消费进度,是很标准的事件驱动架构。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心原理与关键机制解析
2.1 Kinesis Shard 与 Flink 并行度之间的关系
Kinesis 的消费能力上限由 shard 数量决定,而 Flink 任务的并行度决定了你有多少个子任务去消费这些 shard。两者之间不是随意分配的——Flink 的 Kinesis Source 会尝试让每个 Flink 子任务尽量均摊 shard,但不保证一一对应。
比如说你有 4 个 shard,Flink Source 并行度设成 4,那每个子任务消费 1 个 shard,这是最理想的情况。如果并行度设成 8,就有 4 个子任务实际上没有 shard 可消费,白白占用资源。如果并行度设成 2,一个子任务可能会消费 2 个 shard,这时候单个子任务的数据处理压力会翻倍。
实际操作中,我建议启动时先按 shard 数量来设置 Source 并行度,后续根据实际数据量和 CPU 使用率再微调。一个常见的错误是把 Flink 的并行度盲目调大,结果 Kinesis 读取带宽没变,只是白白增加了 task 数量和状态管理开销。
2.2 Sequence Number 与 Checkpoint 的配合
Kinesis 每条记录都有一个 sequence number,它是一个在 shard 内递增的字符串,用来标识记录在分片中的顺序。Flink 的 Kinesis Source 会周期性地把当前消费到的 sequence number 保存到 checkpoint 里。任务失败重启后,从最近一次 checkpoint 恢复,就能从当时的 position 继续消费。
这个机制保证了**精确一次(at-least-once 或 exactly-once)**的语义。Kinesis 连接器还支持将 sequence number 存到 DynamoDB 里(通过 Kinesis Analytics 或 KCL 的方式),但 Flink 官方连接器直接把状态存到 Flink 的 state backend,不需要额外的存储依赖,这一点比 KCL 原生方案简洁不少。
还有一个要特别注意的地方:Flink 的 checkpoint 不是想开就能开的。如果你的上下游没有配套的幂等处理机制,光靠 checkpoint 并不能保证端到端精确一次。比如你把结果写入一个没有幂等保证的数据库,任务重启后重复写入的数据可能产生脏数据。Kinesis Sink 本身是支持幂等写入的,但也需要你在 Sink 侧配置好 partition key,确保相同 key 的记录走同一个分片,减少重复带来的副作用。
2.3 Flink CDC 与 Kinesis 的关系
很多团队关注 Flink CDC,又想知道它跟 Kinesis 怎么配合。Flink CDC 解决的是数据库变更捕获的问题,比如监听 MySQL 的 binlog,把增删改操作实时同步出去。这里有两种常见做法:一种是把 CDC 的变更事件直接发到 Kinesis,让下游消费;另一种是用 Flink 消费 Kinesis 里的 binlog 事件,做实时数仓的入仓。
在云端架构里,Kinesis 往往承担的是“消息总线”的角色,CDC 产生的变更事件先落到 Kinesis,这个流对多个消费者可见,比如 Flink 做实时计算、另一个 Flink 任务做数据同步到数据湖、还有一个做监控告警。如果不用 Kinesis,你可能需要为每个消费者单独建一个 Source 去连数据库,等于把数据库的 binlog 暴露给多个系统,增加了数据库的负担,也放大了安全风险。通过 Kinesis 中转,数据库只跟一个生产者打交道,消费方各自从 Kinesis 拉数据,这是更健康的架构。
2.4 消费者位置重置与初始位置策略
Flink 的 Kinesis Source 支持配置初始消费位置,常见选项包括:TRIM_HORIZON(从最老的记录开始)、LATEST(从最新的记录开始)、AT_TIMESTAMP(从某个时间点开始)。这个配置决定了任务第一次启动时的消费起点。
比如你想跑一个历史数据回补任务,需要从 7 天前开始消费,那就用 AT_TIMESTAMP 指定时间;如果只是实时追数据,用 LATEST 就行;如果希望任务启动后把流里积压的数据全部处理完再进入实时状态,用 TRIM_HORIZON。
但要注意,Kinesis 的默认数据保留期只有 24 小时(可以扩大到 365 天),如果数据已经过期,TRIM_HORIZON 也读不到更老的数据。所以做数据回补前,先确认 Kinesis 里的保留期配置是不是满足需求,否则会白忙一场。
3. 实操配置与核心环节实现
3.1 环境准备:AWS 端需要什么
开始写代码之前,先确认 AWS 侧的东西配齐了。你需要一个 Kinesis Data Stream,以及一个有权限访问它的 IAM 用户或角色。IAM 权限是最容易踩坑的地方,权限不够,Flink 任务启动时会报各种 AccessDeniedException,排查起来很闹心。
建议给 Flink 任务用的 IAM 角色至少包含以下权限:
kinesis:DescribeStream:获取 stream 的元信息,shard 列表等kinesis:GetRecords:消费数据kinesis:GetShardIterator:获取分片迭代器kinesis:ListShards:列出分片kinesis:PutRecord/kinesis:PutRecords:写入数据(如果要写回 Kinesis)kinesis:SubscribeToShard:如果使用 EFO 模式
还要注意,Flink 任务运行在 EC2 上,EC2 的实例角色也要有相应的权限。如果你用的是 EMR 或 KDA,那就配置对应的服务角色。一句话:权限配好再动手,省得后面反复排查。
3.2 Maven 依赖引入与版本匹配
以 Flink 1.17 为例,在 pom.xml 里引入 Kinesis 连接器:
xml复制<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kinesis</artifactId>
<version>4.1.0-1.17</version>
</dependency>
注意版本号的命名规则:4.1.0 是连接器的版本,-1.17 是对应的 Flink 版本。不同 Flink 版本要用不同后缀的连接器,版本不匹配会出现类冲突或者方法找不到的错误。
另外还需要 AWS SDK 的依赖,不过一般会被连接器传递引入,不需要单独加。如果遇到了 NoClassDefFoundError 的 AWS 类问题,手动加上对应版本的 AWS SDK 依赖就行:
xml复制<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sts</artifactId>
<version>1.12.500</version>
</dependency>
3.3 核心代码:Kinesis Source 配置
下面是一个常见的 Source 配置片段,我加了一些注释说明关键参数的作用:
java复制import org.apache.flink.streaming.connectors.kinesis.FlinkKinesisConsumer;
import org.apache.flink.streaming.connectors.kinesis.config.ConsumerConfigConstants;
Properties kinesisProps = new Properties();
// 指定区域,比如 us-east-1
kinesisProps.setProperty(ConsumerConfigConstants.AWS_REGION, "us-east-1");
// 消费起点:TRIM_HORIZON 从最老开始,LATEST 从最新开始
kinesisProps.setProperty(ConsumerConfigConstants.STREAM_INITIAL_POSITION, "LATEST");
// Flink 任务名称,在 Kinesis 侧的消费者监控里可以区分
kinesisProps.setProperty(ConsumerConfigConstants.STREAM_CONSUMER_ARN, "");
// 设置 checkpoint 间隔相关参数
kinesisProps.setProperty(ConsumerConfigConstants.SHARD_GETRECORDS_INTERVAL_MILLIS, "1000");
DataStream<String> kinesisStream = env.addSource(
new FlinkKinesisConsumer<>(
"your-stream-name",
new SimpleStringSchema(),
kinesisProps
)
);
SHARD_GETRECORDS_INTERVAL_MILLIS 是每次拉取记录后的冷却时间。Kinesis 每个 shard 每秒钟最多调用 GetRecords 5 次,超过会被限流。默认值是 200ms,如果你的数据量不大,可以调大到 1000ms,减少不必要的 API 调用,也降低费用。
再强调一次,这个 Source 是 一个有状态的算子,它会消费一个 shard 并在 Flink 的 state 中记录每个 shard 的消费位置。所以如果你的 checkpoint 关闭了,Flink 重启后会从你配置的初始位置重新消费,这会导致重复处理或者遗漏数据。一定要开启 checkpoint,这是所有流处理任务的基本要求。
3.4 核心代码:Kinesis Sink 配置与 Partition Key
Kinesis Sink 比 Source 稍显复杂,因为要指定数据分区策略。Kinesis 的 partition key 决定了数据进入哪个 shard,如果你不关心数据顺序,可以用随机 key;如果希望相同业务 ID 的数据进入同一个 shard,就要用业务键作为 partition key。
java复制import org.apache.flink.streaming.connectors.kinesis.FlinkKinesisProducer;
import org.apache.flink.streaming.connectors.kinesis.config.ProducerConfigConstants;
Properties producerProps = new Properties();
producerProps.setProperty(ConsumerConfigConstants.AWS_REGION, "us-east-1");
FlinkKinesisProducer<String> producer = new FlinkKinesisProducer<>(
new SimpleStringSchema(),
producerProps
);
producer.setDefaultStream("output-stream-name");
producer.setDefaultPartitionKey("fixed-partition-key");
// 如果数据量大,可以开启批量写入
producer.setFailOnError(true);
DataStream<String> resultStream = ...;
resultStream.addSink(producer);
注意 setDefaultPartitionKey 是指定了一个固定的 key,所有记录都会进同一个 shard。如果要按业务字段区分,你需要实现自定义的 KinesisPartitioner。比如:
java复制producer.setCustomPartitioner(new KinesisPartitioner<String>() {
@Override
public String getPartitionId(String element) {
// 假设 element 是 JSON,里面有个 userId 字段
JSONObject obj = JSONObject.parseObject(element);
return obj.getString("userId");
}
});
分区器选得好不好,直接影响下游消费的均衡性。如果你的 partition key 太稀疏,比如只有几个固定值,会导致数据集中在少数几个 shard 上,其他 shard 多数时间是空闲的,吞吐上不去还浪费钱。
3.5 完整演示:消费-处理-写回
这里我写一个可运行的例子,数据源是 Kinesis 里的 JSON 订单消息,Flink 消费后用 1 分钟窗口统计订单金额,结果写回另一个 Kinesis Stream。
java复制import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.connector.kinesis.sink.KinesisStreamsSink;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import java.time.Duration;
public class KinesisOrderAggregator {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(60_000);
Properties consumerProps = new Properties();
consumerProps.setProperty(ConsumerConfigConstants.AWS_REGION, "us-east-1");
consumerProps.setProperty(ConsumerConfigConstants.STREAM_INITIAL_POSITION, "LATEST");
// 1. 读取 Kinesis 原始流
DataStream<String> source = env.addSource(
new FlinkKinesisConsumer<>("raw-order-stream", new SimpleStringSchema(), consumerProps)
);
// 2. 解析 JSON 并提取金额、时间戳
DataStream<OrderEvent> orders = source
.map(new OrderParserFunction())
.returns(TypeInformation.of(OrderEvent.class));
// 3. 事件时间 + 水印,注意这里需要从 JSON 里提取业务时间
DataStream<OrderEvent> withWatermark = orders.assignTimestampsAndWatermarks(
WatermarkStrategy
.<OrderEvent>forBoundedOutOfOrderness(Duration.ofSeconds(10))
.withTimestampAssigner((event, timestamp) -> event.getEventTime())
);
// 4. 窗口聚合
DataStream<String> result = withWatermark
.keyBy(OrderEvent::getOrderType)
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.aggregate(new OrderAggregateFunction())
.map(AggregateResult::toJsonString);
// 5. 将聚合结果写回 Kinesis,供下游使用
Properties sinkProps = new Properties();
sinkProps.setProperty(ConsumerConfigConstants.AWS_REGION, "us-east-1");
KinesisStreamsSink<String> sink = KinesisStreamsSink.<String>builder()
.setStreamName("aggregated-order-stream")
.setRegion("us-east-1")
.setMaxBatchSize(500)
.setMaxBatchSizeInBytes(5 * 1024 * 1024)
.setMaxTimeInBuffer(300)
.setSerializationSchema(new SimpleStringSchema())
.setPartitionKeyGenerator(element -> element.hashCode() % 16 + "")
.build();
result.sinkTo(sink);
env.execute("kinesis-order-agg");
}
}
这段代码有几个地方值得展开说:
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10))表示允许 10 秒乱序,超过这个范围的迟到数据会被丢弃(除非你配置 allowedLateness)。- 窗口用的是事件时间(Event Time),不是处理时间。如果直接用处理时间,数据延迟和乱序会导致统计结果失真,在生产环境里事件时间是必须的。
- Sink 的
setMaxTimeInBuffer(300)是指数据在缓冲里最多放 300 毫秒,到达就批量发送。这个参数需要根据业务实时性要求调整,调太大延迟高,调太小批量效果差,API 调用次数多。
3.6 运行与提交:如何把任务跑起来
任务写完,本地可以直接在 IDE 里跑。如果要在集群上跑,常见的方式是打成 jar 包,然后用 flink run 命令提交:
bash复制flink run -m yarn-cluster -p 4 -c com.example.KinesisOrderAggregator \
-d /path/to/kinesis-order-agg.jar
如果你用的是 Flink standalone 集群,就省掉 -m 参数。提交之前确保 Kinesis 的 region、stream 名称、权限都对。还有一个容易被忽略的地方:Flink 所在的 EC2 需要能够访问 Kinesis 服务的 endpoint,如果你的集群在私有子网里,要走 VPC endpoint 或者 NAT 网关,否则连接会超时。
日志方面,建议把 Flink 的 log4j 级别调到 INFO,里面会输出 Kinesis Source 的 shard 分配情况,方便确认任务是否按照预期消费了所有 shard。
3.7 配置参数速查与选型建议
我把常用配置整理成一张表,方便查阅:
| 配置项 | 作用 | 推荐值/说明 |
|---|---|---|
AWS_REGION |
指定 Kinesis 所在区域 | 必须设置 |
STREAM_INITIAL_POSITION |
初始消费位置 | LATEST / TRIM_HORIZON / AT_TIMESTAMP |
SHARD_GETRECORDS_INTERVAL_MILLIS |
单 shard 拉取冷却时间 | 建议 1000ms,避免限流 |
SHARD_GETRECORDS_MAX |
单次拉取最大记录数 | 默认 10000,不建议调大 |
SHARD_ITERATOR_TYPE |
迭代器类型 | 默认 AT_SEQUENCE_NUMBER |
StreamConsumerARN |
EFO 模式下的消费者 ARN | 使用 EFO 时填写 |
CHECKPOINT_INTERVAL |
checkpoint 间隔 | 建议 60s 或更短 |
老版本的连接器里有些配置名是 kinesis.* 开头的,新版本统一到 ConsumerConfigConstants 里。如果你是从老的 Flink 1.9 项目升级上来,记得检查这些配置名的变化。
4. 常见问题与排查技巧实录
4.1 权限相关报错:AccessDenied 与 AssumeRole 失败
这是最常见的一类问题。Flink 任务启动后,日志里出现 AccessDeniedException 或者 User is not authorized to perform: kinesis:DescribeStream,基本可以确定是 IAM 权限没配好。
排查思路:先登录 AWS 控制台,找到运行 Flink 任务的 EC2 实例,确认它的实例角色;然后打开 IAM 控制台,确认该角色的权限策略包含上面对应的 Kinesis 权限。如果任务在 EMR 上跑,还要确认 EMR 的服务角色(Service Role)和实例角色(EC2 Role)都有权限。这里有个容易混的点:EMR 集群创建时指定的角色,和单个 EC2 实例自身的角色是两回事,两边的权限都需要看。
如果你用了跨账户的 Kinesis Stream,还要检查资源策略(Resource Policy),确认运行 Flink 的账户被允许访问目标 stream。
4.2 消费延迟高:shard 数量与并行度不匹配
消费延迟高的原因分几种:数据量超过 shard 总吞吐、Flink 并行度不足、单条记录处理逻辑太慢。
先看监控。AWS 控制台里 Kinesis 的 GetRecords Bytes 指标可以看消费速度,Flink 的 UI 里可以看到每个 Source 子任务的 currentFetchEventTimeLag。如果 GetRecords Bytes 长期接近 2MB/s 的上限,说明单个 shard 的读取带宽已经打满,需要扩容 shard。如果 shard 的 CPU 使用率不高但任务延迟还是很大,看一下 Sink 是不是瓶颈,比如写数据库时连接数不够。
另外,一个很隐蔽的坑:如果你给 Source 设置的并行度大于每个子任务实际消费的 shard 数量,这些“空转”的子任务也会占用 Flink 的 slot,导致下游算子可用的 slot 变少。资源规划时,Source 并行度应该尽量贴近 shard 数,不要随意调大。
4.3 EFO 模式下消费者 ARN 的配置
EFO 模式需要先注册消费者。在 AWS 控制台的 Kinesis Stream 页面里,选择 “Registered consumers” 注册一个消费者,会得到一个 ARN。然后在 Flink 的配置里填入这个 ARN。
常见错误是消费者 ARN 填成了 stream ARN,或者填了另一个团队的消费者 ARN。还有,EFO 模式下每个 shard 的订阅带宽是 2MB/s,如果同一 shard 有多个注册消费者订阅,每个消费者依然是独立的 2MB/s,但 Kinesis 会按消费者数量收费。所以 EFO 只适合延迟敏感、消费端数量少的场景,否则费用会很可观。
4.4 Flink 的 JDBC 连接器异常与 Sink 侧问题
热词里有“flink的jdbc连接器异常”,这里顺带说下。很多任务从 Kinesis 消费数据后,会通过 JDBC 连接器写入数据库,这时候容易爆出两类异常:
一类是连接池耗尽,报 Connection is not available, request timed out。原因是默认的 JDBC 连接器基于每个并行子任务建立连接池,如果并行度很高,数据库连接数会成倍上涨。解决办法:调大数据库的最大连接数,或者减小 Flink Sink 并行度,再或者把写入方式改成批量异步写。
另一类是主键冲突或者数据类型转换异常,报 BatchUpdateException。这类问题本质不是连接器问题,而是数据质量问题。建议在写入数据库前,先做数据清洗和类型校验,保证字段类型和长度匹配,不要指望数据库来做类型宽容。
4.5 任务提交失败:datasophon 中 Flink 不能上传 job 的问题
有团队用的是 Datasophon 管理 Flink 集群,任务提交时遇到“不能上传 job”的问题。这个通常和 Datasophon 的文件上传路径权限有关。Flink 的 Web UI 上传 jar 包时,会写到临时目录,如果该目录没有写权限,或者磁盘满了,就会报错。
排查办法:先看 Flink 的 web.tmpdir 配置指向哪里,确认该目录存在且有写权限。如果磁盘空间不足,清理日志文件或者加大磁盘。还有一个可能的原因是 Datasophon 管理的 Flink 集群版本和客户端版本不一致,导致上传 jar 后解析异常,这种情况建议统一 Flink 版本,不要用客户端 1.16 连接服务端 1.17。
4.6 序列化与数据格式问题
Kinesis 连接器默认使用的 SimpleStringSchema 只适合纯文本数据。如果数据是 Avro 或者 Protobuf 编码,你需要自定义反序列化器。一个常见的坑是,数据里有脏字符导致 JSON 解析失败,Flink 任务会一直重启。应对办法:在解析函数里加 try-catch,把解析失败的数据单独放到侧输出流(side output),方便排查,而不是让整个任务挂掉。
示例:
java复制OutputTag<String> badDataTag = new OutputTag<String>("bad-data") {};
SingleOutputStreamOperator<OrderEvent> parsed = source.process(new ProcessFunction<String, OrderEvent>() {
@Override
public void processElement(String value, Context ctx, Collector<OrderEvent> out) throws Exception {
try {
out.collect(JSON.parseObject(value, OrderEvent.class));
} catch (Exception e) {
ctx.output(badDataTag, value);
}
}
});
DataStream<String> badData = parsed.getSideOutput(badDataTag);
badData.addSink(new BadDataSink());
这样即使有脏数据,任务也不会中断,同时你能看到具体哪些数据有问题,比埋头查日志快得多。
4.7 问题排查清单
我整理了 A 到 Z 的排查顺序,遇到问题可以按这个走:
- 先看 Flink UI 的 Job 状态,确认任务是否还在 Running。
- 看 Source 子任务的
currentFetchEventTimeLag,确认消费有没有积压。 - 看 AWS CloudWatch 里 Kinesis 的
GetRecords.Bytes是否打满。 - 检查 IAM 权限,确认所有 Kinesis 操作都有授权。
- 检查网络,确认 EC2 能访问 Kinesis endpoint。
- 看日志里有没有
ShardIteratorExpiredException或ProvisionedThroughputExceededException。 - 如果有异常,先看异常堆栈的 Caused by,一层层往里翻,通常真正的错误原因藏在最深处。
5. 成本优化与实战技巧
5.1 控制 Kinesis 的 API 调用成本
Kinesis 是按 API 调用次数收费的,读和写都是。每 shard 每小时大概收取一定的费用,PutRecords 每 1000 条算一次调用。如果你用 Flink 写入数据,API 调用次数基本由 PutRecords 的批大小决定。批大小越大,调用次数越少,费用越低。
所以 Sink 侧要尽量让数据在缓冲里多攒一会儿再发,比如 setMaxTimeInBuffer(500) 或者 setMaxBatchSize(1000)。但要注意,这会增加数据延迟。数据量大时这个延迟影响不大,数据量小时就要权衡了。
5.2 合理规划 shard 数量
shard 数量直接决定月账单,因为每个 shard 都有固定费用,还要加上数据读写的 API 费用。规划 shard 的公式很简单:Shard 数 = 峰值写入吞吐 / 1MB/s,和峰值读取吞吐 / 2MB/s,取两者中的较大值。
如果峰值写入是 5MB/s,读取是 10MB/s,你需要至少 5 个 shard。但实际的业务峰值往往不是稳定的,建议留出 20-30% 的余量,防止瞬时流量打满后触发限流。我还见过一种做法:平时用小 shard 数量,遇到大促或者数据回灌时动态扩容,AWS 支持调高 shard 数,成本也随用随付。
5.3 使用 Flink 的侧输出流分离异常数据
侧输出流的用法在上面已经有了,这是 Flink 里被低估的一个功能。除了异常数据,你还可以用它来分离监控日志、慢日志、重试记录等。侧输出流可以接到独立的 Sink,方便独立存储和分析,不占主流程的资源。
有人会问,我直接 filter 掉不行吗?filter 会丢掉数据,后期想分析就没有了。侧输出流是“分流而不丢弃”,主流程照常处理好数据,异常数据进侧流单独处理,算是可观测性的一个基础能力。
5.4 监控与告警配置建议
对于实时任务,建议至少监控以下几个指标:
- Flink 的 checkpoint 失败率:连续失败意味着状态不一致风险。
- Source 的
currentFetchEventTimeLag:超过阈值(比如 1 分钟)要告警。 - Kinesis 的
GetRecords.Bytes:如果长期接近 2MB/s 上限,考虑扩容。 - Sink 的写入成功率:失败率超过 0.1% 就要排查。
CloudWatch 可以设置基于 Kinesis 指标的告警,Flink 侧可以用 Prometheus + Grafana 监控 JM 导出的指标。一套完整的告警体系能帮你提前发现问题,而不是等业务方反馈。
6. 关于实时处理场景的延展思考
6.1 Kinesis 与 Kafka Connector 的对照
如果你熟悉 Kafka,接触 Kinesis 时会有一种既视感,但二者在概念和配置上有不小的差异。把两者的关键对应关系列出来,能帮助迁移团队快速上手:
| 概念 | Kafka | Kinesis |
|---|---|---|
| 数据分片 | Partition | Shard |
| 消费者进度 | Offset | Sequence Number |
| 消费者组 | Consumer Group | 消费者(共享带宽或 EFO) |
| 数据保留 | 可配置 | 默认 24 小时,可延长 |
| 副本机制 | 多副本,可配置 | 托管,自动冗余 |
相比 Kafka,Kinesis 最大的优点是省运维。最大的缺点是:如果你要跨地域消费数据,或者需要多 Schema 的 Topic 结构,Kinesis 用起来不如 Kafka 顺手。做过混合云或者多云的团队应该会有同感。
6.2 从 Lambda 迁移到 Flink 的判断标准
很多团队为了快速上线,用 Lambda 消费 Kinesis,后面发现扛不住了才考虑 Flink。什么样的场景应该尽早用 Flink?
一是状态化计算。比如实时去重、实时累加、窗口聚合,Kinesis + Lambda 做这些事等于用查 DynamoDB 或者 Redis 来做状态管理,延迟高且并发受限。Flink 把状态放在本地,速度快一个量级。
二是流与流的 join。数据要关联多个 Kinesis 流,Lambda 的模型做这种关联非常别扭,而 Flink 天生支持双流 join。
三是复杂的窗口语义。比如会话窗口(Session Window),超时自动关闭,Lambda 自己写这种逻辑等于要维护一套定时状态机,非常痛苦。
如果你的业务只是做简单的转换、过滤、转发,Lambda 完全够用,成本也更低。但一旦计算变得复杂,建议尽快往 Flink 迁移,越晚迁移成本越高。
6.3 实时数仓中的多层 Kinesis 管道
有一种常见的实时数仓架构,数据流会被切分成多层:
- ODS 层:原始数据直接进入 Kinesis。
- DWD 层:Flink 做清洗、去重、维度补全,结果写回另一个 Kinesis。
- DWS 层:Flink 做汇总聚合,结果写回 Kinesis 或者直接入 ClickHouse、HBase。
- ADS 层:下游服务直接从 DWS 层的 Kinesis 里拉取数据,做实时推荐、大屏展示等。
每层通过 Kinesis 解耦,层与层之间的数据流转由 Flink 任务负责。这种架构的优势在于每一层都可以独立扩容、独立消费、独立定义数据模型,灵活性和可维护性都很高。缺点是链路长,端到端延迟可能达到几十秒,不适合延迟要求在秒级以下的场景。
6.4 端到端精确一次的实现要点
前面提到过,Kinesis Sink 本身支持至少一次的写入语义,实现精确一次还需要上下游配合。如果你把数据写回 Kinesis,并且下游 Kafka、Kinesis 之类支持事务的存储,可以通过 Flink 的 TwoPhaseCommitSinkFunction 来实现端到端的精确一次。但这需要 sink 侧支持事务,不是所有存储都能做到。
更实际的做法是:接受 at-least-once,并把业务设计成幂等。比如写数据库时用主键 upsert,写 Redis 时用 SETNX 或者天然覆盖,这样即使重复消费也不会产生重复数据。从工程角度看,幂等方案比追求精确一次的事务方案更容易实现、更容易排查,代价是写业务代码时要多考虑一层。
我在实际项目中见过一个团队,为了精确一次花了好几周在排查事务边界问题,最后因为下游存储不支持事务而放弃,改成幂等方案,两天就解决了。设计实时管道时,先把业务逻辑的幂等性做好,再去考虑事务机制,顺序千万不要搞反。
6.5 后续扩展:从 Kinesis 到数据湖
Kinesis 的数据不会只在流里转,最终还是要落到数据湖或者数仓。常见的做法是:Kinesis Data Firehose 把数据直接投递到 S3,或者 Flink 把处理后的结果批量写入 Iceberg / Hudi / Delta Lake 表。
从 Flink 写入数据湖的玩法这几年已经很成熟,Flink + Iceberg + S3 + Kinesis 的组合基本是实时湖仓的标配。Kinesis 负责实时的数据接入,Flink 负责实时计算和写入,Iceberg 负责存储和元数据管理,S3 提供海量低成本存储。如果你现在还在用批处理跑 T+1 报表,可以考虑往这个方向演进,把延迟从一天降到分钟级。
个人经验是,不要一开始就把整个链路都搭起来,从一条业务线先跑通,验证稳定性和成本,再逐步扩展到其他业务。毕竟实时链路比批处理链路复杂不少,涉及网络、权限、重试、监控、费用等多方面,一口吃成胖子容易踩坑。
最后再分享一个小技巧:Kinesis 消费端的状态恢复依赖 checkpoint,但如果你改了代码逻辑,想重新消费一段历史数据,而又不想影响正在运行的任务,可以复制该 stream 的备份或者对 Kinesis 开启 RetentionPeriodHours 延长保留期,另起一个 Flink 任务从指定时间点消费,跑完后再合并结果。这个方法比停机重启更稳,适合做数据订正和双跑验证。
