面试复习资料 列表全部文章
面试复习资料

面试复习:网络 / 操作系统 / Linux / Git 基础与线上排查(TCP、HTTP、HTTPS、epoll、零拷贝、排查命令)初中高级

后端面试绕不开的底层功课:TCP 三握四挥/拥塞控制/TIME_WAIT、HTTP/1.1/2/3、HTTPS 握手、DNS、epoll 与零拷贝、进程线程协程、Linux 排查命令、Git 与 Docker/K8s 基础,按初中高三层整理,附追问速查表。

更多

我有个亲身经历:面试一个三年经验的同学,Spring、Redis、MySQL 都答得像模像样,我随口问了句"线上服务突然大量请求超时,你登上机器第一条命令敲什么?",他沉默了五秒,说"重启?"。这不怪他,很多人的知识体系是"框架很厚、底层很薄",平时用不到,一到线上出问题就抓瞎。这篇就是给底层补课的——网络、操作系统、Linux 排查、Git、Docker/K8s,每一块都按三层来讲:

  • 🌱 初级(0–3 年):概念与命令,"是什么/怎么用";
  • 🌿 中级(3–5 年):底层机制与常见坑,"为什么/怎么选";
  • 🌳 高级(5 年+,对标后端约 8 年):线上治理、调优与故障定位,"出问题怎么办、怎么量化"。

每个专题给出标准回答要点、追问链、加分项。文中默认值和命令参数我都对照了官方文档(RFC、man page、MDN 等),不确定的版本差异会特别标注,文末参考资料都是真实打开读过的。

一、知识地图

底层知识看着散,其实是一条从"字节怎么到网卡"到"进程怎么读到字节"的链路。先看全景图:

一个请求的旅程:DNS → TCP → TLS → HTTP → 内核 → epoll → 应用线程 → 磁盘/下游 网络协议 DNS 递归/迭代/缓存 TCP 握手/挥手/状态 滑动窗口/拥塞控制 HTTP/1.1 → 2 → 3 TLS 握手/证书 IO 模型 阻塞/非阻塞 select/poll/epoll LT 与 ET Reactor / Netty 零拷贝 sendfile/mmap 操作系统 进程/线程/协程 虚拟内存/页缓存 上下文切换 用户态/内核态 文件描述符与 ulimit 工具与平台 top/ss/iostat/vmstat jstack/jmap/arthas Git 日常与救援 Docker 隔离与限额 K8s 对象与探针 排查思路:先看整体(CPU/内存/IO/网络四大资源)→ 再定位进程 → 再定位线程/连接 → 最后看代码 口诀:USE 方法——每个资源看 使用率(Utilization)、饱和度(Saturation)、错误(Errors)

二、TCP:三次握手、四次挥手、TIME_WAIT

🌱 初级:握手与挥手

标准回答要点:

  • 三次握手:① 客户端发 SYN(带初始序列号 seq=x);② 服务端回 SYN+ACK(seq=y, ack=x+1);③ 客户端回 ACK(ack=y+1)。目的:双方确认彼此的收发能力,并同步初始序列号。RFC 9293 也强调,握手是为了让双方同步各自的初始序列号,防止旧的重复报文造成错误连接。
  • 四次挥手:① 主动方发 FIN;② 被动方回 ACK(此时被动方可能还有数据没发完,处于 CLOSE_WAIT);③ 被动方数据发完后发 FIN;④ 主动方回 ACK,进入 TIME_WAIT,等待 2MSL 后关闭。为什么是四次:TCP 是全双工,每个方向要单独关闭;被动方收到 FIN 后可能还有数据要发,所以 ACK 和 FIN 通常不能合并。

三次握手 四次挥手 客户端 服务端 SYN, seq=x SYN_RCVD SYN+ACK, ack=x+1 ESTABLISHED ACK, ack=y+1 ESTABLISHED 半连接队列(SYN 队列)满 → SYN Flood 风险 主动关闭方 被动关闭方 FIN ACK CLOSE_WAIT FIN ACK TIME_WAIT (2MSL) 大量 CLOSE_WAIT = 应用没关连接;大量 TIME_WAIT = 主动关闭方短连接多

追问链:为什么不是两次握手?——两次无法确认客户端的接收能力,更关键的是无法防止历史重复的 SYN 造成服务端误建连接:网络里滞留的旧 SYN 到达服务端,服务端回 SYN+ACK 并直接建立连接,而客户端并不认可,白白浪费资源。三次握手让客户端可以用 RST 拒绝这种旧连接。

