栈溢出导致 BlockingPriorityQueue 入队一直阻塞问题排查分析

问题背景

问题背景部分内容比较长,和 BlockingPriorityQueue 无关,不感兴趣的读者可以跳过,内容是关于如何定位到问题出在 BlockingPriorityQueue 上的。

公司内一个项目有通过 netty 打开监听端口,接收到报文,经过一些列处理后,会响应写回对应的通道channel。但是在预投产环境做验证时,发现端口明明是通的,发送报文却迟迟无法获取到响应。

问题定位

关于线程池的怀疑

首先想到的是线程池有问题,对于 netty 接收到的报文,我们的系统会将其放进一个报文队列中,然后有一个固定线程一直做轮询,将报文队列逐个取出做处理。我们恰好最近改动到过这个线程池,怀疑是这个轮询线程挂掉了(实际我们这个端口接收的是心跳报文,因为打印的日志级别过低未在预投产环境打印出,从而无法判定到底是没有接收到,还是没有轮询)。

  • 怀疑的排除:
1
jstack <pid> > 1.jstack

通过 jstack 获取到对应的线程信息。由于这个线程是在做死循环轮询的,所以只要它没有挂,就一定能够在线程信息里找到它,结果如图

我们成功发现了当前线程还活着,正在做每次轮询结束后的睡觉。从而排除了这个可能性

关于新做的改动的排查

刚刚已经拿到了线程栈信息,所以我们也顺便看了看新改动点相关的 jstack,同样的,它也是使用了这个线程池,也是一个死循环的轮询工作。

但是很不幸的是,我们并没有从线程栈中获取到它应该在做的事的线程信息

  • 新的线程挂了?

自然出现了如此一个怀疑思路,我们尝试本地启动程序,重新获取 jstack,出现了如下一个恐怖的图:

  • 栈溢出!

看到这个图,大概率判断会导致栈溢出了,我们的方法在进行嵌套调用,那栈溢出便是 迟早的事了,我们只在线程池调用内 catch 了 Exception。众所周知,StackOverFlowError它是一个 Error,所以没捕获到。线程也就挂了

但这和接收不到报文又有什么关系呢?

上述的排查过程已经初步可以认定,很有可能是 netty 的 nio 线程没有好好干活,没有去接收消息

  • 那么,netty 的线程在做什么呢?

嗯?这个 FgPriorityBlockingQueue 有点熟悉,这不刚刚的那个栈溢出一直在循环调用的对象吗。

所以结论浮现出来了,nio的线程,都被阻塞在priorityQueue 的offer方法里里,所以发给系统监听端口的请求,一直没有被处理。

BlockingPriorityQueue 一直在阻塞问题排查

在上面的背景介绍中,我们得知了两件事

  1. 有一个在FgPriorityBlockingQueue 中的线程,因为 StackOverflowError 挂掉了。
  2. FgPriorityBlockingQueue 在被调用 offer 方法的过程中一直在阻塞状态,等锁。
  • FgPriorityBlockingQueue是什么?

只保留了其关键代码如下所示。它是一个继承了 PriorityBlockingQueue 的类,它的 removeExpiredData 和 removeExpiredTasks 通过循环调用,让队列中的元素出队

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class FgPriorityBlockingQueue extends PriorityBlockingQueue<V> {
// ......
private void removeExpiredData() {
try {
V head = this.peek();
if (head == null) {
break;
}
// ......
V data = this.poll();
}
} finally {
this.removeExpiredTasks();
}
}

private void removeExpiredTasks() {
try {
// ......
this.removeExpiredData();
} catch (Exception e) {
// ......
}
}
}
  • 那,让我们看看PriorityBlockingQueue 是什么?
  1. 首先,它是一个 PriorityQueue,当元素被添加进队列时,将会调用排序方法
  2. 其次,它是一个 Bloking,它通过锁来支持并发入队出队,防止扩容时的并发访问导致的线程安全问题。
  3. 它是无界的,只要不超过int上限

