Skip to content

Cloud Trace Fundamentals - Cơ chế Distributed Tracing

Tại sao lại cần distributed tracing trong production

Trong hệ thống monolith, bạn đặt breakpoint debugger vào function, step through code. Nhưng trong microservices, khi một request đi qua 8 services rồi, bạn không thể:

  • Sử dụng local debugger
  • Nhìn stack trace trực tiếp vì request không chạy trên một process
  • Tính latency bằng cách nhìn timestamps của logs vì mỗi service có clock riêng (clock skew)

Distributed tracing giải quyết bằng cách: gắn một trace ID duy nhất vào request, theo dõi request đi qua mỗi service như thế nào, và sau đó reconstruct toàn bộ flow trong Cloud Trace console.

Internal Model: Trace, Span, và Context

Trace ID - tiêu đề nhận dạng của cả request flow

Một trace là chuỗi hoạt động của một request từ lúc vào hệ thống đến khi hoàn thành. Mỗi trace có một trace ID duy nhất, là một chuỗi hex 32 ký tự (128 bit):

trace_id = a0f3c87f9a1b2c3d4e5f6a7b8c9d0e1f

Trace ID này được:

  1. Sinh ra lần đầu tiên request đến frontend service
  2. Truyền đi trong mỗi HTTP/gRPC request tới downstream services (dùng header như X-Cloud-Trace-Context hoặc W3C traceparent)
  3. Lưu trữ bên Cloud Trace cùng với tất cả span data
  4. Truy vấn để xem toàn bộ flow

Span - đơn vị công việc

Mỗi operation bên trong trace gọi là một span. Ví dụ:

  • Span "Handle HTTP request" ở frontend service
  • Span "Query user database" ở user service
  • Span "Fetch product" ở product service
  • Span "Render template" ở frontend

Mỗi span có:

{
  name: "db.query",
  span_id: "1a2b3c4d5e6f7a8b",
  parent_span_id: "8b7a6f5e4d3c2b1a",  // span nào gọi span này
  trace_id: "a0f3c87f9a1b2c3d4e5f6a7b8c9d0e1f",
  start_time: "2026-06-24T10:30:00.000000Z",
  end_time: "2026-06-24T10:30:00.050000Z",
  duration: 50ms,
  attributes: {
    "db.system": "postgresql",
    "db.statement": "SELECT * FROM users WHERE id = ?",
    "db.rows_affected": 1
  },
  status: "OK"  // hoặc ERROR
}

Span Hierarchy - tạo cây quan hệ

Spans tạo thành một cây:

trace_id=abc123
├── span[http_request] (parent_span_id=null)
│   ├── span[db_query] (parent_span_id=http_request)
│   ├── span[cache_get] (parent_span_id=http_request)
│   └── span[call_downstream_service]
│       └── span[downstream_process] (parent_span_id=call_downstream_service)
└── span[response_render] (parent_span_id=http_request)

Quan hệ parent-child này cho phép:

  • Tính latency của từng operation
  • Xác định bottleneck (span nào lâu nhất)
  • Thấy parallelism (span nào chạy cùng lúc)

Context Propagation - vấn đề của distributed tracing

Khi request từ service A gọi service B:

Service A                      Service B
+----------+                  +----------+
| Span A1  |  HTTP GET        | Span B1  |
| (db)     | + headers -----> | (http)   |
|          |  ?trace_id=...   |          |
|          |  &parent_span=...| ...      |
+----------+                  +----------+

Service B cần biết:

  • trace_id là gì (để biết request này thuộc trace nào)
  • parent_span_id là gì (để biết ai gọi nó)

Nếu không có context propagation, mỗi service sẽ tạo trace ID mới → sau này không thể reconnect chúng lại.

Trace Collection Architecture

Span Generation

Khi instrumented code chạy:

python
from opentelemetry import trace

tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("process_order") as span:
    # span được emit ở đây
    span.set_attribute("order_id", 123)
    # ... code

Span được sinh ra in-memory trong application process, sau đó cần được export tới Cloud Trace.

