Статьи

Заметки о Memcached

Несколько заметок о Memcached. Вот его архитектура. Как это устроено ?


Memcached организован как ферма из N серверов. Модель хранения можно рассматривать как огромный HashTable, разделенный между этими N серверами.

Каждый запрос API принимает параметр «ключ». В клиентской библиотеке есть двухэтапный процесс …

  • Учитывая ключ, найдите сервер
  • Переслать запрос на этот сервер

Сервер, получающий запрос, выполнит локальный поиск этого ключа. Серверы внутри фермы вообще не сплетничают друг с другом. Каждый сервер использует асинхронный, неблокирующий ввод-вывод, и один поток может использоваться для обработки большого количества входящих TCP-сокетов. На самом деле пул потоков используется, но количество потоков не зависит от количества входящих сокетов. Эта архитектура отлично масштабируется для большого количества входящих сетевых подключений.

API

Memcached предоставляет HashTable-подобный интерфейс, поэтому он имеет …

  • получить (ключ)
  • установить (ключ, значение)

Memcached также предоставляет более богатое «множественное получение», так что один запрос на чтение может получить значения для нескольких ключей. Клиентская библиотека будет выдавать разные запросы нескольким серверам и выполнять поиск параллельно.

  • get_multi ([«k1», «k2», «k3»])

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

  • get_multi ([«user1: k1», «user1: k2», «user1: k3»]) — этот запрос просто отправляется на сервер, на котором размещены все ключи «user1: *»

Для обновления данных Memcached предоставляет несколько вариантов.

  • set (ключ, значение, срок действия) — Memcached гарантирует, что элемент никогда не будет оставаться в кэше после истечения времени истечения. (Обратите внимание, что возможно, что элемент был удален до истечения срока действия из-за переполнения кэша)
  • add (ключ, значение, срок действия) — Успешно, только когда не существует записи ключа.
  • replace (ключ, значение, срок действия) — Успешно, только если запись ключа уже существует.

 

Сбои сервера

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

Обратите внимание, что алгоритм хэширования сервера по умолчанию не очень хорошо справляется с увеличением и уменьшением количества серверов. Когда количество серверов меняется, уравнение владения (ключ мод N) все будет неправильным. Другими словами, если сбойный сервер необходимо удалить из пула, общее количество серверов будет уменьшено на один, а все существующие записи необходимо перераспределить на другой сервер. По сути, весь кеш (среди всех серверов) становится недействительным, даже если происходит сбой только одного сервера.

Поэтому одним из подходов к решению этой проблемы является сохранение количества серверов Memcached при сбоях системы. У нас может быть сервер мониторинга для определения пульса всех серверов Memcached, и в случае обнаружения любых сбоев запустите новый сервер с тем же IP-адресом, что и у мертвого сервера. В этом случае, хотя новый сервер все равно потеряет все записи и должен будет снова заполнить кеш, владение ключами не изменится, и данные внутри выживающего узла перераспределять не нужно.

Другой подход заключается в запуске логических серверов в ферме физических машин. При сбое физической машины ее логические серверы будут перезапущены на оставшихся в живых физических машинах. Другими словами, количество логических серверов не изменяется даже в случае сбоя. Этот логический серверный подход также хорош, когда базовые физические машины имеют различную емкость памяти. Мы можем запустить больше процессов Memcached в машине с большим объемом памяти и пропорционально распределить кэш в соответствии с объемом памяти.

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




 

 

 

 

 

 

 

 

валентность

Каждый запрос к Memcached сам по себе атомарен. Но прямой поддержки атомарности в нескольких запросах нет. Тем не менее, приложение может реализовать свой собственный механизм блокировки с помощью операции «add ()», предоставляемой Memcached следующим образом …

success = add("lock", null, 5.seconds)
if success
set("key1", value1)
set("key2", value2)
cache.delete("lock")
else
raise UpdateException.new("fail to get lock")
end

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

data, stamp = get_stamp(key)
...
check_and_set(key, value1, stamp)

Что не делает Memcached?

Целью разработки Memcached является производительность и масштабируемость. По своему дизайну это не касается следующих проблем.

  • Аутентификация по клиентскому запросу
  • Репликация данных между серверами для обеспечения отказоустойчивости
  • Ключ> 250 символов
  • Большой объект> 1 МБ
  • Хранение объектов коллекции

Самостоятельная репликация данных

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

Основная цель использования кеша — по причине «производительности». Если ваша система не может терпеть потери данных на уровне кэша, переосмыслите свой дизайн!

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

На стороне клиента мы можем использовать несколько ключей для представления разных копий одних и тех же данных. Монотонно увеличивающийся номер версии также прикрепляется к данным. Этот номер версии используется для идентификации самой последней копии и будет увеличиваться при каждом обновлении.

При выполнении обновления мы обновляем все копии одних и тех же данных с помощью разных ключей.

def reliable_set(key, versioned_value)
key_array = [key+':1', key+':2', key+':3']
new_value = versioned_value.value
new_version = versioned_value.version + 1
new_versioned_value =
combine(new_value, new_version)

for k in key_array
set(k, new_versioned_value)
end
end

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

def reliable_get(key)
key_array = [key+':1', key+':2', key+':3']
value_array = get_multi(key_array)

latest_version = 0
latest_value = nil
need_fix = false

for v in value_array
if (v.version > latest_verson)
if (!need_fix) && (latest_version > 0)
need_fix = true
end
latest_version = v.version
latest_value = v.value
end
end
versioned_value =
combine(latest_value, latest_version)

if need_fix
Thread.new do
reliable_set(key, versioned_value)
end
end

return versioned_value
end

Когда мы удаляем данные, мы фактически не удаляем их. Вместо этого мы помечаем данные как удаленные, но сохраняем их в кеше и позволяем истечь.

Регулирование пользователя

Интересным вариантом использования, отличным от кэширования, является ограничение слишком активного пользователя. По сути, вы хотите запретить слишком частые запросы пользователей.

user = get(userId)
if user == null
disallow request and warn user
else
add(userId, anything, inactive_period)
handle request
end