PostgreSQL/解析/sorry, too many clients already

更新日: 2020-05-10 (日) 00:08:46 (408d)

PostgreSQL/解析

はじめに

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

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

メッセージの出力箇所

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

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

too-many-clients-already.png

メッセージが出る状況

ProcessStartupPacket

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

参考 postmaster/postmaster.c#ProcessStartupPacket

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;
	}
}

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

参考 postmaster/postmaster.c#canAcceptConnections

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;
}

ここでは、CountChildrenMaxLivePostmasterChildrenという二つの関数による返り値の比較が実行されている。これらの関数の内部を見てみる。

参考 postmaster/postmaster.c#MaxLivePostmasterChildren

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

/*
 * 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);
}

参考 postmaster/postmaster.c#CountChildren

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

/*
 * 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;
}

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

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 */

	... 省略
}

実験

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

void
InitProcess(void)
{
	PGPROC	   *volatile *procgloballist;

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

        ...

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

psql: FATAL:  sorry, too many clients already

参考

InitProcess

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

pgproc.png

参考

ProcArrayAdd

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

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

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

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

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

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")));
		}
	}

        ... 省略
shinvalbuffer.png

参考

参考リンク


添付ファイル: filepgproc.png 354件 [詳細] fileshinvalbuffer.png 343件 [詳細] filetoo-many-clients-already.png 384件 [詳細]

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