Gỡ lỗi ứng dụng Spring WebFlux có thể là một nhiệm vụ đầy thách thức, đặc biệt là khi xử lý các luồng phản ứng phức tạp. Không giống như các ứng dụng chặn truyền thống, trong đó theo dõi ngăn xếp cung cấp dấu hiệu rõ ràng về nguyên nhân cốt lõi của sự cố, các ứng dụng phản ứng có thể khó gỡ lỗi hơn. Các vấn đề như mã chặn, sự cố đồng thời và điều kiện chủng tộc đều có thể gây ra các lỗi tinh vi khó chẩn đoán.
Khi xử lý một lỗi, không phải lúc nào vấn đề cũng liên quan đến mã. Nó có thể là một tập hợp các yếu tố, chẳng hạn như tái cấu trúc gần đây, thay đổi nhóm, thời hạn khó khăn, v.v. Trong cuộc sống thực, việc khắc phục sự cố các ứng dụng lớn được tạo bởi những người đã rời công ty một thời gian trước đây và bạn mới tham gia là một điều phổ biến.
Biết một chút về miền và công nghệ sẽ không giúp cuộc sống của bạn dễ dàng hơn.
Trong ví dụ mã bên dưới, tôi muốn tưởng tượng mã lỗi có thể trông như thế nào đối với một người mới tham gia nhóm.
Hãy coi việc gỡ lỗi mã này giống như một hành trình hơn là thử thách. Nguyên nhân gốc rễ có thể rõ ràng đối với những người đã quen thuộc với các ứng dụng Phản ứng. Tuy nhiên, một số thực hành dưới đây vẫn có thể rất hữu ích để sửa đổi.
@GetMapping("/greeting/{firstName}/{lastName}") public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { return Flux.fromIterable(Arrays.asList(firstName, lastName)) .filter(this::wasWorkingNiceBeforeRefactoring) .transform(this::senselessTransformation) .collect(Collectors.joining()) .map(names -> "Hello, " + names); } private boolean wasWorkingNiceBeforeRefactoring(String aName) { // We don't want to greet with John, sorry return !aName.equals("John"); } private Flux<String> senselessTransformation(Flux<String> flux) { return flux .single() .flux() .subscribeOn(Schedulers.parallel()); }
Vì vậy, những gì đoạn mã này thực hiện là: Nó thêm “Xin chào,” vào các tên được cung cấp dưới dạng tham số.
Đồng nghiệp John của bạn đang nói với bạn rằng mọi thứ đều hoạt động trên máy tính xách tay của anh ấy. Đúng:
> curl localhost:8080/greeting/John/Doe > Hello, Doe
Nhưng khi bạn chạy nó như curl localhost:8080/greeting/Mick/Jagger
, bạn sẽ thấy stacktrace tiếp theo:
java.lang.IndexOutOfBoundsException: Source emitted more than one item at reactor.core.publisher.MonoSingle$SingleSubscriber.onNext(MonoSingle.java:134) ~[reactor-core-3.5.5.jar:3.5.5] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ Handler com.example.demo.controller.GreetingController#greeting(String, String) [DispatcherHandler] *__checkpoint ⇢ HTTP GET "/greeting/Mick/Jagger" [ExceptionHandlingWebHandler] Original Stack Trace: <18 internal lines> at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na] (4 internal lines)
Tốt, không có dấu vết nào dẫn đến mẫu mã ở trên.
Tất cả những gì nó tiết lộ là 1) nó xảy ra trong phương thức GreetingController#greeting
, và 2) máy khách đã thực hiện `HTTP GET "/greeting/Mick/Jagger
Điều đầu tiên và dễ dàng nhất để thử là thêm lệnh gọi lại `.doOnError()` vào cuối chuỗi lời chào.
@GetMapping("/greeting/{firstName}/{lastName}") public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { return Flux.fromIterable(Arrays.asList(firstName, lastName)) // <...> .doOnError(e -> logger.error("Error while greeting", e)); }
Cố gắng tốt, nhưng các bản ghi không cho thấy bất kỳ sự cải thiện nào.
Tuy nhiên, dấu vết ngăn xếp nội bộ của Lò phản ứng:
doOnError
có thể/không thể hữu ích trong quá trình gỡ lỗi: Ghi nhật ký : Bạn có thể sử dụng doOnError
để ghi nhật ký thông báo lỗi và cung cấp thêm ngữ cảnh về những gì đã xảy ra trong luồng phản ứng của bạn. Điều này có thể đặc biệt hữu ích khi gỡ lỗi sự cố trong một luồng phức tạp có nhiều toán tử.
Khôi phục : doOnError
cũng có thể được sử dụng để khôi phục lỗi và tiếp tục xử lý luồng. Ví dụ: bạn có thể sử dụng onErrorResume
để cung cấp giá trị hoặc luồng dự phòng trong trường hợp có lỗi.
Gỡ lỗi : rất có thể doOnError
sẽ không cung cấp bất kỳ dấu vết ngăn xếp nào tốt hơn ngoại trừ những gì bạn đã thấy trong nhật ký. Đừng dựa vào nó như một trình khắc phục sự cố tốt.
Điểm dừng tiếp theo là thay thế lệnh gọi phương thức doOnError()
đã thêm trước đó bằng lệnh gọi phương thức log()
. Đơn giản như nó được. log()
quan sát tất cả các tín hiệu Luồng phản ứng và theo dõi chúng vào các bản ghi dưới cấp INFO theo mặc định.
Chúng ta có thể xem những phương thức Phản ứng nào đã được gọi ( onSubscribe
, request
và onError
). Ngoài ra, việc biết các luồng (nhóm) mà các phương thức này đã được gọi từ đâu có thể là thông tin rất hữu ích. Tuy nhiên, nó không liên quan đến trường hợp của chúng tôi.
Giới thiệu về nhóm chủ đề
Tên chủ đề ctor-http-nio-2
là viết tắt của reactor-http-nio-2
. Các phương thức phản hồi onSubscribe()
và request()
đã được thực thi trên nhóm luồng IO (bộ lập lịch). Các tác vụ này đã được thực thi ngay lập tức trên một chuỗi đã gửi chúng.
Bằng cách có .subscribeOn(Schedulers.parallel())
bên trong senselessTransformation
, chúng tôi đã hướng dẫn Reactor đăng ký các phần tử khác trên nhóm luồng khác. Đó là lý do tại sao onError
đã được thực thi trên parallel-1
song song.
Bạn có thể đọc thêm về thread pool trong bài viết này .
log()
cho phép bạn thêm các câu lệnh ghi nhật ký vào luồng của mình, giúp theo dõi luồng dữ liệu và chẩn đoán sự cố dễ dàng hơn. Nếu chúng tôi có luồng dữ liệu phức tạp hơn với những thứ như FlatMap, chuỗi con, chặn cuộc gọi, v.v., thì chúng tôi sẽ được hưởng lợi nhiều từ việc đăng nhập tất cả. Đó là một điều rất dễ dàng và tốt đẹp để sử dụng hàng ngày. Tuy nhiên, chúng ta vẫn chưa biết nguyên nhân gốc rễ.
Hướng dẫn Hooks.onOperatorDebug()
yêu cầu Reactor bật chế độ gỡ lỗi cho tất cả các toán tử trong luồng phản ứng, cho phép thông báo lỗi chi tiết hơn và dấu vết ngăn xếp.
Khi các lỗi được phát hiện sau này, chúng sẽ được bổ sung thêm bằng một Ngoại lệ bị chặn nêu chi tiết ngăn xếp dây chuyền lắp ráp ban đầu. Phải được gọi trước khi các nhà sản xuất (ví dụ: Flux.map, Mono.fromCallable) thực sự được gọi để chặn đúng thông tin ngăn xếp.
Hướng dẫn nên được gọi một lần mỗi lần chạy. Một trong những nơi tốt nhất sẽ là các lớp Cấu hình hoặc Chính. Đối với trường hợp sử dụng của chúng tôi, nó sẽ là:
public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { Hooks.onOperatorDebug(); return // <...> }
Bằng cách thêm Hooks.onOperatorDebug()
cuối cùng chúng ta cũng có thể đạt được tiến bộ trong cuộc điều tra của mình. Stacktrace hữu ích hơn nhiều:
Và trên dòng 42, chúng tôi có single()
.
Đừng cuộn lên, biến senselessTransformation
sẽ xuất hiện tiếp theo:
private Flux<String> senselessTransformation(Flux<String> flux) { return flux .single() // line 42 .flux() .subscribeOn(Schedulers.parallel()); }
Đó là nguyên nhân sâu xa.
single()
phát ra một mục từ nguồn Flux hoặc báo hiệu IndexOutOfBoundsException
cho một nguồn có nhiều hơn một phần tử. Điều đó có nghĩa là thông lượng trong phương thức phát ra nhiều hơn 1 mục. Bằng cách đi lên trong phân cấp cuộc gọi, chúng ta thấy rằng ban đầu có một Flux với hai phần tử Flux.fromIterable(Arrays.asList(firstName, lastName))
.
Phương pháp lọc wasWorkingNiceBeforeRefactoring
loại bỏ một mục khỏi một dòng khi nó bằng John . Đó là lý do tại sao mã này hoạt động cho một trường đại học tên là John. Huh.
Hooks.onOperatorDebug()
có thể đặc biệt hữu ích khi gỡ lỗi các luồng phản ứng phức tạp, vì nó cung cấp thông tin chi tiết hơn về cách luồng đang được xử lý. Tuy nhiên, việc bật chế độ gỡ lỗi có thể ảnh hưởng đến hiệu suất ứng dụng của bạn (do dấu vết ngăn xếp đã điền), do đó, chế độ này chỉ nên được sử dụng trong quá trình phát triển và gỡ lỗi chứ không phải trong quá trình sản xuất.
Để đạt được hiệu quả gần như tương tự như Hooks.onOperatorDebug()
mang lại với tác động hiệu suất tối thiểu, có một toán tử checkpoint()
đặc biệt. Nó sẽ bật chế độ gỡ lỗi cho phần đó của luồng, trong khi phần còn lại của luồng không bị ảnh hưởng.
public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { return Flux.fromIterable(Arrays.asList(firstName, lastName)) .filter(this::wasWorkingNiceBeforeRefactoring) /* new */ .checkpoint("After filtering") .transform(this::senselessTransformation) /* new */ .checkpoint("After transformation") .collect(Collectors.joining()) .map(names -> "Hello, " + names); }
Hãy nhìn vào các bản ghi:
Bảng phân tích điểm kiểm tra này cho chúng tôi biết rằng lỗi đã được quan sát thấy sau điểm kiểm tra thứ hai được mô tả là Sau khi chuyển đổi . Điều đó không có nghĩa là điểm kiểm tra đầu tiên chưa đạt được trong quá trình thực thi. Đúng như vậy, nhưng lỗi chỉ bắt đầu xuất hiện sau lỗi thứ hai. Đó là lý do tại sao chúng tôi không thấy Sau khi lọc .
Bạn cũng có thể thấy thêm hai điểm kiểm tra được đề cập trong bảng phân tích, từ và . Họ đã đạt được sau cuộc gọi mà chúng tôi đã đặt, theo thứ bậc cuộc gọi.
Bên cạnh mô tả, bạn có thể buộc Reactor tạo stacktrace cho điểm kiểm tra của mình bằng cách thêm true
làm tham số thứ hai cho phương thức checkpoint()
. Điều quan trọng cần lưu ý là stacktrace được tạo sẽ dẫn bạn đến dòng có điểm kiểm tra. Nó sẽ không điền vào stacktrace cho ngoại lệ ban đầu. Vì vậy, nó không có nhiều ý nghĩa vì bạn có thể dễ dàng tìm thấy điểm kiểm tra bằng cách cung cấp mô tả.
Bằng cách làm theo các phương pháp hay nhất này, bạn có thể đơn giản hóa quy trình gỡ lỗi cũng như nhanh chóng xác định và giải quyết các sự cố trong ứng dụng Spring WebFlux của mình. Cho dù bạn là nhà phát triển dày dạn kinh nghiệm hay mới bắt đầu lập trình phản ứng, những mẹo này sẽ giúp bạn cải thiện chất lượng và độ tin cậy của mã, đồng thời mang lại trải nghiệm tốt hơn cho người dùng của bạn.