Дистрибутиран Elixir

Курс по Elixir 2023, ФМИ

Да си припомним

  1. Какво е BEAM Node?
  2. Какво е OTP Application?
  3. Какво е Supersvision дърво?
Курс по Elixir 2023, ФМИ

Как се стартира BEAM node?

iex -S mix
Курс по Elixir 2023, ФМИ

Ако не ни е нужен shell?

mix run --no-halt
elixir -S mix run --no-halt
Курс по Elixir 2023, ФМИ

Image-Absolute

Курс по Elixir 2023, ФМИ

MIX

  • Mix компилира кода и го слага на правилно място.
  • Mix генерира app file и оправя зависимостите.
  • Mix знае пътищата, които са ни нужни, и как да стартира проекта.
Курс по Elixir 2023, ФМИ

RELEASE

  • Има как да правим release, който да се пуска в production и не ползва Mix.
  • OTP Release е нещо, което може да се стартира:
    • Чрез инсталиран Erlang (не трябва Elixir);
    • Или дори без Erlang (енкапсулира ERTS в самия Release).
  • Elixir идва командата mix release за компилиране на release
Курс по Elixir 2023, ФМИ

Image-Absolute

Курс по Elixir 2023, ФМИ

Дистрибутиран Elixir

Image-Absolute

Курс по Elixir 2023, ФМИ

Съдържание

  1. Какво означава да сме дистрибутирани?
  2. Node-ове и функции за свързване и комуникация.
  3. Проблемите на дистрибутираните системи.
Курс по Elixir 2023, ФМИ

Какво означава да сме дистрибутирани?

  • Дистрибутираност значи, че програмата ни се изпълнява на две или повече виртуални машини.
  • Elixir ни дава добри инструменти за постигане на това.
Курс по Elixir 2023, ФМИ

Какво означава да сме дистрибутирани?

  • Един node е една виртуална машина.
  • Всеки node може да е отворен или затворен за комуникация с други nodes.
Курс по Elixir 2023, ФМИ

Функции за статус на node

  • Как да видим името на node-a си?
node()
#=> :nonode@nohost
Курс по Elixir 2023, ФМИ

Функции за статус на node

  • Как да направим node-а си 'жив'?
Node.alive?
#=> false

Node.start(:meddle, :shortnames)
#=> {:ok, #PID<0.115.0>}
node()
#=> :"meddle@meddlandr"
Node.alive?
#=> true
Курс по Elixir 2023, ФМИ

Какво е :net_kernel

  • Това е OTP модул, който се ползва вътрешно от Node модула, за да направи node дистрибутиран.
  • Като цяло дърво от процеси, които знаят как да комуникират с отдалечени nodes и техните процеси.
  • pid-ът, който се връща от Node.start, е на supervisor на това дърво.
  • Когато тези процеси вървят, node-ът придобива име и може да бъде намерен в мрежата.
Курс по Elixir 2023, ФМИ

Стартиране от командния ред (кратко име)

iex --sname njichev
#=> Erlang/OTP 24 [erts-12.0] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

#=> Interactive Elixir (1.14.3) - press Ctrl+C to exit (type h() ENTER for help)
#=> iex(njichev@meddlandr)1>
Курс по Elixir 2023, ФМИ

Стартиране от командния ред (пълно име)

iex --name slavi@127.0.0.1
#=> Erlang/OTP 24 [erts-12.0] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

#=> Interactive Elixir (1.14.3) - press Ctrl+C to exit (type h() ENTER for help)
#=> iex(slavi@127.0.0.1)1>
Курс по Elixir 2023, ФМИ

Свързване на node-ове

  • Списък на свързаните node-ове.
Node.list()
#=> []
Курс по Elixir 2023, ФМИ

Свързване на node-ове

  • Свързване на node-ове.
# @meddle
Node.connect(:ivan@meddlandr)
#=> true

Node.list()
#=> [:ivan@meddlandr]
Курс по Elixir 2023, ФМИ

Свързване на node-ове

  • Връзките между node-ове са симетрични и транзитивни.
# @ivan
Node.list()
#=> [:meddle@meddlandr]

# @njichev
Node.connect(:ivan@meddlandr)
#=> true

