Пример плавного перезапуска веб-сервера — поясняющая версия

Go

Ссылка в этой статьеGRACEFULLY RESTARTING A GOLANG WEB SERVERОбобщить и объяснить. Вы также можете начатьздесьПолучите версию кода с добавленными комментариями. Я сделал подраздел, чтобы вы могли понять.

вопрос

Поскольку golang скомпилирован, когда мы изменяем конфигурацию службы, написанной на go, нам нужно перезапустить службу, а некоторые даже нужно перекомпилировать и опубликовать. Если в процессе перезапуска возникает большой поток запросов, все, что вы можете сделать, это отклонить или заблокировать запросы. В любом случае это не элегантно~, поэтому slax0r и его команда попытались найти более плавный и удобный способ перезапуска.

В дополнение к красивому оформлению в оригинальной статье текстового содержания и описаний все еще относительно мало, поэтому я надеюсь добавить некоторые пояснения самостоятельно.

принцип

Корень вышеупомянутой проблемы заключается в том, что у нас не может быть двух служб, прослушивающих один и тот же порт одновременно. Решение состоит в том, чтобы скопировать текущий файл прослушивания и передать параметры и переменные среды напрямую между старым и новым процессами через сокет. Новый включается, старый выключается, все просто.

Инструкции против понимания

Unix domain socket

все это файл

играть первым

Запускаем программу, в процессе открываем новую консоль, вводимkill -1 [进程号], вы можете увидеть изящный процесс перезапуска.

идеи кода

func main() {
    主函数,初始化配置
    调用serve()
}

func serve() {
    核心运行函数
    getListener()   // 1. 获取监听 listener
    start()         // 2. 用获取到的 listener 开启 server 服务
    waitForSignal() // 3. 监听外部信号,用来控制程序 fork 还是 shutdown
}

func getListener() {
    获取正在监听的端口对象
    (第一次运行新建)
}

func start() {
    运行 http server
}

func waitForSignal() {
    for {
        等待外部信号
        1. fork子进程
        2. 关闭进程
    }
}

Выше приведено описание идеи кода, по сути, мы наполняем и улучшаем код вокруг этой схемы.

определить структуру

Мы абстрагируем две структуры для описания общих структур данных в программе.

var cfg *srvCfg
type listener struct {
	// Listener address
	Addr string `json:"addr"`
	// Listener file descriptor
	FD int `json:"fd"`
	// Listener file name
	Filename string `json:"filename"`
}

type srvCfg struct {
	sockFile string
	addr string
	ln net.Listener
	shutDownTimeout time.Duration
	childTimeout time.Duration
}

listener — это наш прослушиватель, который содержит адрес прослушивания, дескриптор файла и имя файла. Дескриптор файла на самом деле является индексом файла, который необходимо открыть процессу, неотрицательным целым числом. Когда мы обычно создаем процесс, linux по умолчанию открывает три файла: стандартный ввод stdin, стандартный вывод stdout, стандартная ошибка stderr, Каждый из этих трех файлов занимает три файловых дескриптора 0, 1 и 2. Поэтому, если ваш процесс хочет открыть файл позже, вы должны начать с 3. Этот слушатель — это данные, которые будут передаваться между нашими процессами.

srvCfgЯвляется ли наша глобальная конфигурация среды, включая путь к файлу сокета, адрес прослушивания службы, объект прослушивателя, тайм-аут родительского процесса, тайм-аут дочернего процесса. Поскольку это данные конфигурации для глобального использования, давайте сначала изменим их.

Вход

Посмотрите, как выглядит наш основной

func main() {
	serve(srvCfg{
		sockFile: "/tmp/api.sock",
		addr:     ":8000",
		shutDownTimeout: 5*time.Second,
		childTimeout: 5*time.Second,
	}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(`Hello, world!`))
	}))
}

func serve(config srvCfg, handler http.Handler) {
	cfg = &config
	var err error
	// get tcp listener
	cfg.ln, err = getListener()
	if err != nil {
		panic(err)
	}

	// return an http Server
	srv := start(handler)

	// create a wait routine
	err = waitForSignals(srv)
	if err != nil {
		panic(err)
	}
}

