Изящное завершение и перезапуск программ golang
что такое элегантность
Когда онлайн-код обновляется, нам нужно сначала закрыть службу, а затем запустить службу.Если объем трафика относительно велик, когда служба закрыта, на текущем сервере, вероятно, будет много соединение, то, если в это время служба будет закрыта напрямую, все эти соединения будут отключены, что повлияет на работу пользователя, что определенно не элегантно.
Поэтому нам нужно найти способ плавно закрыть или перезапустить программу.
Это элегантно.
идеи
- При запуске сервера открывается еще одна сопрограмма для прослушивания сигнала выключения.
- Когда сопрограмма получит сигнал закрытия, она откажется от новых подключений и отключится после обработки всех текущих подключений.
- Запустите новый серверный процесс, чтобы принять новые соединения
- закрыть текущий процесс
выполнить
отsiluser/bingoпример кадра
Цикл статей об этом фреймворке:
- Напишите простую веб-инфраструктуру MVC, используя Go
- Оборачиваем удобную ORM в Go
- Модернизация httprouter для поддержки промежуточного ПО
- Реализация простого шаблона разработки на языке go по образцу laravel-artisan.
я использовалtim1020/godaemonВ этом пакете реализована функция плавного перезапуска (для большинства проектов прямое использование может удовлетворить большинство потребностей без модификации)
Ожидаемый эффект:
Введите в консолиbingo run daemon [start|restart|stop]
могу сделать сервер启动|重启|停止
- Сначала посмотрите, как запустить сервер (
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
}
Это запустит сервер и будет прослушивать изменения состояния соединения.
- Запустите сервер как демон
когда используешь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
, а затем добавьте каталог прослушивания (здесь только файлы в каталоге прослушивания, а не подкаталоги)
Затем запустите две сопрограммы:
-
Отслеживайте изменения файлов, если есть изменения файлов, записывайте количество изменений в
slice
, что является блокировкойfor
цикл -
Проверять изменение файла записи каждую 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, приветственная звезда, приветственный пиар, приветственные комментарии