给出 Node.js 使用 axios 发送 5 k 文档批量写入的代码并处理 413 错误。
解读
国内线上环境常把 Nginx/Caddy 放在 CouchDB 前面,默认 client_max_body_size 仅 1 MB,5 k 文档 × 单条 2 kB 就能触发 413 Payload Too Large。面试想考察:
- 是否知道 CouchDB 的 /_bulk_docs 接口及 new_edits=false 语义
- 能否把超大 payload 拆分为可配置批次并在 413 时自动降容
- 是否熟悉 axios 的 validateStatus 与 retry 策略
- 是否兼顾 内存流控(避免一次性读 5 k 文档进内存)与 并发安全(防止同一条文档并发写冲突)
知识点
- CouchDB 批量写入:POST /{db}/_bulk_docs,请求体 {"docs":[...]},返回数组带 id/rev/error
- 413 触发链:客户端 → 反向代理 → CouchDB httpd/max_http_request_size(64 MB 默认)
- axios 错误拦截:error.response.status === 413
- 批次策略:二进制搜索法(batch/=2)比固定折半更快收敛到可接受体积
- 并发控制:p-limit 或 p-queue 做 maxConcurrency 限流,防止瞬间打满文件描述符
- 重试退避:结合 Exponential Back-off 与 jitter,避免惊群
- 事务补偿:对 413 拆分后需 幂等写入(带 _rev)或 new_edits=false 去重
- 监控埋点:记录 实际写入批次大小、耗时、重试次数,方便后续调优
答案
// bulk5k.js
const axios = require('axios').default;
const pLimit = require('p-limit');
const { Readable } = require('stream');
const DB = 'http://couchdb:5984/products';
const AUTH = { username: 'admin', password: 'admin' };
const MAX_RETRIES = 4;
const CONCURRENCY = 6;
// 模拟 5 k 条文档的可读流,避免一次性占内存
function* docGenerator() {
for (let i = 0; i < 5000; i++) {
yield {
_id: `item_${i}`,
type: 'product',
name: `商品${i}`,
price: Math.floor(Math.random() * 1000),
// 故意把单条体积撑到 ~2 kB
desc: 'x'.repeat(1800)
};
}
}
// 把流切成指定大小的批次
async function* batchStream(iter, size) {
let batch = [];
for (const doc of iter) {
batch.push(doc);
if (batch.length === size) {
yield batch;
batch = [];
}
}
if (batch.length) yield batch;
}
// 带 413 退避的写入函数
async function bulkInsert(docs, retry = 0) {
try {
const { data } = await axios.post(
`${DB}/_bulk_docs`,
{ docs },
{
auth: AUTH,
headers: { 'Content-Type': 'application/json' },
// 把 413 当成可重试状态,不让 axios 抛错
validateStatus: s => s < 500 || s === 413
}
);
if (data.error === 'too_large' || data.status === 413) throw new Error('413');
return data; // 返回 [{id,rev,error},...]
} catch (e) {
if (e.message.includes('413') && retry < MAX_RETRIES) {
const nextSize = Math.ceil(docs.length / 2);
console.warn(`[413] 降容至 ${nextSize} 条`);
const limit = pLimit(CONCURRENCY);
const chunks = Array.from({ length: 2 }, (_, i) =>
docs.slice(i * nextSize, (i + 1) * nextSize)
).filter(Boolean);
const results = await Promise.all(
chunks.map(c => limit(() => bulkInsert(c, retry + 1)))
);
return results.flat();
}
throw e;
}
}
// 主流程
(async () => {
const limit = pLimit(CONCURRENCY);
const gen = docGenerator();
let ok = 0, fail = 0;
for await (const batch of batchStream(gen, 800)) { // 先按 800 条试探
const res = await bulkInsert(batch);
res.forEach(r => (r.error ? fail++ : ok++));
}
console.log(`写入完成:成功 ${ok},失败 ${fail}`);
})();
运行前务必:
- 调大反向代理 client_max_body_size 20m
- 确认 CouchDB 本地 [httpd] max_http_request_size = 67108864
- 若使用 new_edits=false,需提前准备 _rev 避免冲突
拓展思考
- 如果 5 k 文档里 单条就超过 4 MB,413 无法通过拆批次解决,需要 GridFS 或附件外链方案
- 生产环境可改用 couchdb-bulk-follow 官方库,内部已集成 413 退避与流控
- 对实时性要求高的场景,可把大拆小后写入 Redis 队列,再由消费者异步刷盘,降低前端延迟
- 面试可继续追问:“如何验证写入一致性?” —— 通过 /{db}/_all_docs?keys=... 批量回查,比对 rev 与数量;或开启 /_ensure_full_commit 做 fsync 落盘确认