栈溢出导致 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

群晖docker 安装 chrome,访问服务器内的页面

本文将基于 DSM 群晖操作系统,利用 docker 容器管理,简单介绍 chrome 的安装。

背景

由于群晖系统没有自带的浏览器,难免有一些访问本地页面的需求。如路由器的管理页面,以及一些本地不适合放到公网访问的页面等。基于此需求,可以在docker(现名称Container Manager)中安装 chrome 的内核,来实现浏览器的效果。

前提

  1. 确保你的群晖已经安装了 docker (新版叫container Manager),如下图所示。
    container Manager
  2. 请确保您的docker 能访问到docker hub 的镜像源(由于一些众所周知的原因,目前的docker hub在一般情况下是无法被访问到的)

参考文章:docker 内 clash 的安装可以查看 群晖 DSM 安装 clash

安装

下载镜像

在docker中选中注册表,右侧搜索 oldiy (网上很多文章推荐的是这个,本篇文章基于这个镜像给出)

image-20241027131636279

双击后下载,就会在映像选项卡中找到它。

运行容器

点击映像,点运行,下一步

image-20241027132027314

端口设置里填一下自己的映射。我这里不用默认的5900和8083,是怕这些端口被扫到,改一个端口防止不必要的攻击。

5900对应的是http,8083对应的是https端口

运行好后,进入网页看看情况

访问URL为: https://{本机ip}:1003/vnc.html

image-20241027132635370

界面长这样,点击连接后就可以正常进入浏览器了。

为浏览器设置密码

这一步很重要!!!

尤其是你希望你的chrome在外网开放的时候。一定不要小瞧这些攻击者,等到真的出事就晚了!

点击操作-打开终端机 (也可以直接ssh进去sudo docker exec -it 容器名 /bin/bash 这里只演示界面操作)

image-20241027132833679

点击新增-bash

image-20241027132927726

设置密码(在界面上想要粘贴命令,要先按Ctrl + A 再按 Ctrl + V 才能粘贴)

1
x11vnc -storepasswd

image-20241027133312539

让容器启动时自动加载密码配置, 先安装VIM工具;

1
apt install vim -y

然后执行如下修改设置默认加载;

1
vi /etc/supervisor/conf.d/supervisord.conf

在 command=/usr/bin/x11vnc 后面,加上:

1
-rfbauth /root/.vnc/passwd

(在vi中要先按一次 i 才可以输入)

如图

image-20241027133428308

保存退出(按键 :先 Esc ,然后输入 :wq 再按回车 )

重启容器

可以看到现在的连接就需要输入密码了

image-20241027133756522

为应用端口在路由器中设置端口映射。使能外网访问

因为大家的路由器都不一样,这里只是用我自己的小米路由器做参考。

一般都是有端口映射功能的,找到它,然后添加一条

image-20241027134012934

完成了,现在你就可以在外网,通过映射的端口,访问nas内的浏览器,访问本地应用了

博主目前主要用这个浏览器访问一些敏感内容,如clash的配置,路由器设置,以及用它来测试nas内部应用的部署情况

待更新的文章列表

  • 群晖在ddns配置下,检测ip使用直连模式的 clash 配置群晖在ddns配置下,检测ip使用直连模式的 clash 配置 (小)
  • 群晖docker 安装 chrome,访问服务器内的页面(小)
  • openssl 生成的各种格式的密钥,并使用java程序读取
  • mybatis 框架下mysql 的流式查询,游标
  • DockerFile 编写指南 - 基于 nacos 的 dockerfile,讲解对 java 应用dockerFile的编写指南
  • Spock 单元测试 新手入门
  • elasticSearch 搜索条件快速上手
  • apache httpAsync 客户端和服务端异步
  • mermaid 常用模板
  • 动态获取dubbo提供者(含自指)ip列表,并指定ip进行调用

群晖在ddns配置下,检测ip使用直连模式的 clash 配置

前提条件

  1. 使用的动态 dns 配置,使本地的群晖服务器在外网可访问
  2. docker 内安装了 clash,导致了动态 dns 检测到了机场服务商的 ip。

docker 内 clash 的安装可以查看 群晖 DSM 安装 clash

问题

image-20241020214625670

在 ddns 点击立即更新的时候,会从服务器向群晖指定的某个网址发送请求,但由于这个请求走了代理,导致服务器以为我们的ip是机场代理商的ip。

解决方案

进入上一篇文章中提到的 clash 控制台前端页面。点击 日志 选项卡

在这里可以看到之前命中的各条规则。如果找不到日志的话,可以在前面的ddns界面点击一次 立即更新 按钮

我图片中展示的是已经修改完配置文件后的样例,命中的是 using DIRECT 走直连。如果各位的 DDNS 展示的是机场服务商 ip 而非本机 ip 的,这里一般会命中一条走对应规则的记录。

对了,顺带一提,记得不要开全局模式。配置在这里

image-20241027114724711

那接下来问题就很简单,该配置文件让访问群晖的ip探测地址时,不要走代理就行了。找到config.yaml

image-20241027114941897

我添加了很多条,这里对于群晖的ip检测,一般添加红框内这一条已经足够了

1
2
3
4
5
6
7
8
9
10
- DOMAIN-SUFFIX,whatismyip.akamai.com,DIRECT
- DOMAIN,whatismyip.akamai.com,DIRECT
- DOMAIN-SUFFIX,synology.com,DIRECT
- DOMAIN-SUFFIX,synology.cn,DIRECT
- IP-CIDR,68.183.160.0/20,DIRECT
- DOMAIN,checkip.synology.com,DIRECT
- DOMAIN,checkipv6.synology.com,DIRECT
- DOMAIN,checkport.synology.com,DIRECT
- DOMAIN,ddns.synology.com,DIRECT
- DOMAIN,account.synology.com,DIRECT

记得不要放在最最后边,因为一般config.yaml的最后,有一条什么都匹配不上时的规则,如我这里的 - MATCH, others 会走Others对应的代理。如果- DOMAIN,checkip.synology.com,DIRECT放在最后边的话,优先级关系是会先命中 - MATCH, others `

修改完成后,重新启动一下clash (我是没找到重新加载配置的命令, 直接重启了)

再次尝试ddns刷新配置,发现可以正常映射为本机ip了。大功告成

如果还有不行的小伙伴,也可以用上述方法看clash的日志,看看它到底是命中了哪一条规则。关于规则的介绍:https://clash.wiki/configuration/rules.html

群晖DSM安装clash

本文将基于 DSM 群晖操作系统,利用 docker 容器管理,简单介绍 clash 的安装。

前提条件

  1. 本篇文章基于读者已有可用机场资源,文章将不包含机场推荐
  2. 确保你的群晖已经安装了 docker (新版叫container Manager),如下图所示。
    container Manager

将 clash 镜像导入 docker

由于近些日子,dockerHub 在特定情况下(特指clash安装前)可能无法访问。此时打开注册表大概率长这个样子

所以就下载不到clash,然而不下载 clash 又无法访问 docker 注册表,由此进入死循环。

下面提供几种解决方案

方案一: 临时本地 pc 开启 LAN 访问 (推荐)

打开另一台已经安装好clash的电脑,选好节点,然后点击Allow LAN

然后进入nas控制面板,找到网络,选择代理服务器,如下填写

然后就可以在 docker 中搜索安装clash了,我们选最多星星的这个

方案二:本地下载好镜像后上传至 nas 中安装

* 不推荐这种安装方式,除非就是上面的方案一不可选择的情况

如果 nas 连接本地困难的话,也可以选择在本地先下载好镜像(需要本地拥有能连接外网的docker)。

1
docker pull clash

选好对应的版本

1
docker save -o clash.tar clash:latest

然后到处到本地。访问群晖页面,选到映像

导入进去

方案三:去网上找到一个国内可访问的 镜像地址

这个只能说看运气,网上搜一搜。作者尝试这个方案的时候,就没有找到一个可以使用的国内镜像源。

配置文件和前端ui准备

这里仅做页面配置方案的启动,如果想用 docker 命令行启动的同学,配置是类似的。

1.创建本地配置文件目录,并将原来 clash 配置中的 config.yaml 复制进来

2.下载一个ui管理界面前端(强烈建议要装前端ui,否则某个节点挂的时候,每次都需要修改配置文件或者登录ssh去更改成本非常高)。

如果实在不愿意自己搭建的,有一个在线的网站。
https://clash.razord.top/#/proxies

但这种是个人网站,什么时候挂了也说不定。自己搭建好得多。

这个前端ui目前还是可用的,将其下载下来后,也放在同一个目录下。
本地目录如图所示,例如我的本地目录路径是 /docker/clash

正常应该只有红框中的两个就可以了。

修改配置使前端可访问

打开config.yaml

按如图所示配置:

记得将密码修改为你自己的登录密码。这里ui路径的 /root/ui 是实际容器内的路径。具体的路径映射我们会在下一步完成,这里照抄我的就可以了。

配置容器的路径挂载和端口映射

  1. 进入Container Manager中的镜像,选中 dreamacro/clash ,点击运行,第一个页面没什么需要配置的。直接点击下一步

  2. 在端口设置中新增两条 7890 和 9090 端口对应的外部端口,我这边依旧选择 7890 和 9090. 新增两条存储空间设置,左边的路径选择上一步中配置文件的文件路径。右边的容器内路径可以照抄我的
    image-20241020201326558

  3. 点击下一步,启动容器,在容器选项卡中找到这个容器。双击打开日志,大概长这样
    image-20241020201552898

  4. 尝试通过网页访问 9090 端口,查看网页 http://你的设备ip:9090/ui/ 填写127.0.0.1:9090并输入密码
    image-20241020203947029

  5. 检查下配置页面有没有问题
    image-20241020201805863

  6. 选择你中意的节点,这一步我相信你会的

  7. 然后进入nas控制面板,找到网络,选择代理服务器,将代理服务器地址填写为容器内的7890
    image-20241020202029252

  8. 大功告成,你可以ping一下google 或者在container manager中,点击网络注册表,测试一下连通性

你可能还会遇到

  1. clash 配置后,我通过 ddns 配置的域名,指向的是机场运营商的ip 而非我的公网本地 ip,怎么解决?

    请查看下一篇文章: 群晖在ddns配置下,检测ip使用直连模式的 clash 配置

  2. 我的公网ip指向的是路由器的端口映射,我家路由器不支持在群晖中配置端口转发,我如何在公网访问并添加ip?

    todo 请看下一篇文章:群晖docker 安装 chrome,访问服务器内的页面