💢 Проблема с переходом на 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 в продакшне.
2 комментария
19 июля 2016 12:01

💢 Проблема с переходом на 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 нет реализации последнего параметра — флага, вместо этого всё работает так, как будто он установлен, это ничему не мешает.
8 комментариев
14 июля 2016 11:00

Недокументированный 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, скорее всего — может, но этот способ как раз для случаев, когда на первом месте скорость записи к кеш.
5 комментариев
17 сентября 2014 07:18

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'));
Комментировать
7 сентября 2014 15:15

Недокументированные возможности модулья 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 работает нормально.
4 комментария
27 августа 2014 15:13

Как получить значения всех ключей в 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
5 комментариев
12 февраля 2014 09:53

Memcached

Люди, не используйте memcached, если можно этого не делать! Статьи на «Хабре» говорят о том, что:

a) люди не понимают когда нужно использовать memcached, а когда нельзя
б) люди не знают о других способах использования разделяемой памяти, memcached самый «раскрученный»
16 комментариев
28 августа 2008 15:33

[Без заголовка]

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

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

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

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


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