Очень просто, мы подготовили конфигурацию, а затем зарегистрировали обработчик — вывод Hello, world!

serveСодержание функции такое же, как и наша предыдущая идея, но есть больше недоразумений.

Далее, давайте рассмотрим функции внутри одну за другой...

получить слушателя

Это наша функция getListener().

func getListener() (net.Listener, error) {
    // 第一次执行不会 importListener
	ln, err := importListener()
	if err == nil {
		fmt.Printf("imported listener file descriptor for addr: %s\n", cfg.addr)
		return ln, nil
	}
    // 第一次执行会 createListener
	ln, err = createListener()
	if err != nil {
		return nil, err
	}

	return ln, err
}

func importListener() (net.Listener, error) {
    ...
}

func createListener() (net.Listener, error) {
	fmt.Println("首次创建 listener", cfg.addr)
	ln, err := net.Listen("tcp", cfg.addr)
	if err != nil {
		return nil, err
	}

	return ln, err
}

потому что с первого раза не получитсяimportListener, так что нам пока не нужно знатьimportListenerКак это достигается. только Сяо понимаетcreateListenerВозвращается объект слушателя.

Тогда это нашеstartфункция

func start(handler http.Handler) *http.Server {
	srv := &http.Server{
		Addr: cfg.addr,
		Handler: handler,
	}
	// start to serve
	go srv.Serve(cfg.ln)
	fmt.Println("server 启动完成,配置信息为:",cfg.ln)
	return srv
}

Очевидно, start проходит в обработчике, и сопрограмма запускает http-сервер.

сигнал монитора

Входом в главное событие в нашей статье должен быть сигнал мониторинга.Для начала посмотрим на код:

func waitForSignals(srv *http.Server) error {
	sig := make(chan os.Signal, 1024)
	signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)
	for {
		select {
		case s := <-sig:
			switch s {
			case syscall.SIGHUP:
				err := handleHangup() // 关闭
				if err == nil {
					// no error occured - child spawned and started
					return shutdown(srv)
				}
			case syscall.SIGTERM, syscall.SIGINT:
				return shutdown(srv)
			}
		}
	}
}

Сначала устанавливается канал, который используется для приема команд, отправляемых системой программе, таких какkill -9 myprog, это9передается в канал. Мы используем Notify для ограничения сигналов, которые будут генерировать ответ, вот:

Если вы действительно не можете понять разницу между этими тремя сигналами, просто поймите, что, различая сигналы, процесс остается наедине с самим собой.

Затем мы начали мониторинг шлейфа, очевидно, мониторинг системного сигнала. когда сигналsyscall.SIGHUP, мы собираемся перезапустить процесс. И когда сигналsyscall.SIGTERM, syscall.SIGINT, мы закрываем процесс напрямую.

Итак, давайте посмотрим,handleHangupЧто происходит внутри.

Разговор между отцом и сыном

Изящный перезапуск между процессами можно рассматривать как приятный диалог отца и сына, Папа открыл сыну горячую линию, папа сообщил сыну информацию о порте, которая в данный момент отслеживается через горячую линию. После того, как сын получил необходимую информацию, он унаследовал бизнес отца, запустил новый пустой процесс и сообщил отцу, что его отец официально вышел на пенсию.

func handleHangup() error {
	c := make(chan string)
	defer close(c)
	errChn := make(chan error)
	defer close(errChn)
    // 开启一个热线通道
	go socketListener(c, errChn)

	for {
		select {
		case cmd := <-c:
			switch cmd {
			case "socket_opened":
				p, err := fork()
				if err != nil {
					fmt.Printf("unable to fork: %v\n", err)
					continue
				}
				fmt.Printf("forked (PID: %d), waiting for spinup", p.Pid)

			case "listener_sent":
				fmt.Println("listener sent - shutting down")

				return nil
			}

		case err := <-errChn:
			return err
		}
	}

	return nil
}

