广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Node.js 如何连接 Elasticsearch CCR 跨集群复制实现读写分离?

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

Node.js 如何连接 Elasticsearch CCR 跨集群复制实现读写分离?

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 不提供自动故障转移,主集群恢复前,从集群只能读不能写。真正的高可用写入需要结合跨集群搜索或双活方案。

广告位 · 文章底部(后台-模块管理替换此区域)