Aus Linux-Magazin 10/2017

Web-Praxisbeispiel mit dem Phoenix Framework 1.3

© German Totskyi, 123RF

Wer das Gefühl hat, mit Ruby on Rails performancemäßig aufs Abstellgleis geraten zu sein, und zugleich keine Scheu hat, sich zu einem anderen Programmierparadigma aufzuschwingen, findet im Phoenix Framework einen pfeilschnellen Web-Segler.

Wie alle erfolgreichen Open-Source-Projekte hinterfragen Teile der Community auch das mittlerweile 13 Jahre alte Webframework Ruby on Rails ([1], [2], [3]) regelmäßig. Meist geschieht dies in Form einer Neu-Interpretation per Ruby (etwa [4]). Das Framework Phoenix tut das viel radikaler und ersetzt sowohl die Programmiersprache als auch die Idee des objektorientierten Ansatzes.

Die Vorgeschichte ist schnell erzählt: Vor ein paar Jahren kämpfte der damalige Ruby-on-Rails-Entwickler Chris McCord (Abbildung 1) bei einem Rails-Projekt mit Performance-, Concurrency- und Skalierungsproblemen. Zufall oder nicht, kurz vorher traf ein anderer Rails-Entwickler, José Valim, bei einem Ruby-Projekt auf ähnliche Unzulänglichkeiten. Als Lösung ersann er die funktionale und auf Erlang/OTP (The Open Telecom Platform, Erlang-Standard-Middleware) aufsetzende Programmiersprache Elixir [5].

Abbildung 1: Der Phoenix-Framework-Erfinder Chris McCord.

Abbildung 1: Der Phoenix-Framework-Erfinder Chris McCord.

Chris McCord schaute sich Elixir an und war von den Möglichkeiten begeistert. Er baute mit Elixir eine Alternative zu Ruby on Rails und veröffentlichte im August 2015 die Version 1.0 von Phoenix Framework [6]. Auch wenn Phoenix auf den ersten Blick viel Ähnlichkeit mir Rails hat, so sind die beiden Frameworks sehr unterschiedlich.

In den letzten zwei Jahren ist die Phoenix-Community kräftig gewachsen, auch einige Ruby-on-Rails-Entwickler haben das Lager gewechselt. Phoenix schlägt so munter die Flügel, da ist zu fragen, was die entstandene Software wirklich taugt. Die gerade veröffentlichte Version 1.3 dient dabei als Grundlage.

Elixir und das dafür benötigte Erlang/OTP lassen sich problemlos auf allen populären Betriebssystemen installieren (Anleitungen unter [7]). Auf einem Debian Linux reicht »apt-get install elixir«. Danach kommt das Tool »mix«, welches das Elixir-Paket mitgebracht hat, das erste von vielen Malen zum Einsatz:

mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez

Der Befehl installiert die aktuelle Phoenix-Version.

Shopping-Tour

Webframeworks wie Ruby on Rails, das Python-basierte Django oder Phoenix leben von der in sich selbst logischen Dateistruktur und den Konventionen. Um ein Gefühl dafür zu bekommen, hilft es am meisten, ein kleines Beispielprojekt zu programmieren. Dieser Artikel tut dies in Form eines kleinen Webshops, der mit Produkten und Kategorien zu füllen ist. Den Grundstein legt ein mit dem Tool »mix« angelegtes Phoenix-Projekt:

mix phx.new shop
cd shop

Phoenix konfiguriert im Default den Datenbank-Wrapper Ecto [8] mit der PostgreSQL-Datenbank. Wer auf seinem System Maria DB oder MySQL nutzt, muss sein Projekt dagegen mit dem Befehl »mix phx.new shop –database mysql« initiieren. Wer ein Projekt ganz ohne Datenbankanbindung plant, kann die Installation von Ecto mit dem Schalter »–no-ecto« ganz unterbinden. Bei allen Datenbanken ist es nötig, einen Blick in die Konfigurationsdatei »config/dev.exs« zu werfen (Listing 1) und dort die Datenbank-Parameter anzupassen.

