Mặc dù các nhà phát triển ứng dụng khách và web giao diện người dùng thường sử dụng các công cụ như Trình phân tích CPU của Android Studio hoặc các công cụ lập hồ sơ có trong Chrome để cải thiện hiệu suất mã của họ, nhưng các kỹ thuật tương đương lại không dễ tiếp cận hoặc được những người làm việc trên các dịch vụ phụ trợ áp dụng. Stackdriver Profiler mang đến những chức năng tương tự cho nhà phát triển dịch vụ, bất kể mã của họ có đang chạy trên Google Cloud Platform hay không.

Công cụ này thu thập thông tin về mức sử dụng CPU và việc phân bổ bộ nhớ từ các ứng dụng phát hành công khai của bạn. Công cụ này sẽ liên kết thông tin đó với mã nguồn của ứng dụng, giúp bạn xác định những phần của ứng dụng đang tiêu thụ nhiều tài nguyên nhất, đồng thời làm nổi bật các đặc điểm hiệu suất của mã. Mức hao tổn thấp của các kỹ thuật thu thập mà công cụ này sử dụng giúp công cụ phù hợp để sử dụng liên tục trong môi trường phát hành công khai.
Trong lớp học lập trình này, bạn sẽ tìm hiểu cách thiết lập Stackdriver Profiler cho một chương trình Go và làm quen với những thông tin chi tiết về hiệu suất ứng dụng mà công cụ này có thể trình bày.
Kiến thức bạn sẽ học được
- Cách định cấu hình chương trình Go để lập hồ sơ bằng Stackdriver Profiler.
- Cách thu thập, xem và phân tích dữ liệu hiệu suất bằng Stackdriver Profiler.
Bạn cần có
- Một dự án trên Google Cloud Platform
- Một trình duyệt, chẳng hạn như Chrome hoặc Firefox
- Làm quen với các trình chỉnh sửa văn bản tiêu chuẩn của Linux, chẳng hạn như Vim, EMAC hoặc Nano
Bạn sẽ sử dụng hướng dẫn này như thế nào?
Bạn đánh giá thế nào về trải nghiệm của mình với Google Cloud Platform?
Thiết lập môi trường theo tốc độ của riêng bạn
Nếu chưa có Tài khoản Google (Gmail hoặc Google Apps), bạn phải tạo một tài khoản. Đăng nhập vào bảng điều khiển Google Cloud Platform (console.cloud.google.com) rồi tạo một dự án mới:
Hãy nhớ mã dự án, một tên duy nhất trong tất cả các dự án trên Google Cloud (tên ở trên đã được sử dụng và sẽ không hoạt động đối với bạn, xin lỗi!). Sau này trong lớp học lập trình này, chúng ta sẽ gọi nó là PROJECT_ID.
Tiếp theo, bạn cần bật tính năng thanh toán trong Cloud Console để sử dụng các tài nguyên của Google Cloud.
Việc thực hiện lớp học lập trình này sẽ không tốn của bạn quá vài đô la, nhưng có thể tốn nhiều hơn nếu bạn quyết định sử dụng nhiều tài nguyên hơn hoặc nếu bạn để các tài nguyên đó chạy (xem phần "dọn dẹp" ở cuối tài liệu này).
Người dùng mới của Google Cloud Platform đủ điều kiện dùng thử miễn phí 300 USD.
Google Cloud Shell
Mặc dù bạn có thể vận hành Google Cloud từ xa trên máy tính xách tay, nhưng để đơn giản hoá quá trình thiết lập trong lớp học lập trình này, chúng ta sẽ sử dụng Google Cloud Shell, một môi trường dòng lệnh chạy trên Cloud.
Kích hoạt Google Cloud Shell
Trên Bảng điều khiển GCP, hãy nhấp vào biểu tượng Cloud Shell trên thanh công cụ ở trên cùng bên phải:
Sau đó, hãy nhấp vào "Start Cloud Shell" (Bắt đầu Cloud Shell):
Quá trình cung cấp và kết nối với môi trường chỉ mất vài phút:
Máy ảo này được trang bị tất cả các công cụ phát triển mà bạn cần. Nó cung cấp một thư mục chính có dung lượng 5 GB và chạy trên Google Cloud, giúp tăng cường đáng kể hiệu suất mạng và hoạt động xác thực. Bạn có thể thực hiện hầu hết, nếu không muốn nói là tất cả, công việc trong phòng thí nghiệm này chỉ bằng một trình duyệt hoặc Google Chromebook.
Sau khi kết nối với Cloud Shell, bạn sẽ thấy rằng mình đã được xác thực và dự án đã được đặt thành PROJECT_ID.
Chạy lệnh sau trong Cloud Shell để xác nhận rằng bạn đã được xác thực:
gcloud auth list
Đầu ra của lệnh
Credentialed accounts: - <myaccount>@<mydomain>.com (active)
gcloud config list project
Đầu ra của lệnh
[core] project = <PROJECT_ID>
Nếu không, bạn có thể đặt nó bằng lệnh sau:
gcloud config set project <PROJECT_ID>
Đầu ra của lệnh
Updated property [core/project].
Trong Cloud Console, hãy chuyển đến giao diện người dùng Trình phân tích tài nguyên bằng cách nhấp vào "Trình phân tích tài nguyên" trong thanh điều hướng bên trái:

