본문으로 이동

미디어위키에서 pecl memcached 클라이언트를 쓴다면: Nagle 알고리즘 끄기

.
차단기가 내려간 철도 건널목 앞에서 자전거를 세우고 기다리는 사람.
© Remontees, CC BY-SA 4.0

Nagle 알고리즘을 들어 보셨나요? 미디어위키에서 memcached를 PHP 확장 기능으로 붙여 쓰고 계신다면 설정 한 줄을 확인해 볼 만합니다. 이 글에서는 그 설정이 캐시 명령 하나를 얼마나 늦추는지 재 보고, 페미위키에서 설정을 바꾼 전후를 비교합니다.

배경

2026년 9월까지 페미위키는 미디어위키의 캐시에 PHP로 짠 memcached 클라이언트(MemcachedPhpBagOStuff)를 썼는데, 이 클라이언트가 문서 하나를 그리는 CPU 시간의 14%쯤을 소켓을 다루는 데 쓰고 있었습니다. 위키미디어 재단은 PHP 확장 기능 memcached를 쓰는 MemcachedPeclBagOStuff를 쓰고 있어서, 페미위키도 10월 초 이걸로 옮겼습니다.[1]

그 뒤로 사용자 기여 특수 문서(특수:기여)를 여는 데 사용자에 따라 5~20초가 걸렸습니다. 같은 기여 목록을 API(list=usercontribs)로 받으면 0.5초 만에 끝났고, 느린 요청을 처리하는 동안 CPU도 데이터베이스 서버도 한가했습니다. 그런데 그 요청 하나의 미디어위키 로그를 모아 보니, 구조적 토론(Flow) 글을 캐시에 넣을 때마다 남는 memcached 경고 수백 줄이 거의 정확히 50ms 간격으로 찍혀 있었습니다. 캐시에 한 번 쓸 때마다 50ms 가까이 무언가를 기다리고 있었던 것입니다.

아래는 그 요청에서 Lowered set() TTL 경고가 남은 시각과 캐시 키를 차례로 뽑은 것입니다.

시각 (UTC)         간격      캐시 키
00:02:28.623169            global:flow-tree:parent:wlh3xmdzd6vti07q:4.9
00:02:28.673144  +50.0ms   global:flow-tree:parent:uifvczbgn9hz94if:4.9
00:02:28.723075  +49.9ms   global:flow-tree:parent:wkonm51fol0jag2q:4.9
00:02:28.773073  +50.0ms   global:flow-tree:parent:tw5ymwvf2omgq2af:4.9
00:02:28.823128  +50.1ms   global:flow-tree:parent:whagekporvo7yjwb:4.9
00:02:28.873120  +50.0ms   global:flow-tree:parent:wb4mqxvy0cocks4x:4.9
00:02:28.923113  +50.0ms   global:flow-tree:parent:ulntjz6dqri91ilm:4.9
00:02:28.983193  +60.1ms   global:flow-tree:parent:uoe2yitxyxwa8ktm:4.9
00:02:29.033084  +49.9ms   global:flow-tree:parent:uw9yi830s1yesxcv:4.9
00:02:29.083103  +50.0ms   global:flow-tree:parent:ucejtawk4j2jmvx3:4.9
…

가설

미디어위키의 캐시 클래스 WANObjectCache는 캐시에 값을 쓸 때 대부분 쓰기가 성공했는지 기다리지 않아도 된다는 WRITE_BACKGROUND 플래그를 붙이고, MemcachedPeclBagOStuff는 이 플래그를 받으면 memcached에 noreply로 명령을 보냅니다(set 키 0 60 3 noreply처럼 명령 끝에 붙이면 memcached가 STORED 같은 답을 보내지 않습니다[2]).[3]

private function noReplyScope( $flags ): ?ScopedCallback {
	if ( $flags !== true && !( $flags & self::WRITE_BACKGROUND ) ) {
		return null;
	}
	$client = $this->client;
	$client->setOption( Memcached::OPT_NOREPLY, true );
	// ...
}

쓰기를 빠르게 하려는 장치이지만, 같은 클래스는 기본 설정으로 Nagle 알고리즘을 켜 둡니다.[3]

'allow_tcp_nagle_delay' => true
// ...
Memcached::OPT_TCP_NODELAY => !$params['allow_tcp_nagle_delay'],

