SecureRandom 引發的線程阻塞

寫在前面--每個人都是在不斷碰壁中獲得成長,bug的逼格越高, 成長速度越快。

        本人上週親手寫下了一個牛逼的bug,直接導致的結果是,晚上12點升級後臺接口以後,第二天早上7點多開始,所有的app頁面出現卡頓,白屏。公司研發老總,迅速召集公司運維大佬,產品大佬,研發大佬奔赴公司解決bug。所有人,開始手忙腳亂,查看線上日誌,抓包,阿爾薩斯監聽 接口耗時。各個大神,各種手段,各顯才能。。。經過三個小時的排查,最終用jstack 命令查看線程數,發現整個服務,線程不斷攀升至400多,且絕大多數空閒線程一直處於等待狀態,沒有執行任何任務。弔詭的地方就在這裏,緊急着,進入線程號裏面,發現打印瞭如下堆棧信息:

   http-nio-8080-exec-177" #441 daemon prio=5 os_prio=0 tid=0x00002ae00812e800 nid=0x6183 waiting for monitor entry [0x00002ae042f6b000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at sun.security.provider.NativePRNG$RandomIO.implNextBytes(NativePRNG.java:543)
        - waiting to lock <0x0000000700ad0ca0> (a java.lang.Object)
        at sun.security.provider.NativePRNG$RandomIO.access$400(NativePRNG.java:331)
        at sun.security.provider.NativePRNG$Blocking.engineNextBytes(NativePRNG.java:268)
        at java.security.SecureRandom.nextBytes(SecureRandom.java:468)
        at java.security.SecureRandom.next(SecureRandom.java:491)
        at java.util.Random.nextInt(Random.java:390)
        at com.yunshi.xy.service.impl.XyContentPVServiceImpl.addV2RandomNum(XyContentPVServiceImpl.java:559)
        at com.yunshi.xy.service.thread.ActionLogPvThread.run(ActionLogPvThread.java:69)
        at java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy.rejectedExecution(ThreadPoolExecutor.java:2038)
        at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
        at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
        at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.execute(ThreadPoolTaskExecutor.java:314)
        at com.yunshi.xy.web.api.XyToCQueryApiController.addActionLogByPv(XyToCQueryApiController.java:2974)
        at com.yunshi.xy.web.api.XyToCQueryApiController$$FastClassBySpringCGLIB$$6446a859.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
        at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
        at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
        at com.yunshi.xy.web.api.XyToCQueryApiController$$EnhancerBySpringCGLIB$$8e41f2ae.addActionLogByPv(<generated>)
        at sun.reflect.GeneratedMethodAccessor140.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:190)
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)

 

當我看到SecureRandom這個關鍵字時,恍然大悟,這不是前幾天通過solar進行代碼檢測時,剛剛修改的類嗎?發生了什麼?

線程安全?線程阻塞?有關線程的各個關鍵詞在大腦裏一閃而過。。。於是迅速恢復代碼成原先的Random r = new Random()重新構建服務,app恢復正常,臥槽。。。。。。。。。什麼鬼

         首先去看soloar代碼檢測給出的建議是這樣的:

Noncompliant Code Example

