Gin Framework Practice Series восемь | Как изящно перезапустить и остановить

Go

введение

  • Зачем перезапускать и останавливать изящно
  • В чем разница между перезапуском и остановкой, и как это можно считать элегантным
  • Что такое семафор и какие бывают
  • Как добиться плавного перезапуска и остановки
  • кодgithub

1. Почему?

На этапе разработки, когда мы модифицируем код или конфигурацию, мы сразу нажмем ctrl + c, а затем запустим службу.Если то же самое верно в производственной среде, какие проблемы это вызовет?

  • запрос потерян
  • Поведение пользователя прерывается
  • ты можешь потерять работу

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

2. В чем разница между перезапуском и остановкой?

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

3. Определение семафора

Сигналы — это ограниченное средство межпроцессного взаимодействия в Unix, Unix-подобных и других операционных системах, совместимых с POSIX.

  • общий семафор
Заказ Сигнал описывать
ctrl + c SIGINT Принудительно завершить процесс
ctrl + z SIGTSTP Задача прервана, процесс завис
ctrl + \ SIGQUIT конец процесса и дамп ядра
  • все семафоры
$ kill -l
 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG    24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF    28) SIGWINCH    29) SIGIO    30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14    51) SIGRTMAX-13    52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10    55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX

4. Как реализовать

достигнутые цели

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

Принцип реализации

  • Слушайте сигнал SIGHUP;
  • Передать файловый дескриптор, отслеживаемый службой, новому дочернему процессу после получения сигнала;
  • В это время новый и старый процессы получают запросы одновременно;
  • Родительский процесс перестает получать новые запросы и ждет завершения старых запросов (или истечения времени ожидания);
  • Родительский процесс завершается.

Используйте сторонние компоненты "github.com/fvbock/endless"

import ("github.com/fvbock/endless")

r := InitRouter()
	s := &http.Server{
		Addr:         fmt.Sprintf("%s:%d", config.ServerSetting.HttpAddress, config.ServerSetting.HttpPort),
		Handler:      r,
		ReadTimeout:  config.ServerSetting.ReadTimeout,  //请求响应的超市时间
		WriteTimeout: config.ServerSetting.WriteTimeout, //返回响应的超时时间
		//MaxHeaderBytes: 1 << 20,//默认的1MB
	}
    endless.ListenAndServe()

5. Компоненты горячего перезапуска собственной разработки

основной процесс


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

кодовый адрес

package hotstart

import (
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"sync"
	"syscall"
	"time"
)

const (
	LISTENER_FD           = 3
	DEFAULT_READ_TIMEOUT  = 60 * time.Second
	DEFAULT_WRITE_TIMEOUT = DEFAULT_READ_TIMEOUT
)

var (
	runMutex = sync.RWMutex{}
)

// HTTP server that supported hotstart shutdown or restart
type HotServer struct {
	*http.Server
	listener     net.Listener
	isChild      bool
	signalChan   chan os.Signal
	shutdownChan chan bool
	BeforeBegin  func(addr string)
}

func ListenAndServer(server *http.Server) error {
	return NewHotServer(server).ListenAndServe()
}

func ListenAndServe(addr string, handler http.Handler) error {
	return NewServer(addr, handler, DEFAULT_READ_TIMEOUT, DEFAULT_WRITE_TIMEOUT).ListenAndServe()
}

/*
new HotServer
*/
func NewHotServer(server *http.Server) (srv *HotServer) {
	runMutex.Lock()
	defer runMutex.Unlock()

	isChild := os.Getenv("HOT_CONTINUE") != ""

	srv = &HotServer{
		Server:       server,
		isChild:      isChild,
		signalChan:   make(chan os.Signal),
		shutdownChan: make(chan bool),
	}

	//服务启动之前钩子,命令行输出pid
	srv.BeforeBegin = func(addr string) {
		srv.logf(addr)
	}

	return
}

/*
new HotServer
*/
func NewServer(addr string, handler http.Handler, readTimeout, writeTimeout time.Duration) *HotServer {

	Server := &http.Server{
		Addr:         addr,
		Handler:      handler,
		ReadTimeout:  readTimeout,
		WriteTimeout: writeTimeout,
	}

	return NewHotServer(Server)
}

/*
Listen http server
*/
func (srv *HotServer) ListenAndServe() error {
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}

	ln, err := srv.getNetListener(addr)
	if err != nil {
		return err
	}

	srv.listener = ln

	if srv.isChild {
		//通知父进程不接受请求
		syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
	}

	srv.BeforeBegin(srv.Addr)

	return srv.Serve()
}

