1 Elixir – Ein Werkzeug für hochgradig verteilte, fehlertertolerante Systeme Herzlich willkommen – DevDay 2016 2 Übersicht Geschaffen von José Valim Erlang als erprobte Basis für höchstverfügbare Systeme Funktionale Elemente von Elixir (Pattern matching, looping ohne loop-Konstrukte) Protokolle und Polymorphie in der funktionalen Welt Behaviours (Generic Server) Fehlertolerante Systeme mit Supervision Trees 3 Motivation 4 Erlang als Basis Wurzeln in der Telekommunikation Joe Armstrong (Ericsson, 1987) Nebenläufigkeit und Fehlertoleranz Transparente Verteilung über heterogener Hardware Hochverfügbar (Nine Nines) Lebendig (Erlang/OTP 18.2 16. Dez 2015) 5 Wer setzt Erlang ein? Amazon (SimpleDB, Amazon Elastic Compute Cloud (EC2)) Yahoo! (Delicious, 5 Mio User, 150 Mio Bookmarks) Facebook (Chat backend) WhatsApp (Messaging Server) T-Mobile (SMS und Authentisierung) Ericsson (Telekom) 6 Erlang-Applikationen Ejabberd (Instant Messaging) CouchDB, Riak, Mnesia RabbitMQ (AMQP-Implementierung) 7 Stärken von Erlang Leichtgewichtige Prozesse (Millionen) Kein Shared Memory Funktional (keine Seiteneffekte) «Let it crash»-Philosophie (Supervision) BEAM VM OTP-Plattform 8 Warum Elixir? Erlang als erprobte Basis Übersichtlicherer, kompakterer Code Eleganz («syntaktischer Zucker») Meta-Programmierung Mix Build-Tool 9 Joe Armstrong liebt Elixir! http://joearms.github.io/2013/05/31/a-week-with-elixir.html «This has been my first week with Elixir, and I’m pretty excited.» Elixir has a non-scary syntax and combines the good features of Ruby and Erlang. It’s not Erlang and it’s not Ruby and it has ideas of its own. It’s a new language, but books are being written as the language is being developed. The first Erlang book came 7 years after Erlang was invented, and the first popular book 14 years later. 21 years is too long to wait for a decent book. Dave [Thomas] loves Elixir, I think it’s pretty cool, I think we’re going to have fun together. Erlang powers things like WhatsApp and crucial parts of half the world’s mobile phone networks. It’s going to be great fun to see what will happen when the technology becomes less scary and the next wave of enthusiasts joins the party.“ 10 Elixir im Einsatz Web-Framework Phoenix Millionen aktiver Connections auf einem Server Antwortzeiten im Mikrosekundenbereich möglich IoT-Startups bauen auf Elixir 11 Eleganz 12 Summierungsserver in Erlang -module(sum_server). -behaviour(gen_server). -export([ start/0, sum/1, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). start() -> gen_server:start(?MODULE, [], []). sum(Server, A, B) -> gen_server_call(Server, {sum, A, B}). init(_) -> {ok. Undefined}. handle_call({sum, A, B}, _From, State) -> {reply, A + B, State}). handle_cast(_msg, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. 13 Summierungsserver in Elixir defmodule SumServer do use GenServer def start do GenServer.start(__MODULE__, nil) end def sum(server, a, b) do GenServer.call(server, {:sum, a, b}) end def handle_call({:sum, a, b}, _from, state) do {:reply, a + b, state} end end 14 Sprache 15 Funktionale Elemente von Elixir Nicht-mutierbare Daten Seiteneffektfreie Funktionen Pattern Matching Tail-Rekursion Chaining von Funktionen Polymorphie mit Protokollen 16 Nicht-mutierbare Daten Kein OO-Mutieren a la hashMap.add(key, value) sondern newHash = HashMap.add(oldHash, key, value) Intern effizient durch Daten-Reuse 17 Seiteneffektfreie Funktionen Funktionen liefern wie mathematische Funktionen bei jedem Aufruf das gleiche Ergebnis. 18 Pattern Matching iex(1)> {name, gehalt} = {"Max", 5000} Matchoperator = bindet name und gehalt: iex(1)> name "Max" iex(1)> gehalt 5000 19 Funktionsdefinitionen mittels Patterns defmodule Geom do def area({:square, r}), do: r * r def area({:rectangle, a, b}), do: a * b end iex> c("geom.ex") [Geom] Iex> Geom.area({:rectangle, 45, 5}) 225 Iex> Geom.area({:square, 45}) 2025 Iex> Geom.area({:square, 45, 5}) ** (FunctionClauseError) no function clause matching in Geom.area/1 geom.ex:2: Geom.area({:square, 45, 5}) 20 Tail-Rekursion defmodule M do def fac(0), do: 1 def fac(n), do: n * fac(n - 1) end Letzter Aufruf in der Funktion: TailCall-Optimierung. So baut man Loops in Elixir! 21 Verkettung von Funktionen, Pipe-Operator – Beispiel von Joe Armstrong (Erlang) capitalize_atom(X) -> list_to_atom(binary_to_list(capitalize_binary(list_to_binary(atom_to_list(X))))). Elixir: capitalize_atom = fn(x) -> x |> to_char_list |> to_string |> String.capitalize |> to_char_list |> List.to_atom end Wesentlich lesbarer! 22 Datenstrukturen mit structs defmodule Fraction do defstruct nom: nil, denom: nil def new(n, d) do %Fraction{nom: n, denom: d} end end 23 Protokolle Definition: defprotocol String.Chars do def to_string(thing) end Verwendung: iex(1)> String.Chars.to_string(1) „1“ iex(2)> String.Chars.to_string(:myatom) „myatom“ iex(3)> String.Chars.to_string(Fraction.new(1, 2)) ** (Protocol.UndefinedError) protocol String.Chars not implemented 24 Implementation von Protokollen defimpl String.Chars, for: Fraction do def to_string(frac) do "#{frac.nom} / #{frac.denom}" end end iex> String.Chars.to_string(Fraction.new(1, 2)) "1 / 2" iex> one_half = Fraction.new(1, 2) iex> IO.puts(one_half) 1/2 :ok 25 Verteilung 26 Prozesse, Actor-Modell pid = spawn(fn) startet einen Prozess send(pid, message) schickt Nachricht Empfang: receive do Pattern_1 -> action_1 Pattern_2 -> action_2 end 27 Server-Prozesse Handgestrickt mit Tail-Rekursion Besser: Implementation von GenServer (OTP gen_server behaviour) defmodule SumServer do use GenServer def start do GenServer.start(__MODULE__, nil) end Interface-Funktion (start, sum) def sum(server, a, b) do GenServer.call(server, {:sum, a, b}) end def handle_call({:sum, a, b}, _from, state) do {:reply, a + b, state} end end Callback-Funktion (handle_call, handle_cast) 28 Nodes Nodes sind oberhalb von Prozessen angesiedelt und können auf verschiedenen Maschinen liegen Namen: iex --sname node1@localhost Verbindung: Node.connect(:node1@localhost) Kommunikation: caller = self Node.spawn( :node2@localhost, fn -> send(caller, {:response, „hallo“} end ) 29 Fehlertoleranz 30 Fehlertoleranz Elixir verfügt über try/catch/after-Konstrukt Wird selten benötigt, verträgt sich nicht mit Tail-Rekursion Typischerweise wird ein Prozess von einem Supervisor neu gestartet und verfügt dann wieder über einen «sauberen» Zustand 31 Trapping exits Prozesse können mit spawn_link bidirektional verknüpft werden Mittels Process.monitor kann ein Prozess unidirektional überwacht werden Der Eltern-Prozess kann Exits trappen und entsprechend reagieren Behaviour Supervisor startet überwachte Prozesse neu FractionServer defmodule FractionServer do use GenServer def start_link(nominator) do GenServer.start_link(FractionServer, nominator, name: :fraction_server) end def fraction(pid, denom) do GenServer.call(pid, {:fraction, denom}) end def init(_), do: {:ok, 1} def handle_call({:fraction, denom}, _, state) do {:reply, state / denom, state} end end 32 FractionServer Supervisor defmodule FractionSupervisor do use Supervisor def start_link do Supervisor.start_link(__MODULE__, nil, name: :fraction_server_supervisor) end def init(_) do processes = [worker(FractionServer, [1])] supervise(processes, strategy: :one_for_one) end end 33 Supervision in Aktion iex(3)> FractionSupervisor.start_link Starting fraction server {:ok, #PID<0.72.0>} iex(7)> pid = Process.whereis(:fraction_server) #PID<0.73.0> iex(8)> FractionServer.fraction(pid, 3) 0.3333333333333333 iex(9)> FractionServer.fraction(pid, 0) Starting fraction server iex(9)> pid = Process.whereis(:fraction_server) #PID<0.83.0> iex(10)> FractionServer.fraction(pid, 3) 0.3333333333333333 34 35 Beispiel Supervision Tree Root-Supervisor Supervisor A one_for_one one_for_all Supervisor C one_for_one Supervisor B Worker 1 Server 1 Server 2 Worker 2 Worker 3 simple_one_for_one