DinD 中 OBS 请求超时:根因是 MTU 黑洞

在容器中调用华为云 OBS 时,请求在 DNS 解析成功后长时间没有响应,最终超时。相同 endpoint 从 Pod 的 host network 访问可以正常返回 403,因此问题集中在容器网络路径,而不是 OBS 服务本身。 先看服务端日志 失败容器的 runtime 日志有一条很重要的时间线: Endpoint=https://obs.cn-north-4.myhuaweicloud.com:443/ internet host address: phoenix-autotest.obs.cn-north-4.myhuaweicloud.com/123.60.240.82, phoenix-autotest.obs.cn-north-4.myhuaweicloud.com/123.60.240.83 这说明 DNS 解析已经完成。之后没有 HTTP 状态码,也没有 OBS 的业务异常。因此,失败点位于 DNS 之后、HTTP 响应之前,更接近 TCP/TLS,而不是 bucket、路径或签名参数。 产品的华为云插件确实直接使用 OBS SDK: this.obsClient = new ObsClient( settings.accessKeyId(), settings.secretAccessKey(), settings.endpoint()); 目录操作会调用 listObjects。在本机使用相同的 OBS 配置和 SDK 3.25.10 重放,接口可以正常返回。因此没有证据表明 AK/SK 或配置错误。 对照不同网络路径 运行服务的 Pod 使用 DinD sidecar: DOCKER_HOST: unix:///var/run/docker/docker.sock Phoenix 容器位于 DinD 的 bridge 网络中。在同一个 Pod 中做只读 HTTPS 探测,结果非常明确: ...

September 2, 2026

Talos 节点因 page_table_check BUG 重启:一次从误判到内核栈的调查

自动化测试使用 Playwright、Testcontainers 和 Docker-in-Docker。测试运行一段时间后,talos2 会从 Kubernetes 中消失,iDRAC 报告 System CPU Resetting,对应的测试分片失败。 这次调查最终拿到了完整的内核 panic 栈。直接原因不是硬件 reset,也不是 pid_max 耗尽,而是 Linux 6.18 中 PAGE_TABLE_CHECK 对 time namespace 的 VVAR 页面错误记账,触发了 BUG_ON()。 最初看到的现象 高并发 testcontainers 场景下,监控 Pod 观察到: loadavg=90+ pids=6890 threads=12513 dstate_count=6 eBPF 还记录到一分钟内大量进程和网络命名空间操作: fork_parent[runc]: 1597 fork_parent[containerd-shim]: 1209 fork_parent[dockerd]: 779 netns operations: 1303 unshare: 38 同时出现两类 D-state 栈。一类在网络命名空间清理路径: rcu_barrier netdev_run_todo default_device_exit_batch ops_undo_list cleanup_net process_scheduled_works worker_thread 另一类在容器文件清理路径: folio_wait_bit_common folio_wait_writeback truncate_inode_partial_folio truncate_inode_pages_range xfs_fs_evict_inode evict do_unlinkat __x64_sys_unlinkat 这些现象很容易让人得出一个合理但不完整的结论:高并发容器创建、删除导致 netns、workqueue、XFS 和 NVMe 同时拥塞,最后触发 hard lockup。 ...

August 31, 2026

容器化自动化测试中的 Podman 锁竞争排查

我们的端到端测试是一个由 Playwright、Testcontainers 和容器运行时组成的多层工作流。 测试工作流 每个 Playwright worker 通过 Testcontainers 管理一组相互隔离的容器和 bridge network。启动前会清理残留资源,测试期间按需启动浏览器、应用和数据库,结束后再停止并删除容器和网络。 清理残留资源 -> 创建 network 和容器 -> 执行浏览器测试 -> stop/remove 容器 -> remove network 这些操作都由 Testcontainers 通过连接 podman Docker 兼容 API 完成, worker 的 teardown 会让 stop、remove、network remove 与容器状态查询交错执行,这正是本次调查关注的并发场景。 实验条件 为了避免大型业务镜像的启动成本和业务行为干扰测试,只使用了一个小镜像: 工作负载:busybox:1.36.1 Podman:quay.io/podman/stable:v5.8.2-immutable Docker:docker:dind,Docker Engine 29.7.2 每轮 24 个并发操作,共 5 轮 两个运行时均运行在 privileged 容器内,状态目录使用 4 GiB tmpfs 每个并发操作创建 bridge network,创建并启动一个 busybox 容器,然后执行 stop、remove 和 network remove。teardown 阶段同时发起 24 个 GET /containers/json?all=1 请求,并持续采样 containers-list、/info 和 /_ping。 ...

