Separate storage shard meshes and unify Endor backup topology
Verify and deploy Tabularium / verify-and-deploy (push) Has been cancelled

This commit is contained in:
2026-10-27 21:10:43 +08:00
parent 1c27f6bc1e
commit 36840ef1d4
11 changed files with 925 additions and 549 deletions
+151 -132
View File
@@ -13,9 +13,9 @@ FONT = 'ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, s
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'))
svg = d.Drawing(2000, 1860)
svg.append_title('Endor 架构:全球入口、区域网关、独立存储分片与统一备份')
svg.append(d.Rectangle(0, 0, 2000, 1860, 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,
@@ -68,7 +68,14 @@ def build_architecture(output: Path) -> None:
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'):
elif kind == 'gateway':
# Routing diamond: deliberately distinct from a Runtime server.
g.append(d.Lines(0,-23,23,0,0,23,-23,0,0,-23))
g.append(d.Line(-34,0,-12,0)); g.append(d.Lines(-17,-5,-12,0,-17,5))
for yy in [-12,0,12]:
g.append(d.Lines(0,0,9,yy,34,yy))
g.append(d.Lines(29,yy-4,34,yy,29,yy+4))
elif kind == '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))
@@ -100,139 +107,151 @@ def build_architecture(output: Path) -> None:
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')
text('ENDOR / SYSTEM ATLAS', 40, 42, 14, GREEN, '600')
text('01 全球接入', 40, 83, 13, MUTED, '600')
path([(160,143),(550,143)], arrow=True)
text('动态 API / 对象访问',280,131,13,GREEN)
path([(110,113),(110,100),(1300,100),(1300,114)],BLUE,arrow=True)
text('静态资源 / 可缓存内容',1000,91,13,BLUE)
path([(110,179),(110,214),(1750,214),(1750,179)],PURPLE,'5 6',arrow=True)
text('登录 / 换取凭证',1470,204,12,PURPLE)
icon('client',110,143)
text('全球客户端',110,191,17,INK,'600','middle')
icon('globe',600,143)
text('endorphin API',654,138,24,INK,'600')
text('全球流量入口 · 按健康与策略选区',654,165,14,MUTED)
icon('edge',1300,143,BLUE)
text('CDN',1340,137,21,INK,'600')
text('命中直接返回',1340,162,13,MUTED)
icon('shield',1750,143,PURPLE)
text('身份服务',1790,138,18,INK,'600')
text('登录 / 签发凭证',1790,162,12,MUTED)
# 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)
# A real fan-out includes an open-ended third branch, not just two regions.
path([(835,180),(835,243),(1900,243),(1900,306)],GREEN,arrow=True)
rect(1855,315,110,132,'#f8faf7',LINE,12,'3 5')
text('… / etc',1910,354,23,GREEN,'500','middle')
text('其他区域',1910,388,15,INK,'600','middle')
text('按需扩展',1910,418,12,MUTED,'400','middle')
# 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]
regions = [('北美','NA',40,GREEN),('亚洲','AS',960,BLUE)]
# Each storage family owns its own shard set. No PG/Aerarium/S3 triplets.
families = [('PostgreSQL','PG','db',150),
('Aerarium','AO','pool',435),
('S3','S3','bucket',720)]
backup_ports = []
for name,prefix,ox,accent in regions:
center = ox+435
rect(ox,275,870,1025,'#ffffff',LINE,18)
text(f'{name}区域',ox+28,314,24,INK,'600')
text(f'{prefix} / 独立故障域',ox+28,340,12,MUTED)
badge('区域入口',ox+708,316,accent,'#eff5f0',132)
path([(835,180),(835,243),(center,243),(center,352)],GREEN,arrow=True)
icon('lb',center,377,accent,.72)
text('区域负载均衡',center+42,372,16,INK,'600')
text('健康检查 / TLS 路由',center+42,396,12,MUTED)
gateways = [ox+265,ox+605]
runtime_x = [ox+105+i*165 for i in range(5)]
runtime_y = [605,619,605,619,605]
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)
path([(center,403),(center,423),(gx,423),(gx,444)],accent,arrow=True)
text('TRAEFIK',ox+26,467,12,MUTED,'600')
text('生产冗余 · 可选',ox+26,491,12,MUTED)
for i,gx in enumerate(gateways):
icon('gateway',gx,467,accent,.85)
text(f'Traefik {i+1}',gx,510,14,INK,'500','middle')
if i == 1:
badge('HA 副本',gx+49,469,accent,'#edf5f0',85)
for nx,ny in zip(runtime_x,runtime_y):
path([(gx,523),(nx,ny-30)],accent,width=1.35,opacity=.65)
dot(gx,523,accent)
text('K₂,₅ 全连接',ox+23,556,11,accent)
for i,(nx,ny) in enumerate(zip(runtime_x,runtime_y)):
dot(nx,ny-30,accent)
icon('runtime',nx,ny,accent)
text(f'Nexus Runtime {i+1:02}',nx,ny+39,13,INK,'600','middle')
# VM deliberately offset to the lower right of its host.
path([(nx+26,ny+5),(nx+64,ny+5),(nx+64,ny+70),(nx+50,ny+70)],accent,'2 4',width=1)
icon('vm',nx+35,ny+70,accent,.55)
text('Aster VM',nx+35,ny+100,11,INK,'400','middle')
# Route storage traffic outside the label/VM, not through them.
path([(nx-26,ny),(nx-61,ny),(nx-61,747)],accent,width=1.1,opacity=.75)
path([(runtime_x[0]-61,747),(runtime_x[-1]-61,747)],accent,width=1.2)
path([(center,747),(center,793)],accent,arrow=True)
text('分片定位 / 对象访问',center+16,775,12,MUTED)
text('02 独立存储分片群',ox+26,775,13,INK,'600')
ys = [880,1020,1160]
for label,code,kind,dx in families:
cx=ox+dx
rect(cx-95,802,190,468,'#f8faf7','#e3ebe2',10)
text(label,cx,829,17,INK,'600','middle')
text('Object data pool' if code=='AO' else '独立分片群',cx,850,11,MUTED,'400','middle')
for i,sy in enumerate(ys):
icon(kind,cx,sy,accent,.8)
text(f'{prefix}-{code}-{i+1:02}',cx,sy+36,12,INK,'500','middle')
path([(cx,sy+43),(cx,sy+54)],accent,'2 4',width=1)
icon(kind,cx,sy+69,accent,.32)
text('区域 HA',cx+23,sy+73,10,MUTED)
dot(cx,1270,AMBER)
backup_ports.append((prefix,code,cx))
# K3,3 on each side: every Aerarium shard can address every PG/S3 shard.
# The two storage families do not gain a fictitious direct PG-to-S3 edge.
pg,ao,s3 = ox+150,ox+435,ox+720
for ay in ys:
path([(ao-25,ay),(ao-75,ay)],accent,width=1.1)
path([(ao+25,ay),(ao+75,ay)],accent,width=1.1)
for by in ys:
path([(ao-75,ay),(pg+75,by)],accent,width=1.15,opacity=.55)
path([(ao+75,ay),(s3-75,by)],accent,width=1.15,opacity=.55)
dot(ao-75,ay,accent,2.5); dot(ao+75,ay,accent,2.5)
for sy in ys:
path([(pg+75,sy),(pg+25,sy)],accent,width=1.1,arrow=True)
path([(s3-75,sy),(s3-25,sy)],accent,width=1.1,arrow=True)
dot(pg+75,sy,accent,2.5); dot(s3-75,sy,accent,2.5)
text('Aerarium ↔ PG / S3:各分片群全连接,按归属与映射定位数据',center,1290,12,MUTED,'400','middle')
# 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')
# Identity is a control-plane relation; representative endpoints are labeled.
path([(1750,180),(1750,227),(935,227),(935,653)],PURPLE,'5 6')
path([(935,653),(900,653),(900,605),(871,605)],PURPLE,'5 6',arrow=True)
path([(935,653),(985,653),(985,605),(1038,605)],PURPLE,'5 6',arrow=True)
rect(870,667,130,49,'#fcfdfb','none',4)
text('JWKS / 凭证验证',935,686,11,PURPLE,anchor='middle')
text('Nexus 本地验证',935,705,10,PURPLE,anchor='middle')
# CDN fills from an S3 shard, without crossing other storage labels.
path([(1300,180),(1300,207),(1975,207),(1975,885),(1808,885),(1808,880),(1705,880)],BLUE,'2 6',arrow=True)
rect(1948,559,49,51,'#fcfdfb','none',3)
text('CDN',1972,580,11,BLUE,anchor='middle')
text('回源',1972,601,11,BLUE,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')
# One system, not a North America / Asia pair of backup silos.
for i,(prefix,code,cx) in enumerate(backup_ports):
dest={'PG':370,'AO':980,'S3':1580}[code]
lane=1325+i*18
path([(cx,1270),(cx,lane),(dest,lane),(dest,1460)],AMBER,'6 6',arrow=True)
rect(40,1460,1790,280,'#fbf8f1','#e9dec8',16)
text('03 统一备份系统',68,1502,23,INK,'600')
text('全区域 / 全部分片 · 统一编排、持久化、校验与恢复',68,1531,14,MUTED)
for kind,cx,label,line1,line2 in [
('db',370,'PostgreSQL 备份','基础备份 + WAL / 恢复点','各区域 PostgreSQL 分片的事务恢复材料'),
('pool',980,'Aerarium 备份','对象池状态 + 索引 / 版本映射','关联对象、PG 恢复点与 S3 对象版本'),
('bucket',1580,'S3 备份','对象版本 + Blob / 快照','各区域 S3 分片的实际对象与文件数据'),
]:
icon(kind,cx-145,1596,AMBER,1.05)
text(label,cx-95,1587,18,INK,'600')
text(line1,cx-95,1615,14,MUTED)
text(line2,cx-95,1641,12,MUTED)
path([(90,1670),(1780,1670)],'#e3d5b9',width=1)
text('统一恢复目录:区域 / 存储类型 / 分片 ID / 恢复清单 / 校验和 / 重试状态',935,1709,14,AMBER,'500','middle')
path([(50,1790),(105,1790)],GREEN,arrow=True)
text('请求 / 存储依赖',118,1795,12,MUTED)
path([(365,1790),(420,1790)],PURPLE,'5 6')
text('身份控制 / 公钥',433,1795,12,MUTED)
path([(680,1790),(735,1790)],BLUE,'2 6')
text('缓存回源',748,1795,12,MUTED)
path([(965,1790),(1020,1790)],AMBER,'6 6')
text('统一备份采集',1033,1795,12,MUTED)
text('实线为逻辑可达关系;全连接不表示广播,也不表示任意分片拥有相同数据。',40,1837,12,MUTED)
text('备份保证以已持久化且已校验的恢复点为边界。',1830,1837,12,AMBER,anchor='end')
output.parent.mkdir(parents=True, exist_ok=True)
svg.save_svg(str(output))