QFS MetaServer请求处理流程

164 阅读10分钟
原文链接: zhuanlan.zhihu.com

QFS的元数据服务器MetaServer采用了Master + Worker的架构,Master进程负责监听socket并对新连接按照轮询算法交给Worker线程来处理。

数据结构

ClientManager && ClientManager::Impl

相当于MetaServer的Master角色。其关键的逻辑是在启动时监听端口,对端口上有新连接到来时触发预先设定的回调函数。同时内部维护了所有的Worker线程信息。

这个比较简单,可说的也不多:

class ClientManager::Impl : public IAcceptorOwner
{
    ...
private:
    class ClientThread;
    // The socket object which is setup to accept connections.
    Acceptor*                    mAcceptor;
    // 工作线程
    ClientManager::ClientThread* mClientThreads;
    ...
}

ClientThread

该类抽象了工作线程。每个工作线程被Master线程分配了已经建立好的客户端连接。工作线程维护自己的NetManager对象,使用该对象监听它所管理的网络连接上的读写事件。

工作线程内部维护了待处理请求队列、已完成请求队列、新链接队列等。

class ClientManager::ClientThread :
    public QCRunnable,
    private NetManager::Dispatcher
{
public:
    ...
private:
    ...
    // NetManager对象
    NetManager         mNetManager;
    // 已完成请求队列
    ReqQueue           mReqQueue;
    // 待处理请求队列
    ReqQueue           mReqPendingQueue;
    // 链接队列
    CliQueue           mCliQueue;
    ......
}

工作线程的运行主函数是NetManager::MainLoop

virtual void Run()
{
    QCMutex* const kMutex                = 0;
    bool     const kWakeupAndCleanupFlag = true;
    mNetManager.MainLoop(kMutex, kWakeupAndCleanupFlag, this);
}

ClientThread内部还组织了一个ClientSM对象链表,称之为 CliQueue,每当ClientThread收到客户端的一个新连接的时候都会创建一个ClientSM对象,并添加该对象至CliQueue链表上。

ClientSM

客户端请求状态机。每个ClientSM管理着一个客户端,确切来说,每个客户端的连接都会维护一个ClientSM对象。ClientThread收到一个新连接的时候会创建一个ClientSM对象。以后该ClientSM对象的所有请求都会由该ClientThread来处理。

ClientSM最关键的方法是客户端请求的处理入口:ClientSM::HandleRequest。其内部有一个关键的成员变量是mClientThread,类型为ClientManager::ClientThread

由此可见,ClientSM与ClientThread之间是一个N:1的关系。每个ClientSM内部有成员指向ClientThread,每个ClientThread内组织了ClientSM链表。来自ClientSM的客户端请求最终都会交由其指向的ClientThread来处理。而ClientThread会为Master线程分配给其的每一个客户端连接创建一个对象ClientSM。

MetaRequest

客户端请求的基类。其最主要的方法是MetaRequest::handle,所有子类请求实现自己的handle方法。

LogWriter

实现MetaServer请求写日志功能,所有的元数据更新请求都会被记录在日志中,以在系统故障重启时通过checkpoint + 日志恢复奔溃前内存状态。在这里暂时忽略日志,后续在专门文章中分析日志实现。

核心流程

接收客户端新连接

Master线程负责接收客户端上的新的连接以及管理Worker线程。ClientManager内负责监听网络端口以及接收新连接的是成员Acceptor。关键的API如下:

bool ClientManager::Impl::Bind()
​
bool ClientManager::Impl::StartAcceptor()

当Acceptor接收到一个新的连接时,会触发Acceptor的回调函数:Acceptor::RecvConnection。在这里会调用其拥有者(也即ClientManager对象)的回调:mAcceptorOwner->CreateKfsCallbackObj(conn)

