问题是:在服务器重启时,Bcrypt.hashpw需要100毫秒来解密密码。然而,在一段时间后(没有模式),Bcrypt.hashpw的性能突然从100毫秒飙升到几千毫秒。这没有明显的原因。
问题是/dev/random有时会阻塞,当它阻塞时看起来像随机事件 :) 更让人困惑的是,在尝试测试它的工作原理时,您将遇到观察者效应,即在尝试观察随机行为时,您正在产生熵,并且这可能会导致大量混乱,例如我的结果与你的不同等。这也是为什么看起来没有模式的原因。。
我将演示问题并向您展示如何在自己的服务器上重新创建它(在合理范围内),以便您可以测试解决方案。我将尝试提供一些修复方法,请注意,这是在Linux上进行的,但任何需要熵来生成随机数字并用完所有熵的系统都会发生相同的问题。
在Linux上,/dev/random是一串随机字节流。从此流中读取时,您会消耗可用的熵。当它达到某个点时,从/dev/random的读取将被阻止。可以使用此命令查看可用熵:
cat /proc/sys/kernel/random/entropy_avail
如果你运行以下bash脚本并监控entropy_avail
,你会发现随着bash脚本的消耗,熵急剧下降。
while :
do
cat /dev/random > /dev/null
done
这也应该提示您如何在服务器上重新创建此问题,即运行上述bash脚本以减少可用熵,问题将显现出来。
如果您想查看系统每秒创建多少字节,则可以使用pv
进行测量,例如:
pv /dev/random
如果您让 pv
保持运行状态,它将产生一定影响,因为它会消耗随机的字节流,这意味着其他服务可能开始阻塞。请注意,pv
还显示它的输出,因此它也可能增加系统可用熵 :)
在没有或几乎没有熵的系统上,使用 pv /dev/random
将会非常缓慢。我还遇到过虚拟机生成熵时出现了严重问题。
要重新创建问题,请使用以下类...
import java.security.SecureRandom;
import org.mindrot.jbcrypt.BCrypt;
public class RandTest {
public static void main(String[] args) {
SecureRandom sr = new SecureRandom();
int out = 0;
String password = "very-strong-password-1729";
String hashed;
for (int i = 0; i < 200000 ; i++) {
hashed = BCrypt.hashpw(password, BCrypt.gensalt());
}
}
}
我将bcrypt下载到本地目录,并按以下方式编译和运行。
javac -cp ./jBCrypt-0.4/src/ RandTest.java
java -cp ./jBCrypt-0.4/src/:. RandTest
如果您在运行 RandTest
时再次运行之前的 Bash 脚本,您将会看到系统出现了大的暂停,这是因为它正在阻塞等待更多的熵。如果您运行 strace
您将看到以下内容...
1067 [pid 22481] open("/dev/random", O_RDONLY|O_LARGEFILE) = 12
11068 [pid 22481] fstat64(12, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 8), ...}) = 0
11069 [pid 22481] fcntl64(12, F_GETFD) = 0
11070 [pid 22481] fcntl64(12, F_SETFD, FD_CLOEXEC) = 0
.....
11510 [pid 22481] read(12, "\320\244\317RB\370", 8) = 6
该程序正在从/dev/random
读取。测试熵的问题在于尝试测试熵时可能会生成更多的熵,即观察者效应。
修复:
第一个修复方法是改用/dev/urandom
而不是/dev/random
。
time java -Djava.security.egd=file:///dev/./urandom -cp ./jBCrypt-0.4/src/:. RandTest
一种替代修复方法是将 /dev/random
设备重新创建为 /dev/urandom
设备。您可以从 man 页面中找到如何执行此操作的方法, 即,不要创建它们...
mknod -m 644 /dev/random c 1 8
mknod -m 644 /dev/urandom c 1 9
chown root:root /dev/random /dev/urandom
我们删除一个并伪造它,即
rm /dev/random
mknod -m 644 /dev/random c 1 9
chown root:root /dev/random
/dev/random
现在实际上是/dev/urandom
需要记住的关键是测试需要来自于系统熵的随机数据是困难的,因为观察者效应。
BCrypt.checkpw
方法可能需要 100 毫秒(这是可以接受的),但也可能需要超过 10 秒!我发现这个问题发生在多线程环境下,方法BCrypt#key
占据了绝大部分的执行时间。使用syncrhonized
可以解决这个问题,但对我们的应用程序来说不是一个选项。 - Roman Proshin