note

服务器 · 部署 · 运行:底层到底长什么样

起因:2026-06-30 AIOJ 企业站(47.110.12.36)整机卡死,借这次把"远程访问 / 部署 / 运行"的底层机制梳理成一份能复看的笔记。尽量去掉"大楼"这类比喻,讲真身。

2026年6月29日

起因:2026-06-30 AIOJ 企业站(47.110.12.36)整机卡死,借这次把"远程访问 / 部署 / 运行"的底层机制梳理成一份能复看的笔记。尽量去掉"大楼"这类比喻,讲真身。

0. 一句话总纲

一台"服务器" = 一台(通常是虚拟的)电脑,上面跑着几个互相独立的进程,每个进程在一个端口上"听客"。你平时打交道的"数据库""网站""SSH",其实是这台机器上三个不同的服务/进程,各有各的门、各有各的钥匙、各能干不同的事。

1. "一个服务在跑"的真身 = 进程 + 一个监听 socket

没有"端口"这个实体。真实发生的是:

  • 机器上有个进程node 跑你的 Next.js、nginxpostgres …)。
  • 它启动时向内核要一个套接字:socket()bind() 把它绑到某端口(如 :80)→ listen() 告诉内核"我在这个端口收客"。这就是"占用/监听端口"的真身——内核维护一张表:{协议, IP, 端口} → 哪个进程
  • 在机器上 ss -ltnp(旧命令 netstat)能看到这张表:哪个端口、哪个进程 PID 在听。
  • 客户端连进来时:内核完成 TCP 三次握手,把这条连接放进该 socket 的 accept 队列;进程再调用 accept() 把它取出来,然后 read() / write() 收发数据。

→ 记住"握手是内核干的、收发数据是进程干的",第 7 节会用它解释宕机现象。

2. 三扇门:DB / web / SSH 是三个不同服务

端口服务钥匙能干什么
:5432PostgreSQL 数据库进程数据库账号密码(DATABASE_URL只能读写数据
:80web(nginx / Node)无(公开)看页面
:22SSH(sshd)操作系统账号 / 密钥拿到 shell,变成机器里的用户,能动整台机器

关键:数据库密码 ≠ 机器登录权。 那串 DATABASE_URL 只开数据库这扇窗,开不了 SSH。这是"最小权限"的故意设计:让应用/我们恰好拿到数据访问,不该能进去操作整台服务器。

3. 为什么"不进机器"也能写库、也能部署

我们干过的两类活都不需要 SSH 进机器:

  • 写库(改数据 / 清理字段):拿 DATABASE_URL 直连 :5432 的 postgres 进程,人根本没进机器。
  • 部署代码 / 跑数据库迁移git push → 触发 CI/CD 流水线,流水线(有机器登录权的机器人)替我们进机器 build + 重启 + 跑迁移。

→ 所以"迁移自动生效"不是我们进机器跑的,是流水线在机器上跑的。

4. "部署"机械上到底发生了什么(全链)

  1. git push 到远端(GitHub / Codeup)。
  2. 触发 CI/CD 流水线——流水线跑在另一台临时机器上,叫 runner
  3. runner git clone 代码 → npm ci 装依赖 → next build 编译出产物(.next/、静态文件)。
  4. runner 把产物传到目标服务器scp/rsync,或打成 Docker 镜像推到镜像仓库再在服务器 docker pull)。
  5. 在服务器上重启进程:停旧 node、起新 node(让它重新 bind 端口)。数据库迁移(drizzle migrate)通常就在这一步跑
  6. 新进程起来 listen,部署完成。

第 3 步的"runner 有机器登录权"是核心:有个有权限的机器人替我们进了机器,所以我们自己从不用 SSH。

5. 谁保证进程一直活着:进程管理器(supervisor)

