Hướng dẫn sử dụng Arthas – Công cụ chẩn đoán Java mã nguồn mở từ Alibaba

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ệnhMục đích
helpXem thông tin trợ giúp cho lệnh
catIn nội dung tệp
echoIn tham số
grepTìm kiếm và khớp mẫu
base64Chuyển đổi mã hóa base64
teeSao chép đầu vào tiêu chuẩn ra đầu ra tiêu chuẩn và tệp chỉ định
pwdTrả về thư mục làm việc hiện tại
clsXóa màn hình
sessionXem thông tin phiên hiện tại
resetKhô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)
versionIn phiên bản Arthas mà tiến trình Java đích đang tải
historyIn lịch sử lệnh
quitThoát khỏi client Arthas hiện tại
stopTắt server Arthas, tất cả client sẽ thoát

2.2.2. Lệnh liên quan đến JVM

LệnhMục đíchCách dùng
dashboardIn bảng điều khiển dữ liệu JVM thời gian thựcdashboard
threadIn thông tin stack của các luồng JVMthread
thread <threadId>
thread --state WAITING
jvmIn thông tin hoạt động thời gian thực của JVMjvm
syspropIn và sửa đổi thông tin hệ thống JVMsysprop
sysenvXem biến môi trường JVMsysenv
vmoptionXem và sửa đổi các tùy chọn chẩn đoán JVMvmoption
loggerXem và sửa đổi thông tin loggerlogger
getstaticLấy giá trị của biến tĩnhgetstatic <className> <staticField>
ognlThực thi biểu thức OGNLognl <expression>
mbeanIn thông tin MBeanmbean
heapdumpIn thông tin heap dumpheapdump
heapdump /path/to/dump.hprof
heapdump --live /path/to/dump.hprof
vmtoolTruy vấn đối tượng từ JVM, thực thi forceGcvmtool --action getInstances --className <className>
perfcounterXem thông tin Perf Counter của JVMperfcounter

2.2.3. Lệnh liên quan đến class

LệnhMục đíchCách dùng
scTruy vấn thông tin class đã tải trong JVMsc *<className>*
sc -d *<className>*
sc -d -f *<className>*
smTruy vấn thông tin method đã tải trong JVMsm <className>
sm -f <className>
jadDịch ngược class đã tải trong JVMjad <className>
mcTrình biên dịch trong bộ nhớ, biên dịch tệp .javamc /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
retransformTải tệp .class bên ngoài và retransform vào JVMretransform /path/to/Test.class
redefineTải tệp .class bên ngoài và redefine vào JVMredefine /path/to/Test.class
dumpDump bytecode của class đã tải vào thư mục chỉ địnhdump java.lang.String
dump -d /output/dir java.lang.String
classloaderXem cây kế thừa class loaderclassloader
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ệnhMục đíchCách dùng
watchQuan sát dữ liệu thực thi phương thứcwatch <className> <methodName> "{params,returnObj}" -x 2
monitorGiám sát thực thi phương thứcmonitor -c 5 <className> <methodName>
stackIn đường dẫn gọi của phương thức hiện tạistack <className> <methodName>
traceIn đường dẫn gọi bên trong phương thức và thời gian thực thitrace <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ọitt -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:

  1. Dịch ngược file .class thành mã nguồn.
  2. Chỉnh sửa mã nguồn.
  3. Biên dịch lại mã nguồn thành file .class.
  4. 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 threaddumpdumpheap.

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_OnLoadAgent_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

  1. JVM tải thư viện Instrumentation.
  2. Gọi Agent_OnLoad.
  3. Tạo đối tượng Instrumentation.
  4. Đăng ký lắng nghe sự kiện ClassFileLoadHook.
  5. Gọi phương thức premain của class được chỉ định trong MANIFEST.MF.
  6. Phương thức premain nhận đối tượng Instrumentation và có thể thực hiện các biến đổi bytecode.

Thẻ: Arthas Java JVMTI Instrumentation bytecode

Đăng vào ngày 18 tháng 5 lúc 05:48