その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

memcachedのconn_yieldsはどういう時に増えるのか

memcachedconn_yieldsはどのような時に増えるのかという話です.なおmemcachedはテキストプロトコルかつデフォルトの設定で実行しているものとします (なお,この記事で重要なのは -R がデフォルト,つまり20ということです).

memcached の conn_yields について - tokuhirom's blog

1つのコネクションでコマンドを発行しまくっている場合にここに到達するようだ

と,上記記事にあるとおりなんですが,実際にmemcachedのコードを読みながら試してみたという記録です.

TL;DR

1回のソケットで大量のコマンドを発行している場合にincrementされる.

getコマンドを何度も発行してみる

get 1get 2get 3とクエリを何度も発行していくパターン.

use IO::Socket;
my $socket = IO::Socket::INET->new(
    PeerAddr => 'localhost',
    PeerPort => 11211,
    Proto    => 'tcp',
);

$socket->print("get $_\r\nquit\r\n") for 1..100;

my @lines = $socket->getlines;

$socket->close;

この時は conn_yields がincrementされない.

multi-getで大量のエントリを取得してみる

get 1 2 3...と1つのgetで大量のエントリをひいてくるパターン.

use IO::Socket;
my $socket = IO::Socket::INET->new(
    PeerAddr => 'localhost',
    PeerPort => 11211,
    Proto    => 'tcp',
);

my $keys = join ' ', map { $_ } 1..100;
$socket->print("get $keys\r\nquit\r\n");

my @lines = $socket->getlines;

$socket->close;

この時も conn_yields はincrementされない.

1回のソケットで大量のコマンドを発行してみる

get 1\r\nget 2\r\nget 3\r\n...と1回のリクエストに複数コマンドを詰めるパターン.

use IO::Socket;
my $socket = IO::Socket::INET->new(
    PeerAddr => 'localhost',
    PeerPort => 11211,
    Proto    => 'tcp',
);

my $gets = join "\r\n", map {"get $_"} 1..100;
$socket->print("$gets\r\nquit\r\n");

my @lines = $socket->getlines;

$socket->close;

この場合は conn_yields はincrementされる (memcachedのデフォルトの最大コマンド数の閾値は20なので,その場合は (100 - 20)/20 = 4 ということで conn_yields は4増える).

コードを読んでみる

基本的に memcached.c を読むとわかる.

memcached/memcached.c at 12ea2e4b50a3222412e9e1cffb1253d907c56cd5 · memcached/memcached · GitHub

注目すべきはこのwhileループconn_yields はこのループ内のこの部分でしかincrementされない.
で,どういう時に conn_yields がincrementされるかというと,

  • nreqsという変数が0を下回っていて
  • なおかつstateが conn_new_cmd の時にループが回った

という時にされる (このnreqsというのは「1つのコネクションで何個のコマンドを許容するか」という閾値memcachedコマンドの -R オプション相当).
nreqs はstateが conn_new_cmdの時にループが回るとdecrement される(実装はこの辺).

つまりざっくり言うと,stateが conn_new_cmd の時に何度もループが回ると conn_yields が増加するという事になる.
stateが conn_new_cmd になるのはどういう時かというと色々あるのだけれど,頻度が多いものとしては「コマンド実行後 (正確には結果出力後)」がある.

1コネクション中で大量のコマンドが送られてきた場合,

1. 送られてきた文字列をバッファしつつ読み込みparseする
2. コマンドを実行
3. stateがconn_new_cmdになる (nreqsのdecrement,条件を満足している時は conn_yields のincrement)
4. まだ読み込んでいない文字列がある場合は1に戻る

という挙動をする (実装はこの辺).
例として get 1\r\nget2\r\n という文字列が送られてきた時は get 1\r\n を読み込み・実行した後に get 2\r\n を読み込み,実行する.この時コマンドは2度実行されるので, nreqs-2 されている.

上記の不発だった例のように「getコマンドを何度も発行してみる」という場合はそれぞれのコマンド実行が別のコネクションになっているので,conn_new_cmd時のループは高々1度しか回らない.よってnreqsは-1しかされないので conn_yields はincrementされない.

また「multi-getで大量のエントリを取得してみる」という場合は少し特殊で,送られてきた文字列を読み込む時にバッファ内に収まるのであればそれは通常のコマンド1発のように扱われる.バッファ内に収まらない時は一回読み込んでからstateを conn_waiting に遷移し,更にそこから conn_read に遷移することで続きの文字列を読み込む (読み込み切るまでこれを繰り返す).いくら文字列が長くてもコマンド1発としてみなされるので nreqs は-1しかされない.よって conn_yields はincrementされないということになる (実装はこの辺).




認識が足りなくて,getコマンドを何度も発行してみたり,multi-getで大量のエントリを取得してみたりした時でも conn_yields がincrementされると勘違いしていて,動作検証時にハマったので調べてみました.

所感

Perlの場合,Cache::MemcachedCache::Memcached::Fastを使っていれば1つのコネクションで複数コマンドを発行するケースは無さそうなので, conn_yields のことは考えなくても良さそう?