node 崩了不能靠人盯。机器上有进程管理器负责开机自启、崩溃自动拉起、收日志:

  • systemd(Linux 自带,最常见):写个 unit 文件描述服务怎么启动;systemctl status/restart 服务名 管理,journalctl -u 服务名 看日志。
  • pm2(Node 生态)、Docker / 容器(把进程+依赖+环境打包,docker run 起、docker logs 看日志、restart: always 崩了自动拉)。

→ "重启 web 服务" = systemctl restart / pm2 restart / docker restart 之一。得先知道这台机用哪个,才知道怎么重启——这也是为什么必须进机器看。

6. "机器"是一层层叠上去的

从下到上:

  • 物理服务器(机房里一台真铁:CPU / 内存 / 网卡 / 硬盘)。
  • Hypervisor(虚拟化层,如 KVM):把一台物理机切成很多台虚拟机。它在 OS 之外——所以能不管 OS 死活地画虚拟屏、断电重启(带外控制就住这)。
  • 你的 ECS = 一台 Guest OS(Linux):以为自己是独立电脑,其实跑在 hypervisor 给的虚拟硬件上。
  • OS 里再跑:进程管理器 → 你的进程。
  • 你的代码,只是这台 OS 上的几个文件 + 一个被拉起来的进程。

7. 带内(in-band)vs 带外(out-of-band)—— 远程救援的核心

  • SSH 是带内:sshd 只是机器里的一个普通进程,靠机器的 CPU/内存/磁盘活着。机器整体卡死,SSH 跟着死(连版本号 banner 都发不出)。它和故障"同生死",所以靠不住。
  • 带外 = 不依赖那台机器本身的控制路径
    • :阿里云控制台 →【管理终端 / VNC】(hypervisor 画的虚拟屏,绕开 sshd 和网络,能看到登录界面)+【强制重启实例】(hypervisor 给虚拟机断电再上电,机器不配合也行)。
    • 物理机IPMI / BMC(戴尔 iDRAC、惠普 iLO)= 主板上一块独立小芯片 + 独立网口 + 独立供电,主 OS 死了甚至关机它都还活着,能远程看屏、按电源键。
  • 铁律:严肃基础设施一定留一条与主系统不同生死的带外路径。
  • 但带外锁在云账号后面(不是网络上某个端口,那样太危险),只有机器主人能用。

8. "机器卡死"在底层是什么样(2026-06-30 实测)

现象:ping 通、:22/:80/:5432 端口 TCP 都能握手,但 SSH 连 banner 都发不出、HTTP 进去 0 字节超时。底层机制:

  • 三次握手是内核干的,在进程 accept() 之前就完成 → 所以 nc 显示端口 succeeded,哪怕进程一个字节都处理不了。
  • 进程要真正服务你得 accept() + read()/write()。机器若:
    • 磁盘满:进程一 write()(写日志/临时文件)就返回 ENOSPC 或卡住;sshd 启动要读写文件 → banner 发不出
    • 内存爆(OOM):内核 OOM killer 杀进程(dmesgOut of memory: Killed process …),活着的也卡在内存回收。
    • I/O 卡 / 负载爆:进程卡在 D 状态(uninterruptible sleep,等磁盘 I/O 永不返回),top 里 load average 飙到几十几百。
  • 综合:内核还能握手(端口"开着"),但用户态进程拿不到资源去 accept/收发(应用层一片死寂)。这正是 :22 banner 超时 + :80 零字节的真身。
  • 修它必须带外:进去 df -h(磁盘)/ free -m(内存)/ dmesg(OOM)→ 清理或重启 → 而进机器的 SSH 恰被同一故障掐死。

9. 进到机器后的实用命令小抄

  • ss -ltnp:看哪些端口、哪些进程在听。
  • systemctl status/restart 服务journalctl -u 服务 -n 200:看/重启服务、看日志。
  • df -h:看磁盘是否满。free -m:看内存。top / htop:看负载与进程。
  • dmesg | tail:看内核消息(OOM、磁盘错误)。
  • docker ps / docker logs 容器:看容器与日志。

10. 再往下一层:每个抽象层"怎么实现"

想从"知道每层负责什么"跨到"知道每层用什么齿轮实现",有一把总钥匙:

几乎每一层的实现 = ① 一个硬件机制(让下层能强行夺权或强制隔离)+ ② 内核/宿主里的一张状态数据结构 + ③ 在它上面跑的一套算法。 以后看任何一层,就问三句:靠哪个硬件陷阱?状态记在哪张表?上面跑什么算法?

逐层填这三件套:

① 硬件机制② 状态数据结构③ 算法 / 机制
分 CPU(调度)定时器中断周期性夺回控制权(用户程序拦不住)运行队列(Linux CFS:按"已用虚拟运行时间"排序的红黑树)挑用得最少的,做上下文切换(存/挑/恢复寄存器)
分内存(虚拟内存)MMU + 页表基址寄存器(x86 CR3每进程一棵页表(虚拟→物理地址映射)每次访存 MMU 查表翻译;换进程换 CR3;越界访问→缺页陷入内核
用户/内核态(syscall)CPU 特权级 ring(ring3 用户 / ring0 内核)+ syscall 指令陷入系统调用表调用号进寄存器→查表→跑处理函数→返回 ring3。隔离是硬件强制
文件系统硬盘 = 一堆编号的inode(元信息+数据块指针)+ 目录(名→inode 号)+ 超级块/位图按路径逐级查 inode → 读它指的数据块
网络 / TCP网卡中断每 socket 一个 TCP 状态机 + 收/发缓冲区SYN 到→内核自动推进状态机→回 SYN-ACK→塞进 accept 队列(全程不用进程参与)
虚拟化CPU VT-x / AMD-V(guest 模式)+ EPT 二级页表VMCS 等 guest 状态块陷入并模拟:敏感操作→VM-exit→hypervisor 模拟→恢复 guest
容器无新硬件,纯内核功能namespaces(隔离"视野")+ cgroups(配额账本)clone() 带 namespace 标志起进程;写 cgroup 文件封顶资源
进程管理器它自己就是个用户态进程(PID 1)fork()+exec() 起服务,waitpid() 收尸后重启

一句话:每层都是"硬件陷阱 + 一张状态表 + 一套算法"。这就是"怎么实现"的统一形状。

11. 想真正学到能自己写出来:学习路径

读(先建对模型)

  • OSTEP《操作系统导论 / Operating Systems: Three Easy Pieces》(免费在线)——调度 / 虚拟内存 / 并发 / 文件系统,深度刚好,首推
  • CSAPP《深入理解计算机系统》——打通"硬件 ↔ OS"那条缝(ring、虚拟内存、进程、链接)。
  • 网络:Kurose《自顶向下》建概念 + Beej's Guide 学 socket 编程。

做(落实到能自己写出来 —— 这才是"具体实现")

  • MIT 6.1810(原 6.S081)+ xv6:公开课 + 实验,亲手在一个能跑的小 Unix 内核里实现 syscall / 页表 / 调度 / 文件系统。想落到细节,金标准
  • 虚拟化:搜 KVM API hello world / LWN《Using the KVM API》——几百行写个迷你 hypervisor,亲眼看见 VM-exit。
  • 容器:Liz Rice《Build a container in ~100 lines of Go》(GOTO 演讲) + man namespaces(7) / cgroups(7),一个下午手搓一个。
  • TCP:Saminiir《Let's code a TCP/IP stack》——在 TUN 网卡上从零实现握手。

看它活着跑(把抽象对到真实输出)

  • strace 程序(mac 上 dtruss):实时看它在调哪些 syscall(会看到 openat/read/write/socket/accept)。
  • /proc/<pid>/:某进程的内存映射、打开的 fd 全在里面。
  • bpftrace / eBPF:不改代码,探针式观察内核在干嘛。

关于

北京大学信息科学技术学院大一学生. 课程 / 系统 / 界面 / AI 工具. 学习笔记 / 构建日志 / 随想.

© 2026 Peter Tian. All rights reserved.

||

慢慢来,比较快。

Built with Next.js and MDX