GoのWEBアプリケーション運用について

最近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でのドメインソケット設定例

```httpd.conf
    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の設定例

```nginx.conf
    location /hoge{
        include fastcgi_params;
        fastcgi_pass unix:/path/to/go/sock.sock;
    }
```

# APPサーバ
Go側は、`net.Listen`もしくは`http.ListenAndServe`でリクエストを待ち受けます。

Listenで待ち受けるだけのプログラム例([※こちら](https://speakerdeck.com/catatsuy/golang-ads-deliver)を参考にしています）

```main.go
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の回数やアロケート済みのメモリ量などが確認できます。

パラメタの詳細は[こちら](https://blog.cloudflare.com/recycling-memory-buffers-in-go/)にわかりやすくまとめられています。

`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から見ると１つのプロセスですので、並列処理はあくまで言語内のgoroutineの処理となります。
(必要に応じてOSスレッドを追加します。現在のスレッド数はtop Shift+Hで確認できます）

`fcgi.Serve()`の内部を見ると、リクエストごとに`goroutine`を生成していることがわかります。

```net/http/fcgi/child.go
   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言語](http://golang.jp/2010/03/1260)にも書かれていますが、チャネルを使って同時に行う並列処理に上限を設けます。

接続数制御の例

```main.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アプリケーションをどのように運用するにあたって自分なりに気付いたところを記述しました。ご意見やご指摘がありましたらどうぞコメントください。