OB 开发测试建议 (上)

MQ4096 数据库技术闲谈 2020-09-01 11:51

本文主要是为 OB 开发和测试人员提供一些入门建议,分为上中下三篇。本文比较基础,适合初学者。在使用 OB 之前阅读,可以起到事半功倍的效果。希望能有帮助,欢迎留言讨论。


连接 OB

数据库都支持 TCP 连接,数据库连接至少要提供一个 IP、端口、用户名和密码。数据库默认连接端口各不相同。比如说 oracle 是 1521,mysql 是 3306 。这些都可以通过配置改变,取决于DBA 的考虑。

当那个 IP的 Port 上注册有多个数据库实例或者数据库服务的时候,比如说 oracle ,还需要提供一个 sid 或服务名才能准确连接。oracle 数据库实例里会有多个模式(schema),对应于多个用户,有些用户的权限支持跨 schema 访问。在 mysql 数据库实例了会有多个 databases,只要用户有权限访问就行。所以连接还需要指定默认 schema 或者数据库。
下面就是一个完整的 mysql 数据库的连接命令行:

mysql -h 192.168.1.100 -P 3306 -u user01 -p'passwd01' test

OB 通常以集群形式运行,至少 3 台机器,每个机器上启动一个 observer 进程,进程的连接端口是 2881.正常情况下,连接集群的任何一个节点的 2881 端口,都能跟这个集群通信。但通常不建议这么做。因为如果该节点故障了,客户端还得换 IP,不是很方便。OB 集群会提供一个类似 oracle 监听的反向代理软件 obproxy ,监听端口是 2883 。obproxy 可以运行在任何位置。比如说 OB 机器节点、应用服务器或者其他可靠的虚拟机上。obproxy 就是一个传送门,通向一个 OB 集群。当然也有办法通向多个 OB 集群,这取决于 obproxy 的启动参数配置。你可以在任意位置开启这个传送门 :)  所以通常只需要记录 obproxy 的地址和监听端口(2883)即可,obproxy 会把 SQL 转发到 OB 集群里某台节点上。这个功能叫路由。在分布式数据库或者分布式架构的产品里,路由是很重要的事情,关乎性能

OB 以集群形式运行,提供多实例服务。连接到 OB 的时候,连接的是集群中的某个实例。OB 集群在诞生的时候,会有个内部实例,名字叫 sys 。每个实例都拥有一部分资源(CPU和内存),资源是实例提供数据库读写服务的基础。sys 只会占用 OB 集群资源的少数资源,剩余的都是待分配资源。OB 集群可以分配出多个实例,实例选择兼容 ORACLE 或者 MySQL 。在 ORACLE 实例里,也有一个默认用户叫 sys 。这里后面沟通的时候,注意区分是实例 sys 还是 ORACLE 实例里的用户名 sys 。

所以,连接 OB 的时候,需要提供:

  • IP : 通常就是 obproxy 的地址。生产环境也可以是 F5 上一个 vip 或者域名,后端对应多个 obproxy 地址。
  • 端口 :通常是 obproxy 监听的默认端口 2883 ,运维人员也可能改为 3306 或者 1521 ,或者其他端口。
  • 用户名 :连接的用户名会包含集群信息、实例信息和实例里的用户名。后面详述。
  • 密码 : 连接的密码
  • 默认 schema 或 db :如果是连接 oracle 实例,就是某个 schema;如果是 mysql 实例,就是默认 db。

奇怪的 OB 用户名

OBProxy 可以为多个 OB 集群提供连接服务,每个 OB 的集群里有多实例,OB 的连接为了跟传统的连接习惯一致,并没有增加参数表示连接哪个集群和哪个租户(实例),而是在用户名里放入了集群名和租户名。

OB 的用户名格式有两种:集群名:租户名:用户名 或 用户名@组户名#集群名 。比如说:obdemo:bmsql:tpcc 或 tpcc@bmsql#obdemo 。
由于 OB 的用户名有三部分组成,集群名有运维创建集群的时候指定,租户名由创建实例的时候指定,OCP 为了避免租户名冲突,还会在后面加上 2 个数字。然后实例的用户名是通过实例的管理员创建的。这里就会有个问题,如果这三部分名字命名都很长的话,这个用户名就会非常长,使用和存储上会有点不便。
默认情况下,OB 的 MySQL 租户的管理员用户是 root ,OB 的 ORACLE 租户的管理员用户是 sys 。通常不建议业务直接使用租户的管理员用户 。
总结一下,OB 的用户名分为 3 类,示例如下:
  • 集群管理员: root@sys#obdemo
  • 实例管理员: sys@oboracle1_1#obdemo  , obdemo:obmysql2_1:root
  • 应用账户:  tpcc@oboraacle1_1#obdemo obdemo:obmysql2_1:tpcc


