HAProxyのserver stateについてコードリーディング

はじめに

kazeburoさんのツイートを見て興味が湧いたのでちょっと見てみました。なお私自身は普段HAProxy使ってないです。

リーディング対象は https://github.com/haproxy/haproxy/commit/c4913f6b54e8b323b9ecbd2a711b2cbf486afae0 です。

なお以下のコードの引用ではインデントが深い行は適宜調整しています。

コードリーディング

まず UP 1/3 の文字列を組み立てている箇所を探してみます。

$ rg '"UP '
src/stats.c
2045:   [SRV_STATS_STATE_UP_GOING_DOWN]         = "UP %d/%d",

src/check.c
958:                                  "UP %d/%d", "UP",

stats.c

https://github.com/haproxy/haproxy/blob/c4913f6b54e8b323b9ecbd2a711b2cbf486afae0/src/stats.c#L2025-L2053

enum srv_stats_state {
	SRV_STATS_STATE_DOWN = 0,
	SRV_STATS_STATE_DOWN_AGENT,
	SRV_STATS_STATE_GOING_UP,
	SRV_STATS_STATE_UP_GOING_DOWN,
	SRV_STATS_STATE_UP,
	SRV_STATS_STATE_NOLB_GOING_DOWN,
	SRV_STATS_STATE_NOLB,
	SRV_STATS_STATE_DRAIN_GOING_DOWN,
	SRV_STATS_STATE_DRAIN,
	SRV_STATS_STATE_DRAIN_AGENT,
	SRV_STATS_STATE_NO_CHECK,

	SRV_STATS_STATE_COUNT, /* Must be last */
};

static const char *srv_hlt_st[SRV_STATS_STATE_COUNT] = {
	[SRV_STATS_STATE_DOWN]			= "DOWN",
	[SRV_STATS_STATE_DOWN_AGENT]		= "DOWN (agent)",
	[SRV_STATS_STATE_GOING_UP]		= "DOWN %d/%d",
	[SRV_STATS_STATE_UP_GOING_DOWN]		= "UP %d/%d",
	[SRV_STATS_STATE_UP]			= "UP",
	[SRV_STATS_STATE_NOLB_GOING_DOWN]	= "NOLB %d/%d",
	[SRV_STATS_STATE_NOLB]			= "NOLB",
	[SRV_STATS_STATE_DRAIN_GOING_DOWN]	= "DRAIN %d/%d",
	[SRV_STATS_STATE_DRAIN]			= "DRAIN",
	[SRV_STATS_STATE_DRAIN_AGENT]		= "DRAIN (agent)",
	[SRV_STATS_STATE_NO_CHECK]		= "no check"
};

enum srv_stats_stateSRV_STATS_STATE_UP_GOING_DOWN"UP %d/%d"という文字列が対応しています。

srv_hlt_st で検索してみると/の前後の値を指定している箇所がありました。

https://github.com/haproxy/haproxy/blob/c4913f6b54e8b323b9ecbd2a711b2cbf486afae0/src/stats.c#L2242-L2245

chunk_appendf(out,
  srv_hlt_st[state],
  (ref->cur_state != SRV_ST_STOPPED) ? (ref->check.health - ref->check.rise + 1) : (ref->check.health),
  (ref->cur_state != SRV_ST_STOPPED) ? (ref->check.fall) : (ref->check.rise));

SRV_ST_STOPPEDの定義を見るとenum srv_stateの中にありました。

https://github.com/haproxy/haproxy/blob/c4913f6b54e8b323b9ecbd2a711b2cbf486afae0/include/haproxy/server-t.h#L46-L52

/* server states. Only SRV_ST_STOPPED indicates a down server. */
enum srv_state {
	SRV_ST_STOPPED = 0,              /* the server is down. Please keep set to zero. */
	SRV_ST_STARTING,                 /* the server is warming up (up but throttled) */
	SRV_ST_RUNNING,                  /* the server is fully up */
	SRV_ST_STOPPING,                 /* the server is up but soft-stopping (eg: 404) */
} __attribute__((packed));

SRV_STATS_STATE_UP_GOING_DOWNはおそらくSRV_ST_STOPPINGだろうということで、"UP %d/%d"/の前はref->check.health - ref->check.rise + 1/の後はref->check.fallになりそうです。

health, fall, riseフィールドの定義はstruct check内にありました。 https://github.com/haproxy/haproxy/blob/c4913f6b54e8b323b9ecbd2a711b2cbf486afae0/include/haproxy/check-t.h#L171-L173

	int health;				/* 0 to rise-1 = bad;
						 * rise to rise+fall-1 = good */
	int rise, fall;				/* time in iterations */

