Treasure Data - Support Engineering Team blog

トレジャーデータのサポートエンジニアリングチームのブログです。

Data Tanksのトラブルシューティング

こんにちは、Treasure Dataサポートの伊藤です。

今回は前回に引き続きData Tanksについて説明していきます。今回はData Tanksを使っているときに遭遇するトラブルについて解説します。

Data Tanksへ接続できない

Data Tanksへ接続しようとしたときに、下記エラーで接続できないことがあるかもしれません。

FATAL: no pg_hba.conf entry for host "<ip_address>", user "tank_user", database "datatank", SSL on Invalid username or password.

これは、事前に登録したホワイトリストにないIPアドレスからの接続となっていることに起因しています。 接続元が利用するIPアドレスを確認し、登録したIPアドレスからのアクセスとなるようにご対応ください。

ちなみに、Data Connector(SourceによるインポートやWorkflowのtd_load>:などによるインポート)やResult Export(Presto/Hiveで抽出した結果をData Tanksへエクスポートする)では接続元は皆様が利用する端末ではなくTreasure DataのサーバーとなりIPアドレスも皆様のネットワーク環境のものではありません。

Result Export to Data Tanks がエラーで失敗する

Hive/Prestoで抽出した結果を Data Tanks へエクスポートしようとしたときに、下記エラーで失敗してしまうことがあるかもしれません。

2022-09-14 14:11:04.414 +0000 [ERROR] (main): java.io.IOException: ERROR: could not extend file "base/xxxxx/xxxx": No space left on device
  Hint: Check free disk space.

メッセージ内の Hint にもあるように、このエラーはエクスポート先となるData Tanksのディスク領域の空き領域が不足したことでエクスポートできなくなったことを表します。 エクスポートするデータサイズが小さくなるようにカラム数やレコード数を削減するか、格納してある不要なデータ削除でご対応ください。

Result Export to Data Tanks が長時間かかっても終わらない

Result Export 機能を用いて Hive もしくは Presto を使って抽出した結果を Data Tanks へエクスポートした際に、長時間待っても完了しないことがあるかもしれません。

その場合、ジョブのログを見ると下記のように DROP TABLE 文を実行している段階で止まっているように見えることがあります。

...
2022-06-27 22:18:57.417 +0000 [INFO] (0046:task-0000): Loading 352,349 rows (104,863,339 bytes)
2022-06-27 22:19:02.385 +0000 [INFO] (0046:task-0000): > 4.97 seconds (loaded 2,463,541 rows in total)
2022-06-27 22:19:07.906 +0000 [INFO] (0046:task-0000): Loading 224,826 rows (66,989,710 bytes)
2022-06-27 22:19:11.574 +0000 [INFO] (0046:task-0000): > 3.67 seconds (loaded 2,688,367 rows in total)
2022-06-27 22:19:11.730 +0000 [INFO] (0046:task-0000): SQL: DROP TABLE IF EXISTS "destination_table"

こういう状態になってしまった場合、再度実行しても解消しないことが多いです。そのため DROP TABLE 文がなぜ時間がかかってしまっているのかを特定しなければいけません。 概ねこの状況は、他の処理が対象テーブルに対してロックを獲得していることが原因なので、細かく解説していきます。

ロックとは

ロックとは端的に言ってしまえば排他制御の仕組みのうちの1つです。 並行して複数の処理が行われているときに、処理対象となるテーブルやレコードの整合性を保つために使われます。

自分がこのテーブルを使うから他の人は処理できないようにロックしておく、とイメージしただくとわかりやすいでしょう。

そして、後からロックを獲得しようとした処理は既存のロックが解放されるまで待機することになります。また、基本的にロックは獲得したセッションのトランザクションが完了するまで保持されます。

ロックの種類

Data Tanks(PostgreSQL)のロックはいくつか種類があり、ロックする対象(テーブルなのか行なのかなど)とロックのモードの組み合わせが重要です。

今回は対象がテーブルなので、ロックする対象はもちろんテーブルです。 そしてロックのモードはいくつも種類があり、獲得されているロックのモードによってそのロックがどのモードのロックと競合するのかが決まっています。もし獲得されているロックのモードと新たに獲得しようとするロックのモードが競合しない組み合わせの場合は、別のセッションが同時にロックを獲得することができ同時に処理を進めることができます。 また、どの種類の操作(クエリ文)でどのモードのロックが獲得されるのかが決まっています。

ACCESS SHAREモード

このモードのロックは SELECT 文で参照されるテーブルに対して獲得されます。そのためほとんどのモードと競合することはなく、後述する ACCESS EXCLUSIVE モードのロックとのみ競合します。 対象テーブルが同じ場合、すべてのセッションが SELECT 文で参照しているだけなら同時に実行してもデータ整合性の観点では問題ないと直感でもわかるのではないかと思います。

ACCESS EXCLUSIVEモード

このモードはすべてのモードのロックと競合します。そのためこのモードのロックを獲得する必要がある処理は、先行して他セッションが対象テーブルに対してロックを獲得していない場合にのみ処理されます。 察しの良い方は気づいたかと思いますが、このモードのロックは DROP TABLE 文によって獲得されます。テーブルを削除する場合、対象テーブルを使っている他セッションが存在している場合削除できてしまうと他セッションからすると困りますよね。なのでロックという仕組みを使って処理中には削除できないようにしていると考えると良いでしょう。

