生产化 Flink 作业后需要注意的 7 件事
生产化 Flink 作业后需要注意的 7 件事
将工作投入生产只是第一步
尽管许多公司都在努力启用他们的实时数据,但越来越多的公司实际上正在运行生产、流式工作负载。当我与客户一起运行他们的工作负载时,我意识到通过正确的准备可以避免某些问题。
我提倡尽可能使用流媒体。在 Flink 上开始使用流媒体变得越来越容易——你可以使用 FlinkSQL 或类似的 SaaS 平台 维维里卡 .即使开始很简单,事情也会变得复杂。
当事件时间处理面对现实
事件时间处理是一个引人入胜的概念——在事件中嵌入时间可以实现查看数据的新方法。事件何时加载到系统并不重要——您总是可以将它与正确的时间窗口相关联。机器停机一个小时?没问题!只需重新启动它并推送事件 - 结果最终将是正确的。
好的,这就是理论。如果你从技术角度来看——这并不简单。您应该非常了解
例如,默认时间窗口触发器将为每个延迟事件触发窗口。想象一下,有一个每秒处理数百万个事件的管道,其中一个生产者被卡住了一个小时。您的系统将充斥着通常计算起来非常昂贵的延迟数据。
您可以通过实现自定义窗口触发器来解决这个问题——尽管这需要编写一些自定义代码。因此,您不再编写纯 SQL 作业。
另一个可能被忽略的常见延迟数据问题是窗口状态缺少 TTL。如果您允许延迟 3 天,则在该时间段内创建的所有时间窗口的状态将被保留。之所以如此,是因为处理延迟数据的默认策略是合并窗口(并且需要此工作状态)。
在处理时间时,您还应该注意空闲分区、无序间隔的后果——我们通常认为是理所当然的事情。空闲分区实际上与后期数据处理密切相关。在这种情况下,只有一个明智的策略——撞水印。这再次可能导致数据延迟,以防事件应该在那里,但它们只是卡在上游生产者端的某个地方。
序列化
对于任何分布式框架,您都可以处理序列化。子任务必须交换数据、处理数据并最终将其推送到下游系统。
首先,当 POJO/Avro 序列化失败时,Flink 默认的 fallback 是使用 Kryo。您应该知道,这可能会降低一个数量级的效率。您最安全的选择是禁用 pipeline.auto-type-registration .如果 Flink 回退到 Kryo,它会在运行时抱怨。这并不理想,但在许多情况下比默默地体验 10 倍的性能更可取。
其次,由于序列化开销,您在进行实际上应该提升它的更改时可能会遇到性能开销。考虑在接收器之前重新洗牌您的数据,以减少与外部系统的连接数量。作为一个具体的例子,如果您使用分桶将数据转储到外部文件系统,每个 流式文件接收器 子任务维护一个桶列表以及它们写入事件的文件。对于随机打乱的数据,这可能导致每个子任务写入每个分区的情况。为了避免这种情况,您通常会在 Flink 端重新洗牌数据。
现在想象一下数据已经被洗牌了。介绍一个 关键依据 应该不会有太大变化吧?答案是否定的,因为 Flink 会对每个事件进行序列化和反序列化,即使交换发生在同一个 JVM 中!
准备好您的 JVM 分析工具
在时间压力下调试工作很累。为了增加成功的机会,请做好准备——获取 async-profiler、VisualVM、jemalloc、jeprof 和任何其他您喜欢的工具。确保您已经准备好运行书籍以准备连接到有问题的任务管理器并调用相关的线程/堆转储。
Flink 带有出色的 UI 和指标,但您仍然应该利用您的 JVM 经验!
- 获取堆转储并使用 MAT 对其进行分析以快速发现内存泄漏,
- 继续使用 async-profiler 绘制火焰图以更快地分析线程转储(Flink 提供了自己的火焰图,但它们需要特殊的配置标志,并且一旦 JM 关闭,共享它们就更加困难)。
同样,您可能希望建立一个强大的日志基础设施来收集日志(如 EFK 或 Loki / Promtail / Grafana)。当涉及到流式作业时,添加强大的日志记录是一门科学,它们通常不会经常重新启动,并且由于通过系统的数据量很大,它们不能真正产生太多日志。
注意状态
如果您不小心,状态可能会变得笨拙。请记住,默认情况下,Flink 会无限期地保持状态。尽管 FlinkSQL 向您隐藏了许多状态复杂性,但这些作业与常规流作业一样容易受到状态问题的影响。我已经提到过延迟数据和状态可能出现的问题。尽管如此,任何与会话相关的密钥都将永远保留,如果 空闲状态保持时间 未设置。
DataStream API 在状态方面提供了更大的灵活性,但也有更多的陷阱。你必须自己实际控制它,没有什么能阻止你随意使用这些抽象。过去,我曾经编写过很多基于计时器的代码,这些代码会在此类工作中为我清除状态。如果你不知道,Flink 支持 状态 TTL .我发现这个功能非常有用,它极大地清理了代码。
其中一项自由是在 ListState 和 ReducingState 之间进行选择。没有经验的 Flink 实践者经常过度使用 ListState——例如,理解时间窗口的最简单的心智模型是将它们视为抽象,将所有事件收集到一个列表中,然后可以用来推导出最终结果。 ReducingState——每个时间窗口只维护一个记录,一个累积的结果——似乎是一个聪明的选择。它通常是。除非您使用 RocksDB 并处理小列表。向 RocksDB ListState 添加元素非常便宜——毕竟它不必反序列化 state 中的现有记录。而 ReducingState 的情况则不同——值被序列化保存在堆上,无论何时你想更新它,都必须进行反序列化、计算和序列化。这可能是一个重要的性能瓶颈。
使用大型状态
大状态也可能成为一个问题,它增加了检查点的创建开销。此外,由于检查点,Flink 中的状态会定期保存,但不会在 TM 上复制。每当 TM 崩溃时,您都需要从外部存储中恢复状态。对于 TB 级的状态,这可能需要大量时间。
有时,更好的选择是将所有可能的内容卸载到外部数据库。 Aerospike、Redis 等通常在我参与的项目中运行良好。当然,如果延迟很关键,您将不得不尽可能多地投入 TM 中。
使用 RocksDB 处理大型状态的一个有用技巧是使用本地 SSD。它们比云提供商默认安装的默认网络 HDD 驱动器快几个数量级。尽管 RocksDB 将一些数据保存在内存中,但如果您仔细分析这项工作,您会发现磁盘访问的重要性。
将仓库与流式作业分离
将数据从流式作业直接推送到仓库听起来很诱人,但实际上我发现使用 S3 等中间存储来解耦两个系统会更加健壮。如果您的仓库(例如 Snowflake)出现任何问题,流式传输作业将不可避免地失败。通过 SQS 通知将 Snowpipe 从 S3 设置为 Snowflake 非常简单。
相同的经验法则可以应用于与外部系统的任何集成。如果您发现某些事情可能会失败,可能会在很长一段时间内无法使用,请尝试找到一种解耦的方法。我在这里的观点可能有点偏颇,因为我曾处理过大规模、状态繁重的流媒体工作,这些工作重启起来很痛苦。
注意数据偏差!
Flink 通过其 UI 提供了每个子任务处理的事件数量的清晰可视化。您可能经常意识到某些子任务比其他子任务负担更重。这至少有两个原因:
- Flink 针对将分配给少量工作人员(由任务并行性控制)的大量键进行了优化,
- 默认的子任务分配算法分布不均。
我不会详细介绍,但您可以关注 代码 自己。首先,Flink 使用 key 的 hash code 和 Murmur hash function 对 key 进行编码。它还利用了两者的价值 并行性 和 最大并行度 (默认为 1.5 * 并行度 ) 来计算运算符索引。我认为现实中唯一相关的事情是您应该明确指定这两个值并确保 最大并行度 是的倍数 并行性 以获得均匀分布。
其次,对于少量的键(特别是如果它们是连续的整数),Murmur 哈希函数的模并行分布也会出现偏差。当我想重新分区数据时,我通常会遇到这个用例,以便每个接收器子任务对应于下游系统中的单个分区(无论是 Kafka 主题还是某个分布式数据库)。在这种情况下,通过分区索引对数据进行重新分区是很方便的,希望负载能在 Flink 中均匀分布。情况并非如此,我知道的唯一解决方法是了解 Flink 哈希方案并应用少量逆向工程——而不是将预期的子任务索引作为键传递(例如 0, 1, 2, 3...
) 我重新映射每个子任务索引 一世
为整数 编号
以便 murmur(idx) % 子任务数 == i
.如果我为 Flink 提供这样的密钥,我确信每个事件最终都会出现在正确的子任务上(使用风险自负)。
结论
部署作业是一回事,确保它可以很好地扩展、正确且对故障具有鲁棒性是另一回事。不要误会我的意思,Flink 是一款很棒的产品,我非常喜欢使用它。它教会了我很多关于 JVM 世界的知识,并使我整体上成为了一名更好的工程师。对于我所做的任何未开发的实时项目,它也是我选择的框架。
我希望您发现这些技巧中的任何一个都对您有所帮助,并且这最终将使您使用的系统变得更好一点。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明