それが僕には楽しかったんです。

ミドルウェアとかやってます

VLDB 2022 にある論文のアブストラクトを流し読みする回

はじめに

今度から続・データベース論文輪読会に参加するのでそのアップとして VLDB 2022 のアブストラクトをひたすら眺めて、雑にメモっていく。流し読みなので色々間違っている可能性はだいぶある。
対象は以下のサイトに掲載されているもので、タイトルから気になったやつ。

vldb.org

Design Trade-offs for a Robust Dynamic Hybrid Hash Join

ハイブリッドハッシュジョイン(以下、HHJ)の性能評価に関する話。
HHJ を用いる場合にパーティション数が性能にどう影響するのか、最小の CPU コストで最大のメモリ使用率を得るためのパーティション追加やパーティションの動的選択について既存の研究にあるアルゴリズムと比較している。
ストレージは HDD, SSD, EBS を使っていて、 AsterixDB のコンテキストとしてアルゴリズムは実装されている。
面白そう。

Plush: A Write-Optimized Persistent Log-Structured Hash-Table

不揮発性メモリの利点である低い書き込みレイテンシと小さい書き込み読み込みの帯域幅を使って性能の向上を図るらしい。
これはパッとみてよくわからなかった。

Cardinality Estimation in DBMS: A Comprehensive Benchmark Evaluation

カーディナリティ推定において、その目的である実環境でどの程度有効かというものを計測するベンチマークについての話。
PostgreSQL に各種アルゴリズムを組み込み、クエリプランの精度向上にどの程度有効であるかなどを総合的に評価するというのが主題っぽい。
既存の推定指標への問題点とそれを改善した新たな指標も提示しているらしい。
面白そう。

Your Read is Our Priority in Flash Storage

フラッシュストレージにおいて read-after-write (以下、RAW) は同期的な読み込みを書き込みによって遅くさせる場合がありそれがトランザクションスループットとレイテンシを悪化させる問題を提起した上で、それらを解決する新しいストレージインターフェースとバッファープールを提案している。

Cosine: A Cloud-Cost Optimized Self-Designing Key-Value Storage Engine

入力のワークロード、クラウドサービスの予算、目標パフォーマンスと SLA を与えることで完璧に近いストレージエンジンの設計を見つけ出し、実際のコードとして具現化するストレージエンジンについての紹介。
単純にこれが何か気になる。

Lotus: Scalable Multi-Partition Transactions on Single-Threaded Partitioned Databases

H-Store/VoltDB の同時実行制御の再検討。マルチパーティション(?)ワークロードでパフォーマンスが低下する問題の改善に念頭をおいている。
面白そう。

Memory-Optimized Multi-Version Concurrency Control for Disk-Based Database Systems

メモリに最適化した MVCC の提案。バージョニング情報をフラッシュすることなくメモリ上に保持することが可能であること、それによってオーバーヘッドを低下されることができるらしい。
なかなか面白そう。

A Scalable and Generic Approach to Range Joins

eq join ではない range join などでは、入れ子を伴うループが発生する場合があり、これが許容できない処理時間をもたらす場合がある。
それを解決するために kd-tree ベースの多次元 range join の提案と性能評価を行う。
面白そう。

Designing an Open Framework for Query Optimization and Compilation

クエリコンパイルと最適化は開発者の利便性や柔軟性、拡張性を犠牲にしてパフォーマンスの向上をもたらす事への問題提起とそれを解決するために MLIR をベースにしたクエリコンパイルスタックの提案。
それらを LingoDB に組み込み、性能評価を行う。
面白そう。

おわりに

10 本読むはずだったけど思いの外面白そうなやつが見つかってきたのでここまで。
ここから気になるやつを一本選んで読んでみる。

MySQL 8.0.29 以下をビルドできない場合がある

はじめに

ちょいと調子が戻ってきたので MySQL で検証したいことを検証しようとビルドを元気に進めていたら、仕事用の macOS マシンでビルドできないならまだしも Ubuntu 系統の OS でビルドできない場合があるというまさかの事態にぶち当たったのでメモがてら残す。

ビルド

で、ビルドを試みた。

$ wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-boost-8.0.28.tar.gz
$ tar xzvf mysql-boost-8.0.28.tar.gz
$ cd mysql-boost-8.0.28
$ mkdir build && cd $_
$ cmake ../ -DCMAKE_BUILD_TYPE=Debug -DWITH_BOOST=../boost

