Atlas是由奇虎360公发的基于MySQL协议的数据库中间件产品,它在MySQL官方推出的MySQL-Proxy
0.8.2版本的基础上,修改了若干Bug,并增加了很多功能特性。目前该产品在360内部得到了广泛应用,覆盖80%以上的MySQL业务,每天读写量达数十亿次,于GitHub开源后,业界几十家公司将其应用于生产环境。
Atlas项目源代码托管地址https://github.com/qihoo360/atlas
产品研发背景
该项目最早在2012年提出,主要为了解决两个问题:
一是业务程序员对数据库细节关注过多。在没有中间件的情况下,应用程序直接连接MySQL的主从库,需要在配置文件中指定主从库的IP和端口,并由业务程序员自行决定将写语句和读语句分别发往主库和从库,在数据量较大的情况下还需要自行管理分库分表等技术细节,使得业务程序员负担较重。
二是DBA运维工作不方便。数据库宕机是家常便饭,在需要切换或上下线数据库时,DBA需要协调业务程序员修改配置,运维操作也会对业务造成一定干扰,影响DBA工作顺利进行。
我们希望能将业务与DB进行一定程度的隔离,使业务程序员尽量少关心DB的细节,可以专注于编写业务逻辑;另一方面DBA的运维操作尽量做到对业务无影响。如此则需要将读写分离、分库分表、平滑上下线DB等逻辑提炼出来,以公共组件的方式提供给以上两类人群使用。从形式上看有两种,一种是独立的中间件服务,另一种集成于客户端的LIB。前者优点是升级更新较方便,而且与语言无关,缺点是网络增加一跳,数据需要转发,性能可能不如LIB形式,后者优缺点正相反。经过权衡我们选择了前者,除以上考虑外还有部分原因是我们团队与各条业务线是独立的部门,要推动业务工程师升级LIB有相当的困难。
开始时调研了MySQL官方的MySQL-Proxy、阿里巴巴的Cobar、Amoeba、TDDL等产品,其中MySQL-Proxy更新缓慢,长期停留在Alpha版阶段,Cobar安装部署相对复杂,Amoeba不支持事务,TDDL未完整开源。最终我们选择了MySQL-Proxy进行二次开发。
技术架构
部署架构
Atlas一般搭配LVS使用,客户端通过LVS访问Atlas,以避免单点故障,当然也可以使用Keepalived等。Atlas后面挂接MySQL的一主多从集群,需要注意的是主从同步需要DBA预先配置好,Atlas只涉及客户端与DB间的网络交互,DB与DB间的通讯与Atlas无关。
图1 读写分离架构
Atlas会把客户端发来的SQL语句根据读写分别路由到主库和从库上,对于有多台从库的情况,还会根据加权负载均衡策略决定选择哪台从库来执行每条读语句。
线程模型
Atlas的线程模型与Memcached相似,都是由一个主线程和若干个工作线程组成。主线程负责监听客户端的新连接,工作线程在系统启动时即创建完成,并监听管道读端的读事件。当有客户端连接到来时,主线程将客户端的IP和端口等信息封装成一个CON结构,然后选择某个工作线程来处理该客户端的请求。选择的方法可以是Round-Robin方式,也可以根据各个线程的负载情况选择负载最轻的线程(可以粗略认为待处理队列最短即为负载最轻)。主线程选定工作线程后,将CON结构放入该线程对应的待处理队列,然后向相应管道的写端写入一个字节(字节内容无所谓),则工作线程会收到管道读端的读事件,并从队列中取出CON结构,进行下一步处理。当然也可改用Linux的新系统调用eventfd实现对工作线程的唤醒,性能比管道更高。
图2 线程模型
两个经典技术问题
字符集
MySQL支持多种字符集,字符集状态与会话(连接)绑定,即各个连接上的字符集互不相干。而Atlas拥有连接池,要求实现连接复用,这就不可避免会导致字符集混乱。
图3 访问架构
如图3所示,客户端发来SET NAMES UTF8语句,Atlas从连接池里取出一个空闲连接(连接1),在连接1上执行该语句后,将连接1放回连接池。客户端再发来一条SELECT语句,Atlas又从连接池里取出一个空闲连接(连接2),连接2大概率与连接1不是同一个连接,在连接2上执行SELECT语句,而连接2并未设置UTF-8字符集,因此造成乱码。
图4 访问架构
解决方案:Atlas记录每个客户端和每个连接的当前字符集状态(以一个正整数表示即可)并进行相关修正。当客户端发来SQL语句时,Atlas会检查该语句是否要设置字符集(SET
NAMES或SET CHARACTER_XXX)。如某个客户端发来SET NAMES UTF8,Atlas从连接池内取出连接1并执行该语句,执行成功后,将该客户端和连接1的当前字符集均置为UTF-8。之后该客户端发来SELECT语句(或其他非SET类语句),Atlas从连接池内取出连接2,然后比较客户端的当前字符集(上步已设置为UTF-8)和连接2的当前字符集(假定为GBK),若一致则直接执行,若不一致则在SELECT前插入一条SET
NAMES UTF8,将两条语句一起发给MySQL,第一条语句起到将连接2的字符集修正为UTF-8的作用,然后再执行SELECT则不会乱码。当然也可以进一步优化,就是由Atlas直接返回SET类语句的结果(OK)给客户端,省去一次与MySQL的交互过程。
自验证
图5 连接认证
图5是MySQL的连接认证示意图。
1.Connect阶段,客户端建立与MySQL的TCP连接;
2.HandShake阶段,MySQL向客户端发送握手包,其中携带多项信息,如服务端的特性标志、字符集等,此处我们主要关心的是其包含的20个字节的随机串;
3.Auth阶段,客户端将握手包内的随机串取出,与自身的密码一起执行以下运算SHA1( password)
XOR SHA1( “20-bytes random data from server” SHA1( SHA1(
password ) ) ),得到一个加密串,加上客户端的用户名、特性标志、字符集等附加信息,组成认证包发给MySQL。
4.Auth-Result阶段,MySQL通过在mysql.user中保存的该用户名对应的密码,也执行同样的运算得到一个加密串,并与认证包中的加密串对比,若相同则认为密码正确,返回结果OK给客户端,否则返回ERR拒绝连接。
返回OK后应用层连接即建立完毕,接下来客户端向MySQL发送SQL语句,并从MySQL接收语句的执行结果,然后再发送下条语句……直至有一方断开连接为止。
图6 Atlas连接访问工作原理
图6是1.x版本的Atlas的工作过程。
可以看到,在连接认证阶段,Atlas简单地透传客户端与MySQL双方的通讯包。初看似乎并没有问题,但考虑到Atlas后面挂接的是一主多从多台MySQL时,情况就变得比较复杂。
客户端向Atlas建立连接时,Atlas需要选择转而向哪台MySQL建立连接。以最简单的一主一从为例:若Atlas向主库建连接,则从库上没有连接,无法实现读写分离;若向从库建连接,则问题更严重,因为主库上没有连接,所以写语句无法执行。那么能不能同时向两台MySQL建连接呢?问题在于两台MySQL都会发送握手包,而客户端遵循MySQL协议,它不可能一次接受两个握手包……
在1.x中,我们使用了一个取巧的办法,即在主库上预留一定数量的连接,比如32个。客户端向Atlas建立连接时,首先检查主库上的连接数是否已经达到32个,若未达到则向主库建连接,若已达到则向从库建连接。这样既保证了写语句可以在主库上执行,又使得读语句可以在从库上执行,从而正确实现读写分离。这个办法有几个潜在的问题:
1. 若有多于32个客户端同一瞬间发送写语句,则主库上连接数将不够用,当然也可以让某些客户端阻塞等待其他客户端让出连接来部分解决;
2. 无法支持长连接;
3. 在主库连接数达到32个之前,仍然无法读写分离。
仔细分析以上情况,可以得知,其本质原因是Atlas需要依赖客户端的连接动作转而向MySQL建连接,因此能建立连接的数量最大不超过客户端的Connect次数,这就极大地制约了Atlas作为中间件的灵活性。为了摆脱这一束缚,就要求Atlas能自主决定何时、向哪台MySQL、建立多少个连接。实现该特性的前提是,Atlas必须知悉客户端的用户名和密码,因为从1.x的交互图上可知,Atlas透传客户端的认证包给MySQL,而认证包内只有加密串而没有密码明文,也不可能反解(否则我们就破解了MySQL的加密协议)。
图7 Atlas 连接访问工作原理
图7是2.x版本Atlas的工作过程。
用户名和密码事先已经作为配置在Atlas启动时加载进来(当然也可以通过Atlas的管理接口进行动态的增删改查)。当客户端来连接Atlas时,Atlas自行产生一个握手包(包含20字节随机串)发给客户端,然后接收客户端发来的认证包,根据配置的用户名和密码进行加密计算并检查结果,根据结果正确与否返回OK或ERR给客户端,从而完成客户端的连接认证工作。客户端发来SQL语句时,Atlas检查连接池中是否有空闲连接,若有则直接使用,若没有则根据配置的用户名和密码自行向MySQL建立新连接,然后供该语句使用。
此方案的优点:真正完全的读写分离,支持长连接,连接按需建立,连接数随并发度上升而增加,随并发度下降而减少(可以依靠MySQL的wait_timeout,也可以由Atlas监控连接的空闲时间)。
小结
从以上内容可以看出,MySQL中间件产品的设计与开发需要对相关协议有比较深入的了解,且因MySQL数据库软件自身的升级,协议还可能随之改变。限于篇幅,还有不少功能点不能一一细述,有兴趣者可查阅项目源码托管平台上的资源。
|