连接 OB 数据库

前面提到连接 OB 数据库的时候,实际是连接到具体的 OB 实例下的默认 schema 或者 db 。如果该实例是兼容 MySQL 的,则可以使用 mysql 客户端命令连接。OB 的 MySQL 租户基本兼容 MySQL 5.6/5.7 的连接协议,但不兼容 MySQL 8.0 的连接协议(以后版本可能会修复)。所以 MySQL 8.0 及其以后版本的客户端连接 OB 的 MySQL 实例会提示认证失败类错误(实际密码没有错误)。有兴趣的朋友可以通过 tcpdump 去分析不同版本 MySQL 连接协议的不同。

所以集群管理员连接 OB 集群(sys 实例,兼容 MySQL)的命令格式示例如下:

mysql -h 192.168.1.100 -P 2883 -u root@sys#obtest -p'y6k%N_zwVzOzfW3@' oceanbase -c -A

连接时建议习惯性的加上 -c -A 这两个参数。这样命令行客户端就不会把 SQL Hint 当成注释忽略掉了。以及当数据看下很多表时,不要表名自动补全功能(有代价)。

如果连接 OB 的 ORACLE 实例时,就不能用 mysql 客户端。OB 也不支持 ORACLE 的 sqlplus 客户端(因为 ORACLE 不开源)。OB 提供客户端命令 obclient ,这个需要安装,软件包名字格式类似:obclient-1.1.8-20200407202746.el7.alios7.x86_64 。alios7 跟 centos7 基本通用。

OB 的工具产品很多,有些名字可能容易引起混淆。当说 obclient 的时候,通常就是指这个客户端命令。obclient 可以连接 OB 的 ORACLE 和 MySQL 实例。

示例如下:

# 连接 sys 实例
obclient -h192.168.1.100 -P2883 -u root@sys#obtest -p'xxxxxxxxxxx' oceanbase -c -A
# 连接 oracle 实例的 sys 用户
obclient -h192.168.1.100 -P2883 -usys@bmsql#obtest -p'xxxxxxxxxxx' sys -c
#连接 oracle 实例的 tpcc 用户
obclient -h192.168.1.100 -P2883 -usys@bmsql#obtest -p'xxxxxxxxxxx' tpcc -c
obclient -h192.168.1.100 -P2883 -utpcc@bmsql#obtest -p'123456' tpcc -c
以上是标准的连接,即通过 OBProxy 连接 OB 集群。
有时候,你会看到直连 OB 节点的方法,即连接具体的 OBServer 的节点,端口 2881 。用户名里只需要提供租户名和用户名。格式也有两种:租户名:用户名 或 用户名@租户名 。示例如下,只是为了便于理解,并不建议采取下列方式连接。因为可能某些时候会有问题。
# 连接 sys 实例
obclient -h192.168.1.101 -P2881 -u root@sys-p'xxxxxxxxxxx' oceanbase -c -A
# 连接 oracle 实例的 sys 用户
obclient -h192.168.1.101 -P2881 -usys@bmsql-p'xxxxxxxxxxx' sys -c
#连接 oracle 实例的 tpcc 用户
obclient -h192.168.1.101 -P2881 -usys@bmsql-p'xxxxxxxxxxx' tpcc -c
obclient -h192.168.1.101 -P2881 -utpcc@bmsql-p'123456' tpcc -c


探索 OB


探索用户权限

用户权限的大小,会影响解决问题的能力大小。

OB 集群通常是运维人员搭建的,开发测试人员只会拿到上面这样的连接信息。从用户名里可以判断出连接的是哪个集群的哪个实例。如果拿到的是 sys 实例,那表示可以看到很多信息,这对后期问题分析、性能调优有很大帮助(开发测试环境的问题,运维人员不一定有时间帮开发去排查,更多的时候还是要靠自己)。如果拿到的是业务实例,也能看到有限的一些信息,能自主解决部分问题。

所以拿到连接信息的第一步是判断自己用户的权限。
从用户名中的租户名可以判断是哪个实例,从租户的用户名 运行下面语句可以判断自己的权限。

[root@ocp1 ~]# obclient -h192.168.1.100 -P2883 -utpcc@bmsql#obtest -p'123456' tpcc -c
obclient: [Warning] Using a password on the command line interface can be insecure.
Welcome to OceanBase. Commands end with ; or \g.
Your OceanBase connection id is 1579263
Server version: OceanBase 2.2.51 (r20200721101034-4c669872da87356286763cf7e16ce809950841ca) (Built Jul 21 2020 11:41:46)

Copyright (c) 2000, 2020, OceanBase and/or its affiliates. All rights reserved.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

obclient> show grants for tpcc;
+--------------------------------------------------------------+
| Grants for TPCC@% |
+--------------------------------------------------------------+
| GRANT CREATE, DROP ON *.* TO 'TPCC' |
| GRANT ALL PRIVILEGES ON "TPCC".* TO 'TPCC' WITH GRANT OPTION |
+--------------------------------------------------------------+
2 rows in set (0.01 sec)

OB 的 ORACLE 和 MySQL 租户的权限控制和展示跟 MySQL 的设计思路是很接近的,这一块可以了解一下 MYSQL 的用户授权和展示。

如果权限是 ALL PRIVILEGES ON *.* 则表示这个用户是个管理员权限,那很后面看一些信息就方便多了。如果不是,可以找运维人员要一个能看实例(包括 sys 实例)所有信息的只读账户。比如说

obclient> grant select on *.* to guest ;
Query OK, 0 rows affected (0.04 sec)

obclient> show grants for guest ;
+--------------------------------+
| Grants for GUEST@% |
+--------------------------------+
| GRANT SELECT ON *.* TO 'GUEST' |
+--------------------------------+
1 row in set (0.02 sec)

obclient>

探索 OB 实例

实例资源的大小,会限制实例的性能。

OB 以集群形式运行,提供给业务服务的是实例。实例拥有的资源只是集群机器资源的一部分。所以机器好并不代表某个具体业务实例的性能就一定好。具体要看运维给这个业务实例分配了多少资源(多少 CPU 和内存)。

在 OB 的 MySQL 实例下可以查看视图 gv$unit 获取实例的资源信息。不过在那之前,得先确认一下当前实例的兼容类型。(业务实例的命名不一定像我的例子命名那样直白)。

obclient> show variables like 'ob_compatibility_mode';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| ob_compatibility_mode | MYSQL |
+-----------------------+-------+
1 row in set (0.01 sec)

-- 查看当前租户的资源

SELECT tenant_name, svr_ip, unit_Id, unit_config_name, resource_pool_name, max_cpu, min_cpu, round(max_memory/1024/1024/1024, 2) max_mem_gb, round(min_memory/1024/1024/1024, 2) min_mem_gb
FROM oceanbase.gv$unit ;


租户的资源主要是 CPU 和内存。CPU 对应租户内部的工作线程数(worker),数量默认是 CPU 的 10 倍。高并发时,CPU 越多,worker 数量就越多,自然总体能力就越高。内存的重要性可能比 CPU 更高。OB 的数据库在高并发读写时,在 CPU 不少的情况下,往往内存更容易先到达瓶颈,其次才可能是磁盘 IO。

如上图租户有 12 个 CPU 和 40G 内存。则内部会有 120 个 worker。其中默认 30% 的worker 资源会用于大查询。大查询的标准由 sys 租户的参数指定。这个后面再说。
OB 的读写在内存里是分离的。40G 内存默认情况有一半用于存放增量数据(memstore),剩下的放读入的基线数据。如果写的比例比较高,增量内存不大的情况下,增量内存很快就会用尽,进而触发 OB 的合并或者转储操作。合并是 OB 内存中的增量数据和磁盘上的全量数据会在内存中合并生成新的数据版本再写回到磁盘上,这个会抢占一部分 CPU 、内存和 IO 资源,影响测试性能。转储时 OB 内存中的增量数据直接以 sstable 格式写入到磁盘数据文件中,对性能影响比较小。所以实际情况都会开启转储。合并和转储的力度都是可以通过参数调优的(以后再深入介绍)。

除了性能测试外,常见遭遇内存瓶颈的场景就是数据初始化。

数据初始化

数据初始化有 2 种途径。一是把原来的数据库数据导入到 OB,一种是应用程序重新造数据。

导数

导数通常是离线同步,首先推荐用 datax 产品。datax 是 alibaba 开源的开放的数据交换框架,能在不同数据库源之间同步数据。比如说 oracle、mysql、sqlserver、db2、csv、hadoop 等。在文档《OceanBase ORACLE 租户开发者指南》里有详细介绍过 datax 的使用方法。
datax 的最大的不方便就是按表配置同步,所以如果熟悉了配置文件的写法,可以使用 shell 脚本批量生成同步配置文件。

如果源数据已经是文件了,导入到 OB 可以使用 OB 的 load data 或者 datax 。如果文件非常大的话,建议使用 linux 命令 splitfile 将大文件分割,然后利用 datax 的并行功能导入文件。这样效率最高。