Node.list()
#=> [:ivan@meddlandr, :meddle@meddlandr]
Курс по Elixir 2023, ФМИ

Свързване на node-ове - Kubernetes

  • Най-малката единица за deployment в Kubernetes е pod.
  • Всяко приложение се изпълнява в контейнер в някой pod.
  • При deployment имената на pod-овете се променят (но не винаги).
  • При deployment нашето приложение може да получи различен IP адрес.
  • Това прави формирането на клъстър по-трудно
Курс по Elixir 2023, ФМИ

Свързване на node-ове - Kubernetes

config :libcluster,
  topologies: [
    k8s: [
      strategy: Elixir.Cluster.Strategy.Kubernetes,
      config: [
        mode: :dns,
        kubernetes_node_basename: "sanbase",
        kubernetes_selector: "app=sanbase",
        polling_interval: 10_000
      ]
    ]
  ]
Курс по Elixir 2023, ФМИ

libcluster

  • Библиотека за автоматично формиране на BEAM клъстери
  • Предоставя различни механизми:
    • Стандарните Erlang механизми: epmd, .hosts.erlang
    • Multicast UDP gossip протокол
    • Чрез използване на Kubernetes API и конфигуриране на етитек (label)
    • Чрез Rancher
Курс по Elixir 2023, ФМИ

Проверка дали node е в мрежата

# @ivan
Node.ping(:njichev)
#=> :pang
Node.ping(:njichev@meddlandr)
#=> :pong

Node.ping(:"slavi@127.0.0.1")
#=> [error] ** System NOT running to use fully qualified hostnames **
#=> ** Hostname 127.0.0.1 is illegal **
:pang
Курс по Elixir 2023, ФМИ

Файлът .hosts.erlang

  • Съдържа списък от познати node-ове, които биха могли да се намерят в мрежата.
  • Файлът .hosts.erlang:
'192.168.3.10'.
'192.168.3.11'.
'192.168.3.12'.
  • Функциата :net_adm.names/0 ще листне всичко от там:
:net_adm.names()
#=> {:ok, [{'b', 45325}, {'a', 33599}]}
Курс по Elixir 2023, ФМИ

Стартиране със синхронизирани node-ове

  • Използва се sys.config за задаване на node-ове, които искаме да са online.
  • Ако използваме sync_nodes_optional се чака даденото време в sync_nodes_timeout и приложението тръгва:
[{kernel,
  [
    {sync_nodes_optional, ['b@127.0.0.1', 'c@127.0.0.1']},
    {sync_nodes_timeout, 30000}
  ]}
].
Курс по Elixir 2023, ФМИ

Дистрибутиран Application

  • Използва същата идея като синхронизацията.
  • За един и същ application се стартират няколко node-a.
  • С команди като:
iex --name a@127.0.0.1 --erl "-config da.sys.config" -S mix
Курс по Elixir 2023, ФМИ

Дистрибутиран Application

  • Такъв setup ще се нуждае от sys.config конфигурации като:
[{kernel,
  [{distributed, [{ecio, 5000, ['a@127.0.0.1', {'b@127.0.0.1', 'c@127.0.0.1'}]}]},
   {sync_nodes_mandatory, ['b@127.0.0.1', 'c@127.0.0.1']},
   {sync_nodes_timeout, 20000}
  ]
 }
].
Курс по Elixir 2023, ФМИ

Дистрибутиран Application

  • Описаният distributed Application ще върви само на един node.
  • Ако този node падне, един от другите ще стартира application-а.
  • Ако се появи най-приоритетният node в списъка, той ще поеме контрола в две стъпки:
    • Node-ът, който изпълнява Application-а, ще го спре
    • Приоритетният node ще стартира Application-a.
Курс по Elixir 2023, ФМИ

Къси и дълги имена

  • Късите имена са за локална (localhost) употреба. Не се използва DNS.
  • Дългите имена са за мрежа и могат да съдържат достъпен IP адрес или domain name
Курс по Elixir 2023, ФМИ

Създаване на процеси на отдалечени node-ове

  • Основата на комуникацията между node-ове са процесите.
  • Node.spawn и неговите версии:
Node.spawn(:ivan@meddlandr, fn -> IO.puts(node()) end)
#=> #PID<13382.125.0>
#=> ivan@meddlandr
Курс по Elixir 2023, ФМИ

Създаване на процеси на отдалечени node-ове

# @slavi

defmodule DB do
  def tell_us_secret do
    IO.puts("Познавам всички и всеки!")
  end
end
Курс по Elixir 2023, ФМИ

Създаване на процеси на отдалечени node-ове

# @meddle
Node.spawn(:slavi@meddlandr, DB, :tell_us_secret, [])
#output: Познавам всички и всеки!
#=> #PID<9473.144.0>
Курс по Elixir 2023, ФМИ

Създаване на процеси на отдалечени node-ове

  • Разбира се има Node.spawn_link и Node.spawn_monitor.
  • Това са процеси, които наистина вървят на отдалечения node.
  • Наследяват group_leader-а на процеса, който ги е създал.
    • Ако първият аргумент на Node.spawn* не е node(), то IO операциите ще изпращат съобщения по мрежата към този node, от който е извикан Node.spawn*.
Курс по Elixir 2023, ФМИ

Създаване на процеси на отдалечени node-ове

# @meddle
Node.spawn(:ivan@meddlandr, fn -> Process.sleep(60_000); IO.puts(node()) end)
#=> #PID<13382.126.0>
Process.group_leader
#=> #PID<0.66.0>

# @ivan
Process.info(pid(0, 129, 0), :group_leader)
#=> {:group_leader, #PID<11629.66.0>} # Забележете, че първият елемент на pid не е 0!
Курс по Elixir 2023, ФМИ

Глобален регистър на процеси

  • Къде ще се отпечата "Hey" и защо?
# a@127.0.0.1 (ECIO)
device_pid = Process.whereis(ECIO.Device)
#=> #PID<0.234.0>
:global.register_name(ECIO.Device, device_pid)
#=> :yes

# b@127.0.0.1 (Asynch)

IO.puts(:global.whereis_name(ECIO.Device), "Hey")
#=> :ok
Курс по Elixir 2023, ФМИ

Глобален регистър на процеси

  • Разбира се има и :global.unregister_name/1 по атом.
  • Глобалният регистър е достъпен за всички connected nodes.
Курс по Elixir 2023, ФМИ

Наблюдаване на Node-ове

  • Можем да наблюдаваме node кога става offline.
  • Същата функция с false ще спре наблюдението.
  • Процесът извикал тази функция с true трябва да приема съобщението {:nodedown, <nodename>}.
# @meddle
Node.monitor(:njichev@meddlandr, true)
#=> true

# Kill @njichev!
flush()
#=> {:nodedown, :njichev@meddlandr}
Курс по Elixir 2023, ФМИ

Извикване на отдалечени функции

  • Използва се Erlang модула :rpc (Remote procedure call).
  • Отдолу продължава да работи с процеси.
Курс по Elixir 2023, ФМИ

Извикване на отдалечени функции

  • :rpc.call/4 - Извиква отдалечена функция и връща резултат:
# @meddle

:rpc.call(:slavi@meddlandr, DB, :tell_us_secret, [])
#=> Познавам всички и всеки!
#=> :ok
Курс по Elixir 2023, ФМИ

Извикване на отдалечени функции

  • :rpc.cast/4 - Извиква отдалечена функция асинхронно:
# @meddle

:rpc.cast(:ivan@meddlandr, Enum, :map, [[1, 2, 3], fn x -> Process.sleep(10_000); IO.inspect(self()); x * x end])
#=> true
#=> #PID<0.144.0>
#=> #PID<0.144.0>
#=> #PID<0.144.0>

# @ivan, докато се принтира:
Process.alive?(pid("0.144.0"))
#=> true
Курс по Elixir 2023, ФМИ

Извикване на отдалечени функции

  • Функциите async_call/4 и yield/1:
# @ivan
pid = :rpc.async_call(:meddle@meddland,
                      Enum,
                      :sort,
                      [(1..2_000_000) |> Enum.shuffle])
#=> #PID<0.115.0>

# Това няма да забие текущия процес докато резултата е готов.

