Изящный вебсервер на Go (Graceful Restart)

в 15:30, , рубрики: Веб-разработка, высокая доступность, высокая производительность, метки:

В этой статье я собираюсь описать Graceful Restart на Go. Graceful Restart важен для Go вебприложения. Go обладает одним недостатком. В Go нет возможности перезагружать код вовремя исполнения. Поэтому разработчики на Go встречаются с проблемой, которой нет в серверах написанных на Java, .NET или PHP. Если нужно обновить код сервера написанного на Go, то процесс сервера надо остановить и запустить новый процесс. Это понижает доступность сервера в момент обновления кода.

В предыдущей статье я описал Балансировщик на Go в 200 строк. На базе балансировщика можно обеспечить высокую доступность вовремя обновления приложения, но как тогда обновить сам балансировщик. Использование балансировщика часто может быть просто лишним. Если ваш сервер запущен на Mac OS X или Linux, то есть другой способ обновить код сервера и обработать все запросы поступившие в момент перезапуска сервера. Этим способ является Graceful Restart.

Суть Graceful Restart в том, что в unix/linux системах, открытые файлы и сокеты доступны порожденным процессам. Им достаточно знать значение файлового дескриптора (файловый дескриптор это целое число), что бы получить доступ к файлу или сокету открытому предком.

Вот перечень проблем которые нужно решить для реализации Graceful Restart на Go

  1. В Go автоматически закрываются все открытые файлы при окончании процесса (close-on-exec)
  2. Нужно, что то делать со старыми keep-alive соединениями открытыми в предке

Первая проблема решается двумя способами. С помощью fnctl можно снять флаг syscall.FD_CLOEXEC, или syscall.Dup создаст копию файлового дескриптора, без флага syscall.FD_CLOEXEC. Эти вызовы не доступны в Windows реализации Go, поэтому эта техника и работает Mac OS X и Linux. В данном примере я использую syscall.Dup. Это проще первого подхода.

Вторую проблему я решаю установкой Timeout для соединений в 10 секунд и выключением сервера через 11 секунд после Graceful Restart. Так же вторую проблему можно решить двумя другими способами: врапером net.Listner для того, что бы посчитать количество открытых соединений и предопределением func (c *conn) serve(), что достаточно сложно в Go. Может быть желательно и другое поведение. Например, что бы старый процесс после Graceful Restart сообщал об ошибке и закрывал соединения.

Важно понимать, что после Graceful Restart часть вебброузеров будут соеденены со старым сервером благодаря keep-alive. Новые соединения будут устанавливаться с новым сервером. Для наглядности какой сервер обработал какой запрос я в ответе с сервера указывать PID процесса.

grace1.go
package main

import (
	"flag"
	"fmt"
	"net"
	"net/http"
	"os"
	"os/exec"
	"syscall"
	"time"
	"log"
)

var FD *int = flag.Int("fd", 0, "Server socket FD")
var PID int = syscall.Getpid()
var listener1 net.Listener
var file1 *os.File = nil
var exit1 chan int = make(chan int)
var stop1 = false

func main() {
	fo1, err := os.Create(fmt.Sprintf("pid-%d.log", PID))
	if err != nil { panic(err) }
    	log.SetOutput(fo1)
	log.Println("Grace1 ", PID)

	flag.Parse()

	s := &http.Server{Addr: ":8080",
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	http.HandleFunc("/", DefHandler)
	http.HandleFunc("/stop", StopHandler)
	http.HandleFunc("/restart", RestartHandler)
	http.HandleFunc("/grace", GraceHandler)
	http.HandleFunc("/think", ThinkHandler)


	if *FD != 0 {
		log.Println("Starting with FD ", *FD)
		file1 = os.NewFile(uintptr(*FD), "parent socket")
		listener1, err = net.FileListener(file1)
		if err != nil {
			log.Fatalln("fd listener failed: ", err)
		}
	} else {
		log.Println("Virgin Start")
		listener1, err = net.Listen("tcp", s.Addr)
		if err != nil {
			log.Fatalln("listener failed: ", err)
		}
	}

	err = s.Serve(listener1)
	log.Println("EXITING", PID)
	<-exit1
	log.Println("EXIT", PID)
	
}

func DefHandler(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "def handler %d %s", PID, time.Now().String())
}

func ThinkHandler(w http.ResponseWriter, req *http.Request) {
	time.Sleep(5 * time.Second)
	fmt.Fprintf(w, "think handler %d %s", PID, time.Now().String())
}

