<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>New-Api on Ambertu</title><link>https://ambertu.com/tags/new-api/</link><description>Recent content in New-Api on Ambertu</description><generator>Hugo</generator><language>zh-CN</language><copyright>2026 Ambertu · 粤ICP备18071259号</copyright><lastBuildDate>Fri, 04 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://ambertu.com/tags/new-api/index.xml" rel="self" type="application/rss+xml"/><item><title>大模型 API 中转站怎么选：直连、自建、用现成的，三条路的真实成本</title><link>https://ambertu.com/posts/ai-api-relay-how-to-choose/</link><pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate><guid>https://ambertu.com/posts/ai-api-relay-how-to-choose/</guid><description>调用大模型 API 有三条路：直连官方、自己搭网关、用现成的中转站。本文把三条路的真实成本拆开算——不只是钱，还有你要为此承担的运维责任。文末介绍我自己运营的 aizhuan.fun，一个偏向 AI 图像与视频生产的中转站。</description><content:encoded><![CDATA[<blockquote>
<p><strong>利益披露</strong>：本文最后介绍的中转站是我自己在运营的。前面三分之二的取舍分析对任何人都成立，
你完全可以看完分析之后选择另外两条路——那也是这篇文章想说清楚的事。</p>
</blockquote>
<p>调用大模型 API 有三条路，大多数人只比较了价格，然后在别的地方付出了没预料到的成本。</p>
<h2 id="三条路的取舍">三条路的取舍</h2>
<h3 id="路一直连官方">路一：直连官方</h3>
<p>最干净的一条，也是最应该优先考虑的。你和供应商之间没有中间人，出问题时责任边界清晰，
额度、限速、模型上线时间都以官方为准。</p>
<p><strong>它的实际成本不在价格上，而在这几处</strong>：</p>
<ul>
<li><strong>支付方式。</strong> 需要能付美元的卡，且部分供应商对特定地区的支付有限制。</li>
<li><strong>网络可达性。</strong> 从国内直连的延迟和稳定性，取决于你的出口线路，而不是供应商。</li>
<li><strong>多供应商的碎片化。</strong> 一旦你同时用 OpenAI、Google、Anthropic，就要维护三套 SDK、
三套密钥、三套计费口径。这个成本随供应商数量线性增长，而且很容易被低估。</li>
</ul>
<h3 id="路二自己搭网关">路二：自己搭网关</h3>
<p>用 new-api、one-api 这类开源网关，把多家供应商收敛成一个 OpenAI 兼容接口。
我自己就是这么做的，前面几篇文章都在写这件事。</p>
<p><strong>它解决了碎片化，但把运维责任整个搬到了你身上</strong>：</p>
<table>
	<thead>
			<tr>
					<th>你要负责的事</th>
					<th>做错的后果</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>反向代理的流式配置</td>
					<td>流式输出变成一次性返回，用户体验直接崩</td>
			</tr>
			<tr>
					<td>超时分层与重试策略</td>
					<td>一次 504 触发客户端重试，账单翻倍</td>
			</tr>
			<tr>
					<td>限流的真实 IP 判定</td>
					<td>按可伪造的头限流，等于没限</td>
			</tr>
			<tr>
					<td>TLS 证书生命周期</td>
					<td>过期后所有 SDK 客户端一起硬失败</td>
			</tr>
			<tr>
					<td>数据库与日志保留</td>
					<td>用量表无限增长，半年后磁盘满、服务停摆</td>
			</tr>
			<tr>
					<td>上游账号与额度管理</td>
					<td>某个 key 额度耗尽，整条链路静默失败</td>
			</tr>
	</tbody>
</table>
<p>这张表里的每一行，我都单独写过一篇文章，因为每一行我都实际踩过。
如果你打算走这条路，那几篇可以直接拿去用：
<a href="/posts/new-api-binary-vs-docker/">部署形态</a>、
<a href="/posts/llm-api-sse-proxy-buffering/">流式缓冲</a>、
<a href="/posts/llm-api-timeout-retry-double-billing/">超时与重复计费</a>、
<a href="/posts/api-gateway-real-ip-ratelimit/">限流与真实 IP</a>、
<a href="/posts/caddy-vs-nginx-api-gateway-tls/">证书自动化</a>、
<a href="/posts/api-gateway-database-choice-small-vps/">数据库选型</a>。</p>
<p><strong>什么时候值得自建</strong>：你有稳定的上游渠道、有运维精力、且用量大到中转的差价能覆盖你的时间成本。
<strong>什么时候不值得</strong>：你只是想赶紧把功能做出来，网关不是你的核心业务。</p>
<h3 id="路三用现成的中转站">路三：用现成的中转站</h3>
<p>省掉了运维，代价是<strong>你把可用性、计费准确性和数据流向都托付给了运营方</strong>。</p>
<p>这条路最大的问题不是价格，是<strong>你没有办法验证对方说的话</strong>。
几乎每个中转站都说自己&quot;稳定、快速、官方直连&quot;，但这些词没有一个是可证伪的。
所以选中转站真正该看的是：</p>
<ol>
<li><strong>它敢不敢说自己不做什么。</strong> 只讲优点的介绍页，等于没有信息量。</li>
<li><strong>出问题时有没有人。</strong> 售后渠道是否真实存在、响应大概多久。</li>
<li><strong>计费是否可对账。</strong> 你能不能自己拉到用量明细，而不是只看一个余额数字。</li>
<li><strong>技术细节经不经得起问。</strong> 流式是不是真流式、超时怎么配的、限流按什么算——
这些问题问出去，答不上来的多半没认真做过。</li>
</ol>
<!-- 图 1：三条路的成本结构对比图。建议内容：横轴是"用量规模"，纵轴是"总成本（金钱+时间）"，
     三条曲线交叉，标出各自的拐点。或者简单点：三列对照表的可视化。 -->
<h2 id="我自己那个站">我自己那个站</h2>
<p>前面说的第四条——&ldquo;技术细节经不经得起问&rdquo;——我把答案提前写好了。</p>
<p><a href="https://aizhuan.fun"><strong>aizhuan.fun</strong></a> 是我自己搭的 new-api 实例。它跑在首尔的一台机器上，
用的是裸二进制 + Caddy + 本机 MariaDB 的组合，不是 Docker。
这个选择的理由、以及上面那张表里六行的具体做法，都在前面那六篇文章里写着——
<strong>你可以拿那些文章当作对这个站的技术审查材料</strong>，这也是我写它们的原因之一。</p>
<h3 id="名字的意思aizhuan--ai--赚">名字的意思：aizhuan = AI + 赚</h3>
<p>这个站从一开始就不是给「研究大模型」的人准备的，而是给<strong>用大模型干活、并且要靠产出换钱</strong>的人准备的。</p>
<p>这解释了它的模型清单为什么长成那样——五个分组里三个是生图和视频，而不是清一色的文本对话模型：</p>
<table>
	<thead>
			<tr>
					<th>你实际在做的事</th>
					<th>会用到的</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>电商主图、详情页配图、商品换背景</td>
					<td>gpt-image-2 系列、gemini-3.x 生图、<code>2k生图</code> / <code>4k生图</code> 分组</td>
			</tr>
			<tr>
					<td>小说插画、角色设定、同人图</td>
					<td>NAI diffusion 系列、Stable Diffusion 系列</td>
			</tr>
			<tr>
					<td>短视频素材、分镜、动态封面</td>
					<td>doubao-seedance、grok-imagine-video、<code>SD视频</code> 分组</td>
			</tr>
			<tr>
					<td>图文内容批量生产</td>
					<td>GPT-5.x 系列配合生图模型串起来调</td>
			</tr>
			<tr>
					<td>自己做工具再卖给别人</td>
					<td>OpenAI 兼容接口，客户端改个 <code>base_url</code> 就能接</td>
			</tr>
	</tbody>
