export ETCD_ENDPOINTS=kubectl get endpoints example-etcd-cluster "-o=jsonpath={.subsets[*].addresses[*].ip}:2379," Теперь в кластер можно что-нибудь записать следующей коман-дой:
kubectl exec my-etcd-cluster-0000 -- sh -c "ETCD_API=3 etcdctl --endpoints=${ETCD_ENDPOINTS} set foo bar"
Реализация блокировок
Простейшая форма синхронизации — взаимоисключающая блокировка, также известная как мьютекс (mutual exclusion, mutex). С блокировками знаком каждый, кто хоть раз зани-мался созданием параллельных программ для одной машины. Для распределенных систем работают те же принципы. Распре-деленные блокировки используют не память и ассемблерные 160Часть II. Паттерны проектирования обслуживающих систем инструкции, а рассмотренные ранее функции распределенных хранилищ типа «ключ — значение».
Как и с блокировками памяти, первый шаг — установление бло-кировки:
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() {
Читать дальше