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

от автора

В этой статье я собираюсь описать 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 то ошибок я не наблюдал вообще.

ссылка на оригинал статьи http://habrahabr.ru/post/202584/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *