圧測問題小記のOutOfMemoryError


最近、バックグラウンドインタフェースの圧測を行う過程で、高くてトリガするバグを発見し、バグ自体は複雑ではないが、一定の代表性を持っていることを記録した.

インタフェースの概要


  まずテストするインタフェースはシーン保険の保険インタフェースであり、このインタフェースの業務シーンはコアラがネット易保険に接続して商品の品質保険または延保険に加入し、ネット易保険が保険会社に加入する.
このインタフェースの内部処理ロジックは、以下のフローチャートに示すようになります.

問題の説明


 本論文ではjmeterを用いて保険加入インタフェースに対して圧力テストを行い、圧力を増大してテストを開始した後、多くのエラー要求が返され、バックグラウンドインタフェースログを観察し、具体的なエラーは以下の通りである.
2018-03-30 16:59:46.012 [http-nio-7112-exec-6] ERROR o.a.c.c.C.[.[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: unable to create new native thread] with root cause
java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:717)
	at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1357)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
	at com.netease.baoxian.service.InsuranceService.insured(InsuranceService.java:110)
	at com.netease.baoxian.service.ServiceOrderService.addServiceOrder(ServiceOrderService.java:126)
	at com.netease.baoxian.controller.SceneController.insure(SceneController.java:180)
	at sun.reflect.GeneratedMethodAccessor154.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.web.filter.ApplicationContextHeaderFilter.doFilterInternal(ApplicationContextHeaderFilter.java:55)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:123)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:110)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:108)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.cloud.sleuth.instrument.web.TraceFilter.doFilter(TraceFilter.java:186)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:106)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:504)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)


原因分析


JVMがオペレーティングシステムに新しいnative thread(オリジナルスレッド)の作成を申請するとjavaに遭遇する可能性がある.lang.OutOfMemoryError:Unable to create new native threadエラー.下位OSで新しいnative threadの作成に失敗すると、JVMは対応するOutOfMemoryErrorを放出します.全体的にjava.lang.OutOfMemoryError:Unable to create new native threadエラーのシーンの多くは、次の段階を経験します.
  • JavaプログラムはJVMに新しいJavaスレッドの作成を要求する.
  • JVMローカルコード(native code)はこの要求をエージェントし、オペレーティングシステムレベルのnative thread(オリジナルスレッド)を作成しようとする.
  • オペレーティングシステムは、スレッドにメモリを同時に割り当てる必要がある新しいnative threadを作成しようとしています.
  • オペレーティングシステムの仮想メモリが消費された場合、または32ビットプロセスのアドレス空間の制限(約2-4 GB)を受けた場合、OSはローカルメモリの割り当てを拒否します.
  • JVMはjavaを放出する.lang.OutOfMemoryError:Unable to create new native threadエラー.

  • 問題の位置付け


    スタック情報に基づいて、問題コードが次の方法に現れるように位置決めします.この方法で実現される機能は、保険会社の保険インタフェースを非同期で呼び出すことです.
        public void insured(ServiceOrder serviceOrder, boolean isSync){
            if(isSync){
                insured(serviceOrder);
            }else{
                ExecutorService es = Executors.newFixedThreadPool(1);
                es.submit(()->insured(serviceOrder));
                es.shutdown();
            }
        }
    

    このコードを解析し,非同期タスクを処理する際にスレッドプールを用いた開発を行った.スレッドプールを使用してマルチスレッドを処理するメリットは、次のとおりです.
  • は存在するスレッドを再利用し、オブジェクトの作成、消滅のオーバーヘッドを低減し、性能が優れている.
  • は、最大同時スレッド数を効果的に制御し、システムリソースの使用率を向上させ、同時にリソースの競合を回避し、渋滞を回避することができる.
  • は、タイミング実行、定期実行、単一スレッド、同時数制御などの機能を提供する.

  • 問題はExecutorService=Executorsにあります.newFixedThreadPool(1);この行のコードにあります.新FixedThreadPoolメソッドの機能は、スレッドの最大同時数を制御し、超過したスレッドがキュー内で待機する定長スレッドプールを作成することです.開発者は、ここでスレッドプールを作成するときにローカル変数を使用し、このメソッドに要求するたびに、固定スレッドの個数が1のスレッドプールを新規作成します.このように大量の同時要求が入ると、大量のスレッドが新規作成され、システムの仮想メモリが消費する、JVMがjavaを放出する.lang.OutOfMemoryError:Unable to create new native threadエラー.

    ソリューション


      問題は位置づけられており,解決も簡単で,スレッドプールを正しく使用すればよい.
        private static ExecutorService es = Executors.newFixedThreadPool(50);
        public void insured(ServiceOrder serviceOrder, boolean isSync){
            if(isSync){
                insured(serviceOrder);
            }else{
                es.submit(()->insured(serviceOrder));
            }
        }
    

      開発者はコードを修正する上で,スレッドプールを静的メンバー変数として定義し,50スレッドの長さを定めた.このように大量のリクエストがこのメソッドに入ると、50個のスレッドがリクエストをループ処理し、超過したスレッドがキュー内で待機します.
      問題は解決され,コード修正後,インタフェースは高同時でOOMのエラーを報告しなくなった.しかし、アリ符号化規則を参照すると、このスレッドプールの使用方法にはまだ問題がある.newFixedThreadPoolメソッドを使用する主な問題は、スタックされたリクエスト処理キューが非常に大きなメモリ、さらにはOOMを消費する可能性があることです.アリコード規約によると、スレッドプールはExecutorsを使用して作成することは許されず、ThreadPoolExecutorの方式を通じて、このような処理方式は書く学生にスレッドプールの運行規則をより明確にし、資源が枯渇するリスクを回避することができる.
        private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("demo-pool-%d")
                .build();
        private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingDeque(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
                
        public void insured(ServiceOrder serviceOrder, boolean isSync) {
            if (isSync) {
                insured(serviceOrder);
            } else {
                pool.execute(() -> insured(serviceOrder));
                pool.shutdown();
            }
        }
                
    

    小結


    これで、この問題は基本的に解決された.問題は大きくなく、より重要な意味は、この問題を発見し、解決する過程にある.テスト担当者として、測定システムのバックグラウンドアーキテクチャと実現ロジックを明確に理解するだけでなく、基本的なコードスキルを身につけてこそ、自分の分析と問題解決能力を絶えず向上させることができる.