問題の躓きポイント

するとこれである

Cannot find appropriate system libraries for WITH_SSL=system.
Make sure you have specified a supported SSL version. 
Valid options are : 
system (use the OS openssl library), 
yes (synonym for system), 
</path/to/custom/openssl/installation>

CMake Error at cmake/ssl.cmake:61 (MESSAGE):
  Please install the appropriate openssl developer package.

libssl-dev も入っていて実態もある中でこれが出るともうだめだ、ということでインターネットの海をさまよっていると同じ状態に陥っている以下の記事を見つけた

日々の覚書: ConoHaの上でひたすらMySQLをビルドする 2021

どうやら OpenSSL (か、それ相当) のバージョンをうまく検出できてないっぽい。
実際エラーメッセージの上に出ているログを見ていると

-- OPENSSL_MAJOR_VERSION = 
-- OPENSSL_MINOR_VERSION = 
-- OPENSSL_FIX_VERSION = 

となっているので、本当にバージョンが見つけられていない様子。

OpenSSL 系を探っている cmake は cmake/ssl.cmake にあるのでひとまずそれを見る。
mysql-8.0.28 のタグでその cmake ファイルを探して見てみるとこのあたりが怪しそうな雰囲気がある。

https://github.com/mysql/mysql-server/blob/mysql-8.0.28/cmake/ssl.cmake#L216-L227

OPENSSL_VERSION_NUMBER なるものから、それぞれ Major, Minor Fix バージョンを抜き出している。

と、ここまでわかったので /usr/include/openssl/opensslv.h を見てみると、yoku さんのブログ記事にあるようにバージョンの実装が変更されたことがわかる。

/* Synthesize OPENSSL_VERSION_NUMBER with the layout 0xMNN00PPSL */
# ifdef OPENSSL_VERSION_PRE_RELEASE
#  define _OPENSSL_VERSION_PRE_RELEASE 0x0L
# else
#  define _OPENSSL_VERSION_PRE_RELEASE 0xfL
# endif
# define OPENSSL_VERSION_NUMBER          \
    ( (OPENSSL_VERSION_MAJOR<<28)        \
      |(OPENSSL_VERSION_MINOR<<20)       \
      |(OPENSSL_VERSION_PATCH<<4)        \
      |_OPENSSL_VERSION_PRE_RELEASE )

どこからならビルドできるのか

8.0.29 もだめそう。
https://github.com/mysql/mysql-server/blob/mysql-8.0.29/cmake/ssl.cmake#L228-L239

8.0.30 はいけそう。
https://github.com/mysql/mysql-server/blob/mysql-8.0.30/cmake/ssl.cmake#L113-L135

確かに手元の opensslv.h を見ているとそれぞれ OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH が存在するのでそれを見るように修正が入っていた。
ので、 8.0.30 をビルドしてみると成功したのでどうやらそういうことらしい。

コメントをみるに、OpenSSL version3 未満 (ただし、1.0.0 以上でないと最低要件は満たさない)では 8.0.28 もビルドできる様子はあるが 3 以上は上述の理由でビルドできないっぽい。

まとめ

8.0.29 以下のビルドでは OpenSSL のバージョンが 3 以上だとコケる

MariaDB の Application-Time Periods について

はじめに

気がついたら 3 ヶ月もブログを更新していなかったので、色々あって見ていた MariaDB の Application-Time Periods について雑に書く。ほとんど日本語訳に近いがまぁそれもいいでしょう。
環境は以下のもので debug build したやつを make install で手元に突っ込んでやっている。

  • 10.8.3-MariaDB-debug Source distribution commit: cbf9d8a

Application-Time Periods

まずそれが一体何なのかについて。公式ドキュメントは下記の通り。
mariadb.com

Application-Time Periods Table は 10.4.3 からサポートされている機能で、Time Periods と呼ばれる2つの temporal column によって範囲が決定される。
このテーブルにおいて Time Periods に相当するカラムは Date and Time Data Types*1 にあるデータ型のうち TIME と YEAR を除いたものであり、データ型が同じでないといけない。

ここまでざっと書いたが、何らかの時間的な範囲を何かするテーブルということしかわからないので実例について。まずは公式ドキュメントにあるやつをそのまま見ていく。
Application Time Periods は次の様に宣言することができる。*2

