#author("2018-04-02T13:11:44+00:00","default:haikikyou","haikikyou")
[[PostgreSQL/解析]]

#contents

* はじめに [#q022db75]

PostgreSQLを動かしていると「&code(){sorry, too many clients already};」のようなメッセージを見ることがあるかもしれない。これは、接続するクライアント数がPostgreSQLの制限を超えてしまった場合に表示される。ソースコードから、メッセージの出力箇所を探して見ると、メッセージが記述されている箇所が複数存在することに気づく。では、一体どういった時にこのメッセージが出力されるのだろうか?

ここでは、ソースコードを参照しながら動作内容を確認する。

* メッセージの出力箇所 [#ycffa5c8]

postmasterプロセスは、クライアントの接続のaccept後にバックエンドプロセスの最大数をチェックしてforkしているかと思うが、実際にはそうではない。postmasterプロセスは、クライアント接続を受けるとバックエンドプロセスをforkする。その後、forkで生成されたバックエンドプロセスは、自身の初期化プロセスの中で資源の割り当てが可能かを確認し、空きがない場合は、エラーレベル&code(){FATAL};で「&code(){sorry, too many clients already};」をを出力して終了する流れとなっている。

メッセージが出力されうる箇所は、バックエンドプロセスの処理フローで示すと下図の赤色箇所に該当する。

&ref(./too-many-clients-already.png,100%);


* メッセージが出る状況 [#mf6e83e4]

** ProcessStartupPacket [#z00310b6]

この関数は、クライアントからのStartupPacketを読む。キャンセルリクエストであった場合は、当該バックエンドプロセスのキャンセル(SIGINTシグナル)を行なう。この時点では、エラー(&code(){ERRCODE_TOO_MANY_CONNECTIONS};)の制限には引っかからない(つまりacceptが成功すれば、キャンセルリクエストは正常に処理されると言える)。そして、クライアント接続数の制限チェックは、この関数の末尾で実行されている。

&label(warn){参考}; ''postmaster/postmaster.c#ProcessStartupPacket''

#geshi(c,number){{{
static int
ProcessStartupPacket(Port *port, bool SSLdone)
{
	// .... 省略、キャンセルリクエストの場合はキャンセル(SIGINT)を送るので、エラー(ERRCODE_TOO_MANY_CONNECTIONS)には引っかからない。

	/*
	 * If we're going to reject the connection due to database state, say so
	 * now instead of wasting cycles on an authentication exchange. (This also
	 * allows a pg_ping utility to be written.)
	 */
	switch (port->canAcceptConnections)
	{
		case CAC_STARTUP:
			ereport(FATAL,
					(errcode(ERRCODE_CANNOT_CONNECT_NOW),
					 errmsg("the database system is starting up")));
			break;
		case CAC_SHUTDOWN:
			ereport(FATAL,
					(errcode(ERRCODE_CANNOT_CONNECT_NOW),
					 errmsg("the database system is shutting down")));
			break;
		case CAC_RECOVERY:
			ereport(FATAL,
					(errcode(ERRCODE_CANNOT_CONNECT_NOW),
					 errmsg("the database system is in recovery mode")));
			break;
		case CAC_TOOMANY:  // 制限を超えていた場合、ここでFATALで終了する
			ereport(FATAL,
					(errcode(ERRCODE_TOO_MANY_CONNECTIONS),
					 errmsg("sorry, too many clients already")));
			break;
		case CAC_WAITBACKUP:
			/* OK for now, will check in InitPostgres */
			break;
		case CAC_OK:
			break;
	}
}
}}}

なお、この&code(){port->canAcceptConnections};に、&code(){CAC_TOOMANY};フラグが設定されているのは、以下の箇所のようである。

&label(warn){参考}; ''postmaster/postmaster.c#canAcceptConnections''

#geshi(c,number){{{
static CAC_state
canAcceptConnections(void)
{
	CAC_state	result = CAC_OK;

	... 省略

	/*
	 * Don't start too many children.
	 *
	 * We allow more connections than we can have backends here because some
	 * might still be authenticating; they might fail auth, or some existing
	 * backend might exit before the auth cycle is completed. The exact
	 * MaxBackends limit is enforced when a new backend tries to join the
	 * shared-inval backend array.
	 *
	 * The limit here must match the sizes of the per-child-process arrays;
	 * see comments for MaxLivePostmasterChildren().
	 */
	if (CountChildren(BACKEND_TYPE_ALL) >= MaxLivePostmasterChildren())
		result = CAC_TOOMANY;

	return result;
}
}}}

