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);
}
Тогда соединение благополучно переиспользуется.
27 декабря 2017 21:27

Мимо проходил (инкогнито)
28 декабря 2017, 09:34

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

bolknote.ru (bolknote.ru)
28 декабря 2017, 10:29, ответ предназначен Мимо проходил

у конструктора есть недокументированный второй параметр — колбэк, который вызывается при создании «свежего» объекта
Спасибо! Надеюсь это скоро документируют, без крайней необходимости не люблю использовать недокументированные вещи в продакшне.
принудительное закрытие подключения (send_quit) при изменении некоторых опций происходит в самой libmemcached
Насколько я помню (лень искать в доке) где-то написано, что setOption[s] нужно делать до addServer[s]. Мы так всегда и делаем, так что это точно не тот случай.

bolknote.ru (bolknote.ru)
28 декабря 2017, 10:49, ответ предназначен Мимо проходил

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

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

Мимо проходил (инкогнито)
28 декабря 2017, 12:11, ответ предназначен bolknote.ru:

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

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

bolknote.ru (bolknote.ru)
28 декабря 2017, 13:31, ответ предназначен Мимо проходил

Параметру уже 7 лет, так что не думаю, что его скоро документируют
Это ещё больше настораживает. Значит в любой момент его могут убрать, причём описано это нигде не будет.
К персистентности, к слову, это поведение не имеет никакого отношения: точно так же будут закрываться и «обычные» соединения, если они были открыты.
Мои исследования эту мысль не подтверждают. В частности при переключении опции OPT_NO_BLOCK второго connect в логах я не вижу (при постоянных соединениях было два коннекта), более подробно смотреть не стал.
проблема […] в документации
Документация слабая, да. Такая важная вещь как DISTRIBUTION_VIRTUAL_BUCKET вообще никак не описана.

Мимо проходил (инкогнито)
28 декабря 2017, 15:01, ответ предназначен bolknote.ru:

Мои исследования эту мысль не подтверждают.
Закрытия темы ради:
$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 (bolknote.ru)
28 декабря 2017, 16:17, ответ предназначен Мимо проходил

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

Мимо проходил (инкогнито)
28 декабря 2017, 16:53

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 (bolknote.ru)
28 декабря 2017, 21:19, ответ предназначен Мимо проходил

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

Ваше имя или адрес блога (можно OpenID):

Текст вашего комментария, не HTML:

Кому бы вы хотели ответить (или кликните на его аватару)