Alert: latency P99 tăng gấp 3. Dashboard không đủ context, SSH vào production attach strace — overhead CPU tăng thêm 15%. Thấy process nghi ngờ, chạy kill -9 nhầm PID — service chết. Từ “latency cao” thành “service down hoàn toàn”.
Debugging production là kỹ năng cần thiết nhưng cũng là hoạt động nguy hiểm nhất. Áp lực incident khiến engineer hành động nhanh thay vì hành động an toàn — và đó chính xác là lúc sai lầm phá huỷ nhất xảy ra. Bài này đi qua nguyên tắc và kỹ thuật debug production mà không gây thêm sự cố.
Quan sát trước, không chạm vào
Phản xạ đầu tiên khi thấy lỗi production thường là “phải vào xem ngay”. Nhưng phản xạ đúng là quan sát bằng công cụ có sẵn trước khi chạm vào bất kỳ thứ gì. Hệ thống observability tốt cho phép debug phần lớn vấn đề mà không cần SSH, không cần thêm log, không cần redeploy.
Bắt đầu từ metrics: dashboard RED (Rate, Errors, Duration) cho biết service nào đang có vấn đề, error rate bao nhiêu, latency tăng từ lúc nào. Metrics trả lời câu “có vấn đề không” và “vấn đề bắt đầu khi nào” — hai câu hỏi quan trọng nhất trong 5 phút đầu incident. Nếu error rate tăng đúng lúc deploy mới, rollback trước rồi debug sau — không cần hiểu root cause để hành động đúng. Đây là nguyên tắc mitigate first: giảm thiệt hại cho user trước, tìm nguyên nhân gốc sau.
Tiếp theo là traces: tìm request lỗi hoặc chậm trên Jaeger/Tempo, xem waterfall view để biết thời gian nằm ở span nào. Trace cho biết “vấn đề nằm ở đâu trong chuỗi service” — service A gọi service B mất 50ms nhưng service B gọi Redis mất 3 giây, vậy vấn đề ở Redis chứ không phải code service B.
Cuối cùng là logs: filter theo trace_id từ trace vừa tìm được, đọc log chi tiết của request đó trên mọi service. Log cho biết “cụ thể chuyện gì xảy ra” — error message, stack trace, giá trị biến, query SQL thực thi.
Ba bước metrics → traces → logs này giải quyết được 70-80% vấn đề production mà không cần chạm vào server. Điều kiện tiên quyết là hệ thống observability phải được setup đúng từ trước — đây không phải thứ có thể xây khi đang có incident.
Một sai lầm phổ biến trong bước đầu tiên: nhìn metric thấy bất thường rồi nhảy ngay vào code hoặc server mà bỏ qua câu hỏi “có gì thay đổi gần đây không?”. Correlation với timeline deploy, config change, infrastructure event giải quyết rất nhiều incident mà không cần hiểu code. Error rate tăng gấp đôi đúng lúc deploy version 2.3.1 — rollback về 2.3.0 trước, đọc changelog sau. Latency tăng lúc 14:00 — check xem có cron job nào chạy lúc 14:00 không. Đây là tư duy correlation trước causation — nhanh hơn và an toàn hơn việc đào sâu vào code.
Annotation trên dashboard cũng hỗ trợ lớn cho bước này. Grafana annotation tự động đánh dấu thời điểm deploy, config change, autoscale event lên biểu đồ metric. Nhìn đường error rate vọ lên đúng lúc có annotation “deploy v2.3.1” thì root cause gần như chắc chắn — không cần SSH, không cần đọc code, rollback ngay. Setup annotation tự động từ CI/CD pipeline là đầu tư nhỏ nhưng giá trị lớn.
Structured logging trả lãi lúc debug
Khoản đầu tư vào structured logging trả lãi rõ nhất khi debug production. Log dạng JSON có trường rõ ràng cho phép filter và aggregate mà không cần sửa code, không cần redeploy, không cần chờ lỗi xảy ra lần nữa.
Khi mọi log line chứa trace_id, việc tìm toàn bộ context của một request lỗi chỉ là một query: {trace_id="abc123"} trên Loki hoặc Elastic. Không cần grep nhiều file, không cần ghép timestamp thủ công giữa các service. Thêm user_id vào log thì có thể filter “tất cả request của user X trong 30 phút qua” — hữu ích khi user report lỗi nhưng không biết lỗi ở đâu.
Các trường nên có trong mọi log line: trace_id, span_id, service, route, method, user_id (nếu có authentication), elapsed_ms. Với error log thêm err (message), stack (stack trace), và context cụ thể của domain — order_id, payment_method, region. Những trường này setup một lần qua logging middleware, developer không cần nhớ log thủ công mỗi lần viết code.
Một pattern hữu ích là request context logging: middleware đầu vào gắn tất cả metadata vào context, mọi log call trong request tự động kèm metadata đó. Go có context.Context, Node.js có AsyncLocalStorage, Python có contextvars. Đầu tư vài giờ setup pattern này, tiết kiệm hàng trăm giờ debug sau này.
Điều cần tránh: log PII trần (email, số điện thoại, token) dù đang debug. Log user_id rồi tra cứu PII qua internal tool có audit trail — không phải in PII ra log rồi log đó nằm trên Elastic 90 ngày cho ai cũng query được.
Một chi tiết kỹ thuật quan trọng: log sampling cho debug. Khi bật DEBUG cho tất cả request, volume log tăng 10-50 lần so với INFO. Với service QPS cao (>1000), DEBUG toàn bộ có thể áp đảo log pipeline. Thay vào đó, sample DEBUG: log DEBUG cho 1 trong 100 request (hoặc chỉ cho request match điều kiện), giữ được chi tiết mà không gây áp lực quá lớn. 100% ERROR log vẫn được giữ nguyên — lỗi thì không được bỏ qua.
Dynamic log level — thay đổi verbosity không cần redeploy
Khi log INFO không đủ chi tiết để debug, phản xạ thường thấy là thêm console.log, commit, deploy lên production. Đây là cách nguy hiểm nhất: mỗi deploy có rủi ro riêng (config sai, dependency mới, migration chưa chạy), và deploy chỉ để thêm log là đánh đổi rủi ro deploy lấy vài dòng log.
Cách an toàn hơn nhiều là dynamic log level: thay đổi verbosity tại runtime qua config reload hoặc feature flag, không cần restart hay redeploy.
Cách đơn giản nhất là expose endpoint admin để thay đổi log level — nhiều framework hỗ trợ sẵn. Spring Boot có /actuator/loggers, Go có thể dùng zap.AtomicLevel với HTTP handler, Node.js với pino có thể thay level qua signal hoặc API. Gọi PUT /admin/log-level {"level": "DEBUG"} rồi request tiếp theo sẽ log chi tiết hơn. Debug xong thì set lại INFO.
Cách tinh vi hơn là dùng feature flag để bật DEBUG cho một user cụ thể hoặc một request cụ thể mà không ảnh hưởng toàn bộ traffic. Ví dụ: flag debug_verbose_logging evaluate theo user_id — chỉ user đang report lỗi mới sinh DEBUG log, traffic còn lại vẫn INFO bình thường. Điều này tránh tình trạng bật DEBUG toàn cluster rồi log nổ, storage nổ, bill tăng gấp 5 lần mà tín hiệu tốt chìm trong biển nhiễu.
Quan trọng: phải có cơ chế tự động tắt debug level sau thời gian nhất định — TTL 30 phút hoặc 1 giờ. Quên tắt DEBUG trên production là sai lầm phổ biến và đắt đỏ. Có team bật DEBUG để tìm bug, fix xong quên tắt, hai ngày sau bill observability tăng gấp 3 lần.
Một pattern nâng cao hơn là conditional debug logging: log DEBUG chỉ khi request match một điều kiện cụ thể, được định nghĩa bằng expression tại runtime. Ví dụ: “log DEBUG cho mọi request có order_total > 10000 và payment_method = 'crypto'”. Một số logging framework hỗ trợ dynamic filter expression — hoặc có thể tự implement bằng cách check điều kiện trong middleware trước khi set log level cho request context. Pattern này cực kỳ hữu ích khi bug chỉ xảy ra với một tổ hợp điều kiện cụ thể mà không biết trước user_id nào sẽ trigger.
Reproduce ở staging trước
Trước khi chạm vào production, luôn thử reproduce vấn đề ở staging. Reproduce thành công ở staging nghĩa là có thể debug thoải mái: attach debugger, thêm log, thay đổi code, chạy đi chạy lại — không ảnh hưởng user nào.
Staging reproduce được khi hai điều kiện thoả mãn: data đủ giống production, và traffic pattern đủ giống production.
Về data, staging cần production-like data — không phải data production thật (vì PII), mà là data có cùng distribution, cùng edge case. Anonymize production data rồi import vào staging, hoặc sinh synthetic data có cùng đặc tính (số lượng record, tỷ lệ null, distribution giá trị). Bug do data edge case (field null khi code assume non-null, unicode character trong tên, số âm trong amount) sẽ không reproduce được nếu staging chỉ có 10 record sạch sẽ.
Về traffic, có thể dùng công cụ replay: GoReplay (gor) capture HTTP traffic từ production rồi replay lên staging — giữ nguyên timing, header, body. tcpreplay làm tương tự ở tầng TCP. Replay traffic production lên staging giúp tái hiện race condition, concurrency bug, và load pattern mà synthetic test không tạo được.
Một cách khác đơn giản hơn là dùng production log làm test case: extract request parameter từ log (route, body, header) rồi tạo automated test tái hiện lại. Đây không phải replay full traffic mà là tạo targeted test cho request cụ thể đang lỗi — nhanh hơn và dễ kiểm soát hơn. Nếu structured log chứa đủ thông tin (route, method, body hash, query params), việc tạo test case từ log có thể tự động hoá.
Lưu ý khi replay traffic: phải sanitize request chứa auth token, PII, hoặc idempotency key — replay request payment thật lên staging có thể gây charge thật nếu staging kết nối production payment gateway. Luôn verify staging không kết nối bất kỳ production service nào trước khi replay.
Một kỹ thuật bổ sung là shadow traffic: fork traffic production sang staging instance song song mà không ảnh hưởng response trả cho user. Istio và Envoy hỗ trợ traffic mirroring native — request đến production được copy sang staging, staging xử lý và trả response nhưng response đó bị discard. So sánh log/metric giữa hai bên phát hiện được regression mà synthetic test bỏ sót. Tuy nhiên shadow traffic cần staging có capacity đủ để xử lý full production load, và phải đảm bảo side effect (ghi database, gọi external API) không xảy ra trên staging.
Khi staging không reproduce được
Có những bug chỉ xảy ra trên production — do scale, do data cụ thể, do timing giữa các service, do hardware khác biệt, do config production khác staging theo cách không ai nhớ. Race condition chỉ lộ khi có hàng nghìn concurrent request, memory leak chỉ lộ sau vài ngày chạy liên tục, timeout chỉ xảy ra khi DNS resolver của production chậm hơn staging 50ms — những điều kiện này rất khó mô phỏng. Đây là lúc cần kỹ thuật debug an toàn trên production.
Query log và trace có sẵn
Bước đầu tiên vẫn là khai thác dữ liệu đã có. Filter log theo điều kiện cụ thể: {service="order-svc", level="ERROR", route="/api/checkout"} trong 1 giờ qua. Tìm pattern: lỗi có tập trung ở region nào, user segment nào, thời điểm nào không. Correlation với deploy event hoặc config change. Nhiều khi root cause lộ ra chỉ từ việc đọc log kỹ — một error message kèm stack trace đủ để hiểu vấn đề mà không cần thêm instrumentation.
Một kỹ thuật mà nhiều team bỏ qua: so sánh log pattern giữa lúc bình thường và lúc có vấn đề. Export log của 1 giờ trước incident và 1 giờ trong incident, so sánh error message, log frequency, và các trường metadata. Sự khác biệt đáng chú ý — error mới xuất hiện, warning tăng đột biến, một service bất ngờ log nhiều hơn bình thường — thường là manh mối quan trọng.
Thêm metric tạm thời
Khi log không đủ, thêm counter metric cho code path cụ thể đang nghi ngờ. Ví dụ, nghi ngờ rằng cache miss rate cao bất thường cho một loại query — thêm counter cache_miss_total{query_type="user_profile"}. Deploy metric mới nhẹ hơn nhiều so với thêm log line: metric chỉ là increment số, không sinh data mỗi request như log, overhead gần như bằng không.
Counter metric có thể deploy qua code change nhỏ hoặc qua instrumentation library inject runtime (Java agent, Python monkey-patch). Sau khi debug xong, giữ metric lại nếu nó hữu ích lâu dài, hoặc xoá nếu chỉ phục vụ debug.
Lưu ý về cardinality khi thêm metric tạm: label phải có tập giá trị hữu hạn. Thêm counter error_by_user{user_id="..."} với triệu user là triệu time series — Prometheus sẽ chết trước khi bạn tìm được bug. Thêm error_by_region{region="..."} với 5 region thì OK. Quy tắc đã nói nhiều lần nhưng vẫn bị vi phạm: per-user data thuộc về log, không phải metric.
tcpdump và strace — readonly nhưng cần cẩn thận
tcpdump capture packet trên network interface — hữu ích để xem traffic thực tế giữa service và downstream dependency (database, cache, external API). Chạy tcpdump -i eth0 port 5432 -w /tmp/capture.pcap -c 1000 capture 1000 packet trên port Postgres, download file pcap về local rồi phân tích bằng Wireshark. tcpdump ở chế độ capture ra file có overhead rất thấp, an toàn cho production.
strace trace syscall của process — hữu ích để xem process đang block ở syscall nào (read, write, futex, epoll_wait). Nhưng strace có overhead đáng kể: mỗi syscall bị intercept thêm context switch, có thể tăng latency 20-50% tuỳ workload. Trên production, chỉ dùng strace khi đã cạn kiệt phương án khác, và luôn giới hạn: -p PID -e trace=network -c (chỉ trace network syscall, chỉ hiển thị summary count thay vì mỗi call). Tuyệt đối không strace toàn bộ thread của process production mà không giới hạn scope.
Continuous profiling
Profiling truyền thống yêu cầu attach profiler vào process, gây overhead lớn, không phù hợp production. Continuous profiling giải quyết vấn đề này bằng cách sample stack trace ở tần số thấp (thường 100 Hz), overhead dưới 1-2% CPU, chạy liên tục 24/7 trên production.
Công cụ phổ biến: Pyroscope (open-source, hỗ trợ nhiều ngôn ngữ), pprof endpoint (Go native), async-profiler (Java). Go có lợi thế lớn ở đây: net/http/pprof expose endpoint profiling sẵn, chỉ cần import package và gọi /debug/pprof/profile khi cần CPU profile, /debug/pprof/heap cho memory — không cần restart, không cần redeploy.
Continuous profiling đặc biệt hữu ích cho vấn đề performance: CPU hotspot, memory leak, goroutine leak. Thay vì đoán “function nào chậm”, flame graph chỉ ra chính xác function nào chiếm nhiều CPU nhất, gọi từ đâu, bao nhiêu phần trăm thời gian. Đây là dữ liệu khách quan thay vì cảm tính “code này trông chậm”.
Một lợi thế lớn của continuous profiling: so sánh flame graph giữa hai thời điểm — trước và sau khi vấn đề xuất hiện. Diff flame graph chỉ ra chính xác function nào tăng CPU consumption, giúp thu hẹp phạm vi tìm kiếm từ toàn bộ codebase xuống vài function cụ thể. Pyroscope và Datadog Continuous Profiler đều hỗ trợ diff view này.
Feature flag cho verbose logging theo request
Kết hợp feature flag với logging để bật verbose mode cho một subset traffic cực nhỏ: chỉ request từ một user cụ thể, hoặc request có header X-Debug: true từ internal tool. Middleware check flag, nếu match thì set log level DEBUG cho request đó — log chi tiết mọi thứ: input, output, intermediate value, query plan, cache hit/miss. Request khác vẫn chạy bình thường ở INFO.
Pattern này an toàn vì blast radius cực nhỏ — chỉ 1 user hoặc 1 request sinh thêm log, không ảnh hưởng throughput hay storage đáng kể. Và nó cho context chi tiết hơn nhiều so với log INFO thông thường, thường đủ để tìm root cause.
Core dump và heap dump
Với vấn đề memory (leak, OOM kill, high RSS), heap dump là công cụ chẩn đoán chính xác nhất. Java có jmap -dump:format=b,file=heap.hprof <pid>, Node.js có --heapsnapshot-signal=SIGUSR2, Go có /debug/pprof/heap. Heap dump trên production cần cẩn thận: process có thể bị pause trong lúc dump (Java GC stop-the-world để dump), file dump có thể lớn hàng GB tốn disk, và file chứa tất cả dữ liệu trong memory bao gồm cả PII — phải xử lý như dữ liệu nhạy cảm.
Tốt nhất là dump trên instance đã được drain khỏi load balancer — không nhận traffic mới, dump xong rồi terminate instance. Download file dump về local hoặc staging để phân tích bằng Eclipse MAT (Java), Chrome DevTools (Node.js), hoặc go tool pprof. Không phân tích heap dump trực tiếp trên production server — công cụ phân tích tốn CPU và memory đáng kể.
Canary debugging
Khi cần instrumentation nặng hơn (thêm log, thêm metric, thay đổi code path) mà không muốn ảnh hưởng toàn bộ traffic, có thể dùng canary debugging: route một phần traffic nhỏ (1-5%) hoặc traffic test nội bộ sang instance đã được instrument đặc biệt.
Cách triển khai phổ biến: deploy instance canary với code có thêm instrumentation, configure load balancer hoặc service mesh (Istio, Linkerd) route traffic theo header hoặc percentage. Internal tester gửi request với header đặc biệt (X-Canary: debug), request đó đi vào canary instance, log và trace chi tiết hơn bình thường. Traffic user thật vẫn đi vào instance bình thường không bị ảnh hưởng.
Canary debugging đặc biệt hữu ích khi vấn đề liên quan đến performance: thêm profiling, thêm timing log ở mọi function call — overhead lớn nhưng chỉ ảnh hưởng canary instance, production chính vẫn chạy bình thường. Sau khi thu thập đủ data, tắt canary instance, phân tích offline.
Một biến thể là debug pod/container: trong môi trường Kubernetes, deploy một pod mới cùng namespace với pod production, mount cùng config và secret, nhưng chạy image có thêm debug tool (strace, tcpdump, profiler, debug symbol). Pod này có thể truy cập cùng network và service mesh, nhưng không nhận traffic từ Service/Ingress — chỉ dùng để chạy diagnostic command. kubectl debug từ Kubernetes 1.23+ hỗ trợ tạo ephemeral debug container attach vào pod đang chạy — không cần redeploy, không cần sửa pod spec.
Debugging distributed system
Trong hệ thống microservices, vấn đề thường không nằm ở một service duy nhất mà ở interaction giữa các service. Service A gọi service B, B gọi C, C gọi database — lỗi có thể xảy ra ở bất kỳ điểm nào trong chuỗi, và triệu chứng lộ ra ở điểm khác hoàn toàn.
Distributed tracing là công cụ chính cho bài toán này. Nhưng trace chỉ hữu ích khi context propagation được setup đúng ở mọi service trên đường đi của request. Một service bỏ quên không propagate traceparent header là một “lỗ đen” trong trace — bạn thấy request vào service A rồi biến mất, 2 giây sau xuất hiện ở service C mà không biết chuyện gì xảy ra ở giữa. Khi debug vấn đề distributed, điều đầu tiên cần verify là trace có đầy đủ không — mọi span đều có, không có gap.
Một kỹ thuật hữu ích khi debug interaction giữa service là request diff: so sánh request thành công và request lỗi có cùng đặc điểm (cùng route, cùng user type, cùng payload tương tự). Tìm trace của cả hai, đặt cạnh nhau, xem span nào khác. Request thành công mất 200ms ở Redis, request lỗi mất 3 giây rồi timeout — vấn đề ở Redis connection cụ thể đó, không phải code.
Timeout và retry là nguồn bug phổ biến nhất trong distributed system. Service A set timeout 5 giây gọi B, B set timeout 10 giây gọi C. Nếu C chậm, B chờ 10 giây nhưng A đã timeout sau 5 giây — A retry, tạo request thứ hai tới B, B giờ có hai request đến C. Cascade retry này có thể amplify load gấp 2-4 lần. Khi debug latency spike trong distributed system, luôn kiểm tra timeout chain và retry policy — chúng thường là nguyên nhân hoặc yếu tố làm tồi tệ thêm vấn đề gốc.
Database debugging an toàn
Database là thành phần nhạy cảm nhất khi debug production. Một query nặng trên primary có thể lock table, block write, gây cascade failure cho toàn bộ hệ thống.
Quy tắc tuyệt đối: EXPLAIN ANALYZE chạy trên read replica, không bao giờ trên primary. EXPLAIN ANALYZE thực thi query thật để đo execution plan — với query scan hàng triệu row, nó sẽ thực sự scan hàng triệu row, tốn IO và CPU, có thể lock table nếu query dính row lock. Trên read replica, worst case là replica lag tăng; trên primary, worst case là toàn bộ write bị block.
Phân biệt rõ: EXPLAIN (không ANALYZE) chỉ show execution plan mà không thực thi query — an toàn chạy trên primary. EXPLAIN ANALYZE thực thi query để lấy thời gian thực tế của từng node — chỉ chạy trên replica. EXPLAIN (ANALYZE, BUFFERS) còn cho biết bao nhiêu page đọc từ disk vs cache — hữu ích cho debug IO nhưng tốn resource hơn nữa.
Nếu cần xem query đang chạy trên primary: pg_stat_activity (Postgres) hoặc SHOW PROCESSLIST (MySQL) cho danh sách query active mà không thực thi thêm gì — đây là operation readonly an toàn. Tìm query chạy lâu bất thường (> 30 giây), check wait_event để biết query đang chờ gì (lock, IO, network).
Khi cần kill query chạy quá lâu trên primary: SELECT pg_cancel_backend(pid) (Postgres) cancel query nhẹ nhàng, cho transaction rollback bình thường. Tránh pg_terminate_backend trừ khi cancel không hiệu quả — terminate đóng connection đột ngột, client có thể không xử lý đúng.
Slow query log nên bật sẵn trên production với threshold hợp lý (ví dụ 500ms cho Postgres log_min_duration_statement). Khi debug performance, giảm threshold xuống 100ms tạm thời để bắt query “không quá chậm nhưng chạy quá nhiều lần” — N+1 query mỗi cái 50ms nhưng 100 cái thì tổng 5 giây.
Một cạm bẫy khác khi debug database: chạy SELECT COUNT(*) hoặc aggregate query trên bảng lớn để “kiểm tra data” trong lúc incident. Query aggregate full table scan trên bảng hàng chục triệu row tốn IO đáng kể — trên primary đang overload, đây là thêm dầu vào lửa. Nếu cần kiểm tra data, dùng LIMIT, dùng index scan, hoặc dùng read replica. Và luôn nhớ: production database lúc incident không phải nơi chạy ad-hoc query để thoả mãn tò mò — chỉ chạy query phục vụ trực tiếp việc chẩn đoán.
Break glass — khi phải SSH vào production
Đôi khi không còn cách nào khác: log không đủ, trace bị đứt, vấn đề chỉ xảy ra trên server cụ thể (disk, network, kernel), cần nhìn trực tiếp process state. Đây là tình huống “break glass” — được phép nhưng phải có quy trình chặt chẽ.
Readonly access mặc định
SSH access vào production nên là readonly: user không có quyền write vào application directory, không có quyền kill process, không có quyền modify config. Chỉ có quyền đọc log file, chạy diagnostic command (top, htop, iostat, netstat, ss), và capture data (tcpdump, strace nếu được phép). Nếu cần write access (restart service, modify config), phải escalate qua quy trình riêng — thường là approval từ tech lead hoặc SRE.
Phân quyền này ngăn chặn sai lầm phổ biến nhất: engineer SSH vào production, chạy kill -9 nhầm process, hoặc rm -rf nhầm directory, hoặc sửa config file trực tiếp mà không qua version control. Readonly access biến SSH session thành “nhìn nhưng không sờ” — đủ cho hầu hết nhu cầu debug.
Một pattern tốt là tách biệt diagnostic account và admin account. Engineer dùng diagnostic account hàng ngày để đọc log, chạy top/htop/iostat, xem process list. Admin account chỉ dùng khi cần restart service hoặc thay đổi config, và cần MFA + approval. Tách biệt này giảm rủi ro “tay nhanh hơn não” khi đang stress.
Audit trail
Mọi SSH session vào production phải được ghi lại: ai kết nối, thời gian, từ IP nào, và mọi command đã chạy. Tool phổ biến: auditd trên Linux ghi syscall và command, bastion host (AWS SSM Session Manager, Teleport, Boundary) ghi session recording. Khi incident xảy ra sau SSH session, audit trail cho biết chính xác ai làm gì — không phải để đổ lỗi mà để hiểu timeline và ngăn lặp lại.
AWS SSM Session Manager đặc biệt hữu ích: không cần mở SSH port, session qua IAM authentication, mọi command được log vào CloudWatch. Không cần quản lý SSH key — giảm attack surface đáng kể.
Pair debugging
Khi SSH vào production để debug, nên có hai người: một người thao tác, một người quan sát. Người quan sát kiểm tra command trước khi chạy, ngăn sai lầm do vội vàng hoặc stress (incident lúc 2 giờ sáng, pressure từ stakeholder). Pattern này tương tự pair programming nhưng quan trọng hơn vì sai lầm trên production không có undo.
Nếu không có người thứ hai online, ít nhất phải nói ra command trước khi chạy — hoặc gõ vào Slack channel incident trước khi chạy trên terminal. Đây là self-review tối thiểu khi pair không khả thi.
Time-boxed session
SSH session vào production phải có giới hạn thời gian. Đặt mục tiêu trước khi SSH: “thu thập thread dump và top output trong 10 phút”. Nếu quá thời gian mà chưa tìm ra root cause, disconnect, tổng hợp data đã thu thập, thảo luận với team rồi quyết định bước tiếp theo. Session SSH kéo dài hàng giờ trên production là dấu hiệu cần thay đổi approach — không phải cần thêm thời gian trên server.
Bastion host có thể enforce timeout tự động: session bị đóng sau 30 phút không hoạt động, hoặc tối đa 1 giờ bất kể hoạt động. Nghe strict nhưng ngăn được tình huống engineer mở terminal production rồi quên đóng — session mở là attack surface mở.
Chuẩn bị trước khi SSH
Trước khi SSH vào production, chuẩn bị sẵn danh sách command sẽ chạy — không improvise trên terminal production. Viết command ra notepad hoặc Slack channel incident trước, review một lần, rồi mới paste vào terminal. Điều này ngăn typo nguy hiểm: kill -9 1234 thay vì kill -9 12345 — sai một số PID có thể giết nhầm process hoàn toàn khác.
Nên có sẵn runbook diagnostic cho từng service: danh sách command diagnostic phổ biến (check disk, check memory, check connection pool, check process state) đã được test và verify an toàn. On-call chỉ cần copy-paste từ runbook thay vì nhớ hay Google lúc 3 giờ sáng. Runbook cần review và update định kỳ — command cũ có thể không còn đúng sau khi infra thay đổi.
Dọn dẹp sau debug
Debug xong, root cause tìm được, fix đã deploy — nhiều team dừng ở đây. Nhưng bước dọn dẹp quan trọng không kém: mọi instrumentation tạm thời phải được remove, mọi access đặc biệt phải được thu hồi.
Checklist sau debug: verbose logging đã tắt chưa (dynamic log level về INFO)? Feature flag debug đã OFF chưa? Canary instance đã terminate chưa? SSH access escalated đã revoke chưa? Slow query log threshold đã về mức bình thường chưa? tcpdump process đã stop chưa?
Mỗi thứ tạm thời để lại trên production là một nguồn rủi ro. Debug log quên tắt gây storage nổ. Canary instance quên tắt nhận traffic thật với code instrumented nặng, latency cao. SSH session quên đóng là cửa mở cho attacker nếu credential bị lộ.
Tốt nhất là dùng checklist dạng issue template — tạo ticket “debug cleanup” ngay khi bắt đầu debug, liệt kê mọi thứ tạm thời đã thêm, close ticket khi tất cả đã dọn. Đơn giản nhưng ngăn được quên sót.
Một phần của cleanup mà hay bị quên: gỡ bỏ các workaround tạm thời. Trong lúc debug, có thể đã tăng connection pool, giảm timeout, tắt rate limit, hoặc skip validation để thu hẹp nguyên nhân. Những thay đổi này cần được revert về giá trị gốc sau khi tìm được root cause và deploy fix thực sự. Để workaround tạm chạy lâu dài là nợ kỹ thuật ngầm — connection pool quá lớn tốn memory, timeout quá cao che giấu vấn đề thực tế, skip validation là lỗ hổng bảo mật.
Sai lầm phổ biến
Để debug code trên production sau khi fix là sai lầm hay gặp nhất. Engineer thêm console.log("DEBUG: user data", userData) để tìm bug, fix bug xong nhưng quên xoá log line. Log đó in PII ra file, chạy trên production hàng tuần trước khi ai đó phát hiện. Code review nên catch debug log, nhưng tốt hơn là dùng lint rule cấm console.log trong production code — chỉ cho phép structured logger. ESLint có no-console rule, Go có go-lint rule check fmt.Println trong production package. Tự động hoá việc bắt debug log tốt hơn phụ thuộc vào con người nhớ xoá.
Chạy query nặng trên primary database lúc peak hour là sai lầm đắt thứ hai. SELECT * FROM orders WHERE created_at > '2025-01-01' trên bảng 50 triệu row — full table scan, lock IO, mọi write query khác chờ. Luôn dùng read replica cho ad-hoc query, và luôn EXPLAIN (không ANALYZE) trước để xem execution plan có table scan không.
Attach debugger pause process trên production nghe phi lý nhưng đã xảy ra. Debugger đặt breakpoint, process dừng tại breakpoint, mọi request đến service đó timeout. Một service timeout gây cascade timeout cho service gọi nó — một breakpoint trên một service có thể đánh sập cả hệ thống. Debugger trên production chỉ nên dùng ở chế độ non-breaking: log variable tại breakpoint nhưng không pause execution. Một số debugger hỗ trợ conditional breakpoint chỉ log output mà không dừng process — IntelliJ có “non-suspending breakpoint”, Chrome DevTools có logpoint. Đây là cách duy nhất chấp nhận được nếu phải dùng debugger trên production.
Redeploy chỉ để thêm log là sai lầm quy trình. Mỗi deploy có rủi ro riêng — config drift, dependency update, migration chưa chạy. Dynamic log level và feature flag verbose logging giải quyết nhu cầu này mà không cần deploy.
Sai lầm cuối nhưng ít ai nghĩ tới: debug quá lâu trong incident mà quên mitigate. Khi service đang down, ưu tiên đầu tiên là khôi phục dịch vụ cho user (rollback deploy, restart service, bật kill-switch, scale up), không phải tìm root cause. Debug tìm root cause làm sau khi service đã ổn — postmortem là nơi phù hợp để phân tích sâu. Kéo dài thời gian service down chỉ để hiểu tại sao service down là đánh đổi sai. Mitigate trước, debug sau — nghe đơn giản nhưng áp lực incident khiến nhiều engineer quên.
Xây debuggability vào kiến trúc
Debug production dễ hay khó phụ thuộc rất nhiều vào kiến trúc được thiết kế từ đầu có “debuggable” hay không. Một số đầu tư nhỏ lúc thiết kế tiết kiệm rất nhiều thời gian debug sau này.
Request ID propagation là nền tảng: mọi request vào hệ thống được gán unique ID (UUID hoặc trace ID), ID đó được propagate qua mọi service downstream, xuất hiện trong mọi log line, mọi trace span, mọi error response trả cho client. Khi user report lỗi kèm request ID, tìm toàn bộ context chỉ mất vài giây. Không có request ID, phải tìm theo timestamp + user_id + route — chậm hơn nhiều và dễ nhầm request.
Structured error response giúp cả user lẫn engineer debug nhanh hơn. Thay vì trả 500 Internal Server Error, trả {"error": "payment_gateway_timeout", "request_id": "abc123", "retry_after": 30}. Client biết lỗi gì, có nên retry không. Engineer biết lỗi ở downstream nào mà không cần mở log.
Health endpoint với diagnostics — endpoint /health không chỉ trả 200 OK mà còn cho biết trạng thái dependency: database connection pool (active/idle/max), cache hit rate, downstream service latency. Khi service “chậm” nhưng không “chết”, health endpoint chi tiết cho biết ngay dependency nào đang có vấn đề mà không cần SSH hay query log.
Phân biệt liveness (process còn sống không) và readiness (có sẵn sàng nhận traffic không) là quan trọng cho cả Kubernetes health check và debug. Service có thể liveness OK (process chạy) nhưng readiness fail (database connection pool cạn, cache không kết nối được). Readiness check chi tiết giúp chẩn đoán nhanh: “/readiness trả về 503 với db_pool: exhausted” — rõ nguyên nhân trong 1 giây.
Correlation ID trong async flow — message queue, event bus, cron job đều cần carry request ID hoặc correlation ID. Khi user action trigger event, event trigger worker, worker gọi service khác — toàn bộ chuỗi cần cùng correlation ID để trace end-to-end. Thiếu correlation trong async flow là “blind spot” debug phổ biến nhất.
Error budget cho debug — đây là khái niệm ít phổ biến nhưng hữu ích. Đặt giới hạn cho mức ảnh hưởng mà hoạt động debug được phép gây ra: thêm tối đa 5% CPU overhead, thêm tối đa 10ms latency P99, sinh thêm tối đa 1GB log/giờ. Nếu debug vượt budget, phải giảm scope hoặc chuyển sang approach khác. Budget này ngăn tình trạng “thêm instrumentation một chút nữa” liên tục cho đến khi production bị ảnh hưởng nghiêm trọng mà không ai nhận ra vì mỗi bước thêm đều “nhỏ”.
Reproducible debug environment — invest vào khả năng tạo môi trường debug nhanh từ production state. Snapshot database (anonymized), replay traffic log, clone config — tất cả có thể tự động hoá thành một command: make debug-env tạo local environment giống production nhất có thể. Thời gian đầu tư xây debug environment tự động sẽ tiết kiệm gấp nhiều lần so với thời gian debug ad-hoc trên production.
Thang leo thang debug
Tổng hợp lại, thứ tự debug production nên tuân theo nguyên tắc ít xâm lấn trước, tăng dần khi cần. Mỗi bước tiếp theo chỉ nên thực hiện khi bước trước không đủ thông tin.
Bước đầu tiên là khai thác telemetry có sẵn: metrics dashboard, traces, logs hiện tại. Không chạm gì vào production, chỉ đọc dữ liệu đã có. Phần lớn vấn đề dừng ở đây nếu observability setup tốt.
Bước tiếp theo là tăng verbosity: bật dynamic log level cho service hoặc user cụ thể, thêm counter metric nhẹ cho code path nghi ngờ. Overhead minimal, blast radius nhỏ, có thể revert nhanh.
Bước ba là reproduce offline: replay traffic lên staging, tạo test case từ production log, dùng shadow traffic. Mọi debug diễn ra ngoài production, không ảnh hưởng user.
Bước bốn là debug trên production có kiểm soát: canary debugging, tcpdump/strace có giới hạn, continuous profiling, heap dump trên instance đã drain. Có overhead nhưng giới hạn trong scope nhỏ.
Bước cuối cùng — break glass: SSH vào production với readonly access, audit trail, pair debugging, time-boxed. Chỉ khi tất cả bước trên không đủ.
Nguyên tắc chung: mỗi bước lên thang đều phải justify bằng “bước trước đã thử và không đủ thông tin vì lý do gì”. Nhảy thẳng từ “thấy alert” lên “SSH vào server” là bỏ qua toàn bộ công cụ an toàn đã được xây dựng — và đó là lúc sự cố thêm xảy ra.
Debug incident vs debug bug thường
Phân biệt hai ngữ cảnh debug rất khác nhau giúp chọn đúng approach.
Debug trong incident: hệ thống đang down hoặc degraded, user đang bị ảnh hưởng, clock SLO đang chạy. Mục tiêu không phải tìm root cause mà là khôi phục dịch vụ. Rollback deploy, restart service, bật kill-switch, scale up — bất kỳ hành động nào giúp user trở lại bình thường là hành động đúng, dù chưa hiểu tại sao. Root cause analysis làm trong postmortem sau khi service ổn định, có thời gian suy nghĩ kỹ thay vì quyết định dưới áp lực.
Debug bug thường: user report lỗi nhưng không phải incident, hệ thống vẫn hoạt động, không có pressure thời gian. Đây là lúc có thể đầu tư thời gian reproduce, thêm instrumentation, phân tích kỹ. Không cần vội, không cần SSH vào production lúc nửa đêm.
Sai lầm phổ biến là áp dụng quy trình incident (vội vàng, tìm root cause ngay) cho bug thường, hoặc áp dụng quy trình bug thường (phân tích chậm rãi, thêm instrumentation từ từ) cho incident. Nhận diện đúng ngữ cảnh giúp chọn đúng tốc độ và mức rủi ro chấp nhận được.
Tóm tắt
Debug production an toàn bắt đầu bằng nguyên tắc “quan sát trước, không chạm vào” — khai thác metrics, traces, logs có sẵn trước khi nghĩ đến SSH hay redeploy. Structured logging với trace_id trong mọi log line là khoản đầu tư trả lãi mỗi incident. Dynamic log level qua feature flag hoặc admin endpoint cho phép tăng verbosity tại runtime mà không cần deploy.
Reproduce ở staging trước khi debug production — dùng production-like data và traffic replay. Khi staging không reproduce được, dùng kỹ thuật an toàn: query log/trace có sẵn, thêm counter metric nhẹ, tcpdump readonly, continuous profiling, verbose logging theo feature flag cho subset traffic nhỏ. Tuân theo thang leo thang: ít xâm lấn trước, tăng dần khi bước trước không đủ thông tin.
SSH vào production là biện pháp cuối cùng — readonly access mặc định, audit trail bắt buộc, pair debugging khi có thể, session có giới hạn thời gian. Chuẩn bị command trước khi SSH, dùng runbook diagnostic có sẵn thay vì improvise. Sau debug, dọn dẹp mọi instrumentation tạm thời, revert workaround, thu hồi access đặc biệt — dùng cleanup checklist để không quên sót.
Xây debuggability vào kiến trúc từ đầu: request ID propagation, structured error, health endpoint chi tiết, correlation ID trong async flow. Đặt error budget cho hoạt động debug để không vô tình gây thêm áp lực lên hệ thống đang có vấn đề. Và quan trọng nhất: mitigate trước, debug sau — khôi phục dịch vụ cho user là ưu tiên số một, tìm root cause là ưu tiên số hai. Hệ thống dễ debug là hệ thống ít incident hơn — vì tìm root cause nhanh hơn, fix đúng hơn, và không gây thêm sự cố trong quá trình debug.