docs: add form to estimate realtime throughput (#20260)

* docs: add form to estimate realtime throughput

* fix: compute select resetting rls and filters

* fix: typos

* docs: info on compute impact on realtime streaming

* docs: add 200k realtime test results

* docs: fix realtime single thread processing desc

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>

* docs: fix copy for realtime throughput estimation

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>

* fix: review comments and add collapsed table view

* fix: change concurrency to connected clients

* docs: add rt 100k test results

---------

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>
This commit is contained in:
egorandCharis authored and GitHub committed 2024-01-15 14:24:47 +04:00
1 parent 36a0d62404
commit fb30063d41
3 files changed
+440 -36

No files matched your search

@@ -0,0 +1,203 @@
import { useState } from 'react'
import { Select, Collapsible, Button, IconChevronDown } from 'ui'
import throughputTable from '~/data/realtime/throughput.json'
export default function RealtimeLimitsEstimater({}) {
const findTableValue = ({ computeAddOn, filters, rls, concurrency }) => {
return throughputTable.find(
(l) =>
l.computeAddOn === computeAddOn &&
l.filters === filters &&
l.rls === rls &&
l.concurrency === concurrency
)
}
const [computeAddOn, setComputeAddOn] = useState('micro')
const [filters, setFilters] = useState(false)
const [rls, setRLS] = useState(false)
const [concurrency, setConcurrency] = useState(500)
const [limits, setLimits] = useState(findTableValue({ computeAddOn, filters, rls, concurrency }))
const [expandPreview, setExpandPreview] = useState(false)
const handleComputeAddOnSelection = (e) => {
const val = e.target.value
setComputeAddOn(val)
setConcurrency(500)
setLimits(findTableValue({ computeAddOn: val, filters, rls, concurrency: 500 }))
}
const handleFiltersSelection = (e) => {
const val = e.target.value.toLowerCase() === 'true'
setFilters(val)
setConcurrency(500)
setLimits(findTableValue({ computeAddOn, filters: val, rls, concurrency: 500 }))
}
const handleRLSSelection = (e) => {
const val = e.target.value.toLowerCase() === 'true'
setRLS(val)
setConcurrency(500)
setLimits(findTableValue({ computeAddOn, filters, rls: val, concurrency: 500 }))
}
const handleConcurrencySelection = (e) => {
const val = parseInt(e.target.value)
setConcurrency(val)
setLimits(findTableValue({ computeAddOn, filters, rls, concurrency: val }))
}
return (
<div>
<h4>Set your expected parameters</h4>
<div className="grid mb-8 gap-y-8 gap-x-8 grid-cols-2 xl:grid-cols-4">
<div>
<label htmlFor="computeAddOn">Compute:</label>
<Select id="computeAddOn" className="font-mono" onChange={handleComputeAddOnSelection}>
<Select.Option value="micro">Micro</Select.Option>
<Select.Option value="small">Small to medium</Select.Option>
<Select.Option value="large">Large to 16XL</Select.Option>
</Select>
</div>
<div>
<label htmlFor="filters">Filters:</label>
<Select
id="filters"
className="font-mono"
disabled={true}
onChange={handleFiltersSelection}
>
<Select.Option value="false">No</Select.Option>
<Select.Option value="true">Yes</Select.Option>
</Select>
</div>
<div>
<label htmlFor="rls">RLS:</label>
<Select id="rls" className="font-mono" onChange={handleRLSSelection}>
<Select.Option value="false">No</Select.Option>
<Select.Option value="true">Yes</Select.Option>
</Select>
</div>
<div>
<label htmlFor="concurrency">Connected clients:</label>
<Select id="concurrency" className="font-mono" onChange={handleConcurrencySelection}>
{throughputTable
.filter(
(l) => l.computeAddOn === computeAddOn && l.filters === filters && l.rls === rls
)
.map((l) => (
<Select.Option
value={l.concurrency.toString()}
selected={l.concurrency === concurrency}
>
{Intl.NumberFormat().format(l.concurrency)}
</Select.Option>
))}
</Select>
</div>
</div>
{limits && (
<div className="mt-8">
<h4>Current maximum possible throughput</h4>
<table className="table-auto">
<thead>
<tr>
<th className="px-4 py-2">Total DB changes /sec</th>
<th className="px-4 py-2">Max messages per client /sec</th>
<th className="px-4 py-2">Max total messages /sec</th>
<th className="px-4 py-2">Latency p95</th>
</tr>
</thead>
<tbody>
<tr>
<td className="border px-4 py-2">{limits.maxDBChanges}</td>
<td className="border px-4 py-2">{limits.maxMessagesPerClient}</td>
<td className="border px-4 py-2">
{Intl.NumberFormat().format(limits.totalMessagesPerSecond)}
</td>
<td className="border px-4 py-2">{limits.p95Latency}ms</td>
</tr>
</tbody>
</table>
</div>
)}
<Collapsible open={expandPreview} onOpenChange={setExpandPreview}>
<Collapsible.Trigger asChild>
<div className="py-1 flex items-center">
<p className="text-sm">View raw throughput table</p>
<Button
type="text"
icon={
<IconChevronDown
size={18}
strokeWidth={2}
className={expandPreview && 'rotate-180'}
/>
}
className="px-1"
onClick={() => setExpandPreview(!expandPreview)}
/>
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div>
{throughputTable
.map((l) => l.computeAddOn)
.filter((v, i, a) => a.indexOf(v) === i)
.map((computeAddOn) => (
<div>
<h4>
{computeAddOn === 'micro'
? 'Micro'
: computeAddOn === 'small'
? 'Small to medium'
: 'Large to 16XL'}
</h4>
<table className="table-auto">
<thead>
<tr>
<th className="px-4 py-2">Filters</th>
<th className="px-4 py-2">RLS</th>
<th className="px-4 py-2">Connected clients</th>
<th className="px-4 py-2">Total DB changes /sec</th>
<th className="px-4 py-2">Max messages per client /sec</th>
<th className="px-4 py-2">Max total messages /sec</th>
<th className="px-4 py-2">Latency p95</th>
</tr>
</thead>
<tbody>
{throughputTable
.filter((l) => l.computeAddOn === computeAddOn)
.map((l) => (
<tr>
<td className="border px-4 py-2">{l.filters ? '✅' : '🚫'}</td>
<td className="border px-4 py-2">{l.rls ? '✅' : '🚫'}</td>
<td className="border px-4 py-2">
{Intl.NumberFormat().format(l.concurrency)}
</td>
<td className="border px-4 py-2">{l.maxDBChanges}</td>
<td className="border px-4 py-2">{l.maxMessagesPerClient}</td>
<td className="border px-4 py-2">
{Intl.NumberFormat().format(l.totalMessagesPerSecond)}
</td>
<td className="border px-4 py-2">{l.p95Latency}ms</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
</Collapsible.Content>
</Collapsible>
</div>
)
}
+232
View File
@@ -0,0 +1,232 @@
[
{
"computeAddOn": "micro",
"filters": false,
"rls": false,
"concurrency": 500,
"maxDBChanges": 64,
"maxMessagesPerClient": 64,
"totalMessagesPerSecond": 32000,
"p95Latency": 238
},
{
"computeAddOn": "micro",
"filters": false,
"rls": false,
"concurrency": 5000,
"maxDBChanges": 10,
"maxMessagesPerClient": 10,
"totalMessagesPerSecond": 50000,
"p95Latency": 807
},
{
"computeAddOn": "micro",
"filters": false,
"rls": false,
"concurrency": 10000,
"maxDBChanges": 5,
"maxMessagesPerClient": 5,
"totalMessagesPerSecond": 50000,
"p95Latency": 1310
},
{
"computeAddOn": "micro",
"filters": false,
"rls": false,
"concurrency": 30000,
"maxDBChanges": 1,
"maxMessagesPerClient": 1,
"totalMessagesPerSecond": 30000,
"p95Latency": 941
},
{
"computeAddOn": "micro",
"filters": false,
"rls": true,
"concurrency": 500,
"maxDBChanges": 30,
"maxMessagesPerClient": 6,
"totalMessagesPerSecond": 3000,
"p95Latency": 228
},
{
"computeAddOn": "micro",
"filters": false,
"rls": true,
"concurrency": 1500,
"maxDBChanges": 10,
"maxMessagesPerClient": 2,
"totalMessagesPerSecond": 3000,
"p95Latency": 356
},
{
"computeAddOn": "micro",
"filters": false,
"rls": true,
"concurrency": 3000,
"maxDBChanges": 5,
"maxMessagesPerClient": 1,
"totalMessagesPerSecond": 3000,
"p95Latency": 616
},
{
"computeAddOn": "small",
"filters": false,
"rls": false,
"concurrency": 500,
"maxDBChanges": 64,
"maxMessagesPerClient": 64,
"totalMessagesPerSecond": 32000,
"p95Latency": 184
},
{
"computeAddOn": "small",
"filters": false,
"rls": false,
"concurrency": 5000,
"maxDBChanges": 10,
"maxMessagesPerClient": 10,
"totalMessagesPerSecond": 50000,
"p95Latency": 782
},
{
"computeAddOn": "small",
"filters": false,
"rls": false,
"concurrency": 10000,
"maxDBChanges": 5,
"maxMessagesPerClient": 5,
"totalMessagesPerSecond": 50000,
"p95Latency": 1349
},
{
"computeAddOn": "small",
"filters": false,
"rls": false,
"concurrency": 35000,
"maxDBChanges": 1,
"maxMessagesPerClient": 1,
"totalMessagesPerSecond": 35000,
"p95Latency": 1287
},
{
"computeAddOn": "small",
"filters": false,
"rls": true,
"concurrency": 500,
"maxDBChanges": 30,
"maxMessagesPerClient": 6,
"totalMessagesPerSecond": 3000,
"p95Latency": 282
},
{
"computeAddOn": "small",
"filters": false,
"rls": true,
"concurrency": 1500,
"maxDBChanges": 10,
"maxMessagesPerClient": 2,
"totalMessagesPerSecond": 3000,
"p95Latency": 387
},
{
"computeAddOn": "small",
"filters": false,
"rls": true,
"concurrency": 3000,
"maxDBChanges": 5,
"maxMessagesPerClient": 1,
"totalMessagesPerSecond": 3000,
"p95Latency": 920
},
{
"computeAddOn": "large",
"filters": false,
"rls": false,
"concurrency": 500,
"maxDBChanges": 64,
"maxMessagesPerClient": 64,
"totalMessagesPerSecond": 32000,
"p95Latency": 184
},
{
"computeAddOn": "large",
"filters": false,
"rls": false,
"concurrency": 5000,
"maxDBChanges": 10,
"maxMessagesPerClient": 10,
"totalMessagesPerSecond": 50000,
"p95Latency": 672
},
{
"computeAddOn": "large",
"filters": false,
"rls": false,
"concurrency": 10000,
"maxDBChanges": 5,
"maxMessagesPerClient": 5,
"totalMessagesPerSecond": 50000,
"p95Latency": 1253
},
{
"computeAddOn": "large",
"filters": false,
"rls": false,
"concurrency": 35000,
"maxDBChanges": 1,
"maxMessagesPerClient": 1,
"totalMessagesPerSecond": 35000,
"p95Latency": 1257
},
{
"computeAddOn": "large",
"filters": false,
"rls": false,
"concurrency": 100000,
"maxDBChanges": "0.1 (6/min)",
"maxMessagesPerClient": "0.1 (6/min)",
"totalMessagesPerSecond": 40000,
"p95Latency": 4951
},
{
"computeAddOn": "large",
"filters": false,
"rls": false,
"concurrency": 200000,
"maxDBChanges": "0.05 (3/min)",
"maxMessagesPerClient": "0.05 (3/min)",
"totalMessagesPerSecond": 40000,
"p95Latency": 4581
},
{
"computeAddOn": "large",
"filters": false,
"rls": true,
"concurrency": 500,
"maxDBChanges": 40,
"maxMessagesPerClient": 8,
"totalMessagesPerSecond": 4000,
"p95Latency": 618
},
{
"computeAddOn": "large",
"filters": false,
"rls": true,
"concurrency": 2000,
"maxDBChanges": 10,
"maxMessagesPerClient": 2,
"totalMessagesPerSecond": 4000,
"p95Latency": 606
},
{
"computeAddOn": "large",
"filters": false,
"rls": true,
"concurrency": 4000,
"maxDBChanges": 5,
"maxMessagesPerClient": 1,
"totalMessagesPerSecond": 4000,
"p95Latency": 918
}
]
@@ -1,5 +1,6 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
import RealtimeLimitsEstimater from '~/components/RealtimeLimitsEstimater'
export const meta = {
title: 'Postgres Changes',
@@ -1361,45 +1362,13 @@ Realtime systems usually require forethought because of their scaling dynamics.
There can be a database bottleneck which limits message throughput. If your database cannot authorize the changes rapidly enough, the changes will be delayed until you receive a timeout.
Database changes are processed on a single thread to maintain the change order. That means compute upgrades don't have a large effect on the performance of Postgres change subscriptions. You can estimate the expected maximum throughput for your database below.
If you are using Postgres Changes at scale, you should consider using separate "public" table without RLS and filters. Alternatively, you can use Realtime server-side only and then re-stream the changes to your clients using a Realtime Broadcast.
From our observations, we recommend the following limits depending on your database size:
Enter your database settings to estimate the maximum throughput for your instance:
### Micro
| Filters | RLS Usage | Concurrent Clients | DB Changes per second (total) | Records per second per client | Messages per second (total) | Latency p95 (ms) |
| ------- | --------- | ------------------ | ----------------------------- | ----------------------------- | --------------------------- | ---------------- |
| 🚫 | 🚫 | 500 | 64 | 64 | 32,000 | 238 |
| 🚫 | 🚫 | 5,000 | 10 | 10 | 50,000 | 807 |
| 🚫 | 🚫 | 10,000 | 5 | 5 | 50,000 | 1310 |
| 🚫 | 🚫 | 30,000 | 1 | 1 | 30,000 | 941 |
| 🚫 | ✅ | 500 | 30 | 6 | 3,000 | 228 |
| 🚫 | ✅ | 1,500 | 10 | 2 | 3,000 | 356 |
| 🚫 | ✅ | 3,000 | 5 | 1 | 3,000 | 616 |
### Small to medium
| Filters | RLS Usage | Concurrent Clients | DB Changes per second (total) | Records per second per client | Messages per second (total) | Latency p95 (ms) |
| ------- | --------- | ------------------ | ----------------------------- | ----------------------------- | --------------------------- | ---------------- |
| 🚫 | 🚫 | 500 | 64 | 64 | 32,000 | 184 |
| 🚫 | 🚫 | 5,000 | 10 | 10 | 50,000 | 782 |
| 🚫 | 🚫 | 10,000 | 5 | 5 | 50,000 | 1349 |
| 🚫 | 🚫 | 35,000 | 1 | 1 | 35,000 | 1287 |
| 🚫 | ✅ | 500 | 30 | 6 | 3,000 | 282 |
| 🚫 | ✅ | 1,500 | 10 | 2 | 3,000 | 387 |
| 🚫 | ✅ | 3,000 | 5 | 1 | 3,000 | 920 |
### Large to 16XL
| Filters | RLS Usage | Concurrent Clients | DB Changes per second (total) | Records per second per client | Messages per second (total) | Latency p95 (ms) |
| ------- | --------- | ------------------ | ----------------------------- | ----------------------------- | --------------------------- | ---------------- |
| 🚫 | 🚫 | 500 | 64 | 64 | 32,000 | 184 |
| 🚫 | 🚫 | 5,000 | 10 | 10 | 50,000 | 672 |
| 🚫 | 🚫 | 10,000 | 5 | 5 | 50,000 | 1253 |
| 🚫 | 🚫 | 35,000 | 1 | 1 | 35,000 | 1257 |
| 🚫 | ✅ | 500 | 40 | 8 | 4,000 | 618 |
| 🚫 | ✅ | 2,000 | 10 | 2 | 4,000 | 606 |
| 🚫 | ✅ | 4,000 | 5 | 1 | 4,000 | 918 |
<RealtimeLimitsEstimater />
Don't forget to run your own benchmarks to make sure that the performance is acceptable for your use case.