大量データを export する時のあれこれ

例えば、一定期間に登録・更新したデータを export したい、って要件はまずまずあると思います。 場合によっては期間で絞り込んでも数万件とか対象になることもあるわけです。 そんなデータをメモリにどかんとのせたらどうなるか?OOM 発生が容易に想像できます。

その分メモリを積むことで対処できなくはないとは思いますが、メモリを追加し続けるのにも限度があります。

アプリ側でなんとかするにはいくつか方法が考えられます。

1. アプリ側で適切なサイズに区切る

limit, offset を駆使して、抽出対象のレコードを絞り込む方法です。また、offset だと後ろのデータを取得する時の読み飛ばしに時間がかかるので、抽出した塊の最後のレコードの次から塊を抽出する方法もあります(シーク法と言われているみたいですね)。

データの塊毎に SQL を発行するので、SQL 自体が遅いと最後のページにたどり着くまでにめっちゃ時間がかかるわけです。

そもそもの対象データを範囲指定で絞り込むに加えてデータの並び替えも発生します。

RDBMS によっては絞り込みとソート両方に index が効かせるのが難しいこともあります。index が効くように table の構造を変えたり、複数のカラムを結合したソート用のカラムを作ってみたり、色々厳しい対応が必要になるかもしれません。 既に大量のデータが入ってたりすると、構造変える migration に何時間もかかるという苦行の運用も待っているかもしれません。そんなのは嫌だ。

2. 絞り込みとソートで別の index 効かせる

無理矢理にでも index 効かせちゃえ、ということで

  • 対象データを抽出し、TEMPORARY TABLE に突っ込む(ソート順は意識しない)
    • その table にはソート用の index を張る
  • データの塊を取得するのは TEMPORARY TABLE からソートしつつ取得する

みたいな方法を考えたこともあります。多分動くとは思うのですが、TEMPORARY TABLE の管理が面倒な気がして他に良い案はないか模索していました。

3. サーバーサイドのカーソルを使う

やっと本題です。 RDBMS 側の対象データ Set をちょっとずつ返す方法があることを知りました。

言語やドライバでの違いはあるようですが、JDBC ドライバでいうと、fetchSize に値を設定することで

  • 全件メモリに配置する
  • 少しずつ RDBMS から fetch する

を制御できるようです。で、受け取る側は Stream で処理するようにしておくと、少しずつ RDBMS から fetch しつつ処理を行うことができそうです。

Doma2 でやってみた

大好きな Doma2で簡単な検証用のプログラムと環境(PostgreSQL)を作りました。

@Dao
public interface SampleDao {

  @Select
  @Sql("SELECT id, data FROM sample")
  List<Sample> selectAll();

  @Select(fetchSize = 20)
  @Sql("SELECT id, data FROM sample")
  @Suppress(messages = { Message.DOMA4274 })
  Stream<Sample> selectAllForStream();
}

selectAll の方は、結果データを全てメモリに配置するケース。 selectAllForStream の方は、fetchSize 分だけサーバから取得して処理を行えるケースです。 Stream 処理の中で export ファイルに書き込む処理を行えば、対象データが多くてもメモリも少なく済みます。

テストクラスは以下のようなイメージです。

@Test
@DisplayName("大量の ResultSet を取得する場合、OOM になること")
public void testSelectAll() {
  var tm = config.getTransactionManager();

  tm.required(
        () -> {
            System.out.println("start");
            sut.selectAll();
            System.out.println("end");
        });
}

@Test
@DisplayName("大量の ResultSet を取得する場合、Stream にしておくと取得できること")
public void testSelectAllForStream() {
  var tm = config.getTransactionManager();
  tm.required(
        () -> {
            System.out.println("start");
            var increment = new AtomicLong();
            try (var stream = sut.selectAllForStream()) {
                // 本来はここで export ファイルに書き込む処理が入る想定
                stream.forEach((v)-> increment.incrementAndGet());
            }
            System.out.println("end: " + increment.get());
        });
}

RDBMS 側に大量データ入れておいて、

# select count(*) from sample;
  count
----------
 48828130

テストクラス実行の java 引数に -Xmx30m を設定して testSelectAllForStream を実行すると、

end: 48828130

が表示されました。

testSelectAll を実行すると、

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2371)
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:368)
    at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:498)
    ...

と表示されました。ヒープが小さいのに5000万件のデータをメモリに展開しようとしているので想定通りですね。

まとめ

というわけで、絞り込んでも大量データの export 処理を時間をかけずに行うには?の選択肢としてサーバーサイドのカーソルがあることをお話しました。

RDBMS や言語、ドライバで振る舞いが違うようなので、実際に使用するときは調査が必要だとは思います。 RDBMS のリソースをその分消費するのですが、毎回 SQL を発行するよりはトータルで時間がかからない良い選択なんじゃないかと思います。 パフォーマンスを上げたい人の参考になれば幸いです。