August 27, 2026

Path.GetFullPath 与波浪号引发的路径大小写问题

有些程序会使用 Path.GetFullPath() 规范化路径,再通过 == 或 StartsWith() 判断两个目录是否相同或是否存在包含关系: var expected = Path.GetFullPath(Path.Combine(root, name)); var matched = expected.StartsWith(existingFolder); 大部分情况下它都能正常工作,Path.GetFullPath() 通常会保留传入路径的大小写,后续字符串比较也能通过。我们的代码跑了很多年,似乎没什么问题。 直到遇到了~。 假设磁盘上已有目录: D:\data 程序使用另一种大小写构造根目录,并枚举磁盘中已经存在的目录: using System; using System.IO; var root = @"D:\Data"; var name = "demo~cache"; var expected = Path.GetFullPath(Path.Combine(root, name)); var existing = Directory.GetDirectories(root, name)[0]; Console.WriteLine($"expected: {expected}"); Console.WriteLine($"existing: {existing}"); Console.WriteLine($"Equal: {expected == existing}"); Console.WriteLine($"StartsWith: {expected.StartsWith(existing)}"); 在父目录实际以 data 保存时,可能得到: expected: D:\data\demo~cache existing: D:\Data\demo~cache Equal: False StartsWith: False 两个字符串指向同一个 Windows 目录,但 Path.GetFullPath() 的结果使用了磁盘保存的 data,另一个路径仍使用程序传入的 Data。默认的 == 和 StartsWith() 区分大小写,因此判断失败。 ...

August 13, 2026

一次 MySQL SQL 超时,根因是网卡 MTU 被改成了 8192

客户环境出现 MySQL 执行 SQL 超时:连接可以建立,简单查询大多正常,但应用复用同一个数据库连接执行多次查询后,会逐渐卡住并最终超时。 相同查询直接在 Navicat 中执行却是正常的。一开始容易怀疑慢 SQL、连接池或数据库负载,但数据库侧没有对应的慢查询和异常。检查网络配置后发现,客户将服务器网卡 MTU 从默认的 1500 改成了 8192。 根因 8192 属于巨帧配置。只有服务器、交换机、路由设备和对端链路都支持相同或更大的 MTU 时,巨帧才能正常工作。 这次链路中仍存在 1500 MTU 的设备。较大的 MySQL 响应包无法被路径正确转发,造成部分数据包丢失;TCP 重传后,应用层最终表现为 SQL 执行超时。 这也解释了为什么: Navicat 的单次查询正常 同一数据库连接连续执行多次查询后更容易超时 返回结果较大时更容易触发 数据库本身没有明显异常 处理 将网卡 MTU 恢复为 1500 后,超时现象消失。 巨帧不是单机优化项。调整 MTU 前,应确认网络路径全链路支持,并在变更后覆盖长连接、多次查询和大结果集等实际业务场景。遇到“单次查询正常、复用连接后超时”的问题时,除了查 SQL 和数据库,也应检查 MTU 是否一致。

August 10, 2026

Java 启动性能优化:从 Nested Jar 切换到 Exploded 部署

为什么调整打包形式 Nested Jar 会把应用自身的类和依赖 Jar 一起封装到一个可执行 Jar 中。它的优点是交付物集中、分发方便,但运行时需要通过额外的加载逻辑访问内嵌依赖。 Exploded 形式则会提前展开应用和依赖,Java 进程启动时可以直接从文件系统读取对应的类和资源,减少访问嵌套压缩包带来的开销。 Exploded 之后如何保证 Classpath 顺序 把 Jar 展开后,一种启动方式是通过 -cp 指定应用类和依赖,然后直接运行应用的 main 方法: java -cp "BOOT-INF/classes:BOOT-INF/lib/*" com.example.MyApplication 这种方式可能带来进一步的启动性能提升,但 BOOT-INF/lib/* 的展开顺序并不适合作为稳定的 classpath 顺序保证。一旦依赖中存在同名类或同名资源,顺序变化就可能导致运行行为与原来的可执行 Jar 不一致。 因此,这次调整仍然通过 Spring Boot 的 JarLauncher 启动: java org.springframework.boot.loader.launch.JarLauncher 构建产物中的 classpath.idx 记录了依赖顺序,JarLauncher 会读取该文件并据此构造 classpath。这样既能获得解包后的启动性能收益,也能保持 classpath 顺序可预测,降低打包形式变化带来的兼容性风险。 Spring Boot 官方文档也有相关说明:Unpacking the Executable JAR。类似的启动性能差异也有其他用户反馈:spring-projects/spring-boot#40125。 测试结果 在同一台测试服务器上,分别使用 Nested Jar 和 Exploded 形式对三个包各测试 3 次,结果如下: 测试环境 项目 配置 CPU 架构 x86_64 CPU 型号 Intel Xeon Platinum 8173M @ 2.00 GHz CPU 拓扑 112 个逻辑 CPU,2 路,每路 56 核,每核 1 线程 虚拟化 QEMU / KVM,全虚拟化 NUMA 2 个节点,CPU 0-55 / 56-111 Spring Boot 3.5.14 Java 17 性能数据 包 Nested 3 次(ms) Nested 平均(ms) Exploded 3 次(ms) Exploded 平均(ms) 改善 DESIGNER 8344 / 7886 / 8386 8205.3 5256 / 5273 / 5264 5264.3 2941.0 ms,约 35.84% SERVER_LOCAL 9443 / 9439 / 9540 9474.0 5787 / 5703 / 5276 5588.7 3885.3 ms,约 41.01% RUNTIME 14493 / 14566 / 14571 14543.3 7271 / 7730 / 7268 7423.0 7120.3 ms,约 48.96% 从测试结果看,三个包的启动耗时都有明显下降: ...

August 4, 2026

避免挂载失败写入本地目录:给 Linux 挂载点加不可变保护

给程序配置一个目录来保存上传文件、备份或日志,是很常见的做法。目录可能位于外置硬盘、NFS 或其他网络存储上,例如: /srv/nas 但“目录存在”不代表“存储已经挂载”。如果网络中断、NFS 服务没有启动,或者开机时挂载单元失败,/srv/nas 仍然只是系统盘上的普通目录。程序并不知道这一点,照常读写后会发现读取到的状态与挂载的存储不同步。 这个问题的关键不是“如何让挂载更可靠”,而是要让挂载失败时的写入尽早失败。 挂载点为什么会接收写入 挂载点只是一个目录。挂载成功前,路径解析到的是父文件系统;挂载成功后,Linux VFS 才把这个目录覆盖成另一个文件系统的入口: 挂载前:/srv/nas -> 根文件系统上的目录 挂载后:/srv/nas -> 外置磁盘或 NFS 文件系统 因此,挂载失败不会让路径自动消失,应用仍然可以对它执行 open、mkdir 和 rename。只要应用有权限,数据就会落到根文件系统。 用不可变属性让错误暴露出来 Linux 的 chattr 可以修改文件系统属性。对目录设置 i(immutable,不可变)属性后,目录内容不能被创建、删除、重命名或修改,即使调用者是 root 也不能直接绕过: sudo chattr +i /srv/nas 这正好符合挂载点的保护需求: 挂载还没有发生时,写入会收到 Operation not permitted,而不是悄悄占用系统盘; 挂载成功后,路径进入外部文件系统,外部文件系统自己的权限和属性生效; 外部存储掉线后,应用得到 I/O 错误,问题可以被监控发现。 i 属性作用在挂载点下面那个“被覆盖的目录”上。挂载成功后,这个目录暂时不可见,所以不会把它的权限带到新挂载的文件系统中。 一次完整的验证 下面以 /srv/nas 和 NFS 为例。生产环境请替换为自己的地址,并先在测试机验证。 1. 创建一个空挂载点 sudo install -d -o app -g app -m 0755 /srv/nas 挂载点最好保持为空。否则外部文件系统挂载后,目录中的原有文件会被隐藏;卸载后它们又会出现,容易造成“文件凭空消失”的误判。 2. 确认当前没有挂载 findmnt -T /srv/nas mountpoint -q /srv/nas && echo '已经挂载,请先确认' || echo '当前未挂载' 如果 findmnt 显示的来源仍是根文件系统,说明此时可以继续设置属性。不要在没有确认的情况下对一个已挂载的目录运行 chattr,否则可能修改到外部文件系统根目录的属性。 ...

August 4, 2026

JDK HttpClient 遇上 FastAPI:一次 POST 请求体消失问题的排查

同一个 POST 请求,用 fetch、PowerShell 或 curl 调用都能成功,通过 JDK HttpClient 发出时却得到 400。换成 FastAPI 接口后,响应变成 422,服务端声称请求体不存在。 问题不在 JSON,而在 HTTP 协议协商。 现象 后端使用 Spring RestTemplate: new RestTemplate(new JdkClientHttpRequestFactory()); 两组对照实验得到相同结论: 客户端 结果 Node fetch 200 OK PowerShell Invoke-RestMethod 200 OK 默认 JDK HttpClient 400 Invalid HTTP request received. 或 422 Body missing 强制 JDK HTTP/1.1 200 OK 唯一关键变量是 HTTP 版本。 根因 JdkClientHttpRequestFactory 默认创建 HttpClient.newHttpClient()。JDK HttpClient 偏好 HTTP/2;访问明文 http:// 地址时,会尝试 h2c Upgrade: Connection: Upgrade, HTTP2-Settings Upgrade: h2c HTTP2-Settings: ... User-Agent: Java-http-client/21 部分 Python HTTP parser 不能正确处理这个请求。它们可能直接返回 400,也可能让应用层看到一个没有 Body 的 POST,最终由 FastAPI 返回 422。 ...

July 30, 2026

用 LD_PRELOAD 解决容器环境中的 Chrome ERR_NETWORK_CHANGED

运行 Playwright 自动化测试时,我们遇到过一种很难稳定复现的失败:页面正在正常加载,Chrome 却突然报错: net::ERR_NETWORK_CHANGED 它看起来像目标服务断网,但实际检查后发现: 被访问的服务没有重启 Pod 本身的网络仍然可用 同一时刻,Pod 内的容器引擎正在创建或销毁其他容器 重试通常又能成功 真正变化的不是 Chrome 正在使用的网络,而是同一网络命名空间中的虚拟网卡。 为什么启动容器会影响 Chrome 在服务器运行 Docker 时,每次创建或销毁容器,都可能伴随这些操作: 创建或删除 veth 设备 给虚拟网卡增加或移除 IP 地址 更新链路状态和路由信息 这些变化会通过 Linux Netlink 机制通知用户空间。Chrome 恰好也是这些通知的订阅者。 Chromium 在 Linux 上使用 AddressTrackerLinux 跟踪网络状态。初始化时,它会创建一个路由 Netlink 套接字: netlink_fd_.reset(socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)); 随后订阅多组网络事件: addr.nl_groups = RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR | RTMGRP_NOTIFY | RTMGRP_LINK; 因此,同一网络命名空间里任何网卡的地址或链路发生变化,都可能被 Chrome 观察到。事件再经由 NetworkChangeNotifierLinux 向网络栈传播,最终让正在进行的请求以 ERR_NETWORK_CHANGED 结束。 整个链路可以简化成: 创建或销毁容器 ↓ 虚拟网卡、IP 或链路状态变化 ↓ 内核发送 NETLINK_ROUTE 消息 ↓ Chrome 的 AddressTrackerLinux 收到变化 ↓ NetworkChangeNotifier 通知网络栈 ↓ 正在进行的请求被中断 Chrome 的行为对桌面用户是合理的。例如从 Wi-Fi 切换到有线网络后,旧连接确实可能已经失效。但在 CI 中,其他容器产生的虚拟网卡变化通常与浏览器访问的目标毫无关系。 ...

July 23, 2026

从 0xc00000fd 到具体方法:一次 .NET Stack Overflow 问题排查

线上有些问题很像“应用突然消失”:访问不到页面,应用日志里没有明显异常,服务却不断重启进程。遇到这种情况,Windows Event Log 往往比应用日志更早给出方向。 本文记录一次 .NET 应用 Stack Overflow 的排查过程。重点不在某个具体插件,而在于如何从一条异常码逐步缩小到具体的方法。 先从异常码确认方向 问题最初表现为应用无法访问,同时能看到大量应用异常退出,以及保活机制不断重启应用的日志。 Event Log 中有一条关键记录: Exception code: 0xc00000fd 在 Windows 中,0xc00000fd 通常对应栈溢出(STATUS_STACK_OVERFLOW)。 先拿到 dump,不要急着下结论 在 .NET 5 及更高版本中,可以参考 .NET 的 Stack Overflow 调试文档,让进程生成 dump。现场排查时,可以先让管理员权限启动相关服务,然后收集控制台输出;如果仍然需要更完整的现场信息,可以配置生成 mini dump。 拿到 dump 后使用 dotnet-dump 加载: dotnet-dump analyze .\dump.6012.dmp 一个容易踩到的坑是:加载完成后直接执行 crashinfo、printexception 或 clrstack,可能看不到有价值的信息: > crashinfo ERROR: No crash info to display > pe There is no current managed exception on this thread 这并不表示 dump 没有问题。Stack Overflow 发生时,运行时可能来不及建立普通异常上下文;排查重点应该转向线程状态和栈帧本身。 ...

July 22, 2026