Gong服务实现平滑重启分析(微服务平滑重启)

网友投稿 1277 2022-08-30

Gong服务实现平滑重启分析(微服务平滑重启)

Gong服务实现平滑重启分析(微服务平滑重启)

平滑重启是指能让我们的程序在重启的过程不中断服务,新老进程无缝衔接,实现零停机时间(Zero-Downtime)部署;

目前实现平滑重启的主要策略有两种:

方案一:我们的服务如果是多机器部署,可以通过网关程序,将即将重启服务的机器从网关下线,重启完成后再重新上线,该方案适合多机器部署的企业级应用;

方案二:让我们的程序实现自启动,重启子进程来实现平滑重启,核心策略是通过拷贝文件描述符实现子进程和父进程切换,适合单机器部署应用;

今天我们就主要介绍方案二,让我们的程序拥有平滑重启的功能,相关实现参考一个开源库:https://github.com/fvbock/endless

实现原理介绍

http 连接介绍:

我们知道,http 服务也是基于 tcp 连接,我们通过 golang http 包源码也能看到底层是通过监听 tcp 连接实现的;

func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed

}

addr := srv.Addr if addr == "" {

addr = ":http" }

ln, err := net.Listen("tcp", addr) if err != nil { return err

} return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})

}

复用 socket:

当程序开启 tcp 连接监听时会创建一个 socket 并返回一个文件描述符 handler 给我们的程序;

通过拷贝文件描述符文件可以使 socket 不关闭继续使用原有的端口,自然 http 连接也不会断开,启动一个相同的进程也不会出现端口被占用的问题;

通过如下代码进行测试:

package main

import ( "fmt"

"net/http"

"context"

"time"

"os"

"os/signal"

"syscall"

"net"

"flag"

"os/exec" ) var (

graceful = flag.Bool("grace", false, "graceful restart flag")

procType = "" )

func main() {

flag.Parse()

mux := http.NewServeMux()

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

fmt.Fprintln(w, fmt.Sprintf("Hello world! ===> %s", procType))

})

server := &http.Server{

Addr: ":8080", Handler: mux, } var err error var listener net.Listener if *graceful {

f := os.NewFile(3, "")

listener, err = net.FileListener(f)

procType = "fork process" } else {

listener, _ = net.Listen("tcp", server.Addr)

procType = "main process" //主程序开启5s 后 fork 子进程 go func() { time.Sleep(5*time.Second)

forkSocket(listener.(*net.TCPListener))

}()

}

err=server.Serve(listener.(*net.TCPListener))

fmt.Println(fmt.Sprintf("proc exit %v", err))

}

func forkSocket(tcpListener *net.TCPListener) error {

f, err := tcpListener.File() if err != nil { return err

}

args := []string{"-grace"}

fmt.Println(os.Args[0], args)

cmd := exec.Command(os.Args[0], args...)

cmd.Stdout = os.Stdout

cmd.Stderr = os.Stderr // put socket FD at the first entry cmd.ExtraFiles = []*os.File{f} return cmd.Start()

}

该程序启动后,等待 5s 会自动 fork 子进程,通过 ps 命令查看如图可以看到有两个进程同时共存:

然后我们可以通过浏览器访问 http://127.0.0.1/ 可以看到会随机显示主进程或子进程的输出;

写一个测试代码进行循环请求:

package main

import ( "net/http"

"io/ioutil"

"fmt"

"sync" )

func main(){

wg:=sync.WaitGroup{}

wg.Add(100) for i:=0; i<100; i++ {

go func(index int) {

result:=getUrl(fmt.Sprintf("http://127.0.0.1:8080?%d", i))

fmt.Println(fmt.Sprintf("loop:%d %s", index, result))

wg.Done()

}(i)

}

wg.Wait()

}

func getUrl(url string) string{

resp, _ := http.Get(url)

defer resp.Body.Close()

body, _ := ioutil.ReadAll(resp.Body) return string(body)

}

能看到返回的数据也是有些是主进程有些是子进程。

切换过程:

在开启新的进程和老进程退出的瞬间,会有一个短暂的瞬间是同时有两个进程使用同一个文件描述符,此时这种状态,通过http请求访问,会随机请求到新进程或老进程上,这样也没有问题,因为请求不是在新进程上就是在老进程上;当老进程结束后请求就会全部到新进程上进行处理,通过这种方式即可实现平滑重启;

综上,我们可以将核心的实现总结如下:

1.监听退出信号;

2.监听到信号后 fork 子进程,使用相同的命令启动程序,将文件描述符传递给子进程;

3.子进程启动后,父进程停止服务并处理正在执行的任务(或超时)退出;

4.此时只有一个新的进程在运行,实现平滑重启。

一个完整的 demo 代码,通过发送 USR1 信号,程序会自动创建子进程并关闭主进程,实现平滑重启:

package main

import ( "fmt"

"net/http"

"context"

"os"

"os/signal"

"syscall"

"net"

"flag"

"os/exec" ) var (

graceful = flag.Bool("grace", false, "graceful restart flag")

)

func main() {

flag.Parse()

mux := http.NewServeMux()

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

fmt.Fprintln(w, "Hello world!")

})

server := &http.Server{

Addr: ":8080", Handler: mux, } var err error var listener net.Listener if *graceful {

f := os.NewFile(3, "")

listener, err = net.FileListener(f)

} else {

listener, err = net.Listen("tcp", server.Addr)

} if err != nil{

fmt.Println(fmt.Sprintf("listener error %v", err)) return }

go listenSignal(context.Background(), server, listener)

err=server.Serve(listener.(*net.TCPListener))

fmt.Println(fmt.Sprintf("proc exit %v", err))

}

func forkSocket(tcpListener *net.TCPListener) error {

f, err := tcpListener.File() if err != nil { return err

}

args := []string{"-grace"}

fmt.Println(os.Args[0], args)

cmd := exec.Command(os.Args[0], args...)

cmd.Stdout = os.Stdout

cmd.Stderr = os.Stderr // put socket FD at the first entry cmd.ExtraFiles = []*os.File{f} return cmd.Start()

}

func listenSignal(ctx context.Context, httpSrv *http.Server, listener net.Listener) {

sigs := make(chan os.Signal, 1)

signal.Notify(sigs, syscall.USR1)

select { case <-sigs: forkSocket(listener.(*net.TCPListener))

httpSrv.Shutdown(ctx)

fmt.Println("http shutdown")

}

}

使用 apache 的 ab 压测工具进行验证一下,执行 ab -c 50 -t 20 http://127.0.0.1:8080/ 持续 50 的并发 20s,在压测的期间向程序运行的pid发送 USR1 信号,可以看到压测结果,没有失败的请求,由此可知,该方案实现平滑重启是木有问题的。

最后给大家安利一个 Web 开发框架,该框架已经将平滑重启进行的封装,开箱即用,快速构建一个带平滑重启的 Web 服务。

框架源码:https://gitee.com/zhucheer/orange

文档:https://kancloud-/chase688/orange_framework/1448035

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:23-Spring Authorization Server结合客户端
下一篇:区块链的诞生是为了解决——“去中心化的协同”这个问题
相关文章

 发表评论

暂时没有评论,来抢沙发吧~