ข้ามไปยังเนื้อหา

Continuous Profiler และ Error Tracking

tracing บอกคุณว่าตรงไหนที่ช้าหรือ fail — service ไหน, span ไหน — ส่วน Continuous Profiler กับ Error Tracking บอกคุณว่าทำไม ลงลึกถึงฟังก์ชันที่กิน CPU หรือ root cause ที่ถูก dedup มาจากกอง stack trace ที่ซ้ำกัน

Continuous Profiler รันตลอดเวลาบน production ด้วย overhead ต่ำ เก็บ CPU time, wall-time และ allocation ระดับโค้ด — ฟังก์ชันและ call stack แต่ละตัว ไม่ใช่แค่ aggregate ระดับ service เพราะ always-on แทนที่จะเป็นอะไรที่คุณเปิดทีหลังตอน investigate incident ข้อมูลจึงพร้อมอยู่แล้วตั้งแต่ก่อนคุณต้องการด้วยซ้ำ

profiler ผูก sample ของตัวเองกลับไปหา service, endpoint และแม้แต่ trace ที่เกิดขึ้นด้วย ความเชื่อมโยงนี้เองที่ทำให้ profiler มีประโยชน์ตอน debug request ที่ช้า — แทนที่จะเดาว่าส่วนไหนของ checkout-web ที่แพง คุณกระโดดจาก trace ที่ช้าตรงเข้าไปหา flame graph ว่าฟังก์ชันไหนกำลังรันอยู่และนานแค่ไหนระหว่าง request นั้น

app/profile-start.py
# ddtrace's profiler starts alongside the tracer — no separate instrumentation needed
from ddtrace.profiling import Profiler
prof = Profiler()
prof.start()

พอทั้ง tracer และ profiler รันพร้อมกัน span ที่ช้าใน trace หนึ่งจะพาคุณตรงไปที่ flame graph ระดับฟังก์ชันของช่วงเวลานั้นพอดี — tracing จำกัดให้เหลือ span ไหน profiler จำกัดให้เหลือ บรรทัดโค้ดไหน

พิสูจน์ว่า env var ของ profiler ถูกอ่านจริง — และ profile ในเครื่องจริง ไม่ต้องมี Agent

หัวข้อที่มีชื่อว่า “พิสูจน์ว่า env var ของ profiler ถูกอ่านจริง — และ profile ในเครื่องจริง ไม่ต้องมี Agent”

DD_PROFILING_ENABLED เป็น option จริงมีเอกสารรองรับในทั้งสอง client ที่ pin ไว้ — .d.ts ของ dd-trace ประกาศ profiling?: boolean พร้อม @env DD_PROFILING_ENABLED และ supported-configurations.json ของ package ที่ติดตั้งจริงเอง list ทั้งตระกูล: DD_PROFILING_CPU_ENABLED, DD_PROFILING_ALLOCATION_ENABLED, DD_PROFILING_HEAP_ENABLED, DD_PROFILING_CODEHOTSPOTS_ENABLED, DD_PROFILING_TIMELINE_ENABLED, DD_PROFILING_UPLOAD_PERIOD และตัวที่น่าทดสอบตรง ๆ ที่สุด — DD_PROFILING_EXPORTERS (default agent)

profiler ต้องมี live Agent connection ถึงจะพิสูจน์ได้ว่ากำลังเก็บข้อมูลจริงไหม ไม่ต้อง — DD_PROFILING_EXPORTERS=file คือ exporter implementation จริงตัวที่สอง (profiling/exporters/file.js คู่กับ exporters/agent.js ใน package ที่ติดตั้งจริง) และมันเขียน profile ลงดิสก์ในเครื่องตรง ๆ รันฟังก์ชันที่กิน CPU ภายใต้มัน ไม่มี Agent ไม่มี network เลย

