如何用中间件实现信创DB的适配?

1.引言

随着技术国产化的趋势不断加强,许多大型客户对于数据库的选择也开始倾向于国产产品,例如达梦,以响应国家的号召和政策法规。

在这种背景下,SAAS企业就需要及时将自己的产品系统与国产数据库作对接,以加强其在大客户领域的竞争力。

本文就以MySQL对接国产达梦数据库为例,讨论一种可能的适配方案。

2.构思

2.1 问题

信创数据库对接主要涉及到两类问题:

  1. DDL建表语句适配以及存量数据的迁移,这一部分属于一次性工作,一般由DBA负责,本文暂时不作讨论;
  2. 现有业务服务中用到的DML增删改查语句对新数据库的适配;

DML语句的适配也可以简单分为两类问题:

  1. 简单用字符串替换能解决的

    • 不支持反引号:`,需要替换成双引号:"
    • 不支持group_concat函数,需要使用达梦中的wm_concat函数代替
  2. 语句结构改变需要手动调整SQL的,例如:

    • on duplicate key update
    • replace into
    • case when then else

file

公司基于MySQL的业务服务有50~60个,手动修改所有服务的代码去适配另一个数据库,不太现实,后期维护也是一个问题,于是就想能不能找一个统一的地方来处理这个适配工作。

2.2 思路

在尽量少侵入业务的前提下,有两种处理办法:

a. 在orm层作语法替换,让每个服务集成定制的orm库;

  • 优点:难度小,只需要关心SQL语法替换,后端DB的连接和驱动不涉及;
  • 缺点:
    • 40多个服务都需要集成定制的ORM库,每次orm库变动所有服务都需要重新出包;
    • 只能适用于golang, java、php以及其它语言的服务需要重新考虑方案;

b. 写一个中间件,来代理发给mysql的所有请求,在中间件上完成适配工作。这个中间件需要承担如下角色:

  • 服务器:需要接收Mysql驱动的连接及数据包请求;
  • 语法转换:完成异构数据库的语法替换;
  • 代理:将CRUD的所有操作转发到后端的dm数据库;
  • 协议转换:需要将后端dm返回的数据再封装成mysql协议数据包发给Mysql驱动;
  • 需要支持一个中间件能代理多个数据库;

file

由于中间件对整个系统的侵入最小,所以我们优先尝试走中间件的方案。

3.设计

调研过多个开源项目,能符合我们需求的有go-mysql-server、 vitess、kingshard, 代码了解下来,vitess过于庞大,go-mysql-server代码不够直观且mysql协议部分依赖其它项目,最后我们选择了基于kingshard作二次开发。

3.1 中间件设计

中间件整体采用从上到下的分层依赖结构,几个核心包职责如下:

  • server包负责实现一个mysql服务器,代理mysql的连接建立和数据包协议
  • backend包负责代理与后端dm数据库的操作交互,采用database/sql(标准接口)+ 数据库驱动来与dm数据库进行交互
  • sqlparser包负责sql语法的解析和替换
  • mysql包负责将后端dm返回的数据重新封装成mysql协议的数据包;

file

3.2 server

server部分是基于kingshard的server模块作的二次修改,整体保留了kingshard的函数结构和代码执行流程。

  • 先解析后端DB配置,每个DB建立一个BackendProxy代理实例,本质上就是调用sql.Open来创建一个sql.DB实例;
nodes :
- 
    # db alias name
    name : uc_uniform
    # db driver name
    driver_name: dm

    # default max conns for connection pool
    max_conns_limit : 32

    # master represents a real mysql master server 
    datasource : dm://uc_uniform/uc_uniform@192.168.23.216:5236?socketTimeout=10000
  • 监听9696端口,每accept一个连接,就走onConn开启一个goroutine来处理此连接上的请求,包括用户名/密码验证、mysql协议握手;
  • 启动连接的主循环,接收数据包,并解析指令,调用对应的方法来处理指令;

file

【mysql协议请求的数据包格式示意】

类型字段含义
int<1>COMMAND0x03:COM_QUERY
stringquerySQL语句

【mysql常用指令及对应的处理】

命令含义处理函数是否需要与数据库交互
COM_INIT_DB连接建立时指定DBhandleInitDB()中间件处理
COM_PING心跳包handlePing()中间件处理
COM_QUERYDDL/DML语句handleQuery()后端数据库处理
COM_STMT_PREPARE预编译SQLhandleStmtPrepare()中间件处理
COM_STMT_EXECUTE发送参数执行预编译SQLhandleStmtExecute()后端数据库处理
COM_STMT_CLOSE关闭预编译SQLhandleStmtClose()中间件处理
COM_QUIT连接关闭handleQuit()中间件处理

3.3 backend

基于database/sql的API定义了一套用于SQL处理的标准插件接口:

// db querier
type dbQuerier interface {
	Prepare(query string) (*sql.Stmt, error)
	Exec(query string, args ...interface{}) (sql.Result, error)
	Query(query string, args ...interface{}) (*sql.Rows, error)
	QueryRow(query string, args ...interface{}) *sql.Row
}

// transaction beginner
type txer interface {
	Begin() (*sql.Tx, error)
}

// transaction ending
type txEnder interface {
	Commit() error
	Rollback() error
}

type SQLPlugin interface {
	dbQuerier
	txer
	txEnder
}

只要实现这套接口,就可以当作一个插件灵活插入到SQL的处理流程中执行,例如:

  • sql日志
  • 语法替换

如果不需要了,也可以随时把它移除,对SQL整体处理没有影响。通过此插件设计实现了代码的分层和后续业务灵活扩展:

3.4 sqlparser

sqlparser是SQL语法解析的开源项目,我们的改动主要是加了一个SQL语法的转换功能,并定义了一个标准转换接口:

type SQLConverter interface {
	Convert(sql string) (string, error)
}

目前只实现了Mysql-to-Oracle的语法转换,转换流程简单示意如下:

  • Parse(): 解析sql语法,将一条完整的语句解析成一个SQL语法树对象;
  • convertStmt(): 将mysql语法的AST树转换成oracle语法的AST树;
  • Format: 将oracle语法树对象再格式化成标准SQL语句;

以开篇的SQL语句为例,我们示意下转换前和转换后的语法树结构。

# 转换前带on duplicate key update冲突语法的语句
`insert into webcal_live_info(cal_id,channelId,pullurl,password,extraInfo) values(634311,131722,'https://rlive1uat.rmeet.com.cn/activity/geeZWo3','','') on duplicate key update pullurl='https://rlive1uat.rmeet.com.cn/activity/geeZWo3', password='', extraInfo=''`

# 转换后的merge into语句
`merge into webcal_live_info as t using dual on t.cal_id = 634311 and t.channelId = 131722 when matched then update set t.pullurl = 'https://rlive1uat.rmeet.com.cn/activity/geeZWo3', t.password = '', t.extraInfo = '' when not matched then insert (cal_id, channelId, pullurl, password, extraInfo) values (634311, 131722, 'https://rlive1uat.rmeet.com.cn/activity/geeZWo3', '', '')`

Insert语法树示意:
file

merge语法树示意:
file

转换语法要做的工作就是从原语法树中提取出table、column、data信息,重新组装成一棵符合oracle语法的AST树。

  • merge into是以多表合并数据的方式来解决单表插入或更新的问题。
  • 单表变成多表语句后,每个字段需要加表的前缀,通过一个访问者模式来实现对指定信息的递归遍历
	visit := func(node SQLNode) (kcontinue bool, err error) {
		switch node.(type) {
		case *ColName:
			node.(*ColName).Qualifier = TableName{
				Name: NewTableIdent("t"),
			}
			return true, nil
		default:
			return true, nil
		}
	}
	Walk(visit, node)

3.5 mysql

mysql包是kingshard自带的mysql数据包读写协议封装,我们的改动主要是添加了对列数据封装成行数据的支持。之所以要做这个工作,主要是database/sql结果集与mysql结果集存在一定的差异。

【结构体对比示意】
file

差异点:

  • database/sql中的Rows.Scan()方法只能按列来读取一条数据
  • mysql.Resultset中的RowDatas需要的是以行为单位的二进制数据。

mysql包已经提供了对mysql.Result结构体的协议封装,我们需要做的就是把sql.Result或sql.Rows转换成mysql.Result。大概分为两步:

  1. 调用Rows.Scan()读取列数据,为减少对数据的操作,我们直接用database/sql中定义的二进制类型sql.RawBytes来接收列数据。
  2. 也是最核心的点,将database/sql中打散的列重组成一个行协议数据包。

将列封装成行的协议规则:

  • NULL的列都按0xFB发送
  • 其它非NULL列都转换为string,并按string发送。
func packetTextRowData(row []sql.RawBytes) (RowData, error) {
	length := 0
	for _, val := range row {
		if val == nil {
			length++
		} else {
			l := len(val)
			length += LenEncIntSize(uint64(l)) + l
		}
	}

	data := make([]byte, 0, length)
	for _, val := range row {
		if val == nil {
			data = append(data, NullValue)
		} else {
			data = append(data, PutLengthEncodedString(val)...)
		}
	}

	if len(data) != length {
		return nil, fmt.Errorf("packet row: got %v bytes but expected %v", len(data), length)
	}

	return RowData(data), nil
}

关于MySQL数据传输协议,请参考文末附的MySQL协议文档链接。

小结

本文从信创数据库对接给现有业务系统带来的冲击角度出发,介绍了一种基于异构DB中间件来简化适配工作量的方法,并给出了中间件的整体设计构思。

中间件首先基于开源kingshard的server包改造了一个MySQL服务端,然后基于sqlparser开源项目完成了SQL语法的解析和转换,最后基于一篇文章Mysql协议介绍来实现了最后的MySQL响应数据包协议封装,一个能够自动完成MySQL和达梦语法协议转换的中间件就构建起来了。

这个中间件虽然是以MySQL和达梦的语法转换为案例来构造,但它通过对语法转换模型作抽象接口提取,理论上可以自由扩展对其它SQL型数据库的支持,详情见3.3节Backend包

关于sqlproxy的设计,暂时就介绍这么多,如果有兴趣了解更多,可以查看项目源码,下面参考阅读里有附源码链接。

参考阅读

  • 19
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉下心来学鲁班

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值