Compare commits
2 Commits
dependabot
...
website-do
Author | SHA1 | Date | |
---|---|---|---|
a351565ff0 | |||
d583ddf0a3 |
50
website/docs/install-config/scaling.mdx
Normal file
50
website/docs/install-config/scaling.mdx
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Scaling
|
||||
---
|
||||
|
||||
import ScalingCalculator from "@site/src/components/ScalingCalculator";
|
||||
|
||||
|
||||
authentik can be scaled to several thousands of users. This page aims to provide some guidance on installation sizing and tuning for those large installations.
|
||||
|
||||
:::note
|
||||
The numbers indicated on this page are meant as general guidelines and starting points. Usage patterns vary from one installation to another, and we are constantly optimizing authentik to improve its performance.
|
||||
|
||||
The data used to build this page has been collected from benchmarks run on version 2024.6, and from real-world customer feedback.
|
||||
:::
|
||||
|
||||
## How does authentik scale?
|
||||
|
||||
### authentik server
|
||||
|
||||
The authentik server is a stateless component that serves HTTP requests. As such, it is completely stateless and scales linearly. That means that if a single authentik server with X resources (CPU and RAM) can serve N requests, two authentik server with each X resources can serve 2N requests.
|
||||
|
||||
### authentik worker
|
||||
|
||||
The authentik worker is a stateless component that processes background tasks for authentik. This includes:
|
||||
|
||||
- maintenance tasks, such as removing outdated resources
|
||||
- synchronization tasks for sources and providers such as SCIM, Google Workspace, LDAP, etc.
|
||||
- one-off tasks that are triggered by some action by a user or an administrator, such as sending notifications.
|
||||
|
||||
Scaling the worker thus greatly depends on the usage made of authentik. If not synchronization is configured and there are few one-off tasks, then the worker will use almost no resources. However, if the worker has to process lengthy synchronization tasks and is then backlogged with other tasks, then it needs to be scaled up.
|
||||
|
||||
As such, we recommend setting up some [monitoring](../sys-mgmt/monitoring.md) and observing how the worker behaves, then scaling it up accordingly. You should aim to have as few tasks as possible in the worker queue.
|
||||
|
||||
### PostgreSQL & Redis
|
||||
|
||||
We recommend looking into both tools' respective documentation to get pointers on how to monitor them.
|
||||
|
||||
The specific usage that authentik makes of PostgreSQL and Redis doesn't scale linearly, but rather logarithmically.
|
||||
|
||||
## Calculator
|
||||
|
||||
<ScalingCalculator />
|
||||
|
||||
#### Known issues
|
||||
|
||||
The database recommendations given by the calculator are scaled linearly to the number of concurrent logins. This is not the pattern we observe in reality. As such, if the calculator returns ludicrous values for the database sizing, expect those to not be representative of the actual resources needed.
|
||||
|
||||
## Feedback
|
||||
|
||||
We welcome feedback on this calculator. [Contact us!](mailto:hello@goauthentik.io)
|
@ -123,6 +123,7 @@ const items = [
|
||||
"install-config/reverse-proxy",
|
||||
"install-config/automated-install",
|
||||
"install-config/air-gapped",
|
||||
"install-config/scaling",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
374
website/src/components/ScalingCalculator/index.tsx
Normal file
374
website/src/components/ScalingCalculator/index.tsx
Normal file
@ -0,0 +1,374 @@
|
||||
import Link from "@docusaurus/Link";
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import Admonition from "@theme/Admonition";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import styles from "./style.module.css";
|
||||
|
||||
type RowID =
|
||||
| "replicas"
|
||||
| "requests_cpu"
|
||||
| "requests_memory"
|
||||
| "gunicorn_workers"
|
||||
| "gunicorn_threads";
|
||||
|
||||
const rows: [rowID: RowID, rowLabel: JSX.Element, units?: string][] = [
|
||||
[
|
||||
// ---
|
||||
"replicas",
|
||||
<Translate id="ak.scalingCalculator.replicas">Replicas</Translate>,
|
||||
],
|
||||
[
|
||||
// ---
|
||||
"requests_cpu",
|
||||
<Translate id="ak.scalingCalculator.requestsCpu">CPU Requests</Translate>,
|
||||
],
|
||||
[
|
||||
// ---
|
||||
"requests_memory",
|
||||
<Translate id="ak.scalingCalculator.requestsMemory">Memory Requests</Translate>,
|
||||
"GB",
|
||||
],
|
||||
|
||||
[
|
||||
// ---
|
||||
"gunicorn_workers",
|
||||
<Link to="./configuration#authentik_web__workers">
|
||||
<Translate>Gunicorn Workers</Translate>
|
||||
</Link>,
|
||||
],
|
||||
[
|
||||
// ---
|
||||
"gunicorn_threads",
|
||||
<Link to="./configuration#authentik_web__threads">
|
||||
<Translate>Gunicorn Threads</Translate>
|
||||
</Link>,
|
||||
],
|
||||
];
|
||||
|
||||
type SetupEstimate = {
|
||||
[key in RowID]: number;
|
||||
};
|
||||
|
||||
type SetupEntry = [columnLabel: React.ReactNode, estimate: SetupEstimate];
|
||||
|
||||
const FieldName = {
|
||||
UserCount: "userCount",
|
||||
ConcurrentLogins: "loginCount",
|
||||
FlowDuration: "flowDuration",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
type FieldKey = keyof typeof FieldName;
|
||||
type FieldName = (typeof FieldName)[FieldKey];
|
||||
|
||||
type EstimateInput = { [key in FieldName]: number };
|
||||
|
||||
type FieldID = `${FieldName}-field`;
|
||||
|
||||
const FieldID = Object.fromEntries(
|
||||
Object.entries(FieldName).map(([key, value]) => [key, `${value}-field`]),
|
||||
) as Record<FieldKey, FieldID>;
|
||||
|
||||
const SetupComparisionTable: React.FC<EstimateInput> = ({ loginCount }) => {
|
||||
const cpuCount = Math.max(1, Math.ceil(loginCount / 10));
|
||||
|
||||
const setups: SetupEntry[] = [
|
||||
[
|
||||
<Translate
|
||||
id="ak.setup.kubernetesRAMOptimized"
|
||||
values={{ platform: "Kubernetes", variant: "(RAM Optimized)", separator: <br /> }}
|
||||
>
|
||||
{"{platform}{separator}{variant}"}
|
||||
</Translate>,
|
||||
{
|
||||
gunicorn_threads: 2,
|
||||
gunicorn_workers: 3,
|
||||
replicas: Math.max(2, Math.ceil(cpuCount / 2)),
|
||||
requests_cpu: 2,
|
||||
requests_memory: 1.5,
|
||||
},
|
||||
],
|
||||
[
|
||||
<Translate
|
||||
id="ak.setup.kubernetesCPUOptimized"
|
||||
values={{ platform: "Kubernetes", variant: "(CPU Optimized)", separator: <br /> }}
|
||||
>
|
||||
{"{platform}{separator}{variant}"}
|
||||
</Translate>,
|
||||
{
|
||||
gunicorn_threads: 2,
|
||||
gunicorn_workers: 2,
|
||||
replicas: Math.max(2, cpuCount),
|
||||
requests_cpu: 1,
|
||||
requests_memory: 1,
|
||||
},
|
||||
],
|
||||
[
|
||||
<Translate
|
||||
id="ak.setup.dockerVM"
|
||||
values={{
|
||||
platform: "Docker Compose",
|
||||
variant: "(Virtual machine)",
|
||||
separator: <br />,
|
||||
}}
|
||||
>
|
||||
{"{platform}{separator}{variant}"}
|
||||
</Translate>,
|
||||
{
|
||||
gunicorn_threads: 2,
|
||||
gunicorn_workers: cpuCount + 1,
|
||||
replicas: Math.max(2, cpuCount),
|
||||
requests_cpu: cpuCount,
|
||||
requests_memory: cpuCount,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return (
|
||||
<Admonition type="tip" icon={null} title={null} className={styles.admonitionTable}>
|
||||
<div
|
||||
className={styles.comparisionTable}
|
||||
style={
|
||||
{ "--ak-comparision-table-columns": setups.length + 1 } as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<header>
|
||||
<div className={styles.columnLabel}>
|
||||
<Translate id="ak.scalingCalculator.server">Resources</Translate>
|
||||
</div>
|
||||
{setups.map(([columnLabel], i) => (
|
||||
<div className={styles.columnLabel} key={i}>
|
||||
{columnLabel}
|
||||
</div>
|
||||
))}
|
||||
</header>
|
||||
|
||||
{rows.map(([rowID, rowLabel, units]) => {
|
||||
return (
|
||||
<section key={rowID}>
|
||||
<div className={styles.rowLabel}>{rowLabel}</div>
|
||||
|
||||
{setups.map(([_rowLabel, estimate], i) => {
|
||||
const estimateValue = estimate[rowID] || "N/A";
|
||||
|
||||
return (
|
||||
<div className={styles.fieldValue} key={i}>
|
||||
<Translate
|
||||
id={`ak.scalingCalculator.${rowID}`}
|
||||
values={{
|
||||
value: estimateValue,
|
||||
units: units ? ` ${units}` : "",
|
||||
}}
|
||||
>
|
||||
{"{value}{units}"}
|
||||
</Translate>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Admonition>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatabaseEstimateTable: React.FC<EstimateInput> = ({ loginCount, userCount }) => {
|
||||
const cpuCount = Math.max(1, Math.ceil(loginCount / 10));
|
||||
|
||||
const postgres = {
|
||||
cpus: Math.max(2, cpuCount / 4),
|
||||
ram: Math.max(4, cpuCount),
|
||||
storage_gb: Math.ceil(userCount / 25000),
|
||||
};
|
||||
|
||||
const redis = {
|
||||
cpus: Math.max(2, cpuCount / 4),
|
||||
ram: Math.max(2, cpuCount / 2),
|
||||
};
|
||||
|
||||
return (
|
||||
<Admonition type="tip" icon={null} title={null} className={styles.admonitionTable}>
|
||||
<div
|
||||
className={styles.comparisionTable}
|
||||
style={{ "--ak-comparision-table-columns": 3 } as React.CSSProperties}
|
||||
>
|
||||
<header>
|
||||
<div className={styles.columnLabel}>
|
||||
<Translate id="ak.scalingCalculator.server">Resources</Translate>
|
||||
</div>
|
||||
<div className={styles.columnLabel}>PostgreSQL</div>
|
||||
<div className={styles.columnLabel}>Redis</div>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<div className={styles.rowLabel}>CPUs</div>
|
||||
<div className={styles.fieldValue}>{postgres.cpus}</div>
|
||||
<div className={styles.fieldValue}>{redis.cpus}</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className={styles.rowLabel}>Memory</div>
|
||||
<div className={styles.fieldValue}>{postgres.ram} GB</div>
|
||||
<div className={styles.fieldValue}>{redis.ram} GB</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className={styles.rowLabel}>Storage</div>
|
||||
<div className={styles.fieldValue}>{postgres.storage_gb} GB</div>
|
||||
<div className={styles.fieldValue}>
|
||||
<Translate id="ak.scalingCalculator.varies">Varies</Translate>
|
||||
</div>
|
||||
<div />
|
||||
</section>
|
||||
</div>
|
||||
</Admonition>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScalingCalculator: React.FC = () => {
|
||||
const [estimateInput, setEstimateInput] = useState<EstimateInput>(() => {
|
||||
const userCount = 100;
|
||||
const flowDuration = 15;
|
||||
|
||||
return {
|
||||
userCount,
|
||||
flowDuration,
|
||||
loginCount: -1,
|
||||
};
|
||||
});
|
||||
|
||||
const estimatedLoginCount = useMemo(() => {
|
||||
const { userCount, flowDuration } = estimateInput;
|
||||
|
||||
// if (loginCount > 0) return loginCount;
|
||||
|
||||
// Assumption that users log in over a period of 15 minutes.
|
||||
return Math.ceil(userCount / 15.0 / 60.0) * flowDuration;
|
||||
}, [estimateInput]);
|
||||
|
||||
const estimatedLoginValue = estimateInput[FieldName.ConcurrentLogins];
|
||||
|
||||
const handleFieldChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {
|
||||
const { name, value } = event.target;
|
||||
const nextFieldValue = value.length ? parseInt(value, 10) : -1;
|
||||
|
||||
setEstimateInput((currentEstimate) => ({
|
||||
...currentEstimate,
|
||||
[name as FieldName]: nextFieldValue,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Translate id="ak.scalingCalculator.usageEstimates">Usage Estimates</Translate>
|
||||
</h3>
|
||||
|
||||
<Admonition type="info" icon={null} title={null}>
|
||||
<form className={styles.admonitionForm} autoComplete="off">
|
||||
<div className={styles.labelGroup}>
|
||||
<label htmlFor={FieldID.UserCount}>
|
||||
<Translate id="ak.scalingCalculator.activeUsersLabel">
|
||||
Active Users
|
||||
</Translate>
|
||||
</label>
|
||||
|
||||
<p>
|
||||
<Translate id="ak.scalingCalculator.activeUsersDescription">
|
||||
This is used to calculate database storage, and estimate how many
|
||||
concurrent logins you can expect.
|
||||
</Translate>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<input
|
||||
id={FieldID.UserCount}
|
||||
type="number"
|
||||
step="10"
|
||||
name={FieldName.UserCount}
|
||||
value={estimateInput[FieldName.UserCount]}
|
||||
onChange={handleFieldChange}
|
||||
required
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.labelGroup}>
|
||||
<label htmlFor={FieldID.FlowDuration}>
|
||||
<Translate id="ak.scalingCalculator.flowDurationLabel">
|
||||
Flow Duration
|
||||
</Translate>
|
||||
</label>
|
||||
|
||||
<p>
|
||||
<Translate id="ak.scalingCalculator.flowDurationDescription">
|
||||
A single login may take several seconds for the user to enter their
|
||||
password, MFA method, etc. If you know what usage pattern to expect,
|
||||
you can override that value from the computed one.
|
||||
</Translate>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<input
|
||||
id={FieldID.FlowDuration}
|
||||
type="number"
|
||||
step="5"
|
||||
name={FieldName.FlowDuration}
|
||||
value={estimateInput[FieldName.FlowDuration]}
|
||||
onChange={handleFieldChange}
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.labelGroup}>
|
||||
<label htmlFor={FieldID.ConcurrentLogins}>
|
||||
<Translate id="ak.scalingCalculator.concurrentLoginsLabel">
|
||||
Concurrent Logins
|
||||
</Translate>
|
||||
</label>
|
||||
|
||||
<p>
|
||||
<Translate id="ak.scalingCalculator.concurrentLoginsDescription">
|
||||
We estimate that all of the users will log in over a period of 15
|
||||
minutes, greatly reducing the load on the instance.
|
||||
</Translate>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<input
|
||||
id={FieldID.ConcurrentLogins}
|
||||
type="number"
|
||||
step="10"
|
||||
name={FieldName.ConcurrentLogins}
|
||||
placeholder={estimatedLoginCount.toString()}
|
||||
value={estimatedLoginValue === -1 ? "" : estimatedLoginValue.toString()}
|
||||
onChange={handleFieldChange}
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Admonition>
|
||||
|
||||
<h3>
|
||||
<Translate id="ak.scalingCalculator.deploymentConfigurations">
|
||||
Deployment Configurations
|
||||
</Translate>
|
||||
</h3>
|
||||
|
||||
<SetupComparisionTable {...estimateInput} />
|
||||
|
||||
<h3>
|
||||
<Translate id="ak.scalingCalculator.DatabaseConfigurations">
|
||||
Database Configurations
|
||||
</Translate>
|
||||
</h3>
|
||||
|
||||
<DatabaseEstimateTable {...estimateInput} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScalingCalculator;
|
127
website/src/components/ScalingCalculator/style.module.css
Normal file
127
website/src/components/ScalingCalculator/style.module.css
Normal file
@ -0,0 +1,127 @@
|
||||
.admonitionForm {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: center;
|
||||
gap: var(--ifm-global-spacing);
|
||||
|
||||
@media (max-width: 1119px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.labelGroup {
|
||||
color: var(--ifm-alert-color);
|
||||
label {
|
||||
font-weight: var(--ifm-font-weight-bold);
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
input[type="number"] {
|
||||
font-weight: var(--ifm-font-weight-bold);
|
||||
font-size: 1.25em;
|
||||
|
||||
flex: 1 1 auto;
|
||||
background-color: var(--ifm-alert-background-color-highlight);
|
||||
padding: 1em;
|
||||
border-color: var(--ifm-alert-background-color-highlight);
|
||||
border-radius: var(--ifm-alert-border-radius);
|
||||
border-style: double;
|
||||
box-shadow: inset var(--ifm-global-shadow-lw);
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:has(input[name="flowDuration"])::after {
|
||||
font-variant: all-petite-caps;
|
||||
font-weight: var(--ifm-font-weight-bold);
|
||||
font-size: 1.25em;
|
||||
flex: 0 0 auto;
|
||||
content: "sec";
|
||||
padding: var(--ifm-global-spacing);
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
font-family: var(--ifm-font-family-monospace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admonitionTable {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comparisionTable {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--ak-comparision-table-columns), 1fr);
|
||||
overflow-x: auto;
|
||||
|
||||
& > header {
|
||||
border-bottom: 1px solid var(--ifm-alert-border-color);
|
||||
}
|
||||
& > header,
|
||||
& > section {
|
||||
display: grid;
|
||||
grid-column: span var(--ak-comparision-table-columns);
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
& > section:not(:last-child) {
|
||||
border-block-end: 1px solid var(--ifm-alert-background-color-highlight);
|
||||
}
|
||||
|
||||
& > section:nth-child(odd) {
|
||||
/* background-color: var(--ifm-table-stripe-background); */
|
||||
|
||||
.rowLabel,
|
||||
.fieldValue {
|
||||
background-color: var(--ifm-table-stripe-background);
|
||||
}
|
||||
}
|
||||
|
||||
.columnLabel {
|
||||
background-color: var(--ifm-alert-background-color-highlight);
|
||||
font-weight: var(--ifm-table-head-font-weight);
|
||||
padding: var(--ifm-table-cell-padding) calc(var(--ifm-spacing-horizontal) * 2);
|
||||
text-align: center;
|
||||
font-weight: var(--ifm-font-weight-bold);
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.rowLabel {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: var(--ifm-z-index-dropdown);
|
||||
backdrop-filter: blur(4px);
|
||||
overflow: hidden;
|
||||
border-block-end-width: 4px;
|
||||
}
|
||||
|
||||
.rowLabel,
|
||||
.fieldValue {
|
||||
background-color: var(--ifm-alert-background-color);
|
||||
padding: var(--ifm-table-cell-padding) calc(var(--ifm-spacing-horizontal) * 2);
|
||||
text-align: center;
|
||||
font-weight: var(--ifm-font-weight-bold);
|
||||
place-content: center;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-inline-end: 1px solid var(--ifm-alert-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
font-size: 1.25em;
|
||||
|
||||
padding: var(--ifm-table-cell-padding);
|
||||
place-items: center;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user