来看看它的 offer 和 poll 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class PriorityBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {

/**
* Inserts the specified element into this priority queue.
* As the queue is unbounded, this method will never return {@code false}.
*
* @param e the element to add
* @return {@code true} (as specified by {@link Queue#offer})
* @throws ClassCastException if the specified element cannot be compared
* with elements currently in the priority queue according to the
* priority queue's ordering
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}


public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return dequeue();
} finally {
lock.unlock();
}
}

}

假设推演

这张图里看到,在 offer 方法的 lock.lock(); 行阻塞住了,线程处于 WAITING

ReentrantLock 的非公平锁代码如下

从我们的用法分析,是谁获取到的锁呢?结合我们代码中的用法,提出两个假设

  1. poll 之后获取的锁,没有释放。我大胆假设为:我们抛出StackOverFlow的那个线程,恰恰在lock.lock()的深层嵌套处,抛出的error。而此时未调用unlock,就抛出了异常。(可惜的是,线程没有捕获这个 Error,看不见相关日志)
  2. 另一个线程的 offer 方法获取了锁,由于出队的线程已经挂掉,所以队列内的元素很多,排序花费时间久。

复现验证

1. lock.lock()中的深层调用

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
PriorityBlockingQueue<String> queue = new PriorityBlockingQueue<>();
queue.offer("init");
new Thread(() -> {
queue.poll();
}).start();

new Thread(() -> {
queue.offer("123");
}).start();
}

我在两个thread中打断点,并且在ReentrantLock的setExclusiveOwnerThread方法,抛出Error。

1
2
3
4
5
6
7
8
Connected to the target VM, address: '127.0.0.1:59181', transport: 'socket'
Exception in thread "Thread-0" java.lang.Error
at java.util.concurrent.locks.AbstractOwnableSynchronizer.setExclusiveOwnerThread(AbstractOwnableSynchronizer.java:74)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:207)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
at java.util.concurrent.PriorityBlockingQueue.poll(PriorityBlockingQueue.java:536)
at com.fingard.gardpay.common.queues.FgPriorityBlockingQueue.lambda$main$0(FgPriorityBlockingQueue.java:229)
at java.lang.Thread.run(Thread.java:748)

线程栈如图:

这是一个完美复现,并且再也不会有能对这个Queue入队的元素了

2 另一个offer正在执行

我们实际数量是不大的,从MAT分析工具中,对于dump文件拿到的队列内元素大概 < 2000个。排序算法也不复杂,即使按照 O(nlog(n))的排序算法计2000个元素,每次插入也就是15201次计算,远小于一般认为的程序执行上限10^7 - 10^8 之间的运算数量。基本排除

openssl生成各类格式的秘钥及java程序的读取

前言

工作中,因为会对接不同地方的接口,经常遇到不同保存形式的 公私钥证书。初次遇见,存在很多专有名词,如:(jks,pcks#8, CBC, X509, CER, PEM)。分辨和理解起来难免有些复杂。所以出此文档,一方面更快地理清各种名词的含义,一方面,从java代码的角度,快速对这些公私钥进行读取解析。

名词分类

一、存储格式维度

  1. JKS (Java KeyStore)
    • Java专属的密钥库格式,用于存储私钥、证书链和受信任证书。
    • 文件扩展名:.jks
    • 关联标准:Java安全体系。
  2. PKCS#12 / PFX
    • 跨平台的密钥库格式,支持存储私钥、证书链和证书。
    • 文件扩展名:.p12.pfx
    • 关联标准:PKCS#12(公钥密码学标准)。
  3. Keystore(泛称)
    • 广义的密钥库容器,可以是JKS、PKCS#12、BKS(Android专用)等具体实现。

二、证书格式维度

证书(如 .crt)本质上是公钥 + 签名信息

  1. X.509
    • 国际标准的证书格式,定义公钥证书的结构(如版本、序列号、签名算法等)。
    • 常见扩展名:.cer, .crt, .der, .pem
  2. CER
    • 通常指X.509证书文件,编码格式可能是DER(二进制)或PEM(Base64文本)。
  3. PEM (Privacy Enhanced Mail)
    • 基于文本的证书/密钥编码格式,用-----BEGIN CERTIFICATE-----包裹Base64内容。
    • 常见扩展名:.pem, .crt, .key
  4. DER (Distinguished Encoding Rules)
    • 二进制格式的X.509证书或密钥,适用于机器处理。
    • 常见扩展名:.der, .cer

三、加密标准维度(PKCS系列)

  1. PKCS#1
    • 定义RSA公钥/私钥的语法和加密机制(如RSAES-OAEP)。
    • 常见于.key文件或PEM格式。
  2. PKCS#7
    • 定义加密消息语法(如数字签名、证书链的封装),常用于.p7b证书链文件。
  3. PKCS#8
    • 定义私钥的通用存储格式,支持不同算法(如RSA、ECDSA)。
    • 编码格式为PEM或DER。

四、加密算法与模式维度

  1. 对称加密模式
    • CBC (Cipher Block Chaining)
      块加密模式,需初始化向量(IV)。
    • GCM (Galois/Counter Mode)
      支持认证加密(AEAD)的模式,常用于TLS 1.2+。
    • ECB (Electronic Codebook)
      基础块加密模式,安全性较低。
  2. 非对称加密算法
    • RSA
      基于大数分解的公钥算法,用于加密和签名。
    • ECC (Elliptic Curve Cryptography)
      基于椭圆曲线的公钥算法(如ECDSA、EdDSA)。

五、密钥类型维度

  1. 对称密钥 (Symmetric Key)
    • 如AES密钥(128/256位),用于加密数据。
  2. 非对称密钥对 (Asymmetric Key Pair)
    • 公钥 (Public Key)
      公开分发,用于加密或验证签名。
    • 私钥 (Private Key)
      严格保密,用于解密或生成签名。

六、协议与场景维度

  1. TLS/SSL
    • 使用X.509证书和密钥(如RSA/ECC)建立安全通信。
  2. 数字签名
    • 使用私钥签名(如SHA256withRSA),公钥验证。
  3. 证书颁发机构 (CA)
    • 签发X.509证书的权威实体,依赖PKI体系。

加密模式

快速的 openssl 生成命令

  1. openssl 生成pkcs1格式的私钥,密钥长度1024位, (PKCS1)
    1
    openssl genrsa -out private.pem 1024
  2. PKCS1私钥转换为PKCS8.
    1
    openssl pkcs8 -topk8 -inform PEM -in private.pem -outform pem -nocrypt -out pkcs8.pem
  3. PKCS8格式私钥再转换为PKCS1格式
    1
    openssl rsa -in pkcs8.pem -out pkcs1.pem
  4. 从pkcs1私钥中生成pkcs1公钥
    1
    openssl rsa -in private.pem -RSAPublicKey_out -out public.pem
  5. 从pkcs1私钥中生成pkcs8公钥
    1
    openssl rsa -in private.pem -pubout -out public.pem
  6. 从pkcs8私钥中生成pkcs8公钥
    1
    openssl rsa -in pkcs8.pem -pubout -out public_pkcs8.pem
  7. pkcs8公钥转pkcs1公钥
    1
    openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public_pkcs1.pem
  8. pkcs1公钥转换为pkcs8公钥
    1
    openssl rsa -RSAPublicKey_in -in pub_pkcs1.pem -pubout

参考资料:

  1. openssl 生成RSA秘钥 以及 秘钥格式的相互转换 https://blog.csdn.net/hellokandy/article/details/121162324
  2. RSA密钥文件格式 https://blog.csdn.net/ChenTest100/article/details/138243776
  3. 使用openssl生成Java可以读取的密钥和自签名证书 https://blog.alvinkwok.cn/post/7