Pull to refresh

Comments 79

Советую не делать реализацию секции на PHP, лучше использовать самописное расширение, которое позволяло бы лочить на время входа запись и ставило бы все в очередь.

Все это без проблем реализуется на плюсах, работает быстрее и использует средства Unix, и сможете равномерней сбалансировать ожидание на демонах.
Так исторически сложилось, что проект реализуется на PHP. В данный момент идет планирование ворой версии с применением нормальных решений. Я бы рад на плюсах написать, но у нас штат PHP программистов, которых просто некуда будет девать.
Никто и не говорит о том, что нужно полностью переходить на плюсы. Это всего лишь расширение, позволяющее удобней работать в дальнейшем.
По-моему, тут критические секции не по адресу. Нужна сетевая (с точки зрения приложения), а не внутрипроцессная или внутриосевая блокировка, иначе можно было обойтись стандартными средствами межпроцессного взаимодействия.
Жаль, не могу плюсовать карму. Действительно, было бы на одной машине, использовали бы shared memory
Можно написать обертку для
SELECT GET_LOCK() и SELECT RELEASE_LOCK()
UFO just landed and posted this here
UFO just landed and posted this here
У мемкеш-сервера есть параметр, запрещающий выброс неустаревших ключей.
По ссылке теория, здесь же практика, по сути решаемые задачи одинаковы.
Если я правильно понял проблему, что вы хотите решить, то вам прямиком за модулем php memcached (а не memcache) с его compare and set операциями (http://us2.php.net/manual/en/memcached.cas.php)
изначально использовался memcached. К сожалению, я не участвовал в проекте в это время. Что то в нем не устроило, говорят, что нестабилен. В новой версии memcache будет использоваться только по назначению, костылей не будет.
Если я правильно понял, ваш алгоритм лишь снижает вероятность коллизий, однако полностью избавить от них не может. Пример:

Демон1: проверяем мьютекс, мьютекс открыт…
Демон2: проверяем мьютекс, мьютекс открыт, пишем мьютекс, перепроверяем мьютекс, начинаем писать данные…
Демон1:… пишем мьютекс, перепроверяем мьютекс, начинаем писать данные…
так первый демон после перепроверки не будет писать, если id не его. вернет ошибку или будет ждать.
Есть вероятность, что два демона одновременно прочтут пустую ячейку и оба запишут в неё свой pid и номер сервера. По сути это вероятность такая же как вероятность одновременной записи одного значения без всякой блокировки. Ваш алгоритм имеет смысл использовать если приложения пишут не одну пару за раз, а некое подобие транзакций. Тогда он действительно уменьшит вероятность коллизий.
От этой неприятности автор как раз защитился добавив дополнительную проверку записанного мьютекса. Другое дело, что это всё равно не может полностью исключить коллизии.
Ну это детектирование коллизии, а не её исключение, по-моему. Тем более теоретически даже не 100% детектирование. Вполне возможна ситуация:
Процесс 1 проверил на пустоту.
Процесс 2 проверил на пустоту.
Процесс 1 записал мьютекс.
Процесс 1 проверил мьютекс.
Процесс 2 записал мьютекс.
Процесс 2 проверил мьютекс.
Процессы 1 и 2 пишут в разделяемую область каждый своё, уверенный, что никто не помешает. Хорошо если одно и то же пишут, как часто бывает при инвалидации кэша по таймауту. А если нет…
Не вижу разницы между детектированием и исключением, если честно. А ваш пример один в один повторяет мой :-)
Детектирование — сообщить клиенту, что ресурс заблокирован другим клиентом. Исключение — сообщить клиенту, что ресурс для него заблокирован в любой ситуации (если клиент не дождался очереди — его проблемы).

Не полностью, у меня более параллельный :) Хотя по сути, да, то же самое.
Внимательнее, первый демон пишет свой мьютекс перетирая мьютекс второго (о котором он не знает из-за задержки), соответственно перепроверка пройдёт успешно и данные будут писать одновременно оба процесса.
Осталось вспомнить стандартный способ юникса создания лок-файлов…
Не поможет, тут скорость нужна.
Вспоминаем про файловые системы в памяти…
Не подойдет, memcached — отдельная машина
отдельный демон принимающий запросы от удаленных серверов и работающий с локальным мемкешедом?
тогда мой вариант вполне подходит, только демоны придется немного переписать
Ещё вариант — отдельный демон блокировок, работающий хоть на сервере с мемкэшем, хоть на одном из серверов с приложением, хоть на отдельном сервере в другом полушарии :)

Клиенты (процессы приложения) запрашивают блокировку нужного ключа мемкэша, а демон блокировки или говорит, что блокировка дана, или ставит запрос в очередь (или сообщает что блокировка не может быть дана, предоставляя приложению самому думать то ли вылетить, то ли повторить запрос). Нагрузка на демон будет, наверное, минимальна, потому подойдут простейшие реализации межпотокового взаимодействия с синхронным обменом данными с клиентами, о масштубируемости которых на несколько серверов можно очень долго не думать.
«масштубируемости» — почти по Фрейду :-D
Это уже прокси какое то…
Прокси предлагает iwfyb выше
Реализация демона конечно может быть разной, как и требования к скорости работы разрабатываемого приложения, но в первом приближении это не очень удачная идея. Демон блокировок это дополнительная цепочка которая может сломаться, может не успевать обрабатывать поступающие запросы. А еще неизбежно возникает оверхед на связь демона и приложения (вне зависимости от транспортного уровня) что в итоге на корню убивает идею кэша как быстрого хранилища данных.
спасибо, действительно возможно, но вероятность куда меньше, на практике замечено пока не было
Посмотрел на PHP Memcache API — похоже, единственное, на чём в нём можно построить надёжные блокировки, это increment/decrement. Попробуйте, например, что-то вроде этого:

while(true) {
  if (get("lockKey") == 0) {
    int counter = increment("lockKey");
    try {
      if (counter == 1) {
        // I'm the winner
        set("veryImportantKey", someValue);
        break;
      }
    }
    finally {
      decrement("lockKey");
    }
  }
  sleep(random());
}
для блокировки достаточно атомарной операции добавления ключа (которая фейлится, если такой ключ уже существует):
while (true) {
   if ($mc->add('lock', getmypid())) {
        try {
              // ... work work work
        } catch (Exception $e) { // no 'finally' in PHP =(
              $mc->delete('lock');
              throw $e;
        }
        $mc->delete('lock');
        break;
   }
   usleep(rand(10, 1000));
}


Ненамного, но все же проще.
Я чёта не понял.
Почему в кэш хронически пытаются писать две ноды сразу?
И почему это порождает проблему? Ну перезапишет вторая нода ещё раз кэш, потраченное ею на вычисления время ведь всё-равно уже не вернуть. Процессор мемкэш тоже не особо выедает, на его стороне экономия тоже ничего не даёт.
Потому что я использую кэш как базу данных
Memcached надо использовать как временное хранилище данных. Для постоянного хранилища данных лучше использовать SQL/NoSQL базу данных.
Я и использую как временное хранилище.
Точнее уже не использую. Эволюционировал
Что значит «уже не использую» и «эволюционировал»?

Временное хранилище это то место где Вы можете хранить данные не боясь их потерять. При этом временное хранилище должно предоставлять более быстрый доступ к данным нежели постоянное хранилище (иначе не было бы смысла его использовать). В случае потери данных во временном хранилище они должны быть получены из постоянного хранилища данных и положено во временное.

Положим что временное хранилище это кеш, а постоянное хранилище это база данных, тогда алгоритм работы прост:
как только нам понадобились данные, мы сначала ищем их в кеше, если они там есть, используем их, если нет то ищем их в базе данных, если искомые данные есть в базе данных, читаем их, сохраняем в кеш и продолжаем работу

Таким образом в первый раз мы будем искать данные и в кеше и в базе, но зато в последующие разы мы будем получать данные только из кеша.
Встаёт проблема инвалидации кэша.
— Процесс1 пишет в БД
— Процесс2 пишет в БД
— Процесс2 пишет в кэш
— Процесс1 пишет в кэш, затирая более новое содержание, записанное Процессом2

