利用session作國際化引發的old區內存爆滿及修復方法

題記:昨天加班打車回家,看見前面有輛路虎在高速上開的巨慢,擋住了我坐的出租車的路,因而就跟司機吐槽了一句:「前面這車怎麼這麼面啊?」,司機沉默了大概3秒,說了一句富含哲理性的話:「沒有面車,只有麪人」。借用這句話套在軟件開發上就是:「沒有面代碼,只有面的程序猿」。只不過此次我就是那個面的程序猿。(面:是一個方言,大意就是優柔寡斷,反應遲緩,沒有主見,好欺負之類的)。html

背景是這樣的,最近項目要作國際化,主要是基於spring的i18來作,經過攔截器攔截request過來的url中是否包含locale參數,若是locale參數指定了語言類型,則頁面上顯示的信息按指定的類型來獲取,如content這個字段,若是locale=zh_CN,則content=「你好」,若是locale=en_US,則content=「hello」;返回頁面的時候,一樣根據locale,在返回頁面的路徑前加上具體目錄,如返回sayHi.html,locale=zh_CN時返回cn/sayHi.html,locale=en_US時返回us/sayHi.html,這樣能夠顯示不一樣內容的歡迎頁面,通常國際化也是這樣作的,同一套代碼,不一樣配置就能夠完成國際化。web

因而開始的時候個人攔截器代碼時這樣寫的:spring

public class LocaleHandleInterceptor extends LocaleChangeInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {
        String newLocale = request.getParameter(this.getParamName());
        if (newLocale != null) {
            LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
            if (localeResolver == null) {
                throw new IllegalStateException("No LocaleResolver found,may be not config localeResolver bean!");
            }
            Locale locale = StringUtils.parseLocaleString(newLocale);
            localeResolver.setLocale(request, response, locale);
            request.getSession().setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale);
        } else {
            Object localObject = request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
            if (null == localObject) {
                Locale locale = RequestContextUtils.getLocale(request);
                locale = (null == locale) ? Locale.getDefault() : locale;
                request.getSession().setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale);
            }
        }
        // Proceed in any case.
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (null != modelAndView) {
            // Locale locale =
            // (Locale)request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
            Locale locale = (Locale) WebUtils.getSessionAttribute(request, SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
            modelAndView.setViewName(locale.toString() + "/" + modelAndView.getViewName());
        }
    }

}

 

寫完後跑了幾個例子,換了一個locale參數設置都沒有問題,這件事情就算完成了。json

過了一段時間後進行上線前的壓力測試,用loadrunner 500用戶併發壓測tomcat(jvm Xmx=1024M)的時候發現曲線比較奇怪,剛開始表現正常,但大約30秒後tps急劇降低,一分鐘後tps趨於0,可是tomcat並無報OOM內存溢出,用瀏覽器訪問某個url仍是能有返回,用gcutil查看內存回收狀況,發現每秒都在作full gc,因而用jmap把當前tomcat的堆棧打出來,看到不少concurrentHashMap對象,對象裏有不少locale對象,聯想到這塊代碼最近只加過國際化的攔截器,因而懷疑到是否是session致使的,一開始沒有改代碼,而是在tomcat的conf下找到web.xml,調整了session-timeout 爲1分鐘,從新壓測問題仍是復現,從新審視代碼,發現是這段代碼引發的問題,把它註釋掉就正常:瀏覽器

// Object localObject = request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);

 

找到問題後,對request.getSession()進行分析,此方法內部調用的是request.getSession(true),即沒有session的時候會建立,壓力測試下大併發訪問致使過多的session對象建立用於存儲locale,在必定mina gc後都移到jvm old區,引發頻繁的full gc,拖慢整個響應。那爲何沒有報OOM呢?我猜想是響應慢後拖慢了壓測機打過來的請求,full gc每次都能清理出一些空間出來,避免了OOM。不知道是否是這樣?基於這種session的方式自己是能夠作國際化的,只是我調用的方式不對,因而參考spring的LocaleChangeInterceptor和SessionLocaleResolver類的源碼將咱們的攔截器代碼修改以下:tomcat

public class LocaleHandleInterceptor extends LocaleChangeInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws ServletException {
        String localName = request.getParameter(this.getParamName());
        LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
        if (localeResolver == null) {
            throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
        }
        Locale locale = localeResolver.resolveLocale(request);
        if(!(null ==localName || locale.toString().equalsIgnoreCase(localName))){
            localeResolver.setLocale(request, response, (null == localName?Locale.getDefault():StringUtils.parseLocaleString(localName)));
        }
        // Proceed in any case.
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //Locale locale = (Locale)request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
        if(null != modelAndView){
            // 過濾json返回的狀況
            Locale locale = RequestContextUtils.getLocaleResolver(request).resolveLocale(request);
            modelAndView.setViewName(locale.toString()+"/"+modelAndView.getViewName());
        }
    }

}

以後再壓測就沒問題了。session

後記:不通過壓測的代碼不足以談優秀,出了任何問題,先拋開以前的先入爲主根深蒂固的觀念,從源頭分析,大膽猜想,當心求證,並保持學習的心態,繞開問題很容易,可是直面問題須要勇氣和耐心,踏過了它,你就會前進一步,而技術上的前進是技術人員真正的財富。併發

相關文章
相關標籤/搜索