# 如何管理 OceanBase 集群

## OceanBase 集群架构简述

### 进程简述

OceanBase 是单进程软件，进程名是：`observer`，通常每个机器启动一个进程就是一个节点。在前面部署一课中介绍了 OceanBase 的目录架构。如下是查看 OceanBase 和 OBProxy 安装后的目录。

```bash
[admin@obce02 ~]$
[admin@obce02 ~]$ pwd
/home/admin
[admin@obce02 ~]$
[admin@obce02 ~]$ tree -L 3 --filelimit 10 oceanbase/ obproxy/ /redo /data/
oceanbase/
├── bin
│   ├── import_time_zone_info.py
│   └── observer
├── etc
│   ├── observer.config.bin
│   ├── observer.config.bin.history
│   └── timezone_V1.log
├── lib
│   ├── libaio.so -> libaio.so.1.0.1
│   ├── libaio.so.1 -> libaio.so.1.0.1
│   ├── libaio.so.1.0.1
│   ├── libmariadb.so -> libmariadb.so.3
│   └── libmariadb.so.3
├── log [588 entries exceeds filelimit, not opening dir]
├── run
│   ├── mysql.sock
│   └── observer.pid
└── store
    └── obdemo
        ├── clog -> /redo/obdemo/clog
        ├── etc2 -> /redo/obdemo/etc2
        ├── etc3 -> /data/obdemo/etc3
        ├── ilog -> /redo/obdemo/ilog
        ├── slog -> /redo/obdemo/slog
        └── sstable -> /data/obdemo/sstable
obproxy/
/redo
└── obdemo
    ├── clog [160 entries exceeds filelimit, not opening dir]
    ├── etc2
    ├── ilog [17 entries exceeds filelimit, not opening dir]
    └── slog
        ├── 5
        └── 6
/data/
└── obdemo
    ├── etc3
    └── sstable
        └── block_file

21 directories, 15 files
[admin@obce02 ~]$
```

OceanBase 目录里需要关注下面几个目录：

+ 软件安装目录。如果是用 RPM 包手动安装的，默认会安装在用户 `admin` 的 HOME 目录下（`~/`）。如果是 OBD 部署的，采取的是将 RPM 解压缩到用户的 HOME 目录下隐藏文件夹里（`~/.obd`）。
+ 工作目录。本课程里进程 `observer` 工作目录是 `~/oceanbase` ，进程 `obproxy` 启动目录是 `~/obproxy` 。进程 `observer` 和 `obproxy` 的启动都有共同特点，会在工作目录下寻找文件夹 `etc` 子目录，读取默认配置文件。如果目录不存在就默认创建一个文件夹 `etc` 。同时还会创建日志目录 `log` 等。所以需要知道进程 `observer` 和 `obproxy` 的启动目录，以便进程故障的时候，能够快速拉起进程恢复。此外，启动用户也是要保持一致。本课程使用的是用户 `admin` 。使用 OBD 部署 OceanBase 和 OBProxy 的时候，可以指定工作目录。
+ 参数目录。默认参数文件目录在工作目录下的 `etc` 目录里，此外还可以通过参数 `config_additional_dir` 设定冗余的参数目录。参数文件是二进制文件，不能直接编辑和读取，可以通过 `strings` 命令读取。
+ 日志目录。默认日志目录会在工作目录下的 `log` 目录。如果不存在，就自动生成一个。日志目录里会有三类日志：`observer.log` 、`rootservice.log` 和 `election.log` 。每个日志又有一个完整的目录和一个日志级别在 `WARN` 级别以上（含）的日志。后者可以通过参数关闭。`observer.log` 通过参数设置可以滚动生成，最多保留指定数量。 当数据库访问量非常大的时候，进程 `observer` 的日志输出量是比较大的。所以这个目录在生产环境建议可用空间不少于 100G 。并且在集群的参数里还会真多日志输出限流。
+ 总数据目录。默认目录在工作目录下的 `store` 文件夹。当然进程 `observer` 也可以通过参数 `-d` 指定特定目录。本课程里手动部署 OceanBase 的时候，建议 `store` 目录下有集群名作为目录，然后 `-d` 指定到那一级目录（如 `-d ~/oceanbase/store/obdemo` ）。这个目录下会有实际的数据文件目录（`sstable`) 和事务日志目录（`slog`、`ilog`、`clog`）。通常建议数据文件目录和事务日志目录用独立的盘存放，所以这个下面的数据文件目录和事务日志目录是一个软链接。
+ 数据文件目录。默认文件夹名叫 `sstable` ，下面只有一个大文件 `block_file` 。进程 `observer` 启动的时候初始化，初始化大小默认是磁盘可用空间的 `80%` 。这个大小可以通过参数控制。这个目录通常不要跟事务日志目录共用一块盘存储。如果节点要重置，这个目录下的内容必须清理掉。
+ 事务日志目录。这包含三个文件夹。`slog` 存储静态数据写入的事务日志，`clog` 存储动态数据写入的事务日志，`ilog` 存储日志目录。这些日志 OceanBase 会自动管理，不要手动删除。这三个目录通常建议独立一块盘存储。如果节点要重置，这三个目录下的内容必须清理掉。

### 进程监听端口

下面分别阐述进程 `observer` 和 `obproxy`  的监听端口。

进程 `observer` 第一次启动的时候，需要通过参数 `-p` 指定连接端口，参数 `-P`指定节点 RPC 通信端口。默认是 `-p 2881 -P 2882` 。进程启动成功后，不到几秒就能走到监听环节。查看方法：

```bash
[admin@obce02 oceanbase]$ pidof observer
56779

[admin@obce02 oceanbase]$ ps -ef | grep observer | grep -v grep
admin      56779       1 99 16:26 ?        00:00:49 bin/observer

[admin@obce02 oceanbase]$
[admin@obce02 oceanbase]$ netstat -ntlp | grep -i observer
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 0.0.0.0:2881            0.0.0.0:*               LISTEN      56779/bin/observer
tcp        0      0 0.0.0.0:2882            0.0.0.0:*               LISTEN      56779/bin/observer
[admin@obce02 oceanbase]$

```

最后一列中数字是进程 ID 。
排查问题的时候可以查看节点之间的 TPC 连接状态。或者查看哪个 IP 和 PORT 流量最大等。

```bash
# 查看 TCP 连接
netstat -np |grep -i observer

# 查看 TCP 连接汇总
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# 查看网卡 IP 流量
iftop -i eth0 -nNB
# 按 L 、T 、3 、t 、 B 、l 、 p  找出具体哪个 IP 和 PORT 流量最大
```

