Мир не идеален. В любой момент что-то может пойти не так. К счастью, большинство из нас не запускает ракеты в космос и не строит самолеты. Современный человек зависит от приложения в его телефоне и наша задача, сделать так, что бы в любой момент времени при любом стечении обстоятельств, он мог открыть приложеньку и посмотреть картинки с котиками.
Люди не идеальны. Мы постоянно делаем ошибки. Делаем опечатки, мы можем забыть что-то или поддаться лени. Человек может банально забухать или попасть под машину.
Железо не идеально. Жесткие диски умирают. Датацентры теряют каналы. Процессоры перегреваются и электрические сети выходят из строя.
Софт не идеален. Память течёт. Коннекты рвутся. Реплики ломаются и данные уходят в небытие.
Shit happens — как говорят наши заокеанские друзья. Что же мы можем со всем этим сделать? А ответ банален до простоты — ничего. Мы можем вечно тестировать, поднимать тонну окружений, копировать продакшн и держать сто тысяч резервных серверов, но это все равно не спасет: мир не идеален.
Единственный верное решение здесь — это смириться. Нужно принять мир таким какой он есть и минимизировать потери. Каждый раз настраивая новый сервис нужно помнить — он сломается в самый неподходящий момент.
Он обязательно сломается. Ты обязательно сделаешь ошибку. Железо обязательно выйдет из строя. Кластер обязательно рассыпется. И по законам этого неидеального мира — это случится именно тогда, когда ты этого меньше всего ожидаешь.
Что делает большинство из нас что бы обмануть всех (в том числе и себя)? — Мы настраиваем алерты. Мы пишем хитрые метрики, собираем логи и создаем алерты, тысячи, сотни тысяч алертов. Наши почтовые ящики переполнены. Наши телефоны разрываются от смс и звонков. Мы сажаем целые этажи людей смотреть на графики. А когда в очередной раз мы теряем доступ к сервису, начинаются разборы: что же мы забыли замониторить.
Все это лишь видимость надежности. Никакие алерты, метрики и мониторинги не помогут.
Сегодня тебе позвонили, и ты починил сервис — никто и не заметил, что что-то сломалось. А завтра ты уехал в горы. А послезавтра забухал. Люди не идеальны. К счастью мы инженеры, живем в неидеальном мире и учимся его побеждать.
Так почему же надо просыпаться по ночам или утром вместо кофе читать почту. Почему бизнес должен зависеть от одного человека и от его работоспособности. Почему. Я не понимаю.
Я лишь только понимаю, что так жить нельзя, и я не хочу так жить. А ответ прост: Автоматизируй это (да, именно с большой буквы). Нам нужны не просто алерты и звонки по ночам. Нам нужны автоматические реакции на эти сообщения. Мы должны быть уверены, что система может починить себя сама. Система должна быть гибкой и уметь изменяться.
К сожалению, у нас пока нет достаточно умного ИИ. К счастью, все наши проблемы формализуемы.
У меня нет серебрянной пули, но зато у меня есть Proof of Concept для AWS.
AWS Lambda
Serverless — в первую очередь, то, что не запущено сломаться не может.
Event based — получили событие, обработали, выключились.
Умеет JVM — а значит, можно использовать весь опыт из Java мира (и значит, что я могу использовать Clojure).
3d-party — Не нужно следить за AWS Lambda и поддерживать.
Pipeline выглядит следующим образом:
Событие -> SNS Topic -> AWS Lambda -> Реакция
К слову, SNS topic может иметь несколько endpoints. Значит, можно банально добавить почту и получать так же уведомления. А можем расширить lambda функцию и сделать уведомления намного полезнее: например, слать алерты сразу вместе с графиками или добавить отправку SMS.
Целиком пример одной Lambda функции можно найти по ссылке: github.com/lowl4tency/aws-lambda-example
Лямбда функция прибивает все ноды в ELB не в состоянии inService.
Разбор кода
В данном примере мы будем убивать все ноды которые не находятся в состоянии InService. К слову, вся Lambda функция занимает ~50 строк кода в одном файле, а значит простота поддержки и легкость входа.
Любой проект на Clojure начинается с project.clj
Я использовал официальный Java SDK и прекрасную библиотечку Amazonica, которая является враппером для этого SDK. Ну и что бы не тащить много лишнего, исключаем те части SDK, которые нам не понадобится
[amazonica "0.3.52" :exclusions [com.amazonaws/aws-java-sdk]]
[com.amazonaws/aws-java-sdk-core "1.10.62"]
[com.amazonaws/aws-lambda-java-core "1.1.0"]
[com.amazonaws/aws-java-sdk-elasticloadbalancing "1.11.26"
:exclusions [joda-time]]
[com.amazonaws/aws-java-sdk-ec2 "1.10.62"
:exclusions [joda-time]]
[com.amazonaws/aws-lambda-java-events "1.1.0"
:exclusions [com.amazonaws/aws-java-sdk-dynamodb
com.amazonaws/aws-java-sdk-kinesis
com.amazonaws/aws-java-sdk-cognitoidentity
com.amazonaws/aws-java-sdk-sns
com.amazonaws/aws-java-sdk-s3]]]
Для большей гибкости каждой Lambda функции я использую конфигурационный файл с самым обычным edn. Для того что бы получить возможность обрабатывать события нам нужно немного изменить объявление функции
(ns aws-lambda-example.core
(:gen-class :implements [com.amazonaws.services.lambda.runtime.RequestStreamHandler])
Точка входа. Читаем событие на входе, обрабатываем данное событие с помощью handle-event и пишем в поток JSON в качестве результата.
(defn -handleRequest [this is os context]
"Parser of input and genarator of JSON output"
(let [w (io/writer os)]
(-> (io/reader is)
json/read
(-> (io/reader is)
json/read
walk/keywordize-keys
handle-event
(json/write w))
(.flush w))))
Рабочая лошадка:
(defn handle-event [event]
(let [instances (get-elb-instances-status
(:load-balancer-name
(edn/read-string (slurp (io/resource "config.edn")))))
unhealthy (unhealthy-elb-instances instances)]
(when (seq unhealthy)
(pprint "The next instances are unhealthy: ")
(pprint unhealthy)
(ec2/terminate-instances :instance-ids unhealthy))
{:message (get-in event [:Records 0 :Sns :Message])
:elb-instance-ids (mapv :instance-id instances)}))
Получаем список нод в ELB и фильтруем их по статусу. Все ноды, которые в состоянии InService удаляем из списка. Остальные терминейтим.
Все что мы печатаем через pprint попадет в логи CloudWatch. Это может быть полезно для дебага. Так как у нас нет постоянно запущенной лямбды и нет возможности подключиться к REPL это может быть довольно полезно.
{:message (get-in event [:Records 0 :Sns :Message])
:instance-ids (mapv :instance-id instances)}))
В данном месте вся структура, которуя сгенерим и возвратим из этой функции будет записана в JSON и увидим в результате выполнения в Web интерфейсе Lambda.
В функции unhealthy-elb-instances фильтруем наш список и получаем instance-id только для тех нод, которые ELB посчитал нерабочими. Получаем список инстансев и фильтруем их по тегам.
(defn unhealthy-elb-instances [instances-status]
(->>
instances-status
(remove #(= (:state %) "InService"))
(map :instance-id)))
В функции get-elb-instances-status вызываем АПИ метод и получаем список всех нод со статусами для одного определенного ELB
(defn get-elb-instances-status [elb-name]
(->>
(elb/describe-instance-health :load-balancer-name elb-name)
:instance-states
(map get-health-status )))
Для удобства убираем лишнее и генерируем список только с информацией которая нам интересна. Это instance-id и status каждого instance.
(defn get-health-status [instance]
{:instance-id (:instance-id instance)
:state (:state instance)})
И фильтруем наш список, убирая те ноды, что находятся в состоянии InService.
(defn unhealthy-elb-instances [instances-status]
(->>
instances-status
(remove #(= (:state %) "InService"))
(map :instance-id)))
И это всё: 50 строк, которые позволят не просыпаться по ночам и спокойно ехать в горы.
Deployment
Для простоты деплоймента я использую простой bash-script
#!/bin/bash
# Loader AWS Lambda
aws lambda create-function --debug
--function-name example
--handler aws-lambda-example.core
--runtime java8
--memory 256
--timeout 59
--role arn:aws:iam::611066707117:role/lambda_exec_role
--zip-file fileb://./target/aws-lambda-example-0.1.0-SNAPSHOT-standalone.jar
Настраиваем алерт и прикручиваем его к SNS topic. SNS topic прикручиваем к лямбде как endpoint. Спокойно едем в горы или попадаем под машину.
К слову, за счет гибкости можно запрограммировать любое поведение системы и не только по системным, но и по бизнес-метрикам.
Спасибо.
Автор: 8ll