socketListener открывает новый канал сокета unix, одновременно отслеживает состояние канала и обрабатывает его соответствующим образом. Грубо говоря, есть только два случая:

  1. Канал открыт, что указывает на то, что я могу создать сына (вилку), и сын подхватит информацию отца.
  2. Папа передал сыну файлы объектов мониторинга, папа выполнил задание

handleHangupВнутри много вещей, не паникуйте, давайте рассмотрим их по порядку. Первый взглядsocketListener:

func socketListener(chn chan<- string, errChn chan<- error) {
	// 创建 socket 服务端
	fmt.Println("创建新的socket通道")
	ln, err := net.Listen("unix", cfg.sockFile)
	if err != nil {
		errChn <- err
		return
	}
	defer ln.Close()

	// signal that we created a socket
	fmt.Println("通道已经打开,可以 fork 了")
	chn <- "socket_opened"

	// accept
	// 阻塞等待子进程连接进来
	c, err := acceptConn(ln)
	if err != nil {
		errChn <- err
		return
	}

	// read from the socket
	buf := make([]byte, 512)
	nr, err := c.Read(buf)
	if err != nil {
		errChn <- err
		return
	}

	data := buf[0:nr]
	fmt.Println("获得消息子进程消息", string(data))
	switch string(data) {
	case "get_listener":
		fmt.Println("子进程请求 listener 信息,开始传送给他吧~")
		err := sendListener(c) // 发送文件描述到新的子进程,用来 import Listener
		if err != nil {
			errChn <- err
			return
		}
		// 传送完毕
		fmt.Println("listener 信息传送完毕")
		chn <- "listener_sent"
	}
}

sockectListenerСоздал канал сокета unix и отправил его первым после созданияsocket_openedэта информация. В этот моментhandleHangupвнутреннийcase "socket_opened"Будет реакция. в то же время,socketListenerБлокирует сигнал, ожидая приема новой программы, тем самым отправляя исходныйlistenerинформация о файле. Я не дам вам знать, пока оно не будет отправленоhandlerHangup listener_sent.

Ниже приведен код acceptConn, здесь нет сложной логики, просто дождитесь запроса подпрограммы, обработайте тайм-аут и ошибку.

func acceptConn(l net.Listener) (c net.Conn, err error) {
	chn := make(chan error)
	go func() {
		defer close(chn)
		fmt.Printf("accept 新连接%+v\n", l)
		c, err = l.Accept()
		if err != nil {
			chn <- err
		}
	}()

	select {
	case err = <-chn:
		if err != nil {
			fmt.Printf("error occurred when accepting socket connection: %v\n",
				err)
		}

	case <-time.After(cfg.childTimeout):
		fmt.Println("timeout occurred waiting for connection from child")
	}

	return
}

Помните структуру слушателя, которую мы определили ранее? Вот где это пригодится:

func sendListener(c net.Conn) error {
	fmt.Printf("发送老的 listener 文件 %+v\n", cfg.ln)
	lnFile, err := getListenerFile(cfg.ln)
	if err != nil {
		return err
	}
	defer lnFile.Close()

	l := listener{
		Addr:     cfg.addr,
		FD:       3, // 文件描述符,进程初始化描述符为0 stdin 1 stdout 2 stderr,所以我们从3开始
		Filename: lnFile.Name(),
	}

	lnEnv, err := json.Marshal(l)
	if err != nil {
		return err
	}
	fmt.Printf("将 %+v\n 写入连接\n", string(lnEnv))
	_, err = c.Write(lnEnv)
	if err != nil {
		return err
	}

	return nil
}

func getListenerFile(ln net.Listener) (*os.File, error) {
	switch t := ln.(type) {
	case *net.TCPListener:
		return t.File()
	case *net.UnixListener:
		return t.File()
	}

	return nil, fmt.Errorf("unsupported listener: %T", ln)
}

sendListenerСначала сделайте копию файла прослушивания tcp, который мы используем (все является файлом), и заполните его необходимой информацией.listenerВ структуре после сериализации он передается новому дочернему процессу через unix socket.