</table>
<p>分辨率分组（2k / 4k）单独拆出来也是同一个逻辑：<strong>做商用素材的人在乎能不能直接交付</strong>，
而不是能不能出个缩略图看看效果。</p>
<p>需要说清楚的一句：<strong>这个站提供的是工具和算力，不是收入。</strong>
能不能赚到钱取决于你做什么、做得怎么样、有没有出货渠道——这三件事都和 API 没关系。
任何把「接入某个 API 就能赚钱」当卖点的说法都不要信，包括从我这里听到的。
这个品类里充斥着承诺收入的宣传，我不打算加入它们。</p>
<p>如果你的需求是纯文本对话或者代码补全，市面上专做文本的中转可能更划算；
这里的模型覆盖是往内容生产那一侧偏的。</p>
<h3 id="功能一一个接口替掉一堆-sdk">功能一：一个接口，替掉一堆 SDK</h3>
<p>这是中转站最实在的价值，也是最容易被&quot;模型多&quot;这种话术盖过去的一点。</p>
<figure>
<svg viewBox="0 0 900 300" role="img" aria-label="对比图：不用中转时应用需要分别对接 OpenAI、Google、xAI、字节、Stable Diffusion 五家供应商，各有各的 SDK、密钥和计费口径；用中转时应用只需改一行 base_url，通过一个 OpenAI 兼容接口访问文本、图像、视频三类产出" style="width:100%;height:auto;color:currentColor">
  <defs>
    <marker id="u1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M 0 1 L 10 5 L 0 9 z" fill="currentColor"/>
    </marker>
  </defs>
  <text x="0" y="14" font-size="10.5" font-family="monospace" fill="currentColor" opacity="0.6">不用中转</text>
  <line x1="430" y1="24" x2="430" y2="286" stroke="currentColor" stroke-width="1" opacity="0.25" stroke-dasharray="3 5"/>
  <g font-family="monospace" font-size="10.5" fill="currentColor">
    <rect x="10" y="118" width="100" height="44" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="60" y="145" text-anchor="middle">你的应用</text>
    <rect x="240" y="8"   width="176" height="32" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="252" y="28">OpenAI</text>
    <rect x="240" y="52"  width="176" height="32" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="252" y="72">Google Gemini</text>
    <rect x="240" y="96"  width="176" height="32" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="252" y="116">xAI Grok</text>
    <rect x="240" y="140" width="176" height="32" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="252" y="160">字节 Seedance</text>
    <rect x="240" y="184" width="176" height="32" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="252" y="204">Stable Diffusion / NAI</text>
    <line x1="110" y1="136" x2="236" y2="26"  stroke="currentColor" stroke-width="1.1" opacity="0.55" marker-end="url(#u1)"/>
    <line x1="110" y1="138" x2="236" y2="70"  stroke="currentColor" stroke-width="1.1" opacity="0.55" marker-end="url(#u1)"/>
    <line x1="110" y1="140" x2="236" y2="114" stroke="currentColor" stroke-width="1.1" opacity="0.55" marker-end="url(#u1)"/>
    <line x1="110" y1="144" x2="236" y2="158" stroke="currentColor" stroke-width="1.1" opacity="0.55" marker-end="url(#u1)"/>
    <line x1="110" y1="146" x2="236" y2="202" stroke="currentColor" stroke-width="1.1" opacity="0.55" marker-end="url(#u1)"/>
  </g>
  <text x="10" y="252" font-size="10" font-family="monospace" fill="currentColor" font-weight="bold">5 套 SDK · 5 个密钥 · 5 种计费口径 · 5 种错误格式</text>
  <text x="10" y="272" font-size="10" font-family="monospace" fill="currentColor" opacity="0.7">任何一家改接口或涨价，你都要跟着改代码</text>
  <text x="452" y="14" font-size="10.5" font-family="monospace" fill="currentColor" opacity="0.6">用中转</text>
  <g font-family="monospace" font-size="10.5" fill="currentColor">
    <rect x="452" y="118" width="100" height="44" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="502" y="145" text-anchor="middle">你的应用</text>
    <line x1="552" y1="140" x2="594" y2="140" stroke="currentColor" stroke-width="1.4" marker-end="url(#u1)"/>
    <text x="573" y="122" font-size="9" text-anchor="middle" opacity="0.75">改一行</text>
    <text x="573" y="133" font-size="9" text-anchor="middle" opacity="0.75">base_url</text>
    <rect x="596" y="110" width="160" height="60" fill="currentColor" fill-opacity="0.1" stroke="currentColor" stroke-width="1.6"/>
    <text x="676" y="134" text-anchor="middle" font-weight="bold">aizhuan.fun</text>
    <text x="676" y="152" text-anchor="middle" font-size="9" opacity="0.75">OpenAI 兼容</text>
    <rect x="800" y="76"  width="88" height="32" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="844" y="96"  text-anchor="middle" font-size="10">文本</text>
    <rect x="800" y="124" width="88" height="32" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="844" y="144" text-anchor="middle" font-size="10">图像</text>
    <rect x="800" y="172" width="88" height="32" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="844" y="192" text-anchor="middle" font-size="10">视频</text>
    <line x1="756" y1="134" x2="796" y2="94"  stroke="currentColor" stroke-width="1.1" opacity="0.55" marker-end="url(#u1)"/>
    <line x1="756" y1="140" x2="796" y2="140" stroke="currentColor" stroke-width="1.1" opacity="0.55" marker-end="url(#u1)"/>
    <line x1="756" y1="146" x2="796" y2="186" stroke="currentColor" stroke-width="1.1" opacity="0.55" marker-end="url(#u1)"/>
  </g>
  <text x="452" y="252" font-size="10" font-family="monospace" fill="currentColor" font-weight="bold">1 个接口 · 1 个密钥 · 1 份账单 · 53 个模型</text>
  <text x="452" y="272" font-size="10" font-family="monospace" fill="currentColor" opacity="0.7">换模型只改一个字符串，不动代码结构</text>
</svg>
<figcaption>
左边那五条线不是画着好看——<strong>它们代表你要长期维护的五套东西</strong>：五份文档、五个密钥的轮换、
五种限流策略、五套错误码的兼容代码。中转站把这五条线收敛成一条，代价是你多了一个依赖方。
这个交换划不划算，取决于你打算同时用几家。
</figcaption>
</figure>
<h3 id="功能二53-个模型覆盖三类产出">功能二：53 个模型，覆盖三类产出</h3>
<figure>
<svg viewBox="0 0 880 370" role="img" aria-label="模型能力全景：文本对话包含 GPT-5.2 到 5.6 系列与 codex；图像生成包含 gpt-image 系列、gemini 系列、grok-imagine 与 Stable Diffusion、NAI；视频生成包含 doubao-seedance、grok-imagine-video 与 SD 视频。合计 53 个模型，分属 openai、openai-video、image-generation 三类端点" style="width:100%;height:auto;color:currentColor">
  <text x="0" y="14" font-size="10.5" font-family="monospace" fill="currentColor" opacity="0.6">按「你要交付什么」组织，而不是按模型热度</text>
  <g font-family="monospace" font-size="10" fill="currentColor">
    <rect x="0" y="28" width="278" height="252" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <rect x="0" y="28" width="278" height="30" fill="currentColor" fill-opacity="0.09"/>
    <text x="14" y="48" font-size="11.5" font-weight="bold">文本对话 · 代码</text>
    <text x="14" y="80">GPT-5.6（luna / sol / terra）</text>
    <text x="14" y="102">GPT-5.5</text>
    <text x="14" y="124">GPT-5.4 · 5.4-mini</text>
    <text x="14" y="146">GPT-5.2</text>
    <text x="14" y="168">gpt-5.3-codex-spark</text>
    <text x="14" y="190">codex-auto-review</text>
    <text x="14" y="212" opacity="0.7">各系列另有 openai-compact 变体</text>
    <text x="14" y="252" opacity="0.6">分组</text>
    <text x="14" y="268" font-weight="bold">自建号池plus · 官方号池plus</text>
    <rect x="298" y="28" width="278" height="252" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <rect x="298" y="28" width="278" height="30" fill="currentColor" fill-opacity="0.09"/>
    <text x="312" y="48" font-size="11.5" font-weight="bold">图像生成</text>
    <text x="312" y="80">gpt-image-1 · 1.5 · 2 · 2-plus</text>
    <text x="312" y="102">gemini-3-pro-image</text>
    <text x="312" y="124">gemini-3.1-flash-image</text>
    <text x="312" y="146">gemini-2.5-flash-image</text>
    <text x="312" y="168">grok-imagine-image（含 edit）</text>
    <text x="312" y="190">Stable Diffusion 2 / 2.5 系列</text>
    <text x="312" y="212">nai-diffusion-4.5-full</text>
    <text x="312" y="252" opacity="0.6">分组</text>
    <text x="312" y="268" font-weight="bold">2k生图 · 4k生图</text>
    <rect x="596" y="28" width="284" height="252" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <rect x="596" y="28" width="284" height="30" fill="currentColor" fill-opacity="0.09"/>
    <text x="610" y="48" font-size="11.5" font-weight="bold">视频生成</text>
    <text x="610" y="80">doubao-seedance-2.0 · 2.5</text>
    <text x="610" y="102">doubao-seedance-2.0-mini</text>
    <text x="610" y="124">grok-imagine-video · 1.5</text>
    <text x="610" y="146">first_frame（首帧控制）</text>
    <text x="610" y="168">first_last_frame（首尾帧）</text>
    <text x="610" y="190" opacity="0.7">异步任务，出片后回调</text>
    <text x="610" y="252" opacity="0.6">分组</text>
    <text x="610" y="268" font-weight="bold">SD视频</text>
  </g>
  <line x1="0" y1="300" x2="860" y2="300" stroke="currentColor" stroke-width="1" opacity="0.25"/>
  <g font-family="monospace" font-size="10.5" fill="currentColor">
    <text x="0" y="322">端点类型</text>
    <text x="110" y="322" font-weight="bold">openai 42</text>
    <text x="250" y="322" font-weight="bold">openai-video 11</text>
    <text x="430" y="322" font-weight="bold">image-generation 6</text>
    <text x="650" y="322" opacity="0.7">合计 53 个模型</text>
  </g>
  <text x="0" y="348" font-size="10" font-family="monospace" fill="currentColor" opacity="0.65">同一能力有多个变体（preview / -c 后缀 / 分辨率分组），是为了在质量、速度、成本之间留选择</text>
  <text x="0" y="366" font-size="10" font-family="monospace" fill="currentColor" opacity="0.65">数据取自站点公开的 /api/pricing 接口，可自行核对</text>
