资讯动态

TCP与UDP协议详解:选型、抓包与调优实践

发布时间:2026/10/23 9:39:01 来源:尧图企业网站定制
选型这事我吃过亏。早些年做一个设备状态上报的服务图省事全用TCP长连接结果几千台设备在弱网环境里频繁掉线重连服务端一半的CPU都耗在握手和回收连接上。后来把那段心跳和指标上报改成UDP掉线率没变差CPU掉了一大截——这件事让我彻底明白TCP和UDP不是“可靠”与“不可靠”这么简单的标签它们背后是两套完全不同的成本模型和失败语义。这篇内容我想把TCP与UDP协议详解讲透从报文结构、握手挥手、窗口与拥塞控制到粘包、MTU、TIME_WAIT这些线上真会咬人的细节再到具体怎么写代码、怎么抓包、怎么调内核参数最后给出一个能直接照着走的选型判断表。适合刚接触网络编程的同学打底子也适合已经写过几年代码、但每次遇到连接异常还是靠搜索引擎拼凑答案的开发者对着排查。文中代码和参数都以常见Linux环境为准涉及具体数值的地方我会把计算过程摊开写清楚方便你按自己的业务量级换算。1. 先把两者放回各自的坐标系一次选型的底层逻辑1.1 从“字节管道”到“数据契约”它们真正在解决什么很多人学协议是从头部字段开始背的背完源端口目的端口序列号确认号真到线上还是不知道该选哪个。我的理解是TCP和UDP解决的根本不是同一个层次的问题。UDP给你的是接近IP层的能力一个数据报从A发出去可能到、可能不到、可能乱序、可能重复内核只负责在IP之上加上端口号做进程寻址外加一个可选的校验和剩下的什么都不管。它提供的是一张“原始运单”寄不寄到、路上会不会破损寄件人自己负责。TCP在这张运单上叠了一整套契约字节流有序、不丢、不重、双向、带流量控制和拥塞控制。为了实现这套契约它必须维护连接状态必须有序列号和确认机制必须有重传定时器必须在内核里为每条连接分配一块发送缓冲区、接收缓冲区和一堆控制块。这些不是免费的内存、CPU、以及最关键的——延迟都是代价。所以选型的第一性问题不是“哪个更快”而是“这份数据丢了会怎样”。丢了无所谓、下一帧马上覆盖的比如实时画面、语音包、高频传感器采样用TCP反而会被重传拖累一个包卡住后面全排在缓冲区里这就是典型的队头阻塞。反过来一条转账指令、一段配置下发丢了必须补、补了不能重那就必须在TCP或者自己实现的可靠层里选一个别指望UDP帮你兜底。我常用一个类比TCP像挂号信有签收、有编号、丢了邮局给你重投但你得等UDP像往楼下扔纸条快扔完就不管了风大不大你自己判断。真正成熟的系统往往是两种都用——控制指令走TCP状态心跳走UDP这种混搭在工业、音视频、游戏后端里非常普遍。1.2 我自己的选型决策树五个问题定方向每次新做一个通信模块我会按顺序问自己五个问题基本问完方向就定了很少返工。序号判断问题选TCP的信号选UDP的信号1单条数据丢失是否影响业务正确性影响必须补不影响下一帧覆盖2是否容忍秒级以上的延迟抖动不太容忍但能接受重传完全不能容忍排队3通信双方是否需要长期维持会话状态需要登录态、订阅关系不需要一问一答4是否天然是一对多、多对多广播一般是一对一一对多、组播、广播5团队是否有能力自建重传、拥塞、乱序重组没有直接用有或直接上成熟可靠UDP方案这五问的顺序很重要第一问是硬门槛。如果数据必须完整就算你对延迟再敏感也不能裸用UDP只能在TCP和“基于UDP的可靠传输方案”之间选。第二问决定你在TCP上要不要做额外优化比如关掉Nagle、调小发送缓冲区。第三问经常被忽略无状态的短交互用TCP每条请求都要建连TIME_WAIT和端口消耗会变成运维问题。注意不要把“实时性要求高”直接等同于“必须用UDP”。很多所谓的实时业务瓶颈在业务逻辑和磁盘IO不在传输层。我见过把日志上报改成UDP后因为没有重传丢的数据比省下的延迟更值钱。这里还有个容易被跳过的维度运维可观测性。TCP的连接状态在内核里是可见的ss、netstat、/proc/net/tcp都能看到每条连接处于哪个状态、重传了几次、当前拥塞窗口多大。UDP是无状态的出问题你只能靠抓包和应用侧埋点。选UDP之前先确认你的监控体系能不能接住这种“看不见”的成本否则故障排查会非常痛苦。2. 协议细节拆解那些真正影响线上表现的设计2.1 三次握手与四次挥手连接成本到底花在哪TCP建立连接要三个报文客户端发SYN服务端回SYNACK客户端再回ACK。为什么不是两次因为两次无法让双方都确认“对方的接收能力正常”。第三次ACK的作用不只是礼貌它让服务端确认客户端确实收到了自己的SYNACK从而避免历史重复连接请求造成资源浪费。这段流程有两个队列值得记住半连接队列SYN队列和全连接队列accept队列。服务端收到SYN后把连接放进半连接队列并回SYNACK收到第三次ACK后才移到全连接队列等应用调用accept取走。这两个队列都有上限前者受net.ipv4.tcp_max_syn_backlog影响后者受listen()的backlog参数和net.core.somaxconn共同限制。线上出现“客户端能连上但服务端accept不到”或者“高峰期大量连接超时”八成是这两个队列溢出内核统计里会有SYNs to LISTEN sockets dropped和listen queue overflow。连接成本具体花在哪一次同城RTT大概0.5~2ms跨地域40~80ms。对短连接来说每处理一次业务就要付一次RTT的建连成本加上四次挥手的又一个RTT主动关闭方还要等TIME_WAIT。这就是为什么HTTP早期每个请求一条连接的模式会被长连接取代——不是协议本身慢是握手太贵。挥手的四次是因为TCP是全双工的两个方向要各自关闭。主动关闭方发FIN被动方回ACK此时被动方还能继续发数据半关闭状态等它数据发完再发FIN主动方回ACK。主动关闭方最后进入TIME_WAIT等2MSLLinux上通常60秒由net.ipv4.tcp_fin_timeout和内核常量决定才真正释放。提示TIME_WAIT不是设计缺陷它的作用是保证最后一个ACK能重发、以及让旧连接的残留报文在网络中消散避免污染新连接。见到TIME_WAIT数量多不要第一反应去关它先看是谁在主动关闭。我踩过的一个典型坑某服务端把响应发完就主动关连接客户端那边还慢吞吞读结果服务端堆了几万个TIME_WAIT端口被占满新连接报Cannot assign requested address。后来改成由客户端主动关闭并把连接改成复用问题直接消失。这个经验很朴素——让连接的使用方来决定关闭时机谁更清楚数据发完了谁关。2.2 滑动窗口、拥塞控制与Nagle吞吐和延迟的三方拉扯TCP的发送窗口取接收窗口rwnd和拥塞窗口cwnd的较小值。rwnd由接收方告诉发送方“我还能收多少”是流量控制防止把对方缓冲区打爆cwnd由发送方自己维护是拥塞控制防止把网络打爆。理解这两个窗口很多性能问题就自解释了。先算一笔账解释窗口为什么关键。带宽时延积BDP 带宽 × RTT。假设链路100Mbps、RTT 40msBDP 100Mbps × 0.04s 4Mbit 500KB。这意味着在途数据要有500KB才能把这条链路跑满。如果接收窗口只有64KB那吞吐上限就是64KB / 0.04s 1.6MB/s ≈ 13Mbps无论你带宽多大。所以跨地域传大文件调优第一件事是把net.ipv4.tcp_rmem和tcp_wmem的最大值放开让窗口能自动扩张到BDP以上。拥塞控制算法决定了cwnd怎么长。经典流程是慢启动每RTT翻倍、拥塞避免线性增长、快速重传收到3个重复ACK立即重传不等超时、快速恢复。Linux默认现在是CUBIC在长肥管道上表现不错BBR不依赖丢包判断拥塞靠测量带宽和最小RTT在高丢包链路上优势明显。如果你在做跨境或者移动网络场景值得试一下BBR切换方式是net.ipv4.tcp_congestion_controlbbr需要内核模块支持。Nagle算法是另一个高频坑。它的逻辑是小包先攒着等前一个包的ACK回来或者攒够一个MSS再发目的是减少小包数量。听起来很合理但它和延迟ACK接收方等40ms再回ACK撞在一起时会出现“发送方等ACK、接收方等更多数据”的互相等待典型表现是请求响应场景里凭空多出40ms延迟。解决办法是在明确要求低延迟的连接上设置TCP_NODELAY关掉Nagle。做RPC、做游戏、做交互式请求的基本都应该关。注意TCP_NODELAY不是万能药。大批量小数据顺序写入的场景关掉它会让网络里充满 tinygram反而加重拥塞。判断标准很简单你的写入是不是“一条请求一条响应”的交互模式是就关是批量灌数据就留着。2.3 UDP的轻与重无连接不等于无责任UDP头部只有8字节源端口、目的端口、长度、校验和。没有序列号、没有确认、没有窗口、没有重传。发送方调一次sendto数据报进入内核协议栈然后就没有然后了。内核不会为它保留任何状态也不会告诉你对方收没收到——除非你自己在应用层设计确认机制。这种“轻”带来三个实际后果。第一没有队头阻塞一个包丢了不影响后面的包被递交给应用对实时音视频来说这是刚需宁可花屏一下也不能整体卡住。第二天然支持一对多组播、广播只能在UDP上做这是IP层能力TCP的点对点连接模型做不了。第三应用层完全掌控发送节奏没有拥塞控制意味着你可以按自己的节奏发但也意味着网络拥塞时你会把包一股脑塞进去加剧丢包——所以负责任的UDP应用必须自己做发送速率控制。UDP的“重”体现在应用侧。一旦你需要可靠重传、去重、乱序重组、拥塞控制这四件事都得自己补。业界已经有很多成熟轮子但选型时要清楚它们的代价可靠UDP方案通常要维护自己的连接ID、序号空间、ACK机制和重传定时器复杂度和调试难度都不低。提示UDP的校验和是可选的IPv4下可以置0表示不校验但强烈建议保留。关掉校验和在某些网卡上能省一点CPU代价是坏包会被当成好包交给应用排查起来极其折磨。2.4 MTU、分片与粘包两个最容易被混淆的误解先说MTU。以太网默认MTU 1500字节减去IP头20字节、TCP头20字节TCP单包能承载的最大应用数据是1460字节这就是MSS。UDP没有MSS概念它的上限是MTU减去IP头IPv4下是1472字节。如果一个UDP数据报超过这个值IP层会分片接收端重组。分片的问题在于任何一个分片丢失整个数据报都作废重传成本被放大而且中间设备对分片的处理策略不一致有些会直接丢弃。所以我在工程上给自己定了个硬规矩UDP单包应用数据控制在1200字节以内留出余量给可能的隧道封装和IPv6IPv6要求路径MTU至少1280。超过这个长度的数据自己在应用层切块并编号别指望IP分片。想验证路径MTU可以用ping -M do -s 1472 目标地址逐步试-M do表示禁止分片。再说粘包。这个概念名字起得很误导TCP根本没有“包”的概念它是字节流你写进去的100字节和下一个100字节在接收方看来就是连续的200字节中间没有边界。所谓粘包本质是应用层没有定义消息边界。UDP则天然有边界一次sendto对应一次recvfrom收上来就是一个完整数据报只要没超过缓冲区。解决粘包只有三条路定长消息、分隔符如换行、长度字段前缀。工程上最稳的是长度字段消息体的方案头部固定几字节存body长度接收方先读头、解析长度、再按长度读满body不足就继续等。这套逻辑必须处理“读到的字节数少于期望”的情况也就是半包用循环加缓冲区累积来实现不能假设一次recv就能拿全。3. 动手实操从抓包到代码到参数调优3.1 用抓包把抽象流程变成可见的时序协议这东西光看文字描述永远记不牢抓一次包就通了。我的习惯是每接一个新通信模块先抓一段正常流量存下来后面出问题拿它做基线对比。# 抓取指定端口的TCP流量写入文件避免在终端刷屏 tcpdump -i eth0 -nn -s 0 tcp port 9000 -w /tmp/tcp9000.pcap # 只看握手和挥手相关的标志位快速确认连接生命周期 tcpdump -i eth0 -nn tcp port 9000 and (tcp[tcpflags] (tcp-syn|tcp-fin|tcp-rst) ! 0) # 读取离线包带时间戳和序号观察重传 tcpdump -nn -r /tmp/tcp9000.pcap -tttt | head -50抓包时重点看三样东西。一是序号seq和确认号ack的变化能直观看到发送方发了多少、接收方确认到哪。二是标志位组合[S]是SYN[S.]是SYNACK[.]是纯ACK[P.]是带数据的推送[F.]是FIN[R]是重置。三是时间戳间隔如果看到同一个seq反复出现间隔还在翻倍增长200ms、400ms、800ms那就是典型的超时重传说明链路在丢包。排查性能问题我还会用ss看连接内部状态# 查看所有TCP连接的详细指标包括拥塞窗口、RTT、重传次数 ss -tin state established # 汇总统计看各状态连接数量和重传率 ss -sss -ti输出里的cwnd、rtt、retrans、unacked信息量极大。如果cwnd一直卡在10左右上不去说明拥塞控制认为链路有问题如果retrans持续增长那就是实打实的丢包。这套组合比看应用日志快得多。3.2 最小可用代码TCP与UDP两版实现先把TCP版本写清楚重点是长度前缀分帧和循环读取这两点是线上代码和教学代码的分水岭。import socket import struct HEADER struct.Struct(!I) # 4字节大端无符号整数表示body长度 def send_msg(sock: socket.socket, payload: bytes) - None: 先发长度头再发body。两次send可能被合并但对端按长度解析不受影响。 sock.sendall(HEADER.pack(len(payload)) payload) def recv_exact(sock: socket.socket, n: int) - bytes: 必须读满n字节才返回处理半包 buf bytearray() while len(buf) n: chunk sock.recv(n - len(buf)) if not chunk: raise ConnectionError(对端关闭连接) buf.extend(chunk) return bytes(buf) def recv_msg(sock: socket.socket) - bytes: header recv_exact(sock, HEADER.size) (length,) HEADER.unpack(header) if length 8 * 1024 * 1024: # 防御异常长度避免被恶意包拖垮内存 raise ValueError(f消息体过大: {length}) return recv_exact(sock, length) def serve_tcp(host: str 0.0.0.0, port: int 9000) - None: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv: srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((host, port)) srv.listen(512) # 结合 net.core.somaxconn 一起看 while True: conn, addr srv.accept() conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) try: while True: body recv_msg(conn) send_msg(conn, back: body) except (ConnectionError, ValueError): conn.close()UDP版本要简单得多但要自己处理丢包和乱序。下面这个例子带序号和简单去重实际生产里还要加确认、重传和发送速率控制。import socket import struct UDP_HDR struct.Struct(!IH) # 序号 body长度 MAX_DGRAM 1200 # 单包应用数据上限避开IP分片 def send_udp_segments(sock: socket.socket, addr, data: bytes) - None: seq 0 for i in range(0, len(data), MAX_DGRAM - UDP_HDR.size): chunk data[i:i MAX_DGRAM - UDP_HDR.size] pkt UDP_HDR.pack(seq, len(chunk)) chunk sock.sendto(pkt, addr) seq 1 def recv_udp(sock: socket.socket) - None: expected 0 buf {} while True: pkt, _ sock.recvfrom(65535) if len(pkt) UDP_HDR.size: continue # 非法短包直接丢 seq, length UDP_HDR.unpack(pkt[:UDP_HDR.size]) body pkt[UDP_HDR.size:UDP_HDR.size length] if seq expected: continue # 重复包丢弃 buf[seq] body while expected in buf: # 按序交付给上层 print(buf.pop(expected)) expected 1这两段代码的差别本身就是TCP和UDP差异的最好注解TCP版本我只管读写字节顺序和完整性内核保证UDP版本我几乎一半的代码在维护序号和缓冲。3.3 内核参数与业务侧调优清单调参这件事我的原则是先测量再改没数据支撑的调参就是在赌。下面这张表是我在多数服务端场景会检查的项目数值给的是量级参考不是照抄标准。参数常见默认值调整思路注意事项net.core.somaxconn4096高并发短连接服务可提到 8192~65535必须和 listen() 的 backlog 一起调小的那个生效net.ipv4.tcp_max_syn_backlog1024抗SYN洪峰可提到 8192超过内存允许范围反而增加风险net.ipv4.tcp_rmem / tcp_wmem自适应跨地域大流量按BDP放宽最大值改大是放大缓冲会增加延迟不是越大越好net.ipv4.tcp_tw_reuse2客户端侧开启可复用TIME_WAIT只在主动发起连接的一侧有意义net.ipv4.ip_local_port_range32768 60999高并发客户端可放宽到 10240 65535端口多了但TIME_WAIT总量也会上升net.ipv4.tcp_slow_start_after_idle1长连接建议设为0避免空闲后重新慢启动导致吞吐骤降net.ipv4.tcp_mtu_probing0链路存在MTU黑洞时设为1或2会引入探测开销有个参数我要单独提醒tcp_tw_recycle很多老教程还在推荐开它但它依赖时间戳判断在NAT环境下会把正常连接误判成旧连接直接丢掉新内核已经移除了这个选项。看到一篇教程让你开它这篇教程的年龄基本可以推断出来了。业务侧的调优往往比内核参数更有效。比如长连接加上应用层心跳间隔取30到60秒比依赖TCP的keepalive默认7200秒才开始探测实用得多。再比如写操作使用sendall而不是send不要自己假设一次写完了。还有接收缓冲区大小要按业务最大消息定太小会导致多次系统调用太大会占内存。3.4 在TCP之上补UDP的课可靠UDP与QUIC的取舍如果你的业务确实不能忍受队头阻塞又必须保证可靠那方向就是“基于UDP自建可靠层”。这件事现在有成熟的参考QUIC把可靠性、加密、多路复用都做进了用户态一条连接上多个流互不阻塞丢一个流的数据不影响其他流。它的握手也更快支持0-RTT恢复有重放风险的约束。但我不建议所有团队都上自建方案。判断是否值得看三条一队头阻塞是不是可测量的真实瓶颈别凭感觉二团队能不能吃下用户态协议栈的调试成本抓包、丢包统计、拥塞算法都比TCP复杂三中间网络对UDP的放行策略是否可控某些企业网络会限制UDP大流量。如果只是想让UDP变得“够可靠”最小可行的补法是序号 接收端ACK 发送端超时重传 去重窗口。这四样做扎实能覆盖大多数内网和弱网场景。剩下的拥塞控制可以先用固定速率起步观察丢包率再动态调整别一上来就追求完整算法。4. 常见问题与排查技巧实录4.1 粘包、半包与拆包一套通用的长度字段方案粘包问题的排查有个很明显的特征日志里打印收到的数据长度忽大忽小解析JSON或Protobuf时报“格式错误”“unexpected end of data”。看到这类报错先别怀疑序列化库八成是分帧没做对。我的通用做法是把分帧逻辑封装成一个独立的Decoder类和业务代码彻底解耦class FrameDecoder: def __init__(self, header_size: int 4, max_body: int 8 * 1024 * 1024): self.header_size header_size self.max_body max_body self.buf bytearray() def feed(self, data: bytes): 喂入任意长度数据返回已解析出的完整消息列表 self.buf.extend(data) messages [] while True: if len(self.buf) self.header_size: break (length,) struct.unpack(!I, self.buf[:self.header_size]) if length self.max_body: raise ValueError(消息长度异常可能流已错位) total self.header_size length if len(self.buf) total: break # 半包等更多数据 messages.append(bytes(self.buf[self.header_size:total])) del self.buf[:total] return messages这段代码有三个细节值得说。第一缓冲区用bytearray并按需删除已消费部分避免无限增长第二长度上限必须校验否则一个伪造的长度头就能让你分配几个G内存第三收到的数据长度为零表示对端关闭要在上层处理不能死循环重试。注意长度字段的字节序必须两端一致。我自己定规矩网络传输统一用大端!I本地存储再用小端避免跨语言对接时踩坑。4.2 TIME_WAIT、端口耗尽与连接重置的定位路径这三类问题在线上表现相似都是“连不上”但根因完全不同得按顺序排除。先看错误信息。Cannot assign requested address基本是本地端口耗尽常见于客户端高频短连接。Connection reset by peer是对端发了RST可能是对端进程崩了、或者连接被中间设备重置、或者你往一个已被关闭的连接写了数据。Connection timed out是握手或数据传输没有响应可能是防火墙丢包、对端队列满、路由不通。定位端口耗尽直接看统计# 查看TIME_WAIT数量和各状态分布 ss -s ss -tan state time-wait | wc -l # 查看当前可用端口范围 cat /proc/sys/net/ipv4/ip_local_port_range一个粗略的估算公式如果单机能用的临时端口是28000个TIME_WAIT持续60秒那么每秒最多能发起约28000/60 ≈ 466次短连接。超过这个量级必然出现端口耗尽。解决办法不是无限扩大端口范围而是改用连接池或长连接从源头减少连接创建次数。TIME_WAIT堆积还要分清是谁在主动关闭。服务端如果先关连接会把TIME_WAIT留在自己身上反过来客户端先关服务端就轻松了。所以协议设计上通常让客户端主动关闭更合理客户端数量多每个客户端上的TIME_WAIT量小不会形成单点压力。至于RST我遇到过最隐蔽的一次是服务端设置了读超时超时后直接close但客户端还在慢慢发数据于是服务端内核回了RST客户端报错。后来改成先shutdown(SHUT_WR)半关闭等对端也关闭再彻底释放问题解决。这个细节在优雅关闭的实现里必须处理。4.3 常见问题速查表现象大概率原因快速验证方式处理方向解析报文报长度错误分帧缺失或长度字段不一致打印原始字节流看边界统一使用长度前缀解码器请求响应凭空多40msNagle与延迟ACK互相等待抓包看小包发送间隔开启 TCP_NODELAY高峰期大量SYN超时半连接或全连接队列溢出netstat -s看队列溢出计数调 somaxconn 与 backlog吞吐上不去带宽有余接收窗口小于BDPss -ti看 cwnd 与窗口放宽 tcp_rmem/tcp_wmem短连接报端口耗尽TIME_WAIT占满临时端口统计time-wait数量改连接池或长连接UDP大量丢包且集中在特定大小数据报超过MTU被分片ping -M do探测路径MTU单包控制在1200字节内长连接空闲后变慢空闲后重新慢启动观察空闲前后 cwnd 变化关闭 slow_start_after_idle弱网下TCP延迟剧烈抖动丢包触发重传与窗口收缩抓包看重传间隔评估BBR或改传实时数据用UDP这张表我自己一直贴在工位上遇到问题先扫一遍多数情况能在五分钟内锁定方向。5. 场景对照与我的踩坑记录5.1 典型业务场景选型对照理论讲完落到具体业务上选择其实没那么纠结。下面这些场景我都实际做过或者深度参与过结论有实践支撑。场景推荐协议关键理由必须配套的措施配置下发、指令控制TCP数据必须完整且有序丢一条可能造成状态错误长度前缀分帧、超时重连、幂等设计设备状态心跳上报UDP高频、小包、丢一帧无所谓下一帧覆盖应用层序号、服务端容忍乱序实时音视频UDP不能忍受队头阻塞宁可丢帧前向纠错、抖动缓冲、带宽自适应文件传输、大报文同步TCP完整性优先吞吐靠窗口调优解决合理的缓冲区、断点续传局域网服务发现UDP组播TCP做不了组播限制广播域、加请求去重高频交易行情推送UDP或可靠UDP延迟敏感旧数据价值快速衰减序号快照补发机制移动端弱网API调用TCP或QUIC可靠性优先QUIC可缓解队头阻塞连接复用、超时退避有个规律值得注意同一个系统里往往是分层的。控制面和数据面分开控制面用TCP保正确数据面用UDP保时效。这种架构在设备管理、游戏同步、实时协作里都很常见。别指望一种协议包打天下。5.2 几条踩过坑才肯信的结论第一条不要在应用层重复实现TCP已经做好的事除非你能量化收益。我见过一个项目在TCP上面又加了一层序号和确认代码量翻倍延迟没降反而因为双重确认出了几次时序bug。第二条无连接不等于无状态管理。UDP服务端如果想支持“会话”必须自己维护一张表并且设置过期时间。这张表的过期时间要和NAT映射的存活时间对齐一般取30到120秒。我吃过一次亏服务端会话保留5分钟NAT那边2分钟就回收了映射结果服务端一直往一个不存在的地址发包客户端完全收不到日志里一点异常都没有。第三条缓冲区大小要按业务峰值算不是按平均值。接收缓冲区小于峰值消息长度会导致消息被截断或者多次读取前者引发解析错误后者增加系统调用。一个简单的算法取P99消息长度的两倍作为初始值压测后再调。第四条优雅关闭比优雅启动难写得多。服务重启时如果直接杀进程已建立的TCP连接会被RST客户端拿到的是错误而不是“服务暂不可用”。正确做法是先停止接受新连接、通知注册中心下线、等待存量请求处理完、再关闭监听套接字。这段时间要给足通常30秒起步。第五条监控要盯协议层指标不能只看业务成功率。重传率、连接建立耗时、TIME_WAIT数量、UDP丢包率这些指标能在业务还没明显异常时就暴露问题。我现在的习惯是给每个对外服务都配一张协议层看板出故障时先看它比翻应用日志快一个数量级。最后分享一个我常用的小检查新服务上线前用ss -ti和tcpdump各跑一遍正常业务流量把基线数据存下来。等线上出问题时把当时的抓包和基线一比重传多了、窗口小了、RTT涨了一眼就能看出来。这比任何排查手册都管用因为对比的是你自己的系统而不是别人的经验值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