- PVSM.RU - https://www.pvsm.ru -
В этой статье я собираюсь описать 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
Первая проблема решается двумя способами. С помощью 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 процесса.
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.
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/
Нажмите здесь для печати.