CREATE TABLE t1(
   name VARCHAR(50), 
   date_1 DATE,
   date_2 DATE,
   PERIOD FOR date_period(date_1, date_2));

name カラムは特に今関係無い。重要なのは PERIOD FOR *(date_1, date_2) の部分。これがまさに前述の Time Periods というやつ。

これによって date_1, date_2 を元にした範囲が定義されて、その範囲に対して INSERT, DELETE, UPDATE をすると何かが起こる。
ちなみにこの段階では特殊な何かの値が入っているというわけでもない。

MariaDB [test]> CREATE TABLE t1(
    ->    name VARCHAR(50), 
    ->    date_1 DATE,
    ->    date_2 DATE,
    ->    PERIOD FOR date_period(date_1, date_2));
Query OK, 0 rows affected (0.013 sec)

MariaDB [test]> select * from t1;
Empty set (0.002 sec)

これだけでもまだわからないので実際にテーブルのデータを追加したり、更新したりする。

INSERT

まずは INSERT から。

INSERT INTO t1 (name, date_1, date_2) VALUES
    ('a', '1999-01-01', '2000-01-01'),
    ('b', '1999-01-01', '2018-12-12'),
    ('c', '1999-01-01', '2017-01-01'),
    ('d', '2017-01-01', '2019-01-01');

余談だが date_period(date_1, date_2) という風に Time Periods を宣言した場合、それぞれのカラムに入る値は date_1 < date_2 となる必要がある。

MariaDB [test]> insert into t1(name, date_1, date_2) values ('e', '2022-01-01', '2021-01-01');
ERROR 4025 (23000): CONSTRAINT `date_period` failed for `test`.`t1`
MariaDB [test]> insert into t1(name, date_1, date_2) values ('e', '2022-01-01', '2022-01-01');
ERROR 4025 (23000): CONSTRAINT `date_period` failed for `test`.`t1`

DELETE

次に Time Periods に基づいて DELETE を飛ばしてみる。

MariaDB [test]> DELETE FROM t1
    -> FOR PORTION OF date_period
    ->     FROM '2001-01-01' TO '2018-01-01';
Query OK, 3 rows affected (0.008 sec)

MariaDB [test]> SELECT * FROM t1 ORDER BY name;
+------+------------+------------+
| name | date_1     | date_2     |
+------+------------+------------+
| a    | 1999-01-01 | 2000-01-01 |
| b    | 1999-01-01 | 2001-01-01 |
| b    | 2018-01-01 | 2018-12-12 |
| c    | 1999-01-01 | 2001-01-01 |
| d    | 2018-01-01 | 2019-01-01 |
+------+------------+------------+
5 rows in set (0.001 sec)

すると name = b の行が何故か2つになり、date_1, date_2 の値も変わっている。 name = c, name = d の列は増えてはないが date_1, date_2 の値が変わっている。
大体予想はつくが何が起こったかを図にするとこうなる。

application time periods before after
DELETE FOR PORTION OF

DELETE FOR PORTION OF は指定された Time Periods に基づいてその範囲を含むレコードの Time Periods を縮小・分割する。公式ドキュメントの例にはなかったが、DELETE で指定された Time Periods に収まりきる Time Periods を持つレコードは削除される。

MariaDB [test]> INSERT INTO t1(name, date_1, date_2) values('e', '2002-01-01', '2007-01-01');
Query OK, 1 row affected (0.007 sec)

MariaDB [test]> DELETE FROM t1 FOR PORTION OF date_period     FROM '2001-01-01' TO '2018-01-01';
Query OK, 1 row affected (0.007 sec)

MariaDB [test]> SELECT * FROM t1 ORDER BY name;
+------+------------+------------+
| name | date_1     | date_2     |
+------+------------+------------+
| a    | 1999-01-01 | 2000-01-01 |
| b    | 1999-01-01 | 2001-01-01 |
| b    | 2018-01-01 | 2018-12-12 |
| c    | 1999-01-01 | 2001-01-01 |
| d    | 2018-01-01 | 2019-01-01 |
+------+------------+------------+
5 rows in set (0.001 sec)

UPDATE

次に UPDATE について。 まずはさっきと同じテーブルを作る。

