[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site":3,"post-1-9g-nei-cun-de-fu-wu-qi":59,"related-1-9g-nei-cun-de-fu-wu-qi":93},{"name":4,"url":5,"description":6,"bio":7,"avatar":8,"skills":9,"tools":16,"social":34,"hero":47,"heatmap":52,"icp":55,"police":56,"policeIcon":57,"shortName":58},"时过境迁技术笔记","https:\u002F\u002Fhsgjq.top","一个记录前端与 Node 实践的个人博客，内容围绕 Vue、Nuxt、NestJS 与工程化展开，附源码级的原理笔记。","一名热爱技术的全栈开发者，专注 TypeScript 与 Web 工程化，习惯把踩过的坑和思考记录在这里。","\u002Fuploads\u002Fc485c101-179e-48db-96d1-238693784c38.jpg",[10,11,12,13,14,15],"TypeScript","Vue 3","Nuxt","NestJS","Node.js","工程化",[17,23,28],{"items":18,"title":22},[19,20,21],"Cursor","Git","Chrome DevTools","开发工具",{"items":24,"title":27},[25,26],"Vite","npm","构建工具",{"items":29,"title":33},[30,31,32],"Docker","CI\u002FCD(GitHub Actions)","Vercel","部署运维",[35,39,43],{"href":36,"icon":37,"label":38},"https:\u002F\u002Fgithub.com\u002Fhsgjq","github","GitHub",{"href":40,"icon":41,"label":42},"2971861643@qq.com","mail","邮箱",{"href":44,"icon":45,"label":46},"\u002Frss.xml","rss","RSS",{"lines":48,"subtitle":51},[49,50],"人生若只如初见，","何事秋风悲画扇。","《木兰花·拟古决绝词柬友》",{"enabled":53,"platform":37,"username":54},true,"hsgjq","桂ICP备2024046985号","桂公网安备45011002000103号","\u002Fuploads\u002F72c67ef3-bc86-4b64-b0df-0aba71065050.png","时过境迁",{"slug":60,"title":61,"excerpt":62,"date":63,"updatedAt":64,"tags":65,"content":66,"series":67,"seriesIndex":68,"seriesNav":69,"prev":87,"next":90},"1-9g-nei-cun-de-fu-wu-qi","1.9G 内存的服务器上，nuxt build 在第 36 秒被杀","V8 堆耗尽直接 SIGABRT，日志里只有一句低内存提示。先分清是被系统杀的还是自己崩的，再决定加内存还是加 swap。","2026-08-08","2026-10-19",[30],"一台 2 核 1.9G 的火山服务器，前端和后端都打算在上面构建。跑到大约三十六秒，构建进程消失，日志最后几行：\n\n```\n\u003C--- Last few GCs --->\n[1:0x...]  34812 ms: Mark-Compact 1997.2 -> 1995.1 MB\n\u003C--- JS stacktrace --->\nFATAL ERROR: Ineffective mark-compacts near heap limit\nAllocation failed - JavaScript heap out of memory\n----- Native stack trace -----\n 1: 0xabcdef v8::base::...\n```\n\n退出码 134。128 + 6，也就是 SIGABRT。\n\n这个退出码是第一个关键信息，我一开始的反应完全错了：以为是 Nuxt 构建报错，翻了半天的构建输出找\"错误\"两个字，其实上面那几行才是全部真相。后来我给自己定了条规矩——构建挂了先看退出码，134 是 V8 自己 abort，137 才是被内核 SIGKILL。\n\n第一步是确认是谁下的手。\n\n```bash\ndmesg -T | tail -30 | grep -i \"killed process\"\n```\n\n没有输出。说明内核没参与，是 V8 撞了自己的堆上限。Node 在 1.9G 的机器上默认给 V8 的堆上限不到 2G，而 nuxt build 的峰值要更多，于是先撞软限制。\n\n抬上限：\n\n```dockerfile\nARG NODE_OPTIONS=--max-old-space-size=3072\nENV NODE_OPTIONS=$NODE_OPTIONS\n```\n\n写成 ARG 而不是硬编码，是为了将来 CI 构建时能覆盖，也让这个值在 Dockerfile 里可见、可 review。\n\n再跑，这次不是三十六秒崩，而是越跑越慢，最后整个 SSH 会话都卡住，容器起不来。dmesg 这次有内容了：\n\n```\nOut of memory: Killed process 2143 (node) total-vm:3219456 kB, anon-rss:1802112 kB\n```\n\n内核杀的。原因很清楚：上限抬到 3072，但物理内存只有 1.9G，也没有 swap，进程一路申请到真的没得给，OOM killer 挑了最大的那个下手。\n\n所以 swap 必须一起加：\n\n```bash\nfallocate -l 2G \u002Fswapfile && chmod 600 \u002Fswapfile\nmkswap \u002Fswapfile && swapon \u002Fswapfile\necho '\u002Fswapfile none swap sw 0 0' >> \u002Fetc\u002Ffstab\n```\n\n第二篇 2G 是后来补的（一共 4G），因为第一次跑通之后我又撞了一次。fstab 那行不能漏，漏了重启就没 swap，表现是\"上次能构建这次不行\"。\n\nvm.swappiness 默认 10，意思是内核很不愿意用 swap，宁可回收页缓存、宁可杀进程。构建期间我临时调到 60：\n\n```bash\nsysctl vm.swappiness=60      # 构建完调回 10\n```\n\n调完的结果：构建能跑完了，耗时从预期的四十秒变成三分多钟。这个交换我认了，因为失败的代价是重跑，重跑也是几分钟，而且失败还不出产物。\n\n三个数字要记下来，将来换机器直接照着判断：\n\n- 1792 不够，还在三十六秒那个位置崩；\n- 3072 能过，靠 4G swap 兜住；\n- 峰值 anon-rss 大约 1.8G，也就是说这台机器光是构建就要把内存+swap 用到接近极限，同时跑着 MySQL 的话不能让它俩撞在一起。\n\n最后我做的其实是对的决定：把构建挪到 CI，服务器只 pull 镜像。这条链路通了之后，swap 和堆上限这些都不再是发布流程的一部分——但我没把 swap 删掉，因为运行时的 Node 进程也需要一点缓冲空间。\n\n有一件事我到现在也没解决，写在这里免得别人踩：Dockerfile 里如果用了带 `$` 锚点的 sed 去改文件，而构建上下文是从 Windows 打包上传的（CRLF 行尾），那条 sed 会静默不匹配，不报错也不改。我为这个白跑了两轮十几分钟的构建。判断方法是构建完 `docker run --rm 镜像 cat 那个文件`，看改动在不在。","线上事故记录",1,[70,71,75,79,83],{"slug":60,"title":61,"seriesIndex":68},{"slug":72,"title":73,"seriesIndex":74},"wo-ba-compose-de-override-xie-cuo-zhan","我把 compose 的 override 写错，站点停了二十分钟",2,{"slug":76,"title":77,"seriesIndex":78},"jian-li-de-xie-gang-hui-bian-cheng","键里的斜杠会变成冒号：清缓存清了个空",3,{"slug":80,"title":81,"seriesIndex":82},"dian-xiang-mu-ye-mian-jiu-si-yi","点「项目」页面就死：一次客户端无限重定向",4,{"slug":84,"title":85,"seriesIndex":86},"gang-deng-lu-jiu-guo-qi-yi-ci","一个 401，查出四个前端机制：缓存回放、隐藏页的 rAF、百分比圆角、缓动过冲",5,{"slug":88,"title":89},"dai-ma-kuai-li-de-san-ge-fan","代码块里的三个反引号把正文截断了",{"slug":91,"title":92},"swap-dui-shang-xian-he-gou-jian-nei","swap、堆上限和构建内存墙",[94,100,104,108,116],{"slug":95,"title":96,"excerpt":97,"date":98,"tags":99},"cong-da-bao-shang-chuan-dao-push-ji","从打包上传到 push 即上线：把 CD 接通","私有仓库的镜像包默认也是私有的，健康检查的探测地址在宿主机有 nginx 时会打错地方。两处都是配好之后才验出来的。","2026-10-02",[30,15],{"slug":72,"title":73,"excerpt":101,"date":102,"tags":103},"ports 是追加不是替换，合并之后 80 端口撞了；而 up 失败前旧容器已经被换掉。配置检查命令的输出格式也骗了我一次。","2026-09-03",[30],{"slug":91,"title":92,"excerpt":105,"date":106,"tags":107},"swap 让构建不被杀但变慢，抬堆上限让 Node 敢用更多内存，两件事要一起做。试过的几个数字和哪个最后成了，都写在里面。","2026-08-20",[30],{"slug":84,"title":85,"excerpt":109,"date":110,"tags":111},"响应头里的 token 被 304 原样回放，把刚签发的新 token 顶掉，表现成刚登录就过期。顺带记三个前端细节：隐藏标签页里 rAF 不执行、扁盒子的 50% 圆角是胶囊、缓动曲线的过冲只有 0.0008。","2026-10-13",[112,113,114,115],"CSS","JavaScript","Vue","浏览器",{"slug":80,"title":81,"excerpt":117,"date":98,"tags":118},"为了保留旧地址大小写加的一条跳转规则，被注册成了客户端路由；路由匹配不区分大小写，于是它自己跳自己。",[12,115]]