:rpc.yield(pid)
# Ще чака за резултат, или ако е готов ще го върне
Курс по Elixir 2023, ФМИ

Извикване на отдалечени функции

  • Функцията nb_yield/4 (non blocking yield).
# @meddle
pid = :rpc.async_call(:slavi@meddland, Process, :sleep, [50_000])
#=> #PID<0.108.0>

# По подразбиране timeout-ът е нула.
# Пробва и ако няма резултат - :timeout

:rpc.nb_yield(pid)
#=> :timeout
:rpc.nb_yield(pid, 1_000)
#=> :timeout

:rpc.nb_yield(pid, 50_000)
#=> {:value, :ok}
Курс по Elixir 2023, ФМИ

Извикване на отдалечени функции

  • Функциите multicall/4 и eval_everywhere
# @slavi
Node.list()
#=> [:meddle@meddland, :ivan@meddland]

:rpc.multicall([Node.self() | Node.list()], Node, :self, [])
#=> {[:slavi@meddland, :meddle@meddland, :ivan@meddland], []}
Курс по Elixir 2023, ФМИ

EPMD

Image-Absolute

Курс по Elixir 2023, ФМИ

EPMD

  • Когато стартираме node с име или пък му дадем име впоследствие, той ще се свърже към програма, наречена EPMD (Erlang Port Mapper Daemon).
  • Такава програма върви на всеки компютър на който има поне един 'жив' Erlang или Elixir node.
Курс по Elixir 2023, ФМИ

EPMD

  • EPMD е нещо като сървър за имена, който позволява регистриране, комуникация и връзка между node-ове.
  • EPMD map-ва имена на node-ове към машинни адреси.
  • Програмата пази само name частта от name@host, защото си знае хоста.
Курс по Elixir 2023, ФМИ

Кога се стартира EPMD?

  • Ако няма вървящ EPMD и стартираме node с име, автоматично се стартира.
  • Портът му по подразбиране е 4369, но може да се конфигурира друг.
  • Не е добра идея да ползваме друг порт, защото Ericsson са го регистрирали официално за EPMD и би трябвало да е свободен.
Курс по Elixir 2023, ФМИ

EPMD и имена

iex --sname andi --erl \
  "-kernel inet_dist_listen_min 54300 inet_dist_listen_max 54400"
Курс по Elixir 2023, ФМИ

EPMD и имена

# @meddle
:net_adm.names()
#=> {:ok, [{'meddle', 38657}, {'ivan', 46597}, {'slavi', 33253}, {'andi', 41137}]}
Курс по Elixir 2023, ФМИ

PID

Image-Absolute

Курс по Elixir 2023, ФМИ

PIDs

# @ivan

Node.spawn(:meddle@meddlandr, fn -> send(pid(0, 110, 0), "Hello from ivan!") end)
#=> #PID<11629.200.0>

# @meddle
flush()
#=> "Hello from ivan!"
#=> :ok
Курс по Elixir 2023, ФМИ

PIDs

  • Това, което Node.spawn/2 връща е pid, но изглежда малко странно.
  • Свикнали сме първото число да е 0, а тук не е.
  • Това е така, защото първото число на pid-а е свързано с node-а, на който процеса му се изпълнява.
Курс по Elixir 2023, ФМИ

PIDs

  • Ако процесът върви на текущия node, то винаги е 0.
  • Ако обаче процесът върви на друг node, числото уникално ще идентифицира този друг node.
  • Тази стойност за един и същи node ще е различна на различни node-ове, свързани с него.
Курс по Elixir 2023, ФМИ

Типа данни PID

  • Първото число на pid показва на кой node върви процеса.
  • Второто е брояч, а третото допълнение към този брояч.
  • Когато минем максималния брой процеси за дадения node, третото число се увеличава с едно.
Курс по Elixir 2023, ФМИ

External Term Format

binary_pid = :erlang.term_to_binary(self())
#=> <<131, 88, 100, 0, 16, 109, 101, 100, 100, 108, 101, 64, 109, 101, 100, 100,
#=>  108, 97, 110, 100, 114, 0, 0, 0, 110, 0, 0, 0, 0, 100, 56, 62, 180>>
Курс по Elixir 2023, ФМИ