### 集群节点组织架构

OceanBase 集群是由多个节点的进程组合而成，集群最小规模是 1 个节点，这是一个单副本集群，没有高可用能力。在生产环境，OceanBase 集群最小规模是三节点。会分为三个 `Zone` 。每个 `Zone` 最少一个节点。实际上集群里节点组织关系如下：

| 集群     | ZONE  | REGION  | IDC  | IP             |
|--------|-------|---------|------|----------------|
| obdemo | ZONE1 | region1 | idc1 | 192.168.249.52 |
| obdemo | ZONE2 | region1 | idc1 | 192.168.249.49 |
| obdemo | ZONE3 | region1 | idc2 | 192.168.249.51 |
| obdemo | ZONE4 | region1 | idc2 | 192.168.249.54 |
| obdemo | ZONE5 | region2 | idc3 | 192.168.249.55 |

节点的 `Zone` 是在进程启动的时候指定的。节点的 `region` 和 `idc` 是跟 `Zone` 绑定的，在集群初始化好后查看和修改。
下面只是示例，并不表示一定要修改这个。

```sql
alter system change zone 'zone1' region 'region1';
alter system change zone 'zone2' region 'region1';
alter system change zone 'zone3' region 'region2';

alter system modify zone zone1 idc='idc1';
alter system modify zone zone2 idc='idc2';
alter system modify zone zone3 idc='idc3';

select * from __all_zone where name in ('region','idc','status','zone_type','cluster') order by name;
+----------------------------+----------------------------+-------+-----------+-------+-----------+
| gmt_create                 | gmt_modified               | zone  | name      | value | info      |
+----------------------------+----------------------------+-------+-----------+-------+-----------+
| 2021-09-12 14:49:43.533839 | 2021-09-12 14:49:43.533839 |       | cluster   |     0 | obdemo    |
| 2021-09-12 14:49:43.536334 | 2021-09-20 17:08:13.727095 | zone1 | idc       |     0 | idc1      |
| 2021-09-12 14:49:43.537407 | 2021-09-20 17:08:20.052725 | zone2 | idc       |     0 | idc2      |
| 2021-09-12 14:49:43.538453 | 2021-09-20 17:08:25.589423 | zone3 | idc       |     0 | idc3      |
| 2021-09-12 14:49:43.536334 | 2021-09-20 17:07:44.025486 | zone1 | region    |     0 | region1   |
| 2021-09-12 14:49:43.537407 | 2021-09-20 17:07:48.804019 | zone2 | region    |     0 | region1   |
| 2021-09-12 14:49:43.538453 | 2021-09-20 17:07:57.702095 | zone3 | region    |     0 | region2   |
| 2021-09-12 14:49:43.535302 | 2021-09-14 10:25:55.748709 | zone1 | status    |     2 | ACTIVE    |
| 2021-09-12 14:49:43.536334 | 2021-09-12 14:49:43.536334 | zone2 | status    |     2 | ACTIVE    |
| 2021-09-12 14:49:43.537407 | 2021-09-12 14:49:43.537407 | zone3 | status    |     2 | ACTIVE    |
| 2021-09-12 14:49:43.536334 | 2021-09-12 14:49:43.536334 | zone1 | zone_type |     0 | ReadWrite |
| 2021-09-12 14:49:43.537407 | 2021-09-12 14:49:43.537407 | zone2 | zone_type |     0 | ReadWrite |
| 2021-09-12 14:49:43.538453 | 2021-09-12 14:49:43.538453 | zone3 | zone_type |     0 | ReadWrite |
+----------------------------+----------------------------+-------+-----------+-------+-----------+
13 rows in set (0.003 sec)
```

当集群所有节点都是同机房的时候，`region` 和 `idc` 就只设置为同一个值即可。只有真的三机房部署的时候，才需要设置这些，方便集群正确管理各个节点。比如说避免把一个机房的节点错划到另外一个机房的 `zone` 里了。这些元数据是靠 DBA 维护，前面集群部署时对节点的要求只是连通性、时间延时和网络延时满足要求即可，并不要求机器是否一定在同一个机房。

在后期对集群做读写分离规划时，`region` 和 `idc` 还是会对 OBPROXY 的 SQL 路由策略有很大影响的。所以，尽量正确的设置。

## 如何管理 OceanBase 集群参数

OceanBase 集群支持参数定制，以适应不同的场景需求。参数的查看和修改主要是在集群内部租户 SYS 里。

### 参数简介

参数英文名是：`parameter` 。部分参数的生效范围是集群内所有实例，可以通过命令控制是在哪些节点生效。少数参数是在租户范围内生效。
大部分参数的变更是立即生效，极少数参数的变更是需要重启节点。

参数的值、描述、生效范围、是否要重启节点等，都可以通过命令查看。命令是：`show parameters [ like '%参数名特征%' ;`
参数的修改命令是：`alter system set 参数名 = 参数值（如果是字符串，用两个单引号引起来）[ scope = 'xxx.xxx.xxx.xxx:2882' ] ;`  。

除了集群参数可以定制集群行为外，租户也可以定制，通过变量（`variable`）设置。变量跟参数其实是同一类概念，参数的定位和风格取自 ORACLE 数据库，变量的定位和风格取自 MySQL 数据库。集群 SYS 租户一样可以设置变量值（SYS 租户变量通常不用调整，必须很谨慎），不同租户的变量设置彼此独立，互不影响。大部分变量的设置是立即生效，生效范围根据设置方法不同有实例全局层面和会话层面生效。极少数变量类似 MySQL 的初始化变量，不能在租户里后期修改，只是在 SYS 租户里新建业务租户的时候设置。当然也能后期在 SYS 租户后期修改，只是这个修改也必须充分评估影响。

变量的值、描述都可以通过命令查看。命令是： ` show global |  [session] variables [ like '%变量名特征%' ] ; ` ， 或者 `show global | [session] variables where variable_name in ('参数1' [, '参数2']) ;`
变量的值的修改命令是： ` set global | [session] 变量名 = 变量值（如果是字符串，用两个单引号引起来）; ` 。`global` 指全局生效，对新建会话生效，当前会话不生效。 `session` 或者没有限定符，都表示对当前会话生效。会话断开重连后，又会取全局层面的默认值。

部分变量还有语句级别的设置方法，通过 SQL HINT，影响范围是所修饰的语句。语句级的变量名名字会去掉前面的 `ob_` 部分。下面是常用的几个语句级别的变量命名映射关系。

| 全局|会话变量名            | 语句级变量名           | 描述                           |
|---------------------|------------------|------------------------------|
| ob_query_timeout    | query_timeout    | 指定语句执行超时的阈值                  |
| ob_read_consistency | read_consistency | 指定读一致性级别，默认是强一致性读，可以修改为弱一致性读 |

常用集群参数作用的分类如下：

+ 影响节点可用性的判断。如 `server_permanent_offline_time`  和 `server_temporary_offline_time` 。
+ 影响集群日志的特点。如  `enable_syslog_recycle` 、`enable_syslog_wf` 、`max_syslog_file_count` 、`syslog_level` 和 `syslog_io_bandwidth_limit` 。
+ 影响集群冻结和合并行为。如 `minor_freeze_times` 、`major_freeze_duty_time` 、`enable_major_freeze` 、`freeze_trigger_percentage` 、`micro_block_merge_verify_level` 、`minor_merge_concurrency` 、`merge_thread_count` 、`zone_merge_timeout` 、`enable_merge_by_turn` 、`zone_merge_order`  、`zone_merge_concurrency` `enable_manual_merge` 。
+ 影响集群节点内存分配的行为。如 `memstore_limit_percentage` 、 `memory_limit_percentage`  、`sql_audit_memory_limit` 、`system_memory`  、`memory_limit` 。
+ 影响集群负载均衡行为。如 `server_balance_cpu_mem_tolerance_percent` 、`server_balance_disk_tolerance_percent`、`balancer_tolerance_percentage` 、`data_copy_concurrency` 、 `server_data_copy_out_concurrency` 、`server_data_copy_in_concurrency` 。

以上这些参数的作用在命令里可以查，也可以查询官方文档。在后面运维、性能调优的时候用到这些参数的时候再详细描述。

### 参数文件

查看集群的参数除了登录集群 SYS 实例，还可以通过查看参数文件的方法。OB 集群参数文件名字固定是：`observer.config.bin` ，在目录 `etc/` 下。这个目录在进程工作目录下。如果是 OBD 部署的 OceanBase 集群，配置文件里里的 `home_path` 就是集群的工作目录。另外一个确定进程工作目录方法就是直接看进程的运行环境。

```bash
[admin@obce02 oceanbase]$ sudo ls -lrth /proc/`pidof observer`/cwd
lrwxrwxrwx 1 admin admin 0 Sep 21 13:10 /proc/56779/cwd -> /home/admin/oceanbase

```

进程目录下的 `cwd` 就是指向实际工作目录的软链接。集群的参数文件是二进制文件，不能直接编辑和查看，可以使用命令 `strings` 。

```bash
[admin@obce02 oceanbase]$ strings /home/admin/oceanbase/etc/observer.config.bin | grep config_addition
config_additional_dir=/data/obdemo/etc3;/redo/obdemo/etc2

```

通常集群里参数修改成功后都会持久化到参数文件里。

### 进程启动参数

如果不小心参数文件设置的不合理，可能会导致进程启动失败。而这个参数文件又不能直接修改，此时的办法就是启动进程 `observer` 的时候指定启动参数。

如：

```bash
cd /home/admin/oceanbase && bin/observer -o "memory_limit=50G, system_memory=10G"

```

这种适合调整少数参数，让进程启动起来。启动成功后，这个参数也会持久化到进程的参数文件里。

## 如何管理 OceanBase 内存

有关 OceanBase 相关的内存可以先看这个内存分配图。
![oceanbase memory](media/16357302538843.jpg)

下面逐个详细介绍。

### 进程总内存

OceanBase 进程启动后会从主机那里拿到绝大部分资源（CPU/内存/空间），具体多少就是由集群参数控制的。其中集群节点进程启动需要多少内存是由参数 `memory_limit` 和 `memory_limit_percentage` 共同控制的。生产环境通常是不设置 `memory_limit` 值（默认值为 0），只设置 `memory_limit_percentage` 值（默认值 80% ）。这样每个节点进程获取的总内存是主机可用内存的 80% 。当主机内存扩容后（比如说是虚拟机），OB 节点能获得的内存也相应增加。如果设置了 `memory_limit` 的值，那进程获取的总内存就是它指定的具体的值。这个值最少不能少于 8G 。 设置这个参数通常是测试环境跑 OceanBase ，或者想单机启动多个 OceanBase 进程测试用。

这两个参数可以在线调整，但是注意，最大不能超过主机可用内存，最小不能少于  8G 。通常调大是安全的，调小可能有风险。

### 保留内存

OceanBase 内部租户除了 SYS 租户外，还有一批不可见的内部租户需要一些内存，这部分内存通过参数 `system_memory` 大小控制。不同版本的 OB，这个参数默认值不一样。生产环境主机内存有 386G 内存以上的时候，这个参数默认值是 50G 。如果主机内存只有 256G 的时候，这个参数内存可以小一些，如 30G 左右。第 2 课里为了在 10G 内存的机器上跑 OceanBase ，这个参数值是 3~5G 。
这个内存在内部租户  ID 500 的租户下，给业务租户某些内部操作使用，是有作用的。

### SYS 租户内存

 SYS 租户是集群初始化的时候自动创建，SYS 租户的资源规格随着可用内存大小、版本变化，会有一些细节差别。SYS 租户的内存大小是由默认资源单元 `sys_unit_config` 的规格定义的。

 ```sql
 select unit_config_id, name, round(max_memory/1024/1024) max_mem_MB, round(min_memory/1024/1024) min_mem_MB from __all_unit_config where unit_config_id = 1;
+----------------+-----------------+------------+------------+
| unit_config_id | name            | max_mem_MB | min_mem_MB |
+----------------+-----------------+------------+------------+
|              1 | sys_unit_config |       1536 |       1024 |
+----------------+-----------------+------------+------------+
1 row in set (0.009 sec)
```

默认的 `sys_unit_config` 的内存规格最小值和最大值不一样，为了更准确的计算集群可用资源，建议把这个内存最小值跟最大值更新为同一个值（只能变大，不要小于默认最大值）。后续创建业务租户的时候，也强烈建议 CPU 和 内存的最小值和最大值保持一致。

```sql
alter resource unit sys_unit_config min_memory = '1536M';

```

如果集群节点规模在 10台以内， SYS 租户资源通常不需要调大。如果集群规模有几十台时，观察一下 SYS 租户的内存和CPU 利用率，如果有出现资源瓶颈，就适当调整 SYS 租户资源规格。最大的时候只需要独占一台机器资源。 SYS 租户最多只需要独占 3台机器。

### MEMSTORE 内存

每个租户分得的内存，默认有一半用于存放租户的增量数据，也叫 MEMSTOER 。这个内存大小通过参数 `memstore_limit_percentage` 控制。默认值是 `50%` 。当写多读少的时候，可以将这个参数值调高，比如说在大批量导数据的时候，没有读的时候，可以临时调整到 `80%` 。当然这个这个参数也不是任意可以调整。租户的内存除去增量数据占用的内存外，其他的放静态的数据（`KVCache`）、SQL执行计划缓存等（`PLAN CACHE`)、SQL运算的临时内存等（`SQLAREA`）。如果这些内存资源过少，可能有时候会有报错，或者性能很慢的现象出现。目前 OceanBase 对内存的管理还是手动分配为主，将来或许可以做到动态管理。

OceanBase 的读写模型是 LSM-Tree，写操作也是追加模式。数据表是以有序表格，又叫 `SSTABLE`，全称 `SORTED STRING TABLE` 。写不是在原来的数据块上修改，而是新开辟内存记录变化的增量。原有的数据保存在 `KVCache` 不变，称之为基线数据。绝大部分业务一天的数据变化量相当于数据总量来说比重很小，OceanBase 的这种增量写是很节省内存的。所以，通常说 OceanBase 的增量数据都是在内存里不落盘的。严格来说并不对，是延迟落盘。

### 如何对OB 集群进行合并

OceanBase 会每天定时将这部分增量数据跟基线数据在内存中合并成新的数据块然后以追加方式写回到磁盘数据文件中。这个合并操作叫 `major freeze`，合并的时间调度是通过参数 `major_freeze_duty_time` 设定，默认值是 `02:00` 。每天调度一次。合并操作也可以手动发起。合并的时候会消耗一定资源，对影响有一些影响。不过合并的并发也是可以通过参数 `merge_thread_count` 控制的，并且参数 `merger_warm_up_duration_time` 调整合并线程启动时间。调整这两个参数可以在合并的速度和稳定性之间取一个最佳平衡。

当租户内存不是很大，或者业务写量非常大的时候，这个租户的增量内存可能会写满，然后业务就写不进去，报错 `NO MEMORY` 。这是 OceanBase 运维要极力避免的。

命令：`alter system major freeze;` 。

```sql
SELECT ZONE,svr_ip,major_version,ss_store_count,merged_ss_store_count,modified_ss_store_count,merge_start_time,merge_finish_time,merge_process
FROM __all_virtual_partition_sstable_image_info
order by major_version desc ;
```

最终合并结果确认是查看 合并状态。

```sql
select * from __all_zone where name in ('merge_status','all_merged_version','broadcast_version');

```

正常情况都是 'IDLE'，且合并版本和广播版本保持一致。

### 如何对OB 集群进行限速和转储

首先，OceanBase 针对租户可以有一个增量内存写限速的设计，触发限速有两个维度，一是增量内存利用率到达参数 `writing_throttling_trigger_percentage` 设置，二是预估剩余增量内存持续可用时间小于或等于参数 `writing_throttling_maximum_duration` 定义。默认 `writing_throttling_trigger_percentage` 参数的值是 `100%`，就是不限速。实际业务建议都改为 最大 `90%` 。这个限速设计并不是特别精准，所以如果写入量非常大的话的，这个参数还要适当再调低。当业务写被限速的时候，业务表现为普通的 DML SQL 性能会变慢（延时变大），等待事件是 `mem allocation` 等。限速很影响性能，所以也是要极力避免的。这就要靠下一个机制。

第二，OceanBase 除了 `major freeze` 操作可以释放内存外，还有 `minor freeze` 操作也可以释放内存，这个通常也叫转储。转储相比合并，对资源的占用更少，对性能的影响也很小。转储是直接将内存中的增量数据以 SSTABLE 格式直接写到磁盘数据文件中，从而释放增量内存。
转储的触发有两个机制。一是手动触发，命令是 `alter system minor freeze ;` 。命令立即返回，命令发生时内存中的增量块会被冻结禁止写入，新的写入会生成新的增量内存块去接受写入。二是自动触发，当增量内存部分的利用率达到参数 `freeze_trigger_percentage` 指定的值（默认是 `70`，表示 `70%`），自动触发 `minor freeze` 操作。转储可以有很多轮，当转储的次数达到参数 `minor_freeze_times` 指定的值，会自动触发合并（`major freeze`）操作。
转储也是可以调优的，通过调整转储的并发参数（`minor_merge_concurrency`），也可以在转储的速度和稳定性之间取得一个平衡。

实际经验表明，当租户内存规模在 50G 以上时，通过调整转储参数，绝大部分时候是可以避免增量内存写入限速或内存不足的错误。如果业务增量写非常大的时候，那说明租户内存有瓶颈了，那就是要对内存资源扩容的信号。

注意：如果是临时大批量导入数据，可以临时对租户内存进行扩容。等导入完毕后，一轮合并结束后，就可以最租户内存进行缩容。在线扩容和缩容，也是 OceanBase 最核心和实用的能力。

## 如何重启 OceanBase 节点和集群

OceanBase 集群生产环境默认是有三副本，任意一个副本不可用，OceanBase 的数据库内部可能会有一次故障切换。对业务的影响是部分数据访问会中断，恢复时间在 30s 左右，数据绝对不丢。这个故障切换过程不需要运维介入，可靠性很高。
如果是计划中的节点重启，OceanBase 集群内部也会自动发起切换，对业务的影响是大部分读写是感知不到切换。少数大事务会中断需要重试。所以，整体上，OceanBase 集群计划重启是可以做到业务不用停机，数据库服务不中断。具体的做法就是按机器所属的 ZONE ，分批次重启 OceanBase 机器。

### (建议）OB 集群合并

由于 OceanBase 节点重启后会有一个恢复的过程，恢复的时间取决于从最近的基线版本到当前时间要恢复的数据量。OceanBase 可能要应用很多 `clog` 去追赶数据。如果追赶的 `clog` 数据量实在太大，OceanBase 节点可能会直接从主副本那里拉取最新的数据。

如果条件允许的话，在重启节点之前，先对整个集群发起一次合并。待合并结束后，每个节点内存中就只有最近一段时间的增量数据，那么节点重启后的恢复时间也是最短的。