HAProxy “server status” rise fallとかで検索するとUsing HAProxy as an API Gateway, Part 3 [Health Checks] - HAProxy Technologiesにriseとfallの説明がありました。

fall 	The number of failed checks before marking the server as down.
rise 	The number of successful checks before marking a server as up again.

check.c

"UP の文字列があるのは以下の箇所です。 https://github.com/haproxy/haproxy/blob/c4913f6b54e8b323b9ecbd2a711b2cbf486afae0/src/check.c#L950-L981

/* Builds the server state header used by HTTP health-checks */
int httpchk_build_status_header(struct server *s, struct buffer *buf)
{
	int sv_state;
	int ratio;
	char addr[46];
	char port[6];
	const char *srv_hlt_st[7] = { "DOWN", "DOWN %d/%d",
				      "UP %d/%d", "UP",
				      "NOLB %d/%d", "NOLB",
				      "no check" };

	if (!(s->check.state & CHK_ST_ENABLED))
		sv_state = 6;
	else if (s->cur_state != SRV_ST_STOPPED) {
		if (s->check.health == s->check.rise + s->check.fall - 1)
			sv_state = 3; /* UP */
		else
			sv_state = 2; /* going down */

		if (s->cur_state == SRV_ST_STOPPING)
			sv_state += 2;
	} else {
		if (s->check.health)
			sv_state = 1; /* going up */
		else
			sv_state = 0; /* DOWN */
	}

	chunk_appendf(buf, srv_hlt_st[sv_state],
		      (s->cur_state != SRV_ST_STOPPED) ? (s->check.health - s->check.rise + 1) : (s->check.health),
		      (s->cur_state != SRV_ST_STOPPED) ? (s->check.fall) : (s->check.rise));

"UP %d/%d"に対応するのはsv_state = 2; /* going down */のケースです。chunk_appendfで引数に指定している/の前後の数はstats.cと同じです。

health =の箇所を見ていたらset_server_check_statusという関数内にhealthの値を増減している箇所が見つかりました。 https://github.com/haproxy/haproxy/blob/c4913f6b54e8b323b9ecbd2a711b2cbf486afae0/src/check.c#L486-L519

ヘルスチェックに成功すると増えて失敗すると減るようになっています。

	switch (check->result) {
	case CHK_RES_FAILED:
		/* Failure to connect to the agent as a secondary check should not
		 * cause the server to be marked down.
		 */
		if ((!(check->state & CHK_ST_AGENT) ||
		    (check->status >= HCHK_STATUS_L57DATA)) &&
		    (check->health > 0)) {
			_HA_ATOMIC_INC(&s->counters.failed_checks);
			report = 1;
			check->health--;
			if (check->health < check->rise)
				check->health = 0;
		}
		break;

	case CHK_RES_PASSED:
	case CHK_RES_CONDPASS:
		if (check->health < check->rise + check->fall - 1) {
			report = 1;
			check->health++;

			if (check->health >= check->rise)
				check->health = check->rise + check->fall - 1; /* OK now */
		}

		/* clear consecutive_errors if observing is enabled */
		if (s->onerror)
			HA_ATOMIC_STORE(&s->consecutive_errors, 0);
		break;

	default:
		break;
	}

これを踏まえてもう一度

(s->cur_state != SRV_ST_STOPPED) ? (s->check.health - s->check.rise + 1) : (s->check.health),
(s->cur_state != SRV_ST_STOPPED) ? (s->check.fall) : (s->check.rise));

のコードを見ると、理解しやすいのはs->cur_state != SRV_ST_STOPPEDfalseのケース(:の右側)、つまり分子がhealthで分母がriseです。こちらはrise回ヘルスチェックが成功したらサーバを起動状態(up)とマークするけど現在はそのうちhealth回成功したという状況ということですね。

s->cur_state != SRV_ST_STOPPEDtrueのケース(:の左側)、つまり分子がhealth-rise+1で分母がfallです。こちらは分母の方はfall回ヘルスチェックが失敗したらサーバを停止状態(down)とマークすることを意味しているのでしょうが、分子はちょっと複雑です。

上のコードに

			if (check->health >= check->rise)
				check->health = check->rise + check->fall - 1; /* OK now */

という箇所があってhealthの最大値をrise+fall-1にしています。

struct checkhealthフィールドのコメントにも

	int health;				/* 0 to rise-1 = bad;
						 * rise to rise+fall-1 = good */

とあるので最大値は一致しています。ここからfallを引いたrise-1までhealthが下がるとdownになるけど、そこまであと何回猶予があるかというのがhealth-(rise-1)つまりhealth-rise+1になるということですね。 

というわけでUP 1/3はあと一回ヘルスチェックが失敗したらDOWNになる状態だと思われます。