Trace Metrics และ DDSketch
ไอเดียหลักในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียหลักในหนึ่งประโยค”ไม่ว่า sampling ทั้งสองชั้นในบทที่แล้วจะเป็นยังไง Datadog คำนวณ trace metrics — hit count, error count และ latency — อย่างต่อเนื่องจากทุก span ที่มาถึง Agent/Datadog ทำให้ dashboard เรื่อง traffic กับ latency ของคุณยังแม่นยำในเชิงสถิติ แม้ว่า trace แต่ละตัวที่ยัง inspect ได้จะเหลืออยู่แค่เศษเสี้ยวก็ตาม
Trace metrics: คำนวณจากทุก span อย่างต่อเนื่อง
หัวข้อที่มีชื่อว่า “Trace metrics: คำนวณจากทุก span อย่างต่อเนื่อง”สำหรับทุก service และ resource Datadog aggregate สามอย่างนี้จาก stream ของ span ที่ไหลเข้ามา
- hit count — มี request/operation เกิดขึ้นกี่ครั้ง
- error count — กี่ครั้งที่ error
- latency — distribution ของเวลาที่ใช้ไป
การ aggregate นี้เกิดขึ้นต่อเนื่องและมาจากทุก span ที่มาถึง Agent/Datadog — ไม่ได้คำนวณจากการเปิด trace ที่เก็บไว้แล้วมาอ่านซ้ำทีหลัง นี่คือจุดสำคัญที่ต่างจากบทที่แล้ว ingestion-time sampling กับ retention filter ควบคุมว่า trace เต็มตัวใดตัวหนึ่งโดยเฉพาะ จะเปิดดูได้ทีหลังหรือไม่ แต่การนับและจับเวลาแบบ aggregate ทำงานจาก span stream เอง โดยไม่ขึ้นกับสอง layer นั้นเลย
DDSketch: percentile ที่รอดจากการ aggregate
หัวข้อที่มีชื่อว่า “DDSketch: percentile ที่รอดจากการ aggregate”latency ไม่ใช่ตัวเลขเดียว — สิ่งที่คุณสนใจคือ distribution โดยเฉพาะ tail (p95, p99) ไม่ใช่แค่ค่าเฉลี่ย แต่ percentile มีคุณสมบัติที่ยุ่งยาก — เอา percentile จากสอง host มา average กันแล้วได้ percentile รวมที่ถูกต้องไม่ได้ Datadog แก้ปัญหานี้ด้วย DDSketch ที่เป็น distribution metric type แบบ sketch ที่ออกแบบมาให้ percentile estimate แม่นยำและ merge กันได้ ที่ scale ใหญ่
from ddsketch import DDSketch
sketch_host_a = DDSketch(relative_accuracy=0.01)sketch_host_b = DDSketch(relative_accuracy=0.01)# each host's sketch summarizes every observed span latency locallyfor v in latencies_host_a: sketch_host_a.add(v)for v in latencies_host_b: sketch_host_b.add(v)sketch_host_a.merge(sketch_host_b) # merges in place, host A's sketch now covers both hostsp95 = sketch_host_a.get_quantile_value(0.95)แต่ละ process หรือ host เก็บ sketch ของตัวเองที่สรุป latency ที่สังเกตเห็น sketch จากหลาย host merge กันเป็น sketch เดียว และ percentile ที่ดึงจาก sketch รวมนั้นยังเป็น estimate ที่แม่นยำของ percentile จริงระดับ global — สิ่งที่วิธี average-percentile แบบตรง ๆ ให้คุณไม่ได้ ติดตั้ง package ddsketch ตัวจริงที่ open-source (reference implementation ของ Datadog เอง — sketch library ฝั่ง Go ของ Agent เอง DataDog/sketches-go implement algorithm เดียวกัน) แล้วพิสูจน์ accuracy guarantee กับ sample จริง 5,000 ตัวจาก random.gauss(100, 15) เทียบ estimate ของ sketch กับ percentile ที่แม่นยำ ที่คำนวณจากการ sort raw data
$ python -c "from ddsketch import DDSketchimport randomrandom.seed(1)data = [random.gauss(100, 15) for _ in range(5000)]s = DDSketch(relative_accuracy=0.01)for v in data: s.add(v)data_sorted = sorted(data)for p in [0.5, 0.95, 0.99]: exact = data_sorted[int(p * len(data_sorted))] est = s.get_quantile_value(p) print(f'p{int(p*100)}: exact={exact:.3f} sketch={est:.3f} rel_err={abs(est-exact)/exact:.4f}')"p50: exact=99.902 sketch=100.495 rel_err=0.0059p95: exact=124.875 sketch=125.225 rel_err=0.0028p99: exact=135.150 sketch=135.655 rel_err=0.0037relative error ทุกตัวต่ำกว่าขอบเขต relative_accuracy=0.01 (1%) ที่ตั้งไว้ — หลักฐานจริง ไม่ใช่แค่พูดซ้ำ claim ว่า percentile estimate ของ sketch ตามการกระจายจริงอยู่ในความแม่นยำที่มันสัญญาไว้ DDSketch คือ algorithm ที่ Datadog publish เอง (Masson, Rim, Lee, “DDSketch: A Fast and Fully-Mergeable Quantile Sketch with Relative-Error Guarantees”, VLDB 2019) — mergeability ที่พิสูจน์ด้านบน (sketch อิสระสองตัว รวมกันเป็นผลลัพธ์เดียว) คือคุณสมบัติที่ paper นั้นพูดถึงโดยตรง
ทำไมสิ่งนี้ไม่ถูกกระทบจาก sampling แต่การเข้าถึง example trace ถูกกระทบ
หัวข้อที่มีชื่อว่า “ทำไมสิ่งนี้ไม่ถูกกระทบจาก sampling แต่การเข้าถึง example trace ถูกกระทบ”นี่คือจุดที่ควรจำให้แม่น trace metrics และ percentile ที่อิงอยู่บน DDSketch ไม่ถูกกระทบจาก trace sampling หรือ retention filter ทั้งชั้นที่ 1 (ingestion-time sampling) และชั้นที่ 2 (APM Retention Filters) ไม่แตะ hit count, error count หรือ p50/p95/p99 latency ที่คุณเห็นบนหน้า overview ของ service เลย — ตัวเลขพวกนั้นมาจาก span stream เอง aggregate อย่างต่อเนื่อง
สิ่งที่ถูกกระทบจากทั้งสอง layer คือความสามารถในการเปิด example trace ตัวใดตัวหนึ่งโดยเฉพาะ — เช่น request ที่ช้าที่สุดในชั่วโมงที่ผ่านมา หรือ error ตัวหนึ่งของ error type ใดตัวหนึ่ง การหาและเปิด trace ตัวนั้นขึ้นกับว่ารอดจาก ingestion-time sampling และ retention filter เก็บ trace ตัวนั้นไว้หรือเปล่า
# Two different questions, two different data pathsp95 latency for checkout-web <- DDSketch, from every span reaching Datadog, unaffected by sampling"show me one slow example trace" <- must have been ingested AND retained to openนี่เป็นเหตุผลตรง ๆ ว่าทำไมคุณถึงเชื่อถือ dashboard เรื่อง latency หรือ error rate ได้ แม้จะตั้ง ingestion sampling rate ต่ำมาก ส่วน retention filter ที่ออกแบบมาดี (จากบทที่แล้ว) ยังจำเป็น เพื่อรับประกันว่าคุณจะหาและเปิด trace ที่ช้าหรือ error ตัวใดตัวหนึ่งได้จริงตอนที่ต้องการ
flowchart LR Spans[Every span reaching Agent/Datadog] --> Stats[Trace metrics: hit count, error count, latency] Stats --> Sketch[DDSketch per service/resource] Sketch --> P[p50/p95/p99 dashboards: unaffected by sampling] Spans -->|ingestion-time sampling| L1[Layer 1 gate] L1 -->|retention filters rate/trace_rate| L2[Layer 2 gate] L2 --> Kept[Individual example traces you can open]
Agent config key จริงที่ยืนยันว่า “trace metrics” ไม่ใช่แค่คำที่บทนี้ตั้งขึ้นเอง
หัวข้อที่มีชื่อว่า “Agent config key จริงที่ยืนยันว่า “trace metrics” ไม่ใช่แค่คำที่บทนี้ตั้งขึ้นเอง”pull และอ่าน datadog.yaml.example ของ image Agent 7.83.3 จริงในส่วน apm_config เจอ key ที่ doc comment ของมันเองใช้คำเดียวกับที่บทนี้พูดถึง
$ docker run --rm datadog/agent:7 sh -c \ "grep -B2 -A6 'compute_stats_by_span_kind' /etc/datadog-agent/datadog.yaml.example"# # @param compute_stats_by_span_kind - boolean - optional - default: true# # @env DD_APM_COMPUTE_STATS_BY_SPAN_KIND - boolean - optional - default: true# # Enables an additional stats computation check on spans to see they have an eligible `span.kind`# # (server, consumer, client, producer). If enabled, a span with an eligible span.kind will have# # stats computed. If disabled, only top-level and measured spans will have stats computed.compute_stats_by_span_kind: trueนี่คือ control ระดับ Agent จริง มีเอกสารกำกับ ที่คุมspan ไหนบ้างที่ป้อนเข้า stats computation ที่บทนี้อธิบาย — compute_stats_by_span_kind (default true) ขยาย stats collection ให้ span ที่มี span.kind tag ที่เข้าเกณฑ์ ไม่ใช่แค่ top-level/measured span key ที่เกี่ยวข้องกันในบล็อกเดียวกัน peer_tags_aggregation มีเอกสารระบุว่าคุมว่า “aggregated trace stats” จะรวม peer tag เป็น dimension หรือไม่ — source ของ Agent เองเรียกข้อมูลนี้ว่า trace metrics/trace stats ตรง ๆ ไม่ใช่คำที่คอร์สนี้คิดขึ้นเอง agent configcheck ยอมรับทั้งสอง key โดยไม่ error กับ container datadog/agent:7 ที่ pull มาสด ๆ จริง (session นี้ APM section ของ agent status ยืนยันตัวเลข stats ที่คำนวณสด ๆ ไม่ได้ — trace-agent process ของ container แบบ sandbox ไม่ reachable ภายในการรันสั้น ๆ ที่ชี้ไป offline เดียวกับข้อจำกัดที่บทที่แล้วบอกไว้ หลักฐานเรื่อง config ยอมรับได้ด้านบนคือสิ่งที่บทนี้พูดได้อย่างตรงไปตรงมา)
Custom metric จาก span: SpansMetricCreateRequest ที่ validate แล้ว
หัวข้อที่มีชื่อว่า “Custom metric จาก span: SpansMetricCreateRequest ที่ validate แล้ว”นอกเหนือจาก trace metrics อัตโนมัติของ hit/error/latency Datadog ให้คุณ define custom metric ที่คำนวณจาก span ที่ match query ได้ — validate กับ model v2 ของ datadog_api_client==2.61.0 ที่ติดตั้งจริง (ชื่อตรงกับที่สเปคระบุเป๊ะ ๆ ต่างจาก model retention-filter ในบทที่แล้ว)
$ python -c "from datadog_api_client.v2.model.spans_metric_create_request import SpansMetricCreateRequestpayload = {'data': {'type': 'spans_metrics', 'id': 'checkout.apply_discount.duration', 'attributes': { 'compute': {'aggregation_type': 'distribution', 'path': '@duration', 'include_percentiles': True}, 'filter': {'query': 'service:checkout-web resource_name:checkout.apply_discount'}, 'group_by': [{'path': '@http.status_code', 'tag_name': 'status_code'}],}}}print(SpansMetricCreateRequest(**payload))"validated OKการตรวจ signature จริง (inspect.signature) ยืนยันรูปร่าง: aggregation_type มีแค่ enum จริงสองค่า {'distribution', 'count'} — include_percentiles มีความหมาย (และถูกยอมรับ) แค่กับ distribution เท่านั้น นี่คือเหตุผลตรง ๆ ที่มันอยู่บน compute ไม่ใช่บน attributes ระดับบนสุด ขอ metric ที่ไม่มี compute block เลยจะ fail ทันทีด้วย error จริงที่เจาะจง
# @expect-error SpansMetricCreateAttributes.__init__() missing 1 required positional argument: 'compute'from datadog_api_client.v2.model.spans_metric_create_request import SpansMetricCreateRequest
payload = {"data": {"type": "spans_metrics", "id": "broken-metric", "attributes": {}}}SpansMetricCreateRequest(**payload)Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”🐛 หา bug ในโค้ดนี้
# AI comment: "Compare p99 latency across our two regions"from ddsketch import DDSketch
us_sketch = DDSketch(relative_accuracy=0.01)eu_sketch = DDSketch(relative_accuracy=0.01)for v in us_latencies: us_sketch.add(v)for v in eu_latencies: eu_sketch.add(v)
combined_p99 = (us_sketch.get_quantile_value(0.99) + eu_sketch.get_quantile_value(0.99)) / 2เฉลย
โค้ดนี้รันได้โดยไม่ error และให้ตัวเลขที่ดูสมเหตุสมผล ซึ่งเป็นเหตุผลที่มันอันตรายพอดี การ average ค่า p99 สองตัวที่คำนวณแยกกันไว้แล้วไม่ใช่สิ่งเดียวกับ p99 รวมจริง ๆ ของ traffic ทั้งสอง region — นี่คือกับดัก naive-average เป๊ะ ๆ ที่บทนี้เปิดเรื่องมาด้วย และ DDSketch มีไว้เพื่อไม่ให้คุณต้องทำแบบนั้นเลย ทางแก้ที่พิสูจน์แล้วด้านบนว่าให้ percentile รวมที่แม่นยำ: merge sketch เข้าด้วยกันก่อน (us_sketch.merge(eu_sketch)) แล้วค่อย เรียก get_quantile_value(0.99) ครั้งเดียวบนผลลัพธ์ที่ merge แล้ว — พิสูจน์ด้านบนว่ายังอยู่ในขอบเขต relative_accuracy ที่ตั้งไว้ของ percentile จริง ซึ่งการ average ค่า p99 สองตัวแยกกันไม่มี guarantee แบบนั้นเลย
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”DDSketch ไม่ใช่ sample ของข้อมูล — มันคือ summary ที่กระชับ ที่ทำตัวราวกับว่ายังเก็บ raw observation ทุกตัวไว้ เพื่อจุดประสงค์เรื่อง percentile นั่นคือคุณสมบัติที่พิสูจน์ด้านบน: sort ตัวเลขจริง 5,000 ตัวแล้วอ่านค่าตัวที่ 99 เปอร์เซ็นไทล์ ให้คำตอบใกล้เคียงกับการถาม sketch มาก ทั้งที่ sketch ไม่เคยเก็บตัวเลข 5,000 ตัวนั้นไว้เลย มีแค่ summary แบบ bucket ของมัน การ merge sketch สองตัวไม่ใช่การ average summary สองตัว — ใกล้เคียงกับการรวม underlying data ก่อนแล้วค่อย summarize ครั้งเดียวมากกว่า — นี่คือเหตุผลตรง ๆ ที่ sketch ที่ merge แล้วยัง percentile แม่นยำ ในขณะที่ average ของ percentile ที่คำนวณแยกกันไม่แม่นยำ
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”- Trace metrics (คำนวณเสมอ) เทียบกับ example trace (ขึ้นกับ sampling) — พิสูจน์ตลอดทั้งบทนี้และบทที่แล้ว: dashboard ที่สร้างจาก trace metrics เชื่อถือได้ที่ sampling rate ไหนก็ได้ แต่การ debug request ที่ช้าตัวใดตัวหนึ่งโดยเฉพาะยังต้องการ trace ตัวนั้นที่รอดทั้ง ingestion sampling และ retention filter — คุณไม่มีทางได้ทั้ง “ingestion ถูก” กับ “รับประกันเข้าถึง example trace ได้” ฟรี ๆ พร้อมกัน
- การตั้งค่า
relative_accuracyของ DDSketch — accuracy ที่แน่นกว่า (พิสูจน์ด้านบนที่0.01ทั้งสาม percentile อยู่ต่ำกว่า 1% relative error มาก) ใช้ memory ต่อ sketch มากกว่า setting ที่หลวมกว่าถูกกว่า แต่ guarantee เรื่อง merge-แล้ว-percentile เดียวกันยังคงอยู่ที่ accuracy ไหนก็ตามที่คุณตั้ง — ไม่มีของฟรี มีแค่ trade ที่ปรับได้ระหว่างความแม่นยำกับพื้นที่ใช้งาน - Trace metrics อัตโนมัติเทียบกับ custom metric แบบ
SpansMetricCreateRequest— hit/error/latency metric อัตโนมัติไม่ต้องตั้งค่าอะไรเลยและครอบคลุมทุก service เป็น default ส่วน custom metric จาก span (พิสูจน์ด้านบน model จริง) ต้องมี query กับcomputeblock ที่คุณดูแลเอง แต่วัดสิ่งที่ metric อัตโนมัติจับไม่ได้ เช่น latency distribution ที่ tag เจาะจงของ resource หนึ่ง
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “average percentile สองตัวจากสอง host แล้วได้ percentile รวมที่ถูกต้อง” ไม่ใช่ — พิสูจน์ด้านบนด้วยตัวอย่าง SpotTheBug: นี่คือกับดักที่ DDSketch มีไว้เพื่อหลีกเลี่ยงเป๊ะ ๆ ต้อง merge sketch ที่เป็นต้นตอก่อนแล้วค่อยคำนวณ percentile ครั้งเดียวเท่านั้นถึงจะได้คำตอบรวมที่แม่นยำ
- “trace metrics สะท้อนแค่ subset ที่ถูก sample เข้ามาใน Datadog” ไม่ใช่ — พิสูจน์ด้านบนผ่าน Agent config จริง: stats computation (
compute_stats_by_span_kindและเพื่อน ๆ) ทำงานจาก span stream ที่มาถึง Agent โดยไม่ขึ้นกับ layer ingestion-sampling และ retention-filter จากบทที่แล้ว - “DDSketch ให้ percentile ที่แน่นอน ไม่ใช่ estimate” ไม่ใช่ — พิสูจน์ด้านบน: estimate ของ sketch ต่างจาก percentile จริงที่คำนวณจากการ sort ข้อมูลอยู่นิดหน่อยแต่ไม่ใช่ศูนย์ (0.28%–0.59% ใน run ของบทนี้เอง) — มันคือ estimate ที่มี error bound แม่นยำในขอบเขต
relative_accuracyที่ตั้งไว้ ไม่ใช่การคำนวณที่แน่นอน