CI を H2 -> MySQL にした時に遅いと思ったので試行錯誤してみた記録

今まで CI での RDBMS がらみのテストには H2 使ってたのですが、本番にもうちょっと近づけたいと思って、MySQL にしてみました。 その辺の調査と対応を備忘録的に残します。

前提条件

  • migration tool に Flyway 使った SpringBoot アプリ
  • 長いこと開発しているので migration ファイルの数は数十くらいあります
    • ある程度の時期で merge したいけど環境数が多いので migration の手間を考えて二の足を踏んでいる現状です

何をしようとしたか

本番は MySQL だが、CI では速度重視で H2 に接続するようにしていました。DDL やエラーハンドリングで RDBMS を意識するケースがちょいちょいあり、H2 用の処理ってアプリの価値に寄与していないのでは?というところから CI でも MySQL で行うようにして本質的でないコードを駆逐したいのがモチベーションでした。

何が起こったか

CI の時間が長くなりました。

これによる課題

CI にかかるコスト増

調査

実際にサンプルを作成して、時間を計測してみます。

1. H2 で実行

サンプル

2. MySQL8.0 に置き換えて実行

差分

サンプルの処理時間が少ないので誤差みたいなものかもしれませんが、確実に遅くなっています。 ローカルで実行するとより「詰まってる」感があります。

3. コード調査

コードを見たところ、@FlywayTest を使っている場合はデフォルトが invokeCleanDB=true になっていて、この設定だとテストメソッドが終わるたびに MySQL の場合は drop table を呼んでいました。で、次のテストメソッド実行前に migrate を呼ぶので時間がかかっていそうな感じでした。 このアプリは Repository のテストに @FlywayTest を付与しており、数十の migration ファイルを実行するので、確かに時間がかかりそうです。

4. 対応

drop table の箇所を truncate にすればテスト毎に最初から migration を行う必要がなくなるので早くなるのでは?と考え、新しく TestExecutionListener を作成してみます。

5. TestExecutionListener 追加して実行

差分 *1

素の状態よりマシになったように思えます。ローカルで実行しても「詰まる」感は薄れたように感じました。

まとめ

@FlywayTest を使ったテストを大量に使っている時に H2 以外にしたテストが遅いな、と思った時はテスト前後に何をしているか確認してみては如何でしょうか。 drop table でなく、truncate にするモードもあれば良いのに、とも思ったのですが、あんまりやりたくないみたいですね

*1:テスト用なので雑なのはご容赦ください

PEACOCK MEETS UP #01 に参加してみた #PeacockMeetsUp

そろそろオフラインの勉強会に参加してもいいかなーという気持ちになってきていたので、これに参加してみました。

最初は、「勉強会の空気がよくわからないからおとなしく雰囲気を感じるくらいにしよう」と思ってたのですが、久しぶりに人前で話したい、という気持ちを抑えられなくなってきたのでやっぱり LT 発表しちゃいました。

発表内容

元ネタは事前に作ったやつの焼き直しではあるのですが、テストについて発表させてもらいました。

テストを惰性で書くんじゃなくて、

  • 変化しないテストを書く
    • 人生2回目の「mock 使うの控えよう」ムーブです
  • 明確なテストを書く
    • テスト対象メソッドの挙動すべてを1つのテストメソッドの verify に含めてしまうと挙動が増えたときにどこのテストメソッドに書けばいいか迷子になるから、挙動毎にテストメソッドを書いて1つだけ verify する

みたいなことを述べさせてもらいました。偉そうに書いてますが、これからこういうことを心がけていくよ、というお気持ち発表です。

結論: 楽しかった!

やっぱりオフライン勉強会は聞いてる人の反応が見られるので良いですね。

もともと長岡にはNDSというITの勉強会があって、そちらによくお邪魔させてもらってたのですが、今回はあまりメンバーの知らない勉強会に飛び込んでみました。自分より若い方とも話ができたし、勉強会はやっぱり良いですね!

この勉強会を主催している kinocoboyさんは長岡に移住で来てくれた方で、それだけでもありがたいのに勉強会まで企画してくれて...感謝しかありません。他にも新潟に拠点を移す方も多数いらっしゃることがわかったので、そういう人たちと盛り上げて行きたいなーと思いました。

祝辞をパソコンで印刷する

PTA 会長に回ってくるお仕事に「祝辞」があります。