Всё равно какой-то механизм блокировки ресурсов должен быть (необязательно с этими ресурсами «физически» связанный). Если ни кэш, ни БД блокировку не реализуют, то нужно или использовать сторонний, или реализовывать свой, или смириться с коллизиями и нарушениями целостности.

Ну и плюс есть сервисы, для которых все данные (или их часть) временные по определению и в постоянном хранении не нуждаются.
Я попытался описать абстрактный вариант, если мы говорим о memcached, то там валидация данных реализована с помощью cas token для варианта когда мы сначала читаем данные, изменяем их и потом пытаемся их записать. Для Вашего варианта можно использовать метод memcached::add который не позволит перезаписать уже существующие данные. В общем говоря memcached (не путать с memcache) предоставляет достаточно инструментов что бы разрулить конфликты, надо просто мануал почитывать, перед тем как писать костыли.
а потом переписывать практически процедурный код из десятков тысяч строк…
Когда потом? Надо все с самого начала правильно спроектировать и не будет никаких потом.
Полностью согласен, потому и проектируем заново.
Автор в комментах указал, что memcached им по каким-то причинам не подошёл.

Для моего варианта memcached::add не подойдёт, т. к. происходит именно замена существующего значения (а может создание нового, если не существует). cas тоже, по-моему, не решит проблемы, если не вводить в данные какого-то глобального счётчика, позволяющего процессу определить не устарели ли данные, которые оно пытается записать в кэш. Memcached::increment в качестве такого счётчика тоже может сфэйлить, даже если не учитывать, что значение может быть вытеснено. Нужно какое-то решение, гарантирующее, что между записью в БД и записью в кэш не будет записи в кэш другим процессом. Если БД или кэш не осуществляет блокировку (именно блокировку, атомарные операции не спасут), то не вижу выхода, кроме использования четвёртого демона (кроме приложения, кэша и хранилища) чисто для блокировки. Это может быть специальный «костыль», учитывающий все требования приложения, например организацию очереди блокировок и принудительное снятие блокировки для слишком задумавшегося клиента с его убийством, а может быть, например, банальное создание .lock файла на удаленной ФС.
Инвалидацию кэша можно вести через теги кэша. Для этого можно при проектировании приложения работающего в указанных процессах ввести требование, что пишуший-запрос-в-бд получает, к примеру, значение первичного ключа который автоикрементный монотонно возрастающий. Это нам гарантирует используемая СУБД. Дальше при записи в кэш в качестве значения тега используется значение этого ключа. Гарантию атомарности (в том числе и при работе с тегом) нам должен гарантировать код кэширующего приложения при этом при попытке захвата вне зависимости от результата оно должно в том числе возвращать и версию тега. Сравнивая эту версию с той, что у нас пытается записать процесс сам процесс уже может принять решение о том, нужно ли изменять кэш или нет. При описанном мною алгоритме Процесс1 получив текущую версию тега (которою до него записал Процесс2) увидит, что у него более старая версия данных и просто не будет писать в кэш, затирая кэша не будет.

Это один из вариантов того, что принимать величину версии. В зависимости от задачи это не всегда должен и может быть монотонно возрастающий PK.

Вообще Смирнов все это в своем докладе очень хорошо и доходчиво изложил, ссылку я уже приводил, рекомендую прочесть.
Троллейбус из буханки хлеба.jpg
Например, кэш «инкрементный». Ноды не перевычисляют его полностью, а лишь добавляют свои данные. Удобно для, например, чатов, где полная (включая аппаратные отказы) персистентность данных не важна, но и в нормальном режиме терять их тоже не хотелось бы.
например в мемкэш есть команда инкремент.
я поискал как вы говорите и там много непонятных слов, и статьи очень сложные. думаю, что лучше я просто буду записывать данные дважды, чтобы точно быть уверенным, что они записались. как думаете, это решит все мои проблемы? будет ли такое решение масштабироваться до cloud-scale?
>Выяснилось, что в 80% случаев приложения пытаются одновременно записать
> свои данные в одну ячейку. Идеальным решением было бы использование shared memory

