1. Giới thiệu về Arthas
Arthas là một công cụ chẩn đoán Java mã nguồn mở do Alibaba phát triển. Nó sử dụng giao diện dòng lệnh tương tác để giúp lập trình viên điều tra và giải quyết các vấn đề liên quan đến JVM (Java Virtual Machine).
Arthas cung cấp các tính năng chính sau:
- Giám sát thời gian thực trạng thái hoạt động của JVM.
- Xem thông tin về các lớp đã được tải và các class loader tương ứng.
- Sử dụng kỹ thuật tăng cường bytecode để giám sát và thống kê việc thực thi phương thức.
2. Cách sử dụng Arthas
2.1. Cài đặt và khởi chạy Arthas
Bản chất Arthas cũng là một chương trình Java. Bạn không cần thực hiện quy trình cài đặt phức tạp, chỉ cần tệp JAR và chạy nó bằng lệnh java -jar.
Tải tệp arthas-boot.jar:
wget https://arthas.aliyun.com/arthas-boot.jar
Khởi chạy Arthas:
java -jar arthas-boot.jar
Sau khi chạy, Arthas sẽ hiển thị danh sách tất cả các tiến trình Java đang chạy trên máy, kèm theo số thứ tự. Bạn chỉ cần nhập số tương ứng để chọn tiến trình cần kết nối. Ví dụ, nhập 1 để chọn tiến trình đầu tiên.
Khi kết nối thành công, bạn sẽ vào chế độ tương tác của Arthas. Lúc này, bạn không thể nhập các lệnh hệ thống thông thường mà chỉ có thể sử dụng các lệnh của Arthas. Gõ help để xem danh sách tất cả các lệnh được hỗ trợ.
2.2. Tổng quan về các lệnh Arthas
Các lệnh Arthas được chia thành nhiều loại: lệnh cơ bản, lệnh liên quan đến JVM, lệnh liên quan đến class, và lệnh giám sát.
2.2.1. Lệnh cơ bản
| Lệnh | Mục đích |
|---|---|
| help | Xem thông tin trợ giúp cho lệnh |
| cat | In nội dung tệp |
| echo | In tham số |
| grep | Tìm kiếm và khớp mẫu |
| base64 | Chuyển đổi mã hóa base64 |
| tee | Sao chép đầu vào tiêu chuẩn ra đầu ra tiêu chuẩn và tệp chỉ định |
| pwd | Trả về thư mục làm việc hiện tại |
| cls | Xóa màn hình |
| session | Xem thông tin phiên hiện tại |
| reset | Khôi phục các lớp đã bị tăng cường bởi Arthas về trạng thái ban đầu (tự động thực hiện khi tắt Arthas) |
| version | In phiên bản Arthas mà tiến trình Java đích đang tải |
| history | In lịch sử lệnh |
| quit | Thoát khỏi client Arthas hiện tại |
| stop | Tắt server Arthas, tất cả client sẽ thoát |
2.2.2. Lệnh liên quan đến JVM
| Lệnh | Mục đích | Cách dùng |
|---|---|---|
| dashboard | In bảng điều khiển dữ liệu JVM thời gian thực | dashboard |
| thread | In thông tin stack của các luồng JVM | thread thread <threadId> thread --state WAITING |
| jvm | In thông tin hoạt động thời gian thực của JVM | jvm |
| sysprop | In và sửa đổi thông tin hệ thống JVM | sysprop |
| sysenv | Xem biến môi trường JVM | sysenv |
| vmoption | Xem và sửa đổi các tùy chọn chẩn đoán JVM | vmoption |
| logger | Xem và sửa đổi thông tin logger | logger |
| getstatic | Lấy giá trị của biến tĩnh | getstatic <className> <staticField> |
| ognl | Thực thi biểu thức OGNL | ognl <expression> |
| mbean | In thông tin MBean | mbean |
| heapdump | In thông tin heap dump | heapdump heapdump /path/to/dump.hprof heapdump --live /path/to/dump.hprof |
| vmtool | Truy vấn đối tượng từ JVM, thực thi forceGc | vmtool --action getInstances --className <className> |
| perfcounter | Xem thông tin Perf Counter của JVM | perfcounter |
2.2.3. Lệnh liên quan đến class
| Lệnh | Mục đích | Cách dùng |
|---|---|---|
| sc | Truy vấn thông tin class đã tải trong JVM | sc *<className>* sc -d *<className>* sc -d -f *<className>* |
| sm | Truy vấn thông tin method đã tải trong JVM | sm <className> sm -f <className> |
| jad | Dịch ngược class đã tải trong JVM | jad <className> |
| mc | Trình biên dịch trong bộ nhớ, biên dịch tệp .java | mc /path/to/Test.java mc -c <classLoaderHash> /path/to/Test.java mc --classLoadClass <classLoaderClassName> /path/to/Test.java mc -d /output/dir /path/to/Test.java |
| retransform | Tải tệp .class bên ngoài và retransform vào JVM | retransform /path/to/Test.class |
| redefine | Tải tệp .class bên ngoài và redefine vào JVM | redefine /path/to/Test.class |
| dump | Dump bytecode của class đã tải vào thư mục chỉ định | dump java.lang.String dump -d /output/dir java.lang.String |
| classloader | Xem cây kế thừa class loader | classloader classloader -l classloader -t classloader -c <classLoaderHash> --load <className> |
2.2.4. Lệnh giám sát
Các lệnh giám sát sử dụng kỹ thuật tăng cường bytecode để chèn logic giám sát vào class mục tiêu. Sau khi giám sát xong, bạn cần chạy lệnh reset để loại bỏ phần tăng cường.
| Lệnh | Mục đích | Cách dùng |
|---|---|---|
| watch | Quan sát dữ liệu thực thi phương thức | watch <className> <methodName> "{params,returnObj}" -x 2 |
| monitor | Giám sát thực thi phương thức | monitor -c 5 <className> <methodName> |
| stack | In đường dẫn gọi của phương thức hiện tại | stack <className> <methodName> |
| trace | In đường dẫn gọi bên trong phương thức và thời gian thực thi | trace <className> <methodName> |
| tt | "Cỗ máy thời gian" ghi lại dữ liệu thực thi phương thức, lưu tham số đầu vào và giá trị trả về cho mỗi lần gọi | tt -t <className> <methodName> |
2.3. Chi tiết các lệnh Arthas
2.3.1. Dashboard (Bảng điều khiển)
Cú pháp: dashboard [i:] [n:]
dashboard # Mặc định in dữ liệu JVM mỗi 5 giây
dashboard -i 2000 # In dữ liệu JVM mỗi 2 giây
dashboard -n 10 # In dữ liệu JVM 10 lần
dashboard -i 1000 -n 10 # In dữ liệu JVM mỗi 1 giây, tổng cộng 10 lần
Lệnh này hiển thị thông tin JVM theo thời gian thực, bao gồm ba phần: trạng thái luồng, sử dụng bộ nhớ và thông tin môi trường.
- Luồng: Hiển thị tất cả các luồng, bao gồm ID, tên, mức ưu tiên, trạng thái, tỷ lệ sử dụng CPU, thời gian sử dụng CPU, v.v.
- Bộ nhớ: Hiển thị mức sử dụng bộ nhớ heap và non-heap, cùng với số lần và thời gian GC.
- Môi trường: Hiển thị thông tin hệ điều hành, phiên bản JDK và các thông tin môi trường khác.
2.3.2. Thread (Thông tin luồng)
Lệnh thread cho phép bạn xem thông tin tất cả các luồng đang chạy, và xem stack của một luồng cụ thể bằng ID.
Cú pháp:
thread # Xem tất cả luồng
thread <id> # Xem thông tin luồng theo ID
thread -n <n> # Xem n luồng "bận" nhất
thread -i <i> # Thống kê thời gian CPU của luồng trong i mili giây gần đây
thread -n <n> -i <i> # Liệt kê stack của n luồng bận nhất trong i mili giây gần đây
thread -b # Tìm luồng đang chặn các luồng khác
thread --state [RUNNABLE|WAITING|...] # Xem luồng theo trạng thái
Ví dụ:
thread
thread 1
thread -n 10
thread -b
thread --state WAITING
Lệnh thread -b đặc biệt hữu ích để tìm luồng đang giữ khóa và chặn các luồng khác (hiện chỉ hỗ trợ synchronized).
2.3.3. JVM (Xem thông tin JVM)
Cú pháp: jvm
Lệnh này in thông tin JVM thời gian thực, bao gồm môi trường, thống kê class, bộ nhớ, GC, luồng, và file descriptor.
- THREAD: COUNT (số luồng đang hoạt động), DAEMON-COUNT (số luồng daemon), PEAK-COUNT (số luồng tối đa từ khi khởi động), STARTED-COUNT (tổng số luồng đã khởi tạo), DEADLOCK-COUNT (số luồng bị deadlock).
- FILE DESCRIPTOR: MAX-FILE-DESCRIPTOR-COUNT (số file descriptor tối đa), OPEN-FILE-DESCRIPTOR-COUNT (số file descriptor đang mở).
2.3.4. Jad (Dịch ngược)
Cú pháp:
jad <đường_dẫn_đầy_đủ_của_class> # Dịch ngược toàn bộ class
jad <đường_dẫn_đầy_đủ_của_class> <tên_method> # Dịch ngược một phương thức
Ví dụ:
jad com.example.ArthasDemo
2.3.5. Sc (Xem class đã tải)
Cú pháp: sc [-d] [-f] *<className>*
-d in thông tin chi tiết, -f in thông tin thuộc tính. Hỗ trợ tìm kiếm mờ.
Ví dụ:
sc -d -f *Service
Lệnh này hiển thị đường dẫn nguồn, khai báo class, class loader, v.v. Nếu một class được tải bởi nhiều class loader, nó sẽ xuất hiện nhiều lần.
2.3.6. Sm (Xem phương thức của class đã tải)
Cú pháp:
sm <đường_dẫn_đầy_đủ_của_class>
sm -d <đường_dẫn_đầy_đủ_của_class>
sm -d <đường_dẫn_đầy_đủ_của_class> <tên_method>
Ví dụ:
sm -d java.math.RoundingMode
2.3.7. Getstatic (Xem thuộc tính tĩnh)
Cú pháp: getstatic <đường_dẫn_đầy_đủ_của_class> <tên_thuộc_tính_tĩnh>
2.3.8. Watch (Giám sát thực thi phương thức)
Cú pháp và các tùy chọn:
watch <className> <method> # Quan sát thực thi, trả về thông tin như thời gian, giá trị trả về
watch <className> <method> "{params,returnObj}" -x <x> # Quan sát tham số và kết quả trả về, độ sâu x
watch <className> <method> "{params,returnObj}" -b # Quan sát trước khi gọi phương thức
watch <className> <method> "{params,returnObj}" -b -s -n <n> # Quan sát cả trước và sau, n lần
watch <className> <method> "{params,throwExp}" -e # Quan sát khi có ngoại lệ
watch <className> <method> "{params,target}" -b -s # Quan sát tham số và đối tượng hiện tại
watch <className> <method> "{params,target.fieldName}" -b -s # Quan sát tham số và giá trị thuộc tính của đối tượng
watch <className> <method> "{params}" '#cost>100' -f # Quan sát tham số khi thời gian thực thi > 100ms
Lệnh watch có 4 điểm quan sát: -b (trước khi gọi), -e (sau khi có ngoại lệ), -s (sau khi trả về), -f (sau khi kết thúc). Mặc định -f được bật.
2.3.9. Monitor (Giám sát thực thi phương thức)
Cú pháp:
monitor -c <số_giây> <className> <methodName> # Thống kê định kỳ mỗi second giây
monitor -c <số_giây> -b <className> <methodName> 'params[1] > 1' # Thống kê với điều kiện
Khác với watch (trả về ngay), monitor là lệnh không trả về ngay lập tức; nó chạy nền cho đến khi bạn nhấn Ctrl+C. Nó thống kê các chỉ số như timestamp, class, method, total, success, fail, RT trung bình, tỷ lệ thất bại.
2.3.10. Trace (Đường dẫn gọi phương thức)
Cú pháp:
trace <className> <methodName> # In đường dẫn gọi thời gian thực
trace <className> <methodName> --skipJDKMethod false # Bao gồm cả phương thức JDK
trace <className> <method> '#cost > 100' # Lọc các cuộc gọi có thời gian > 100ms
trace <className> <method> '#cost > 100' -n <n> # Giới hạn số lần giám sát
Lệnh này hiển thị từng bước trong chuỗi gọi, kèm số dòng và thời gian thực thi.
2.3.11. Stack (In đường dẫn gọi đến phương thức hiện tại)
Cú pháp:
stack <className> <methodName> # In đường dẫn gọi đến phương thức
stack <className> <methodName> 'params[0] > 1' # Lọc theo tham số
stack <className> <methodName> '#cost > 100' # Lọc theo thời gian
Khi một phương thức có thể được gọi từ nhiều nơi, lệnh này giúp xác định người gọi.
3. Ví dụ sử dụng Arthas
Ví dụ 1: Điều tra ngoại lệ thực thi phương thức
Tình huống: Một API đôi khi trả về lỗi 500. Cần kiểm tra tham số cụ thể khi lỗi xảy ra.
Sử dụng watch với tùy chọn -e để in tham số và thông tin ngoại lệ khi có lỗi:
watch <class_pattern> <method_pattern> "{params, throwExp}" -e
Ví dụ 2: Cập nhật mã nóng (Hot update)
Tình huống: Một lỗi nhỏ trên production cần sửa nhanh mà không muốn triển khai lại toàn bộ.
Các bước thực hiện:
- Dịch ngược file .class thành mã nguồn.
- Chỉnh sửa mã nguồn.
- Biên dịch lại mã nguồn thành file .class.
- Thay thế file .class trong JVM.
Giả sử class TestController có phương thức getUserName:
@RestController
@RequestMapping(value = "/testUser")
public class TestController {
@RequestMapping(value = "/detail", method = RequestMethod.GET)
public String getUserName(@RequestParam("userId") String userId){
return "name is :" + Long.valueOf(userId);
}
}
Bước 1: Dịch ngược class:
jad --source-only com.example.TestController > /tmp/TestController.java
Bước 2: Sửa file: thay đổi kiểu tham số userId từ String thành Long (hoặc thêm try-catch).
Bước 3: Tìm class loader hash và biên dịch lại:
sc -d *TestController | grep 'classLoader'
mc -c <hash> /tmp/TestController.java -d /tmp
Bước 4: Nạp file .class mới vào JVM:
redefine /tmp/com/example/TestController.class
Lưu ý quan trọng: Khi dùng redefine, bạn không được thay đổi số lượng phương thức, kiểu tham số, kiểu trả về. Chỉ được sửa đổi logic bên trong phương thức. Do đó, thay vì đổi kiểu tham số, bạn nên thêm try-catch bên trong:
@RestController
@RequestMapping(value={"/testUser"})
public class TestController {
@RequestMapping(value={"/detail"}, method={RequestMethod.GET})
public String getUserName(@RequestParam(value="userId") String userId) {
Long id = null;
try {
id = Long.parseLong(userId);
} catch (Exception e) { }
return "name is :" + id;
}
}
Ví dụ 3: Điều tra request chậm
Tình huống: Một API phản hồi chậm, khó xác định nguyên nhân từ mã nguồn.
Dùng trace để xem đường dẫn gọi và thời gian thực thi từng bước:
trace com.example.controller.SomeController * '#cost > 1'
Kết quả sẽ cho thấy tầng Service mất nhiều thời gian nhất. Sau đó, tiếp tục trace vào Service method để đi sâu hơn, cuối cùng xác định được điểm nghẽn là tầng Mapper (câu lệnh SQL). Từ đó, bạn có thể tối ưu SQL.
4. Nguyên lý hoạt động của Arthas
4.1. Cơ chế Attach của JVM
4.1.1. Nguyên lý
Cơ chế Attach cho phép một tiến trình JVM giao tiếp và gửi lệnh đến một tiến trình JVM khác. Đây là nền tảng cho các công cụ như jstack, jmap, debug, v.v.
Hai luồng quan trọng trong cơ chế này là:
- Attach Listener: Nhận và xử lý các lệnh từ bên ngoài.
- Signal Dispatcher: Phân phối tín hiệu đến các luồng khác.
Khi một tiến trình bên ngoài attach vào JVM đích, nó gửi tín hiệu sigquit. Signal Dispatcher xử lý tín hiệu này và tạo ra Attach Listener. Attach Listener sau đó tạo một socket file (/tmp/.java_pid) để nhận lệnh.
4.1.2. Sử dụng cơ chế Attach
JDK cung cấp class VirtualMachine để thực hiện attach:
VirtualMachine vm = VirtualMachine.attach("1357"); // Attach vào tiến trình PID 1357
Các phương thức như remoteDataDump(), dumpHeap() tương ứng với các lệnh threaddump và dumpheap.
4.2. Java Agent
Trong khi cơ chế Attach chỉ đọc dữ liệu, Java Agent cho phép sửa đổi bytecode đã tải. Arthas sử dụng Java Agent để thực hiện các thao tác như redefine class.
Java Agent có thể can thiệp vào bytecode ở hai thời điểm:
- Trước khi chạy main: Thông qua phương thức
premain. - Trong lúc chạy: Thông qua cơ chế Attach và phương thức
agentmain.
4.2.1. JVMTI và JVMTI Agent
- JVMTI (JVM Tool Interface): Giao diện C/C++ cho phép tương tác với JVM. Nó hoạt động dựa trên sự kiện (event-driven).
- JVMTI Agent: Một thư viện động (dynamic library) đóng vai trò trung gian, cho phép Java tương tác với JVMTI. Nó cung cấp các hàm như
Agent_OnLoad,Agent_OnAttach.
4.2.2. Instrumentation
JDK 6 giới thiệu Instrumentation, một JVMTI Agent được tích hợp sẵn. Nó triển khai Agent_OnLoad và Agent_OnAttach, cho phép nạp Java Agent khi JVM khởi động (qua tham số -javaagent) hoặc trong lúc chạy (qua VirtualMachine.loadAgent()).
4.2.3. Quy trình làm việc của Java Agent khi khởi động JVM
- JVM tải thư viện Instrumentation.
- Gọi
Agent_OnLoad. - Tạo đối tượng
Instrumentation. - Đăng ký lắng nghe sự kiện
ClassFileLoadHook. - Gọi phương thức
premaincủa class được chỉ định trongMANIFEST.MF. - Phương thức
premainnhận đối tượngInstrumentationvà có thể thực hiện các biến đổi bytecode.