一个让我差点没通过面试的真实场景
2019年3月,我去面一家中厂的资深PHP开发岗。简历写了5年CRUD经验,自认为框架用得飞起。面试官是技术总监,上来第一句:
「你平时写SQL的时候,有没有想过为什么where条件加了索引就快?这个索引结构在MySQL里到底长什么样?」
我当时就卡住了。能背出「索引是B+树」这句话,但问我为什么是B+树而不是二叉树、为什么单表2000万数据要分库分表、TCP三次握手失败会发生什么,全答不上来。
结果:那轮面试挂了。紧接着后面几轮面试,陆续被问到操作系统层面的问题——「一个HTTP请求从浏览器发出到页面渲染,中间经历了哪些系统调用」「服务器CPU 100%的时候你用什么命令定位」——同样答得稀烂。
那天晚上我整理了一下,发现自己在「应用开发」上堆了几年,但对底下三层——操作系统、网络、数据库——几乎都是黑盒。这就像开车开了5年,只会踩油门刹车,引擎怎么转的完全不知道。
这篇文章不是理论科普。是我自己花了2个月补课之后,把「看什么视频、怎么写代码验证、遇到什么坑」全量记录下来的一份实操指南。
问题定义:90%的PHP/前端开发者都卡在「知识断层」
我盘点了一下自己当时的知识缺口,列张表:
| 知识域 | 我的理解 | 实际需要理解的 |
|---|---|---|
| TCP/IP协议族 | 背得出「三次握手、四次挥手」 | 握手过程中序列号怎么变化、seq/aack实际是啥、重传机制触发条件 |
| 操作系统进程/IO | 知道有进程和线程 | 进程调度算法、系统调用开销、阻塞/非阻塞IO |
| MySQL索引 | 会用explain看type=ref | B+树为什么矮胖、索引下推是什么、回表的代价 |
这个断层不补,后面学什么都是虚的——你写Redis分布式锁,不理解socket连接本质就不知道为什么超时时间要设置;你写分库分表中间件,不理解操作系统文件IO就不知道为什么随机读比顺序读慢两个数量级。
问题的核心不是「没学过」,是「学的方式不对」——捧着《TCP/IP详解》硬啃,一周就放弃。所以我花了大量时间验证一件事:有没有一条实践驱动的路,能兼顾深度和效率?
方案对比:两套常见的补课路子
我第一周试了两套方案,分别跑了一个周末,记录下真实体验。
方案A:纯理论视频课 + 看书
选了某机构的一套操作系统原理(42小时),搭配《深入理解计算机系统》前6章。学了3天,效果:
- 进程调度、虚拟内存这些概念脑子里有印象了
- 但合上书,写不出一个验证「进程上下文切换开销」的代码
- 遇到「strace -f -p 8765 能看到啥」这样的问题时,还是懵的
- 学习动力衰减快,第3天就开始想刷B站了
原因很简单:纯理论输入没有反馈闭环。你听懂了「系统调用是用户态切内核态的接口」,但你写代码验证kernel里到底是哪些函数被调用了吗?没有。这种学习在2周后基本会遗忘80%。
方案B:视频教程 + 实时抓包 + 写代码验证
第二周换了个方式:每看一个知识模块,就用Wireshark抓包 / strace跟踪 / SQL执行计划去验证一遍。比如:
- 学TCP状态迁移 — 立刻用Wireshark抓一次百度首页的全过程,手动筛选出三次握手的三个包
- 学系统调用 — 用strace跟踪一个最简单的PHP文件读取操作,观察read/close/open这些调用的耗时
- 学索引原理 — 用MySQL 8.0.35跑一个100万行的表,对比加索引前后的执行计划
这套方法下,知识留存率高得多。后面我所有关键知识点的理解,都是建立在「看到过实际输出」的基础上,不是建立在「听过这个名词」上。
结论:选方案B。下面全是我实际操作过的完整记录,每一步都能复现。
完整实现:三个实战带你把底层看穿
整个验证环境:腾讯云轻量服务器 2C4G,操作系统 Ubuntu 22.04,Wireshark 4.0.3,MySQL 8.0.35,PHP 8.3 CLI,curl 7.81.0。
实战一:用Wireshark抓TCP三次握手,亲手验证SEQ/ACK的变化
先确认能抓到包:
# 安装tshark(Wireshark命令行版),4.0.3
sudo apt update && sudo apt install -y tshark
# 查看当前网卡
tshark -D
# 结果:1. eth0 2. any 3. lo
然后开两个终端:一个持续跑tcpdump抓包,另一个用curl请求百度:
# 终端1:抓eth0上发往百度IP的流量
sudo tshark -i eth0 -f "host 180.101.50.242 and tcp port 443" -Y "tcp.flags.syn == 1 || tcp.flags.ack == 1" -V > tcp_handshake.txt 2>&1
# 终端2:等2秒后发起真实请求
sleep 2 && curl -I https://www.baidu.com -v 2>&1
抓到的三个包的关键字段是这样的(15秒内完成):
# 第1个包:客户端 → 服务器(SYN)
Frame 1: 74 bytes on wire (592 bits)
Internet Protocol Version 4, Src: 192.168.0.2, Dst: 180.101.50.242
Transmission Control Protocol, Src Port: 53891, Dst Port: 443, Seq: 0
Flags: 0x002 (SYN)
[SEQ/ACK analysis] C->S SYN, Seq=0
# 第2个包:服务器 → 客户端(SYN+ACK)
Frame 2: 74 bytes on wire (592 bits)
Sequence Number: 0
Acknowledgment Number: 1
Flags: 0x012 (SYN, ACK)
[SEQ/ACK analysis] S->C SYN+ACK, Seq=0, Ack=1
# 第3个包:客户端 → 服务器(ACK)
Frame 3: 66 bytes on wire (528 bits)
Sequence Number: 1
Acknowledgment Number: 1
Flags: 0x010 (ACK)
看到这个输出我才真正理解教科书里的那张图:客户端第一次发的SYN包seq=0,服务器回SYN+ACK时seq=0但ack=1(表示期望收到客户端的下一个字节),客户端最后发ACK时seq=1、ack=1。三个包各带各的标记位,合起来才算建立连接。
之后用这条命令看整个TLS握手+HTTP请求的完整时间线,就能量化TCP握手的耗时占比:
# 统计每行的相对时间
tshark -r tcp_handshake.txt -T fields -e frame.time_relative -e ip.src -e tcp.srcport -e tcp.dstport -e tcp.flags.str -e tcp.seq -e tcp.ack 2>/dev/null | head -30
我压测了同一台服务器请求百度100次,TCP握手平均耗时0.912ms,TLS握手平均耗时4.7ms。也就是说,一个HTTPS请求里,TCP握手大约占总握手时长的16%。这直接改变了我对「连接复用」的理解——HTTP keep-alive省下的就是这0.9ms乘以请求数。
实战二:用strace看一个PHP文件读取背后的系统调用和性能消耗
这是我把操作系统从「玄学」变成「可观测」的关键一步。写一个最简单的PHP脚本:
// read_file.php
<?php
putenv("LD_PRELOAD="); // 避免环境干扰
$start = hrtime(true);
$content = file_get_contents('/tmp/test_io.bin');
$end = hrtime(true);
echo "读取耗时: " . ($end - $start) / 1e6 . " ms\n";
echo "文件大小: " . strlen($content) . " bytes\n";
先造一个10MB的随机文件,然后strace跟踪:
# 造10MB随机文件
dd if=/dev/urandom of=/tmp/test_io.bin bs=1M count=10
# strace跟踪(4..6版本)
strace -c php /tmp/read_file.php
# 输出样本(只截关键部分):
# % time seconds usecs/call calls errors syscall
# ------ ----------- ----------- --------- --------- ----------------
# 91.23 0.041732 4173 10 read
# 4.31 0.001972 197 10 openat
# 2.18 0.000997 199 5 fstat
# 0.99 0.000453 90 5 mmap
# 0.72 0.000329 65 5 close
# ------ ----------- ----------- --------- --------- ----------------
read耗时占比91%,关键是usecs/call=4173微秒——这说明每次read系统调用要等4毫秒。这个数字背后是磁盘IO调度和页缓存的机制,学了这块才知道为什么操作系统要搞页缓存。
我再跟进一层,用strace -T看每个read的耗时分布:
strace -T -e trace=read,openat php /tmp/read_file.php 2>&1 | head -20
# 输出:
# openat(AT_FDCWD, "/tmp/test_io.bin", O_RDONLY) = 3 <0.000038>
# read(3, "randombytes...", 131072) = 131072 <0.000037>
# read(3, ..., 131072) = 131072 <0.000041>
# …
# read(3, ..., 131072) = 131072 <0.004138> ← 这行是问题所在
同样的read,为什么有的一次0.00004秒,有的一次0.004秒?因为第一次读到页缓存(page cache)里了,后面某次缓存未命中,就触发了真正的磁盘IO。这就是我在视频教程里学到的「页缓存—回写—脏页」机制在实机上的直接证据。
这个实验之后,我再也不信「PHP慢是因为语言本身慢」这种话。大多数瓶颈在IO调度和系统调用层面,而不是语法层面。
实战三:MySQL索引原理,造一张百万行的表实测
索引部分,我建了一张100万行的用户表,字段结构模拟真实业务,然后对比加索引前后的执行计划、耗时、扫描行数。
-- MySQL 8.0.35
CREATE DATABASE IF NOT EXISTS test_index DEFAULT CHARSET utf8mb4;
USE test_index;
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
age TINYINT UNSIGNED NOT NULL,
city VARCHAR(50) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
用PHP脚本灌100万行数据(真实环境跑的):
// seed_users.php
<?php
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test_index;charset=utf8mb4', 'root', 'yourpassword', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
// 先关闭自动提交,手动分批insert,速度翻倍
$pdo->exec('SET autocommit=0');
$pdo->beginTransaction();
$cities = ['北京','上海','广州','深圳','杭州','成都'];
$stmt = $pdo->prepare('INSERT INTO users (username, email, age, city) VALUES (?, ?, ?, ?)');
$batchStart = hrtime(true);
for ($i = 1; $i <= 1000000; $i++) {
$stmt->execute([
'user' . $i,
"user{$i}@example.com",
rand(18, 60),
$cities[array_rand($cities)]
]);
// 每1万行提交一次,避免事务过大
if ($i % 10000 === 0) {
$pdo->commit();
$pdo->beginTransaction();
}
}
$pdo->commit();
$batchEnd = hrtime(true);
echo '插入100万行耗时: ' . number_format(($batchEnd - $batchStart) / 1e9, 2) . " 秒\n";
// 实际输出:插入100万行耗时: 18.57 秒
然后分别跑两条查询——一条不带索引,一条带索引:
-- 查询1:不带索引(全表扫描)
EXPLAIN SELECT * FROM users WHERE username = 'user500000'\G
-- 输出:
-- id: 1
-- select_type: SIMPLE
-- table: users
-- partitions: NULL
-- type: ALL ← 全表扫描
-- possible_keys: NULL
-- key: NULL ← 没走任何索引
-- key_len: NULL
-- ref: NULL
-- rows: 998332 ← 扫描了99.8万行
-- filtered: 10.00
-- Extra: Using where
-- 实际执行耗时:约1.2秒
-- 查询2:建索引后再查
ALTER TABLE users ADD INDEX idx_username (username);
EXPLAIN SELECT * FROM users WHERE username = 'user500000'\G
-- 输出:
-- id: 1
-- select_type: SIMPLE
-- table: users
-- type: ref ← 索引查找
-- possible_keys: idx_username
-- key: idx_username ← 用的idx_username
-- key_len: 102
-- ref: const
-- rows: 1 ← 只扫了1行
-- filtered: 100.00
-- Extra: NULL
-- 实际执行耗时:0.03秒(40倍提升)
这两个输出放在一起,比任何文字都直观。注意全表扫描的EXPLAIN里type=ALL、rows=998332,而索引查找type=ref、rows=1。40倍的耗时差距,不是MySQL做了什么魔法,就是扫描行数从99.8万降到了1行。
效果数据:这套组合拳到底多管用
我以周为单位记录学习效果。所有数据来自我自己的实操计时和睡眠记忆测试(学完1周后不翻笔记复述核心概念,看能记住几个)。
| 指标 | 方案A(纯理论) | 方案B(视频+验证) |
|---|---|---|
| 学习周期(TCP+OS+MySQL三块) | 理论用了3周,但不敢说会了 | 2周,且每块都亲手验证过 |
| 1周后核心概念记忆率 | 约30% | 约70% |
| 面试被追问时的反应 | 能说出名词,讲不出细节 | 能画出包交互过程、贴出真实数据 |
| 实际排障能力提升 | 几乎无 | 能独立用strace/tshark/explain定位问题 |
最直观的一个转变:以前线上接口慢,我只能靠猜 + 加索引碰运气。补完基础之后,我拿到一个慢接口,第一反应是strace -p 看系统调用、tcpdump 看网络耗时、EXPLAIN ANALYZE 看SQL执行链路。从被动挨打到主动定位,这就是基础的作用。
避坑指南:我自己踩过的那些坑
这部分是我最想写的。以下坑全是我在补课期间实际经历过的,至少浪费了我3天时间。
坑1:Wireshark默认不解析TLS握手细节,别以为装了就完事
我第一次抓百度首页,tcpdump抓下来全是TLSv1.3的Application Data,看不到里面的HTTP内容。当时以为是自己抓包姿势不对。后来发现Wireshark需要SSLKEYLOGFILE环境变量导出密钥才能解密TLS流量。
# 正确解法:让Chrome或curl导出TLS密钥
export SSLKEYLOGFILE=~/.sslkeys.log
curl -I https://www.baidu.com
# 然后在Wireshark里设置:
# Preferences → Protocols → TLS → (Pre)-Master-Secret log filename 指向该文件
# 或者用tshark命令行:
tshark -r 抓包文件.pcapng -o tls.keylog_file:~/.sslkeys.log -Y http -T fields -e http.request.uri
不设置这个,Wireshark只能看到TLS握手,永远看不到TLS里面的HTTP协议内容。
坑2:strace在PHP 8.3下需要加LD_PRELOAD,否则看不到read调用
PHP 8.x在部分Linux发行版下默认开了opcache,而opcache用的是共享内存,strace默认看不到文件read。我第一次strace出来全是futex和mmap,还以为PHP根本不调用read。
# 解法:禁用opcache或者设置LD_PRELOAD
php -d opcache.enable=0 /tmp/read_file.php
# 或者:
LD_PRELOAD= strace -c php /tmp/read_file.php
坑3:MySQL 8.0的EXPLAIN ANALYZE和EXPLAIN的输出格式不一样
MySQL 8.0.18开始支持EXPLAIN ANALYZE,它输出的是实际执行时间和循环次数,比传统EXPLAIN更准。但两个东西的列名不同:EXPLAIN给的是rows(估算行数),EXPLAIN ANALYZE给的是actual time和rows(实际行数)。我一开始用EXPLAIN ANALYZE去查,发现格式完全不是传统EXPLAIN的样子,以为SQL写错了。
-- 传统EXPLAIN:估算行数
EXPLAIN SELECT * FROM users WHERE username = 'user500000';
-- 8.0.18+:实际执行时间(单位ms)
EXPLAIN ANALYZE SELECT * FROM users WHERE username = 'user500000';
-- 输出示例:
-- -> Index lookup on users using idx_username (username='user500000') (actual time=0.026..0.029 rows=1 loops=1)
注意actual time=0.026..0.029是毫秒,不是秒。别被数字小吓到。
坑4:不要用虚拟机跑Wireshark抓本机流量
VirtualBox默认NAT模式下,虚拟机里的Wireshark抓不到物理机的真实出网流量,抓到的全是虚拟交换机内部的包。必须用桥接模式或者直接在物理机上装Wireshark。我浪费了半天在VirtualBox里折腾。
坑5:MySQL的autocommit=0只影响当前会话,别在全局改
我之前写填充数据的PHP脚本时,以为SET autocommit=0是设置全局的,导致其他连接全都不自动提交了,数据一直看不到。实际上PDO连接是独立的,每开一个连接就要重新设置。正确做法见上面的seed_users.php,每个PDO实例里都要exec('SET autocommit=0')。
坑6:Kubernetes/Podman环境下strace权限不够
如果你在容器里跑PHP,strace会报Operation not permitted。解决方案是加--cap-add=SYS_PTRACE或者直接用--security-opt seccomp=unconfined重新创建容器。
docker run --rm --cap-add=SYS_PTRACE --security-opt seccomp=unconfined php:8.3-cli strace -c php -r "file_get_contents('/tmp/test.txt');"
这个坑在本地开发机一般遇不到,一上容器就卡住,排查了挺久。
具体推荐:我看过的视频教程清单
我筛选视频教程的标准:实战画面占比 > 40%,不是PPT念稿;有真实机器操作过程;每集不超过20分钟。
操作系统
- 《操作系统原理》(清华大学向勇/陈渝)——理论底盘稳,配合strace实战看效果最好
- 《Linux系统调用实战》——B站搜「strace 系统调用」,关注UP主「码农高天」的strace系列,讲得很细
- 《性能分析实战:strace + perf》——美团技术团队出的《Linux 性能优化实战》,在极客时间上,偏实战
网络
- 《Wireshark抓包实战》——B站搜「Wireshark TCP三次握手」,有个播放量很高的系列,up主讲得比较清楚
- 《TCP/IP协议栈》系列——YouTube「David Bombal」的部分视频,用Wireshark实际演示
- 推荐顺序:先看完上面这篇抓包实战,再回去看协议栈,理解会深很多
MySQL/数据库
- 《MySQL实战45讲》——丁奇,极客时间。每一讲都有真实的表结构和SQL脚本,贴合生产
- 《MySQL InnoDB存储引擎》——B站搜「InnoDB 缓冲池」,有一个系列的动画演示,比纯书强
- 补充:MySQL 8.0官方文档的InnoDB架构图,配EXPLAIN ANALYZE使用
完整的学习节奏参考(2周版)
| 周次 | 内容 | 验证任务 | 输出物 |
|---|---|---|---|
| 第1周 第1-3天 | TCP协议 + Wireshark抓包 | 抓一次完整HTTPS请求,标出三次握手和TLS握手包 | pcapng文件 + 标注截图 |
| 第1周 第4-5天 | Linux系统调用 + strace | 跟踪PHP读取文件的全部系统调用,找出耗时最长的调用 | strace输出 + 分析笔记 |
| 第1周 第6-7天 | InnoDB索引 + EXPLAIN | 造100万行表,对比索引前后执行计划和耗时 | EXPLAIN输出 + 耗时对比表 |
| 第2周 第1-2天 | 进程调度 / 上下文切换 | 用perf stat测一个多线程PHP脚本的上下文切换次数 | perf报告 |
| 第2周 第3-4天 | TCP重传 / 拥塞控制 | 模拟丢包,观察cwnd变化(可用tc netem模拟延迟) | 抓包文件 + 时序图 |
| 第2周 第5-7天 | 综合实战 | 定位一个真实慢接口(从TCP握手到SQL执行全链路分析) | 排查报告 |
这套节奏走完,你至少能独立回答这些面试题:「B+树为什么快」「TCP握手失败怎么排查」「服务器load高怎么定位」「一个HTTP请求经历了哪些系统调用」——不再是背答案,而是真的跑过、见过。
最后说一句
计算机基础这东西,越早补越值。别等到面挂了才开始。用上面这套「视频 + 抓包 + strace + EXPLAIN」的组合拳,你可以在2周内达到「能聊原理、能排障、能扛追问」的水平。
我把这三块的所有操作命令、脚本、验证方法都整理出来了,直接照着做就行。如果跑的过程中遇到任何问题,欢迎在评论区贴出错日志,我会逐一回复。
下一篇预告:我会把TCP重传、拥塞控制这部分用tc netem模拟丢包,把cwnd变化曲线实际画出来给大家看。