成为全栈·基础补充·Linux 服务器入门

成为全栈·基础补充·Linux 服务器入门

我部署这个项目的第一次尝试,失败在一个我完全没想到的地方:

toml 复制代码
# 注意:即便生产用 STORAGE_DRIVER=r2、local 驱动永不被实例化,
# 顶层 node:fs 导入仍会求值,故本 flag 不可省。
compatibility_flags = ["nodejs_compat"]

这是我自己在 wrangler.toml 里写的注释 ,而它记录的是一个真实踩过的坑:Cloudflare Workers 不支持 node:* 的顶层导入 ,即使那段代码的分支永远不会执行。

而这件事的教训不是"Workers 有个限制"------而是我的代码结构让一个永不执行的分支产生了副作用。

这篇讲服务器上那些"本地没事、线上出事"的问题,以及判断它们的方法。

服务器和本地最大的不同:文件系统

先讲一条最容易踩、也最难查的差异。

你的开发机有一个可写的文件系统,服务器不一定有。

而这个项目里有一处直接相关的代码:

ts 复制代码
/** 可重建缓存的容量预算;本地投稿草稿不在这些清理范围内。 */
static const dataDiskBytes = 30 * mib;

这个缓存要写盘------在本地 SQLite 上是毫无问题的(就是一个文件),而在线上:

环境 数据库 写盘
本地 SQLite 文件 ✅
D1 Cloudflare 托管 不存在这个概念
Workers 本地盘 只读 ❌ 失败

而 wrangler.toml 里那行配置就是在处理这个:

toml 复制代码
STORAGE_DRIVER = "r2"          # 生产必须 r2(local 写盘在 CF 只读文件系统会失败)

"local 写盘在 CF 只读文件系统会失败" ------这行注释记录的就是那个现象:代码按预期走了 local 驱动,然后写盘失败了。

而失败的表现很糟糕:它不会在启动时报错 ,因为那段代码在启动时没执行;而是在第一次写缓存时才失败------那时候可能已经是运行几小时之后。

所以这条规则是:配置项不能只按"当前会不会用到"来决定值,要按"万一被用到时会怎样"来决定。

而 M4-27 讲的那个 --dart-define 默认值也是同一类判断:默认值要让"产物能用",而不是让错误早暴露。

环境变量的三个来源

服务器上的配置来自三个地方,而它们的优先级和生效时机都可能不一致:

bash 复制代码
# 1. 环境变量
export NODE_ENV=production

# 2. 配置文件
wrangler.toml 里的 [vars]

# 3. 加密 secret(不在配置文件里)
wrangler secret put JWT_SECRET

而这个项目的组织方式很清晰:

toml 复制代码
[vars]
NODE_ENV = "production"
DB_FILE = ":memory:"            # CF 下无意义(走 D1 binding),保留无害
STORAGE_DRIVER = "r2"          # 生产必须 r2
CORS_ORIGINS = "https://www.example.com,https://manage.example.com"

# JWT_SECRET 为敏感项,走加密 secret(不进 git / 不写本文件):
#   wrangler secret put JWT_SECRET

三个设计点:

一、非敏感配置进 [vars],敏感的走 secret。 判断标准是"会不会进 git"------JWT_SECRET 进 git 就等于公开了。

二、DB_FILE = ":memory:" 保留了一个"无意义的值"。

注释写得很坦率:"CF 下无意义(走 D1 binding),保留无害。"

而这个做法是有价值的:它让本地和线上用同一份配置 schema (B-19 讲的那个 zod schema),而 schema 里这个字段有默认值 (z.string().default(':memory:'))。

如果不保留这个键,本地和线上的配置结构就不一样了------而那种差异会在某个时刻变成一个 bug。

三、CORS_ORIGINS 明确列出了两个真实域名。

而注释里有一句:

toml 复制代码
# CORS:填前端实际域名(逗号分隔);留空或 '*' 将被拒跨域(审阅 B05)