Nagle 알고리즘은 앞서 보낸 데이터가 아직 확인 응답(ACK)을 받지 못했으면 다음 작은 패킷을 보내지 않고 기다리게 합니다.[4] 보통은 서버가 답을 보내면서 ACK를 함께 실어 보내므로 문제가 없습니다. 하지만 noreply 명령에는 memcached가 아무것도 답하지 않습니다. 실어 보낼 데이터가 없으니 서버의 커널은 지연 ACK 타이머가 끝날 때까지 ACK를 미루고, 그동안 클라이언트의 다음 명령은 Nagle 알고리즘에 막혀 나가지 못합니다.

이 가설이 맞다면 다음과 같은 결과가 나와야 합니다.

  • noreply로 쓴 바로 다음에 답이 필요한 명령을 보내면, 리눅스의 지연 ACK 타이머인 40ms 남짓만큼 늦어집니다.
  • noreply 쓰기만 이어서 보내거나 답을 받는 쓰기를 하면 늦어지지 않습니다.
  • Nagle 알고리즘을 끄면 어느 경우에도 늦어지지 않습니다.

실험

미디어위키 밖에서 MemcachedPeclBagOStuff와 같은 옵션으로 memcached에 연결해, 세 가지 경우를 Nagle 알고리즘을 켜고 끈 채로 200번씩 반복했습니다. 우분투 24.04에 memcached 1.6.24와 php-memcached 3.2.0을 설치하고, 같은 컴퓨터의 루프백 주소로 연결했습니다.

<?php
function client( bool $nagle ): Memcached {
	// MemcachedPeclBagOStuff가 쓰는 옵션
	$c = new Memcached();
	$c->setOptions( [
		Memcached::OPT_NO_BLOCK => false,
		Memcached::OPT_BUFFER_WRITES => false,
		Memcached::OPT_NOREPLY => false,
		Memcached::OPT_BINARY_PROTOCOL => false,
		Memcached::OPT_TCP_NODELAY => !$nagle,
		Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
	] );
	$c->addServer( '127.0.0.1', 11211 );
	return $c;
}

function bgSet( Memcached $c, string $k, $v ): void {
	// noReplyScope()와 같은 순서
	$c->setOption( Memcached::OPT_NOREPLY, true );
	$c->set( $k, $v, 60 );
	$c->setOption( Memcached::OPT_NOREPLY, false );
}

$n = 200;
$value = str_repeat( 'x', 300 );
foreach ( [ true, false ] as $nagle ) {
	$c = client( $nagle );
	$c->get( 'warmup' );

	$t = hrtime( true );
	for ( $i = 0; $i < $n; $i++ ) {
		bgSet( $c, "a$i", $value );
		$c->get( "a$i" );
	}
	$a = ( hrtime( true ) - $t ) / 1e6 / $n;

	$t = hrtime( true );
	for ( $i = 0; $i < $n; $i++ ) {
		bgSet( $c, "b$i", $value );
	}
	$c->get( 'b0' );
	$b = ( hrtime( true ) - $t ) / 1e6 / $n;

	$t = hrtime( true );
	for ( $i = 0; $i < $n; $i++ ) {
		$c->set( "c$i", $value, 60 );
		$c->get( "c$i" );
	}
	$d = ( hrtime( true ) - $t ) / 1e6 / $n;

	printf( "Nagle %-3s  %6.2f ms  %6.2f ms  %6.2f ms\n", $nagle ? 'on' : 'off', $a, $b, $d );
}

결과

시간 (밀리초)경우01020304050noreply 쓰기 뒤 읽기답을 받는 쓰기 뒤 읽기Nagle 켬(기본값)Nagle 끔캐시 명령 한 번에 걸린 시간

세 번을 돌려도 0.1ms 안쪽으로 같은 값이 나왔고, 세 예측이 모두 맞았습니다. noreply 쓰기만 이어서 보낼 때는 Nagle 알고리즘이 작은 패킷을 모아 보내므로 거의 느려지지 않습니다. 하지만 미디어위키는 캐시에 값을 쓰고 곧이어 다른 값을 읽는 일이 흔해서, 첫 번째 경우가 요청마다 여러 번 일어납니다.

운영 환경에서 확인

페미위키에는 2026년 11월 8일 0시 38분(UTC)에 Nagle 알고리즘을 끈 설정이 반영되었습니다.[5] 반영 전후 하루씩 웹 서버 로그에서 로그인하지 않은 방문자의 특수:기여 요청을 모아 보면, 응답 시간의 90번째 백분위수가 8.94초에서 0.45초로 줄었습니다. 구조적 토론은 글 하나마다 캐시 항목을 하나씩 쓰기 때문에, 구조적 토론 글이 많은 사용자의 기여 목록일수록 크게 빨라졌습니다.