Listing 1

config/dev.exs

01 config :shop, Shop.Repo,
02   adapter: Ecto.Adapters.Postgres,
03   username: "postgres",
04   password: "postgres",
05   database: "shop_dev",
06   hostname: "localhost",
07   pool_size: 10

Vor dem ersten Start eines Phoenix-Servers muss der Entwickler die Datenbank mit dem Befehl

mix ecto.create

anlegen. Danach startet er den Phoenix-Server im Entwicklungsmodus:

mix phx.server

Der Server lässt sich jederzeit mit zweimal [Ctrl]+[C] beenden.

Untrügliches Zeichen im Log: Ein My schneller

Jetzt kann der Entwickler mit dem Webbrowser auf die URL »http://localhost:4000« zugreifen und bekommt per Default die View »ShopWeb.PageController.index« zu Gesicht. Ein Blick auf das parallel auf der eben gestarteten Konsole mitlaufende Log

[info] GET /
[debug] Processing with ShopWeb.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 266µs

zeigt bei der Processing-Zeit ein für viele Webentwickler ungewohntes Zeichen: µ. Der Phoenix-Server – selbst auf einem gewöhnlichen Laptop installiert – antwortet nämlich auf viele Abfragen in viel kürzerer Zeit als einer Millisekunde (0,001 Sekunden), weshalb er in sein Log gleich Mikrosekunden (0,000001 Sekunden) einträgt – ein Vorgeschmack auf die Schnelligkeit von Phoenix.

Mit der Version 1.3 stellt Phoenix auf ein neues Kontext-System um. Kontexte (»Store« in diesem Beispiel) sorgen für eine saubere Abgrenzung innerhalb der Codebasis und spiegeln sich in der Verzeichnisstruktur wider. Im Verzeichnis »lib/shop_web/« liegen alle für die Generierung der Webseiten relevanten Dateien des Beispielprojekts. Die Business-Logik für den Kontext »Store« nimmt in »lib/shop/store/« Platz. Keine Angst vor zu viel Komplexität: Die Generatoren kümmern sich um alles.

Für die Neuauflage des klassischen “Hello World!”-Beispiels ersetzt der Entwickler den Inhalt der Datei »/lib/shop_web/templates/page/index.html.eex« einfach durch »<h1>Hello World!</h1>«. Das HTML-Boilerplate findet sich in »lib/shop_web/templates/layout/app.html.eex«. Nach dem Speichern der Datei im Editor lädt sich im Entwicklungsmodus die Seite im Browser praktischerweise automatisch neu.

Produktkategorien automatisch anlegen

Für den neuen Beispiel-Onlineshop erzeugt der Entwickler zuerst per HTML-Generator eine Produktkategorie mit dem Namen »Category« und dem Attribut »name«:

mix phx.gen.html Store Category categories name:string

Wie bei Rails nehmen die Generatoren dem Programmierer fehleranfällige und langweilige Arbeit ab, so wie im Folgenden: Der HTML-Generator »phx.gen.html« erzeugt eine Ressource inklusive einem Restful-Webinterface zur Abfrage dieser Ressource. »phx.gen.html« ist das Pendant zum Scaffold Generator bei Ruby on Rails (»rails g scaffold«). Wer den Generator verwendet, dem fällt sofort auf, dass Phoenix sehr explizit arbeitet. Ein Rails-Generator würde sich aus dem Singular »Category« selbstständig den Plural zusammenbauen.

Die gerade angelegte Ressource ist allerdings noch nicht per Web erreichbar. Das ändert sich mit dem Einbauen der Route »resources “/categories”, CategoryController« in die Datei »lib/shop_web/router.ex« (Listing 2).

Listing 2

lib/shop_web/router.ex

01 scope "/", ShopWeb do
02   pipe_through :browser
03
04   get "/", PageController, :index
05   resources "/categories", CategoryController
06 end