+ 发起合并操作

命令：`alter system major freeze;` 。
具体合并操作确认逻辑请参考前面章节：如何对 OceanBase 集群进行合并 。

### 如何重启 OceanBase 节点

+ 节点停服

OceanBase 并没有提供重启节点的命令。但是提供了节点停服的命令。
命令是：`alter system stop server '节点ip:2882' ;` 。

节点停服后，节点上如果有主副本，会自动切换为备副本。节点的备副本依然参与投票，但不会当选为主副本。OB 节点停服跟 OceanBase 宕机性质不一样，节点停服时间可以超出参数永久下线时间（`server_permanent_offline_time`) 而不会导致节点真的下线。

节点停服后，大概1-2秒后就能观察到有主备副本切换事件。
确认 SQL 如下：

```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip,name3,value3,name4,value4
FROM __all_rootservice_event_history
WHERE 1 = 1   
   AND module IN ('leader_coordinator', 'balancer' )
ORDER BY gmt_create DESC
LIMIT 20;
```

通常几秒钟所有分区副本的主备切换就结束了。

+ 杀进程

然后在主机里杀掉 节点进程 `observer` 。命令如下：

```bash
kill `pidof observer`

```

如果是测试环境，比较着急可以不用发起节点停服命令，直接强行杀进程。

```bash
kill -9 `pidof observer` 
```

+ 启动进程

启动节点进程最关键的是在上一次工作目录启动。所以建议第一次就把工作目录跟安装目录保持一致，那就不会有出错的可能。

```bash
cd /home/admin/oceanbase && bin/observer

```

通常启动的时候不需要再带启动参数了。除非这次启动节点就是为了修改某个参数，那就加上 `-o` 带上具体的参数。示例如下：

```bash
cd /home/admin/oceanbase && bin/observer -o "datafile_size=80G,clog_disk_usage_limit_percentage=96 "

```

进程启动后等 5~10 秒钟后再查看一下确认进程启动成功。

```bash
# 确认进程还在
ps -ef | grep observer | grep -v grep

# 确认端口监听成功
netstat -ntlp | grep `pidof observer`

```

+ 启动服务并确认节点服务状态

进程监听成功还不够，节点还需要一个数据恢复过程，就是应用 CLOG 的过程。

如果此前对节点停服了，先把节点服务启动。

```sql
alter system start server '节点IP:2882' ;

```

确认节点的服务状态。

```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time, b.build_version 
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
```

关注：

+ 节点状态 `status` ：升级前没有 `inactive` 值，升级过程中会有。
+ 节点服务时间 `start_service_time` 的时间不是默认值（`1970-01-01 08:00:00.000000`）。如果是，则表示节点还没有恢复结束。
+ 节点停止时间 `stop_time` 的时间是默认值（`1970-01-01 08:00:00.000000`）。如果不是，则表示节点被停服（ `stop server`） 了，先启动服务（`start server`）。

一定要确认节点的`start_service_time` 正常了才去重启其他 Zone 对应的其他副本所在的机器。同一个 Zone 的多台机器是可以并行重启的。

## 如何重置 OceanBase 节点

重置 OceanBase 节点属于应急手段之一，是高危操作，仅用在特殊的场景。如下：

+ 节点磁盘损坏后修复，怀疑数据有丢失的。此时需要重做副本。
+ 想对节点数据文件大小进行缩容，重新初始化进程 `observer`，相当于重做副本。

重置 OceanBase 节点的步骤如下。

### 停掉节点进程

因为是要重置节点，所以停进程的方法就不用像重启节点那么繁琐。直接杀进程。

```bash
kill -9 `pid of observer`
```

如果进程本来就停掉了，就不用这步操作。
杀进程之前，确保知道此前的进程启动时工作目录是什么。用下面命令

```bash
ll /proc/`pidof observer`/cwd
lrwxrwxrwx 1 admin admin 0 Sep 25 08:18 /proc/15495/cwd -> /home/admin/oceanbase-ce

```

### 确认节点永久下线

通常情况下，节点进程异常，相当于节点掉线。首先节点掉线时间超过参数 `server_temporary_offline_time` 值（默认 60s ）后，状态会进入“临时下线”状态。
此后，如果节点进程能重新正常起来，节点还是集群的成员，会自动同步落后的数据。

示例：

```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time 
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;

输出：
+-------+--------------------+----------------------------+----------------------------+----------+----------------------------+
| zone  | observer           | last_offline_time          | start_service_time         | status   | stop_time                  |
+-------+--------------------+----------------------------+----------------------------+----------+----------------------------+
| zone1 | 172.20.249.52:2882 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:06.622351 | active   | 1970-01-01 08:00:00.000000 |
| zone2 | 172.20.249.49:2882 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:07.392669 | active   | 1970-01-01 08:00:00.000000 |
| zone3 | 172.20.249.51:2882 | 1970-01-01 08:00:00.000000 | 1970-01-01 08:00:00.000000 | inactive | 1970-01-01 08:00:00.000000 |
+-------+--------------------+----------------------------+----------------------------+----------+----------------------------+
3 rows in set (0.008 sec)
```

节点掉线后进入“临时下线”状态时，上面节点视图的 `status` 列会变为 `inactive` 。
同时，在 OceanBase 事件日志视图里也会有一笔“临时下线”记录。

```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1   
   AND module IN ('server','root_service')
   and gmt_create > SUBDATE(now(),interval  1800 second)
ORDER BY gmt_create DESC
LIMIT 10;

输出：
+----------------+--------------+------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
| gmt_create_    | module       | event            | name1  | value1               | name2 | value2                                                                                                                                                                                               | rs_svr_ip     |
+----------------+--------------+------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
| Sep26 12:39:32 | root_service | admin_set_config | ret    | 0                    | arg   | {items:[{name:"rootservice_list", value:"172.20.249.52:2882:2881;172.20.249.49:2882:2881", comment:"", zone:"", server:"0.0.0.0", tenant_name:"", exec_tenant_id:1, tenant_ids:[]}], is_inner:false} | 172.20.249.52 |
| Sep26 12:39:32 | server       | lease_expire     | server | "172.20.249.51:2882" |       |                                                                                                                                                                                                      | 172.20.249.52 |
+----------------+--------------+------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
2 rows in set (0.001 sec)
```

节点掉线首先会有个 `lease_expire` 事件，都是节点掉线。节点掉线原因可以有很多。如进程宕掉、网络超时或延时过大、时间误差过大等。
此外，由于这个示例集群是 三节点，所以一个节点的掉线对总控服务成员也是有影响的，所以参数 `rootservice_list` 会自动变化，踢掉了故障节点。

