TiDB 常见问题处理 - 热点

作者:李坤,PingCAP 互联网架构师,TUG Ambassador,前美团、去哪儿数据库专家。

背景

从现有的数据库使用场景来看,随着数据规模的爆发式增长,考虑采用TiDB这种分布式存储的业务,通常都是由于触发了单机数据库的瓶颈,我认为瓶颈分为3点:存储瓶颈、写入瓶颈、读取瓶颈。我们希望TiDB能够解决这3个瓶颈,而存储瓶颈是可以首先被解决的,随着机器的扩容,存储瓶颈自然就可以几乎线性的趋势扩展;而写入和读取,我们希望能够利用上多机的CPU、内存,这样才能突破单机的读写性能瓶颈,这就需要将压力分摊到多台机器上。如果需要读写的数据,集中到了一台机器上,甚至一个region上,这就必然导致热点。

这里就从使用者的角度和大家分享下,处理和热点相关的问题一些思考和手段。

  1. TiDB和MySQL在设计Schema的时候,有什么区别?
  2. 当集群性能出现问题,如何分析是否由热点导致?
  3. 热点是读热点,还是写热点?
  4. 如何确定热点是产生在哪个表上?是数据热点还是索引热点?
  5. 确定热点后,如何处理?

TiDB和MySQL在设计Schema主键的时候,有什么区别

MySQL的使用习惯

MySQL上通常都建议使用int(bigint)类型的自增id作为主键,这是由于以下几个原因:

  1. 性能:随机写改为顺序写,性能更好
  2. 功能:更容易运维,比如osc改表,归档等
  3. 易用:方便id范围检索,分段update、delete数据

TiDB的使用对比

使用TiDB的用户,有相当一部分都是从MySQL迁移过来的,可能就会错误的认为任何情况都可以沿用MySQL设计Schema的习惯,而没有考虑分布式数据库的特点。我们分别针对以上几点,考虑TiDB该如何应对。(当写入qps达到一定大小时考虑,比如超过1k,否则可以沿用MySQL的方式)

  1. 从性能考虑,TiDB使用自增id并没有带来性能的好处。我们知道TiDB是按照range的方式将数据分为一个个region,我们希望TiDB的写入分散到各个region中把多机利用起来,而不是集中在一个range,新数据都集中在一起,这会带来数据热点,包括读热点和写热点,所以采用自增id反倒影响性能。
  2. TiDB可以在线改表,直接改元数据,不需要依靠自增id改表。分布式数据库,应该也不用怎么归档。
  3. TiDB根据业务需求,或许也需要分段检索,可以通过业务的一些特征来处理,比如订单号、时间等。
  4. 在MySQL中,基本上单机容量关系,单表到达不了自增ID的瓶颈值,但是在特殊的replace into场景中,一旦写入了一个大值却能够到达这一瓶颈,TiDB也是一样,这种问题是不好处理的。

SHARD_ROW_ID_BITS

TiDB 会在以下情况下采用一个隐式的自增 _tidb_rowid,作为数据的key

  1. 对于 PK 非整数的表
  2. 对于没有 PK 的表
  3. 对于组合主键,无论是否都为int类型

通过设置 SHARD_ROW_ID_BITS 可以把 _tidb_rowid 自动打散,解决写入热点问题。比如设置为4,代表16个分片 ,如果你的tikv个数在16个以内是足够用了。设置的过大会造成 RPC 请求数增多,增加 CPU 和网络开销。举例如下:

# 创建一个分16片的表,产生的key就是随机的了,可以分散在不同range范围的region中,最大可以同时利用16个region

CREATE TABLE t (c int) SHARD_ROW_ID_BITS = 4; 
  
insert into t values(1);
insert into t values(2);
insert into t values(3);
insert into t values(4);
insert into t values(5);

mysql> SELECT _tidb_rowid,c from t order by c;
+---------------------+------+
| _tidb_rowid         | c    |
+---------------------+------+
|                   1 |    1 |
| 6917529027641081858 |    2 |
| 5188146770730811395 |    3 |
| 3458764513820540932 |    4 |
| 1729382256910270469 |    5 |
+---------------------+------+
5 rows in set (0.00 sec)

我们来解析一下_tidb_rowid的真面目,转换为2进制就很清晰了。总共64位,它的前4位是4bit的随机数,也就是我们设置的SHARD_ROW_ID_BITS = 4,将顺序值转换为随机值解决热点问题;后面60位是自增的值,足够用了。

[~]$ echo "obase=2;ibase=10;1"|bc
_0000_00000000000000000000000000000000000000000000000000000000_001

[~]$ echo "obase=2;ibase=10;6917529027641081858"|bc
_1100_00000000000000000000000000000000000000000000000000000000_010

[~]$ echo "obase=2;ibase=10;5188146770730811395"|bc
_1001_00000000000000000000000000000000000000000000000000000000_011

[~]$ echo "obase=2;ibase=10;3458764513820540932"|bc
_0110_00000000000000000000000000000000000000000000000000000000_100

[~]$ echo "obase=2;ibase=10;1729382256910270469"|bc
_0011_00000000000000000000000000000000000000000000000000000000_101

TiDB使用建议

从以上几点可以看到TiDB并没有那么依赖自增id,那么TiDB用什么作为主键呢?

  1. 如果写入qps很小,依然用自增id也是没问题的
  2. 如果业务有一个随机并唯一的int类型的业务id,建议用该值作为主键,这样可以自然分散到不同region中;
  3. 如果业务有一个自增并唯一的int类型的业务id,建议不使用主键,将该id设为唯一索引,使用SHARD_ROW_ID_BITS = [4~6],分别对应最多16~64个tikv。
  4. 如果业务有一个唯一的字符串类型的业务id,同上。

