<?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>SaaS · 仁才德</title>
    <link>https://jared.lynskey.co.nz/zh-cn/tags/saas/</link>
    <description>关于软件工程与研发团队领导力的短文——绩效评估、招聘与团队扩张、CI/CD、DevOps 监控、智能体 AI。写于首尔。</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <copyright>© 2026 仁才德</copyright>
    <lastBuildDate>Mon, 26 Oct 2026 00:00:00 +1200</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/zh-cn/tags/saas/index.xml" rel="self" type="application/rss+xml" />
    <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>