如果节点掉线时间再超过参数 `server_permanent_offline_time` 值（默认是 3600s ）后，节点会进入“永久下线”状态。此时，集群会清空该节点上的数据副本，并自动在同 Zone 其他节点寻求资源补足被清空的数据副本。如果没有可用资源，则这个副本对应的分区就只剩下两个副本（或者四个副本）。此时依然是多数派副本可用，所以数据读写是正常的，只是如果再宕机就面临进一步风险，可能就是不可用。

```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip FROM __all_rootservice_event_history WHERE 1 = 1       AND module IN ('server','root_service')    and gmt_create > SUBDATE(now(),interval  7200 second) ORDER BY gmt_create DESC LIMIT 10;

输出：
+----------------+--------------+----------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
| gmt_create_    | module       | event                | name1  | value1               | name2 | value2                                                                                                                                                                                               | rs_svr_ip     |
+----------------+--------------+----------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
| Sep26 13:39:48 | server       | clear_with_partition | server | "172.20.249.51:2882" |       |                                                                                                                                                                                                      | 172.20.249.52 |
| Sep26 13:39:22 | server       | permanent_offline    | server | "172.20.249.51:2882" |       |                                                                                                                                                                                                      | 172.20.249.52 |
| Sep26 12:39:32 | root_service | admin_set_config     | ret    | 0                    | arg   | {items:[{name:"rootservice_list", value:"172.20.249.52:2882:2881;172.20.249.49:2882:2881", comment:"", zone:"", server:"0.0.0.0", tenant_name:"", exec_tenant_id:1, tenant_ids:[]}], is_inner:false} | 172.20.249.52 |
| Sep26 12:39:32 | server       | lease_expire         | server | "172.20.249.51:2882" |       |                                                                                                                                                                                                      | 172.20.249.52 |
+----------------+--------------+----------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
4 rows in set (0.004 sec)
```

从上面可以看到，节点被永久下线时，会有事件 `permanent_offline`，节点的数据也会随后被清空，有事件 `clear_with_partition`。

从“临时下线”到“永久下线”时间可能有点长，默认 1个小时。如果你不想等，可以临时把这个节点的参数 `server_permanent_offline_time` 调小。等节点永久下线后再重新上线时，把参数再改回来。

```sql
alter system set server_permanent_offline_time='360s' server='172.20.249.51:2882';

```

### （可选）清理数据库相关文件

通常如果目的是重建副本，这一步并不是必须的。当想借重建副本操作重新定义一下数据文件的大小。

清理的文件包括：

+ 运行日志。包括 `log` 目录下  `observer.log`、`rootservice.log`、`election.log` 。
+ 数据文件。包括 `sstable` 下的 `block_file`  文件。
+ 事务日志文件。包括 目录 {`ilog`、`clog`、`slog` } 下的文件。

注意，参数文件 `etc` 不要删除，这样再次启动节点进程的时候方便一些，只需要在启动参数 `-o` 里指定需要修改的参数。

下面删除只是示例，注意目录结构：

```bash
[admin@obce03 store]$ cd /home/admin/oceanbase-ce/store
[admin@obce03 store]$ ls
clog  ilog  slog  sstable
[admin@obce03 store]$ pwd
/home/admin/oceanbase-ce/store
[admin@obce03 store]$ /bin/rm -rf */*

```

### 启动进程

如果没有清理参数文件，则可以直接启动进程 `observer` 。可以通过 `-o` 修改自己的参数。

```bash
cd /home/admin/oceanbase-ce && bin/observer 

或 

cd /home/admin/oceanbase-ce && bin/observer  -o "datafile_size=100G"

```

启动后确认端口监听正常，节点状态正常，这个前面已经阐述，就不再重复了。

## 如何给 OceanBase 集群扩容/缩容/替换机器

OceanBase 集群扩容或缩容主要是调整集群机器资源池。
扩容就是往集群里加机器，考虑点很简单，复杂的是缩容。缩容的前提是剩余的机器能容纳所有租户的资源需求。如果资源不足，通常就要先对集群里的租户进行缩容，这个会在后面介绍到。所以集群的缩容留到租户缩容介绍后再补充介绍。

集群机器替换就是先扩容后缩容，集群机器资源池并没有发生变化，所以操作也很简单。

### 集群扩容节点

OceanBase 集群的标准部署模式是有三个 ZONE，每个ZONE 的机器数量都对等，每个 ZONE 里可以存在不同配置的机器，但是不同 ZONE 之间的机器配置是对等的，机器资源总量也是对等的。非标准部署就是 ZONE 之间的机器配置不对等、数量也不对等。技术上这也是可以运行的，只是资源最少的机器或 ZONE 会成为集群资源的瓶颈。

OceanBase 集群扩容的标准形式就是向每个 ZONE 里添加同等配置的机器。非标准做法就是只向一个 ZONE 里添加机器。步骤如下：

+ 初始化新机器环境，启动进程 OBSERVER ，注意在启动参数里指定该机器所属的 ZONE 。

OBSERVER 进程第一启动，需要指定一些参数，具体步骤可以参考第 2 课安装部署。部分参数再重复说明一下。

```
-p : 这是连接端口。默认是 2881 ，可以根据实际情况修改
-P : 这是 RPC 端口，默认是 2882 ，可以根据实际情况修改
-r ：指定 `rootservice list`，跟集群当前的 `rootservice list` 保持一致。
-z ：指定 节点所属 ZONE 。

```

+ 在集群 SYS 租户里添加这个新节点。

命令是：`ALTER SYSTEM ADD SERVER '节点IP:RPC端口'  ZONE  '节点所属ZONE';`

```sql
alter system add server '11.166.87.5:2882' zone 'zone1';

```

以上命令立即返回，然后查看节点状态就可以看到新节点的状态是 `active` 。

```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time 
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
```

然后换下一个 ZONE 继续新增机器。当三个 ZONE 都扩容完毕，集群扩容操作就完成了。是不是很简单？
不过，这里只是运维的工作结束了。集群扩容后，新的节点还不会马上被集群里的租户使用到。集群资源池扩大后，新节点的利用率是 0 ，约 1 min 集群就会发现资源利用不均衡，就会启动负载均衡逻辑，尝试将其他租户的资源单元迁移到新的节点上。集群缩容也会触发负载均衡。有关负载均衡的介绍留在后面。

