前言
OceanBase 是一个通用的分布式的关系型数据库, 有很多独特的特点. 比如数据库的多租户, 高可用, 极致弹性伸缩能力. 如果把 OceanBase 当作单库使用, 就没有把 OceanBase 的分布式优势发挥到极致.
本文主要分享一个基于分布式架构的应用把 OceanBase 数据库的分布式优势发挥到极致所需要了解的 OceanBase 基础, 这也是理解蚂蚁金服的基于 OceanBase 构建的三地五中心异地多活架构的基础.
分布式数据库开发相关问题
好的性能首先是设计出来的, 应用如果追求极致的性能, 就需要关注 OceanBase 里数据的相关事情. 如:
数据如何分布?
数据如何读写?
存储容量瓶颈怎么办?
访问性能瓶颈怎么办?
数据库出故障时数据可用性和可靠性具体怎样? 应用需要做什么特殊处理么?
数据库扩展时应用需要迁移数据么? 数据迁移的时候对应用有什么影响?
这些问题对理解 OceanBase 的分布式特点很有帮助. 后面我们逐步看看 OceanBase 是如何应对.
OceanBase 集群外观
首先简介一下 OceanBase 集群的外观.
图 1 OceanBase 集群外观
OceanBase 是以集群形式运行的, 由一堆服务器组成. 上图是「三副本」部署, 机器会分为三组, 每组一个区域(称为 Zone), 各个机器通过网络互相访问. 没有光纤交换机, 共享存储以及直连网线等.
服务器通常建议 CPU, 内存和磁盘尽可能的大, 磁盘建议用普通 SSD 盘. 普通服务器的好处是便宜, 劣势是可靠性和性能可能不如小型机那么高. 也就是说 OceanBase 可以部署在一组可靠性和性能不是特别高的普通服务器上, 却提供了高性能, 高可用和高可靠, 弹性伸缩等多项能力.
以上是一个 OceanBase 集群的外观和能力, 但是提供给业务的并不是这个集群的全部资源和能力, 而是其子集, 即租户(Tenant).
OceanBase 多租户特性
OceanBase 定义了一些基本的资源规格(Resource unit config, 如 4CPU8Gmem500Gdisk 等), 然后选取某类资源规格创建一组资源池(Resource Pool), 此时集群资源就有一部分被分配出去了. 最后将这个资源池关联到一个新建租户, 则租户就可以使用这个资源池的能力.
OceanBase 默认有个 sys 租户, 管理整个集群. 用户租户必须在 sys 租户内部创建.
如下示例就是创建租户的过程.
- #sys 租户登录方法
- $MySQL -hxxx.xx.11.11 -uroot@sys#obdemo -P2883 -proot oceanbase -A
- # 资源规格(UnitConfig)
- createresourceunitS0_uc max_cpu=2,max_memory='5G',...
资源单元(Unit)
createresourcepoolPool_01 unit='S0_uc',unit_num=2,...
租户(Tenant)
createtenanttest_ins resource_pool_list=('Pool_01'),...
OceanBase 兼容了大部分 MySQL 连接协议和语法, 租户的使用体验跟 MySQL 实例很像. 研发可以在租户里创建数据库(Database), 表(Table). 还包括分区表等.
OceanBase 里描述数据的最小粒度是分区. 普通的表 (非分区表) 就是一个分区, 分区表则包含多个分区.
租户的示意图如下. 租户之间数据是绝对隔离, 资源有一定程度隔离. 研发可以将业务先垂直拆分为多个独立的子业务, 分别使用不同的租户或者集群.
图 2 OceanBase 多租户示意图
OceanBase 资源单元
租户里并不知道数据具体在哪个机器上, 也可以说没必要知道. 只是租户的性能还取决于运维为租户规划的资源池分布情况, 所以了解一下资源单元的分布特点对性能规划也是有意义的.
资源池 (Resource Pool) 是由一组资源单元 (Resource Unit) 组成. 资源单元数量默认跟 Zone 的数量一致或者是它的倍数(可以配置具体分布在哪些 Zone 以及每个 Zone 里的 Unit 数量). 如下图
图 3 OceanBase 资源池分配示意图
资源单元具备一定的资源能力, 是数据的容器. 租户拥有的资源单元规格和数量决定了这个租户最大性能. 资源单元可以在同一个 Zone 的不同节点之间自由迁移, OceanBase 借此来维持各个节点的资源利用率尽可能维持一个均衡状态.
OceanBase 拆分设计
数据库拆分
数据库拆分有两种.
一是垂直拆分. 即按业务模块拆分到不同的实例或库里. 为了模块之间互不影响, 拆分到不同的实例比较好. 在 OceanBase 里实现时可以是拆分到同一个集群里不同租户或者不同集群里的租户都可以, 取决于业务规模和数据库集群规模. 垂直拆分很好理解, 后面不再赘述.
一是水平拆分. 即按某个业务维度将数据拆分到多个分片. 这些分片可以是在一个库或者不同库或者不同实例的不同库下. 水平拆分实现又有两类常用的选择. 如下:
分库分表. 将一个业务表拆分到 N 个相同结构的物理表中. 中间件做业务表 (逻辑表) 到分表 (物理表) 的映射路由以及其他相关操作 (如结果聚合计算) 等. 这个 N 个物理表可以在不同实例的不同分库中. 分库的维度和分表的维度可以不一样, 比较灵活.
分区表. 将一个物理表设计为分区表, 拆分到 N 个分区. 分区表的各个分区结构是数据库内部保证一致. OceanBase 选择的是分区表的水平拆分方式, 并且支持二级分区表(即有 2 个不同的拆分维度叠加使用).
水平拆分示意图如下:
图 4 水平拆分的分库分表和分区表
上图是分库分表和分区表同时结合使用. 业务表 order 先经过中间件拆分为 100 个分表(存在 10 个分库里), 每个分表在 OceanBase 内部又是一个分区表(100 个分区). 分库分表的维度和分区表分区的维度都是一致的, 根据用户 ID.
分库分表和分区各有利弊.
分库分表的好处是各个分表的结构一致性是在中间件层保证, 比较好控制, 比较适合灰度变更(允许部分分表结构不一致, 最终必须全部一致). 此外更大的好处是, 分库分表是实现异地多活单元话架构的必不可少的条件. 缺点是中间件的 SQL 支持范围有限.
分区的好处是在数据库内部解决了拆分问题. 针对分区表的 SQL 功能是数据库 SQL 引擎的本质工作, 相关特性 (全局索引, 二级分区等) 会持续开发完善.
分区
分库分表架构设计, 需要确定机器数, 实例数, 分库数和分表数的拓扑, 性能理论上限取决于主实例所处的机器节点数. 此后要做扩展就要调整这四个元素的数目及其联系. 这种扩展很可能涉及到分表数据的迁移, 需要借助外部工具或产品实现.
分区架构设计, 研发确定分区策略和分区数, 运维确定租户的资源单元数量, OceanBase 确定资源单元 (Unit) 在哪些机器节点上以及分区 (Partition) 在哪些资源单元里. 同一个分区不能跨节点存储. 如下图. 此后要做扩展就是调整资源单元的规格, 数量.
OceanBase 在确定 Unit 里的分区的位置时会尽量让每个节点的负载维持均衡. 这个负载的计算方式比较复杂, 会综合考虑 OB 节点内部 CPU, 内存和空间利用率等. 分区随意分布对应用性能很可能有负面影响. 当业务上有联系的两个表的分区分布在不同的资源单元里(同时也分布在不同的节点里), 这两个表的连接就难以避免跨节点请求数据, 网络上的延时会影响这个连接的性能.
图 5 分区在资源单元中的位置
注: t1(p0) 表示表 t1 的 0 号分区.
每个分区在集群里数据实际有三份, 即三副本(Replica). 图中忽略了 Zone2 和 Zone3 的细节. 三副本之间的数据同步靠把 Leader 副本的事务日志同步到其他 Follower 副本中. Paxos 协议会保障这个事务日志传输的可靠性(事务日志在一半以上成员里落盘, 剩余成员最终也会落盘), 同时还有个以分区为粒度的选举机制, 保障 Leader 副本不可用的时候, 能快速从现有两个 Follower 副本里选举出新的 Leader 副本, 并且数据还绝对不丢. 这里就体现了故障切换时两个重要指标: RPO=0, RTO<30s.
Locality
图 5 中 t0 和 t1 业务上是有联系的表 (如主表和详情表), 两者都是分区表, 分区策略和分片数都相同, OceanBase 提供了一个表属性 "表分组"(TableGroup). 设置为同一个表分组的不同表的分区数一定一样, 并且同号分区组成一个 "分区分组"(PartitionGroup). 同一个分区分组的分区一定会分配在同一个资源单元(Unit) 内部(也就是会在同一个节点内部), 彼此的连接逻辑就避免了跨节点请求. 另外一个效果是如果一个事务同时修改两个有业务关联的分区, 使用分区分组也可以规避跨节点的分布式事务. 这个表分组属性的设置就是 OceanBase 的 Locality 特性之一 -- 影响相关分区的分布.
实际上每个分区都有三副本 (Replica, 本文例子), 图 5 中省略了 t0(p0) 和 t1(p0)的其他两个副本都分别会在同一个 Unit 里分配. 不仅如此, 每个分区的三副本里都会有 leader 副本默认提供读写服务. leader 副本是选举出来的. t0(p0)和 t1(p0)的 leader 副本也一定会在同一个 Unit 里(即在同一个 Zone 里). 这样才彻底的避免了连接的时候跨节点请求.
OceanBase 的 Locality 特性还可以指定租户 / 数据库 / 表的默认 Zone, 这样下面的表的 leader 副本会优先被选举为这个 Zone 里副本.
如下面例子, 数据库和表会继承租户的默认设置, 当然也可以自己修改 primary_zone 或者 locality 属性覆盖上层设置.:
- create tenantobtrans0primary_zone='hz1';
- create table item (...)locality= 'F@hz1, F@hz2, F@hz3,R{all_server}@hz1, R{all_server}@hz2, R{all_server}@hz3'
注: F 表示全功能副本, R 表示只读副本.
设置 primary_zone 后单个租户的所有表的读写都集中到一个 Zone 里, 该租户在其他 zone 里的 Unit 就没有读写压力. 通常这样做是源于业务应用的要求. 如应用的请求都是来自于这个 Zone, 为了规避应用跨 Zone 读写数据性能下降. 不过 primary_zone 更大的意义在于当集群里有很多租户的时候, 可以将不同业务租户的读写压力分摊到不同 Zone 的机器, 这样集群整体资源利用率提升, 所有应用的总体性能也得到提升. 后面要介绍的异地多活架构就充分利用 OceanBase 这个特性, 在数据库层面将拆分的表的业务读写点尽可能分散到所有 Zone 的所有机器上.
除了表与表之间可能有联系, 业务模块之间也可能有联系. 一个业务过程可能会横跨多个业务模块, 前面这些模块的数据被垂直拆分到多个租户里. OceanBase 的 Locality 特性 "租户分组"(TenantGroup)还可以设置不同租户之间的联系. 如下租户交易订单和支付订单在业务上是先后发生.
create tenantgroup tgtrade tenant_array=('obtrade0', 'obpay0');
租户分组的意义依然是为了在分布式架构下尽可能将一个业务流程内多次数据库请求都约束在同一个 Zone 或者 Region(注: OceanBase 将地域相邻的 Zone 定义为一个 Region)里.
OceanBase 异地多活架构
异地多活概念
异地多活的概念一直都有, 只是内涵不断变化. 以双机房多活为例, 应用通常都是无状态的, 可以多地部署. 数据库有状态, 传统数据库只有主库可以提供读写, 备库最多只能提供只读服务(如 ORACLE 的 Active Dataguard):
1. 应用双活, 数据库 A 地读写, B 地不可读写. 这种只有应用多活, 数据库是异地备份容灾(无并发).
2. 应用双活, 数据库 A 地读写, B 地只读. 这种也是应用双活, 数据库读写分离(实例级并发).
3. 应用双活, 数据库双活, 两地应用同时读写不同表. 这种数据库双向同步, 应用同时错开写不同的数据(表级并发).
4. 应用双活, 数据库双活, 两地应用同时读写相同表不同记录. 这种数据库双向同步, 应用同时错开写不同的数据(行级并发).
5. 应用双活, 数据库双活, 两地应用同时读写相同表相同记录. 这种数据库双向同步, 应用同时写相同的数据, 最终会因为冲突一方事务回滚(行级并发写冲突)
上面第 1 种情形, B 地应用是跨地域远程读写数据库. 两地距离较大的时候性能会很不好. 2 的 B 地应用是本地访问数据库. 3,4,5 三种情形两地数据库都提供读写服务, 对应用而言是本地访问数据库, 但到分布式数据库内部, 其要读写的数据是否正好在本地就取决于业务和数据库的拆分设计. 有这么一种情形, B 地应用访问 B 地数据库实例, 请求的数据的写入点恰好是 A 地, 这样会走分布式数据库内部路由远程 A 地实例中拿到数据, 性能上会有些下降, 而业务可能不知道为什么.
OceanBase 水平拆分方案
为了避免在分布式数据库 OceanBase 内部发生跨 Zone 请求, 应用的请求流量的水平拆分规则和数据的水平拆分规则要保持一致并联动, 就可以实现真正的应用向本地实例请求读写的数据恰好就是在本地实例种. 这就是 Locality 的用途所在.
图 6 OceanBase 集群异地多活水平拆分示意图
首先业务架构层面能根据用户 ID(uid)做拆分, 将用户拆分为 100 分. x 和 y 是用户直接相关的表, 都根据 uid 拆分为 100 个分表, 分布在 5 个不同的租户里. x[00-19]表示 20 个分表. 每个租户的 Leader 分别分布在不同的 Zone.uid:00-19 表示 分到这 20 个分片的用户数据和用户流量.
应用层面在某个环节能根据 UID 将用户请求转发到对应的机房 (Zone), 应用服务之间的请求信息都有这个 UID 信息, 后面应用请求都在本机房流转, 并且访问数据库时通过分库分表中间件(DBP) 和 OceanBase 的反向代理 (OBProxy) 就路由到本机房的业务租户上.
实际使用这个架构时, 有几个隐含的前提, Zone1 和 Zone2 是同城双机房, Zone3 和 Zone4 是同城双机房, 两个城市靠的比较近, Zone5 实际很远, 所以一般不提供写入. 为节省空间, Zone5 里的副本放的是日志副本.
应用异地多活架构
上面要实现 OceanBase 在每个 Zone 的应用写入都是恰好是本地访问, 关键点就是应用流量水平拆分规则跟数据水平拆分规则保持一致. 而应用要维持这样一个规则, 需要很多产品都要支持水平拆分等. 如下图
图 7 应用三地五中心多活解决方案
图中流量接入层的 "负载均衡" 环节负责调整用户流量到对应的机房(Zone), 此后的应用之间的请求就都是本地访问.
能使用这个解决方案的业务都是能按用户维度做水平拆分. 有些业务的拆分维度跟用户维度是冲突的, 或者有些业务的数据只支持集中写入等, 不能做到上面这种多活, 但也可以使用 OceanBase, 实现单点写入, 多点读取的功能.
OceanBase 在异地容灾和多活架构方案中的价值就是支持水平拆分规则的定义, 解决了多个机房间数据同步和一致性问题, 始终具备高可用和弹性伸缩能力等.
参考
OceanBase 负载均衡的魅力
蚂蚁金服异地多活的微服务体系
来源: https://yq.aliyun.com/articles/684005