"留空或 '*' 将被拒" ------这说明这个项目的 CORS 配置在代码层面就拒绝通配符,而不是"允许但不推荐"。

这是一个值得学的做法:把"不安全但常见"的做法直接禁掉,代价是配起来麻烦一点,收益是不需要每次 review 都检查 CORS。

权限:文件权限位那三个数字

服务器上最常遇到的问题是"权限不足",而它的表现往往不是清晰的报错:

bash 复制代码
-rwxr-xr-x  1 user  staff  5223 Aug  8 10:23 run_android.sh
位 含义
rwx 所有者:读、写、执行
r-x 同组:读、执行
r-x 其他人:读、执行

而三个常见问题对应三个不同原因:

症状 原因 解法
Permission denied 文件没有执行位 chmod +x script.sh
目录不可写 目录权限 chmod 755 dir
端口被占用 需要特权端口 sudo 或改端口

而"改了权限还是不行"是一个常见状态------因为浏览器缓存了文件权限。

而在这个项目里 run_android.sh 是个真实的例子:它是可执行的(-rwxr-xr-x),所以可以直接 ./tool/run_android.sh 而不是 bash tool/run_android.sh。

而它开头那行 shebang 就是在解释这个文件该用哪个解释器跑:

bash 复制代码
#!/usr/bin/env bash

没有 shebang 的 shell 脚本,直接执行时会用默认 shell(可能是 sh),而 sh 不支持某些 bash 语法 ------症状是"在我这能跑,在你那报错"。

进程和网络:两个必须会查的东西

部署之后最常问的两个问题是"它跑起来了吗"和"它能被别人访问吗"。而它们对应两套命令。

进程:

bash 复制代码
ps aux | grep node          # 有没有进程
lsof -i :3000               # 谁占了这个端口
kill -9 <pid>               # 干掉它(-9 是强杀,先试 kill)

而这个项目里 run_android.sh 用了 exec:

bash 复制代码
exec flutter run ...

就是让 flutter 替换掉 shell 进程 ------这样 Ctrl-C 直接传给 flutter,而不是留下一个孤儿进程。

少了 exec 的症状:终端回来了,但 flutter 还在跑、还占着设备。

网络:

bash 复制代码
curl -sS https://api.example.com/api/v1/articles | jq '.code'

而这一条命令能区分三类问题(B-15 讲的那张排查表):

curl 结果 问题在哪
连不上 网络 / 防火墙 / 服务没起
502 服务没起,但代理在
返回 JSON 但 code != 0 服务正常,是业务错误

而 curl 是这个专栏里最有用的一个命令 ------因为它能在不打开浏览器的前提下确认"服务到底通不通"。

而 B-19 讲的那个微信登录排查,本质上就是一次"用无效 code 做 curl 探测" ------一次廉价的请求把排查范围砍掉大半。

文件传输和同步

服务器上改代码的两个方式,各有代价。

方式一:在服务器上直接改。 快,但没有历史、容易手滑 。而这个专栏的规矩是"用户自己管 commit"(M0-06 讲过),所以服务器上的改动最终要回到本地仓库------那时候很容易发现改错了地方。

方式二:本地改、推送、服务器拉取。

bash 复制代码
# 服务器上
git pull
npm ci          # 注意是 ci 不是 install
systemctl restart <service>

而这里有个容易出错的命令区别:

命令 行为 什么时候用
npm install 更新 package-lock.json 加了新依赖
npm ci 完全按 lock 文件安装 部署时

npm ci 是部署的正确选择 ,因为它保证"服务器上装的版本和 lock 文件里完全一致"------而 npm install 可能因为 lock 文件的约束不一致而装出不同的版本。

这个差异在开发时看不出来(你的 lock 是最新的),只在服务器上暴露。

一条我一开始想省掉的事

