OneProxy :: 分库分表很难吗?不用开发或运维写任何代码也能轻松实施!

对于一些大型网站(如淘宝、eBay等),读写分离功能只是解决了读流量的问题,对企业最重要的部份还有写的流量,成千上万的用户同时下单、同时支付等业务才能给企业带来利润和成长。比如需要支持每秒1万笔业务,每笔业务大约需要30个事务来完成所有的处理(从创建交易到最后完成交易、确认收货),则需要一个具有每秒钟30万个事务处理能力的数据库系统来支撑。因此很难找到一台主机(包括最强的小型机、大机)或一台数据库(包括Oracle)来支持这么大的业务量,因为高峰时刻的交易量可能达到每秒10笔,必须要将写压力拆分到多台主机和多个数据库中,才能支持业务的发展。也许垂直拆分(不同的业务用不同的主机来支持)是一个好办法,但这是不够的,最早遇到拆分需求的风控系统中,用户在支付网站上的任何一下操作都会有一次轨迹记录,一行业务交易在风控系统中对应的记录可能有几十条,就算一台主机和一个数据库只存放一个表,也无法支持这么高的访问量。只有水平拆分才能解决这么大的压力,比如按用户的编号来打散到几十台不同的机器上。

oneproxy_database_sharding_arch

OneProxy可以实时分析应用程序发往数据库的每一个通信包,在透明读写分离方案中已经通过包分析来识别不同类型的操作(SQL语句类型及事务上下文),平民软件进一步扩展了SQL解释器来支持人SQL语句中取出表名及对应列的列值功能,发现可以轻松地根据表名或某个列的列值来做更高级的SQL路由,并且对前端的所有应用都适用,和开发语言无关。如果系统中不同的应用由不同的语言编写,那基于协议分析的OneProxy中间件将成为最好的选择,基本上无需改写应用程序就可以实现分库分表的路由。

要实现分库分表,就需要在OneProxy里配置多个MySQL集群,比如“trade1”和“trade2”,每个集群里都有一主一备,如下所示:

export ONEPROXY_HOME=/data/oneproxy

# valgrind --leak-check=full --show-reachable=yes \
${ONEPROXY_HOME}/oneproxy --keepalive --proxy-address=:3307 \
  --proxy-master-addresses=172.30.12.4:3306@trade1 \
  --proxy-slave-addresses=172.30.12.5:3306@trade1 \
  --proxy-master-addresses=172.30.12.6:3306@trade2 \
  --proxy-slave-addresses=172.30.12.7:3306@trade2 \
  --proxy-user-list=test/1378F6CC3A8E8A43CA388193FBED5405982FBBD3@test \
  --proxy-part-tables=${ONEPROXY_HOME}/part.txt \
  --proxy-group-policy=trade1:master_only \
  --proxy-group-policy=trade2:master_only \
  --log-file=${ONEPROXY_HOME}/oneproxy.log \
  --pid-file=${ONEPROXY_HOME}/oneproxy.pid

对于任何一组MySQL集群(“trade1”和“trade2”),可以根据业务的具体需求来配置不同的读写分流策略。第二步是需要创建一个配置文件来告诉OneProxy哪些表是水平拆分的,可以将数据库的分区表一样来理解分库分表,只要允许分区表的不同分区可以位于不同的服务器即可。因此需要在配置文件里,指定分片列(称为分区键,简称“pkey”)及其字段类型,并指定分区的方法和每一个分区的位置(指定MySQL集群的名字),下面是将一个表分成四份,并分布到两个不同的MySQL集群的例子。

[
  {
        "table"   : "trade",
        "pkey"    : "tradeno",
        "type"    : "char",
        "method"  : "hash",
        "partitions":
           [
               { "suffix" : "_0", "group": "trade1" },
               { "suffix" : "_1", "group": "trade2" },
               { "suffix" : "_2", "group": "trade1" },
               { "suffix" : "_3", "group": "trade2" }
           ]
  }
]

将上面的配置信息保存成一个JSON格式的文件,并且“proxy-part-tables”选项来将分库分表的信息加载到OneProxy中,OneProxy就可以按照指定的要求来对SQL语句进行分库分表路由了,可以执行一些SQL来操作“trade”表进行验证。在应用程序中应当使用“trade”这个表名,但记录真正存放的表名则是“trade_0”、 “trade_1”、 “trade_2”和“trade_3”,通过OneProxy既可以直接访问总表,也要以直查询分表名,这一点和数据库中的分区表一模一样,上层应用无需关心每一个子表的具体位置,全部交由OneProxy来进行路由。

OneProxy支持以下几种分区方法:

  1. 根据范围(Range)划分,按照某个字段的值范围来进行切分,最常用的是按照时间。
  2. 根据值列表(List)划分, 根据具体值进行切分,比如按月份值进行分表保存。
  3. 用函数(Hash)计算后,再根据分片数取模进行划分,比如根据用户编号,特点是比较平均。
  4. 同步,使用函数(Crc32)计算后,再根据分片数取模进行划分,比如根据用户编号,特点是比较平均,并且数据库中也有此函数。
  5. 同一个表可以进行二级分片,通过设置合理的“subpkey”、 “subtype”和“subpartitions”属性来实现。 二级分区的方法可以是“Range”、 “List”、 “Hash”或“Crc32”中的任意一种,不同于一级分区,所有二级分区必须位于同一个实例中,即不能单独指定子分区的位置。

下面是一个二级分区表的配置例子。

[
  {
     "table"   : "huge_table",
     "pkey"    : "col1",
     "type"    : "int",
     "method"  : "range",
     "partitions":
         [
            {"suffix":"_0", "group":"server1","value" : 100000 },
            {"suffix":"_1", "group":"server1","value" : 200000 },
            {"suffix":"_2", "group":"server1","value" : 300000 },
            {"suffix":"_3", "group":"server1","value" : 400000 }
         ],
     "subpkey"       : "col2",
     "subtype"       : "int",
     "submethod"     : "crc32",
     "subpartitions" :
         [
            { "suffix" : "_a" },
            { "suffix" : "_b" },
            { "suffix" : "_c" },
            { "suffix" : "_d" }
        ]
  }
]

OneProxy对于分区表也有一些限制和要求,如下所示:

  1. OneProxy只支持单列分区,分区键只支持单个列,分区和子分区的列可以不同。这个限制可以让中间件的分区定位代码更加高效。
  2. 分区键的值必须是原子值,在SQL中为分区键指定值是必须是明确的值,不能是函数或表达式。
  3. 分区键上允许单值查找(“where pkey = xxx”),可以是多值列表比较(“where pkey in (1,2,3)”),或区间查找(“where pkey > … and pkey < …”,包括“between”操作)。但不支持多个“or”条件(“where pkey = 1 or pkey = 2”)
  4. 分区键不允许为空,并且不应当被程序后续更新, OneProxy无法检测出更新后的目标值是否符合分区配置,也不支持分区之间的记录移动。
  5. 对于“insert”和“replace”语句,请显式指定字段列表,例如:“insert into xxx (col1, col2,…) values (…,…)”。
  6. 分区键类型可以是“int”、 “char”、 “date”或“timestamp”中的一种,其中“date”表示不含时间信息的日期(如“2015-11-11”),而“timestamp”表示带时间的日期(如“2015-11-11 00:00:00”)。
  7. 需要根据业务的目标仔细设计分片个数及分片的方式,后期调整有可能会涉及数据迁移,会使得切换工作非常复杂。

OneProxy只是一个高级的SQL路由器,不具备完整的分布式数据库的功能。对查询也有一些要求,对涉及多个分片的SQL语句或事务也有一些限制,在正式使用它之前需要有很好的了解和思考。

  1. 不支持分布式事务,OneProxy只支持单库事务,在事务中的所有语句都应当在一个MySQL实例中完成;事务中的第一个语句会被用来定位事务所在的MySQL实例,请确保在分库分表的情况下,第一个语句可以准确地定位到所在的数据库。与此相对应的限制是事务中的SQL不能跨MySQL实例,但允许跨同一个实例下的多个分表。
  2. 不支持跨实例的多表关联,我们正在努力,希望后续的版本可以支持它。
  3. 不支持“Insert into … select … ”这样的的语句。
  4. 不支持数据装载(“load data … ”)命令。
  5. 涉及多个分片的复杂SQL语句将会受到一些限制,受制于结果集合并功能,我们也正在努力改进

普通的DDL语句,如创建新表或更改表结构的语句,可以被OneProxy准确路由,当在OneProxy中配置好分库分表后,可以通过OneProxy来统一执行建表语句,而不需要每录每一个MySQL实例,进行分别建表,OneProxy会根据配置自动创建带有后缀的真实表名。

OneProxy是为OLTP场景设计的中间件,目标是为了线性扩展数据库系统的事务处理能力,因此没有跨实例的多表关联功能,以及缺少分布式事务的支持。 多表关联要以从业务模型的设计角度得到优化,再结全应用层的少量代码来实现;分布式事务则推荐使用消息系统来进行解耦,这是目前各大网站通用的解决方案。相信最终平民软件会解决这两个技术难点的,有你的支持我们才会做得更好。