集群给每个 ZONE 扩容节点后，集群整体的资源池能力新增了，可用的资源也增加了。但是租户的资源并没有增加。租户不一定能利用上新机器，所以后面还有租户扩容。

### 集群缩容节点

OceanBase 集群的标准部署模式是有三个 ZONE，每个ZONE 的机器数量都对等。集群缩容节点就是减小某个 ZONE 的节点数。通常缩容节点建议每个 ZONE 都减少相同的节点。实际操作没有这个限制，是按 ZONE 操作。

集群缩容节点时，会出发租户资源的负载均衡。剩余的节点必须能容纳租户资源需求。否则，这个节点缩容命令会报错。所以，集群节点缩容之前，也是要计算集群可用资源和了解各个节点的资源池分布。

集群缩容节点命令就是将某个节点从某个 ZONE 里删除。
示例 SQL 如下：

```sql
alter system delete server '11.166.87.5:2882' zone 'zone1';

```

集群缩容节点，一定会触发租户资源负载均衡操作，会触发数据迁移逻辑。建议关注数据迁移的进度和影响。
查看集群事件日志视图，可以观察到这个。

```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1 
   AND gmt_create > SUBDATE(now(),interval  1 hour)
ORDER BY gmt_create DESC
LIMIT 20;
```

## 如何给 OceanBase 集群扩容/缩容副本

### 三副本扩容到五副本

生产环境 OceanBase 集群通常有三个 ZONE，分布在一个机房或者同城三个机房，数据有三份也简称三副本。当要建设异地容灾机房的时候，可能会选择从三个 ZONE 扩容到 五个 ZONE（即五副本）。另外一种就是机房搬迁。将集群从一个机房在线搬迁到另外一个机房。这其中都涉及到三副本扩容到五副本。

+ 机器节点进程初始化

主要是 ZONE4 和 ZONE5 的机器初始化，注意启动参数中所属的 ZONE 要正确。

+ 新增 ZONE

```sql
alter system add zone 'ZONE4';
alter system add zone 'ZONE5';

```

+ 给新增的 ZONE 添加节点

```sql
alter system add server '11.166.87.5:2882' zone 'zone4';
alter system add server '11.166.87.6:2882' zone 'zone5';
```

+ 为租户在新增节点上分配资源

```sql
create resource pool test_pool_z4 unit='unit_4c16g' , unit_num=1, zone_list=('ZONE_4');
create resource pool test_pool_z5 unit='unit_4c16g' , unit_num=1, zone_list=('ZONE_5');

```

该命令立即返回。

+ 修改租户的 LOCALITY ，给租户增加新增的资源池

注意，每次只能给租户新增一个资源池。

```sql
alter tenant test227 resource_pool_list=('test_pool_z1','test_pool_z2','test_pool_z3','test_pool_z4');

alter tenant test227 resource_pool_list=('test_pool_z1','test_pool_z2','test_pool_z3','test_pool_z4','test_pool_z5');

```

该命令立即返回。但是租户在新增 ZONE 补数据副本是异步进行的，租户分区数越多，数据量越大，这个时间越长。

