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

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

多进程配置文件读写的演化历程

第一阶段:共享一套读写逻辑,谁都可以写 一开始,多个进程共用一套读写逻辑: if (!Exists(configPath)) { WriteDefaultConfig(configPath); } var config = ReadConfig(configPath); 问题有两个: 多个进程可能同时发现“文件不存在”,然后一起创建 一个进程正在写,另一个进程正好在读 如果是直接覆盖写,读取方可能看到: 文件刚被创建,内容还是空的 文件已经存在,但只写了一半 配置结构还没写完整 目标文件在替换过程中短暂不可用 对应的现象就是: 偶发读取失败 配置解析异常 文件内容丢失 某些进程启动时报错 根因很简单:读者看到了写入中的中间状态。 第二阶段:加锁、重试、缓存,先把系统稳住 先说 FileShare。 它想解决的其实是两个很具体的问题: 读取正好撞上写入,结果读到一半内容 两次写入互相重叠,结果把文件写坏 最直接的做法,就是用共享模式限制文件如何被同时打开。 它的目标不是提供原子替换,而是尽量减少下面这些情况: 读取撞上写入中的半成品 两次写入同时落到同一个目标文件 文件直接被另一进程占用导致操作失败 FileShare 带来的直接结果是:冲突不会消失,只会变成可以观察、可以重试的失败。 所以后面又加了重试。 这能减少两类问题: 少报“文件被占用” 让偶发冲突尽量靠重试跨过去 再往后,问题变成了独占时间。 配置读取很频繁,导致写入一直在等待读取释放文件锁。 所以开始引入缓存: var config = ReadConfig(configPath); var lastWriteTime = GetLastWriteTime(configPath); cache = (config, lastWriteTime); 下次访问时先比较时间戳: var currentWriteTime = GetLastWriteTime(configPath); if (currentWriteTime == cache.LastWriteTime) { return cache.Config; } cache = ReadConfig(configPath); return cache.Config; 没变,缓存有效;变了,缓存失效,重新加载。 ...

July 17, 2026