MariaDB [test]> TRUNCATE t1;
MariaDB [test]> INSERT INTO t1 (name, date_1, date_2) VALUES
    ->     ('a', '1999-01-01', '2000-01-01'),
    ->     ('b', '1999-01-01', '2018-12-12'),
    ->     ('c', '1999-01-01', '2017-01-01'),
    ->     ('d', '2017-01-01', '2019-01-01');
Query OK, 4 rows affected (0.001 sec)
Records: 4  Duplicates: 0  Warnings: 0

MariaDB [test]> SELECT * FROM t1;
+------+------------+------------+
| name | date_1     | date_2     |
+------+------------+------------+
| a    | 1999-01-01 | 2000-01-01 |
| b    | 1999-01-01 | 2018-12-12 |
| c    | 1999-01-01 | 2017-01-01 |
| d    | 2017-01-01 | 2019-01-01 |
+------+------------+------------+
4 rows in set (0.001 sec)

その上で次のように UPDATE する。

MariaDB [test]> UPDATE t1 FOR PORTION OF date_period
    ->   FROM '2000-01-01' TO '2018-01-01' 
    -> SET name = CONCAT(name,'_original');
Query OK, 3 rows affected (0.009 sec)
Rows matched: 3  Changed: 3  Inserted: 4  Warnings: 0

MariaDB [test]> SELECT * FROM t1 ORDER BY name;
+------------+------------+------------+
| name       | date_1     | date_2     |
+------------+------------+------------+
| a          | 1999-01-01 | 2000-01-01 |
| b          | 1999-01-01 | 2000-01-01 |
| b          | 2018-01-01 | 2018-12-12 |
| b_original | 2000-01-01 | 2018-01-01 |
| c          | 1999-01-01 | 2000-01-01 |
| c_original | 2000-01-01 | 2017-01-01 |
| d          | 2018-01-01 | 2019-01-01 |
| d_original | 2017-01-01 | 2018-01-01 |
+------------+------------+------------+
8 rows in set (0.001 sec)

これもまた図にするとこうなる。

application time periods before after
UPDATE FOR PORTION OF

name = a は例によって範囲外なので影響を受けない。name = b, name = c, name = d は範囲内が更新され範囲外の Time Periods は分割・縮小される。
ここで気になるのが、やはり範囲内に収まりきる Time Periods の挙動について。UPDATE により前述の *_original も影響を受け、とても見づらいが範囲内に収まる Time Periods については DELETE 時と同様に UPDATE の影響を受け、Time Periods が分割・縮小されない。

MariaDB [test]> INSERT INTO t1(name, date_1, date_2) values('e', '2002-01-01', '2007-01-01');
Query OK, 1 row affected (0.007 sec)

MariaDB [test]> UPDATE t1 FOR PORTION OF date_period   FROM '2000-01-01' TO '2018-01-01'  SET name = CONCAT(name,'_original');
Query OK, 4 rows affected (0.009 sec)
Rows matched: 4  Changed: 4  Inserted: 0  Warnings: 0

MariaDB [test]> SELECT * FROM t1 ORDER BY name;
+---------------------+------------+------------+
| name                | date_1     | date_2     |
+---------------------+------------+------------+
| a                   | 1999-01-01 | 2000-01-01 |
| b                   | 1999-01-01 | 2000-01-01 |
| b                   | 2018-01-01 | 2018-12-12 |
| b_original_original | 2000-01-01 | 2018-01-01 |
| c                   | 1999-01-01 | 2000-01-01 |
| c_original_original | 2000-01-01 | 2017-01-01 |
| d                   | 2018-01-01 | 2019-01-01 |
| d_original_original | 2017-01-01 | 2018-01-01 |
| e_original          | 2002-01-01 | 2007-01-01 |
+---------------------+------------+------------+
9 rows in set (0.001 sec)

WITHOUT OVERLAPS

ここまで書いた上で WITHOUT OVERLAPS なんて名前のものが出てきたら察しがつくが MariaDB 10.5.3 から重複不可にも利用できる。

MariaDB [test]> CREATE OR REPLACE TABLE rooms (
    ->  room_number INT,
    ->  guest_name VARCHAR(255),
    ->  checkin DATE,
    ->  checkout DATE,
    ->  PERIOD FOR p(checkin,checkout),
    ->  UNIQUE (room_number, p WITHOUT OVERLAPS)
    ->  );
Query OK, 0 rows affected (0.012 sec)

