From 00f8334fd053468887c06aaa89460677e7b452ad Mon Sep 17 00:00:00 2001 From: Andrei Smirnov Date: Thu, 8 Oct 2026 06:44:31 +0300 Subject: [PATCH 1/3] Retry 403 responses with urllib's default agent in the link checker dev.mysql.com rejects the descriptive User-Agent that Wikipedia requires but accepts urllib's default, so a 403 is retried once without it. Co-Authored-By: Claude Opus 5.5 --- scripts/check_links.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/scripts/check_links.py b/scripts/check_links.py index 7eb594f..b11717a 100644 --- a/scripts/check_links.py +++ b/scripts/check_links.py @@ -10,6 +10,7 @@ import pathlib import re import sys +import urllib.error import urllib.parse import urllib.request @@ -18,18 +19,27 @@ LINK = re.compile(r"\]\((https?://[^)\s]+)\)") # Minified HTML (the WHATWG spec, Hugo sites) leaves attribute values unquoted. ANCHOR = re.compile(r"""(?]+))""") -# Wikipedia rejects requests without a descriptive User-Agent. -USER_AGENT = "interview-briefs-link-checker (+https://github.com/pinebit/interviews)" +# Wikipedia rejects requests without a descriptive User-Agent; dev.mysql.com rejects +# agents it doesn't recognize, so a 403 is retried with urllib's default agent. +USER_AGENTS = ["interview-briefs-link-checker (+https://github.com/pinebit/interviews)", None] def fetch(url): """Return (error, set of anchor ids) for a page URL without a fragment.""" - request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) - try: - with urllib.request.urlopen(request, timeout=30) as response: - body = response.read().decode("utf-8", "replace") - except (OSError, http.client.HTTPException) as e: # URLError and timeouts are OSErrors - return str(e), set() + for user_agent in USER_AGENTS: + headers = {"User-Agent": user_agent} if user_agent else {} + try: + with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=30) as response: + body = response.read().decode("utf-8", "replace") + break + except urllib.error.HTTPError as e: + error = str(e) + if e.code != 403: + return error, set() + except (OSError, http.client.HTTPException) as e: # URLError and timeouts are OSErrors + return str(e), set() + else: + return error, set() return None, {html.unescape("".join(groups)) for groups in ANCHOR.findall(body)} From f104015142ee6849826a65940ba2628aa954a706 Mon Sep 17 00:00:00 2001 From: Andrei Smirnov Date: Thu, 8 Oct 2026 06:44:34 +0300 Subject: [PATCH 2/3] Link fundamentals brief terms and remove bold Add external links to algorithms, database, networking and os, remove bold, and fix facts flagged in review. Co-Authored-By: Claude Opus 5.5 --- docs/topics/algorithms.md | 172 ++++++++++++++++---------------- docs/topics/database.md | 204 +++++++++++++++++++------------------- docs/topics/networking.md | 126 +++++++++++------------ docs/topics/os.md | 136 ++++++++++++------------- 4 files changed, 319 insertions(+), 319 deletions(-) diff --git a/docs/topics/algorithms.md b/docs/topics/algorithms.md index 519a118..ea03c37 100644 --- a/docs/topics/algorithms.md +++ b/docs/topics/algorithms.md @@ -6,9 +6,9 @@ What experienced engineers forget before an algorithms interview, grouped by sub ### Time and space bounds -- Producing an output of size `k` already costs **Ω(k)**, whatever the algorithm. +- Producing an output of size `k` already costs [Ω(k)](https://en.wikipedia.org/wiki/Big_O_notation#Family_of_Bachmann–Landau_notations), whatever the algorithm. - Nested loops aren't automatically O(n²): if two pointers each advance at most `n` times, total work is O(n). -- Recursion stack counts as space: balanced divide-and-conquer uses O(log n) frames, a linear chain O(n). +- Recursion stack counts as space: balanced [divide-and-conquer](https://en.wikipedia.org/wiki/Divide-and-conquer_algorithm) uses O(log n) frames, a linear chain O(n). ### Input size and target complexity @@ -18,225 +18,225 @@ At ~10⁸ simple operations per second, the constraints tell you which complexit |---|---|---| | 10–11 | O(n!) | permutations | | 20 | O(2ⁿ) | subsets, bitmask DP | -| 500 | O(n³) | interval DP, Floyd–Warshall | +| 500 | O(n³) | interval DP, [Floyd–Warshall](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm) | | 5,000 | O(n²) | pairwise DP | -| 10⁶ | O(n log n) | sorting, heaps, binary search | +| 10⁶ | O(n log n) | sorting, heaps, [binary search](https://en.wikipedia.org/wiki/Binary_search) | | beyond | O(n) or O(log n) | linear scan, math | ### Amortized analysis -- A dynamic array's occasional **O(n) resize** still gives **O(1) amortized** append, because capacity grows geometrically. -- Amortized is a guarantee over a sequence of operations; hash-table lookup is **expected** O(1), and collisions can make one operation O(n). +- A [dynamic array's](https://en.wikipedia.org/wiki/Dynamic_array#Geometric_expansion_and_amortized_cost) occasional O(n) resize still gives O(1) [amortized](https://en.wikipedia.org/wiki/Amortized_analysis) append, because capacity grows geometrically. +- Amortized is a guarantee over a sequence of operations; [hash-table](https://en.wikipedia.org/wiki/Hash_table) lookup is expected O(1), and collisions can make one operation O(n). ### Recurrence patterns -- **Master theorem** for `T(n) = aT(n/b) + f(n)`: compare `f(n)` with `n^(log_b a)` — smaller → Θ(n^(log_b a)); equal → Θ(n^(log_b a) · log n); larger → Θ(f(n)). -- Merge sort (a = b = 2, f = n) is the equal case → O(n log n); binary search (a = 1, b = 2, f = 1) → O(log n). -- Recursion without memoization can revisit states exponentially often; after memoizing, cost = **states × work per state**. +- [Master theorem](https://en.wikipedia.org/wiki/Master_theorem_%28analysis_of_algorithms%29) for `T(n) = aT(n/b) + f(n)`: compare `f(n)` with `n^(log_b a)` — polynomially smaller → Θ(n^(log_b a)); equal → Θ(n^(log_b a) · log n); polynomially larger (plus a regularity condition) → Θ(f(n)). +- [Merge sort](https://en.wikipedia.org/wiki/Merge_sort) (a = b = 2, f = n) is the equal case → O(n log n); [binary search](https://en.wikipedia.org/wiki/Binary_search) (a = 1, b = 2, f = 1) → O(log n). +- Recursion without [memoization](https://en.wikipedia.org/wiki/Memoization) can revisit states exponentially often; after memoizing, cost = states × work per state. ## Arrays and sequences ### Binary search boundaries -- Search for the **boundary** of a monotonic predicate: keep a known-false and a known-true index until they're adjacent. -- **Lower bound** = first element `>= x`; upper bound = first `> x`; their difference counts occurrences. -- Binary search on the answer needs a monotonic feasibility test: O(log R × cost(test)) over a range of size `R`. +- Search for the boundary of a monotonic predicate: keep a known-false and a known-true index until they're adjacent. +- [Lower bound](https://en.cppreference.com/cpp/algorithm/lower_bound) = first element `>= x`; [upper bound](https://en.cppreference.com/cpp/algorithm/upper_bound) = first `> x`; their difference counts occurrences. +- [Binary search](https://en.wikipedia.org/wiki/Binary_search) on the answer needs a monotonic feasibility test: O(log R × cost(test)) over a range of size `R`. ### Two pointers and sliding windows -- On a sorted array, move the left or right pointer depending on whether the pair sum is too small or too large — each moves at most `n` times. -- A variable window needs a **monotonic condition** when expanding and shrinking; sum-based windows break with negative values. +- On a sorted array, [move the left or right pointer](https://usaco.guide/silver/two-pointers) depending on whether the pair sum is too small or too large — each moves at most `n` times. +- A [variable window](https://usaco.guide/gold/sliding-window) needs a monotonic condition when expanding and shrinking; sum-based windows break with negative values. - A fixed-size window adds the new element and removes the old one in O(1) per step. ### Prefix sums and difference arrays -- With `P[0]=0`, `P[i+1]=P[i]+a[i]`, the sum of `[l,r)` is **`P[r]-P[l]`**: O(n) setup, O(1) per query. -- For many range increments, mark `D[l]+=v`, `D[r]-=v`, then take one prefix sum. +- With `P[0]=0`, `P[i+1]=P[i]+a[i]`, the sum of `[l,r)` is `P[r]-P[l]`: O(n) setup, O(1) per query. +- For many range increments, mark `D[l]+=v`, `D[r]-=v`, then take one [prefix sum](https://en.wikipedia.org/wiki/Prefix_sum). ### Monotonic stacks -- **Next greater/smaller element**, scanning left to right: the new value is the answer for every index it pops. -- Each index is pushed and popped once, so the scan is **O(n)**; pick strict vs non-strict comparison for duplicates. +- Next greater/smaller element, scanning left to right: the new value is the answer for every index it pops. +- Each index is pushed and popped once, so the scan is O(n); pick strict vs non-strict comparison for duplicates. ### Monotonic deque -- **Sliding window maximum**: keep indices with decreasing values; pop smaller values from the back, drop the front once it leaves the window. -- Every index enters and leaves once — **O(n)** total instead of O(nk). +- [Sliding window maximum](https://usaco.guide/gold/sliding-window#sliding-window-maximum-in-mathcalon): keep indices with decreasing values; pop smaller values from the back, drop the front once it leaves the window. +- Every index enters and leaves once — O(n) total instead of O(nk). ### Maximum subarray (Kadane) -- Scan once: `cur = max(x, cur + x)`, `best = max(best, cur)` — **O(n)** time, O(1) space. +- [Scan once](https://en.wikipedia.org/wiki/Maximum_subarray_problem#Kadane's_algorithm): `cur = max(x, cur + x)`, `best = max(best, cur)` — O(n) time, O(1) space. - Initialize both with the first element, not 0, so an all-negative array returns its largest value. -- **Circular** variant: `max(best, total − minimum subarray)`, unless every value is negative (then just `best`). +- Circular variant: `max(best, total − minimum subarray)`, unless every value is negative (then just `best`). ### Intervals -- **Merge overlapping intervals**: sort by start, extend the last merged interval while the next start ≤ its end — O(n log n). -- **Minimum meeting rooms**: sweep sorted start and end points (or a min-heap of end times); the maximum overlap is the answer. +- Merge overlapping intervals: sort by start, extend the last merged interval while the next start ≤ its end — O(n log n). +- Minimum meeting rooms: [sweep](https://en.wikipedia.org/wiki/Sweep_line_algorithm) sorted start and end points (or a [min-heap](https://en.wikipedia.org/wiki/Binary_heap) of end times); the maximum overlap is the answer. ### Cycle detection in sequences -- **Floyd's tortoise and hare**: a slow and a fast pointer meet inside a cycle — O(n) time, **O(1) space**. +- [Floyd's tortoise and hare](https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare): a slow and a fast pointer meet inside a cycle — O(n) time, O(1) space. - Restart one pointer from the head and advance both by one; they meet at the cycle's entry. ## Strings ### KMP prefix function -- **KMP** precomputes, for each pattern prefix, the longest proper prefix that is also a suffix, and reuses it after a mismatch instead of rescanning the text. -- Search takes **O(n+m)** time for text length `n` and pattern length `m`, with O(m) extra space. +- [KMP](https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm) precomputes, for each pattern prefix, the [longest proper prefix that is also a suffix](https://cp-algorithms.com/string/prefix-function.html), and reuses it after a mismatch instead of rescanning the text. +- Search takes O(n+m) time for text length `n` and pattern length `m`, with O(m) extra space. ### Rolling hash (Rabin-Karp) -- A **polynomial rolling hash** updates a window's hash in O(1) as it slides, so search is O(n + m) expected. -- Collisions happen: **verify each match** for exact results — two moduli only make collisions rarer; also finds duplicate substrings via binary search on length. +- A [polynomial rolling hash](https://en.wikipedia.org/wiki/Rolling_hash#Polynomial_rolling_hash) updates a window's hash in O(1) as it slides, so search is O(n + m) expected. +- Collisions happen: verify each match for exact results — two moduli only make collisions rarer; also finds duplicate substrings via [binary search](https://en.wikipedia.org/wiki/Binary_search) on length. ## Sorting, selection, and sampling ### Comparison sorts -| Algorithm | Time | Extra space | Stable? | Useful distinction | +| Algorithm | Time | Extra space | [Stable?](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability) | Useful distinction | |---|---|---|---|---| -| Merge sort | O(n log n) worst | O(n) | yes | predictable bound | -| Quicksort | O(n log n) expected, O(n²) worst | O(log n) expected stack | no | randomized pivot; in-place partitioning | -| Heapsort | O(n log n) worst | O(1) | no | bounded extra space | +| [Merge sort](https://en.wikipedia.org/wiki/Merge_sort) | O(n log n) worst | O(n) | yes | predictable bound | +| [Quicksort](https://en.wikipedia.org/wiki/Quicksort) | O(n log n) expected, O(n²) worst | O(log n) expected stack | no | randomized pivot; in-place partitioning | +| [Heapsort](https://en.wikipedia.org/wiki/Heapsort) | O(n log n) worst | O(1) | no | bounded extra space | Standard array implementations; variants differ in stability and space. ### Sorting lower bound and counting sort -- Any **comparison sort** needs Ω(n log n) comparisons in the worst case — it must distinguish `n!` orders. -- **Counting sort** runs in O(n + k) for integer keys in a range of size `k`, with O(k) counts plus O(n) output for stable placement. +- Any [comparison sort](https://en.wikipedia.org/wiki/Comparison_sort#Number_of_comparisons_required_to_sort_a_list) needs Ω(n log n) comparisons in the worst case — it must distinguish `n!` orders. +- [Counting sort](https://en.wikipedia.org/wiki/Counting_sort) runs in O(n + k) for integer keys in a range of size `k`, with O(k) counts plus O(n) output for stable placement. ### Quickselect and top-k -- **Quickselect** partitions around a random pivot and recurses into one side: O(n) expected, O(n²) worst. -- A size-`k` **min-heap** keeps the largest `k` of `n` items in O(n log k) time and O(k) space. +- [Quickselect](https://en.wikipedia.org/wiki/Quickselect) partitions around a random pivot and recurses into one side: O(n) expected, O(n²) worst. +- A size-`k` [min-heap](https://en.wikipedia.org/wiki/Binary_heap) keeps the largest `k` of `n` items in O(n log k) time and O(k) space. ### Random sampling and shuffling -- **Reservoir sampling** picks `k` items from a stream of unknown length: keep the first `k`, then replace a random slot with the i-th item with probability k/i — one pass, O(k) memory. -- **Fisher–Yates** shuffle: for `i` from `n−1` down to 1, swap `a[i]` with `a[rand(0..i)]` — O(n), every permutation equally likely. -- Swapping with `a[rand(0..n−1)]` at each step looks similar but is **biased** (nⁿ outcomes can't split evenly over n! permutations). +- [Reservoir sampling](https://en.wikipedia.org/wiki/Reservoir_sampling) picks `k` items from a stream of unknown length: keep the first `k`, then replace a random slot with the i-th item with probability k/i — one pass, O(k) memory. +- [Fisher–Yates](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) shuffle: for `i` from `n−1` down to 1, swap `a[i]` with `a[rand(0..i)]` — O(n), every permutation equally likely. +- Swapping with `a[rand(0..n−1)]` at each step looks similar but is [biased](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Naïve_method) (nⁿ outcomes can't split evenly over n! permutations). ## Data structures ### Heap invariants -- A binary heap is a **complete tree** in an array; the root is the min or max, siblings are unordered. -- Peek O(1); insert and remove-root O(log n); **bottom-up construction is O(n)**. +- A [binary heap](https://en.wikipedia.org/wiki/Binary_heap) is a [complete tree](https://en.wikipedia.org/wiki/Binary_tree#Types_of_binary_trees) in an array; the root is the min or max, siblings are unordered. +- Peek O(1); insert and remove-root O(log n); [bottom-up construction is O(n)](https://en.wikipedia.org/wiki/Binary_heap#Building_a_heap_%28Heapify%29). ### Heap patterns -- **k-way merge**: push the head of each of `k` sorted lists into a min-heap, pop and push the next from the same list — O(n log k). -- Running median with **two heaps**: a max-heap for the lower half, a min-heap for the upper, sizes differing by at most one — O(log n) per insert, O(1) median. -- Heaps can't delete arbitrary items cheaply: **lazy deletion** marks items removed and discards them when they reach the top. +- [k-way merge](https://en.wikipedia.org/wiki/K-way_merge_algorithm): push the head of each of `k` sorted lists into a [min-heap](https://en.wikipedia.org/wiki/Binary_heap), pop and push the next from the same list — O(n log k). +- Running median with two heaps: a max-heap for the lower half, a min-heap for the upper, sizes differing by at most one — O(log n) per insert, O(1) median. +- Heaps can't delete arbitrary items cheaply: lazy deletion marks items removed and discards them when they reach the top. ### Hash tables and collisions -- Expected O(1) lookup and update, no ordering; **collisions** are resolved by chaining or probing, and the table rehashes past a load threshold. +- Expected O(1) lookup and update, no ordering; collisions are resolved by [chaining](https://en.wikipedia.org/wiki/Hash_table#Separate_chaining) or [probing](https://en.wikipedia.org/wiki/Hash_table#Open_addressing), and the table rehashes past a [load threshold](https://en.wikipedia.org/wiki/Hash_table#Load_factor). - Equal keys must hash equally; mutating a key's hashed fields after insertion makes it unfindable. ### Disjoint-set union -- **Union-find** tracks components with `find` (representative) and `union` (merge); no efficient deletion or path queries. -- **Path compression + union by size/rank** gives amortized O(α(n)) — effectively constant. +- [Union-find](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) tracks components with `find` (representative) and `union` (merge); no efficient deletion or path queries. +- [Path compression](https://en.wikipedia.org/wiki/Disjoint-set_data_structure#Finding_set_representatives) + union by [size](https://en.wikipedia.org/wiki/Disjoint-set_data_structure#Union_by_size)/[rank](https://en.wikipedia.org/wiki/Disjoint-set_data_structure#Union_by_rank) gives [amortized](https://en.wikipedia.org/wiki/Amortized_analysis) O([α(n)](https://en.wikipedia.org/wiki/Ackermann_function#Inverse)) — effectively constant. ### Tries and prefix lookup -- A **trie** follows one edge per symbol: lookup is O(key length), independent of the number of keys. -- Prefix search is natural, but a node per prefix costs much more space than a hash map; compressed tries merge single-child paths. +- A [trie](https://en.wikipedia.org/wiki/Trie) follows one edge per symbol: lookup is O(key length), independent of the number of keys. +- Prefix search is natural, but a node per prefix costs much more space than a hash map; [compressed tries](https://en.wikipedia.org/wiki/Radix_tree) merge single-child paths. ### LRU cache -- **Hash map + doubly linked list**: the map finds a node in O(1), the list keeps recency. +- Hash map + [doubly linked list](https://en.wikipedia.org/wiki/Doubly_linked_list): the map finds a node in O(1), the list keeps [recency](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_Recently_Used_%28LRU%29). - Move a node to the front on every access and evict from the tail — both O(1). ### Fenwick and segment trees -- A **Fenwick tree** (binary indexed tree) gives prefix sums with point updates, both O(log n), in one array. -- A **segment tree** answers any associative range query (min, max, sum) with O(log n) updates; **lazy propagation** adds range updates. +- A [Fenwick tree](https://en.wikipedia.org/wiki/Fenwick_tree) (binary indexed tree) gives prefix sums with point updates, both O(log n), in one array. +- A [segment tree](https://cp-algorithms.com/data_structures/segment_tree.html) answers any associative range query (min, max, sum) with O(log n) updates; [lazy propagation](https://cp-algorithms.com/data_structures/segment_tree.html#range-updates-lazy-propagation) adds range updates. ## Trees and graphs ### Tree traversal and BST ordering -- **Inorder** traversal of a BST yields sorted keys; preorder suits serialization, postorder bottom-up aggregation. -- An unbalanced BST degrades to **O(n)** height; AVL/red-black rotations keep operations O(log n). +- [Inorder](https://en.wikipedia.org/wiki/Tree_traversal#In-order,_LNR) traversal of a [BST](https://en.wikipedia.org/wiki/Binary_search_tree) yields sorted keys; preorder suits serialization, postorder bottom-up aggregation. +- An unbalanced BST degrades to O(n) height; [AVL](https://en.wikipedia.org/wiki/AVL_tree)/[red-black](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) rotations keep operations O(log n). ### Graph representation and traversal -- An **adjacency list** takes O(V+E) space and supports BFS/DFS in O(V+E); a matrix takes O(V²) but tests an edge in O(1). -- **BFS** gives shortest paths by edge count; DFS exposes cycles, components, and finishing order. -- Mark a node visited when **enqueuing** it, not when dequeuing, to avoid duplicate BFS work. +- An [adjacency list](https://en.wikipedia.org/wiki/Adjacency_list) takes O(V+E) space and supports [BFS](https://en.wikipedia.org/wiki/Breadth-first_search)/[DFS](https://en.wikipedia.org/wiki/Depth-first_search) in O(V+E); a [matrix](https://en.wikipedia.org/wiki/Adjacency_matrix) takes O(V²) but tests an edge in O(1). +- BFS gives shortest paths by edge count; DFS exposes cycles, components, and finishing order. +- Mark a node visited when enqueuing it, not when dequeuing, to avoid duplicate BFS work. ### Topological ordering -- A topological order exists **only for a DAG**. -- **Kahn's algorithm** repeatedly removes zero-indegree vertices; fewer than `V` removals means a cycle. -- DFS alternative: reverse finishing order, detecting back edges. Both O(V+E). +- A [topological order](https://en.wikipedia.org/wiki/Topological_sorting) exists only for a [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph). +- [Kahn's algorithm](https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm) repeatedly removes zero-indegree vertices; fewer than `V` removals means a cycle. +- [DFS alternative](https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search): reverse finishing order, detecting back edges. Both O(V+E). ### Shortest-path choices | Edge weights | Algorithm | Time with adjacency lists | Caveat | |---|---|---|---| -| all equal, positive | BFS | O(V+E) | counts edges | -| nonnegative | Dijkstra + binary heap | O((V+E) log V) | negative edges invalidate greedy settlement | -| negative allowed | Bellman-Ford | O(VE) | detects reachable negative cycles | -| DAG, any weights | topological relaxation | O(V+E) | needs acyclic graph | +| all equal, positive | [BFS](https://en.wikipedia.org/wiki/Breadth-first_search) | O(V+E) | counts edges | +| nonnegative | [Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) + [binary heap](https://en.wikipedia.org/wiki/Binary_heap) | O((V+E) log V) | negative edges invalidate greedy settlement | +| negative allowed | [Bellman-Ford](https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm) | O(VE) | detects reachable negative cycles | +| [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph), any weights | [topological relaxation](https://en.wikipedia.org/wiki/Topological_sorting#Application_to_shortest_path_finding) | O(V+E) | needs acyclic graph | ### Minimum spanning tree -- An **MST** connects all vertices of an undirected graph with minimum total weight — not a shortest-path tree. -- **Kruskal** sorts edges and keeps those joining different union-find components; **Prim** grows one tree from a min-edge priority queue. +- An [MST](https://en.wikipedia.org/wiki/Minimum_spanning_tree) connects all vertices of an undirected graph with minimum total weight — not a [shortest-path tree](https://en.wikipedia.org/wiki/Shortest-path_tree). +- [Kruskal](https://en.wikipedia.org/wiki/Kruskal%27s_algorithm) sorts edges and keeps those joining different [union-find](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) components; [Prim](https://en.wikipedia.org/wiki/Prim%27s_algorithm) grows one tree from a min-edge priority queue. ## Dynamic programming ### DP state and order -- Define a **state** holding everything future decisions need, a recurrence, base cases, and an evaluation order where dependencies are ready. -- Top-down **memoization** visits only reachable states; bottom-up tabulation fills them all. Cost = states × transitions. +- Define a state holding everything future decisions need, a [recurrence](https://en.wikipedia.org/wiki/Recurrence_relation), base cases, and an evaluation order where dependencies are ready. +- Top-down [memoization](https://en.wikipedia.org/wiki/Memoization) visits only reachable states; bottom-up tabulation fills them all. Cost = states × transitions. ### Knapsack and one-dimensional DP -- **0/1 knapsack**: iterate capacities **downward** per item so it isn't reused; upward iteration models unbounded reuse. -- O(nC) is **pseudopolynomial**: `C` is a number whose input length is only O(log C). +- [0/1 knapsack](https://en.wikipedia.org/wiki/Knapsack_problem#0-1_knapsack_problem): iterate capacities downward per item so it isn't reused; upward iteration models unbounded reuse. +- O(nC) is [pseudopolynomial](https://en.wikipedia.org/wiki/Pseudo-polynomial_time): `C` is a number whose input length is only O(log C). ### Longest increasing subsequence -- The O(n log n) method keeps the **smallest tail** for each length and binary-searches where each value goes. +- The [O(n log n) method](https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms) keeps the smallest tail for each length and binary-searches where each value goes. - For strictly increasing, replace the first tail `>= x`; the tails array gives the length, not the subsequence itself. ### Two-sequence DP: LCS and edit distance -- **LCS**: `dp[i][j] = dp[i-1][j-1] + 1` on a match, else `max(dp[i-1][j], dp[i][j-1])`. -- **Edit distance**: diagonal on a match, else `1 + min(insert, delete, replace)`. -- Both O(nm) time, reducible to **O(min(n, m)) space** with two rows. +- [LCS](https://en.wikipedia.org/wiki/Longest_common_subsequence): `dp[i][j] = dp[i-1][j-1] + 1` on a match, else `max(dp[i-1][j], dp[i][j-1])`. +- [Edit distance](https://en.wikipedia.org/wiki/Levenshtein_distance): diagonal on a match, else `1 + min(insert, delete, replace)`. +- Both O(nm) time, reducible to O(min(n, m)) space with two rows. ### Bitmask DP -- Index DP by a subset encoded as bits: travelling salesman is `dp[mask][last]` in **O(2ⁿ · n²)** — feasible up to n ≈ 20. -- Iterate submasks of `mask` with `s = (s - 1) & mask`; over all masks that totals O(3ⁿ). +- Index DP by a subset encoded as bits: [travelling salesman](https://en.wikipedia.org/wiki/Held%E2%80%93Karp_algorithm) is `dp[mask][last]` in O(2ⁿ · n²) — feasible up to n ≈ 20. +- [Iterate submasks](https://cp-algorithms.com/algebra/all-submasks.html) of `mask` with `s = (s - 1) & mask`; over all masks that totals O(3ⁿ). ## Greedy, backtracking, and hardness ### Greedy proof pattern -- A greedy choice needs an **exchange argument** or a maintained invariant — locally best isn't enough. -- Maximum non-overlapping intervals: sort by **earliest finish time**; sorting by start or by length fails. +- A [greedy choice](https://en.wikipedia.org/wiki/Greedy_algorithm) needs an exchange argument or a maintained invariant — locally best isn't enough. +- [Maximum non-overlapping intervals](https://en.wikipedia.org/wiki/Interval_scheduling): sort by earliest finish time; sorting by start or by length fails. ### Backtracking and pruning -- Backtracking walks a **decision tree**, undoing each choice after recursing; reject invalid partial solutions early. -- Prune only branches that can't produce an answer or beat the best score; enumerating all outputs stays exponential. +- [Backtracking](https://en.wikipedia.org/wiki/Backtracking) walks a decision tree, undoing each choice after recursing; reject invalid partial solutions early. +- Prune only branches that can't produce an answer or [beat the best score](https://en.wikipedia.org/wiki/Branch_and_bound); enumerating all outputs stays exponential. ### Bit manipulation -- `x & (x − 1)` clears the lowest set bit, so `x > 0 && (x & (x − 1)) == 0` tests a power of two; **`x & −x`** isolates it. -- XOR of all elements cancels pairs — finds the single unpaired number in O(1) space. +- `x & (x − 1)` clears the lowest set bit, so `x > 0 && (x & (x − 1)) == 0` tests a power of two; `x & −x` isolates it. +- [XOR](https://en.wikipedia.org/wiki/Exclusive_or#Bitwise_operation) of all elements cancels pairs — finds the single unpaired number in O(1) space. ### Recognizing NP-hard problems -- Traveling salesman, subset sum, knapsack, graph coloring, SAT, Hamiltonian path, and set cover are **NP-hard** in general. -- No polynomial algorithm is known (none exists unless P = NP); exact answers use exponential search with pruning, or pseudopolynomial DP when numbers are small (subset sum, knapsack only). -- Otherwise use heuristics or an **approximation** algorithm where a guarantee exists (greedy set cover: H(n) ≤ ln n + 1); general TSP and coloring have no good ratio unless P = NP. +- [Traveling salesman](https://en.wikipedia.org/wiki/Travelling_salesman_problem), [subset sum](https://en.wikipedia.org/wiki/Subset_sum_problem), [knapsack](https://en.wikipedia.org/wiki/Knapsack_problem), [graph coloring](https://en.wikipedia.org/wiki/Graph_coloring), [SAT](https://en.wikipedia.org/wiki/Boolean_satisfiability_problem), [Hamiltonian path](https://en.wikipedia.org/wiki/Hamiltonian_path_problem), and [set cover](https://en.wikipedia.org/wiki/Set_cover_problem) are [NP-hard](https://en.wikipedia.org/wiki/NP-hardness) in general. +- No polynomial algorithm is known (none exists unless [P = NP](https://en.wikipedia.org/wiki/P_versus_NP_problem)); exact answers use exponential search with pruning, or [pseudopolynomial DP](https://en.wikipedia.org/wiki/Pseudo-polynomial_time) when numbers are small (subset sum, knapsack only). +- Otherwise use heuristics or an [approximation](https://en.wikipedia.org/wiki/Approximation_algorithm) algorithm where a guarantee exists ([greedy set cover](https://en.wikipedia.org/wiki/Set_cover_problem#Greedy_algorithm): [H(n)](https://en.wikipedia.org/wiki/Harmonic_number) ≤ ln n + 1); general TSP and coloring have no good ratio unless P = NP. diff --git a/docs/topics/database.md b/docs/topics/database.md index ec27266..d707a50 100644 --- a/docs/topics/database.md +++ b/docs/topics/database.md @@ -6,248 +6,248 @@ What experienced database engineers forget before an interview, grouped by subto ### B+tree indexes -- The default index is a **B+tree**: balanced, sorted, O(log n), serves range scans and `ORDER BY`. -- **Clustered** (InnoDB): rows are stored in primary-key order and secondary indexes point to the PK. **Heap** (PostgreSQL): rows are unordered and indexes point to a physical location (`ctid`). +- The default index is a [B+tree](https://en.wikipedia.org/wiki/B%2B_tree): balanced, sorted, O(log n), serves range scans and `ORDER BY`. +- [Clustered](https://dev.mysql.com/doc/refman/8.4/en/innodb-index-types.html) (InnoDB): rows are stored in primary-key order and secondary indexes point to the PK. [Heap](https://www.postgresql.org/docs/current/storage-page-layout.html) (PostgreSQL): rows are unordered and indexes point to a physical location ([`ctid`](https://www.postgresql.org/docs/current/ddl-system-columns.html#DDL-SYSTEM-COLUMNS-CTID)). ### Composite indexes -- **Leftmost-prefix rule**: `(a, b, c)` seeks efficiently on `a`, `a,b`, `a,b,c` — not on `b` alone. -- **Skip scan** (MySQL 8.0.13+, PostgreSQL 18+) serves `b` alone by probing each distinct `a`; cheap only when `a` has few values. -- Put **equality columns first, range columns last**; columns after a range condition only filter inside the scanned range (unless skip scan applies). +- [Leftmost-prefix rule](https://dev.mysql.com/doc/refman/8.4/en/multiple-column-indexes.html): `(a, b, c)` seeks efficiently on `a`, `a,b`, `a,b,c` — not on `b` alone. +- [Skip scan](https://dev.mysql.com/doc/refman/8.4/en/range-optimization.html#range-access-skip-scan) (MySQL 8.0.13+, [PostgreSQL 18+](https://www.postgresql.org/docs/current/indexes-multicolumn.html)) serves `b` alone by probing each distinct `a`; cheap only when `a` has few values. +- Put equality columns first, range columns last; columns after a range condition only filter inside the scanned range (unless skip scan applies). ### Covering indexes -- A **covering index** holds every column the query needs, enabling an **index-only scan** (`INCLUDE` adds payload columns in PostgreSQL). -- PostgreSQL still checks the **visibility map**; recently modified pages force heap visits until VACUUM runs. +- A [covering index](https://www.postgresql.org/docs/current/indexes-index-only-scans.html) holds every column the query needs, enabling an index-only scan ([`INCLUDE`](https://www.postgresql.org/docs/current/sql-createindex.html) adds payload columns in PostgreSQL). +- PostgreSQL still checks the [visibility map](https://www.postgresql.org/docs/current/storage-vm.html); recently modified pages force heap visits until [VACUUM](https://www.postgresql.org/docs/current/routine-vacuuming.html) runs. ### Partial and expression indexes -- **Partial** (`WHERE deleted_at IS NULL`) and **expression** (`ON lower(email)`) indexes cover only what queries use. -- **Sargability**: a function or implicit cast on an indexed column (`WHERE lower(email) = ...`) blocks index use unless a matching expression index exists. +- [Partial](https://www.postgresql.org/docs/current/indexes-partial.html) (`WHERE deleted_at IS NULL`) and [expression](https://www.postgresql.org/docs/current/indexes-expressional.html) (`ON lower(email)`) indexes cover only what queries use. +- [Sargability](https://en.wikipedia.org/wiki/Sargable): a function or implicit cast on an indexed column (`WHERE lower(email) = ...`) blocks index use unless a matching expression index exists. ### Specialized index types | Index | Use for | |---|---| -| **GIN** | full-text, JSONB, arrays (inverted index) | -| **GiST** | geometry, ranges, nearest neighbor | -| **BRIN** | huge, naturally ordered tables (append-only time series); tiny | -| Hash | equality only | +| [GIN](https://www.postgresql.org/docs/current/gin.html) | full-text, JSONB, arrays ([inverted index](https://en.wikipedia.org/wiki/Inverted_index)) | +| [GiST](https://www.postgresql.org/docs/current/gist.html) | geometry, ranges, nearest neighbor | +| [BRIN](https://www.postgresql.org/docs/current/brin.html) | huge, naturally ordered tables (append-only time series); tiny | +| [Hash](https://www.postgresql.org/docs/current/hash-index.html) | equality only | ### Building indexes online -- Plain `CREATE INDEX` blocks writes for the whole build; **`CREATE INDEX CONCURRENTLY`** doesn't, but is slower, can't run in a transaction, and leaves an `INVALID` index if it fails. -- Index every foreign key you join or cascade-delete on — PostgreSQL doesn't create them automatically. +- Plain `CREATE INDEX` blocks writes for the whole build; [`CREATE INDEX CONCURRENTLY`](https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY) doesn't, but is slower, can't run in a transaction, and leaves an `INVALID` index if it fails. +- Index every [foreign key](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK) you join or cascade-delete on — PostgreSQL doesn't create them automatically. ## Transactions and isolation ### Anomalies × isolation levels -| Level | Dirty read | Non-repeatable read | Phantom read | +| Level | [Dirty read](https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Dirty_reads) | [Non-repeatable read](https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Non-repeatable_reads) | [Phantom read](https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Phantom_reads) | |---|---|---|---| -| Read Uncommitted | possible | possible | possible | -| Read Committed | — | possible | possible | -| Repeatable Read | — | — | possible (per SQL standard) | -| Serializable | — | — | — | +| [Read Uncommitted](https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Read_uncommitted) | possible | possible | possible | +| [Read Committed](https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Read_committed) | — | possible | possible | +| [Repeatable Read](https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Repeatable_reads) | — | — | possible (per SQL standard) | +| [Serializable](https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Serializable) | — | — | — | ### Lost update and write skew -- **Lost update**: two read-modify-write cycles overwrite each other. -- **Write skew**: two transactions read overlapping data and write different rows, together breaking an invariant (both doctors go off call). +- Lost update: two read-modify-write cycles overwrite each other. +- [Write skew](https://en.wikipedia.org/wiki/Snapshot_isolation): two transactions read overlapping data and write different rows, together breaking an invariant (both doctors go off call). ### PostgreSQL isolation -- **Default: Read Committed**; Repeatable Read is **snapshot isolation** — no phantoms, and a concurrent update of the same row aborts (no lost update), but write skew is possible. -- Serializable is **SSI**: it prevents write skew by aborting with a serialization error, so the app must retry. +- Default: [Read Committed](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED); [Repeatable Read](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-REPEATABLE-READ) is [snapshot isolation](https://en.wikipedia.org/wiki/Snapshot_isolation) — no phantoms, and a concurrent update of the same row aborts (no lost update), but write skew is possible. +- Serializable is [SSI](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-SERIALIZABLE): it prevents write skew by aborting with a serialization error, so the app must retry. ### InnoDB isolation and MVCC -- **Default: Repeatable Read** with **next-key locks** (record + gap) on locking reads, blocking many phantoms. -- Plain `SELECT` reads the snapshot, but `UPDATE` sees the latest committed row — lost updates slip through without `SELECT ... FOR UPDATE`. -- Updates happen in place; older versions are rebuilt from the **undo log**, and a long transaction makes undo history grow. +- Default: [Repeatable Read](https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html#isolevel_repeatable-read) with [next-key locks](https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html#innodb-next-key-locks) (record + gap) on locking reads, blocking many phantoms. +- Plain `SELECT` reads the [snapshot](https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html), but `UPDATE` sees the latest committed row — lost updates slip through without [`SELECT ... FOR UPDATE`](https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html). +- Updates happen in place; older versions are rebuilt from the [undo log](https://dev.mysql.com/doc/refman/8.4/en/innodb-undo-logs.html), and a long transaction makes undo history grow. ## MVCC and locking ### PostgreSQL MVCC -- Plain `SELECT`s read a snapshot and neither block nor wait for row writes; writers (and `FOR UPDATE`) still lock rows against each other. -- Old row versions stay in the table, tagged **`xmin`/`xmax`**; **VACUUM** reclaims them, and a long-running transaction blocks it, causing bloat. -- **XID wraparound** (32-bit transaction IDs) forces aggressive anti-wraparound vacuuming. -- HOT updates skip index maintenance when no indexed column changed and the page has room. +- Plain `SELECT`s read a [snapshot](https://www.postgresql.org/docs/current/mvcc-intro.html) and neither block nor wait for row writes; writers (and `FOR UPDATE`) still lock rows against each other. +- Old row versions stay in the table, tagged [`xmin`/`xmax`](https://www.postgresql.org/docs/current/ddl-system-columns.html#DDL-SYSTEM-COLUMNS-XMIN); [VACUUM](https://www.postgresql.org/docs/current/routine-vacuuming.html) reclaims them, and a long-running transaction blocks it, causing bloat. +- [XID wraparound](https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND) (32-bit transaction IDs) forces aggressive anti-wraparound vacuuming. +- [HOT updates](https://www.postgresql.org/docs/current/storage-hot.html) skip index maintenance when no indexed column changed and the page has room. ### Row locks, queue tables, advisory locks -- `SELECT ... FOR UPDATE` locks the rows it returns; **`SKIP LOCKED`** lets workers claim different rows of a queue table without blocking. -- PostgreSQL **advisory locks** are app-defined locks on a number, not a row — for leader election or job dedup without a table to lock. +- [`SELECT ... FOR UPDATE`](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS) locks the rows it returns; [`SKIP LOCKED`](https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE) lets workers claim different rows of a queue table without blocking. +- PostgreSQL [advisory locks](https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS) are app-defined locks on a number, not a row — for leader election or job dedup without a table to lock. ### Optimistic locking -- Add a `version` column: `UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?`; zero rows updated means someone else won — reload and retry. +- Add a [`version` column](https://en.wikipedia.org/wiki/Optimistic_concurrency_control): `UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?`; zero rows updated means someone else won — reload and retry. - Best when conflicts are rare; pessimistic locks when they're frequent. ### Deadlocks -- Two transactions each hold a lock the other needs; the database detects the cycle and **aborts one**. -- Prevent with a **consistent lock order** and short transactions. +- Two transactions each hold a lock the other needs; the database [detects the cycle](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS) and aborts one. +- Prevent with a consistent lock order and short transactions. ### DDL locks and the lock queue -- Most `ALTER TABLE` forms take an **`ACCESS EXCLUSIVE`** lock. If it waits behind a long query, **every later query queues behind it** — a brief outage from a "fast" migration. -- Set **`lock_timeout`** (a few seconds) on migrations and retry; rollout patterns in [devops.md](devops.md). +- Most `ALTER TABLE` forms take an [`ACCESS EXCLUSIVE`](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-TABLES) lock. If it waits behind a long query, every later query queues behind it — a brief outage from a "fast" migration. +- Set [`lock_timeout`](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT) (a few seconds) on migrations and retry; rollout patterns in [devops.md](devops.md). ## Storage engines and durability ### B-tree vs LSM-tree -| | B-tree (PostgreSQL, InnoDB) | LSM-tree (RocksDB, Cassandra) | +| | [B-tree](https://en.wikipedia.org/wiki/B-tree) (PostgreSQL, InnoDB) | [LSM-tree](https://en.wikipedia.org/wiki/Log-structured_merge-tree) ([RocksDB](https://rocksdb.org/), [Cassandra](https://cassandra.apache.org/doc/latest/cassandra/architecture/storage-engine.html)) | |---|---|---| -| Writes | in-place page updates, random I/O | append to **memtable** → flush immutable **SSTables** | -| Reads | one tree walk, predictable | may check several files; **bloom filters** skip most | -| Background work | page splits, vacuum | **compaction** | +| Writes | in-place page updates, random I/O | append to [memtable](https://cassandra.apache.org/doc/latest/cassandra/architecture/storage-engine.html#memtables) → flush immutable [SSTables](https://cassandra.apache.org/doc/latest/cassandra/architecture/storage-engine.html#sstables) | +| Reads | one tree walk, predictable | may check several files; [bloom filters](https://en.wikipedia.org/wiki/Bloom_filter) skip most | +| Background work | page splits, [vacuum](https://www.postgresql.org/docs/current/routine-vacuuming.html) | [compaction](https://github.com/facebook/rocksdb/wiki/Compaction) | | Fits | read-heavy, range scans | write-heavy | -- Compaction strategy trades **read, write, and space amplification** against each other. +- Compaction strategy trades [read, write, and space amplification](https://smalldatum.blogspot.com/2015/11/read-write-space-amplification-pick-2_23.html) against each other. ### Write-ahead log -- A commit is durable once its **WAL** record is fsynced; dirty data pages are flushed later (**checkpoints**, background writer), never before their WAL. -- Crash recovery replays WAL from the last checkpoint: PostgreSQL only redoes (uncommitted rows stay invisible by commit status); InnoDB redoes, then rolls back with undo. -- **Group commit** batches many commits into one fsync. +- A commit is durable once its [WAL](https://www.postgresql.org/docs/current/wal-intro.html) record is [fsynced](os.md); dirty data pages are flushed later ([checkpoints](https://www.postgresql.org/docs/current/wal-configuration.html), background writer), never before their WAL. +- Crash recovery replays WAL from the last checkpoint: PostgreSQL only redoes (uncommitted rows stay invisible by commit status); InnoDB redoes, then rolls back with [undo](https://dev.mysql.com/doc/refman/8.4/en/innodb-undo-logs.html). +- [Group commit](https://www.postgresql.org/docs/current/wal-configuration.html) batches many commits into one fsync. ### Backups and PITR -- Logical backups (`pg_dump`): portable, slow to restore at scale. Physical (`pg_basebackup`, snapshots): fast restore, same major version. -- **PITR** = physical base backup + WAL replayed to a chosen moment. Replicas are **not backups** — they replicate mistakes too. +- Logical backups ([`pg_dump`](https://www.postgresql.org/docs/current/app-pgdump.html)): portable, slow to restore at scale. Physical ([`pg_basebackup`](https://www.postgresql.org/docs/current/app-pgbasebackup.html), snapshots): fast restore, same major version. +- [PITR](https://www.postgresql.org/docs/current/continuous-archiving.html) = physical base backup + [WAL](https://www.postgresql.org/docs/current/wal-intro.html) replayed to a chosen moment. Replicas are not backups — they replicate mistakes too. ## Query execution and tuning ### Logical evaluation order -- **`FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT`** — why a `SELECT` alias works in `ORDER BY` but not in `WHERE`. +`FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT` — why a `SELECT` alias works in `ORDER BY` but not in `WHERE`. ### Why an index isn't used -- Low selectivity: when a filter matches a large share of rows, the planner prefers a **sequential scan**, since one sequential pass beats many random heap reads. -- `LIKE 'x%'` uses a B-tree only with C collation or `text_pattern_ops`; `LIKE '%x'` needs a trigram GIN index (`pg_trgm`). +- Low selectivity: when a filter matches a large share of rows, the planner prefers a [sequential scan](https://www.postgresql.org/docs/current/using-explain.html), since one sequential pass beats many random heap reads. +- `LIKE 'x%'` uses a B-tree only with C collation or [`text_pattern_ops`](https://www.postgresql.org/docs/current/indexes-opclass.html); `LIKE '%x'` needs a trigram GIN or GiST index ([`pg_trgm`](https://www.postgresql.org/docs/current/pgtrgm.html)). - A function or implicit cast on the column, and `OR` across columns without an index on each, also defeat it. -- **Stale statistics** show up in `EXPLAIN ANALYZE` as estimated vs actual rows far apart — run `ANALYZE`; also watch for sorts or hashes spilling to disk. -- Scan types: Seq Scan, Index Scan, Index Only Scan, and **Bitmap Heap Scan** (collects matches from one or more indexes, then reads pages in physical order). -- `OFFSET` still reads and discards every skipped row, so deep pages get slower; keyset pagination seeks by index — API side in [backend.md](backend.md). +- Stale statistics show up in [`EXPLAIN ANALYZE`](https://www.postgresql.org/docs/current/using-explain.html#USING-EXPLAIN-ANALYZE) as estimated vs actual rows far apart — run [`ANALYZE`](https://www.postgresql.org/docs/current/sql-analyze.html); also watch for sorts or hashes spilling to disk. +- Scan types: Seq Scan, Index Scan, Index Only Scan, and [Bitmap Heap Scan](https://www.postgresql.org/docs/current/indexes-bitmap-scans.html) (collects matches from one or more indexes, then reads pages in physical order). +- `OFFSET` still reads and discards every skipped row, so deep pages get slower; [keyset pagination](https://use-the-index-luke.com/no-offset) seeks by index — API side in [backend.md](backend.md). ### Join algorithms | Join | Best when | |---|---| -| **Nested loop** | outer side small, inner side index-backed | -| **Hash join** | large equality joins without a useful index; builds on the smaller side | -| **Merge join** | both inputs already sorted (e.g. from indexes) | +| [Nested loop](https://en.wikipedia.org/wiki/Nested_loop_join) | outer side small, inner side index-backed | +| [Hash join](https://en.wikipedia.org/wiki/Hash_join) | large equality joins without a useful index; builds on the smaller side | +| [Merge join](https://en.wikipedia.org/wiki/Sort-merge_join) | both inputs already sorted (e.g. from indexes) | ## SQL gotchas ### NULL and three-valued logic -- Any comparison with NULL — even **`NULL = NULL`** — is UNKNOWN, and `WHERE` drops it; use `IS [NOT] NULL` or `IS [NOT] DISTINCT FROM`. -- **`NOT IN` with a NULL in the list returns no rows** — use `NOT EXISTS`. -- `COUNT(*)` counts rows; `COUNT(col)`, `SUM`, `AVG`, `MIN`/`MAX` skip NULLs, but `array_agg`/`json_agg` keep them. +- Any comparison with NULL — even `NULL = NULL` — is [UNKNOWN](https://en.wikipedia.org/wiki/Null_%28SQL%29#Comparisons_with_NULL_and_the_three-valued_logic_%283VL%29), and `WHERE` drops it; use `IS [NOT] NULL` or [`IS [NOT] DISTINCT FROM`](https://www.postgresql.org/docs/current/functions-comparison.html). +- [`NOT IN`](https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-NOTIN) with a NULL in the list returns no rows — use [`NOT EXISTS`](https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-EXISTS). +- [`COUNT(*)`](https://www.postgresql.org/docs/current/functions-aggregate.html) counts rows; `COUNT(col)`, `SUM`, `AVG`, `MIN`/`MAX` skip NULLs, but `array_agg`/`json_agg` keep them. ### Window functions -- `fn() OVER (PARTITION BY ... ORDER BY ...)` computes per row without collapsing rows. -- `ROW_NUMBER` 1,2,3; **`RANK`** 1,1,3 (gaps); **`DENSE_RANK`** 1,1,2. -- The default frame with `ORDER BY` is **`RANGE ... CURRENT ROW`**, which includes all peer rows with equal sort keys — use `ROWS` for a true running total. +- [`fn() OVER (PARTITION BY ... ORDER BY ...)`](https://www.postgresql.org/docs/current/tutorial-window.html) computes per row without collapsing rows. +- [`ROW_NUMBER`](https://www.postgresql.org/docs/current/functions-window.html) 1,2,3; `RANK` 1,1,3 (gaps); `DENSE_RANK` 1,1,2. +- The [default frame](https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-WINDOW-FUNCTIONS) with `ORDER BY` is `RANGE ... CURRENT ROW`, which includes all peer rows with equal sort keys — use `ROWS` for a true running total. ### CTE materialization -- **Since PostgreSQL 12**, a side-effect-free CTE referenced **once** is inlined into the outer query; one referenced several times is materialized. +- Since PostgreSQL 12, a non-recursive, side-effect-free CTE referenced once is [inlined](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CTE-MATERIALIZATION) into the outer query; one referenced several times is materialized. - `MATERIALIZED` / `NOT MATERIALIZED` override the default. ### Join pitfalls -- A condition on the right table in **`WHERE`** discards the NULL-extended rows and turns a `LEFT JOIN` into an inner join; put it in `ON`. -- Joining a parent to **two one-to-many children** multiplies rows (fan-out) and inflates `SUM`/`COUNT`; aggregate each child in a subquery first. -- `EXISTS` / `NOT EXISTS` are semi- and anti-joins: they never duplicate rows, unlike `JOIN` + `DISTINCT`. +- A condition on the right table in `WHERE` (other than `IS NULL`) discards the NULL-extended rows and turns a [`LEFT JOIN`](https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-JOIN) into an inner join; put it in `ON`. +- Joining a parent to two one-to-many children multiplies rows (fan-out) and inflates `SUM`/`COUNT`; aggregate each child in a subquery first. +- [`EXISTS`](https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-EXISTS) / `NOT EXISTS` are [semi-](https://en.wikipedia.org/wiki/Join_%28relational_algebra%29#Semijoin) and [anti-joins](https://en.wikipedia.org/wiki/Join_%28relational_algebra%29#Antijoin): they never duplicate rows, unlike `JOIN` + `DISTINCT`. ### Upsert -- **`INSERT ... ON CONFLICT (...) DO UPDATE`** (PostgreSQL) / `ON DUPLICATE KEY UPDATE` (MySQL) replaces a racy check-then-write. +[`INSERT ... ON CONFLICT (...) DO UPDATE`](https://www.postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT) (PostgreSQL) / [`ON DUPLICATE KEY UPDATE`](https://dev.mysql.com/doc/refman/8.4/en/insert-on-duplicate.html) (MySQL) replaces a racy check-then-write. ## Non-relational stores ### Redis -- Commands execute on **one thread**, so each command (and Lua script or `MULTI` block) is atomic; a slow command (`KEYS *`, big `SMEMBERS`) blocks everyone. -- Structures: strings, hashes, lists, sets, **sorted sets** (skip list + hash — leaderboards, rate limiters), streams, HyperLogLog. -- Redis Cluster splits keys into **16,384 hash slots**; multi-key operations need the same slot, forced with hash tags (`{user42}:cart`). +- Commands execute on one thread, so each command (and [Lua script](https://redis.io/docs/latest/develop/programmability/eval-intro/) or [`MULTI` block](https://redis.io/docs/latest/develop/using-commands/transactions/)) is atomic; a slow command ([`KEYS *`](https://redis.io/docs/latest/commands/keys/), big `SMEMBERS`) blocks everyone. +- Structures: strings, hashes, lists, sets, [sorted sets](https://redis.io/docs/latest/develop/data-types/sorted-sets/) ([skip list](https://en.wikipedia.org/wiki/Skip_list) + hash — leaderboards, rate limiters), [streams](https://redis.io/docs/latest/develop/data-types/streams/), [HyperLogLog](https://redis.io/docs/latest/develop/data-types/probabilistic/hyperloglogs/). +- Redis Cluster splits keys into [16,384 hash slots](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/); multi-key operations need the same slot, forced with [hash tags](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/#hash-tags) (`{user42}:cart`). ### Redis persistence and replication -- RDB: periodic fork + snapshot (copy-on-write), loses writes since the last one. AOF: logs every write; `appendfsync everysec` (default) loses up to **~1 s**. -- Replication is **asynchronous** — failover can drop acknowledged writes. +- [RDB](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/): periodic fork + snapshot ([copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write)), loses writes since the last one. [AOF](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/#append-only-file): logs every write; `appendfsync everysec` (default) loses up to ~1 s. +- Replication is [asynchronous](https://redis.io/docs/latest/operate/oss_and_stack/management/replication/) — failover can drop acknowledged writes. ### Redis eviction, expiry, and messaging -- `maxmemory-policy` defaults to **`noeviction`**: at the memory limit, writes fail with an OOM error. +- [`maxmemory-policy`](https://redis.io/docs/latest/develop/reference/eviction/#eviction-policies) defaults to `noeviction`: at the memory limit, writes fail with an OOM error. - Caches use `allkeys-lru` or `allkeys-lfu`; `volatile-*` policies evict only keys with a TTL, so without TTLs they behave like `noeviction`. -- Expired keys are removed **lazily** on access plus by background sampling, so memory lags behind expirations. -- Pub/sub is fire-and-forget (offline subscribers miss messages); Streams persist entries, with consumer groups, acknowledgments, and redelivery of pending entries. +- Expired keys are removed [lazily](https://redis.io/docs/latest/commands/expire/) on access plus by background sampling, so memory lags behind expirations. +- [Pub/sub](https://redis.io/docs/latest/develop/pubsub/) is fire-and-forget (offline subscribers miss messages); [Streams](https://redis.io/docs/latest/develop/data-types/streams/) persist entries, with consumer groups, acknowledgments, and redelivery of pending entries. ### Document stores (MongoDB) -- **Embed** data that is read together (one read, atomic update); reference unbounded or shared data — a document is capped at **16 MB**. -- Single-document writes are atomic; multi-document transactions (replica sets since 4.0, sharded clusters since 4.2) cost more, so design for single-document atomicity. -- Write concern **`w: "majority"`** is the default since 5.0; `w: 1` acknowledges before replication, so a failover can roll back acknowledged writes. -- A query without a matching index scans the collection (`COLLSCAN`); order compound index keys by the ESR rule: Equality, Sort, Range. +- [Embed](https://www.mongodb.com/docs/manual/data-modeling/embedding/) data that is read together (one read, atomic update); reference unbounded or shared data — a document is capped at [16 MB](https://www.mongodb.com/docs/manual/reference/limits/#bson-documents). +- Single-document writes are atomic; [multi-document transactions](https://www.mongodb.com/docs/manual/core/transactions/) (replica sets since 4.0, sharded clusters since 4.2) cost more, so design for single-document atomicity. +- [Write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) `w: "majority"` is the default since 5.0 (except some arbiter setups); `w: 1` acknowledges before replication, so a failover can roll back acknowledged writes. +- A query without a matching index scans the collection ([`COLLSCAN`](https://www.mongodb.com/docs/manual/reference/explain-results/#collection-scan)); order compound index keys by the [ESR rule](https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-guideline/): Equality, Sort, Range. ### Wide-column key design (Cassandra, DynamoDB) -- The **partition key** picks the node; the sort/clustering key orders rows inside the partition — an efficient query must supply the partition key; otherwise it's a full scan (DynamoDB `Scan`, Cassandra `ALLOW FILTERING`) or a secondary index. -- Model **tables per query** (denormalized), not per entity; unbounded partitions (all events of a popular user) become hot and huge — add a time bucket to the key. -- Deletes write **tombstones**, which slow reads until compaction purges them. +- The [partition key](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html) picks the node; the sort/clustering key orders rows inside the partition — an efficient query must supply the partition key; otherwise it's a full scan (DynamoDB [`Scan`](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html), Cassandra [`ALLOW FILTERING`](https://cassandra.apache.org/doc/latest/cassandra/developing/cql/dml.html#allow-filtering)) or a secondary index. +- Model [tables per query](https://cassandra.apache.org/doc/latest/cassandra/developing/data-modeling/index.html) (denormalized), not per entity; unbounded partitions (all events of a popular user) become hot and huge — add a time bucket to the key. +- Deletes write [tombstones](https://cassandra.apache.org/doc/latest/cassandra/managing/operating/compaction/tombstones.html), which slow reads until [compaction](https://cassandra.apache.org/doc/latest/cassandra/managing/operating/compaction/index.html) purges them. ### Search engines and inverted indexes -- An **inverted index** maps each term to the list of documents containing it; scoring is **BM25**. -- Elasticsearch/OpenSearch are **near-real-time** (new docs searchable after a refresh, ~1 s default), and the primary shard count is fixed at index creation — resize by reindexing or split/shrink. -- Treat the search index as a derived store, fed by CDC from the source of truth. +- An [inverted index](https://en.wikipedia.org/wiki/Inverted_index) maps each term to the list of documents containing it; scoring is [BM25](https://en.wikipedia.org/wiki/Okapi_BM25). +- Elasticsearch/OpenSearch are [near-real-time](https://www.elastic.co/docs/manage-data/data-store/near-real-time-search) (new docs searchable after a refresh, ~1 s default), and the primary shard count is fixed at index creation — resize by reindexing or [split](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-split)/shrink. +- Treat the search index as a derived store, fed by [CDC](distributed.md) from the source of truth. ## Schema design ### Normalization -- 1NF atomic values → 2NF no partial-key dependency → **3NF** no transitive dependency through non-key columns: each fact stored once. -- **BCNF** is stricter: every nontrivial dependency's determinant is a superkey; 3NF still allows it when the dependent column is part of a candidate key. -- **Denormalize** measured hot paths only (copied columns, counters, materialized views), accepting harder writes. +- [1NF](https://en.wikipedia.org/wiki/First_normal_form) atomic values → [2NF](https://en.wikipedia.org/wiki/Second_normal_form) no partial-key dependency → [3NF](https://en.wikipedia.org/wiki/Third_normal_form) no transitive dependency through non-key columns: each fact stored once. +- [BCNF](https://en.wikipedia.org/wiki/Boyce%E2%80%93Codd_normal_form) is stricter: every nontrivial dependency's determinant is a superkey; 3NF still allows it when the dependent column is part of a candidate key. +- [Denormalize](https://en.wikipedia.org/wiki/Denormalization) measured hot paths only (copied columns, counters, [materialized views](https://www.postgresql.org/docs/current/rules-materializedviews.html)), accepting harder writes. ### Surrogate keys - Auto-increment bigint: compact and ordered, but reveals counts and needs one sequence. -- **UUIDv4** scatters inserts across the B-tree; **UUIDv7** is time-ordered and native via **`uuidv7()` since PostgreSQL 18** — ID schemes in [distributed.md](distributed.md). +- [UUIDv4](https://www.rfc-editor.org/rfc/rfc9562#section-5.4) scatters inserts across the B-tree; [UUIDv7](https://www.rfc-editor.org/rfc/rfc9562#section-5.7) is time-ordered and native via [`uuidv7()`](https://www.postgresql.org/docs/current/functions-uuid.html) since PostgreSQL 18 — ID schemes in [distributed.md](distributed.md). ### Constraints and soft deletes -- `FOREIGN KEY`, `CHECK`, and `UNIQUE` are the last line of defense — application checks alone race. -- **Soft deletes** (`deleted_at`) keep deleted rows for undo and audit; a partial unique index (`WHERE deleted_at IS NULL`) frees the value for reuse, so an undelete can then conflict. +- [`FOREIGN KEY`](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK), [`CHECK`](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS), and [`UNIQUE`](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS) are the last line of defense — application checks alone race. +- Soft deletes (`deleted_at`) keep deleted rows for undo and audit; a [partial unique index](https://www.postgresql.org/docs/current/indexes-partial.html) (`WHERE deleted_at IS NULL`) frees the value for reuse, so an undelete can then conflict. ## Scaling ### Physical vs logical replication -- **Physical (streaming)**: ships WAL bytes, exact copy, same major version. -- **Logical**: row changes per table, works across versions and subsets, feeds CDC. -- PostgreSQL **`synchronous_commit`** picks how far a commit waits (`remote_write`, `on`, `remote_apply`); sync vs async trade-offs are in [distributed.md](distributed.md). +- Physical ([streaming](https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION)): ships [WAL](https://www.postgresql.org/docs/current/wal-intro.html) bytes, exact copy, same major version. +- [Logical](https://www.postgresql.org/docs/current/logical-replication.html): row changes per table, works across versions and subsets, feeds [CDC](distributed.md). +- PostgreSQL [`synchronous_commit`](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-SYNCHRONOUS-COMMIT) picks how far a commit waits (`remote_write`, `on`, `remote_apply`); sync vs async trade-offs are in [distributed.md](distributed.md). ### Table partitioning -- **Range** (most common, by date), list, or hash; **partition pruning** skips irrelevant partitions. -- Dropping an old partition is a cheap metadata change, unlike a bloating bulk `DELETE`, but takes an `ACCESS EXCLUSIVE` lock on the parent — `DETACH PARTITION CONCURRENTLY` (PostgreSQL 14+) first avoids that. -- PostgreSQL unique constraints must include the partition key. +- [Range](https://www.postgresql.org/docs/current/ddl-partitioning.html) (most common, by date), list, or hash; [partition pruning](https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITION-PRUNING) skips irrelevant partitions. +- Dropping an old partition is a cheap metadata change, unlike a bloating bulk `DELETE`, but takes an [`ACCESS EXCLUSIVE`](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-TABLES) lock on the parent — [`DETACH PARTITION CONCURRENTLY`](https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-DETACH-PARTITION) (PostgreSQL 14+) first avoids that. +- PostgreSQL [unique constraints must include the partition key](https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE-LIMITATIONS). ### Connection pooling -- Each PostgreSQL connection is a **process** (a few MB), so thousands of direct connections hurt — pool them. -- PgBouncer session mode keeps `SET` and advisory locks; **transaction mode** scales further but loses session state; protocol-level prepared statements work in it **since PgBouncer 1.21** when `max_prepared_statements` > 0 (default 0 before 1.24, now 200); SQL `PREPARE` still doesn't. +- Each PostgreSQL connection is a [process](https://www.postgresql.org/docs/current/connect-estab.html) (a few MB), so thousands of direct connections hurt — pool them. +- [PgBouncer](https://www.pgbouncer.org/features.html) session mode keeps `SET` and [advisory locks](https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS); transaction mode scales further but loses session state; protocol-level prepared statements work in it since PgBouncer 1.21 when [`max_prepared_statements`](https://www.pgbouncer.org/config.html#max_prepared_statements) > 0 (default 0 before 1.24, now 200); SQL [`PREPARE`](https://www.postgresql.org/docs/current/sql-prepare.html) still doesn't. ### OLTP vs OLAP -- **OLAP**: **column storage**, compression, vectorized scans of few columns over many rows, star schemas (facts + dimensions). -- Materialized views store a query result for fast reads that stay stale until refresh; `REFRESH MATERIALIZED VIEW CONCURRENTLY` avoids blocking reads but needs a unique index. +- [OLAP](https://en.wikipedia.org/wiki/Online_analytical_processing): [column storage](https://en.wikipedia.org/wiki/Column-oriented_DBMS), compression, vectorized scans of few columns over many rows, [star schemas](https://en.wikipedia.org/wiki/Star_schema) (facts + dimensions). +- [Materialized views](https://www.postgresql.org/docs/current/rules-materializedviews.html) store a query result for fast reads that stay stale until refresh; [`REFRESH MATERIALIZED VIEW CONCURRENTLY`](https://www.postgresql.org/docs/current/sql-refreshmaterializedview.html) avoids blocking reads but needs a unique index. diff --git a/docs/topics/networking.md b/docs/topics/networking.md index 80699bb..6c7bbdf 100644 --- a/docs/topics/networking.md +++ b/docs/topics/networking.md @@ -6,147 +6,147 @@ What experienced engineers forget about networking before an interview, grouped ### Loading a URL -- DNS resolves the name → TCP handshake (1 RTT) → **TLS 1.3** handshake (1 RTT) → HTTP request → response → the browser parses and renders ([frontend.md](frontend.md)). -- A cold HTTPS request over TCP costs about **3 RTTs** before the first response byte; **HTTP/3** saves one, and resumption with 0-RTT saves more. -- Every later request reuses the connection, which is why keep-alive and connection pools matter. +- DNS resolves the name → [TCP handshake](https://www.rfc-editor.org/rfc/rfc9293#section-3.5) (1 RTT) → [TLS 1.3](https://www.rfc-editor.org/rfc/rfc9846) handshake (1 RTT) → HTTP request → response → the browser parses and renders ([frontend.md](frontend.md)). +- A cold HTTPS request over TCP costs about 3 RTTs before the first response byte; [HTTP/3](https://www.rfc-editor.org/rfc/rfc9114) saves one, and resumption with [0-RTT](https://www.rfc-editor.org/rfc/rfc9846#section-2.3) saves more. +- Every later request reuses the connection, which is why [keep-alive](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) and connection pools matter. ### Connection failure signatures | Symptom | Usual cause | Check with | |---|---|---| -| Name doesn't resolve, or resolves to an old IP | NXDOMAIN, stale cache, TTL not expired | `dig` (answer, TTL) | -| Connection refused | RST: host reachable, nothing listening on the port | `ss -tlnp` on the server | -| Connect timeout | SYN dropped: firewall, security group, or dead host; Linux retries the SYN for **~127 s**, so set connect timeouts | `traceroute`/`mtr`, `tcpdump` for SYNs | -| Connection reset by peer | peer crashed, or a middlebox dropped the flow after its idle timeout | `tcpdump`/Wireshark around the RST | -| **502** from a proxy | upstream refused, reset, or sent an invalid response | proxy error log, `ss -tanp` states | -| **504** from a proxy, or just slow | upstream didn't answer in time | `curl -w '%{time_connect} %{time_appconnect} %{time_starttransfer}'` per phase | +| Name doesn't resolve, or resolves to an old IP | [NXDOMAIN](https://www.rfc-editor.org/rfc/rfc9499#section-3), stale cache, [TTL](https://en.wikipedia.org/wiki/Time_to_live#DNS_records) not expired | [`dig`](https://bind9.readthedocs.io/en/latest/manpages.html#dig-dns-lookup-utility) (answer, TTL) | +| Connection refused | RST: host reachable, nothing listening on the port | [`ss -tlnp`](https://man7.org/linux/man-pages/man8/ss.8.html) on the server | +| Connect timeout | SYN dropped: firewall, security group, or dead host; Linux [retries the SYN](https://docs.kernel.org/networking/ip-sysctl.html#tcp-variables) for ~131 s (~127 s before Linux 6.5), so set connect timeouts | `traceroute`/`mtr`, [`tcpdump`](https://www.tcpdump.org/manpages/tcpdump.1.html) for SYNs | +| Connection reset by peer | peer crashed, or a middlebox dropped the flow after its idle timeout | `tcpdump`/[Wireshark](https://www.wireshark.org/docs/) around the RST | +| [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/502) from a proxy | upstream refused, reset, or sent an invalid response | proxy error log, `ss -tanp` states | +| [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/504) from a proxy, or just slow | upstream didn't answer in time | [`curl -w '%{time_connect} %{time_appconnect} %{time_starttransfer}'`](https://curl.se/docs/manpage.html#-w) per phase | ## TCP ### Handshake and connection queues -- SYN → SYN-ACK → ACK: one RTT before any data. -- The kernel keeps a **SYN queue** (half-open) and an **accept queue** (established, waiting for `accept()`); an overflowing accept queue drops connections under load. -- A SYN flood fills the SYN queue; **SYN cookies** encode state in the sequence number so no queue slot is needed. +- [SYN → SYN-ACK → ACK](https://www.rfc-editor.org/rfc/rfc9293#section-3.5): one RTT before any data. +- The kernel keeps a [SYN queue](https://blog.cloudflare.com/syn-packet-handling-in-the-wild/) (half-open) and an [accept queue](https://man7.org/linux/man-pages/man2/listen.2.html) (established, waiting for [`accept()`](https://man7.org/linux/man-pages/man2/accept.2.html)); an overflowing accept queue drops connections under load. +- A [SYN flood](https://en.wikipedia.org/wiki/SYN_flood) fills the SYN queue; [SYN cookies](https://en.wikipedia.org/wiki/SYN_cookies) encode state in the sequence number so no queue slot is needed. ### Teardown and TIME_WAIT -- The side that closes first enters **TIME_WAIT** for 2×MSL (**60 s** on Linux) so delayed packets can't corrupt a new connection on the same port pair. -- Many short-lived outbound connections exhaust ephemeral ports (~28k by default) — reuse connections (keep-alive, pooling) instead of tuning the kernel. -- Many connections in **CLOSE_WAIT** mean the application never called `close()` — a bug in your code, not the network. +- The side that closes first enters [TIME_WAIT](https://www.rfc-editor.org/rfc/rfc9293#section-3.6.1) for 2×MSL (60 s on Linux) so delayed packets can't corrupt a new connection on the same port pair. +- Many short-lived outbound connections exhaust [ephemeral ports](https://en.wikipedia.org/wiki/Ephemeral_port) (~28k by default) — reuse connections (keep-alive, pooling) instead of tuning the kernel. +- Many connections in [CLOSE_WAIT](https://www.rfc-editor.org/rfc/rfc9293#section-3.6) mean the application never called [`close()`](https://man7.org/linux/man-pages/man2/close.2.html) — a bug in your code, not the network. ### Flow and congestion control -- Flow control (receive window) protects the receiver; congestion control (congestion window) protects the network. -- **Slow start** begins at **10 segments (~14 KB)** and roughly doubles per RTT — why the first 14 KB of a page matter and why new connections are slow. -- Throughput ≤ window / RTT (**bandwidth-delay product**) — high-latency links need large windows. -- CUBIC (Linux default) backs off on packet loss; BBR models bandwidth and RTT instead, doing better on lossy links. +- [Flow control](https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Flow_control) (receive window) protects the receiver; [congestion control](https://www.rfc-editor.org/rfc/rfc5681) (congestion window) protects the network. +- [Slow start](https://www.rfc-editor.org/rfc/rfc5681#section-3.1) begins at [10 segments](https://www.rfc-editor.org/rfc/rfc6928) (~14 KB) and roughly doubles per RTT — why the first 14 KB of a page matter and why new connections are slow. +- Throughput ≤ window / RTT ([bandwidth-delay product](https://en.wikipedia.org/wiki/Bandwidth-delay_product)) — high-latency links need large windows. +- [CUBIC](https://www.rfc-editor.org/rfc/rfc9438) (Linux default) backs off on packet loss; [BBR](https://datatracker.ietf.org/doc/draft-ietf-ccwg-bbr/) models bandwidth and RTT instead, doing better on lossy links. ### Retransmission and loss recovery -- The retransmission timeout (**RTO**) derives from smoothed RTT; Linux floors it at **200 ms** (initial 1 s) and doubles it on each retry. -- One lost packet on a quiet connection therefore adds ≥ 200 ms — a classic cause of p99 spikes. -- **Fast retransmit**: 3 duplicate ACKs resend the missing segment without waiting for the RTO; SACK tells the sender exactly which ranges arrived. -- A lost last segment produces no duplicate ACKs; Linux's tail loss probe resends it after ~2 RTTs instead of waiting for the RTO. +- The retransmission timeout ([RTO](https://www.rfc-editor.org/rfc/rfc6298)) derives from smoothed RTT; Linux floors it at 200 ms (initial 1 s) and doubles it on each retry. +- One lost packet on a quiet connection can therefore add ≥ 200 ms — a classic cause of p99 spikes. +- [Fast retransmit](https://www.rfc-editor.org/rfc/rfc5681#section-3.2): 3 duplicate ACKs resend the missing segment without waiting for the RTO; [SACK](https://www.rfc-editor.org/rfc/rfc2018) tells the sender exactly which ranges arrived. +- A lost last segment produces no duplicate ACKs; Linux's [tail loss probe](https://www.rfc-editor.org/rfc/rfc8985) resends it after ~2 RTTs instead of waiting for the RTO. ### Keepalive and half-open connections -- **TCP keepalive** probes an idle connection to detect a dead peer (Linux: first probe after **2 hours** by default); HTTP keep-alive only means reusing a connection — unrelated. -- A peer that crashes or loses the network without sending FIN/RST leaves a half-open connection, invisible until you write or probe. -- Keep the client's idle timeout **shorter than the server's or load balancer's**, or requests land on connections the other side just closed (sporadic 502s and resets). +- [TCP keepalive](https://man7.org/linux/man-pages/man7/tcp.7.html) probes an idle connection to detect a dead peer (Linux: first probe after 2 hours by default); [HTTP keep-alive](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) only means reusing a connection — unrelated. +- A peer that crashes or loses the network without sending FIN/RST leaves a [half-open connection](https://www.rfc-editor.org/rfc/rfc9293#section-3.5.1), invisible until you write or probe. +- Keep the client's idle timeout shorter than the server's or load balancer's, or requests land on connections the other side just closed (sporadic 502s and resets). ### Nagle and delayed ACKs -- **Nagle's algorithm** holds small writes until earlier data is acknowledged; combined with the receiver's delayed ACK it adds up to **~40 ms** stalls for request-response traffic. -- Set **`TCP_NODELAY`** for latency-sensitive protocols (most RPC libraries do). +- [Nagle's algorithm](https://www.rfc-editor.org/rfc/rfc9293#section-3.7.4) holds small writes until earlier data is acknowledged; combined with the receiver's [delayed ACK](https://www.rfc-editor.org/rfc/rfc9293#section-3.8.6.3) it adds ~40 ms stalls on Linux (~200 ms on Windows) for request-response traffic. +- Set [`TCP_NODELAY`](https://man7.org/linux/man-pages/man7/tcp.7.html) for latency-sensitive protocols (most RPC libraries do). ### Head-of-line blocking -- TCP delivers bytes in order, so one lost packet stalls everything behind it — even data for unrelated HTTP/2 streams. -- HTTP/1.1 blocks at the request level, HTTP/2 fixes that but not TCP's, and **QUIC** fixes both: streams are delivered independently, so a lost packet stalls only the streams whose data it carried. +- TCP delivers bytes in order, so one lost packet [stalls everything behind it](https://en.wikipedia.org/wiki/Head-of-line_blocking) — even data for unrelated [HTTP/2 streams](https://www.rfc-editor.org/rfc/rfc9113#section-5). +- HTTP/1.1 blocks at the request level, HTTP/2 fixes that but not TCP's, and [QUIC](https://www.rfc-editor.org/rfc/rfc9000) fixes both: streams are delivered independently, so a lost packet stalls only the streams whose data it carried. ## HTTP ### HTTP/1.1 -- Persistent connections (keep-alive), but **one outstanding request per connection** in practice (pipelining is unused). -- Browsers open about **6 connections per host** to parallelize. +- [Persistent connections](https://www.rfc-editor.org/rfc/rfc9112#section-9.3) (keep-alive), but one outstanding request per connection in practice ([pipelining](https://www.rfc-editor.org/rfc/rfc9112#section-9.3.2) is unused). +- Browsers open about 6 connections per host to parallelize. ### HTTP/2 -- Binary framing with many **multiplexed streams** on one TCP connection and **HPACK** header compression. +- Binary framing with many [multiplexed streams](https://www.rfc-editor.org/rfc/rfc9113#section-5) on one TCP connection and [HPACK](https://www.rfc-editor.org/rfc/rfc7541) header compression. - Fixes HTTP-level head-of-line blocking but not TCP-level: one lost packet stalls all streams. -- Server push is effectively dead — **Chrome removed it in 2022**; use `103 Early Hints` or preload instead. +- Server push is effectively dead — [Chrome removed it in 2022](https://developer.chrome.com/blog/removing-push); use [`103 Early Hints`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/103) or [preload](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload) instead. ### HTTP/3 and QUIC -- UDP has no handshake, ordering, or retransmission, so QUIC implements them in user space; DNS, VoIP, and games use UDP directly and handle loss themselves. -- QUIC runs over UDP with TLS 1.3 built in: **1-RTT** setup (0-RTT on resumption) and independent stream delivery, so no cross-stream head-of-line blocking; loss detection and congestion control stay per connection. -- **Connection migration**: a connection ID survives an IP change (Wi-Fi → cellular). -- Clients discover it via the `Alt-Svc` header or a DNS HTTPS record; many networks block UDP, so browsers fall back to TCP. +- [UDP](https://www.rfc-editor.org/rfc/rfc768) has no handshake, ordering, or retransmission, so [QUIC](https://www.rfc-editor.org/rfc/rfc9000) implements them in user space; DNS, VoIP, and games use UDP directly and handle loss themselves. +- QUIC runs over UDP with [TLS 1.3 built in](https://www.rfc-editor.org/rfc/rfc9001): 1-RTT setup (0-RTT on resumption) and independent stream delivery, so no cross-stream head-of-line blocking; [loss detection and congestion control](https://www.rfc-editor.org/rfc/rfc9002) stay per connection. +- [Connection migration](https://www.rfc-editor.org/rfc/rfc9000#section-9): a connection ID survives an IP change (Wi-Fi → cellular). +- Clients discover it via the [`Alt-Svc`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Alt-Svc) header or a [DNS HTTPS record](https://www.rfc-editor.org/rfc/rfc9460); many networks block UDP, so browsers fall back to TCP. ### WebSocket handshake -- Starts as HTTP/1.1 **`Upgrade: websocket`** → **`101 Switching Protocols`**, then framed full-duplex messages on the same TCP connection. -- Proxies and load balancers need idle timeouts above the app's ping interval, or they drop quiet connections. +- Starts as HTTP/1.1 [`Upgrade: websocket`](https://www.rfc-editor.org/rfc/rfc6455#section-4) → [`101 Switching Protocols`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/101), then framed full-duplex messages on the same TCP connection. +- Proxies and load balancers need idle timeouts above the app's [ping interval](https://www.rfc-editor.org/rfc/rfc6455#section-5.5.2), or they drop quiet connections. ### Client IP behind proxies -- L7 proxies append the peer's address to **`X-Forwarded-For`**; clients can forge earlier entries, so trust only those your own proxies added — read from the right, skipping known proxy hops. -- L4 load balancers either preserve the source IP or pass it in a **PROXY protocol** header, which the backend must be configured to expect. +- L7 proxies append the peer's address to [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For); clients can forge earlier entries, so trust only those your own proxies added — read from the right, skipping known proxy hops. +- L4 load balancers either preserve the source IP or pass it in a [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) header, which the backend must be configured to expect. - Rate limits, geo-blocking, and audit logs depend on it: taking the leftmost entry lets any client spoof its IP. ## DNS ### Resolution path -- Stub resolver → **recursive resolver** (ISP, 1.1.1.1, VPC resolver) → root → TLD → authoritative server; each answer is cached for its **TTL** at every layer. -- Failed lookups are cached too (**negative caching**, from the SOA record). +- Stub resolver → [recursive resolver](https://en.wikipedia.org/wiki/Domain_Name_System#Recursive_and_caching_name_server) (ISP, 1.1.1.1, VPC resolver) → [root](https://en.wikipedia.org/wiki/Root_name_server) → [TLD](https://en.wikipedia.org/wiki/Top-level_domain) → authoritative server; each answer is cached for its [TTL](https://en.wikipedia.org/wiki/Time_to_live#DNS_records) at every layer. +- Failed lookups are cached too ([negative caching](https://www.rfc-editor.org/rfc/rfc2308), from the [SOA record](https://en.wikipedia.org/wiki/SOA_record)). ### Record types -- A/AAAA (IPv4/IPv6), **CNAME** (alias — **not allowed at the zone apex**), MX, NS, SRV. -- TXT carries SPF, DKIM, DMARC, and domain verification; **CAA** limits which CAs may issue certificates; HTTPS/SVCB advertise protocol hints such as HTTP/3. +- [A/AAAA](https://en.wikipedia.org/wiki/List_of_DNS_record_types) (IPv4/IPv6), [CNAME](https://www.rfc-editor.org/rfc/rfc1912#section-2.4) (alias — not allowed at the zone apex), MX, NS, SRV. +- TXT carries [SPF](https://www.rfc-editor.org/rfc/rfc7208), [DKIM](https://www.rfc-editor.org/rfc/rfc6376), [DMARC](https://www.rfc-editor.org/rfc/rfc9989), and domain verification; [CAA](https://www.rfc-editor.org/rfc/rfc8659) limits which CAs may issue certificates; [HTTPS/SVCB](https://www.rfc-editor.org/rfc/rfc9460) advertise protocol hints such as HTTP/3. ### TTL trade-offs -- Low TTL = faster failover and migration, more queries; high TTL = fewer queries, slow changes. -- Before a migration, lower the TTL **at least one old-TTL in advance** — resolvers keep the old record until it expires; some clients (JVMs, apps with their own cache) ignore TTLs anyway. +- Low [TTL](https://en.wikipedia.org/wiki/Time_to_live#DNS_records) = faster failover and migration, more queries; high TTL = fewer queries, slow changes. +- Before a migration, lower the TTL at least one old-TTL in advance — resolvers keep the old record until it expires; some clients (JVMs, apps with their own cache) ignore TTLs anyway. ### Transport and privacy -- UDP port 53 by default, TCP for large responses: an answer bigger than the client's advertised EDNS buffer size (commonly **1232 bytes**, to avoid fragmentation) is truncated and retried over TCP. -- DoT/DoH encrypt queries to the resolver; **DNSSEC** signs records for authenticity but doesn't encrypt them. +- UDP port 53 by default, [TCP for large responses](https://www.rfc-editor.org/rfc/rfc7766): an answer bigger than the client's advertised [EDNS](https://www.rfc-editor.org/rfc/rfc6891) buffer size (commonly [1232 bytes](https://www.dnsflagday.net/2020/), to avoid fragmentation) is truncated and retried over TCP. +- [DoT](https://www.rfc-editor.org/rfc/rfc7858)/[DoH](https://www.rfc-editor.org/rfc/rfc8484) encrypt queries to the resolver; [DNSSEC](https://www.rfc-editor.org/rfc/rfc9364) signs records for authenticity but doesn't encrypt them. ## IP and routing ### Addressing and CIDR -- `/24` = 256 addresses, `/16` = 65,536; each bit less doubles the block. -- Private ranges: **10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16**; 100.64.0.0/10 is carrier-grade NAT; AWS reserves **5 addresses** per subnet. +- [`/24`](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) = 256 addresses, `/16` = 65,536; each bit less doubles the block. +- [Private ranges](https://www.rfc-editor.org/rfc/rfc1918): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16; 100.64.0.0/10 is [carrier-grade NAT](https://www.rfc-editor.org/rfc/rfc6598); AWS reserves [5 addresses](https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html) per subnet. ### Routing tables -- The kernel picks the **longest prefix match**: a `/32` route beats a `/24`, which beats the default route `0.0.0.0/0` (the gateway). -- On the local link, **ARP** (IPv6: neighbor discovery) resolves the next hop's MAC address; MACs change at every hop, while IPs stay end to end unless NAT rewrites them. -- `ip route get ` shows the route, interface, and source address the kernel would use. +- The kernel picks the [longest prefix match](https://en.wikipedia.org/wiki/Longest_prefix_match): a `/32` route beats a `/24`, which beats the default route `0.0.0.0/0` (the gateway). +- On the local link, [ARP](https://www.rfc-editor.org/rfc/rfc826) (IPv6: [neighbor discovery](https://www.rfc-editor.org/rfc/rfc4861)) resolves the next hop's MAC address; MACs change at every hop, while IPs stay end to end unless NAT rewrites them. +- [`ip route get `](https://man7.org/linux/man-pages/man8/ip-route.8.html) shows the route, interface, and source address the kernel would use. ### NAT -- NAT rewrites source IP and port and tracks each flow in a **connection-tracking table** — unsolicited inbound connections are dropped unless a port forward or static mapping exists. -- One public IP offers at most ~64k source ports **per destination IP:port** (AWS NAT gateway: **55k**), so heavy traffic to one endpoint exhausts it; a full conntrack table drops new connections. +- [NAT](https://en.wikipedia.org/wiki/Network_address_translation) rewrites source IP and port and tracks each flow in a [connection-tracking table](https://en.wikipedia.org/wiki/Netfilter#Connection_tracking) — unsolicited inbound connections are dropped unless a [port forward](https://en.wikipedia.org/wiki/Port_forwarding) or static mapping exists. +- One public IP offers at most ~64k source ports per destination IP:port (AWS NAT gateway: [55k](https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-basics.html)), so heavy traffic to one endpoint exhausts it; a full [conntrack table](https://docs.kernel.org/networking/nf_conntrack-sysctl.html) drops new connections. ### MTU and fragmentation -- Ethernet **MTU 1500** → TCP MSS 1460 over IPv4 (1440 over IPv6); tunnels (VPN, VXLAN) shrink it. -- **Path MTU discovery** needs ICMP "fragmentation needed" messages; firewalls that drop ICMP cause **black holes** — small requests work, large ones hang. +- Ethernet [MTU](https://en.wikipedia.org/wiki/Maximum_transmission_unit) 1500 → TCP [MSS](https://en.wikipedia.org/wiki/Maximum_segment_size) 1460 over IPv4 (1440 over IPv6); tunnels (VPN, [VXLAN](https://www.rfc-editor.org/rfc/rfc7348)) shrink it. +- [Path MTU discovery](https://www.rfc-editor.org/rfc/rfc1191) needs ICMP "fragmentation needed" messages; firewalls that drop ICMP cause [black holes](https://en.wikipedia.org/wiki/Path_MTU_Discovery#Problems) — small requests work, large ones hang. ### Anycast and BGP -- **BGP** exchanges routes between autonomous systems; a bad announcement or withdrawal can take a whole company offline (Facebook, October 2021). -- **Anycast** announces one IP from many locations and BGP delivers each client to the nearest by routing policy (usually, not always, the lowest-latency) — CDNs, public DNS resolvers, DDoS absorption. +- [BGP](https://www.rfc-editor.org/rfc/rfc4271) exchanges routes between [autonomous systems](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29); a bad announcement or withdrawal can take a whole company offline ([Facebook, October 2021](https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/)). +- [Anycast](https://en.wikipedia.org/wiki/Anycast) announces one IP from many locations and BGP delivers each client to the nearest by routing policy (usually, not always, the lowest-latency) — CDNs, public DNS resolvers, DDoS absorption. ### IPv6 -- 128-bit addresses, no NAT needed; hosts configure themselves with **SLAAC**, and a subnet is normally a `/64`. -- Dual-stack clients prefer IPv6 but race both families (**Happy Eyeballs**) so a broken path doesn't stall connections. +- [128-bit addresses](https://www.rfc-editor.org/rfc/rfc8200), no NAT needed; hosts configure themselves with [SLAAC](https://www.rfc-editor.org/rfc/rfc4862), and a subnet is normally a `/64`. +- Dual-stack clients prefer IPv6 but race both families ([Happy Eyeballs](https://www.rfc-editor.org/rfc/rfc8305)) so a broken path doesn't stall connections. diff --git a/docs/topics/os.md b/docs/topics/os.md index 47a8827..ef263aa 100644 --- a/docs/topics/os.md +++ b/docs/topics/os.md @@ -6,170 +6,170 @@ What experienced engineers forget about Linux and OS internals before an intervi ### fork, exec, copy-on-write -- **`fork()`** duplicates the process lazily: pages are shared **copy-on-write** until one side writes. -- **`exec()`** replaces the program image, keeping the PID and open descriptors (unless `O_CLOEXEC`). +- [`fork()`](https://man7.org/linux/man-pages/man2/fork.2.html) duplicates the process lazily: pages are shared [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write) until one side writes. +- [`exec()`](https://man7.org/linux/man-pages/man2/execve.2.html) replaces the program image, keeping the PID and open descriptors (unless [`O_CLOEXEC`](https://man7.org/linux/man-pages/man2/open.2.html)). - Forking a multi-threaded process copies only the calling thread — locks held by others stay locked forever in the child. -- On Linux, processes and threads are both tasks created by `clone()` with different sharing flags; the scheduler treats them alike. +- On Linux, processes and threads are both tasks created by [`clone()`](https://man7.org/linux/man-pages/man2/clone.2.html) with different sharing flags; the scheduler treats them alike. ### Process states | State | Meaning | |---|---| -| R | running or runnable (on a run queue) | +| R | [running or runnable](https://man7.org/linux/man-pages/man1/ps.1.html#PROCESS_STATE_CODES) (on a run queue) | | S | interruptible sleep: waiting for an event; signals wake it | -| **D** | uninterruptible sleep, usually disk or NFS I/O; even `SIGKILL` waits, and it counts toward load average | -| Z | zombie: exited, not yet reaped by its parent | +| D | uninterruptible sleep, usually disk or NFS I/O; even `SIGKILL` waits, and it counts toward [load average](https://en.wikipedia.org/wiki/Load_%28computing%29#Linux_as_an_example) | +| Z | [zombie](https://en.wikipedia.org/wiki/Zombie_process): exited, not yet reaped by its parent | | T | stopped (`SIGSTOP`, a debugger) | ### Zombies and orphans -- A **zombie** has exited but its parent hasn't called `wait()` — it holds only a PID table entry, but enough of them exhaust PIDs. -- An **orphan** is re-parented to PID 1 (or a subreaper), which must reap it — why containers need a proper init. +- A [zombie](https://en.wikipedia.org/wiki/Zombie_process) has exited but its parent hasn't called [`wait()`](https://man7.org/linux/man-pages/man2/wait.2.html) — it holds only a PID table entry, but enough of them exhaust PIDs. +- An [orphan](https://en.wikipedia.org/wiki/Orphan_process) is re-parented to PID 1 (or a [subreaper](https://man7.org/linux/man-pages/man2/PR_SET_CHILD_SUBREAPER.2const.html)), which must reap it — why containers need a [proper init](https://github.com/krallin/tini). ### Signals -- **`SIGTERM`** asks politely and can be handled; **`SIGKILL`** and `SIGSTOP` can't be caught or ignored. +- [`SIGTERM`](https://man7.org/linux/man-pages/man7/signal.7.html) asks politely and can be handled; `SIGKILL` and `SIGSTOP` can't be caught or ignored. - `SIGCHLD` tells a parent a child changed state; `SIGHUP` conventionally means reload config; `SIGPIPE` kills a process writing to a closed socket unless ignored. -- Signal handlers may call only **async-signal-safe** functions (not `malloc`, not `printf`). +- Signal handlers may call only [async-signal-safe](https://man7.org/linux/man-pages/man7/signal-safety.7.html) functions (not `malloc`, not `printf`). ### User mode, kernel mode, syscalls -- A **syscall** switches to kernel mode — a few hundred nanoseconds, much more with mitigations and cache effects; batching syscalls matters at high rates. -- The **vDSO** maps some calls (`clock_gettime`) into user space so they cost no mode switch. +- A [syscall](https://en.wikipedia.org/wiki/System_call) switches to kernel mode — a few hundred nanoseconds, much more with [mitigations](https://docs.kernel.org/admin-guide/hw-vuln/index.html) and cache effects; batching syscalls matters at high rates. +- The [vDSO](https://man7.org/linux/man-pages/man7/vdso.7.html) maps some calls (`clock_gettime`) into user space so they cost no mode switch. ## Scheduling ### Context switches -- A switch saves and restores registers and may change the address space; the direct cost is **~1–5 µs**, the indirect cost is cold **caches and TLB**. +- A switch saves and restores registers and may change the address space; the direct cost is ~1–5 µs, the indirect cost is cold caches and [TLB](https://en.wikipedia.org/wiki/Translation_lookaside_buffer). - Thread switches within a process are cheaper — no page-table change. -- Thousands of runnable threads mostly add switching overhead; that's why event loops and green threads (goroutines) exist. +- Thousands of runnable threads mostly add switching overhead; that's why event loops and [green threads](https://en.wikipedia.org/wiki/Green_thread) ([goroutines](golang.md)) exist. ### Linux scheduler -- **EEVDF** replaced **CFS** as the default scheduler in **Linux 6.6** (2023): each task gets a fair share weighted by its `nice` value, with better latency for short-running tasks. -- Real-time policies (`SCHED_FIFO`, `SCHED_RR`) always run before normal tasks. +- [EEVDF](https://docs.kernel.org/scheduler/sched-eevdf.html) replaced [CFS](https://docs.kernel.org/scheduler/sched-design-CFS.html) as the default scheduler in [Linux 6.6](https://kernelnewbies.org/Linux_6.6#New_task_scheduler:_EEVDF) (2023): each task gets a fair share weighted by its [`nice`](https://man7.org/linux/man-pages/man7/sched.7.html) value, with better latency for short-running tasks. +- [Real-time policies](https://man7.org/linux/man-pages/man7/sched.7.html) (`SCHED_FIFO`, `SCHED_RR`) always run before normal tasks. ### CPU quotas and throttling -- A cgroup CPU limit is a **quota per period** (`cpu.max`, default 100 ms): a 1-CPU limit lets 4 threads use 25 ms each, then **throttles** the whole group for the rest of the period — latency spikes while average CPU looks low. -- Watch `nr_throttled` in `cpu.stat`; match thread-pool sizes (`GOMAXPROCS`, JVM) to the quota. +- A cgroup CPU limit is a quota per period ([`cpu.max`](https://docs.kernel.org/admin-guide/cgroup-v2.html#cpu-interface-files), default 100 ms): a 1-CPU limit lets 4 threads use 25 ms each, then throttles the whole group for the rest of the period — latency spikes while average CPU looks low. +- Watch `nr_throttled` in `cpu.stat`; match thread-pool sizes ([`GOMAXPROCS`](https://pkg.go.dev/runtime#GOMAXPROCS), JVM) to the quota. ### Load average -- Counts tasks that are **running, runnable, or in uninterruptible sleep** (D state, usually disk or NFS I/O), averaged over 1, 5, 15 minutes. -- High load with idle CPUs means tasks are **blocked on I/O**, not CPU-bound; compare against the core count. +- Counts tasks that are running, runnable, or in [uninterruptible sleep](https://man7.org/linux/man-pages/man1/ps.1.html#PROCESS_STATE_CODES) (D state, usually disk or NFS I/O), [averaged over 1, 5, 15 minutes](https://en.wikipedia.org/wiki/Load_%28computing%29#Linux_as_an_example). +- High load with idle CPUs means tasks are blocked on I/O, not CPU-bound; compare against the core count. ## Memory ### Virtual memory and the TLB -- Each process sees a private virtual address space mapped to physical frames in pages (**4 KB** on x86-64; arm64 can use 16 or 64 KB) through page tables. -- The **TLB** caches translations; **huge pages** (2 MB, 1 GB) cut TLB misses for large heaps — transparent huge pages can also cause latency spikes during compaction, so databases often disable them. +- Each process sees a private [virtual address space](https://en.wikipedia.org/wiki/Virtual_memory) mapped to physical frames in pages (4 KB on x86-64; arm64 can use 16 or 64 KB) through [page tables](https://en.wikipedia.org/wiki/Page_table). +- The [TLB](https://en.wikipedia.org/wiki/Translation_lookaside_buffer) caches translations; [huge pages](https://docs.kernel.org/admin-guide/mm/hugetlbpage.html) (2 MB, 1 GB) cut TLB misses for large heaps — [transparent huge pages](https://docs.kernel.org/admin-guide/mm/transhuge.html) can also cause latency spikes during compaction, so databases often disable them. ### Page faults -- **Minor fault**: the page is in memory and just needs mapping (first touch, copy-on-write). -- **Major fault**: the page must be read from disk (swap or a memory-mapped file) — milliseconds each. +- [Minor fault](https://en.wikipedia.org/wiki/Page_fault#Minor_page_fault): served without disk I/O — map a cached page, allocate on first touch, or [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write). +- [Major fault](https://en.wikipedia.org/wiki/Page_fault#Major_page_fault): the page must be read from disk (swap or a memory-mapped file) — tens of microseconds on NVMe, milliseconds on spinning disks. ### Overcommit and the OOM killer -- `malloc` usually succeeds without backing memory; physical pages are allocated **on first touch** (overcommit). -- When memory runs out, the **OOM killer** kills the process with the highest `oom_score` (tuned by `oom_score_adj`); in a container, a **cgroup limit** that reclaim can't satisfy triggers an OOM kill inside that cgroup only. +- `malloc` usually succeeds without backing memory ([overcommit](https://docs.kernel.org/mm/overcommit-accounting.html)); physical pages are allocated on [first touch](https://docs.kernel.org/admin-guide/mm/concepts.html#anonymous-memory). +- When memory runs out, the [OOM killer](https://docs.kernel.org/admin-guide/mm/concepts.html#oom-killer) kills the process with the highest [`oom_score`](https://man7.org/linux/man-pages/man5/proc_pid_oom_score.5.html) (tuned by [`oom_score_adj`](https://man7.org/linux/man-pages/man5/proc_pid_oom_score_adj.5.html)); in a container, a [cgroup limit](https://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-files) that reclaim can't satisfy triggers an OOM kill inside that cgroup only. ### cgroup memory limits -- cgroup v2 **`memory.max`** is the hard limit: at it the kernel reclaims, and OOM-kills inside the cgroup only if reclaim fails (a Kubernetes memory limit maps here). -- **`memory.high`** is a soft limit: above it the kernel throttles and reclaims aggressively instead of killing. -- Page cache counts toward the cgroup but is reclaimed first, so watch the **working set** (usage minus inactive file pages), which the kubelet uses for eviction. +- cgroup v2 [`memory.max`](https://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-files) is the hard limit: at it the kernel reclaims, and OOM-kills inside the cgroup only if reclaim fails (a [Kubernetes memory limit](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) maps here). +- [`memory.high`](https://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-files) is a soft limit: above it the kernel throttles and reclaims aggressively instead of killing. +- [Page cache](https://en.wikipedia.org/wiki/Page_cache) counts toward the cgroup but is usually reclaimed first, so watch the working set (usage minus inactive file pages), which the [kubelet uses for eviction](https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/). ### Swap -- Swap moves anonymous pages to disk under pressure: fewer OOM kills, but touching them costs **major faults**. -- `vm.swappiness` biases reclaim between page cache and anonymous memory; latency-sensitive servers usually run with little or no swap. +- [Swap](https://en.wikipedia.org/wiki/Memory_paging#Swap_files_and_partitions) moves anonymous pages to disk under pressure: fewer OOM kills, but touching them costs [major faults](https://en.wikipedia.org/wiki/Page_fault#Major_page_fault). +- [`vm.swappiness`](https://docs.kernel.org/admin-guide/sysctl/vm.html#swappiness) biases reclaim between [page cache](https://en.wikipedia.org/wiki/Page_cache) and anonymous memory; latency-sensitive servers usually run with little or no swap. ### NUMA -- On multi-socket servers each socket has local memory; reaching another node's memory costs **1.5–2×** the latency. -- Linux allocates on the node of the thread that **first touches** a page, so a thread moved to another socket reads remotely. -- Pin latency-critical processes and their memory with `numactl`; `numastat` shows remote allocations. +- On multi-socket servers each socket has [local memory](https://en.wikipedia.org/wiki/Non-uniform_memory_access); reaching another node's memory costs 1.5–2× the latency. +- Linux allocates on the node of the thread that first touches a page, so a thread moved to another socket reads remotely. +- Pin latency-critical processes and their memory with [`numactl`](https://man7.org/linux/man-pages/man8/numactl.8.html); [`numastat`](https://man7.org/linux/man-pages/man8/numastat.8.html) shows remote allocations. ### Page cache and "free" memory -- Unused RAM caches file data (**page cache**), so low `free` is normal — check **`available`** in `free -m`. -- `write()` returns once data is in the page cache; dirty pages are flushed later unless you call **`fsync`**. +- Unused RAM caches file data ([page cache](https://en.wikipedia.org/wiki/Page_cache)), so low `free` is normal — check `available` in [`free -m`](https://man7.org/linux/man-pages/man1/free.1.html). +- [`write()`](https://man7.org/linux/man-pages/man2/write.2.html) returns once data is in the page cache; dirty pages are flushed later unless you call [`fsync`](https://man7.org/linux/man-pages/man2/fsync.2.html). ### RSS, VSZ, and shared memory -- VSZ counts all mapped virtual memory (mostly irrelevant); **RSS** counts resident pages but double-counts shared ones; **PSS** splits shared pages proportionally. -- Default thread stack size is typically **8 MB** of virtual memory (`ulimit -s`), mostly untouched. +- [VSZ](https://man7.org/linux/man-pages/man1/ps.1.html) counts all mapped virtual memory (mostly irrelevant); [RSS](https://en.wikipedia.org/wiki/Resident_set_size) counts resident pages but double-counts shared ones; [PSS](https://en.wikipedia.org/wiki/Proportional_set_size) splits shared pages proportionally. +- Default thread stack size is typically 8 MB of virtual memory (`ulimit -s`), mostly untouched. ## Files and I/O ### File descriptors and inodes -- Everything open — files, sockets, pipes — is a file descriptor; the soft limit is often **1024** (`ulimit -n`), causing "too many open files" on busy servers. -- A filename points to an inode; deleting a file frees its space only when no hard links, **open descriptors**, or mappings remain — why `df` and `du` can disagree. -- **Hard links** are extra names for the same inode (same filesystem only); symlinks store a path and can dangle. +- Everything open — files, sockets, pipes — is a [file descriptor](https://en.wikipedia.org/wiki/File_descriptor); the [soft limit](https://man7.org/linux/man-pages/man2/getrlimit.2.html) is often 1024 (`ulimit -n`), causing "too many open files" on busy servers. +- A filename points to an [inode](https://en.wikipedia.org/wiki/Inode); deleting a file frees its space only when no [hard links](https://en.wikipedia.org/wiki/Hard_link), open descriptors, or mappings remain — why `df` and `du` can disagree. +- Hard links are extra names for the same inode (same filesystem only); [symlinks](https://en.wikipedia.org/wiki/Symbolic_link) store a path and can dangle. ### I/O multiplexing: select, poll, epoll -- `select`/`poll` pass the whole descriptor set on every call — O(n) per wait; `select` is capped at 1024 descriptors. -- **`epoll`** registers descriptors once and returns only ready ones — O(ready); the basis of Nginx, Node's libuv, and Go's netpoller (kqueue on BSD/macOS). -- **Level-triggered** keeps reporting while data remains; **edge-triggered** reports once per change, so you must read until `EAGAIN`. +- [`select`](https://man7.org/linux/man-pages/man2/select.2.html)/[`poll`](https://man7.org/linux/man-pages/man2/poll.2.html) pass the whole descriptor set on every call — O(n) per wait; `select` is capped at 1024 descriptors. +- [`epoll`](https://man7.org/linux/man-pages/man7/epoll.7.html) registers descriptors once and returns only ready ones — O(ready); the basis of Nginx, Node's [libuv](https://docs.libuv.org/en/v1.x/design.html), and Go's [netpoller](golang.md) ([kqueue](https://man.freebsd.org/cgi/man.cgi?kqueue) on BSD/macOS). +- [Level-triggered](https://man7.org/linux/man-pages/man7/epoll.7.html) keeps reporting while data remains; edge-triggered reports once per change, so you must read until `EAGAIN`. ### io_uring -- **io_uring** (Linux 5.1+) submits and completes I/O through rings shared with the kernel — true async file and network I/O with few syscalls. -- Its large kernel attack surface means many container runtimes and hardened hosts **block it via seccomp**. +- [io_uring](https://man7.org/linux/man-pages/man7/io_uring.7.html) ([Linux 5.1+](https://kernelnewbies.org/Linux_5.1)) submits and completes I/O through rings shared with the kernel — true async file and network I/O with few syscalls. +- Its large kernel attack surface means many container runtimes and hardened hosts [block it via seccomp](https://docs.docker.com/engine/security/seccomp/). ### Durability and fsync -- Data is durable only after **`fsync`** (or `fdatasync`); a new file also needs its **directory** fsynced. -- After an fsync error, Linux may drop the dirty pages and a retry can falsely succeed — since 2018's "fsyncgate", **PostgreSQL panics** on fsync failure instead of retrying. +- Data is durable only after [`fsync`](https://man7.org/linux/man-pages/man2/fsync.2.html) (or `fdatasync`); a new file also needs its directory fsynced. +- After an fsync error, Linux may drop the dirty pages and a retry can falsely succeed — since 2018's "[fsyncgate](https://wiki.postgresql.org/wiki/Fsync_Errors)", [PostgreSQL panics](https://www.postgresql.org/docs/current/runtime-config-error-handling.html#GUC-DATA-SYNC-RETRY) on fsync failure instead of retrying. ### Atomic file replace -- Write a temp file in the **same directory** → `fsync` it → `rename()` it over the target → `fsync` the directory. -- `rename` within one filesystem is **atomic** on POSIX: readers see the old or the new file, never a torn mix. -- Skipping the first `fsync` can leave an empty file after a crash (the rename persisted before the data). +- Write a temp file in the same directory → [`fsync`](https://man7.org/linux/man-pages/man2/fsync.2.html) it → [`rename()`](https://man7.org/linux/man-pages/man2/rename.2.html) it over the target → `fsync` the directory. +- `rename` within one filesystem is [atomic](https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html) on POSIX: readers see the old or the new file, never a torn mix. +- Skipping the first `fsync` can leave an [empty file after a crash](https://lwn.net/Articles/322823/) (the rename persisted before the data). ### Zero-copy I/O -- **`sendfile`**/`splice` move file data to a socket inside the kernel, skipping user-space copies — how static file servers and Kafka reach high throughput. -- **`mmap`** maps a file into memory; reads become page faults served from the page cache. +- [`sendfile`](https://man7.org/linux/man-pages/man2/sendfile.2.html) (or [`splice`](https://man7.org/linux/man-pages/man2/splice.2.html) through a pipe) moves file data to a socket inside the kernel, skipping user-space copies — how static file servers and [Kafka](https://kafka.apache.org/43/design/design/#efficiency) reach high throughput. +- [`mmap`](https://man7.org/linux/man-pages/man2/mmap.2.html) maps a file into memory; reads become page faults served from the [page cache](https://en.wikipedia.org/wiki/Page_cache). ## Synchronization ### Mutexes, spinlocks, futexes -- A **spinlock** busy-waits — only for very short critical sections, never while sleeping. -- A mutex sleeps when contended; Linux implements it on **futexes**, which stay entirely in user space until there's contention. -- A semaphore counts permits (connection limits); a **condition variable** waits for a predicate and must be re-checked in a loop (spurious wakeups). +- A [spinlock](https://en.wikipedia.org/wiki/Spinlock) busy-waits — only for very short critical sections, never while sleeping. +- A [mutex](https://man7.org/linux/man-pages/man3/pthread_mutex_lock.3p.html) sleeps when contended; Linux implements it on [futexes](https://man7.org/linux/man-pages/man7/futex.7.html), which stay entirely in user space until there's contention. +- A [semaphore](https://en.wikipedia.org/wiki/Semaphore_%28programming%29) counts permits (connection limits); a [condition variable](https://en.wikipedia.org/wiki/Monitor_%28synchronization%29#Condition_variables) waits for a predicate and must be re-checked in a loop ([spurious wakeups](https://en.wikipedia.org/wiki/Spurious_wakeup)). ### Deadlock conditions -- All four **Coffman conditions** must hold: mutual exclusion, hold and wait, no preemption, **circular wait**. -- Break one — usually circular wait, with a global **lock ordering** — or use `trylock` with timeouts. +- All four [Coffman conditions](https://en.wikipedia.org/wiki/Deadlock_%28computer_science%29#Conditions) must hold: mutual exclusion, hold and wait, no preemption, circular wait. +- Break one — usually circular wait, with a global lock ordering — or use [`trylock`](https://man7.org/linux/man-pages/man3/pthread_mutex_lock.3p.html) with timeouts. ### Atomics, CAS, and the ABA problem -- **Compare-and-swap** writes only if the value still equals the expected one; retry loops build lock-free stacks, counters, and mutex fast paths. -- **ABA**: the value goes A → B → A between the read and the CAS, so a stale CAS succeeds (a popped stack node freed and reused); fix with version-tagged pointers or safe reclamation (hazard pointers, epochs). +- [Compare-and-swap](https://en.wikipedia.org/wiki/Compare-and-swap) writes only if the value still equals the expected one; retry loops build lock-free stacks, counters, and mutex fast paths. +- [ABA](https://en.wikipedia.org/wiki/ABA_problem): the value goes A → B → A between the read and the CAS, so a stale CAS succeeds (a popped stack node freed and reused); fix with version-tagged pointers or safe reclamation ([hazard pointers](https://en.wikipedia.org/wiki/Hazard_pointer), epochs). - Under heavy contention, CAS loops bounce the cache line between cores and can be slower than a mutex. -- x86 is strongly ordered (TSO), ARM weakly ordered, so code that works on x86 can break on ARM without the right memory orderings ([rust.md](rust.md)). +- x86 is strongly ordered ([TSO](https://www.cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf)), ARM weakly ordered, so code that works on x86 can break on ARM without the right memory orderings ([rust.md](rust.md)). ### False sharing -- Cores transfer memory in **cache lines** (64 bytes on x86-64, 128 on Apple M-series); two threads writing different variables on the same line keep invalidating each other's caches. +- Cores transfer memory in [cache lines](https://en.wikipedia.org/wiki/CPU_cache#Cache_entries) (64 bytes on x86-64, 128 on Apple M-series); two threads writing different variables on the same line [keep invalidating each other's caches](https://en.wikipedia.org/wiki/False_sharing). - Pad or align hot per-thread counters to separate lines. ## Performance analysis ### Linux observability tools -- **`perf`** samples stacks with low overhead → flame graphs; **eBPF** tools (`bpftrace`, bcc) trace kernel events in production safely. -- **`strace`** shows every syscall but slows the process heavily (ptrace) — avoid on hot production paths. +- [`perf`](https://perfwiki.github.io/main/) samples stacks with low overhead → [flame graphs](https://www.brendangregg.com/flamegraphs.html); [eBPF](https://ebpf.io/what-is-ebpf/) tools ([`bpftrace`](https://github.com/bpftrace/bpftrace), [bcc](https://github.com/iovisor/bcc)) trace kernel events in production safely. +- [`strace`](https://strace.io/) shows every syscall but slows the process heavily ([ptrace](https://man7.org/linux/man-pages/man2/ptrace.2.html)) — avoid on hot production paths. ### Memory hierarchy latencies @@ -178,12 +178,12 @@ What experienced engineers forget about Linux and OS internals before an intervi | L1 cache | ~1 ns | | L2 cache | ~4 ns | | L3 cache | ~10–40 ns | -| DRAM | **~100 ns** | +| DRAM | ~100 ns | | NVMe SSD read | ~10–100 µs | -Sequential access and contiguous layouts (arrays over linked lists, struct-of-arrays) win through prefetching and full cache-line use. +Sequential access and contiguous layouts (arrays over linked lists, [struct-of-arrays](https://en.wikipedia.org/wiki/AoS_and_SoA)) win through [prefetching](https://en.wikipedia.org/wiki/Cache_prefetching) and full cache-line use. ### CPU time breakdown -- us (user code), sy (kernel), **wa** (idle waiting for I/O), **st** (stolen by the hypervisor — noisy neighbors on VMs). -- High sy points at syscall-heavy code or lock contention; high **st** means you need a bigger or dedicated instance, not code changes. +- `us` (user code), `sy` (kernel), `wa` (idle waiting for I/O), `st` ([stolen by the hypervisor](https://man7.org/linux/man-pages/man1/top.1.html) — noisy neighbors on VMs). +- High `sy` points at syscall-heavy code or lock contention; high `st` means you need a bigger or dedicated instance, not code changes. From 0e82cc69fea3eea2f7de7727fd4f9bf05f7a1b77 Mon Sep 17 00:00:00 2001 From: Andrei Smirnov Date: Thu, 8 Oct 2026 06:44:35 +0300 Subject: [PATCH 3/3] Point system and distributed links at canonical sources Co-Authored-By: Claude Opus 5.5 --- docs/topics/distributed.md | 4 ++-- docs/topics/system.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/topics/distributed.md b/docs/topics/distributed.md index 30b68fc..65e8ca1 100644 --- a/docs/topics/distributed.md +++ b/docs/topics/distributed.md @@ -40,7 +40,7 @@ Key distributed-systems building blocks and trade-offs, grouped by subtopic. ### Single-leader replication - All writes go to one leader; [sync](https://www.postgresql.org/docs/current/warm-standby.html#SYNCHRONOUS-REPLICATION) followers are durable but add latency, async followers are fast but lose recent writes on failover. -- [Semi-sync](https://mariadb.com/docs/server/ha-and-performance/standard-replication/semisynchronous-replication) waits for at least one follower. +- [Semi-sync](https://dev.mysql.com/doc/refman/8.4/en/replication-semisync.html) waits for at least one follower. - Replication lag breaks read-your-writes: a user can't see their own update right after writing. ### Multi-leader and leaderless @@ -203,7 +203,7 @@ Key distributed-systems building blocks and trade-offs, grouped by subtopic. ### Change data capture -- [Log-based CDC](https://debezium.io/documentation/reference/stable/features.html) (Debezium) tails the [WAL](https://www.postgresql.org/docs/current/wal-intro.html) or [binlog](https://debezium.io/documentation/reference/stable/connectors/mysql.html): every committed change in commit order, deletes included, with no dual writes in the application. +- [Log-based CDC](https://debezium.io/documentation/reference/stable/features.html) (Debezium) tails the [WAL](https://www.postgresql.org/docs/current/wal-intro.html) or [binlog](https://dev.mysql.com/doc/refman/8.4/en/binary-log.html): every committed change in commit order, deletes included, with no dual writes in the application. - Polling an `updated_at` column misses hard deletes and rows that commit late with an older timestamp. - Bootstrap from a [consistent snapshot](https://debezium.io/documentation/reference/stable/connectors/postgresql.html#postgresql-snapshots), then stream from the log position it was taken at. - Events are ordered per key (partition by primary key); connectors deliver at least once, so consumers must be idempotent. diff --git a/docs/topics/system.md b/docs/topics/system.md index b9f9417..8327540 100644 --- a/docs/topics/system.md +++ b/docs/topics/system.md @@ -117,7 +117,7 @@ Key system design building blocks and trade-offs, grouped by subtopic. For CAP, ### Distributed rate limiting - Enforce at the gateway, keyed by user, API key, or IP; reject with [429](https://www.rfc-editor.org/rfc/rfc6585#section-4) and [`Retry-After`](https://www.rfc-editor.org/rfc/rfc9110#field.retry-after). -- Share counters in Redis so every server sees one limit; wrap [`INCR`](https://redis.io/docs/latest/commands/incr/) + [`EXPIRE`](https://redis.io/docs/latest/commands/expire/) in [`MULTI`](https://redis.io/docs/latest/develop/interact/transactions/) or a [Lua script](https://redis.io/docs/latest/develop/interact/programmability/eval-intro/), or a crash between them leaves a counter with no TTL. +- Share counters in Redis so every server sees one limit; wrap [`INCR`](https://redis.io/docs/latest/commands/incr/) + [`EXPIRE`](https://redis.io/docs/latest/commands/expire/) in [`MULTI`](https://redis.io/docs/latest/develop/using-commands/transactions/) or a [Lua script](https://redis.io/docs/latest/develop/programmability/eval-intro/), or a crash between them leaves a counter with no TTL. - A local per-instance limiter skips the round trip but is only approximate (limit ÷ instances). ## Reliability and operations