Кусок архитектуры М-Тех, который занимается нарезкой видео из лайв-потока:
Зеленым обозначены микросервисы. Здесь их 17, и это 1/20 часть всей системы. То есть всего около 340 микросервисов. С учетом того, что некоторые сервисы разворачиваются в нескольких экземплярах, получается 1120 нод.
Насколько мелким должен быть микросервис?
Настолько, чтобы он был независимым!
Как проверить независимость:
- Бизнес-задача сервиса должна описываться одним простым предложением. Если не получается - значит можно еще поделить. Например, "показать картинки на таймлайне видеоредактора". Если на эта картинку, например, нужно вотермарк наложить, то это надо делать в этом же сервисе. То есть делить нужно не технически, а по бизнес-задачам, иначе грануляция будет слишком высокая. Общие вещи - копипастятся и кладутся в отдельную папку vendor.
- У сервиса должно быть больше одного потребителя. Если один - значит слишком мелко поделили.
- Деплой сервиса не должен приводить к деплою других сервисов. Если приводит - значит есть зависимость нашего сервиса от другого, значит слишком мелко поделили.
Требования к микросервису:
- скрывает внутренние детали реализации
- деплоится независимо
- падая, не роняет все остальное
- легко мониторится
- реализует бизнес-модель
- полностью децентрализован
Конфигурация
Что пробовали?
- файлы конфигурации при деплое разливать по всем целевым машинам. Минус: сложно менять конфигурацию для одной ноды, количество загрузок конфигов растет линейно с ростом машин.
- файлы конфигурации класть в контейнер. Минус: при изменении конфига нужно пересобрать и передеплоить контейнет.
-
выставлять переменные окружения (рекомендуется в https://12factor.net/)
- при сборке контейнера (
ENV DB=...
). Минус: на этапе деплоя нет информации, где это будет лежать - при запуске контейнера. Минус: на этапе запуска нет возможности абстрагироваться от системы запуска.
- при сборке контейнера (
Решение
- общее K/V хранилище (у них consul)
- сервис должен быть заточен под смену конфигурации
-
микс вариаций с приоритетами:
- <СЕРВИС>/ (/conf/ms/recorder)
- <СЕРВИС>/<ВЕРСИЯ>/ (/conf/ms/recorder/0.12)
- <СЕРВИС>/<СРЕДА>/
- <СЕРВИС>/<ВЕРСИЯ>/<СРЕДА>/
- <СЕРВИС>/<ВЕРСИЯ>/<НОДА>/
Конфиги хранятся в тех же репозиториях, что и проекты.
Утилита git2consul при деплое загружает конфиги в consul.
Стартовый путь:
/conf/ms/<ИМЯ СЕРВИСА>/<BUILD NO>/
Все секретные данные (сертификаты, пароли к внешним сервисам, соль) хранятся в отдельных проектах, загружаются в Vault (https://www.vaultproject.io/), а в основном конфиге указывается путь, по которому их можно достать.
Мониторинг
Zabbix / Cacti / Nagios
Проблема в том, что при использовании микросервисов отказ в одном сервисе влечет за собой отказы во всей цепочке сервисов, зависимых от него. В результате из-за одной ошибки получаем десятки алертов.
Как решать?
- вместо полноценного мониторинга - строим dashboard.
- алерты по статистическим и пороговым значениям вместо бинарных
- на одном дэшболде сводить графики хост-машин и статистики по docker-демону
Diamond собирает метрики с хост-машины, cAdvisor - с контейнера. Потом это все собирается в InfluxDB и выводится в Grafana.
Логи из контейнеров собираются через Syslog, аггрегируются в Logstash, загоняются в ElasitSearch. Оттуда метрики по логам загоняются в Grafana, а общая работа с логами ведется в Kibana.
Трейсинг запросов: OpenZipkin
Альтернативы:
- cAdvisor - нет альтернатив, есть telegraf, но он пока сырой.
- InfluxDB - альтернативой может быть Prometheus, но он не умеет горизонтально масштабироваться, а InfluxDB умеет, но за деньги. Еще можно сделать сборку метрик в ElasticSearch, но он менее производителен и более требователен к ресурсам.
Еще недостатки Prometheus:
- хорош, когда нужно собирать данные с готовых стандартных компонент, которых очень много. Если таких компонент мало и плюс к этому нужны метрики по бизнес-логике, то нужно много писать кода на стороне клиента и разбираться во внутренней логике работы Prometheus
- нельзя использовать как events time-series db, то есть мы не можем кидать туда события, чтобы он потом сам посчитал метрики. Нужно заранее самому считать метрики на клиенте и кидать их уже в готовом виде.
- сложно делать интегрированные метрики. например, персентили времени отклика клиентам по всем серверам фронтенда. Это сделать в Prometheus невозможно вообще.
Общие рекомендации
- Сбор метрик - это важно, но еще важнее постоянный анализ полученных данных
- Система мониторинга должна быть более надежной и масштабируемой, чем то, что она контролирует
- Система должна быть оптимизирована для распределенных, недолговечных, облачных, контейнеризованных микросервисов
- Собирайте метрики часто и очень часто, стремитесь к интервалам < 10 сек, иначе разовые всплески можно пропустить
Тестирование
Не нужно тестировать взаимодействие с другими сервисами, нужно тестировать только контракт каждого сервиса.
Сложно тестировать ситуации, когда сервис доступен, но отвечает медленно, эпизодически или некорректно. В этой ситуации поможет паттерн Circuit Breaker и утилита, его реализующая - Hystrix.
Полезным будет сделать тестовый кластер из нескольких Raspberry Pi. Из него можно спокойно выдергивать сеть, питание, ставить туда медленные или сбойные флешки.
Хранение
Что пробовали
- проброс файловой системы в контейнер. Минусы: на одной машине данные пробросили, а на другой их нет. То есть нарушается изоляция.
- "Собирающие контейнеры" - на бэкэнд ставится проксирующий nginx, он пробрасывает трафик на фронтенд, а у себя собирает всю проходящую через него статику и за несколько итераций получается контейнер, где собраны все данные. Минусы: на монолите прекрасно работает, на микросервисах - нет.
- Shared data volume - заранее создается волюм, именуется, аттачится к нескольким контейнерам сервисов и таким образом получаем отдельный контейнер с данными, который можно деплоить вместе с контейнером сервисов.
- Flocker - сторонний менеджер волюмов данных. В отличие от стандартных волюмов докера, которые привязаны к одному серверсу, волюмы Флокера мобильны и могут быть использованы с любым контейнером. Минус: работает конечно через сеть, а большие объемы данных через сеть гонять неудобно.
Решение
Тут я не очень понял, поэтому далее цитата:
Используем **Ceph**, выделили отдельный storage cluster, подняли распределенную файловую систему, часть машин из докер-кластера помечается определенными метками. Когда запускаются контейнеры, то микросервисы, которые требуют такого типа хранения, ориентируются на эти метки. То есть разливаем именно на это подмножество машин.
Для баз данных:
- выделенные сервера
- особая конфигурация машин
- расширенная сеть
Базы крутятся не в докере, потому что у них особые требования к ресурсам.
Для инстансов БД используется особая схема именования, поэтому каждый сервис знает, как найти свою базу в этом кластере.
При этом здесь не нарушается паттерн "одна БД на сервис", потому что внутри сервера есть, например, 10 инстансов монги и каждый микросервис использует свой инстанс.
Обнаружение сервисов
Используется реестр сервисов:
- consul - K/V хранилище, DNS, health-check, работа с несколькими ДЦ из коробки
-
etcd - K/V хранилище
- skydock - Health-Check, регистрация docker-контейнеров
- skydns - DNS
- Apache Zookeeper - вариация K/V хранилища, сервис блокировок, вариация на тему DNS
Оркестрация
- docker swarm [1.2.2] - доставка, регистрация, масштабирование (consul)
- nomad [0.3.2] - доставка, регистрация, масштабирование (consul). Самый простой для разработчика, запуск одной командой. Но и сильно ограничен по функционалу.
- **kubernetes [1.2.4] - доставка, регистрация, масштабирование, LB (etcd)
- mesos [0.28.1] (marathon [1.1.1]) - доставка, регистрация, масштабирование, LB (zookeeper, HAProxy)
Разрешение адресов в момент деплоя
Есть проблема в том, что на момент разработки неизвестно, на какие адреса будет развернута система и следовательно неизвестно, кому по каким адресам обращаться. Эта проблема решена комбинацией nginx и consul.
Для consul используется следующий шаблон конфига nginx:
{{range services}}
{{ if in .Tags "demo" }}
{{if .Tags | join "," | regexMatch "urlprefix-"}} #urlprefix-/playlist
upstream {{.Name}} {
least_conn;
{{range service .Name}}server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=30 weight=1;{{end}}
}
{{end}}
{{end}}
{{end}}
server {
listen 80 default_server;
location /health {
add_header Content-Type text/plain;
return 200 'OK';
}
{{range services}}
{{ if in .Tags "demo" }}
{{if .Tags | join "," | regexMatch "urlprefix-"}}
{{range .Tags}}{{ if . | contains "urlprefix-" }}
location {{. | replaceAll "urlprefix-" ""}} {
{{end}}{{end}}
proxy_pass http://{{.Name}}/; # / в конце означает обрезку проксируемого пути
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
{{end}}
{{end}}
{{end}}
}
В первом куске шаблона: services
- это список живых сервисов, полученный из consul. Затем выбирается подмножество с тэгом demo, заем из них выбираем те сервисы, которым нужен роутинг (т.е. те, у которых в тэге есть флаг urlprefix). Если все эти условия выполнились, то генерится списко апстримов.
Во втором куске, который в разделе server: эти апстримы подключаются на те роуты, которые заявлены как требующие роутинга.
Результат работы шаблона:
upstream playlistapi {
least_conn;
server 10.1.1.2:5100 max_fails=3 fail_timeout=30 weight=1;
}
...
location /playlist {
proxy_pass http://playlistapi/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Исходники примера здесь: http://bit.ly/1TE1KdM
Чек-лист
- Как конфигурировать?
- Как мониторить и считать метрики?
- Как сервисы будут находить друг друга
- Как сервис будет взаимодействовать с остальными частями проекта?
- Как тестировать сервис и систему целиком?
- Как обрабатывать сбои связанных сервисов?
- Как переезжать на новую версию?
- Как масштабировать под нагрузкой?