🌿 中级:TIME_WAIT 与 CLOSE_WAIT

  • TIME_WAIT 存在的原因(RFC 9293):① 确保最后一个 ACK 能到达对方——如果 ACK 丢了,对方会重发 FIN,需要有人应答;② 让旧连接的延迟报文在网络中消失,避免干扰复用同一四元组的新连接。持续时间为 2×MSL(RFC 定义 MSL 为 2 分钟,Linux 实现中 TIME_WAIT 固定约 60 秒);
  • 谁会有 TIME_WAIT:主动关闭连接的一方。如果服务端每次响应完主动关闭(HTTP/1.0 短连接、Connection: close),服务端就积累大量 TIME_WAIT;
  • TIME_WAIT 太多的危害:占用四元组(主要是客户端端口范围有限,约 3 万个,耗尽后 connect 报 Cannot assign requested address),内存占用其实很小;
  • 解决方案:首选使用长连接/连接池(治本);客户端可扩大端口范围 net.ipv4.ip_local_port_range;开启 net.ipv4.tcp_tw_reuse=1(仅对主动发起连接的一端有效,需要时间戳);不要用 tcp_tw_recycle(在 NAT 环境下会导致连接失败,Linux 4.12 已移除);
  • CLOSE_WAIT 大量堆积:说明对方发了 FIN,本端应用没有调用 close(),典型是代码里连接/流没关闭(HTTP 客户端响应体未消费、连接池泄漏)。排查:ss -antp | grep CLOSE_WAIT 找到进程,再用 jstack 看线程卡在哪,通常是 bug 而不是内核参数问题。
# 统计各状态连接数,一眼看出是 TIME_WAIT 还是 CLOSE_WAIT 多
ss -ant | awk 'NR>1 {s[$1]++} END {for (k in s) print k, s[k]}'
# 只看 CLOSE_WAIT 并定位进程
ss -antp state close-wait | head

🌳 高级:拥塞控制、可靠传输与线上调优

可靠传输机制:序列号 + 确认、超时重传(RTO 按 RTT 动态估算)、快速重传(收到 3 个重复 ACK 立即重传,不等超时)、滑动窗口流量控制(接收方通告 rwnd)、拥塞控制(发送方维护 cwnd,实际发送窗口 = min(cwnd, rwnd))。

拥塞控制四阶段(RFC 5681):

阶段行为
慢启动cwnd 从较小值开始,每收到一个新 ACK 增加至多 1 个 SMSS,约每 RTT 翻倍(指数增长),直到超过 ssthresh 或发现拥塞
拥塞避免cwnd 超过 ssthresh 后,每 RTT 大约增加 1 个 MSS(线性增长)
快速重传收到 3 个重复 ACK,立即重传丢失的段,不等 RTO 超时
快速恢复重传后不回到慢启动,ssthresh 减半,cwnd 在此基础上继续(避免吞吐骤降);超时(RTO)则回到慢启动,代价更大

现代 Linux 默认拥塞算法是 CUBIC;Google 的 BBR 基于带宽与 RTT 建模而不是丢包,在高延迟、有随机丢包的跨国链路上收益明显(sysctl net.ipv4.tcp_congestion_control 查看)。

线上治理清单:

  1. 半连接/全连接队列溢出:突发连接量大时,服务端 accept 队列(min(backlog, net.core.somaxconn))满了会丢弃握手最后一个 ACK,表现为客户端偶发连接超时。查 ss -lnt(Recv-Q 表示当前队列长度,Send-Q 表示上限)和 netstat -s | grep -i overflow;调大 somaxconn 与应用 backlog(Tomcat 的 acceptCount,Nginx 的 listen ... backlog);
  2. Nagle 与延迟确认:小包场景(RPC、Redis 客户端)建议开启 TCP_NODELAY,避免 Nagle + 延迟 ACK 叠加造成 40ms 抖动;
  3. KeepAlive:默认 7200 秒才探测,对检测死连接太慢,应用层(连接池的心跳、testWhileIdle)比内核参数更可控;NAT/防火墙常会静默丢弃长时间空闲连接,表现为"第一次请求超时",解决方法是心跳间隔小于网络设备空闲超时;
  4. RST 出现的场景:向未监听端口发数据、连接已被对方关闭仍写数据、半连接队列满且 tcp_abort_on_overflow=1、防火墙拦截;线上遇到 Connection reset by peer 要区分"对端主动重置"和"中间设备重置",用 tcpdump 抓包看 RST 是谁发的。
