エレガントなGoサーバー(グレースフルリスタート)

この記事では、Goでのグレースフルリスタートについて説明します。 Go Webアプリケーションでは、グレースフルリスタートが重要です。 Goには1つの欠点があります。 Goには、実行時にコードをリロードする方法がありません。 そのため、Goの開発者は、Java、.NET、またはPHPで記述されたサーバーでは見られない問題に直面しています。 Goで記述されたサーバーコードを更新する必要がある場合は、サーバープロセスを停止して、新しいプロセスを開始する必要があります。 これにより、コードを更新する際のサーバーの可用性が低下します。



前の記事で、 Goバランサーを200行で説明しました。 バランサーに基づいて、アプリケーションの更新中に高可用性を確保できますが、その後、バランサー自体を更新する方法を確認できます。 多くの場合、バランサーを使用することは不要です。 サーバーがMac OS XまたはLinuxで実行されている場合、サーバーコードを更新し、サーバーの再起動時に受信したすべての要求を処理する別の方法があります。 これがグレースフルリスタートです。



Graceful Restartの本質は、unix / linuxシステムでは、開いているファイルとソケットが生成されたプロセスで利用できることです。 先祖によって開かれたファイルまたはソケットにアクセスするには、ファイル記述子の値(ファイル記述子は整数)を知るだけで十分です。



Goでグレースフルリスタートを実装するために解決する必要がある問題のリストを次に示します



  1. Goでは、開いているファイルはすべてプロセスの終了時に自動的に閉じられます(close-on-exec)
  2. 祖先で開いている古いキープアライブ接続で何かをする必要がある


最初の問題は2つの方法で解決されます。 fnctlを使用すると、syscall.FD_CLOEXECフラグをクリアできます。そうしないと、syscall.Dupはsyscall.FD_CLOEXECフラグなしでファイル記述子のコピーを作成します。 これらの呼び出しはGoのWindows実装では使用できないため、この手法はMac OS XおよびLinuxで機能します。 この例では、syscall.Dupを使用しています。 これは、最初のアプローチよりも簡単です。



接続のタイムアウトを10秒で設定し、グレースフルリスタートの11秒後にサーバーの電源をオフにすることで、2番目の問題を解決します。 また、2番目の問題は他の2つの方法で解決できます。オープンコネクションの数を計算するためのwraper net.Listnerと、Goでは非常に難しい事前定義されたfunc(c * conn)serve()です。 他の動作が望ましい場合があります。 たとえば、グレースフルリスタート後の古いプロセスがエラーを報告し、接続を閉じるようにします。



グレースフルリスタート後、キープアライブにより一部のWebブラウザーが古いサーバーに接続されることを理解することが重要です。 新しいサーバーとの新しい接続が確立されます。 明確にするために、どのサーバーがどの要求を処理したかについて、サーバーからの応答でプロセスの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
      
      







サーバーが実行されたので、次のハンドラーがあります



http://127.0.0.1:8080/-デフォルトハンドラー

http://127.0.0.1:8080/restart-通常のサーバーの再起動

http://127.0.0.1:8080/grace-サーバーの正常な再起動

http://127.0.0.1:8080/think-遅延ハンドラー



すべての動作を確認するために、Goで別のプログラムを作成しました。 エラーがなければ、サーバーに連続して要求を行い、エラーがEであれば、文字gが表示されます 各要求の後、プログラムは10ミリ秒でスリープ状態になります。



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
      
      







1つ以上の文字Eは、サーバーエラーと再起動中の使用不可を表します。 (サーバーを何度もオーバーロードしたため、Eという文字が一般的です)



グレースフルリスタートを使用する場合、エラーはまったく見られませんでした。



All Articles