MTDDL——美团点评分布式数据访问层中间件
背景
2016年Q3季度初,在美团外卖上单2.0项目上线后,商家和商品数量急速增长,预估商品库的容量和写峰值QPS会很快遇到巨大压力。随之而来也会影响线上服务的查询性能、DB(数据库,以下统一称DB)主从延迟、表变更困难等一系列问题。
要解决上面所说的问题,通常有两种方案。第一种方案是直接对现有的商品库进行垂直拆分,可以缓解目前写峰值QPS过大、DB主从延迟的问题。第二种方案是对现有的商品库大表进行分库分表,从根本上解决现有问题。方案一实施起来周期较短,但只能解决一时之痛,由此可见,分库分表是必然的。
在确定分库分表的方案之后,我们调研了外卖订单、结算以及主站等业务的分库分表实现方案,也调研了业界很多分库分表中间件。在综合考虑性能、稳定性及实现成本的前提下,最终决定自主研发客户端分库分表中间件MTDDL来支撑外卖商品分库分表项目,这也就是MTDDL的由来。
当然,在MTDDL的设计研发过程中,我们充分考虑了MTDDL的通用性、可扩展性、功能的全面性和接入的便利性。到目前为止一共开发了四期,实现了MySQL动态数据源、读写分离、分布式唯一主键生成器、分库分表、连接池及SQL监控、动态化配置等一系列功能,支持分库分表算法、分布式唯一主键生成算法的高可扩展性,而且支持全注解的方式接入,业务方不需要引入任何配置文件。
下面就部分业界方案及MTDDL的设计目标详细展开下,然后从源码的角度来剖析下MTDDL的整个逻辑架构和具体实现。
业界调研
设计目标
MTDDL(Meituan Distributed Data Layer),美团点评分布式数据访问层中间件,旨在为全公司提供一个通用数据访问层服务,支持MySQL动态数据源、读写分离、分布式唯一主键生成器、分库分表、动态化配置等功能,并且支持从客户端角度对数据源的各方面(比如连接池、SQL等)进行监控,后续考虑支持NoSQL、Cache等多种数据源。
功能特性
- 动态数据源
- 读写分离
- 分布式唯一主键生成器
- 分库分表
- 连接池及SQL监控
- 动态化配置
逻辑架构
下图是一次完整的DAO层insert方法调用时序图,简单阐述了MTDDL的整个逻辑架构。其中包含了分布式唯一主键的获取、动态数据源的路由以及SQL埋点监控等过程:
具体实现
动态数据源及读写分离
在Spring JDBC AbstractRoutingDataSource的基础上扩展出MultipleDataSource动态数据源类,通过动态数据源注解及AOP实现。
动态数据源
MultipleDataSource动态数据源类,继承于Spring JDBC AbstractRoutingDataSource抽象类,实现了determineCurrentLookupKey方法,通过setDataSourceKey方法来动态调整dataSourceKey,进而达到动态调整数据源的功能。其类图如下:
动态数据源AOP
ShardMultipleDataSourceAspect动态数据源切面类,针对DAO方法进行功能增强,通过扫描DataSource动态数据源注解来获取相应的dataSourceKey,从而指定具体的数据源。具体流程图如下:
配置和使用方式举例
/**
* 参考配置
*/
<bean id="multipleDataSource" class="com.sankuai.meituan.waimai.datasource.multi.MultipleDataSource">
/** 数据源配置 */
<property name="targetDataSources">
<map key-type="java.lang.String">
/** 写数据源 */
<entry key="dbProductWrite" value-ref="dbProductWrite"/>
/** 读数据源 */
<entry key="dbProductRead" value-ref="dbProductRead"/>
</map>
</property>
</bean>
/**
* DAO使用动态数据源注解
*/
public interface WmProductSkuDao {
/** 增删改走写数据源 */
@DataSource("dbProductWrite")
public void insert(WmProductSku sku);
/** 查询走读数据源 */
@DataSource("dbProductRead")
public void getById(long sku_id);
}
分布式唯一主键生成器
众所周知,分库分表首先要解决的就是分布式唯一主键的问题,业界也有很多相关方案:
综上,方案3的缺点可以通过一些手段避免,但其他方案的缺点不好处理,所以选择第3种方案。目前该方案已由美团点评技术工程部实现——分布式ID生成系统Leaf,MTDDL集成了此功能。
分布式ID生成系统Leaf
美团点评分布式ID生成系统Leaf,其实是一种基于DB的Ticket服务,通过一张通用的Ticket表来实现分布式ID的持久化,执行update更新语句来获取一批Ticket,这些获取到的Ticket会在内存中进行分配,分配完之后再从DB获取下一批Ticket。整体架构图如下:
每个业务tag对应一条DB记录,DB MaxID字段记录当前该Tag已分配出去的最大ID值。
IDGenerator服务启动之初向DB申请一个号段,传入号段长度如 genStep = 10000,DB事务置 MaxID = MaxID + genStep,DB设置成功代表号段分配成功。每次IDGenerator号段分配都通过原子加的方式,待分配完毕后重新申请新号段。
唯一主键生成算法扩展
MTDDL不仅集成了Leaf算法,还支持唯一主键算法的扩展,通过新增唯一主键生成策略类实现IDGenStrategy接口即可。IDGenStrategy接口包含两个方法:getIDGenType用来指定唯一主键生成策略,getId用来实现具体的唯一主键生成算法。其类图如下:
分库分表
在动态数据源AOP的基础上扩展出分库分表AOP,通过分库分表ShardHandle类实现分库分表数据源路由及分表计算。ShardHandle关联了分库分表上下文ShardContext类,而ShardContext封装了所有的分库分表算法。其类图如下:
分库分表流程图如下:
分库分表取模算法
分库分表目前默认使用的是取模算法,分表算法为 (#shard_key % (group_shard_num * table_shard_num)),分库算法为 (#shard_key % (group_shard_num * table_shard_num)) / table_shard_num,其中group_shard_num为分库个数,table_shard_num为每个库的分表个数。
例如把一张大表分成100张小表然后散到2个库,则0-49落在第一个库、50-99落在第二个库。核心实现如下:
public class ModStrategyHandle implements ShardStrategy {
@Override
public String getShardType() {
return "mod";
}
@Override
public DataTableName handle(String tableName, String dataSourceKey, int tableShardNum,
int dbShardNum, Object shardValue) {
/** 计算散到表的值 */
long shard_value = Long.valueOf(shardValue.toString());
long tablePosition = shard_value % tableShardNum;
long dbPosition = tablePosition / (tableShardNum / dbShardNum);
String finalTableName = new StringBuilder().append(tableName).append("_").append(tablePosition).toString();
String finalDataSourceKey = new StringBuilder().append(dataSourceKey).append(dbPosition).toString();
return new DataTableName(finalTableName, finalDataSourceKey);
}
}
分库分表算法扩展
MTDDL不仅支持分库分表取模算法,还支持分库分表算法的扩展,通过新增分库分表策略类实现ShardStrategy接口即可。ShardStrategy接口包含两个方法:getShardType用来指定分库分表策略,handle用来实现具体的数据源及分表计算逻辑。其类图如下:
全注解方式接入
为了尽可能地方便业务方接入,MTDDL采用全注解方式使用分库分表功能,通过ShardInfo、ShardOn、IDGen三个注解实现。
ShardInfo注解用来指定具体的分库分表配置:包括分表名前缀tableName、分表数量tableShardNum、分库数量dbShardNum、分库分表策略shardType、唯一键生成策略idGenType、唯一键业务方标识idGenKey;ShardOn注解用来指定分库分表字段;IDGen注解用来指定唯一键字段。具体类图如下:
配置和使用方式举例
// 动态数据源
@DataSource("dbProductSku")
// tableName:分表名前缀,tableShardNum:分表数量,dbShardNum:分库数量,shardType:分库分表策略,idGenType:唯一键生成策略,idGenKey:唯一键业务方标识
@ShardInfo(tableName="wm_food", tableShardNum=100, dbShardNum=1, shardType="mod", idGenType=IDGenType.LEAF, idGenKey=LeafKey.SKU)
@Component
public interface WmProductSkuShardDao {
// @ShardOn("wm_poi_id") 将该注解修饰的对象的wm_poi_id字段作为shardValue
// @IDGen("id") 指定要设置唯一键的字段
public void insert(@ShardOn("wm_poi_id") @IDGen("id") WmProductSku sku);
// @ShardOn 将该注解修饰的参数作为shardValue
public List<WmProductSku> getSkusByWmPoiId(@ShardOn long wm_poi_id);
}
连接池及SQL监控
DB连接池使用不合理容易引发很多问题,如连接池最大连接数设置过小导致线程获取不到连接、获取连接等待时间设置过大导致很多线程挂起、空闲连接回收器运行周期过长导致空闲连接回收不及时等等,如果缺乏有效准确的监控,会造成无法快速定位问题以及追溯历史。
再者,如果缺乏SQL执行情况相关监控,会很难及时发现DB慢查询等潜在风险,而慢查询往往就是DB服务端性能恶化乃至宕机的根源(关于慢查询,推荐阅读《MySQL索引原理及慢查询优化》一文)。MTDDL从1.0.2版本开始正式引入连接池及SQL监控等相关功能。
连接池监控
实现方案
结合Spring完美适配c3p0、dbcp1、dbcp2、mtthrift等多种方案,自动发现新加入到Spring容器中的数据源进行监控,通过美团点评统一监控组件JMonitor上报监控数据。整体架构图如下:
连接数量监控
监控连接池active、idle、total连接数量,Counter格式:(连接池类型.数据源.active/idle/total_connection),效果图如下:
获取连接时间监控
监控获取空闲连接时间,Counter格式:(ds.getConnection.数据源.time),效果图如下:
SQL监控
实现方案
采用Spring AOP技术对所有DAO方法进行功能增强处理,通过美团点评分布式会话跟踪组件MTrace进行SQL调用数据埋点及上报,进而实现从客户端角度对SQL执行耗时、QPS、调用量、超时率、失败率等指标进行监控。整体架构图如下:
实现效果
登录美团点评的服务治理平台OCTO选择服务查看去向分析,效果图如下:
动态化配置
为了满足业务方一些动态化需求,如解决线上DB紧急事故需动态调整数据源或者分库分表相关配置,要求无需重启在线修改立即生效,MTDDL从1.0.3版本开始正式引入动态化配置相关功能。
实现方案
在Spring容器启动的时候自动注册数据源及分库分表相关配置到美团点评的统一配置中心MCC,在MCC配置管理页面可以进行动态调整,MCC客户端在感知到变更事件后会刷新本地配置,如果是数据源配置变更会根据新的配置构造出一个新数据源来替换老数据源,最后再将老的数据源优雅关闭掉。具体流程图如下:
动态化数据源
目前支持dbcp、dbcp2、c3p0等数据源,效果图如下:
分库分表动态化
支持动态化配置分库分表数量、分库分表策略、唯一键生成策略、唯一键业务方标识等,效果图如下:
版本迭代
MTDDL到目前为止总共开发了四期,后续考虑逐步开源,具体版本迭代如下:
- iKcamp|基于Koa2搭建Node.js实战(含视频)☞ HTTP请求
- ubuntu中安装tomcat
- python文件操作
- Owasp测试4.0手册
- 推荐一款Web渗透测试数据库
- 【提莫】一个域名收集及枚举工具
- chmod: changing permissions of `/usr/local/bin/...
- a windows service with the name MYSQL already e...
- NameError: name 'admin' is not defined(彻底解决方案)
- Error: No module named blog
- ubuntu中的django安装配置与操作
- IOS开发之-搜索栏UISearchController详解
- java归并排序(最精简代码)
- java希尔排序(最精简代码)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 搭建hadoop集群虚拟机试验环境
- PLSQL-简单的语句块及变量的定义
- Python 技术篇-使用PIL库等比例压缩、缩小图片
- linux 安装并配置zsh
- 听音乐不过瘾?自制一个音乐播放器!【附带函数源码】
- Python 技术篇-3行代码实现Gif动画生成,Gif动画素材获取方法
- Linux 文件隐藏权限
- 还在用print()查找错误?日志消息这顿排骨它不香嘛?
- CNS图表复现02—Seurat标准流程之聚类分群
- Python 技术篇-莉莉机器人api调用方法,实例演示。免费的机器人
- Hugo Travis 完结!
- 【Python】秀儿!两行代码制作你的专属动态二维码
- Windows 技术篇-网卡物理(MAC)地址查看方法
- Go使用工厂方法实例结构体
- Java交互界面实现计算器开发设计【附函数源码】