Skip to content

Commit 83fdc6e

Browse files
fix: switch to shell-based multi-process for proper multi-core scaling
Crystal's Process.fork doesn't work reliably in Docker containers. Switch to LD_PRELOAD SO_REUSEPORT shim + shell wrapper spawning N independent processes (same approach as other entries).
1 parent 44286d9 commit 83fdc6e

4 files changed

Lines changed: 54 additions & 41 deletions

File tree

frameworks/kemal/Dockerfile

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ FROM crystallang/crystal:1.14-alpine AS builder
33

44
WORKDIR /app
55

6-
# Install SQLite dev headers
7-
RUN apk add --no-cache sqlite-dev sqlite-static
6+
# Install SQLite dev headers and gcc for reuseport shim
7+
RUN apk add --no-cache sqlite-dev sqlite-static gcc musl-dev
8+
9+
# Build SO_REUSEPORT shim
10+
COPY reuseport.c /tmp/reuseport.c
11+
RUN gcc -shared -fPIC -o /tmp/libreuseport.so /tmp/reuseport.c -ldl
812

913
# Install dependencies
1014
COPY shard.yml shard.lock ./
@@ -14,14 +18,18 @@ RUN shards install --production
1418
COPY src/ src/
1519
RUN crystal build src/server.cr -o server --release --no-debug
1620

17-
# Runtime stage — need same libc as builder (musl on Alpine)
21+
# Runtime stage
1822
FROM alpine:3.20
1923

2024
RUN apk add --no-cache ca-certificates libgcc gc libevent sqlite-libs pcre2
2125

2226
WORKDIR /app
2327
COPY --from=builder /app/server /app/server
28+
COPY --from=builder /tmp/libreuseport.so /usr/lib/libreuseport.so
29+
COPY run.sh /app/run.sh
30+
RUN chmod +x /app/run.sh
2431

2532
EXPOSE 8080
2633

27-
CMD ["/app/server"]
34+
ENV LD_PRELOAD=/usr/lib/libreuseport.so
35+
CMD ["/app/run.sh"]

frameworks/kemal/reuseport.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* LD_PRELOAD shim: set SO_REUSEPORT on every SOCK_STREAM bind() */
2+
#define _GNU_SOURCE
3+
#include <dlfcn.h>
4+
#include <sys/socket.h>
5+
#include <sys/types.h>
6+
7+
typedef int (*bind_fn)(int, const struct sockaddr *, socklen_t);
8+
9+
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
10+
int optval = 1;
11+
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
12+
bind_fn real_bind = (bind_fn)dlsym(RTLD_NEXT, "bind");
13+
return real_bind(sockfd, addr, addrlen);
14+
}

frameworks/kemal/run.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/sh
2+
# Multi-process launcher: one worker per CPU core with SO_REUSEPORT
3+
NPROC=$(nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo 2>/dev/null || echo 1)
4+
5+
if [ "$NPROC" -le 1 ]; then
6+
exec /app/server
7+
fi
8+
9+
PIDS=""
10+
cleanup() {
11+
for pid in $PIDS; do
12+
kill "$pid" 2>/dev/null
13+
done
14+
wait
15+
exit 0
16+
}
17+
trap cleanup INT TERM
18+
19+
i=0
20+
while [ "$i" -lt "$NPROC" ]; do
21+
/app/server &
22+
PIDS="$PIDS $!"
23+
i=$((i + 1))
24+
done
25+
26+
wait

frameworks/kemal/src/server.cr

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -187,47 +187,12 @@ post "/upload" do |env|
187187
end
188188

189189
# ---------------------------------------------------------------------------
190-
# Server config — multi-core via SO_REUSEPORT + fork
190+
# Server config
191191
# ---------------------------------------------------------------------------
192192

193193
Kemal.config.port = 8080
194194
Kemal.config.env = "production"
195-
196-
# Disable Kemal's default signal handlers and logging for workers
197195
Kemal.config.shutdown_message = false
198196
Kemal.config.logging = false
199197

200-
worker_count = System.cpu_count.to_i
201-
worker_count = 1 if worker_count < 1
202-
203-
def start_server
204-
# Build Kemal's handler chain manually
205-
Kemal.config.setup
206-
207-
# Register 404 handler (normally done by Kemal.run)
208-
unless Kemal.config.error_handlers.has_key?(404)
209-
error 404 do
210-
render_404
211-
end
212-
end
213-
214-
handlers = Kemal.config.handlers
215-
216-
# Create server with SO_REUSEPORT
217-
server = HTTP::Server.new(handlers)
218-
server.bind_tcp("0.0.0.0", 8080, reuse_port: true)
219-
server.listen
220-
end
221-
222-
if worker_count > 1
223-
pids = [] of Int64
224-
worker_count.times do
225-
child = Process.fork { start_server }
226-
pids << child.pid
227-
end
228-
Signal::INT.trap { pids.each { |p| Process.signal(Signal::TERM, p) rescue nil }; exit }
229-
Signal::TERM.trap { pids.each { |p| Process.signal(Signal::TERM, p) rescue nil }; exit }
230-
sleep
231-
else
232-
start_server
233-
end
198+
Kemal.run

0 commit comments

Comments
 (0)