負載均衡策略問題導致服務宕機(記一次生產問題)

tags : 避坑指南


一.問題

2019年12月4日上午11點左右收到線上報警,核心服務CPU使用率達到了3000%,看了下進程的線程信息都是active。因爲使用的容器是weblogic,所以在console平臺查看線程相關信息。
信息整理如下:

  • 活動線程43個,隊列的長度已經到了23個
  • 數據庫連接爲不可用連接數爲5,活動連接數爲5,可用連接數爲15
  • 線程池 線程空閒指標大部分都是false(暫時不可用)
  • 查看線程池 不可用連接執行操作爲請求CAS或者或者請求portal
  • 查看轉儲線程堆棧,部分異常信息如下:
"[ACTIVE] ExecuteThread: '2' for queue: 'weblogic.kernel.Default (self-tuning)'" id=34 idx=0xec tid=23897 prio=5 alive, native_blocked, daemon
at java/util/HashMap.put(HashMap.java:468)[optimized]
at org/jasig/cas/client/session/HashMapBackedSessionMappingStorage.addSessionById(HashMapBackedSessionMappingStorage.java:36)[optimized]
at org/jasig/cas/client/session/SingleSignOutFilter.doFilter(SingleSignOutFilter.java:95)[optimized]
at weblogic/servlet/internal/FilterChainImpl.doFilter(FilterChainImpl.java:56)[optimized]
at com/isoftstone/iaeap/web/filter/SetCharacterEncodingFilter.doFilter(SetCharacterEncodingFilter.java:105)[optimized]
at weblogic/servlet/internal/FilterChainImpl.doFilter(FilterChainImpl.java:56)[inlined]
at weblogic/servlet/internal/WebAppServletContext$ServletInvocationAction.wrapRun(WebAppServletContext.java:3730)[inlined]
at weblogic/servlet/internal/WebAppServletContext$ServletInvocationAction.run(WebAppServletContext.java:3696)[optimized]
at weblogic/security/acl/internal/AuthenticatedSubject.doAs(AuthenticatedSubject.java:321)[optimized]
at weblogic/security/service/SecurityManager.runAs(SecurityManager.java:120)[inlined]
at weblogic/servlet/internal/WebAppServletContext.securedExecute(WebAppServletContext.java:2273)[inlined]
at weblogic/servlet/internal/WebAppServletContext.execute(WebAppServletContext.java:2179)[optimized]
at weblogic/servlet/internal/ServletRequestImpl.run(ServletRequestImpl.java:1490)[optimized]
at weblogic/work/ExecuteThread.execute(ExecuteThread.java:256)[optimized]
at weblogic/work/ExecuteThread.run(ExecuteThread.java:221)
at jrockit/vm/RNI.c2java(JJJJJ)V(Native Method)
-- end of trace
"GC Daemon" id=35 idx=0xf0 tid=23903 prio=2 alive, waiting, native_blocked, daemon
-- Waiting for notification on: sun/misc/GC$LatencyLock@0x51155130[fat lock]
at jrockit/vm/Threads.waitForNotifySignal(JLjava/lang/Object;)Z(Native Method)
at java/lang/Object.wait(J)V(Native Method)
at sun/misc/GC$Daemon.run(GC.java:100)
^-- Lock released while waiting: sun/misc/GC$LatencyLock@0x51155130[fat lock]
at jrockit/vm/RNI.c2java(JJJJJ)V(Native Method)
-- end of trace

此時發現熟悉的身影,因爲項目引用的是cas-client3.1.3版本的jar包,其中存儲session的數據結構是HashMap,且操作時未進行加鎖,之前別的核心的小夥伴已經因爲多線程環境下觸發HashMap擴容死循環問題的BUG宕機過一次,此時看到這裏立馬確認CPU飆升罪魁禍首是這個jar包。

經過確認,核心一共9臺服務器,其中4臺爲前端應用服務,CPU飆升的服務器正是這4臺。

2.曲折

當有了一把錘子看什麼都是釘子,因爲此時心中已經認定是jar導致問題,所以看着服務器的各種異像都覺得符合邏輯。只是心中有些納悶爲何jar包問題這麼久了都沒有觸發HashMap擴容死循環的bug,怎麼單單就這次服務重啓不過幾十分鐘CPU就又直線飆升。
在服務器剛重啓完的時候此時收到一個信息,有出單員頁面部分資源打不開了。開了network跟了下發現部分資源返回404,頁面間不涉及到數據加載的頁面跳轉無異,但是點擊查詢時頁面就會卡死,而此時該進程的CPU也會上升。這個時候就有點詭異,之前的猜測跟目前情形好像並無關聯。
到了中午使用的人數少了一些抱着病急亂投醫,總得做點什麼的心態,將jar包更新到3.5升級了上去,經過了幾個小時CPU仍然狀態穩定,心中長舒了一口氣。
好景不長,沒過多久又有出單員反饋頁面卡死問題,此時看了一下線程嚇了一跳,多個線程觸發GC,後臺日誌多處報錯提示OOM。趕緊看了下源碼,發現原來jar包中有一個守護線程來定時觸發刪除過期的session,而新的jar包去除了此方法,而留了一個clean的方法清理session。猜測到可能是因爲沒有顯式調用該方法導致的內存泄漏,並且並沒有解決前端資源丟失加載不出頁面,還是緊急將jar包替換了回來,重新查找原因。