</svg>
<figcaption>
分辨率单独拆成 <code>2k生图</code> 和 <code>4k生图</code> 两个分组，是这份清单里最能说明定位的细节——
<strong>做商用素材的人要的是能直接交付的成品尺寸</strong>，而不是先出个缩略图看效果。
视频走异步任务队列，提交后回调取结果，这是视频生成必须的形态。
</figcaption>
</figure>
<!-- 图（待你提供）：站点模型列表页截图，展示分组切换和模型数量。
     注意打码：API key、用户邮箱、余额数字全部要遮掉。 -->
<h3 id="功能三不只是裸-api">功能三：不只是裸 API</h3>
<p>如果只有 API，那和自己搭一个没区别。实际能省事的是这些配套：</p>
<table>
	<thead>
			<tr>
					<th>功能</th>
					<th>具体规格</th>
					<th>解决什么</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Image Studio</strong></td>
					<td>独立图形界面 <a href="https://studio.aizhuan.fun">studio.aizhuan.fun</a></td>
					<td>不写代码也能出图，团队里非技术成员可直接用</td>
			</tr>
			<tr>
					<td><strong>表单转提示词</strong></td>
					<td>填表单 → 自动改写成生图/视频提示词</td>
					<td>不用背 prompt 写法，降低上手门槛</td>
			</tr>
			<tr>
					<td><strong>参考图</strong></td>
					<td>单次最多 <strong>9 张</strong></td>
					<td>图生图、垫图、风格迁移、角色一致性</td>
			</tr>
			<tr>
					<td><strong>文档解析</strong></td>
					<td>最大 <strong>20 MB / 5 万字符</strong></td>
					<td>长文档喂给模型，不用自己切分</td>
			</tr>
			<tr>
					<td><strong>生成图保留</strong></td>
					<td><strong>7 天</strong></td>
					<td>出图后不用立刻下载，但也别当网盘用</td>
			</tr>
			<tr>
					<td><strong>用量数据导出</strong></td>
					<td>可导出，最小粒度<strong>按小时</strong></td>
					<td>自己对账，不用只看一个余额数字</td>
			</tr>
			<tr>
					<td><strong>可用性监控</strong></td>
					<td>接了 Uptime Kuma</td>
					<td>站点状态对外可查，不是嘴上说稳定</td>
			</tr>
			<tr>
					<td><strong>用量排行榜</strong></td>
					<td>日榜 / 周榜</td>
					<td>看别人在用什么模型，选型时有参考</td>
			</tr>
	</tbody>
</table>
<p>第六行值得单独说：前面我列过选中转站要看的四条，其中一条是**「计费是否可对账」——
你能不能自己拉到用量明细，而不是只看一个余额数字**。按小时粒度的数据导出就是对这条的回答。
这不是我事后找补的说法，那四条标准和这个功能都在，你可以自己对。</p>
<!-- 图（待你提供）：Image Studio 的实际出图界面，或一组由站上模型生成的图像样例。
     这是唯一能直观说明"图像能力"的东西，比任何文字描述都有效。 -->
<h3 id="功能四客户端一键接入">功能四：客户端一键接入</h3>
<p>不想写代码的话，主流 AI 客户端可以直接一键导入配置，不用手动填 base_url 和 key：</p>
<figure>
<svg viewBox="0 0 900 210" role="img" aria-label="四步接入流程：注册获得 1 美元额度、创建 API key、客户端一键导入或手动改 base_url、开始调用。支持一键导入的客户端包括 Cherry Studio、CC Switch、DeepChat、Lobe Chat、AionUI、OpenCat、AMA 问天、AI as Workspace、流畅阅读" style="width:100%;height:auto;color:currentColor">
  <defs>
    <marker id="s1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M 0 1 L 10 5 L 0 9 z" fill="currentColor"/>
    </marker>
  </defs>
  <g font-family="monospace" font-size="10.5" fill="currentColor">
    <rect x="0" y="30" width="170" height="56" fill="currentColor" fill-opacity="0.1" stroke="currentColor" stroke-width="1.4"/>
    <text x="85" y="54" text-anchor="middle" font-weight="bold">① 注册</text>
    <text x="85" y="72" text-anchor="middle" font-size="9.5">送 $1 额度</text>
    <line x1="170" y1="58" x2="228" y2="58" stroke="currentColor" stroke-width="1.3" marker-end="url(#s1)"/>
    <rect x="230" y="30" width="170" height="56" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="315" y="54" text-anchor="middle" font-weight="bold">② 创建 key</text>
    <text x="315" y="72" text-anchor="middle" font-size="9.5">选分组、设额度</text>
    <line x1="400" y1="58" x2="458" y2="58" stroke="currentColor" stroke-width="1.3" marker-end="url(#s1)"/>
    <rect x="460" y="30" width="200" height="56" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="560" y="54" text-anchor="middle" font-weight="bold">③ 接入</text>
    <text x="560" y="72" text-anchor="middle" font-size="9.5">一键导入 或 改 base_url</text>
    <line x1="660" y1="58" x2="718" y2="58" stroke="currentColor" stroke-width="1.3" marker-end="url(#s1)"/>
    <rect x="720" y="30" width="170" height="56" fill="none" stroke="currentColor" stroke-width="1.3"/>
    <text x="805" y="54" text-anchor="middle" font-weight="bold">④ 调用</text>
    <text x="805" y="72" text-anchor="middle" font-size="9.5">先用 $1 验证</text>
  </g>
  <line x1="0" y1="106" x2="880" y2="106" stroke="currentColor" stroke-width="1" opacity="0.25"/>
  <text x="0" y="128" font-size="10.5" font-family="monospace" fill="currentColor" opacity="0.6">③ 支持一键导入的客户端</text>
  <g font-family="monospace" font-size="10" fill="currentColor">
    <rect x="0"   y="140" width="132" height="26" fill="none" stroke="currentColor" stroke-width="1.1"/>
    <text x="66"  y="157" text-anchor="middle">Cherry Studio</text>
    <rect x="144" y="140" width="132" height="26" fill="none" stroke="currentColor" stroke-width="1.1"/>
    <text x="210" y="157" text-anchor="middle">CC Switch</text>
    <rect x="288" y="140" width="132" height="26" fill="none" stroke="currentColor" stroke-width="1.1"/>
    <text x="354" y="157" text-anchor="middle">DeepChat</text>
    <rect x="432" y="140" width="132" height="26" fill="none" stroke="currentColor" stroke-width="1.1"/>
    <text x="498" y="157" text-anchor="middle">Lobe Chat</text>
    <rect x="576" y="140" width="132" height="26" fill="none" stroke="currentColor" stroke-width="1.1"/>
    <text x="642" y="157" text-anchor="middle">AionUI</text>
    <rect x="720" y="140" width="132" height="26" fill="none" stroke="currentColor" stroke-width="1.1"/>
    <text x="786" y="157" text-anchor="middle">OpenCat</text>
    <rect x="0"   y="172" width="132" height="26" fill="none" stroke="currentColor" stroke-width="1.1"/>
    <text x="66"  y="189" text-anchor="middle">AMA 问天</text>
    <rect x="144" y="172" width="180" height="26" fill="none" stroke="currentColor" stroke-width="1.1"/>
    <text x="234" y="189" text-anchor="middle">AI as Workspace</text>
    <rect x="336" y="172" width="132" height="26" fill="none" stroke="currentColor" stroke-width="1.1"/>
    <text x="402" y="189" text-anchor="middle">流畅阅读</text>
    <text x="486" y="189" opacity="0.7">另有 Windows / Mac 客户端与配置教程</text>
  </g>
