Go Web 服务器的进程管理

Process Management for the Go Webserver

我是一名新的 Go 程序员,来自 Web 应用程序和服务开发领域。抱歉,这是一个 herp de-derp 问题,但我四处寻找答案没有找到任何答案。此外,这是边缘服务器故障领域,但由于我对 APIs/programmatic 接口更感兴趣,所以我在这里提问。

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

具体来说——在我习惯的环境中(PHP、Ruby、Python),我们有一个 Web 服务器(Apache、Nginx 等)位于在我们的应用程序前面,我们将这些 Web 服务器配置为使用一定数量的 worker processes/threads,并配置每个线程应处理多少个单独的 HTTP(S) 连接。

我无法找到有关 Go 的网络服务器如何处理此问题的信息,或有关如何 scale/plan-for-scale 用于 Go 网络服务器的实用信息。

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

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

HandleFunc 将尝试同时处理多少个连接?或者它会在连接打开时开始阻塞,并在连接关闭后只为下一个连接提供服务吗?

或者我是否应该不担心这个并将所有内容都塞进一个 go 程序中?但是,如果我这样做,我如何防止系统因过多的执行线程而陷入困境?

我基本上是在尝试

  1. 了解go web服务器的进程模式
  2. 找到 go 的内置功能来调整它,and/or 无论人们使用什么标准包

就像我说的,我很新,所以如果我完全错过了这方面的情节,请告诉我!

调整/配置 HTTP 服务器

实现 HTTP 服务器的类型是 http.Server. If you don't create an http.Server yourself e.g. because you call the 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 服务器的 "Process Management" 记录在 Server.Serve():

Serve accepts incoming connections on the Listener l, creating a new service goroutine for each. The service goroutines read requests and then call srv.Handler to reply to them. Serve always returns a non-nil error.

因此每个传入的 HTTP 请求都在其新的 goroutine 中处理,这意味着它们是同时服务的。不幸的是,API 没有记录任何进入和改变其工作方式的方法。

并且查看当前的实现(Go 1.6.2),也没有未记录的方法来做到这一点。 server.go, currently line #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 or handler functions (http.HandlerFunc) 插入一个代码,只有当并发请求服务 goroutine 的数量小于指定限制时才会继续。

此类限制同步代码有多种结构。一个示例可能是:创建一个缓冲通道,其容量是您想要的限制。每个处理程序应首先在此通道上发送一个值,然后执行工作。当处理程序 returns 时,它必须从通道接收一个值:因此最好在延迟函数中完成(不要忘记 "clean" 本身)。

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

请注意,您不必将此限制代码注入所有处理程序,您可以使用 "middleware" 模式,一种包装您的处理程序的新处理程序类型,执行此限制同步工作,并且在中间调用包装处理程序。

在处理程序中进行限制(相对于在侦听器中进行限制)的优势在于,在处理程序中我们知道处理程序的作用,因此我们可以进行选择性 限制(例如我们可以选择限制某些请求,例如数据库操作,而不限制其他请求,例如服务静态资源)或者我们可以根据需要任意创建多个不同的限制组(例如限制并发数据库请求最多 10 个,静态请求最多 100 个,繁重的计算请求最多 3 个)等。我们还可以轻松地实现限制,例如登录/付费用户的无限制(或上限),以及匿名/非用户的下限-付费用户。

另请注意,您甚至可以在一个地方进行速率限制,而无需使用中间件。创建一个 "main handler",并将其传递给 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 编写的网络服务器:Go 网络服务器已准备就绪。

如果您愿意,您当然可以使用其他服务器来完成其他任务(例如 HTTPS 处理、身份验证/授权、路由、多台服务器之间的负载平衡)。

ListenAndServe 启动具有给定地址和处理程序的 HTTP 服务器。 handler一般为nil,表示使用DefaultServeMux。句柄和 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有点不同,但大体上是一样的)。

如果您想控制连接,您有 2 个选择:

  1. 使用服务器创建自定义服务器(其 3 行代码)。ConnState 回调并从那里控制客户端连接。 (但无论如何它们都会被内核接受)

  2. 使用您自己实现的 net.Listener(如 LimitedListener)创建自定义服务器并从那里控制连接,这样您将拥有控制连接的最终权力。

由于默认 http.Server 无法停止,第二种方式是优雅终止侦听器的唯一方式。您可以结合两种方法来实现不同的策略,做得很好 already.