Не имею целью обидеть автора, но ради справедливости замечу, что указанный кусок наглядно показывает, что предметной областью он владеет плохо. Надеюсь он адекватно это воспримет и сделает правильные выводы.

Теперь по существу вопроса. Использование SH абсолютно не снимает проблемы коллизий. Просто из-за высокой скорости работы с SH приложение работает с этим видом памяти очень короткое время и до определенного момента конкурирующие запросы не пересекаются. Но коллизии могут и обязательно возникнут особенно по мере роста нагрузки. Поэтому если приложение работает с разделяемыми данными (не важно где и в каком виде они хранатся) нужно обязательно предусматривать механизмы синхронизации. Иначе придется столкнуться с плавающими багами в приложении когда ни какой проблемы при тестах не возникает, на продакшене все работает, но в какие то моменты времени на продакшене возникают ошибки и воспроизвести их потом ни как не получается и бага не отлавливается.

Создать распределенный кэш на база SH возможно. Говорю как человек который как раз пишет такой кэшер (с поддержкой времени жизни каждого кэша, с возможностью сброса группы кэшей, с тегами). Потому как распределенный кэш или нет зависит от самого приложения, если оно спроектировано и написано правильно, но уже не столь важно что у него работает бэкэндом, SH, memched или какой-то другой сторедж.

P.S. Кроме set есть вообще еще и add, рекомендую погуглить по этой теме, а так же почитать это: "Кэширование и memcached"
Да, впервые работаю с такого рода приложениями. Спасибо за ответ, ссылку в закладки.
Использование SH абсолютно не снимает проблемы коллизий.
Создать распределенный кэш на база SH возможно.

:-/

А вообще, имхо, проблему сетевых коллизий нельзя решить только на клиент-сайде (в данном случае на стороне серверов приложений) — сервер должен поддерживать блокировку или, как минимум, атомарную операцию «записать, если не пусто».
Можно. Если я при разработке приложения знаю, что приложение будет работать со сторонним ПО в котором нет гарантии атомарности и у моем приложении могут возникнуть поэтому коллизии при работе с данными, то могу в архитектуру разрабатываемого приложения заложить механизмы исключающие коллизии. При этом эти механизмы могут быть написаны как в рамках этого приложения, так и просто использовать какое-то другое стороннее ПО (к примеру, семафоры) которое позволит моему приложению выполнять синхронизацию.

Если же стороннее ПО уже включает в себя атомарные операции, то мое приложение просто использует их. Но в любом случае моё приложение знает о возможных коллизиях и должно самостоятельно их решать (сервер при вызове атомарной операции может лишь сообщить удалось действие или нет, решить подождать ли еще и повторить запрос, а может просто упасть или ругнуться в лог и продолжить работу решает разрабатываемое приложение просто потому что оно знает о характере обрабатываемых данных).
Приведите пример, пожалуйста, как только на клиент-сайде (фронт-енде) можно решить проблему коллизий, не превращая клиентов в сервера блокировок друг для друга. Вот пытается клиент захватить ресурс, как другие клиенты узнают об этом?
Через атомарные операции. Причем атомарные неблокируемые операции. В операционной системе достаточно механизмов для этого. К примеру через создание файла, семафоры, мьютексы, однопточные приложения вариантом много. Клиент используя выбранный механизм пытается получить монопольный доступ к ресурсу и в свете атомарности и неблокируемости доступ получит только один из запросов, остальные завершаться с ошибкой. Исходя из этого клиент сам решает, попытаться ли сделать еще один запрос, как долго делать эти запросы, возможно просто завершит запрос с ошибкой. Взаимных блокировок тут по не получается. Клиент либо получил право на работу с ресурсом и воспользовался им, либо сделал несколько попыток и отвалился по таймауту (если конечно это в нем реализованно).
Не понял. Вот конкретная схема приложения: АппСервер1, АппСервер2, КэшСервер. К каким механизмам ОС, а главное к какой ОС должен обратиться АппСервер1, чтобы АппСервер2 не получил одновременный с ним доступ на КэшСервере, если КэшСервер даже атомарного инкремента не предоставляет.
Атомарность операции должен гарантировать КэшСервер. Именно на нем нужно обращаться к соответсвующим механизмам ОСи. АппСервер1 и АппСервер2 выступают всего лишь как клиенты и либо получает доступ к элементу в кэше, либо нет. Обработка ответа возлагается на клиент.

