ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Atlas数据库中间件:分库分表与读写分离实战解析

Atlas数据库中间件:分库分表与读写分离实战解析

先聊聊我为什么写这篇东西。前阵子帮一个做电商的朋友排查线上慢SQL,发现他们还在用业务代码里自己拼分表逻辑,拼得想骂人。我说你们为什么不上中间件?他说怕踩坑。后来我帮他梳理了一遍 Atlas 的路由和配置,他自己把分库分表接完了,才反应过来原来数据库代理层没那么玄乎。

这篇文章聊的 Atlas,是开源社区里那套基于 MySQL 协议做数据库代理的中间件。不是地图集,也不是波士顿动力的机器人。它主要解决三件事:分库分表的自动路由、读写分离、数据库连接池的统一管理。如果你正在面临单表数据量过大、读多写少、库表拆分后应用层越来越难管这类问题,这篇文章应该对你有用。

Atlas 在技术圈里其实有年头了。早期由奇虎360团队开源,后来不少大厂基于它做过二次开发并持续贡献代码,社区也积累了大量生产案例。我一直觉得,它最大的价值不是在某个特定版本里炫技,而是提供了一套“应用无感知”的数据库访问方案:业务侧不需要改SQL,只需要把数据库连接地址指向 Atlas,剩下的路由和分发由代理来完成。

这套东西适合谁?适合后端开发、DBA、架构师。尤其是那些已经在拆库拆表,但发现代码里到处都是按ID取模、到处手动切换数据源的项目组。如果你还没到那个阶段,也可以先了解下代理层的工作原理,至少在方案选型的时候不吃亏。

1. Atlas 到底解决什么问题:从单体数据库到代理层

1.1 分库分表后,应用层为什么越来越难写

在有 Atlas 这类中间件之前,业务代码要玩分库分表,基本是自己在DAO层写路由规则。订单表按买家ID取模拆成 16 张,查询的时候先算一下 userId % 16 落在哪张表;后台管理要全表统计,就只能遍历所有分片再聚合。这还只是查询,等到做事务、做批量插入、做分页排序的时候,那代码基本没法看了。

我自己见过最夸张的项目,一个用户查询方法里嵌套了三层switch-case,每层对应一个分库策略,还专门写了一个工具类解析分片键。后来产品和运营要加新维度查询,开发说改不动了,因为路由逻辑已经跟业务SQL耦合死了。这类痛点的本质是:路由规则本身是一个独立的系统问题,不应该散落在业务代码里。Atlas 这类数据库代理把路由规则收敛到配置层,让应用只管连接,SQL正常写,这就是它的核心价值。

1.2 代理层 vs 应用层路由 vs 新一代数据库

说到方案选型,很多人会问:为什么不用应用层路由?为什么不用 ShardingSphere?为什么不用 NewSQL?我的理解是这样的。

应用层路由(就是自己写路由规则)最灵活,但代价是每个团队都要维护一套自己的规则引擎,而且SQL写法会被强制约束。Atlas 这种代理层的方案是把路由做在客户端和数据库之间,业务侧几乎无感,部署和运维集中在中间件上。它的缺点是功能边界比完备的中间件生态要小一些,比如跨分片事务、跨分片JOIN这些能力有限,所以团队必须约定SQL规范。

ShardingSphere 走的是另一种路,它可以在应用内(ShardingSphere-JDBC)或中间层(ShardingSphere-Proxy)工作,功能更丰富,生态也更完整。但对应的学习成本和配置复杂度也更高。NewSQL(比如TiDB)则是彻底换数据库,如果你能接受技术栈更替,那自然是最省心的,但很多遗留系统根本不可能在短期内把 MySQL 换掉。

Atlas 最适合的场景,恰恰是那种“暂时还不想换库、又不想把路由逻辑写进业务代码”的过渡期。它能帮团队把分库分表的事情从代码里抽出来,让业务先跑顺,后续再决定要不要往更重的方案迁移。

1.3 它做不到的事:先认清楚边界

用 Atlas 之前,有几个边界必须搞清楚,不然上线就是坑。

第一,它不适合做跨分片的复杂查询优化。代理层能做的是根据分片键把SQL路由到正确的分片,但如果一个查询没有带分片键,代理只能把SQL广播到所有分片,再把结果合并回来。并发一大,代理本身可能成为瓶颈。

第二,跨分片事务是硬伤。Atlas 解决的是单分片内的写操作,做不到多个分片间的分布式事务。业务设计时得把事务尽量控制在单分片内。

第三,它不改变SQL语义。代理层不是查询优化器,不会因为你SQL写得烂就把性能变好。慢SQL该优化还是要优化。

把这些边界想清楚,你才能判断 Atlas 适不适合你的场景。

2. 部署与核心配置拆解:从零搭起一套可用环境

2.1 部署架构与前置条件

Atlas 的部署形态不复杂。它本质是一个独立进程,对外提供一个 MySQL 协议端口,客户端连接它就像连接 MySQL 一样。后端连接到真实的 MySQL 实例(或主从集群)。最少情况下,一台 4C8G 的虚拟机就能跑一个 Atlas 实例,但生产环境建议至少两台,配合 LVS 或 Keepalived 做高可用。

