作者简介
蓝寅,开源分布式中间件 DBLE 项目负责人;持续专注于数据库方面的技术, 始终在一线从事开发;对数据复制,读写分离,分库分表的有深入的理解与实践。
3 月 14 日,爱可生开源社区联合 IT168 发布了一期《MyCat 的坑如何在分布式中间件 DBLE 上改善》的直播,根据反馈,现将直播内容节选成文,以供大家回顾重温。
Tips:考虑到大家的不同口味,开源社区官网上线了完整版录播视频,无论是喜欢文字,爱好图文,青睐于完整版视频的同学都能找到自己喜欢的打开方式!
直播视频回顾请点击“录屏”,一键直达。
“以下为分享内容的正文部分”
背景
近年来,随着移动互联网、物联网、人工智能等技术的兴起,需要处理的数据越来越多,作为存储架构核心的关系型数据库不可避免的引发了需要扩容的问题,在这个过程中分库分表被发明出来。
分库分表最初不需要中间件,由各自应用的开发人员自己来负责,应用除了要了解业务逻辑以外,还需要明确完整的拆分规则,成本较高,对开发人员要求也很高,并且不利于任务和逻辑的解耦。因此,中间件应运而生。
分布式系统架构基本分成三层,最上面是一层是 APP 层,中间是中间件层,下面是数据存储层。
今天分享的内容主要为中间件,那么一个理想的中间件应该是什么样的?
第一,透明性,理想的中间件会向应用开发人员屏蔽后面具体拆分的细节。数据存储的工作被独立出来,应用开发人员可以更关注业务逻辑而不是存储方式。
第二,兼容性,理想的中间件最好是不要自定义一套规则,而是去兼容当前大家熟悉的规则,对我们来说,这个熟悉的规则就是 MySQL。所以中间件的语法也好,协议也好,对于使用者来说,最好的是用户使用时就像使用原生 MySQL 一样,而不是需要花很长时间学习一套新的规则,否则无论是学习成本或者迁移成本都很高。
兼容性还有一个好处,现有的 JDBC 或者是其他的一些驱动都可以用,不需要再去定制开发一个驱动。
第三,性能,一般对性能的考量是延迟和吞吐量。因为中间件多了一层,单个查询的 response 会多一个 RTT 延迟,所以延迟方面不一定有优势。主要看吞吐量是不是变得比原来更强。
第四,安全,不能因为有了中间件而将原来完好的密码管理规则变成名存实亡的存在,这种做法也是不妥的。
最后,运维性,比如与中间件配套的备份扩容工具等,这方面组件也是很重要的。
开源社区里 MyCat 的是比较著名的。我们深度研究了 MyCat,加上我们在分布式中间件上既有的一些经验,结合起来,就是形成了我们新的一个分布式中间件 DBLE。DBLE 的结构大致如图,内部主要有协议,解析,路由和运算模块。
那么 DBLE 跟 MyCat 相比解决了哪些问题?以下将从 DBA 与研发两个方面介绍:
1.DBA 的角度,站在 DBA 的角度,如何实现他们并不是太关心,对能用,好用十分关注,即:正确性,安全性,稳定性,可运维性等。本次分享主要关注于正确性,因为这是最大的坑,其他方面鉴于时长有限,不在这次分享中详细讲述了。
2. 开发测试的角度,从开发测试的角度来看最关注的是代码质量,是否可维护,代码管理是否科学,能否持续报纸质量,保证项目健康发展。
首先我们从 DBA 角度分享一下在 MyCat 上踩的坑,当然,这些坑 DBLE 都填了,具体的实现方式欢迎大家关注我们正在陆续释放的公开课,会有更多的内容揭秘。
一.DBA 角度看中间件
我们主要从两方面来讨论,一部分是 SQL 语言实现:包括 select,insert,set 等语句来说明正确性的问题,另一部分将举个运维管理的例子来说明安全性的问题。
1. SQL 语言实现
以下案例都采用最新版的 MyCat 1.6.7 举例,在此之前分享过的一些 MyCat 的 bug 和坑,此次查看已经修了一部分,不过坑还是太多。
1.1 数据查询
简单查询对比案例
从拆分规则来看,最常用的 hash 拆分,用 ID 值对 1024 求模,求出的结果 0~1023 按照每 256 个数拆分,拆成 4 份,0~255 在结点 1;256~511 在结点 2,以此类推。我们准备用 10 条数据,覆盖到各个分片上,都通过中间件写入。
准备完成之后,用查询语句案例 select * from employee where id between 511 and 1791 order by id,加 order by 主要是为了更容易看出问题。
如图所示,在查询结果中 MyCat 丢了三条数据,原因是因为计算路由错误。像这样大范围查询的 SQL 应该下发给所有后端结点,而实际上 MyCat 下发的少了。
聚合函数查询案例
聚合函数查询案例的准备数据与简单查询类似,在此不赘述了,我们计算出 ID 的方差,可以看到 MyCat 返回的是四个数,并且这 4 个数无论如何也不可能捏回标准差,而 DBLE 的结果是正确的。当然,有同学验证的话会发现有细微的精度误差,这是因为二进制存储会损失一些精度,分布式的算法又会损失一些精度,因此会有精度上的误差。
数据查询 - 函数嵌套查询案例
继续举例,准备数据不变,SQL 变成了复杂一点的表达式,对 count 的结果取绝对值。可以看到 MyCat 是支持 count 的,但是前面去嵌套了一个其他的函数,MyCat 就不认识了,它把整个语句下发给各个节点,然后对各个节点做了简单合并,这个合并没有加起来,只是简单的堆积在一起,然后回到了应用;而 DBLE 结果正确无误。
数据查询 – union 查询案例
union 查询案例的结果,数据准备如图是简单的两张表,一张表 hotnews 分为四个节点,规则也很简单,就是对四求模,按照求模的结果拆分到了四个节点上。另一张表 travelrecord 稍微复杂一点,是两个节点,它的规则是按 1024 次求模,然后 0 到 511 分到第一个节点,512 到 1023 分到第二个节点。第一张表是四行数据,第二张表是五行数据。这个例子已经能说明问题了,现实生活中情况可能更复杂一些。
查询:select id from hotnews union select id from travelrecord 语句,即用 ID 做一个 union,如图所示,MyCat 的结果并没有去重,把所有的结果都拿到了。对比之下 DBLE 的结果则是正确的。
数据查询 – union all 查询案例
在 union all 的查询案例中,MyCat 的查询结果还是和 union 一样。因为 MyCat 在 union 查询时是将 union 语句整体下发到各个节点上,而在计算时则是按照 hotnews 这张表来计算节点,由于 MyCat 只把查询下发给两个节点,拿到的结果其实是不全的。
数据查询 – 子查询
子查询对比结果有三个,MyCat 会直接 hang 住。看代码 hang 住的原因是 MyCat 内部死锁。中间件在做子查询任务时,其实是拿到子查询结果以后再拼出新的 SQL 来,然后再下发第二句 SQL。
在这个过程中,MyCat 固定大小的线程池被占满了,造成了死锁,而 DBLE 结果还是正确的。
数据查询 – Join 案例
重点讨论一下 Join,MyCat 解决跨表 Join 的方式有 3 种:配置 global 表,配置 ER 表,使用 hint,下面一一剖析,看看是否是真的能解决所有问题。
数据查询 – Join 案例 剖析 global 表
对于数据量不大的字典表可以采用 global 表。举例,超市的几十万商品表,销售详单非常多,拆表时往往选择拆数据最多的销售详单表,假设按照日期,将销售详单拆分,按天将详单表拆成 N 片,在每一片的 schema 中有一个全量的商品表,即全局表。
当进行销售详单和商品表的 Join 查询的时,之所以用 Join,是因为详单里面只有 ID 没有商品名称,进行 Join 查询时才能拿到名称,Join 查询时 Join 语句下发到各个节点,而各个节点上的全局表都是全量数据,因此 Join 可以拿到正确的数据,这就是全局表的作用。
举一个具体例子,将商品表和销售详单表通过商品 ID 来关联,在一定时间范围内,根据 group by 日期和商品名,查看订单量。
这样一句 Join,因为 group by 中包含了拆分列,所以这条语句可以下推给所有节点,这些节点得到的结果,直接简单的进行合并,返回到客户端就是正确的数据,这是 global 表的正确用法。
global 表能不能解决所有的问题呢?答案是不行。
举例说明,在这个 case 中,在 query 里,首先 group by 并不是按照拆分列去分组,其次 select row 里面有 count distinct 的过程,这句 SQL,如果下发到各个节点,会发生什么样的情况?
如图,第一个分片上得到的日用品和文具是一和二,第二个节点上得到的也是。
但如果把左边的图不看成拆分表,大家应该对 distinct 都非常熟悉,可以自己试着用 group by 做一下,结论应该会是日用品一文具三,通过两个节点得到的结果分别是一和二,无论怎么合并,也无法合出第三个这样的结果。
所以这就是 global 表解决不了的问题,当碰到这样的查询时 global 表就无法解决,因此它不能解决所有问题。
数据查询 – Join 案例 剖析 ER 表
ER 表可以简单地理解为两张表有逻辑外键关系,按照这列来拆分,几张表都可以按照同样一个规则拆分。涉及到了关联列的 Join,也可以同样下发到各个节点上。
注意,外键列需要依赖于拆分列,不能有拆分列和外键列是 1 比 N 的关系。
再举例,按照销售单的日期拆分,流水号和日期有一一对应关系,不会出现一个流水号有两个日期。根据流水号去拆分另一张表,拆分完之后,如果这两张表通过流水号关联做 Join,可以直接到下发到各个节点。
ER 表是万能的吗?
假如不是所有表的关联关系都是同一列,当关联关系比较复杂,A 表和 B 表是通过关联列 COLUMN1 来关联,B 表和 C 表是通过 COLUMN2 来关联,会发现无论用哪种方式去做拆分,都无法得到一个完美的拆分方案,一定会有一张表被打散。
打散之后再做 Join,就又回到了跨节点 Join 的问题。
跨节点 Join 的问题,把语句直接分发到各个节点是不正确的。
因此,ER 表也不能解决所有问题。
数据查询 – Join 案例 剖析 Hint
MyCat 解决跨表 Join 的第三个方法:注解。
举例说明,A 表和 B 表在做 Join 的时候,前面加了一部分 hint,在里面写好用哪个类来处理。
这其实就是 next loopJoin 的方式。如果通过 MySQL 的 general log,或是根据 debug 去调试,就会发现这句 Join 在 MyCat 解析以后是分成两句下发的。
先从第一张表中 select 出结果集,再按照关联关系把结果集放在第二个表中拼接成新语句,然后再下发第二句 SQL,MyCat 实际是这样一个过程。
MyCat 这种操作方式存在什么问题?
第一,解决不了多于两个表 Join 的问题。
第二,无法解决复杂 Join 语句的问题,只能解决 A.id 等于 B.id 这两个表格列关系直接相等的情况,稍微改变形式就不行。
第三,侵入性。应用的开发需要在每个 Join 下的每个查询前拼接这样一个 hint,并且需要改应用,侵入性比较强。
所以 hint 表也解决不了所有的问题。
有趣的是 MyCat 1.6.5 之后,将 hint 方式直接固化到代码里,这样的处理方式实在不像是工程级别的代码,反而会引入更多的问题。
举例说明:这个 Join 内部其实偷偷在代码中加了 hint,如果是 MyCat 1.6.1 版本,直接结果不争取,加了 hint 以后有部分改善。根据测试,MyCat 的反馈结果并不稳定,有时会返回 NP 异常并且这个 NP 异常会影响当前 session 的正确性。
将 SQL 语句调整为查询:select a.id,a.description,b.title from travelrecord a inner join hotnews b on a.id =b.id;,B.id 变成 B.id+1, 这句 SQL,就无法返回正确结果了。受到前一个例子的影响,MyCat 的查询结果非常不稳定,即使使用新的连接,也会只返回空集,因为 MyCat 本身只是把 hint 固化到代码里,并没有良好的跨表 Join 的实现。
Tips:
MyCat 的内部实现十分粗糙,它判断是否要自己加 hint 采用的依据是拆分关的规则不一样。但是是否能做成 ER 关系有 2 个条件,是拆分规则以及分片结点的完全一致。
如果拆分规则相同,结点或结点顺序不同,返回来也是空集,此处就不举例说明了,感兴趣的同学可自行尝试验证。
1.2 数据操作
DBLE 与 MyCat 的 Insert 对比
在 Insert 的处理上 MyCat 的 insert 必须将列名完全写清楚,否则会报列名没有提供。而 DBLE 则更良好的兼容了 MySQL 的语法。
MyCat 某些时候会报告不正确的返回,比如 insert 拼写错误,它报错不会是语法错误,而是默默通过 SQL 语句,如果不仔细看行的影响数甚至都无法发现拼写错误。
MyCat 的全局序列自定义了一个语法,必须是 nextvalue for sth 才可以插入。
这个语法,对应用的业务开发者而言侵入性是非常强的,需要对应用做很多无法兼容的改造。
同样是全局序列,DBLE 的实现则比较优雅,支持不带自增列的插入,由中间件来生成自增列数据。
1.3 DBLE 与 MyCat 的上下文变量
除了 select 和 insert,以下将再列举部分系统变量的例子。
如图,表格中原来包含 4 条数据,现插入一行数据,然后将 session 的状态设置为只读,显示再继续增加一条数据也可以通过。
虽然能够通过 select 筛选出来,但实际上 MyCat 对于 set read only 并不支持并且没有任何报错。如果事先并没有了解 MyCat 这个功能缺陷或进行测试,这个问题是很难被发现的。
同样的案例,在 DBLE 中设置为只读后,再插入数据 DBLE 将会报错,如此才真正符合设置 session 级别变量的含义。
MyCat 为什么会出现这种情况?
再举一个有趣的例子,如图 MyCat 对于 set you =me,set 1=2 也返回 OK,似乎无所不能。而 DBLE 则会诚实的告诉你,这个变量不支持。
在使用过程中,如果存在不小心写错的情况 DBLE 会提供明确的报错,而 MyCat 什么 set 都返回 ok 的问题根因后面将详述。
2. 运维管理 - 用户权限
以管理端用户权限为例,任何数据库用户都可登录 MyCat 管理端进行高级操作,如:服务下线,修改配置等。因为缺乏对用户的分级,导致应用开发者本应只能进行查询或 DML 等基本权限,但却也可以进行服务下线类似的不安全操作,究其根源是项目开发者没有从权限管理的角度思考问题,也埋下了安全隐患。
在 DBLE 中,我们将此问题进行改进,对不同用户进行划分,普通用户不能直接登录管理端口进行操作,如图所示,普通用户尝试管理端口会遭到拒绝,更有利于安全。
以上的诸多案例都是站在 DBA 的角度来验证 MyCat 的正确性及其存在的问题,作为 MyCat 的增强版,DBLE 更多的以使用者的视角对一款中间件应当具备的正确性,安全性,稳定性,可运维性等方面进行了深度系统性的考量并持续完善相应功能特性,同时,我们也吸取经验对 MyCat 既存的问题也进行了增强与改进。
二.开发者角度
下面将从开发者的角度来分析 MyCat 的代码质量,让大家对于这个开源项目有更充分的认识。
概括而言,MyCat 存在以下四个问题:
代码修复质量差
代码半成品残留
部分提交者有灌水嫌疑
伪造实现
1.bug 修复质量
首先,bug 修复质量。MyCat bug #1194:在旧内存管理模式下,查询两个 avg,会报超索引超出界限异常。
上图为 MyCat bug #1194 在 GitHub 上的截图,bug 提供者发现 bug 和重现 bug,包括描述 bug 的逻辑都非常正确,实际上在 for 循环里删除了数据元素,然后导致下一个去处理的时候报错越界。
在修复上,如图,红色部分为删除的代码,绿色部分为对应增加的代码,仔细观察可发现中间部分被注释起来,没有实际作用,最关键的部分在最下方,仍然是在 for 循环中 remove 某一个索引的值。
为什么这个修复结果却是修复成功?
细敲其逻辑,实际上是不正确的。原因在于 for 循环里采用的是 int 类型的包装类,此时从数组中 remove 的不是某个索引的值,而是 remove 这个包装类对象,数组中根本不存在这个对象,因此实际上没有 remove 任何内容,而真正生效的是标记黄色的部分,将它的 size 减了一。
这种操作歪打正着,比如原有四个数组,正常情况下是将第三和第四数组 remove 掉,但现在没有 remove 成功,然后通过 size 4-1- 1 结果变成了 2。这时再去遍历此数组是通过 field count 来遍历的,序号为第三和第四的数组尽管没有删掉,但效果却和已经删掉的相同了。
bug #1194 的修复如果只进行测试会发现这个问题已经完美的解决了,但是作为开发者,我们对代码质量进行管理时会发现这样代码的存在十分奇怪,不但难以读懂难以理解并且很可能存在:为了性能放弃包装类改成 Java 的基本类型、int 类型,bug 就会被 reopen。
2. 代码半成品残留
上图为 MyCat 启动类的部分代码截图,从图中可了解这是一个 switch case 语句,case= 0 和 case 等于 1。其中 case 0 初始化了一个 buffer pool,然后初始化了 total buffer size,做了这两件事情;而 case 1 除了大段注释外,只初始化了 total buffer size,并没有初始化 buffer pool。这会发生什么情况呢?
当 pufferpooltype 设置为 1 时,会发现 MyCat 启动以后,客户端根本连不上,然后日志里面也全是 NP 异常。作为著名开源软件,在它的启动类上就存在这样的残留代码,我们能够相信它的质量吗?
我们相信 MyCat 当初设计时应该也设计了不同的实现,但没完成,这至少说明了没有一个固定的开发团队就没有人去处理类似很容易被发现的问题。
3. 代码灌水
我们在对 MyCat 做测试的时候,发现有部分代码覆盖率很低,于是去查看这部分代码实现了哪些功能。结果发现:代码质量非常高,但整个 package 都是从其他著名开源项目的某个版本 copy 过来的,当然也不算完全 copy,还是有加部分注释的。
这部分代码除了被贡献者自己的单元测试使用外,没有被任何其他人使用。即使把整个 package 连带测试完全删掉,也不影响软件的任何功能。
可能这位贡献者把 MyCat 项目当作自己学习笔记的笔记本或是能够展示自己贡献了很多代码?具体原因不得而知,不过这样的代码贡献也能被合并到项目里来,实在匪夷所思。
4. 伪造实现
前面我们列举了一个较为夸张的例子,写 set you = me 也显示成功执行。
set 语句为什么会出现这种情况?从源码角度来看,MyCat 枚举了几个特殊处理,比如 set names= utf8 确实进行了处理。但除了枚举的几个特殊的例子,其他无论 set 什么,MyCat 都直接返回 OK,因此你会看到前面 set you=me 也会得到 OK 的结果,这对于应用端而言是相当不负责任的。
尤其是遇到真的有意义的 set 语句,但却没有实现其语义,很容易造成开发事故。
DBLE 的自动化工具的引入
最后分享一下 DBLE 是如何进行代码管理和保证质量的,除了正常的 review 机制外,我们引入了很多自动化的工具,包括静态代码的分析工具,用于做代码规范的工具,可持续集成工具等。社区的 travis CI 会自动跑单元测试,如果代码变更发生错误,那么工具就会报错,这样也可以提高代码质量。
内外部使用的工具有稍许不同,我们内部用的可持续集成工具是 go cd,自动化的测试方面我们用 behave 做了一些行为的比较测试,之后可能也会开源出来。还有测试代码覆盖率的工具,帮助我们发现测试的薄弱环节等等。
“正文完”
以上分享对一款中间件的正确性进行了详尽充分的解读,而“安全性、稳定性、可运维性”以及如何评估开源中间件的代码质量、代码管理等都将在 ”DBLE 系列公开课 ” 一一为大家拆分揭秘。
DBLE 系列公开课
自 3 月 15 日起每周更新一节发布在「爱可生开源社区官网」,点击官网(http://opensource.actionsky.com)博客专栏,即可查看“DBLE 系列公开课”。
直播视频回顾请点击“录屏”,一键直达。
开源分布式中间件 DBLE 社区官网:https://opensource.actionsky….GitHub 主页:https://github.com/actiontech… 技术交流群:669663113
开源数据传输中间件 DTLE 社区官网:https://opensource.actionsky….GitHub 主页:https://github.com/actiontech… 技术交流群:852990221