Elixir e Phoenix: construindo aplicações escaláveis
1. Introdução ao Ecossistema Elixir e Phoenix
1.1. Por que Elixir? A Máquina Virtual BEAM e o modelo de concorrência baseado em atores
Elixir é uma linguagem funcional que roda sobre a Máquina Virtual BEAM (originalmente criada para Erlang). O que torna Elixir excepcional para escalabilidade é seu modelo de concorrência baseado no padrão de atores. Diferente de linguagens tradicionais que usam threads do sistema operacional, Elixir utiliza processos leves (green threads) que consomem pouquíssima memória — cada processo ocupa aproximadamente 2-4 KB. Isso permite que uma única instância do BEAM gerencie milhões de processos simultaneamente sem degradação significativa de desempenho.
1.2. Phoenix como framework web: produtividade, desempenho e tolerância a falhas
Phoenix é o framework web mais popular do ecossistema Elixir. Ele herda todas as vantagens do BEAM e adiciona camadas de produtividade como LiveView (para interfaces reativas sem JavaScript complexo), Ecto (para persistência de dados) e Channels (para comunicação em tempo real). Phoenix é conhecido por sua baixa latência e alta throughput, sendo capaz de servir milhares de requisições por segundo com uso mínimo de recursos.
1.3. Escalabilidade horizontal vs. vertical: como Elixir lida naturalmente com milhões de conexões
Enquanto muitas linguagens exigem arquiteturas complexas para escalar horizontalmente (adicionando mais servidores), Elixir escala verticalmente de forma natural. Um único servidor BEAM pode gerenciar 2 milhões de conexões WebSocket simultâneas com 2 GB de RAM. Quando necessário, a escalabilidade horizontal é alcançada através de distribuição nativa entre nós BEAM, sem necessidade de balanceadores externos complexos.
2. Fundamentos de Concorrência e Escalabilidade no BEAM
2.1. Processos leves (green threads) e comunicação via mensagens assíncronas
No BEAM, cada processo é isolado e se comunica exclusivamente através de mensagens assíncronas. Não há memória compartilhada, eliminando problemas de race conditions e locks. Um exemplo prático:
defmodule MeuModulo do
def criar_processos(quantidade) do
1..quantidade
|> Enum.map(fn id ->
spawn(fn -> processar_tarefa(id) end)
end)
end
defp processar_tarefa(id) do
receive do
{:tarefa, dados} -> IO.puts("Processo #{id} recebeu: #{dados}")
end
end
end
# Criando 100.000 processos simultaneamente
MeuModulo.criar_processos(100_000)
2.2. Supervisão e tolerância a falhas: a filosofia “let it crash”
A filosofia central do BEAM é "deixe quebrar" (let it crash). Em vez de tentar prever e tratar todos os erros possíveis, você define árvores de supervisão que reiniciam automaticamente processos que falham. Isso torna sistemas extremamente resilientes:
defmodule MeuSupervisor do
use Supervisor
def start_link(_opts) do
Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
end
def init(:ok) do
children = [
{MeuGenServer, []},
{OutroProcesso, []}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
2.3. Gerenciamento de estado imutável e isolamento entre processos
Cada processo no BEAM possui seu próprio estado isolado. Isso significa que um processo nunca pode corromper o estado de outro. O estado é gerenciado através de GenServers ou Agents, que encapsulam dados mutáveis de forma segura:
defmodule Contador do
use Agent
def start_link(initial_value \\ 0) do
Agent.start_link(fn -> initial_value end, name: __MODULE__)
end
def incrementar do
Agent.update(__MODULE__, fn state -> state + 1 end)
end
def valor_atual do
Agent.get(__MODULE__, fn state -> state end)
end
end
3. Estruturando uma Aplicação Phoenix para Escalar
3.1. Arquitetura de contexto (Contexts) e separação de responsabilidades
Phoenix incentiva o uso de Contexts para organizar o código em domínios coesos. Cada Context encapsula um conjunto de funcionalidades relacionadas, facilitando a manutenção e evolução do sistema:
# lib/minha_app/catalogo.ex
defmodule MinhaApp.Catalogo do
alias MinhaApp.Catalogo.{Produto, Categoria}
def listar_produtos do
Repo.all(Produto)
end
def criar_produto(attrs) do
%Produto{}
|> Produto.changeset(attrs)
|> Repo.insert()
end
def buscar_por_categoria(categoria_id) do
Repo.get_by(Categoria, id: categoria_id)
|> Repo.preload(:produtos)
end
end
3.2. Uso de GenServers e Agents para estado compartilhado escalável
Para estado que precisa ser compartilhado entre múltiplos processos, GenServers oferecem uma abstração robusta com suporte a chamadas síncronas e assíncronas:
defmodule CacheManager do
use GenServer
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def put(chave, valor) do
GenServer.cast(__MODULE__, {:put, chave, valor})
end
def get(chave) do
GenServer.call(__MODULE__, {:get, chave})
end
def handle_call({:get, chave}, _from, state) do
{:reply, Map.get(state, chave), state}
end
def handle_cast({:put, chave, valor}, state) do
{:noreply, Map.put(state, chave, valor)}
end
end
3.3. Configuração de pools de conexão e balanceamento de carga com Phoenix PubSub
Phoenix PubSub permite comunicação publish-subscribe distribuída entre processos e nós:
# config.exs
config :my_app, MyApp.PubSub,
adapter: Phoenix.PubSub.PG2,
name: MyApp.PubSub
# Em um GenServer
Phoenix.PubSub.subscribe(MyApp.PubSub, "notificacoes")
# Em outro processo
Phoenix.PubSub.broadcast(MyApp.PubSub, "notificacoes", %{tipo: :nova_mensagem, dados: "Olá!"})
4. Comunicação em Tempo Real com Phoenix Channels
4.1. WebSockets nativos no Phoenix: como gerenciar milhares de conexões simultâneas
Phoenix Channels abstraem WebSockets e oferecem um modelo baseado em tópicos. Cada conexão é tratada como um processo leve:
# lib/my_app_web/channels/chat_channel.ex
defmodule MyAppWeb.ChatChannel do
use Phoenix.Channel
def join("chat:" <> sala, _params, socket) do
{:ok, assign(socket, :sala, sala)}
end
def handle_in("nova_mensagem", %{"texto" => texto}, socket) do
broadcast!(socket, "mensagem_recebida", %{usuario: socket.assigns.usuario, texto: texto})
{:noreply, socket}
end
end
4.2. Tópicos e difusão de mensagens com PubSub distribuído
Os tópicos permitem criar canais de comunicação flexíveis. Um usuário pode se inscrever em múltiplos tópicos simultaneamente:
defmodule MyAppWeb.RoomChannel do
use Phoenix.Channel
def join("room:" <> room_id, _params, socket) do
{:ok, assign(socket, :room_id, room_id)}
end
def handle_out("mensagem", payload, socket) do
push(socket, "nova_mensagem", payload)
{:noreply, socket}
end
end
4.3. Estratégias de backpressure e rate limiting para evitar sobrecarga
Para evitar sobrecarga, implemente rate limiting com ETS ou Redis:
defmodule RateLimiter do
use GenServer
def check_rate(key, max_requests, time_window_ms) do
GenServer.call(__MODULE__, {:check, key, max_requests, time_window_ms})
end
def handle_call({:check, key, max, window}, _from, state) do
now = System.monotonic_time(:millisecond)
requests = Map.get(state, key, [])
# Remove requisições antigas
recent = Enum.filter(requests, fn t -> now - t < window end)
if length(recent) < max do
new_state = Map.put(state, key, [now | recent])
{:reply, :ok, new_state}
else
{:reply, {:error, :rate_limit_exceeded}, state}
end
end
end
5. Banco de Dados e Persistência Escalável
5.1. Ecto: consultas otimizadas, replicação e sharding de dados
Ecto oferece consultas otimizadas com composição e lazy evaluation:
defmodule MyApp.Produtos do
import Ecto.Query
def buscar_produtos_em_promocao(preco_max) do
Produto
|> where([p], p.preco < ^preco_max)
|> where([p], p.em_promocao == true)
|> order_by([p], desc: p.data_criacao)
|> limit(50)
|> Repo.all()
end
def buscar_com_filtros(params) do
Produto
|> filter_by_categoria(params[:categoria])
|> filter_by_preco(params[:preco_min], params[:preco_max])
|> Repo.paginate(page: params[:page], page_size: 20)
end
end
5.2. Uso de filas assíncronas (Oban, Broadway) para processamento em lote
Oban gerencia filas de trabalho com suporte a retry e agendamento:
defmodule MyApp.Workers.EmailWorker do
use Oban.Worker, queue: :emails
@impl Oban.Worker
def perform(%Oban.Job{args: %{"user_id" => user_id, "template" => template}}) do
user = Repo.get!(User, user_id)
MyApp.Mailer.send_email(user, template)
:ok
end
# Agendando um job
%{user_id: 123, template: "welcome"}
|> MyApp.Workers.EmailWorker.new()
|> Oban.insert()
5.3. Cache distribuído com Redis e integração com Phoenix
Redis oferece cache rápido e distribuído:
defmodule MyApp.Cache do
use GenServer
def get(key) do
case Redix.command(:redix, ["GET", key]) do
{:ok, nil} -> {:miss, nil}
{:ok, value} -> {:hit, value}
end
end
def set(key, value, ttl \\ 3600) do
Redix.command(:redix, ["SETEX", key, ttl, value])
end
def invalidate(pattern) do
{:ok, keys} = Redix.command(:redix, ["KEYS", pattern])
Enum.each(keys, fn key ->
Redix.command(:redix, ["DEL", key])
end)
end
end
6. Deploy, Monitoramento e Observabilidade
6.1. Implantação em clusters distribuídos
Elixir suporta deploy em diversas plataformas. Um exemplo de configuração para Fly.io:
# fly.toml
app = "my-app"
[build]
builder = "elixir:1.16"
[http_service]
internal_port = 4000
force_https = true
[[services]]
protocol = "tcp"
internal_port = 4000
[[services.ports]]
port = 80
handlers = ["http"]
[[services.ports]]
port = 443
handlers = ["tls", "http"]
6.2. Métricas e tracing com Telemetry, Prometheus e Grafana
Telemetry é o sistema de métricas nativo do Elixir:
# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
use Supervisor
def start_link(_opts) do
Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
end
def init(:ok) do
children = [
{Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
defp metrics do
[
counter("http.request.count"),
last_value("http.request.latency"),
distribution("http.request.duration")
]
end
end
6.3. Logs estruturados e debugging remoto em produção
Logger configurado para JSON estruturado:
# config/prod.exs
config :logger, :console,
format: "$metadata[$level] $message\n",
metadata: [:request_id, :user_id]
config :logger_json, :backend,
formatter: LoggerJSON.Formatters.GoogleCloud,
level: :info
7. Casos de Uso Reais e Padrões Avançados
7.1. Aplicações de chat, streaming e jogos multiplayer em tempo real
Aplicações de chat são implementadas naturalmente com Phoenix Channels:
defmodule MyAppWeb.ChatLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "chat")
end
{:ok, assign(socket, mensagens: [], texto: "")}
end
def handle_event("enviar", %{"texto" => texto}, socket) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "chat", %{usuario: socket.assigns.current_user, texto: texto})
{:noreply, assign(socket, texto: "")}
end
def handle_info(%{usuario: usuario, texto: texto}, socket) do
{:noreply, update(socket, :mensagens, fn msgs -> msgs ++ [%{usuario: usuario, texto: texto}] end)}
end
end
7.2. Integração com sistemas legados e microserviços via RabbitMQ ou Kafka
Broadway é ideal para processamento de streams de dados:
defmodule MyApp.BroadwayPipeline do
use Broadway
alias Broadway.Message
def start_link(_opts) do
Broadway.start_link(__MODULE__,
name: __MODULE__,
producer: [
module: {BroadwayRabbitMQ.Producer, queue: "orders"},
concurrency: 10
],
processors: [
default: [concurrency: 50]
],
batchers: [
default: [batch_size: 100, batch_timeout: 2000]
]
)
end
def handle_message(_, message, _) do
message
|> Message.update_data(fn data -> process_order(data) end)
end
def handle_batch(_, messages, _, _) do
Repo.insert_all(Order, Enum.map(messages, & &1.data))
messages
end
end
7.3. Testes de carga e simulação de milhões de conexões
defmodule MyAppWeb.LoadTest do
use Phoenix.ChannelTest
@endpoint MyAppWeb.Endpoint
test "simula 10000 conexões simultâneas" do
connections =
1..10_000
|> Enum.map(fn _ ->
Task.async(fn ->
{:ok, _, socket} = socket(MyAppWeb.UserSocket, %{user_id: :rand.uniform(1000)})
|> subscribe_and_join(MyAppWeb.RoomChannel, "room:lobby")
push(socket, "nova_mensagem", %{texto: "Hello"})
assert_push "mensagem_recebida", _
:ok
end)
end)
results = Task.await_many(connections, :infinity)
assert Enum.all?(results, &(&1 == :ok))
end
end
Referências
- Documentação Oficial do Elixir — Guia completo da linguagem, incluindo módulos de concorrência, GenServer, Agent e Supervisor
- Documentação Oficial do Phoenix Framework — Tutoriais sobre Channels, PubSub, LiveView e arquitetura de Contexts
- [Elixir School: Concorrência e OTP](https://elixirschool.com/en/lessons/advanced/concurrency
) — Guia prático sobre processos, mensagens e árvores de supervisão
- Oban: Job Processing for Elixir — Documentação oficial para filas assíncronas com suporte a retry e agendamento
- Broadway: Data Processing Pipelines — Framework para processamento de streams e integração com RabbitMQ, Kafka e outros
- Telemetry: Métricas e Instrumentação — Sistema nativo de métricas para Elixir, compatível com Prometheus e Grafana
- Phoenix PubSub: Comunicação Distribuída — Documentação sobre difusão de mensagens em clusters
- Fly.io: Deploy de Aplicações Elixir — Guia de implantação em plataforma gerenciada com suporte nativo a BEAM
- Tsung: Teste de Carga Distribuído — Ferramenta para simular milhões de conexões simultâneas
- Ecto: Consultas e Migrations — Documentação oficial do banco de dados para Phoenix
- Redix: Cliente Redis para Elixir — Biblioteca para cache distribuído e filas
- Kubernetes para Elixir — Guia de orquestração de clusters BEAM em contêineres
Conclusão: O Caminho para Aplicações Escaláveis com Elixir e Phoenix
Construir aplicações escaláveis não é apenas uma questão de tecnologia, mas de arquitetura e mentalidade. Elixir e Phoenix oferecem um ecossistema maduro que abraça a concorrência desde o nível mais baixo — com processos leves na BEAM — até o mais alto, com ferramentas como Phoenix PubSub, Oban e Broadway. A filosofia "let it crash" combinada com árvores de supervisão garante tolerância a falhas sem complexidade adicional, enquanto o modelo de atores elimina problemas clássicos de concorrência, como race conditions e deadlocks.
Ao longo deste artigo, exploramos desde os fundamentos de concorrência até casos de uso reais, como chat em tempo real, processamento de filas e integração com sistemas legados. Cada camada — banco de dados, cache, mensageria e deploy — foi projetada para escalar horizontalmente, permitindo que sua aplicação lide com milhões de conexões simultâneas sem perda de desempenho.
O segredo do sucesso está em adotar os padrões certos: Contexts para organização, GenServers para estado compartilhado, PubSub para comunicação em tempo real e filas para tarefas assíncronas. Com as ferramentas certas e uma arquitetura bem planejada, Elixir e Phoenix permitem que você construa sistemas que não apenas escalam, mas também permanecem resilientes e fáceis de manter.
Agora é sua vez: experimente, implemente um protótipo, teste com milhares de conexões e veja como a BEAM transforma a maneira como você pensa sobre escalabilidade. O futuro das aplicações web é distribuído, tolerante a falhas e, acima de tudo, escalável — e Elixir está na vanguarda dessa revolução.