文章を考えないといけないのですが、それよりも、紙で残すことが気になってました(数年前の祝辞を見せてもらったのですが、どれもきれいな手書き(毛筆)で...。

字が下手な私。どうしたもんかな、イマドキは A4 の横書き印刷で良いかな、と思ったのですが、プリンタ対応の式辞用紙売ってるんですね。

テンプレートをダウンロードして A4 で印刷すればいい感じに作れます。Mac でも大丈夫でした。

1点注意するとするなら、↓だとプリンタによってはうまくいかないかもしれません。

私は最初こっちを買って、家のプリンタで印刷できないじゃん、と慌ててしまいました。 (あやうく対応しているプリンタに買い換えるか、まで考えました...w)

Quarkus + Doma 2 + Lombok を使う

f:id:nemuzuka:20190714095600p:plain

Doma 2が好きなので Quarkus でも使いたいと思って、これ を参考にサンプル作ってたのですが、Lombok と組み合わせると build が通らなくてハマってました。 どうも、Lombok が生成するメソッドが見つからないと怒られる。 で、ここ で聞いてみたわけです。すると、

Lombokアノテーションプロセッサが動いていないようですね。

と、@nakamura_to さんからコメントもらいました。

これで勝つる!

参考コードはこちら

これからも Doma 2 使っていこう。

ソフトウェア品質を高める開発者テスト

「知識ゼロから学ぶソフトウェアテスト」の著者である高橋さんの本です。 「そうそう」「わかるわかる」と頷きながら読み進めて、あっという間に読み切ってしまいました。 私が気になったところと共感できたところを書き出していきます。

上流品質を向上させる

そもそも私は、「上流品質」という言葉、知りませんでした。 個人的には上流/下流工程とかいう響きがあまり好きではないのですが、開発初期でも品質向上を上げる努力をしましょうってことなんですかね。

プロジェクトのしわ寄せは大体コーディングの後のテストフェーズで起こります。 それまでは順調と報告を受けていたプロジェクトもだんだんと暗雲立ち込めてきます。 下手をすると保守フェーズになってもバグが見つかり、システム利用者に迷惑をかけ、信頼貯金がなくなっていく...。

ほんと日本のソフトウェア開発現場はヤバい。

この一言に尽きると思います。わざわざリリース期日に近付いてから手戻りリスクの高い作業をする、というのはリスクヘッジできてない開発組織だと思います。

上流品質を向上させるには

本書では

  • 要求仕様の明確化
  • クラスや関数構造をシンプルに保つ
  • 単体・統合テストの実行
  • レビューの実施

を実行すれば良いと書かれています。 当たり前のようにやっている開発組織もあると思いますが、私もこれである程度の品質は担保できると思っています。 間違っても「システムテストをちゃんとやる」みたいなことではないみたいです。

開発初期でもテストをしなければ、多くのバグを後半で潰すことになり、(間に合わない為)納期を優先することで、潰し切れないバグを残してしまうことになります。 バグを0にするのは難しいですが、システムを利用する上で致命的なバグを出さない為にはこの作業をしておいた方が良いと私も思います。

単体テストで大事なのは網羅率でなく期待値の確認

これは本当にそう思います。 テスト対象のメソッド呼び出しで Exception が起きなければ OK としているテストコードのなんと多いことか。 なんかよく分からんけど正常終了している確認にはなりますし、Exception を throw するような振る舞いに変わった時にデグレに気づくことができるので全く意味がないとは言いませんが、「これでテストはバッチリです!」とは言いたくないなぁと思います。

カバレッジはわかりやすいので目標値として設定しやすいですけど、カバレッジ100%だからと言って品質が高いかというと全然そんなことないです。 境界値や状態遷移で起こりうる入力値のパターンを100%網羅し、その期待値を確認する方が高品質に繋がると私も思っています。

最近のシステムは外部サービス呼び出しも多くあって、単体テスト実行のたびに API 叩くわけにも行かないこともあるでしょうから、その辺りの単体テストは mock することが多いのではないでしょうか。 その時の期待値として、「mock を呼び出したこと」だけでなく、「mock の引数に正しく値を渡しているか」も確認する必要があると思っています。 Java で言うと、Mockitoをよく使うのですが、ArgumentCaptor でテスト対象メソッドから mock に渡したパラメータも確認する必要があると言うことです。 外部サービス呼び出しとか RDBMS に永続化処理があるテストは mock に置き換えて引数をチェックするってことをよくやります。

レビューしよう

しましょう。 レビューは「本人が気づくことに重点をおくもの」だそうです。なので、指摘より「なぜこういうふうになっているの?」という問いかけ形式にすると気づきに繋がるようです。理由によってはコメントした人も気付けるかもしれませんしね。

システムテストの自動化

SIer あるあるですけど、システムテストを自動化するのは夢物語だと思っています。 キャプチャー・リプレイの自動化テストなんか...悪夢でしかないです。 仮にうまくいったとしても、スクリプトのメンテナンスコストがバカにならないと思います。 ちょっとの変更でテストが壊れて、それを放置して自動化しなくなる未来が見えます。

やっぱり単体テストで多くのバグを見つけることが重要

重大なバグをシステムテストで見つける方式はリスクしかないです。それですり抜ける奴があったら...大騒ぎになります。 システムテストはやはり今の時点では手動でやるしかなく、それを毎回人の手でやるってのは苦痛です。 単体テストは自動化しやすく、内部的な状態を再現させやすいので、それに対するテストケースの追加が容易にできます。 バグが出てその修正とテストをセットにしておけば、他の修正でデグレが起きても早い段階で気づけます。 デグレ検知の為に毎回手動でシステムテストをやるとしたら...私はその現場から抜けようと思いますね。

あとがき

薄くて高い!

と読者の方に怒られるみたいですが、そういう方はもっと難しそうな文献を私たちにわかりやすく教えて欲しいものです。 ここまでわかりやすく要約してくれる本、そうそうないと思うんですよね。

まとめ

品質を上げるには特効薬はないんですよね。 要求仕様だって具体的に書かなければテストに落とせないし、テストに落とせないってことは開発完了にならないってことです。それだけでリスクです。 開発したそばからテストも書いておけば、レビューの input にもなるし、デグレ検知もできるしカバレッジも見れるしで「進んでる感」を得られます。

自分の経験を押し付けているように受け取る人がいるようですが、私は解決パターンの引き出しとして良書だと思いました。 他人が経験したものを知ることで同じような失敗しなかったり、成功への近道ができるなんて最高じゃないですか。

あと、まだ売っているらしい。(最近は amazon と中古で売ってるのくらいしか見てないな)

f:id:nemuzuka:20210318182721p:plain

CircleCI実践入門

CircleCI 自体は前から使っていたのですが、知識を update せにゃいかん気になってきたので読みました。 まとまった状態で読めるのはありがたいです。

構成

本書は、なぜ CI / CD が必要か?から始まり、CircleCI の基本、GitHub との連携やジョブが失敗した時のデバッグ方法の説明等がなされています。基本的に CircleCI の機能の説明なのですが、CI / CD のインフラをオンプレ、SaaS どちらにするのかの判断材料として読んでも良いんじゃないかと思います。

気になったところ

CircleCI の機能自体も発見が多かったのですが、ワークフローとジョブの作り方の所が気になりました。

ジョブは分割すべし

最初の方はプロジェクトのコードも小さいので、1つのジョブに

  • 静的解析
  • テスト
  • docker image 生成

みたいな感じで step を連ねて記載していくと思いますが、規模が大きくなってくると、途中で失敗しまうことも多くなります。 長い時間をかけて失敗してしまった時も、リソースを使用した分だけコストが発生します。 再実行する時も最初から実行することになります。 その辺りの余計なコストを発生させないようにするために、それぞれジョブで分けて、実行するのはワークフローで制御する方がベストプラクティスなようです。

確かに規模が大きくなってくると、テストの実行時間を短縮したくなり、並列で実行することを考えます。 その時に1つのジョブになっていると、並列で実行したい内容と1つのプロセスで実行したい内容が混在してきます。 テストだけ並列数を上げて実行したいのに静的解析も並列でやっても意味ありませんし、「他の処理が終わるのを待つ時間」というのもコストになりますから、もったいないですよね。

ジョブ間のファイル共有も考えられていて、ストレージやキャッシュを使って共有できます。

ジョブを小さな責務で分けておくことで、1つのジョブを並列で実行することもできますし、異なるジョブを同時実行することもできます。トータルでかかる時間を減らしやすくなると思いました。これは気にしていこう。

Exit Code 137 問題

私は Java で SpringBoot 使ったアプリケーションのビルドに使うことが多いので、gradle 使うことが多いです。 よく目にするのが、OOM でジョブが失敗する、という奴です。

ジョブを動かすリソースクラスを良いものにすれば大体解決はするのですが、コストが跳ね上がります。 そのために、メモリチューニングする必要があるのですが、GRADLE_OPTS-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2 しましょうと書いてあるのに好感が持てました。確かにサポートページに書いてあるのですが、これだけでも本書を読む価値ありだと思います。

gradle のプラグインによっては、環境変数でなく設定の方でメモリ指定する奴もいるのでご注意くださいませ。

まとめ

CI / CD のインフラ部分を面倒見られる体制があるなら考えなくて良いかもしれませんが、そんな余裕ない or リソースを他のことに回したいのであれば SaaS の検討しても良いんじゃないかと思います。他の SaaS は正直使ったことないのですが、CircleCI は安定していてお勧めできると思います。

目下の悩みは、機能追加に伴ってテストの数も増えていって、CI 回すたびに時間(+SaaS のコスト)がかかる問題の折り合いをどうつけるかなのですが、誰か相談できる人おらんかな...。