Мир не идеален

в 15:32, , рубрики: Amazon Web Services, AWS, aws lambda, clojure, monitoring, функциональное программирование

image
Мир не идеален. В любой момент что-то может пойти не так. К счастью, большинство из нас не запускает ракеты в космос и не строит самолеты. Современный человек зависит от приложения в его телефоне и наша задача, сделать так, что бы в любой момент времени при любом стечении обстоятельств, он мог открыть приложеньку и посмотреть картинки с котиками.

Люди не идеальны. Мы постоянно делаем ошибки. Делаем опечатки, мы можем забыть что-то или поддаться лени. Человек может банально забухать или попасть под машину.

Железо не идеально. Жесткие диски умирают. Датацентры теряют каналы. Процессоры перегреваются и электрические сети выходят из строя.

Софт не идеален. Память течёт. Коннекты рвутся. Реплики ломаются и данные уходят в небытие.

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js