本文就是我从零搭建读写分离主备 + 异步集群的完整记录,希望对你也有帮助。
搭之前看看架构图,知道自己每一步在干嘛:
+-------------------------------------------------------------+
| DM8 数据守护集群 |
| |
| +-----------+ REDO 实时同步 +-----------+ |
| | 主库 | --------------> | 实时备库 | |
| | 131 DW1_01| <-------------- | 132 DW1_02 | |
| | PRIMARY | 故障时可切换 | STANDBY | |
| +-----+-----+ +------------+ |
| | |
| | REDO 异步定时发送(每1分钟批量推) |
| v |
| +-----------+ +-----------+ |
| | 异步备库 | | 确认监视器 | |
| | 133 DW1_03| | 134 | |
| | STANDBY | | dmmonitor | |
| +-----------+ +-----------+ |
+-------------------------------------------------------------+
读写分离的核心逻辑与适用场景
概念:达梦的读写分离集群底层依赖数据守护(Data
Watch)机制。简单来说,主库(Primary)包揽所有的写入和修改操作,备库(Standby)在实时同步主库数据的同时,帮主库分担查询(SELECT)的压力。
适用场景:最适合“读多写少”的业务模型。以“全局通知发布系统”为例:**管理员在后台发布了一条全员通知(这是写入动作,只有 1 次并发),但瞬间会有成千上万的员工打开 APP 去查看这条通知(这是读取动作,可能有 10000 次并发)。在这种大家只能看、不能评论的场景下,所有的查询压力都被完美分流到了备库上,主库在海量并发下依然能稳如泰山。
三种角色,一句话区分:
集群规划:
| 1 机器 | 2 机器 | 3 机器(异步) | |
|---|---|---|---|
| 业务 IP | 192.168.166.131 | 192.168.166.132 | 192.168.166.133 |
| 心跳 IP | 192.168.166.131 | 192.168.166.132 | 192.168.166.133 |
| 实例名 | DW1_01 | DW1_02 | DW1_03 |
| 实例端口 | 15236 | 15236 | 15236 |
| MAL 端口 | 5336 | 5336 | 5336 |
| MAL 守护进程端口 | 5436 | 5436 | 5436 |
| 守护进程端口 | 5536 | 5536 | 5536 |
| INST_OGUID | 45331 | 45331 | 45331 |
| 守护组 | GDW1 | GDW1 | GDW1 |
| 安装路径 | /dmdbms | /dmdbms | /dmdbms |
| 实例路径 | /dmdata/ | /dmdata/ | /dmdata/ |
| 归档路径 | /dmarch | /dmarch | /dmarch |
| 归档上限(MB) | 51200 | 51200 | 51200 |
| 确认监视器 IP | 192.168.166.134 |
关于业务 IP 和心跳 IP:生产环境这两个必须用不同网卡分开,心跳走专用内网,业务走外网,这样才能避免业务流量把心跳链路堵死导致误判故障。本实验是学习环境,只有一块网卡,所以两者填的是同一个 IP,知道这个差异就行。
(1)按照https://eco.dameng.com/community/post/202604061906375B122IUUKRZZT42M2B完成基础环境(至3.5)虚拟机规格如下:
| 项目 | 配置 |
|---|---|
| 内存 | 2.9 GB |
| 处理器 | 2 核 |
| 硬盘 | 50 GB |
| 网络模式 | NAT |
(2)root用户创建所需目录、赋予权限
mkdir /dmdbms mkdir /dmdata mkdir /dmarch chown -R dmdba:dinstall /dmdbms chown -R dmdba:dinstall /dmdata chown -R dmdba:dinstall /dmarch
(3)修改为静态ip,务必删除uuid
vi /etc/sysconfig/network-scripts/ifcfg-ens33
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
# BOOTPROTO=dhcp 从dhcp改成static
BOOTPROTO=static
DEFROUTE=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=ens33
#UUID=f72480f8-57dc-4bcd-b5a4-01e056963051
DEVICE=ens33
# ONBOOT=no 从no改成yes
ONBOOT=yes
#IPADDR配置相关的IPV4地址
IPADDR=192.168.166.130
#NETMASK配置对应的子网掩码
NETMASK=255.255.255.0
#GATEWAY配置对应的网关
GATEWAY=192.168.166.1
#DNS1、DNS2配置对应的域名解析服务器
DNS1=8.8.8.8
(1)基础机器做好后直接克隆三份(完整克隆,快照克隆会共享磁盘,别用):
VMware → 右键虚拟机 → 管理 → 克隆 → 创建完整克隆
克隆三份,命名 dmdw02、dmdw03、dmdw04
(2)分配ip:
机器1、2、3、4执行
# 以 131 为例,修改 IP
vi /etc/sysconfig/network-scripts/ifcfg-ens33
IPADDR=192.168.166.131
systemctl restart NetworkManager
VMware → 右键虚拟机 →设置 → 网络适配器 → 高级 → 生成MAC地址 → 确定 → 重启
# 改主机名
hostnamectl set-hostname dmdw01 # 131 机器
hostnamectl set-hostname dmdw02 # 132 机器
hostnamectl set-hostname dmdw03 # 133 机器
hostnamectl set-hostname dmdw04 # 134 机器
所有操作以 dmdba 用户执行,注册服务切换 root。
只在 1 机器执行 dminit,备库不要自己初始化:
[dmdba@~]$ /dmdbms/bin/dminit PATH=/dmdata/ INSTANCE_NAME=DW1_01 PORT_NUM=15236 PAGE_SIZE=32 EXTENT_SIZE=32 LOG_SIZE=2048 SYSDBA_PWD=Dameng@1234 SYSAUDITOR_PWD=Dameng@1234
PAGE_SIZE=32这个参数在 init 之后就锁死了,后面改不了,一定要最开始就定好。
前台临时启动,用 disql 连上去改参数:
[dmdba@~]$ /dmdbms/bin/dmserver /dmdata/DAMENG/dm.ini
# 新开一个终端连进去
[dmdba@~]$ /dmdbms/bin/disql SYSDBA/'"Dameng@1234"':15236
-- 先切到 MOUNT 模式,很多参数只有在 MOUNT 状态才能改
SQL> ALTER DATABASE MOUNT;
SQL> SP_SET_PARA_VALUE (2,'PORT_NUM',15236);
SQL> SP_SET_PARA_VALUE (2,'DW_INACTIVE_INTERVAL',60); -- 守护进程心跳间隔
SQL> SP_SET_PARA_VALUE (2,'ALTER_MODE_STATUS',0); -- 禁止手工切换主备角色,防止误操作
SQL> SP_SET_PARA_VALUE (2,'ENABLE_OFFLINE_TS',2); -- 允许备库表空间脱机
SQL> SP_SET_PARA_VALUE (2,'MAL_INI',1); -- 开启 MAL 通信,集群必须开
SQL> SP_SET_PARA_VALUE (2,'ARCH_INI',1); -- 开启归档,集群必须开
SQL> SP_SET_PARA_VALUE (2,'TIMER_INI',1); -- 开启定时器,给异步归档用
SQL> SP_SET_PARA_VALUE (2,'RLOG_SEND_APPLY_MON',64); -- 监控发送/应用日志的滑动窗口大小
SQL> ALTER DATABASE OPEN;
关掉前台实例,然后
做脱机备份:
[dmdba@~]$ /dmdbms/bin/dmrman CTLSTMT="BACKUP DATABASE '/dmdata/DAMENG/dm.ini' FULL BACKUPSET '/dmdata/DAMENG/bak/BACKUP_FILE' PARALLEL 4"
这里备份的意义不是日常备份,而是给备库提供"初始数据"。后面备库会用这份备份做还原,和主库保证数据一致性。
[dmdba@~]$ vi /dmdata/DAMENG/dmarch.ini
[ARCHIVE_LOCAL]
ARCH_TYPE = LOCAL # 本地归档,必须有,是其他归档的"原材料"
ARCH_DEST = /dmarch # 本地归档存放路径
ARCH_FILE_SIZE = 1024 # 单个归档文件大小,单位 MB
ARCH_SPACE_LIMIT = 51200 # 归档总上限,超了会自动清理最老的,单位 MB
[ARCHIVE_REALTIME]
ARCH_TYPE = REALTIME # 实时归档,主库每次 COMMIT 都会实时推给备库
ARCH_DEST = DW1_02 # 目标是实时备库的实例名(不是 IP!)
[ARCHIVE_ASYNC]
ARCH_TYPE = ASYNC # 异步归档,按定时器批量推
ARCH_DEST = DW1_03 # 目标是异步备库的实例名
ARCH_TIMER_NAME = TIMER1 # 关联哪个定时器(在 dmtimer.ini 里定义)
重点:
ARCH_DEST填的是实例名,不是 IP 或主机名。系统通过 dmmal.ini 里的实例名去查对应的 IP。这个设计的好处是切换后不用改归档配置,因为实例名不变。
这个文件必须在所有节点上完全一致,少一个节点或者写错 IP 都会导致通信失败。
[dmdba@~]$ vi /dmdata/DAMENG/dmmal.ini
MAL_CHECK_INTERVAL = 10 # MAL 链路心跳检测间隔,秒
MAL_CONN_FAIL_INTERVAL = 10 # 超过这个时间收不到响应,判定链路断开
MAL_TEMP_PATH = /dmdata/malpath/ # 日志传输的临时缓冲目录,要提前建好
MAL_BUF_SIZE = 512 # 单个 MAL 连接的缓冲大小,单位 MB
MAL_SYS_BUF_SIZE = 2048 # MAL 系统总缓冲上限,单位 MB
MAL_COMPRESS_LEVEL = 0 # 日志压缩等级,0 不压缩,跨地域传输可以考虑开
[MAL_INST1]
MAL_INST_NAME = DW1_01 # 实例名,与 dm.ini 中 INSTANCE_NAME 对应
MAL_HOST = 192.168.166.131 # 心跳 IP,用于日志传输和守护通信
MAL_PORT = 5336 # MAL 监听端口,用于实例间日志传输
MAL_INST_HOST = 172.16.166.131 # 业务 IP,应用程序连接数据库用
MAL_INST_PORT = 15236 # 业务端口,数据库对外服务端口
MAL_DW_PORT = 5436 # 守护进程对外监听端口(守护进程互联、监视器连接用)
MAL_INST_DW_PORT = 5536 # 实例监听守护进程的端口(守护向实例发指令用)
[MAL_INST2]
MAL_INST_NAME = DW1_02
MAL_HOST = 192.168.166.132
MAL_PORT = 5336
MAL_INST_HOST = 172.16.166.132
MAL_INST_PORT = 15236
MAL_DW_PORT = 5436
MAL_INST_DW_PORT = 5536
[MAL_INST3]
MAL_INST_NAME = DW1_03
MAL_HOST = 192.168.166.133
MAL_PORT = 5336
MAL_INST_HOST = 172.16.166.133
MAL_INST_PORT = 15236
MAL_DW_PORT = 5436
MAL_INST_DW_PORT = 5536
端口关系:5336 是节点间传日志的,5436 是守护进程之间、守护进程和监视器握手的,5536 是守护进程向实例下命令用的(比如"你给我切换成 STANDBY")。三个端口职责完全不同,不要混淆。
[dmdba@~]$ vi /dmdata/DAMENG/dmwatcher.ini
[GDW1]
DW_TYPE = GLOBAL # 全局守护,主库和实时备库用这个;异步备库用 LOCAL
DW_MODE = AUTO # AUTO=故障自动切换,MANUAL=故障手动切换
DW_ERROR_TIME = 20 # 多少秒收不到远端守护进程的心跳,认定对方故障,秒
INST_ERROR_TIME = 20 # 多少秒连不上本机实例,认定本机实例故障,秒
INST_RECOVER_TIME = 60 # 主库守护进程尝试恢复实例的间隔时间,秒
INST_OGUID = 45331 # 整个守护系统的唯一标识,所有节点必须相同!
INST_INI = /dmdata/DAMENG/dm.ini # 本机 dm.ini 路径
INST_AUTO_RESTART = 1 # 实例崩溃后守护进程自动拉起,生产必开
INST_STARTUP_CMD = /dmdbms/bin/DmServiceDW start
RLOG_SEND_THRESHOLD = 0 # 主库发送日志延迟告警阈值,0=关闭告警
RLOG_APPLY_THRESHOLD = 0 # 备库应用日志延迟告警阈值,0=关闭告警
OGUID 必须一致:它是整个守护组的组号,如果不一致,守护进程启动后会互相认不出对方。
这个文件是异步备库的核心,主库靠它来决定什么时候、什么频率把归档推给异步备库。
[dmdba@~]$ vi /dmdata/DAMENG/dmtimer.ini
示例一:每隔 1 分钟推送一次(适合实验环境,实时性强)
[TIMER1]
TYPE = 2 # 按日执行模式
FREQ_MONTH_WEEK_INTERVAL = 1 # 间隔周/月数
FREQ_SUB_INTERVAL = 1 # 间隔天数
FREQ_MINUTE_INTERVAL = 1 # 间隔分钟数,这里设 1 分钟推一次
START_TIME = 00:00:00
END_TIME = 00:00:00 # 开始和结束都是 00:00:00 代表全天有效
DURING_START_DATE = 2020-01-01 01:01:01
DURING_END_DATE = 9999-12-31 23:59:59
NO_END_DATE_FLAG = 1
DESCRIBE = RT TIMER
IS_VALID = 1
示例二:每天凌晨 01:30~04:30 推送(适合生产,错开业务高峰)
[TIMER1]
TYPE = 2
FREQ_MONTH_WEEK_INTERVAL = 1
FREQ_SUB_INTERVAL = 0
FREQ_MINUTE_INTERVAL = 0
START_TIME = 01:30:00 # 从凌晨 1:30 开始
END_TIME = 04:30:00 # 到凌晨 4:30 结束
DURING_START_DATE = 2020-01-01 01:01:01
DURING_END_DATE = 9999-12-31 23:59:59
NO_END_DATE_FLAG = 1
DESCRIBE = RT TIMER
IS_VALID = 1
异步备库的数据延迟到底有多大,完全由这个定时器决定。1 分钟推一次,最大 RPO 就是 1 分钟的数据量。生产上具体设多少,要结合业务对数据损失的容忍度来定。
[dmdba@~]$ scp -r /dmdata/DAMENG dmdba@192.168.166.132:/dmdata/ [dmdba@~]$ scp -r /dmdata/DAMENG dmdba@192.168.166.133:/dmdata/
为什么不在备库重新 dminit,而是要拷贝?
两个原因:
dminit会生成随机的加密密钥,备库如果用自己的密钥初始化,根本无法解析主库传来的数据。- 每个实例初始化时会生成一个数据库
permanent_magic(永久魔数),主库传日志前会校验这个值是否匹配,不一致直接拒绝。所以:只在主库
dminit,然后把整个实例目录拷过去,备库通过dmrman RESTORE/RECOVER来建立自己的数据版本,再用UPDATE DB_MAGIC更新"数据库魔数"(注意不是永久魔数)来标记这是一个独立的还原版本。
# 注册数据库服务:-m mount 是关键!必须以 MOUNT 状态启动
# 如果以 OPEN 启动,数据库会开始产生 Redo 日志,破坏主备数据一致性
# 以 root 执行
[root@~]# /dmdbms/script/root/dm_service_installer.sh -t dmserver -p DW -dm_ini /dmdata/DAMENG/dm.ini -m mount
# 注册守护进程服务
[root@~]# /dmdbms/script/root/dm_service_installer.sh -t dmwatcher -p Watcher -watcher_ini /dmdata/DAMENG/dmwatcher.ini
拷贝过来的实例名还是 DW1_01,要改成自己的:
[dmdba@~]$ vi /dmdata/DAMENG/dm.ini
INSTANCE_NAME = DW1_02 # 改成自己的实例名,这是唯一需要改的 dm.ini 参数
2 机器是实时备库,也配了 ARCHIVE_ASYNC,故障切换后,DW1_02 会变成新的主库,那时候它就需要负责向异步备库 DW1_03 推送日志。
[dmdba@~]$ vi /dmdata/DAMENG/dmarch.ini
[ARCHIVE_LOCAL]
ARCH_TYPE = LOCAL
ARCH_DEST = /dmarch
ARCH_FILE_SIZE = 1024
ARCH_SPACE_LIMIT = 51200
[ARCHIVE_REALTIME]
ARCH_TYPE = REALTIME
ARCH_DEST = DW1_01 # 反向指向主库——切换后 DW1_02 变主库,DW1_01 变备库,要向它同步
[ARCHIVE_ASYNC]
ARCH_TYPE = ASYNC
ARCH_DEST = DW1_03
ARCH_TIMER_NAME = TIMER1
dmmal.ini、dmwatcher.ini、dmtimer.ini 与 1 机器完全相同,不需要改。
[root@~]# /dmdbms/script/root/dm_service_installer.sh -t dmserver -p DW -dm_ini /dmdata/DAMENG/dm.ini -m mount
[root@~]# /dmdbms/script/root/dm_service_installer.sh -t dmwatcher -p Watcher -watcher_ini /dmdata/DAMENG/dmwatcher.ini
用主库的备份在备库上做还原,建立一致的数据起点:
# 第一步:还原(用备份覆盖当前数据文件)
[dmdba@~]$ /dmdbms/bin/dmrman CTLSTMT="RESTORE DATABASE '/dmdata/DAMENG/dm.ini' FROM BACKUPSET '/dmdata/DAMENG/bak/BACKUP_FILE'"
# 第二步:恢复(应用备份集中的归档日志,把数据推进到备份结束时)
[dmdba@~]$ /dmdbms/bin/dmrman CTLSTMT="RECOVER DATABASE '/dmdata/DAMENG/dm.ini' FROM BACKUPSET '/dmdata/DAMENG/bak/BACKUP_FILE'"
# 第三步:更新数据库魔数(标记这是一个独立的数据库实体,区别于主库)
[dmdba@~]$ /dmdbms/bin/dmrman CTLSTMT="RECOVER DATABASE '/dmdata/DAMENG/dm.ini' UPDATE DB_MAGIC"
异步备库和实时备库的配置差异主要集中在三个地方:
dm.ini关闭定时器、dmarch.ini只配本地归档、dmwatcher.ini用 LOCAL 守护类型。
[dmdba@~]$ vi /dmdata/DAMENG/dm.ini
INSTANCE_NAME = DW1_03 # 实例名
TIMER_INI = 0 # 关闭定时器!异步备库只接收日志,不主动推送,所以不需要定时器
异步备库只配本地归档就够了,它的职责就是「被动接收」,不需要向其他节点推送什么:
[dmdba@~]$ vi /dmdata/DAMENG/dmarch.ini
[ARCHIVE_LOCAL]
ARCH_TYPE = LOCAL
ARCH_DEST = /dmarch
ARCH_FILE_SIZE = 1024
ARCH_SPACE_LIMIT = 51200
# 没有 REALTIME 也没有 ASYNC,纯被动接收节点
这里是异步备库和其他节点最关键的区别:DW_TYPE = LOCAL,本地守护模式:
[dmdba@~]$ vi /dmdata/DAMENG/dmwatcher.ini
[GDW1]
DW_TYPE = LOCAL # 本地守护!不参与全局切换决策,监视器不会选它当主库
DW_MODE = AUTO
DW_ERROR_TIME = 20
INST_ERROR_TIME = 20
INST_OGUID = 45331 # OGUID 还是要相同
INST_INI = /dmdata/DAMENG/dm.ini
INST_AUTO_RESTART = 1
INST_STARTUP_CMD = /dmdbms/bin/DmServiceDW start
LOCAL 守护的含义:这个守护进程只负责看着本机实例活没活,不参与主备切换的投票。就算它大声喊"主库挂了",监视器也不会理它。这设计很合理——异步备库数据本来就有延迟,当然不能让它自作主张接管。
dmmal.ini、dmtimer.ini 与 1 机器相同(拷贝过来的,不用改)。
[root@~]# /dmdbms/script/root/dm_service_installer.sh -t dmserver -p DW -dm_ini /dmdata/DAMENG/dm.ini -m mount
[root@~]# /dmdbms/script/root/dm_service_installer.sh -t dmwatcher -p Watcher -watcher_ini /dmdata/DAMENG/dmwatcher.ini
[dmdba@~]$ /dmdbms/bin/dmrman CTLSTMT="RESTORE DATABASE '/dmdata/DAMENG/dm.ini' FROM BACKUPSET '/dmdata/DAMENG/bak/BACKUP_FILE'"
[dmdba@~]$ /dmdbms/bin/dmrman CTLSTMT="RECOVER DATABASE '/dmdata/DAMENG/dm.ini' FROM BACKUPSET '/dmdata/DAMENG/bak/BACKUP_FILE'"
[dmdba@~]$ /dmdbms/bin/dmrman CTLSTMT="RECOVER DATABASE '/dmdata/DAMENG/dm.ini' UPDATE DB_MAGIC"
监视器是整个守护系统的"仲裁者",放在独立的第四台机器上(不能放在集群节点上,否则脑裂时仲裁失效)。
切换模式说明:
| dmwatcher 要求 | dmmonitor 要求 | 说明 | |
|---|---|---|---|
| 故障手动切换 | DW_MODE = MANUAL | MON_DW_CONFIRM = 0 | 非确认监视器 |
| 故障自动切换 | DW_MODE = AUTO | MON_DW_CONFIRM = 1 | 需要确认监视器,放在仲裁机 |
[dmdba@~]$ vi /dmdbms/bin/dmmonitor.ini
MON_DW_CONFIRM = 1 # 1=确认监视器,发起切换需要多数票(防脑裂的关键)
MON_LOG_PATH = ../log
MON_LOG_INTERVAL = 60 # 每 60 秒记录一次系统状态到日志
MON_LOG_FILE_SIZE = 512
MON_LOG_SPACE_LIMIT = 2048
[GDW1]
MON_INST_OGUID = 45331
MON_DW_IP = 192.168.166.131:5436 # IP 对应 MAL_HOST,端口对应 MAL_DW_PORT
MON_DW_IP = 192.168.166.132:5436
MON_DW_IP = 192.168.166.133:5436 # 异步备库也要加进来,纳入监控范围
[root@~]# /dmdbms/script/root/dm_service_installer.sh -t dmmonitor -p Monitor -monitor_ini /dmdbms/bin/dmmonitor.ini
| 命令 | 含义 |
|---|---|
list |
查看守护进程的配置信息 |
show global info |
查看所有实例组的信息 |
tip |
快速查看当前运行状态 |
login |
登录(执行切换操作前必须登录) |
logout |
退出登录 |
choose switchover GDW1 |
主库正常时,查看可切换为主库的备库列表 |
switchover GDW1.实例名 |
主库正常时,将指定实例切换为主库 |
choose takeover GDW1 |
主库故障时,查看可接管的备库列表 |
takeover GDW1.实例名 |
主库故障时,让指定备库接管 |
takeover force GDW1.实例名 |
强制接管,不管数据是否完整(慎用) |
1 机器(主库):
[dmdba@~]$ /dmdbms/bin/DmServiceDW start
[dmdba@~]$ /dmdbms/bin/disql SYSDBA/'"Dameng@1234"':15236
SQL> SP_SET_OGUID(45331); -- 先设 OGUID,告诉数据库自己属于哪个守护组
SQL> ALTER DATABASE PRIMARY; -- 声明为主库
2 机器(实时备库):
[dmdba@~]$ /dmdbms/bin/DmServiceDW start
[dmdba@~]$ /dmdbms/bin/disql SYSDBA/'"Dameng@1234"':15236
SQL> SP_SET_OGUID(45331);
SQL> ALTER DATABASE STANDBY;
3 机器(异步备库):
[dmdba@~]$ /dmdbms/bin/DmServiceDW start
[dmdba@~]$ /dmdbms/bin/disql SYSDBA/'"Dameng@1234"':15236
SQL> SP_SET_OGUID(45331);
SQL> ALTER DATABASE STANDBY; -- 跟实时备库一样,都是 STANDBY 模式
[dmdba@~]$ /dmdbms/bin/DmWatcherServiceWatcher start
# 前台启动,能实时看到切换日志,调试时推荐
[dmdba@~]$ /dmdbms/bin/dmmonitor /dmdbms/bin/dmmonitor.ini
# 后台启动
[dmdba@~]$ /dmdbms/bin/DmMonitorServiceMonitor start
验证集群状态,在监视器前台执行:
show
# 各节点 WSTATUS = OPEN、RSTAT = VALID 就说明集群正常
# 启动:启守护进程就行,数据库会被自动拉起
1/2/3 机器:/dmdbms/bin/DmWatcherServiceWatcher start
# 停止
1/2/3 机器:/dmdbms/bin/DmWatcherServiceWatcher stop
1/2/3 机器:/dmdbms/bin/DmServiceDW stop
dm_svc.conf 放 /etc/System32System32 和 SysWOW64TIME_ZONE=(480)
LANGUAGE=(cn)
# 服务别名,列出集群所有节点
DW1=(192.168.166.131:15236, 192.168.166.132:15236)
[DW1]
CLUSTER=(DW) # 守护组名,对应 dmwatcher.ini 里的 [GDW1]
SWITCH_TIMES=(300) # 连接失败时重试次数
SWITCH_INTERVAL=(200) # 重试间隔,毫秒
RW_SEPARATE=(1) # 开启读写分离,写请求路由主库,读请求路由备库
第 1 步:在 IDEA 中创建项目
Java: 选择 8
在依赖选择页面,勾选以下两项:
Web -> Spring Web
SQL -> JDBC API
第 2 步:安放并配置达梦驱动
在项目的根目录新建一个文件夹,命名为 lib。
达梦安装目录找到 DmJdbcDriver8.jar,放在lib文件夹里。
打开 pom.xml,添加
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<version>8.1.2</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/DmJdbcDriver18.jar</systemPath>
</dependency>
第 3 步:编写配置文件
server:
port: 8080
spring:
datasource:
# 认准达梦驱动
driver-class-name: dm.jdbc.driver.DmDriver
#不写 IP,只写服务名 DW1
url: jdbc:dm://DW1
# 你的数据库账号和密码
username: SYSDBA
password: Dameng@1234
第 4 步:写入测试代码
在 src/main/java/com/example/dmclustertest/(你的基础包名路径)下,新建一个 Java 类,命名为 DmClusterTestController。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
package org.example.demo418;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DmClusterTestController {
@Autowired
private JdbcTemplate jdbcTemplate;
@GetMapping("/test-dm")
public String testCluster() {
try {
// 简单的查询测试,证明连接畅通
String instanceName = jdbcTemplate.queryForObject("SELECT INSTANCE_NAME FROM V$INSTANCE", String.class);
String dbVersion = jdbcTemplate.queryForObject("SELECT * FROM V$VERSION WHERE ROWNUM = 1", String.class);
return "<div style='font-family: Arial; padding: 20px; line-height: 1.6;'>" +
"<h2 style='color: green;'>✅ 达梦数据库连接成功!</h2>" +
"<b>当前连接的数据库实例名:</b> <span style='color: red; font-size: 20px;'>" + instanceName + "</span><br>" +
"</div>";
} catch (Exception e) {
return "<h2 style='color: red;'>❌ 连接失败</h2><br>错误信息: " + e.getMessage();
}
}
}
第 5 步:启动
应用代码不感知集群拓扑变化,切换后只需要驱动重新路由,应用代码一行不改。
(1)读写分离是怎么实现的?
读写分离的实现,依赖于驱动(如 DM JDBC、DPI 等)与 dm_svc.conf 文件的深度配合,整个过程对应用层代码透明。其核心路由逻辑如下:
拓扑感知与连接分配:
应用发起连接时,接口会根据 dm_svc.conf 中配置的 IP 列表进行**“裂变访问”,探测并获取当前集群的主、备机信息**。随后,严格根据配置的 LOGIN_MODE(登录模式)参数来返回相应的节点:
0(默认值):优先连主库(Primary) → 其次普通库(Normal) → 最后备库(Standby)
1:只连主库(Primary)
2:只连备库(Standby)
3:优先连备库(Standby) → 其次主库(Primary) → 最后普通库(Normal)
4:优先连普通库(Normal) → 其次主库(Primary) → 最后备库(Standby)
试探性路由分发:
在开启读写分离RW_SEPARATE=(1)后,接口在处理 SQL 时采用了试错策略:
优先抛给备库:接口会优先把 SQL 语句发送到备库去执行。
成功即返回:如果是普通的 SELECT 查询,备库执行成功,结果就直接返回给用户,完美分担压力。
失败切主库:如果是 INSERT/UPDATE/DELETE 或是DDL 语句,备库(因为是只读状态)执行必然会报错。接口在底层瞬间捕获到这个失败后,会立刻将同一个事务里的所有操作,全部打包重新发送给主库去执行。只要发生了切主,该事务周期内的后续所有 SQL 都会死死锁定在主库上,保证数据绝对的一致性。
(2)测试读写分离是否真的生效了
我们可以利用刚才 Spring Boot 里的测试代码,做验证。
验证读请求路由(走备库):
直接在浏览器访问我们刚才写的 /test-dm 接口(里面只执行了 SELECT INSTANCE_NAME FROM V$INSTANCE)。
👉 预期结果:页面显示的当前连接实例名应该是 DW1_02(实时备库)。这说明纯查询操作被成功分发给了备库。
验证写请求路由(走主库):
覆盖原来的 DmClusterTestController.java:
package org.example.demo418;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DmClusterTestController {
@Autowired
private JdbcTemplate jdbcTemplate;
@GetMapping("/test-dm")
public String testCluster() {
try {
// 1. 纯写操作:只要执行了写类型的 SQL(如 CREATE, INSERT, UPDATE, DELETE),
// 达梦驱动就会立刻将当前事务/连接强制路由给主库。
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS RW_TEST_ONLY_WRITE(ID INT)");
jdbcTemplate.execute("INSERT INTO RW_TEST_ONLY_WRITE SELECT 1");
// 2. 验证路由结果:此时查询实例名,应该一定是主库的实例名(例如 DW1_01)
String instanceName = jdbcTemplate.queryForObject("SELECT INSTANCE_NAME FROM V$INSTANCE", String.class);
String dbVersion = jdbcTemplate.queryForObject("SELECT * FROM V$VERSION WHERE ROWNUM = 1", String.class);
// 3. 随手清理测试表,方便下次刷新页面继续测试
jdbcTemplate.execute("DROP TABLE RW_TEST_ONLY_WRITE");
return "<div style='font-family: Arial; padding: 20px; line-height: 1.6;'>" +
"<h2 style='color: green;'>✅ 纯写操作路由测试成功!</h2>" +
"<b>执行了 CREATE 和 INSERT 语句。</b><br>" +
"<b>当前连接被强制路由到的实例名:</b> <span style='color: red; font-size: 20px;'>" + instanceName + "</span>" +
" <span style='color: #666;'>(预期应为主库实例)</span><br>" +
"</div>";
} catch (Exception e) {
return "<h2 style='color: red;'>❌ 测试失败</h2><br>错误信息: " + e.getMessage();
}
}
}
👉 预期结果:页面显示的实例名会变成 DW1_01(主库)。
直接 Kill 主库进程,守护进程超过 INST_ERROR_TIME联系不上主库后触发切换流程,确认监视器收到两个守护进程的确认后,自动将 DW1_02 提升为新主库。
在监视器前台观察日志,能看到切换的完整过程。主库重启后,守护进程自动将其降级为备库,执行 show 确认角色变化。
# 在监视器执行
login
switchover GDW1.DW1_01 # 切换到指定实例
| 维度 | 主库(PRIMARY) | 实时备库 | 异步备库 |
|---|---|---|---|
| 角色声明 | ALTER DATABASE PRIMARY | ALTER DATABASE STANDBY | ALTER DATABASE STANDBY |
| 读写权限 | 可读可写 | 只读 | 只读 |
| REDO 日志角色 | 产生 REDO | 接收并重演 | 接收并重演 |
| dmarch.ini | LOCAL + REALTIME + ASYNC | LOCAL + REALTIME(反向)+ ASYNC | 只有 LOCAL |
| dmwatcher DW_TYPE | GLOBAL | GLOBAL | LOCAL |
| TIMER_INI | 1(开,负责推送) | 1(开,切换后备用) | 0(关,只接收) |
| 参与切换决策 | 是 | 是(可成为新主库) | 否(不参与) |
| 数据延迟 | 无 | 毫秒级 | = 定时器间隔 |
MAL(Message Access Layer)是达梦守护系统的底层通信框架,所有的日志传输、心跳检测、切换指令都跑在 MAL 上。
+------------------+ +------------------+
| 主库进程 | MAL_PORT(5336) | 备库进程 |
| MAL 发送端 | -----------------> | MAL 接收端 |
| | << REDO 日志 >> | |
+------------------+ +------------------+
| |
MAL_DW_PORT(5436) MAL_DW_PORT(5436)
| |
v v
+------------------+ << 心跳+指令 >> +------------------+
| 守护进程 dmwatcher| <---------------> | 守护进程 dmwatcher|
+------------------+ +------------------+
| |
MAL_INST_DW_PORT(5536) MAL_INST_DW_PORT(5536)
| |
v v
数据库实例 数据库实例
(守护向实例发"切换"指令走这里)
故障或变慢:无论备库是宕机,还是因网络/磁盘原因响应变慢,主库都会立刻将其归档状态置为无效(Invalid)并暂停同步,以防拖慢主库的业务处理。
自动切换模式:监视器自动选举合适的备库接管。注意:对网络稳定性要求极高,网络抖动极易引发脑裂。
手动切换模式:系统不自动干预,需DBA通过监视器手动执行接管命令。
故障重启:原主库修复重启后,会根据日志(LSN)等记录,自动判定是继续做主库,还是降级为备库重新加入集群。
自动触发:备库重启或网络恢复后,主库守护进程会自动触发恢复流程,无需监视器或人工干预。
核心动作:主库向备库发送归档日志来同步历史数据。当有多个备库需要恢复时,系统支持动态并行恢复,大幅提升效率。
恢复完成:数据同步一致后,备库归档恢复为有效状态,主库短暂切为 Suspend 后立刻切回 Open,集群恢复正常。
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 同城双机房(延迟 ≤10ms) | 实时主备 | 低延迟 |
| 异地容灾(北京→广州,延迟 ≥50ms) | 异步备库 | 跨城延迟高,实时同步会拖慢主库 |
| 金融核心系统 | 实时主备 + 确认监视器 | 业务要求 |
| 报表/分析只读场景 | 异步备库开放只读 | 可接受数据轻微滞后,保护主库 |
| 升级演练/变更测试 | 在异步备库上折腾 | 不影响生产 |
文章
阅读量
获赞
