- PVSM.RU - https://www.pvsm.ru -

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

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

В предыдущей статье я описал Балансировщик на Go в 200 строк [1]. На базе балансировщика можно обеспечить высокую доступность вовремя обновления приложения, но как тогда обновить сам балансировщик. Использование балансировщика часто может быть просто лишним. Если ваш сервер запущен на 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() [2], что достаточно сложно в 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/ [3] — обработчик по умлчанию
http://127.0.0.1:8080/restart [4] — обычный перезапуск сервера
http://127.0.0.1:8080/grace [5] — Graceful перезапуск сервера
http://127.0.0.1:8080/think [6] — обработчик с задержкой

Для того, что бы проверить как это все работает, я написал другую программу на 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

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/vy-sokaya-proizvoditel-nost/48613

Ссылки в тексте:

[1] Балансировщик на Go в 200 строк: http://habrahabr.ru/post/197570/

[2] func (c *conn) serve(): http://golang.org/src/pkg/net/http/server.go?s=45668:45714#L1017

[3] http://127.0.0.1:8080/: http://127.0.0.1:8080/

[4] http://127.0.0.1:8080/restart: http://127.0.0.1:8080/restart

[5] http://127.0.0.1:8080/grace: http://127.0.0.1:8080/grace

[6] http://127.0.0.1:8080/think: http://127.0.0.1:8080/think

[7] Источник: http://habrahabr.ru/post/202584/