radon 工具使用及问题汇总

radon 工具作为 MySQL 的中间件对外提供服务, 其以 jump consistent hash 算法实现了扩展 MySQL 读写的目的. 业务所常用的 sql 语法都做了相应的支持, 比如 DDL, SHOW, Full Text Search, JOIN 以及聚合排序等, 详细 sql 支持见 radon_sql_support. 同时 radon 也提供了 api 接口方便管理员进行配置状态的管理, 故障的诊断以及监控数据的收集.

下述的问题列表则主要介绍在使用 radon 的过程中可能碰到的疑问和问题, 一些问题 radon 开发者修复即可, 一些问题则需要在业务层调整做更多的支持. 更权威的则需要关注 radon-issue 列表以确定具体问题的解决方式. 后期碰到的问题也会在该列表中持续更新.

问题列表

关键字问题

下面的 keywords 条目限制了一部分 sql, 执行的 sql 需要把关键之反引起来才可以.

// github.com/xelabs/go-mysqlstack/sqlparser/token.go
62 var keywords = map[string]int{...

如下所示

mysql admin@[dbt:3308 db_test rw] > insert into user_test(user_id, app_id, status, descmsg, create_time) values(1223, 10021, 1, 'one insert test', now()); 
ERROR 1149 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use, syntax error at position 46 near 'status'

mysql admin@[dbt:3308 db_test rw] > insert into user_test(user_id, app_id, `status`, descmsg, create_time) values(1225, 10020, 1, 'two insert test', now()); 
Query OK, 1 row affected (0.00 sec)

权限放大问题

通过 api 接口创建的用户权限太大, 如下所示, 正式使用的时候希望限制权限, 接口可以增加 host 和 privileges 选项,默认应该仅开启增删改查的权限. 如果程序不做修改, 可以手工在后端的所有节点创建相同权限的普通用户.

//ctl/v1/user.go
37 func createUserHandler(log *xlog.Log, proxy *proxy.Proxy, w rest.ResponseWriter, r *rest.Request) {
38         spanner := proxy.Spanner()
...
60         for _, db := range dbList {
61                 query := fmt.Sprintf("GRANT ALL ON %s.* TO '%s'@'%%' IDENTIFIED BY '%s'", db, p.User, p.Password)
62                 if _, err := spanner.ExecuteScatter(query); err != nil {
...

事务问题

未支持 autocommit = 0 当作开启一个事务, 同类的很多工具都有此问题, 详细见 proxysql-issue1256.

备注: 官方已修复, 详见: radon-issue465

压测问题

通过 benchyou 创建2个表(32张子表), 单台机器 seq 查询 8w qps, 通过 radon 代理三台DB可以达到 11w 左右, 写扩展较好, 读扩展则受 radon 所在机器的性能, 如果为了提高读可通过 peer 方式创建多个 radon 节点, 应用程序可以通过 dns 或 haproxy 等方式连接.

适用场景问题

从大的方面来看, 只要数据在不同的节点, 就肯定面临下面的几个问题:

1. shard 只能按照一个维护拆分, 通过其它维度查询的时候必须要做全表扫描;
2. 如果表中包含多个唯一键, 则需要业务层做更多的保障;
3. 在有唯一性要求的业务中(比如用户名, 手机号等), 后端有一个节点出现故障, 业务层就需要降级, 因为不能保证唯一性;

可以通过缓存来缓解这几个问题, 但很难做到缓存与数据的一致性. 这几个问题就意味着一些单节点的接口请求性能要优于 shard 分区的方式, 所以一般对于用户中心这类业务, 应当尽量先按照功能进行拆分, 功能难以细分的时候再考虑水平切分. 另外对于一些单一的查询, 比如只按用户 id 做更新或查找的接口通过 shard 分区方式就能得到很好的扩展, 比如记录用户 token 和登录日志类的业务就很适合. radon 工具在后端有一个节点出现问题的时候则进入降级状态, 这时候不允许应用做查询或更新操作. 这种方式简洁明了, 不过对于单一接口的业务不够友好.

列名问题

插入的时候必须指定列名, 且包含 shardkey, 如下所示:

mysql admin@[dbt:3308 db_test] > insert into user values(1233, 'arster', now());
ERROR 1105 (HY000): unsupported: shardkey.column[user_id].missing
mysql admin@[dbt:3308 db_test] > insert into user(user_id, name, create_time) values(1233, 'arster', now());
Query OK, 1 row affected (0.00 sec)

唯一性问题

可以通过 radon 创建带多个唯一键的表, 不过不能保证所有的子表的唯一键的唯一性, 需要在业务曾对唯一需求做额外的处理.

jump consistent hash 介绍

jump consisten hash 哈希算法适合在分 shard 的分布式系统中, 具备均匀分配, 快速计算, 低消耗等特性. 具体的算法为输入一个 64位的 key 和桶的数量, 最后输出桶的编号. 其设计目标包含以下两点:

1. 平衡性, 把对象均匀分布到所有桶中;
2. 单调性, 在桶的数量变化时, 仅需移动一些对象到新桶, 不需要做其它的移动;

不像割环法,jump consistent hash不需要对 key 做 hash,这是由于 jump consistent hash 使用内置的伪随机数生成器,来对每一次 key 做 hash, 如下所示, key 可以为整形或字符串类型, 如果有浮点类型的 key, 需要转成响应的整形表示形式:

func Hash(key uint64, buckets int32) int32 {
	var b, j int64

	if buckets <= 0 {
		buckets = 1
	}

	for j < int64(buckets) {
		b = j
		key = key*2862933555777941757 + 1
		j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1)))
	}

	return int32(b)
}

func HashString(key string, buckets int32, h KeyHasher) int32 {
	h.Reset()
	_, err := io.WriteString(h, key)
	if err != nil {
		panic(err)
	}
	return Hash(h.Sum64(), buckets)
}

分区原理说明

src/router/hash.go 源文件中可以看到, radon 采用了 jump consistent hash 算法实现了 shard 操作. 对给定的 key 算出具体的桶编号. 从 src/router/compute.gosrc/backend/scatter.go 可以看出实际的 backends 仅和 配置文件 backend.json 中每个条目的 name 字段关联, radon 通过 backend 的数量来计算每个节点所拥有的桶的范围, 每个 key 通过 jump consistent hash 算法得到桶的编号, 进而到对应的 backend 节点进行操作.

另外桶的数量可以通过以下参数进行调整:

"router": {
     "slots-readonly": xxx,    # 桶的数量
     "blocks-readonly": xxx    # 每个节点每个子表拥有的桶数
}

hash 函数问题

在 key 为字符串的时候, 通过 jump consistent hash 的 HashString 方法中采用了 jump.CRC64 作为 KeyHasher 参数的值, 不过 jump.CRC64 在并发环境中存在安全隐患, 需要改为官方建议的 NewCRC64 方法, 更多见 go-jump-consistent-hash-issue6

// src/router/hash.go
163 // GetIndex returns index based on sqlval.
164 func (h *Hash) GetIndex(sqlval *sqlparser.SQLVal) (int, error) {
	......
180         case sqlparser.StrVal:
181                 idx = int(jump.HashString(valStr, int32(h.slots), jump.CRC64))
    ......

备注: 官方已修复, 详见 radon-issue464

故障恢复

从分区原理来看, 对可用性要求高的工程, radon 可以采用 vip 或 dns 的方式连接后端的每个 master. 对可以容忍中断一段时间的工程, radon 可以直接连接后端的每个 master ip, 在一个后端 master 节点出现故障的时候, 可以手动修改 radon-meta 目录中的 backend.json 元信息, 不过不能修改 name 字段, 最后重启 radon 进程即可, 如果是通过 peer 配置的多 radon 节点, 则稍微麻烦些, 不过处理的方式都一样.

灵活性问题

目前来看仅支持按 jump consistent hash 方式进行 shard 分区, 如果已有的工程是按照范围或者一致性 hash 割环方式分区, 需要单独修改 src/router/hash.go 中的 方法以适应已有的工程.

另外每个表的元信息可以做单独的修改, 比如已有的工程仅分了两个库, 每个库都有一个 user 表, 如果也是按照 jump consistent hash 方式进行的分区, 可以直接修改 radon 中表的元信息以适应操作:

# less db_test/user.json 
{
        "name": "user",
        "slots-readonly": 4096,
        "blocks-readonly": 2048,
        "shardtype": "HASH",
        "shardkey": "user_id",
        "partitions": [
                {
                        "table": "user",
                        "segment": "0-2048",
                        "backend": "db1"
                },
                {
                        "table": "user",
                        "segment": "2048-4096",
                        "backend": "db2"
                }
        ]
}