kagamihogeの日記

kagamihogeの日記です。

JEP 353: Reimplement the Legacy Socket APIをテキトーに訳した

http://openjdk.java.net/jeps/353

JEP 353: Reimplement the Legacy Socket API

Owner    Alan Bateman
Type    Feature
Scope   JDK
Status  Proposed to Target
Release 13
Component   core-libs / java.net
Discussion  net dash dev at openjdk dot java dot net
Effort  S
Reviewed by Brian Goetz, Chris Hegarty, Michael McMahon
Endorsed by Brian Goetz
Created 2019/02/06 13:49
Updated 2019/05/16 19:55
Issue   8218559

Summary

java.net.Socketおよびjava.net.ServerSocket APIで使われる基底実装のリプレース。メンテナンスとデバッグがしやすいシンプルで近代的な実装にします。新規実装は、ユーザモードスレッドいわゆるfibersを使いやすいようにします。Project Loomを参照してください。

Motivation

java.net.Socketjava.net.ServerSocket APiとその基底実装はJDK 1.0の時代から存在します。この実装はレガシーJavaとCのミックスでメンテナンスとデバッグが極めて困難です。I/Oバッファーにスレッドスタックを使用しており、この方法ではデフォルトのスレッドスタックサイズを数回増やす必要がありました。非同期closeにネイティブデータ構造を使用しており、厄介な信頼性と移植性の問題が長年続いています。また、コンカンレンシーの問題もあり適切に対処するには全体的な改修が必要です。ネイティブメソッドでスレッドをブロックする代わりにfiberを将来的に使用する事を想定すると、現行実装ではフィットしません。

Description

java.net.Socketjava.net.ServerSocket APIはすべてのソケット操作をjava.net.SocketImplにデリゲートし、こうしたService Provider Interface (SPI)はJDK 1.0から存在しています。これの組み込み実装は“plain”実装と呼ばれており、非publicのPlainSocketImplとそのサポートクラスSocketInputStreamSocketOutputStreamがそれです。PlainSocketImplは2つのJDK内部実装が拡張しており、SOCKSとHTTPプロキシサーバ経由のコネクションを行います。デフォルトでは、SocketServerSocketはSOCKSベースのSocketImplで生成(場合により遅延生成)します。ServerSocketの場合、SOCKS実装の使用は奇妙な事であり、これはJDK 1.4のプロキシーサーバコネクションの実験的サポート(と削除)にさかのぼります*1

新実装NioSocketImplPlainSocketImpldrop-in replacementです。メンテナンスとデバッグをしやすくするために開発します。New I/O (NIO)などJDK内部機能を使用し、ネイティブコードを無くします。既存のバッファキャッシュ機能と統合し、I/Oにスレッドスタックを使う必要性を無くします。synchronizedメソッドではなくjava.util.concurrentのロックを使用し、将来的にfibersを使用できるように備えます。JDK 11のNIO SocketChannelとその他のSelectableChannel実装の再実装は同じ目標の下に実施しました。

以下は新規実装のポイントです。

  • SocketImplはレガシーSPIで極めてunder-specifiedです。新規実装は旧実装との互換性維持を、unspecifiedな振る舞いと該当する例外(exceptions where applicable)のエミュレートで行います。以下のRisks and Assumptionsセクションで新旧実装間の振る舞いの差の詳細を説明します。
  • タイムアウトを使うSocket操作(connect, accept, read)は、ソケットをノンブロッキングモードとソケットポーリングに変更して実装します。
  • java.lang.ref.CleanerSocketImplGC時にソケットをクローズするのに使用し、ソケットの明示的クローズは行っていません。
  • コネクションリセットハンドリングは旧実装と同様な方法で実装し、コネクションリセット後にreadしようとすると一貫して失敗します。

ServerSocketはデフォルトではNioSocketImpl(もしくはPlainSocketImpl)を使うよう修正します。SOCKS実装は使いません。

SOCKSとHTTPプロキシサーバのSocketImpl実装はデリゲートするよう修正し、新旧実装どちらでも動かせるようにします。

Java Flight RecorderのソケットI/O用のinstrumentation supportはSocketImplと独立するよう修正し、新・旧・カスタム実装のいずれの実行時にもソケットI/Oイベントを記録可能にします。

20年後に実装を切り替える際のリスク軽減のため、旧実装は削除しません。旧実装はJDKに残して、旧実装を使うためのJDKシステムプロパティを導入します。旧実装に切り替えるJDKシステムプロパティはjdk.net.usePlainSocketImplです。これを設定、ないしtrueに設定すると、起動時に旧実装を使用するようになります。将来リリースでPlainSocketImplシステムプロパティを削除する予定です。

本JEPの時点ではDatagramSocketImplの代替実装は提供しません(DatagramSocketImpljava.net.DatagramSocketがデリゲートする基底実装です)。組み込みデフォルト実装(PlainDatagramSocketImpl)はメンテナンス(と移植)の負荷が高く、別のJEPで扱う予定です。

