一、背景
最近做接口的性能改造,大概背景如下:
舊:
1.前端每秒輪詢後端接口,接口返回數據狀態,前端用狀態做判斷,變更頁面交互。
2.前端固定調用後端接口,接口阻塞100秒,等待後端隨時返回結果,100秒到達後無結果,直接失敗。
新:
改為ServerSentEvent以text-event-stream固定時間窗口由後端返回處理進度。
二、簡單對比
1.後端服務壓力大。尤其在用户量大,併發量大,又是系統核心業務時,不好把控。
2.阻塞Servlet線程, 當前業務同時訪問量大時,影響其他業務請求;最大等待時間不好把控,且受HTTP響應超時時間影響。
3.自由簡單,且實時性比較強,只要不是高併發入口請求業務就能使用。不佔用Servlet線程。
三、問題現象
1.主代碼展示
以下代碼均為精簡後的最小問題復現demo
- SSE主接口
2.問題表現
通過curl加-N參數調用後,發現返回的data都被雙引號包裹,並且前端的EventSource.onmessage無法回調到(總是觸發onerror回調)
四.問題思考
參考並閲讀以下文檔
前端MDN ServerSentEvent文檔
Spring ServerSentEvent文檔
在前端側瀏覽器看到開發者工具內EventStream標籤頁顯示空白, 意識到確實是後端自身問題,觀察數據結構後發現,像是返回了JSON結構的String字符串,只有JSON結構下String兩側才會有引號,接着立刻翻源碼驗證。
從源碼org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler.HttpMessageConvertingHandler#sendInternal中找到線索,發現Spring在處理text-event-stream響應時,仍然是從Servlet Web的全局HttpMessageConverter查找,但這裏的mediaType值到底是哪個值呢,存在以下三種情況
Controller方法整體的MediaType,這個值固定是text/event-streamSseEmitter#send(java.lang.Object, org.springframework.http.MediaType)方法整體的MediaType,這個值固定是text/plainSseEmitter#send(java.lang.Object, org.springframework.http.MediaType)方法第二個參數傳入的MediaType,這個值動態的,我這裏是application/json;charset=UTF-8
這裏大概率是第二種導致的。
五、得到結論
由於在K8s內網部署,本機無法啓動,關聯的中間件和附加Bean太多。到這裏為止,其實問題清晰了,結合對Spring源碼的理解,項目中的全局HttpMessageConverter配置肯定有問題,我們公司前身有很多認為自己很厲害的人,都是從阿里轉過來的,酷愛fastjson,項目基建時,肯定幹掉了所有的HttpMessageConverter,統一換成了fastjson,接着找這個代碼
這是2021年,前人留下來的寶藏,只能説這個人複製的時候可能沒過腦
- 紅色框出來的就是根因
- 綠色框出來的就是完全無腦,二進制文檔,圖片,也要Json嗎?
1.看看上游Spring的處理
隨即問題來了,Spring默認會有一大堆的HttpMessageConverter自動配置,當前這個是add到List<HttpMessageConverter<?>> converters的第一位嗎?默認的還在不在?
源碼如下: org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getMessageConverters
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters如下
如圖就是Spring的解法
六、圈定影響
那麼新的問題來了,如果這個項目中有其他直接返回String的接口,豈不是意味着全部被包了一層雙引號?
恰好想起之前有其他人不聽勸,不使用Spring Actuator的健康檢查,非要手搓,返回的恰好是String,線上驗證一下
那就是影響到了所有直接返回String的接口。
七、問題復現
本地不能啓動,最小demo,debug一下
debug啓動後發現確實是這裏影響的,這裏會走兩次
一次前面的text/plain
實際的第二個參數的MediaType,值application/json;charset=UTF-8
看看HttpMessageConverter是不是錯選了
根因確定: text/plain錯誤配置了使用FastJsonHttpMessageConverter,導致返回的不是字符串,而是json字符串
八、問題修復
修復就非常簡單了,刪除不合理的MediaType配置,最前面追加StringHttpMessageConverter即可
當然為了縮小影響範圍,只刪除MediaType.TEXT_PLAIN配置即可
至此,問題修復。