</svg>
<figcaption>
一键导入走的是各客户端自己的协议链接（<code>cherrystudio://</code>、<code>aionui://</code> 之类），
点一下把 base_url 和 key 直接写进客户端，<strong>不用手抄密钥</strong>——手抄是这一步最常见的出错点。
不在列表里的客户端，改 <code>base_url</code> 手动填也一样能用，它是标准 OpenAI 兼容接口。
</figcaption>
</figure>
<h3 id="怎么开始">怎么开始</h3>
<ul>
<li><strong>直接在 <a href="https://aizhuan.fun">aizhuan.fun</a> 注册</strong>，不需要邀请码。</li>
<li><strong>注册即送 $1 额度</strong>，够把下面的接入示例跑通、把想用的模型挨个试一遍。</li>
<li><strong>充值支持微信和支付宝，没有起充门槛。</strong></li>
</ul>
<p>后两条正好对上前面提出的两个问题。</p>
<p>第三条对应「直连官方」那节说的支付摩擦——不需要能付美元的卡，这是选中转站相对直连的主要实际收益之一。</p>
<p>第二条对应更重要的那个：<strong>你没有办法验证一个中转站说的话。</strong>
所以别信我上面写的任何一句，用那 $1 自己验：流式是不是真流式、首字节多久、
你要的模型在不在、并发起来会不会掉。验完再决定要不要充钱。</p>
<p>一个中转站敢不敢让你先验证再付费，本身就是个信号。</p>
<!-- 图 4：注册到拿到 key 的流程截图（2-3 张拼一张即可）。
     或者一张最简单的调用示例截图（终端里 curl 成功返回）。 -->
<h3 id="接入示例">接入示例</h3>
<p>它是 OpenAI 兼容接口，绝大多数客户端和 SDK 改两行就能用：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">openai</span> <span class="kn">import</span> <span class="n">OpenAI</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAI</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">base_url</span><span class="o">=</span><span class="s2">&#34;https://aizhuan.fun/v1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">api_key</span><span class="o">=</span><span class="s2">&#34;你的 key&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_retries</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>   <span class="c1"># 按 token 计费的接口，务必显式关掉自动重试</span>
</span></span><span class="line"><span class="cl">    <span class="n">timeout</span><span class="o">=</span><span class="mf">600.0</span><span class="p">,</span>   <span class="c1"># 生成慢是正常的，别让客户端先放弃</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>那两行注释不是客套。<code>max_retries</code> 默认是 2，一次超时会让上游生成两次、计费两次——
<a href="/posts/llm-api-timeout-retry-double-billing/">这条链路我单独写过一篇</a>，
无论你用哪家中转都建议先看一眼。</p>
<h2 id="我不承诺什么">我不承诺什么</h2>
<p>这一节比上面所有内容都重要，因为它是可证伪的。</p>
<ul>
<li><strong>不承诺 100% 可用。</strong> 上游会抖、机房会抽风、模型会临时下线。
我能承诺的是出问题时有人在，以及事后会说清楚发生了什么。</li>
<li><strong>不承诺永远最便宜。</strong> 中转站的价格随上游成本浮动，任何&quot;永久最低价&quot;的说法都不可信。</li>
<li><strong>不承诺模型和官方完全同步。</strong> 新模型上线需要时间接入和测试。</li>
<li><strong>不承诺替代直连。</strong> 如果你有稳定的官方渠道和付款方式，直连仍然是更干净的选择——
这话写在推广文里看着奇怪，但它是真的。</li>
</ul>
<!-- 图 5（可选）：站点的实际可用性或用量统计截图。
     只有在数据真实且你愿意公开时才放；数据不好看就不要放，
     放一张修饰过的图比不放伤害大得多。 -->
<h2 id="最后">最后</h2>
<p>选哪条路取决于你在乎什么：</p>
<ul>
<li>在乎<strong>责任边界清晰</strong> → 直连官方</li>
<li>在乎<strong>掌控力，且愿意承担运维</strong> → 自建，那六篇文章拿去用</li>
<li>在乎<strong>尽快开始做正事</strong> → 用现成的，包括但不限于我这个</li>
</ul>
<p>三条路我都走过，所以这篇文章里没有一句&quot;中转站是唯一选择&quot;。
如果看完你决定自建，我反而挺高兴——那六篇文章就没白写。</p>
<h2 id="相关阅读">相关阅读</h2>
<ul>
<li><a href="/posts/new-api-binary-vs-docker/">new-api 部署选型：2G 小内存机器上裸二进制比 Docker 省多少</a></li>
<li><a href="/posts/llm-api-sse-proxy-buffering/">反代把大模型 API 的 SSE 流式响应缓冲了：五个缓冲点与三层排查法</a></li>
<li><a href="/posts/llm-api-timeout-retry-double-billing/">大模型 API 中转的超时与重试：一次 504 可能让你双倍计费</a></li>
</ul>
]]></content:encoded></item><item><title>给 API 中转站选数据库：SQLite / MySQL / PostgreSQL 在小机器上的取舍</title><link>https://ambertu.com/posts/api-gateway-database-choice-small-vps/</link><pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate><guid>https://ambertu.com/posts/api-gateway-database-choice-small-vps/</guid><description>API 中转站的写入量约等于请求量，这决定了数据库选型。本文给出一台 2G 机器上 MariaDB 占 285MB 的实测数据、三种数据库的内存画像与并发模型差异，以及一个比选型更要紧的问题：用量日志表的无限增长。</description><content:encoded><![CDATA[<p>new-api 这类中转程序通常同时支持 SQLite、MySQL 和 PostgreSQL，安装文档也就一句&quot;按需选择&quot;。但在一台 2G 内存的小机器上，这个选择是有真实代价的——<strong>光是数据库进程本身就可能吃掉可用内存的四分之一。</strong></p>
<p>先给一个锚点。我实测一台 2 核 2G 的云主机，上面跑着 nginx、Redis 和两个 Go 服务，MariaDB 的进程 RSS 是：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">mysql  14.5%  291932 KB  /usr/libexec/mariadbd
</span></span></code></pre></div><p><strong>285 MB</strong>，是这台机器上最大的单个进程。而整机 <code>available</code> 只有 1.1 G。换句话说，你还没开始跑业务，数据库已经拿走了四分之一的可用余量。</p>
<h2 id="选型的真正依据写入量--请求量">选型的真正依据：写入量 ≈ 请求量</h2>
<p>在动手比较之前，先想清楚 API 中转站的负载长什么样，否则比较是没有意义的。</p>
<ul>
<li><strong>读</strong>：API key 校验、用户配置、配额查询。数据量小、可缓存、频率高——<strong>三种数据库都轻松应对，不构成差异。</strong></li>
<li><strong>写</strong>：每次请求落一条用量/计费记录。<strong>这是全部的关键：写入速率基本等于请求速率。</strong></li>
</ul>
<p>所以问题可以化简成一句：<strong>你每秒有几次请求？</strong></p>
<table>
	<thead>
			<tr>
					<th>请求量</th>
					<th>每天写入行数</th>
					<th>SQLite 是否够用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1 req/s</td>
					<td>约 8.6 万</td>
					<td>够，且很轻松</td>
			</tr>
			<tr>
					<td>10 req/s</td>
					<td>约 86 万</td>
					<td>勉强够，需调好 WAL 和 busy_timeout</td>
			</tr>
			<tr>
					<td>50 req/s</td>
					<td>约 430 万</td>
					<td>不够，写入会明显排队</td>
			</tr>
			<tr>
					<td>多实例部署</td>
					<td>——</td>
					<td><strong>不行</strong>，SQLite 无法跨进程跨机共享</td>
			</tr>
	</tbody>
</table>
<p>大多数个人自建的中转站落在第一档，很多人却直接上了 MySQL——在小机器上这笔开销是白花的。</p>
<h2 id="三种数据库的内存画像">三种数据库的内存画像</h2>
<figure>
<svg viewBox="0 0 760 280" role="img" aria-label="三种数据库在 2G 机器上的内存占用对比条形图：SQLite 常驻内存接近零，MariaDB 实测 285MB 调优后约 150MB，PostgreSQL 由 shared_buffers 加每连接独立进程构成" style="width:100%;height:auto;color:currentColor">
  <text x="0" y="14" font-size="10.5" font-family="monospace" fill="currentColor" opacity="0.6">数据库常驻内存（2G 机器 · available ≈ 1126 MB）</text>
  <!-- reference line -->
  <line x1="170" y1="26" x2="170" y2="230" stroke="currentColor" stroke-width="1" opacity="0.25"/>
  <g font-family="monospace" font-size="10.5" fill="currentColor">
    <!-- sqlite -->
    <text x="0" y="56">SQLite</text>
    <rect x="170" y="44" width="6" height="18" fill="currentColor" opacity="0.75"/>
    <text x="186" y="57" opacity="0.8">≈ 0 MB 常驻</text>
    <text x="186" y="74" font-size="9" opacity="0.6">库跑在应用进程里，没有独立进程</text>
    <!-- mariadb -->
    <text x="0" y="112">MariaDB</text>
    <rect x="170" y="100" width="285" height="18" fill="currentColor" opacity="0.75"/>
    <text x="463" y="113" font-weight="bold">285 MB 实测</text>
    <rect x="170" y="122" width="150" height="12" fill="none" stroke="currentColor" stroke-width="1.2" stroke-dasharray="3 3"/>
    <text x="328" y="132" font-size="9" opacity="0.7">约 150 MB（关掉 performance_schema 并调小 buffer pool 后）</text>
    <!-- postgres -->
    <text x="0" y="176">PostgreSQL</text>
    <rect x="170" y="164" width="128" height="18" fill="currentColor" opacity="0.55"/>
    <text x="306" y="177" font-size="9.5" opacity="0.8">shared_buffers 128 MB（默认）</text>
    <rect x="170" y="186" width="40" height="12" fill="currentColor" opacity="0.3"/>
    <rect x="214" y="186" width="40" height="12" fill="currentColor" opacity="0.3"/>
    <rect x="258" y="186" width="40" height="12" fill="currentColor" opacity="0.3"/>
    <rect x="302" y="186" width="40" height="12" fill="currentColor" opacity="0.3"/>
    <text x="350" y="196" font-size="9" opacity="0.7">+ 每个连接一个独立进程，随连接数线性增长</text>
  </g>
  <line x1="0" y1="216" x2="740" y2="216" stroke="currentColor" stroke-width="1" opacity="0.2"/>
  <text x="0" y="238" font-size="10" font-family="monospace" fill="currentColor" opacity="0.75">只有 285 MB 那个数是实测值，其余为默认配置下的典型量级。</text>
  <text x="0" y="256" font-size="10" font-family="monospace" fill="currentColor" opacity="0.75">PostgreSQL 的总占用高度依赖 max_connections —— 它是进程模型，不是线程模型。</text>
  <text x="0" y="274" font-size="10" font-family="monospace" fill="currentColor" opacity="0.75">SQLite 仍会用到系统 page cache，但那部分是可回收的，不计入进程 RSS。</text>