# 抓包定位 RST:只看 8080 端口的 RST 包
tcpdump -i any -nn 'tcp port 8080 and (tcp[tcpflags] & tcp-rst != 0)' -c 20
# 看全连接队列是否溢出(关注 "times the listen queue of a socket overflowed")
netstat -s | grep -i -E "overflow|drop"

三、HTTP 演进与 HTTPS 握手、DNS

🌱 初级:HTTP/1.1、2、3 的区别

特性HTTP/1.1HTTP/2HTTP/3
格式文本二进制帧二进制帧
传输层TCPTCPQUIC(基于 UDP)
连接复用持久连接,但同一连接上请求串行;浏览器对同域开多个(约 6 个)连接单连接多路复用单连接多路复用
头部压缩无HPACKQPACK
队头阻塞应用层有(请求排队)应用层解决,但 TCP 层丢包会阻塞所有流各流独立,丢包只影响对应的流
握手开销TCP 1 RTT + TLS同左QUIC 把传输与加密握手合并,通常 1 RTT,可 0-RTT 恢复

常见追问:HTTP/2 的服务端推送现在为什么被弃用?——实践中难以判断客户端是否已有缓存,容易浪费带宽,主流浏览器已移除支持,取而代之的是 103 Early Hints。

🌿 中级:HTTPS 握手与证书校验

HTTPS = HTTP + TLS。TLS 1.2 完整握手(2 RTT):ClientHello(支持的套件、随机数)→ ServerHello + 证书 + ServerKeyExchange → 客户端验证证书、生成预主密钥并用服务端公钥或 (EC)DHE 参数交换 → 双方用随机数和预主密钥推导会话密钥 → ChangeCipherSpec + Finished。TLS 1.3(RFC 8446):把握手压缩到 1 RTT,ClientHello 里就携带密钥共享参数;ServerHello 之后的握手消息全部加密;移除了静态 RSA 密钥交换,所有密钥交换都具备前向安全;支持 PSK 恢复会话与 0-RTT 早期数据。

  • 0-RTT 的代价:早期数据没有前向安全性,且可能被重放(RFC 8446 明确警告),所以只能用于幂等请求(GET);
  • 为什么用非对称加密交换密钥、对称加密传数据:非对称计算开销大,对称加密快;密钥交换解决的是"如何在不安全信道上协商出共享密钥";
  • 证书校验链:服务端证书 → 中间 CA → 根 CA(预置在系统/浏览器信任库);校验内容包括签名、有效期、域名匹配(SAN)、吊销状态(CRL/OCSP);
  • 中间人攻击为什么无效:攻击者拿不到受信 CA 签发的、域名匹配的证书私钥,伪造证书会被客户端识别;
  • SNI:ClientHello 里的扩展,告诉服务端要访问哪个域名,使同一 IP 能托管多个 HTTPS 站点。

🌳 高级:HTTPS 线上问题治理

  1. 证书过期是最常见的低级线上事故:需要证书到期监控(提前 30/14/7 天告警)、自动续期(ACME/cert-manager)、以及全链路(网关、LB、内部服务、第三方回调域名)的证书清单;
  2. 握手性能:开启会话复用(Session Ticket / TLS 1.3 PSK)、OCSP Stapling(避免客户端单独查询吊销状态)、HTTP keep-alive,减少握手次数;TLS 终止放在网关/LB,内部走内网明文或 mTLS(按安全等级决定);
  3. 排查命令:openssl s_client -connect host:443 -servername host 查看证书链与协议版本;curl -v --http2/--http3 验证协议协商;curl -w 输出各阶段耗时(time_namelookup / time_connect / time_appconnect / time_starttransfer),一眼看出慢在 DNS、TCP、TLS 还是服务端处理;
  4. HTTP/3 落地取舍:需要 UDP 443 放通,部分企业网络会限制 UDP;QUIC 在用户态实现,CPU 开销略高;浏览器会通过 Alt-Svc 头发现并升级,降级到 HTTP/2 是安全网。
curl -o /dev/null -s -w "dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://example.com/

DNS:从输入域名到拿到 IP

🌱 浏览器缓存 → 操作系统缓存/hosts → 本地 DNS 解析器(递归解析)→ 根服务器 → 顶级域(.com)服务器 → 权威服务器 → 返回 IP 并逐级缓存(TTL 控制)。客户端到本地 DNS 是递归查询,本地 DNS 到各级服务器是迭代查询。