从我的实践来看,部署前先把下面几件事做好:

  • MySQL 主从要提前搭好,主库负责写、从库负责读,至少保证主从复制正常。
  • 分片表结构要提前在多个库/多张表上建好,Atlas 不会自动帮你建表。
  • 确定分片键和分片策略。订单表用 order_id 做 hash 还是 range,要在配置前定清楚,因为上线后改分片规则代价很大。

2.2 main.conf 逐项解读:路由规则、账号体系与连接池

安装完成后,核心配置文件在 conf/main.conf。我用一个最小可运行的配置来逐项说明:

[mysql-proxy] proxy-address = 0.0.0.0:1234 admin-address = 0.0.0.0:2345 admin-username = admin admin-password = admin proxy-backend-addresses = 192.168.1.10:3306 proxy-read-only-backend-addresses = 192.168.1.11:3306,192.168.1.12:3306 pwds = report:******** daemon = true proxy-pool-no-change = true proxy-range-tables = order_info:order_id:0-9999,10000-19999,20000-29999 proxy-hash-tables = user_info:user_id:0-3 log-path = /var/log/mysql-proxy log-level = debug

几个关键项我重点说一下。proxy-address 是应用连接的地址,端口随意但别跟现有服务冲突。proxy-backend-addresses 是写库,proxy-read-only-backend-addresses 是只读从库,多个从库用逗号分隔,Atlas 会做负载均衡。pwds 这里填的是“用户名:加密密码”,不是明文,密码需要用 bin/encrypt 工具生成,这个细节经常有人忽略。

分片规则写起来很直观。proxy-range-tables 表示按范围分片,order_info 表按 order_id 分成 0-9999、10000-19999 三段,分别对应三个分片(配置顺序对应后端库顺序)。proxy-hash-tables 表示按哈希取模,user_info 按 user_id 对 4 取模分成四片。这里要注意,range 和 hash 只能二选一,同一张表不能同时配两种规则。

接下来是连接池。proxy-pool-no-change = true 这个参数的意思是让连接池不自动调整后端连接数。我建议生产环境保持这个配置,因为自动调整在一些版本里会导致连接数不稳定。

启动服务用 service mysql-proxy start 或者直接执行 /usr/local/mysql-proxy/bin/mysql-proxy 都可以。然后用 mysql 客户端连一下:

mysql -h 127.0.0.1 -P 1234 -u report -p

能正常执行 SELECT 1,说明代理已经通了。

2.3 高可用方案:LVS + Keepalived 的接入思路

单机 Atlas 一旦宕机,所有业务数据库访问都会挂掉,所以生产环境必须考虑高可用。我常用的方案是两台 Atlas 实例 + Keepalived 提供 VIP,应用层只连 VIP,不感知后端的 Atlas 切换。

Keepalived 做的是虚拟IP漂移,配置上跟数据库本身关系不大,核心是保证同一时间只有一台 Atlas 在工作。需要注意两件事:一是 Atlas 的配置要在两台机器上保持一致,尤其是 pwds 加密串和分片规则;二是切换后旧实例上的 MySQL 连接还挂着,需要做好连接超时时间控制,否则切换瞬间应用会有一批报错。

如果你已经有 LVS 基础设施,也可以让 LVS 直接把流量分发给两台 Atlas。Atlas 本身是无状态的,它不保存会话状态,理论上可以水平扩展。但我实测下来,连接数比较高的场景还是 Keepalived 主备模式最稳,故障切换时间短,也不会出现多实例同时写的问题。

3. 关键机制实现与业务侧接入:让应用真正跑起来

3.1 分表规则设计:从订单表说起

路由规则的设计是整个接入过程中最需要花心思的地方。我先用一个电商订单表的例子来拆。

假设订单表 order_info 月新增千万级数据,单表已经扛不住,我们决定拆成 32 张物理表,分布在 4 个库上。分片键选什么?绝大多数情况下选订单号或者买家ID,选型原则是:这条业务数据访问时是否总能带得上这个字段。如果用户下单后只能拿到 order_id,那就用 order_id 做分片键;如果管理员后台需要按商户查订单,还得有 mch_id,那就得权衡一下到底是按买家维度分片还是按商户维度分片。

按 hash 取模配置就是:

proxy-hash-tables = order_info:order_id:0-31

Atlas 会解析 INSERT / UPDATE / DELETE / SELECT 语句里的 order_id 条件,算出模值,路由到对应分片。注意一点:SQL里必须带着分片键条件,而且条件要是等值条件。如果写成 order_id IN (1,2,3,4),Atlas 也能处理,但如果是 order_id > 1000 这种范围查询,走 hash 分片时会广播到所有分片,性能就比较难看了。

3.2 读写分离与多从库负载均衡

Atlas 的读写分离判断很简单:SELECT 走从库,INSERT / UPDATE / DELETE 走主库,事务内语句强制走主库。这个逻辑听起来简单,实际操作中有几个关键细节很容易踩。