Ngoài ra, bạn có thể sử dụng thanh tìm kiếm của Cloud Console để chuyển đến giao diện người dùng Trình phân tích tài nguyên: chỉ cần nhập "Stackdriver Profiler" rồi chọn mục tìm được. Dù bằng cách nào, bạn cũng sẽ thấy giao diện người dùng Profiler với thông báo "Không có dữ liệu để hiển thị" như bên dưới. Dự án này là dự án mới nên chưa thu thập được dữ liệu lập hồ sơ nào.

Bây giờ là lúc để lập hồ sơ về một số nội dung!
Chúng ta sẽ sử dụng một ứng dụng Go tổng hợp đơn giản có trên GitHub. Trong cửa sổ dòng lệnh Cloud Shell mà bạn vẫn đang mở (và trong khi thông báo "Không có dữ liệu để hiển thị" vẫn xuất hiện trong giao diện người dùng Trình phân tích tài nguyên), hãy chạy lệnh sau:
$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...
Sau đó, chuyển sang thư mục ứng dụng:
$ cd ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/profiler/hotapp
Thư mục này chứa tệp "main.go", là một ứng dụng tổng hợp đã bật tác nhân lập hồ sơ:
main.go
...
import (
...
"cloud.google.com/go/profiler"
)
...
func main() {
err := profiler.Start(profiler.Config{
Service: "hotapp-service",
DebugLogging: true,
MutexProfiling: true,
})
if err != nil {
log.Fatalf("failed to start the profiler: %v", err)
}
...
}Theo mặc định, tác nhân lập hồ sơ sẽ thu thập hồ sơ CPU, heap và luồng. Đoạn mã ở đây cho phép thu thập các hồ sơ mutex (còn được gọi là "tranh chấp").
Bây giờ, hãy chạy chương trình:
$ go run main.go
Khi chương trình chạy, tác nhân lập hồ sơ sẽ định kỳ thu thập hồ sơ của 5 loại được định cấu hình. Bộ sưu tập được tạo ngẫu nhiên theo thời gian (với tốc độ trung bình là một hồ sơ mỗi phút cho mỗi loại), vì vậy, có thể mất tối đa 3 phút để thu thập từng loại. Chương trình sẽ cho bạn biết thời điểm tạo hồ sơ. Thông báo được bật bằng cờ DebugLogging trong cấu hình ở trên; nếu không, tác nhân sẽ chạy âm thầm:
$ go run main.go 2018/03/28 15:10:24 profiler has started 2018/03/28 15:10:57 successfully created profile THREADS 2018/03/28 15:10:57 start uploading profile 2018/03/28 15:11:19 successfully created profile CONTENTION 2018/03/28 15:11:30 start uploading profile 2018/03/28 15:11:40 successfully created profile CPU 2018/03/28 15:11:51 start uploading profile 2018/03/28 15:11:53 successfully created profile CONTENTION 2018/03/28 15:12:03 start uploading profile 2018/03/28 15:12:04 successfully created profile HEAP 2018/03/28 15:12:04 start uploading profile 2018/03/28 15:12:04 successfully created profile THREADS 2018/03/28 15:12:04 start uploading profile 2018/03/28 15:12:25 successfully created profile HEAP 2018/03/28 15:12:25 start uploading profile 2018/03/28 15:12:37 successfully created profile CPU ...
Giao diện người dùng sẽ tự động cập nhật ngay sau khi thu thập được hồ sơ đầu tiên. Sau đó, công cụ này sẽ không tự động cập nhật. Vì vậy, để xem dữ liệu mới, bạn cần làm mới giao diện người dùng Profiler theo cách thủ công. Để thực hiện việc này, hãy nhấp vào nút Hiện tại trong bộ chọn khoảng thời gian hai lần:

Sau khi giao diện người dùng làm mới, bạn sẽ thấy nội dung như sau:

Bộ chọn loại hồ sơ cho thấy 5 loại hồ sơ hiện có:

Bây giờ, hãy xem xét từng loại hồ sơ và một số chức năng quan trọng của giao diện người dùng, sau đó tiến hành một số thử nghiệm. Ở giai đoạn này, bạn không cần đến thiết bị đầu cuối Cloud Shell nữa, vì vậy, bạn có thể thoát bằng cách nhấn CTRL-C rồi nhập "exit".
Sau khi thu thập một số dữ liệu, hãy xem xét kỹ hơn. Chúng tôi đang sử dụng một ứng dụng tổng hợp (nguồn có trên Github) mô phỏng các hành vi thường thấy của nhiều loại vấn đề về hiệu suất trong quá trình phát hành công khai.
Mã sử dụng nhiều CPU
Chọn loại hồ sơ CPU. Sau khi giao diện người dùng tải, bạn sẽ thấy trong biểu đồ ngọn lửa 4 khối lá cho hàm load, tổng cộng chiếm toàn bộ mức tiêu thụ CPU:

Hàm này được viết riêng để tiêu thụ nhiều chu kỳ CPU bằng cách chạy một vòng lặp chặt chẽ:
main.go
func load() {
for i := 0; i < (1 << 20); i++ {
}
}Hàm này được gọi gián tiếp từ busyloop() thông qua 4 đường dẫn gọi: busyloop → {foo1, foo2} → {bar, baz} → load. Chiều rộng của một hộp hàm biểu thị chi phí tương đối của đường dẫn gọi cụ thể. Trong trường hợp này, cả 4 đường dẫn đều có chi phí tương đương nhau. Trong một chương trình thực tế, bạn nên tập trung vào việc tối ưu hoá những đường dẫn lệnh gọi quan trọng nhất về hiệu suất. Biểu đồ ngọn lửa (nhấn mạnh trực quan các đường dẫn tốn kém hơn bằng các hộp lớn hơn) giúp bạn dễ dàng xác định các đường dẫn này.
Bạn có thể sử dụng bộ lọc dữ liệu hồ sơ để tinh chỉnh thêm chế độ hiển thị. Ví dụ: hãy thử thêm bộ lọc "Show stacks" (Hiện ngăn xếp) và chỉ định "baz" làm chuỗi bộ lọc. Bạn sẽ thấy một hình ảnh như ảnh chụp màn hình bên dưới, trong đó chỉ có 2 trong số 4 đường dẫn lệnh gọi đến load() được hiển thị. Hai đường dẫn này là hai đường dẫn duy nhất đi qua một hàm có chuỗi "baz" trong tên. Việc lọc này sẽ hữu ích khi bạn muốn tập trung vào một phần nhỏ của một chương trình lớn hơn (ví dụ: vì bạn chỉ sở hữu một phần của chương trình đó).

Mã sử dụng nhiều bộ nhớ
Bây giờ, hãy chuyển sang loại hồ sơ "Heap". Nhớ xoá mọi bộ lọc mà bạn đã tạo trong các thử nghiệm trước đó. Bây giờ, bạn sẽ thấy một biểu đồ dạng ngọn lửa, trong đó allocImpl (do alloc gọi) được hiển thị là thành phần chính tiêu thụ bộ nhớ trong ứng dụng:

Bảng tóm tắt phía trên biểu đồ ngọn lửa cho biết tổng dung lượng bộ nhớ đã dùng trong ứng dụng trung bình là ~57,4 MiB, phần lớn dung lượng này được phân bổ bởi hàm allocImpl. Điều này không có gì đáng ngạc nhiên, vì việc triển khai hàm này như sau:
main.go
func allocImpl() {
// Allocate 64 MiB in 64 KiB chunks
for i := 0; i < 64*16; i++ {
mem = append(mem, make([]byte, 64*1024))
}
}Hàm này thực thi một lần, phân bổ 64 MiB thành các khối nhỏ hơn, sau đó lưu trữ con trỏ đến các khối đó trong một biến chung để bảo vệ chúng khỏi bị thu gom rác. Xin lưu ý rằng lượng bộ nhớ mà trình phân tích tài nguyên cho thấy là 64 MiB, hơi khác một chút: trình phân tích tài nguyên vùng nhớ khối xếp Go là một công cụ thống kê, vì vậy, các phép đo có mức hao tổn thấp nhưng không chính xác đến từng byte. Đừng ngạc nhiên khi thấy sự khác biệt khoảng 10% như thế này.
Mã có mức sử dụng I/O cao
Nếu bạn chọn "Luồng" trong bộ chọn loại hồ sơ, màn hình sẽ chuyển sang biểu đồ ngọn lửa, trong đó hầu hết chiều rộng được chiếm bởi các hàm wait và waitImpl:

Trong phần tóm tắt phía trên biểu đồ ngọn lửa, bạn có thể thấy có 100 goroutine tăng ngăn xếp lệnh gọi từ hàm wait. Điều này hoàn toàn chính xác, vì mã khởi tạo các lệnh chờ này có dạng như sau:
main.go
func main() {
...
// Simulate some waiting goroutines.
for i := 0; i < 100; i++ {
go wait()
}Loại hồ sơ này hữu ích khi bạn muốn biết liệu chương trình có tốn thời gian chờ không mong muốn hay không (chẳng hạn như I/O). Thông thường, những ngăn xếp lệnh gọi như vậy sẽ không được trình phân tích tài nguyên CPU lấy mẫu vì chúng không tiêu tốn bất kỳ phần đáng kể nào trong thời gian CPU. Bạn thường muốn sử dụng bộ lọc "Hide stacks" (Ẩn ngăn xếp) với hồ sơ Threads (Luồng) – ví dụ: để ẩn tất cả ngăn xếp kết thúc bằng một lệnh gọi đến gopark, vì đó thường là các goroutine không hoạt động và ít thú vị hơn so với các goroutine chờ trên I/O.
Loại hồ sơ luồng cũng có thể giúp xác định những điểm trong chương trình mà các luồng đang chờ một mutex do một phần khác của chương trình sở hữu trong một khoảng thời gian dài, nhưng loại hồ sơ sau đây sẽ hữu ích hơn cho việc đó.
Mã có mức độ tranh chấp cao
Loại hồ sơ Contention (Tranh chấp) xác định những khoá "được yêu cầu" nhiều nhất trong chương trình. Loại hồ sơ này có sẵn cho các chương trình Go nhưng bạn phải bật rõ ràng bằng cách chỉ định "MutexProfiling: true" trong mã cấu hình tác nhân. Hoạt động thu thập này diễn ra bằng cách ghi lại (theo chỉ số "Tranh chấp") số lần một khoá cụ thể, khi được mở khoá bởi goroutine A, có một goroutine B khác đang chờ khoá được mở khoá. Thao tác này cũng ghi lại (trong chỉ số "Độ trễ") thời gian mà goroutine bị chặn đã chờ khoá. Trong ví dụ này, có một ngăn xếp tranh chấp duy nhất và tổng thời gian chờ khoá là 11,03 giây:

Mã tạo hồ sơ này bao gồm 4 goroutine tranh giành một mutex:
main.go
func contention(d time.Duration) {
contentionImpl(d)
}
func contentionImpl(d time.Duration) {
for {
mu.Lock()
time.Sleep(d)
mu.Unlock()
}
}
...
func main() {
...
for i := 0; i < 4; i++ {
go contention(time.Duration(i) * 50 * time.Millisecond)
}
}Trong phòng thí nghiệm này, bạn đã tìm hiểu cách định cấu hình một chương trình Go để sử dụng với Stackdriver Profiler. Bạn cũng đã tìm hiểu cách thu thập, xem và phân tích dữ liệu hiệu suất bằng công cụ này. Giờ đây, bạn có thể áp dụng kỹ năng mới này cho các dịch vụ thực tế mà bạn chạy trên Google Cloud Platform.
Bạn đã tìm hiểu cách định cấu hình và sử dụng Stackdriver Profiler!
Tìm hiểu thêm
- Stackdriver Profiler: https://cloud.google.com/profiler/
- Gói thời gian chạy/pprof của Go mà Stackdriver Profiler sử dụng: https://golang.org/pkg/runtime/pprof/
Giấy phép
Tác phẩm này được cấp phép theo Giấy phép chung Ghi nhận tác giả theo Creative Commons 2.0.