🌿 记录类型:A(IPv4)、AAAA(IPv6)、CNAME(别名,不能与其他记录共存于同名)、MX、TXT、NS。DNS 默认走 UDP 53,响应过大或区域传送时转 TCP。🌳 线上要点:① Java 默认的 DNS 缓存(networkaddress.cache.ttl)——有安全管理器时默认永久缓存,无安全管理器时默认 30 秒,云上后端 IP 变更后应用不生效就是此类问题,需要显式设置合理 TTL;② K8s 内 CoreDNS 的 ndots:5 会让外部域名解析多发几次无效查询,造成延迟与 DNS 压力,可用全限定域名(末尾加点)或调整 ndots;③ DNS 故障是"隐形"的:应用超时但机器都正常,用 dig +trace、dig @server 对比不同解析器定位。

四、IO 模型、epoll 与零拷贝

🌱 初级:五种 IO 模型与 select/poll/epoll

Unix 五种 IO 模型:阻塞 IO、非阻塞 IO、IO 多路复用、信号驱动 IO、异步 IO。IO 多路复用:一个线程通过 select/poll/epoll 同时监听大量文件描述符,哪个就绪处理哪个,从而用少量线程服务海量连接。

对比selectpollepoll
fd 数量限制FD_SETSIZE,默认 1024无硬上限(链表/数组)无硬上限(受系统 fd 限制)
每次调用把整个 fd 集合从用户态拷贝到内核同左epoll_ctl 只注册一次,epoll_wait 只返回就绪的
找出就绪 fd遍历全部 O(n)遍历全部 O(n)内核维护就绪链表,O(就绪数)
触发模式水平触发水平触发水平触发(默认)+ 边缘触发

epoll 三个系统调用(man page):epoll_create 创建实例、epoll_ctl 增删改监听的 fd、epoll_wait 阻塞等待就绪事件。

🌿 中级:LT 与 ET、Reactor、Netty

  • 水平触发(LT,默认):只要 fd 上还有数据可读,每次 epoll_wait 都会通知,编程简单;
  • 边缘触发(ET):只在状态变化(新数据到达)时通知一次;没读完就不会再通知,所以必须配合非阻塞 fd,循环读取直到返回 EAGAIN。好处是减少 epoll_wait 唤醒次数;
  • Reactor 模式:事件循环(Selector/epoll)+ 事件分发 + 处理器。单 Reactor 单线程(Redis 6 之前的命令处理)、单 Reactor 多线程(IO 线程读写 + 业务线程池)、主从 Reactor(Netty 的 boss 线程组接收连接,worker 线程组处理读写);
  • Java NIO 与 epoll:Linux 下 Selector 的底层实现就是 epoll;Netty 还提供了原生 EpollEventLoopGroup(支持 ET、更少 GC);
  • 惊群与 JDK epoll 空轮询 bug:早期 JDK 在某些 Linux 版本上 select() 返回 0 却不阻塞导致 CPU 100%,Netty 通过统计空轮询次数并重建 Selector 规避。
// Netty 主从 Reactor 的典型启动:boss 负责 accept,worker 负责读写
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup();   // 默认 CPU 核数 * 2
ServerBootstrap b = new ServerBootstrap()
    .group(boss, worker)
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<SocketChannel>() { /* pipeline: 编解码 + 业务 handler */ });

零拷贝:到底省掉了什么

🌱 传统"读文件再发到网络":磁盘 → 内核页缓存(DMA)→ 用户缓冲区(CPU 拷贝)→ socket 缓冲区(CPU 拷贝)→ 网卡(DMA),共 4 次拷贝、4 次上下文切换。

🌿 sendfile:man page 指出它在内核内部完成 fd 到 fd 的拷贝,比 read + write 少了用户态往返;配合支持 scatter-gather DMA 的网卡,可以进一步省掉页缓存到 socket 缓冲区的 CPU 拷贝,做到"CPU 零拷贝"。mmap + write:把文件映射到用户空间,省一次 CPU 拷贝,但仍有 3 次拷贝。

方式CPU 拷贝次数上下文切换使用场景
read + write24需要在用户态处理数据
mmap + write14需要部分处理、随机读;大文件有缺页风险
sendfile1(有 SG-DMA 时为 0)2静态文件、消息队列消费(Kafka 使用)

🌳 高级:零拷贝不是万能的——数据需要在用户态加密/压缩/修改时用不了 sendfile(TLS 场景需要内核 TLS,kTLS,才能保持零拷贝);Java 里对应 FileChannel.transferTo(底层调用 sendfile)与 MappedByteBuffer;Kafka 高吞吐依赖顺序写 + 页缓存 + sendfile 的组合,RocketMQ 则主要使用 mmap。回答时强调"减少的是 CPU 拷贝和内核/用户态切换,不是 DMA 拷贝",这是区分理解深度的点。

五、进程、线程、协程与内存

🌱 初级:三者区别

对比进程线程协程
资源单位资源分配的基本单位,有独立地址空间CPU 调度的基本单位,共享进程地址空间用户态调度,更轻量
切换开销大(切换页表、TLB 刷新)中(内核态切换,寄存器与栈)小(用户态保存少量寄存器)
通信IPC:管道、消息队列、共享内存、信号、Socket共享内存,需要同步(锁)共享内存,单线程调度下无需锁(但要注意阻塞)
崩溃影响互相隔离一个线程崩溃可能拖垮整个进程同线程
典型Nginx worker、Chrome 标签页Java 线程(JDK 21 之前 1:1 映射内核线程)Go goroutine、Kotlin 协程、Java 21 虚拟线程

🌿 中级:上下文切换、虚拟内存与页缓存

  • 上下文切换类型:自愿(等待 IO、sleep、锁)与非自愿(时间片用完、被更高优先级抢占)。vmstat 1 的 cs 列是每秒切换数,pidstat -w 看每个进程的 cswch/s(自愿)与 nvcswch/s(非自愿);非自愿切换高说明 CPU 抢占激烈,自愿切换高说明在等 IO 或锁;
  • 虚拟内存:每个进程有独立的虚拟地址空间,通过页表映射到物理内存,带来隔离、按需分配(缺页中断)、内存共享与交换(swap);TLB 缓存页表项以加速转换;
  • 页缓存(Page Cache):文件读写经过内核页缓存,free -h 里的 buff/cache 属于可回收缓存,available 才是真实可用内存,不要看到 free 很低就以为内存不够;
  • OOM Killer:物理内存与 swap 耗尽时内核按 oom_score 杀进程,日志在 dmesg -T | grep -i "killed process";容器里则是 cgroup 内存限制触发(退出码 137);
  • Java 线程模型与协程:JDK 21 引入虚拟线程(正式特性),由 JVM 调度到少量平台线程上,遇到阻塞 IO 自动让出载体线程,适合 IO 密集型;不适合 CPU 密集型,也要避免在虚拟线程里长时间持有 synchronized(JDK 21 中会钉住载体线程,后续版本有改进)。

🌳 高级:容量与调优视角

  1. 线程数怎么定:CPU 密集型约 CPU 核数(+1);IO 密集型约 核数 × (1 + 等待时间/计算时间)。更可靠的做法是压测拟合,并监控线程池队列长度与活跃线程数,而不是拍公式;
  2. 文件描述符:一个连接一个 fd,ulimit -n 默认常见是 1024,高并发服务必须调大(并检查 systemd 的 LimitNOFILE);出现 Too many open files 用 ls /proc/PID/fd | wc -l 和 lsof -p PID 看泄漏来源;
  3. NUMA、CPU 亲和性:对延迟极敏感的服务(交易、网关)会绑核、避免跨 NUMA 访问内存;一般业务不需要,但要知道存在这类瓶颈;
  4. Swap 策略:延迟敏感的服务(数据库、缓存)通常把 vm.swappiness 调低甚至关闭 swap,避免不可预测的抖动,代价是 OOM 更直接,需要配合内存监控。

六、Linux 线上排查命令

🌱 初级:先看整体,四大资源

看什么命令关注点
CPUtop / uptime / vmstat 1us 用户态、sy 内核态、wa IO 等待、load average(1/5/15 分钟)
内存free -h / topavailable、swap 使用、进程 RES
磁盘df -h / df -i / iostat -x 1 / du -sh *使用率、inode、%util、await
网络ss -s / ss -antp / sar -n DEV 1连接状态分布、带宽、重传
文件/日志tail -f / grep / less / lsof错误关键字、被删除但未释放的文件

ss(man page)是 netstat 的现代替代:-t 只看 TCP,-l 监听,-n 不解析名字,-p 显示进程,-s 汇总统计,还支持状态过滤如 ss -t state established。

top                                   # 按 P 按 CPU 排序,按 M 按内存排序,按 1 看每核
ss -ltnp                              # 谁在监听哪个端口
ss -s                                 # 各状态连接数汇总(TIME_WAIT 多不多)
iostat -x 1 3                         # 磁盘:%util 接近 100 且 await 高 = IO 瓶颈
df -h && df -i                        # 空间和 inode 都要看,inode 满了同样写不进
free -h                               # 看 available 而不是 free

🌿 中级:从进程到线程到代码(Java)

Java 进程 CPU 高的经典链路:top 找进程 → top -Hp PID 找线程 → printf '%x' TID 转十六进制 → jstack PID | grep -A 30 nid=0x... 看栈。内存与 GC:jstat -gcutil PID 1000 看各代使用率与 GC 次数/耗时;jmap -histo:live PID | head -20 看对象数量排行(live 会触发 Full GC,线上慎用);jmap -dump:live,format=b,file=heap.hprof PID 导出堆,用 MAT 分析。

现象先看可能原因
load 高,CPU 不高vmstat 的 b 列/wa,iostat磁盘 IO 等待、大量线程阻塞、D 状态进程
CPU us 高top -Hp + jstack死循环、正则回溯、序列化、GC 线程
CPU sy 高pidstat -w、strace -c -p上下文切换过多、系统调用频繁、锁竞争
内存持续增长jmap -histo、jcmd GC.class_histogram、NMT缓存无上限、连接泄漏、堆外内存泄漏
接口偶发超时ss -s、GC 日志、下游耗时、DNSFull GC、连接池耗尽、下游抖动
磁盘满但 du 对不上lsof | grep deleted进程仍持有已删除的大日志文件,重启或清空 fd 才释放
top -Hp 8231 -n 1 | head -15
printf '%x\n' 8479                                  # 211f
jstack 8231 | grep -A 25 'nid=0x211f'
jstat -gcutil 8231 1000 5                           # 每秒一次,共 5 次
lsof -p 8231 | wc -l                                # 该进程打开的 fd 数
strace -c -f -p 8231                                # 统计系统调用(线上短时间使用)

🌳 高级:Arthas 与系统化排查方法论

Arthas(阿里开源的 Java 诊断工具)的价值是不重启、不改代码就能在线观察:

  • dashboard:线程、内存、GC 概览;thread -n 3 列出最忙的 3 个线程栈,thread -b 直接找出阻塞其他线程的线程(死锁/锁竞争);
  • trace 类名 方法名 '#cost>100':方法内部调用链耗时,只输出超过 100ms 的调用,快速定位慢在哪一层;
  • watch 类名 方法名 '{params, returnObj, throwExp}' -x 2:观察入参、返回值、异常;-n 5 限制次数避免刷屏;
  • jad 反编译线上类确认部署版本;redefine/retransform 热替换(生产慎用,务必评估风险);profiler start/stop 生成火焰图;
  • 注意:Arthas 本身有开销,trace/watch 用完要 stop 或 reset 还原字节码增强。

方法论——USE 与 RED,先广后深:

  1. 先止血再定位:影响面大时先限流/摘流量/回滚,保留现场(jstack ×3、GC 日志、top 输出、必要时 heap dump),再重启;
  2. 用指标缩小范围:对每种资源看使用率、饱和度、错误数(USE);对服务看请求速率、错误率、耗时(RED);Google SRE 提出的四个黄金信号——延迟、流量、错误、饱和度——也是同一套思路;
  3. 时间线对齐:把"发布、配置变更、流量变化、告警、日志异常"放到同一条时间轴上,多数事故的根因就是最近一次变更;
  4. 复盘固化:把排查命令写进值班手册,把根因转成监控/告警/压测用例,避免下次重复踩坑。

七、Git 常见问题

🌱 初级:日常命令与概念

  • 工作区、暂存区、本地仓库、远程仓库四个区域,git add 工作区→暂存区,git commit 暂存区→本地仓库,git push 本地→远程;
  • git fetch 只拉取不合并;git pull = fetch + merge(或 --rebase);
  • git merge:保留分支历史,产生合并提交;git rebase:把提交"重放"到目标分支之后,线性历史,但会改写提交,已推送到公共分支的提交不要 rebase;
  • git stash 暂存未提交改动,git stash pop 恢复;git cherry-pick <commit> 摘取某次提交到当前分支。

🌿 中级:撤销、回滚、冲突与救援