当然,导数性能越高时,就意味着对 OB 内存写入速度越高。就可能有上面内存瓶颈的问题。一方面 OB 租户的内存尽量多分配一些,另外一方面 OB 集群层面设置一些转储参数,加速 OB 的转储性能,最终这个会达到一个平衡。

在 datax 的导数设置里,有一个 batchsize 的选项,控制事务的大小。通常建议设置为 1000 。datax 会用 jdbc 的 batch insert 功能,生成一个 insert values 后跟 1000 个记录的值。单笔事务大小就是 1000 。MySQL 是支持这种写法, ORACLE 并不支持,不过 OB 的 ORACLE 实例支持这种批量 insert 写法,可以降低网络请求次数,提升导数效率。

造数据

造数据性能的关键就在于事务的大小和日志盘的性能。这点跟传统数据库原理相同。事务太小,提交太频繁;事务太大,太耗数据库某种资源可能引发其他异常(在 ORACLE 里是 UNDO,在 OB 里是内存)。所以批量写入是最佳建议。批量的大小可以再几千或几万之间尝试。


异常


OB 是通用的分布式数据库软件,功能非常丰富,并有相应的异常处理逻辑。下面仅介绍初学者常碰到的异常。

内存不足报错

关于内存,业务实例能使用的内存只是 OB 集群机器的内存的一部分,这取决于运维分配租户的时候给的资源多大。其次,租户的内存默认情况下有一半(50%)用于存储业务写的增量数据。OB 的读写模型是 LSM-Tree,传统数据库的读写模型是 B-Tree。OB 的内存默认一半用于存储数据基线数据,一半存储增量写。OB 的写对增量内存的消耗速度是远小于传统数据库的,所以 OB 倾向于增量修改部分尽可能在内存里不落盘(当然,事务日志 REDO 是实时落盘的,所以不会丢数据。)。不过如果业务短时间内有大量写操作,数据库增量内存可能会消耗过快进而耗尽导致应用收到内存不足报错。OB 设计上也考虑过这种情形,默认每天凌晨 2 点会自动发起一次合并操作,将内存中的增量和磁盘上基线数据在内存中合并并写回到磁盘数据文件中,大量增量内存得到释放。OB 对合并有参数控制,可以在稳定性和速度方面做一个平衡。通常合并都是在低峰期做。合并期间业务也是可以正常读写。

合并的另外一个触发条件就是内存使用达到某个阈值就进行,为了避免白天业务高峰期发生合并,OB 还可以通过转储操作释放增量内存。转储就是把内存中增量数据直接写到数据文件中临时存放,后面在需要的时候再读回来。转储对性能的影响可以控制在 2% 以内。转储也有很多参数,在稳定性和速度方面取一个平衡,并且转储可以有多轮,比较类似传统数据库的 checkpoint 。

对于业务人员来说,如果不是搞了大事务,大批量写遇到内存不足错误通常是运维需要去解决的问题。是否租户资源不足?是否相关参数没有调优设置?
这里说的是通常情况,也存在少数内存不足报错跟这个增量无关,可能是 SQL 性能方面的问题。

语句超时报错

多数人使用 OB 导入大量数据后会喜欢做一个统计看看 OB 到底有多快,结果往往会遇到查询超时报错。查询超时报错是 OB 默认的一个自我保护机制。为了防止高并发场景下大量慢 SQL 不返回,拖垮数据库和应用,OB 会对 SQL 执行时间做一个限制,超出这个时间的 SQL 会自动报错释放 CPU 资源。默认这个超时时间是 10 秒。通常这个可以通过调整租户参数解决,在实例级别、会话级别或者 sql 级别设置都可以。在文章《查询超时》里有详细描述。

调大超时参数只是解决了报错问题,并没有解决性能问题。大表的统计查询建议加上 SQL Hint。如, 

select /*+ parallel(32) query_timeout(1000000000) */ count(*) from big_table;

并行度可以从 16 开始,32,64 都尝试一下,以感受提高并行度对性能的提升。不过,也不是越高就越快。SQL 里指定并行度之后,SQL 有并发执行的时候,也要考虑 OB 服务端能提供多少个并行会话去处理这个。这个后面会再深入介绍。

语句超时不仅仅是指查询语句,还包括 DML 语句。比如说 update sql 可能因为拿不到锁而等待,等待时间到达这个超时时间后也会报超时错误,进而放弃等待啊。这个超时只是语句超时。

连接异常断开

