2010-08-22

Master-Workers 模式处理高负载

Views: 27762 | 7 Comments

对于高负载的网络服务器, 瓶颈几乎总是在等待 IO, 而 CPU 的计算能力往往不是最先遇到的问题. 你的服务器程序, 接收客户端的请求, 可能还要连接另一台网络服务器, 一起合作处理客户端的请求. 很多情况下, 你无法把客户端的连接以及与另一台服务器的连接统一处理, 不可避免地要出现等待. 这时, 只能使用多进程或者多线程.

Master-Workers(管理者-工作者)模式是处理这种情况的主要方式, 只要有 IO 等待或者其它耗时的操作, 都交互若干个工作者之一处理, 这样, 后续的请求不会被阻塞, 从而实现高负载.

目录:

下文中有时用"进程"一词同时指代进程和线程, 它们的区别主要考虑的是通信方式.

1. 单进程(单线程)网络服务器的结构

单进程网络服务器使用 IO 多路利用接口 - select(doc)/epoll(doc)/kqueue(doc) 等 - 等待网络连接的信号, 收到读信号后, 接收一个应用层报文(如HTTP请求或者其它自定义的报文), 处理, 然后返回响应. 结构如下:

// 主循环
while(1){
    events = select.wait(timeout=100ms)
    foreach(events as e){
        req = e.sock.recv()
        resp = server.process(req)
        e.sock.send(resp)
    }
}

很明显, 如果 process 方法处理的时间较长的话, 那么并发量就很小了, 因为下一个请求必须等待前一个被处理完毕之后才能处理. 这时候, 只能使用多进程或者多线程, 创建所谓的工作者(Worker)进程.

2. 多进程工作者模式(Master-Workers)

首先有一个 IO 进程(称为 Master), 也就是循环执行 select 的那个进程. 另有若干个工作者进程(称为 worker), 等待 IO 进程把报文给它们. 这也是一种"生产者-消费者"的模型. 具体上, 到底 IO 进程应该把哪一层次的数据交给工作者, 是简单的字节数组, 还是应用层的报文, 不同的应用不同设计. 现在假设传递的是完整的应用层报文, 也就是说, IO 进程负责报文解析, 如果报文的解析只消耗极少的资源, 这是最佳的方式.

2.1 Master-Workers 双向通信(长连接之一)

// 启动 10 个工作者
Workers = start_Workers(num=10)
// 主循环
while(1){
    resps = Workers.wait(Workers_timeout=10ms);
    if(resps.count > 0){
        foreach(resps as resp){
            resps.sock.send(resp)
        }
    }

    events = select.wait(select_timeout=10ms)
    if(events > 0){
        foreach(events as e){
            req = e.sock.recv()
            // 仅仅是委托, 会立即返回. 以后通过 Workers.wait() 获取响应.
            Workers.delegate(req)
        }
    }
}

Workers 的工作模式是:

while(1){
    req = req_queue.pop();
    resp = process(req)
    // 响应被加入到发送队列中
    resp_queue.push(resp)
}

wokers.wait 是等待工作者的处理结果事件(从响应队列中读取响应), 而 select.wait 是等待网络事件. 两者逻辑上相同. Workers 和 select 应该等待多久(即 timeout 的取值是多少)? 如果 Workers_timeout 太大, 那么就会导致 Workers_timeout 时间内即使有新的请求进来, 也无法及时处理, 因为 select.wait 还没被执行到. 相对比, 如果 select_timeout 太大, 那么又会导致 select_timeout 时间内响应无法及时发出, 因为程序阻塞在了 select.wait.

2.2 Master-Workers 单向通信(短连接)

短连接是指一次请求响应后便关闭的连接, 而不是时间长短.

