Такое чувство, будто в последние месяцы я черезчур уж сильно сосредоточился на дебаггинге и .NET Core, и как-то подзабил на топик, ради которого весь этот блог и затевался — DevOps и распределённые приложения. Ошибку осознал, и сегодня мы посмотрим на что-нибудь более релевантное. Например, etcd.
Во время моего романа с распределёнными приложениями etcd всегда был где-то неподалёку. То он был альтернативой Consul, когда я разбирался с сервис-дискавери и управлением конфигурациями. То etcd всплыл как основное хранилище настроек кластера в Kubernetes. В общем, он везде. Так что я думаю, стоит разобраться, на что же это за зверь такой.
Что такое etcd
etcd — это примитивное но при этом высоконадёжное распределённое хранилище для пар ключ-значение. «Примитивное» не значит, что тупое. Это просто значит что etcd фокусируется только на хранении и связанными с этим задачами. Ну и «высоконадёжный» и «распределённый» подразумевает, что если даже одна из кластера машин с etcd упадёт, то данными и кластером всё будет в порядке. По сравнению с тем же Consul фич тут будет поменьше, но в реальных задачах для того, чтобы забить гвоздь, отбойный молоток и не нужен.
Правда, то, что etcd просто хранит пары ключ-значение, не означает, что там всего три команды (get/set/remove) и на этом всё. Данные можно хранить по-разному, и etcd поддерживает и транзакции для работы с данными, и возможность отслеживать изменения, и настраивать пользователей с ролями, и всякие примитивы для синхронизации использовать, и даже даёт возможность установить время жизни значений (TTL).
В общем, будем смотреть.
Установка
Etcd поддерживает лучший из способов для установки — скачай-и-готово, и это прекрасно. Как всегда, я очень ленивый и параноидальный, поэтому вместо того, чтобы запускать временное и непонятно что на своей машине, я лучше напишу Vagrantfile с инструкциями для закачки и развёртывания, и получу готовую к надругиванию виртуальную с etcd внутри в одну команду.
По итогу, вот какой Vagrantfile у меня получился.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Vagrant.configure("2") do |config|   config.vm.box = "ubuntu/xenial64"   etcd_version = "v3.3.1"   download_url = "https://storage.googleapis.com/etcd"   install_dir = "/tmp/etcd-download"   config.vm.provision "shell", inline: %{ 	mkdir -p #{install_dir} 	curl -L #{download_url}/#{etcd_version}/etcd-#{etcd_version}-linux-amd64.tar.gz -o /tmp/etcd-${etcd_version}-linux-amd64.tar.gz 	tar xzvf /tmp/etcd-${etcd_version}-linux-amd64.tar.gz -C #{install_dir} --strip-components=1 	rm -f /tmp/etcd-${etcd_version}-linux-amd64.tar.gz 	echo 'PATH=$PATH:#{install_dir}' >> /home/vagrant/.bashrc   } end | 
Код вполне простой. Первая подсвеченная секция задаёт переменные и адреса, а вторая качает, распаковывает и обновляет PATH для удобства. Как обычно, vagrant up запустит это всё великолепие, vagrant ssh запустит меня внутрь виртуальной машины, а там, например, можно посмотреть, с какой же версией etcd мы имеем дело:
| 1 2 3 4 5 | etcd --version #etcd Version: 3.3.1 #Git SHA: 28f3f26c0 #Go Version: go1.9.4 #Go OS/Arch: linux/amd64 | 
Запускаем маленький etcd кластер
В официальной документации команда для развёртывания даже однохостового кластера выглядит очень уже заморочено. На самом деле для экспериментов на localhost запускать etcd можно вообще без параметров:
| 1 2 3 | etcd # 2018-03-07 04:28:55.591939 I | etcdmain: etcd Version: 3.3.1 # 2018-03-07 04:28:55.592331 I | etcdmain: Git SHA: 28f3f26c0 | 
Чтобы убедиться, что кластер живой и реагирует на свет, его можно пнуть ещё одной утилиткой — etcdctl, и запросить членов кластера:
| 1 2 | etcdctl member list # 8e9e05c52164694d: name=default peerURLs=http://localhost:2380 clientURLs=http://localhost:2379 isLeader=true | 
Если бы я хотел посмотреть на кластер на другой машине, то пришлось бы подавать дополнительные параметр на вход (вроде -c http://localhost:2379), но опять же, на localhost всё работает и так.
Добавляем, запрашиваем, удаляем данные
Тот же самый etcdctl, что общался с кластером, может манипулировать и данными в нём. Правда, есть больше чем один API, которым можно это сделать, поэтому сразу стоит поставить переменную окружения с версией, которая нам подходит. Например, export ETCDCTL_API=3 переключит etcdctl на самый свежий 3й API .
И это на самом деле меняет много чего. Например, во второй версии была команда set для сохранения данных. А в третьей это уже put. Да и вообще, если вывести список команд, которые поддерживает API 2 и 3, можно очень хорошо удивиться.
Кстати о put, Вот как можно добавить и запросить назад парочку ключей-значений:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | etcdctl put debug-mode-server on  # создать debug-mode-server ключ со значением'on' #OK etcdctl put debug-mode-client off # создать ещё одину пару ключ-значение #OK etcdctl get debug-mode-server     # запрашиваем один ключ #debug-mode-client #on etcdctl get debug --prefix        # запрашиваем все, начинающиеся с 'debug' #debug-mode-client #off #debug-mode-server #on etcdctl del debug-mode-server     # удаляем debug-mode-server #1 | 
Работаем с данными по HTTP
etcdctl, конечно, не самый удобный способ скрестить свою программу с etcd. Хотя под капотом etcd общается по протоколу gRPC, к нему можно подключаться и по старому доброму HTTP при помощи встроенного grpc-gateway. HTTP API, конечно, немного с душком, и определённо застрял где-то у основания Модели Зрелости Ричардсона, но работает. Хотя кодировать и ключ и значение в base64 в каждом запросе, конечно, раздражает.
Вот как можно отправить в кластер пару mykey:myvalue по HTTP:
| 1 2 | curl -L http://localhost:2379/v3beta/kv/put -X POST -d "{\"key\":\"`echo 'mykey' | base64`\",\"value\":\"`echo 'myvalue' | base64`\"}" #{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"7","raft_term":"6"}} | 
И забрать назад:
| 1 2 3 | curl -L http://localhost:2379/v3beta/kv/range -X POST -d "{\"key\":\"`echo 'mykey' | base64`\"}" #{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"7","raft_term":"6"}, #"kvs":[{"key":"bXlrZXkK","create_revision":"7","mod_revision":"7","version":"1","value":"bXl2YWx1ZQo="}],"count":"1"} | 
Стрёмно, конечно, но работает ведь.
Другие приятные фичи
Дожидаемся изменений значений
А что если нам хочется, чтобы etcd как-то сообщил нам, когда значение какого-нибудь полезного ключа меняется? Без проблем. Можно даже ждать изменений ключа, которого вообще нет:
| 1 2 | etcdctl watch shutdown-requested #...долго и утомительно ждем | 
В Consul кстати была такая же штука. Так вот, watch запрос будет ждать до тех пор, пока кто-нибудь не обновит значение нашего shutdown-requested ключа.
| 1 | etcdctl put shutdown-requested now | 
И всё только для того, чтобы продолжить ждать дальше.
| 1 2 3 4 5 | etcdctl watch shutdown-requested #PUT #shutdown-requested #now #... ждем дальше | 
Транзакции
Или вот такой сценарий: нам нужно установить новое значение в ключ только в том случае, если текущее равно чему-то особенному. Ну вроде как есть кластер машин, в etcd хранится имя лидера кластера — ключ leader, и нам нужно установить имя нашей машины только в том случае, если лидера пока нет (none). Интерактивно это можно сделать при помощи команды txn.
Допустим, сначала leader-таки есть.
| 1 2 3 4 5 6 7 8 9 10 11 12 | etcdctl put leader me etcdctl txn --interactive #compares: #value("leader") = "none" # #success requests (get, put, del): #put leader myHostName # #failure requests (get, put, del): # #FAILURE | 
При таких начальных данных транзакция, разумеется ответила FAILURE, ведь значение ключа отличалось от none. Но если лидера убрать, то всё пройдёт хорошо:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | etcdctl put leader none #OK etcdctl txn --interactive #compares: #value("leader") = "none" # #success requests (get, put, del): #put leader myHostName # #failure requests (get, put, del): # #SUCCESS # #OK etcdctl get leader #leader #myHostName | 
Мораль
Вот такой он etcd. Не весь конечно, но достаточный кусок для того, чтобы сформировать какое-то мнение. Я всё ещё играюсь с его документацией, и практически в восторге от их «Demos» секции. Анимация, конечно, там зря, но короткие куски кода для всех основных фич — это прекрасно, и очень помогает на первых порах. Ну и, как это часто бывает, я уже вижу, где etcd можно было бы засунуть нам в продакшен, как бы двусмысленно эта фраза не звучала.
 
				 
                     
                    