专栏首页Java架构沉思录又出生产事故!那些年我曾犯过的错-SecureRandom

又出生产事故!那些年我曾犯过的错-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
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
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等待。

键盘和鼠标输入以及磁盘活动可以产生所需的随机性或熵。

但在一个服务器缺乏这样的活动,可能会出现问题。

如果是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

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")。

总结

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一下。

本文分享自微信公众号 - Java架构沉思录(code-thinker),作者:大黄

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 记一次SpringBoot项目启动卡住问题排查记录

    一个spring boot开发的项目,spring boot版本是1.5.7,携带的spring版本是4.1.3。开发反馈,突然在本地启动不起来了,表象特征就是...

    黄泽杰
  • 由一次线上故障来理解下TCP三握、四挥; Java堆栈分析到源码的探秘

    该服务主要是提供对外的代理接口,大部分接口都会调用第三方接口,获取数据后做聚合处理后,提供给客户端使用。

    黄泽杰
  • Spring Cloud Zuul 那些你不知道的功能点

    当@EnableZuulProxy与Spring Boot Actuator配合使用时,Zuul会暴露一个路由管理端点/routes。

    黄泽杰
  • RedisTemplate执行lua脚本,集群模式下报错解决

    在使用spring的RedisTemplate执行lua脚本时,报错EvalSha is not supported in cluster environmen...

    stys35
  • redis 反序列化deserialize异常问题解决

    可以看出是sping对redis查询的返回结果进行deserialize的时候出错了

    MickyInvQ
  • Spring Boot2集成Elasticsearch、PostgreSQL遇到的问题

      这个错误确实有点奇怪,不过好在Github上已经有相关Issue,有兴趣的可以去看看,该问题的解决方法是添加配置项:spring.jpa.propertie...

    happyJared
  • nested exception is java.lang.IllegalStateException: refreshAfterWrite requires

    已解决 nested exception is java.lang.IllegalStateException: refreshAfterWrite requi...

    谙忆
  • SpringMvc上传文件抛出3次Max

    SpringMvc 3.2.18 版本开发的文件上传在Tomcat7 上运行抛出了3个MaxUploadSizeExceededException 异常; 正...

    py3study
  • 【Hibernate那点事儿】—— Hibernate应该了解的知识

    前言: 最近由于有点时间,就像深入的学习一下Hibernate.之前只是简单的使用,并没领会它的妙处。这里就趁着分享的机会,好好整理一下。   这篇主要...

    用户1154259
  • java.lang.ClassNotFoundException: org.apache.commons.fileupload.FileItemFactory

    1、启动项目报出这个错误,未找到响应的包,所以需要你将包从远程仓库下载到本地仓库即可。

    别先生

扫码关注云+社区

领取腾讯云代金券