public void doSomethingCommon() {
  Random rand = new Random();  // Noncompliant; new instance created with each invocation
  int rValue = rand.nextInt();
  //...

Compliant Solution

private Random rand = SecureRandom.getInstanceStrong();  // SecureRandom is preferred to Random

public void doSomethingCommon() {
  int rValue = this.rand.nextInt();
  //...

當時未進行更多的思考,直接把SecureRandom.getInstanceStrong();寫在了方法內,引發了線程阻塞

 

今天再一次查SecureRandom.getInstanceStrong();這個方法時,才發現如下問題:

SecureRandom.getInstanceStrong(); 是jdk1.8裏新增的加強版隨機數實現

如果你的服務器在Linux操作系統上,這裏的罪魁禍首是SecureRandom generateSeed()。它使用/dev/random生成種子。但是/dev/random是一個阻塞數字生成器,如果它沒有足夠的隨機數據提供,它就一直等,這迫使JVM等待。鍵盤和鼠標輸入以及磁盤活動可以產生所需的隨機性或熵。但在一個服務器缺乏這樣的活動,可能會出現問題
 

 

1,如果是tomcat 環境,有如下解決方式

可以通過配置JRE使用非阻塞的Entropy Source: 
在catalina.sh中加入這麼一行:-Djava.security.egd=file:/dev/./urandom 即可。 
加入後再啓動Tomcat,整個啓動耗時下降到Server startup in 20130 ms。 
這種方案是在修改隨機數獲取方式,那這裏urandom是啥呢?

/dev/random的一個副本是/dev/urandom(“unblocked”,非阻塞的隨機數發生器[4]),它會重複使用熵池中的數據以產生僞隨機數據。這表示對/dev/urandom的讀取操作不會產生阻塞,但其輸出的熵可能小於/dev/random的。它可以作爲生成較低強度密碼的僞隨機數生成器,不建議用於生成高強度長期密碼。 - - - wikipedia

在JVM環境中解決 
打開$JAVA_PATH/jre/lib/security/java.security這個文件,找到下面的內容:

securerandom.source=file:/dev/random
替換成

securerandom.source=file:/dev/./urandom
 

2 springboot項目,網上發現很多朋友啓動時加了啓動參數:

-Djava.security.egd=file:/dev/./urandom

但是沒有起作用

去查 NativePRNG$Blocking代碼,看到它的文檔描述:

A NativePRNG-like class that uses /dev/random for both seed and random material. Note that it does not respect the egd properties, since we have no way of knowing what those qualities are.

奇怪怎麼-Djava.security.egd=file:/dev/./urandom參數沒起作用,仍使用/dev/random作爲隨機數的熵池,時間久或調用頻繁的話熵池很容易不夠用而導致阻塞;於是看了一下 SecureRandom.getInstanceStrong()的文檔:

Returns a SecureRandom object that was selected by using the algorithms/providers specified in the securerandom.strongAlgorithms Security property.

原來有自己的算法,在 jre/lib/security/java.security 文件裏,默認定義爲:

securerandom.strongAlgorithms=NativePRNGBlocking:SUN

如果修改算法值爲NativePRNGNonBlocking:SUN的話,會採用NativePRNG$NonBlocking裏的邏輯,用/dev/urandom作爲熵池,不會遇到阻塞問題。但這個文件是jdk系統文件,修改它或重新指定一個路徑都有些麻煩,最好能通過系統環境變量來設置,可這個變量不像securerandom.source屬性可以通過系統環境變量-Djava.security.egd=xxx來配置,找半天就是沒有對應的系統環境變量。只好修改代碼,不採用SecureRandom.getInstanceStrong這個新方法,改成了SecureRandom.getInstance("NativePRNGNonBlocking")

 

整理一下知識點(轉載自:https://www.iteye.com/blog/wwwcomy-2342229)

1.SecureRandom本身並不是僞隨機算法的實現,而是使用了其他類提供的算法來獲取僞隨機數。 

2.如果簡單的new一個SecureRandom對象的話,在不同的操作平臺會獲取到不同的算法,windows默認是SHA1PRNG,Linux的話是NativePRNG。 

3. Linux下的NativePRNG,如果調用generateSeed()方法,這個方法會讀取Linux系統的/dev/random文件,這個文件在JAVA_HOME/jre/lib/securiy/java.security裏面有默認定義。而/dev/random文件是動態生成的,如果沒有數據,就會阻塞。也就造成了第一個現象。 

4.可以使用-Djava.security.egd=file:/dev/./urandom (這個文件名多個u)強制使用/dev/urandom這個文件,避免阻塞現象。中間加個點的解釋是因爲某個JDK BUG,SO那個帖子有鏈接。 

5.如果使用SecureRandom.getInstanceStrong()這種方法初始化SecureRandom對象的話,會使用NativePRNGBlocking這個算法,而NativePRNGBlocking算法的特性如下: 
NativePRNGBlocking uses /dev/random for all of the following operations: 
Initial seeding: This initializes an internal SHA1PRNG instance using 20 bytes from /dev/random 
Calls to nextBytes(), nextInt(), etc.: This provides the XOR of the output from the internal SHA1PRNG instance (see above) and data read from /dev/random 
Calls to getSeed(): This provides data read from /dev/random 

可見這個算法完全依賴/dev/random,所以當這個文件隨機數不夠的時候,自然會導致卡頓了。 
6.如果使用NativePRNGBlocking算法的話,4中的系統參數失效!!!(這個是從http://hongjiang.info/java8-nativeprng-blocking/看到的) 
7.一般使用SecureRandom不需要設置Seed,不需要設置算法,使用默認的,甚至一個靜態的即可,如果有需求的話可以在運行一段時間後setSeed一下

文章參考

1:tp://hongjiang.info/java8-nativeprng-blocking/

2:https://blog.csdn.net/bigtree_3721/article/details/85085413

3:https://blog.csdn.net/upshi/article/details/54907464

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