如果你正准备学计算机网络,却又总觉得“OSI七层模型”“TCP三次握手”这些概念离真实开发很远,那我特别建议你换一个切入点:拿nginx原理当“活教材”。nginx可能是最容易把计算机网络概述里散装知识点串起来的软件——它同时涉及TCP连接管理、HTTP协议解析、反向代理、负载均衡、事件驱动模型。这篇笔记,我打算用“一次网页请求从输入URL到页面呈现”作为主线,先把网络基础概念过一遍,然后拆解nginx是怎么工作的。无论你是刚开始接触计算机网络的学生,还是在业务里经常跟nginx打交道的开发者,这篇内容都值得你花点时间慢慢读。
1. 计算机网络概述:先搞清楚一个请求是怎么“出门”的
1.1 为什么必须先讲“概述”
很多人学计算机网络,第一反应就是去背协议:ARP是什么、ICMP又是什么、TCP和UDP区别是什么。背了一堆,遇到实际问题照样懵。原因在于缺少一个整体视角:这些协议和分层到底是为了解决什么问题?
计算机网络的核心矛盾很简单:一堆设备要互相通信,但设备型号不同、系统不同、距离有远有近、线路质量参差不齐。如果让每个应用都自己管信号、管路由、管重传,那应用代码会爆炸。于是人们想到一个办法——把通信这件事拆成几层,每一层只解决一个特定问题,层与层之间只通过标准接口打交道。这和快递公司的运作逻辑一模一样:你只需要在包裹上写地址,剩下的揽收、分拣、运输、派送全都由不同岗位负责,每一层都不需要知道其他层怎么干活。
这就是分层模型的价值。它让网络变得可设计、可维护、可排查故障。计算机网络概述里最值钱的东西,不是那些协议缩写,而是这个“分层思考”的框架。你理解了这个框架,后面看nginx是做什么的、为什么性能高,就会顺理成章。
1.2 一次访问,串起各层协议
我们来走一条最普通的请求链路。你在浏览器里输入一个域名,回车。接下来发生的事就是计算机网络概述的核心内容:
第一步,浏览器需要知道这个域名对应的服务器IP。它先去查本地DNS缓存,没命中就向配置的DNS服务器发起递归查询,拿到IP地址。这是应用层的DNS协议在干活。
第二步,浏览器发起TCP连接。IP地址有了,但还需要端口。HTTP默认80,HTTPS默认443。连接建立用三次握手:客户端发SYN,服务器回SYN+ACK,客户端再回ACK。这一步是传输层TCP协议在负责。TCP的可靠传输能力,保证后面发的HTTP请求不会丢、不会乱序。
第三步,TCP连接建立后,浏览器把HTTP请求报文发给服务器。请求报文里有请求行、请求头、空行、请求体。服务器解析之后返回HTTP响应,里面有状态码、响应头、响应体。这依然是应用层的HTTP协议在工作。
第四步,当服务器返回响应之后,浏览器开始解析HTML、CSS、JS,加载图片资源。页面上的每个资源都可能触发新的HTTP请求,这些请求可以复用已经建立好的TCP连接,也可以另开新连接。
整个过程里,还有一个常被忽略的参与者:网络层的IP协议。它负责把数据包从你的电脑路由到目标服务器。中间经过多少路由器、走哪条路径,就是IP路由在做的事。
1.3 “数据包出门”时到底发生了什么
这个环节特别容易迷糊:数据不是一个完整的HTTP请求直接“嗖”地传到服务器,而是被一层层打包的。应用层产生HTTP报文,传输层把报文切成适合传输的段,加上TCP头;网络层加上IP头,变成数据报;链路层再包上以太网头,变成帧,最后通过网卡发出去。
这个过程叫封装。服务器收到时反着来,一层层解封装,剥掉链路层头、网络层头、传输层头,最后把HTTP报文交给应用处理。
我建议你对照一张表把这些层和实物对应起来:
| 层次 | 常见协议/技术 | 负责解决的问题 | 类比 |
|---|---|---|---|
| 应用层 | HTTP、HTTPS、DNS、FTP | 应用之间如何理解和交换业务数据 | 快递面单上的内容 |
| 传输层 | TCP、UDP | 端到端可靠传输与端口寻址 | 电话线路保证双方接通 |
| 网络层 | IP、ICMP、路由协议 | 数据包如何在网络中寻址和路由 | 快递分拣中心的路线规划 |
| 链路层 | 以太网、WiFi、ARP | 相邻设备之间如何传输比特流 | 卡车把包裹送到下一个站点 |
这张表会一直跟随我们后面说nginx。nginx主要工作在应用层,但它依赖传输层的TCP连接,也受网络层路由影响。所以nginx的很多调优参数,本质是你在处理传输层问题。
1.4 分层的价值:出了问题你知道去哪查
分层带来的最大实际好处是故障定位。线上一个请求失败,如果报错发生在应用层,比如HTTP 500,那多半是后端代码出问题;如果请求根本到不了后端,连接超时、连接被拒绝,那就要怀疑传输层和网络层。
我见过不少新手排错时像无头苍蝇:后端代码没问题,就怀疑nginx配置;nginx配置改了一通,发现还是不通,才想起来看服务器防火墙和端口监听状态。这就是没有分层思维的表现。
正确的排错顺序是:先看链路层通不通(ping、网卡状态),再看网络层通不通(路由、连通性),然后看传输层通不通(端口、TCP握手),最后才看应用层(HTTP状态码、业务日志)。每一层都有独立的证据链,逐层排查,基本不会漏。计算机网络概述的价值,不在于让你记住七层协议各叫什么名字,而在于它给了你一套完整的排错地图。
2. 理解nginx之前,先把TCP/IP这几个概念吃透
2.1 IP和端口:一个连接到底是怎么被唯一确定的
nginx是一个监听端口、处理请求的软件。监听端口这个动作,背后涉及一个非常基础的概念:五元组。
一个TCP连接由五个元素唯一确定:源IP、源端口、目的IP、目的端口、协议类型。服务端监听在某个IP和端口上,客户端从自己的随机端口发起连接。随机端口的范围一般是1024到65535之间。所以一个服务器虽然只有一个80端口,但它能同时维护几万个连接,就是因为连接的另一端是海量不同的客户端IP和端口。
nginx配置里经常写listen 80;,全写是listen 0.0.0.0:80;。0.0.0.0表示监听本机所有网卡地址上的80端口。如果服务器上有多个IP,且你只想让nginx服务其中一个IP,可以显式写成具体的IP。这个细节在高可用场景里很实用,多IP绑定网卡后,监听地址写错就会导致外部访问不来。
2.2 三次握手和连接队列:nginx为什么有backlog
TCP三次握手是计算机网络概述必考的内容,但很多人不知道它跟nginx的并发行为直接相关。
客户端发SYN,服务端内核收到后,进入SYN_RECV状态,同时把这个半连接放进半连接队列。服务端回SYN+ACK,客户端回ACK,连接建立,进入ESTABLISHED状态,然后被放入全连接队列。应用程序(比如nginx的worker进程)从全连接队列里取连接进行处理。
这个全连接队列的长度,就是所谓的backlog。Linux里有个参数net.core.somaxconn控制这个队列上限。nginx的listen指令里也可以配backlog=4096这样的值。如果你的nginx在连接量大时出现连接等待或握手超时,可能需要检查队列长度是否不够。你可以用ss -lnt查看当前socket的Send-Q和Recv-Q,如果Recv-Q长期接近或等于backlog值,说明连接队列有积压,应用处理速度跟不上。
握手只是个开始,更让新手头疼的是TIME_WAIT状态,后面我会专门讲排查实战。
2.3 四次挥手与连接状态:TIME_WAIT为什么值得关注
HTTP请求结束后,TCP连接会被关闭,关闭过程是四次挥手:主动关闭方发FIN,对方回ACK,对方再发FIN,主动方再回ACK。
这里有个关键的细节:主动关闭的一方在发送最后的ACK之后,不会立刻进入CLOSED状态,而是进入TIME_WAIT状态,要等2MSL(最大报文段生存时间的两倍)才真正关闭。之所以这样设计,是为了防止最后的ACK丢失导致对方重发FIN,同时也是为了让旧连接上的延迟包在网络中自然消逝,不影响后续新连接。
nginx的默认行为值得我们留意:当nginx作为客户端去连接后端上游时,它通常是发起连接的一方,但在关闭连接时它不一定是主动方。如果nginx和后端之间频繁建立短连接,那么连接关闭后就会产生大量TIME_WAIT状态的连接。虽然TIME_WAIT本身不占太多资源,但如果QPS很高,端口和连接表项会被短暂占用,极端情况下会出现“connection refused”或者端口耗尽。所以nginx反向代理场景里,建议开启upstream keepalive长连接,让连接复用,从根源上减少频繁建连和断连。
2.4 Keep-Alive:一个TCP连接处理多个HTTP请求
理解Keep-Alive,是理解nginx性能优化的一个关键点。HTTP/1.1默认开启Keep-Alive,意思是HTTP请求和响应都走同一个TCP连接,而不是每发一次请求就重新建一条TCP连接。
这个机制在浏览器访问网页时特别直观。一个网页平均有几十个资源请求,如果没有Keep-Alive,浏览器就要建立几十次TCP连接,每次握手都要一个RTT,页面加载速度会被拖得很慢。启用Keep-Alive之后,TCP连接复用,所有资源请求都在同一条连接上排队发送,效率提升非常明显。
nginx里有两个地方跟Keep-Alive相关。一是keepalive_timeout,这是控制nginx与客户端之间连接的空闲超时时间。二是upstream块里的keepalive参数,它控制nginx与后端服务器之间保持空闲连接的数量。我见过很多配置,客户端keepalive_timeout设得很大,但upstream keepalive没配,导致每次请求都要重新连接后端,延迟白白增加。记住:反向代理模式下,两端连接复用都要配,缺一不可。
3. nginx为什么快:进程模型与事件驱动
3.1 Master进程与Worker进程,各干各的活
nginx刚开始启动时,会有一个master进程,然后fork出多个worker进程。记住一句话:master管人,worker干活。
master进程负责读取和校验配置、监听信号、启动和停止worker进程。它不处理任何业务请求。当你执行nginx -s reload时,master先检查新配置的合法性,然后给worker进程发信号,让老的worker优雅退出、新的worker用新配置启动。整个过程对外服务不中断,这就是为什么线上更新nginx配置不需要重启服务的原因。
worker进程处理连接和请求。每个worker都是独立的进程,内部是单线程的事件循环,多个worker之间通过操作系统调度分配到不同CPU核心上运行。nginx官方建议worker进程数设为CPU核心数,或者使用worker_processes auto让nginx自己探测。设太多反而不一定好,因为进程切换有开销,还会争抢CPU资源。
我在实际项目里踩过一个坑:把worker_processes直接设成32,以为并行越多越快,结果在高并发下CPU上下文切换飙升,吞吐量反而下降。后来改成和CPU核数一致,整体表现稳了很多。这个细节说明,nginx调优一定基于实测,不能盲目堆配置。
3.2 事件驱动模型:一个进程怎么管几万连接
很多人第一次听说nginx能支持高并发时,会问一个很自然的问题:一个worker只是一个单线程进程,它怎么同时处理那么多请求?
这就要说到I/O多路复用,尤其是epoll。传统做法是一个连接对应一个线程,线程阻塞在read上等待数据,连接一多,线程数爆炸,上下文切换开销巨大。epoll的做法是:内核帮你盯着所有关注的socket,某个socket有数据可读时,内核主动通知你,你再去处理它。这样一个线程就能管理几千、几万个连接,nginx就是在事件循环里不断处理这些就绪事件。
这个过程很像一个高效的客服调度员:如果他每个电话都一对一盯着等对方说话,那他同时只能服务一个客户。但如果电话交换机主动告诉他哪条线路有人说话,他就可以轮流应对,同一时间服务几十个客户也不会乱。
nginx的worker内部没有锁,也没有阻塞调用,所有事件都靠回调驱动。所以nginx在处理高并发静态文件、反向代理等任务时,CPU利用率和内存占用都控制得非常好。这也是为什么很多场景下,使用nginx比直接用某些多线程Web服务器省内存。
3.3 反向代理与负载均衡:nginx在架构里的位置
nginx最常用的身份是反向代理。正向代理替代客户端去访问外部资源,反向代理则是替服务端接客,客户端感知不到后端真实服务器的存在。在反向代理模式下,客户端连接nginx,nginx再连接后端的应用服务器,比如Tomcat、某应用服务进程等。
与之紧密相关的是负载均衡。一个后端扛不住流量,就加多个后端。nginx通过upstream定义一组后端服务器,然后按策略把请求分发到不同后端上。
常见的策略有几种,我实际使用中按场景选择:
| 策略 | 工作原理 | 适用场景 |
|---|---|---|
| 轮询 | 请求依次发给每个后端 | 后端性能均衡、无状态服务 |
| 加权轮询 | 按权重分配请求比例 | 新旧机器混用、配置差异大时 |
| IP_HASH | 按客户端IP哈希选后端 | 需要会话保持的旧系统 |
| least_conn | 发给活跃连接数最少的后端 | 后端处理耗时差异明显 |
我在大部分场景里默认用加权轮询,同时结合后端健康检查来剔除故障节点。要注意:nginx做负载均衡时不看业务状态,它只看后端TCP能否建连。如果后端进程假死、端口还监听,nginx照样会把流量分过去。所以后端自己的健康检查接口很重要,nginx能把检查接口、重试机制配合起来用,才能及时发现异常节点。
3.4 缓冲与零拷贝:让数据搬运更高效
nginx不只是转发数据,它还做缓冲。所谓缓冲,就是nginx在从上游读完响应后,把它暂存到自己的内存或临时文件里,再按一定的速度回给客户端。
这个设计带来了几个好处。第一,如果客户端网速慢,而nginx已经把上游响应读完了,那nginx可以先把响应存进缓冲区,客户端慢慢收,这时上游连接可以及时释放,不至于被慢客户端拖住。第二,如果上游返回超大响应,nginx可以用临时文件落盘,防止内存被撑爆。相关指令包括proxy_buffering、proxy_buffer_size、proxy_buffers等。
另一个值得说的是零拷贝。传统文件传输需要把磁盘数据读到内核缓冲区,再复制到用户态,再写回socket发送缓冲区。零拷贝技术通过内核直接完成磁盘到网卡的数据搬运,减少两次复制。nginx里开启sendfile on可以让静态文件直接由内核发出去,文件下载和静态资源访问性能提升明显。
不过要留意:缓冲不是越多越好。如果后端的接口要求实时响应,比如流式接口、Server-Sent Events,就不能开大缓冲甚至要关掉proxy_buffering,否则数据会攒到缓冲区满了再发给客户端,导致前端收到数据的延迟明显变大。我曾经排查过一个SSE推送延迟的故障,最后就是proxy_buffering在中间“攒数据”,关闭之后延迟立刻下降。
4. 一条HTTP请求在nginx里的完整旅程
4.1 从客户端连接到请求解析
前面讲了那么多离散概念,这里把它们串起来:一条HTTP请求到达nginx,到底发生了什么。
假设客户端已经跟nginx建好TCP连接。这个连接的状态由Linux内核维护,nginx的worker进程通过epoll关注这个连接的可读事件。当请求数据到达,worker会读取数据,按HTTP协议解析出请求行、请求头、请求体。
这里有个细节:nginx解析请求头时,默认限制头部大小,client_header_buffer_size默认1KB,超大的头部要用large_client_header_buffers。如果一个请求的Cookie特别多,或自定义头特别长,正是这些参数决定nginx会不会返回400错误。
请求解析完之后,nginx会根据配置找到对应的location块,然后决定是直接返回本地文件或响应,还是转发给上游。到这里,nginx的“路由”环节就完成了。
4.2 路由匹配与proxy_pass的关键区别
nginx的路由匹配围绕server和location。server块根据域名区分虚拟主机,location块根据URI路径进一步区分处理方式。location匹配有前缀匹配、精确匹配、正则匹配,优先级是:精确匹配大于前缀最长匹配大于正则匹配大于普通前缀匹配。这个优先级顺序我建议画个简单流程图再对照配置,否则很容易搞混。
转发时最让人困惑的是proxy_pass后有没有带URI的区别。看两个例子:
location /api/ { proxy_pass http://backend; }location /api/ { proxy_pass http://backend/; }第一个例子,nginx会把完整的URI传给后端,客户端请求/api/user,后端收到/api/user。第二个例子,nginx会把location前缀/api/替换成/再传给后端,客户端请求/api/user,后端收到/user。
这个差别很大。实际项目中,前端和后端对路径的约定经常不一致,改nginx匹配规则时一旦搞错转发路径,后端就直接404。排查这类问题时打开nginx access log,看$request_uri和$upstream_addr对应的真实请求路径,很快能定位问题。
4.3 连接上游:建立、写请求、读响应、回写客户端
确定转发目标后,nginx会去连接后端。如果upstream里配置了keepalive,nginx会优先从空闲连接池里取一条连接,没有空闲连接时才新建。
连接建立成功后,nginx把客户端请求改写后发送给后端。注意,nginx转发给后端时,会把请求头里的Host改成后端配置的Host,还会携带X-Forwarded-For这个头记录客户端原始IP。很多时候后端程序拿不到用户真实IP,是因为nginx配置里没有正确设置这些头。
后端返回响应后,nginx按前面说的缓冲机制处理响应,然后把响应头发给客户端,再逐步发送响应体。响应发完后,一条请求就结束了。如果开启了keepalive,TCP连接不会立刻关闭,而是留在连接池里等待下一个请求复用。
这个过程听起来简单,但每一步都有对应的超时配置:proxy_connect_timeout控制连接后端超时,proxy_send_timeout控制向后端写请求超时,proxy_read_timeout控制读取后端响应超时。默认值分别是60s、60s、60s。对大多数API场景来说太长了,我通常会把connect_timeout设成5s以内,给慢后端一个快速失败的机会。
4.4 自己动手:本地搭一个nginx反向代理并抓包验证
理论讲再多,不如动手做一遍。我建议你花20分钟做这个小实验。
准备两个进程:一个简单的后端服务,比如用某框架写一个返回JSON的接口,监听8000端口;nginx监听8080端口,把请求转发到8000。
nginx配置示意:
upstream backend_servers { server 127.0.0.1:8000; } server { listen 8080; location /api/ { proxy_pass http://backend_servers; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }保存配置后启动nginx,然后执行:
curl -v http://127.0.0.1:8080/api/hello你会看到TCP连接建立、发送HTTP请求、接收HTTP响应整个过程。如果想看得更底层,用tcpdump抓本机回环网卡:
tcpdump -i lo -nn port 8080 or port 8000再执行一次curl,你能在抓包里看到SYN、SYN-ACK、ACK、HTTP GET、响应数据、FIN等所有报文。把抓包结果和前面的分层模型对照起来看,计算机网络概述里那些抽象名词瞬间就有了画面感。我自己带新人时经常用这个实验,比单纯看PPT效果好很多。
5. 常见问题与故障排查实录
5.1 高并发下TIME_WAIT堆积,怎么处理
线上业务量一大,短连接场景下很容易看到大量TIME_WAIT状态连接。执行ss -s能看到TIME_WAIT数量几千上万。
TIME_WAIT本身并不可怕,它是TCP正常状态。怕的是数量太多占满连接表项或端口,导致新连接无法建立。我遇到过一次典型的端口耗尽问题:某个服务短连接QPS非常高,客户端端口被TIME_WAIT占满,报错“Cannot assign requested address”。
处理方法有两种思路。思路一是减少新建连接,即使用keepalive长连接,让一条连接处理多个请求。思路二是让TIME_WAIT状态的连接尽快释放。Linux里可以开启net.ipv4.tcp_tw_reuse,允许复用TIME_WAIT连接作为新连接。另外调整net.ipv4.ip_local_port_range扩大可用端口范围也能缓解。
但注意,不要贸然开启tcp_tw_recycle,它在NAT环境下会引发随机丢包问题,属于明确不建议使用的参数。我曾经在一个老旧运维文档里看到有人推荐它,实际一开就出问题,后来又调回去了。靠抓包确定根因永远比盲目调参数更可靠。
5.2 502、504、499分别是什么意思
这是nginx反向代理场景下最常见的三个状态码,很多人分不清楚。
502 Bad Gateway表示nginx连接后端的请求失败,或者后端返回了无效响应。常见原因:后端服务没启动、端口被防火墙挡了、后端进程崩溃、nginx配置的upstream地址写错。排查思路:先检查后端端口是否监听,再检查后端日志,再从nginx error.log里看具体的connect错误信息。
504 Gateway Timeout表示nginx把请求转发到后端后,后端在nginx配置的超时时间内没有返回响应。常见原因:后端接口处理太慢、后端线程池满了、proxy_read_timeout设得太短。我曾排查过一个报表导出接口,导出大文件超过60秒,默认超时触发504,把proxy_read_timeout调大后解决。
499状态码比较特殊,它是nginx自己定义的状态码,表示客户端主动关闭了连接,nginx还没把请求完整转发出去或后端还没返回,客户端就关了连接。常见于客户端设置了超时,等不及就先断开。排查时重点看客户端超时时间,以及后端响应是否过慢。
把三个状态码对比一下会更清楚:
| 状态码 | 含义 | 常见根因 |
|---|---|---|
| 502 | 无法有效连接后端 | 后端未启动,防火墙拦截 |
| 504 | 后端响应超时 | 后端处理慢,超时配置太小 |
| 499 | 客户端提前断开 | 客户端等不及,主动取消请求 |
5.3 连接被重置:RST包背后的真相
有一次朋友让我看一个“nginx间歇性报错”的问题:客户端请求偶尔失败,错误信息里出现“connection reset by peer”。从应用日志看,后端服务很稳定,没有崩溃记录。
我让他在nginx上抓包。抓包结果显示,某些请求在nginx往后端建立连接时,后端直接回了RST包。后来一查,后端服务所在服务器的并发连接数到了上限,系统拒绝新连接,于是发RST。把后端的文件描述符上限调高、接入port范围扩大后,问题消失。
RST包在抓包里是很好认的:没有后续交互,直接断开。产生RST的常见原因包括:端口未监听、防火墙拒绝、连接数满了、程序主动关闭未处理完成的连接。遇到连接重置,不要只盯着应用日志,优先抓包看RST从哪一端发出来,问题就缩小到那个方向。
5.4 一键排查速查表
把我这些经验整理成速查表,遇到问题时对号入座:
| 现象 | 排查命令 | 对应层次 |
|---|---|---|
| 访问不通 | ping、traceroute、检查网卡 | 网络层/链路层 |
| 端口连不上 | telnet、nc、ss -lnt | 传输层 |
| 连接被重置 | tcpdump抓包看RST | 传输层 |
| 请求未到后端 | tail -f access.log、error.log | 应用层/传输层连接状态 |
| 504响应 | 后端日志、连接池状态 | 应用层 |
| 499响应 | 客户端日志、上游响应耗时 | 应用层 |
这张表的价值在于,它把计算机网络概述里的分层概念真正落地成了排查流程。以后遇到问题,先判断在哪一层,再决定用什么工具和命令,基本不会乱。
写到这里,我最后想分享一个习惯:排查网络问题不要靠猜,抓包和日志比什么都诚实。我自己带新人时特别推荐一个动作,把浏览器开发者工具里的请求时间线、nginx access log里的$upstream_response_time、以及抓包文件三个视图对齐起来看,判断慢请求究竟卡在DNS解析、TCP连接、nginx转发、后端处理还是响应传输哪一个环节。很多人一开始觉得麻烦,试过几次之后就会发现,这才是理解计算机网络概述那些概念最正确的方式——让每一个理论名词,都在真实请求中有一个明确的位置。