src/profile-test.js
const tracer = require('dd-trace').init({ service: 'checkout-web', profiling: true, logInjection: false });
function busy() {
let x = 0;
const end = Date.now() + 3000;
while (Date.now() < end) x += Math.sqrt(x + 1);
return x;
}
busy();
setTimeout(() => process.exit(0), 1000);
$ DD_PROFILING_ENABLED=true DD_PROFILING_EXPORTERS=file \
DD_PROFILING_PPROF_PREFIX=./pprof-out/ DD_PROFILING_UPLOAD_PERIOD=2 \
node src/profile-test.js
$ ls pprof-out/
events_worker_0_20260929T135419Z.pprof
space_worker_0_20260929T135419Z.pprof
wall_worker_0_20260929T135419Z.pprof

profile file จริง อยู่บนดิสก์ ไม่มี account หรือ Agent ของ Datadog เข้ามาเกี่ยวข้องเลย format .pprof คือ protobuf ที่ compress ด้วย zstd — decompress wall-time profile แล้ว grep raw byte หาชื่อฟังก์ชันของ script นี้เองยืนยันว่ามันจับ sample ระดับโค้ดจริง ไม่ใช่ placeholder data

$ zstd -d pprof-out/wall_worker_0_20260929T135419Z.pprof -o wall.decoded && strings wall.decoded | grep busy
(anonymous:L#1711:C#37)
/private/tmp/.../src/profile-test.js
busy

busy — ฟังก์ชันของบทนี้เอง — อยู่ในนั้นจริง ๆ ตรงกับ claim ที่ว่า profiler จับ “individual functions and call stacks ไม่ใช่แค่ service-level aggregate” เป๊ะ

ฝั่ง Python ทำงานแบบเดียวกัน แต่ config accessor ไม่ใช่ ddtrace.config.profiling — มันโยน AttributeError จริงใน ddtrace==4.15.2 ที่ pin ไว้

$ python -c "from ddtrace import config; print(config.profiling)"
AttributeError: <class '...Config'> object has no attribute profiling, profiling is not a valid configuration

accessor จริงอยู่ใน module แยกต่างหาก และ env var จริงสำหรับ export ไฟล์ในเครื่องคือ DD_PROFILING_OUTPUT_PPROF (ไม่ใช่ DD_PROFILING_EXPORTERS=file — อันนั้นเฉพาะ Node)

$ DD_PROFILING_ENABLED=true python -c \
"from ddtrace.internal.settings.profiling import config as pconfig; print(pconfig.enabled, pconfig.output_pprof)"
True None
$ DD_PROFILING_ENABLED=true DD_PROFILING_OUTPUT_PPROF=./pprof-out-py/profile \
python app/profile_test.py
$ ls pprof-out-py/
profile.<pid>.1.info.json
profile.<pid>.1.internal_metadata.json
profile.<pid>.1.pprof

info.json เองก็เป็นหลักฐานว่า env var ถูกอ่านจริง — มันคือ settings dump เต็มรูปแบบที่ profiler เขียนออกมา และ output_pprof ในนั้นอ่านค่ากลับมาตรงกับ path ที่ตั้งไว้เป๊ะ

$ python -m json.tool pprof-out-py/profile.*.1.info.json | grep output_pprof
"output_pprof": "./pprof-out-py/profile"

Error Tracking เอา error event ที่มาจาก trace และ log แล้ว group กับ dedup error ที่คล้ายกันให้เป็น issue เดียวอัตโนมัติ แทนที่จะปล่อยให้คุณต้องมาสังเกตเองว่า NullPointerException ตัวเดิมเกิดขึ้นไปแล้ว 482 ครั้งตลอดหนึ่งสัปดาห์ของ trace แต่ละ issue มี lifecycle ที่คุณจัดการได้ชัดเจน — ปกติคือ new, triaged และ resolved — error ที่เกิดซ้ำ ๆ จึงกลายเป็นสิ่งเดียวที่ทีมของคุณ track จนปิดได้ แทนที่จะเป็น event หลายร้อยตัวที่ดูเหมือนไม่เกี่ยวกันเลย

{
"issue": {
"fingerprint": "[email protected]_discount",
"status": "new",
"occurrences": 482,
"first_seen": "2026-07-01T10:03:00Z",
"last_seen": "2026-07-08T09:12:00Z",
"example_trace_ids": ["6141ee2f9a4d4b2e8f3c1a90", "9af23cd1b8e7f4a6c2d5e901"]
}
}

สังเกต field example_trace_ids — Error Tracking เก็บ link กลับไปหา example trace จริงของ issue นั้นไว้ด้วย คุณจึงกระโดดจาก “issue นี้เกิดไปแล้ว 482 ครั้ง” ตรงเข้าไปหา occurrence จริงหนึ่งตัวเพื่อดู context ของ request รอบ ๆ ได้

สองเครื่องมือนี้รวมกันแล้วปิดช่องว่างที่ tracing เพียงอย่างเดียวทิ้งไว้

  • Tracing — service ไหนและ span ไหนที่ช้าหรือ fail
  • Continuous Profiler — ฟังก์ชันไหนระดับโค้ดที่กิน CPU/wall-time/allocation ระหว่าง span ที่ช้านั้น
  • Error Tracking — span ที่ fail นั้นอยู่ใน issue ที่ dedup แล้วอันไหน และ issue นั้นอยู่ที่จุดไหนใน lifecycle new/triaged/resolved

ไม่มีเครื่องมือไหนแทนที่ tracing ทั้งคู่เริ่มจาก trace หรือ span แล้วลงลึกไปอีกระดับกว่าที่ “service ไหน” จะพาไปถึงได้

flowchart LR
  Trace[Slow or failing trace] --> Where[Tracing: which service, which span]
  Where --> Profiler[Continuous Profiler: which function, CPU/wall-time/allocations]
  Where --> ErrorTracking[Error Tracking: deduplicated issue, new/triaged/resolved]
  Profiler --> WhySlow[Why it is slow]
  ErrorTracking --> WhyFailing[Why it is failing]
จากตรงไหนสู่ทำไม

🐛 หา bug ในโค้ดนี้

app/profiling_setup.py
# AI comment: "Enable profiling and route it through the tracer's own config"
from ddtrace import config, tracer
config.profiling = True
def handle_request():
with tracer.trace("checkout.handle_request"):
...
เฉลย

ดูเหมือนเป็นการต่อยอด pattern เดียวกับที่ใช้ตั้งค่า tracer ทั่วทั้งคอร์สนี้ — ตั้ง attribute บน ddtrace.config จบ มันโยน AttributeError จริงทันทีที่ module นี้ถูก import พิสูจน์ด้านบน: config.profiling ไม่มีอยู่ใน ddtrace==4.15.2 ที่ pin ไว้ profiling ใน client ตัวนี้ไม่ใช่ attribute แบบ config.<x> เลย มันคือ object Profiler แยกต่างหาก (from ddtrace.profiling import Profiler; Profiler().start()) ที่เริ่มแยกจาก tracer ตั้งค่าผ่าน env var ของ module ตัวเอง (DD_PROFILING_ENABLED, DD_PROFILING_OUTPUT_PPROF และเพื่อน ๆ) ไม่ใช่ผ่าน ddtrace.config

tracing กับ profiling ต่าง sample process เดียวกันที่กำลังรันอยู่ แต่คนละความละเอียดและคนละตารางเวลา span คือการวัดแบบตั้งใจ — มีอะไรบางอย่างในโค้ดพูดตรง ๆ ว่า “จับเวลา operation นี้ ติด tag ชื่อนี้” ส่วน profiler sample เป็นแบบไม่ตั้งใจ — พิสูจน์ด้านบน มันเดิน call stack จริงของ while loop ธรรมดาที่ไม่เคยสร้าง span แม้แต่ตัวเดียว แล้วยังจับ busy ได้ตามชื่อ นี่คือเหตุผลที่สองอย่างนี้เข้ากันได้ดีมาก tracing บอกคุณว่าเมื่อไหร่ที่มีอะไรช้า (span ที่มี duration ยาว) ส่วน profiler — sample ต่อเนื่องไม่ว่าจะมีอะไรเรียก tracer.trace() หรือไม่ — มีโอกาสสูงมากที่จะจับสิ่งที่ CPU กำลังทำอยู่ในช่วงเวลานั้นไว้แล้ว ไม่ว่าโค้ดข้างในจะคิดจะวัดตัวเองหรือเปล่าก็ตาม

  • Agent exporter เทียบกับ file exporter — พิสูจน์ด้านบน file exporter ไม่ต้องมี infrastructure เลยและเหมาะกับการ investigate ในเครื่อง หรืออย่างที่บทนี้ใช้ เพื่อพิสูจน์ output ของ profiler เองโดยไม่ต้องมี account ส่วน Agent exporter คือสิ่งที่ deployment จริงใช้ batch และส่ง profile ต่อเนื่องให้โผล่คู่กับ trace ใน UI
  • Always-on profiling เทียบกับ on-demand profiling — always-on (ท่าเริ่มต้นของ profiler) แปลว่าข้อมูลมีอยู่แล้วตั้งแต่ก่อนคุณต้องการ แลกกับ overhead พื้นหลังต่ำ ๆ ตลอดเวลา ส่วน profiler ที่เปิดแค่ตอน incident ไม่มีต้นทุนตอน steady-state เลย แต่บอกคุณไม่ได้ว่าเกิดอะไรขึ้นก่อนที่คุณจะสังเกตเห็นปัญหา
  • การ group อัตโนมัติของ Error Tracking เทียบกับ triage ด้วยมือ — dedup 482 occurrence ให้เหลือ issue เดียว (จากตัวอย่าง JSON ด้านบน) ประหยัดความสนใจได้จริง แต่ logic การ group บางครั้งอาจรวม error ที่จริง ๆ แยกกัน หรือแยก error ที่จริง ๆ root cause เดียวกันแค่ stack ต่างกันนิดหน่อย — ควรเช็ค example_trace_ids ของ issue เป็นครั้งคราวแทนที่จะเชื่อตัวเลขเฉย ๆ
  • “Continuous Profiler ต้องมี live Agent connection ถึงจะพิสูจน์ได้ว่าทำงาน” ไม่ใช่ — พิสูจน์ด้านบน: DD_PROFILING_EXPORTERS=file (Node) และ DD_PROFILING_OUTPUT_PPROF (Python) ทั้งคู่เขียนไฟล์ .pprof จริงที่ inspect ได้ลงดิสก์ในเครื่องตรง ๆ ไม่มี Agent หรือ account เข้ามาเกี่ยวข้องเลย
  • “ddtrace.config.profiling คือวิธีเช็คหรือตั้ง profiling state ใน Python” ไม่ใช่ — พิสูจน์ด้านบนด้วย AttributeError จริง: profiling อยู่ใน object ddtrace.profiling.Profiler ของตัวเองและ settings module ของตัวเอง ไม่ได้อยู่บน namespace ddtrace.config ที่ใช้ร่วมกัน
  • “profiler sample มีอยู่แค่กับโค้ดที่ห่อด้วย span เท่านั้น” ไม่ใช่ — พิสูจน์ด้านบน: while loop ธรรมดาที่ไม่มี tracer call เลยยังโผล่ตามชื่อฟังก์ชัน (busy) ใน wall-time profile จริงที่จับได้
Continuous Profiler เพิ่มอะไรเข้ามาเหนือกว่าที่ tracing แสดงให้เห็นอยู่แล้ว
Error Tracking ทำอะไรกับ error ตัวเดิม 482 occurrence ที่กระจายอยู่ในหลาย trace
ทำไม Continuous Profiler ถึงถูกเรียกว่า "always-on" แทนที่จะเป็นอะไรที่เปิดใช้ตอน incident
สรุปเป็นประโยคเดียว tracing กับสองเครื่องมือนี้แบ่งหน้าที่กันยังไง
การพิสูจน์ว่า Continuous Profiler ทำงานจริง ต้องมี Agent connection จริงไหม
โค้ด Python ที่ AI generate เขียน config.profiling = True (import config จาก ddtrace) เพื่อเปิด profiling เกิดอะไรขึ้นจริง