Export Mechanism - OTLP (OpenTelemetry Protocol)

Application không gửi span trực tiếp tới Google Cloud API (quá chậm, overhead cao). Thay vào đó:

  1. Local Collector (optional) hoặc Application Buffer: Spans được lưu tạm trong memory buffer
  2. Batch Export: Mỗi N spans (thường N=512) hoặc mỗi T milliseconds (thường T=5000ms), spans được batch lại
  3. OTLP Protocol: Batch được gửi qua OTLP (OpenTelemetry Protocol) tới Cloud Trace API
  4. GCP Ingest: Cloud Trace API nhận batch, decompress, validate, lưu vào backend storage
Application
  |
  +-> [In-Memory Buffer]
  |       |
  |       v (mỗi 5s hoặc 512 spans)
  |   [OTLP Batch]
  |       |
  +----> gRPC ---> [Cloud Trace API]
              |
              v
          [Validation]
              |
              v
          [Storage Backend]

Sampling - Quyết định không export

Nếu export mỗi span, overhead sẽ không chịu nổi:

Một service xử lý 10,000 request/giây, mỗi request có 20 spans → 200,000 spans/giây. Nếu export tất cả:

  • Network overhead: 200KB/s+
  • API call volume: rate limiting
  • Storage cost: xuyên tế chi phí ngất ngưởng

Sampling decision quyết định: "Span này có nên export hay không?"

Head-Based Sampling

Quyết định được làm ở đầu trace (khi request đầu tiên đến):

python
# Sampler quyết định: "Có sample trace này không?"
sampler = TraceIdRatioBased(0.1)  # 10% sampled

if sampler.should_sample(trace_id):
    # export span
else:
    # đừng export

Lợi ích:

  • Nhanh, quyết định cục bộ
  • Overhead thấp

Nhược điểm:

  • Không thể capture tail latencies (những request chậm mà lẻ tẻ)
  • Nếu sample 0.1%, bạn miss 99.9% traces

Tail-Based Sampling

Quyết định được làm sau khi trace hoàn thành:

[Receive all spans]
    |
    v
[Wait for trace completion]
    |
    v
[Analyze: does trace match sampling criteria?]
    |
    v (e.g., latency > 1s? error occurred?)
[Keep or discard]

Ví dụ: "Keep tất cả traces với latency > 1 giây"

Lợi ích:

  • Capture tail latencies
  • Có thể prioritize interesting traces

Nhược điểm:

  • Cần lưu tạm tất cả spans cho đến khi trace complete
  • Storage overhead cao
  • Cloud Trace không hỗ trợ native tail-based sampling (phải dùng OpenTelemetry Collector)

GCP Sampling Strategy trong thực tế

Cloud Trace không hỗ trợ tail-based sampling natively. Thay vào đó, Google recommend:

  1. Probability-based head sampling (đơn giản nhất):

    sample_rate = 0.1  # 10%

    Với high-traffic systems (10K+ QPS), 0.1% hay 0.01% là normal.

  2. OpenTelemetry Collector với Tail Sampling Processor (nếu cần):

    • Deploy collector ở mỗi region
    • Collector nhận tất cả spans, buffer locally
    • Sau trace complete, collector quyết định keep/discard
    • Chỉ send sampled spans tới Cloud Trace

    Trade-off: Collector deployment complexity, local storage overhead, nhưng bạn get tail insights.

Storage & Retention

Trace Data Structure

Cloud Trace lưu:

{
  project_id: "my-gcp-project",
  trace_id: "a0f3c87f9a1b2c3d4e5f6a7b8c9d0e1f",
  spans: [
    { span_id, parent_span_id, name, start_time, end_time, ... },
    { ... }
  ],
  process_time: "2026-06-24T10:30:00.100000Z"
}

Mỗi trace có tối đa 10,000 spans (limit này hiếm khi hit).

Retention

  • Default retention: 30 ngày
  • Tier 1 (In-Situ Analysis): 30 ngày, tối ưu cho real-time analysis
  • Tier 2 (Archive): Optional, giữ lâu hơn (phí storage cao hơn)

