Изящное завершение и перезапуск программ golang

задняя часть Go сервер Windows

Изящное завершение и перезапуск программ golang

что такое элегантность

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

Поэтому нам нужно найти способ плавно закрыть или перезапустить программу.

Это элегантно.

идеи

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

выполнить

отsiluser/bingoпример кадра

Цикл статей об этом фреймворке:

я использовалtim1020/godaemonВ этом пакете реализована функция плавного перезапуска (для большинства проектов прямое использование может удовлетворить большинство потребностей без модификации)

Ожидаемый эффект:

Введите в консолиbingo run daemon [start|restart|stop]могу сделать сервер启动|重启|停止

  1. Сначала посмотрите, как запустить сервер (bingo run dev)

оbingoРеализацию команды можно увидеть в моем предыдущем блоге:Реализация простого шаблона разработки на языке go по образцу laravel-artisan.

Поскольку это среда разработки, общая идеяbingo runпреобразовать команду в командуgo run start.goэтоshellЗаказ

такbingo run devэквивалентноgo run start.go dev

//处理http.Server,使支持graceful stop/restart
func Graceful(s http.Server) error {
	// 设置一个环境变量
	os.Setenv("__GRACEFUL", "true")
	// 创建一个自定义的server
	srv = &server{
		cm:     newConnectionManager(),
		Server: s,
	}

	// 设置server的状态
	srv.ConnState = func(conn net.Conn, state http.ConnState) {
		switch state {
		case http.StateNew:
			srv.cm.add(1)
		case http.StateActive:
			srv.cm.rmIdleConns(conn.LocalAddr().String())
		case http.StateIdle:
			srv.cm.addIdleConns(conn.LocalAddr().String(), conn)
		case http.StateHijacked, http.StateClosed:
			srv.cm.done()
		}
	}
	l, err := srv.getListener()
	if err == nil {
		err = srv.Server.Serve(l)
	} else {
		fmt.Println(err)
	}
	return err
}

Это запустит сервер и будет прослушивать изменения состояния соединения.

  1. Запустите сервер как демон

когда используешьbingo run daemonилиbingo run daemon start, это вызоветDaemonInit()функция, содержание следующее:

func DaemonInit() {
	// 得到存放pid文件的路径
	dir, _ := os.Getwd()
	pidFile = dir + "/" + Env.Get("PID_FILE")
	if os.Getenv("__Daemon") != "true" { //master
		cmd := "start" //缺省为start
		if l := len(os.Args); l > 2 {
			cmd = os.Args[l-1]
		}
		switch cmd {
		case "start":
			if isRunning() {
				fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+strconv.Itoa(pidVal)+"] Bingo is running", 0x1B)
			} else { //fork daemon进程
				if err := forkDaemon(); err != nil {
					fmt.Println(err)
				}
			}
		case "restart": //重启:
			if !isRunning() {
				fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "[Warning]bingo not running", 0x1B)
				restart(pidVal)
			} else {
				fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+strconv.Itoa(pidVal)+"] Bingo restart now", 0x1B)
				restart(pidVal)
			}
		case "stop": //停止
			if !isRunning() {
				fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "[Warning]bingo not running", 0x1B)
			} else {
				syscall.Kill(pidVal, syscall.SIGTERM) //kill
			}
		case "-h":
			fmt.Println("Usage: " + appName + " start|restart|stop")
		default:   //其它不识别的参数
			return //返回至调用方
		}
		//主进程退出
		os.Exit(0)
	}
	go handleSignals()
}

первым, чтобы получитьpidFileЭтот файл в основном используется для хранения процессов, запускающих программу.pid, зачем упорствоватьpidШерстяная ткань? Это нужно для того, чтобы определить, запускается ли одна и та же программа и другие операции во время нескольких запусков программы.

Затем, чтобы получить соответствующую операцию (старт|перезапуск|стоп), произнесите по очереди

case start:

первое использованиеisRunning()Как определить, запущена ли текущая программа? как указано вышеpidFileИзвлеките номер процесса из

Затем оцените, запущен ли процесс в текущей системе, если да, докажите, что он запущен, вернитеtrue, иначе возвратfalse

Если не работает, звонитеforkDaemon()Функция запускает программу, эта функция является ядром всей функции

func forkDaemon() error {
	args := os.Args
	os.Setenv("__Daemon", "true")
	procAttr := &syscall.ProcAttr{
		Env:   os.Environ(),
		Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
	}
	pid, err := syscall.ForkExec(args[0], []string{args[0], "dev"}, procAttr)
	if err != nil {
		panic(err)
	}
	savePid(pid)
	fmt.Printf("\n %c[0;48;32m%s%c[0m", 0x1B, "["+strconv.Itoa(pid)+"] Bingo running...", 0x1B)
	fmt.Println()
	return nil
}

syscallПакет не поддерживает систему win, а это значит, что если вы хотитеwindowsЕсли вы занимаетесь разработкой в ​​Интернете, вы можете использовать только виртуальную машину илиdockerЛа

Основная функция здесь заключается в использованииsyscall.ForkExec(),forkпроцесс

Команда, выполняемая для запуска этого процесса, является аргументом здесь (поскольку наша исходная команда былаgo run start.go dev, так вотargs[0]в действительностиstart.goскомпилированный бинарник)

