异构数据库数据同步最佳实践

背景

在日常项目中,我们经常需要将 MySQL 中的数据同步到 Elasticsearch,以支持复杂的搜索或数据分析需求。传统的方式往往是定时批量同步,但面对实时性要求较高的场景,这种方式显得不够灵活。

本文将介绍如何通过 Maxwell 监听 MySQL 的 binlog,并借助 RabbitMQ 与 Logstash 实现实时数据同步到 Elasticsearch 的完整流程。除了 MySQL,我们将使用 Docker Compose 管理所有服务,力求部署简单、逻辑清晰、便于扩展。架构如下。

lua 复制代码
+-------+      +---------+      +-----------+      +----------+      +---------------+
| MySQL |----->| Maxwell |----->| RabbitMQ  |----->| Logstash |----->| Elasticsearch |
+-------+      +---------+      +-----------+      +----------+      +---------------+
   ^                                                                       |
   |                                                                       |
 (Binlog)                                                                  |
                                                                           |
                                                                     +---------+
                                                                     | Kibana  |
                                                                     +---------+

binlog 监听

目前,监听 binlog 的主流方案有:

  • Maxwell (可以独立运行,将 MySQL 的 binlog 解析成易于理解、易于使用的JSON 格式,并将其发送到 Kafka 或其他消息队列,方便消费者进行数据处理和分析。)
  • Debezium
  • Cannel (阿里开源项目,夹带私货多,不推荐)
  • FlinkCDC

消息队列

binlog 获取到之后,需要将获取到的数据发送到消息队列,这里就有一个问题,为什么还需要引入消息队列:

  1. 解耦架构:消息队列将 Maxwell 和 Logstash 解耦,让它们独立运行。Maxwell 专注于监听 MySQL 的 binlog,Logstash 按需消费,互不影响,系统更稳定。
  2. 削峰填谷:RabbitMQ 充当缓冲器,在数据库更新突发高峰时可以积压消息,防止瞬时写入压垮 Elasticsearch,提高整体吞吐能力。
  3. 增加容错能力:当 Logstash 或 Elasticsearch 出现故障时,消息依然保存在队列中,待服务恢复后自动继续消费,不易丢数据。
  4. 支持多消费者: RabbitMQ 支持发布/订阅模式,一份数据可以同时被多个系统消费,如:一个服务写入 Elasticsearch,一个服务更新缓存,一个服务触发通知
  5. 可观测、可控制: RabbitMQ 提供可视化界面,支持查看队列积压、消费速率,有利于系统监控与调优。

数据代理转换器(agent)

  • Logstash
  • Filebeat
  • Fluentd
  • Vector(只能增加,不能修改)

实现

准备 mysql

Maxwell 的原理是模拟一个 MySQL 的从库,所以必须得让主库开启 binlog

开启 Binlog

打开 MySQL 配置文件(通常是 my.cnfmy.ini),在 [mysqld] 部分下添加或修改以下配置:

ini 复制代码
# my.cnf

[mysqld]
# 开启 binlog
log-bin=mysql-bin
# 设置 server_id,必须是唯一的整数
server_id=1
# 设置 binlog 格式为 ROW
binlog_format=ROW
# (可选,推荐) 包含完整的行镜像,便于获取变更前后的数据
binlog_row_image=FULL

重启mysql查看是否生效 sudo systemctl restart mysqld

sql 复制代码
select @@log_bin
-- 1

select @@binlog_format
--ROW

如果要监听的数据库开启了主从同步,并且不是主数据库,需要再从数据开启binlog联级同步

ini 复制代码
#/etc/mysql/my.cnf

log_slave_updates = 1

创建 Maxwell 专用账户

为了安全起见,我们不应该使用 root 用户。应该为 Maxwell 创建一个专用用户,并授予它所需的权限。

sql 复制代码
-- 创建一个名为 'maxwell' 的数据库,Maxwell 将用它来存储自己的状态信息
CREATE DATABASE maxwell;

-- 创建用户 'maxwell' 并设置密码
CREATE USER 'maxwell'@'%' IDENTIFIED BY 'your_strong_password';

-- 授予 Maxwell 管理其自身数据库的权限
GRANT ALL ON maxwell.* TO 'maxwell'@'%';

