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,这样不影响主节点的写入性能。但要注意,从节点的数据可能有延迟——你刚写入的数据,可能过几秒才能在从节点读到。
故障转移流程
故障转移听起来高大上,其实就三步:
- 主节点挂了,从节点检测到心跳超时
- 从节点发起选举,选出一个新主节点
- 客户端自动重连到新主节点
整个过程大概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语言处理
驱动内部会自动处理故障转移,但你的代码需要做好两件事:
- 检查返回值:每次操作都要检查是否成功
- 实现重试:遇到网络错误,等几秒再试
示例:
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; // 重试耗尽
}
为什么要有重试?因为故障转移期间,主节点不可用,写操作会返回错误。等新主节点选出来,重试就能成功。
副本集架构图
下面这张图展示了副本集的核心流程:
避坑指南
最后分享几个我踩过的坑:
- 不要用太少的节点:最少3个节点(含仲裁),否则选举时可能无法形成多数票
- 仲裁节点不要和主节点放一起:如果机器挂了,仲裁和主一起挂,等于白搭
- 写关注别设太高:w=majority虽然安全,但写入延迟会增加。根据业务需求权衡
- 从节点不是备份:副本集的数据同步是实时的,误删数据会立刻同步到所有节点。备份还是要靠mongodump
我曾经把仲裁节点和主节点放在同一台机器上,结果那台机器宕机,副本集直接无法选举——嗯,这个错误我只犯过一次。
好了,副本集的内容就这些。C语言连接副本集其实不复杂,关键是理解背后的选举和故障转移机制。代码写好了,剩下的交给MongoDB自己去处理。
公众号:蓝海资料掘金营,微信deep3321