Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

最近GoでWEBアプリケーションを運用するにあたってどこに気をつけているかをメモしました。

WEBサーバ

GoでWEBアプリケーションを作る場合、net/httpパッケージを使うことになります。
Go製のWEBフレームワークを使用する場合であっても、HTTPリクエストの処理部分はnet/httpを使用していると思います。

GoでWEBアプリケーションを書いた場合、すべてのリクエストをGoで受けるというケースはあまり想定されず、殆どの場合はリバースプロキシとしてWEBサーバを配置し、そのバックエンドにアプリケーションサーバを配置する構成になると思います。

このような構成を取る理由として、下記のようなものが挙げられると思います。

  • ロギング
  • ヘッダの柔軟な扱い
  • 静的ページのリクエスト処理
  • URLのパースやrewrite処理など
  • キャッシュの利用など

WEBサーバを利用する場合、多くの場合でnginxapacheを採用するかと思いますが、どちらの場合でも、まずは同一ホスト内に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で待ち受けるだけのプログラム例(※こちらを参考にしています)

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

パラメタの詳細はこちらにわかりやすくまとめられています。

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を生成していることがわかります。

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言語にも書かれていますが、チャネルを使って同時に行う並列処理に上限を設けます。

接続数制御の例

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のコア数などによって異なるので、サーバごとにチューニングする必要があります。

その他のエラーに備える

あまり良い方法とは思いませんが、ユニットテストでも発生しなかったが、想定しないpanicmainが死ぬケースを避けるためにServeHTTP内でrecoverを記述しておく方法があります。

まとめ

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away