Search
Write a publication
Pull to refresh

Comments 77

А чем обусловлен выбор именно такого метода.
Чем плох мемкэш? Он рассматривался как вариант?
Да, рассматривался, мемкеш — отличный выбор, если понимать его плюсы и минусы по сравнению с данными способами: если мемкеш на отдельном сервере, а все скрипты крутятся на другом, то это лишние сетевые издержки.

Здесь я просто хотел показать альтернативные мемкешу варианты, т.к. на хабре не нашёл упоминаний таких решений.
А если нужный сервис крутится на нескольких машинах?
Ведь для отказоустойчивости нужно минимум на двух-трёх машинах запускаться.
Хотя конечно зависит от того, какие задачи решаются, и нужна ли глобальная блокировка.
Можно намекнуть, для чего примерно используется это решение?
Конкретно это решение изменено и завуалировано, чтобы никто не заподозрил меня в плохих делах =)
Если на нескольких машинах, то либо использовать тяжеловесный ZooKeeper (недавно была статья про него тут), либо же — синхронизироваться через мастер-БД (в постгресе есть pg_try_advisory_lock, который подойдет для большинства случаев). Можно и через мемкэшевый cas, да, но только мемкэш может прилечь или очиститься внезапно (мало ли, какие бывают форс-мажоры), а если приляжет мастер-база, то уж будет точно не до блокировок. :)
Как известно, APC сейчас стоит почти на всех продакшн-машинах с пхп (будь шаред хостинг, или свой сервер) просто для кеширования опкода, так что нет смысла ставить мемкешд (тем более что вы проиграете в производительности) до тех пор, пока кеш-сервера не надо шардить. Да и там встанет вопрос — почему бы не редись, который по мне так на голову лучше
Есть мнение что user vars кеш в APC имеет проблемы с фрагментацией.Сам сталкивался всего пару раз, но осадочек остался…
Мемкэш быстрей примерно в 146% раз. Поэтому блокировки на нём самое то. А редис — это всё таки БД, а не кэш.
Сетевой демон быстрее shared memory или семафоров? Ого! Расскажите поподробнее, очень интересно.
Да это мне чё-то в голову пришло, что разговор про блокировку на файлах. Не знаю почему.
Как мне кажется, сложность с мемкешом в том, что если у нас есть несколько бекендов, то:
— придется вводить префиксы для серверов (каждому бекенду придется иметь отдельный конфиг)
— появятся затраты на сетевое взаимодействие
я имел в виду одну блокировку на все машины, см. комментарий выше
Всё так, но вот APC и семафорами при нескольких бэкендах пользоваться точно не стоит =)
А чем он тут хорош? Расточительно по памяти — мемкешд использует корзины, да и при нехватке памяти корзины буду молча вываливаться из кеша.

В данном случае, я бы просто проверил на is_writeable, подавил бы ошибку и не заморачивался.
на мемкеше ситуация когда куча потоков пытаются одновременно просчитать и сохранить тяжелые данные в хайлоаде тоже более чем актуальна.
Обходится при помощи php_memcached и cas, но читаемость и предсказуемость кода резко падает.
ПС: Мне кажется, на хабре пора вводить разделение статей на уровень подготовки читателя)
Не у Вас одного в голове такие мысли появляются.
Причем уровни:
-Чайник
-Джуниор
-Интермедиэйт
-Сениор

как минимум, ибо статьи 1-3 категори вообще не интересно читать :)
Возможно стоит у статьи добавить помимо голосов ещё и рейтинг сложности материала, плюсовать/минусовать который могут люди с повышенной кармой или все.
Согласен. Для комфортного чтения статьи нужны знания в следующих обсластях.
Пока можно просто ставить теги, чтоб не заходить в статьи, которые не интересны, но это на совести авторов.
Не все удосуживаются почитать документацию и поэтому не знают об этой прекрасной функции.
Кстати возможности блокировки можно использовать в file_put_contents указав флаг.
Например:

file_put_contents($filename, $data, FILE_APPEND | LOCK_EX);
Жаль, что у file_get_contents такого флага нет.
Перед использованием flock() необходимо получить дескриптор файла, например с помощью fopen() — это обращение к диску. Зачем? У абстрактного race condition =) (пример из статьи не очень подходит) в критической части операций с файлами может не быть вообще, и добавлять сюда файловую систему (да, операционка много что закеширует, но тем не менее) — лишняя трата ресурсов.
А, то есть file_exists и метод validate() автора статью к диску не обращаются?

Неувязочка :)
Да, я выше написал, что конкретно этот пример здесь не очень подходит, т.к. обращения всё-равно остаются. Буду выбирать примеры понагляднее, обещаю! =)
Был бы у Вас наглядный пример — я бы и не начинал дискуссию.
В любом случае использование flock — признак хорошего тона.
Когда-то в мануале было написано, что FILE_APPEND и LOCK_EX взаимоисключают друг-друга, т.к FILE_APPEND сама по себе атомарная операция. Теперь этот текст почему то убрали: ru2.php.net/manual/en/function.file-put-contents.php
Быть может потому, что они теперь работают вместе, не?