Nach »mix ecto.migrate«, einer Datenbank-Migration, welche die Tabelle »categories« anlegt, ist auch die noch leere Liste aller verfügbaren Kategorien unter »http://localhost:4000/categories« abrufbar. Eine neue Kategorie lässt sich unter der URL »http://localhost:4000/categories/new« eintragen.

Richtig validieren

Per Default erstellt der Generator mit »validate_required([:name])« eine Required-Validierung für das Attribut »name«. Dies verhindert, dass jemand einen leeren Datensatz über das Web-GUI einträgt. Das Ziel ist aber, zusätzlich eine Mindestlänge von drei Zeichen und eine Maximallänge von 100 Zeichen zu validieren. Dafür erweitert der Entwickler die Funktion »changeset/2« in der Datei »lib/shop/store/category.ex« (Listing 3). »|>« ist syntaktischer Zucker, um in Elixir das Ergebnis einer Funktion als Eingabe für die nächste Funktion zu nutzen.

Listing 3

lib/shop/store/category.ex

01 def changeset(%Category{} = category, attrs) do
02   category
03   |> cast(attrs, [:name])
04   |> validate_required([:name])
05   |> validate_length(:name, min: 3, max: 100)
06 end

Der nun folgende Schritt legt eine Produkt-Ressource mit einem Namen an, einem Preis und der ID der Kategorie:

mix phx.gen.html Store Product products category_id:references:categories name:string price:decimal

Die Route »resources “/products”, ProductController« fügt der Entwickler manuell in der »lib/shop_web/router.ex«-Datei hinzu und startet die Datenbank-Migration mit »mix ecto.migrate«.

Jetzt lassen sich neue Produkte unter »http://localhost:4000/products/new« anlegen. Jedoch fehlt im Formular ein Drop-down-Feld für die Kategorie. Um es in das Formular einzubauen, bedarf es eines Tupels mit den jeweiligen IDs und Namen der Kategorien. Zum Generieren und zur Übergabe dieses Tupels an das Template muss der Programmierer die Funktion »new/2« in »lib/shop_web/controllers/product_controller.ex«, wie in Listing 4 zu sehen, verändern.

Listing 4

lib/shop_web/controllers/product_controller.ex

01 def new(conn, _params) do
02   changeset = Store.change_product(%Product{})
03   categories = Store.list_categories()
04                |> Enum.map(&{&1.name, &1.id})
05   render(conn, "new.html", changeset: changeset, categories: categories)
06 end

Rails-Entwickler würden das im Controller mit einer Instance-Variablen »@categories« übergeben. Elixir ist aber funktional und kennt keine Seiteneffekte. Die Funktion »render()« kann nur dann die Werte von »categories« nutzen, wenn sie als Parameter übergeben werden. Außerdem ist es nötig, das Formular in der Datei »lib/shop_web/templates/product/form.html.eex« um das Drop-down-Feld für die »category_id« zu ergänzen (Listing 5).

Listing 5

lib/shop_web/templates/product/form.html.eex

01 <div class="form-group">
02   <%= label f, :category_id, "Category", class: "control-label" %>
03   <%= select f, :category_id, @categories ,class: "form-control" %>
04 </div>

Damit zeigt »http://localhost:4000/products/new« schon alles korrekt an (Abbildung 2). Allerdings würde das Feld »category_id« im Controller beim Create noch nicht gespeichert werden. Phoenix verarbeitet nur die Attribute, welche die »changeset/2«-Funktion in »lib/shop/store/product.ex« castet. Listing 6 passt darum die Funktion entsprechend an.

Listing 6

lib/shop/store/product.ex (1)

01 def changeset(%Product{} = product, attrs) do
02   product
03   |> cast(attrs, [:name, :price, :category_id])
04   |> validate_required([:name, :price, :category_id])
05 end

Abbildung 2: Das Webformular zum Eintragen eines neuen Produkts funktioniert schon im Wesentlichen.

Abbildung 2: Das Webformular zum Eintragen eines neuen Produkts funktioniert schon im Wesentlichen.

Produkte aus der Datenbank

