背景
在日常项目中,我们经常需要将 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 获取到之后,需要将获取到的数据发送到消息队列,这里就有一个问题,为什么还需要引入消息队列:
- 解耦架构:消息队列将 Maxwell 和 Logstash 解耦,让它们独立运行。Maxwell 专注于监听 MySQL 的 binlog,Logstash 按需消费,互不影响,系统更稳定。
- 削峰填谷:RabbitMQ 充当缓冲器,在数据库更新突发高峰时可以积压消息,防止瞬时写入压垮 Elasticsearch,提高整体吞吐能力。
- 增加容错能力:当 Logstash 或 Elasticsearch 出现故障时,消息依然保存在队列中,待服务恢复后自动继续消费,不易丢数据。
- 支持多消费者: RabbitMQ 支持发布/订阅模式,一份数据可以同时被多个系统消费,如:一个服务写入 Elasticsearch,一个服务更新缓存,一个服务触发通知
- 可观测、可控制: RabbitMQ 提供可视化界面,支持查看队列积压、消费速率,有利于系统监控与调优。
数据代理转换器(agent)
- Logstash
- Filebeat
- Fluentd
- Vector(只能增加,不能修改)
实现
准备 mysql
Maxwell 的原理是模拟一个 MySQL 的从库,所以必须得让主库开启 binlog
。
开启 Binlog
打开 MySQL 配置文件(通常是 my.cnf
或 my.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
, 可以更直观的看到数据
