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が腐る」なので、ここでは「コードベースを壊す」ぐらいの感じだろうか

JEP 310: Application Class-Data Sharingをテキトーに訳した

https://openjdk.java.net/jeps/310

JEP 310: Application Class-Data Sharing

Owner    Ioi Lam
Type    Feature
Scope   Implementation
Status  Closed / Delivered
Release 10
Component   hotspot / runtime
Discussion  hotspot dash dev at openjdk dot java dot net
Reviewed by Karen Kinnear, Mikael Vidstedt, Vladimir Kozlov
Endorsed by Mikael Vidstedt, Vladimir Kozlov
Created 2017/08/08 22:02
Updated 2018/08/17 20:35
Issue   8185996

Summary

起動時間とフットプリント改善目的で、既存のClass-Data Sharing ("CDS")でアプリケーションクラスを共有アーカイブ内に配置可能なように拡張します。

Goals

  • 異なるJavaプロセス間で共通のクラスメタデータを共有する事によりフットプリントを削減。
  • 起動時間の改善。
  • JDKランタイムイメージファイル($JAVA_HOME/lib/modules)のアーカイブクラスとプラットフォームおよびシステムクラスローダにロードされるアプリケーションクラスパスを使えるようにCDSを拡張。
  • アーカイブクラスをカスタムクラスローダにロード出来るようにCDSを拡張。

Non-Goals

  • 本実装で使用する共有クラスのアーカイブストレージフォーマットの標準化はしない。
  • 本リリースでは、CDSはユーザ定義モジュール(ex. --module-pathで定義する)のクラスはアーカイブしない。将来リリースでサポート予定。

Success Metrics

本プロジェクトは、(1)複数JVMプロセス間でJavaのクラスメタデータが使用するメモリの著しい削減(2)スタートアップ時間の著しい改善、が達成出来れば成功と考えられます。

実際に適用例としては、

上記の数値は特定のベンチマーク下であり一般的に言えるとは限りません。クラスローダがロードするクラス数およびアプリケーション全体のヒープ使用量に依存します。

Description

JDK 5で導入したClass-Data Sharingは事前にクラスを共有アーカイブにすることで起動時間改善のため実行時にメモリマッピングします。また、複数JVMが同一アーカイブファイルを共有する場合、動的にメモリフットプリントを削減します。

現行のCDSアーカイブクラスのロードにはブートストラップクラスローダのみ使用可能です。Application CDS ("AppCDS")はCDSを拡張し、ビルトインシステムクラスローダ(別名"app class loader")・ビルトインプラットフォームクラスローダ・アーカイブクラスロード用のカスタムクラスローダ、を使用可能です。

大規模エンタープライズアプリケーションのメモリ使用分析によると、アプリケーションクラスローダに数万クラスをロードします。こうしたアプリケーションにAppCDSを適用するとJVMプロセスごとに数十から数百MBを削減可能です。

サーバレスクラウドサービスの分析によると、起動時に数千アプリケーションクラスをロードします。AppCDSでサービスの起動時間短縮とシステムレスポンスタイム全体を改善できます。

Enabling AppCDS

デフォルトでは、Class-Data SharingはJVMのブートストラップクラスローダでだけ有効化されています。-XX:+UseAppCDSコマンドラインオプション指定により、システムクラスローダ(別名"app class loader")・プラットフォームクラスローダ・その他のユーザ定義クラスローダ、でクラスデータ共有を有効化します。

Determining the classes to archive

多数のクラスでアプリケーションをパッケージしても、通常の実行ではそれらの一部のみしか使用しない事が多いです。そうした使用するクラスのみのアーカイブ化により、ファイルストレージサイズと実行時のメモリ使用量を削減します。これを行うには、まず-Xshare:offでアプリケーションを通常実行する際に-XX:DumpLoadedClassListでロードされる全クラスを記録します。

注意点としてデフォルトでは-XX:DumpLoadedClassListはブートストラップクラスローダがロードするクラスのみ含みます。-XX:+AppCDSオプション指定により、システムクラスローダ・プラットフォームクラスローダがロードするクラスも含むようになります。以下は例です。

java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst -cp hello.jar HelloWorld

Creating the AppCDS archive

AppCDSアーカイブを生成するには、-Xshare:dump -XX:+UseAppCDSオプションを指定し、-XX:SharedClassListFileにクラスのリストを指定し、アプリケーションで使用するのと同じクラスパスを指定します。また、-XX:SharedArchiveFileにクラスを格納するアーカイブファイル名を指定します。注意点として、-XX:SharedArchiveFileが未指定の場合、アーカイブされるクラスはJDKインストールディレクトリに保存されます。

$ java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=hello.lst \
    -XX:SharedArchiveFile=hello.jsa -cp hello.jar

Using the AppCDS archive

AppCDSアーカイブを生成したら、アプリケーション開始時にそれを使用します。-Xshare:on -XX:+UseAppCDSオプションと、アーカイブファイル名を指定する-XX:SharedArchiveFileを指定して実行します。

$ java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \
    -cp hello.jar HelloWorld

Classpath mismatch

