最近GoでWEBアプリケーションを運用するにあたってどこに気をつけているかをメモしました。
WEBサーバ
GoでWEBアプリケーションを作る場合、net/http
パッケージを使うことになります。
Go製のWEBフレームワークを使用する場合であっても、HTTPリクエストの処理部分はnet/http
を使用していると思います。
GoでWEBアプリケーションを書いた場合、すべてのリクエストをGoで受けるというケースはあまり想定されず、殆どの場合はリバースプロキシとしてWEBサーバを配置し、そのバックエンドにアプリケーションサーバを配置する構成になると思います。
このような構成を取る理由として、下記のようなものが挙げられると思います。
- ロギング
- ヘッダの柔軟な扱い
- 静的ページのリクエスト処理
- URLのパースやrewrite処理など
- キャッシュの利用など
WEBサーバを利用する場合、多くの場合でnginx
かapache
を採用するかと思いますが、どちらの場合でも、まずは同一ホスト内にWEB-アプリケーションサーバを配置して、アプリケーション間の通信にドメインソケットを用いる構成のほうが、portをlistenするよりも速いため、こちらを採用するケースが多いと思います。
- apacheでのドメインソケット設定例
AddHandler fastcgi-script fcgi
<IfModule mod_fastcgi.c>
FastCgiExternalServer /path/to/file -socket /path/to/go/sock.sock
</IfModule>
<Directory "/path/to/file">
</Directory>
- nginxの設定例
location /hoge{
include fastcgi_params;
fastcgi_pass unix:/path/to/go/sock.sock;
}
APPサーバ
Go側は、net.Listen
もしくはhttp.ListenAndServe
でリクエストを待ち受けます。
Listenで待ち受けるだけのプログラム例(※こちらを参考にしています)
package main
import (
"fmt"
"net"
"net/http"
"net/http/fcgi"
"os"
"os/signal"
"syscall"
)
type Server struct {
}
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request){
str := "Hello World!"
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.Header().Set("Content-Length", fmt.Sprint(len(str)))
fmt.Fprint(w, str)
}
func main(){
const SOCK = "/path/to/sock"
sig := make(chan os.Signal)
signal.Notify(sig, os.Interrupt)
signal.Notify(sig, syscall.SIGTERM)
server := Server{}
l, _ := net.Listen("unix", SOCK)
go func() {
fcgi.Serve(l, server)
}()
<-sig
if err := os.Remove(SOCK); err != nil {
panic("socket file remove error.")
}
}
fcgi.Serve()
はWEBアプリケーションからのリクエストごとにgoroutineを発行します。
goroutineの並列性能がGoで捌ける処理数であり、その性能はGOMAXPROCS
などによって変動します。
ここで注意したいのは、goroutine
を生成するmain
プログラムが異常終了すると、アプリケーション全体が停止してしまうという点が挙げられす。
main
が死なないようにするには、panicやエラーが発生しないよう十分なユニットテストを行うことが最重要ですが、メモリが増えてOOMキラーに殺されないようメモリリークの可能性を潰しておくことも大事です。
メモリの使用率を確認する
Goのアプリケーションでメモリの使用量は、プロファイラを使うことで簡単に見ることができます。
先ほどのプログラムで、pprofを仕掛けた例です。
heap profile: 1: 288 [1: 288] @ heap/1048576
1: 288 [1: 288] @ 0x41667a 0x43315f 0x435aff 0x430727 0x431be9 0x4318e5 0x43cbc6
# 0x43315f allocg+0x1f /usr/local/go/src/runtime/proc.c:925
# 0x435aff runtime.malg+0x1f /usr/local/go/src/runtime/proc.c:2106
# 0x430727 runtime.mpreinit+0x27 /usr/local/go/src/runtime/os_linux.c:219
# 0x431be9 mcommoninit+0xc9 /usr/local/go/src/runtime/proc.c:201
# 0x4318e5 runtime.schedinit+0x55 /usr/local/go/src/runtime/proc.c:138
# 0x43cbc6 runtime.rt0_go+0x116 /usr/local/go/src/runtime/asm_amd64.s:95
0: 0 [0: 0] @ 0x409446 0x4085a8 0x62459f 0x597336 0x4dcda7 0x4014c2 0x415ea4 0x43f1c1
# 0x62459f html.init+0xdf /usr/local/go/src/html/entity.go:2155
# 0x597336 html/template.init+0x76 /usr/local/go/src/html/template/url.go:105
# 0x4dcda7 net/http/pprof.init+0x77 /usr/local/go/src/net/http/pprof/pprof.go:209
# 0x4014c2 main.init+0x42 /hoge/fuga/main.go:48
# 0x415ea4 runtime.main+0xe4 /usr/local/go/src/runtime/proc.go:58
# runtime.MemStats
# Alloc = 322680
# TotalAlloc = 477560
# Sys = 2885880
# Lookups = 8
# Mallocs = 1786
# Frees = 458
# HeapAlloc = 322680
# HeapSys = 802816
# HeapIdle = 212992
# HeapInuse = 589824
# HeapReleased = 0
# HeapObjects = 1328
# Stack = 245760 / 245760
# MSpan = 5824 / 16384
# MCache = 1200 / 16384
# BuckHashSys = 1440400
# NextGC = 364336
# PauseNs = [137872 85399 122811 132260 146674 865689 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
# NumGC = 6
# EnableGC = true
# DebugGC = false
ここではGCの回数やアロケート済みのメモリ量などが確認できます。
パラメタの詳細はこちらにわかりやすくまとめられています。
HeapSys
がプログラムがOSに求めるメモリの量、HeapAlloc
が現在プログラムに割り当てられているメモリ量です。HeapIdle
は割り当て済みだが使われていないメモリ量、HeapReleased
がOSに返却するメモリの量で、GoはHeapIdle
にメモリを確保しておき、確保しておいたものの、5分以上使われなかった領域がリリース対象となるようです。
Goのメモリの使用量の推移はtop
や、下記のコマンドで見ることができます。
$ while sleep 1 ; do date -d '5 seconds ago' '+%H:%M:%S' ; ps aux | grep main(もしくはバイナリ名) ; done
root 42599 0.0 0.0 90352 2752 ? S Jun22 0:00 ./main
www 42600 0.0 0.4 569504 15824 ? Sl Jun22 5:51 ./main
14050 48019 0.0 0.0 6384 684 pts/0 S+ 10:48 0:00 grep main
この場合、2行めの./main
の15824が./mainが保持している実メモリのサイズとなります。
pprofのheapで現在割り当てられているメモリ量を確認してもほとんど変化がないのに、main
の使用するメモリ量が徐々に増えていく場合がありますが、その場合、一時的にメモリがプールされていることも多く、継続して監視を行い負荷が下がったところでメモリの使用量がちゃんと減ってることまで確認します。
さらに増え続けていくようであれば、メモリリークが起きている可能性があります。
接続数の制御
GoのアプリケーションはOSから見ると1つのプロセスですので、並列処理はあくまで言語内のgoroutineの処理となります。
(必要に応じてOSスレッドを追加します。現在のスレッド数はtop Shift+Hで確認できます)
fcgi.Serve()
の内部を見ると、リクエストごとにgoroutine
を生成していることがわかります。
280 // Serve accepts incoming FastCGI connections on the listener l, creating a new
281 // goroutine for each. The goroutine reads requests and then calls handler
282 // to reply to them.
283 // If l is nil, Serve accepts connections from os.Stdin.
284 // If handler is nil, http.DefaultServeMux is used.
285 func Serve(l net.Listener, handler http.Handler) error {
286 if l == nil {
287 var err error
288 l, err = net.FileListener(os.Stdin)
289 if err != nil {
290 return err
291 }
292 defer l.Close()
293 }
294 if handler == nil {
295 handler = http.DefaultServeMux
296 }
297 for {
298 rw, err := l.Accept()
299 if err != nil {
300 return err
301 }
302 c := newChild(rw, handler)
303 go c.serve()
304 }
305 }
このとき、goはgoroutine
発行時にCPUの負荷を考慮しない(?)ようで、CPUに余裕がない状態でgoroutine
生成が続くと、大量の処理待ちが発生してしまい急激に処理速度が劣化します。
これを防ぐために同時処理数の制御を行います。
同時処理制御は実践GO言語にも書かれていますが、チャネルを使って同時に行う並列処理に上限を設けます。
接続数制御の例
var sem = make(chan int, 1) //上限数を設定
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request){
sem <- 1 // キューのセット
str := "Hello World!"
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.Header().Set("Content-Length", fmt.Sprint(len(str)))
fmt.Fprint(w, str)]
<-sem //キューの処理完了まで待つ
}
並列性はCPUのコア数などによって異なるので、サーバごとにチューニングする必要があります。
その他のエラーに備える
あまり良い方法とは思いませんが、ユニットテストでも発生しなかったが、想定しないpanic
でmain
が死ぬケースを避けるためにServeHTTP
内でrecover
を記述しておく方法があります。
まとめ
GoのWEBアプリケーションをどのように運用するにあたって自分なりに気付いたところを記述しました。ご意見やご指摘がありましたらどうぞコメントください。