ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FastDFS Socket异常排查实战:从TCP链路到连接池的闭环治理

FastDFS Socket异常排查实战:从TCP链路到连接池的闭环治理 1. 先搞清楚这条链路socket异常为什么总从 com.github.tobato.fastdfs 冒出来1.1 FastDFS的通信模型与Tobato客户端的调用方式FastDFS本身是一个用C语言实现的分布式文件系统核心就两个角色Tracker Server和Storage Server。Tracker负责调度Storage负责真正存文件。Tobato这套组件就是把服务端协议封装成了Spring环境下可以方便使用的Java API核心类是FastFileStorageClient。一个正常的上传动作从客户端视角拆开是这样的客户端从配置的tracker-list里取一个地址跟Tracker建立TCP连接Tracker默认端口是22122客户端向Tracker发起“找storage”的请求Tracker根据group、负载、剩余空间等信息返回一个可用的storage节点信息里面至少包含IP和端口客户端再跟这个storage节点建立TCP连接storage默认端口一般是23000客户端在storage连接上完成写文件、写meta等交互。所以你在日志里看到一个socket异常时先别急着怀疑代码。这个异常可能出现在第1步连Tracker失败、第2步请求Tracker后等待响应超时、第3步连storage失败或者第4步跟storage交互过程中出错。位置不同排查方向完全不同。很多人习惯只看异常文本比如看到SocketTimeoutException就把超时调大看到Connection refused就去重启服务这其实容易把问题带偏。正确做法是先看堆栈走到哪一层FastFileStorageClient只是入口真正干活的是TrackerClient和SocketConnection。堆栈如果停留在TrackerClient的getStoreStorage说明问题出在tracker这侧如果已经进入storage的上传流程问题出在storage这侧。这一步判断对了后面90%的排查思路基本就明确了。1.2 后台日志里的socket异常常见长相我按平时排障的经验把常见异常文本和对应的出错阶段整理成了下面这张表。你翻日志的时候可以直接对照先做一个粗分类异常文本/典型堆栈出错阶段比较常见的原因java.net.ConnectException: Connection refusedTCP握手阶段被拒绝服务端端口没监听、进程没起来、防火墙主动RSTjava.net.SocketTimeoutException: connect timed outTCP握手阶段超时SYN包发出去了没人响应大概率是防火墙丢包或路由不通java.net.SocketTimeoutException: Read timed out已建立连接等待响应超时对端处理太慢、so-timeout设太短、链路拥塞java.net.SocketException: Connection reset已建立连接读写过程中被重置对端主动关闭连接、中间设备回收空闲连接、Nginx/LVS空闲超时java.io.EOFException读取响应时提前读到EOF响应数据被截断、连接被对端关闭后客户端继续读、协议版本不匹配java.net.NoRouteToHostException: No route to host路由层找不到路径路由表问题、目标IP不回包从com.github.tobato.fastdfs包抛出来的异常外层常会包一层FdfsException看到Caused by里的原生socket异常才能定性。另外有一点容易忽略Connection reset和Connection refused是两回事。前者是连接曾经建立过、后来被对端或中间设备强制断掉后者是压根没建立成功。二者的排查重点完全不同别混在一起调参数。2. 排查路径从配置、网络到服务端一层一层剥2.1 基础配置核对connect-timeout与so-timeoutTobato的配置前缀是fdfs最常用的两个超时参数是connect-timeout和so-timeout单位都是毫秒。很多第一次用的人把单位当成秒随手写个600结果连接稍微慢一点就报connect timed out。Spring Boot环境下的典型配置长这样fdfs: connect-timeout: 3000 so-timeout: 5000 tracker-list: - 192.168.1.100:22122这里有几个容易踩的点。第一tracker-list必须是Tracker的地址不是storage的地址。把storage的IP写进去客户端会拿它当tracker去问“有没有可用storage”自然得不到合法响应最后报错的样子往往也是各种IO异常。这种配置错误比较隐蔽因为端口可能通着但业务就是一直失败。第二tracker-list里尽量写IP别写hostname。Java底层要经过一次DNS解析容器环境里/etc/hosts、DNS配置如果比较乱解析慢或者解析到错误IP都会导致连接异常。我见过一个案例写的是内网域名客户端时不时UnknownHostException但重试一下又好了最后把配置改成IP后彻底稳定。第三so-timeout影响的是建立连接之后的读写。上传的文件越大单次socket读写不一定会变慢但如果storage处理事务需要较长时间建议把so-timeout放宽到5000甚至更长防止storage还没处理完客户端这边就先等得不耐烦。反过来如果设置得过大线程会在socket读取上挂很久业务线程被大量占用看起来像“假死”。注意connect-timeout是毫秒不是秒。600毫秒的连接超时在跨机房、有安全设备做检测的场景下非常容易触发生产环境建议2000到5000毫秒起步。2.2 端口连通性与防火墙、安全组排查FastDFS默认端口是tracker 22122、storage 23000。实际端口以服务端配置为准但绝大多数默认安装就是这两个。在应用服务器上执行telnet 192.168.1.100 22122 telnet 192.168.1.101 23000如果tracker端口能通、storage端口不通十有八九是安全组或iptables只放行了22122。云上环境尤其常见刚部署时只加了tracker端口上传一直失败排查半天才发现23000被漏了。放行端口即可解决但要注意安全只对受信网段开放别把整个公网都能访问的规则加上去。如果两个端口都不通优先查客户端到tracker的网络路径。ping看链路通不通route -n看路由表再用tcpdump抓包定位丢包点tcpdump -i eth0 host 192.168.1.100 and port 22122抓包结果里如果只有SYN没有SYN-ACK说明中间设备把包丢了去查防火墙、安全组、路由ACL如果回的是RST说明服务端没有监听这个端口或者监听的不是当前IP。还有一种情况是服务端进程起来了但监听在127.0.0.1上外部IP自然连不上用ss -lntp在服务端确认一下监听地址即可。2.3 tracker返回了不可达的storage地址这是个非常隐蔽的坑客户端从tracker拿到的storage地址是tracker根据storage节点自己上报的地址返回的。如果部署时storage上报的是内网IP而应用服务器和那个内网网段不可达就会反复出现连接storage失败。判断方法也简单在应用服务器上用监控命令查一下tracker认为的storage状态和地址。FastDFS自带fdfs_monitor工具fdfs_monitor /etc/fdfs/client.conf输出里能看到storage的IP、端口以及在线状态。如果发现返回的IP和应用服务器根本不在一个网段问题基本就定位了。解决方案有两种一是调整网络让应用和storage互通二是修改storage的配置让上报地址变成应用服务器实际能访问的地址。这类问题调客户端参数是没有用的别白费力气。3. 连接池与运行期状态容易被忽略的socket异常制造机3.1 连接池参数与高并发下的连接耗尽Tobato客户端内部用连接池缓存到tracker、storage的长连接。相关参数以fdfs.pool.*为前缀常见配置类似这样fdfs: pool: max-total: 200 max-idle: 100 min-idle: 10 max-wait-millis: 3000 test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 30000 num-tests-per-eviction-run: 20这里最关键的两个参数是max-total和max-wait-millis。并发上传很高时如果池里连接不够新的请求要么借不到连接要么在等连接时超时最终抛出来的异常往往看起来就是socket相关。我遇到过压测时日志里全是连接池借钱方向的异常把max-total调大后立刻缓解。估算max-total时别拍脑袋。先看峰值并发假设同时上传的线程有100个max-total至少大于100最好留30%到50%的冗余。max-wait-millis别设成0否则获取不到连接直接抛异常也别设太大否则业务线程大量堆积在等待连接上服务整体吞吐下降。如果应用启动后第一次上传就失败、之后又正常通常不是池容量问题而是初始化时机和连接预热问题。可以在项目启动完成后主动上传一个极小文件把连接“热”起来很多偶发的首次连接异常都能避开。3.2 空闲连接被链路设备回收导致Connection reset这条是我最想强调的排查思路。连接在池里放的时间长了中间如果经过LVS、F5、云上NAT网关这类设备设备对空闲TCP连接一般都有超时时间比如60秒或300秒。超时后设备直接把这个连接清掉不通知两端。客户端在池里并不知道下次拿到这个连接去写数据对端回一个RST日志里就是Connection reset或者Broken pipe。这种异常的典型规律是只在“一段时间没有流量之后的第一个请求”出现。比如每天凌晨跑批量任务任务前的几个小时都没有上传请求第一个请求大概率失败重试又成功。白天请求密集反而很难触发。解决办法有几个方向开启test-while-idle让连接池的eviction线程定期对空闲连接做检测把time-between-eviction-runs-millis设得比链路设备空闲超时时间小比如链路设备90秒清连接就设成30000毫秒如果链路太黑盒干脆test-on-borrow: true每次借出前都验证一遍代价是每个请求多一次交互性能有一定损失还有一种兜底做法写一个定时任务每隔一段时间主动借一个连接做小文件上传或协议探测让连接一直保持活跃。我复盘的很多生产问题最后都落在这个原因上。表面上看起来是“socket异常”实际是连接池和网络设备的空闲回收策略博弈跟FastDFS本身没有半毛钱关系。3.3 版本兼容与服务端异常退出带来的隐性影响如果排查完配置、网络、连接池都正常就要怀疑版本兼容性了。FastDFS服务端5.x、6.x在不同的版本对协议细节有调整而Tobato客户端1.26.x、1.27.x也有各自适配的边界。大部分核心接口是兼容的但遇到奇怪的EOFException时还是要确认服务端和客户端的版本组合是否在社区推荐的范围内。另外storage或tracker所在进程如果因为内存不足、磁盘满被系统杀掉客户端重连时就会表现为refused或RST。这时候先去服务端看进程状态和日志别在客户端反复调参数。tracker和storage的日志默认在各自base_path下的logs目录里tracker日志叫trackerd.logstorage日志叫storaged.log。服务端日志里有大段的磁盘、心跳、同步错误记录时优先解决服务端问题socket异常自然就消失了。4. 一次生产环境的排查复盘与最终解决步骤4.1 现场过程从日志栈到telnet再到服务端日志分享一个我实际处理过的案例。某系统用com.github.tobato.fastdfs上传图片后台日志每天凌晨批量任务时间段会报一批SocketTimeoutException: Read timed out堆栈指向com.github.tobato.fastdfs.proto.conn.SocketConnection。业务侧重试后又能成功白天基本正常。一开始怀疑是so-timeout太小从1500毫秒调到3000毫秒失败时间点往后推了但没有根治。后来的排查顺序是看失败时刻的并发情况发现该批量任务会开20个线程同时传文件telnet测试tracker和storage端口都通排除防火墙在应用服务器抓包发现很多连接在写文件前已经空闲了几分钟检查网络链路负载均衡设备空闲超时是90秒而连接池没有主动回收空闲连接把time-between-eviction-runs-millis调成10000开启test-while-idle再把空闲超过60秒的连接淘汰问题消失。这个案例典型就典型在“只有特定时间点失败”“白天单次请求多所以不太触发空闲回收”很难联想到是连接已经死了。所以看到间歇性socket异常时一定要把“空闲连接失效”这个维度加进怀疑清单不能只盯着超时配置。4.2 配置修正与验证方法以一个相对稳健的最终配置为例可以作为模板参考fdfs: connect-timeout: 3000 so-timeout: 8000 tracker-list: - 192.168.1.100:22122 - 192.168.1.101:22122 pool: max-total: 200 max-idle: 80 min-idle: 5 max-wait-millis: 3000 time-between-eviction-runs-millis: 10000 min-evictable-idle-time-millis: 60000 test-while-idle: true这里so-timeout: 8000是为大文件传输留的余量min-evictable-idle-time-millis: 60000确保空闲超过60秒的连接会被淘汰time-between-eviction-runs-millis: 10000表示每10秒检查一次。整体思路就是让连接池里的连接保持新鲜避免用到已经被链路设备回收的“僵尸连接”。验证方式不要只测一遍最好是连续上传100个小文件停5分钟再传100个。如果中间停顿时长已经超过了网络设备的空闲超时时间而后一批请求不再出现reset或read timeout基本就算稳了。有条件的话在服务器上执行tcpdump观察连接是否一直处于活跃复用状态能更直观地佐证。4.3 日常健康检查与重试机制的补充长期稳定运行不能只靠一次调参。我的习惯是给上传服务加两个东西一是探活任务。定时上传一个极小的文本文件到FastDFS上传成功说明链路、服务端、连接池都正常失败则触发告警。探活频率建议低于网络链路空闲超时时间比如每30秒一次这样还能顺带把空闲连接维持在活跃状态一举两得。二是业务层的重试机制。上传入口统一封装捕获FdfsException后做一次重试重试前先做一次连接池清理或者直接换下一个tracker。这样即使偶发问题没有被完全消灭业务侧也能自动消化用户无感知。但重试要防止雪崩重试次数控制在1到2次并且记录重试日志方便后续继续分析根因。最后再提醒一句日志里出现socket异常时先别急着改代码把异常出现的时间、频率、并发量、网络设备空闲超时、连接池状态这几个信息对齐往往比盲目调参有效得多。FastDFS本身是稳定的Tobato客户端也是稳定的大多数异常其实是部署架构和连接生命周期管理的问题。把这层想透了以后再遇到类似问题基本可以在半小时内给出方向。
返回列表