一生经过彷徨的挣扎,自信可改变未来。 《中国合伙人》
1. 前言
很多人在启动双端口服务的时候很搞笑,经常会直接go出去就不管了或者go出去之后select一下就以为万事大吉了,我现在告诉大家启动双端口服务真的没你想的那么简单,那么这篇文章我们来聊聊双端口到底如何实现的。
2. 需求描述
研发童鞋们为了排查问题会在生产环境部署一个debug服务,专门采集线上性能数据,比如golang的pprof工具采集cpu,内存以及磁盘等相关指标。但是又不能为了这个需求单独部署一个服务,所以在APP项目中又额外加入一个debug端口服务,供研发来调用。
3. 实现双端口
package main import ( "context" "fmt" "net/http" ) func Server(addr string, handler http.Handler, stop <- chan struct{}) error { s := http.Server{ Addr: addr, Handler: handler, } //接受信号 关闭服务 go func() { <-stop s.Shutdown(context.Background()) }() return s.ListenAndServe() } //线上debug服务 func ServerDebug(stop <- chan struct{}) error { return Server("127.0.0.1:8001", nil, stop) } //app服务 func ServerApp(stop <- chan struct{}) error { return Server("127.0.0.1:8002", nil, stop) } func main() { done := make(chan error, 2) stop := make(chan struct{}) //启动debug服务 go func() { done <- ServerDebug(stop) }() //启动app服务 go func() { done <- ServerApp(stop) }() var stopped bool for i:=0; i<cap(done); i++ { if err := <- done; err != nil { fmt.Println("error is ", err) } //只要有一个错误,就发stop信号 把两个服务端口都停掉 if !stopped { stopped = true close(stop) } } }
实现方式包括:
- 创建done接受服务异常的信号
- 创建stop发送服务关闭的信号
- 把goroutine的调度交给caller
第一步就是接受这两个端口服务任意一个因为错误返回的err,收集到错误之后启动第二步就是发送关闭信号,这样两个端口服务都会收到这个信号而shutdown退出,最后一步就是把创建goroutine的权利交给调度者。
这样的写法可以很好的控制goroutine退出和服务关闭。
4. 不好的实现方式1
package main import ( "net/http" ) func Server(addr string, handler http.Handler) error { s := http.Server{ Addr: addr, Handler: handler, } return s.ListenAndServe() } //线上debug服务 func ServerDebug() error { return Server("127.0.0.1:8001", nil) } //app服务 func ServerApp() error { return Server("127.0.0.1:8002", nil) } func main() { //启动debug服务 go func() { ServerDebug() }() //启动app服务 go func() { ServerApp() }() select {} }
直接用select{}来阻塞处理,这样的缺点就是当某个端口服务挂掉的时候你根本不知道什么原因挂掉,而且理论上某个端口挂了,应该整个程序都关掉才对,不然这样的写法一点意义都没有。
那你可能说了加一个log.Fatal,比如:
//启动debug服务 go func() { err := ServerDebug() if err != nil { log.Fatal(err) } }()
这样可以退出程序,但是万一我们main中还需要处理defer呢,log.fatal是调用底层exit的,不走defer的,所以这种建议不可取。
5. 不好的实现方式2
package main import ( "net/http" ) func Server(addr string, handler http.Handler) error { s := http.Server{ Addr: addr, Handler: handler, } return s.ListenAndServe() } //线上debug服务 func ServerDebug() { go Server("127.0.0.1:8001", nil) } //app服务 func ServerApp() { go Server("127.0.0.1:8002", nil) } func main() { //启动debug服务 ServerDebug() //启动app服务 ServerApp() select {} }
这种写法也不推荐,第一个原因就是对于调用者来说,ServerDebug
和ServerApp
是同步调用的,一眼看不到有goroutine,所以对调度者不太友好。第二个原因就是它俩不再受main goroutine的控制了,大家注意这种情况是最可怕的,一定要避免。第三个原因就是底层Server函数的err没办法在main中收集,导致调度者没办法做最后的收尾工作。
6. 小结
双端口服务比较常见,尤其线上排查问题的时候比较常用,所以建议大家亲自试试,以备不时之需。今天就先到这里了,下期我们再见。
7. 关注公众号
微信公众号:堆栈future