Сказав так много, это код отцовского процесса В середине мы пропустили создание дочернего процесса. Тогда давайте посмотримfork, также является изюминкой:

func fork() (*os.Process, error) {
	// 拿到原监听文件描述符并打包到元数据中
	lnFile, err := getListenerFile(cfg.ln)
	fmt.Printf("拿到监听文件 %+v\n,开始创建新进程\n", lnFile.Name())
	if err != nil {
		return nil, err
	}
	defer lnFile.Close()

	// 创建子进程时必须要塞的几个文件
	files := []*os.File{
		os.Stdin,
		os.Stdout,
		os.Stderr,
		lnFile,
	}

	// 拿到新进程的程序名,因为我们是重启,所以就是当前运行的程序名字
	execName, err := os.Executable()
	if err != nil {
		return nil, err
	}
	execDir := filepath.Dir(execName)

	// 生孩子了
	p, err := os.StartProcess(execName, []string{execName}, &os.ProcAttr{
		Dir:   execDir,
		Files: files,
		Sys:   &syscall.SysProcAttr{},
	})
	fmt.Println("创建子进程成功")
	if err != nil {
		return nil, err
	}
	// 这里返回 nil 后就会直接 shutdown 爸爸进程
	return p, nil
}

при исполненииStartProcessВ тот момент, когда вы понимаете, что выполнение дочернего процесса вернется к тому, с чего оно началось, то есть к main. В это время мыполучить слушателясерединаimportListenerметод будет активирован:

func importListener() (net.Listener, error) {
	// 向已经准备好的 unix socket 建立连接,这个是爸爸进程在之前就建立好的
	c, err := net.Dial("unix", cfg.sockFile)
	if err != nil {
		fmt.Println("no unix socket now")
		return nil, err
	}
	defer c.Close()
	fmt.Println("准备导入原 listener 文件...")
	var lnEnv string
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func(r io.Reader) {
		defer wg.Done()
		// 读取 conn 中的内容
		buf := make([]byte, 1024)
		n, err := r.Read(buf[:])
		if err != nil {
			return
		}

		lnEnv = string(buf[0:n])
	}(c)
	// 写入 get_listener
	fmt.Println("告诉爸爸我要 'get-listener' 了")
	_, err = c.Write([]byte("get_listener"))
	if err != nil {
		return nil, err
	}

	wg.Wait() // 等待爸爸传给我们参数

	if lnEnv == "" {
		return nil, fmt.Errorf("Listener info not received from socket")
	}

	var l listener
	err = json.Unmarshal([]byte(lnEnv), &l)
	if err != nil {
		return nil, err
	}
	if l.Addr != cfg.addr {
		return nil, fmt.Errorf("unable to find listener for %v", cfg.addr)
	}

	// the file has already been passed to this process, extract the file
	// descriptor and name from the metadata to rebuild/find the *os.File for
	// the listener.
	// 我们已经拿到了监听文件的信息,我们准备自己创建一份新的文件并使用
	lnFile := os.NewFile(uintptr(l.FD), l.Filename)
	fmt.Println("新文件名:", l.Filename)
	if lnFile == nil {
		return nil, fmt.Errorf("unable to create listener file: %v", l.Filename)
	}
	defer lnFile.Close()

	// create a listerer with the *os.File
	ln, err := net.FileListener(lnFile)
	if err != nil {
		return nil, err
	}

	return ln, nil
}

Время выполнения importListener здесь после того, как родительский процесс создаст новый канал сокета unix.

至此,子进程开始了新的一轮监听,服务...

конец

Хотя объем кода невелик, он хорошо передает идею элегантного перезапуска, а некоторые места еще нужно потренировать, чтобы понять (для новичка вроде меня). На самом деле в интернете есть много других элегантных способов перезагрузки, можете погуглить. Надеюсь, мое простое объяснение выше поможет вам, если есть какие-то ошибки, пожалуйста, вовремя указывайте на них, и я их исправлю.

Вы также можете начатьздесьПолучите версию кода с добавленными комментариями. Я сделал подраздел, чтобы вы могли понять.