-- 授予 Maxwell 读取 binlog 和复制数据的核心权限
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'maxwell'@'%';

-- 刷新权限使其立即生效
FLUSH PRIVILEGES;

服务编排 (Docker Compose)

现在,有趣的部分来了。我们将使用一个 docker-compose.yml 文件来定义和启动 Elasticsearch、Kibana、RabbitMQ 和 Logstash。

目录结构如下:

arduino 复制代码
.
├── docker-compose.yml
├── .env  # 配置文件
├── elasticsearch
│   ├── config
│   │   └── elasticsearch.yml
│   ├── Dockerfile
│   └── init-users.sh  # 如果需要kibana,则需要执行该脚本,为kibana设置登录密码
├── kibana
│   ├── config
│   │   └── kibana.yml
│   └── Dockerfile
├── logstash
│   ├── config
│   │   └── logstash.yml
│   ├── Dockerfile
│   └── pipeline
│       └── logstash.conf
├── maxwell
│   ├── config
│   │   └── config.properties
│   └── Dockerfile
├── rabbitmq
│   ├── config
│   │   └── rabbitmq.conf
│   └── Dockerfile
└── README.md

.env

yml 复制代码
# 使用的es版本
ELASTIC_VERSION=8.18.4
# 使用的rabbitmq版本
RABBITMQ_VERSION=4-management
# 使用的maxwell版本
MAXWELL_VERSION=latest

## Passwords for stack users
#

# User 'elastic' (built-in)
#
# Superuser role, full access to cluster management and data indices.
# https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html
ELASTIC_PASSWORD='changeme'

# User 'logstash_internal' (custom)
#
# The user Logstash uses to connect and send data to Elasticsearch.
# https://www.elastic.co/guide/en/logstash/current/ls-security.html
LOGSTASH_INTERNAL_PASSWORD='changeme'

# User 'kibana_system' (built-in)
#
# The user Kibana uses to connect and communicate with Elasticsearch.
# https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html
KIBANA_SYSTEM_PASSWORD='changeme'

docker-compose.yml

yaml 复制代码
services:
  elasticsearch:
    build:
      context: elasticsearch/
      args:
        ELASTIC_VERSION: ${ELASTIC_VERSION}
    container_name: elasticsearch
    volumes:
      - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro,Z
      - ./elasticsearch/data:/usr/share/elasticsearch/data:Z
      - ./elasticsearch/init-users.sh:/usr/local/bin/init-users.sh:ro,Z
    ports:
      - 9200:9200
      - 9300:9300
    environment:
      node.name: elasticsearch
      ES_JAVA_OPTS: -Xms512m -Xmx512m
      discovery.type: single-node
      ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-}
      KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-}
    networks:
      - My2ES
    restart: unless-stopped
    command: >
      bash -c "/usr/local/bin/docker-entrypoint.sh &
             sleep 10 &&
             /usr/local/bin/init-users.sh &&
             wait"

  kibana:
    build:
      context: kibana/
      args:
        ELASTIC_VERSION: ${ELASTIC_VERSION}
    container_name: kibana
    volumes:
      - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml:ro,Z
    ports:
      - 5601:5601
    environment:
      KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-}
    networks:
      - My2ES
    depends_on:
      - elasticsearch
    restart: unless-stopped

  rabbitmq:
      build:
        context: ./rabbitmq
        args:
          RABBITMQ_VERSION: ${RABBITMQ_VERSION}
      container_name: rabbitmq
      ports:
        - 5672:5672
        - 15672:15672
      volumes:
      - ./rabbitmq/config/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
      networks:
        - My2ES
      restart: unless-stopped

  logstash:
    build:
      context: logstash/
      args:
        ELASTIC_VERSION: ${ELASTIC_VERSION}
    container_name: logstash
    volumes:
      - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro,Z
      - ./logstash/pipeline:/usr/share/logstash/pipeline:ro,Z
    ports:
      - 5044:5044
      - 50000:50000/tcp
      - 50000:50000/udp
      - 9600:9600
    environment:
      LS_JAVA_OPTS: -Xms256m -Xmx256m
      LOGSTASH_INTERNAL_PASSWORD: ${LOGSTASH_INTERNAL_PASSWORD:-}
    networks:
      - My2ES
    depends_on:
      - elasticsearch
      - rabbitmq
    restart: unless-stopped

  maxwell:
    build:
      context: ./maxwell
    container_name: maxwell
    command: bin/maxwell --config /app/config/config.properties
    volumes:
    - ./maxwell/config/config.properties:/app/config/config.properties:ro
    networks:
      - My2ES
    depends_on:
      - rabbitmq
    restart: unless-stopped