Acceptor::Acceptor(
    NetManager&           netManager,
    const ServerLocation& location,
    bool                  ipV6OnlyFlag,
    IAcceptorOwner*       owner,
    bool                  bindOnlyFlag)
    : mLocation(location),
      mIpV6OnlyFlag(ipV6OnlyFlag),
      mAcceptorOwner(owner),
      mConn(),
      mNetManager(netManager)
{
    SET_HANDLER(this, &Acceptor::RecvConnection);
    Acceptor::Bind();
    if (! bindOnlyFlag) {
        Acceptor::StartListening();
    }
}
​
int
Acceptor::RecvConnection(int code, void* data)
{
    switch (code) {
        case EVENT_NEW_CONNECTION:
        break;
        ......
    }
    NetConnectionPtr& conn = *reinterpret_cast<NetConnectionPtr*>(data);
    KfsCallbackObj* const obj = mAcceptorOwner->CreateKfsCallbackObj(conn);
    if (conn) {
        if (obj) {
            conn->SetOwningKfsCallbackObj(obj);
            mNetManager.AddConnection(conn);
        } else {
            conn->Close();
        }
    }
    return 0;
}

接收到一个新的Connection的关键在于mAcceptorOwner->CreateKfsCallbackObj(conn)。对于MetaServer来说,其管理客户端连接的类是ClientSM,因此,mAcceptorOwner便是ClientSM对象,其CreateKfsCallbackObj实现是:

KfsCallbackObj*
ClientManager::Impl::CreateKfsCallbackObj(NetConnectionPtr& conn)
{
    if (mClientThreadCount < 0 || ! conn || ! conn->IsGood()) {
        return 0;
    }
    const int connCount = ClientSM::GetClientCount();
    if (GetMaxClientCount() <= connCount) {
      ......
    }
    if (mClientThreadCount == 0) {
        return new ClientSM(conn);
    }
    int idx = mNextThreadIdx;
    if (idx >= mClientThreadCount || idx < 0) {
        idx = 0;
        mNextThreadIdx = idx + 1;
    } else {
        mNextThreadIdx++;
    }
    mClientThreads[idx].Add(conn);
    return 0;
}

可以看到,对于一个新连接,按照轮询算法为其分配一个ClientThread。然后将该连接加入到ClientThread中(mClientThreads[idx].Add(conn))。与此同时,ClientThread会创建一个ClientSM来管理该新连接,以后该连接上所有的用户请求都会由ClientSM统一负责管理。

void Add(NetConnectionPtr& conn)
{
    ...
    ClientSM* const cli = new ClientSM(
            conn, this, &mWOStream, mParseBuffer);
    mCliQueue.PushBack(*cli);
    ...
}

可以看到,ClientSM和客户端的Connection一一对应。而在创建ClientSM对象的时候,设置了连接的事件触发回调函数:

ClientSM::ClientSM(
    const NetConnectionPtr&      conn,
    ClientManager::ClientThread* thread,
    IOBuffer::WOStream*          wostr,
    char*                        parseBuffer)
    : KfsCallbackObj(),
      ......
{
    ...
    // 设置事件触发回调函数
    SET_HANDLER(this, &ClientSM::HandleRequest);
}

接收客户端请求

上面说到,每个客户端连接上一旦有新的请求到来时便触发预先设置的回调函数ClientSM::HandleRequest。在这里会从连接上读取并解析命令。一旦完整命令解析完毕,最终会将命令通过ClientManager::SubmitRequest提交。

前面我们说到,每个连接一一对应了一个ClientSM对象,而ClientSM对象则是由Worker线程ClientThread创建,创建时顺便设置了ClientSM对应的工作线程。也即:所有来自该ClientSM上的请求都由ClientSM对应的ClientThread来处理。而所谓的命令提交,其实是将该请求插入到ClientThread内部请求队列(mReqPendingQueue)尾部等待被处理。将请求添加至请求队列后,还会顺带唤醒工作线程。

void ClientManager::SubmitRequestSelf(ClientManager::ClientThread* thread,
    MetaRequest& op)
{
    assert(thread);
    thread->Add(op);
}
​
// 每个ClientThread都绑定了自己的NetManager对象
void ClientThread::Add(MetaRequest& op)
{
    const bool wasEmptyFlag = mReqPendingQueue.IsEmpty();
    mReqPendingQueue.PushBack(op);
    if (wasEmptyFlag) {
        mNetManager.Wakeup();
    }
}

到这里,连接上的请求最终就被添加到了ClientThread的mReqPendingQueue队列上,那这些请求会在何时被处理呢?

