Tình huống ác mộng: một service đang chạy bỗng treo cứng — không crash, không in log, không phản hồi request nào. Bạn không thể sửa nếu không biết nó đang kẹt ở đâu: goroutine/thread nào, dòng code nào, đang chờ gì. May thay, nhiều runtime hiện đại có một "cửa hậu" tuyệt vời: gửi một tín hiệu để ép chương trình tự khai toàn bộ trạng thái nội tại. Bài này (phần 7 loạt Debug) chạy thật kỹ thuật này với Go — gửi SIGQUIT cho một chương trình bị treo và để nó in ra stack của mọi goroutine, chỉ đúng dòng đang kẹt.
Vì sao tín hiệu là công cụ debug
Tín hiệu (signal) là cách hệ điều hành "gõ vai" một tiến trình. Ngoài các tín hiệu quen thuộc (SIGTERM yêu cầu dừng, SIGKILL giết cứng), có những tín hiệu mà runtime của ngôn ngữ bắt để làm việc chẩn đoán:
- Go:
SIGQUIT(kill -QUIT, hayCtrl+\ở terminal) → runtime in stack của tất cả goroutine rồi thoát. - Java:
kill -3(SIGQUIT) → in thread dump ra stdout. - Python: không có sẵn như vậy, nhưng
py-spy dump --pid(công cụ ngoài) làm điều tương tự mà không cần sửa code.
Điểm mạnh: không cần dừng bằng debugger, không cần thêm log, không cần gdb — chỉ một tín hiệu và chương trình tự phơi bày trạng thái.
kill -QUIT <pid> # Go: in stack mọi goroutine

Hình 1: Chương trình treo không cho thông tin gì; gửi SIGQUIT khiến Go in stack mọi goroutine (Java kill -3 in thread dump); đọc dump tìm goroutine bị chặn và dòng code; panic/SIGSEGV cũng tự in nơi crash.
Đo thật: bắt một deadlock tự khai
Mình viết một chương trình Go có một goroutine chờ mãi trên một channel không ai gửi (kẹt), rồi gửi SIGQUIT:

Hình 2: Chạy thật — SIGQUIT in SIGQUIT: quit và stack 8 goroutine; goroutine thủ phạm ở trạng thái [chan receive] chỉ đúng main.congViec tại hang.go:12 (dòng <-ch), tạo bởi main.main tại hang.go:19; và SIGSEGV/panic in nơi crash main.main tại crash.go:4.
- Dump toàn bộ goroutine:
kill -QUITkhiến Go inSIGQUIT: quitrồi stack của 8 goroutine. Không cần gdb, không cần thêm dòng log nào — runtime tự làm. - Chỉ đúng thủ phạm: goroutine kẹt ở trạng thái
[chan receive](đang chờ nhận từ channel), và stack chỉ chính xácmain.congViectạihang.go:12— đúng dòng<-ch. Nó còn cho biết goroutine này được tạo ởhang.go:19. Trạng thái trong ngoặc ([chan receive],[semacquire]chờ khoá,[IO wait]chờ mạng) cho biết đang chờ gì — đủ để chẩn đoán deadlock. - Crash cũng tự khai: một chương trình Go truy cập con trỏ nil in ngay
panic: nil pointer dereference [signal SIGSEGV]và stack chỉmain.maintạicrash.go:4. Bạn biết dòng nào gây crash mà không cần công cụ ngoài.
Còn core dump và gdb?
Kỹ thuật cổ điển hơn: core dump — khi chương trình crash, kernel lưu một ảnh chụp toàn bộ bộ nhớ ra file core (cần bật ulimit -c unlimited), rồi bạn mở bằng gdb ./binary core để xem stack, biến, trạng thái lúc chết. Rất mạnh cho C/C++ và các binary không tự in stack. (Môi trường lab này không có sẵn gdb nên mình không demo bước gdb — nói thẳng thay vì bịa; nhưng cơ chế là: crash → core file → gdb đọc lại.) Với các runtime tự in stack như Go/Java, thường không cần tới core dump cho phần lớn trường hợp.
Đánh đổi cần cân nhắc
SIGQUIT làm chương trình Go thoát — đây là biện pháp chẩn đoán, không phải giám sát liên tục. Sau khi in stack, chương trình dừng. Với một service production đang treo, đây thường là điều bạn muốn (dump để phân tích rồi restart), nhưng biết trước hậu quả. Nếu chỉ muốn xem stack mà không giết, dùng công cụ ngoài (py-spy cho Python, hoặc GODEBUG/pprof endpoint cho Go — bài sau) để lấy stack của tiến trình đang chạy mà không làm nó thoát.
Dump goroutine dài — biết cách lọc. Một service thật có hàng nghìn goroutine; dump ra hàng nghìn dòng. Đừng đọc hết — tìm các goroutine ở trạng thái chờ đáng ngờ (chan receive, semacquire, select) và các dòng code của bạn (main.*, package của bạn), bỏ qua các goroutine hệ thống (runtime.*, GC). Nhóm nhiều goroutine cùng kẹt ở một chỗ = dấu hiệu deadlock/nghẽn ở đó.
Bật core dump có rủi ro bảo mật và dung lượng. Core dump chứa toàn bộ bộ nhớ tiến trình lúc crash — có thể gồm mật khẩu, khoá, dữ liệu người dùng. Trên production, bật ulimit -c unlimited bừa bãi vừa tốn đĩa (core của app lớn có thể hàng GB) vừa lộ bí mật nếu file core bị đọc trộm. Bật có kiểm soát, đặt đường dẫn an toàn (/proc/sys/kernel/core_pattern), và dọn sau khi phân tích.
Ba ý mang về
- Tín hiệu ép chương trình tự khai trạng thái: đo thật
kill -QUITkhiến Go in stack 8 goroutine — không cần gdb, không cần thêm log; Java dùngkill -3(thread dump), Python dùngpy-spy dump. - Dump chỉ đúng chỗ kẹt: đo thật goroutine
[chan receive]chỉmain.congViectạihang.go:12(dòng<-ch) — trạng thái + dòng code cho bạn tìm ra deadlock ngay; crash/SIGSEGV cũng in nơi gây lỗi (crash.go:4). - Core dump là phương án cổ điển cho crash: crash → file
core(cầnulimit -c unlimited) →gdbđọc lại trạng thái lúc chết; nhưngSIGQUITthoát chương trình (dùng py-spy/pprof nếu muốn xem mà không giết), và core dump chứa bí mật nên bật có kiểm soát.
Nguồn
- Go docs — Runtime / GOTRACEBACK và SIGQUIT: https://pkg.go.dev/runtime#hdr-Environment_Variables
- man7.org — signal(7): https://man7.org/linux/man-pages/man7/signal.7.html
- man7.org — core(5): https://man7.org/linux/man-pages/man5/core.5.html
Phần sau ta tìm thủ phạm ở cấp hệ thống: dùng top/ps đọc từ /proc để tìm tiến trình ngốn CPU hay RAM nhất, và đọc đúng cột để không quy tội nhầm.