为什么要用数据库中间件

严格的来说,数据库中间件的选择和 Servic Mesh 无关,一般公司很早就应该上数据库中间件了。

数据库中间件一般有两个方案:SDK 模式或者 Proxy 模式。SDK 模式性能更好,Proxy 模式兼容性更好。

既然我们都在往 Service Mesh 方向走了,就是不想在业务代码去接 SDK 了,所以 Proxy 模式是我们优先选择的方案。虽然延迟会高一点,但还是那句话,不要只盯着单次调用的延时。

那数据库中间件到底解决了哪些问题?一般来说,利用数据库中间件可以实现如下功能:

  1. 读写分离
  2. 分库分表
  3. 故障转移
  4. 动态配置
  5. 统计分析
  6. SQL 防火墙
  7. 查询缓存

之前在大众点评做 Zebra 的时候,主要的技术方案就是 SDK 模式,因为整个大众点评是 Java 技术栈,没有多语言的问题,所以用 SDK 模式可以尽量提高性能。

 

从 MySQL 到 Aurora

上面说了,数据库中间件在做 Service Mesh 之前就有这个需求了,但我们之前并没有做这块。原因就是 Amazon Aurora 太好用了。

以前在大众点评花了大量的时间做故障转移,但是 Aurora 直接全部帮你搞定了。

Aurora 是 AWS 基于 MySQL 魔改出来的,这篇文章可以一窥 Aurora 的架构设计:Amazon Aurora 是如何设计原生云关系型数据库的?

而读写分离和分库分表,我们因为只有一个单体程序,所以都是通过手写来实现的。并不是自动读写分离,自动分库分表。

 

新的挑战

虽然 Aurora 帮我们解决了故障转移这个最棘手的问题,但是别的还是要自己来做。微服务化后,很多轻量级的服务出现,它们只需要基本的读写分离功能,并不希望为了这个再去整合什么 SDK;另外我们涉及到的开发语言也不少。

能否用 Proxy 模式来解决这个问题?Proxy 模式的数据库中间件也是一个非常常见的技术方案。另外这两个方案也并不冲突,完全可以根据不同的业务类型来使用不同的技术方案。

而我们现在是快速迭代试错的阶段,所以找到一个满足我们需求的 Proxy 是第一位的。

 

MyCAT 与 ShardingSphere

因为之前是 Java 技术栈,那么自然先找到了两个 Java 开发的两大中间件了。

MyCAT 成名早,ShardingSphere 是后起之秀,知乎上有一篇两者的对比文章特别有意思:mycat和sharding-jdbc哪个比较好?各有什么优缺点?

说真的,的确受不了 MyCAT 的土味气息。而 ShardingSphere 就靠谱多了,首先是在京东有大规模实践,代码完全开源并归属于开源社区,开源运作方式也很标准。

最最重要的就是知乎上提到的,ShardingSphere 的官网和文档好太多了。

然而使用 ShardingSphere 的过程也没那么顺利。一开始就遇到了两个问题,我也给官方提了 issue。

The result of getBytes is wrong.

Does sharding-proxy support hint?

例如第一个问题,因为 JDBC 规范也挺熟悉了,所以尝试自己去修复一下,但看到他们代码内的Binary相关的 MySQL 字段都是处理错误的,心凉了半截。讲真代码还是有点乱的,他们Binary这块的逻辑并没有理得很清楚。

当时他们刚开源不久,也还没有加入 Apache 基金会,虽然现在上面两个问题都已经解决,但基于当时的情况,还是不敢用的。另外 Java 在容器化下的慢启动和高内存问题也是我不太敢用 Java 中间件的原因。

 

ProxySQL

后来继续搜寻相关中间件的时候偶然发现了 ProxySQL,于是决定尝试一下。

它可以解决上面提到了所有数据库中间件需要解决的问题。

然而,万万没想到,刚开始试用就遇上了 Bug:ProxySQL hangs after run set names 'binary'

如果不设置这个,MySQL 会认为'a' = 'ä',因为我们是面向全球用户的 APP,很多用户的用户名会用这些字符,这个参数是否设置对结果影响很大,可以看 MySQL 的官方文档:

我给作者提了 Issue,作者响应也非常快,一天内就给我反馈了。他问了我一些细节,也让我帮他抓包看看。

最后大概一个月过后,他把这个 Bug 修复了,并发布了新版本。

而在他修复 Bug 期间,我们其实已经用起来了,因为当时迁移的业务不包含这样子的特殊文本比较,所以不会命中这个 Bug。