networks:
  My2ES:
    driver: bridge

volumes:
  elasticsearch:

maxwell 配置

maxwell/maxwell/config/config.properties

yaml 复制代码
# tl;dr config
log_level=info

# 指定需要将数据发送至哪里
producer=rabbitmq

# mysql配置
host=mysql的主机host
user=<用上面创建的maxwell的用户名>
password= <用上面创建的maxwell的密码>

#            *** rabbit-mq配置 ***
rabbitmq_host= rabbithost地址
rabbitmq_port=5672
rabbitmq_user= <username> # 默认为guest
rabbitmq_pass= <password> # 默认为guest
rabbitmq_virtual_host=/
rabbitmq_handshake_timeout=20000

rabbitmq_exchange=maxwell # 指定exchange,后面配置logstash的时候需要对应
rabbitmq_queue=maxwell # 指定queue,后面配置logstash的时候需要对应

rabbitmq_exchange_type=fanout
rabbitmq_exchange_durable=false
rabbitmq_exchange_autodelete=false
# rabbitmq_routing_key_template=%db%.%table%
rabbitmq_message_persistent=false
rabbitmq_declare_exchange=true
# rabbitmq_use_ssl=false


#          *** filtering ***
# 配置需要监听的数据库,我指定的是 test 下的所有表
#filter= exclude: *.*, include: foo.*, include: bar.baz, include: foo.bar.col_eg = "value_to_match"
filter= exclude: *.*, include: test.*

logstash 配置

按照自己的设置,

  • 替换 input.rabbitmq 的 host,user、password 等字段,
  • exchange,queue 等字段需要和 maxwell 配置的一样
  • 替换 output.elasticsearch.host 字段为自己的服务的 IP 地址

logstash/pipeline/logstash.conf

yml 复制代码
input {
  rabbitmq {
    host => "10.0.1.36"
    port => 5672
    user => "guest"
    password => "guest"
    queue => "maxwell"                 # 声明 queue
    exchange => "maxwell"              # 绑定到 exchange
    exchange_type => "fanout"          # 类型要匹配 maxwell 的配置
    auto_delete => false
    ack => true
    # 关键:直接在 input 中使用 json codec 解析消息体,这比在 filter 中处理更高效
    codec => "json"
  }
}

filter {
  # 在这里添加 grok 或 mutate 来处理 maxwell 的数据结构
  # 例如:添加 @timestamp 或提取字段
  # json {
  #   # source => "message"
  # }

  translate {
    source => "[type]"
    target => "[@metadata][action]"
    dictionary => {
      "insert" => "index"
      "bootstrap-insert" => "index"
      "update" => "update"
      "delete" => "delete"
    }
    fallback => "unknown"
  }

  # 如果事件类型未被成功映射 (例如 DDL 事件),则丢弃该事件
  if [@metadata][action === 'unknown'] {
    drop {}
  }

  mutate {
    add_field => { "[@metadata][es_index]" => "%{[database]}-%{[table]}" }
    add_field => { "[@metadata][es_id]" => "%{[data][id]}" }
  }

  mutate {
    lowercase => [ "[@metadata][es_index]" ]
  }

  # 处理datetime字段
  if [data][createdAt] {
    date {
      match => [ "[data][createdAt]", "yyyy-MM-dd HH:mm:ss" ]
      target => "[data][createdAt]"
    }
  }
  
  if [data][updatedAt] {
    date {
      match => [ "[data][updatedAt]", "yyyy-MM-dd HH:mm:ss" ]
      target => "[data][updatedAt]"
    }
  }

  if [@metadata][action] in ["index", "update"] {
    ruby {
      code => '
        # 获取 data 字段的值
        data_hash = event.get("data")
        # 检查 data_hash 是否是一个有效的哈希表 (Hash)
        if data_hash.is_a?(Hash)
          # 遍历哈希表,将其所有键值对设置到事件的顶层
          data_hash.each do |k, v|
            event.set(k, v)
          end
        end
      '
    }
  }

  mutate {
    # 删除掉不需要的字段 
    remove_field => [
      "message",
      "original",
      "@version",
      "@timestamp",
      "event",
      "database",
      "type",
      "ts",
      "xid",
      "commit",
      "data",
      "old",
      "table"
    ]
  }
}