Testing

jdk/jdkリポジトリの既存テストを新規実装に使用します。jdk_netテストグループでは長年にわたりネットワーキングにおける多数のコーナーケースの蓄積があります。テストグループのいくつかのテストは2回実行するよう修正し、2回目では-Djdk.net.usePlainSocketImplJDKが新旧実装を両方含む場合でも旧実装がbit-rot*2しないことを確認します。

今日では、java.net.Socketjava.net.ServerSocketでなくjava.nio.channelsを使うライブラリを直接・間接に使用するコードが多くあります。本提案の周知を行い、SocketServerSocketを使っているコードをearly-accessビルドでテストする事を開発者に奨励するような活動を行う予定です。early-accessビルドはjdk.java.netかどこかで公開します。

jdk/jdkリポジトリのマイクロベンチマークにはソケットのread/writeとストリーミング用のテストがあります。これらベンチマークは新旧実装間の比較を簡単に出来るよう改良が加えられてきました。現状、ソケットread/writeテストにおいて、新規実装は旧実装に比べて同じか1-3%良くなっています。

Risks and Assumptions

本提案の第一のリスクは、新旧実装で異なる振る舞いをするコーナーケースにおいてunspecified behaviorに依存する既存コードです。現在認識してえる相違のリストは以下の通りで、最初の2つ以外は-Djdk.net.usePlainSocketImplにより軽減できます。

  • PlainSocketImplgetInputStream()getOutputStream()が返すInputStreamOutputStreamは、それぞれjava.io.FileInputStreamjava.io.FileOutputStreamを拡張しています。これに依存する既存コードは、理論的にはありえますが、可能性としては低いです。
  • カスタムのSocketImplを使用するServerSocketは、プラットフォームのSocketImplを使うSocketを返すコネクションは使えません。同様に、プラットフォームのSocketImplを使うServerSocketは、カスタムのSocketImplを使うSocketを返すコネクションを使えません。
  • 旧実装が返すInputStreamOutputStreamはストリームのEOFをチェックして他のチェック前に-1を返します。新規実装はストリームEOFチェック前にnullと境界チェックをします。チェック順序に依存する壊れやすい実装は、理論的にはありえますが、可能性としては低いです。
  • Java SE 9でSocketServerSocketに導入されたsetOption(SocketOption<T>, T value)は、ソケットオプションの値が非validな場合にIllegalArgumentExceptionをスローします(例:ソケットバッファサイズに負の値を指定)。旧実装は間違った例外をスローする場合があります。新規実装は期待通りの例外をスローするものの、既存コードが現在とは異なる方法で失敗する可能性があります。
  • Oracle Solaris specific: Oracle Solarisはアプリケーションに"connection reset"を伝える方法が他のプラットフォームと異なります。例えば、setsockoptioctlはネットワークエラーで失敗する可能性があります。/etc/systemxnet_skip_checksを設定することでこの振る舞いを無効化できます(echo "xnet_skip_checks/W 1" | mdb -kw)。The old implementation handles the case where ioctl(FIOREAD) fails so that attempts to read after available fails with a “connection reset” will fail consistently. This is fragile and unmaintainable, the new implementation does not attempt to emulate this behavior.
  • Oracle Solaris specific: Oracle Solarisは接続後にTCPソケットにIPV6_TLCASSソケットオプションを変更出来ません。旧実装はsetTrafficClassの指定値をキャッシュすることでこれをマスクしています。
  • java.netパッケージにはSocketExceptionのサブクラスが多数存在します。新規実装は旧実装と同じSocketExceptionをスローしますが、そうでないものもあります。また、例外メッセージが異なるケースも多数あります。例えばMicrosoft Windowsでは、旧実装はWindows Socketエラーコードを英語オンリーのメッセージに変換していましたが、新規実装はシステムメッセージを使用します。

振る舞いの相違とは別に、新規実装のパフォーマンスは特定のワークロード実行時に旧実装と異なる可能性があります。旧実装では、ServerSocketacceptを呼ぶ複数のスレッドがカーネルにキューします。新規実装では、単一スレッドがacceptシステムコールをブロックし、それ以外はjava.util.concurrentのロックを得るためにqueue waitingします。パフォーマンス特性はこれ以外の場合にも起こりえます。

最後に、instrumentation agentsやツールでI/Oイベント取得に非publicのjava.net.SocketInputStreamjava.net.SocketOutputStreamクラスを呼ぶものがあります。これらのクラスは新規実装では利用できません。

*1: the use of the SOCKS implementation is an oddity that dates back to experimental (and since removed) support for proxying server connections in JDK 1.4. が原文。あんま訳に自信無し

*2:ぐぐってみると直訳すれば「bitが腐る」なので、ここでは「コードベースを壊す」ぐらいの感じだろうか