为什么第一次调用 scrypt() 仅使用 1% CPU 并在 GCE 中花费半小时?
Why does the 1st call to scrypt() use just 1% CPU and take half an hour in GCE?
[ 总结与回答:显然,问题是随机数生成器的播种时间很长。请参阅下面的答案。 ]
在 Google Compute Engine (GCE) 中,我的 Java 虚拟机应用程序对 scrypt 密码哈希函数发出的第一个请求需要很长时间——因为代码还没有我想是即时编译的。所以我正在热身 scrypt,通过制作
服务器启动时虚拟 scrypt("pswd", 2,1,1)
调用。然而,发生的事情是 CPU 上升到 300%+,停留 10-20 秒,然后回落到 1%,尽管对 scrypt() 的请求尚未完成。现在,CPU 保持在 1%,持续很多分钟(最多半小时,2 GCE vCPU),直到最终 scrypt() 完成。
为什么会出现这种奇怪的行为?
为什么 scrypt() 不会以 300% CPU 继续 运行 直到完成?不是
运行 内存不足。看看下面的 Docker 统计数据。
第一个 scrypt() 请求后,后续请求完成 "immediately"。例如,这个:
SCryptUtil.scrypt("pswd", 65536, 8, 1)
耗时 < 0.2 秒,尽管它比以下方法做的工作多得多:
SCryptUtil.scrypt("pswd", 2, 1, 1)
这(如前所述)是我的第一个 scrypt() 调用,通常需要几分钟,使用 4 GCE vCPU — 通常需要半小时左右,使用 2 GCE vCPU.
我正在使用具有 4 vCPU、3.6 GB RAM 的 GCE 实例。 Docker 1.11.1。 OpenJDK 1.8.0_77。在 Alpine Linux 3.3 Docker 容器中,Ubuntu 16.04 Docker 主机。无法在我的笔记本电脑上重现;在我的笔记本电脑上,scrypt 总是很快,不需要任何预热。
docker stats
,5-10 秒后:(现在 edp_play_1,第 2 行,使用 300+% CPU)
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
edp_nginx_1 0.02% 55.92 MB / 104.9 MB 53.33% 6.191 kB / 2.897 kB 0 B / 0 B 6
edp_play_1 315.12% 914.7 MB / 2.831 GB 32.31% 43.4 kB / 66.09 kB 0 B / 2.58 MB 67
edp_postgres_1 0.33% 29.84 MB / 314.6 MB 9.49% 529.1 kB / 307.9 kB 0 B / 327.7 kB 17
edp_redis_1 0.08% 6.513 MB / 52.43 MB 12.42% 4.984 kB / 1.289 kB 0 B / 0 B 3
docker stats
半分钟后:(现在 edp_play_1 仅使用 0.97% CPU — 保持这种状态,最多半小时,直到完成)
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
edp_nginx_1 0.02% 55.92 MB / 104.9 MB 53.33% 6.341 kB / 3.047 kB 0 B / 0 B 6
edp_play_1 0.97% 1.011 GB / 2.831 GB 35.71% 130.2 kB / 215.2 kB 0 B / 5.546 MB 66
edp_postgres_1 0.28% 29.84 MB / 314.6 MB 9.49% 678.2 kB / 394.7 kB 0 B / 458.8 kB 17
edp_redis_1 0.06% 6.513 MB / 52.43 MB 12.42% 4.984 kB / 1.289 kB 0 B / 0 B 3
如果您想在 Scala 和 sbt 中进行测试,这就是我在 GCE 中发生的情况:
scala> import com.lambdaworks.crypto.SCryptUtil
import com.lambdaworks.crypto.SCryptUtil
scala> def time[R](block: => R): R = { val t0 = System.nanoTime() ; val result = block ; val t1 = System.nanoTime() ; println("Elapsed time: " + (t1 - t0) + "ns") ; result ; }
time: [R](block: => R)R
scala> time { SCryptUtil.scrypt("dummy password 1", 2, 1, 1) }
Elapsed time: 313823012366ns <-- 5 minutes
res0: String = $s0101g6nrD0f5gDOTuP44f0mKg==$kqEe4TWSFXwtwGy3YgmIcqAhDvjMS89acST7cwPf/n4=
scala> time { SCryptUtil.scrypt("dummy password 1", 2, 1, 1) }
Elapsed time: 178461ns
res1: String = $s0101$C0iGNvfP+ywAxDS0ARoqVw==$k60w5Jpdt28PHGKT0ypByPocCyJISrq+T1XwmPlHR5w=
scala> time { SCryptUtil.scrypt("dummy password 1", 65536, 8, 1) }
Elapsed time: 130900544ns <-- 0.1 seconds
res2: String = $s00801$UMTfIuBRY6lO1asECmVNYg==$b8i7GABgeczVHKVssJ8c2M7Z011u0TMBtVF4VSRohKI=
scala> 313823012366L / 1e9
res3: Double = 313.823012366
scala> 130900544L / 1e9
res4: Double = 0.130900544
注意:这与 Docker 无关。我刚刚在 Docker 外测试,直接在 GCE 实例上安装了 openjdk 8,结果是一样的:scrypt(..)
第一次需要 3 分钟,但 CPU 是 90- 100% 闲置。此后,对 scrypt 的请求立即完成。
问题是为随机数生成器设置种子需要很长时间。 Scrypt 这样做:
public static String scrypt(String passwd, int N, int r, int p) {
try {
byte[] salt = new byte[16];
SecureRandom.getInstance("SHA1PRNG").nextBytes(salt); <--- look
byte[] derived = SCrypt.scrypt(passwd.getBytes("UTF-8"), salt, N, r, p, 32);
(here)
对 nextBytes(salt)
的调用导致 SecureRandom 对象自行播种,这在我的 Google Compute Engine 实例上最多需要半小时。
这与 Java 或 Docker 无关,而是看这里:(直接在主机上,不在任何 Docker 容器内)
# < /dev/random stdbuf -o0 strings --bytes 1 | stdbuf -o0 tr -d '\n\t '
这从 /dev/random 中读取随机字符,我已经 运行 几分钟了,但几分钟后到目前为止只输出了 3 个字符。所以超级慢。
使用随机性较低但速度更快的 /dev/urandom 代替,然后:
# < /dev/urandom stdbuf -o0 strings --bytes 1 | stdbuf -o0 tr -d '\n\t '
立即打印 99999 个字符。
(我在这里找到上面的 < /dev/random ...
命令:https://unix.stackexchange.com/a/114883/128585 )
不过,在我的笔记本电脑上,/dev/random/
版本会立即打印 30-40 个字符。然后它会阻塞,并每 10 秒左右打印一个或几个字符。当我使用鼠标或键盘或网络时,它可能会从我这里获得随机性。
更新
我做了什么:我现在改用 /dev/urandom
— 据我在 Internet 上阅读的内容,这完全没问题。
而且我也开始使用硬件随机数生成器;显然 GCE 实例有这些。
apt install rng-tools # start using any hardware rand num gen, on Ubuntu
[ 总结与回答:显然,问题是随机数生成器的播种时间很长。请参阅下面的答案。 ]
在 Google Compute Engine (GCE) 中,我的 Java 虚拟机应用程序对 scrypt 密码哈希函数发出的第一个请求需要很长时间——因为代码还没有我想是即时编译的。所以我正在热身 scrypt,通过制作
服务器启动时虚拟 scrypt("pswd", 2,1,1)
调用。然而,发生的事情是 CPU 上升到 300%+,停留 10-20 秒,然后回落到 1%,尽管对 scrypt() 的请求尚未完成。现在,CPU 保持在 1%,持续很多分钟(最多半小时,2 GCE vCPU),直到最终 scrypt() 完成。
为什么会出现这种奇怪的行为?
为什么 scrypt() 不会以 300% CPU 继续 运行 直到完成?不是 运行 内存不足。看看下面的 Docker 统计数据。
第一个 scrypt() 请求后,后续请求完成 "immediately"。例如,这个:
SCryptUtil.scrypt("pswd", 65536, 8, 1)
耗时 < 0.2 秒,尽管它比以下方法做的工作多得多:
SCryptUtil.scrypt("pswd", 2, 1, 1)
这(如前所述)是我的第一个 scrypt() 调用,通常需要几分钟,使用 4 GCE vCPU — 通常需要半小时左右,使用 2 GCE vCPU.
我正在使用具有 4 vCPU、3.6 GB RAM 的 GCE 实例。 Docker 1.11.1。 OpenJDK 1.8.0_77。在 Alpine Linux 3.3 Docker 容器中,Ubuntu 16.04 Docker 主机。无法在我的笔记本电脑上重现;在我的笔记本电脑上,scrypt 总是很快,不需要任何预热。
docker stats
,5-10 秒后:(现在 edp_play_1,第 2 行,使用 300+% CPU)
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
edp_nginx_1 0.02% 55.92 MB / 104.9 MB 53.33% 6.191 kB / 2.897 kB 0 B / 0 B 6
edp_play_1 315.12% 914.7 MB / 2.831 GB 32.31% 43.4 kB / 66.09 kB 0 B / 2.58 MB 67
edp_postgres_1 0.33% 29.84 MB / 314.6 MB 9.49% 529.1 kB / 307.9 kB 0 B / 327.7 kB 17
edp_redis_1 0.08% 6.513 MB / 52.43 MB 12.42% 4.984 kB / 1.289 kB 0 B / 0 B 3
docker stats
半分钟后:(现在 edp_play_1 仅使用 0.97% CPU — 保持这种状态,最多半小时,直到完成)
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
edp_nginx_1 0.02% 55.92 MB / 104.9 MB 53.33% 6.341 kB / 3.047 kB 0 B / 0 B 6
edp_play_1 0.97% 1.011 GB / 2.831 GB 35.71% 130.2 kB / 215.2 kB 0 B / 5.546 MB 66
edp_postgres_1 0.28% 29.84 MB / 314.6 MB 9.49% 678.2 kB / 394.7 kB 0 B / 458.8 kB 17
edp_redis_1 0.06% 6.513 MB / 52.43 MB 12.42% 4.984 kB / 1.289 kB 0 B / 0 B 3
如果您想在 Scala 和 sbt 中进行测试,这就是我在 GCE 中发生的情况:
scala> import com.lambdaworks.crypto.SCryptUtil
import com.lambdaworks.crypto.SCryptUtil
scala> def time[R](block: => R): R = { val t0 = System.nanoTime() ; val result = block ; val t1 = System.nanoTime() ; println("Elapsed time: " + (t1 - t0) + "ns") ; result ; }
time: [R](block: => R)R
scala> time { SCryptUtil.scrypt("dummy password 1", 2, 1, 1) }
Elapsed time: 313823012366ns <-- 5 minutes
res0: String = $s0101g6nrD0f5gDOTuP44f0mKg==$kqEe4TWSFXwtwGy3YgmIcqAhDvjMS89acST7cwPf/n4=
scala> time { SCryptUtil.scrypt("dummy password 1", 2, 1, 1) }
Elapsed time: 178461ns
res1: String = $s0101$C0iGNvfP+ywAxDS0ARoqVw==$k60w5Jpdt28PHGKT0ypByPocCyJISrq+T1XwmPlHR5w=
scala> time { SCryptUtil.scrypt("dummy password 1", 65536, 8, 1) }
Elapsed time: 130900544ns <-- 0.1 seconds
res2: String = $s00801$UMTfIuBRY6lO1asECmVNYg==$b8i7GABgeczVHKVssJ8c2M7Z011u0TMBtVF4VSRohKI=
scala> 313823012366L / 1e9
res3: Double = 313.823012366
scala> 130900544L / 1e9
res4: Double = 0.130900544
注意:这与 Docker 无关。我刚刚在 Docker 外测试,直接在 GCE 实例上安装了 openjdk 8,结果是一样的:scrypt(..)
第一次需要 3 分钟,但 CPU 是 90- 100% 闲置。此后,对 scrypt 的请求立即完成。
问题是为随机数生成器设置种子需要很长时间。 Scrypt 这样做:
public static String scrypt(String passwd, int N, int r, int p) {
try {
byte[] salt = new byte[16];
SecureRandom.getInstance("SHA1PRNG").nextBytes(salt); <--- look
byte[] derived = SCrypt.scrypt(passwd.getBytes("UTF-8"), salt, N, r, p, 32);
(here)
对 nextBytes(salt)
的调用导致 SecureRandom 对象自行播种,这在我的 Google Compute Engine 实例上最多需要半小时。
这与 Java 或 Docker 无关,而是看这里:(直接在主机上,不在任何 Docker 容器内)
# < /dev/random stdbuf -o0 strings --bytes 1 | stdbuf -o0 tr -d '\n\t '
这从 /dev/random 中读取随机字符,我已经 运行 几分钟了,但几分钟后到目前为止只输出了 3 个字符。所以超级慢。
使用随机性较低但速度更快的 /dev/urandom 代替,然后:
# < /dev/urandom stdbuf -o0 strings --bytes 1 | stdbuf -o0 tr -d '\n\t '
立即打印 99999 个字符。
(我在这里找到上面的 < /dev/random ...
命令:https://unix.stackexchange.com/a/114883/128585 )
不过,在我的笔记本电脑上,/dev/random/
版本会立即打印 30-40 个字符。然后它会阻塞,并每 10 秒左右打印一个或几个字符。当我使用鼠标或键盘或网络时,它可能会从我这里获得随机性。
更新
我做了什么:我现在改用 /dev/urandom
— 据我在 Internet 上阅读的内容,这完全没问题。
而且我也开始使用硬件随机数生成器;显然 GCE 实例有这些。
apt install rng-tools # start using any hardware rand num gen, on Ubuntu