工作线程处理请求

每个工作线程由ClientThread来抽象。在MetaServer初始化时根据配置创建特定数量的此类线程。线程内部运行大循环NetManager::MainLoop。工作线程被唤醒后进入循环处理事件。由于ClientThread继承了NetManager::Dispatcher,在MainLoop主循环中会运行ClientThread::DispatchStart

在该方法中,首先处理请求队列中的客户端请求。请求的处理是先将请求插入到日志的写入队列中(关于这个我们会在后面详细描述),然后开始处理请求,请求的处理是调用了每个请求类的handle方法。这些都是在函数MetaRequest::SubmitBegin中执行。

而请求一旦处理完成后,会调用MetaRequest::SubmitEnd来进行收尾工作。这里的收尾工作主要是要将处理结果调度返回给客户端,其主要的调用流程是:

MetaRequest::SubmitEnd()
    --->NetDispatch::Dispatch()
             --->ClientSM::HandleRequest(EVENT_CMD_DONE, r)
                     --->ClientManager::Enqueue(mClientThread...)
                             --->ClientThread::Enqueue(MetaRequest& op)

最终是在ClientThread::Enqueue内将该完成的请求插入至工作线程ClientThread的完成队列mReqQueue尾部。这样,一个请求便完成了在MetaServer上的处理。

这样,每个工作线程内如何处理请求、发送响应等流程已经非常清晰。

每个ClientThread都绑定了自己的NetManager对象,也就是说,每个ClientThread独立处理自身的所有事件:新请求接收、请求处理等。NetManager::MainLoop在被唤醒后会运行一个Dispatcher对象的Start方法,如下:

void
NetManager::MainLoop(
    QCMutex*                mutex                /* = 0 */,
    bool                    wakeupAndCleanupFlag /* = true */,
    NetManager::Dispatcher* dispatcher           /* = 0 */,
    bool                    runOnceFlag          /* false */)
{
    while (mRunFlag) {
        ......
        if (dispatcher) {
            dispatcher->DispatchStart();
        }
        ......
    }
    .....
}

而ClientThread在启动运行的时候传入的参数参考如下:

// ClientThread继承并实现了Dispatcher:
class ClientManager::ClientThread :
    public QCRunnable,
    private NetManager::Dispatcher
{
    virtual void DispatchStart()
    {...}
}
​
virtual void ClientThread::Run()
{
    QCMutex* const kMutex                = 0;
    bool     const kWakeupAndCleanupFlag = true;
    // 将this(当前对象指针作为Dispatcher参数传入)
    mNetManager.MainLoop(kMutex, kWakeupAndCleanupFlag, this);
}

而其具体实现:

virtual void ClientThread::DispatchStart()
{
    ReqQueue reqPendingQueue;
    reqPendingQueue.PushBack(mReqPendingQueue);
​
    MetaRequest* op;
    // 开始真正地处理请求
    while ((op = reqPendingQueue.PopFront())) {
        submit_request(op);
    }
    ...
​
    CliQueue cliQueue;
    ReqQueue reqQueue;
    QCStMutexLocker threadQueuesLocker(mMutex);
    reqQueue.PushBack(mReqQueue);
    cliQueue.PushBack(mCliQueue);
    threadQueuesLocker.Unlock();
​
    FlushQueue::iterator it = mFlushQueue.begin();
    NetConnectionPtr conn;
    while ((op = reqQueue.PopFront())) {
       ...
    }
    for (FlushQueue::iterator cit = mFlushQueue.begin();
            cit != it;
            ++cit) {
        ...
    }
    // jeff.ding: 处理Master分配给自身的新连接
    const bool runningFlag = mNetManager.IsRunning();
    ClientSM*  cli;
    while ((cli = cliQueue.PopFront())) {
        const NetConnectionPtr& conn = cli->GetConnection();
        conn->SetOwningKfsCallbackObj(cli);
        if (runningFlag) {
            mNetManager.AddConnection(conn);
        } else {
            conn->HandleErrorEvent();
        }
    }
    // Wake main thread if need to process requests waiting for
    // io buffers, if any.
    CheckIfIoBuffersAvailable();
}

