ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

ginとendressを使ってGraceful Restartを実装してみた

GolangのWeb Frameworkであるgin、"Zero downtime restarts"を行うためのendressを使って
タイトルにあるようにGraceful RestartするWebのサンプルを作ってみました。

https://github.com/gin-gonic/gin
https://github.com/fvbock/endless

サンプルソース

ソースは以下のようになります。

main.go

package main

import (
	"fmt"
	"log"
	"os/exec"
	"syscall"
	"time"

	"github.com/fvbock/endless"
	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()

	router.GET("/hello", func(c *gin.Context) {
		t := time.Now()
		const layout = "2006-01-02 15:04:05"
		const version = "V1"
		message := fmt.Sprintf("Hello! Now is %s. This version is %s.", t.Format(layout), version)
		c.JSON(200, gin.H{"message": message})
	})
	router.GET("/restart", func(c *gin.Context) {
		pid, err := readPid()
		if err != nil {
			log.Fatal(err)
		}
		err = exec.Command("kill", "-SIGHUP", pid).Start()
		if err != nil {
			log.Fatal(err)
		}
		message := "accept restart. pid = " + pid
		c.JSON(200, gin.H{"message": message})
	})

	server := endless.NewServer("localhost:4430", router)
	server.BeforeBegin = func(add string) {
		pid := syscall.Getpid()
		err := writePid(pid)
		if err != nil {
			log.Fatal(err)
		}
		log.Printf("Actual pid is %d", pid)
	}
	err := server.ListenAndServe()
	if err != nil {
		log.Fatal(err)
	}
}

pid.go

package main

import (
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strconv"
)

func getPidFilePath() (string, error) {
	exe, err := os.Executable()
	if err != nil {
		return "", err
	}
	return filepath.Dir(exe) + "/pid", nil
}

func writePid(pid int) error {
	path, err := getPidFilePath()
	if err != nil {
		log.Fatal(err)
	}
	file, err := os.Create(path)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()
	_, err = file.Write(([]byte)(strconv.Itoa(pid)))
	if err != nil {
		return err
	}
	return nil
}

func readPid() (string, error) {
	path, err := getPidFilePath()
	if err != nil {
		log.Fatal(err)
	}
	data, err := ioutil.ReadFile(path)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

挨拶と現在日時を返す/hello、(セキュリティ的には微妙ですが)Graceful Restartを行う/restartの2つのGetメソッドを持ちます。
やっていることはソースを見てもらえれば分かりやすいかとは思いますが、ざっと書くと

  • ginでrouterを作り、Getメソッドを実装する
  • そのrouterをendressに渡してserverとして起動する

ことを行っています。

起動時にはpid.go内のwritePid()にてプロセスIDをファイルに出力しておき、/restartが呼ばれた時にはreadPid()で読み込んで使用します。

動作確認

ビルドしてバイナリを起動すると以下のように出力されます。

[GIN-debug] GET    /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] GET    /restart                  --> main.main.func2 (3 handlers)
2019/06/03 22:03:51 Actual pid is 97

同時にpidファイルにプロセスIDが出力されるはずです。
別のターミナルを起動し、動作確認のためにいくつかのコマンドを実行します。

$ curl http://localhost:4430/hello
{"message":"Hello! Now is 2019-06-03 22:05:02. This version is V1."}

$ curl http://localhost:4430/restart
{"message":"accept restart. pid = 97"}

$ curl http://localhost:4430/hello
{"message":"Hello! Now is 2019-06-03 22:06:15. This version is V1."}

$ kill -SIGHUP 202 # 最初にバイナリを起動したターミナルにrestart後のプロセスIDが出力される
$ kill -SIGINT 212

「kill -SIGHUP プロセスID」でGraceful Restartができます。ここで指定するプロセスIDは、バイナリの起動時にターミナルに出力された値です。
URLの/restartでもRestartできますが、実際は「kill -SIGHUP」を内部で呼び出しているだけです。

終了は「kill -SIGINT プロセスID」で行います。