前言:
最近一直在做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 (没有那个文件或目录)