(:
И что тогда это было? Косяк доки, или изменение принципа работы «по-тихому»?
Я никогда не видел, чтобы их нельзя было юзать вместе, поэтому не могу ответить.
Тоже тестил движок, на 1000 одновременных запросов (с помощью ab) и c кэшем начались твориться чудеса, пришлось выкручиваться.
Кусочек кода из метода записи данных в кеш:

$cache_size_file	= fopen(CACHE.'/size', 'c+b');
$time					= microtime(true);
while (!flock($cache_size_file, LOCK_EX)) {
	if ($time < microtime() - .5) {
		fclose($cache_size_file);
		return false;
	}
	time_nanosleep(0, 1000000);
}
unset($time);
/**
	Some work here
*/
flock($cache_size_file, LOCK_UN);
fclose($cache_size_file);

return false потому, как кеш — необязательный, и чтобы долго не ждать — можно и пропустить запись.
Работает стабильно, с помощью штатных функций, не нужно дополнительных классов.
Если файл уже блокирован другим процессом, flock() вернёт false
Нет. Он будет висеть и ждать. false будет только если Вы сделаете flock($f, LOCK_EX | LOCK_NB).
Вы правы, проверил, добавил себе LOCK_NB
Спасибо, знал только про sleep() и time_nanosleep()
Есть два потока 1 и 2.

function canUseApc() {
    return extension_loaded('apc') && ini_get('apc.enabled') && php_sapi_name() !== 'cli';
}

function getFlagFromFile($filename) {
    if (file_exists($filename)) {
        if (!$this->validate()) {
            if ($this->canUseApc() && apc_add('some_key', 1)) {    //***
                unlink($filename);
                apc_delete('some_key');    //***
            }
            return false;
        }
        else {
            return file_get_contents($filename);
        }
    }
    return false;
}


1: file_exists($filename) — true
2: file_exists($filename) — true
1: !$this->validate() -true
2: !$this->validate() -true
1: $this->canUseApc() -true
2: $this->canUseApc() -true
2: apc_add('some_key', 1)
2: unlink($filename);
2: apc_delete('some_key');
1: apc_add('some_key', 1)
1: unlink($filename); — NO SUCH FILE!

function getFlagFromFile($filename) {
    if (file_exists($filename)) {
        if (!$this->validate()) {
            $sem = sem_get(1);    //***
            if (sem_acquire($sem) && file_exists($filename)) {    //***
                unlink($filename);
                sem_remove($sem);    //***
            }
            return false;
        }
        else {
            return file_get_contents($filename);
        }
    }
    return false;
}


1: file_exists($filename) — true
2: file_exists($filename) — true
1: !$this->validate() — true
2: !$this->validate() — true
1: $sem = sem_get(1);
1: sem_acquire($sem) && file_exists($filename) — true
1: unlink($filename);
1: sem_remove($sem);
2: $sem = sem_get(1);
2: sem_acquire($sem)
2: file_exists($filename) — false!
2: return false;
3: зависнет на веки т.к. семафор так и не был сброшен
php.net/manual/ru/function.sem-get.php
resource sem_get ( int $key [, int $max_acquire = 1 [, int $perm = 0666 [, int $auto_release = 1 ]]] )
sem_get() returns an id that can be used to access the System V semaphore with the given key.

A second call to sem_get() for the same key will return a different semaphore identifier, but both identifiers access the same underlying semaphore.


function getFlagFromFile($filename) {
    if (file_exists($filename)) {
        if (!$this->validate()) {
            if ($race = RaceCondition::prevent('FLAG_'.$filename)) {    //***
                unlink($filename);
                $race->release();    //***
            }
            return false;
        }
        else {
            return file_get_contents($filename);
        }
    }
    return false;
}


OK :)

Не знаю насколько вероятно что подобные сценарии когда-нибудь возникнут на реально работающей системе — но, насколько я знаю в случае с многопоточным программированием если что-то может произойти — оно обязательно случиться на продакшне :)
Кстати говоря, если в случае устаревания файла мы его всёравно удаляем, то разве @unlink($filename) не лучшее решение?) в том и другом случае возвращаем false. Нам впринципе не важно был ли файл удален нами или кто-то до нас уже заметил что он устарел и удалил его…
Лучшее, но, подозреваю, смысл статьи показать как поступать в случаях, когда атомарности не добиться.
Ну тогда случай можно было выбрать поудачнее. Как минимум тот, в котором был смысл решать «проблему» с race condition.
Нет, только не собака, пожалуйста, не надо!!!
@unlink($filename) вернёт false например ещё тогда, когда файл не удалён, а просто прав на удаление не хватило ну и ещё кучу других забавных ситуаций можно придумать с собакой.
Первый пример странен в этом месте:
1: $this->canUseApc() -true
2: $this->canUseApc() -true
2: apc_add('some_key', 1)
2: unlink($filename);
2: apc_delete('some_key');
1: apc_add('some_key', 1)