ここでは、&code(){CountChildren};と&code(){MaxLivePostmasterChildren};という二つの関数による返り値の比較が実行されている。これらの関数の内部を見てみる。

&label(warn){参考}; ''postmaster/postmaster.c#MaxLivePostmasterChildren''

ここでは、MaxBackendsの2倍の値を返すようになっている。

#geshi(c,number){{{
/*
 * MaxLivePostmasterChildren
 *
 * This reports the number of entries needed in per-child-process arrays
 * (the PMChildFlags array, and if EXEC_BACKEND the ShmemBackendArray).
 * These arrays include regular backends, autovac workers, walsenders
 * and background workers, but not special children nor dead_end children.
 * This allows the arrays to have a fixed maximum size, to wit the same
 * too-many-children limit enforced by canAcceptConnections().  The exact value
 * isn't too critical as long as it's more than MaxBackends.
 */
int
MaxLivePostmasterChildren(void)
{
	return 2 * (MaxConnections + autovacuum_max_workers + 1 +
				max_worker_processes);
}
}}}

&label(warn){参考}; ''postmaster/postmaster.c#CountChildren''

関数の引数は、&code(){BACKEND_TYPE_ALL};なので、Backendで&code(){dead_end};フラグがたっていない場合にカウントされる。

#geshi(c,number){{{
/*
 * Count up number of child processes of specified types (dead_end children
 * are always excluded).
 */
static int
CountChildren(int target)
{
	dlist_iter	iter;
	int			cnt = 0;

	dlist_foreach(iter, &BackendList)
	{
		Backend    *bp = dlist_container(Backend, elem, iter.cur);

		if (bp->dead_end)
			continue;

		/*
		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
		 * it first and avoid touching shared memory for every child.
		 */
		if (target != BACKEND_TYPE_ALL)
		{
			/*
			 * Assign bkend_type for any recently announced WAL Sender
			 * processes.
			 */
			if (bp->bkend_type == BACKEND_TYPE_NORMAL &&
				IsPostmasterChildWalSender(bp->child_slot))
				bp->bkend_type = BACKEND_TYPE_WALSND;

			if (!(target & bp->bkend_type))
				continue;
		}

		cnt++;
	}
	return cnt;
}
}}}

&code(){dead_end};が&code(){true};になるのは、調べる限り以下の箇所しか見当たらなかった。&code(){canAcceptConnections};が、&code(){CAC_OK};でも&code(){CAC_WAITBACKUP};でもない時である。postmasterの状態が&code(){PM_RUN};の時は、通常&code(){CAC_OK};となるので、forkで生成されたバックエンドプロセスはカウント対象であろう。