MariaDB [test]> INSERT INTO rooms VALUES 
    ->  (1, 'Regina', '2020-10-01', '2020-10-03'),
    ->  (2, 'Cochise', '2020-10-02', '2020-10-05'),
    ->  (1, 'Nowell', '2020-10-03', '2020-10-07'),
    ->  (2, 'Eusebius', '2020-10-04', '2020-10-06');
ERROR 1062 (23000): Duplicate entry '2-2020-10-06-2020-10-04' for key 'room_number'

UNIQUE と組み合わせる必要があるが、こういった利用方法もありらしい。

おわりに

これと partitioning が絡んだらバグるパターンが存在していてそれを直そうとするなかで面白い機能だったのでまとめたが、イマイチこれの使いどころが想像つかない。
普段は MySQL ばかりなので、この手の勝手に何か行が増えたり書き換わったりするやつは見たことがなくデバッグしていても中々に面白かった。唐突に write_row が出てきたり。
で、書いていて思ったがその辺りや諸々気になったことがあるのでまた余裕があるときにまとめる。


訂正: 3ヶ月じゃなくて約 7 ヶ月だった…

*1:Date and Time Data Types - MariaDB Knowledge Base

*2: ALTER TABLE でも Time Periods の追加、削除ができるが面倒なので今回はそこについて言及しない。

MariaDB のデバッグをいい感じにやる

はじめに

久々に MariaDBmacOS 上でデバッグしていたらデバッグしたまま寝てしまい、それを機に色々と壊してしまい戻すまでに時間がかかったので雑にまとめる。

デバッグの構成

mtrgdb をいい感じに利用できないことが多いので CLion に組み込まれている lldb を愛用している。この時、手元で実行すると data ディレクトリなどなどをいい感じに初期化して配置しておく必要がある。
basedir は /usr/local/mysql, datadir は /usr/local/mysql/data としている。

やることリスト

普通にビルドする。
rabbitfoot141.hatenablog.com
ただし、オプションに -DMYSQL_MAINTAINER_MODE=OFF を追加する。
これで groonga, mroonga 周りのビルド大ゴケ問題をだいたい解決できる。

ビルドしたら次にやることは make install。
これをやると basedir 以下にバイナリなどが配置される。

バイナリを配置したらやることは

$ /usr/local/mysql/scripts/mysql_install_db --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data

で datadir を初期化する。

あとはいつも通り mariadbd を起動してデバッグする。

ビルド役立ちメモ

特定ストレージエンジンのビルドをスキップしたい時は -DWITHOUT_MROONGA=1 みたいにオプションを渡してやると幸せになれる

MariaDB 10.6 を macOS BigSur 11.4 でビルドする

はじめに

どうも、最近クラウドアトラスを見てからあまり映画を漁りに行くことをしなくなり、代わりに異世界スローライフ系の作品を脳死で見ているけんつです。
気軽に Fork していた mariadb/sever リポジトリに対して fetch upstream を実行したらビルドが全く通らなくなったのでこれを機にビルド方法を残しておく。
なにやら macOS 上でのビルドは気軽に死ぬので mariadb/server 10.6 のこのコミット時点でビルドすることを前提にする。
bump the VERSION · MariaDB/server@a841069 · GitHub

ビルドする前に

ソースコードを持ってくる

いつもの。

> git@github.com:MariaDB/server.git

必要なパッケージ群をインストールする

Build Environment Setup for Mac - MariaDB Knowledge Base
ここに書いてあるように必要なパッケージをインストールする。今回は homebrew を使って突っ込んだ。

bison を変更する

XCode に頼っていると次のようなエラーがビルド時に出力される。

[ 22%] [BISON][gen_mariadb_cc_hh] Building parser with bison 2.3
/Users/lrf141/mariaProject/server/build/sql/yy_mariadb.yy:351.9-16: syntax error, unexpected identifier, expecting string
make[2]: *** [sql/yy_mariadb.cc] Error 1
make[1]: *** [sql/CMakeFiles/gen_lex_token.dir/all] Error 2
make: *** [all] Error 2

なにやら bison のバージョンが古いことが原因らしいので、homebrew を使っていい感じにする。

$ brew install bison
$ brew link bison --force

brew link 時にパスを通せと言われるので、お手持ちの .bashrc なり .zshrc なりに追加する。

どうしても /usr/bin 以下に突っ込みたい人

System Integrity Protection が有効になっていると sudo でも操作できないので

$ csrutil status 

