8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GoのHTTPサーバの実装例でよく見るあのパターンはベストプラクティスなのか?

Last updated at Posted at 2020-01-30

Techブログでよく見かけるHTTPサーバとDBコネクションの実装例です。

サーバ接続時にDBのコネクションを張るので、移行はDB接続を意識しなくていいので、一見とてもよさそうに見えます。
というか実際が楽になっていいのです。

しかし、Goを書き始めた頃に似たような実装を見て、そのまま実装したら障害起きて悲しい気持ちになったこともあります。

package main

import (
	"context"
	"database/sql"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, err := sql.Open("mysql", fmt.Sprintf(
		"%s:%s@tcp(%s:%s)/%s?parseTime=true",
		"(user name)", "(password)", "(host)", "(port)", "(database)")+"&loc=Asia%2FTokyo",
	)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	router := http.NewServeMux()

	router.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		if err := db.PingContext(r.Context()); err != nil {
			w.WriteHeader(503)
		}
	})

	srv := &http.Server{
		Handler: router,
		Addr:    fmt.Sprintf(":%s", "8081"),
	}
	go func() {
		panic(srv.ListenAndServe())
	}()

	// Create channel for shutdown signals.
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt)
	signal.Notify(stop, syscall.SIGTERM)

	//Recieve shutdown signals.
	<-stop
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := srv.Shutdown(ctx); err != nil {
		log.Printf("error shutting down server %s", err)
	} else {
		log.Println("Server gracefully stopped")
	}
}

正常系

まずは普通の状態でリクエストしてみます。

1回目

$ curl -v localhost:8081/hello
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Thu, 30 Jan 2020 02:36:28 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

2回目

普通に再度アクセスします。

$ curl -v localhost:8081/hello
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Thu, 30 Jan 2020 02:36:28 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

異常系

では、DBのコネクションが切れてしまったときはどうなるでしょう。
サーバを上げっぱなししている状態で、DBをあげたり落としたりしてみます。

まずは普通にアクセス

$ curl -v localhost:8081/hello
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Thu, 30 Jan 2020 02:36:28 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

DBを落とした状態でアクセス

DBにアクセスできないので、503が返ってきました。
これは期待値です

$ curl -v localhost:8081/hello
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 503 Service Unavailable
< Date: Thu, 30 Jan 2020 03:06:08 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

DBを再度立ち上げてからリクエスト

200が返ってきました。
どうやら動きとしては問題なさそうです。

$ curl -v localhost:8081/hello
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Thu, 30 Jan 2020 02:36:28 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

やらかし系

次に、どこかのAPIハンドラで異常時にDB接続を切ってしまった場合、もしくは依存パッケージで実はコネクションを切ってしまったパターンを考えます。

仕込み

今回は、Handlerの末尾にわかりやすいようにdb.Close()を仕込みます

router.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
	if err := db.PingContext(r.Context()); err != nil {
		w.WriteHeader(503)
	}
	db.Close() // コネクションを意図的にClose
})

1回目

$ curl -v localhost:8081/hello
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Thu, 30 Jan 2020 02:36:28 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

2回目

再度、普通にアクセスします。
すると503が返ってきます。

$ curl -v localhost:8081/hello
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 503 Service Unavailable
< Date: Thu, 30 Jan 2020 03:06:08 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

#何が問題か?

問題点

サーバを立ち上げるときにしかDBのコネクションを作る機会がないので、エラーハンドリングの処理などで不意にコネクションを切ってしまった場合に、DBコネクションのリトライをかける手段がないことです。

Unitテストを書いていたとしても、サーバ上げっぱなしで異常系を挟んで複数回アクセスするなんていうテストを真面目に書いている人はそこまでいないと思うので、気づきづらい挙動です。

気をつければいいという声もありそうですが、実装時に精神的負担が増えてしまいます。
Handler増やすたびに上記のようなテストパターンを作らなきゃいけないのもつらそうです。

解決方法

最も簡単なのは、middlewareなどでDB再接続の処理を共通処理として行ってしまうことだと思います。

  • Handlerにわたす前にDBコネクションが張られているか確認してからはられていなかったら再接続してからHandlerに渡す。
  • DBコネクションが張られていなかったらそのリクエストはエラーにしてしまうけどその後にDBに再接続して、その後のリクエストは正常に動くようにする。

などが考えられます。

8
0
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?