PostgreSQLのログをFluentdで収集する

2014年10月4日(土)
中西 剛紀

データベースの運用管理にログの情報は不可欠ですが、オープンソースのDBMSであるPostgreSQLが出力するログは、直接取り扱うのに適した形式とは言えません。そこで本記事では、オープンソースのログ収集ツールであるFluentdを使って、PostgreSQLのログを運用管理に活用しやすい形に加工する方法を紹介します。

PostgreSQLの運用管理に使う稼働統計情報とログ

データベースを安定稼働させるには、データベースを健全な状態に保つ運用管理が必要不可欠です。データベースの運用管理には一般的に死活監視、リソース監視、性能分析、チューニング、バックアップといった項目が挙げられますが、まずは監視によって正しく現状を把握することが、運用管理の第一歩と言えるでしょう。PostgreSQLで監視に使える機能には、稼働統計情報とログがあります。稼働統計情報はPostgreSQLの様々な稼働情報を蓄積したもので、通常は専用のビューにSQL文で参照できます。稼働統計情報では、接続中のクライアントの実行状態や実行中のSQL文、データベース単位のコミット/ロールバック回数やSQLの実行回数、テーブル単位で更新されたレコード数といった情報を参照でき、性能分析やチューニングに活用できます(リスト1)。

リスト1:稼働統計情報からデータベースのキャッシュヒット率を取得

# SELECT  datname, round(blks_hit*100/(blks_hit+blks_read), 2)  AS cache_hit_ratio
FROM pg_stat_database WHERE blks_read > 0
datname  | cache_hit_ratio
---------+-----------------
postgres | 99.00

ログにはPostgreSQLサーバから出力された様々な情報が記録されます。例えば、サーバが即停止するような深刻なレベルからサーバの稼働に影響しない警告レベルまで何らかの事象が発生したことを示すメッセージや、処理されたSQL文、チェックポイント、自動VACUUMの実行状況、デッドロックの発生状況といった情報は全てログに出力させることが可能です。またPostgreSQLのログは、任意のファイルにテキスト形式で出力する以外に、syslogやWindowsのイベントログへの出力や、外部ツールで扱いやすいCSV形式での出力も可能です。

リスト2:PostgreSQLのログファイルの中身

[2014-06-26 06:32:14 GMT][11550][53abb2b8.2d1e-21][0][00000](postgres, fluentd, [local], psql) LOG:  duration: 20020.457 ms  statement: select pg_sleep(20);
[2014-06-26 07:36:38 GMT][17077][53abcc5a.42b5-1][0][00000]LOG:  checkpoint starting: time
[2014-06-26 07:36:39 GMT][17077][53abcc5a.42b5-2][0][00000]LOG:  checkpoint complete: wrote 4 buffers (0.0%); 0 transaction log file(s) added, 0 removed, 0 recycled; write=0.303 s, sync=0.004 s, total=0.320 s; sync files=3, longest=0.002 s, average=0.001 s

ログにしか出力されない情報もあるため、運用管理にはログも活用したいところですが、PostgreSQLのログにはいくつか扱いづらい点があります。特にPostgreSQLは1つのファイルに全てのログを出力するため、エラーレベルや事象の種別で分割することができず、ログから必要な情報を探したり、分析に活用したりするのが困難です。そこで本記事では、オープンソースのログ収集ツールであるFluentdを使って、PostgreSQLのログを分析しやすい形に加工する方法を紹介していきます。

Fluentdの概要

Fluentdは軽量でプラガブルなログ収集ツールで、Apache License Version 2.0のもとOSSとして公開されており、自由に使うことができます。またFluentdは、ログ管理を3つの層(インプット、バッファ、アウトプット)に分けて管理しており、しかも各層がプラガブルなアーキテクチャになっています。そのため用途に応じたプラグインを追加するだけで、柔軟に機能拡張できるメリットがあります。Fluentdの詳細はドキュメントをご覧ください。

PostgreSQLのログを加工する

ここからはPostgreSQLのログをFluentdで収集し、分析しやすい形に加工する手順を解説します。図1はPostgreSQLサーバが出力したログをFluentdプロセスが収集・加工して、最終的にPostgreSQLデータベースのテーブルにログデータを格納するまでの処理の流れを図示したものです。なお、テキスト形式のログデータをデータベースのテーブルに格納するのは、使い慣れたSQLでログデータの抽出や分析を可能にすることを狙ったからです。

図1:処理の流れ(クリックで拡大)

なお、今回の環境で利用するソフトウェアは以下のとおりです。

プロダクト名バージョン
CentOS6.4(64bit)
PostgreSQL9.3.5
td-agent (Fluentd)1.1.20(0.10.50)
fluent-plugin-rewrite-tag-filter1.4.1
fluent-plugin-parser0.3.4
fluent-plugin-pgjson0.0.4

Fluentdのセットアップ

Fluentdはバイナリパッケージ、RubyGems、ソースパッケージからと様々なインストール方法があります。今回は「td-agent」という名称でTreasure Data, Inc.から提供されているRPMパッケージを利用します。

リスト3:td-agentのインストール

$ curl -L http://toolbelt.treasuredata.com/sh/install-redhat.sh | sh

PostgreSQLのセットアップ

リスト4:PostgreSQLのインストールパッケージとデータベースへの接続確認

PostgreSQLはサーバ、クライアントモジュールがRPMパッケージですでにインストールされており、psqlコマンドでデータベースに接続できる状態を前提とします。

# rpm -qa | grep postgres
postgresql93-contrib-9.3.5-1PGDG.rhel6.x86_64
postgresql93-libs-9.3.5-1PGDG.rhel6.x86_64
postgresql93-devel-9.3.5-1PGDG.rhel6.x86_64
postgresql93-server-9.3.5-1PGDG.rhel6.x86_64
postgresql93-9.3.5-1PGDG.rhel6.x86_64
# psql -U postgres
psql (9.3.5)
Type "help" for help.

postgres=#

PostgreSQLの設定ファイル(postgresql.conf)は以下のように編集します。今回はCSV形式のログを使用するため、log_destinationに「csvlog」を加えます。なお、ここではログに出せる情報を全て出力する設定にしていますが、ログの量が増大するとパフォーマンスに影響するため、実運用時は本当に必要なログのみ出力するようにしてください。

リスト5:postgresql.confの設定

log_destination = 'stderr,csvlog'           # CSV形式のログを出力
logging_collector       = on
log_directory   = '/tmp'                    # ログを /tmp 配下に出力
log_filename    = 'postgresql.log'
log_file_mode   = '0644'
log_line_prefix         = '[%t][%p][%c-%l][%x][%e]%q (%u, %d, %r, %a)'
log_rotation_age        = 0  # Fluentdからファイル名固定でアクセスするためローテーションを無効化
log_rotation_size       = 0  # Fluentdからファイル名固定でアクセスするためローテーションを無効化
log_min_duration_statement        = '1s'    # 1秒以上かかったSQLをログに出力
log_autovacuum_min_duration  =  0           # 全ての自動VACUUMMのログを出力
log_checkpoints         = 'on'              # チェックポイント処理をログに出力
log_lock_waits  = 'on'                      # ロック待ちのセッション情報をログに出力
log_temp_files  = 0                         # 作成された全ての一時ファイルの情報をログに出力

上記の設定を適用したPostgreSQLサーバは、以下のような形式のCSVログ(postgresql.csv)を生成します。

リスト6:CSVログの中身

2014-06-26 07:19:23.292 GMT,,,14376,,53abc97b.3828,1,,2014-06-26 07:19:23 GMT,,0,LOG,00000,"database system is ready to accept connections",,,,,,,,,""
2014-06-26 07:19:23.293 GMT,,,14383,,53abc97b.382f,1,,2014-06-26 07:19:23 GMT,,0,LOG,00000,"autovacuum launcher started",,,,,,,,,""
2014-06-26 07:22:20.667 GMT,"postgres","fluentd",14782,"[local]",53abc9ff.39be,1,"SELECT",2014-06-26 07:21:35 GMT,3/0,0,LOG,00000,"duration: 30031.664 ms  statement: select pg_sleep(30);",,,,,,,,,"psql"

Fluentdのプラグインとデータの流れ

