Chẩn đoán

Giới thiệu

Hệ sinh thái Go cung cấp một bộ API và công cụ phong phú để chẩn đoán các vấn đề về logic và hiệu suất trong các chương trình Go. Trang này tóm tắt các công cụ hiện có và giúp người dùng Go chọn đúng công cụ cho vấn đề cụ thể của họ.

Các giải pháp chẩn đoán có thể được phân loại thành các nhóm sau:

Lưu ý: Một số công cụ chẩn đoán có thể can thiệp lẫn nhau. Ví dụ, memory profiling chính xác làm lệch CPU profile và goroutine blocking profiling ảnh hưởng đến scheduler trace. Hãy dùng từng công cụ riêng lẻ để có thông tin chính xác hơn.

Profiling

Profiling hữu ích để xác định các phần mã tốn kém hoặc được gọi thường xuyên. Go runtime cung cấp dữ liệu profiling ở định dạng mà công cụ trực quan hóa pprof mong đợi. Dữ liệu profiling có thể được thu thập trong quá trình kiểm thử qua go test hoặc các endpoint được cung cấp từ gói net/http/pprof. Người dùng cần thu thập dữ liệu profiling và sử dụng công cụ pprof để lọc và trực quan hóa các đường dẫn mã hàng đầu.

Các profile được định nghĩa sẵn bởi gói runtime/pprof:

Tôi có thể dùng profiler nào khác để profile chương trình Go?

Trên Linux, công cụ perf có thể được dùng để profile chương trình Go. Perf có thể profile và unwind mã cgo/SWIG và kernel, nên nó hữu ích để có được thông tin chi tiết về các điểm nghẽn hiệu suất ở native/kernel. Trên macOS, bộ Instruments có thể được dùng để profile chương trình Go.

Tôi có thể profile các dịch vụ production của mình không?

Có. Việc profile các chương trình trong môi trường production là an toàn, nhưng bật một số profile (ví dụ: CPU profile) sẽ tăng chi phí. Bạn nên kỳ vọng thấy hiệu suất giảm. Có thể ước tính mức phạt hiệu suất bằng cách đo overhead của profiler trước khi bật nó trong môi trường production.

Bạn có thể muốn định kỳ profile các dịch vụ production của mình. Đặc biệt trong một hệ thống có nhiều bản sao của một tiến trình, việc chọn một bản sao ngẫu nhiên định kỳ là lựa chọn an toàn. Chọn một tiến trình production, profile nó trong X giây mỗi Y giây và lưu kết quả để trực quan hóa và phân tích; sau đó lặp lại định kỳ. Kết quả có thể được xem xét thủ công và/hoặc tự động để tìm vấn đề. Việc thu thập profile có thể can thiệp lẫn nhau, vì vậy nên thu thập chỉ một profile tại một thời điểm.

Những cách tốt nhất để trực quan hóa dữ liệu profiling là gì?

Các công cụ Go cung cấp trực quan hóa văn bản, đồ thị và callgrind của dữ liệu profile bằng go tool pprof. Đọc Profiling Go programs để xem chúng hoạt động.


Liệt kê các lời gọi tốn kém nhất dưới dạng văn bản.


Trực quan hóa các lời gọi tốn kém nhất dưới dạng đồ thị.

Chế độ xem Weblist hiển thị các phần tốn kém của mã nguồn theo từng dòng trong một trang HTML. Trong ví dụ sau, 530ms được dành cho runtime.concatstrings và chi phí của mỗi dòng được trình bày trong danh sách.


Trực quan hóa các lời gọi tốn kém nhất dưới dạng weblist.

Một cách khác để trực quan hóa dữ liệu profile là flame graph. Flame graph cho phép bạn di chuyển theo một đường ancestry cụ thể, để bạn có thể zoom vào/ra các phần mã cụ thể. pprof upstream có hỗ trợ flame graph.


Flame graph cung cấp trực quan hóa để phát hiện các đường dẫn mã tốn kém nhất.

Tôi có bị giới hạn với các profile tích hợp sẵn không?