整体的使用过程中是非常可靠的,因为 ProxySQL 是 C++ 写的,而且作为 Proxy 主要功能也只是做一些转发,所以 CPU 和内存消耗非常小,启动也非常快。

 

ProxySQL 配置

我们目前通过 ProxySQL 实现了读写分离,强制主库 Hint 和非法 SQL 拦截。这里可以给配置作为参考:

mysql_servers =
(
    {
        address = "[change to your mysql master server]" # no default, required
        port = 3306                                      # no default, required
        hostgroup = 0                                    # no default, required
        status = "ONLINE"                                # default: ONLINE
        weight = 1                                       # default: 1
        compression = 0                                  # default: 0
    },
    {
        address = "[change to your mysql slave server]"  # no default, required
        port = 3306                                      # no default, required
        hostgroup = 1                                    # no default, required
        status = "ONLINE"                                # default: ONLINE
        weight = 1                                       # default: 1
        compression = 0                                  # default: 0
    }
)

#defines MySQL Query Rules
mysql_query_rules:
(
    {
        rule_id=0
        active=1
        match_pattern="^\s*UPDATE (?!.[\s\S]*(where))"
        destination_hostgroup=1
        apply=0
        flagOUT=403
    },
    {
        rule_id=1
        active=1
        match_pattern="^\s*DELETE (?!.[\s\S]*(where))"
        destination_hostgroup=1
        apply=0
        flagOUT=403
    },
    {
        rule_id=2
        active=1
        match_pattern="^\s*/\*master\*/"
        destination_hostgroup=0
        apply=1
    },
    {
        rule_id=3
        active=1
        match_pattern="^\s*SELECT [\s\S]* FOR UPDATE$"
        destination_hostgroup=0
        apply=1
    },
    {
        rule_id=4
        ctive=1
        match_pattern="^\s*SELECT"
        destination_hostgroup=1
        apply=1
    },
    {
        rule_id=1001
        active=1
        apply=1
        flagIN=403
        error_msg="Query not allowed"
    }
)

前面两条规则是禁止使用没有WHEREUPDATEDELETE,这个是血泪史,有同事在线上出过事。还好当时那张表太大,没有执行完就赶紧终止掉了。不然就真是的从删库到跑路了。

第三条是支持强制走写库,因为 ProxySQL 实现了自动读写分离,业务不需要整合任何框架。但是有时候业务就是需要SELECT语句强制走写库,那么这时候只要在 SQL 语句前面加上注释就行了。

用法:/*master*/SELECT * FROM users LIMIT 1

第四条支持SELECT * FROM user FOR UPDATE强制走写库。这条 SQL 语句是在同一个事务中为了后续更新数据,读取数据并加锁的操作。

第五条规则就是默认所有SELECT走从库了。

最后一个是一个通用行为,第一条和第二条规则会跳转到这里。

 

ProxySQL 基准测试与性能调优

如果把它作为长久的方案,那跑一下符合我们环境的基准测试还是必须的。需要了解一下在有 Proxy 和没 Proxy 下的性能区别,各个版本 ProxySQL 的性能区别,还有不同负载下的性能区别。

这里我也做了个基于 Sysbench 的工具可以方便地做 ProxySQL 基准测试,对不同版本,不同参数的对比很有参考意义。

https://github.com/dozer47528/proxysql-benchmark

 

极限压力测试

首先测试一下不限制 Sysbench 的 rate,用尽全力去压测。

  • MySQL: CPU 2, Memory 4Gi, Max Connection 2048
  • ProxySQL: CPU 1, Memory 256Mi, Max Connection 2048
  • Sysbench: CPU 1, Memory 1Gi
Threads MySQL Min ProxySQL Min MySQL Avg ProxySQL Avg MySQL Max ProxySQL Max MySQL P95 ProxySQL P95
10 1.38 ms 5.65 ms 8.40 ms 8.63 ms 72.90 ms 215.26 ms 38.94 ms 13.22 ms
50 1.39 ms 5.60 ms 42.66 ms 44.50 ms 280.67 ms 224.27 ms 92.42 ms 82.96 ms
100 1.47 ms 5.54 ms 87.65 ms 87.22 ms 605.22 ms 504.15 ms 189.93 ms 155.80 ms
500 4.32 ms 13.20 ms 569.17 ms 436.58 ms 2751.15 ms 3829.34 ms 893.56 ms 831.46 ms

Full

从结果可以看出来并发量增加后最后的瓶颈都是 MySQL 了,并且随着并发量的增加,ProxySQL 的性能损耗基本是常数级别的,Avg 这一栏在并发数是 10,50 的时候都是慢 2ms 左右。

