Б Бёрнс - Распределенные системы. Паттерны проектирования
- Название:Распределенные системы. Паттерны проектирования
- Автор:
- Жанр:
- Издательство:Питер
- Год:2019
- ISBN:978-5-4461-0950-0
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Б Бёрнс - Распределенные системы. Паттерны проектирования краткое содержание
Распределенные системы. Паттерны проектирования - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
Как и с блокировками памяти, первый шаг — установление бло-кировки:
func (Lock l) simpleLock() boolean {
// Сравнение с заменой "1" на "0"
locked, _ = compareAndSwap(l.lockName, "1", "0") return locked
}
Блокировки может еще не существовать, поскольку мы первые, кто ее запрашивает. Учтем и это:
func (Lock l) simpleLock() boolean {
// Сравнение с заменой "1" на "0"
locked, error = compareAndSwap(l.lockName, "1", "0") // Блокировки не существует, пытаемся записать "1" // поверх несуществующего значения
if error != nil {
locked, _ = compareAndSwap(l.lockName, "1", nil) }
return locked
}
Традиционные реализации блокировки останавливают испол-нение на время действия блокировки, поэтому нам еще понадо-бится что-то вроде:
func (Lock l) lock() {
while (!l.simpleLock()) {
sleep(2)
}
}
Проблема данной реализации, несмотря на ее простоту, состоит в том, что после снятия блокировки придется ждать минимум 1 секунду, чтобы установить ее снова. К счастью, многие храни-лища «ключ — значение» позволяют следить за изменениями, Глава 9. Выбор владельца 161
не прибегая к опросу сервера, поэтому реализация может вы-глядеть таким образом:
func (Lock l) lock() {
while (!l.simpleLock()) {
waitForChanges(l.lockName)
}
}
Реализация снятия блокировки будет выглядеть так: func (Lock l) unlock() {
compareAndSwap(l.lockName, "0", "1")
}
Может показаться, что работа сделана, но помните, что речь идет о распределенной системе. Установивший блокировку процесс может отказать до ее снятия, а значит, снять ее будет уже некому. В такой ситуации система зависнет. Для разрешения данной про-блемы воспользуемся возможностью назначить ключу срок дей-ствия (TTL). Изменим функцию simpleLock так, чтобы она всегда записывала TTL. Таким образом, если в течение данного времени процесс не снимет блокировку, она будет снята автоматически. func (Lock l) simpleLock() boolean {
// Сравнение с заменой "1" на "0"
locked, error = compareAndSwap(l.lockName, "1", "0", l.ttl) // Блокировки не существует, пытаемся записать "1" // поверх несуществующего значения
if error != nil {
locked, _ = compareAndSwap(l.lockName, "1", nil, l.ttl) }
return locked
}