ここからは、Fluentdプロセスの中で行われる処理に着目します。今回はCSV形式のサーバログを入力とし、PostgreSQLデータベースへログデータを格納するまでに4つのプラグインを経由しており、それぞれ様々な処理を行っています。それぞれのプラグインの役割と設定方法について、以下に解説していきます。

図2:Fluentdプラグインとデータの流れ(クリックで拡大)

tail インプットプラグイン

ログファイルに書き込まれた情報を常時取り込んでいくインプットプラグインで、Fluentdに標準で組み込まれています。このプラグインを使ってPostgreSQLのCSVログをパースし、後続の処理に渡すJSONデータを生成します。Fluentdの設定ファイルに記載するこのプラグイン用の設定は、以下のようになります。なお、この記事ではFluentdをRPMパッケージでインストールしており、デフォルトで読み込まれる設定ファイルは「/etc/td-agent/td-agent.conf」になります。プラグインの設定は、このファイルに追記していくことにします。

リスト7:tailプラグインの設定(td-agent.confに追記)

<source>
  type      tail
  path      /tmp/postgresql.csv
  pos_file   /var/log/td-agent/postgresql.log.pos
  tag       postgresql
  format    multiline
  format_firstline  /^\d{4}-\d{2}-\d{2}/
  format1   /^(?<time>[^",]*),"?(?<user_name>(?:[^",]|"")*)"?,"?(?<database_name>(?:[^",]|"")*)"?,(?<process_id>[^",]*),"?(?<connection_from>(?:[^",]|"")*)"?,(?<session_id>[^",]*),(?<session_line_num>[^",]*),"?(?<command_tag>(?:[^",]|"")*)"?,(?<session_start_time>[^",]*),(?<virtual_transaction_id>[^",]*),(?<transaction_id>[^",]*),(?<error_severity>[^",]*),(?<sql_state_code>[^",]*),"?(?<message>(?:[^"]|"")*)"?,(?<detail>[^",]*),"?(?<hint>(?:[^",]|"")*)"?,(?<internal_query>[^",]*),(?<internal_query_pos>[^",]*),(?<context>[^",]*),"?(?<query>(?:[^"]|"")*)"?,(?<query_pos>[^",]*),(?<location>[^",]*),"?(?<application_name>(?:[^",]|"")*)"?$/
</source>

PostgreSQLのログは、1件の情報内に改行が含まれる可能性があるため、「format」は複数行に対応可能な「multiline」に設定し、ログを区切る文字列を「format_firstline」に指定します。また、「format1」の部分にCSVデータのカラムをパースしてJSONデータに変換するルールを正規表現で定義し、PostgreSQLのログデータであることを区別するため「postgresql」という名前のタグを付けることにします。
上記の設定により、PostgreSQLのCSVログは以下のようなJSONデータに変換されます。

リスト8:変換されたJSONデータ

{
  "tag": "postgresql",
  "time": "2014-06-30 01:12:02+00",
  {"user_name":"","database_name":"","process_id":"27136","connection_from":"","session_id":"53b0b980.6a00",
    "session_line_num":"10","command_tag":"","session_start_time":"2014-06-30 01:12:32 GMT",
    "virtual_transaction_id":"","transaction_id":"0","error_severity":"LOG","sql_state_code":"00000",
    "message":"checkpoint starting: xlog","detail":"","hint":"","internal_query":"","internal_query_pos":"","context":"",
    "query":"","query_pos":"","location":"","application_name":""}
}

fluent-plugin-rewrite-tag-filter

データの内容によって、タグを書き換えるバッファプラグインです。このプラグインを使い、ログの内容に応じてタグを分別してみます。このプラグインは以下のようにインストールできます。

リスト9:fluent-plugin-rewrite-tag-filterプラグインのインストール

$ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-rewrite-tag-filter

このプラグインの設定は以下のようになります。「rewriterule1~9」の部分に定義された正規表現にログメッセージがマッチしたら、対応するタグに置き換えられます。例えば、スロークエリのログなら「raw.postgresql.slow_query」、一時ファイル出力のログなら「postgresql.tempfiles」という名前のタグが付けられます。

リスト10:fluent-plugin-rewrite-tag-filterプラグインの設定(td-agent.confに追記)

<match       postgresql>
  type      rewrite_tag_filter
  rewriterule1  message  ^duration:                             raw.postgresql.slow_query
  rewriterule2  message  ^checkpoints\sare\soccurring\stoo\sfrequently                                                                                  postgresql.checkpoints.frequently
  rewriterule3  message  ^checkpoint\sstarting:                 postgresql.checkpoint.start
  rewriterule4  message  ^checkpoint\scomplete:                 postgresql.checkpoint.complete
  rewriterule5  message  ^automatic                             postgresql.vacuum
  rewriterule6  message  ^temporary file:                       postgresql.tempfiles
  rewriterule7  message  ^process.*detected\sdeadlock           postgresql.deadlock
  rewriterule8  message  ^process.*(still waiting|acquired)     postgresql.lockwait
  rewriterule9  message  .*                                     postgresql.others
</match>

fluent-plugin-parser

Fluentd上に流れるデータの任意のフィールドを、さらに分解するバッファプラグインです。このプラグインを使ってスロークエリログの処理時間、SQL文のデータを分解し、後から分析しやすくなるよう別の要素としてJSONに付与します。このプラグインも以下のようにインストールできます。

リスト11:fluent-plugin-parserプラグインのインストール

$ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-parser

このプラグインの設定は以下のようになります。「format」の部分にキー「message」のデータを変換するルールを正規表現で定義しています。

リスト12:fluent-plugin-parserプラグインの設定(td-agent.confに追記)

<match raw.postgresql.slow_query>
  type          parser
  remove_prefix raw
  reserve_data  yes
  key_name      message
  format        /^duration: (?<duration>[0-9\.]+) ms  statement: (?<statement>.+)$/
</match>

この設定により、リスト13のスロークエリログは「duration」「statement」というキーが追加されて、リスト14のような形に変換されます。

リスト13:変換前のスロークエリログ

{
  "tag": "raw.postgresql.slow_query",
  "time": "2014-06-30 01:12:02+00",
  “user_name”:“postgres”,“database_name”:“fluentd”, …,
  “message”:“duration: 4004.423 ms  statement: select pg_sleep(4);”, … ,"application_name":"psql"}
}

リスト14:変換後のスロークエリログ

{
  "tag": "postgresql.slow_query",
  "time": "2014-06-30 01:12:02+00",
  “user_name”:“postgres”,“database_name”:“fluentd”, …,
  “message”:“duration: 4004.423 ms  statement: select pg_sleep(4);”, … ,"application_name":"psql",
  "duration":"4004.423",
  "statement":"select pg_sleep(4);"}
}

fluent-plugin-pgjson

ログデータをPostgreSQLのテーブルに出力するアウトプットプラグインです。このプラグインは、出力先にPostgreSQLのJSON型カラムを利用します。このプラグインのインストール方法も、他のものと同様です。

リスト15:fluent-plugin-pgjsonプラグインのインストール

$ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-pgjson

このプラグインを使って、Fluentdで加工したJSON形式のログデータをPostgreSQLデータベース内のテーブルに挿入してみます。まず、ログデータを格納するテーブルをあらかじめ任意のデータベースに作成しておきます。

リスト16:ログデータ格納テーブルの作成

# CREATE DATABASE fluentd;    # fluentdという名のデータベースを作成
# \c fluentd                  # ターゲットデータベースをfluentdに変更
# CREATE TABLE fluentd (      # fluentdという名のテーブルを作成
    tag  Text,  time  Timestamptz,  record  Json
);

このプラグインの設定は以下のようになります。データを格納するPostgreSQLサーバのホスト名、ポート番号、データベース名、テーブル名といった、PostgreSQLデータベースに接続するために必要な情報を定義しています。

リスト17:fluent-plugin-pgjsonプラグインの設定(td-agent.confに追記)

<match {postgresql.**}>
  type          pgjson
  host          localhost
  port          5432
  sslmode       prefer
  database      fluentd
  table         fluentd
  user          postgres
  password      postgres
  time_col      time
  tag_col       tag
  record_col    record
</match>;

ここまでの設定が終わったら、Fluentdのプロセスを起動します。今回はRPMパッケージでインストールしているので、以下のようにserviceコマンドで起動します。

リスト18:Fluentdプロセスの起動

$ sudo service td-agent start

起動してしばらくすると、ログデータ格納テーブルにログデータのレコードが蓄積されていきます。

収集したログをSQLで検索する

PostgreSQLにはJSONデータを操作する演算子が用意されており、JSON型のカラムに格納されたデータの任意のフィールドへSQLでアクセスすることが可能となります。各種のログデータを検索する例を以下に示します。

例: メッセージ深刻度レベルがERRORのメッセージを検索

全てのログメッセージには、重要度を表すメッセージ深刻度レベルが付与されています。これはFluentdテーブルにおけるrecordカラム内のJSONデータに「error_severity」というオブジェクトフィールドで格納されており、以下のSQLでメッセージ深刻度レベルが「ERROR」のものに絞り込んで参照することができます。また、WHERE句に「time」カラムの条件を加えることで、確認したい時間帯を絞り込むことも可能です。

# SELECT time, record->>'user_name' as user, record->>'database_name' as database,
record->>'error_severity' as severity, record->>'message' as message, record->>'query' as query,
record->>'query_pos' as pos
  FROM  fluentd  WHERE record->>'error_severity‘  =  'ERROR';
-[ RECORD 1 ]--------------------------------------------------------------------------------------
time            |  2014-06-30 09:49:12+00
user            |  postgres
database        |  fluentd
severity        |  ERROR
message         |  column ""datname"" does not exist
query           |  select count(*) from fluentd where tag = 'postgresql.slow_query' and datname = 'postgres'
pos             |  70

例: スロークエリを検索(時間が長い10件)

スロークエリログと呼ばれる一定時間(今回の設定では1秒)以上かかったSQLの情報は、遅いSQLをチューニングするために必要な情報です。この情報は「postgresql.slow_query」というタグが付与されてtagカラムに格納されますので、WHERE句にタグを絞り込む条件を付与することでスロークエリの情報のみを参照できます。また、fluent-plugin-parserで「duration」「statement」というオブジェクトフィールドを追加しているため、以下のようにORDER BY句で時間順に並び替えることも可能です。

# SELECT time, record->>'user_name' as user, record->>'database_name' as db,
record->>'duration' as duration, record->>'statement' as statement
FROM  fluentd  WHERE  tag  =  'postgresql.slow_query‘
  ORDER  BY  (record->>'duration')::numeric  desc  LIMIT 10;
         time           |    user    |    db     |  duration  |       statement
------------------------+------------+-----------+------------+-----------------------
2014-07-01 07:32:03+00  |  postgres  |  fluentd  | 10011.009  |  SELECT pg_sleep(10);
2014-07-01 07:34:36+00  |  postgres  |  fluentd  | 10010.756  |  SELECT pg_sleep(10);
2014-07-01 07:32:40+00  |  postgres  |  fluentd  | 10010.394  |  SELECT pg_sleep(10);
2014-07-01 07:37:26+00  |  postgres  |  fluentd  | 10010.393  |  SELECT pg_sleep(10);
2014-07-01 07:32:27+00  |  postgres  |  fluentd  | 10010.384  |  SELECT pg_sleep(10);
2014-07-01 07:34:04+00  |  postgres  |  fluentd  | 10010.358  |  SELECT pg_sleep(10);
2014-07-01 07:34:21+00  |  postgres  |  fluentd  | 10010.342  |  SELECT pg_sleep(10);
2014-07-01 07:36:09+00  |  postgres  |  fluentd  | 10010.327  |  SELECT pg_sleep(10);
2014-07-01 07:36:27+00  |  postgres  |  fluentd  | 10010.279  |  SELECT pg_sleep(10);
2014-07-01 07:37:12+00  |  postgres  |  fluentd  | 10010.259  |  SELECT pg_sleep(10);

例: チェックポイントの実行状況

定期的に実行されるチェックポイントはデータベース負荷を高める原因になることがあるため、実行状況を把握しておくことが好ましいでしょう。この情報は「postgresql.checkpoint」というタグが付与されてtagカラムに格納されますので、WHERE句にタグを絞り込む条件を付与することで参照できます。

# SELECT time, record->>‘message’ as message  FROM fluentd  WHERE  tag  like  'postgresql.checkpoint.%';
          time           |                                    message
-------------------------+-------------------------------------------------------------------------
2014-06-30 01:17:32+00   |  checkpoint starting: time
2014-06-30 01:17:33+00   |  checkpoint complete: wrote 5 buffers (0.0%); 0 transaction log file(s) added, 0 removed, 0 recycled; write=0.403 s, sync=0.463 s, total=0.885 s; sync files=3, longest=0.460 s, average=0.154 s
2014-06-30 01:27:32+00   |  checkpoint starting: time
2014-06-30 01:27:32+00   |  checkpoint complete: wrote 1 buffers (0.0%); 0 transaction log file(s) added, 0 removed, 0 recycled; write=0.000 s, sync=0.002 s, total=0.022 s; sync files=1, longest=0.002 s, average=0.002 s
2014-06-30 01:30:13+00   |  checkpoint starting: xlog
 2014-06-30 01:30:16+00  |  checkpoint complete: wrote 1006 buffers (6.1%); 0 transaction log file(s) added, 0 removed, 0 recycled; write=2.019 s, sync=0.272 s, total=2.396 s; sync files=19, longest=0.161 s, average=0.014 s
 2014-06-30 01:30:21+00  |  checkpoint starting: xlog
 2014-06-30 01:30:24+00  |  checkpoint complete: wrote 1894 buffers (11.6%); 0 transaction log file(s) added, 0 removed, 1 recycled; write=2.223 s, sync=0.209 s, total=2.518 s; sync files=9, longest=0.148 s, average=0.023 s

例: デッドロックの発生情報

PostgreSQLにはデッドロックを自動的に検出して解決する機能がありますが、デッドロックが頻発するのは好ましい状況とはいえませんので、発生状況を把握して対策を講じるべきでしょう。この情報は「postgresql.deadlock」というタグが付与されてtagカラムに格納されますので、WHERE句にタグを絞り込む条件を付与することで参照できます。

# SELECT  time, record->>'database_name' as db, record->>'command_tag' as command,
record->>'message' as message, record->>'query'  as query
 FROM  fluentd  WHERE  tag  =  'postgresql.deadlock';
-[ RECORD 1 ]--------------------------------------------------------------------------------------
time          | 2014-07-02 09:45:02+00
db            | fluentd
command       | LOCK TABLE waiting
message       | process 8184 detected deadlock while waiting for ExclusiveLock on relation 16394 of database 16384 after 1000.235 ms
query         | LOCK TABLE pgbench_history IN EXCLUSIVE MODE;

PostgreSQLにJSON形式のログを格納する意義

JSON型は、PostgreSQLに標準で用意されているデータ型です。PostgreSQLのバージョンアップに伴いJSONサポートの機能が強化されており、PostgreSQLでJSONを扱いやすくする土壌が整いつつあります。一般的にログのようなデータは非定型な場合が多く、スキーマを一律に定義することが難しいため、ログをデータベースに格納する場合はスキーマ定義が不要なMongoDBを使うケースが多かったのではないでしょうか。ログデータをPostgreSQLに格納できれば、非定型なログを使い慣れたSQLで検索、分析できるようになります。また、元々PostgreSQLで管理しているマスターデータとのJOINなど、便利な使い方も可能になりそうです。

最後に

本記事ではPostgreSQLのログをFluentdで収集し、後から分析しやすい形に加工する方法を紹介しました。実際に運用管理を行う上でPostgreSQLの標準機能では少々物足りないログ機能も、他のツールと組み合わせることで運用を効率化できるという1つのヒントになればと考えています。

<編集部より> Fluentdの綴りについて、正式には"F"が大文字だったため修正しました。(2014.10.3)

TIS株式会社

戦略技術センター所属。過去に様々なDBMSと苦楽を共にし、現在はPostgreSQLをはじめするOSSの活用推進をテーマに活動中。アプリケーション基盤「ISHIGAKI Template」の開発とともに、日本PostgreSQLユーザ会、PostgreSQLコンソーシアムといったコミュニティを通じてPostgreSQLの普及活動にも力を注いでいる。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています