In modern database reliability engineering, traditional observability tools operate exclusively in user space inside the database SQL engine. While these tools answer what queries are running, they leave a critical Observability Gap: they cannot reveal why a PostgreSQL backend process stalls when the performance bottleneck occurs below the database engine inside the Linux kernel.
pg-tracer bridges this gap. Using Extended Berkeley Packet Filter (eBPF) technology, kernel tracepoints, kernel probes (kprobes/kretprobes), user probes (uprobes), User-Space Statically Defined Tracing (USDT), and eBPF stack sampling, pg-tracer captures dynamic, full-stack metrics directly from the kernel and PostgreSQL binary without requiring source code modifications, database extensions, server restarts, or high CPU overhead.
flowchart TD
subgraph UserSpacePostgres ["User Space (PostgreSQL Engine)"]
A["SQL Queries / Transactions"] --> B["Traditional Tools: pg_stat_statements & pg_stat_activity"]
B --> C["Observability Gap: Sees query text & wait names, BUT misses OS kernel stalls"]
A --> D["Engine Probes: exec_simple_query, LockAcquire, XLogInsert, ReadBuffer_common"]
end
subgraph KernelSpace ["Kernel Space (Linux Subsystems)"]
D --> E["Syscalls Subsystem: read, write, pread64, pwrite64, fsync, fdatasync, openat, close"]
E --> F["VFS Subsystem: vfs_read, vfs_write, vfs_fsync"]
F --> G["Block I/O Subsystem: block_rq_issue, block_rq_complete"]
E --> H["CPU Scheduler Subsystem: sched_switch, sched_wakeup, sched_migrate_task"]
E --> I["Memory Management: page_fault_user, mm_page_alloc, mm_page_free, oom_score"]
E --> J["Network Subsystem: tcp_sendmsg, tcp_recvmsg, tcp_retransmit_skb"]
end
subgraph Correlation ["Cross-Layer Correlation Engine"]
K["Unified Correlation Key: Process ID (PID) & Thread ID (TID)"]
E --> K
D --> K
end
Database administrators (DBAs) and Site Reliability Engineers (SREs) frequently encounter performance anomalies that standard SQL stat views fail to explain. Standard SQL stat views (pg_stat_statements, pg_stat_activity, pg_locks) operate at the top of the stack and cannot report kernel-level stalls:
- Kernel Scheduling Preemption: A PostgreSQL backend thread is ready to run but CPU cores are saturated by background system tasks or other database backends.
- Physical Block Device Latency: A query issues an I/O request, but disk queue depth or storage controller saturation causes long wait times.
- VFS Cache Misses: Read requests pass through the Virtual File System but fail to hit the kernel page cache, forcing physical disk access.
- Memory Pressure & Page Fault Stalls: Minor and major page faults stalling query execution during large memory allocations or sorting operations.
- Network Transport Latency & Retransmissions: TCP packet loss or socket buffer bottlenecks during client query execution or Write-Ahead Log (WAL) replication.
- Off-CPU Lock Contention: Backends sleeping on kernel futexes or waiting for I/O operations while off-CPU.
pg-tracer solves this problem by using a 3-Layer Unified Tracing Model:
- Layer 1 (User Space / PostgreSQL Internals): Traces PostgreSQL internal functions (
exec_simple_query,LockAcquire,XLogInsert,XLogFlush,ReadBuffer_common) usinguprobesand USDT probes. - Layer 2 (Kernel Space / OS Subsystems): Traces syscalls, VFS functions, block device queues, CPU scheduler task switches, memory page allocations/faults, and TCP sockets using eBPF tracepoints and kprobes.
- Layer 3 (Cross-Layer Correlation Engine): Correlates high-level SQL queries with lower-level OS bottlenecks using the backend Process ID (PID) and Thread ID (TID) as unifying keys.
pg-tracer is built around a dual-space decoupled architecture separating in-kernel C/eBPF probes from user-space Go analytics and visualization.
flowchart LR
subgraph Kernel ["Kernel Space (C / eBPF)"]
direction TB
KP["kprobes / tracepoints / USDT / perf_event"] --> MAPS["Inflight Hash Maps & Stack Trace Maps"]
KP --> LOG2["In-Kernel Log2 Histogram Maps (Sub-us Buckets)"]
KP --> RB["BPF Ring Buffer (Lock-Free Event Queue)"]
end
subgraph UserSpace ["User Space (Go Application)"]
direction TB
RB --> RING["RingBufferReader (Zero-Alloc Async Stream)"]
MAPS --> FLT["PIDFilter Manager & VMA Symbolizer"]
DISC["procfs Auto-Discovery & ELF Inspector"] --> FLT
RING --> AGG["Analytics Engine (Percentiles & Latency Distributions)"]
LOG2 --> AGG
end
subgraph Presentation ["Presentation Layer (Output)"]
AGG --> CLI["CLI Stream & Stats Engine (JSON, Log2 Histograms, Periodic Summaries)"]
MAPS --> PROF["Brendan Gregg HTML/SVG Flame Graph Generator"]
end
- Kernel Space: C11 eBPF bytecode compiled via Clang/LLVM with BPF CO-RE (Compile Once - Run Everywhere) utilizing
vmlinux.hBTF metadata definitions. - User Space: Go application loading eBPF ELF objects using
cilium/ebpfand auto-generatedbpf2gobindings. - Presentation Layer:
- CLI Tracing Engine supporting text event streaming, JSON event output, periodic statistics summaries, and in-kernel log2 ASCII histogram rendering.
- Interactive Brendan Gregg HTML/SVG Flame Graph Profiler with VMA-to-ELF PID symbol resolution.
Event data streams from kernel space to user space via a BPF ring buffer (BPF_MAP_TYPE_RINGBUF). Both C and Go define identical 128-byte event structures with explicit compile-time alignment assertions.
Offset Size Field Description
0 8 timestamp_ns Kernel ktime in nanoseconds
8 4 pid Process ID
12 4 tid Thread ID
16 4 cpu CPU core ID
20 2 event_type Subsystem identifier
22 2 event_subtype Specific event / tracepoint ID
24 8 latency_ns Operation duration in nanoseconds
32 8 return_value Syscall or function return code
40 8 arg1 Subsystem-specific argument 1 (e.g. fd, offset, bytes)
48 8 arg2 Subsystem-specific argument 2 (e.g. runq_latency_ns)
56 16 comm Process command name (16 bytes)
72 56 detail Human-readable context detail string (56 bytes)
Total: 128 bytes
Compile-time assertions in both C and Go prevent memory layout drift:
- C BPF Guard:
_Static_assert(sizeof(struct event) == 128, "struct event must be 128 bytes"); - Go Guard:
var _ = [1]struct{}{}[1-int(unsafe.Sizeof(Event{}))/128]
eBPF probes measure duration using entry/exit probe pairs connected by BPF Hash maps.
sequenceDiagram
autonumber
participant PG as PostgreSQL Backend Thread
participant KP1 as sys_enter_* Tracepoint / Kprobe
participant MAP as inflight BPF Hash Map
participant KP2 as sys_exit_* Tracepoint / Kretprobe
participant RB as BPF Ring Buffer
participant GO as Go User-Space Collector
PG->>KP1: Issue Syscall / Kernel Operation
KP1->>KP1: Check if PID is in target_pids
KP1->>MAP: Store entry timestamp & arguments [Key: TID]
PG->>PG: Kernel Operation Execution
PG->>KP2: Syscall / Kprobe Completion
KP2->>MAP: Lookup entry timestamp by TID
KP2->>KP2: Calculate duration (latency = now - start_ts)
KP2->>MAP: Delete entry from inflight map
KP2->>RB: Reserve & submit 128-byte Event struct
RB->>GO: Asynchronous ringbuf read event stream
- Tracepoints: 18 enter and exit handlers covering
read,write,pread64,pwrite64,fsync,fdatasync,sync_file_range,openat, andclose. - Inflight Map: Keyed by Thread ID (
TID) to prevent collisions across concurrent PostgreSQL backends. - Captured Data: File descriptor, I/O byte count, file offset, syscall return code, and latency in nanoseconds.
- Tracing Command:
sudo ./build/bin/pg-tracer trace --probes syscall --log-level debug sudo ./build/bin/pg-tracer trace --probes syscall --stats --interval 2s
- Kernel Probes:
kprobe/vfs_read,kretprobe/vfs_read,kprobe/vfs_write,kretprobe/vfs_write,kprobe/vfs_fsync,kretprobe/vfs_fsync. - Purpose: Measures VFS latency directly beneath syscalls, allowing pg-tracer to isolate file system cache hits from underlying physical block storage requests.
- Tracing Command:
sudo ./build/bin/pg-tracer trace --probes vfs --log-level debug sudo ./build/bin/pg-tracer trace --probes syscall,vfs --stats --interval 2s
- Tracepoints:
block/block_rq_issueandblock/block_rq_complete. - Request Pointer Keying: Uses the kernel request structure pointer (
struct request *) as key in theblock_inflightmap. This guarantees accurate matching even when completion executes in interrupt context (irq) on a different thread/CPU. - In-Kernel Log2 Histogram: Bucketizes block device latencies directly in kernel space into log2 power-of-two microsecond buckets, reducing ring buffer event overhead to zero during summary statistics collection.
- Tracing Command:
sudo ./build/bin/pg-tracer trace --probes block --log-level debug sudo ./build/bin/pg-tracer trace --probes block --histogram --interval 2s
- Tracepoints:
sched/sched_switch,sched/sched_wakeup,sched/sched_wakeup_new,sched/sched_migrate_task. - Off-CPU Time: Calculated when a target PostgreSQL thread switches off CPU (
sched_switchprev_pid) and switches back on (sched_switchnext_pid). - Run Queue Latency (Scheduling Delay): Measures duration between thread wakeup (
sched_wakeup/sched_wakeup_new) and CPU assignment (sched_switch). Aggregated in-kernel insched_runq_hist. - Counters: Tracks real-time context switches per second, CPU migrations across cores/NUMA sockets, and run queue latency averages.
- Tracing Command:
sudo ./build/bin/pg-tracer trace --probes sched --log-level debug sudo ./build/bin/pg-tracer trace --probes sched --stats --histogram --interval 2s
- Tracepoints:
exceptions/page_fault_user,exceptions/page_fault_kernel,kmem/mm_page_alloc,kmem/mm_page_free,oom/oom_score_adj_update. - Page Fault Classification: Inspects
error_codebit 1 to differentiate minor page faults from major page faults that require physical disk reads. - Page Throughput Calculation: Computes exact page counts using order bit-shifts (
1 << order), reporting page allocation rate, page free rate, and net memory allocation deltas. - OOM Monitoring: Tracks Out-Of-Memory score adjustments triggered by system managers or container runtimes.
- Tracing Command:
sudo ./build/bin/pg-tracer trace --probes memory --log-level debug sudo ./build/bin/pg-tracer trace --probes memory --stats --interval 2s
- Kernel Probes:
kprobe/tcp_sendmsg,kretprobe/tcp_recvmsg,kprobe/tcp_recvmsg,kretprobe/tcp_recvmsg,kprobe/tcp_retransmit_skb. - Metrics: Socket send/recv execution latency, message counts, total bytes transferred, and TCP retransmission rates per PostgreSQL backend.
- Tracing Command:
sudo ./build/bin/pg-tracer trace --probes network --log-level debug sudo ./build/bin/pg-tracer trace --probes network --stats --interval 2s
- User Probes:
uprobe/exec_simple_queryanduretprobe/exec_simple_query. - Metrics: Captures full SQL query execution latency (
latency_ns) and query text (up to 56 bytes ine->detail). - Container Support: Resolves containerized PostgreSQL binaries via
/proc/[pid]/exeauto-discovery without requiring host binary mounts. - Tracing Command:
sudo ./build/bin/pg-tracer trace --probes pg-query --log-level debug sudo ./build/bin/pg-tracer trace --probes pg-query --format json
- User Probes:
uprobe/LockAcquire,uretprobe/LockAcquire,uprobe/LockRelease,uprobe/LWLockAcquire,uretprobe/LWLockAcquire,uprobe/LWLockRelease. - Metrics: Heavyweight lock wait duration, lock hold time, lock mode (
ExclusiveLock,ShareLock, etc.), and LWLock wait duration. - Tracing Command:
sudo ./build/bin/pg-tracer trace --probes pg-locks --log-level debug sudo ./build/bin/pg-tracer trace --probes pg-locks --format json
- User Probes:
uprobe/XLogInsert,uretprobe/XLogInsert,uprobe/XLogFlush,uretprobe/XLogFlush. - Metrics: WAL record insert rates/s (
XLogInsert), WAL flush call rates and flush duration (XLogFlush), and in-kernel log2 latency histograms (wal_flush_hist). - Tracing Command:
sudo ./build/bin/pg-tracer trace --probes pg-wal --log-level debug sudo ./build/bin/pg-tracer trace --probes pg-wal --stats --histogram --interval 2s
- User Probes:
uprobe/ReadBuffer_common(orReadBuffer),uretprobe/ReadBuffer_common,uprobe/FlushBuffer,uretprobe/FlushBuffer. - Metrics: Buffer reads/s, buffer hits/s, buffer misses/s, buffer cache hit ratio %, and dirty buffer flush count (
FlushBuffer). - Tracing Command:
sudo ./build/bin/pg-tracer trace --probes pg-buffer --log-level debug sudo ./build/bin/pg-tracer trace --probes pg-buffer --stats --interval 2s
- User Probes:
uprobe/CreateCheckPoint,uretprobe/CreateCheckPoint,uprobe/BufferSync,uretprobe/BufferSync. - Metrics: Full checkpoint execution latency (
CreateCheckPoint), dirty buffer sync duration (BufferSync), and checkpoint count. - Tracing Command:
sudo ./build/bin/pg-tracer trace --probes pg-checkpoint --log-level debug sudo ./build/bin/pg-tracer trace --probes pg-checkpoint --format json
- USDT Tracepoints:
postgresql:query__start,postgresql:query__done,postgresql:transaction__start,postgresql:transaction__commit,postgresql:lock__wait__start,postgresql:lock__wait__done,postgresql:buffer__read__start,postgresql:buffer__read__done. - Pure Go Note Parser: Zero-dependency
.note.stapsdtSystemTap note metadata parser resolving probe locations and reference counter semaphores (RefCtrOffset). - Tracing Command:
sudo ./build/bin/pg-tracer trace --probes pg-usdt --log-level debug sudo ./build/bin/pg-tracer trace --probes pg-usdt --stats --interval 2s
flowchart TD
Start["Start Process Discovery"] --> ProcScan["Scan /proc for running processes"]
ProcScan --> CmdCheck["Inspect /proc/[pid]/cmdline & exe link"]
CmdCheck --> FilterPG["Identify PostgreSQL processes (Exclude non-postgres binaries)"]
FilterPG --> RoleClass["Classify Process Roles (Postmaster, Checkpointer, WALWriter, Backend, etc.)"]
RoleClass --> ElfCheck["Inspect /proc/[pid]/exe ELF Binary"]
ElfCheck --> DwarfVal["Validate DWARF Debug Info & Section Headers"]
ElfCheck --> UsdtVal["Count USDT Probes (.note.stapsdt section)"]
ElfCheck --> SymVal["Verify Engine Symbol Table (exec_simple_query, LockAcquire, XLogInsert, ReadBuffer_common)"]
SymVal --> PopulateMap["Populate target_pids BPF Map for Kernel Filtering"]
- Process Scanner: Reads
/procto discover PostgreSQL processes and determine parent-child hierarchies and process roles (postmaster,checkpointer,background writer,walwriter,autovacuum launcher,logical replication launcher,backend), filtering out host tool binaries (docker,bash,grep,pgbench). - Container-Aware Symbol Inspection: Resolves
/proc/[pid]/exeto inspect the PostgreSQL binary via Godebug/elfto count USDT probes (.note.stapsdtsection) and verify DWARF debug symbols regardless of container boundaries. - Engine Symbol Verification: Verifies presence of key internal symbols required for uprobe attachment:
exec_simple_query,LockAcquire,LWLockAcquire,XLogInsert,XLogFlush,ReadBuffer_common,FlushBuffer,smgrread,smgrwrite,CreateCheckPoint,BufferSync. - Tracing Command:
sudo ./build/bin/pg-tracer trace --auto-discover --probes all --log-level debug
# Check build dependencies
make check-deps
# Generate eBPF Go bindings using bpf2go
make generate
# Build binary
make buildThe compiled binary will be placed at build/bin/pg-tracer.
Sample PostgreSQL execution for 15 seconds and generate On-CPU & Off-CPU interactive flame graphs:
sudo ./build/bin/pg-tracer profile --duration 15s
# Outputs flamegraph_oncpu.html and flamegraph_offcpu.htmlOpen generated Flame Graphs in your browser:
xdg-open flamegraph_oncpu.html
xdg-open flamegraph_offcpu.htmlRun tracer with auto-discovery:
sudo ./build/bin/pg-tracer trace --auto-discoverRun in combined statistics and histogram mode:
sudo ./build/bin/pg-tracer trace --probes syscall,vfs,block,sched,memory,network --stats --histogram --interval 5sDual-licensed under MIT and GPL-2.0. See the LICENSE file for details.