Для разных — задач разные инструменты.
Наш опыт переезда на Elixir
Черепанов Сергей
Размахнин Никита
Fullstack Development
План
- Описание задачи.
- Почему именно Elixir и из чего мы выбирали
- Основы функционального программирования
- Основы межпроцессного взаимодействия
- Live Coding
Предостережение
Бизнес-логика
У нас курьерское приложение и много асинхронной логики во время выполнения заказа
?
Аналог Uber, только по доставке не людей
3к
курьеров
400+
заказов в день
2–3мин
поиск курьера
За год по Москве и Питеру
Проект состоит из
REST API на Django
TS React+Redux SSR приложение формы заказа
Telegram бот на Python (asyncio)
Flow React-Native Android приложение
Elixir Worker
AWS
AWS
CloudFormation
EC2 + AutoScaling
LoadBalancing
RDS
VPC with Bastion
SQS
Как это работает
Заказ через форму
Отправка данных на REST Server
Отправка сообщений в SQS
Worker подхватывает сообщение о новом заказе
Броадкаст курьерам
Performance:
7 couriers per second
Курьеры оставляют отклики
Берем первого готового и останавливаем броадкаст
Если курьер откажется от заказа,
броадкаст начинается заново
но уже не рассылается тем, кто его пропустил
Если за 5 минут курьер не выехал и не отказался,
шлем СМС клиенту
Если курьер выехал, шлем другое СМС
(если первая не была отправлена, то ее отменяем)
Отмена заказа
Остановка броадкаста
Уведомления в чат-админку и курьеру
Таймауты
Не найден курьер после броадкаста в течение 3-х минут
Время заказа вышло, а он не завершен
...
Все это действо должно работать и для бота, и для приложения
Выбирая
подходящий язык
C#
Система типов
Богатая экосистема
Core — сыроват?
но есть отзывы переноса крупных проектов, и небольшой опыт
Язык хорош и зрел
Java/Scala
Система типов
...давайте вы будете уметь готовить еще и Java
Python
Быстро и круто, хайпово и удобно
Опыт
Асинхронность?
Динамическая типизация?
Haskell
Мощная система типов
ФП
Экосистема?
Асинхронность??
Скорость разработки???
Node.JS
Шарим в JS
Один поток в 2017???
Масштабироваться почти только железом
оверхеды по памяти и системным процессам
JavaScript :(
Elixir
Whatsap, Heroku, Amazon, Wargaming, Discord
Автогенерируемые, довольно подробные
Большой мир публикаций на Медиуме, есть чуть-чуть на Хабре
Доки
Статьи на Хабре
ПОСМОТРЕЛИ НА
Написан в середине 80-х в лабораториях компании Ericsson для работы в сфере телекома
Erlang
По требованиям ПО в телекоме: быстро, эффективно, параллельно и отказоустойчиво
Развивается инженерами по сей день
ПОСМОТРЕЛИ НА
Разработчиков не так много, как на js, но они есть
Экосистему
Основное количество библиотек — весьма высокое качество
ПОСМОТРЕЛИ НА
В 90% случаев есть тесты, которые зачастую лучше документации
Тесты
ПОСМОТРЕЛИ НА
Пулл реквесты
ПОСМОТРЕЛИ НА
4к закрытых пуллерквестов, большинство которых - code style & docs
Остальное время комьюнити обсуждает красивости кода (лично мой взгляд)
Как у нас развивался воркер.
...потом оказалось
Долго запускали супервизор
--no-halt
Состоянию уделено большое внимание в Erlang: качественные абстракции для работы со стейтом
Долго понимали, как работать со стейтом
Первым сделана работа с очередями сообщений
Интеграция с нашим REST
Столкнулись с косяками AWS SQS (накрутили Meta Storage)
Релиз настроили быстро и безболезненно,
но с трудом поставили сам Эликсир на Amazon Linux
В начале осени: допокрыли тестами и разнесли работу с курьерами
Сделали два рефакторинга
В конце осени: выделили под каждый заказ по отдельному процессу
Продолжаем переносить бизнес-логику из телеграм бота на воркер
Основы функционального программирования
Основные принципы
Побочные эффекты:
Чистота функций
- Вывод в консоль
- Запись в базу данных
Недетерминированность
-
Time.now()
- Чтение из базы
High Order Function
sum = fn a, b -> a + b end
10 == Enum.reduce([1, 2, 3, 4], sum)
Частичное применения
add2 = fn number -> sum(number, 2) end
[3, 4, 5] == Enum.map([1, 2, 3], add2)
Никто не перетрет ваши переменные в промежутке между строками кода
Иммутабельность из коробки
Быстрое сравнение по ссылке
Формализованность, которая улучшает статический анализ
Не нужны локи :)
Иммутабельность из коробки
man = %{name: "john"}
try_mutate_if_you_can(man)
assert man.name == "john"
Инкапсуляция работы со стейтом
Pattern Matching
[head | tail] = [1, 2, 3, 4]
man = %{gender: :male, name: "john", age: 34}
greet(man)
def greet (%{name: name} = man) do
..
end
Рекурсия
def loop(state) do
receive do ->
:inc ->
loop(state + 1)
:stop ->
state
end
end
Elixir
Piping
datetime
|> Timex.parse!("{ISO:Extended}")
|> Timex.Timezone.convert(@timezone)
|> Timex.format!("{h24}:{m}")
Function capturing
couriers = [
%{name: "joe", id: 1},
%{name: "mike", id: 2},
]
slot = Enum.map(couriers, &(&1.id))
# fn courier -> courier.id end
Real world
slot = {1400, [123, 126, 129]}
couriers_slots = couriers
|> Enum.sort(&(&1.priority > &2.priority))
|> Enum.chunk_by(&(&1.priority))
|> Enum.map(fn (slot = [head | _]) -> {
head.priority,
Enum.map(slot, &(&1.id))
} end)
Макросы
defmodule Worker.Models.CourierTest do
use ExUnit.Case
test "is_has_permission_for_order" do
assert Courier.is_has_permission_for_order(
@default_courier, @default_order
) == true
assert Courier.is_has_permission_for_order(
@default_courier, @car_required_order
) == false
end
end
Основы межпроцессного взаимодействия
О процессах
Об эрланговом шедулере и зачем он нужен
О сообщениях и стандартных паттернах работы с ними
Очереди
Send
send(pid, {:hello, :world})
Receive
message =
receive do
{:hello, name} -> name
end
Timeout
{:ok, message} =
receive do
{:hello, name} -> {:ok, name}
after
1_000 -> {:error, :timeout}
end
Ref для ответа
{from, ref, name} =
receive do
{:hello, from, ref, name} -> {from, ref, name}
end
send(from, {:aloha, ref, name})
Back Pressure
{:message_queue_len, length} =
:erlang.process_info(pid, :message_queue_len)
Fault Tolerance
Let it crash!
Позови меня с собой
Умирая, процесс шлет сигнал смерти всем слинкованным процессам
spawn_link(fn ... end)
Мониторинг процессов
Умирая, процесс шлет уведомление, что он умер
ref = Process.monitor(pid)
Супервизоры
Слушают уведомления о дочерних процессах и перезапускают их
Деревья наблюдения
Супервизоры можно композировать
Другие супервизоры будут ветвями, а рабочие процессы — листьями
Пример supervisor tree:
Перезапуск после краша
One For One
One For All
Rest For One
Мониторинг
Erlang позволяет мониторить почти что все
Общее состояние VM
Количество процессов | 8782 |
Run queue | 0 |
Error logger queue | 0 |
Memory | 149Mb |
Bytes in/out | 0 |
GC count | 0 |
Reductions | 12010 |
Sheduler Usage | 1% |
Системные сообщения
busy_port |
busy_dist_port |
{long_gc, 50} |
{long_schedule, 50} |
{large_heap, 1_000_000}
Мониторинг каждого отдельного процесса
- reductions,
- heap_size,
- memory_used,
- message_queue_len
iex --sname baz --remsh worker@sergey-laptop
Hot Code Reload
У Эрланга есть подмена кода на лету
То есть да, можно без остановки сервера и приложения на нем сделать релиз
Но надо ручками почти во всех модулях написать хуки, которые умеют мигрировать стейт, например.
И еще много другой боли перетерпеть, мы туда даже не лезем...
Выводы
Live coding
How we start elixir development
By Sergey Cherepanov
How we start elixir development
We have learned Elixir and use it in our courier delivery service Ptichka. It is a talk about our experience
- 565