тогда поставьforkНомер исходящего процесса хранится вpidFileвнутри

Итак, окончательный эффект — это то, что мы сказали на первом шаге.bingo run devдостигнутый эффект

case restart:

Это относительно просто,pidFileОпределить, запущена ли программа, если она запущена, она будет продолжать выполняться вниз

Тело функции также относительно простое, всего две строки.

syscall.Kill(pid, syscall.SIGHUP) //kill -HUP, daemon only时,会直接退出
forkDaemon()

Первая строка убивает процесс Вторая строка запускает новый процесс

case stop:

Вот строка кода, которая убивает процесс

дополнительные идеи

В процессе разработки каждый раз, когда происходит небольшое изменение (например, изменение небольшого контроллера), его необходимо выполнить снова.bingo run daemon restartКоманда, пусть новые изменения вступят в силу, очень хлопотно

Так что я снова разработалbingo run watchкоманда, прослушивание изменений, автоматический перезапуск сервера

я использовалGitHub.com/отправить уведомление/случается…пакет для мониторинга


func startWatchServer(port string, handler http.Handler) {
	// 监听目录变化,如果有变化,重启服务
	// 守护进程开启服务,主进程阻塞不断扫描当前目录,有任何更新,向守护进程传递信号,守护进程重启服务
	// 开启一个协程运行服务
	// 监听目录变化,有变化运行 bingo run daemon restart
	f, err := fsnotify.NewWatcher()
	if err != nil {
		panic(err)
	}
	defer f.Close()
	dir, _ := os.Getwd()
	wdDir = dir
	fileWatcher = f
	f.Add(dir)

	done := make(chan bool)

	go func() {
		procAttr := &syscall.ProcAttr{
			Env:   os.Environ(),
			Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
		}
		_, err := syscall.ForkExec(os.Args[0], []string{os.Args[0], "daemon", "start"}, procAttr)
		if err != nil {
			fmt.Println(err)
		}
	}()

	go func() {
		for {
			select {
			case ev := <-f.Events:
				if ev.Op&fsnotify.Create == fsnotify.Create {
					fmt.Printf("\n %c[0;48;33m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]created file:"+ev.Name, 0x1B)
				}
				if ev.Op&fsnotify.Remove == fsnotify.Remove {
					fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]deleted file:"+ev.Name, 0x1B)
				}
				if ev.Op&fsnotify.Rename == fsnotify.Rename {
					fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]renamed file:"+ev.Name, 0x1B)
				} else {
					fmt.Printf("\n %c[0;48;32m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]modified file:"+ev.Name, 0x1B)
				}
				// 有变化,放入重启数组中
				restartSlice = append(restartSlice, 1)
			case err := <-f.Errors:
				fmt.Println("error:", err)
			}
		}
	}()

	// 准备重启守护进程
	go restartDaemonServer()

	<-done
}

Сначала подпишитесьfsnotifyдокумент, создатьwatcher, а затем добавьте каталог прослушивания (здесь только файлы в каталоге прослушивания, а не подкаталоги)

Затем запустите две сопрограммы:

  1. Отслеживайте изменения файлов, если есть изменения файлов, записывайте количество изменений вslice, что является блокировкойforцикл

  2. Проверять изменение файла записи каждую 1сslice, если есть, перезапустите сервер и сбросьте каталог прослушивания, затем очиститеslice, иначе пропустить

    Рекурсивно перемещайтесь по подкаталогам, чтобы добиться эффекта мониторинга всего каталога проекта:

func listeningWatcherDir(dir string) {
	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		dir, _ := os.Getwd()
		pidFile = dir + "/" + Env.Get("PID_FILE")
		fileWatcher.Add(path)
		
		// 这里不能监听 pidFile,否则每次重启都会导致pidFile有更新,会不断的触发重启功能
		fileWatcher.Remove(pidFile)
		return nil
	})
}

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

Давайте посмотрим на код для перезагрузки сервера:

	go func() {
				// 执行重启命令
				cmd := exec.Command("bingo", "run", "daemon", "restart")
				stdout, err := cmd.StdoutPipe()
				if err != nil {
					fmt.Println(err)
				}
				defer stdout.Close()

				if err := cmd.Start(); err != nil {
					panic(err)
				}
				reader := bufio.NewReader(stdout)
				//实时循环读取输出流中的一行内容
				for {
					line, err2 := reader.ReadString('\n')
					if err2 != nil || io.EOF == err2 {
						break
					}
					fmt.Print(line)
				}

				if err := cmd.Wait(); err != nil {
					fmt.Println(err)
				}
				opBytes, _ := ioutil.ReadAll(stdout)
				fmt.Print(string(opBytes))

			}()

использоватьexec.Command()способ получитьcmd

перечислитьcmd.Stdoutput()Получите выходной канал, данные, напечатанные командой, будут вытекать из этого канала.

затем используйтеreader := bufio.NewReader(stdout)читать данные из трубы

с блокировкойforцикл, непрерывно считывающий данные из канала, чтобы\nчитать построчно

И распечатать в консоли для достижения эффекта вывода, если эти строчки не прописаны, в новом процессеfmt.Println()Данные, напечатанные методом, не будут отображаться на консоли.

Просто соус, и, наконец, вставьте ссылку на проектsilsuer/bingo, приветственная звезда, приветственный пиар, приветственные комментарии