因为 ProxySQL 不会做额外的计算,所以不会因为 MySQL 压力大而影响自身性能。

但这种把 MySQL 往死里压的场景其实很少,只有 MySQL 故障的时候才会出现。

 

限制 Sysbench rate

平日里,MySQL 一般都不会是满负载运行,一般来说连接会很多,但大部分连接都不会一直在请求。所以我在这里尝试把 Sysbench 每秒请求速率控制一下,保证不把 MySQL 压垮。

  • MySQL: CPU 2, Memory 4Gi, Max Connection 2048
  • ProxySQL: CPU 1, Memory 256Mi, Max Connection 2048
  • Sysbench: CPU 1, Memory 1Gi, Rate 256/s
Threads MySQL Min ProxySQL Min MySQL Avg ProxySQL Avg MySQL Max ProxySQL Max MySQL P95 ProxySQL P95
10 3.24 ms 4.37 ms 3.82 ms 4.95 ms 33.06 ms 15.21 ms 4.33 ms 5.37 ms
50 3.23 ms 4.52 ms 3.84 ms 5.10 ms 12.34 ms 14.20 ms 4.33 ms 5.57 ms
100 3.24 ms 4.55 ms 3.82 ms 5.21 ms 11.91 ms 13.97 ms 4.41 ms 5.77 ms
500 3.25 ms 5.04 ms 3.87 ms 6.05 ms 12.26 ms 17.71 ms 4.49 ms 6.91 ms
1000 3.23 ms 5.73 ms 3.84 ms 7.56 ms 14.47 ms 17.80 ms 4.41 ms 8.90 ms
2000 3.21 ms 7.99 ms 3.84 ms 11.15 ms 48.86 ms 45.65 ms 4.41 ms 17.63 ms

Rate

在这样的压力下,MySQL 非常稳,而 ProxySQL 的性能却随着连接数的增加而变差了。良好设计后的数据库实例本身就应该由单一的几个业务访问,而不是让服务都能访问,所以在 1000 以下的连接数下的性能损耗完全是可以接受的。

是否可以尝试改善一下大量连接下的性能?ProxySQL 默认 4 个线程,Sysbench 并发高的话 4 个线程会不会太小?

 

尝试提升 ProxySQL 线程数

  • MySQL: CPU 2, Memory 4Gi, Max Connection 2048
  • ProxySQL: CPU 1, Memory 256Mi, Max Connection 2048, Threads 8
  • Sysbench: CPU 1, Memory 1Gi, Rate 256/s
Sysbench Threads ProxySQL Threads Min Avg Max P95
1000 4 5.73 ms 7.56 ms 17.80 ms 8.90 ms
1000 8 4.97 ms 6.01 ms 50.00 ms 6.91 ms
2000 4 7.99 ms 11.15 ms 45.65 ms 17.63 ms
2000 8 5.68 ms 7.54 ms 58.87 ms 9.91 ms

这里就出现了一个很有意思的现象了,除了 Max 外别的都降低了。

如果它并发做得好,在线程数大于 CPU 核心数的前提下,线程数越少越好。

官方的一个 Issue 也很好地解释了应该如何配置线程数:How can i find the correct number of mysql-threads

但为什么我配置的 CPU limits 是 1,加大了线程数却有效果呢?因为 Kubernetes limits 里配置的 1 不是给你一个核,而是指相当于一个核的 CPU 时间。

这篇文章讲解的很好:Kubernetes Container Resource Requirements — Part 2: CPU

举个例子就是在 1 秒内让一个工人给你工作 1 秒和在 1 秒内让 4 个工人分别给你工作 0.25 秒 的区别。

我的电脑是 8 核的 CPU,所以配置成 8 个线程后整体延迟下降了。但是,虽然 8 个核都可以用到,但都是残血的。所以有些线程跑到一半资源又被别的线程抢过去了,导致 Max 增加。

所以在容器化下跑这些东西还是要压测一下真实数据才更靠谱。

后面还要继续调优 ProxySQL 的话,可以看看官方文档,还有这里有些博客,也有很多介绍:MySQL-中间件:ProxySQL

 

后续

目前 ProxySQL 连接数在 1000 以下的情况下性能完全够用,后面我们要重度使用的话,还是要做一下性能调优的。

另外 ProxySQL 本身把所有配置写入了自己内置的一个数据库中,启动的时候可以读一份配置,运行的时候也可以直接修改。后面数据库实例多了,做一些管理工具是必须的。

还有 ProxySQL 自身监控数据也已经非常多了,但还是需要做一些整合,例如配合 Prometheus 和 Grafana,把它们呈现出来。

本作品由 Dozer 创作,采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。