kettle5.3实现元数据注入的转换以集群模式运行

1. 背景

基于kettle进行二次开发时,想做一些比较通用的kettle作业,何为通用,例如要将A表数据迁移到B表,C表的数据迁移到D表,常规就是在spoon建两个作业(表输入->表输出),如果做到通用,那么就是建一个作业(表输入->表输出),然后在运行作业的时候把一些配置参数(数据库连接、源表名、目标表名、字段映射等)作为变量的形式传入,在作业运行的过程中利用元数据注入组件(MetaInject)把这些配置注入到表输入和表输出组件中。

之所以可以通过注入的方式把元数据注入到表输入和表输出步骤中,是因为这些步骤实现了StepMetaInjectionInterface接口:

This interface allows an external program to inject metadata using a standard flat set of metadata attributes.

在kettle5.3中大概有30+个步骤实现了该接口

2. 问题描述

2.1 集群环境

在spoon中创建一个kettle集群,名称cluster1,集群有3个子服务器,全部运行在本机中:

名称 主机 端口 是否主服务器
slave1 localhost 7086
master localhost 7088
slave2 localhost 7089

2.2 kettle作业

整个kettle作业由1个job以及2个trans组成,以下截图只是为了辅助详解,并不是为了说明作业的具体实现,所以不涉及相关的配置。

1.作业 (表->表)

kettle-1

在这个job中只是简单接收传入来的变量,然后就传递给转换(元数据注入到《表->表》)中,该转换也配置了集群运行

2.转换 (元数据注入到《表->表》)

kettle-2

这个tran就是上面的job所关联的转换,它从job中接收到变量后,然后传递给ETL元数据注入步骤,这个步骤关联了表->表转换,会把变量注入到转换

3.转换 (表->表)

kettle-3

这两个步骤的元数据都是通过上一个转换的(元数据注入到《表->表》)步骤注入的,包括:数据库连接,SQL,目标表,字段映射等。可以看到表输出被设置了集群,即上cluster1,该步骤将会被运行于slave1,slave2子服务器上,所以显示Cx2字样。

2.3 运行作业

运行以上作业,选择本地执行并把变量填写好(根据实际的作业配置),最后发现作业成功运行并结束时,3个carte服务器的控制台并没有收到任何请求,而期望是表输出步骤是可以跑在集群的。从下图可以看出,控制台的最后输出信息正是3个carte服务器刚启动时的信息,如果有接收到请求的请会有相应的日志打印:

kettle-5

由此,可以证明了是因为元数据注入步骤影响到表输出步骤不能运行于集群

3. 问题分析

既然定位到了依赖元数据注入转换不能运行集群,那么直接分析ETL元数据注入步骤的源码,看看它是如何工作的,对应的类为MetaInject,从processRow方法入口,代码有点多,不过定位到关键处即可:

//5.3版本的源码中大概在290行
//injectTrans是被注入元数据的转换,即我们上面提及到的表->表转换
//前面一堆代码都是在解析并把元数据注入到该转换中
injectTrans.startThreads();  //调用Trans类的startThreads()方法

从该行代码看到,把元数据注入到转换后,就调用Trans类的startThreads()方法启动转换,选择本地执行时也会进入到该方法中,而这个方法是没有任何对集群处理的逻辑的,只是单纯地把转换的所有步骤放到各自的线程中并启动,所以即使在表输入中配置了集群cluster1,但在这里并不生效。

4. 解决问题

思路:预期的效果是,如果(表->表)转换中有某些步骤配置了集群运行,那么它会被以集群方式运行,所以在MetaInject.processRow()的方法中要进行判断。

根据思路,首先要解决两个问题:
1. 如何判断转换是否有步骤配置了集群
2. 如果将转换运行于集群上

其实在kettle中都有封装对以上问题的方法,关键在于能否找得到而已,如果对kettle源码比较了解的话应该就不是问题了,刚好自己属于不太属性的一类。

SpoonTransformationDelegate.executeTransformation()

直接运行一个转换时会进入该方法,在约810行处可以看到以下的代码

 if ( transMeta.findFirstUsedClusterSchema() != null ) {  //判断转换里的是否配置了集群
        executionConfiguration.setExecutingLocally( false );
        executionConfiguration.setExecutingRemotely( false ); 
        executionConfiguration.setExecutingClustered( true );  //如果有就设置以集群方式运行
      }

可以看到它是根据转换里的步骤来判断是否以集群方式运行转换,如果某步骤配置了集群,那么转换将会被以集群运行

JobEntryTrans.execute()

当运行一个作业,这个作业有一个转换的作业实体,而这个转换实体配置了集群模式下运行,那么这个转换实体所关联的转换将会运行于集群。会进入代码约848行的判断分支:

if ( clustering ) {

//一大堆初始化以及集群运行的逻辑,而这里的代码将会被参考到元数据注入组件中

}

