前言:

最近一直在做datax的增量更新,算是对datax有了一点新的认识吧。

因为公司需要以greenplum为核心来搭建自己的数仓平台,以满足业务上的多维快速查询(以前多维查询用到是kylin,但随着数据量的增加,kylin越来越难以满足我们的需求了)。

然而,greenplum的数据导入方面并不是很友好,通常,需要使用copy或者是gpfdist才能快速的往GP里面导入数据。

我们试了kettle来往GP导,但速度非常慢,原因是kettle导数据进入GP需要经过GP的master,是一条一条insert进去的。试了datax,同样是速度奇慢。

所以我们采用了别人开发好的datax版本,https://github.com/HashDataInc/DataX ,接下来我对datax增量更新所做的,都是在该版本的基础上来实现的。

 

datax简介

datax是阿里开源的一个etl工具,支持多种异构数据源。当然,datax适合用于离线数据的同步,不适合实时同步。具体的介绍,网上有不少博客,这里就不多介绍了

 

datax增量更新

思路:

第一种思路:datax的job是以一个json文件来描述的,本身提供了where条件,支持简单的增量更新

第二种思路:一旦我们的job中,增量抽取的数据比较复杂,比如,本身sql中需要多表关联或者有多个子查询,此时where条件已经无法满足。这种情况就是我接下来要讲的。

 

首先,为什么说如果抽取数据的sql比较复杂,where条件就无法满足

以mysqlreader举例,datax支持普通配置和用户自定义配置两种

普通配置需要用户配置Table、Column、Where的信息,而用户自定义配置则比较简单粗暴,只需要配置querySql信息,两者的模版如下:

普通配置:

{
    "job": {
        "content": [
            {
                "reader": {
                    "name": "mysqlreader", 
                    "parameter": {
                        "column": [], 
                        "connection": [
                            {
                                "jdbcUrl": [], 
                                "table": []
                            }
                        ], 
                        "password": "", 
                        "username": "", 
                        "where": ""
                    }
                }, 
                "writer": {
                    "name": "hdfswriter", 
                    "parameter": {
                        "column": [], 
                        "compress": "", 
                        "defaultFS": "", 
                        "fieldDelimiter": "", 
                        "fileName": "", 
                        "fileType": "", 
                        "path": "", 
                        "writeMode": ""
                    }
                }
            }
        ], 
        "setting": {
            "speed": {
                "channel": ""
            }
        }
    }
}

  自定义配置:

{
    "job": {
        "content": [
            {
                "reader": {
                    "name": "mysqlreader", 
                    "parameter": {
                        "connection": [
                            {
                                "jdbcUrl": [], 
                                "querySql": []
                            }
                        ], 
                        "password": "", 
                        "username": ""
                    }
                }, 
                "writer": {
                    "name": "hdfswriter", 
                    "parameter": {
                        "column": [], 
                        "compress": "", 
                        "defaultFS": "", 
                        "fieldDelimiter": "", 
                        "fileName": "", 
                        "fileType": "", 
                        "path": "", 
                        "writeMode": ""
                    }
                }
            }
        ], 
        "setting": {
            "speed": {
                "channel": ""
            }
        }
    }
}

 

而且,阿里官方githup上有这样的描述:

对于用户配置Table、Column、Where的信息,MysqlReader将其拼接为SQL语句发送到Mysql数据库;对于用户配置querySql信息,MysqlReader直接将其发送到Mysql数据库。

当用户配置querySql时,MysqlReader直接忽略table、column、where条件的配置,
querySql优先级大于table、column、where选项。

也就是说,querySql适用于复杂情况下,配置更灵活。

所以,一般的增量更新,我们都会采取querySql来进行

 

第二个问题:我怎么让datax知道一个job是需要增量的还是全量的

很遗憾,datax本身不适合用于做增量,所以,正常的描述一个job的json中,我们没有办法让datax知道job是不是增量的;

那么,唯一的比较省力的解决办法,是在datax的基础上做二次开发

just like this:

{
  "info": {
    "increment_sql": [
      "SELECT ifnull(max(id),0) from datax_m_rec_consume"
    ],
    "is_increment": "1"
  },
  "job": {
    "content": [
      {
        "reader": {
          "name": "sqlserverreader",
          "parameter": {
            "connection": [
              {
                "jdbcUrl": ["jdbc:sqlserver://******:1433;DatabaseName=sjtb"],
                "querySql": ["SELECT * from m_rec_consume where id > ${last_max}"]
              }
            ],
            "password": "*******",
            "username": "sa"
          }
        },
        "writer": {
          "name": "mysqlwriter",
          "parameter": {
            "username": "*****",
            "password": "****************",
            "column": [
              "*"
            ],
            "connection": [
              {
                "jdbcUrl": "jdbc:mysql://*********:3306/datacenter_dev?characterEncoding=utf8",
                "table": ["datax_m_rec_consume"]
              }
            ]
          }
        }
      }
    ],
    "setting": {
      "speed": {
        "channel": "1"
      }
    }
  }
}

  如上,为每一个job添加一个配置项info,代表一个job的信息

在info项中,配置了2项信息,一种是表示这个job是否是增量的is_increment,1表示是增量,0 表示全量。当然,datax本身是没有该项的,所以,这个配置项是由我们的java代码来读取解析的。

当我们配置了增量的话,我们可以拿到该增量sql,在该任务执行之前,通过用户配置的writer插件的连接信息,执行该sql,将对应的值替换掉querySql中的${last_max},并将新的json内容写出到json文件中。

这样,在调用datax.py脚本时,就可以执行到具体的job了。

如果我们设置了全量,那么就直接由datax执行该json任务即可

 

第三个问题:怎么才能利用起datax自身提供的日志

在改造完增量更新之后,突然意识到,如果用脚本将datax自身的脚本与我们的程序进行封装,我们很难拿到datax自身提供的详细日志。

so,修改datax.py脚本吧。

 

我们需要在datax执行任务之前去调用我们的java代码,以便实现增量与全量的判断,以及增量更新的动态修改json文件中的占位符

那么,就添加一个方法,在该方法中,发送请求调用java程序即可,返回json文件的真实路径(如果是增量的任务,我们会将json内容写出到一个临时json文件中,datax实际执行的是该临时文件的内容)

def findRealPath(path):
    #import pdb;pdb.set_trace()
    #data = {'fileName': args[0], 'flag': args[1]}
    data = {'path': path}
    data_urlencode = urllib.urlencode(data)
    requrl = "http://localhost:7777/job/runJob2"
    #if '0' == args[1]:
     #   requrl = "http://localhost:7777/job/saveJob"
    #elif '0' != args[1] and '1' != args[1]:
     #   sys.exit(RET_STATE['FAIL'])

    req = urllib2.Request(url=requrl, data=data_urlencode)
    #print req

    res_data = urllib2.urlopen(req)
    res = res_data.read()
    resdict = json.loads(res)
    #print type(resdict)
    file_real_path =str(resdict['data'])
    if 500 == resdict['code']:
        sys.exit(RET_STATE['FAIL'])
    elif 200== resdict['code']:
        print file_real_path
        #sys.exit(RET_STATE['OK'])
    print res
    return file_real_path

 

修改main方法:

if __name__ == "__main__":
    printCopyright()
    parser = getOptionParser()
    options, args = parser.parse_args(sys.argv[1:])
    if options.reader is not None and options.writer is not None:
        generateJobConfigTemplate(options.reader,options.writer)
        sys.exit(RET_STATE['OK'])
    if len(args) != 1:
        parser.print_help()
        sys.exit(RET_STATE['FAIL'])

    real_path =  os.path.abspath(args[0])
    file_real_path =findRealPath(real_path)
    arglist = []
    arglist.append(file_real_path)
    #startCommand = buildStartCommand(options, args)
    startCommand = buildStartCommand(options, arglist)
    #print startCommand

    child_process = subprocess.Popen(startCommand, shell=True)
    register_signal()
    (stdout, stderr) = child_process.communicate()

    sys.exit(child_process.returncode)

在main方法中调用我们自己写的findRealPath方法,获取到要执行的json文件的真实路径后,交由datax执行。

 

这里有个bug,在main方法中,为了适配绝对路径和相对路径,我用了

real_path =  os.path.abspath(args[0])
去获取用户传入的json文件的绝对路径,将这个绝对路径传入我们的java代码中,然而,用jenkins去调度时发现 os.path.abspath(args[0]) 这个方法实际找到的绝对路径是不对的,
它是以jenkins程序所在的路径为基准,来转变相对路径的。所以这个还是要改一下。

具体bug如下:
我配置了:
python /home/hashdatax/datax/bin/datax.py ../job/user_defined_job/test_mysql_to_sqlserver.json

实际获取到的路径是:
java.io.IOException: /root/.jenkins/workspace/job/user_defined_job/test_mysql_to_sqlserver.json (没有那个文件或目录)

 

posted on 2018-11-25 23:05  tianyafu  阅读(7891)  评论(0编辑  收藏  举报