の結果が enable だった場合、リカバリーモードで立ち上げて

$ csrutil disable

をやれば突っ込めるようになる。

gcc, g++ を突っ込む(宗教に応じて選択してください、おすすめはしません)

やりたい人はお勧めしないが Free Software Foundation の方の gcc, g++ を突っ込む。

$ brew install gcc
// path の確認
$ ls /usr/local/bin | grep gcc
$ ls /usr/local/bin | grep g++

$ ln -s /usr/local/bin/gcc-8 /usr/local/bin/gcc
$ ln -s /usr/local/bin/g++-8 /usr/local/bin/g++

ここまでやったらさっきと同様にパスを通す。

ビルド

CMake でよろしくやる

zenn.dev
Nayuta Yanagisawa さんの記事によると

cmake -DCMAKE_BUILD_TYPE=Debug ..

とすると良いようですが macOS 版でこのまま進むと mroonga のビルド時に vendor に入っている groonga をコンパイルするあたりで -Wformat を吐くのでこいつだけ抑制しておきます。
なので基本的には以下のような感じにするといい感じにやってくれます。
どうしても mroonga, groonga を触りたいんだという人は頑張ってください。

$ cd build
$ cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAG="-Wno-format" ..
$ cmake --build . --config Debug -j 8
homebrew で突っ込んだ gcc, g++ を突っ込むと何が起こるか

結論から書くとビルドでこけます。
何が起こるかというと

In file included from server/sql/structs.h:26,
                 from server/sql/handler.h:34,
                 from server/storage/innobase/include/trx0xa.h:27,
                 from server/storage/innobase/include/trx0trx.h:34,
                 from server/storage/innobase/btr/btr0pcur.cc:30:
server/include/my_time.h: In function 'void my_timeval_trunc(timeval*, uint)':
server/include/my_time.h:249:65: error: conversion from 'long int' to '__darwin_suseconds_t' {aka 'int'} may change value [-Werror=conversion]
  249 |   tv->tv_usec-= my_time_fraction_remainder(tv->tv_usec, decimals);

my_time.h で int と long int の変換やってええんか?って感じでエラーになります。
server/my_time.h at 76f4a78ba2639b5abd01a88b24a3c509c11530ce · MariaDB/server · GitHub


この辺をよく見てみると _timeval.h なるヘッダーで

/*
 * Copyright (c) 2003-2012 Apple Inc. All rights reserved.
 *
 * @APPLE_OSREFERENCE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. The rights granted to you under the License
 * may not be used to create, or enable the creation or redistribution of,
 * unlawful or unlicensed copies of an Apple operating system, or to
 * circumvent, violate, or enable the circumvention or violation of, any
 * terms of an Apple operating system software license agreement.
 *
 * Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_OSREFERENCE_LICENSE_HEADER_END@
 */
#ifndef _STRUCT_TIMEVAL
#define _STRUCT_TIMEVAL         struct timeval

#include <machine/types.h> /* __darwin_time_t */
#include <sys/_types.h> /* __darwin_suseconds_t */

_STRUCT_TIMEVAL
{
	__darwin_time_t         tv_sec;         /* seconds */
	__darwin_suseconds_t    tv_usec;        /* and microseconds */
};
#endif /* _STRUCT_TIMEVAL */

https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/_types/_timeval.h.auto.html

__darwin_suseconds_t なる輩が int なのでこいつが問題になってくるわけです。

おわりに

10.6 がリリースされる前に触ってたけどその時はさらに色々あって大変だったけどいい感じにビルドできることがわかって大変満足。
特にどのストレージエンジンか忘れたけど鬼のように出力されていた -Wsign-compare が消え去った上に、デバッグしようものなら gdb で table とか見れなかったのが見れるようになっていてこの上なく満足。

JVM における G1GC とヒープの雑な話

はじめに

どうも、けんつです。GW は特に何もせず映画とチェスに勤しんでいたら気がついた時には連休は既に過去のものとなっていました。
やるやる詐欺をしてきた JVM の G1GC について基本的なことなどをまとめていこうと思い立ったので書きます。ヒープではない領域の話については以前書いたので割愛。

ヒープと GC の基本

ヒープの構造

よく言われるやつだけど、ヒープは以下のような構造になっている。*1*2
f:id:RabbitFoot141:20210508233655p:plain

雑な GC の基本