Die URL »http://localhost:4000/products« zeigt jetzt zwar eine Liste der Produkte an (Abbildung 3), aber es fehlen noch die Kategorie-ID oder besser die Kategorie-Namen des jeweiligen Produkts. Die reine »category_id« als Zahl anzuzeigen wäre einfach. Um aber den Namen zu ermitteln, fehlt die Definition einer Verbindung (Association) von »product« zu »category«. Die gelingt mit dem Hinzufügen von »belongs_to« im »schema« der Datei »lib/shop/store/product.ex« (Listing 7).

Listing 7

lib/shop/store/product.ex (2)

01 schema "products" do
02   field :name, :string
03   field :price, :decimal
04   belongs_to :category, Shop.Store.Category
05
06   timestamps()
07 end

Abbildung 3: Die URL <code>http://localhost:4000/products</code> zeigt jetzt eine Liste der Produkte an.

Abbildung 3: Die URL »http://localhost:4000/products« zeigt jetzt eine Liste der Produkte an.

Jetzt hat Ecto die 1:n-Verbindung auf der Produktseite, aber der Controller lädt die entsprechenden Daten noch nicht. Das ändert sich, sobald der Entwickler die »index/2«-Funktion in der »lib/shop_web/controllers/product_controller.ex« analog zu Listing 8 erweitert. »Shop.Repo.preload(:category)« lädt die entsprechende Kategorie ab sofort aus der Datenbank und fügt sie an den Produkte-Datensatz an.

Listing 8

lib/shop_web/controllers/product_controller.ex

01 def index(conn, _params) do
02   products = Store.list_products()
03              |> Shop.Repo.preload(:category)
04   render(conn, "index.html", products: products)
05 end

Als Letztes gilt es, in der Template-Datei »lib/shop_web/templates/product/index.html.eex« die Ausgabe für »product.category.name« entsprechend Listing 9 einzubauen. Mit Ausnahme der »product#show«-View, die noch nicht den Namen der Kategorie anzeigt, und der »product#edit«-View, die noch das Tupel mit allen Kategorien übergeben bekommen muss, ist der Mini-Webshop fertig. Die beiden noch fehlenden Teile kann sich jeder leicht aus den obigen Codebeispielen herleiten.

Listing 9

lib/shop_web/templates/product/index.html.eex

01 <table class="table">
02   <thead>
03     <tr>
04       <th>Name</th>
05       <th>Category</th>
06       <th>Price</th>
07       [...]
08     </tr>
09   </thead>
10   <tbody>
11 <%= for product <- @products do %>
12     <tr>
13       <td><%= product.name %></td>
14       <td><%= product.category.name %></td>
15       <td><%= product.price %></td>
16       [...]
17     </tr>
18 <% end %>
19   </tbody>
20 </table>

Testen und probieren

Es ist sehr praktisch, wenn der Entwickler die Datenbank schnell mal löschen und danach wieder neu mit Seed-Datensätzen bespielen kann. In Phoenix darf er dazu die Datei »priv/repo/seeds.exs« heranziehen, Listing 10 zeigt ein passendes Beispiel. So löscht

Listing 10

priv/repo/seeds.exs

01 alias Shop.Store
02
03 {:ok, fruits} = Store.create_category(%{ name: "Obst" })
04 {:ok, vegetables} = Store.create_category(%{ name: "Gemüse" })
05
06 Store.create_product(%{ name: "Banane", price: 1, category_id: fruits.id })
07 Store.create_product(%{ name: "Apfel", price: 0.5, category_id: fruits.id })
08 Store.create_product(%{ name: "Kartoffel", price: 2, category_id: vegetables.id })

mix ecto.drop
mix ecto.create
mix ecto.migrate
mix run priv/repo/seeds.exs

– auf der Shell schnell getippt – die Datenbank, erzeugt eine neue, migriert und spielt die Seeds ein.

Die meisten, die mit einem Webframework entwickeln, probieren öfter Code-Fetzen aus oder fragen testweise Daten ab. Bei Phoenix Framework geht das prima, indem man eine Konsole öffnet:

iex -S mix phoenix.server

Hier hat der Entwickler auf alle Funktionen seines Projekts Zugriff. So kann er, wie in Abbildung 4 zu sehen ist, eine Liste von allen Kategorien abfragen. Nach einem doppelten [Ctrl]+[C] beendet sich die Konsole wieder.

Abbildung 4: Auf der Phoenix-Konsole l&auml;sst sich per <code>Shop.Store.list_categories()</code> interaktiv die Kategorien-Liste abfragen.

Abbildung 4: Auf der Phoenix-Konsole lässt sich per »Shop.Store.list_categories()« interaktiv die Kategorien-Liste abfragen.

Eindeutiger Geschwindigkeitsvorteil

Da Phoenix der Ruf vorauseilt, sehr flink zu arbeiten, hat der Autor das Gerüst des Beispiel-Webshops auf seinem Laptop vermessen. Der Abruf der Produktliste via Webbrowser auf dem Entwicklungsserver dauerte rund 2 Millisekunden. Zum Vergleich hat er die gleiche Applikation mit Rails implementiert und auch dort die Produktliste abgefragt. Ergebnis: 19 Millisekunden! Auch bei vielen anderen Projekten stellt sich ein etwa zehnfacher Geschwindigkeitsvorteil ein.

Einen gewissen Teil seines Geschwindigkeitsvorteils erzielt Phoenix durch seinen Datenbank-Wrapper Ecto [8]. Rails nutzt an gleicher Stelle das sehr mächtige ORM-Framework (Object Relational Mapping) Active Record [9]. Es arbeitet unterm Strich deutlich langsamer als Ecto, bietet aber – das muss man gerechterweise hinzufügen – Entwicklern deutlich mehr Komfort und viel mehr magische Automatiken.

Für den Produktivbetrieb

Neben der geprüften Tatsache, dass Phoenix schneller fliegt als die Konkurrenz, kann der Wundervogel im produktiven Einsatz noch mit weiteren positiven Eigenschaften aufwarten. Elixir, die Sprache, in der Phoenix verfasst ist, läuft auf Beam [10], der virtuellen Maschine für Erlang/OTP-Kompilate. Der Beam wird gestartet und lädt dann die Phoenix-Applikation.

Erlang wiederum gibt es seit 1987 und hat somit alle Kinderkrankheiten lange hinter sich gebracht. Auch Beam ist von jeher auf Stabilität und Skalierbarkeit hin optimiert. Das liegt daran, dass die Firma Ericsson Erlang erfunden hat, um es auf Telefonswitches einzusetzen – OTP steht für Open Telecom Platform. Elixir hat zwar nichts mit Telefonie zu tun, profitiert aber von der Stabilität und den Möglichkeiten dieses Unterbaus:

  • Hot-Deployment: Der Beam kann im laufenden Betrieb Funktionen austauschen. Da die funktionale Programmiersprache Elixir keine Seiteneffekte kennt, darf der Admin auch unter Volllast eine neue Version einer Webapplikation einspielen, ohne den Server auch nur für eine Millisekunde offline nehmen zu müssen.
  • Skalierbarkeit: Der Beam schließt sich auf Anforderung einer einfachen Konfigurationsdatei mit Beams auf anderen Servern zu einem Cluster zusammen. Der Systemverwalter kann den Cluster im laufenden Betrieb vergrößern und verkleinern, also auf Lastspitzen mit zusätzlichen Servern reagieren. Die Beams verteilen die Arbeit im Cluster selbsttätig.
  • Parallelität: Der Beam stellt sicher, dass er alle Prozessoren auf einem Server optimal ausnutzt und die Arbeit gerecht verteilt. (Zu den positiven Auswirkungen siehe Kasten “Performante Websockets”.)
  • Fehlertoleranz: Sollte ein Teil eines Programms wegen eines Programmierfehlers oder durch externe Einflüsse (beispielsweise ein nicht antwortendes externes API) abstürzen, so startet das System dieses Programm automatisch neu. Die Selbstheilung funktioniert auch deshalb, weil die Teile einer Phoenix-Applikation geschachtelt sind wie bei einer russischen Matroschka. Fällt ein Teil aus, wird es neu gestartet; führt das nicht zum Erfolg, startet das Laufzeitsystem auch die Elternfunktion neu.

