网络/操作系统 面试题
1. TCP 三次握手与四次挥手
❓ 题目: TCP 为什么是三次握手而不是两次或四次?四次挥手时为什么有 TIME_WAIT 状态?
💡 答案:
三次握手的根本目标是:在不可靠的网络上建立可靠的连接,同时防止旧的连接请求(来自历史网络延迟)突然到达服务端导致错误。如果只有两次握手,客户端发送 SYN,服务端回 SYN+ACK 就认为连接建立——但此时客户端可能没收到 SYN+ACK(丢包),服务端已经为这个连接分配了资源。更关键的问题是:网络中的一个历史 SYN 包延迟到达服务端,服务端以为这是一个新连接请求,回复 SYN+ACK 后建立连接,客户端早已关闭不会有数据发送,服务端的资源就白白浪费了。三次握手中,服务端还需要等待客户端一个 ACK 才能确认建立连接——这个 ACK 可以确认客户端确实收到了 SYN+ACK 且愿意建立连接,同时能区分”这是一个活跃请求”和”这是一个历史延迟请求”。
四次挥手中的 TIME_WAIT 状态持续 2MSL(Maximum Segment Lifetime,通常是 60 秒),主要作用有两个:
- 一是确保最后一个 ACK 能被对方收到——如果最后 ACK 丢失,对方会重发 FIN,TIME_WAIT 状态下还可以再次回复 ACK;
- 二是”等待网络中所有属于这个连接的包都消失”——防止新建立的连接(相同的四元组)收到旧连接的延迟包造成混淆。
追问1: 大量 TIME_WAIT 状态连接出现在服务器上,是什么原因?如何优化?
大量 TIME_WAIT 状态的连接通常出现在”主动关闭连接”的一方。如果是 Web 服务器主动关闭(比如使用了短连接,服务器每次都主动断开 TCP),就会积累大量 TIME_WAIT,耗尽端口号或系统资源。主要优化方案:
- 一是改用长连接(HTTP Keep-Alive),复用 TCP 连接避免频繁断开;
- 二是将主动关闭权交给客户端(比如 Nginx 中
keepalive_timeout设短一些让客户端先关); - 三是调整内核参数——
net.ipv4.tcp_tw_reuse允许多个 TIME_WAIT 连接重用(只对客户端有效),tcp_tw_recycle(在新版本 Linux 中已废弃,因为对 NAT 环境不兼容),tcp_max_tw_buckets限制 TIME_WAIT 最大数量,多余的连接直接清理掉。
追问2: TCP 的 KeepAlive 机制是什么?它和 HTTP 的 Keep-Alive 有区别吗?
TCP KeepAlive 是传输层的保活机制——它定期发送一个空的 ACK 探测报文,确认对端是否仍然可达。如果对端无响应,KeepAlive 重试几次后 OS 认为连接断开,通知应用层。HTTP Keep-Alive 是应用层的连接复用机制——它表示一个 TCP 连接可以同时承载多次 HTTP 请求-响应,而不是一个请求就关闭一个连接。两者的层级和目的完全不同,TCP KeepAlive 感知”物理链路是否畅通”;HTTP Keep-Alive 是”业务层要不要复用这个连接”减少握手开销。在实际工程中 TCP KeepAlive 默认间隔很长(7200 秒),通常不适合直接用于快速故障检测,需要调整 tcp_keepalive_time 等参数,或者在应用层做心跳(如 WebSocket ping/pong)。
📌 易错点 / 加分项:
- TIME_WAIT 只出现在”主动关闭方”,是被动关闭方没有这个状态
- CLOSE_WAIT 大量出现是一种典型问题——被动关闭方收到了 FIN 但应用层没关连接,导致泄漏
tcp_tw_recycle在 Linux 4.12 已正式废除——面试时说用了它反而不好
2. HTTP 协议演进与性能优化
❓ 题目: 从 HTTP/1.1 到 HTTP/2 再到 HTTP/3,每一代协议解决了什么核心问题?
💡 答案:
HTTP/1.1 的核心痛点是”一个 TCP 连接同一时间只能处理一个请求”(队头阻塞),浏览器为了并发加载页面资源必须开 6-8 个 TCP 连接——但这增加了连接建立成本、也容易造成网络拥塞。HTTP/2 引入了多路复用——一个 TCP 连接上同时跑多个 Stream,每个 Stream 可以独立传输请求和响应,互不影响。解决了应用层的队头阻塞,大量减少了连接数。HTTP/2 还引入了头部压缩(HPACK)和服务端推送。但 HTTP/2 引出了新的问题:因为所有 Stream 跑在一个 TCP 连接上,如果 TCP 中某一个包丢失,整个 TCP 连接的所有 Stream 都被阻塞等待重传——这就是 TCP 层的队头阻塞,HTTP/2 无法解决这个问题。HTTP/3 彻底放弃了 TCP,改用 QUIC 协议(基于 UDP),每个 Stream 独立拥塞控制——单个 Stream 丢包只影响该 Stream,不影响其他 Stream 的数据传输。
追问1: HTTP/2 的多路复用解决了 HTTP/1.1 的什么痛点?它又是如何引入”队头阻塞”的?
HTTP/1.1 的队头阻塞是串行请求的问题:浏览器请求 A → 服务器响应 A → 浏览器请求 B → 服务器响应 B,A 没处理完 B 就得排队。Pipelining 试图让浏览器一口气发多个请求然后服务器按序响应,但实现复杂和中间件兼容性差导致几乎没有实际部署。HTTP/2 的帧(Frame)设计解决了这个问题:一个 TCP 连接包含多个 Stream,每个 Stream 由多个 Frame 组成(DATA 帧、HEADERS 帧)。发送方可以将不同 Stream 的 Frame 交错发送,接收方根据 Frame 头中的 Stream ID 组装回各自的请求/响应。所以 HTTP/2 中多个请求的 Frame 可以在同一个 TCP 连接上交错传输,不再需要等 A 完成再发 B。但又产生了 TCP 层的队头阻塞——底层 TCP 的滑动窗口中有任何一个包丢失,窗口内所有后续数据都无法被应用层读取。
追问2: HTTP/3 为什么把传输层从 TCP 换成了 QUIC?QUIC 的 0-RTT 连接建立是怎么做到安全的?
QUIC 解决的核心问题是 TCP 层的队头阻塞。QUIC 基于 UDP,在网络层之上实现了 Stream 独立传输——每个字节流有个 Stream ID,不同 Stream 的数据在 UDP 中独立成包传输,某个 Stream 的包丢失只阻塞这个 Stream,不影响其他 Stream 的数据到达。0-RTT 连接建立是 QUIC 的另一个杀手特性:客户端和服务端在首次连接时交换密钥并缓存(客户端保存服务端的密钥配置),之后重新连接时可在第一条 SYN 包中就带上业务数据,省去了一次 RTT。0-RTT 的安全保障是通过”客户端在生成的会话票证中加密存储上次协商的密钥参数”来实现——服务端验证通过后即可解密 0-RTT 数据。但 0-RTT 有”重放攻击”的风险——攻击者截获 0-RTT 包重放,服务端会重复处理。防御措施是:幂等请求允许 0-RTT,非幂等请求(转账、扣费)必须等握手完成。
📌 易错点 / 加分项:
- HTTP/3 基于 QUIC,QUIC 基于 UDP——但不是简单的 UDP,QUIC 重新实现了类似 TCP 的可靠传输+拥塞控制在用户空间
3. Linux 常用分析工具
❓ 题目: Linux 服务器上排查性能问题常用的命令行工具有哪些?每个工具的用途和典型用法是什么?
💡 答案:
- top / htop:实时进程监控。
top看 CPU 使用率(us/sy/id/wa)、内存使用、load average。重点关注 wa(IO 等待)——wa 很高说明磁盘是瓶颈。 - vmstat:系统级性能概览。
vmstat 1每秒刷新——r(运行队列长度,> CPU 核数说明竞争严重)、cs(上下文切换/秒)、us/sy/id/wa。高 cs 通常意味着线程太多或锁竞争激烈。 - iostat:磁盘 IO 统计。
iostat -x 1——%util(磁盘繁忙率,接近 100% 说明磁盘瓶颈)、await(平均 IO 等待时间)。 - free:内存使用。
free -h——available 才是真正可用的内存(包括缓存),不要只看 free 列。 - ss:网络连接。
ss -tunlp查看所有 TCP/UDP 连接。TIME_WAIT 大量出现时用这个排查。ss 是 netstat 的现代替代品——性能更好。 - pidstat:进程级性能分析。
pidstat -p <pid> 1看指定进程的 CPU/内存/IO/上下文切换。 - perf top:实时 CPU 火焰图,看 CPU 在哪些内核函数或进程符号上消耗时间。
📌 易错点 / 加分项:
- load average 不是 CPU 使用率——它是”正在运行 + 等待运行 + 不可中断睡眠的进程数”
free的 available ≈ free + buffer + cache(可回收部分),比 free 列更能反映真实可用内存ss在连接数很大时性能远超 netstat——netstat 遍历 /proc 很慢
4. DNS 解析全过程
❓ 题目: 从浏览器输入 www.example.com 到获取 IP 地址,完整的 DNS 解析过程是怎样的?CDN 如何利用 DNS 做智能调度?
💡 答案:
DNS 解析流程:
- 浏览器缓存:Chrome 先查自己的 DNS 缓存,有就返回。
- 操作系统缓存:浏览器 miss → 调用
getaddrinfo()→ OS 先查 hosts 文件(/etc/hosts),再查系统 DNS 缓存。 - 本地 DNS 服务器(递归查询):OS miss → 向
resolv.conf中配置的本地 DNS 发起查询。 - 根域名服务器:本地 DNS 向根查询
.com的权威 DNS 地址。 - 顶级域名服务器:向
.comTLD 查询example.com的权威 DNS 地址。 - 权威 DNS 服务器:向
example.com的权威 DNS 查询 A 记录,获取 IP 返回给客户端。
CDN 智能调度:当用户查询 cdn.example.com 时,权威 DNS 根据请求来源 IP(本地 DNS 的 IP)判断用户地理位置和运营商,返回离用户最近、负载最低的 CDN 边缘节点 IP。同一个域名在不同地区解析出不同 IP——这就是 GSLB(全局负载均衡)。
📌 易错点 / 加分项:
- DNS 默认走 UDP 53 端口——响应超过 512 字节时切换为 TCP
- CDN 场景下 DNS 的 TTL 设得很短(如 30 秒)以实现灵活的流量调度
- Anycast DNS 让多个地理位置的服务器共享同一个 IP,BGP 路由自动选择最近实例
5. HTTPS 握手与证书机制
❓ 题目: 请详细描述 HTTPS 的 TLS 握手过程。证书链是如何验证的?中间人攻击为什么无法在 HTTPS 下生效?
💡 答案:
TLS 1.2 握手(RSA 示例):
- Client Hello:客户端发送支持的 TLS 版本、加密套件列表、Client Random。
- Server Hello:服务端选择 TLS 版本和加密套件,返回 Server Random 和数字证书(含公钥)。
- 证书验证:客户端检查证书有效期、域名匹配、CA 签名有效性(用 CA 公钥解密签名得 hash,对证书内容做同样 hash 比对)。逐级验证直到根证书(根证书内置在浏览器/OS 信任存储中)。
- 密钥交换:客户端生成 Premaster Secret,用服务端公钥加密发送。服务端用私钥解密。双方用 Client Random + Server Random + Premaster Secret 计算出相同的 Session Key。
- 加密通信:双方发送 Finished 消息(用 Session Key 加密),握手完成。后续所有数据用 Session Key 对称加密。
防止中间人的原理:中间人可以拦截流量,但无法伪造服务端证书——它没有 CA 的私钥,无法为伪造证书生成合法 CA 签名。客户端验证证书时发现签名不匹配,拒绝连接。
📌 易错点 / 加分项:
- TLS 1.3 把握手精简为 1-RTT(去掉了 RSA 密钥交换,强制 ECDHE 前向保密)
- 证书的 CN 和 SAN 的区别——现代浏览器只看 SAN,CN 已弃用
- ECDHE 密钥交换提供前向保密——即使服务端私钥未来被泄露,以前的会话也无法被解密
6. CDN 工作原理
❓ 题目: CDN 是如何加速静态资源访问的?它的核心原理是什么?回源策略和缓存策略是如何设计的?
💡 答案:
CDN 的核心原理是就近访问——将静态资源提前分发到全球各地的边缘节点,用户请求时 DNS 智能解析到最近节点,从边缘节点直接返回,无需回源到中心机房。
工作流程:源站将内容 push 到 CDN 或 CDN 在首次请求时 pull 并缓存。后续用户 → DNS 解析 → 最近 CDN 节点 → 缓存命中直接返回 → 缓存 miss 则回源拉取后缓存返回。
缓存策略:CDN 遵循 HTTP 标准缓存头——Cache-Control: max-age=3600 缓存 1 小时,过期后回源验证(带 If-None-Match,源站返回 304 继续用缓存)。
回源策略:合并回源——同一 URL 的并发回源请求在 CDN 侧排队,只放一个请求回源,防止缓存穿透式回源打垮源站;预热——大促前手动推送热点资源到 CDN;分层缓存——边缘节点 miss → 父节点 → 源站,减少直接回源次数。
📌 易错点 / 加分项:
- CDN 的缓存 key 默认是 URL——URL 带了随机参数或 token 每次都是新 key,导致缓存命中率为零
- 源站可以做限速——防止边缘节点的大量回源把源站带宽打满
- CDN 也可以加速动态内容——通过智能路由选择最优回源路径
7. WebSocket 与长连接
❓ 题目: WebSocket 和 HTTP 长轮询、HTTP/2 Server Push 有什么区别?WebSocket 的握手和心跳机制是怎样的?
💡 答案:
WebSocket 是一种全双工通信协议,通过一次 HTTP 升级握手后切换到 WebSocket 协议,客户端和服务端可随时双向发送消息。
与相近技术的区别:长轮询——客户端发 HTTP 请求,服务端 hold 住直到有数据或超时,本质半双工,每次 HTTP 头部开销。HTTP/2 Server Push——服务端主动推送资源,是”预加载”而非”实时双向通信”。WebSocket——真正的全双工双向实时通信,单次握手后无额外头部开销。
WebSocket 握手:客户端发 HTTP 请求带 Upgrade: websocket 和 Sec-WebSocket-Key。服务端返回 101 Switching Protocols,用 Sec-WebSocket-Accept(SHA-1 + base64)响应。握手完成后 TCP 切换到 WebSocket 帧协议。
心跳机制:协议内置 Ping/Pong 帧——一端发 Ping,另一端必须回 Pong。如果一段时间没收到 Pong 认为连接断开。客户端库通常自动处理心跳,服务端需要配置空闲超时。
📌 易错点 / 加分项:
- WebSocket 没有跨域限制——但握手阶段的 HTTP Upgrade 请求需要 CORS
- 负载均衡器需要 WebSocket 支持——Nginx/HAProxy 需要配置 WebSocket proxy
- WebSocket 在移动网络和某些代理环境下易断开——需做好重连和心跳保活
8. select / poll / epoll 区别
❓ 题目: Linux 的 IO 多路复用中 select、poll、epoll 的区别是什么?为什么 epoll 在高并发下遥遥领先?
💡 答案:
三者都是 IO 多路复用机制,允许单线程监控多个文件描述符的 IO 事件。
select:fd 集合用 bitmap,限制最大 1024 个。每次调用需要把整个 fd 集合从用户态拷贝到内核态,内核遍历所有 fd 检查就绪状态。O(n),n 为监控总数。
poll:用 pollfd 数组替代 bitmap,没有 1024 限制。但每次仍需拷贝整个数组到内核态,内核仍遍历所有 fd。O(n),与 select 性能模型一样。
epoll:完全不同的架构。epoll_create 在内核创建 eventpoll 对象,epoll_ctl 把 fd 注册进去(只注册一次)。内核用红黑树存储所有注册 fd,fd 就绪时通过回调加入就绪链表。epoll_wait 直接从就绪链表取就绪 fd,不需要遍历。O(m),m 为就绪 fd 数。
epoll 领先的关键:只处理活跃连接。10000 个连接只有 10 个活跃时,select 遍历 10000 个,epoll 只返回 10 个。ET(边缘触发)比 LT(水平触发)效率更高——ET 只通知一次,强制应用一次读完所有数据。
📌 易错点 / 加分项:
- ET 模式要求 fd 必须设为非阻塞——否则读操作会一直阻塞直到 EOF
- epoll 的惊群问题——多个 epoll 线程等待同一 fd 时全部被唤醒,Nginx 用 accept_mutex 解决
- Redis 的性能瓶颈在单线程处理命令而非 IO——用 epoll 就够了
9. Linux 内存管理基础
❓ 题目: Linux 的虚拟内存机制是怎样的?Page Fault 是什么?OOM Killer 什么时候会触发?
💡 答案:
虚拟内存让每个进程以为自己独占整个内存空间。进程通过页表将虚拟地址映射到物理地址。MMU 查找页表——如果映射存在直接访问物理内存;如果不存在,触发 Page Fault。
Page Fault 分三种:Minor Page Fault——数据在物理内存中只是页表还没建立,内核建立映射即可(极快)。Major Page Fault——数据不在物理内存中(被换出到 swap),需要从磁盘读回(很慢,毫秒级)。Invalid Page Fault——访问无效地址(如 NULL 指针),内核发送 SIGSEGV 杀死进程。
OOM Killer 的触发条件:系统内存极度紧张,连内核都无法分配内存。触发时扫描所有进程,给每个进程一个 oom_score(基于内存使用量、进程重要性等),选择分数最高的 kill 掉释放内存。/proc/<pid>/oom_score 可查看分数,oom_score_adj 可手动调整——Java 进程建议设为负值降低被杀概率。
📌 易错点 / 加分项:
- 关闭 swap 不等于”不用虚拟内存”——进程仍然使用虚拟地址,只是没有换出能力
- Page Fault 过多导致性能急剧下降——频繁 Major Page Fault 意味内存不够一直在和磁盘倒换
- 容器环境中的 OOM Killer 是 K8s 的 OOMKilled——直接 kill Pod,不是 Linux 的 OOM Killer
10. Nginx 反向代理与负载均衡
❓ 题目: Nginx 作为反向代理和负载均衡器,它的核心配置有哪些?支持哪些负载均衡算法?在高并发下为什么 Nginx 性能这么高?
💡 答案:
Nginx 核心配置:upstream 定义后端集群,proxy_pass 转发请求。SSL 终端在 Nginx 层终结——Nginx 处理 HTTPS 加解密,后端走 HTTP。静态资源由 Nginx 直接返回(root /var/www/html)。缓存(proxy_cache)将后端响应缓存到磁盘。
负载均衡算法:轮询(默认,支持 weight 加权)、IP Hash(同一 IP 始终到同一后端)、最少连接(least_conn)、URL Hash(同 URL 到同后端)。生产最常用加权轮询——不同配置的机器设不同权重。
Nginx 高性能来源:事件驱动架构——Master + 多个 Worker 进程,每个 Worker 单线程通过 epoll 处理数万并发连接,无线程切换开销。非阻塞 IO——所有请求通过 epoll 事件驱动。内存池——自己管理内存减少 malloc/free。模块化设计——核心精简,功能通过模块插入。单机可抗数十万并发连接。
📌 易错点 / 加分项:
- Worker 进程数设为 CPU 核心数——多了反而增加上下文切换
proxy_pass末尾/有讲究——http://backend/和http://backend对 URI 转发行为不同- Nginx 的
keepalive对 upstream 连接池很重要——避免每次代理请求都重新建 TCP 连接 - HTTP/2 的服务端推送并没有被主流浏览器完全启用(因为浏览器缓存的 hit 率等现实问题)
- H3 的普及现状:Google、YouTube、Cloudflare 等已经全面支持,但企业内网还很少