Skip to content

Commit a57597e

Browse files
committed
feat: http server grace
1 parent 9becc17 commit a57597e

File tree

1 file changed

+192
-0
lines changed

1 file changed

+192
-0
lines changed

golang_http_clinet_graceful.md

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# 源码分析golang http shutdown优雅退出的原理
2+
3+
我们知道在go 1.8.x后,golang在http里加入了shutdown方法,用来控制优雅退出。什么是优雅退出? 简单说就是不处理新请求,但是会处理正在进行的请求,把旧请求都处理完,也就是都response之后,那么就退出。
4+
5+
社区里不少http graceful动态重启,平滑重启的库,大多是基于http.shutdown做的。平滑启动的原理很简单,fork子进程,继承listen fd, 老进程优雅退出。以前写过文章专门讲述在golang里如何实现平滑重启 (graceful reload)。有兴趣的朋友可以翻翻。
6+
7+
## http shutdown 源码分析
8+
9+
先来看下 http shutdown 的主方法实现逻辑。用atomic来做退出标记的状态,然后关闭各种的资源,然后一直阻塞的等待无空闲连接,每 500ms 轮询一次。
10+
11+
```go
12+
// xiaorui.cc
13+
var shutdownPollInterval = 500 * time.Millisecond
14+
15+
func (srv *Server) Shutdown(ctx context.Context) error {
16+
// 标记退出的状态
17+
atomic.StoreInt32(&srv.inShutdown, 1)
18+
srv.mu.Lock()
19+
// 关闭listen fd,新连接无法建立。
20+
lnerr := srv.closeListenersLocked()
21+
22+
// 把server.go的done chan给close掉,通知等待的worekr退出
23+
srv.closeDoneChanLocked()
24+
25+
// 执行回调方法,我们可以注册shutdown的回调方法
26+
for _, f := range srv.onShutdown {
27+
go f()
28+
}
29+
30+
// 每500ms来检查下,是否没有空闲的连接了,或者监听上游传递的ctx上下文。
31+
ticker := time.NewTicker(shutdownPollInterval)
32+
defer ticker.Stop()
33+
for {
34+
if srv.closeIdleConns() {
35+
return lnerr
36+
}
37+
select {
38+
case <-ctx.Done():
39+
return ctx.Err()
40+
case <-ticker.C:
41+
}
42+
}
43+
}
44+
```
45+
46+
是否没有空闲的连接
47+
遍历连接,当客户单的连接已空闲,则关闭连接,并在 activeConn 连接池中剔除该连接。
48+
49+
```go
50+
func (s *Server) closeIdleConns() bool {
51+
s.mu.Lock()
52+
defer s.mu.Unlock()
53+
quiescent := true
54+
for c := range s.activeConn {
55+
st, unixSec := c.getState()
56+
if st == StateNew && unixSec < time.Now().Unix()-5 {
57+
st = StateIdle
58+
}
59+
if st != StateIdle || unixSec == 0 {
60+
quiescent = false
61+
continue
62+
}
63+
c.rwc.Close()
64+
delete(s.activeConn, c)
65+
}
66+
return quiescent
67+
}
68+
```
69+
70+
关闭server.doneChan和监听的文件描述符
71+
72+
```go
73+
// xiaorui.cc
74+
// 关闭doen chan
75+
func (s *Server) closeDoneChanLocked() {
76+
ch := s.getDoneChanLocked()
77+
select {
78+
case <-ch:
79+
// Already closed. Don't close again.
80+
default:
81+
// Safe to close here. We're the only closer, guarded
82+
// by s.mu.
83+
close(ch)
84+
}
85+
}
86+
87+
// 关闭监听的fd
88+
func (s *Server) closeListenersLocked() error {
89+
var err error
90+
for ln := range s.listeners {
91+
if cerr := (*ln).Close(); cerr != nil && err == nil {
92+
err = cerr
93+
}
94+
delete(s.listeners, ln)
95+
}
96+
return err
97+
}
98+
99+
// 关闭连接
100+
func (c *conn) Close() error {
101+
if !c.ok() {
102+
return syscall.EINVAL
103+
}
104+
err := c.fd.Close()
105+
if err != nil {
106+
err = &OpError{Op: "close", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
107+
}
108+
return err
109+
}
110+
```
111+
112+
这么一系列的操作后,server.go的serv主监听方法也就退出了。
113+
114+
```go
115+
// xiaorui.cc
116+
func (srv *Server) Serve(l net.Listener) error {
117+
...
118+
for {
119+
rw, e := l.Accept()
120+
if e != nil {
121+
select {
122+
// 退出
123+
case <-srv.getDoneChan():
124+
return ErrServerClosed
125+
default:
126+
}
127+
...
128+
return e
129+
}
130+
tempDelay = 0
131+
c := srv.newConn(rw)
132+
c.setState(c.rwc, StateNew) // before Serve can return
133+
go c.serve(ctx)
134+
}
135+
}
136+
```
137+
138+
那么如何保证用户在请求完成后,再关闭连接的?
139+
140+
```go
141+
// xiaorui.cc
142+
143+
func (s *Server) doKeepAlives() bool {
144+
return atomic.LoadInt32(&s.disableKeepAlives) == 0 && !s.shuttingDown()
145+
}
146+
147+
148+
// Serve a new connection.
149+
func (c *conn) serve(ctx context.Context) {
150+
defer func() {
151+
... xiaorui.cc ...
152+
if !c.hijacked() {
153+
// 关闭连接,并且标记退出
154+
c.close()
155+
c.setState(c.rwc, StateClosed)
156+
}
157+
}()
158+
...
159+
ctx, cancelCtx := context.WithCancel(ctx)
160+
c.cancelCtx = cancelCtx
161+
defer cancelCtx()
162+
163+
c.r = &connReader{conn: c}
164+
c.bufr = newBufioReader(c.r)
165+
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
166+
167+
for {
168+
// 接收请求
169+
w, err := c.readRequest(ctx)
170+
if c.r.remain != c.server.initialReadLimitSize() {
171+
c.setState(c.rwc, StateActive)
172+
}
173+
...
174+
...
175+
// 匹配路由及回调处理方法
176+
serverHandler{c.server}.ServeHTTP(w, w.req)
177+
w.cancelCtx()
178+
if c.hijacked() {
179+
return
180+
}
181+
...
182+
// 判断是否在shutdown mode, 选择退出
183+
if !w.conn.server.doKeepAlives() {
184+
return
185+
}
186+
}
187+
...
188+
```
189+
190+
## 总结:
191+
192+
总觉得golang net/http的代码写得有点乱,应该能写得更好。我也看过不少golang标准库的源代码,最让我头疼的就是net/http了。😅

0 commit comments

Comments
 (0)