当集群性能出现问题,如何分析是否由热点导致

当我们接手一个业务,或者业务在测试时反馈性能不理想,如何确定是否是热点导致的呢?

需要从几个方面观察,如果某个节点相比其他节点特别突出,说明这个节点有热点。

  1. 是否某个tikv节点的raft store、apply的cpu使用率偏高


  1. 是否某个tikv节点grpc消息偏多
sum(rate(tikv_grpc_msg_duration_seconds_count{instance=~"$instance", type="kv_prewrite"}[1m])) by (type,instance)

  1. 是否某个tikv节点流量偏多,重点关注下面3个指标(prometheus公式增加 by (instance)
    keys flow:按keys个数统计
    read flow:读流量
    write flow:写流量



  1. 直接看监控中的hotregion面板,这里展示的是pd识别到的热点
    目前统计热点的标准是根据总流量信息动态算分一个阈值,pd会将统计为热点的region,在每个store上面的个数保持均衡。如果某个store的热点region集中,则表示有热点。

如何确定热点是读还是写

  1. 参考上一步read flow、write flow,看是哪一个类型流量不均匀
  2. 可以用官方pd_ctl命令行工具,执行 hot store、hot read、hot write 3种命令,可以直接看到哪个节点上是热点,读写热点的region分别存在哪些,来实时观察读写热点
命令的几个输出参数的含义:

as_peer/as_leader:按2个维度来统计,作为peer或者leader 
total_flow_bytes:这个store总的流量
regions_count:这个store热点的region数量
region_id:region的id
flow_bytes:这个region的流量
hot_degree:表示 region 是持续热点,每分钟上报 1次,如果是热点,degree+1
AntiCount:类似 ttl,过了几次后才踢出hot cache(定期更新,如果判断 flowBytes < hotRegionThreshold,不会马上从 cache 清掉,如果没有这个判断,可能存在误判踢掉后后面又重新加进来)
Version:记录region分裂的次数
Stats:记录一些最近添加的信息

举例一小段输出:可以看出store 10上有1个热点region,在近643次都被上报为热点,该region分裂231次,很有可能是一直在region尾部追加数据导致的热点

  "as_peer": null,
  "as_leader": {
    "10": {
      "total_flow_bytes": 2307649,
      "regions_count": 1,
      "statistics": [
        {
          "region_id": 7705,
          "flow_bytes": 2341686,
          "hot_degree": 643,
          "last_update_time": "2019-08-05T18:30:15.901413761+08:00",
          "AntiCount": 1,
          "Version": 231,
          "Stats": null
        }
      ]
    },

如何确定热点是哪张表,是表数据还是索引

使用tidb的http接口,根据上一步的region_id,查看region,可以看到是表还是索引,如果是索引是哪个索引,如下面的例子,可以看到region为库db1的表table1的索引idx_column1

curl http://{tidb_ip}:10080/regions/12345
{"region_id":12345,"start_key":"xxxAAAAAJLX2mAAAAAAAAAAwOAAAAAATPu8gOAAAAAAB6M4gOAAAAAANMeywOAAAAAANMezA==","end_key":"xxxxAAAAAJLX2mAAAAAAAAAAwOAAAAAATPu8gOAAAAAAB6ZaAOAAAAAAMNDgQOAAAAAAMNDgg==","frames":[{"db_name":"db1","table_name":"table1","table_id":123,"is_record":false,"index_name":"idx_column1","index_id":3,"index_values":["11111","22222","33333","44444"]}]}

甚至可以使用mok工具,分析region的start_key和end_key对应的数据,https://github.com/disksing/mok,根据start和end返回的table和row,分析具体是哪部分数据

确定热点后,如何处理

  1. 如果是热点region不是唯一的,有多个,但都集中到某一个store上,先确认热点调度是否可运行(如果没有调整过默认是打开的),从pd_ctl执行config show all,查看2个地方:
  • region-schedule-limit 是否大于0

  • hot-region 部分是否开启

    如果这2处正常,观察pd下的operator监控,调度中是否有较多的balance-region,而一直没有热点调度的指标,则可能是热点调度被抢占,这个问题在2.1.14解决,该版本加了hot-region-schedule-limit,将热点调度从region-schedule-limit调度中拆分开了

  1. 如果是由于前期设计不当,导致类似自增id的情形,导致热点region,目前是不能在线调整主键的,只可以去掉自增属性,需要业务配合整改 schema 设计;
  2. 如果是热点小表的问题,可以通过命令手动 split region
>> operator add split-region 1 --policy=approximate     // 将 Region 1 对半拆分成两个 Region,基于粗略估计值,消耗更少的 I/O,可以更快地完成。
>> operator add split-region 1 --policy=scan            // 将 Region 1 对半拆分成两个 Region,基于精确扫描值,更加精确

后续

目前分析热点问题,对于新手会有一点门槛,有时候复杂时要靠按region、store流量排序,才能分析出来。据了解官方在3.0和4.0的规划中,都将热点问题的排查和调度进行了优化,到时可能热点调度很可能就会变得很自动和直观,但我们了解其内部原理还是非常有好处的。

1赞

这里提供的mok工具下载链接中的已编译安装包对应链接访问不了,对应源码包下载下来后使用go编译报出了“go: github.com/cznic/mathutil@v0.0.0-20181122101859-297441e03548: Get “https://proxy.golang.org/github.com/cznic/mathutil/@v/v0.0.0-20181122101859-297441e03548.mod”: dial tcp 172.217.160.113:443: i/o timeout” 异常,请重新提供一个可用的工具下载链接。

你好,https://github.com/disksing/mok 这里提供的 release 链接应该是可用的 https://github.com/disksing/mok/releases

在别的服务器上试了一下可以访问,估计是我们服务器的互联网网络通路问题。