首页 文章 精选 留言 我的

精选列表

搜索[node.js],共4362篇文章
优秀的个人博客,低调大师

Node.js中处理大文件上传的内存溢出问题与流式方案

最近在维护一个后台管理系统时遇到了一个挺典型的问题:用户上传几百兆的日志文件或备份包时,服务端偶尔会直接崩溃,日志里报 `JavaScript heap out of memory`。排查后发现,问题出在早期为了省事用 `fs.readFile` 一次性读取整个文件到内存,再交给后续的解析逻辑。这种写法在几十 KB 的配置文件上没问题,但面对几十上百 MB 的上传文件,Node 的默认堆内存很快就撑不住了。更隐蔽的情况是,即使没崩溃,内存占用也会在并发上传时迅速叠加,导致整个服务响应变慢。这篇文章从这次实际故障出发,聊聊为什么大文件上传会触发内存溢出,以及如何用流式处理、背压控制和临时文件落盘来重构上传链路,让服务在有限内存下稳定处理大文件。 ## 问题复现:一次性读取的代价 出问题的接口大致是这样的: ```js const express = require('express'); const fs = require('fs'); const app = express(); app.post('/upload', express.raw({ type: '*/*', limit: '500mb' }), (req, res) => { // req.body 此时已经是完整的 Buffer,全部在内存里 const content = req.body.toString('utf8'); const lines = content.split('\n'); // 后续解析... res.json({ lines: lines.length }); }); ``` 这里有两个内存放大点。第一,`express.raw` 中间件会把请求体完整缓冲到内存,500 MB 的文件就对应约 500 MB 的 Buffer。第二,`toString('utf8')` 又会生成一份字符串副本,V8 的字符串在内存中的表示通常比原始字节更大。再加上 `split('\n')` 产生的行数组,峰值内存可能是文件本身的两到三倍。Node 默认的堆上限在 64 位环境下大约 1.4 GB 到 2 GB 之间,单个大文件加上并发就很容易越界。 ## 用流式写入替代内存缓冲 处理上传的第一步是不要在应用层缓冲整个文件,而是把请求流直接管道到磁盘上的临时文件。Node 的 `stream.pipeline` 能很好地处理错误传递和资源清理: ```js const { pipeline } = require('stream/promises'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); app.post('/upload', async (req, res) => { const tmpName = `${Date.now()}-${crypto.randomBytes(6).toString('hex')}.tmp`; const tmpPath = path.join('/var/tmp/uploads', tmpName); try { await pipeline(req, fs.createWriteStream(tmpPath)); } catch (err) { await fs.promises.unlink(tmpPath).catch(() => {}); return res.status(500).json({ error: 'upload failed' }); } // 文件已落盘,后续按需逐行读取 res.json({ tempFile: tmpPath }); }); ``` 这样做的好处是内存占用基本恒定:`pipeline` 内部按 highWaterMark(默认 16 KB 左右)分块流动,无论文件多大,同一时刻应用层只持有少量数据块。注意这里没有用 `express.raw` 或 `multer` 的 memoryStorage,它们都会把文件塞进内存。 ## 解析阶段同样要流式 文件落盘后,解析也不能图省事用 `readFile`。比如要统计日志行数、提取错误行,可以用 `readline` 逐行消费: ```js const readline = require('readline'); async function scanLog(filePath) { const rl = readline.createInterface({ input: fs.createReadStream(filePath), crlfDelay: Infinity }); let total = 0; let errors = 0; for await (const line of rl) { total++; if (line.includes('ERROR')) errors++; } return { total, errors }; } ``` `readline` 内部维护缓冲区并按行切分,不会把整个文件读成一个大字符串。对于需要做 JSON 数组解析的场景,可以考虑 `stream-json` 这类库,它能在流式输入下逐个产出数组元素,避免 `JSON.parse` 对超大字符串的内存冲击。 ## 背压不是可选项 流式处理里容易忽略的是背压。当可写流处理速度慢于可读流时,数据会在内存中排队。手动监听 `data` 事件并调用 `write` 时,如果 `write` 返回 `false`,就必须暂停读取,等 `drain` 事件再恢复: ```js const src = fs.createReadStream(input); const dst = fs.createWriteStream(output); src.on('data', (chunk) => { const ok = dst.write(chunk); if (!ok) { src.pause(); dst.once('drain', () => src.resume()); } }); ``` 直接用 `pipeline` 或 `src.pipe(dst)` 时,Node 已经帮你处理了背压,所以优先用这些组合方式。自己写事件处理时漏掉背压,等于把流式的内存优势又还了回去。 ## 并发上传和临时文件清理 即使单次上传内存可控,并发上传仍会带来磁盘和文件句柄压力。实际项目里我加了两个约束:临时目录设置配额,上传完成后根据业务结果决定立即删除还是转存;同时在反向代理层限制单请求体大小,比如 Nginx 的 `client_max_body_size`,让明显超限的请求在网关层就被拒绝,不进入 Node 进程。 临时文件清理用一个定时任务扫描超过一定时间的 `.tmp` 文件并删除,避免异常中断留下的孤儿文件占满磁盘。上传成功转存后再删除临时文件,顺序不能反,否则解析阶段会读到不存在的文件。 ## 一个容易踩的细节 如果上传接口同时支持表单字段和文件,用 `busboy` 或 `multer` 的 diskStorage 时,要注意字段顺序和流事件的处理时机。曾经遇到在 `file` 事件里同步做耗时解析,阻塞了流的消费,反而造成内存堆积。正确做法是先把文件流消费完落盘,再在 `finish` 后异步处理。流式编程的核心是让数据尽快流动起来,任何在数据路径上的同步阻塞都可能破坏这个模型。 推荐查看天气:http://www.tianqi556.com/chengyu/488.html 推荐查看天气:http://www.tianqi556.com/chengyu/490.html 推荐查看天气:http://www.tianqi556.com/chengyu/491.html 推荐查看天气:http://www.tianqi556.com/chengyu/492.html 推荐查看天气:http://www.tianqi556.com/chengyu/493.html 推荐查看天气:http://www.tianqi556.com/chengyu/494.html 推荐查看天气:http://www.tianqi556.com/chengyu/495.html 推荐查看天气:http://www.tianqi556.com/chengyu/496.html 推荐查看天气:http://www.tianqi556.com/chengyu/497.html 推荐查看天气:http://www.tianqi556.com/chengyu/498.html 推荐查看天气:http://www.tianqi556.com/chengyu/499.html 推荐查看天气:http://www.tianqi556.com/chengyu/500.html 推荐查看天气:http://www.tianqi556.com/chengyu/501.html 推荐查看天气:http://www.tianqi556.com/chengyu/502.html 推荐查看天气:http://www.tianqi556.com/chengyu/503.html 推荐查看天气:http://www.tianqi556.com/chengyu/504.html 推荐查看天气:http://www.tianqi556.com/chengyu/505.html 推荐查看天气:http://www.tianqi556.com/chengyu/506.html 推荐查看天气:http://www.tianqi556.com/chengyu/507.html 推荐查看天气:http://www.tianqi556.com/chengyu/508.html 推荐查看天气:http://www.tianqi556.com/chengyu/509.html 推荐查看天气:http://www.tianqi556.com/chengyu/510.html 推荐查看天气:http://www.tianqi556.com/chengyu/511.html 推荐查看天气:http://www.tianqi556.com/chengyu/512.html 推荐查看天气:http://www.tianqi556.com/chengyu/513.html 推荐查看天气:http://www.tianqi556.com/chengyu/514.html 推荐查看天气:http://www.tianqi556.com/chengyu/519.html 推荐查看天气:http://www.tianqi556.com/chengyu/520.html 推荐查看天气:http://www.tianqi556.com/chengyu/521.html 推荐查看天气:http://www.tianqi556.com/chengyu/522.html 推荐查看天气:http://www.tianqi556.com/chengyu/523.html 推荐查看天气:http://www.tianqi556.com/chengyu/524.html 推荐查看天气:http://www.tianqi556.com/chengyu/525.html 推荐查看天气:http://www.tianqi556.com/chengyu/526.html 推荐查看天气:http://www.tianqi556.com/chengyu/527.html 推荐查看天气:http://www.tianqi556.com/chengyu/528.html 推荐查看天气:http://www.tianqi556.com/chengyu/529.html 推荐查看天气:http://www.tianqi556.com/chengyu/530.html 推荐查看天气:http://www.tianqi556.com/chengyu/531.html 推荐查看天气:http://www.tianqi556.com/chengyu/532.html 推荐查看天气:http://www.tianqi556.com/chengyu/533.html 推荐查看天气:http://www.tianqi556.com/chengyu/534.html 推荐查看天气:http://www.tianqi556.com/chengyu/535.html 推荐查看天气:http://www.tianqi556.com/chengyu/536.html 推荐查看天气:http://www.tianqi556.com/chengyu/537.html 推荐查看天气:http://www.tianqi556.com/chengyu/538.html 推荐查看天气:http://www.tianqi556.com/chengyu/539.html 推荐查看天气:http://www.tianqi556.com/chengyu/540.html 推荐查看天气:http://www.tianqi556.com/chengyu/541.html 推荐查看天气:http://www.tianqi556.com/chengyu/542.html 推荐查看天气:http://www.tianqi556.com/chengyu/543.html 推荐查看天气:http://www.tianqi556.com/chengyu/544.html 推荐查看天气:http://www.tianqi556.com/chengyu/545.html 推荐查看天气:http://www.tianqi556.com/chengyu/546.html 推荐查看天气:http://www.tianqi556.com/chengyu/547.html 推荐查看天气:http://www.tianqi556.com/chengyu/548.html 推荐查看天气:http://www.tianqi556.com/chengyu/549.html 推荐查看天气:http://www.tianqi556.com/chengyu/550.html 推荐查看天气:http://www.tianqi556.com/chengyu/551.html 推荐查看天气:http://www.tianqi556.com/chengyu/552.html 推荐查看天气:http://www.tianqi556.com/chengyu/553.html 推荐查看天气:http://www.tianqi556.com/chengyu/554.html 推荐查看天气:http://www.tianqi556.com/chengyu/556.html 推荐查看天气:http://www.tianqi556.com/chengyu/557.html 推荐查看天气:http://www.tianqi556.com/chengyu/558.html 推荐查看天气:http://www.tianqi556.com/chengyu/559.html 推荐查看天气:http://www.tianqi556.com/chengyu/560.html 推荐查看天气:http://www.tianqi556.com/chengyu/561.html 推荐查看天气:http://www.tianqi556.com/chengyu/562.html 推荐查看天气:http://www.tianqi556.com/chengyu/564.html 推荐查看天气:http://www.tianqi556.com/chengyu/565.html 推荐查看天气:http://www.tianqi556.com/chengyu/566.html 推荐查看天气:http://www.tianqi556.com/chengyu/567.html 推荐查看天气:http://www.tianqi556.com/chengyu/568.html 推荐查看天气:http://www.tianqi556.com/chengyu/569.html 推荐查看天气:http://www.tianqi556.com/chengyu/570.html 推荐查看天气:http://www.tianqi556.com/chengyu/571.html 推荐查看天气:http://www.tianqi556.com/chengyu/572.html 推荐查看天气:http://www.tianqi556.com/chengyu/573.html 推荐查看天气:http://www.tianqi556.com/chengyu/574.html 推荐查看天气:http://www.tianqi556.com/chengyu/575.html 推荐查看天气:http://www.tianqi556.com/chengyu/576.html 推荐查看天气:http://www.tianqi556.com/chengyu/577.html 推荐查看天气:http://www.tianqi556.com/chengyu/578.html 推荐查看天气:http://www.tianqi556.com/chengyu/579.html 推荐查看天气:http://www.tianqi556.com/chengyu/580.html 推荐查看天气:http://www.tianqi556.com/chengyu/581.html 推荐查看天气:http://www.tianqi556.com/chengyu/582.html 推荐查看天气:http://www.tianqi556.com/chengyu/583.html 推荐查看天气:http://www.tianqi556.com/chengyu/584.html 推荐查看天气:http://www.tianqi556.com/chengyu/585.html 推荐查看天气:http://www.tianqi556.com/chengyu/586.html 推荐查看天气:http://www.tianqi556.com/chengyu/587.html 推荐查看天气:http://www.tianqi556.com/chengyu/588.html 推荐查看天气:http://www.tianqi556.com/chengyu/589.html 推荐查看天气:http://www.tianqi556.com/chengyu/590.html 推荐查看天气:http://www.tianqi556.com/chengyu/591.html 推荐查看天气:http://www.tianqi556.com/chengyu/592.html 推荐查看天气:http://www.tianqi556.com/chengyu/593.html 推荐查看天气:http://www.tianqi556.com/chengyu/594.html 推荐查看天气:http://www.tianqi556.com/chengyu/596.html 推荐查看天气:http://www.tianqi556.com/chengyu/597.html 推荐查看天气:http://www.tianqi556.com/chengyu/598.html 推荐查看天气:http://www.tianqi556.com/chengyu/599.html 推荐查看天气:http://www.tianqi556.com/chengyu/600.html 推荐查看天气:http://www.tianqi556.com/chengyu/601.html 推荐查看天气:http://www.tianqi556.com/chengyu/603.html 推荐查看天气:http://www.tianqi556.com/chengyu/604.html 推荐查看天气:http://www.tianqi556.com/chengyu/605.html 推荐查看天气:http://www.tianqi556.com/chengyu/606.html 推荐查看天气:http://www.tianqi556.com/chengyu/607.html 推荐查看天气:http://www.tianqi556.com/chengyu/608.html 推荐查看天气:http://www.tianqi556.com/chengyu/609.html 推荐查看天气:http://www.tianqi556.com/chengyu/610.html 推荐查看天气:http://www.tianqi556.com/chengyu/612.html 推荐查看天气:http://www.tianqi556.com/chengyu/613.html 推荐查看天气:http://www.tianqi556.com/chengyu/614.html 推荐查看天气:http://www.tianqi556.com/chengyu/615.html 推荐查看天气:http://www.tianqi556.com/chengyu/616.html 推荐查看天气:http://www.tianqi556.com/chengyu/617.html 推荐查看天气:http://www.tianqi556.com/chengyu/618.html 推荐查看天气:http://www.tianqi556.com/chengyu/619.html 推荐查看天气:http://www.tianqi556.com/chengyu/620.html 推荐查看天气:http://www.tianqi556.com/chengyu/621.html 推荐查看天气:http://www.tianqi556.com/chengyu/622.html 推荐查看天气:http://www.tianqi556.com/chengyu/623.html 推荐查看天气:http://www.tianqi556.com/chengyu/624.html 推荐查看天气:http://www.tianqi556.com/chengyu/625.html 推荐查看天气:http://www.tianqi556.com/chengyu/626.html 推荐查看天气:http://www.tianqi556.com/chengyu/627.html 推荐查看天气:http://www.tianqi556.com/chengyu/628.html 推荐查看天气:http://www.tianqi556.com/chengyu/629.html 推荐查看天气:http://www.tianqi556.com/chengyu/630.html 推荐查看天气:http://www.tianqi556.com/chengyu/631.html 推荐查看天气:http://www.tianqi556.com/chengyu/632.html 推荐查看天气:http://www.tianqi556.com/chengyu/633.html 推荐查看天气:http://www.tianqi556.com/chengyu/634.html 推荐查看天气:http://www.tianqi556.com/chengyu/635.html 推荐查看天气:http://www.tianqi556.com/chengyu/636.html 推荐查看天气:http://www.tianqi556.com/chengyu/637.html 推荐查看天气:http://www.tianqi556.com/chengyu/638.html 推荐查看天气:http://www.tianqi556.com/chengyu/639.html 推荐查看天气:http://www.tianqi556.com/chengyu/640.html 推荐查看天气:http://www.tianqi556.com/chengyu/641.html 推荐查看天气:http://www.tianqi556.com/chengyu/642.html 推荐查看天气:http://www.tianqi556.com/chengyu/643.html 推荐查看天气:http://www.tianqi556.com/chengyu/644.html 推荐查看天气:http://www.tianqi556.com/chengyu/645.html 推荐查看天气:http://www.tianqi556.com/chengyu/646.html 推荐查看天气:http://www.tianqi556.com/chengyu/647.html 推荐查看天气:http://www.tianqi556.com/chengyu/648.html 推荐查看天气:http://www.tianqi556.com/chengyu/649.html 推荐查看天气:http://www.tianqi556.com/chengyu/650.html 推荐查看天气:http://www.tianqi556.com/chengyu/651.html 推荐查看天气:http://www.tianqi556.com/chengyu/652.html 推荐查看天气:http://www.tianqi556.com/chengyu/653.html 推荐查看天气:http://www.tianqi556.com/chengyu/654.html 推荐查看天气:http://www.tianqi556.com/chengyu/655.html 推荐查看天气:http://www.tianqi556.com/chengyu/656.html 推荐查看天气:http://www.tianqi556.com/chengyu/657.html 推荐查看天气:http://www.tianqi556.com/chengyu/658.html 推荐查看天气:http://www.tianqi556.com/chengyu/659.html 推荐查看天气:http://www.tianqi556.com/chengyu/660.html 推荐查看天气:http://www.tianqi556.com/chengyu/661.html 推荐查看天气:http://www.tianqi556.com/chengyu/662.html 推荐查看天气:http://www.tianqi556.com/chengyu/664.html 推荐查看天气:http://www.tianqi556.com/chengyu/665.html 推荐查看天气:http://www.tianqi556.com/chengyu/666.html 推荐查看天气:http://www.tianqi556.com/chengyu/667.html 推荐查看天气:http://www.tianqi556.com/chengyu/668.html 推荐查看天气:http://www.tianqi556.com/chengyu/669.html 推荐查看天气:http://www.tianqi556.com/chengyu/670.html 推荐查看天气:http://www.tianqi556.com/chengyu/671.html 推荐查看天气:http://www.tianqi556.com/chengyu/672.html 推荐查看天气:http://www.tianqi556.com/chengyu/673.html 推荐查看天气:http://www.tianqi556.com/chengyu/674.html 推荐查看天气:http://www.tianqi556.com/chengyu/675.html 推荐查看天气:http://www.tianqi556.com/chengyu/676.html 推荐查看天气:http://www.tianqi556.com/chengyu/677.html 推荐查看天气:http://www.tianqi556.com/chengyu/678.html 推荐查看天气:http://www.tianqi556.com/chengyu/679.html 推荐查看天气:http://www.tianqi556.com/chengyu/680.html 推荐查看天气:http://www.tianqi556.com/chengyu/681.html 推荐查看天气:http://www.tianqi556.com/chengyu/682.html 推荐查看天气:http://www.tianqi556.com/chengyu/683.html 推荐查看天气:http://www.tianqi556.com/chengyu/684.html 推荐查看天气:http://www.tianqi556.com/chengyu/685.html 推荐查看天气:http://www.tianqi556.com/chengyu/686.html 推荐查看天气:http://www.tianqi556.com/chengyu/687.html 推荐查看天气:http://www.tianqi556.com/chengyu/688.html 推荐查看天气:http://www.tianqi556.com/chengyu/689.html 推荐查看天气:http://www.tianqi556.com/chengyu/690.html 推荐查看天气:http://www.tianqi556.com/chengyu/691.html 推荐查看天气:http://www.tianqi556.com/chengyu/692.html 推荐查看天气:http://www.tianqi556.com/chengyu/693.html 推荐查看天气:http://www.tianqi556.com/chengyu/694.html 推荐查看天气:http://www.tianqi556.com/chengyu/695.html 推荐查看天气:http://www.tianqi556.com/chengyu/696.html 推荐查看天气:http://www.tianqi556.com/chengyu/697.html 推荐查看天气:http://www.tianqi556.com/chengyu/698.html

优秀的个人博客,低调大师

亚马逊数据 API Node.js:容器化之后的成本与可靠性治理

![amazon-data-api-nodejs-cover-zh.png](https://developer.qcloudimg.com/http-save/yehe-11158438/1adccc71a5d17b4358436e408b31e3f5.png) 把亚马逊数据采集任务从本地脚本搬进容器,本以为运维会省心。滚动发布、弹性扩容、统一日志,听起来都是进步。但在亚马逊数据 API 这条链路里,上云后暴露的是另一类问题:发布窗口里任务被 SIGTERM 砍断、失败后重跑产生重复行、月底账单金额对不上业务量。 这三件事表面互不相干,根子都在同一个地方——你写的 Node 客户端把「发出请求」当成了「完成任务」,而云环境的生命周期、并发语义和成本口径,和本地跑一轮脚本不是一回事。本文从云原生与后端的视角,把上云之后的成本与可靠性治理拆成几条可落地的线。 前文讨论过 [Amazon Data API 返回 200 却字段为空的八类缺口](https://cloud.tencent.com/developer/article/2739704),本文接着把视角拉到生产负载:指纹碎片化、HTTP/2 并发模型、幂等续跑,以及云上必须补的四道治理题。 ## 一、任务上了容器之后,新问题是哪一类的 本地跑脚本,失败了大不了手动重跑;上容器之后,生命周期不再由你掌控。 - **滚动发布打断任务**:k8s 滚动更新时旧 Pod 收到 SIGTERM,terminationGracePeriodSeconds 默认 30 秒。单块抓取耗时超过这个宽限期,任务被强杀,已取到但未入库的数据直接丢失。 - **重跑产生重复行**:发布中断或节点重启后,你本能地重跑整批。如果没有幂等设计,同一批 asin 会被写两遍,下游去重压力转嫁给业务。 - **账单对不上业务量**:月底看云厂商账单,请求数和业务侧的「可用记录数」对不上。你记的是调用次数,业务看的是拿到多少条有效数据,两种口径中间隔着失败重试、空字段和重复行。 这三个坑,靠「把脚本跑起来」解决不了,要在客户端和数据库两端同时补治理。 这三类问题有个共同点:它们都是规模上去之后才显形的问题。本地一天跑几千次,断一次手动补就行;上云之后一天几十万次调用,任何没有治理的环节都会被放大成账单和告警。下面三条主线先讲清楚技术根因,再落到云上的四道治理题。 ## 二、主线一:Node 指纹生态碎片化 在 Python 侧,你至少有一个像 `curl_cffi` 那样开箱即用的默认答案。Node 侧没有。 `got-scraping` 已停止维护,且它只改写请求头、从不碰 TLS 握手。卡在 JA3/JA4 的拦截,它本来解决不了——因为 JA3 是 ClientHello 五字段拼接取 MD5,Chrome 110 之后扩展顺序随机置换导致 JA3 不稳定,主流风控已改用排序后哈希的 JA4。只改请求头不碰 TLS,等于在风控最严的那一层毫无作为。 把几个库摆在一张表上(**2026-08 实测,数值随版本变化**,测量条件为 M 系列 Mac、300 次串行请求打本地服务): | 库 | 引擎 | 最新 Chrome | HTTP/2 指纹 | req/s | 冷启动 | |---|---|---|---|---| | wreq-js | Rust wreq + BoringSSL | 149 | 正确 | 12842 | 7 ms | | impers | curl-impersonate | 146 | 正确 | 8439 | 16 ms | | node-wreq | Rust | 149 | 正确 | 6500 | 10 ms | | impit | Rust reqwest + rustls | 124 | 不正确 | 6710 | 37 ms | | CycleTLS | Go 子进程 IPC | 未参与 | 未参与 | 未参与 | IPC 开销 | impit 的问题说得直白些:**它声称 Chrome,却在用 Rust 的语法说话**。它的 HTTP/2 SETTINGS 用底层 Rust 库默认值,缺 `HEADER_TABLE_SIZE`,发送 Chrome 从不发的 `MAX_FRAME_SIZE`。指纹里那一处不匹配,风控侧只要挑出来就能把你挡掉。 更要紧的是,这套指纹不是一次配好就完事。Chrome 每发一个新版本,ClientHello 的扩展顺序、ALPN 列表、TLS 握手的字段布局都可能变;JA4 的排序哈希规则也随风控升级调整。你在 Node 侧自己维护一整套指纹逻辑,等于养了一个永远在追版本的岗位。对于只想稳定拿数据的业务团队,逐库维护指纹是沉没成本。把指纹、住宅 IP、TLS 一致性交给服务端统一处理,是后面要说的全包口径。 ## 三、主线二:HTTP/2 改变并发模型 ![undici-concurrency-zh.png](https://developer.qcloudimg.com/http-save/yehe-11158438/c60c8848a94f6c1129aff60c56fa7216.png) Node 内置 `fetch` 基于 undici,**默认不协商 HTTP/2**(官方称实验性、未默认开启)。你需要显式 `setGlobalDispatcher(new Agent({ allowH2: true }))`。注意 undici `Client` 的 `allowH2` 默认是 `true`,两个入口默认值不一致,这是一个容易踩的坑。 一旦协商成 HTTP/2,单连接并发天花板由 `maxConcurrentStreams`(默认 100)决定,取代 pipelining;流控窗口 `initialWindowSize` 默认 262144。 更大的坑在限流逻辑。`p-limit(8)` 只限并发不限速率:10 ms 响应时约 800 req/s,5 s 响应时约 1.6 req/s。并发数固定,速率随响应时间剧烈波动。你需要 `p-queue` 的 `intervalCap` 补上速率限制,把「同时几个」和「每秒几个」分开控制: ```ts import PQueue from "p-queue"; // 同时最多 8 个,且每 1 秒最多放行 4 个——并发与速率双层闸门 const queue = new PQueue({ concurrency: 8, interval: 1_000, intervalCap: 4 }); ``` 会话复用也能省握手成本:会话内复用 TLS 约 15 ms,单次调用新建握手约 53 ms(量级差异)。高频采集一天几百万次调用,握手时间差会直接体现在账单和延迟上。用会话把一批 asin 绑在同一条连接上: ```ts // 会话复用:一批 asin 走同一条连接,省去每次新建 TLS 握手 const session = await createSession({ browser: "chrome_149" }); for (const asin of asins) { await session.fetch(buildUrl(asin)); } await session.close(); // 循环结束后显式关闭,释放连接 ``` 分阶段超时用 `AbortSignal.timeout`(Node 17.3+)和 `AbortSignal.any`(Node 22+)组合:连接、头、体的超时分开设,避免单点卡死拖垮整批。 ## 四、主线三:幂等与断点续跑 失败之后,你重跑一遍,还是产生重复行?这是数据库设计问题,不是网络问题。 主键四要素:`asin`、`marketplace`、`captured_at`、`contract_version`。把这四个作为唯一约束,`INSERT` 时 `ON CONFLICT(asin, marketplace, captured_at, contract_version) DO UPDATE SET ...`。重跑的幂等于是落在数据库一侧,客户端崩了也不怕。 断点续跑要配 checkpoint:每次成功落库后记录已完成的 asin 区间,重启时从断点继续,而不是从头扫。这一条直接消掉「重跑产生重复行」和「中断后从头跑」两个坑。 checkpoint 本身也要落库,否则重启后不知道从哪续。建一张 `crawl_checkpoint(asin_cursor, batch_no, updated_at)`,每批完成更新游标;进程起来先读游标,跳过已完成的区间。这样节点被杀、Pod 重建,都从断点继续,既不会产生重复行,也不会漏采。幂等约束负责「重跑不增行」,checkpoint 负责「重启不重头」,两者合起来才是完整的可靠性闭环。 ```sql -- 主键四要素保证幂等:同一条快照无论重跑多少次,只留下一行 INSERT INTO product_snapshot (asin, marketplace, captured_at, contract_version, title, price, currency) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (asin, marketplace, captured_at, contract_version) DO UPDATE SET title = EXCLUDED.title, price = EXCLUDED.price, currency = EXCLUDED.currency; ``` ## 五、云上重点:优雅停机 滚动发布时旧 Pod 收到 SIGTERM,宽限期(terminationGracePeriodSeconds)默认 30 秒。**宽限期必须大于单块(单个 asin 或单批)的最大耗时**,否则 flush 来不及,已取到未入库的数据丢失。 正确做法:收到 SIGTERM 后打一个 `shuttingDown` 标记,当前在跑的 asin 处理完就 `flush` 落库再退出,不再接新任务。 ```ts let shuttingDown = false; process.on("SIGTERM", () => { shuttingDown = true; }); async function runOne(asin: string, pending: Row[]) { if (shuttingDown) { await flush(pending); return; } // 刷写已取到未入库的数据 const row = await fetchSnapshot(asin); pending.push(row); if (pending.length >= 200) await flush(pending); } async function flush(pending: Row[]) { if (pending.length) { await bulkUpsert(pending); pending.length = 0; } } ``` 在 k8s 里,这套逻辑落到两处配置:`terminationGracePeriodSeconds` 必须大于单块耗时;preStop 钩子可以先把实例从负载均衡摘掉,再等当前批次 `flush` 完,最后才让 Pod 退出。只改代码不改配置,发布窗口照样丢数据——因为宽限期满之后 kubelet 会发 SIGKILL,flush 来不及执行。 把 `terminationGracePeriodSeconds` 设成单块耗时的 2–3 倍,并在退出前 `await flush(pending)`,发布窗口就不再丢数据。 ## 六、云上重点:结构化日志 云上排查靠日志聚合,不是本地 `console.log`。每条抓取记录要带结构化字段:`traceId`、`attempt`、`errorName`、`durationMs`、`gate`。`gate` 指这一跳走到了哪一步(resolve / tls / http2 / parse / upsert),失败时能直接定位是网络层还是业务层。 JSON 样本: ```json { "traceId": "a1b2c3", "asin": "B0X1Y2Z3W4", "attempt": 2, "gate": "upsert", "httpVersion": "2", "durationMs": 3120, "errorName": null, "itemCount": 1 } ``` 失败时 `errorName` 取 Node 真实错误名,比「请求失败」有用得多:`UND_ERR_CONNECT_TIMEOUT`、`UND_ERR_HEADERS_TIMEOUT`、`UND_ERR_BODY_TIMEOUT`、`TimeoutError`(DOMException)、`ENOTFOUND`、`EAI_AGAIN`。 `traceId` 要在同一次采集任务的所有 attempt 之间保持一致,这样一次失败重试的多次请求能聚到同一条链路下。`attempt` 字段记录第几次重试,配合 `errorName` 能看出是网络抖动还是稳定失败。把这两个字段和 `gate` 一起做聚合,你能区分「某市场整体在 auth 步骤失败」和「个别 asin 偶发超时」——前者是服务端问题,后者是限流问题,处理动作不同。日志的价值不在于单条好看,在于能按 `traceId` 串起整条路径,按 `gate` 定位断点。 按天聚合,看每天的失败分布和延迟分位: ```sql SELECT to_char(captured_at, 'YYYY-MM-DD') AS day, gate, error_name, COUNT(*) AS cnt, PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY duration_ms) AS p95_ms FROM fetch_log WHERE captured_at >= NOW() - INTERVAL '7 days' GROUP BY 1, 2, 3 ORDER BY day DESC, cnt DESC; ``` 这条 SQL 能直接回答「哪一天、哪一步、什么错误在涨」,是成本与可靠性治理的仪表盘底座。 ## 七、云上重点:成本埋点 月底账单对不上业务量,是因为你记的是「请求数」,业务看的是「可用记录数」。两者要摊到一个口径上。 给每个请求埋点:`traceId`、`attempt`、`durationMs`、`statusClass`、`itemCount`、`httpVersion`、`errorName`、`costCredits`。注意 200 也可能 `itemCount` 为 0(字段为空),这种请求花了钱却没产出,必须在成本公式里扣掉。 **每千条可用记录成本 = 总花费 / (可用记录总数 / 1000)** ```ts import PQueue from "p-queue"; import { randomUUID } from "node:crypto"; const queue = new PQueue({ concurrency: 8, interval: 1_000, intervalCap: 4 }); const metrics: Metric[] = []; async function pricedFetch(asin: string) { const start = Date.now(); const res = await queue.add(() => fetchSnapshot(asin)); const durationMs = Date.now() - start; metrics.push({ traceId: randomUUID().slice(0, 8), asin, attempt: res.attempt, durationMs, httpVersion: res.httpVersion, itemCount: res.items.length, // 空字段这里就是 0,成本要摊掉 errorName: res.errorName ?? null, costCredits: 1, }); return res; } // 月底结算:把请求成本摊到可用记录上 function costPerThousand(records: Metric[]) { const totalCost = records.reduce((s, r) => s + r.costCredits, 0); const usable = records.reduce((s, r) => s + r.itemCount, 0); return totalCost / (usable / 1000); } ``` 有了这个函数,你能回答「这周每千条可用记录花了多少」,也能定位哪些 asin 长期空字段、在悄悄拉高单位成本。 举个具体数字:某周调用 120 万次,按单价 1 credit 计,总花费 120 万 credit;其中 8 万次返回 200 但 `itemCount` 为 0,另有 3 万次因 `EAI_AGAIN` 失败无产出。可用记录约 109 万条,每千条成本 = 1,200,000 / 1,090 ≈ 1100 credit。若不去扣空字段,把 8 万次空请求也摊进分母,单位成本会被低估约 7%。这个误差在月度账单上就是一笔对不上的差额,也是「账单对不上业务量」最常见的来源。 ## 八、云上重点:分批与背压 内存不会报错,它只会慢慢变慢。一次性把 10 万个 asin 塞进数组再 `map`,前几万条跑得飞快,后面 GC 压力上来,延迟无声无息翻倍,你却收不到任何异常。 分批加背压:每批 500 个,一批完成再取下一批;用 `p-queue` 的并发与速率上限做全局闸门。内存占用有上限,延迟曲线平稳,发布中断时丢失的也只是当前这一小批。 ```ts const BATCH = 500; const pending: Row[] = []; for (let i = 0; i runOne(a, pending))); await checkpoint(i + slice.length); // 记录断点,重启从这里续跑 } ``` 分批和 checkpoint 一起,把「重跑重复行」「中断丢数据」「内存悄悄变慢」三个问题一次性压住。 背压的另一层含义是给下游留缓冲。如果你的 upsert 走的是同一张热表,下游写入成为瓶颈时,上游还在一个劲生产,内存里的 `pending` 数组会越积越大。用 `p-queue` 的并发上限天然限制了在途请求数,再配合每批固定大小,内存占用就被钉死在一个可预测的值上。云上监控看的是这条内存曲线是否平稳,而不是有没有报错——内存不会主动抛错,它只会让延迟曲线慢慢爬升,等你注意到时,已经慢了一整天。 ## 九、上云治理清单(可勾选) 把上文收成一张可勾选的清单: - 指纹:优先把 TLS 与 HTTP/2 指纹交给服务端,不在客户端逐库维护; - 并发:HTTP/2 下用 `maxConcurrentStreams` 看上限,`p-queue` 的 `intervalCap` 控速率; - 幂等:主键四要素 + `ON CONFLICT`,重跑不增行; - 停机:SIGTERM 后 flush,`terminationGracePeriodSeconds` 大于单块耗时; - 日志:`traceId/attempt/errorName/durationMs/gate` 结构化落库,按天聚合; - 成本:埋 `itemCount`,按每千条可用记录结算,空字段要扣。 这六条都做到,账单才会和业务量对齐,发布窗口也不再悄悄丢数据。 ## 十、把治理收口到一次请求的价格里 把上面这些碎片拼起来,你会发现贵的那部分不在代码,而是维持指纹、IP、TLS 一致性的持续投入:住宅与移动 IP 出口与轮换、TLS 与 HTTP/2 指纹、浏览器指纹一致性、需要时的 JS 渲染、验证码与拦截页的服务端处理与重试、地域与邮区对齐。每一项自己维护都要长期投入。 Pangolinfo的口径是:一次请求的费用包含一切,上述项目都不作为加价项。你发一个请求,收一份结构化实时 JSON。中位延迟约 3 秒,成功率 99%,日调用 3000 万以上,SP 广告位采集率跨 13 个市场 91.4%。 当你把成本埋点接到这套全包口径上,月底那张对不上的账单就消失了——你付的是「每千条可用记录」的钱,而不是「每次调用」的账。 接入文档参考Pangolinfo开发者文档

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册