Пишу, по большей части, про историю, свою жизнь и немного про программирование.

Memcached и persistent connections

В работе с модулем «Мемкешд» есть целая куча подводных камней, которые иногда всплывают в какой-то незначительной строчке документации, иногда — в комментариях к ней, а иногда выясняются в процессе экспериментов.

Упомянутый модуль имеет малозаметную возможность, которая помогает экономии ресурсов в проектах с большой нагрузкой — если в конструктор передать строку, то она станет идентификатором для так называемого «персистент коннекта» — соединения, которое открывается процессом интерпретатора ПХП и живёт между запуском обрабатываемым этим процессом программ.

Оказалось у этого функционала есть особенность:

socket(PF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 4

connect(4, {sa_family=AF_INET, sin_port=htons(11211), sin_addr=inet_addr("127.0.0.1")}, 16) = -1

poll([{fd=4, events=POLLOUT}], 1, 4000) = 1 ([{fd=4, revents=POLLOUT}])
getsockopt(4, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
sendto(4, "version\r\n", 9, MSG_NOSIGNAL, NULL, 0) = 9
recvfrom(4, "VERSION 1.5.1\r\n", 8196, MSG_NOSIGNAL, NULL, NULL) = 15
sendto(4, "quit\r\n", 6, MSG_NOSIGNAL, NULL, 0) = 6
shutdown(4, SHUT_WR)                    = 0
shutdown(4, SHUT_RD)                    = -1 ENOTCONN (Transport endpoint is not connected)
close(4)                                = 0

socket(PF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 4
setsockopt(4, SOL_SOCKET, SO_LINGER, {onoff=1, linger=0}, 8) = 0

connect(4, {sa_family=AF_INET, sin_port=htons(11211), sin_addr=inet_addr("127.0.0.1")}, 16) = -1

poll([{fd=4, events=POLLIN|POLLOUT}], 1, 4000) = 1 ([{fd=4, revents=POLLOUT}])
getsockopt(4, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
sendto(4, "version\r\n", 9, MSG_NOSIGNAL, NULL, 0) = 9
recvfrom(4, "VERSION 1.5.1\r\n", 8196, MSG_NOSIGNAL, NULL, NULL) = 15

В логе выше видно, что процесс постоянно устанавливает новые соединения с сервером мемкешда, несмотря на то, что в коде (поверьте) используется один и тот же постоянный идентификатор.

Мы долго были в недоумении, но в конечно счёте выяснилось, что такое поведение воспроизводится, если после создания объекта выставлять любые опции, влияющие на сокет. В этом случае модуль переоткрывает соединение с сервером, причём неважно открывался сокет с теми же настройками или нет.

Решение — любым доступным способ проверять получили ли мы «свежий» объект или имеем дело с уже открытым соединением и выставлять опции только в первом случае:

$mc = new Memcached('persistent');

if (!$mc->getServerList()) {
    $mc->setOptions([
        Memcached::OPT_NO_BLOCK => true,
    ]);

    $mc->addServer('127.0.0.1', 11211);
}

Тогда соединение благополучно переиспользуется.

9 комментариев
Мимо проходил 2017

Пара моментов:
1) у конструктора есть недокументированный второй параметр — колбэк, который вызывается при создании «свежего» объекта:
$m = new Memcached(’test’, function (Memcached $m) {var_dump($m);});
2) принудительное закрытие подключения (send_quit) при изменении некоторых опций происходит в самой libmemcached:
http://bazaar.launchpad.net/%7Etangent-trunk/libmemcached/1.0/view/head:/libmemcached/behavior.cc#L124

Евгений Степанищев (bolknote.ru) 2017

Комментарий для Мимо проходил:

у конструктора есть недокументированный второй параметр — колбэк, который вызывается при создании «свежего» объекта

Спасибо! Надеюсь это скоро документируют, без крайней необходимости не люблю использовать недокументированные вещи в продакшне.

принудительное закрытие подключения (send_quit) при изменении некоторых опций происходит в самой libmemcached

Насколько я помню (лень искать в доке) где-то написано, что setOption[s] нужно делать до addServer[s]. Мы так всегда и делаем, так что это точно не тот случай.

Евгений Степанищев (bolknote.ru) 2017

Комментарий для Мимо проходил:

Вообще у модуля memcached очень много всего недокументированного. Я уже писал об этом пару раз:
http://bolknote.ru/all/4197
http://bolknote.ru/all/4208

Из более свежего могу отметить:
1) конструктора есть ещё и третий параметр — туда можно передавать параметры для конфигурирования libmemcached
2) недокументированные команды flushBuffers и setBucket
3) у команды getStats есть параметр $type

Мимо проходил 2017

Комментарий для Евгения Степанищева:

Надеюсь это скоро документируют

Параметру уже 7 лет, так что не думаю, что его скоро документируют: https://github.com/php-memcached-dev/php-memcached/commit/73e30fecf922e00f0a379c0040afa1de0b10d59e

А проблема с setOptions() опять же в документации: там должно быть написано, что изменение некоторых (еще лучше — список) опций принудительно закрывает ранее открытые соединения. К персистентности, к слову, это поведение не имеет никакого отношения: точно так же будут закрываться и «обычные» соединения, если они были открыты.

Евгений Степанищев (bolknote.ru) 2017

Комментарий для Мимо проходил:

Параметру уже 7 лет, так что не думаю, что его скоро документируют

Это ещё больше настораживает. Значит в любой момент его могут убрать, причём описано это нигде не будет.

К персистентности, к слову, это поведение не имеет никакого отношения: точно так же будут закрываться и «обычные» соединения, если они были открыты.

Мои исследования эту мысль не подтверждают. В частности при переключении опции OPT_NO_BLOCK второго connect в логах я не вижу (при постоянных соединениях было два коннекта), более подробно смотреть не стал.

проблема […] в документации

Документация слабая, да. Такая важная вещь как DISTRIBUTION_VIRTUAL_BUCKET вообще никак не описана.

Мимо проходил 2017

Комментарий для Евгения Степанищева:

Мои исследования эту мысль не подтверждают.

Закрытия темы ради:

$m = new Memcached();
$m->addServer(’127.0.0.1’, 11211);
$m->set(’test’, ’0’);
$m->setOption(Memcached::OPT_NO_BLOCK, true);
$m->set(’test’, ’1’);
$m->setOption(Memcached::OPT_NO_BLOCK, true);
$m->set(’test’, ’2’);

socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(11211), sin_addr=inet_addr(«127.0.0.1»)}, 16) = -1 EINPROGRESS (Operation now in progress)
poll([{fd=3, events=POLLOUT}], 1, 4000) = 1 ([{fd=3, revents=POLLOUT}])
getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
sendto(3, «set test 0 0 1\r\n0\r\n», 19, MSG_NOSIGNAL, NULL, 0) = 19
recvfrom(3, «STORED\r\n», 8196, MSG_NOSIGNAL, NULL, NULL) = 8
sendto(3, «quit\r\n», 6, MSG_NOSIGNAL, NULL, 0) = 6
shutdown(3, SHUT_WR) = 0
shutdown(3, SHUT_RD) = -1 ENOTCONN (Transport endpoint is not connected)
close(3) = 0
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 3
setsockopt(3, SOL_SOCKET, SO_LINGER, {onoff=1, linger=0}, 8) = 0
connect(3, {sa_family=AF_INET, sin_port=htons(11211), sin_addr=inet_addr(«127.0.0.1»)}, 16) = -1 EINPROGRESS (Operation now in progress)
poll([{fd=3, events=POLLIN|POLLOUT}], 1, 4000) = 1 ([{fd=3, revents=POLLOUT}])
getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
sendto(3, «set test 0 0 1\r\n1\r\n», 19, MSG_NOSIGNAL, NULL, 0) = 19
recvfrom(3, «STORED\r\n», 8196, MSG_NOSIGNAL, NULL, NULL) = 8
sendto(3, «quit\r\n», 6, MSG_NOSIGNAL, NULL, 0) = 6
shutdown(3, SHUT_WR) = 0
shutdown(3, SHUT_RD) = 0
close(3) = 0
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 3
setsockopt(3, SOL_SOCKET, SO_LINGER, {onoff=1, linger=0}, 8) = 0
connect(3, {sa_family=AF_INET, sin_port=htons(11211), sin_addr=inet_addr(«127.0.0.1»)}, 16) = -1 EINPROGRESS (Operation now in progress)
poll([{fd=3, events=POLLIN|POLLOUT}], 1, 4000) = 1 ([{fd=3, revents=POLLOUT}])
getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
sendto(3, «set test 0 0 1\r\n2\r\n», 19, MSG_NOSIGNAL, NULL, 0) = 19
recvfrom(3, «STORED\r\n», 8196, MSG_NOSIGNAL, NULL, NULL) = 8
sendto(3, «quit\r\n», 6, MSG_NOSIGNAL, NULL, 0) = 6
shutdown(3, SHUT_WR) = 0
shutdown(3, SHUT_RD) = 0
close(3)

Евгений Степанищев (bolknote.ru) 2017

Комментарий для Мимо проходил:

Не думаю, что тема закрыта. Я в этой ситуации вижу только один коннект. PHP 7.0.23.

Мимо проходил 2017

strace? Для справки:

libmemcached.x86_64 1.0.16-5.el7 @base
php-cli.x86_64 7.1.12-4.el7.remi @remi-php71
php-pecl-memcached.x86_64 3.0.4-2.el7.remi.7.1 @remi-php71

Евгений Степанищев (bolknote.ru) 2017

Комментарий для Мимо проходил:

strace?

Я просто грепнул по слову «connect», там одна строка. Возиться дальше лень — я с сегодняшнего дня на праздниках уже :)
Пакеты:

libmemcached-1.0.16-5.el7.x86_64
php-cli-7.0.23-1.el7.remi.x86_64
php-pecl-memcached-3.0.3-1.el7.remi.7.0.x86_64