kino 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/Cargo.lock +115 -24
- data/README.md +113 -41
- data/doc/architecture.md +61 -7
- data/doc/benchmarks.md +120 -26
- data/ext/kino/Cargo.toml +8 -3
- data/ext/kino/src/control.rs +267 -60
- data/ext/kino/src/env_strings.rs +172 -12
- data/ext/kino/src/io_shards.rs +437 -0
- data/ext/kino/src/lib.rs +12 -7
- data/ext/kino/src/listen.rs +4 -1
- data/ext/kino/src/log.rs +10 -2
- data/ext/kino/src/queue.rs +61 -44
- data/ext/kino/src/registry.rs +103 -10
- data/ext/kino/src/request.rs +150 -69
- data/ext/kino/src/response.rs +40 -9
- data/ext/kino/src/server.rs +759 -97
- data/ext/kino/src/tls.rs +129 -12
- data/lib/kino/configuration.rb +16 -0
- data/lib/kino/server.rb +10 -0
- data/lib/kino/templates/kino.rb.tt +13 -2
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +14 -11
- data/sig/kino.rbs +3 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 591ec1716d7d8f23c1483ba2db85f517c42da5de78343c938a27d3e00ca1a66a
|
|
4
|
+
data.tar.gz: 91a95efe00e8193be3792c4e2b8055f1b45e2b1a4dcb6acaa1a1a4525300f878
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed8d00148990ce34bd73308b376f342b011de37ccf39a078a8676183b0689ad1762af4dc038d6baf2b0501cdb8556a5448e37e0f7ab0c8c755d4e719ceca62c6
|
|
7
|
+
data.tar.gz: 8305703e2c93bfbe4a00944a4773213f0414459bfdc2e85f963736767a382d85437889d9b216a845df2e314c4d339e0ddd40aebb5b6689b413c16cdf04e4cc87
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
## [0.6.0] - 2026-09-01
|
|
2
|
+
|
|
3
|
+
- HTTP/2 support. Kino now speaks HTTP/2 natively, on by default:
|
|
4
|
+
browsers negotiate it over TLS (ALPN), h2-preferring balancers and
|
|
5
|
+
tools get prior-knowledge h2c on plaintext, and everyone else keeps
|
|
6
|
+
getting HTTP/1.1 on the same port. `http2 false` restores an
|
|
7
|
+
HTTP/1-only server. What ships with it:
|
|
8
|
+
- A spec-complete Rack env on h2: `SERVER_PROTOCOL` is `"HTTP/2"`,
|
|
9
|
+
`HTTP_HOST`/`SERVER_NAME`/`SERVER_PORT` come from the `:authority`
|
|
10
|
+
pseudo-header, split cookie headers are rejoined, and uploads
|
|
11
|
+
stream with the same backpressure as HTTP/1.
|
|
12
|
+
- Uploads at full speed: body reads now drain every arrived chunk in
|
|
13
|
+
one native call, so h2's 16 KB data frames don't pay a GVL
|
|
14
|
+
round-trip each—h2 uploads run at HTTP/1 parity, and chunked
|
|
15
|
+
HTTP/1 uploads got faster too.
|
|
16
|
+
- `MAX_CONCURRENT_STREAMS` advertised from worker-slot capacity, so
|
|
17
|
+
an h2-aware balancer sees the server's real admission and a single
|
|
18
|
+
connection cannot multiply into hundreds of queued requests.
|
|
19
|
+
- Graceful connection drains: on shutdown, every connection finishes
|
|
20
|
+
its in-flight request and then closes (`Connection: close` on
|
|
21
|
+
HTTP/1, GOAWAY on h2) instead of being cut mid-stream at the
|
|
22
|
+
deadline.
|
|
23
|
+
- Interned header values: low-cardinality headers (user-agent,
|
|
24
|
+
accept-*, sec-ch-*) reuse one frozen string instead of allocating
|
|
25
|
+
per request—the env-side analogue of HPACK's wire dedup, worth a
|
|
26
|
+
few percent on header-heavy traffic and 6-8 fewer allocations per
|
|
27
|
+
request, on HTTP/1 too. Cookies and authorization are deliberately
|
|
28
|
+
never cached.
|
|
29
|
+
|
|
30
|
+
Measured on the reference box: native h2 is +79% over HTTP/1.1
|
|
31
|
+
cleartext and 2× over TLS on the same server, +50% over
|
|
32
|
+
nginx-terminated h2 in front of Kino, ~3× falcon on fast handlers.
|
|
33
|
+
- Leaner request hot path: up to 2-4% more throughput on fast handlers
|
|
34
|
+
with `lanes` (measured on Linux), no change elsewhere.
|
|
35
|
+
- All benchmarks re-measured from scratch on a fresh reference box
|
|
36
|
+
(c7a.2xlarge, Ruby 4.0.6, Puma 8.0.2): the 2026-06 numbers
|
|
37
|
+
reproduced within 1-3% at matched configurations, memory to the
|
|
38
|
+
megabyte. The published tables carry the new run, with two additions
|
|
39
|
+
and one honest downgrade: new studies for sharded I/O (+1-3% on 8
|
|
40
|
+
cores, best at `io_threads 8`) and HTTP/2 (above), and a narrower
|
|
41
|
+
ractor /cpu lead over the cluster (+25%, was +34%) because Puma
|
|
42
|
+
8.0.2 itself got faster. The bench harness gained the sharded-I/O
|
|
43
|
+
lanes and the Rails memory comparison, and the methodology notes now
|
|
44
|
+
record two measurement traps: memory is only comparable after the
|
|
45
|
+
full endpoint battery, and /io only at equal slot counts.
|
|
46
|
+
|
|
47
|
+
## [0.5.0] - 2026-08-29
|
|
48
|
+
|
|
49
|
+
- Add opt-in `io_shards true`: accepted HTTP connections can run on
|
|
50
|
+
current-thread Tokio I/O shards instead of Tokio's shared multi-thread
|
|
51
|
+
runtime, reducing scheduler contention on very fast handlers. `io_threads`
|
|
52
|
+
sets the shard count; otherwise Kino uses half the available CPUs
|
|
53
|
+
([Patrik Wenger](https://github.com/paddor)).
|
|
54
|
+
- Update Rust dependencies: hyper 1.11.1, rb-sys 0.9.130,
|
|
55
|
+
rustls-webpki 0.103.15, among others.
|
|
56
|
+
|
|
1
57
|
## [0.4.0] - 2026-08-22
|
|
2
58
|
|
|
3
59
|
- Rack handler: `rails server -u kino` and `rackup -s kino` boot Kino
|
data/Cargo.lock
CHANGED
|
@@ -80,9 +80,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|
|
80
80
|
|
|
81
81
|
[[package]]
|
|
82
82
|
name = "cc"
|
|
83
|
-
version = "1.4.
|
|
83
|
+
version = "1.4.4"
|
|
84
84
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
85
|
-
checksum = "
|
|
85
|
+
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
|
|
86
86
|
dependencies = [
|
|
87
87
|
"find-msvc-tools",
|
|
88
88
|
"shlex 2.0.1",
|
|
@@ -147,9 +147,9 @@ dependencies = [
|
|
|
147
147
|
|
|
148
148
|
[[package]]
|
|
149
149
|
name = "either"
|
|
150
|
-
version = "1.
|
|
150
|
+
version = "1.18.0"
|
|
151
151
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
152
|
-
checksum = "
|
|
152
|
+
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
|
153
153
|
|
|
154
154
|
[[package]]
|
|
155
155
|
name = "equivalent"
|
|
@@ -168,9 +168,9 @@ dependencies = [
|
|
|
168
168
|
|
|
169
169
|
[[package]]
|
|
170
170
|
name = "find-msvc-tools"
|
|
171
|
-
version = "0.1.
|
|
171
|
+
version = "0.1.11"
|
|
172
172
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
173
|
-
checksum = "
|
|
173
|
+
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
|
174
174
|
|
|
175
175
|
[[package]]
|
|
176
176
|
name = "flume"
|
|
@@ -184,6 +184,12 @@ dependencies = [
|
|
|
184
184
|
"spin",
|
|
185
185
|
]
|
|
186
186
|
|
|
187
|
+
[[package]]
|
|
188
|
+
name = "fnv"
|
|
189
|
+
version = "1.0.7"
|
|
190
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
191
|
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
192
|
+
|
|
187
193
|
[[package]]
|
|
188
194
|
name = "foldhash"
|
|
189
195
|
version = "0.2.0"
|
|
@@ -253,6 +259,25 @@ version = "0.3.4"
|
|
|
253
259
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
254
260
|
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
|
255
261
|
|
|
262
|
+
[[package]]
|
|
263
|
+
name = "h2"
|
|
264
|
+
version = "0.4.19"
|
|
265
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
266
|
+
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
|
|
267
|
+
dependencies = [
|
|
268
|
+
"atomic-waker",
|
|
269
|
+
"bytes",
|
|
270
|
+
"fnv",
|
|
271
|
+
"futures-core",
|
|
272
|
+
"futures-sink",
|
|
273
|
+
"http",
|
|
274
|
+
"indexmap",
|
|
275
|
+
"slab",
|
|
276
|
+
"tokio",
|
|
277
|
+
"tokio-util",
|
|
278
|
+
"tracing",
|
|
279
|
+
]
|
|
280
|
+
|
|
256
281
|
[[package]]
|
|
257
282
|
name = "hashbrown"
|
|
258
283
|
version = "0.17.1"
|
|
@@ -311,14 +336,15 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|
|
311
336
|
|
|
312
337
|
[[package]]
|
|
313
338
|
name = "hyper"
|
|
314
|
-
version = "1.11.
|
|
339
|
+
version = "1.11.1"
|
|
315
340
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
316
|
-
checksum = "
|
|
341
|
+
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
|
|
317
342
|
dependencies = [
|
|
318
343
|
"atomic-waker",
|
|
319
344
|
"bytes",
|
|
320
345
|
"futures-channel",
|
|
321
346
|
"futures-core",
|
|
347
|
+
"h2",
|
|
322
348
|
"http",
|
|
323
349
|
"http-body",
|
|
324
350
|
"httparse",
|
|
@@ -327,6 +353,7 @@ dependencies = [
|
|
|
327
353
|
"pin-project-lite",
|
|
328
354
|
"smallvec",
|
|
329
355
|
"tokio",
|
|
356
|
+
"want",
|
|
330
357
|
]
|
|
331
358
|
|
|
332
359
|
[[package]]
|
|
@@ -343,6 +370,16 @@ dependencies = [
|
|
|
343
370
|
"tokio",
|
|
344
371
|
]
|
|
345
372
|
|
|
373
|
+
[[package]]
|
|
374
|
+
name = "indexmap"
|
|
375
|
+
version = "2.14.1"
|
|
376
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
377
|
+
checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
|
|
378
|
+
dependencies = [
|
|
379
|
+
"equivalent",
|
|
380
|
+
"hashbrown",
|
|
381
|
+
]
|
|
382
|
+
|
|
346
383
|
[[package]]
|
|
347
384
|
name = "itertools"
|
|
348
385
|
version = "0.13.0"
|
|
@@ -407,7 +444,7 @@ dependencies = [
|
|
|
407
444
|
|
|
408
445
|
[[package]]
|
|
409
446
|
name = "kino"
|
|
410
|
-
version = "0.
|
|
447
|
+
version = "0.6.0"
|
|
411
448
|
dependencies = [
|
|
412
449
|
"ahash",
|
|
413
450
|
"bytes",
|
|
@@ -471,15 +508,15 @@ dependencies = [
|
|
|
471
508
|
|
|
472
509
|
[[package]]
|
|
473
510
|
name = "log"
|
|
474
|
-
version = "0.4.
|
|
511
|
+
version = "0.4.34"
|
|
475
512
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
476
|
-
checksum = "
|
|
513
|
+
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
|
477
514
|
|
|
478
515
|
[[package]]
|
|
479
516
|
name = "lru"
|
|
480
|
-
version = "0.18.
|
|
517
|
+
version = "0.18.3"
|
|
481
518
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
482
|
-
checksum = "
|
|
519
|
+
checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c"
|
|
483
520
|
dependencies = [
|
|
484
521
|
"hashbrown",
|
|
485
522
|
]
|
|
@@ -631,18 +668,18 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|
|
631
668
|
|
|
632
669
|
[[package]]
|
|
633
670
|
name = "rb-sys"
|
|
634
|
-
version = "0.9.
|
|
671
|
+
version = "0.9.130"
|
|
635
672
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
636
|
-
checksum = "
|
|
673
|
+
checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
|
|
637
674
|
dependencies = [
|
|
638
675
|
"rb-sys-build",
|
|
639
676
|
]
|
|
640
677
|
|
|
641
678
|
[[package]]
|
|
642
679
|
name = "rb-sys-build"
|
|
643
|
-
version = "0.9.
|
|
680
|
+
version = "0.9.130"
|
|
644
681
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
645
|
-
checksum = "
|
|
682
|
+
checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
|
|
646
683
|
dependencies = [
|
|
647
684
|
"bindgen",
|
|
648
685
|
"lazy_static",
|
|
@@ -752,9 +789,9 @@ dependencies = [
|
|
|
752
789
|
|
|
753
790
|
[[package]]
|
|
754
791
|
name = "rustls-webpki"
|
|
755
|
-
version = "0.103.
|
|
792
|
+
version = "0.103.15"
|
|
756
793
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
757
|
-
checksum = "
|
|
794
|
+
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
|
758
795
|
dependencies = [
|
|
759
796
|
"ring",
|
|
760
797
|
"rustls-pki-types",
|
|
@@ -796,7 +833,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|
|
796
833
|
dependencies = [
|
|
797
834
|
"proc-macro2",
|
|
798
835
|
"quote",
|
|
799
|
-
"syn 3.0.
|
|
836
|
+
"syn 3.0.4",
|
|
800
837
|
]
|
|
801
838
|
|
|
802
839
|
[[package]]
|
|
@@ -817,6 +854,12 @@ version = "2.0.1"
|
|
|
817
854
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
818
855
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
819
856
|
|
|
857
|
+
[[package]]
|
|
858
|
+
name = "slab"
|
|
859
|
+
version = "0.4.12"
|
|
860
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
861
|
+
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|
862
|
+
|
|
820
863
|
[[package]]
|
|
821
864
|
name = "smallvec"
|
|
822
865
|
version = "1.15.2"
|
|
@@ -861,9 +904,9 @@ dependencies = [
|
|
|
861
904
|
|
|
862
905
|
[[package]]
|
|
863
906
|
name = "syn"
|
|
864
|
-
version = "3.0.
|
|
907
|
+
version = "3.0.4"
|
|
865
908
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
866
|
-
checksum = "
|
|
909
|
+
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
|
867
910
|
dependencies = [
|
|
868
911
|
"proc-macro2",
|
|
869
912
|
"quote",
|
|
@@ -887,7 +930,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
|
|
887
930
|
dependencies = [
|
|
888
931
|
"proc-macro2",
|
|
889
932
|
"quote",
|
|
890
|
-
"syn 3.0.
|
|
933
|
+
"syn 3.0.4",
|
|
891
934
|
]
|
|
892
935
|
|
|
893
936
|
[[package]]
|
|
@@ -913,7 +956,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
|
|
913
956
|
dependencies = [
|
|
914
957
|
"proc-macro2",
|
|
915
958
|
"quote",
|
|
916
|
-
"syn 3.0.
|
|
959
|
+
"syn 3.0.4",
|
|
917
960
|
]
|
|
918
961
|
|
|
919
962
|
[[package]]
|
|
@@ -926,6 +969,45 @@ dependencies = [
|
|
|
926
969
|
"tokio",
|
|
927
970
|
]
|
|
928
971
|
|
|
972
|
+
[[package]]
|
|
973
|
+
name = "tokio-util"
|
|
974
|
+
version = "0.7.19"
|
|
975
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
976
|
+
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
|
977
|
+
dependencies = [
|
|
978
|
+
"bytes",
|
|
979
|
+
"futures-core",
|
|
980
|
+
"futures-sink",
|
|
981
|
+
"libc",
|
|
982
|
+
"pin-project-lite",
|
|
983
|
+
"tokio",
|
|
984
|
+
]
|
|
985
|
+
|
|
986
|
+
[[package]]
|
|
987
|
+
name = "tracing"
|
|
988
|
+
version = "0.1.44"
|
|
989
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
990
|
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
|
991
|
+
dependencies = [
|
|
992
|
+
"pin-project-lite",
|
|
993
|
+
"tracing-core",
|
|
994
|
+
]
|
|
995
|
+
|
|
996
|
+
[[package]]
|
|
997
|
+
name = "tracing-core"
|
|
998
|
+
version = "0.1.36"
|
|
999
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1000
|
+
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
|
1001
|
+
dependencies = [
|
|
1002
|
+
"once_cell",
|
|
1003
|
+
]
|
|
1004
|
+
|
|
1005
|
+
[[package]]
|
|
1006
|
+
name = "try-lock"
|
|
1007
|
+
version = "0.2.5"
|
|
1008
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1009
|
+
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
|
1010
|
+
|
|
929
1011
|
[[package]]
|
|
930
1012
|
name = "unicode-ident"
|
|
931
1013
|
version = "1.0.24"
|
|
@@ -944,6 +1026,15 @@ version = "0.9.5"
|
|
|
944
1026
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
945
1027
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|
946
1028
|
|
|
1029
|
+
[[package]]
|
|
1030
|
+
name = "want"
|
|
1031
|
+
version = "0.3.1"
|
|
1032
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1033
|
+
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
|
1034
|
+
dependencies = [
|
|
1035
|
+
"try-lock",
|
|
1036
|
+
]
|
|
1037
|
+
|
|
947
1038
|
[[package]]
|
|
948
1039
|
name = "wasi"
|
|
949
1040
|
version = "0.11.1+wasi-snapshot-preview1"
|
data/README.md
CHANGED
|
@@ -11,11 +11,14 @@ on every core in **one small process**. A **Rust** (tokio + hyper)
|
|
|
11
11
|
front-end owns the network, parallel **Ractors** run your Rack 3 app,
|
|
12
12
|
and a threaded fallback mode runs everything else, Rails included.
|
|
13
13
|
|
|
14
|
-
* **Fast.** On a real 8-core server, every Kino mode is **1.5-
|
|
15
|
-
ahead of a Puma fork cluster on I/O-light endpoints
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
* **Fast.** On a real 8-core server, every Kino mode is **1.5-1.7×**
|
|
15
|
+
ahead of a Puma fork cluster on I/O-light endpoints—with native
|
|
16
|
+
**HTTP/2** adding another **+79%** over HTTP/1.1 on the same server.
|
|
17
|
+
Ractor mode also wins on pure CPU, **+25%**.
|
|
18
|
+
[Benchmarks](#benchmarks) below.
|
|
19
|
+
* **A fraction of the memory.** About **~8×** on the simplistic bench
|
|
20
|
+
Ractor app, and about **4× less memory** than a Puma cluster serving
|
|
21
|
+
Rails in fallback threaded mode.
|
|
19
22
|
* **Parallel without forking.** Ractor mode runs CPU work **more than
|
|
20
23
|
5× faster** than Kino's own GVL-bound threaded mode, in the same
|
|
21
24
|
small process.
|
|
@@ -73,32 +76,32 @@ rather than an app. It is Ractor-shareable, so Kino runs it in `:ractor`
|
|
|
73
76
|
mode (and `:threaded` for comparison). **A real Rails app is a different
|
|
74
77
|
story:** it is *not* Ractor-shareable, so it runs only in Kino's
|
|
75
78
|
`:threaded` fallback, with its own numbers—see [Rails](#rails) below.
|
|
76
|
-
Ruby 4.0.
|
|
77
|
-
3 threads, Kino stays in one process
|
|
78
|
-
modes, 3 in threaded). Numbers are
|
|
79
|
-
connections, same host).
|
|
80
|
-
[doc/benchmarks.md](doc/benchmarks.md).
|
|
79
|
+
Ruby 4.0.6 with YJIT (re-measured 2026-09), every server at its
|
|
80
|
+
defaults: Puma forks 8 workers × 3 threads, Kino stays in one process
|
|
81
|
+
(8 workers; 1 thread each in ractor modes, 3 in threaded). Numbers are
|
|
82
|
+
req/s by wrk (8-second windows, 64 connections, same host).
|
|
83
|
+
Methodology: [doc/benchmarks.md](doc/benchmarks.md).
|
|
81
84
|
|
|
82
85
|
| endpoint | Kino :ractor | + lanes | :ractor, `workers 32`² | Kino :threaded | Puma (cluster) |
|
|
83
86
|
|-------------|-------------:|--------:|-----------------------:|---------------:|---------------:|
|
|
84
|
-
| /plaintext |
|
|
85
|
-
| /10k |
|
|
86
|
-
| /cpu (fib) | **
|
|
87
|
-
| /io (5 ms) | 1,
|
|
88
|
-
| /io_native | 1,570 | 1,
|
|
87
|
+
| /plaintext | 222,980 | **244,652** | 115,553 | 213,104 | 142,094 |
|
|
88
|
+
| /10k | 175,182 | **188,365** | 103,147 | 132,009 | 125,116 |
|
|
89
|
+
| /cpu (fib) | **76,695**¹| 71,830 | 54,062 | 13,463 | 61,329 |
|
|
90
|
+
| /io (5 ms) | 1,548 | 1,556 | **5,388** | 4,722 | 4,699 |
|
|
91
|
+
| /io_native | 1,570 | 1,573 | **6,207** | 4,695 | 4,693 |
|
|
89
92
|
|
|
90
93
|
Memory tells two different stories depending on the app, both by **PSS**
|
|
91
94
|
(proportional set size; see note) after sustained load.
|
|
92
95
|
|
|
93
96
|
**The tiny benchmark app** (Ractor-shareable, so Kino runs it in `:ractor`
|
|
94
|
-
or `:threaded`). Kino is **~
|
|
95
|
-
than the Puma cluster
|
|
97
|
+
or `:threaded`). Kino is **~8× lighter in :ractor mode, ~10× in :threaded**
|
|
98
|
+
than the Puma cluster—the gap stays large because a trivial app is almost
|
|
96
99
|
all private per-worker heap, which copy-on-write can't share:
|
|
97
100
|
|
|
98
101
|
| tiny app, Kino | Kino (one process) | Puma cluster (8 workers) | ratio |
|
|
99
102
|
|-----------------|-------------------:|-------------------------:|------:|
|
|
100
|
-
| :ractor (8×1) | **
|
|
101
|
-
| :threaded (8×3) | **107 MB**³| 1,
|
|
103
|
+
| :ractor (8×1) | **135 MB** | 1,072 MB | ~8× |
|
|
104
|
+
| :threaded (8×3) | **107 MB**³| 1,072 MB | ~10× |
|
|
102
105
|
|
|
103
106
|
**A real Rails app** (not Ractor-shareable—Kino's `:threaded` fallback
|
|
104
107
|
only, [below](#rails)). The gap is **~4×**, smaller because Rails' large
|
|
@@ -106,24 +109,25 @@ framework *is* shared copy-on-write across Puma's forks:
|
|
|
106
109
|
|
|
107
110
|
| Rails hello-world | Kino :threaded | Puma cluster (8 workers) | ratio |
|
|
108
111
|
|-------------------|---------------:|-------------------------:|------:|
|
|
109
|
-
| **PSS** | **
|
|
112
|
+
| **PSS** | **95 MB** | **405 MB** | ~4× |
|
|
110
113
|
|
|
111
114
|
"+ lanes" is the experimental per-worker-queue dispatcher (`lanes true`).
|
|
112
115
|
It posts the fastest plaintext/10k of any configuration here. Details:
|
|
113
116
|
[doc/benchmarks.md](doc/benchmarks.md#lane-dispatch-experimental-lanes-true).
|
|
114
117
|
|
|
115
118
|
¹ Stock settings, no tuning. Ractor mode beats the fork cluster on pure
|
|
116
|
-
CPU by +
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
`
|
|
119
|
+
CPU by +25% (+17% with lanes; Puma 8.0.2 narrowed this from the +34%
|
|
120
|
+
measured against 7.x). Threaded mode shows the GVL ceiling that every
|
|
121
|
+
single-process Ruby server hits. The old CPU-tuning recipe stays
|
|
122
|
+
retired: its `threads 1` half **is** the default, and its
|
|
123
|
+
`tokio_threads 1` half still costs on real hardware; see
|
|
120
124
|
[doc/benchmarks.md](doc/benchmarks.md#cpu-bound-tuning).
|
|
121
125
|
|
|
122
126
|
² Wait-bound throughput is slots ÷ wait, and the default columns bring
|
|
123
127
|
8 single-thread workers against the cluster's 24 threads. Kino slots
|
|
124
128
|
are threads, not processes—when your app waits a lot, raise `workers`.
|
|
125
|
-
The `workers 32` column is that tuning: **+
|
|
126
|
-
(+
|
|
129
|
+
The `workers 32` column is that tuning: **+15% over the cluster on /io
|
|
130
|
+
(+32% via `Kino.sleep`)** while still ahead of it on pure CPU, all in
|
|
127
131
|
one small process. The cost is the CPU-light rows (32 ractors
|
|
128
132
|
oversubscribe 8 cores); pick the topology your app's wait profile
|
|
129
133
|
needs. See
|
|
@@ -131,7 +135,7 @@ needs. See
|
|
|
131
135
|
|
|
132
136
|
³ With `MALLOC_ARENA_MAX=2` (the standard Ruby deployment setting;
|
|
133
137
|
Heroku's default). Without it, 24 threads churning 10 KB responses
|
|
134
|
-
through one glibc heap balloon to ~
|
|
138
|
+
through one glibc heap balloon to ~647 MB—an arena-fragmentation
|
|
135
139
|
footgun, not a leak, and ractor mode sidesteps it. See
|
|
136
140
|
[doc/benchmarks.md](doc/benchmarks.md#memory-under-load-and-the-glibc-arena-footgun).
|
|
137
141
|
|
|
@@ -141,36 +145,64 @@ doc):
|
|
|
141
145
|
|
|
142
146
|
| endpoint | Kino :ractor (8×3) | Puma + ractor wrapper | Falcon + ractor wrapper |
|
|
143
147
|
|------------|-------------------:|----------------------:|------------------------:|
|
|
144
|
-
| /plaintext | **
|
|
145
|
-
| /cpu (fib) | **68,
|
|
146
|
-
| /io (5 ms) | **4,
|
|
148
|
+
| /plaintext | **190,206** | 22,055 | 107,203 |
|
|
149
|
+
| /cpu (fib) | **68,230** | 16,528 | 50,101 |
|
|
150
|
+
| /io (5 ms) | **4,477** | 1,482 | 1,545 |
|
|
151
|
+
|
|
152
|
+
(The Kino column here runs 8×3—the same 24 slots the wrappers get—so
|
|
153
|
+
the /io row is comparable; the main table's ractor column runs the 8×1
|
|
154
|
+
default.)
|
|
155
|
+
|
|
156
|
+
### HTTP/2 head-to-head
|
|
157
|
+
|
|
158
|
+
Kino speaks HTTP/2 natively, so it skips the usual nginx-termination
|
|
159
|
+
hop. Same box as the tables above; all over TLS, h2load, 64 in-flight,
|
|
160
|
+
same app:
|
|
161
|
+
|
|
162
|
+
| /plaintext | req/s | /upload (64 KB) |
|
|
163
|
+
|-------------------------------|------------:|----------------:|
|
|
164
|
+
| Kino native h2 | **164,044** | **19,226** |
|
|
165
|
+
| Kino HTTP/1.1 (same boot) | 80,789 | 17,210 |
|
|
166
|
+
| nginx h2 → Kino HTTP/1.1 | 109,219 | 1,217¹ |
|
|
167
|
+
| Falcon (native h2) | 55,637 | 18,838 |
|
|
168
|
+
|
|
169
|
+
Native h2 **doubles** HTTP/1.1 throughput on the same TLS server
|
|
170
|
+
(+79% on cleartext h2c; fewer, larger socket operations, and HPACK
|
|
171
|
+
spares re-sending cookies) and is **+50% over fronting the same Kino
|
|
172
|
+
with nginx**—the proxy hop is pure cost. Uploads, h2's classic weak
|
|
173
|
+
spot, run at HTTP/1 parity. Full matrix, cleartext h2c lanes, and
|
|
174
|
+
methodology: [doc/benchmarks.md](doc/benchmarks.md#http2); reproduce
|
|
175
|
+
with `bench/h2.sh`.
|
|
176
|
+
|
|
177
|
+
¹ nginx default-config h2 request-body flow control; see the doc.
|
|
147
178
|
|
|
148
179
|
### Rails
|
|
149
180
|
|
|
150
181
|
Rails is not Ractor-shareable today, so Kino serves it in `:threaded`
|
|
151
|
-
fallback
|
|
182
|
+
fallback—one GVL-bound process. On the same box (`examples/rails-hello`,
|
|
152
183
|
edge Rails, production, 8×5):
|
|
153
184
|
|
|
154
185
|
| Rails hello-world | req/s | memory (PSS) |
|
|
155
186
|
|------------------------------|-------:|-------------:|
|
|
156
|
-
| Kino :threaded (one process) | 2,
|
|
157
|
-
| Puma cluster (8 workers) |
|
|
187
|
+
| Kino :threaded (one process) | 2,731 | **95 MB** |
|
|
188
|
+
| Puma cluster (8 workers) | 15,422 | 405 MB |
|
|
158
189
|
|
|
159
190
|
The honest trade-off: Puma's fork cluster uses all 8 cores, so it serves
|
|
160
|
-
~
|
|
191
|
+
~5.6× the throughput—at ~4× the memory. Ractor-mode Rails would close
|
|
161
192
|
the throughput gap at one-process memory cost; the upstream blockers are
|
|
162
193
|
tracked in [doc/rails-on-ractors.md](doc/rails-on-ractors.md).
|
|
163
194
|
|
|
164
|
-
In short: on the tiny synthetic app, ractor mode beats fork-level CPU parallelism (**5.
|
|
165
|
-
own GVL-bound threaded mode, +
|
|
166
|
-
about 1/
|
|
167
|
-
Every Kino mode is 1.5-
|
|
195
|
+
In short: on the tiny synthetic app, ractor mode beats fork-level CPU parallelism (**5.7×** Kino's
|
|
196
|
+
own GVL-bound threaded mode, +25% over the cluster) in one process, at
|
|
197
|
+
about 1/8th of the cluster's memory by PSS (~4× on a real Rails app).
|
|
198
|
+
Every Kino mode is 1.5-1.7× ahead of the cluster on I/O-light endpoints,
|
|
199
|
+
and native HTTP/2 adds +79% over HTTP/1.1 on top. The macOS numbers
|
|
168
200
|
(secondary; everything there hits the loopback ceiling) and the
|
|
169
201
|
YJIT × Ractors gotcha are in [doc/benchmarks.md](doc/benchmarks.md).
|
|
170
202
|
|
|
171
203
|
Reproduce: `bench/run.sh [seconds] [concurrency]` for the main table,
|
|
172
204
|
`bench/studies.sh` for the follow-ups (CPU recipe, topology, scaling,
|
|
173
|
-
logging, memory).
|
|
205
|
+
sharded I/O, logging, memory), `bench/h2.sh` for the HTTP/2 matrix.
|
|
174
206
|
|
|
175
207
|
## Install
|
|
176
208
|
|
|
@@ -239,6 +271,7 @@ server = Kino::Server.new(app,
|
|
|
239
271
|
control_bind: "127.0.0.1:9293", # monitoring: /stats /metrics /ready /live; port 0 reads back via server.control_port
|
|
240
272
|
control_token: ENV["KINO_CONTROL_TOKEN"], # optional Bearer auth for /stats + /metrics
|
|
241
273
|
tls: { cert: "cert.pem", key: "key.pem" }, # file paths or inline PEM
|
|
274
|
+
http2: true, # ALPN h2 on TLS + plaintext h2c; false = HTTP/1 only
|
|
242
275
|
)
|
|
243
276
|
server.start
|
|
244
277
|
server.shutdown # graceful: drain → deadline → abort stragglers
|
|
@@ -259,6 +292,45 @@ server.shutdown # graceful: drain → deadline → abort straggler
|
|
|
259
292
|
always counts as "shareable" (classes are), even if calling it touches
|
|
260
293
|
unshareable state. Force `:threaded` for those.
|
|
261
294
|
|
|
295
|
+
### Sharded I/O
|
|
296
|
+
|
|
297
|
+
`io_shards true` (off by default) moves HTTP I/O from Tokio's shared
|
|
298
|
+
multi-thread runtime onto current-thread shards: one thread accepts and
|
|
299
|
+
hands each connection to the least-loaded shard, which then owns it for
|
|
300
|
+
its lifetime—no work-stealing, no cross-thread wakeups on the hot path.
|
|
301
|
+
On the 8-core reference box fast handlers gain +1-3% (best with
|
|
302
|
+
`io_threads 8`); the win grows with scheduler contention, so measure on
|
|
303
|
+
your own core count. Ruby-bound endpoints are unchanged. Orthogonal to
|
|
304
|
+
`mode`: it reshapes the Rust side only.
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
# kino.rb
|
|
308
|
+
io_shards true
|
|
309
|
+
io_threads 8 # optional; default: half the available CPUs
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### HTTP/2
|
|
313
|
+
|
|
314
|
+
On by default, on both transports, with nothing to configure:
|
|
315
|
+
|
|
316
|
+
- **TLS binds** advertise `h2` via ALPN, so browsers and h2-capable
|
|
317
|
+
clients negotiate HTTP/2 and everything else stays on HTTP/1.1.
|
|
318
|
+
- **Plaintext binds** serve prior-knowledge h2c: a client that opens
|
|
319
|
+
with the HTTP/2 preface (an h2-preferring load balancer, a gRPC-style
|
|
320
|
+
backend hop, `curl --http2-prior-knowledge`) gets HTTP/2; ordinary
|
|
321
|
+
clients are HTTP/1.1 exactly as before. Browsers never do h2 on
|
|
322
|
+
plaintext, so a certificate-less kino behaves identically for them.
|
|
323
|
+
|
|
324
|
+
The Rack side is spec-complete on h2: `SERVER_PROTOCOL` is `"HTTP/2"`,
|
|
325
|
+
`HTTP_HOST`/`SERVER_NAME`/`SERVER_PORT` come from the `:authority`
|
|
326
|
+
pseudo-header (h2 requests carry no Host header), split cookie headers
|
|
327
|
+
are rejoined with `"; "`, and streamed uploads flow through the same
|
|
328
|
+
backpressured body channel as HTTP/1. Streams multiplex into the same
|
|
329
|
+
worker slots as keep-alive requests—`workers × threads` bounds
|
|
330
|
+
concurrency either way. (`rack.hijack` stays out on h2 as it is on h1;
|
|
331
|
+
the protocol has no 101 upgrade to hijack anyway.) `http2 false` pins
|
|
332
|
+
the server to HTTP/1 and drops `h2` from ALPN.
|
|
333
|
+
|
|
262
334
|
## Config file and CLI
|
|
263
335
|
|
|
264
336
|
Settings can live in a Puma-style Ruby DSL file: `kino.rb` in the
|
|
@@ -424,8 +496,8 @@ not `queue_time`.
|
|
|
424
496
|
|
|
425
497
|
## Logging
|
|
426
498
|
|
|
427
|
-
With one log line per request, `Kino::Logger` sustained **
|
|
428
|
-
throughput of a shared `::Logger`** (
|
|
499
|
+
With one log line per request, `Kino::Logger` sustained **1.7× the
|
|
500
|
+
throughput of a shared `::Logger`** (155k vs 90k req/s on the benchmark
|
|
429
501
|
box). There are two native pieces. Both write through a lock-free
|
|
430
502
|
channel to a Rust flusher thread, so request threads never take a log
|
|
431
503
|
mutex and never make a write syscall:
|