Explain Endor concepts with a reproducible multi-region topology
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 4m14s

This commit is contained in:
2026-10-27 17:45:30 +08:00
parent 75debc0068
commit 1c27f6bc1e
20 changed files with 1362 additions and 2 deletions
+8
View File
@@ -39,6 +39,14 @@ tools/figures/ 独立 Python 项目,使用 drawsvg 编程制图
生产构建会生成文档路由;新增或修改文档后重新运行 build。开发模式按请求读取内容。
main 推送通过 CI 验证后自动部署;Pull Request 只验证,不部署。工作流也支持手动触发。
## Endor 概念图
`content/docs/00-endor-concepts-draft.md` 解释全球入口、区域网关与 Runtime、分片、CDN、身份及跨区恢复的概念设计,页面为 `/docs/00-endor-concepts-draft`。
图形源码位于 `tools/figures/src/endor_figures/architecture.py`,由 `pnpm figures` 生成 `apps/docs/public/figures/endor-architecture.svg`。Python 负责可复现的节点、全连接和复制路径,React 查看器提供放大、滚动与 SVG 下载。无需外部图标、字体或绘图服务。图中的数量是示意,身份服务/CDN 接入属于建议,所有未实现的关系均以草案说明。
拓扑检查:`uv run --locked --project tools/figures python -m unittest discover -s tools/figures/tests -v`。容器检查流程会运行同一检查。
## CI/CD 与缓存
`.gitea/workflows/ci-deploy.yml` 使用已有 ces-control Runner,不修改共享 Runner 配置。
@@ -0,0 +1,637 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="1760" height="1640" viewBox="0 0 1760 1640">
<defs>
</defs>
<title>Endor 概念架构草案:全球入口、区域网关与运行时、分片和跨区恢复</title>
<rect x="0" y="0" width="1760" height="1640" fill="#fcfdfb" />
<text x="45" y="48" font-size="13" fill="#2d8b77" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">ENDOR / SYSTEM ATLAS</text>
<text x="45" y="92" font-size="31" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">一个全球入口,多个区域,一组可恢复的对象。</text>
<rect x="1520" y="40" width="192" height="25" rx="5" fill="#f8f2e6" stroke="none" stroke-width="1.2" />
<text x="1616.0" y="57" font-size="12" fill="#b58b47" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">概念设计 · 草案</text>
<text x="45" y="142" font-size="13" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">01 全球接入</text>
<path d="M170,214 L500,214" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M493.791035540545,217.23245422879037 L500,214 M493.791035540545,210.76754577120963 L500,214" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="280" y="201" font-size="13" fill="#2d8b77" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">动态 API / 对象访问</text>
<path d="M120,185 L120,163 L1160,163 L1160,184" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1156.7675457712096,177.791035540545 L1160,184 M1163.2324542287904,177.791035540545 L1160,184" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="815" y="155" font-size="13" fill="#467fb4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">静态资源 / 可缓存内容</text>
<path d="M110,250 L110,300 L1540,300 L1540,253" fill="none" stroke="#8a6ab4" stroke-width="1.6" stroke-dasharray="5 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1543.2324542287904,259.208964459455 L1540,253 M1536.7675457712096,259.208964459455 L1540,253" fill="none" stroke="#8a6ab4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="1140" y="290" font-size="13" fill="#8a6ab4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">登录 / 换取凭证</text>
<g transform="translate(120,214) scale(1)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-26" y="-20" width="52" height="33" rx="4" />
<path d="M-32,21 L32,21" />
<path d="M-26,13 L-32,21 L32,21 L26,13" />
<path d="M-6,17 L6,17" />
</g>
<text x="120" y="268" font-size="17" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="middle">全球客户端</text>
<g transform="translate(550,214) scale(1)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<circle cx="0" cy="0" r="23" />
<ellipse cx="0" cy="0" rx="10" ry="23" />
<path d="M-22,-8 L22,-8" />
<path d="M-22,8 L22,8" />
</g>
<text x="620" y="208" font-size="23" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">endorphin API</text>
<text x="620" y="237" font-size="14" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">全球流量入口 · 按健康与策略选区</text>
<g transform="translate(1160,214) scale(1)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<circle cx="0" cy="0" r="24" />
<path d="M-10,-6 L0,-14 L11,-6 L0,2 L-10,-6" />
<path d="M-10,3 L0,11 L11,3" />
<path d="M-10,11 L0,19 L11,11" />
</g>
<text x="1200" y="208" font-size="21" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">CDN</text>
<text x="1200" y="234" font-size="13" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">命中直接返回</text>
<g transform="translate(1540,214) scale(1)" stroke="#8a6ab4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M0,-25 L22,-16 L20,5 Q17,19,0,27 Q-17,19,-20,5 L-22,-16 Z" />
<path d="M-9,0 L-2,7 L11,-7" />
</g>
<text x="1580" y="207" font-size="17" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">身份服务</text>
<text x="1580" y="232" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">登录 / 签发凭证</text>
<text x="1580" y="256" font-size="11" fill="#8a6ab4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">独立控制服务 · 建议</text>
<rect x="40" y="337" width="780" height="815" rx="18" fill="#ffffff" stroke="#dce8e3" stroke-width="1.2" />
<text x="68" y="375" font-size="23" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">北美区域</text>
<text x="68" y="401" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">NA / 独立故障域</text>
<rect x="663" y="359" width="126" height="25" rx="5" fill="#f0f6f3" stroke="none" stroke-width="1.2" />
<text x="726.0" y="376" font-size="12" fill="#2d8b77" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">区域入口</text>
<path d="M800,252 L800,318 L430,318 L430,419" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M426.7675457712096,412.791035540545 L430,419 M433.2324542287904,412.791035540545 L430,419" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<g transform="translate(430,446) scale(0.72)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<circle cx="0" cy="-17" r="7" />
<circle cx="-22" cy="19" r="7" />
<circle cx="22" cy="19" r="7" />
<path d="M0,-10 L0,1 L-22,1 L-22,12" />
<path d="M0,1 L22,1 L22,12" />
</g>
<text x="472" y="442" font-size="16" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">区域负载均衡</text>
<text x="472" y="464" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">健康检查 / TLS 路由</text>
<path d="M430,470 L430,488 L275,488 L275,514" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M271.7675457712096,507.791035540545 L275,514 M278.2324542287904,507.791035540545 L275,514" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<path d="M430,470 L430,488 L585,488 L585,514" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M581.7675457712096,507.791035540545 L585,514 M588.2324542287904,507.791035540545 L585,514" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="67" y="528" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">TRAEFIK</text>
<text x="67" y="550" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">生产冗余 · 可选</text>
<g transform="translate(275,536) scale(0.9)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#2d8b77" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#2d8b77" />
<path d="M-8,8 L16,8" />
</g>
<text x="275" y="579" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">Traefik 1</text>
<g transform="translate(585,536) scale(0.9)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#2d8b77" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#2d8b77" />
<path d="M-8,8 L16,8" />
</g>
<text x="585" y="579" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">Traefik 2</text>
<rect x="627" y="522" width="78" height="25" rx="5" fill="#edf5f0" stroke="none" stroke-width="1.2" />
<text x="666.0" y="539" font-size="12" fill="#2d8b77" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">HA 副本</text>
<path d="M275,592 L175,665" fill="none" stroke="#2d8b77" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<path d="M275,592 L430,665" fill="none" stroke="#2d8b77" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<path d="M275,592 L685,665" fill="none" stroke="#2d8b77" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<circle cx="275" cy="592" r="3" fill="#fcfdfb" stroke="#2d8b77" stroke-width="1.5" />
<path d="M585,592 L175,665" fill="none" stroke="#2d8b77" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<path d="M585,592 L430,665" fill="none" stroke="#2d8b77" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<path d="M585,592 L685,665" fill="none" stroke="#2d8b77" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<circle cx="585" cy="592" r="3" fill="#fcfdfb" stroke="#2d8b77" stroke-width="1.5" />
<circle cx="175" cy="665" r="3" fill="#fcfdfb" stroke="#2d8b77" stroke-width="1.5" />
<g transform="translate(175,701) scale(1)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#2d8b77" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#2d8b77" />
<path d="M-8,8 L16,8" />
<path d="M-13,20 L13,20" />
<path d="M0,14 L0,20" />
</g>
<text x="175" y="746" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="middle">Nexus Runtime 1</text>
<g transform="translate(126,779) scale(0.65)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-17" y="-13" width="34" height="26" rx="4" />
<path d="M-10,-18 L-10,-13" />
<path d="M-10,13 L-10,18" />
<path d="M0,-18 L0,-13" />
<path d="M0,13 L0,18" />
<path d="M10,-18 L10,-13" />
<path d="M10,13 L10,18" />
<path d="M-22,-6 L-17,-6" />
<path d="M17,-6 L22,-6" />
<path d="M-22,6 L-17,6" />
<path d="M17,6 L22,6" />
<path d="M-6,-5 L0,0 L-6,5" />
<path d="M4,6 L10,6" />
</g>
<text x="154" y="785" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">Aster VM</text>
<text x="175" y="813" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">1 Nexus : 1 VM</text>
<circle cx="430" cy="665" r="3" fill="#fcfdfb" stroke="#2d8b77" stroke-width="1.5" />
<g transform="translate(430,701) scale(1)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#2d8b77" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#2d8b77" />
<path d="M-8,8 L16,8" />
<path d="M-13,20 L13,20" />
<path d="M0,14 L0,20" />
</g>
<text x="430" y="746" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="middle">Nexus Runtime 2</text>
<g transform="translate(381,779) scale(0.65)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-17" y="-13" width="34" height="26" rx="4" />
<path d="M-10,-18 L-10,-13" />
<path d="M-10,13 L-10,18" />
<path d="M0,-18 L0,-13" />
<path d="M0,13 L0,18" />
<path d="M10,-18 L10,-13" />
<path d="M10,13 L10,18" />
<path d="M-22,-6 L-17,-6" />
<path d="M17,-6 L22,-6" />
<path d="M-22,6 L-17,6" />
<path d="M17,6 L22,6" />
<path d="M-6,-5 L0,0 L-6,5" />
<path d="M4,6 L10,6" />
</g>
<text x="409" y="785" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">Aster VM</text>
<text x="430" y="813" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">1 Nexus : 1 VM</text>
<circle cx="685" cy="665" r="3" fill="#fcfdfb" stroke="#2d8b77" stroke-width="1.5" />
<g transform="translate(685,701) scale(1)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#2d8b77" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#2d8b77" />
<path d="M-8,8 L16,8" />
<path d="M-13,20 L13,20" />
<path d="M0,14 L0,20" />
</g>
<text x="685" y="746" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="middle">Nexus Runtime 3</text>
<g transform="translate(636,779) scale(0.65)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-17" y="-13" width="34" height="26" rx="4" />
<path d="M-10,-18 L-10,-13" />
<path d="M-10,13 L-10,18" />
<path d="M0,-18 L0,-13" />
<path d="M0,13 L0,18" />
<path d="M10,-18 L10,-13" />
<path d="M10,13 L10,18" />
<path d="M-22,-6 L-17,-6" />
<path d="M17,-6 L22,-6" />
<path d="M-22,6 L-17,6" />
<path d="M17,6 L22,6" />
<path d="M-6,-5 L0,0 L-6,5" />
<path d="M4,6 L10,6" />
</g>
<text x="664" y="785" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">Aster VM</text>
<text x="685" y="813" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">1 Nexus : 1 VM</text>
<text x="62" y="621" font-size="11" fill="#2d8b77" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">K₂,₃ 全连接</text>
<text x="62" y="640" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">选择健康节点</text>
<path d="M175,829 L175,850" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.8" />
<path d="M430,829 L430,850" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.8" />
<path d="M685,829 L685,850" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.8" />
<path d="M175,850 L685,850" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.8" />
<text x="430" y="874" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">分片定位 / 对象读写</text>
<path d="M239,850 L239,905" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M235.76754577120963,898.791035540545 L239,905 M242.23245422879037,898.791035540545 L239,905" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<rect x="64" y="912" width="350" height="208" rx="10" fill="#f8faf7" stroke="#e2eae1" stroke-width="1.2" />
<text x="79" y="939" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">NA-01 / 数据分片</text>
<rect x="276" y="921" width="124" height="25" rx="5" fill="#edf3ec" stroke="none" stroke-width="1.2" />
<text x="338.0" y="938" font-size="12" fill="#2d8b77" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">主 + 区域 HA</text>
<g transform="translate(126,989) scale(0.8)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M-21,-12 L-21,15 C-21,27,21,27,21,15 L21,-12" />
<ellipse cx="0" cy="-12" rx="21" ry="8" />
<path d="M-21,1 C-21,12,21,12,21,1" />
</g>
<text x="126" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">PostgreSQL</text>
<path d="M126,1041 L126,1062" fill="none" stroke="#2d8b77" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(126,1080) scale(0.38)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M-21,-12 L-21,15 C-21,27,21,27,21,15 L21,-12" />
<ellipse cx="0" cy="-12" rx="21" ry="8" />
<path d="M-21,1 C-21,12,21,12,21,1" />
</g>
<g transform="translate(239,989) scale(0.8)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-23" y="-19" width="22" height="20" rx="5" />
<circle cx="-12" cy="-9" r="3" fill="#2d8b77" stroke="none" />
<rect x="1" y="-19" width="22" height="20" rx="5" />
<circle cx="12" cy="-9" r="3" fill="#2d8b77" stroke="none" />
<rect x="-11" y="2" width="22" height="20" rx="5" />
<circle cx="0" cy="12" r="3" fill="#2d8b77" stroke="none" />
</g>
<text x="239" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">Aerarium</text>
<path d="M239,1041 L239,1062" fill="none" stroke="#2d8b77" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(239,1080) scale(0.38)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-23" y="-19" width="22" height="20" rx="5" />
<circle cx="-12" cy="-9" r="3" fill="#2d8b77" stroke="none" />
<rect x="1" y="-19" width="22" height="20" rx="5" />
<circle cx="12" cy="-9" r="3" fill="#2d8b77" stroke="none" />
<rect x="-11" y="2" width="22" height="20" rx="5" />
<circle cx="0" cy="12" r="3" fill="#2d8b77" stroke="none" />
</g>
<g transform="translate(352,989) scale(0.8)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="0" cy="-15" rx="23" ry="7" />
<path d="M-23,-15 L-18,21 L18,21 L23,-15" />
<path d="M-17,6 Q0,17,17,6" />
</g>
<text x="352" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">S3</text>
<path d="M352,1041 L352,1062" fill="none" stroke="#2d8b77" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(352,1080) scale(0.38)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="0" cy="-15" rx="23" ry="7" />
<path d="M-23,-15 L-18,21 L18,21 L23,-15" />
<path d="M-17,6 Q0,17,17,6" />
</g>
<path d="M214,986 L151,986" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M157.208964459455,982.7675457712096 L151,986 M157.208964459455,989.2324542287904 L151,986" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<path d="M264,986 L327,986" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M320.791035540545,989.2324542287904 L327,986 M320.791035540545,982.7675457712096 L327,986" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="239" y="1052" font-size="10" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">Object data pool</text>
<text x="239" y="1108" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">区域内副本 · 跨故障域放置</text>
<circle cx="239" cy="1120" r="3" fill="#fcfdfb" stroke="#b58b47" stroke-width="1.5" />
<path d="M621,850 L621,905" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M617.7675457712096,898.791035540545 L621,905 M624.2324542287904,898.791035540545 L621,905" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<rect x="446" y="912" width="350" height="208" rx="10" fill="#f8faf7" stroke="#e2eae1" stroke-width="1.2" />
<text x="461" y="939" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">NA-02 / 数据分片</text>
<rect x="658" y="921" width="124" height="25" rx="5" fill="#edf3ec" stroke="none" stroke-width="1.2" />
<text x="720.0" y="938" font-size="12" fill="#2d8b77" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">主 + 区域 HA</text>
<g transform="translate(508,989) scale(0.8)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M-21,-12 L-21,15 C-21,27,21,27,21,15 L21,-12" />
<ellipse cx="0" cy="-12" rx="21" ry="8" />
<path d="M-21,1 C-21,12,21,12,21,1" />
</g>
<text x="508" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">PostgreSQL</text>
<path d="M508,1041 L508,1062" fill="none" stroke="#2d8b77" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(508,1080) scale(0.38)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M-21,-12 L-21,15 C-21,27,21,27,21,15 L21,-12" />
<ellipse cx="0" cy="-12" rx="21" ry="8" />
<path d="M-21,1 C-21,12,21,12,21,1" />
</g>
<g transform="translate(621,989) scale(0.8)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-23" y="-19" width="22" height="20" rx="5" />
<circle cx="-12" cy="-9" r="3" fill="#2d8b77" stroke="none" />
<rect x="1" y="-19" width="22" height="20" rx="5" />
<circle cx="12" cy="-9" r="3" fill="#2d8b77" stroke="none" />
<rect x="-11" y="2" width="22" height="20" rx="5" />
<circle cx="0" cy="12" r="3" fill="#2d8b77" stroke="none" />
</g>
<text x="621" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">Aerarium</text>
<path d="M621,1041 L621,1062" fill="none" stroke="#2d8b77" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(621,1080) scale(0.38)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-23" y="-19" width="22" height="20" rx="5" />
<circle cx="-12" cy="-9" r="3" fill="#2d8b77" stroke="none" />
<rect x="1" y="-19" width="22" height="20" rx="5" />
<circle cx="12" cy="-9" r="3" fill="#2d8b77" stroke="none" />
<rect x="-11" y="2" width="22" height="20" rx="5" />
<circle cx="0" cy="12" r="3" fill="#2d8b77" stroke="none" />
</g>
<g transform="translate(734,989) scale(0.8)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="0" cy="-15" rx="23" ry="7" />
<path d="M-23,-15 L-18,21 L18,21 L23,-15" />
<path d="M-17,6 Q0,17,17,6" />
</g>
<text x="734" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">S3</text>
<path d="M734,1041 L734,1062" fill="none" stroke="#2d8b77" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(734,1080) scale(0.38)" stroke="#2d8b77" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="0" cy="-15" rx="23" ry="7" />
<path d="M-23,-15 L-18,21 L18,21 L23,-15" />
<path d="M-17,6 Q0,17,17,6" />
</g>
<path d="M596,986 L533,986" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M539.208964459455,982.7675457712096 L533,986 M539.208964459455,989.2324542287904 L533,986" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<path d="M646,986 L709,986" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M702.791035540545,989.2324542287904 L709,986 M702.791035540545,982.7675457712096 L709,986" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="621" y="1052" font-size="10" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">Object data pool</text>
<text x="621" y="1108" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">区域内副本 · 跨故障域放置</text>
<circle cx="621" cy="1120" r="3" fill="#fcfdfb" stroke="#b58b47" stroke-width="1.5" />
<rect x="910" y="337" width="780" height="815" rx="18" fill="#ffffff" stroke="#dce8e3" stroke-width="1.2" />
<text x="938" y="375" font-size="23" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">亚洲区域</text>
<text x="938" y="401" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">AS / 独立故障域</text>
<rect x="1533" y="359" width="126" height="25" rx="5" fill="#f0f6f3" stroke="none" stroke-width="1.2" />
<text x="1596.0" y="376" font-size="12" fill="#467fb4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">区域入口</text>
<path d="M800,252 L800,318 L1300,318 L1300,419" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1296.7675457712096,412.791035540545 L1300,419 M1303.2324542287904,412.791035540545 L1300,419" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<g transform="translate(1300,446) scale(0.72)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<circle cx="0" cy="-17" r="7" />
<circle cx="-22" cy="19" r="7" />
<circle cx="22" cy="19" r="7" />
<path d="M0,-10 L0,1 L-22,1 L-22,12" />
<path d="M0,1 L22,1 L22,12" />
</g>
<text x="1342" y="442" font-size="16" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">区域负载均衡</text>
<text x="1342" y="464" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">健康检查 / TLS 路由</text>
<path d="M1300,470 L1300,488 L1145,488 L1145,514" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1141.7675457712096,507.791035540545 L1145,514 M1148.2324542287904,507.791035540545 L1145,514" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<path d="M1300,470 L1300,488 L1455,488 L1455,514" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1451.7675457712096,507.791035540545 L1455,514 M1458.2324542287904,507.791035540545 L1455,514" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="937" y="528" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">TRAEFIK</text>
<text x="937" y="550" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">生产冗余 · 可选</text>
<g transform="translate(1145,536) scale(0.9)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#467fb4" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#467fb4" />
<path d="M-8,8 L16,8" />
</g>
<text x="1145" y="579" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">Traefik 1</text>
<g transform="translate(1455,536) scale(0.9)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#467fb4" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#467fb4" />
<path d="M-8,8 L16,8" />
</g>
<text x="1455" y="579" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">Traefik 2</text>
<rect x="1497" y="522" width="78" height="25" rx="5" fill="#edf5f0" stroke="none" stroke-width="1.2" />
<text x="1536.0" y="539" font-size="12" fill="#467fb4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">HA 副本</text>
<path d="M1145,592 L1045,665" fill="none" stroke="#467fb4" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<path d="M1145,592 L1300,665" fill="none" stroke="#467fb4" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<path d="M1145,592 L1555,665" fill="none" stroke="#467fb4" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<circle cx="1145" cy="592" r="3" fill="#fcfdfb" stroke="#467fb4" stroke-width="1.5" />
<path d="M1455,592 L1045,665" fill="none" stroke="#467fb4" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<path d="M1455,592 L1300,665" fill="none" stroke="#467fb4" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<path d="M1455,592 L1555,665" fill="none" stroke="#467fb4" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.65" />
<circle cx="1455" cy="592" r="3" fill="#fcfdfb" stroke="#467fb4" stroke-width="1.5" />
<circle cx="1045" cy="665" r="3" fill="#fcfdfb" stroke="#467fb4" stroke-width="1.5" />
<g transform="translate(1045,701) scale(1)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#467fb4" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#467fb4" />
<path d="M-8,8 L16,8" />
<path d="M-13,20 L13,20" />
<path d="M0,14 L0,20" />
</g>
<text x="1045" y="746" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="middle">Nexus Runtime 1</text>
<g transform="translate(996,779) scale(0.65)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-17" y="-13" width="34" height="26" rx="4" />
<path d="M-10,-18 L-10,-13" />
<path d="M-10,13 L-10,18" />
<path d="M0,-18 L0,-13" />
<path d="M0,13 L0,18" />
<path d="M10,-18 L10,-13" />
<path d="M10,13 L10,18" />
<path d="M-22,-6 L-17,-6" />
<path d="M17,-6 L22,-6" />
<path d="M-22,6 L-17,6" />
<path d="M17,6 L22,6" />
<path d="M-6,-5 L0,0 L-6,5" />
<path d="M4,6 L10,6" />
</g>
<text x="1024" y="785" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">Aster VM</text>
<text x="1045" y="813" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">1 Nexus : 1 VM</text>
<circle cx="1300" cy="665" r="3" fill="#fcfdfb" stroke="#467fb4" stroke-width="1.5" />
<g transform="translate(1300,701) scale(1)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#467fb4" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#467fb4" />
<path d="M-8,8 L16,8" />
<path d="M-13,20 L13,20" />
<path d="M0,14 L0,20" />
</g>
<text x="1300" y="746" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="middle">Nexus Runtime 2</text>
<g transform="translate(1251,779) scale(0.65)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-17" y="-13" width="34" height="26" rx="4" />
<path d="M-10,-18 L-10,-13" />
<path d="M-10,13 L-10,18" />
<path d="M0,-18 L0,-13" />
<path d="M0,13 L0,18" />
<path d="M10,-18 L10,-13" />
<path d="M10,13 L10,18" />
<path d="M-22,-6 L-17,-6" />
<path d="M17,-6 L22,-6" />
<path d="M-22,6 L-17,6" />
<path d="M17,6 L22,6" />
<path d="M-6,-5 L0,0 L-6,5" />
<path d="M4,6 L10,6" />
</g>
<text x="1279" y="785" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">Aster VM</text>
<text x="1300" y="813" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">1 Nexus : 1 VM</text>
<circle cx="1555" cy="665" r="3" fill="#fcfdfb" stroke="#467fb4" stroke-width="1.5" />
<g transform="translate(1555,701) scale(1)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-14" width="50" height="12" rx="3" />
<circle cx="-16" cy="-8" r="1.3" fill="#467fb4" />
<path d="M-8,-8 L16,-8" />
<rect x="-25" y="2" width="50" height="12" rx="3" />
<circle cx="-16" cy="8" r="1.3" fill="#467fb4" />
<path d="M-8,8 L16,8" />
<path d="M-13,20 L13,20" />
<path d="M0,14 L0,20" />
</g>
<text x="1555" y="746" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="middle">Nexus Runtime 3</text>
<g transform="translate(1506,779) scale(0.65)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-17" y="-13" width="34" height="26" rx="4" />
<path d="M-10,-18 L-10,-13" />
<path d="M-10,13 L-10,18" />
<path d="M0,-18 L0,-13" />
<path d="M0,13 L0,18" />
<path d="M10,-18 L10,-13" />
<path d="M10,13 L10,18" />
<path d="M-22,-6 L-17,-6" />
<path d="M17,-6 L22,-6" />
<path d="M-22,6 L-17,6" />
<path d="M17,6 L22,6" />
<path d="M-6,-5 L0,0 L-6,5" />
<path d="M4,6 L10,6" />
</g>
<text x="1534" y="785" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">Aster VM</text>
<text x="1555" y="813" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">1 Nexus : 1 VM</text>
<text x="932" y="621" font-size="11" fill="#467fb4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">K₂,₃ 全连接</text>
<text x="932" y="640" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">选择健康节点</text>
<path d="M1045,829 L1045,850" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.8" />
<path d="M1300,829 L1300,850" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.8" />
<path d="M1555,829 L1555,850" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.8" />
<path d="M1045,850 L1555,850" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="0.8" />
<text x="1300" y="874" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">分片定位 / 对象读写</text>
<path d="M1109,850 L1109,905" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1105.7675457712096,898.791035540545 L1109,905 M1112.2324542287904,898.791035540545 L1109,905" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<rect x="934" y="912" width="350" height="208" rx="10" fill="#f8faf7" stroke="#e2eae1" stroke-width="1.2" />
<text x="949" y="939" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">AS-01 / 数据分片</text>
<rect x="1146" y="921" width="124" height="25" rx="5" fill="#edf3ec" stroke="none" stroke-width="1.2" />
<text x="1208.0" y="938" font-size="12" fill="#467fb4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">主 + 区域 HA</text>
<g transform="translate(996,989) scale(0.8)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M-21,-12 L-21,15 C-21,27,21,27,21,15 L21,-12" />
<ellipse cx="0" cy="-12" rx="21" ry="8" />
<path d="M-21,1 C-21,12,21,12,21,1" />
</g>
<text x="996" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">PostgreSQL</text>
<path d="M996,1041 L996,1062" fill="none" stroke="#467fb4" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(996,1080) scale(0.38)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M-21,-12 L-21,15 C-21,27,21,27,21,15 L21,-12" />
<ellipse cx="0" cy="-12" rx="21" ry="8" />
<path d="M-21,1 C-21,12,21,12,21,1" />
</g>
<g transform="translate(1109,989) scale(0.8)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-23" y="-19" width="22" height="20" rx="5" />
<circle cx="-12" cy="-9" r="3" fill="#467fb4" stroke="none" />
<rect x="1" y="-19" width="22" height="20" rx="5" />
<circle cx="12" cy="-9" r="3" fill="#467fb4" stroke="none" />
<rect x="-11" y="2" width="22" height="20" rx="5" />
<circle cx="0" cy="12" r="3" fill="#467fb4" stroke="none" />
</g>
<text x="1109" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">Aerarium</text>
<path d="M1109,1041 L1109,1062" fill="none" stroke="#467fb4" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(1109,1080) scale(0.38)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-23" y="-19" width="22" height="20" rx="5" />
<circle cx="-12" cy="-9" r="3" fill="#467fb4" stroke="none" />
<rect x="1" y="-19" width="22" height="20" rx="5" />
<circle cx="12" cy="-9" r="3" fill="#467fb4" stroke="none" />
<rect x="-11" y="2" width="22" height="20" rx="5" />
<circle cx="0" cy="12" r="3" fill="#467fb4" stroke="none" />
</g>
<g transform="translate(1222,989) scale(0.8)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="0" cy="-15" rx="23" ry="7" />
<path d="M-23,-15 L-18,21 L18,21 L23,-15" />
<path d="M-17,6 Q0,17,17,6" />
</g>
<text x="1222" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">S3</text>
<path d="M1222,1041 L1222,1062" fill="none" stroke="#467fb4" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(1222,1080) scale(0.38)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="0" cy="-15" rx="23" ry="7" />
<path d="M-23,-15 L-18,21 L18,21 L23,-15" />
<path d="M-17,6 Q0,17,17,6" />
</g>
<path d="M1084,986 L1021,986" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1027.208964459455,982.7675457712096 L1021,986 M1027.208964459455,989.2324542287904 L1021,986" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<path d="M1134,986 L1197,986" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1190.791035540545,989.2324542287904 L1197,986 M1190.791035540545,982.7675457712096 L1197,986" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="1109" y="1052" font-size="10" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">Object data pool</text>
<text x="1109" y="1108" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">区域内副本 · 跨故障域放置</text>
<circle cx="1109" cy="1120" r="3" fill="#fcfdfb" stroke="#b58b47" stroke-width="1.5" />
<path d="M1491,850 L1491,905" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1487.7675457712096,898.791035540545 L1491,905 M1494.2324542287904,898.791035540545 L1491,905" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<rect x="1316" y="912" width="350" height="208" rx="10" fill="#f8faf7" stroke="#e2eae1" stroke-width="1.2" />
<text x="1331" y="939" font-size="14" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">AS-02 / 数据分片</text>
<rect x="1528" y="921" width="124" height="25" rx="5" fill="#edf3ec" stroke="none" stroke-width="1.2" />
<text x="1590.0" y="938" font-size="12" fill="#467fb4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">主 + 区域 HA</text>
<g transform="translate(1378,989) scale(0.8)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M-21,-12 L-21,15 C-21,27,21,27,21,15 L21,-12" />
<ellipse cx="0" cy="-12" rx="21" ry="8" />
<path d="M-21,1 C-21,12,21,12,21,1" />
</g>
<text x="1378" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">PostgreSQL</text>
<path d="M1378,1041 L1378,1062" fill="none" stroke="#467fb4" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(1378,1080) scale(0.38)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M-21,-12 L-21,15 C-21,27,21,27,21,15 L21,-12" />
<ellipse cx="0" cy="-12" rx="21" ry="8" />
<path d="M-21,1 C-21,12,21,12,21,1" />
</g>
<g transform="translate(1491,989) scale(0.8)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-23" y="-19" width="22" height="20" rx="5" />
<circle cx="-12" cy="-9" r="3" fill="#467fb4" stroke="none" />
<rect x="1" y="-19" width="22" height="20" rx="5" />
<circle cx="12" cy="-9" r="3" fill="#467fb4" stroke="none" />
<rect x="-11" y="2" width="22" height="20" rx="5" />
<circle cx="0" cy="12" r="3" fill="#467fb4" stroke="none" />
</g>
<text x="1491" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">Aerarium</text>
<path d="M1491,1041 L1491,1062" fill="none" stroke="#467fb4" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(1491,1080) scale(0.38)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-23" y="-19" width="22" height="20" rx="5" />
<circle cx="-12" cy="-9" r="3" fill="#467fb4" stroke="none" />
<rect x="1" y="-19" width="22" height="20" rx="5" />
<circle cx="12" cy="-9" r="3" fill="#467fb4" stroke="none" />
<rect x="-11" y="2" width="22" height="20" rx="5" />
<circle cx="0" cy="12" r="3" fill="#467fb4" stroke="none" />
</g>
<g transform="translate(1604,989) scale(0.8)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="0" cy="-15" rx="23" ry="7" />
<path d="M-23,-15 L-18,21 L18,21 L23,-15" />
<path d="M-17,6 Q0,17,17,6" />
</g>
<text x="1604" y="1033" font-size="13" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">S3</text>
<path d="M1604,1041 L1604,1062" fill="none" stroke="#467fb4" stroke-width="1.1" stroke-dasharray="2 4" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<g transform="translate(1604,1080) scale(0.38)" stroke="#467fb4" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="0" cy="-15" rx="23" ry="7" />
<path d="M-23,-15 L-18,21 L18,21 L23,-15" />
<path d="M-17,6 Q0,17,17,6" />
</g>
<path d="M1466,986 L1403,986" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1409.208964459455,982.7675457712096 L1403,986 M1409.208964459455,989.2324542287904 L1403,986" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<path d="M1516,986 L1579,986" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1572.791035540545,989.2324542287904 L1579,986 M1572.791035540545,982.7675457712096 L1579,986" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="1491" y="1052" font-size="10" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">Object data pool</text>
<text x="1491" y="1108" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">区域内副本 · 跨故障域放置</text>
<circle cx="1491" cy="1120" r="3" fill="#fcfdfb" stroke="#b58b47" stroke-width="1.5" />
<path d="M1160,253 L1160,278 L1718,278 L1718,883 L1680,883 L1680,989 L1627,989" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-dasharray="2 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1633.208964459455,985.7675457712096 L1627,989 M1633.208964459455,992.2324542287904 L1627,989" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<rect x="1698" y="582" width="58" height="46" rx="4" fill="#fcfdfb" stroke="none" stroke-width="1.2" />
<text x="1727" y="600" font-size="12" fill="#467fb4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">CDN</text>
<text x="1727" y="621" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">S3 回源</text>
<path d="M1540,245 L1540,309 L869,309 L869,705" fill="none" stroke="#8a6ab4" stroke-width="1.6" stroke-dasharray="5 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M865.7675457712096,698.791035540545 L869,705 M872.2324542287904,698.791035540545 L869,705" fill="none" stroke="#8a6ab4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<path d="M869,705 L710,705" fill="none" stroke="#8a6ab4" stroke-width="1.6" stroke-dasharray="5 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M716.208964459455,701.7675457712096 L710,705 M716.208964459455,708.2324542287904 L710,705" fill="none" stroke="#8a6ab4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<path d="M869,705 L1020,705" fill="none" stroke="#8a6ab4" stroke-width="1.6" stroke-dasharray="5 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1013.791035540545,708.2324542287904 L1020,705 M1013.791035540545,701.7675457712096 L1020,705" fill="none" stroke="#8a6ab4" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<rect x="791" y="682" width="156" height="39" rx="4" fill="#fcfdfb" stroke="none" stroke-width="1.2" />
<text x="869" y="699" font-size="12" fill="#8a6ab4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">JWKS / 凭证验证</text>
<text x="869" y="716" font-size="11" fill="#8a6ab4" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="middle">Nexus 本地验证</text>
<text x="45" y="1486" font-size="16" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">02 跨区域恢复层</text>
<text x="45" y="1514" font-size="14" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">每个源分片都有一个异地目标;正常读取优先本区域,故障时恢复或转至已追平的副本。</text>
<rect x="40" y="1280" width="780" height="174" rx="14" fill="#fbf8f1" stroke="#e9dec8" stroke-width="1.2" />
<text x="65" y="1310" font-size="17" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">亚洲分片的异地备份 · 位于北美</text>
<path d="M1109,1120 L1121,1134 L1121,1180 L227,1180 L227,1280" fill="none" stroke="#b58b47" stroke-width="1.6" stroke-dasharray="6 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M223.76754577120963,1273.791035540545 L227,1280 M230.23245422879037,1273.791035540545 L227,1280" fill="none" stroke="#b58b47" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<g transform="translate(162,1376) scale(0.85)" stroke="#b58b47" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-15" width="50" height="36" rx="4" />
<path d="M-29,-15 L29,-15" />
<path d="M-6,5 L0,-2 L6,5" />
<path d="M0,-2 L0,14" />
</g>
<text x="201" y="1369" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">AS-01 备份集</text>
<text x="201" y="1393" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">PG + Aerarium + S3</text>
<text x="201" y="1415" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">恢复点 / 清单校验 / 重试</text>
<path d="M1491,1120 L1503,1134 L1503,1200 L609,1200 L609,1280" fill="none" stroke="#b58b47" stroke-width="1.6" stroke-dasharray="6 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M605.7675457712096,1273.791035540545 L609,1280 M612.2324542287904,1273.791035540545 L609,1280" fill="none" stroke="#b58b47" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<g transform="translate(544,1376) scale(0.85)" stroke="#b58b47" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-15" width="50" height="36" rx="4" />
<path d="M-29,-15 L29,-15" />
<path d="M-6,5 L0,-2 L6,5" />
<path d="M0,-2 L0,14" />
</g>
<text x="583" y="1369" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">AS-02 备份集</text>
<text x="583" y="1393" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">PG + Aerarium + S3</text>
<text x="583" y="1415" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">恢复点 / 清单校验 / 重试</text>
<rect x="910" y="1280" width="780" height="174" rx="14" fill="#fbf8f1" stroke="#e9dec8" stroke-width="1.2" />
<text x="935" y="1310" font-size="17" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">北美分片的异地备份 · 位于亚洲</text>
<path d="M239,1120 L251,1134 L251,1220 L1097,1220 L1097,1280" fill="none" stroke="#b58b47" stroke-width="1.6" stroke-dasharray="6 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1093.7675457712096,1273.791035540545 L1097,1280 M1100.2324542287904,1273.791035540545 L1097,1280" fill="none" stroke="#b58b47" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<g transform="translate(1032,1376) scale(0.85)" stroke="#b58b47" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-15" width="50" height="36" rx="4" />
<path d="M-29,-15 L29,-15" />
<path d="M-6,5 L0,-2 L6,5" />
<path d="M0,-2 L0,14" />
</g>
<text x="1071" y="1369" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">NA-01 备份集</text>
<text x="1071" y="1393" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">PG + Aerarium + S3</text>
<text x="1071" y="1415" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">恢复点 / 清单校验 / 重试</text>
<path d="M621,1120 L633,1134 L633,1240 L1479,1240 L1479,1280" fill="none" stroke="#b58b47" stroke-width="1.6" stroke-dasharray="6 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M1475.7675457712096,1273.791035540545 L1479,1280 M1482.2324542287904,1273.791035540545 L1479,1280" fill="none" stroke="#b58b47" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<g transform="translate(1414,1376) scale(0.85)" stroke="#b58b47" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-25" y="-15" width="50" height="36" rx="4" />
<path d="M-29,-15 L29,-15" />
<path d="M-6,5 L0,-2 L6,5" />
<path d="M0,-2 L0,14" />
</g>
<text x="1453" y="1369" font-size="15" fill="#233b38" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="600" text-anchor="start">NA-02 备份集</text>
<text x="1453" y="1393" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">PG + Aerarium + S3</text>
<text x="1453" y="1415" font-size="11" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">恢复点 / 清单校验 / 重试</text>
<rect x="724" y="1148" width="312" height="25" rx="4" fill="#fcfdfb" stroke="none" stroke-width="1.2" />
<text x="880" y="1166" font-size="13" fill="#b58b47" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="500" text-anchor="middle">异步复制 / 版本化备份</text>
<path d="M50,1555 L105,1555" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<path d="M98.79103554054501,1558.2324542287904 L105,1555 M98.79103554054501,1551.7675457712096 L105,1555" fill="none" stroke="#2d8b77" stroke-width="1.6" stroke-linecap="round" opacity="1" />
<text x="118" y="1560" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">请求与数据依赖</text>
<path d="M355,1555 L410,1555" fill="none" stroke="#8a6ab4" stroke-width="1.6" stroke-dasharray="5 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<text x="423" y="1560" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">身份控制 / 公钥</text>
<path d="M670,1555 L725,1555" fill="none" stroke="#467fb4" stroke-width="1.6" stroke-dasharray="2 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<text x="738" y="1560" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">缓存回源</text>
<path d="M950,1555 L1005,1555" fill="none" stroke="#b58b47" stroke-width="1.6" stroke-dasharray="6 6" stroke-linecap="round" stroke-linejoin="round" opacity="1" />
<text x="1018" y="1560" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">跨区复制 / 备份</text>
<text x="45" y="1608" font-size="12" fill="#6d817c" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="start">示意数量:2 区域 × 2 网关 × 3 Runtime × 2 分片;数量不构成部署要求。</text>
<text x="1715" y="1608" font-size="12" fill="#b58b47" font-family="ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif" font-weight="400" text-anchor="end">可恢复性以已持久化且已校验的恢复点为边界;异步 ≠ 零 RPO。</text>
</svg>

After

Width:  |  Height:  |  Size: 61 KiB

@@ -0,0 +1,105 @@
import { useRef, useState } from "react";
const source = "/figures/endor-architecture.svg";
export function ArchitectureFigure({ alt }: { alt: string }) {
const dialog = useRef<HTMLDialogElement>(null);
const trigger = useRef<HTMLButtonElement>(null);
const pan = useRef<HTMLDivElement>(null);
const [fit, setFit] = useState(true);
const [zoom, setZoom] = useState(1);
function open() {
setFit(true);
setZoom(1);
dialog.current?.showModal();
}
function changeZoom(delta: number) {
const current = fit
? ((pan.current?.clientWidth ?? 1776) - 16) / 1760
: zoom;
setFit(false);
setZoom(
Math.max(0.1, Math.min(2, Math.round((current + delta) * 100) / 100)),
);
}
return (
<figure className="architecture-figure">
<span className="figure-toolbar">
<span>ENDOR / 概念架构草案</span>
<button ref={trigger} onClick={open}>
放大查看 ↗
</button>
</span>
<button
className="figure-preview"
onClick={open}
aria-label="放大 Endor 架构图"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={source} alt={alt} />
</button>
<span className="figure-caption">
两区域拓扑 · 点击放大查看全连接与复制路径
</span>
<dialog
ref={dialog}
className="figure-dialog"
aria-label="Endor 架构图查看器"
onClose={() => trigger.current?.focus()}
>
<div className="figure-viewer-toolbar">
<strong>
Endor 概念架构 <small>草案</small>
</strong>
<div className="figure-controls">
<button
onClick={() => changeZoom(-0.25)}
aria-label="缩小架构图"
disabled={!fit && zoom <= 0.1}
>
−
</button>
<span aria-live="polite">
{fit ? "适应宽度" : `${Math.round(zoom * 100)}%`}
</span>
<button
onClick={() => changeZoom(0.25)}
aria-label="放大架构图"
disabled={!fit && zoom >= 2}
>
+
</button>
<button onClick={() => setFit(true)}>适应</button>
<a href={source} download="endor-architecture.svg">
下载 SVG
</a>
<button
onClick={() => dialog.current?.close()}
aria-label="关闭架构图"
>
关闭 ×
</button>
</div>
</div>
<div
ref={pan}
className="figure-pan"
tabIndex={0}
aria-label="架构图滚动区域,可使用方向键滚动"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={source}
alt={alt}
style={{ width: fit ? "100%" : 1760 * zoom }}
/>
</div>
<p className="figure-viewer-hint">
实线:请求 /
依赖 · 紫色:身份 · 蓝色点线:回源 · 金色:跨区备份。放大后可横向、纵向滚动,Esc
关闭。
</p>
</dialog>
</figure>
);
}
+1 -1
View File
@@ -116,7 +116,7 @@ export function Shell({
<Brand />
<nav className="top-nav" aria-label="主导航">
<Link href="/">文档概览</Link>
<Link href="/docs/02-mounting-draft">架构设计</Link>
<Link href="/docs/00-endor-concepts-draft">架构设计</Link>
<a href="https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs">
仓库 ↗
</a>
+7
View File
@@ -5,6 +5,13 @@ export type Section = {
symbol: string;
};
export function sectionFor(slug: string): Section {
if (slug.includes("endor-concepts"))
return {
group: "架构设计",
label: "概念草案",
symbol: "◇",
description: "从全球入口、区域运行时到对象数据,理解 Endor 的概念拓扑。",
};
if (slug.includes("mounting"))
return {
group: "架构设计",
+17
View File
@@ -16,6 +16,7 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { listDocs, readDoc, type Doc, type DocLink } from "@/lib/docs";
import { Shell, Navigation } from "@/components/Shell";
import { ArchitectureFigure } from "@/components/ArchitectureFigure";
import { CodeBlock } from "@/components/CodeBlock";
import { headingsFor, sectionFor } from "@/lib/navigation";
@@ -102,6 +103,22 @@ export default function DocPage({
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({ children, node }) =>
node?.children.some(
(child) =>
child.type === "element" && child.tagName === "img",
) ? (
<div>{children}</div>
) : (
<p>{children}</p>
),
img: ({ src, alt }) =>
src === "/figures/endor-architecture.svg" ? (
<ArchitectureFigure alt={alt ?? "Endor 概念架构草案"} />
) : (
// eslint-disable-next-line @next/next/no-img-element
<img src={src} alt={alt ?? ""} />
),
table: ({ children }) => (
<div className="table-scroll">
<table>{children}</table>
+165 -1
View File
@@ -455,7 +455,7 @@ button {
}
.doc-cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px;
}
.doc-card {
@@ -1340,3 +1340,167 @@ button {
border: 0;
border-radius: 0;
}
/* The topology stays a reproducible SVG; React only supplies the viewer. */
.architecture-figure {
display: block;
border: 1px solid var(--line);
border-radius: 10px;
overflow: hidden;
margin: 28px 0;
}
.figure-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
background: #f5f8f3;
font:
11px ui-monospace,
monospace;
color: #6c806f;
}
.figure-toolbar button {
color: var(--green);
border: 1px solid #d7e3d4;
padding: 5px 10px;
border-radius: 5px;
background: white;
}
.figure-preview {
display: block;
width: 100%;
background: #fcfdfb;
cursor: zoom-in;
}
.prose .figure-preview img {
display: block;
width: 100%;
padding: 0;
margin: 0;
border: 0;
border-radius: 0;
}
.figure-caption {
display: block;
padding: 10px 16px;
font-size: 11px;
color: #758576;
border-top: 1px solid var(--line);
}
.figure-dialog {
padding: 0;
width: 96vw;
max-width: 1900px;
height: 94dvh;
max-height: 94dvh;
border: 1px solid #dce5d9;
border-radius: 12px;
margin: auto;
overflow: hidden;
background: #fcfdfb;
box-shadow: 0 20px 100px #18312530;
}
.figure-dialog::backdrop {
background: #1e322e66;
backdrop-filter: blur(4px);
}
.figure-dialog[open] {
display: flex;
flex-direction: column;
}
.figure-viewer-toolbar {
display: flex;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 12px 18px;
background: #fff;
border-bottom: 1px solid var(--line);
align-items: center;
}
.figure-viewer-toolbar strong {
font-size: 14px;
font-weight: 600;
color: var(--ink);
}
.figure-viewer-toolbar small {
color: #a68b52;
font-size: 11px;
margin-left: 5px;
}
.figure-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 11px;
color: #647c67;
}
.figure-controls button,
.figure-controls a {
min-height: 32px;
min-width: 32px;
padding: 4px 9px;
border: 1px solid #dae4d6;
border-radius: 5px;
background: #fff;
font-weight: 400;
text-decoration: none;
}
.figure-controls button:disabled {
opacity: 0.35;
cursor: default;
}
.figure-controls > span {
min-width: 62px;
text-align: center;
}
.figure-pan {
overflow: auto;
flex: 1;
min-height: 0;
overscroll-behavior: contain;
padding: 8px;
}
.prose .figure-pan img {
display: block;
max-width: none;
height: auto;
margin: 0 auto;
padding: 0;
border: 0;
border-radius: 0;
background: #fcfdfb;
}
.figure-viewer-hint {
margin: 0 !important;
padding: 10px 18px;
border-top: 1px solid var(--line);
background: #fff;
color: #798d79;
font-size: 10px !important;
line-height: 1.7 !important;
}
@media (max-width: 560px) {
.figure-viewer-toolbar {
padding: 10px;
gap: 8px;
}
.figure-controls {
gap: 5px;
}
.figure-controls button,
.figure-controls a {
min-height: 36px;
}
.figure-toolbar {
padding: 10px;
font-size: 10px;
}
.figure-caption {
font-size: 10px;
padding: 10px;
}
}
+110
View File
@@ -0,0 +1,110 @@
# 理解 Endor:从全球入口到对象数据
Endor 的概念模型把全球访问、区域运行时和对象数据连接起来:请求从统一入口进入,在合适的区域找到 Nexus Runtime,再由它承载的 Aster VM 执行对象操作。数据以分片组织,在区域内保持高可用,并为每个分片安排跨区域恢复目标。
> **概念设计草案**:本文按当前讨论绘制目标架构,不表示这些组件和协议已经全部实现。图中的区域、节点、分片数量仅为示例;CDN 和身份服务的接入方式是建议方案。
## 架构总览
![Endor 概念拓扑:全球入口连接北美与亚洲,各区 Traefik 与 Nexus Runtime 全连接,每个 Nexus 配一个 Aster VM;数据分片包含 PostgreSQL、Aerarium 和 S3,并具有区域内副本和跨区备份](/figures/endor-architecture.svg)
图中绿色/蓝色实线表示请求路径或区域内数据依赖,紫色虚线表示身份控制与公钥分发,蓝色点线表示 CDN 回源,金色虚线表示跨区复制和备份。连线表示可达或依赖关系,**不表示请求向所有节点广播**。
图的编排由 Python 生成 SVG:节点图标、二分全连接和复制线路都能复现和编辑。Python 更适合这张静态架构图;页面查看、缩放与下载由 TypeScript/React 负责。
## 全球入口与区域入口
客户端包括浏览器、Tessera、SDK 和 CLI。`endorphin API` 是逻辑上的全球流量入口,尚未在本文规定一个真实的域名。全球调度根据健康、延迟、项目归属和数据放置策略选择区域;不能只选择网络最近的区域而忽略对象在哪个分片。
示例使用北美和亚洲两个区域。每个区域有自己的负载均衡入口,下面是 Traefik 网关,负责按配置进行 TLS、主机名或路径路由。图中的第二个 Traefik 标记为 **HA 副本**:多副本是生产冗余选项,单实例概念部署不要求两个网关。
### 为什么画成二分全连接
每个区域示例中有两个 Traefik 和三个 Nexus Runtime,画出全部六条边,即二分图 **K₂,₃**。每个网关都能把请求路由到任意一个符合条件的健康 Runtime,避免把一个网关固定绑定到一个运行时。
这是逻辑可达关系。实际转发仍需考虑服务发现、健康检查、项目和对象归属,以及有状态连接的路由策略;Runtime 之间并没有因此形成全连接网状网络。
## Nexus Runtime 与 Aster VM
此处采用 **一个 Nexus Runtime 节点配一个 Aster VM** 的概念约定。Nexus 表示对外接入与运行时承载边界,Aster VM 执行对象相关逻辑。图中每台服务器下面都单独画出一个 VM 芯片,避免把多个 VM 误读成另一个共享池。
请求经过区域入口和 Traefik 到达 Nexus,再定位目标分片。图中运行时下方的连接汇入分片路由层,表示节点可以按归属访问区域分片,**不是**「Runtime 1 只能访问分片 1」。对象身份、远程引用、生命周期与调用语义仍需结合[节点与对象挂载草案](/docs/02-mounting-draft)继续定义。
## 三类存储如何组成一个分片
示例中每个区域都有两个逻辑数据分片。每个分片展示三个协作部分,它们是逻辑数据边界,不要求一分片对应三台独占物理机器。
| 部分 | 概念职责 | 依赖关系 |
| ------------------------- | ------------------------------------------------------------ | ------------------------- |
| PostgreSQL | 结构化元数据、对象索引及需要事务约束的状态;具体数据模型待定 | 提供关系数据与事务能力 |
| Aerarium Object data pool | 对象数据的逻辑组织与访问层;不是把 S3 换一个名字 | 同时依赖 PostgreSQL 与 S3 |
| S3 兼容存储 | Blob、文件、快照和适合对象存储的数据;具体格式待定 | 提供对象持久化能力 |
图中 Aerarium 向左右各画一条依赖箭头,分别连接 PostgreSQL 和 S3。它们**不是**三个互不相关的平行数据池。
### 区域内高可用
每个存储图标下面有一个小型副本图标,用点线连接,表示本区域的高可用副本。生产部署应跨独立故障域放置,具体可以是不同可用区或独立节点。
这是逻辑 HA 要求,各层的实现不同:PostgreSQL 可以使用主备复制,Aerarium 需要自己的状态协调和故障恢复设计,S3 的耐久性与副本由选定的存储系统提供。图不强制三者采用同一种同步复制机制,也不把单纯增加副本等同于已经实现故障自动切换。
## 每个分片都要有跨区恢复目标
区域内副本解决本地节点或故障域失效;跨区备份解决整个区域不可用。四个源分片均有独立的异地目标:
| 源分片 | 异地备份位置 | 覆盖内容 |
| ------------ | ------------ | -------------------------------------------- |
| NA-01、NA-02 | 亚洲区域 | 各分片的 PostgreSQL、Aerarium 状态和 S3 数据 |
| AS-01、AS-02 | 北美区域 | 各分片的 PostgreSQL、Aerarium 状态和 S3 数据 |
每个目标应保留可校验的恢复清单,将关系数据恢复点、对象池元数据版本与 S3 对象版本关联起来;不能只复制其中一种存储就宣称整个分片可恢复。图中的金色线路明确跨越区域,备份集按源分片编号标识。
### “最终一定拿到数据”需要怎样的保证
设计目标是:正常访问优先本区域;本地不可用时,仍能找到异地保存的数据。需要区分两类能力:
- **异地在线副本**:已有服务能读取它;追平状态、授权和路由满足要求后,才可承担故障读取。
- **异地备份**:保留了恢复材料,需要恢复 PostgreSQL、重建 Aerarium 并校验对象版本后才能提供服务,通常更慢。
> **保证边界**:异步复制与定期备份不能无条件保证最后一笔已确认写入不会丢失。若源区在复制完成前永久丢失,未到达异地的数据仍可能丢失。当前图承诺的是“已校验恢复点可恢复”的目标,而非已实现的零数据丢失保证。
要实现更强的承诺,需要明确写入确认条件。例如必须等跨区持久化成功才返回写入成功,或者以独立耐久日志作为重放来源,并设计重试、校验、告警与恢复演练。这会增加延迟或实现成本,不能只靠一条“最终一致”虚线获得。
后续设计需要分别确定 **RPO**(可接受的数据丢失窗口)和 **RTO**(恢复服务所需时间),以及备份保留、删除传播、加密密钥和数据跨区合规策略。
## CDN 应该放在哪里
建议把 CDN 作为面向静态和可缓存内容的边缘路径,与动态 API 调度区分:
1. 客户端请求静态前端、文档或允许缓存的对象,优先命中 CDN;命中后直接返回,无需访问 Nexus。
2. 缓存未命中时,CDN 回源到指定的 S3 兼容存储,或专门的资源服务。图中仅示例回源到亚洲区域的 S3,并不把亚洲设为唯一源站。
3. 私有对象可使用短时签名 URL、受控回源或独立授权服务。缓存键、过期、权限隔离和失效策略需要单独设计。
4. 登录、写入、实时连接和个性化动态 API 默认不进入共享缓存。CDN 能代理这些流量,不代表应该缓存这些响应。
CDN 是可替换、可选的加速层,不是数据库副本,也不承担跨区灾难恢复的唯一责任。
## 身份服务是否依赖 Nexus
**建议在逻辑上独立,不把它画成依赖某个业务 Nexus 的服务。** 客户端向身份服务登录或刷新凭证;Nexus 验证访问凭证、项目归属和操作权限后才接受业务请求。
图中紫色线路表示公钥或认证配置的分发关系:使用适当的签名访问令牌方案时,Nexus 可以缓存 JWKS,在本地完成签名及必要声明校验。具体访问令牌格式尚未确定,不能把 OpenID Connect 的 ID Token 直接当作所有 API 的授权凭证。
如果采用不透明令牌、即时撤销或实时策略评估,可能需要在线内省或授权查询;这会引入身份服务的在线依赖,需要单独设计可用性和失败策略。已有令牌可离线验证也不意味着公钥轮换、过期、撤销和权限变化可以不处理。
身份服务本身仍可以使用 PostgreSQL、缓存和密钥管理等基础设施;**逻辑独立不等于没有存储依赖**。将来即使选择在通用 Nexus 基础设施上托管它,也应保留独立的故障和信任边界,避免启动、登录与业务运行时形成循环依赖。
## 继续确定的设计边界
- 全球入口如何发现项目与分片的归属,跨区域请求采用转发还是让客户端重连。
- Nexus 接入职责与 Aster VM 边界、连接亲和性和 Runtime 故障迁移。
- Aerarium 的数据格式、事务边界、对象版本和跨存储一致恢复点。
- 区域内 HA 的实际协议与跨区域 RPO/RTO、故障读取与恢复流程。
- 身份签发者、访问令牌验证与撤销机制,以及私有内容的 CDN 授权。
## 参考依据
以下资料仅用于解释通用组件的能力,不证明 Endor 已经实现本文的概念设计:
- [Traefik:服务与后端负载均衡](https://doc.traefik.io/traefik/master/routing/services/)。
- [OpenID Connect Discovery:配置发现和 JWKS URI](https://openid.net/specs/openid-connect-discovery-1_0.html)。
- [Amazon S3:同区与跨区异步复制](https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html)。本文中的 S3 指兼容对象存储,具体部署并未指定 AWS。
@@ -0,0 +1,31 @@
# Endor 概念页验收 · 2026-10-27
新增页面 `/docs/00-endor-concepts-draft`。按用户给出的五张参考图组织为清晰的矢量图标、节点、实线/点线和二分全连接;未使用 Mermaid 自动矩形流程图,也未引入外部图标、字体或 CDN。
## 内容与图形核对
- 全球 `endorphin API` 入口分流到北美、亚洲各自的区域负载均衡入口。
- 每区两个 Traefik(冗余可选,第二个标 HA)与三个 Nexus Runtime 构成 K₂,₃;总共12条网关到 Runtime 边,六个 Nexus 各配一个 Aster VM。
- 四个区域分片分别有 PostgreSQL、Aerarium、S3 图标;Aerarium 指向另外两种存储。各层均有点线连接的区域内小型副本图标。
- 四个源分片各有独立的金色跨区线路和同编号异地备份集,备份位置明确为对侧区域。
- CDN 作为可选缓存路径,命中直接返回,点线回源到示例 S3;动态 API 和登录不默认进入共享缓存。
- 身份服务以独立控制服务建议呈现,客户端登录与签发、公钥分发和 Nexus 校验分别说明;没有宣称已实现的部署关系。
- 正文区分异步备份、在线副本、恢复点、RPO/RTO,说明“最后一笔写入绝不丢失”需要额外写入确认与耐久协议。
## 验证
- `pnpm lint`、`pnpm typecheck`:通过。
- `pnpm --filter @endor/docs run build --webpack`:通过,新增文档静态路由成功生成。本机采用前次验收已确定的 webpack 构建方式,仓库原有构建配置不变。
- `pnpm figures`:通过;独立 SVG,无外部图像资源。
- `uv run --locked --project tools/figures python -m unittest discover -s tools/figures/tests -v`:通过,覆盖重复生成字节一致、6个 VM、每区三个 Runtime/两个网关、12条全连接边、四个分片各有备份目标。该检查也接入容器校验脚本。
- 现有四个 CI/部署脚本单元测试通过。
- 实际生产预览 `127.0.0.1:3016`:检查桌面与390px手机。手机正文页面宽度390px,没有页面横向溢出;查看器放大至45%时内容宽808px,仅在宽372px的图面区域滚动,正文仍宽390px。
- 浏览器实际验证放大、适应宽度、关闭与 Escape;关闭后焦点恢复至“放大查看”按钮。桌面从适应状态放大到94%,按实际容器宽度计算比例。
- SVG 下载链接指向本地生成文件,并设置下载文件名。全部路由及 SVG/JS/CSS 的 HTTP 冒烟由现有 `scripts/smoke.py` 检查。
- 页面与查看器捕获的浏览器 error/warn 日志为空。
## 截图
`architecture.png` 为完整 SVG 的实际浏览器渲染;`page-desktop.png` / `page-mobile.png` 为正文页面;`viewer-desktop.png` / `viewer-zoom.png` 为桌面图形查看与缩放;`viewer-mobile.png` / `viewer-mobile-zoom.png` 为手机适应与缩放。
本次未改变共享 Runner、Docker 缓存、部署端口与回退策略。CDN/身份服务讨论为架构建议,不代表把当前文档站接到新的外部服务。
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

+1
View File
@@ -6,6 +6,7 @@ pnpm run lint
pnpm run typecheck
cp -R apps/docs/public/figures /tmp/expected-figures
pnpm run figures
uv run --locked --project tools/figures python -m unittest discover -s tools/figures/tests -v
# Generated assets must be committed alongside their scripts.
diff -r /tmp/expected-figures apps/docs/public/figures
pnpm run build
@@ -0,0 +1,238 @@
"""Endor concept topology: explicit meshes, storage dependencies and recovery paths."""
from pathlib import Path
import drawsvg as d
INK = '#233b38'
GREEN = '#2d8b77'
BLUE = '#467fb4'
PURPLE = '#8a6ab4'
AMBER = '#b58b47'
MUTED = '#6d817c'
LINE = '#dce8e3'
FONT = 'ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif'
def build_architecture(output: Path) -> None:
svg = d.Drawing(1760, 1640)
svg.append_title('Endor 概念架构草案:全球入口、区域网关与运行时、分片和跨区恢复')
svg.append(d.Rectangle(0, 0, 1760, 1640, fill='#fcfdfb'))
def rect(x, y, w, h, fill='white', stroke=LINE, radius=12, dash=None):
svg.append(d.Rectangle(x, y, w, h, rx=radius, fill=fill, stroke=stroke,
stroke_width=1.2, stroke_dasharray=dash))
def text(s, x, y, size=16, color=INK, weight='400', anchor='start'):
svg.append(d.Text(s, size, x, y, fill=color, font_family=FONT,
font_weight=weight, text_anchor=anchor))
def path(points, color=GREEN, dash=None, width=1.6, arrow=False, opacity=1):
p = d.Path(fill='none', stroke=color, stroke_width=width,
stroke_dasharray=dash, stroke_linecap='round',
stroke_linejoin='round', opacity=opacity)
p.M(*points[0])
for pt in points[1:]:
p.L(*pt)
svg.append(p)
if arrow:
x, y = points[-1]
px, py = points[-2]
import math
angle = math.atan2(y-py, x-px)
a = d.Path(fill='none', stroke=color, stroke_width=width,
stroke_linecap='round', opacity=opacity)
for sign in [-1, 1]:
a.M(x-7*math.cos(angle+sign*.48), y-7*math.sin(angle+sign*.48)).L(x, y)
svg.append(a)
def dot(x, y, color=GREEN, r=3):
svg.append(d.Circle(x, y, r, fill='#fcfdfb', stroke=color, stroke_width=1.5))
def icon(kind, x, y, color=GREEN, scale=1):
# Icons are vector geometry rather than font glyphs or downloaded artwork.
g = d.Group(transform=f'translate({x},{y}) scale({scale})',
stroke=color, stroke_width=1.8, fill='none',
stroke_linecap='round', stroke_linejoin='round')
if kind == 'globe':
g.append(d.Circle(0, 0, 23)); g.append(d.Ellipse(0, 0, 10, 23))
g.append(d.Line(-22, -8, 22, -8)); g.append(d.Line(-22, 8, 22, 8))
elif kind == 'client':
g.append(d.Rectangle(-26,-20,52,33,rx=4)); g.append(d.Line(-32,21,32,21))
g.append(d.Lines(-26,13,-32,21,32,21,26,13))
g.append(d.Line(-6,17,6,17))
elif kind == 'edge':
g.append(d.Circle(0,0,24)); g.append(d.Lines(-10,-6,0,-14,11,-6,0,2,-10,-6))
g.append(d.Lines(-10,3,0,11,11,3)); g.append(d.Lines(-10,11,0,19,11,11))
elif kind == 'shield':
g.append(d.Path().M(0,-25).L(22,-16).L(20,5).Q(17,19,0,27).Q(-17,19,-20,5).L(-22,-16).Z())
g.append(d.Lines(-9,0,-2,7,11,-7))
elif kind == 'lb':
g.append(d.Circle(0,-17,7)); g.append(d.Circle(-22,19,7)); g.append(d.Circle(22,19,7))
g.append(d.Lines(0,-10,0,1,-22,1,-22,12)); g.append(d.Lines(0,1,22,1,22,12))
elif kind in ('gateway','runtime'):
for dy in [-14,2]:
g.append(d.Rectangle(-25,dy,50,12,rx=3))
g.append(d.Circle(-16,dy+6,1.3,fill=color)); g.append(d.Line(-8,dy+6,16,dy+6))
if kind == 'runtime':
g.append(d.Line(-13,20,13,20)); g.append(d.Line(0,14,0,20))
elif kind == 'vm':
g.append(d.Rectangle(-17,-13,34,26,rx=4))
for a in [-10,0,10]:
g.append(d.Line(a,-18,a,-13)); g.append(d.Line(a,13,a,18))
for a in [-6,6]:
g.append(d.Line(-22,a,-17,a)); g.append(d.Line(17,a,22,a))
g.append(d.Path().M(-6,-5).L(0,0).L(-6,5)); g.append(d.Line(4,6,10,6))
elif kind == 'db':
g.append(d.Path().M(-21,-12).L(-21,15).C(-21,27,21,27,21,15).L(21,-12))
g.append(d.Ellipse(0,-12,21,8)); g.append(d.Path().M(-21,1).C(-21,12,21,12,21,1))
elif kind == 'pool':
for dx,dy in [(-12,-9),(12,-9),(0,12)]:
g.append(d.Rectangle(dx-11,dy-10,22,20,rx=5))
g.append(d.Circle(dx,dy,3,fill=color,stroke='none'))
elif kind == 'bucket':
g.append(d.Ellipse(0,-15,23,7)); g.append(d.Lines(-23,-15,-18,21,18,21,23,-15))
g.append(d.Path().M(-17,6).Q(0,17,17,6))
elif kind == 'backup':
g.append(d.Rectangle(-25,-15,50,36,rx=4)); g.append(d.Line(-29,-15,29,-15))
g.append(d.Lines(-6,5,0,-2,6,5)); g.append(d.Line(0,-2,0,14))
svg.append(g)
def badge(s, x, y, color=GREEN, bg='#edf5ee', w=118):
rect(x, y-17, w, 25, bg, 'none', 5)
text(s, x+w/2, y, 12, color, '500', 'middle')
text('ENDOR / SYSTEM ATLAS', 45, 48, 13, GREEN, '600')
text('一个全球入口,多个区域,一组可恢复的对象。', 45, 92, 31, INK, '600')
badge('概念设计 · 草案', 1520, 57, AMBER, '#f8f2e6', 192)
text('01 全球接入', 45, 142, 13, MUTED, '600')
# Request routes first, icons and labels above them.
path([(170,214),(500,214)], arrow=True)
text('动态 API / 对象访问', 280, 201, 13, GREEN)
path([(120,185),(120,163),(1160,163),(1160,184)], BLUE, arrow=True)
text('静态资源 / 可缓存内容', 815, 155, 13, BLUE)
path([(110,250),(110,300),(1540,300),(1540,253)], PURPLE, '5 6', arrow=True)
text('登录 / 换取凭证', 1140, 290, 13, PURPLE)
icon('client',120,214)
text('全球客户端',120,268,17,INK,'600','middle')
icon('globe',550,214)
text('endorphin API',620,208,23,INK,'600')
text('全球流量入口 · 按健康与策略选区',620,237,14,MUTED)
icon('edge',1160,214,BLUE)
text('CDN',1200,208,21,INK,'600')
text('命中直接返回',1200,234,13,MUTED)
icon('shield',1540,214,PURPLE)
text('身份服务',1580,207,17,INK,'600')
text('登录 / 签发凭证',1580,232,12,MUTED)
text('独立控制服务 · 建议',1580,256,11,PURPLE)
# Two equally detailed regions. Solid edges mean eligible request routes,
# not broadcast execution or a requirement for inter-runtime mesh links.
regions = [('北美', 'NA', 40, GREEN), ('亚洲', 'AS', 910, BLUE)]
for name, prefix, ox, accent in regions:
center = ox+390
rect(ox,337,780,815,'#ffffff',LINE,18)
text(f'{name}区域',ox+28,375,23,INK,'600')
text(f'{prefix} / 独立故障域',ox+28,401,12,MUTED)
badge('区域入口',ox+623,376,accent,'#f0f6f3',126)
# Region's own load balancer; global routing fans into these, not nodes.
path([(800,252),(800,318),(center,318),(center,419)], GREEN, arrow=True)
icon('lb',center,446,accent,.72)
text('区域负载均衡',center+42,442,16,INK,'600')
text('健康检查 / TLS 路由',center+42,464,12,MUTED)
gateways = [ox+235, ox+545]
runtime_x = [ox+135, ox+390, ox+645]
for gx in gateways:
path([(center,470),(center,488),(gx,488),(gx,514)],accent,arrow=True)
text('TRAEFIK',ox+27,528,12,MUTED,'600')
text('生产冗余 · 可选',ox+27,550,12,MUTED)
for gi,gx in enumerate(gateways):
icon('gateway',gx,536,accent,.9)
text(f'Traefik {gi+1}',gx,579,14,INK,'500','middle')
if gi == 1:
badge('HA 副本',gx+42,539,accent,'#edf5f0',78)
# Explicit K2,3 complete bipartite graph. Every edge is drawn.
for gx in gateways:
for nx in runtime_x:
path([(gx,592),(nx,665)],accent,width=1.35,opacity=.65)
dot(gx,592,accent)
for ni,nx in enumerate(runtime_x):
dot(nx,665,accent)
icon('runtime',nx,701,accent)
text(f'Nexus Runtime {ni+1}',nx,746,15,INK,'600','middle')
icon('vm',nx-49,779,accent,.65)
text('Aster VM',nx-21,785,14,INK)
text('1 Nexus : 1 VM',nx,813,11,MUTED,'400','middle')
text('K₂,₃ 全连接',ox+22,621,11,accent)
text('选择健康节点',ox+22,640,11,MUTED)
# Shared regional shard routing bus, not a fixed node-to-shard assignment.
for nx in runtime_x:
path([(nx,829),(nx,850)],accent,opacity=.8)
path([(runtime_x[0],850),(runtime_x[-1],850)],accent,opacity=.8)
text('分片定位 / 对象读写',center,874,12,MUTED,'400','middle')
for si,sx in enumerate([ox+24,ox+406]):
mid=sx+175
path([(mid,850),(mid,905)],accent,arrow=True)
rect(sx,912,350,208,'#f8faf7', '#e2eae1',10)
text(f'{prefix}-{si+1:02} / 数据分片',sx+15,939,14,INK,'600')
badge('主 + 区域 HA',sx+212,938,accent,'#edf3ec',124)
for kind,dx,label in [('db',62,'PostgreSQL'),('pool',175,'Aerarium'),('bucket',288,'S3')]:
icon(kind,sx+dx,989,accent,.8)
text(label,sx+dx,1033,13,INK,'500','middle')
# Every tier has an explicit local standby, linked by dots.
path([(sx+dx,1041),(sx+dx,1062)],accent,'2 4',width=1.1)
icon(kind,sx+dx,1080,accent,.38)
# Aerarium depends on both PostgreSQL and S3.
path([(sx+150,986),(sx+87,986)],accent,arrow=True)
path([(sx+200,986),(sx+263,986)],accent,arrow=True)
text('Object data pool',mid,1052,10,MUTED,'400','middle')
text('区域内副本 · 跨故障域放置',mid,1108,11,MUTED,'400','middle')
# Each shard gets its own, separately routed off-region copy.
dot(mid,1120,AMBER)
# Cross-region origin fill. CDN is intentionally outside private dynamic API.
path([(1160,253),(1160,278),(1718,278),(1718,883),(1680,883),(1680,989),(1627,989)],BLUE,'2 6',arrow=True)
rect(1698,582,58,46,'#fcfdfb','none',4)
text('CDN',1727,600,12,BLUE,anchor='middle')
text('S3 回源',1727,621,11,MUTED,anchor='middle')
# Control-plane key retrieval, deliberately not a runtime hosting dependency.
path([(1540,245),(1540,309),(869,309),(869,705)],PURPLE,'5 6',arrow=True)
path([(869,705),(710,705)],PURPLE,'5 6',arrow=True)
path([(869,705),(1020,705)],PURPLE,'5 6',arrow=True)
rect(791,682,156,39,'#fcfdfb','none',4)
text('JWKS / 凭证验证',869,699,12,PURPLE,anchor='middle')
text('Nexus 本地验证',869,716,11,PURPLE,anchor='middle')
text('02 跨区域恢复层',45,1486,16,INK,'600')
text('每个源分片都有一个异地目标;正常读取优先本区域,故障时恢复或转至已追平的副本。',45,1514,14,MUTED)
# Source routes are independent, with a visible crossing gap at the center.
# Backups physically hosted in the opposite region are explicitly labeled.
backup_specs = [
('亚洲分片的异地备份 · 位于北美', 40, 'AS', [1109,1491], [239,621]),
('北美分片的异地备份 · 位于亚洲', 910, 'NA', [239,621], [1109,1491]),
]
for bi, (label, bx, source, sources, targets) in enumerate(backup_specs):
rect(bx,1280,780,174,'#fbf8f1','#e9dec8',14)
text(label,bx+25,1310,17,INK,'600')
for i,(origin,target) in enumerate(zip(sources,targets)):
lane=1180+bi*40+i*20
path([(origin,1120),(origin+12,1134),(origin+12,lane),(target-12,lane),(target-12,1280)],AMBER,'6 6',arrow=True)
icon('backup',target-77,1376,AMBER,.85)
text(f'{source}-{i+1:02} 备份集',target-38,1369,15,INK,'600')
text('PG + Aerarium + S3',target-38,1393,12,MUTED)
text('恢复点 / 清单校验 / 重试',target-38,1415,11,MUTED)
# A caption is painted over empty whitespace, no connectivity is implied.
rect(724,1148,312,25,'#fcfdfb','none',4)
text('异步复制 / 版本化备份',880,1166,13,AMBER,'500','middle')
path([(50,1555),(105,1555)],GREEN,arrow=True)
text('请求与数据依赖',118,1560,12,MUTED)
path([(355,1555),(410,1555)],PURPLE,'5 6')
text('身份控制 / 公钥',423,1560,12,MUTED)
path([(670,1555),(725,1555)],BLUE,'2 6')
text('缓存回源',738,1560,12,MUTED)
path([(950,1555),(1005,1555)],AMBER,'6 6')
text('跨区复制 / 备份',1018,1560,12,MUTED)
text('示意数量:2 区域 × 2 网关 × 3 Runtime × 2 分片;数量不构成部署要求。',45,1608,12,MUTED)
text('可恢复性以已持久化且已校验的恢复点为边界;异步 ≠ 零 RPO。',1715,1608,12,AMBER,anchor='end')
output.parent.mkdir(parents=True, exist_ok=True)
svg.save_svg(str(output))
+4
View File
@@ -1,6 +1,7 @@
"""Generate local SVG assets without external fonts or rendering services."""
from pathlib import Path
import drawsvg as draw
from .architecture import build_architecture
def main() -> None:
@@ -32,3 +33,6 @@ def main() -> None:
stroke="#0f766e", stroke_width=2))
diagram.save_svg(str(output))
print(f"Generated {output.relative_to(root)}")
topology = output.parent / "endor-architecture.svg"
build_architecture(topology)
print(f"Generated {topology.relative_to(root)}")
+38
View File
@@ -0,0 +1,38 @@
"""Protect the relationships that make this a topology rather than a block chart."""
from pathlib import Path
import tempfile
import unittest
import xml.etree.ElementTree as ET
from endor_figures.architecture import build_architecture
class ArchitectureTest(unittest.TestCase):
def test_topology_and_reproducibility(self):
with tempfile.TemporaryDirectory() as directory:
first, second = (Path(directory) / name for name in ('first.svg', 'second.svg'))
build_architecture(first)
build_architecture(second)
self.assertEqual(first.read_bytes(), second.read_bytes())
root = ET.parse(first).getroot()
ns = {'s': 'http://www.w3.org/2000/svg'}
texts = [''.join(e.itertext()) for e in root.findall('.//s:text', ns)]
self.assertEqual(texts.count('Aster VM'), 6)
for i in range(1, 4):
self.assertEqual(texts.count(f'Nexus Runtime {i}'), 2)
for i in range(1, 3):
self.assertEqual(texts.count(f'Traefik {i}'), 2)
for prefix in ('NA', 'AS'):
for i in range(1, 3):
self.assertIn(f'{prefix}-{i:02} / 数据分片', texts)
self.assertIn(f'{prefix}-{i:02} 备份集', texts)
mesh_edges = [e for e in root.findall('.//s:path', ns)
if e.get('stroke-width') == '1.35' and e.get('opacity') == '0.65']
self.assertEqual(len(mesh_edges), 12, 'Each region must retain the full K2,3 mesh')
# The figure must remain self-contained and editable.
self.assertEqual(root.findall('.//s:image', ns), [])
self.assertIn('异步复制 / 版本化备份', texts)
if __name__ == '__main__':
unittest.main()