Elasticsearch CCR(Cross Cluster Replication)是在 Elasticsearch 6.7 之后引入的跨集群索引复制能力,它允许一个集群中的索引以近乎实时的方式复制到另一个集群。对于 Node.js 应用来说,CCR 的价值集中在容灾、就近读取和数据分析隔离,但客户端需要自己管理连接路由,因为 CCR 不提供跨集群自动切换。下面先看 CCR 的复制模型,再逐步完成 Windows 双集群配置和 Node.js 代码接入。

CCR 的复制模型与限制
CCR 采用 leader/follower 模型。主集群上的索引称为 leader 索引,从集群上的同名索引称为 follower 索引。复制单元是分片,leader 分片会把操作历史同步给 follower 分片,follower 分片在本地重放这些操作来保持数据一致。这个机制依赖 Elasticsearch 的软删除特性,需要在主集群和从集群都设置 index.soft_deletes.retention_leases.period 或直接开启软删除。默认情况下较新版本已经自动开启软删除,但在较老版本或自定义模板中可能被关闭。
复制不是双向的。leader 索引可以正常读写,follower 索引只能执行读取和部分只读操作,写入请求会直接报错。Node.js 应用如果误把写入请求发到从集群,就会遇到 cluster_block_exception 或 index_not_found_exception。因此读写分离的关键不是让 CCR 自动帮你分流,而是在应用侧明确知道当前应该连哪个集群。
另一个容易忽略的点是 CCR 与索引模板的关系。可以使用自动跟随规则,让从集群自动复制符合条件的新索引。例如主集群每天生成 logs-2025.01.01 这类索引时,从集群可以根据 logs-* 模式自动创建 follower 索引。但自动跟随只对新索引生效,已经存在的历史索引需要手工创建 follower 索引。
Windows 环境配置双集群和远程连接
为了演示 CCR,需要在同一台 Windows 机器或两台机器上运行两个 Elasticsearch 实例。假设主集群端口为 9200,从集群端口为 9201。解压两个 Elasticsearch 压缩包后,分别修改 C:\Elasticsearch\leader\config\elasticsearch.yml 和 C:\Elasticsearch\follower\config\elasticsearch.yml。注意路径中的反斜杠必须保留,Windows 注册表或服务配置中同样使用反斜杠。
主集群的关键配置如下。
cluster.name: leader-cluster node.name: leader-node-1 network.host: 127.0.0.1 http.port: 9200 path.data: C:\Elasticsearch\leader\data path.logs: C:\Elasticsearch\leader\logs
从集群配置把端口改为 9201,路径改为 C:\Elasticsearch\follower\data 和 C:\Elasticsearch\follower\logs。确认两个集群能通过 HTTP 互相访问后,在从集群上建立到主集群的远程连接。可以在 Kibana Dev Tools 或通过 curl 执行:
PUT _cluster/settings
{
"persistent": {
"cluster": {
"remote": {
"leader_remote": {
"seeds": ["127.0.0.1:9300"]
}
}
}
}
}
这里的 leader_remote 是远程集群别名,后面创建 follower 索引时会用到。旧版本使用 9300 传输端口,较新版本如果启用了 security,需要配置证书和用户权限。对于本地测试可以直接关闭安全认证,但生产环境务必开启 TLS 和认证。
接下来创建一个 leader 索引并写入几条测试文档,然后创建 follower 索引。主集群写入命令如下。
PUT logs-2025.01.01/_doc/1
{
"level": "info",
"message": "first log"
}
在从集群执行 follower 索引创建,指定远程集群和 leader 索引。
PUT logs-2025.01.01/_ccr/follow
{
"remote_cluster": "leader_remote",
"leader_index": "logs-2025.01.01"
}
创建成功后,从集群上的 logs-2025.01.01 就是只读的 follower 索引。CCR 会从主集群拉取历史数据并持续同步新写入内容。
Node.js 客户端实现读写分离
官方 Node.js 客户端不会因为 CCR 而自动感知两个集群。需要在代码中创建两个 Client 实例,一个连接主集群负责写入,一个连接从集群负责查询。这样能避免把写入请求误发到 follower 索引。
const { Client } = require('@elastic/elasticsearch');
// 主集群客户端,用于写入
const leaderClient = new Client({
node: 'http://127.0.0.1:9200'
});
// 从集群客户端,用于读取
const followerClient = new Client({
node: 'http://127.0.0.1:9201'
});
async function writeLog(index, doc) {
const response = await leaderClient.index({
index,
body: doc
});
return response.body;
}
async function searchLogs(index, query) {
const response = await followerClient.search({
index,
body: {
query: {
match: { message: query }
}
}
});
return response.body.hits;
}
这段代码简单清晰,但要注意两个客户端各自维护连接池,不要试图把两个地址放进同一个 node 数组。Elasticsearch 客户端使用数组时是作为同一集群的多个节点做负载均衡,不是跨集群路由。把 9200 和 9201 放进一个客户端,请求会随机发到两个集群,写入流量一旦落到从集群就会失败,读取也可能拿到不完整数据。
如果业务层不想维护两个客户端,可以封装一个仓储层,根据操作类型选择连接。例如 save() 方法始终使用 leaderClient,find() 方法始终使用 followerClient。对于实时性要求高的查询,也可以先查主集群,只有报表分析、历史数据扫描等允许延迟的场景才读从集群。
对于查询延迟,可以从 follower 集群查看同步状态,确认 follower 索引已经追上 leader 后再返回结果。下面例子在执行查询前先检查 _ccr/stats 中的 global_checkpoint 与 max_seq_no 的差距。
async function waitForSync(index, timeoutMs = 10000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const stats = await followerClient.ccr.stats();
const item = stats.body.follow_stats.indices.find(i => i.index === index);
if (!item) throw new Error('follower index not found');
const shard = item.shards[0];
const lag = shard.leader_max_seq_no - shard.follower_global_checkpoint;
if (lag <= 10) return;
await new Promise(resolve => setTimeout(resolve, 500));
}
throw new Error('sync timeout');
}
这里用 leader_max_seq_no 与 follower_global_checkpoint 的差值表示尚未复制的操作数量。小于等于 10 代表基本同步完成,实际阈值需要根据业务可接受的延迟调整。生产环境建议结合异步队列或缓存,避免每次查询都轮询 CCR 状态。
同步延迟与常见问题
CCR 是异步复制机制,它的延迟主要受网络带宽、主集群写入速率、从集群合并策略和批处理大小影响。通过 _ccr/stats 可以看到每个 follower 分片的 operations_behind、last_requested_seq_no 等指标。如果延迟持续增加,可以先检查主集群的分片是否有大量写入,以及从集群的 refresh interval 是否设置过大。
在 Windows 服务器上还容易出现文件句柄或磁盘 IO 瓶颈。Elasticsearch 数据目录如果放在机械硬盘,复制速度会明显下降。建议把 path.data 指向 SSD,并且使用 index.store.throttle.type 控制合并限速。另外要注意 Windows 文件路径长度限制,解压 Elasticsearch 时目录层级不要太深,否则可能出现 java.nio.file.PathTooLongException。
写入 follower 索引时报 cluster_block_exception 属于正常现象,说明请求发到了从集群。解决方法是把写操作切回主集群客户端。如果 follower 索引需要回退为普通索引以便写入,可以执行 _ccr/unfollow 停止复制,然后关闭索引并打开为普通索引。不过解除跟随后再恢复 CCR 需要新建 follower 索引,因此不建议频繁执行。
从集群故障恢复后,CCR 会从上次检查点继续同步,不需要重新初始化。如果主集群不可用,从集群仍然可以读取已经复制到的数据,这是 CCR 作为容灾方案的核心价值。但要注意,CCR 不提供自动故障转移,主集群恢复前,从集群只能读不能写。真正的高可用写入需要结合跨集群搜索或双活方案。

