在不停机的情况下,替换二进制文件或修改配置
两种可行的方案:
方案一:
在套接字上设置 SO_REUSEPORT 从而让多个进程能被绑定到同一个端口上,此时有多个接受队列向多个进程提供数据
现状:由于有多个接受队列,偶有丢弃挂起的TCP连接;Go对设置该属性支持不够好,需要借助第三方包,如
https://github.com/libp2p/go-reuseport
方案二:
复制套接字,并将其以文件的形式传送给一个子进程,然后在新的进程中重新创建这个套接字,此时有一个接受队列向多个进程提供数据
os/exec实际不赞同这种用法,出于安全考虑,只传递stdin、stdout、stderr给子进程
但os包确实提供较低级原语,用于将文件传递给子进程
使用SIGUSR2信号,当进程接收到该信号后,复制监听套接字,然后创建一个新的进程,同时将监听套接字以文件形式和这个套接字的元数据以环境变量形式传入子进程,子进程开始运行后,会依据传进来的文件和元数据重建套按字,并开始处理流量
当一个套接字被复制时,入栈流量会在两个套接字之间以轮询方式进行负载。即在替换的过程中,两个进程都会接受新的连接
父进程接受到SIGQUIT信号后,开始关闭进程,停止接受新的连接,待所有现有连接断开或超时,然后会关闭监听套接字并退出
下面为示例代码(对原文代码进行了注解和修整)
Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
packagemain
import(
"context"
"encoding/json"
"flag"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
)
typelistenerstruct{
Addrstring`json:"addr"`// 监听地址 带端口
FDint`json:"fd"`// 文件描述符
Filenamestring`json:"filename"`// 文件名
}
// 从进程运行的环境变量中导入listener 变量名称LISTENER
funcimportListener(addrstring)(net.Listener,error){
// 从环境变量中获取被编码的listener元数据 json格式
listenerEnv:=os.Getenv("LISTENER")
iflistenerEnv==""{
returnnil,fmt.Errorf("找不到环境变量LISTENER")
}
// fmt.Println(string(listenerEnv)) // 打印变量
// 解码listener元数据
varllistener
err:=json.Unmarshal([]byte(listenerEnv),&l)
iferr!=nil{
returnnil,err
}
ifl.Addr!=addr{
returnnil,fmt.Errorf("找不到 %v 的listener",addr)
}
// 文件已经被传入到这个进程中
// 从元数据中抽离文件描述符和名字
// 为listener 重建/发现 *os.file
listenerFile:=os.NewFile(uintptr(l.FD),l.Filename)
iflistenerFile==nil{
returnnil,fmt.Errorf("创建listener文件失败")
}
deferlistenerFile.Close()
// 创建net.Listener
ln,err:=net.FileListener(listenerFile)
iferr!=nil{
returnnil,err
}
returnln,nil
}
// 创建listener
funccreateListener(addrstring)(net.Listener,error){
ln,err:=net.Listen("tcp",addr)
iferr!=nil{
returnnil,err
}
returnln,nil
}
// 导入或创建listener
funccreateOrImportListener(addrstring)(net.Listener,error){
// 尝试导入一个listener 若导入成功 则使用
ln,err:=importListener(addr)
iferr==nil{
fmt.Printf("导入listener 描述符 %v\n",addr)
returnln,nil
}
// 没有listener被导入 则创建一个
ln,err=createListener(addr)
iferr!=nil{
returnnil,err
}
fmt.Printf("创建listener 描述符 %v\n",addr)
returnln,nil
}
// 响应内容
funchandler(whttp.ResponseWriter,r *http.Request){
fmt.Fprintf(w,"Hello from %v\n",os.Getpid())
}
// Http Server
funcstartServer(addrstring,lnnet.Listener)*http.Server{
http.HandleFunc("/hello",handler)
httpServer:=&http.Server{
Addr:addr,
}
gohttpServer.Serve(ln)
returnhttpServer
}
funcgetListenerFile(lnnet.Listener)(*os.File,error){
switcht:=ln.(type){
case*net.TCPListener:
returnt.File()
case*net.UnixListener:
returnt.File()
}
returnnil,fmt.Errorf("不支持的listener %T",ln)
}
// fork子进程
funcforkChild(addrstring,lnnet.Listener)(*os.Process,error){
// 从listener中获取文件描述符 环境变量编码再传递给该子进程作为元数据
lnFile,err:=getListenerFile(ln)
iferr!=nil{
returnnil,err
}
deferlnFile.Close()
l:=listener{
Addr:addr,
FD:3,
Filename:lnFile.Name(),
}
listenerEnv,err:=json.Marshal(l)
iferr!=nil{
returnnil,err
}
// 将stdin,stdout,stderr,listener传入子进程
// 以上四个文件描述符分别为0,1,2,3
files:=[]*os.File{
os.Stdin,
os.Stdout,
os.Stderr,
lnFile,
}
// 获取当前环境变量 并传入子进程
environment:=append(os.Environ(),"LISTENER="+string(listenerEnv))
// 获取当前进程名和工作目录
execName,err:=os.Executable()
iferr!=nil{
returnnil,err
}
execDir:=filepath.Dir(execName)
// 创建子进程
p,err:=os.StartProcess(execName,[]string{execName},&os.ProcAttr{
Dir:execDir,
Env:environment,
Files:files,
Sys:&syscall.SysProcAttr{},
})
iferr!=nil{
returnnil,err
}
returnp,nil
}
// 信号处理
funcwaitForSignals(addrstring,lnnet.Listener,server *http.Server)error{
signalCh:=make(chanos.Signal,1024)
// 没有syscall.SIGUSR2 所以使用 syscall.Signal(12)
// 注册要处理的信号
signal.Notify(signalCh,syscall.SIGHUP,syscall.Signal(12),syscall.SIGINT,syscall.SIGQUIT)
for{
select{
cases:=
fmt.Printf("接收到信号 %v\n",s)
switchs{
casesyscall.SIGHUP:
// Fork 一个子进程
p,err:=forkChild(addr,ln)
iferr!=nil{
fmt.Printf("不能fork子进程 %v\n",err)
continue
}
fmt.Printf("Forked 子进程 %v\n",p.Pid)
// 创建一个5s过期的Context 用该超时定时器关闭
ctx,cancel:=context.WithTimeout(context.Background(),5*time.Second)
defercancel()
// 返回关闭过程中发生的任何错误
returnserver.Shutdown(ctx)
casesyscall.Signal(12):
// Fork一个子进程
p,err:=forkChild(addr,ln)
iferr!=nil{
fmt.Printf("不能fork子进程 %v\n",err)
continue
}
// 输出被fork的子进程的PID 并等待更多的信号
fmt.Printf("Forked子进程 %v\n",p.Pid)
casesyscall.SIGQUIT:
fallthrough
casesyscall.SIGINT:
// 创建一个5s的Context 使用该超时定时器半闭
ctx,chancel:=context.WithTimeout(context.Background(),5*time.Second)
deferchancel()
// 返回关闭过程中发生的任何错误
returnserver.Shutdown(ctx)
}
}
}
}
funcmain(){
// 解析命令行参数
varaddrstring
flag.StringVar(&addr,"addr",":8080","设置监听端口")
flag.Parse()
// 导入/创建一个net.Listener并启动gotoutine在这个net.Listener上运行HTTP Server
ln,err:=createOrImportListener(addr)
iferr!=nil{
fmt.Printf("不能导入或创建listener: %v\n",err)
os.Exit(1)
}
server:=startServer(addr,ln)
// 信号处理
err=waitForSignals(addr,ln,server)
iferr!=nil{
fmt.Printf("Exiting... Err %v\n",err)
return
}
fmt.Printf("Exiting.\n")
}
测试方法:
./gracedown &
curl http://localhost:8080
kill -SIGUSR2 1234
curl http://localhost:8080
curl http://localhost:8080
lsof -i :8080 -P
# 在发送完SIGUSR2信号后 可以看到 两个进程监听同一个端口8080
喜欢 (1)or分享 (0)