скорее вот так:
1: $this->canUseApc() -true
2: $this->canUseApc() -true
2: apc_add('some_key', 1) -true
1: apc_add('some_key', 1) -false
2: unlink($filename);
2: apc_delete('some_key');
1: return;

а второй — замечание верное, sem_remove($sem); надо вынести из условия
А что мешает второму потоку получить лок, удалить файл, освободить лок до того как первый поток получит шанс взять лок? :) Я согласен что это маловероятно — более того, если бы мы обсуждали код на С то в некоторых случаях (архитектура, количество ядер… ) это было бы даже невозможно — но мы говорим о php :) Какие гарантии дает php о последовательности выполнений инструкций в нескольких потоках? Тут можно сказать что это невероятно, но в таком случае тем труднее будет потом отловить возникшую багу, если она таки случиться, например, 2 раза… за два месяца :) и оба раза под носом у заказчика…
> Какие гарантии дает php о последовательности выполнений инструкций в нескольких потоках?

Никаких. В пхп потоков нет. Скрипты работают абсолютно независимо друг от друга.
Тело Вашего цикла никогда не будет выполнено, разве нет?

while (!flock($cache_size_file, LOCK_EX)) {
	if ($time < microtime() - .5) {
		fclose($cache_size_file);
		return false;
	}
	time_nanosleep(0, 1000000);
}
Почему бы не отдать чистку кеша третьей стороне? Допустим пусть крон стартует регулярно и чистит устарелые данные. Сами же скрипты будут заниматься своим делом и брать всегда данные из кеша, если он есть и не заниматься проверками и удалением.
Если речь идет именно об удалении.
Не важно очередь или метка времени, факт в том чтоб основные потоки не удаляли ничего, чтоб этим залималось третье лицо. А очередь это будет или нет не важно, крон тоже с очередью может работать.
Я бы вообще вопрос ребром поставил: зачем чистить кеш? Не лучше ли использовать кеш с необходимым автовытеснением.
Ну кэш разный бывает. Бывает, что нужно пересоздать его из актуальных данных. В некоторых случаях для этого можно применить очистку и последующее создание его заново.
А в таких ситуациях необходимо иметь номер ревизии. При изменении факторов эта ревизия должна автоматически увеличиваться.
Может, я чего-то не понимаю, но почему не добавить проверку на существование файла сразу перед удалением? А для надежности, можна после удаления еще файловый кеш почистить, как тут в первом примере
Между проверкой и удалением может и вклинится при хорошей нагрузке другая операция удаления.
UFO landed and left these words here
UFO landed and left these words here
Отлично работают, пока, как в примере автора, только один сервер используется.
А зачем вам кешировать данные в файл? Может лучше сразу в APC?
Боже, пошли заказчика, который после таких писателей в отчаении обратится и заплатит и так не дешовый рейт * 3, лишь бы исправить проблемы и вернуть проект к жизни.

Я даже не хочу пояснять в чем тут проблема, ибо ответы очевидны — дам лишь намёк: у вас поток в 2-3 тысячи запросов в секунду и тут кеш протухает…
UFO landed and left these words here
Вы можете меня бить палками, но на более-менее нагруженных проектах кешем должен заниматься отдельный скрипт по крону и только. Или подобные варианты как отдельно висящий кеш демон.
Уточняю: валидацией кеша занимается только демон. Если кеш-страница отсутствует, то лочится кеш-страница до момента окончания её генерации (все скрипты ожидают её генерации или таймаут). А вот генерацией может заниматься первый поток, который наткнулся на отсутсвие.
Хотя, вариант валидировать в скрипте после отдачи контента вполне годный. Но тоже не удаление, а лишь установка флага невалидности.
Да, @ зло, но не проще ли было использовать @unlink(); в данном конкретном случае?

Объективно — зачем городить здесь решение «проблемы» с race condition в данном случае?
UFO landed and left these words here
а почему просто не обернуть удаление файла try catch? И в catch записывать, например в лог-файл или БД ошибку и потом кроном проверять.

Ведь если ошибка возникает из-за того, что файл был удален в «соседнем потоке», то такая ошибка подавится.
А если ошибка была вызвана из-за другой причины, то такая ошибка так же вылетит и в коде из топика.

А еще файл можно помечать как «удаленный», эту информацию хранить в БД и кроном потом чистить.
В качестве первого аргумента sem_get можно передавать любое число? Это не вызовет коллизий с другими процессами в системе?
Sign up to leave a comment.

Articles