1.背景
由于业务原因,工作中一直使用的是专有云,且目前不支持OpenAPI,因此作业只能使用手工创建,这在有大量表对象需要创建作业时造成了明显的时间和人力的耗费。
针对这种问题,首先想到了编写资源,再通过一个Pyodps作业调用资源处理批量的表对象。但是必须解决两个问题:1.各个子实例需要实现独立并行;2.需要不影响子实例的运维。即某个子实例报错需要反馈出来且不影响其他子实例的正常运行。
2.思路
总体思路:通过一个Pyodps作业,并行地去调起表操作实例。
1.由于Pyodps提供了两种执行SQL语句的方法,execute_sql与run_sql,前者会阻塞调起SQL实例,而后者是不会阻塞的,可实现并行,这就为第一个问题提供了解决方案。值的注意的是run_sql这个方法默认的是只能执行一句SQL(即一个分号前的语句),如果想执行多个SQL过程,需要加一个设置参数"odps.sql.submit.mode":"script",即
o.run_sql(sql,hints={"odps.sql.submit.mode":"script"})
2.为了保证同一个Pyodps作业可以对所有子实例起到运维监测的作用,就需要在作业里有两个全局列表变量,一个用来记录下每一个子实例以及其所操作的对象名称(一般是表名),另一个用来记录失败的实例表名。即在所有run_sql实例都提交云上并行处理后,遍历这个实例列表,对每个子实例进行状态的追踪,如果子实例运行结束且它运行失败了(成功不做处理),那么就将这个实例所操作的对象名记录在失败的清单里,然后增加偏移量,继续追踪下一个子实例。
每个实例即instance,都有status.name属性显示是否运行完成(运行完成不代表成功或失败),'TERMINATED'表示instance完成。检验是否成功则调用instance的is_successful方法,若instance.is_successful()返回True即代表成功,False则代表失败。
3.在遍历完所有子实例(即子实例全部'TERMINATED')后,检查失败清单,如果失败清单为空则成功结束。如果失败清单不为空,则打印出失败清单,并且抛出错误(这样此作业会报错,可以通过运维中心查看日志定位失败清单)。
3.示例
简单起见,这里举一个表批量同步落地的示例,将其他空间的表同步到本空间(表名与表结构不变)。
1.编写资源(也可以直接放在作业里)
创建名为utils_data_sync.py的py资源,加入以下代码并提交。
# -*- coding: utf-8 -*-
#批量数据落地
def exec_merge_sql_batch (o,sourceproject,sourcetable):
#检查源空间是否存在此表
if not o.exist_table(sourcetable, sourceproject):
return None
#获得源表
table = o.get_table(sourcetable, sourceproject)
source_columns = ''
#获得源表字段以拼接插表语句
for col in table.schema.columns:
source_columns += ' ,' + col.name + '\n'
#检查是否存在插入目标表,若不存在则创建
if not o.exist_table(sourcetable):
#若对建表的结构有处理需求也可以单起一个建表方法,返回建表语句,也可用gen_create_table_sql
#但是需要注意,如果是执行建表语句应该使用execute_sql保证阻塞
o.create_table(sourcetable,table.schema,if_not_exists=True)
print('\n========建表sql========\n' + sourcetable)
#插表语句的最终形成
dm_sql = '''
insert overwrite table %s
select %s from %s
''' %(sourcetable,source_columns[5:],sourceproject + '.' + sourcetable)
instance=o.run_sql(dm_sql)
print('\n========合并sql========\n' + sourcetable)
#注意,一定要返回实例(元组绑定实例和表名)
return (instance,sourcetable)
2.Pyodps作业
# -*- coding: utf-8 -*-
##@resource_reference{"utils_data_sync.py"}
import sys
import os
import time
sys.path.append(os.path.dirname(os.path.abspath('utils_data_sync.py'))) #将资源引入工作空间
reload(sys)
sys.setdefaultencoding('utf8')
import utils_data_sync as uds
#批量表清单(自行填写)
tables=['TABLE_A',
'TABLE_B',
'TABLE_C']
# 数据源空间名(自行填写)
sourceproject = 'XXXXXXX'
# 实例容器
instances=[]
fail_table_list=[]
all_cnt=0
complete_cnt=0
#批量调起并行拉表实例
for table in tables:
ret=uds.exec_merge_sql_batch(o,sourceproject,table)
if not ret:
fail_table_list.append(table)
else:
instances.append(ret)
all_cnt+=1
#实例状态追踪
while complete_cnt<all_cnt:
if instances[complete_cnt][0].status.name=='TERMINATED':
if not instances[complete_cnt][0].is_successful():
fail_table_list.append(instances[complete_cnt][1])
complete_cnt+=1
else:
time.sleep(10)
print('已完成实例---总实例')
print('{:.0f}---{:.0f}'.format(complete_cnt,all_cnt))
try:
if not fail_table_list:
print('========全部实例运行成功========')
else:
print('========存在实例出现异常========')
print(fail_table_list)
raise Exception("请查看打印出的异常实例!")
except RuntimeError as e:
print(repr(e))
3.结果展示
将上述Pyodps作业上调度,然后如果自然调度报错,可在运维空间查看,如下:
(有需要的也可使用logging和traceback等做记录)
4.总结
这都是在没有OpenAPI情况下的权宜之计。。。
5.更新
其实可以跟进一步地打印出报错的实例log,即把错误实例直接放进报错列表,抛错时通过实例的get_logview_address方法打印log地址备查。这样就不需要单独再检查报错实例的问题了。
#实例状态追踪
while complete_cnt<all_cnt:
if instances[complete_cnt][0].status.name=='TERMINATED':
if not instances[complete_cnt][0].is_successful():
fail_table_list.append(instances[complete_cnt])
complete_cnt+=1
else:
time.sleep(10)
print('已完成实例---总实例')
print('{:.0f}---{:.0f}'.format(complete_cnt,all_cnt))
try:
if not fail_table_list:
print('========全部实例运行成功========')
else:
print('========存在实例出现异常========')
for instance in fail_table_list:
print('异常表'+instance[1])
if instance[0]=='数源无此数据':print('异常log:数源无此数据')
#如果是专有云,那么打印log需根据空间的实际网络地址替换
else:print('异常log:'+instance[0].get_logview_address().replace('http://logview.odps.aliyun.com','专有云地址'))
raise Exception("请查看打印出的异常实例!")
except RuntimeError as e:
print(repr(e))