Kino: Een high-performance Ractor webserver voor Ruby 4.0+
Omdat Ruby-threads geen Ruby-code parallel kunnen uitvoeren, maken productie-omgevingen meestal gebruik van een fork per CPU-core, waarbij men betaalt voor elke kopie in het geheugen. Kino voert je code op elke core uit binnen één klein proces. Een front-end geschreven in Rust (tokio + hyper) beheert het netwerk, parallelle Ractors draaien je Rack 3-applicatie, en een threaded fallback-modus draait alles overig, inclusief Rails.
Belangrijkste kenmerken
- Snel. Op een echte server met 8 cores is elke Kino-modus 1,5 tot 2 keer sneller dan een Puma fork-cluster op endpoints met weinig I/O. De Ractor-modus wint ook op pure CPU-taken met 30% of meer.
- Minimaal geheugengebruik. Gebruikt ongeveer 7 keer minder geheugen bij een eenvoudige benchmark Ractor-app, en ongeveer 4 keer minder geheugen dan een Puma-cluster die Rails serveert in de threaded fallback-modus.
- Parallel zonder forking. De Ractor-modus voert CPU-werk meer dan 5 keer sneller uit dan de GVL-gebonden threaded-modus van Kino, binnen hetzelfde kleine proces.
- Productie-gereed. Inclusief graceful drain, crash-supervisie en respawn, begrensde wachtrijen met 503 backpressure, request-timeouts, beveiligde intake (deadlines voor slowloris en TLS-handshakes, limieten voor connecties en body-grootte), een
on_errorhook voor error-trackers, TLS (rustls), live statistieken, asynchrone toegang en app-logging. - Diagnostische tools.
kino --checklijst precies op wat je applicatie blokkeert voor de Ractor-modus, zodat je niet zelfRactor::IsolationErrorhoeft te decoderen. - Puma-compatibel. Gebruikt dezelfde workers × threads topologie, een vertrouwde config DSL en een
kinoCLI. Als je Puma kunt draaien, kun je Kino draaien.
N.B.: Ractors zijn officieel experimenteel in Ruby 4.0, en dat geldt ook voor deze server. De threaded-modus is stabiel. Kino streeft ernaar de beste manier te zijn om vandaag met Ractors te experimenteren, en de beste Ractor-server te worden zodra deze stabiel zijn.
---
Waarom Kino?
De GVL (Global VM Lock) staat toe dat slechts één Ruby-thread tegelijk draait. Om alle cores te benutten, maken Ruby-servers gebruik van forks, waarbij elke fork een volledige kopie van de applicatie kost. Ractors hebben deze limiet niet: elke Ractor heeft zijn eigen lock, waardoor één proces Ruby parallel kan uitvoeren. Wat ontbrak was een server die verzoeken naar deze Ractors kon distribueren. Ruby 4.0 heeft Ractors herzien (Ractor::Port, shareable_proc, minder lock-contention), waardoor de bouw van zo'n server zinvol is geworden.
---
Benchmarks
Gemeten op een AWS c7a.2xlarge server (8-core AMD EPYC 9R14, 16 GB, Amazon Linux 2023).
Doorvoersnelheid (Requests per seconde)
De onderstaande tabel toont een kleine synthetische Rack-app (plaintext, 10 KB body, CPU-bound fib, 5 ms wachttijd). De tests zijn uitgevoerd met Ruby 4.0.5 met YJIT.
| Endpoint | Kino :ractor | Kino :ractor (+ lanes, 32 workers) | Kino :threaded | Puma (cluster) |
|---|---|---|---|---|
/plaintext | 229.534 | 250.222 | 182.997 | 118.176 |
/10k | 178.083 | 189.862 | 151.034 | 106.768 |
/cpu (fib) | 77.999¹ | 70.885 | 66.100 | 58.006 |
/io (5 ms) | 1.552 | 1.551 | 5.888 | 4.693 |
/io_native | 1.570 | 1.571 | 6.274 | 4.691 |
¹ Standaardinstellingen, zonder tuning. Ractor-modus verslaat de fork-cluster op pure CPU met +34%.
Geheugengebruik (PSS)
Kleine benchmark app (Ractor-shareable):
| Modus | Kino (één proces) | Puma cluster (8 workers) | Ratio |
|---|---|---|---|
:ractor (8×1) | 148 MB | 1.068 MB | ~7× |
:threaded (8×3) | 107 MB³ | 1.068 MB | ~10× |
Echte Rails app (niet Ractor-shareable, gebruikt :threaded fallback):
| Modus | Kino :threaded | Puma cluster (8 workers) | Ratio |
|---|---|---|---|
| PSS | 92 MB | 389 MB | ~4× |
³ Gemeten met MALLOCARENAMAX=2.
Vergelijking met Ractor wrappers
Het wrappen van een applicatie in een Ractor-pool binnen een bestaande server is minder efficiënt dan Kino:
| Endpoint | Kino :ractor (8×3) | Puma + ractor wrapper | Falcon + ractor wrapper |
|---|---|---|---|
/plaintext | 193.826 | 19.480 | 99.776 |
/cpu (fib) | 68.061 | 17.755 | 48.721 |
/io (5 ms) | 4.530 | 1.454 | 1.549 |
---
Rails
Rails is momenteel niet Ractor-shareable, waardoor Kino dit serveert in de :threaded fallback-modus (één GVL-gebonden proces).
Rails hello-world vergelijking:
| Modus | Requests/s | Geheugen (PSS) |
|---|---|---|
| Kino :threaded (één proces) | 2.637 | 92 MB |
| Puma cluster (8 workers) | 12.138 | 389 MB |
De afweging is eerlijk: de Puma fork-cluster gebruikt alle 8 cores en haalt zo ongeveer 4,6x de doorvoer, maar tegen 4x de geheugenkosten. Ractor-mode Rails zou dit gat dichten tegen de geheugenkosten van één proces.
---
Installatie
Je hebt Ruby >= 4.0 nodig. Voeg Kino toe aan je bundle:
bundle add kino
# of: gem install kino (buiten een bundle)
Of voeg het toe aan je Gemfile:
gem "kino"
Genereer vervolgens een configuratie en start de server:
bundle exec kino --init # schrijft kino.rb
bundle exec kino # start server op poort :9292 via config.ru + kino.rb
Er is geen Rust-compiler nodig voor released versies; deze worden geleverd als voorgecompileerde native gems voor Linux (x86_64/aarch64, glibc en musl) en macOS (arm64).
---
Gebruik
Basisvoorbeeld
require "kino"
# Ractor-modus vereist een Ractor-shareable app: niets vastleggen, config bevriezen.
app = Ractor.shareable_proc do |env|
[200, { "content-type" => "text/plain" }, ["Hello from #{Ractor.current}"]]
end
Kino::Server.run(app, port: 9292) # vangt INT/TERM op; Ctrl-C voert graceful drain uit
Geavanceerde configuratie
server = Kino::Server.new(app,
bind: "127.0.0.1",
port: 9292, # 0 = efemer; uit te lezen via server.port
workers: Etc.nprocessors, # ractors (parallellisme)
threads: 1, # per worker; ractor default 1, threaded default 3
mode: :auto, # :auto | :ractor | :threaded
queue_depth: 1024, # begrensde wachtrij; overflow → 503
queue_timeout: 5.0, # seconden voor 503 bij volle wachtrij
request_timeout: nil, # seconden voor 504 (nil = uit)
max_connections: 8192, # max concurrent connecties
max_body_size: 50 * 1024 * 1024, # bytes voor 413; nil = proxy handleiding
on_error: ->(e, env) { ErrorTracker.capture(e) }, # na verzenden van 500
shutdown_timeout: 30, # drain deadline
control_bind: "127.0.0.1:9293", # monitoring: /stats /metrics /ready /live
control_token: ENV["KINO_CONTROL_TOKEN"], # optionele Bearer auth
tls: { cert: "cert.pem", key: "key.pem" }, # bestandspaden of inline PEM
)
server.start
server.shutdown # graceful: drain → deadline → abort
Modi
:ractor: GebruiktworkersRactors ×threadsThreads per stuk. De app moetRactor.shareable?zijn. Een gecrashte Ractor geeft direct een 500 terug aan actieve verzoeken en wordt daarna opnieuw opgestart.:threaded: Gebruikt dezelfde structuur op basis van normale Threads. Draait elke Rack-app, inclusief Rails. Parallel voor I/O, maar geserialiseerd door de GVL voor CPU.:auto(standaard): Gebruikt:ractorals de app shareable is, anders een waarschuwing en:threaded. Let op: een class die als Rack-app wordt gebruikt, wordt altijd als "shareable" gezien. Forceer in dat geval:threaded.
---
Configuratiebestand en CLI
Instellingen kunnen in een Puma-stijl Ruby DSL-bestand worden geplaatst. Prioriteit: expliciete kwargs en CLI-vlaggen > configuratiebestand > standaardwaarden.
Voorbeeld kino.rb:
port 9292
workers 8
threads 1
mode :ractor
Handige commando's:
kino --init: Schrijft een volledig gedocumenteerd voorbeeldkino.rb.kino: Start de server metconfig.ru+kino.rbop poort 9292.kino --check: Legt uit of de app in:ractormodus kan draaien.kino -C config/kino.rb -p 3000 -w 4 -m ractor my_app.ru: Start met specifieke parameters.
---
Analyse met kino --check
Wanneer een app niet in :ractor modus kan draaien, geeft Kino aan waarom dit zo is in plaats van een generieke Ractor::IsolationError. Het identificeert:
- Vastgelegde variabelen en waar ze zijn gedefinieerd.
- Instantievariabelen via hun pad.
- Class-level instantievariabelen.
Voorbeeld output:
$ kino --check
check: app is NOT Ractor-shareable
- app (Proc at app.rb:12)—captures `cache` = {} (Hash) (unshareable)
- app (HelloApp).@instance—class-level ivar holds #<HelloApp…>
hints: freeze config at boot; build endpoints with Ractor.shareable_proc;
keep per-worker resources in Ractor.store_if_absent; or run mode :threaded.
---
Request Time-outs en Beveiliging
request_timeout begrenst hoe lang een app mag doen over een respons. Na de deadline krijgt de client een 504; de handler blijft draaien maar de late respons wordt weggegooid. De handler wordt niet geforceerd gestopt omdat dit in Ruby onveilig is.
Netwerkbeveiliging:
- Nieuwe connecties boven
max_connectionswachten in de kernel backlog. - Request bodies boven
maxbodysize(standaard 50 MB) krijgen een 413. - Vaste deadlines voor trage headers (15s), gestagneerde TLS-handshakes (10s) en uploads die halverwege stoppen (30s).
---
Lifecycle Hooks
Kino biedt verschillende hooks:
Worker-context (binnen de worker):
afterworkerboot { |workerid| }: Draait één keer voor de worker begint. In:ractormodus moet dit eenRactor.shareableproczijn.afterrequestcomplete { |env, status| }: Draait na elke succesvolle respons. In:ractormodus moet dit eenRactor.shareable_proczijn.
Main-context (hoofdthread):
after_boot { }: Draait nadat de worker pool is opgestart.onworkerexit { |worker_index, error| }: Draait wanneer een worker stopt.
---
Quarantaine voor vastgelopen workers
quarantinetimeout zet een dispatch-slot in quarantaine als een request langer duurt dan de deadline, en start een vervangende worker om de capaciteit te herstellen. Dit is verschillend van requesttimeout, die alleen de client een 504 geeft maar het slot bezet houdt.
quarantine_max begrenst het totaal aantal vervangingen. Een vastgelopen worker wordt nooit geforceerd gestopt; in :ractor modus lekt de wedged Ractor tot het proces stopt.
---
Statistieken (Stats)
server.stats geeft een live snapshot van de configuratie en tellers:
mode,workers,threads,served,rejected,timeouts, etc.
Externe monitoring via control_bind:
GET /stats: Snapshot als JSON.GET /metrics: Prometheus text-formaat (bijv.kinorequestsserved_total).GET /ready: 200 bij actieve service, 503 tijdens boot of drain (ideaal voor Kubernetes readiness probe).GET /live: 200 zolang het proces leeft (liveness probe).
---
Logging
Kino gebruikt een asynchrone Rust-flusher thread, waardoor request-threads nooit op een log-mutex hoeven wachten.
- Access log (
log_requests true): Eén regel per request naar stdout, inclusief 503s. Kleurgecodeerd op status (2xx groen, 3xx geel, 4xx maroon, 5xx rood). - Kino::Logger: Een
::Loggersubclass voor applicatie-logging.
Integratie met Rails:
# config/environments/production.rb
config.logger = Kino::Logger.new # stdout
# of
config.logger = Kino::Logger.new("log/production.log") # bestand
---
Overige technische details
Timer-wachtrijen
Kino.sleep(seconds) is een high-resolution sleep op de OS-clock waarbij de GVL wordt vrijgegeven. Gebruik dit in handlers voor expliciete wachttijden.
Rack 3 Compliance
De spec-suite is getest onder Rack::Lint via echte sockets, inclusief streaming request bodies, full-duplex response bodies en correcte HEAD/204 semantiek.
Ontwikkeling
Voor developers:
bin/setupbundle exec rake: Compileert, draait Rust tests, specs, RBS en lint.RBSYSCARGO_PROFILE=dev bundle exec rake compile: Voor snellere rebuilds tijdens ontwikkeling.
Groetjes,