您的位置:首页 > 理论基础 > 计算机网络

Go 自带的 http/server.go 的连接解析 与 如何结合 master-worker 并发模式,提高单机并发能力

2018-05-17 14:35 896 查看

作者:林冠宏 / 指尖下的幽灵

掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8

博客:http://www.cnblogs.com/linguanh/

GitHub : https://github.com/af913337456/

腾讯云专栏: https://cloud.tencent.com/developer/user/1148436/activities

关于

server.go
源码的解析可以去搜下,已经有很多且还不错的文章。

正文:

从我们启动

http.ListenAndServe(port,router)
开始,
server.go
内部最终在一个
for
循环中的
accept
方法中不停地等待
客户端
的连接到来。

每接收到一个

accept
就启动一个
gorutine
去处理当前
ip
的连接。也就是源码里的
go c.serve(ctx)
。这一个步骤在
c.serve(ctx)
它并不是简单的形式:

请求-->处理请求-->返回结果-->断开这个连接-->结束当前的 gorutine

根据我的
调试结果
源码分析
显示,正确的形式是下面这样的:

  1. 为每一个连接的用户启动了一个长连接,

    serve
    方法内部有个超时的设置是
    c.rwc.SetReadDeadline(time.Time{})
    ,这样子的情况,如果内部不出错,当前的连接断开的条件是
    客户端
    自己断开,或
    nat
    超时。

  2. 这个连接建立后,以

    ip
    为单位,当前的
    客户端
    ,此时它的所有
    http请求
    ,例如
    get
    post
    ,它们都会在这个启动的
    gorutine
    内进行
    分发
    被处理

  3. 也就是说,同一个

    ip
    ,多个
    不同的
    请求,这里不会触发另一个
    accept
    ,不会再去启动一个
    go c.serve(ctx)

上述我们得出结论:

  1. 如果有

    100万
    accept
    ,就证明有
    100万
    个连接,
    100万
    ip
    与当前
    server
    连接。即是我们说的
    百万连接

  2. 百万连接
    不是
    百万请求

  3. 每一个连接,它可以进行多个

    http请求
    ,它的请求都在当前启动这个连接的
    gorutine
    里面进行。

  4. c.serve(...)
    源码中的
    for 死循环
    就是负责读取每个请求再分发
for {
w, err := c.readRequest(ctx) // 读取一个 http 请求
//...
ServeHTTP(...)
}
  1. 我们的
    100万
    连接里面,有可能并发更多的请求,例如几百万请求,一个
    客户端
    快速调用多个
    请求api

图解总结

结合 master-worker 并发模式

根据我们上面的分析,每一个新连接到来,go 就会启动一个

gorutine
,在源码里面也没有看到有一个量级的限制,也就是达到多少连接就不再接收。我们也知道,服务器是有处理瓶颈的。

所以,在这里插播

一个优化点
,就是在
server.go
内部做一个连接数目的限制。

master-worker
模式本身是启动多个
worker
线程,去并发
读取
有界队列里面的任务,并执行。

我自身已经实现了一个

go版本
master-worker
,做过下面的尝试:

  1. go c.serve(ctx)
    处做修改,如下。
if srv.masterWorkerModel {
// lgh --- way to execute
PoolMaster.AddJob(
masterworker.Job{
Tag:" http server ",
Handler: func() {
c.serve(ctx)
fmt.Println("finish job") // 这一句在当前 ip 断开连接后才会输出
},
})
}else{
go c.serve(ctx)
}

func (m Master) AddJob(job Job)  {
fmt.Println("add a job ")
m.JobQueue <- job // jobQueue 是具备缓冲的
}
// worker
func (w Worker) startWork(master *Master)  {
go func() {
for {
select {
case job := <-master.JobQueue:
job.doJob(master)
}
}
}()
}
// job
func (j Job) doJob(master *Master) {
go func() {
fmt.Println(j.Tag+" --- doing job...")
j.Handler()
}()
}

不难理解它的模式。

现在我们使用

生产者--消费者模式
进行假设,
连接的产生
生产者
<-master.JobQueue
消费者
,因为每一次消费就是启动一个处理的
gorutine

因为我们在

accept
一个请求到
<-master.JobQueue
,管道输出一个的这个过程中,可以说是没有耗时操作的,
这个job
,它很快就被输出了管道。也就是说,
消费很快
,那么实际的
生产环境
中,我们的
worker
工作
协程
启动
5~10
个就有余了。

考虑如果出现了

消费跟不上
的情况,那么多出来的
job
将会被缓冲到
channel
里面。这种情况可能出现的情景是:

短时间十万+级别连接的建立,就会导致

worker
读取不过来。不过,即使发生了,也是很快就取完的。因为间中的耗时几乎可以忽略不计!

也就说,

短时间
大量连接的建立,它的瓶颈在
队列的缓冲数
。但是即使瓶颈发生了,它又能很快被分发处理掉。所以说:

  • 我的这个第一点的尝试的意义事实上没有多大的。只不过是换了一种方式去分发

    go c.serve(ctx)

  1. 这个是第二种结合方式,把
    master-worker
    放置到
    ServeHTTP
    的分发阶段。例如下面代码,是常见的
    http handler
    写法,我们就可以嵌套进去。
func (x XHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)  {
//...
if x.MasterWorker {
poolMaster.AddJob(master_worker.Job{
Tag:"normal",
XContext:xc,
Handler: func(context model.XContext) {
x.HandleFunc(w,r)
},
})
return
}
x.HandleFunc(w,r)
//...
}

这样的话,我们就能控制所有连接的并发请求最大数。超出的将会进行排队,等待被执行,而不会因为

短时间 http 请求数目不受控暴增
而导致
服务器
挂掉。

此外上述

第二种
还存在一个:
读,过早关闭问题
,这个留给读者尝试解决。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: