The package formerly known as TACC Stats
A toolkit for monitoring resource usage on HPC systems at multiple levels of resolution.
The hpcperfstats package is split into two parts:
| Component | Build system | Role |
|---|---|---|
| monitor | Autotools | Online data collection and transmission in production |
| hpcperfstats | Python setuptools | Data curation and analysis (off-cluster) |
| Document | What it is for |
|---|---|
| MONITOR_VARIABLES.md | Canonical reference for monitor-reported variables: names, types, units, and semantics. Use this instead of any legacy “attributes definition” doc. |
| DEPLOY_CONCURRENCY_AND_NUMA.md | Thread/process limits vs PostgreSQL, total_cores / effective_cores, and pool sizing for web and pipeline. |
| design-document.md | As-built system design: architecture, data flow, components, contracts, and operations context. |
| measurements/ | Recorded ops measurements (for example node-daemon CPU overhead on Stampede3 SPR). |
| using-the-website-as-a-researcher.md | How to read the Django/React job UI—plots, metrics, and diagnostic themes—for HPC users and researchers. |
| TESTING.md | Test commands, CI, compose-backed workflows, Playwright/Vitest, and host vs container pytest notes. |
| upgrade.md | Existing stacks only: image rebuilds, Redis/INI/volume layout, RabbitMQ recreate, PG18 dual-run. |
| OPERATOR_HOST_DATA_DEV_UNIQUENESS.md | host_data 5-col uniqueness: Stage 1 decompress (+ 02 PK normalize) and Stage 2 one-shot migrate for 0032 / compress_after 8d. |
Maintaining MONITOR_VARIABLES.md: the catalog is generated and augmented by maintainer scripts in the same folder: regenerate_monitor_variables_catalog.py, augment_monitor_variables_diagnostics.py.
REST API note: GET /api/jobs/{jid}/{type_name}/ (type detail) returns a Bokeh tplot_item (json_item payload) plus stats_data / schema. Legacy tscript / tdiv fields were removed; clients should embed only tplot_item via Bokeh embed_item.
API contract changes (2026-06):
GET /api/jobs/with filters that match zero jobs now returns HTTP 200 withnj: 0, an emptyjob_list, and afilter_summaryobject. Do not treat HTTP 404 as “no matches” for successful searches (404 is reserved for database/unavailable errors on this endpoint).GET /api/search/was removed; job-ID and host routing are handled in the React SPA. API clients should useGET /api/jobs/?jid=…(or open/machine/job/{jid}/in a browser) instead of the old search redirect endpoint.
Building and installing the hpcperfstatsd-3.0-1.el9.x86_64.rpm package (via monitor/hpcperfstats.spec) installs a systemd service hpcperfstats. Production sampling is typically configured for multi-minute intervals (often on the order of minutes), with samples at job start and end. On a Stampede3 Sapphire Rapids node (2026-08-14), a ten-minute pidstat observation of hpcperfstatsd showed sample-window peaks of at most 3.0% of one core and a 0.19% average over the full window; see docs/measurements/monitor_overhead_stampede3_spr_2026-08-14.md. The daemon hpcperfstatsd sends data to a RabbitMQ server over the administrative network. RabbitMQ must be installed and running on the server to receive data.
The hpcperfstats container orchestration sets up a Django/PostgreSQL ingest and archival stack plus a RabbitMQ server to receive data from the monitor on the nodes.
These steps are a fresh install on a new host. For an existing Compose stack (rebuilds, Redis/INI/volume/RabbitMQ changes, PostgreSQL 18 dual-run), use docs/upgrade.md instead.
The monitor now uses a static-bundle build flow for packaging. The canonical path builds pinned static archives for libev, rabbitmq-c, and (on x86) LIKWID, then compiles hpcperfstatsd with --enable-all-static.
-
Install RPM build prerequisites (Rocky/EL-like systems):
sudo dnf install \ gcc gcc-c++ make autoconf automake libtool cmake pkgconfig \ systemd-rpm-macros gzip tar curl perl gawk pciutils rdma-core-devel \ rpm-build
On aarch64, install one of:
sudo dnf install datacenter-gpu-manager-4-devel # or sudo dnf install libdcgm-devel -
Prepare rpmbuild directories and source tarball (from
HPCPerfStats/monitor):./scripts/prepare_rpmbuild_dirs.sh
This script:
- creates
monitor/rpmbuild/{SPECS,SOURCES,BUILD,RPMS,SRPMS,BUILDROOT} - runs
scripts/build_static_bundle.sh --deps-onlyintomonitor/rpmbuild/static-prefix - runs
autoreconf -fi,./configure, andmake dist - copies
hpcperfstats-<version>.tar.gztorpmbuild/SOURCES
- creates
-
Build the RPM:
Use the
rpmbuildcommand printed byscripts/prepare_rpmbuild_dirs.sh. A typical script output is:rpmbuild -ba --define "_topdir $(pwd)/rpmbuild" rpmbuild/SPECS/hpcperfstats.spec -
Optional build options:
- Reuse existing static deps when already staged:
SKIP_DEPS=1 ./scripts/prepare_rpmbuild_dirs.sh
- Build dependencies + monitor binary directly (without rpmbuild staging):
./scripts/build_static_bundle.sh
- Build only pinned dependency archives:
./scripts/build_static_bundle.sh --deps-only
- Release-optimized monitor build:
./scripts/build_static_bundle.sh --release # equivalent to: HPC_BUNDLE_RELEASE_BUILD=1 ./scripts/build_static_bundle.sh - Pass extra configure args through bundle build (example):
./scripts/build_static_bundle.sh --disable-lustre
- Reuse existing static deps when already staged:
-
Configuration — after install, edit
/etc/hpcperfstats/hpcperfstats.conf:Field Description serverHostname or IP of the RabbitMQ server queueSystem/cluster name being monitored portRabbitMQ port (default 5672)freqSampling interval in seconds Example:
server localhost queue default port 5672 freq 600
Reload a running daemon with:
kill -HUP <pid>(or restart the service). -
Service control:
sudo systemctl start hpcperfstats sudo systemctl stop hpcperfstats sudo systemctl restart hpcperfstats
Job start/end: Notify hpcperfstats by writing to /var/run/stats_jobid on each node:
- Job start: echo the job ID into the file
- Job end: echo
-into the file
Do this from your scheduler’s prolog and epilog.
Accounting ingest (SLURM sacct): Use hpcperfstats-sacct-gen from the hpcperfstats-tools package. By default this command runs sacct for a date range and POSTs the results to the HPCPerfStats API ingest endpoint. Each successful POST also creates or overwrites a daily accounting file at {acct_path}/YYYY-MM-DD.txt (same pipe-delimited body Slurm returned) only when the body has at least one job row. Empty or header-only days are not written. Alternatively, use -f DIR to write those YYYY-MM-DD.txt files locally (same format/naming) without calling the API; -f is mutually exclusive with --api-key, and DIR must already exist. The scheduled sync_acct.py job can reingest these files from disk. The API rejects payloads with fewer lines than the existing file for that date (HTTP 409) so a partial sacct export cannot replace a fuller one. Header-only or empty POSTs skip the write without a 409.
-
Install the tools (Python):
# Client CLIs live in-tree under hpcperfstats-tools/ (separate distribution). # From the HPCPerfStats git checkout: python3 -m pip install ./hpcperfstats-tools # Editable during development: # python3 -m pip install -e ./hpcperfstats-tools
-
Configure the API base URL:
Set
HPCPERFSTATS_TOOLS_INIto an INI file that contains[API] base_url(seehpcperfstats-tools/hpcperfstats-tools.ini.examplein the repo for a template). -
Run the ingest (requires a staff-capable API key):
# Ingest today only (default date range is today .. today, inclusive) hpcperfstats-sacct-gen --api-key YOUR_KEY # Ingest an explicit date range (both ends inclusive) hpcperfstats-sacct-gen 2024-01-01 2024-01-08 --api-key YOUR_KEY # Write daily YYYY-MM-DD.txt files locally instead of POSTing (DIR must exist; # mutually exclusive with --api-key) hpcperfstats-sacct-gen -f /path/to/accounting 2024-01-01 2024-01-08
Run-location and permissions requirements:
- Run
hpcperfstats-sacct-genon a host where Slurm’ssacctbinary exists and works (typically a Slurm login node). - Run it as a user that has the correct Slurm permissions to query the relevant jobs/accounts via
sacct. - API mode: the API key you pass with
--api-keymust be staff-capable for the ingest endpoint; thewebcontainer must have the shared data volume mounted at/hpcperfstats/(same aspipeline) so the API can write underacct_path(default/hpcperfstats/accounting). - File mode (
-f DIR):DIRmust already exist; no API URL or key is required. Place or sync the resulting.txtfiles wheresync_acct.pyexpects them (acct_path).
This is a container orchestration with Django/PostgreSQL, ingest/archival tools, and RabbitMQ. The steps below assume a Rocky Linux host.
-
Install rootless Podman, Compose, and Node 24:
sudo dnf module reset -y nodejs sudo dnf module enable -y nodejs:24 sudo dnf install -y git podman podman-compose nodejs npm \ shadow-utils fuse-overlayfs slirp4netnsRun containers as the unprivileged deployment account—never with
sudo podman. Rootless subordinate IDs must cover image uid 901860; allocate a non-overlapping range of at least 1,048,576 IDs in both/etc/subuidand/etc/subgid, then runpodman system migrateas that account. Example for usersharrell(choose a site-safe unused start):sudo usermod --add-subuids 1000000-2048575 \ --add-subgids 1000000-2048575 sharrell podman system migrate
Redis / Linux kernel: Redis warns when
vm.overcommit_memoryis disabled; background saves usefork(), and the kernel can reject that fork even with free RAM. Enable it on the Linux Podman host:sudo sysctl -w vm.overcommit_memory=1
Persist across reboots:
echo 'vm.overcommit_memory = 1' | sudo tee /etc/sysctl.d/99-redis-overcommit.conf sudo sysctl --system
Alternatively, add
vm.overcommit_memory = 1to/etc/sysctl.confand reboot (or run thesysctl -wcommand once).Compose pins Redis Open Source 8.10 (
redis:8.10.1-alpine3.23) withmaxmemory 16gb,volatile-lru(Django cache keys keep TTL and remain evictable),--io-threads 4/--io-threads-do-reads yes, Redis 8.10 compact hashes (--hash-min-template-entries 1, default 0 disables auto-conversion), and a Unix domain socket at/run/redis/redis.sockshared byredis,web, andpipelinevia theredis_runtimenamed volume (not a host bind). Do not put Redis onweb/pipelinedepends_on(anycondition:, includingservice_started/service_healthy) — podman-compose can createhpcperfstats_redis_1and never start it, sologs redisstays empty. Redis still has a TCP+socketPINGhealthcheck forps. Startup wait uses[CACHE] redis_location, remaps Compose hostnameredis(including bakedredis://redis:6379/1) tounix:///run/redis/redis.sock?db=1, and falls back to that socket URL when the INI key is missing. TCP6379stays up forredis-cliand external Redis URLs. Do not useallkeys-*for Django/listend cache keys. Redis has no persistence volume (appendonly no). Size the host (or Colima) so Redis can use that cap alongside Postgresshm_size/shared_buffers. Redis image bumps on an existing host: docs/upgrade.md. -
Enable container restart after reboot:
sudo loginctl enable-linger sharrell systemctl --user enable --now podman-restart.serviceKeep every non-DNF download, cache, tool, temporary file, image layer, writable layer, and volume under
/data. Create the roots once:sudo mkdir -p /data/user/sharrell/{cache,tmp,tools} sudo mkdir -p /data/podman/sharrell/{storage,images,volumes,cache,tmp} sudo chown -R sharrell:sharrell /data/user/sharrell /data/podman/sharrellSet login exports for
XDG_CACHE_HOME=/data/user/sharrell/cache,TMPDIR=/data/user/sharrell/tmp,PIP_CACHE_DIR=/data/user/sharrell/cache/pip,npm_config_cache=/data/user/sharrell/cache/npm,npm_config_prefix=/data/user/sharrell/tools/npm, andPLAYWRIGHT_BROWSERS_PATH=/data/user/sharrell/cache/ms-playwright. Configure rootlessstorage.confwithgraphroot=/data/podman/sharrell/storageandimagestore=/data/podman/sharrell/images; configurecontainers.confwithvolume_path=/data/podman/sharrell/volumesandimage_copy_tmp_dir=/data/podman/sharrell/tmp. Small config files may stay under~/.config;/run/user/$UIDremains required ephemeral state. Verify these paths withpodman infobefore the first build. -
Clone the repo:
git clone https://github.com/TACC/hpcperfstats.git cd hpcperfstats -
Compose settings (site bind volumes):
cp docker-compose.settings.yaml.example docker-compose.settings.yaml
docker-compose.settings.yamlis gitignored — treatdocker-compose.settings.yaml.exampleas the committed operator template. Basedocker-compose.yamlincludes the settings file automatically; you do not pass a second-ffor settings. Named volume definitions (binddevice:paths) live only in settings — base compose mounts them by name and must not declare empty volume stubs (podman-compose cannot merge null stubs with settings dicts). Aftercp, edit site-specificdevice:paths below; any new bind volumes or optional knobs must be added to.examplein the repo (seehpcperfstats/cursor-rules/docker-compose-settings-example-sync.mdc) so the next clone gets them.Edit
docker-compose.settings.yamland set at least:volumes → ssh_keys → device: host directory with pipeline SSH keys (permissions suitable for mount as/hpcperfstats/.ssh/)volumes → proxy_ssl_source → device: host directory containingfullchain.pemandprivkey.pem(flat PEM dir; default example uses/opt/certs). For Let's Encrypt, setdevice: /etc/letsencryptand uncomment the optionalservices.proxy.environmentblock in the settings example withHPCPERFSTATS_SSL_CERTS_REL=live/your.hostname— do not bind only thelive/hostnameleaf (archive symlinks break).
TLS PEMs are not baked into the image. The
proxyservice mountsproxy_ssl_sourceread-only at/mnt/ssl-source;proxy_entrypoint.shmaterializes real PEM files into/etc/ssl/hpcperfstatsbefore nginx starts (no.env, no manual resolve step). After cert renew,docker compose restart proxy(no image rebuild).Create host directories for every bind
device:beforeup(defaults from the example):sudo mkdir -p /data/hpcperfstats_data/site_data sudo mkdir -p /data/hpcperfstats_data/site_data/accounting sudo mkdir -p /data/hpcperfstats_data/site_data/archive sudo mkdir -p /data/hpcperfstats_data/site_data/daily_archive sudo mkdir -p /data/hpcperfstats_data/site_data/logs/current sudo mkdir -p /data/hpcperfstats_data/site_data/logs/log_archive sudo mkdir -p /data/hpcperfstats_data/rabbitmq sudo mkdir -p /data/hpcperfstats_site/staticfiles sudo mkdir -p /data/hpcperfstats_site/media sudo mkdir -p /data/hpcperfstats_db/pg15 # Also ensure the ssh_keys and proxy_ssl_source device paths you set above exist.The
hpcperfstatsdatabind maps to/hpcperfstats/in thepipelineandwebcontainers (for example/hpcperfstats/accounting,/hpcperfstats/archive,/hpcperfstats/daily_archive, and/hpcperfstats/logs/for cluster syslog).Daily monitor archive compression:
sync_timedbseals each day’sYYYY-MM-DD.tartoYYYY-MM-DD.tar.zstwith zstd. Defaults:archive_zstd_threads=0(-T0, niced seal/restore),ingest_zstd_threads=4(-T4, un-niced ingest/populate streams),archive_seal_parallel_workers=4(concurrent daily seals),nice/ionicedeprioritization so seal yields to web/db on shared hosts — seedocs/DEPLOY_CONCURRENCY_AND_NUMA.md(Archive zstd priority). Other[PIPELINE]keys:archive_zstd_level,archive_zstd_nice,archive_zstd_ionice_class,archive_zstd_ionice_levelinhpcperfstats.ini.example. When inspecting archives by hand, use for examplezstd -d -o YYYY-MM-DD.tar YYYY-MM-DD.tar.zst(legacy.tar.gzuseszstd -d --format=gzip). Before raisingarchive_zstd_levelabove 9 on production data, benchmark a representative daily tar on the pipeline host:zstd -b3 -e12 -T0 -S -- ./YYYY-MM-DD.tar.Daily archive member cache:
sync_timedbkeeps complete member maps in process memory and persists them under{archive_dir}/.sync_timedb_archive_members/. Dedicated[thread:populate-pool]workers stream sealed/tar archives into that store. Django[CACHE] redis_locationremains for the web/listend cache only. Ingest duplicate-check zstd runs at normal priority; janitor seal paths keep archivenice/ionice. Member maps are invalidated after tar append, dedupe, seal, and archive finalize — wipe day sidecars withscripts/invalidate_archive_members.py(never.sync_timedb_job_store.json). Seesync_archive_members_populate_*/sync_archive_members_wait_poll_secondsinhpcperfstats.ini.exampleanddocs/DEPLOY_CONCURRENCY_AND_NUMA.md.Ingest-first durability vs archive failure: with
sync_enable_ingest_first_durability_mode=yes(default), DB-ingested raw may be checkpointed as processed when archive append retries are exhausted (ingest_first_archive_abandoned_rawin logs; entry also in.sync_timedb_dead_letter.json). Raw is not deleted until a later pass verifies tar+sealed archive membership. Recovery: fix archive/tar issues, clear or replay dead-letter entries, and rescan — do not delete raw manually unless you have confirmed DB and archive parity.Startup snapshot wait: on large trees with
backlog, first pending rescan may wait up tosync_startup_snapshot_wait_seconds(default 300, min 120) for the janitor startup heavy pass to publishStartupArchiveScanCoordinatorsnapshot before single-flight fallback build. Grepsync_timedb: pending rescan begin,startup archive scan ready,janitor: discover_ready_day_close. BootDAY_CLOSEdiscover runs on[sync_timedb:thread:archive-janitor]only — ingest is not gated on day-close completion. Inspect async manifest.sync_timedb_async_day_close.jsonfor in-flight rows. These startup paths run only when the pipeline command includesbacklog(see below).Startup maintenance (
backlogonly): janitorreason=startupheavy snapshot + boot handoff for ingest catch-up, thenstartup ingest gate cleared; ingest may begin. CLIbacklogis ingest-only for day-close (current/ date-range own seal/verify/delete). Runs whensync_timedb.pyis invoked withbacklog. Date-window runs skip startup maintenance and begin ingest immediately.sync_timedb.pydate arguments: with no dates, ingest uses the last five calendar days through now. A singleYYYY-MM-DDlimits ingest to that day only. Two dates set an explicit start/end range. Prefixonceto exit after one idle rescan (for exampleonce 2024-01-15oronce backlog).Startup archive scan (single-flight): on large trees with
backlog, janitor startup maintenance publishes onebuild_archive_maintenance_snapshotviaStartupArchiveScanCoordinator— tunesync_startup_snapshot_wait_seconds(default 300, min 120) if logs show long waits beforestartup archive scan ready. Details:docs/DEPLOY_CONCURRENCY_AND_NUMA.md§ canonical startup archive scan.Cluster syslog (optional; not auto-started): the
pipelineservice still publishes TCP and UDP port 514 on the Docker host for sites that enable ingest manually. Compute or login nodes should forward syslog to<docker-host>:514(rsyslog examples: TCP@@host:514, UDP@host:514).syslog-ngandseal_syslog_dailyare not supervisord programs — the pipeline supervisor runs ashpcperfstats(uid 901860) and cannot bind privileged port 514. Live files, when syslog is enabled, go under/hpcperfstats/logs/current/as$HOST.$R_YEAR$R_MONTH$R_DAY.log.seal_syslog_dailypacks the previous day’s files into/hpcperfstats/logs/log_archive/YYYY-MM-DD-syslog.tar.gz. Do not add composeuser: "901860:901860"onpipeline— the rootsupervisor_startup.shmust stillchownthe data bind and copy ssh keys before supervisord drops privileges.Manual cluster syslog enable (as root inside
pipeline): uncomment or run the two commented lines insupervisor_startup.sh, or by hand:docker compose exec pipeline sh -lc ' mkdir -p /var/lib/hpcperfstats-syslog && /usr/local/bin/python3 -m hpcperfstats.render_syslog_ng_generated && /usr/sbin/syslog-ng -F --no-caps -f /home/hpcperfstats/services-conf/syslog-ng.conf '
Render must run before starting syslog-ng — boot no longer refreshes
/var/lib/hpcperfstats-syslog/generated.conf. Runpython3 -m hpcperfstats.seal_syslog_dailyashpcperfstatson a schedule if you need daily seals (not supervised).[SYSLOG]inhpcperfstats.ini: setallow_fromto a comma- or line-separated list of IPv4 CIDRs that may send remote syslog (for example10.0.0.0/8, 192.168.50.0/24). Ifallow_fromis blank or[SYSLOG]is omitted, all IPv4 sources are accepted (backward compatible). Changingallow_fromrequires re-runningrender_syslog_ng_generated(then restart syslog-ng) so/var/lib/hpcperfstats-syslog/generated.confmatches INI — a pipeline recreate alone does not refresh that fragment.listen_tcp/listen_udp(defaultyes) toggle listeners.Operational notes: when syslog-ng is running, it emits periodic internal stats (
stats(freq(3600))inservices-conf/syslog-ng.conf); operators can runsyslog-ng-ctl stats(as root) insidepipelinefor counters. Monitor disk use on the data volume (logs/log_archivegrows with cluster size and retention). Pipeline process control usespodman-compose -p hpcperfstats(logs/ps/stop/exec) —supervisorctlis not configured. Troubleshooting: if packets reach the host but nothing is logged, confirm syslog-ng is actually running, check firewall rules, that traffic targets the published 514 on the host runningpipeline,allow_fromincludes the sender’s IPv4 address, and (for filenames) that forwarders preserve a sensible hostname/FQDN. -
Application config:
cp hpcperfstats.ini.example hpcperfstats.ini
In
hpcperfstats.iniunder[DEFAULT](install-required and site-wide):machine— cluster namehost_name_ext— FQDN of the clusterserver— FQDN of the host running the containersrestricted_queue_keywords- queues you want to filter out and prevent jobs in them from being displayedstaff_email_domain- the email domain of the institution/organization so authorized staff can see all jobstimezone- your machine's local timezonetotal_cores- CPU budget for app parallelism (omit to use code default 40; seedocs/DEPLOY_CONCURRENCY_AND_NUMA.md)secret_key- a random string- PostgreSQL connection:
engine_name,dbname,username,password,host,port(Compose useshost=dbin the image-built ini)
Optional tuning lives in other sections (see
hpcperfstats.ini.example):[PORTAL]— Gunicorn/Django web stack only:gunicorn_workers,parallel_db_prefetch_max,api_small_executor_max_workers,db_conn_max_age,db_statement_timeout_ms,db_idle_in_transaction_timeout_ms,cors_origin_scheme, and development-onlyseparate_test_login(default no)[PIPELINE]— ingest, archive, and metrics: required pathsacct_path,archive_dir,daily_archive_dir; optionalsync_*,metrics_*,archive_*,metrics_pool_processes, and related keys[RMQ],[OAUTH2], optional[CACHE],[SYSLOG],[XALT]— integration sections unchanged
For a fresh Docker install you typically edit
[DEFAULT]as above;[RMQ]and PostgreSQL defaults in the example are already wired for Compose. Do not change[RMQ]hostnames unless your RabbitMQ layout differs. For cluster syslog, add or edit the optional[SYSLOG]section (seehpcperfstats.ini.exampleand the compose step above).hpcperfstats.ini.examplelayout: each setting has a one-line#comment directly above it; optional tuning keys appear commented with defaults matchingconf_parser. Pipeline/archive behavior (zstd seal, DB-before-append gatesync_archive_require_db_ingestunder[PIPELINE], syslog allowlist) is described in the bullets above and indocs/DEPLOY_CONCURRENCY_AND_NUMA.md.PostgreSQL container (
docker-compose.yamldbservice):max_connectionsis 500 so overlapping Gunicorn workers, threaded API routes (job_plots,home_options,job_detailaux tasks), and pipeline pools rarely hittoo many clients. Parallel helpers:max_worker_processes=32,max_parallel_workers=24,max_parallel_workers_per_gather=4,max_parallel_maintenance_workers=2. Memory spikes are still controlled by a lowerwork_mem, smaller maintenance/autovacuum work mem,temp_buffers, and a slightly lowershared_buffers—see inline comments there. Summary plot aggregate prefetch uses at most two inner threads (seesummaryplot.compute_summary_aggregate_prefetch_pool_size) so nested thread pools do not stack against the shared API executor. If legitimate bulk jobs slow down, prefer raisingwork_memonly during batch windows or increasing thedbcontainermem_limitrather than unconstrained per-query memory.For memory-constrained deployments, start with the conservative baseline values documented in
hpcperfstats.ini.example, then scale up gradually after observing stable DB checkpoints and container RSS headroom.pipelinememory cap (docker-compose.yaml): on hosts with ~192 GiB RAM and no swap, setmem_limit: 128gandmemswap_limit: 128gon thepipelineservice (defaults in base compose; override indocker-compose.settings.yamlif needed) so ingest spikes cgroup-OOM inside the container before starvingdb/web.stop_grace_perioddefaults to 30s forwebandpipeline(HPCPERFSTATS_WEB_STOP_GRACE/HPCPERFSTATS_PIPELINE_STOP_GRACE); short cutovers may SIGKILL before fullsync_timedbdrain. Pair with[PIPELINE]RSS knobs documented indocs/DEPLOY_CONCURRENCY_AND_NUMA.md§ OOM. Recreating after limit changes: docs/upgrade.md.Python interpreters (image): web/gunicorn and helpers use GIL
/opt/python3.14via/usr/local/bin/python3/gunicorn(built ondebian:trixie, not Hubpython:*). Pipeline daemonslistend,sync_timedb, andupdate_metricsare baked onto free-threaded/opt/python3.14t/bin/python(no INI toggle). Image jemalloc is force-linked and preloaded (LD_PRELOAD+/etc/ld.so.preload) so CPython and manylinux wheels share it; gunicorn keepsMALLOC_CONF=background_thread:false. Stdlibzliband other image-built natives link zlib-ng under/opt/zlib-ng(ZLIB_COMPAT; direct rpath link, not aptzlib1g). ImagezstdCLI and CPython_zstd/compression.zstduse zstd 1.5.7 under/opt/zstd(CLI gzip/zlib support linked to zlib-ng; symlinked into/usr/local/binand/usr/bin; not aptzstd).docker compose exec pipeline python3stays GIL for operator one-liners; greppable startup linespython_abi executable=… Py_GIL_DISABLED=…prove the live daemon ABI.sync_timedbingest/append/populate run as in-process threads; durable queues are the job-store sidecar. Production images install Intel MKL from PyPI and source-compile numpy/numexpr/pandas against it for both ABIs (not a hostpipstep); full image rebuilds take longer than wheel-only installs.RabbitMQ memory cap (
docker-compose.yamlrabbitmqservice): defaultvm_memory_high_watermarkis 40% of detected host RAM, which can OOM the box under thousands of monitor publishers. Compose setsmem_limit: 96g/memswap_limit: 96gand mountsservices-conf/rabbitmq_vm_memory.conf(vm_memory_high_watermark.absolute = 80GiB) so publishers block ~16 GiB below the cgroup hard wall (avoids Erlangbinary_allocat the limit). Compose also setsERL_FLAGS=+MBas aobf +MBlmbcs 512 +MHlmbcs 512(address-order best-fit binary allocator and 512 KiB largest multiblock carriers) to reduce Erlang fragmentation under many publishers. Console logging iswarning(notinfo) with connection/channel aterror; crash dumps are off (ERL_CRASH_DUMP_SECONDS=0,ulimits.core: 0).stop_grace_period: 10mgives the broker time for orderly shutdown (quorum/mnesia) before SIGKILL. On hosts with less than 96 GiB RAM, lower bothmem_limit/memswap_limitand the absolute watermark together (then recreate as in docs/upgrade.md). Recovery / preserve-extract: docs/OPERATOR_RABBITMQ_RECOVERY.md. -
Supervisord and rsync:
Tracked
services-conf/supervisord.confis baked into the image — do not copy a supervisord.example. Thersync_dataprogram always runsrsync_data_wrapper.sh, which prefersrsync_data.shif present, otherwisersync_data.sh.example.Both scripts ship with a top-of-file guard (
sleep 43200,echo "rsync not yet configured",exit) so default deploys idle for 12 hours and never SSH/rsync to remote hosts. To enable site rsync: editrsync_data.sh(the wrapper-preferred file), remove those three guard lines, put your rsync commands in thewhile trueloop, and putsleep 43200at the end of the loop so a configured site does not tight-loop. Ensure SSH keys are configured in compose as documented for the pipeline service. -
Web server (nginx):
Committed
services-conf/nginx.confuses fixed in-container TLS paths (/etc/ssl/hpcperfstats/fullchain.pemandprivkey.pem). Cert PEMs are materialized at container start from theproxy_ssl_sourcesettings volume (mounted at/mnt/ssl-source) viaresolve_proxy_ssl_certs_dir.pyinproxy_entrypoint.sh. There is no Composessl_certsvolume, no build-time host/bind, noadditional_contexts/ manual resolve step, and no production.env. Do not edit TLS paths in nginx — setproxy_ssl_source.deviceindocker-compose.settings.yaml(and for Let's Encrypt, optionalHPCPERFSTATS_SSL_CERTS_RELin settings).After Let's Encrypt renew (or changing the TLS source path): restart
proxyonly —docker compose restart proxy. Never point productionproxy_ssl_source.deviceattests/fixtures/proxy-ssl. Image rebuilds that leaveproxyrunning: docs/upgrade.md.Compose bind-mounts
./services-conf/nginx-main.confto/etc/nginx/nginx.conf(process/http tunables:worker_processes auto, affinity, sendfile/tcp_*, open_file_cache) and./services-conf/nginx.confto/etc/nginx/http.d/default.confonproxy, and bind-mounts the shared snippets (nginx-static-files.conf,nginx-django-proxy-common.inc,nginx-compress-proxy.inc,nginx-compress-static.inc,nginx-edge-security-headers.inc,nginx-csp-no-active.inc,nginx-csp-django-html.inc) as the only runtime source for those snippets (they are not baked intoproxy.Dockerfile).The
proxyimage is source-built fromservices-conf/proxy.Dockerfile: pinned nginx 1.31.5, jemalloc, zlib-ng (ZLIB_COMPAT), OpenSSL 3.5.x, ngx_brotli, and zstd 1.5.7 (staticlibzstd.a+ GetPageSpeed zstd-nginx-module), with-march=native -mtune=nativeon every source-built lib (including OpenSSL--with-openssl-opt). Build the proxy image on the production host (native flags are not portable across CPU generations). Do not rely on Alpine edge apk nginx packages. The image **COPY**snginx-main.confandcpsnginx.confintodefault.confat build time for a non-Compose baseline, generateshps-proxy-allowed-hosts.incfrom[DEFAULT] server=, and shipsproxy_entrypoint.shplus the TLS/resolver helpers. Compose still replacesnginx.conf/default.confwith the host mounts. Runtimeproxy_entrypoint.shmaterializes TLS PEMs from/mnt/ssl-source, regenerates the OCSPresolverinclude from container/etc/resolv.conf, waits for SPA HTML under/srv/static/frontend/{machine,pub}/index.html, and may write private diagnostic CSP includes under/etc/nginx/(never under/srv/static). SPA shells carry their own hash CSP via HTML<meta http-equiv="Content-Security-Policy">(written at frontend export / SPA heal); nginx/machine/and/pub/locations must not send a competing hash CSP header (open_file_cache offon those locations so SPA heal is not stale). HTTP GETs for*.inc(and direct*.br/*.gz/*.zstsidecar URLs) under/static/return 404. Nginx compresses Gunicorn and SPA HTML on the fly (zstd, then Brotli, then gzip) and serves precompressed.br/.gzsiblings for hashed/static/frontend/_next/files (1y cache); unhashed/static/stays 30d. Nginx is the public authority for HSTS, framing, COOP, Permissions-Policy, Referrer-Policy, and CSP (hash-based for SPA shells; no-active for JSON/redirects). Certificates without an AIA OCSP URL will not staple; that must not take the site offline. Hostnames come from[DEFAULT] server=inhpcperfstats.ini(preferred in the build context, elsehpcperfstats.ini.example):parse_hpcperfstats_proxy_hosts.pyemits/etc/nginx/hps-proxy-allowed-hosts.inc, which the main configincludes forserver_name. Requests whoseHostheader does not match receive 404 on port 80; on port 443, unknown names get TLS handshake rejection (ssl_reject_handshake). Restartproxyafter changingserver=, TLS cert PEMs, ornginx.conf. Nodocker-compose.yamledits are required for TLS paths or hostnames (TLS paths are fixed; certs viaproxy_ssl_sourcesettings volume + entrypoint materialization).Static/media routing is split into a reusable include mounted at
services-conf/nginx-static-files.conf; nginx serves/static/and/media/directly, shells the SPA under/machine/and/pub/, and proxies only an explicit Django URL prefix list (sharedproxy_*directives inservices-conf/nginx-django-proxy-common.inc); every other path gets 404 from nginx. When you add a new top-level Django route, extend the allowlist innginx-static-files.confand keep it aligned with Django’s rooturlpatterns.Production: browsers must load
/static/*through theproxyservice (ports 80/443); nginx reads thestaticfiles_ramtmpfs volume at/srv/static, published from diskSTATIC_ROOT(staticfiles_data) onwebstartup after collectstatic, SPA heal, and sidecar compress./media/is the same pattern: diskmedia_datastaysMEDIA_ROOTstaging; nginx readsmedia_ramat/srv/media(empty media is valid). Hittingweb:8000directly is not a supported way to load hashed SPA assets (Gunicorn does not implement/static/URL serving). For local parity with that layout, use full compose includingproxy, or runmanage.py runserver --nostaticand still obtain/static/via nginx rather than Django’s dev static handler. The proxy container is built fromservices-conf/proxy.Dockerfileand enables hybrid compression (on-the-fly zstd/Brotli/gzip for proxied and SPA HTML; Brotli/Gzip sidecars for hashed static files). -
Build and start:
podman-compose -p hpcperfstats up --build -d
View logs (
docker-compose.yamluses thejson-filelogging driver withmax-size: 100mandmax-file: 3so stdout stays available to Compose on Docker and Podman and does not flood host syslog/journald):podman-compose -p hpcperfstats logs
Rootless development uses unprivileged host ports without changing production defaults:
HPCPERFSTATS_HTTP_PORT=8080 HPCPERFSTATS_HTTPS_PORT=8443 \ HPCPERFSTATS_SYSLOG_PORT=1514 \ podman-compose -p hpcperfstats-dev up --build -d
These variables are a development launch override only. Production site configuration remains in
hpcperfstats.inianddocker-compose.settings.yaml, never a required.env.On first startup, the
webcontainer runs Django migrations (manage.py migrateonly — schema changes ship as reviewed, committed migration files; production startup never runsmakemigrations) andcollectstatic --noinput --clearso diskSTATIC_ROOT(staging for the tmpfs nginx serves as/static/) is emptied of unused leftovers then populated before Gunicorn starts. Collectstatic omits*.mapsource maps. Aftercollectstatic, startup verifies SPA shells underSTATIC_ROOT/frontend/{machine,pub}/index.html. If the package image lacks the shells, web fail-closes. Volume fingerprint heal after a later image rebuild is documented in docs/upgrade.md. After heal, startup writes Brotli-11 / Gzip-9 sidecars beside compressible static files (hashed Next chunks, Django/DRF admin assets), then always publishesSTATIC_ROOTandMEDIA_ROOTonto shared tmpfs (staticfiles_ram/media_ram) thatproxymounts at/srv/staticand/srv/media. Existing stacks pick this up on the nextwebrestart; keep the hostmkdirfor/data/hpcperfstats_site/staticfilesand/data/hpcperfstats_site/media. Direct*.br/*.gzURLs stay 404 at nginx.The compose DB service includes explicit PostgreSQL checkpoint/memory tuning (
max_connections,shared_buffers,work_mem,maintenance_work_mem,autovacuum_work_mem,checkpoint_*,min_wal_size,max_wal_size, and parallel-worker caps) plusshm_size. Keep these aligned with host RAM and service memory limits; tune upward one notch at a time only after confirming checkpoint stability and no OOM events. The pipeline daemons (listend,sync_timedb, andupdate_metrics) use in-process threads and ordinary Python objects, so the pipeline service does not reserve a separateshm_sizefor worker IPC. Do not changedbshm_size: "16gb".
| Task | Command |
|---|---|
| Build and start container stack | podman-compose -p hpcperfstats up --build -d |
| Stop and remove containers | podman-compose -p hpcperfstats down |
| Existing-stack rebuilds / Redis / PG18 / INI | docs/upgrade.md |
Restart proxy after cert renew or server= change |
docker compose restart proxy |
| View logs | podman-compose -p hpcperfstats logs |
| PostgreSQL shell | docker compose exec db psql -h localhost -U hpcperfstats |
| Pipeline shell (data/processing) | docker compose exec pipeline su hpcperfstats |
| Get queues and message counts from rabbitmq | docker compose exec rabbitmq rabbitmqctl list_queues name messages consumers |
| RabbitMQ default queue type | Compose mounts services-conf/rabbitmq_default_queue_type.conf (default_queue_type = quorum). New durable monitor ingest queues are declared quorum. Existing brokers: docs/upgrade.md. |
| RabbitMQ memory cap | Compose mem_limit / memswap_limit 96g plus services-conf/rabbitmq_vm_memory.conf (vm_memory_high_watermark.absolute = 80GiB headroom). Erlang allocator: ERL_FLAGS=+MBas aobf +MBlmbcs 512 +MHlmbcs 512. Logging: console warning (not info); ERL_CRASH_DUMP_SECONDS=0. Inspect: docker compose exec rabbitmq rabbitmqctl status (Alarms + watermark; do not use rabbitmqctl list_alarms — absent on 4.3.x). Recovery: docs/OPERATOR_RABBITMQ_RECOVERY.md. |
| RabbitMQ memory scream | Pipeline supervisord rabbitmq-watcher polls management every 5 min; lines start with [rabbitmq-watcher]; literal ERROR at 40 GiB and every +10 GiB. Grep: docker compose logs pipeline 2>&1 | grep '\[rabbitmq-watcher\]'. Recreate pipeline after deploy. |
| RabbitMQ frame_max | Compose mounts services-conf/rabbitmq_frame_max.conf (frame_max = 131072, RabbitMQ default) so listend AMQP frames match pika LISTEND_AMQP_FRAME_MAX. Recreate rabbitmq and pipeline after changing that file (docs/upgrade.md). |
| Admin Monitor RabbitMQ stats | Staff Admin Monitor → RabbitMQ statistics uses the management HTTP API on compose-internal http://rabbitmq:15672 (image rabbitmq:*-management-alpine). Port 15672 is not published on the host; loopback_users.guest = false in services-conf/rabbitmq_management.conf allows web→rabbitmq auth. |
- Comprehensive Resource Use Monitoring for HPC Systems with TACC Stats
- Understanding application and system performance through system-wide monitoring
- Amit Ruhela — aruhela@tacc.utexas.edu
- Stephen Lien Harrell — sharrell@tacc.utexas.edu
- Sangamithra Goutham — sgoutham@tacc.utexas.edu
- Chris Ramos — cramos@tacc.utexas.edu
John Hammond · R. Todd Evans · Bill Barth · Albert Lu · Junjie Li · John McCalpin
Copyright (c) 2011 University of Texas at Austin
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.