diff --git a/ansible/files/mysql-delivery@.service b/ansible/files/mysql-delivery@.service index 431cb4a..2229074 100644 --- a/ansible/files/mysql-delivery@.service +++ b/ansible/files/mysql-delivery@.service @@ -1,20 +1,53 @@ +# ============================================================================= +# XINFRA MySQL 交付实例 Systemd 模板单元 +# ============================================================================= +# 这是 systemd 的模板单元(template unit),文件名中的 @ 是模板标识 +# 使用方式:systemctl start mysql-delivery@{实例名}.service +# 例如:mysql-delivery@db01.service、mysql-delivery@db02.service +# +# %i 参数会被 systemd 自动替换为 @ 后面的实例名 +# 这样同一台主机可以运行多个 MySQL 实例,彼此隔离 +# ============================================================================= + [Unit] +# 服务描述,%i 会被替换为实际实例名 Description=XINFRA MySQL delivery instance %i +# 依赖:等待网络在线后再启动(MySQL 需要绑定网络端口) After=network-online.target Wants=network-online.target [Service] +# Type=notify:mysqld 启动完成后会通过 sd_notify 通知 systemd +# 比 Type=simple 更可靠,systemd 能准确知道服务何时真正就绪 Type=notify + +# 以 mysql 用户/组身份运行(最小权限原则) User=mysql Group=mysql + +# 创建运行时目录 /run/mysql-delivery-{实例名}/ +# 用于存放 socket 和 pid 文件,放在 tmpfs(内存文件系统)上,重启自动清理 RuntimeDirectory=mysql-delivery-%i RuntimeDirectoryMode=0755 + +# 启动命令:使用实例专属配置文件 +# %i 被替换为实例名,指向 /etc/mysql/mysql-delivery/{实例名}.cnf ExecStart=/usr/sbin/mysqld --defaults-file=/etc/mysql/mysql-delivery/%i.cnf + +# 故障自动重启:崩溃后 5 秒自动重启 Restart=on-failure RestartSec=5s + +# 启动超时:120 秒内未完成启动则判定为失败 TimeoutStartSec=120s + +# 最大打开文件数:MySQL 需要大量文件描述符(连接、临时表等) LimitNOFILE=65535 + +# OOM 评分调整:-200 表示被 OOM Killer 杀掉的优先级较低 +# MySQL 是数据库服务,被误杀影响较大,尽量保护 OOMScoreAdjust=-200 [Install] +# 开机自启:加入 multi-user.target(多用户模式,即正常启动完成后) WantedBy=multi-user.target diff --git a/ansible/inventory.example.yml b/ansible/inventory.example.yml index c7f5795..441cc86 100644 --- a/ansible/inventory.example.yml +++ b/ansible/inventory.example.yml @@ -1,11 +1,35 @@ +# ============================================================================= +# Ansible Inventory 示例文件 +# ============================================================================= +# Inventory 定义了要管理的主机列表和分组 +# 实际使用时,平台会动态生成 inventory 并传入变量 +# +# 使用方式: +# ansible-playbook -i inventory.example.yml mysql-deploy.yml \ +# -e "target_hosts=mysql_hosts" \ +# -e "instance_name=db01" \ +# -e "topology=mgr_3" \ +# -e "mysql_version=8.0" \ +# -e "memory_mb=8192" \ +# -e "storage_gb=100" +# ============================================================================= + all: children: + # --- MySQL 主机组 --- + # 按拓扑需求选择对应数量的主机: + # standalone = 1 台 + # primary_replica = 2 台(一主一从) + # mgr_3 = 3 台(三节点组复制) mysql_hosts: hosts: + # 格式:主机名: { ansible_host: IP地址 } k8s-server-01: { ansible_host: 192.168.1.4 } k8s-server-02: { ansible_host: 192.168.1.5 } k8s-server-03: { ansible_host: 192.168.1.2 } k8s-worker-01: { ansible_host: 192.168.1.3 } + + # --- 全局变量 --- vars: - ansible_user: root - ansible_python_interpreter: /usr/bin/python3 + ansible_user: root # SSH 登录用户(需要 root 权限执行系统级操作) + ansible_python_interpreter: /usr/bin/python3 # Python 解释器路径(Ubuntu 默认位置) diff --git a/ansible/mysql-rollback.yml b/ansible/mysql-rollback.yml new file mode 100644 index 0000000..c302b1d --- /dev/null +++ b/ansible/mysql-rollback.yml @@ -0,0 +1,79 @@ +--- +- name: Roll back native MySQL delivery instance + hosts: "{{ target_hosts }}" + become: true + gather_facts: false + any_errors_fatal: true + vars: + mysql_instance: "{{ instance_name }}" + mysql_data_disk: "{{ data_disk }}" + mysql_base_dir: "{{ mysql_data_disk }}/mysql-delivery/{{ mysql_instance }}" + mysql_install_dir: "/opt/mysql-delivery/{{ mysql_instance }}" + mysql_config_file: "/etc/mysql/mysql-delivery/{{ mysql_instance }}.cnf" + mysql_run_dir: "/run/mysql-delivery-{{ mysql_instance }}" + + pre_tasks: + - name: Validate rollback target + ansible.builtin.assert: + that: + - mysql_instance is match('^[a-z0-9][a-z0-9-]{0,62}$') + - mysql_data_disk is match('^/') + - mysql_data_disk != '/' + - "'..' not in mysql_data_disk" + fail_msg: "Rollback target is outside the native MySQL delivery layout" + quiet: true + + tasks: + - name: Stop the delivery instance when present + ansible.builtin.systemd_service: + name: "mysql-delivery@{{ mysql_instance }}.service" + state: stopped + enabled: false + register: mysql_stop_result + failed_when: false + + - name: Remove the instance systemd failure state + ansible.builtin.command: + argv: + - systemctl + - reset-failed + - "mysql-delivery@{{ mysql_instance }}.service" + changed_when: false + failed_when: false + + - name: Remove the instance configuration + ansible.builtin.file: + path: "{{ mysql_config_file }}" + state: absent + + - name: Remove instance-local binary links + ansible.builtin.file: + path: "{{ mysql_install_dir }}" + state: absent + + - name: Remove the instance data and log tree + ansible.builtin.file: + path: "{{ mysql_base_dir }}" + state: absent + + - name: Remove the instance runtime directory + ansible.builtin.file: + path: "{{ mysql_run_dir }}" + state: absent + + - name: Confirm the instance artifacts are gone + ansible.builtin.stat: + path: "{{ item }}" + loop: + - "{{ mysql_config_file }}" + - "{{ mysql_install_dir }}" + - "{{ mysql_base_dir }}" + - "{{ mysql_run_dir }}" + register: mysql_rollback_artifacts + + - name: Assert that all instance artifacts are gone + ansible.builtin.assert: + that: + - mysql_rollback_artifacts.results | selectattr('stat.exists') | list | length == 0 + fail_msg: "One or more MySQL delivery artifacts remain after rollback" + quiet: true diff --git a/ansible/templates/mysql-instance.cnf.j2 b/ansible/templates/mysql-instance.cnf.j2 index da6e3cf..fc3918c 100644 --- a/ansible/templates/mysql-instance.cnf.j2 +++ b/ansible/templates/mysql-instance.cnf.j2 @@ -1,82 +1,134 @@ -# Managed by XINFRA MySQL delivery - generated, do not edit by hand -# instance={{ mysql_instance }} version={{ mysql_version_value }} topology={{ topology }} +# ============================================================================= +# XINFRA MySQL 实例配置文件(Jinja2 模板) +# ============================================================================= +# 此文件由 Playbook 自动生成,禁止手动编辑 +# 修改配置请通过平台参数传递,重新部署即可 +# ============================================================================= + +# 客户端连接配置(mysql 命令行工具使用) [client] -socket={{ mysql_run_dir }}/mysql.sock -port={{ mysql_port_value }} +socket={{ mysql_run_dir }}/mysql.sock # Unix socket 连接路径(本地连接更快) +port={{ mysql_port_value }} # TCP 端口(远程连接使用) +# ============================================================================= +# MySQL 服务端核心配置 +# ============================================================================= [mysqld] -user=mysql -basedir=/usr -datadir={{ mysql_data_dir }} -tmpdir={{ mysql_tmp_dir }} -socket={{ mysql_run_dir }}/mysql.sock -pid-file={{ mysql_run_dir }}/mysql.pid -port={{ mysql_port_value }} -bind-address=0.0.0.0 -skip-name-resolve=ON -server-id={{ mysql_server_id_value }} -local-infile=OFF +# --- 基础运行参数 --- +user=mysql # 运行用户(与 systemd 单元一致) +basedir=/usr # MySQL 安装根目录(Ubuntu apt 安装位置) +datadir={{ mysql_data_dir }} # 数据目录(所有表、索引、数据文件) +tmpdir={{ mysql_tmp_dir }} # 临时文件目录(排序、临时表等) +socket={{ mysql_run_dir }}/mysql.sock # Unix socket 路径(本地客户端连接) +pid-file={{ mysql_run_dir }}/mysql.pid # 进程 PID 文件路径 +port={{ mysql_port_value }} # 监听端口 +bind-address=0.0.0.0 # 监听所有网络接口(允许远程连接) +skip-name-resolve=ON # 跳过 DNS 解析(提升连接速度,授权时需用 IP) +server-id={{ mysql_server_id_value }} # 服务器唯一 ID(复制必须,全机唯一) +local-infile=OFF # 禁用 LOAD DATA LOCAL(安全考虑,防止读取客户端文件) -# --- charset / collation / timezone / identifier case --- -character-set-server={{ mysql_character_set }} -collation-server={{ mysql_collation }} -default-time-zone={{ mysql_timezone }} -lower-case-table-names={{ mysql_lower_case_table_names }} +# --- 字符集 / 排序规则 / 时区 / 标识符大小写 --- +character-set-server={{ mysql_character_set }} # 服务端字符集,默认 utf8mb4 +collation-server={{ mysql_collation }} # 排序规则,默认 utf8mb4_general_ci +default-time-zone={{ mysql_timezone }} # 默认时区,东八区 +lower-case-table-names={{ mysql_lower_case_table_names }} # 表名大小写敏感:1=不敏感(Linux 推荐) -# --- error / slow log --- -log-error={{ mysql_log_dir }}/error.log -slow-query-log=ON -slow-query-log-file={{ mysql_log_dir }}/slow.log -long-query-time={{ mysql_long_query_time }} +# --- 错误日志 / 慢查询日志 --- +log-error={{ mysql_log_dir }}/error.log # 错误日志路径 +slow-query-log=ON # 启用慢查询日志 +slow-query-log-file={{ mysql_log_dir }}/slow.log # 慢查询日志路径 +long-query-time={{ mysql_long_query_time }} # 慢查询阈值(秒),超过此时间记录 -# --- binlog (PITR + replication base) --- -log-bin={{ mysql_binlog_dir }}/binlog -binlog-format=ROW -sync-binlog={{ mysql_sync_binlog }} -max-binlog-size={{ mysql_max_binlog_size }} -binlog-expire-logs-seconds={{ mysql_binlog_expire_logs_seconds }} +# ============================================================================= +# Binlog 配置(用于数据恢复 PITR + 复制基础) +# ============================================================================= +log-bin={{ mysql_binlog_dir }}/binlog # binlog 文件前缀(二进制日志) +binlog-format=ROW # 格式:ROW(行变更),复制最安全 +sync-binlog={{ mysql_sync_binlog }} # 每次提交同步 binlog 到磁盘,1=最安全 +max-binlog-size={{ mysql_max_binlog_size }} # 单个 binlog 文件最大大小 +binlog-expire-logs-seconds={{ mysql_binlog_expire_logs_seconds }} # binlog 过期时间(秒),默认 7 天 + +# --- 非 standalone 拓扑才启用的复制参数 --- {% if topology != 'standalone' %} -gtid-mode=ON -enforce-gtid-consistency=ON -relay-log={{ mysql_binlog_dir }}/relay-bin +gtid-mode=ON # 启用 GTID(全局事务标识符) +enforce-gtid-consistency=ON # 强制 GTID 一致性(确保复制安全) +relay-log={{ mysql_binlog_dir }}/relay-bin # 中继日志路径(从库重放 binlog 使用) {% endif %} -# --- InnoDB core --- +# ============================================================================= +# InnoDB 存储引擎核心配置 +# ============================================================================= +# 缓冲池大小:分配内存的 55%(MySQL 官方推荐 50%-75%) innodb-buffer-pool-size={{ ((mysql_memory_mb_value | int) * 55 / 100) | int }}M + +# 刷盘方式:O_DIRECT 绕过操作系统缓存,直接写磁盘 +# 避免双重缓存,适合有电池保护写缓存的 RAID 控制器 innodb-flush-method=O_DIRECT + +# 事务提交时刷日志到磁盘:1=每次提交都刷(最安全,性能最低) innodb-flush-log-at-trx-commit={{ mysql_flush_log_at_trx_commit }} + +# InnoDB I/O 能力:告诉 InnoDB 磁盘的 IOPS 能力 +# 2000 适合 SSD,HDD 建议 200-400 innodb-io-capacity={{ mysql_io_capacity }} + +# 最大连接数(按内存梯度计算,默认值见 Playbook) max-connections={{ mysql_max_connections }} -# --- redo log (version-sensitive) --- -innodb-log-group-home-dir={{ mysql_redo_dir }} +# ============================================================================= +# Redo Log 配置(InnoDB 崩溃恢复必需) +# ============================================================================= +innodb-log-group-home-dir={{ mysql_redo_dir }} # redo log 目录 + +# MySQL 8.0.30+ 使用 innodb-redo-log-capacity 控制 redo log 总大小 +# 旧版本使用 innodb-log-file-size 控制单个文件大小 {% if mysql_version_value in ['8.0', '8.4'] %} innodb-redo-log-capacity={{ mysql_redo_capacity }} {% else %} innodb-log-file-size={{ mysql_redo_capacity }} {% endif %} -# --- X Protocol disabled (version-sensitive) --- +# ============================================================================= +# X Protocol 禁用(版本相关) +# ============================================================================= +# X Protocol 是 MySQL 的 NoSQL 部分,我们不需要,禁用节省资源 {% if mysql_version_value in ['8.0', '8.4'] %} -mysqlx=0 +mysqlx=0 # 8.0/8.4 直接禁用 {% elif mysql_version_value == '5.7' %} -loose-mysqlx=0 +loose-mysqlx=0 # 5.7 使用 loose 前缀(参数可能不存在) {% endif %} +# ============================================================================= +# Group Replication 配置(仅 mgr_3 拓扑) +# ============================================================================= +# 注意:这里只做配置就绪,实际启动组复制需要平台侧执行 START GROUP_REPLICATION {% if topology == 'mgr_3' %} -# --- Group Replication (config-ready; runtime bootstrap handled out-of-band) --- +# 加载 Group Replication 插件 plugin-load-add=group_replication.so + +# MySQL 8.0.26 之前需要设置事务写集提取(8.4 已移除此参数) {% if mysql_version_value != '8.4' %} -{# deprecated since 8.0.26 and removed in 8.3+; only inject for older series #} -transaction-write-set-extraction=XXHASH64 +transaction-write-set-extraction=XXHASH64 # 使用 XXHASH64 算法提取写集 {% endif %} + +# GR 组 UUID(所有节点必须相同) loose-group-replication-group-name={{ mgr_group_name }} + +# 不在启动时自动加入组(由平台侧手动控制加入时机) loose-group-replication-start-on-boot=OFF + +# 本节点用于 GR 通信的地址和端口 loose-group-replication-local-address={{ ansible_host | default(inventory_hostname) }}:{{ mysql_gr_port_value }} + +# 组内所有节点的种子列表(用于新节点发现和连接) loose-group-replication-group-seeds={% for host in ansible_play_hosts_all %}{{ hostvars[host].ansible_host | default(host) }}:{{ mysql_gr_port_value }}{% if not loop.last %},{% endif %}{% endfor %} +# IP 白名单:只允许组内节点 IP 加入(安全限制) loose-group-replication-ip-allowlist={% for host in ansible_play_hosts_all %}{{ hostvars[host].ansible_host | default(host) }}{% if not loop.last %},{% endif %}{% endfor %} +# 单主模式:只有 primary 节点可写,replica 只读(推荐生产使用) loose-group-replication-single-primary-mode=ON + +# 关闭强制更新检查:单主模式下不需要(多主模式才需要) loose-group-replication-enforce-update-everywhere-checks=OFF {% endif %} diff --git a/frontend/src/api/delivery.ts b/frontend/src/api/delivery.ts index 4593cb5..21aca13 100644 --- a/frontend/src/api/delivery.ts +++ b/frontend/src/api/delivery.ts @@ -136,6 +136,24 @@ export const deliveryApi = { const query = search.toString() return new EventSource(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/stream${query ? `?${query}` : ''}`) }, + + async retryRollback(taskId: string): Promise { + await authRequest(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/rollback/retry`, { + method: 'POST', + }) + }, + + async releaseRollback(taskId: string): Promise { + await authRequest(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/rollback/release`, { + method: 'POST', + }) + }, + + async retryCloudDMRegistration(taskId: string): Promise { + await authRequest(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/clouddm/retry`, { + method: 'POST', + }) + }, } function createIdempotencyKey(payload: CreateMySQLDeliveryPayload) { diff --git a/frontend/src/views/service/Catalog.vue b/frontend/src/views/service/Catalog.vue index f9d5c2b..2232db2 100644 --- a/frontend/src/views/service/Catalog.vue +++ b/frontend/src/views/service/Catalog.vue @@ -279,7 +279,7 @@
-
+
{{ deliveryFailed ? '!' : '✓' }}

{{ resultTitle }}

@@ -287,6 +287,27 @@
+
+
+ 目标机清理未确认 +

资源目前仍被保护性占用。可重试自动回退,或确认已完成目标机清理后释放。

+
+
+ 重试回退 + 确认清理并释放资源 +
+
+ +
+
+ 实例已交付,CloudDM 注册未完成 +

数据库健康实例和资源账本已保留,仅需重试 CloudDM 注册。

+
+
+ 重试 CloudDM 注册 +
+
+
访问地址 @@ -326,7 +347,7 @@ {{ deliveryForm.version }} {{ currentModeLabel }} {{ resultAddress }} - {{ deliveryFailed ? '已回退' : '正常' }} + {{ deliveryRolledBack ? '已回退' : deliveryAcknowledged ? '已确认释放' : deliveryRegisterFailed ? '实例保留' : deliveryFailed ? '需处理' : '正常' }} {{ activeService.registerTo }} @@ -348,10 +369,11 @@