Кроме того в зависимости от общих требований к системе при построении архитуктруры в кэшер могут быть вынесены и другие задачи, не только атомарность. К примеру контроль версионности тегов при записи может быть так же внесен в кэшер. Потому что даже при атомарности операции из-за сетевого оверхеда коннекта с АппСерверами возможно затирание актуальных данных на более старые. Если кэшер контролирует версии тегов и содержит бизнес-логику которая знает об используемой схеме нумерации, то он может не произвести запись в кэш даже в случае если АппСервер производящий запись имеет монопольные права в текущий момент. Кэшер просто видит, что данные присланные на запись более старые, чем имеющиеся в кэше и возращает на АппСервер сообщение об ошибке.
Так мы об одном и том же, блокировку/атомарность должен обеспечивать либо кэшер, либо отдельный демон. Клиенты без посредника между собой не договорятся, если хотя бы один из них сам не станет сервером.
Безусловно, где-то атомарность и блокировки должны существовать. Но я веду к тому, что отдельный демон для этого не нужен, он снизит общую надежность системы. Поэтому эту логику нужно сосредотачивать в кэшере если наши клиенты это внешние приложения.
А я про то, что если кэшер её не обеспечивает (а он по определению не обеспечивает, он же кэшер, кто его посадит а не хранилище), то нужен отдельный демон, либо блокировку обеспечивать ещё каким-нибудь используемым сервисом типа мускула. Причём мускул должен обеспечивать и блокировку кэша.
Ну это уже вопрос к разработчику архитектуры, должно ли это обеспечиваться кэшером или нет. У меня к примеру условия такие, что существующие решения в полной мере не содержат нужный функционал, поэтому пишется свой кэшер с тегами, версиями, атомарностью, блокировками.
Ох уж эти студенты, как понаписуют костылей, и давай их постить на хабре… Лучше бы матчасть курили про memcached и cas.
Откуда Вам известны столь интимные подробности? Не стоит основываться на собственных догадках.
Какие такие интимные подробности? Я основываюсь на Вашем возрасте и материалу изложеному в статье, со временем Вы и сами научитесь это замечать.
Уже умею замечать, но не по возрасту в профиле, а стилю изложения.
Давно не студент. В проект я включился на стадии дедлайна, потому без костылей не обошлось. С каждым может случиться.
Всем спасибо за критику и замечания. Действительно некоторые моменты не видел.
Могут помочь методы incr и decr для реализации мутекса на стороне memcached.

Но на самом деле вам нужен отдельный setter-process, который один пишет в memcached, а в него операции передавать через очередь любую (rabbitmq, 0zq, к примеру).
Простой способ реализации блокировки.
Memcached::add — добавляет значение по ключу, если его нет. Возвращает истину если получилось, или ложь если не получилось (такой ключ уже есть).

Допустим нам надо заблокировать ячейку

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

function get_block($key) {
global $m;
$block_key = 'block_'.$key;
return $m->add($block_key, 1, 10); // надо указывать expiration так чтобы если пхп скрипт умрет, блокировка в конце концов снялась
}

function release_block($key) {
global $m;
$block_key = 'block_'.$key;
$m->delete($block_key);
}

Теперь каждый процесс пытается получить блокировку с помощью get_block($key) и в случае успеха пишет и освобождает ячейку. Остальные процессы ждут.

На инкременте тоже можно сделать, см. выше.
Не исключает ситуации, когда два процесса считают, что они имеют монопольное право записи.
По идее операция атомарная и два пользователя не смогут записать один ключ. Так что исключает. А как в реальности — надо смотреть.

Memcached::add() is similar to Memcached::set(), but the operation fails if the key already exists on the server.
Мемкэш может выкинуть записанное значение без всякого предупреждения.
Если ему это не запретить, да.
когда памяти с запасом, этого не происходит. Но не стоит на это полагаться
Sign up to leave a comment.

Articles