[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-v2-\u002Fblog\u002Fnuxt-ssr-crash-debug":3},{"id":4,"title":5,"body":6,"date":1543,"description":1544,"draft":1545,"extension":1546,"meta":1547,"navigation":96,"path":1549,"seo":1550,"stem":1551,"tags":1552,"__hash__":1558},"blog\u002Fblog\u002Fnuxt-ssr-crash-debug.md","从 HTTP 200 到 Connection Reset——Nuxt SSR 容器崩溃排障手记",{"type":7,"value":8,"toc":1517},"minimark",[9,13,25,28,31,36,39,49,52,199,202,204,208,211,214,402,405,429,432,434,438,443,446,452,459,502,505,510,513,518,565,568,574,578,696,699,715,720,724,727,748,751,797,800,839,842,844,848,851,911,918,921,944,947,953,956,960,963,969,972,975,981,984,1071,1074,1078,1081,1087,1090,1093,1100,1104,1107,1113,1116,1119,1126,1128,1132,1135,1139,1189,1192,1196,1202,1238,1241,1327,1330,1338,1369,1376,1379,1389,1391,1395,1398,1404,1406,1410,1470,1473,1475,1478,1481,1498,1501,1504,1507,1510,1513],[10,11,5],"h1",{"id":12},"从-http-200-到-connection-resetnuxt-ssr-容器崩溃排障手记",[14,15,16],"blockquote",{},[17,18,19,20,24],"p",{},"一切正常。然后你重启了一个 Docker 容器。然后 HTTPS 请求全部返回 ",[21,22,23],"code",{},"Connection reset by peer","。但 HTTP 是正常的。但 nginx 在跑。但 Nuxt 也在跑。",[17,26,27],{},"这是一个真实的排障故事。没有 AI 的\"我猜是这个问题\"——只有一步步的、可复现的诊断。",[29,30],"hr",{},[32,33,35],"h2",{"id":34},"一架构回顾","一、架构回顾",[17,37,38],{},"我的个人网站（deeeli.com）部署在腾讯云香港的单台 2GB 内存服务器上。",[40,41,46],"pre",{"className":42,"code":44,"language":45},[43],"language-text","┌─────────────────────────────────────┐\n│  腾讯云香港 (43.135.47.130)          │\n│                                     │\n│  ┌──────────┐    ┌───────────────┐  │\n│  │  nginx   │───→│  Nuxt 3 SSR   │  │\n│  │  :80\u002F443 │    │  :3000 (容器) │  │\n│  │          │    │  内存限制 1GB  │  │\n│  └──────────┘    └───────────────┘  │\n│       │                             │\n│       │  静态资源                    │\n│       ▼                             │\n│  \u002Fvar\u002Fwww\u002Fhtml\u002F                     │\n│  \u002Fvar\u002Fwww\u002Fpreview\u002F                  │\n└─────────────────────────────────────┘\n","text",[21,47,44],{"__ignoreMap":48},"",[17,50,51],{},"nginx 配置：",[40,53,57],{"className":54,"code":55,"language":56,"meta":48,"style":48},"language-nginx shiki shiki-themes github-light github-dark","server {\n    listen 80;\n    server_name deeeli.com;\n    return 301 https:\u002F\u002F$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    server_name deeeli.com;\n\n    ssl_certificate     \u002Fetc\u002Fssl\u002Fdeeeli.crt;\n    ssl_certificate_key \u002Fetc\u002Fssl\u002Fdeeeli.key;\n\n    location \u002F {\n        proxy_pass http:\u002F\u002F127.0.0.1:3000;\n        proxy_set_header Host $host;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n\n    location \u002Fpreview\u002F {\n        alias \u002Fvar\u002Fwww\u002Fpreview\u002F;\n    }\n}\n","nginx",[21,58,59,67,73,79,85,91,98,103,109,114,119,125,131,136,142,148,154,160,166,172,177,183,189,194],{"__ignoreMap":48},[60,61,64],"span",{"class":62,"line":63},"line",1,[60,65,66],{},"server {\n",[60,68,70],{"class":62,"line":69},2,[60,71,72],{},"    listen 80;\n",[60,74,76],{"class":62,"line":75},3,[60,77,78],{},"    server_name deeeli.com;\n",[60,80,82],{"class":62,"line":81},4,[60,83,84],{},"    return 301 https:\u002F\u002F$host$request_uri;\n",[60,86,88],{"class":62,"line":87},5,[60,89,90],{},"}\n",[60,92,94],{"class":62,"line":93},6,[60,95,97],{"emptyLinePlaceholder":96},true,"\n",[60,99,101],{"class":62,"line":100},7,[60,102,66],{},[60,104,106],{"class":62,"line":105},8,[60,107,108],{},"    listen 443 ssl;\n",[60,110,112],{"class":62,"line":111},9,[60,113,78],{},[60,115,117],{"class":62,"line":116},10,[60,118,97],{"emptyLinePlaceholder":96},[60,120,122],{"class":62,"line":121},11,[60,123,124],{},"    ssl_certificate     \u002Fetc\u002Fssl\u002Fdeeeli.crt;\n",[60,126,128],{"class":62,"line":127},12,[60,129,130],{},"    ssl_certificate_key \u002Fetc\u002Fssl\u002Fdeeeli.key;\n",[60,132,134],{"class":62,"line":133},13,[60,135,97],{"emptyLinePlaceholder":96},[60,137,139],{"class":62,"line":138},14,[60,140,141],{},"    location \u002F {\n",[60,143,145],{"class":62,"line":144},15,[60,146,147],{},"        proxy_pass http:\u002F\u002F127.0.0.1:3000;\n",[60,149,151],{"class":62,"line":150},16,[60,152,153],{},"        proxy_set_header Host $host;\n",[60,155,157],{"class":62,"line":156},17,[60,158,159],{},"        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n",[60,161,163],{"class":62,"line":162},18,[60,164,165],{},"        proxy_set_header X-Forwarded-Proto $scheme;\n",[60,167,169],{"class":62,"line":168},19,[60,170,171],{},"    }\n",[60,173,175],{"class":62,"line":174},20,[60,176,97],{"emptyLinePlaceholder":96},[60,178,180],{"class":62,"line":179},21,[60,181,182],{},"    location \u002Fpreview\u002F {\n",[60,184,186],{"class":62,"line":185},22,[60,187,188],{},"        alias \u002Fvar\u002Fwww\u002Fpreview\u002F;\n",[60,190,192],{"class":62,"line":191},23,[60,193,171],{},[60,195,197],{"class":62,"line":196},24,[60,198,90],{},[17,200,201],{},"架构很简单：nginx 终止 SSL，反向代理到 Nuxt 的 Docker 容器。",[29,203],{},[32,205,207],{"id":206},"二故障发生","二、故障发生",[17,209,210],{},"日常部署流程：Coder 修改了首页的 Three.js 3D 场景，从 v5（精简）回退到 v4（赛博朋克完整版）。",[17,212,213],{},"Ops Worker 执行了安全部署：",[40,215,219],{"className":216,"code":217,"language":218,"meta":48,"style":48},"language-bash shiki shiki-themes github-light github-dark","# 1. 本地构建镜像\ndocker build -t website:v4-fix .\n\n# 2. 压缩上传\ndocker save website:v4-fix | gzip > website-v4-fix.tar.gz\nsftp upload → \u002Ftmp\u002F\n\n# 3. 服务器加载\nssh root@43.135.47.130 \"docker load \u003C \u002Ftmp\u002Fwebsite-v4-fix.tar.gz\"\n\n# 4. 测试端口启动\ndocker run -d --name web-test -p 3001:3000 --memory=1g website:v4-fix\ncurl http:\u002F\u002F127.0.0.1:3001\u002F   # → 200 OK ✅\n\n# 5. 切换\ndocker stop web && docker rm web\ndocker rename web-test web\n","bash",[21,220,221,227,247,251,256,278,292,296,301,312,316,321,349,360,364,369,391],{"__ignoreMap":48},[60,222,223],{"class":62,"line":63},[60,224,226],{"class":225},"sJ8bj","# 1. 本地构建镜像\n",[60,228,229,233,237,241,244],{"class":62,"line":69},[60,230,232],{"class":231},"sScJk","docker",[60,234,236],{"class":235},"sZZnC"," build",[60,238,240],{"class":239},"sj4cs"," -t",[60,242,243],{"class":235}," website:v4-fix",[60,245,246],{"class":235}," .\n",[60,248,249],{"class":62,"line":75},[60,250,97],{"emptyLinePlaceholder":96},[60,252,253],{"class":62,"line":81},[60,254,255],{"class":225},"# 2. 压缩上传\n",[60,257,258,260,263,265,269,272,275],{"class":62,"line":87},[60,259,232],{"class":231},[60,261,262],{"class":235}," save",[60,264,243],{"class":235},[60,266,268],{"class":267},"szBVR"," |",[60,270,271],{"class":231}," gzip",[60,273,274],{"class":267}," >",[60,276,277],{"class":235}," website-v4-fix.tar.gz\n",[60,279,280,283,286,289],{"class":62,"line":93},[60,281,282],{"class":231},"sftp",[60,284,285],{"class":235}," upload",[60,287,288],{"class":235}," →",[60,290,291],{"class":235}," \u002Ftmp\u002F\n",[60,293,294],{"class":62,"line":100},[60,295,97],{"emptyLinePlaceholder":96},[60,297,298],{"class":62,"line":105},[60,299,300],{"class":225},"# 3. 服务器加载\n",[60,302,303,306,309],{"class":62,"line":111},[60,304,305],{"class":231},"ssh",[60,307,308],{"class":235}," root@43.135.47.130",[60,310,311],{"class":235}," \"docker load \u003C \u002Ftmp\u002Fwebsite-v4-fix.tar.gz\"\n",[60,313,314],{"class":62,"line":116},[60,315,97],{"emptyLinePlaceholder":96},[60,317,318],{"class":62,"line":121},[60,319,320],{"class":225},"# 4. 测试端口启动\n",[60,322,323,325,328,331,334,337,340,343,346],{"class":62,"line":127},[60,324,232],{"class":231},[60,326,327],{"class":235}," run",[60,329,330],{"class":239}," -d",[60,332,333],{"class":239}," --name",[60,335,336],{"class":235}," web-test",[60,338,339],{"class":239}," -p",[60,341,342],{"class":235}," 3001:3000",[60,344,345],{"class":239}," --memory=1g",[60,347,348],{"class":235}," website:v4-fix\n",[60,350,351,354,357],{"class":62,"line":133},[60,352,353],{"class":231},"curl",[60,355,356],{"class":235}," http:\u002F\u002F127.0.0.1:3001\u002F",[60,358,359],{"class":225},"   # → 200 OK ✅\n",[60,361,362],{"class":62,"line":138},[60,363,97],{"emptyLinePlaceholder":96},[60,365,366],{"class":62,"line":144},[60,367,368],{"class":225},"# 5. 切换\n",[60,370,371,373,376,379,383,385,388],{"class":62,"line":150},[60,372,232],{"class":231},[60,374,375],{"class":235}," stop",[60,377,378],{"class":235}," web",[60,380,382],{"class":381},"sVt8B"," && ",[60,384,232],{"class":231},[60,386,387],{"class":235}," rm",[60,389,390],{"class":235}," web\n",[60,392,393,395,398,400],{"class":62,"line":156},[60,394,232],{"class":231},[60,396,397],{"class":235}," rename",[60,399,336],{"class":235},[60,401,390],{"class":235},[17,403,404],{},"切换完成。然后：",[40,406,408],{"className":216,"code":407,"language":218,"meta":48,"style":48},"$ curl https:\u002F\u002Fdeeeli.com\ncurl: (35) OpenSSL SSL_connect: Connection reset by peer\n",[21,409,410,421],{"__ignoreMap":48},[60,411,412,415,418],{"class":62,"line":63},[60,413,414],{"class":231},"$",[60,416,417],{"class":235}," curl",[60,419,420],{"class":235}," https:\u002F\u002Fdeeeli.com\n",[60,422,423,426],{"class":62,"line":69},[60,424,425],{"class":231},"curl:",[60,427,428],{"class":381}," (35) OpenSSL SSL_connect: Connection reset by peer\n",[17,430,431],{},"这就开始了。",[29,433],{},[32,435,437],{"id":436},"三系统化排障","三、系统化排障",[439,440,442],"h3",{"id":441},"第一步隔离问题层次","第一步：隔离问题层次",[17,444,445],{},"从最外层开始，逐层向内隔离：",[40,447,450],{"className":448,"code":449,"language":45},[43],"用户 ─→ DNS ─→ CDN ─→ nginx ─→ Nuxt\n",[21,451,449],{"__ignoreMap":48},[17,453,454,458],{},[455,456,457],"strong",{},"检查 DNS","：",[40,460,462],{"className":216,"code":461,"language":218,"meta":48,"style":48},"$ dig deeeli.com\n;; ANSWER SECTION:\ndeeeli.com.    600    IN    A    43.135.47.130\n",[21,463,464,474,485],{"__ignoreMap":48},[60,465,466,468,471],{"class":62,"line":63},[60,467,414],{"class":231},[60,469,470],{"class":235}," dig",[60,472,473],{"class":235}," deeeli.com\n",[60,475,476,479,482],{"class":62,"line":69},[60,477,478],{"class":381},";; ",[60,480,481],{"class":231},"ANSWER",[60,483,484],{"class":235}," SECTION:\n",[60,486,487,490,493,496,499],{"class":62,"line":75},[60,488,489],{"class":231},"deeeli.com.",[60,491,492],{"class":239},"    600",[60,494,495],{"class":235},"    IN",[60,497,498],{"class":235},"    A",[60,500,501],{"class":239},"    43.135.47.130\n",[17,503,504],{},"DNS 正常，解析到正确的服务器。",[17,506,507,458],{},[455,508,509],{},"检查 CDN",[17,511,512],{},"没有 CDN。直接到服务器。",[17,514,515,458],{},[455,516,517],{},"检查 nginx 是否在跑",[40,519,521],{"className":216,"code":520,"language":218,"meta":48,"style":48},"$ curl -I http:\u002F\u002Fdeeeli.com\nHTTP\u002F1.1 301 Moved Permanently\nServer: nginx\u002F1.25.3\nLocation: https:\u002F\u002Fdeeeli.com\u002F\n",[21,522,523,535,549,557],{"__ignoreMap":48},[60,524,525,527,529,532],{"class":62,"line":63},[60,526,414],{"class":231},[60,528,417],{"class":235},[60,530,531],{"class":239}," -I",[60,533,534],{"class":235}," http:\u002F\u002Fdeeeli.com\n",[60,536,537,540,543,546],{"class":62,"line":69},[60,538,539],{"class":231},"HTTP\u002F1.1",[60,541,542],{"class":239}," 301",[60,544,545],{"class":235}," Moved",[60,547,548],{"class":235}," Permanently\n",[60,550,551,554],{"class":62,"line":75},[60,552,553],{"class":231},"Server:",[60,555,556],{"class":235}," nginx\u002F1.25.3\n",[60,558,559,562],{"class":62,"line":81},[60,560,561],{"class":231},"Location:",[60,563,564],{"class":235}," https:\u002F\u002Fdeeeli.com\u002F\n",[17,566,567],{},"nginx 活着，HTTP 80 正常响应 301 重定向。",[17,569,570,573],{},[455,571,572],{},"这告诉我们","：问题出在 HTTPS 层，而不是 HTTP 层。",[439,575,577],{"id":576},"第二步深入-ssl","第二步：深入 SSL",[40,579,581],{"className":216,"code":580,"language":218,"meta":48,"style":48},"$ curl -v https:\u002F\u002Fdeeeli.com 2>&1 | head -20\n*   Trying 43.135.47.130:443...\n* Connected to deeeli.com (43.135.47.130) port 443\n* ALPN: curl offers h2,http\u002F1.1\n* SSL connection using TLSv1.3\n* Server certificate:\n*  subject: CN=deeeli.com\n*  start date: May 10 00:00:00 2026 GMT\n*  expire date: Aug  8 23:59:59 2026 GMT\n*  SSL certificate verify ok.\n* Send failure: Connection reset by peer\n* OpenSSL SSL_connect: Connection reset by peer\n",[21,582,583,606,614,627,634,641,648,661,668,675,682,689],{"__ignoreMap":48},[60,584,585,587,589,592,595,598,600,603],{"class":62,"line":63},[60,586,414],{"class":231},[60,588,417],{"class":235},[60,590,591],{"class":239}," -v",[60,593,594],{"class":235}," https:\u002F\u002Fdeeeli.com",[60,596,597],{"class":267}," 2>&1",[60,599,268],{"class":267},[60,601,602],{"class":231}," head",[60,604,605],{"class":239}," -20\n",[60,607,608,611],{"class":62,"line":69},[60,609,610],{"class":267},"*",[60,612,613],{"class":381},"   Trying 43.135.47.130:443...\n",[60,615,616,618,621,624],{"class":62,"line":75},[60,617,610],{"class":267},[60,619,620],{"class":381}," Connected to deeeli.com (",[60,622,623],{"class":231},"43.135.47.130",[60,625,626],{"class":381},") port 443\n",[60,628,629,631],{"class":62,"line":81},[60,630,610],{"class":267},[60,632,633],{"class":381}," ALPN: curl offers h2,http\u002F1.1\n",[60,635,636,638],{"class":62,"line":87},[60,637,610],{"class":267},[60,639,640],{"class":381}," SSL connection using TLSv1.3\n",[60,642,643,645],{"class":62,"line":93},[60,644,610],{"class":267},[60,646,647],{"class":381}," Server certificate:\n",[60,649,650,652,655,658],{"class":62,"line":100},[60,651,610],{"class":267},[60,653,654],{"class":381},"  subject: CN",[60,656,657],{"class":267},"=",[60,659,660],{"class":235},"deeeli.com\n",[60,662,663,665],{"class":62,"line":105},[60,664,610],{"class":267},[60,666,667],{"class":381},"  start date: May 10 00:00:00 2026 GMT\n",[60,669,670,672],{"class":62,"line":111},[60,671,610],{"class":267},[60,673,674],{"class":381},"  expire date: Aug  8 23:59:59 2026 GMT\n",[60,676,677,679],{"class":62,"line":116},[60,678,610],{"class":267},[60,680,681],{"class":381},"  SSL certificate verify ok.\n",[60,683,684,686],{"class":62,"line":121},[60,685,610],{"class":267},[60,687,688],{"class":381}," Send failure: Connection reset by peer\n",[60,690,691,693],{"class":62,"line":127},[60,692,610],{"class":267},[60,694,695],{"class":381}," OpenSSL SSL_connect: Connection reset by peer\n",[17,697,698],{},"关键信息：",[700,701,702,706,709,712],"ul",{},[703,704,705],"li",{},"TCP 连接成功（443 端口可达）",[703,707,708],{},"TLS 握手完成 ✅",[703,710,711],{},"SSL 证书验证通过 ✅",[703,713,714],{},"但在发送 HTTP 请求时被 reset",[17,716,717,719],{},[455,718,572],{},"：nginx 的 SSL 层正常工作，但在尝试代理请求到上游时出了问题。",[439,721,723],{"id":722},"第三步直接测试上游","第三步：直接测试上游",[17,725,726],{},"绕过 nginx，直接访问 Nuxt：",[40,728,730],{"className":216,"code":729,"language":218,"meta":48,"style":48},"$ curl http:\u002F\u002F127.0.0.1:3000\ncurl: (7) Failed to connect to 127.0.0.1 port 3000: Connection refused\n",[21,731,732,741],{"__ignoreMap":48},[60,733,734,736,738],{"class":62,"line":63},[60,735,414],{"class":231},[60,737,417],{"class":235},[60,739,740],{"class":235}," http:\u002F\u002F127.0.0.1:3000\n",[60,742,743,745],{"class":62,"line":69},[60,744,425],{"class":231},[60,746,747],{"class":381}," (7) Failed to connect to 127.0.0.1 port 3000: Connection refused\n",[17,749,750],{},"端口 3000 没有监听！",[40,752,754],{"className":216,"code":753,"language":218,"meta":48,"style":48},"$ docker ps\nCONTAINER ID   IMAGE            STATUS\na1b2c3d4e5f6   nginx:alpine     Up 30 minutes\n",[21,755,756,766,780],{"__ignoreMap":48},[60,757,758,760,763],{"class":62,"line":63},[60,759,414],{"class":231},[60,761,762],{"class":235}," docker",[60,764,765],{"class":235}," ps\n",[60,767,768,771,774,777],{"class":62,"line":69},[60,769,770],{"class":231},"CONTAINER",[60,772,773],{"class":235}," ID",[60,775,776],{"class":235},"   IMAGE",[60,778,779],{"class":235},"            STATUS\n",[60,781,782,785,788,791,794],{"class":62,"line":75},[60,783,784],{"class":231},"a1b2c3d4e5f6",[60,786,787],{"class":235},"   nginx:alpine",[60,789,790],{"class":235},"     Up",[60,792,793],{"class":239}," 30",[60,795,796],{"class":235}," minutes\n",[17,798,799],{},"只有一个 nginx 容器在运行。Nuxt 容器去哪了？",[40,801,803],{"className":216,"code":802,"language":218,"meta":48,"style":48},"$ docker ps -a | grep website\nb5c6d7e8f9a0   website:v4-fix  Exited (137) 2 minutes ago\n",[21,804,805,825],{"__ignoreMap":48},[60,806,807,809,811,814,817,819,822],{"class":62,"line":63},[60,808,414],{"class":231},[60,810,762],{"class":235},[60,812,813],{"class":235}," ps",[60,815,816],{"class":239}," -a",[60,818,268],{"class":267},[60,820,821],{"class":231}," grep",[60,823,824],{"class":235}," website\n",[60,826,827,830,833,836],{"class":62,"line":69},[60,828,829],{"class":231},"b5c6d7e8f9a0",[60,831,832],{"class":235},"   website:v4-fix",[60,834,835],{"class":235},"  Exited",[60,837,838],{"class":381}," (137) 2 minutes ago\n",[17,840,841],{},"退出码 137。这是 Linux 内核发送 SIGKILL 的信号——通常是 OOM。",[29,843],{},[32,845,847],{"id":846},"四oom-根因分析","四、OOM 根因分析",[439,849,850],{"id":850},"退出码含义",[852,853,854,867],"table",{},[855,856,857],"thead",{},[858,859,860,864],"tr",{},[861,862,863],"th",{},"退出码",[861,865,866],{},"含义",[868,869,870,879,887,895,903],"tbody",{},[858,871,872,876],{},[873,874,875],"td",{},"0",[873,877,878],{},"正常退出",[858,880,881,884],{},[873,882,883],{},"1",[873,885,886],{},"应用错误",[858,888,889,892],{},[873,890,891],{},"137",[873,893,894],{},"被 SIGKILL 杀死（通常 OOM）",[858,896,897,900],{},[873,898,899],{},"139",[873,901,902],{},"段错误 (SIGSEGV)",[858,904,905,908],{},[873,906,907],{},"143",[873,909,910],{},"被 SIGTERM 终止",[17,912,913,914,917],{},"137 = ",[21,915,916],{},"128 + 9","，9 是 SIGKILL 的信号编号。这是 Docker 或内核因为内存超出限制而杀死了容器。",[439,919,920],{"id":920},"日志回溯",[40,922,924],{"className":216,"code":923,"language":218,"meta":48,"style":48},"$ docker logs b5c6d7e8f9a0 --tail 100\n",[21,925,926],{"__ignoreMap":48},[60,927,928,930,932,935,938,941],{"class":62,"line":63},[60,929,414],{"class":231},[60,931,762],{"class":235},[60,933,934],{"class":235}," logs",[60,936,937],{"class":235}," b5c6d7e8f9a0",[60,939,940],{"class":239}," --tail",[60,942,943],{"class":239}," 100\n",[17,945,946],{},"关键片段：",[40,948,951],{"className":949,"code":950,"language":45},[43],"[nuxt] [request] GET \u002F\n[nuxt] [ssr] Rendering page: \u002F\n\u003C--- Last few GCs --->\n[45:0x5a3c000]  1800 ms: Scavenge 950.0 (992.0) -> 948.0 (992.0) MB\n[45:0x5a3c000]  2000 ms: Mark-sweep 992.0 (1024.0) -> 990.0 (1024.0) MB\n[45:0x5a3c000]  2500 ms: Mark-sweep 1010.0 (1024.0) -> 1005.0 (1024.0) MB\n\n\u003C--- JS stacktrace --->\nFATAL ERROR: Ineffective mark-compacts near heap limit\nAllocation failed - JavaScript heap out of memory\n",[21,952,950],{"__ignoreMap":48},[17,954,955],{},"这是 V8 的经典 OOM 信息。Nuxt 在进行 SSR 时，V8 堆内存溢出。",[439,957,959],{"id":958},"为什么-v5-没事v4-就-oom","为什么 v5 没事，v4 就 OOM？",[17,961,962],{},"问题出在 Three.js。v4 的首页场景包含：",[40,964,967],{"className":965,"code":966,"language":45},[43],"- IcosahedronGeometry（20 面的正二十面体）\n- 双环面（两个 TorusGeometry）\n- 900 个粒子的 Points 系统\n- Canvas 纹理（每个粒子着色用）\n- 自定义 ShaderMaterial\n",[21,968,966],{"__ignoreMap":48},[17,970,971],{},"Three.js 在 SSR 时（通过 headless-gl 创建 WebGL 上下文），每个几何体、纹理、着色器都要在内存中实例化。即使只是服务端渲染一瞬间，也足以触发 OOM。",[17,973,974],{},"v5 的场景简化了：",[40,976,979],{"className":977,"code":978,"language":45},[43],"- SphereGeometry（简化球体）\n- 无环面\n- 300 个纯色粒子（无 Canvas 纹理）\n- 基础 MeshStandardMaterial\n",[21,980,978],{"__ignoreMap":48},[17,982,983],{},"内存占用差异：",[852,985,986,999],{},[855,987,988],{},[858,989,990,993,996],{},[861,991,992],{},"组件",[861,994,995],{},"v5 内存",[861,997,998],{},"v4 内存",[868,1000,1001,1012,1022,1033,1044,1054],{},[858,1002,1003,1006,1009],{},[873,1004,1005],{},"几何体缓冲区",[873,1007,1008],{},"~10MB",[873,1010,1011],{},"~50MB",[858,1013,1014,1017,1019],{},[873,1015,1016],{},"Canvas 纹理",[873,1018,875],{},[873,1020,1021],{},"~80MB",[858,1023,1024,1027,1030],{},[873,1025,1026],{},"着色器编译",[873,1028,1029],{},"~30MB",[873,1031,1032],{},"~100MB",[858,1034,1035,1038,1041],{},[873,1036,1037],{},"粒子系统",[873,1039,1040],{},"~15MB",[873,1042,1043],{},"~45MB",[858,1045,1046,1049,1052],{},[873,1047,1048],{},"V8 堆其他",[873,1050,1051],{},"~150MB",[873,1053,1051],{},[858,1055,1056,1061,1066],{},[873,1057,1058],{},[455,1059,1060],{},"总计（SSR 峰值）",[873,1062,1063],{},[455,1064,1065],{},"~200MB",[873,1067,1068],{},[455,1069,1070],{},"~425MB",[17,1072,1073],{},"1GB 容器限制看起来是够的——但 SSR 时需要同时加载整个 Nuxt 应用 + 页面组件 + Three.js 场景。V8 的堆管理在 GC 来不及回收时，峰值内存可能远超静态分析的数字。",[439,1075,1077],{"id":1076},"tester-为什么没发现","Tester 为什么没发现？",[17,1079,1080],{},"这是整件事最令人沮丧的部分。在这次部署之前，完整的流水线是：",[40,1082,1085],{"className":1083,"code":1084,"language":45},[43],"Coder 修改 → Reviewer 审查 → Git 提交 → Tester 测试 → Ops 部署\n",[21,1086,1084],{"__ignoreMap":48},[17,1088,1089],{},"Tester 跑了单元测试、集成测试、E2E 测试，全部通过。但为什么没发现 OOM？",[17,1091,1092],{},"答案很简单：Tester 运行在本地开发环境，有 16GB 内存可用。Docker 容器限制的 1GB 对本地来说毫无意义——V8 可以自由扩展到 4GB 堆空间，自然不会有 OOM。",[17,1094,1095,1096,1099],{},"这是典型的",[455,1097,1098],{},"环境不对称","问题。测试环境 ≠ 生产环境。Tester 和 Ops 之间缺少一个\"接近生产\"的预发布阶段。如果 Tester 在一个同样限制 1GB 内存的容器中跑一次 SSR 请求，这个 OOM 会在 30 秒内被抓住，根本不会进入部署阶段。",[439,1101,1103],{"id":1102},"雪上加霜ssh-被封","雪上加霜：SSH 被封",[17,1105,1106],{},"容器崩溃后，AI Ops Worker 自动启动了修复流程。但在多次 SSH 连接尝试后（大约第 8 次），出现了新问题：",[40,1108,1111],{"className":1109,"code":1110,"language":45},[43],"paramiko.ssh_exception.SSHException: Error reading SSH protocol banner\n",[21,1112,1110],{"__ignoreMap":48},[17,1114,1115],{},"服务器的 fail2ban 检测到了异常频繁的 SSH 连接，把 Ops Worker 的 IP 地址封禁了。同一个 Worker 一边在尝试修复容器，一边在触发安全防护——它在和自己的安全策略打架。",[17,1117,1118],{},"加上 paramiko 在连接失败时的自动重试机制（默认 3 次），fail2ban 的触发速度比预期快得多。从第一个连接失败到被封禁，只用了不到两分钟。",[17,1120,1121,1122,1125],{},"这是一个元问题：",[455,1123,1124],{},"AI 自动修复系统需要一个\"不会搞死自己\"的约束","。在尝试修复时，它不应该触发安全机制把自己排除在外。理想的方案是给 Ops Worker 配置 SSH 连接频率限制——每分钟最多 3 次新连接，超过则等待冷却。",[29,1127],{},[32,1129,1131],{"id":1130},"五修复方案","五、修复方案",[17,1133,1134],{},"三个可选方案：",[439,1136,1138],{"id":1137},"方案-a增加容器内存-不可行","方案 A：增加容器内存 → 不可行",[40,1140,1142],{"className":216,"code":1141,"language":218,"meta":48,"style":48},"$ docker update --memory 2g web\nError: cannot update memory limit of a running container\n",[21,1143,1144,1161],{"__ignoreMap":48},[60,1145,1146,1148,1150,1153,1156,1159],{"class":62,"line":63},[60,1147,414],{"class":231},[60,1149,762],{"class":235},[60,1151,1152],{"class":235}," update",[60,1154,1155],{"class":239}," --memory",[60,1157,1158],{"class":235}," 2g",[60,1160,390],{"class":235},[60,1162,1163,1166,1169,1171,1174,1177,1180,1183,1186],{"class":62,"line":69},[60,1164,1165],{"class":231},"Error:",[60,1167,1168],{"class":235}," cannot",[60,1170,1152],{"class":235},[60,1172,1173],{"class":235}," memory",[60,1175,1176],{"class":235}," limit",[60,1178,1179],{"class":235}," of",[60,1181,1182],{"class":235}," a",[60,1184,1185],{"class":235}," running",[60,1187,1188],{"class":235}," container\n",[17,1190,1191],{},"即使更新了，服务器总共 2GB，给 Nuxt 2GB 意味着系统和 nginx 没有内存了。",[439,1193,1195],{"id":1194},"方案-b在-ssr-中跳过-threejs-最佳","方案 B：在 SSR 中跳过 Three.js → 最佳",[17,1197,1198,1199,458],{},"修改 ",[21,1200,1201],{},"3DBackground.vue",[40,1203,1207],{"className":1204,"code":1205,"language":1206,"meta":48,"style":48},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F 之前\nconst scene = new THREE.Scene();\n\n\u002F\u002F 之后\nconst isSSR = typeof window === 'undefined';\nconst scene = isSSR ? null : new THREE.Scene();\n","javascript",[21,1208,1209,1214,1219,1223,1228,1233],{"__ignoreMap":48},[60,1210,1211],{"class":62,"line":63},[60,1212,1213],{},"\u002F\u002F 之前\n",[60,1215,1216],{"class":62,"line":69},[60,1217,1218],{},"const scene = new THREE.Scene();\n",[60,1220,1221],{"class":62,"line":75},[60,1222,97],{"emptyLinePlaceholder":96},[60,1224,1225],{"class":62,"line":81},[60,1226,1227],{},"\u002F\u002F 之后\n",[60,1229,1230],{"class":62,"line":87},[60,1231,1232],{},"const isSSR = typeof window === 'undefined';\n",[60,1234,1235],{"class":62,"line":93},[60,1236,1237],{},"const scene = isSSR ? null : new THREE.Scene();\n",[17,1239,1240],{},"并在模板中加条件渲染：",[40,1242,1246],{"className":1243,"code":1244,"language":1245,"meta":48,"style":48},"language-html shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Cdiv v-if=\"!isSSR\" ref=\"container\" class=\"three-background\" \u002F>\n  \u003Cdiv v-else class=\"placeholder-bg\" \u002F>\n\u003C\u002Ftemplate>\n","html",[21,1247,1248,1260,1298,1318],{"__ignoreMap":48},[60,1249,1250,1253,1257],{"class":62,"line":63},[60,1251,1252],{"class":381},"\u003C",[60,1254,1256],{"class":1255},"s9eBZ","template",[60,1258,1259],{"class":381},">\n",[60,1261,1262,1265,1268,1271,1273,1276,1279,1281,1284,1287,1289,1292,1296],{"class":62,"line":69},[60,1263,1264],{"class":381},"  \u003C",[60,1266,1267],{"class":1255},"div",[60,1269,1270],{"class":231}," v-if",[60,1272,657],{"class":381},[60,1274,1275],{"class":235},"\"!isSSR\"",[60,1277,1278],{"class":231}," ref",[60,1280,657],{"class":381},[60,1282,1283],{"class":235},"\"container\"",[60,1285,1286],{"class":231}," class",[60,1288,657],{"class":381},[60,1290,1291],{"class":235},"\"three-background\"",[60,1293,1295],{"class":1294},"s7hpK"," \u002F",[60,1297,1259],{"class":381},[60,1299,1300,1302,1304,1307,1309,1311,1314,1316],{"class":62,"line":75},[60,1301,1264],{"class":381},[60,1303,1267],{"class":1255},[60,1305,1306],{"class":231}," v-else",[60,1308,1286],{"class":231},[60,1310,657],{"class":381},[60,1312,1313],{"class":235},"\"placeholder-bg\"",[60,1315,1295],{"class":1294},[60,1317,1259],{"class":381},[60,1319,1320,1323,1325],{"class":62,"line":81},[60,1321,1322],{"class":381},"\u003C\u002F",[60,1324,1256],{"class":1255},[60,1326,1259],{"class":381},[17,1328,1329],{},"这样 SSR 只输出一个占位 div，Three.js 场景仅在客户端挂载后初始化。",[439,1331,1333,1334,1337],{"id":1332},"方案-c使用-clientonly-包装-最简单","方案 C：使用 ",[21,1335,1336],{},"ClientOnly"," 包装 → 最简单",[40,1339,1341],{"className":1243,"code":1340,"language":1245,"meta":48,"style":48},"\u003CClientOnly>\n  \u003CThreeBackground \u002F>\n\u003C\u002FClientOnly>\n",[21,1342,1343,1351,1361],{"__ignoreMap":48},[60,1344,1345,1347,1349],{"class":62,"line":63},[60,1346,1252],{"class":381},[60,1348,1336],{"class":1294},[60,1350,1259],{"class":381},[60,1352,1353,1355,1358],{"class":62,"line":69},[60,1354,1264],{"class":381},[60,1356,1357],{"class":1294},"ThreeBackground",[60,1359,1360],{"class":381}," \u002F>\n",[60,1362,1363,1365,1367],{"class":62,"line":75},[60,1364,1322],{"class":381},[60,1366,1336],{"class":1294},[60,1368,1259],{"class":381},[17,1370,1371,1372,1375],{},"Nuxt 的 ",[21,1373,1374],{},"\u003CClientOnly>"," 组件会在 SSR 时输出注释占位符，在客户端 hydration 后渲染实际内容。一行代码解决问题。",[439,1377,1378],{"id":1378},"最终选择",[17,1380,1381,1382,1384,1385,1388],{},"用了方案 C + 方案 B 的组合：",[21,1383,1374],{}," 确保 SSR 安全，同时内部的 ",[21,1386,1387],{},"isSSR"," 检查作为防御性编程。",[29,1390],{},[32,1392,1394],{"id":1393},"六排障-sop","六、排障 SOP",[17,1396,1397],{},"基于这次经验，我总结了一套 Nuxt SSR 容器问题的排障标准流程：",[40,1399,1402],{"className":1400,"code":1401,"language":45},[43],"1. 从外到内隔离\n   curl http → 通，curl https → 不通\n   → 问题在 nginx↔Nuxt 或 SSL 层\n\n2. SSL 层诊断\n   curl -v https → TLS 握手 OK，请求被 reset\n   → nginx SSL 正常，上游有问题\n\n3. 端口检查\n   curl localhost:3000 → Connection refused\n   → Nuxt 没有在监听\n\n4. 容器状态\n   docker ps -a → Exited (137)\n   → OOM killed\n\n5. 日志回溯\n   docker logs --tail 200 → FATAL ERROR: heap out of memory\n   → 确定根因：SSR 内存溢出\n\n6. 修复\n   方案 A：加内存（需评估整机资源）\n   方案 B：SSR 跳过 3D 库\n   方案 C：ClientOnly 包装\n",[21,1403,1401],{"__ignoreMap":48},[29,1405],{},[32,1407,1409],{"id":1408},"七预防措施","七、预防措施",[1411,1412,1413,1423,1429,1435,1441,1455,1461],"ol",{},[703,1414,1415,1418,1419,1422],{},[455,1416,1417],{},"Docker 资源监控","：在 compose 中加入 ",[21,1420,1421],{},"deploy.resources.limits"," 和健康检查。容器不应该静默 OOM——要么主动限制内存避免 OOM，要么监控 OOM 事件并告警。",[703,1424,1425,1428],{},[455,1426,1427],{},"SSR 内存预算","：在 CI 中跑一次 production build + SSR 请求，记录内存峰值。当新的改动导致 SSR 内存增长超过 20%，CI 应该 Block 部署。",[703,1430,1431,1434],{},[455,1432,1433],{},"Tester 容器化","：Tester 必须在与生产相同的内存限制下运行测试。本地 16GB 通过的测试，在 1GB 容器里可能连 SSR 都跑不完——这个差距必须消灭。",[703,1436,1437,1440],{},[455,1438,1439],{},"渐进式部署","：先部署到 staging 端口，跑完整 E2E 测试，再切换。不要一步到位。",[703,1442,1443,1446,1447,1450,1451,1454],{},[455,1444,1445],{},"回滚方案","：每次部署前 ",[21,1448,1449],{},"docker tag website:latest website:rollback","，出问题 ",[21,1452,1453],{},"docker stop web && docker run -d --name web website:rollback","。",[703,1456,1457,1460],{},[455,1458,1459],{},"SSH 频率限制","：在 Ops Worker 中实现连接频率控制——每分钟最多 3 次新连接，防止触发 fail2ban。这是 AI 自我防卫的基础设施。",[703,1462,1463,1466,1467,1469],{},[455,1464,1465],{},"ClientOnly 防御","：对所有含 Three.js \u002F WebGL \u002F Canvas 的组件，默认使用 ",[21,1468,1374],{}," 包裹。这不是\"可选优化\"——在 SSR 上下文中，这是防止 OOM 的必需操作。",[17,1471,1472],{},"最关键的改进是第 3 条。当 Tester 和 Ops 运行在相同的资源约束下，\"环境差异\"这个坑就不存在了。这也是 DevOps 的老生常谈——\"在相同环境中测试和部署\"——AI 运维也需要遵守这个原则。区别在于，人类 DevOps 工程师靠经验知道这一点，而 AI 需要踩一次坑才知道。",[29,1474],{},[32,1476,1477],{"id":1477},"结语",[17,1479,1480],{},"HTTP 200 → Connection reset，只隔了一个 Docker 重启。",[17,1482,1483,1484,1487,1488,1490,1491,1490,1494,1497],{},"排障这件事，90% 的工作是",[455,1485,1486],{},"正确的隔离和缩小范围","，10% 才是找到根因。",[21,1489,353],{},"、",[21,1492,1493],{},"docker ps",[21,1495,1496],{},"docker logs"," 三个命令，配合对退出码的理解，足够定位绝大多数容器问题。",[17,1499,1500],{},"但真正要解决的，不是\"怎么修\"——是\"为什么没在修之前发现\"。如果 Tester 在容器化环境中跑测试，如果 CI 有内存回归检查，如果 Ops Worker 有 SSH 频率限制——这次故障根本不会发生，或者至少不会恶化。",[17,1502,1503],{},"每条预防措施，都是用一次生产故障换来的。",[17,1505,1506],{},"下一个排障对象？",[17,1508,1509],{},"已经踩过的坑，不会再踩第二次。",[17,1511,1512],{},"（但会有新的坑。）",[1514,1515,1516],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":48,"searchDepth":69,"depth":69,"links":1518},[1519,1520,1521,1526,1533,1540,1541,1542],{"id":34,"depth":69,"text":35},{"id":206,"depth":69,"text":207},{"id":436,"depth":69,"text":437,"children":1522},[1523,1524,1525],{"id":441,"depth":75,"text":442},{"id":576,"depth":75,"text":577},{"id":722,"depth":75,"text":723},{"id":846,"depth":69,"text":847,"children":1527},[1528,1529,1530,1531,1532],{"id":850,"depth":75,"text":850},{"id":920,"depth":75,"text":920},{"id":958,"depth":75,"text":959},{"id":1076,"depth":75,"text":1077},{"id":1102,"depth":75,"text":1103},{"id":1130,"depth":69,"text":1131,"children":1534},[1535,1536,1537,1539],{"id":1137,"depth":75,"text":1138},{"id":1194,"depth":75,"text":1195},{"id":1332,"depth":75,"text":1538},"方案 C：使用 ClientOnly 包装 → 最简单",{"id":1378,"depth":75,"text":1378},{"id":1393,"depth":69,"text":1394},{"id":1408,"depth":69,"text":1409},{"id":1477,"depth":69,"text":1477},"2026-06-02","网站从正常运行到 SSL 连接被重置，中间只隔了一个 Docker 重启。本文记录了完整的排障过程：curl → SSL 超时 → HTTP 回退 → nginx vs Nuxt 定位 → docker logs → OOM → 修复。",false,"md",{"author":1548},"陈德立","\u002Fblog\u002Fnuxt-ssr-crash-debug",{"title":5,"description":1544},"blog\u002Fnuxt-ssr-crash-debug",[1553,232,1554,56,1555,1556,1557],"nuxt","ssr","debugging","devops","troubleshooting","urfLqRklKNM0zPtZZ31ECPmnJu_Tyr_A2uHON11IZyo"]