<?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>内存优化 on Ambertu</title><link>https://ambertu.com/tags/%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96/</link><description>Recent content in 内存优化 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/%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96/index.xml" rel="self" type="application/rss+xml"/><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>