事务内强制走主库是必须的。如果事务里第一条是 SELECT 路由到从库,然后 UPDATE 走主库,主从延迟时就会出现读到旧数据或者干脆读不到已更新数据。Atlas 一旦发现客户端发了 BEGIN,整个连接后续语句都锁定在主库,直到 COMMIT / ROLLBACK。所以业务侧要有这个意识:长时间挂起的事务会一直占用主库连接,导致主库连接池被打满。

多个从库的负载均衡是自动做的,但算法是轮询还是最小时延,不同版本有差异。我的经验是:如果你有大量重查询(报表、批量导出)在从库上跑,最好把这类流量隔离开,不要把重查询和在线业务复用同一组从库,否则负载均衡再均匀也扛不住慢SQL堆积。

3.3 业务侧接入注意事项:语法限制与SLA约定

接入 Atlas 不是改个连接串就完事了,团队内部得先定一套 SQL 规范。我总结了几条硬约束:

  • 所有路由相关的查询都带分片键,且使用等值条件。
  • 禁止跨分片 JOIN。确需跨分片的数据,由应用层做二次组装。
  • 事务边界要小,尽量控制在单分片内。
  • 避免 SELECT * 不带 WHERE,这类 SQL 会被广播到全部分片。
  • 存储过程、自定义函数、临时表等特性在 Atlas 层支持很有限,尽量不用。

这些约束看起来多,但实际执行下来,对业务影响最大的就是第1条和第3条。很多团队在接入时没把这套规范讲清楚,上线后才开始返工。

4. 常见问题与排障实录:运行中踩过的坑

4.1 连接数暴涨,Atlas 直接拒绝服务

我遇到过最典型的问题是应用连接数突然暴涨,Atlas 报 “too many connections”。这个问题的根子往往不在 Atlas,而在应用侧没有管理好连接池。很多应用框架默认连接池不够用时会新建连接,一旦后端接口变慢,应用就会疯狂创建新连接,把 Atlas 的连接数打满。

排查思路分三步:先在 Atlas 管理端口执行 SELECT * FROM status 看当前连接数;再去应用侧看连接池配置,把最大连接数收敛到一个合理范围;最后看后端MySQL的 max_connections,确认没被顶到上限。Atlas 本身的连接队列容量也可以调大,但不能只调这个,治标不治本。

4.2 分页排序结果不对:跨分片 SQL 的隐藏坑

有段时间运营反馈后台分页数据偶尔会重复,查了很久才发现问题出在跨分片 ORDER BY + LIMIT 上。Atlas 会把一条带 ORDER BY 和 LIMIT 的SQL广播到多个分片,每个分片各自排序取前 N 条,然后由代理合并排序后再取前 N 条。听起来没问题,但如果排序字段不是唯一键,或者数据分布不均匀,合并后的结果和真实全局排序就会不一致。

这种情况的解决办法,要么是改业务设计,避免跨分片全局排序;要么是让应用层自己把每个分片的TOP N拉回来再合并。两种方案都不完美,但至少要知道根因在哪。

4.3 主从延迟带来的读不到数据

读写分离后经常听到的一句话是:数据刚写完就读不到。这基本就是主从延迟。Atlas 本身不解决延迟问题,它只负责把读流量分到从库。要缓解这个问题,可以从几个方向入手:核心写后读场景,业务层强制走主库;对延迟敏感度低的查询,可以在应用层做个小的延迟重试;监控从库 Seconds_Behind_Master,超过阈值时临时把这台从库剔除出读组。

管理端口操作后端状态很简单:

SET OFFLINE 1; SET ONLINE 1;

把故障或延迟的从库先摘掉,恢复后再上线,这个操作在 Atlas 管理接口里都有。要注意的是,SET OFFLINE 会断开该后端上的现有连接,尽量在业务低峰期操作。

4.4 监控与告警:Atlas 性能指标的观测方法

最后聊聊监控。Atlas 管理端口提供了不少状态信息,我常用的几条命令是:

SELECT * FROM status; SELECT * FROM backends; SELECT * FROM help;

status 里能看到当前连接数、请求总数、慢查询数等统计,backends 里能看到每个后端的状态。生产环境建议把日志级别开成 debug 也不怕,只要定期清理日志就行,因为 debug 日志在异常排查时是唯一能还原现场的东西。慢SQL记录在 log-path 下,可以用来反推业务侧的SQL问题。

我自己的监控告警习惯是:每5分钟采集一次连接数、请求量、后端状态,对连接数设置95%容量告警,对后端状态设置非ONLINE即告警。

Atlas 适合作为学习数据库代理的第一站。它的源码不复杂,读完你能理解SQL解析、路由、读写分离这些概念是怎么落地的。如果后面你们团队真要搞大规模分库分表加分布式事务,基于 Atlas 学到的这一套底层逻辑,再去看 ShardingSphere、TiDB 这些方案会轻松很多。我个人在实际接入中的体会是:选型的关键不在于谁功能多,而在于你的团队能不能在它设定的边界内写出清楚SQL规范。一个小建议是正式上线前拉一次全量SQL评审,把不带分片键的SQL全部找出来先改掉,比上线后再补规范省太多事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进