9 заметок с тегом

memcached

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);
}

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

2017   memcached   php   программирование

💢 Проблема с переходом на PHP7: Memcached, часть вторая

В прошлый раз я писал о проблемном Мемкешд в ПХП7 и оказалось, что я не совсем прав. Проблема есть, но её корень я понимал неверно.

Я-то думал, что получение токена cas просто сломали при переезде на следующую версию ПХП, а оказалось это особенность — в ПХП7 используется третья версия модуля, а ней токен получается иначе — надо передать специальный параметр и после вызова токен будет в результирующем массиве.

Черновым кодом это выглядит примерно так (должно работать, но я его не запускал):

if (version_compare(phpversion('memcached'), '3.0.0-dev', '<')) {
    // работаем по-старому
    return $memcached;
} else {
    // возвращаем обёртку
    return new class($memcached) {
        use \Core\ProxyTrait;

        public function __construct($mc)
        {
            $this->setObject($mc);
        }

        public function get($key, callable $cache_cb = null, &$cas_token = null)
        {
            $result = $this->obj->get($key, $cache_cb, Memcached::GET_EXTENDED);

            if ($result === Memcached::GET_ERROR_RETURN_VALUE) {
                return false;
            }

            if ($result) {
                $cas_token = $result['cas'];
                return $result['value'];
            }

            return $result;
        }

        public function getMulti(array $keys, array &$cas_tokens = null, int $flag = null)
        {
            $result = $this->obj->getMulti($keys, Memcached::GET_EXTENDED | $flag);

            if ($result === Memcached::GET_ERROR_RETURN_VALUE) {
                return false;
            }

            if ($result) {
                $values = [];
                $cas_tokens = [];

                foreach ($result as $key => $d) {
                    $values[$key] = $d['value'];
                    $cas_tokens[$key] = $d['cas'];
                }

                return $values;
            }

            return $result;
        }
    }
}

Вся мякотка в последнем параметре Memcached::GET_EXTENDED, он заставляет возвращать соответствующие методы не искомое значение, а массив, содержащий в том числе и cas.

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

2016   memcached   php   php7   программирование

💢 Проблема с переходом на PHP7: Memcached

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

Поэтому этот шаг, к ПХП7 очень труден для больших проектов, вроде тех, который разрабатываем мы. Тем не менее, мы медленно, но верно движемся в финишу. Вчера столкнулись с неожиданной сложностью, на которую мне пришлось потратить вечер.

Оказалось, что в модуле Мемкешд для ПХП7 нет реализации получения токена cas в методах get и getMulti (наверняка нет ещё в каких-то), но мы их не используем. Это видно, например, по прототипу:

Method [ <internal:memcached> public method get ] {

  - Parameters [2] {
    Parameter #0 [ <required> $key ]
    Parameter #1 [ <optional> $cache_cb ]
  }
}

Как видите, параметра cas нет вообще (он должен быть последним). Это печальное обстоятельство подтолкнуло меня к исследованию и к ночи я сделал решение. Возможно кому-то пригодится:

class MemcachedPHP7
{
    use \Core\ProxyTrait;

    public function __construct($mc)
    {
        $this->setObject($mc);
    }

    public function get($key, callable $cache_cb = null, &$cas_token = null)
    {
        switch (func_num_args()) {
            case 1:
                return $this->obj->get($key);
            case 2:
                return $this->obj->get($key, $cache_cb);
            default:
                if ($this->obj->getDelayed([$key], true) === false) {
                    return false;
                }

                $res = $this->obj->fetchAll();

                if ($res === false || !$res) {
                    if ($cache_cb !== null) {
                        if ($cache_cb($this->obj, $key, $value)) {
                            $this->obj->set($key, $value);
                        }
                    } else {
                        $value = false;
                    }
                } else {
                    $cas_token = $res[0]['cas'];
                    $value = $res[0]['value'];
                }

                return $value;
        }
    }

    public function getMulti(array $keys, array &$cas_tokens = null)
    {
        if (func_num_args() === 1) {
            return $this->obj->getMulti($key);
        } else {
            if ($this->obj->getDelayed($keys, true) === false) {
                return false;
            }
            $res = $this->obj->fetchAll();

            if ($res === false) {
                return false;
            }

            $cas_tokens = [];
            $values = [];

            $results = array_column($res, null, 'key');

            foreach ($keys as $key) {
                $cas_tokens[$key] = $results[$key]['cas'];
                $values[$key] = $results[$key]['value'];
            }

            return $values;
        }
    }
}

