该企业的数据传输平台主要由三部分构成:
1) 一部分是基于Quartz和Docker的调度系统。主要功能是负责调度和运行ETL任务。监视和变换当前任务实例的状态,当实例状态发生变化或者符合变化条件时主动更新数据库和内存中实例状态。
2) 一部分是数据交换工具Wormhole。主要用来多个数据库之间数据的交换,是ETL平台的基础。这个工具我们一般把它集成到Docker镜像中,同时我们还集成了各种脚本任务和Java运行环境的镜像,具体的选择取决于任务的类型。
3) 最后是基于Web的图形化工具。主要用来帮助用户手工配置任务、监控任务运行状态和日志查看进行调试。对任务和实例进行管理。管理操作包括挂起,预跑,置为成功,修改,杀死正在运行的实例等。
图 4.1 ETL传输平台架构
如上图(图4.1)所示,用户通过Web界面来创建和管理任务,把数据存储到数据库中,而Quartz调度系统的定时任务会把状态为新建的任务读出,初始化为实例,接着检查上游实例和并发状况,并申请资源,接下来就是根据负载均衡选择执行机,远程启动执行机上的容器,并把容器和实例映射关系存入数据库中。执行机上Docker守护进程收到启动命令后,还需要从远程仓库中根据任务类型下载镜像,如果是传输任务就下载Wormhole镜像,并设置启动命令为Wormhole启动命令(其他任务类型类似)。容器运行后,有定时任务会向每台执行机查询容器列表,并逐一检查容器的状态,更新实例和容器在数据库中的状态。这样,整个ETL传输平台就正常运转了起来。
图 4.2 Kepler架构图
调度系统Kepler机制的关键就是调度机上的三个定时任务(如图4.2):Init任务、Ready任务和Running任务。
1) Init定时任务的运行频率是10分钟一次,用途是从数据库中获得所有有效任务检查下一次触发时间是否在,根据任务新建它的实例,并把这些实例保存到实例表中和内存Init队列中。
2) Ready定时任务运行频率是1分钟一次,负责把Init队列中的任务实例,检查运行条件(如前置任务的实例是否运行完成,是否有同时并发运行的实例等)是否满足,然后申请一些共享资源,主要是Hive、Mysql等并发资源,这一切成功后就把实例的状态改为Ready并把它从Init队列移到Ready队列中,并在数据库中更改实例状态为Ready。
3) Running进程的频率是30秒一次,主要如任务是定时通过Zookeeper查询在线的机器以免出现分配新的任务到已下线的机器上,以及选择被占Slot(槽位,对应一个Docker容器用的标准资源量)数最少的机器为任务运行的目标机器。然后根据任务类型确定镜像名称,利用Docker Client API远程创建和启动Docker容器。
另外,调度系统的三个比较重要的组件:资源管理器,负责统计所有任务依赖的共享资源,保证它们的总和不达到上限;状态管理器,维护这各种状态的实例列表;容器调度器,维护当前运行的容器列表和存活的执行机列表。
下面介绍一下Wormhole的架构,这里有两张图:
图 4.3 Wormhole架构图
图 4.4 Wormhole的Splitter组件
图4.3所示的是整个Wormhole的架构,但Splitter的作用不是很明显。图4.4说明了Splitter的作用。可以看出传输任务和数据源的关系是一对一,而与输出目的地的关系是一对多。
Wormhole可以在各种数据存储类型之间高速交换数据。整体上是Framework + Plugin架构[15],Framework提供了以行为单位的数据流缓冲机制,分片和多线程读、写机制,读写分离的数据交换队列等高性能数据传输技术。框架为读写、分片、预处理等插件都提供了通用的接口,针对每个数据类型的插件实现类负责处理具体的连接和读写等规则。一个数据传输任务对应一个进程,全部数据传输都在内存中进行。
框架为读写、分片、预处理等插件都提供了通用的接口,这些接口分Reader和Writer两类,如果你需要开发面向某种类型数据源的插件,只需实现这些接口即可。比如数据源是Oracle,数据传输目标是Mysql,那么对应要开发的读写插件就是出 OracleReader和MysqlWriter插件,分片是OracleReaderSplitter和MysqlWriterSplitter。预处理是OracleReaderPeriphery和MysqlWriterPeriphery。把这些类实现相关接口即可,就算是加入了框架中。。
在生产实践中,每个数据输入类型除了要开发Reader和Writer,考虑到传输过程的性能和复杂性,还要为每个Reader和Writer开发一个Splitter和Periphery来做分段读、写和读写预处理工作。
Wormhole的数据交换主要使用了两种技术:读写双缓冲队列与线程池。下面详细介绍。线程池是一种复用线程对象、减少创建线程花销的技术手段[16]。现在线程池技术应用广泛。无论是主流的Web服务器TCP连接、数据库访问连接、文件、邮件之类的连接都有使用。这主要是因为两点:第一,这些服务器访问都有一个共同的特点:访问量大、频率高但每次连接的时间短。第二,线程池相对其他多线程技术也有如下的优点:1)线程数可以预先设定,并在实际使用中控制在预定数的一定范围内。这样就能有效控制创建多个线程带来的内存消耗,同时也减轻了JVM在垃圾回收上的压力。2)复用预先创建或已经存在的线程,提高了资源的利用效率。多个访问连接复用线程,这就大大节省了线程对象创建的时间,并且节省了系统资源,防止资源浪费[17]。3)提高系统响应速度。现在有资料表明,现代服务器在短时间内处理大量访问请求会创建大量线程,线程的创建和销毁时间会成为系统性能的瓶颈。因此复用线程对象能够降低服务器访问的延迟。
在互联网经典TCP协议服务器的请求处理逻辑中,监听TCP连接、数据发送和接收等事件的是主线程,而具体数据的收发则由Handler线程处理。于是就需要一个队列在主线程和各个Handler线程之间交换数据,类似于经典的生产者-消费者模式。这个队列读写都需要加锁,在实际处理过程中实际并发性能并不特别好,如果我们要提高并发性能就要用到双端缓冲队列。
双端缓冲队列是读写分离的两个队列,发送数据的线程把数据插入写队列,而读取数据的线程则从读队列中读取数据[18]。如果读数据时读队列为空且写队列不为空,则交换两个队列,否则阻塞等待。这个过程中有两把锁发挥作用:一是写锁。写锁用于写线程把数据插入写队列时以及读队列和写队列进行交换时。不过队列交换时,读线程必须具有读写两把锁,否则会死锁。二是读锁。读锁只用于读线程从读队列中获取数据时。最后还要读写缓冲队列长度的问题,队列短时,能够保证数据交换的及时性,但如果太短,队列交换频繁会降低并发性能。所以一般数据量大时队列长一些,反之则短一些。另外,双端缓冲队列还有两种实现策略使用于不同场景:
1) 读优先。数据消费者发现读队列为空时尝试交换读写队列。这种情况适合读比写速度慢的情况下。
2) 写优先。数据生产者发现写队列满时尝试交换读写队列。这种情况适合写比读慢的情况下。
开发前台是MVC(模型-视图-控制器)的架构。用户在视图层次配置管理任务,控制层负责处理业务逻辑和转发请求,模型层(数据访问层)进行持久化管理。考虑到系统的灵活性,我们并没把控制层和数据访问层放在同一服务器集群上访问,而是进行的分离,中间采用了该企业的服务框架中间件,进行远程RPC访问。下面我们来看看Galaxy和Galaxy-Halley的后端架构图(如图4.5)。
图 4.5 Galaxy和Galaxy-Halley后端架构图
可以看出,用户在Web上配置和管理自己的任务发起Http请求,请求从Nginx服务器通过负载均衡交替选择服务器,由于没有Session Sticky,所以必须在Web前端存储用户登录信息(内部系统,并不存在安全问题)。Galaxy服务器在处理完逻辑后,就需要向Galaxy-Halley数据库服务发起远程RPC调用,获取或者修改实例信息。
图 4.6 任务代码或文件上线架构图
下面我们重点讨论任务代码上线模块。模块的架构如上图(如图4.6)。
用户有些文件必须从本地上传,有些代码则是项目的代码,可以从Github上拉取项目打包结果,发布到执行机上。整个发布流程大概是:用户把文件上传到Galaxy Server或者Galaxy Server拉取Github的打包结果成功后,再把文件上传到Hdfs。利用Zookeeper实现的、个数与执行机个数相当的分布式消息队列发布消息给执行机,运行在执行机上的消息消费程序接收到消息后,立刻到Hdfs上的指定地址下载代码到该任务在执行机上的预订目录,无论下载成功还是失败,立刻往数据库中插入一条消费状态信息。Galaxy通过间隔一段时间轮询数据库来获取整个发布消费过程中各执行机的消费状况,一旦有机器消费失败或者时间超时,则返回失败。
本章主要介绍了平台的整体架构设计和三个模块(调度系统、数据交换工具、开发前台)的架构设计,为下一章的具体实现打好基础。