3.整理線索

3.1

重新梳理了下系統的日誌,發現了除了正常的業務流程異常外還有一個異常信息比較可疑。

<2019-12-5 下午023541秒 CST> <Error> <HTTP> <BEA-101020> <[ServletContext@199457131[app:pcisv7 module:WebRoot path:/pcisv7 spec-version:2.5]] Servlet failed with Exception
java.lang.NullPointerException
	at jsp_servlet._core.__header._jspService(__header.java:295)
	at weblogic.servlet.jsp.JspBase.service(JspBase.java:34)
	at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:227)
	at weblogic.servlet.internal.StubSecurityHelper.invokeServlet(StubSecurityHelper.java:125)
	at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:301)
	Truncated. see log file for complete stacktrace
> 

找到了weblogic緩存目錄下的文件和測試環境文件比對了下也並沒有區別,此文件也沒有涉及過開發改動。查看了下源碼,找到了一處可能發生空指針的地方:

<%
 IUserDetails userDetails = CurrentUser.getUser();
 String operCnm = userDetails.getOpCnm();
 String companyCnm = userDetails.getCompanyCnm();
 long start = System.currentTimeMillis();
 GrtRightService grtRightService =(GrtRightService)SpringUtils.getSpringBean("grtRightService");
 List<GrtMenuVO> lstMenuVo=null;
 try{
   lstMenuVo = grtRightService.getPermissionRootMenus();
   if(lstMenuVo ==null || lstMenuVo.size()==0){
	   out.println("<script type='text/javascript'>parent.window.location ='./common/error/errorinfo.html';</script>");
	   return;
   }
 }catch(Exception e){
   e.printStackTrace();
 }
  String skin = (String)request.getSession().getAttribute("ISOFTSTONESKIN");
  String rootPath = request.getContextPath();
  String companyId = userDetails.getCompanyId();
  String[] companyIds =userDetails.getCompanyIds();
  String[] companyCnms =userDetails.getCompanyCnms();
%>

17行的skin 中是從session中取值,如果session沒有set此處使用會產生空指針,從而導致頁面部分資源加載不出來。向上查了一下代碼,在session中存放ISOFTSTONESKIN的地方是登陸首頁時存放,跳轉到目標頁面後獲取的。查看了下源碼ISOFTSTONESKIN的值也是設置的默認值,當時考慮了下並沒有考慮到什麼場景會導致session中的ISOFTSTONESKIN失效,就在代碼中對該變量重新賦值了下。(其實在這個場景時就已經初步能看出來一些問題,既session丟失問題)

3.2

因爲考慮到是因爲CAS登陸導致的使用cas-client jar包產生的CPU使用率問題,考慮了下,決定先將核心模塊從cas登陸中拿出,啓用spring security驗證登陸。
applicationContext.xml

<import resource="applicationContext-spring-security.xml"/>
<!-- <import resource="applicationContext-spring-security-cas-ns.xml"/> 啓用單點登錄時使用些配置文件-->

重新啓動後,CPU使用率正常,header等幾個從session取值的頁面因爲設置了默認值,也暫時沒有出現空指針問題。但是出現了使用不到幾分鐘,就會跳轉到登陸頁面,提示重新登陸。(操作的頁面沒有嵌入別的系統頁面,不存在跨系統登陸問題)
看了眼核心session失效時間
web.xml

<session-config>
	<session-timeout>300</session-timeout>
</session-config>

順便看了下CAS的 Ticket超時時間
ticketExpirationPolicies.xml

<bean id="grantingTicketExpirationPolicy" class="org.jasig.cas.ticket.support.TimeoutExpirationPolicy">
    <constructor-arg index="0"  value="7200000" />
</bean>

CAS的session超時時間
web.xml

<session-config>
	<session-timeout>300</session-timeout>
</session-config>

核心系統客戶端的session失效時間設置的半小時,cas的session失效時間是半小時,ticket的票據失效時間是兩小時。移除了CAS的認證後半小時內操作是不會登出的,但是實際場景操作過程中就出現跳轉到登陸頁面的情況。(session方面的問題愈發明顯,但是急於將問題解決,與真相一次次擦肩而過)

3.3

此時懷疑是網絡負載方面的問題,因爲系統切換了IPV6考慮到會產生的影響,決定將核心系統加入CAS驗證,並且前端節點只開一個處理請求,進行觀察。(因負責網絡的同事當時不在,只是初步懷疑是IPV6的影響,後續經過確認只改造了公司官網,其餘系統並未改造。)
開單節點運行了一上午發現CPU穩定,頁面加載也再沒有出現加載異常,開雙節點時就會出現加載異常的情況。由此找到問題原因是負載均衡將客戶端X請求轉發到某臺實例A的時候,由於未知原因將網絡請求轉發至另一服務器B。導致本應該存在於session的值直接訪問另一臺服務器時該值並不存在,因爲客戶端瀏覽器未關閉的原因,ticket會存在於header頭中,轉發請求至新的服務時,校驗並沒有登陸便會去CAS認證一次,但是並不會要求用戶重新登陸。
負載是由citrix進行硬件方面的負載,它與Nginx軟件負載功能類似,只是功能更加強勁,其中大部分的負載策略是一致的。查看了下負載策略使用的是least_conn最小連接數,會話保持時間爲兩分鐘。該策略使用了很長時間都沒有問題,考慮到使用iphash會導致服務分部不均,所以未做改動,僅將連接的會話時間修改爲了30分鐘,同session過期時間保持同步。(會話時間既將用戶請求負載至某一IP,若在會話時間內如果有操作,則會話保持,若沒有操作則會話斷開,重新請求時將重新對其進行轉發)

4.問題總結

至此問題已經找到了,對於其中相關問題進行分析梳理。

  1. 因爲執行某些操作或者在頁面中靜止導致兩分鐘內沒有與後臺進行交互,再次操作時因爲citrix使用最小連接數的策略會將新的請求負載至另一臺服務器。
  2. 該服務器取出sessionID校驗是否通過了登陸驗證,瀏覽器Cookie中並沒有緩存該臺服務器的sessionID,驗證失敗,此時重定向請求至CAS服務,因CAS服務只有單臺並且該瀏覽器已經通過了認證,此時訪問通過Cookie直接認證通過,跳轉到了目標頁面。(此時還存在一個場景就是CAS的session也過期了,跳轉到單點登錄地址,帶着ticket參數去驗證用戶,如果單點登錄驗證到ticket沒過期,就不會去登錄頁面,但是會刷新當前頁,因爲從單點登錄地址重定向到了當前頁面地址。所以使用時的感覺就是長時間不操作時,點擊頁面元素會出現刷新頁面的情況。)
  3. 因爲在登陸頁面中緩存在session中的一些默認值,重新登陸後直接跳轉到了目標頁面所以並沒有緩存,jsp此時加載時就產生了資源丟失,後臺報錯空指針的情況。
  4. 而因爲負載頻繁的將請求轉發,也導致出現了類似大規模登陸的場景(多次去CAS認證,認證通過將session緩存)因爲CAS-Client.jar緩存使用的數據結構是HashMap,並且移除和添加緩存操作都沒有加鎖,導致HashMap出現擴容死循環問題。

5.瀏覽器訪問過程

詳細流程:https://4ark.me/post/b6c7c0a2.html
流程圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bNw79hB2-1577028699334)(https://ftp.bmp.ovh/imgs/2019/12/d1ecc658b38ca606.jpg)]
cas:https://www.cnblogs.com/notDog/p/5276643.html
http://www.voidcn.com/article/p-waqcmlak-bqr.html

6.內容擴展

IPV6與IPV4區別:https://blog.51cto.com/7658423/1339259

負載策略

  • 輪詢策略(輪詢加權/round-robin):加權輪詢帶有優先級,按照設置的權值分配請求比例。
  • ip hash :針對IP地址hash決定下一請求轉發至哪一服務(負載不均問題,使用一致性hash)
  • 最少連接(least_conn) :下一個請求將被分派到活動連接數量最少的服務器
  • url hash
  • 隨機Random
  • 最短響應時間LRT :通過ping等方式探測,請求分配給響應時間最短的服務。

虛擬IP

使用一個未分配給真實主機的IP,與真實主機IP,熱備主機IP加起來一共三個IP。在以太網中IP地址只是邏輯地址,真正傳輸的是MAC地址,而每臺設備中都有一個ARP緩存,緩存中用來存儲同一網絡內IP地址和MAC地址對應關係,在向其他主機發送數據時會先從緩存中查詢目標IP所對應的MAC地址,拿到MAC地址後向主機發送數據。
例如緩存內容:
主機A:192.168.0.3 at ec:f4:bb:49:s4:44
主機B:192.168.0.4 at ec:f4:bb:49:s5:64
虛擬Ip:192.168.0.5 at ec:f4:bb:49:s4:44
其中A,B爲真實主機,對外提供服務的是A,B爲熱備,如果A宕機了那麼通過心跳檢測到A已經發生故障,此時主機B會將自己的ARP緩存發送出去,讓路由器修改路由表,告知虛擬地址由A指向B。
更新後的緩存內容:
主機A:192.168.0.3 at ec:f4:bb:49:s4:44
主機B:192.168.0.4 at ec:f4:bb:49:s5:64
虛擬Ip:192.168.0.5 at ec:f4:bb:49:s5:64
此時再次訪問虛擬IP時,機器B會變成主服務器,A降級爲熱備服務器。這就完成了主從切換,這個過程稱之爲IP漂移
參考:http://xiaobaoqiu.github.io/blog/2015/04/02/xu-ni-iphe-ippiao-yi/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章