#geshi(c,number){{{
static int
BackendStartup(Port *port)
{
	Backend    *bn;				/* for backend cleanup */
	pid_t		pid;

	/*
	 * Create backend data structure.  Better before the fork() so we can
	 * handle failure cleanly.
	 */
	bn = (Backend *) malloc(sizeof(Backend));
	if (!bn)
	{
		ereport(LOG,
				(errcode(ERRCODE_OUT_OF_MEMORY),
				 errmsg("out of memory")));
		return STATUS_ERROR;
	}

	/*
	 * Compute the cancel key that will be assigned to this backend. The
	 * backend will have its own copy in the forked-off process' value of
	 * MyCancelKey, so that it can transmit the key to the frontend.
	 */
	if (!RandomCancelKey(&MyCancelKey))
	{
		free(bn);
		ereport(LOG,
				(errcode(ERRCODE_INTERNAL_ERROR),
				 errmsg("could not generate random cancel key")));
		return STATUS_ERROR;
	}

	bn->cancel_key = MyCancelKey;

	/* Pass down canAcceptConnections state */
	port->canAcceptConnections = canAcceptConnections();
	bn->dead_end = (port->canAcceptConnections != CAC_OK &&
					port->canAcceptConnections != CAC_WAITBACKUP);

	/*
	 * Unless it's a dead_end child, assign it a child slot number
	 */
	if (!bn->dead_end)
		bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
	else
		bn->child_slot = 0;

	/* Hasn't asked to be notified about any bgworkers yet */
	bn->bgworker_notify = false;

#ifdef EXEC_BACKEND
	pid = backend_forkexec(port);
#else							/* !EXEC_BACKEND */
	pid = fork_process();
	if (pid == 0)				/* child */
	{
		free(bn);

		/* Detangle from postmaster */
		InitPostmasterChild();

		/* Close the postmaster's sockets */
		ClosePostmasterPorts(false);

		/* Perform additional initialization and collect startup packet */
		BackendInitialize(port);

		/* And run the backend */
		BackendRun(port);
	}
#endif							/* EXEC_BACKEND */

	... 省略
}
}}}

&label(study){実験};

InitProcessの途中にsleepで停止させ、psqlでクライアント接続を行なった。以下のような感じである。

#geshi(c,number){{{
void
InitProcess(void)
{
	PGPROC	   *volatile *procgloballist;

	wait_if_exists("InitProcess"); // InitProcessというファイルを見つけたら一定秒sleepを繰り返させる。

        ...
}}}

結果、接続数が&code(){MaxLivePostmasterChildren()};で返される値に達した時、以下のメッセージを確認することができた。

#geshi{{{
psql: FATAL:  sorry, too many clients already
}}}

