Elixir 1.0 setzt auf die virtuelle Maschine von Erlang, bietet aber dank seiner Syntax einen einfachen Einstieg in die funktionale Programmierung. Mit Hilfe dieser dynamischen Mischung entwirft der Entwickler skalierbare und fehlertolerante Anwendungen im Serverumfeld.
Was Elixir in der Praxis kann, zeigt dieser Artikel, indem er mit Elixir [1] das verteilte Serversystem aus Abbildung 1 ausrollt. Darin läuft ein Proxy auf dem ersten Serverknoten und leitet eingehende HTTP-Anfragen zwecks Leistungssteigerung jeweils an einen von zwei Serverknoten im lokalen Netzwerk weiter. Beide Knoten speichern die Inhalte redundant und liefern auf die Anfrage hin jeweils identische Dokumente aus, die als Antwort über den Proxy zurück an den Client wandern.
Freundlicher Parasit
Listing 1 zeigt die Installation der aktuellen Version von Elixir (Stand: 1.0.4-1) unter Ubuntu 14.04. Zeile 1 holt zunächst über »wget« ein Debian-Paket ab, das auf ein externes Paket-Repository verweist, das die aktuellen Versionen von Elixir und Erlang [2] vorhält. Der Paketmanager »dpkg« verfrachtet die Liste in Zeile 2 an den richtigen Ort im Dateisystem, in der nächsten Zeile liest »apt-get update« die Liste ein. Abschließend installiert der Entwickler Elixir und das Paket »erlang-dev« auf dem Rechner.
Listing 1
Elixir unter Ubuntu 14.04 installieren
01 wget http://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb 02 sudo dpkg -i erlang-solutions_1.0_all.deb 03 sudo apt-get update 04 sudo apt-get install elixir erlang-dev
Der Befehl »iex« startet nach erfolgreicher Installation eine interaktive Session in der Shell. Der Ausdruck
:crypto.md5("Verwende crypto aus Erlang OTP")
testet, ob Elixir mit Erlang kompatibel ist, indem er die Funktion »md5()« aus dem Erlang-Modul Crypto aufruft. In der Shell sollte »iex« die von »md5()« zurückgegebene Zeichenkette anzeigen, und zwar als Folge von Bytes, die spitze Klammern einzäunen (Abbildung 2).
Neben Bytefolgen bietet Elixir Tupel, Listen und Structs, um strukturierte Daten zu speichern. Die Liste
[{:app, :httpd}, {:version, "0.0.1"}]
speichert in ihrem Kopf das Tupel »{:app, :httpd}« , das seinerseits die beiden Literale »:app« und »:httpd« enthält. Die mit »:« eingeleiteten Literale nennt Elixir Atome. Sie kommen in der Sprache großflächig zum Einsatz.
Um Datenstrukturen zu entwirren, bringt Elixir Muster mit und tut es dabei Haskell [3] gleich. So ein Muster lässt sich beispielsweise links vom Gleichheitszeichen in dem Ausdruck
[{a, b}|_] = [mein: :haus, dein: :haus]
bewundern. Bevor Elixir es mit der Liste rechts abgleicht, überführt es diese zunächst in ihre Normalform »[{:mein, :haus}, {:dein :haus}]« . Beim Musterabgleich passt das Tupel im Kopf der Liste auf der linken Seite auf das entsprechende Tupel im Kopf der Liste auf der rechten Seite. Beide Tupel nehmen jeweils zwei Elemente auf. Konkret sind nach dem Abgleich die Variablen »a« und »:mein« sowie »b« und »:haus« . Der »_« -Operator rechts neben dem Pipe-Symbol im Muster passt zu jedem beliebigen Ausdruck, im Beispiel auf »:dein :haus« . Abbildung 2 zeigt noch einmal die Werte aus »a« und »b« nach dem Musterabgleich.
Alles im Griff
Das Buildtool »mix« unterstützt seine Benutzer dabei, Elixir-Projekte umzusetzen. Per Befehl über die Kommandozeile erstellt, kompiliert, startet und testet es Projekte. Ebenso wie »iex« landet »mix« auf dem Rechner, wenn der Entwickler Elixir installiert. Der Befehl »mix new httpd« erzeugt das Anwendungsgerüst für den geplanten Webserver (Listing 2).
Listing 2
Das Anwendungsgerüst für den Webserver
01 httpd 02 |- config 03 |- config.exs 04 |- lib 05 |- httpd.ex 06 |- mix.exs 07 |- README.md 08 |- test 09 |- httpd_test.exs 10 |- test_helper.exs
Die Datei »mix.exs« speichert die Konfiguration des Projekts (Listing 3). Zeile 1 deklariert mit dem Schlüsselwort »defmodule« ein Elixir-Modul »Httpd.Mixfile« , dann mündet der Code in einen »do end« -Block, der bis Zeile 15 reicht.
Listing 3
httpd/mix.exs (Projektkonfiguration)
01 defmodule Httpd.Mixfile do
02 use Mix.Project
03
04 def project do
05 [app: :httpd,
06 version: "0.0.1",
07 elixir: "~> 1.0",
08 deps: deps]
09 end
10
11 defp deps do
12 [{:cowboy, "~> 1.0.0"},
13 {:plug, "~> 0.13"}]
14 end
15 end
Der Block bindet in Zeile 2 über »use« das Makro »Mix.Project« ein. Öffentliche Funktionen wie »project()« leitet das Schlüsselwort »def« ein, bei privaten wie »deps()« kommt »defp« zum Zuge. Den Schlüsselwörtern folgen der Funktionsname sowie eine optionale Parameterliste. Der nachfolgende »do end« -Block bestimmt jeweils die Rückgabewerte der Funktionen.
Der Aufruf der Funktion »project()« (Zeilen 4 bis 9) gibt die Kerndaten des Projekts in Form einer Liste zurück, während »deps()« ab Zeile 11 die Abhängigkeiten des Projekts auflistet. Zu ihnen gehören das in Erlang verfasste Webserver-Framework Cowboy [4] (ab Version 1.0.0) und die Middleware Plug [5] (ab Version 0.13).
Private Funktionen lassen sich nur innerhalb des Moduls aufrufen. Gibt der Programmierer in die interaktive Shell »iex« den Aufruf »Http.Mixfile.deps()« ein, erhält er eine Fehlermeldung (»UndefinedFunctionError« ) zurück.
Code-Veredelung
Listing 4 zeigt den Code der Datei »lib/httpd.ex« . Darin antwortet das Modul Httpd auf HTTP-Anfragen und spannt dabei das Elixir-Modul Plug ein. Das bearbeitet HTTP-Anfragen in einer Pipeline, ähnlich wie das Node.js-Middleware-Framework Connect [6].
Listing 4
httpd/lib/httpd.ex (Httpd-Modul)
01 defmodule Httpd do 02 use Plug.Router 03 use Plug.Builder 04 05 plug Plug.Static, at: "/", from: "/home/pa" 06 plug :match 07 plug :dispatch 08 09 get "/info" do 10 send_resp(conn, 200, inspect(conn)) 11 end 12 13 match _ do 14 send_resp(conn, 404, "Not found!") 15 end 16 end
Das Plug-Framework übernimmt zunächst ein Anfrage-Objekt von einem Webserver und reicht es an eine Reihe von Filterfunktionen weiter, an die so genannten Plugs. Diese werten das Objekt aus, modifizieren es und delegieren es an das nachfolgende Plug. Alternativ veranlassen die Plugs das Framework über den Aufruf der Funktion »send_resp()« (Zeilen 10 und 14), die Pipeline zu beenden. Aus den Aufrufparametern der Funktion konstruieren sie dann eine HTTP-Antwort und schicken diese an den Webserver zurück (Abbildung 3).
Zunächst bindet Listing 4 aber die Makros »Plug.Router« und »Plug.Builder« ein. Die Funktion »plug()« (Zeilen 5 bis 7) registriert die Plugs und hält sie in einem Fifo (First in, first out) vor. Wie weiter oben beschrieben, arbeitet das Programm den Fifo ab, sobald ein Anfrage-Objekt eintrifft.
Das Plug »Static« in Zeile 5 bildet eine URL auf das Verzeichnissystem des Servers ab. Dank der »at:« -Angabe liefert die URL »http://127.0.0.3/info.html« die Datei »/home/pa/info.html« an den Client zurück. Passt auf die URL keine Datei, kümmert sich das folgende Plug um die HTTP-Anfrage. Dabei arbeiten »:match« und »:dispatch« Hand in Hand. Während »:match« per Musterabgleich nach passenden HTTP-Anfragen sucht und Treffer an »:dispatch« weiterleitet, kümmert sich das zweite um die HTTP-Antwort.
Die Codeblöcke der Zeilen 9 bis 11 sowie 13 bis 15 zeigen dabei keinen regulären Elixir-Code. Vielmehr handelt es sich um Code-Erweiterungen der Plug-Erfinder [7]. Das darin auftretende Makro »Plug-Router« wandelt diese Codeblöcke beim Erstellen des Syntaxbaums in gültige Baumbestandteile um. Theoretisch ließe sich der Codeblock auch als Elixir-Ausdruck umsetzen:
{method: "get", url: "/info", _} -> Usend_resp(conn, 200, inspect(conn))
Listing 4 geht jedoch einen anderen Weg. Die Zeilen 9 bis 11 geben für die URL »/info« dank der Funktion »inspect()« eine formatierte Version des Anfrage-Objekts im Körper der HTTP-Antwort aus. Als Antwortcode schreibt die Funktion »200« in den Kopf der Antwort. Für alle noch nicht beantworteten Anfragen gibt der Ausdruck ab Zeile 13 die bekannte »404« -Meldung über den Webserver an den Client zurück.
Vor dem Start der Anwendung wechselt der Benutzer mit »cd httpd« ins Projektverzeichnis und kompiliert die Anwendung mit Hilfe von »mix« . Der Befehl »mix deps.get« lädt zunächst die Quellcodes der benötigten Module aus Hex.pm [8], dem gemeinsamen Paketarchiv von Erlang und Elixir, dann verwandelt ein »mix deps.compile« die Anwendung in eine Schar von Beam-Dateien. Sie landen im Projektverzeichnis unterhalb des Ordners »_build« .
Der Befehl »sudo iex -S mix« läutet erneut eine interaktive Sitzung ein. Der Funktionsaufruf
Plug.Adapters.Cowboy.http Httpd, [], ip: {127, 0, 0, 3}, port: 80
startet eine Instanz des Webservers aus dem Cowboy-Framework. Der ist dann über die IP-Adresse »127.0.0.3« und Port 80 erreichbar und lädt das Modul aus Listing 4, das auf HTTP-Anfragen wartet. Abbildung 4 zeigt das formatierte Anfrage-Objekt als Antwort auf die Anfrage-URL »http://127.0.0.3/info« in der Browseransicht von Firefox.
Großer Bruder
Listing 5 zeigt die Konfiguration des Proxys (Abbildung 1). Sie steckt in der Datei »mix.exs« im Projektverzeichnis »htdist« . Im Gegensatz zum HTTP-Server verzichtet der Proxy auf Plug und verwendet das Modul Httpoison (Zeile 18), um HTTP-Anfragen an die Webserver im lokalen Netzwerk weiterzureichen. Zusätzlich verfügt die Konfiguration ab Zeile 11 über einen Application-Callback, der »application()« beim Start der Anwendung ausführt. Die Angabe »mod: { Htdist.Supervisor, []}« (Zeile 12) im Rückgabewert der Funktion ruft ihrerseits »start()« (Listing 6, Zeile 5) aus dem Modul Htdist.Supervisor auf.
Listing 5
htdist/mix.exs (Proxy-Konfiguration)
01 defmodule Htdist.Mixfile do
02 use Mix.Project
03
04 def project do
05 [app: :htdist,
06 version: "0.0.1",
07 elixir: "~> 1.0",
08 deps: deps]
09 end
10
11 def application do
12 [ mod: { Htdist.Supervisor, []},
13 applications: [:cowboy, :httpoison]]
14 end
15
16 defp deps do
17 [{:cowboy, "~> 1.0.0"},
18 {:httpoison, "~> 0.7"}]
19 end
20 end
Fehlertoleranz und Skalierbarkeit erreicht Elixir durch sein Prozessmodell. Die leichtgewichtigen Elixir-Threads übernehmen die Arbeit. Tritt im Thread ein Fehler auf, beendet Elixir den Thread, nicht aber die Anwendung. Die Fehlermeldung behandelt gewöhnlich ein Supervisor, der im Prozess der Anwendung läuft. Er ist auch für den Neustart des Thread verantwortlich. Listing 6 zeigt den Code des Supervisors aus dem Modul Htdist.Supervisor, den die erwähnte Zeile 12 aus Listing 5 beim Systemstart aktiviert.
Listing 6
htdist/lib/httpd.supervisor.ex (Supervisor)
01 defmodule Htdist.Supervisor do 02 use Application 03 use Supervisor 04 05 def start(_type, _args) do 06 Supervisor.start_link(__MODULE__, :ok) 07 end 08 09 def init(:ok) do 10 supervise([worker(Htdist, [])], strategy: :one_for_one) 11 end 12 13 end
Das Makro »Application« (Zeile 2) erhebt das Modul zu einer Anwendung, »Supervisor« (Zeile 3) bindet zusätzliche Funktionalität ein. Die Funktion »start_link()« in Zeile 6 erweckt einen Supervisor zum Leben. Dabei übergibt der Aufruf im ersten Parameter den Namen des Supervisors, der in der Variablen »__MODULE__« steckt.
Einmal aktiviert, ruft der Supervisor seinerseits die Funktion »init()« (Zeile 9) auf. Aus ihrem Funktionskörper heraus startet »supervise()« in Zeile 10 den Proxy aus Listing 7, der in einem eigenen Thread läuft und den der Supervisor überwacht. Bricht der Proxy mit einem Fehler ab, würde der Supervisor ihn gemäß der Strategie “One for one” erneut aufs Gleis setzen.
Willkürliche Kontrolle
Lädt Elixir ein Modul, ruft der Supervisor standardmäßig die Funktion »start_link()« in Listing 7 (Zeile 2) auf. In der nächsten Zeile erzeugt die Funktion »compile()« einen Router, indem sie das Modul Cowboy_router aufruft. Sie speichert den Router in der Variablen »dispatch« . Hier beantwortet er, analog zu den Plugs »:match« und »:dispatch« aus Listing 4, HTTP-Anfragen.
Listing 7
htdist/lib/htdist.ex (Proxy-Modul)
01 defmodule Htdist do
02 def start_link() do
03 dispatch = :cowboy_router.compile([{:_, [{"/[...]", Distr, []}]}])
04 {:ok, _} = :cowboy.start_http(:http, 100, [{:ip, {127, 0, 0, 2}}, {:port, 80}], [{ :env, [{:dispatch, dispatch}]}])
05 end
06 end
Seine Konfiguration holt sich der Router aus der Liste am Ende von Zeile 3. Mit ihr wertet er Anfragen für verschiedene IP-Adressen und Portnummern aus. Das Muster »:_« im ersten Tupel passt auf alle denkbaren IP-Adressen und Portnummern, das Muster »/[…]« auf sämtliche URLs. Schließlich ruft Zeile 3 das Modul Dist auf. In Zeile 4 von Listing 7 bringt »start_http()« den Webserver auf Trab. Über die anfangs zugewiesene Variable »dispatch« landet der Router nebst IP-Adresse und Portnummer in der Parameterliste.
Das Modul Distr, dessen vollständigen Code Listing 8 zeigt, wird aktiv, sobald die Zeile 3 in Listing 7 es aufruft. Es erstellt die HTTP-Antwort nach dem Vorbild eines Load Balancer. Die Funktionen »init()« und »terminate()« des Moduls sind rein obligatorisch, denn tatsächlich kümmert sich die Rückruffunktion »handle()« um HTTP-Anfragen (Zeilen 10 bis 18). Bei jeder eintreffenden HTTP-Anfrage legt sie das Anfrage-Objekt in der Variablen »req« und den Status der Anfragebearbeitung in »state« ab.
Listing 8
htdist/lib/distr.ex (Load Balancer)
01 defmodule Distr do
02 def init(_type, req, []) do
03 {:ok, req, :no_state}
04 end
05
06 def terminate(reason, request, state) do
07 :ok
08 end
09
10 def handle(req, state) do
11 {url, _} = :cowboy_req.url(req)
12 {code, body, headers} = case HTTPoison.request (:get, next_url(url)) do
13 {:ok, resp} -> {resp.status_code, resp.body, resp.headers}
14 {:error, _} -> {500, "<h1>500 - Internal Server Error</h1>", []}
15 end
16 {:ok, reply} = :cowboy_req.reply(code, headers, body, req)
17 {:ok, reply, state}
18 end
19
20 def next_url(url) do
21 purl = URI.parse(url)
22 "#{purl.scheme}://#{next_host()}#{purl.path}"
23 end
24
25 def next_host() do
26 :random.seed(:os.timestamp)
27 [next|_] = Enum.shuffle(["127.0.0.3", "127.0.0.4"])
28 next
29 end
30 end
Zeile 11 kopiert die angeforderte URL im Anfrage-Objekt und speichert den Wert in der Variablen »url« . Zeile 12 reicht die HTTP-Anfrage an einen lokalen HTTP-Server weiter. Dazu ruft sie »request()« aus dem Modul Httpoison auf, wobei sich der Proxy auf Get-Anfragen (»:get« ) beschränkt.
Die anschließend aufgerufene Funktion »next_url()« befindet sich in den Zeilen 20 bis 23. Sie übersetzt die URL der HTTP-Anfrage in eine Adresse im lokalen Netzwerk. In Zeile 21 zerlegt die Funktion »parse()« aus dem Standardmodul URI die eingegangene URL in ihre Adressenbestandteile. Die nachfolgende Zeile reproduziert die URL anhand der Adressenbestandteile wieder, allerdings ohne den IP-Anteil.
Diesen wiederum ersetzt die Funktion »next_host()« (Zeilen 25 bis 29) durch die IP-Adresse eines HTTP-Servers im lokalen Netzwerk, den sie per Zufall bestimmt. Den Zufall erzeugt ein Pseudozufallszahlen-Generator, den »seed()« in Zeile 26 initialisiert. In der darauf folgenden Zeile nimmt die Variable »next« den Kopf der mit »shuffle()« zufällig umsortierten Liste aus Zieladressen auf, Zeile 28 gibt im Anschluss das Ergebnis zurück. Das Ergebnis werten dann wiederum die Zeilen 13 und 14 aus. Diese unterziehen die Antwort der HTTP-Anfrage einer Case-Fallunterscheidung, die zwei mögliche Ergebnisse zeitigt.
Im Erfolgsfall passt das Muster der ersten Fallunterscheidung. Als Reaktion kopiert daraufhin die Zeile 13 den Statuscode, den Körper und alle übrigen Meta-Angaben aus der HTTP-Antwort in die drei Variablen »code« , »body« und »headers« (Zeile 12). Im Fehlerfall belegt hingegen der Code in Zeile 14 die drei Variablen mit konstanten Werten, wobei einmal mehr der »_« -Operator eine zentrale Rolle spielt.
Aus den drei Variablen erstellt schließlich der Aufruf der Funktion »reply()« in Zeile 16 eine HTTP-Antwort und speichert den Wert in der gleichnamigen Variablen »reply« . Die nachfolgende Zeile gibt »reply()« zusammen mit dem Literal »:ok« und dem Status (»state« ) als Antwort an den Webserver zurück.
Der Entwickler startet die Proxyanwendung, indem er wieder eine Shell aufruft und zunächst die Abhängigkeiten mittels »mix deps.get« lädt. Er kompiliert sie dann mit dem Kommando »mix deps.compile« , um daraufhin den Befehl »sudo iex -S mix« abzusetzen. Um die Anwendung aus Abbildung 1 zu vervollständigen, initialisiert er über eine weitere Shell einen zusätzlichen HTTP-Server mit der neuen IP-Adresse 127.0.0.4: Dazu wechselt er in das Verzeichnis »httpd« und gibt »sudo iex -S mix« gefolgt von
Plug.Adapters.Cowboy.http Httpd, [], ip: {127, 0, 0, 4}, port: 80
ein. Abbildung 5 zeigt rechts den Load Balancer im Praxiseinsatz unter Ubuntu 14.04. Auf Anfrage von »http://127.0.0.2/info« antwortet die Anwendung unter Firefox mit der aus Abbildung 4 bekannten Replik. Jedoch variiert der Wert des Feldes »host« zufällig zwischen den IP-Adressen der beiden Webserver.
Fazit
Mit Elixir bauen Entwickler verteilte, fehlertolerante und skalierbare Anwendungen. Dank der Kompatibilität lässt sich das Ökosystem von Erlang auf komfortable Weise mitnutzen. Zugleich verzichtet Elixir auf das akademische Ideengestrüpp, das den Zugang zu funktionalen Sprachen meist überwuchert. Somit lassen sich damit schnell wartbare und fehlerresistente Anwendungen schreiben. Darüber hinaus erweitern Makros den Sprachumfang von Elixir und vereinfachen den Programmcode zusätzlich. So ungefähr könnte sich das Programmieren von morgen anfühlen.
Infos
- Elixir: http://elixir-lang.org
- Erlang: http://www.erlang.org
- Haskell: http://learnyouahaskell.com/syntax-in-functions#pattern-matching
- Cowboy: http://ninenines.eu
- Plug: https://github.com/elixir-lang/plug
- Connect: https://github.com/senchalabs/connect#readme
- Datenstrukturen: http://elixir-lang.org/getting-started/meta/quote-and-unquote.html
- Hex.pm: http://hex.pm