场景命令说明
撤销工作区改动git restore file(旧:git checkout -- file)丢弃未暂存的修改,不可恢复
取消暂存git restore --staged file保留修改,只移出暂存区
修改最后一次提交git commit --amend会改写提交,已推送的不要用
回退到某提交(本地)git reset --soft/--mixed/--hard <commit>soft 保留暂存区,mixed(默认)保留工作区,hard 全丢弃
回滚已推送的提交git revert <commit>生成一个反向提交,不改写历史,公共分支首选
误 reset/删分支后找回git reflog记录 HEAD/分支指针的移动,默认保留 90 天,可用它找回提交并 git checkout -b recovered
合并冲突手动编辑冲突标记 → git add → git commit/rebase --continue放弃合并 git merge --abort
推送被拒绝git pull --rebase 后再 push不要随手 push --force
# reset --hard 之后后悔了:reflog 找回
git reflog                       # 找到 reset 之前的提交 hash,如 a1b2c3d HEAD@{2}
git checkout -b rescue a1b2c3d   # 基于它建一个新分支,救回来

# 安全强推:只有远程没有别人的新提交才会成功
git push --force-with-lease

🌳 高级:协作规范与仓库治理

  1. 分支策略:Trunk-based(主干开发 + 短分支 + 特性开关)适合持续交付;Git Flow(develop/release/hotfix)适合有版本发布节奏的产品。团队要统一,并配合保护分支、必需的 Code Review 与 CI 检查;
  2. 提交规范与历史整洁:Conventional Commits,合并前 squash 琐碎提交,保证每个提交可构建、可回滚;
  3. 敏感信息泄露:密码/密钥一旦提交进历史,仅删除文件不够。要用 git filter-repo(或 BFG)清理历史、立刻轮换泄露的密钥,并推送重写后的历史;预防用 pre-commit 钩子 + 密钥扫描;
  4. 大文件:不要把二进制大文件塞进仓库,用 Git LFS 或制品库;仓库过大用浅克隆 --depth 与 partial clone;
  5. 定位问题引入点:git bisect 二分查找引入 bug 的提交,配合自动化测试脚本(git bisect run)非常高效;git blame/git log -S 追溯某段代码的来源。

八、Docker 与 Kubernetes 基础概念

🌱 初级:镜像、容器、隔离

  • 镜像(Image):只读模板,分层构建,Dockerfile 改动后只重建变化的层;容器(Container):镜像的运行实例,有自己的文件系统、网络接口和配置,可动态创建、启动、停止、删除;
  • 隔离原理:Docker 官方文档说明它使用 Linux namespaces 提供隔离的工作空间(PID、NET、MNT、UTS、IPC、USER 等);资源限制靠 cgroups(CPU、内存、IO)。容器共享宿主机内核,所以比虚拟机轻,但隔离性弱于虚拟机;
  • 常用命令:docker ps -a、docker logs -f --tail 100、docker exec -it ID sh、docker stats、docker inspect。

🌿 中级:常见坑

  • 容器内 JVM:JDK 8u191+ / 10+ 支持感知 cgroup 限制(-XX:+UseContainerSupport 默认开启),堆大小推荐用 -XX:MaxRAMPercentage 按比例设置,而不是写死 -Xmx 与容器 limit 不匹配,否则超限被 OOMKilled(退出码 137);老版本 JDK 8 早期版本读的是宿主机内存,会算错;
  • PID 1 问题:容器里的主进程是 PID 1,收不到默认信号处理导致 docker stop 等 10 秒才强杀;应用要处理 SIGTERM 做优雅停机,必要时使用 tini;
  • 数据持久化:容器文件系统是临时的,数据用 Volume/Bind Mount;日志输出到 stdout/stderr,由平台收集,不要写容器内文件;
  • 镜像瘦身:多阶段构建、使用精简基础镜像、合并 RUN 层、.dockerignore;
  • 磁盘被占满:docker system df 查看,docker system prune 清理无用资源(注意确认后再执行)。

🌳 高级:K8s 核心对象与治理

Kubernetes 官方对自己的定位是"管理容器化工作负载和服务的开源平台",是声明式的控制系统:你声明期望状态,控制器不断把实际状态调和到期望状态。

