È uscito il Corso Java Completo
Torna al blog

Cos'è Elixir e perché regge milioni di connessioni

Cos'è Elixir: la macchina virtuale BEAM di Erlang, i processi leggeri, la filosofia let it crash, Phoenix LiveView e i limiti reali del linguaggio.

Edoardo Midali

Edoardo Midali

Developer · Content Creator

9 min di lettura

Hai costruito una chat o un sistema di notifiche, e appena gli utenti connessi insieme crescono il server comincia a soffrire: thread che si bloccano, memoria che sale, un errore in una connessione che si porta dietro tutte le altre. Esiste una tecnologia nata esattamente per questo problema, quarant'anni fa, per le centrali telefoniche. In questo articolo trovi cos'è Elixir, perché il suo vero segreto non è Elixir stesso, e quando ha senso sceglierlo.

Cos'è Elixir

Elixir è un linguaggio funzionale creato nel 2012 che gira sulla macchina virtuale di Erlang, la BEAM: porta una sintassi moderna e strumenti comodi sopra una tecnologia progettata per sistemi che devono gestire moltissime attività contemporanee e non fermarsi mai.

L'ha creato José Valim, che veniva dal core team di Ruby on Rails. Si vede: la sintassi ricorda molto Ruby, con do ... end, parentesi facoltative e un'attenzione alla leggibilità. Ma la somiglianza è solo in superficie. Sotto, il modello di esecuzione non ha niente a che fare con Ruby.

Il segreto è la BEAM, non Elixir

Per capire Elixir devi partire da Erlang. Negli anni Ottanta Ericsson aveva un problema preciso: il software delle centrali telefoniche non poteva mai fermarsi. Né per un bug, né per un aggiornamento, né per un guasto hardware. Una centrale che si riavvia fa cadere migliaia di telefonate.

Da quel vincolo è nato Erlang, insieme alla sua macchina virtuale (la BEAM) e a un insieme di librerie chiamato OTP. Le scelte fatte allora sono ancora quelle:

  • Tantissime attività contemporanee, ognuna isolata dalle altre.
  • Gli errori sono normali: il sistema deve sopravvivere ai propri guasti, non evitarli tutti.
  • Aggiornare senza spegnere: la BEAM permette perfino di sostituire il codice mentre il sistema gira.

Erlang funziona benissimo — WhatsApp è il caso più citato di sistema costruito su Erlang — ma la sua sintassi, derivata da Prolog, è spigolosa per chi arriva da linguaggi moderni. Elixir prende tutta quella tecnologia e le mette sopra un linguaggio piacevole da scrivere, con un gestore di pacchetti (Hex), uno strumento di build (Mix) e un sistema di macro. Il codice Elixir e quello Erlang si chiamano a vicenda senza attrito: le librerie di trent'anni sono tutte disponibili.

I processi leggeri: niente memoria condivisa

Il concetto centrale della BEAM è il processo. Attenzione: non è un processo del sistema operativo e nemmeno un thread. È un'unità di esecuzione gestita dalla macchina virtuale, che parte con pochissima memoria e si crea in microsecondi. Averne centinaia di migliaia su una sola macchina è normale.

for n <- 1..100_000 do
  spawn(fn -> IO.puts("Sono il processo #{n}") end)
end

La differenza vera rispetto ai thread di Java o alle goroutine di Go è un'altra: i processi non condividono memoria. Ognuno ha il suo spazio, e l'unico modo di comunicare è mandarsi messaggi.

pid = spawn(fn ->
  receive do
    {:saluta, nome} -> IO.puts("Ciao #{nome}")
  end
end)

send(pid, {:saluta, "Giulia"})

Le conseguenze sono enormi:

  • Niente race condition classiche. Se due processi non possono toccare la stessa variabile, non possono corromperla insieme. Non servono lucchetti.
  • Un processo che va in crash non sporca gli altri. Non c'è stato condiviso lasciato a metà.
  • La garbage collection è per processo. Ogni processo pulisce la sua memoria per conto suo, senza fermare tutto il sistema: la latenza resta prevedibile anche sotto carico.
  • Lo scheduler è preemptive. La BEAM distribuisce il tempo tra i processi e non lascia che uno solo, magari bloccato in un ciclo, monopolizzi la CPU.

In un server web Elixir, ogni connessione è un processo. Diecimila utenti connessi sono diecimila processi isolati, e uno che si comporta male non tocca gli altri novemilanovecentonovantanove.

"Let it crash": il cambio di mentalità

Questa è la parte più difficile da digerire, ed è anche la più importante.

Nella programmazione difensiva classica cerchi di prevedere ogni errore: controlli se il file esiste, se la rete risponde, se il dato ha il formato giusto, e per ogni caso scrivi una gestione. Il codice si riempie di try/catch, e comunque qualche caso ti sfugge — e quello che ti sfugge lascia il programma in uno stato strano.

La filosofia Erlang dice un'altra cosa: gestisci gli errori che sai gestire, e per tutto il resto lascia morire il processo. Qualcuno lo riavvierà in uno stato pulito e noto.

Quel qualcuno è il supervisor: un processo il cui unico lavoro è sorvegliare altri processi e riavviarli quando muoiono, secondo una strategia che decidi tu.

defmodule Contatore do
  use GenServer

  def start_link(iniziale),
    do: GenServer.start_link(__MODULE__, iniziale, name: __MODULE__)

  def incrementa, do: GenServer.cast(__MODULE__, :incrementa)
  def valore, do: GenServer.call(__MODULE__, :valore)

  @impl true
  def init(iniziale), do: {:ok, iniziale}

  @impl true
  def handle_cast(:incrementa, stato), do: {:noreply, stato + 1}

  @impl true
  def handle_call(:valore, _da, stato), do: {:reply, stato, stato}
end

figli = [{Contatore, 0}]
Supervisor.start_link(figli, strategy: :one_for_one)

Se Contatore va in crash, il supervisor lo fa ripartire da zero. I supervisor si organizzano ad albero: un supervisor sorveglia altri supervisor, e un guasto risale solo fin dove serve.

"Let it crash" non vuol dire ignorare gli errori. Vuol dire separare il codice che fa il lavoro dal codice che gestisce i guasti, e accettare che il modo più affidabile di riprendersi da uno stato imprevisto è ricominciare da uno stato conosciuto. È lo stesso ragionamento per cui "spegni e riaccendi" risolve tanti problemi — solo che qui è progettato, automatico, e riguarda un singolo processo invece dell'intero server.

Immutabilità e pattern matching

Elixir è un linguaggio funzionale: se il paradigma ti è nuovo, trovi le basi in paradigmi di programmazione. Qui conta vedere due conseguenze pratiche.

I dati sono immutabili. Non modifichi una lista: ne ottieni una nuova. Questo si sposa perfettamente con i processi isolati, perché un messaggio inviato non può essere cambiato da nessuno dopo.

Il pattern matching è ovunque. Invece di estrarre i valori con condizioni, descrivi la "forma" che ti aspetti:

case File.read("config.json") do
  {:ok, contenuto} -> elabora(contenuto)
  {:error, :enoent} -> IO.puts("File non trovato")
  {:error, motivo} -> IO.puts("Errore: #{inspect(motivo)}")
end

E l'operatore pipe |> rende leggibili le trasformazioni in sequenza:

"  Ciao Mondo Elixir  "
|> String.trim()
|> String.downcase()
|> String.split(" ")
# ["ciao", "mondo", "elixir"]

Se ti interessa il lato più rigoroso del funzionale, con un sistema di tipi statico molto potente, il confronto naturale è Haskell. Elixir è più pragmatico: tipizzazione dinamica, con un sistema di tipi graduale che sta entrando nelle versioni recenti.

Phoenix e LiveView

Il framework web di riferimento è Phoenix, che nel suo territorio — applicazioni con tante connessioni aperte — è tra i più efficienti in circolazione.

La sua funzione più interessante è LiveView: interfacce interattive in tempo reale senza scrivere JavaScript applicativo. Il browser apre una connessione WebSocket verso il server; lo stato della pagina vive in un processo Elixir; quando qualcosa cambia, il server calcola solo la differenza nell'HTML e la invia. Un contatore, un form con validazione immediata, una dashboard che si aggiorna: tutto scritto in Elixir.

Funziona proprio grazie alla BEAM: un processo per ogni utente connesso costa poco, e tenerne migliaia aperti è il caso d'uso per cui la macchina è nata.

Il limite: ogni interazione passa dal server, quindi con una connessione lenta o instabile l'esperienza ne risente. Per interfacce che devono funzionare offline o con animazioni complesse lato client, un framework JavaScript resta la scelta giusta.

Dove brilla e dove no

AmbitoAdatto?Perché
Chat, messaggisticaMoltoUn processo per connessione, isolamento dei guasti
Sistemi realtime, notifiche, dashboard liveMoltoPhoenix Channels e LiveView
Tante connessioni contemporanee (IoT, giochi multiplayer)MoltoÈ il motivo per cui la BEAM esiste
API web classicheSìFunziona bene, ma il vantaggio è meno netto
Calcolo numerico intensivoNoLa BEAM non è ottimizzata per la potenza di calcolo bruta
Data science, machine learningPocoEsiste Nx, ma l'ecosistema vero è in Python
App mobile, desktopNoNon è il suo territorio

Discord è il nome che si cita più spesso tra chi usa Elixir in produzione, proprio per la parte di messaggistica in tempo reale.

Errori comuni

Usare un processo come fosse un oggetto. Chi arriva dalla programmazione a oggetti tende a creare un processo per ogni entità. Un processo serve quando c'è concorrenza o stato da isolare, non per organizzare il codice: per quello bastano moduli e funzioni.

Creare un collo di bottiglia con un singolo GenServer. Un processo gestisce un messaggio alla volta. Se tutte le richieste passano da lì, hai reintrodotto la serializzazione che volevi evitare.

Fraintendere "let it crash" come "non gestire niente". Gli errori prevedibili — input non valido, utente inesistente — si gestiscono normalmente con {:ok, ...} e {:error, ...}. Il crash è per ciò che non sai come recuperare.

Sceglierlo per mode. Se il tuo progetto è un gestionale con poche decine di utenti, non stai usando nessuno dei vantaggi di Elixir e paghi tutti i costi di un ecosistema più piccolo.

In sintesi

Elixir è una sintassi moderna sopra una tecnologia collaudata: la forza viene dalla BEAM di Erlang, nata per centrali telefoniche che non potevano fermarsi.

Il modello a processi isolati cambia le regole della concorrenza. Niente memoria condivisa, comunicazione solo via messaggi, e un processo che muore non trascina gli altri.

"Let it crash" e i supervisor sono un cambio di mentalità vero: invece di prevedere ogni errore, progetti il sistema per riprendersi da solo ripartendo da uno stato pulito.

I limiti sono concreti: la comunità è piccola, in Italia le offerte di lavoro sono poche, e per il calcolo numerico o il machine learning ci sono scelte migliori. Brilla dove servono tante connessioni e tempo reale; altrove, raramente vale il cambio.

Per collocarlo tra gli altri linguaggi c'è tutti i linguaggi di programmazione.