<?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/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Architecture · 仁才德</title>
    <link>https://jared.lynskey.co.nz/zh-cn/tags/architecture/</link>
    <description>关于软件工程与研发团队领导力的短文——绩效评估、招聘与团队扩张、CI/CD、DevOps 监控、智能体 AI。写于首尔。</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <copyright>© 2026 仁才德</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/zh-cn/tags/architecture/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>决策文档是什么？附模板的实用指南</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2024/2024-12-03-decision-docs/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2024/2024-12-03-decision-docs/</guid>
      <pubDate>Tue, 03 Dec 2024 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>decision-making</category>
      <category>documentation</category>
      <category>ADR</category>
      <category>architecture</category>
      <category>team alignment</category>
      <description>决策文档就是在做决定的那一刻，把背景、考虑过的方案和最终选它的理由记下来，免得半年后又从头争一遍。怎么写出真正有人用的 ADR 和 RFC，附一份 ADR 示例和可以直接拿走的模板。</description>
      <content:encoded><![CDATA[<p>每个团队都在做决策，而大多数团队会忘记当初为什么这么决定。半年后有人问“我们为什么选了 PostgreSQL 而不是 MongoDB？”，没有人记得当时的约束条件，于是同一场争论从头再来一遍——而且到场的人通常比上次还少。</p>
<p>决策文档就是解这个问题的。叫 ADR、RFC 还是决策日志都无所谓，关键是在做选择的那一刻，把背景、备选方案和推理写下来。今年开始带开发团队之后，我对它的价值体会得更深了：它决定了团队拥有的是组织记忆，还是组织传说。</p>
<hr>

<h3 class="relative group">为什么值得花这个功夫
    <div id="为什么值得花这个功夫" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e5%80%bc%e5%be%97%e8%8a%b1%e8%bf%99%e4%b8%aa%e5%8a%9f%e5%a4%ab" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>最直接的回报是不用把同一场架吵两遍。“API 版本策略不是已经定了吗？”只要决定了什么、为什么决定有清楚的记录，尘埃落定的问题就能保持落定，团队可以把力气花在执行上，而不是原路折返。</p>
<p>更安静的回报要过一段时间才显现。项目一跑就是几个月甚至几年，人来人往，最后留下的只有代码——代码能告诉你决定了“什么”，却永远不会告诉你“为什么”。决策文档保存的正是推理本身：当时存在的约束、被否掉的备选方案、拍板的人是谁。新人得到的是上下文而不是传说；跨团队的决策因为所有人都能看到权衡的因素而更容易获得真正的认同；决策者和参与者都写着名字，后续落实也就有了明确的主人。</p>
<hr>

<h3 class="relative group">什么时候写
    <div id="什么时候写" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e5%86%99" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>不是每个决策都配得上一份文档。我会在决策属于以下情况时动笔：</p>
<ul>
<li><strong>回头代价高的</strong> — 技术栈选择（框架、数据库、云服务商）、架构模式（微服务还是单体、事件驱动还是请求-响应）、第三方供应商选型、安全与合规方案</li>
<li><strong>影响长期存在的</strong> — 外部系统依赖的 API 契约、跨多个服务的数据模型、对既定模式的破坏性变更</li>
<li><strong>需要跨团队的</strong> — 需要多个团队或非技术相关方认同的事项、资源分配和优先级取舍</li>
<li><strong><a href="https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-10-18-spike/" >Spike</a>（技术预研）的结论</strong> — 限时调研结束后，趁发现和决定还没蒸发，赶紧落成文档</li>
</ul>
<p>反过来，容易撤销的决定、团队内部的实现细节、对自己范围之外几乎没有影响的选择，就别走形式了。没人需要的文档只是作业。</p>
<hr>

<h3 class="relative group">生命周期
    <div id="生命周期" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
</span></span><span class="line"><span class="cl">│    草稿     │ ──► │    评审     │ ──► │    批准     │ ──► │    实施     │
</span></span><span class="line"><span class="cl">│    提议     │     │   反馈      │     │    接受     │     │    完成     │
</span></span><span class="line"><span class="cl">└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
</span></span><span class="line"><span class="cl">                           │
</span></span><span class="line"><span class="cl">                           ▼
</span></span><span class="line"><span class="cl">                    ┌─────────────┐
</span></span><span class="line"><span class="cl">                    │   否决/     │
</span></span><span class="line"><span class="cl">                    │   取代      │
</span></span><span class="line"><span class="cl">                    └─────────────┘</span></span></code></pre></div></div>
<p>作者起草问题和候选方案，相关方进行评审——整套流程的价值主要就在评审这一步，因为假设被挑战、盲点被暴露都发生在这里。然后由决策者（通常是技术负责人、架构师或产品负责人）拍板，团队实施；实施中如果现实逼着改了方案，文档也要跟着更新。有的文档死在评审里，有的日后随情况变化被取代——两种都要清楚标记，一份过期却还挂着“已批准”的文档，是给下一个发现它的人挖的坑。</p>
<hr>

<h3 class="relative group">什么样的文档才值得读
    <div id="什么样的文档才值得读" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bb%80%e4%b9%88%e6%a0%b7%e7%9a%84%e6%96%87%e6%a1%a3%e6%89%8d%e5%80%bc%e5%be%97%e8%af%bb" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>从问题开始，而不是从你心仪的方案开始。我们在解决什么？为什么是现在？什么都不做会怎样？如果大家对问题本身都没有共识，讨论方案就是演戏。</p>
<p>然后给出真正的备选项——哪怕你已有强烈倾向，也至少写两三个。每个选项配上简短描述、优缺点、粗略的工作量估算（T 恤尺码就够用：S/M/L/XL），以及风险和应对办法。把落选方案写下来，未来的读者才能明白胜者为什么胜出。</p>
<p>把取舍大声说出来。“我们选择上市速度，放弃架构纯粹性。”“我们接受运维复杂度上升，换取更好的扩展性。”“我们把开发者体验放在原始性能之前。”这样的句子对未来读者的帮助，超过几页纸的分析。</p>
<p>让讨论落在具体的东西上：展示方案实现样子的代码片段、系统交互图、指向 <a href="https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-10-18-spike/" >Spike</a> 概念验证的链接。抽象的讨论只会产出抽象的决定。</p>
<p>最后，给它一个期限：什么时候必须定下来，谁在什么时间前要给意见，拖着不决会卡住什么。没有期限的决策会永远飘着。</p>
<hr>

<h3 class="relative group">示例：架构决策记录（ADR）
    <div id="示例架构决策记录adr" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%a4%ba%e4%be%8b%e6%9e%b6%e6%9e%84%e5%86%b3%e7%ad%96%e8%ae%b0%e5%bd%95adr" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>下面是一个实际的决策文档示例：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># ADR-001: API Authentication Strategy
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Status:**</span> Accepted
</span></span><span class="line"><span class="cl"><span class="gs">**Date:**</span> 2024-11-15
</span></span><span class="line"><span class="cl"><span class="gs">**Decision Maker:**</span> Sarah Chen (Platform Architect)
</span></span><span class="line"><span class="cl"><span class="gs">**Contributors:**</span> Backend Team, Security Team, Mobile Team
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Context
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Our public API currently uses API keys for authentication. As we expand to
</span></span><span class="line"><span class="cl">support third-party integrations and mobile apps, we need a more robust
</span></span><span class="line"><span class="cl">authentication mechanism that supports:
</span></span><span class="line"><span class="cl"><span class="k">-</span> Token expiration and refresh
</span></span><span class="line"><span class="cl"><span class="k">-</span> Scoped permissions
</span></span><span class="line"><span class="cl"><span class="k">-</span> User-level authentication (not just service-level)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Decision
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">We will implement OAuth 2.0 with JWT tokens for API authentication.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Options Considered
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 1: OAuth 2.0 with JWT (Recommended)
</span></span></span><span class="line"><span class="cl">**Description:** Industry-standard protocol with self-contained tokens
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Industry standard, well-documented | More complex initial implementation |
</span></span><span class="line"><span class="cl">| Self-contained tokens reduce database lookups | Tokens cannot be revoked instantly |
</span></span><span class="line"><span class="cl">| Broad library support | Requires refresh token management |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Medium (2-3 sprints)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 2: Session-based Authentication
</span></span></span><span class="line"><span class="cl">**Description:** Traditional server-side sessions with cookies
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Simple to implement | Not suitable for mobile apps |
</span></span><span class="line"><span class="cl">| Easy to revoke sessions | Requires sticky sessions or shared session store |
</span></span><span class="line"><span class="cl">| Familiar to most developers | Doesn&#39;t scale as well |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Small (1 sprint)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 3: Custom Token System
</span></span></span><span class="line"><span class="cl">**Description:** Build our own token-based authentication
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Fully customizable | Reinventing the wheel |
</span></span><span class="line"><span class="cl">| No external dependencies | Security risks from custom implementation |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Large (4+ sprints)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Consequences
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Mobile team can implement standard OAuth flows
</span></span><span class="line"><span class="cl"><span class="k">-</span> We&#39;ll need to set up a token refresh mechanism
</span></span><span class="line"><span class="cl"><span class="k">-</span> API documentation will need updates for OAuth flows
</span></span><span class="line"><span class="cl"><span class="k">-</span> Existing API key users will need a migration path (6-month deprecation)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## References
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> [<span class="nt">OAuth 2.0 RFC 6749</span>](<span class="na">https://tools.ietf.org/html/rfc6749</span>)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Internal spike document: [<span class="nt">Authentication Options Spike</span>](<span class="na">/spikes/auth-spike-2024</span>)
</span></span><span class="line"><span class="cl">- Security team review: SEC-2024-042</span></span></code></pre></div></div>
<hr>

<h3 class="relative group">一份可以直接拿走的模板
    <div id="一份可以直接拿走的模板" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%80%e4%bb%bd%e5%8f%af%e4%bb%a5%e7%9b%b4%e6%8e%a5%e6%8b%bf%e8%b5%b0%e7%9a%84%e6%a8%a1%e6%9d%bf" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>把它当起点，按团队需要往下删：</p>
<table>
	<thead>
			<tr>
					<th><strong>属性</strong></th>
					<th><strong>详情</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>决策/议题名称</strong></td>
					<td>[清晰、描述性的标题]</td>
			</tr>
			<tr>
					<td><strong>状态</strong></td>
					<td>[草稿 / 评审中 / 已批准 / 已否决 / 已取代]</td>
			</tr>
			<tr>
					<td><strong>影响度</strong></td>
					<td>[高 / 中 / 低]</td>
			</tr>
			<tr>
					<td><strong>负责人</strong></td>
					<td>[推动决策的人]</td>
			</tr>
			<tr>
					<td><strong>决策者</strong></td>
					<td>[拥有最终决定权的人]</td>
			</tr>
			<tr>
					<td><strong>截止日期</strong></td>
					<td>[做出决策的截止时间]</td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">问题描述
    <div id="问题描述" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%97%ae%e9%a2%98%e6%8f%8f%e8%bf%b0" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><em>我们要解决什么问题？为什么重要？不采取行动的代价是什么？</em></p>

<h4 class="relative group">背景
    <div id="背景" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%83%8c%e6%99%af" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><em>提供上下文、历史和相关细节。链接到相关文档、过去的决策或Spike结果。</em></p>

<h4 class="relative group">约束条件
    <div id="约束条件" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%ba%a6%e6%9d%9f%e6%9d%a1%e4%bb%b6" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><em>所有方案必须遵守哪些限制或要求？</em></p>
<ul>
<li>预算约束</li>
<li>时间线要求</li>
<li>技术约束（现有系统、技能储备）</li>
<li>合规或安全要求</li>
</ul>

<h4 class="relative group">考虑的方案
    <div id="考虑的方案" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%80%83%e8%99%91%e7%9a%84%e6%96%b9%e6%a1%88" aria-label="锚点">#</a>
    </span>
    
</h4>
<table>
	<thead>
			<tr>
					<th><strong>选项</strong></th>
					<th><strong>描述</strong></th>
					<th><strong>优点</strong></th>
					<th><strong>缺点</strong></th>
					<th><strong>工作量</strong></th>
					<th><strong>风险</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>选项 1</strong></td>
					<td><em>描述</em></td>
					<td><em>优点A，优点B</em></td>
					<td><em>缺点A，缺点B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>低/中/高</em></td>
			</tr>
			<tr>
					<td><strong>选项 2</strong></td>
					<td><em>描述</em></td>
					<td><em>优点A，优点B</em></td>
					<td><em>缺点A，缺点B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>低/中/高</em></td>
			</tr>
			<tr>
					<td><strong>选项 3</strong></td>
					<td><em>描述</em></td>
					<td><em>优点A，优点B</em></td>
					<td><em>缺点A，缺点B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>低/中/高</em></td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">建议
    <div id="建议" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%bb%ba%e8%ae%ae" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><em>说明推荐的选项，并总结在约束和取舍条件下为什么它是最佳选择。</em></p>

<h4 class="relative group">决定
    <div id="决定" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%86%b3%e5%ae%9a" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><em>做出决定后记录最终决策。如果与建议不同，说明原因。</em></p>

<h4 class="relative group">后果
    <div id="后果" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%90%8e%e6%9e%9c" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><em>这个决策有什么影响？需要哪些后续工作？</em></p>

<h4 class="relative group">行动项
    <div id="行动项" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%a1%8c%e5%8a%a8%e9%a1%b9" aria-label="锚点">#</a>
    </span>
    
</h4>
<table>
	<thead>
			<tr>
					<th><strong>行动</strong></th>
					<th><strong>负责人</strong></th>
					<th><strong>截止日期</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><em>行动 1</em></td>
					<td><em>姓名</em></td>
					<td><em>日期</em></td>
			</tr>
			<tr>
					<td><em>行动 2</em></td>
					<td><em>姓名</em></td>
					<td><em>日期</em></td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">参考资料
    <div id="参考资料" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><em>相关文档、外部资源、Spike结果或过去决策的链接。</em></p>
<hr>

<h3 class="relative group">让习惯扎根
    <div id="让习惯扎根" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%ae%a9%e4%b9%a0%e6%83%af%e6%89%8e%e6%a0%b9" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>文档放在固定的地方——仓库里的专用文件夹也好，Wiki 空间也好——找不到的决策等于不存在。保持轻量：一份要写好几天的文档根本不会被写出来。在代码注释、提交信息和 PR 里引用决策文档，让“为什么”一直贴着“是什么”。决策被取代时别删旧文档，标记为已取代并链接到新决策，历史本身就是价值。另外，在<a href="https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-09-01-retrospecs/" >回顾会</a>上时不时翻翻过去的决策：哪些经受住了考验，哪些换成今天你会做得不一样。</p>
<p>失败的方式也同样好预测。分析瘫痪：定个期限，用手头的信息做决定，大多数决策以后都能再议。橡皮图章式评审从来提不出真正的批评，只加流程不加价值。写了却从未落地的孤儿文档，比没有文档更糟。细节过多也是病：文档管“为什么”和“是什么”，不管“怎么做”，实现细节属于技术规格书。还有，当有人不同意时，把反对意见也写进去——决定也许照旧，但那条异议恰恰是一年后某个人最需要的上下文。</p>
<p>从你的下一个重要决策开始吧。写得糙一点没关系，流程边用边磨。每一场因为有人直接读了 ADR 而没开成的“我们当初为什么选 X”会议，都是还给团队的时间。</p>
<hr>

<h3 class="relative group">延伸阅读
    <div id="延伸阅读" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%bb%b6%e4%bc%b8%e9%98%85%e8%af%bb" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><strong>本站相关文章：</strong></p>
<ul>
<li><a href="https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-10-18-spike/" >理解敏捷软件开发中的Spike</a> - 在做重大决策前用 Spike 收集信息</li>
<li><a href="https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-09-01-retrospecs/" >回顾会：反馈循环的力量</a> - 把审视过去的决策纳入持续改进</li>
<li><a href="https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-10-08-planning-an-api/" >API规划</a> - 把决策文档的思路用在 API 设计上</li>
<li><a href="https://jared.lynskey.co.nz/zh-cn/posts/2024/2024-01-04-githubforprojectmanagement/" >用GitHub做项目管理</a> - 让决策文档和代码放在一起</li>
</ul>
<p><strong>外部资源：</strong></p>
<ul>
<li><a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions"  target="_blank" rel="noreferrer">Documenting Architecture Decisions - Michael Nygard</a> - 让 ADR 流行起来的那篇经典博文</li>
<li><a href="https://adr.github.io/"  target="_blank" rel="noreferrer">ADR GitHub Organization</a> - 架构决策记录的工具、模板和示例</li>
<li><a href="https://www.industrialempathy.com/posts/design-docs-at-google/"  target="_blank" rel="noreferrer">Design Docs at Google</a> - Google 的设计文档实践</li>
<li><a href="https://rust-lang.github.io/rfcs/"  target="_blank" rel="noreferrer">RFC Process at Rust</a> - Rust 语言完善的 RFC 流程</li>
<li><a href="https://engineering.atspotify.com/2020/04/when-should-i-write-an-architecture-decision-record/"  target="_blank" rel="noreferrer">Spotify&rsquo;s Decision Record Template</a> - Spotify 关于何时写 ADR 的思考</li>
</ul>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-12-03-decision-docs/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>单租户 vs 多租户 SaaS：什么时候用哪种</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2024/2024-04-07-singletenantsingleinstance/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2024/2024-04-07-singletenantsingleinstance/</guid>
      <pubDate>Sat, 06 Apr 2024 00:30:03 +0000</pubDate>
      <dc:creator>Me</dc:creator>
      <category>SaaS</category>
      <category>architecture</category>
      <category>single-tenant</category>
      <category>multi-tenant</category>
      <description>单租户（多实例）和多租户 SaaS 到底有什么区别？从隔离、成本、运维三方面对比，介绍两者之间的中间方案、什么时候用哪种、怎么在共享数据库里隔离租户，以及让“每个客户一套实例”撑得下去的工具。</description>
      <content:encoded><![CDATA[<p>单租户 SaaS 给每个客户一整套独立的应用和数据库。多租户 SaaS 让所有客户共用一套应用和数据库，靠租户 ID 区分，而且每条查询都得检查它。单租户换来的是隔离、按客户的控制权和更省事的合规，代价是要运维一大堆副本。多租户更便宜，运维也简单得多，但把客户彼此隔开就成了代码的活儿。大多数产品从多租户起步，只给少数真正需要的客户提供独享实例。</p>
<p>做B2B SaaS，租户架构这个问题迟早会找上门，通常就是某个大客户问“我们的数据能不能和别人分开放”的那一天。这种讨论我经历过不止一次。答案基本就两个：给每个客户独立的实例（单租户，多实例），或者让所有人共享一个实例（多租户，单实例）。没有哪个是“正确答案”，只是坏起来的方式不一样。</p>

<h2 class="relative group">基础设施设计
    <div id="基础设施设计" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%9f%ba%e7%a1%80%e8%ae%be%e6%96%bd%e8%ae%be%e8%ae%a1" aria-label="锚点">#</a>
    </span>
    
</h2>
<figure class="diagram">
  <div role="img" aria-label="单租户和多租户的区别"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 790" aria-hidden="true" focusable="false">
<defs><marker id="tn-ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="d-head" d="M0,0 L10,5 L0,10 z"/></marker></defs>
<text class="d-title" x="320" y="164" text-anchor="middle" font-size="42">多实例</text>
<text class="d-sub" x="320" y="204" text-anchor="middle" font-size="24">单租户：每个客户一套</text>
<rect class="d-panel" x="12" y="234" width="616" height="304" rx="36"/>
<rect class="d-group" x="25" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="33" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="118.0" y="337.64" text-anchor="middle" font-size="24">数据库</text>
<polygon class="d-badge" points="118.0,256.0 143.7,274.7 133.9,304.8 102.1,304.8 92.3,274.7"/>
<g class="d-icon">
<ellipse cx="118" cy="273" rx="10" ry="4"/>
<path d="M108,273 v15 a10,4 0 0 0 20,0 v-15 M108,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="33" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="118.0" y="475.64" text-anchor="middle" font-size="24">应用</text>
<polygon class="d-badge" points="118.0,394.0 143.7,412.7 133.9,442.8 102.1,442.8 92.3,412.7"/>
<g class="d-icon">
<rect x="110" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="114.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M114,411 v-4 M114,427 v4 M110,415 h-4 M126,415 h4 M118,411 v-4 M118,427 v4 M110,419 h-4 M126,419 h4 M122,411 v-4 M122,427 v4 M110,423 h-4 M126,423 h4"/>
</g>
<line class="d-line" x1="118" y1="508" x2="118" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="84" cy="710.88" r="14.44"/><path d="M58.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="152" cy="710.88" r="14.44"/><path d="M126.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="118" cy="704.0" r="19.0"/><path d="M84.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<rect class="d-group" x="227" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="235" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="320.0" y="337.64" text-anchor="middle" font-size="24">数据库</text>
<polygon class="d-badge" points="320.0,256.0 345.7,274.7 335.9,304.8 304.1,304.8 294.3,274.7"/>
<g class="d-icon">
<ellipse cx="320" cy="273" rx="10" ry="4"/>
<path d="M310,273 v15 a10,4 0 0 0 20,0 v-15 M310,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="235" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="320.0" y="475.64" text-anchor="middle" font-size="24">应用</text>
<polygon class="d-badge" points="320.0,394.0 345.7,412.7 335.9,442.8 304.1,442.8 294.3,412.7"/>
<g class="d-icon">
<rect x="312" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="316.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M316,411 v-4 M316,427 v4 M312,415 h-4 M328,415 h4 M320,411 v-4 M320,427 v4 M312,419 h-4 M328,419 h4 M324,411 v-4 M324,427 v4 M312,423 h-4 M328,423 h4"/>
</g>
<line class="d-line" x1="320" y1="508" x2="320" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="286" cy="710.88" r="14.44"/><path d="M260.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="354" cy="710.88" r="14.44"/><path d="M328.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="320" cy="704.0" r="19.0"/><path d="M286.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<rect class="d-group" x="429" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="437" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="522.0" y="337.64" text-anchor="middle" font-size="24">数据库</text>
<polygon class="d-badge" points="522.0,256.0 547.7,274.7 537.9,304.8 506.1,304.8 496.3,274.7"/>
<g class="d-icon">
<ellipse cx="522" cy="273" rx="10" ry="4"/>
<path d="M512,273 v15 a10,4 0 0 0 20,0 v-15 M512,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="437" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="522.0" y="475.64" text-anchor="middle" font-size="24">应用</text>
<polygon class="d-badge" points="522.0,394.0 547.7,412.7 537.9,442.8 506.1,442.8 496.3,412.7"/>
<g class="d-icon">
<rect x="514" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="518.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M518,411 v-4 M518,427 v4 M514,415 h-4 M530,415 h4 M522,411 v-4 M522,427 v4 M514,419 h-4 M530,419 h4 M526,411 v-4 M526,427 v4 M514,423 h-4 M530,423 h4"/>
</g>
<line class="d-line" x1="522" y1="508" x2="522" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="488" cy="710.88" r="14.44"/><path d="M462.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="556" cy="710.88" r="14.44"/><path d="M530.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="522" cy="704.0" r="19.0"/><path d="M488.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<text class="d-title" x="982" y="50" text-anchor="middle" font-size="42">多租户</text>
<text class="d-sub" x="982" y="88" text-anchor="middle" font-size="24">所有客户共用一个实例</text>
<rect class="d-panel" x="698" y="104" width="568" height="434" rx="44"/>
<rect class="d-box" x="887" y="126" width="190" height="86" rx="14"/>
<text class="d-label" x="982.0" y="183.64" text-anchor="middle" font-size="24">数据库</text>
<polygon class="d-badge" points="982.0,102.0 1007.7,120.7 997.9,150.8 966.1,150.8 956.3,120.7"/>
<g class="d-icon">
<ellipse cx="982" cy="119" rx="10" ry="4"/>
<path d="M972,119 v15 a10,4 0 0 0 20,0 v-15 M972,126 a10,4 0 0 0 20,0"/>
</g>
<line class="d-line" x1="982" y1="216" x2="982" y2="246" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<rect class="d-group" x="704" y="240" width="556" height="156" rx="30"/>
<rect class="d-box" x="714" y="282" width="536" height="90" rx="16"/>
<polygon class="d-badge" points="982.0,248.0 1007.7,266.7 997.9,296.8 966.1,296.8 956.3,266.7"/>
<g class="d-icon">
<rect x="974" y="265" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="978.5" y="269.5" width="7" height="7"/>
<path stroke-width="1.4" d="M978,265 v-4 M978,281 v4 M974,269 h-4 M990,269 h4 M982,265 v-4 M982,281 v4 M974,273 h-4 M990,273 h4 M986,265 v-4 M986,281 v4 M974,277 h-4 M990,277 h4"/>
</g>
<rect class="d-box" x="722" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="806.0" y="347.92" text-anchor="middle" font-size="22">应用</text>
<rect class="d-box" x="898" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="982.0" y="347.92" text-anchor="middle" font-size="22">应用</text>
<rect class="d-box" x="1074" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="1158.0" y="347.92" text-anchor="middle" font-size="22">应用</text>
<rect class="d-box" x="887" y="440" width="190" height="86" rx="14"/>
<text class="d-label" x="982.0" y="497.64" text-anchor="middle" font-size="24">负载均衡器</text>
<polygon class="d-badge" points="982.0,404.0 1007.7,422.7 997.9,452.8 966.1,452.8 956.3,422.7"/>
<g class="d-icon">
<path d="M982,416 l6,9 h-12 z M982,425 v5 M972,433 h20 M972,433 v3 M982,433 v3 M992,433 v3"/>
<rect class="d-icon-fill" x="969" y="436" width="6" height="5"/>
<rect class="d-icon-fill" x="979" y="436" width="6" height="5"/>
<rect class="d-icon-fill" x="989" y="436" width="6" height="5"/>
</g>
<line class="d-line" x1="806" y1="364" x2="922" y2="438" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="982" y1="364" x2="982" y2="398" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="1158" y1="364" x2="1042" y2="438" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="932" y1="528" x2="780" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="716" cy="710.88" r="14.44"/><path d="M690.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="784" cy="710.88" r="14.44"/><path d="M758.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="750" cy="704.0" r="19.0"/><path d="M716.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<line class="d-line" x1="982" y1="528" x2="982" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="948" cy="710.88" r="14.44"/><path d="M922.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="1016" cy="710.88" r="14.44"/><path d="M990.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="982" cy="704.0" r="19.0"/><path d="M948.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<line class="d-line" x1="1032" y1="528" x2="1184" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="1180" cy="710.88" r="14.44"/><path d="M1154.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="1248" cy="710.88" r="14.44"/><path d="M1222.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="1214" cy="704.0" r="19.0"/><path d="M1180.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
</svg>
</div>
</figure>


<h3 class="relative group">单租户，多实例
    <div id="单租户多实例" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8d%95%e7%a7%9f%e6%88%b7%e5%a4%9a%e5%ae%9e%e4%be%8b" aria-label="锚点">#</a>
    </span>
    
</h3>

  



<div
  
    class="flex px-4 py-3 rounded-md shadow bg-primary-100 dark:bg-primary-900"
  
  >
  <span
    
      class="text-primary-400 pe-3 flex items-center"
    
    >
    <span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"/></svg>
</span>
  </span>

  <span
    
      class="dark:text-neutral-300"
    
    ><strong>单租户，多实例</strong>：在这种架构下，多套基础设施实例分别服务多个租户。</span>
</div>

<p>这条路的吸引力就在“隔离”二字，几乎所有好处都由此而来。每个客户的数据都在自己独立的环境里，安全审查和合规谈判会轻松很多。一次糟糕的部署或一个吃资源的任务，只会影响一个客户，而不是全部。每个实例可以独立扩容、调优、定制；资源专用，性能不会因为隔壁租户在跑什么而波动。维护也能按客户逐个安排，停机影响小而可控。定价合理的话，还能让基础设施成本对上每个租户的实际用量。</p>
<p>代价是：所有东西你都要运维N份。资源开销不断累积，实例之间的配置慢慢漂移，让整个集群的版本和数据保持一致本身就成了一个项目。扩容、隔离、成本管理的问题不会消失，只会成倍增加。</p>

<h3 class="relative group">多租户，单实例
    <div id="多租户单实例" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%a4%9a%e7%a7%9f%e6%88%b7%e5%8d%95%e5%ae%9e%e4%be%8b" aria-label="锚点">#</a>
    </span>
    
</h3>

  



<div
  
    class="flex px-4 py-3 rounded-md shadow bg-primary-100 dark:bg-primary-900"
  
  >
  <span
    
      class="text-primary-400 pe-3 flex items-center"
    
    >
    <span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"/></svg>
</span>
  </span>

  <span
    
      class="dark:text-neutral-300"
    
    ><strong>多租户，单实例</strong>：一套应用程序同时服务多个租户。</span>
</div>

<p>它是默认选项不是没有原因的：一套部署、一个要备份的数据库、生产环境只有一个版本。资源利用率高，运维简单，所有客户同时拿到更新和安全补丁。</p>
<p>难点是共享的另一面。一个租户用得猛，所有人的性能都会被拖累；按客户定制的空间有限；数据隔离不再靠基础设施，而是落在你的应用代码里，一个bug就可能变成跨租户的数据泄露。扩容瓶颈一来就是所有客户一起受影响，维护也要更小心，因为再也没有“只影响这一个客户”的爆炸半径了。</p>
<p><a href="https://www.scaleway.com/en/blog/saas-multi-tenant-vs-multi-instance-architectures/"  target="_blank" rel="noreferrer">延伸阅读：SaaS 方案中的多实例与多租户架构</a></p>

<h2 class="relative group">多租户 vs 多实例
    <div id="多租户-vs-多实例" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%a4%9a%e7%a7%9f%e6%88%b7-vs-%e5%a4%9a%e5%ae%9e%e4%be%8b" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>这个选择也常被说成“多租户 vs 多实例”，其实是同一件事从另一面看。“租户”说的是客户：一套软件有几个客户在共用。“实例”说的是部署：你跑了几套软件。单租户产品就是每个客户一套的多实例部署，多租户产品通常是大家共用的单实例。所以有人要“多实例”，意思就是要单租户。</p>
<p>实际上它更像一条光谱，而不是二选一，真正的决定大多落在数据库上。最共享的一端，是所有客户的数据行放在同一批表里，每行带一个 <code>tenant_id</code> 列，每条查询都按它过滤。运行成本最低，也最容易出错。往前一步，是在共享数据库里给每个客户单独一个 schema：隔离更干净，也能按客户备份，代价是每次迁移都要按 schema 的数量跑一遍。再往前，应用共享，但每个客户有自己的数据库：不用给每个客户跑一整套技术栈，数据隔离也够强，是很常见的折中。另一端就是完全的单租户，应用和数据库都是独享的。AWS 的 SaaS 指南把两端叫作 silo（筒仓）和 pool（池），两者混用叫 bridge（桥接），向客户的安全团队解释你的架构时，这套说法挺好用。</p>

<h2 class="relative group">单租户 vs 多租户一览
    <div id="单租户-vs-多租户一览" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8d%95%e7%a7%9f%e6%88%b7-vs-%e5%a4%9a%e7%a7%9f%e6%88%b7%e4%b8%80%e8%a7%88" aria-label="锚点">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>单租户（多实例）</th>
					<th>多租户（单实例）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>数据隔离</td>
					<td>每个客户独立的基础设施</td>
					<td>靠代码或数据库策略保证</td>
			</tr>
			<tr>
					<td>每个客户的成本</td>
					<td>高：每个客户一整套技术栈</td>
					<td>低：大家共用一套</td>
			</tr>
			<tr>
					<td>运维</td>
					<td>部署、升级、监控面板都按客户数量来</td>
					<td>各一份</td>
			</tr>
			<tr>
					<td>吵闹的邻居</td>
					<td>没有</td>
					<td>一个重度租户可能拖慢所有人</td>
			</tr>
			<tr>
					<td>定制</td>
					<td>可按客户定制，甚至版本都能不同</td>
					<td>基本只到配置和功能开关</td>
			</tr>
			<tr>
					<td>发布</td>
					<td>按客户逐个推</td>
					<td>所有人同时</td>
			</tr>
			<tr>
					<td>一次糟糕的部署会影响</td>
					<td>一个客户</td>
					<td>所有客户</td>
			</tr>
			<tr>
					<td>安全审查和数据驻留</td>
					<td>好回答</td>
					<td>要多费口舌</td>
			</tr>
	</tbody>
</table>

<h2 class="relative group">什么时候用哪种
    <div id="什么时候用哪种" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e7%94%a8%e5%93%aa%e7%a7%8d" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>除非有具体原因把你推开，否则就选多租户。客户多、每家付得不多、用户自己注册、团队小到三十套环境就能把人拖垮，这些情况下，多租户都是正确的默认选项。</p>
<p>单租户只在少数几种情况下值回它的开销。医疗、金融、政府类客户常常要求数据物理隔离，或者必须放在某个国家，而在安全审查里，独享实例是对此最简单也最老实的回答。如果只有几家签企业合同的大客户，他们负担得起自己那一套，往往也期待有一套。再就是客户需要自己的发布节奏、锁定某个版本，或者深度定制的时候，分开的实例就不再是开销，而成了你卖的产品的一部分。</p>
<p>大多数团队最后两种都用：绝大多数客户用多租户产品，少数需要并且愿意付钱的客户，作为企业版给独享实例。只要多租户版本从第一天起处处带着租户 ID，以后把某个客户单独迁到自己的数据库，就是一次迁移，而不是重写。</p>

<h2 class="relative group">让多租户的隔离更难被打破
    <div id="让多租户的隔离更难被打破" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%ae%a9%e5%a4%9a%e7%a7%9f%e6%88%b7%e7%9a%84%e9%9a%94%e7%a6%bb%e6%9b%b4%e9%9a%be%e8%a2%ab%e6%89%93%e7%a0%b4" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>多租户里最让人睡不着的 bug，是忘了加租户过滤的查询。它不会报错，只会悄悄把别人的数据返回给你，而代码评审也不可能每次都抓到。如果你用的是 Postgres，行级安全（RLS）可以把这道检查挪到数据库里，这样就算查询忘了过滤，也只能看到当前客户的数据行：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">invoices</span><span class="w"> </span><span class="n">ENABLE</span><span class="w"> </span><span class="k">ROW</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">SECURITY</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="k">CREATE</span><span class="w"> </span><span class="n">POLICY</span><span class="w"> </span><span class="n">tenant_isolation</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">invoices</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">USING</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;app.tenant_id&#39;</span><span class="p">)::</span><span class="n">uuid</span><span class="p">);</span></span></span></code></pre></div></div>
<p>然后每个请求在事务里用 <code>SET LOCAL app.tenant_id = '…'</code> 设置租户。这样连接池里的连接不会把上一个客户的 ID 带进下一个请求，忘了设置租户的请求会直接报错，而不是拿到数据。有两个坑：应用要用一个不是表所有者的角色连接，因为除非再加上 <code>FORCE ROW LEVEL SECURITY</code>，表的所有者会绕过这些策略；也永远不要用超级用户连接，超级用户无论如何都会绕过。</p>

<h2 class="relative group">单租户从哪里开始？
    <div id="单租户从哪里开始" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8d%95%e7%a7%9f%e6%88%b7%e4%bb%8e%e5%93%aa%e9%87%8c%e5%bc%80%e5%a7%8b" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>真要走单租户，工具决定它是“可以管”还是一场慢动作灾难。三十套环境，靠手是管不过来的。</p>
<p>核心是容器加基础设施即代码。Docker把应用打包成对每个租户都完全一致的形态，Kubernetes负责这一堆实例的部署和扩缩容；用Terraform、Pulumi或AWS CloudFormation把一个租户的完整技术栈定义成代码，第31个客户的开通就是一个PR，而不是一个报废的周末。如果你更贴近虚拟机的世界，Ansible、Chef、Puppet这类配置管理工具可以承担同样的角色。CI/CD流水线（Jenkins、GitLab CI/CD）在这里比平时更重要，因为每次发布都要在所有实例上滚动完成，不能靠人盯。</p>
<p>再往后就看产品形态了。有些工作负载可以直接用无服务器（AWS Lambda、Azure Functions）把服务器这一层整个抹掉。Amazon RDS、Azure SQL Database这类托管数据库，能把你从“每个租户手动维护一个库”的苦役里救出来。有了一个集群之后，监控（Prometheus、Grafana、New Relic）就不再是可选项：哪个实例不舒服，你得赶在客户开口之前知道。身份这块交给Auth0或Okta这样的IAM服务，就不用每个租户重造一遍认证。不管你用什么语言，多半都有面向SaaS的框架或库能帮你省掉一部分这类底层活儿；托管方面，AWS、Azure、Google Cloud Platform这些大厂云都能轻松接住。</p>
<p>别想着第一天就把清单买齐。先上容器和IaC，租户多起来再加监控和IAM，剩下的，让真实的痛点来决定。</p>

<h2 class="relative group">长话短说
    <div id="长话短说" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%95%bf%e8%af%9d%e7%9f%ad%e8%af%b4" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>单租户，也就是多实例，是每个客户一套独立的应用和数据库：隔离强、能按客户控制，代价是要运维一整个集群。多租户是大家共用一套：便宜、简单，隔离靠代码保证。大多数产品应该从多租户起步，处处带上租户 ID 并让数据库来强制执行，再给少数需要并愿意付钱的客户提供独享实例。如果真要跑单租户，让它保持可控的关键是容器和基础设施即代码。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-04-07-singletenantsingleinstance/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