结合上面两个方法的代码就可以解决以上两个问题,接下来参考两个方法的代码来修改MetaInject的源码,在290行injectTrans.startThreads()处先判断转换是否有步骤配置了集群,如果有,那么以集群方式运行(这个逻辑要自己实现,参考JobEntryTrans.execute()的实现);如果没,则保留原运行方式,修改后的代码 :

/* 扩展元数据注入的转换可以集群运行--开始代码 */
            ClusterSchema cluster = injectTrans.getTransMeta().findFirstUsedClusterSchema();
            if (cluster != null) {
                TransExecutionConfiguration executionConfiguration = new TransExecutionConfiguration();
                executionConfiguration.setClusterPosting(true);
                executionConfiguration.setClusterPreparing(true);
                executionConfiguration.setClusterStarting(true);
                executionConfiguration.setClusterShowingTransformation(false);
                executionConfiguration.setSafeModeEnabled(false);
                executionConfiguration.setRepository(repository);
                executionConfiguration.setLogLevel(LogLevel.BASIC);
                // executionConfiguration.setPreviousResult( r );
                executionConfiguration.setVariables(injectTrans.getTransMeta());
                // executionConfiguration.setArgumentStrings( args );
                TransSplitter transSplitter = null;
                long errors = 0;
                TransMeta transMeta = injectTrans.getTransMeta();
                Job pJob = getTrans().getParentJob();
                try {
                    transSplitter = Trans.executeClustered(transMeta, executionConfiguration);

                    // Monitor the running transformations, wait until they are
                    // done.
                    // Also kill them all if anything goes bad
                    // Also clean up afterwards...
                    //
                    errors += Trans.monitorClusteredTransformation(log, transSplitter, pJob);

                } catch (Exception e) {
                    logError("Error during clustered execution. Cleaning up clustered execution.", e);
                    // In case something goes wrong, make sure to clean up
                    // afterwards!
                    //
                    errors++;
                    if (transSplitter != null) {
                        Trans.cleanupCluster(log, transSplitter);
                    } else {
                        // Try to clean anyway...
                        //
                        SlaveServer master = null;
                        for (StepMeta stepMeta : transMeta.getSteps()) {
                            if (stepMeta.isClustered()) {
                                for (SlaveServer slaveServer : stepMeta.getClusterSchema().getSlaveServers()) {
                                    if (slaveServer.isMaster()) {
                                        master = slaveServer;
                                        break;
                                    }
                                }
                            }
                        }
                        if (master != null) {
                            try {
                                master.deAllocateServerSockets(transMeta.getName(), null);
                            } catch (Exception e1) {
                                e1.printStackTrace();
                            }
                        }

                    }
                }
                if (transSplitter != null) {
                    Result result = Trans.getClusteredTransformationResult(log, transSplitter, pJob, false);
                    setLinesInput(result.getNrLinesInput());
                    setLinesOutput(result.getNrLinesOutput());
                    setLinesRead(result.getNrLinesRead());
                    setLinesWritten(result.getNrLinesWritten());
                    setLinesUpdated(result.getNrLinesUpdated());
                    setLinesRejected(result.getNrLinesRejected());
                    setErrors(result.getNrErrors() + errors);
                }
            } else {/*扩展元数据注入的转换可以集群运行--结束代码 */

                injectTrans.startThreads();

                if (data.streaming) {
                    // Deplete all the rows from the parent transformation into
                    // the modified transformation
                    //
                    RowSet rowSet = findInputRowSet(data.streamingSourceStepname);
                    if (rowSet == null) {
                        throw new KettleException("Unable to find step '" + data.streamingSourceStepname + "' to stream data from");
                    }
                    Object[] row = getRowFrom(rowSet);
                    while (row != null && !isStopped()) {
                        rowProducer.putRow(rowSet.getRowMeta(), row);
                        row = getRowFrom(rowSet);
                    }
                    rowProducer.finished();
                }

                // Wait until the child transformation finished processing...
                //
                while (!injectTrans.isFinished() && !injectTrans.isStopped() && !isStopped()) {
                    copyResult(injectTrans);

                    // Wait a little bit.
                    try {
                        Thread.sleep(50);
                    } catch (Exception e) {
                        // Ignore errors
                    }
                }
                // copyResult( injectTrans );
                copyMoreResult(injectTrans);
            }
        }

5. 重新运行

修改了代码后,重新运行2.2的kettle作业,最后在3个carte服务器的控制台都看到了有接收到任务请求

kettle-4

观察上图,表输入运行于master,表输出步骤刚好运行于slave1和slave2子服务器上,一个处理了4条记录,另一个处理了5条记录,而测试表是9条记录,运行的结果正确,初步可以确定了修改的源码生效,至于其它的步骤还需要测试一下。

猜你喜欢

转载自blog.csdn.net/czmacd/article/details/53212573