AWS ec2 t2タイプには51300コネクションしか繋がらない

2017/6/26追記 すみません。追試をしたら51300での限界が確認できたのはt2.mediumまででした。ただし、t2.largeでも約8万の限界があるようです。4章、5章を修正しました。

2017/6/27追記 Developer Forums(米)に質問をしたところ、原因不明だがm4シリーズでも10万以上で限界値を確認したという報告がありました。どうもAWS特有のアンドキュメントな現象らしいとのこと。

この記事ではAWS ec2上のCentOS7にインストールしたGo1.8を使っています。

AWS ec2のt2タイプには51300コネクションしか繋がらないようです。どのドキュメントを見てもこのような仕様は書かれていません。原因は不明でしたが、事実関係を集めたいので、同意非同意によらずに広くコメントいただきたいです。

いろいろ切り分けを繰り返したので、「CentOS7での大規模サーバーチューニングノウハウ」としても役立つ内容になっているかもしれません。

1. テスト環境

ソケットを使ってコネクションをいくつも待ち受けするサーバと、コネクションを多数貼り、貼り終わったら切断するクライアントを作成します。

testserver.go
package main

import (
  "fmt"
  "net"
)

const default_listener string = "0.0.0.0:8000"

func main() {
  listener, err := net.Listen("tcp", default_listener)
  if err != nil {
    fmt.Println("Listen error:", err)
    return
  }
  defer listener.Close()
  fmt.Println("Listening on ", default_listener)

  for {
    conn, err := listener.Accept()
    if err != nil {
      fmt.Println("Accept error:", err)
      break
    }
    go func() {
      defer conn.Close()
      buf := make([]byte, 1024)
      for {
        n, err := conn.Read(buf)
        if n == 0 {
          break
        }
        if err != nil {
          fmt.Println("Read error:", err)
          break
        }
        fmt.Println("Read:", string(buf[:n]))
      }
    } ()
  }
}
testclient.go
package main

import (
  "fmt"
  "net"
  "time"
  "os"
  "strconv"
  "sync"
)

const default_listener string = "172.31.17.55:8000"

func main() {
  // confirm arg
  maxConns := 100
  maxThreads := 100
  if len(os.Args) >= 2 {
    maxConns, _ = strconv.Atoi(os.Args[1])
  }
  if len(os.Args) >= 3 {
    maxThreads, _ = strconv.Atoi(os.Args[2])
  }
  fmt.Println("maxConns :=", maxConns, "maxThreads :=", maxThreads)

  conn := make([]net.Conn, maxConns)
  count := 0
  m := new(sync.Mutex)

  //connect
  var wg sync.WaitGroup
  for val := 0; val < maxThreads; val++ {
    wg.Add(1)
    go func(i int) {
      for j := 0; j <= maxConns / maxThreads; j++ {
        conns := j * maxThreads + i
        if conns >= maxConns {
          break
        }
        var err error
        conn[conns], err = net.Dial("tcp", default_listener)
        if err != nil {
            fmt.Println("Dial error:", err)
            time.Sleep(3600 * time.Second)
            panic(err)
        }
        m.Lock()
        count++    
        if count % 1000 == 0 {
          fmt.Println(count, "Connected.")
        }
        m.Unlock()
      }
      wg.Done()
    } (val)
  }
  wg.Wait()
  if count % 1000 > 0 {
    fmt.Println(count, "Connected.")
  }

  //disconnect
  for i := 0; i < maxConns; i++ {
    conn[i].Close()
    if (i + 1) % 1000 == 0 {
      fmt.Println(i + 1, "Disconnected.")
    }
  }
}

2. 実行結果

t2.micro同士でクライアント〜サーバ構成をとり、同時6万接続を実行させると、51000コネクション接続後に、タイムアウトエラーが発生します。

client
$ go run testclient.go 60000
maxConns := 60000 maxThreads := 100
1000 Connected.
(snip)
51000 Connected.
Dial error: dial tcp 172.31.17.55:8000: getsockopt: connection timed out
(repeat)

このとき、サーバ側でコネクション数を数えると51302でした。

server
$ netstat -tan | grep ':8000 ' | awk '{print $6}' | sort | uniq -c
  51302 ESTABLISHED
      1 LISTEN

なお、必ずしも51302ではなく、試行によって、約51300と言える中で若干数値は変わります。

3. 原因究明のための試行錯誤

3.1. メモリの枯渇ではない

topやvmstatでメモリを調べても余裕がありました。
なお、goでメモリ不足の場合signal: killedと表示してプログラムが終了しますので、このことからもメモリの枯渇では無いことがわかります。

3.2. ファイルディクリプタの枯渇ではない

クライアント、サーバともにファイルディスクリプタは拡張済みです。

$ ulimit -n
120000

なお、わざとファイルディクリプタを小さくすると、以下のようなエラー(サーバーの例)が出ます。このことからもファイルディクリプタの枯渇では無いことがわかります。

Accept error: accept tcp [::]:8000: accept4: too many open files

3.3. TCPポート数の枯渇ではない

クライアント、サーバともにip_local_port_rangeは拡張済みです。

$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 1024 65000

なお、わざとip_local_port_rangeを狭くすると、クライアント側に以下のエラーが出ます。このことからもTCPポートの枯渇では無いことがわかります。

Dial error: dial tcp 172.31.17.55:8000: connect: cannot assign requested address

ちなみにサーバー側はip_local_port_rangeをほとんど消費しませんので、狭くしてもエラーは出ません。

3.4. iptablesやfirewalldの影響では無い

iptalbesやfirewalldが動作していれば、セッションテーブル等のバッファ容量の影響も考えられます。しかし、ec2のデフォルトでは、これらは動作していません。

3.5. SELinuxの影響では無い

SELinuxを切ってみましたが、挙動は変わりません。

3.6. 最終セッションの次の接続要求がネットワーク経路上でロストしている

tcpdumpで見てみると、最終セッションの次の接続要求で、クライアント側はSYNを再送してリトライアウトしており、サーバー側ではSYNが届いていません。ネットワーク経路上のどこかでロストしているようにみえます。

なお、このことを観察するには、最終セッションまで接続完了させてその次のセッションでの失敗を見る必要があるため、クライアントのスレッド数を1にするとよいでしょう。

3.7. 「満杯」になったインスタンスは新規のネットワーク利用ができなくなる

本状態において、クライアントまたはサーハーのいずれかは「満杯」状態になっており、新規のSSH接続はもちろん、別インスタンスとの間のpingでさえ、双方向で通らなくなります。また、「満杯」ではない側のインスタンスも、SSH接続を1〜数本追加することで「満杯」状態になります。

4. 解消の手段

いろいろ試行錯誤して、インスタンスを(クライアント〜サーバーともに)変えることで、解消することがわかりました。どうも、t2シリーズ(mediumまで)のみの現象のようです。おまけでAzureでもやってみましたが、こちらも事象発生はありません。

インスタンスタイプ 結果
t2.micro 約51300接続で限界
t2.medium 約51300接続で限界
t2.large 60000接続完了
t2.xlarge 60000接続完了
m4.large 60000接続完了
m3.medium 60000接続完了
c4.large 60000接続完了
Azure A1 60000接続完了

6万を超えると、クライアント〜サーバーの1対1構成では、クライアントのTCPポート不足で試せなくなります。よって、クライアントをt2.medium×2台構成にして最大10万コネクションまで測定してみました。

インスタンスタイプ 結果
t2.large 約82460接続で限界
t2.xlarge 10万接続完了
m4.large 10万接続完了
m3.medium 10万接続完了
c4.large 10万接続完了

すると、t2.largeでは、8万コネクションを超えたところで「満杯」になりました。他のインスタンスタイプも10万コネクションを超えたところで満杯になる可能性はありますが、キリがないためテストできていません。

5. 原因の推定

事象的には、ネットワーク経路上でのステートフルファイアウォールのセッションテーブルが溢れているようにみえます。セキュリティグループが原因でしょうか・・・?

ec2のインスタンスタイプによりネットワーク性能が異なることは、いろいろな測定結果が出ています。コネクション数の上限については、特に報告されていませんが、同様の原因(安価なインスタンスはネットワーク性能が低い)によるものでしょうか・・・?