那个 nodejs_compat 的坑,我第一次遇到时的判断是"那就别在顶层导入 node:fs"。

于是我把 import fs from 'node:fs' 挪进了一个函数里------只在 local 驱动真正用到时才 import。

结果代码变丑了,而且没解决问题。

因为注释里写的那句话是关键:

即便生产用 STORAGE_DRIVER=r2、local 驱动永不被实例化,顶层 node:fs 导入仍会求值,故本 flag 不可省。

而我挪进函数里之后,顶层导入确实没有了------所以那个坑其实是被我"绕过"了。

但这带来了新问题 :我为了绕过一个环境限制,把本来清晰的模块结构拆成了"动态导入的样子",而收益只是让部署通过了。

所以我后来改回来了 :保留顶层导入 + 开 nodejs_compat。

理由是:为了绕过环境限制而扭曲代码结构,代价会一直留在代码里------下一个人看到那个奇怪的动态导入,会以为是某种性能优化。

而这个判断标准我认为可以推广:

如果绕过限制的做法会让代码变得不自然,说明你在绕过的是症状而不是原因。

nodejs_compat 那个 flag 就是原因(Workers 需要显式声明支持 node 内置模块);而"顶层导入即使不执行也会求值"是它的机制。理解了机制,就知道 flag 是正解而不是 workaround。

小结

服务器这一章,沉淀下来的是四件事:

  1. 文件系统可能是只读的 ------ 配置项要按"万一被用到会怎样"来决定,不只看"当前用不用"。
  2. 配置分三种来源 (env / vars / secret),而本地和线上应该用同一份 schema,即使某些键在某个环境里无意义。
  3. npm ci 而不是 npm install ------ 部署必须严格按 lock 文件。
  4. curl 是最重要的诊断工具 ------ 它能区分"网络不通""服务没起""业务报错"三类问题。

而这一章真正的判断标准是第 1 条背后那个:

本地能跑不能证明线上能跑,因为它们的文件系统、CPU 时间、指令集都可能不同。

这个专栏有三处真实的例子:

问题 本地 线上
文件写盘 ✅ ❌ 只读文件系统
CPU 耗时 无限制 Workers 有硬配额
fetch 选项 error 可用 不被支持

三个问题的共同点是:本地测试全部通过,而它们只在部署后出现。

所以这个专栏现在的做法是(B-17 记的那笔债):

CI 里必须有一条针对目标运行时的最小执行检查 ------ 因为"本地全绿"这件事本身提供不了保证。

下一篇讲 Docker。它解决的是另一个问题:把"我机器上能跑"变成"任何机器上都能跑"------而这个专栏没用 Docker,因为它的部署目标是 Cloudflare Workers(不是容器平台)。

延伸阅读


如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」:

🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html

📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer

相关推荐
爱和冰阔落35 分钟前
【Linux网络】TCP 服务器怎么写?从 listen、accept 到 read/write,手写完整 Echo Server
linux·运维·服务器·网络
鲁邦通物联网1 小时前
工业装备OEM远程服务架构:边缘计算网关与售后运维系统设计
运维·边缘计算·数据采集·工业数据采集·边缘计算网关·plc数采·厂内数采
冯小鸟的探戈1 小时前
电脑重生之安装xp系统
运维·服务器·电脑
分布式存储与RustFS1 小时前
Longhorn 的卷备份别堆本地盘:用 RustFS 给它当 S3 备份目标
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
全栈陈序员3 小时前
【Git】从入门到精通(十)—— 撤销与恢复:git checkout与git restore
运维·网络·数据库·git
瑞克-桑切斯10 小时前
7、MySQL 慢查询排查与优化实战指南
java·服务器·数据库
vibecode12 小时前
网站服务器失去响应怎么解决?我把连接超时和读超时这两段计时拆开跑了一遍
运维·服务器
工业一体机老司机13 小时前
Docker部署工控机采集服务 Compose编排与开机自启
运维·docker·容器