</svg>
<figcaption>
PostgreSQL 那一行的画法是有意的：<strong>它的内存不是一个固定数字，而是「基础 + 连接数 × 每连接」</strong>。
在小机器上 <code>max_connections</code> 配错，比 <code>shared_buffers</code> 配错更容易出事。
</figcaption>
</figure>
<h2 id="并发写入模型的差异">并发写入模型的差异</h2>
<figure>
<svg viewBox="0 0 780 250" role="img" aria-label="对比图：SQLite 的写入被序列化为单一写者队列，并发写请求需要排队；MySQL 与 PostgreSQL 支持多个写者通过行级锁并发写入" style="width:100%;height:auto;color:currentColor">
  <defs>
    <marker id="d1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M 0 1 L 10 5 L 0 9 z" fill="currentColor"/>
    </marker>
  </defs>
  <text x="0" y="14" font-size="10.5" font-family="monospace" fill="currentColor" opacity="0.6">并发写请求的处理方式</text>
  <!-- sqlite -->
  <text x="0" y="42" font-size="10.5" font-family="monospace" fill="currentColor">SQLite</text>
  <g font-family="monospace" font-size="9.5" fill="currentColor">
    <rect x="100" y="30" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="44" text-anchor="middle">写 A</text>
    <rect x="100" y="54" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="68" text-anchor="middle">写 B</text>
    <rect x="100" y="78" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="92" text-anchor="middle">写 C</text>
    <path d="M 170 40 L 250 60" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <path d="M 170 64 L 250 64" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <path d="M 170 88 L 250 68" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <rect x="252" y="46" width="130" height="36" fill="currentColor" fill-opacity="0.12" stroke="currentColor" stroke-width="1.5"/>
    <text x="317" y="62" text-anchor="middle">单写者队列</text>
    <text x="317" y="75" text-anchor="middle" font-size="8.5" opacity="0.7">串行化</text>
    <line x1="382" y1="64" x2="446" y2="64" stroke="currentColor" stroke-width="1.2" marker-end="url(#d1)"/>
    <rect x="448" y="46" width="100" height="36" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="498" y="68" text-anchor="middle">db 文件</text>
    <text x="566" y="54" opacity="0.75">WAL 模式下读不阻塞，</text>
    <text x="566" y="68" opacity="0.75">但写仍然一个一个来。</text>
    <text x="566" y="82" font-weight="bold">busy_timeout 必须配</text>
  </g>
  <line x1="0" y1="116" x2="760" y2="116" stroke="currentColor" stroke-width="1" opacity="0.2"/>
  <!-- mysql/pg -->
  <text x="0" y="146" font-size="10.5" font-family="monospace" fill="currentColor">MySQL</text>
  <text x="0" y="160" font-size="10.5" font-family="monospace" fill="currentColor">PostgreSQL</text>
  <g font-family="monospace" font-size="9.5" fill="currentColor">
    <rect x="100" y="134" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="148" text-anchor="middle">写 A</text>
    <rect x="100" y="158" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="172" text-anchor="middle">写 B</text>
    <rect x="100" y="182" width="70" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="135" y="196" text-anchor="middle">写 C</text>
    <line x1="170" y1="144" x2="250" y2="144" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <line x1="170" y1="168" x2="250" y2="168" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <line x1="170" y1="192" x2="250" y2="192" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <rect x="252" y="134" width="130" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="317" y="148" text-anchor="middle" font-size="8.5">连接/工作进程</text>
    <rect x="252" y="158" width="130" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="317" y="172" text-anchor="middle" font-size="8.5">连接/工作进程</text>
    <rect x="252" y="182" width="130" height="20" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="317" y="196" text-anchor="middle" font-size="8.5">连接/工作进程</text>
    <line x1="382" y1="144" x2="446" y2="160" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <line x1="382" y1="168" x2="446" y2="168" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <line x1="382" y1="192" x2="446" y2="176" stroke="currentColor" stroke-width="1.1" marker-end="url(#d1)" opacity="0.6"/>
    <rect x="448" y="150" width="100" height="36" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="498" y="172" text-anchor="middle">行级锁</text>
    <text x="566" y="160" opacity="0.75">真并发写，但代价是</text>
    <text x="566" y="174" opacity="0.75">常驻内存和连接管理。</text>
  </g>
  <line x1="0" y1="216" x2="760" y2="216" stroke="currentColor" stroke-width="1" opacity="0.2"/>
  <text x="0" y="238" font-size="10" font-family="monospace" fill="currentColor" font-weight="bold">选型问题因此变成：你的写入速率会不会撞上 SQLite 的单写者上限。</text>
