MySQL Router架构实践

    MySQL Router作为InnoDB Cluster(MySQL 7.X)的一部分,它是一个轻量级的中间,可以在Application与下游的MySQL Server之间提供透明的路由方式,它主要用以解决数据库主从库集群的高可用、易于扩展性等。

    MySQL Router可以脱离InnoDB Cluster而单独实施,即MySQL 5.6等版本数据库仍然可以使用Router作为其中间代理曾,Router的核心原理为

    1、Router作为一个流量转发层,它的架构层面,位于Application与MySQL Servers之间。

    2、其功能角色,类似于Nginx、LVS等,MySQL Servers作为Router的“upstream”(NAT模式);Application不再直连MySQL Servers,而是与Router相连。根据Router的配置,将会把应用程序的READ、WRITE请求转发给下游的MySQL Servers。

    3、当下游有多个MySQL Servers,无论主、从,那么它可以对READ、WRITE请求进行负载均衡。(loadbalance)

    4、当下游某个Server失效时,Router可以将其从Active列表中移除,当其online后再次加入Active列表,即提供了Failover特性。

    5、当MySQL Servers集群拓扑变更时,比如增减Slaves节点,我们只需要修改Router的配置即可,无需修改Application中JDBC URL配置,因为Application配置的为Router地址而非MySQL Servers的原始地址;即“数据库集群迁移”对Application是透明的。

    6、Router支持集中式部署,即一个Group通常有多个Router节点,但是MySQL官方并没有提供集群的HA,即Router每个节点均为独立,它们之间互不通信,无Leader角色,无选举机制。那么当某个Router节点失效,Application层面需要借助MySQL Connector的高级特性,比如:failover、loadbalance等协议来实现Failover功能。简单而言,Router中间件与Connector的高级协议互相协作,才能够实现请求在Router集群之间的负载均衡、Failover等。

    7、Router中间件,本身不会对请求“拆包”(unpackage),所以我们无法在Router中间件上实现比如“SQL审计”、“隔离”、“限流”、“分库分表”等。但是Router提供了plugin机制,你可以开发自己的plugin来扩展Router的额外特性。(C语言)

    8、如果你的MySQL Servers为5.7+版本,且构建为InnoDB Cluster模式,那么Router还能基于metaCache(metaServers)机制,感知MySQL Servers的主从切换、从库增减等集群拓扑变更,而且基于变更能够实现Master自动切换、Slaves列表自动装配等。比如Master失效后,Cluster将会自动选举一个新的Master,此时Router不需要任何调整、可以自动发现此新Master进而继续为Application服务。

    9、考虑到Router集中式部署可能引入“额外的部署成本”、“性能降级”、“连接数上限”等问题,我们通常建议大家基于“Agent”方式部署,即部署在Application宿主机器上,潜在的问题就是自动化运维设施需要即备。

    10、Router通常是解决“MySQL集群规模性迁移”:比如跨机房部署、流量迁移、异构兼容,或者解决MySQL集群规模性宕机时快速切换等。如果仅仅是为了解决日常的节点增减、读写分离、Failover等,我们仍然建议使用mysql-connector-j支持的“replication”、“loadbalance”协议来实现,基于客户端,而且轻量级。

    MySQL Router目前已知的“局限性”:

    1、不支持比如“分库分表”、“SQL审计”等。

    2、在非InnoDB Cluster架构模式下,如果主从库拓扑变更,需要手动修改Router配置。且Router不支持“reload”,修改配置后需要重启,这在一定程度上会影响Application的服务可用性。但是Router的重启非常快,我们通过验证,通常在秒级别。(5S)。

    3、MySQL Router非常轻量级,与直连Servers相比,其性能损耗低于1%,我们通过压力测试,100W读写请求总耗时只增长了200多秒。不过摆在Router面前的问题,就是其对链接数的支撑能力,原则上我们一个Router节点限定在500个TCP链接。Router本身CPU、内存、磁盘消耗都极低,但是我们要求Router节点对网络IO的支撑能力应该较强,考虑到Router底层为“异步IO”,如果条件允许,我们应该构建在较高版本的Linux平台下,且给予合理的CPU资源。(目前我们线上为8Core、16G,万兆网卡)。

    备注:MySQL Router在2.1.4版本以下,内核基于select() IO模型,存在连接数500上限、较大SQL请求导致CPU过高,以及并发连接过高时Router假死等问题,建议升级到2.1.6+。

    4、Router对连接的管理是基于“粘性”方式,即Application与Router的一个TCP连接,将对应一个Router与MySQL Server的连接,当Application与Router的连接失效时,Router也将断开其与MySQL Server的连接;只要Router上下游网络联通性正常,那么Router将不会主动断开与Application的连接,也不会切换其与Server的连接。即当Application与Router创建一个新连接时,Router将根据负载均衡算法,选择一个Server并与其建立连接,此后将唯一绑定,直到此Server失效时触发重新选择其他Server。

    这就引入一个问题,如果某个连接上发生了“繁重”的SQL操作,那么将会导致下游Server伴随高负载而无法“负载均衡”,或许这就是基于TCP NAT代理模式的通病吧。比较有幸的是,我们通常使用DataSource Pool,且MySQL Connector在面对这样的问题时,也有相应的解决办法,参见下文“loadBalanceAutoCommitStatementThreshold”。

    5、不能设定权重,即按照权重负载均衡。

     MySQL Router部署方案参考(非InnoDB Cluster模式)

 
MySQL Router架构实践
     针对一组MySQL M-S架构集群,通过有一个Master和多个Slave:

    1、针对Master,我们只需要一个Router集群即可,此集群接收WRITE操作。(当然也可以接收读)对于应用而言,它们统一将WRITE操作发给此Router集群,建议三个Router节点。(主要为了避免一个Router节点失效后,至少还有2个节点承载服务,当然节点数量应该与MySQL Server的请求量来综合权衡)

    2、考虑到不同的Application可能使用不同的slaves,即按照常规设计,不同的Application使用不同的slaves也是考虑隔离,比如某个Application因为操作不当(慢SQL)不至于将所有的slaves都压垮。那么对于Router集群而言,我们也可以按照Application或者业务组来分割,隔离力度因人而异,建议Router集群的数量不要太多,否则对Router也是浪费、此外运维Router也是一个比较繁杂的事情。

     针对Slaves的Router集群,每个集群建议2个节点,当然节点数可以根据slaves个数、请求量等综合考虑。

    Router配置参考(${mysqlrouter}/mysqlrouter.conf)

[DEFAULT]
logging_folder=/data/mysql-router/log
runtime_folder=/data/mysql-router/run
data_folder=/data/mysql-router/data

[logger]
level = INFO
[routing:router_rw]
bind_address=0.0.0.0
bind_port=3306
destinations=127.0.0.1:3306,127.0.0.1:3406
client_connect_timeout=6
connect_timeout=3
max_connections=2048
##如果你的master为单节点master,可以使用read-only模式
##当需要master切换时,手动修改此配置文件并重启
mode=read-write
protocol=classic

[routing:router_ro]
bind_address=0.0.0.0
bind_port=4306
destinations=127.0.0.1:4306,127.0.0.1:4307
client_connect_timeout=6
connect_timeout=3
max_connections=1024
mode=read-only
protocol=classic

     对于READ、WRITE两种操作,对应两种“mode”,因为Router不对请求拆包,所以它无法判断请求的读写类型,我们只能在配置文件中,分别为读写设定不同的配置:读写使用绑定不同的端口,比如示例中“3306”端口接收到的请求都会转发给master,“4306”端口接收的请求会转发给slaves。当然具体“destinations”中是master还是slave地址,由你来决定,最好保持对应。

    原则上,“destinations”中可以指定多个Servers地址,以“,”分割。

    1、对于read-write模式:将采用“首个可用”算法,优先使用第一个server,当第一个server(即3306)不可达时,将会Failover到第二个server(3307);依次进行。如果都不可达,那么此端口上的请求将会被中断,不可用,且此时Router将不可用(主要是此port将不能服务)。(需要注意,此算法只遍历一次列表,即逐个验证destinations中的Server,不会循环)

       特别注意,一旦所有的Servers依次验证且不可用后,Router将不能继续服务,内装状态设定为aborted,即使此后Servers恢复上线,也不能继续对Client提供服务,因为它不会与Servers保持心跳检测;对于Router而言,直接拒绝Client连接请求,只有重启Router节点才能解决。(我认为这是个Router完全可以解决的问题,不知道为什么却如此设计)

    2、对于read-only模式:将采用“轮询”算法,依次选择server新建连接,如果某个Server不可达,将会重试下一个Server,如果所有的Server都不可达,那么此端口上的请求将中断,即READ操作将不可用。同时Router将会持续与每个Server保持心跳探测,当恢复后重新加入Active列表,此后那些新建连接请求将可以分发给此Server。

    但是比较遗憾,Router不会将已有的连接重新分配给“新加入”列表的Server,比如Router有2个Server地址(S1,S2),某时刻S1不可达,那么在S1上粘性的客户端连接也将被断开,新建连接将会全部在S2上,此后S1恢复正常,那么在S2上的旧的连接将不会迁移到S1上,此时S1只会接收新的连接,如果没有新连接请求,那么S1将会在一段时间看起来是“不提供服务”的。为了解决此问题,我们要求Connection Pool有管理“连接生命周期”的相关控制,比如一个connection被创建X秒以后在返回连接池时应该被主动关闭,这个参数在tomcat-jdbc-pool中为“maxAge”。

    如果你的应用程序中,部署方式是单Master、多Slaves(而不是多Master方式或者metaCache + MySQL Cluster架构),我们完全可以在承接“master”请求的Router节点上,也配置为“read-only”模式,那么此单master节点失效重启后,可以不需要重启Router节点即可继续服务。

    因为Router不会对TPC拆包,所有“read-write”、“read-only”并不会干扰实际的SQL执行结果,严格来说,这两种mode映射两种“路由算法”:“首个可用”、“轮询”;除此之外,再无特殊含义。(仅限:单Master、多Slave模式,常规HA架构)

    Application接入方案

    1、mysql-connector-j版本至少在5.1.27以上,建议为5.1.43+。短期内不建议升级为mysql-connector-j 8.x。

    2、连接池组件,优选tomcat-jdbc-pool,建议为8.0.x+,通常与你的tomcat版本互相兼容即可。建议为8.5+。

    3、建议使用JDBC replication协议:此协议有非常多的高级特性,可以认为是客户端级的路由组件,支持“负载均衡”、“failover”等。JDBC URL中地址使用Router集群的地址,Router集群的高可用将借助于replication协议实现,当某个Router节点失效,replication协议可以将请求Failover到URL列表中的其他Router节点。

<bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource" destroy-method="close">
    <property name="url" value="jdbc:mysql:replication://.../test"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="maxIdle" value="12"/>
    <property name="minIdle" value="2"/>
    <property name="maxActive" value="32"/>
    <property name="maxWait" value="6000"/>
    <property name="initialSize" value="2"/>
    <property name="testOnBorrow" value="true"/>
    <property name="testOnReturn" value="true"/>
    <!-- 必须为true -->
    <property name="defaultAutoCommit" value="true"></property>
    <property name="removeAbandoned" value="true"/>
    <property name="removeAbandonedTimeout" value="60"/>
    <property name="validationQuery" value="SELECT 1"/>
    <property name="validationInterval" value="30000"/>
    <property name="testWhileIdle" value="true"/>
    <property name="timeBetweenEvictionRunsMillis" value="30000"/>
    <property name="minEvictableIdleTimeMillis" value="300000"/>
    <!-- 此参数很重要,建议1分钟 -->
    <property name="maxAge" value="60000"/>
    <property name="jmxEnabled" value="true"/>
</bean>

     我们需要特别注意“maxAge”、“defaultAutoCommit”、“testOnBorrow”参数,适度兼容Router的局限性才能更好的发挥作用。

    4、JDBC URL范式

jdbc:mysql:replication://address=(type=master)(host=mr1)(port=mp1),address=(type=master)(host=mr2)(port=mp2),address=(type=slave)(host=sr1)(port=sp1)../database

    根据上文约定,良好架构下,针对WRITE操作的Router集群应该有3个节点,那么我们需要在URL中指定三个“type=master”的Router地址;此外“type=slave”的地址为接收READ操作的Router节点地址,需要多个。

    根据replication协议的设计原理,对于WRITE、READ操作将会在相应的Router节点之间“负载均衡”,默认策略为“轮询”;当其中某个Router失效,replication协议提供了Failover机制,将会把请求路由到同一type的其他Router节点。

    此外有几个可以参考的参数,我们配合replication协议:

    1)autoReconnect:设置为false,遵循默认值。

    2)failOverReadOnly:此参数只会在autoReconnect为true时生效,建议保持默认值“true”。

    3)roundRobinLoadBalance:此参数只会在autoReconnect为true、failOverReadOnly为false时生效,建议保持默认值。

    4)readFromMasterWhenNoSlaves:当“type=slave”的所有地址都不可达时,是否可以将read请求转发给master,默认值为“false”,这个值根据实际情况设定,如果你的master可以承载所有的read请求,可以设置为true。

    5)loadBalanceStrategy:可选值为“random”、“bestResponseTime”、“serverAffinity”,默认值为“random”。

    6)loadBalanceAutoCommitStatementThreshold:当请求状态为“autoCommit”时,在一个连接上操作一定次数的请求后,触发“负载均衡”,选在其他Server。(参见源码)。

    7)allowMasterDownConnections:默认值为false,在replication协议中,如果Master无法连接时是否允许Client创建或者获取连接(包括获取Slave的连接)。“false”表示当Master无法连接时,将不能创建任何连接,包括Slave读操作。在基于Router集群时,建议设置为true。

    8)allowSlaveDownConnections:默认为false,同上。

相关推荐