linux 优雅重启进程,Go程序的优雅重启机制

在不停机的情况下,替换二进制文件或修改配置

两种可行的方案:

方案一:

在套接字上设置 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)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值