+ (可选）确认复制表的副本是否扩容

复制表的 LOCALITY 可以指定在哪些 ZONE 里配置复制表副本，也可以不指定，默认是整个集群范围内。如果原来指定 LOCALITY，这里需要手动添加新的 ZONE 。
这个待验证。

```sql
alter table BMSQL_ITEM locality='F,R{all_server}@ZONE1, F,R{all_server}@ZONE2, F,R{all_server}@ZONE3, F,R{all_server}@ZONE4,F,R{all_server}@ZONE_5';

```

+ 确认集群和租户副本扩容结果

```sql

select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time 
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;

select t1.name resource_pool_name, t2.`name` unit_config_name, t2.max_cpu, t2.min_cpu, t2.max_memory/1024/1024/1024 max_mem_gb, t2.min_memory/1024/1024/1024 min_mem_gb, t3.unit_id, t3.zone, concat(t3.svr_ip,':',t3.`svr_port`) observer,t4.tenant_id, t4.tenant_name
from __all_resource_pool t1 join __all_unit_config t2 on (t1.unit_config_id=t2.unit_config_id)
    join __all_unit t3 on (t1.`resource_pool_id` = t3.`resource_pool_id`)
    left join __all_tenant t4 on (t1.tenant_id=t4.tenant_id)
order by t1.`resource_pool_id`, t2.`unit_config_id`, t3.unit_id
;

select * from __all_tenant;

```

### 五副本缩容到三副本

将上面步骤反过来执行即可。

+ （可选）调整复制表的 LOCALITY ，缩减副本数。

如果有复制表，并且当初复制表创建的时候指定了

+ 修改租户的 LOCALITY，缩减副本数。

```sql
alter tenant sys resource_pool_list = ('sys_pool','sys_pool_1');
alter tenant sys resource_pool_list = ('sys_pool');
alter tenant test227 resource_pool_list=('test_pool_z1','test_pool_z2','test_pool_z3','test_pool_z4');
alter tenant test227 resource_pool_list=('test_pool_z1','test_pool_z2','test_pool_z3');

```

注意，SYS 租户也许要缩减副本数。
这一步命令立即返回，但是删除副本会异步进行，速度也很快，分区数多一些也不要紧，基本上在分钟级别完成。

+ 删除多余的资源池

```sql
drop resource pool sys_pool_1 ;
drop resource pool sys_pool_2 ;
drop resource pool test_pool_z4 ;
drop resource pool test_pool_z5 ;
```

## 如何升级 OceanBase 集群

OceanBase 集群升级就是将集群所有节点进程 OBSERVER 的版本升级。通常小版本的升级是直接替换可执行文件 `observer` 然后将进程重启即可。但有时候升级还会涉及到元数据的变更，这个就需要参考新版本发布时具体的 RELEASE NOTE 说明。

### 确认集群当前状态正常

OceanBase 集群可以在线不停服升级，具体版本就是按 ZONE 滚动升级。升级前首先要确认集群和各个 ZONE 状态都正常。避免停掉某个 ZONE 时出现多数派故障，数据库访问不可用问题等。

+ 确保集群节点状态正常

```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time, b.build_version 
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
```

关注：

+ 节点状态 `status` ：升级前没有 `inactive` 值，升级过程中会有。
+ 节点服务时间 `start_service_time` 的时间不是默认值（`1970-01-01 08:00:00.000000`）。如果是，则表示节点异常。
+ 节点停止时间 `stop_time` 的时间是默认值（`1970-01-01 08:00:00.000000`）。如果不是，则表示节点被 `stop server` 了。
+ 节点版本 `b.build_version` 。升级重启后会变为新版本。

+ 观察集群最近一段时间的事件，确保无异常事件

```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1   
   AND module IN ('server','root_service','daily_merge')
   and gmt_create > SUBDATE(now(),interval  1 day)
ORDER BY gmt_create DESC
LIMIT 100;
```

如果大量正常的事件影响查看，就过滤掉。
留意报错的事件是否有影响。下列异常事件一定要先解决。如：合并异常、副本创建/搬迁异常等。

+ （可选）发起合并（`MAJOR FREEZE`）

由于集群升级都需要重启节点。为了减少节点重启后的恢复时间，建议升级之前发起一次合并。具体合并方法和确认方法跟“如何重启OB节点”中方法一样。

```sql
alter system major freeze;
```

### 停止 ZONE 服务

OceanBase 集群的升级，按 ZONE 滚动升级。首先选中一个 ZONE ，将该 ZONE 停服。

```sql
alter system stop zone 'zone1';

SELECT * FROM __all_zone where name in ('status','merge_status') and zone = 'zone1'; 
输出：
+----------------------------+----------------------------+-------+--------------+-------+----------+
| gmt_create                 | gmt_modified               | zone  | name         | value | info     |
+----------------------------+----------------------------+-------+--------------+-------+----------+
| 2021-09-25 08:19:05.067944 | 2021-09-29 15:44:27.162171 | zone1 | merge_status |     0 | IDLE     |
| 2021-09-25 08:19:05.066915 | 2021-09-29 19:36:13.108063 | zone1 | status       |     1 | INACTIVE |
+----------------------------+----------------------------+-------+--------------+-------+----------+
2 rows in set (0.028 sec)
```

查看事件日志确认。

```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1   
   AND module IN ('server','root_service','leader_coordinator')
   AND gmt_create > SUBDATE(now(),interval  1 hour)
ORDER BY gmt_create DESC
LIMIT 20;

输出：
+----------------+--------------------+--------------------------+------------+----------------------+-----------+--------------------------------+---------------+
| gmt_create_    | module             | event                    | name1      | value1               | name2     | value2                         | rs_svr_ip     |
+----------------+--------------------+--------------------------+------------+----------------------+-----------+--------------------------------+---------------+
| Sep29 19:36:40 | root_service       | full_rootservice         | result     | 0                    | self_addr | "172.20.249.51:2882"           | 172.20.249.51 |
| Sep29 19:36:39 | server             | load_servers             | ret        | 0                    | has_build | 1                              | 172.20.249.51 |
| Sep29 19:36:39 | server             | online                   | server     | "172.20.249.52:2882" |           |                                | 172.20.249.51 |
| Sep29 19:36:39 | server             | online                   | server     | "172.20.249.49:2882" |           |                                | 172.20.249.51 |
| Sep29 19:36:39 | server             | load_servers             | ret        | 0                    | has_build | 1                              | 172.20.249.51 |
| Sep29 19:36:39 | root_service       | finish_start_rootservice | result     | 0                    | self_addr | "172.20.249.51:2882"           | 172.20.249.51 |
| Sep29 19:36:39 | root_service       | start_rootservice        | self_addr  | "172.20.249.51:2882" |           |                                | 172.20.249.51 |
| Sep29 19:36:39 | root_service       | finish_wait_stop         | cost       | 350586               |           |                                | 172.20.249.52 |
| Sep29 19:36:38 | root_service       | finish_stop_thread       | ret        | 0                    | self_addr | "172.20.249.52:2882"           | 172.20.249.52 |
| Sep29 19:36:38 | root_service       | stop_rootservice         | self_addr  | "172.20.249.52:2882" |           |                                | 172.20.249.52 |
| Sep29 19:36:38 | leader_coordinator | switch_leader            | current_rs | "172.20.249.52:2882" | tenant_id | 1                              | 172.20.249.52 |
| Sep29 19:36:22 | leader_coordinator | switch_leader            | current_rs | "172.20.249.52:2882" | tenant_id | 1                              | 172.20.249.52 |
| Sep29 19:36:13 | leader_coordinator | switch_leader            | current_rs | "172.20.249.52:2882" | tenant_id | 1002                           | 172.20.249.52 |
| Sep29 19:36:13 | leader_coordinator | switch_leader            | current_rs | "172.20.249.52:2882" | tenant_id | 1002                           | 172.20.249.52 |
| Sep29 19:36:13 | root_service       | stop_zone                | ret        | 0                    | sql_text  | alter system stop zone 'zone1' | 172.20.249.52 |
+----------------+--------------------+--------------------------+------------+----------------------+-----------+--------------------------------+---------------+
15 rows in set (0.026 sec)
```

从事件日志看出，当停掉 ZONE 的时候，如果总控服务（`rootservice`）的主副本也在该 ZONE 时，总控服务会发生切换（停掉老的服务，在新的节点上启动新的服务，其他 ZONE 所有节点重新上线），SYS 租户和业务租户都发生切换。

确认节点状态：

```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time 
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;

输出：
+-------+--------------------+----------------------------+----------------------------+--------+----------------------------+
| zone  | observer           | last_offline_time          | start_service_time         | status | stop_time                  |
+-------+--------------------+----------------------------+----------------------------+--------+----------------------------+
| zone1 | 172.20.249.52:2882 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:06.622351 | active | 1970-01-01 08:00:00.000000 |
| zone2 | 172.20.249.49:2882 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:07.392669 | active | 1970-01-01 08:00:00.000000 |
| zone3 | 172.20.249.51:2882 | 1970-01-01 08:00:00.000000 | 2021-09-26 14:05:58.641570 | active | 1970-01-01 08:00:00.000000 |
+-------+--------------------+----------------------------+----------------------------+--------+----------------------------+
3 rows in set (0.017 sec)

```

### 更新节点的 OBSERVER 软件包

通常是 RPM 包更新。方法是：`rpm -uvh oceanbase-xxx.rpm` 。
命令会自动覆盖可执行文件。但是由 LINUX 系统的设计可知，运行中的 OBSERVER 进程依然持有老的文件句柄，所以不会释放文件。只有在 OceanBase 节点进程退出后才释放。

如果使用了 obd 管理 OceanBase 集群，软件包还需要维护到 OBD 本地仓库或远程仓库里。