那么, 能不能让 Workers.wait(及其对应的网络 send 操作) 和 select.wait(及其对应的网络 recv 操作) 运行在两个不同的线程里? 答案是可以的. 但是, 把同一个连接用两个线程(一个是 select.wait, 另一个是 Workers.wait)来操作, 有时很难处理多线程问题. 还有一个重要的问题, 如果把网络发送放在工作者线程里, 那么一个连接的网络发送阻塞, 会导致其中一整个线程完全失去效用, 仅仅是因为阻塞. 我们不仅要考虑网络读阻塞, 也要考虑网络写阻塞的问题. 不过, 如果是基于短连接的模式, 是可以这么做的, 如果一个响应不会超过 socket 发送缓冲大小的话, send 永远不会阻塞. 程序的结构改变为:

// 启动 10 个工作者
Workers = start_Workers(num=10)
// 主循环
while(1){
    events = select.wait(select_timeout=10ms)
    if(events > 0){
        foreach(events as e){
            req = e.sock.recv()
            // 仅仅是委托, 会立即返回. 不关心响应.
            Workers.delegate(req)
        }
    }
}

Workers 的工作模式是:

while(1){
    req = req_queue.pop();
    resp = process(req)
    // 响应这里发送给客户端
    resp.send()
    resp.close()
}

对于短连接, 这种工作模式非常好. 因为 resp.send() 和 resp.close() 非常快, 操作系统保证调用它们几乎不会阻塞. 但是, 这种模式无法扩展到多台机器.

2.3 Master-Workers 双向通信(长连接之二)

对于长连接, 是否还有更好的处理方法呢? 有的, 那就是把 Worker 做成可 select 的(Selectable), 比如 Master 和 Worker 通过网络通信, 那么就可以 select 处理结果了. 程序结构如下:

// 启动 10 个工作者
Workers = start_Workers(num=10)
// 主循环
while(1){
    events = select.wait(select_timeout=10ms)
    if(events > 0){
        foreach(events as e){
            if(req.type == USER_REQ){
                req = e.sock.recv()
                // 仅仅是委托, 会立即返回. 会通过 select 得到响应.
                Workers.delegate(req)
            }else if(req.type == WORKER_RESPONSE){
                resp = req;
                resp.sock.send(resp)
            }
        }
    }
}

在主循环中只有一个地方会等待, 无论是网络事件, 还是工作者事件, 都会立即唤醒 select.wait, 使事件得到及时得到响应.

这种方式是得到普遍应用的, 比如 lighttpd主循环IO 进程和 fastcgi 的通信就是这种方式. 当 Master 和 Worker 的通信使用的是网络 socket 的话, 可扩展性比 PIPE 更高.

欢迎讨论!

Related posts:

  1. 集成于 iphp 框架的 PHP 并发模型和工具
  2. 数据传输中的停止等待机制的实现
  3. 关于TCP粘包和拆包的终极解答
  4. Windows Python select标准输入输出
  5. Linux下整合Apache和Tomcat
Posted by ideawu at 2010-08-22 16:03:57

7 Responses to "Master-Workers 模式处理高负载"

  • 除了这种半同步半异步模型,全异步更加 Reply
  • lighttd 是这种模式? 我怎么记得是watcher ,worker 模式, watcher 只负责管理worker 进程的个数,不负责处理网络io。各个worker 自己抢占网络连接,自己处理网络io。 Reply
    回复hoterran: 抱歉, 我的描述不严密. 我的意思是指lighttpd(作为Web Server)和fascgi进程(作为Application Server)之间的协作就是Master-Worker模式. 另外, 关于一个进程做IO是否是瓶颈呢? 其实, lighttpd默认是单进程, IO进程一般不是瓶颈. 如果IO进程是瓶颈的话, 也只可能是读取逻辑太复杂(比如要解析复杂文本, 判断超时的逻辑复杂等等), 仅做不关心语义的read/write的话, 真的很少是瓶颈. Reply
  • 无论如何你的master 都要处理所有的网络io ,这点就够呛阿~~~
    只不过后续的process 交给worker 来处理 Reply
    @hoterran: 网络IO单进程(线程)一般不会是瓶颈,我们测过纯网络IO每秒可达几十W Reply
    @jackson: 没错, 网络IO其实消耗的CPU很少, 结合IO多路复用就可以轻松搞定. 主要是业务逻辑消耗CPU. Reply
  • 学习了。谢谢 Reply

Leave a Comment