Performante Websockets

Das vorgestellte Programmierbeispiel kapriziert sich auf normale Webseiten, die der Browser per HTTP-»GET« abruft. Technisch anspruchsvollere Sites halten zusätzlich über Websockets einen Channel zwischen dem Server und jedem zugreifenden Browser offen. Der Server pusht über den Kanal Informationen zum Client.

Als Standard-Webanwendung gilt ein Chatsystem, bei dem mehrere User sich per Browser auf Server einloggen und dann miteinander chatten. Dabei läuft die Eingabe von User A über einen Channel verteilt an alle anderen Teilnehmer des Chatraums.

Natürlich lässt sich das auch mit dem Phoenix Framework realisieren – und das skaliert sogar sehr, sehr gut: 2015 hat das Phoenix-Core-Team als Benchmark einen neuen Rekord aufgestellt mit zwei Millionen Channels auf einem einzelnen Server [11].

Die eingebaute Fehlertoleranz stellt natürlich keine Einladung zur unsauberen Programmierung dar. Aber jeder Programmierer weiß, dass ein Neustart zumindest einen schnellen Workaround bringt, um den Fehler am lebenden System einzugrenzen.

Nicht den Vogel abschießen: Rails, Django oder Phoenix?

Die meisten bestehenden Projekte halten zu Recht den “Never change a running system”-Grundsatz in Ehren und werden ihre frühere Entscheidung zwischen objektorientiert (Rails und Django) oder funktional (Phoenix) nicht über den Haufen werfen. Hinzu kommt: Wer die letzten Jahre nur objektorientiert programmiert hat – Ruby on Rails ist zurzeit das populärste Framework –, für den ist der Umstieg auf die funktionale Programmiersprache Elixir ein steiniger Weg.

Entwickler(teams), die heute ein neues Projekt beginnen und verschmerzen, dass die ersten Schritte mit Rails einen Tick schneller gelingen, fahren mit Phoenix tatsächlich am besten. Denn rein technisch gesehen spricht alles für Phoenix als Plattform: Enorm kurze Antwortzeiten der Applikation, Skalierbarkeit, Parallelität, Hot Deployment, Fehlertoleranz. Und die anfangs gegenüber Rails verlorene Entwicklungszeit holt man langfristig durch Phoenix’ bessere Struktur wieder raus – ein wirklich heißer Vogel.

Infos

  1. Ruby on Rails: http://rubyonrails.org

  2. Dr. Armin Roehrl, “Webprogrammierung mit Ruby und Rails”: Linux-Magazin 12/04, S. 100

  3. Stefan Wintermeyer, “Neue Version 5.1 von Ruby on Rails”: Linux-Magazin 06/17, S. 86

  4. Hanami: http://hanamirb.org

  5. Elixir: https://elixir-lang.org

  6. Phoenix Framework: http://phoenixframework.org

  7. Erlang/OTP installieren: https://elixir-lang.org/install.htm

  8. Ecto: https://github.com/elixir-ecto/ecto

  9. Einführung in Active Record Ruby on Rails: http://guides.rubyonrails.org/active_record_basics.html

  10. Robert Virding, “Hitchhiker’s Tour of the BEAM”: http://www.erlang-factory.com/upload/presentations/708/HitchhikersTouroftheBEAM.pdf (Vortragsfolien)

  11. Websockets-Benchmark: http://phoenixframework.org/blog/the-road-to-2-million-websocket-connections

Der Autor

Stefan Wintermeyer ist Consultant, Trainer und Buchautor für die Themen Ruby on Rails, Phoenix und Webperformance: https://www.wintermeyer-consulting.de.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben