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);
}
Тогда соединение благополучно переиспользуется.
Пара моментов:
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
Комментарий для Мимо проходил:
Спасибо! Надеюсь это скоро документируют, без крайней необходимости не люблю использовать недокументированные вещи в продакшне.
Насколько я помню (лень искать в доке) где-то написано, что setOption[s] нужно делать до addServer[s]. Мы так всегда и делаем, так что это точно не тот случай.
Комментарий для Мимо проходил:
Вообще у модуля memcached очень много всего недокументированного. Я уже писал об этом пару раз:
http://bolknote.ru/all/4197
http://bolknote.ru/all/4208
Из более свежего могу отметить:
1) конструктора есть ещё и третий параметр — туда можно передавать параметры для конфигурирования libmemcached
2) недокументированные команды flushBuffers и setBucket
3) у команды getStats есть параметр $type
Комментарий для Евгения Степанищева:
Параметру уже 7 лет, так что не думаю, что его скоро документируют: https://github.com/php-memcached-dev/php-memcached/commit/73e30fecf922e00f0a379c0040afa1de0b10d59e
А проблема с setOptions() опять же в документации: там должно быть написано, что изменение некоторых (еще лучше — список) опций принудительно закрывает ранее открытые соединения. К персистентности, к слову, это поведение не имеет никакого отношения: точно так же будут закрываться и «обычные» соединения, если они были открыты.
Комментарий для Мимо проходил:
Это ещё больше настораживает. Значит в любой момент его могут убрать, причём описано это нигде не будет.
Мои исследования эту мысль не подтверждают. В частности при переключении опции OPT_NO_BLOCK второго connect в логах я не вижу (при постоянных соединениях было два коннекта), более подробно смотреть не стал.
Документация слабая, да. Такая важная вещь как DISTRIBUTION_VIRTUAL_BUCKET вообще никак не описана.
Комментарий для Евгения Степанищева:
Закрытия темы ради:
Комментарий для Мимо проходил:
Не думаю, что тема закрыта. Я в этой ситуации вижу только один коннект. PHP 7.0.23.
strace? Для справки:
Комментарий для Мимо проходил:
Я просто грепнул по слову «connect», там одна строка. Возиться дальше лень — я с сегодняшнего дня на праздниках уже :)
Пакеты: