最近在阿里云买了一台轻量应用服务器,但在部署Tomcat启动服务之后,浏览器访问http://[公网IP]:8080时迟迟访问不到。于是我开始了漫长的调试之路。
正常情况
1.保证阿里云安全组开启了8080端口
2.保证Linux防火墙放行8080端口
# 查看firewall服务状态
systemctl status firewalld
# 开启、重启、关闭、firewalld.service服务
# 开启
service firewalld start
# 重启
service firewalld restart
# 关闭
service firewalld stop
# 查看防火墙规则
firewall-cmd --list-all # 查看全部信息
firewall-cmd --list-ports # 只看端口信息
# 开启端口
开端口命令:firewall-cmd --zone=public --add-port=8080/tcp --permanent
重启防火墙:systemctl restart firewalld.service
命令含义:
--zone #作用域
--add-port=80/tcp #添加端口,格式为:端口/通讯协议
--permanent #永久生效,没有此参数重启后失效
#查看端口是否正常监听
netstat -an | grep 8080
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN
正常情况下,到这里,启动Tomcat服务,浏览器就可以访问了,但哪有那么多正常情况。
非正常情况
1.遇到问题
我来详细说下我遇到的问题:
-
打开Tomcat服务后,浏览器访问不了,一直转圈圈~
-
就在这时我想关闭服务重启,问题来了
SEVERE: Could not contact localhost:8005. Tomcat may not be running. SEVERE: Catalina.stop: java.net.ConnectException: Connection refused at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) at java.net.Socket.connect(Socket.java:579) at java.net.Socket.connect(Socket.java:528) at java.net.Socket.<init>(Socket.java:425) at java.net.Socket.<init>(Socket.java:208) at org.apache.catalina.startup.Catalina.stopServer(Catalina.java:450) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.apache.catalina.startup.Bootstrap.stopServer(Bootstrap.java:400) at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:487)
2.找到原因
这里找到原因了,SEVERE: Could not contact localhost:8005. Tomcat may not be running.
也就是说,Tomcat服务还没有启动,那么这个时候我去停止也是停止不了的。那它为什么没有启动还没有报错呢??
这里其实是不准确的,是因为我们在Tomcat没完全启动前就关闭Tomcat。
那么既然没报错,说明服务启动会成功,那我就等着你成功!
等了6分钟,终于等到你!
3.探究原理
那么现在也就是说,我们操作没毛病,只是这个服务启动的太慢了。
可,为什么这么慢嘞???
我查看了一下进程,Tomcat所在的JVM进程已经被启动了所以可以排除是JVM退出引起的问题。那么问题真的就是JVM因为某种原因被阻塞了。
分析
程序被阻塞一般来说一定是要等待某个资源,而现在的情况是所有资源都充足,仔细想想我需要找到Tomcat停止在了哪里?代码里发生了什么事情。我决定试一下 strace,这是一个跟踪系统调用(System Call)的工具,无论是Java还是Pyhton很多资源申请都会变成System Call。(比如打开文件、新建线程、读写数据、等待I/O)通过这个工具我至少可以知道Tomcat是停止在哪个System Call上的,这样可以方便我推断出问题的原因。
在我寻找问题的时候,他又启动好了,所以我们先把它关闭。
./shutup.sh
然后进行跟踪
strace -f -o strace.out ./catalina.sh run
strace有很多参数,我用了两个参数
- -f 跟踪fork的子进程,通俗的说会跟踪所有线程的系统调用
- -o把内容输出到文件
这一步结束后我们来看看结果。
cat strace.out
分析的方法是从下往上(被阻塞的地方肯定是在最后咯)。首先我们需要去掉Tomcat停止引起的System Call,它们不是我们需要的。从后往前搜索找到SIGINT。
红色部分以上就是引起阻塞的系统调用了,上面有一大堆一大堆的futex的调用,它们是Linux中的一种轻量级的同步方法,所以我们可以判断出最上面肯定是有某个System Call就是阻塞的真正元凶。跳过所有的futex:
这个read就是引起后面一串futex的真正原因,strace非常聪明它不仅仅给出了System Call还给出了传递的参数和返回值,read读取的是61号文件句柄,没有返回成功(unfinished)。
顺着这条路,我们看一下61号文件句柄是什么:
/dev/random是Linux下的随机函数生成器,读取它相当于生成随机数字。
搜索它,简单来说就是,/dev/random的随机数生成依赖系统环境噪声,如鼠标、键盘操作等。
当噪声数据不够的时候,就会出现读取阻塞。
深入分析
如果用Tomcat /dev/random作为关键字基本上就能够回答我们的疑惑了。Tocmat的Session ID是通过SHA1算法计算得到的,计算Session ID的时候必须有一个密钥。为了提高安全性Tomcat在启动的时候会通过随机生成一个密钥。这个随机数是通过 linux的提供的随机函数生成器提供永不为空的随机字节数据流,许多加密解密程序需要用到它们提供的随机数。
解决问题
明白了问题的原因解决起来就非常简单了——替换/dev/random为/dev/./urandom,用伪随机函数生成器(/dev/./urandom)来替代随机函数生成器(/dev/random)。
- 通过修改Tomcat启动文件catalina.sh
–Djava.security.egd=file:/dev/./urandom - 通过修改JRE中的java.security文件securerandom.source=file:/dev/./urandom
4.彻底解决问题
在Linux(CentOS)环境下,随机数可以从两个特殊的文件中产生,一个是/dev/urandom,另外一个是/dev/random。
它们产生随机数的原理是利用当前系统的熵池来计算出固定一定数量的随机比特,然后将这些比特作为字节流返回。熵池就是当前系统的环境噪音,熵指的是一个系统的混乱程度,系统噪音可以通过很多参数来评估,如内存的使用,文件的使用量,不同类型的进程数量等等。
上面介绍的两种方式都是用/dev/urandom替换/dev/random,其实还有第三种方式——增大/dev/random的熵池。问题的原因是由于熵池不够大,所以增大它是最彻底的方法。
通过以下命令,我们可以查看现在的熵池大小;
cat /proc/sys/kernel/random/entropy_avail
189
我们需要找到一种方式来提高这个值就行了。如果你的CPU带有DRNG特性,可以充分利用硬件来提高熵池产生的速度 。
通过cat /proc/cpuinfo | grep rdrand可以查看自己的CPU是否支持,一般来说Intel的Ivy_Bridge架构的CPU都支持(i3、i5需要注意是否采用该种架构,i7和xeon基本上都支持);AMD的CPU在2015年以后生成的都支持。(如果你是虚拟机需要开启额外的参数)。如果你的硬件不支持,也没有关系,我们可以让/dev/urandom来做“熵源”。
以Centos7为例,
-
yum install rngd-tools(或者rng-tools)安装rngd服务(熵服务)
-
systemctl start rngd启动服务(支持DRNG特性的同学,到这里就可以cat /proc/sys/kernel/random/entropy_avail查看了)
cat /proc/sys/kernel/random/entropy_avail 3009
-
如果你的CPU不支持DRNG特性,可以使用/dev/urandom来模拟。
-
cp /usr/lib/systemd/system/rngd.service /etc/systemd/system
-
vim /etc/systemd/system/rngd.service
-
ExecStart=/sbin/rngd -f -r /dev/urandom
-
systemctl daemon-reload重新载入服务
-
systemctl restart rngd重启服务
5.选择哪种解决方法
个人建议选择第三种方式,熵池不仅仅Tomcat用,Linux下的所有应用程序产生随机数都会用到这个,所以不仅仅是Tomcat可能被阻塞。如果你搜索会发现Apache、Nginx、OpenSSL都被这个问题坑过。如果我们通过修改Java的配置来解决这个问题其实只是解决Java应用程序的问题,只能是治标不治本。根治的方法应该是通过rngd提高随机数生成的速度。
6.如何重现故障
可以很容易的重现上面描述的故障
- systemctl stop rngd 停止rngd服务(如果你有启动rngd)
- 查看当前熵池的大小cat /proc/sys/kernel/random/entropy_avail
- head -c1024 /dev/random,强制消费1024个随机数,系统会长时间没有反应。直接ctrl+c
- 再次查看熵池的大小cat /proc/sys/kernel/random/entropy_avail,保证它的大小在尽可能的小
- 启动tomcat,会发现又是很长时间的等待