この記事は enechain Advent Calendar 2023 の 14 日目の記事です。 本日は eScan デスクのエンジニアの平田が担当します。
eScan では以前 BigQuery + Argo Workflowsを利用した計算処理基盤の構築 で紹介した通りリスク計算の基盤として BigQuery を使っています。また、他にも非同期処理に Pub/Sub や、ファイルストレージに Cloud Storage を利用するなど Google Cloud Platform のサービスを利用しています。 各自のローカルでの開発時やテストの際には、これらのサービスは実際の GCP のサービスに接続せずエミュレータを利用しています。
今回はこのうち BigQuery Emulator の活用事例と、BigQuery Emulator を利用する中で得た Tips を紹介します。
- BigQuery Emulator導入の背景
- BigQuery Emulatorを利用する際のTips
- 直面した問題とその解決アプローチ
- 正しくパラメータを渡しているのに「not enough query arguments」が発生する問題
- 関数の互換性による問題
- Cloud Storage Emulator(fake-gcs-server)からのインポートが動作しない問題
- 空の配列をうまく処理できない問題
- テストが失敗すると実行にとても長い時間がかかる問題
- データセットやテーブルの作成方法によって異なる振る舞いをすることによる問題
- 謎のエラー 「Error: failed to scan rows: failed to get value layout: invalid character '×' looking for beginning of value」
- おわりに
BigQuery Emulator導入の背景
BigQuery Emulator はローカルで起動できる BigQuery 互換のデータベースです。
eScan では、ローカルでの開発時の計算やデータの保存と自動テストの実行のため BigQuery Emulator を利用しています。BigQuery Emulator を導入した背景には大きく2つの理由があります。
各開発者が自由に使えて壊れても問題ない環境を手軽に構築するため
eScan では PostgreSQL に保存されるデータを BigQuery にコピーして計算に利用しています。 PostgreSQL(RDBMS) のテーブルスキーマが変更になることがあるように、しばしば BigQuery のテーブルのスキーマも変更されることがあります。 共用環境で BigQuery を利用すると誰かが開発時にスキーマを変更してしまうと他の開発者の開発時にエラーが起きてしまうなどの可能性があります。 今は各自に GCP プロジェクトを割り当てておらず、また eScan ではデータセットをサービス共通のものと顧客ごとのデータセットに分けて複数使用するためデータセットで隔離するのも難しく、GCP 上で隔離された環境を作るのは手間がかかります。 これをシンプルにローカルで各自の環境を用意できるようにするため、Emulator を利用しています。
大量の自動テストを費用や認証の問題を気にせず頻度高く実行するため
リスク計算は複雑で、多くのクエリから構成されています。単純なコピーのクエリなどを除いて 70 ファイルほどのクエリがあります。様々なパターンで正常に計算ができることを確認するため、クエリのテストを書いていて、ローカルでの開発時や Pull Request を出した際の CI でテストを実行しています。
多くのテストを頻繁に実行しているため、テスト時のデータは僅かな量ではあるとはいえ BigQuery に大量にリクエストを投げ続けるには気を遣います。これも大半のテストを Emulator で実行することで CI でも docker で Emulator を動かすだけでクエリのテストを実行できています。
BigQuery Emulatorを利用する際のTips
開発用のテーブル作成やシードデータの投入方法
開発やテスト時に利用するテーブルの作成やシードデータの投入には BigQuery Emulator の --data-from-yaml
オプションを使うことができます。
project, dataset, table のスキーマ、データを記述した YAML ファイルを BigQuery Emulator に --data-from-yaml
オプションを渡すと起動時にテーブルを作成しデータを投入しておくことができます。
テストに利用する場合は、テストごとに本物のBigQueryに接続する仕組みを用意しておくといい
後述しますが、BigQuery Emulator も完璧ではないためどうしても本物の BigQuery でなければうまく動作しないパターンはでてきてしまいます。その際に特定のテストだけ BigQuery に逃がすことができるように、接続先を全体で制御するだけでなく個別に制御できるような仕組みを作っておくと良いでしょう。
eScan では NestJS を利用しているため、テストの際に createTestingModule
でモジュールを作成する際に BigQuery の接続先を個別に Inject できるようにしています。これにより特定のテストのみ本物の BigQuery に接続してテストを実行できます。
直面した問題とその解決アプローチ
BigQuery Emulator を使う上では様々な問題があり、Emulator の利用を諦めかけたこともありました。 同じ問題に遭遇して過去に利用を諦めた方や、これから同じエラーに遭遇する方がいらっしゃるかもしれないので、まだ解決には至っていないものも含め我々が遭遇した問題とどう対処したかを紹介します。
正しくパラメータを渡しているのに「not enough query arguments」が発生する問題
BigQuery には RDB でいう Prepared Statement のようにクエリに対してパラメータを渡す仕組みとして Parameterized Query という仕組みがあります。 我々のクエリでは必ず計算リクエストの ID や計算を要求している組織の ID をパラメータとして渡すため、Parameterized Query を使っています。 BigQuery Emulator でこの Parameterized Query を使うと、パラメータを全て渡しているにもかかわらず、 not enough query arguments というエラーが発生してしまうことがありました。
これは BEGIN ~ END や BEGIN TRANSACTION~COMMIT TRANSACTION 等で囲まれた、複数の文で構成されるクエリを実行した際に発生してしまうものでした。 我々のクエリはほぼすべて、冪等性を担保するためにデータの挿入前に既存のデータを削除するようにしており、1 つの SQL ファイルは複数の SQL 文から構成されています。 そのためほとんどのクエリがテスト実行時エラーになってしまい、これには悩まされました。
これは go-zetasqlite の Parameterized Query の処理の不具合でした。 修正の Pull Request を出して取り込んで頂けたため、 BigQuery Emulator v0.4.3 からこの問題は解消しています。
関数の互換性による問題
BigQuery Emulator は非常に多くの BigQuery の機能や組み込み関数をカバーしていますが、それでも完全に互換性があるわけではありません。
例えば BigQuery の組み込み関数である DATE_DIFF
関数の挙動は BigQuery と異なっています。
現在は BigQuery 互換にする修正の Pull Request が提案されています。
DATE_DIFF など組み込み関数は特殊な引数を受け取ることがあり、UDF で上書きするなどで回避できないため、Emulator ではなく別の方法でテストする必要があります。 このケースでは残念ですが諦めて他の方法でテストするか今後のために issue の報告や改善の PR を提案しましょう。
Cloud Storage Emulator(fake-gcs-server)からのインポートが動作しない問題
BigQuery Emulator を使う場合、同時に Cloud Storage Emulator (fake-gcs-server)を使うケースがあると思います。
BigQuery Emulator では STORAGE_EMULATOR_HOST
を指定することで Cloud Storage Emulator を利用できます。
しかし Cloud Storage Emulator にファイルを配置してインポートしても「ファイルが見つからない」というエラーが発生してファイルをインポートできないことがあります。 これは Cloud Storage Emulator に public host を設定していない、または public host として設定している host と host 部分が異なる URL で Cloud Storage Emulator にアクセスしている場合に発生します。
例えば、Cloud Storage Emulator を以下のように起動している場合、
fake-gcs-server -scheme http -public-host localhost:4443
STORAGE_EMULATOR_HOST
にlocalhost:4443
ではなく127.0.0.1:4443
を指定しているとエラーになります。
Cloud Storage Emulator に public host を設定し、BigQuery Emulator のSTORAGE_EMULATOR_HOST
に同じ host を指定することでこの問題を回避できます。
fake-gcs-server -scheme http -public-host localhost:4443 STORAGE_EMULATOR_HOST=http://localhost:4443 bigquery-emulator
これは BigQuery Emulator の問題とも言い切れませんが、public-host の指定がなくても動作するような提案をしています。
空の配列をうまく処理できない問題
現在の BigQuery Emulator では空の配列の処理に問題があります。 例えば以下のような WHERE 句の条件に空の配列を渡すとエラーになるケースや、 Node.js の BigQuery SDK からクエリを投げるとサーバがハングしてしまうケースがあります。
SELECT * FROM xxx WHERE id IN UNNEST(@ids)
この問題については現在まだ解消していないため、配列が空になるケースでのテストは BigQuery Emulator 上で行わないようにして回避しています。
他にも、空の配列を NULL として扱ってしまうために成功すべき処理が失敗してしまうことがありました。
こちらは修正の Pull Request が最近マージされたため、 v0.4.4 から問題が解消しています。 該当する Pull Request はこちらです。
テストが失敗すると実行にとても長い時間がかかる問題
これは Emulator の問題ではなく BigQuery のクライアントに関するものですが、テストが失敗した際にすぐに終了せず、非常に長い時間が経過したあとにテストが失敗になることがあります。 しばしば Emulator の挙動により予期せぬ失敗が発生する BigQuery Emulator でのテスト環境ではこれにはなかなか苦しめられます。
これは BigQuery のクライアントがデフォルトではリクエストが失敗した際に自動でリトライを行うことにより発生するものです。 本当に flaky なテストが生まれてしまってリトライが必要という場合でなければ、テスト時には自動リトライをしないようにすることでこの問題は回避できます。
データセットやテーブルの作成方法によって異なる振る舞いをすることによる問題
データセットやテーブルを作成する方法はいくつかありますが、 API で作成した場合と DDL または --data-from-yaml
オプションによって作成したもので振る舞いが異なるケースがあります。
特に API でテーブルを作成した場合に SQL からは問題なく操作できるが API を経由した操作ではテーブルが見つからないというエラーが発生してしまうことがあります。
恐らく内部のメタデータの扱いによるものと思いますが、現状では極力 DDL でテーブルを作成したほうが問題に遭遇することが少なそうです。
謎のエラー 「Error: failed to scan rows: failed to get value layout: invalid character '×' looking for beginning of value」
これは些細なミスにより発生するのですが、エラーメッセージから原因が想像できず、解決が難しかった問題です。
BigQuery Emulator の内部では一部のプリミティブな値を除き、値は型と値をセットにしてエンコードした形式で格納されています。 特定の条件で本来エンコードされるべきカラムの値がエンコードされずにプリミティブ値として保存されてしまうことがあります。 その場合、本来のカラムの型であればデコードしてから値を取り出さなければいけないためデコードしようとしますが、誤ってプリミティブな値が保存されてしまっているためデコードに失敗し、このエラーが発生します。
詳細な原因は特定できていませんが、今まで調査したところでは特定のケースでスキーマの取り違えが起きています。 異なるプロジェクトに同名のデータセット・テーブルがある場合、最初に作成されたテーブルのスキーマが使用されてしまうことがあります。
我々のケースでの原因は次のようなものでした。我々の Emulator にはローカル開発用のプロジェクトと自動テスト用のプロジェクトがあります。 このうちローカル開発用のプロジェクトでは INT64 型で作成されているカラムが、テスト用のプロジェクトでは NUMERIC 型で作成されていました。 これによりローカル開発用のプロジェクトの NUMERIC 型のカラムに INT64 型のデータが挿入されてしまい、Unmarshal の際にエラーが発生していました。
よって、この問題が発生したときは意図しないプロジェクトや、本来のスキーマと異なるスキーマで作成されているテーブルがないか確認しましょう。 そういったものがある場合は削除やスキーマの修正をすることでこの問題を解決できる可能性があります。
おわりに
BigQuery Emulator の活用事例と Tips、 直面した課題を紹介しました。 同じように BigQuery Emulator を使っている方、使おうとしている方の参考になれば幸いです。
明日は @Shunya078 さんから、 enechain のプロダクト開発を支えるデザインシステムについての記事です。
enechain に興味を持った方は以下からご応募よろしくお願いします。