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 程序中?但是,如果我这样做,我如何防止系统因过多的执行线程而陷入困境?
我基本上是在尝试
- 了解go web服务器的进程模式
- 找到 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 个选择:
使用服务器创建自定义服务器(其 3 行代码)。ConnState 回调并从那里控制客户端连接。 (但无论如何它们都会被内核接受)
使用您自己实现的 net.Listener
(如 LimitedListener
)创建自定义服务器并从那里控制连接,这样您将拥有控制连接的最终权力。
由于默认 http.Server
无法停止,第二种方式是优雅终止侦听器的唯一方式。您可以结合两种方法来实现不同的策略,做得很好 already.
我是一名新的 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 程序中?但是,如果我这样做,我如何防止系统因过多的执行线程而陷入困境?
我基本上是在尝试
- 了解go web服务器的进程模式
- 找到 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 个选择:
使用服务器创建自定义服务器(其 3 行代码)。ConnState 回调并从那里控制客户端连接。 (但无论如何它们都会被内核接受)
使用您自己实现的
net.Listener
(如LimitedListener
)创建自定义服务器并从那里控制连接,这样您将拥有控制连接的最终权力。
由于默认 http.Server
无法停止,第二种方式是优雅终止侦听器的唯一方式。您可以结合两种方法来实现不同的策略,做得很好 already.