この辺を雑に書くと、オブジェクトが生成された場合はまず Eden 領域に配置されて、そこから Minor GC によって Survivor 領域に移されたり、Survivor 領域が足りなくなったら Tenured 領域に配置され、そこでも領域が足りなくなると Major GC で解放される。

もう少し細かく書くと、Eden 領域に配置されたオブジェクトは Minor GC が走った時に利用されていないオブジェクトを破棄して、利用されているオブジェクトを Survivor 領域に移動させる。Survivor 領域にも移動させられない場合 or 一定回数 Minor GC を生き残ったオブジェクトは Tenured 領域に移動させる。

Tenured 領域にオブジェクトを移動させることができない場合は Major GC が走って不要なオブジェクトを解放する。一般的にはこの時どちらの GC でも停止時間が発生する。特に Full GC の方が時間がかかりがち。

G1GC

概要

G1GC はそれなりに大きいメモリを持つマルチプロセッサマシンで高パフォーマンスを発揮する、正確には高いスループットを安定した一時停止時間目標内で実現することを目標としている。

さらに細かい要件では、6 GB 以上のヒープサイズを持つ JVM に対して 0.5 秒未満の安定した予測可能な一時停止時間を実現することを目的としている。*3

G1GC も世代別 GC となっているがヒープをリージョンという単位で分割し、管理している。その上でそれぞれのリージョンは Young 領域または Old 領域に属することとなる。この時、それぞれの領域に属するリージョンは必ずしも連続するとは限らない。また Tenured 領域に対する処理はバックグラウンドスレッドが行うため大抵の場合、アプリケーションスレッドは停止しない。その代わり複雑な処理を内部で行っているため CPU 使用率が高くなりやすい。

仕組み

G1GC は特に Old 領域に対する GC が他の GC アルゴリズムと異なる。Old 領域に対する GC では対象リージョン内にどれだけ不要となったオブジェクトが存在するかを基準にして領域を確保する。つまり不要オブジェクトが多いリージョンの操作に注力する。そのため短時間で GC を実行することが可能となる。この仕組みは Young 領域には適用されない。

その上で G1GC では次の 4 つの処理が中心となる。これらは別々に動作するわけではなく後述のコンカレントサイクル内で実行される場合がある。

  • Young 領域に対する GC
  • バックグラウンドで行われるコンカレントサイクル
  • 混合 GC
  • Major GC (必要に応じて)
Young 領域に対する GC

Young 領域に対する GC は Eden 空間*4を使い果たすと実行される。この時、Eden 空間は解放され最低でも一つの Survivor 空間が確保された上で必要に応じて Old 領域に移動される。

コンカレントサイクル

コンカレントサイクルは大まかに以下のフェーズから構成される。

  • 初期マーク付け

Young 領域に対する GC に乗じて行われるためアプリケーションスレッドは停止する

  • ルートリージョンスキャン

初期マーク付けでマークされた Survivor 領域を走査して、 Old 領域のオブジェクトに対する参照をマークする。次の停止を伴う Young 領域に対する GC が実行される前に完了している必要がある。このフェーズはアプリケーションスレッドと並列実行されるため停止を伴わない。ただし G1GC が利用するスレッドに対する CPU リソースが十分でない時は停止時間が増大する可能性がある。またこのフェーズは中断できないことに注意が必要。

  • 並列マーク付け

このフェーズではヒープ全体が対象となり、オブジェクトグラフから到達可能なオブジェクトを探索する。これも並列実行されアプリケーションスレッドを停止させない。Young 領域に対する GC によって中断されることがある。

  • 再マーク付け

未探索のオブジェクトグラフから到達可能なオブジェクトを再度探索する。アプリケーションスレッドの停止を伴う。

  • クリーンアップ

領域を解放して、完全に利用していないリージョンと混合 GC で解放するオブジェクトを識別する。アプリケーションスレッドの停止を伴う。

ここで行っているのはあくまでも Old 領域に対するマーク付けで Young 領域に対する GC によって多少領域が解放されることはあるが、この一連のフェーズに色々期待してはいけないらしい。

混合 GC