&label(warn){参考};
- [[Postmasterプロセス>PostgreSQL/解析/Postmasterプロセス]]
- [[postmaster.c>https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/postmaster/postmaster.c;h=a8e19c92381dc0de643b1837149ff148026ce175;hb=ad4fb805ad08c86dd6389e6755081dfd7c864416]] - &size(11){&color(gray){https://git.postgresql.org/gitweb/?p=postgresql.git;a=shortlog;h=refs/heads/REL_10_STABLE};};
- [[51.5. メッセージの書式>https://www.postgresql.jp/document/9.6/html/protocol-message-formats.html]] - &size(11){&color(gray){on https://www.postgresql.jp/document/9.6/html/protocol-message-formats.html};}; 
** InitProcess [#i5735c1d]

通常、エラー&code(){ERRCODE_TOO_MANY_CONNECTIONS};に引っかかるのはこの箇所であろう。バックエンドプロセスは、PGPROCという構造体で表現される。このPGPROC構造体は、&code(){max_connections};や&code(){autovacuum_max_workers};、&code(){max_worker_processes};などのパラメータで指定されたサイズ分、共有メモリ上に確保され配置される(詳しくは下図およびソースリンクを参照のこと)。バックエンドプロセスのPGPROCのフリーリストは、ProcGlobal構造体で管理されており、通常のバックエンドプロセスのフリーリストの先頭はfreeProcsメンバ変数が指している。バックエンドプロセスが起動する度に、フリーリストからPGPROC構造体が割り当てられ、クライアント接続数が&code(){max_connections};に達すると、割り当てられるPGPROCがないため、「&code(){sorry, too many clients already};」でエラーとなる。

#ref(./pgproc.png,100%)

&label(warn){参考};
- [[storage/lmgr/proc.c>https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/proc.c;h=bfa84992ea318a4c02dcf5b0bab39bd2d40e44a2;hb=ad4fb805ad08c86dd6389e6755081dfd7c864416#l283]] - &size(11){&color(gray){https://git.postgresql.org/gitweb/?p=postgresql.git;a=shortlog;h=refs/heads/REL_10_STABLE};};
** ProcArrayAdd [#y8810658]

この関数は、PGPROC構造体を引数で受け取り、共有メモリ上のProc配列にprocnoを追加する。バックエンドプロセスは、アクティブなバックエンドとして管理される。

ProcArrayAddのエラーになる箇所を参照すると、「ここでERRCODE_TOO_MANY_CONNECTIONSエラーことはないはずである」と書かれている。通常は、先のPGPROC構造体の割り当て時にfreeProcsがないためにエラーになるはずである。

#geshi(c,number){{{
void
ProcArrayAdd(PGPROC *proc)
{
	ProcArrayStruct *arrayP = procArray;
	int			index;

	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	if (arrayP->numProcs >= arrayP->maxProcs)
	{
		/*
		 * Oops, no room.  (This really shouldn't happen, since there is a
		 * fixed supply of PGPROC structs too, and so we should have failed
		 * earlier.)
		 */
		LWLockRelease(ProcArrayLock);
		ereport(FATAL,
				(errcode(ERRCODE_TOO_MANY_CONNECTIONS),
				 errmsg("sorry, too many clients already")));
	}

        ... 省略
}
}}}
** SharedInvalBackendInit [#k65c2909]

Shared Invalidation Cacheとは、バックエンドプロセス間でシステムカタログなどのキャッシュが無効化されたことを通知する仕組みの事である。

バックエンドごとにProcState構造体が割り当てられる。SharedInvalBackendInit関数では、その初期化が行われているが、このProcState構造体がMaxBackendsを超えた場合にエラーERRCODE_TOO_MANY_CONNECTIONSとなる(ただし、maxBackendsは、MaxBackendsで設定されており、通常この値を超えることはない?と思われる)。

#geshi(c,number){{{
void
SharedInvalBackendInit(bool sendOnly)
{
	int			index;
	ProcState  *stateP = NULL;
	SISeg	   *segP = shmInvalBuffer;

	/*
	 * This can run in parallel with read operations, but not with write
	 * operations, since SIInsertDataEntries relies on lastBackend to set
	 * hasMessages appropriately.
	 */
	LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);

	/* Look for a free entry in the procState array */
	for (index = 0; index < segP->lastBackend; index++)
	{
		if (segP->procState[index].procPid == 0)	/* inactive slot? */
		{
			stateP = &segP->procState[index];
			break;
		}
	}

	if (stateP == NULL)
	{
		if (segP->lastBackend < segP->maxBackends)
		{
			stateP = &segP->procState[segP->lastBackend];
			Assert(stateP->procPid == 0);
			segP->lastBackend++;
		}
		else
		{
			/*
			 * out of procState slots: MaxBackends exceeded -- report normally
			 */
			MyBackendId = InvalidBackendId;
			LWLockRelease(SInvalWriteLock);
			ereport(FATAL,
					(errcode(ERRCODE_TOO_MANY_CONNECTIONS),
					 errmsg("sorry, too many clients already")));
		}
	}

        ... 省略
}}}

#ref(./shinvalbuffer.png,100%)

&label(warn){参考};

- [[storage/ipc/sinvaladt.c>https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/ipc/sinvaladt.c;h=1b9d32b7cff28d6639b44f3b62710987786b9ec4;hb=ad4fb805ad08c86dd6389e6755081dfd7c864416#l253]] - &size(11){&color(gray){https://git.postgresql.org/gitweb/?p=postgresql.git;a=shortlog;h=refs/heads/REL_10_STABLE};};
* 参考リンク [#he9d90bb]

-

PR

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
目次
ダブルクリックで閉じるTOP | 閉じる
GO TO TOP