10 月頭に Linux の [netdev メーリングリスト](https://lore.kernel.org/netdev/)に以下のメールが届きました。 - [netdev development stats for 6.1?](https://lore.kernel.org/netdev/20221004212721.069dd189@kernel.org/) 送信者は Jakub Kicinski で Networking subsystem のメンテナの一人です。元々 netdev は David S. Miller (DaveM) が一人でメンテナンスしており、 Jakub は [DaveM の次の世代のメンテナ](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=bda6a35505e28b70d48096c933d5949c9b6caf9a)で、このメールの他にも [peer feedback](https://lore.kernel.org/netdev/20221020183031.1245964-1-kuba@kernel.org/) の文化を netdev に持ち込んだり、積極的にコミュニティを良くしようと活動しています。 メールの内容は v5.19 から v6.1 までの開発サイクルに関する統計で、その範囲は netdev に限定されているようでした。この手の統計は LWN.net が各サイクル毎に出しているものの、 Linux 全体の統計はほとんど自分に縁のないものだったので、このメールを見つけた時は少しわくわくしました。 まず最初は内容を読む前に Ctrl + f を押して自分の名前を検索してみました。すると Top 10 authors の欄に二つ自分の名前を見つけました。少し喜んでいたところ、すぐ下にもう一つ自分の名前を見つけました。 ``` Top 10 authors (thr): Top 10 authors (msg): 1. [ 84] ... 1. [287] ... 2. [ 52] ... 2. [232] ... 3. [ 43] ... 3. [166] ... 4. [ 28] ... 4. [156] Kuniyuki Iwashima 5. [ 28] ... 5. [134] ... 6. [ 23] ... 6. [122] ... 7. [ 23] ... 7. [106] ... 8. [ 20] ... 8. [ 93] ... 9. [ 20] Kuniyuki Iwashima 9. [ 93] ... 10. [ 20] ... 10. [ 86] ... ``` どうやら negative なスコアで 2 位を叩き出してしまったようで、ここでメールを最初から読み始めました。 ``` Top 10 scores (positive): Top 10 scores (negative): 1. [4102] ... 1. [397] ... 2. [1848] ... 2. [116] Kuniyuki Iwashima 3. [737] ... 3. [105] ... 4. [620] ... 4. [ 93] ... 5. [611] ... 5. [ 82] ... 6. [588] ... 6. [ 82] ... 7. [429] ... 7. [ 77] ... 8. [418] ... 8. [ 68] ... 9. [406] ... 9. [ 67] ... 10. [344] ... 10. [ 64] ... ``` ## Who is helping ? メールの冒頭にあるように、この統計は「誰が助けになるか」、つまり、「誰がパッチのレビューを多くしているか」に焦点を当てたものでした。また、 LWN.net の統計がコミットメッセージに含まれる Signed-off-by や Reviewed-by の数に基づく一方で、この統計ではメーリングリストに送信されたメール数も考慮されているようでした。 ``` For a while now I had been curious if we can squeeze any interesting stats from the ML traffic. In particular I was curious "who is helping", who is reviewing the most patches (but based on the emails sent not just review tags). ``` メールの最初には Top 10 reviewers のランキングがあり、二つ目のランキングが Top 10 authors でした。さらにその下では**パッチを多く書いた人のほとんどがレビューを行っていない**と苦言が呈されていました。 ``` most of the authors are not making the top reviewer list :( ``` そこから先ほどの Top 10 scores (negative) のランキングが記載されており、各人のスコアは `10 * reviews - 3 * authorship` で算出されていました。このスコアが *good citizen* としての指標であることから、メンテナにとってパッチのレビューがどれほど負荷になっているか、そしてその負荷を軽減する *citizen* がどれだけありがたがられる存在かが汲んで取れます。 ``` And here is the part that I was most curious about. Calculate a "score" which is roughly: 10 * reviews - 3 * authorship, to see who is a "good citizen": ``` そんな中、他のパッチのレビューをほとんどせずに自分のパッチばかり出していた私は *bad citizen* として 2 位にランクインしてしまいました。 さらに続く企業別ランキングでも同様の結果となっていました。メッセージの数から分かるように Amazon からのメールのうち 156/157 が私のメールで、 netdev においては自分の活動がほとんどそのまま Amazon の印象として反映されていました。 ``` Top 8 authors (thr): Top 7 authors (msg): ... ... ... 7. [157] Amazon ... ... Top 12 scores (positive): Top 12 scores (negative): ... ... ... 4. [ 95] Amazon ... ... ... Amazon also seem to send a lot more code than they help to review. ``` AWS に入社できたおかげでカーネル開発者として働く機会を得られたのもあって、自分が関わるコミュニティでは Amazon の OSS に関する印象を良くできればと思っていました。それだけにこの結果は残念に感じました。一方で少なくとも Amazon が全く OSS に貢献していないという印象は変えられたようにも思えました。それでも会社の印象を悪くしてしまったのは事実なので、この統計についてマネージャーに共有したところ、優しい言葉に救われました。 > You are an amazing engineer and great contributor. YOU ARE helping to change the narrative and help Amazon have a positive image in the community.. It will take some time and work with XXX and YYY (Pr. と Sr. のチームメイト) to help you with any review you are unsure of - I am so impressed with you and your approach and awareness here.. keep up the great work!! You are doing great! Keep in mind that the path to being a technical leader is hard, and you may stumble, but that is the path you are on - The path to being a great technical leader inside Amazon and in the Linux Kernel community -- keep pushing, ask for help when needed.. ## The path to being a good citizen カーネル開発を始めた頃から夢はメンテナと口にしていました。毎日メーリングリストを追いかける中、読んでも分からないパッチがほとんどで、メンテナはまだまだ遠い存在に感じていました。そのせいもあって以前から人のパッチをレビューするにはまだ早いと思い込んでいて、 Jakub のメールを見てもなお自分がレビューに参加できるものかと悩んでいました。そんな時背中を押してくれたのが同じ時期に公開された [Martin KaFai Lau のインタビュー記事](https://developers.facebook.com/blog/post/2022/10/06/meet-the-developers-linux-kernel-team-martin-lau/)でした。 Martin と最初にやりとりしたのは一昨年に [SO_REUSEPORT に関するパッチセット](https://lore.kernel.org/netdev/20201117094023.3685-1-kuniyu@amazon.co.jp/)を投稿した時でした。 Martin は最初にパッチセットに興味を持ってくれて、その後に何度もプライベートでレビューを重ねてくれました。おかげで最終的にパッチセットはマージされ、昨年の [Linux Plumbers Conference](https://lpc.events/event/11/contributions/946/) での登壇まで叶いました。当時 BPF レビュアーだった Martin は「TCP はコードリーディングの練習にいい」と言っていて、これまた遠い存在でした。 しかし記事を読むと、そんな彼が少しだけ近い存在にも思えました。 Martin でさえも最初は分からないことの方が多く、狭い領域での貢献を繰り返すことで徐々に知識の幅を広げていったようです。そんな Martin は最近 [BPF のいくつかの領域でメンテナになっていて](https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/commit/?id=32df6fe110c443763d6749a758f33a7117ec1270)、それを踏まえるとなんとも勇気付けられる内容でした。 > **What are some of the misconceptions about kernel or OSS development that you have encountered in your career?** > > ... It is impossible to get a grip on everything in the kernel. I shrink the scope, focus on one piece and fix something to gain credibility in the upstream. Then, I repeat the process to expand my knowledge space. よくよく思い返すと、自分にも狭い範囲でなら自信のある領域はありました。今まで出したパッチの多くはソケットに関するもので、その範囲に限定すればある程度レビューは出来ると思えました。 それまで netdev に参加しつつもどこか距離を感じていたのですが、この時に自分もコミュニティを形成する一人なのだと考えを改めて、積極的にレビューに参加するよう心掛けました。 そしてレビューを始めた頃、 Jakub からプライベートで 1 通のメールを受け取りました。どうもその時だけ `git send-email` でタイトルに `Re: ` と付け忘れていたようで、統計に正しく反映されないことを親切に指摘してくれました。 ``` > Acked-by: Kuniyuki Iwashima <kuniyu@amazon.com> Thanks for the review. FWIW I noticed that your mail client / script does not add 'Re: ' to the subject which may be mildly confusing, for example to scripts generating review statistics :) ``` 指摘についてありがとうと返信しつつ、最後に統計に触れてレビューに参加していくことを宣言しました。すると Jakub から期待していると返信がきたので益々頑張ろうと思えました。 ``` > BTW, the statistics helped me justify spending more time on reviewing :) Awesome! :) > I'm not so knowledgeable but will try to broaden expertise and join reviewing. Great, looking forward! ``` そして遂に先週、 v6.2 の開発サイクルの統計が出ました。 - [[ANN] netdev development stats for 6.2](https://lore.kernel.org/netdev/20221215180608.04441356@kernel.org/) この 2 ヶ月は個人的に [Open Source Summit Japan](https://www.youtube.com/watch?v=EB71gr1MZSg) の準備で忙しくしていたせいもあり、あまりパッチを出せていませんでした。またソケットに関するパッチはさして多くないので、レビューする機会もあまり多くはありませんでした。今回はランキングにも載らないだろうと思いつつも一応自分の名前を検索してみると、一箇所ヒットしました。 ``` Kuniyuki has become an active reviewer of the socket layer (thanks!!) therefore taking Amazon off the negative scores. ``` とても嬉しかったです。 Amazon の悪い印象を払拭できたのもそうですが、メンテナの Jakub にソケットレイヤのレビュアーとして認識されていたことが特に嬉しかったです。 [MAINTAINERS ファイル](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/MAINTAINERS)にソケットレイヤ単体のエントリはないので正式なレビュアーになった訳ではないですが、コミュニティ内での立ち位置を確かなものに出来たように思いました。 とはいえ *good citizen* には程遠いのでこれからも頑張ろうと思います。最後に Jakub の[スクリプト](https://github.com/kuba-moo/ml-stat)を使ってメンテナと自分の遠さを確認して終わろうと思います。 ``` $ python3 ./ml-stat.py --db db.json.sample --email-count 18000 --top-extra 90 Tue, 04 Oct 2022 17:55:42 -0700 ... Top 100 reviewers (thr): 1. [354] Jakub Kicinski 2. [171] ... 3. [140] ... 4. [ 87] Paolo Abeni 5. [ 85] Eric Dumazet ... 32. [ 18] Kuniyuki Iwashima ... Top 100 reviewers (msg): 1. [583] Jakub Kicinski 2. [325] ... 3. [264] ... 4. [162] ... 5. [148] Eric Dumazet ... 58. [ 21] Kuniyuki Iwashima ... Top 105 authors (thr): 1. [ 65] ... 2. [ 57] ... 3. [ 56] Jakub Kicinski 4. [ 30] ... 5. [ 29] ... 6. [ 29] Eric Dumazet ... 12. [ 24] Kuniyuki Iwashima ... Top 100 authors (msg): 1. [234] ... 2. [182] ... 3. [148] Jakub Kicinski 4. [146] ... 5. [125] ... ... 17. [ 77] Kuniyuki Iwashima ... Top 105 scores (positive): 1. [4462] Jakub Kicinski 2. [2355] ... 3. [1810] ... 4. [1057] Paolo Abeni 5. [1028] Eric Dumazet ... 68. [110] Kuniyuki Iwashima ```
TCP ソケットと `SO_REUSEPORT` オプションに関する問題を解決するために Linux カーネル v5.14 から取り込まれる予定のパッチセットについて 2 回に分けて解説します。 - https://lore.kernel.org/bpf/20210612123224.12525-1-kuniyu@amazon.co.jp/ - https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/commit/?id=1f26622b791b6a1b346d1dfd9d04450e20af0f41 Part 1 では `SO_REUSEPORT` オプション、カーネルの挙動と問題点、パッチセットの効果について解説し、 Part 2 ではカーネルの実装と修正方法、追加した eBPF の機能について解説します。 ## SO_REUSEPORT とは 従来、 Linux では一つの IP アドレス・ポート番号の組に対する TCP コネクションは一つのソケットのみで待ち受けることができました。トラフィックの多いサーバーでは単一のソケットに対する `accept()` がボトルネックになるため、 [v3.9 から SO_REUSEPORT というオプションが導入されました](https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=c617f398edd4db2b8567a28e899a88f8f574798d)。 `SO_REUSEPORT` を有効した複数のソケットは同一の IP アドレス・ポート番号への通信を待ち受けることが可能なため、複数のプロセスから異なるソケットに対して `accept()` を呼び出せるようになり、ボトルネックが解消されました。 まずは簡単な Python のスクリプトを実行して `SO_REUSEPORT` の効果を確認してみましょう。 ### クイズ 1 以下のスクリプト (`quiz1.py`) を実行した時、何が出力されるでしょうか。少しだけスクロールを止めて考えてみてください。 ```python3:quiz1.py import socket ADDR = '127.0.0.1' PORT = 8000 def get_server(): s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) s.bind((ADDR, PORT)) s.listen(32) return s def get_client(): c = socket.socket() c.connect((ADDR, PORT)) return c def main(): server_1 = get_server() server_2 = get_server() client = get_client() client.send(b'Hello World') server_2.settimeout(3) child = server_2.accept()[0] print(child.recv(1024)) if __name__ == '__main__': main() ``` #### 正解 何度かスクリプトを実行すると、おおよそ同じ頻度で 2 つの出力が確認できます。 `client` からのコネクションを `server_2` から `accept()` できた場合は以下のように表示され、 ``` $ python3 quiz1.py b'Hello World' ``` `accept()` できない場合はタイムアウトが発生して以下のように出力されます。 ``` $ python3 quiz1.py Traceback (most recent call last): File "quiz1.py", line 30, in <module> main() File "quiz1.py", line 26, in main child = server_2.accept()[0] File "/usr/lib64/python3.7/socket.py", line 212, in accept fd, addr = self._accept() socket.timeout: timed out ``` ### クイズ 2 では以下のスクリプト (`quiz2.py`) を実行した時、何が出力されるでしょうか。今回は `server_2` を作成する前に `client` が `connect()` を呼び出しており、 `server_2` の作成後に `server_1` が `close()` されている点に注意してください。 ```python3:quiz2.py import socket ADDR = '127.0.0.1' PORT = 8000 def get_server(): s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) s.bind((ADDR, PORT)) s.listen(32) return s def get_client(): c = socket.socket() c.connect((ADDR, PORT)) return c def main(): server_1 = get_server() client = get_client() client.send(b'Hello World') server_2 = get_server() server_1.close() server_2.settimeout(3) child = server_2.accept()[0] print(child.recv(1024)) if __name__ == '__main__': main() ``` #### 正解 今度は何度実行しても必ずタイムアウトが発生します。 ``` $ python3 quiz2.py Traceback (most recent call last): File "quiz2.py", line 33, in <module> main() File "quiz2.py", line 29, in main child = server_2.accept()[0] File "/usr/lib64/python3.7/socket.py", line 212, in accept fd, addr = self._accept() socket.timeout: timed out ``` #### 不正解...? 決して以下のように出力されることはありません。 ``` $ python3 quiz2.py b'Hello World' ``` ### クイズ 3 最後のクイズです。以下のスクリプト (`quiz3.py`) を実行した際、何が出力されるでしょうか。 `drop_ack(True)` は 3-way handshake (3WHS) の最後の ACK パケットのみを破棄します。したがってクライアントの視点では 3WHS が即座に完了するものの、サーバーの視点では 3WHS を完了する前に `server_1` が `close()` される点に注意してください。 ```python3:quiz3.py import socket import subprocess ADDR = '127.0.0.1' PORT = 80 def get_server(): s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) s.bind((ADDR, PORT)) s.listen(32) return s def get_client(): c = socket.socket() c.connect((ADDR, PORT)) return c def drop_ack(add=True): subprocess.run( 'iptables -{} INPUT -d {} -p tcp --dport {} ' '--tcp-flags SYN,ACK ACK -j DROP'.format( 'A' if add else 'D', ADDR, PORT ).split(" ") ) def main(): server_1 = get_server() drop_ack(True) client = get_client() client.send(b'Hello World') server_2 = get_server() server_1.close() drop_ack(False) server_2.settimeout(3) child = server_2.accept()[0] print(child.recv(1024)) if __name__ == '__main__': main() ``` #### 正解 今回も必ずタイムアウトが発生します。 ``` $ sudo python3 quiz3.py Traceback (most recent call last): File "quiz3.py", line 48, in <module> main() File "quiz3.py", line 44, in main child = server_2.accept()[0] File "/usr/lib64/python3.7/socket.py", line 212, in accept fd, addr = self._accept() socket.timeout: timed out ``` #### 不正解...? クイズ 2 と同じく、このように出力されることはありません。 ``` $ sudo python3 quiz3.py b'Hello World' ``` ## カーネルの挙動と問題点 クイズ 1 では `accept()` に成功・失敗する 2 通りの結果が得られました。これよりカーネルは `SO_REUSEPORT` が有効なソケットに対してコネクションをランダムに割り当てることが分かります。 `SO_REUSEPORT` が有効なソケットは `listen()` した時点でコネクションを割り当てる対象になり、 `close()` (または `shutdown()`) するとその対象から外れます。このおかげで単純にプロセスを増減させれば、簡単にサーバーをスケールさせることができます。また設定変更などが生じた際は、新しいプロセスを起動して古いプロセスを停止することで、簡単にリロードを行えます。 その一方、[オプション導入当時の LWN.net の記事](https://lwn.net/Articles/542629/)からも分かるように、コネクションを特定のソケットに割り当てる挙動によって問題が発生することが知られています。この問題は 2 つに分類でき、クイズ 2 と 3 でそれぞれを簡単にシミュレートしています。そしてそのいずれもがサーバーのリロードは実際は単純に古いソケットを `close()` するだけでは上手くいかないことを示しています。 ### accept() できるコネクション クイズ 2 では `connect()` を呼び出した時点でサーバーのソケットは `server_1` のみが存在するため、カーネルはコネクションを `server_1` に割り当てます。しかし、 `server_1` を `close()` すると RST が送信されてこのコネクションは破棄されてしまいます。 ``` $ sudo tcpdump -i lo -nn -t ... IP 127.0.0.1.55042 > 127.0.0.1.8000: Flags [S], seq 2468297767, win 65495, options [mss 65495,sackOK,TS val 2200745635 ecr 0,nop,wscale 7], length 0 IP 127.0.0.1.8000 > 127.0.0.1.55042: Flags [S.], seq 2618403070, ack 2468297768, win 65483, options [mss 65495,sackOK,TS val 2200745635 ecr 2200745635,nop,wscale 7], length 0 IP 127.0.0.1.55042 > 127.0.0.1.8000: Flags [.], ack 1, win 512, options [nop,nop,TS val 2200745635 ecr 2200745635], length 0 IP 127.0.0.1.55042 > 127.0.0.1.8000: Flags [P.], seq 1:12, ack 1, win 512, options [nop,nop,TS val 2200745635 ecr 2200745635], length 11 IP 127.0.0.1.8000 > 127.0.0.1.55042: Flags [.], ack 12, win 512, options [nop,nop,TS val 2200745635 ecr 2200745635], length 0 IP 127.0.0.1.8000 > 127.0.0.1.55042: Flags [R.], seq 1, ack 12, win 512, options [nop,nop,TS val 2200745635 ecr 2200745635], length 0 ``` ある程度ソケットプログラミングに馴染みのあるユーザーであればこの点を不思議に思うことはないかもしれません。詳しくは Part 2 で説明しますが、カーネルは 3WHS が完了したコネクションをサーバーのソケットの accept キューに入れます。そのソケットを `close()` すると、それに伴って accept キューも破棄されるため、 accept キューに入ったコネクションも同様に破棄されます。 この問題を回避するには、サーバーのソケットを `close()` する前に、失敗するまで `accept()` を繰り返し実行する必要があります。いわゆる Connection Draining です。 ### accept() できないコネクション クイズ 3 でも `connect()` を呼び出した時点ではサーバーのソケットは `server_1` のみが存在し、最終的には `server_1` の `close()` によって RST が送信されます。クイズ 2 と違うのは 3WHS が完了する前に `server_1` が `close()` される点です。 ``` $ sudo tcpdump -i lo -nn -t ... IP 127.0.0.1.55044 > 127.0.0.1.8000: Flags [S], seq 2794889430, win 65495, options [mss 65495,sackOK,TS val 2200808677 ecr 0,nop,wscale 7], length 0 IP 127.0.0.1.8000 > 127.0.0.1.55044: Flags [S.], seq 3423506731, ack 2794889431, win 65483, options [mss 65495,sackOK,TS val 2200808677 ecr 2200808677,nop,wscale 7], length 0 IP 127.0.0.1.55044 > 127.0.0.1.8000: Flags [.], ack 1, win 512, options [nop,nop,TS val 2200808677 ecr 2200808677], length 0 IP 127.0.0.1.55044 > 127.0.0.1.8000: Flags [P.], seq 1:12, ack 1, win 512, options [nop,nop,TS val 2200808677 ecr 2200808677], length 11 IP 127.0.0.1.55044 > 127.0.0.1.8000: Flags [P.], seq 1:12, ack 1, win 512, options [nop,nop,TS val 2200808882 ecr 2200808677], length 11 IP 127.0.0.1.8000 > 127.0.0.1.55044: Flags [R], seq 3423506732, win 0, length 0 ``` クイズ 2 から `close()` の前に `accept()` を繰り返し実行する必要があると学びました。しかし、実際に試してみると上手くいかないことがわかるでしょう。 `server_1` を `close()` していた時点では 3WHS が完了しておらず、 accept キューにコネクションがないためです。 これより、カーネルは SYN を受信した時点でコネクションを特定のソケットに割り当てていることが分かります。基本的にこの挙動はユーザースペースで認知できないため、 3WHS を完了していないコネクションはユーザーが知らないうちに破棄される可能性があります。 [v4.6 で追加された eBPF の機能](https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=fd1914b2901badd942f008ce57bf4a938d29fde4)を利用すればこの問題は回避できます。これも詳しくは Part 2 で扱いますが、 eBPF のプログラムを `SO_REUSEPORT` が有効なソケットにアタッチすると、コネクションを割り当てるソケットを選択できるようになります。逆に eBPF で特定のソケットにコネクションを割り当てないようにすることもできます。したがって、以下のように eBPF を利用して Connection Draining を実装することでこの問題を回避できます。 - eBPF でコネクションを割り当てる際にカウンタをインクリメントする - eBPF でコネクションを割り当てないようにする - カウンタの数まで `accept()` を繰り返す、または、全てのタイマーが切れるまで `accept()` を繰り返す ## 解決策 クイズ 2 と 3 ではいずれも `server_1` を `close()` する時点で `server_2` が存在します。同じ IP アドレスとポート番号へのコネクションを扱える `server_2` であれば破棄されるコネクションを `accept()` できるはずです。つまり、 `server_1` に一度割り当てられたコネクションを `server_2` に移植できれば問題は解決します。 同様の議論は過去にも[メーリングリスト](https://lore.kernel.org/netdev/1443313848-751-1-git-send-email-tolga.ceylan@gmail.com/)で行われていました。[ちょうどこの時期に eBPF で Connection Draining が可能になった](https://lore.kernel.org/netdev/1450492683.8474.123.camel@edumazet-glaptop2.roam.corp.google.com/)こともあり、 [TCP](https://lore.kernel.org/netdev/1458830744.10868.72.camel@edumazet-glaptop3.roam.corp.google.com/) と [eBPF](https://lore.kernel.org/netdev/20160325162114.GA72479@ast-mbp.thefacebook.com/) で改修の難しさが割りに合わないため、 [eBPF による Connection Draining が推奨されていました](https://lore.kernel.org/netdev/1458925242.6473.41.camel@edumazet-glaptop3.roam.corp.google.com/)。 ただし Connection Draining にも欠点があり、セキュリティなどの理由から即座にプロセスを置き換えたい場合に問題になります。例えば Connection Draining した HTTP リクエストが WebSocket に Upgrade した場合、終了したいプロセスが想定以上に長く動作し、結局プロセスを強制終了してコネクションを破棄する必要が出てくるかもしれません。もし Connection Draining を行わずに新しいプロセスに移植できていれば、このコネクションは破棄されなかったかもしれません。コネクションを移植したところで同じ問題が起きる可能性はありますが、 Connection Draining で破棄されるコネクションを減らすことはできるでしょう。 ### net.ipv4.tcp_migrate_req さて、いよいよパッチセットのお話です。 v5.14 からは新しい sysctl ノブ `net.ipv4.tcp_migrate_req` が追加されます。これを有効にするとカーネルが本来コネクションを破棄していたタイミングで、同じ IP アドレスとポート番号への通信を待ち受けるソケットをランダムに選択してコネクションを移植します。つまり、クイズ 2 と 3 で*真に*正しい結果を取得できるようになります。 ``` $ sudo sysctl -w net.ipv4.tcp_migrate_req=1 net.ipv4.tcp_migrate_req = 1 $ python3 quiz2.py b'Hello World' $ sudo python3 quiz3.py b'Hello World' ``` 一見素晴らしい機能ですが、もちろん注意すべき点があります。それが `net.ipv4.tcp_migrate_req` のデフォルト値を `0` にした理由です。 最終的に `accept()` で取得するソケット (クイズ中の `child`) で使用できる機能は、コネクションが最初に割り当てられたサーバーのソケットに依存するため、そこに差異が生じると思わぬエラーが発生するかもしれません。例えばサーバーのソケットに `setsockopt()` で `TCP_SAVE_SYN` を設定すると、割り当てられたコネクションの SYN のデータをカーネルが内部的に保持します。保持されたデータは `accept()` したソケットに対して `getsockopt()` で `TCP_SAVED_SYN` を指定することで取得できます。もし新しいプロセスでのみ `TCP_SAVE_SYN` が有効になっていた場合、古いプロセスから移植されてきたコネクションに対して `TCP_SAVED_SYN` でデータの取得を試みるも失敗するでしょう。 このように移植元と移植先のソケットで `setsockopt()` レベルの差異がある場合や、そもそも違うプロセス (nginx と squid など) を同じポートで動作させて eBPF でコネクションを振り分けている場合などは、[別途追加した eBPF の機能](https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/commit/?id=d5e4ddaeb6ab2c3c7fbb7b247a6d34bb0b18d87e)で移植先を選択するようにしてください。詳しくは Part 2 で扱います。 ## その他の回避策 最後に Connection Draining 以外の二つの回避策を紹介します。 1 つ目は Unix ドメインソケットで `SCM_RIGHTS` を使用する fd passing です。サーバーのソケットをプロセス間で引き継げばそもそも `close()` を呼び出すことはありません。ただし[ユースケース次第でそのロジックは複雑になる](https://lore.kernel.org/bpf/20210428012734.cbzie3ihf6fbx5kp@kafai-mbp.dhcp.thefacebook.com/)でしょう。 2 つ目が最も簡単な方法です。お気づきかも知れませんが、この問題の発生頻度はそこまで高くない上に、問題が発生する時間もとても短いです。 RST を受信するとクライアント側で `connect()` が失敗しますが、リトライで容易に問題は回避できます。実際、クライアント側が堅牢に実装されていれば私はこの問題に気づくことはなかったでしょう。 ## おわりに Part 1 では eBPF が必要となる特殊なユースケースを除いて、ユーザースペースで知っておくべき内容は概ね解説できたと思います。実際に使ってみてもしバグを発見した場合は `netdev@vger.kernel.org` と `bpf@vger.kernel.org` を CC に入れて私にメールを送って頂ければ幸いです。
Linux カーネルを開発する際、コードを書き換える度に、ビルド、インストール、再起動、テスト、というのはなかなか面倒です。この手間を出来るだけ省くために、私は [QEMU](https://www.qemu.org/) を利用しています。この記事では私がカーネル開発で使用する環境を構築する方法について紹介します。 ## 環境 普段は EC2 上で開発を行っています。特にディストリビューションにこだわりは無いのでなんとなく Amazon Linux 2 を選んでいます。 OS: Amazon Linux 2 ## 手順 ホームディレクトリ (~/) 配下で作業を行なっているので、必要に応じて書き換えてください。 ``` $ WORKDIR=~ ``` ### 1. カーネルのダウンロード git でカーネルのソースコードをダウンロードします。各サブシステムの最新のカーネルは [git.kernel.org](https://git.kernel.org/) からダウンロードできます。私はネットワークサブシステムの開発を行うために、 [netdev の net-next](https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/) を選びました。特に決まったものがなければ [linux-next](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/) を選ぶと良いでしょう。 ``` $ cd ${WORKDIR} $ sudo yum install -y git $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git ``` ### 2. カーネルのビルド QEMU で x86_64 をエミュレートするので、 x86_64 向けのデフォルトの設定を `.config` に書き込んでビルドしています。 `make menuconfig` で設定を書き換えることも可能ですが、その場合は `ncurses-devel` をインストールする必要があります。 ``` $ sudo yum install -y gcc flex bison elfutils-libelf-devel openssl-devel $ cd net-next $ make x86_64_defconfig $ make -j $(nproc) ``` ### 3. Buildroot のダウンロード QEMU でカーネルを起動する際、ルートファイルシステムが必要になります。基本的なコマンド群を含むルートファイルシステムを簡単に作成できるので、 [Buildroot](https://buildroot.org/) を使用します。 ``` $ cd ${WORKDIR} $ git clone git://git.buildroot.net/buildroot ``` ### 4. Buildroot のビルド ビルドするコマンドは以下の通りです。 `make menuconfig` でアーキテクチャや必要なコマンドを選択します。ビルドにはそこそこ時間がかかるので洗濯機を回して、 YouTube で期間限定で公開されている好きなアーティストの[ライブ映像](https://www.youtube.com/watch?v=YU-RGLuC-Ho)を流しながら、たまったお皿洗いでもすると良いかもしれません。 ``` $ sudo yum install -y ncurses-devel gcc-c++ patch perl-Data-Dumper perl-ExtUtils-MakeMaker perl-Thread-Queue $ cd buildroot $ make menuconfig $ make -j $(nproc) ``` QEMU で x86_64 をエミュレートするので、アーキテクチャは x86_64 を選択します。読み書き出来ればファイルシステムはなんでもいいと思いますが、昔から Ubuntu をよく使っていたので ext4 を選びました。 Btrfs の開発をしたい方は btrfs を選びましょう。 - Target options - Target Architecture - x86_64 を選択 - Filesystem images - ext2/3/4 root filesystem にチェックを入れる - ext2/3/4 variant - ext4 を選択 複数セッションでコマンドを実行したい場面は良くあると思うので、 openssh は選択した方が良いでしょう。その他には curl, iptables, nginx, tcpdump, python3 を選択しました。 - Toolchain - C library - glibc を選択 - Install glibc utilities にチェックを入れる - Target packages - Interpreter languages and scripting - python3 にチェックを入れる - core python3 modules - ssl にチェックを入れる - Libraries - Crypto - openssl support - ssl library - openssl を選択 - openssl - openssl binary にチェックを入れる - openssl additional engines にチェックを入れる - Networking - libcurl にチェックを入れる - curl binary にチェックを入れる - SSL/TLS library to use - OpenSSL を選択 - Networking applications - iptables にチェックを入れる - nginx にチェックを入れる - ngx_http_ssl_module にチェックを入れる - openssh にチェックを入れる - tcpdump にチェックを入れる ### 5. DHCP の設定 外部向けの通信をするためには IP アドレスの設定が必要になります。 QEMU でカーネルを起動する時に `-netdev` オプションで特に何も指定しない場合、 QEMU のビルトイン DHCP サーバーを利用できます。([参考](https://wiki.archlinux.org/index.php/QEMU#User-mode_networking)) Buildroot で作成したルートファイルシステム (`${WORKDIR}/buildroot/output/images/rootfs.ext4`) 内の、 `/etc/network/interfaces` に DHCP の設定があります。 ``` # interface file auto-generated by buildroot auto lo iface lo inet loopback ``` デフォルトではループバックインターフェイス (lo) のみ設定されており、このままでは外部宛の通信ができないので、 eth0 に IP アドレスが割り当てられるように以下の 2 行を書き加える必要があります。 ``` auto eth0 iface eth0 inet dhcp ``` 以下のコマンドで Buildroot で作成したルートファイルシステムをマウントして設定を追記します。 ``` $ sudo mkdir /mnt/buildroot $ sudo mount -o loop ${WORKDIR}/buildroot/output/images/rootfs.ext4 /mnt/buildroot $ echo -e '\nauto eth0\niface eth0 inet dhcp' | sudo tee -a /mnt/buildroot/etc/network/interfaces ``` ### 6. SSH の設定 Buildroot ではデフォルトユーザーが root で、パスワードも設定されていません。手元の OS であればちゃんと別のユーザーで公開鍵認証の設定をするところですが、 QEMU 上のテスト環境なのでパスワードなしで root にログインできるようにします。そのために `/etc/ssh/sshd_config` で以下の設定に書き換えます。 ``` PermitRootLogin yes PermitEmptyPasswords yes ``` 既にマウントしているファイルシステム内の `/etc/ssh/sshd_config` を書き換えます。(かっこつけて sed コマンドを使用していますが、初めは emacs で書き換えてました。) ``` $ sudo sed -i -e 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' \ -e 's/#PermitEmptyPasswords no/PermitEmptyPasswords yes/' \ /mnt/buildroot/etc/ssh/sshd_config $ sudo umount /mnt/buildroot ``` ### 7. QEMU でカーネルを起動 QEMU をインストールして、ちょっと長いコマンドでカーネルを起動します。カーネルのツリーや、ファイルシステムに応じて少しコマンドが変わるので、適宜変更してください。このコマンドではホストの 10022 番ポートを QEMU で起動したカーネルの 22 番ポートへフォワードするようになっています。 ``` $ sudo yum install -y qemu $ qemu-system-x86_64 -boot c -m 2048M \ -kernel ${WORKDIR}/net-next/arch/x86/boot/bzImage \ -hda ${WORKDIR}/buildroot/output/images/rootfs.ext4 \ -append "root=/dev/sda rw console=ttyS0,115200 nokaslr" \ -serial stdio -display none \ -nic user,hostfwd=tcp::10022-:22 ``` ユーザー名は root でパスワードなしでログインできます。 ``` Welcome to Buildroot buildroot login: root # uname -r 5.8.0-rc1 ``` 別のターミナルを開いて SSH でもログインできます。 ``` $ ssh root@localhost -p 10022 ... # curl -kI https://kuniyu.jp HTTP/1.1 200 OK ... ``` 終了するには QEMU を起動したターミナルで Ctrl+C を押してください。 ``` # (Ctrl+C を押す) qemu-system-x86_64: terminating on signal 2 ``` ### 8. Alias の設定 QEMU のコマンドは当然覚えられませんし、毎回コピペして実行するのも面倒です。なのでエイリアスを登録して短いコマンドで実行出来るようにしています。私は `net-next` コマンドでカーネルを起動できるようにしています。 ``` $ echo "alias net-next='qemu-system-x86_64 -boot c -m 2048M -kernel ${WORKDIR}/net-next/arch/x86/boot/bzImage -hda ${WORKDIR}/buildroot/output/images/rootfs.ext4 -append \"root=/dev/sda rw console=ttyS0,115200 nokaslr\" -serial stdio -display none -nic user,hostfwd=tcp::10022-:22'" >> ~/.bashrc $ source ~/.bashrc ``` ## おわりに カーネル開発を始めた半年ちょっと前、コードを書き換えてはビルドして、、、と行なっていたのですが、 GRUB の設定を間違えたり、間違ったコードを書いて EC2 がお釈迦になることが多々ありました。自分と同じようにカーネルに興味を持った方が同じ悲しみを味合わないよう、少しでも参考になれば幸いです。