Performance Trade-offs & Optimization Strategy Questions
Deciding what to optimize, how far, and at what cost to other qualities. Covers performance vs readability/reliability/cost trade-offs, prioritizing the optimization with the highest payoff, knowing when a system is fast enough, and sequencing optimization work. Emphasizes optimization as a strategic engineering judgment rather than a reflex.
Describe how you would use Linux perf, flamegraphs, and eBPF tools to diagnose kernel-level and user-space bottlenecks in a high-throughput web server. Explain setup for production-safe collection, sampling intervals, overhead trade-offs, and how to correlate kernel and user-space findings.
Sample Answer
Situation: I’d approach this as an SRE diagnosing a sustained high-throughput web server showing latency/p95 regressions. Goal: identify whether CPU, kernel (syscalls, locks, I/O), or user-space code is the bottleneck, using production-safe sampling and correlate results across kernel/user space.
Plan and tools:
- perf: low-level sampling profiler that captures kernel+user stacks (perf record -F <hz> -p <pid> -g -- sleep <dur>)
- Flamegraphs: use Brendan Gregg’s perf script → stackcollapse-perf.pl → flamegraph.pl to visualize hot paths
- eBPF (bcc / bpftrace / libbpf-tools): for low-overhead continuous metrics, syscall traces, off-CPU stacks, and histograms (e.g., offcputime, biolatency, runqlat)
Production-safe collection:
- Run sampling (not tracing every event). Typical sampling rates: 49–199 Hz. Start ~99 Hz; reduce to 49 Hz if overhead must be minimal. Higher Hz (>=400) increases fidelity but also overhead.
- Short continuous captures: e.g., 30–60s perf recordings during issue windows. For longer baseline, run eBPF histograms (aggregated) rather than perf record.
- Use perf_event_paranoid adjustments or run as root via controlled automation. Prefer ephemeral collectors (ssh+tunnel) and store profiles off-box.
- In containers: mount /sys and enable CAP_SYS_ADMIN or use node-level collectors. Use flamegraphs offline to avoid production CPU spikes.
Commands/examples:
- Kernel+user sampling (30s at 99Hz):
perf record -F 99 -p <PID> -g -- sleep 30
perf script | ./stackcollapse-perf.pl > out.folded
./flamegraph.pl out.folded > perf.svg - Capture system-wide stacks:
perf record -a -F 99 -g -- sleep 30 - eBPF quick histogram (bcc):
sudo /usr/share/bcc/tools/offcputime -p <PID> 30 > offcpu.txt - Trace syscalls with bpftrace (low overhead aggregated):
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[comm, probe] = count(); }' -d 60
Overhead trade-offs:
- perf sampling: moderate overhead mostly proportional to sample rate; inlined stacks vs. unwinders (unwinder adds cost). Use frame-pointer-based unwinding when available; enable -g.
- eBPF aggregations: very low overhead for histograms and counters; avoid per-event logging.
- Instrumentation (log/tracing): highest overhead — avoid full traces in prod.
Correlating kernel and user-space findings:
- Ensure perf records both kernel and user stacks (-k/-u flags default). When you see hot kernel functions (e.g., submit_bio, tcp_sendmsg, futex_wait), map them to the user stacks in the same perf flamegraph — flamegraphs will show call stacks crossing user→kernel (if folded to include kernel frames).
- Use per-thread PID/TID tagging: perf script emits pid/tid; include them in folded output to join with application logs/trace IDs.
- Use eBPF offcputime to find where threads spend time sleeping in kernel and pair with perf user-space CPU hotspots to tell if latency is due to scheduling/waits or user CPU work.
- For IO or network, run bpftrace/bcc tools that capture syscall latencies (biolatency, tcpconnect, tcplife) and correlate timestamps with perf samples or logs.
- If results are ambiguous, capture perf.map for JIT languages / symbolization (e.g., Java) and use addr2line / pprof stacks.
Result pattern examples:
- If flamegraph shows user CPU hot path in request handler → optimize algorithm or add CPU capacity.
- If kernel hot path shows tcp_sendmsg or lock contention → tune network stack, socket buffers, or reduce syscalls (batching) and use epoll/reactor changes.
- If off-CPU shows futex or schedule → investigate lock contention, GC pauses, or thread starvation.
Final note: start with low-frequency perf or eBPF aggregated captures, iterate with slightly higher fidelity when you reproduce the issue, always test collection overhead in staging, and automate safe collectors for postmortems.
Implement a thread-unsafe LRU cache in Python with O(1) get and put operations. Your class should support: constructor(capacity), get(key) -> value or -1, put(key, value). Evict the least recently used item when capacity is exceeded. Include example usage and explain the time/space trade-offs of your implementation.
Sample Answer
Use a hash map for O(1) key lookup and a doubly linked list to track usage order (most recently used at head, least at tail). On get/put we move nodes to head. This is thread-unsafe (no locks) as requested.
class Node:
def __init__(self, key, val):
self.key, self.val = key, val
self.prev = self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.cap = capacity
self.map = {} # key -> Node
# Dummy head/tail to simplify ops
self.head = Node(None, None)
self.tail = Node(None, None)
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_head(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key):
node = self.map.get(key)
if not node:
return -1
self._remove(node)
self._add_to_head(node)
return node.val
def put(self, key, value):
node = self.map.get(key)
if node:
node.val = value
self._remove(node)
self._add_to_head(node)
return
if len(self.map) >= self.cap:
# evict LRU (tail.prev)
lru = self.tail.prev
self._remove(lru)
del self.map[lru.key]
newn = Node(key, value)
self.map[key] = newn
self._add_to_head(newn)
# Example usage:
# c = LRUCache(2); c.put(1,1); c.put(2,2); c.get(1) -> 1; c.put(3,3) evicts key 2
Key points:
- Time: get and put are O(1) average because hashmap ops and constant-time linked-list manipulations.
- Space: O(capacity) for map + nodes.
- Trade-offs: this is memory-efficient and fast for single-process use. For concurrent access, add locking or use thread-safe collections. For very large caches, consider segmented caches or offloading to an external store (Redis) to avoid memory pressure. Edge cases: capacity <= 0 (treat as no-op), updating existing keys, handling non-hashable keys.
Multiple services report CPU contention and you have limited engineering bandwidth. Provide a framework to prioritize optimization work, weighing user impact, cost savings, implementability, rollback risk, monitoring improvements, and alignment with product goals.
Sample Answer
Framework (overview):
- Turn prioritization into a repeatable scoring model combining quantitative and qualitative factors. Use a 0–5 score for each axis, weightable to match org priorities, sum to a priority score.
Axes and how to score:
- User impact (weight 30%): estimate affected users, feature criticality, and SLO breach risk. Score higher for user-visible outages or revenue paths.
- Cost savings (weight 20%): estimate immediate and recurring infra cost reduction (e.g., CPU hours, reserved instance changes).
- Implementability (weight 15%): engineering effort (person-days), required cross-team coordination. Lower effort → higher score.
- Rollback/risk (weight 15%): risk of regressions, blast radius, data migration needs. High rollback risk → lower score.
- Monitoring & observability improvement (weight 10%): ability to add instrumentation that prevents future work; scores higher if changes include better alerts, dashboards, or SLOs.
- Alignment with product goals / strategic value (weight 10%): long-term roadmap fit, customer commitments.
Process:
- Triage: collect top contenders from alerts, on-call notes, and cost reports.
- Estimate: rapid sizing for each axis (use historical incidents, metrics).
- Score & rank: apply weights, compute total score.
- Quick wins bucket: pick items with high score and low implementability cost first.
- Plan experiments: for high-impact but risky items, run small pilot or A/B rollout.
- Lock in monitoring: require any optimization to include measurable success criteria (CPU reduction %, latency, SLO improvements) and dashboarding before full rollout.
- Review cadence: weekly/biweekly reassess with product and infra leads; update weights if priorities shift.
Example (short):
- Service A: user-facing payments (user impact 5, cost 2, implementability 3, risk 2, monitoring 4, alignment 5) → high priority.
- Service B: internal batch job (user impact 2, cost 5, implementability 4, risk 4, monitoring 2, alignment 2) → medium (cost savings but lower user impact).
Decision principles:
- Favor fixes that reduce incidents against SLOs first.
- Prefer low-risk, high-observability changes (quick wins + monitoring).
- Reserve larger changes for controlled experiments with rollback plans and feature flags.
Outcome measurement:
- Define KPIs per item (CPU utilization, p95 latency, cost/month, incident count) and validate within a fixed post-deploy window; rollback if negative impact or no improvement.
Design a storage-cost reduction plan for logs that must be preserved 7 days for compliance and 30 days for analysis. Recommend retention tiers, compression levels, indexing/queryability trade-offs, lifecycle policies, and a migration plan that minimizes disruption. Include rough cost/latency trade-offs and monitoring strategies.
Sample Answer
Requirements & constraints:
- Compliance: full, tamper-evident logs retained 7 days.
- Analysis: accessible logs for 30 days (queries, aggregation).
- Cost goal: minimize storage cost while maintaining required query latency for analysis (interactive <5s for recent; batch OK for older).
- No disruption to producers.
Retention tiers & storage targets:
- Hot (0–7 days): high-availability, full indexing for fast queries/alerts. Store in SSD-backed object store or log-indexing cluster (e.g., ELK/Opensearch hot nodes or managed service) with replication 2–3x.
- Warm (7–14 days): reduced replicas, partial indices (time-based indices), compressed storage on cheaper SSD/HDD-backed nodes.
- Cold (14–30 days): compressed, archived blocks in low-cost object storage (S3 Standard-IA / Azure Cool) with minimal indexing (metadata only) enabling batch queries via rehydration or archival query features.
- Archive (>30 days if needed): immutable compressed blobs in Glacier/Archive class for long-term retention beyond 30d if policy requires.
Compression & formats:
- Use chunked, columnar-friendly formats where possible (e.g., JSON-lines gzipped for hot/warm, parquet or ORC for cold) to improve compression and query performance.
- Hot: light compression (gzip level 1–3) to keep CPU low and enable streaming queries.
- Warm: medium (gzip level 6) or zstd for better ratio with moderate CPU.
- Cold: aggressive compression (zstd level 10–19 or parquet with brotli) since access is infrequent.
Indexing / queryability trade-offs:
- Hot: full inverted indices, field mappings for top N query fields, retention of raw message; supports sub-second search and alerts.
- Warm: time-based shards, limited indices for commonly queried fields; rely on columnar storage for aggregations.
- Cold: only metadata indices (timestamp, source, severity, request_id); full-text requires rehydration or on-the-fly indexing (higher latency).
- Provide an on-demand rehydration path: when cold logs are needed for ad-hoc investigations, copy to warm-hot with background reindex (expect minutes–hours).
Lifecycle policies & automation:
- Automate rollovers daily into time-partitioned indices/buckets.
- Lifecycle rules:
- 0d: ingest into hot.
- 7d: reduce replicas, move to warm (reindex to medium-compressed format).
- 14d: convert to columnar compressed files, push to cold object storage, delete heavy indices.
- 30d: delete or move to archive per retention.
- Maintain WORM/tamper-evidence for the 7-day compliance window (object lock or immutable indices).
Migration plan minimizing disruption:
- Phase 1 (pilot): Route subset of traffic to new tiered pipeline; validate ingest, queries, alerts, SLOs.
- Phase 2 (dual-write): Temporarily write to both existing system and new tiered pipeline for 1–2 weeks; compare results and reconcile.
- Phase 3 (cutover): Switch consumers (dashboards, alerts) to new endpoints; keep old system read-only for a rollback window.
- Phase 4 (backfill & prune): Backfill past 7–30d into cold tier as needed, ensuring compliance; decommission old storage gradually.
Cost vs latency rough estimates:
- Hot storage (0–7d): highest cost (~50–70% of monthly cost) but sub-second to <5s query latency.
- Warm (7–14d): medium cost (~15–25%); interactive queries 5–30s.
- Cold (14–30d): low cost (~5–15%); queries after rehydration minutes–hours or via batch jobs.
- Compression choices shift cost: aggressive compression reduces storage cost by 2–5x but increases CPU/ingest cost and latency for reads.
Monitoring & alerting:
- Track: storage usage per tier, ingest rate, compression ratio, index sizes, query latencies (p95/p99), rehydration times, failed reindex operations, SLOs for alerting/analysis.
- Alerts:
- Tier capacity >80% and growth trending upward.
- Query latency/p95 breach.
- Failed lifecycle job or migration error.
- Implement dashboards (per-cluster, per-tenant), cost forecasts, and runbooks for rehydration and rollback.
- Periodic audits: verify retention policies, WORM integrity, and restore tests for compliance.
Operational considerations & trade-offs:
- CPU vs storage: zstd higher CPU on ingest/rehydration; consider offloading compression to batch jobs.
- Query UX: provide query gateway that hides tier complexity; show expected latency and allow "rehydrate now" button.
- Security/compliance: encrypt at rest/in transit, audit logs, and enforce RBAC for rehydration.
This plan balances cost and access: keep critical, compliance-bound logs instantly available for 7 days, progressively reduce cost and queryability to 30 days, and provide controlled rehydration for deeper analysis with minimal producer disruption.
Implement a simple token-bucket rate limiter in Python for a single process that supports allow_request(key, timestamp) -> bool. Parameters: capacity and refill_rate (tokens per second). The implementation only needs to be correct single-threaded; explain time and space complexity and trade-offs of your approach.
Sample Answer
Approach: Use a per-key token bucket storing (tokens, last_timestamp). On each allow_request(key, timestamp) compute tokens to add = (timestamp - last_timestamp) * refill_rate, cap to capacity, then if tokens >= 1 consume one token and allow; else deny.
from collections import defaultdict
from typing import Dict, Tuple
class TokenBucket:
def __init__(self, capacity: float, refill_rate: float):
"""
capacity: max tokens per bucket
refill_rate: tokens added per second
"""
self.capacity = float(capacity)
self.refill_rate = float(refill_rate)
# store key -> (tokens, last_timestamp)
self.buckets: Dict[str, Tuple[float, float]] = {}
def allow_request(self, key: str, timestamp: float) -> bool:
tokens, last = self.buckets.get(key, (self.capacity, timestamp))
# accumulate tokens since last timestamp
elapsed = max(0.0, timestamp - last)
tokens = min(self.capacity, tokens + elapsed * self.refill_rate)
if tokens >= 1.0:
tokens -= 1.0
allowed = True
else:
allowed = False
# update bucket state
self.buckets[key] = (tokens, timestamp)
return allowed
Key points:
- Single-threaded, in-memory, per-key buckets.
- Time complexity: O(1) per request (hash lookup + O(1) math).
- Space complexity: O(n) for n distinct keys tracked.
- Trade-offs: simple and efficient for single process; doesn't persist across restarts and doesn't work across multiple processes/hosts. For distributed rate limiting use a central store (Redis with atomic Lua scripts) or a token server. Consider periodic cleanup of stale keys to bound memory. Using floats handles fractional tokens; using integers with nanosecond timestamps is an alternative to avoid float drift.
Unlock Full Question Bank
Get access to all 40 Performance Trade-offs & Optimization Strategy interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.