ロックの状態を確認するには

説明でお察しかと思いますが DROP TABLE 文が完了しないのは ACCESS EXCLUSIVE モードのロックが獲得できないからで、別セッションが対象テーブルに対して何かしらのモードのロックを既に獲得していることが考えられます。 ではそういう状態になっているかどうかはどのようにして確認すればよいでしょうか?

答えはシンプルで、pg_locks という名前のビューで確認することができます。このビューは参照した時点のロックの情報を提供してくれます。ロックを獲得している側やロックを獲得しようとして待っている側の情報が取得できるので、下記カラム を使ってみると良いでしょう。

列名 説明
locktype ロック対象の種類。テーブルの場合relationという値になります。
relation ロック対象となるリレーション(テーブル)のOID
pid ロックを保持、もしくは獲得しようとして待っているサーバプロセスのプロセスID
mode 保持されている、もしくは獲得しようとしているロックのモード
granted ロックが保持されてい場合はtrue、ロックを獲得しようとして待っている場合はfalse

具体的には、例えば下記のようなクエリ文で簡単に確認できるでしょう。

SELECT pid, granted, locktype, mode, relation AS table_oid, relation::regclass AS table_name
  FROM pg_locks l
 WHERE l.pid != pg_backend_pid()
ORDER BY table_name
;

実際にあるセッションが SELECT 文を実行している状態で確認してみると下記のようになります。

datatank=> SELECT usename FROM pg_user WHERE usename=CURRENT_USER;
  usename  
-----------
 tank_user ★★task_userで接続
(1 row)

datatank=> SELECT pid, granted, locktype, mode, relation AS table_oid, relation::regclass AS table_name
  FROM pg_locks l
 WHERE l.pid != pg_backend_pid()
ORDER BY table_name
;
  pid  | granted |  locktype  |      mode       | table_oid |   table_name    
-------+---------+------------+-----------------+-----------+-------------------
 24329 | t       | relation   | AccessShareLock |     16963 | destination_table
...

pidが24329のセッションが destination_table というテーブルに対して AccessShareLock を獲得(grantedがtrue)していることがわかります。

この状態でさらに別のセッションで DROP TABLE 文を実行し、ロックの状態を確認してみます。

datatank=> select usename from pg_user where usename = CURRENT_USER;
  pid  | granted |   locktype    |        mode         | table_oid |   table_name    
-------+---------+---------------+---------------------+-----------+-------------------
 26422 | f       | relation      | AccessExclusiveLock |     16963 | destination_table
 24329 | t       | relation      | AccessShareLock     |     16963 | destination_table
...

pidが26422のセッションが同じテーブルに対して AccessExclusiveLock を獲得しようとしているが競合して待っている(grantedがfalse)ことがわかりますね。 Result Exportジョブが完了しないときもこのような状態になっていることが想定されます。そのため、先行して獲得されているロックが解放されるまで待つ必要があるわけです。

誰がロックを獲得しているの?

ロックの解放を待てば良いと言いましたが、何かしらの理由で意図せずロックが獲得され続けてしまっている場合があります。その場合ロックを獲得しているセッションをkillすることでロックが解放されるのですが、killできるかどうかは誰がロックを獲得しているかが重要ですよね。

先程 pg_locks ビューを確認しましたが、確認結果のpidを使うことで誰がロックを獲得しているのかある程度特定することができます。 その際、別の pg_stat_activity ビューを使います。このビューはプロセス(≒セッション)の現在の活動状況に関連する情報を持っており、下記列を使うと良いでしょう。

列名 説明
pid プロセスID
usename データベースユーザー名
application_name 接続しているクライアントのアプリケーション名
client_addr 接続しているクライアント側のIPアドレス
query 対象プロセスが最後に実行したクエリ文
query_start クエリ文が開始した時刻

クエリ文サンプルは下記になります。

SELECT pid, usename, application_name, client_addr, query_start, query
  FROM pg_stat_activity
 WHERE pid=<pg_locksビューで確認したpid>
;

実際に実行してみると下記のようにロックを獲得しているセッションの情報が確認できます。今回は psql コマンドで接続したため下記のようになっていますが、例えばTableau Desktopで接続している場合は application_name が Tableau Desktop/Desktop/jdbc-server .... という値になったりします。client_addrから接続元も特定できるので、killして良いかどうかの判断材料とできるでしょう。

datatank=> SELECT pid, usename, application_name, client_addr, query_start, query
  FROM pg_stat_activity
 WHERE pid=24329
;
  pid  |  usename  | application_name | client_addr  |          query_start          |             query              
-------+-----------+------------------+--------------+-------------------------------+--------------------------------
 24329 | tank_user | psql             | xx.xx.xxx.xx | 2022-09-15 10:44:34.732635+00 | select * from destination_table;
(1 row)

killしてよいセッションだと判断できたら、下記を実行することでkillしてください。

datatank=> select pg_terminate_backend(24329);

最後に

いかがでしたでしょうか? PostgreSQLについて詳しい方であれば真新しい内容ではないかと思いますが、そうではない場合は役立つケースがあるのではないかと思いますので、本記事が理解の一助になれば幸いです。