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. テスト環境
ソケットを使ってコネクションをいくつも待ち受けするサーバと、コネクションを多数貼り、貼り終わったら切断するクライアントを作成します。
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]))
}
} ()
}
}
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コネクション接続後に、タイムアウトエラーが発生します。
$ 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でした。
$ 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のインスタンスタイプによりネットワーク性能が異なることは、いろいろな測定結果が出ています。コネクション数の上限については、特に報告されていませんが、同様の原因(安価なインスタンスはネットワーク性能が低い)によるものでしょうか・・・?