前言:最近上線時遇到一個很詭異的問題,這個問題在測驗環境和pr環境都沒問題,但是在生產環境必現,排查配置、代碼是否一致等原因后,最終通過cat生成的traceId和研讀代碼,花費了兩個小時,定位到問題是由于SecureRandom產生亂數系統熵池中數量不足,阻塞了當前執行緒,記錄一下這個踩坑經驗,希望大家規避掉類似的使用~
情境描述以及隨機演算法介紹
系統中經常有獲取亂數的場景,我們通常new一個Random物件,通過random.nextInt(size)來獲取一個亂數,通過jdk8中對Random類的解釋,我們知道:
- Random 類使用線性同余法 linear congruential formula 來生成偽亂數,
- 兩個 Random 實體,如果使用相同的種子 seed,那他們產生的亂數序列也是一樣的,
- Random 是執行緒安全的,你的程式如果對性能要求比較高的話,推薦使用 ThreadLocalRandom,
- Random 不是密碼學安全的,加密相關的推薦使用 SecureRandom,
從下面的原始碼中可以看到,Random 的默認使用當前系統時鐘來生成種子 seed,
private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
// 默認使用當前系統時鐘來生成種子seed
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
// 指定初始化種子seed
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
// 子類可能重寫了setSeed方法
this.seed = new AtomicLong();
setSeed(seed);
}
}
// 線性同余演算法
private static long seedUniquifier() {
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
在上面介紹Random類時,該類的解釋加密相關的推薦使用 SecureRandom類,所以我們看下Random的子類SecureRandom類的解釋,主要有以下幾點:
- 該類提供了能滿足加密要求的強亂數生成器,
- 許多SecureRandom實作都是偽隨機的,數字發生器(PRNG),這意味著他們使用確定性演算法,從一個真正的隨機種子產生一個偽隨機序列,
- 傳遞給 SecureRandom 種子必須是不可預測的,seed 使用不當引發的安全漏洞,比如: 位元幣電子錢包漏洞,
// 超類建構式的呼叫將導致呼叫到SecureRandom類重寫的setSeed方法
public SecureRandom() {
super(0);
getDefaultPRNG(false, null);
}
//
private void getDefaultPRNG(boolean setSeed, byte[] seed) {
String prng = getPrngAlgorithm();
if (prng == null) {
// bummer, get the SUN implementation
// SUN提供程式
prng = "SHA1PRNG";
this.secureRandomSpi = new sun.security.provider.SecureRandom();
this.provider = Providers.getSunProvider();
if (setSeed) {
this.secureRandomSpi.engineSetSeed(seed);
}
} else {
// NativePRNG演算法,使用/dev/random或/dev/urandom獲取種子,啟動應用程式時可以通過引數 -Djava.security.egd=file:/dev/urandom 來指定seed源,使用/dev/random會阻塞執行緒直到足夠的熵可用
try {
SecureRandom random = SecureRandom.getInstance(prng);
this.secureRandomSpi = random.getSecureRandomSpi();
this.provider = random.getProvider();
if (setSeed) {
this.secureRandomSpi.engineSetSeed(seed);
}
} catch (NoSuchAlgorithmException nsae) {
throw new RuntimeException(nsae);
}
}
if (getClass() == SecureRandom.class) {
this.algorithm = prng;
}
}
由上面的分析,Random產生的亂數不夠隨機,并且為了提升性能和隨機性,Sonar建議定義一個 Random 單例來統一產生亂數, 建議使用 SecureRandom.getInstanceStrong() ,我們根據Sonar建議改掉之后,忽略了一個問題:使用/dev/ random會阻塞執行緒直到足夠的熵可用,
所以上線當天的現象是:同一份代碼配置相同其他環境都ok,但是生產環境總是介面執行超時,場景無法重現,
問題定位
由于該介面呼叫了很多三方介面,通過traceId分析日志,發現該介面呼叫日志和部分三方日志時間為00:32分,并且介面回應rt也無例外,但是另一些三方日志竟然在01:16分才列印出來,很顯然出現了執行緒阻塞問題,通過日志給出的線索,發現執行到 SecureRandom.getInstanceStrong() 方法后就阻塞了,
private Random rand = SecureRandom.getInstanceStrong();
this.rand.nextInt(hasNoRuleSalerId.size())
通過查詢資料發現SecureRandom.getInstanceStrong() 方法在 linux 環境下使用 /dev/random 生成種子,其實作原理是:作業系統收集了一些隨機事件,比如滑鼠點擊、鍵盤點擊、磁盤活動等,SecureRandom 使用這些隨機事件作為種子,當服務器缺乏”活動”時,就會等待種子,從而阻塞執行緒,
- /dev/random 設備會回傳小于熵池噪聲總數的隨機位元組,/dev/random 可生成高隨機性的公鑰或一次性密碼本,若熵池空了,對/dev/random的讀操作將會被阻塞
- /dev/random 的一個副本是 /dev/urandom (“unlocked”,非阻塞的亂數發生器),它會重復使用熵池中的資料以產生偽隨機資料,這表示對/dev/urandom的讀取操作不會產生阻塞,但其輸出的熵可能小于 /dev/random 的,它可以作為生成較低強度密碼的偽亂數生成器,不建議用于生成高強度長期密碼,
問題解決
- 在不要求強隨機性和安全性的業務場景下,推薦使用new Random()來獲取亂數,
- 如果需要強隨機性的業務場景,只需使用去空引數建構式new SecureRandom(),讓系統選擇最好的亂數生成器,但是一些要求非常高速的操作情況下,SecureRandom中的隨機演算法稍差些,
- 取其精華去其糟粕,我們也可以使用Random時,手動寫工具類,在獲取亂數方法中增加隨機性因子來達到我們的要求,
public synchronized long nextId() {
Long id = Instant.now().toEpochMilli();
int asInt = new Random().ints(0, (999 + 1)).findFirst().getAsInt();
return id + asInt;
}
參考檔案:https://www.cnblogs.com/xiekun/p/11938196.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/208529.html
標籤:其他
