libpqで複数の接続先を指定

# はじめに
にゃーん
この記事は、[PostgreSQL 10全部ぬこ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/pg10-nuko-only) の24日目のエントリです。
まったく、クリスマス・イブの日だというのに、一体何をやっているんでしょうね俺は。

今日のお題は、libpqの改善項目の話。

# libpqとは
libpqというのは、C言語で書かれたクライアントライブラリ。「今どきC言語なんかでアプリケーション組まねーよ」という人も多いだろうが、PostgreSQLへ接続する言語ライブラリの多くは、実はこのlibpqに依存している

* C++, Perl, Python, Tcl, PHP. Ruby, ...

例外はJDBCドライバくらいかな？

なので、libpqの変更というのは、他の言語でPostgreSQLアプリケーションを組んでいる人にも無関係ではない。もしかすると、各言語ライブラリのアップデートで、libpqの追加機能に対応するかもしれないし。

# PostgreSQL 10で追加された機能

PostgreSQL 10でもlibpqへの改善項目が入っている。

* 複数の接続先指定が可能になった。
*  libpq接続パラメータtarget_session_attrsが追加された。
*  接続文字列にパスワードファイル名を指定可能になった。
*  PQencryptPasswordConn()でmd5だけでなSCRAM認証用のパスワードが生成可能になった。

今日は、追加された機能のうち、複数の接続先指定について検証してみる。

## 複数の接続先指定
PostgreSQL文書での複数の接続先の指定は、以下のSectionに書かれている。
[Specifying Multiple Hosts](https://www.postgresql.org/docs/10/static/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS)

これをざっと読んだ感じでは、
```'host=host1,host2 port=5432,5432 '```
のようにカンマで区切って値を記述するという形式のようだ。
ただ、この**複数記述の意味があるのは、host(hostaddr)とportのみ。**
他のパラメータ、例えば```dbname=test,test```と書いても、2つのtestという値にはならず、```test,test```という値でdbnameが解釈される。

# 検証モデル
せっかくなので、PostgreSQL 10の他の機能と組合せてみる。

![LOGREP+libpq.png](https://qiita-image-store.s3.amazonaws.com/0/32626/272c7f24-f992-4b6d-9017-65bfb42ad7ba.png)

1. 入力APからテーブルにデータを投入する。
2. そのテーブルをPUBLISHERとして、2つの別サーバ上のテーブルをSUBSCRIBERとした構成で、ロジカルレプリケーションを行う。
3. 分析APからレプリケーションされたテーブルへアクセスして検索を行う。このときに、1つのサーバがダウンしていたら、自動的にもう1つのサーバに接続するようにする。

今回は、この3.の部分をlibpqの新機能で試してみることにした。
といっても手元のマシンで実施するので、全て同じサーバ上で

* 入力サーバはport=10000
* レプリカ1はport=10001
* レプリカ2はport=10002

といい構成で試してみることにした。
libpqを使ったテストプログラムは、第1引数に接続文字列を、第2引数に任意のSELECT文を記述する。
実行すると、最初に接続したPostgreSQLサーバのポート番号を出力し、その後に指定したSELECT文を実行する。
（なお、元ネタはPostgreSQL 10.1ソースの```src/test/examples/testlibpq.c```である）

## 実行結果
まず、入力サーバ(port=10000)、レプリカ1(port=10001)、レプリカ2(port=10002)が全て起動している状態にする。

```
[nuko@localhost ~]$ ps -ef | grep postgres | grep srv
nuko      2263     1  0 16:49 pts/8    00:00:00 /home/nuko/pgsql/pgsql-10.1/bin/postgres -D /tmp/srv10000
nuko      2521     1  0 16:50 pts/8    00:00:00 /home/nuko/pgsql/pgsql-10.1/bin/postgres -D /tmp/srv10002
nuko     56526     1  0 19:10 pts/9    00:00:00 /home/nuko/pgsql/pgsql-10.1/bin/postgres -D /tmp/srv10001
[nuko@localhost ~]$ 
```

この状態で、以下のようにテストプログラムを実行してみる。

```
[nuko@localhost libpq]$ ./testlibpq \
> 'host=localhost,localhost port=10001,10002 dbname=test user=postgres' \
> 'SELECT SUM(data) FROM table_x'
port=10001
sum            

50105660       
```
接続したサーバのport=10001を表示し、その後にSELECT文を実行している。

次にsrv10001をシャットダウンする。シャットダウン後のpsコマンドの結果はこうなったいる。

```
[nuko@localhost ~]$ ps -ef | grep postgres | grep srv
nuko      2263     1  0 16:49 pts/8    00:00:00 /home/nuko/pgsql/pgsql-10.1/bin/postgres -D /tmp/srv10000
nuko      2521     1  0 16:50 pts/8    00:00:00 /home/nuko/pgsql/pgsql-10.1/bin/postgres -D /tmp/srv10002
[nuko@localhost ~]$ 
```

この状態で、さっきと同じようにテストプログラムを実行する。

```
[nuko@localhost libpq]$ ./testlibpq \
> 'host=localhost,localhost port=10001,10002 dbname=test user=postgres' \
> 'SELECT SUM(data) FROM table_x'
port=10002
sum            

50105660       
```
port=10001への接続はせずに、次に書かれたport=10002のサーバへ接続を行い、その後にSELECT文を実行している(両方とも入力サーバからロジカルレプリケーションしているので、SUMの結果は同じになる)。

# おわりに
今回はlibpqの複数接続先指定を試してみた。
ロジカルレプリケーションを用いて、同じパブリッシャから複数のサーバ上のサブスクライバにレプリケーションするような構成に対して、**libpqライブラリの機能を使うだけで、簡易なHAっぽいことができる**ことが確認できた。

# 参考：該当するリリースノート
本エントリに関連するPostgreSQL 10リリースノートの記載です。

E.2.3.1.6. Monitoring

> * Properly update the statistics collector during REFRESH MATERIALIZED VIEW (Jim Mlodgenski)

# 参考：検証で使ったCソースコード

```c
/*
 * src/test/examples/testlibpq.c
 *
 *
 * testlibpq.c
 *
 *		Test the C version of libpq, the PostgreSQL frontend library.
 */
#include <stdio.h>
#include <stdlib.h>
#include "libpq-fe.h"

static void
exit_nicely(PGconn *conn)
{
	PQfinish(conn);
	exit(1);
}

int
main(int argc, char **argv)
{
	const char *conninfo;
	PGconn	   *conn;
	PGresult   *res;
	int			nFields;
	int			i,
				j;
        const char *sql;

	/*
	 * If the user supplies a parameter on the command line, use it as the
	 * conninfo string; otherwise default to setting dbname=postgres and using
	 * environment variables or defaults for all other connection parameters.
	 */
	if (argc > 2) {
		conninfo = argv[1];
		sql = argv[2];
	} else if (argc > 1) {
		conninfo = argv[1];
		sql= "SELECT 1";
	} else {
		conninfo = "dbname = postgres";
		sql= "SELECT 1";
	}

	/* Make a connection to the database */
	conn = PQconnectdb(conninfo);

	/* Check to see that the backend connection was successfully made */
	if (PQstatus(conn) != CONNECTION_OK)
	{
		fprintf(stderr, "Connection to database failed: conninfo=%s, message=%s",
				conninfo,
				PQerrorMessage(conn));
		exit_nicely(conn);
	}
	/*
	 * print inet_server_port
	 *
	 */
	res = PQexec(conn, "SELECT inet_server_port()");
	if (PQresultStatus(res) != PGRES_TUPLES_OK)
	{
		fprintf(stderr, "PQexec failed: sql=%s,message=%s\n", sql, PQerrorMessage(conn));
		PQclear(res);
		exit_nicely(conn);
	}

	printf("port=%s\n", PQgetvalue(res, 0, 0));
	PQclear(res);

	/*
	 * Our test case here involves using a cursor, for which we must be inside
	 * a transaction block.  We could do the whole thing with a single
	 * PQexec() of "select * from pg_database", but that's too trivial to make
	 * a good example.
	 */

	res = PQexec(conn, sql);
	if (PQresultStatus(res) != PGRES_TUPLES_OK)
	{
		fprintf(stderr, "PQexec failed: sql=%s,message=%s\n", sql, PQerrorMessage(conn));
		PQclear(res);
		exit_nicely(conn);
	}

	/* first, print out the attribute names */
	nFields = PQnfields(res);
	for (i = 0; i < nFields; i++)
		printf("%-15s", PQfname(res, i));
	printf("\n\n");

	/* next, print out the rows */
	for (i = 0; i < PQntuples(res); i++)
	{
		for (j = 0; j < nFields; j++)
			printf("%-15s", PQgetvalue(res, i, j));
		printf("\n");
	}

	PQclear(res);

	/* close the connection to the database and cleanup */
	PQfinish(conn);

	return 0;
}
```