Apache NiFi 的架构设计特点

265 阅读8分钟

NiFi 介绍

Apache NiFi 是一个强大的数据集成工具,专门设计用于自动化和管理数据流的生成、转移和处理。可以非常方便的集成不同的数据源,并在可视化界面配置数据流,最后定义数据的导出方式。

image.png

上图工作流的作用是在一个数据库执行sql,并把结果导出到另一个数据库中。

功能特点

  1. 上手成本低: 可视化界面操作,文档丰富。
  2. 功能强大: 组件丰富(处理器超200种),例如包含了 mysql,excel,restApi,kafka 等的接入和导出。
  3. 监控完善,消息可以溯源,可以展示每个处理器的处理结果。
  4. 支持分布式部署,快速的进行横向拓展。

NiFi 的数据流组成部分

在讲述之前要先介绍一下 NiFi 数据流的几个重要组成部分。

image.png

上面配置的处理器组件就是 Processor,DataIngressProcessor(数据导入) 和 DataIngressProcessor(数据导出) 都是一种 Processor,命名参考官网文档。

Processor 之间是通过一个 Connection 的队列来存储数据,这个是一定要有的,Processor 不能直接把数据丢给下一个处理器。这种设计使得 Processor 不用再关心他下游的处理速度,也不用担心下游的处理结果。

一个 Processor 一般会有两个出口端口(Relationship),一个是处理成功的结果,一个是处理失败的结果,所以关联处理器的时候是需要选择哪一个 Relationship,这提升了系统的健壮性。例如当处理出现异常的时候可以配置一个告警的处理节点即可。

Processor 处理的消息单位是 FlowFile,DataIngressProcessor 会不断生成 FlowFile,并把 FlowFile 投递到 Connection 中。下一个处理节点会从 Connection 拿到 FlowFile 进行处理。

NiFi 的非功能性设计

作为一个数据集成系统,NiFi 对拓展性,可靠性,性能都有对应的设计。

自定义组件与类加载机制

NiFi 是一种微内核架构,拓展性是不可或缺的。NiFi 会把组件定义成一个个 nar 包,nar 包就像是 tomcat 的 war包一样,里面会有 META-INFO 配置,依赖的 jar 包。在启动项目的时候会解压 nar 包,然后再进行动态加载。

image.png

和 tomcat 一样, NiFi 也实现了自定义 classLoader 来隔离不同组件的。但 NiFi 和 tomcat 不同的点在于,tomcat 加载的是 web 服务,一个 war 包一般只会有一个实例,而且依赖的 jar 包多,不同项目的 jar 包关联度大。因此 tomcat 里面每一个 war 一个实例,对应有一个 webappLoader,而且会有 sharedLoader 用于共享 jar 包。

而 NiFi 的组件一般不会太大,依赖不会很多,而且组件和组件之间依赖的 jar 包差异也会比较大,因此一个组件会有一个 classLoader。组件可能还会动态加载别的 lib 包,例如一个连接 mysql 的组件,会根据配置去依赖不同版本的 mysql-connector-java,因此每一个组件的实例也需要一个 classLoader 去进行加载。NarClassLoader 是负责共享组件的 jar 包,如果不希望共享,甚至可以每一个实例的 InstanceClassLoader 分别加载组件的 jar 包。但会带来内存的增加。

image.png

事务

为了保证在处理中途发生异常不会导致脏数据且能进行回滚重试,NiFi 实现了简单的事务。每次消息处理开启一个事务。因为有了 Connection 缓存消息,且 Connection 的消费不存在冲突。因此这里的事务不用考虑资源冲突需要加锁的问题。处理器处理过程中产生或者修改的记录,都会先把日志记录下来,在 commit 前调用 checkpoint 是为了把所有的修改内容固定下来,然后一次 commit 把数据全部提交。

image.png

image.png

故障恢复 - FlowFile 的持久化

NiFi 支持故障恢复不丢数据,这也是系统可靠性的一个保证。NiFi 使用 FlowFileRepository 对 Connection 的FlowFile 进行持久化。要对每个 FlowFile 都进行持久化对性能的要求就会非常高。为了提升性能,FlowFile 的持久化采用 Write-Log-Ahead 的策略,也就是先记录 FlowFile 的修改日志,再定时生成最新的 FlowFile 快照。这样可以把随机写入变成顺序写入,提升写入性能。

Mysql 其实也是采用了这个策略,在执行更新操作的时候,会先写入 redo log,更新内存buffer,定时再把数据更新到 b+ 树中。这就要求 FlowFile 是不可变的,任何对 FlowFile 的修改都会产生一个新的 FlowFile,新的 FlowFile 会记录原始的 FlowFile 和修改的内容。

image.png

NiFi 内部把修改日志称为 journal,定时和 snapshot 合并产生新的 snapshot。

这种持久化方式也让事务的提交变得简单,FlowFile 可以实时落库,事务提交只需要记录日志文件的 offset 即可。

内存优化

有了 Connection 做缓存也意味着可能会有大量的 FlowFile 需要存下来。放硬盘会导致性能下降,放内存又会占用大量内存,容易导致 oom。因此 FlowFile 做了2个设计解决这个问题,一个是实现了内存交换,允许 FlowFile 存在内存中,超过一定数量则会持久化到硬盘,在需要加载的时候再交换到内存中,虽然降低了消费速度,但控制了内存的增长。另一个设计则是元数据和内容分离。

FlowFile 除了 Content 之外还会有 Attribute。FlowFileRepository 负责持久化 FlowFile,ContentRepository 负责持久化 Content。Content 默认会持久化,不放内存。在 Connection 里面的 FlowFile 是通过指针引用 Content。这种设计一方面减少了 Connection 的内存消耗,提升了 FlowFile 的流动速度,一方面也可以针对 Content 的特点进行设计提升性能。要用好这种方式前提是不要在每个处理器都对 Content 进行处理。因此官方推荐尽可能在靠近消息源头的位置先把后面可能要用的上的内容先更新到 Attribute 中,后面只需要对 Attribute 进行处理即可,不用反复读 Content。

image.png

Content 特点是占用空间大,为了提升写入性能,NiFi 先对上层业务做了层抽象。ContentClaim 就是 FlowFile 指向实际文件内容的指针。ResourceClaim 就是面向物理存储的单位,例如一个文件。如果一个FlowFile 对应一个文件势必导致生成大量文件,因此多个 Content Claim 关联一个 ResourceClaim,并且记录在 ResourceClaim 的位置和大小。

不同的文件可能会放到不同的硬盘中,提升写入和读取性能。

如果只是对 FlowFile 修改,虽然是新的 FlowFile,但实际指向的 Content 其实是不会复制的,所以会有多个 FlowFile 引入同一个 ContentClaim 的情况,这样也导致需要通过引用计数的方式记录 Content 的引用情况,定时删除没有 FlowFile 引用的 Content。

image.png

事件的持久化

前面提到 NiFi 的数据可以溯源,是通过对事件的持久化实现的。NiFi 会记录每个处理器处理的消息,更新的属性。还能看到 Content (通过 ContentRepository 获取,如果 Content 不再被 Connection 引用可能会删除)。事件的持久化是通过 Provenance Content 实现的。事件都有时效性,一般关心的都是最近处理的数据,因此 ProvenanceRepository 会滚动更新,覆盖历史事件。

事件还会自动导入 Lucence,便于进行搜索。

image.png

微小批处理模拟流处理

虽然从界面看上像是流式处理,但实际上并不是。每个 Processor 需要配置定时任务,默认是 1ns,相当于不断循环执行了。为了避免影响性能,还需要配置 Penality Duration 和 Yield Duration。当 Connection 没数据的时候,会触发等待 Yield Duration。默认10s。当处理过程出现异常的时候会等待 Penalty Duration。

要提升实时性感觉只要把触发条件改成事件驱动即可(Connection 为空则 wait)。应该不是难事。

image.png

image.png

总结

从中可以看到 NiFi 的设计还是挺多亮点的,包括 Connection 的设计,FlowFile 的不可变,Attribute 和 Content 的分离,针对不同数据的持久化策略等等。和类似的产品对比,更能显示 NiFi 的特点。

Connection 是我认为最重要的设计,事务,故障恢复,FlowFile 设计几乎都和它有关。另一款类似的产品 Kettle,功能相似,没有 Connection,上游处理完再丢给下游,因此无法做到故障恢复,消息缓存,只适合一次性的离线操作。又例如 flink,它没有 Connection,导致节点之间无法解耦,但节点有状态,需要做故障恢复,因此需要 StreamBarrier 来进行 checkpoint,相当于把整个数据流当成一个节点来处理。也因为没有 Connection,容易造成生产和消费速率不匹配降低吞吐率。不是说别的系统设计有缺陷,而是面向的场景不同,设计也会有所不同。

NiFi 的 Connection 决定了它支持大 Content 的处理,对于复杂的流程更容易定位问题,可以定时触发数据流而不用担心出现系统故障。可以先快速把数据加载进来,再慢慢消费数据。

从 NiFi 的设计可以看出,NiFi 很适合定时触发数据流,对不同数据源数据进行简单的提取,修改,路由,转发。并且组件丰富,比较稳定,不丢数据,方便定位问题。但不适合实时性要求较高,计算复杂,组件有状态,架构简单,性能要求很高(要频繁写库对性能有影响),资源占用低的场景。