请求处理完成,返回响应给客户端

工作线程ClientThread在上面处理完本地请求队列内的请求后,还会:1). 将完成队列内的请求响应发送给客户端;2). 处理Master线程分配给自己的新连接。这里就不再细说

总结

至此,MetaServer接收、处理、响应客户端请求的流程就已经全部梳理完毕。这样,每个工作线程内如何处理客户端请求、发送响应等流程已经非常清晰。MetaServer处理ChunkServer的请求流程基本类似,不再赘述。


总结

下面对客户端建立连接到发送请求等整个流程对MetaServer处理客户端请求做一个整体的梳理。

从整体架构上来说,MetaServer也属于Master + Worker结构。Master负责监听网络端口上是否有新的连接建立,如果有,会将该连接交给Worker线程,接下来Worker线程会负责处理该连接上的所有请求。

接收客户端新连接

Master线程负责接收客户端上的新的连接以及管理Worker线程。QFS为此抽象了类ClientManager以及ClientManager::Impl,创建的关键数据结构是Acceptor。关键的API如下:

bool ClientManager::Impl::Bind()
​
bool ClientManager::Impl::StartAcceptor()

当Acceptor接收到一个新的连接时,会触发Acceptor的回调函数,即:Acceptor::RecvConnection。在这里会调用其拥有者(也即ClientManager对象)的回调:mAcceptorOwner->CreateKfsCallbackObj(conn)

ClientManagerCreateKfsCallbackObj方法中,按照轮询算法为新建立的连接分配一个ClientThread对象,并调用ClientThread::Add()方法将新连接添加至ClientThread的队列中(mCliQueue)。

ClientThread收到一个新连接时还会为其创建一个ClientSM来管理新连接,并为该连接设置了事件回调函数ClientSM::HandleRequest。以后该连接上有任何事件到来后都会先触发该回调函数,再作进一步处理。

接收客户端请求

上面说到,每个客户端连接上一旦有新的请求到来时便触发预先设置的回调函数ClientSM::HandleRequest。在这里会从连接上读取并解析命令。一旦完整命令解析完毕,最终会将命令通过ClientManager::SubmitRequest提交。

前面我们说到,每个连接一一对应了一个ClientSM对象,而ClientSM对象则是由Worker线程ClientThread创建,创建时顺便设置了ClientSM对应的工作线程。也即:所有来自该ClientSM上的请求都由ClientSM对应的ClientThread来处理。而所谓的命令提交,其实是将该请求插入到ClientThread内部请求队列(mReqPendingQueue)尾部等待被处理。

将请求添加至请求队列后,还会顺带唤醒工作线程。

工作线程处理请求

每个工作线程由对象ClientThread来抽象。在MetaServer初始化时根据配置创建特定数量的此类线程。线程内部运行大循环NetManager::MainLoop。工作线程被唤醒后进入循环处理事件。由于ClientThread继承了NetManager::Dispatcher,在MainLoop主循环中会运行ClientThread::DispatchStart

在该方法中,首先处理请求队列中的客户端请求。请求的处理是先将请求插入到日志的写入队列中(关于这个我们会在后面详细描述),然后开始处理请求,请求的处理是调用了每个请求类的handle方法。这些都是在函数MetaRequest::SubmitBegin中执行。

而请求一旦处理完成后,会调用MetaRequest::SubmitEnd来进行收尾工作。这里的收尾工作主要是要将处理结果调度返回给客户端,其主要的调用流程是:

MetaRequest::SubmitEnd()
    --->NetDispatch::Dispatch()
             --->ClientSM::HandleRequest(EVENT_CMD_DONE, r)
                     --->ClientManager::Enqueue(mClientThread...)
                             --->ClientThread::Enqueue(MetaRequest& op)

最终是在ClientThread::Enqueue内将该完成的请求插入至工作线程ClientThread的完成队列mReqQueue尾部。这样,一个请求便完成了在MetaServer上的处理。

工作线程在处理完成本地请求队列内的请求后,还需要:1). 将完成队列内的请求响应发送给客户端;2). 处理Master线程分配给自己的新连接。