External Term Format

  • Винаги започват с 131
  • Вторият байт е tag (в 1 байт) - за pid-ове : 88 (103 за по-стар формат)
<<131, tag::binary-size(1), rest::binary >> = binary_pid
#=> <<131, 88, 100, 0, 16, 109, 101, 100, 100, 108, 101, 64, 109, 101, 100, 100,
#=>  108, 97, 110, 100, 114, 0, 0, 0, 110, 0, 0, 0, 0, 100, 56, 62, 180>>
Курс по Elixir 2023, ФМИ

External Term Format

  • За атомите, след tag байта следва закодирана стойността на атома.
  • След името на node-a имаме 4 * 3 байта, които ще разгледаме след малко.
name_size = byte_size(rest) - (4*3)
#=> 19

<<131, tag::binary-size(1), binary_atom::binary-size(name_size), rest::binary >> = binary_pid
#=> <<131, 88, 100, 0, 16, 109, 101, 100, 100, 108, 101, 64, 109, 101, 100, 100,
#=>  108, 97, 110, 100, 114, 0, 0, 0, 110, 0, 0, 0, 0, 100, 56, 62, 180>>
Курс по Elixir 2023, ФМИ

External Term Format

  • Името е атом, но във вложени term-ове нямаме 131 за начало.
  • Атомите се кодират по няколко начина, ATOM_EXT, което е deprecated се ползва все още в pid-ове.
  • Тагът на ATOM_EXT e 100, след това имаме 2 байта дължина на името и след това името:
<<100, atom_len::integer-size(16), name::binary-size(atom_len)>> = binary_atom
#=> <<100, 0, 16, 109, 101, 100, 100, 108, 101, 64, 109, 101, 100, 100, 108, 97,
#=>  110, 100, 114>>

name
#=> "meddle@meddlandr"
Курс по Elixir 2023, ФМИ

External Term Format

  • Последните 12 байта са 3 integer-a
  • id-то е номерът на pid-a (2-рото му число)
  • serial e третото число на pid-a
  • creation е дата в unix int, кога е бил създаден node-a.
<<
  131,
  tag::binary-size(1),
  binary_atom::binary-size(name_size),
  id::integer-size(32),
  serial::integer-size(32),
  creation::integer-size(32)
>> = binary_pid
Курс по Elixir 2023, ФМИ

External Term Format

  • Всеки term може да се кодира и си има формат.
  • Това включва и функции, така могат да се пращат на други node-ове.
  • Или да се съхраняват да речем в база данни за изпълнение по-късно.
Курс по Elixir 2023, ФМИ

Свързване с вървящ node

elixir --pipe-to pipe logs --name a@127.0.0.1 -S mix run --no-halt
iex --name b@127.0.0.1 --remsh a@127.0.0.1 --hidden
Курс по Elixir 2023, ФМИ

Дистрибутирани програми - проблемите

Курс по Elixir 2023, ФМИ

Image-Absolute

Курс по Elixir 2023, ФМИ

Дистрибутирани програми - проблемите

  • На мрежата не винаги може да се разчита. Node-ове могат да изчезнат.
  • Мрежата може да бъде бавна от време на време. Резултатите от извиквания могат да се забавят.
  • Bandwidth. Малки и прости съобщения!
  • Security. Това трябва да си го постигнем сами!
Курс по Elixir 2023, ФМИ

Дистрибутирани програми - проблемите

  • Топологията на мрежата не е константа - имена и локации - трябва да внимаваме.
  • Рядко ние имаме пълен контрол над физическите машини в мрежата.
  • Транспортът е скъп. Малки, прости съобщения!
  • Мрежата няма определен формат. Трябва ние да определим формат и протокол.
Курс по Elixir 2023, ФМИ

CAP

Image-Absolute

Курс по Elixir 2023, ФМИ

CAP

Image-Absolute

Курс по Elixir 2023, ФМИ

CAP

Image-Absolute

Курс по Elixir 2023, ФМИ

Let there be Еternity : Next time we continue...

Image-Absolute

Курс по Elixir 2023, ФМИ