-Xshare:dumpのクラスパスは、-Xshare:onのクラスパスと同一もしくはプレフィクスの必要があります。そうでない場合、JVMはミスマッチのクラスパスのエラーメッセージを表示して停止します。ミスマッチの調査には、-Xlog:class+path=infoを追加すると、JVMは期待されるクラスパスと実際に使用されたクラスパスに関する詳細な診断情報を出力します。

Using -Xshare:auto

AppCDSの動作は固定アドレスにアーカイブをメモリマッピングします。ある種のOSでは、address space layout randomization (ASLR)が有効の場合には特に、要求アドレス空間が利用不可能な場合にメモリマッピング操作が失敗することがあります。-Xshare:onを指定する場合、JVMはこれをエラーとして扱い起動を失敗します。この場合にアプリケーションを柔軟に対処させるには、代わりに-Xshare:autoを推奨します。この場合、JVMアーカイブのメモリマッピングに失敗すると、AppCDSを無効化してアプリケーションを通常実行します。

なお、-Xshare:autoはクラスパスのミスマッチの場合にもAppCDSを無効化します。よって、まずミスマッチが無いことを-Xshare:onでテストしてから本番環境で-Xshare:autoにするのを推奨します。

Listing the Classes Loaded from the AppCDS Archive

AppCDSアーカイブからロードされたクラスを参照するには-Xlog:class+load=infoを使います。ロードされたクラス名とどこからロードされたかを表示します。CDSアーカイブからロードされたクラスはsource: shared objects fileの形で表示します。

$ java -Xshare:on   -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \
    -cp hello.jar -Xlog:class+load=info HelloWorld | grep HelloWorld
[0.272s][info][class,load] HelloWorld source: shared objects file

Implementation

  • Platform and system class loaders: HotSpot VMはプラットフォームとシステムクラスローダのクラスロード要求を解釈します。これらローダーがCDSアーカイブのクラスを要求すると、VMは通常のクラスファイルパースおよび検証をスキップし、アーカイブのコピーをロードします。
  • Custom class loaders: カスタムクラスローダがClassLoader::defineClassを呼ぶと、アーカイブクラスとクラスファイルのマッチングを、クラスファイルデータのフィンガープリントでマッチングを試行します。もしマッチする場合、VMはクラスファイルのパースおよび検証をスキップし、アーカイブのコピーを直接ロードします。

Alternatives

複数のJVMプロセスが動的にロードするクラスの共有に共有メモリ領域を検討しましたが、共有のポテンシャルは低く*1実装困難なことが分かりました。

その代わりに、アプリケーションクラスデータの共有をstaticにしました。

  • 'dump'のひと手間が追加。
  • アプリケーションのJARファイルを更新すると再度dumpが必要。

本機能は既存のCDS上に構築するため、実装をシンプルにして、想定ユースケースにおいて高レートの共有を実現します。

Testing

互換性の保証とパフォーマンスを確認する網羅的なテストが必要です。

テストはすべてのサポートプラットフォームで実行します。ある種のプラットフォーム(特にWindows/x86)では、Address Space Layout Randomization (ASLR)が理由でJVMアーカイブマッピングが出来ずにテストが失敗する可能性があります。

Risks and Assumptions

AppCDSはJDK 8とJDK 9のOracle JDKで実装されています。本JEPではそのソースコードをオープンなリポジトリに移行し、一般利用可能にします。AppCDSはJDK 8とJDK 9で網羅的なテストをしているため、互換性と安定性のリスクは低いです。

*1:the sharing potential to be lower

OracleからLogstashでElasticsearchにデータおくる

OracleからLogstashを経由してElasticsearchにデータを追加する。Oracleのtimestampカラムを使用して、Logstashが定期的に前回以降のデータを取得して、Elasticsearchのインデックスに追加する。KIbanaはデータ確認用としてのみ使用する。

なお、環境構築はdockerで行うのでELK Stackとは直接的に関係無い設定がこのエントリには含まれる。また、ホスト側はWIndows 10な点に注意。

手順

dockerのネットワーク作成

それぞれ異なるコンテナで動かすため、通信設定が必要。方法は幾つかあるけど、ここではあらかじめネットワークを作っておくやり方にした。

docker network create --driver bridge common_link

参考: https://qiita.com/reneice/items/20e981062b093264cd0a

Oracle Database 18c XE

http://kagamihoge.hatenablog.com/entry/2018/12/28/204636 を参考にdockerでOracle Database 18c XEをうごかす。なお、上記リンク先と異なる点として、docker-compose.ymlにネットワーク設定を追加している。修正後のファイルは以下の通り。

version: '3'
services:
  database:
    image: oracle/database:18.4.0-xe
    volumes:
      - C:\mydata\oracle\oradata:/opt/oracle/oradata
    ports:
      - 11521:1521
      - 18080:8080
      - 15500:5500
    environment:
      - ORACLE_PWD=oracle
    networks:
      - common_link
networks:
    common_link:
        external: true

Logstashが参照するテーブル定義は以下の通り。