Quyết định retention phải cân nhắc:

  • Compliance requirement (PCI, HIPAA có thể yêu cầu 1-7 năm)
  • Cost (lưu lâu = tốn tiền)
  • Investigation window (bạn thường investigate lỗi trong bao lâu?)

Quotas

Cloud Trace có quota:

MetricLimit
Traces ingested per minute~ 1M
Spans per trace10,000
API read requests20K/min
Spans per project per dayUnlimited (pay per GB)

Nếu exceed quota:

  • Spans bị drop silently (điều này nguy hiểm!)
  • Bạn không biết bạn đã miss data
  • Cost tăng nhanh không kiểm soát

Cost Model

Cloud Trace pricing (GCP 2026):

  • Ingestion: $0.50 per million spans (trên 1 triệu spans/tháng miễn phí)
  • Storage (Archive tier): $0.03 per GB per month
  • Analysis queries: $6 per 1M traces queried

Điều này có nghĩa:

  • Sampling at 0.1% thay vì 100% = 1000x cost reduction
  • Quyết định sampling là quyết định business, không chỉ technical

Sampling Decision Framework

Trade-off Matrix

FactorThiên về High SamplingThiên về Low Sampling
CostCaoThấp
CoverageTất cả requests đều có traceCó blind spot
Tail latency captureDễ thấy slow requestsCó thể miss slow requests
Investigation resolutionCaoThấp
Data noiseNhiều traces fast & normalÍt traces, chỉ sampled

Heuristic Sampling

Thay vì uniform probability (0.1% tất cả), có thể dùng heuristic:

python
def should_sample(request):
    # Luôn sample lỗi
    if request.has_error:
        return True
    
    # Luôn sample slow requests
    if request.latency > 1000ms:
        return True
    
    # Sample requests tới critical endpoints
    if request.endpoint in CRITICAL_ENDPOINTS:
        return True
    
    # Uniform sample 0.1% của cái còn lại
    return random() < 0.001

Kết quả: Bạn sample 5-10% thay vì 0.1%, nhưng 95% trong đó là "interesting" (slow, error, critical).

Trace Query & Analysis

Trace Explorer

Cloud Trace console cho phép:

  1. Timeline visualization: Xem mỗi span kéo dài bao lâu
  2. Span hierarchy: Xem parent-child relationships
  3. Latency breakdown: "90% latency ở đâu?" (database query, external API, etc)
  4. Stats: "P50, P99 latency across millions of traces"

SQL-based Analytics (Observability Analytics)

Nếu muốn query ngàn traces:

sql
SELECT
  PERCENTILE_CONT(span.duration_millis, 0.99) as p99_latency,
  COUNT(*) as trace_count,
  span.attributes['endpoint'] as endpoint
FROM traces.spans
WHERE trace.start_time >= "2026-06-24T00:00:00Z"
GROUP BY endpoint

Điều này useful cho:

  • SLO analysis (có meet latency target không?)
  • Trend detection (latency tăng theo thời gian?)
  • Service comparison (service A vs B, ai chậm hơn?)

Constraints & Failure Modes

Constraint 1: Clock Skew

Mỗi server có clock riêng. Khi span từ service A (server 1) gọi span từ service B (server 2), nếu clock khác nhau:

Server A (clock +5s)     Server B (clock -2s)
Span start: 10:30:00     Span start: 10:29:53
(nhưng thực tế cùng lúc)

Kết quả: trace timeline có thể bị sai lệch, thậm chí parent_span_id bị recorded sau child_span_id.

Mitigation:

  • Deploy NTP daemon trên tất cả servers
  • Cloud Trace có clock skew detection/correction algorithm
  • Metric: "clock skew" được track như một observability signal

Constraint 2: Lossy Due to Sampling

Nếu sampling 0.1%, bạn sẽ miss 99.9% traces. Điều này có nghĩa:

  • Một lỗi hiếm (xảy ra 1/5000 requests) có thể không bao giờ bị capture
  • Bạn không thể verify "lỗi đã fix chưa" vì bạn chỉ thấy 0.1% data

Mitigation:

  • Combine tracing với metrics (error rate, latency percentile) - nếu metrics spike, biết là có vấn đề
  • Tail sampling cho error/slow spans
  • Alert nếu sampling rate quá thấp

Constraint 3: Context Propagation Failure

Nếu một service không pass trace ID headers:

Service A                           Service B
Span A123 ----HTTP---> (no headers) Span B999  ← span ID mới!

Result: Trace bị break, service B có separate trace. Không thể see full flow.

Mitigation:

  • Validate context propagation: check logs để verify headers đúng format
  • Auto-instrumentation (OpenTelemetry agents) handle propagation automatically

Constraint 4: Trace Completeness

Khi trace có 1000+ spans từ nhiều services, một service delay hoặc timeout → trace incomplete.

Cloud Trace sẽ:

  • Timeout wait 30-60 seconds
  • Publish partial trace
  • Mark as incomplete

Khi xem trace, bạn thấy dấu "⚠️ incomplete" → không full picture.

Performance Implications

Span Generation Overhead

Tạo span (gọi tracer.start_span()) có cost:

  • Allocate span object: ~100 nanoseconds
  • Set attributes: ~10 nanoseconds mỗi attribute
  • Close span: ~50 nanoseconds

Với 10,000 spans/second, CPU cost ≈ 1.6ms/s = 0.16% CPU. Chấp nhận được.

Export Overhead

Batching + export OTLP:

  • Local processing: minimal
  • Network: 1-10ms per batch (depend on latency)
  • GCP ingestion: typically <100ms end-to-end

Điều quan trọng: Export không block application thread (async), nên latency application không bị impact.

Memory Overhead

In-memory buffer lưu spans:

  • Per span: ~500-1000 bytes (depend on attributes)
  • Buffer size: typically ~512 spans = 250-500 KB

Chấp nhận được với modern JVMs/Go processes (mem sử dụng gigs).

Production Patterns

Pattern 1: Baseline Sampling + Alert-Triggered High Sampling

Bình thường: sample 0.1%

Khi alert fire (latency spike detected):

  • Tạm thời increase sample rate lên 5-10%
  • Capture more traces
  • Debug issue

Sau khi debug xong:

  • Quay lại 0.1%

Implementation: Sampler lấy sample rate từ dynamic config service.

Pattern 2: Sampling by Criteria

python
def should_sample(request):
    # Tất cả internal services: 1%
    if request.is_internal:
        return random() < 0.01
    
    # External API: 0.1% (cost conscious)
    if request.is_external_api:
        return random() < 0.001
    
    # Custom: critical customers get higher sampling
    if request.customer_tier == "enterprise":
        return random() < 0.5
    
    # Default
    return random() < 0.01

Pattern 3: Tail Sampling với OpenTelemetry Collector

Deploy collector regional:

App1 ─┐
App2 ─┼─> [OTel Collector] (tail sampling logic)
App3 ─┘          |
                 v
            Cloud Trace (only sampled traces)

Collector filtering:

  • Keep: latency > 1 second
  • Keep: status == ERROR
  • Drop: otherwise

Anti-patterns

Anti-pattern 1: "Vô điều kiện export mỗi span"

python
# BAD
exporter.export_every_span = True

Vấn đề:

  • 10K QPS × 20 spans = 200K spans/sec = huge cost
  • Network saturation
  • GCP quota exceeded
  • Service bị throttle

Anti-pattern 2: "Không sample gì cả, chỉ 0%"

python
# BAD
sampler.ratio = 0.0  # never sample

Vấn đề:

  • Không có trace data
  • Không có debug capability
  • Latency issue xảy ra, bạn không biết where

Anti-pattern 3: "Sampling rate fixed, không adjust"

python
# BAD
sampler.ratio = 0.001  # hardcoded

Vấn đề:

  • High-traffic periods: miss important traces
  • Low-traffic periods: wasting quota sampling boring traces
  • Không adaptive

References