61、MongoDB副本集:主从选举、读写分离、故障转移、C语言连接副本集

副本集这东西,说白了就是MongoDB的高可用方案。我最早接触MongoDB副本集是在一个物联网项目里,单节点挂了,整个服务就瘫了,老板急得直跳脚。从那以后,我对副本集的理解就深刻了——它不只是备份,而是一整套自动故障恢复机制。

今天咱们就聊聊副本集的几个核心机制,以及怎么用C语言连上去。嗯,内容不少,但都是实战干货。

副本集的核心角色

一个副本集通常由三部分组成:

  • 主节点(Primary):负责处理所有写操作。客户端写数据,只能找它。
  • 从节点(Secondary):同步主节点的数据。可以处理读请求(如果开启了读写分离)。
  • 仲裁节点(Arbiter):不存数据,只参与选举投票。适合资源紧张的场景。

你想想看,如果主节点挂了,从节点里得选一个出来当老大。这个选举过程,就是副本集的核心价值。

主从选举机制

选举是怎么触发的?主节点心跳超时(默认10秒),从节点就会发起选举。选举算法基于Raft协议,简单说就是「谁的数据新,谁就更可能当选」。

我遇到过一个问题:两个从节点数据一样新,怎么办?这时候就看优先级(priority)了。优先级高的节点更容易当选。如果优先级也相同,那就看谁先发起选举。

选举的关键参数

  • electionTimeoutMillis:心跳超时时间,默认10000ms
  • priority:节点优先级,范围0-100,默认1
  • votes:节点投票权,默认1

我曾经在生产环境里把某个从节点的priority设成了0,结果它永远当不了主节点——嗯,有时候我们需要这样的「纯备胎」。

读写分离策略

默认情况下,客户端只连主节点。但读压力大的时候,可以把读请求分散到从节点上。MongoDB提供了读偏好(read preference)设置:

读偏好模式说明适用场景
primary只读主节点(默认)强一致性要求
primaryPreferred优先读主,主不可用时读从主节点故障时降级
secondary只读从节点报表、分析等非关键查询
secondaryPreferred优先读从,从不可用时读主读写分离典型配置
nearest读网络延迟最低的节点跨地域部署

我个人习惯把报表查询设为secondaryPreferred,这样不影响主节点的写入性能。但要注意,从节点的数据可能有延迟——你刚写入的数据,可能过几秒才能在从节点读到。

故障转移流程

故障转移听起来高大上,其实就三步:

  1. 主节点挂了,从节点检测到心跳超时
  2. 从节点发起选举,选出一个新主节点
  3. 客户端自动重连到新主节点

整个过程大概10-30秒。我见过一个案例,因为网络抖动导致频繁选举,业务中断了好几次。后来把electionTimeoutMillis调大到15000ms,问题就解决了。

注意:故障转移期间,写操作会失败。客户端必须实现重试逻辑。别指望MongoDB帮你搞定一切。

C语言连接副本集

用C语言连MongoDB副本集,官方驱动是libmongoc。我最早用的时候踩了不少坑,这里直接给一个完整的示例。

先安装驱动:

# Ubuntu
sudo apt-get install libmongoc-1.0-0 libbson-1.0-0

# macOS
brew install mongo-c-driver

连接副本集的代码:

#include <mongoc/mongoc.h>
#include <stdio.h>

