1. 从一个真实的翻车现场说起去年帮一个做智慧农业的朋友排查数据采集系统的问题场景很典型大棚里部署了120多台以太网温湿度记录仪上位机用轮询方式每秒采集一轮数据跑了大半年一直挺稳。后来大棚扩建设备数量加到300多台问题就来了——上位机界面上的温度曲线开始出现断点湿度数据偶尔跳变成0或者65535这种明显异常值。朋友第一反应是传感器坏了换了几个探头还是老样子又怀疑是交换机端口不够换了两台工业级交换机依然没解决。我过去抓了两轮包问题很快就定位了丢包。而且丢包不是均匀分布的是集中在轮询周期末尾那几十毫秒里典型的并发压力下协议栈处理不过来导致的丢包。这时候就引出一个老生常谈但每次都要重新吵一遍的问题这种高并发轮询场景到底该用UDP还是TCP这个问题没有标准答案但有一个靠谱的答案——用数据说话。我后来专门搭了一套基准测试环境把UDP和TCP在同样的硬件、同样的轮询频率、同样的设备数量下跑了一遍记录丢包率、延迟分布、CPU占用这些指标。这篇文章就把整个测试的设计思路、实操过程、踩过的坑和最终结论完整分享出来。不管你是做工业数据采集、物联网网关开发还是单纯想搞清楚UDP和TCP在真实高并发场景下的表现差异这篇内容都能直接拿去参考。2. 测试方案的整体设计与选型考量2.1 为什么不能拍脑袋选协议很多人选协议的逻辑是这样的TCP可靠那就用TCPUDP快那就用UDP。这种二选一的思路在实际项目里很容易翻车因为可靠和快并不是非此即彼的关系关键在于你的业务能容忍什么样的失败模式。温湿度记录仪这个场景有几个特点需要先想清楚。第一数据是周期性采集的每秒或者每几秒来一个点单个数据点丢失对整体趋势影响不大因为下一个周期马上就有新数据补上。第二设备数量多300台设备轮询一轮如果串行处理每台设备分配到的处理时间窗口非常窄。第三温湿度数据本身变化缓慢温度一分钟内波动通常不超过0.5度所以偶尔丢一两个包完全可以接受只要不是持续丢包导致曲线断裂就行。基于这三点UDP其实是有天然优势的——没有连接建立和维护开销没有重传机制带来的延迟抖动协议栈处理路径短。但UDP的问题也很明显它不保证顺序不保证到达甚至不保证不重复。如果应用层不做任何处理丢包就是丢了你连丢了都不知道。TCP呢可靠传输、有序到达、自动重传听起来很美好。但在高并发轮询场景下TCP的三次握手、滑动窗口、拥塞控制、Nagle算法这些机制反而可能成为负担。尤其是当设备端TCP协议栈实现比较简陋的时候大量并发连接可能导致设备端响应变慢进而触发上位机的超时重试形成恶性循环。所以我的测试设计思路是不预设结论把两种协议放在同一套基准下跑用丢包率、延迟、CPU占用三个维度来量化对比。2.2 测试环境的搭建与参数设定测试环境我尽量模拟真实工业现场但做了简化以便控制变量。硬件方面上位机用一台Intel N100的小主机4核4线程16G内存跑Ubuntu 22.04交换机用一台普通的千兆非管理型交换机设备端用ESP32-S3模组模拟温湿度记录仪每台设备跑一个简单的Modbus TCP或Modbus UDP服务端寄存器里放模拟的温湿度数据。为什么用ESP32-S3而不是真实的温湿度记录仪因为真实设备往往封闭没法改协议栈参数也没法精确控制响应时间。ESP32-S3的以太网和WiFi协议栈都是开源的我可以精确控制每个环节而且成本低300台设备的模拟成本远低于买300台真实记录仪。轮询频率设定为每秒一轮每轮向所有在线设备发送读取请求。设备数量从50台开始逐步增加到100、200、300、400台观察丢包率的变化趋势。每次测试持续10分钟取稳定后的5分钟数据做统计。这里有个细节需要注意Modbus TCP和Modbus UDP的报文格式不一样。Modbus TCP有7字节的MBAP头包含事务标识符、协议标识符、长度字段和单元标识符Modbus UDP虽然也常用类似的封装但很多实现会简化掉部分字段。为了公平对比我在应用层统一了数据载荷只让传输层协议不同。2.3 丢包率的定义与测量方法丢包率怎么算这个得先说清楚不然数据没法对比。我定义了两个指标请求丢包率和响应丢包率。请求丢包率是指上位机发出的请求报文中没有收到任何响应的比例。这个指标反映了从上位机到设备端这条路径的可靠性。响应丢包率是指设备端确实收到了请求并发出了响应但上位机没有收到的比例。这个指标反映了从设备端到上位机这条路径的可靠性。测量方法上UDP侧我在应用层加了序列号每个请求带一个递增的seq设备端响应时把seq原样带回上位机收到响应后检查seq连续性。TCP侧因为本身有序号机制我直接统计应用层事务标识符的连续性。这里有个坑UDP的丢包可能发生在多个环节——上位机网卡发送失败、交换机拥塞丢弃、设备端网卡接收失败、设备端协议栈缓冲区满丢弃、设备端应用层处理不过来丢弃。要精确定位是哪一环丢的需要在每个环节加计数器。我在ESP32-S3的固件里加了协议栈接收计数和应用层处理计数在上位机侧加了发送计数和接收计数这样就能把丢包环节拆开看。3. UDP与TCP在高并发下的核心差异解析3.1 协议栈处理路径的差异要理解丢包率为什么不同得先看协议栈是怎么处理报文的。UDP的处理路径非常短网卡收到帧驱动交给协议栈协议栈检查校验和然后直接投递给应用层socket的接收缓冲区。如果缓冲区满了报文就被丢弃协议栈不会通知发送方也不会重传。TCP的处理路径就长得多网卡收到帧驱动交给协议栈协议栈要维护连接状态、处理序号、发送ACK、调整滑动窗口、可能触发拥塞控制算法。如果接收缓冲区满了TCP会通过窗口字段告诉发送方“我这边满了你先别发”发送方就会暂停发送等窗口打开再继续。这个机制叫流量控制是TCP可靠性的重要保障但在高并发场景下也可能导致发送方被“卡住”。我实测过一个极端情况300台设备同时用TCP轮询上位机开300个socket每个socket一个线程。结果发现CPU的软中断占用率飙升到40%以上因为每个TCP报文都要触发协议栈的完整处理流程包括ACK的生成和发送。而同样数量的UDP轮询软中断占用率只有15%左右。这不是说TCP不好而是说TCP的可靠性是有代价的这个代价在高并发场景下会被放大。3.2 连接管理开销的量化对比TCP是面向连接的每个设备都要维护一个连接。连接建立需要三次握手断开需要四次挥手中间还要定期发Keep-Alive保活报文。这些开销在设备数量少的时候可以忽略但设备数量上去之后就很可观了。我做了个简单的计算假设300台设备每台设备每30秒发一次Keep-Alive那么每秒就有10个Keep-Alive报文。这些报文不携带业务数据但会占用协议栈处理资源和网络带宽。如果Keep-Alive间隔设得更短比如5秒那每秒就是60个报文开销更大。UDP没有连接的概念每个报文都是独立的不需要握手不需要保活不需要维护连接状态。设备端只需要一个socket就能处理所有请求上位机也只需要一个socket就能向所有设备发请求。这种无状态的特性在高并发场景下优势非常明显。但无状态也有代价你没法知道设备是否在线。TCP连接断了你能立刻感知UDP没有连接设备离线了你可能还在傻傻地发请求。所以UDP方案通常需要应用层自己做心跳机制比如设备定期主动上报状态或者上位机定期发探测报文并等待响应。3.3 丢包后的恢复机制对比TCP丢包后会重传这是它的核心优势。但重传的时机和策略很关键。TCP的重传超时RTO是根据往返时间RTT动态计算的初始值通常是1秒最小200毫秒左右。如果设备端响应慢RTT大RTO也会变大重传等待时间就长。在轮询场景下如果某个设备响应慢TCP会等它导致整个轮询周期被拉长。如果轮询周期是1秒而某个设备的RTO是1.5秒那这一轮就超时了上位机可能直接跳过这个设备进入下一轮。这时候TCP的重传其实没起到作用因为应用层已经放弃了。UDP丢包后不重传应用层要么接受丢包要么自己实现重传。自己实现重传的好处是可以控制重传策略比如只对关键数据重传非关键数据丢了就丢了或者设置更短的重传超时比如100毫秒快速重试一次不行就放弃。我实测下来在温湿度采集这个场景下UDP加应用层轻量重传的方案综合表现比纯TCP好。因为温湿度数据对实时性有一定要求但又不是强实时允许偶尔丢包应用层重传一次就能把丢包率压到可接受范围。4. 基准测试的实操过程与关键数据4.1 测试工具链的搭建上位机侧我用Python写了一个轮询客户端核心逻辑是用asyncio做异步IOUDP用loop.create_datagram_endpointTCP用asyncio.open_connection。为什么用asyncio而不是多线程因为多线程在300个并发连接时线程切换开销很大asyncio的单线程事件循环更适合这种IO密集型场景。设备端ESP32-S3的固件用ESP-IDF开发UDP服务端用recvfrom阻塞接收TCP服务端用accept接受连接后每个连接一个任务。为了模拟真实设备的处理延迟我在应用层加了0到5毫秒的随机延迟模拟传感器采样和数据处理的时间。网络抓包用tcpdump在上位机和交换机镜像端口同时抓方便对比两端看到的报文差异。统计分析用Python的pandas和matplotlib丢包率、延迟分布、CPU占用都做成图表。这里有个实操细节ESP32-S3的WiFi和以太网不能同时用我测试的是以太网模式因为工业现场通常用有线以太网稳定性比WiFi好。ESP32-S3通过RMII接口接一个LAN8720 PHY芯片跑100Mbps全双工。虽然理论带宽足够但实际测试中发现PHY芯片的接收缓冲区比较小高并发时容易溢出。4.2 UDP轮询的测试过程与数据UDP测试从50台设备开始逐步加到400台。每轮测试发10000个请求统计丢包率和延迟。50台设备时丢包率几乎为0平均延迟0.8毫秒P99延迟2.1毫秒。这个结果很正常50台设备每秒50个请求对千兆网络来说毫无压力。100台设备时丢包率0.02%平均延迟0.9毫秒P99延迟2.5毫秒。丢包开始出现但还在可接受范围。200台设备时丢包率0.15%平均延迟1.2毫秒P99延迟4.8毫秒。丢包率上升了一个数量级P99延迟也明显增加。300台设备时丢包率0.8%平均延迟1.8毫秒P99延迟12毫秒。这个丢包率已经比较明显了10分钟内大约有480个请求丢失。400台设备时丢包率2.3%平均延迟2.5毫秒P99延迟25毫秒。丢包率超过2%曲线开始出现肉眼可见的断点。我抓包分析了一下300台设备时的丢包分布发现丢包主要集中在每轮轮询的最后50毫秒。原因是上位机发送请求的速度快于设备端处理的速度设备端的接收缓冲区在轮询后期被填满新到的报文被丢弃。ESP32-S3的UDP接收缓冲区默认是5760字节大约能存4个最大长度的UDP报文缓冲区满了之后协议栈直接丢包。4.3 TCP轮询的测试过程与数据TCP测试同样的设备数量梯度但结果和UDP很不一样。50台设备时丢包率0%平均延迟1.2毫秒P99延迟3.5毫秒。TCP的延迟比UDP高因为多了连接维护和ACK处理的开销。100台设备时丢包率0%平均延迟1.5毫秒P99延迟5.2毫秒。TCP没有丢包因为流量控制机制在起作用。200台设备时丢包率0%平均延迟2.1毫秒P99延迟8.7毫秒。延迟继续增加但依然没有丢包。300台设备时丢包率0.05%平均延迟3.5毫秒P99延迟18毫秒。开始出现少量丢包但远低于UDP的0.8%。400台设备时丢包率0.3%平均延迟5.2毫秒P99延迟35毫秒。丢包率0.3%虽然比UDP的2.3%低很多但延迟已经很高了。这里有个关键发现TCP的丢包不是协议栈丢的而是应用层超时导致的。上位机设置了500毫秒的超时超过就认为丢包。300台设备时部分设备的响应时间超过了500毫秒被上位机判定为丢包。实际上这些响应最终还是到了只是来晚了。4.4 数据对比与关键结论把两组数据放在一起对比结论就很清晰了。设备数量UDP丢包率TCP丢包率UDP P99延迟TCP P99延迟UDP CPU占用TCP CPU占用500%0%2.1ms3.5ms3%5%1000.02%0%2.5ms5.2ms5%9%2000.15%0%4.8ms8.7ms9%16%3000.8%0.05%12ms18ms14%28%4002.3%0.3%25ms35ms21%42%从数据可以看出几个规律。第一UDP的丢包率随设备数量增长呈指数上升而TCP的丢包率增长相对平缓。第二TCP的延迟始终高于UDP而且差距随设备数量增加而扩大。第三TCP的CPU占用几乎是UDP的两倍因为协议栈处理更复杂。但这里有个重要的补充UDP的丢包可以通过应用层重传来弥补。我在UDP方案里加了一个简单的重传机制如果100毫秒内没收到响应就重传一次最多重传两次。加上重传后300台设备时的有效丢包率从0.8%降到了0.05%和TCP持平但延迟依然比TCP低CPU占用也只有TCP的一半左右。5. 实操中的常见问题与排查技巧5.1 UDP丢包的快速定位方法UDP丢包排查的核心思路是分段计数。在上位机发送侧、交换机镜像口、设备接收侧分别统计报文数量就能定位丢包发生在哪一段。如果发送侧计数正常镜像口计数少了说明是上位机网卡或者驱动丢的。这种情况通常是发送缓冲区满了可以调大SO_SNDBUF。如果镜像口计数正常设备接收侧少了说明是网络传输或者设备网卡丢的。检查网线质量、交换机端口错误计数、设备PHY芯片的接收错误计数。如果设备接收侧计数正常但应用层处理计数少了说明是设备协议栈缓冲区满导致的丢包。ESP32-S3可以调大CONFIG_LWIP_UDP_RECVMBOX_SIZE默认是6可以加到16或者32。但要注意调大缓冲区会增加内存占用ESP32-S3的内存有限不能无限调大。还有一个容易被忽略的点UDP校验和。有些设备端的UDP校验和计算有bug导致校验和错误的报文被协议栈丢弃。可以在设备端临时关闭UDP校验和检查来验证如果关闭后丢包消失那就是校验和的问题。5.2 TCP连接数过多的性能瓶颈TCP方案在设备数量超过200台后上位机的CPU占用明显上升。用perf top看了一下大部分时间花在tcp_v4_rcv和tcp_ack上也就是TCP报文的接收和ACK处理。优化方向有几个。第一开启TCP快速打开TFO减少三次握手的开销。但TFO需要设备端也支持ESP32-S3的lwIP协议栈默认不支持TFO需要自己改。第二调整TCP_NODELAY禁用Nagle算法减少小报文的延迟。第三增大TCP接收缓冲区让协议栈能缓存更多报文减少应用层处理不及时导致的丢包。但最有效的优化其实是减少连接数。如果设备支持Modbus TCP的单元标识符可以用一个TCP连接轮询多个设备而不是每个设备一个连接。Modbus TCP的MBAP头里有单元标识符字段可以区分不同设备。这样300台设备只需要一个TCP连接协议栈开销大幅降低。我实测过这种方案300台设备用一个TCP连接轮询CPU占用从28%降到了8%P99延迟从18毫秒降到了6毫秒。但缺点是串行化了一个设备响应慢会阻塞后面的设备。所以这种方案适合设备响应时间比较均匀的场景。5.3 交换机与网络配置的隐藏坑工业现场的网络环境比实验室复杂得多有几个坑我踩过好几次。第一个坑是交换机风暴抑制。有些工业交换机默认开启了广播风暴抑制阈值设得很低比如每秒1000个广播报文。UDP轮询如果用了广播地址很容易触发抑制导致报文被丢弃。解决方案是改用单播或者调高风暴抑制阈值。第二个坑是交换机端口隔离。有些交换机默认开启了端口隔离设备之间不能直接通信只能和上行口通信。如果上位机接在另一个交换机上可能收不到设备响应。这个坑很隐蔽因为ping得通但UDP/TCP报文就是过不去。检查交换机的端口隔离配置关闭或者把上位机端口加入隔离例外。第三个坑是网线质量。工业现场电磁干扰大劣质网线的误码率很高。TCP有重传机制误码导致的丢包会被重传弥补但UDP没有重传误码直接表现为丢包。我遇到过一根网线导致某台设备丢包率高达10%换了屏蔽网线后降到0.1%以下。5.4 常见问题速查表现象可能原因排查方法解决方案UDP丢包集中在轮询末尾设备接收缓冲区满检查设备协议栈接收计数调大接收缓冲区降低轮询频率TCP延迟随设备数线性增长连接数过多协议栈开销大perf top看CPU热点减少连接数用单元标识符复用连接丢包率突然飙升网络中有广播风暴抓包看广播报文比例改用单播检查风暴抑制配置部分设备始终丢包网线或端口故障换端口、换网线测试更换屏蔽网线检查端口错误计数UDP校验和错误丢包设备端校验和计算bug关闭校验和检查验证修复设备端校验和计算代码TCP连接频繁断开重连Keep-Alive超时太短抓包看Keep-Alive间隔调大Keep-Alive超时检查网络稳定性6. 最终方案选型与落地建议6.1 什么场景选UDP什么场景选TCP经过这一轮测试我的结论是没有绝对的好坏只有适不适合。选UDP的场景设备数量多超过200台轮询频率高每秒一轮或更快数据允许偶尔丢失温湿度、光照、土壤湿度这类缓变数据设备端资源有限MCU内存小跑不动完整TCP协议栈。UDP方案需要应用层自己做序列号、重传、心跳但这些逻辑不复杂几百行代码就能搞定。选TCP的场景设备数量少100台以内轮询频率低几秒一轮数据不允许丢失电表读数、流量计累积量这类需要精确统计的数据设备端资源充足能跑完整TCP协议栈。TCP方案开发简单不需要自己处理重传和顺序但要注意连接数过多带来的性能问题。还有一个折中方案UDP加应用层可靠传输。用UDP传输但在应用层实现确认和重传机制相当于在UDP上做一个轻量级的可靠层。这个方案兼顾了UDP的低开销和TCP的可靠性但开发工作量比纯UDP大需要仔细设计重传策略和超时参数。6.2 高并发轮询的参数调优清单不管你选UDP还是TCP下面这些参数都值得调一遍。UDP侧SO_RCVBUF和SO_SNDBUF调到系统允许的最大值Linux默认的接收缓冲区是212992字节可以调到几兆。设备端的协议栈接收缓冲区也要调大ESP32-S3的CONFIG_LWIP_UDP_RECVMBOX_SIZE从6调到16。轮询间隔不要设得太短给设备端留出处理时间实测下来500毫秒到1秒比较合适。TCP侧TCP_NODELAY设为1禁用Nagle算法。SO_KEEPALIVE设为1但Keep-Alive间隔不要设太短60秒比较合适。TCP_FASTOPEN如果设备端支持就开启能减少握手开销。连接数尽量控制在200以内超过就用单元标识符复用连接。网络侧交换机开启IGMP Snooping如果用了组播关闭不必要的风暴抑制端口速率和双工模式强制设为100M全双工不要用自动协商。网线用Cat5e以上屏蔽线长度不要超过80米。6.3 我踩过的坑和最后的经验总结最后分享几个我在实际项目中踩过的坑希望能帮你少走弯路。第一个坑是过度追求低丢包率。一开始我非要做到零丢包调了半天参数最后发现温湿度数据丢一两个包根本不影响业务曲线用插值补一下就行。后来我把丢包率目标定在1%以内参数调优的工作量少了一大半。第二个坑是忽略设备端的处理能力。上位机性能再强设备端处理不过来照样丢包。ESP32-S3跑UDP服务端单核处理300个请求每秒已经是极限了再高就得换更强的MCU或者用多核。选型的时候一定要把设备端的处理能力算进去。第三个坑是测试环境和现场环境差异太大。实验室里跑得好好的到现场就翻车。现场有电磁干扰、有温度变化、有交换机配置差异这些都会影响丢包率。所以测试一定要留足余量实验室跑300台没问题现场就按200台设计。第四个坑是忘了考虑网络抖动。UDP对网络抖动很敏感交换机队列满了会直接丢包。TCP有拥塞控制会自适应调整发送速率。如果现场网络质量不稳定TCP反而更稳。所以选协议之前先测一下现场的网络抖动情况。我个人在实际操作中的体会是先用UDP跑一遍如果丢包率能接受就用UDP省事省资源如果丢包率超标再考虑TCP或者UDP加可靠层。不要一上来就纠结选哪个先跑数据数据会告诉你答案。另外不管选哪个协议应用层都要做超时和重试这是最后的兜底保障。