This is an automated email from the ASF dual-hosted git repository.
tuhaihe pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudberry-site.git
The following commit(s) were added to refs/heads/main by this push:
new 7cb8f97f48 Add Sizing Calculator page
7cb8f97f48 is described below
commit 7cb8f97f48fe50e7b38f08d9c5123c89f70fe53e
Author: Dianjin Wang <[email protected]>
AuthorDate: Thu Aug 6 10:07:34 2026 +0800
Add Sizing Calculator page
Source from https://cloudberry-fe.github.io/sizing/index.html
Authored-by: Ryan Wei <[email protected]>
---
docs/deployment/capacity_planning.md | 4 +
docusaurus.config.ts | 4 +
src/components/sizing/calc.js | 160 +++++++++++++++++++
src/components/sizing/config.js | 109 +++++++++++++
src/components/sizing/logic.ts | 170 +++++++++++++++++++++
src/components/sizing/sizing.css | 134 ++++++++++++++++
src/components/sizing/strings.ts | 94 ++++++++++++
src/pages/sizing/index.tsx | 112 ++++++++++++++
src/pages/sizing/methodology.mdx | 124 +++++++++++++++
.../version-2.x/deployment/capacity_planning.md | 4 +
10 files changed, 915 insertions(+)
diff --git a/docs/deployment/capacity_planning.md
b/docs/deployment/capacity_planning.md
index 46b818e8a0..c2d6d0fa23 100644
--- a/docs/deployment/capacity_planning.md
+++ b/docs/deployment/capacity_planning.md
@@ -4,6 +4,10 @@ title: Estimating storage capacity
To estimate how much data your Apache Cloudberry system can accommodate, use
these measurements as guidelines. Also keep in mind that you may want to have
extra space for landing backup files and data load files on each segment host.
+:::tip
+For a quick starting point, try the [Sizing Calculator](/sizing/). Enter your
data size and infrastructure type (physical, VM, or cloud) and it suggests node
counts and per-node specs. Results are estimates — validate with a POC before
final sizing.
+:::
+
## Calculating usable disk capacity
Start with the raw capacity of the physical disks on a segment host that are
available for data storage:
diff --git a/docusaurus.config.ts b/docusaurus.config.ts
index 8e7a11867a..701c2922b3 100644
--- a/docusaurus.config.ts
+++ b/docusaurus.config.ts
@@ -313,6 +313,10 @@ const config: Config = {
label: "Powered By",
to: "/powered-by",
},
+ {
+ label: "Sizing Calculator",
+ to: "/sizing/",
+ },
],
},
{
diff --git a/src/components/sizing/calc.js b/src/components/sizing/calc.js
new file mode 100644
index 0000000000..24990753d2
--- /dev/null
+++ b/src/components/sizing/calc.js
@@ -0,0 +1,160 @@
+import { COMPUTE_RULE, PHYSICAL_PRESETS, VM_PROFILES, VM_COORD, CLOUD_SCHEMES
} from './config.js';
+
+export function toTB(value, unit) {
+ if (unit === 'GB') return value / 1024;
+ if (unit === 'PB') return value * 1024;
+ return value;
+}
+
+export function evenUp(n) {
+ return n % 2 ? n + 1 : n;
+}
+
+// One quota rule for every path: each primary segment gets 8 OS-visible
+// logical cores + 32G (scaled by the concurrency factor). capacity may be
+// fractional (a node smaller than one full quota contributes
+// proportionally); the displayed layout is an integer >= 1. Mirrors are
+// co-hosted 1:1 (spread mirroring).
+export function segLayoutFor(cpu, memGB, concurrencyFactor = 1) {
+ const capacity = Math.min(cpu / (COMPUTE_RULE.vcpuPerTB * concurrencyFactor),
+ memGB / (COMPUTE_RULE.memGBPerTB *
concurrencyFactor));
+ const primaries = Math.max(1, Math.floor(capacity));
+ return { primaries, mirrors: primaries, capacity };
+}
+
+function layoutBom(layout, perSegTB, memGB) {
+ const layoutText = layout.mirrors ? `${layout.primaries} primary +
${layout.mirrors} mirror` : `${layout.primaries} primary`;
+ const lines = [{ labelKey: 'bom.layout', value: layoutText }];
+ if (perSegTB != null) lines.push({ labelKey: 'bom.perseg', value: `≈
${perSegTB.toFixed(1)} TB` });
+ if (memGB != null) lines.push({ labelKey: 'bom.segmem', value:
`${Math.floor(memGB / layout.primaries)}G` });
+ return lines;
+}
+
+// Unified per-node usable capacity, every discount applied exactly once:
+// ×0.9 OS/FS overhead, ×0.8 keep 20% free, ÷(copies + 1/3 workspace) where
+// copies = 2 (segment primary + mirror, the default HA layout).
+// Same formula for every path; physical passes post-RAID arrayTB,
+// VM/cloud pass nominal data-disk capacity.
+export function nodeUsableTB(nominalTB) {
+ return nominalTB * 0.9 * 0.8 / (2 + 1 / 3);
+}
+
+export function calcPhysical({ dataTB, compressionRatio, presetId,
concurrencyFactor = 1 }) {
+ const p = PHYSICAL_PRESETS.find(x => x.id === presetId);
+ const onDiskTB = dataTB / compressionRatio;
+ const usable = nodeUsableTB(p.arrayTB);
+ const layout = segLayoutFor(p.cores, p.memGB, concurrencyFactor);
+ const storageNodes = Math.max(2, Math.ceil(onDiskTB / usable));
+ const computeNodes = Math.ceil(onDiskTB / layout.capacity); // 1TB per
segment-quota
+ const segNodes = evenUp(Math.max(storageNodes, computeNodes));
+ const perSegTB = onDiskTB / (segNodes * layout.primaries);
+ return {
+ product: 'lightning',
+ layout,
+ roles: [
+ { key: 'coordinator', count: 2, cpu: p.cores, memGB: p.memGB,
+ storageTB: p.coordStorageTB, cpuUnitKey: 'unit.cores', noteKey:
'note.coord.physical',
+ bom: [
+ { labelKey: 'bom.cpu', value: p.bom.cpu },
+ { labelKey: 'bom.mem', value: p.bom.mem },
+ { labelKey: 'bom.sysdisk', value: p.bom.sysDisk },
+ { labelKey: 'bom.datadisk', value: p.bom.coordDataDisk },
+ { labelKey: 'bom.nic', value: p.network },
+ ] },
+ { key: 'segment', count: segNodes, cpu: p.cores, memGB: p.memGB,
+ storageTB: p.arrayTB, cpuUnitKey: 'unit.cores', noteKey:
'note.segment.physical',
+ bom: [
+ { labelKey: 'bom.cpu', value: p.bom.cpu },
+ { labelKey: 'bom.mem', value: p.bom.mem },
+ { labelKey: 'bom.sysdisk', value: p.bom.sysDisk },
+ { labelKey: 'bom.datadisk', value: p.bom.dataDisk },
+ { labelKey: 'bom.raid', valueKey: p.bom.raidKey },
+ { labelKey: 'bom.nic', value: p.network },
+ ...layoutBom(layout, perSegTB, p.memGB),
+ ] },
+ ],
+ binding: { type: computeNodes > storageNodes ? 'compute' : 'storage',
storageNodes, computeNodes },
+ capacityTB: segNodes * usable * compressionRatio,
+ sourceKey: p.sourceKey,
+ };
+}
+
+export function recommendVMProfile(dataTB) {
+ return VM_PROFILES.find(p => dataTB <= p.maxTB);
+}
+
+export function calcVM({ dataTB, compressionRatio, profileId,
concurrencyFactor = 1 }) {
+ const p = VM_PROFILES.find(x => x.id === profileId);
+ const usable = nodeUsableTB(p.storageTB);
+ const onDiskTB = dataTB / compressionRatio;
+ const layout = segLayoutFor(p.vcpu, p.memGB, concurrencyFactor);
+ const storageNodes = Math.max(2, Math.ceil(onDiskTB / usable));
+ const computeNodes = Math.ceil(onDiskTB / layout.capacity); // 1TB per
segment-quota
+ const rawNodes = Math.max(storageNodes, computeNodes);
+ const n = { usable, storageNodes, computeNodes, dataNodes: evenUp(rawNodes)
};
+ const perSegTB = onDiskTB / (n.dataNodes * layout.primaries);
+ return {
+ product: 'lightning',
+ layout,
+ roles: [
+ { key: 'coordinator', count: 2, cpu: VM_COORD.vcpu, memGB:
VM_COORD.memGB,
+ storageTB: VM_COORD.storageTB, cpuUnitKey: 'unit.vcpu', noteKey:
'note.coord.vm' },
+ { key: 'datanode', count: n.dataNodes, cpu: p.vcpu, memGB: p.memGB,
+ storageTB: p.storageTB, cpuUnitKey: 'unit.vcpu', noteKey:
'note.datanode.vm',
+ bom: [
+ { labelKey: 'bom.datadisk', value: `${p.storageTB}TB SSD` },
+ { labelKey: 'bom.throughput', value: p.throughput },
+ { labelKey: 'bom.host', valueKey: p.hostKey },
+ ...layoutBom(layout, perSegTB, p.memGB),
+ ] },
+ ],
+ binding: { type: n.computeNodes > n.storageNodes ? 'compute' : 'storage',
+ storageNodes: n.storageNodes, computeNodes: n.computeNodes },
+ capacityTB: n.dataNodes * n.usable * compressionRatio,
+ profileId: p.id,
+ };
+}
+
+export function calcCloud({ dataTB, compressionRatio, schemeId,
concurrencyFactor = 1 }) {
+ const s = CLOUD_SCHEMES.find(x => x.id === schemeId);
+ const seg = s.segment;
+ const usable = nodeUsableTB(seg.storageTB);
+ const onDiskTB = dataTB / compressionRatio;
+ const layout = segLayoutFor(seg.vcpu, seg.memGB, concurrencyFactor);
+ const storageNodes = Math.max(2, Math.ceil(onDiskTB / usable));
+ const computeNodes = Math.ceil(onDiskTB / layout.capacity); // 1TB per
segment-quota
+ const rawNodes = Math.max(storageNodes, computeNodes);
+ const n = { usable, storageNodes, computeNodes, dataNodes: evenUp(rawNodes)
};
+ const perSegTB = onDiskTB / (n.dataNodes * layout.primaries);
+ return {
+ product: 'lightning',
+ layout,
+ roles: [
+ { key: 'coordinator', count: 2, cpu: s.coordinator.vcpu, memGB:
s.coordinator.memGB,
+ storageTB: s.coordinator.storageTB, instance: s.coordinator.instance,
+ cpuUnitKey: 'unit.vcpu', noteKey: 'note.coord.vm',
+ bom: [{ labelKey: 'bom.datadisk', value: s.coordinator.diskDesc }] },
+ { key: 'datanode', count: n.dataNodes, cpu: seg.vcpu, memGB: seg.memGB,
+ storageTB: seg.storageTB, instance: seg.instance,
+ cpuUnitKey: 'unit.vcpu', noteKey: s.noteKey || 'note.datanode.vm',
+ bom: [{ labelKey: 'bom.datadisk', value: seg.diskDesc },
...layoutBom(layout, perSegTB, seg.memGB)] },
+ { key: 'oss', count: 1, cpu: null, memGB: null, storageTB: null,
+ instance: s.oss, noteKey: 'note.oss' },
+ ],
+ binding: { type: n.computeNodes > n.storageNodes ? 'compute' : 'storage',
+ storageNodes: n.storageNodes, computeNodes: n.computeNodes },
+ capacityTB: n.dataNodes * n.usable * compressionRatio,
+ sourceKey: s.sourceKey,
+ };
+}
+
+export function summarize(roles) {
+ const s = { nodes: 0, cpu: 0, memGB: 0, storageTB: 0 };
+ for (const r of roles) {
+ s.nodes += r.count;
+ if (r.cpu != null) s.cpu += r.count * r.cpu;
+ if (r.memGB != null) s.memGB += r.count * r.memGB;
+ if (r.storageTB != null) s.storageTB += r.count * r.storageTB;
+ }
+ return s;
+}
diff --git a/src/components/sizing/config.js b/src/components/sizing/config.js
new file mode 100644
index 0000000000..38ceb08169
--- /dev/null
+++ b/src/components/sizing/config.js
@@ -0,0 +1,109 @@
+// All tunable sizing data. Formula-structural constants live in calc.js.
+export const COMPUTE_RULE = { vcpuPerTB: 8, memGBPerTB: 32 };
+
+// Lightning-path concurrency levels. Baseline: one 8c/32G segment per TB
+// on-disk supports up to ~80 concurrent queries (statement_mem math:
+// 32G x 0.9 / 80 = 368MB per query). Higher tiers scale per-segment
+// resources linearly per the statement_mem formula. Storage math unaffected.
+export const CONCURRENCY_LEVELS = [
+ { id: 'std', max: 80, factor: 1 }, // default — leaves results unchanged
+ { id: 'high', max: 120, factor: 1.5 },
+ { id: 'xhigh', max: 160, factor: 2 },
+];
+
+// Physical presets. cores = OS-visible logical cores (x86 with HT; on ARM,
+// physical cores are the logical count). arrayTB = usable RAID array
+// capacity per data node.
+// sas_std / ssd_perf: fin-industry 2023 deck (24 disks, 2×RAID5 of 12 → 22
disks usable).
+// nvme_modern: 2025 mainstream practice (12 × 3.84T NVMe, RAID5 → 11 usable).
+export const PHYSICAL_PRESETS = [
+ {
+ id: 'sas_std', cores: 128, memGB: 512, arrayTB: 26.4, coordStorageTB: 1.8,
+ network: '2 × 10GbE',
+ bom: {
+ cpu: '2 × 32C x86 (HT, 128 threads)', mem: '512 GB',
+ sysDisk: '2 × 600GB 10K SAS, RAID1',
+ dataDisk: '24 × 1.2TB 10K SAS', raidKey: 'raid.2x12r5',
+ coordDataDisk: '4 × 600GB 10K SAS, RAID5',
+ },
+ sourceKey: 'source.fin2023',
+ },
+ {
+ id: 'ssd_perf', cores: 128, memGB: 1024, arrayTB: 21.12, coordStorageTB:
2.8,
+ network: '2 × 10GbE / 25GbE',
+ bom: {
+ cpu: '2 × 32C x86 (HT, 128 threads)', mem: '1024 GB',
+ sysDisk: '2 × 960GB SSD, RAID1',
+ dataDisk: '24 × 960GB SATA SSD', raidKey: 'raid.2x12r5',
+ coordDataDisk: '4 × 960GB SSD, RAID5',
+ },
+ sourceKey: 'source.fin2023',
+ },
+ {
+ id: 'nvme_modern', cores: 128, memGB: 1024, arrayTB: 42.24,
coordStorageTB: 1.9,
+ network: '2 × 25GbE',
+ bom: {
+ cpu: '2 × 32C x86 (Xeon SPR / EPYC 9004; HT, 128 threads)', mem: '1024
GB DDR5',
+ sysDisk: '2 × 960GB NVMe, RAID1',
+ dataDisk: '12 × 3.84TB NVMe U.2', raidKey: 'raid.1x12r5',
+ coordDataDisk: '2 × 1.92TB NVMe, RAID1',
+ },
+ sourceKey: 'source.mainstream',
+ },
+];
+
+// VM profiles (virtualized sizing practice, modernized). maxTB = business
data threshold.
+// 1:4 vCPU:memory, exactly N x (8c/32G per segment). Memory-optimized (1:8)
+// variants are a valid upgrade for extra cache/concurrency headroom but do
+// not change node counts under the compute rule.
+export const VM_PROFILES = [
+ { id: 'lite', maxTB: 5, vcpu: 8, memGB: 32, storageTB: 2,
throughput: '≥ 500 MB/s', hostKey: 'vmhost.lite' },
+ { id: 'medium', maxTB: 50, vcpu: 16, memGB: 64, storageTB: 4,
throughput: '≥ 1000 MB/s', hostKey: 'vmhost.medium' },
+ { id: 'large', maxTB: Infinity, vcpu: 24, memGB: 96, storageTB: 8,
throughput: '≥ 1500 MB/s', hostKey: 'vmhost.large' },
+];
+export const VM_COORD = { vcpu: 8, memGB: 32, storageTB: 0.5 };
+
+// Cloud schemes: per provider one managed-disk scheme (cloud deployment
+// best practice) and one local-NVMe scheme (production practice). Both
+// deploy segment primary + mirror for HA.
+export const CLOUD_SCHEMES = [
+ {
+ id: 'aws_ebs', provider: 'AWS', kindKey: 'scheme.managed', sourceKey:
'source.cloudbp',
+ network: '10–25 Gbps (ENA)', oss: 'Amazon S3',
+ coordinator: { instance: 'r5.xlarge', vcpu: 4, memGB: 32, storageTB: 0.5,
diskDesc: '1 × 500GB EBS GP3' },
+ segment: { instance: 'r5.4xlarge', vcpu: 16, memGB: 128, storageTB: 6,
diskDesc: '3 × 2TB EBS ST1/GP3' },
+ },
+ {
+ id: 'aws_local', provider: 'AWS', kindKey: 'scheme.local', sourceKey:
'source.hashdata',
+ network: '25 Gbps', oss: 'Amazon S3',
+ coordinator: { instance: 'i3.2xlarge', vcpu: 8, memGB: 61, storageTB: 1.9,
diskDesc: '1 × 1.9TB NVMe' },
+ segment: { instance: 'i3en.2xlarge', vcpu: 8, memGB: 64, storageTB: 5,
diskDesc: '2 × 2.5TB NVMe local' },
+ },
+ {
+ id: 'azure_premium', provider: 'Azure', kindKey: 'scheme.managed',
sourceKey: 'source.cloudbp',
+ network: 'Accelerated Networking, UDP interconnect', oss: 'Azure Blob',
+ coordinator: { instance: 'Standard_E8s_v5', vcpu: 8, memGB: 64, storageTB:
1, diskDesc: '1 × P30 1TB Premium SSD' },
+ segment: { instance: 'Standard_E16s_v5', vcpu: 16, memGB: 128,
storageTB: 6, diskDesc: '3 × P40 2TB Premium SSD' },
+ noteKey: 'note.azure.extra',
+ },
+ {
+ id: 'azure_local', provider: 'Azure', kindKey: 'scheme.local', sourceKey:
'source.hashdata',
+ network: '12.5–32 Gbps', oss: 'Azure Blob',
+ coordinator: { instance: 'Standard_E8s_v5', vcpu: 8, memGB: 64, storageTB:
1, diskDesc: '1 × P30 1TB Premium SSD' },
+ segment: { instance: 'Standard_L8s_v3', vcpu: 8, memGB: 64, storageTB:
1.92, diskDesc: '1 × 1.92TB NVMe local' },
+ },
+ {
+ id: 'gcp_pd', provider: 'GCP', kindKey: 'scheme.managed', sourceKey:
'source.cloudbp',
+ network: '10–25 Gbps', oss: 'Google Cloud Storage',
+ coordinator: { instance: 'n2-highmem-8', vcpu: 8, memGB: 64, storageTB:
0.5, diskDesc: '1 × 500GB pd-ssd' },
+ segment: { instance: 'n2-highmem-8', vcpu: 8, memGB: 64, storageTB: 4,
diskDesc: '1 × 4TB pd-ssd' },
+ noteKey: 'note.gcp.extra',
+ },
+ {
+ id: 'gcp_local', provider: 'GCP', kindKey: 'scheme.local', sourceKey:
'source.hashdata',
+ network: '20 Gbps', oss: 'Google Cloud Storage',
+ coordinator: { instance: 'c3d-standard-8-lssd', vcpu: 8, memGB: 32,
storageTB: 2, diskDesc: 'Local SSD' },
+ segment: { instance: 'c3d-standard-8-lssd', vcpu: 8, memGB: 32,
storageTB: 2, diskDesc: '2TB Local SSD' },
+ },
+];
+
diff --git a/src/components/sizing/logic.ts b/src/components/sizing/logic.ts
new file mode 100644
index 0000000000..ad4dba2187
--- /dev/null
+++ b/src/components/sizing/logic.ts
@@ -0,0 +1,170 @@
+// DOM wiring for the sizing calculator (English-only, no language toggle).
+// Pure calculation lives in calc.js; tunable data in config.js.
+// Adapted from https://github.com/cloudberry-fe/sizing (Apache-2.0).
+import { toTB, calcPhysical, calcVM, calcCloud, summarize, recommendVMProfile
} from './calc.js';
+import { PHYSICAL_PRESETS, VM_PROFILES, CLOUD_SCHEMES, CONCURRENCY_LEVELS }
from './config.js';
+import { t } from './strings';
+
+type Vars = Record<string, string | number>;
+
+export function initSizing(): void {
+ const state = {
+ infra: 'physical',
+ presetId: 'sas_std',
+ vmProfileSel: 'auto',
+ schemeId: 'aws_ebs',
+ };
+
+ const $ = (id: string) => document.getElementById(id) as HTMLElement;
+
+ function fmt(key: string, vars?: Vars): string {
+ let s = t(key);
+ for (const [k, v] of Object.entries(vars || {})) s =
s.replaceAll(`{${k}}`, String(v));
+ return s;
+ }
+
+ function fmtNum(x: number): string {
+ const s = x.toFixed(1);
+ return s.endsWith('.0') ? s.slice(0, -2) : s;
+ }
+
+ function populatePresetCards() {
+ $('preset-cards').innerHTML = PHYSICAL_PRESETS.map((p: any) => `
+ <button type="button" class="preset-card${p.id === state.presetId ? '
selected' : ''}" data-preset="${p.id}">
+ <span class="preset-name">${t('preset.' + p.id)}</span>
+ <span class="preset-desc">${t('preset.' + p.id + '.desc')}</span>
+ </button>`).join('');
+ }
+
+ function populateVMProfile() {
+ const opts = [`<option value="auto"${state.vmProfileSel === 'auto' ? '
selected' : ''}>${t('vmprofile.auto')}</option>`]
+ .concat(VM_PROFILES.map((p: any) =>
+ `<option value="${p.id}"${state.vmProfileSel === p.id ? ' selected' :
''}>${t('vmprofile.' + p.id)}</option>`));
+ ($('vm-profile') as HTMLSelectElement).innerHTML = opts.join('');
+ }
+
+ function populateCloudScheme() {
+ const byProvider: Record<string, any[]> = {};
+ for (const s of CLOUD_SCHEMES as any[]) (byProvider[s.provider] ||=
[]).push(s);
+ ($('cloud-scheme') as HTMLSelectElement).innerHTML =
Object.entries(byProvider).map(([prov, schemes]) =>
+ `<optgroup label="${prov}">` + schemes.map((s: any) =>
+ `<option value="${s.id}"${s.id === state.schemeId ? ' selected' :
''}>` +
+ `${t(s.kindKey)} · ${s.segment.instance}</option>`).join('') +
'</optgroup>').join('');
+ }
+
+ function populateConcurrency() {
+ ($('concurrency') as HTMLSelectElement).innerHTML = CONCURRENCY_LEVELS
+ .map((c: any) => `<option value="${c.id}">${t('conc.' +
c.id)}</option>`).join('');
+ }
+
+ function populateControls() {
+ populatePresetCards();
+ populateVMProfile();
+ populateCloudScheme();
+ populateConcurrency();
+ }
+
+ function specText(r: any): string {
+ const bomLines = (r.bom || []).map((b: any) =>
+ `<div class="bom-line"><span class="bom-k">${t(b.labelKey)}</span>` +
+ `<span class="bom-v">${b.valueKey ? t(b.valueKey) :
b.value}</span></div>`).join('');
+ if (r.cpu == null) return `<span class="spec-main">${r.instance ||
'—'}</span>` + bomLines;
+ const st = r.storageTB == null ? '' :
+ ` / ${r.storageTB >= 1 ? fmtNum(r.storageTB) + 'T' : fmtNum(r.storageTB
* 1024) + 'G'}`;
+ const inst = r.instance ? `<span class="spec-inst">${r.instance}</span>` :
'';
+ const main = `<span class="spec-main">${r.cpu} ${t(r.cpuUnitKey)} /
${r.memGB}G${st}</span>`;
+ return inst + main + bomLines;
+ }
+
+ function activeVMProfileId(dataTB: number): string {
+ return state.vmProfileSel === 'auto' ? recommendVMProfile(dataTB).id :
state.vmProfileSel;
+ }
+
+ function compute() {
+ const size = parseFloat(($('data-size') as HTMLInputElement).value);
+ const valid = Number.isFinite(size) && size > 0;
+ ($('input-error') as HTMLElement).hidden = valid;
+ ($('result-card') as HTMLElement).hidden = !valid;
+ if (!valid) return;
+
+ const dataTB = toTB(size, ($('data-unit') as HTMLSelectElement).value);
+ const compressionRatio = Math.max(1, parseFloat(($('compression') as
HTMLInputElement).value) || 1);
+ const concurrencyFactor = (CONCURRENCY_LEVELS.find((c: any) => c.id ===
($('concurrency') as HTMLSelectElement).value) || CONCURRENCY_LEVELS[0]).factor;
+ let r: any;
+ if (state.infra === 'physical') {
+ r = calcPhysical({ dataTB, compressionRatio, presetId: state.presetId,
concurrencyFactor });
+ } else if (state.infra === 'vm') {
+ r = calcVM({ dataTB, compressionRatio, profileId:
activeVMProfileId(dataTB), concurrencyFactor });
+ } else {
+ r = calcCloud({ dataTB, compressionRatio, schemeId: state.schemeId,
concurrencyFactor });
+ }
+
+ ($('huge-warning') as HTMLElement).hidden = dataTB <= 10240;
+ $('product-line').textContent = t(`product.${state.infra}`);
+
+ const hint = $('vm-profile-hint');
+ if (state.infra === 'vm' && state.vmProfileSel === 'auto') {
+ hint.textContent = fmt('vmprofile.picked', { p:
r.profileId.charAt(0).toUpperCase() + r.profileId.slice(1) });
+ hint.hidden = false;
+ } else hint.hidden = true;
+
+ const scheme = CLOUD_SCHEMES.find((x: any) => x.id === state.schemeId) as
any;
+ $('network-line').textContent =
+ state.infra === 'cloud' ? `${t('network.label')}: ${scheme.network}` :
t('network.10g');
+
+ const badge = $('binding-badge');
+ if (r.binding) {
+ badge.hidden = false;
+ badge.className = `sz-badge sz-badge-${r.binding.type}`;
+ badge.textContent = fmt(`binding.${r.binding.type}`, { s:
r.binding.storageNodes, c: r.binding.computeNodes });
+ } else badge.hidden = true;
+
+ ($('role-table').querySelector('tbody') as HTMLElement).innerHTML =
r.roles.map((role: any) => `<tr>
+ <td>${t('role.' + role.key)}</td><td class="num">${role.count}</td>
+ <td>${specText(role)}</td><td
class="note">${t(role.noteKey)}</td></tr>`).join('');
+
+ const s = summarize(r.roles);
+ const rows: [string, string | number][] = [
+ ['summary.nodes', s.nodes],
+ ['summary.cpu', fmtNum(s.cpu)],
+ ['summary.mem', `${fmtNum(s.memGB)} GB`],
+ ['summary.storage', `${fmtNum(s.storageTB)} TB`],
+ ];
+ if (r.capacityTB != null) rows.push(['summary.capacity',
`${fmtNum(r.capacityTB)} TB`]);
+ ($('summary-table').querySelector('tbody') as HTMLElement).innerHTML =
+ rows.map(([k, v]) => `<tr><th>${t(k)}</th><td>${v}</td></tr>`).join('');
+ }
+
+ function showAdvancedFor(infra: string) {
+ document.querySelectorAll('.adv').forEach(el => {
+ (el as HTMLElement).hidden = !(el as HTMLElement).dataset.for!.split('
').includes(infra);
+ });
+ }
+
+ $('infra-tabs').addEventListener('click', e => {
+ const btn = (e.target as HTMLElement).closest('button[data-infra]') as
HTMLElement | null;
+ if (!btn) return;
+ state.infra = btn.dataset.infra as string;
+ document.querySelectorAll('#infra-tabs button').forEach(b =>
b.classList.toggle('active', b === btn));
+ showAdvancedFor(state.infra);
+ compute();
+ });
+
+ $('preset-cards').addEventListener('click', e => {
+ const card = (e.target as HTMLElement).closest('button[data-preset]') as
HTMLElement | null;
+ if (!card) return;
+ state.presetId = card.dataset.preset as string;
+ document.querySelectorAll('.preset-card').forEach(c =>
c.classList.toggle('selected', c === card));
+ compute();
+ });
+
+ ($('vm-profile') as HTMLSelectElement).addEventListener('input', () => {
state.vmProfileSel = ($('vm-profile') as HTMLSelectElement).value; compute();
});
+ ($('cloud-scheme') as HTMLSelectElement).addEventListener('input', () => {
state.schemeId = ($('cloud-scheme') as HTMLSelectElement).value; compute(); });
+
+ ['data-size', 'data-unit', 'compression', 'concurrency']
+ .forEach(id => $(id).addEventListener('input', compute));
+
+ populateControls();
+ showAdvancedFor(state.infra);
+ compute();
+}
diff --git a/src/components/sizing/sizing.css b/src/components/sizing/sizing.css
new file mode 100644
index 0000000000..bae1a4dcbf
--- /dev/null
+++ b/src/components/sizing/sizing.css
@@ -0,0 +1,134 @@
+/*
+ * Sizing calculator widget styles.
+ * All rules are scoped under .sizingRoot and built on the site's design
+ * tokens (--color-*, --brand-*, --ifm-*) so the tool matches the main
+ * theme and follows dark / light mode automatically.
+ */
+.sizingRoot {
+ --sz-warn: #d97706;
+ --sz-shadow: 0 1px 2px rgba(0, 0, 0, .05), 0 8px 24px -12px rgba(0, 0, 0,
.18);
+ --sz-radius: 14px;
+ max-width: var(--global-main-width);
+ margin: 0 auto;
+ padding: 32px var(--mobile-padding-width) 80px;
+}
+
+.sizingRoot .sizing-head { margin-bottom: 20px; }
+.sizingRoot .sizing-head h1 { font-size: 1.9rem; margin: 0; letter-spacing:
-.01em; }
+.sizingRoot .sizing-head .subtitle { margin: 8px 0 0; color:
var(--color-text-muted); font-size: .95rem; }
+.sizingRoot .sizing-head .method-link {
+ display: inline-block; margin-top: 12px; font-weight: 600; font-size: .9rem;
+}
+
+.sizingRoot [hidden] { display: none !important; }
+
+.sizingRoot .sz-card {
+ display: block;
+ background: var(--color-surface); border: 1px solid var(--color-border);
+ border-radius: var(--sz-radius); padding: 26px 28px; margin-bottom: 20px;
+ box-shadow: var(--sz-shadow);
+}
+
+.sizingRoot .field-grid { display: grid; grid-template-columns: minmax(220px,
320px) 1fr; gap: 24px; }
+.sizingRoot label { display: block; margin: 0 0 8px; font-weight: 600;
font-size: .92rem; }
+.sizingRoot .sz-row { display: flex; flex-wrap: nowrap; align-items: center;
gap: 8px; margin: 0; }
+.sizingRoot .sz-row input[type=number] { flex: 1 1 auto; min-width: 0; }
+
+.sizingRoot input, .sizingRoot select, .sizingRoot button { font: inherit;
color: inherit; }
+.sizingRoot input[type=number], .sizingRoot select {
+ padding: 9px 12px; border: 1px solid var(--color-border-strong);
border-radius: 9px;
+ background: var(--color-bg-subtle); color: var(--color-text); width: 100%;
+ transition: border-color .15s, box-shadow .15s;
+}
+.sizingRoot input[type=number]:focus, .sizingRoot select:focus {
+ outline: none; border-color: var(--color-accent); box-shadow: 0 0 0 3px
var(--brand-orange-soft);
+}
+.sizingRoot .sz-row select { flex: 0 0 auto; width: auto; }
+
+.sizingRoot .sz-tabs {
+ display: flex; gap: 0; background: var(--color-bg-muted); border-radius:
10px;
+ padding: 4px; width: fit-content; flex-wrap: wrap; margin: 0; list-style:
none;
+}
+.sizingRoot .sz-tabs button {
+ cursor: pointer; border: 0; background: transparent; border-radius: 8px;
+ padding: 8px 18px; font-weight: 600; color: var(--color-text-muted);
+ transition: background .15s, color .15s;
+}
+.sizingRoot .sz-tabs button.active {
+ background: var(--color-surface); color: var(--color-accent);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
+}
+
+.sizingRoot .adv { margin-top: 20px; }
+.sizingRoot .preset-cards { display: grid; grid-template-columns:
repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; }
+.sizingRoot .preset-card {
+ cursor: pointer; text-align: left; display: flex; flex-direction: column;
gap: 4px;
+ border: 1.5px solid var(--color-border-strong); border-radius: 12px;
padding: 14px 16px;
+ background: var(--color-bg-subtle); color: inherit;
+ transition: border-color .15s, box-shadow .15s, background .15s;
+}
+.sizingRoot .preset-card:hover { border-color: var(--color-accent); }
+.sizingRoot .preset-card.selected {
+ border-color: var(--color-accent); background: var(--brand-orange-soft);
+ box-shadow: 0 0 0 3px var(--brand-orange-soft);
+}
+.sizingRoot .preset-name { font-weight: 700; font-size: .98rem; }
+.sizingRoot .preset-desc { color: var(--color-text-muted); font-size: .84rem; }
+
+.sizingRoot .hint { color: var(--color-text-muted); font-size: .85rem; margin:
8px 0 0; }
+.sizingRoot summary { cursor: pointer; font-weight: 600; font-size: .92rem;
color: var(--color-text-muted); }
+.sizingRoot .adv-grid { display: grid; grid-template-columns: repeat(auto-fit,
minmax(200px, 260px)); gap: 12px; margin-top: 12px; }
+.sizingRoot .adv-grid label span { display: block; margin-bottom: 6px; }
+
+.sizingRoot .result-head { display: flex; align-items: center; gap: 14px;
flex-wrap: wrap; margin-bottom: 4px; }
+.sizingRoot h2 { font-size: 1.15rem; margin: 18px 0 10px; }
+.sizingRoot .result-head h2 { margin: 0; }
+.sizingRoot .product-chip {
+ background: var(--brand-orange-soft); color: var(--color-accent);
border-radius: 999px;
+ padding: 4px 14px; font-size: .82rem; font-weight: 600;
+}
+.sizingRoot .sz-badge {
+ display: inline-block; border: 0; padding: 5px 14px; border-radius: 999px;
+ font-size: .85rem; font-weight: 600; margin: 6px 0;
+}
+.sizingRoot .sz-badge-storage { background: var(--color-bg-muted); color:
var(--color-text-muted); }
+.sizingRoot .sz-badge-compute { background: var(--brand-orange-soft); color:
var(--color-accent); }
+.sizingRoot .warn { color: var(--sz-warn); font-weight: 600; margin: 10px 0 0;
}
+.sizingRoot .callout {
+ display: flex; align-items: center; gap: 10px;
+ background: var(--brand-orange-soft); border: 1px solid rgba(255, 89, 0,
.25);
+ border-radius: 10px; padding: 10px 14px; margin: 12px 0 4px;
+ color: var(--color-accent); font-weight: 600; font-size: .9rem;
+}
+.sizingRoot .callout::before { content: "⚠"; font-size: 1.05rem; }
+
+.sizingRoot .table-wrap { overflow-x: auto; }
+.sizingRoot table { width: 100%; border-collapse: collapse; margin: 8px 0
10px; }
+.sizingRoot th, .sizingRoot td {
+ text-align: left; padding: 11px 12px; border-bottom: 1px solid
var(--color-border);
+ vertical-align: top; font-size: .92rem;
+}
+.sizingRoot thead th {
+ font-size: .76rem; text-transform: uppercase; letter-spacing: .05em;
+ color: var(--color-text-muted); border-bottom: 1px solid
var(--color-border-strong);
+}
+.sizingRoot tbody tr:hover { background: var(--color-bg-subtle); }
+.sizingRoot td.num { font-variant-numeric: tabular-nums; font-weight: 700; }
+.sizingRoot td.note { color: var(--color-text-muted); font-size: .85rem;
max-width: 260px; }
+
+.sizingRoot .spec-inst { display: block; font-weight: 700; color:
var(--color-accent); }
+.sizingRoot .spec-main { display: block; font-weight: 600; }
+.sizingRoot .bom-line { display: flex; gap: 8px; font-size: .82rem; color:
var(--color-text-muted); margin-top: 3px; }
+.sizingRoot .bom-k { flex: 0 0 5.5em; color: var(--color-text-soft); }
+.sizingRoot .bom-v { flex: 1; }
+
+.sizingRoot .summary th { width: 40%; color: var(--color-text-muted);
font-weight: 600; }
+.sizingRoot .summary td { font-weight: 700; font-variant-numeric:
tabular-nums; }
+.sizingRoot .meta-line { color: var(--color-text-muted); font-size: .85rem;
margin: 8px 0 0; }
+
+@media (max-width: 680px) {
+ .sizingRoot .field-grid { grid-template-columns: 1fr; }
+ .sizingRoot .sz-card { padding: 20px 18px; }
+ .sizingRoot .sz-tabs { width: 100%; }
+ .sizingRoot .sz-tabs button { flex: 1; padding: 8px 8px; }
+}
diff --git a/src/components/sizing/strings.ts b/src/components/sizing/strings.ts
new file mode 100644
index 0000000000..38464ce6a4
--- /dev/null
+++ b/src/components/sizing/strings.ts
@@ -0,0 +1,94 @@
+// English-only string table for the sizing calculator.
+// Adapted from https://github.com/cloudberry-fe/sizing (Apache-2.0).
+export const STRINGS: Record<string, string> = {
+ 'title': 'Apache Cloudberry Sizing Calculator',
+ 'subtitle': 'Enter data size and infrastructure type to get a
recommended hardware configuration',
+ 'input.datasize': 'Data size (uncompressed business data)',
+ 'input.infra': 'Infrastructure type',
+ 'infra.physical': 'Physical',
+ 'infra.vm': 'VM',
+ 'infra.cloud': 'Cloud',
+ 'product.physical': 'Apache Cloudberry · bare metal',
+ 'product.vm': 'Apache Cloudberry · VM',
+ 'product.cloud': 'Apache Cloudberry · cloud',
+ 'advanced': 'Advanced options',
+ 'adv.compression': 'Compression ratio',
+ 'adv.concurrency': 'Concurrency',
+ 'conc.std': 'Standard (≤80 concurrent, default)',
+ 'conc.high': 'High (≤120 concurrent, resources ×1.5)',
+ 'conc.xhigh': 'Very high (≤160 concurrent, resources ×2)',
+ 'nav.method': 'Methodology',
+
+ 'preset.title': 'Hardware preset',
+ 'preset.sas_std': 'Standard · SAS',
+ 'preset.sas_std.desc': '2×32C / 512G / 24×1.2T SAS dual RAID5',
+ 'preset.ssd_perf': 'High-throughput · SSD',
+ 'preset.ssd_perf.desc': '2×32C / 1T / 24×960G SSD dual RAID5',
+ 'preset.nvme_modern': 'Modern · NVMe',
+ 'preset.nvme_modern.desc': '2×32C / 1T DDR5 / 12×3.84T NVMe RAID5',
+
+ 'vmprofile.title': 'VM profile',
+ 'vmprofile.auto': 'Auto (by data size)',
+ 'vmprofile.lite': 'Lite — 8 vCPU / 32G / 2T (≤5TB)',
+ 'vmprofile.medium': 'Medium — 16 vCPU / 64G / 4T (≤50TB)',
+ 'vmprofile.large': 'Large — 24 vCPU / 96G / 8T (>50TB)',
+ 'vmprofile.picked': 'Auto-selected profile: {p}',
+ 'vmhost.lite': 'Shared host acceptable',
+ 'vmhost.medium': 'CPU overcommit ≤ 1:2',
+ 'vmhost.large': 'Dedicated host, 1:1 physical',
+
+ 'scheme.title': 'Cloud scheme',
+ 'scheme.managed': 'Managed-disk',
+ 'scheme.local': 'Local-NVMe',
+
+ 'bom.cpu': 'CPU',
+ 'bom.mem': 'Memory',
+ 'bom.sysdisk': 'System disk',
+ 'bom.datadisk': 'Data disks',
+ 'bom.raid': 'RAID',
+ 'bom.nic': 'NIC',
+ 'bom.throughput': 'Disk throughput',
+ 'bom.layout': 'Segment layout',
+ 'bom.perseg': 'Data per primary',
+ 'bom.segmem': 'Memory per segment',
+ 'bom.host': 'Host requirement',
+ 'raid.2x12r5': '2 × RAID5 groups of 12 (22 disks usable)',
+ 'raid.1x12r5': '12-disk RAID5 (11 usable); needs tri-mode RAID (PERC
H755N/H965i, MegaRAID 9560-class) or VROC; JBOD + mirror redundancy also
viable',
+
+ 'result.title': 'Recommended configuration',
+ 'col.role': 'Role',
+ 'col.count': 'Nodes',
+ 'col.spec': 'Per-node spec',
+ 'col.note': 'Notes',
+ 'role.coordinator': 'Coordinator (primary + standby)',
+ 'role.segment': 'Segment node',
+ 'role.datanode': 'DataNode',
+ 'role.oss': 'Object storage (OSS)',
+ 'note.coord.physical': 'Active/standby HA',
+ 'note.segment.physical': 'primary+mirror; XFS filesystem',
+ 'note.coord.vm': 'Active/standby HA',
+ 'note.datanode.vm': 'primary+mirror; SSD preferred; XFS',
+ 'note.azure.extra': 'primary+mirror; use UDP interconnect, reserve port
65330',
+ 'note.gcp.extra': 'primary+mirror; prefer more, smaller nodes on GCP
(vs AWS)',
+ 'note.oss': 'On demand, S3-compatible API',
+
+ 'summary.title': 'Resource summary',
+ 'summary.nodes': 'Total nodes',
+ 'summary.cpu': 'Total vCPU/cores',
+ 'summary.mem': 'Total memory',
+ 'summary.storage': 'Total data storage',
+ 'summary.capacity': 'Usable data capacity (at chosen compression)',
+ 'binding.storage': 'Storage-bound: storage needs {s} nodes > compute needs
{c}',
+ 'binding.compute': 'Compute-bound: compute needs {c} nodes > storage needs
{s} (8c32G per TB rule)',
+ 'err.invalid': 'Enter a data size greater than 0',
+ 'warn.huge': 'Beyond typical scale — contact field engineering',
+ 'disclaimer': 'Estimates only. Validate with a POC before final
sizing.',
+ 'unit.vcpu': 'vCPU',
+ 'unit.cores': 'cores',
+ 'network.label': 'Network',
+ 'network.10g': 'Network: 10Gbps Ethernet',
+};
+
+export function t(key: string): string {
+ return STRINGS[key] || key;
+}
diff --git a/src/pages/sizing/index.tsx b/src/pages/sizing/index.tsx
new file mode 100644
index 0000000000..d8a61471a7
--- /dev/null
+++ b/src/pages/sizing/index.tsx
@@ -0,0 +1,112 @@
+import { useEffect } from "react";
+import Layout from "@theme/Layout";
+import Link from "@docusaurus/Link";
+import { initSizing } from "../../components/sizing/logic";
+import { t } from "../../components/sizing/strings";
+import "../../components/sizing/sizing.css";
+
+export default function SizingCalculator(): JSX.Element {
+ useEffect(() => {
+ initSizing();
+ }, []);
+
+ return (
+ <Layout
+ title="Sizing Calculator"
+ description="Estimate an Apache Cloudberry hardware configuration from
your data size and infrastructure type."
+ >
+ <div className="sizingRoot">
+ <div className="sizing-head">
+ <h1>{t("title")}</h1>
+ <p className="subtitle">{t("subtitle")}</p>
+ <Link className="method-link" to="/sizing/methodology">
+ {t("nav.method")} →
+ </Link>
+ </div>
+
+ <section className="sz-card">
+ <div className="field-grid">
+ <div>
+ <label htmlFor="data-size">{t("input.datasize")}</label>
+ <div className="sz-row">
+ <input id="data-size" type="number" min={0} step="any"
defaultValue={100} />
+ <select id="data-unit" defaultValue="TB">
+ <option value="GB">GB</option>
+ <option value="TB">TB</option>
+ <option value="PB">PB</option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <label>{t("input.infra")}</label>
+ <div className="sz-tabs" id="infra-tabs">
+ <button type="button" data-infra="physical"
className="active">{t("infra.physical")}</button>
+ <button type="button" data-infra="vm">{t("infra.vm")}</button>
+ <button type="button"
data-infra="cloud">{t("infra.cloud")}</button>
+ </div>
+ </div>
+ </div>
+
+ <div className="adv" data-for="physical">
+ <label>{t("preset.title")}</label>
+ <div className="preset-cards" id="preset-cards" />
+ </div>
+
+ <div className="adv" data-for="vm">
+ <label htmlFor="vm-profile">{t("vmprofile.title")}</label>
+ <select id="vm-profile" />
+ <p id="vm-profile-hint" className="hint" />
+ </div>
+
+ <div className="adv" data-for="cloud">
+ <label htmlFor="cloud-scheme">{t("scheme.title")}</label>
+ <select id="cloud-scheme" />
+ </div>
+
+ <details id="advanced" className="adv" data-for="physical vm cloud">
+ <summary>{t("advanced")}</summary>
+ <div className="adv-grid">
+ <label>
+ <span>{t("adv.compression")}</span>
+ <input id="compression" type="number" min={1} step="0.5"
defaultValue={2} />
+ </label>
+ <label>
+ <span>{t("adv.concurrency")}</span>
+ <select id="concurrency" />
+ </label>
+ </div>
+ </details>
+ <p id="input-error" className="warn" hidden>{t("err.invalid")}</p>
+ </section>
+
+ <section className="sz-card" id="result-card" hidden>
+ <div className="result-head">
+ <h2>{t("result.title")}</h2>
+ <span id="product-line" className="product-chip" />
+ </div>
+ <p className="callout">{t("disclaimer")}</p>
+ <p id="binding-badge" className="sz-badge" hidden />
+ <p id="huge-warning" className="warn" hidden>{t("warn.huge")}</p>
+ <div className="table-wrap">
+ <table id="role-table">
+ <thead>
+ <tr>
+ <th>{t("col.role")}</th>
+ <th>{t("col.count")}</th>
+ <th>{t("col.spec")}</th>
+ <th>{t("col.note")}</th>
+ </tr>
+ </thead>
+ <tbody />
+ </table>
+ </div>
+ <h2>{t("summary.title")}</h2>
+ <table id="summary-table" className="summary">
+ <tbody />
+ </table>
+ <p id="network-line" className="meta-line" />
+ </section>
+ </div>
+ </Layout>
+ );
+}
diff --git a/src/pages/sizing/methodology.mdx b/src/pages/sizing/methodology.mdx
new file mode 100644
index 0000000000..1dfdfa2861
--- /dev/null
+++ b/src/pages/sizing/methodology.mdx
@@ -0,0 +1,124 @@
+---
+title: Sizing Methodology
+description: How the Apache Cloudberry sizing calculator estimates compute and
storage resources.
+---
+
+# Sizing Methodology
+
+How compute and storage resources are estimated. [← Back to
calculator](/sizing/)
+
+## 1. Inputs
+
+The inputs are the **uncompressed business data size** (GB/TB/PB) and the
infrastructure type. Every path derives node counts and per-node specs from
these two values.
+
+## 2. Common rules
+
+### Compression
+
+With columnar storage + compression, on-disk data ≈ business data ÷
compression ratio (default 2, adjustable in advanced options).
+
+```
+onDiskTB = dataTB ÷ compressionRatio
+```
+
+### Compute constraint (8c32G / TB)
+
+Baseline: one segment of **8 logical cores (vCPU) + 32 GB** per TB of on-disk
data supports up to **~80 concurrent queries** — enough for most data-warehouse
workloads. Grounding, from MPP memory-management best practice: 8GB minimum and
32GB recommended per segment; per-query memory `statement_mem =
gp_vmem_protect_limit × 0.9 ÷ expected_concurrency`, and 32G × 0.9 ÷ 80 ≈ 368MB
per query is a healthy value. For higher concurrency the per-segment resources
scale linearly (advanced option [...]
+
+```
+perNodeTB = min( vCPU ÷ (8 × f), memGB ÷ (32 × f) ) # f = concurrency
factor: ≤80→1 (default) / ≤120→1.5 / ≤160→2
+computeNodes = CEIL( onDiskTB ÷ perNodeTB )
+```
+
+**What "concurrency" means here**: the number of **simultaneously active
statements** after resource-group queueing (mixed BI/reporting workloads,
~300–400MB per query). If the workload is dominated by heavyweight analytical
queries (large joins needing GBs per query), plan for **5–10 concurrent per
segment** instead — the same 8-logical-core + 32G quota then gives each query
32G × 0.9 ÷ 5 ≈ 5.7GB. There is no hard "X concurrent per core" formula for
CPU: queries share CPU elastically vi [...]
+
+### Per-segment resource quota (field experience)
+
+The complete resource profile of one primary segment. The first two rows scale
with the concurrency factor; network and disk I/O are **per-segment full-load
peak references** for sanity-checking host aggregate bandwidth and array
throughput (analytical bursts rarely align across all segments, so do not
multiply them rigidly):
+
+| Resource | Quota per segment | Ratio to vCPU |
+|---|---|---|
+| Logical cores (vCPU) | 8 × f | — |
+| Memory | 32 GB × f | 4 GB : 1 vCPU |
+| Managed data (on-disk) | 1 TB | 128 GB : 1 vCPU |
+| Interconnect network | 4 Gbps | 0.5 Gbps : 1 vCPU |
+| Disk read | 300 MB/s | ≈38 MB/s : 1 vCPU |
+| Disk write | 300 MB/s | ≈38 MB/s : 1 vCPU |
+
+Applicable concurrency: ≤80 active statements of mixed BI at f=1; plan 5–10
per segment for heavyweight analytics. Coordinators do not follow this table —
use the fixed specs given per path.
+
+### Storage constraint (unified formula)
+
+Every path uses one per-node usable-capacity formula, each discount applied
exactly once: ×0.9 OS/filesystem overhead, ×0.8 to keep 20% free, ÷(copies +
1/3 workspace). **Copies is 2** (segment primary + mirror, the default HA
layout). Physical passes post-RAID array capacity; VM/cloud pass nominal
data-disk capacity:
+
+```
+usableTB = nominalTB × 0.9 × 0.8 ÷ (2 + 1/3) # primary+mirror ≈ ×0.31
+storageNodes = max( 2, CEIL( onDiskTB ÷ usableTB ) )
+```
+
+### Final node count
+
+Storage-driven and compute-driven node counts are derived independently; the
**larger** wins, then rounds up to an even number (for symmetric primary/mirror
placement). The result badge shows which constraint binds.
+
+```
+nodes = roundUpToEven( max( storageNodes, computeNodes ) )
+```
+
+## 3. Physical · Apache Cloudberry
+
+Storage nodes use the unified formula with nominalTB = post-RAID array
capacity. The method derives from a financial-industry production practice
(2023); the unified formula adds the workspace term on top, so it is slightly
more conservative (e.g. 14 instead of 12 high-throughput nodes at 160TB / cr 2).
+
+Three hardware presets (arrayTB = usable disks after RAID5 × disk size):
+
+| Preset | CPU / MEM | Data disks | RAID | Usable/node | NIC |
+|---|---|---|---|---|---|
+| Standard · SAS | 2×32C / 512G | 24 × 1.2TB 10K SAS | 2 × RAID5 groups of 12
(22 usable) | 26.4 TB | 2×10GbE |
+| High-throughput · SSD | 2×32C / 1024G | 24 × 960GB SSD | same | 21.1 TB |
2×10GbE/25GbE |
+| Modern · NVMe | 2×32C / 1024G DDR5 | 12 × 3.84TB NVMe U.2 | RAID5 (11
usable; tri-mode RAID or VROC) | 42.2 TB | 2×25GbE |
+
+NVMe RAID note: mainstream 2U servers take 12–24 hot-swap U.2/U.3 front bays
(backplane-attached to CPU PCIe lanes, consuming no expansion slots; 12 drives
× 4 lanes = 48 of the 128–160 lanes on EPYC 9004 / dual-socket SPR); hardware
NVMe RAID5 needs a tri-mode controller (Dell PERC H755N/H965i, Broadcom
MegaRAID 9560/9600) or Intel VROC. JBOD with mirror-only redundancy is a common
higher-throughput alternative — a single disk failure then triggers mirror
takeover; this tool sizes conse [...]
+
+Coordinator is fixed at 2 (primary + standby), same CPU/memory class, RAID1
system disks + a small data array. The compute constraint uses the host's
OS-visible logical cores (2×32C with HT = 128 threads) and memory in the common
rule.
+
+### Segments per host
+
+Primaries per host = host resources ÷ per-segment quota (8 logical cores / 32G
× f), the tighter of CPU and memory; mirrors equal primaries, spread across
other hosts (the result page shows `N primary + N mirror` plus the actual data
per primary). Cores always mean **OS-visible logical cores** (thread count on
x86 with hyperthreading; physical cores on SMT-less ARM) — one rule for
physical, VM, and cloud alike, with no special cases. Examples: a physical host
with 2×32C + HT = 128 thread [...]
+
+Segments per host must weigh: cores, RAM, NICs, attached storage, the
primary/mirror mixture, and ETL or other processes on the host. Memory
parameters follow these formulas (same-named Cloudberry GUCs):
+
+```
+gp_vmem (host ≥256G) = ((SWAP + RAM) − (7.5GB + 0.05 × RAM)) ÷ 1.17
+gp_vmem_protect_limit = gp_vmem ÷ max_acting_primary_segments
+statement_mem = gp_vmem_protect_limit × 0.9 ÷ expected_concurrency
+```
+
+## 4. VM · Apache Cloudberry
+
+Storage nodes use the unified formula with nominalTB = the VM's nominal
data-disk capacity. Deploys primary+mirror for high availability.
+
+Three VM profiles (1:4 memory ratio — each tier is exactly N × the 8c/32G
per-segment quota; a 1:8 memory-optimized variant adds cache/concurrency
headroom without changing node counts). Auto-recommended by data size, manual
override available:
+
+| Profile | Fits | vCPU / MEM / Disk | Throughput | Host |
+|---|---|---|---|---|
+| Lite | ≤5 TB | 8 / 32G / 2T SSD | ≥500 MB/s | Shared host OK |
+| Medium | ≤50 TB | 16 / 64G / 4T SSD | ≥1000 MB/s | CPU overcommit ≤1:2 |
+| Large | >50 TB | 24 / 96G / 8T SSD | ≥1500 MB/s | Dedicated host, 1:1
physical |
+
+## 5. Cloud · Apache Cloudberry
+
+Same node-count formula as VM, with per-node storage from the selected cloud
scheme. Two schemes per provider:
+
+- **Managed-disk schemes** (cloud deployment best practice): AWS r5.4xlarge +
3×EBS ST1/GP3; Azure Standard_E16s_v5 + 3×P40 Premium SSD; GCP n2-highmem-8 +
pd-ssd. Deploys primary+mirror for high availability.
+- **Local-NVMe schemes** (production practice): AWS i3en.2xlarge, Azure
Standard_L8s_v3, GCP c3d-standard-8-lssd. Higher throughput, lower cost; local
disks die with the host, so **primary/mirror is always kept**.
+
+Extra guidance: on Azure use the UDP interconnect and reserve port 65330; on
GCP prefer more, smaller nodes (vs AWS).
+
+## 6. Totals & usable capacity
+
+Totals sum count × per-node spec across roles. "Usable data capacity" inverts
the storage formula at the final node count — it is ≥ your input, and the
difference is the recommendation's natural headroom.
+
+```
+capacityTB = nodes × usableTB × compressionRatio
+```
+
+⚠ All results are estimates; validate with a POC before final sizing.
diff --git a/versioned_docs/version-2.x/deployment/capacity_planning.md
b/versioned_docs/version-2.x/deployment/capacity_planning.md
index 3586598340..1c268ac33d 100644
--- a/versioned_docs/version-2.x/deployment/capacity_planning.md
+++ b/versioned_docs/version-2.x/deployment/capacity_planning.md
@@ -4,6 +4,10 @@ title: Estimating storage capacity
To estimate how much data your Apache Cloudberry system can accommodate, use
these measurements as guidelines. Also keep in mind that you may want to have
extra space for landing backup files and data load files on each segment host.
+:::tip
+For a quick starting point, try the [Sizing Calculator](/sizing/). Enter your
data size and infrastructure type (physical, VM, or cloud) and it suggests node
counts and per-node specs. Results are estimates — validate with a POC before
final sizing.
+:::
+
## Calculating usable disk capacity
Start with the raw capacity of the physical disks on a segment host that are
available for data storage:
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]