diff --git a/infra/assets/sharding/main.go b/infra/assets/sharding/main.go new file mode 100644 index 0000000..6923e74 --- /dev/null +++ b/infra/assets/sharding/main.go @@ -0,0 +1,323 @@ +package main + +import ( + "fmt" + "hash/fnv" + "sort" + "strconv" +) + +func hash64(s string) uint64 { + h := fnv.New64a() + _, _ = h.Write([]byte(s)) + return h.Sum64() +} + +// ModRouter is the simplest hash-based router: hash(key) % N. +type ModRouter struct { + numShards int +} + +func NewModRouter(numShards int) *ModRouter { + if numShards <= 0 { + panic("numShards must be > 0") + } + return &ModRouter{numShards: numShards} +} + +func (r *ModRouter) ShardFor(key string) int { + return int(hash64(key) % uint64(r.numShards)) +} + +type vnode struct { + token uint64 + node string +} + +// Ring is a minimal consistent-hash ring with virtual nodes. +type Ring struct { + points []vnode +} + +func NewRing(nodeNames []string, vnodesPerNode int) *Ring { + if len(nodeNames) == 0 { + panic("nodeNames must not be empty") + } + if vnodesPerNode <= 0 { + panic("vnodesPerNode must be > 0") + } + + points := make([]vnode, 0, len(nodeNames)*vnodesPerNode) + for _, name := range nodeNames { + for i := 0; i < vnodesPerNode; i++ { + key := name + "#" + strconv.Itoa(i) + points = append(points, vnode{ + token: hash64(key), + node: name, + }) + } + } + + sort.Slice(points, func(i, j int) bool { + return points[i].token < points[j].token + }) + + return &Ring{points: points} +} + +func (r *Ring) Get(key string) string { + h := hash64(key) + idx := sort.Search(len(r.points), func(i int) bool { + return r.points[i].token >= h + }) + if idx == len(r.points) { + idx = 0 + } + return r.points[idx].node +} + +// GetN returns up to rf distinct nodes by walking the ring clockwise. +func (r *Ring) GetN(key string, rf int) []string { + if rf <= 0 { + return nil + } + h := hash64(key) + idx := sort.Search(len(r.points), func(i int) bool { + return r.points[i].token >= h + }) + if idx == len(r.points) { + idx = 0 + } + + seen := make(map[string]struct{}, rf) + result := make([]string, 0, rf) + + for i := 0; i < len(r.points) && len(result) < rf; i++ { + p := r.points[(idx+i)%len(r.points)] + if _, ok := seen[p.node]; ok { + continue + } + seen[p.node] = struct{}{} + result = append(result, p.node) + } + return result +} + +// TenantRouter maps tenants to dedicated rings and falls back to a default ring. +type TenantRouter struct { + fallback *Ring + rings map[string]*Ring +} + +func NewTenantRouter(fallback *Ring) *TenantRouter { + if fallback == nil { + panic("fallback ring must not be nil") + } + return &TenantRouter{ + fallback: fallback, + rings: make(map[string]*Ring), + } +} + +func (tr *TenantRouter) SetTenantRing(tenant string, ring *Ring) { + if tenant == "" { + panic("tenant must not be empty") + } + if ring == nil { + panic("ring must not be nil") + } + tr.rings[tenant] = ring +} + +func (tr *TenantRouter) Route(tenant, key string) string { + if ring, ok := tr.rings[tenant]; ok { + return ring.Get(key) + } + return tr.fallback.Get(key) +} + +// SparseTable supports static range minimum query (RMQ). +type SparseTable struct { + values []int + lg []int + st [][]int // st[k][i] holds the index of min in range [i, i+2^k-1] +} + +func NewSparseTable(values []int) *SparseTable { + n := len(values) + if n == 0 { + panic("values must not be empty") + } + + lg := make([]int, n+1) + for i := 2; i <= n; i++ { + lg[i] = lg[i/2] + 1 + } + + kMax := lg[n] + 1 + st := make([][]int, kMax) + st[0] = make([]int, n) + for i := 0; i < n; i++ { + st[0][i] = i + } + + for k := 1; k < kMax; k++ { + span := 1 << k + half := span >> 1 + st[k] = make([]int, n-span+1) + for i := 0; i+span <= n; i++ { + left := st[k-1][i] + right := st[k-1][i+half] + if values[left] <= values[right] { + st[k][i] = left + } else { + st[k][i] = right + } + } + } + + return &SparseTable{values: values, lg: lg, st: st} +} + +// RangeMinIndex returns the index of the minimum value in [l, r]. +func (s *SparseTable) RangeMinIndex(l, r int) int { + if l < 0 || r >= len(s.values) || l > r { + panic("invalid range") + } + k := s.lg[r-l+1] + left := s.st[k][l] + right := s.st[k][r-(1<> 1) + tree := make([]minPair, size*2) + for i := range tree { + tree[i] = minPair{idx: -1, val: inf} + } + + for i, v := range values { + tree[size+i] = minPair{idx: i, val: v} + } + for i := size - 1; i >= 1; i-- { + tree[i] = better(tree[i*2], tree[i*2+1]) + } + + return &SegmentTree{n: n, size: size, tree: tree} +} + +func (t *SegmentTree) Update(idx, val int) { + if idx < 0 || idx >= t.n { + panic("index out of range") + } + p := t.size + idx + t.tree[p] = minPair{idx: idx, val: val} + for p > 1 { + p >>= 1 + t.tree[p] = better(t.tree[p*2], t.tree[p*2+1]) + } +} + +// RangeMin returns (index, value) of minimum in [l, r]. +func (t *SegmentTree) RangeMin(l, r int) (int, int) { + if l < 0 || r >= t.n || l > r { + panic("invalid range") + } + l += t.size + r += t.size + + inf := int(^uint(0) >> 1) + leftBest := minPair{idx: -1, val: inf} + rightBest := minPair{idx: -1, val: inf} + + for l <= r { + if l%2 == 1 { + leftBest = better(leftBest, t.tree[l]) + l++ + } + if r%2 == 0 { + rightBest = better(t.tree[r], rightBest) + r-- + } + l >>= 1 + r >>= 1 + } + + ans := better(leftBest, rightBest) + return ans.idx, ans.val +} + +func main() { + fmt.Println("== 1) Basic modulo hashing ==") + mod := NewModRouter(8) + fmt.Printf("key=user:42 -> shard=%d\n", mod.ShardFor("user:42")) + + fmt.Println("\n== 2) Consistent hash ring ==") + ring := NewRing([]string{"ingester-a", "ingester-b", "ingester-c", "ingester-d"}, 64) + seriesKey := "tenant-a|service=api|metric=http_requests_total" + fmt.Printf("primary node for %q -> %s\n", seriesKey, ring.Get(seriesKey)) + fmt.Printf("replication set (rf=3) -> %v\n", ring.GetN(seriesKey, 3)) + + fmt.Println("\n== 3) Tenant-aware routing ==") + premiumRing := NewRing([]string{"premium-a", "premium-b"}, 64) + router := NewTenantRouter(ring) + router.SetTenantRing("premium-tenant", premiumRing) + fmt.Printf("tenant=free-tenant -> %s\n", router.Route("free-tenant", "user:1")) + fmt.Printf("tenant=premium-tenant -> %s\n", router.Route("premium-tenant", "user:1")) + + fmt.Println("\n== 4) Sparse Table (static snapshot RMQ) ==") + // Example: periodic snapshot of shard latency (ms). + latencySnapshot := []int{14, 19, 11, 17, 13, 9, 16, 21} + sparse := NewSparseTable(latencySnapshot) + best := sparse.RangeMinIndex(2, 6) + fmt.Printf("best shard index in window [2,6] -> %d (latency=%dms)\n", best, latencySnapshot[best]) + + fmt.Println("\n== 5) Segment Tree (dynamic load RMQ) ==") + // Example: live load scores for shards; lower is better. + load := []int{55, 21, 43, 18, 62, 27, 35, 40} + seg := NewSegmentTree(load) + idx, val := seg.RangeMin(0, len(load)-1) + fmt.Printf("least loaded shard before update -> %d (load=%d)\n", idx, val) + + seg.Update(3, 70) // shard 3 becomes busy + idx, val = seg.RangeMin(0, len(load)-1) + fmt.Printf("least loaded shard after update -> %d (load=%d)\n", idx, val) +} diff --git a/infra/sharding_en.md b/infra/sharding_en.md index e08d93b..739225b 100644 --- a/infra/sharding_en.md +++ b/infra/sharding_en.md @@ -1,289 +1,348 @@ -# Sharding Fundamentals: Sparse Table, Segment Tree, Elasticsearch, and Cortex +# Sharding: From First Principles to Go Implementation -This guide explains practical sharding design for distributed systems and connects theory to production systems: +This guide is a clean, implementation-oriented view of sharding for distributed systems. -- **Elasticsearch** for index/data sharding -- **Cortex** for time-series ingestion/query sharding with a hash ring -- **Sparse Table** and **Segment Tree** as supporting data structures for shard-aware planning and balancing +It explains: -## Goal +1. What sharding is and why it exists +2. How to choose and validate a sharding strategy +3. How to handle hotspots, fan-out, and re-sharding +4. Why control-plane stability is as important as data-plane throughput +5. How Sparse Table and Segment Tree support shard-aware decisions +6. How to implement core patterns in Go +7. How Elasticsearch and Cortex map to these concepts -By reading this guide, you should be able to: +--- -1. Choose a sharding strategy (hash/range/tenant-aware) based on workload shape. -2. Select a shard key that minimizes skew and cross-shard fan-out. -3. Understand where sparse tables and segment trees fit in sharded control planes. -4. Map sharding concepts to Elasticsearch and Cortex. -5. Understand the impact of Cortex PRs #7266 and #7270 on ring-control loops. +## 1) What Is Sharding? ---- +Sharding is horizontal partitioning of data and traffic across multiple nodes. + +Instead of: -## Why Sharding Exists +```text +all data -> one machine +``` + +you do: -A single node eventually hits limits in one or more dimensions: +```text +data partitioned -> many machines +``` -- storage size +Each partition is a **shard**. + +Sharding becomes necessary when one node can no longer absorb: + +- storage growth - write throughput - query concurrency -- fault domain blast radius +- failure-domain blast radius + +Sharding solves scale limits, but introduces distributed-systems costs: -Sharding splits data and traffic across nodes so the system can scale horizontally and recover from node loss without full outage. +- cross-shard query fan-out +- key skew and hotspots +- online rebalancing complexity +- routing correctness +- control-plane convergence under churn --- -## Common Sharding Strategies +## 2) Core Concepts You Must Get Right -### 1. Hash-based sharding +### 2.1 Shard key -- Route by `hash(key) % N` (or consistent hashing ring). -- Usually best for write distribution. -- Risk: query fan-out when access patterns are not key-local. +The shard key answers: "which shard owns this record?" -### 2. Range-based sharding +A bad key causes: -- Route by key range (time range, lexicographic ID range, etc.). -- Efficient for range scans. -- Risk: hotspot shards when new writes cluster on one range. +- uneven load +- hot partitions +- expensive multi-shard queries -### 3. Tenant-aware or domain-aware sharding +A good key keeps scaling predictable. -- Route by tenant, org, customer, or business domain. -- Useful for noisy-neighbor isolation and SLO boundaries. -- Risk: skew when tenants are very different in traffic volume. +### 2.2 Routing function ---- +Once a key is selected, routing is: -## Shard Key Selection Framework +```text +route(key) -> shard +``` -When selecting a shard key, evaluate these questions first: +Common routing families: -1. What is the dominant operation: write-heavy, point read, or range scan? -2. What is the hot key risk: are a few IDs responsible for most traffic? -3. Can common queries be answered within one shard? -4. How often do entities move across keys (for example, user re-partition)? +- modulo hash +- consistent hash ring +- range map +- tenant/domain mapping -Typical outcomes: +### 2.3 Replication -- write-heavy + uniform keys: hash sharding -- range scans by time: range or hybrid (time + hash suffix) -- multi-tenant SaaS: tenant-aware sharding, optionally with intra-tenant hash +Sharding distributes ownership. Replication provides availability. -Anti-patterns: +Together they define: -- shard key chosen from low-cardinality fields -- shard key unrelated to major query predicates -- no migration path defined for future re-sharding +- write quorum and durability +- read consistency model +- behavior during partial failures --- -## Consistent Hashing and Rehash Cost +## 3) Strategy Selection (Hash, Range, Tenant) -Naive `hash(key) % N` remaps most keys when `N` changes. Consistent hashing reduces movement during scale-out/scale-in. +### 3.1 Hash-based sharding -Practical options: +Route by hash (modulo or ring). -- token ring with virtual nodes -- rendezvous (highest-random-weight) hashing -- jump consistent hash for low-memory client-side routing +Best for: -Operational notes: +- high write rates +- near-uniform key distribution -- virtual nodes improve distribution smoothness -- replication factor must be considered with topology awareness -- scale events should be rate-limited to avoid cache churn and queue spikes +Tradeoff: ---- +- point writes distribute well +- analytical queries often fan out + +### 3.2 Range-based sharding + +Route by key interval (time range, lexical range, numeric range). + +Best for: + +- time-series windows +- range scans and pagination -## Sparse Table vs Segment Tree in Sharded Systems +Tradeoff: -These are not sharding methods themselves. They are **auxiliary data structures** for fast routing and balancing decisions. +- range reads are efficient +- latest range often becomes hot -### Sparse Table (static range query) +Common mitigation: -- Best when underlying values are mostly immutable between rebuilds. -- Precompute range answers (commonly RMQ/min/max/GCD style). -- Query in `O(1)` for idempotent operations like min/max. -- Build in `O(n log n)` and updates are expensive. +- time partition + hash suffix -Practical use: +### 3.3 Tenant-aware sharding -- query planner snapshots with precomputed shard-latency minima -- read-heavy shard-selection policies updated in batch windows +Route by tenant/org/customer boundary. -### Segment Tree (dynamic range query) +Best for: -- Best when values change continuously. -- Query and point update typically in `O(log n)`. -- Suitable for online balancing with frequent metric updates. +- multi-tenant SaaS +- noisy-neighbor isolation +- explicit per-tenant SLOs -Practical use: +Tradeoff: -- live shard load tracking (QPS, p95/p99 latency, queue depth) -- fast “least loaded shard in range/window” decisions +- large tenants dominate capacity unless split further -### Rule of thumb +Common mitigation: -- Use **Sparse Table** when routing metadata is mostly static and query volume is high. -- Use **Segment Tree** when routing metadata is dynamic and update frequency is high. +- shuffle sharding +- intra-tenant hashing --- -## Hotspot Mitigation Patterns +## 4) Consistent Hashing and Rehash Cost -### Write hotspots +Naive modulo routing: -- key salting (`user_id#bucket`) -- adaptive partitioning (split hot ranges) -- buffering and batch writes +```text +hash(key) % N +``` + +Problem: changing `N` remaps most keys. + +Consistent hashing limits movement during scale events. -### Read hotspots +Typical implementations: -- request coalescing / singleflight -- read replicas with bounded staleness -- edge/cache tiers with key-level TTL strategy +- token ring + virtual nodes +- rendezvous (HRW) hashing +- jump consistent hash -### Tenant hotspots +Operational guidance: -- shuffle sharding (isolate tenants to shard subsets) -- per-tenant quotas and fairness scheduling -- noisy-neighbor circuit breakers +- use enough virtual nodes for smooth distribution +- make replica placement topology-aware +- rate-limit scale events to avoid cache churn and queue spikes --- -## Rebalancing and Re-sharding +## 5) Sparse Table and Segment Tree: Why They Matter to Sharding -Re-sharding should be treated as a product feature, not an emergency action. +These are not shard-routing strategies. They are **control-plane data structures** used to make routing and balancing decisions efficiently. -Safe migration patterns: +### 5.1 Where they fit in a sharded system -1. **Dual-write + read-from-old** phase. -2. Backfill historical data to new shard layout. -3. Read-compare (shadow reads) for consistency checks. -4. Switch reads to new layout. -5. Decommission old layout after verification window. +- Data plane: + - write/read traffic using hash/range/tenant routing +- Control plane: + - shard health snapshots + - load-aware shard selection + - rebalancing decisions -What to monitor during migration: +Sparse Table and Segment Tree belong to the control plane. -- cross-shard latency and fan-out count -- mismatch rate between old/new reads -- error budget burn rate -- queue depth and retry storm signals +### 5.2 Sparse Table (static range query) ---- +Use when shard metadata changes infrequently between rebuild windows. + +Properties: -## Query Fan-out Control +- build: `O(n log n)` +- query (idempotent ops like min/max): `O(1)` +- updates: expensive (rebuild-oriented) -Cross-shard fan-out is often the hidden cost of sharding. +Sharding-related use cases: -Reduction strategies: +- precomputed minimum-latency shard per interval from a snapshot +- read-heavy planner paths where metadata is batch-refreshed +- "choose best shard set for this time window" with immutable window stats -- align shard key with dominant filter predicates -- add pre-aggregation indexes -- use routing hints (tenant, partition key, time window) -- execute two-phase queries (candidate shard discovery, then targeted fetch) +### 5.3 Segment Tree (dynamic range query) + +Use when shard metrics change continuously. + +Properties: + +- build: `O(n)` or `O(n log n)` depending on implementation +- point update: `O(log n)` +- range query: `O(log n)` + +Sharding-related use cases: + +- live shard load tracking (QPS, p95, queue depth) +- choosing least-loaded shard in a subset +- online balancing with frequent updates from telemetry + +### 5.4 Practical rule + +- metadata mostly static + heavy query rate -> Sparse Table +- metadata highly dynamic + frequent updates -> Segment Tree + +The connection to sharding is direct: they make control decisions fast enough to keep routing stable under load. --- -## Replication and Failure Domains +## 6) Fan-out, Hotspots, and Re-sharding + +### 6.1 Fan-out is the hidden tax + +When one query touches many shards: -Sharding without replication gives scale but weak availability. +- tail latency increases +- memory and merge cost increase +- probability of partial failure increases -Design points: +Mitigation: -- replication factor per data criticality tier -- zone-aware replica placement -- leader election and write quorum policy -- read semantics during partial failures (strong vs eventual) +- align shard key with dominant filters +- constrain time windows +- route with tenant/partition hints +- use two-phase query (discover candidate shards -> targeted fetch) + +### 6.2 Hotspot patterns + +- write hotspot: key salting, adaptive split, write buffering +- read hotspot: coalescing, replicas, cache tiers +- tenant hotspot: shuffle sharding, quotas, fairness schedulers + +### 6.3 Safe re-sharding sequence + +1. dual-write + read-old +2. backfill new layout +3. shadow-read compare +4. switch read traffic +5. decommission old layout after verification window + +Never treat shard-key migration as an emergency shortcut. --- -## Example 1: Elasticsearch +## 7) Control Plane vs Data Plane -Elasticsearch shards an index into **primary shards** (plus replicas). Writes and queries are routed to shards, then merged. +Data plane handles user traffic. +Control plane handles ownership, ring updates, health propagation, and balancing loops. -### How it maps to sharding concepts +Most large incidents are caused by control-plane instability, not by hash math itself. -- Default routing hashes routing value (by default `_id`) to choose primary shard. -- Search can fan out to many shards and then merge results. -- Hotspots appear when routing keys or write windows are uneven. +### Cortex ring loop optimization notes -### Practical design tips +- PR #7266: merged on **February 16, 2026** + - [cortexproject/cortex#7266](https://github.com/cortexproject/cortex/pull/7266) + - replaced repeated `time.After(...)` calls in DynamoDB watch loops with reusable timers + - benchmark in PR discussion: around `248 B/op, 3 allocs/op` -> `0 B/op, 0 allocs/op` +- PR #7270: merged on **February 20, 2026** + - [cortexproject/cortex#7270](https://github.com/cortexproject/cortex/pull/7270) + - extended timer reuse to lifecycler and backoff loops, with shared timer helpers -- use custom routing for tenant locality when applicable -- plan index lifecycle and shard size boundaries early -- reduce fan-out by constraining query time range and index pattern -- review shard imbalance regularly and rebalance proactively +Why this matters to sharding: + +Control loops run continuously. Per-iteration allocations add GC pressure and jitter, which can delay ownership convergence and destabilize shard transitions during churn. --- -## Example 2: Cortex +## 8) Mapping to Real Systems -Cortex uses a **consistent-hash ring** for sharding and replication of time-series responsibilities across ingesters and ring-based components. +### 8.1 Elasticsearch -```mermaid -graph LR - D[Distributor] -->|hash series labels| R[(Ring)] - R --> I1[Ingesters A] - R --> I2[Ingesters B] - R --> I3[Ingesters C] - Q[Querier] --> R -``` +- index split into primary shards + replicas +- default routing hashes `_id` (or custom routing key) +- query path often fans out then merges + +Design implications: -### How it maps to sharding concepts +- choose routing key for locality where possible +- constrain index/time scope to reduce fan-out +- plan shard sizing and lifecycle early -- Hash-based sharding maps series to token ranges in the ring. -- Replication set assigns multiple ingesters for HA. -- Ring health and convergence speed directly affect ingestion/query stability. +### 8.2 Cortex -### Practical Cortex-specific notes +- distributor hashes series labels into a consistent-hash ring +- ring selects replication set (multiple ingesters) +- ring convergence quality directly affects ingestion/query stability -- ring backend latency affects ownership propagation -- token movement should be controlled during rollouts -- shuffle-sharding style isolation can reduce tenant blast radius in multi-tenant environments +Design implications: + +- ring backend latency affects ownership propagation speed +- token movement should be controlled during rollout +- control-loop efficiency directly impacts ring stability --- -## Cortex PR Notes on Ring Watch Loop Optimization +## 9) Go Reference Implementation -### PR #7266 +A runnable Go example is included at: -- URL: [cortexproject/cortex#7266](https://github.com/cortexproject/cortex/pull/7266) -- Title: `ring/kv/dynamodb: reuse timers in watch loops to avoid per-poll allocations` -- Status: **merged on February 16, 2026** -- Key changes: - - Replaced repeated `time.After(...)` in DynamoDB watch loops with reusable `time.Timer`. - - Added safe `resetTimer` behavior (stop + drain + reset). - - Added benchmark file `pkg/ring/kv/dynamodb/client_timer_benchmark_test.go`. -- Reported benchmark result in PR: - - `time.After`: about `248 B/op`, `3 allocs/op` - - reusable timer: `0 B/op`, `0 allocs/op` +- `infra/assets/sharding/main.go` -### PR #7270 +It demonstrates: -- URL: [cortexproject/cortex#7270](https://github.com/cortexproject/cortex/pull/7270) -- Title: `[ENHANCEMENT] ring/backoff: reuse timers in lifecycler and backoff loops` -- Status: **open as of February 18, 2026** -- Key changes: - - Extends timer reuse into `lifecycler`, `basic_lifecycler`, and `util/backoff`. - - Introduces shared safe timer helpers in `pkg/ring/ticker.go`. - - Includes a small DynamoDB CAS allocation improvement (`make(map..., len(buf))`). +1. modulo hash router +2. consistent hash ring with virtual nodes and replication set lookup +3. tenant-aware routing with per-tenant ring +4. Sparse Table for static range-min snapshot queries +5. Segment Tree for dynamic load-aware range-min queries -### Why these matter for sharding +Run it: -Ring watch/backoff/lifecycler loops run continuously in control-plane paths. Reducing per-iteration allocations lowers GC pressure and jitter, which helps keep shard ownership transitions and ring convergence stable under load. +```bash +go run infra/assets/sharding/main.go +``` --- -## Practical Design Checklist +## 10) Final Design Rules -1. Pick shard key by access pattern first, not by schema aesthetics. -2. Define rebalancing strategy before production (split, migrate, throttle). -3. Add hotspot observability: per-shard QPS, p95/p99 latency, queue depth. -4. Track cross-shard fan-out and keep it within explicit SLO limits. -5. Separate control-plane stability from data-plane throughput budgets. -6. Use static vs dynamic metadata structures intentionally: - - static-heavy: Sparse Table - - update-heavy: Segment Tree +1. Choose shard key by dominant query shape, not schema aesthetics. +2. Design re-sharding before production. +3. Track per-shard metrics (QPS, p95/p99, queue depth, fan-out). +4. Keep control loops allocation-light and predictable. +5. Replication and topology awareness are mandatory for production. +6. Use Sparse Table / Segment Tree intentionally based on update dynamics. --- diff --git a/infra/sharding_ja.md b/infra/sharding_ja.md index d98efda..90347e6 100644 --- a/infra/sharding_ja.md +++ b/infra/sharding_ja.md @@ -1,289 +1,350 @@ -# シャーディング基礎: Sparse Table, Segment Tree, Elasticsearch, Cortex +# シャーディング: 原理から Go 実装まで -このガイドは、分散システムにおける実践的なシャーディング設計を、次の実システムと結びつけて整理します。 +このドキュメントは、シャーディングを「概念説明」ではなく「実装して運用できる設計」として整理します。 -- **Elasticsearch** におけるインデックス/データ分割 -- **Cortex** におけるハッシュリングベースの時系列シャーディング -- シャード選択や負荷分散に使える補助データ構造としての **Sparse Table** と **Segment Tree** +扱う内容: -## ゴール +1. シャーディングが必要になる理由 +2. 戦略選定(hash / range / tenant) +3. fan-out・ホットスポット・再シャーディング +4. 制御プレーン安定性の重要性 +5. Sparse Table / Segment Tree とシャーディングの関係 +6. Go での実装パターン +7. Elasticsearch / Cortex への対応づけ -この資料を読むことで、以下を判断できるようになることを目指します。 +--- -1. ワークロード形状に応じて戦略(hash/range/tenant-aware)を選べる。 -2. スキューとクロスシャード fan-out を抑えるシャードキーを選べる。 -3. シャーディング制御面で Sparse Table / Segment Tree の使い分けができる。 -4. Elasticsearch と Cortex に概念を対応づけられる。 -5. Cortex PR #7266 / #7270 がリング制御ループへ与える影響を説明できる。 +## 1) シャーディングとは何か ---- +シャーディングは、データとトラフィックを複数ノードへ水平分割する設計です。 -## なぜシャーディングが必要か +単一ノード集中: + +```text +all data -> one machine +``` + +分割後: + +```text +data partitioned -> many machines +``` -単一ノードは、いずれ以下のどれかで限界に達します。 +各分割単位を **shard** と呼びます。 -- 保存容量 -- 書き込みスループット -- クエリ同時実行性 -- 障害ドメインの広さ +必要になる背景: -シャーディングはデータとトラフィックを分割し、水平スケールと障害時の影響局所化を実現します。 +- 保存容量の上限 +- 書き込みスループットの上限 +- クエリ同時実行数の上限 +- 障害時の blast radius 拡大 + +ただし、スケールと引き換えに次の課題が増えます。 + +- クロスシャード fan-out +- スキューとホットスポット +- オンライン再分配の複雑化 +- ルーティング整合性 +- 制御プレーン収束の不安定化 --- -## 代表的なシャーディング戦略 +## 2) 最初に固定すべきコア概念 -### 1. ハッシュベース +### 2.1 シャードキー -- `hash(key) % N`(または一貫性ハッシュリング)で振り分ける。 -- 書き込み分散に強い。 -- 主要アクセスがキー局所でないと fan-out が増えやすい。 +「このデータはどのシャードが持つか」を決める軸です。 -### 2. レンジベース +悪いキー選定は: -- 時刻帯や ID 範囲など、キー範囲で振り分ける。 -- 範囲検索に強い。 -- 新規書き込みが一部レンジに集中するとホットシャード化しやすい。 +- 負荷偏り +- ホットパーティション +- 高コストな多シャード検索 -### 3. テナント/ドメインベース +につながります。 -- tenant/org/customer などの境界で振り分ける。 -- ノイジーネイバー対策や SLO 分離に有効。 -- テナント規模差が大きいと偏りやすい。 +### 2.2 ルーティング関数 ---- +キーが決まると、ルーティングは次です。 -## シャードキー選定フレームワーク +```text +route(key) -> shard +``` -シャードキー選定時は、まず以下を確認します。 +代表パターン: -1. 主体は write-heavy / point read / range scan のどれか。 -2. ホットキーが生じるか(少数 ID にアクセス集中するか)。 -3. 代表クエリを単一シャードで完結できるか。 -4. 将来の再分割でキー移動が頻繁に発生しないか。 +- modulo hash +- consistent hash ring +- range map +- tenant/domain map -よくある選択: +### 2.3 レプリケーション -- 書き込み中心かつキー分布が均一: ハッシュ分割 -- 時系列範囲検索中心: レンジ分割または time+hash のハイブリッド -- マルチテナント SaaS: テナント分割 + 必要に応じてテナント内ハッシュ +シャーディングは分散、レプリケーションは可用性です。 -アンチパターン: +組み合わせにより以下が決まります。 -- 低カーディナリティ列をそのままシャードキー化 -- 主要クエリ条件と無関係なキー選定 -- 再シャーディング手順を設計せずに本番投入 +- 書き込みクォーラムと耐久性 +- 読み取り整合性 +- 部分障害時の挙動 --- -## 一貫性ハッシュと再配置コスト +## 3) 戦略選定: Hash / Range / Tenant -単純な `hash(key) % N` はノード数 `N` の変更時に大半キーが再配置されます。 -一貫性ハッシュは、スケール時の再配置量を抑えます。 +### 3.1 ハッシュ分割 -実装パターン: +キーをハッシュして振り分けます。 -- 仮想ノード付きトークンリング -- Rendezvous (HRW) hashing -- 低メモリなクライアント側ルーティングに向く Jump Consistent Hash +向いているケース: -運用上の要点: +- 高書き込み +- キー分布が比較的均一 -- 仮想ノードは分布平滑化に有効 -- レプリカ配置はトポロジ(ゾーン)を考慮 -- スケールイベントはキャッシュミス急増を避けるため段階的に実行 +トレードオフ: ---- +- 書き込み分散はしやすい +- 集計クエリは fan-out しやすい + +### 3.2 レンジ分割 + +時刻帯や ID 範囲で分割します。 + +向いているケース: -## Sparse Table と Segment Tree の使い分け +- 時系列ウィンドウ検索 +- 範囲スキャン -これらはシャーディング方式そのものではなく、**シャード選択・負荷分散判断のための補助データ構造**です。 +トレードオフ: -### Sparse Table(静的レンジクエリ向け) +- 範囲読み取りが効率的 +- 最新レンジがホット化しやすい -- 値更新が少ないケースに向く。 -- RMQ/min/max/GCD などを前計算して保持。 -- min/max のような冪等演算ではクエリ `O(1)`。 -- 構築 `O(n log n)`、更新は重い。 +定番対策: -使いどころ: +- time partition + hash suffix -- シャード遅延の最小値スナップショットを定期再構築 -- 読み取り主体のルーティング判断 +### 3.3 テナント分割 -### Segment Tree(動的レンジクエリ向け) +tenant/org/customer 境界で分割します。 -- 値が継続的に変化するケースに向く。 -- クエリ/一点更新ともに `O(log n)` が基本。 -- メトリクス更新頻度が高いオンライン負荷分散に適する。 +向いているケース: -使いどころ: +- マルチテナント SaaS +- ノイジーネイバー分離 +- テナント別 SLO -- シャードごとの QPS / p95 / p99 / queue depth を動的管理 -- 範囲内の低負荷シャード探索 +トレードオフ: -### 実務上の目安 +- 大規模テナントが容量を圧迫しやすい -- ほぼ静的なメタデータ + 高クエリ頻度: **Sparse Table** -- 高頻度更新されるメタデータ: **Segment Tree** +定番対策: + +- shuffle sharding +- テナント内ハッシュ --- -## ホットスポット対策 +## 4) 一貫性ハッシュと再配置コスト + +単純な modulo 方式: + +```text +hash(key) % N +``` + +問題: -### 書き込みホットスポット +`N` の変更で多くのキーが再配置される。 -- key salting(例: `user_id#bucket`) -- ホットレンジの自動分割 -- バッファリングとバッチ書き込み +一貫性ハッシュはスケール時の移動量を抑えます。 -### 読み取りホットスポット +実装パターン: -- リクエスト集約(singleflight) -- 許容遅延つきリードレプリカ -- キー特性に応じた TTL 設計 +- 仮想ノード付きトークンリング +- Rendezvous (HRW) hashing +- Jump Consistent Hash -### テナント偏り +運用上の要点: -- shuffle sharding によるテナント分離 -- テナント別クォータと公平スケジューリング -- ノイジーネイバー用サーキットブレーカー +- 仮想ノード数で分布平滑化 +- レプリカ配置はトポロジを考慮 +- スケールイベントは段階実行 --- -## リバランスと再シャーディング +## 5) Sparse Table / Segment Tree とシャーディングの関係 + +これらはシャーディング方式そのものではありません。**シャード制御判断を高速化する制御プレーン向けデータ構造**です。 + +### 5.1 どこで使うか + +- データプレーン: + - 実トラフィックの write/read +- 制御プレーン: + - シャード状態スナップショット + - 負荷ベース選択 + - 再配置判断 + +Sparse Table / Segment Tree は制御プレーンで効きます。 + +### 5.2 Sparse Table(静的レンジクエリ向け) + +シャードメタデータが「一定期間ほぼ不変」の場合に使います。 + +性質: + +- 構築: `O(n log n)` +- クエリ(min/max など): `O(1)` +- 更新: 重い(再構築前提) + +シャーディング文脈での用途: + +- 期間スナップショットから最小遅延シャードを即時参照 +- バッチ更新されるプランナーメタデータの高速参照 +- 静的区間統計に基づく候補シャード抽出 -再シャーディングは緊急対応ではなく、通常運用機能として設計します。 +### 5.3 Segment Tree(動的レンジクエリ向け) -安全な移行パターン: +シャード負荷メトリクスが継続的に変化する場合に使います。 -1. `dual-write + old read` を開始 -2. 新配置へバックフィル -3. shadow read で整合性比較 -4. read を新配置へ切替 -5. 検証期間後に旧配置を廃止 +性質: -移行時の監視項目: +- 構築: `O(n)` もしくは `O(n log n)` +- 一点更新: `O(log n)` +- 範囲クエリ: `O(log n)` -- クロスシャード遅延と fan-out 数 -- old/new 読み取り差分率 -- エラーバジェット消費速度 -- キュー深さとリトライ急増兆候 +シャーディング文脈での用途: + +- shard ごとの QPS / p95 / queue depth の動的更新 +- 一部範囲から最小負荷 shard を選ぶ +- テレメトリ駆動のオンライン再分配 + +### 5.4 使い分け基準 + +- ほぼ静的メタデータ + 高クエリ頻度 -> Sparse Table +- 高頻度更新メタデータ -> Segment Tree + +要点は、シャーディングの安定性は「どのデータ構造で制御判断を回すか」に強く依存することです。 --- -## クエリ fan-out 制御 +## 6) Fan-out、ホットスポット、再シャーディング + +### 6.1 Fan-out は隠れコスト + +1 クエリが多数シャードへ広がると: -シャーディングの隠れコストは fan-out です。 +- tail latency が悪化 +- メモリ/マージコストが増加 +- 部分障害の影響確率が上昇 低減策: -- シャードキーを主要フィルタ条件に寄せる -- 事前集計インデックスを導入 -- tenant/partition/time window などの routing hint を活用 -- 二段階検索(候補シャード特定 -> 対象取得) +- 主要フィルタとシャードキーを一致させる +- 時間窓を絞る +- tenant/partition ヒントでルーティングする +- 二段階クエリ(候補抽出 -> 対象取得) ---- +### 6.2 ホットスポット対策 -## レプリケーションと障害ドメイン +- 書き込み偏り: key salting, adaptive split, buffering +- 読み取り偏り: coalescing, replicas, cache tiers +- テナント偏り: shuffle sharding, quota, fairness scheduler -シャーディングのみでは可用性は十分ではありません。レプリケーション戦略を同時に設計します。 +### 6.3 安全な再シャーディング -設計ポイント: +1. dual-write + old read +2. 新配置へ backfill +3. shadow read で比較 +4. read を切替 +5. 検証後に旧配置廃止 -- データ重要度別レプリカ数 -- ゾーン分散配置 -- リーダー選出と書き込みクォーラム -- 部分障害時の読み取り整合性(strong / eventual) +シャードキー変更を即時対応で済ませないことが重要です。 --- -## 例1: Elasticsearch +## 7) データプレーンと制御プレーン + +データプレーンはユーザートラフィック処理。 +制御プレーンは所有権、リング更新、ヘルス伝播、再配置ループ処理。 -Elasticsearch はインデックスを **primary shards**(+ replica)へ分割し、書き込み/検索をシャード単位で処理して結果をマージします。 +重大障害の多くは、ハッシュ関数そのものではなく制御プレーン不安定化で発生します。 -### 概念対応 +### Cortex のリングループ最適化 -- 既定ルーティングは(既定では `_id` の)ハッシュで primary shard を決定。 -- 検索は多シャード fan-out + マージになりやすい。 -- ルーティングキーや時系列偏りでホットシャードが発生しやすい。 +- PR #7266: **2026年2月16日**にマージ + - [cortexproject/cortex#7266](https://github.com/cortexproject/cortex/pull/7266) + - DynamoDB watch loop の `time.After(...)` を再利用 `time.Timer` へ置換 + - PR 掲載ベンチ: 約 `248 B/op, 3 allocs/op` -> `0 B/op, 0 allocs/op` +- PR #7270: **2026年2月20日**にマージ + - [cortexproject/cortex#7270](https://github.com/cortexproject/cortex/pull/7270) + - lifecycler / backoff ループまで timer 再利用を拡張 -### 実務上の設計ポイント +シャーディングへの意味: -- テナント局所性が必要なら custom routing を検討 -- シャードサイズとライフサイクルを早期に設計 -- 時間範囲とインデックスパターンを絞って fan-out を抑制 -- シャード偏りを定常的に監視し、先手で再配置 +制御ループは常時動作するため、微小な allocation でも GC ジッタを増幅し、所有権収束遅延や遷移不安定化を招きます。 --- -## 例2: Cortex +## 8) 実システムへの対応づけ -Cortex は **consistent hash ring** を使い、ingester などのリング対象コンポーネントへ時系列責務を分散します。 +### 8.1 Elasticsearch -```mermaid -graph LR - D[Distributor] -->|series labels をハッシュ| R[(Ring)] - R --> I1[Ingester A] - R --> I2[Ingester B] - R --> I3[Ingester C] - Q[Querier] --> R -``` +- index を primary shards + replicas へ分割 +- `_id`(または custom key)ハッシュで routing +- 検索は fan-out 後にマージ -### 概念対応 +設計示唆: -- ハッシュ分割により series をトークン範囲へ割り当て -- 複数 ingester へのレプリケーションで HA を確保 -- リング収束速度と健全性が ingestion/query 安定性へ直結 +- 局所性が必要なら routing key を設計 +- 時間範囲と index 対象を絞って fan-out 抑制 +- shard サイズと lifecycle を初期段階で決定 -### Cortex運用の注意点 +### 8.2 Cortex -- ring backend の遅延は所有権伝播遅延に直結 -- ロールアウト時のトークン移動量は制御が必要 -- マルチテナントでは shuffle-sharding 的な隔離が有効 +- series labels を consistent-hash ring に投入 +- ring から replication set を選択 +- ring 収束品質が ingestion/query 安定性に直結 + +設計示唆: + +- ring backend レイテンシは所有権伝播に影響 +- rollout 時の token movement を制御 +- 制御ループ効率がシャード安定性を左右 --- -## Cortex PR(リング監視ループ最適化) +## 9) Go 実装リファレンス -### PR #7266 +実行可能なサンプルを追加しています: -- URL: [cortexproject/cortex#7266](https://github.com/cortexproject/cortex/pull/7266) -- タイトル: `ring/kv/dynamodb: reuse timers in watch loops to avoid per-poll allocations` -- 状態: **2026年2月16日にマージ済み** -- 変更点: - - DynamoDB watch loop の `time.After(...)` を再利用可能 `time.Timer` に置換 - - 安全な `resetTimer`(stop + drain + reset)を導入 - - ベンチマーク `pkg/ring/kv/dynamodb/client_timer_benchmark_test.go` を追加 -- PR 記載のベンチ結果: - - `time.After`: 約 `248 B/op`, `3 allocs/op` - - reusable timer: `0 B/op`, `0 allocs/op` +- `infra/assets/sharding/main.go` -### PR #7270 +含まれる内容: -- URL: [cortexproject/cortex#7270](https://github.com/cortexproject/cortex/pull/7270) -- タイトル: `[ENHANCEMENT] ring/backoff: reuse timers in lifecycler and backoff loops` -- 状態: **2026年2月18日時点で open** -- 変更点: - - `lifecycler`, `basic_lifecycler`, `util/backoff` へ timer 再利用を拡張 - - `pkg/ring/ticker.go` に安全な timer helper を共通化 - - DynamoDB CAS で `make(map..., len(buf))` による小さな割り当て最適化 +1. modulo hash router +2. 仮想ノード付き consistent hash ring + 複製セット取得 +3. tenant-aware routing +4. Sparse Table による静的 range-min クエリ +5. Segment Tree による動的 range-min クエリ -### シャーディング観点での意義 +実行: -ring watch/backoff/lifecycler は制御プレーンの常時ループです。 -反復ごとのメモリアロケーション削減は GC 圧とジッタを下げ、シャード所有権遷移とリング収束の安定化に寄与します。 +```bash +go run infra/assets/sharding/main.go +``` --- -## 実践チェックリスト +## 10) 最終設計ルール -1. スキーマ都合ではなくアクセスパターンでシャードキーを決める。 -2. 本番前に再分割手順(split/migrate/throttle)を定義する。 -3. シャード単位の QPS/p95/p99/queue depth を監視する。 -4. fan-out 指標に上限 SLO を設定して追跡する。 -5. 制御プレーン安定性とデータプレーン性能を分離して予算化する。 -6. メタデータ更新特性で Sparse Table / Segment Tree を使い分ける。 +1. シャードキーはスキーマ都合ではなく主要クエリで決める。 +2. 本番前に再シャーディング経路を設計する。 +3. shard 単位メトリクス(QPS, p95/p99, queue depth, fan-out)を常時監視する。 +4. 制御ループは低 allocation で予測可能に保つ。 +5. レプリケーションとトポロジ考慮は必須。 +6. Sparse Table / Segment Tree は更新特性で使い分ける。 ---