func StopHandler(w http.ResponseWriter, req *http.Request) {
	log.Println("StopHandler", req.Method)
	if(stop1){
		fmt.Fprintf(w, "stopped %d %s", PID, time.Now().String())
	}
	stop1 = true
	fmt.Fprintf(w, "stop %d %s", PID, time.Now().String())
	go func() {
		listener1.Close()
		if file1 != nil {
			file1.Close()
		}

		exit1<-1
	}()
}

func RestartHandler(w http.ResponseWriter, req *http.Request) {
	log.Println("RestartHandler", req.Method)
	if(stop1){
		fmt.Fprintf(w, "stopped %d %s", PID, time.Now().String())
	}
	stop1 = true
	fmt.Fprintf(w, "restart %d %s", PID, time.Now().String())

	go func() {
		listener1.Close()
		if file1 != nil {
			file1.Close()
		}

		cmd := exec.Command("./grace1")
		err := cmd.Start()
		if err != nil {
			log.Fatalln("starting error:", err)
		}
		exit1<-1
	}()
}
func GraceHandler(w http.ResponseWriter, req *http.Request) {
	log.Println("GraceHandler", req.Method)
	if(stop1){
		fmt.Fprintf(w, "stopped %d %s", PID, time.Now().String())
	}
	stop1 = true
	fmt.Fprintf(w, "grace %d %s", PID, time.Now().String())

	go func() {
		defer func() { log.Println("GoodBye") }()
		listener2 := listener1.(*net.TCPListener)
		file2, err := listener2.File()
		if err != nil {
			log.Fatalln(err)
		}
		fd1 := int(file2.Fd())

		fd2, err := syscall.Dup(fd1)
		if err != nil {
			log.Fatalln("Dup error:", err)
		}

		listener1.Close()
		if file1 != nil {
			file1.Close()
		}

		cmd := exec.Command("./grace1", fmt.Sprint("-fd=", fd2))
		err = cmd.Start()
		if err != nil {
			log.Fatalln("grace starting error:", err)
		}

		log.Println("sleep11", PID)
		time.Sleep(10 * time.Second)
		log.Println("exit after sleep", PID)
		exit1<-1
	}()
}

Запускать эту программу следует без go run.

go build grace1.go
./grace1

Теперь когда сервер запущен у нас есть следующие обрабочики (handlers)

http://127.0.0.1:8080/ — обработчик по умлчанию
http://127.0.0.1:8080/restart — обычный перезапуск сервера
http://127.0.0.1:8080/grace — Graceful перезапуск сервера
http://127.0.0.1:8080/think — обработчик с задержкой

Для того, что бы проверить как это все работает, я написал другую программу на Go. Она делает последовательно запросы к серверу, если нет ошибки то на экран выводится буква g, если ошибка то E. После каждого запроса программа засыпает на 10ms.

bench1.go
package main

import (
	"net/http"
	"time"
)

func main() {
	nerr := 0
	ngood := 0
	for i := 0; i < 10000; i++ {
		resp, err := http.Get("http://127.0.0.1:8080/")
		if err != nil {
			// error
			print("E")
			nerr++
		}else{
			print("g")
			ngood++
			resp.Body.Close()
		}
		time.Sleep(10 * time.Millisecond)
	}
	println()
	println("Good:", ngood, "Error", nerr)
}

Если перезапускать сервер под нагрузкой то bench1.go выдаёт следующую картину.

gggggggggggggggggggggggggggggggggggggggggggggggggggggggEEEEEgggggggggggggggggggg
gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
gggggggggggggggggggggggggggggggggEEggggggggggggggggggggggggggggggggggggggggggggg
ggggggggggggggggggggggggggggggggggggggggggggggggEEgggggggggggggggggggggggggggggg
ggggggggggggggggggggggggEggggggggggggggggggggggggggggggggggggggggggggggggggggggg
gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
gggEEggggggggggggggggggEgggggggggggggggggggEggggggggggggggggEEgggggggggggggggggE
gggggggggggggggggggggEEgggggggggggggggggEggggggggggggggggggggEggggggggggggggggEE
gggggggggggggggggEEgggggggggggggggggEEggggggggggggggggggEgggggggggggggggEEgggggg

Одна или несколько букв E символизирует об ошибке и недоступности сервева вовремя перезапуска. (Я многократно перегрузил сервер, поэтому буквы E встречаются часто)

Если же использовать Graceful Restart то ошибок я не наблюдал вообще.

Автор: pyra

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js