平滑升级, 连接平滑迁移

在客户端连接不断开的情况下升级系统

client.go

package main

import (
	"fmt"
	"net"
	"time"
)

// Example command: go run client.go
func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:9000")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	for {
		conn.Write([]byte("hello"))
		time.Sleep(time.Millisecond * 100)
		var data = make([]byte, 1024)
		conn.Read(data)
		fmt.Println(string(data))
	}
}

server.go

package main

import (
	"fmt"
	"github.com/pubgo/xerror"
	"net"
	"os"
	"os/signal"
	"syscall"
	"time"
)

var forkExec = func(argv0 string, argv []string, files ...uintptr) (pid int, err error) {
	lll := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}
	lll = append(lll, files...)
	fmt.Println(lll)
	return syscall.ForkExec(argv0, argv, &syscall.ProcAttr{
		Env:   os.Environ(),
		Files: lll,
		Sys: &syscall.SysProcAttr{
			Setsid: true,
		},
	})
}

// Example command: go run client.go
func main() {
	var connList []uintptr
	isUpgrade := os.Getenv("fork") != ""
	var err error
	var ln net.Listener
	if isUpgrade {
		ln, err = net.FileListener(os.NewFile(3, ""))
		if err != nil {
			panic(err)
		}
	} else {
		ln, _ = net.Listen("tcp", "localhost:9000")
	}

	if isUpgrade {
		conn, err := net.FileConn(os.NewFile(4, ""))
		xerror.Panic(err)

		go func() {
			for {
				conn.Write([]byte("hello" + os.Getenv("fork")))
				time.Sleep(time.Millisecond * 500)
				var data = make([]byte, 1024)
				conn.Read(data)
				fmt.Println(string(data))
			}
		}()
	}

	//defer ln.Close()

	rawConn, ok := ln.(syscall.Conn)
	if !ok {
		panic("not raw")
	}

	raw, err := rawConn.SyscallConn()
	xerror.Panic(err)

	var dupfd uintptr
	xerror.Panic(raw.Control(func(fd uintptr) {
		dupfd, err = dupFd(fd)
		if err != nil {
			panic(err)
		}
		xerror.Panic(err)
	}))

	connList = append(connList, dupfd)

	go func() {
		for i := 0; ; i++ {
			conn, err := ln.Accept()
			if err != nil {
				break
			}

			rawConn, ok := conn.(syscall.Conn)
			if !ok {
				panic("not raw")
			}

			raw, err := rawConn.SyscallConn()
			xerror.Panic(err)

			var dupfd uintptr
			xerror.Panic(raw.Control(func(fd uintptr) {
				dupfd, err = dupFd(fd)
				xerror.Panic(err)
			}))

			connList = append(connList, dupfd)

			go func() {
				for {
					conn.Write([]byte("hello"))
					time.Sleep(time.Millisecond * 500)
					var data = make([]byte, 1024)
					conn.Read(data)
					fmt.Println(string(data))
				}
			}()
		}
	}()

	ch := make(chan os.Signal, 1)
	signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGHUP)
	<-ch

	os.Setenv("fork", "true")

	pid, err := forkExec(os.Args[0], os.Args, connList...)
	xerror.Panic(err)

	go func() {
		// 防止子进程变成僵尸进程
		for {
			_, _ = syscall.Wait4(pid, nil, syscall.WNOWAIT, nil)
			time.Sleep(time.Second * 5)
			return
		}
	}()

	time.Sleep(time.Second * 5)
}

func dupFd(fd uintptr) (uintptr, error) {
	dupfd, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_DUPFD_CLOEXEC, 0)
	if errno != 0 {
		return 0, fmt.Errorf("can't dup fd using fcntl: %s", errno)
	}
	return dupfd, nil
}
  1. 获取Listener, Conn的fd
  2. 通过fork传递给子进程, 然后退出
  3. 子进程判断, 然后获取fd地址, 还原Listener和Conn

Go使用unixSocket实现进程间传递文件描述符

  1. 感谢 chrispink_yang老哥,讲解的很详细
  2. 这种方法也可以实现平滑重启

go优雅升级/重启工具调研

  1. https://www.yuque.com/kshare/2020/180566c6-9625-497d-8f1f-636351e4c98c
  2. 介绍了其他的方法探索
posted @ 2020-12-22 20:58  白云辉  阅读(299)  评论(0编辑  收藏  举报