几乎每一种数据库都会有连接池, 来减少频繁的创建删除连接的开销, 在MongoDB里面是通过信号量线程同步方式来对创建、销毁进行管理。

信号量基础

int sem_init(sem_t *sem, int pshared, unsigned int value)

sem是要初始化的信号量,pshared表示此信号量是在进程间共享(=1)还是线程间共享(=0),value是信号量的初始值。

int sem_destroy(sem_t *sem);

其中sem是要销毁的信号量。只有用sem_init初始化的信号量才能用sem_destroy销毁。

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

sem_wait:等待信号量,如果信号量的值大于0, 将信号量的值减1,立即返回。如果信号量的值为0,则线程阻塞。相当于P操作。成功返回0,失败返回-1。
sem_trywait:与sem_wait类似, 只是如果递减不能立即执行,调用将返回错误(errno 设置为EAGAIN)而不是阻塞。
sem_timewait:sem_wait() 类似,只不过 abs_timeout 指定一个阻塞的时间上限,如果调用因不能立即执行递减而要阻塞。

int sem_post(sem_t *sem);

释放信号量,让信号量的值加1。相当于V操作。

连接池的用法

在大部分的MongoDB driver下, 都有实现了如下的URI 选项, 通过指定相关的连接池控制项, 可以指定连接池的具体行为:

连接项

说明

maxPoolSize

连接池的最大连接数, 默认值是100

minPoolSize

连接池的最小连接数, 默认值是0, 注意不是所有driver支持该选项

maxIdleTimeMS

一个空闲连接在被删除或者关闭之前存在的最大时间, 单位是毫秒,注意,不是所有driver支持该选线

waitQueueMultiple

driver强行限制线程同时等待连接的个数。 这个限制了连接池的倍数

waitQueueTimeoutMS

在超时之前,线程等待连接生效的总时间。如果连接池到达最大并且所有的连接都在使用,这个参数就生效了

URI连接的实例:
如下URI代表write_concern的设定是majority, 写入等待majority的超时时间是2000ms, 连接池的最大连接数是200, 最小的连接数是10.

mongodb://USER:PASSWD@host1,host2,host3/?w=majority&wtimeoutMS=2000&maxPoolSize=200&minPoolSize=10

连接池的代码实现

使用TicketHoder来封装类线程控制信号量函数:

TicketHolder::TicketHolder(int num) : _outof(num) {
    _check(sem_init(&_sem, 0, num));
}

TicketHolder::~TicketHolder() {
    _check(sem_destroy(&_sem));
}

bool TicketHolder::tryAcquire() {
    while (0 != sem_trywait(&_sem)) {
        switch (errno) {
            case EAGAIN:
                return false;
            case EINTR:
                break;
            default:
                _check(-1);
        }
    }
    return true;
}

void TicketHolder::waitForTicket() {
    while (0 != sem_wait(&_sem)) {
        switch (errno) {
            case EINTR:
                break;
            default:
                _check(-1);
        }
    }
}

void TicketHolder::release() {
    _check(sem_post(&_sem));
}

在MongoDB启动的时候, 会先创建一个PortMessageServer,并在里面指定一个有配置参数或者命令行参数指定的最大的连接数, 然后通过setupSockets创建一个socket并绑定, 并将其加入到Listener里面的std::vector _socks;

static void _initAndListen(int listenPort) { 
    ...
    auto handler = std::make_shared<MyMessageHandler>();
    MessageServer* server = createServer(options, std::move(handler));
    server->setAsTimeTracker();

    if (!server->setupSockets()) {
        error() << "Failed to set up sockets during startup.";
        return;
    }
    ...
    server->run();
 }

MessagePortServer里面, 通过Listener::initAndListen函数,最终在 PortMessageServer::accepted里面来创建新的线程处理本次的操作。
这里可以看到, 每一次新的操作, TicketHoder::tryAcquire会试图进入信号量的代码区, 如果信号量的连接数大于1, 就会获得信号量锁, 并且将连接数减少1; 如果此时的连接数为0 , tryAcquire会返回失败, 表明连接数已满, 无法连接。

获得信号量锁之后, 会建立一下新的处理线程, 指定其处理函数为:
PortMessageServer::handleIncomingMsg.

class PortMessageServer : public MessageServer, public Listener {
    virtual void accepted(std::shared_ptr<Socket> psocket, long long connectionId) {
        ScopeGuard sleepAfterClosingPort = MakeGuard(sleepmillis, 2);
        std::unique_ptr<MessagingPortWithHandler> portWithHandler(
            new MessagingPortWithHandler(psocket, _handler, connectionId));

        if (!Listener::globalTicketHolder.tryAcquire()) {
            log() << "connection refused because too many open connections: "
                  << Listener::globalTicketHolder.used() << endl;
            return;
        }

        {
                stdx::thread thr(stdx::bind(&handleIncomingMsg, portWithHandler.get()));
                thr.detach();
        }
  }