</svg>
<figcaption>
SQLite 的"单写者"经常被误解成"不能并发"。<strong>WAL 模式下读写是可以并发的</strong>，
受限的只有写与写之间。对写入速率等于请求速率的中转站来说，这个限制什么时候变成问题是可以直接算出来的。
</figcaption>
</figure>
<h2 id="sqlite如果选它这几个-pragma-不能少">SQLite：如果选它，这几个 pragma 不能少</h2>
<p>默认配置的 SQLite 在并发场景下表现很差，而这常被误当成&quot;SQLite 不能上生产&quot;的证据。实际要配的就几行：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">journal_mode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">WAL</span><span class="p">;</span><span class="w">      </span><span class="c1">-- 读写不再互相阻塞。默认的 delete 模式才是性能灾难
</span></span></span><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">busy_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5000</span><span class="p">;</span><span class="w">     </span><span class="c1">-- 遇锁等 5 秒而不是立刻报 SQLITE_BUSY
</span></span></span><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">synchronous</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">NORMAL</span><span class="p">;</span><span class="w">    </span><span class="c1">-- 配合 WAL 的合理折中
</span></span></span><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span><span class="w">       </span><span class="c1">-- 默认是关的，很多人不知道
</span></span></span><span class="line"><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">cache_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">-</span><span class="mi">32000</span><span class="p">;</span><span class="w">     </span><span class="c1">-- 负数表示 KB，这里约 32 MB
</span></span></span></code></pre></div><p>其中 <strong><code>busy_timeout</code> 是最容易漏、后果最直接的一条</strong>。不配它，两个并发写会有一个立刻收到 <code>SQLITE_BUSY</code> 失败——表现出来就是&quot;偶发写入失败&quot;，而你会以为是 SQLite 不稳定，其实只是它在如实告诉你&quot;锁被占了&quot;，而你没让它等。</p>
<p><code>synchronous = NORMAL</code> 的取舍要说清：配合 WAL 时它能抵御应用崩溃，但<strong>操作系统崩溃或断电可能丢掉最近几个已提交事务</strong>。对用量日志这种损失几条可接受的数据没问题；如果这张表直接用于扣款对账，就该用 <code>FULL</code>。</p>
<p><strong>备份不能直接 <code>cp</code>。</strong> WAL 模式下 <code>-wal</code> 文件里有还没合并进主库的数据，直接复制主文件会拿到一个不一致的快照：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ✓ 正确</span>
</span></span><span class="line"><span class="cl">sqlite3 /path/to/app.db <span class="s2">&#34;VACUUM INTO &#39;/backup/app-</span><span class="k">$(</span>date +%F<span class="k">)</span><span class="s2">.db&#39;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ❌ 错误：WAL 模式下这样拷出来的库可能缺最近的写入</span>
</span></span><span class="line"><span class="cl">cp /path/to/app.db /backup/
</span></span></code></pre></div><h2 id="mysql--mariadb小机器上的调优">MySQL / MariaDB：小机器上的调优</h2>
<p>如果已经在用 MySQL 系，285 MB 是可以压下来的：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[mysqld]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 关掉它就能省几十 MB，个人站几乎不需要</span>
</span></span><span class="line"><span class="cl"><span class="na">performance_schema</span> <span class="o">=</span> <span class="s">OFF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="na">innodb_buffer_pool_size</span> <span class="o">=</span> <span class="s">96M</span>
</span></span><span class="line"><span class="cl"><span class="na">innodb_buffer_pool_instances</span> <span class="o">=</span> <span class="s">1    # 池子小的时候拆成多个实例反而浪费</span>
</span></span><span class="line"><span class="cl"><span class="na">innodb_log_file_size</span> <span class="o">=</span> <span class="s">32M</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 每个连接都要分配缓冲区，这个数不是免费的</span>
</span></span><span class="line"><span class="cl"><span class="na">max_connections</span> <span class="o">=</span> <span class="s">30</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="na">key_buffer_size</span> <span class="o">=</span> <span class="s">8M</span>
</span></span><span class="line"><span class="cl"><span class="na">tmp_table_size</span> <span class="o">=</span> <span class="s">16M</span>
</span></span><span class="line"><span class="cl"><span class="na">max_heap_table_size</span> <span class="o">=</span> <span class="s">16M</span>
</span></span></code></pre></div><p><code>performance_schema = OFF</code> 是性价比最高的一条——它默认开启，为性能诊断收集大量数据，而个人站基本用不上。代价是出问题时少了一套诊断视图，需要时再临时打开即可。</p>
<p><code>max_connections = 30</code> 看着很小，但要算清楚：中转程序的连接池通常配 10–20，加上你偶尔用客户端连进去看数据，30 足够。这个数配成 151（默认值）不会立刻占用内存，但<strong>它决定了内存占用的上限</strong>，在没有 swap 的机器上等于把 OOM 的可能性留在了那里。</p>
<h2 id="postgresql注意它是进程模型">PostgreSQL：注意它是进程模型</h2>
<p>PostgreSQL 和 MySQL 最大的运维差异在这里：<strong>每个连接是一个独立的操作系统进程，不是线程。</strong> 所以连接数对内存的影响比 MySQL 显著得多。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">shared_buffers</span> <span class="o">=</span> <span class="s">128MB          # 通用建议是内存的 25%，但共享机器上别照抄</span>
</span></span><span class="line"><span class="cl"><span class="na">max_connections</span> <span class="o">=</span> <span class="s">20            # 进程模型下这个数很值钱</span>
</span></span><span class="line"><span class="cl"><span class="na">work_mem</span> <span class="o">=</span> <span class="s">2MB                  # 注意：单次查询可能同时用掉好几份</span>
</span></span><span class="line"><span class="cl"><span class="na">maintenance_work_mem</span> <span class="o">=</span> <span class="s">32MB</span>
</span></span><span class="line"><span class="cl"><span class="na">effective_cache_size</span> <span class="o">=</span> <span class="s">512MB    # 这只是给优化器的提示，不实际占用</span>
</span></span></code></pre></div><p><code>work_mem</code> 有个容易踩的坑：它是<strong>每个排序/哈希操作</strong>的上限，不是每个连接的上限。一个复杂查询里有多个排序节点时会各分配一份，并发多个这样的查询，内存就会超出直觉。小机器上宁可配小，让它落盘排序。</p>
<p>如果应用的连接池配得比较大，考虑在前面加 pgbouncer 做连接复用——但这又是一个常驻进程，在 2G 机器上要权衡它值不值。</p>
<h2 id="比选型更要紧的问题日志表会无限长大">比选型更要紧的问题：日志表会无限长大</h2>
<p>这是本文最想强调的一点。<strong>无论你选哪个数据库，真正会在半年后咬你的是用量日志表。</strong></p>
<p>机制很简单：每个请求写一行，从不删除。按前面的换算：</p>
<ul>
<li>10 req/s → 每天 86 万行 → <strong>一年约 3.1 亿行</strong></li>
<li>每行假设 300 字节 → 一年约 90 GB</li>
</ul>
<p>后果不是&quot;磁盘满了&quot;这么简单，而是一连串的连带效应：查询变慢 → 管理后台的用量统计页面开始超时 → 备份时间越来越长 → 最后磁盘真的满了，而磁盘满会让数据库直接拒绝写入，<strong>整个中转站停摆</strong>。</p>
<p>所以从第一天就该有保留策略：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 最朴素但有效：定期删除旧数据 + 保留聚合结果
</span></span></span><span class="line"><span class="cl"><span class="k">DELETE</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">logs</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="mi">90</span><span class="w"> </span><span class="k">DAY</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 删之前先把要长期保留的口径聚合出来
</span></span></span><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">usage_daily</span><span class="w"> </span><span class="p">(</span><span class="k">day</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="n">requests</span><span class="p">,</span><span class="w"> </span><span class="n">tokens</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="nb">DATE</span><span class="p">(</span><span class="n">created_at</span><span class="p">),</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">),</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">total_tokens</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">logs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="mi">90</span><span class="w"> </span><span class="k">DAY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>两点提醒：</p>
<ul>
<li><strong>大表上的 <code>DELETE</code> 本身就是个事故来源。</strong> 一次删几千万行会长时间持锁、把 binlog/WAL 撑爆。要分批删（每次几千行、中间 sleep），或者用按时间分区的表直接 <code>DROP PARTITION</code>——后者是从一开始就该做的设计。</li>
<li><strong>SQLite 删完记得 <code>VACUUM</code></strong>，否则文件大小不会回落（空间被标记为可复用但不归还给文件系统）。注意 <code>VACUUM</code> 需要与库等大的临时空间，磁盘快满时反而做不了——所以别等到那时候。</li>
</ul>
<h2 id="决策建议">决策建议</h2>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>选择</th>
					<th>理由</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>个人自建、单实例、几 req/s</td>
					<td><strong>SQLite</strong></td>
					<td>零常驻内存，省下的 285 MB 在 2G 机器上是实打实的。配好 WAL 和 busy_timeout 就够</td>
			</tr>
			<tr>
					<td>2G 机器、已有 MySQL 在跑</td>
					<td><strong>复用现有 MySQL</strong></td>
					<td>再起一个数据库进程是纯浪费。调优比换数据库收益大</td>
			</tr>
			<tr>
					<td>请求量大 / 计划多实例</td>
					<td><strong>PostgreSQL 或 MySQL</strong></td>
					<td>SQLite 无法跨进程共享，这是硬限制而非性能问题</td>
			</tr>
			<tr>
					<td>已经在用 Docker 编排</td>
					<td>跟着编排走</td>
					<td>容器化环境里 SQLite 的数据卷管理反而更麻烦</td>
			</tr>
			<tr>
					<td>需要复杂用量分析查询</td>
					<td><strong>PostgreSQL</strong></td>
					<td>窗口函数、CTE、JSON 查询能力更强，统计报表写起来省事</td>
			</tr>
	</tbody>
