ReplicationExecutor的实现原理

本文介绍了MongoDB中副本集的任务调度机制,包括ReplicationExecutor的架构及其内部组件Event、Callback、TaskRunner和OldThreadPool的功能。阐述了不同类型的队列如何协作以确保任务的正确执行。

副本集在维护以及更新结构的过程中, 需要执行大量的任务, 这些任务的执行需要满足一定的条件, 例如, 先触发的时间先执行, 或者等待一段时间以后执行某个任务, 又或者有的任务需要全局锁, 有的只需要集合锁等等。 Mongodb 内部通过ReplicationExecutor来实现这些任务的执行。
从功能上面讲,ReplicationExecutor的作用和golang里面的channel是非常类似的, 都是一端负责添加任务, 另外一端负责处理最先收到的任务。

先来看一下replication executor的整体架构图:
这里写图片描述

ReplicationExecutor 继承自TaskExecutor, 它包含了4个主要的类来完成任务: Event, Callback, TaskRunner, OldThreadPool。 接下来分别介绍他们, 以及他们如何相互协作。

Event

Event 是一个时间生成, 通知等功能的类, 他的作用跟他的名字一样, 通知等待事件发生。在每一个节点中, 都有一个event list, 记录了所有的event。 每当我们要改变接点的状态, 就会发出这样的event, 并且通过event可以通知其他节点进行相应的更改。
每当收到一个event, 就要将所有等待该signal的task放进readyQueue里面。

void ReplicationExecutor::Event::_signal_inlock() {
    invariant(!_isSignaled);
    _isSignaled = true;

    if (!_waiters.empty()) {
        _executor->_readyQueue.splice(_executor->_readyQueue.end(), _waiters);
        _executor->_networkInterface->signalWorkAvailable();
    }

    _isSignaledCondition.notify_all();
}

Callback

每当有一个task被加入到某种类型的队列, 都会返回一个该任务的CallbackHandle, 通过它, 可以得到这个task的执行时的callback 函数, 以及这个task结束的event。

StatusWith<ReplicationExecutor::CallbackHandle> ReplicationExecutor::enqueueWork_inlock(
    WorkQueue* queue, const CallbackFn& callbackFn) {
    invariant(callbackFn);
    StatusWith<EventHandle> event = makeEvent_inlock();
    if (!event.isOK())
        return StatusWith<CallbackHandle>(event.getStatus());

    if (_freeQueue.empty())
        _freeQueue.push_front(WorkItem());
    const WorkQueue::iterator iter = _freeQueue.begin();
    WorkItem& work = *iter;

    invariant(!work.callback.isValid());
    setCallbackForHandle(&work.callback,
                         std::shared_ptr<executor::TaskExecutor::CallbackState>(
                             new Callback(this, callbackFn, iter, event.getValue())));

    work.generation++;
    work.finishedEvent = event.getValue();
    work.readyDate = Date_t();
    queue->splice(queue->end(), _freeQueue, iter);
    return StatusWith<CallbackHandle>(work.callback);
}

TaskRunner

这是一个任务的执行器, 它主要是处理DB相关的task 和exclusive task。它的工作流程如下:
这里写图片描述

ReplicationExecutor

这个类里面包含了多个对列:
_freeQueue:
已经执行的task, 放入该队列;

_readyQueue:
已经ready的任务, 通过ReplicationExecutor::scheduleWork 的任务;

_dbWorkInProgressQueue:
通过ReplicationExecutor::scheduleDBWork加入的任务;

_exclusiveLockInProgressQueue:
通过ReplicationExecutor::scheduleWorkWithGlobalExclusiveLock加入的任务;

_networkInProgressQueue:
通过ReplicationExecutor::scheduleRemoteCommand 加入的任务;

_sleepersQueue:
通过 ReplicationExecutor::scheduleWorkAt 加入的任务, 这个队列里面的任务是按照时间排序的, 新加入的任务也要保证时间的有序性。

每一个节点需要起一个叫做ReplicationExecutor的线程, 不停地通过getWork()得到下一个任务, 执行它, 并且发出该任务执行的信号。 这样的过程重复进行, 一直到所有的任务执行。
整个的逻辑大致如下:

void ReplicationExecutor::run() {
    setThreadName("ReplicationExecutor");
    _networkInterface->startup();
    _dblockWorkers.startThreads();
    std::pair<WorkItem, CallbackHandle> work;
    while ((work = getWork()).first.callback.isValid()) {
        {
            stdx::lock_guard<stdx::mutex> lk(_terribleExLockSyncMutex);
            const Callback* callback = _getCallbackFromHandle(work.first.callback);

            makeNoExcept(
                stdx::bind(callback->_callbackFn, CallbackArgs(this, work.second, inStatus)))();
        }
        signalEvent(work.first.finishedEvent);
    }
    finishShutdown();
    _networkInterface->shutdown();
}

这里, 比较复杂的是执行远端命令的处理, 其流程图如下:
这里写图片描述

这里, ReplicationExecutor 通过NetworkInterface来执行远端命令, 其执行的命令通过RemoteCommandRequest来指定, 执行之后, 通过RemoteCommandCompletionFn 来回调调用方。
NextworkInterface里面主要通过RemoteCommandRunner来处理网络事件, 他通过host:port得到来自ConnectionPool里面的一个ConnectPtr, 然后吧RemoteCommandRequest 变成一个Message的对象。
这里, 该节点需要调用其他的节点, 它自己就相当于一个client 被调用方是一个server, 所以他生成一个DBClientReplicasSet对象, 来调用目的端。当目的端返回, RemoteCommandCompletionFn被调用, 然后逐级返回, 到ReplicationExecutor, 本次remote command执行完成。