Ngoài những gì runtime cung cấp, người dùng Go có thể tạo các profile tùy chỉnh qua pprof.Profile và sử dụng các công cụ hiện có để kiểm tra chúng.

Tôi có thể phục vụ các handler profiler (/debug/pprof/...) trên đường dẫn và cổng khác không?

Có. Gói net/http/pprof đăng ký các handler của mình vào default mux theo mặc định, nhưng bạn cũng có thể tự đăng ký bằng cách sử dụng các handler được xuất ra từ gói.

Ví dụ, đoạn code sau sẽ phục vụ pprof.Profile handler trên :7777 tại /custom_debug_path/profile:

package main

import (
	"log"
	"net/http"
	"net/http/pprof"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/custom_debug_path/profile", pprof.Profile)
	log.Fatal(http.ListenAndServe(":7777", mux))
}

Tracing

Tracing là cách đo lường mã để phân tích độ trễ trong suốt vòng đời của một chuỗi lời gọi. Go cung cấp gói golang.org/x/net/trace như một tracing backend tối giản cho mỗi Go node và cung cấp một thư viện đo lường tối giản với dashboard đơn giản. Go cũng cung cấp một execution tracer để trace các sự kiện runtime trong một khoảng thời gian.

Tracing giúp chúng ta:

Trong các hệ thống nguyên khối, tương đối dễ dàng thu thập dữ liệu chẩn đoán từ các khối xây dựng của chương trình. Tất cả các module tồn tại trong một tiến trình và chia sẻ các tài nguyên chung để báo cáo log, lỗi và các thông tin chẩn đoán khác. Khi hệ thống của bạn phát triển vượt qua một tiến trình duy nhất và bắt đầu trở thành phân tán, việc theo dõi một lời gọi bắt đầu từ web server front-end đến tất cả các back-end của nó cho đến khi phản hồi được trả về người dùng trở nên khó hơn. Đây là nơi distributed tracing đóng vai trò lớn trong việc đo lường và phân tích các hệ thống production của bạn.

Distributed tracing là cách đo lường mã để phân tích độ trễ trong suốt vòng đời của một yêu cầu người dùng. Khi một hệ thống phân tán và khi các công cụ profiling và debugging thông thường không mở rộng được, bạn có thể muốn sử dụng các công cụ distributed tracing để phân tích hiệu suất của các yêu cầu người dùng và RPC của bạn.

Distributed tracing giúp chúng ta:

Hệ sinh thái Go cung cấp nhiều thư viện distributed tracing khác nhau cho từng hệ thống tracing và các thư viện không phụ thuộc backend.

Có cách nào để tự động chặn từng lời gọi hàm và tạo trace không?

Go không cung cấp cách để tự động chặn mọi lời gọi hàm và tạo các trace span. Bạn cần đo lường mã thủ công để tạo, kết thúc và ghi chú các span.

Tôi nên truyền trace header trong các thư viện Go như thế nào?

Bạn có thể truyền các trace identifier và tag trong context.Context. Hiện chưa có trace key chính tắc hoặc biểu diễn chung của trace header trong ngành. Mỗi nhà cung cấp tracing chịu trách nhiệm cung cấp các tiện ích truyền trong các thư viện Go của họ.

Các sự kiện cấp thấp nào khác từ thư viện chuẩn hoặc runtime có thể được bao gồm trong trace?

Thư viện chuẩn và runtime đang cố gắng hiển thị một số API bổ sung để thông báo về các sự kiện nội bộ cấp thấp. Ví dụ, httptrace.ClientTrace cung cấp API để theo dõi các sự kiện cấp thấp trong vòng đời của một yêu cầu gửi đi. Đang có nỗ lực liên tục để lấy các sự kiện runtime cấp thấp từ execution tracer của runtime và cho phép người dùng định nghĩa và ghi lại các sự kiện người dùng của họ.

Debugging

Debugging là quá trình xác định tại sao một chương trình hoạt động sai. Debugger cho phép chúng ta hiểu luồng thực thi và trạng thái hiện tại của chương trình. Có một số phong cách debugging; phần này sẽ chỉ tập trung vào việc gắn một debugger vào chương trình và debugging core dump.

