案例 | 得物:彩虹桥架构演进之路-性能篇

eb5f2d841deae1d660fe4b14fcd02401.jpeg

目录

ShardingSphere

一、前言

二、改造前的架构

    1. Proxy 三层模块

    2. BIO 模式下的问题

三、改造后的架构

    1. 自研 NIO 数据库驱动

    2. 自研 NIO 数据库连接池

    3. 跳过不必要的编解码

    4. 全链路异步化

    5. 兼容性

四、性能表现

五、总结

01前言

一年前的《彩虹桥架构演进之路》侧重探讨了稳定性和功能性两个方向。在过去一年中,尽管业务需求不断增长且流量激增了数倍,彩虹桥仍保持着零故障的一个状态,算是不错的阶段性成果。而这次的架构演进,主要分享一下近期针对性能层面做的一些架构调整和优化。其中最大的调整就是 Proxy-DB 层的线程模式从 BIO 改造成了性能更好的 NIO。下面会详细介绍一下具体的改造细节以及做了哪些优化。

阅读本文预计需要 20~30 分钟,整体内容会有些枯燥难懂,建议阅读前先看一下上一篇彩虹桥架构演进的文章(彩虹桥架构演进之路)以及 MySQL 协议相关基础知识。

02  改造前架构

彩虹桥是基于 Apache ShardingSphere 深度二次开发的,本次改造主要是针对 ShardingSphere Proxy 模式的架构调整,主要改动的模块是 Backend 层,也就是与数据库交互的模块,从原来的 HikariCP+JDBC 改造成自研连接池&驱动,然后就是 Fronted->Core->Backend 层的主链路异步化改造。得益于 ShardingSphere 优秀的架构,ShardingSphere 内核层&功能层可以直接复用,下面是一些核心改造点的详细介绍。

先来复习一下彩虹桥的全景架构图:

ac29f5f5a30c92c7de31da333d31fde8.png

534826a6e5d2f69c0c524826dff35f26.jpeg

Proxy 三层模块

ShardingSphere

针对 Proxy 这一层,可以大致分成 Frontend、Core、Backend 三层:

Frontend-服务暴露层:使用 Netty 作为服务器,按照 MySQL 协议对接收&返回的数据进行编解码。

Core-功能&内核层:通过解析、改写、路由等内核能力实现数据分片、读写分离、影子库路由等核心功能。

Backend-底层DB交互层:通过 JDBC 实现与数据库交互、对结果集改列、归并等操作。

eb0729173fdcfddc72cfa528909baf51.jpeg

BIO模式下的问题

ShardingSphere

这里 Core 层为纯计算操作,而 Frontend、Backend 都涉及 IO 操作,Frontend 层使用 Netty 暴露服务为 NIO 模式,但是 Backend 使用了数据库厂商提供的传统 JDBC 驱动,为 BIO 模式。所以 Proxy 的整体架构还是 BIO 模式。在 BIO 模型中,每个连接都需要一个独立的线程来处理。这种模型有一些明显的缺点:

  • 高资源消耗:每个请求创建独立线程,伴随大量线程开销。线程切换与调度额外消耗 CPU。

  • 扩展性受限:受系统线程上限影响,处理大量并发连接时,性能急剧下降。

  • I/O阻塞:BIO 模型中,读/写操作均为阻塞型,导致线程无法执行其他任务,造成资源浪费。

  • 复杂的线程管理:线程管理和同步问题增加开发和维护难度。

我们看最简单的一个场景:在 JDBC 在发起请求后,当前线程会一直阻塞直到数据库返回数据,当出现大量慢查或者数据库出现故障时,会导致大量线程阻塞,最终雪崩。在上一篇彩虹桥架构演进文章中,我们做了一些改进来避免了 BIO 模型下的一些问题,比如使用线程池隔离来解决单库阻塞导致全局雪崩的问题。

7bf11c8da3a2e83963187cd32dea1267.png

但是随着逻辑库数量的增多,最终导致 Proxy 的线程数膨胀。系统的可伸缩性和吞吐量都受到了挑战。因此有必要将现有的基于 JDBC 驱动的阻塞式连接升级为采用 NIO(非阻塞 I/O)方式连接数据库。

03  改造后的架构

  • BIO->NIO