Трейт ProxyTrait я тут не привожу, там идея простая — он тупо проксирует всё, что получает через магические методы __get, __set, __call и прочие, setObject — метод этого трейта. Очень удобно, если надо оставить всё как есть, за исключением каких-то методов.

В остальном всё основано на том, что в методе getDelayed реализация токена cas есть, его я и использую, чтобы заткнуть эту дыру в функциональности. Работает всё так же как в ПХП 5.6, за исключением того, что в методе getMulti нет реализации последнего параметра — флага, вместо этого всё работает так, как будто он установлен, это ничему не мешает.

2016   memcached   php   php7   программирование

Недокументированный Memcached-2

У модуля Мемкешед для ПХП удивительно плохо с документацией, я иногда заглядываю в его исходники, с целью почитать. Недавно наткнулся на очередную опцию, описания которой в документации нет (причём в документации аналогичного модуля для Пайтона она есть).

Оказываются UDP не единственный способ ускорить работу с Мемкешед, если вам скорость важнее результата операции. Способ хорош тем, что используется протокол TCP (т. е. с гарантированной доставкой), но мы не ждём — нам главное, что кеш-сервер переданное получил, а получилось ли записать — дело десятое.

Посмотрите на пример и обратите внимание на опцию OPT_NOREPLY.

$m = new Memcached;
$m->addServer(0, 11211);

$val = str_repeat(file_get_contents('/etc/passwd'), 200);

$m->set('key', $val);
$m->setOption(Memcached::OPT_NOREPLY, true);
$m->setOption(Memcached::OPT_COMPRESSION, false);

$time = microtime(true);
$m->append('key', $val);
echo number_format(microtime(true) - $time, 4, ',', ''), "\n";

Кстати, такое большое (как в примере) значение мне потом обратно прочитать не удалось, видимо превысил какие-то лимиты, так что оно здесь просто для иллюстрации и чтобы разница в задержке была очевидней. Без ключа операция append у меня заняла 0,0407 секунд, с ним — 0,0013.

Интересно, что можно делать несколько операций append подряд, очерёдность их сохраняется, корректность прочитанного — тоже. Вот только непонятно не может ли чтение получить данные между двумя append, скорее всего — может, но этот способ как раз для случаев, когда на первом месте скорость записи к кеш.

2014   memcached   php   программирование

Memcached и UDP

Когда я написал про поддержку модулем Мемкешед для ПХП протокола UDP, я ещё не знал многих подробностей. Например, я думал, что ребятам из Мемкешед чем-то не понравился TCP/IP и они написали поверх UDP свой протокол гарантированной доставки. В приципе, так бывает, например так работает гугловский «КВИК».

Оказывается ничего подобного — UDP нужен только тогда, когда вас устраивает негарантированная доставка, т. е. вы швырнули пакет, а дойдёт ли он до кеша — неважно, при чтении это будет считаться промахом кеша.

В интернете есть статья, которая кратко раскрывает что это даёт, в ней же приводится патч «multi UDP», который Мемкешеду уже не нужен — я смотрел в код, всё там нормально давно.

Кстати, обнаружились две подробности в поддержке UDP модулем Мемкешед ПХП. Во-первых, указывать в конструкторе persistent_id нельзя — ничего работать не будет без каких-либо ошибок. Во-вторых, реализована только операция записи (при попытке сделать get получим ошибку ACTION NOT SUPPORTED), читать придётся через TCP/IP.

Работоспособный тест выглядит так:

$m = new Memcached('SomeID');
$m->addServer('127.0.0.1', 11211, 1);

$m_udp = new Memcached();
$m_udp->setOption(Memcached::OPT_USE_UDP, true);
$m_udp->addServer('127.0.0.1', 11211, 1);

var_dump($m_udp->set('foo', "test string"));
sleep (1);
var_dump($m->get('foo'));
2014   memcached   php   программирование

Недокументированные возможности модулья Memcached (для PHP)

Читая на досуге исходники модуля Memcached для ПХП, наткнулся на неожиданное: оказывается этот модуль поддерживает соединение по протоколу UDP, причём документация молчит об этом.

