こんにちは。KOUKIです。
とある企業でWebのシステム開発に従事しています。
本記事では、次のノウハウを記事にしました。
- Go言語とPostgreSQLとDockerを使った開発環境の構築
- http serverのTimeout処理の実装
Goのコードは、Udemyの「Learn the Why’s and How’s of concurrency in Go.」で学習したものを載せています※オススメです
Workspace
作業で使うディレクトリとファイルを用意してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
mkdir workspace cd workspace touch main.go go mod init httpApp touch docker-compose.yml touch Dockerfile $ tree . ├── Dockerfile ├── docker-compose.yml ├── go.mod ├── go.sum └── main.go |
開発環境構築
httpサーバー
Goでhttpサーバーを実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
package main import ( "database/sql" "fmt" "log" "net/http" "time" _ "github.com/lib/pq" ) var db *sql.DB const ( // postgres接続情報 conn = "host=postgres port=5432 user=alice password=password dbname=wonderland sslmode=disable" ) func logFatal(err error) { if err != nil { log.Fatal(err) } } // めっちゃ重い処理 func slowQuery() error { _, err := db.Exec("SELECT pg_sleep(5)") return err } func slowHandler(w http.ResponseWriter, req *http.Request) { start := time.Now() err := slowQuery() if err != nil { log.Printf("Error: %s\n", err.Error()) return } fmt.Fprintln(w, "OK") fmt.Printf("slowHandler took: %v\n", time.Since(start)) } func main() { var err error // postgresに接続 db, err = sql.Open("postgres", conn) logFatal(err) // 応答確認 err = db.Ping() logFatal(err) // serverの基本設定 srv := http.Server{ Addr: ":8080", WriteTimeout: 2 * time.Second, Handler: http.HandlerFunc(slowHandler), } // serverスタート log.Println("Start Http Server...") log.Fatal(srv.ListenAndServe()) } |
「Addr: “:8080”,」で、localhost:8080にアクセスするとslowHandlerを呼び出せるようにしています。注意点ですが、Docker上で動作させるアプリケーションだと「Addr: “localhost:8080″,」のようにlocalhostを指定してしまうとリクエストがコンテナに届かないので注意してください(理由は確認中)。
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
version: '3' services: http-server: build: context: . ports: - "8080:8080" depends_on: - postgres container_name: http-server volumes: - .:/go/src/app/ command: > sh -c "reflex -s -r '\.go$$' go run main.go" postgres: image: postgres:latest environment: - POSTGRES_USER=alice # aliceという名前のUserを作る - POSTGRES_PASSWORD=password # passwordでパスワードを設定 - POSTGRES_DB=wonderland # wonderlandというデータベースを作る container_name: postgres |
postgresは、environmentにデータベースおよびユーザ名/パスワードを指定することで、コンテナ起動時に自動的に作成されます。かなり便利です。
golang docker
GoのDockerfileイメージを用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
FROM golang:1.15-alpine3.12 RUN apk update && \ apk upgrade && \ apk add git RUN go get github.com/cespare/reflex ENV CGO_ENABLED=0 WORKDIR /go/src/app COPY go.mod go.sum main.go ./ RUN go mod download EXPOSE 8080 |
reflexを導入しているので、MacやLinux環境だとホットリロードができるようになります。つまり、ソースコードを変更してもコンテナを止める必要がありません。
ちなみに、Windows環境だとreflexは動きませんでした。
ビルド
準備が整いました。次のコマンドで、ビルドしましょう。
1 |
docker-compose build |
動作確認
以下のコマンドで、コンテナを立ち上げましょう。
1 2 3 |
docker-compose up http-server | [00] 2020/12/16 07:32:13 Start Http Server... |
特にエラーがでず「Start Http Server…」が表示されたので、Postgresには問題なく接続できたようですね。
time コマンドでレスポンスを測ってみましょう。
1 2 3 4 5 6 |
$ time curl localhost:8080 curl: (52) Empty reply from server real 0m5.068s user 0m0.007s sys 0m0.008s |
約5sですね。「curl: (52) Empty reply from server」とかえってきますが、これでOKです。http-serverコンテナも以下のログを出しています。
1 |
http-server | [00] slowHandler took: 5.027101548s |
TimeOut処理
http.Timeout
開発環境の構築が完了したので、次はhttp-serverのタイムアウトについて学んでみましょう。
先ほど、localhost:8080にリクエストを送信すると5s後にレスポンスがかえってきたことを確認しました。
httpサーバーでは、一定期間レスポンスがかえって来なかった場合、Timeoutしてあげる方が良いです。
Go言語では、httpパッケージのTimeoutHandlerを使うと簡単に実装することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func main() { ... // serverの基本設定 srv := http.Server{ ... Handler: http.TimeoutHandler( http.HandlerFunc(slowHandler), 1*time.Second, // 1秒でTimeout "Timeout!", ), } ... } |
Timeコマンドを実行してみましょう。
1 2 3 4 5 |
$ time curl localhost:8080 Timeout! real 0m1.058s user 0m0.006s sys 0m0.009s |
「Timeout!」メッセージとともに約1秒でTimeoutしました。What a usefull!
このTimeoutはクライアントからのリクエストをTimeoutするだけなので、サーバー側では処理が継続しているようです。
1 2 |
// slowHandlerが処理した結果 http-server | [00] slowHandler took: 5.005475475s |
これはよくありませんね。slowHandlerが実行されないようにしたいです。
Contextパッケージ
そんな時は、Contextパッケージが使えそうです。
Contextパッケージの詳細については、以下の記事を参照してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
package main import ( "context" "database/sql" "fmt" "log" "net/http" "time" _ "github.com/lib/pq" ) var db *sql.DB const ( // postgres接続情報 conn = "host=postgres port=5432 user=alice password=password dbname=wonderland sslmode=disable" ) func logFatal(err error) { if err != nil { log.Fatal(err) } } // コンテキストを引数に渡す func slowQuery(ctx context.Context) error { // ExecContextに変更 _, err := db.ExecContext(ctx, "SELECT pg_sleep(5)") return err } func slowHandler(w http.ResponseWriter, req *http.Request) { start := time.Now() // コンテキストを渡す err := slowQuery(req.Context()) if err != nil { log.Printf("Error: %s\n", err.Error()) return } fmt.Fprintln(w, "OK") fmt.Printf("slowHandler took: %v\n", time.Since(start)) } func main() { var err error db, err = sql.Open("postgres", conn) logFatal(err) // Timeout付きのコンテキストを作成(10秒でタイムアウト) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // PingContextに変更 err = db.PingContext(ctx) logFatal(err) srv := http.Server{ Addr: ":8080", WriteTimeout: 2 * time.Second, Handler: http.TimeoutHandler( http.HandlerFunc(slowHandler), 1*time.Second, "Timeout!", ), } log.Println("Start Http Server...") log.Fatal(srv.ListenAndServe()) } |
slowHandlerの*http.Requestは内部でコンテキストを持っています。このコンテキストはリクエストの状態を保持しており、このコンテキストをslowQuery関数に渡します。
slowQuery関数では、呼び出し元から渡されたコンテキストと共にDBにポーリングを行う処理に変更しました。こうすることで、何らかの理由によりコンテキストが閉じられた場合、データベースはクエリを停止しエラーメッセージを返せるようになります。
コンテキストは呼び出し元から呼び出し先へと情報を伝達させることができるので、http.TimeoutHandlerにてTimeoutになった時、このコンテキストは閉じられるはずです。
また、ついでにmain関数内のPingにもTimeoutを設定しています。
context.WithTimeoutにてコンテキストを作成しており、コンテキストの2番目の戻り値であるcancelを呼び出すことでコンテキストを閉じることができます。
そして、コンテキストを渡せるようにPingContextに変更して先程作成したコンテキストを渡しました。
動作を確認してみましょう。
1 2 3 4 5 6 7 8 |
$ time curl localhost:8080 Timeout! real 0m1.128s user 0m0.008s sys 0m0.010s // server側 http-server | [00] 2020/12/16 22:57:18 Error: pq: canceling statement due to user request |
今度は、クライアント側でTimeoutした場合、即座にサーバー側もTimeoutしました。slowHandlerは実行されていません。OKですね。
おわりに
Dockerを使えばめんどくさい開発環境も一瞬で構築できるので便利ですね^^
これからどんどん使って行きましょう。
ServerのTimeout処理も結構難しいかもしれませんね。自分もまだcancelの使い方が曖昧かもです。
もっと深くまで学習してGo言語のスペシャリストになりたいですね!
それでは、また!
最近のコメント