kagamihogeの日記

kagamihogeの日記です。

JEP 346: Promptly Return Unused Committed Memory from G1をテキトーに訳した

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

JEP 346: Promptly Return Unused Committed Memory from G1

Authors  Rodrigo Bruno, Thomas Schatzl, Ruslan Synytsky
Owner   Thomas Schatzl
Type    Feature
Scope   Implementation
Status  Proposed to Target
Release 12
Component   hotspot/gc
Discussion  hotspot dash gc dash dev at openjdk dot java dot net
Effort  M
Duration    S
Reviewed by Mikael Vidstedt, Stefan Johansson
Endorsed by Vladimir Kozlov
Created 2018/05/30 14:23
Updated 2018/11/29 22:49
Issue   8204089

Summary

アイドル時にOSへJavaヒープメモリを自動返却するようにG1 GCを拡張します。

Non-Goals

  • コミット済みで空のページをJavaプロセス間で共有する。メモリがOSに返却(未コミット)される。
  • メモリ返却処理がCPUリソースを節約する必要は無く、瞬時に終わる必要も無い。
  • 利用可能な未コミットメモリ以外のメモリを返却するのに別のメソッドを使用する。
  • G1以外のコレクターのサポート。

Success Metrics

アプリケーションがほぼ活動していない状況で、G1が妥当な時間内に未使用Javaヒープメモリを解放する。

Motivation

現行のG1 GCはOSにコミット済Javaヒープメモリをタイムリーに返さない場合があります。G1はfull GCかconcurrent cycleのどちらかでだけJavaヒープからメモリを返却します。G1はfull GCを完全回避する努力をし、concurrent cycleはJavaヒープ占有率とアロケーションアクティビティだけをベースにトリガされるので、外部から強制的に行わない限り大半のケースでJavaヒープメモリを返却しません。

この振る舞いが特に欠点となるのはリソース使用分に課金されるコンテナ環境です。VMが不活性中で割り当てメモリリソースの一部のみしか使用してない状況であっても、G1はすべてのJavaヒープを保持します。その結果として常に全リソースに対する支払をすることになり、また、クラウドプロバイダーでそのようなハードウェアを完全に用意することは出来ません

仮に、VMJavaヒープが不使用(アイドル)状態を検出可能で、自動的にその間はヒープの使用を減少させれば、クラウド提供者・使用者双方にメリットがあります。

ShenandoahおよびGenCon collectorが既に類似の機能を提供しています。

Bruno et al., section 5.5でHTTPリクエストを提供するTomcatサーバの実際の使用を基にしたプロトタイプテストを公開しています。日中は普通に使用して夜間の大部分はアイドルするもので、Java VMのコミット済みメモリ総量の85%を減少可能としています。

Description

OSに最大限のメモリを返却するため、アプリケーションが非アクティブのとき、G1の継続を定期的に試行するか、Javaヒープの使用を全体的に確認するconcurrent cycleを試行します。これにより、Javaヒープの未使用部分がOSに自動返却されます。または、ユーザが制御する場合、フルGCでメモリを最大限返却できます。

アプリケーションが非アクティブと見なされると、G1は以下を両方満たす場合にperiodic garbage collectionをトリガします。

  • 以前に何らかのGCが停止してG1PeriodicGCIntervalミリ秒よりも経過し、かつ、その時点でconcurrent cycleが実行中では無い場合。ゼロは即時メモリ回収のperiodic garbage collectionsがdisabledになります。
  • ホストシステムのgetloadavg()が返すロードアベレージG1PeriodicGCSystemLoadThresholdを上回る場合。G1PeriodicGCSystemLoadThresholdがゼロの場合無視される。

上記条件のいずれかを満たさなくなった場合、その時点でのperiodic garbage collectionの予定はキャンセルされます。periodic garbage collectionはG1PeriodicGCInterval経過後に再チェックされます。

periodic garbage collectioのタイプはG1PeriodicGCInvokesConcurrentオプションで決定します。設定している場合、G1は継続するかconcurrent cycleを開始し、設定しない場合、G1はフルGCを実行します。いずれのコレクション終了時にも、G1は現在のJavaヒープサイズを調整し、場合によってはOSにメモリ返却をします。その後のJavaヒープサイズはヒープ設定で決定され、これにはMinHeapFreeRatio, MaxHeapFreeRatio, 最小・最大ヒープサイズ設定などがあります。

デフォルトでは、G1が開始するかperiodic garbage collection中concurrent cycleを継続します。この場合アプリケーションの混乱は最小限ですが、full collectionと比較するとより多くのメモリ返却を結局は出来ない可能性があります。

