简介:
Alibaba Otter和Canal是抽取mysqlbinlog的工具,可以在不修改任何业务程序的情况下,以极小的网络IO代价抽取业务数据库的数据到离线分析数据库,在网络状况良好的情况下,可以实现延时小于1s的近实时数据同步。但是他仅仅支持同步到mysql,oracle数据库,实际离线分析中,往往需要数据导入到Kafka,HBase,Cassandra,GreenPlum,ElasticSearch等数据库中,公司有这个需求,因而花了几个礼拜扩展开发了Otter,主要步骤如下:
1、修改Manager端,使管理界面支持nosql DB的数据源和表,字段映射的配置;
1.1 数据库类型扩展定义:
com.alibaba.otter.shared.common.model.config.data.DataMediaType
public enum DataMediaType {
GREENPLUM,
/** mysql DB */
MYSQL,
/** oracle DB */
ORACLE,
/** elsaticsearch */
ELASTICSEARCH,
/** KAFKA DB */
KAFKA,
/** HBASE DB */
HBASE,
/** CASSANDRA DB */
CASSANDRA,
/** HDFS-ARVO DB */
HDFS_ARVO,
/** cobar */
COBAR,
/** tddl */
TDDL,
/** cache */
MEMCACHE,
/** mq */
MQ,
/** napoli */
NAPOLI,
/** diamond push for us */
DIAMOND_PUSH;
public boolean isKafka() {
return this.equals(DataMediaType.KAFKA);
}
public boolean isElasticSearch() {
return this.equals(DataMediaType.ELASTICSEARCH);
}
public boolean isHBase() {
return this.equals(DataMediaType.HBASE);
}
public boolean isCassandra() {
return this.equals(DataMediaType.CASSANDRA);
}
public boolean isHDFSArvo() {
return this.equals(DataMediaType.HDFS_ARVO);
}
public boolean isOracle() {
return this.equals(DataMediaType.ORACLE);
}
public boolean isMysql() {
return this.equals(DataMediaType.MYSQL);
}
public boolean isTddl() {
return this.equals(DataMediaType.TDDL);
}
public boolean isCobar() {
return this.equals(DataMediaType.COBAR);
}
public boolean isMemcache() {
return this.equals(DataMediaType.MEMCACHE);
}
public boolean isMq() {
return this.equals(DataMediaType.MQ);
}
public boolean isGreenPlum() {
return this.equals(DataMediaType.GREENPLUM);
}
public boolean isNapoli() {
return this.equals(DataMediaType.NAPOLI);
}
public boolean isDiamondPush() {
return this.equals(DataMediaType.DIAMOND_PUSH);
}
}
1.2 修改vm页面和相应webx action,screen类,修改支持页面参数配置和check.
注意,screen等中原来otter做了判断非mysql或者oracle类型的不显示,都需要硬编码改为可以显示,这个有点土,害我找了好会儿;
check需要修改的比较多,使用的是Dwr调用的,
com.alibaba.otter.manager.biz.utils.DataSourceChecker
需要修改public String check(String name,String url, String username, String password, String encode, String sourceType) 测试数据源连接是否成功
public String checkMap(String namespace, String name, Long dataSourceId) 测试数据表是否连接
public String checkNamespaceTables(final String namespace, final String name, final Long dataSourceId) 测试多个表是否select连接,对应数据源和表2个模块。
扩展支持其它NoSql DB类型。1.3 vm页面修改
2、因为Otter使用的Guava版本和HBase等的guava冲突:
otter node中大量使用了guava的MapMaker,需要升级改为LoadingCache(这个工作量其实不小,而且要小心实现准确,待后面node中调试的时候测试);
主要修改实现不同NoSqlDB的连接获取,表元数据查询等:
DbDialectFactory
DBDataSourceService 原接口返回sql的connection对象改为Object,在调用的时候根据数据库类型转换为不同的连接
// 构建第一层map
dataSources = CacheBuilder.newBuilder().maximumSize(1000)
.build(new CacheLoader<Long, LoadingCache<DbMediaSource, Object>>() {
@Override
public LoadingCache<DbMediaSource, Object> load(Long pipelineId) throws Exception {
return CacheBuilder.newBuilder().maximumSize(1000)
.build(new CacheLoader<DbMediaSource, Object>() {
@Override
public Object load(DbMediaSource dbMediaSource) throws Exception {
// 扩展功能,可以自定义一些自己实现的 dataSource
if (dbMediaSource.getType().isCassandra()) {
return getCluster(dbMediaSource);
} else if (dbMediaSource.getType().isElasticSearch()) {
return getClient(dbMediaSource);
} else if (dbMediaSource.getType().isHBase()) {
return getHBaseConnection(dbMediaSource);
} else if (dbMediaSource.getType().isHDFSArvo()) {
return getHDFS(dbMediaSource);
} else if (dbMediaSource.getType().isKafka()) {
return getProducer(dbMediaSource);
} else {
DataSource customDataSource = preCreate(pipelineId, dbMediaSource);
if (customDataSource != null) {
return customDataSource;
}
return createDataSource(dbMediaSource.getUrl(), dbMediaSource.getUsername(),
dbMediaSource.getPassword(), dbMediaSource.getDriver(),
dbMediaSource.getType(), dbMediaSource.getEncode());
}
}
});
}
});
}
3、Node端修改:
3.1 实现不同类型的数据写入
新建 NoSqlTemplate接口,nosql db不使用sqlTemplate的sql,直接实现增删改和DDL的操作:
package com.alibaba.otter.node.etl.common.db.dialect;
import java.util.List;
import com.alibaba.otter.node.etl.load.exception.ConnClosedException;
import com.alibaba.otter.shared.etl.model.EventData;
public interface NoSqlTemplate {
/**
* 批量执行dml数据操作,增,删,改
*
* @param events
* @return 执行失败的记录集合返回,失败原因消息保存在exeResult字段中
*/
public List<EventData> batchEventDatas(List<EventData> events) throws ConnClosedException;
/**
* 插入行数据
*
* @param event
* @return 记录返回,失败原因消息保存在exeResult字段中
*/
public EventData insertEventData(EventData event) throws ConnClosedException;
/**
* 更新行数句
*
* @param event
* @return 记录返回,失败原因消息保存在exeResult字段中
*/
public EventData updateEventData(EventData event) throws ConnClosedException;
/**
* 删除记录
*
* @param event
* @return 记录返回,失败原因消息保存在exeResult字段中
*/
public EventData deleteEventData(EventData event) throws ConnClosedException;
/**
* 建立表
*
* @param event
* @return
*/
public EventData createTable(EventData event) throws ConnClosedException;
/**
* 修改表
*
* @param event
* @return
*/
public EventData alterTable(EventData event) throws ConnClosedException;
/**
* 删除表
*
* @param event
* @return
*/
public EventData eraseTable(EventData event) throws ConnClosedException;
/**
* 清空表
*
* @param event
* @return
*/
public EventData truncateTable(EventData event) throws ConnClosedException;
/**
* 改名表
*
* @param event
* @return
*/
public EventData renameTable(EventData event) throws ConnClosedException;
}
修改原 DbDialect,扩展原来的返回JdbcTemplate和SqlTemplate,在DbLoad端根据返回类型和数据类型分别判断后执行。
<span style="white-space:pre"> </span>//JdbcTemplate在nosqldb返回相应nosql db的链接
<span style="white-space:pre"> </span>public <T> T getJdbcTemplate();
/**
* 获取数据操作相关类型,关系数据库,返回SqlTemplate,返回组织的sql NoSql
* database放回NoSqlTemplate,执行相关操作
*
* @return
*/
public <T> T getSqlTemplate();
实现不同Nosql DB的DbDialect和NoSqlTemplate;
每个数据库系统类型2个实现类:
CassandraDialect;CassandraTemplate;
ElasticSearchDialect;ElasticSearchTemplate;
GreenPlumDialect;GreenPlumSqlTemplate gp继承SqlTemplate,跟关系数据库一样实现即可;
HBaseDialect;HBaseTemplate;
KafkaDialect;KafkaTemplate
3.2 修改DbLoader模块,支持各个数据类型写入
com.alibaba.otter.node.etl.load.loader.db.DbLoadAction
主要修改它的inner class
DbLoadWorker
DoCall方法:
if (dbDialect.isNoSqlDB()) {
NoSqlTemplate nosqltemplate = (NoSqlTemplate) dbDialect.getSqlTemplate();
failedDatas.clear(); // 先清理
processedDatas.clear();
if (useBatch && canBatch) {
nosqltemplate.batchEventDatas(splitDatas);
// 更新统计信息
for (int i = 0; i < splitDatas.size(); i++) {
processStat(splitDatas.get(i));
}
} else {
for (EventData event : splitDatas) {
if (event.getEventType().isDelete()) {// 删除
nosqltemplate.deleteEventData(event);
} else if (event.getEventType().isUpdate()) {
nosqltemplate.updateEventData(event);
} else if (event.getEventType().isInsert()) {
nosqltemplate.insertEventData(event);
}
processStat(event);
}
}
} else {
final LobCreator lobCreator = dbDialect.getLobHandler().getLobCreator();
然后基本完成,根据自己修改的过程毛糙的记录了一下。