在使用 OB 开发时,可能还会碰到连接断开的报错。其中有一种报错信息是:ERROR-02013: Lost connection to MySQL server during query 或者 ERROR-02006: MySQL server has gone away 

这个通常有两种可能。一是OB 集群正常但是 obproxy 进程挂了或者重启了。二是obproxy 正常,客户端会话事务空闲超时了。无论是哪种,只要客户端有重连时,重试查询都会恢复。

obproxy 进程挂的可能性非常小,一般是运维重启,开发可以不用担心这个。第二种事务空闲超时可能性会高一些。通常发生在 OB 的 ORACLE 实例里。ORACLE 实例下默认事务 autocommit 设置是 off 的,MySQL 实例默认事务 autocommit 设置是 ON.所以 ORACLE 实例下如果运行了SQL,然后会话一直空闲的话,事务并没有提交,可能会超出会话空闲超时时间进而被 OBServer 自动断开连接(事务也会自动回滚掉)。事务空闲超时时间默认是 120 秒,实际超时时间会比 120 秒大一点点。

事务超时

事务除了有空闲超时外,还有事务未提交超时。此时主要报错信息是 ERROR-00600: internal error code, arguments: -6210, Transaction is timeout 。事务的未提交超时时间默认是 100 秒。这个超时时间比事务空闲超时时间要少 20 秒,通常未提交的事务会首先报这个错。

一旦报错后,数据库会自动回滚掉事务(释放锁),但是事务状态会维持在“超时状态”,此时需要客户端发起一个 commit 或 rollback 命令才能清除掉超时状态。通常为了便于理解,发起 rollback 命令比较合适。


解决方法

总结一下上面三个超时机制。都是由实例(租户)变量控制。 

  • 语句超时:语句执行时间超时,由变量 ob_query_timeout 控制,单位微秒。默认时间 10 秒。

  • 事务锁等待超时:DML 语句以及 select for update 等申请写锁的语句的超时时间,可以单独由变量 ob_trx_lock_timeout 控制,单位微妙.这是后期 OB 版本增加的变量,为了兼容早期版本,默认值是-1,等待时间无限长。如果设定了具体的值,要比 ob_query_timeout 小才有意义。

  • 事务空闲超时:事务会话空闲时间超时,由变量 ob_trx_idle_timeout 控制,单位微妙。默认时间 120 秒。

  • 事务未提交超时:事务持续时间超时,由变量 ob_trx_timeout 控制,单位微妙。默认时间 10 秒。


查看超时变量值方法如下。

obclient> show variables where variable_name in ('ob_query_timeout','ob_trx_idle_timeout','ob_trx_timeout','ob_trx_lock_timeout');
+---------------------+-----------+
| VARIABLE_NAME | VALUE |
+---------------------+-----------+
| ob_query_timeout | 10000000 |
| ob_trx_idle_timeout | 120000000 |

| ob_trx_lock_timeout | -1        |

| ob_trx_timeout | 100000000 |
+---------------------+-----------+
3 rows in set (0.00 sec)

四个超时变量各自生效机制是独立的。这些是分布式数据库 OB 的自我保护机制。跟传统数据库使用习惯有很大出入。初期的时候,DBA 可以在实例全局层面调大这些参数,以避免开发人员使用过程中频繁报错。等熟悉 OB 特点后,再根据业务特点为这些参数设置一些合理的超时值。

开发也可以尝试修改这些变量值。如果账户没有权限修改全局层面的设置,那就修改会话级别的设置。也可以在语句级别设置。

obclient> set global ob_query_timeout=1000000000; ERROR-00600: internal error code, arguments: -5036, Access denied; you need (at least one of) the SUPER privilege(s) for this operation
obclient>
obclient> set session ob_query_timeout=1000000000; Query OK, 0 rows affected (0.00 sec)

obclient> show variables like 'ob_query_timeout';
+------------------+------------+
| VARIABLE_NAME | VALUE |
+------------------+------------+
| ob_query_timeout | 1000000000 |
+------------------+------------+
1 row in set (0.00 sec)

obclient> update /*+ query_timeout(300000000) */ t1 set id = id+10 ;
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2 Changed: 2 Warnings: 0

obclient>
obclient> select /*+ query_timeout(30000000) */ count(*) from t1;
+----------+
| COUNT(*) |
+----------+
| 2 |
+----------+
1 row in set (0.00 sec)

obclient> set session ob_trx_lock_timeout=5000000;
Query OK, 0 rows affected (0.00 sec)
obclient> update t1 set id = id+10 ;
ERROR-30006: resource busy; acquire with WAIT timeout expired
obclient>

下一篇介绍 OB 参数调优,敬请关注。