output {
  elasticsearch {
    hosts => ["http://10.0.1.36:9200"]
    index => "%{[@metadata][es_index]}"
    document_id => "%{[@metadata][es_id]}"
    action => "%{[@metadata][action]}"
    user => "elastic"
    password => "changeme"
    # 开启重试机制,以防 ES 暂时不可用
    retry_on_conflict => 3
  }

  # 将结果打印到标准输出
  stdout {
    codec => rubydebug {
      # 将 metadata 选项设置为 true
      metadata => true
    }
  }
}

kibana 配置

kibana/config/kibana.yml

yml 复制代码
server.name: kibana
server.host: 0.0.0.0
elasticsearch.hosts: [ http://10.0.1.36:9200 ]
i18n.locale: "zh-CN"

monitoring.ui.container.elasticsearch.enabled: true
monitoring.ui.container.logstash.enabled: true

## Security credentials
elasticsearch.username: kibana_system
elasticsearch.password: ${KIBANA_SYSTEM_PASSWORD}

ES 配置

yml 复制代码
cluster.name: docker-cluster
network.host: 0.0.0.0

xpack.license.self_generated.type: trial
xpack.security.enabled: true

服务启动与检查

启动

通过如下命令启动服务

复制代码
docker compose up -d

检查

分别查看各个容器的日志信息,检查服务是否启动成功的

bash 复制代码
docker logs -f 容器id
es 启动成功标志
rabbitmq 启动成功标志
maxwell 启动成功标志
logstash 启动成功标志
kibana 启动成功标志

访问 http://127.0.0.1:5601并出现如下界面

数据同步

当服务启动之后,首先检查 rabbitmq 的 queue 和 exchanges 是否绑定

访问 http://127.0.0.1:15672/

创建数据表

sql 复制代码
CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(36) NOT NULL,
  `age` int DEFAULT NULL,
  `createdAt` datetime DEFAULT CURRENT_TIMESTAMP,
  `updatedAt` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- 插入数据
INSERT INTO test.user VALUES (1, 'zs', 12, '2025-07-30 10:41:02', '2025-07-30 10:41:04');
INSERT INTO test.user VALUES (2, 'ls', 13, '2025-07-30 10:42:49', '2025-07-30 10:42:54');

查看 logstash 日志

此时,可以看到,我们的数据已经被 logstash 进行了解析,登录到 kibana,我们就可以看到 数据库名称-表名称的索引已经被创建

也推荐 chrome 插件 es-client, 可以更直观的看到数据

项目地址

github.com/0x0bit/MySQ...

相关推荐
千册19 分钟前
python+pyside6+sqlite 数据库测试
数据库·python·sqlite
java叶新东老师2 小时前
PowerDesigner 画ER图并生成sql 教程
数据库·sql
Jonariguez2 小时前
Mysql InnoDB存储引擎
数据库·mysql
nbsaas-boot3 小时前
SQL Server 窗口函数全指南(函数用法与场景)
开发语言·数据库·python·sql·sql server
Y.ppm3 小时前
数分思维12:SQL技巧与分析方法
数据库·sql
森叶3 小时前
Claude Code 安装向量数据库MCP服务
数据库
bestsun9993 小时前
Time drifts can result in unexpected behavior such as time-outs.
数据库·oracle
waveee1235 小时前
学习嵌入式的第三十四天-数据结构-(2025.7.29)数据库
数据结构·数据库·学习
何传令5 小时前
SQL优化系统解析
数据库·sql·mysql
找不到、了5 小时前
Redis内存使用耗尽情况分析
数据库·redis·缓存