本機能で起動するgarbage collectionはG1 Periodic Collectionの発生としてタグ付けされます。ログの見え方の例は以下になりまs。

(1) [6.084s][debug][gc,periodic ] Checking for periodic GC.
    [6.086s][info ][gc          ] GC(13) Pause Young (Concurrent Start) (G1 Periodic Collection) 37M->36M(78M) 1.786ms
(2) [9.087s][debug][gc,periodic ] Checking for periodic GC.
    [9.088s][info ][gc          ] GC(15) Pause Young (Prepare Mixed) (G1 Periodic Collection) 9M->9M(32M) 0.722ms
(3) [12.089s][debug][gc,periodic ] Checking for periodic GC.
    [12.091s][info ][gc          ] GC(16) Pause Young (Mixed) (G1 Periodic Collection) 9M->5M(32M) 1.776ms
(4) [15.092s][debug][gc,periodic ] Checking for periodic GC.
    [15.097s][info ][gc          ] GC(17) Pause Young (Mixed) (G1 Periodic Collection) 5M->1M(32M) 4.142ms
(5) [18.098s][debug][gc,periodic ] Checking for periodic GC.
    [18.100s][info ][gc          ] GC(18) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 1.685ms
(6) [21.101s][debug][gc,periodic ] Checking for periodic GC.
    [21.102s][info ][gc          ] GC(20) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.868ms
(7) [24.104s][debug][gc,periodic ] Checking for periodic GC.
    [24.104s][info ][gc          ] GC(22) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.778ms

上記例では、3000msのG1PeriodicGCIntervalを実行し、step(1)でG1はconcurrent cycleを初期化しており、(Concurrent Start)(G1 Periodic Collection)がそれを示していて、アプリケーションが何らかで非アクティブになった後にこうなります。このconcurrent cycleは最初にいくらかのメモリを返却しており、(1)から(2)で(78M)から(32M)に減少しています。(2)から(4)でperiodic collectionsが数度起動し、この際ヒープ圧縮のためにmixed collectionを起動します。それ以降のperiodic garbage collectionsの(5)から(7)のconcurrent cycleは、G1がその時点では、mixed GCを開始するold generationでのGCは不要と判断しています。これは、periodic garbage collectionsの(5)から(7)は、既に最小ヒープサイズに達したので、それ以上ヒープを縮小していません。

アプリケーションの非アクティブ期間に生存オブジェクトが変化(例:ソフト参照の期限切れなど)すると、アイドル時にコミット済Javaヒープの更なる縮小が起こります。

Alternatives

同様なことをVM外から実行可能で、例えば、jcmdツールやVMに何らかのコードを追加します。これは隠れコストが発生し、cronライクなチェックを想定すると、例えばあるノードに数百数千コンテナを乗せる場合、ヒープ圧縮が同時に多数のコンテナで実行される可能性があり、ホストに大量のCPUスパイクが発生します。

別案に個々のJavaプロセスに自動アタッチされるJavaエージェントがあります。Then the time of the check is distributed naturally as containers start at different time, 新規プロセスを立ち上げないので高めのCPUコストを減らせます。ただ、このやり方はユーザにかなりの負担を強いるため、採用されるとユーザを失望させるでしょう。

ここで挙げるユースケースで、適時Javaヒープを縮小することは、VMの特別サポートを正当化するほどの極めて一般的なユースケースと見なしています。

Risks and Assumptions

デフォルト設定値では本機能を無効にします。これによりレイテンシやスループットにシビアなアプリケーションにおいてVMの振る舞いに予測不能な振る舞いを無くせます。有効にする場合、通常はOSにJavaヒープを戻すのが望ましく、結果生じるconcurrent cycleやアプリケーションスループットへのインパクトは無視出来ます。

本機能を有効化すると、VMはperiodic collectionsを上述の設定に基づき実行しますが、それ以外のオプションとは無関係です。例えば、ユーザが-Xmsおよび-Xmxとそれ以外のオプション(の組み合わせ)をする場合、VMは最小かつ一貫性のあるGC停止という推定が出来ます。This will not be the case for consistency reasons.

periodic garbage collectionsがプロプラム実行を阻害するケースでは、システム全体CPUを考慮する制御方法か、ユーザで完全にperiodic garbage collectionsを無効化する方法を提供します。

Jedisで単一コネクションをマルチスレッドで使用してはいけない

JedisJavaのRedisクライアント。自明だが、単一コネクションをマルチスレッドで使いまわすとその動作は不定となる。

とりあえずソースコード

package kagamihoge.jedissample;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import redis.clients.jedis.Jedis;

