MoreRSS

site iconAllen Hua修改

Java工程师,常驻南京。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Allen Hua的 RSS 预览

使用 Java 写了一个局域网端口扫描器

2026-02-26 14:11:43

一开始打算使用 web 来做的,但是发现浏览器没有那么高的权限,限制来自浏览器安全模型。浏览器不允许:创建任意 TCP Socket,主动向任意 IP:PORT 建立 TCP 连接,探测端口是否 open / closed / filtered

最终我改回了 Java 实现。并开源在了 https://github.com/hellodk34/LanPortScanner

直接下载 jar 包:https://github.com/hellodk34/LanPortScanner/releases


LanPortScanner - 局域网端口扫描器

一个用 Java 编写的局域网端口扫描工具,支持 TCP 和 HTTP 协议扫描。现成的 LanPortScanner.jar 文件(仅5.18KB)请在项目 release 页面下载。

我的使用场景举例

  1. 群晖 nas 设备接入了一个局域网,但是我没有局域网管理权限,可以借助本程序快速扫描群晖的 5000 web 端口
  2. 我的 x86 小主机安装了 immortalwrt 系统作为旁路网关使用,小主机开机后也可以借助本程序快速扫描出来 IP,可以使用 http 扫描 88 端口(luci web uhttpd 的服务端口默认是 80),也可以使用 tcp 扫描 1003 端口(dropbear ssh 服务默认端口是 22)

✨ 特性

  • 🔍 支持 TCP 和 HTTP 两种协议扫描
  • 🎯 精确的参数验证(IP格式、范围、端口等)
  • ⚡ 多线程并发扫描,提高效率
  • 📊 详细的扫描结果和统计信息
  • 🔒 仅支持局域网IP扫描,确保安全
  • 🔄 兼容 Java 8 到 Java 21+

🚀 快速开始

1. 编译项目

# Windows一键操作(双击运行)
build_win.bat

# linux/macOS一键操作
chmod +x build_linux.sh
./build_linux.sh

# 或者手动编译
javac -encoding UTF-8 LanPortScanner.java
jar cfe LanPortScanner.jar LanPortScanner LanPortScanner.class

2. 运行扫描

# TCP 扫描(默认2秒超时)
java -jar target/LanPortScanner.jar tcp 192.168.1.1 192.168.1.254 80

# HTTP 扫描(自定义超时3秒)
java -jar target/LanPortScanner.jar http 192.168.10.1 192.168.10.254 8080 3

# 跨网段扫描
java -jar target/LanPortScanner.jar tcp 192.168.0.1 192.168.1.254 22 5

📖 参数说明

java -jar LanPortScanner.jar <协议> <起始IP> <结束IP> <端口> [超时秒数]
参数 必需 说明 示例
协议 tcphttp(不区分大小写) tcp
起始IP 起始IP地址(必须是局域网IP) 192.168.1.1
结束IP 结束IP地址(必须是局域网IP) 192.168.1.254
端口 目标端口号(1-65535) 80
超时时间 超时秒数(默认2秒) 3

📋 支持的局域网IP范围

  • 10.0.0.0/8:10.0.0.0 - 10.255.255.255
  • 172.16.0.0/12:172.16.0.0 - 172.31.255.255
  • 192.168.0.0/16:192.168.0.0 - 192.168.255.255

🛡️ 安全特性

  • ❌ 禁止扫描公网IP地址
  • ❌ 严格的参数验证
  • ❌ 防止IP范围错误
  • ❌ 限制端口范围(1-65535)

📊 输出示例

========================================
🔍 局域网端口扫描器
========================================
协议类型: TCP
IP范围: 192.168.1.1 - 192.168.1.254
目标端口: 22
超时设置: 2 秒
扫描总数: 254 个IP
========================================
开始扫描...

✅ 发现设备: 192.168.1.100:22
✅ 发现设备: 192.168.1.150:22

========================================
🏁 扫描完成
========================================
总耗时: 15.23 秒
扫描IP数: 254
发现设备: 2 个

发现的设备列表:
  - 192.168.1.100:22
  - 192.168.1.150:22
========================================

⚠️ 注意事项

  1. 仅限局域网使用:本工具只能扫描局域网IP,无法扫描公网IP
  2. 遵守法律:请仅在自己拥有权限的网络中使用
  3. 性能建议
    • 建议扫描范围不要超过C类网段(254个IP)
    • 可根据网络情况调整超时时间
    • 大范围扫描时建议增加超时时间

🛠️ 技术细节

  • 兼容性:Java 8 - Java 21+
  • 并发:使用线程池实现并发扫描
  • 协议支持
    • TCP:使用Socket连接测试
    • HTTP:使用HttpURLConnection测试
  • 编码:UTF-8

📦 依赖

本项目为纯Java实现,无外部依赖,可直接编译运行。


运行截图

局域网端口扫描器使用截图.jpg

openwrt使用外置根extroot机制扩展根分区大小

2026-02-01 19:08:58

这里我们把整个 OpenWrt 系统迁移到 ext4 磁盘上,通过 extroot 机制实现一个更大的根分区。


extroot 机制简述(OpenWrt)

在 OpenWrt 中,extroot 是一种利用外部存储设备(USB / SATA / SD 卡)来扩展系统可写空间的机制。
它并不是对原有根分区进行在线扩容,而是通过启动后切换根文件系统或 overlay,让系统运行在一个容量更大的外部磁盘之上。

工作原理

  1. 启动阶段

    • OpenWrt 首先从内部 Flash 启动最小系统
    • 根文件系统通常是只读的 squashfs,配合一个很小的可写 overlay
  2. 挂载外部存储

    • 启动过程中,block-mount 根据 /etc/config/fstab
    • 挂载外部磁盘分区(通常是 ext4
  3. 根文件系统切换

    • 如果检测到 extroot 配置
    • 外部磁盘中的根文件系统会接管 /overlay(或直接接管 /
    • 系统随后运行在外部磁盘之上

效果

  • 从系统视角看:

    • //overlay 位于外部磁盘
    • 文件系统类型通常为 ext4
    • 可用空间显著增加
  • 对上层应用和用户来说:

    • 与普通根分区无差别
    • 所有软件、配置、日志都写入外部存储

为什么需要复制整个根文件系统

extroot 并不是只挂载一个空目录作为 overlay,
而是要求外部存储上存在完整、可启动的 OpenWrt 根文件系统结构

因此在启用 extroot 前,通常需要将当前系统的 / 完整复制到外部磁盘中,
以确保 init、库文件和配置在切换后仍然可用。

适用场景

  • 内置 Flash 空间很小的路由器
  • 需要安装大量软件包
  • 需要更高的写入寿命或更大的日志空间

一句话总结

extroot 通过在启动过程中将 OpenWrt 的可写根文件系统迁移到外部存储设备,实现“逻辑上的根分区扩容”。


好的,官话结束,我们来讲怎么做。很简单,建议安装 diskman luci 界面配合命令行操作。

一、安装 diskman

# 更新一下软件源
# opkg update

# 安装 diskman 程序和中文汉化包
# opkg install luci-app-diskman luci-i18n-diskman-zh-cn

二、新建分区并格式化(作为新的根使用,以此扩展根分区大小)

在 luci 界面中切换到“DiskMan 磁盘管理”,编辑当前磁盘,在“分区信息”最后一行直接新建分区,这样可以快捷创建分区,不需要手动计算“起始扇区”和“中止扇区”,新建好之后建议格式化此分区为 ext4 文件系统。

diskman磁盘管理.webp

分区管理-新建分区.webp

三、回到挂载点将上述分区挂载为根/

如下图

挂载点-存储区.webp

在“保存并应用”之前,执行如下命令(就是页面上显示的那几行),我添加了注释,更方便理解每一行干了啥,作用是什么

# 创建临时挂载点,用于访问当前正在运行的内部根文件系统
mkdir -p /tmp/introot

# 创建临时挂载点,用于挂载即将作为 extroot 的外部磁盘分区
mkdir -p /tmp/extroot

# 将当前根目录 / 绑定挂载到 /tmp/introot,便于完整复制现有系统
mount --bind / /tmp/introot

# 挂载外部磁盘分区(ext4),作为未来的 extroot 根文件系统
mount /dev/sda1 /tmp/extroot

# 使用 tar 管道方式,将整个根文件系统原样复制到外部磁盘分区
tar -C /tmp/introot -cvf - . | tar -C /tmp/extroot -xf -

# 卸载临时挂载的内部根文件系统
umount /tmp/introot

# 卸载外部磁盘分区,确保数据完整写入磁盘
umount /tmp/extroot

逐行执行之后,再“保存并应用”。

现在通过 df -hT 查看根分区大小是 290.4MB

旧的根分区大小290.4兆.webp

四、重启生效

重启之后查看根分区大小,变成了磁盘余下空间 28.4G,成功实现根分区扩容。

新的根分区大小28.4G.webp

将机场ss节点批量转换成ss字符串链接批量添加到passwall

2026-01-23 14:53:00

最近我使用的机场节点都更新了,我在一个新的 immortalwrt 环境上使用原有的订阅链接,始终无法订阅到节点,我还一直以为是我找的订阅转换服务有问题。试了好几个服务,都无法订阅到节点。

测试了这些订阅转换服务(subscription converter)

订阅全部都失败了。passwall 给出的错误要么是“订阅失败,可能是订阅地址无效,或是网络问题,请诊断”,要么是 “成功解析【xxx_airport】节点数量: 0”。

我开始了研究。

旧 immortalwrt 环境上的 passwall 版本

# opkg list-installed | grep passwall
luci-app-passwall - 25.9.23-r1
luci-i18n-passwall-zh-cn - 25.270.37028~65fa739

新 immortalwrt 环境上的 passwall 版本

# opkg list-installed | grep passwall
luci-app-passwall - 25.12.16-r1
luci-i18n-passwall-zh-cn - 25.350.06350~ba7f272

版本确实不一样,源里的版本升级了。但因为 op 源里一般只保存最新的软件包,我放弃了降级,也放弃了卸载和重新安装同样的版本(要去找同版本 ipk 文件),后面的经历证明此举明智。

我开始对订阅链接产生兴趣,看看能发现什么规律。

果然!发现规律了,我使用的机场提供的订阅链接,我一般只使用 ?clash=1 的那个,在 passwall 中订阅时需要先转换,把节点目标设成 shadowsocks(SIP002)

注:SIP002 是 Shadowsocks 官方定义的标准 URI 格式规范(即标准的“ss://”链接格式)

我把 clash=1 的订阅拉到本地发现就是 clash 的 yaml 文本,最终发现所有节点都是 type: ssr,原来是这样!!!就是因为 clash 的订阅链接只返回了 ssr 节点,但我本地的 passwall 使用 ss/trojan/vmess/vless/hysteria2 节点,因为没有安装 ssr。

clash订阅的全部是ssr节点.jpg

我又从机场的web后台复制了一个 ss 节点的 ss:// 链接手动导入,可以成功导入!遗憾的是,机场提供了批量复制 ssr 链接功能,但是没有提供批量复制 ss 链接功能。于是我开始想办法手动转换。

我本地访问了 quanx 的订阅链接 ?list=quantumultx,得到一个94行的文本,乍一看就能看出规律,47个节点,每个节点各有一个 ss 链接和一个 ssr 链接。长得像这样

shadowsocks=xxx.com:3044, method=chacha20-ietf, password=xxx, ssr-protocol=auth_aes128_sha1, ssr-protocol-param=xxx, obfs=plain, obfs-host="xxx, tag=Standard|台湾|IEPL|01
shadowsocks=xxx.com:30333, method=rc4-md5, password=xxx, obfs=http, obfs-host=xxx, obfs-uri=/, tag=Standard|台湾|IEPL|01

于是我将他们转到 linux 下,

cat > sub.txt<<'EOF'
94 行文本
EOF

执行命令 grep -v 'ssr-protocol=' sub.txt > sub_new.txt,将 ssr 节点对应的行全都删掉,重定向到新文件,新文件里全部都是 ss 节点信息。

最后我就是找规律,看机场给的 ss 链接是根据什么规律生成的,然后将剩下的 47 个节点数据全部转换成 ss:// 文本链接。附上我使用的 java 代码(java17)

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class SSUrlParser {

    public static String parseAndGenerate(String line) throws Exception {
        Map<String, String> map = new HashMap<>();
        for (String part : line.split(",")) {
            String[] kv = part.trim().split("=", 2);
            map.put(kv[0], kv[1]);
        }

        String[] hostPort = map.get("shadowsocks").split(":");
        String host = hostPort[0];
        int port = Integer.parseInt(hostPort[1]);

        String userInfo = map.get("method") + ":" + map.get("password");
        String userInfoBase64 = Base64.getEncoder().encodeToString(userInfo.getBytes(StandardCharsets.UTF_8));

        // 插件参数
        String pluginRaw = "obfs-local;obfs=" + map.get("obfs") + ";obfs-host=" + map.get("obfs-host");
        String pluginEncoded = URLEncoder.encode(pluginRaw, StandardCharsets.UTF_8.toString());

        // group 参数:Base64 编码
        String group = map.getOrDefault("tag", ""); // 没有单独 group 时用 tag
//        String groupBase64 = Base64.getEncoder().encodeToString(group.getBytes(StandardCharsets.UTF_8));

        String groupName = "机场名称特有字符串"; // 这里的规律,可能不同机场不一样
        String groupBase64 = Base64.getEncoder()
                .withoutPadding()
                .encodeToString(groupName.getBytes(StandardCharsets.UTF_8));

        // tag 参数 URL encode
        String tagEncoded = URLEncoder.encode(group, StandardCharsets.UTF_8.toString());

        return "ss://" + userInfoBase64
                + "@" + host + ":" + port
                + "/?plugin=" + pluginEncoded
                + "&group=" + groupBase64
                + "#" + tagEncoded;
    }

    public static void main(String[] args) throws Exception {
        String input = """
                shadowsocks=xxx.com:30333, method=rc4-md5, password=xxx, obfs=http, obfs-host=xxx, obfs-uri=/, tag=Standard|台湾|IEPL|01
                shadowsocks=xxx.com:30334, method=rc4-md5, password=xxx, obfs=http, obfs-host=xxx, obfs-uri=/, tag=Standard|台湾|IEPL|02
                shadowsocks=xxx.com:30335, method=rc4-md5, password=xxx, obfs=http, obfs-host=xxx, obfs-uri=/, tag=Standard|台湾|IEPL|03
                """;  // 使用 Text Blocks 特性
        // 遍历每一行
        input.lines()
                .map(String::trim)        // 去掉首尾空白
                .filter(line -> !line.isEmpty()) // 判空
                .forEach(line -> {
                    try {
                        System.out.println(parseAndGenerate(line));
                    }
                    catch (Exception e) {
                        throw new RuntimeException("当前行计算出了问题...");
                    }
                }); // 调用自定义方法
    }
}

上述代码执行后即可将机场ss节点信息全都转换成 ss链接,但是注意,这可能不适用于你的机场。最终我将47行ss链接成功一次性导入 passwall:节点列表标签页下 -> 通过链接添加节点功能 -> 一行一个,全部复制到这里,可以添加一个单独的分组。

passwall 节点列表 - 通过链接添加节点- 一行一个.jpg

没有使用订阅的方式(机场没有提供符合我需求的订阅链接),成功批量导入节点!完美。

没有使用订阅的方式-成功批量导入节点.jpg

不过后来我发现,将 ?list=quantumultx 这个订阅链接作为源,然后转换为 ShadowsocksSIP002,最终生成临时订阅链接放进 passwall,成功订阅 😂️,解析到了93个,应该是把所有的节点 ss/ssr 都识别出来了,passwall 这边每个节点显示了两次,至于为什么还少一个(应该94个),就不再探究了。

最终学到了:
ss:// URL 中,只有 method / password / host / port / plugin 是“协议级可推导”的
grouptag 都是“订阅生成器的业务字段”

还有

String groupBase64 = Base64.getEncoder()
                .withoutPadding()
                .encodeToString(groupName.getBytes(StandardCharsets.UTF_8));

String groupBase64 = Base64.getEncoder()
                .encodeToString(groupName.getBytes(StandardCharsets.UTF_8));

的区别。

给机械革命钛钽plus换屏:NY2换成NZ2

2025-09-13 23:41:00

本文记录给我的机械革命钛钽PLUS换屏。

旧屏幕用了将近4年(2021年6月购入),今年花屏(几月份来着,不记得了,这不重要),然后全黑,最后怎么都不亮了。我外接了两块显示器使用,一根使用 typec to hdmi 接入显示器,另一根使用 hdmi to hdmi 接入显示器。最近准备给这个电脑换一下屏幕,了解到机革同年甚至22年几个系列都喜欢用京东方 NY2 屏幕,网上能搜到很多翻车信息。

哔哩哔哩这两个视频评论区有很多有价值的信息,如果你有同款电脑并且屏幕有问题的不妨看看。

机械革命钛钽PLUS 花屏,黑屏,坏屏问题:

【铭达数码电子商行:机械革命蛟龙7 钛钽Plus 旷世 笔记本屏幕换屏教程。】 https://www.bilibili.com/video/BV1PN4y1W7jf/

【机械革命 钛钽plus 刚过保 NE173QHM-NY2屏幕挂了 自己换屏】 https://www.bilibili.com/video/BV1mP4119768/


原屏幕

我在淘宝买了一块新的,A+品质,将近无暇的屏幕,我选择了NZ2这个型号

屏幕到货,准备拆下旧的屏幕,建议参考上面第二个bilibili视频,记住要点

  • 一定要先断电再拆屏幕(电池的排线也要断开,所以要拆后盖)
  • 易拉胶一定要清除干净,这玩意还是挺粘的,不清除干净影响屏幕取下。如果易拉胶断头了,用薄的翘片或者不用的卡片(比如说过期了的理发店会员卡)借助吹风机从上往下“切断”易拉胶,这个时候如果能将易拉胶拉出来就更好,后面就小心“卷出来”易拉胶即可
  • 换上新的屏幕先测试屏幕是否能点亮,可以再装回去

我拆下了的旧的显示屏
拆下了旧的显示屏.webp

一次就成功点亮显示屏
一次就成功点亮显示屏.webp

旧的NY2屏幕
旧的NY2屏幕.webp

新的NZ2屏幕
新的NZ2屏幕.webp

换上了新的 NZ2 显示屏,但是开机后只能 60Hz 使用,切换到 240Hz 就闪屏。我在网上查询,都说是intel核显电压不足导致的,也可能就是核显问题,和供电电压无关。再问客服,客服给了我一个视频,让我从 bios 里切换到 dGPU only (也就是独显 only),默认是 hybrid 核显独显混合模式。实际上就是机械革命的电竞控制台独显直连的开关,我观察了开和关会让bios里变成混合模式/dGPU only,只不过有些人说在bios里修改会更稳定 那就切独显直连吧

但我切换到独显就无法开机,重启(或者开机)进入到输入密码的界面机器就强制关机了(键盘灯灭了),后面问客服,客服找到了技术,技术找我要电话,打电话给我,但他也并没有给我任何实质性的建议,或者操作方式,于是我就说算了吧,我就60Hz 用。。。

我在网上搜了一些资料

  • 有的说需要重置 bios,我做了没有用
  • 有的说需要关闭 windows 电源管理的 fast boot 快速启动,但我检查了我的 win11已经没有这个选项了

我都测试了,但没有成功。在我抱着试试的态度,将 Secure Boot 关掉,最后又因为 BitLocker 问题重新打开了 Secure Boot,最终保存 bios 设置重启电脑,居然就成功启动了!!!

最后核显被成功屏蔽,资源管理器当中只显示了 GPU0 - nvidia rtx 3060 laptop gpu,并且在显示设置中将刷新率调到 240Hz 也正常运行,不会闪烁了,不会闪屏了,太棒了!

资源管理器当中 GPU0.webp

并且这一个 display 也显示 connected to rtx 3060

内置屏幕已连接到rtx3060独显.webp

所有测试完毕,使用3mm黑色双面胶粘贴在屏幕周围塑料边框上再将边框贴紧
最后使用3mm黑色双面胶粘贴在屏幕周围塑料边框上再将边框贴紧.webp

将屏幕装好,再拆下D壳,将电池排线重新连接上,最后装好D壳,就完成了,完美搞定(实际上不是完美,在用纤维布擦拭屏幕时将呼吸灯的一颗灯珠碰掉了。。😭️ 不过也无伤大雅就是)

记录一次pve宿主机和上面的debian虚机无故down机事件

2025-09-05 17:13:00

我去年8月出去旅行,今年8月出去旅行,在外面访问家里 memos,都挂了,我每次都吐槽出远门家中网络必挂,-_-。一开始我一直认为是主路由网络挂了,以前我用的是野路子来的 OpenWrt 固件(比如各种高大全固件),但是今年我转战了 ImmortalWrt 官方构建的固件,这个稳定性很好建议使用这个。建议从他们的 firmware-selector 网站下载适合你的镜像 -> https://firmware-selector.immortalwrt.org/

第一次down机买了个风扇给机械硬盘散热

最近发现可能不是这么回事,应该是我的 pve 宿主机和上面的 debian11 虚拟机的问题,这个问题从一次远程访问家中 pve 宿主机执行一些命令时候报错 Input/output error 开始。

我的 pve 宿主机上有两块磁盘,如下

NAME SIZE MODEL
sda 3.6T ST4000VX000-2AG166
nvme0n1 238.5G HS-SSD-C2000Pro 256G

在宿主机上执行各种需要读写磁盘的命令都会报错 Input/output error
inputoutputerror.webp

回家后发现主机温度很高,直接强制断电了,然后将外壳拆下,发现机械硬盘非常烫,我就赶紧拿起风扇对着吹。
pve主机硬盘非常烫.webp

大概吹了30多分钟,主机很凉快了,尝试再次开机,发现一切如常。支线:决定购买风扇增加散热

第二次down机发现已失效的nfs挂载

第三天发现memos又无法访问了,进 pve 终端查看虚机状态是 RUNNING,但是 ping 不通,最后只好执行 qm stop 100 && qm start 100 重启了一下虚机,也正常了。接着进入debian11 的终端立即检查,发现了关键错误 Read-only file system, rsyslogd: file '/var/log/syslog' write error - OS error: Read-only file system

从日志中的 Read-only file system 错误可以看出,你的系统根文件系统已被内核强制挂载为 只读模式 。这是Linux系统检测到严重磁盘错误时的保护机制,防止进一步数据损坏。以下是详细分析和解决方案:

关键日志解读
核心错误 :rsyslogd: file '/var/log/syslog' write error - OS error: Read-only file system
含义 :rsyslog(系统日志服务)尝试写入 /var/log/syslog 时失败,因为文件系统已处于只读状态。

还有一段核心错误

kernel: FS-Cache: Duplicate cookie detected
kernel: FS-Cache: O-cookie c=00000000439c879b [p=000000000e48f285 fl=222 nc=0 na=1]
kernel: FS-Cache: N-key=[16] '0400000002000000020008010a0a0a01'

这段日志帮我找到了磁盘挂载的问题。我查看 /etc/fstab 发现了4个无效挂载点(下图是我注释之后截的)
fstab 挂载了不存在的nfs磁盘.webp

原来是我以前在软路由(10.10.10.1)上接了这4块硬盘,但是后来取下了这几块硬盘,然后这边没删除,给忘掉了。几年前就意识到要将家里各种设备的职责划分清楚,软路由就只专心做好路由,不搞下载和做种,所以当时就把这几块硬盘拔掉了,对应的 NFS 服务也就不存在了。所以现在这个小问题的解决办法就是删除这几行或者注释这几行。

pve宿主机 /etc/fstab 这个文件还硬编码写死了这几个挂载,导致拖慢系统,有时候甚至卡死了,于是我赶紧添加了注释,重启了pve,再次执行 journalctl -b -p 3 --no-pager 输出如下

-- Journal begins at Sun 2025-07-27 21:59:23 CST, ends at Fri 2025-09-05 22:04:09 CST. --
-- No entries --

没有输出就是最好的状态,于是我以为这就解决问题了。

第三次down机安装intel-microcode后彻底解决

第四天发现又访问不了 memos 了。这一次一定要找到原因。在我大量的 google, bing, chatgpt, deepseek 之后,看到一些有用的信息,猜测有可能是内核版本 5.13.19,pve版本7.1-7,还有我的CPU Intel(R) Xeon(R) CPU E3-1265L v3 @ 2.50GHz 之间的冲突。也看到了国内恩山论坛这个帖子

https://www.right.com.cn/forum/thread-8266555-1-1.html [虚拟机相关] 记录对于说N5105在PVE不稳定无故死机的一种解决方法

最后我增加了 non-free 的四个源之后,试图安装 intel-microcode,期待解决问题。

新的源文件/etc/apt/sources.list内容

# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free

deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free

deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free

# 以下安全更新软件源包含了官方源与镜像站配置,如有需要可自行修改注释切换
deb https://security.debian.org/debian-security bullseye-security main contrib
deb https://security.debian.org/debian-security bullseye-security main contrib non-free

执行 apt update 之后执行 apt-get install intel-microcode,成功安装 intel-microcode 软件包

安装微码软件包.webp

一段和chatgpt的对话,它照着日志讲出了核心原因
一段和chatgpt的对话它讲出了核心问题对照着日志.webp

目前接着观望pve宿主机和debian11虚机的运行情况。

后续更新

2025年09月15日上午更新,稳定运行,10.10.10.5 是虚拟机 IP,宿主机本身的IP是 10.10.10.3,虚机没断过网络,各种服务也没有 down 过。基本上可以判断出之前就是因为 intel 微码没有安装导致的虚拟机 down…… 不过这个机器组装以及系统安装是在 2021 年,怎么现在才出现这样的问题呢……有点不解

# uptime
08:54:49 up 8 days, 19:06,  1 user,  load average: 0.02, 0.03, 0.05

# ping -c 4 10.10.10.5
PING 10.10.10.5 (10.10.10.5) 56(84) bytes of data.
64 bytes from 10.10.10.5: icmp_seq=1 ttl=64 time=0.170 ms
64 bytes from 10.10.10.5: icmp_seq=2 ttl=64 time=0.198 ms
64 bytes from 10.10.10.5: icmp_seq=3 ttl=64 time=0.215 ms
64 bytes from 10.10.10.5: icmp_seq=4 ttl=64 time=0.214 ms

--- 10.10.10.5 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3074ms
rtt min/avg/max/mdev = 0.170/0.199/0.215/0.018 ms

给图床部署cdn腾讯云的edgeone并排查Cache-Control max-age 3600的问题

2025-08-19 11:13:00

正篇

最近发布了徒步反穿武功山,以及自驾皖浙赣的游记文章,点我直达 ,我分享到了 v2ex 以及 linux.do ,各路网友都说图床太慢了。由于原文图片数量的确较多,我的图床部署在家里,然后只有 VPS 的6Mbps小水管,在并发不大的时候阅读文章还行(图片加载的速度还OK,并且有配置懒加载,所以体验还行),但是昨天发了帖子之后有几波流量激增。于是我寻找办法:

  • 准备使用活菩萨 cloudflare,但是要整体迁移域名解析到 cf,因为我这个域名有经过备案,并且数量众多,而且还要兼顾国内访问速率,我就放弃了 cloudflare 方案
  • 准备在源图床那边创建新的存储策略,当前是存储在本地磁盘的,然后想想又要买对象存储,又放弃
  • 最后发现了腾讯云的 edgeone,开通试试一个月只要4.8元,实付款4.7,最近微信支付很多借记卡、信用卡减免活动

最终我买了一个月的 edgeone,只花费4.7元~

现在部署好了 cdn,访问速度大大增加。当前时间点 2025-08-19 10:30:42 edgeone 流量情况
edgeone流量情况.webp

我这篇文章不算是教程吧,只能是个人的心路历程,我也不想写保姆级教程,太费事了。在此就简单讲讲过程和原理。

  1. 首先购买了一个月的 edgeone 产品,需要绑定站点,首先绑定根域名,也就是 940304.xyz 然后经过认证
  2. 域名认证方式是:添加 TXT 类型记录,主机记录和对应的值都是 edgeone 给出的,我的域名托管在 dnspod,需要在 dnspod 那边改
  3. 也可以不认证根域名,每个二级域名都分别认证
  4. 通过CNAME 方式接入 CDN,需要先停止解析或删除之前的 image 主机名的 A记录(DNS A记录),然后添加 image 主机名的 CNAME 记录(因为同一个主机名A记录和 CNAME记录只能有一个条)
  5. 创建成功后就等待 edgeone 下发 cdn 配置,等待部署生效即可

我顺利的部署成功,也看到 edgeone 流量页面产生了非常多的数据,当前我博客中的多图文章访问体验肯定就好很多了。

但是后面我发现一个问题,就是图片资源访问后返回头当中的 Cache-Control 始终是 max-age=3600,这说明 cdn 边缘节点只会缓存这张图片一个小时,如果用户在一个小时之后访问同链接会无法击中缓存,导致cdn节点回源获取原始文件,这势必会增加很多源站的流量,我的公网VPS以及家里服务器的流量都会增大,设置CDN的效果就大为下降了,而且我这就是图床程序,资源是静态的,不需要长时间变动。一般情况下图片资源都会设置30天后过期,也就是 max-age=2592000(单位s),于是我开启了排查。

在部署 edgeone 之前,我的图床架构是这样的
图床旧的网络架构.webp

在部署 edgeone 之后,我的图床架构变成这样
部署了cdn后的图床架构.webp

在任意有网络机器执行 curl -I https://image.940304.xyz/i/2025/05/21/682dd7d3137c3.jpg 发现返回 Cache-Control: max-age=3600,我开启了漫长的排查,总结下来就是

1、在VPS上修改 nginx 配置,显式增加 Cache-Control 的返回头,设置成 public, max-age=2592000,但是无果
2、在以上基础上,区分单独的匹配图片的 location,因为之前是一个反向代理 location / 就能搞定

配置是这样的

location ~* \.(jpg|jpeg|png|gif|webp|ico|css|js|svg|woff2)$ {
                proxy_pass http://127.0.0.1:81;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Host $server_name;
                proxy_set_header X-Forwarded-Proto https;
                proxy_set_header Host $host;

                # 30天缓存(仅对静态资源)
                add_header Cache-Control "public, max-age=2592000, s-maxage=2592000";

                # 跨域允许设置,允许所有跨域
                add_header 'Access-Control-Allow-Origin' *;
                # 自定义 Debug 头部(仅用于测试)
                add_header HDKRESP "This-request-matched-jpg-location_test"; # 新增
                # 跨域允许设置结束
                # 防盗链设置,因为上述跨域是允许所有的,这里就要设置防盗链从而进行域名的白名单设置
                valid_referers none blocked 940304.xyz *.940304.xyz hellodk.cn *.hellodk.cn hellodk.com *.hellodk.com 10.10.10.5;

                #nginx不允许嵌套if语句,这个很关键,否则nginx -t 会测试失败,无法reload或者start nginx服务

                set $block_access 0;

                # 如果 referer 无效
                if ($invalid_referer) {
                        set $block_access 1;
                }

                # 根据之前的判断结果决定是否拦截
                if ($block_access) {
                        rewrite ^/ https://i.imgur.com/CB4Fseq.jpeg;
                }
        }

这样的确能增加Cache-Control 的返回头,但是edgeone 那边始终没有生效。我还去lsky pro 图床程序的源 apache2 服务端添加了 Cache-Control 头,因为根据 edgeone 的规则,会先读源站的缓存相关的返回头(Cache-Control:s-maxage, Cache-Control:max-age, Expires),如果有会优先遵循源站的配置

edgeone 的文档:https://cloud.tencent.com/document/product/1552/87651

edgeone默认缓存规则
edgeone默认缓存规则.webp

默认缓存规则文字说明
默认缓存规则文字说明.webp

3、发现修改vps nginx配置无果,我进入了 lsky pro 容器,做了以下操作

# Apache 需要启用 mod_headers 才能使用 Header set 指令,启用这个模块
a2enmod headers

# 修改生效配置文件 `/etc/apache2/sites-enabled/000-default.conf` 可以通过命令 `apache2ctl -S` 寻找生效的虚拟主机所在文件
# 并增加如下配置
<LocationMatch "^/i/.*\.(jpg|jpeg|png|gif|webp|ico|bmp)$">
    Header set Cache-Control "public, max-age=2592000"
</LocationMatch>

# 重启apache2服务
service apache2 restart

现在图床源站点会给静态图片资源返回 Cache-Control 了,在图床容器中执行 curl -I http://localhost/i/2025/05/21/682dd7d3137c3.jpg 有如下返回

HTTP/1.1 200 OK
Date: Tue, 19 Aug 2025 03:11:18 GMT
Server: Apache/2.4.53 (Debian)
Last-Modified: Wed, 21 May 2025 13:40:35 GMT
ETag: "ef67-635a58279843c"
Accept-Ranges: bytes
Content-Length: 61287
Cache-Control: public, max-age=2592000
Content-Type: image/jpeg

在 edgeone 管理页面清除了这个URL https://image.940304.xyz/i/2025/05/21/682dd7d3137c3.jpg 的缓存,从 EO-Cache-Status MISS 到 HIT(也就是第一次请求未击中缓存cdn节点回源了,到第二次击中了缓存),返回的 max-age 仍然是 3600

4、开始排查 edgeone 这边,终于找到了原因所在!

需要在这里点击站点加速,我以为默认的全局配置就是对的,因为设置的是遵循源站的 Cache-Control,所以一直没细看
点击站点加速.webp

这里全局配置是正确的,没毛病,但是右侧还有一个规则引擎默认是打开的(但是默认没有任何配置,尼玛坑死我),并且优先级更高。
全局配置和规则引擎.webp

规则引擎默认打开并且默认无配置坑我导致max-age一直是3600!!!(欲哭无泪
规则引擎默认打开并且默认无配置坑我导致max-age一直是3600.webp

最后我因为不需要个性的规则引擎配置,我就将这边关掉了,直接使用全局配置,遵循源站的 Cache-Control 即可。

最终执行 curl -I https://image.940304.xyz/i/2025/05/21/682dd7d3137c3.jpg 返回了 Cache-Control: public, max-age=2592000 最终解决,这样可以减少 cdn 回源的频率,需求满足!

ok,如果你有任何疑问,欢迎留言与我讨论。

番外篇

edgeone国际版测速分享到X或者Facebook得FreePlan,网站 https://edgeone.ai 甚至分享到X+分享到Facebook可得两份FreePlan
edgeone国际版测速分享到X或者Facebook得FreePlan.webp

我等当前买的国内版失效之前的几天就切换到国际版。但是需要注意的是,腾讯云的一份身份认证只能实名制一个腾讯云账号,edgeone国际版要想使用中国大陆节点需要实名认证的账号才行。我的身份已经实名了国内版的了,后面大概就选择国际节点

国内版edgeone产品,4.7元买的一个月
国内版edgeone产品4.7元买的一个月.webp