162Часть II. Паттерны проектирования обслуживающих систем Сторожевой таймер аварийно прекратит исполнение програм-мы, если вы не сняли блокировку в период TTL. Добавив к блокировкам TTL, мы привнесли ошибку в функцию разблокировки. Рассмотрим следующую ситуацию.
1. Процесс 1 устанавливает блокировку с TTL, равным t .
2. В силу некоторых причин процесс работает очень медленно и не справляется с задачей за время t .
3. Срок действия блокировки истекает.
4. Процесс 2 ставит блокировку, поскольку процесс 1 ее потерял в связи с истечением TTL.
5. Процесс 1 заканчивает работу и вызывает функцию разбло-кировки .
6. Процесс 3 ставит блокировку.
На данный момент процесс 1 считает, что снял установленную им блокировку. Он не знает, что потерял блокировку из-за ис-течения TTL, и фактически снял блокировку, установленную процессом 2. Тут появляется процесс 3 и ставит блокировку. Теперь процессы 2 и 3 оба считают, что поставили блокировку, и тут-то и начинается потеха.
К счастью, хранилища «ключ — значение» поддерживают верси-онность ресурсов . Версия ресурса меняется при каждой записи. Функция блокировки может записывать версию ресурса и рас-ширять операцию compareAndSwap проверкой на совпадение не только значения, но и версии ресурса. Функция блокировки будет выглядеть следующим образом:
func (Lock l) simpleLock() boolean {
// Сравнение с заменой "1" на "0"
locked, l.version, error = compareAndSwap(l.lockName, "1", "0", l.ttl)
Глава 9. Выбор владельца 163
// Блокировки не существует, пытаемся записать "1" // поверх несуществующего значения
if error != null {
locked, l.version, _ = compareAndSwap(l.lockName, "1", null, l.ttl)
}
return locked
}
Функция разблокировки будет выглядеть так: func (Lock l) unlock() {
compareAndSwap(l.lockName, "0", "1", l.version) }
Таким образом, блокировка снимается, только если TTL не истек.
Практикум. Реализация блокировок в etcd При реализации блокировок в etcd ключи можно применять для имен блокировок, а начальные условия записи задавать таким образом, чтобы в каждый момент времени существовал только один владелец блокировки. Для простоты поработаем с утилитой командной строки etcdctl для установки и снятия блокировки. В действительности же вам следует воспользо-ваться языком программирования. Клиенты etcd есть для всех популярных языков программирования.
Начнем с создания блокировки my-lock :
kubectl exec my-etcd-cluster-0000 -- sh -c \ "ETCD_API=3 etcdctl --endpoints=${ETCD_ENDPOINTS} set my-lock unlocked"
Это создаст в etcd блокировку my-lock с начальным значением unlocked .
164Часть II. Паттерны проектирования обслуживающих систем Допустим, Алиса и Боб хотят установить блокировку my-lock . Они пытаются записать свои имена в блокировку, исходя из того, что она изначально снята.
Алиса выполняет команду:
kubectl exec my-etcd-cluster-0000 -- sh -c \ "ETCD_API=3 etcdctl --endpoints=${ETCD_ENDPOINTS} \ set --swap-with-value unlocked my-lock alice" тем самым ставя блокировку. Теперь блокировку пытается уста-новить Боб:
kubectl exec my-etcd-cluster-0000 -- sh -c \ "ETCD_API=3 etcdctl --endpoints=${ETCD_ENDPOINTS} \ set --swap-with-value unlocked my-lock bob" Error: 101: Compare failed ([unlocked != alice]) [6] Как видим, попытка Боба установить блокировку оказалась не-удачной, так как блокировка была поставлена Алисой. Чтобы снять блокировку, Алиса записывает значение unlocked вместо alice :
kubectl exec my-etcd-cluster-0000 -- sh -c \ "ETCD_API=3 etcdctl --endpoints=${ETCD_ENDPOINTS} \ set --swap-with-value alice my-lock unlocked" Реализация владения
Блокировки хороши для установления временного владения некоторым важным компонентом. Иногда может понадобиться установить владение ресурсом на все время работы компонента. Возьмем, к примеру, кластер Kubernetes с высокой доступ-ностью. Допустим, в нем есть несколько экземпляров плани-ровщика, но только один из них активно принимает решения. Кроме того, как только процесс становится активным планиров-щиком, он остается таковым до момента отказа. Глава 9. Выбор владельца 165
Одним из подходов будет поднятие TTL до очень большой величины — скажем, недели или даже больше. Существенный недостаток такого подхода состоит в том, что в случае отказа текущего владельца блокировки новый будет выбран только по истечении TTL, неделю спустя.
Вместо этого мы можем создать возобновляемую блокировку , которая будет периодически возобновляться владельцем, по-зволяя процессу держать блокировку в течение произвольного периода времени.
Расширим предыдущую версию функции блокировки возмож-ностью возобновления ее владельцем:
func (Lock l) renew() boolean {
locked, _ = compareAndSwap(l.lockName, "1", "1", l.version, ttl) return locked
}
Для того чтобы поддерживать блокировку в течение неопре-деленного промежутка времени, придется выполнять эти дей-ствия в отдельном потоке. Обратите внимание, что блокировка возобновляется каждые TTL / 2 секунды, чтобы снизить риск ее случайного истечения в силу особенностей подсчета вре-мени:
Читать дальшеИнтервал:
Закладка: