应用程序部署上线正式运行后,都会遇到程序升级维护的问题,一般情况下都直接kill掉运行中的应用程序,然后更新程序,启动程序。这样进行操作会遇到一个问题就是,在kill掉程序的时候,不能保证当前程序是没有进行业务处理的,如果存在请求,当前的业务会失败,如果是获取数据,不会产生太大的问题,如果是其它情况,有可能会产生错误数据或脏数据,如何优雅的解决程序升级但是又不影响正在运行的业务的运行(如何进行热升级)?
进行程序的热升级的必要条件:
- 应用程序的更新
- 正在运行的程序停止数据的接入(停止端口监听),处理完接入的请求后自动退出
- 启动更新后的应用程序,并进行数据接入。
如何监听程序的退出操作
程序的启动和退出,操作系统都会发出相对应的信号,程序可以监听对应的信号来进行处理。golang中,可以通过signal包中的Notify进行信号的监听。
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL)
select {
case sg := <-signals:
fmt.Println(sg)
}
}
需要注意的是,不同的操作系统对信号的处理方式是不一样的。golang的信号主要是对类unix的操作系统。对于window系统,control+c会触发os.Interrupt,对于信号的支持可以参考godoc。
如何统计判断当前正在进行的业务数
golang中的http server中有一个connstate的函数变量,可以用于监听请求的链接状态变化。
package main
import (
"fmt"
"net"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(resp http.ResponseWriter, req *http.Request) {
resp.Write([]byte("hello"))
})
s := http.Server{
Addr: "0.0.0.0:9090",
ConnState: func(c net.Conn, s http.ConnState) {
fmt.Println(c.RemoteAddr(), s.String())
},
}
s.ListenAndServe()
}
可以在ConnState中通过监听state的变化来计数接入数判断当前正在处理中的业务数。需要注意的是,http的复用,有可能多次请求,只有一个new的state
如何优雅的退出程序
- 接收操作系统发送的SIGINT信号,阻塞退出
- 监听请求链接数变化,链接数为0且收到sigint信号出发真正的退出信号
package main
import (
"fmt"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
http.HandleFunc("/hello", func(resp http.ResponseWriter, req *http.Request) {
time.Sleep(time.Second * 20)
resp.Write([]byte("hello"))
})
exit := make(chan int, 1) //用于mark是否真的退出
iscalllexit := false //用于mark是否接收到exit signal
count := 0 //用于mark 正在处理请求数
s := http.Server{
Addr: "0.0.0.0:9090",
ConnState: func(c net.Conn, s http.ConnState) {
fmt.Println(count, s)
switch s { //需要注意的是并发的问题,只是用来演示原理
case http.StateActive:
count++
case http.StateIdle:
count--
}
if count == 0 && iscalllexit { //链接数为0且受到exit信号
exit <- 1
}
},
}
ln, err := net.Listen("tcp", s.Addr)
if err != nil {
fmt.Println(err)
}
go s.Serve(ln)
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL)
select {
case <-signals:
ln.Close()
iscalllexit = true
fmt.Println("exit")
if i := <-exit; i != 0 {
return
}
}
fmt.Println("exit success")
}
如何进行热升级
使用os.StartProcess启动新的应用程序(由于原来的应用程序的端口已经释放,新的程序可以重新进行监听)