2014年5月8日木曜日

LinuxでTCP_DEFER_ACCEPTが有効でもaccept後readできない理由

listen()のbacklogが不足した際のTCP_DEFER_ACCEPTの動作について - blog.nomadscafe.jpという記事の中で、listen backlog があふれた後に accept(2) すると、その後の read(2) が EAGAIN を返したり、接続が不安定になるという事象が説明されていました。気になったので調べてみたことをまとめます。

結論から言うとこれはLinuxの仕様です。manのtcp(7)を見ると、

TCP_DEFER_ACCEPT (since Linux 2.4)

Allow a listener to be awakened only when data arrives on the socket. Takes an integer value (seconds), this can bound the maximum number of attempts TCP will make to complete the connection. This option should not be used in code intended to be portable.

「TCP_DEFER_ACCEPTで指定した秒数を過ぎるまではacceptを遅らせるよ!」と書いてあります。つまりこれを過ぎると、データが到着していなくてもacceptできてしまう場合があります。

なんでこんなことになっているのか、パッチを見てみましょう。tcp: accept socket after TCP_DEFER_ACCEPT periodというパッチにヒントがあります。要約すると

タイムアウトでSYN-ACKを再送するたびにACKを返してくるクライアントに対しては、TCP_DEFER_ACCEPTで指定した秒数を過ぎたらESTABLISHEDにしたほうが良い。そうしたらサーバープログラムがもっと待つか、エラーとして接続を閉じるか選ぶことができる。副作用としてはaccept(2)したら必ずデータがあると思ってるアプリケーションに対しては副作用があるけど、もともとTCP_DEFER_ACCEPTが有効でも無効でも動作するように設計するほうが良いよね。

とあります。

そもそもTCP_DEFER_ACCEPTはどのような動作になっているのでしょうか? tcp_minisocks.c の tcp_check_req() を見ると、単にACKを落とすだけの処理になっています。

    /* While TCP_DEFER_ACCEPT is active, drop bare ACK. */
    if (req->num_timeout < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
        TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
        inet_rsk(req)->acked = 1;
        NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP);
        return NULL;
    }   

この状態でクライアントが次にデータを送ってくると、そのままSYN_RECVからESTABLISHEDにしてしまうようになっています。言い換えると、SYN_RECVの状態のソケットでもデータを受信する場合があり、ESTABLISHEDな状態になるということです。ここでaccept backlogがいっぱいだとlisten overflowになり、だまってパケットが落とされます。この時サーバーはSYN_RECVのままですが、クライアントはESTABLISHEDという奇妙なことになります。サーバー側はタイムアウトを迎えSYN+ACKを再送します。クライアント側はそれに対応するACKを返送しますが、やっぱり無視されます。そのうちクライアントは再送タイムアウトを迎えデータを再送します。それでもaccept backlogがいっぱいだと再びパケットが落ちます。そのうち指定秒数過ぎた後にSYN+ACKがサーバー側から再送(この時点ではソケットの状態はSYN_RECV)されてきてクライアントがACKを返し、さらにそのタイミングでaccept backlogに空きがあった場合にデータの存在しないソケットができる事になります。クライアントは再送タイムアウトを迎えた後データを送信します。

追記: ブコメの指摘で、頻繁にSYN+ACK を再送するのは古い動作だということがわかりました。Patch: tcp: reduce SYN-ACK retrans for TCP_DEFER_ACCEPT この場合、SYN+ACKが最初に再送されるのはTCP_DEFER_ACCEPTでの指定秒数が経過してからとなります。

というわけで、 TCP_DEFER_ACCEPT はあくまで「遅らせる」だけなので、accept(2)すれば必ずread(2)できるということを保証しないということでした。

2014年3月28日金曜日

WebScaleSQLとは何か(まとめと想像)

WebScaleSQLが公開されました。 これはFacebook, Google, LinkedInそしてTwitterのMySQLエンジニアが、大規模環境で使用する際に必要な変更点をまとめて、違う名前をつけてリリースしたものです。

MySQLとの違い

FAQにまとまってるものと、GitHubのcommitsを見ればなんとなくわかります。

  • 変更点それぞれについて全部のテストを自動で走らせるようなフレームワークを追加
  • ストレステストと自動化された性能テストを追加
  • テストを整理して、安全な変更が余計なテストを壊さないようにした
  • NUMA interleave、innodb バッファプールフラッシュの最適化、いくつかのクエリに対する最適化など性能改善
  • super_read_onlyと、1秒以下の精度でタイムアウトを指定するといった、web scaleで必要な変更

super_read_only とは、SUPER な権限を持った人でも read only にしてしまう機能のようです。

なぜ独立のブランチなのか

