Spring WebFlux的明顯陷阱 - ŁukaszKyć

banq發表於2019-11-08

幾個月前,我們開始了一個新專案。我們的目標是設計一個可以處理許多併發連線的微服務。我們預測該應用程式將花費大量時間等待多個並行I / O操作。理想的體系結構解決方案似乎使用了非阻塞方法。經過簡短的調查,我們決定使用Spring WebFlux作為主要框架。這是因為它基於無阻塞堆疊,具有出色的可伸縮性和靈活性。流stream形式的出色抽象看起來真的很方便使用,遠比基於Java NIONetty的傳統非阻塞程式碼要好得多。這就是為什麼我們嘗試基於Spring WebFlux實施該服務的原因。但是,一旦我們的開發工作開始,麻煩便隨之而來。

新思維
如果您習慣於以傳統的順序方式編寫程式碼,則可能會感到有些迷茫。那是我們開始該專案時的印象。有時候,簡單的事情似乎很困難。最令人驚訝的是,即使是經驗豐富的Java開發人員也不得不重新發明輪子。對於他們來說,這是一個新的程式設計範例,經過多年的不同程式設計方式轉換到它並不容易。事實是,反應流可以使某些任務大大簡化,但其他任務似乎要複雜得多。此外,以運算子序列的形式編寫的程式碼,例如map或flatMap,不像傳統的逐行阻塞方法呼叫那樣容易理解。您不能簡單地將任何一段程式碼提取到私有方法中以使其更具可讀性。您必須在流上使用運算子序列。
我的觀察是,反應靈敏的流stream對於思想開闊的開發人員而言不那麼痛苦。雖然學習曲線絕對陡峭,但我認為值得花時間。

記錄上下文資訊
我們的技術要求之一是可追溯服務之間的對話。相關ID模式正是我們所需要的東西。簡而言之:每條傳入的訊息都附加了一個唯一的文字值,它是在我們服務之外的某個位置生成的對話識別符號。在我們的情況下,我們有兩種型別的傳入訊息:

  • HTTP請求。
  • 從SQS(Amazon簡單佇列服務)佇列使用的訊息。

為了提供可追溯性,我們必須在每條日誌行的旁邊包括相關性ID,該ID由在給定對話上下文中的程式碼執行列印出來。如果參與對話的每個服務都列印一個“相關性ID”,則很容易找到與其相關的所有日誌。日誌聚合器很有幫助,因為它將所有日誌收集在一個地方,提供了便捷的搜尋功能。
最常用的日誌記錄庫實現了“ 對映診斷上下文”(MDC)功能,該功能完全符合我們的需求。
在Servlet的世界中,我們將編寫一個過濾器來處理每個傳入的HTTP請求。過濾器將在請求處理程式(即Spring控制器)啟動之前將Correlation ID放入MDC,並在請求處理完成後清理對映。它將完美地工作,但請記住,MDC使用執行緒相似性模式(實現為ThreadLocalJava類)中,儲存與單個執行緒關聯的上下文資訊。這意味著只能在單個執行緒從開始接受請求和到處理請求結束時使用。
不幸的是,Spring WebFlux不能那樣工作。一個執行緒很可能有多個執行緒參與單個請求處理。那我們該怎麼辦呢?
幸運的是,Reactor庫的作者實現了另一種儲存上下文資訊的機制-Context。它可用於在流的運算子之間傳輸相關ID。要將其與知名的日誌記錄庫MDC連結,我們使用此處描述的解決方案: https://www.novatec-gmbh.de/en/blog/how-can-the-mdc-context-be-used-in-the-reactive-spring-applications/.  如果您不喜歡Kotlin語言,請檢視Java中類似的實現: https://github.com/archie-swif/webflux-mdc
我們實現了這個想法,並且效果很好!我們只需要建立一個自定義WebFilter,即可從HTTP請求中讀取Correlation ID並將其設定在響應流的上下文中。我們發現了一個缺點-由於隱式呼叫MdcContextLifter,堆疊跟蹤變得更長了。

很長的堆疊跟蹤
反應流的優點在於它們形成宣告鏈,告訴釋出者發出的每個元素髮生了什麼。這種抽象允許清晰地實現複雜的邏輯。如果程式碼按預期工作,則一切看起來都很光彩。您僅用幾行程式碼就解決了一個非同尋常的問題,您為此感到自豪。
這種快樂一直持續到有人從生產中向您傳送堆疊跟蹤為止。您看到的絕大多數行都以Reactor.core.publisher(以及包含MdcContextLifter class的軟體包中的其他行)開頭,如果您實現了上一節中提到的上下文資訊記錄的變通方法。它不會告訴您任何資訊,因為它是與您的程式碼無關的程式包。我會說這沒用。

此外,我還記得我們遇到的一個非常嚴重的問題。使用者抱怨該應用程式偶爾會返回錯誤,但我們無法弄清楚原因。日誌沒有任何有趣的內容,儘管我們知道應用程式會捕獲所有可能的錯誤並使用stacktrace記錄錯誤級別。
那是什麼問題呢?我們使用Papertrail作為日誌聚合器。將應用程式日誌推送到Papertrail的轉發器將日誌條目的最大大小限制為100,000位元組。這聽起來綽綽有餘,但在我們看來並非如此。Stacktrace更大,並且充滿了不相關的資訊,例如包reacte.code.publisher中的上述方法呼叫。如果Papertrail轉發器看到很長的訊息,則直接跳過它。這就是最重要的日誌條目被忽略的原因。那我們做了什麼?我們僅實現了對Logback的擴充套件,該擴充套件刪除了包含有包的程式行,這些程式包從react.core.publisher開始,並將stacktrace修剪為90,000個字元。

我的結論是,需要進一步的努力來實現類似的功能,尤其是如果您不想看到不相關的資訊並且認真考慮降低問題的風險時。

Swagger
我們希望向我們的服務使用者提供發現RESTful API的能力。Swagger是最著名且最成熟的工具。不幸的是,當我們看到Spring WebFlux沒有整合庫時,我們再次感到驚訝。經過一番調查後,它發現整合方面的開發仍在進行中,公開Swagger UI的唯一方法是使用io.springfox:springfox-spring-webflux的快照版本。在生產就緒型應用程式中使用不穩定的庫聽起來很可怕,但是,另一方面,Swagger不是我們服務的關鍵元件,只有在非生產部署中才啟用。我們使用快照版本已有六個多月的時間了,目前還沒有穩定的版本。真是可悲。

資料庫
如果您很幸運,並且擁有MongoDB,那麼您就不必擔心。有一個官方的反應式驅動程式可用。由於JDBC的阻塞性質,因此沒有對關聯式資料庫的任何反應式支援。這正是我們面臨的問題。客戶告訴我們,我們必須在MySQL風味中使用Aurora DB。最糟糕的是,當我們解決了上述所有問題時,它處於專案的後期,因此現在回到非反應堆已為時已晚。我們決定要做的是使用官方的MySQL驅動程式,因為我們熟悉它,並且我們可以非常快地釋出該服務。我們意識到了所有侷限性,包括缺乏對事務性的現成支援Spring中的註解,用於返回Mono / Flux的方法。目前,我們的服務根本不使用事務機制。但是我們知道潛在的後果。
幸運的是,R2DBC專案正在積極開發中。目標之一是使SQL資料庫與反應式API配合使用。它不是官方標準,因此我們懷疑在與其他庫整合時會出現問題,因為JDBC是很多年以來眾所周知的API。最近釋出的Spring 5.2支援在後臺使用R2DBC在Reactive Streams Publishers上進行反應式事務管理。聽起來確實很有前途,這絕對值得一試,但是我們還沒有任何經驗。

總結

  • 您的團隊必須改變思維方式,並經常重新發明輪子,以實現在順序的阻塞方法中可能眾所周知的模式。如果您與不願學習的人一起工作,您將很快失敗。
  • 許多知名的庫都以阻塞的方式工作。很多時候,使用非阻塞路徑的伺服器還不夠成熟。希望這會很快改變。
  • 在基於Spring WebFlux啟動專案之前,請仔細檢查所需內容並確保其可用。如果您開始使用反應式流,則必須在各處使用它。如果要退後,沒有簡單的方法。您必須更改大多數檔案,包括測試。








 

相關文章