Go Web服务器的进程管理

19

我是一名新的Go程序员,来自于Web应用和服务开发领域。抱歉,如果这是一个问题的话,我的搜索结果没有找到任何答案。同时,由于我更关心API /编程接口,因此这是边缘Server Fault领域,但我在这里提问。

我使用net/http包的内置Web服务器编写了一个小型Go程序。我准备部署到生产环境,但我不太清楚如何处理Go的Web服务器过程以及如何部署。

具体而言-在我习惯的环境中(PHP、Ruby、Python),我们有一个Web服务器(Apache、Nginx等)坐在我们的应用程序前面,并配置这些Web服务器使用一定数量的工作进程/线程,并配置每个线程应该处理多少个单独的HTTP(S)连接。

我没能找到关于Go Web服务器如何处理这个问题的信息,也没有关于如何扩展/规划Go Web服务器的实际信息。

即-如果我运行一个简单的程序,准备处理HTTP请求

func main() {
   http.HandleFunc("/", processRequest)
   http.ListenAndServe(":8000", nil)    
}

HandleFunc 一次性会处理多少个连接?当一个连接打开时,它会开始阻塞,直到该连接被关闭后才会继续服务下一个连接吗?

如果我把所有东西都塞到 go routine 中,会不会出现太多执行线程导致系统过载的情况?或许我不需要担心这个问题吗?

我的主要目标是:

  1. 了解 Go Web 服务器的处理模式。
  2. 查找用于调整此过程的 Go 内置特性和/或标准包。

如我所说,我对 Go 还非常陌生,如果我完全没有理解,请告诉我!


2
您可以通过创建/配置自己的http.Server(详见其文档)来调整/定制HTTP服务器。net/http包文档中包含了一个示例。此外,每个传入的HTTP请求都会并发处理,通过为每个请求创建一个新的服务goroutine,如Server.Serve()所述。 - icza
@icza 谢谢!非常有用的信息。但是,我不确定我完全理解了关于http.Server的评论。文档(似乎?)说我可以配置Addr、Handler、ReadTimeout、WriteTimeout、MaxHeaderBytes、TLSConfig、TLSNextProto、ConnState和ErrorLog。我没有看到任何关于进程管理的内容。我是否遗漏了什么,还是你只是一般性地推荐一个好的起点? - Alana Storm
2个回答

17

调整/配置HTTP服务器

实现HTTP服务器的类型是http.Server。如果你不自己创建http.Server,例如因为你调用了http.ListenAndServe()函数,它会在幕后为您创建一个 http.Server

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

如果您想微调/自定义HTTP服务器,则可以创建一个自己的服务器并调用其Server.ListenAndServe()方法。 http.Server是一个结构体,其零值是有效配置。查看其文档以了解其拥有哪些字段以及您可以微调/配置哪些内容。

HTTP服务器的“进程管理”在Server.Serve()中记录:

Serve接受监听器l上的传入连接,为每个连接创建一个新的服务goroutine。服务goroutine读取请求,然后调用srv.Handler进行回复。Serve始终返回非nil错误。

这意味着每个传入的HTTP请求都在其自己的goroutine中处理,因此它们是并发提供的。不幸的是,API没有记录任何更改此工作方式的方法。

查看当前实现(Go 1.6.2)也没有未记录的方法来做到这一点。在server.go,目前是第2107-2139行

2107    func (srv *Server) Serve(l net.Listener) error {
2108        defer l.Close()
2109        if fn := testHookServerServe; fn != nil {
2110            fn(srv, l)
2111        }
2112        var tempDelay time.Duration // how long to sleep on accept failure
2113        if err := srv.setupHTTP2(); err != nil {
2114            return err
2115        }
2116        for {
2117            rw, e := l.Accept()
2118            if e != nil {
2119                if ne, ok := e.(net.Error); ok && ne.Temporary() {
2120                    if tempDelay == 0 {
2121                        tempDelay = 5 * time.Millisecond
2122                    } else {
2123                        tempDelay *= 2
2124                    }
2125                    if max := 1 * time.Second; tempDelay > max {
2126                        tempDelay = max
2127                    }
2128                    srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
2129                    time.Sleep(tempDelay)
2130                    continue
2131                }
2132                return e
2133            }
2134            tempDelay = 0
2135            c := srv.newConn(rw)
2136            c.setState(c.rwc, StateNew) // before Serve can return
2137            go c.serve()
2138        }
2139    }

如您在第2137行所见,连接会在一个新的 goroutine 上被无条件地服务,因此你无法对其进行任何操作。

限定“worker” goroutines

如果您想要限制处理请求的goroutine数量,您仍然可以这样做。

您可以在多个级别上进行限制。要在监听器级别上进行限制,请参阅Darigaaz的答案。 要在处理程序级别上进行限制,请继续阅读。

例如,您可以将代码插入到每个http.Handler或处理函数(http.HandlerFunc),该代码仅在并发请求处理的goroutine数小于指定限制时才继续执行。