</table>
<p>最后一条实践建议：<strong>先从 SQLite 开始不是妥协。</strong> 但要知道 SQLite → MySQL 的迁移不是无痛的——SQLite 是动态类型（type affinity），日期存储没有原生类型，<code>AUTOINCREMENT</code> 语义也不同。如果你已经能预见到半年内会上多实例，那一开始就选 MySQL 或 PostgreSQL，省掉一次迁移。</p>
<h2 id="相关阅读">相关阅读</h2>
<ul>
<li><a href="/posts/new-api-binary-vs-docker/">new-api 部署选型：2G 小内存机器上裸二进制比 Docker 省多少</a> —— 那台机器 285 MB 的 MariaDB 实测数据出自这里</li>
<li><a href="/posts/llm-api-timeout-retry-double-billing/">大模型 API 中转的超时与重试：一次 504 可能让你双倍计费</a> —— 用量日志正是核对重复计费的依据</li>
<li><a href="/posts/caddy-vs-nginx-api-gateway-tls/">Caddy 还是 nginx 给 API 中转做反代：证书自动化是决定性差异</a> —— 同一台小机器上的另一项运维取舍</li>
</ul>
]]></content:encoded></item><item><title>new-api 部署选型：2G 小内存机器上裸二进制比 Docker 省多少</title><link>https://ambertu.com/posts/new-api-binary-vs-docker/</link><pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate><guid>https://ambertu.com/posts/new-api-binary-vs-docker/</guid><description>在 2 核 2G 云主机上部署 new-api，裸二进制和 Docker 到底差多少内存？本文用一台真实 2G 机器的内存实测数据说明：系统底噪吃掉的比你以为的多，选型的关键根本不在应用本身。</description><content:encoded><![CDATA[<p>在 2 核 2G 的云主机上部署 new-api，如果这台机器还跑着别的服务，<strong>裸二进制方案通常是唯一可行的选择</strong>。原因不是 new-api 本身重——它是个 Go 单体，编译出来就一个二进制——而是 Docker 守护进程加上容器化的数据库和缓存，会在系统底噪之上再叠几百 MB 常驻开销，而 2G 机器的可用余量往往只剩 1G 出头。</p>
<p>这篇文章不谈&quot;Docker 好还是不好&quot;，只回答一个具体问题：<strong>在小内存机器上，这几百 MB 的差价花得值不值。</strong></p>
<h2 id="先看一台真实-2g-机器的内存都被谁吃了">先看一台真实 2G 机器的内存都被谁吃了</h2>
<p>大多数部署教程会告诉你&quot;new-api 占用很小&quot;，这话没错，但它回答的不是你真正要问的问题。下面是我手上一台腾讯云 2 核 2G / CentOS Stream 9 机器的实测数据，已连续运行 34 天，跑着 nginx、MariaDB、Redis 和两个自己写的 Go 服务：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">              total        used        free      shared  buff/cache   available
</span></span><span class="line"><span class="cl">Mem:          1.9Gi       613Mi        74Mi       7.0Mi       1.2Gi       1.1Gi
</span></span><span class="line"><span class="cl">Swap:            0B          0B          0B
</span></span></code></pre></div><p>按进程 RSS 排出来的大户：</p>
<table>
	<thead>
			<tr>
					<th>进程</th>
					<th>RSS</th>
					<th>是什么</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>mariadbd</code></td>
					<td>285 MB</td>
					<td>数据库，单进程最大户</td>
			</tr>
			<tr>
					<td><code>systemd-journald</code></td>
					<td>200 MB</td>
					<td><strong>系统日志，默认无上限增长</strong></td>
			</tr>
			<tr>
					<td><code>YDService</code></td>
					<td>93 MB</td>
					<td>腾讯云主机安全 agent</td>
			</tr>
			<tr>
					<td><code>rsyslogd</code></td>
					<td>60 MB</td>
					<td>又一份日志</td>
			</tr>
			<tr>
					<td><code>barad_agent</code></td>
					<td>27 MB</td>
					<td>腾讯云监控 agent</td>
			</tr>
			<tr>
					<td><code>petserver</code></td>
					<td>26 MB</td>
					<td>自己写的 Go 服务</td>
			</tr>
	</tbody>
</table>
<figure>
<svg viewBox="0 0 720 300" role="img" aria-label="一台 2 核 2G 云主机的进程内存占用条形图：MariaDB 285MB、journald 200MB、腾讯云安全 agent 93MB、rsyslogd 60MB、监控 agent 27MB、自研 Go 服务 26MB，可用内存约 1126MB" style="width:100%;height:auto;color:currentColor">
  <text x="0" y="14" font-size="11" font-family="monospace" fill="currentColor" opacity="0.6">进程 RSS（实测 · 2C2G CentOS Stream 9）</text>
  <g font-size="11" font-family="monospace" fill="currentColor">
    <text x="0" y="48">mariadbd</text>
    <rect x="150" y="38" width="285" height="14" fill="currentColor" opacity="0.75"/>
    <text x="443" y="48" opacity="0.7">285 MB</text>
    <text x="0" y="76">journald</text>
    <rect x="150" y="66" width="200" height="14" fill="currentColor" opacity="0.75"/>
    <text x="358" y="76" opacity="0.7">200 MB</text>
    <text x="0" y="104">YDService</text>
    <rect x="150" y="94" width="93" height="14" fill="currentColor" opacity="0.5"/>
    <text x="251" y="104" opacity="0.7">93 MB</text>
    <text x="0" y="132">rsyslogd</text>
    <rect x="150" y="122" width="60" height="14" fill="currentColor" opacity="0.5"/>
    <text x="218" y="132" opacity="0.7">60 MB</text>
    <text x="0" y="160">barad_agent</text>
    <rect x="150" y="150" width="27" height="14" fill="currentColor" opacity="0.5"/>
    <text x="185" y="160" opacity="0.7">27 MB</text>
    <text x="0" y="188">petserver</text>
    <rect x="150" y="178" width="26" height="14" fill="currentColor" opacity="0.5"/>
    <text x="184" y="188" opacity="0.7">26 MB（自研 Go 服务）</text>
  </g>
  <line x1="150" y1="208" x2="700" y2="208" stroke="currentColor" stroke-width="1" opacity="0.25"/>
  <g font-size="11" font-family="monospace" fill="currentColor">
    <text x="0" y="234">available</text>
    <rect x="150" y="224" width="422" height="14" fill="none" stroke="currentColor" stroke-width="1.2" stroke-dasharray="4 3"/>
    <text x="580" y="234" opacity="0.7">≈ 1126 MB</text>
  </g>
  <text x="0" y="268" font-size="10" font-family="monospace" fill="currentColor" opacity="0.6">Swap: 0B —— 没有兜底，OOM 直接杀进程</text>
  <text x="0" y="286" font-size="10" font-family="monospace" fill="currentColor" opacity="0.6">注：RSS 之和大于 free 的 used，因为共享页在每个进程里都被重复计入</text>
</svg>
<figcaption>实测数据。真正值得注意的不是哪个进程最大，而是<strong>你的应用还没启动，系统底噪加数据库已经吃掉了大约 580 MB</strong>。</figcaption>
</figure>
<p>这里有个细节要说清楚，否则数字看着不自洽：<strong>上面各进程 RSS 加起来（约 691 MB）大于 <code>free</code> 报的 <code>used</code>（613 MB）</strong>，这不是我算错了。RSS 会把进程间共享的内存页（共享库、写时复制的父子页）在每个进程里各算一遍，所以 RSS 求和天然偏高。判断&quot;还剩多少能用&quot;要看 <code>available</code>，判断&quot;谁是大户&quot;才看 RSS。</p>
<p>从这张图能得出两个结论，它们直接决定选型：</p>
<ol>
<li><strong>系统底噪比应用大得多。</strong> journald 200 MB、两个云厂商 agent 合计 120 MB、rsyslogd 60 MB —— 这些你一行代码没写就已经存在了。</li>
<li><strong>可用余量只有 1.1 G，且没有 swap。</strong> 这意味着任何一次内存尖峰都直接触发 OOM killer，没有降级缓冲。</li>
</ol>
<h2 id="裸二进制方案的实际构成">裸二进制方案的实际构成</h2>
<p>new-api 是 Go 编译的单体，部署形态非常简单：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">systemd
</span></span><span class="line"><span class="cl">├── new-api            单二进制，Go 运行时
</span></span><span class="line"><span class="cl">├── mariadbd           本机数据库
</span></span><span class="line"><span class="cl">└── caddy / nginx      反向代理 + TLS
</span></span></code></pre></div><p>一个够用的 systemd unit：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">new-api</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network-online.target mariadb.service</span>
</span></span><span class="line"><span class="cl"><span class="na">Wants</span><span class="o">=</span><span class="s">network-online.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">simple</span>
</span></span><span class="line"><span class="cl"><span class="na">User</span><span class="o">=</span><span class="s">newapi</span>
</span></span><span class="line"><span class="cl"><span class="na">WorkingDirectory</span><span class="o">=</span><span class="s">/opt/new-api</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/opt/new-api/new-api --port 3000 --log-dir /var/log/new-api</span>
</span></span><span class="line"><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">always</span>
</span></span><span class="line"><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">5</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 小内存机器上值得加的两道保险</span>
</span></span><span class="line"><span class="cl"><span class="na">MemoryMax</span><span class="o">=</span><span class="s">512M</span>
</span></span><span class="line"><span class="cl"><span class="na">OOMPolicy</span><span class="o">=</span><span class="s">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">multi-user.target</span>
</span></span></code></pre></div><p><code>MemoryMax</code> 这行是小内存机器的关键：给应用设一个上限，让它在自己超标时被 cgroup 限制住，而不是把整机推向 OOM、让内核随机挑一个进程杀——被杀的往往是内存占用最大的 MariaDB，那就是数据库直接下线。</p>
<p><strong>代价要讲清楚</strong>，裸二进制不是没有成本的：</p>
<ul>
<li>升级是手动的：下载新二进制、<code>systemctl stop</code>、替换文件、<code>systemctl start</code>，回滚要自己留旧版本</li>
<li>没有进程隔离，应用和宿主机共享一切</li>
<li>依赖宿主机的 glibc 版本，跨发行版迁移可能编译不兼容（Go 静态编译能规避大部分，但 CGO 开启时不行）</li>
<li>数据库、缓存的版本管理全靠宿主机包管理器</li>
</ul>
<h2 id="docker-方案多出来的常驻开销">Docker 方案多出来的常驻开销</h2>
<figure>
<svg viewBox="0 0 760 340" role="img" aria-label="裸二进制与 Docker 两种部署方式的进程树对比：裸二进制下 systemd 直接管理 new-api、mariadbd 和反向代理；Docker 下额外增加 dockerd、containerd 以及每个容器的 shim 进程" style="width:100%;height:auto;color:currentColor">
  <text x="0" y="14" font-size="11" font-family="monospace" fill="currentColor" opacity="0.6">裸二进制</text>
  <text x="400" y="14" font-size="11" font-family="monospace" fill="currentColor" opacity="0.6">Docker</text>
  <line x1="378" y1="26" x2="378" y2="320" stroke="currentColor" stroke-width="1" opacity="0.25" stroke-dasharray="3 5"/>
  <!-- left -->
  <g font-size="11" font-family="monospace" fill="currentColor">
    <rect x="0" y="34" width="110" height="26" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="12" y="51">systemd</text>
    <path d="M 20 60 L 20 90 L 40 90" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>
    <rect x="40" y="78" width="150" height="24" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="50" y="94">new-api</text>
    <text x="200" y="94" opacity="0.6">40–80 MB</text>
    <path d="M 20 90 L 20 128 L 40 128" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>
    <rect x="40" y="116" width="150" height="24" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="50" y="132">mariadbd</text>
    <text x="200" y="132" opacity="0.6">285 MB 实测</text>
    <path d="M 20 128 L 20 166 L 40 166" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>
    <rect x="40" y="154" width="150" height="24" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="50" y="170">caddy</text>
    <text x="200" y="170" opacity="0.6">10–25 MB</text>
  </g>
  <text x="0" y="228" font-size="11" font-family="monospace" fill="currentColor">额外常驻开销</text>
  <text x="0" y="250" font-size="15" font-family="monospace" fill="currentColor" font-weight="bold">0 MB</text>
  <!-- right -->
  <g font-size="11" font-family="monospace" fill="currentColor">
    <rect x="400" y="34" width="110" height="26" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="412" y="51">systemd</text>
    <path d="M 420 60 L 420 90 L 440 90" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>
    <rect x="440" y="78" width="140" height="24" fill="currentColor" fill-opacity="0.1" stroke="currentColor" stroke-width="1.2"/>
    <text x="450" y="94">dockerd</text>
    <text x="590" y="94" opacity="0.6">80–150 MB</text>
    <path d="M 460 102 L 460 128 L 480 128" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>
    <rect x="480" y="116" width="140" height="24" fill="currentColor" fill-opacity="0.1" stroke="currentColor" stroke-width="1.2"/>
    <text x="490" y="132">containerd</text>
    <text x="630" y="132" opacity="0.6">40–70 MB</text>
    <path d="M 500 140 L 500 166 L 520 166" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>
    <rect x="520" y="154" width="150" height="24" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="530" y="170">shim → new-api</text>
    <path d="M 500 166 L 500 198 L 520 198" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>
    <rect x="520" y="186" width="150" height="24" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="530" y="202">shim → postgres</text>
    <path d="M 500 198 L 500 230 L 520 230" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>
    <rect x="520" y="218" width="150" height="24" fill="none" stroke="currentColor" stroke-width="1.2"/>
    <text x="530" y="234">shim → redis</text>
  </g>
  <text x="400" y="278" font-size="11" font-family="monospace" fill="currentColor">额外常驻开销</text>
  <text x="400" y="300" font-size="15" font-family="monospace" fill="currentColor" font-weight="bold">120–220 MB</text>
  <text x="400" y="322" font-size="10" font-family="monospace" fill="currentColor" opacity="0.6">灰底两项是应用之外白交的</text>
</svg>
<figcaption>右侧灰底的 <code>dockerd</code> 与 <code>containerd</code> 是纯粹的编排开销，跟你的业务一点关系没有。每个容器还各带一个 shim 进程。<strong>标注中只有 mariadbd 的 285 MB 是我实测值，其余是常见范围</strong>，具体数字随版本和配置浮动。</figcaption>
</figure>
<p>把这 120–220 MB 放回前面那张图里看：可用内存 1126 MB，Docker 一上来先拿走两成，剩下的才是留给 new-api、Postgres 和 Redis 的。在 4G 机器上这笔账无所谓；在 2G 机器上它就是压垮骆驼的那根稻草。</p>
<p>Docker 换来的东西也是真的，别因为省内存就否定它：</p>
<ul>
<li>升级是 <code>docker compose pull &amp;&amp; docker compose up -d</code>，回滚是改个 tag</li>
<li>环境完全一致，本地能跑线上就能跑</li>
<li>数据库、Redis 的版本与宿主机解耦，不用跟发行版的包管理器较劲</li>
<li>多实例、多环境并存很容易</li>
</ul>
<h2 id="决策建议">决策建议</h2>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>建议</th>
					<th>理由</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>内存 ≥ 4G，机器专用</td>
					<td><strong>Docker</strong></td>
					<td>省心值这个钱，运维成本的节省远超内存代价</td>
			</tr>
			<tr>
					<td>2G，且已跑其他生产服务</td>
					<td><strong>裸二进制</strong></td>
					<td>编排开销挤占的是业务余量，这笔交换不划算</td>
			</tr>
			<tr>
					<td>2G，机器专用于 new-api</td>
					<td>都行，倾向 Docker</td>
					<td>没有邻居要抢内存，但务必先加 swap</td>
			</tr>
			<tr>
					<td>需要多实例 / 多环境并存</td>
					<td><strong>Docker</strong></td>
					<td>裸二进制做这件事会很痛苦</td>
			</tr>
	</tbody>
</table>
<h2 id="两个几乎所有人都忽略的内存大户">两个几乎所有人都忽略的内存大户</h2>
<h3 id="journald-默认没有上限">journald 默认没有上限</h3>
<p>前面实测里 <code>systemd-journald</code> 占了 200 MB，这不是异常，是默认行为——它会一直增长到磁盘配额上限。小内存机器上值得直接压掉：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/journald.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[Journal]</span>
</span></span><span class="line"><span class="cl"><span class="na">SystemMaxUse</span><span class="o">=</span><span class="s">200M</span>
</span></span><span class="line"><span class="cl"><span class="na">RuntimeMaxUse</span><span class="o">=</span><span class="s">50M</span>
</span></span></code></pre></div><p>改完 <code>systemctl restart systemd-journald</code>。如果你的服务日志已经写到文件并由 logrotate 管，journald 里那份基本是重复的。</p>
<h3 id="云厂商-agent">云厂商 agent</h3>
<p>实测里 <code>YDService</code>（主机安全）加 <code>barad_agent</code>（监控）合计约 120 MB。这些是装系统镜像时预置的，你未必需要全部。<strong>要不要关取决于你的取舍</strong>：关掉监控 agent 会失去云控制台的监控图表，关掉安全 agent 会失去入侵检测告警。在 2G 机器上这 120 MB 确实值钱，但别为了省内存把安全能力一起关掉——如果你的机器上有数据库对外暴露，那点内存远不如告警重要。</p>
<h2 id="无-swap-是这类机器的隐藏地雷">无 swap 是这类机器的隐藏地雷</h2>
<p>前面实测数据里 <code>Swap: 0B</code>。云主机默认不带 swap，而 2G 机器最需要它——不是为了性能，是为了<strong>在内存尖峰时有个降级路径,而不是直接被 OOM killer 杀掉</strong>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 加 1G swapfile</span>
</span></span><span class="line"><span class="cl">fallocate -l 1G /swapfile
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> /swapfile
</span></span><span class="line"><span class="cl">mkswap /swapfile
</span></span><span class="line"><span class="cl">swapon /swapfile
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;/swapfile none swap sw 0 0&#39;</span> &gt;&gt; /etc/fstab
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 小内存机器上把 swappiness 调低，只在真的紧张时才用</span>
</span></span><span class="line"><span class="cl">sysctl -w vm.swappiness<span class="o">=</span><span class="m">10</span>
</span></span></code></pre></div><p>代价是数据库一旦被换出磁盘，性能会明显下降。但&quot;慢&quot;永远好过&quot;被杀&quot;。</p>
<h2 id="相关阅读">相关阅读</h2>
<p>部署完成后，反向代理的配置有两个坑几乎人人会踩，我单独写了：</p>
<ul>
<li><a href="/posts/llm-api-sse-proxy-buffering/">反代把大模型 API 的 SSE 流式响应缓冲了：五个缓冲点与三层排查法</a> —— 流式输出变成一次性返回，问题通常不在应用</li>
<li><a href="/posts/api-gateway-real-ip-ratelimit/">API 中转站的限流为什么不能信 X-Forwarded-For</a> —— 这个头客户端能自己塞，按它限流等于没限</li>
</ul>
<p>而本文里那个占 285 MB 的 MariaDB，本身也是个可以重新选的决定：</p>
<ul>
<li><a href="/posts/api-gateway-database-choice-small-vps/">给 API 中转站选数据库：SQLite / MySQL / PostgreSQL 在小机器上的取舍</a> —— 换成 SQLite 能把这 285 MB 整个省掉，代价是写并发上限</li>
</ul>
]]></content:encoded></item></channel></rss>