WebScaleSQLと新しく名前がついた割にはそこまで大きな変更点はなく、なぜリリースされたのかと疑問に思いました。私の想像ですが次のような感じなのかなぁと思っています。根拠なしなので間違ってたらごめんなさい。

  • お互いにエンジニア同士が交流する中、「それウチも困ってる!」というのが増えた
  • Oracle にパッチを投げるには少し面倒な手続きが必要
  • もう少し身軽に、様々な機能を取り込んで速いペースで試せる共通基盤がほしい
  • そうだ、名前をつけて公開してしまえば開発も加速できるしライセンスもはっきり

もう一度書きますが根拠レスです。あとはテストが彼らの求める水準としてはcommunity版には含まれていなくて、そういうのをもうちょっと気軽にメンテしておきたいんじゃないかとも思いました。

今後の見通し

しばらくはMySQLの最新プロダクションリリースに対するパッチセットのような感じで、"fork"と言わず"branch"と名乗って開発を続けるようです。コミットを見ていると1年以上前のコミットも多くて、とりあえずまとめてみました、ここからスタートですという感じに受け止めました。

最近また新しい機能の追加がぽつぽつ行われているようなので、しばらくするともう少し変わったものになるのかもしれません。

蛇足

きちんとかっこいいWebを作って、プレスリリースも出して、ニュースにもなってというのはすごいなぁと思いました。

2014年3月17日月曜日

JAWS Days 2014 で「最強のAWS」について発表しました

JAWS Days 2014に登壇させていただき、「最強のAWS」というテーマで発表しました。

モデレーターの@con_mameさんからは「AWSのここが良くなればいいのに」とか「この機能が無いのはどうよ」みたいな話をしてほしいと言われてました。でもAWSの怖いところは、「この機能があれば良いのに」と思っていると気づかないうちに実装されていたり、あまりに機能が多いので「実は存在した」というオチがありそうなところ。話題の選定には気を遣いましたが、結果的にAWSの中の人に「いいね!」と言ってもらえてよかったです。

今回の話題は大きく2つ。ひとつは「レイテンシ」、もうひとつは「テスト」です。レイテンシは、やはりクラウドや仮想技術につきものというか、どうしても物理環境よりも長くなってしまうので「もうひと押し!」のような考えで書きました。発表後に聞いたのですが、c3インスタンスでplacement groupを有効にすると非常にレイテンシが小さくなるとのこと。試してみたいですね。

もう一つの話題は「テスト」です。AWSのWeb Consoleはすごく充実しているけど、実はWebインタフェースで作業するのって"Infrastructure as Code"に反していると思うんですよね。再利用できないし、事前のレビューもできない。そこで多くの利用者はSDKを使って自動化していると思うんですけど、もう少し中間的な、お手軽に"Code"っぽいことができたらなぁという発想です。

発表に用いたスライドは以下です。

当日の参加者の皆様と関係者の皆様、ありがとうございました。

See also

2014年1月28日火曜日

somaxconnの上限値は65535

Linuxのネットワークパラメータの一つに、net.core.somaxconnというのがあります。これはlisten(2)の第二引数backlogの上限値となっています。このsomaxconnは一見intに見えますが、実はunsigned shortの範囲の数値しか受け付けません。それを超える数値を入れると黙って切り捨てられます。つまり

  • -1→65535
  • 0→0
  • 65535→65535
  • 65536→0
  • 65537→1

と同じような動作を内部的にします。なので、この値は絶対に0~65535の範囲を超えてはいけません。以下、詳しい説明です。

おことわり: この仕様はLinux 3.11以降変更されており、範囲外の数値を設定できないようになっています。ここに書いてある内容が再現するのは、Linux 3.10以前の古いカーネルのみです。

まずsysctlの定義ですが

net/core/sysctl_net_core.c

static struct ctl_table netns_core_table[] = {
 {
  .procname = "somaxconn",
  .data  = &init_net.core.sysctl_somaxconn,
  .maxlen  = sizeof(int),
  .mode  = 0644,
  .proc_handler = proc_dointvec
 },

include/net/netns/core.h

struct netns_core {
        /* core sysctls */
        struct ctl_table_header *sysctl_hdr;

        int     sysctl_somaxconn;

        struct prot_inuse __percpu *inuse;
};

以上のようにしっかりと"int"と書かれています。

さて、この値がどう使われているかというと、listen(2)でbacklogの最大値として利用された後inet_listenに引き渡されます。

net/ipv4/af_inet.c

int inet_listen(struct socket *sock, int backlog)
{
        struct sock *sk = sock->sk;
        unsigned char old_state;
        int err;
(途中省略)
        sk->sk_max_ack_backlog = backlog;
        err = 0;

out:
        release_sock(sk);
        return err;
}

という感じで、socket.sk_max_ack_backlogに渡されています。この値の宣言を見ると

include/net/sock.h

struct sock {
(途中省略)
        unsigned short          sk_ack_backlog;
        unsigned short          sk_max_ack_backlog;

unsigned shortとなっています。intをunsigned shortにそのまま代入してますね。なので、一番上に書いたようにオーバーフローします。黙ってオーバーフローします。net.core.somaxconnの値は正しくセットされるのが余計にたちが悪いですね。

ところでbacklog=0ってなんでしょうか?全くacceptできないように思えますが、実はキューの長さは

include/net/sock.h

static inline bool sk_acceptq_is_full(const struct sock *sk)
{
        return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

という感じで不等号で比較されているので、最低一つはaccept待ちのソケットが使用できます。

これはTCPのコードですが、unix domain socketでも似たような事象が起こります。

あと何年かすれば役に立たなくなる感じの知識ではありますが、今のところsomaxconnは16bitです。

2014年1月22日水曜日

ssh で id_rsa と違うキーの id_rsa.pub が置いてあるとログイン出来ない

sshでログインする際にローカルホストで使用する鍵は.ssh/id_rsaですが、この時このid_rsaと異なる鍵の公開鍵がid_rsa.pubとして置かれていると、そちらを参照してしまいログインできないという状況が発生しました。

再現手順は以下のとおり。

  1. ssh-keygen -t rsa -b 2048 で適当に鍵を作る
  2. mv .ssh/id_rsa .ssh/id_rsa2 としてバックアップ
  3. ssh-keygen -t rsa -b 2048 でもう一つ鍵を作る
  4. mv .ssh/id_rsa2 .ssh/id_rsa として秘密鍵を戻す

これで正しい秘密鍵を見ているはずなのに、ログインできなくなります。デバッグ出力には

debug1: Next authentication method: publickey
debug1: Offering RSA public key: /home/yuryu/.ssh/id_rsa
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password
debug1: Trying private key: /home/yuryu/.ssh/id_dsa
debug1: Trying private key: /home/yuryu/.ssh/id_ecdsa
debug1: Next authentication method: password

と出力されています。この「Offering RSA public key」が、.pubを見ているという出力です。 もし.pubが存在しないと

debug1: Next authentication method: publickey
debug1: Trying private key: /home/yuryu/.ssh/id_rsa
debug1: read PEM private key done: type RSA

といったログ出力になります。

この公開鍵がなぜ使われているのかまでは調べていませんが、おそらく公開鍵の計算をサボるために、公開鍵がすでに存在したら読み込むといった感じで使っているのではないかと思います。

ぐぐってもパーミッションの設定を間違えているケースばかりで、このハマり方をしている人はいませんでした。無駄に2時間ぐらい悩みました...

2014年1月15日水曜日

Linux Kernel Updates Vol.2013.12 の Kindle版を出しました

コミケ85で頒布した、Linux Kernel Updates Vol.2013.12 の Kindle版を出しました。

Linux Kernel Updates 2013.12の購入はこちらから!

以下は冊子版のプレビューです

目次

  • What's New in Linux 3.11?
  • What's New in Linux 3.12?
  • TCP/IPチューニング特集

TCP/IPチューニング特集はsysctlで設定できるパラメーターについて詳しく説明しています。紛らわしい設定、間違えやすい設定についてカーネルコードを参照しながら正確な説明を心がけました。

Linux Kernel Updates 2013.12の購入はこちらから!

2014年1月9日木曜日

VMware Fusionの中からPXE Bootする方法

VMware Fusionで仮想マシンを立ち上げた時に、外にあるサーバーからPXE Bootしたいと思いました。ところが普通にやってもfilenameやnext-serverが取れないので、起動できません。いろいろググったところ、下記のようにするのがよさそうでした。

dhcpd.confを編集

/Library/Preferences/VMware Fusion/vmnet8/dhcpd.conf を編集します。vmnet8というのは使っているネットワークのインタフェースになります。設定ファイルをのぞいてみて、IPのレンジが現在使っているものと一致すればいいと思います。私の環境にはvmnet1とvmnet8がありました。

dhcpd.confを開くと

###### VMNET DHCP Configuration. Start of "DO NOT MODIFY SECTION" #####

という行がありますから、その手前に

allow booting;
filename "pxelinux.0";
next-server 192.168.123.45;

という3行を追加します。 next-server の右側はPXE用のサーバーのアドレスです。

dhcpdを再起動

一番簡単な方法はOSを再起動することです。もし何らかの理由で再起動できない時は、psすると

root              581   0.0  0.0  2467008    468   ??  Ss    7:36PM   0:00.00 /Applications/VMware Fusion.app/Contents/Library/vmnet-dhcpd -s 7 -cf /Library/Preferences/VMware Fusion/vmnet8/dhcpd.conf -lf /var/db/vmware/vmnet-dhcpd-vmnet8.leases -pf /var/run/vmnet-dhcpd-vmnet8.pid vmnet8

というようなプロセスがいるので、これを kill して同じパラメータで起動しなおせば動くと思います。

まとめ

以上の手順でVMware Fusion上の仮想マシンからPXE Bootすることができました。