BCrypt 性能下降

21
我们有三个Web应用程序(标准的Spring MVC-Hibernate),运行在Jboss服务器6.1中。这三个应用程序共享一个通用的身份验证方法,该方法编译为JAR并包含在每个WAR文件中。我们的身份验证方法使用org.springframework.security.crypto.bcrypt.BCrypt来哈希用户密码,请参见下文:
hashedPassword.equals(BCrypt.hashpw(plainTextPassword, salt));

JBOSS启动选项

set "JAVA_OPTS=-Xms2048m -Xmx4096m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -verbosegc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.txt -XX:+UseParallelOldGC

问题: 重新启动服务器后,似乎Bcrypt.hashpw需要100毫秒才能解密密码。但是在一段时间后(没有模式),Bcrypt.hashpw的性能突然从100毫秒飙升到数秒。这没有明显的原因。

更多信息:

  • Hibernate版本:4.2.4.Final
  • Spring版本:4.0.5.RELEASE Spring
  • Security版本:3.2.4.RELEASE

有其他人遇到过这个问题吗?


1
附加一个分析器并找出哪个部分花费了这么长时间。 - M. Deinum
你好 @m-deinum,我们已经完成了这个任务,并发现问题出在库本身。但这并不能解释为什么该库在一段时间内运行良好,然后会突然升高(并保持在10秒或更长时间)。 - George Artemiou
哪个库以及库的哪个部分... 事实上,仅仅说“这个库”并不能澄清问题,你可能需要更具体地说明是库的哪个部分。方法等。 - M. Deinum
你好 @m-deinum,正如我在问题描述中所说的,我们已经发现了一个与 Spring Security 的 BCrypt 库中的 hashpw 方法有关的问题。 - George Artemiou
@GeorgeArtemiou 你能找出问题的原因吗?我也遇到了同样的问题:调用 BCrypt.checkpw 方法可能需要 100 毫秒(这是可以接受的),但也可能需要超过 10 秒!我发现这个问题发生在多线程环境下,方法 BCrypt#key 占据了绝大部分的执行时间。使用 syncrhonized 可以解决这个问题,但对我们的应用程序来说不是一个选项。 - Roman Proshin
显示剩余3条评论
4个回答

18
问题是:在服务器重启时,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());
            //If we print, we're generating entroy :) System.out.println(hashed);
        }
    }
}
我将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

需要记住的关键是测试需要来自于系统熵的随机数据是困难的,因为观察者效应。


3
谢谢您的回复。但是我们的情况不同。简而言之,我们没有使用Bcrypt.gensalt()。在验证用户时,我们不会生成盐值。相反,我们已经将盐值存储在数据库中,在使用hashpw方法之前,先检索该值。 - George Artemiou
Bcrypt.gensalt正在消耗熵。它从SecureRandom使用的随机流中获取盐。 - Harry

4
可能的解释是SecureRandomSeedGenerator导致了延迟。
Spring的BCrypt实现使用SecureRandom,而SecureRandom又使用SeedGenerator,而SeedGenerator可能会使用阻塞的/dev/random这里有关于这些类的良好描述。
那个bugreport还报告了BCrypt中的性能问题,并将其跟踪到种子生成器,展示了完整的堆栈跟踪。 BCrypt实现不同,但SecureRandom下面的堆栈跟踪必须与spring实现相同。他们的解决方案是减少BCrypt的重新播种频率。

谢谢您的回复。但是我们的情况不同。简而言之,我们没有使用Bcrypt.gensalt()。我们在验证用户时不会生成盐。相反,我们已经将盐存储在数据库中,并在使用hashpw方法之前检索它。 - George Artemiou

0

对于任何遇到相同问题的人,我们通过安装rng-tools解决了这个问题。


0

将标签更改为urandom仅适用于JDK8或更高版本,我们长期以来一直面临这个问题,在1.7中更改为urandom并没有帮助,但在1.8中解决了该问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接