对象作用面试要点
Pod最小部署单元,一个或多个共享网络/存储的容器Pod 是短暂的,IP 会变
Deployment管理无状态应用的副本与滚动更新通过 ReplicaSet 实现;maxSurge/maxUnavailable 控制滚动节奏
StatefulSet有状态应用(稳定网络标识、稳定存储)有序部署与伸缩;数据库、ZK 类
DaemonSet每个节点运行一个 Pod日志采集、监控 agent
Service为一组 Pod 提供稳定访问入口与负载均衡ClusterIP/NodePort/LoadBalancer;基于 Label 选择器
Ingress管理外部 HTTP/HTTPS 访问需要 Ingress Controller(如 Nginx)
ConfigMap / Secret配置与敏感信息Secret 默认只是 base64,不是加密,需开启静态加密并控制 RBAC
PV / PVC持久化存储抽象存储与 Pod 生命周期解耦

线上治理要点:

  1. 探针要用对:livenessProbe 失败会重启容器(别把"依赖的下游不健康"放进去,否则下游抖动导致全量重启雪崩);readinessProbe 失败只是摘除流量;startupProbe 用于慢启动应用,避免被 liveness 误杀;
  2. requests 与 limits:requests 决定调度与 QoS,limits 决定被限流/被杀的边界。CPU 超限是被节流(throttle,表现为延迟抖动),内存超限是被 OOMKilled;requests = limits 得到 Guaranteed 级别,最不易被驱逐;
  3. 优雅上下线:preStop 钩子 + terminationGracePeriodSeconds,先让 Service 摘除端点、等待在途请求完成,应用响应 SIGTERM 停止接收新请求;否则滚动发布时出现少量 502;
  4. 排查命令:kubectl get pods -o wide、kubectl describe pod(看 Events:调度失败、镜像拉取失败、探针失败、OOMKilled)、kubectl logs --previous(看上一个崩溃容器的日志)、kubectl top pod、kubectl exec;CrashLoopBackOff 先看 --previous 日志与退出码(137=被杀,143=SIGTERM,1=应用异常);
  5. 发布治理:滚动更新 + 就绪探针 + PodDisruptionBudget + HPA(基于 CPU/自定义指标弹性伸缩),灰度用 Ingress 权重或服务网格。

九、高频追问速查表

问题一句话答案出处/延伸
为什么三次握手确认双方收发能力 + 同步序列号 + 防止历史 SYN 造成错误连接RFC 9293
为什么挥手四次全双工,各方向单独关闭;被动方可能还有数据要发第二节
TIME_WAIT 为什么要等 2MSL确保最后 ACK 到达、让旧报文在网络中消失RFC 9293
大量 CLOSE_WAIT 怎么办应用没 close,查代码里的连接/流泄漏,而不是调内核参数ss -antp
TIME_WAIT 太多怎么办用长连接/连接池;扩端口范围;tcp_tw_reuse;不要 tw_recycle第二节
快速重传触发条件收到 3 个重复 ACK,不等 RTO 立即重传RFC 5681
HTTP/2 还有什么队头阻塞TCP 层丢包会阻塞所有流;HTTP/3 用 QUIC 各流独立解决MDN
TLS 1.3 比 1.2 好在哪1 RTT 握手、握手加密、只保留前向安全的密钥交换、支持 0-RTTRFC 8446
0-RTT 风险可能被重放、无前向安全,只用于幂等请求RFC 8446
epoll 为什么快注册一次、就绪链表返回、无需每次全量拷贝与遍历epoll man page
ET 模式注意什么非阻塞 fd + 循环读到 EAGAIN,否则丢事件epoll man page
零拷贝省了什么省 CPU 拷贝和用户/内核态切换,DMA 拷贝仍在sendfile man page
CPU 高怎么排查top → top -Hp → printf %x → jstack;先分清 us/sy/wa第六节
load 高 CPU 低多半 IO 等待或大量线程阻塞,看 vmstat 与 iostat第六节
reset --hard 后找回提交git reflog 找到 hash,checkout -b 新分支git reflog 文档
公共分支怎么回滚git revert,不改写历史第七节
liveness 与 readiness前者失败重启容器,后者失败摘流量K8s 文档
容器 137 退出码被 SIGKILL,常见是内存超限 OOMKilled第八节

一句话备忘:网络看状态(握手/挥手/TIME_WAIT/CLOSE_WAIT),IO 看多路复用与零拷贝,排查先看四大资源再钻进程、线程、代码;Git 记住 reflog 是后悔药,容器记住"共享内核 + 限额 + 探针"。

底层知识的价值,不在于面试时背得多流利,而在于线上出问题的那个凌晨,你能不能在三分钟内敲对第一条命令。建议把第六节的命令表打印出来贴在显示器边上,我就是这么干的,真的有用。

参考资料

— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人