想把 Proxy 整体架构从 BIO->NIO,最简单的方式就是把传统的 BIO 数据库驱动 JDBC 换成 NIO 的数据库驱动,但是在调研过后发现开源的 NIO 驱动并不多,而且基本上没有什么最佳实践。最后在参考 ShardingSphere 社区之前做的调研后(https://github.com/apache/shardingsphere/issues/13957),决定使用 Vertx 来替换 JDBC。最开始使用 Vert.x 的原因,第一是 Vertx 的异步编码方式更友好,编码复杂度相对较低,第二是因为它实现了主流数据库的驱动。但最终的结果不尽人意,由于 Vertx 相关抽象化的架构,导致链路较长时,整个调用栈深非常夸张。最终压测出来的吞吐量提升只有 5% 不到,而且存在很多兼容性问题。于是推倒重来,决定自研数据库驱动和连接池。

  • 跳过不必要的编解码阶段

由于 JDBC 驱动会自动把 MySQL 的字节数据编解码成 Java 对象,然后 Proxy 再把这些结果集经过一些加工(元信息修正、结果集归并)后再进行编码返回给上游。如果自研驱动的话,就可以把编解码流程控制的更细致一些,把 Proxy 不需要加工的数据直接转发给上游,跳过无意义的编解码。后面会介绍一下哪些场景是不需要 Proxy 对结果集进行加工的。

ef325ccdf895d8339f586899cf1f6056.jpeg

自研NIO数据库驱动

ShardingSphere

数据库驱动主要是封装了与 DB 层交互协议,封装成高级 API。下面 2 张图是 java.sql 包中的 Connection 和 Statement 的一些核心接口。

54c371e3c3661f62a014c6e285a182ff.png

所以首先我们需要了解一下,如何与数据库进行数据交互,以 MySQL 为例,使用 Netty 连接 MySQL,简单的交互流程如下。

2dabf5802df605a9f6da87989e4d2d7b.png

使用 Netty 与 MySQL 连接建立后,我们要做的就是按照 MySQL 协议规定的数据格式,先鉴权后再发送具体的命令包即可。下面是 MySQL 官方文档中鉴权流程和命令执行流程:

  • 鉴权流程:

    https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html

  • 执行命令流程:

    https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase.html

下面就是按照 MySQL 的文档,去实现编解码 Handle,我们简单看一下实现的代码。

518b75c5dced575fd676377b8d2bd850.png

  • decode 解码

就是针对 MySQL 返回的数据包解码,根据长度解析出 Palyload 封装成 MySQLPacketPayload 传给对应的 Handle 处理。

  • encode 编码

把具体的命令类转换成具体的 MySQL 数据包,这里的 MySQLPacket 有多个实现类,跟 MySQL的Command 类型一一对应。

现在还需要一个类似 java.sql.Connection 的实现类,来组装 MySQLPacket 并写入到 Netty 通道中,并且解析编码后的 MySQLPacketPayload 转换成 ResultSet。

37c02e480b848e8f295bb12396750730.png

bf3544d7d0442742adab7404289a0d0a.png

看起来比较简单,交互流程和传统的 JDBC 几乎一样,但是由于现在是异步化流程,所有的 Response 都是通过回调返回,所以这里有 2 个难点:

由于 MySQL 在上一条命令没结束前无法接受新的命令,所以如何控制单个连接的命令串行化?

如何将 MySQL 返回的数据包和发起命令的 Request 一一绑定?

首先 NettyDbConnection 引入了一个无锁化非阻塞队列 ConcurrentLinkedQueue。

a34a63b19240cd728f7a47d22995c61c.png

在发送 Command 时,如何没有正在进行中的 Command,则直接发送,如果有正在进行中的 Command,直接扔到队列中,等待上一条 Command 处理完成后推动下一条命令的执行。保证了单个连接命令串行化。

其次,NettyDbConnection 在执行命令时,传入一个 Promise,在 MySQL 数据包全部返回后,这个 Promise 将会被设置完成,即可于发起命令的 Request 一一绑定。

4b3e56835b6e9963b2738611758e2b05.png

cd3b9368e01affc538494edc3b880986.jpeg

自研NIO数据库连接池

ShardingSphere

前面介绍了 NettyDbConnection 这个类,实现了与 MySQL 的交互,并且提供了执行 SQL 的高级 API,但实际使用过程中,不可能每次都创建一个连接执行完 SQL 就关闭。所以需要对 NettyDbConnection 进行池化,统一管理连接的生命周期。其功能类似于传统连接池 HikariCP,在完成基本能力的基础上,做了很多性能优化。

  • 连接生命周期管控

  • 连接池动态伸缩

  • 完善的监控

  • 连接异步保活

  • 超时控制

  • EventLoop 亲和性

这里除了 EventLoop 亲和性,其他几个功能只要用过传统的数据库连接池应该都比较熟悉,这里不做过多展开。这里主要针对 EventLoop 亲和性展开介绍一下。

在文章开头我们说到 Proxy 的三层模块,Frontend、Core、Backend,如果现在我们把 Backend 层于数据库交互的组件换成了我们自研的驱动,那么 Proxy 就即是Netty Server,也是Netty Client,所以 Frontend 和 Backend 可以共用一个 EventLoopGroup。为了降低线程上下文切换,在单个请求从 Frontend 接收、经过 Core 层计算后转发到 MySQL ,再到接收 MySQL 服务响应,以及最终的回写给 Client 端,这一些列操作尽量放在一个 EventLoop 线程中处理。

54685ec49a89eeccd946ccd5c874e960.png

具体的做法就是 Backend 在选择与数据库连接时,优先选择与当前 EventLoop 绑定的连接。也就是前面提到的 EventLoop 亲和性,这样就能保证大部分场景下一次请求从头到尾都由同一个 EventLoop 处理,下面我们看一下具体的代码实现。

在 NettyDbConnectionPool 类中使用一个 Map 存储连接池中的空闲连接,Key 为 EventLoop,Value 为当前 EventLoop 绑定的空闲连接队列。

669b114663e35b922934161ffa1e3cc7.png

在获取时,优先获取当前 EventLoop 绑定的连接,如果当前 EventLoop 未绑定连接,则会借用其他 EventLoop 的连接。

539862b90eb960f17bda9229bbc654b7.png

为了提高 EventLoop 命中率,需要注意几点配置:

  • EventLoop 线程数量尽量与 CPU 核心数保持一致。

  • 连接池最大连接数超过 EventLoop 线程数越多,EventLoop 命中率越高。

下面放一张压测环境(8C16G、连接池最大连接数 10~30)的命中率监控,大部分保持在 75% 左右。

fdea66eba4a79dcba8b333d017bde5a1.png

b61e54e3cd1fd0e08b62df6eca223816.jpeg

跳过不必要的编解码

ShardingSphere

前面说到,有部分 SQL 的结果集是不需要 Proxy 进行加工的,也就是可以直接把 MySQL 返回的数据流原封不动转发给上游,直接省去编解码操作。那什么 SQL 是不需要 Proxy 进行加工的呢,我们举个例子说明一下。

假设逻辑库 A 里面有一张表 User 做了分库,分了 2 个库 DB1 和 DB2,分片算法是 user_id%2。

  • SQL 1

SELECT id, name FROM user WHERE user_id in (1, 2)

  • SQL 2

SELECT id, name FROM user WHERE user_id in (1)

很显然 SQL 1由于有 2 个分片 Value,最终匹配到了 2 个节点,SQL 2 只会匹配到 1 个节点。

355c5ac123d489be41643e61563688b2.png

104b4140c1ce7a056dc14ce91d1f9b4b.jpeg

全链路异步化

ShardingSphere

Backend 层用自研连接池+驱动替换原先的 HikariCP+JDBC 后,从 Frontend-Core-Backend 全链路涉及到阻塞的操作需要全部替换成异步化编码,也就是通过 Netty 的 Promise 和 Future 来实现。

4ca327f8383de77ba129440ee93f78f8.png

由于部分场景拿到 Future 时,可能当前 Future 已经完成了,如果每次都是无脑的加 Listener 会让调用栈加长,所以我们定义了一个通用的工具类来处理 Future,即 future.isDone() 时直接执行,反之才会 addListener,最大化降低整个调用栈的深度。

52491b470e9c3b8140effe458e081249.png

4c8a9ed3bcc50d0341acbe1c6b5b141d.jpeg

兼容性

ShardingSphere

除了以上基本代码的改造外,还需要做大量的兼容工作:

  • 特殊数据库字段类型处理

  • JDBC URL 参数兼容

  • ThreadLocal 相关数据全部需要迁移至 ChannelHandlerContext 中

  • 日志 MDC、TraceContext 相关数据传递

  • ……

04  性能表现

经过几轮性能压测后,NIO架构相较于BIO架构性能有较大提升:

  • 整体最大吞吐量提升 67%

  • LOAD 下降 37% 左右

  • 高负载情况下 BIO 多次出现进程夯住现象,NIO 相对较稳定

  • 线程数减少 98% 左右

05  总结

NIO 架构的改造工作量相当巨大,中间也经历了一些曲折,但是最终的结果令人满意。得益于 ShardingShpere 本身内核层面的高性能加上本次 NIO 改造后,彩虹桥在 DAL 中间件性能层面基本上可以算是第一梯队了。

*文/新一

觉得文章对你有帮助,欢迎评论转发点赞~
本文由得物技术授权转载

如何加入 ShardingSphere 社区成为贡献者?

  1. 社区答疑:积极在社区中进行答疑、分享技术、帮助群内的其他开源爱好者解决问题。

  2. 代码贡献:社区整理了简单且容易上手的任务,非常适合新人做代码贡献。可以查阅新手任务列表:

  3. https://github.com/apache/shardingsphere/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22%2Cdiscussion+no%3Aassignee

  4. 内容贡献:发布 ShardingSphere 相关的内容,比如安装部署教程、使用经验、案例实践等,形式不限,欢迎扫码投稿给社区助手。

  5. 社区布道:积极参与社区活动、成为社区志愿者、帮助社区宣传、为社区发展提供有效建议等。

  6. 官方文档贡献:发现文档的不足、优化文档,持续更新文档等方式参与社区贡献。通过文档贡献,让开发者熟悉如何提交 PR 和真正参与到社区的建设。

97fdc82f8448bf43d751dd5e798033b1.jpeg

长按识别回复“志愿者”了解更多吧~

关于 Apache ShardingSphere

Apache ShardingSphere 是一款分布式 SQL 事务和查询引擎,可通过数据分片、弹性伸缩、加密等能力对任意数据库进行增强。

点击阅读原文了解更多~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,根据您提供的信息,我为您制定一份旅游计划如下: 第一天:长沙 早上到达长沙后,您可以先前往岳麓山欣赏美丽的日出和清晨的城市风光。接着您可以前往长沙的标志性建筑岳阳楼,领略楚河汉街的历史文化。中午可以品尝长沙的特色小吃,例如口味独特的臭豆腐和爽口的米粉汤。下午可以前往岳麓山下的橘子洲头,留下美丽的照片。晚上可以在岳麓山呼吸清新空气,感受长沙的夜生活。 第二天:凤凰古城 早上从长沙出发前往凤凰古城,途中可以欣赏美丽的桃花源风景。抵达凤凰古城后,您可以游览世界文化遗产凤凰古城,并以其红色砖墙,乌篱岭的石板路,清晨的古韵漫步其中。中午可以品尝当地美食清明饮水、糯米鸡等。下午可以前往周边的黄帝故里、明清古屋、玉华洞、龙潭景区等旅游景点。晚上可以尽情享受凤凰古城的美丽夜景。 第三天:张家界 早上从凤凰古城出发前往张家界,途中可以欣赏天门山的美丽风景。抵达张家界之后,可以先去张家界国家森林公园游览。中午可以品尝当地美食,例如石锅鱼和张家界蕨菜粉。下午可以前往天门山,感受玻璃栈道和玻璃桥的惊险之旅,还可以坐缆车欣赏天门山的壮观风景。晚上在张家界城区寻找当地特色小吃,例如平价小吃、黄袍山煎饼等。 第四天:岳阳 早上从张家界出发前往岳阳市,途中可以欣赏洞庭湖的美丽风景。抵达岳阳市后,可以去岳阳楼游览,并尝试切歌词,感受湘菜文化的独特魅力。中午可以品尝当地特色——岳阳米粉。下午可以前往岳阳楼下的君山岛,万绿山葱、百花争鸣,内有二王庙、三清宫和长田彩虹桥、灵泉谷等景点。晚上可以到岳阳楼边的湘江酒吧感受湘江风情。 以上就是我为您制定的四天自驾旅游计划,希望对您有所帮助。如有需要,可以随时联系我。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值