Код на стороне сервера часто требует обновления.Обычной практикой онлайн-обновления системы является использование внешней балансировки нагрузки (например, nginx), чтобы убедиться, что во время обновления доступна по крайней мере одна служба, и последовательное обновление (оттенки серого). ).
Другой более удобный способ — выполнить горячий перезапуск приложения и напрямую обновить приложение, не останавливая службу.
принцип
Принцип горячего перезапуска очень прост, но он включает в себя больше деталей, таких как некоторые системные вызовы и передача дескрипторов файлов между родительским и дочерним процессами.
Процесс обработки делится на следующие этапы:
- Сигнал монитора (USR2)
- Когда сигнал получен, разветвите дочерний процесс (используя ту же команду запуска), передав дескриптор файла сокета, который служба прослушивает дочернему процессу.
- Дочерний процесс слушает сокет родительского процесса, в это время и родительский, и дочерний процессы могут получать запросы
- После успешного запуска дочернего процесса родительский процесс перестает получать новые подключения и ожидает обработки старого подключения (или истечения времени ожидания).
- Родительский процесс завершается, и обновление завершено.
деталь
- Родительский процесс передает дескриптор файла сокета дочернему процессу через командную строку, переменные среды и т. д.
- Дочерний процесс запускается с той же командной строки, что и родительский процесс, и для 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 мастера не меняется, а процесс находится в нормальном для диспетчера процессов состоянии.краткая реализация