Теплый перезапуск сервера golang (изящный перезапуск http-сервера golang)

Go

Код на стороне сервера часто требует обновления.Обычной практикой онлайн-обновления системы является использование внешней балансировки нагрузки (например, nginx), чтобы убедиться, что во время обновления доступна по крайней мере одна служба, и последовательное обновление (оттенки серого). ).
Другой более удобный способ — выполнить горячий перезапуск приложения и напрямую обновить приложение, не останавливая службу.

принцип

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

  1. Сигнал монитора (USR2)
  2. Когда сигнал получен, разветвите дочерний процесс (используя ту же команду запуска), передав дескриптор файла сокета, который служба прослушивает дочернему процессу.
  3. Дочерний процесс слушает сокет родительского процесса, в это время и родительский, и дочерний процессы могут получать запросы
  4. После успешного запуска дочернего процесса родительский процесс перестает получать новые подключения и ожидает обработки старого подключения (или истечения времени ожидания).
  5. Родительский процесс завершается, и обновление завершено.

деталь

  • Родительский процесс передает дескриптор файла сокета дочернему процессу через командную строку, переменные среды и т. д.
  • Дочерний процесс запускается с той же командной строки, что и родительский процесс, и для golang перезаписывает старую программу более новой исполняемой программой.
  • Метод изящного завершения работы server.Shutdown() — это новая функция go1.8.
  • Метод server.Serve(l) немедленно возвращает значение при завершении работы, а метод Shutdown блокируется до завершения контекста, поэтому метод Shutdown должен быть написан в основной горутине.

код

package main

import (
    "context"
    "errors"
    "flag"
    "log"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
    "time"
)

var (
    server   *http.Server
    listener net.Listener
    graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)

func handler(w http.ResponseWriter, r *http.Request) {
    time.Sleep(20 * time.Second)
    w.Write([]byte("hello world233333!!!!"))
}

func main() {
    flag.Parse()

    http.HandleFunc("/hello", handler)
    server = &http.Server{Addr: ":9999"}

    var err error
    if *graceful {
        log.Print("main: Listening to existing file descriptor 3.")
        // cmd.ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
        // when we put socket FD at the first entry, it will always be 3(0+3)
        f := os.NewFile(3, "")
        listener, err = net.FileListener(f)
    } else {
        log.Print("main: Listening on a new file descriptor.")
        listener, err = net.Listen("tcp", server.Addr)
    }

    if err != nil {
        log.Fatalf("listener error: %v", err)
    }

    go func() {
        // server.Shutdown() stops Serve() immediately, thus server.Serve() should not be in main goroutine
        err = server.Serve(listener)
        log.Printf("server.Serve err: %v\n", err)
    }()
    signalHandler()
    log.Printf("signal end")
}

func reload() error {
    tl, ok := listener.(*net.TCPListener)
    if !ok {
        return errors.New("listener is not tcp listener")
    }

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

    args := []string{"-graceful"}
    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 signalHandler() {
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
    for {
        sig := <-ch
        log.Printf("signal: %v", sig)

        // timeout context for shutdown
        ctx, _ := context.WithTimeout(context.Background(), 20*time.Second)
        switch sig {
        case syscall.SIGINT, syscall.SIGTERM:
            // stop
            log.Printf("stop")
            signal.Stop(ch)
            server.Shutdown(ctx)
            log.Printf("graceful shutdown")
            return
        case syscall.SIGUSR2:
            // reload
            log.Printf("reload")
            err := reload()
            if err != nil {
                log.Fatalf("graceful restart error: %v", err)
            }
            server.Shutdown(ctx)
            log.Printf("graceful reload")
            return
        }
    }
}

systemd & supervisor

После завершения родительского процесса дочерний процесс зависнет на процессе №1. В этом случае использование гипервизоров, таких как systemd и supervisord, покажет, что процесс находится в состоянии сбоя. Есть два способа решить эту проблему:

  • Используя pidfile, обновляйте pidfile каждый раз при перезапуске процесса, чтобы менеджер процессов мог воспринимать изменение mainpid через этот файл.
  • Запустите мастер для управления сервисным процессом.Каждый раз при перезапуске мастера подтягивается новый процесс, а старый уничтожается. В это время pid мастера не меняется, а процесс находится в нормальном для диспетчера процессов состоянии.краткая реализация

References