Người dùng Go chủ yếu sử dụng các debugger sau:

Debugger hoạt động tốt với chương trình Go như thế nào?

Trình biên dịch gc thực hiện các tối ưu hóa như function inlining và variable registerization. Những tối ưu hóa này đôi khi làm cho việc gỡ lỗi với debugger khó hơn. Đang có nỗ lực liên tục để cải thiện chất lượng thông tin DWARF được tạo ra cho các tệp nhị phân đã được tối ưu hóa. Cho đến khi các cải tiến đó có sẵn, chúng tôi khuyến nghị tắt tối ưu hóa khi xây dựng mã đang được gỡ lỗi. Lệnh sau xây dựng một gói không có tối ưu hóa trình biên dịch:

$ go build -gcflags=all="-N -l"

Trong khuôn khổ nỗ lực cải tiến, Go 1.10 đã giới thiệu cờ trình biên dịch mới -dwarflocationlists. Cờ này khiến trình biên dịch thêm danh sách vị trí giúp debugger hoạt động với các tệp nhị phân đã tối ưu hóa. Lệnh sau xây dựng một gói với tối ưu hóa nhưng có các DWARF location list:

$ go build -gcflags="-dwarflocationlists=true"

Giao diện người dùng debugger nào được khuyến nghị?

Mặc dù cả delve và gdb đều cung cấp CLI, nhưng hầu hết các tích hợp trình soạn thảo và IDE đều cung cấp giao diện người dùng dành riêng cho debugging.

Có thể thực hiện postmortem debugging với chương trình Go không?

Tệp core dump là một tệp chứa memory dump của một tiến trình đang chạy và trạng thái tiến trình của nó. Nó chủ yếu được dùng để gỡ lỗi post-mortem của một chương trình và để hiểu trạng thái của nó trong khi nó vẫn đang chạy. Hai trường hợp này làm cho việc gỡ lỗi core dump trở thành công cụ chẩn đoán tốt để phân tích postmortem và phân tích các dịch vụ production. Có thể lấy core file từ chương trình Go và dùng delve hoặc gdb để gỡ lỗi, xem trang core dump debugging để có hướng dẫn từng bước.

Thống kê và sự kiện runtime

Runtime cung cấp thống kê và báo cáo về các sự kiện nội bộ cho người dùng để chẩn đoán các vấn đề về hiệu suất và mức sử dụng ở cấp độ runtime.

Người dùng có thể theo dõi các thống kê này để hiểu rõ hơn về tình trạng sức khỏe tổng thể và hiệu suất của các chương trình Go. Một số thống kê và trạng thái được theo dõi thường xuyên:

Execution tracer

Go đi kèm với một execution tracer runtime để nắm bắt một loạt rộng các sự kiện runtime. Lập lịch, syscall, garbage collection, kích thước heap và các sự kiện khác được thu thập bởi runtime và có sẵn để trực quan hóa bởi go tool trace. Execution tracer là công cụ để phát hiện các vấn đề về độ trễ và mức sử dụng. Bạn có thể kiểm tra mức sử dụng CPU, và khi nào networking hoặc syscall là nguyên nhân gây preemption cho các goroutine.

Tracer hữu ích để:

Tuy nhiên, nó không tốt để xác định các điểm nóng như phân tích nguyên nhân sử dụng bộ nhớ hoặc CPU quá mức. Hãy dùng công cụ profiling trước để giải quyết chúng.

Ở trên, trực quan hóa go tool trace cho thấy thực thi bắt đầu tốt, sau đó trở thành tuần tự. Điều này gợi ý có thể có tranh chấp lock cho một tài nguyên chia sẻ tạo ra điểm nghẽn.

Xem go tool trace để thu thập và phân tích trace runtime.

GODEBUG

Runtime cũng phát ra sự kiện và thông tin nếu biến môi trường GODEBUG được đặt phù hợp.

Biến môi trường GODEBUG có thể được dùng để tắt việc sử dụng các phần mở rộng tập lệnh trong thư viện chuẩn và runtime.