你看到的输出: ``` 等待数据库服务就绪... 等待MongoDB服务... 等待MongoDB服务... ... ``` 说明你的 **启动脚本正在轮询等待 MongoDB 完全初始化并开始监听连接**,但似乎已经持续了较长时间,可能意味着: > ⚠️ **MongoDB 虽然启动了进程,但未能成功绑定端口、加载数据或初始化完成** > 或者 > ✅ **只是启动较慢(比如数据量大、磁盘慢),还在正常进行中** --- ### 🔍 问题排查流程 我们需要判断: ✅ 是“暂时等待”?还是 ❌ “卡住无法启动”? --- ## ✅ 第一步:检查 MongoDB 是否真的在运行 ```bash ps aux | grep mongod ``` 查看是否有类似输出: ``` mongodb 27004 1.2 2.5 1234567 89012 ? Sl 06:30 0:10 /usr/bin/mongod --config /etc/mongod.conf ``` - 如果有 → 进程存在,继续下一步。 - 如果没有 → 启动失败,需查日志。 --- ## ✅ 第二步:检查 MongoDB 是否监听了端口 ```bash sudo ss -tulnp | grep :27017 ``` 或: ```bash sudo netstat -tulnp | grep :27017 ``` 预期输出: ``` tcp 0 0 0.0.0.0:27017 0.0.0.0:* LISTEN 27004/mongod ``` - ✅ 有输出 → 正常,可以连接 - ❌ 无输出 → 可能卡在初始化,未完成启动 --- ## ✅ 第三步:查看 MongoDB 日志(关键!) MongoDB 的详细启动过程记录在日志文件中,默认路径是: ```bash /var/log/mongodb/mongod.log ``` 实时查看最后几行: ```bash sudo tail -f /var/log/mongodb/mongod.log ``` 重点关注以下关键字: | 关键字 | 含义 | |-------|------| | `Waiting for connections on port 27017` | 🟢 成功启动,可接受连接 | | `exception in initAndListen` | ❌ 初始化失败 | | `Address already in use` | 端口被占用 | | `Permission denied` | 权限问题 | | `Recovery required` | 需要恢复(上次异常关闭) | | `Attempting to reconnect to ReplicaSet` | 副本配置下等待其他节点 | --- ### 🧩 常见导致“卡住”的原因 #### 1. **需要恢复(Recovery Required)** > 上次未正常关闭,MongoDB 启动时自动做 `WiredTiger` 恢复,数据量大时会很慢。 日志中会出现: ``` [initandlisten] Detected unclean shutdown - set startingUpAsStandalone = true [initandlisten] WiredTiger message [167...] Recovery mode enabled ``` 📌 **解决方案**: - 等待恢复完成(可能几分钟到几小时,取决于数据大小) - 不要中断! #### 2. **副本或分片配置下等待其他成员** 如果你配置了副本(replica set),但只有一个节点在线,它不会进入可读写状态,会一直等待。 日志中可能出现: ``` [ReplicationExecutor] Member is now in state STARTUP2 [ReplicationExecutor] Not becoming primary because a primary is already present ``` 📌 **解决方案**: - 确保所有副本成员可达 - 或临时改为单机模式测试 #### 3. **磁盘 I/O 慢或空间不足** ```bash df -h /var/lib/mongodb ``` 如果磁盘使用率 >90%,MongoDB 可能无法写入日志或 mmap 文件。 #### 4. **权限问题导致无法创建 socket 或写文件** 确保目录权限正确: ```bash sudo chown -R mongodb:mongodb /var/lib/mongodb sudo chown -R mongodb:mongodb /var/log/mongodb ``` --- ## ✅ 第四步:手动测试连接(验证是否就绪) 即使进程在跑,也可能还没 ready。你可以用以下命令测试: ```bash # 尝试连接本地 MongoDB mongo --eval "db.adminCommand('ping')" --quiet ``` - 如果返回 `{"ok":1}` → 已就绪 ✅ - 如果报错:`Failed to connect` 或超时 → 未就绪 ❌ 你也可以写一个简单的等待脚本: ```bash #!/bin/bash echo "等待 MongoDB 就绪..." while ! mongo --eval "db.runCommand({ping:1})" >/dev/null 2>&1; do echo -n "." sleep 2 done echo echo "MongoDB 已就绪!" ``` --- ## ✅ 总结 你现在看到“等待 MongoDB 服务...”是因为脚本在轮询,但你需要判断: | 判断依据 | 结论 | |--------|------| | `ps aux \| grep mongod` 有进程 | 进程已启动 | | `ss -tulnp \| grep 27017` 有监听 | 端口已打开 | | 日志中出现 `Waiting for connections` | 已就绪 ✅ | | 日志中出现 `Recovery` 或卡住 | 正在恢复,需等待 | | 完全无日志更新 | 可能卡死,需重启 | --- ### ✅ 推荐操作 ```bash # 实时查看日志 sudo tail -f /var/log/mongodb/mongod.log # 单独开一个终端,运行测试连接 watch 'mongo --eval "db.runCommand({ping:1})" --quiet' ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值