시간 (초)사용자0510152025사용자 A사용자 C사용자 E반영 전반영 후사용자 기여 목록 응답 시간 (90번째 백분위수)

적용 방법

$wgObjectCaches에서 MemcachedPeclBagOStuff를 쓰는 항목에 allow_tcp_nagle_delay를 false로 넣으면 됩니다.

$wgObjectCaches['memcached-pecl'] = [
	'class' => \Wikimedia\ObjectCache\MemcachedPeclBagOStuff::class,
	'loggroup' => 'memcached',
	'allow_tcp_nagle_delay' => false,
];

($wgObjectCaches의 기본값에 있는 memcached-pecl 항목을 덮어쓰는 예시입니다. 페미위키처럼 이름을 따로 지어 쓰고 계신다면 그 항목에 한 줄만 더하시면 됩니다.)

위키미디어 재단은 운영 설정에서 이 값을 꺼 두었기 때문에 이 문제를 겪지 않습니다.[6] 또 $wgMainCacheType = CACHE_MEMCACHED처럼 기본 이름을 쓰면 noreply를 쓰지 않는 순수 PHP 클라이언트가 쓰이므로 역시 해당하지 않습니다. 그러니 memcached-pecl을 직접 고르신 경우에만 확인해 보시면 됩니다.

기본값의 역사

이 옵션은 2019년 7월 미디어위키 코어에 들어왔습니다.[7] 그전까지 미디어위키는 Nagle 알고리즘을 따로 설정하지 않아 켜진 채로 쓰였으니, 기본값을 true로 둔 것은 원래 동작을 바꾸지 않으려던 것으로 보입니다. 위키미디어 재단은 2020년 2월 운영 설정에서 이 값을 껐습니다.[8]

noreply 쓰기는 그보다 늦은 2022년 7월, 캐시 쓰기를 기다리지 않게 하려고 들어왔습니다.[9] 재단 서버는 이미 Nagle 알고리즘을 끈 상태였기 때문에, 두 설정이 맞물려 생기는 지연은 재단에서 드러나지 않았고 기본값도 그대로 남았습니다.

맺음말

CPU도 데이터베이스도 한가한데 요청이 느리다면, 한 요청의 로그가 일정한 간격으로 찍혀 있지 않은지 보시기를 권합니다. 페미위키는 50ms라는 간격을 보고 네트워크 쪽을 의심할 수 있었습니다.

출처

  1. ↑ “docker(fastcgi): switch MediaWiki's caches to the pecl memcached client · Pull Request #1198 · femiwiki/infra”. 《GitHub》 (영어). 2026년 11월 9일에 확인함. 
  2. ↑ “memcached/doc/protocol.txt at master · memcached/memcached”. 《GitHub》 (영어). 2026년 11월 9일에 확인함. 
  3. ↑ 3.0 3.1 “includes/libs/ObjectCache/MemcachedPeclBagOStuff.php - mediawiki/core - Git at Google”. 《Wikimedia Gerrit》 (영어). 2026년 11월 9일에 확인함. 
  4. ↑ “RFC 896: Congestion Control in IP/TCP Internetworks”. 《RFC Editor》 (영어). 2026년 11월 9일에 확인함. 
  5. ↑ “perf(femiwiki): turn off Nagle's algorithm for the pecl memcached client · Pull Request #1807 · femiwiki/docker-mediawiki”. 《GitHub》 (영어). 2026년 11월 9일에 확인함. 
  6. ↑ “wmf-config/mc.php - operations/mediawiki-config - Git at Google”. 《Wikimedia Gerrit》 (영어). 2026년 11월 9일에 확인함. 
  7. ↑ “objectcache: use OPT_TCP_NODELAY for MemcachedPeclBagOStuff”. 《Wikimedia Gerrit》 (영어). 2026년 11월 9일에 확인함. 
  8. ↑ “Set "allow_tcp_nagle_delay" to false in mc.php”. 《Wikimedia Gerrit》 (영어). 2026년 11월 9일에 확인함. 
  9. ↑ “objectcache: Accelerate async memcached writes with "noreply"”. 《Wikimedia Gerrit》 (영어). 2026년 11월 9일에 확인함.