/*
监听 https server
*/
func (srv *HotServer) ListenAndServeTLS(certFile, keyFile string) error {
	addr := srv.Addr
	if addr == "" {
		addr = ":https"
	}

	config := &tls.Config{}
	if srv.TLSConfig != nil {
		*config = *srv.TLSConfig
	}
	if config.NextProtos == nil {
		config.NextProtos = []string{"http/1.1"}
	}

	var err error
	config.Certificates = make([]tls.Certificate, 1)
	config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return err
	}

	ln, err := srv.getNetListener(addr)
	if err != nil {
		return err
	}

	srv.listener = tls.NewListener(ln, config)

	if srv.isChild {
		syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
	}

	srv.BeforeBegin(srv.Addr)

	return srv.Serve()
}

/*
服务启动
*/
func (srv *HotServer) Serve() error {
	//监听信号
	go srv.handleSignals()
	err := srv.Server.Serve(srv.listener)

	srv.logf("waiting for connections closed.")
	//阻塞等待关闭
	<-srv.shutdownChan
	srv.logf("all connections closed.")

	return err
}

/*
get lister
*/
func (srv *HotServer) getNetListener(addr string) (ln net.Listener, err error) {
	if srv.isChild {
		file := os.NewFile(LISTENER_FD, "")
		ln, err = net.FileListener(file)
		if err != nil {
			err = fmt.Errorf("net.FileListener error: %v", err)
			return nil, err
		}
	} else {
		ln, err = net.Listen("tcp", addr)
		if err != nil {
			err = fmt.Errorf("net.Listen error: %v", err)
			return nil, err
		}
	}
	return ln, nil
}

/*
监听信号
*/

func (srv *HotServer) handleSignals() {
	var sig os.Signal

	signal.Notify(
		srv.signalChan,
		syscall.SIGTERM,
		syscall.SIGUSR2,
	)

	for {
		sig = <-srv.signalChan
		switch sig {
		case syscall.SIGTERM:
			srv.logf("received SIGTERM, hotstart shutting down HTTP server.")
			srv.shutdown()
		case syscall.SIGUSR2:
			srv.logf("received SIGUSR2, hotstart restarting HTTP server.")
			if err := srv.fork(); err != nil {
				log.Println("Fork err:", err)
			}
		default:
		}
	}
}

/*
优雅关闭后台
*/
func (srv *HotServer) shutdown() {
	if err := srv.Shutdown(context.Background()); err != nil {
		srv.logf("HTTP server shutdown error: %v", err)
	} else {
		srv.logf("HTTP server shutdown success.")
		srv.shutdownChan <- true
	}
}

// start new process to handle HTTP Connection
func (srv *HotServer) fork() (err error) {
	listener, err := srv.getTCPListenerFile()
	if err != nil {
		return fmt.Errorf("failed to get socket file descriptor: %v", err)
	}

	// set hotstart restart env flag
	env := append(
		os.Environ(),
		"HOT_CONTINUE=1",
	)

	execSpec := &syscall.ProcAttr{
		Env:   env,
		Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), listener.Fd()},
	}

	_, err = syscall.ForkExec(os.Args[0], os.Args, execSpec)
	if err != nil {
		return fmt.Errorf("Restart: Failed to launch, error: %v", err)
	}

	return
}

/*
获取TCP监听文件
*/
func (srv *HotServer) getTCPListenerFile() (*os.File, error) {
	file, err := srv.listener.(*net.TCPListener).File()
	if err != nil {
		return file, err
	}
	return file, nil
}

/*
格式化输出Log
*/

func (srv *HotServer) logf(format string, args ...interface{}) {
	pids := strconv.Itoa(os.Getpid())
	format = "[pid " + pids + "] " + format
	log.Printf(format, args...)
}

demo адрес

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

func test(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("test 22222"))
}

func main() {
	http.HandleFunc("/hello", hello)
	http.HandleFunc("/test", test)
	pid := os.Getpid()
	address := ":9999"
	s := &http.Server{
		Addr:    address,
		Handler: nil,
	}
	err := Hot.ListenAndServer(s)
	log.Printf("process with pid %d stoped, error: %s.\n", pid, err)
}

6. Скрипт

перезапустить (согласно сценарию запуска проекта)

ps aux | grep "gintest" | grep -v grep | awk '{print $2}' | xargs -i kill -SIGUSR2 {}

stop

ps aux | grep "gintest" | grep -v grep | awk '{print $2}' | xargs -i kill -SIGTERM {}

перезапуск порта

ps aux | lsof -i:8080 | grep -v grep | awk '{print $1}' | xargs -i kill -1 {}

портовая остановка

ps aux | lsof -i:8080 | grep -v grep | awk '{print $1}' | xargs -i kill {}

7. Резюме

В ежедневном http-сервисе изящный перезапуск (горячее обновление) является очень важной частью.В настоящее время у Golang есть много решений, мы можем выбирать в зависимости от ситуации

8. Рекомендуемый случай

Другая моя собственная структура добавила горячий запуск для поддержки компиляции в нескольких средах,адрес

9. Цикл статей