BigQueryの運用を行っていると定期的に無駄なテーブルを削除することは良くあると思います。
テーブル数が少ない場合はwebからぽちぽちと削除するので対応可能ですが、テーブル数が多い場合はスクリプトを作って削除したほうが楽です。
最初は以下のようなスクリプトを書いていたのですが、削除対象のテーブル数が膨大なため、なかなか終わりませんでした。
※ロギング部分は省略
require 'google/cloud/bigquery'
table_names = [削除したいテーブル名の配列]
project_name = プロジェクト名
dataset_name = データセット名
table_names.each do |table_name|
bq_client = ::Google::Cloud::Bigquery.new(project: project_name)
dataset = bq_client.dataset(dataset_name)
table = dataset.table(table_name)
table.delete
end
テーブルの削除処理はクライアントサイドのCPU・メモリーをほぼ使わず、ほとんどの待ち時間がBigQuery側での処理待をポーリングしているだけであるため、parallelというgemを使って並列化しました。
https://github.com/grosser/parallel
Rubyのマルチスレッド処理の実装はGVLによって同時に実行されるスレッドが1つだけになってしまっていますが、IO系のブロッキング処理に対してはGVLの解放をするので、今回のケースでは高速化が見込まれます。
require 'google/cloud/bigquery'
require 'parallel'
table_names = [削除したいテーブル名の配列]
project_name = プロジェクト名
dataset_name = データセット名
::Parallel.each(table_names, in_threads: 30).each do |table_name|
bq_client = ::Google::Cloud::Bigquery.new(project: project_name)
dataset = bq_client.dataset(dataset_name)
table = dataset.table(table_name)
table.delete
end
これで、爆速になると思いプログラムを実行したところ、一瞬でCPU使用率が100%に張り付きました。
あまりにもこのプログラムがCPUを使うせいで、discordで通話中の相手から「やたらエコーかかっているんですが、風呂場で仕事してます?」と言われてしまいました。
discordはクライアントサイドで音声のノイズ除去などの信号処理をしているらしいというムダ知識が1つ増えました。
さて、あまりにもCPUを使いすぎるので、ちょっと気になってdtraceでシステムコール呼び出しの様子を調べてみたら、大量のforkが呼ばれていました。
どんなサブプロセスを生成しているのかを確認するために、top
コマンドを実行したところ、なぜかRubyが大量のPythonを呼び出していることが分かりました。
CPUが100%になる原因はこれです。
では、google-cloud-rubyのどこコードがPythonを呼び出しているのでしょうか?
ソースコードを確認していたら見つかりました。
gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", &:read)
このpopenメソッドの第一引数は部分は最終的に gcloud config config-helper --format json --verbosity none
になります。
gcloudコマンドはPythonで実装されているため、このpopenによってPythonが呼び出されていました。
この処理はGCPクライアントインスタンスの初期化をする際に呼ばれる処理で、システムデフォルトの認証情報を取得するものです。
::Google::Cloud::Bigquery.new
をループの中で毎回呼び出す必要はなかったので、ループの外に出して1回だけ呼ばれるようにしました。
require 'google/cloud/bigquery'
require 'parallel'
table_names = [削除したいテーブル名の配列]
project_name = プロジェクト名
dataset_name = データセット名
bq_client = ::Google::Cloud::Bigquery.new(project: project_name)
::Parallel.each(table_names, in_threads: 30).each do |table_name|
dataset = bq_client.dataset(dataset_name)
table = dataset.table(table_name)
table.delete
end
これにより、CPUが100%に張り付くことはなくなり、またテーブルの削除速度も大幅に向上しました。