public class NoPool {
    static Jedis jedis = new Jedis("localhost", 16379);

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);

        pool.submit(new TTask("a0"));
        pool.submit(new TTask("a1"));
    }

    static class TTask implements Runnable {
        public String a;

        public TTask(String a) {
            super();
            this.a = a;
        }

        @Override
        public void run() {
            while (true) {
                for (int i = 0; i < 100; i++) {
                    System.out.println(jedis.set(a + i, "v"));
                    System.out.println((a + i) + jedis.get(a + i));
                }
            }
        }
    }
}

これを実行すると……何も起きない。何が起きているかまでは調べる気は無いが、ともかくやってはいけない、という事だけは分かる。

というわけでJedisPoolを使用する。

package kagamihoge.jedissample;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class Pooling {
    static JedisPool jedisPool = new JedisPool("localhost", 16379);

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);

        pool.submit(new TTask("a0"));
        pool.submit(new TTask("a1"));
        pool.submit(new TTask("a2"));
        pool.submit(new TTask("a3"));
        pool.submit(new TTask("a4"));
        pool.submit(new TTask("a5"));
        pool.submit(new TTask("a6"));
        pool.submit(new TTask("a7"));
        pool.submit(new TTask("a8"));
        pool.submit(new TTask("a9"));
    }

    static class TTask implements Runnable {

        public String a;

        public TTask(String a) {
            super();
            this.a = a;
        }

        @Override
        public void run() {
            while (true) {
                for (int i = 0; i < 100; i++) {
                    try (Jedis jedis = jedisPool.getResource()) {
                        System.out.println(jedis.set(a + i, "v"));
                        System.out.println((a + i) + jedis.get(a + i));
                    }
                }
            }
        }
    }
}

これは動作する。

なお、上はメチャクチャ適当な使い方なのでJedis - Getting Startedのusing Jedis in a multithreaded environmentを最低限読んでから使うのが良い。

Docker for WindowsでOracle Database 18c XEうごかす

環境

手順

Docker for Windows準備

https://docs.docker.com/docker-for-windows/からダウンロードしてインストール。

イメージのビルド

https://github.com/oracle/docker-images.gitOracle関連の各種Dockerfileがあるのでチェックアウトしてくる。

OracleDatabase\SingleInstance\dockerfiles\18.4.0\Dockerfile.xe を使用する。このDoclerfileに色々説明が書いてある。

その説明に従い、Oracle XE 18cのバイナリをhttps://www.oracle.com/technetwork/database/database-technologies/express-edition/downloads/index.htmlからダウンロードして、上述のDockerfile.xeと同一ディレクトリに配置する。

コマンドプロンプト開いてOracleDatabase\SingleInstance\dockerfiles\18.4.0\に移動してイメージをビルドする。

docker build -t oracle/database:18.4.0-xe -f Dockerfile.xe .

実行時のログは以下のような感じ。

>docker build -t oracle/database:18.4.0-xe -f Dockerfile.xe .

base:18.4.0-xe -f Dockerfile.xe .
Sending build context to Docker daemon  2.574GB
Step 1/11 : FROM oraclelinux:7-slim
 ---> b19454a5f17a

(中略)

Complete!
Removing intermediate container 9e9e429de83e
 ---> 935f0511899d
Step 8/11 : VOLUME ["$ORACLE_BASE/oradata"]
 ---> Running in dda39dea4b17
Removing intermediate container dda39dea4b17
 ---> 4cc812ed66ec
Step 9/11 : EXPOSE 1521 8080 5500
 ---> Running in 8dc03e2708f0
Removing intermediate container 8dc03e2708f0
 ---> c5306a367b3a
Step 10/11 : HEALTHCHECK --interval=1m --start-period=5m    CMD "$ORACLE_BASE/$CHECK_DB_FILE" >/dev/null || exit 1
 ---> Running in 9aa006ae229b
Removing intermediate container 9aa006ae229b
 ---> e613ed045760
Step 11/11 : CMD exec $ORACLE_BASE/$RUN_FILE
 ---> Running in edd84324c27f
Removing intermediate container edd84324c27f
 ---> dff7f2815b94
Successfully built dff7f2815b94
Successfully tagged oracle/database:18.4.0-xe
SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.

確認。

>docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
oracle/database     18.4.0-xe           dff7f2815b94        3 minutes ago       8.43GB
oraclelinux         7-slim              b19454a5f17a        6 weeks ago         117MB
hello-world         latest              1815c82652c0        18 months ago       1.84kB

起動

とりあえず、適当にポートフォワーディングの設定で起動する。

docker run -it -p 11521:1521 -p 15500:5500 -p 18080:8080 oracle/database:18.4.0-xe

起動ログはこんな感じ。

>docker run -it -p 11521:1521 -p 15500:5500 -p 18080:8080 oracle/database:18.4.0-xe
ORACLE PASSWORD FOR SYS AND SYSTEM: 15c82b8165fa4f47
Specify a password to be used for database accounts. Oracle recommends that the password entered should be at least 8 characters in length, contain at least 1 uppercase character, 1 lower case character and 1 digit [0-9]. Note that the same password will be used for SYS, SYSTEM and PDBADMIN accounts:
Confirm the password:
Configuring Oracle Listener.
Listener configuration succeeded.
Configuring Oracle Database XE.
Enter SYS user password:
*****************
Enter SYSTEM user password:
*****************
Enter PDBADMIN User Password:
****************
Prepare for db operation
7% complete
Copying database files
29% complete
Creating and starting Oracle instance
30% complete
31% complete
34% complete
38% complete
41% complete
43% complete
Completing Database Creation
47% complete
50% complete
Creating Pluggable Databases
54% complete
71% complete
Executing Post Configuration Actions
93% complete
Running Custom Scripts
100% complete
Database creation complete. For details check the logfiles at:
 /opt/oracle/cfgtoollogs/dbca/XE.
Database Information:
Global Database Name:XE
System Identifier(SID):XE
Look at the log file "/opt/oracle/cfgtoollogs/dbca/XE/XE.log" for further details.

Connect to Oracle Database using one of the connect strings:
     Pluggable database: 14799974b6e0/XEPDB1
     Multitenant container database: 14799974b6e0
Use https://localhost:5500/em to access Oracle Enterprise Manager for Oracle Database XE
The Oracle base remains unchanged with value /opt/oracle
#########################
DATABASE IS READY TO USE!
#########################
The following output is now a tail of the alert.log:
2018-12-28T08:18:10.880011+00:00
XEPDB1(3):Resize operation completed for file# 10, old size 358400K, new size 368640K
2018-12-28T08:18:13.025781+00:00
XEPDB1(3):CREATE SMALLFILE TABLESPACE "USERS" LOGGING  DATAFILE  '/opt/oracle/oradata/XE/XEPDB1/users01.dbf' SIZE 5M REUSE AUTOEXTEND ON NEXT  1280K MAXSIZE UNLIMITED  EXTENT MANAGEMENT LOCAL  SEGMENT SPACE MANAGEMENT  AUTO
XEPDB1(3):Completed: CREATE SMALLFILE TABLESPACE "USERS" LOGGING  DATAFILE  '/opt/oracle/oradata/XE/XEPDB1/users01.dbf' SIZE 5M REUSE AUTOEXTEND ON NEXT  1280K MAXSIZE UNLIMITED  EXTENT MANAGEMENT LOCAL  SEGMENT SPACE MANAGEMENT  AUTO
XEPDB1(3):ALTER DATABASE DEFAULT TABLESPACE "USERS"
XEPDB1(3):Completed: ALTER DATABASE DEFAULT TABLESPACE "USERS"
2018-12-28T08:18:14.347027+00:00
ALTER PLUGGABLE DATABASE XEPDB1 SAVE STATE
Completed: ALTER PLUGGABLE DATABASE XEPDB1 SAVE STATE

SQL Developerで接続確認

上のログにもある通り、パスワードとかSIDとかは指定しないとデフォルトのものが使われる。

  • SYS AND SYSTEMのパスワード - run時に適当なものが振られる。起動ログのORACLE PASSWORD FOR SYS AND SYSTEM:のとこで確認できる。
  • SID - XE
  • PDB - XEPDB1

SQL Developerで接続確認する。Oracle 18cのマルチコンテナのこと良く分かってないが、左がインスタンスに対してで、右がデフォルトで作られるXEPDB1、に対する接続の一例。ポートフォワーディング11521:1521にしているので、ポートは11521

f:id:kagamihoge:20181228190137j:plain

docker-compose.yml

docker-compose.ymlをつくる。OracleDatabase\SingleInstance\samples\1830-docker-composeにサンプルがあるので、それを基につくる。

ホスト側にOracleのデータを保存

コンテナを作成すると毎回Oracleの色んなデータを作り直すので、起動に時間がかかる。あと、テーブル作ったりしても消えてしまうので、その場合はホスト側に保存する必要がある。

まず、Docker for WindwosのSettingsで共有ディレクトリを作りたいドライブにチェックを入れておく必要がある。

f:id:kagamihoge:20181228204006j:plain

volumesでホスト側の適当なディレクトリに/opt/oracle/oradataを保存するように設定する。ついでに、環境変数ORACLE_PWD=oracleでパスワードを固定している。環境変数は他にもORACLE_SIDとかORACLE_PDBとかがある。

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