バックグランドで実行されるコンカレントサイクルによって、リージョンに対するマーク付けが行われたらいよいよ混合 GC によって領域が解放される。混合 GC とは Young 領域に対する GC とマークがついたリージョンの解放が両方行われるため、そう呼ばれている。
Young 領域に対する GC では、Eden 空間が完全に空になり Survivor 空間に対して調整が入る。Old 空間に存在するマーク付けされたリージョンに対しては一度に全てを解放することはせずに、繰り返し実行される。その中でマーク付きリージョン内で到達可能なオブジェクトがマーク付きでない Old 領域のリージョンに退避される。マーク付きのリージョンがおおよそ解放された段階で Young 領域に対する GC が再開され、コンカレントサイクルによってマークされるを繰り返す。

Major GC が実行される可能性のある場合
  • concurrent mode failure

マーク付けフェーズを開始し、完了するまでに Old 領域がいっぱいになった場合。この時該当するフェーズを中断して Major GC が走る。

  • promotion failure

混合 GC が開始されたが、Old 領域に移動させる必要のあるものが解放されるものより多く、かつ Old 領域がいっぱいになってしまった場合。

  • evacuation failure

Minor GC 時に Survivor 空間に十分な空きがなく対象となる全てオブジェクトを Old 領域に移動させる必要のある場合。リージョンが確保されているが、各リージョンでデータが断片化してしまった場合に起きやすい。必ず Major GC を伴うわけではないが、Major GC が走る可能性がある。またログ上では Minor GC として記録されているらしい。

おわりに

次は G1GC のチューニングにどういったものがあるのか調べようかね。疲れた。

参考文献

*1:図中で各領域が占める割合は適当。

*2:Java 8 以降の話をしたいので、Permanent は除外。それに相当するものは Metaspace で非ヒープ領域。

*3:Oracle のドキュメントでは 6 GB 以上, 0.5 秒未満とあるが、Java パフォーマンスでは 4 GB 以上, 一時停止時間の明確な数値は言及されていない。

*4:ここでの空間とは各領域のリージョンを指す。

Java で利用できるオプションをバージョン別で比較してくれるサービスを作った話

2021/07/12 VPSMariaDB ビルド用サーバに化けたのでサイトを運用していないです

はじめに

言語のバージョンを変更すると何らかの原因不明なトラブルに遭遇したりする場合がありますが、そういう時明確に機能変更などがあり原因がある程度サクッとわかる場合もあれば、一体どこか原因かわからないといった場合もあると思います。
それらの中で個人的にしんどいのが Java であればオプションのデフォルト値が変更になっていて、production build でそれらを指定していないがために暗黙的に性能劣化を引き起こしてしまうパターンに遭遇することです。

そういったときにどのバージョンからどのバージョンにあげると何が変わるのかということがサクッと見れると嬉しいですが、意外とそういうサービスがなく不便に思ったので作って見ました、という話です。

作ったもの

https://jvm.lrf141.dev/jvm.lrf141.dev

f:id:RabbitFoot141:20210227223136p:plain

こんな感じのサービスを Springboot + Java 11, React で作成しました。

対象となるオプションたち

サイトタイトルの下にもありますが

java -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsInitial -version

各バージョン( 現段階では Java 8 ~ 11 の 4 バージョン ) でこれを実行し、json にした上で resoruces ディレクトリに配置し利用しています。

UnlockDiagnosticVMOptions

これは JVM の診断用オプションを有効にするためのオプションです。
診断用と言ってもなんだそれはとなりそうですが、例えば -XX:+LogCompilation や -XX:+PrintInlining といったオプションたちが当てはまります。
これによって色々捗る場合もあるので一応ここでは取得するオプションリストに突っ込んでいます。

UnlockExperimentalVMOptions

これは試験的に追加されているオプション群を利用可能にするというものです。
一応将来的にはサポート予定となっているオプションもあるようですが、基本的にその段階ではガチでサポートされていないオプションたちになります。
Experimental なオプションがその後サポートされたオプションになったケースもあるようで一応追加しています。

改善したい

一応それなりに使えるようにはなりましたが、フロントエンドの細かい部分とかめんどくさがって結構粗い実装になっていたりするので修正したいのと
諸事情でめちゃくちゃ急いで作ったため、バックエンド側のテストも不十分なのでいい加減追加したいなと考えています。

余談

API をいい感じに追加しようと思って設計とかまじめに考えてやってみたのですが、気軽に 1000 行ぐらいになってしまってやはりこの手の機能をちゃんとレイヤーを分割してコードを書くと結構しんどい。

おわりに

なんかこうしたらいいんじゃないかみたいなことがあれば教えてくださいな。