前因:
我寫了一個小項目,主要功能是用Spring task定時任務每天定時給用戶發送郵件。執行了幾個月一直沒有問題,前幾天,莫名其妙的突然不再發送郵件了。
只好花費一些時間來查看到底是什么原因造成的。
定時任務的作業內容:
step1: http請求方式調用遠程的一個WebService接口,來獲取數據,一個任務中需要調用n(n>1)次遠程接口。
step2: 解析獲取的數據,生成數據表和曲線圖。
step3: 將數據表和曲線圖,以郵件的形式發送給用戶。
問題出現:
在步驟1中,需要調用10(假定值)次webservice接口,在日志中可以看到執行了3(假定值)次后,突然不在往下執行了。此時的服務已經執行了好幾個月了。
問題分析:
因為步驟1中的代碼,全部放在了try{}catch(){}代碼快中,如果有異常被捕獲,肯定會被記錄到日志當中。
所以這個異常不是我本地的代碼造成的,如果是我本地的代碼,異常肯定會被拋出,並且被記錄下來。
如果不是我本地的代碼出錯,那基本可以確定異常是在http請求遠程接口的時候出現的,而且這個異常可能造成了任務線程的阻塞或者死亡。
順着這個思路,網搜了一下可能或造成http請求中斷的可能原因。找到了兩篇比較有價值的文章。
Http請求偶爾超時+總結各種超時死掉的可能和相應的解決辦法
最終鎖定了兩個可能的原因:
1. 和遠程服務間創建的keep alive的connection過多,導致后面創建的連接會一直連接超時,導致線程出現異常。
2. http請求出現某種錯誤時,http請求僵死,導致線程也不再往下執行。最終導致后面的定時任務也不再執行。
之前一直認為http會有一個默認的超時時間(可能是5min),超過這個時間后會報超時異常。這個看法誤導了我。
先check是不是第一個原因,逐行的檢查http請求部分的代碼,發現所有的connection都是即時的關閉的。所以第一個原因排除了。
那么就剩第二個原因了。做個試驗來驗證是不是果真如此呢。
測試1:
1. 設置每2min執行一個定時任務(為了縮短測試時間,減少了需要請求的次數,設置只請求2次),啟動服務。
2. 程序運行,我去吃了個午飯,回來時程序運行了近半小時,郵件一直正常發出,沒有出現異常。
3. 在又一次請求遠程接口的時候,我拔掉了網線。見證奇跡的時候到了,線程不再繼續執行了。而且沒有錯誤日志。
4. 過了30秒,插上網線,恢復網絡。線程還是沒有繼續執行。
5. 過了2min,該是下一次定時任務執行的時候了,線程還是沒有繼續執行。
6. 過了30min,如果有默認的超時時間,也該拋出time out的異常了吧。結果沒有。程序還是停留在第一次斷網的那個時候。這個時候我已經開始寫這篇日志了。
原因找到了。因為網絡的不穩定,可能是暫時性的斷網,導致了http請求僵死,最終導致整個線程不再執行。
那么怎么解決呢?如果給http請求設置一個默認的超時時間,能不能解決問題呢?
測試2:
1. 在http請求的代碼中設置超時時間,連接超時時間設置為30s,讀取數據超時時間設置為3min。
// 設置連接主機超時(30s) connection.setConnectTimeout(30 * 1000); // 設置從主機讀取數據超時(3min) connection.setReadTimeout(180 * 1000);
2. 仍然是2min執行一次定時任務,啟動服務。
3. 第一次定時任務執行成功,開始執行第二次定時任務,在http發送后還未返回的時候(一次查詢耗時10-20s),果斷又拔掉網線。線程又中斷了。30s后連上網線。
4. 2min后該下一個定時任務執行了,線程還是沒有繼續執行。
5. 大約3min的時候,報了一個異常出來(好長沒全拷貝出來)。報出異常后程序又繼續向下執行了。
java.net.SocketTimeoutException: Read timed out at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:152) at java.net.SocketInputStream.read(SocketInputStream.java:122) at java.io.BufferedInputStream.fill(BufferedInputStream.java:235) at java.io.BufferedInputStream.read1(BufferedInputStream.java:275)
不同時間斷網,還報了下面3種異常:
java.io.IOException: missing CR at sun.net.www.http.ChunkedInputStream.processRaw(ChunkedInputStream.java:405) at sun.net.www.http.ChunkedInputStream.readAheadBlocking(ChunkedInputStream.java:572) at sun.net.www.http.ChunkedInputStream.readAhead(ChunkedInputStream.java:609) at sun.net.www.http.ChunkedInputStream.read(ChunkedInputStream.java:696) at java.io.FilterInputStream.read(FilterInputStream.java:133)
java.net.UnknownHostException: ceshi11.test.com at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:178) at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) at java.net.Socket.connect(Socket.java:579) at java.net.Socket.connect(Socket.java:528)
java.net.NoRouteToHostException: No route to host: connect at java.net.DualStackPlainSocketImpl.connect0(Native Method) at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
6. 大約10min后,在來看看日志。發現,定時任務又開始每2min執行一次。但是因為報錯超時導致中間的錯過的2個定時任務,卻是沒有再被執行。
至此,問題應該就可以被解決了。就是在http請求時設置一下超時時間即可。
