В предыдущей статье я описал Балансировщик на Go в 200 строк. На базе балансировщика можно обеспечить высокую доступность вовремя обновления приложения, но как тогда обновить сам балансировщик. Использование балансировщика часто может быть просто лишним. Если ваш сервер запущен на Mac OS X или Linux, то есть другой способ обновить код сервера и обработать все запросы поступившие в момент перезапуска сервера. Этим способ является Graceful Restart.
Суть Graceful Restart в том, что в unix/linux системах, открытые файлы и сокеты доступны порожденным процессам. Им достаточно знать значение файлового дескриптора (файловый дескриптор это целое число), что бы получить доступ к файлу или сокету открытому предком.
Вот перечень проблем которые нужно решить для реализации Graceful Restart на Go
- В Go автоматически закрываются все открытые файлы при окончании процесса (close-on-exec)
- Нужно, что то делать со старыми 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/
Добавить комментарий