CREATE TABLE LOGSTASH_SAMPLE (
  "ID"                 NUMBER(10, 0) NOT NULL,
  "TIMESTAMPE_VALUE"   TIMESTAMP(6) default current_timestamp NOT NULL,
  constraint "LOGSTASH_SAMPLE_PK" PRIMARY KEY("ID")
);

ELK Stackのdocker-compose.xml

Elasticsearch, Logstash, Kibanaのdocker-compose.xmlを作成する。

version: '3'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.0.1
    ports:
      - 9200:9200
      - 9300:9300
    volumes:
      - ./elasticsearch/data:/usr/share/elasticsearch/data
    networks:
      - common_link
    environment:
      - discovery.type=single-node
  logstash:
    image: docker.elastic.co/logstash/logstash-oss:7.0.1
    container_name: logstash
    volumes:
      - ./logstash/pipeline:/usr/share/logstash/pipeline
      - ./logstash/log:/usr/share/logstash/logs
      - ./logstash/jdbc:/opt/jdbc
      - ./logstash/metadata/:/usr/share/logstash/metadata
    depends_on:
      - elasticsearch
    networks:
      - common_link
  kibana:
    image: docker.elastic.co/kibana/kibana-oss:7.0.1
    ports:
      - 5601:5601
    depends_on:
      - elasticsearch
    networks:
      - common_link
networks:
    common_link:
        external: true
  • networks ですべてのコンテナを同一ネットワークに入れておく。また、Logstashのところで後述するがこれによってホスト名をdatabaseとかelasticsearchとかで参照可能になる。

Logstash

Logstashの設定について。まず、上記docker-compose.ymlのlogstashのvolumesでマウントしてるディレクトリについて。

  • logstash
    • jdbc - oracleに接続するためのJDBCドライバを配置。
    • metadata - timestampカラムに対する最終取得時刻を保存するファイルをここに自動保存する
    • pipeline - パイプラインと呼ばれる処理設定ファイルを配置。

パイプライン設定ファイル

logstashでは、パイプラインと呼ばれる処理設定ファイルを記述する。このエントリではlogstash/pipeline/sample.confファイルを作成する。

input {
  jdbc {
    jdbc_driver_library => "/opt/jdbc/ojdbc7.jar"
    jdbc_driver_class => "Java::oracle.jdbc.driver.OracleDriver"
    jdbc_connection_string => "jdbc:oracle:thin:@database:1521/XEPDB1"
    jdbc_user => "system"
    jdbc_password => "**********"
    schedule => "* * * * *"
    statement_filepath => "/usr/share/logstash/pipeline/sql/sample.sql"
    tracking_column => timestampe_value
    tracking_column_type  => "timestamp"
    use_column_value => true
    jdbc_default_timezone => "Japan"
    last_run_metadata_path=> "/usr/share/logstash/metadata/last_run_metadata.txt"
    clean_run => true
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "sample_data"
    document_id => "sample_%{id}"
  }
}
  • jdbc_driver_class - かなりハマったが、Java::oracle.jdbc.driver.OracleDriver"と、プレフィクスにJava::と書かなければならない。
  • jdbc_connection_string - 前述の通りdockerのネットワーク設定をしてあるので、ここではdatabaseというホスト名でoracleにアクセスできる。
  • statement_filepath - データ取得に使うSQLファイルを指定。ここではファイルに外出ししているが、直接各ことも可能。ドキュメント参照。
  • tracking_column - データ取得時に参照するカラムを指定する。timestampe以外も使えるのでその辺はドキュメントを参照。
  • last_run_metadata_path - ここでは最終取得時刻を保存するファイルを指定。
  • clean_run - 開発用に毎回クリーン実行するためtrueにしている。

データ取得用sql

select id, timestampe_value from logstash_sample where timestampe_value > :sql_last_value
  • :sql_last_value - last_run_metadata_pathに保存してある値をこれで参照できる。これによって、logstashがこのSQLを実行するたびに最終取得時刻以降のデータを差分取得してくる。

last_run_metadata_pathの最終取得時刻保存ファイル

ここでは最終取得時刻が保存される。中身はこんな感じ。

--- !ruby/object:DateTime '2019-05-03 17:10:45.559465000 +09:00'

実行

この状態でELK stackのdockerを起動する。正常動作すれば、logstashが定期的にoracleからelasticsearchにデータを出力するログが流れる。

logstash         | [2019-05-06T02:58:02,743][INFO ][logstash.inputs.jdbc     ] (0.144195s) select id, timestampe_value from logstash_sample where timestampe_value > TIMESTAMP '1970-01-01 09:00:00.000000 +09:00'
logstash         | [2019-05-06T02:59:00,376][INFO ][logstash.inputs.jdbc     ] (0.006701s) select id, timestampe_value from logstash_sample where timestampe_value > TIMESTAMP '2019-05-03 17:10:45.559465 +09:00'
logstash         | [2019-05-06T03:00:00,170][INFO ][logstash.inputs.jdbc     ] (0.000849s) select id, timestampe_value from logstash_sample where timestampe_value > TIMESTAMP '2019-05-03 17:10:45.559465 +09:00'