有许多构造用于这种限制同步代码。 一个例子是:创建具有所需限制容量的缓冲通道。每个处理程序应首先在该通道上发送值然后执行工作。当处理程序返回时,它必须从通道接收一个值:因此最好使用延迟函数(不要忘记“清理”自己)。

如果缓冲区已满,则试图在通道上发送请求的新请求将被阻塞:等待请求完成其工作。

请注意,您不必将此限制代码注入到所有处理程序中,您可以使用“中间件”模式,这是一种新的处理程序类型,它包裹了您的处理程序,执行此限制同步任务,并在其中间调用包装后的处理程序。

在处理程序中进行限制(而不是在监听器中进行限制)的优点是,在处理程序中我们知道处理程序在做什么,所以我们可以进行有选择性的限制(例如,我们可以选择限制某些请求,如数据库操作,而不是限制其他请求,如提供静态资源),或者我们可以根据需要创建多个不同的限制组(例如,最大限度地限制并发db请求为10,最大限制静态请求为100,最大限制重计算请求为3等等)。 我们还可以轻松地实现像登录/付费用户无限制(或高限制)和匿名/非付费用户低限制等限制。

还要注意,您甚至可以在单个位置进行速率限制,而无需使用中间件。创建一个“主处理程序”,并将其传递给http.ListenAndServe()(或Server.ListenAndServe())。在此主处理程序中,执行速率限制(例如,使用上述缓冲通道),然后简单地将调用转发到您正在使用的http.ServeMux

以下是一个简单的示例,它使用http.ListenAndServe()http包的默认多路复用器(http.DefaultServeMux)进行演示。 它将并发请求数限制为2:

func fooHandler(w http.ResponseWriter, r *http.Request) {
    log.Println("Foo called...")
    time.Sleep(3 * time.Second)
    w.Write([]byte("I'm Foo"))
    log.Println("Foo ended.")
}

func barHandler(w http.ResponseWriter, r *http.Request) {
    log.Println("Bar called...")
    time.Sleep(3 * time.Second)
    w.Write([]byte("I'm Bar"))
    log.Println("Bar ended.")
}

var ch = make(chan struct{}, 2) // 2 concurrent requests

func mainHandler(w http.ResponseWriter, r *http.Request) {
    ch <- struct{}{}
    defer func() {
        <-ch
    }()

    http.DefaultServeMux.ServeHTTP(w, r)
}

func main() {
    http.HandleFunc("/foo", fooHandler)
    http.HandleFunc("/bar", barHandler)

    panic(http.ListenAndServe(":8080", http.HandlerFunc(mainHandler)))
}

部署

使用Go编写的Web应用程序不需要外部服务器来控制进程,因为Go Web服务器本身可以并发处理请求。

因此,您可以按原样启动以Go编写的Web服务器:Go Web服务器已经准备就绪。

当然,如果您愿意,可以使用其他服务器执行其他任务(例如处理HTTPS、身份验证/授权、路由、在多个服务器之间进行负载平衡等)。


请问您能否举个有关选择性限制处理程序的例子?我正在寻找一种方法,因为其中一个处理程序需要大量资源,希望将其限制为最多5个并发请求。 - Abhinav
@Abhinav 在速率限制中间件中,您可以根据请求的性质判断是否需要进行速率限制。如果需要,则将其发送到通道上,然后调用处理程序,最后从通道接收。如果不需要速率限制,则直接调用处理程序,无需在通道上发送或接收任何内容。 - icza

5

ListenAndServe函数使用给定的地址和处理程序启动HTTP服务器。处理程序通常为nil,这意味着使用DefaultServeMux。Handle和HandleFunc将处理程序添加到DefaultServeMux

看一下http.Server,许多字段是可选的,并且使用默认值可以正常工作。

现在让我们看看http.ListenAndServe,这并不难。

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

所以默认服务器非常简单创建。
func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
 }

func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    if fn := testHookServerServe; fn != nil {
        fn(srv, l)
    }
    var tempDelay time.Duration // how long to sleep on accept failure
    if err := srv.setupHTTP2(); err != nil {
        return err
    }
    for {
        rw, e := l.Accept()
        if e != nil {
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve()
    }
}

它监听“addr”并接受每个连接,然后生成一个goroutine以独立处理每个连接。(HTTP / 2.0有点不同,但总体上是相同的)。
如果要控制连接,您有两个选项:
1. 创建自定义服务器(只需3行代码)并使用server.ConnState回调控制客户端连接。 (但它们仍将被内核接受) 2. 创建具有自己实现的net.Listener(如LimitedListener)的自定义服务器,并从那里控制连接,这样您将对连接拥有终极控制权。
由于默认的http.Server没有停止的方法,第二种方法是优雅终止侦听器的唯一方法。 您可以结合两种方法来实现不同的策略,已经完成了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接