Memory Profiling¶
Numa container memory profiling¶
This section explores attaching bytehound (heap profiler via LD_PRELOAD)
mem profiler to the numaflow-core process in the numa main container and captures a .dat for analysis offline.
The stock numaflow image cannot be profiled as-is since the Rust binary is built with -C target-feature=+crt-static
(fully static -> no dynamic loader) and the release image is FROM scratch (no ld.so/libc).
Both make LD_PRELOAD silently do nothing. We need a dynamic glibc binary on a glibc base.
Use make image-memprofile for that: it builds a dynamically linked numaflow-rs, compiles
libbytehound.so from source (amd64 and arm64), and packages them on debian:trixie-slim.
Prerequisites¶
- kubectl + permission to edit the numaflow controller Deployment (
numaflow-system). - Docker (BuildKit) to build & push a single-arch image locally, or use the
memprofile-imageGitHub Actions workflow for a multi-arch image.
Step 1 - Build a profilable numaflow image¶
Local:
# Default tag: <IMAGE_NAMESPACE>/numaflow:<VERSION>-memprofile
make image-memprofile
# Faster local rebuilds (image-dev Cargo profile) + skip clean/UI when already built:
make image-memprofile CARGO_PROFILE=image-dev SKIP_CLEAN=true SKIP_UI_BUILD=true
# Custom tag / push:
make image-memprofile MEMPROFILE_IMAGE_TAG=my-tag DOCKER_PUSH=true IMAGE_NAMESPACE=<your-registry>
CI (multi-arch): run the memprofile-image workflow (workflow_dispatch) with the branch to build.
It publishes quay.io/<org>/numaflow:<branch>-<sha7>-memprofile (amd64 + arm64 manifest).
Verify the image is dynamic and includes bytehound:
docker run --rm --entrypoint /bin/sh <IMAGE_NAMESPACE>/numaflow:<tag> -c '
ldd /bin/numaflow-rs | grep libc # must list libc.so.6, NOT "not a dynamic executable"
test -f /usr/share/libbytehound.so
'
Step 2 - Point the controller at the profiling image¶
kubectl -n <numaflow-ns> set env deploy/<controller> NUMAFLOW_IMAGE=<your-registry>/<img>:<tag>
kubectl -n <numaflow-ns> rollout restart deploy/<controller>
Step 3 - Pipeline spec: profiling env on the reduce vertex¶
- name: <vertex-name>
containerTemplate:
env:
- { name: LD_PRELOAD, value: /usr/share/libbytehound.so }
- { name: MEMORY_PROFILER_TRACK_CHILD_PROCESSES, value: "1" } # REQUIRED (see note)
- { name: MEMORY_PROFILER_OUTPUT, value: /var/numaflow/pbq/memory-profiling_%e_%t_%p.dat }
- { name: MEMORY_PROFILER_LOG, value: info }
- { name: MEMORY_PROFILER_CULL_TEMPORARY_ALLOCATIONS, value: "1" } # drop <10s allocs, shrink .dat
- { name: MEMORY_PROFILER_GRAB_BACKTRACES_ON_FREE, value: "0" } # cut overhead
Then kubectl apply -f <pipeline>.yaml && kubectl delete pod <accum-pod>.
Critical: MEMORY_PROFILER_TRACK_CHILD_PROCESSES=1. The image ENTRYPOINT is /bin/entrypoint
(a Rust dispatcher) which execves /bin/numaflow-rs. With the default (0), bytehound unsets
LD_PRELOAD during its own init in entrypoint (to avoid following children), so the exec'd
numaflow-rs starts unprofiled. Setting 1 keeps LD_PRELOAD across the exec.
Step 4 - Verify bytehound is attached¶
kubectl exec <accum-pod> -c numa -- env LD_PRELOAD= sh -c '
echo "mapped: $(grep -c bytehound /proc/1/maps)"; # expect: >=1
ldd /bin/numaflow-rs | grep libc; # expect: libc.so.6 (dynamic)
ls -la /var/numaflow/pbq/memory-profiling_numaflow-rs_*' # a growing .dat (MBs, binary embedded)
env LD_PRELOAD= clears the var for your shell/commands only.
Step 5 - Collect the .dat (do NOT use kubectl cp)¶
kubectl cp runs tar inside the pod; under TRACK_CHILD_PROCESSES=1 bytehound injects into that
tar and it SIGSEGVs. Stream it out with LD_PRELOAD cleared:
kubectl exec <accum-pod> -c numa -- \
env LD_PRELOAD= tar cf - -C /var/numaflow/pbq memory-profiling_numaflow-rs_<t>_1.dat | tar xf -
# optional clean/flushed snapshot: kill -USR1 1 before (flush+pause) and again after (resume)
kubectl exec <accum-pod> -c numa -- env LD_PRELOAD= kill -USR1 1
Step 6 - Analyze¶
Download the bytehound CLI from the bytehound releases
(prebuilt is x86_64; the image itself already embeds an arch-matched libbytehound.so built from source).
./bytehound server memory-profiling_numaflow-rs_<t>_1.dat # -> http://localhost:8080
./bytehound strip --threshold 60 -o stripped.dat <file>.dat # if the file is huge / server strains
UD container memory profiling¶
Rust SDK¶
We again use bytehound for memory profiling Rust SDK images.
Image¶
(a source/sink example - adapt to your UDF).
Drop bytehound-x86_64-unknown-linux-gnu.tgz (from link) and start.sh into the build context:
FROM rust:1.76-bookworm AS build
RUN apt-get update && apt-get install -y protobuf-compiler
WORKDIR /source-sink
COPY ./ ./
RUN cargo build --release # dynamic glibc binary - LD_PRELOAD-able
FROM debian:bookworm AS simple-source
COPY --from=build /source-sink/target/release/sourcer-sinker .
COPY --from=build /source-sink/bytehound-x86_64-unknown-linux-gnu.tgz .
COPY --from=build /source-sink/start.sh .
RUN chmod +x start.sh && tar -xvzf bytehound-x86_64-unknown-linux-gnu.tgz
ENV MEMORY_PROFILER_LOG=info
CMD ["./start.sh"]
start.sh - set LD_PRELOAD scoped to the app only:
#!/bin/bash
LD_PRELOAD=./libbytehound.so ./sourcer-sinker
Build & push¶
docker buildx create --name multiarch --driver docker-container --use # once
IMAGE_NAMESPACE=quay.io/$(whoami) DOCKER_PUSH=true VERSION=<tag> make image-multi # or plain buildx build of your SDK image
docker buildx rm multiarch # cleanup
(bytehound prebuilt is x86_64-only; for arm64 nodes build libbytehound.so from source and COPY the
arch-matching .so.)
Collect¶
kubectl cp --retries 999 <pod>:/memory-profiling_<exe>_<t>_<pid>.dat /tmp/prof.dat