Делается это вот так:

$udp = new Memcached();
$udp->setOption(Memcached::OPT_USE_UDP, true);
$udp->addServer($host, $port);

В частности, это позволит нам перейти на multi-UDP схему, если добавить разные порты как несколько серверов. Надо пробовать.

Другая вещь, которая никак не раскрывается документацией — пользовательские флаги (UDF — user defined flag). К каждому значению можно прикрепить один или несколько флагов, которые при получении значения будут складываться в заданную переменную:

const FLAG = 1;
const ANOTHER_FLAG = 2;

$m->set($key, 'value', 0, FLAG | ANOTHER_FLAG); // установили флаг
$m->get($key, null, null, $flags); // флаги будут лежать в переменной $flags

У каждой функции установки и получения значений (а их несколько, этих функций) в конце есть необязательный параметр для работы с флагами. Так что во всех вариантах использования значения можно как-то пометить, а потом эти пометки получить.

Позднее дополнение: похоже memcached больше не нуждается в multiport UDP patch, он и без него теперь с UDP работает нормально.

2014   memcached   php   программирование

Как получить значения всех ключей в Memcached

В комментариях к одной из статей на «Хабре» привели однострочник для командной строки, чтобы вывести содержимое всех ключего локального memcached, я его прямо там причесал, но подумал, что он может мне и самому в будущем пригодиться. Переношу сюда:

nc localhost 11211 <<<"stats items" | awk -F: '/STAT items:[0-9]+/{if (!a[$2]++) print $2}' |
xargs -I{} echo 'stats cachedump {} 100' | nc localhost 11211 | awk '/ITEM/{print $2}' | xargs -n1 echo get |
nc localhost 11211

Без использования awk

nc localhost 11211 <<<"stats items" | sed -n '/STAT/{s/[^:]*:\([^:]*\).*/stats cachedump \1 100/;p}' | sort -u |
nc localhost 11211 | sed -n '/ITEM/{s/[^ ]* \([^ ]*\) .*/get \1/;p}' | nc localhost 11211

Без использования awk и sed

nc localhost 11211 <<<"stats items" | grep -Po '(?<=STAT items:)\d+' | sort -u |
xargs -I. echo stats cachedump . 100 | nc localhost 11211 | grep -oP '(?<=ITEM )\S*' |
xargs -n1 echo get | nc localhost 11211
2014   bash   memcached   программирование
2008   memcached   программирование

Разделяемая память

Удивительная вещь — до появления memcached никто, как будто бы, и не знал о существовании разделяемой памяти. Сейчас memcached почему-то называют лучшим решением. Это не так — у этого решения есть особенности, которые в некоторых проектах делают его не лучшим выбором. Memcached хорош двумя вещами — он позволяет держать данные в едином хранилище (например, несколько frontend делят общий кеш) и объединять несколько серверов (масштабируемость). Другие его особенности делают его выбор в некоторых проектах нерациональным (в других — напротив, лучшим решением), а именно: отсутствие блокировок (нельзя заблокировать какой-то ключ, прочитать, вычислить новое значение, разблокировать) и работа через TCP/IP.

Кроме того, у memcached есть особенность, которую тоже надо учитывать. Есть случаи, когда memcached может удалить данные из кеша, вот комментарий разработчика Анатолия Воробья:

Есть набор классов по величине памяти — например, в самом простом и дефолтном варианте, есть класс размером в 128 байт, потом 256 байт, потом 512, 1k, 2k итд. до 1Mb. Если какая-то запись (ключ+значение+ дополнительные заголовки) занимает, скажем, 1.5k, под нее отводится кусок из класса в 2k, т. е. кусок размером в 2k, и она в него пишется.

У каждого класса отдельно есть своя LRU queue всех записей в этом классе — каждый раз, когда кто-то просит прочесть какую-то запись, она перемещается в начало списка своего класса. Таким образом, в конце списка находятся те записи, которых давно никто не просил — их куски освобождаются, когда нужно место для новой записи в этом классе, и нельзя больше согласно конфигурации попросить памяти у системы.

Ссылки на другие способы доступа к разделяемой памяти есть в аннотации к модулю PEAR System::SharedMemory.

2006   memcached   php   prog