int main(void) {
    mongoc_client_t *client;
    mongoc_collection_t *collection;
    bson_t *doc;
    bson_error_t error;

    // 初始化驱动
    mongoc_init();

    // 连接副本集,指定所有节点地址
    // 格式:mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplSet
    client = mongoc_client_new(
        "mongodb://192.168.1.10:27017,"
        "192.168.1.11:27017,"
        "192.168.1.12:27017/"
        "?replicaSet=myReplSet"
    );

    if (!client) {
        fprintf(stderr, "连接失败\n");
        return -1;
    }

    // 设置读偏好为secondaryPreferred
    mongoc_client_set_read_prefs(client, 
        mongoc_read_prefs_new(MONGOC_READ_SECONDARY_PREFERRED));

    // 获取集合
    collection = mongoc_client_get_collection(client, "mydb", "mycoll");

    // 插入一条文档
    doc = BCON_NEW("name", BCON_UTF8("Alice"), "age", BCON_INT32(30));
    if (!mongoc_collection_insert_one(collection, doc, NULL, NULL, &error)) {
        fprintf(stderr, "插入失败: %s\n", error.message);
    }
    bson_destroy(doc);

    // 清理
    mongoc_collection_destroy(collection);
    mongoc_client_destroy(client);
    mongoc_cleanup();

    return 0;
}

编译命令

gcc -o replset_demo replset_demo.c $(pkg-config --cflags --libs libmongoc-1.0)

这段代码里,我用了secondaryPreferred读偏好。这样写操作走主节点,读操作走从节点。你想想看,如果从节点挂了,它会自动切回主节点——这就是副本集的好处。

连接字符串的细节

连接副本集时,有几个参数很关键:

  • replicaSet:指定副本集名称,必须和配置一致
  • readPreference:读偏好模式
  • w:写关注,比如w=majority表示写入大多数节点才算成功
  • retryWrites:是否重试写操作,建议设为true

一个更完整的连接字符串:

mongodb://node1:27017,node2:27017,node3:27017/mydb?
replicaSet=myReplSet&
readPreference=secondaryPreferred&
w=majority&
retryWrites=true

我曾经在生产环境里忘了加retryWrites=true,结果主节点切换时写操作全丢了。嗯,这个坑我替你们踩过了。

故障转移的C语言处理

驱动内部会自动处理故障转移,但你的代码需要做好两件事:

  1. 检查返回值:每次操作都要检查是否成功
  2. 实现重试:遇到网络错误,等几秒再试

示例:

int retry_insert(mongoc_collection_t *collection, bson_t *doc) {
    bson_error_t error;
    int retries = 3;

    for (int i = 0; i < retries; i++) {
        if (mongoc_collection_insert_one(collection, doc, NULL, NULL, &error)) {
            return 0; // 成功
        }
        fprintf(stderr, "第%d次插入失败: %s\n", i+1, error.message);
        sleep(2); // 等2秒再试
    }
    return -1; // 重试耗尽
}

为什么要有重试?因为故障转移期间,主节点不可用,写操作会返回错误。等新主节点选出来,重试就能成功。

副本集架构图

下面这张图展示了副本集的核心流程:

MongoDB副本集架构与故障转移流程 主节点 (Primary) 读写操作入口 从节点1 (Secondary) 数据同步 + 读请求 从节点2 (Secondary) 数据同步 + 读请求 仲裁节点 (Arbiter) 仅参与选举投票 oplog同步 oplog同步 心跳检测 故障转移流程 ① 主节点宕机 ② 从节点检测到心跳超时 ③ 发起选举,选出新主节点 ④ 客户端自动重连到新主节点 ⚠ 写操作会短暂失败 ⚠ 客户端需实现重试逻辑 ⚠ 选举时间约10-30秒

避坑指南

最后分享几个我踩过的坑:

  • 不要用太少的节点:最少3个节点(含仲裁),否则选举时可能无法形成多数票
  • 仲裁节点不要和主节点放一起:如果机器挂了,仲裁和主一起挂,等于白搭
  • 写关注别设太高:w=majority虽然安全,但写入延迟会增加。根据业务需求权衡
  • 从节点不是备份:副本集的数据同步是实时的,误删数据会立刻同步到所有节点。备份还是要靠mongodump

我曾经把仲裁节点和主节点放在同一台机器上,结果那台机器宕机,副本集直接无法选举——嗯,这个错误我只犯过一次。

好了,副本集的内容就这些。C语言连接副本集其实不复杂,关键是理解背后的选举和故障转移机制。代码写好了,剩下的交给MongoDB自己去处理。


公众号:蓝海资料掘金营,微信deep3321