diff --git a/.ci/test-sql-snippets.sh b/.ci/test-sql-snippets.sh new file mode 100755 index 00000000..efcbb32c --- /dev/null +++ b/.ci/test-sql-snippets.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +IFS=$'\n\t' + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +POSTGRES_VERSIONS="${POSTGRES_VERSIONS:-12 13 14 15 16 17 18}" +DOCKER_BIN="${DOCKER_BIN:-docker}" +IFS=' ' read -r -a DOCKER_CMD <<< "${DOCKER_BIN}" +TMP_DIR="$(mktemp -d)" +CONTAINERS=() + +cleanup() { + local container + for container in "${CONTAINERS[@]:-}"; do + "${DOCKER_CMD[@]}" rm -f "${container}" >/dev/null 2>&1 || true + done + rm -rf "${TMP_DIR}" +} +trap cleanup EXIT + +extract_snippet() { + local doc_path="$1" + local snippet_name="$2" + local output_path="$3" + + awk -v snippet="${snippet_name}" ' + $0 == "" { inside=1; next } + $0 == "" { inside=0; next } + inside && $0 !~ /^```/ { print } + ' "${doc_path}" > "${output_path}" + + if ! grep -q '[^[:space:]]' "${output_path}"; then + echo "No SQL extracted for snippet ${snippet_name} from ${doc_path}" >&2 + return 1 + fi +} + +wait_for_postgres() { + local container="$1" + local attempt + for attempt in $(seq 1 60); do + if "${DOCKER_CMD[@]}" exec "${container}" pg_isready -U postgres >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + echo "Postgres did not become ready in ${container}" >&2 + "${DOCKER_CMD[@]}" logs "${container}" >&2 || true + return 1 +} + +run_snippet_on_version() { + local version="$1" + local sql_path="$2" + local container="docs-sql-snippets-pg${version}-$$" + + echo "[sql-snippets] PostgreSQL ${version}: starting" + "${DOCKER_CMD[@]}" rm -f "${container}" >/dev/null 2>&1 || true + "${DOCKER_CMD[@]}" run \ + --detach \ + --name "${container}" \ + --env POSTGRES_PASSWORD=postgres \ + "postgres:${version}" \ + >/dev/null + CONTAINERS+=("${container}") + wait_for_postgres "${container}" + + echo "[sql-snippets] PostgreSQL ${version}: running xmin horizon snippet" + "${DOCKER_CMD[@]}" exec -i "${container}" \ + psql -U postgres -d postgres -v ON_ERROR_STOP=1 \ + < "${sql_path}" \ + >/dev/null + echo "[sql-snippets] PostgreSQL ${version}: ok" +} + +main() { + local doc_path="${ROOT_DIR}/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-xmin-horizon.md" + local sql_path="${TMP_DIR}/xmin-horizon.sql" + + extract_snippet "${doc_path}" "xmin-horizon" "${sql_path}" + + local versions=() + local version + IFS=' ' read -r -a versions <<< "${POSTGRES_VERSIONS}" + for version in "${versions[@]}"; do + run_snippet_on_version "${version}" "${sql_path}" + done +} + +main "$@" diff --git a/.cursor b/.cursor deleted file mode 160000 index e58b6940..00000000 --- a/.cursor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e58b6940c32d8790118eea4ef82267c247b5a91d diff --git a/.github/dependabot.yml b/.github/dependabot.yml index abe23499..f0431ef7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,11 @@ version: 2 updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "npm" directory: "/" schedule: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..5f382e95 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,40 @@ +# NOTE: This repo is mirrored to GitHub where this workflow runs automatically. +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + branches: [master] + schedule: + - cron: '40 17 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['javascript'] + # CodeQL supports: 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@820e3160e279568db735cee8ed8f8e77a6da7818 # v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@820e3160e279568db735cee8ed8f8e77a6da7818 # v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@820e3160e279568db735cee8ed8f8e77a6da7818 # v3 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 86a7c4b2..ebfc0989 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,12 +1,28 @@ -# 2019 © PostgresAI +# 2019-2026 © PostgresAI # # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings include: - template: Security/SAST.gitlab-ci.yml + - project: 'postgres-ai/infra' + file: '/ci/templates/approval-check.yml' + +# Run pipeline only once per push: +# - MR pipelines for feature branches (not duplicate branch pipelines) +# - Push pipelines for master/main/production only +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == "master" + - if: $CI_COMMIT_BRANCH == "main" + - if: $CI_COMMIT_BRANCH == "production" image: docker:20.10.12 +# Allow auto-canceling running pipelines when a newer commit is pushed +default: + interruptible: true + stages: - validate - prepare_image @@ -38,9 +54,29 @@ validate_feeds: - build/blog/*.xml - build/blog/*.json expire_in: 1 week + allow_failure: true # broken on Node v24 (sharp binary mismatch) — TODO fix only: - branches +docs_sql_snippets: + stage: validate + image: docker:27 + services: + - docker:27-dind + variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_TLS_CERTDIR: "" + POSTGRES_VERSIONS: "12 13 14 15 16 17 18" + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + changes: + - .ci/test-sql-snippets.sh + - docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-xmin-horizon.md + - if: $CI_COMMIT_BRANCH == "master" + script: + - apk add --no-cache bash + - bash .ci/test-sql-snippets.sh + # Stages templates. .job_template: &build_and_push_definition stage: prepare_image @@ -66,7 +102,10 @@ validate_feeds: # Build the image. # `--cache-from` arg in `docker build` doesn't have any troubles if # passed image tag is unavailable - it will simply ignore it. + # --network=host: the inner bridge network in dind stalls on outbound + # TLS (bun install hangs forever, #226); host = the dind service netns. - docker build + --network=host --cache-from $TAG_LATEST --tag $TAG_VERSION --tag $TAG_LATEST @@ -121,24 +160,234 @@ validate_feeds: build_and_push_production: <<: *build_and_push_definition <<: *env_production + # bun install occasionally hangs forever inside dind on shared runners + # (#226); a healthy build takes ~10 min — fail fast and retry instead of + # burning the 1h default timeout. + timeout: 30m + retry: + max: 2 + when: + - job_execution_timeout + - stuck_or_timeout_failure + - runner_system_failure only: - master +# DEPRECATED — old v2 staging (k8s ns `staging`, v2.postgres.ai docs path) +# is being sunset. The `build_and_push_staging` and `deploy_staging` jobs +# below are gated off with `when: never` and kept for git history; full +# deletion is Phase 2. +# New staging is preview-based, deployed from `master` — see +# `build_and_push_main_staging` / `deploy_main_staging` +# (https://docs-main.pgai.green). +# Tracking: https://gitlab.com/postgres-ai/infra/-/work_items/50 build_and_push_staging: <<: *build_and_push_definition <<: *env_staging - except: - - master + rules: + - when: never deploy_production: <<: *deploy_definition <<: *env_production + # DAG: deploy as soon as the production image is ready — don't let a flaky + # build_and_push_main_staging (bun install hangs in dind, see #226) skip + # the whole deploy stage and block production. + needs: + - build_and_push_production only: - master deploy_staging: <<: *deploy_definition <<: *env_staging - when: manual - except: - - master + rules: + - when: never + +# --- Green: Preview environments (per-MR) --- + +.environment_template: &env_review + environment: + name: review/$CI_COMMIT_REF_SLUG + url: https://docs-$CI_COMMIT_REF_SLUG.pgai.green + on_stop: stop_review + auto_stop_in: 1 week + variables: + ENV: review + BRANCH_SLUG: $CI_COMMIT_REF_SLUG + +# SSH setup for preview VM deployments (anchored to avoid duplication) +.preview_ssh_setup: + before_script: &preview_ssh_steps + - apk add --no-cache openssh-client + - eval $(ssh-agent -s) + - echo "$PREVIEW_SSH_KEY" | base64 -d | ssh-add - + - mkdir -p ~/.ssh && echo "${PREVIEW_VM_HOST} ${PREVIEW_VM_HOST_KEY}" >> ~/.ssh/known_hosts + +# No `environment:` here — build jobs don't deploy, and setting environment +# creates phantom deployment records that suppress the MR's "Deployed to ..." +# widget (GitLab keys it off the latest deployment for the env). Pull just the +# variables (ENV, BRANCH_SLUG) the build script needs from review.sh, not the +# environment template. Same pattern as platform-all. +build_and_push_review: + stage: prepare_image + image: docker:27 + services: + - docker:27-dind + variables: + DOCKER_BUILDKIT: "1" + ENV: review + BRANCH_SLUG: $CI_COMMIT_REF_SLUG + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + script: + - export BRANCH_SLUG="${CI_COMMIT_REF_SLUG}" + - source "./deploy/configs/review.sh" + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" + - docker pull "${CI_REGISTRY_IMAGE}:review-cache" || true + # --network=host: see build_and_push_definition (#226) + - docker build + --network=host + --cache-from "${CI_REGISTRY_IMAGE}:review-cache" + --tag "${CI_REGISTRY_IMAGE}:review-${BRANCH_SLUG}" + --tag "${CI_REGISTRY_IMAGE}:review-latest" + --tag "${CI_REGISTRY_IMAGE}:review-cache" + --build-arg ARG_URL="${URL}" + --build-arg ARG_BASE_URL="${BASE_URL}" + --build-arg ARG_SIGN_IN_URL="${SIGN_IN_URL}" + --build-arg ARG_BOT_WS_URL="${BOT_WS_URL}" + --build-arg ARG_API_URL_PREFIX="${API_URL_PREFIX}" + --build-arg ARG_UMAMI_WEBSITE_ID="${UMAMI_WEBSITE_ID}" + --build-arg ARG_UMAMI_SCRIPT_URL="${UMAMI_SCRIPT_URL}" + . + - docker push "${CI_REGISTRY_IMAGE}:review-${BRANCH_SLUG}" + - docker push "${CI_REGISTRY_IMAGE}:review-latest" + - docker push "${CI_REGISTRY_IMAGE}:review-cache" + +deploy_review: + stage: deploy + image: alpine:3.20 + <<: *env_review + resource_group: preview-docs-${CI_COMMIT_REF_SLUG} + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + needs: + - build_and_push_review + before_script: *preview_ssh_steps + script: + - export BRANCH_SLUG="${CI_COMMIT_REF_SLUG}" + # Guard against slug collision with permanent staging + - if [ "${BRANCH_SLUG}" = "main" ]; then echo "ERROR - slug 'main' conflicts with permanent staging"; exit 1; fi + - export IMAGE="${CI_REGISTRY_IMAGE}:review-${BRANCH_SLUG}" + # Pipe credential via stdin so it never appears in process arguments + - echo "${CI_REGISTRY_PASSWORD}" | ssh deploy@${PREVIEW_VM_HOST} "docker login -u gitlab-ci-token --password-stdin ${CI_REGISTRY}" + - | + ssh deploy@${PREVIEW_VM_HOST} "\ + docker rm -f docs-${BRANCH_SLUG} 2>/dev/null; \ + docker pull ${IMAGE} && \ + docker run -d \ + --name docs-${BRANCH_SLUG} \ + --restart unless-stopped \ + --memory=512m --cpus=0.5 \ + --network traefik \ + --label traefik.enable=true \ + --label 'traefik.http.routers.docs-${BRANCH_SLUG}.rule=Host(\`docs-${BRANCH_SLUG}.pgai.green\`)' \ + --label traefik.http.routers.docs-${BRANCH_SLUG}.entrypoints=websecure \ + --label traefik.http.routers.docs-${BRANCH_SLUG}.tls=true \ + --label traefik.http.routers.docs-${BRANCH_SLUG}.tls.certresolver=letsencrypt \ + --label traefik.http.services.docs-${BRANCH_SLUG}.loadbalancer.server.port=3000 \ + ${IMAGE}" + # Preview URL is shown via GitLab environment "View app" button on the MR page + - echo "Preview deployed at https://docs-${BRANCH_SLUG}.pgai.green" + +stop_review: + stage: deploy + image: alpine:3.20 + resource_group: preview-docs-${CI_COMMIT_REF_SLUG} + allow_failure: true + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: manual + environment: + name: review/$CI_COMMIT_REF_SLUG + action: stop + variables: + GIT_STRATEGY: none + BRANCH_SLUG: $CI_COMMIT_REF_SLUG + before_script: *preview_ssh_steps + script: + - | + ssh deploy@${PREVIEW_VM_HOST} "\ + docker rm -f docs-${BRANCH_SLUG} 2>/dev/null || true && \ + docker rmi ${CI_REGISTRY_IMAGE}:review-${BRANCH_SLUG} 2>/dev/null || true" + +# --- Green: Permanent staging at docs-main.pgai.green (on master push) --- + +build_and_push_main_staging: + stage: prepare_image + image: docker:27 + services: + - docker:27-dind + environment: + name: main-staging + url: https://docs-main.pgai.green + rules: + - if: $CI_COMMIT_BRANCH == "master" + # Same flaky bun-install hang as build_and_push_production (#226) + timeout: 30m + retry: + max: 2 + when: + - job_execution_timeout + - stuck_or_timeout_failure + - runner_system_failure + script: + - source "./deploy/configs/main-staging.sh" + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" + - docker pull "${CI_REGISTRY_IMAGE}:main-latest" || true + # --network=host: see build_and_push_definition (#226) + - docker build + --network=host + --cache-from "${CI_REGISTRY_IMAGE}:main-latest" + --tag "${CI_REGISTRY_IMAGE}:main-latest" + --build-arg ARG_URL="${URL}" + --build-arg ARG_BASE_URL="${BASE_URL}" + --build-arg ARG_SIGN_IN_URL="${SIGN_IN_URL}" + --build-arg ARG_BOT_WS_URL="${BOT_WS_URL}" + --build-arg ARG_API_URL_PREFIX="${API_URL_PREFIX}" + --build-arg ARG_UMAMI_WEBSITE_ID="${UMAMI_WEBSITE_ID}" + --build-arg ARG_UMAMI_SCRIPT_URL="${UMAMI_SCRIPT_URL}" + . + - docker push "${CI_REGISTRY_IMAGE}:main-latest" + +deploy_main_staging: + stage: deploy + image: alpine:3.20 + environment: + name: main-staging + url: https://docs-main.pgai.green + resource_group: preview-docs-main + rules: + - if: $CI_COMMIT_BRANCH == "master" + needs: + - build_and_push_main_staging + before_script: *preview_ssh_steps + script: + # Pipe credential via stdin so it never appears in process arguments + - echo "${CI_REGISTRY_PASSWORD}" | ssh deploy@${PREVIEW_VM_HOST} "docker login -u gitlab-ci-token --password-stdin ${CI_REGISTRY}" + - | + ssh deploy@${PREVIEW_VM_HOST} "\ + docker rm -f docs-main 2>/dev/null; \ + docker pull ${CI_REGISTRY_IMAGE}:main-latest && \ + docker run -d \ + --name docs-main \ + --restart unless-stopped \ + --memory=512m --cpus=0.5 \ + --network traefik \ + --label traefik.enable=true \ + --label 'traefik.http.routers.docs-main.rule=Host(\`docs-main.pgai.green\`)' \ + --label traefik.http.routers.docs-main.entrypoints=websecure \ + --label traefik.http.routers.docs-main.tls=true \ + --label traefik.http.routers.docs-main.tls.certresolver=letsencrypt \ + --label traefik.http.services.docs-main.loadbalancer.server.port=3000 \ + ${CI_REGISTRY_IMAGE}:main-latest" \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index bec6ee81..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule ".cursor"] - path = .cursor - url = https://gitlab.com/postgres-ai/rules.git diff --git a/CLAUDE.md b/CLAUDE.md index 5692478a..4b475414 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,15 @@ # Claude Code Instructions -Read and follow all rules in `.cursor/rules/` directory. +## GitLab is the source of truth; GitHub is a mirror -@.cursor/rules +- `master` flows **GitLab → GitHub** — GitLab is authoritative; never push `master` to GitHub. +- Dev branches flow **GitHub → GitLab** — branches created/pushed via the GitHub Claude Code integration mirror into GitLab, where MRs are opened, reviewed, and merged. +- Use `glab` for repo operations (not `gh`). PRs live in GitLab as MRs. +- If a branch was started from a stale GitHub mirror of `master`, its `.gitlab-ci.yml` may be out of date (e.g., still deploying previews via the old k8s path). Rebase onto current `master` before debugging CI. + +## Preview environments + +- Per-MR previews deploy automatically on `merge_request_event` to `https://docs-{CI_COMMIT_REF_SLUG}.pgai.green`. +- Infra: Hetzner VM (`PREVIEW_VM_HOST`) running Docker + Traefik with Let's Encrypt DNS-01 via Cloudflare. **Not Kubernetes.** +- Permanent staging from `master`: `https://docs-main.pgai.green`. +- Canonical spec: `postgres-ai/infra` → `green/SPEC.md` (MR !12). Original CI: docs MR !880. diff --git a/Dockerfile b/Dockerfile index 7e0477fb..79993202 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM oven/bun:1.3-debian +# Pin Bun to a patch release. The floating 1.3 tag moved under us and forced +# fresh dependency installs in CI. +FROM oven/bun:1.3.13-debian@sha256:e95356cb8e1de62ad69ab3bd3584ba947013d27650a226804d2fc0af4e17dac2 # Install only libvips runtime (not -dev) so sharp uses prebuilt binaries # This is much faster than compiling from source (~2 min saved) diff --git a/README.md b/README.md index 407c6d15..5be9cf7d 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,12 @@ bun start ``` This command starts a local development server and opens a browser window. Most changes are reflected live without having to restart the server. + +### Preview environments + +Every merge request automatically gets a live preview deployment: + +- **Preview URL**: `https://docs-{branch-slug}.pgai.green` (find it via the "View app" button on the MR page) +- **Permanent staging**: `https://docs-main.pgai.green` (updated on every push to `master`) +- **Auto-cleanup**: Preview environments stop automatically after 1 week, or can be stopped manually via the CI job +- **Access**: Public (open-source repo) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..f091ac22 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security guidelines + +## Reporting vulnerabilities + +If you discover a security vulnerability in this project, please report it to **security@postgres.ai**. All reports are thoroughly investigated by the project maintainers. + +### When should I report a vulnerability? + +- You think you have discovered a potential security vulnerability in this project or related components. +- You are unsure how a vulnerability affects this project. +- You think you discovered a vulnerability in another project that this project depends on. +- You want to report any other security risk that could potentially harm users. + +### When should I NOT report a vulnerability? + +- Your issue is not security related. + +## Security Vulnerability Response + +Each report is acknowledged and analyzed by the project maintainers and the security team within 3 working days. + +The reporter will be kept updated at every stage of the issue's analysis and resolution (triage → fix → release). + +## Public Disclosure Timing + +A public disclosure date is negotiated by the maintainers (security@postgres.ai) and the bug submitter. We prefer to fully disclose the bug as soon as possible once user mitigation is available. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for vendor coordination. The timeframe for disclosure is from immediate (especially if it's already publicly known) to a few weeks. We expect the timeframe between a report and public disclosure to typically be in the order of 7 days. diff --git a/blog/20210714-dle-2-4-test-db-changes-in-ci.md b/blog/20210714-dle-2-4-test-db-changes-in-ci.md index c34cf7d0..758c529e 100644 --- a/blog/20210714-dle-2-4-test-db-changes-in-ci.md +++ b/blog/20210714-dle-2-4-test-db-changes-in-ci.md @@ -68,7 +68,7 @@ Let's open this job and see the details: --- -What happened here? Behind the schenes, a pre-installed DLE server (in AWS) quickly provisioned a thin clone of the Demo database. Next, the DB change was applied in this clone, and DB Migration Checker collected telemetry, and it becomes clear that such change is going to hold an `AccessExclusiveLockё` blocking other queries for a significant time (according to the settings, longer than for 10 seconds). Therefore, this change marked as failed in CI/CD. This is exactly what we need to be protected to avoid deploying such changes to production. +What happened here? Behind the scenes, a pre-installed DLE server (in AWS) quickly provisioned a thin clone of the Demo database. Next, the DB change was applied in this clone, and DB Migration Checker collected telemetry, and it becomes clear that such change is going to hold an `AccessExclusiveLock` blocking other queries for a significant time (according to the settings, longer than for 10 seconds). Therefore, this change is marked as failed in CI/CD. This is exactly what we need to be protected to avoid deploying such changes to production. Of course, if we get the word `CONCURRENTLY` back (as I did in [commit 6059bf4](https://github.com/postgres-ai/green-zone/commit/6059bf4b80a1930bcb531ecd5ae607d623f2a64d)), we'll have our "green light": @@ -100,7 +100,7 @@ Currently, full automation is supported for the DB migrations tracked in GitHub - [Ruby on Rails: Active Record Migrations](https://guides.rubyonrails.org/active_record_migrations.html) (using [`rake db:migrate`](https://ruby.github.io/rake/)) - [Django migrations](https://docs.djangoproject.com/en/3.2/topics/migrations/) -It is also supposed that the automated testing is done using [GitHub Actions](https://github.com/marketplace/actions/database-lab-realistic-db-testing-in-ci). However, the list of supported Git platforms, CI/CD tools, and DB migration version control systems is quite easy to extend – you can do it (please publish an MR if you do!) or open an issue to ask about it in the [DLE & DB Migration Checker issue tracker](https://gitlab.com/postgres-ai/database-lab/-/issues). +It is also supposed that the automated testing is done using [GitHub Actions](https://github.com/marketplace/actions/database-lab-realistic-db-testing-in-ci). However, the list of supported Git platforms, CI/CD tools, and DB migration version control systems is quite easy to extend – you can do it (please publish an MR if you do!) or open an issue to ask about it in the [DLE & DB Migration Checker issue tracker](https://github.com/postgres-ai/database-lab-engine/issues). ## :large_blue_diamond: Terraform module to deploy DLE and its components in AWS @@ -128,7 +128,7 @@ Feedback and contributions are very welcome. Feedback and contributions would be greatly appreciated: - Database Lab Community Slack: https://slack.postgres.ai/ -- DLE & DB Migration Checker issue tracker: https://gitlab.com/postgres-ai/database-lab/-/issues +- DLE & DB Migration Checker issue tracker: https://github.com/postgres-ai/database-lab-engine/issues - Issue tracker of the Terraform module for Database Lab: https://gitlab.com/postgres-ai/database-lab-infrastructure/-/issues diff --git a/blog/20210831-postgresql-subtransactions-considered-harmful.md b/blog/20210831-postgresql-subtransactions-considered-harmful.md index 6ad528d4..23ebe856 100644 --- a/blog/20210831-postgresql-subtransactions-considered-harmful.md +++ b/blog/20210831-postgresql-subtransactions-considered-harmful.md @@ -131,7 +131,7 @@ Besides SAVEPOINTs, there are other ways to create subtransactions: - `BEGIN / EXCEPTION WHEN .. / END` blocks in PL/pgSQL code (the official documentation does not describe it well; explored, for example, in this article: ["PL/PgSQL Exception and XIDs"](https://fluca1978.github.io/2020/02/05/PLPGSQLExceptions.html)) - [`plpy.subtransaction()`](https://www.postgresql.org/docs/current/plpython-subtransaction.html) in PL/Python code -One may assume that many applications that use PL/pgSQL or PL/Python functions use subtransactions. Systems that run API built on [PostgREST](https://postgrest.org/en/latest/search.html?q=plpgsql), [Supabase](https://github.com/supabase/postgres/issues/26), [Hasura](https://hasura.io/docs/latest/graphql/core/databases/postgres/schema/default-values/sql-functions.html#step-2-create-a-trigger) might have PL/pgSQL functions (including trigger functions) that involve `BEGIN / EXCEPTION WHEN .. / END` blocks; in such cases, those systems use subtransactions. +One may assume that many applications that use PL/pgSQL or PL/Python functions use subtransactions. Systems that run API built on [PostgREST](https://postgrest.org/en/latest/search.html?q=plpgsql), [Supabase](https://github.com/supabase/postgres/issues/26), [Hasura](https://hasura.io/docs/latest/schema/postgres/default-values/sql-functions/#step-2-create-a-trigger) might have PL/pgSQL functions (including trigger functions) that involve `BEGIN / EXCEPTION WHEN .. / END` blocks; in such cases, those systems use subtransactions. ## Problem 1: XID growth @@ -187,7 +187,7 @@ In this example, the main transaction had `XID = 1549100656`, and additional XID This example clearly shows two facts that may be not intuitive: 1. XIDs assigned to subtransactions are used in tuple headers, hence participating in MVCC tuple visibility checks – although results of subtransactions are never visible to other transactions until the main transaction is committed (in PostgreSQL, "minimal" isolation level supported is `READ COMMITTED`). -1. Subtransactions contribute to the growth of global XID value (32 bit, requiring special automated maintenance usually done by autovacuum). Therefore it implicitly increases risks associated with XID wraparound: if the mentioned maintenance is lagging for some reason and this issue is not resolved, the system may reach a point when the mechanism of transaction ID wraparound protection puts the cluster to the single-user mode causing long-lasting downtime (see examples of how popular SaaS systems were down because of that: [Sentry](https://blog.sentry.io/2015/07/23/transaction-id-wraparound-in-postgres), [Mailchimp](https://mailchimp.com/what-we-learned-from-the-recent-mandrill-outage/)). One may have, say, 1000 writing transactions per second, but if they all use 10 subtransactions, then XID is incremented by 10000 per second. This might not be expected by users – poor autovacuum needs to run in the "transaction ID wraparound prevention" mode more often than it would be if subtransactions had "local" IDs inside each transaction, not "wasting" global XIDs. +1. Subtransactions contribute to the growth of global XID value (32 bit, requiring special automated maintenance usually done by autovacuum). Therefore it implicitly increases risks associated with XID wraparound: if the mentioned maintenance is lagging for some reason and this issue is not resolved, the system may reach a point when the mechanism of transaction ID wraparound protection puts the cluster to the single-user mode causing long-lasting downtime (see examples of how popular SaaS systems were down because of that: [Sentry](https://blog.sentry.io/transaction-id-wraparound-in-postgres/), [Mailchimp](https://mailchimp.com/what-we-learned-from-the-recent-mandrill-outage/)). One may have, say, 1000 writing transactions per second, but if they all use 10 subtransactions, then XID is incremented by 10000 per second. This might not be expected by users – poor autovacuum needs to run in the "transaction ID wraparound prevention" mode more often than it would be if subtransactions had "local" IDs inside each transaction, not "wasting" global XIDs. Bottom line: there is a trade-off between active use of subtransactions and the XID growth. Understanding this "price" of using subtransactions is essential to avoid issues in heavily-loaded systems. diff --git a/blog/20210914-dle-2-5.md b/blog/20210914-dle-2-5.md index d2cb5427..0cfff3a8 100644 --- a/blog/20210914-dle-2-5.md +++ b/blog/20210914-dle-2-5.md @@ -126,7 +126,7 @@ If you have problems or questions, please contact our communities for help: http Feedback and contributions would be greatly appreciated: - Database Lab Community Slack: https://slack.postgres.ai/ -- DLE & DB Migration Checker issue tracker: https://gitlab.com/postgres-ai/database-lab/-/issues +- DLE & DB Migration Checker issue tracker: https://github.com/postgres-ai/database-lab-engine/issues - Issue tracker of the Terraform module for Database Lab: https://gitlab.com/postgres-ai/database-lab-infrastructure/-/issues diff --git a/blog/20211029-how-partial-and-covering-indexes-affect-update-performance-in-postgresql.md b/blog/20211029-how-partial-and-covering-indexes-affect-update-performance-in-postgresql.md index 493caa4d..22f5e285 100644 --- a/blog/20211029-how-partial-and-covering-indexes-affect-update-performance-in-postgresql.md +++ b/blog/20211029-how-partial-and-covering-indexes-affect-update-performance-in-postgresql.md @@ -90,7 +90,7 @@ include(col2); If our SELECT involves only those columns which values are present in the index, we can rely on Index-only Scans – the may be a much faster alternative to Index scans, because dealing with the heap is not needed anymore. In this scenario, all we need is to tune autovacuum to keep our tables in a "good shape", to have as few interactions with the heap (table data) as possible. Franck Pachot explores it very well in his article ["Boosts Secondary Index Queries with Index Only Scan"](https://dev.to/yugabyte/boosts-secondary-index-queries-with-index-only-scan-5e7j). -I had always an impression that covering indexes are mostly useful in the case of UNIQUE index and unique constraint enforcement – if we have a unique index and want to add a column to it, we cannot just extend the list of columns because it would change the uniqueness constraint. Instead, we add it to `INCLUDE`. However, Franck showed to me another benefit of covering indexes compared to multicolumn ones – changing values of the "extra" column in a covering index is lighter (hence, faster) than the same change in the case when the column is a part of the column list in a multicolumn index (here is [a simple demo from Franck](https://dbfiddle.uk/?rdbms=postgres_13&fiddle=4fc59c7e6a05b14f1dd03b3d2f8859e1)). +I had always an impression that covering indexes are mostly useful in the case of UNIQUE index and unique constraint enforcement – if we have a unique index and want to add a column to it, we cannot just extend the list of columns because it would change the uniqueness constraint. Instead, we add it to `INCLUDE`. However, Franck showed to me another benefit of covering indexes compared to multicolumn ones – changing values of the "extra" column in a covering index is lighter (hence, faster) than the same change in the case when the column is a part of the column list in a multicolumn index (here is [a simple demo from Franck](https://web.archive.org/web/20211201000000/https://dbfiddle.uk/?rdbms=postgres_13&fiddle=4fc59c7e6a05b14f1dd03b3d2f8859e1)). ## Experiments diff --git a/blog/20211221-dle-3-0-0-brings-ui-and-persistent-clones.md b/blog/20211221-dle-3-0-0-brings-ui-and-persistent-clones.md index f0d18941..4148b15d 100644 --- a/blog/20211221-dle-3-0-0-brings-ui-and-persistent-clones.md +++ b/blog/20211221-dle-3-0-0-brings-ui-and-persistent-clones.md @@ -68,7 +68,7 @@ Some users have told us that with UI in hands, it becomes much easier to explain ## Persistent clones: keep working with your cloned Postgres databases during maintenance Another feature added to DLE 3.0 is also something that DLE users have asked a lot about. Before 3.0, any restart of DLE meant the loss of all clones created – so DLE upgrades, VM restarts, and even simple reconfiguration of DLE always needed a maintenance window, interrupting work. -A partial solution to this problem was the ability to [reconfigure DLE without restarts](/docs/how-to-guides/administration/engine-manage#reconfigure-database-lab-engine) introduced in DLE 2.0. However, this wasn't helpful in the cases of DLE upgrades or VM restarts. Now with DLE 3.0, this problem is fully solved: +A partial solution to this problem was the ability to [reconfigure DLE without restarts](/docs/dblab-howtos/administration/engine-manage#reconfigure-dblab-engine) introduced in DLE 2.0. However, this wasn't helpful in the cases of DLE upgrades or VM restarts. Now with DLE 3.0, this problem is fully solved: - If you are running DLE 2.5 or older, plan one more maintenance window – and this will be the last one for upgrades. All subsequent upgrades will keep clones alive. - If you experience a VM failure – not uncommon in cloud environments – once it's back, clones will be re-created, keeping the database state. @@ -93,7 +93,7 @@ We are planning to discuss the aspects of running multiple DLEs on a single mach - [Database Lab documentation](/docs) - [Tutorial for any database](/docs/tutorials/database-lab-tutorial) - [Tutorial for Amazon RDS](/docs/tutorials/database-lab-tutorial-amazon-rds) -- [Interactive tutorial (Katacoda)](https://www.katacoda.com/postgres-ai/scenarios/database-lab-tutorial) +- [Interactive tutorial (Katacoda, archived)](https://web.archive.org/web/2022/https://www.katacoda.com/postgres-ai/scenarios/database-lab-tutorial) :::tip To get help, reach out to the Postgres.ai team and the growing community of Database Lab users and contributors: https://postgres.ai/contact. @@ -102,7 +102,7 @@ To get help, reach out to the Postgres.ai team and the growing community of Data ## Request for feedback and contributions Feedback and contributions would be greatly appreciated: - [Database Lab Community Slack](https://slack.postgres.ai/) -- [DLE & DB Migration Checker issue tracker](https://gitlab.com/postgres-ai/database-lab/-/issues) +- [DLE & DB Migration Checker issue tracker](https://github.com/postgres-ai/database-lab-engine/issues) - [Issue tracker of the Terraform module for Database Lab](https://gitlab.com/postgres-ai/database-lab-infrastructure/-/issues) Like Database Lab? Give us a GitHub star: https://github.com/postgres-ai/database-lab. diff --git a/blog/20220405-database-lab-engine-3-1-released.md b/blog/20220405-database-lab-engine-3-1-released.md index a8cdccec..ef0b4577 100644 --- a/blog/20220405-database-lab-engine-3-1-released.md +++ b/blog/20220405-database-lab-engine-3-1-released.md @@ -37,7 +37,7 @@ Community news: - 🌠 DLE repository on GitHub now has 1,100+ stars; many thanks to everyone who supports the project in any way - 💥 Pieter Vincken has published a blog post describing their experience of using DLE: [\"Testing with production data made easy\"](https://ordina-jworks.github.io/cloud/2022/02/14/postgres-ai.html) - 📈 The Twitter account has reached 400 followers – please follow [@Database_Lab](https://twitter.com/Database_Lab) -- 🎉 DLE now has 15 contributors. More contributions are welcome! See [\"good first issues\"](https://gitlab.com/postgres-ai/database-lab/-/issues?sort=created_date&state=opened&label_name%5B%5D=good+first+issue) +- 🎉 DLE now has 15 contributors. More contributions are welcome! See [\"good first issues\"](https://github.com/postgres-ai/database-lab-engine/issues?q=is%3Aopen+label%3A%22good+first+issue%22) - 🥇 Please consider various ways to contribute – read [CONTRIBUTING.md](https://github.com/postgres-ai/database-lab-engine/blob/master/CONTRIBUTING.md) ## What's new @@ -69,8 +69,8 @@ If you are running DLE 3.0 or older, please read the full [Migration notes](http ## Get started To get started with Database Lab Engine 3.1: -1. Check out the [installation guide](https://postgres.ai/docs/database-lab/getting-started) -2. Join our [community Slack](https://postgres.ai/community) +1. Check out the [installation guide](https://postgres.ai/docs/tutorials/database-lab-tutorial) +2. Join our [community Slack](https://slack.postgres.ai/) 3. Follow [@Database_Lab](https://twitter.com/Database_Lab) on Twitter for updates \ No newline at end of file diff --git a/blog/20220703-dle-in-aws-marketplace.md b/blog/20220703-dle-in-aws-marketplace.md index 352b4223..dae9d061 100644 --- a/blog/20220703-dle-in-aws-marketplace.md +++ b/blog/20220703-dle-in-aws-marketplace.md @@ -39,6 +39,6 @@ Achieving the lowest entry barrier for the new DLE users remains to be one of ou 1. Physical mode (for those who manage Postgres themselves) 2. Many advanced DLE configuration options are not available in AWS Marketplace / CloudFormation interface; however, they can still be adjusted once the instance is created -To start, please read the documentation: ["How to install DLE from the AWS Marketplace"](/docs/how-to-guides/administration/install-dle-from-aws-marketplace). +To start, please read the documentation: ["How to install DLE from the AWS Marketplace"](/docs/dblab-howtos/administration/install-dle-from-aws-marketplace). diff --git a/blog/20221020-database-lab-engine-3-2-released.md b/blog/20221020-database-lab-engine-3-2-released.md index c8e8c581..6b32e26c 100644 --- a/blog/20221020-database-lab-engine-3-2-released.md +++ b/blog/20221020-database-lab-engine-3-2-released.md @@ -31,7 +31,7 @@ In DLE 3.2: Community news: - 🌠 DLE repository on GitHub reached 1,4k stars; many thanks to everyone who supports the project in any way - 📈 The Twitter account has reached 800 followers – please follow [@Database_Lab](https://twitter.com/Database_Lab) -- 🎉 DLE now has 19 contributors. More contributions are welcome! See ["good first issues"](https://gitlab.com/postgres-ai/database-lab/-/issues?sort=created_date&state=opened&label_name%5B%5D=good+first+issue) +- 🎉 DLE now has 19 contributors. More contributions are welcome! See ["good first issues"](https://github.com/postgres-ai/database-lab-engine/issues?q=is%3Aopen+label%3A%22good+first+issue%22) - 🥇 Please consider various ways to contribute – read [CONTRIBUTING.md](https://github.com/postgres-ai/database-lab-engine/blob/master/CONTRIBUTING.md) diff --git a/blog/20230722-10-postgres-tips-for-beginners.md b/blog/20230722-10-postgres-tips-for-beginners.md index f95754a2..b9d49526 100644 --- a/blog/20230722-10-postgres-tips-for-beginners.md +++ b/blog/20230722-10-postgres-tips-for-beginners.md @@ -87,7 +87,7 @@ For more info, check out our podcast episode: As with many systems, in Postgres, the logs are a treasure trove of information, giving you detailed insights into the system's operations and potential issues. By enabling comprehensive logging, you can stay ahead of problems, optimize performance, and ensure the overall health of your database. - **Choosing what to log**: The key to effective logging is knowing what to log without overwhelming your system. By setting parameters like `log_checkpoints = 0`, `log_autovacuum_min_duration = 0`, `log_temp_files = 0`, and `log_lock_waits = on`, you gain visibility into checkpoints, autovacuum operations, temporary file creations, and lock waits. These are some of the most common areas where issues can arise, making them crucial for monitoring. -- **Balance between insight and overhead**: It's important to note that while extensive logging can provide valuable insights, it can also introduce overhead. This is especially true if you set the `log_min_duration_statement` to a very low value. For instance, setting it to `200ms` would log every statement taking longer than that, which can be both informative and potentially performance-degrading. Always be cautious and aware of the ["observer effect"](https://en.wikipedia.org/wiki/Observer_effect_(information_technology)) – the impact of the monitoring process on the system being observed. +- **Balance between insight and overhead**: It's important to note that while extensive logging can provide valuable insights, it can also introduce overhead. This is especially true if you set the `log_min_duration_statement` to a very low value. For instance, setting it to `200ms` would log every statement taking longer than that, which can be both informative and potentially performance-degrading. Always be cautious and aware of the ["observer effect"](https://en.wikipedia.org/wiki/Observer_effect_%28information_technology%29) – the impact of the monitoring process on the system being observed. But without the granular insights from the logs, diagnosing the problem would have been much more challenging. In essence, while logging is an immensely powerful tool in your Postgres arsenal, it requires careful configuration and periodic review to ensure it remains a help, not a hindrance. diff --git a/blog/20230819-dblab-engine-3-4-released.md b/blog/20230819-dblab-engine-3-4-released.md index 374824bb..9db7db45 100644 --- a/blog/20230819-dblab-engine-3-4-released.md +++ b/blog/20230819-dblab-engine-3-4-released.md @@ -114,6 +114,6 @@ Interested in giving back to the project? Here's how you can make an impact: - Give a star to our [GitHub repository](https://github.com/postgres-ai/database-lab-engine) - Help us reach more enthusiasts. Share about Database Lab on Twitter (don't forget to tag [@Database_Lab](https://twitter.com/Database_Lab)) or any other platform you fancy - Multilingual? Consider [translating our README.md](https://gitlab.com/postgres-ai/database-lab/-/blob/master/CONTRIBUTING.md#translation) to share the knowledge in your language -- Are you a developer? Dive in and enhance the Database Lab Engine (DLE) experience; check out our [CONTRIBUTING guidelines](https://gitlab.com/postgres-ai/database-lab/-/blob/master/CONTRIBUTING.md) and explore [the "good first issues" list](https://gitlab.com/postgres-ai/database-lab/-/issues?sort=created_date&state=opened&label_name[]=good+first+issue) on GitLab +- Are you a developer? Dive in and enhance the Database Lab Engine (DLE) experience; check out our [CONTRIBUTING guidelines](https://gitlab.com/postgres-ai/database-lab/-/blob/master/CONTRIBUTING.md) and explore [the "good first issues" list](https://github.com/postgres-ai/database-lab-engine/issues?q=is%3Aopen+label%3A%22good+first+issue%22) on GitHub diff --git a/blog/20240127-postgres-ai-bot.md b/blog/20240127-postgres-ai-bot.md index decbe235..50e826a5 100644 --- a/blog/20240127-postgres-ai-bot.md +++ b/blog/20240127-postgres-ai-bot.md @@ -65,7 +65,7 @@ It has always been important to validate my own ideas, which often led to refine Although experiments are enriching, they usually require significant effort. This challenge is addressed through automation. Our new Postgres.AI bot elevates experimenting with Postgres to a new level, saving considerable time and resources. ### IVO – immediately validatable output -Another principle is one of the first lessons we have learned at Google AI Startup school, from [Zack Akil](https://www.linkedin.com/in/zackakil/): if the system you're building has IVO – immediately validatable output – then you're on a right path. It means that the generative AI (GenAI) output should be possible to quickly and easily validate. This idea is close to [the Shift-left testing concept](https://en.wikipedia.org/wiki/Shift-left_testing), and it is fully aligned with the 2nd consulting principle above. +Another principle is one of the first lessons we have learned at Google AI Startup school, from [Zack Akil](https://www.zackakil.com/): if the system you're building has IVO – immediately validatable output – then you're on a right path. It means that the generative AI (GenAI) output should be possible to quickly and easily validate. This idea is close to [the Shift-left testing concept](https://en.wikipedia.org/wiki/Shift-left_testing), and it is fully aligned with the 2nd consulting principle above. This principle leads to two important conclusions for our case: 1. Users who understand the risks of false positives in LLM answers (called "hallucinations" in the field of GenAI) and are ready to iterate achieve much better results. Therefore, our goal is to explain this to our users and offer them paths to iterate conveniently. @@ -144,9 +144,9 @@ To implement the second consulting principle, Verification, we decided to build To check SQL or planner behavior, when one or two database connections are enough, it is ideal to conduct the experiment in a shared environment using thin clones provided by [Postgres.AI DBLab Engine](https://postgres.ai/docs/database-lab). We have 5 DBLab Engines set up for each Postgres major version currently supported (from 12 to 16), and the bot can request a new clone at any time, which takes ~1s to provide, benefiting from [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write) for data. ### Dedicated-environment experiments -To study Postgres as a whole, to analyze the behavior of all its components such as the buffer pool, lock manager, we need experiments on separate VMs in GCP. Machine provisioning and software installation are managed by [postgresql_cluster](https://github.com/vitabaks/postgresql_cluster/); it installs the needed version of Postgres, [Patroni](https://github.com/zalando/patroni), various extensions, including those used for observability. Experiments are conducted using `pgbench` or `psql`, depending on the needs. Optionally, the bot can request installation of arbitrary software, compile a patched version from source code, tune Postgres, and much more. Although postgresql_cluster can provision a Patroni cluster consisting of multiple nodes, experiments are currently limited to only one node, the primary. An experiment can consist of multiple iterations. Before each iteration, caches (both page cache and Postgres buffer pool) are automatically flushed and cumulative statistic system is reset. After each iteration, artifacts are automatically collected and stored in the Postgres.AI database, for further analysis. +To study Postgres as a whole, to analyze the behavior of all its components such as the buffer pool, lock manager, we need experiments on separate VMs in GCP. Machine provisioning and software installation are managed by [Autobase](https://github.com/vitabaks/autobase) (formerly postgresql_cluster); it installs the needed version of Postgres, [Patroni](https://github.com/zalando/patroni), various extensions, including those used for observability. Experiments are conducted using `pgbench` or `psql`, depending on the needs. Optionally, the bot can request installation of arbitrary software, compile a patched version from source code, tune Postgres, and much more. Although postgresql_cluster can provision a Patroni cluster consisting of multiple nodes, experiments are currently limited to only one node, the primary. An experiment can consist of multiple iterations. Before each iteration, caches (both page cache and Postgres buffer pool) are automatically flushed and cumulative statistic system is reset. After each iteration, artifacts are automatically collected and stored in the PostgresAI database, for further analysis. -All automation is done via GitLab pipelines. For each experimentation step, we collect 70+ artifacts such as information about machine, Postgres version, all settings, content of all `pg_stat_***` views. You can find an example in [this GitLab pipeline's data](https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/jobs/5969127912/artifacts/browse/ARTIFACTS/). +All automation is done via GitLab pipelines. For each experimentation step, we collect 70+ artifacts such as information about machine, Postgres version, all settings, content of all `pg_stat_***` views. You can find an example in [this GitLab pipeline's data](https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/jobs/5969127912) (artifacts have since expired; the job page is still available). ## Google Cloud AI Startup Program Recently, Postgres.AI was accepted to [Google Cloud's AI startup program](https://cloud.google.com/startup/ai?hl=en), receiving $350k in cloud credits, as well as access to other benefits. This is a great help for us, allowing us to move faster in bot development, research, and knowledge base growth. diff --git a/blog/20241003-how-does-planning-time-depend-on-number-of-partitions.md b/blog/20241003-how-does-planning-time-depend-on-number-of-partitions.md index d2b65be5..a052757f 100644 --- a/blog/20241003-how-does-planning-time-depend-on-number-of-partitions.md +++ b/blog/20241003-how-does-planning-time-depend-on-number-of-partitions.md @@ -127,7 +127,7 @@ You can explore these results further and even modify the experiment using the P

gplan; // Use cached plan } ``` -4. Inside [`CheckCachedPlan`](CheckCachedPlan) executor locks are acquired: +4. Inside `CheckCachedPlan` executor locks are acquired: ```c AcquireExecutorLocks(plan->stmt_list, true); ``` diff --git a/blog/20251028-postgres-marathon-2-009.md b/blog/20251028-postgres-marathon-2-009.md index b2c1b38a..2df0ab7a 100644 --- a/blog/20251028-postgres-marathon-2-009.md +++ b/blog/20251028-postgres-marathon-2-009.md @@ -2,7 +2,7 @@ title: "#PostgresMarathon 2-009: Prepared statements and partitioned table lock explosion, part 1" date: 2025-10-28 12:00:00 authors: nik -tags: [Postgres insights, PostgresMarathon, internals, locks, prepared statements, partitioning] +tags: [Postgres insights, PostgresMarathon, internals, locks, lockmanager, prepared statements, partitioning] --- In [#PostgresMarathon 2-008](https://postgres.ai/blog/20251014-postgres-marathon-2-008), we discovered that prepared statements can dramatically reduce `LWLock:LockManager` contention by switching from planner locks (which lock everything) to executor locks (which lock only what's actually used). Starting with execution 7, we saw locks drop from 6 (table + 5 indexes) to just 1 (table only). diff --git a/blog/20251029-postgres-marathon-2-010.md b/blog/20251029-postgres-marathon-2-010.md index e18960ab..85bf39d7 100644 --- a/blog/20251029-postgres-marathon-2-010.md +++ b/blog/20251029-postgres-marathon-2-010.md @@ -2,7 +2,7 @@ title: "#PostgresMarathon 2-010: Prepared statements and partitioned table lock explosion, part 2" date: 2025-10-29 23:59:59 authors: nik -tags: [Postgres insights, PostgresMarathon, internals, locks, prepared statements, partitioning] +tags: [Postgres insights, PostgresMarathon, internals, locks, lockmanager, prepared statements, partitioning] --- In [#PostgresMarathon 2-009](https://postgres.ai/blog/20251028-postgres-marathon-2-009), we focused on Lock Manager's behavior when dealing with prepared statements and partitioned tables. diff --git a/blog/20251030-postgres-marathon-2-011.md b/blog/20251030-postgres-marathon-2-011.md index 805f8d49..1a821f5d 100644 --- a/blog/20251030-postgres-marathon-2-011.md +++ b/blog/20251030-postgres-marathon-2-011.md @@ -2,7 +2,7 @@ title: "#PostgresMarathon 2-011: Prepared statements and partitioned tables — the paradox, part 3" date: 2025-10-30 23:59:59 authors: nik -tags: [Postgres insights, PostgresMarathon, internals, locks, prepared statements, partitioning] +tags: [Postgres insights, PostgresMarathon, internals, locks, lockmanager, prepared statements, partitioning] --- In [#PostgresMarathon 2-009](https://postgres.ai/blog/20251028-postgres-marathon-2-009) and [#PostgresMarathon 2-010](https://postgres.ai/blog/20251029-postgres-marathon-2-010), we explored why execution 6 causes a lock explosion when building a generic plan for partitioned tables — the planner must lock all 52 relations because it can't prune without parameter values. @@ -172,7 +172,7 @@ The obvious question arises: doesn't re-planning on every execution have overhea Is planning overhead worse than locking overhead? With partitioned tables, planning involves partition pruning logic. With 12 partitions, planning is fast. In some cases, planning may be expensive. The O(n) locking overhead typically dominates the planning cost, especially with many partitions. -Amit Langote has been working on this problem, with preparatory work in early 2025. The main optimization to move runtime pruning before `AcquireExecutorLocks()` is still work in progress, discussed in the [pgsql-hackers thread](https://www.postgresql.org/message-id/flat/CA+HiwqGKid5q1KnOg3ih7pJ+tpxUKmWS=KpoiBLoRfMCcHig0g@mail.gmail.com). When this lands, executor lock acquisition will only lock the partitions that survive runtime pruning, so generic plan execution 2+ would acquire only 8 locks (parent + 1 partition + indexes), making generic plans viable again for partitioned tables. +Amit Langote has been working on this problem, with preparatory work in early 2025. The main optimization to move runtime pruning before `AcquireExecutorLocks()` is still work in progress, tracked in the related [CommitFest entry](https://commitfest.postgresql.org/38/3478/). When this lands, executor lock acquisition will only lock the partitions that survive runtime pruning, so generic plan execution 2+ would acquire only 8 locks (parent + 1 partition + indexes), making generic plans viable again for partitioned tables. There's still a catch: the planner's cost estimation still sees generic plans as expensive, so even with the optimization, `auto` mode may keep choosing custom plans. As Amit notes in [his 2022 blog post](https://amitlan.com/2022/05/16/param-query-partition-woes.html), "Till that's also fixed, users will need to use `plan_cache_mode = force_generic_plan` to have plan caching for partitions." This recommendation applies to future Postgres versions with his locking optimization — for current versions without it, the situation is different. diff --git a/blog/20260311-not-exists-vs-exists-partial-index.md b/blog/20260311-not-exists-vs-exists-partial-index.md new file mode 100644 index 00000000..9396f7bf --- /dev/null +++ b/blog/20260311-not-exists-vs-exists-partial-index.md @@ -0,0 +1,348 @@ +--- +title: "How moving one word can speed up a query 10–50x" +date: 2026-03-11 12:00:00 +slug: 20260311-not-exists-vs-exists-partial-index +authors: [maxim, nik] +tags: [Postgres insights, performance, indexing, SQL, partial index] +--- + +import { TldrTabs } from '@site/src/components/TldrTabs' + +One of these queries is 32x faster than the other. Which one, and why? + +

+
+ +**Query 1:** +
+{`select pt.*
+from post_tags pt
+where pt.tag_id = any($1)
+  and exists (
+      select
+      from posts
+      where posts.post_id = pt.post_id
+        and `}not{` deleted
+  );`}
+
+ +
+
+ +**Query 2:** +
+{`select pt.*
+from post_tags pt
+where pt.tag_id = any($1)
+  and `}not{` exists (
+      select
+      from posts
+      where posts.post_id = pt.post_id
+        and deleted
+  );`}
+
+ +
+
+ +These two queries are logically equivalent here because `posts.post_id` is a primary key — the subquery matches at most one row, so `AND NOT deleted` vs. `AND deleted` simply flips a boolean. A foreign key on `post_tags.post_id` guarantees no orphans. + +Read on to find out why the performance difference is so dramatic. This pattern was first spotted by Maxim Boguk on a 400 GiB production table, where the speedup was approximately 50x. + + + + + +## The setup + +A classic soft-delete pattern: a `posts` table with a boolean `deleted` flag, +and a `post_tags` junction table linking posts to tags. +Most posts are active; 2% are soft-deleted. +Two partial indexes: one on active rows (`posts_not_deleted_id_key`, 1,050 MiB), +one on deleted rows (`posts_deleted_id_key`, 22 MiB). + +Q1 hits the large index; Q2 hits the small one. Same result. Dramatically different cost — as the numbers below show. + +## Reproduce it + +:::tip +Full script, raw `EXPLAIN (ANALYZE, BUFFERS)` output, and configs: +https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/issues/74 +::: + +```sql +-- setup (~3-5 min for 50M rows on modern hardware) +create table posts ( + post_id bigint primary key, + deleted boolean not null default false, + content text not null default repeat('x', 200) +) with (autovacuum_enabled = false); -- benchmark only: keeps VM state deterministic; never disable autovacuum in production + +insert into posts (post_id, deleted) +select g, (random() < 0.02) +from generate_series(1, 50000000) g; + +create unique index posts_not_deleted_id_key on posts (post_id) where not deleted; +create unique index posts_deleted_id_key on posts (post_id) where deleted; + +create table post_tags ( + tag_id int not null, + post_id bigint not null, + primary key (tag_id, post_id) +) with (autovacuum_enabled = false); -- benchmark only; never disable autovacuum in production + +insert into post_tags (tag_id, post_id) +select (g % 1000) + 1, ceil(random() * 50000000)::bigint +from generate_series(1, 500000) g +on conflict do nothing; + +-- step 1: fill visibility map cleanly +vacuum analyze posts; +vacuum analyze post_tags; + +-- step 2: controlled dirty — simulate active production table +-- autovacuum_enabled=false ensures VM state stays dirty until benchmark +update posts +set content = repeat('y', 200) +where post_id % 10 = 0; +analyze posts; + +-- verify: must be NULL +select relname, last_autovacuum +from pg_stat_user_tables +where relname = 'posts'; + +-- drop OS page cache + restart PG before each query +-- systemctl stop postgresql && echo 3 > /proc/sys/vm/drop_caches && systemctl start postgresql + +-- run both queries with EXPLAIN (ANALYZE, BUFFERS) +-- compare: Execution Time, Heap Fetches, Buffers: shared read +``` + +## Benchmark results + +Environment: PostgreSQL 18 (beta), 8 dedicated cores, 32 GiB RAM, SSD storage, +`shared_buffers=8GB`, `effective_cache_size=24GB`, `work_mem=64MB`, `random_page_cost=1.1` (appropriate for SSD), `track_io_timing=on`. +Cold OS page cache before each run (`drop_caches` + PG restart). +Dirty visibility map: `vacuum analyze` after load, then 10% of rows updated. + +Full raw `EXPLAIN (ANALYZE, BUFFERS)` output: +https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/issues/74 + +
+ +[![Execution time: NOT EXISTS vs EXISTS](/assets/blog/not-exists-vs-exists-chart-time.png)](/assets/blog/not-exists-vs-exists-chart-time.png) + +[![Buffer reads: NOT EXISTS vs EXISTS](/assets/blog/not-exists-vs-exists-chart-reads.png)](/assets/blog/not-exists-vs-exists-chart-reads.png) + +
+ +``` +tag_ids Q1: EXISTS(NOT deleted) Q2: NOT EXISTS(deleted) speedup Q1 reads Q2 reads ratio +-------------- ------------------------- ------------------------- --------- ----------- ---------- ------ +50 (~25k) 5,350 ms 384 ms 14x 50,900 4,132 12x +250 (~125k) 22,574 ms 717 ms 31x 211,017 7,296 29x +1000 (~500k) 63,750 ms 1,996 ms 32x 588,083 18,759 31x +``` + +*500k post_tags rows across 1,000 tag_ids — ~500 rows per tag_id on average. Parenthetical counts are approximate result set sizes.* + +Both queries use Nested Loop throughout all scales. + +`EXPLAIN (ANALYZE, BUFFERS)` at 250 tag_ids, cold cache: + +
+Q1: EXISTS(NOT deleted) — 22,574 ms — key numbers: Heap Fetches: 132,280 · Buffers read: 209,207 (× 8 KiB = 1,634 MiB) — one heap fetch nearly every lookup, most of them cold reads + +``` +Gather (cost=1077.65..12313.25 rows=4885 width=12) (actual time=130.759..22556.785 rows=122521.00 loops=1) + Workers Planned: 1 + Workers Launched: 1 + Buffers: shared hit=542482 read=211017 dirtied=112214 + I/O Timings: shared read=40822.933 + InitPlan 1 + -> ProjectSet (cost=0.00..1.27 rows=250 width=4) (actual time=0.005..0.036 rows=250.00 loops=1) + -> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.002 rows=1.00 loops=1) + -> Nested Loop (cost=76.38..10823.48 rows=2874 width=12) (actual time=124.611..22276.230 rows=61260.50 loops=2) + Buffers: shared hit=542482 read=211017 dirtied=112214 + I/O Timings: shared read=40822.933 + -> Parallel Bitmap Heap Scan on post_tags pt (cost=75.82..2739.08 rows=2928 width=12) (actual time=121.114..153.806 rows=62500.00 loops=2) + Recheck Cond: (tag_id = ANY ((InitPlan 1).col1)) + Heap Blocks: exact=604 + Buffers: shared hit=20 read=1810 + I/O Timings: shared read=113.180 + -> Bitmap Index Scan on post_tags_pkey (cost=0.00..74.57 rows=4978 width=0) (actual time=124.449..124.449 rows=125000.00 loops=1) + Index Cond: (tag_id = ANY ((InitPlan 1).col1)) + Buffers: shared read=635 + I/O Timings: shared read=111.714 + -> Index Only Scan using posts_not_deleted_id_key on posts (cost=0.56..2.76 rows=1 width=8) (actual time=0.352..0.352 rows=0.98 loops=125000) + Index Cond: (post_id = pt.post_id) + Heap Fetches: 132280 + Buffers: shared hit=542462 read=209207 dirtied=102763 + I/O Timings: shared read=40709.753 +Settings: effective_cache_size = '24GB', work_mem = '64MB', random_page_cost = '1.1' +Planning Time: 18.443 ms +Execution Time: 22573.605 ms +``` + +
+ +
+Q2: NOT EXISTS(deleted) — 717 ms — key numbers: Heap Fetches: 2,680 · Buffers read: 7,296 total — heap fetches only for the 2% that are actually deleted + +``` +Gather (cost=1077.51..10437.12 rows=4885 width=12) (actual time=119.394..711.118 rows=122521.00 loops=1) + Workers Planned: 1 + Workers Launched: 1 + Buffers: shared hit=374645 read=7296 dirtied=2109 + I/O Timings: shared read=976.376 + InitPlan 1 + -> ProjectSet (cost=0.00..1.27 rows=250 width=4) (actual time=0.009..0.034 rows=250.00 loops=1) + -> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1.00 loops=1) + -> Nested Loop Anti Join (cost=76.24..8947.35 rows=2874 width=12) (actual time=113.648..695.187 rows=61260.50 loops=2) + Buffers: shared hit=374645 read=7296 dirtied=2109 + I/O Timings: shared read=976.376 + -> Parallel Bitmap Heap Scan on post_tags pt (cost=75.82..2739.08 rows=2928 width=12) (actual time=111.997..122.751 rows=62500.00 loops=2) + Recheck Cond: (tag_id = ANY ((InitPlan 1).col1)) + Heap Blocks: exact=594 + Buffers: shared hit=20 read=1810 + I/O Timings: shared read=106.570 + -> Bitmap Index Scan on post_tags_pkey (cost=0.00..74.57 rows=4978 width=0) (actual time=115.628..115.628 rows=125000.00 loops=1) + Index Cond: (tag_id = ANY ((InitPlan 1).col1)) + Buffers: shared read=635 + I/O Timings: shared read=105.342 + -> Index Only Scan using posts_deleted_id_key on posts (cost=0.42..2.12 rows=1 width=8) (actual time=0.009..0.009 rows=0.02 loops=125000) + Index Cond: (post_id = pt.post_id) + Heap Fetches: 2680 + Buffers: shared hit=374625 read=5486 dirtied=1994 + I/O Timings: shared read=869.805 +Settings: effective_cache_size = '24GB', work_mem = '64MB', random_page_cost = '1.1' +Planning Time: 16.347 ms +Execution Time: 717.256 ms +``` + +
+ +## Why it's faster + +Two factors stack on top of each other, both pointing the same direction. + +**Factor 1: "not found in index" skips the heap entirely** + +This is the dominant factor. + +When Q2 looks up a post in `posts_deleted_id_key` and **finds nothing** — +it's done. **"Not found" unconditionally skips the heap. No visibility check. +The [visibility map](https://www.postgresql.org/docs/current/storage-vm.html) doesn't matter at all.** +The index scan itself is cheap — a few index page reads — but not the expensive random heap fetches that follow a "found" result. +(See: [Index-Only Scans](https://www.postgresql.org/docs/current/indexes-index-only-scans.html) in the Postgres docs.) + +When Q1 looks up a post in `posts_not_deleted_id_key` and **finds a row** — +**"Found" skips the heap only if the page is all-visible in the [visibility map](https://www.postgresql.org/docs/current/storage-vm.html) +— a condition that breaks on any actively updated table.** +If the page is not all-visible, Postgres must verify visibility with a heap fetch: +a random read into a 13 GiB table. + +Heap fetches measured across all scales: + +``` +tag_ids Q1: EXISTS(NOT deleted) Q2: NOT EXISTS(deleted) +-------------- ------------------------- ----------------------- +50 (~25k) 26,966 heap fetches 554 +250 (~125k) 132,280 heap fetches 2,680 +1000 (~500k) 526,344 heap fetches 10,766 +``` + +Q1 heap-fetches nearly every active post in the result — up to 527k random reads into a table that doesn't fit in memory. + +Q2 heap-fetches only the deleted posts. For 98% of lookups, it finds nothing in the small index and stops there. The small number of heap fetches Q2 does make (for the 2% that are actually deleted) is incidental — even if every deleted-row heap page were dirty, Q2 would still win by roughly 50x. The cardinality difference is the driver, not the visibility map state of the deleted pages. + +One more cost visible in the EXPLAIN output: Q1 shows `dirtied=112,214`. Heap fetches that encounter tuples whose hint bits haven't been set yet write those bits back, marking the page dirty. Those 112k dirty pages must eventually be flushed by bgwriter or written at checkpoint. Q2 dirties 2,109. Under concurrent load, Q1 is not just slow for the session running it — it generates write pressure that affects everyone. + +Why can't the planner avoid this? Both queries use Nested Loop with [Index Only Scan](https://www.postgresql.org/docs/current/indexes-index-only-scans.html) — the right plan shape for a unique index lookup. The key asymmetry: an IOS that **finds nothing** has nothing to verify and stops at the index. An IOS that **finds a tuple** must still confirm visibility — and on any actively updated table, if the page isn't marked all-visible in the [visibility map](https://www.postgresql.org/docs/current/storage-vm.html), that means a heap fetch. Q1 finds a match on 98% of probes; 10% of pages are dirty; heap fetches cascade. The planner picks the right plan shape; it just can't predict how many of those heap fetches will be cold. + +The deeper issue: the planner has cardinality — `pg_class.reltuples`, `pg_stat_user_tables.n_dead_tup`, visibility map fraction — but no model for buffer pool state under concurrent load. "How many of these heap pages will be in shared_buffers at runtime?" is not something statistics can answer. There's no GUC to tune your way out of this. The query rewrite is the fix. + +**Factor 2: index size vs. available cache** + +``` +index covers size +-------------------------- ---------------- -------- +posts_not_deleted_id_key 49M active rows 1,050 MiB +posts_deleted_id_key 1M deleted rows 22 MiB +``` + +`posts_deleted_id_key` is 22 MiB — fits in shared_buffers easily and stays warm. + +`posts_not_deleted_id_key` is 1,050 MiB — nearly 50x larger. On a cold start it must be read from disk page by page during the nested loop. But cold cache is just the extreme case. The same penalty applies whenever the index doesn't fit in cache: memory-constrained servers, buffer pools competed for by other queries, or any index that exceeds the combined OS page cache and shared_buffers available to it. + +The 22 MiB index stays warm under any realistic production load. The 1,050 MiB index gets evicted. When the index pages are cold, the heap fetch pages are almost certainly cold too — the two factors compound. + +To confirm that heap fetches are the dominant factor and not just a cold-cache story: with both indexes and the heap fully warmed via `pg_prewarm`, Q1 drops to 724 ms and Q2 to 161 ms — still a 4.5x gap with zero I/O. Q1 still performed 122k heap fetches; Q2 performed 2,562. Q1 also re-read 55,690 blocks that had been evicted from shared_buffers during the scan itself — the 13 GiB heap can't stay resident when a nested loop scatters 122k accesses across 1.67M pages with only ~1M pages of buffer space. Increasing RAM reduces Factor 2 but cannot fully eliminate Factor 1. The rewrite matters regardless of how much memory you have. + +## The takeaway + +> **"Not found in the index" skips the heap. "Found" requires a heap fetch on any actively updated table.** + +When 98% of your lookups are for the majority case (active posts), you want +those lookups to return "not found" against the small index — not "found" +against the large one. + +> Use `NOT EXISTS` with a partial index on the **minority** (deleted, disabled, +> flagged) — not `EXISTS` with a partial index on the **majority**. + +This applies to any boolean minority pattern: `is_archived`, `is_banned`, `is_draft`, `is_suspended` — anywhere a small fraction of rows carry a flag. diff --git a/blog/20260324-pg18-stats-upgrade-across-versions.md b/blog/20260324-pg18-stats-upgrade-across-versions.md new file mode 100644 index 00000000..c750edcc --- /dev/null +++ b/blog/20260324-pg18-stats-upgrade-across-versions.md @@ -0,0 +1,214 @@ +--- +title: "PG18 preserves planner statistics on upgrade — even from PG14" +date: 2026-03-24 12:00:00 +slug: 20260324-pg18-stats-upgrade-across-versions +authors: [nik] +tags: [PostgreSQL, pg_upgrade, performance, major upgrades] +--- + +import { TldrTabs } from '@site/src/components/TldrTabs' + +Postgres 18 can preserve planner statistics during major version upgrades. But can it work when upgrading from an older version *to* PG18 — which many plan to do soon? Let's test and see. + + + + + +## The problem: upgrading Postgres has always meant losing statistics + +Every major Postgres upgrade — 14 to 15, 15 to 16, 16 to 17 — has reset planner statistics to zero. The moment you switch to the new cluster, the query planner is blind. It falls back to default assumptions, produces terrible plans, and your application hits a performance cliff. This means you need to run ANALYZE, either in a single connection or parallelizing it with `vacuumdb --analyze-only -j$N` (but this has [issues](https://x.com/samokhvalov/status/1849574252888064224) with partitioned tables — it's going to be [much better in Postgres 19](https://commitfest.postgresql.org/patch/5871/), but that's another story). + +This is not a theoretical risk. It happens constantly (it's one of my favorite topics to blame managed platforms for lack of upgrade automation – their tradition is to [leave it on your shoulders](https://x.com/samokhvalov/status/1844593601638260850)): + +- **AWS RDS upgrade, PG14 to 16:** A developer skipped the post-upgrade `ANALYZE`. All looked good during the weekend. Then Monday came, and a frequent query that ran on SeqScan put the server down. P0 incident, painful RCA with trivial mitigation that came too late – `ANALYZE;`. +- **On-prem upgrade, 16 to 17, 70+ databases:** CPU spiked immediately after cutover. All looked great in planning. And the sad part is that they had parallelized stats recalculation using `vacuumdb -j`, and it worked smoothly in the past, but the team had recently partitioned lots of tables... Guess which databases experienced performance issues? Yep, those with partitioned tables. + +Another thing: the speed of `ANALYZE`. As [Greg Sabino Mullane noted back in 2016](https://www.endpointdev.com/blog/2016/12/postgres-statistics-and-pain-of-analyze/), analyze can be painfully slow — slow enough that the default analyze methods sometimes take longer than the entire rest of the upgrade. + +[CYBERTEC](https://www.cybertec-postgresql.com/en/preserve-optimizer-statistics-during-major-upgrades-with-postgresql-v18/) echoed this for large-scale environments: + +> "For large databases with hundreds of tables, some containing billions of rows and dozens of columns, the post-upgrade analyze could take hours." + +And if `default_statistics_target` is adjusted to a higher value (e.g., 1000), the duration of `ANALYZE` increases proportionally. + +Multiple cloud providers and Postgres vendors — including [Azure](https://techcommunity.microsoft.com/blog/azuredbsupport/azure-postgresql-lesson-learned-8-post-upgrade-performance-surprises-the-one-ste/4471807) and [AWS](https://repost.aws/knowledge-center/aurora-postgresql-major-upgrade-cpu) — document this problem in their upgrade guides. It is a well-known source of post-upgrade performance regression. + +## What PG18 changes + +Postgres 18 introduces statistics export and import. From the [release notes](https://www.postgresql.org/about/news/postgresql-18-released-3142/): + +> "Before PostgreSQL 18, these statistics didn't carry over on a major version upgrade, which could cause significant query performance degradations on busy systems until the ANALYZE finished running. PostgreSQL 18 introduces the ability to keep planner statistics through a major version upgrade, which helps an upgraded cluster reach expected performance more quickly after the upgrade." + +But can we use PG18's new `pg_dump --statistics-only` to extract stats from an **older** server — say, PG16 or even PG14? Let's find out. + +## Let's test it + +Start PG16, create some data, then use PG18's `pg_dump` to extract stats from PG16 and import them into an empty PG18 table. A shared volume lets both containers access the dump file: + +```bash +mkdir -p /tmp/pgstats-demo + +docker run -d --name pg16 \ + -p 5416:5432 \ + -v /tmp/pgstats-demo:/shared \ + -e POSTGRES_PASSWORD=postgres \ + postgres:16 + +until docker exec pg16 \ + pg_isready -U postgres 2>/dev/null +do sleep 1; done + +docker exec pg16 psql -U postgres -c " + create table demo as + select g as id, md5(g::text) as val + from generate_series(1, 10000000) g; + analyze demo;" + +# PG18's pg_dump extracts stats from PG16 +docker run --rm \ + --network host \ + -v /tmp/pgstats-demo:/shared \ + postgres:18 \ + bash -c "PGPASSWORD=postgres \ + pg_dump -h localhost -p 5416 \ + -U postgres --statistics-only \ + postgres > /shared/pg16_stats.sql" + +docker rm -f pg16 + +docker run -d --name pg18 \ + -p 5418:5432 \ + -v /tmp/pgstats-demo:/shared \ + -e POSTGRES_PASSWORD=postgres \ + postgres:18 + +until docker exec pg18 \ + pg_isready -U postgres 2>/dev/null +do sleep 1; done + +docker exec pg18 psql -U postgres \ + -c "create table demo (id int, val text)" +docker exec pg18 \ + bash -c "psql -U postgres < /shared/pg16_stats.sql" +``` + +Now verify — the PG18 table has zero rows, but the planner sees 10M: + +```sql +select reltuples, relpages, + (select count(*) from demo) as actual_rows +from pg_class +where relname = 'demo'; +``` + +``` + reltuples | relpages | actual_rows +--------------+----------+------------- + 9.999034e+06 | 83392 | 0 +``` + +`reltuples ≈ 10M` on an empty table. Autovacuum would show `0`. The stats came from PG16, via PG18's `pg_dump`. It works. And this is exactly what `pg_upgrade` does internally — automatically, no manual steps. + +## Why it works + +The reason is simple once you see the architecture: + +**1. pg_upgrade always uses the NEW cluster's pg_dump.** + +In [`src/bin/pg_upgrade/dump.c`](https://github.com/postgres/postgres/blob/REL_18_3/src/bin/pg_upgrade/dump.c#L54-L63), pg_upgrade invokes the target cluster's `pg_dump` binary ([`new_cluster.bindir`](https://github.com/postgres/postgres/blob/REL_18_3/src/bin/pg_upgrade/dump.c#L57)) and points it at the old cluster ([`cluster_conn_opts(&old_cluster)`](https://github.com/postgres/postgres/blob/REL_18_3/src/bin/pg_upgrade/dump.c#L57)). When you upgrade PG16 to PG18, it is PG18's pg_dump — the one with [`--statistics` support](https://github.com/postgres/postgres/blob/REL_18_3/src/bin/pg_upgrade/dump.c#L61) — that connects to your PG16 server. Using a newer `pg_dump` against an older server is [officially supported](https://www.postgresql.org/docs/current/app-pgdump.html#id-1.9.4.13.16): "pg_dump can also dump from PostgreSQL servers older than its own version." + +**2. pg_dump reads from standard catalog views.** + +The statistics export reads from [`pg_class`](https://github.com/postgres/postgres/blob/REL_18_3/src/bin/pg_dump/pg_dump.c#L7097-L7098) (for `relpages`, `reltuples`, `relallvisible`) and [`pg_stats`](https://github.com/postgres/postgres/blob/REL_18_3/src/bin/pg_dump/pg_dump.c#L10983-L10985) (for per-column statistics like `null_frac`, `n_distinct`, `avg_width`, `correlation`, `most_common_vals`, `histogram_bounds`). These are standard system catalog views that have existed in Postgres for decades. + +**3. The restore functions only need to exist on the target.** + +`pg_restore_relation_stats()` and `pg_restore_attribute_stats()` are new PG18 functions. They run on the new cluster during restore. The old cluster never needs to know they exist. + +The implementation (commit [`1fd1bd871012`](https://github.com/postgres/postgres/commit/1fd1bd871012) by Corey Huinker and Jeff Davis, with Nathan Bossart's follow-up [`pg_restore_extended_stats()`](https://github.com/postgres/postgres/commit/0e80f3f88dea)) also handles version differences gracefully: + +- **Pre-v14 clusters**: `reltuples = 0` gets [remapped to `-1`](https://github.com/postgres/postgres/blob/REL_18_3/src/bin/pg_dump/pg_dump.c#L11052-L11060) (the modern "never analyzed" convention) +- **Pre-v17 clusters**: range type statistics are [skipped](https://github.com/postgres/postgres/blob/REL_18_3/src/bin/pg_dump/pg_dump.c#L10984-L10993) — rebuilt on first `ANALYZE` +- **Pre-v18 clusters**: `relallfrozen` (new in PG18) [defaults to `0`](https://github.com/postgres/postgres/blob/REL_18_3/src/bin/pg_dump/pg_dump.c#L7101-L7103) — set by first `vacuum` + +## Caveats + +One important limitation: **extended statistics** created with `create statistics` (multivariate n_distinct, functional dependencies, multivariate MCV lists) are **not** preserved. Single-column statistics from `pg_stats` (including per-column `most_common_vals` and `histogram_bounds`) and relation-level statistics from `pg_class` are all carried over — it's only the multi-column extended stats that require re-analysis. + +The [official pg_upgrade documentation](https://www.postgresql.org/docs/18/pgupgrade.html) recommends a two-step post-upgrade process: + +```bash +vacuumdb --all \ + --analyze-in-stages \ + --missing-stats-only + +vacuumdb --all \ + --analyze-only +``` + +The first command uses `--missing-stats-only` (also new in PG18) to quickly regenerate only the statistics that were not carried over — extended statistics and expression index stats. The second command re-analyzes everything, which is still worthwhile: the new major version may have improved statistics collection algorithms, so fresh stats can produce better plans than the carried-over ones. + +Since the stats dump is metadata-only — no table data is read, just catalog queries — it adds seconds, not minutes, to the `pg_upgrade` process even for large schemas with thousands of tables. + +## Planning your next major upgrade + +PG18's statistics preservation removes one of the biggest risks in major Postgres upgrades, and it works retroactively — you do not need to be on PG18 already to benefit. If you are on PG14, PG15, PG16, or PG17, upgrading to PG18 will preserve your planner statistics. + +At [PostgresAI](https://postgres.ai), we specialize in zero-downtime, zero-data-loss, reversible major Postgres upgrades. Our methodology — battle-tested at GitLab scale (multi-TB databases, 100k+ TPS) — combines physical-to-logical replication with pause/resume capabilities. PG18's statistics preservation complements this approach perfectly: your upgraded cluster starts with optimal query plans from the first second. + +Note: statistics preservation applies to `pg_upgrade`-based workflows. Logical replication upgrades (including our [zero-downtime approach](https://postgres.ai/products/postgres-ai-zdu)) still require a post-upgrade `ANALYZE` on the target. The key difference with our methodology: that `ANALYZE` runs while the old cluster is still serving production traffic, so there is never a moment when the planner is blind. + +[Learn about zero-downtime upgrades](https://postgres.ai/products/postgres-ai-zdu) | [See what customers say about our help](https://postgres.ai/consulting) diff --git a/blog/20260408-dblab-engine-4-1-released.md b/blog/20260408-dblab-engine-4-1-released.md new file mode 100644 index 00000000..f1944619 --- /dev/null +++ b/blog/20260408-dblab-engine-4-1-released.md @@ -0,0 +1,222 @@ +--- +authors: denis +date: 2026-04-08 00:00:00 +publishDate: 2026-04-08 00:00:00 +linktitle: "DBLab 4.1: protection leases, Teleport, Prometheus, and more" +title: "DBLab 4.1: protection leases, Teleport, Prometheus, and more" +weight: 0 +image: /assets/thumbnails/dblab-4.1-blog.png +tags: + - Product announcements + - DBLab Engine + - Database Lab Engine +--- + +import { BlogFooter } from '@site/src/components/BlogFooter' +import { denis } from '@site/src/config/authors' +import { TldrTabs } from '@site/src/components/TldrTabs' + +DBLab 4.0 introduced [instant database branching with O(1) economics](/blog/20250721-dblab-engine-4-0-released). With 4.1, we're making it safe to hand off to a platform team: automatic resource governance, enterprise access control, production-safe data refresh, and native observability. + + + +` on `dblab clone create` sets a lease; protection lifts automatically when it expires", + "Apple Silicon + Colima support — run DBLab locally on M-series Macs, no cloud VM required", + "`databaseRename` in snapshot config strips production names from clones at the source", + "Pre-expiration webhook fires so clone owners can extend if still working", + ] + }} + dbas={{ + title: "Operational and observability wins:", + points: [ + "`/metrics` endpoint in Prometheus format — disk, clones, snapshots, sync (WAL replay lag), instance, branches; no auth, no plugins", + "OpenTelemetry Collector config ships for Grafana Cloud / Datadog / New Relic / any OTLP backend", + "`rds-refresh` runs against a temporary RDS instance from the latest automated snapshot — production xmin horizon stays clean, no vacuum blocking, no wraparound risk", + "Built-in orphan protection cleans up the temporary RDS instance even if the refresh process crashes", + ] + }} + managers={{ + title: "Cost, compliance, and risk reductions:", + points: [ + "Lease defaults and hard caps are server-side, enforced by policy — eliminates the 'who left this clone running?' audit", + "Teleport integration covers SOC2 / regulated environments: role-based access and session recording for every clone (SE/EE)", + "RDS refresh: ~$2.60–$3.85 per nightly refresh on a `db.r7g.2xlarge` temporary instance — negligible vs. the production risk it eliminates", + ] + }} + aiContext={ + "You're answering questions about the blog post 'DBLab 4.1: protection leases, Teleport, Prometheus, and more'. " + + "Summary of what shipped in DBLab 4.1: " + + "(1) Protection leases — clones can now be protected with a timer (e.g. `dblab clone create --protected 120` for 120 minutes). " + + "When the lease expires, protection lifts and idle cleanup reclaims the clone automatically. " + + "Platform teams can set default durations and hard caps server-side. A pre-expiration webhook fires so owners can extend if still working. " + + "Solves the 'engineer protects a clone and forgets to unprotect it' problem that drives over-provisioning. " + + "(2) Database rename — `databaseRename` in snapshot config maps production database names (e.g. `myapp_production` → `myapp`) at clone time, so developers never see production names in dev. " + + "(3) ARM64 + Colima — Apple Silicon support, run DBLab locally on M-series Macs without a cloud VM. " + + "(4) Teleport integration — when a clone is created it auto-registers as a Teleport database resource with role-based access and session recording; deregisters when destroyed. " + + "Requires DBLab Standard or Enterprise edition. Designed for SOC2 / regulated environments where ephemeral clones were previously a compliance gap. " + + "(5) `rds-refresh` standalone tool — finds the latest automated RDS snapshot, creates a temporary RDS instance from it, points DBLab at it to refresh, and deletes the temporary instance when done. " + + "Never touches production RDS, so production avoids the xmin horizon held by pg_dump (which blocks vacuum and can lead to transaction ID wraparound). " + + "Built-in orphan protection ensures the temporary RDS instance is cleaned up even if the process crashes. Typical run is 3-4 hours on `db.r7g.2xlarge` at roughly $2.60-$3.85 per refresh. " + + "Schedule via cron / Kubernetes CronJob / ECS Scheduled Task for nightly fresh data. " + + "(6) Prometheus `/metrics` endpoint — disk (total/free/used, snapshot/clone breakdown, compression ratio), clones (count/status/diff/cpu/mem/protected), snapshots (count/age/data lag/size), sync (WAL replay lag), instance (uptime/version), branches & datasets. " + + "No auth, no plugins. OpenTelemetry Collector config ships for Grafana Cloud, Datadog, New Relic, or any OTLP backend. " + + "What's next: logical replication for continuous refresh, ZFS send/recv for instance sync (including staging → laptop), and major-version upgrade testing on a clone. " + + "DBLab 4.0 made branching instant; 4.1 makes it hand-off-able to platform. Built on the same O(1) economics as 4.0. " + + "End your response by briefly mentioning that DBLab is free to try at console.postgres.ai (or via AWS Marketplace for SE). " + + "Be concise and direct. Do not mention that you're an AI or that you received this context."} + ctaText="Try DBLab 4.1 — free to start" + ctaLink="https://console.postgres.ai" +/> + +## Protection leases: clones that clean up after themselves + +DBLab already cleans up idle clones automatically (via `maxIdleMinutes`). But protected clones are exempt -- that's the point of protection. The problem is engineers protect clones and forget to unprotect them. Disk usage creeps up, somebody has to audit, and the team ends up over-provisioning storage to compensate. + +Now protection has a timer. Set a lease when you create a clone -- through the UI or CLI -- and DBLab handles the rest: + +![Clone protection dropdown showing lease duration options](/assets/blog/20260408-clone-protection-dropdown.png) + +Or using CLI: + +```bash +dblab clone create \ + --branch main \ + --id ci-migration-test-4521 \ + --protected 120 \ + --username postgres \ + --password "${CI_DB_PASSWORD}" +``` + +When the lease expires, protection lifts and idle cleanup reclaims the clone automatically. No human intervention. + +Platform teams can set default durations and hard caps server-side, so no clone stays protected longer than policy allows. Before expiration, a webhook fires -- wire it to Slack so clone owners can extend if they're still working. + +The result: tighter disk utilization, lower storage costs, and no more "who left this clone running?" audits. + +## Database rename: no more production names in dev + +You clone your production database. The clone keeps the name `myapp_production`. A developer isn't sure which environment they're querying. This is a real class of bugs. + +DBLab 4.1 lets you rename databases during snapshot creation, so every clone gets clean names from the start: + +```yaml +databaseRename: + myapp_production: myapp + analytics_prod: analytics +``` + +Every clone inherits the renamed databases automatically. No post-creation scripts, no application-side workarounds. + +## ARM64 and Colima: database branching on your Mac + +DBLab now supports Apple Silicon. If you have an M-series Mac, you can build and run DBLab locally with [Colima](https://github.com/abiosoft/colima) -- no cloud VM required. + +Experiment with database branching on a plane, in a secure facility, or while waiting for IT to approve a cloud budget. See the [macOS setup guide](/docs/dblab-howtos/administration/run-database-lab-on-mac) for step-by-step instructions. + +## Teleport integration: auditable access for every clone + +In regulated environments, every database connection must be logged and access-controlled. Ephemeral clones were historically a gap: they spin up fast, live briefly, and often bypass the controls you'd apply to long-lived databases. + +DBLab 4.1 bridges this with native TeleportTeleport integration. When a clone is created, it automatically registers as a Teleport database resource with role-based access and session recording. When the clone is destroyed, the resource is removed. No more manually setting up SSH tunnels to reach clones -- engineers connect through Teleport like any other database, with every connection logged and access policy-controlled. + +```mermaid +flowchart LR + A[DBLab Engine] -- clone created --> B[Teleport Sidecar] + B -- tctl create --> C[Teleport Auth] + D[Developer] -- tsh db connect --> E[Teleport Proxy] + E --> F[DBLab Clone] + A -- clone destroyed --> B + B -- tctl rm --> C +``` + +:::note +Teleport integration requires Standard Edition (SE) or Enterprise Edition (EE). +::: + +## RDS/Aurora data refresh without touching production + +Running `pg_dump` directly against a production RDS instance is risky: it holds an `xmin` horizon for the duration of the dump, blocking vacuum and accumulating bloat. In severe cases, you risk transaction ID wraparound. + +DBLab 4.1 ships `rds-refresh`, a standalone tool that gets fresh data into DBLab without ever connecting to production. It finds the latest automated snapshot, creates a temporary RDS instance from it, points DBLab at the temporary instance to refresh, and deletes it when done: + +```mermaid +flowchart LR + subgraph rds-refresh + B[RDS Snapshot] --> C["Temporary Instance (auto-deleted)"] + C --> D[DBLab refresh] + end + A[Production RDS] -. automated .-> B +``` + +Built-in orphan protection ensures temporary instances are always cleaned up -- even if the process crashes. + +The temporary instance typically runs for 3-4 hours. At `db.r7g.2xlarge` (8 vCPU, 64 GiB RAM), that's roughly **$2.60-$3.85 per refresh** -- negligible compared to the production risk it eliminates. + +Schedule it with cron, Kubernetes CronJob, or ECS Scheduled Task for nightly refreshes. Your developers and CI pipelines always start the day with fresh data. + +:::note +Parallel dump/restore (`-j`) is currently configured manually. Automatic parallelism tuning is coming in the next release. +::: + +## Prometheus metrics: monitor everything, build nothing + +DBLab now exposes a `/metrics` endpoint in Prometheus format -- ready to scrape with no auth or plugins: + +- **Disk** -- total, free, used, snapshot/clone breakdown, compression ratio +- **Clones** -- count, status, diff size, CPU and memory usage, protected count +- **Snapshots** -- count, age, data lag, physical and logical size +- **Sync** -- WAL replay lag, last replayed timestamp (physical mode) +- **Instance** -- uptime, status, version/edition info +- **Branches and datasets** -- counts and availability + +Add DBLab to your Prometheus config: + +```yaml +scrape_configs: + - job_name: 'dblab' + static_configs: + - targets: ['dblab.internal:2345'] + metrics_path: /metrics +``` + +Set up alerts on the metrics that matter most -- disk pressure, stale snapshots, WAL lag -- so you know before things break. + +Not using Prometheus? DBLab includes an [OpenTelemetry Collector configuration](https://github.com/postgres-ai/database-lab-engine/blob/master/engine/configs/otel-collector.example.yml) that exports to Grafana Cloud, Datadog, New Relic, or any OTLP-compatible backend. + +## What's next + +1. **Logical replication for continuous refresh** -- keep snapshots updated in real time without full `pg_dump` cycles +2. **ZFS send/recv for instance sync** -- replicate data between DBLab instances, including from staging to a developer's laptop +3. **Major version upgrade testing** -- spin up a clone on a newer Postgres version to test upgrades before committing + +## Get started + +Already on 4.0? See the [upgrade guide](https://postgres.ai/docs/dblab-howtos/administration/engine-manage) and [full changelog](https://gitlab.com/postgres-ai/database-lab/-/releases/v4.1.0). + +1. **Try the demo**: [demo.dblab.dev](https://demo.dblab.dev) (token: `demo-token`) +2. **Deploy DBLab SE**: [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-wlmm2satykuec) or [Postgres.ai Console](https://console.postgres.ai) +3. **Install open source**: [How-to](https://postgres.ai/docs/dblab-howtos/administration/install-dle-manually) +4. **macOS setup**: [Run DBLab on Mac](/docs/dblab-howtos/administration/run-database-lab-on-mac) +5. **Enterprise**: Contact [sales@postgres.ai](mailto:sales@postgres.ai) for DBLab EE + +--- + +DBLab 4.0 made database branching instant. DBLab 4.1 makes it something you can hand off to a platform team and trust to run itself. Protection leases keep resources in check. Teleport keeps access auditable. Prometheus keeps you informed. And `rds-refresh` keeps data fresh without risking production. + +All of it on top of the [O(1) economics](/blog/20250721-dblab-engine-4-0-released) that make DBLab unique. + +[Get Started](https://postgres.ai/docs/database-lab) | [GitHub](https://github.com/postgres-ai/database-lab-engine) | [Join our Slack](https://slack.postgres.ai) + + diff --git a/blog/authors.yml b/blog/authors.yml index d9437275..688745fe 100644 --- a/blog/authors.yml +++ b/blog/authors.yml @@ -24,7 +24,7 @@ nik: denis: name: Denis Morozov - title: Lead Engineer + title: Staff Engineer email: denis@postgres.ai image_url: '/assets/author/denis.png' diff --git a/blog/dle-2.0-release.md b/blog/dle-2.0-release.md index 46526db9..f5e79530 100644 --- a/blog/dle-2.0-release.md +++ b/blog/dle-2.0-release.md @@ -26,7 +26,7 @@ The Postgres.ai team is proud to announce version 2.0 of Database Lab Engine (DL This release continues our strategy to automate all routine tasks such as initialization of the PostgreSQL data directory, data transformation, and snapshot management. In DLE 2.0, all these tasks can be flexibly configured in a single configuration file. As a result, building dev&test environments for projects with many databases (such as those that adopted microservice architecture) becomes much easier. -The previous versions of the Database Lab introduced the core technology: thin clone provisioning, based on either [ZFS](https://en.wikipedia.org/wiki/ZFS) (default) or [LVM](). It was already possible to provision full-sized multi-terabyte database clones in just a few seconds and use them for a broad spectrum of tasks such as database schema changes verification, SQL query analysis, or general application testing. +The previous versions of the Database Lab introduced the core technology: thin clone provisioning, based on either [ZFS](https://en.wikipedia.org/wiki/ZFS) (default) or [LVM](). It was already possible to provision full-sized multi-terabyte database clones in just a few seconds and use them for a broad spectrum of tasks such as database schema changes verification, SQL query analysis, or general application testing. Version 2.0 speeds up and empowers the initialization of DLE itself. Instead of using custom scripts for initial and continuous data retrieval, it is now possible to configure everything in a declarative manner to get the data and be up and running. diff --git a/blog/dle-2.1-release.md b/blog/dle-2.1-release.md index a1f3ada1..ba19556e 100644 --- a/blog/dle-2.1-release.md +++ b/blog/dle-2.1-release.md @@ -55,7 +55,7 @@ Please send us any feedback you have – it is hard to overestimate its meaning - Follow us on Twitter: [@Database_Lab](https://twitter.com/Database_Lab) - [Community Slack (English)](https://slack.postgres.ai/), and [Telegram group (Russian)](https://t.me/databaselabru) -- [Database Lab Engine repository](https://gitlab.com/postgres-ai/database-lab), with the [issue tracker](https://gitlab.com/postgres-ai/database-lab/-/issues) +- [Database Lab Engine repository](https://gitlab.com/postgres-ai/database-lab), with the [issue tracker](https://github.com/postgres-ai/database-lab-engine/issues) --- diff --git a/blog/dle-2.2-release.md b/blog/dle-2.2-release.md index b1e24b7f..bb34450e 100644 --- a/blog/dle-2.2-release.md +++ b/blog/dle-2.2-release.md @@ -97,7 +97,7 @@ Your feedback is highly appreciated! - Twitter: [@Database_Lab](https://twitter.com/Database_Lab) - [Community Slack with Joe Bot live demo](https://slack.postgres.ai) (English), and [Telegram group](https://t.me/databaselabru) (Russian) -- [Database Lab Engine repository](https://gitlab.com/postgres-ai/database-lab), with the [issue tracker](https://gitlab.com/postgres-ai/database-lab/-/issues) -- [SQL Optimization Chatbot repository](https://gitlab.com/postgres-ai/joe), with the [issue tracker](https://gitlab.com/postgres-ai/joe/-/issues) +- [Database Lab Engine repository](https://gitlab.com/postgres-ai/database-lab), with the [issue tracker](https://github.com/postgres-ai/database-lab-engine/issues) +- [SQL Optimization Chatbot repository](https://gitlab.com/postgres-ai/joe), with the [issue tracker](https://github.com/postgres-ai/joe/issues) diff --git a/blog/joe-0.5.md b/blog/joe-0.5.md index bcdd3e9f..878428cb 100644 --- a/blog/joe-0.5.md +++ b/blog/joe-0.5.md @@ -59,7 +59,7 @@ Version 0.5.0 adds support of Slack API signed secrets, automated notifications - Tutorial: https://postgres.ai/docs/tutorials/joe-setup - Open-source repository: https://gitlab.com/postgres-ai/joe/ - Changelog: https://gitlab.com/postgres-ai/joe/-/releases -- Bug reports, ideas, and merge requests are welcome: https://gitlab.com/postgres-ai/joe/issues/ +- Bug reports, ideas, and pull/merge requests are welcome: https://github.com/postgres-ai/joe/issues - Community Slack (English): https://slack.postgres.ai/. After joining, the live demo of Joe is available in the #joe-bot-demo channel: https://database-lab-team.slack.com/archives/CTL5BB30R diff --git a/blog/joe-0.6.md b/blog/joe-0.6.md index 4c050266..6c64e949 100644 --- a/blog/joe-0.6.md +++ b/blog/joe-0.6.md @@ -169,7 +169,7 @@ See the full list of Joe's commands in the docs: https://postgres.ai/docs/refere ### Links: - Open-source repository and issue tracker: https://gitlab.com/postgres-ai/joe/ -- Full command list: https://postgres.ai/docs/joe-bot/usage +- Full command list: https://postgres.ai/docs/joe-bot/ - Extended images with PostgreSQL: https://hub.docker.com/r/postgresai/extended-postgres Includes HypoPG, pg_hint_plan, more - Proposals to add more extensions are welcome in the Custom Images repo: https://gitlab.com/postgres-ai/custom-images - Community Slack (English): https://slack.postgres.ai/. After joining, the live demo is available in the #joe-bot-demo channel: https://database-lab-team.slack.com/archives/CTL5BB30R diff --git a/blog/joe-0.7.md b/blog/joe-0.7.md index f847fad8..037f1690 100644 --- a/blog/joe-0.7.md +++ b/blog/joe-0.7.md @@ -44,7 +44,7 @@ Originally, only the Slack version of Joe Bot was publicly available. Today, we The good news is that you can use both of them in parallel. -Thanks to recent refactoring of Joe codebase, and the fact that this codebase is open-source, you can develop and add support for any messenger. Feel free to open issues to discuss the implementation and merge requests to include the code into the main Joe Bot repository. See also: [communication channels issues](https://gitlab.com/postgres-ai/joe/-/issues?label_name%5B%5D=Communication+channel), and discussions in our [Community Slack](https://slack.postgres.ai/). +Thanks to recent refactoring of Joe codebase, and the fact that this codebase is open-source, you can develop and add support for any messenger. Feel free to open issues to discuss the implementation and merge requests to include the code into the main Joe Bot repository. See also: [the Joe Bot issue tracker](https://github.com/postgres-ai/joe/issues), and discussions in our [Community Slack](https://slack.postgres.ai/). Check [Platform Overview](https://postgres.ai/docs/platform) to discover all advantages of using Web UI working on Postgres.ai Platform. diff --git a/deploy/configs/main-staging.sh b/deploy/configs/main-staging.sh new file mode 100644 index 00000000..5443f138 --- /dev/null +++ b/deploy/configs/main-staging.sh @@ -0,0 +1,9 @@ +export REPLICAS=1 +export URL="https://docs-main.pgai.green" +export BASE_URL="/" +export SIGN_IN_URL="https://console-main.pgai.green/signin" +export BOT_WS_URL="wss://console-main.pgai.green/ai-bot-ws/" +export API_URL_PREFIX="https://console-main.pgai.green/api/general" +# No analytics for main staging +export UMAMI_WEBSITE_ID="" +export UMAMI_SCRIPT_URL="" diff --git a/deploy/configs/review.sh b/deploy/configs/review.sh index 25d74ee7..086c7176 100644 --- a/deploy/configs/review.sh +++ b/deploy/configs/review.sh @@ -1 +1,9 @@ export REPLICAS=1 +export URL="https://docs-${BRANCH_SLUG}.pgai.green" +export BASE_URL="/" +export SIGN_IN_URL="https://console-main.pgai.green/signin" +export BOT_WS_URL="wss://console-main.pgai.green/ai-bot-ws/" +export API_URL_PREFIX="https://console-main.pgai.green/api/general" +# No analytics for review environments +export UMAMI_WEBSITE_ID="" +export UMAMI_SCRIPT_URL="" diff --git a/deploy/configs/staging.sh b/deploy/configs/staging.sh index f1d22a6c..94b39b00 100644 --- a/deploy/configs/staging.sh +++ b/deploy/configs/staging.sh @@ -1,4 +1,17 @@ -export REPLICAS=1 +# DEPRECATED — old v2 staging environment (k8s ns `staging`, +# served at v2.postgres.ai docs path) is being sunset. +# CI jobs that source this file (build_and_push_staging, deploy_staging) +# are gated off in .gitlab-ci.yml. +# REPLICAS=0 is defensive: any accidental deploy scales the workload to +# zero instead of bringing the env back up. +# +# New staging is preview-based, deployed from `master`: +# - Static docs site: https://docs-main.pgai.green +# (jobs: build_and_push_main_staging, deploy_main_staging) +# +# Tracking: https://gitlab.com/postgres-ai/infra/-/work_items/50 + +export REPLICAS=0 export URL="https://v2.postgres.ai" export BASE_URL="/" export SIGN_IN_URL="https://console-v2.postgres.ai/signin" diff --git a/docs/all-features.md b/docs/all-features.md index ab6086d4..cf674422 100644 --- a/docs/all-features.md +++ b/docs/all-features.md @@ -72,4 +72,4 @@ keywords: |Standard support|❌|✅|✅| |Premium support (24/7, 1 hour), trainings|❌|✅|✅| -See also: [Development roadmap](/docs/dblab-roadmap). +See also: [Development roadmap](/docs/roadmap). diff --git a/docs/checkup/index.md b/docs/checkup/index.md index df530b47..3d838483 100644 --- a/docs/checkup/index.md +++ b/docs/checkup/index.md @@ -16,15 +16,15 @@ keywords: Postgres Checkup ([postgres-checkup](https://gitlab.com/postgres-ai/postgres-checkup)) is a diagnostics tool for a deep analysis of a Postgres database health. It detects current and potential issues with database performance, scalability, and security. It also produces recommendations on how to resolve or prevent them. postgres-checkup also reveals sneaking up, deeper problems that may hit you in the future. It helps to solve many known database administration problems and common pitfalls. It aims to detect issues at a very early stage and to suggest the best ways to prevent them. -It makes sense to run this tool on a regular basis — weekly, monthly, and quarterly. Additionally, it is recommended using postgres-checkup during major database changes, right before and right after making the change, for the sake of regression control. +It makes sense to run this tool on a regular basis — weekly, monthly, and quarterly. Additionally, it is recommended to use postgres-checkup during major database changes, right before and right after making the change, for the sake of regression control. -Do you know how big was your database 1, 6, 12 months ago? What are the growth trends for each table and index, how fast the bloat grows in database objects after repacking? Depending on how much detail your monitoring system has and what its retention policies are, these questions might be very tricky to answer in the longer term. This is why it is recommended to store the resulting reports as long as possible; it will enable trend analysis for your database. If you are going to use postgres-checkup with Postgres.ai Platform, uploading reports to the Platform's storage will automatically help you achieve this. +Do you know how big your database was 1, 6, 12 months ago? What are the growth trends for each table and index, how fast the bloat grows in database objects after repacking? Depending on how much detail your monitoring system has and what its retention policies are, these questions might be very tricky to answer in the longer term. This is why it is recommended to store the resulting reports as long as possible; it will enable trend analysis for your database. If you are going to use postgres-checkup with Postgres.ai Platform, uploading reports to the Platform's storage will automatically help you achieve this. ## Reports At the moment, postgres-checkup generates 28 reports organized in 7 groups. -* А. General / Infrastructural +* A. General / Infrastructural - A001 System information - A002 Version information - A003 Postgres settings @@ -91,7 +91,7 @@ Usage: Postgres checkup can separately collect, process and upload data to server. You can set the working mode with --mode option. Available values for mode: 'collect', 'process', 'upload', 'run'. -Mode 'run' executes collecting and processing at once, it is a default mode. +Mode 'run' executes collecting and processing at once — it is the default mode. General options: @@ -122,7 +122,7 @@ General options: | -S | --statement-timeout | Statement timeout for all SQL queries (default: 30 seconds) | | -t | --connection-timeout | | -'proccess' options: +'process' options: | Short option | Long option | Description | |---|---|---| @@ -148,7 +148,7 @@ PGPASSWORD=mypasswd ./checkup collect -h [ssh_user]@host_to_connect_via_ssh \ ## Installation and configuration ### Usage postgres-checkup with docker run -The best way to use Postgres Checkup is by using of docker image of the tool. +The best way to use Postgres Checkup is by using the docker image of the tool. The docker container will run, execute all checks and stop itself. The check result can be found inside the `artifacts` folder in current directory (pwd). #### Requirements @@ -163,7 +163,7 @@ grant pg_monitor to pgai_observer; ``` #### Usage -Use the postgres-checkup in this case as follow: +Use the postgres-checkup in this case as follows: ``` export DB_PWD="****" @@ -191,7 +191,7 @@ docker run \ So, firstly you need to fill the configuration file. The next step is running docker with image `registry.gitlab.com/postgres-ai/postgres-checkup:latest`. -We recommend that you name the configuration file like the project name. Сonfiguration file can be filled once and just be used every time you run postgres-checkup docker image. +We recommend that you name the configuration file like the project name. Configuration file can be filled once and just be used every time you run postgres-checkup docker image. Please be careful and run the docker image as one command, like in the example. It means that command `bash run_checkup.sh` should be started inside the docker container. @@ -217,7 +217,7 @@ If you try to check the local instance of Postgres on your host from a container ### Usage postgres-checkup from sources #### Requirements -The second way to use postgres-checkup run it from sources. In this case, requirements follow. +The second way to use postgres-checkup is to run it from sources. In this case, requirements follow. The following OS are supported: diff --git a/docs/data-access/index.md b/docs/data-access/index.md index 7075323a..2a362978 100644 --- a/docs/data-access/index.md +++ b/docs/data-access/index.md @@ -7,7 +7,7 @@ slug: /data-access Better performance for analytics - Run heavy analytical SQL, perform data export without affecting the production servers -- Bring E and T to a replica: a DBLab Engine can be considered as a specialized replica, where data modifications are allowed on a temporary clones – this approach can simplify ETL processes +- Bring E and T to a replica: a DBLab Engine can be considered as a specialized replica, where data modifications are allowed on temporary clones – this approach can simplify ETL processes - Analysts work with thin clones, which are fully independent - When a long-lasting query needs to be executed, an analyst can work independently, not interfering with production workload or a colleague's work - Production servers are not in danger: autovacuum activity is not affected, long-running queries are not causing bloat diff --git a/docs/data-recovery/index.md b/docs/data-recovery/index.md index 164a7974..4d686035 100644 --- a/docs/data-recovery/index.md +++ b/docs/data-recovery/index.md @@ -8,7 +8,7 @@ Recover accidentally deleted data - In the case of manually deleted data that needs to be restored ASAP, it can be done almost instantly (proper snapshot management configuration is required in Database Lab in advance) - Using thin cloning, the point-in-time recovery (PITR) can be performed without long waiting -- Typically, for 1 TiB database, PITR requires more than 1 hour in classic setups; with Database Lab, it takes a few seconds to a few minutes +- Typically, for a 1 TiB database, PITR requires more than 1 hour in classic setups; with Database Lab, it takes a few seconds to a few minutes :::note This page is unfinished. Reach out to the PostgresAI team to learn more. diff --git a/docs/database-lab/db-migration-checker.md b/docs/database-lab/db-migration-checker.md index a84d0e5e..9d34ac1e 100644 --- a/docs/database-lab/db-migration-checker.md +++ b/docs/database-lab/db-migration-checker.md @@ -12,7 +12,7 @@ keywords: --- ## Overview -DB Migration Checker is a DLE's component that enables integration with CI/CD tools to automatically test migrations in CI/CD pipelines. +DB Migration Checker is a DLE component that enables integration with CI/CD tools to automatically test migrations in CI/CD pipelines. ## Key features - **Automated:** DB migration testing in CI/CD pipelines @@ -46,14 +46,13 @@ DB Migration Checker is a DLE's component that enables integration with CI/CD to --volume /var/run/docker.sock:/var/run/docker.sock \ --volume /tmp/ci_checker:/tmp/ci_checker \ --volume ~/.dblab/ci_checker/configs:/home/dblab/configs \ - --env DOCKER_API_VERSION=1.41 \ postgresai/dblab-ci-checker:3.5.0 ``` - [optional] Run the [localtunnel](https://github.com/localtunnel/localtunnel) (or an analog) - use it only for debug purposes to make DB migration instance accessible for a CI pipeline `lt --port 2500` -- Prepare a new repository with your DB migrations(Flyway, Sqitch, Liquibase, etc.) +- Prepare a new repository with your DB migrations (Flyway, Sqitch, Liquibase, etc.) - add secrets: - `DLMC_CI_ENDPOINT` - an endpoint of your Database Lab Migration Checker service. For example, `https://ci-checker.example.com/`, or in case of debug the endpoint given from the localtunnel. - `DLMC_VERIFICATION_TOKEN` - verification token for the Database Lab Migration Checker API diff --git a/docs/database-lab/index.md b/docs/database-lab/index.md index 1d8a2bbb..ad29592a 100644 --- a/docs/database-lab/index.md +++ b/docs/database-lab/index.md @@ -14,6 +14,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; - [DBLab tutorial for Amazon RDS Postgres](/docs/tutorials/database-lab-tutorial-amazon-rds) - [Supported databases](/docs/database-lab/supported-databases) - [DBLab UI](/docs/database-lab/user-interface) +- [Prometheus monitoring](/docs/database-lab/prometheus-monitoring) - [Data masking](/docs/database-lab/masking) - [DB Migration Checker](/docs/database-lab/db-migration-checker) - [Telemetry](/docs/database-lab/telemetry) @@ -24,7 +25,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; - [Client CLI reference (`dblab`)](/docs/reference-guides/dblab-client-cli-reference) - [DBLab Engine configuration reference](/docs/reference-guides/database-lab-engine-configuration-reference) -## User Guides +## User guides - [How to create DBLab clones](/docs/dblab-howtos/cloning/create-clone) - [How to connect to DBLab clones](/docs/dblab-howtos/cloning/connect-clone) - [How to reset DBLab clone](/docs/dblab-howtos/cloning/reset-clone) @@ -57,7 +58,7 @@ DBLab Engine includes the server with API with basic single-user authentication, As an example, cloning of 10 TiB PostgreSQL database takes less than 2 seconds when a single user is using the DBLab Engine instance, and up to 30 seconds when 15 users are working with it at the same time. Moreover, such cloning (called "thin cloning") does not increase budgets: on a single mid-size machine with a single physical copy of the database, it is possible to run dozens of thin clones simultaneously. -Thin cloning is possible thanks to [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write) capabilities provided by either [ZFS filesystem](https://en.wikipedia.org/wiki/ZFS) or [LVM2](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)) (other options such as hardware-based support of thin cloning, can be developed thanks to the modular and open architecture of DBLab Engine). +Thin cloning is possible thanks to [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write) capabilities provided by either [ZFS filesystem](https://en.wikipedia.org/wiki/ZFS) or [LVM2](https://en.wikipedia.org/wiki/Logical_Volume_Manager_%28Linux%29) (other options such as hardware-based support of thin cloning, can be developed thanks to the modular and open architecture of DBLab Engine). Some problems that can be solved by using DBLab: @@ -66,10 +67,10 @@ Some problems that can be solved by using DBLab: - help verify database migrations (DB schema changes) and massive data operations. ### Features -- Works well both on-premise and in clouds. -- Thin provisioning in seconds thanks to copy-on-write (CoW) provided by [ZFS](https://en.wikipedia.org/wiki/ZFS) and a special methodology for preparing PostgreSQL database snapshots. There is also an option to use [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)) instead of ZFS. +- Works well both on-premises and in clouds. +- Thin provisioning in seconds thanks to copy-on-write (CoW) provided by [ZFS](https://en.wikipedia.org/wiki/ZFS) and a special methodology for preparing PostgreSQL database snapshots. There is also an option to use [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_%28Linux%29) instead of ZFS. - Unlimited size of databases (Postgres database size [is unlimited](https://www.postgresql.org/docs/current/limits.html), ZFS volume can be up to 21^28 bytes, or [256 trillion yobibytes](https://en.wikipedia.org/wiki/ZFS)). -- Supports PostgreSQL from version 9.6 up to the most recently released version. +- Supports PostgreSQL from version 10 up to the most recently released version. - Thin cloning takes only a few seconds, regardless of the database size. - REST API. - Client CLI included. @@ -82,7 +83,7 @@ Some problems that can be solved by using DBLab: ### Paid versions: DBLab SE and EE DBLab Engine is also packaged in two paid offerings: -- **DBLab SE (Standard Edition)** – standalone DBLab Engine, installed via [PostgresAI Console](https://postgres.ai/docs/dblab-howtos/administration/install-dle-from-postgres-ai) or [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-wlmm2satykuec), ideal for small to mid-size teams. It extends the free open-source option with commercial support and compatibility with various Postgres flavours such as AWS RDS and RDS Aurora, GCP CloudSQL, Heroku, Supabase, Timescale Cloud, PostGIS. +- **DBLab SE (Standard Edition)** – standalone DBLab Engine, installed via [PostgresAI Console](https://postgres.ai/docs/dblab-howtos/administration/install-dle-from-postgres-ai) or [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-wlmm2satykuec), ideal for small to mid-size teams. It extends the free open-source option with commercial support and compatibility with various Postgres flavors such as AWS RDS and RDS Aurora, GCP CloudSQL, Heroku, Supabase, Timescale Cloud, PostGIS. - **DBLab EE (Enterprise Edition)** – full-fledged solution that includes enterprise features like unified control plane, user management, comprehensive audit capabilities, SSO, holistic query optimization workflows, and more. Version comparison and pricing info – see the [DBLab pricing](https://postgres.ai/pricing) page. @@ -96,5 +97,5 @@ For DBLab EE inquiries, reach out to the PostgresAI team: **:'] + metrics_path: /metrics +``` + +Replace `` and `` with your DBLab instance's host and API port (default: `2345`). + +## Example queries + +### Free disk space percentage + +```promql +100 * dblab_disk_free_bytes / dblab_disk_total_bytes +``` + +### Number of active clones + +```promql +dblab_clones_total +``` + +### Maximum clone age in hours + +```promql +dblab_clone_max_age_seconds / 3600 +``` + +### Data freshness (lag from current time) + +```promql +dblab_snapshot_max_data_lag_seconds / 60 +``` + +### WAL replay lag (physical mode) + +```promql +dblab_sync_wal_lag_seconds +``` + +## Alerting examples + +### Low disk space alert + +```yaml +- alert: DBLabLowDiskSpace + expr: (dblab_disk_free_bytes / dblab_disk_total_bytes) * 100 < 20 + for: 5m + labels: + severity: warning + annotations: + summary: "DBLab low disk space" + description: "DBLab pool {{ $labels.pool }} has less than 20% free disk space" +``` + +### Stale snapshot alert + +```yaml +- alert: DBLabStaleSnapshot + expr: dblab_snapshot_max_data_lag_seconds > 86400 + for: 10m + labels: + severity: warning + annotations: + summary: "DBLab snapshot data is stale" + description: "DBLab snapshot data is more than 24 hours old" +``` + +### High clone count alert + +```yaml +- alert: DBLabHighCloneCount + expr: dblab_clones_total > 50 + for: 5m + labels: + severity: warning + annotations: + summary: "DBLab has many clones" + description: "DBLab has {{ $value }} clones running" +``` + +### High WAL replay lag alert (physical mode) + +```yaml +- alert: DBLabHighWALLag + expr: dblab_sync_wal_lag_seconds > 3600 + for: 10m + labels: + severity: warning + annotations: + summary: "DBLab sync instance has high WAL lag" + description: "DBLab sync instance WAL replay is {{ $value | humanizeDuration }} behind" +``` + +### Metrics collection stale alert + +```yaml +- alert: DBLabMetricsStale + expr: time() - dblab_scrape_success_timestamp > 300 + for: 5m + labels: + severity: warning + annotations: + summary: "DBLab metrics collection is stale" + description: "DBLab metrics have not been updated for more than 5 minutes" +``` + +### Sync instance down alert (physical mode) + +```yaml +- alert: DBLabSyncDown + expr: dblab_sync_status{status="Down"} == 1 or dblab_sync_status{status="Error"} == 1 + for: 5m + labels: + severity: critical + annotations: + summary: "DBLab sync instance is down" + description: "DBLab sync instance is not healthy" +``` + +## OpenTelemetry integration + +DBLab metrics can be exported to OpenTelemetry-compatible backends using the OpenTelemetry Collector. This allows you to send metrics to Grafana Cloud, Datadog, New Relic, and other observability platforms. + +### Quick start + +1. Install the OpenTelemetry Collector: + ```bash + docker pull otel/opentelemetry-collector-contrib:latest + ``` + +2. Copy the example configuration from the DBLab Engine repository: + ```bash + cp engine/configs/otel-collector.example.yml otel-collector.yml + ``` + +3. Edit `otel-collector.yml` to configure your backend: + ```yaml + exporters: + otlp: + endpoint: "your-otlp-endpoint:4317" + headers: + Authorization: "Bearer " + ``` + +4. Run the collector: + ```bash + docker run -v $(pwd)/otel-collector.yml:/etc/otelcol/config.yaml \ + -p 4317:4317 -p 8889:8889 \ + otel/opentelemetry-collector-contrib:latest + ``` + +### Supported backends + +The OTel Collector can export to: +- **Grafana Cloud** — use OTLP exporter with Grafana Cloud endpoint +- **Datadog** — use the datadog exporter +- **New Relic** — use OTLP exporter with New Relic endpoint +- **Prometheus Remote Write** — use prometheusremotewrite exporter +- **AWS CloudWatch** — use awsemf exporter +- **Any OTLP-compatible backend** diff --git a/docs/database-lab/supported-databases.md b/docs/database-lab/supported-databases.md index 936b119b..e0780c65 100644 --- a/docs/database-lab/supported-databases.md +++ b/docs/database-lab/supported-databases.md @@ -4,7 +4,6 @@ title: PostgreSQL versions and extensions supported in DBLab Engine ## PostgreSQL versions Currently, DBLab Engine fully supports the following [PostgreSQL major versions](https://www.postgresql.org/support/versioning/): -- 9.6 (released: 2016-09-29; EOL: 2021-11-11) - 10 (released: 2017-10-05; EOL: 2022-11-10) - 11 (released: 2018-10-18; EOL: 2023-11-09) - 12 (released: 2019-10-03; EOL: 2024-11-14) @@ -15,7 +14,7 @@ Currently, DBLab Engine fully supports the following [PostgreSQL major versions] - 17 (released: 2024-09-26; EOL: 2029-11-08) - 18 (released: 2025-09-25; EOL: 2030-11-13) -By default, version 17 is used: `postgresai/extended-postgres:17`. +By default, version 18 is used in the example configurations: `postgresai/extended-postgres:18`. The images are published in [Docker Hub](https://hub.docker.com/r/postgresai/extended-postgres). @@ -31,7 +30,7 @@ All these extended images include the following extensions: - [bg_mon](https://github.com/CyberDem0n/bg_mon) - [pg_auth_mon](https://github.com/RafiaSabih/pg_auth_mon) - [PoWA](https://github.com/powa-team/powa) -- [pg_hint_plan](https://pghintplan.osdn.jp/pg_hint_plan.html) +- [pg_hint_plan](https://github.com/ossc-db/pg_hint_plan) - [Timescale](https://github.com/timescale/timescaledb) (only for Postgres 12+) - [Citus](https://github.com/citusdata/citus) (only for Postgres 11+) - [HypoPG](https://github.com/HypoPG/hypopg) @@ -45,7 +44,7 @@ All these extended images include the following extensions: - [pgextwlist](https://github.com/dimitri/pgextwlist) - [hll](https://github.com/citusdata/postgresql-hll) - [topn](https://github.com/citusdata/postgresql-topn) (only for Postgres 10+) -- [postgresql_anonymizer](https://github.com/webysther/postgresql_anonymizer) +- [postgresql_anonymizer](https://gitlab.com/dalibo/postgresql_anonymizer) - [pgaudit](https://github.com/pgaudit/pgaudit) - [set_user](https://github.com/pgaudit/set_user) (only for Postgres 10+) diff --git a/docs/database-lab/telemetry.md b/docs/database-lab/telemetry.md index 2a344fb6..dd22e110 100644 --- a/docs/database-lab/telemetry.md +++ b/docs/database-lab/telemetry.md @@ -108,7 +108,7 @@ global: ... ``` -If the change is done when DLE is running, follow the [DLE reconfiguration guide](/docs/dblab-howtos/administration/engine-manage#reconfigure-database-lab-engine) to apply the change without restart. +If the change is done when DLE is running, follow the [DLE reconfiguration guide](/docs/dblab-howtos/administration/engine-manage#reconfigure-dblab-engine) to apply the change without restart. ## Enabling telemetry If telemetry was disabled earlier, you can enable it again changing the flag `global.telemetry.enabled` to `true`. @@ -119,4 +119,4 @@ global: ... ``` -If the change is done when DLE is running, follow the [DLE reconfiguration guide](/docs/dblab-howtos/administration/engine-manage#reconfigure-database-lab-engine) to apply the change without restart. +If the change is done when DLE is running, follow the [DLE reconfiguration guide](/docs/dblab-howtos/administration/engine-manage#reconfigure-dblab-engine) to apply the change without restart. diff --git a/docs/database-lab/timing-estimator.md b/docs/database-lab/timing-estimator.md index 76aa7c19..7131c25c 100644 --- a/docs/database-lab/timing-estimator.md +++ b/docs/database-lab/timing-estimator.md @@ -15,7 +15,7 @@ The Query Estimator is an experimental feature of [DBLab Engine](https://gitlab. This feature has been removed in DLE 3.4.0. Future versions might include a different implementation of this feature. ::: -Database Lab clones are almost exact copies of the production database yet some limitations should be kept in mind. Under the hood, Database Lab clones use copy-on-write technology (currently supported: [ZFS](https://en.wikipedia.org/wiki/ZFS) and [LVM 2](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). This technology allows reducing cloning time and amount of the extra disk space needed for it almost to zero. On the other side, it has different IO performance in comparison to file systems most commonly used on production environments (such as [ext4](https://en.wikipedia.org/wiki/Ext4)). This difference affects the database operations *timing*, it may be noticeably bigger on the clones. Other factors may affect query *timing* too. That is why it is recommended to focus on the plan structure and data volumes (buffer numbers in the case of "physical" provisioning mode, and row numbers in the case of "logical" provisioning mode) when dealing with EXPLAIN plans. Timing numbers in such plans obtained on thing clones should not be directly compared to the corresponding numbers obtained on production. +Database Lab clones are almost exact copies of the production database yet some limitations should be kept in mind. Under the hood, Database Lab clones use copy-on-write technology (currently supported: [ZFS](https://en.wikipedia.org/wiki/ZFS) and [LVM 2](https://en.wikipedia.org/wiki/Logical_Volume_Manager_%28Linux%29)). This technology allows reducing cloning time and amount of the extra disk space needed for it almost to zero. On the other side, it has different IO performance in comparison to file systems most commonly used on production environments (such as [ext4](https://en.wikipedia.org/wiki/Ext4)). This difference affects the database operations *timing* — it may be noticeably bigger on the clones. Other factors may affect query *timing* too. That is why it is recommended to focus on the plan structure and data volumes (buffer numbers in the case of "physical" provisioning mode, and row numbers in the case of "logical" provisioning mode) when dealing with EXPLAIN plans. Timing numbers in such plans obtained on thin clones should not be directly compared to the corresponding numbers obtained on production. :::note When [configured properly](https://postgres.ai/docs/dblab-howtos/administration/postgresql-configuration#postgresql-configuration-in-clones), Database Lab clones execute SQL queries very similarly to production: @@ -26,14 +26,14 @@ When [configured properly](https://postgres.ai/docs/dblab-howtos/administration/ ::: ## Query Estimator -The Query Estimator aims to forecast the timing numbers for the source (production) eliminating the difference related to the disk IO (slower disks, difference filesystem). The estimator is triggered only for queries that are running more than 0.01 seconds (default value, configurable) and it works under the following assumptions: -- CPU and RAM models on prod and those that used on clones are the same or very similar +The Query Estimator aims to forecast the timing numbers for the source (production) eliminating the difference related to the disk IO (slower disks, different filesystem). The estimator is triggered only for queries that are running more than 0.01 seconds (default value, configurable) and it works under the following assumptions: +- CPU and RAM models on prod and those used on clones are the same or very similar - No resources are saturated (CPU, memory, disk IO, network) – neither on clones nor on the source (production) node - The state of caches is very similar (estimation works better when the majority of the data we are working with is cached or not cached at all). - Locking issues do not affect the timing: there is no significant time spent waiting for some lock to be acquired ## How is the estimated timing calculated? -General idea is to use the PostgreSQL waits model, which can be taken from `pg_stat_activity` +The general idea is to use the PostgreSQL waits model, which can be taken from `pg_stat_activity` During SQL query (or batch) execution, `pg_stat_activity` is queried 100 times per second (default value, configurable) to get current wait events for the target PID or PIDs. The frequency can be changed in the configuration file. @@ -61,7 +61,7 @@ LOG: Profiling process 63 with 10ms sampling The estimation methodology implies that the non-IO parts of the overall timing are expected to be very similar on production and clones, focusing on adjusting the values of the IO parts (reads, writes). This makes sense only if the assumptions explained above are true (resources are not saturated, cache states are similar, and so on). The general idea is to provide 2 numbers: minimum and maximum estimated execution time of given SQL query. -- Minimum execution time will be seen if all needed data already in database cache and database doesn't spend time on Read IO. +- Minimum execution time will be seen if all needed data is already in database cache and database doesn't spend time on Read IO. - Maximum execution time will be seen if we need to read all data from disk. In real execution some data (not all) could be in cache and time will be in between minimum and maximum values. @@ -126,4 +126,4 @@ estimator: - `readRatio` - the ratio evaluating the timing difference for operations involving IO Read between Database Lab and production environments (use the value from the previous step) - `writeRatio` - the ratio evaluating the timing difference for operations involving IO Write between Database Lab and production environments (use the value from the previous step) - `profilingInterval` - time interval of samples taken by the profiler, more frequent sampling gives more precise picture of database server waits, but with additional overhead as a trade-off, default is 10ms (100 samples per second) -- `sampleThreshold` - the minimum number of samples sufficient to display the estimation results, more samples gives more precise picture of database server waits during SQL execution, default value is 20 samples; for example, 20 samples with 10ms interval is 0.2 seconds, it means SQL queries with execution time lower than 0.2 seconds cannot be analysed, decreasing this value increases cost of one sample (20 samples equals 5% as cost of one sample) and decrease quality of such analysis +- `sampleThreshold` - the minimum number of samples sufficient to display the estimation results, more samples gives more precise picture of database server waits during SQL execution, default value is 20 samples; for example, 20 samples with 10ms interval is 0.2 seconds, it means SQL queries with execution time lower than 0.2 seconds cannot be analyzed, decreasing this value increases cost of one sample (20 samples equals 5% as cost of one sample) and decreases quality of such analysis diff --git a/docs/dblab-howtos/administration/add-disk-space-to-zfs-pool.md b/docs/dblab-howtos/administration/add-disk-space-to-zfs-pool.md index e3769475..c72a489e 100644 --- a/docs/dblab-howtos/administration/add-disk-space-to-zfs-pool.md +++ b/docs/dblab-howtos/administration/add-disk-space-to-zfs-pool.md @@ -1,17 +1,18 @@ --- title: How to add disk space to a ZFS pool without downtime sidebar_label: Increase ZFS pool size without downtime +description: Expand a ZFS pool used by DBLab Engine online, with no downtime, by resizing the cloud disk and enabling ZFS autoexpand on Linux. --- -For ZFS, performance degradation might occur when more than 80% of disk space is used. Therefore, it is recommended to monitor the used and free disk space and increase the size of the pool for the DBLab Engine (DLE) in a timely fashion. +With ZFS, performance can degrade once more than 80% of disk space is used. Monitor the used and free disk space and increase the pool size for DBLab Engine before it fills up. -ZFS on Linux does support online pool resizing, or "auto-expand". Thanks to this, we can increase the pool size without any downtime. +ZFS on Linux supports online pool resizing, also known as "autoexpand". This lets you increase the pool size without any downtime. :::tip -When we talk about resizing the ZFS pool without downtime, we assume that the DLE is hosted in the cloud, and the cloud provider allows you to change the disk size online without restarting the server. +Resizing the ZFS pool without downtime assumes that DBLab Engine is hosted in the cloud and that the cloud provider lets you change the disk size online, without restarting the server. ::: -To add disk space to a ZFS pool without downtime, follow the below steps. +To add disk space to a ZFS pool without downtime, follow the steps below. ## 1. Check the free space in the pool @@ -92,9 +93,9 @@ nvme1n1 259:2 0 80G 0 disk └─nvme1n1p9 259:4 0 8M 0 part ``` -## 5. Set your zpool with autoextend on +## 5. Enable autoexpand on the ZFS pool -Check if autoexpand is enabled (it defaults to off): +Check whether autoexpand is enabled (it defaults to off): ```bash sudo zpool get autoexpand dblab_pool @@ -116,9 +117,9 @@ NAME PROPERTY VALUE SOURCE dblab_pool autoexpand on local ``` -## 6. Resize ZFS pool +## 6. Resize the ZFS pool -You can expand the pool online by running the following command: +Expand the pool online by running the following command: ```bash sudo zpool online -e dblab_pool nvme1n1 diff --git a/docs/dblab-howtos/administration/ci-observer-postgres-log-masking.md b/docs/dblab-howtos/administration/ci-observer-postgres-log-masking.md index 92ea3e0e..75c4bbe0 100644 --- a/docs/dblab-howtos/administration/ci-observer-postgres-log-masking.md +++ b/docs/dblab-howtos/administration/ci-observer-postgres-log-masking.md @@ -6,15 +6,15 @@ keywords: - "PII, GDPR, sensitive data" - "Masking sensitive data in PostgreSQL logs" - "database log masking" - - "Database Lab CI Observer" + - "DBLab CI Observer" - "automated testing of database migrations" - "automated testing of schema changes" --- -## Configure masking for PostgreSQL log -When Database Lab's CI Observer is used for automated testing of database migrations, it stores PostgreSQL log in DBLab Platform's centralized storage. You can optionally configure masking rules for sensitive data in the PostgreSQL log. Such rules will be continuously applied before sending any PostgreSQL log entries to the Platform's storage. +## Configure masking for the PostgreSQL log +When DBLab's CI Observer is used for automated testing of database migrations, it stores the PostgreSQL log in DBLab Platform's centralized storage. You can optionally configure masking rules for sensitive data in the PostgreSQL log. These rules are applied continuously, before any PostgreSQL log entries are sent to the Platform's storage. -You can define masking rules in the form of regular expressions. To do it, open the DBLab Engine configuration file (usually, `~/.dblab/engine/configs/server.yml`; see config file examples [here](https://gitlab.com/postgres-ai/database-lab/-/tree/v4.0.3/engine/configs)) and define subsection `replacementRules` in the section `replacementRules`. A basic example: +You can define masking rules in the form of regular expressions. To do it, open the DBLab Engine configuration file (usually, `~/.dblab/engine/configs/server.yml`; see config file examples [here](https://gitlab.com/postgres-ai/database-lab/-/tree/v4.1.3/engine/configs)) and define subsection `replacementRules` in the section `observer`. A basic example: ```yaml observer: replacementRules: @@ -33,13 +33,13 @@ ip: *.*.*.*, email: '***@example.com' ``` -You can specify as many masking rules as you need, in key-value format. In example above, two rules are specified: one is for masking all IP addresses, and another to mask all emails. +You can specify as many masking rules as you need, in key-value format. In the example above, two rules are specified: one is for masking all IP addresses, and another to mask all emails. Each masking rule consists of a key and a value: - Keys are regular expressions (see details below) -- Values is replacement templates, where substitution is supported (`$1`, `$2`, etc.) +- Values are replacement templates, where substitution is supported (`$1`, `$2`, etc.) -Use backslash(`\`) to escape special characters: https://yaml.org/spec/1.2/spec.html#id2788097. +Use a backslash (`\`) to escape special characters: https://yaml.org/spec/1.2/spec.html#id2788097. :::caution When many sophisticated regular expressions are used, one might expect a slowdown of Postgres log processing. Try to define as few rules as possible, as simple as possible. @@ -54,7 +54,7 @@ Replacement rules are applied to all log fields of the incoming PostgreSQL CSV l - `query` ## Regular expressions -The syntax of the regular expressions accepted is the same general syntax used by Perl, Python, and other languages. You can find syntax details here: https://github.com/google/re2/wiki/Syntax. +The accepted regular expression syntax is the same general syntax used by Perl, Python, and other languages. You can find syntax details here: https://github.com/google/re2/wiki/Syntax. In a template, a variable is denoted by a substring of the form `$name` or `${name}`, where name is a non-empty sequence of letters, digits, and underscores. A purely numeric name like `$1` refers to a submatch with the corresponding index; other names refer to capturing parentheses named with the `(?P...)` syntax. diff --git a/docs/dblab-howtos/administration/data/custom.md b/docs/dblab-howtos/administration/data/custom.md index 6ccd7857..b1c613ed 100644 --- a/docs/dblab-howtos/administration/data/custom.md +++ b/docs/dblab-howtos/administration/data/custom.md @@ -1,6 +1,7 @@ --- title: "Data source: Custom" sidebar_label: "Custom" +description: Configure DBLab Engine to load data with any Postgres backup tool, such as pg_basebackup, Barman, or pgBackRest, using a custom physical restore command. --- :::info @@ -8,21 +9,21 @@ As the first step, you need to set up a machine for DBLab Engine instance. See t ::: ## Configuration -With this data source type you can use any PostgreSQL backup tool (e.g. pg_basebackup, Barman, pgBackRest) to transfer the data to the DBLab Engine instance. +With this data source type, you can use any Postgres backup tool (such as pg_basebackup, Barman, or pgBackRest) to transfer the data to the DBLab Engine instance. ### Jobs -To set up it you need to use following jobs: +To set it up, you need to use the following jobs: - [physicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalrestore) - [physicalSnapshot](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalsnapshot) ### Options -Copy the example configuration file [`config.example.physical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.physical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml`. For demo purposes we've used `pg_basebackup` tool, but you can use any tool suitable for the task. Check and update the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the Engine +Copy the example configuration file [`config.example.physical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.physical_generic.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml`. This example uses the `pg_basebackup` tool, but you can use any tool suitable for the task. Check and update the following options: +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the Engine - Set connection options in `physicalRestore:options:envs`, based on your tool - Set PostgreSQL commands in `physicalRestore:options:customTool`: - `command`: defines the command to restore data using a custom tool - `restore_command`: defines the PostgreSQL `restore_command` configuration option to refresh data -- Set a proper version in Postgres Docker image tag (change the images itself only if you know what are you doing): +- Set a proper version in Postgres Docker image tag (change the image itself only if you know what you are doing): - `databaseContainer:dockerImage` ## Run DBLab Engine @@ -40,10 +41,9 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` :::info diff --git a/docs/dblab-howtos/administration/data/database-rename.md b/docs/dblab-howtos/administration/data/database-rename.md new file mode 100644 index 00000000..c758505e --- /dev/null +++ b/docs/dblab-howtos/administration/data/database-rename.md @@ -0,0 +1,64 @@ +--- +title: Rename databases during snapshot creation +sidebar_label: Rename databases +description: Use the databaseRename option in DBLab Engine to expose different database names in clones than in the source, for logical and physical snapshots. +--- + +Use the `databaseRename` option when you want DBLab clones to expose different database names than the source system. This is useful when production database names include environment-specific suffixes such as `_prod`, but your development and CI tooling expects names like `_dev` or `_test`. + +:::note +`databaseRename` is supported in DBLab Engine 4.1 and later. +::: + +## When to use it + +Typical cases: + +- production uses `app_prod`, but clones should expose `app_dev` +- you want to normalize database names across environments before tests run +- you need clone database names to match application defaults without changing production + +The rename happens during snapshot preparation on the DBLab side. It does not rename databases in the source system. + +## Logical snapshots + +For dump/restore workflows, add `databaseRename` under `retrieval.spec.logicalSnapshot.options`: + +```yaml +retrieval: + spec: + logicalSnapshot: + options: + databaseRename: + app_prod: app_dev + analytics_prod: analytics_ci +``` + +DBLab will restore data from the original names and then rename those databases before the snapshot is finalized. + +## Physical snapshots + +For physical workflows, add `databaseRename` under `retrieval.spec.physicalSnapshot.options`: + +```yaml +retrieval: + spec: + physicalSnapshot: + options: + databaseRename: + app_prod: app_dev + analytics_prod: analytics_ci +``` + +This runs after `preprocessingScript`, which is useful if your rename logic depends on earlier prep steps. + +## Things to keep in mind + +- Keys are original source database names; values are the names that clones will expose. +- Update connection settings in your application or test suite to use the renamed database names. +- If you already use `preprocessingScript`, remember that `databaseRename` runs after it. + +## Related + +- Reference: [DBLab Engine configuration reference](/docs/reference-guides/database-lab-engine-configuration-reference) +- Guide: [How to create a DBLab clone](/docs/dblab-howtos/cloning/create-clone) diff --git a/docs/dblab-howtos/administration/data/dump.md b/docs/dblab-howtos/administration/data/dump.md index eba65682..e76ce0cc 100644 --- a/docs/dblab-howtos/administration/data/dump.md +++ b/docs/dblab-howtos/administration/data/dump.md @@ -1,6 +1,7 @@ --- title: "Data source: pg_dump" sidebar_label: "pg_dump" +description: Set up DBLab Engine to load data with pg_dump and pg_restore, including multi-database dumps, plain-text and compressed dumps, and direct restore. --- :::info @@ -9,21 +10,21 @@ As the first step, you need to set up a machine for DBLab Engine instance. See t ## Configuration ### Jobs -In order to set up DBLab Engine to automatically get the data from database using [dump/restore](https://www.postgresql.org/docs/current/app-pgdump.html) you need to use following jobs: +To set up DBLab Engine to automatically get the data from a database using [dump/restore](https://www.postgresql.org/docs/current/app-pgdump.html), use the following jobs: - [logicalDump](/docs/reference-guides/database-lab-engine-configuration-reference#job-logicaldump) - [logicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-logicalrestore) - [logicalSnapshot](/docs/reference-guides/database-lab-engine-configuration-reference#job-logicalsnapshot) ### Options -Copy the contents of configuration example [`config.example.logical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.logical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml` and update the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the Engine +Copy the contents of configuration example [`config.example.logical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.logical_generic.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the Engine - Set connection options in `retrieval:spec:logicalDump:options:source:connection`: - `dbname`: database name to connect to - `host`: database server host - `port`: database server port - `username`: database user name - - `password`: database master password (can be also set as `PGPASSWORD` environment variable of the Docker container) -- Set proper version in Postgres Docker image tag (change the images itself only if you know what are you doing): + - `password`: database master password (can also be set as the `PGPASSWORD` environment variable of the Docker container) +- Set a proper version in Postgres Docker image tag (change the image itself only if you know what you are doing): - `databaseContainer:dockerImage` ## Run DBLab Engine @@ -41,13 +42,12 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` -You can use PGPASSWORD env to set the password. +You can use the `PGPASSWORD` environment variable to set the password. :::info Parameter `--publish 127.0.0.1:2345:2345` means that only local connections will be allowed. @@ -73,9 +73,9 @@ sudo rm -rf /var/lib/dblab/dblab_pool/dump ## Ways to prepare a snapshot ### How to dump and restore a database -A basic way of restoring database from the source contains three steps: -- `logicalDump` where DLE dumps from the source into files -- `logicalRestore` where downloaded dumps are restored into the DLE instance +A basic way of restoring a database from the source contains three steps: +- `logicalDump` where DBLab Engine dumps from the source into files +- `logicalRestore` where downloaded dumps are restored into the DBLab Engine instance - `logicalSnapshot` where a snapshot is taken Since dump files are stored in intermediate files, make sure there is enough disk space. @@ -119,9 +119,9 @@ To restore from existing dumps, describe two jobs `logicalRestore` and `logicalS For the `logicalRestore` job provide with the `dumpLocation` option a dump location where files are stored. The `dumpLocation` option must provide a file or directory that contains dump files of various formats: plain, custom, directory. -You can specify both a separate file and directory containing dumps to restore. Please note that DLE will skip dumps of unknown format. +You can specify both a separate file and a directory containing dumps to restore. Note that DBLab Engine skips dumps of an unknown format. -DLE supports consecutive (but single-threaded) restoring multiple dumps. You even can mix dumps of different formats in a `dumpLocation` directory. +DBLab Engine supports consecutive (but single-threaded) restoring multiple dumps. You can even mix dumps of different formats in a `dumpLocation` directory. For example, ```yaml @@ -140,7 +140,7 @@ retrieval: logicalSnapshot: ``` -Since DLE has to explore the `dumpLocation` directory and parse objects (files and directories) inside it, you must mount the directory from `dumpLocation` to the running DBLab Engine container. +Since DBLab Engine has to explore the `dumpLocation` directory and parse objects (files and directories) inside it, you must mount the directory from `dumpLocation` to the running DBLab Engine container. #### Supported plain-text formats and naming DBLab Engine supports restoring from a plain-text file (using the `psql` utility). @@ -151,15 +151,15 @@ There are a number of possible scenarios of how a dump might be created: - via `dumpall` :::info -DLE supports all derived dump files of the described types, such as those generated by `pg_dump_anon` +DBLab Engine supports all derived dump files of the described types, such as those generated by `pg_dump_anon` ::: DBLab Engine automatically detects plain-text dump files and their origin type. -If DLE is working with a dump made by `dumpall` or `pg_dump` with the `--create` option, it doesn't need to know all database names from this file because psql runs queries and restores the dump to a correct database (even if the database already exists, even if the name is `postgres`) +If DBLab Engine is working with a dump made by `dumpall` or `pg_dump` with the `--create` option, it doesn't need to know all database names from this file because psql runs queries and restores the dump to a correct database (even if the database already exists, even if the name is `postgres`) and extracts database names as well. -If a provided dump has been made without the `--create` option (or there are no tables, or the original type cannot be detected because of compression), then DLE will use the filename as a database name, adjust it (if necessary, see a note about a fallback naming below), and will try to create a new database and restore the dump into it. +If a provided dump has been made without the `--create` option (or there are no tables, or the original type cannot be detected because of compression), then DBLab Engine will use the filename as a database name, adjust it (if necessary, see a note about a fallback naming below), and will try to create a new database and restore the dump into it. :::info Fallback naming. All characters in the file name other than words (`[^0-9A-Za-z_]`) will be replaced with an underscore (`_`). @@ -172,9 +172,9 @@ Fallback naming. All characters in the file name other than words (`[^0-9A-Za-z_ So, the `parallelJobs` option is not supported. #### Process compressed dumps -It is a great idea to compress dump files of large databases. DBLab Engine supports restoring compressed plain-text dumps. +Compressing dump files of large databases is recommended. DBLab Engine supports restoring compressed plain-text dumps. -DLE supports several compression options for plain-text dumps: +DBLab Engine supports several compression options for plain-text dumps: - [gzip](https://www.gnu.org/software/gzip/) - [bzip2](https://www.sourceware.org/bzip2/) - no compression @@ -183,13 +183,13 @@ This means that you can specify the `dumpLocation` parameters pointing not only ### Direct restore to DBLab Engine instance -DLE provides a way of restoring from the source on the fly. It's useful to dump and restore a database without saving an intermediate file - so called `immediateRestore` +DBLab Engine provides a way of restoring from the source on the fly. It's useful to dump and restore a database without saving an intermediate file — the so-called `immediateRestore`. The advantage of this method is that no additional disk space is required to restore the database. Keep in mind that unlike a classic "logicalRestore", this option does not support parallelization (specify `parallelJobs: 1` for logicalDump job). It is always a single-threaded (both for dumping on the source, and restoring on the destination end). -To restore directly, you do not need to use "logicalRestore" job. Just define a `logicalDump` job and uncomment the `immediateRestore` section inside it. +To restore directly, you do not need to use the "logicalRestore" job. Just define a `logicalDump` job and uncomment the `immediateRestore` section inside it. For example, ```yaml @@ -219,7 +219,7 @@ retrieval: ``` ## Logical dump and restore of multiple databases -By default, DLE dumps and restores all available databases. To manage list of databases you may option (`databases`). Add this option to `logicalDump` and `logicalRestore` jobs to specify a list of databases that must be copied. +By default, DBLab Engine dumps and restores all available databases. To manage the list of databases you may use an option (`databases`). Add this option to `logicalDump` and `logicalRestore` jobs to specify a list of databases that must be copied. Do not specify this option to take all databases. @@ -237,7 +237,7 @@ To dump multiple databases, add a `databases` section to the existing `logicalDu databaseN: ``` -You could dump database partially by providing the list of tables to be dumped: +You could dump a database partially by providing the list of tables to be dumped: ```yaml spec: logicalDump: @@ -253,7 +253,7 @@ You could dump database partially by providing the list of tables to be dumped: databaseN: ``` -Or do not add `tables` section to dump all tables +Or do not add the `tables` section to dump all tables. ### Logical restore job To restore multiple databases, add a `databases` section to the existing `logicalRestore` job listing the databases to be restored. For instance: @@ -268,7 +268,7 @@ To restore multiple databases, add a `databases` section to the existing `logica database2: databaseN: ``` -Or do not add `databases` section to restore all databases +Or do not add the `databases` section to restore all databases. You could specify a non-default format of dumps (both: files and directories). @@ -280,7 +280,7 @@ Supported [dump formats](https://www.postgresql.org/docs/current/app-pgdump.html By default, the logical restore job uses a `directory` dump format. The DBLab Engine will extract the database name from dump files. -You could restore database partially by providing the list of tables to be restored: +You could restore a database partially by providing the list of tables to be restored: ```yaml spec: logicalRestore: diff --git a/docs/dblab-howtos/administration/data/index.md b/docs/dblab-howtos/administration/data/index.md index 1cdb8f79..536e9dd0 100644 --- a/docs/dblab-howtos/administration/data/index.md +++ b/docs/dblab-howtos/administration/data/index.md @@ -1,23 +1,29 @@ --- -title: Database Lab data sources +title: DBLab data sources sidebar_label: Overview slug: /dblab-howtos/administration/data +description: Overview of DBLab Engine data retrieval methods, comparing logical (dump/restore) and physical data sources for thin cloning of Postgres databases. --- ## Guides ### Logical - [Dump](/docs/dblab-howtos/administration/data/dump) - [RDS](/docs/dblab-howtos/administration/data/rds) +- [RDS/Aurora refresh](/docs/dblab-howtos/administration/data/rds-refresh) — refreshes from a temporary RDS clone instead of production - [Full refresh](/docs/dblab-howtos/administration/logical-full-refresh) +### Shared +- [Rename databases during snapshot creation](/docs/dblab-howtos/administration/data/database-rename) + ### Physical - [WAL-G](/docs/dblab-howtos/administration/data/wal-g) - [pgBackRest](/docs/dblab-howtos/administration/data/pgbackrest) - [pg_basebackup](/docs/dblab-howtos/administration/data/pg_basebackup) +- [rsync](/docs/dblab-howtos/administration/data/rsync) - [Custom](/docs/dblab-howtos/administration/data/custom) ## Overview -To start using cloning, you need to transfer the data to the DBLab Engine machine first. Data retrieval can be also considered as "thick" cloning. Once it's done, users can use "thin" cloning to get independent full-size clones of the database in seconds, for testing and development. Normally, retrieval (thick cloning) is a slow operation (1 TiB/h is a good speed). Optionally, the process of keeping the Database Lab data directory in sync with the source (being continuously updated) can be configured. +To start using cloning, you first need to transfer the data to the DBLab Engine machine. Data retrieval can also be considered "thick" cloning. Once it is done, users can use "thin" cloning to get independent, full-size clones of the database in seconds, for testing and development. Retrieval (thick cloning) is normally a slow operation (1 TiB/h is a good speed). Optionally, you can configure the DBLab Engine data directory to stay in sync with the source as it is continuously updated. :::info Read how you can protect personal data: [Data masking](/docs/database-lab/masking). @@ -25,13 +31,13 @@ Read how you can protect personal data: [Data masking](/docs/database-lab/maskin ## Data retrieval types ### Logical -Use [dump/restore](https://www.postgresql.org/docs/current/app-pgdump.html) processes, obtaining a logical copy of the initial database (as a set of SQL commands), and then loading it to the target Database Lab data directory. This is the only option for managed cloud PostgreSQL services such as Amazon RDS. +Use [dump/restore](https://www.postgresql.org/docs/current/app-pgdump.html) processes to obtain a logical copy of the initial database (as a set of SQL commands), then load it into the target DBLab Engine data directory. This is the only option for managed cloud Postgres services such as Amazon RDS. -Physically, the copy of the database created using this method differs from the original one (data blocks are stored differently). However, row counts are the same, as well as internal database statistics, allowing to do various kinds of development and testing, including running EXPLAIN command to optimize SQL queries. +Physically, the copy of the database created with this method differs from the original (data blocks are stored differently). However, the row counts are the same, as are the internal database statistics, so you can perform various kinds of development and testing, including running the EXPLAIN command to optimize SQL queries. ### Physical Physically copy the data directory from the source (or from the archive if a physical backup tool such as WAL-G, pgBackRest or Barman is used). -This approach allows to have a copy of the original database which is physically identical, including the existing bloat, data blocks location. Not supported for managed cloud Postgres services such as Amazon RDS. +This approach gives you a copy of the original database that is physically identical, including the existing bloat and data block layout. It is not available for managed cloud Postgres services such as Amazon RDS. [↵ Back to Guides](/docs/dblab-howtos/) diff --git a/docs/dblab-howtos/administration/data/pg_basebackup.md b/docs/dblab-howtos/administration/data/pg_basebackup.md index 0d69ab03..5fb7a4fa 100644 --- a/docs/dblab-howtos/administration/data/pg_basebackup.md +++ b/docs/dblab-howtos/administration/data/pg_basebackup.md @@ -1,6 +1,7 @@ --- title: "Data source: pg_basebackup" sidebar_label: "pg_basebackup" +description: Configure DBLab Engine to build a physical data directory from a Postgres source using pg_basebackup as a custom physical restore tool. --- :::info @@ -9,13 +10,13 @@ As the first step, you need to set up a machine for DBLab Engine instance. See t ## Configuration ### Jobs -In order to set up DBLab Engine to automatically get the data from database using [pg_basebackup](https://www.postgresql.org/docs/current/app-pgbasebackup.html) you need to use following jobs: +To set up DBLab Engine to automatically get the data from a database using [pg_basebackup](https://www.postgresql.org/docs/current/app-pgbasebackup.html), use the following jobs: - [physicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalrestore) - [physicalSnapshot](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalsnapshot) ### Options -Copy the contents of configuration example [`config.example.physical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.physical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml` and update the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the Engine +Copy the contents of configuration example [`config.example.physical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.physical_generic.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the Engine - Set connection options in `physicalRestore:options:envs`: - `PGUSER`: database user name - `PGPASSWORD`: database master password @@ -23,7 +24,7 @@ Copy the contents of configuration example [`config.example.physical_generic.yml - Set PostgreSQL commands in `physicalRestore:options:customTool`: - `command`: `pg_basebackup -X stream -D /var/lib/dblab/dblab_pool/data` - `restore_command`: `TBD` -- Set a proper version in Postgres Docker image tag (change the images itself only if you know what are you doing): +- Set a proper version in Postgres Docker image tag (change the image itself only if you know what you are doing): - `databaseContainer:dockerImage` ## Run DBLab Engine @@ -41,10 +42,9 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` :::info diff --git a/docs/dblab-howtos/administration/data/pgbackrest.md b/docs/dblab-howtos/administration/data/pgbackrest.md index 55261108..c6f3d3b6 100644 --- a/docs/dblab-howtos/administration/data/pgbackrest.md +++ b/docs/dblab-howtos/administration/data/pgbackrest.md @@ -1,9 +1,10 @@ --- title: "Data source: pgBackRest" sidebar_label: "pgBackRest" +description: Configure DBLab Engine to restore a Postgres data directory from a pgBackRest repository, including stanza, delta restore, and repository settings. --- -Native support of pgBackRest has been implemented in DLE 3.1. +Native support for pgBackRest was added in DBLab Engine 3.1. :::info As the first step, you need to set up a machine. See the [guide](/docs/dblab-howtos/administration/install-dle-manually). @@ -11,23 +12,23 @@ As the first step, you need to set up a machine. See the [guide](/docs/dblab-how ## Configuration ### Jobs -In order to configure DLE to automatically restore the database using the [pgBackRest](https://github.com/pgbackrest/pgbackrest) archival restoration tool you need to use following jobs: +To configure DBLab Engine to automatically restore the database using the [pgBackRest](https://github.com/pgbackrest/pgbackrest) archival restoration tool, use the following jobs: - [physicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalrestore) - [physicalSnapshot](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalsnapshot) ### Options -Copy the example configuration file [`config.example.physical_pgbackrest.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.physical_pgbackrest.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml` and update the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the Engine +Copy the example configuration file [`config.example.physical_pgbackrest.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.physical_pgbackrest.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the Engine - Set repository options in `physicalRestore:options:envs`: - pgBackRest allows using environment variables instead of command-line options (see [pgBackRest docs](https://pgbackrest.org/command.html#introduction)): Any option may be set in an environment variable using the `PGBACKREST_` prefix and the option name in all caps replacing `-` with `_`, e.g. `pg1-path` becomes `PGBACKREST_PG1_PATH`. Boolean options are represented as they would be in a configuration file, e.g. `PGBACKREST_COMPRESS="n"`, and `reset-*` variants are not allowed. Options that can be specified multiple times in the command line or in a config file can be represented by separating the values with colons, e.g. PGBACKREST_DB_INCLUDE="db1:db2". - Set pgBackRest settings in `physicalRestore:options:pgbackrest`: - `stanza` - defines the stanza name to restore ([pgBackRest docs](https://pgbackrest.org/user-guide.html#quickstart/configure-stanza)) - - `delta` - defines usage the `--delta` option for restore using checksums ([pgBackRest docs](https://pgbackrest.org/user-guide.html#restore/option-delta); this will override `PGBACKREST_DELTA` if it is specified in `physicalRestore:options:envs`)) + - `delta` - defines usage of the `--delta` option for restore using checksums ([pgBackRest docs](https://pgbackrest.org/user-guide.html#restore/option-delta); this will override `PGBACKREST_DELTA` if it is specified in `physicalRestore:options:envs`)) - Set a proper version of Postgres Docker image (change the tag only leaving the image name itself as is, unless you need to use some custom built Postgres image and know what you are doing): - `databaseContainer:dockerImage` -## Run DLE +## Run DBLab Engine :::tip Use Docker volumes to make host secret key and repository public key available to pgBackRest in case of using `--repo-type=posix`. For example: ``` @@ -67,13 +68,12 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` :::info -Parameter `--publish 127.0.0.1:2345:2345` means that only local connections will be allowed to work with DLE API. To allow external connections, consider either using additional software such as NGINX or Envoy or change this parameter. Removing the host/IP part (`--publish 2345:2345`) will make it possible to work using any available network interface. +Parameter `--publish 127.0.0.1:2345:2345` means that only local connections are allowed to work with the DBLab Engine API. To allow external connections, consider either using additional software such as NGINX or Envoy or changing this parameter. Removing the host/IP part (`--publish 2345:2345`) makes it possible to work using any available network interface. See more details in the official [Docker command-line reference](https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose). ::: diff --git a/docs/dblab-howtos/administration/data/rds-refresh.md b/docs/dblab-howtos/administration/data/rds-refresh.md new file mode 100644 index 00000000..68e709f2 --- /dev/null +++ b/docs/dblab-howtos/administration/data/rds-refresh.md @@ -0,0 +1,251 @@ +--- +title: "Data source: RDS/Aurora refresh" +sidebar_label: "RDS/Aurora refresh" +description: Refresh DBLab Engine from Amazon RDS or Aurora by dumping a temporary RDS clone instead of production, avoiding xmin horizon holds, load, and bloat. +--- + +:::note +This component was added in DBLab Engine 4.1. +::: + +The RDS/Aurora refresh tool provides an alternative approach to refreshing DBLab data from Amazon RDS and Aurora databases. Instead of running `pg_dump` directly against production, it dumps from a **temporary RDS clone**, leaving production untouched. + +## Why use this approach? + +Running `pg_dump` directly against a production database can be problematic: +- **Holds the xmin horizon for hours**, leading to bloat accumulation +- **Creates load on production** for the duration of the dump +- **Requires direct network access** to the production database + +The RDS/Aurora refresh tool avoids all of these issues: + +``` +Production --> RDS Snapshot --> RDS Clone --> pg_dump --> DBLab + (automated) (temporary) +``` + +## Quick start + +### 1. Configure + +Create a configuration file: + +```yaml +source: + type: rds # or "aurora-cluster" + identifier: my-prod-db + dbName: postgres + username: postgres + password: ${DB_PASSWORD} + +clone: + instanceClass: db.t3.medium + securityGroups: [sg-xxx] # must allow DBLab inbound + +dblab: + apiEndpoint: https://dblab:2345 + token: ${DBLAB_TOKEN} + +aws: + region: us-east-1 +``` + +### 2. Test + +```bash +docker run --rm \ + -v $PWD/config.yaml:/config.yaml \ + -e DB_PASSWORD -e DBLAB_TOKEN -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY \ + postgresai/rds-refresh -config /config.yaml -dry-run +``` + +### 3. Run + +```bash +docker run --rm \ + -v $PWD/config.yaml:/config.yaml \ + -e DB_PASSWORD -e DBLAB_TOKEN -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY \ + postgresai/rds-refresh -config /config.yaml +``` + +## Configuration reference + +| Field | Required | Description | +|-------|----------|-------------| +| `source.type` | Yes | `rds` or `aurora-cluster` | +| `source.identifier` | Yes | RDS instance or Aurora cluster identifier | +| `source.dbName` | Yes | Database name | +| `source.username` | Yes | Database user | +| `source.password` | Yes | Password (supports `${ENV_VAR}` syntax) | +| `source.snapshotIdentifier` | No | Specific snapshot ID to use; if empty, uses latest automated snapshot | +| `clone.instanceClass` | Yes | RDS clone instance type (e.g., `db.t3.medium`) | +| `clone.securityGroups` | No | Security groups allowing DBLab access | +| `clone.subnetGroup` | No | DB subnet group | +| `clone.parameterGroup` | No | RDS parameter group name | +| `clone.optionGroup` | No | RDS option group name (RDS instances only) | +| `clone.clusterParameterGroup` | No | Cluster parameter group (Aurora only) | +| `clone.publiclyAccessible` | No | Make clone publicly accessible (default: `false`) | +| `clone.enableIAMAuth` | No | Enable IAM database authentication (default: `false`) | +| `clone.storageType` | No | Storage type: `gp2`, `gp3`, `io1`, `io2` | +| `clone.deletionProtection` | No | Enable deletion protection on clone (default: `false`) | +| `clone.port` | No | Custom port for the clone (default: RDS default) | +| `clone.tags` | No | Additional tags (key-value map) for the RDS clone | +| `clone.maxAge` | No | Max age before clone is considered stale (default: `48h`) | +| `dblab.apiEndpoint` | Yes | DBLab API URL | +| `dblab.token` | Yes | DBLab verification token | +| `dblab.insecure` | No | Skip TLS certificate verification (default: `false`) | +| `dblab.pollInterval` | No | Status polling interval (default: `30s`) | +| `dblab.timeout` | No | Max refresh wait (default: `4h`) | +| `aws.region` | Yes | AWS region | + +## IAM policy + +The AWS user or role running the tool needs these permissions: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "rds:DescribeDBSnapshots", + "rds:DescribeDBClusterSnapshots", + "rds:DescribeDBInstances", + "rds:DescribeDBClusters" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "rds:RestoreDBInstanceFromDBSnapshot", + "rds:RestoreDBClusterFromSnapshot", + "rds:CreateDBInstance", + "rds:DeleteDBInstance", + "rds:DeleteDBCluster", + "rds:AddTagsToResource", + "rds:ModifyDBInstance", + "rds:ModifyDBCluster" + ], + "Resource": [ + "arn:aws:rds:*:ACCOUNT:db:dblab-refresh-*", + "arn:aws:rds:*:ACCOUNT:cluster:dblab-refresh-*", + "arn:aws:rds:*:ACCOUNT:snapshot:*", + "arn:aws:rds:*:ACCOUNT:cluster-snapshot:*", + "arn:aws:rds:*:ACCOUNT:subgrp:*", + "arn:aws:rds:*:ACCOUNT:pg:*" + ] + } + ] +} +``` + +Replace `ACCOUNT` with your AWS account ID. + +## DBLab setup + +DBLab must run in **logical mode**. The tool updates config via API (no SSH required). + +```yaml +retrieval: + refresh: + timetable: "" # disable built-in scheduler — rds-refresh handles timing + jobs: [logicalDump, logicalRestore, logicalSnapshot] + spec: + logicalDump: + options: + source: + connection: + host: placeholder # updated by rds-refresh + port: 5432 +``` + +## Scheduling + +### Cron (weekly, Sunday 2 AM) + +```bash +0 2 * * 0 docker run --rm -v /etc/dblab/config.yaml:/config.yaml \ + --env-file /etc/dblab/env postgresai/rds-refresh -config /config.yaml +``` + +### Kubernetes CronJob + +```yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: dblab-refresh +spec: + schedule: "0 2 * * 0" + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + spec: + serviceAccountName: dblab-refresh # IRSA + containers: + - name: refresh + image: postgresai/rds-refresh + args: ["-config", "/config/config.yaml"] + envFrom: + - secretRef: + name: dblab-refresh-secrets + volumeMounts: + - name: config + mountPath: /config + volumes: + - name: config + configMap: + name: dblab-refresh-config + restartPolicy: Never +``` + +## How it works + +1. **Startup cleanup**: check for orphaned clones from previous runs +2. Check DBLab health +3. Find latest RDS snapshot +4. Create RDS clone from RDS snapshot (`dblab-refresh-YYYYMMDD-HHMMSS`) +5. Wait for RDS clone to become available (~15 min) +6. Update DBLab config via API to point to the temporary clone +7. Trigger refresh, wait for completion +8. Delete RDS clone (always, even on error) + +## Orphan protection + +The tool has multiple layers of protection against orphaned RDS clones: + +1. **Defer cleanup**: clone is deleted when process exits normally +2. **Signal handlers**: catches SIGINT, SIGTERM, SIGHUP (SSH disconnect) +3. **State file**: tracks active clone in `./meta/rds-refresh.state` +4. **Tag scan**: finds clones by `ManagedBy=dblab-rds-refresh` tag + +### Manual cleanup + +```bash +# Dry run — see what would be deleted +docker run --rm -v /etc/dblab/config.yaml:/config.yaml \ + --env-file /etc/dblab/env postgresai/rds-refresh \ + cleanup -config /config.yaml -dry-run + +# Delete stale clones older than 24 hours +docker run --rm -v /etc/dblab/config.yaml:/config.yaml \ + --env-file /etc/dblab/env postgresai/rds-refresh \ + cleanup -config /config.yaml -max-age 24h +``` + +## Networking + +The RDS clone must be reachable from DBLab on port 5432. Use the same VPC or VPC peering. + +## Cost + +RDS clone cost is only incurred while running (~2-5 hours): +- `db.t3.medium`: ~$0.35 +- `db.r5.large`: ~$1.20 + +## Related +- [Data source: AWS RDS (direct)](/docs/dblab-howtos/administration/data/rds) +- [Logical full refresh](/docs/dblab-howtos/administration/logical-full-refresh) diff --git a/docs/dblab-howtos/administration/data/rds.md b/docs/dblab-howtos/administration/data/rds.md index 4629e598..67897062 100644 --- a/docs/dblab-howtos/administration/data/rds.md +++ b/docs/dblab-howtos/administration/data/rds.md @@ -1,6 +1,7 @@ --- title: "Data source: AWS RDS" sidebar_label: "AWS RDS" +description: Connect DBLab Engine to an Amazon RDS Postgres database using either master password authentication or IAM database authentication. --- :::info @@ -8,32 +9,32 @@ As the first step, you need to set up a machine for DBLab Engine instance. See t ::: :::tip See also -To get started using DBLab Engine for Amazon RDS databses, see the [Database Lab tutorial for Amazon RDS](/docs/tutorials/database-lab-tutorial-amazon-rds). +To get started using DBLab Engine for Amazon RDS databases, see the [Database Lab tutorial for Amazon RDS](/docs/tutorials/database-lab-tutorial-amazon-rds). ::: -We have two options to connect to the RDS database, you need to consider the **Database authentication** method that is assigned to your RDS database. +There are two options to connect to the RDS database. The right choice depends on the **Database authentication** method assigned to your RDS database. Options: -- Using **password authentication (master password)**. This option can be used for all **Database authentication** method enabled for your database and requires to set the master password of the database in the DBLab Engine configuration file -- **IAM database authentication**. This option can be used only with **Password and IAM database authentication**, it requires AWS user credentials and does not require the master password, use this option for granular control of the access to your database +- Using **password authentication (master password)**. This option can be used for all **Database authentication** methods enabled for your database and requires setting the master password of the database in the DBLab Engine configuration file +- **IAM database authentication**. This option can be used only with **Password and IAM database authentication** — it requires AWS user credentials and does not require the master password. Use this option for granular control of the access to your database If you want to use **IAM database authentication**, read how to enable it [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Enabling.html). ## Option 1: Password authentication :::tip -You need to know the **master password**. If you lost the password it can be reset. Read how to reset it [here](https://aws.amazon.com/premiumsupport/knowledge-center/reset-master-user-password-rds/). +You need to know the **master password**. If you have lost the password, you can reset it. Read how to reset it [here](https://aws.amazon.com/premiumsupport/knowledge-center/reset-master-user-password-rds/). ::: -Copy the contents of configuration example [`config.example.logical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.logical_generic.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the Engine +Copy the contents of configuration example [`config.example.logical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.logical_generic.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the Engine - Set connection options in `retrieval:spec:logicalDump:options:source:connection`: - `dbname`: database name to connect to - `host`: database server host - `port`: database server port - `username`: database user name - - `password`: database master password (can be also set as `PGPASSWORD` environment variable of the Docker container) -- Set proper version in Postgres Docker image tag (change the images itself only if you know what are you doing): + - `password`: database master password (can also be set as the `PGPASSWORD` environment variable of the Docker container) +- Set a proper version in Postgres Docker image tag (change the image itself only if you know what you are doing): - `databaseContainer:dockerImage` Launch DBLab Engine: @@ -51,10 +52,9 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` :::info @@ -73,26 +73,26 @@ See more details in the official [Docker command-line reference](https://docs.do - `export AWS_ACCESS_KEY="access_key"` - `export AWS_SECRET_ACCESS_KEY="secret_access_key"` Read how you can get the AWS access keys for the existing user [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) -3. Create and attach an IAM Policy for IAM Database Access to an AWS user. Read how you can to it [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html) +3. Create and attach an IAM Policy for IAM Database Access to an AWS user. Read how you can do it [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html) :::info Alternatively, you can add `AmazonRDSFullAccess`, `IAMFullAccess` policies to an AWS user (not recommended). ::: ### Set up and run DBLab Engine -Copy the contents of configuration example [`config.example.logical_rds_iam.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.logical_rds_iam.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the Engine +Copy the contents of configuration example [`config.example.logical_rds_iam.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.logical_rds_iam.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the Engine - Set connection options `retrieval:spec:logicalDump:options:source:connection`: - `dbname`: database name to connect to - `username`: database user name - Set AWS params in `retrieval:spec:logicalDump:options:source:rdsIam`: - `awsRegion`: RDS instance region - `dbInstanceIdentifier`: RDS instance identifier -- Set proper version in Postgres Docker image tag (change the images itself only if you know what are you doing): +- Set a proper version in Postgres Docker image tag (change the image itself only if you know what you are doing): - `databaseContainer:dockerImage` ### Download AWS RDS certificate -This type of data retrieval requires a secure connection to a database. To setup it we need to download a certificate from AWS. +This type of data retrieval requires a secure connection to a database. To set it up, we need to download a certificate from AWS. ```bash wget https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem -P ~/.dblab/ @@ -117,10 +117,9 @@ sudo docker run \ --volume ~/.dblab/rds-combined-ca-bundle.pem:/cert/rds-combined-ca-bundle.pem \ --env AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY}" \ --env AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}" \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` :::info @@ -143,3 +142,7 @@ sudo rm -rf /var/lib/dblab/dblab_pool/data/* sudo umount /var/lib/dblab/dblab_pool/dump sudo rm -rf /var/lib/dblab/dblab_pool/dump ``` + +## Alternative: RDS/Aurora refresh tool + +For large production databases, running `pg_dump` directly against production can hold xmin horizon for hours and create significant load. The [RDS/Aurora refresh tool](/docs/dblab-howtos/administration/data/rds-refresh) provides an alternative approach that dumps from a temporary RDS clone instead, leaving production untouched. diff --git a/docs/dblab-howtos/administration/data/rsync.md b/docs/dblab-howtos/administration/data/rsync.md index 2b847f1f..dcf99828 100644 --- a/docs/dblab-howtos/administration/data/rsync.md +++ b/docs/dblab-howtos/administration/data/rsync.md @@ -1,6 +1,7 @@ --- title: "Data source: rsync" sidebar_label: "rsync" +description: Configure DBLab Engine to build a physical Postgres data directory from a source or WAL archive using rsync as a custom physical restore command. --- :::info @@ -9,13 +10,13 @@ As the first step, you need to set up a machine for DBLab Engine instance. See t ## Configuration ### Jobs -In order to set up DBLab Engine to automatically get the data from database using [rsync](https://rsync.samba.org/) you need to use following jobs: +To set up DBLab Engine to automatically get the data from a database using [rsync](https://rsync.samba.org/), use the following jobs: - [physicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalrestore) - [physicalSnapshot](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalsnapshot) ### Options -Copy the example configuration file [`config.example.physical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.physical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml` and update the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the Engine +Copy the example configuration file [`config.example.physical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.physical_generic.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the Engine - Set connection options in `physicalRestore:options:envs`: - `PGUSER`: database user name - `PGPASSWORD`: database master password @@ -29,7 +30,7 @@ Copy the example configuration file [`config.example.physical_generic.yml`](http ${PGDATA}/temp_wal/ 2>/dev/null` ``` - `restore_command`: `TBD` -- Set proper version in Postgres Docker image tag (change the images itself only if you know what are you doing): +- Set a proper version in Postgres Docker image tag (change the image itself only if you know what you are doing): - `databaseContainer:dockerImage` ### pg_basebackup -D - -Ft -X @@ -51,10 +52,9 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` :::info diff --git a/docs/dblab-howtos/administration/data/wal-g.md b/docs/dblab-howtos/administration/data/wal-g.md index 1ef16984..a7f8ba8d 100644 --- a/docs/dblab-howtos/administration/data/wal-g.md +++ b/docs/dblab-howtos/administration/data/wal-g.md @@ -1,6 +1,7 @@ --- title: "Data source: WAL-G" sidebar_label: "WAL-G" +description: Configure DBLab Engine to restore a physical Postgres data directory from a WAL-G backup archive, using WAL-G environment variables and settings. --- :::info @@ -9,18 +10,18 @@ As the first step, you need to set up a machine for DBLab Engine instance. See t ## Configuration ### Jobs -In order to set up DBLab Engine to automatically get the data from database using [WAL-G](https://github.com/wal-g/wal-g) archival restoration tool you need to use following jobs: +To set up DBLab Engine to automatically get the data from a database using the [WAL-G](https://github.com/wal-g/wal-g) archival restoration tool, use the following jobs: - [physicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalrestore) - [physicalSnapshot](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalsnapshot) ### Options -Copy the example configuration file [`config.example.physical_walg.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.physical_walg.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the Engine +Copy the example configuration file [`config.example.physical_walg.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.physical_walg.yml) from the DBLab Engine repository to `~/.dblab/engine/configs/server.yml` and update the following options: +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the Engine - Set connection options in `physicalRestore:options:envs`: - Use WAL-G environment variables to configure the job, see the [WAL-G configuration reference](https://github.com/wal-g/wal-g#configuration) - Set WAL-G settings in `physicalRestore:options:walg`: - `backupName` - defines the backup name to restore -- Set a proper version in Postgres Docker image tag (change the images itself only if you know what are you doing): +- Set a proper version in Postgres Docker image tag (change the image itself only if you know what you are doing): - `databaseContainer:dockerImage` ## Run DBLab Engine @@ -30,7 +31,7 @@ Use Docker volumes to make credential files available to WAL-G. For example: `--volume ~/.dblab/credentials.json:/home/dblab/credentials.json` or store them into a config directory. -Note that credentials location inside the container matches the right part of the mount expression +Note that the credentials location inside the container matches the right part of the mount expression. ::: ```bash @@ -47,10 +48,9 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` :::info diff --git a/docs/dblab-howtos/administration/engine-manage.md b/docs/dblab-howtos/administration/engine-manage.md index 3c8df329..c7b53b08 100644 --- a/docs/dblab-howtos/administration/engine-manage.md +++ b/docs/dblab-howtos/administration/engine-manage.md @@ -8,15 +8,17 @@ keywords: - "postgres.ai cloning management" --- +This guide explains how to configure, start, reconfigure, upgrade, and monitor a DBLab Engine instance running in a Docker container. + ## Configure and start a DBLab Engine instance -Define config file `~/.dblab/engine/configs/server.yml` +Define the config file `~/.dblab/engine/configs/server.yml`. :::tip All YAML features can be used, including anchors and aliases, to help you conveniently manage your configuration sections. For instance, you can define a binding with `&` and then refer to it using an alias denoted by `*`. -See config examples [here](https://gitlab.com/postgres-ai/database-lab/-/tree/v4.0.3/engine/configs) +See config examples [here](https://gitlab.com/postgres-ai/database-lab/-/tree/v4.1.3/engine/configs) ::: After configuring DBLab Engine, run the following command: @@ -36,8 +38,7 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.41 \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` :::info @@ -52,12 +53,12 @@ See more details in the official [Docker command-line reference](https://docs.do DBLab Engine supports reconfiguration without a restart (therefore, without any downtime): - Edit the configuration file (usually, `~/.dblab/engine/configs/server.yml`). -- Issue a [SIGHUP](https://en.wikipedia.org/wiki/SIGHUP) signal to the main process in the DLE container – if the container name is `dblab_server`, then run this (note that `kill` here is not killing the process, it just sends the SIGHUP signal to it): +- Issue a [SIGHUP](https://en.wikipedia.org/wiki/SIGHUP) signal to the main process in the DBLab Engine container – if the container name is `dblab_server`, then run this (note that `kill` here is not killing the process, it just sends the SIGHUP signal to it): ```bash sudo docker exec -it dblab_server kill -SIGHUP 1 ``` -- Ensure that configuration was reloaded, it should be seen in the logs (message `Configuration has been reloaded`): +- Ensure that the configuration was reloaded — it should be seen in the logs (message `Configuration has been reloaded`): ```bash sudo docker logs --since 5m dblab_server ``` @@ -75,23 +76,23 @@ echo 'set backupcopy=yes' >> ~/.vimrc ::: ## Upgrade DBLab Engine -Stop and remove the container using `sudo docker stop dblab_server` and `sudo docker rm dblab_server` After that, [launch](#configure-and-start-a-dblab-engine-instance) a new container. +Stop and remove the container using `sudo docker stop dblab_server` and `sudo docker rm dblab_server`. After that, [launch](#configure-and-start-a-dblab-engine-instance) a new container. :::caution -Prior to version 3.0.0, upgrading or restarting DLE meant losing all the running clones. In DLE 3.0.0, clones became persistent: after any restart – including VM restart - existing Postgres containers are restarted as well. The same should apply to future upgrades unless a specific upgrade breaks backward compatibility (consulting release notes is advised). +Prior to version 3.0.0, upgrading or restarting DBLab Engine meant losing all running clones. In DBLab Engine 3.0.0, clones became persistent: after any restart — including a VM restart — existing Postgres containers are restarted as well. The same should apply to future upgrades unless a specific upgrade breaks backward compatibility (consulting the release notes is advised). ::: :::caution -Before version 3.1.0, DLE images (`postgresai/dblab-server`) were based on ZFS 0.8.x. Since 3.1.0, we switched to ZFS 2.1.x. +Before version 3.1.0, DBLab Engine images (`postgresai/dblab-server`) were based on ZFS 0.8.x. Since 3.1.0, they use ZFS 2.1.x. An example of error: ``` "RunnerError(cmd=\"zfs clone -o mountpoint=/var/lib/dblab/dblab_pool/clones/dblab_clone_6000 dblab_pool@snapshot_20220712153456 dblab_pool/dblab_clone_6000 \u0026\u0026 chown -R root /var/lib/dblab/dblab_pool/clones/dblab_clone_6000\", inerr=\"exit status 1\", stderr=\"chown: /var/lib/dblab/dblab_pool/clones/dblab_clone_6000: No such file or directory\n\" exit=\"1\")" ``` -If you need to upgrade an existing DLE setup that is running on ZFS 0.8.x, consider the following options: +If you need to upgrade an existing DBLab Engine setup that is running on ZFS 0.8.x, consider the following options: -Option 1: upgrade your system to use ZFS 2.1, optionally upgrade your pool (`zpool upgrade dblab_pool`), and then upgrade DLE to use the default image, `postgresai/dblab-server:3.5.0` +Option 1: upgrade your system to use ZFS 2.1, optionally upgrade your pool (`zpool upgrade dblab_pool`), and then upgrade DBLab Engine to use the default image, `postgresai/dblab-server:3.5.0` -Option 2: postpone the ZFS upgrade, stay on ZFS 0.8, and upgrade DLE to version 3.1 using a special image, `postgresai/dblab-server:3.5.0-zfs08` +Option 2: postpone the ZFS upgrade, stay on ZFS 0.8, and upgrade DBLab Engine to version 3.1 using a special image, `postgresai/dblab-server:3.5.0-zfs08` ::: ## Observe DBLab Engine logs @@ -105,7 +106,7 @@ If you need to save the logs in a file: sudo docker logs dblab_server 2>&1 | gzip > dblab_server.log.gz ``` -If you want to see more details, enable debug mode setting option `debug` to `true` (see [example](https://gitlab.com/postgres-ai/database-lab/-/tree/v4.0.3/engine/configs)). Next, follow [the reconfiguration guidelines](#reconfigure-database-lab) to apply the change. +If you want to see more details, enable debug mode by setting the option `debug` to `true` (see [example](https://gitlab.com/postgres-ai/database-lab/-/tree/v4.1.3/engine/configs)). Next, follow [the reconfiguration guidelines](#reconfigure-dblab-engine) to apply the change. :::caution When debug mode is turned on, logs may contain sensitive data such as API secret keys for the backup system. diff --git a/docs/dblab-howtos/administration/engine-secure.md b/docs/dblab-howtos/administration/engine-secure.md index 42e3bd15..808dc639 100644 --- a/docs/dblab-howtos/administration/engine-secure.md +++ b/docs/dblab-howtos/administration/engine-secure.md @@ -1,15 +1,16 @@ --- title: Secure DBLab Engine sidebar_label: Secure DBLab Engine +description: Secure the DBLab Engine UI, API, CLI, and database clones with an Envoy proxy and a Let's Encrypt SSL certificate for encrypted HTTPS access. --- -To make your work with DBLab Engine UI / API / CLI and clones secure, install and configure [Envoy proxy](https://www.envoyproxy.io) with a SSL certificate. +To make your work with DBLab Engine UI / API / CLI and clones secure, install and configure [Envoy proxy](https://www.envoyproxy.io) with an SSL certificate. :::note Before you begin, you will need your Organization key and Project name provided by the PostgresAI platform. Obtain these by registering on the [platform](http://console.postgres.ai). Detailed instructions are available [here](https://postgres.ai/docs/dblab-howtos/administration/install-dle-from-postgres-ai). ::: -## Configuring a secure DBLab engine +## Configuring a secure DBLab Engine ### 1. DNS configuration Update the DNS `A` record for your public domain to resolve to the public IP address of the DBLab Engine server. Note that DNS changes may take some time to propagate. @@ -49,3 +50,7 @@ PGPASSWORD=secret_password psql \ ``` Adjust the port numbers accordingly for other clones, following the pattern `original_port+3000` (e.g., `6001->9001`, `6002->9002`, etc.). + +## Alternative: Teleport integration + +For zero-trust access control with audit logging, certificate-based authentication, and role-based access, consider using [Teleport integration](/docs/dblab-howtos/administration/teleport-integration) (DBLab Engine 4.1+). diff --git a/docs/dblab-howtos/administration/index.md b/docs/dblab-howtos/administration/index.md index abfc4d60..e19d530c 100644 --- a/docs/dblab-howtos/administration/index.md +++ b/docs/dblab-howtos/administration/index.md @@ -2,7 +2,7 @@ title: DBLab Engine administration sidebar_label: Administration slug: /dblab-howtos/administration -description: How to administer DBLab Engine +description: Guides for installing, configuring, securing, and maintaining DBLab Engine, managing Joe Bot, and refreshing data for Postgres environments. --- ## Guides @@ -16,4 +16,5 @@ description: How to administer DBLab Engine - [How to refresh data when working in the "logical" mode](/docs/dblab-howtos/administration/logical-full-refresh) - [Masking sensitive data in PostgreSQL logs when using CI Observer](/docs/dblab-howtos/administration/ci-observer-postgres-log-masking) - [Add disk space to ZFS pool without downtime](/docs/dblab-howtos/administration/add-disk-space-to-zfs-pool) +- [Teleport integration](/docs/dblab-howtos/administration/teleport-integration) diff --git a/docs/dblab-howtos/administration/install-database-lab-with-terraform.md b/docs/dblab-howtos/administration/install-database-lab-with-terraform.md index bb59c5a1..1ce53ccc 100644 --- a/docs/dblab-howtos/administration/install-database-lab-with-terraform.md +++ b/docs/dblab-howtos/administration/install-database-lab-with-terraform.md @@ -1,6 +1,7 @@ --- title: How to install Database Lab with Terraform on AWS sidebar_label: Install Database Lab with Terraform on AWS +description: Terraform is no longer supported for installing DBLab Engine. Use the PostgresAI Console or the AWS Marketplace to install DBLab instead. --- -Terraform is no longer a supported method for installing DLE. +Terraform is no longer a supported method for installing DBLab Engine. Instead, install DBLab using [the PostgresAI Console](/docs/dblab-howtos/administration/install-dle-from-postgres-ai) or [the AWS Marketplace](/docs/dblab-howtos/administration/install-dle-from-aws-marketplace). diff --git a/docs/dblab-howtos/administration/install-dle-from-aws-marketplace.md b/docs/dblab-howtos/administration/install-dle-from-aws-marketplace.md index 6336080d..09791cea 100644 --- a/docs/dblab-howtos/administration/install-dle-from-aws-marketplace.md +++ b/docs/dblab-howtos/administration/install-dle-from-aws-marketplace.md @@ -1,16 +1,17 @@ --- title: How to install DBLab using the AWS Marketplace sidebar_label: Install DBLab from AWS Marketplace +description: Install DBLab Engine from the AWS Marketplace to get instant database branching and thin clones for RDS, RDS Aurora, and any Postgres source. ---

DBLab Engine and AWS Marketplace

-If you're using AWS, [installing DBLab from the AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-wlmm2satykuec) is the fastest way to have powerful database branching for any database, including RDS and RDS Aurora. But not only RDS: any Postgres and Postgres-compatible source is supported as a source for DBLab. +If you're using AWS, [installing DBLab from the AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-wlmm2satykuec) is the fastest way to get database branching for any database, including RDS and RDS Aurora. It is not limited to RDS: any Postgres or Postgres-compatible database can serve as a source for DBLab. :::info -Currently, only the "logical" mode of data retrieval (dump/restore) is supported – the only available method for managed PostgreSQL cloud services such as RDS Postgres, RDS Aurora Postgres, Azure Postgres, or Heroku. "Physical" mode is not yet supported by the module, but it will be in the future. More about [various data retrieval options for DBLab](/docs/dblab-howtos/administration/data). +Currently, only the "logical" mode of data retrieval (dump/restore) is supported – the only available method for managed Postgres cloud services such as RDS Postgres, RDS Aurora Postgres, Azure Postgres, or Heroku. "Physical" mode is not yet supported by the module, but it will be in the future. More about [various data retrieval options for DBLab](/docs/dblab-howtos/administration/data). ::: :::note @@ -20,12 +21,12 @@ Check out the DBLab installation tutorial: ## Prerequisites - [AWS cloud account](https://aws.amazon.com) -- SSH client (available by default on Linux and MacOS; Windows users: consider using [PuTTY](https://www.putty.org/)) -- A key pair already generated for the AWS region that we are going to use during the installation. Both RSA or ed25519 will work. If you're not familiar with the process of creation a key pair in AWS, read their documentation: ["Create key pairs"](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-key-pairs.html). +- SSH client (available by default on Linux and macOS; Windows users: consider using [PuTTY](https://www.putty.org/)) +- A key pair already generated for the AWS region that we are going to use during the installation. Both RSA and ed25519 will work. If you're not familiar with the process of creating a key pair in AWS, read their documentation: ["Create key pairs"](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-key-pairs.html). ## Steps to install DBLab Engine from AWS Marketplace The first steps are trivial: -- Log in into AWS: https://console.aws.amazon.com/ +- Log in to AWS: https://console.aws.amazon.com/ - Open [the DBLab on AWS Marketplace page](https://aws.amazon.com/marketplace/pp/prodview-wlmm2satykuec) And press the "View purchase options" button: @@ -70,7 +71,7 @@ Now, it is time to fill the form that defines the AWS resources that we need: Next, on the same page: - define the size of EBS volume that will be created (you can find pricing calculator here: ["Amazon EBS pricing"](https://aws.amazon.com/ebs/pricing/)): - put as many GiB as roughly your database has (it is always possible to add more space without downtime), - - define how many snapshots you'll be needed (minimum 2); + - define how many snapshots you'll need (minimum 2); - define secret token (at least 9 characters are required!) – it will be used to communicate with DBLab API, CLI, and UI. Then, press "Next". @@ -93,7 +94,7 @@ Once you've pressed "Submit", the process begins. You need to wait a few minutes, while all resources are provisioned and DBLab setup is complete. Check out the "Outputs" section – once DBLab API and UI are ready, you'll see the ordered list of instructions on how to connect to UI and API. -Note that the initial data retrieval can be long – it depends on the size of the source database(s). However, DBLab API, CLI, and UI are available for use while it's happening. And once the retrieval is finished, DBLab is ready for use. Happy cloning! +Note that the initial data retrieval can be long – it depends on the size of the source database(s). However, DBLab API, CLI, and UI are available for use while it is happening. Once the retrieval is finished, DBLab is ready for use. ## Setup demonstration
@@ -105,7 +106,7 @@ To troubleshoot: - Use SSH to connect to the EC2 instance - Check the containers that are running: `sudo docker ps` - Check the DBLab Engine container's logs: `sudo docker logs dblab_server` -- If needed, check Postgres logs for the main branch. They are located in `/var/lib/dblab/dblab_pool/dataset_1/data/log` for the first snapshot of the database, in ``/var/lib/dblab/dblab_pool/dataset_2/data/log` for the second one (if it's already fetched); if you've configured DBLab to have more than 2 snapshots, check out the other directories too (`/var/lib/dblab/dblab_pool/dataset_$N/data/log`, where `$N` is the snapshot number, starting with `1`) +- If needed, check Postgres logs for the main branch. They are located in `/var/lib/dblab/dblab_pool/dataset_1/data/log` for the first snapshot of the database, in `/var/lib/dblab/dblab_pool/dataset_2/data/log` for the second one (if it's already fetched); if you've configured DBLab to have more than 2 snapshots, check out the other directories too (`/var/lib/dblab/dblab_pool/dataset_$N/data/log`, where `$N` is the snapshot number, starting with `1`) ## Getting support With DBLab installed from AWS Marketplace, guaranteed vendor support is included – please use [one of the available ways to contact](https://postgres.ai/contact). diff --git a/docs/dblab-howtos/administration/install-dle-from-postgres-ai.md b/docs/dblab-howtos/administration/install-dle-from-postgres-ai.md index 841f56f3..d48513bf 100644 --- a/docs/dblab-howtos/administration/install-dle-from-postgres-ai.md +++ b/docs/dblab-howtos/administration/install-dle-from-postgres-ai.md @@ -1,6 +1,7 @@ --- title: How to install DBLab using the PostgresAI Console sidebar_label: Install DBLab from PostgresAI Console +description: Install DBLab Standard Edition in your own cloud or on-premises infrastructure in minutes using the PostgresAI Console, with vendor support included. --- Use [the PostgresAI Console](https://console.postgres.ai/) for an easy and quick installation of DBLab. Following the steps below, in a few minutes, you will get: @@ -38,7 +39,7 @@ In both scenarios, your data remains securely within your infrastructure. - [Create](https://console.postgres.ai/addorg) a new organization - Inside your organization, go to the "Billing" section and add a new payment method: - press the "Edit payment methods" button, - - you will see the Stripe portal – note it has the address `https://billing.stripe.com/...` (Postgres.ai partners with Stripe for simplified billing), + - you will see the Stripe portal – note it has the address `https://billing.stripe.com/...` (PostgresAI partners with Stripe for simplified billing), - add your payment methods there and close the page. ## DBLab installation @@ -66,7 +67,7 @@ Choose the volume type and size:

:::note -In this example the database size is 100 GiB, we want to create 3 datasets to be able to create 3 snapshots, so the volume with size 300 GiB will be created. +In this example the database size is 100 GiB — we want to create 3 datasets to be able to create 3 snapshots, so the volume with size 300 GiB will be created. ::: Provide a name for your DBLab instance: @@ -94,7 +95,7 @@ Provide SSH public keys:

:::note -These SSH public keys will be added to the DBLab server's ~/.ssh/authorized_keys file. Providing at least one public key is recommended to ensure access to the server after deployment. +These SSH public keys will be added to the DBLab server's `~/.ssh/authorized_keys` file. Providing at least one public key is recommended to ensure access to the server after deployment. ::: Review the specifications of the virtual machine, and click "Create": @@ -102,7 +103,7 @@ Review the specifications of the virtual machine, and click "Create": DBLab Engine in DBLab Platform: step 9

-Select the installation method and follow the instructions to create server and install DLE SE: +Select the installation method and follow the instructions to create a server and install DBLab SE:

DBLab Engine in DBLab Platform: step 10

@@ -111,7 +112,7 @@ Select the installation method and follow the instructions to create server and To perform the initial deployment, a new temporary SSH key will be generated and added to the Cloud. After the deployment is completed, this key will be deleted and the SSH key that was specified in the "ssh_public_keys" variable will be added to the server. ::: -After running the deployment command, You need to wait a few minutes, while all resources are provisioned and DBLab setup is complete. Check out the "usage instructions" – once DBLab API and UI are ready, you'll see the ordered list of instructions on how to connect to UI and API. +After running the deployment command, you need to wait a few minutes, while all resources are provisioned and DBLab setup is complete. Check out the "usage instructions" – once DBLab API and UI are ready, you'll see the ordered list of instructions on how to connect to UI and API. Example: @@ -170,7 +171,7 @@ Now UI should be available at http://127.0.0.1:2346

:::note -Currently, configuring DBLab in UI allows config changes only for the "logical" mode of data retrieval (dump/restore) – the only available method for managed PostgreSQL cloud services such as RDS Postgres, RDS Aurora Postgres, Azure Postgres, or Heroku. "Physical" mode is not yet supported in UI but is still possible (through SSH connection and [editing DBLab config file directly](/docs/dblab-howtos/administration/engine-manage)). More about [various data retrieval options for DBLab](/docs/dblab-howtos/administration/data). +Currently, configuring DBLab in UI allows config changes only for the "logical" mode of data retrieval (dump/restore) – the only available method for managed Postgres cloud services such as RDS Postgres, RDS Aurora Postgres, Azure Postgres, or Heroku. "Physical" mode is not yet supported in UI but is still possible (through SSH connection and [editing DBLab config file directly](/docs/dblab-howtos/administration/engine-manage)). More about [various data retrieval options for DBLab](/docs/dblab-howtos/administration/data). ::: ## Configure DBLab and run the first data retrieval @@ -182,7 +183,7 @@ To troubleshoot: - Use SSH to connect to the DBLab server - Check the containers that are running: `sudo docker ps` - Check the DBLab container's logs: `sudo docker logs dblab_server` -- If needed, check Postgres logs for the main branch. They are located in `/var/lib/dblab/dblab_pool/dataset_1/data/log` for the first snapshot of the database, in ``/var/lib/dblab/dblab_pool/dataset_2/data/log` for the second one (if it's already fetched); if you've configured DBLab to have more than 2 snapshots, check out the other directories too (`/var/lib/dblab/dblab_pool/dataset_$N/data/log`, where `$N` is the snapshot number, starting with `1`) +- If needed, check Postgres logs for the main branch. They are located in `/var/lib/dblab/dblab_pool/dataset_1/data/log` for the first snapshot of the database, in `/var/lib/dblab/dblab_pool/dataset_2/data/log` for the second one (if it's already fetched); if you've configured DBLab to have more than 2 snapshots, check out the other directories too (`/var/lib/dblab/dblab_pool/dataset_$N/data/log`, where `$N` is the snapshot number, starting with `1`) ## Getting support -With DBLab installed from DBLab Platform, guaranteed vendor support is included – please use [one of the available ways to contact](https://postgres.ai/contact). +With DBLab installed from the PostgresAI Console, guaranteed vendor support is included – please use [one of the available ways to contact](https://postgres.ai/contact). diff --git a/docs/dblab-howtos/administration/install-dle-manually.md b/docs/dblab-howtos/administration/install-dle-manually.md index f4daa699..aa7a5ccf 100644 --- a/docs/dblab-howtos/administration/install-dle-manually.md +++ b/docs/dblab-howtos/administration/install-dle-manually.md @@ -1,6 +1,7 @@ --- title: How to install DBLab manually sidebar_label: Install DBLab manually +description: Step-by-step guide to manually install DBLab Engine Community Edition on Ubuntu with Docker and ZFS or LVM for thin cloning of Postgres databases. --- import Tabs from '@theme/Tabs'; @@ -19,16 +20,16 @@ This describes how to manually install the DBLab Engine Community Edition (DBLab ## Step 1. Prepare a machine with disk, Docker, and ZFS ### Prepare a machine -Create a virtual machine with Ubuntu 22.04, and add a disk to store the data. You can use any cloud provider (e.q, AWS, Google Cloud, etc) or run your Database Lab on a hypervisor (e.q, VMware), or on bare metal. +Create a virtual machine with Ubuntu 22.04, and add a disk to store the data. You can use any cloud provider (e.g., AWS, Google Cloud) or run DBLab Engine on a hypervisor (e.g., VMware) or on bare metal. -### (optional) Ports need to be open +### (Optional) Ports need to be open You will need to open the following ports: - `22`: to connect to the instance using SSH - `2346`: to work with DBLab Engine UI and API (can be changed in the DBLab Engine configuration file) -- `6000-6100`: to connect to PostgreSQL clones (this is the default port range used in the DBLab Engine configuration file, and can be changed if needed) +- `6000-6100`: to connect to Postgres clones (this is the default port range used in the DBLab Engine configuration file, and can be changed if needed) :::caution -For real-life use, it is not a good idea to open ports to the public. Instead, it is recommended to use VPN or SSH port forwarding to access both Database Lab API and PostgreSQL clones, or to enforce encryption for all connections using NGINX with SSL and configuring SSL in PostgreSQL configuration. +For real-life use, it is not a good idea to open ports to the public. Instead, use a VPN or SSH port forwarding to access both the DBLab Engine API and Postgres clones, or enforce encryption for all connections using NGINX with SSL and configuring SSL in the Postgres configuration. ::: @@ -93,7 +94,7 @@ Some examples: ``` ### Set up either ZFS or LVM to enable thin cloning -ZFS is a recommended way to enable thin cloning in Database Lab. LVM is also available, but has certain limitations: +ZFS is the recommended way to enable thin cloning in DBLab Engine. LVM is also available, but has certain limitations: - much less flexible disk space consumption and risks for a clone to be destroyed during massive operations in it - inability to work with multiple snapshots ("time travel"), cloning always happens based on the most recent version of data @@ -190,7 +191,7 @@ sudo sed -i 's/snapshot_autoextend_percent.*/snapshot_autoextend_percent = 20/g' ## Step 2. Configure and launch the DBLab Engine :::caution -To make your work with Database Lab API secure, do not open Database Lab API and Postgres clone ports to the public and instead use VPN or SSH port forwarding. It is also a good idea to encrypt all the traffic: for Postgres clones, set up SSL in the configuration files; and for Database Lab API, install, and configure NGINX with a self-signed SSL certificate. See the [How to Secure DBLab Engine](/docs/dblab-howtos/administration/engine-secure). +To make your work with the DBLab Engine API secure, do not open the DBLab Engine API and Postgres clone ports to the public; instead, use a VPN or SSH port forwarding. It is also a good idea to encrypt all traffic: for Postgres clones, set up SSL in the configuration files; and for the DBLab Engine API, install and configure NGINX with a self-signed SSL certificate. See [How to secure DBLab Engine](/docs/dblab-howtos/administration/engine-secure). ::: ### Prepare database data directory @@ -210,7 +211,7 @@ Next, we need to get the data to the DBLab Engine server. For our testing needs, }> -If you don't have an existing database for testing, then let's just generate some synthetic database in the data directory ("PGDATA") located at `/var/lib/dblab/dblab_pool/data`. A simple way of doing this is to use PostgreSQL standard benchmarking tool, `pgbench`. With scale factor `-s 100`, the database size will be ~1.4 GiB; feel free to adjust the scale factor value according to your needs. +If you don't have an existing database for testing, then let's just generate some synthetic database in the data directory ("PGDATA") located at `/var/lib/dblab/dblab_pool/data`. A simple way of doing this is to use the standard Postgres benchmarking tool, `pgbench`. With scale factor `-s 100`, the database size will be ~1.4 GiB; feel free to adjust the scale factor value according to your needs. To generate PGDATA with `pgbench`, we are going to run a regular Docker container with Postgres temporarily. We will use `POSTGRES_HOST_AUTH_METHOD=trust` to allow a connection without authentication (not suitable for real-life use). @@ -236,22 +237,22 @@ Generate data in the `test` database using `pgbench`: sudo docker exec -it dblab_pg_initdb pgbench -U postgres -i -s 100 test ``` -PostgreSQL data directory is ready. Now let's stop and remove the container: +The Postgres data directory is ready. Now let's stop and remove the container: ```bash sudo docker stop dblab_pg_initdb sudo docker rm dblab_pg_initdb ``` -Now, we need to take care of DBLab Engine configuration. Copy the contents of configuration example [`config.example.logical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.logical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml`: +Now, we need to take care of DBLab Engine configuration. Copy the contents of configuration example [`config.example.logical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.logical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml`: ```bash mkdir -p ~/.dblab/engine/configs -curl -fsSL https://gitlab.com/postgres-ai/database-lab/-/raw/v4.0.3/engine/configs/config.example.logical_generic.yml \ +curl -fsSL https://gitlab.com/postgres-ai/database-lab/-/raw/v4.1.3/engine/configs/config.example.logical_generic.yml \ --output ~/.dblab/engine/configs/server.yml ``` Open `~/.dblab/engine/configs/server.yml` and edit the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the DBLab Engine +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the DBLab Engine - Remove `logicalDump` section completely - Remove `logicalRestore` section completely - Leave `logicalSnapshot` as is @@ -261,18 +262,18 @@ Open `~/.dblab/engine/configs/server.yml` and edit the following options: -If you want to try Database Lab for an existing database, you need to copy the data to PostgreSQL data directory on the Database Lab server, to the directory `/var/lib/dblab/dblab_pool/data`. This step is called "thick cloning". It only needs to be completed once. There are several options to physically copy the data directory. Here we will use the standard PostgreSQL tool, `pg_basebackup`. However, we are not going to use it directly (although, it is possible) – we will specify its options in the DBLab Engine configuration file. +If you want to try Database Lab for an existing database, you need to copy the data to the Postgres data directory on the DBLab Engine server, to the directory `/var/lib/dblab/dblab_pool/data`. This step is called "thick cloning". It only needs to be completed once. There are several options to physically copy the data directory. Here we will use the standard Postgres tool, `pg_basebackup`. However, we are not going to use it directly (although, it is possible) – we will specify its options in the DBLab Engine configuration file. -First, copy the example configuration file [`config.example.physical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.physical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml`: +First, copy the example configuration file [`config.example.physical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.physical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml`: ```bash mkdir -p ~/.dblab/engine/configs -curl -fsSL https://gitlab.com/postgres-ai/database-lab/-/raw/v4.0.3/engine/configs/config.example.physical_generic.yml \ +curl -fsSL https://gitlab.com/postgres-ai/database-lab/-/raw/v4.1.3/engine/configs/config.example.physical_generic.yml \ --output ~/.dblab/engine/configs/server.yml ``` Next, open `~/.dblab/engine/configs/server.yml` and edit the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the DBLab Engine +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the DBLab Engine - In `retrieval:spec:physicalRestore:options:envs`, specify how to reach the source Postgres database to run `pg_basebackup`: `PGUSER`, `PGPASSWORD`, `PGHOST`, and `PGPORT` - If your Postgres major version is not 17 (default), set the proper version in Postgres Docker image tag: - `databaseContainer:dockerImage` @@ -284,20 +285,20 @@ Optionally, you might want to keep PGDATA up-to-date (which is being continuousl -If you want to try Database Lab for an existing database, you need to copy the data to the PostgreSQL data directory on the Database Lab server, to the directory `/var/lib/dblab/dblab_pool/data`. This step is called "thick cloning". It only needs to be completed once. +If you want to try Database Lab for an existing database, you need to copy the data to the Postgres data directory on the DBLab Engine server, to the directory `/var/lib/dblab/dblab_pool/data`. This step is called "thick cloning". It only needs to be completed once. Here we will configure DBLab Engine to use a "logical" method of thick cloning, dump/restore. -First, copy the configuration example configuration file [`config.example.logical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.0.3/engine/configs/config.example.logical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml`: +First, copy the example configuration file [`config.example.logical_generic.yml`](https://gitlab.com/postgres-ai/database-lab/-/blob/v4.1.3/engine/configs/config.example.logical_generic.yml) from the Database Lab repository to `~/.dblab/engine/configs/server.yml`: ```bash mkdir -p ~/.dblab/engine/configs -curl -fsSL https://gitlab.com/postgres-ai/database-lab/-/raw/v4.0.3/engine/configs/config.example.logical_generic.yml \ +curl -fsSL https://gitlab.com/postgres-ai/database-lab/-/raw/v4.1.3/engine/configs/config.example.logical_generic.yml \ --output ~/.dblab/engine/configs/server.yml ``` Now open `~/.dblab/engine/configs/server.yml` and edit the following options: -- Set secure `server:verificationToken`, it will be used to authorize API requests to the DBLab Engine +- Set a secure `server:verificationToken` — it will be used to authorize API requests to the DBLab Engine - Set connection options in `retrieval:spec:logicalDump:options:source:connection`: - `dbname`: database name to connect to - `host`: database server host @@ -310,7 +311,7 @@ Now open `~/.dblab/engine/configs/server.yml` and edit the following options: -### Launch Database Lab server +### Launch DBLab Engine server @@ -360,10 +360,9 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` @@ -383,10 +382,9 @@ sudo docker run \ --volume /sys/kernel/debug:/sys/kernel/debug:rw \ --volume /lib/modules:/lib/modules:ro \ --volume /proc:/host_proc:ro \ - --env DOCKER_API_VERSION=1.39 \ --detach \ --restart on-failure \ - postgresai/dblab-server:4.0.3 + postgresai/dblab-server:4.1.3 ``` @@ -399,6 +397,12 @@ To allow external connections, consider either using additional software such as See more details in the official [Docker command-line reference](https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose). ::: +:::warning +Publish only `2345` (the API port) on the `dblab_server` container — **do not** also add `--publish 127.0.0.1:2346:2346`. + +In DBLab Engine 4.x, the web UI runs in a *separate* container that binds host port `2346` itself. If `dblab_server` also publishes `2346`, the UI container fails to start with `Bind for 127.0.0.1:2346 failed: port is already allocated`; the engine logs `failed to start embedded UI container` and keeps running without a UI. (Older 3.x-era commands published `2346` on the server container — drop it when upgrading.) +::: + ### Check the DBLab Engine logs ```bash diff --git a/docs/dblab-howtos/administration/joe-manage.md b/docs/dblab-howtos/administration/joe-manage.md index e12dd60c..33375f3d 100644 --- a/docs/dblab-howtos/administration/joe-manage.md +++ b/docs/dblab-howtos/administration/joe-manage.md @@ -1,10 +1,11 @@ --- -title: How to manage Joe bot -sidebar_label: Manage Joe bot +title: How to manage Joe Bot +sidebar_label: Manage Joe Bot +description: Start, reconfigure, upgrade, and monitor the Joe Bot container for SQL optimization on DBLab Engine clones, and check its status and logs. --- ## Start Joe Bot container -Define the config file `~/.dblab/joe/configs/joe.yml` according the [configuration options page](/docs/reference-guides/joe-bot-configuration-reference) and run the command: +Define the config file `~/.dblab/joe/configs/joe.yml` according to the [configuration options page](/docs/reference-guides/joe-bot-configuration-reference) and run the command: ```bash sudo docker run \ --name joe_bot \ @@ -16,7 +17,7 @@ sudo docker run \ postgresai/joe:latest ``` -Ensure that apps folder is writable by Joe inside docker container. +Ensure that the apps folder is writable by Joe inside the Docker container. ## Reconfigure Joe Bot container Update the configuration file `~/.dblab/joe/configs/joe.yml`. @@ -25,18 +26,18 @@ Restart the running Joe Bot container: sudo docker restart joe_bot ``` -After restart, all user sessions are restored and should keep working (but PostgreSQL connections are re-established so if users set some session variables, they are lost). This feature works only in Joe versions 0.10 and newer. If you need to reset user sessions, stop the container, remove the file `sessions.json` located in `~/.dblab/joe/meta`, and start the container. +After restart, all user sessions are restored and should keep working (but Postgres connections are re-established, so if users set some session variables, they are lost). This feature works only in Joe versions 0.10 and newer. If you need to reset user sessions, stop the container, remove the file `sessions.json` located in `~/.dblab/joe/meta`, and start the container. ## Upgrade Joe Bot -Stop and remove the container using `sudo docker stop joe_bot` and `sudo docker rm joe_bot` and then [launching](#start-joe-bot-container) it again. +Stop and remove the container using `sudo docker stop joe_bot` and `sudo docker rm joe_bot`, and then [launch](#start-joe-bot-container) it again. -After upgrading, all user sessions are restored and should keep working (but PostgreSQL connections are re-established so if users set some session variables, they are lost). This feature works only in Joe versions 0.10 and newer. If you need to reset user sessions, stop the container, remove the file `sessions.json` located in `~/.dblab/joe/meta`, and start the container. +After upgrading, all user sessions are restored and should keep working (but Postgres connections are re-established, so if users set some session variables, they are lost). This feature works only in Joe versions 0.10 and newer. If you need to reset user sessions, stop the container, remove the file `sessions.json` located in `~/.dblab/joe/meta`, and start the container. ## Observe Joe Bot logs To enable the debugging mode you can use one of the following approaches: -- Set the option `app: debug` to `true` in the [configuration file](/docs/reference-guides/joe-bot-configuration-reference#joe-bot-configuration-file). [Reconfigure the container](#reconfigure-the-joe-bot-container) if the option has been changed. -- Alternatively, use the environment variable [`JOE_DEBUG`](/docs/reference-guides/joe-bot-configuration-reference#joe_debug) when starting the container (`docker run ... --env JOE_DEBUG=true ...`). +- Set the option `app: debug` to `true` in the [configuration file](/docs/reference-guides/joe-bot-configuration-reference#joe-bot-configuration-file). [Reconfigure the container](#reconfigure-joe-bot-container) if the option has been changed. +- Alternatively, use the environment variable [`JOE_APP_DEBUG`](/docs/reference-guides/joe-bot-configuration-reference#joe_app_debug) when starting the container (`docker run ... --env JOE_APP_DEBUG=true ...`). To observe the container logs, run: ```bash diff --git a/docs/dblab-howtos/administration/logical-full-refresh.md b/docs/dblab-howtos/administration/logical-full-refresh.md index a2d874a1..dbdbe972 100644 --- a/docs/dblab-howtos/administration/logical-full-refresh.md +++ b/docs/dblab-howtos/administration/logical-full-refresh.md @@ -1,17 +1,40 @@ --- title: How to refresh data when working in the "logical" mode sidebar_label: Full refresh for "logical" mode +description: How to fully refresh data in DBLab Engine when using the "logical" provisioning mode, both manually on a single disk and automatically without downtime. --- -For the "logical" provisioning mode, the "sync" instance is not yet supported (although, it is possible to implement based on logical replication) and the only option to get fresh data on DBLab Engine is to refresh it fully. Follow these instructions to automate this process. Note, that it is designed for ZFS; if you have a different setup, adjust the snippets accordingly. +For the "logical" provisioning mode, the "sync" instance is not yet supported (although it could be implemented based on logical replication), so the only way to get fresh data on DBLab Engine is to refresh it fully. Follow these instructions to automate this process. Note that it is designed for ZFS; if you have a different setup, adjust the snippets accordingly. :::caution -Note, that the process described here requires a maintenance window (brief period of downtime) for the DBLab Engine. Also, the existing clones are deleted and completely lost during the process. It means that the proper planning of the maintenance windows is needed. +Note that the process described here requires a maintenance window (brief period of downtime) for the DBLab Engine. Also, the existing clones are deleted and completely lost during the process. It means that the proper planning of the maintenance windows is needed. ::: If you are using the "physical" provisioning mode, read [how to configure the "sync" instance](/docs/dblab-howtos/administration/postgresql-configuration#the-sync-instance) instead. -## Refresh data from source +## Triggering a full refresh via CLI or API (DBLab Engine 4.0+) + +If you have multiple pools (disks) configured, you can trigger a full refresh without downtime using the CLI or API. DBLab Engine will refresh data on an inactive pool while clones continue to run on the active pool. + +**CLI:** +```bash +dblab instance full-refresh +``` + +**API:** +```bash +curl -X POST -H "Verification-Token: YOUR_TOKEN" http://localhost:2345/full-refresh +``` + +:::tip +A scheduled full refresh can also be configured using the `retrieval.refresh.timetable` option in `server.yml` (crontab format). +::: + +## Manual refresh (single disk) + +The process described below requires a maintenance window and deletes existing clones. + +### Refresh data from source ### 1. Cleanup Stop and remove the existing containers, then clean up the data directory and destroy the pool: ```bash @@ -23,7 +46,7 @@ sudo zpool destroy dblab_pool ``` ### 2. Set $DBLAB_DISK -Further, we will need `$DBLAB_DISK` environment variable. It must contain the device name corresponding to the disk where all the DBLab Engine data will be stored. +Further, we will need the `$DBLAB_DISK` environment variable. It must contain the device name corresponding to the disk where all the DBLab Engine data will be stored. To understand what needs to be specified in `$DBLAB_DISK` in your particular case, check the output of `lsblk`: ```bash @@ -143,7 +166,7 @@ sudo zpool create -f \ ``` ### 3. Define a refresh timetable -Set up a desirable timetable in the `retrieval` section of [your configuration](/docs/dblab-howtos/administration/engine-manage#configure-and-start-a-database-lab-engine-instance) to perform a full refresh automatically +Set up a desirable timetable in the `retrieval` section of [your configuration](/docs/dblab-howtos/administration/engine-manage#configure-and-start-a-dblab-engine-instance) to perform a full refresh automatically ```yaml retrieval: refresh: diff --git a/docs/dblab-howtos/administration/postgresql-configuration.md b/docs/dblab-howtos/administration/postgresql-configuration.md index 852d85a0..d29630f3 100644 --- a/docs/dblab-howtos/administration/postgresql-configuration.md +++ b/docs/dblab-howtos/administration/postgresql-configuration.md @@ -1,32 +1,33 @@ --- title: How to configure PostgreSQL used by DBLab Engine sidebar_label: Configure PostgreSQL used by DBLab Engine +description: How to configure Postgres instances managed by DBLab Engine, including shared_buffers, work_mem, and query planning settings for accurate clones. --- ## PostgreSQL configuration -It is important to properly configure all PostgreSQL instances managed by DBLab Engine: the single "sync" instance, and clones. +It is important to properly configure all Postgres instances managed by DBLab Engine: the single "sync" instance and the clones. ### The "sync" instance -The "sync" instance, which is an asynchronous replica by nature, is currently supported only for the physical mode of data directory initialization, see option `syncInstance` in job [physicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalsnapshot). The only purpose of this PostgreSQL is fetching and replaying [WAL segments](https://www.postgresql.org/docs/current/wal-intro.html), maintaining the data directory in sync. +The "sync" instance, which is an asynchronous replica by nature, is currently supported only for the physical mode of data directory initialization, see option `syncInstance` in job [physicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalsnapshot). The only purpose of this Postgres instance is fetching and replaying [WAL segments](https://www.postgresql.org/docs/current/wal-intro.html), keeping the data directory in sync. -Normally, there is no need in configuring this PostgreSQL instance, as DBLab Engine controls it fully, using a small value for `shared_buffers`, and very reliable values for all the configuration options. The only option that can be controlled by the DBLab Engine administrator is `restore_command` (see [physicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalsnapshot)). +Normally, there is no need to configure this Postgres instance, as DBLab Engine controls it fully, using a small value for `shared_buffers` and very reliable values for all the configuration options. The only option that can be controlled by the DBLab Engine administrator is `restore_command` (see [physicalRestore](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalsnapshot)). ### PostgreSQL configuration in clones :::info For [DBLab SE](https://postgres.ai/docs/dblab-howtos/administration/install-dle-from-postgres-ai), this step is automated – no additional actions are required. ::: -It is possible and in many cases necessary to configure various PostgreSQL options in clones. It can be done both for logical and physical modes of data directory initialization: +It is possible and in many cases necessary to configure various Postgres options in clones. It can be done for both logical and physical modes of data directory initialization: - for the logical mode, all PostgreSQL parameters are to be specified in option `configs` of job `logicalSnapshot` - for the physical mode, these parameters are configured in option `configs` of job `physicalSnapshot` -Technically, the specified PostgreSQL parameters are applied to `postgresql.conf` located in the data directory when preparing a working snapshot used for thin cloning. All thin clones automatically inherit these values. +Technically, the specified Postgres parameters are applied to `postgresql.conf` located in the data directory when preparing a working snapshot used for thin cloning. All thin clones automatically inherit these values. When configuring DBLab Engine, review and set up if needed the following parameters: -- **`shared_buffers`**: one of the most important parameters. The use of the same value that is used on the source is not recommended because it might lead to out-of-memory errors and the inability to create more than a few clones. Instead, use some moderate value such as `1GB`; with this value, if your server has, say, 64 GiB of RAM, then theoretical maximum number of clones is ~63 (some RAM is already used by OS and other apps) -- **`shared_preload_libraries`**: use the same value as on the source, to allow the same extensions that is used there +- **`shared_buffers`**: one of the most important parameters. The use of the same value that is used on the source is not recommended because it might lead to out-of-memory errors and the inability to create more than a few clones. Instead, use some moderate value such as `1GB`; with this value, if your server has, say, 64 GiB of RAM, then the theoretical maximum number of clones is ~63 (some RAM is already used by OS and other apps) +- **`shared_preload_libraries`**: use the same value as on the source, to allow the same extensions that are used there - **`work_mem`**: set the same value as used on the source database unless your DBLab Engine server lacks memory and there are significant risks of out-of-memory errors -- **[Query Planning](https://www.postgresql.org/docs/current/runtime-config-query.html)** parameters (all of them). This is essential to ensure that cloned PostgreSQL most likely generates the same plans as on the source (specifically, it is crutial for query performance troubleshooting and optimization, including working with EXPLAIN plans) +- **[Query Planning](https://www.postgresql.org/docs/current/runtime-config-query.html)** parameters (all of them). This is essential to ensure that the cloned Postgres instance most likely generates the same plans as on the source (specifically, it is crucial for query performance troubleshooting and optimization, including working with EXPLAIN plans) Use the following SQL on the source database to get all non-default values of the parameters affecting the planner's behavior: ```sql @@ -44,7 +45,7 @@ where ``` Additionally, ensure that `shared_preload_libraries` has all extensions that you use (or might start using in the future), plus the following: -- `pg_stat_kcache` and `logerrors` – required for some observability tasks DBLab may need to performance +- `pg_stat_kcache` and `logerrors` – required for some observability tasks DBLab may need to perform - `anon` – required for PII removal, in some cases The mentioned extensions above may not be available in the source – still, it makes sense to have them loaded in DBLab clones. diff --git a/docs/dblab-howtos/administration/run-database-lab-on-mac.md b/docs/dblab-howtos/administration/run-database-lab-on-mac.md index b21cb72e..1c42c4d7 100644 --- a/docs/dblab-howtos/administration/run-database-lab-on-mac.md +++ b/docs/dblab-howtos/administration/run-database-lab-on-mac.md @@ -1,17 +1,22 @@ --- title: How to run DBLab Engine on macOS sidebar_label: Run DBLab on macOS +description: Run DBLab Engine 4.1+ with full ZFS support on Intel or Apple Silicon macOS using Colima, a lightweight Linux VM with Docker. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; This guide explains how to run the DBLab Engine with full ZFS support **on macOS**, using [**Colima**](https://github.com/abiosoft/colima), a lightweight Linux VM with Docker support. -All ZFS operations happen **inside the Colima VM**, so you don't need to install the ZFS module to your macOS. +All ZFS operations happen **inside the Colima VM**, so you don't need to install the ZFS module on macOS. :::note This guide provides an experimental way to run DBLab Engine on macOS. ::: +:::info +This guide applies to both Intel and Apple Silicon Macs. DBLab Engine 4.1 includes the ARM64 / Colima compatibility work needed for this setup, so use DLE 4.1+ and a current Colima release. +::: + ## Prerequisites: Docker, Colima, Go First, install Homebrew, if you don't have it yet: ```bash @@ -23,7 +28,7 @@ Then install Docker, Colima, and Go: brew install docker colima go ``` -To build the DLE binary locally, **Go 1.23 or higher** is required, so let's check Go version: +To build the DBLab Engine binary locally, **Go 1.23 or higher** is required, so let's check the Go version: ```bash go version ``` @@ -48,6 +53,8 @@ colima start --cpu 4 --memory 6 --disk 20 --mount $HOME:w The `--mount $HOME:w` flag makes your home directory accessible inside Colima at `/mnt/host/Users/yourname/...`. +On Apple Silicon, Colima typically uses an ARM64 Linux VM by default. On Intel Macs, it uses AMD64. + ## 3. Initialize ZFS in Colima VM You can either use the provided setup script or run all steps manually if you prefer better control. @@ -136,7 +143,14 @@ exit ## 4. Build engine -Compile DBLab for Linux: +Compile DBLab for Linux using the same architecture as your Colima VM: + +Apple Silicon / ARM64: +```bash +GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o bin/dblab-server ./cmd/database-lab/main.go +``` + +Intel / AMD64: ```bash GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bin/dblab-server ./cmd/database-lab/main.go ``` @@ -182,7 +196,6 @@ docker run \ -v "$(pwd)/configs:/home/dblab/configs:rw" \ -v "$(pwd)/configs/standard:/home/dblab/standard:ro" \ -v "$(pwd)/meta:/home/dblab/meta" \ - --env DOCKER_API_VERSION=1.39 \ -p 2345:2345 \ dblab_server:local ``` @@ -191,11 +204,15 @@ docker run \ ### Open DBLab UI When the main container (`dblab_server`) starts, it launches an additional container with UI, whose name looks like `dblab_embedded_ui_xxx`; it provides UI available at port `2346` by default (can be changed in `server.yml`). +:::warning +Because the UI runs in a *separate* container that binds host port `2346` itself, do **not** add `-p 2346:2346` to the `dblab_server` command above. Publishing `2346` on `dblab_server` makes the UI container fail to start with `Bind for 127.0.0.1:2346 failed: port is already allocated` (the engine then logs `failed to start embedded UI container` and continues without a UI). +::: + In your browser, open [http://127.0.0.1:2346](http://127.0.0.1:2346). You'll see a **"refreshing"** state while the engine initializes. This may take some time; please wait until the refresh is complete. Once it's done, you will be able to create snapshots, branches, and clones. -To learn how to work with DBLab UI, see [DBLab Guides](/docs/how-to-guides). +To learn how to work with DBLab UI, see [DBLab how-to guides](/docs/dblab-howtos). ### Install and configure DBLab CLI ```bash @@ -221,7 +238,7 @@ docker stop dblab_server docker rm -f dblab_server ``` -Ensure no extra containers (UI, Postgres) that were launched by `dblab_server`, are present (if there are, delete them using `docker rm`): +Ensure no extra containers (UI, Postgres) that were launched by `dblab_server` are present (if there are, delete them using `docker rm`): ``` docker ps -a \ --format "ID: {{.ID}}\tName: {{.Names}}\tImage: {{.Image}}\tLabels: {{.Labels}}" \ diff --git a/docs/dblab-howtos/administration/teleport-integration.md b/docs/dblab-howtos/administration/teleport-integration.md new file mode 100644 index 00000000..1e9b62c3 --- /dev/null +++ b/docs/dblab-howtos/administration/teleport-integration.md @@ -0,0 +1,244 @@ +--- +title: Teleport integration +sidebar_label: Teleport integration +description: Integrate DBLab Engine 4.1+ with Teleport for secure, audited, certificate-based access to Postgres database clones with role-based access control. +--- + +DBLab Engine 4.1 includes built-in integration with [Teleport](https://goteleport.com/), enabling secure, audited access to database clones through Teleport's access control. The integration works as a sidecar process that automatically registers and deregisters DBLab clones as Teleport database resources. + +## Architecture + +``` ++--------------+ webhooks +------------------+ tctl +------------------+ +| DBLab Engine |-------------->| dblab teleport |---------->| Teleport Auth | +| (Docker) | | serve (sidecar) | | Server | ++--------------+ +------------------+ +------------------+ + | | + | clone containers | + v v ++--------------+ +------------------+ +------------------+ +| Clone PG |<--------------| Teleport DB Agent|<----------| tsh proxy db | +| (port 6000) | proxied | (db_service) | tunnel | (end user) | ++--------------+ +------------------+ +------------------+ +``` + +The sidecar: +- Receives `clone_create` / `clone_delete` webhooks from DBLab Engine +- Calls `tctl create` / `tctl rm` to register/deregister Teleport DB resources +- Runs startup reconciliation to catch missed events + +The sidecar does **not** proxy database connections. A separate Teleport agent with `db_service` enabled handles the actual proxying. + +## Prerequisites + +### 1. Teleport bot role + +Create a bot role with permissions to manage database resources: + +```yaml +kind: role +version: v7 +metadata: + name: dblab-bot +spec: + allow: + db_labels: + '*': '*' + db_names: ['*'] + db_users: ['*'] + rules: + - resources: [db, db_server] + verbs: [list, create, read, update, delete] + - resources: [app, app_server] + verbs: [list, create, read, update, delete] +``` + +Apply with `tctl create -f dblab-bot-role.yaml`. + +### 2. Teleport bot identity + +Create a bot and generate the identity file. The role from step 1 must already exist before this step. + +**Self-hosted Teleport:** +```bash +tctl bots add dblab-sidecar --roles=dblab-bot +tctl auth sign --format=tls --user=bot-dblab-sidecar -o /etc/teleport/dblab-identity +``` + +**Teleport Cloud:** +```bash +tctl bots add dblab-sidecar --roles=dblab-bot +# Use the token from the output above +tbot start --oneshot \ + --token= \ + --proxy-server=yourcluster.teleport.sh:443 \ + --join-method=token \ + --data-dir=/etc/teleport/bot-data \ + --destination-dir=/etc/teleport/bot-dest +# The identity file is at /etc/teleport/bot-dest/identity +``` + +### 3. Teleport database agent + +A Teleport agent must run on the DBLab host with `db_service` enabled: + +```yaml +# /etc/teleport.yaml (on the DBLab host) +db_service: + enabled: true + resources: + - labels: + dblab: "true" +``` + +### 4. User role for database access + +Teleport users who need to connect to DBLab clones need a role granting database access: + +```yaml +kind: role +version: v7 +metadata: + name: dblab-user +spec: + allow: + db_labels: + dblab: "true" + db_names: ['*'] + db_users: ['*'] +``` + +### 5. SSL/TLS for Postgres clones + +Teleport always initiates TLS to backend databases. DBLab clones must have SSL enabled. + +**Generate self-signed certs:** +```bash +openssl req -new -x509 -days 3650 -nodes \ + -out /etc/dblab/certs/server.crt \ + -keyout /etc/dblab/certs/server.key \ + -subj "/CN=dblab-clone" + +chown 999:999 /etc/dblab/certs/server.crt /etc/dblab/certs/server.key +chmod 600 /etc/dblab/certs/server.key +``` + +**Export the Teleport DB CA certificate:** +```bash +tctl auth export --type=db-client > /etc/dblab/certs/teleport-ca.crt +chown 999:999 /etc/dblab/certs/teleport-ca.crt +``` + +### 6. pg_hba.conf — certificate authentication + +Starting with DBLab Engine 4.1, the default `pg_hba.conf` includes a `hostssl ... cert` rule that enables Teleport certificate authentication out of the box: + +``` +local all all trust +hostssl all all 0.0.0.0/0 cert +host all all 0.0.0.0/0 md5 +``` + +No custom `pg_hba.conf` or volume mount is required for Teleport. + +### 7. Volume mounting for certs + +Clone containers only inherit DBLab Engine container volumes whose source is under `poolManager.mountDir`. For SSL certs stored outside the pool, use `containerConfig`: + +```yaml +databaseContainer: &db_container + dockerImage: "postgresai/extended-postgres:16" + containerConfig: + "shm-size": 1gb + volume: "/etc/dblab/certs:/var/lib/postgresql/cert:ro" +``` + +Cert files on the host must have uid 999 ownership before DBLab Engine starts, because the postgres user inside the container runs as uid 999. + +### 8. Webhook URL — Docker networking + +DBLab Engine runs inside Docker, so `localhost:9876` from within the Engine container resolves to the container itself, not the host. + +Options: +- Use `host.docker.internal:9876` (Docker Desktop / Docker 20.10+) +- Use the Docker bridge IP (typically `172.17.0.1:9876`) +- Run the sidecar in the same Docker network as DBLab Engine + +## Configuration + +### DBLab Engine server.yml + +Add SSL configuration and webhook settings: + +```yaml +databaseContainer: &db_container + dockerImage: "postgresai/extended-postgres:16" + containerConfig: + "shm-size": 1gb + volume: "/etc/dblab/certs:/var/lib/postgresql/cert:ro" + +databaseConfigs: &db_configs + configs: + ssl: "on" + ssl_cert_file: "/var/lib/postgresql/cert/server.crt" + ssl_key_file: "/var/lib/postgresql/cert/server.key" + ssl_ca_file: "/var/lib/postgresql/cert/teleport-ca.crt" + +webhooks: + hooks: + - url: "http://host.docker.internal:9876/teleport-sync" + secret: "your-webhook-secret" + trigger: + - clone_create + - clone_delete +``` + +:::tip +After adding or changing `databaseConfigs`, a data refresh is required. These settings are applied during snapshot creation. Existing snapshots are not affected. +::: + +## Running the sidecar + +```bash +dblab teleport serve \ + --environment-id production \ + --teleport-proxy teleport.example.com:3025 \ + --teleport-identity /etc/teleport/dblab-identity \ + --listen-addr 0.0.0.0:9876 \ + --dblab-url http://localhost:2345 \ + --dblab-token "$DBLAB_TOKEN" \ + --webhook-secret "$WEBHOOK_SECRET" +``` + +See the [CLI reference](/docs/reference-guides/dblab-client-cli-reference#command-teleport) for all available options. + +## Connecting to a clone + +Once everything is running, users connect through Teleport: + +```bash +# Login to Teleport +tsh login --proxy=teleport.example.com + +# List available databases (clones appear automatically) +tsh db ls + +# Connect to a clone +tsh db connect dblab-clone-production--6000 \ + --db-user postgres --db-name postgres + +# Or use a local tunnel (works with any psql client) +tsh proxy db --tunnel dblab-clone-production--6000 +``` + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Clone registered but can't connect | No Teleport DB agent running | Start `teleport` with `db_service.enabled: true` | +| TLS handshake failure | Clone doesn't have SSL enabled | Add `ssl: "on"` + cert paths to `databaseConfigs.configs` | +| "no pg_hba.conf entry" | Missing `hostssl ... cert` entry | Upgrade to DBLab Engine 4.1+ which includes this rule by default | +| "root certificate store not available" | Missing `ssl_ca_file` | Export Teleport DB CA with `tctl auth export --type=db-client` | +| SSL settings not applied to new clones | Snapshot created before SSL config | Trigger a data refresh to create a new snapshot | +| Webhook not received | Docker networking issue | Use `host.docker.internal` or bridge IP for webhook URL | +| Permission denied on cert files | Wrong file ownership | `chown 999:999` on cert files | diff --git a/docs/dblab-howtos/branching/create-branch.md b/docs/dblab-howtos/branching/create-branch.md index a7ef33a8..f4abc265 100644 --- a/docs/dblab-howtos/branching/create-branch.md +++ b/docs/dblab-howtos/branching/create-branch.md @@ -1,6 +1,7 @@ --- title: How to create a database branch sidebar_label: Create a database branch +description: Create a database branch in DBLab Engine from the GUI or the dblab CLI, including branching from a specific parent branch or snapshot ID. --- :::info @@ -13,15 +14,15 @@ DBLab Engine must be version `4.0` or higher. ![Database Lab instance page / Create branch](/assets/guides/create-branch-1.png) 3. Click the **Create branch** button. ![Database Lab instance page / Create branch](/assets/guides/create-branch-2.png) -4. Fill the **Branch name** field with a meaningful name. -5. (optional) Change the **Parent branch** and **Snapshot ID** if needed. +4. Fill in the **Branch name** field with a meaningful name. +5. (Optional) Change the **Parent branch** and **Snapshot ID** if needed. 6. Click the **Create branch** button. ![Database Lab instance page / Create branch](/assets/guides/create-branch-3.png) 7. You will be redirected to the **Database Lab branch** page. ![Database Lab instance page / Create branch](/assets/guides/create-branch-4.png) ## CLI -Before you run any commands, install Database Lab CLI and initialize configuration. For more information, see [Install and initialize Database Lab CLI](/docs/dblab-howtos/cli/cli-install-init). +Before you run any commands, install the DBLab CLI and initialize the configuration. For more information, see [Install and initialize DBLab CLI](/docs/dblab-howtos/cli/cli-install-init). ### Reference - Command [`dblab branch`](/docs/reference-guides/dblab-client-cli-reference#command-branch) @@ -33,7 +34,7 @@ $ dblab branch test ``` ### Create a database branch with a different parent -By default, the created branch will be a child of the current branch. You can specify a different parent branch using `--parent-branch`: +By default, the created branch will be a child of the current branch. If no current branch is set in the local CLI context, DBLab uses `main` as the base branch. You can specify a different parent branch using `--parent-branch`: ```bash $ dblab branch --parent-branch dev test ``` diff --git a/docs/dblab-howtos/branching/delete-branch.md b/docs/dblab-howtos/branching/delete-branch.md index f61c092c..9f3b93e5 100644 --- a/docs/dblab-howtos/branching/delete-branch.md +++ b/docs/dblab-howtos/branching/delete-branch.md @@ -1,6 +1,7 @@ --- title: How to delete a database branch sidebar_label: Delete a database branch +description: Delete a database branch in DBLab Engine from the GUI or with the dblab branch command, freeing up resources when the branch is no longer needed. --- :::info @@ -19,13 +20,13 @@ DBLab Engine must be version `4.0` or higher. ![Database Lab instance page / Delete branch](/assets/guides/delete-branch-3.png) ## CLI -Before you run any commands, install Database Lab CLI and initialize configuration. For more information, see [Install and initialize Database Lab CLI](/docs/dblab-howtos/cli/cli-install-init). +Before you run any commands, install the DBLab CLI and initialize the configuration. For more information, see [Install and initialize DBLab CLI](/docs/dblab-howtos/cli/cli-install-init). ### Reference - Command [`dblab branch`](/docs/reference-guides/dblab-client-cli-reference#command-branch) ### Delete branch -Delete a database branch with `dblab branch` command, using `-d` or `--delete`: +Delete a database branch with the `dblab branch` command, using `-d` or `--delete`: ```bash $ dblab branch -d test ``` diff --git a/docs/dblab-howtos/branching/index.md b/docs/dblab-howtos/branching/index.md index a65be5a3..a842c244 100644 --- a/docs/dblab-howtos/branching/index.md +++ b/docs/dblab-howtos/branching/index.md @@ -2,9 +2,11 @@ title: How to work with database branches sidebar_label: Overview slug: /dblab-howtos/branching +description: Guides for working with database branches in DBLab Engine, including how to create and delete branches and build preview environments with Coolify. --- ## Guides - [How to create a database branch](/docs/dblab-howtos/branching/create-branch) -- [How to delete a database branch](/docs/dblab-howtos/branching/delete-branch) \ No newline at end of file +- [How to delete a database branch](/docs/dblab-howtos/branching/delete-branch) +- [Preview environments with DBLab and Coolify](/docs/dblab-howtos/branching/preview-environments-with-dblab-and-coolify) \ No newline at end of file diff --git a/docs/dblab-howtos/branching/preview-environments-with-dblab-and-coolify.md b/docs/dblab-howtos/branching/preview-environments-with-dblab-and-coolify.md index 0891cd4e..b7360812 100644 --- a/docs/dblab-howtos/branching/preview-environments-with-dblab-and-coolify.md +++ b/docs/dblab-howtos/branching/preview-environments-with-dblab-and-coolify.md @@ -1,17 +1,18 @@ --- title: How to set up full-stack preview environments with DBLab, Coolify and GitHub -sidebar_label: How to set up full-stack preview environments with DBLab, Coolify and GitHub +sidebar_label: Preview environments with Coolify +description: Set up automated full-stack preview environments that provision an isolated Postgres clone with DBLab and deploy with Coolify for every GitHub pull request. --- # How to set up full-stack preview environments with DBLab, Coolify and GitHub -This how-to guide walks you through setting up automated preview environments that create isolated PostgreSQL database clones for each pull request. Each environment runs independently with its own database, allowing safe testing of migrations and data changes. +This how-to guide walks you through setting up automated preview environments that create isolated Postgres database clones for each pull request. Each environment runs independently with its own database, allowing safe testing of migrations and data changes. ## What you'll achieve By the end of this guide, you'll have: - Automatic preview environments for every pull request -- Isolated PostgreSQL database clones using DBLab +- Isolated Postgres database clones using DBLab - Automatic deployment via Coolify - Automatic cleanup when pull requests are closed @@ -19,11 +20,11 @@ By the end of this guide, you'll have: Before starting, ensure you have: -- **DBLab 4.0+** - for database cloning and branching ([installation guide](https://postgres.ai/docs/how-to-guides/administration/install-dle-from-postgres-ai)) +- **DBLab 4.0+** - for database cloning and branching ([installation guide](/docs/dblab-howtos/administration/install-dle-from-postgres-ai)) - **Coolify latest version** - self-hosted deployment platform ([installation guide](https://coolify.io/docs/get-started/installation)) - **GitHub repository** - for code storage and CI/CD - **Virtual machine with Docker** - to run DBLab and Coolify -- **PostgreSQL database** - as source for cloning +- **Postgres database** - as the source for cloning - **Admin access** - to both GitHub repository and Coolify instance --- @@ -32,7 +33,7 @@ Before starting, ensure you have: ### 1.1 Create a new project -Open the Coolify projects page and click `+ Add` button. +Open the Coolify projects page and click the `+ Add` button. ![Coolify projects page](/assets/guides/preview-deployment-1.png) @@ -104,7 +105,7 @@ Before setting up preview environments, you need to configure DBLab with a data ### 2.1 Set up a data source 1. Follow the [DBLab configuration guide](https://postgres.ai/docs/how-to-guides/administration/install-dle-from-postgres-ai) to set up your data source -2. Ensure your PostgreSQL database is accessible from the DBLab instance +2. Ensure your Postgres database is accessible from the DBLab instance 3. Configure the data source in your DBLab server configuration file or in the UI (recommended) --- @@ -385,7 +386,7 @@ jobs: ### 7.3 Test cleanup 1. Close or merge the pull request -2. Verify that cleanup workflow runs successfully +2. Verify that the cleanup workflow runs successfully 3. Check that DBLab resources are removed 4. Verify that Coolify preview deployment is stopped diff --git a/docs/dblab-howtos/cli/cli-install-init.md b/docs/dblab-howtos/cli/cli-install-init.md index 14e9ea43..ee47b1ea 100644 --- a/docs/dblab-howtos/cli/cli-install-init.md +++ b/docs/dblab-howtos/cli/cli-install-init.md @@ -1,15 +1,15 @@ --- title: How to install and initialize DBLab CLI sidebar_label: Install and initialize DBLab CLI +description: Install the DBLab CLI, optionally connect over an SSH tunnel, and initialize it with your instance URL and token to run dblab commands against DBLab Engine. --- - ## Reference - Command [`dblab init`](/docs/reference-guides/dblab-client-cli-reference#command-init) - Command [`dblab instance status`](/docs/reference-guides/dblab-client-cli-reference#subcommand-status-1) ## Install CLI and connect -1. Install Database Lab CLI: +1. Install the DBLab CLI: ```bash curl -sSL dblab.sh | bash ``` @@ -17,25 +17,25 @@ curl -sSL dblab.sh | bash 2. (optional) Connect to DBLab Engine using SSH port forwarding :::note -A Database Lab instance might be running behind firewalls and opening proper ports might be impossible or prohibited. In this case, SSH keys should be on the server with DBLab Engine in order to use this connection option. +A DBLab instance might run behind a firewall where opening the required ports is impossible or prohibited. In this case, the SSH keys must be on the server with DBLab Engine to use this connection option. ::: -In a separate terminal tab launch SSH port forwarding. Use `http://localhost:2344` as URL in the step 3 below. +In a separate terminal tab, launch SSH port forwarding. Use `http://localhost:2344` as the URL in step 3 below. ``` ssh -NTML 2344:localhost:2345 ssh://USERNAME@HOSTNAME:22 -i ~/.ssh/id_rsa ``` -3. Initialize configuration. Use URL and verification token of your instance. Instead of using verification token you can generate and use your personal access token. See details [here](/docs/dblab-howtos/platform/tokens). +3. Initialize the configuration. Use the URL and verification token of your instance. Instead of a verification token, you can generate and use a personal access token. See details [here](/docs/dblab-howtos/platform/tokens). ```bash dblab init --environment-id=ENV_ID --url=URL --token=TOKEN ``` -- `--environment-id` - an arbitrary environment ID of Database Lab instance's API -- `--url` - URL of Database Lab instance's API -- `--token` - verification token of the Database Lab instance to send API requests +- `--environment-id` - an arbitrary environment ID for the DBLab instance's API +- `--url` - URL of the DBLab instance's API +- `--token` - verification token of the DBLab instance used to send API requests -> You can also run [`dblab config`](#command-config) at any time to change your settings or create a new configuration. +> You can also run [`dblab config`](/docs/reference-guides/dblab-client-cli-reference#command-config) at any time to change your settings or create a new configuration. 4. Test your configuration with instance status request `dblab instance status`: ```json @@ -48,5 +48,5 @@ dblab init --environment-id=ENV_ID --url=URL --token=TOKEN } ``` -# Related -- Video: [Basic install and initialization of Database Lab CLI](https://www.youtube.com/watch?v=0En7misx2mg) +## Related +- Video: [Basic install and initialization of DBLab CLI](https://www.youtube.com/watch?v=0En7misx2mg) diff --git a/docs/dblab-howtos/cli/index.md b/docs/dblab-howtos/cli/index.md index 9e0969d5..247088cc 100644 --- a/docs/dblab-howtos/cli/index.md +++ b/docs/dblab-howtos/cli/index.md @@ -2,6 +2,7 @@ title: How to work with DBLab CLI sidebar_label: Overview slug: /dblab-howtos/cli +description: Guides for working with the DBLab CLI, including how to install and initialize the dblab client to manage clones and branches from the command line. --- ## Guides diff --git a/docs/dblab-howtos/cloning/clone-protection.md b/docs/dblab-howtos/cloning/clone-protection.md index 92acdd0a..011af4de 100644 --- a/docs/dblab-howtos/cloning/clone-protection.md +++ b/docs/dblab-howtos/cloning/clone-protection.md @@ -1,53 +1,127 @@ --- title: Protect clones from manual and automatic deletion sidebar_label: Protect clones from manual and automatic deletion +description: Protect DBLab clones from manual and automatic deletion, including time-limited protection leases in DBLab Engine 4.1+, from the GUI, CLI, or API. --- -Database Lab clones can be protected from manual and automatic deletion by enabling the **protected** status of a clone. When enabled no one can delete this clone and automated deletion is also disabled. +DBLab clones can be protected from manual and automatic deletion by enabling the **protected** status of a clone. When enabled, no one can delete the clone, and automatic deletion is also disabled. :::tip -DBLab Engine automatically deletes idle unprotected clones after the idle interval which is defined in the configuration. +DBLab Engine automatically deletes idle unprotected clones after the idle interval defined in the configuration. ::: :::caution -Please be careful: abandoned protected clones may cause out-of-disk-space events. Check disk space on a daily basis and delete protected clones once the work is done. +Be careful: abandoned protected clones may cause out-of-disk-space events. Check disk space daily and delete protected clones once the work is done. ::: +## Protection leases (DBLab Engine 4.1+) + +Starting with DBLab Engine 4.1, clone protection supports **time-limited leases**. Instead of protecting a clone indefinitely, you can specify a duration after which the protection automatically expires. This prevents abandoned protected clones from consuming disk space indefinitely. + +### How it works + +- When a clone is protected with a lease, the `protectedTill` field indicates when protection expires +- After expiration, the clone becomes unprotected and subject to normal idle deletion rules +- The default lease duration and maximum allowed duration are configurable + +### Configuration + +Configure protection lease defaults in the `cloning` section of `server.yml`: + +```yaml +cloning: + accessHost: "localhost" + maxIdleMinutes: 120 + protectionLeaseDurationMinutes: 1440 # Default: 1 day + protectionMaxDurationMinutes: 10080 # Maximum: 7 days + protectionExpiryWarningMinutes: 1440 # Warning webhook 24 hours before expiry +``` + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `protectionLeaseDurationMinutes` | 1440 (1 day) | Default lease duration when `--protected true` is used. `0` means infinite protection. | +| `protectionMaxDurationMinutes` | 10080 (7 days) | Maximum duration users can request. `0` means no limit. | +| `protectionExpiryWarningMinutes` | 1440 (1 day) | Send webhook warning this many minutes before expiry. | + ## GUI From the **Database Lab clone** page enable or disable the **Enable deletion protection** checkbox. ![DBLab Engine page / Create clone](/assets/guides/clone-protection-1.png) ## CLI -Before you run any commands, install Database Lab CLI and initialize configuration. For more information, see [Install and initialize Database Lab CLI](/docs/dblab-howtos/cli/cli-install-init). +Before you run any commands, install the DBLab CLI and initialize the configuration. For more information, see [Install and initialize DBLab CLI](/docs/dblab-howtos/cli/cli-install-init). ### Reference +- Command [`dblab clone create`](/docs/reference-guides/dblab-client-cli-reference#subcommand-create) - Command [`dblab clone update`](/docs/reference-guides/dblab-client-cli-reference#subcommand-update) -### Protect a clone +### Protect a clone with default lease duration ```bash -dblab clone update --protected CLONE_ID +dblab clone update --protected true CLONE_ID ``` +### Protect a clone for a specific duration (in minutes) +```bash +# Protect for 8 hours (480 minutes) +dblab clone update --protected 480 CLONE_ID ``` -{ - "id": "CLONE_ID", - "protected": true, -} + +### Protect a clone indefinitely (no expiry) +```bash +dblab clone update --protected 0 CLONE_ID ``` ### Unprotect a clone ```bash -dblab clone update CLONE_ID +dblab clone update --protected false CLONE_ID +``` + +### Protect at creation time +```bash +# With default lease duration +dblab clone create --username user --password pass --protected true --id my-clone + +# With custom duration (2 hours) +dblab clone create --username user --password pass --protected 120 --id my-clone +``` + +## API + +### Protect with lease duration +```bash +curl -X PATCH \ + -H "Verification-Token: YOUR_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"protected": true, "protectionDurationMinutes": 480}' \ + http://localhost:2345/clone/CLONE_ID ``` +### Create a protected clone +```bash +curl -X POST \ + -H "Verification-Token: YOUR_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "my-clone", + "protected": true, + "protectionDurationMinutes": 1440, + "db": {"username": "user", "password": "pass"} + }' \ + http://localhost:2345/clone ``` + +The response includes `protectedTill` showing when protection expires: +```json { - "id": "CLONE_ID", - "protected": false, + "id": "my-clone", + "protected": true, + "protectedTill": "2027-01-15T14:00:00Z", + "metadata": { + "protectionLeaseDurationMinutes": 1440, + "protectionMaxDurationMinutes": 10080 + } } ``` -Also, clones can marked as protected at creation time. See [Create a clone](/docs/dblab-howtos/cloning/create-clone). - ## Related - Guide: [Destroy a clone](/docs/dblab-howtos/cloning/destroy-clone) +- Reference: [Cloning configuration](/docs/reference-guides/database-lab-engine-configuration-reference#section-cloning-thin-cloning-policies) diff --git a/docs/dblab-howtos/cloning/clone-upgrade.md b/docs/dblab-howtos/cloning/clone-upgrade.md index 0da7abec..0cb77cd8 100644 --- a/docs/dblab-howtos/cloning/clone-upgrade.md +++ b/docs/dblab-howtos/cloning/clone-upgrade.md @@ -1,16 +1,17 @@ --- title: How to perform a Postgres major upgrade in a DBLab clone sidebar_label: Upgrade Postgres in a clone +description: Run an in-place Postgres major version upgrade with pg_upgrade inside a DBLab clone to test a new Postgres release before upgrading production. --- -Here we discuss in-place major upgrades of Postgres inside DBLab clones, which can be very helpful for testing new Postgres versions before upgrading production. Switching to a new Postgres major version for the whole DBLab instance is outside of the scope of this help article. +This guide covers in-place major upgrades of Postgres inside DBLab clones, which is helpful for testing new Postgres versions before upgrading production. Switching to a new Postgres major version for the whole DBLab instance is outside the scope of this article. :::info -DBLab Engine must be version `3.4.0` or higher. Postgres image used by DBLab has to be either "Generic" version `0.3.0` or newer, or "SE" (paid customers) version `0.4.0` or newer. +DBLab Engine must be version `3.4.0` or higher. The Postgres image used by DBLab must be either "Generic" version `0.3.0` or newer, or "SE" (paid customers) version `0.4.0` or newer. ::: :::info -The process described here is semi-automated. Full automation of Postgres upgrades inside clones is not yet supported. Some actions require SSH connection to the server with DBLab Engine. +The process described here is semi-automated. Full automation of Postgres upgrades inside clones is not yet supported. Some actions require an SSH connection to the server with DBLab Engine. ::: ## 1. Create a clone and mark it "protected" @@ -19,10 +20,9 @@ Create a clone [as usual](/docs/dblab-howtos/cloning/create-clone). It is recommended to mark the clone [protected](/docs/dblab-howtos/cloning/clone-protection), so that DBLab Engine does not delete it during database maintenance. Once the clone is created, remember its port. -::: ## 2. Connect to DBLab server using SSH and perform Postgres major upgrade -Perform the following steps to upgrade PostgreSQL inside your clone. +Perform the following steps to upgrade Postgres inside your clone. ### 1. Export the clone port Assuming your clone's port is 6000: @@ -36,7 +36,7 @@ sudo docker exec -it dblab_clone_${DBLAB_CLONE_PORT} bash ``` ### 3. Define necessary variables -Define a bunch of additional environment variables (edit if needed, e.g., `$PG_NEW_VERSION`) +Define additional environment variables (edit if needed, for example `$PG_NEW_VERSION`): ```bash export PG_USER=postgres export PG_NEW_VERSION=17 # target major version @@ -122,7 +122,7 @@ su postgres -c "cd /var/lib/postgresql && /usr/lib/postgresql/${PG_NEW_VERSION}/ --check" ``` -If your Postgres setup is compatible with the new version (you received the message "`Clusters are compatible`"), you can proceed. Otherwise, all reported issues have to be resolved before proceeding. +If your Postgres setup is compatible with the new version (you received the message "`Clusters are compatible`"), you can proceed. Otherwise, resolve all reported issues before proceeding. ### 10. Upgrade Postgres ```bash @@ -190,7 +190,7 @@ Optional (if any), collect statistics for partitioned tables. where relkind = 'p' \gexec ``` -Done! You can exit from container: +Done. You can exit the container: ```bash exit ``` diff --git a/docs/dblab-howtos/cloning/connect-clone.md b/docs/dblab-howtos/cloning/connect-clone.md index b5b88217..bb9553fa 100644 --- a/docs/dblab-howtos/cloning/connect-clone.md +++ b/docs/dblab-howtos/cloning/connect-clone.md @@ -1,20 +1,20 @@ --- title: How to connect to a DBLab clone sidebar_label: Connect to a clone +description: Connect to a DBLab clone with psql or JDBC, directly or over SSH port forwarding, using the connection info from the GUI or the dblab clone status command. --- - ## Direct connection (psql) ### GUI 1. From the **DBLab clone** page under section **Connection info** copy **psql connection string** field contents by clicking the **Copy** button. ![DBLab clone page / psql connection string](/assets/guides/connect-clone-1.png) -1. In terminal type `psql` and paste **psql connection string** field contents. Change the database name `DBNAME` parameter, you can always use `postgres` for the initial connection. -1. Run the command and type the password you've set during clone creation. -1. Test established connection by listing tables in the database with `\d` command. +1. In the terminal, type `psql` and paste the **psql connection string** field contents. Change the database name `DBNAME` parameter; you can always use `postgres` for the initial connection. +1. Run the command and type the password you set during clone creation. +1. Test the established connection by listing tables in the database with the `\d` command. ![Terminal / psql](/assets/guides/connect-clone-2.png) ### CLI -Before you run any commands, install Database Lab CLI and initialize configuration. For more information, see [Install and initialize Database Lab CLI](/docs/dblab-howtos/cli/cli-install-init). +Before you run any commands, install the DBLab CLI and initialize the configuration. For more information, see [Install and initialize DBLab CLI](/docs/dblab-howtos/cli/cli-install-init). #### Reference - Command [`dblab clone status`](/docs/reference-guides/dblab-client-cli-reference#subcommand-status) @@ -44,7 +44,7 @@ dblab clone status CLONE_ID } ``` -2. Connect to the clone using any Postgres client, e.g. psql. Change the database name `DBNAME` parameter, you can always use `postgres` for the initial connection. Type password you've set during clone creation: +2. Connect to the clone using any Postgres client, e.g., psql. Change the database name `DBNAME` parameter; you can always use `postgres` for the initial connection. Type the password you set during clone creation: ```bash psql "host=HOSTNAME port=6000 user=USERNAME dbname=DBNAME" ``` @@ -57,7 +57,7 @@ Type "help" for help. DBNAME=# ``` -3. Test established connection by listing tables in the database with `\d` command. +3. Test the established connection by listing tables in the database with the `\d` command. ## Direct connection (JDBC) 1. From the **Database Lab clone** page under section **Connection info** copy **JDBC connection string** field contents by clicking the **Copy** button. @@ -65,7 +65,7 @@ DBNAME=# 1. Use any Java-based PostgreSQL client to connect. For this guide, we will use [CloudBeaver](https://demo.cloudbeaver.io). Open the client. 1. Click **Connection** / **New connection** / **Custom**. ![CloudBeaver / New connection](/assets/guides/connect-clone-4.png) -1. Select the **URL** radio button and paste **JDBC connection string** field contents to **JDBC URL**. Change the database name `DBNAME` parameter, you can always use `postgres` for the initial connection. Change the password `DBPASSWORD` parameter to the password you've set during clone creation. +1. Select the **URL** radio button and paste **JDBC connection string** field contents to **JDBC URL**. Change the database name `DBNAME` parameter; you can always use `postgres` for the initial connection. Change the password `DBPASSWORD` parameter to the password you set during clone creation. ![CloudBeaver / New connection](/assets/guides/connect-clone-5.png) 1. Test the connection by fetching tables. ![CloudBeaver / Tables](/assets/guides/connect-clone-6.png) @@ -78,24 +78,24 @@ SSH keys need to be on the server with the DBLab Engine to use this connection o ### GUI 1. From the **Database Lab clone** page under section **Connection info** copy **SSH port forwarding** field contents by clicking the **Copy** button. ![Database Lab clone page / SSH port forward](/assets/guides/connect-clone-7.png) -1. In the first tab of terminal start SSH port forwarding using the provided command. Change `USERNAME` to match the username of your SSH key. Change the path to the SSH key if needed. +1. In the first terminal tab, start SSH port forwarding using the provided command. Change `USERNAME` to match the username of your SSH key. Change the path to the SSH key if needed. ![Terminal / SSH port forward](/assets/guides/connect-clone-8.png) 1. From the **Database Lab clone** page under section **Connection info** copy **psql connection string** (will work the same with JDBC). ![Database Lab clone page / psql connection string](/assets/guides/connect-clone-9.png) -1. In the second tab of terminal type `psql` and paste **psql connection string** field contents. Change the database name `DBNAME` parameter, you can always use `postgres` for the initial connection. Make sure that `host=localhost`, as we need to connect to the local port forwarding tunnel. -1. Run the command and type password you've set during clone creation. -1. Test established connection by fetching the list of tables with `\d` command. +1. In the second terminal tab, type `psql` and paste the **psql connection string** field contents. Change the database name `DBNAME` parameter; you can always use `postgres` for the initial connection. Make sure that `host=localhost`, as we need to connect to the local port forwarding tunnel. +1. Run the command and type the password you set during clone creation. +1. Test the established connection by fetching the list of tables with the `\d` command. ![Terminal / psql with port forward](/assets/guides/connect-clone-10.png) ### CLI -Before you run any commands, install Database Lab CLI and initialize configuration. For more information, see [Install and initialize Database Lab CLI](/docs/dblab-howtos/cli/cli-install-init). +Before you run any commands, install the DBLab CLI and initialize the configuration. For more information, see [Install and initialize DBLab CLI](/docs/dblab-howtos/cli/cli-install-init). #### Reference - Command [`dblab clone status`](/docs/reference-guides/dblab-client-cli-reference#subcommand-status) #### Connection -1. In the first tab of terminal start SSH port forwarding using the provided command. Change `USERNAME` to match the username of your SSH key. Change the path to the SSH key if needed. +1. In the first terminal tab, start SSH port forwarding using the provided command. Change `USERNAME` to match the username of your SSH key. Change the path to the SSH key if needed. ```bash ssh -NTML 6000:localhost:6000 ssh://USERNAME@HOSTNAME:22 -i ~/.ssh/id_rsa ``` @@ -123,7 +123,7 @@ dblab clone status CLONE_ID } ``` -2. Connect to the clone using any Postgres client, e.g. psql launched from a second tab. Change the database name `DBNAME` parameter, you can always use `postgres` for the initial connection. Type password you've set during clone creation. Make sure that `host=localhost`, as we need to connect to the local port forwarding tunnel. +2. Connect to the clone using any Postgres client, e.g., psql launched from a second tab. Change the database name `DBNAME` parameter; you can always use `postgres` for the initial connection. Type the password you set during clone creation. Make sure that `host=localhost`, as we need to connect to the local port forwarding tunnel. ```bash psql "host=localhost port=6000 user=USERNAME dbname=DBNAME" ``` @@ -136,7 +136,7 @@ Type "help" for help. DBNAME=# ``` -3. Test established connection by listing tables in the database with `\d` command. +3. Test the established connection by listing tables in the database with the `\d` command. ## Related - Video: [Connect to Database Lab clone through SSH port forwarding](https://www.youtube.com/watch?v=Yq2Kv0-GYXg) diff --git a/docs/dblab-howtos/cloning/create-clone.md b/docs/dblab-howtos/cloning/create-clone.md index de6c9dc1..e1e89225 100644 --- a/docs/dblab-howtos/cloning/create-clone.md +++ b/docs/dblab-howtos/cloning/create-clone.md @@ -1,16 +1,17 @@ --- title: How to create a DBLab clone sidebar_label: Create a clone +description: Create a thin DBLab clone from the GUI or the dblab CLI, including choosing a snapshot or branch, protecting the clone, and setting extra Postgres config. --- ## GUI 1. Go to the **DBLab instance** page. 1. Click the **Create clone** button. ![DBLab Engine page / Create clone](/assets/guides/create-clone-1.png) -1. Fill the **ID** field with a meaningful name. -1. (optional) By default, the latest data snapshot (closest to production state) will be used to provision a clone. You can select any other available snapshot. -1. Fill **database credentials**. Remember the password, it will not be available later, but you will need to use it to connect to the clone. -1. (optional) Enable protected status (it can be done later if needed). Please be careful: abandoned protected clones may cause out-of-disk-space events. Read the details [here](/docs/dblab-howtos/cloning/clone-protection). +1. Fill in the **ID** field with a meaningful name. +1. (optional) By default, the latest data snapshot (closest to production state) is used to provision a clone. You can select any other available snapshot. +1. Fill in the **database credentials**. Remember the password; it will not be available later, but you will need it to connect to the clone. +1. (optional) Enable protected status (it can be done later if needed). Be careful: abandoned protected clones may cause out-of-disk-space events. Read the details [here](/docs/dblab-howtos/cloning/clone-protection). 1. Click the **Create clone** button and wait for a clone to provision. ![DBLab Engine clone creation page](/assets/guides/create-clone-2.png) 1. You will be redirected to the **DBLab clone** page. @@ -24,7 +25,9 @@ Before you run any commands, install DBLab CLI and initialize configuration. For - Command [`dblab snapshot list`](/docs/reference-guides/dblab-client-cli-reference#subcommand-list-1) ### Basic clone creation -Create a clone using `dblab clone create` command. You need to specify the username and password that will be used to connect to the clone. Remember the password, it will not be available later, but you will need to use it to connect to the clone. +Create a clone using the `dblab clone create` command. You need to specify the username and password that will be used to connect to the clone. Remember the password; it will not be available later, but you will need it to connect to the clone. + +Starting with DBLab Engine 4.1, if you do not specify `--branch`, the clone is created from the default branch `main`. ```bash $ dblab clone create --username USERNAME --password PASSWORD --id CLONE_ID @@ -49,13 +52,13 @@ $ dblab clone create --username USERNAME --password PASSWORD --id CLONE_ID ``` ### Create a clone with a non-default snapshot -By default latest data snapshot (closest to production state) will be used to provision a clone. You can select any other available snapshot. +By default, the latest data snapshot (closest to production state) is used to provision a clone. You can select any other available snapshot. -1. List available snapshots. - -```bash -$ dblab snapshot list -``` +1. List available snapshots: + + ```bash + $ dblab snapshot list + ``` ```json [ @@ -94,16 +97,36 @@ $ dblab clone create --username USERNAME --password PASSWORD --id CLONE_ID --sna } ``` +### Create a clone from a branch +:::note +Requires DBLab 4.0 or higher +::: + +DBLab uses `main` as the default branch. Specify `--branch` only when you want a different branch. + +Create a clone from a specific branch: +```bash +$ dblab clone create --username USERNAME --password PASSWORD --id CLONE_ID --branch main +``` + ### Protected status -You can make clone protected during the creation or later (if needed). Please be careful: abandoned protected clones may cause out-of-disk-space events. Read the details [here](/docs/dblab-howtos/cloning/clone-protection). +You can make a clone protected during creation or later. Be careful: abandoned protected clones may cause out-of-disk-space events. Read the details [here](/docs/dblab-howtos/cloning/clone-protection). + +Protect with default lease duration: +```bash +$ dblab clone create --username USERNAME --password PASSWORD --id CLONE_ID --protected true +``` + +Protect for a specific duration (e.g., 8 hours = 480 minutes): ```bash -$ dblab clone create --username USERNAME --password PASSWORD --id CLONE_ID --protected +$ dblab clone create --username USERNAME --password PASSWORD --id CLONE_ID --protected 480 ``` ```json { "id": "democlone", "protected": true, + "protectedTill": "2027-01-15T06:00:00Z", "status": { "code": "OK", "message": "Clone is ready to accept Postgres connections." @@ -112,6 +135,12 @@ $ dblab clone create --username USERNAME --password PASSWORD --id CLONE_ID --pro } ``` +### Extra Postgres configuration +You can set additional Postgres configuration parameters for a clone: +```bash +$ dblab clone create --username USERNAME --password PASSWORD --id CLONE_ID --extra-config statement_timeout='30s' +``` + ## Related - Guide: [Connect to a clone](/docs/dblab-howtos/cloning/connect-clone) - Guide: [Destroy a clone](/docs/dblab-howtos/cloning/destroy-clone) diff --git a/docs/dblab-howtos/cloning/destroy-clone.md b/docs/dblab-howtos/cloning/destroy-clone.md index 72daaa8f..c7292086 100644 --- a/docs/dblab-howtos/cloning/destroy-clone.md +++ b/docs/dblab-howtos/cloning/destroy-clone.md @@ -1,23 +1,23 @@ --- -title: How to destroy a Database Lab clone +title: How to destroy a DBLab clone sidebar_label: Destroy a clone +description: Destroy a DBLab clone from the GUI or with the dblab clone destroy command, synchronously or asynchronously, to free up disk space when work is done. --- :::tip -DBLab Engine automatically deletes idle unprotected clones after the idle interval which is defined in the configuration. To disable auto-deletion for a particular clone, [protect this clone](/docs/dblab-howtos/cloning/clone-protection). +DBLab Engine automatically deletes idle unprotected clones after the idle interval defined in the configuration. To disable auto-deletion for a particular clone, [protect this clone](/docs/dblab-howtos/cloning/clone-protection). ::: :::info -The protected clone could not be deleted automatically or manually. In order to delete the clone, you would need to [unprotect it](/docs/dblab-howtos/cloning/clone-protection). +A protected clone cannot be deleted automatically or manually. To delete it, first [remove protection](/docs/dblab-howtos/cloning/clone-protection). With protection leases (DBLab Engine 4.1+), protection expires automatically after the configured duration. ::: ## GUI -1. On the **Database Lab clone** page click the **Destroy** button. - ![DBLab Engine page / Create clone](/assets/guides/create-clone-1.png) -1. Accept confirmation dialog and wait for it. You will be redirected to the **Database Lab instance** page. +1. On the **Database Lab clone** page, click the **Destroy** button. +1. Accept the confirmation dialog and wait for the operation to complete. You will be redirected to the **Database Lab instance** page. ## CLI -Before you run any commands, install Database Lab CLI and initialize configuration. For more information, see [Install and initialize Database Lab CLI](/docs/dblab-howtos/cli/cli-install-init). +Before you run any commands, install the DBLab CLI and initialize the configuration. For more information, see [Install and initialize DBLab CLI](/docs/dblab-howtos/cli/cli-install-init). ### Reference - Command [`dblab clone destroy`](/docs/reference-guides/dblab-client-cli-reference#subcommand-destroy) @@ -31,6 +31,12 @@ dblab clone destroy CLONE_ID The clone has been successfully destroyed: CLONE_ID ``` +### Destroy a clone asynchronously +For long-running operations, use the `--async` flag: +```bash +dblab clone destroy --async CLONE_ID +``` + ## Related - Guide: [Clone protection from manual and automatic deletion](/docs/dblab-howtos/cloning/clone-protection) - Guide: [Resetting a clone state](/docs/dblab-howtos/cloning/reset-clone) diff --git a/docs/dblab-howtos/cloning/index.md b/docs/dblab-howtos/cloning/index.md index 5a64072a..47dc44db 100644 --- a/docs/dblab-howtos/cloning/index.md +++ b/docs/dblab-howtos/cloning/index.md @@ -2,6 +2,7 @@ title: How to work with DBLab clones sidebar_label: Overview slug: /dblab-howtos/cloning +description: Guides for working with DBLab clones, including how to create, connect to, reset, destroy, protect, and run a Postgres major upgrade in a clone. --- ## Guides diff --git a/docs/dblab-howtos/cloning/reset-clone.md b/docs/dblab-howtos/cloning/reset-clone.md index c8e0ce60..ce8aa414 100644 --- a/docs/dblab-howtos/cloning/reset-clone.md +++ b/docs/dblab-howtos/cloning/reset-clone.md @@ -1,26 +1,27 @@ --- -title: How to reset Database Lab clone's state +title: How to reset a DBLab clone's state sidebar_label: Reset clone's state +description: Reset a DBLab clone to its initial snapshot state from the web UI or the DBLab CLI, discarding all changes made during testing on the clone. --- -With Database Lab clones, you can verify any changes and without any risks for the source database (such as production). +With DBLab clones, you can test any change without risk to the source database (such as production). ## GUI -1. Connect to your clone and execute DDL or DML query – for example, drop some table: +1. Connect to your clone and run a DDL or DML query. For example, drop a table: ![DBLab Engine page / Create clone](/assets/guides/reset-clone-1.png) -1. At the **Database Lab clone page**, click the **Reset** button: +1. On the **DBLab clone page**, click the **Reset** button: ![DBLab Engine page / Create clone](/assets/guides/reset-clone-2.png) -1. Wait for the **OK** status and connect to your clone again. The data will be recovered to the initial state: +1. Wait for the **OK** status and connect to your clone again. The data is restored to its initial state: ![DBLab Engine page / Create clone](/assets/guides/reset-clone-3.png) ## CLI -Before you run any commands, install Database Lab CLI and initialize configuration. For more information, see [Install and initialize Database Lab CLI](/docs/dblab-howtos/cli/cli-install-init). +Before you run any commands, install the DBLab CLI and initialize its configuration. For more information, see [Install and initialize DBLab CLI](/docs/dblab-howtos/cli/cli-install-init). ### Reference - Command [`dblab clone reset`](/docs/reference-guides/dblab-client-cli-reference#subcommand-reset) ### Reset a clone -If you need to reset the clone to the initial state and discard all changes that were done (revert to the snapshot that was used for clone creation): +To reset the clone to its initial state and discard all changes (revert to the snapshot used to create the clone): ```bash dblab clone reset CLONE_ID ``` @@ -30,12 +31,12 @@ Result: The clone has been successfully reset: CLONE_ID ``` -To reset to the latest available snapshot (feature available in DLE version 2.5+) – this is especially useful for long-living clones because you can get the fresh version of data not changing the DB credentials (including port) of your clone: +To reset to the latest available snapshot (feature available in DBLab Engine 2.5+). This is especially useful for long-lived clones, because you get a fresh version of the data without changing the database credentials (including the port) of your clone: ```bash dblab clone reset --latest CLONE_ID ``` -Finally, if you want to reset the clone's state using specific snapshot: +Finally, to reset the clone's state using a specific snapshot: ```bash dblab clone reset --snapshot-id SNAPSHOT_ID CLONE_ID ``` @@ -44,5 +45,10 @@ dblab clone reset --snapshot-id SNAPSHOT_ID CLONE_ID The parameters `--latest` and `--snapshot-id` must not be specified at the same time. ::: +For long-running operations, use the `--async` flag: +```bash +dblab clone reset --async --latest CLONE_ID +``` + ## Related - Guide: [Destroy a clone](/docs/dblab-howtos/cloning/destroy-clone) diff --git a/docs/dblab-howtos/index.md b/docs/dblab-howtos/index.md index a0e965af..9bedc149 100644 --- a/docs/dblab-howtos/index.md +++ b/docs/dblab-howtos/index.md @@ -2,16 +2,20 @@ title: DBLab how-to guides sidebar_label: Overview slug: /dblab-howtos +description: Step-by-step DBLab how-to guides for installing DBLab Engine, cloning and branching Postgres databases, managing snapshots, using Joe bot, and more. --- ## Administration - [How to install DBLab Engine from PostgresAI Console](/docs/dblab-howtos/administration/install-dle-from-postgres-ai) - [How to install DBLab Engine from AWS Marketplace](/docs/dblab-howtos/administration/install-dle-from-aws-marketplace) - [How to install DBLab Engine manually (Community Edition)](/docs/dblab-howtos/administration/install-dle-manually) +- [How to install Database Lab with Terraform on AWS](/docs/dblab-howtos/administration/install-database-lab-with-terraform) +- [How to run DBLab Engine on macOS](/docs/dblab-howtos/administration/run-database-lab-on-mac) - [How to configure PostgreSQL used by DBLab Engine](/docs/dblab-howtos/administration/postgresql-configuration) - [How to manage DBLab Engine](/docs/dblab-howtos/administration/engine-manage) - [How to manage Joe Bot](/docs/dblab-howtos/administration/joe-manage) - [Secure DBLab Engine](/docs/dblab-howtos/administration/engine-secure) +- [Teleport integration](/docs/dblab-howtos/administration/teleport-integration) - [How to refresh data when working in the "logical" mode](/docs/dblab-howtos/administration/logical-full-refresh) - [Masking sensitive data in PostgreSQL logs when using CI Observer](/docs/dblab-howtos/administration/ci-observer-postgres-log-masking) - [Add disk space to ZFS pool without downtime](/docs/dblab-howtos/administration/add-disk-space-to-zfs-pool) @@ -27,6 +31,7 @@ slug: /dblab-howtos ## How to work with DBLab branches - [How to create a database branch](/docs/dblab-howtos/branching/create-branch) - [How to delete a database branch](/docs/dblab-howtos/branching/delete-branch) +- [Preview environments with DBLab and Coolify](/docs/dblab-howtos/branching/preview-environments-with-dblab-and-coolify) ## How to work with DBLab snapshots - [How to create a snapshot](/docs/dblab-howtos/snapshots/create-snapshot) @@ -48,16 +53,22 @@ slug: /dblab-howtos ## Obtaining data for DBLab ### Logical retrieval - [Amazon RDS](/docs/dblab-howtos/administration/data/rds) +- [RDS/Aurora refresh from a temporary clone](/docs/dblab-howtos/administration/data/rds-refresh) - [Any database (dump/restore)](/docs/dblab-howtos/administration/data/dump) - [Full refresh](/docs/dblab-howtos/administration/logical-full-refresh) ### Physical retrieval - [pg_basebackup](/docs/dblab-howtos/administration/data/pg_basebackup) - [WAL-G](/docs/dblab-howtos/administration/data/wal-g) -- [pgBackRest](/docs/dblab-howtos/administration/data/pgBackRest) +- [pgBackRest](/docs/dblab-howtos/administration/data/pgbackrest) +- [rsync](/docs/dblab-howtos/administration/data/rsync) - [Custom](/docs/dblab-howtos/administration/data/custom) +### Shared (logical and physical) +- [Rename databases during snapshot creation](/docs/dblab-howtos/administration/data/database-rename) + ## DBLab (PostgresAI) Platform - [Start using PostgresAI Platform](/docs/dblab-howtos/platform/start-using-platform) - [Create and use DBLab Platform access tokens](/docs/dblab-howtos/platform/tokens) - [DBLab Platform onboarding checklist](/docs/dblab-howtos/platform/onboarding) +- [Audit logs and SIEM integration](/docs/dblab-howtos/platform/audit-logs) diff --git a/docs/dblab-howtos/joe-bot/count-rows.md b/docs/dblab-howtos/joe-bot/count-rows.md index 1f958f93..a6dd2168 100644 --- a/docs/dblab-howtos/joe-bot/count-rows.md +++ b/docs/dblab-howtos/joe-bot/count-rows.md @@ -1,20 +1,21 @@ --- title: How to get row counts for arbitrary SELECTs sidebar_label: Get row counts for arbitrary SELECTs +description: Use Joe bot and EXPLAIN with actual execution to get exact row counts for any SELECT on production-like data, without direct access to the source database. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -One of the good side-effects of using Joe bot is the ability that any EXPLAIN plan with actual execution provides: one can get row counts for any SELECT without having direct access to the data. +A useful side effect of running an EXPLAIN plan with actual execution in Joe bot is that you can get exact row counts for any SELECT without direct access to the data. -This can be useful when you develop or troubleshoot something and need to learn how many rows a query would return in real life (on production). Of course, it makes sense only if your DBLab Engine is set up to work with production-like data. +This helps when you develop or troubleshoot a query and need to know how many rows it would return in real life (on production). This works only when your DBLab Engine is set up to work with production-like data. -To get exact row counts, use the `Actual rows` parameter of the query execution plan which satisfies the specified condition. +To get exact row counts, use the `Actual rows` value in the query execution plan for the node that satisfies your condition. -In the following steps let's assume that we need to answer the question: "How many rows in the table `table1` have `col1 = 1`?" So, our SELECT would be `select * from table1 where col1`. +In the following steps, let's assume we need to answer the question: "How many rows in the table `table1` have `col1 = 1`?" So our SELECT would be `select * from table1 where col1`. -1. Execute `explain select * from table1 where col1 = 1` command to get the query execution plan. The session will start automatically, and a new clone will be created in a few seconds by the DBLab Engine. +1. Run the `explain select * from table1 where col1 = 1` command to get the query execution plan. The session starts automatically, and DBLab Engine creates a new clone within a few seconds. :::tip Notice that using `count(*)` is not really needed – `select * from table1` (or even `select from table1`) is absolutely enough. @@ -44,9 +45,9 @@ Keep in mind that the clone you are working with might be, depending on the sett -2. Open the **full execution plan**. You can get the rows number from the first line. For example, if you see `(actual ... rows=1000)`, it means that 1000 rows match the specified criteria. +2. Open the **full execution plan**. You can read the number of rows from the first line. For example, if you see `(actual ... rows=1000)`, then 1000 rows match the specified criteria. -This recipe may be very useful for quite complex queries. You can benefit from one of the key features of DBLab Engine and Joe bot: your session is fully independent, your work doesn't affect the production performance of your colleague's work, even if the query your use is suboptimal and runs many hours. +This approach is especially useful for complex queries. It relies on one of the key features of DBLab Engine and Joe bot: your session is fully independent, so your work does not affect production performance or your colleagues' work, even if the query is suboptimal and runs for many hours. -3. If you want to stop the execution of a long-running query, run the [`terminate`](/docs/reference-guides/joe-bot-commands-reference#terminate) command with query's `PID` from the [`activity`](/docs/reference-guides/joe-bot-commands-reference#activity) list. +3. To stop the execution of a long-running query, run the [`terminate`](/docs/reference-guides/joe-bot-commands-reference#terminate-pid) command with the query's `PID` from the [`activity`](/docs/reference-guides/joe-bot-commands-reference#activity) list. -4. Check that query was stopped. You can run the [`activity`](/docs/reference-guides/joe-bot-commands-reference#activity) command again or scroll to the query execution message, it should have `terminating connection due to administrator command` status now. +4. Check that the query was stopped. You can run the [`activity`](/docs/reference-guides/joe-bot-commands-reference#activity) command again or scroll to the query execution message; it should now have the `terminating connection due to administrator command` status. -2. Execute [reset](/docs/reference-guides/joe-bot-commands-reference#reset) command. +2. Run the [reset](/docs/reference-guides/joe-bot-commands-reference#reset) command. -3. Wait for ✅ **OK** status. The data will be recovered to the initial state. +3. Wait for the ✅ **OK** status. The data is restored to its initial state. 10 and created > '2019-10-01'; - Comparing to the previous analysis: - **Index Only Scan** instead of Index Scan - Execution Time: *~ 150 ms* – **1000x faster** - - Shared buffers reads: 778 (~6.10 MiB) from the OS file cache, including disk I/O – **218x fewer data** + - Shared buffers reads: 778 (~6.10 MiB) from the OS file cache, including disk I/O – **218x less data** diff --git a/docs/joe-bot/index.md b/docs/joe-bot/index.md index 14ba4011..b5d5633c 100644 --- a/docs/joe-bot/index.md +++ b/docs/joe-bot/index.md @@ -9,26 +9,26 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; ## Summary -Joe is a Postgres query optimization assistant. Joe allows to boost the development process: +Joe is a Postgres query optimization assistant. Joe allows you to boost the development process: - eliminating annoying waiting time needed to provision copies of large databases for development and testing purposes - helping engineers understand details of SQL query performance -Joe works on top of [DBLab Engine](/docs/database-lab/). Every time when an engineer starts communicating with Joe, a new full-size copy of the database is provisioned. +Joe works on top of [DBLab Engine](/docs/database-lab/). Every time an engineer starts communicating with Joe, a new full-size copy of the database is provisioned. -This process is fully automated and takes only a few seconds, even for multi-terabyte databases. Such database copies are called "thin clones" because multiple clones share the same data blocks, so provisioning is super fast, and disk space consumption is very low. The clones are fully independent, so developers can modify databases. Finally, SQL execution plans are identical to production, which makes possible to troubleshoot and optimize queries reliably without involving production databases. +This process is fully automated and takes only a few seconds, even for multi-terabyte databases. Such database copies are called "thin clones" because multiple clones share the same data blocks, so provisioning is super fast, and disk space consumption is very low. The clones are fully independent, so developers can modify databases. Finally, SQL execution plans are identical to production, which makes it possible to troubleshoot and optimize queries reliably without involving production databases. -## Comparison of Database Lab to Other Options of Testing on Large Databases +## Comparison of Database Lab to other options of testing on large databases ![Comparison of Database Lab to Other Options of Testing on Large Databases](/assets/joe/comparison-matrix.png) ## Features - "Serverless EXPLAIN": engineers do not need to worry about the provisioning of independent database clones. The process is fully automated, so all the work looks like requests to analyze some query execution plan or modify database schema – and Joe takes care of it, ensuring that delivered results are identical to production. -- PostgreSQL versions 9.6, 10, 11, and 12 are currently supported. +- PostgreSQL versions 10 through 18 are supported. - Currently, Joe is provided in the form of Slack chatbot. - The provisioning of a new clone takes only a few seconds, regardless of the database size. - Each database clone is fully independent, so developers do not interfere with each other and do not need to wait. - Users do not have direct access to the data, working only with metadata (viewing schema, database sizes, query performance metrics, and execution plans), -- When the `explain` command is used for some query, Joe immediately provides the plan without execution and start executing the query. Once the execution is complete, the detailed execution plan is also provided. +- When the `explain` command is used for some query, Joe immediately provides the plan without execution and starts executing the query. Once the execution is complete, the detailed execution plan is also provided. - The actual timing values may differ from production because actual caches in the Database Lab are usually smaller. However, the structure of plans and the number of bytes and pages/buffers in plans are identical to production thanks to identical planner configuration. - The plans are provided both in JSON and textual forms. - For long-lasting queries, Joe uses @-notification to help understand when the results are ready. @@ -37,12 +37,12 @@ This process is fully automated and takes only a few seconds, even for multi-ter - Developers can reset sessions using the `reset` command, starting from scratch at any time, which allows quick iterations. - Database Lab supports various kinds of Docker images for Postgres, which means that it is possible to use various extensions. - Using the `exec` command one can set or reset any PostgreSQL variables such as `enable_seqscan` or `random_page_cost` (e.g., `exec set random_page_cost to 1;`), controlling planner parameters. -- Each session will be destroyed after the specified amount of minutes of inactivity (configurable on the Database Lab). The corresponding thin clone will be deleted. +- Each session will be destroyed after the specified number of minutes of inactivity (configurable on the Database Lab). The corresponding thin clone will be deleted. - Joe can work with a Database Lab instance, which is constantly updated (being a replica of some Postgres server or consuming WALs from WAL archive). Sophisticated snapshot strategies can be used. In this case, Joe will always use the latest snapshot, reporting its timestamp (`Snapshot data state at`) to users. - Integration with PostgresAI Platform to allow history viewing, plan visualization, and sharing. - SQL optimization knowledge base – a history of Joe sessions, including details of `EXPLAIN` plans, recommendations, various visualization of query plans, and additional meta-data, to support "team memory" and collaboration within particular engineering teams and between various teams/departments in an organization (e.g., between DBA and Development teams). ## Resources - Open-source repository: https://gitlab.com/postgres-ai/joe/ -- Bug reports, ideas, and merge requests are welcome: https://gitlab.com/postgres-ai/joe/issues/ +- Bug reports, ideas, and pull/merge requests are welcome: https://github.com/postgres-ai/joe/issues - To discuss and try Joe Bot, join the Database Lab Community Slack: https://slack.postgres.ai/; after joining, try a live demo of Joe in the `#joe-bot-demo` channel: https://database-lab-team.slack.com/archives/CTL5BB30R diff --git a/docs/monitoring/advanced/architecture.md b/docs/monitoring/advanced/architecture.md index d69beb33..942855b6 100644 --- a/docs/monitoring/advanced/architecture.md +++ b/docs/monitoring/advanced/architecture.md @@ -49,26 +49,26 @@ Deep-dive into PostgresAI monitoring system components and data flow. **Key functions:** - Execute SQL queries against PostgreSQL - Transform results to Prometheus metrics -- Expose `/metrics` endpoint +- Expose the `/pgwatch` metrics endpoint on `:9091` (Prometheus sink) **Configuration:** | Setting | Default | Description | |---------|---------|-------------| -| Scrape interval | 15s | Collection frequency | -| Statement timeout | 30s | Max query duration | -| Max connections | 3 | Connections per database | +| Scrape interval | 30s | VictoriaMetrics scrapes the `pgwatch-prometheus` job every 30s (the 15s global default is overridden for this job; the separate `query-info` job — `metrics_path: /query_info_metrics` — runs every 300s) | +| Collection interval | per-metric | Each metric group has its own interval in `metrics.yml` (most 30s; `pg_stat_activity`/`wait_events` 15s) | **Collected data sources:** | View | Metrics | |------|---------| | pg_stat_statements | Query performance | -| pg_stat_activity | Session state, wait events | -| pg_stat_user_tables | Table access patterns | -| pg_stat_user_indexes | Index usage | -| pg_stat_database | Database-level stats | -| pg_stat_bgwriter | Checkpoint behavior | +| pg_stat_activity | Session state | +| wait_events (from pg_stat_activity) | Wait event sampling | +| pg_stat_all_tables / table_stats | Table access patterns | +| pg_stat_all_indexes | Index usage | +| db_stats (from pg_stat_database) | Database-level stats | +| bgwriter | Checkpoint behavior | -### VictoriaMetrics — Time-series database +### VictoriaMetrics — time-series database **Purpose:** Store and query metrics @@ -86,15 +86,11 @@ Deep-dive into PostgresAI monitoring system components and data flow. | High availability | Built-in clustering | Federation | **Storage model:** -``` -Data directory structure: -/var/lib/victoriametrics/ -├── data/ -│ ├── small/ # Recent data (in-memory) -│ └── big/ # Historical data (on-disk) -├── indexdb/ # Label indexes -└── snapshots/ # Point-in-time backups -``` + +In this deployment VictoriaMetrics is started with `-storageDataPath=/victoria-metrics-data`, and +the `victoria_metrics_data` Docker volume is mounted at `/victoria-metrics-data`. The on-disk +layout under that path follows VictoriaMetrics' standard structure (recent vs. historical data +parts, a label index, and optional snapshots). ### Grafana — Visualization @@ -121,6 +117,7 @@ PostgresAI dashboards: ├── 11. Single index (index deep-dive) ├── 12. SLRU (cache stats) ├── 13. Lock contention (lock waits) +├── 14. I/O statistics (pg_stat_io, PG16+) └── Self-monitoring (stack health) ``` @@ -137,13 +134,13 @@ PostgresAI dashboards: └── Column values → metric values └── Column names → labels -3. Metrics exposed on /metrics endpoint +3. Metrics exposed on the `/pgwatch` endpoint (`:9091`) └── Prometheus exposition format └── Timestamp attached -4. VictoriaMetrics scrapes pgwatch - └── HTTP GET /metrics - └── Configurable interval (default 15s) +4. VictoriaMetrics scrapes the pgwatch-prometheus sink + └── HTTP GET pgwatch-prometheus:9091/pgwatch + └── `pgwatch-prometheus` job scrape_interval: 30s (scrape_timeout 25s) 5. Metrics stored in VictoriaMetrics └── Compressed time-series storage @@ -174,20 +171,32 @@ PostgresAI dashboards: ### Convention +pgwatch exports series as `pgwatch__`. The Prometheus metric **type** is +driven by each metric group's `gauges:` list in `config/pgwatch-prometheus/metrics.yml`: a column +is emitted as a Prometheus gauge only if its group lists it (or uses `gauges: ['*']`); otherwise it +is emitted as a counter. Note this is the **exported** type, not the PostgreSQL semantics — the +`db_stats` and `pg_stat_statements` groups use `gauges: ['*']` / explicit gauge lists, so their +cumulative columns (e.g. `xact_commit`, `exec_time_total`) are exported as **gauges** even though +they only ever increase. Cumulative columns in the `pg_stat_database` family are also **not** +`_total`-suffixed. + ``` -{source}_{view}_{metric}_{unit}_{type} +pgwatch__ -Examples: -pg_stat_database_xact_commit_total # Counter -pg_stat_database_numbackends # Gauge -pg_stat_statements_total_exec_time_seconds # Counter +Examples (Type = the exporter's emitted Prometheus type): +pgwatch_db_stats_xact_commit # Gauge (transactions committed; db_stats uses gauges: ['*']) +pgwatch_db_stats_numbackends # Gauge (current backends) +pgwatch_pg_stat_statements_exec_time_total # Gauge (total exec time, ms; listed in pg_stat_statements gauges) ``` ### Labels +The cluster label is `cluster` (set from `custom_tags.cluster`). `cluster_name` is only the +Grafana template variable; dashboard filters select with `cluster="$cluster_name"`. + ``` -{metric_name}{ - cluster_name="production", +pgwatch__{ + cluster="production", node_name="primary", datname="myapp", schemaname="public", @@ -209,7 +218,7 @@ Typical values: Example: 5 databases, 14-day retention = 5 × 100 × 1.5 × 1,209,600 -= ~900 MiB += 907,200,000 bytes ≈ 907 MB (≈ 865 MiB) ``` ### Scaling factors @@ -288,8 +297,8 @@ PostgresAI monitoring collects **only database metadata** — no actual data or Review exactly what is collected: -- **Prometheus metrics**: [pgwatch-prometheus/metrics.yml](https://gitlab.com/postgres-ai/postgresai/-/blob/0.14.0/config/pgwatch-prometheus/metrics.yml) -- **PostgreSQL metrics** (with query texts): [pgwatch-postgres/metrics.yml](https://gitlab.com/postgres-ai/postgresai/-/blob/0.14.0/config/pgwatch-postgres/metrics.yml) +- **Prometheus metrics**: [pgwatch-prometheus/metrics.yml](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-prometheus/metrics.yml) +- **PostgreSQL metrics** (with query texts): [pgwatch-postgres/metrics.yml](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-postgres/metrics.yml) ### Verify monitoring database role and its permissions @@ -315,13 +324,14 @@ npx postgresai@latest prepare-db --print-sql │ Internal Network │ │ │ │ │ ┌──────────────────────▼─────────────────────────┐ │ -│ │ VictoriaMetrics │ │ -│ │ (port 8428 internal) │ │ +│ │ VictoriaMetrics (sink-prometheus) │ │ +│ │ (port 9090 internal, host 59090) │ │ │ └──────────────────────┬─────────────────────────┘ │ -│ │ │ +│ scrapes pgwatch-prometheus:9091/pgwatch │ │ ┌──────────────────────▼─────────────────────────┐ │ -│ │ pgwatch │ │ -│ │ (port 8080 internal) │ │ +│ │ pgwatch (pgwatch-postgres, pgwatch-prometheus)│ │ +│ │ metrics scraped on pgwatch-prometheus:9091 │ │ +│ │ (web/health ports 8080/8089 are internal) │ │ │ └──────────────────────┬─────────────────────────┘ │ │ │ │ └─────────────────────────┬───────────────────────────┘ @@ -348,12 +358,15 @@ npx postgresai@latest prepare-db --print-sql ### Collection overhead +Frequencies below are the `full` preset intervals from `config/pgwatch-prometheus/metrics.yml` +(see the per-metric note above): + | Metric type | Query cost | Frequency | |-------------|------------|-----------| -| pg_stat_database | Low | 15s | -| pg_stat_statements | Medium | 15s | -| pg_stat_user_tables | Medium | 60s | -| Bloat estimation | High | 300s | +| pg_stat_database (`db_stats`) | Low | 30s | +| pg_stat_statements | Medium | 30s | +| pg_stat_all_tables / `table_stats` | Medium | 30s | +| Bloat estimation (`pg_table_bloat`, `pg_btree_bloat`) | High | 7200s (2h) | ### Query performance diff --git a/docs/monitoring/advanced/index.md b/docs/monitoring/advanced/index.md index 7635f715..b72854a3 100644 --- a/docs/monitoring/advanced/index.md +++ b/docs/monitoring/advanced/index.md @@ -14,6 +14,8 @@ Advanced configuration and integration guides for PostgresAI monitoring. |-------|-------------| | [Multi-cluster monitoring](/docs/monitoring/advanced/multi-cluster) | Centralized monitoring for multiple clusters | | [Architecture](/docs/monitoring/advanced/architecture) | Deep-dive into system components | +| [Security](/docs/monitoring/advanced/security) | VictoriaMetrics auth, credential rotation, hardening | +| [Telemetry](/docs/monitoring/advanced/telemetry) | What the monitoring telemetry reporter sends, and how to disable it | ## When to use advanced features diff --git a/docs/monitoring/advanced/multi-cluster.md b/docs/monitoring/advanced/multi-cluster.md index 6bac4aba..d6cf0cf9 100644 --- a/docs/monitoring/advanced/multi-cluster.md +++ b/docs/monitoring/advanced/multi-cluster.md @@ -8,11 +8,12 @@ sidebar_position: 2 Centralized monitoring for multiple PostgreSQL clusters from a single Grafana instance. -## Architecture options +## Architecture -### Option 1: Single pgwatch, multiple targets - -Best for: 5-20 clusters in the same network +The stack runs one pair of pgwatch collectors (`pgwatch-postgres` and `pgwatch-prometheus`) that +read a list of monitored databases from a generated `sources.yml`, write metrics to +VictoriaMetrics (the `sink-prometheus` service, internal port `9090`, host port `59090`), and +expose them in Grafana. ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ @@ -21,78 +22,97 @@ Best for: 5-20 clusters in the same network │ │ │ └───────────────────┼───────────────────┘ │ - ┌──────▼──────┐ - │ pgwatch │ - └──────┬──────┘ - │ - ┌──────▼────────┐ - │VictoriaMetrics│ - └──────┬────────┘ + ┌──────────▼───────────┐ + │ pgwatch-prometheus │ (reads sources.yml, + │ pgwatch-postgres │ generated from instances.yml) + └──────────┬───────────┘ + │ prometheus sink :9091/pgwatch + ┌──────────▼───────────┐ + │ VictoriaMetrics │ sink-prometheus :9090 (host 59090) + └──────────┬───────────┘ │ ┌──────▼──────┐ - │ Grafana │ + │ Grafana │ :3000 └─────────────┘ ``` -### Option 2: Distributed pgwatch, central storage +## Configuration -Best for: Clusters in different networks/regions +Monitored databases are defined in `instances.yml` (a YAML list). The +`config/scripts/generate-pgwatch-sources.sh` script renders this at runtime into the two +`sources.yml` files that pgwatch reads — `pgwatch/sources.yml` and `pgwatch-prometheus/sources.yml` +under the `/postgres_ai_configs` volume (i.e. `/postgres_ai_configs/pgwatch/sources.yml` and +`/postgres_ai_configs/pgwatch-prometheus/sources.yml`). These generated files are **not** committed +to the repository. There is **no** `PW_TARGETS` (or any `PW_*`) environment variable. -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ Cluster A │ │ Cluster B │ │ Cluster C │ -│ + pgwatch │ │ + pgwatch │ │ + pgwatch │ -└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ - │ │ │ - └───────────────────┼───────────────────┘ - │ remote_write - ┌──────▼────────┐ - │VictoriaMetrics│ - │ (central) │ - └──────┬────────┘ - │ - ┌──────▼──────┐ - │ Grafana │ - └─────────────┘ +### Adding clusters + +**CLI approach (recommended):** + +```bash +# Add a target. The second positional argument is the instance name (optional). +postgresai mon targets add postgresql://user:pass@prod-us:5432/postgres production-us +postgresai mon targets add postgresql://user:pass@prod-eu:5432/postgres production-eu ``` -## Configuration +`mon targets add` takes `[connStr]` and an optional positional `[name]` — there is no +`--cluster-name` flag. The connection string is parsed for user/password/host/port/database only; +cluster identity is **not** read from a `?cluster_name=...` query parameter. After adding a target, +the CLI regenerates `sources.yml` and applies it. -### Adding multiple clusters +**instances.yml approach:** -**docker-compose.yml approach:** +Each entry is a YAML object. Cluster identity is set through the `cluster` key under +`custom_tags:` (the default is `cluster: local` in demo mode, `cluster: default` for +CLI-added targets): ```yaml -services: - pgwatch: - environment: - # Use environment variable substitution for credentials - PW_TARGETS: | - postgresql://${PGWATCH_USER}:${PGWATCH_PASSWORD}@cluster-a:5432/postgres?cluster_name=cluster-a - postgresql://${PGWATCH_USER}:${PGWATCH_PASSWORD}@cluster-b:5432/postgres?cluster_name=cluster-b - postgresql://${PGWATCH_USER}:${PGWATCH_PASSWORD}@cluster-c:5432/postgres?cluster_name=cluster-c +- name: production-us + conn_str: postgresql://user:pass@prod-us:5432/postgres + preset_metrics: full + custom_metrics: + is_enabled: true + group: default + custom_tags: + env: production + cluster: production-us # <-- this becomes the `cluster` metric label + node_name: prod-us-primary + +- name: production-eu + conn_str: postgresql://user:pass@prod-eu:5432/postgres + preset_metrics: full + custom_metrics: + is_enabled: true + group: default + custom_tags: + env: production + cluster: production-eu + node_name: prod-eu-primary +``` + +When you edit `instances.yml` by hand, the change does not take effect until you re-render the +generated `sources.yml` files and restart the collectors so they reload them: + +```bash +postgresai mon update-config # runs sources-generator to re-render sources.yml +postgresai mon restart pgwatch-postgres +postgresai mon restart pgwatch-prometheus ``` +`mon update-config` only re-renders the files (it does **not** restart the collectors), and +`mon restart` only restarts the collectors (it does **not** re-render the files) — you need both. +(The CLI `mon targets add` / `mon targets remove` path does this for you automatically: it +re-renders the sources and recreates the collectors.) + :::tip Security -Define `PGWATCH_USER` and `PGWATCH_PASSWORD` in your `.env` file or use Docker secrets for production deployments. +Keep credentials in `instances.yml` out of version control. The stack's `.env` file holds stack +secrets (such as `REPLICATOR_PASSWORD` and `VM_AUTH_USERNAME` / `VM_AUTH_PASSWORD`), not the +monitored-database role passwords. ::: -**CLI approach:** - -```bash -# Add clusters one at a time -postgresai mon add-target \ - --cluster-name "production-us" \ - postgresql://user@prod-us:5432/postgres - -postgresai mon add-target \ - --cluster-name "production-eu" \ - postgresql://user@prod-eu:5432/postgres -``` - ### Cluster naming conventions -Use consistent, descriptive names: +Use consistent, descriptive values for the `cluster` custom tag: | Pattern | Example | Use case | |---------|---------|----------| @@ -100,84 +120,32 @@ Use consistent, descriptive names: | app-env | orders-prod | Per-application | | team-purpose | platform-analytics | Per-team | -```bash -# Good ---cluster-name="production-us-east-1" - -# Avoid - too generic ---cluster-name="db1" -``` - -## Distributed collection - -### Remote write configuration - -Each pgwatch instance writes to central VictoriaMetrics: - -```yaml -# pgwatch config at each site -remote_write: - url: https://central-vm.example.com/api/v1/write - basic_auth: - username: pgwatch - password: ${REMOTE_WRITE_PASSWORD} # Use environment variable - tls_config: - insecure_skip_verify: false -``` - -:::warning Security -Never commit plaintext passwords. Use environment variables or a secrets manager. -::: - -### Authentication - -Use unique credentials per pgwatch instance: - -```yaml -# Central VictoriaMetrics -basic_auth_users: - - username: pgwatch-us-east - password: # Generate with: htpasswd -nbB pgwatch-us-east - - username: pgwatch-eu-west - password: -``` - -### Network considerations - -| Requirement | Configuration | -|-------------|---------------| -| Firewall | Allow outbound 8428 from pgwatch | -| TLS | Use HTTPS for remote write | -| Compression | Enable gzip (`remote_write.compress: true`) | -| Buffering | Configure local queue for network failures | - ## Label strategy ### Required labels -Every metric should include: +Every metric carries (via pgwatch and `custom_tags`): | Label | Purpose | Example | |-------|---------|---------| -| cluster_name | Primary identifier | `production-us` | -| node_name | Primary/replica distinction | `primary`, `replica-1` | -| datname | Database name | `orders` | +| `cluster` | Primary cluster identifier (from `custom_tags.cluster`) | `production-us` | +| `node_name` | Primary/replica distinction (from `custom_tags.node_name`) | `prod-us-primary` | +| `datname` | Database name | `orders` | -### Optional labels +Note: the metric **label** is `cluster`. `cluster_name` is only the name of the Grafana template +variable; dashboard filters select with `cluster="$cluster_name"`. -| Label | Purpose | Example | -|-------|---------|---------| -| region | Geographic region | `us-east-1` | -| environment | env classification | `production`, `staging` | -| team | Ownership | `platform` | +### Extra labels -### Adding external labels +Add any extra labels per instance via additional keys under `custom_tags:` (for example `env`, +`region`, or `team`). There is no `external_labels:` configuration key in this stack. ```yaml -# pgwatch config -external_labels: +custom_tags: + cluster: production-us + node_name: prod-us-primary region: us-east-1 - environment: production + env: production team: platform ``` @@ -185,99 +153,38 @@ external_labels: ### Cluster selector variable -All dashboards include a `cluster_name` variable: +Dashboards include a `cluster_name` template variable populated from the `cluster` label: -```yaml -# Variable definition +```text +# Grafana template variable name: cluster_name -query: label_values(pg_stat_database_xact_commit_total, cluster_name) -multi: true -include_all: true +query: label_values(pgwatch_db_size_size_b, cluster) ``` ### Cross-cluster queries -**Compare metrics across clusters:** +**Compare TPS across clusters** (the metric is `pgwatch_db_stats_xact_commit`; there is no +`_total`-suffixed pg_stat_database series): ```promql -# TPS comparison -sum by (cluster_name) ( - rate(pg_stat_database_xact_commit_total[5m]) +sum by (cluster) ( + rate(pgwatch_db_stats_xact_commit[5m]) ) ``` -**Alert on any cluster:** +**Connection saturation per cluster** (current backends come from +`pgwatch_db_stats_numbackends`; `max_connections` from the `settings` metric as +`pgwatch_settings_numeric_value{setting_name="max_connections"}` — there is no +`pgwatch_settings_max_connections` series): ```promql -# Alert if any cluster has high connection usage -max by (cluster_name) ( - pg_stat_database_numbackends / pg_settings_max_connections +max by (cluster) ( + sum by (cluster) (pgwatch_db_stats_numbackends) + / + scalar(max(pgwatch_settings_numeric_value{setting_name="max_connections"})) ) > 0.8 ``` -### Cluster overview dashboard - -Create a dashboard showing all clusters: - -```promql -# Cluster health summary -# Status: 1 = healthy, 0 = issues - -( - # Connection health - (pg_stat_database_numbackends / pg_settings_max_connections < 0.8) - and - # Recent activity - (time() - pg_stat_database_stats_reset < 3600) -) -# Note: For replication health, create a separate alert: -# pg_replication_lag_seconds > 60 -``` - -## High availability - -### Redundant pgwatch - -Run multiple pgwatch instances for HA: - -```yaml -services: - pgwatch-1: - environment: - PW_INSTANCE_ID: pgwatch-1 - PW_HA_MODE: active-passive - PW_HA_PEERS: pgwatch-1:8080,pgwatch-2:8080 - - pgwatch-2: - environment: - PW_INSTANCE_ID: pgwatch-2 - PW_HA_MODE: active-passive - PW_HA_PEERS: pgwatch-1:8080,pgwatch-2:8080 -``` - -### VictoriaMetrics cluster - -For large deployments, use VictoriaMetrics cluster mode: - -```yaml -services: - vmstorage-1: - image: victoriametrics/vmstorage - vmstorage-2: - image: victoriametrics/vmstorage - - vminsert: - image: victoriametrics/vminsert - command: - - -storageNode=vmstorage-1:8400,vmstorage-2:8400 - - -replicationFactor=2 - - vmselect: - image: victoriametrics/vmselect - command: - - -storageNode=vmstorage-1:8401,vmstorage-2:8401 -``` - ## Scaling considerations ### Metrics volume @@ -292,48 +199,40 @@ services: ### Storage planning ``` -Storage per cluster = (metrics/sec) × 4 bytes × retention_seconds # VictoriaMetrics compressed +Storage per cluster = (metrics/sec) × ~4 bytes × retention_seconds # VictoriaMetrics, compressed Example: 10 clusters, 30-day retention -= 10 × 100 × 100 × 30 × 86400 -= ~260 GiB += 10 × 100 × 4 × 30 × 86400 += 10,368,000,000 bytes +≈ 10 GiB ``` +Retention is controlled by `VM_RETENTION_PERIOD` (default `336h` = 14 days). + ## Troubleshooting ### Cluster not appearing -1. Check pgwatch logs for connection errors -2. Verify cluster_name is set in connection string -3. Check VictoriaMetrics is receiving data: +1. Check pgwatch logs for connection errors: + ```bash + docker compose logs pgwatch-postgres pgwatch-prometheus + ``` +2. Verify the `cluster` custom tag is set for the target in `instances.yml`. +3. Check VictoriaMetrics is receiving data (host port `59090`, VM basic auth required): ```bash - curl 'http://localhost:8428/api/v1/query?query=up{cluster_name="missing-cluster"}' + curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=pgwatch_db_size_size_b{cluster="missing-cluster"}' ``` ### Mixed-up metrics -Symptoms: Metrics from one cluster appearing under another - -Cause: Duplicate cluster_name labels - -Solution: Ensure unique cluster_name per connection: -```bash -grep -r "cluster_name" /etc/pgwatch/ -``` +Symptoms: metrics from one cluster appearing under another. -### High latency for remote clusters - -1. Enable compression: - ```yaml - remote_write: - compress: true - ``` - -2. Increase batch size: - ```yaml - remote_write: - queue_config: - max_samples_per_send: 5000 - ``` +Cause: duplicate `cluster` custom-tag values across targets. -3. Consider regional VictoriaMetrics instances with federation +Solution: ensure a unique `cluster` value per target in `instances.yml`, then regenerate sources +with `postgresai mon update-config` (which runs `sources-generator` to re-render `sources.yml`) and +restart the collectors so they reload the file: `postgresai mon restart pgwatch-postgres` and +`postgresai mon restart pgwatch-prometheus`. (`mon restart` alone only runs `docker compose restart` +and does **not** re-render `sources.yml`; `update-config` re-renders the file but does **not** +restart the collectors — you need both.) diff --git a/docs/monitoring/advanced/security.md b/docs/monitoring/advanced/security.md new file mode 100644 index 00000000..b8a7fcb4 --- /dev/null +++ b/docs/monitoring/advanced/security.md @@ -0,0 +1,90 @@ +--- +title: Monitoring security +sidebar_label: Security +sidebar_position: 3 +keywords: + - "PostgresAI monitoring security" + - "VictoriaMetrics basic auth" + - "credential rotation" + - "monitoring at rest encryption" +--- + +# Monitoring security + +Security model and hardening options for the self-hosted monitoring stack. Several of these +were added or made required in 0.15. + +## Monitoring database access + +The monitoring role created by `prepare-db` has **read-only access to metadata only** — system +statistics, normalized query text, and object sizes. It never reads table data or query +parameter values. To review the exact SQL before running it: + +```bash +npx postgresai@latest prepare-db --print-sql +``` + +See [Permissions](/docs/monitoring/troubleshooting/permissions) and +[System requirements](/docs/monitoring/getting-started/requirements#permissions) for the full +permission breakdown, and +[Rotate monitoring database credentials](/docs/monitoring/troubleshooting/permissions#rotate-monitoring-database-credentials) +for rotating the monitored-database role's password. + +## VictoriaMetrics basic auth + +New in 0.15, the VictoriaMetrics endpoint is protected with HTTP basic auth. Two `.env` keys +are required: + +```bash +VM_AUTH_USERNAME=vmauth +VM_AUTH_PASSWORD= +``` + +These credentials guard the metrics endpoint and are also used by Grafana's provisioned +datasource — if they are missing, Grafana cannot query VictoriaMetrics. The CLI generates and +preserves them automatically; manual Docker Compose users must set them before +`docker compose up -d`. + +Full details, including what they protect and why they are required, are in +[Authentication and security](/docs/monitoring/configuration/prometheus-config#authentication-and-security). + +### Rotating VictoriaMetrics credentials + +```bash +# From the monitoring directory +VM_AUTH_PASSWORD="$(openssl rand -base64 18)" ./scripts/rotate-vm-auth.sh +``` + +This regenerates the VictoriaMetrics basic-auth credentials and re-applies the Grafana +datasource so the new password takes effect. See +[Rotating VictoriaMetrics credentials](/docs/monitoring/configuration/prometheus-config#rotating-victoriametrics-credentials). + +## Grafana + +- Change the default Grafana admin password immediately after first login (default user + `monitor`; the default password is intended for demo use only). +- Put Grafana behind a TLS-terminating reverse proxy for any internet-facing deployment — see + [Network requirements](/docs/monitoring/getting-started/requirements#self-managed-installation). +- The bundled-version update-check banner is disabled by default in 0.15 (no phone-home on a + pinned-version stack). + +See [Grafana configuration](/docs/monitoring/configuration/grafana-config) for authentication +options (anonymous access, LDAP, OAuth/OIDC). + +## Supply-chain hardening + +All stack images are version-pinned (no `:latest`) for reproducible, auditable deployments. +See [Image tags](/docs/monitoring/getting-started/installation-docker#image-tags-and-supply-chain). + +## Encryption at rest + +For self-hosted deployments, encryption at rest is provided by the underlying storage you run +the stack on (for example, an encrypted volume / filesystem for the Docker volumes that hold +VictoriaMetrics and Grafana data). PostgresAI's own hosted infrastructure uses +KMS-backed encrypted storage validated by infrastructure checks in CI. + +## Related + +- [Authentication and security (VictoriaMetrics)](/docs/monitoring/configuration/prometheus-config#authentication-and-security) +- [Telemetry](/docs/monitoring/advanced/telemetry) — what the monitoring telemetry reporter sends +- [Architecture](/docs/monitoring/advanced/architecture) — component and credential overview diff --git a/docs/monitoring/advanced/telemetry.md b/docs/monitoring/advanced/telemetry.md new file mode 100644 index 00000000..92528659 --- /dev/null +++ b/docs/monitoring/advanced/telemetry.md @@ -0,0 +1,72 @@ +--- +title: Monitoring telemetry +sidebar_label: Telemetry +sidebar_position: 4 +keywords: + - "PostgresAI monitoring telemetry" + - "telemetry reporter" + - "monitoring instance telemetry" +--- + +# Monitoring telemetry + +New in 0.15. Self-hosted monitoring instances include an optional **telemetry reporter** that +periodically sends a small operational health snapshot of the monitoring host to the PostgresAI +Console. This helps surface monitoring-stack health (for example, an out-of-memory event or a +container in a crash loop) without requiring access to the host. + +This is separate from the metrics collected about your PostgreSQL databases, which always stay +in your own VictoriaMetrics instance. + +## What it sends + +Each report contains a small, fixed set of host-level operational signals: + +| Field | Description | +|-------|-------------| +| OOM count (last 24h) | Number of kernel out-of-memory events observed | +| Faulty containers | Names of monitoring-stack containers that are unhealthy or restarting | +| Free RAM | Free memory on the monitoring host, in bytes | +| Free disk | Free disk space on the monitored path (default `/`), in bytes | +| Collected-at timestamp | When the snapshot was taken | + +The reporter identifies the monitoring instance by its UUID. It does **not** send PostgreSQL +data, query text, or table contents. + +## Enabling and disabling + +The reporter is driven entirely by environment variables. It only runs when the required +variables are present, so it is effectively **off by default** unless your installation +configures it (for example, an installation registered with an API key). + +**Required** (the reporter does nothing unless all three are set): + +| Variable | Description | +|----------|-------------| +| `PGAI_PLATFORM_API_URL` | Console API base URL (e.g. `https://postgres.ai/api/v1`) | +| `PGAI_API_TOKEN` | API token for the monitoring instance | +| `PGAI_MONITORING_INSTANCE_ID` | UUID of this monitoring instance | + +To **disable** telemetry, unset these variables (or remove them from `.env`) and restart the +stack. With the required variables absent, the reporter does not start. + +**Optional tuning:** + +| Variable | Default | Description | +|----------|---------|-------------| +| `PGAI_TELEMETRY_INTERVAL_SEC` | `3600` | Report interval in seconds (minimum 60) | +| `PGAI_TELEMETRY_DISK_PATH` | `/` | Path checked for free disk space | +| `PGAI_TELEMETRY_MEMINFO_PATH` | `/proc/meminfo` | Source for free-RAM readings | +| `PGAI_TELEMETRY_OOM_LOOKBACK` | `24 hours ago` | How far back to scan for OOM events | + +## Privacy + +- Reports are scoped to the monitoring host and the monitoring instance, not to any monitored + database. +- No database contents, credentials, or query parameter values are transmitted. +- Data is sent only to the configured `PGAI_PLATFORM_API_URL` using the instance API token. + +## Related + +- [DBLab Engine telemetry](/docs/database-lab/telemetry) — telemetry for the Database Lab Engine +- [Architecture](/docs/monitoring/advanced/architecture) — monitoring stack components diff --git a/docs/monitoring/configuration/alerting.md b/docs/monitoring/configuration/alerting.md index c4f4c4ca..228aafcd 100644 --- a/docs/monitoring/configuration/alerting.md +++ b/docs/monitoring/configuration/alerting.md @@ -6,112 +6,88 @@ sidebar_position: 5 # Alerting configuration -Configure alert rules and notification channels for PostgresAI monitoring. +:::info No bundled alerting in 0.15.0 +PostgresAI monitoring 0.15.0 does **not** ship any alert rules, an Alertmanager service, or a +`vmalert` component. There are no pre-configured alerts (such as connection, replication, or +bloat alerts), no Alertmanager API on port `9093`, and no notification receivers bundled with the +stack. -## Alert rule basics - -PostgresAI includes pre-configured alert rules for common PostgreSQL issues. - -### Alert structure +The metrics-storage backend is VictoriaMetrics (the `sink-prometheus` service), exposed on host +port `59090` (container `9090`). The bundled `config/prometheus/prometheus.yml` contains only a +commented-out `rule_files:` placeholder and **no** `alerting:` / `alertmanager` block: ```yaml -groups: - - name: postgresql_alerts - rules: - - alert: HighConnectionUsage - expr: | - sum(pg_stat_database_numbackends) - / - scalar(max(pg_settings_max_connections)) - > 0.8 - for: 5m - labels: - severity: warning - annotations: - summary: "Connection usage above 80%" - description: "{{ $labels.cluster_name }} has {{ $value | humanizePercentage }} connections used" +rule_files: + # - "first_rules.yml" + # - "second_rules.yml" ``` -### Alert components - -| Component | Purpose | -|-----------|---------| -| expr | PromQL expression that triggers alert | -| for | Duration condition must be true | -| labels | Metadata for routing and filtering | -| annotations | Human-readable alert details | - -## Pre-configured alerts - -### Connection alerts - -| Alert | Condition | Severity | -|-------|-----------|----------| -| HighConnectionUsage | > 80% of max_connections | warning | -| CriticalConnectionUsage | > 95% of max_connections | critical | -| IdleInTransactionLong | Session idle in transaction > 5min | warning | - -### Performance alerts +Everything on this page describes how you can *add your own* alerting on top of the stack. None of +it is provided out of the box. +::: -| Alert | Condition | Severity | -|-------|-----------|----------| -| HighTransactionRollbackRate | Rollbacks > 5% of commits | warning | -| LowBufferCacheHitRatio | Buffer hit ratio < 95% | warning | -| HighDeadTupleRatio | Dead tuples > 20% of live | warning | +## Adding alerting yourself -### Replication alerts +Because the stack stores metrics in VictoriaMetrics, you have two common options to add alerting. -| Alert | Condition | Severity | -|-------|-----------|----------| -| ReplicationLagHigh | Lag > 100MB | warning | -| ReplicationLagCritical | Lag > 1 GiB | critical | -| ReplicaDisconnected | Replica not in pg_stat_replication | critical | +### Option 1: Grafana alerting -### Storage alerts +Grafana (the `grafana` service, container `grafana-with-datasources`, on host port `3000`, +default login `monitor` / `demo`) ships with its own alerting engine. You can define alert rules +against the `PGWatch-Prometheus` datasource entirely inside Grafana, with no extra components. -| Alert | Condition | Severity | -|-------|-----------|----------| -| TableBloatHigh | Estimated bloat > 50% | warning | -| IndexBloatHigh | Estimated bloat > 30% | warning | -| TempFileUsageHigh | Temp files > 1 GiB/hour | warning | +1. Open a panel and switch to the **Alert** tab, or use **Alerting → Alert rules**. +2. Build a query against the `PGWatch-Prometheus` datasource using the real metric names exported + by pgwatch (all prefixed `pgwatch_`, for example `pgwatch_pg_stat_activity_count`, + `pgwatch_db_stats_xact_commit`, `pgwatch_db_stats_numbackends`, and + `pgwatch_settings_numeric_value{setting_name="max_connections"}`). See the + [metrics reference](/docs/reference-guides/postgres-ai-monitoring-reference) for the exact + series names. +3. Configure a threshold, an evaluation interval, and a contact point. -## Custom alert rules +### Option 2: vmalert + Alertmanager (not bundled) -### Creating custom rules +VictoriaMetrics supports Prometheus-style alerting through the separate `vmalert` component plus a +Prometheus Alertmanager. Neither is part of the PostgresAI compose stack, so you would run them +yourself and point them at the `sink-prometheus` datasource (`http://sink-prometheus:9090`, +which uses basic auth — `VM_AUTH_USERNAME` / `VM_AUTH_PASSWORD`). -1. Create rules file: +If you go this route, write your own rules against the real `pgwatch_*` series. For example, a +connection-saturation rule: ```yaml -# custom-alerts.yml groups: - - name: custom_postgresql + - name: postgresql_alerts rules: - - alert: SlowQueryDetected + - alert: HighConnectionUsage expr: | - pg_stat_statements_mean_exec_time_seconds - > 1 - for: 10m + sum(pgwatch_db_stats_numbackends) by (cluster, node_name) + / + scalar(max(pgwatch_settings_numeric_value{setting_name="max_connections"})) + > 0.8 + for: 5m labels: severity: warning annotations: - summary: "Slow query detected" - description: "Query {{ $labels.queryid }} averaging {{ $value }}s" + summary: "Connection usage above 80%" + description: "{{ $labels.cluster }} has {{ $value | humanizePercentage }} connections used" ``` -2. Mount into container: - -```yaml -volumes: - - ./custom-alerts.yml:/etc/prometheus/rules/custom-alerts.yml -``` +:::note Verify metric and label names first +Use real series names and labels. pgwatch exports series as `pgwatch__`, +and the cluster label is `cluster` (not `cluster_name`; `cluster_name` is only a Grafana template +variable). Query `http://localhost:59090/api/v1/query?query=...` against VictoriaMetrics (with VM +basic auth) to confirm a series exists before writing a rule against it. +::: -### Alert rule best practices +## Best practices for any rules you add -**Use `for` duration wisely:** -- Too short — false positives from transient spikes -- Too long — delayed notification +**Use the `for` duration wisely:** +- Too short — false positives from transient spikes. +- Too long — delayed notification. **Recommended `for` values:** + | Alert type | Duration | |------------|----------| | Critical outages | 1m | @@ -119,207 +95,9 @@ volumes: | Resource usage | 10m | | Trend alerts | 30m | -## Notification channels - -### Email - -```yaml -receivers: - - name: email-team - email_configs: - - to: dba-team@example.com - from: alerts@example.com - smarthost: smtp.example.com:587 - auth_username: alerts@example.com - auth_password: ${SMTP_PASSWORD} # Use environment variable -``` - -:::warning Security -Never hardcode SMTP passwords. Use environment variable interpolation or external secrets management. -::: - -### Slack - -```yaml -receivers: - - name: slack-alerts - slack_configs: - - api_url: https://hooks.slack.com/services/xxx/yyy/zzz - channel: '#postgres-alerts' - title: '{{ .GroupLabels.alertname }}' - text: '{{ .Annotations.description }}' -``` - -### PagerDuty - -```yaml -receivers: - - name: pagerduty-critical - pagerduty_configs: - - service_key: - severity: '{{ .Labels.severity }}' -``` - -### OpsGenie - -```yaml -receivers: - - name: opsgenie - opsgenie_configs: - - api_key: your-api-key - priority: '{{ if eq .Labels.severity "critical" }}P1{{ else }}P3{{ end }}' -``` - -## Alert routing - -### Route configuration - -```yaml -route: - receiver: default - group_by: [alertname, cluster_name] - group_wait: 30s - group_interval: 5m - repeat_interval: 4h - - routes: - - match: - severity: critical - receiver: pagerduty-critical - repeat_interval: 1h - - - match: - severity: warning - receiver: slack-alerts - repeat_interval: 4h -``` - -### Routing labels - -| Label | Purpose | -|-------|---------| -| severity | critical, warning, info | -| cluster_name | Target specific teams | -| team | Route to team channel | - -## Silencing alerts - -### Temporary silence - -```bash -# Via Alertmanager API -curl -X POST http://localhost:9093/api/v2/silences \ - -H "Content-Type: application/json" \ - -d '{ - "matchers": [ - {"name": "alertname", "value": "HighConnectionUsage"} - ], - "startsAt": "2024-01-15T00:00:00Z", - "endsAt": "2024-01-15T06:00:00Z", - "createdBy": "admin", - "comment": "Planned maintenance" - }' -``` - -### Inhibition rules - -Suppress dependent alerts: - -```yaml -inhibit_rules: - - source_match: - alertname: PostgresDown - target_match: - severity: warning - equal: [cluster_name] -``` - -## Grafana alerting - -### Creating Grafana alerts - -1. Open panel edit mode -2. Click "Alert" tab -3. Configure conditions: - -```yaml -conditions: - - evaluator: - type: gt - params: [0.8] - query: - params: [A, 5m, now] - reducer: - type: avg -``` - -### Grafana contact points - -```yaml -apiVersion: 1 -contactPoints: - - orgId: 1 - name: slack - receivers: - - uid: slack-1 - type: slack - settings: - url: https://hooks.slack.com/xxx -``` - -## Testing alerts - -### Dry run - -```bash -# Check rule syntax -promtool check rules custom-alerts.yml - -# Test PromQL expression -curl 'http://localhost:8428/api/v1/query?query=...' -``` - -### Alert testing - -```bash -# Fire test alert -curl -X POST http://localhost:9093/api/v2/alerts \ - -H "Content-Type: application/json" \ - -d '[{ - "labels": {"alertname": "TestAlert", "severity": "warning"}, - "annotations": {"summary": "Test alert"} - }]' -``` - -## Troubleshooting - -### Alert not firing - -1. Check expression returns data: - ```bash - curl 'http://localhost:8428/api/v1/query?query=' - ``` - -2. Verify `for` duration has elapsed - -3. Check Alertmanager received alert: - ```bash - curl http://localhost:9093/api/v2/alerts - ``` - -### Alert not delivered - -1. Check Alertmanager logs -2. Verify notification channel configuration -3. Test channel directly: - ```bash - curl -X POST https://hooks.slack.com/xxx -d '{"text":"test"}' - ``` - -### Common issues +## Related -| Issue | Cause | Solution | -|-------|-------|----------| -| No alerts | Expression returns empty | Check metric exists and labels match | -| Too many alerts | Threshold too sensitive | Adjust threshold or add `for` duration | -| Duplicate alerts | Multiple Alertmanagers | Configure HA clustering | +- [Monitoring reference](/docs/reference-guides/postgres-ai-monitoring-reference) — exact metric + and label names to use in alert expressions. +- [pgwatch configuration](/docs/monitoring/configuration/pgwatch-config) — how metric collection + is configured. diff --git a/docs/monitoring/configuration/grafana-config.md b/docs/monitoring/configuration/grafana-config.md index aa9a0ec9..cb2c4154 100644 --- a/docs/monitoring/configuration/grafana-config.md +++ b/docs/monitoring/configuration/grafana-config.md @@ -12,15 +12,21 @@ Configuration for Grafana dashboards and visualization. ### Default credentials -Default admin credentials for local installation: +Default admin credentials for the local Docker installation (set via +`GF_SECURITY_ADMIN_USER` / `GF_SECURITY_ADMIN_PASSWORD` in the compose file): ``` -Username: admin -Password: admin +Username: monitor +Password: demo ``` +The password is `${GF_SECURITY_ADMIN_PASSWORD:-demo}` — override it by setting +`GF_SECURITY_ADMIN_PASSWORD` in `.env`. On Helm, the admin username and password come from the +chart secret keys `grafana-admin-user` / `grafana-admin-password`. + :::warning -Change the default password immediately after first login. +The default password `demo` is for local testing only. Always set a strong +`GF_SECURITY_ADMIN_PASSWORD` before any production or exposed deployment. ::: ### Disable anonymous access @@ -54,46 +60,92 @@ api_url = https://auth.example.com/userinfo ## Data sources -### VictoriaMetrics data source +The stack provisions three data sources automatically (from +`config/grafana/provisioning/datasources/datasources.yml`). They are re-provisioned on every +startup (`editable: false`), so edit the provisioning file rather than the Grafana UI. + +| Data source | Type | URL | Default | +|-------------|------|-----|---------| +| `PGWatch-Prometheus` | `prometheus` | `http://sink-prometheus:9090` | **Yes** | +| `PGWatch-PostgreSQL` | `postgres` | `sink-postgres:5432` (db `measurements`) | No | +| `Infinity` | `yesoreyeram-infinity-datasource` | — | No | -Automatically configured during installation: +The default `PGWatch-Prometheus` source points at the single-node VictoriaMetrics instance +(`sink-prometheus`, internal port 9090) and uses basic auth from `VM_AUTH_USERNAME` / +`VM_AUTH_PASSWORD`: ```yaml apiVersion: 1 datasources: - - name: VictoriaMetrics + - name: PGWatch-Prometheus type: prometheus - url: http://victoriametrics:8428 access: proxy + url: http://sink-prometheus:9090 isDefault: true + basicAuth: true + basicAuthUser: ${VM_AUTH_USERNAME} + secureJsonData: + basicAuthPassword: ${VM_AUTH_PASSWORD} + editable: false ``` ### Adding additional data sources +Add entries to the source provisioning file +`config/grafana/provisioning/datasources/datasources.yml`: + ```yaml datasources: - name: PostgreSQL type: postgres - url: postgresql://host:5432/db + url: host:5432 + database: db user: readonly_user secureJsonData: password: ${DB_PASSWORD} # Use environment variable ``` +The datasources file lives only in the `postgres_ai_configs` volume (Grafana reads it +read-only via `GF_PATHS_PROVISIONING`), and Grafana provisions datasources **only at +container startup**. `postgresai mon update-config` does **not** apply this change — it +regenerates the pgwatch `sources.yml` only, and does not reseed the volume or restart +Grafana. To apply a datasources edit, reseed the config volume (which requires recreating +`config-init` with `docker compose up -d` directly), then restart Grafana so it re-reads the +provisioned file: + +```bash +docker compose run --rm --entrypoint rm config-init /target/.pgai-configs-version +docker compose up -d --force-recreate config-init # recreate config-init -> reseed the volume with the edited file +postgresai mon restart grafana # restart Grafana so it re-provisions from the reseeded volume +``` + +Use `docker compose up -d` here, **not** `postgresai mon start`: because you are editing a live +stack, `mon start` sees the running containers, prints `Monitoring services are already running`, +and exits **without** running `docker compose up -d`, so `config-init` is never recreated and the +edited `datasources.yml` is never reseeded — the subsequent `mon restart grafana` would then +re-provision Grafana from the **old** volume contents. `mon restart` alone would not work either: +`docker compose restart` restarts the existing `config-init` container in place, but with the marker +already gone the reseed only runs when `config-init` is **recreated** by `docker compose up -d`. + ## Dashboard provisioning ### Auto-loading dashboards -PostgresAI dashboards are provisioned automatically: +PostgresAI dashboards are provisioned automatically from +`config/grafana/provisioning/dashboards/dashboards.yml`, which loads the JSON files mounted at +`/postgres_ai_configs/grafana/dashboards`: ```yaml apiVersion: 1 providers: - - name: postgres_ai - folder: postgres_ai + - name: 'PostgresAI Dashboards' + orgId: 1 type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true options: - path: /var/lib/grafana/dashboards/postgres_ai + path: /postgres_ai_configs/grafana/dashboards ``` ### Custom dashboard folder @@ -138,18 +190,42 @@ Control when variables are refreshed: ```bash # Use browser time zone -GF_DEFAULT_TIMEZONE=browser +GF_USERS_DEFAULT_TIMEZONE=browser # Use specific time zone -GF_DEFAULT_TIMEZONE=UTC +GF_USERS_DEFAULT_TIMEZONE=UTC ``` +These set Grafana's `[users] default_timezone` setting (the env override for an ini setting is +`GF_
_`). + ### Theme +New in 0.15, the bundled Grafana ships with **Desert Bloom** as the default theme. It is set in +the provisioned `grafana.ini`: + +```ini +[users] +default_theme = desertbloom + +[feature_toggles] +# Required for the bundled experimental themes (desertbloom, etc.) +enable = grafanaconThemes +``` + +The `grafanaconThemes` feature toggle is enabled by default in the stack so the theme is +available. To revert to a stock theme, override the default: + ```bash -GF_DEFAULT_THEME=dark # dark, light +# Revert to the stock Grafana theme (overrides the [users] default_theme ini setting) +GF_USERS_DEFAULT_THEME=dark # dark, light ``` +:::note +`grafanaconThemes` must remain enabled for `desertbloom` to apply. If you disable it, set +`GF_USERS_DEFAULT_THEME` to `dark` or `light` to avoid falling back to an unavailable theme. +::: + ### Refresh interval Default auto-refresh: @@ -160,9 +236,19 @@ GF_DASHBOARDS_DEFAULT_INTERVAL=15s ## Performance +:::note +The bundled stack ships a minimal `grafana.ini` +(`config/grafana/provisioning/grafana.ini`) that only sets `[analytics]`, +`[users]`, `[feature_toggles]`, `[auth]`, and `[auth.generic_oauth]` keys. The +`grafana.ini` examples below are **optional, generic Grafana customizations**, +not stack defaults — add them yourself if you need them. +::: + ### Query caching -Enable query result caching: +Grafana's built-in query result caching (`[caching]`) is a **Grafana Enterprise** +feature and is **not** available in the OSS `grafana/grafana` image the stack +ships. If you run Grafana Enterprise, enable it with: ```ini [caching] @@ -172,15 +258,25 @@ ttl = 60s ### Max data points -Limit data points returned per query: - -```ini -[server] -router_logging = false +Grafana has **no `grafana.ini` key** that limits the number of points returned +per query. The point count is capped per panel by the **Max data points** +(`maxDataPoints`) query option, set in the panel editor under **Query options** +(or as `"maxDataPoints"` in the panel's dashboard JSON). When left empty, Grafana +auto-derives it from the panel's pixel width; lowering it reduces the resolution +(and cost) of each query: + +```json +// In a panel's dashboard JSON +{ + "maxDataPoints": 500 +} ``` ### Concurrent queries +Size Grafana's own backend database connection pool (this governs connections to +Grafana's config/session database, not to your monitored Postgres): + ```ini [database] max_open_conn = 100 @@ -188,6 +284,10 @@ max_open_conn = 100 ## Embedding +The `[security]` settings below are **optional, generic Grafana customizations** +— the bundled `grafana.ini` does not set any `[security]` keys (admin +credentials are supplied via the `GF_SECURITY_*` env vars instead). + ### Allow embedding in iframes ```ini @@ -235,29 +335,30 @@ grafana-cli plugins install grafana-clock-panel ### Required plugins -PostgresAI dashboards use these plugins: +The stack installs one external plugin (via `GF_INSTALL_PLUGINS` in Docker Compose, or +`grafana.plugins` on Helm): | Plugin | Purpose | |--------|---------| -| Stat panel | Single value displays | -| Time series | Metric charts | -| Table | Data grids | -| Heatmap | Wait event visualization | +| `yesoreyeram-infinity-datasource` | Backs the `Infinity` data source used by some panels | + +The remaining panel types used by the dashboards (stat, time series, table, heatmap) are built +into Grafana and require no extra plugins. ## Resource limits ### Memory -```yaml -# docker-compose.yml -services: - grafana: - deploy: - resources: - limits: - memory: 512M +Grafana's memory limit is set with `mem_limit` (default 512 MiB), overridable via the +`GRAFANA_MEM` `.env` variable (bytes): + +```bash +# .env — raise Grafana's memory limit to 1 GiB +GRAFANA_MEM=1073741824 ``` +See [Resource limits (per service)](/docs/monitoring/configuration#resource-limits-per-service). + ### Concurrent users ```ini diff --git a/docs/monitoring/configuration/index.md b/docs/monitoring/configuration/index.md index 80483ce3..bb53a967 100644 --- a/docs/monitoring/configuration/index.md +++ b/docs/monitoring/configuration/index.md @@ -21,34 +21,40 @@ Configuration guides for customizing PostgresAI monitoring components. ### CLI installation -Configuration via environment variables and command-line flags: +Configuration is stored in the monitoring directory `.env` file. `update-config` migrates `.env` +and regenerates the pgwatch `sources.yml`, but it does **not** restart services — keys read by a +service at container startup (e.g. the `VM_*` flags below, consumed by sink-prometheus) only take +effect once that service is recreated: ```bash -postgresai mon local-install \ - --retention 30d \ - --scrape-interval 15s \ - postgresql://user@host:5432/db +# Example .env overrides (default VM_RETENTION_PERIOD is 336h ≡ 14 days) +VM_RETENTION_PERIOD=30d +VM_QUERY_DURATION=30s +VM_MAX_CONCURRENT_REQUESTS=16 + +postgresai mon update-config +# These VM_* values are read by sink-prometheus at startup; recreate it to apply: +docker compose up -d --force-recreate sink-prometheus ``` ### Docker Compose -Configuration via `docker-compose.yml` and environment files: +Configuration is passed through `docker-compose.yml` and the generated `.env` file: -```yaml -services: - pgwatch: - environment: - PW_SCRAPE_INTERVAL: 15s - PW_RETENTION: 720h +```bash +# Example overrides (default VM_RETENTION_PERIOD is 336h ≡ 14 days) +VM_RETENTION_PERIOD=30d +VM_QUERY_DURATION=30s +VM_MAX_CONCURRENT_REQUESTS=16 ``` ### Helm -Configuration via `values.yaml`: +Configuration via `values.yaml` (see [Helm installation](/docs/monitoring/getting-started/installation-helm)): ```yaml -monitoring: - retention: 30d +victoriaMetrics: + retentionPeriod: 336h # default; 14 days scrapeInterval: 15s ``` @@ -56,9 +62,89 @@ monitoring: | Setting | Default | Description | |---------|---------|-------------| -| Scrape interval | 15s | How often to collect metrics | -| Retention | 14d | How long to keep metrics | -| Max connections | 3 | Connections per monitored database | +| Scrape interval | 15s | How often to collect metrics (`victoriaMetrics.scrapeInterval`) | +| Retention | 14 days (`336h`) | How long to keep metrics (`VM_RETENTION_PERIOD` / `victoriaMetrics.retentionPeriod`) | +| Query-id mapping retention | 720 hours (30 days) | How long the Flask backend keeps the queryid → query-text mapping (`QUERYID_RETENTION_HOURS`, a bare integer number of hours — no `h` suffix) | + +## Resource limits (per service) + +New in 0.15, each monitoring-stack service has optional CPU and memory limits exposed as `.env` +variables. All of them are **optional** — leaving them unset preserves the default +laptop/dev sizing and produces no behavior change. Memory limits are in **bytes** (Docker +Compose `mem_limit` convention); `*_CPUS` values are floats (Docker Compose `cpus:` semantics). +Provisioning playbooks can export production-grade values per VM size class without forking the +compose file. + +| Service | CPU variable | Memory variable | Default memory | +|---------|--------------|-----------------|----------------| +| Demo target database | `TARGET_DB_CPUS` (0.2) | `TARGET_DB_MEM` | 768 MiB | +| Demo target standby | `TARGET_STANDBY_CPUS` (0.2) | `TARGET_STANDBY_MEM` | 768 MiB | +| Postgres sink | `SINK_POSTGRES_CPUS` (0.4) | `SINK_POSTGRES_MEM` | 1 GiB | +| VictoriaMetrics sink | `SINK_PROMETHEUS_CPUS` (0.75) | `SINK_PROMETHEUS_MEM` | 1.5 GiB | +| pgwatch (Postgres sink) | `PGWATCH_POSTGRES_CPUS` (0.35) | `PGWATCH_POSTGRES_MEM` | 512 MiB | +| pgwatch (Prometheus sink) | `PGWATCH_PROMETHEUS_CPUS` (0.5) | `PGWATCH_PROMETHEUS_MEM` | 512 MiB | +| Grafana | `GRAFANA_CPUS` (0.5) | `GRAFANA_MEM` | 512 MiB | +| Flask backend | `FLASK_CPUS` (0.5) | `FLASK_MEM` | 1 GiB | +| Reporter | `POSTGRES_REPORTS_CPUS` (1.0) | `POSTGRES_REPORTS_MEM` | 1.75 GiB | +| cAdvisor (self-monitoring) | `CADVISOR_CPUS` (0.25) | `CADVISOR_MEM` | 384 MiB | +| node exporter (self-monitoring) | `NODE_EXPORTER_CPUS` (0.05) | `NODE_EXPORTER_MEM` | 96 MiB | +| postgres exporter (self-monitoring) | `POSTGRES_EXPORTER_CPUS` (0.1) | `POSTGRES_EXPORTER_MEM` | 128 MiB | + +Set the values in the monitoring stack `.env`. These are Compose `cpus:` / `mem_limit:` keys that +only take effect when a container is recreated, so after migrating `.env` with +`postgresai mon update-config` recreate the affected services with +`docker compose up -d --force-recreate ` (or set the values before the initial +`docker compose up -d` for manual installs); `update-config` does not recreate services. The +VictoriaMetrics engine tuning flags +(`VM_QUERY_DURATION`, `VM_MAX_CONCURRENT_REQUESTS`) are documented under +[Query and search tuning](/docs/monitoring/configuration/prometheus-config#query-and-search-tuning). + +## Config seeding and operator edits + +The stack seeds its generated configuration once and guards it with a **version marker**, so +operator edits to provisioned config persist across restarts and are not silently overwritten. +The reseed is performed by the `config-init` service: on start it compares the image's `/VERSION` +against the `.pgai-configs-version` marker in the config volume, and reseeds only when they +differ. A reseed therefore happens when you **recreate** the stack on a newer image — i.e. on +`docker compose up -d` after the image tag (and thus the image version) is bumped, since `up -d` +recreates `config-init` from the new image and it then sees the version mismatch. (`postgresai mon +start` runs `docker compose up -d` **only when the stack is stopped** — on an already-running stack +it reports `Monitoring services are already running` and exits without recreating anything — so for +an in-place upgrade of a running stack, run `docker compose up -d` directly to recreate `config-init`, +not `mon start`.) `postgresai mon restart` does **not** trigger a reseed: it runs +`docker compose restart`, which restarts the existing `config-init` container in place on the +**old** image, so it still reads the old `/VERSION`, the marker still matches, and nothing is +re-copied. Note that `postgresai mon update-config` does **not** reseed the volume either: it +migrates the `.env` file (additive required keys), refreshes the CLI-owned `docker-compose.yml` +to match the stack version for non-git/global installs (a no-op for git checkouts; it touches +only the compose file, never `.env`/`instances.yml`/`.pgwatch-config`), and regenerates the +pgwatch sources (`docker compose run --rm sources-generator`). It does not restart Grafana or +sink-prometheus. To force a fresh reseed, remove the version marker from the config volume so +`config-init` re-copies the image defaults on the next start. + +The `config-init` service is the only one that mounts the volume read-write (at `/target`); every +long-running service mounts it read-only (`/postgres_ai_configs:ro`), so you cannot delete the +marker from, say, the Grafana container. The service is also a one-shot init container that exits +immediately, so plain `docker exec config-init …` fails (the container name is +`postgres-ai-config-init` and it is not running). Instead, run a throwaway `config-init` with its +entrypoint overridden to `rm` (its normal entrypoint is the seeder script, which ignores extra +arguments), then restart: + +```bash +docker compose run --rm --entrypoint rm config-init /target/.pgai-configs-version +docker compose up -d --force-recreate config-init +``` + +`docker compose up -d --force-recreate config-init` recreates the one-shot `config-init` container; +with the marker now absent, `init-configs.sh` reseeds unconditionally. (A plain `docker compose up -d` +also works — it recreates `config-init` because nothing else depends on the cleared marker — but the +explicit `--force-recreate config-init` makes the intent unambiguous.) Do **not** use `postgresai mon +start` here: on an already-running stack `mon start` only checks whether the containers are up and, if +they are, prints `Monitoring services are already running` and exits **without** running `docker compose +up -d`, so `config-init` is never recreated and the reseed silently does not happen. Likewise +`postgresai mon restart` only restarts the existing containers in place and would not re-run the seeder +against the cleared volume. Recreating `config-init` on a live stack must therefore go through +`docker compose up -d` directly. ## Sections diff --git a/docs/monitoring/configuration/pgwatch-config.md b/docs/monitoring/configuration/pgwatch-config.md index 7e2a0111..c63e4154 100644 --- a/docs/monitoring/configuration/pgwatch-config.md +++ b/docs/monitoring/configuration/pgwatch-config.md @@ -6,228 +6,172 @@ sidebar_position: 2 # pgwatch configuration -Configuration options for the pgwatch metrics collector. +Configuration options for the pgwatch metrics collectors. -## Collection intervals +The 0.15 stack runs **two** pgwatch v3 collectors — one writing to the PostgreSQL sink +(`pgwatch-postgres`) and one writing to the Prometheus/VictoriaMetrics sink +(`pgwatch-prometheus`). Both are configured the same way: from generated `sources.yml` and +`metrics.yml` files, plus a small set of command-line flags. There are **no `PW_*` environment +variables** in this stack — collection is driven by file-based config, not env vars. -### Global interval +## What you edit vs. what is generated -Default collection interval for all metrics: +| File | Edited by you? | Purpose | +|------|----------------|---------| +| `instances.yml` | **Yes** | The list of databases to monitor (connection, preset, tags) | +| `sources.yml` (runtime volume) | No (generated) | Rendered from `instances.yml` by `sources-generator` into the `postgres_ai_configs` volume | +| `metrics.yml` (runtime volume) | Rarely | Metric/SQL definitions and the `full` preset | -```bash -# CLI -postgresai mon local-install --scrape-interval 15s - -# Environment variable -PW_SCRAPE_INTERVAL=15s -``` - -| Interval | Use case | -|----------|----------| -| 10s | High-resolution troubleshooting | -| 15s | Default — balanced | -| 30s | Lower resource usage | -| 60s | Large-scale deployments | - -### Per-metric intervals - -Some metrics use different intervals by default: - -| Metric group | Default interval | Rationale | -|--------------|------------------|-----------| -| pg_stat_statements | 15s | Query metrics — high value | -| pg_stat_activity | 5s | Wait events — time-sensitive | -| Table/index stats | 60s | Slow-changing, expensive | -| Bloat estimates | 300s | Very expensive queries | - -## Connection settings - -### Max connections - -Limit connections to monitored databases: - -```bash -PW_MAX_PARALLEL_CONNECTIONS_PER_DB=3 -``` - -Impact: -- Higher values — faster collection, more load -- Lower values — slower collection, less load - -### Connection timeout - -```bash -PW_CONNECT_TIMEOUT=10s -``` - -### Statement timeout - -Prevent long-running collection queries: - -```bash -PW_STATEMENT_TIMEOUT=30s -``` - -## Metric presets - -### Preset levels - -| Preset | Metrics collected | Use case | -|--------|-------------------|----------| -| basic | pg_stat_database, pg_stat_activity | Minimal monitoring | -| standard | Above + pg_stat_statements, table/index stats | Most deployments | -| full | All available metrics | Deep analysis | -| exhaustive | Full + expensive bloat queries | Troubleshooting | - -```bash -postgresai mon local-install --preset standard -``` - -### Custom metric selection - -Enable specific metrics: - -```bash -PW_ENABLED_METRICS="pg_stat_statements,pg_stat_activity,table_stats" -``` +Nothing named `sources.yml` is committed under `config/`. The `sources-generator` service renders +two copies into the `postgres_ai_configs` volume, one per sink: -Disable specific metrics: +- `/postgres_ai_configs/pgwatch/sources.yml` — for the PostgreSQL-sink collector (note the + directory is `pgwatch/`, **not** `pgwatch-postgres/`) +- `/postgres_ai_configs/pgwatch-prometheus/sources.yml` — for the Prometheus-sink collector -```bash -PW_DISABLED_METRICS="bloat_indexes,bloat_tables" -``` - -## Database filtering - -### Include specific databases +After editing `instances.yml`, re-render `sources.yml` and restart the collectors so they reload +it. `mon update-config` re-renders the file but does **not** restart the collectors, so restart +each one afterward: ```bash -PW_INCLUDE_DATABASES="production,staging" +postgresai mon update-config +postgresai mon restart pgwatch-postgres +postgresai mon restart pgwatch-prometheus ``` -### Exclude databases - -```bash -PW_EXCLUDE_DATABASES="template0,template1,postgres" -``` - -## Custom metrics - -### Adding a custom metric +When running Docker Compose manually, the `sources-generator` service re-renders both +`sources.yml` files from `instances.yml` on `docker compose up`. -1. Create metric definition file: +## Defining databases (instances.yml) -```yaml -# custom-metrics/my_metric.yaml -metrics: - my_custom_metric: - query: | - select - datname, - count(*) as connection_count - from pg_stat_activity - where state = 'active' - group by datname - interval: 30s - labels: - - datname - value_columns: - - connection_count -``` - -2. Mount into container: +Each entry in `instances.yml` is one monitored database: ```yaml -volumes: - - ./custom-metrics:/etc/pgwatch/custom-metrics +- name: prod-primary + conn_str: postgresql://postgres_ai_mon:pass@prod-db:5432/app + preset_metrics: full + custom_metrics: + is_enabled: true + group: production + custom_tags: + env: production + cluster: prod + node_name: primary ``` -### Metric definition fields - | Field | Required | Description | |-------|----------|-------------| -| query | Yes | SQL query to execute | -| interval | No | Override default interval | -| labels | No | Columns to use as labels | -| value_columns | Yes | Columns containing metric values | -| is_counter | No | Mark as counter vs gauge | - -## Logging +| `name` | Yes | Unique name for the source | +| `conn_str` | Yes | PostgreSQL connection string for the monitoring role | +| `preset_metrics` | Yes | Metrics preset to collect (the stack ships the `full` preset) | +| `custom_metrics` | No | Map of additional metric definitions (leave empty to use only the preset) | +| `is_enabled` | Yes | Whether this source is collected | +| `group` | No | Logical group label | +| `custom_tags` | No | Extra labels (e.g. `env`, `cluster`, `node_name`) used as Grafana variables | -### Log level +Use the `cluster` and `node_name` custom tags to switch between environments in Grafana. -```bash -PW_LOG_LEVEL=info # debug, info, warn, error -``` +## Metric presets -### Log format +The collectors apply a metrics preset named in `preset_metrics`. The stack ships a **`full`** +preset, but note that the two collectors use **different** `metrics.yml` files with +**different** `full` presets — they are not the same metric set: + +- `config/pgwatch-prometheus/metrics.yml` (the Prometheus/VictoriaMetrics collector, which feeds + the Grafana dashboards) defines a `full` preset of 49 metric groups, described as + *"almost all available metrics for a even deeper performance understanding"*. It includes groups + like `bgwriter`, `checkpointer`, `db_size`, `db_stats`, `table_stats`, `pg_stat_statements`, + `wait_events`, and `pg_stat_activity` (it does **not** contain `pgss_queryid_queries` or + `index_definitions`): + + ```yaml + presets: + full: + description: almost all available metrics for a even deeper performance understanding + metrics: + bgwriter: 30 + db_stats: 30 + table_stats: 30 + pg_stat_statements: 30 + wait_events: 15 + pg_stat_activity: 15 + # ... 49 groups total + ``` + +- `config/pgwatch-postgres/metrics.yml` (the Postgres-sink collector) defines its own, much + smaller `full` preset, described as *"Full metrics for PostgreSQL storage"*, containing only two + groups: + + ```yaml + presets: + full: + description: "Full metrics for PostgreSQL storage" + metrics: + pgss_queryid_queries: 30 + index_definitions: 3600 + ``` + +The preset maps each metric group to its collection interval (in seconds). To change which metrics +are collected or how often, edit the relevant `metrics.yml` definitions (advanced) or add entries +under `custom_metrics` for a specific database in `instances.yml`. + +## Collector command-line flags + +Both collectors are started with the same flag shape (see `docker-compose.yml`); the +Prometheus-sink collector differs only in its sources/metrics paths, sink URL, and web address: -```bash -PW_LOG_FORMAT=json # json, text -``` +```yaml +pgwatch-prometheus: + command: + - "--sources=/postgres_ai_configs/pgwatch-prometheus/sources.yml" + - "--metrics=/postgres_ai_configs/pgwatch-prometheus/metrics.yml" + - "--sink=prometheus://0.0.0.0:9091/pgwatch" + - "--web-addr=:8089" + - "--log-level=error" +``` + +| Flag | Purpose | +|------|---------| +| `--sources` | Path to the generated `sources.yml` | +| `--metrics` | Path to the `metrics.yml` definitions | +| `--sink` | Where collected metrics are written (Postgres or Prometheus sink) | +| `--web-addr` | Address for the collector's built-in web/health endpoint | +| `--log-level` | Log verbosity (`error` by default; also `info`, `warn`, `debug`) | + +On Helm, the equivalent log level is set with `pgwatchPostgres.logLevel` / +`pgwatchPrometheus.logLevel` (default `error`). ## Resource limits -### Memory - -Limit pgwatch memory usage: +Collector CPU and memory limits are exposed as optional `.env` variables (Docker Compose +`mem_limit` is in bytes, `cpus:` is a float). Defaults preserve laptop/dev sizing: -```yaml -# docker-compose.yml -services: - pgwatch: - deploy: - resources: - limits: - memory: 512M -``` - -### CPU +```bash +# pgwatch (Postgres sink) — defaults: 0.35 CPU, 512 MiB +PGWATCH_POSTGRES_CPUS=0.35 +PGWATCH_POSTGRES_MEM=536870912 -```yaml -deploy: - resources: - limits: - cpus: '1.0' +# pgwatch (Prometheus sink) — defaults: 0.5 CPU, 512 MiB +PGWATCH_PROMETHEUS_CPUS=0.5 +PGWATCH_PROMETHEUS_MEM=536870912 ``` -## High availability - -### Multiple pgwatch instances - -For HA, run multiple pgwatch instances with load balancing: - -```yaml -services: - pgwatch-1: - environment: - PW_INSTANCE_ID: pgwatch-1 - PW_CLUSTER_NODES: "pgwatch-1:8080,pgwatch-2:8080" - - pgwatch-2: - environment: - PW_INSTANCE_ID: pgwatch-2 - PW_CLUSTER_NODES: "pgwatch-1:8080,pgwatch-2:8080" -``` +See [Resource limits (per service)](/docs/monitoring/configuration#resource-limits-per-service) +for the full table. On Helm, set `pgwatchPostgres.resources` / `pgwatchPrometheus.resources`. ## Troubleshooting -### Check collection status - -```bash -curl http://localhost:8080/metrics | grep pgwatch_ -``` - -### View collected metrics +### Check the collector logs ```bash -curl http://localhost:8080/api/v1/metrics +docker compose logs pgwatch-postgres pgwatch-prometheus | grep -i error ``` ### Common issues | Issue | Cause | Solution | |-------|-------|----------| -| No metrics | Connection failed | Check credentials and network | -| Missing pg_stat_statements | Extension not loaded | Add to shared_preload_libraries | -| High CPU on target | Expensive queries | Use lower preset or longer intervals | +| No metrics | Connection failed | Check the `conn_str` credentials and network reachability | +| Missing pg_stat_statements | Extension not loaded | Add `pg_stat_statements` to `shared_preload_libraries` and restart | +| High CPU on target | Expensive queries | Reduce the number of monitored databases or raise preset intervals | +| Source not collected | `is_enabled: false` | Set `is_enabled: true` in `instances.yml`, run `mon update-config` to re-render `sources.yml`, then restart the collectors so they reload it: `mon restart pgwatch-postgres` and `mon restart pgwatch-prometheus` | diff --git a/docs/monitoring/configuration/prometheus-config.md b/docs/monitoring/configuration/prometheus-config.md index 1e660c7b..11f29f79 100644 --- a/docs/monitoring/configuration/prometheus-config.md +++ b/docs/monitoring/configuration/prometheus-config.md @@ -10,220 +10,266 @@ Configuration for the time-series database storing monitoring metrics. PostgresAI uses VictoriaMetrics by default — a Prometheus-compatible TSDB with better performance and compression. -## Retention +## Authentication and security -### Setting retention period +New in 0.15, the VictoriaMetrics endpoint is protected with HTTP basic auth. Two `.env` keys +are now **required**: ```bash -# CLI -postgresai mon local-install --retention 30d - -# Environment variable -VM_RETENTION_PERIOD=30d +VM_AUTH_USERNAME=vmauth +VM_AUTH_PASSWORD= ``` -| Retention | Disk usage (approx) | Use case | -|-----------|---------------------|----------| -| 7d | ~500 MiB per database | Development | -| 14d | ~1 GiB per database | Default | -| 30d | ~2 GiB per database | Production | -| 90d | ~6 GiB per database | Compliance requirements | +| Variable | Default | Purpose | +|----------|---------|---------| +| `VM_AUTH_USERNAME` | `vmauth` | Basic-auth username for the VictoriaMetrics endpoint | +| `VM_AUTH_PASSWORD` | (none — must be set) | Basic-auth password; generate with `openssl rand -base64 18` | -### Retention with downsampling +**What it protects and why it is required:** -For long retention with reduced storage: +- It guards the VictoriaMetrics HTTP API so the metrics store is not exposed unauthenticated. +- Grafana's provisioned datasource authenticates with these same credentials. If they are + missing or empty, **Grafana cannot query VictoriaMetrics and all dashboards show no data**. +- The shipped `.env.example` ships empty placeholders that make Docker Compose fail fast until + a value is set, to prevent an accidentally unauthenticated deployment. -```bash -# Keep full resolution for 14 days -# Downsample to 1-minute resolution for 90 days -VM_RETENTION_PERIOD=14d -VM_DOWNSAMPLING_PERIOD=90d:1m -``` +The CLI (`postgresai mon local-install` / `mon update` / `mon update-config`) generates and +preserves these automatically. If you run Docker Compose directly, you must add them before +`docker compose up -d` — see +[Upgrading the monitoring stack](/docs/monitoring/getting-started/upgrade#required-new-keys-in-015-victoriametrics-basic-auth). -## Storage +### Rotating VictoriaMetrics credentials -### Data directory +The monitoring project directory (`~/.config/postgresai/monitoring/` for npx/global installs) +contains only `docker-compose.yml`, `instances.yml`, `.pgwatch-config`, and `.env` — the +`scripts/` directory is **not** copied there. So the simplest path that works for every install +type is to set a new password in `.env` and then recreate the affected services: ```bash -VM_STORAGE_DATA_PATH=/var/lib/victoriametrics +# Edit ~/.config/postgresai/monitoring/.env and set a new VM_AUTH_PASSWORD, e.g. +# VM_AUTH_PASSWORD=$(openssl rand -base64 18) +# then recreate sink-prometheus (to pick up the new -httpAuth password) and Grafana +# (to re-provision its datasource with the new password): +docker compose up -d --force-recreate sink-prometheus grafana ``` -### Disk allocation +:::warning `mon update-config` does not rotate the password +Running `postgresai mon update-config` alone is **not** enough here: in 0.15 it migrates required +`.env` keys (additive), refreshes the CLI-owned `docker-compose.yml` for non-git installs (a +no-op for git checkouts), and regenerates the pgwatch sources (`docker compose run --rm +sources-generator`). It does not restart Grafana or sink-prometheus. Grafana provisions its datasource +(with `editable: false`) only at container startup, so a rotated `VM_AUTH_PASSWORD` does not take +effect in the running Grafana until it is recreated — which is exactly why the bundled +`scripts/rotate-vm-auth.sh` runs `docker compose up -d --force-recreate sink-prometheus grafana`. +::: -Estimate storage needs: +If you are working from a full git checkout of the repository, the bundled helper +`scripts/rotate-vm-auth.sh` does the same thing in one step (run it from the repo root, not from +the monitoring project directory): -``` -Storage = (metrics/sec) × (bytes/metric) × (retention_seconds) +```bash +VM_AUTH_PASSWORD="$(openssl rand -base64 18)" ./scripts/rotate-vm-auth.sh ``` -Typical values: -- ~100 metrics/sec per monitored database -- ~3-5 bytes/sample (VictoriaMetrics with typical monitoring data) -- 14 days = 1,209,600 seconds +After rotating, confirm the recreated services are running: -Result: ~12 GiB per database for 14-day retention - -### Memory allocation +```bash +postgresai mon health +``` -VictoriaMetrics uses memory for caching: +`mon health` only checks that each container is running (via `docker inspect`); it does not query +Grafana or VictoriaMetrics, so it will not surface a stale credential on its own. To confirm the +new credentials actually work, open a dashboard in Grafana (it should render data, not an auth +error) or query the VictoriaMetrics API directly with the new basic-auth credentials (the bundled +stack publishes VictoriaMetrics on host port `59090`): -```yaml -# docker-compose.yml -services: - victoriametrics: - deploy: - resources: - limits: - memory: 2G +```bash +curl -fsS -u "${VM_AUTH_USERNAME:-vmauth}:${VM_AUTH_PASSWORD}" \ + 'http://localhost:59090/api/v1/query?query=up' ``` -Rule of thumb: 2 GiB minimum, add 512 MiB per 10 monitored databases. +A `200` with a JSON result confirms the new credentials are accepted; a `401` means Grafana would +also fail to authenticate. -## Scrape configuration - -### Scrape interval +:::note Not the same as the database role +This rotates the *VictoriaMetrics* basic-auth credentials. To rotate the *monitored database* +role's password instead, see +[Rotate monitoring database credentials](/docs/monitoring/troubleshooting/permissions#rotate-monitoring-database-credentials). +::: -How often VictoriaMetrics pulls from pgwatch: +## Retention -```yaml -# prometheus.yml or vmagent config -scrape_configs: - - job_name: pgwatch - scrape_interval: 15s - static_configs: - - targets: ['pgwatch:8080'] -``` +### Setting retention period -### Scrape timeout +```bash +# Set in the monitoring stack .env file. +# Default is 336h (14 days); the value below overrides it to 30 days. +VM_RETENTION_PERIOD=30d -```yaml -scrape_timeout: 10s +# Migrate .env, then recreate sink-prometheus so it reads the new value. +# `mon update-config` migrates .env but does NOT restart sink-prometheus, +# which only reads VM_RETENTION_PERIOD at container startup. +postgresai mon update-config +docker compose up -d --force-recreate sink-prometheus ``` -### Labels - -Add global labels to all metrics: - -```yaml -global: - external_labels: - environment: production - region: us-east-1 -``` +| Retention | Disk usage (approx) | Use case | +|-----------|---------------------|----------| +| 7d | ~500 MiB per database | Development | +| 14d | ~1 GiB per database | Default | +| 30d | ~2 GiB per database | Production | +| 90d | ~6 GiB per database | Compliance requirements | -## Query settings +### Query-id mapping retention -### Query timeout +New in 0.15. The Flask backend keeps a mapping from `queryid` to query text so dashboards can +show readable query text instead of numeric IDs. How long that mapping is retained is +controlled separately from metrics retention: ```bash -VM_SEARCH_QUERY_TIMEOUT=30s +# Hours to retain the queryid -> query text mapping in the Flask backend. +# Independent of VM_RETENTION_PERIOD; for new plan-specific configuration, +# use the same window as VM_RETENTION_PERIOD expressed in hours. +QUERYID_RETENTION_HOURS=720 ``` -### Max concurrent queries +| Setting | Controls | Independent of | +|---------|----------|----------------| +| `VM_RETENTION_PERIOD` | How long time-series metrics are kept in VictoriaMetrics | — | +| `QUERYID_RETENTION_HOURS` | How long the query-id → query-text mapping is kept | `VM_RETENTION_PERIOD` | -```bash -VM_SEARCH_MAX_CONCURRENT_REQUESTS=16 -``` +Paired examples: -### Max query memory +| History | `VM_RETENTION_PERIOD` | `QUERYID_RETENTION_HOURS` | +|---------|-----------------------|---------------------------| +| Short (7 days) | `168h` | `168` | +| Long (6 months) | `4380h` | `4380` | -```bash -VM_SEARCH_MAX_MEMORY_PER_QUERY=512MB -``` +Migrate `.env` with `postgresai mon update-config`, then recreate the services that read these +values at startup — `VM_RETENTION_PERIOD` is read by sink-prometheus and `QUERYID_RETENTION_HOURS` +by the Flask backend: `docker compose up -d --force-recreate sink-prometheus monitoring_flask_backend` +(`update-config` does not restart services). When running Compose manually, set the keys before the +initial `docker compose up -d`. -## Remote write +## Storage -### Writing to external TSDB +### Disk allocation -Send metrics to additional destinations: +Estimate storage needs: -```yaml -# VictoriaMetrics remote write --remoteWrite.url=https://external-tsdb.example.com/api/v1/write --remoteWrite.basicAuth.username=user --remoteWrite.basicAuth.password=secret ``` +Storage = (metrics/sec) × (bytes/metric) × (retention_seconds) +``` + +Typical values: +- ~100 metrics/sec per monitored database +- ~3-5 bytes/sample (VictoriaMetrics with typical monitoring data) +- 14 days = 1,209,600 seconds -### Multi-tenancy +Worked example: 100 × 5 × 1,209,600 ≈ 605 MB of compressed samples; allowing for indexes and +on-disk overhead this rounds to roughly **~1 GiB per database for 14-day retention**, matching +the 14d row in the [retention table](#setting-retention-period) above. -For SaaS deployments with multiple customers: +### Memory allocation -```bash -# Enable multi-tenancy -VM_ENABLE_MULTI_TENANCY=true +VictoriaMetrics sizes its caches from the container memory limit. In this stack the service is +named `sink-prometheus` and its limit is set with `mem_limit` (bytes), overridable via the +`SINK_PROMETHEUS_MEM` `.env` variable (default 1.5 GiB): -# Tenant ID from label -VM_TENANT_LABEL=customer_id +```bash +# .env — raise the VictoriaMetrics memory limit to 2 GiB +SINK_PROMETHEUS_MEM=2147483648 ``` -## Clustering +`SINK_PROMETHEUS_MEM` sets the Compose `mem_limit`, which only takes effect when the +container is recreated — `postgresai mon update-config` does not recreate services, so +apply it by recreating sink-prometheus: `docker compose up -d --force-recreate sink-prometheus`. +See [Resource limits (per service)](/docs/monitoring/configuration#resource-limits-per-service). -### VictoriaMetrics cluster mode +## Scrape configuration -For high availability and horizontal scaling: +VictoriaMetrics scrapes the collectors and self-monitoring exporters using the bundled +`config/prometheus/prometheus.yml`. The main job pulls metrics from the `pgwatch-prometheus` +collector: ```yaml -services: - vmstorage-1: - image: victoriametrics/vmstorage - command: - - -storageDataPath=/data - - -retentionPeriod=30d - - vminsert: - image: victoriametrics/vminsert - command: - - -storageNode=vmstorage-1:8400,vmstorage-2:8400 - - -replicationFactor=2 - - vmselect: - image: victoriametrics/vmselect - command: - - -storageNode=vmstorage-1:8401,vmstorage-2:8401 -``` - -### Replication +global: + scrape_interval: 15s + scrape_timeout: 10s -```bash -# Replicate data across N storage nodes --replicationFactor=2 +scrape_configs: + # Main monitoring target: pgwatch metrics + - job_name: 'pgwatch-prometheus' + static_configs: + - targets: ['pgwatch-prometheus:9091'] + scrape_interval: 30s + scrape_timeout: 25s + metrics_path: /pgwatch + sample_limit: 10000 ``` -## Backup +The same file also defines self-monitoring jobs (`victoriametrics`, `self-cadvisor`, +`self-node-exporter`, `self-postgres-exporter`) and a `query-info` job that scrapes the Flask +backend's `/query_info_metrics` endpoint every 5 minutes for query-text labels. -### Creating backups +:::note Basic auth in the scrape file +The `victoriametrics` self-scrape job authenticates with `%{VM_AUTH_USERNAME}` / +`%{VM_AUTH_PASSWORD}` — VictoriaMetrics expands these percent-brace env references in the scrape +file (this is a VictoriaMetrics extension, not standard Prometheus syntax). +::: -```bash -# Snapshot-based backup -curl http://localhost:8428/snapshot/create +## Query and search tuning -# Download backup -vmbackup -snapshotName= -dst=s3://bucket/path -``` +VictoriaMetrics query/search behavior is tuned with two `.env` variables. These map directly to +the underlying VictoriaMetrics `-search.*` flags and have the same literal defaults whether or +not you set them — leaving them unset is a no-op. Set them in the monitoring stack `.env`, then +migrate `.env` with `postgresai mon update-config` and recreate sink-prometheus so it picks up +the new flags (`update-config` does not restart sink-prometheus, which reads these only at +container startup). -### Restore +| `.env` variable | Default | VictoriaMetrics flag | Purpose | +|-----------------|---------|----------------------|---------| +| `VM_QUERY_DURATION` | `30s` | `-search.maxQueryDuration` | Maximum duration of a single query before it is cancelled | +| `VM_MAX_CONCURRENT_REQUESTS` | `16` | `-search.maxConcurrentRequests` | Maximum number of concurrent search requests | ```bash -vmrestore -src=s3://bucket/path -storageDataPath=/var/lib/victoriametrics -``` +# Example overrides in .env +VM_QUERY_DURATION=30s +VM_MAX_CONCURRENT_REQUESTS=16 -## Performance tuning +postgresai mon update-config +docker compose up -d --force-recreate sink-prometheus +``` -### Compaction +:::note Canonical variable names +Earlier drafts referenced `VM_SEARCH_*` names; the shipped 0.15 variables are +`VM_QUERY_DURATION` and `VM_MAX_CONCURRENT_REQUESTS` as listed above. These match +[`.env` configuration](/docs/monitoring/configuration#cli-installation). +::: + +:::note Single-node VictoriaMetrics +This stack ships a **single-node** `victoriametrics/victoria-metrics` instance (the +`sink-prometheus` service). VictoriaMetrics cluster mode, remote write, multi-tenancy, and +downsampling are upstream/Enterprise features that are **not configured by this stack** and have +no `.env` or chart knobs here. The only supported VictoriaMetrics tuning is retention +(`VM_RETENTION_PERIOD`), the search limits above (`VM_QUERY_DURATION`, +`VM_MAX_CONCURRENT_REQUESTS`), basic auth (`VM_AUTH_USERNAME` / `VM_AUTH_PASSWORD`), and the +container memory limit (`SINK_PROMETHEUS_MEM`). +::: -```bash -# Merge compaction interval -VM_STORAGE_MIN_FREE_DISK_SPACE_BYTES=1 GiB -``` +## Backup -### Cache sizes +VictoriaMetrics backups use its native snapshot tooling. In the Docker Compose stack the +VictoriaMetrics API is published on host port `59090` (container port 9090): ```bash -# Index cache -VM_STORAGE_CACHE_SIZE_STORAGE_TSID=256MB +# Create a snapshot (VM basic auth required) +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + http://localhost:59090/snapshot/create -# Data cache -VM_STORAGE_CACHE_SIZE_INDEX_DB=128MB +# Back up / restore with vmbackup / vmrestore (run against the data path) +vmbackup -snapshotName= -dst=s3://bucket/path +vmrestore -src=s3://bucket/path -storageDataPath=/victoria-metrics-data ``` ## Monitoring VictoriaMetrics @@ -246,7 +292,9 @@ vm_data_size_bytes ### Check ingestion ```bash -curl http://localhost:8428/api/v1/status/tsdb +# Docker Compose publishes VictoriaMetrics on host port 59090 (container port 9090); VM basic auth +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + http://localhost:59090/api/v1/status/tsdb ``` ### Debug slow queries @@ -260,6 +308,7 @@ curl http://localhost:8428/api/v1/status/tsdb | Issue | Cause | Solution | |-------|-------|----------| -| High memory | Large queries | Increase VM_SEARCH_MAX_MEMORY_PER_QUERY | -| Slow queries | No indexes | Check cardinality, reduce label count | -| Disk full | Retention too long | Reduce retention or add storage | +| High memory | Large / concurrent queries | Lower `VM_MAX_CONCURRENT_REQUESTS`, reduce query cardinality | +| Slow queries | High cardinality | Check cardinality, reduce label count; consider lowering `VM_QUERY_DURATION` to fail fast | +| Disk full | Retention too long | Reduce `VM_RETENTION_PERIOD` or add storage | +| No data in Grafana | Missing VM auth | Set `VM_AUTH_USERNAME` / `VM_AUTH_PASSWORD`, run `mon update-config`, then recreate sink-prometheus + Grafana (`docker compose up -d --force-recreate sink-prometheus grafana`) so both pick up the credentials | diff --git a/docs/monitoring/dashboards/01-node-overview.md b/docs/monitoring/dashboards/01-node-overview.md index 02f4b70c..44c7ddb2 100644 --- a/docs/monitoring/dashboards/01-node-overview.md +++ b/docs/monitoring/dashboards/01-node-overview.md @@ -46,17 +46,17 @@ Similar to AWS RDS Performance Insights, this panel shows wait event distributio |----------|-------|-----------| | CPU* | Green | On-CPU activity (query execution) | | IO | Blue | Disk I/O waits | -| LWLock | Red | Lightweight lock contention | -| Lock | Orange | Row/table lock waits | -| Timeout | Gray | Sleep/timeout events | +| Lock | Red | Row/table lock waits | +| LWLock | Dark red | Lightweight lock contention | +| Timeout | Brown (`#6f450c`) | Sleep/timeout events | **Healthy state:** - Mostly green (CPU) with occasional blue (IO) - Total height below `max_connections * 0.5` **Warning signs:** -- Sustained red (LWLock) — Internal contention -- Sustained orange (Lock) — Application-level locking issues +- Sustained dark red (LWLock) — Internal contention +- Sustained red (Lock) — Application-level locking issues - Spikes above normal baseline — Sudden load increase ### Sessions @@ -83,17 +83,17 @@ Focused view of sessions doing actual work. - Stable pattern matching application load - No sudden spikes without corresponding application events -### TPS (transactions per second) +### TPS **What it shows:** -- Commit rate +- Transactions per second: commit rate - Rollback rate (if significant) **Use for:** - Capacity baseline - Detecting throughput drops -### QPS (queries per second) +### QPS (pg_stat_statements) From `pg_stat_statements`, showing actual query execution rate. @@ -119,12 +119,13 @@ From `pg_stat_statements`, showing actual query execution rate. 1. Verify pgwatch is collecting metrics: ```bash - docker compose logs pgwatch | grep -i "wait\|session" + docker compose logs pgwatch-postgres pgwatch-prometheus | grep -i "wait\|session" ``` -2. Check VictoriaMetrics has data: +2. Check VictoriaMetrics has wait-event data backing the ASH panel (host port `59090`, VM basic auth): ```bash - curl 'http://localhost:8428/api/v1/query?query=pg_stat_activity_count' + curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=pgwatch_wait_events_total' ``` ### Sessions count doesn't match pg_stat_activity diff --git a/docs/monitoring/dashboards/02-query-analysis.md b/docs/monitoring/dashboards/02-query-analysis.md index 0ee921b4..930f4b27 100644 --- a/docs/monitoring/dashboards/02-query-analysis.md +++ b/docs/monitoring/dashboards/02-query-analysis.md @@ -111,31 +111,31 @@ Identify the most resource-intensive queries across multiple dimensions: | `cluster_name` | Cluster filter | Your clusters | | `node_name` | Node filter | Specific nodes | | `db_name` | Database filter | Filter by database | -| `top_n` | Number of queries | 5, 10, 20, 50 | +| `top_n` | Number of queries | 5, 10, 15, 20, 50, 100, 500 | | `legend_label` | Query display format | See below | ### Legend label options -| Value | Shows | Example | -|-------|-------|---------| -| `queryid` | Numeric ID | `-4021163671685...` | -| `displayname` | Smart truncation | `update pgbench_ac...` | -| `displayname_long` | Full context | `update pgbench_accounts set abalance = abalance + $1 where aid = $2` | +The `legend_label` variable (**Query texts**) has two options: + +| Option | Value | Shows | +|--------|-------|-------| +| Smart truncation (default) | `displayname_long` | Query text with smart truncation | +| Raw texts | `displayname_raw_long` | Full raw query text | :::tip -Use `displayname_long` during debugging to see complete query context. +Switch to **Raw texts** (`displayname_raw_long`) when you need the complete, untruncated query text. ::: ## Detailed table view Expand the **Detailed table view** section for a tabular breakdown including: -- queryid +- Query ID - Query text - Calls -- Total time -- Mean time +- Exec time (ms) +- Exec time/call (ms) - Rows -- Hit ratio ![Detailed table view](/img/monitoring/dashboards/02-query-analysis-table-view.png) @@ -158,9 +158,12 @@ The dashboard shows actual query text (not just queryid) by: ### Query texts show as "unknown" or queryid only -1. Check Flask backend is running: +1. Check the Flask backend is running. It is not published to the host, so check from inside the + container. The backend image is `python:3.11-slim` and has no `curl`, so hit the endpoint with + the bundled Python interpreter: ```bash - curl http://localhost:8000/health + docker compose exec monitoring_flask_backend \ + python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)" ``` 2. Verify pg_stat_statements has data: @@ -190,4 +193,5 @@ limit 10; select pg_stat_statements_reset(); -- caution: resets all stats ``` -Or wait for the next scrape interval (default: 60s). +Or wait for the next collection interval. `pg_stat_statements` is collected every 30s by default +in the `full` preset (activity metrics such as `pg_stat_activity` and `wait_events` every 15s). diff --git a/docs/monitoring/dashboards/03-single-query.md b/docs/monitoring/dashboards/03-single-query.md index 825edd09..8099ef7a 100644 --- a/docs/monitoring/dashboards/03-single-query.md +++ b/docs/monitoring/dashboards/03-single-query.md @@ -29,12 +29,20 @@ When you've identified a problematic query in [02. Query analysis](/docs/monitor ### Query text -Displays the full, non-truncated query text for the selected `queryid`. +A table at the top of the dashboard displays the full, non-truncated query text for the selected `query_id`. :::tip The query shown uses parameter placeholders (`$1`, `$2`). For actual parameter values, check application logs. ::: +### Active session history + +**What it shows:** +- ASH-style breakdown of the wait events sampled for this `query_id` over time + +**Use for:** +- Seeing what this specific query spends its time waiting on (CPU, IO, locks, LWLocks) + ### Calls per second **What it shows:** @@ -71,17 +79,6 @@ The query shown uses parameter placeholders (`$1`, `$2`). For actual parameter v - Prioritizing optimization efforts - Measuring optimization impact -### Planning time - -**What it shows:** -- Query planning overhead -- Relevant for frequently executed queries - -**When planning time matters:** -- Queries with < 10ms execution time -- High-frequency OLTP queries -- Complex JOINs that benefit from plan caching - ### Rows per call **What it shows:** @@ -92,29 +89,39 @@ The query shown uses parameter placeholders (`$1`, `$2`). For actual parameter v - High rows fetched, low rows returned = inefficient filtering - Growing over time = data growth impact -### Shared buffer usage +### shared_blks_hit (in bytes) **What it shows:** -- Buffer hits vs physical reads +- Shared buffer hits for this query (total and per call), expressed in bytes - Cache efficiency for this query **Healthy state:** -- Hit ratio > 99% for frequently accessed data -- Low physical reads +- High hit volume relative to physical reads for frequently accessed data + +### WAL and temp file usage + +**What it shows:** +- WAL bytes and WAL fpi (full page images) generated by this query, total and per call +- Temp bytes read and written, total and per call + +**Use for:** +- Spotting queries that generate excessive WAL (write amplification) +- Catching queries that spill to temp files (raise `work_mem`) ## Variables | Variable | Purpose | How to Get | |----------|---------|------------| -| `queryid` | Query identifier | From Dashboard 02 or pg_stat_statements | +| `query_id` | Query identifier (textbox) | From Dashboard 02 or pg_stat_statements | | `cluster_name` | Cluster filter | Your cluster | | `node_name` | Node filter | Primary or replica | +| `db_name` | Database filter | Database names or `All` | -### Finding the queryid +### Finding the query_id From [02. Query analysis](/docs/monitoring/dashboards/query-analysis): 1. Hover over a query in any chart -2. Note the queryid from the legend +2. Note the queryid from the legend (it populates the `query_id` textbox here) From PostgreSQL directly: ```sql diff --git a/docs/monitoring/dashboards/04-wait-events.md b/docs/monitoring/dashboards/04-wait-events.md index e54d6e7c..1522b1a4 100644 --- a/docs/monitoring/dashboards/04-wait-events.md +++ b/docs/monitoring/dashboards/04-wait-events.md @@ -47,17 +47,22 @@ This dashboard uses wait event data collected from `pg_stat_activity` by pgwatch ## Key panels -### Wait event distribution +The dashboard has three panels, each an Active Session History view at a different level of detail: +**Active session history** (grouped by wait event type), **Active session history by event type** +(adds the specific wait event), and **Active session history by event type and event** (further +broken down per `query_id`). + +### Active session history **What it shows:** -- Stacked area chart of wait events over time +- Stacked bar chart of active sessions by wait event type over time - Each color represents a wait event type **Wait event categories:** | Category | Description | Common events | |----------|-------------|---------------| -| **CPU** | On-CPU processing | `CPU` | +| **CPU** | On-CPU processing | `CPU*` | | **IO** | Disk I/O operations | `DataFileRead`, `WALWrite` | | **LWLock** | Internal PostgreSQL locks | `BufferContent`, `LockManager` | | **Lock** | Row/table locks | `tuple`, `transactionid` | @@ -65,17 +70,11 @@ This dashboard uses wait event data collected from `pg_stat_activity` by pgwatch | **Activity** | Background processes | `LogicalLauncherMain` | | **IPC** | Inter-process communication | `BgWorkerStartup` | -### Top wait events - -**What it shows:** -- Ranked list of most common wait events -- Percentage of total wait time - -**Interpretation guide:** +**Interpretation guide** for the most common wait events you will see in these panels: | Wait event | Meaning | Action | |------------|---------|--------| -| `CPU` | Query processing | Normal if workload-appropriate | +| `CPU*` | Query processing | Normal if workload-appropriate | | `DataFileRead` | Reading from disk | Check shared_buffers, add memory | | `DataFileWrite` | Writing to disk | Normal for writes | | `WALWrite` | WAL I/O | Check storage speed | @@ -83,13 +82,17 @@ This dashboard uses wait event data collected from `pg_stat_activity` by pgwatch | `Lock:tuple` | Row lock wait | Check for lock conflicts | | `Lock:transactionid` | Transaction wait | Long transactions blocking | -### Wait events by database +### Active session history by event type -Breakdown showing which databases contribute most to waits. +**What it shows:** +- The same ASH view, additionally split out by the specific wait event (`wait_event`) within each + type, so you can see exactly which event dominates a wait type -### Wait events by query +### Active session history by event type and event -Links wait events to specific queries (when available). +**What it shows:** +- The ASH view further attributed to the `query_id` responsible, linking waits to specific queries + (when captured during sampling) **Limitations:** - Only captures queries active during sampling @@ -138,10 +141,11 @@ Lightweight locks are internal to PostgreSQL: | Variable | Purpose | |----------|---------| +| `wait_event_type` | Filter by wait event type | +| `wait_event` | Filter by specific wait event | | `cluster_name` | Cluster filter | | `node_name` | Node filter | | `db_name` | Database filter | -| `wait_event_type` | Filter by event type | ## Related dashboards @@ -155,23 +159,27 @@ Lightweight locks are internal to PostgreSQL: 1. Verify pgwatch is collecting metrics: ```bash - docker compose logs pgwatch | grep -i wait + docker compose logs pgwatch-postgres pgwatch-prometheus | grep -i wait ``` -2. Check VictoriaMetrics has wait event data: +2. Check VictoriaMetrics has wait event data (host port `59090`, VM basic auth): ```bash - curl 'http://localhost:8428/api/v1/query?query=pg_stat_activity_count' + curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=pgwatch_wait_events_total' ``` 3. Ensure `pg_stat_activity` is accessible to the monitoring user ### Wait events don't match RDS Performance Insights -Different sampling rates and methodologies may cause variations. PostgresAI monitoring uses: -- Default: 10ms sampling interval -- Aggregation into time buckets for visualization +Different collection mechanisms and methodologies may cause variations. PostgresAI monitoring does +not use a sub-second sampler. Instead, the `wait_events` metric is a snapshot `count(*)` of active +sessions in `pg_stat_activity` (`WHERE state = 'active'`), grouped by `wait_event_type` / +`wait_event`. This snapshot is collected on the metric interval — every 15 seconds in the `full` +preset — and then aggregated into time buckets for visualization. -RDS Performance Insights may use different intervals. +RDS Performance Insights samples at a higher frequency (roughly once per second), so absolute counts +and short-lived waits can differ. ### "Other" category too large diff --git a/docs/monitoring/dashboards/05-backups.md b/docs/monitoring/dashboards/05-backups.md index edb9c7c9..b25810cb 100644 --- a/docs/monitoring/dashboards/05-backups.md +++ b/docs/monitoring/dashboards/05-backups.md @@ -1,10 +1,10 @@ --- -title: "05. Backups" -sidebar_label: "05. Backups" +title: "05. WAL, backups, DR" +sidebar_label: "05. WAL, backups, DR" sidebar_position: 6 --- -# 05. Backups and DR +# 05. WAL, backups, DR Monitor backup status, WAL archiving, and disaster recovery readiness. @@ -28,45 +28,61 @@ Track backup health to ensure: ## Key panels -### WAL archiving status +The dashboard is organized into four rows: **WAL overview**, **WAL archiving**, **Replication slot +retention**, and **Configuration and WAL producers**. + +### pg_wal directory size + +**What it shows:** +- Size of the `pg_wal` directory over time + +**Warning signs:** +- Steady growth = WAL is accumulating (archiving stuck or an inactive replication slot pinning WAL) + +### WAL generation rate + +**What it shows:** +- WAL bytes generated per second +- Helps size archive storage and bandwidth + +### WAL archive success and errors / WAL archive success rate **What it shows:** -- WAL files waiting to be archived -- Archive success/failure rate -- Archive lag time +- Counts of successful vs failed archive attempts (from `pg_stat_archiver`) +- The success rate as a percentage **Healthy state:** -- `ready_count` near 0 (no backlog) -- Consistent archive rate matching WAL generation +- 100% success rate, errors flat at zero **Warning signs:** -- Growing `ready_count` = archiving falling behind - Archive failures = storage or network issues -### Last backup age +### Archive lag bytes / Archive lag time and files **What it shows:** -- Time since last successful backup -- Backup duration trend +- How far behind archiving is, in bytes, in time, and in number of unarchived files -**Healthy range:** -- Within your backup schedule (e.g., < 24h for daily backups) +**Healthy state:** +- Lag near 0 (no backlog), consistent archive rate matching WAL generation -### WAL generation rate +**Warning signs:** +- Growing lag = archiving falling behind + +### Retained WAL by replication slot / Inactive replication slots **What it shows:** -- WAL bytes generated per second -- Helps size archive storage and bandwidth +- WAL retained on behalf of each replication slot +- Count of inactive replication slots -### Checkpoint activity +**Warning signs:** +- An inactive slot retaining large amounts of WAL can fill the disk -**What it shows:** -- Checkpoint frequency and duration -- Checkpoint write/sync times +### WAL-related settings / Top queries by WAL bytes/s -**Healthy state:** -- Checkpoints completing within `checkpoint_timeout` -- No checkpoint warnings in logs +**What it shows:** +- A table of WAL-related configuration settings (e.g. `archive_mode`, `archive_command`, + `wal_keep_size`, `max_wal_size`, `checkpoint_timeout`) +- The queries generating the most WAL per second (from `pg_stat_statements`) ## Variables @@ -74,6 +90,7 @@ Track backup health to ensure: |----------|---------| | `cluster_name` | Cluster filter | | `node_name` | Node filter | +| `db_name` | Database filter | ## Backup tools integration diff --git a/docs/monitoring/dashboards/06-replication.md b/docs/monitoring/dashboards/06-replication.md index 652ad724..1420dce6 100644 --- a/docs/monitoring/dashboards/06-replication.md +++ b/docs/monitoring/dashboards/06-replication.md @@ -26,65 +26,18 @@ Ensure replication health for: - Validating HA setup - Capacity planning for replicas -## Key panels +## Dashboard status -### Replication lag (Bytes) - -**What it shows:** -- Bytes of WAL not yet replayed on replica -- Per-replica breakdown - -**Healthy range:** -- < 1 MB for synchronous replication -- < 100 MB for async (depends on workload) - -**Warning signs:** -- Growing lag = replica can't keep up -- Sudden spikes = network issues or replica overload - -### Replication lag (Time) - -**What it shows:** -- Estimated time behind primary -- More intuitive than bytes for SLA monitoring - -**Calculation:** -Based on WAL generation rate and byte lag. - -### Replication slot status - -**What it shows:** -- Active slots and their consumers -- Slot lag (retained WAL) - -**Warning signs:** -- Inactive slots with growing lag = WAL retention risk -- Slots without active connections +In 0.15.0 this dashboard ships as a single placeholder panel ("Coming soon...") and has no data +panels or template variables yet. Replication metrics are still collected by the stack (for +example `replication`, `replication_slots`, and `pg_stat_replication` in the `full` preset), so +until the visualizations land you can inspect replication health directly via SQL using the +queries below. :::warning WAL retention Unused replication slots prevent WAL cleanup and can fill disk. ::: -### Sent vs replayed - -**What it shows:** -- WAL sent to replica -- WAL replayed (applied) on replica -- Gap indicates apply lag - -### Replica connections - -**What it shows:** -- Connected replicas -- Connection state (streaming, catchup) - -## Variables - -| Variable | Purpose | -|----------|---------| -| `cluster_name` | Cluster filter | -| `node_name` | Primary or replica | - ## Replication modes ### Streaming replication diff --git a/docs/monitoring/dashboards/07-autovacuum.md b/docs/monitoring/dashboards/07-autovacuum.md index 20601233..1b706249 100644 --- a/docs/monitoring/dashboards/07-autovacuum.md +++ b/docs/monitoring/dashboards/07-autovacuum.md @@ -1,92 +1,183 @@ --- -title: "07. Autovacuum" -sidebar_label: "07. Autovacuum" +title: "07. Autovacuum and xmin horizon" +sidebar_label: "07. Autovacuum & xmin horizon" sidebar_position: 8 +keywords: + - "PostgreSQL autovacuum dashboard" + - "xmin horizon" + - "transaction ID wraparound" + - "vacuum monitoring" + - "dead tuple monitoring" --- -# 07. Autovacuum and bloat +# 07. Autovacuum and xmin horizon -Monitor vacuum activity, dead tuple accumulation, and table bloat. - -:::info Dashboard in development -This dashboard is currently under development. Autovacuum and bloat metrics are collected as part of the health check system, and the full dashboard visualization is coming soon. -::: +Monitor autovacuum activity, transaction ID / MultiXID wraparound risk, and — new in 0.15 — the +**xmin horizon**: how far back the oldest snapshot reaches and which sessions, replication +slots, standbys, or prepared transactions are holding it back. (Per-table dead-tuple and bloat +detail lives on [08. Table stats](/docs/monitoring/dashboards/table-stats) and +[10. Index health](/docs/monitoring/dashboards/index-health).) ## Purpose -Track autovacuum health to prevent: -- Table bloat degrading query performance +Track autovacuum health and the xmin horizon to prevent: + +- Dead tuples that vacuum cannot clean up because a snapshot still needs them +- Table and index bloat degrading query performance - Transaction ID wraparound emergencies -- Excessive dead tuple accumulation +- WAL and slot retention growth driven by a stuck horizon + +The xmin horizon is the single most useful signal when vacuum runs but bloat keeps growing: +PostgreSQL can only remove dead tuples that are older than the oldest snapshot still in use +anywhere in the cluster. This dashboard attributes the horizon to its blocker so you can act +on the real cause instead of just running `VACUUM` again. ## When to use - Routine maintenance monitoring -- Investigating slow sequential scans +- Investigating bloat that vacuum is not reducing +- Diagnosing "dead tuples high but autovacuum is running" +- Tracking down long-running transactions, idle-in-transaction sessions, stale replication + slots, lagging standbys, or orphaned prepared transactions - Tuning autovacuum settings - Diagnosing disk space growth +## The xmin horizon + +PostgreSQL keeps a row version visible as long as any transaction might still need it. The +oldest such transaction defines the **xmin horizon**. While the horizon is held back, vacuum +cannot reclaim dead tuples even on busy tables, so bloat accumulates and the wraparound clock +keeps ticking. + +The horizon can be held back by five classes of "blocker", each tracked as a separate +component on this dashboard: + +| Blocker class | Source | Typical cause | +|---------------|--------|---------------| +| Client backends | `pg_stat_activity` (`backend_xmin`) | Long-running query or `idle in transaction` session | +| Replication slots (data) | `pg_replication_slots.xmin` | Inactive or lagging physical/logical slot | +| Replication slots (catalog) | `pg_replication_slots.catalog_xmin` | Logical slot holding the catalog horizon | +| Standby feedback | `pg_stat_replication.backend_xmin` | Replica with `hot_standby_feedback = on` lagging | +| Prepared transactions | `pg_prepared_xacts` | Orphaned two-phase commit (`PREPARE TRANSACTION`) | + +The dashboard separates the **data horizon** (blocks cleanup of ordinary table tuples) from +the **catalog horizon** (blocks cleanup of system catalogs, relevant to logical replication), +because a logical slot can hold the catalog horizon far behind the data horizon. + +:::tip Companion how-to +For a step-by-step methodology, see +[How to monitor the xmin horizon](/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-xmin-horizon) +and +[How to monitor transaction ID wraparound risks](/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-transaction-id-wraparound-risks). +::: + ## Key panels -### Autovacuum workers +The dashboard is organized into three sections: **Wraparound risk — top-N tables**, **xmin +horizon overview (experimental)**, and **Autovacuum mechanics**. + +### Wraparound risk — top-N tables + +#### Top-N tables by XID age (relfrozenxid) / by MultiXID age (relminmxid) **What it shows:** -- Active autovacuum workers -- Worker utilization vs `autovacuum_max_workers` +- The top-N tables by transaction ID (`relfrozenxid`) age and by MultiXID (`relminmxid`) age +- Reference lines for `autovacuum_freeze_max_age` / `vacuum_failsafe_age` (and the multixact + equivalents), plus the ~2.1B wraparound limit -**Healthy state:** -- Workers active during low-traffic periods -- Not constantly at max workers +**Critical thresholds:** +- Warning: age approaching `autovacuum_freeze_max_age` +- Critical: age approaching `vacuum_failsafe_age` / the 2B limit -**Warning signs:** -- Always at max workers = autovacuum can't keep up -- Zero workers for extended periods = check if enabled +:::danger Transaction ID wraparound +If a database reaches roughly 2 billion transactions without freezing, PostgreSQL stops +accepting writes to prevent data corruption. A stuck xmin horizon makes wraparound risk worse +because it blocks the freezing that vacuum would otherwise perform. +::: -### Dead tuples by table +### xmin horizon overview (experimental) + +:::note Experimental in 0.15 +This section is labeled **(experimental)** on the shipped dashboard. The panel titles and +signals may change in a future release. +::: + +#### xmin horizon age by source **What it shows:** -- Tables with most dead tuples -- Rate of dead tuple accumulation +- Data horizon age and catalog horizon age, in transactions (the `*_age_tx` signals) +- The per-component blocker ages (`pg_stat_activity`, `pg_replication_slots`, + `pg_replication_slots` catalog, `pg_stat_replication`, `pg_prepared_xacts`) as separate series **Healthy state:** -- Dead tuples cleared periodically by vacuum -- No single table dominating +- Horizon age stays low and tracks normal transaction throughput +- No single component dominates for long periods **Warning signs:** -- Dead tuples growing unbounded -- Ratio of dead to live tuples > 20% +- Horizon age climbing steadily — something is pinning old snapshots +- Catalog horizon far older than data horizon — a logical replication slot is stuck -### Tables approaching wraparound +#### Longest non-idle transaction age, > 1 min **What it shows:** -- Tables closest to transaction ID wraparound -- Age of oldest transaction (datfrozenxid) +- The age of the longest-running non-idle transaction (over 1 minute), a common cause of a + `pg_stat_activity` backend pinning the horizon -**Critical thresholds:** -- Warning: age > 500 million -- Critical: age > 1 billion (approaching 2B limit) +#### Current blocker counts -:::danger Transaction ID wraparound -If a table reaches 2 billion transactions without vacuum, PostgreSQL will shut down to prevent data corruption. +**What it shows:** +- The number of active blockers per component (`pg_stat_activity`, `pg_replication_slots`, + `pg_replication_slots` catalog, `pg_stat_replication`, `pg_prepared_xacts`) + +**Interpretation:** +- A `pg_stat_activity` blocker → find the session (use the `queryid` to inspect the query + text) and end the long transaction or fix the idle-in-transaction leak +- A `pg_replication_slots` blocker → check whether the slot is still needed; drop or advance it +- A `pg_stat_replication` blocker → a standby with `hot_standby_feedback` is lagging +- A `pg_prepared_xacts` blocker → resolve the orphaned prepared transaction with + `COMMIT PREPARED` / `ROLLBACK PREPARED` + +:::note Monitoring-user noise filtered +The blocker signals intentionally exclude the monitoring role's own sessions, so the monitoring +stack never reports itself as the top blocker. ::: -### Vacuum progress +### Autovacuum mechanics + +#### Autovacuum debt — top-N overdue tables **What it shows:** -- Currently running vacuum operations -- Phase and progress percentage -- Estimated completion +- The top-N tables by how far past their autovacuum threshold they are (the overdue factor) +- A reference line at the threshold (factor 1) where autovacuum should trigger + +**Warning signs:** +- Tables persistently above factor 1 — autovacuum is not keeping up on those tables -### Table bloat estimates +#### Autovacuum worker pool — active vs max **What it shows:** -- Estimated wasted space per table -- Based on dead tuple ratio and page density +- Active autovacuum workers vs `autovacuum_max_workers`, plus worker utilization -**Interpretation:** -- < 20% bloat: Normal -- 20-50% bloat: Consider manual vacuum -- > 50% bloat: May need VACUUM FULL or pg_repack +**Healthy state:** +- Workers active during low-traffic periods +- Not constantly at max workers + +**Warning signs:** +- Always at max workers = autovacuum can't keep up +- Zero workers for extended periods = check if enabled + +#### Autovacuum workers blocked on lock + +**What it shows:** +- Autovacuum workers that are stuck waiting on a lock, with how long they have been blocked + +**Warning signs:** +- Workers blocked for extended periods — a conflicting lock is stalling vacuum progress + +#### Vacuum timeline + +**What it shows:** +- A timeline of vacuum activity per table (including index-vacuum cycles and vacuum mode) ## Variables @@ -95,6 +186,12 @@ If a table reaches 2 billion transactions without vacuum, PostgreSQL will shut d | `cluster_name` | Cluster filter | | `node_name` | Node filter | | `db_name` | Database filter | +| `schema_name` | Schema filter (for the top-N table panels) | +| `table_name` | Table filter (for the top-N table panels) | +| `top_n` | How many tables to show in top-N panels (5, 10, 15, 20, 25, 50, 100) | + +The xmin horizon panels are instance-level (collected on the primary), so they are scoped by +`cluster_name` / `node_name` rather than per database. ## Autovacuum tuning @@ -122,6 +219,14 @@ alter table large_table set ( - **Table details** — [08. Table stats](/docs/monitoring/dashboards/table-stats) - **Single table** — [09. Single table](/docs/monitoring/dashboards/single-table) - **Index bloat** — [10. Index health](/docs/monitoring/dashboards/index-health) +- **Lock contention** — [13. Lock contention](/docs/monitoring/dashboards/lock-contention) + +## Metrics reference + +The signals behind this dashboard are documented in the +[monitoring reference](/docs/reference-guides/postgres-ai-monitoring-reference): the +`xmin_horizon` and `xmin_horizon_blockers` metric groups (xmin horizon attribution) and the +`pg_database_wraparound` / `pg_vacuum_progress` / table-statistics groups (vacuum and bloat). ## Troubleshooting @@ -144,17 +249,41 @@ alter table large_table set ( ### Vacuum running but not reducing bloat -1. Check for long-running transactions: +This is the classic xmin-horizon symptom. Start with the **xmin horizon age by source** and +**Current blocker counts** panels, then confirm from SQL: + +1. Check for long-running or idle-in-transaction sessions holding `backend_xmin`: ```sql - select pid, age(backend_xmin), query + select pid, state, age(backend_xmin) as xmin_age, query from pg_stat_activity where backend_xmin is not null order by age(backend_xmin) desc; ``` -2. Check for unused replication slots holding back vacuum +2. Check replication slots holding the horizon back: + ```sql + select slot_name, slot_type, active, + age(xmin) as xmin_age, + age(catalog_xmin) as catalog_xmin_age + from pg_replication_slots + where xmin is not null or catalog_xmin is not null + order by greatest(coalesce(age(xmin), 0), coalesce(age(catalog_xmin), 0)) desc; + ``` + +3. Check standby feedback and prepared transactions: + ```sql + select application_name, age(backend_xmin) as xmin_age + from pg_stat_replication + where backend_xmin is not null; -3. Consider VACUUM FULL for severely bloated tables (requires downtime) + select gid, prepared, owner, age(transaction) as xmin_age + from pg_prepared_xacts + order by age(transaction) desc; + ``` + +4. Once the blocker is gone, the horizon advances and autovacuum reclaims the dead tuples on + its next pass. For severely bloated tables, consider `VACUUM FULL` or `pg_repack` (requires + a lock / downtime). ### High wraparound age @@ -170,3 +299,6 @@ limit 20; -- Manual vacuum freeze vacuum freeze table_name; ``` + +Always clear the xmin horizon blocker first — `VACUUM FREEZE` cannot freeze rows newer than +the oldest snapshot still in use. diff --git a/docs/monitoring/dashboards/08-table-stats.md b/docs/monitoring/dashboards/08-table-stats.md index 68ec134b..30898274 100644 --- a/docs/monitoring/dashboards/08-table-stats.md +++ b/docs/monitoring/dashboards/08-table-stats.md @@ -27,60 +27,43 @@ Identify tables that need attention: ## Key panels -### Tables by sequential scans +The dashboard opens with a **Detailed table view** table and is then organized into four rows of +top-N panels: **Size stats**, **Activity stats**, **IO stats**, and **Estimated bloat stats**. -**What it shows:** -- Tables with highest sequential scan counts -- Rate of seq scans over time - -**Warning signs:** -- Large tables with high seq scan rate — may need indexes -- Growing seq scan trend on tables that should use indexes - -### Tables by size +### Size stats **What it shows:** -- Largest tables by total size (data + indexes + toast) -- Growth trend over time +- Top-N tables by total size, heap size (excl. TOAST), TOAST size, and indexes size +- A matching growth-per-second panel for each (total, heap, TOAST, indexes) **Use for:** - Capacity planning - Identifying candidates for partitioning - Storage optimization -### Tables by dead tuples +### Activity stats **What it shows:** -- Tables with most dead tuples -- Dead tuple accumulation rate - -**Healthy state:** -- Dead tuples cleared regularly by autovacuum -- No single table dominating +- Top-N tables by tuple inserts, deletes, HOT updates, and non-HOT updates per second +- Top-N tables by sequential reads of live tuples and by index fetches of live tuples -**Warning signs:** -- Continuously growing dead tuples — autovacuum not keeping up -- High ratio of dead to live tuples +**Interpretation:** +- High HOT updates vs non-HOT updates is good (HOT avoids index updates) +- Large tables high in sequential reads of live tuples may be missing indexes -### Tables by insert/update/delete rate +### IO stats **What it shows:** -- Write activity by table -- Helps identify hot tables +- Top-N tables by total/heap/TOAST/index shared block hits and reads +- Total shared block hit ratio and read ratio per table -### HOT update ratio - -**What it shows:** -- Percentage of updates using Heap-Only Tuples -- Higher is better (avoids index updates) +**Healthy state:** +- High hit ratio for hot tables, minimal reads -**Healthy range:** -- HOT ratio > 90% for frequently updated tables +### Estimated bloat stats -**Low HOT ratio causes:** -- Updates to indexed columns -- `fillfactor` not set appropriately -- Index bloat +**What it shows:** +- Top-N tables by estimated heap bloat % and by estimated heap bloat size ## Variables @@ -89,6 +72,15 @@ Identify tables that need attention: | `cluster_name` | Cluster filter | | `node_name` | Node filter | | `db_name` | Database filter | +| `schema_name` | Schema filter | +| `top_n` | Number of tables to show in top-N panels (5, 10, 15, 20, 50, 100) | + +:::note Top-N filtering +Per-relation panels use `topk($top_n, ...)` to show only the highest-ranked tables and drop the +long tail — they do not aggregate the remainder into a separate series. If the table you need is +not shown, raise `top_n` (or use [09. Single table](/docs/monitoring/dashboards/single-table)). +See [Top-N filtering](/docs/monitoring/dashboards/#top-n-filtering). +::: ## Interpreting table metrics @@ -119,7 +111,7 @@ Bloat shown in this dashboard is estimated based on: - Free space map - Statistical sampling -For accurate bloat measurement, use `pgstattuple` extension. +For accurate bloat measurement, use the `pgstattuple` extension. ## Related dashboards diff --git a/docs/monitoring/dashboards/09-single-table.md b/docs/monitoring/dashboards/09-single-table.md index 375c8447..af9c9a3a 100644 --- a/docs/monitoring/dashboards/09-single-table.md +++ b/docs/monitoring/dashboards/09-single-table.md @@ -31,39 +31,31 @@ When you've identified a problematic table in [08. Table Stats](/docs/monitoring ## Key panels -### Table size over time +The dashboard is organized into four rows: **Size stats**, **Estimated bloat stats**, **Activity +stats**, and **IO stats**. + +### Size stats **What it shows:** -- Total table size (data + toast + indexes) -- Growth trend +- **Table logical size distribution** — the logical size of the table over time +- **Size growth /s** — the rate of size change **Use for:** - Capacity forecasting - Detecting unexpected growth - Measuring impact of cleanup operations -### Sequential vs index scans - -**What it shows:** -- Scan type distribution over time -- Helps identify query pattern changes - -**Healthy pattern:** -- Predominantly index scans for OLTP tables -- Sequential scans acceptable for small tables or analytics - -### Tuple statistics +### Estimated bloat stats **What it shows:** -- Live tuples -- Dead tuples -- Inserts, updates, deletes per second +- **Estimated bloat %** and **Estimated bloat size** for the table -### HOT updates +### Activity stats **What it shows:** -- HOT update count and ratio -- Non-HOT updates +- **Tuple operations /s** — inserts, updates (HOT and non-HOT), and deletes per second +- **Tuple operations distribution (%)** — the same operations as a share of the total +- **Tuple fetch methods /s** — sequential vs index access over time **Improving HOT ratio:** ```sql @@ -71,12 +63,14 @@ When you've identified a problematic table in [08. Table Stats](/docs/monitoring alter table your_table set (fillfactor = 80); ``` -### Last vacuum/analyze +### IO stats **What it shows:** -- Time since last vacuum -- Time since last analyze -- Auto vs manual operations +- **Shared block hits /s** and **Shared block reads /s** +- **Shared block hit ratio** + +**Healthy pattern:** +- High hit ratio, minimal reads for hot tables ## Variables @@ -85,6 +79,7 @@ alter table your_table set (fillfactor = 80); | `cluster_name` | Cluster filter | | `node_name` | Node filter | | `db_name` | Database filter | +| `schema_name` | Schema filter | | `table_name` | Specific table to analyze | ## Table information queries @@ -153,7 +148,7 @@ where relname = 'your_table'; Some metrics require activity to populate: - Run queries against the table -- Wait for next metrics collection cycle (60s default) +- Wait for next metrics collection cycle (table stats collect every 30s by default) ### Size metrics don't match pg_relation_size diff --git a/docs/monitoring/dashboards/10-index-health.md b/docs/monitoring/dashboards/10-index-health.md index 25b2745a..30038bc1 100644 --- a/docs/monitoring/dashboards/10-index-health.md +++ b/docs/monitoring/dashboards/10-index-health.md @@ -31,59 +31,58 @@ Identify indexes that need attention: ## Key panels -### Unused indexes +The dashboard opens with a **Detailed index view** table and is then organized into four rows of +top-N panels: **Size stats**, **Estimated bloat stats**, **Index usage stats**, and **IO stats**. + +### Size stats **What it shows:** -- Indexes with zero or very low scan count -- Size of unused indexes +- **Top $top_n indexes by size** — the largest indexes -**Warning signs:** -- Large indexes with zero scans — candidates for removal -- Indexes unused since last stats reset +**Use for:** +- Spotting oversized indexes that dominate storage :::warning Before dropping -Verify index isn't used for: -- Unique constraints -- Foreign key references -- Periodic batch jobs (check longer time range) +The aggregated panels here show size, bloat, usage, and I/O — use them together with the +**Detailed index view** table and a longer time range to find genuinely unused indexes. Before +dropping any index, verify it isn't backing a unique constraint, a foreign key, or a periodic +batch job, and drill into [11. Single index](/docs/monitoring/dashboards/single-index). ::: -### Index size by table +### Estimated bloat stats **What it shows:** -- Total index size per table -- Index to table size ratio - -**Healthy range:** -- Index size typically 20-100% of table size -- Ratio > 200% may indicate over-indexing +- **Top $top_n indexes by estimated bloat %** and **by estimated bloat size** -### Index scan rate +### Index usage stats **What it shows:** -- Index usage frequency -- Trends in index utilization - -### Redundant indexes +- **Top $top_n indexes by tuples read** and **by tuples fetched** -**What it shows:** -- Indexes that are subsets of other indexes -- Example: `(a)` is redundant if `(a, b)` exists +**Interpretation:** +- Indexes with persistently zero tuples read over a long range are candidates for removal -### Index bloat estimates +### IO stats **What it shows:** -- Estimated wasted space in indexes -- Based on statistical analysis +- **Top $top_n indexes by block reads** and **by block hits** ## Variables | Variable | Purpose | |----------|---------| +| `top_n` | Number of indexes to show in top-N panels (10, 15, 20, 50, 100) | | `cluster_name` | Cluster filter | | `node_name` | Node filter | | `db_name` | Database filter | +:::note Top-N filtering +Per-index panels use `topk($top_n, ...)` to show only the highest-ranked indexes and drop the +long tail — they do not aggregate the remainder into a separate series. If the index you need is +not shown, raise `top_n` (or use [11. Single index](/docs/monitoring/dashboards/single-index)). +See [Top-N filtering](/docs/monitoring/dashboards/#top-n-filtering). +::: + ## Index analysis queries ### Find unused indexes diff --git a/docs/monitoring/dashboards/11-single-index.md b/docs/monitoring/dashboards/11-single-index.md index b1c3edf0..076c39cb 100644 --- a/docs/monitoring/dashboards/11-single-index.md +++ b/docs/monitoring/dashboards/11-single-index.md @@ -31,11 +31,8 @@ When investigating a specific index from [10. Index Health](/docs/monitoring/das ## Key panels -### Index scans over time - -**What it shows:** -- Scan frequency trend -- Helps identify usage patterns (batch jobs, peak hours) +The dashboard is organized into four rows: **Size stats**, **Index usage stats**, **IO stats**, +and **Estimated bloat stats**. ### Index size @@ -43,7 +40,13 @@ When investigating a specific index from [10. Index Health](/docs/monitoring/das - Current index size - Size trend over time -### Tuples read vs fetched +### Index scans + +**What it shows:** +- Scan frequency trend +- Helps identify usage patterns (batch jobs, peak hours) + +### Tuples read and fetched **What it shows:** - `idx_tup_read` — tuples returned by index @@ -53,11 +56,22 @@ When investigating a specific index from [10. Index Health](/docs/monitoring/das - Large gap may indicate index-only scans (good) - Or visibility map issues requiring heap fetches -### Index bloat estimate +### Shared block reads and hits **What it shows:** -- Estimated wasted space -- Bloat percentage +- Buffer reads and hits for this index over time + +**Healthy state:** +- Hits dominate reads for a frequently used index + +### Boguk ratio (index size / reltuples) + +**What it shows:** +- The Boguk ratio — index size divided by the table's estimated row count (`reltuples`) — a + bloat proxy in this dashboard's **Estimated bloat stats** row + +**Interpretation:** +- A rising ratio over time suggests the index is bloating relative to the rows it covers ## Variables @@ -65,7 +79,8 @@ When investigating a specific index from [10. Index Health](/docs/monitoring/das |----------|---------| | `cluster_name` | Cluster filter | | `node_name` | Node filter | -| `db_name` | Database filter | +| `datname` | Database filter (label "DB name") | +| `schema_name` | Schema filter | | `index_name` | Specific index to analyze | ## Index information queries diff --git a/docs/monitoring/dashboards/12-slru.md b/docs/monitoring/dashboards/12-slru.md index 2672a774..ab8e9b90 100644 --- a/docs/monitoring/dashboards/12-slru.md +++ b/docs/monitoring/dashboards/12-slru.md @@ -33,20 +33,28 @@ Poor SLRU performance can cause system-wide slowdowns. ## Key panels -### SLRU blocks read - -**What it shows:** -- Blocks read from each SLRU cache -- Breakdown by cache type +The dashboard's **SLRU stats** row has one panel per `pg_stat_slru` counter, each broken down by +cache type (name). ### SLRU blocks hit **What it shows:** -- Cache hit rate -- Higher is better +- Blocks served from each SLRU cache (cache hits) +- Higher relative to reads is better **Healthy range:** -- Hit rate > 99% for most caches +- Hits dominate reads for most caches + +### SLRU blocks exist + +**What it shows:** +- `blks_exists` — checks whether a block already exists in the cache + +### SLRU blocks read + +**What it shows:** +- Blocks read into each SLRU cache from disk +- Breakdown by cache type ### SLRU blocks written @@ -54,6 +62,17 @@ Poor SLRU performance can cause system-wide slowdowns. - Write activity to SLRU caches - High writes may indicate configuration issues +### SLRU blocks zeroed + +**What it shows:** +- `blks_zeroed` — newly initialized (zeroed) SLRU pages + +### SLRU truncates / SLRU flushes + +**What it shows:** +- `truncates` — SLRU segment truncations (e.g. as old transaction data is removed) +- `flushes` — SLRU buffer flushes to disk + ### SLRU cache types | Cache | Purpose | Tuning parameter | @@ -71,6 +90,7 @@ Poor SLRU performance can cause system-wide slowdowns. |----------|---------| | `cluster_name` | Cluster filter | | `node_name` | Node filter | +| `db_name` | Database filter | ## SLRU statistics query diff --git a/docs/monitoring/dashboards/13-lock-contention.md b/docs/monitoring/dashboards/13-lock-contention.md index c51559a7..095afc36 100644 --- a/docs/monitoring/dashboards/13-lock-contention.md +++ b/docs/monitoring/dashboards/13-lock-contention.md @@ -31,31 +31,27 @@ Diagnose lock-related performance issues: ## Key panels -### Lock waits over time +The dashboard has a **Blocking overview** row (five timeseries panels) and a **Blocking tree** +row (one table). + +### Lock conflicts **What it shows:** -- Number of sessions waiting for locks -- Lock wait duration distribution +- Number of lock-wait conflicts over time **Warning signs:** -- Sustained lock waits -- Increasing wait times +- Sustained conflicts - Correlation with specific operations -### Blocking chains +### Wait duration **What it shows:** -- Which sessions are blocking others -- Depth of blocking chains - -**Interpretation:** -- Single blocker affecting many — address that query -- Deep chains — potential design issue +- How long blocked backends have been waiting for locks (in ms) -### Locks by type +### By lock type **What it shows:** -- Distribution of lock types +- Distribution of lock-wait conflicts by lock type - Most common contention points | Lock type | Description | Common cause | @@ -65,11 +61,50 @@ Diagnose lock-related performance issues: | `AccessExclusiveLock` | DDL operations | ALTER TABLE, DROP | | `ShareLock` | Index creation | CREATE INDEX | -### Lock wait duration +### Blocker age + +**What it shows:** +- Age of the blocking transaction (in ms) — how long the blocker has held its lock + +### By table + +**What it shows:** +- Lock-wait conflicts broken down by the table being contended + +### Blocking tree **What it shows:** -- How long queries wait for locks -- Percentile distribution +- A table view of the blocked/blocker relationships (the blocking chain), including the + blocked and blocking PIDs + +**Interpretation:** +- A single blocker affecting many rows — address that query +- Deep chains — potential design issue + +## Lock-wait metrics carry session PIDs + +New in 0.15. The collected lock-wait metrics (the `lock_waits` metric group) now expose the +blocked and blocking backend PIDs as **labels**, so you can identify the blocker directly in +Grafana / PromQL without running the manual `pg_locks` join below. + +Available labels include `blocked_pid` and `blocker_pid`, plus `blocked_user` / `blocker_user`, +`blocked_appname` / `blocker_appname`, `blocked_table` / `blocker_table`, and `blocked_query_id` / +`blocker_query_id` (plus `datname`). The two gauges are `pgwatch_lock_waits_blocked_ms` (how long +the blocked backend has waited) and `pgwatch_lock_waits_blocker_tx_ms` (age of the blocking +transaction). See the +[monitoring reference](/docs/reference-guides/postgres-ai-monitoring-reference#lock-waits-lock_waits). + +```promql +# Longest current lock waits, labeled with blocker/blocked PIDs +topk(10, pgwatch_lock_waits_blocked_ms) +``` + +Because the `blocker_pid` is on the metric itself, you can read it straight off the panel and +terminate the blocker without any blocking-chain SQL: + +```sql +select pg_terminate_backend(); +``` ## Variables @@ -81,6 +116,12 @@ Diagnose lock-related performance issues: ## Lock analysis queries +:::tip +The queries below remain useful for ad-hoc investigation, but in 0.15 you no longer need them +just to find the blocking PID — it is available as the `blocker_pid` label on the lock-wait +metrics (see [above](#lock-wait-metrics-carry-session-pids)). +::: + ### Current lock waits ```sql @@ -148,9 +189,10 @@ where deadlocks > 0; where pid in (select pid from pg_locks where not granted); ``` -2. **Terminate if necessary:** +2. **Terminate if necessary** (use the `blocker_pid` label from the lock-wait metric, or the + `blocking_pid` from the query above): ```sql - select pg_terminate_backend(blocking_pid); + select pg_terminate_backend(); ``` ### Preventive measures diff --git a/docs/monitoring/dashboards/14-io-statistics.md b/docs/monitoring/dashboards/14-io-statistics.md new file mode 100644 index 00000000..fb12a0a8 --- /dev/null +++ b/docs/monitoring/dashboards/14-io-statistics.md @@ -0,0 +1,149 @@ +--- +title: "14. I/O statistics (pg_stat_io)" +sidebar_label: "14. I/O statistics" +sidebar_position: 15 +keywords: + - "pg_stat_io" + - "PostgreSQL I/O monitoring" + - "I/O statistics dashboard" + - "backend type I/O" + - "PostgreSQL 16 I/O" +--- + +# 14. I/O statistics (pg_stat_io) + +New in 0.15. Monitor PostgreSQL I/O activity broken down by backend type using the +`pg_stat_io` view. + +:::note PostgreSQL 16+ required +`pg_stat_io` was introduced in PostgreSQL 16. On PostgreSQL 15 and earlier this dashboard has +no data — the collector emits nothing for the `pg_stat_io` metric group on those versions. See +[System requirements](/docs/monitoring/getting-started/requirements#postgresql-requirements). +::: + +## Purpose + +Understand where I/O is happening inside PostgreSQL and which backend type is responsible: + +- Distinguish client-backend reads from background-writer, checkpointer, autovacuum, and WAL I/O +- See cache hits versus actual reads +- Spot excessive evictions (shared buffers under pressure) +- Track relation extends (table/index growth) and fsync activity + +`pg_stat_io` exposes operations that older views could not attribute to a backend type, which +makes it far easier to tell, for example, whether reads come from queries or from vacuum. + +## When to use + +- Investigating disk I/O pressure on PostgreSQL 16+ +- Deciding whether `shared_buffers` is too small (high evictions / reuses) +- Attributing read/write load to a specific backend type +- Correlating I/O spikes with checkpointer or autovacuum activity + +## Key panels + +The dashboard is organized into four rows: **I/O overview**, **I/O by backend type**, +**Writebacks, fsyncs, and file extends**, and **Buffer cache efficiency**. + +### I/O overview + +**What it shows:** +- **Total I/O throughput (MiB/s)** and **I/O time (ms/s)** +- **Buffer hit ratio (%)** — a gauge of blocks found in shared buffers vs fetched from the OS/disk +- **I/O operations (ops/s)** + +**Healthy state:** +- High buffer hit ratio for hot data + +**Warning signs:** +- Falling hit ratio / rising reads = working set no longer fits in shared buffers + +### I/O by backend type + +**What it shows:** +- **Reads by backend type (ops/s)** and **Writes by backend type (ops/s)**, grouped by `backend_type` + +**Interpretation:** +- `client backend` dominating reads = query workload is I/O bound +- `autovacuum worker` dominating reads = vacuum is reading heavily; check bloat and the + [Autovacuum dashboard](/docs/monitoring/dashboards/autovacuum) +- `checkpointer` / `background writer` dominating writes = normal flushing behavior + +### Writebacks, fsyncs, and file extends + +**What it shows:** +- **Writebacks (ops/s)**, **Fsyncs (ops/s)**, and **File extends (ops/s)** + +**Interpretation:** +- High file-extend rates track heavy inserts / table growth +- High fsync activity can point to slow storage + +### Buffer cache efficiency + +**What it shows:** +- **Buffer evictions and reuses (ops/s)** — evictions (a buffer had to be evicted to make room) + and reuses (a buffer was reused directly, e.g. by ring buffers during bulk operations) +- **Buffer hits by backend type (ops/s)** + +**Interpretation:** +- High evictions under steady load is a classic signal that `shared_buffers` is undersized + +## Variables + +| Variable | Purpose | +|----------|---------| +| `cluster_name` | Cluster filter | +| `node_name` | Node filter | + +I/O statistics are instance-level; this dashboard has no database or table filter (only +`cluster_name` and `node_name`). + +## Underlying query + +The dashboard is built on the `pg_stat_io` view, aggregated by backend type: + +```sql +select + coalesce(backend_type, 'total') as backend_type, + sum(reads) as reads, + sum(writes) as writes, + sum(extends) as extends, + sum(hits) as hits, + sum(evictions) as evictions, + sum(reuses) as reuses, + sum(fsyncs) as fsyncs +from pg_stat_io +group by rollup (backend_type); +``` + +`pg_stat_io` counters are cumulative until `pg_stat_reset_shared('io')`; the dashboard shows +rates derived from them. + +## Related dashboards + +- **Node overview** — [01. Node overview](/docs/monitoring/dashboards/node-overview) +- **Autovacuum and xmin horizon** — [07. Autovacuum](/docs/monitoring/dashboards/autovacuum) +- **Backups and WAL** — [05. Backups](/docs/monitoring/dashboards/backups) + +## Metrics reference + +The signals behind this dashboard are documented as the `pg_stat_io` metric group in the +[monitoring reference](/docs/reference-guides/postgres-ai-monitoring-reference#io-statistics-pg_stat_io-postgresql-16). + +## Troubleshooting + +### No data on this dashboard + +1. Confirm the monitored database runs PostgreSQL 16 or newer: + ```sql + select current_setting('server_version_num')::int >= 160000 as has_pg_stat_io; + ``` +2. Verify the view is readable by the monitoring role: + ```sql + select count(*) from pg_stat_io; + ``` + +### Counters look reset + +`pg_stat_io` is reset by `pg_stat_reset_shared('io')` and on server restart. The `stats_reset` +timestamp is exposed so you can tell when the window started. diff --git a/docs/monitoring/dashboards/index.md b/docs/monitoring/dashboards/index.md index 4022fd9c..8f07767a 100644 --- a/docs/monitoring/dashboards/index.md +++ b/docs/monitoring/dashboards/index.md @@ -36,7 +36,7 @@ PostgresAI monitoring includes 14 pre-built Grafana dashboards designed for expe | # | Dashboard | Purpose | |---|-----------|---------| | 05 | [Backups](/docs/monitoring/dashboards/backups) | Backup status and WAL archiving | -| 07 | [Autovacuum](/docs/monitoring/dashboards/autovacuum) | Vacuum progress and bloat | +| 07 | [Autovacuum & xmin horizon](/docs/monitoring/dashboards/autovacuum) | Autovacuum, dead tuples, bloat, and xmin-horizon root cause analysis | | 08 | [Table stats](/docs/monitoring/dashboards/table-stats) | Aggregated table metrics | | 09 | [Single table](/docs/monitoring/dashboards/single-table) | Deep-dive into specific table | | 10 | [Index health](/docs/monitoring/dashboards/index-health) | Index usage and bloat | @@ -49,6 +49,12 @@ PostgresAI monitoring includes 14 pre-built Grafana dashboards designed for expe |---|-----------|---------| | 06 | [Replication](/docs/monitoring/dashboards/replication) | Replication lag and slot status | +### I/O + +| # | Dashboard | Purpose | +|---|-----------|---------| +| 14 | [I/O statistics](/docs/monitoring/dashboards/io-statistics) | I/O by backend type (`pg_stat_io`, PostgreSQL 16+) | + ### Stack health | # | Dashboard | Purpose | @@ -57,7 +63,7 @@ PostgresAI monitoring includes 14 pre-built Grafana dashboards designed for expe ## Common variables -All dashboards share these filter variables: +Most dashboards share these filter variables: | Variable | Purpose | Example | |----------|---------|---------| @@ -65,6 +71,13 @@ All dashboards share these filter variables: | `node_name` | Node within cluster | `primary`, `replica-1` | | `db_name` | Database filter | `myapp`, `All` | +**Exceptions:** +- **06. Replication** and **Self-monitoring** have no template variables at all (06 is a + placeholder; self-monitoring reports on the single monitoring instance). +- **14. I/O statistics** has only `cluster_name` and `node_name` (no database filter — `pg_stat_io` + is instance-level). +- **11. Single index** names its database variable `datname` (label "DB name") rather than `db_name`. + ## Recommended workflow ### Incident response @@ -90,23 +103,39 @@ All dashboards share these filter variables: | Query review | 02. Query analysis | New slow queries, regression | | Index health | 10. Index health | Unused indexes, bloat | | Table health | 08. Table stats | Bloat, sequential scans | -| Vacuum status | 07. Autovacuum | Dead tuple accumulation | +| Vacuum status | 07. Autovacuum & xmin horizon | Dead tuple accumulation, xmin-horizon blockers | +| I/O attribution | 14. I/O statistics | Reads/writes by backend type (PG16+) | ## Legend options -Most query-related dashboards support multiple legend formats: +[02. Query analysis](/docs/monitoring/dashboards/query-analysis) has a **Query texts** variable +(`legend_label`) that switches how query texts are rendered in legends: -| Format | Shows | Use case | -|--------|-------|----------| -| `queryid` | Numeric ID only | Compact view | -| `displayname` | Truncated query | Default | -| `displayname_long` | Full query with context | Debugging | +| Option | Value | Shows | +|--------|-------|-------| +| Smart truncation (default) | `displayname_long` | Query text with smart truncation | +| Raw texts | `displayname_raw_long` | Full raw query text | -Select the format using the **Query texts** variable at the top of dashboards. +Select the format using the **Query texts** variable at the top of the dashboard. + +## Top-N filtering + +Many dashboards limit each panel to the top-N series (for example, the `top_n` variable on +[02. Query analysis](/docs/monitoring/dashboards/query-analysis) offers 5, 10, 15, 20, 50, 100, 500). +These panels use plain PromQL `topk($top_n, ...)`, which keeps only the highest-ranked series and +**drops the long tail** — it does not sum the remainder into a separate bucket. The per-relation +dashboards ([08. Table stats](/docs/monitoring/dashboards/table-stats), +[10. Index health](/docs/monitoring/dashboards/index-health)) use the same `topk($top_n, ...)` approach. + +If the objects you care about are not visible, raise `top_n` or drill into the corresponding +single-object dashboard to see the detail. ## Time range tips -- **Incident investigation**: Start with 15m-1h to see recent patterns +Dashboards default to a **`now-1h`** time range in 0.15, tuned for readable, recent patterns +out of the box. + +- **Incident investigation**: The default `now-1h` shows recent patterns; widen as needed - **Trend analysis**: Use 24h-7d for capacity planning - **Comparison**: Use "Compare to" feature for week-over-week analysis diff --git a/docs/monitoring/dashboards/self-monitoring.md b/docs/monitoring/dashboards/self-monitoring.md index aefb27ce..839f6f66 100644 --- a/docs/monitoring/dashboards/self-monitoring.md +++ b/docs/monitoring/dashboards/self-monitoring.md @@ -1,7 +1,7 @@ --- title: "Self-monitoring" sidebar_label: "Self-monitoring" -sidebar_position: 15 +sidebar_position: 16 --- # Self-monitoring dashboard @@ -31,85 +31,79 @@ Ensure the monitoring infrastructure is functioning correctly: ## Key panels -### Scrape success rate +The dashboard is organized into six rows: **Overview**, **Host stats**, **Disk I/O metrics**, +**Container resource usage**, **Victoria Metrics metrics**, and **Sink Postgres database**. -**What it shows:** -- Percentage of successful metric scrapes -- Per-target breakdown - -**Healthy state:** -- 100% success rate -- Consistent scrape intervals +### Overview -**Warning signs:** -- Scrape failures — check target availability -- Timeouts — target may be overloaded +**What it shows (single-stat tiles):** +- **Active monitoring services** and **Running containers** +- **Application memory usage** and **System CPU usage** +- **Victoria Metrics storage size** and **Victoria Metrics time series** -### Metrics ingestion rate +### Host stats and Disk I/O metrics **What it shows:** -- Samples ingested per second -- Trend over time +- System CPU / memory / network / disk usage breakdowns +- Disk I/O operations (IOPS), throughput, utilization, and average latency -**Use for:** -- Capacity planning -- Detecting metric explosion - -### Storage usage +### Container resource usage **What it shows:** -- VictoriaMetrics disk usage -- Projected capacity based on retention - -**Warning threshold:** -- Alert when > 80% capacity +- Per-container CPU, memory, network I/O, and disk I/O -### Active time series +### Victoria Metrics metrics **What it shows:** -- Number of unique metric series -- Growth trend +- **Victoria Metrics ingestion rate** — samples ingested per second +- **Scrape duration by target** — how long each scrape takes (rising durations = a target is slow) +- **Victoria Metrics storage size** — disk usage; project capacity against your retention +- **Victoria Metrics rows count** — number of stored rows; watch for cardinality explosion -**Monitoring series growth:** -- Sudden spikes may indicate cardinality explosion -- Gradual growth expected as you add targets - -### Query performance +### Sink Postgres database **What it shows:** -- Grafana query latency -- Slow queries +- Sink Postgres connections, transactions, database size, and block I/O ## Variables -| Variable | Purpose | -|----------|---------| -| `cluster_name` | Filter by monitored cluster | +This dashboard has no template variables — it reports on the monitoring stack itself (Grafana, +VictoriaMetrics, the sink Postgres, cAdvisor, and node-exporter), which is a single instance, so +there is nothing to filter by cluster or node. ## Health check commands +:::note VictoriaMetrics basic auth +The VictoriaMetrics API on host port `59090` requires basic auth in 0.15. Every `curl` below passes +`-u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD"`; export those from your stack's `.env` first (or substitute +the values). Without credentials these endpoints return `401 Unauthorized`. +::: + ### Check VictoriaMetrics status ```bash -curl http://localhost:8428/api/v1/status/tsdb +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + http://localhost:59090/api/v1/status/tsdb ``` ### Check pgwatch status ```bash -docker compose logs pgwatch --tail=50 +docker compose logs pgwatch-postgres pgwatch-prometheus --tail=50 ``` ### Check Prometheus/VM targets ```bash -curl http://localhost:8428/api/v1/targets +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + http://localhost:59090/api/v1/targets ``` ### Verify metrics collection ```bash -curl 'http://localhost:8428/api/v1/query?query=up' +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=up' ``` ## Common issues @@ -118,12 +112,14 @@ curl 'http://localhost:8428/api/v1/query?query=up' 1. Check scrape targets are up: ```bash - curl http://localhost:8428/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}' + curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + http://localhost:59090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}' ``` 2. Verify metric exists: ```bash - curl 'http://localhost:8428/api/v1/label/__name__/values' | jq '.data[]' | grep pg_ + curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/label/__name__/values' | jq '.data[]' | grep pg_ ``` 3. Check time range alignment @@ -132,20 +128,22 @@ curl 'http://localhost:8428/api/v1/query?query=up' 1. Check for cardinality explosion: ```bash - curl 'http://localhost:8428/api/v1/status/tsdb' | jq '.data.totalSeries' + curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/status/tsdb' | jq '.data.totalSeries' ``` 2. Review high-cardinality metrics: ```bash - curl 'http://localhost:8428/api/v1/status/tsdb' | jq '.data.seriesCountByMetricName | to_entries | sort_by(-.value) | .[0:10]' + curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/status/tsdb' | jq '.data.seriesCountByMetricName | to_entries | sort_by(-.value) | .[0:10]' ``` -3. Adjust retention if needed: +3. Adjust retention if needed (default is `336h` ≡ 14 days): ```yaml # docker-compose.yml - victoriametrics: - command: - - "-retentionPeriod=30d" # Reduce from 90d + sink-prometheus: + environment: + - VM_RETENTION_PERIOD=30d # Adjust retention if needed ``` ### Scrape timeouts @@ -154,7 +152,7 @@ curl 'http://localhost:8428/api/v1/query?query=up' ```yaml # prometheus.yml scrape_configs: - - job_name: 'pgwatch' + - job_name: 'pgwatch-prometheus' scrape_timeout: 30s ``` @@ -181,7 +179,8 @@ Daily storage ≈ (series_count × samples_per_day × bytes_per_sample) / compre Typical values: - Bytes per sample: ~2-4 (compressed) - Compression ratio: 10-15x -- Samples per day at 60s interval: 1,440 +- Samples per day at the default 30s interval: ~2,880 (most metric groups collect every 30s; + `pg_stat_activity` and `wait_events` every 15s) ### Scaling recommendations diff --git a/docs/monitoring/getting-started/index.md b/docs/monitoring/getting-started/index.md index 5c52aa58..e03f7dfb 100644 --- a/docs/monitoring/getting-started/index.md +++ b/docs/monitoring/getting-started/index.md @@ -14,12 +14,15 @@ Get PostgresAI monitoring running in minutes with PostgresAI Cloud. ## Step 1: Choose your plan -Go to [console.postgres.ai](https://console.postgres.ai) and navigate to **Checkup → Monitoring instances → Choose plan**. +Go to [console.postgres.ai](https://console.postgres.ai), open **Monitoring** in the left sidebar to reach the **Monitoring instances** page, then click **Start setup** (under **Hosted by PostgresAI**) and choose a plan. ![Plans page](/img/monitoring/cloud-setup/01-plans-page.png) +:::note +The console screenshot above still shows the retired **Starter** plan. Hobby and Express do not include the monitoring stack — this guide covers **Scale** and **Enterprise**. +::: + Select a plan based on your needs: -- **Starter** ($128/mo) — Full monitoring stack for small production databases - **Scale** ($512/mo) — 6-month retention, trend analysis, 1 business day SLA - **Enterprise** — Dedicated support, Kubernetes & Terraform, custom workflows @@ -63,7 +66,7 @@ For other PostgreSQL databases (RDS, CloudSQL, self-hosted): 4. Deploy the monitoring stack :::tip Database preparation -For automatic setup, provide superuser credentials (used once, never stored). For manual setup, follow the [database preparation guide](/docs/monitoring/getting-started/requirements#database-preparation). +For automatic setup, provide superuser credentials (used once, never stored). For manual setup, follow the [database preparation guide](/docs/monitoring/getting-started/requirements#permissions). ::: ## Step 4: Access your dashboards @@ -72,7 +75,7 @@ Once deployed, you'll receive: - Grafana URL with your dashboards - Login credentials -Start with **01. Node overview** for a high-level health check. +Start with **01. Single node performance overview (high-level)** for a high-level health check. ## Verify database permissions @@ -96,15 +99,15 @@ Only database metadata is collected — no actual data or query parameters: To review exactly what metrics are collected, examine the metric definitions: - **Prometheus sink metrics**: - [metrics.yml (pgwatch-prometheus)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.14.0/config/pgwatch-prometheus/metrics.yml) + [metrics.yml (pgwatch-prometheus)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-prometheus/metrics.yml) - **PostgreSQL sink metrics** (including normalized queries): - [metrics.yml (pgwatch-postgres)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.14.0/config/pgwatch-postgres/metrics.yml) + [metrics.yml (pgwatch-postgres)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-postgres/metrics.yml) See [data privacy details](/docs/monitoring/#data-privacy-metadata-only). ## First dashboard walkthrough -Key panels to check in **01. Node overview**: +Key panels to check in **01. Single node performance overview (high-level)**: 1. **Active session history (ASH)** — Wait events over time (similar to RDS Performance Insights) 2. **Sessions** — Active, idle, and idle in transaction connections 3. **TPS** — Transactions per second @@ -114,16 +117,16 @@ Key panels to check in **01. Node overview**: ``` Is there an ongoing incident? -├─ Yes — Start with "01. Node Overview" for quick triage -│ └─ High wait events? — "04. Wait Events" for deep-dive -│ └─ Slow queries? — "02. Query Analysis" then "03. Single Query" -│ └─ Lock contention? — "13. Lock Contention" +├─ Yes — Start with "01. Single node performance overview (high-level)" for quick triage +│ └─ High wait events? — "04. Wait event analysis (Active Session History)" for deep-dive +│ └─ Slow queries? — "02. Query performance analysis (top-N)" then "03. Single queryid analysis" +│ └─ Lock contention? — "13. Lock contention" │ ├─ No, routine monitoring -│ ├─ Query performance review — "02. Query Analysis" -│ ├─ Index health check — "10. Index Health" -│ ├─ Table bloat check — "07. Autovacuum" or "08. Table Stats" -│ └─ Replication lag — "06. Replication" +│ ├─ Query performance review — "02. Query performance analysis (top-N)" +│ ├─ Index health check — "10. Aggregated index analysis" +│ ├─ Table bloat check — "07. Autovacuum and xmin horizon" or "08. Aggregated table analysis" +│ └─ Replication lag — "06. Replication and HA" ``` ## Self-hosted alternative diff --git a/docs/monitoring/getting-started/installation-cli.md b/docs/monitoring/getting-started/installation-cli.md index a6c087f2..f371fb23 100644 --- a/docs/monitoring/getting-started/installation-cli.md +++ b/docs/monitoring/getting-started/installation-cli.md @@ -12,7 +12,7 @@ The fastest way to get PostgresAI monitoring running locally. - Node.js 18+ or Bun 1.0+ - Docker 20.10+ -- PostgreSQL 14+ with `pg_stat_statements` +- PostgreSQL 13+ (14+ recommended) with `pg_stat_statements` :::tip Bun support All commands work with both `npx` and `bunx`. The CLI is written in TypeScript and runs natively on Bun. @@ -29,7 +29,7 @@ PGPASSWORD=your_password npx postgresai@latest prepare-db "postgresql://postgres ### What prepare-db does 1. **Enables pg_stat_statements** extension -2. **Creates monitoring user** (`pgwatch`) with minimal read-only privileges +2. **Creates monitoring user** (`postgres_ai_mon` by default) with minimal read-only privileges 3. **Validates configuration** for optimal monitoring ### prepare-db options @@ -38,10 +38,11 @@ PGPASSWORD=your_password npx postgresai@latest prepare-db "postgresql://postgres npx postgresai@latest prepare-db [connection] [options] Options: - --mon-user Monitoring username (default: pgwatch) - --mon-password Monitoring password (auto-generated if not set) - --skip-extension Skip pg_stat_statements setup - --dry-run Show what would be done without executing + --monitoring-user Monitoring username (default: postgres_ai_mon) + --password Monitoring password (auto-generated if not set) + --skip-optional-permissions Skip optional permissions for managed providers + --print-sql Print SQL instead of executing it + --verify Verify monitoring permissions after setup ``` ### Example output @@ -49,7 +50,7 @@ Options: ``` ✓ Connected to PostgreSQL 16.2 ✓ pg_stat_statements extension enabled -✓ Created monitoring user 'pgwatch' +✓ Created monitoring user 'postgres_ai_mon' ✓ Granted required permissions Monitoring connection string: @@ -64,13 +65,14 @@ postgresql://postgres_ai_mon:auto_generated_pass@localhost:5432/mydb npx postgresai@latest mon local-install --demo ``` -Starts a complete stack with a sample PostgreSQL database pre-loaded with pgbench data. +Starts a complete stack with a sample PostgreSQL database seeded with a small demo dataset (a +`sample_data` table plus the monitoring schema objects). ### Production mode ```bash npx postgresai@latest mon local-install \ - --target-db postgresql://postgres_ai_mon:pass@host:5432/mydb + --db-url postgresql://postgres_ai_mon:pass@host:5432/mydb ``` ### local-install options @@ -79,15 +81,13 @@ npx postgresai@latest mon local-install \ npx postgresai@latest mon local-install [options] Options: - --target-db PostgreSQL connection string - --demo Start with demo database - --grafana-port Grafana port (default: 3000) - --vm-port VictoriaMetrics port (default: 8428) - --cluster-name Cluster identifier in dashboards - --node-name Node identifier (default: node-01) - --detach Run in background - --stop Stop running stack - --status Show stack status + --demo demo mode with sample database (default: false) + --api-key Postgres AI API key for automated report uploads + --db-url PostgreSQL connection URL to monitor + --tag Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33) + --project Docker Compose project name (default: postgres_ai) + -y, --yes accept all defaults and skip interactive prompts (default: false) + -h, --help display help for command ``` ## Step 3: Access Grafana @@ -100,9 +100,12 @@ Credentials: To retrieve the password later: ```bash -grep grafana_password ~/.postgresai/.pgwatch-config +grep grafana_password ~/.config/postgresai/monitoring/.pgwatch-config ``` +(The monitoring project lives in `~/.config/postgresai/monitoring` by default, or in +`$PGAI_PROJECT_DIR` if set.) + Navigate to **Dashboards — Browse — postgres_ai** to see your monitoring dashboards. ## Managing the stack @@ -110,38 +113,25 @@ Navigate to **Dashboards — Browse — postgres_ai** to see your monitoring das ### Check status ```bash -npx postgresai@latest mon local-install --status +npx postgresai@latest mon status ``` -Output: -``` -PostgresAI Monitoring Stack Status -─────────────────────────────────── -Grafana: Running (`http://localhost:3000`) -VictoriaMetrics: Running (`http://localhost:8428`) -pgwatch: Running -Flask Backend: Running - -Monitoring: - Cluster: local - Node: node-01 - Target: postgresql://...@localhost:5432/mydb -``` +Output is the Docker Compose service table for the monitoring stack, including `grafana-with-datasources`, `sink-postgres`, `sink-prometheus`, `pgwatch-postgres`, `pgwatch-prometheus`, and `flask-pgss-api`. ### Stop stack ```bash -npx postgresai@latest mon local-install --stop +npx postgresai@latest mon stop ``` ### View logs ```bash # All containers -docker compose -f ~/.postgresai/docker-compose.yml logs -f +docker compose -f ~/.config/postgresai/monitoring/docker-compose.yml logs -f # Specific service -docker compose -f ~/.postgresai/docker-compose.yml logs -f pgwatch +docker compose -f ~/.config/postgresai/monitoring/docker-compose.yml logs -f pgwatch-postgres pgwatch-prometheus ``` ## Troubleshooting @@ -166,15 +156,17 @@ Restart PostgreSQL after this change. 2. Verify Docker can reach the host: ```bash # On macOS/Windows, use host.docker.internal - --target-db postgresql://user:pass@host.docker.internal:5432/mydb + --db-url postgresql://user:pass@host.docker.internal:5432/mydb ``` ### "Permission denied" -The monitoring user needs these minimum privileges: +The monitoring user needs the built-in `pg_monitor` role (this is what `prepare-db` grants and +what the install/verify step checks for; `pg_read_all_stats` alone is a strict subset and is not +sufficient): ```sql -grant pg_read_all_stats to postgres_ai_mon; +grant pg_monitor to postgres_ai_mon; ``` For RDS/CloudSQL, ensure you're using the master user for `prepare-db`. diff --git a/docs/monitoring/getting-started/installation-cloud.md b/docs/monitoring/getting-started/installation-cloud.md index c26e01ca..97a1cf72 100644 --- a/docs/monitoring/getting-started/installation-cloud.md +++ b/docs/monitoring/getting-started/installation-cloud.md @@ -23,6 +23,7 @@ Managed PostgreSQL services (RDS, CloudSQL, Supabase) require specific configura - RDS PostgreSQL 14+ - Parameter group with `pg_stat_statements` enabled - Security group allowing monitoring access +- Node.js 18+ (or Bun 1.0+) on the host running the `postgresai` CLI — older versions fail fast ### Step 1: Enable pg_stat_statements @@ -30,8 +31,8 @@ Create or modify a parameter group: ``` shared_preload_libraries = pg_stat_statements -pg_stat_statements.track = all -pg_stat_statements.max = 10000 +pg_stat_statements.track = top +pg_stat_statements.max = 5000 ``` Apply to your RDS instance and reboot if required. @@ -44,18 +45,26 @@ Connect as the master user: -- Create monitoring user create user postgres_ai_mon with password ''; --- Grant required permissions -grant pg_read_all_stats to postgres_ai_mon; +-- Grant required permissions (the product requires the built-in pg_monitor role, +-- not pg_read_all_stats) +grant pg_monitor to postgres_ai_mon; -- Enable extension (if not already) create extension if not exists pg_stat_statements; --- For each database to monitor +-- For each database to monitor, connect and grant connect (pg_monitor already +-- covers reading statistics — monitoring needs metadata only, NOT table data, +-- so do NOT `grant select on all tables`). \c your_database -grant usage on schema public to postgres_ai_mon; -grant select on all tables in schema public to postgres_ai_mon; +grant connect on database your_database to postgres_ai_mon; ``` +:::tip Use prepare-db for the exact grants +Instead of granting by hand, run `npx postgresai@latest prepare-db --print-sql` (or run +`prepare-db` against the master/admin user) to apply the exact, minimal read-only grants the +product uses. See [Permissions](/docs/monitoring/getting-started/requirements#permissions). +::: + ### Step 3: Configure security group Allow inbound traffic from your monitoring stack: @@ -68,10 +77,15 @@ Allow inbound traffic from your monitoring stack: ```bash npx postgresai@latest mon local-install \ - --target-db "postgresql://postgres_ai_mon:password@your-instance.region.rds.amazonaws.com:5432/your_db" \ - --cluster-name "rds-production" + --db-url "postgresql://postgres_ai_mon:password@your-instance.region.rds.amazonaws.com:5432/your_db" ``` +:::note Labeling clusters and nodes +The 0.15 `local-install` command does not accept `--cluster-name`/`--node-name`. To +tag metrics by cluster or node, set `custom_tags.cluster` and `custom_tags.node_name` +in `instances.yml` (see [Docker Compose → Adding multiple databases](/docs/monitoring/getting-started/installation-docker#adding-multiple-databases)). +::: + ### RDS-specific considerations **Enhanced Monitoring:** @@ -89,7 +103,7 @@ Monitor the primary endpoint. For read replicas, add separate monitoring targets - Cloud SQL PostgreSQL 14+ - Private IP or authorized network -- `cloudsql.pg_stat_statements` flag enabled +- `cloudsql.enable_pg_stat_statements` flag enabled ### Step 1: Enable extensions @@ -110,12 +124,14 @@ Using Cloud SQL admin user: -- Create monitoring user create user postgres_ai_mon with password ''; --- Grant permissions -grant pg_read_all_stats to postgres_ai_mon; +-- Grant permissions (the product requires the built-in pg_monitor role, +-- not pg_read_all_stats) +grant pg_monitor to postgres_ai_mon; --- On each database -grant usage on schema public to postgres_ai_mon; -grant select on all tables in schema public to postgres_ai_mon; +-- On each database to monitor (pg_monitor already covers reading statistics — +-- monitoring needs metadata only, NOT table data, so do NOT `grant select on +-- all tables`): +grant connect on database your_database to postgres_ai_mon; ``` ### Step 3: Configure network access @@ -134,8 +150,7 @@ Add your monitoring stack's IP to authorized networks: For private IP: ```bash npx postgresai@latest mon local-install \ - --target-db "postgresql://postgres_ai_mon:password@10.x.x.x:5432/your_db" \ - --cluster-name "cloudsql-production" + --db-url "postgresql://postgres_ai_mon:password@10.x.x.x:5432/your_db" ``` For Cloud SQL Auth Proxy: @@ -145,10 +160,15 @@ cloud_sql_proxy -instances=PROJECT:REGION:INSTANCE=tcp:5432 # Connect npx postgresai@latest mon local-install \ - --target-db "postgresql://postgres_ai_mon:password@localhost:5432/your_db" \ - --cluster-name "cloudsql-production" + --db-url "postgresql://postgres_ai_mon:password@localhost:5432/your_db" ``` +:::note Labeling clusters and nodes +The 0.15 `local-install` command does not accept `--cluster-name`/`--node-name`. To +tag metrics by cluster or node, set `custom_tags.cluster` and `custom_tags.node_name` +in `instances.yml` (see [Docker Compose → Adding multiple databases](/docs/monitoring/getting-started/installation-docker#adding-multiple-databases)). +::: + ### Cloud SQL-specific considerations **Insights:** @@ -188,22 +208,28 @@ Connect to your Supabase database and run: -- Create monitoring user create user postgres_ai_mon with password ''; --- Grant permissions -grant pg_read_all_stats to postgres_ai_mon; +-- Grant permissions (the product requires the built-in pg_monitor role, +-- not pg_read_all_stats) +grant pg_monitor to postgres_ai_mon; --- Grant access to your schemas -grant usage on schema public to postgres_ai_mon; -grant select on all tables in schema public to postgres_ai_mon; +-- pg_monitor already covers reading statistics — monitoring needs metadata +-- only, NOT table data, so do NOT `grant select on all tables`: +grant connect on database postgres to postgres_ai_mon; ``` ### Step 4: Start monitoring ```bash npx postgresai@latest mon local-install \ - --target-db "postgresql://postgres_ai_mon:password@db.xxxx.supabase.co:5432/postgres?sslmode=require" \ - --cluster-name "supabase-production" + --db-url "postgresql://postgres_ai_mon:password@db.xxxx.supabase.co:5432/postgres?sslmode=require" ``` +:::note Labeling clusters and nodes +The 0.15 `local-install` command does not accept `--cluster-name`/`--node-name`. To +tag metrics by cluster or node, set `custom_tags.cluster` and `custom_tags.node_name` +in `instances.yml` (see [Docker Compose → Adding multiple databases](/docs/monitoring/getting-started/installation-docker#adding-multiple-databases)). +::: + :::note Connection pooling Use the "Direct connection" string, not the pooled connection (port 6543). Monitoring requires direct PostgreSQL protocol access. ::: @@ -216,10 +242,10 @@ Most cloud providers require SSL: ```bash # Require SSL ---target-db "postgresql://...?sslmode=require" +--db-url "postgresql://...?sslmode=require" # Verify certificate (recommended for production) ---target-db "postgresql://...?sslmode=verify-full&sslrootcert=/path/to/ca.crt" +--db-url "postgresql://...?sslmode=verify-full&sslrootcert=/path/to/ca.crt" ``` ### Permission limitations @@ -248,9 +274,9 @@ For optimal metric collection: ### "permission denied for function" -Grant required permissions: +Grant the required role (the product requires `pg_monitor`, not `pg_read_all_stats`): ```sql -grant pg_read_all_stats to postgres_ai_mon; +grant pg_monitor to postgres_ai_mon; ``` ### "pg_stat_statements must be loaded" diff --git a/docs/monitoring/getting-started/installation-docker.md b/docs/monitoring/getting-started/installation-docker.md index 25b0bbf5..d0c7c131 100644 --- a/docs/monitoring/getting-started/installation-docker.md +++ b/docs/monitoring/getting-started/installation-docker.md @@ -12,7 +12,7 @@ For custom deployments and development environments. - Docker 20.10+ - Docker Compose v2 -- PostgreSQL 14+ target database +- PostgreSQL 13+ (14+ recommended) target database ## Quick start @@ -21,14 +21,34 @@ For custom deployments and development environments. git clone https://gitlab.com/postgres-ai/postgresai.git cd postgresai -# Configure target database +# Configure stack secrets cp .env.example .env -# Edit .env with your database connection +# Edit .env and set (at minimum): +# PGAI_TAG=0.15.0 # .env.example ships 0.14.0 — bump it to this release +# VM_AUTH_PASSWORD=... # required (non-empty) — Grafana datasource won't provision without it +# REPLICATOR_PASSWORD=... # required if you keep the demo target-db/target-standby services + +# Create instances.yml (the list of databases to monitor). +# This file MUST exist as a FILE before `docker compose up`: docker-compose.yml +# bind-mounts ./instances.yml, so if it is missing Docker creates a *directory* +# of that name and sources-generator produces zero targets. +cp instances.demo.yml instances.yml +# Edit instances.yml: set is_enabled: true and a real conn_str for each target +# (the connection string lives here, NOT in .env). + +# Render the pgwatch source files from instances.yml +docker compose run --rm sources-generator # Start the stack docker compose up -d ``` +:::tip Prefer the CLI +`postgresai mon local-install` automates the steps above (copies the demo files, prompts for a +target, generates sources, and starts the stack). Use the manual flow here only for custom +deployments. +::: + ## Configuration ### Environment variables @@ -37,67 +57,130 @@ Create a `.env` file or set these environment variables: ```bash # Required -TARGET_DB_HOST=your-postgres-host -TARGET_DB_PORT=5432 -TARGET_DB_NAME=your_database -TARGET_DB_USER=pgwatch -TARGET_DB_PASSWORD=your_password +PGAI_TAG=0.15.0 +REPLICATOR_PASSWORD= -# Optional - Cluster identification -CLUSTER_NAME=production -NODE_NAME=primary +# Required in 0.15: VictoriaMetrics basic auth. VM_AUTH_PASSWORD must be non-empty. +# Grafana datasource provisioning depends on these — without them, dashboards show no data. +# See: Authentication and security in the Prometheus/VictoriaMetrics config guide. +VM_AUTH_USERNAME=vmauth +VM_AUTH_PASSWORD= + +# Target databases are defined in instances.yml +# Use postgres_ai_mon by default after running prepare-db. # Optional - Grafana admin password (REQUIRED for production!) # Default is 'demo' - always change this in production GF_SECURITY_ADMIN_PASSWORD=your_secure_password # Optional - Retention -VM_RETENTION_PERIOD=90d +VM_RETENTION_PERIOD=336h ``` -### docker-compose.yml Overview +### docker-compose.yml excerpt + +This excerpt omits the `config-init` and `sources-generator` helper services that +the pgwatch collectors depend on (see `depends_on` below); both are defined in the +full `docker-compose.yml` in the repository. `sources-generator` renders the pgwatch +source files from `instances.yml`. ```yaml services: grafana: - image: grafana/grafana:latest + image: grafana/grafana:12.3.2 + container_name: grafana-with-datasources ports: - "${GRAFANA_BIND_HOST:-}3000:3000" volumes: - - grafana-data:/var/lib/grafana - - ./config/grafana/provisioning:/etc/grafana/provisioning + - grafana_data:/var/lib/grafana + - postgres_ai_configs:/postgres_ai_configs:ro environment: - - GF_SECURITY_ADMIN_USER=monitor - - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD:-demo} - - victoriametrics: - image: victoriametrics/victoria-metrics:latest + GF_SECURITY_ADMIN_USER: monitor + GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:-demo} + GF_PATHS_PROVISIONING: /postgres_ai_configs/grafana/provisioning + GF_PATHS_CONFIG: /postgres_ai_configs/grafana/provisioning/grafana.ini + VM_AUTH_USERNAME: ${VM_AUTH_USERNAME:?VM_AUTH_USERNAME is required for Grafana datasource provisioning} + VM_AUTH_PASSWORD: ${VM_AUTH_PASSWORD:?VM_AUTH_PASSWORD is required for Grafana datasource provisioning} + restart: unless-stopped + + sink-postgres: + image: postgres:17 + container_name: sink-postgres + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_HOST_AUTH_METHOD: trust + + sink-prometheus: + image: victoriametrics/victoria-metrics:v1.140.0 + container_name: sink-prometheus ports: - - "${VICTORIAMETRICS_PORT:-8428}:8428" + - "${BIND_HOST:-}59090:9090" volumes: - - vm-data:/victoria-metrics-data + - victoria_metrics_data:/victoria-metrics-data + environment: + - VM_AUTH_USERNAME=${VM_AUTH_USERNAME:-} + - VM_AUTH_PASSWORD=${VM_AUTH_PASSWORD:-} + - VM_RETENTION_PERIOD=${VM_RETENTION_PERIOD:-336h} + + pgwatch-postgres: + image: postgresai/pgwatch:${PGAI_TAG} + container_name: pgwatch-postgres command: - - "-retentionPeriod=${VM_RETENTION_PERIOD:-90d}" + - "--sources=/postgres_ai_configs/pgwatch/sources.yml" + - "--metrics=/postgres_ai_configs/pgwatch/metrics.yml" + - "--sink=postgresql://pgwatch@sink-postgres:5432/measurements?sslmode=disable" + - "--web-addr=:8080" + - "--log-level=error" + depends_on: + - sources-generator + - sink-postgres - pgwatch: - image: cybertec/pgwatch:latest - environment: - - PW_SOURCES=postgresql://${TARGET_DB_USER}:${TARGET_DB_PASSWORD}@${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME} + pgwatch-prometheus: + image: postgresai/pgwatch:${PGAI_TAG} + container_name: pgwatch-prometheus + command: + - "--sources=/postgres_ai_configs/pgwatch-prometheus/sources.yml" + - "--metrics=/postgres_ai_configs/pgwatch-prometheus/metrics.yml" + - "--sink=prometheus://0.0.0.0:9091/pgwatch" + - "--web-addr=:8089" + - "--log-level=error" depends_on: - - victoriametrics + - sources-generator + - sink-prometheus + # Flask backend is internal-only (no published host port); other services + # reach it over the Docker network. monitoring_flask_backend: - build: ./monitoring_flask_backend - ports: - - "8000:8000" + image: postgresai/monitoring-flask-backend:${PGAI_TAG} + container_name: flask-pgss-api environment: - - POSTGRES_URI=postgresql://${TARGET_DB_USER}:${TARGET_DB_PASSWORD}@${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME} + - PROMETHEUS_URL=http://sink-prometheus:9090 + - POSTGRES_SINK_URL=postgresql://pgwatch@sink-postgres:5432/measurements + - QUERYID_RETENTION_HOURS=${QUERYID_RETENTION_HOURS:-720} volumes: - grafana-data: - vm-data: + grafana_data: + victoria_metrics_data: ``` +## Image tags and supply chain + +All stack images are **version-pinned** in 0.15 — none use `:latest`. PostgresAI images +(`pgwatch`, `monitoring-flask-backend`, `reporter`, configs) are pinned to `PGAI_TAG`, and the +third-party images are pinned to specific releases (for example `grafana/grafana:12.3.2`, +`victoriametrics/victoria-metrics:v1.140.0`, `postgres:17`). Pinning makes deployments +reproducible and auditable and avoids silent, unreviewed upgrades — set `PGAI_TAG=0.15.0` to +deploy this release. + +## Reliability and restart behavior + +Critical services ship with `restart: unless-stopped` so the monitoring stack comes back +automatically after a Docker daemon restart or host reboot — no manual systemd unit is needed. +The excerpt above shows the `restart:` key on the Grafana service; the full +`docker-compose.yml` sets it on the other long-running services as well. + ## Access Grafana After starting the stack, open Grafana at `localhost:3000` in your browser. @@ -134,32 +217,42 @@ Single-node time-series database optimized for Prometheus metrics. **Performance tuning:** ```yaml -victoriametrics: - command: - - "-retentionPeriod=90d" - - "-memory.allowedPercent=60" - - "-search.maxConcurrentRequests=16" +sink-prometheus: + environment: + - VM_RETENTION_PERIOD=336h + - VM_QUERY_DURATION=30s + - VM_MAX_CONCURRENT_REQUESTS=16 ``` ### pgwatch -Metrics collector for PostgreSQL. Scrapes your database every 60 seconds by default. +Metrics collectors for PostgreSQL. The 0.15 stack runs separate `postgresai/pgwatch` services for PostgreSQL and Prometheus-compatible sinks. -**Custom metrics interval:** +**Prometheus sink options:** ```yaml -pgwatch: - environment: - - PW_INTERNAL_STATS_PORT=8081 - - PW_MIN_DB_SIZE_MB=0 +pgwatch-prometheus: + command: + - "--sources=/postgres_ai_configs/pgwatch-prometheus/sources.yml" + - "--metrics=/postgres_ai_configs/pgwatch-prometheus/metrics.yml" + - "--sink=prometheus://0.0.0.0:9091/pgwatch" + - "--web-addr=:8089" + - "--log-level=error" ``` ### Flask backend Provides query text lookup for Grafana dashboards (joining pg_stat_statements queryid with actual SQL). +The backend listens on port 8000 inside the Docker network and is **not published to the host**, so +reach its health endpoint from within the network: **Health check:** + +The backend image is `python:3.11-slim` and does not include `curl`, so hit the endpoint with the +Python interpreter that ships in the image: + ```bash -curl http://localhost:8000/health +docker compose exec monitoring_flask_backend \ + python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)" ``` ## Directory structure @@ -168,21 +261,27 @@ curl http://localhost:8000/health postgresai/ ├── docker-compose.yml ├── .env +├── instances.yml # Databases to monitor ├── config/ │ ├── grafana/ -│ │ ├── provisioning/ -│ │ │ ├── dashboards/ -│ │ │ │ └── postgres_ai/ # Dashboard JSON files -│ │ │ └── datasources/ -│ │ │ └── default.yaml # VictoriaMetrics datasource -│ │ └── grafana.ini +│ │ ├── dashboards/ # Dashboard JSON files (14 dashboards + self-monitoring) +│ │ └── provisioning/ +│ │ ├── dashboards/ +│ │ │ └── dashboards.yml # Dashboard provider +│ │ ├── datasources/ +│ │ │ └── datasources.yml # PGWatch-PostgreSQL / PGWatch-Prometheus / Infinity datasources +│ │ └── grafana.ini │ └── prometheus/ -│ └── prometheus.yml # Scrape configuration +│ └── prometheus.yml # Scrape configuration └── monitoring_flask_backend/ ├── app.py └── Dockerfile ``` +At runtime these configs are copied into the `postgres_ai_configs` Docker volume by the +`config-init` service and mounted into the containers at `/postgres_ai_configs` (Grafana reads +its provisioning from `GF_PATHS_PROVISIONING=/postgres_ai_configs/grafana/provisioning`). + ## Operations ### Start stack @@ -203,8 +302,8 @@ docker compose down # All services docker compose logs -f -# Specific service -docker compose logs -f pgwatch +# Specific services +docker compose logs -f pgwatch-postgres pgwatch-prometheus ``` ### Restart service @@ -222,24 +321,39 @@ docker compose up -d ## Adding multiple databases -To monitor multiple PostgreSQL instances, add additional pgwatch services: +To monitor multiple PostgreSQL instances, add them to `instances.yml`; `sources-generator` renders both pgwatch source files from that list: ```yaml -services: - pgwatch-prod: - image: cybertec/pgwatch:latest - environment: - - PW_SOURCES=postgresql://postgres_ai_mon:pass@prod-db:5432/app - - PW_SOURCE_NAME=prod-primary +- name: prod-primary + conn_str: postgresql://postgres_ai_mon:pass@prod-db:5432/app + preset_metrics: full + is_enabled: true + group: production + custom_tags: + env: production + cluster: prod + node_name: primary + +- name: staging-primary + conn_str: postgresql://postgres_ai_mon:pass@staging-db:5432/app + preset_metrics: full + is_enabled: true + group: staging + custom_tags: + env: staging + cluster: staging + node_name: primary +``` - pgwatch-staging: - image: cybertec/pgwatch:latest - environment: - - PW_SOURCES=postgresql://postgres_ai_mon:pass@staging-db:5432/app - - PW_SOURCE_NAME=staging-primary +After editing `instances.yml`, re-render both `sources.yml` files and restart the collectors so +they reload the new list (the running pgwatch collectors read `sources.yml` only at startup): + +```bash +docker compose run --rm sources-generator # re-render pgwatch/*/sources.yml +docker compose restart pgwatch-postgres pgwatch-prometheus # reload the regenerated sources ``` -Use the `cluster_name` variable in Grafana to switch between environments. +Use the `cluster` and `node_name` custom tags in Grafana to switch between environments. ## Troubleshooting @@ -247,12 +361,14 @@ Use the `cluster_name` variable in Grafana to switch between environments. 1. Check pgwatch is collecting metrics: ```bash - docker compose logs pgwatch | grep -i error + docker compose logs pgwatch-postgres pgwatch-prometheus | grep -i error ``` -2. Verify VictoriaMetrics has data: +2. Verify VictoriaMetrics has data (host port `59090`, VM basic auth; all pgwatch series are + `pgwatch_`-prefixed): ```bash - curl 'http://localhost:8428/api/v1/query?query=pg_stat_user_tables_n_tup_ins' + curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=pgwatch_pg_stat_all_tables_n_tup_ins' ``` 3. Check Grafana datasource: @@ -261,22 +377,27 @@ Use the `cluster_name` variable in Grafana to switch between environments. ### High memory usage -VictoriaMetrics memory is proportional to active time series: +VictoriaMetrics memory is proportional to active time series. It sizes its caches +from the container memory limit, so cap memory by lowering that limit (or set +`SINK_PROMETHEUS_MEM` in `.env`) rather than overriding the service `command`: ```yaml -victoriametrics: - command: - - "-memory.allowedPercent=40" # Reduce from default 60% +sink-prometheus: + mem_limit: 1073741824 # 1 GiB (default is 1.5 GiB / SINK_PROMETHEUS_MEM) ``` ### Container networking issues -If pgwatch can't reach your database: +If the pgwatch collectors can't reach your database, add the host mapping to both collector services: ```yaml -pgwatch: +pgwatch-postgres: extra_hosts: - "host.docker.internal:host-gateway" # For host machine access + +pgwatch-prometheus: + extra_hosts: + - "host.docker.internal:host-gateway" ``` ## Next steps diff --git a/docs/monitoring/getting-started/installation-helm.md b/docs/monitoring/getting-started/installation-helm.md index 779b5849..e9a253b9 100644 --- a/docs/monitoring/getting-started/installation-helm.md +++ b/docs/monitoring/getting-started/installation-helm.md @@ -6,171 +6,332 @@ sidebar_position: 5 # Helm installation -:::info Enterprise feature -Kubernetes deployment with Helm is available for Enterprise customers. [Contact us](https://postgres.ai/contact) to get started. +:::info Positioned for production / Enterprise +Kubernetes deployment with Helm is the recommended path for production and Enterprise use. The chart +is self-service and the full procedure is below; for dedicated support, SLAs, and managed rollout, +[contact us](https://postgres.ai/contact). ::: Deploy PostgresAI monitoring on Kubernetes using Helm. +:::info 0.15 chart defaults +The Helm chart defaults have been prepared for PostgresAI 0.15 — review updated chart values +before upgrading. Two things to note when moving to 0.15: + +- **VictoriaMetrics basic auth** is available but **off by default** (`victoriaMetrics.auth.enabled: false`). + To protect the VictoriaMetrics endpoint, set `victoriaMetrics.auth.enabled: true` and supply the + password — see [VictoriaMetrics authentication](#victoriametrics-authentication) below. +- **Metrics retention** is set with `victoriaMetrics.retentionPeriod`, which defaults to `336h` + (14 days) — the same default as the Docker Compose stack. Set it explicitly to match your plan. +::: + ## Prerequisites - Kubernetes 1.19+ - Helm 3.0+ - `kubectl` configured for your cluster -- PostgreSQL 14+ target database (accessible from cluster) +- PostgreSQL 13+ (14+ recommended) target database (accessible from cluster) ## Quick start +Monitored databases are configured as a list under `monitoredDatabases`, and their passwords +come from a Kubernetes secret (never inline). The minimal flow is: + +```bash +# 1. Create the namespace +kubectl create namespace postgres-ai-mon + +# 2. Create the secret holding the sink, Grafana, and per-database passwords +kubectl create secret generic postgres-ai-monitoring-secrets \ + --namespace postgres-ai-mon \ + --from-literal=postgres-password='SINK_POSTGRES_PASSWORD' \ + --from-literal=grafana-admin-user='monitor' \ + --from-literal=grafana-admin-password='GRAFANA_PASSWORD' \ + --from-literal=pgai-api-key='POSTGRES_AI_API_KEY' \ + --from-literal=db-password-my-db-password='DB_PASSWORD' + +# 3. Install the chart from a repository checkout +git clone https://gitlab.com/postgres-ai/postgresai.git && cd postgresai +helm install postgres-ai-monitoring ./postgres_ai_helm \ + --namespace postgres-ai-mon \ + --values custom-values.yaml +``` + +Installing from the chart directory in a repository checkout (the command above) works against +any tag or branch and is the most reliable option. + +If you prefer a packaged `.tgz`, the chart is published as a release asset under a `helm-v` +tag. Once the matching release exists (for example `helm-v0.15.0`), open its +[Releases page](https://gitlab.com/postgres-ai/postgresai/-/releases) and download the +`postgres-ai-monitoring-chart.tgz` asset from that release, then install it: + ```bash -# Add the postgres-ai Helm repository -helm repo add postgres-ai https://charts.postgres.ai -helm repo update - -# Install with default values -helm install postgres-ai-monitoring postgres-ai/monitoring \ - --set target.host=your-postgres-host \ - --set target.port=5432 \ - --set target.database=your_database \ - --set target.user=pgwatch \ - --set target.password=your_password +helm install postgres-ai-monitoring postgres-ai-monitoring-chart.tgz \ + --namespace postgres-ai-mon \ + --values custom-values.yaml ``` +:::note +The packaged chart is registered as a generic release-asset link, so use the **Releases page** +asset for the tag to obtain its download URL. There is no stable +`/-/releases//downloads/` permalink for it. The `helm-v0.15.0` release may not be cut +yet at the time of reading — in that case use the repository-checkout install above. +::: + +The secret key for each monitored database must be named `db-password-`, +where `` matches the `passwordSecretKey` set on that database in +`monitoredDatabases` (see below). + ## Configuration ### values.yaml overview +The chart configures monitored databases as a **list** (`monitoredDatabases`), not a single +`target`. Cluster/node identity comes from `global.clusterName` / `global.nodeName` (with optional +per-database overrides), and all credentials live in a Kubernetes secret. The keys below match +the chart's `values.yaml` exactly: + ```yaml -# Target PostgreSQL connection -target: - host: "postgres.default.svc.cluster.local" - port: 5432 - database: "myapp" - user: "pgwatch" - password: "" # Use secret instead - existingSecret: "" # Name of secret containing password - -# Cluster identification -cluster: - name: "production" - nodeName: "primary" - -# Grafana configuration +# Top-level secret reference (recommended). When set, the chart reads all +# credentials (postgres, grafana, vm-auth, per-database passwords) from this secret. +existingSecret: + name: postgres-ai-monitoring-secrets + +# Cluster/node identity applied to all monitored databases by default +global: + clusterName: my-cluster + nodeName: my-node + customTags: {} + +# Databases to monitor (a list). Passwords are referenced by secret key, never inline. +monitoredDatabases: + - name: my-db + host: db-host.example.com + port: 5432 + database: postgres + user: postgres_ai_mon + passwordSecretKey: my-db-password # secret key: db-password-my-db-password + presetMetrics: full + customMetrics: {} + isEnabled: true + group: production + # Optional per-database overrides: + # clusterName: prod-us-east + # nodeName: node-02 + customTags: + env: production + +# VictoriaMetrics configuration +victoriaMetrics: + image: victoriametrics/victoria-metrics:v1.140.0 + retentionPeriod: 336h # default 336h (14 days); same as the Docker stack — set to match your plan + scrapeInterval: 15s + # VictoriaMetrics basic auth (optional in 0.15; off by default). The VictoriaMetrics + # StatefulSet, Flask, and the reporter read the password from the chart secret key + # `vm-auth-password`; the Grafana datasource instead reads the inline value + # `secrets.vmAuth.password`, so enabling auth requires supplying both (see below). + auth: + enabled: false + username: "vmauth" + service: + type: ClusterIP + port: 8428 + resources: {} + +# pgwatch collectors. Two deployments are shipped: one writing to the Postgres +# sink and one writing to the Prometheus (VictoriaMetrics) sink. Each runs a +# single replica. +pgwatchPostgres: + enabled: true + image: postgresai/pgwatch:0.15.0 + logLevel: error + resources: {} + +pgwatchPrometheus: + enabled: true + image: postgresai/pgwatch:0.15.0 + logLevel: error + resources: {} + +# Grafana is the upstream grafana/grafana subchart. Admin credentials come from +# the secret via grafana.admin; persistence/service/ingress are subchart values. grafana: enabled: true - replicas: 1 - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: 500m - memory: 512Mi - ingress: - enabled: false - hosts: - - grafana.example.com - tls: [] - adminPassword: "" # Auto-generated if empty + image: + repository: grafana/grafana + tag: "12.3.2" + admin: + existingSecret: postgres-ai-monitoring-secrets + userKey: grafana-admin-user + passwordKey: grafana-admin-password persistence: enabled: true size: 5Gi + service: + type: ClusterIP + port: 80 + +# Persistent volume sizing +storage: + postgresSize: 50Gi + victoriaMetricsSize: 150Gi + grafanaSize: 5Gi + storageClassName: "" # empty = cluster default +``` -# VictoriaMetrics configuration -victoriametrics: - enabled: true - replicas: 1 - retention: 90d - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: 1000m - memory: 1Gi - persistence: - enabled: true - size: 50Gi +:::note Subchart-managed Grafana +`grafana` is the upstream [grafana/grafana](https://github.com/grafana/helm-charts) subchart, so +its resource requests/limits, ingress, and replica count are configured with that subchart's own +values (for example `grafana.resources`). The PostgresAI chart only pins `grafana.image`, +`grafana.admin`, `grafana.persistence`, `grafana.service`, and the dashboard/datasource sidecars. +There is no top-level `grafana.adminPassword` value — set the admin password in the secret +(`grafana-admin-password`). +::: -# pgwatch collector -pgwatch: - enabled: true - replicas: 1 - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - cpu: 500m - memory: 256Mi +### Using Kubernetes secrets + +For production, store all credentials in a single Kubernetes secret and reference it with the +top-level `existingSecret`. The chart reads these keys: + +| Secret key | Used for | +|------------|----------| +| `postgres-password` | Internal Postgres metrics sink | +| `grafana-admin-user` | Grafana admin username | +| `grafana-admin-password` | Grafana admin password | +| `pgai-api-key` | PostgresAI platform API key (reporter) | +| `vm-auth-password` | VictoriaMetrics basic auth (when `victoriaMetrics.auth.enabled: true`) | +| `db-password-` | Password for each monitored database | + +```bash +kubectl create secret generic postgres-ai-monitoring-secrets \ + --namespace postgres-ai-mon \ + --from-literal=postgres-password='...' \ + --from-literal=grafana-admin-user='monitor' \ + --from-literal=grafana-admin-password='...' \ + --from-literal=pgai-api-key='...' \ + --from-literal=db-password-my-db-password='...' ``` -### Using Kubernetes secrets +Reference it in values: -For production, store credentials in a secret: +```yaml +existingSecret: + name: postgres-ai-monitoring-secrets +``` + +:::note Development only +For development/testing, the chart can render the secret from values by setting +`secrets.createFromValues: true` and supplying `secrets.postgres.password`, +`secrets.grafana.adminPassword`, etc. **Never commit real secrets to version control.** +::: + +### VictoriaMetrics authentication + +New in 0.15, VictoriaMetrics can be protected with HTTP basic auth. It is **off by default** +(`victoriaMetrics.auth.enabled: false`); enable it to secure the metrics endpoint. When enabled, +the VictoriaMetrics StatefulSet, the Flask backend, and the reporter read the password from the +chart secret key **`vm-auth-password`** (via `secretKeyRef`). + +:::warning The Grafana datasource reads the password from an inline value, not the secret +The Grafana datasource ConfigMap is the exception: it renders `basicAuthPassword` from the +**inline** chart value `secrets.vmAuth.password`, **not** from the `vm-auth-password` secret key. +So when `victoriaMetrics.auth.enabled: true`, you must also supply `secrets.vmAuth.password` (for +example via `secrets.createFromValues: true`, or by setting it directly) — otherwise the datasource +is rendered with the unset placeholder default (`CHANGE_ME_vm_auth_password`), Grafana cannot +authenticate to VictoriaMetrics, and all dashboards show no data. Putting the password only in the +pre-created `existingSecret` is enough for VictoriaMetrics, Flask, and the reporter, but **not** for +the Grafana datasource. +::: + +Turn it on in values: ```yaml -apiVersion: v1 -kind: Secret -metadata: - name: postgres-ai-target -type: Opaque -stringData: - password: "your-secure-password" +victoriaMetrics: + auth: + enabled: true + username: "vmauth" # default ``` -Reference in values: +Provide the `vm-auth-password` value through the chart's secret. For production, store it in a +pre-created Kubernetes secret and reference it via the top-level `existingSecret` (the same secret +also holds the Postgres and Grafana credentials): + +```bash +kubectl create secret generic postgres-ai-monitoring-secrets \ + --namespace postgres-ai-mon \ + --from-literal=postgres-password='...' \ + --from-literal=grafana-admin-user='monitor' \ + --from-literal=grafana-admin-password='...' \ + --from-literal=vm-auth-password='your-vm-auth-secret' +``` ```yaml -target: - existingSecret: "postgres-ai-target" - # passwordKey defaults to "password" +existingSecret: + name: postgres-ai-monitoring-secrets ``` +For development/testing only, the chart can render the secret from values +(`secrets.createFromValues: true`): + +```yaml +secrets: + createFromValues: true # never commit real secrets to version control + vmAuth: + password: "your-vm-auth-secret" +``` + +This mirrors the `VM_AUTH_USERNAME` / `VM_AUTH_PASSWORD` keys used by the Docker Compose stack — +see [Authentication and security](/docs/monitoring/configuration/prometheus-config#authentication-and-security). + ### Ingress configuration -Enable external access to Grafana: +External access to Grafana is configured with the chart's **top-level** `ingress` block (enabled +by default). The host is set under `ingress.hosts.grafana`: ```yaml -grafana: - ingress: - enabled: true - className: nginx - annotations: - cert-manager.io/cluster-issuer: letsencrypt - hosts: - - host: grafana.example.com - paths: - - path: / - pathType: Prefix - tls: - - secretName: grafana-tls - hosts: - - grafana.example.com +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt + hosts: + grafana: grafana.example.com + tls: + - secretName: grafana-tls + hosts: + - grafana.example.com ``` +:::note +This is the chart's own `ingress` value, not the Grafana subchart's. The subchart's +`grafana.ingress` is disabled by default and is not used by this stack. +::: + ## Installation commands ### Install with custom values ```bash -helm install postgres-ai-monitoring postgres-ai/monitoring \ - -f values.yaml \ - --namespace monitoring \ +# postgres-ai-monitoring-chart.tgz is the release artifact from GitLab Releases (see Quick start), +# or use ./postgres_ai_helm from a repository checkout +helm install postgres-ai-monitoring postgres-ai-monitoring-chart.tgz \ + -f custom-values.yaml \ + --namespace postgres-ai-mon \ --create-namespace ``` ### Upgrade existing installation ```bash -helm upgrade postgres-ai-monitoring postgres-ai/monitoring \ - -f values.yaml \ - --namespace monitoring +helm upgrade postgres-ai-monitoring postgres-ai-monitoring-chart.tgz \ + -f custom-values.yaml \ + --namespace postgres-ai-mon ``` ### Uninstall ```bash -helm uninstall postgres-ai-monitoring --namespace monitoring +helm uninstall postgres-ai-monitoring --namespace postgres-ai-mon ``` :::warning @@ -181,62 +342,63 @@ Uninstalling removes all components including data. Back up VictoriaMetrics PVC ### Port forward (development) +The Grafana subchart's service listens on port 80: + ```bash -kubectl port-forward svc/postgres-ai-monitoring-grafana 3000:3000 -n monitoring +kubectl port-forward svc/postgres-ai-monitoring-grafana 3000:80 -n postgres-ai-mon ``` -Open Grafana +Then open `http://localhost:3000`. -### Get admin password +### Get admin credentials -If not specified in values: +The admin username and password come from the chart secret (keys `grafana-admin-user` / +`grafana-admin-password`): ```bash -kubectl get secret postgres-ai-monitoring-grafana -n monitoring \ - -o jsonpath="{.data.admin-password}" | base64 --decode +kubectl get secret postgres-ai-monitoring-secrets -n postgres-ai-mon \ + -o jsonpath="{.data.grafana-admin-password}" | base64 --decode ``` ## Multi-cluster monitoring -Monitor multiple PostgreSQL clusters from a single Grafana instance: - -### Option 1: Multiple pgwatch deployments +Monitor multiple PostgreSQL clusters from a single installation by adding entries to +`monitoredDatabases`. Set `clusterName` / `nodeName` per database to tag each one; if omitted, +they inherit `global.clusterName` / `global.nodeName`. The reporter creates a separate cronjob +for each unique cluster/node combination. ```yaml -# values-cluster1.yaml -cluster: - name: "prod-us-east" -pgwatch: - enabled: true -grafana: - enabled: false # Disable for secondary clusters - -# values-cluster2.yaml -cluster: - name: "prod-us-west" -pgwatch: - enabled: true -grafana: - enabled: false -``` - -Install each: -```bash -helm install monitoring-us-east postgres-ai/monitoring -f values-cluster1.yaml -helm install monitoring-us-west postgres-ai/monitoring -f values-cluster2.yaml +global: + clusterName: default + nodeName: default + +monitoredDatabases: + - name: prod-us-east + host: db-east.example.com + port: 5432 + database: postgres + user: postgres_ai_mon + passwordSecretKey: prod-us-east-password + presetMetrics: full + isEnabled: true + group: production + clusterName: prod-us-east + nodeName: node-01 + + - name: prod-us-west + host: db-west.example.com + port: 5432 + database: postgres + user: postgres_ai_mon + passwordSecretKey: prod-us-west-password + presetMetrics: full + isEnabled: true + group: production + clusterName: prod-us-west + nodeName: node-01 ``` -### Option 2: Multi-target configuration - -Configure pgwatch to scrape multiple databases: - -```yaml -pgwatch: - extraSources: - - name: "staging-db" - connstring: "postgresql://user:pass@staging-host:5432/db" - cluster_name: "staging" -``` +Add a matching `db-password-` key to the secret for each database. ## Resource sizing @@ -246,11 +408,11 @@ pgwatch: grafana: resources: requests: { cpu: 100m, memory: 256Mi } -victoriametrics: +victoriaMetrics: resources: requests: { cpu: 100m, memory: 256Mi } - persistence: - size: 10Gi +storage: + victoriaMetricsSize: 10Gi ``` ### Medium (5-20 databases) @@ -259,11 +421,11 @@ victoriametrics: grafana: resources: requests: { cpu: 200m, memory: 512Mi } -victoriametrics: +victoriaMetrics: resources: requests: { cpu: 500m, memory: 1Gi } - persistence: - size: 50Gi +storage: + victoriaMetricsSize: 50Gi ``` ### Large (20+ databases) @@ -272,11 +434,11 @@ victoriametrics: grafana: resources: requests: { cpu: 500m, memory: 1Gi } -victoriametrics: +victoriaMetrics: resources: requests: { cpu: 1000m, memory: 4Gi } - persistence: - size: 200Gi +storage: + victoriaMetricsSize: 200Gi ``` ## Troubleshooting @@ -285,30 +447,35 @@ victoriametrics: Check events: ```bash -kubectl describe pod -l app=postgres-ai-monitoring -n monitoring +kubectl describe pod -l app.kubernetes.io/instance=postgres-ai-monitoring -n postgres-ai-mon ``` Common issues: - PVC not provisioned (check storage class) -- Secret not found +- Secret not found (check `existingSecret.name` and the `db-password-*` keys) - Resource limits too low ### No data in Grafana -1. Check pgwatch logs: +1. Check the pgwatch collector logs (two deployments: `-pgwatch-postgres` and + `-pgwatch-prometheus`): ```bash - kubectl logs -l app=pgwatch -n monitoring + kubectl logs -l app.kubernetes.io/component=pgwatch-prometheus -n postgres-ai-mon ``` -2. Verify target connectivity: +2. Verify target connectivity. The pgwatch image is a minimal Alpine build + with only the collector binary (no `psql`), so run the check from a throwaway + `postgres:17` pod instead of `kubectl exec` into the pgwatch pod: ```bash - kubectl exec -it deploy/pgwatch -n monitoring -- \ + kubectl run pg-check --rm -it --image=postgres:17 --restart=Never -n postgres-ai-mon -- \ psql "postgresql://user:pass@host:5432/db" -c "SELECT 1" ``` + (If you prefer not to spin up a pod, the collector logs from step 1 already + surface connection errors.) 3. Check VictoriaMetrics has data: ```bash - kubectl port-forward svc/victoriametrics 8428:8428 -n monitoring + kubectl port-forward svc/postgres-ai-monitoring-victoriametrics 8428:8428 -n postgres-ai-mon curl 'http://localhost:8428/api/v1/query?query=up' ``` diff --git a/docs/monitoring/getting-started/quickstart-rds-privatelink.md b/docs/monitoring/getting-started/quickstart-rds-privatelink.md new file mode 100644 index 00000000..c4b38874 --- /dev/null +++ b/docs/monitoring/getting-started/quickstart-rds-privatelink.md @@ -0,0 +1,280 @@ +--- +title: Quick start for Amazon RDS / Aurora over AWS PrivateLink +sidebar_label: Quick start for Amazon RDS (AWS PrivateLink) +sidebar_position: 9 +keywords: + - "PostgresAI for Amazon RDS" + - "Postgres monitoring for RDS" + - "Amazon RDS AWS PrivateLink" + - "private RDS monitoring" + - "Aurora monitoring Grafana" +--- + +# Quick start guide for Amazon RDS / Aurora over AWS PrivateLink + +Set up PostgresAI monitoring for a **private** Amazon RDS or Aurora database — one that has no +public endpoint — using the guided setup in [PostgresAI Console](https://console.postgres.ai/). +PostgresAI reaches your database over **AWS PrivateLink**, so **no inbound port is ever opened** on +your account: the connection is outbound-only, from your VPC to PostgresAI. + +:::info Available on the Scale plan and Enterprise only +Private-RDS monitoring over **AWS PrivateLink** is available on the **Scale** plan and on +**Enterprise**. Consulting clients get it packaged as part of their +engagement. If you prefer not to use PrivateLink, you can instead monitor RDS by exposing a +publicly reachable database port to PostgresAI +(see [Cloud installation](/docs/monitoring/getting-started/installation-cloud)). See +[Pricing](/pricing) for the full feature comparison. +::: + +## Overview + +This flow connects a database that lives entirely inside your VPC: + +1. You create a least-privilege, **read-only** monitoring role on your RDS instance. +2. You launch a one-click CloudFormation stack **in your own AWS account**. It publishes your RDS + over **AWS PrivateLink** (an internal Network Load Balancer plus a VPC endpoint service) and + allowlists only the PostgresAI principal. +3. You paste the resulting endpoint-service name back into PostgresAI Console. +4. PostgresAI provisions a dedicated monitoring VM, creates an interface endpoint to your service, + and starts collecting metrics. + +The collector is **read-only and metadata-only** — it reads statistics, normalized query text, and +wait events. It never reads your data or raw query parameters. See +[data privacy details](/docs/monitoring/#data-privacy-metadata-only). + +:::caution Amazon RDS (single instance) only +The one-click CloudFormation template publishes a **single-instance Amazon RDS** database. Aurora +clusters and multi-instance setups are not yet supported by this flow. +::: + +## Prerequisites + +1. A private Amazon RDS for PostgreSQL instance (PostgreSQL 14+), with no public access required. +2. A [PostgresAI Console](https://console.postgres.ai/) account on the **Scale** plan (or + Enterprise). Sign up with Google, LinkedIn, GitHub, or GitLab. +3. An organization in PostgresAI Console. + [Create one](https://console.postgres.ai/addorg) if you don't have one yet. You must be an + **organization admin** to provision RDS monitoring. +4. A payment method on file. In your organization, open **Billing**, click **Edit payment + methods**, and add a card in the Stripe portal. +5. AWS permissions to launch a CloudFormation stack in the account and Region where your RDS runs + (it creates an internal NLB and a VPC endpoint service), and the **master database user** to run + the one-time preparation SQL. + +## Step 1. Start the guided setup + +In PostgresAI Console, navigate to **Checkup — Getting started**. On the **RDS / Aurora** card, +click **Start guided setup**. + +[![PostgresAI Console: Getting started page with the RDS / Aurora "Start guided setup" card](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-1.png)](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-1.png) + +## Step 2. Choose the Scale (or Enterprise) plan + +AWS PrivateLink monitoring runs on the full monitoring stack, which is available on **Scale** and +**Enterprise**. On the plan page, click **Choose Scale** (or **Contact sales** for Enterprise). +Consulting clients already have it enabled and can skip this step. + +See [Pricing](/pricing) for the full list of options with feature comparison. + +[![Choose the PostgresAI plan: Scale includes private RDS via AWS PrivateLink](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-2.png)](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-2.png) + +:::note +The console screenshot above still shows the retired **Starter** plan (the Scale card's "Everything +included in Starter" bullet refers to it). Starter is no longer offered — click **Choose Scale**, or +contact us about **Enterprise**. +::: + +## Step 3. Create the read-only monitoring role + +The **Set up RDS monitoring over AWS PrivateLink** wizard opens. In **Step 1**, click **Generate +database-preparation SQL**. PostgresAI shows a one-time script that creates the least-privilege, +**read-only, metadata-only** `postgres_ai_mon` role, grants `pg_monitor`, and creates the +`postgres_ai` schema with a few read-only helper views. + +[![Set up RDS monitoring over AWS PrivateLink: the four-step in-console wizard](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-3.png)](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-3.png) + +Run it once as the **master user**, for example: + +```bash +psql "host= port=5432 dbname= sslmode=require" +``` + +Then paste the SQL shown in the console. + +:::tip Review the SQL and run it against the right database +Read through the generated SQL before you run it — it is short, and only creates a read-only role, +the `postgres_ai` schema, and helper views; it grants no write access and touches no table data. +Make sure you connect to the **correct logical database** — the one you actually want monitored +(typically your application database, not the default `postgres`) — because the role and helper +objects are created in whichever database you run the script against. +::: + +To review the exact statements at any time, run: + +```bash +npx postgresai@latest prepare-db --print-sql +``` + +This confirms the minimal, read-only nature of the permissions. + +## Step 4. Launch and fill the CloudFormation stack + +Back in the wizard, in **Step 2** select your **AWS Region** (it scopes the stack-launch link, and +it must match the Region your RDS runs in). In **Step 3**, click **Launch stack in AWS console**. +This opens the AWS **Quick create stack** page in your own account, pre-filled with the PostgresAI +principal to allowlist. The launch link carries only non-secret parameters. + +Review the template (it is intentionally published and line-by-line auditable), then fill in the +typed parameters: + +- **Stack name** — for example `postgresai-rds-privatelink`. +- **RDS DB instance identifier** — your RDS instance's identifier, taken from the RDS console (a + **single-instance** Amazon RDS, not an Aurora cluster). +- **RDS port** — default `5432`. +- **VPC of the RDS** — the VPC your RDS instance runs in (a typed `AWS::EC2::VPC::Id` dropdown). +- **Subnets (the RDS's AZs)** — pick one subnet per Availability Zone your RDS can run in. **For a + Multi-AZ instance, select the subnets for _all_ of its AZs** so the internal load balancer can + follow the database if it fails over. They must be in the VPC above and able to reach the RDS (no + NAT gateway or special egress is required — it stays inside the VPC). +- **PostgresAI principal ARN** — pre-filled; leave it as-is unless instructed otherwise. + +[![AWS CloudFormation Quick create stack: the Your RDS parameters (identifier, port, VPC, subnets)](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-4.png)](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-4.png) + +A filled-in example — note that **all** of the RDS's subnets are selected for failover coverage: + +[![CloudFormation parameters filled in: DB identifier, VPC, and all AZ subnets selected](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-5.png)](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-5.png) + +The stack creates an **internal** Network Load Balancer in front of your RDS primary (its target IP +is kept current by a small failover Lambda) and a **VPC endpoint service** — the AWS PrivateLink +provider — that allows **only** the PostgresAI principal. **No inbound security-group rule or public +database port is created**; the path is outbound-only from your VPC to PostgresAI. + +:::caution Keep everything in the same AWS Region +Your RDS instance, the Network Load Balancer, and the VPC endpoint service must all be in the +**same AWS Region** — AWS PrivateLink does not cross Regions. Make sure the Region you selected in +PostgresAI Console matches the Region of your RDS. +::: + +## Step 5. Copy the endpoint-service name + +When the stack reaches **CREATE_COMPLETE**, open its **Outputs** tab and copy the +**`VpceServiceName`** value — it looks like `com.amazonaws.vpce..vpce-svc-0abc…`. + +[![CloudFormation stack Outputs tab with the VpceServiceName value highlighted](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-6.png)](/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-6.png) + +:::tip +The **`RevokeAccessHint`** output tells you exactly how to cut PostgresAI off later (drop the role, +remove the principal from the endpoint-service permissions, or delete the stack). +::: + +## Step 6. Paste it back and deploy + +Back in PostgresAI Console (wizard **Step 4**), paste the **`VpceServiceName`** into the +**VPC Endpoint Service name** field and your **RDS endpoint** hostname into the **RDS endpoint** +field, then click **Deploy**. + +## Step 7. Wait for deployment + +PostgresAI provisions a dedicated monitoring VM, creates an interface endpoint to your endpoint +service over **AWS PrivateLink**, and connects to your database as the read-only role. + +The connection is verified automatically before monitoring is reported as active: PostgresAI +confirms it can reach the database, that the monitoring role works, and that the database belongs to +your organization. Once all checks pass, the console shows **Monitoring active — this database is +now being monitored.** + +While waiting, you can set up the CLI tools: + +```bash +# Install CLI +npm i -g postgresai + +# Authenticate +postgresai auth + +# Set up MCP for your AI coding tool (Cursor, Claude Code, etc.) +postgresai mcp install +``` + +:::note Deployment, Grafana, and Issues are the same across providers +The installation-progress, Grafana sign-in, and first-Issues screens are identical for every +PostgresAI monitoring setup. See the [Supabase quick start](/docs/monitoring/getting-started/quickstart-supabase) +(steps 5–8) for screenshots of those steps. +::: + +## Step 8. Open Grafana dashboards + +Once monitoring is active, open the Grafana URL from the console. You can sign in with the Grafana +credentials shown after deployment, or click **Sign in with PostgresAI** for passwordless access. + +Start with **01. Single node performance overview (high-level)** for a high-level health check of +your RDS / Aurora database. Key panels to check first: + +1. **Active session history (ASH)** — wait events over time (similar to RDS Performance Insights) +2. **Sessions** — active, idle, and idle in transaction connections +3. **TPS** — transactions per second +4. **QPS** — queries per second + +## Step 9. Review first issues + +After about 30 minutes, PostgresAI generates the first automated issue reports. Navigate to +**Issues** in PostgresAI Console to see detected problems and recommended actions. + +Common issues detected automatically include: + +- **Redundant indexes** — duplicate indexes wasting storage +- **Unused indexes** — indexes that are never scanned +- **Invalid indexes** — indexes that failed to build +- **Autovacuum tuning** — recommended configuration changes +- **Minor version updates** — available PostgreSQL updates + +See [How to work with issues](/docs/postgresai-howtos/how-to-work-with-issues) for details on +managing issues, assigning team members, and integrating with AI coding tools. + +## Next steps + +- [Dashboard guide](/docs/monitoring/dashboards/) — complete dashboard reference +- [PostgresAI CLI](/docs/postgresai-howtos/postgresai-cli) — CLI setup and commands +- [MCP integration](/docs/postgresai-howtos/how-to-install-mcp) — set up MCP for Cursor, Claude + Code, or other AI coding tools + +## FAQ + +### Does PostgresAI open any inbound ports on my account? + +No. The connection uses **AWS PrivateLink** and is outbound-only: your VPC endpoint service exposes +the database to the PostgresAI principal you allowlist, with no inbound security-group rule and no +public database port. To cut PostgresAI off, drop the monitoring role, remove the PostgresAI +principal from the endpoint-service permissions, or delete the CloudFormation stack (see the +`RevokeAccessHint` stack output). + +### What database role is created and what permissions does it have? + +The `postgres_ai_mon` role is created with read-only, metadata-only access (`pg_monitor` plus the +`postgres_ai` helper schema). On RDS, optional superuser-only grants are skipped +(`include_optional = false`). To review the exact SQL statements at any time: + +```bash +npx postgresai@latest prepare-db --print-sql +``` + +### What data is collected from my database? + +Only database metadata — no actual data or raw query parameters. Query text is collected +**normalized** (parameters stripped) from `pg_stat_statements`. To review exactly what metrics are +collected, examine the metric definitions: + +- **Prometheus sink metrics**: + [metrics.yml (pgwatch-prometheus)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-prometheus/metrics.yml) +- **PostgreSQL sink metrics** (including normalized queries): + [metrics.yml (pgwatch-postgres)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-postgres/metrics.yml) + +See also: [data privacy details](/docs/monitoring/#data-privacy-metadata-only). + +### Why is private-RDS monitoring available only on Scale and Enterprise? + +The **AWS PrivateLink** path provisions dedicated infrastructure (a monitoring VM and an interface +endpoint) and is part of the full monitoring stack, available on the **Scale** plan and +**Enterprise**. Consulting clients get it packaged with their engagement. If you prefer not to use +PrivateLink, you can instead monitor RDS by exposing a publicly reachable database port — see +[Cloud installation](/docs/monitoring/getting-started/installation-cloud). diff --git a/docs/monitoring/getting-started/quickstart-supabase.md b/docs/monitoring/getting-started/quickstart-supabase.md index 05d84a4d..3c566a3d 100644 --- a/docs/monitoring/getting-started/quickstart-supabase.md +++ b/docs/monitoring/getting-started/quickstart-supabase.md @@ -1,7 +1,7 @@ --- title: Quick start for Supabase sidebar_label: Quick start for Supabase -sidebar_position: 7 +sidebar_position: 8 keywords: - "PostgresAI for Supabase" - "Postgres monitoring for Supabase" @@ -24,7 +24,7 @@ Two monitoring levels are available: | Level | Includes | Plan | |-------|----------|------| | **Quick setup** | Auto-discovery, daily checkups, JSON reports | Free | -| **Full monitoring** | Grafana dashboards, real-time metrics, advanced alerts, historical data | Scaling | +| **Full monitoring** | Grafana dashboards, real-time metrics, advanced alerts, historical data | Scale | ## Prerequisites @@ -33,7 +33,7 @@ Two monitoring levels are available: GitHub, or GitLab. 3. An organization in PostgresAI Console. [Create one](https://console.postgres.ai/addorg) if you don't have one yet. -4. A payment method on file (required for the Scaling plan). In your organization, open **Billing**, +4. A payment method on file (required for the **Scale** plan). In your organization, open **Billing**, click **Edit payment methods**, and add a card in the Stripe portal. ## Step 1. Start the Supabase setup @@ -48,7 +48,7 @@ In the **Supabase monitoring** dialog, choose the monitoring level: - **Quick setup** (Free) — one-click OAuth connection with auto-discovery, daily checkups, and JSON reports. -- **Full monitoring** (Scaling) — dedicated monitoring infrastructure with Grafana dashboards, +- **Full monitoring** (Scale) — dedicated monitoring infrastructure with Grafana dashboards, real-time metrics, advanced alerts, and historical data. See [Pricing](/pricing) for the full list of available options with feature comparison. @@ -137,8 +137,8 @@ Open the Grafana URL from the deployment page. You can sign in using one of the [![Grafana login page with Sign in with PostgresAI button](/assets/supabase-monitoring/supabase-monitoring-7.png)](/assets/supabase-monitoring/supabase-monitoring-7.png) -Start with **01. Node overview** for a high-level health check of your Supabase database. Key panels -to check first: +Start with **01. Single node performance overview (high-level)** for a high-level health check of +your Supabase database. Key panels to check first: 1. **Active session history (ASH)** — wait events over time 2. **Sessions** — active, idle, and idle in transaction connections @@ -189,8 +189,8 @@ PostgresAI monitoring collects only database metadata — no actual data or quer review exactly what metrics are collected, examine the metric definitions: - **Prometheus sink metrics**: - [metrics.yml (pgwatch-prometheus)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.14.0/config/pgwatch-prometheus/metrics.yml) + [metrics.yml (pgwatch-prometheus)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-prometheus/metrics.yml) - **PostgreSQL sink metrics** (including normalized queries): - [metrics.yml (pgwatch-postgres)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.14.0/config/pgwatch-postgres/metrics.yml) + [metrics.yml (pgwatch-postgres)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-postgres/metrics.yml) See also: [data privacy details](/docs/monitoring/#data-privacy-metadata-only). diff --git a/docs/monitoring/getting-started/requirements.md b/docs/monitoring/getting-started/requirements.md index 64bfdcc2..a2780f06 100644 --- a/docs/monitoring/getting-started/requirements.md +++ b/docs/monitoring/getting-started/requirements.md @@ -12,7 +12,7 @@ sidebar_position: 2 | PostgreSQL version | Support status | |--------------------|----------------| -| 18 (beta) | Supported | +| 18 | Fully supported | | 17 | Fully supported | | 16 | Fully supported | | 15 | Fully supported | @@ -20,6 +20,13 @@ sidebar_position: 2 | 13 | Not recommended (EOL Nov 2025) | | 12 and earlier | Not supported | +:::note PostgreSQL 16+ for I/O statistics +The [I/O statistics dashboard (14)](/docs/monitoring/dashboards/io-statistics) and the +`pg_stat_io` metric group require **PostgreSQL 16 or newer** (`pg_stat_io` was introduced in +PG16). On PostgreSQL 15 and earlier, all other dashboards work but the I/O statistics dashboard +has no data. +::: + ### Required extensions #### pg_stat_statements (required) @@ -47,14 +54,26 @@ Changes to `shared_preload_libraries` require a PostgreSQL restart. The wait events dashboard uses `pg_stat_activity`, which is built into PostgreSQL. No additional extensions are required. ::: +## CLI runtime + +The `postgresai` CLI (used for `prepare-db`, `checkup`, `mon`, and MCP) requires: + +| Runtime | Version | +|---------|---------| +| Node.js | 18+ | +| Bun (alternative) | 1.0+ | + +Older Node.js versions fail fast in 0.15 with a clear error instead of breaking partway through +a command. + ## Monitoring stack requirements ### Local installation (Docker) | Component | Minimum | Recommended | |-----------|---------|-------------| -| CPU | 2 cores | 4 cores | -| RAM | 2 GiB | 4 GiB | +| CPU | 4 cores | 6 cores | +| RAM | 8 GiB | 12 GiB | | Disk | 10 GiB | 50 GiB | | Docker | 20.10+ | Latest | @@ -78,11 +97,17 @@ For self-managed deployments, you need connectivity between the monitoring node **Inbound ports** (on the monitoring node): -| Service | Default port | Notes | -|---------|--------------|-------| -| Grafana | 3000 | Recommended: protect behind a reverse proxy with TLS | -| VictoriaMetrics | 8428 | Internal use | -| Flask backend | 8000 | Internal use | +| Service | Host port | Notes | +|---------|-----------|-------| +| Grafana | 3000 | Published to the host. Recommended: protect behind a reverse proxy with TLS | +| VictoriaMetrics | 59090 | Published to the host (container listens on 9090); used for direct metric queries | +| Flask backend | — | Internal only — not published to the host (container port 8000) | + +:::note Helm uses different ports +The port numbers above apply to the Docker Compose stack. On Kubernetes, the VictoriaMetrics +service listens on port 8428 and Grafana on port 80 (cluster-internal); expose them via Ingress +or `kubectl port-forward`. +::: ## Cloud-specific requirements @@ -102,16 +127,20 @@ For self-managed deployments, you need connectivity between the monitoring node ### Metrics retention Default retention periods: -- **VictoriaMetrics**: 90 days -- **Prometheus**: 15 days (if using instead of VM) +- **VictoriaMetrics**: 14 days (`336h`), tunable via `VM_RETENTION_PERIOD` +- **Query-id → query-text mapping** (Flask backend): tunable via `QUERYID_RETENTION_HOURS`, independent of metrics retention +- **Prometheus**: not used by the default local-install stack + +See [Retention](/docs/monitoring/configuration/prometheus-config#retention) for tuning both +knobs together. ### Disk usage estimates -| Monitored databases | Daily growth | 90-day storage | +| Monitored databases | Daily growth | 14-day storage | |--------------------|--------------|----------------| -| 1 | ~50 MiB | ~4.5 GiB | -| 5 | ~200 MiB | ~18 GiB | -| 20 | ~800 MiB | ~72 GiB | +| 1 | ~50 MiB | ~700 MiB | +| 5 | ~200 MiB | ~2.8 GiB | +| 20 | ~800 MiB | ~11.2 GiB | :::tip Compression VictoriaMetrics typically achieves 10-15x compression on time-series data. @@ -124,10 +153,18 @@ VictoriaMetrics typically achieves 10-15x compression on time-series data. The `prepare-db` command creates a user with **read-only access to metadata only** — no actual data is ever accessed. ```sql --- Read-only access to system catalogs -grant pg_read_all_stats to postgres_ai_mon; +-- Standard monitoring privileges (the built-in pg_monitor role) +grant pg_monitor to postgres_ai_mon; +grant connect on database to postgres_ai_mon; +grant select on pg_catalog.pg_index to postgres_ai_mon; +-- plus a small postgres_ai schema (with a pg_statistic view for bloat analysis) +-- that prepare-db creates and grants usage/select on ``` +`prepare-db` grants the built-in `pg_monitor` role (not `pg_read_all_stats`, which is a strict +subset of `pg_monitor` and would not be sufficient — the install/verify step checks for +`pg_monitor` membership). + :::tip Review exact permissions To see the complete SQL used to create the monitoring role: ```bash diff --git a/docs/monitoring/getting-started/upgrade.md b/docs/monitoring/getting-started/upgrade.md new file mode 100644 index 00000000..f92f1c59 --- /dev/null +++ b/docs/monitoring/getting-started/upgrade.md @@ -0,0 +1,589 @@ +--- +title: Upgrading the monitoring stack +sidebar_label: Upgrading +sidebar_position: 7 +keywords: + - "PostgresAI monitoring upgrade" + - "mon update" + - "mon update-config" + - "VM_AUTH upgrade" + - "monitoring .env migration" +--- + +# Upgrading the monitoring stack + +This page covers upgrading an existing self-hosted monitoring stack to a newer PostgresAI +release (for example, from 0.14.x to 0.15.0). New installs should follow the +[CLI](/docs/monitoring/getting-started/installation-cli) or +[Docker Compose](/docs/monitoring/getting-started/installation-docker) guides instead. + +:::danger Breaking change in 0.15.0: bundled PostgreSQL 15 → 17 +0.15.0 upgrades the bundled PostgreSQL images from **15 to 17** for the stack's own +databases (`sink-postgres`, and on the demo `target-db` / `target-standby`). PostgreSQL's +on-disk format is **not** compatible across major versions, so the new images **refuse to +start** on a PostgreSQL 15 data directory (the entrypoint exits with a clear message instead +of crash-looping). **Your data is not deleted** — the guard refuses to touch it — but the +stack will not come up until you migrate the data. **Do the +[PostgreSQL 15 → 17 migration](#postgresql-15--17-major-version-migration) below *before* the +bring-up step** (`docker compose up -d`) in either upgrade path. This affects every +self-hosted deployment; your externally-monitored databases are **not** touched. +::: + +## PostgreSQL 15 → 17 major-version migration + +**Read this before bringing the stack up.** 0.15.0 bumps the stack's bundled PostgreSQL from +15 to 17. PostgreSQL stores data in a major-version-specific on-disk format, so PostgreSQL 17 +cannot read a PostgreSQL 15 data directory. To make this safe and obvious, the 0.15.0 entrypoint +checks the on-disk `PG_VERSION` against the image version and, on a mismatch, **refuses to start** +with an actionable message (exit code 3) instead of crash-looping with a buried +"database files are incompatible with server" error. + +**Your historical data is not lost** by the upgrade itself — the guard never writes to the old +data directory. You migrate it (or deliberately reset it) with the steps below. + +### Who is affected + +| Database (Docker container) | Role | Present in | Action | +|---|---|---|---| +| `sink-postgres` | pgwatch **measurements** DB — stores historical PostgreSQL metrics | **Every** self-hosted deploy | Migrate **or** reset (your choice — see below) | +| `target-db` | Bundled **sample** database being monitored | Demo only (`--demo`) | Migrate or reset (same procedure) | +| `target-standby` | Streaming **replica** of `target-db` | Demo only | **Do not migrate the replica** — re-clone it (see [Standbys](#standbys-target-standby)) | + +Your **own monitored databases are external and are *not* touched** by this upgrade — PostgresAI +connects to them over the network and never alters their on-disk format. This migration is only +about the stack's *own* bundled PostgreSQL containers. + +### Choose: preserve history, or reset + +For `sink-postgres` you have two options. Pick one before you start: + +- **(a) Preserve historical measurements (dump/restore).** Keeps all of your accumulated pgwatch + measurements across the upgrade. Use this if historical trends matter to you. Follow + [Option A](#option-a-preserve-history-dumprestore). +- **(b) Accept a reset of the measurements DB (simpler).** Discard the historical pgwatch + measurements and start PostgreSQL 17 with a **fresh, empty** `sink-postgres`. Live monitoring + resumes immediately and history re-accumulates from now on; only past measurements are lost. + Choose this if you do not need the history or want the fastest path. Follow + [Option B](#option-b-accept-a-reset-no-dump). + +Either way, **VictoriaMetrics metrics** (the `sink-prometheus` time-series, used by most Grafana +dashboards) live in a **separate** volume and are **not** affected by the PostgreSQL major-version +change. This choice only concerns the PostgreSQL-format `sink-postgres` measurements DB. + +:::danger Data-safety rules (apply to every option) +- **Never delete or overwrite the PostgreSQL 15 data volume until the PostgreSQL 17 restore is + verified.** Keep the old volume as your rollback. +- **Rename / keep the old volume**, do not reuse it in place: restore into a **fresh** volume so + the original PostgreSQL 15 data stays intact until you have confirmed the new one works. +- **Verify before cleanup:** check `pg_isready`, expected databases exist, and row counts / + table counts look sane on PostgreSQL 17, and that Grafana/app health is green — *then* remove + the old volume. +- Take the dump and run the migration **while the stack is stopped**, so nothing writes to the + measurements DB mid-migration. +::: + +### Find your volume names first + +The migration commands below operate on Docker **volumes** and **containers** by name. Container +names are stable (`sink-postgres`, `target-db`, `target-standby`). Volume names are +**prefixed with your Compose project name**, which is the **basename of your monitoring +directory** — for the default npx / global install (`~/.config/postgresai/monitoring`) the +prefix is `monitoring_`, giving `monitoring_sink_postgres_data`. **Do not assume the prefix** — +list your actual volumes and use those exact names: + +```bash +# From your monitoring directory. List the stack's PostgreSQL data volumes: +docker volume ls --format '{{.Name}}' | grep -E 'sink_postgres_data|target_db_data|target_standby_data' +# Example output (default install): +# monitoring_sink_postgres_data +# monitoring_target_db_data +# monitoring_target_standby_data +``` + +Throughout this section, substitute **``** with your actual `*_sink_postgres_data` +volume name (and `` / `` on the demo). + +### Stop the stack + +```bash +# From your monitoring directory +docker compose down # stops containers; named volumes are preserved +``` + +### Option A: preserve history (dump/restore) + +This runs the **previous PostgreSQL 15 image** against your existing data volume to take a logical +dump, then restores it into a **fresh PostgreSQL 17 volume**. The old volume is never modified. + +```bash +# 0. Substitute your real sink volume name (see "Find your volume names first"). +SINK_VOL= # e.g. monitoring_sink_postgres_data +DUMP_DIR="$(pwd)/pg15-migration" # dump lands on the host, outside any volume +mkdir -p "$DUMP_DIR" + +# 1. Dump the PG15 data using the OLD image, read-only against the EXISTING volume. +# pg_dumpall captures ALL databases + global roles (the pgwatch 'measurements' +# DB, the 'pgwatch' role, etc.). The volume is mounted but never written to. +docker run --rm \ + -v "${SINK_VOL}:/var/lib/postgresql/data:ro" \ + -v "${DUMP_DIR}:/dump" \ + -e POSTGRES_HOST_AUTH_METHOD=trust \ + --entrypoint bash \ + postgres:15 -c ' + set -e + # Start PG15 transiently from the existing data dir, dump, then stop. + chown -R postgres:postgres /var/lib/postgresql/data + su postgres -c "pg_ctl -D /var/lib/postgresql/data -w start" + su postgres -c "pg_dumpall -U postgres" > /dump/all.sql + su postgres -c "pg_ctl -D /var/lib/postgresql/data -w stop" + ' +# Sanity-check the dump is non-empty and contains your DB: +ls -lh "$DUMP_DIR/all.sql" +grep -c 'CREATE DATABASE' "$DUMP_DIR/all.sql" || true +``` + +:::note Read-only mount keeps PostgreSQL 15 intact +The old volume is mounted `:ro` here so the dump physically cannot modify your PostgreSQL 15 +data. If your platform rejects starting PostgreSQL on a read-only mount, drop `:ro` — but then +**do not** run any other PostgreSQL 15 step against it, and keep the volume untouched as your +rollback. +::: + +```bash +# 2. Create a FRESH PG17 volume (do NOT reuse the PG15 volume). +NEW_SINK_VOL="${SINK_VOL}-pg17" +docker volume create "$NEW_SINK_VOL" + +# 3. Initialize + restore into the fresh PG17 volume using the PG17 image. +docker run --rm \ + -v "${NEW_SINK_VOL}:/var/lib/postgresql/data" \ + -v "${DUMP_DIR}:/dump:ro" \ + -e POSTGRES_HOST_AUTH_METHOD=trust \ + --entrypoint bash \ + postgres:17 -c ' + set -e + su postgres -c "initdb -D /var/lib/postgresql/data" + su postgres -c "pg_ctl -D /var/lib/postgresql/data -w start" + su postgres -c "psql -U postgres -f /dump/all.sql" + su postgres -c "pg_ctl -D /var/lib/postgresql/data -w stop" + ' +``` + +```bash +# 4. Swap the stack onto the new volume WITHOUT destroying the old one. +# Rename the old PG15 volume aside (rollback), then give the new PG17 data +# the name the stack expects. Docker has no native rename, so re-create the +# target volume from the PG17 data via a copy. +docker volume create "${SINK_VOL}-pg15-backup" +# Copy PG15 data into the *-pg15-backup volume (preserves your rollback under a safe name)… +docker run --rm -v "${SINK_VOL}:/from:ro" -v "${SINK_VOL}-pg15-backup:/to" \ + alpine sh -c 'cp -a /from/. /to/' +# …then overwrite the canonical volume with the PG17 data: +docker run --rm -v "${NEW_SINK_VOL}:/from:ro" -v "${SINK_VOL}:/to" \ + alpine sh -c 'rm -rf /to/* /to/..?* /to/.[!.]* 2>/dev/null; cp -a /from/. /to/' +``` + +:::tip Simpler alternative to step 4 +If you would rather not copy volumes, you can instead tell Compose to use the new volume by name. +But the copy approach above keeps the canonical volume name the stack already references, which +avoids editing `docker-compose.yml`. Whichever you choose, the original PostgreSQL 15 data must +survive under a clearly-named backup volume (`*-pg15-backup`) until you have verified PostgreSQL 17. +::: + +Now continue to **[Bring the stack up and verify](#bring-up-and-verify)**. + +### Option B: accept a reset (no dump) + +This discards the historical pgwatch measurements and lets PostgreSQL 17 initialize a brand-new, +empty `sink-postgres`. Live monitoring resumes immediately; only past measurements are lost. +**Still keep the old volume as a backup** until you have confirmed the fresh stack is healthy — +do not delete it yet. + +```bash +SINK_VOL= # e.g. monitoring_sink_postgres_data + +# Preserve the PG15 data under a backup name (rollback), THEN clear the canonical +# volume so PG17 initializes fresh into it. +docker volume create "${SINK_VOL}-pg15-backup" +docker run --rm -v "${SINK_VOL}:/from:ro" -v "${SINK_VOL}-pg15-backup:/to" \ + alpine sh -c 'cp -a /from/. /to/' +docker run --rm -v "${SINK_VOL}:/to" \ + alpine sh -c 'rm -rf /to/* /to/..?* /to/.[!.]* 2>/dev/null || true' +``` + +On the next bring-up, `sink-postgres` (PostgreSQL 17) sees an empty data directory and runs a +fresh `initdb`; pgwatch recreates the `measurements` database and starts collecting again. +Continue to **[Bring the stack up and verify](#bring-up-and-verify)**. + +### Demo sample DB (`target-db`) + +On a `--demo` deployment, `target-db` is the bundled sample database and follows the **same +pattern** as `sink-postgres`. It usually holds only throwaway sample data, so most operators just +**reset** it (Option B applied to ``). If you want to keep its contents, apply +Option A to `` instead. Real users' monitored databases are external and need no +action. + +### Standbys (`target-standby`) + +**Do not migrate the replica.** A standby's data directory is a physical copy of its primary and +cannot be independently `pg_upgrade`d or dump/restored. Instead: + +1. Upgrade/initialize the **primary** (`target-db`) first, per the steps above. +2. **Re-clone the standby** from the upgraded primary via `pg_basebackup`. + +The bundled demo does this automatically: the `target-standby` service re-clones itself with +`pg_basebackup` whenever its data directory is empty. So the correct action is simply to **clear +the standby's volume** and let it rebuild on bring-up: + +```bash +STANDBY_VOL= # e.g. monitoring_target_standby_data +# Keep a backup name for safety, then clear so the demo re-clones from the new primary. +docker volume create "${STANDBY_VOL}-pg15-backup" +docker run --rm -v "${STANDBY_VOL}:/from:ro" -v "${STANDBY_VOL}-pg15-backup:/to" \ + alpine sh -c 'cp -a /from/. /to/' +docker run --rm -v "${STANDBY_VOL}:/to" \ + alpine sh -c 'rm -rf /to/* /to/..?* /to/.[!.]* 2>/dev/null || true' +``` + +On bring-up, `target-standby` waits for the upgraded `target-db` and re-streams a fresh +PostgreSQL 17 base backup. (No dump/restore is possible or needed for a replica.) + +### Bring up and verify + +After migrating/resetting the volumes above, proceed with the normal bring-up of your upgrade +path (continue to [Upgrade with the CLI](#upgrade-with-the-cli-recommended) or +[Upgrade with Docker Compose](#upgrade-with-docker-compose-manual)). Then verify: + +```bash +# Containers are up and PG17 is actually serving: +docker compose ps +docker exec sink-postgres postgres --version # expect: postgres (PostgreSQL) 17.x +docker exec sink-postgres pg_isready -U postgres # expect: accepting connections + +# (Option A only) confirm the measurements DB and its contents survived: +docker exec sink-postgres psql -U postgres -c '\l' # 'measurements' DB present +docker exec sink-postgres psql -U postgres -d measurements -c \ + "SELECT count(*) FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog','information_schema');" +# Compare this table count (and, for key tables, row counts) against the PG15 dump +# before you trust the migration. Spot-check a metrics table row count if you can. +``` + +Then open Grafana and confirm dashboards render data, and run the stack health check: + +```bash +npx postgresai@latest mon health +``` + +### Post-migration: restore query-text collection on the fresh `sink-postgres` + +After a dump/restore migration ([Option A](#option-a-preserve-history-dumprestore)) — or any +path that lands the `measurements` database on a **freshly initialized PostgreSQL 17** data +directory — query-text collection breaks **silently** even though metrics keep flowing. The new +data directory is bootstrapped from scratch, so two things the stack normally configures only on +**first init** are missing. This affects **all dump/restore upgraders**. Apply both fixes below. + +**Symptom (how to recognize it).** Metrics graphs still draw, but **query texts vanish**: + +- Dashboard 02 — the **"Query text" column is blank**. +- Dashboard 03 (per-query) — **"No data"**. +- Graph legends show the **raw label JSON** (e.g. `{queryid="…", …}`) instead of the actual + query text. + +There are **two independent root causes**, both on the fresh `sink-postgres`. Fix both. + +#### Cause 1 — pg_hba.conf does not allow the monitoring services over the Docker IPv6 network + +A fresh PG17 data directory ships a default `pg_hba.conf` that does **not** include the +Docker **IPv6 ULA** network the monitoring stack uses. The Flask backend +(`monitoring_flask_backend`) and `pgwatch-postgres` resolve `sink-postgres` to its IPv6 +address first (per RFC 6724) and are rejected, so **0 query texts** are written. You'll see +this in the `sink-postgres` / Flask logs: + +```text +FATAL: no pg_hba.conf entry for host "", user "...", database "measurements", no encryption +``` + +**Discover the real IPv6 range** for your deployment (do **not** copy a value from this page — +ranges differ per host). Either read the rejected host straight from the logs: + +```bash +docker logs sink-postgres 2>&1 | grep "no pg_hba.conf entry" +docker logs monitoring_flask_backend 2>&1 | grep -i "pg_hba\|no encryption" +``` + +…or inspect the monitoring Docker network's IPv6 subnet directly: + +```bash +# Replace with your compose network (see: docker network ls) +docker network inspect \ + --format '{{range .IPAM.Config}}{{.Subnet}} {{end}}' +``` + +Add a matching IPv6 ULA entry to the fresh `sink-postgres` `pg_hba.conf` and reload (no +restart needed). Use the **range you discovered** above: + +```bash +# Use the IPv6 ULA subnet from the step above (example shape only — substitute yours): +IPV6_ULA='fdXX:XXXX:XXXX::/48' + +docker exec -i sink-postgres bash -lc \ + "echo \"host all all ${IPV6_ULA} trust\" >> \"\$PGDATA/pg_hba.conf\"" + +# Reload so the new rule takes effect without a restart +docker exec -i sink-postgres psql -U postgres -d measurements -c "select pg_reload_conf()" +``` + +#### Cause 2 — `init.sql` schema bootstrap was not re-applied (dedup function missing) + +The fresh `measurements` database lacks the partition-safe dedup function/trigger and +supporting schema that `config/sink-postgres/init.sql` installs (it only runs automatically on +the very first init of a volume). Without it, the Flask backend refuses to export query texts +and logs: + +```text +WARNING: Sink dedup function public.enforce_queryid_uniqueness ... missing ... + Re-run config/sink-postgres/init.sql as the bootstrap role. +... +Exported 0 active queryids for metrics +``` + +Re-run the schema bootstrap as the **bootstrap/superuser role** (`postgres`). The script is +idempotent (`create … if not exists` / `create or replace`), so it is safe to re-apply: + +```bash +# From the monitoring project directory (where config/sink-postgres/init.sql lives) +docker exec -i sink-postgres psql -U postgres -d measurements < config/sink-postgres/init.sql +``` + +#### Restart the collectors and verify + +After **both** fixes, restart the two services that write query texts: + +```bash +docker compose restart pgwatch-postgres monitoring_flask_backend +``` + +Confirm the Flask backend now exports query texts (allow **one query-info scrape cycle, +~5 minutes**): + +```bash +# Expect a non-zero count, not "Exported 0 active queryids": +docker logs --since 6m monitoring_flask_backend 2>&1 | grep "active queryids for metrics" +``` + +Then reload Grafana: Dashboard 02's **"Query text"** column populates, Dashboard 03 shows +data, and graph legends render real query text instead of raw label JSON. + +### Rollback + +If anything looks wrong, you have **not** lost the PostgreSQL 15 data — it is in the +`*-pg15-backup` volume(s): + +```bash +docker compose down +SINK_VOL= +# Restore the PG15 data back into the canonical volume… +docker run --rm -v "${SINK_VOL}-pg15-backup:/from:ro" -v "${SINK_VOL}:/to" \ + alpine sh -c 'rm -rf /to/* /to/..?* /to/.[!.]* 2>/dev/null; cp -a /from/. /to/' +# …then temporarily pin the affected service back to postgres:15 (edit docker-compose.yml) +# and bring the stack up on the old image while you investigate. +``` + +### Clean up (only after verification) + +Once PostgreSQL 17 is verified healthy and you no longer need the rollback, reclaim space: + +```bash +docker volume rm "${SINK_VOL}-pg15-backup" +docker volume rm "${SINK_VOL}-pg17" 2>/dev/null || true # the scratch restore volume (Option A) +rm -rf ./pg15-migration # the host-side dump (Option A) +# (demo) docker volume rm "${TARGET_DB_VOL}-pg15-backup" "${STANDBY_VOL}-pg15-backup" +``` + +:::tip An automated `pg-upgrade` command is planned +A built-in command to automate this major-version migration (in-place `pg_upgrade`) is in +progress (draft MR +[!145](https://gitlab.com/postgres-ai/postgresai/-/merge_requests/145)). It is **not** part of +0.15.0 — on 0.15.0 use the manual dump/restore procedure above. +::: + +## Upgrade with the CLI (recommended) + +If you installed with `postgresai mon local-install`, upgrade with the CLI. `mon update` pulls the +new images and migrates your `.env` file; `mon update-config` regenerates the pgwatch sources. +Neither command restarts or recreates the running services, so after pulling you **must** recreate +the containers with the new images — preserving your existing values. Because the stack is already +running, recreate it with `docker compose up -d` directly: `up -d` recreates any container whose +image changed. A plain `postgresai mon restart` only runs `docker compose restart` and restarts the +**existing** containers on the **old** image, so it does not apply a pulled image — and a bare +`postgresai mon start` is a **no-op** on a running stack (it sees the running containers, prints +`Monitoring services are already running`, and exits without running `docker compose up -d`). If you +prefer the CLI, run `mon stop` first so the next `mon start` sees no running containers and actually +performs `up -d`. + +```bash +# From the monitoring directory (~/.config/postgresai/monitoring by default for npx/global installs) +$EDITOR .env # set PGAI_TAG=0.15.0 FIRST — see note below +npx postgresai@latest mon update # migrate .env + pull new images (does NOT restart) +npx postgresai@latest mon update-config # regenerate pgwatch sources.yml +# ⚠️ 0.15.0 only: complete the PostgreSQL 15 → 17 migration BEFORE this bring-up. +# See "PostgreSQL 15 → 17 major-version migration" above. +docker compose up -d # recreate containers to apply the pulled images +# (CLI alternative: `npx postgresai@latest mon stop && npx postgresai@latest mon start`) +``` + +> **Set `PGAI_TAG=0.15.0` in `.env` first.** All stack images are pinned to `${PGAI_TAG}`, and +> `mon update` / `mon update-config` do **not** change `PGAI_TAG` (only `mon local-install` rewrites +> it). If you leave a stale `PGAI_TAG=0.14.x`, the commands above just re-pull and recreate the +> **old** images — not an upgrade. Edit `.env` and set `PGAI_TAG=0.15.0` before running `mon update`. + +> `mon update` prints a hint to run `postgres-ai mon restart` afterward, but `docker compose restart` +> restarts containers in place and does **not** pull in a newly-fetched image. A bare `mon start` +> also will not help on a running stack — it short-circuits with `Monitoring services are already +> running`. Recreate the running stack with `docker compose up -d` (or `mon stop` then `mon start`), +> which recreates the containers on the new image. + +| Command | What it does | +|---------|--------------| +| `$EDITOR .env` → `PGAI_TAG=0.15.0` | **Do this first.** Pins the stack images to the new tag. `mon update` / `mon update-config` do **not** change `PGAI_TAG` (only `mon local-install` rewrites it), and every image is pinned to `${PGAI_TAG}` — so without this step the commands below just re-pull the **old** tag. | +| `mon update` | Migrates `.env` (additively) and pulls the pinned images for the tag in `.env` (set `PGAI_TAG=0.15.0` first — `mon update` does **not** advance it). It does **not** restart or recreate the services — run `docker compose up -d` afterward to recreate the containers and apply the new images. (A bare `postgresai mon start` will not do this on a running stack; it no-ops with `Monitoring services are already running`. Use `docker compose up -d`, or `mon stop` then `mon start`.) | +| `mon update-config` | Migrates `.env` and regenerates the pgwatch `sources.yml` files (via the `sources-generator`). It does not regenerate the Grafana datasources, does not restart the collectors, and does not reseed the config volume. | + +### Additive, value-preserving `.env` migration (`mon update` / `mon update-config`) + +`mon update` and `mon update-config` migrate `.env` **additively**: new keys required by the +release are appended with safe defaults, and every existing value (passwords, retention, resource +limits, OAuth, `GF_SERVER_ROOT_URL`, …) is preserved. These two commands never overwrite a value +you have already set, so they are the recommended way to upgrade an existing, tuned deployment. + +:::warning `local-install` rewrites `.env` — it does not migrate additively +`postgresai mon local-install -y` does **not** preserve arbitrary keys. It rewrites `.env` from +scratch, carrying forward only your **credentials and registry** — `PGAI_REGISTRY`, +`GF_SECURITY_ADMIN_PASSWORD`, `REPLICATOR_PASSWORD`, `VM_AUTH_USERNAME`, `VM_AUTH_PASSWORD` — and +it always resets `PGAI_TAG` to the CLI's own version. Any other key you had set is **dropped**, +including retention (`VM_RETENTION_PERIOD`, `QUERYID_RETENTION_HOURS`), resource-limit overrides +(`*_CPUS` / `*_MEM`, e.g. `SINK_PROMETHEUS_MEM`), and `GF_SERVER_ROOT_URL` / `BIND_HOST`. To +upgrade a tuned deployment, prefer `mon update` / `mon update-config` above; if you do run +`local-install`, re-apply those settings to `.env` afterward and run `docker compose up -d` so the +affected containers are recreated with the restored values (`mon restart` would not pick up changed +container env vars — those are read only when a container is recreated — and a bare `postgresai mon +start` is a no-op on a running stack, so it would not recreate them either; use `docker compose up -d`, +or `mon stop` then `mon start`). +::: + +:::tip Node.js 18+ required +0.15 requires Node.js 18+ (or Bun 1.0+). Older Node versions now fail early with a clear error. +See [System requirements](/docs/monitoring/getting-started/requirements#cli-runtime). +::: + +### Bundled `docker-compose.yml` refresh for non-git (npx) installs + +If you installed via `npx postgresai@latest` or a global npm install, your project directory is **not** +a git checkout, so `git pull` cannot bring in the new compose file. `docker-compose.yml` is a +version-coupled asset — for example, 0.15 wires `VM_AUTH_*` into the VictoriaMetrics service and +the Grafana datasource — and a stale compose would leave that wiring missing and blank all +dashboards. + +To handle this, `mon local-install -y`, `mon update`, and `mon update-config` automatically +refresh the bundled `docker-compose.yml` for non-git installs when it is stale relative to the +target version. The refresh: + +- Is a **no-op for git checkouts** (those upgrade via `git pull`) and a no-op when the deployed + compose already matches the target. +- **Backs up** the previous compose before overwriting it, to a uniquely named file + `docker-compose.yml.bak--` (the backup is never clobbered on repeated runs, so + your original compose is always preserved). +- **Validates** the fetched compose before replacing anything. If it cannot retrieve a valid + compose (for example, no network), it keeps the existing file, writes no backup, warns, and + the upgrade still proceeds. +- Touches **only** `docker-compose.yml` — never `.env`, `instances.yml`, or `.pgwatch-config`. + +When it refreshes, the CLI prints a confirmation such as +`✓ Refreshed docker-compose.yml to 0.15.0 (backup: docker-compose.yml.bak-0.14.0-)`. + +## Required new keys in 0.15: VictoriaMetrics basic auth + +0.15 protects the VictoriaMetrics endpoint with HTTP basic auth, so two keys are now required: + +```bash +VM_AUTH_USERNAME=vmauth +VM_AUTH_PASSWORD= +``` + +The CLI generates and preserves these automatically during `local-install`, `update`, and +`update-config`. `VM_AUTH_USERNAME` defaults to `vmauth` when absent, and a random +`VM_AUTH_PASSWORD` is generated when missing. + +:::warning Manual Docker Compose users: add these before upgrading +If you run `docker compose` directly instead of through the CLI, you **must** add +`VM_AUTH_USERNAME=vmauth` and a non-empty `VM_AUTH_PASSWORD` to `.env` before running +`docker compose up -d`. Grafana datasource provisioning depends on these credentials; without +them, Grafana cannot query VictoriaMetrics and dashboards show no data. The shipped +`.env.example` includes empty placeholders that intentionally make Docker Compose fail fast +until you set a value. + +```bash +# Generate a password +VM_AUTH_PASSWORD="$(openssl rand -base64 18)" +``` +::: + +See [Authentication and security](/docs/monitoring/configuration/prometheus-config#authentication-and-security) +for what these credentials protect and how to rotate them. + +## Upgrade with Docker Compose (manual) + +If you manage the stack with `docker compose` directly: + +```bash +# 1. Pull the latest repository state (new compose / config templates) +git pull + +# 2. Pin the new image tag and add any newly required keys +# Required in 0.15: +# PGAI_TAG=0.15.0 +# VM_AUTH_USERNAME=vmauth +# VM_AUTH_PASSWORD= +$EDITOR .env + +# 3. Pull images and restart +docker compose pull +# ⚠️ 0.15.0 only: complete the PostgreSQL 15 → 17 migration BEFORE this bring-up. +# See "PostgreSQL 15 → 17 major-version migration" above. +docker compose up -d +``` + +All stack images are version-pinned (no `:latest`) for reproducible upgrades — see +[Image tags](/docs/monitoring/getting-started/installation-docker#image-tags-and-supply-chain). + +## Other 0.15 upgrade notes + +- **Retention is now plan-parameterizable.** `VM_RETENTION_PERIOD` (metrics) and + `QUERYID_RETENTION_HOURS` (query-id mapping) can be tuned per deployment. `mon update` / + `mon update-config` preserve any values you have set; `mon local-install -y` does **not** (it + rewrites `.env` and drops these keys — see the warning above), so re-apply them afterward if you + upgrade via `local-install`. See + [Retention](/docs/monitoring/configuration/prometheus-config#retention). +- **Restart policies.** Critical services ship with `restart: unless-stopped` and survive host + reboots without a manual systemd unit. See + [Reliability and restart behavior](/docs/monitoring/getting-started/installation-docker#reliability-and-restart-behavior). +- **Idempotent config seeding.** Generated config is seeded once and guarded by a version + marker, so operator edits persist across restarts; `mon update-config` regenerates the pgwatch + `sources.yml` files after a version bump. + +## Verify the upgrade + +```bash +npx postgresai@latest mon health +npx postgresai@latest mon status +``` + +Then open Grafana and confirm dashboards render data. If panels are empty after the upgrade, +check that `VM_AUTH_USERNAME` / `VM_AUTH_PASSWORD` are set and that you recreated the stack with +`docker compose up -d` so the containers are rebuilt on the pulled images and re-read the updated +VM_AUTH credentials. (A plain `postgresai mon restart` restarts the existing containers in place and +would pick up neither the new image nor changed env values; a bare `postgresai mon start` no-ops on +an already-running stack and does not recreate anything — recreate via `docker compose up -d`, or +`mon stop` then `mon start`.) The +Grafana datasource is static (provisioned from `config/grafana` into the config volume) and only +re-reads those credentials when Grafana restarts — it is not regenerated by `mon update-config`. diff --git a/docs/monitoring/index.md b/docs/monitoring/index.md index f52b0715..3bda5ba9 100644 --- a/docs/monitoring/index.md +++ b/docs/monitoring/index.md @@ -19,11 +19,34 @@ Expert-level Postgres monitoring tool designed for humans and AI systems Built for senior DBAs, SREs, and AI systems who need rapid root cause analysis and deep performance insights. This isn't a tool for beginners — it's designed for Postgres experts who need to understand complex performance issues in minutes, not hours. -Part of [Self-Driving Postgres](/blog/20250725-self-driving-postgres) - PostgresAI monitoring is a foundational component of PostgresAI's open-source Self-Driving Postgres (SDP) initiative, providing the advanced monitoring and intelligent root cause analysis capabilities essential for achieving higher levels of database automation. +Part of [Self-Driving Postgres](/blog/20250725-self-driving-postgres) — PostgresAI monitoring is a foundational component of PostgresAI's open-source Self-Driving Postgres (SDP) initiative, providing the advanced monitoring and intelligent root cause analysis capabilities essential for achieving higher levels of database automation. ## Live demo Experience the full monitoring solution: https://demo.postgres.ai (login: demo / password: demo) +## Supported Postgres versions + +PostgresAI full monitoring, express-mode checkups, and PostgresAI Console +checkup analysis support PostgreSQL 14 through PostgreSQL 19. + +PostgreSQL 19 is currently a pre-release (Beta 2). Use it for compatibility +testing rather than production workloads until PostgreSQL 19 reaches general +availability. PostgresAI preserves the beta version label in checkup reports, +selects PostgreSQL 19-compatible metric SQL, and does not report a PostgreSQL +19 beta server as being behind the latest stable major release. + +On PostgreSQL 19, the full monitoring collector uses the native +[`pg_get_multixact_stats()`](https://www.postgresql.org/docs/19/functions-info.html) +function instead of scanning `pg_multixact` files. The monitoring role needs +`pg_read_all_stats` privileges (included by `pg_monitor`) for the function to +return values; otherwise the metric is reported as unavailable without +interrupting other collection. + +PostgresAI Console's separate PostgreSQL cluster-provisioning workflow depends +on the PostgreSQL versions supported by its automation provider. Monitoring and +checkup compatibility does not make a pre-release version available for +provisioning automatically. + ## Console.Postgres.ai integration @@ -87,8 +110,8 @@ PostgresAI monitoring collects **only database metadata** — no actual data or Review exactly what metrics are collected by examining the metric definitions: -- **Prometheus sink metrics**: [metrics.yml (pgwatch-prometheus)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.14.0/config/pgwatch-prometheus/metrics.yml) -- **PostgreSQL sink metrics** (including normalized queries): [metrics.yml (pgwatch-postgres)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.14.0/config/pgwatch-postgres/metrics.yml) +- **Prometheus sink metrics**: [metrics.yml (pgwatch-prometheus)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-prometheus/metrics.yml) +- **PostgreSQL sink metrics** (including normalized queries): [metrics.yml (pgwatch-postgres)](https://gitlab.com/postgres-ai/postgresai/-/blob/0.15.0/config/pgwatch-postgres/metrics.yml) ### Verify database permissions @@ -106,6 +129,6 @@ The easiest way to set up PostgresAI monitoring is through [Console.Postgres.ai] 1. Navigate to **Checkup → Monitoring instances** in the left menu 2. Click **Choose plan** -3. Select **Starter** or **Scale** plan +3. Select the **Scale** plan (or contact us about **Enterprise**) -See [pricing](/pricing) for plan details and features. \ No newline at end of file +See [pricing](/pricing) for plan details and features. diff --git a/docs/monitoring/metrics/index-metrics.md b/docs/monitoring/metrics/index-metrics.md index bae18c4f..49a9185e 100644 --- a/docs/monitoring/metrics/index-metrics.md +++ b/docs/monitoring/metrics/index-metrics.md @@ -6,16 +6,18 @@ sidebar_position: 5 # Index metrics -Index usage and health metrics from `pg_stat_user_indexes` and related views. +Index usage and health metrics. In this stack the usage metric reads **`pg_stat_all_indexes`** +(not `pg_stat_user_indexes`) and the I/O metric reads **`pg_statio_all_indexes`**. Series are +exported with the `pgwatch_` prefix. ## Data sources -| View | Description | -|------|-------------| -| `pg_stat_user_indexes` | Index usage statistics | -| `pg_statio_user_indexes` | Index I/O statistics | -| `pg_indexes` | Index definitions | -| `pg_class` | Index sizes | +| Metric group | Underlying view | Description | +|--------------|-----------------|-------------| +| `pg_stat_all_indexes` | `pg_stat_all_indexes` | Index usage (`pgwatch_pg_stat_all_indexes_*`) | +| `pg_statio_all_indexes` | `pg_statio_all_indexes` | Index I/O (`pgwatch_pg_statio_all_indexes_*`) | +| `pg_class` | `pg_class` | Index sizes (`pgwatch_pg_class_relation_size_bytes`) | +| `pg_btree_bloat` | – | B-tree bloat estimates (`pgwatch_pg_btree_bloat_*`) | ## Core metrics @@ -23,32 +25,35 @@ Index usage and health metrics from `pg_stat_user_indexes` and related views. | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_user_indexes_idx_scan_total` | Counter | Index scans initiated | -| `pg_stat_user_indexes_idx_tup_read_total` | Counter | Index entries read | -| `pg_stat_user_indexes_idx_tup_fetch_total` | Counter | Table rows fetched via index | +| `pgwatch_pg_stat_all_indexes_idx_scan` | Gauge | Index scans initiated | +| `pgwatch_pg_stat_all_indexes_idx_tup_read` | Gauge | Index entries read | +| `pgwatch_pg_stat_all_indexes_idx_tup_fetch` | Gauge | Table rows fetched via index | ### Size metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_index_size_bytes` | Gauge | Index size in bytes | +| `pgwatch_pg_class_relation_size_bytes` | Gauge | Relation size in bytes (used for index size) | +| `pgwatch_pg_btree_bloat_bloat_pct` | Gauge | Estimated B-tree index bloat percentage | ### I/O metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_statio_user_indexes_idx_blks_read_total` | Counter | Index blocks read from disk | -| `pg_statio_user_indexes_idx_blks_hit_total` | Counter | Index blocks hit in buffer cache | +| `pgwatch_pg_statio_all_indexes_idx_blks_read` | Gauge | Index blocks read from disk | +| `pgwatch_pg_statio_all_indexes_idx_blks_hit` | Gauge | Index blocks hit in buffer cache | ## Labels +The cluster label is `cluster` (not `cluster_name`). The index identity labels are correct: + | Label | Description | Example | |-------|-------------|---------| | `indexrelname` | Index name | `users_email_idx` | | `relname` | Parent table name | `users` | | `schemaname` | Schema name | `public` | | `datname` | Database name | `myapp` | -| `cluster_name` | Cluster identifier | `production` | +| `cluster` | Cluster identifier (from `custom_tags.cluster`) | `production` | | `node_name` | Node identifier | `primary` | ## Common queries @@ -56,31 +61,31 @@ Index usage and health metrics from `pg_stat_user_indexes` and related views. ### Unused indexes ```promql -pg_stat_user_indexes_idx_scan_total == 0 +pgwatch_pg_stat_all_indexes_idx_scan == 0 ``` ### Index scan rate ```promql -rate(pg_stat_user_indexes_idx_scan_total[5m]) +rate(pgwatch_pg_stat_all_indexes_idx_scan[5m]) ``` ### Index buffer hit ratio ```promql -rate(pg_statio_user_indexes_idx_blks_hit_total[5m]) +rate(pgwatch_pg_statio_all_indexes_idx_blks_hit[5m]) / ( - rate(pg_statio_user_indexes_idx_blks_hit_total[5m]) + rate(pgwatch_pg_statio_all_indexes_idx_blks_hit[5m]) + - rate(pg_statio_user_indexes_idx_blks_read_total[5m]) + rate(pgwatch_pg_statio_all_indexes_idx_blks_read[5m]) ) ``` ### Largest indexes ```promql -topk(10, pg_index_size_bytes) +topk(10, pgwatch_pg_class_relation_size_bytes) ``` ### Index read efficiency @@ -88,15 +93,15 @@ topk(10, pg_index_size_bytes) Ratio of tuples fetched vs tuples read from index: ```promql -rate(pg_stat_user_indexes_idx_tup_fetch_total[5m]) +rate(pgwatch_pg_stat_all_indexes_idx_tup_fetch[5m]) / -rate(pg_stat_user_indexes_idx_tup_read_total[5m]) +rate(pgwatch_pg_stat_all_indexes_idx_tup_read[5m]) ``` ### Most active indexes ```promql -topk(10, rate(pg_stat_user_indexes_idx_scan_total[5m])) +topk(10, rate(pgwatch_pg_stat_all_indexes_idx_scan[5m])) ``` ## Dashboard usage diff --git a/docs/monitoring/metrics/index.md b/docs/monitoring/metrics/index.md index fbeb8069..f995e4ad 100644 --- a/docs/monitoring/metrics/index.md +++ b/docs/monitoring/metrics/index.md @@ -12,37 +12,46 @@ Reference documentation for all metrics collected by PostgresAI monitoring. PostgresAI monitoring collects metrics from multiple PostgreSQL sources: -| Source | Description | Dashboard usage | -|--------|-------------|-----------------| -| `pg_stat_statements` | Query-level performance metrics | 02, 03 | -| `pg_stat_activity` | Session and wait event data | 01, 04 | -| `pg_stat_user_tables` | Table-level statistics | 07, 08, 09 | -| `pg_stat_user_indexes` | Index usage statistics | 10, 11 | -| `pg_stat_replication` | Replication metrics | 06 | -| `pg_stat_bgwriter` | Background writer stats | 01 | -| `pg_stat_database` | Database-level aggregates | 01 | +| pgwatch metric group | Underlying view(s) | Dashboard usage | +|----------------------|--------------------|-----------------| +| `pg_stat_statements` | `pg_stat_statements` | 02, 03 | +| `pg_stat_activity` / `wait_events` | `pg_stat_activity` | 01, 04 | +| `table_stats` / `pg_stat_all_tables` | `pg_stat_all_tables` | 07, 08, 09 | +| `pg_stat_all_indexes` / `pg_statio_all_indexes` | `pg_stat_all_indexes`, `pg_statio_all_indexes` | 10, 11 | +| `pg_stat_replication` / `replication` | `pg_stat_replication` | 06 | +| `bgwriter` | `pg_stat_bgwriter` | 01 | +| `db_stats` | `pg_stat_database` | 01 | ## Metric naming convention -All metrics follow the pattern: +pgwatch exports each series as: ``` -pg_{source}_{metric_name} +pgwatch_{metric-group}_{column} ``` -Examples: -- `pg_stat_statements_calls_total` — Total query calls -- `pg_stat_activity_count` — Active session count -- `pg_stat_user_tables_seq_scan_total` — Sequential scans +Examples (these are the names you query in VictoriaMetrics/Grafana): +- `pgwatch_pg_stat_statements_calls` — query calls (no `_total` suffix) +- `pgwatch_pg_stat_activity_count` — session count +- `pgwatch_pg_stat_all_tables_seq_tup_read` — tuples read by sequential scans +- `pgwatch_db_stats_xact_commit` — transactions committed + +There is no `pg_{source}_{metric_name}` naming in this stack; all series carry the `pgwatch_` +prefix and use the column name (not a `_total`/`_seconds` suffix convention). ## Collection intervals -| Metric type | Default interval | Configurable | -|-------------|------------------|--------------| -| Session metrics | 10s | Yes | -| Query metrics | 60s | Yes | -| Table/index metrics | 60s | Yes | -| Replication metrics | 10s | Yes | +Intervals are set per metric group in `config/pgwatch-prometheus/metrics.yml` (the `full` preset). +Representative defaults: + +| Metric group | Default interval | +|--------------|------------------| +| `pg_stat_activity`, `wait_events` | 15s | +| `pg_stat_statements` | 30s | +| `table_stats`, `pg_stat_all_tables`, `pg_stat_all_indexes` | 30s | +| `db_stats`, `bgwriter`, `replication` | 30s | +| `settings` | 300s | +| Bloat groups (`pg_table_bloat`, `pg_btree_bloat`) | 7200s | ## Metric categories @@ -93,20 +102,23 @@ All metrics are stored in VictoriaMetrics (Prometheus-compatible). Example queri ```promql # Queries per second -rate(pg_stat_statements_calls_total[5m]) +rate(pgwatch_pg_stat_statements_calls[5m]) # Transactions per second -rate(pg_stat_database_xact_commit_total[5m]) +rate(pgwatch_db_stats_xact_commit[5m]) ``` ### Aggregations ```promql -# Total active sessions across all databases -sum(pg_stat_activity_count{state="active"}) - -# Average query time by database -avg by (datname) (pg_stat_statements_mean_exec_time_seconds) +# Active sessions across all databases (the activity metric carries a `state` label) +sum(pgwatch_pg_stat_activity_count{state="active"}) + +# Average exec time per call by database. There is no mean_exec_time series; +# derive it from the cumulative exec_time_total (ms) and calls counters. +sum by (datname) (rate(pgwatch_pg_stat_statements_exec_time_total[5m])) +/ +sum by (datname) (rate(pgwatch_pg_stat_statements_calls[5m])) ``` ## Related documentation diff --git a/docs/monitoring/metrics/pg-stat-statements.md b/docs/monitoring/metrics/pg-stat-statements.md index 5b3b1d46..e1a675b8 100644 --- a/docs/monitoring/metrics/pg-stat-statements.md +++ b/docs/monitoring/metrics/pg-stat-statements.md @@ -30,39 +30,40 @@ pg_stat_statements.max = 10000 ## Core metrics +All series are exported as `pgwatch_pg_stat_statements_`. Times are in **milliseconds** +(not seconds), buffer usage is reported in **bytes** (not blocks), and counters do **not** carry a +`_total` suffix unless the column name itself ends in `_total`. There are no `mean_*` series. + ### Execution metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_statements_calls_total` | Counter | Total number of query executions | -| `pg_stat_statements_total_exec_time_seconds` | Counter | Total execution time | -| `pg_stat_statements_mean_exec_time_seconds` | Gauge | Average execution time per call | -| `pg_stat_statements_rows_total` | Counter | Total rows returned or affected | +| `pgwatch_pg_stat_statements_calls` | Gauge | Number of query executions | +| `pgwatch_pg_stat_statements_exec_time_total` | Gauge | Total execution time (ms) | +| `pgwatch_pg_stat_statements_rows` | Gauge | Rows returned or affected | ### Planning metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_statements_total_plan_time_seconds` | Counter | Total planning time | -| `pg_stat_statements_mean_plan_time_seconds` | Gauge | Average planning time per call | +| `pgwatch_pg_stat_statements_plans_total` | Gauge | Number of times the statement was planned | +| `pgwatch_pg_stat_statements_plan_time_total` | Gauge | Total planning time (ms) | -### Buffer metrics +### Buffer metrics (bytes) | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_statements_shared_blks_hit_total` | Counter | Shared buffer hits | -| `pg_stat_statements_shared_blks_read_total` | Counter | Shared blocks read from disk | -| `pg_stat_statements_shared_blks_written_total` | Counter | Shared blocks written | -| `pg_stat_statements_shared_blks_dirtied_total` | Counter | Shared blocks dirtied | - -### I/O timing metrics +| `pgwatch_pg_stat_statements_shared_bytes_hit_total` | Gauge | Shared buffer hits (bytes) | +| `pgwatch_pg_stat_statements_shared_bytes_read_total` | Gauge | Shared bytes read from disk | +| `pgwatch_pg_stat_statements_shared_bytes_written_total` | Gauge | Shared bytes written | +| `pgwatch_pg_stat_statements_shared_bytes_dirtied_total` | Gauge | Shared bytes dirtied | -Available when `track_io_timing = on`: +### Block I/O timing metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_statements_blk_read_time_seconds` | Counter | Time spent reading blocks | -| `pg_stat_statements_blk_write_time_seconds` | Counter | Time spent writing blocks | +| `pgwatch_pg_stat_statements_block_read_total` | Gauge | Block read time (ms) | +| `pgwatch_pg_stat_statements_block_write_total` | Gauge | Block write time (ms) | ### WAL metrics @@ -70,21 +71,32 @@ PostgreSQL 13+: | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_statements_wal_records_total` | Counter | WAL records generated | -| `pg_stat_statements_wal_bytes_total` | Counter | WAL bytes generated | +| `pgwatch_pg_stat_statements_wal_records` | Gauge | WAL records generated | +| `pgwatch_pg_stat_statements_wal_fpi` | Gauge | WAL full-page images | +| `pgwatch_pg_stat_statements_wal_bytes` | Gauge | WAL bytes generated | + +### Temp I/O metrics + +| Metric | Type | Description | +|--------|------|-------------| +| `pgwatch_pg_stat_statements_temp_bytes_read` | Gauge | Temp bytes read | +| `pgwatch_pg_stat_statements_temp_bytes_written` | Gauge | Temp bytes written | ## Labels -All pg_stat_statements metrics include these labels: +The `pg_stat_statements` metric is grouped only by database and query id, so it carries these +labels (plus the instance labels): | Label | Description | Example | |-------|-------------|---------| | `queryid` | Unique query identifier | `-4021163671685...` | | `datname` | Database name | `myapp` | -| `usename` | User name | `app_user` | -| `cluster_name` | Cluster identifier | `production` | +| `cluster` | Cluster identifier (from `custom_tags.cluster`) | `production` | | `node_name` | Node identifier | `primary` | +There is no `usename` label on `pg_stat_statements` metrics, and the cluster label is `cluster` +(not `cluster_name`). + ## Common queries ### Top queries by total time @@ -92,7 +104,7 @@ All pg_stat_statements metrics include these labels: ```promql topk(10, sum by (queryid, datname) ( - rate(pg_stat_statements_total_exec_time_seconds[5m]) + rate(pgwatch_pg_stat_statements_exec_time_total[5m]) ) ) ``` @@ -101,34 +113,38 @@ topk(10, ```promql sum by (queryid) ( - rate(pg_stat_statements_calls_total[5m]) + rate(pgwatch_pg_stat_statements_calls[5m]) ) ``` -### Average query latency +### Average query latency (ms per call) + +There is no mean series; derive it from the cumulative `exec_time_total` (ms) and `calls`: ```promql -pg_stat_statements_mean_exec_time_seconds +sum by (queryid) (rate(pgwatch_pg_stat_statements_exec_time_total[5m])) +/ +sum by (queryid) (rate(pgwatch_pg_stat_statements_calls[5m])) ``` -### Buffer hit ratio per query +### Buffer hit ratio per query (bytes) ```promql -sum by (queryid) (rate(pg_stat_statements_shared_blks_hit_total[5m])) +sum by (queryid) (rate(pgwatch_pg_stat_statements_shared_bytes_hit_total[5m])) / ( - sum by (queryid) (rate(pg_stat_statements_shared_blks_hit_total[5m])) + sum by (queryid) (rate(pgwatch_pg_stat_statements_shared_bytes_hit_total[5m])) + - sum by (queryid) (rate(pg_stat_statements_shared_blks_read_total[5m])) + sum by (queryid) (rate(pgwatch_pg_stat_statements_shared_bytes_read_total[5m])) ) ``` ### Queries with high planning time ratio ```promql -pg_stat_statements_mean_plan_time_seconds +rate(pgwatch_pg_stat_statements_plan_time_total[5m]) / -(pg_stat_statements_mean_plan_time_seconds + pg_stat_statements_mean_exec_time_seconds) +(rate(pgwatch_pg_stat_statements_plan_time_total[5m]) + rate(pgwatch_pg_stat_statements_exec_time_total[5m])) > 0.1 ``` @@ -153,9 +169,10 @@ These metrics are used in: show pg_stat_statements.track; ``` -3. Ensure monitoring user has access: +3. Ensure the monitoring user has access (the product grants the built-in `pg_monitor` role, not + `pg_read_all_stats`): ```sql - grant pg_read_all_stats to postgres_ai_mon; + grant pg_monitor to postgres_ai_mon; ``` ### queryid changes after PostgreSQL upgrade diff --git a/docs/monitoring/metrics/system-metrics.md b/docs/monitoring/metrics/system-metrics.md index a2772f92..b85cc357 100644 --- a/docs/monitoring/metrics/system-metrics.md +++ b/docs/monitoring/metrics/system-metrics.md @@ -6,17 +6,19 @@ sidebar_position: 6 # Database- and cluster-level metrics -PostgreSQL system-level metrics from various `pg_stat_*` views. +PostgreSQL system-level metrics. In this stack they are exported by the `db_stats`, `bgwriter`, +`archive_lag` / `pg_archiver`, `pg_stat_replication`, and `settings` metric groups, with the +`pgwatch_` prefix and the source column name (no `_total`/`_seconds` suffix convention). ## Data sources -| View | Description | -|------|-------------| -| `pg_stat_database` | Database-level aggregates | -| `pg_stat_bgwriter` | Background writer statistics | -| `pg_stat_archiver` | WAL archiver status | -| `pg_stat_replication` | Replication status | -| `pg_settings` | Configuration parameters | +| Metric group | Underlying view | Description | +|--------------|-----------------|-------------| +| `db_stats` | `pg_stat_database` | Database-level aggregates (`pgwatch_db_stats_*`) | +| `bgwriter` | `pg_stat_bgwriter` | Background writer statistics (`pgwatch_bgwriter_*`) | +| `archive_lag` / `pg_archiver` | `pg_stat_archiver` | WAL archiver status | +| `pg_stat_replication` | `pg_stat_replication` | Replication status | +| `settings` | `pg_settings` | Configuration parameters (`pgwatch_settings_*`) | ## Database metrics @@ -24,70 +26,96 @@ PostgreSQL system-level metrics from various `pg_stat_*` views. | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_database_xact_commit_total` | Counter | Transactions committed | -| `pg_stat_database_xact_rollback_total` | Counter | Transactions rolled back | -| `pg_stat_database_deadlocks_total` | Counter | Deadlocks detected | -| `pg_stat_database_conflicts_total` | Counter | Recovery conflicts (replicas) | +| `pgwatch_db_stats_xact_commit` | Gauge | Transactions committed | +| `pgwatch_db_stats_xact_rollback` | Gauge | Transactions rolled back | +| `pgwatch_db_stats_deadlocks` | Gauge | Deadlocks detected | +| `pgwatch_db_stats_conflicts` | Gauge | Recovery conflicts (replicas) | ### Connection metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_database_numbackends` | Gauge | Active connections | -| `pg_settings_max_connections` | Gauge | Maximum allowed connections | +| `pgwatch_db_stats_numbackends` | Gauge | Active connections | +| `pgwatch_settings_numeric_value{setting_name="max_connections"}` | Gauge | Maximum allowed connections (from the `settings` metric; there is no `pgwatch_settings_max_connections` series) | ### Buffer metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_database_blks_read_total` | Counter | Blocks read from disk | -| `pg_stat_database_blks_hit_total` | Counter | Blocks found in buffer cache | +| `pgwatch_db_stats_blks_read` | Gauge | Blocks read from disk | +| `pgwatch_db_stats_blks_hit` | Gauge | Blocks found in buffer cache | ### Temporary file metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_database_temp_files_total` | Counter | Temporary files created | -| `pg_stat_database_temp_bytes_total` | Counter | Temporary file bytes written | +| `pgwatch_db_stats_temp_files` | Gauge | Temporary files created | +| `pgwatch_db_stats_temp_bytes` | Gauge | Temporary file bytes written | ## Background writer metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_bgwriter_checkpoints_timed_total` | Counter | Scheduled checkpoints | -| `pg_stat_bgwriter_checkpoints_req_total` | Counter | Requested checkpoints | -| `pg_stat_bgwriter_checkpoint_write_time_seconds_total` | Counter | Checkpoint write time | -| `pg_stat_bgwriter_checkpoint_sync_time_seconds_total` | Counter | Checkpoint sync time | -| `pg_stat_bgwriter_buffers_checkpoint_total` | Counter | Buffers written during checkpoints | -| `pg_stat_bgwriter_buffers_clean_total` | Counter | Buffers written by background writer | -| `pg_stat_bgwriter_buffers_backend_total` | Counter | Buffers written by backends | -| `pg_stat_bgwriter_buffers_alloc_total` | Counter | Buffers allocated | +| `pgwatch_bgwriter_checkpoints_timed` | Counter | Scheduled checkpoints | +| `pgwatch_bgwriter_checkpoints_req` | Counter | Requested checkpoints | +| `pgwatch_bgwriter_checkpoint_write_time` | Counter | Checkpoint write time (ms) | +| `pgwatch_bgwriter_checkpoint_sync_time` | Counter | Checkpoint sync time (ms) | +| `pgwatch_bgwriter_buffers_checkpoint` | Counter | Buffers written during checkpoints | +| `pgwatch_bgwriter_buffers_clean` | Counter | Buffers written by background writer | +| `pgwatch_bgwriter_buffers_backend` | Counter | Buffers written by backends | +| `pgwatch_bgwriter_buffers_alloc` | Counter | Buffers allocated | ## WAL archiver metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_archiver_archived_count_total` | Counter | WAL files archived | -| `pg_stat_archiver_failed_count_total` | Counter | Failed archive attempts | -| `pg_stat_archiver_last_archived_time` | Gauge | Last successful archive timestamp | +| `pgwatch_archive_lag_archived_count` | Gauge | WAL files archived | +| `pgwatch_archive_lag_failed_count` | Gauge | Failed archive attempts | +| `pgwatch_archive_lag_seconds_since_archive` | Gauge | Seconds since last successful archive | +| `pgwatch_archive_lag_wal_files_behind` | Gauge | WAL files behind the latest segment | + +## WAL directory size + +New in 0.15, the collector reports the total size of the `pg_wal` directory using +`pg_ls_waldir()`. This is the on-disk WAL size, which complements the archiver and replication +metrics for diagnosing disk-fill risk. + +| Metric | Type | Description | +|--------|------|-------------| +| `pgwatch_pg_wal_size_bytes` | Gauge | Total size of regular files in `pg_wal` (excludes subdirectories like `pg_wal/archive_status`) | +| `pgwatch_pg_wal_size_status_code` | Gauge | Collection status: `0` = success, `1` = `pg_ls_waldir()` unavailable, `2` = monitoring role lacks EXECUTE privilege | + +`pg_wal` growth that is **not** matched by archive or replica progress is a disk-fill warning — +typically a stuck WAL archiver, an inactive replication slot retaining WAL, or sustained high +WAL generation. Correlate `pgwatch_pg_wal_size_bytes` with the `pgwatch_archive_lag_*` and +replication-slot metrics, and see +[How to troubleshoot a growing pg_wal directory](/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-a-growing-pg-wal-directory). + +```promql +# Alert when pg_wal exceeds, e.g., 20 GiB while archiving is failing +pgwatch_pg_wal_size_bytes > 20 * 1024 * 1024 * 1024 +``` ## Replication metrics +LSN positions come from the `pg_stat_replication` group; lag is reported in `replication_*_lag_ms` +/ `_lag_b` fields. There is no `pg_replication_lag_seconds` series. + | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_replication_sent_lsn` | Gauge | WAL sent to replica | -| `pg_stat_replication_write_lsn` | Gauge | WAL written on replica | -| `pg_stat_replication_flush_lsn` | Gauge | WAL flushed on replica | -| `pg_stat_replication_replay_lsn` | Gauge | WAL replayed on replica | -| `pg_replication_lag_bytes` | Gauge | Replication lag in bytes | -| `pg_replication_lag_seconds` | Gauge | Estimated replication lag | +| `pgwatch_pg_stat_replication_sent_lsn` | Gauge | WAL sent to replica | +| `pgwatch_pg_stat_replication_write_lsn` | Gauge | WAL written on replica | +| `pgwatch_pg_stat_replication_flush_lsn` | Gauge | WAL flushed on replica | +| `pgwatch_pg_stat_replication_replay_lsn` | Gauge | WAL replayed on replica | ## Labels +The cluster label is `cluster` (not `cluster_name`). + | Label | Description | Example | |-------|-------------|---------| | `datname` | Database name | `myapp` | -| `cluster_name` | Cluster identifier | `production` | +| `cluster` | Cluster identifier (from `custom_tags.cluster`) | `production` | | `node_name` | Node identifier | `primary` | ## Common queries @@ -95,39 +123,39 @@ PostgreSQL system-level metrics from various `pg_stat_*` views. ### Transactions per second ```promql -sum(rate(pg_stat_database_xact_commit_total[5m])) +sum(rate(pgwatch_db_stats_xact_commit[5m])) ``` ### Rollback ratio ```promql -rate(pg_stat_database_xact_rollback_total[5m]) +rate(pgwatch_db_stats_xact_rollback[5m]) / -(rate(pg_stat_database_xact_commit_total[5m]) + rate(pg_stat_database_xact_rollback_total[5m])) +(rate(pgwatch_db_stats_xact_commit[5m]) + rate(pgwatch_db_stats_xact_rollback[5m])) ``` ### Buffer cache hit ratio ```promql -sum(rate(pg_stat_database_blks_hit_total[5m])) +sum(rate(pgwatch_db_stats_blks_hit[5m])) / -(sum(rate(pg_stat_database_blks_hit_total[5m])) + sum(rate(pg_stat_database_blks_read_total[5m]))) +(sum(rate(pgwatch_db_stats_blks_hit[5m])) + sum(rate(pgwatch_db_stats_blks_read[5m]))) ``` ### Connection utilization ```promql -sum(pg_stat_database_numbackends) +sum(pgwatch_db_stats_numbackends) / -pg_settings_max_connections +scalar(max(pgwatch_settings_numeric_value{setting_name="max_connections"})) ``` ### Checkpoint frequency ```promql -rate(pg_stat_bgwriter_checkpoints_timed_total[5m]) +rate(pgwatch_bgwriter_checkpoints_timed[5m]) + -rate(pg_stat_bgwriter_checkpoints_req_total[5m]) +rate(pgwatch_bgwriter_checkpoints_req[5m]) ``` ### Backend buffer writes (problematic) @@ -135,27 +163,21 @@ rate(pg_stat_bgwriter_checkpoints_req_total[5m]) High values indicate checkpoint tuning needed: ```promql -rate(pg_stat_bgwriter_buffers_backend_total[5m]) +rate(pgwatch_bgwriter_buffers_backend[5m]) / ( - rate(pg_stat_bgwriter_buffers_checkpoint_total[5m]) + rate(pgwatch_bgwriter_buffers_checkpoint[5m]) + - rate(pg_stat_bgwriter_buffers_clean_total[5m]) + rate(pgwatch_bgwriter_buffers_clean[5m]) + - rate(pg_stat_bgwriter_buffers_backend_total[5m]) + rate(pgwatch_bgwriter_buffers_backend[5m]) ) ``` -### Replication lag - -```promql -pg_replication_lag_seconds -``` - ### WAL archive status ```promql -time() - pg_stat_archiver_last_archived_time +pgwatch_archive_lag_seconds_since_archive ``` ## Dashboard usage diff --git a/docs/monitoring/metrics/table-metrics.md b/docs/monitoring/metrics/table-metrics.md index 6dbba74a..24824819 100644 --- a/docs/monitoring/metrics/table-metrics.md +++ b/docs/monitoring/metrics/table-metrics.md @@ -6,80 +6,87 @@ sidebar_position: 4 # Table metrics -Table-level statistics from `pg_stat_user_tables` and related views. +Table-level statistics. In this stack they come from the `table_stats` metric group, whose query +reads **`pg_stat_all_tables`** (not `pg_stat_user_tables`). Series are exported as +`pgwatch_table_stats_`. ## Data sources -| View | Description | -|------|-------------| -| `pg_stat_user_tables` | Table access statistics | -| `pg_statio_user_tables` | Table I/O statistics | -| `pg_class` | Table sizes and properties | +| Metric group | Underlying view | Description | +|--------------|-----------------|-------------| +| `table_stats` | `pg_stat_all_tables` | Table access, modification, tuple, vacuum, and size stats | +| `pg_class` | `pg_class` | Table sizes (`pgwatch_pg_class_*`) | +| `db_size` | – | Database size (`pgwatch_db_size_size_b`) | ## Core metrics +All series are `pgwatch_table_stats_`. There are no `_total` suffixes and no +`pg_statio_user_tables_*` / `pg_table_size_bytes` series. + ### Access metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_user_tables_seq_scan_total` | Counter | Sequential scans initiated | -| `pg_stat_user_tables_seq_tup_read_total` | Counter | Rows fetched by sequential scans | -| `pg_stat_user_tables_idx_scan_total` | Counter | Index scans initiated | -| `pg_stat_user_tables_idx_tup_fetch_total` | Counter | Rows fetched by index scans | +| `pgwatch_table_stats_seq_scan` | Counter | Sequential scans initiated | +| `pgwatch_table_stats_seq_tup_read` | Counter | Rows fetched by sequential scans | +| `pgwatch_table_stats_idx_scan` | Counter | Index scans initiated | +| `pgwatch_table_stats_idx_tup_fetch` | Counter | Rows fetched by index scans | ### Modification metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_user_tables_n_tup_ins_total` | Counter | Rows inserted | -| `pg_stat_user_tables_n_tup_upd_total` | Counter | Rows updated | -| `pg_stat_user_tables_n_tup_del_total` | Counter | Rows deleted | -| `pg_stat_user_tables_n_tup_hot_upd_total` | Counter | HOT updates (heap-only tuple) | +| `pgwatch_table_stats_n_tup_ins` | Counter | Rows inserted | +| `pgwatch_table_stats_n_tup_upd` | Counter | Rows updated | +| `pgwatch_table_stats_n_tup_del` | Counter | Rows deleted | +| `pgwatch_table_stats_n_tup_hot_upd` | Counter | HOT updates (heap-only tuple) | ### Tuple metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_user_tables_n_live_tup` | Gauge | Estimated live rows | -| `pg_stat_user_tables_n_dead_tup` | Gauge | Estimated dead rows | -| `pg_stat_user_tables_n_mod_since_analyze` | Gauge | Rows modified since last analyze | +| `pgwatch_table_stats_n_live_tup` | Gauge | Estimated live rows | +| `pgwatch_table_stats_n_dead_tup` | Gauge | Estimated dead rows | ### Vacuum metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_user_tables_last_vacuum` | Gauge | Timestamp of last manual vacuum | -| `pg_stat_user_tables_last_autovacuum` | Gauge | Timestamp of last autovacuum | -| `pg_stat_user_tables_last_analyze` | Gauge | Timestamp of last manual analyze | -| `pg_stat_user_tables_last_autoanalyze` | Gauge | Timestamp of last autoanalyze | -| `pg_stat_user_tables_vacuum_count_total` | Counter | Manual vacuum count | -| `pg_stat_user_tables_autovacuum_count_total` | Counter | Autovacuum count | +| `pgwatch_table_stats_seconds_since_last_vacuum` | Gauge | Seconds since last (auto)vacuum | +| `pgwatch_table_stats_seconds_since_last_analyze` | Gauge | Seconds since last (auto)analyze | +| `pgwatch_table_stats_vacuum_count` | Counter | Manual vacuum count | +| `pgwatch_table_stats_autovacuum_count` | Counter | Autovacuum count | +| `pgwatch_table_stats_analyze_count` | Counter | Manual analyze count | +| `pgwatch_table_stats_autoanalyze_count` | Counter | Autoanalyze count | ### Size metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_table_size_bytes` | Gauge | Table size (excluding indexes) | -| `pg_total_relation_size_bytes` | Gauge | Total size (table + indexes + toast) | -| `pg_indexes_size_bytes` | Gauge | Total index size for table | +| `pgwatch_table_stats_table_size_b` | Gauge | Table size in bytes (excluding indexes) | +| `pgwatch_table_stats_total_relation_size_b` | Gauge | Total size (table + indexes + toast) | +| `pgwatch_table_stats_toast_size_b` | Gauge | TOAST size in bytes | -### I/O metrics +### Freeze-age metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_statio_user_tables_heap_blks_read_total` | Counter | Heap blocks read from disk | -| `pg_statio_user_tables_heap_blks_hit_total` | Counter | Heap blocks hit in buffer cache | -| `pg_statio_user_tables_idx_blks_read_total` | Counter | Index blocks read from disk | -| `pg_statio_user_tables_idx_blks_hit_total` | Counter | Index blocks hit in buffer cache | +| `pgwatch_table_stats_tx_freeze_age` | Counter | Transaction-id freeze age | +| `pgwatch_table_stats_mxid_freeze_age` | Counter | Multixact-id freeze age | ## Labels +The `table_stats` metric is grouped by schema and table, so it carries these labels (plus the +instance labels). Note the names are `schema` / `table_name` / `table_full_name` (not +`schemaname` / `relname`), and the cluster label is `cluster` (not `cluster_name`). + | Label | Description | Example | |-------|-------------|---------| -| `relname` | Table name | `users` | -| `schemaname` | Schema name | `public` | +| `table_name` | Table name | `users` | +| `table_full_name` | Schema-qualified table name | `public.users` | +| `schema` | Schema name | `public` | | `datname` | Database name | `myapp` | -| `cluster_name` | Cluster identifier | `production` | +| `cluster` | Cluster identifier (from `custom_tags.cluster`) | `production` | | `node_name` | Node identifier | `primary` | ## Common queries @@ -87,65 +94,53 @@ Table-level statistics from `pg_stat_user_tables` and related views. ### Sequential scan ratio ```promql -rate(pg_stat_user_tables_seq_scan_total[5m]) +rate(pgwatch_table_stats_seq_scan[5m]) / ( - rate(pg_stat_user_tables_seq_scan_total[5m]) + rate(pgwatch_table_stats_seq_scan[5m]) + - rate(pg_stat_user_tables_idx_scan_total[5m]) + rate(pgwatch_table_stats_idx_scan[5m]) ) ``` ### Tables with high dead tuple ratio ```promql -pg_stat_user_tables_n_dead_tup +pgwatch_table_stats_n_dead_tup / -(pg_stat_user_tables_n_live_tup + pg_stat_user_tables_n_dead_tup) +(pgwatch_table_stats_n_live_tup + pgwatch_table_stats_n_dead_tup) > 0.1 ``` ### HOT update ratio ```promql -rate(pg_stat_user_tables_n_tup_hot_upd_total[5m]) +rate(pgwatch_table_stats_n_tup_hot_upd[5m]) / -rate(pg_stat_user_tables_n_tup_upd_total[5m]) +rate(pgwatch_table_stats_n_tup_upd[5m]) ``` ### Tables not vacuumed recently ```promql -time() - pg_stat_user_tables_last_autovacuum > 86400 -``` - -### Buffer hit ratio per table - -```promql -rate(pg_statio_user_tables_heap_blks_hit_total[5m]) -/ -( - rate(pg_statio_user_tables_heap_blks_hit_total[5m]) - + - rate(pg_statio_user_tables_heap_blks_read_total[5m]) -) +pgwatch_table_stats_seconds_since_last_vacuum > 86400 ``` ### Largest tables ```promql -topk(10, pg_total_relation_size_bytes) +topk(10, pgwatch_table_stats_total_relation_size_b) ``` ### Write-heavy tables ```promql topk(10, - rate(pg_stat_user_tables_n_tup_ins_total[5m]) + rate(pgwatch_table_stats_n_tup_ins[5m]) + - rate(pg_stat_user_tables_n_tup_upd_total[5m]) + rate(pgwatch_table_stats_n_tup_upd[5m]) + - rate(pg_stat_user_tables_n_tup_del_total[5m]) + rate(pgwatch_table_stats_n_tup_del[5m]) ) ``` diff --git a/docs/monitoring/metrics/wait-events.md b/docs/monitoring/metrics/wait-events.md index 41158717..38b80d83 100644 --- a/docs/monitoring/metrics/wait-events.md +++ b/docs/monitoring/metrics/wait-events.md @@ -25,47 +25,53 @@ where backend_type = 'client backend'; ## Core metrics +Series are exported with the `pgwatch_` prefix. Wait-event sampling counts come from the +`wait_events` metric group (column `total`), and session state/duration come from the +`pg_stat_activity` group. + ### Session state metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_stat_activity_count` | Gauge | Sessions by state | -| `pg_stat_activity_max_tx_duration_seconds` | Gauge | Longest running transaction | -| `pg_stat_activity_oldest_query_seconds` | Gauge | Oldest active query duration | +| `pgwatch_pg_stat_activity_count` | Gauge | Sessions, grouped by state | +| `pgwatch_pg_stat_activity_max_tx_duration` | Gauge | Longest running transaction (seconds) | ### Wait event metrics | Metric | Type | Description | |--------|------|-------------| -| `pg_wait_event_count` | Gauge | Sessions waiting by event type | -| `pg_wait_event_activity_count` | Gauge | Sessions in activity wait states | +| `pgwatch_wait_events_total` | Gauge | Sampled count of backends per wait event / type | + +There is no `pg_wait_event_count` or `pg_wait_event_activity_count` series. ## Labels -### Session state labels +### Session state labels (`pgwatch_pg_stat_activity_count`) | Label | Values | Description | |-------|--------|-------------| -| `state` | `active`, `idle`, `idle in transaction`, `idle in transaction (aborted)`, `fastpath function call`, `disabled` | Session state | +| `state` | `active`, `idle`, `idle in transaction`, `idle in transaction (aborted)`, ... | Session state | | `datname` | Database name | Target database | -| `usename` | User name | Connected user | +| `application_name` | Application name | Reported `application_name` | -### Wait event labels +### Wait event labels (`pgwatch_wait_events_total`) | Label | Values | Description | |-------|--------|-------------| -| `wait_event_type` | `CPU`, `IO`, `Lock`, `LWLock`, `BufferPin`, `Activity`, `Extension`, `Client`, `IPC`, `Timeout` | Wait category | +| `wait_event_type` | `LWLock`, `Lock`, `BufferPin`, `Activity`, `Client`, `Extension`, `IPC`, `Timeout`, `IO`, plus the synthesized `CPU*` placeholder (see below) | Wait category | | `wait_event` | Various | Specific wait event | +| `datname` | Database name | Target database | +| `query_id` | queryid (PG14+) | Associated query id, when available | ## Wait event categories -### CPU +### CPU* (synthesized placeholder) -On-CPU processing, no actual wait: - -| Event | Description | -|-------|-------------| -| `CPU` | Query execution on CPU | +`CPU` is **not** a real PostgreSQL `wait_event_type`. When a backend is running on CPU, its +`wait_event` and `wait_event_type` are `NULL`. To make on-CPU activity visible, the `wait_events` +metric coalesces those NULLs into a `CPU*` placeholder value — so you will see `CPU*` (not `CPU`) +in the `wait_event` / `wait_event_type` labels. The categories below are the standard PostgreSQL +wait-event types reported as-is. ### IO @@ -129,39 +135,39 @@ Background process waits: ### Sessions by state ```promql -sum by (state) (pg_stat_activity_count) +sum by (state) (pgwatch_pg_stat_activity_count) ``` ### Wait events distribution ```promql -sum by (wait_event_type) (pg_wait_event_count) +sum by (wait_event_type) (pgwatch_wait_events_total) ``` ### Active (non-idle) sessions ```promql -sum(pg_stat_activity_count{state!~"idle.*"}) +sum(pgwatch_pg_stat_activity_count{state!~"idle.*"}) ``` ### Sessions waiting on locks ```promql -sum(pg_wait_event_count{wait_event_type="Lock"}) +sum(pgwatch_wait_events_total{wait_event_type="Lock"}) ``` ### I/O wait ratio ```promql -sum(pg_wait_event_count{wait_event_type="IO"}) +sum(pgwatch_wait_events_total{wait_event_type="IO"}) / -sum(pg_stat_activity_count{state="active"}) +sum(pgwatch_pg_stat_activity_count{state="active"}) ``` ### Long-running transactions ```promql -pg_stat_activity_max_tx_duration_seconds > 300 +pgwatch_pg_stat_activity_max_tx_duration > 300 ``` ## Dashboard usage @@ -172,18 +178,64 @@ These metrics are used in: - [04. Wait events](/docs/monitoring/dashboards/wait-events) — Wait event deep-dive - [13. Lock contention](/docs/monitoring/dashboards/lock-contention) — Lock analysis +## Average Active Sessions (AAS) health matrix + +The PostgresAI console rolls this wait-event data up into its **DB health matrix** as Average Active +Sessions (AAS) checks: an overall check plus one per wait-event category (IO, IPC, Lock, LWLock). +Each check pairs a sustained **avg** value with a **peak** value. The peak is derived from +`pgwatch_wait_events_total` — the worst single time bucket, plus a p99 across buckets — and +normalized to the instance's vCPU count, so a cell reads the same regardless of machine size. + +### Peak granularity: 1-minute buckets + +The peak metrics are computed over **1-minute** buckets: + +- The zone is driven by the **worst 1-minute** bucket (previously the worst 5-minute bucket). +- The displayed p99 label is the **p99 of 1-minute** buckets (previously p99 of 5-minute buckets). + +A 1-minute maximum catches short bursts that 5-minute averaging smooths away, surfacing +sub-5-minute CPU-queuing spikes that the coarser bucket hid. The **avg (sustained) checks are +unaffected** — only the peak metrics change. + +### Thresholds are absolute and resolution-invariant + +The peak zone cutoffs are unchanged, and intentionally so: + +| Peak check | Green | Yellow | Red | +|------------|-------|--------|-----| +| Overall (`AASp`) | < 50% of vCPUs | 50–80% | > 80% | +| Per type (`AAS_IOp`, `AAS_IPCp`, `AAS_LOCKp`, `AAS_LWLOCKp`) | < 20% of vCPUs | 20–50% | > 50% | + +These cutoffs are **absolute** and deliberately not raised for the finer granularity. The worst +minute exceeding 80% of vCPUs means real CPU queuing regardless of bucket size, so the threshold is +resolution-invariant. Raising the cutoffs to compensate for 1-minute granularity would either +under-alert spiky workloads or re-smooth the very sub-5-minute bursts that 1-minute buckets exist to +surface. + +### The switchover is a calibration boundary + +Because a 1-minute maximum catches bursts that 5-minute averaging smooths, worst/peak values +computed over 1-minute buckets run **greater than or equal to** the old 5-minute values for the same +workload. At the switchover: + +- Peak cells may shift **redder** even though the underlying workload has not changed. +- Peak history from before the switch is **not directly comparable** to peak history after it. + +Treat the switchover as a **calibration boundary**: compare peaks within a single granularity +regime, not across it. The avg/sustained values are continuous across the boundary. + ## Troubleshooting ### No wait event data 1. Verify pgwatch is collecting from pg_stat_activity: ```bash - docker compose logs pgwatch | grep -i "stat_activity" + docker compose logs pgwatch-postgres pgwatch-prometheus | grep -i "stat_activity" ``` -2. Check monitoring user permissions: +2. Check monitoring user permissions (the product grants `pg_monitor`, not `pg_read_all_stats`): ```sql - grant pg_read_all_stats to postgres_ai_mon; + grant pg_monitor to postgres_ai_mon; ``` ### Wait events show "unknown" diff --git a/docs/monitoring/troubleshooting/index.md b/docs/monitoring/troubleshooting/index.md index 44c42de8..eeac0403 100644 --- a/docs/monitoring/troubleshooting/index.md +++ b/docs/monitoring/troubleshooting/index.md @@ -16,24 +16,35 @@ Guides for diagnosing and resolving common PostgresAI monitoring issues. # Docker Compose docker compose ps -# Expected output -# NAME STATUS -# pgwatch Up -# victoriametrics Up -# grafana Up +# Expected services (names) +# NAME STATUS +# pgwatch-postgres Up +# pgwatch-prometheus Up +# sink-postgres Up +# sink-prometheus Up (VictoriaMetrics) +# grafana Up (container grafana-with-datasources) ``` ### Verify metrics flow ```bash -# 1. Check pgwatch is collecting -curl http://localhost:8080/metrics | grep pg_stat - -# 2. Check VictoriaMetrics is receiving -curl 'http://localhost:8428/api/v1/query?query=up' - -# 3. Check Grafana data source -curl http://monitor:YOUR_PASSWORD@localhost:3000/api/datasources/proxy/1/api/v1/query?query=up +# 1. Check pgwatch metrics are exposed (internal only; the prometheus sink +# serves them at pgwatch-prometheus:9091/pgwatch — no host port 8080). +# sink-prometheus enables VM basic auth when VM_AUTH_USERNAME/PASSWORD are set, +# so /api/v1/query needs credentials (otherwise it returns 401). +# sink-prometheus is the VictoriaMetrics image: its wget is BusyBox wget, which +# has NO --user/--password flags — pass the credentials in the URL userinfo instead. +docker compose exec sink-prometheus wget -qO- \ + "http://$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD@localhost:9090/api/v1/query?query=pgwatch_pg_stat_activity_count" + +# 2. Check VictoriaMetrics is receiving (host port 59090; VM basic auth) +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=up' + +# 3. Check the Grafana data source proxy (admin: monitor / demo; the proxy path +# takes the datasource UID, not its name — PGWatch-Prometheus has uid +# P7A0D6631BB10B34F; Grafana adds the VM basic auth under the hood) +curl 'http://monitor:demo@localhost:3000/api/datasources/proxy/uid/P7A0D6631BB10B34F/api/v1/query?query=up' ``` ## Common issues @@ -50,13 +61,13 @@ curl http://monitor:YOUR_PASSWORD@localhost:3000/api/datasources/proxy/1/api/v1/ ### pgwatch logs ```bash -docker compose logs pgwatch --tail 100 +docker compose logs pgwatch-postgres pgwatch-prometheus --tail 100 ``` ### VictoriaMetrics logs ```bash -docker compose logs victoriametrics --tail 100 +docker compose logs sink-prometheus --tail 100 ``` ### Grafana logs @@ -67,23 +78,29 @@ docker compose logs grafana --tail 100 ### PostgreSQL connectivity +The `pgwatch-postgres` image is a minimal Alpine build that ships only the pgwatch binary — it has no +`psql`. Run the connectivity check from a container that does have a client, such as `sink-postgres` +(image `postgres:17`): + ```bash -docker compose exec pgwatch psql -h target-host -U monitoring_user -c "select 1" +docker compose exec sink-postgres psql -h target-host -U monitoring_user -c "select 1" ``` ## Health check endpoints +Only Grafana (port `3000`) and VictoriaMetrics (host `59090`) are reachable from the host. pgwatch's +internal web address (`:8080` on `pgwatch-postgres`) is not published to the host. + | Component | Endpoint | Expected | |-----------|----------|----------| -| pgwatch | `http://localhost:8080/health` | `{"status": "ok"}` | -| VictoriaMetrics | `http://localhost:8428/health` | `OK` | | Grafana | `http://localhost:3000/api/health` | `{"database": "ok"}` | +| VictoriaMetrics | `http://localhost:59090/health` | `OK` | ## Getting help 1. Check logs for error messages 2. Review the specific troubleshooting guide -3. Search [GitHub Issues](https://github.com/postgres-ai/postgresai/issues) +3. Search [GitLab Issues](https://gitlab.com/postgres-ai/postgresai/-/issues) 4. Open a new issue with diagnostic output ## Sections diff --git a/docs/monitoring/troubleshooting/no-data.md b/docs/monitoring/troubleshooting/no-data.md index b7e780db..84c7ec66 100644 --- a/docs/monitoring/troubleshooting/no-data.md +++ b/docs/monitoring/troubleshooting/no-data.md @@ -22,21 +22,24 @@ Diagnosing and fixing "No data" issues in PostgresAI dashboards. ### 1. Check pgwatch status ```bash -docker compose ps pgwatch +docker compose ps pgwatch-postgres pgwatch-prometheus ``` Expected: `Up` status If not running: ```bash -docker compose logs pgwatch --tail 50 +docker compose logs pgwatch-postgres pgwatch-prometheus --tail 50 ``` ### 2. Verify PostgreSQL connectivity +The `pgwatch-postgres` container has no `psql` (it is a minimal Alpine image with only the pgwatch +binary). Run the check from `sink-postgres` (image `postgres:17`), which has a client: + ```bash # Use PGPASSWORD environment variable for authentication -docker compose exec -e PGPASSWORD="$PGPASSWORD" pgwatch psql \ +docker compose exec -e PGPASSWORD="$PGPASSWORD" sink-postgres psql \ "postgresql://user@host:5432/dbname" \ -c "select 1" ``` @@ -74,22 +77,21 @@ Adding to shared_preload_libraries requires PostgreSQL restart. ### 4. Verify metrics collection -```bash -# Check pgwatch is scraping -curl http://localhost:8080/metrics | head -20 -``` - -Expected: Prometheus-format metrics +The pgwatch-prometheus sink serves metrics at `pgwatch-prometheus:9091/pgwatch` on the internal +network; it is not published to the host. The simplest check is to query VictoriaMetrics (which +scrapes it) for a `pgwatch_`-prefixed series: ```bash -# Check specific metrics -curl http://localhost:8080/metrics | grep pg_stat_statements_calls +# Confirm pgwatch metrics have been ingested (host port 59090, VM basic auth) +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=pgwatch_pg_stat_statements_calls' ``` ### 5. Check VictoriaMetrics ingestion ```bash -curl 'http://localhost:8428/api/v1/query?query=up' +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=up' ``` Expected: @@ -97,18 +99,20 @@ Expected: {"status":"success","data":{"result":[...]}} ``` -Check data for specific metric: +Check data for a specific metric (all pgwatch series are `pgwatch_`-prefixed; the transaction +commit counter is `pgwatch_db_stats_xact_commit`): ```bash -curl 'http://localhost:8428/api/v1/query?query=pg_stat_database_xact_commit_total' +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=pgwatch_db_stats_xact_commit' ``` ### 6. Verify Grafana data source ```bash -curl http://monitor:YOUR_PASSWORD@localhost:3000/api/datasources +curl 'http://monitor:demo@localhost:3000/api/datasources' ``` -Check data source URL points to VictoriaMetrics. +Check the `PGWatch-Prometheus` data source URL points to `http://sink-prometheus:9090`. ## Specific scenarios @@ -158,8 +162,10 @@ Check data source URL points to VictoriaMetrics. create extension pg_stat_statements; ``` -2. **Database excluded from collection:** - Check pgwatch configuration for `PW_EXCLUDE_DATABASES`. +2. **Database not included in collection:** + pgwatch monitors the databases listed in `instances.yml` (rendered into `sources.yml`). There + is no `PW_EXCLUDE_DATABASES` (or any `PW_*`) environment variable. Verify the target's + `conn_str` and that `is_enabled: true` in `instances.yml`. ### No data for specific cluster @@ -169,14 +175,15 @@ Check data source URL points to VictoriaMetrics. **Causes and solutions:** -1. **Check connectivity for that specific connection:** +1. **Check connectivity for that specific connection** (run `psql` from `sink-postgres`, since the + `pgwatch-postgres` image has no client): ```bash - docker compose exec pgwatch psql "connection_string" -c "select 1" + docker compose exec sink-postgres psql "connection_string" -c "select 1" ``` 2. **Check pgwatch logs for errors:** ```bash - docker compose logs pgwatch | grep "cluster_name" + docker compose logs pgwatch-postgres pgwatch-prometheus | grep -i cluster ``` ### Data stops after some time @@ -189,7 +196,7 @@ Check data source URL points to VictoriaMetrics. 1. **pgwatch crashed:** ```bash - docker compose restart pgwatch + docker compose restart pgwatch-postgres pgwatch-prometheus ``` 2. **Target PostgreSQL connection dropped:** @@ -212,14 +219,15 @@ Grafana time range may not include collected data: Dashboard variables filter displayed data: - Check `cluster_name` variable -- Check `datname` variable +- Check `db_name` variable (the standard database-selection variable; only + [11. Single index](/docs/monitoring/dashboards/single-index) names it `datname`) - Try "All" option if available ### Query timeout Complex dashboards may timeout: - Check Grafana query inspector for errors -- Increase `VM_SEARCH_QUERY_TIMEOUT` +- Increase `VM_QUERY_DURATION` (default `30s`; maps to VictoriaMetrics' `-search.maxQueryDuration`) ## Permission issues @@ -229,10 +237,10 @@ If pgwatch can connect but gets no data: -- Check monitoring user permissions \du monitoring_user --- Grant required permissions +-- Grant the required role (pg_monitor; this is what prepare-db grants and what +-- the install/verify step checks for). pg_read_all_stats is a strict subset and +-- is not sufficient on its own. grant pg_monitor to monitoring_user; --- or for older PostgreSQL versions: -grant pg_read_all_stats to monitoring_user; ``` See [Permission errors](/docs/monitoring/troubleshooting/permissions) for details. @@ -243,9 +251,10 @@ See [Permission errors](/docs/monitoring/troubleshooting/permissions) for detail ```bash docker compose logs > monitoring-logs.txt docker compose ps >> monitoring-logs.txt - curl http://localhost:8080/metrics >> monitoring-logs.txt 2>&1 + curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://localhost:59090/api/v1/query?query=up' >> monitoring-logs.txt 2>&1 ``` -2. Check GitHub Issues for similar problems +2. Check [GitLab Issues](https://gitlab.com/postgres-ai/postgresai/-/issues) for similar problems 3. Open a new issue with the diagnostic output diff --git a/docs/monitoring/troubleshooting/performance.md b/docs/monitoring/troubleshooting/performance.md index 2fa7f60e..14545908 100644 --- a/docs/monitoring/troubleshooting/performance.md +++ b/docs/monitoring/troubleshooting/performance.md @@ -33,30 +33,30 @@ docker stats ### Reducing collection overhead -**1. Increase collection interval:** +There are no `PW_*` environment variables in this stack. Collection is controlled per metric group +in the pgwatch `metrics.yml` files, and the set of metrics is fixed by the `full` preset that the +generated `sources.yml` uses. -```bash -# Default: 15s, increase for less load -PW_SCRAPE_INTERVAL=30s -``` +**1. Increase a metric group's collection interval:** -**2. Use appropriate preset:** +Each group lists an interval (seconds) under `presets:` in +`config/pgwatch-prometheus/metrics.yml`. For example, most groups collect every `30s` while +`pg_stat_activity` and `wait_events` collect every `15s`. Raise these values for the groups you +care less about to reduce load. There is no `PW_SCRAPE_INTERVAL` variable. -| Preset | Load | Coverage | -|--------|------|----------| -| basic | Low | Essential metrics only | -| standard | Medium | Most use cases | -| full | Higher | Complete visibility | +**2. Preset selection:** -```bash -postgresai mon local-install --preset basic -``` +The generated source hardcodes `preset_metrics: full` (see +`config/scripts/generate-pgwatch-sources.sh` and `cli/lib/instances.ts`). `mon local-install` has +**no** `--preset` flag — its options are `--demo`, `--api-key`, `--db-url`, `--tag`, `--project`, +and `-y/--yes`. There are no `basic`/`standard` preset tiers. To trim collection, edit the `full` +preset (or define a custom preset) in `metrics.yml`. **3. Disable expensive metrics:** -```bash -PW_DISABLED_METRICS="bloat_tables,bloat_indexes" -``` +Remove or lengthen the interval of expensive groups (for example the bloat groups +`pg_table_bloat`, `pg_btree_bloat` — already at `7200s`) directly in the `full` preset in +`metrics.yml`. There is no `PW_DISABLED_METRICS` variable. ### Monitoring query overhead @@ -76,41 +76,23 @@ limit 10; ## VictoriaMetrics tuning -### Memory optimization - -**Reduce active time series:** - -```bash -# Limit cardinality -VM_STORAGE_MAX_UNIQUE_SERIES=1000000 -``` - -**Adjust cache sizes:** - -```bash -# Reduce if memory constrained -VM_STORAGE_CACHE_SIZE_STORAGE_TSID=128MB -VM_STORAGE_CACHE_SIZE_INDEX_DB=64MB -``` +The compose stack reads only these VictoriaMetrics (`sink-prometheus`) environment variables: +`VM_AUTH_USERNAME`, `VM_AUTH_PASSWORD`, `VM_RETENTION_PERIOD`, `VM_QUERY_DURATION`, and +`VM_MAX_CONCURRENT_REQUESTS`. Variables such as `VM_STORAGE_*`, `VM_SEARCH_*`, and a per-query +memory limit do not exist here. ### Query performance -**Increase query timeout:** +**Increase query duration limit:** ```bash -VM_SEARCH_QUERY_TIMEOUT=60s +VM_QUERY_DURATION=60s # default 30s; maps to -search.maxQueryDuration ``` **Limit concurrent queries:** ```bash -VM_SEARCH_MAX_CONCURRENT_REQUESTS=8 -``` - -**Reduce query memory:** - -```bash -VM_SEARCH_MAX_MEMORY_PER_QUERY=256MB +VM_MAX_CONCURRENT_REQUESTS=8 # default 16; maps to -search.maxConcurrentRequests ``` ### Storage optimization @@ -118,15 +100,20 @@ VM_SEARCH_MAX_MEMORY_PER_QUERY=256MB **Shorter retention:** ```bash -VM_RETENTION_PERIOD=7d # Down from 14d +VM_RETENTION_PERIOD=168h # 7 days, down from the default 336h (14 days) ``` +`VM_RETENTION_PERIOD` accepts VictoriaMetrics durations with hour/day/week/year suffixes — for +example `168h` or `7d`, `336h` or `14d`, `30d`, `4380h` (a bare integer is interpreted as months). +The bundled `.env.example` lists `30d` as a valid example. + **Enable compression:** -VictoriaMetrics compresses by default. Check compression ratio: +VictoriaMetrics compresses by default. Check TSDB status (host port `59090`, VM basic auth): ```bash -curl http://localhost:8428/api/v1/status/tsdb +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + http://localhost:59090/api/v1/status/tsdb ``` ## Grafana optimization @@ -140,12 +127,12 @@ curl http://localhost:8428/api/v1/status/tsdb **Optimize panel queries:** - Use `rate()` instead of raw counters - Limit time series with `topk()` or `bottomk()` -- Add `{cluster_name="production"}` filters +- Add `{cluster="production"}` filters (the metric label is `cluster`, not `cluster_name`) **Example — limit to top 10:** ```promql -topk(10, rate(pg_stat_statements_calls_total[5m])) +topk(10, rate(pgwatch_pg_stat_statements_calls[5m])) ``` ### Query caching @@ -171,36 +158,14 @@ row_limit = 10000 ## pgwatch tuning -### Connection pooling - -**Limit connections per database:** - -```bash -PW_MAX_PARALLEL_CONNECTIONS_PER_DB=2 # Down from 3 -``` - -**Increase connection timeout:** - -```bash -PW_CONNECT_TIMEOUT=15s -``` - -### Collection scheduling +### Connection and collection settings -**Stagger collection times:** - -For multiple databases, avoid simultaneous collection: - -```yaml -# Configure different scrape offsets -databases: - - name: db1 - scrape_offset: 0s - - name: db2 - scrape_offset: 5s - - name: db3 - scrape_offset: 10s -``` +pgwatch in this stack is configured through its `sources.yml` / `metrics.yml` files (generated +from `instances.yml`), not through `PW_*` environment variables. Variables such as +`PW_MAX_PARALLEL_CONNECTIONS_PER_DB` and `PW_CONNECT_TIMEOUT` do not exist here. To reduce load, +adjust per-metric collection intervals in `metrics.yml` (see +[Reducing collection overhead](#reducing-collection-overhead) above) or disable targets in +`instances.yml`. ## Resource allocation @@ -226,29 +191,31 @@ VictoriaMetrics Disk: 10 × 4 weeks × 5 GiB = 200 GiB ## Docker resource limits +Each service in `docker-compose.yml` sets top-level `cpus:` and `mem_limit:` keys whose defaults +come from environment variables — there is no `deploy.resources.limits` block. Override them in +`.env` rather than editing the compose file. These limits apply only when a container is +recreated, so after editing `.env` run `docker compose up -d --force-recreate ` to apply +them (`postgresai mon update-config` migrates `.env` but does not recreate services). CPUs are +floats (Docker Compose `cpus:` semantics); memory is in **bytes**. + +```bash +# .env — override the per-service defaults +PGWATCH_PROMETHEUS_CPUS=1.0 +PGWATCH_PROMETHEUS_MEM=536870912 # 512 MiB (default) + +SINK_PROMETHEUS_CPUS=2.0 # VictoriaMetrics (sink-prometheus) +SINK_PROMETHEUS_MEM=4294967296 # 4 GiB + +GRAFANA_CPUS=1.0 +GRAFANA_MEM=1073741824 # 1 GiB +``` + +The matching `cpus:`/`mem_limit:` lines in `docker-compose.yml` read these variables, for example: + ```yaml -# docker-compose.yml -services: - pgwatch: - deploy: - resources: - limits: - memory: 512M - cpus: '1.0' - - victoriametrics: - deploy: - resources: - limits: - memory: 4G - cpus: '2.0' - - grafana: - deploy: - resources: - limits: - memory: 1G - cpus: '1.0' + pgwatch-prometheus: + cpus: ${PGWATCH_PROMETHEUS_CPUS:-0.5} + mem_limit: ${PGWATCH_PROMETHEUS_MEM:-536870912} ``` ## High cardinality issues @@ -256,7 +223,8 @@ services: ### Identify high cardinality ```bash -curl http://localhost:8428/api/v1/status/tsdb | jq '.data.totalSeries' +curl -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + http://localhost:59090/api/v1/status/tsdb | jq '.data.totalSeries' ``` ### Common cardinality sources @@ -270,20 +238,19 @@ curl http://localhost:8428/api/v1/status/tsdb | jq '.data.totalSeries' ### Reduce cardinality -**Drop high-cardinality labels:** +**Reduce query-identity cardinality:** -```yaml -# VictoriaMetrics relabel config -relabel_configs: - - action: labeldrop - regex: query # Drop full query text -``` +Query-level series are keyed by the `queryid` label (used throughout the dashboards via +`pgwatch_query_info`). There is no `query` label carrying full query text on the Prometheus +metrics to drop. The primary cardinality control is the per-metric `LIMIT 100` in the pgwatch +`metrics.yml` (and the `sample_limit` safety nets in `prometheus.yml`); lower these to cap the +number of distinct `queryid`s retained. **Aggregate metrics:** ```promql -# Instead of per-table -sum by (datname) (pg_stat_user_tables_seq_scan_total) +# Instead of per-table, aggregate across tables +sum by (datname) (pgwatch_pg_stat_all_tables_seq_tup_read) ``` ## Monitoring the monitoring @@ -295,15 +262,11 @@ Use the Self-Monitoring dashboard to track: - Memory usage - Disk usage -Set up alerts for monitoring health: - -```yaml -- alert: MonitoringCollectionSlow - expr: pgwatch_collection_duration_seconds > 30 - for: 5m - labels: - severity: warning -``` +The stack does not ship alert rules (there is no Alertmanager or `vmalert`; see +[Alerting configuration](/docs/monitoring/configuration/alerting)). If you add your own alerting, +note that there is no `pgwatch_collection_duration_seconds` metric in this stack — base health +alerts on series that actually exist (for example `up{job="pgwatch-prometheus"}` for the pgwatch +scrape job, or VM's own self-monitoring metrics). ## Troubleshooting slow dashboards @@ -324,8 +287,8 @@ Look for: ```promql # Before (slow) -sum(rate(pg_stat_statements_calls_total[5m])) +sum(rate(pgwatch_pg_stat_statements_calls[5m])) -# After (faster - add filter) -sum(rate(pg_stat_statements_calls_total{cluster_name="$cluster"}[5m])) +# After (faster - add filter; the label is `cluster`) +sum(rate(pgwatch_pg_stat_statements_calls{cluster="$cluster_name"}[5m])) ``` diff --git a/docs/monitoring/troubleshooting/permissions.md b/docs/monitoring/troubleshooting/permissions.md index 3453b45d..90e7ea5e 100644 --- a/docs/monitoring/troubleshooting/permissions.md +++ b/docs/monitoring/troubleshooting/permissions.md @@ -184,8 +184,12 @@ grant execute on function public.monitoring_stats_reset() to monitoring_user; 3. Network connectivity exists ```bash -# Test from Grafana container -docker compose exec grafana curl http://victoriametrics:8428/api/v1/query?query=up +# Test from the Grafana container. The datasource points at sink-prometheus:9090 +# (there is no host named "victoriametrics", and the container port is 9090, not +# 8428). The endpoint requires VM basic auth. +docker compose exec grafana curl \ + -u "$VM_AUTH_USERNAME:$VM_AUTH_PASSWORD" \ + 'http://sink-prometheus:9090/api/v1/query?query=up' ``` ### Dashboard access @@ -259,16 +263,33 @@ host all monitoring_user 10.0.0.5/32 scram-sha-256 hostssl all monitoring_user 10.0.0.0/24 scram-sha-256 ``` -### Rotate credentials +### Rotate monitoring database credentials -Update connection string in pgwatch configuration and restart: +This rotates the **monitored database** role's password. To rotate the VictoriaMetrics +basic-auth credentials instead, see +[Rotating VictoriaMetrics credentials](/docs/monitoring/configuration/prometheus-config#rotating-victoriametrics-credentials). + +The monitored-database connection (including its password) lives in `instances.yml`, which is +rendered into pgwatch's `sources.yml` by `config/scripts/generate-pgwatch-sources.sh` — **not** in +`docker-compose.yml` or `.env` (the `.env` file holds stack secrets such as `REPLICATOR_PASSWORD` +and `VM_AUTH_*`). Update the target's `conn_str` in `instances.yml`, then regenerate sources and +recreate the pgwatch collectors: ```bash -docker compose down -# Update credentials in docker-compose.yml or .env -docker compose up -d +# Edit the target's conn_str in instances.yml, then regenerate sources.yml from it: +postgresai mon update-config +# Apply the new connection by restarting the collectors. `mon restart` takes at most one +# service argument, so restart each collector separately: +postgresai mon restart pgwatch-postgres +postgresai mon restart pgwatch-prometheus ``` +`postgresai mon restart` alone is **not** enough: it only runs `docker compose restart` and never +re-renders `sources.yml` from `instances.yml`, so the new `conn_str` would never reach the +collectors. `mon update-config` runs the `sources-generator` service that regenerates the sources. +(`mon restart` with no service argument restarts the whole stack; it accepts at most one service, +so the two collectors must be restarted with two separate commands.) + ## Troubleshooting checklist 1. ☐ User exists in database diff --git a/docs/platform/index.md b/docs/platform/index.md index 9b861c76..defefa03 100644 --- a/docs/platform/index.md +++ b/docs/platform/index.md @@ -7,7 +7,7 @@ slug: /platform ## Summary DBLab Platform is a unified solution that helps developers, DBAs, SREs, and QA engineers boost their work with PostgreSQL databases. They all can get their work done much faster thanks to the thin cloning of databases and the high level of automation of various tasks that the Platform offers. -## Open Source Components +## Open-source components The Platform uses the following open-source (AGPL v3) components: - [postgres-checkup](/docs/checkup/) – automated health checks of PostgreSQL databases - [Database Lab](/docs/database-lab/) – ultra-fast provisioning of full-size databases for development, testing, and analytics (thin provisioning, thin cloning) @@ -15,8 +15,8 @@ The Platform uses the following open-source (AGPL v3) components: All these components are optional. You may use the Platform to work, say, with postgres-checkup reports only. Or you may use it to enable fast development iterations using thin clones provided by Database Lab, not using postgres-checkup component at all. There is only one dependency: Joe Bot requires Database Lab configured. And such a configuration may or may not be visible in the Platform GUI, depending on your needs. -## Why Integrate with PostgresAI Platform -The open-source components described above may be installed and operate without the Platform. Although, when integrated with the Platform, they get you the following benefits: +## Why integrate with PostgresAI Platform +The open-source components described above may be installed and operate without the Platform. However, when integrated with the Platform, they get you the following benefits: 1. Centralized storage for data: - health-check reports generated by postgres-checkup are collected with all the details, including raw JSON reports (available for download) - Joe Bot session history allows you to collaborate with teammates more easily: you can share an SQL query with all the details such as EXPLAIN plans @@ -30,10 +30,10 @@ The open-source components described above may be installed and operate without 1. GUI and CLI to work with Database Lab instances 1. Web UI for Joe Bot -## Installation Options +## Installation options DBLab Platform is available in the form of SaaS ([https://postgres.ai](https://postgres.ai)) and Self-managed setup. -To start using SaaS, you need to enter to the system using one of the following accounts: +To start using SaaS, you need to enter the system using one of the following accounts: - Google/Gmail account - LinkedIn - GitLab diff --git a/docs/platform/security.md b/docs/platform/security.md index cb7d0b10..6087f9c1 100644 --- a/docs/platform/security.md +++ b/docs/platform/security.md @@ -17,12 +17,12 @@ If you found a possible vulnerability or have an urgent security-related concern ## Security incidents and incident management ### Security incident -Security incident – any violation or reasonable risk (or threat) to violate of: +Security incident – any violation or reasonable risk (or threat) to violate: - DBLab Platform (PostgresAI) integral security (including but not limited to: databases, backups, application code, infrastructure components, customer data) - Internal Postgres.ai Information Security Policies ### How incidents are processed -Once the information about an incident becomes known to the Engineer on Call, it is analyzed, the incident registration performed, severity level is assigned, and then it is determined if customer notification is needed. +Once the information about an incident becomes known to the Engineer on Call, it is analyzed, the incident registration is performed, severity level is assigned, and then it is determined if customer notification is needed. ### Communication with customers Any severe incident with any risk to affect customer data triggers a customer alert. The corresponding customers are alerted using an appropriate channel (email, phone, messaging system) within 24 hours. This communication includes the following information: @@ -35,7 +35,7 @@ Any severe incident with any risk to affect customer data triggers a customer al ## Architecture and data availability ## Customer data management ### Self-managed installations -In the case of a Self-managed setup, all the components are installed and operate inside your infrastructure, in clouds, or on-premise. It is strongly recommended to treat Database Lab instances that work with production clones as production-like machines, therefore protecting them correspondingly, using firewalls, secure connections. +In the case of a Self-managed setup, all the components are installed and operate inside your infrastructure, in clouds, or on-premises. It is strongly recommended to treat Database Lab instances that work with production clones as production-like machines, therefore protecting them correspondingly, using firewalls, secure connections. ### Key security principles of communication between Postgres.ai SaaS and your infrastructure In [Postgres.ai SaaS](https://postgres.ai/console), all the components that can communicate directly to database clones are installed inside your infrastructure. The Platform works "outside". This means that both Postgres.ai components and Postgres.ai engineers cannot reach your infrastructure: @@ -60,11 +60,11 @@ Please contact Postgres.ai support to obtain your registration key and detailed Further, we discuss all Postgres.ai components that are to be installed in your infrastructure and what kind of information can be transferred to Postgres.ai. #### Database Lab -When integrated, the Database Lab component may receive only control signals such as "create clone", "destroy clone", "refresh clone", "list snapshots". The full list of capabilities you may find in [Database Lab CLI Reference](/docs/database-lab/cli-reference). By no means is data from your databases available to the Platform. +When integrated, the Database Lab component may receive only control signals such as "create clone", "destroy clone", "refresh clone", "list snapshots". The full list of capabilities you may find in [DBLab CLI Reference](/docs/reference-guides/dblab-client-cli-reference). By no means is data from your databases available to the Platform. To be able to connect to a clone, users need to work inside your infrastructure, where connections to Database Lab clones (by default, ports 6000..6100) are possible, using the username and the password defined at clone creation time. Postgres.ai never stores passwords for clones, and it is the users' responsibility to remember them. -If CI observability is enabled (Database migration verification, "Observed sessions"), then partial PostgreSQL logs corresponding to activity observed on Database Lab clones is sent to Postgres.ai and stored there. Such logs may contain sensitive data. Customers can configure rules to automatically mask the sensitive data in these logs prior to sending to Postgres.ai. +If CI observability is enabled (Database migration verification, "Observed sessions"), then partial PostgreSQL logs corresponding to activity observed on Database Lab clones are sent to Postgres.ai and stored there. Such logs may contain sensitive data. Customers can configure rules to automatically mask the sensitive data in these logs prior to sending to Postgres.ai. #### Joe Bot diff --git a/docs/platform/service-providers.md b/docs/platform/service-providers.md index cfef9d8c..9ed9e9b4 100644 --- a/docs/platform/service-providers.md +++ b/docs/platform/service-providers.md @@ -17,7 +17,7 @@ Postgres.ai uses the following data subprocessors: | [DocuSign, Inc.](https://docusign.com/) | USA | Contract signing and document management | | [GitHub, Inc.](https://github.com/) | USA | Authorized user account authentication (OAuth) | | [GitLab B.V.](https://gitlab.com/) | USA | Authorized user account authentication (OAuth) | -| [Google LLC](https://cloud.google.com/) | USA | Primary data hosting (Google Cloud Platform). Customer Data (including backups) are stored here; AI models (Gemini) provided via API. **Customer Data is not used in Gemini API calls, unless explicitly approved by Customer** | +| [Google LLC](https://cloud.google.com/) | USA | Primary data hosting (Google Cloud Platform). Customer Data (including backups) is stored here; AI models (Gemini) provided via API. **Customer Data is not used in Gemini API calls, unless explicitly approved by Customer** | | [Hetzner Online GmbH](https://hetzner.com/) | Germany, Finland, USA | Managed monitoring data hosting (customer-selected region) | | [LinkedIn Corporation](https://linkedin.com/) | USA | Authorized user account authentication (OAuth) | | [OpenAI, LLC](https://openai.com/) | USA | AI models provided via API (GPT, Whisper). **Customer Data is not used in OpenAI API calls, unless explicitly approved by Customer** | diff --git a/docs/postgres-howtos/advanced-topics/index.md b/docs/postgres-howtos/advanced-topics/index.md index ebafd122..7182f57d 100644 --- a/docs/postgres-howtos/advanced-topics/index.md +++ b/docs/postgres-howtos/advanced-topics/index.md @@ -8,7 +8,7 @@ description: Deep dives into PostgreSQL internals, extensions, and advanced feat Deep dives into PostgreSQL internals, extensions, and advanced features. -## Guides by Category +## Guides by category ### Misc diff --git a/docs/postgres-howtos/advanced-topics/misc/how-many-tuples-can-be-inserted-in-a-page.md b/docs/postgres-howtos/advanced-topics/misc/how-many-tuples-can-be-inserted-in-a-page.md index f0d37854..cf5a7ee2 100644 --- a/docs/postgres-howtos/advanced-topics/misc/how-many-tuples-can-be-inserted-in-a-page.md +++ b/docs/postgres-howtos/advanced-topics/misc/how-many-tuples-can-be-inserted-in-a-page.md @@ -21,7 +21,7 @@ estimated_time: 5 min --- -In Postgres, all tables have hidden, system columns; `ctid` being one of them. Reading it, we can see physical +In Postgres, all tables have hidden, system columns; `ctid` being one of them. Reading it, we can see the physical location of the tuple (tuple = row physical version), the page number and offset inside it: ```sql diff --git a/docs/postgres-howtos/advanced-topics/misc/how-to-compile-postgres-on-ubuntu-22.04.md b/docs/postgres-howtos/advanced-topics/misc/how-to-compile-postgres-on-ubuntu-22.04.md index a4081734..4c1e24cc 100644 --- a/docs/postgres-howtos/advanced-topics/misc/how-to-compile-postgres-on-ubuntu-22.04.md +++ b/docs/postgres-howtos/advanced-topics/misc/how-to-compile-postgres-on-ubuntu-22.04.md @@ -23,13 +23,13 @@ estimated_time: 5 min This post describes how to quickly compile Postgres on Ubuntu 22.04. -The [official docs](https://postgresql.org/docs/current/installation.html). It is very detailed, and it's great to be +See the [official docs](https://postgresql.org/docs/current/installation.html). They are very detailed, and they're great to be used as a reference, but you won't find a concrete list of steps for a particular OS version, e.g., Ubuntu. This howto is enough to build Postgres from the `master` branch and start using it. Extend this basic set of steps if needed. -A couple of more notes: +A couple more notes: - The current (as of 2023, PG16) docs mention a new approach to building Postgres – [Meson](https://mesonbuild.com/) – but it is still considered experimental, we won't use it here. diff --git a/docs/postgres-howtos/advanced-topics/misc/how-to-help-others.md b/docs/postgres-howtos/advanced-topics/misc/how-to-help-others.md index b654dac7..e043eaa0 100644 --- a/docs/postgres-howtos/advanced-topics/misc/how-to-help-others.md +++ b/docs/postgres-howtos/advanced-topics/misc/how-to-help-others.md @@ -43,7 +43,7 @@ roughly: And (important!) always provide the link to your sources; you get two benefits from this: -- advertise good source and pay them back; +- advertise good sources and pay them back; - share the responsibility to some extent (very helpful if you are not very experienced yet; everyone might make a mistake). @@ -71,10 +71,10 @@ Two types of database experiments: those that aim to study macro-level query analysis: `pgss`, wait event analysis (aka active session history or performance/query insights), `auto_explain`, `pgBadger`, etc. - More about this type of experiments: [Day 13: How to benchmark](/docs/postgres-howtos/performance-optimization/indexing/how-to-benchmark). + More about this type of experiments: [Day 13: How to benchmark](/docs/postgres-howtos/performance-optimization/benchmarks/how-to-benchmark). 2. Single-session experiments – testing one or a sequence of SQL queries using a single session (sometimes, two), to - check query syntax, study individual query behavior, optimize particular query, etc. + check query syntax, study individual query behavior, optimize a particular query, etc. These experiments can be conducted in shared environments, on weaker machines. However, to study query performance, you need to have the same PG version, same or similar database, and matching planner settings (how to do it: diff --git a/docs/postgres-howtos/advanced-topics/misc/how-to-install-postgres-16-with-plpython3u.md b/docs/postgres-howtos/advanced-topics/misc/how-to-install-postgres-16-with-plpython3u.md index 30d35144..d7c86ed1 100644 --- a/docs/postgres-howtos/advanced-topics/misc/how-to-install-postgres-16-with-plpython3u.md +++ b/docs/postgres-howtos/advanced-topics/misc/how-to-install-postgres-16-with-plpython3u.md @@ -38,7 +38,7 @@ Python, a widely-used, high-level, and versatile programming language. file I/O, network communication, and other actions that could potentially affect the server's behavior or security. We used `plpython3u` in -[How to use OpenAI APIs right from Postgres to implement semantic search and GPT chat](/docs/postgres-howtos/advanced-topics/extensions/how-to-use-openai-apis-in-postgres), +[How to use OpenAI APIs right from Postgres to implement semantic search and GPT chat](/docs/postgres-howtos/advanced-topics/misc/how-to-use-openai-apis-in-postgres), let's now discuss how to install it. And something tells me that we'll be using it more in the future, for various tasks. diff --git a/docs/postgres-howtos/advanced-topics/misc/how-to-use-openai-apis-in-postgres.md b/docs/postgres-howtos/advanced-topics/misc/how-to-use-openai-apis-in-postgres.md index c7eb9305..102c9235 100644 --- a/docs/postgres-howtos/advanced-topics/misc/how-to-use-openai-apis-in-postgres.md +++ b/docs/postgres-howtos/advanced-topics/misc/how-to-use-openai-apis-in-postgres.md @@ -38,7 +38,7 @@ Today we will implement RAG ([Retrieval Augmented Generation](https://en.wikiped > ⚠️ Warning: this approach doesn't scale well, so it's not recommended for larger production clusters. Consider this as either for fun or for only small projects/services. 3. For each commit, generate OpenAI embeddings and store them in the "vector" format (`pgvector`). -4. Use semantic search to find commits, sped by `pgvector`'s HNSW index. +4. Use semantic search to find commits, sped up by `pgvector`'s HNSW index. 5. Finally, "talk to commit history" via OpenAI GPT4. (Inspired by: [@jonatasdp's Tweet](https://twitter.com/jonatasdp/status/1714711585191596419)) @@ -115,11 +115,11 @@ psql -X \ ``` As of July 2025, this will yield ~96k rows, covering more than 10k days of the Postgres development history – more -than 27 years – the first commit was made on in July 1996! +than 27 years – the first commit was made in July 1996! ## Create and store embeddings Here is a function that we'll use to get vectors for each commit entry, from OpenAI API using `plpython3u` (`u` here -means "untrusted" – it is allowed to such functions to talk to the external world): +means "untrusted" – it is allowed for such functions to talk to the external world): ```sql create or replace function openai_get_embedding( @@ -146,7 +146,7 @@ $$ language plpython3u; ``` Once it's created, start obtaining and storing vectors. We'll do it in small batches, to avoid long-running -transactions – no to lose large data volumes in case of failure and not to block concurrent sessions, if any: +transactions – not to lose large data volumes in case of failure and not to block concurrent sessions, if any: ```sql with scope as ( @@ -230,7 +230,7 @@ order by embedding <-> :'q_vector' limit 5 \gx ``` -If index is created, the second query should be very fast. You can check the plan and details of execution using +If the index is created, the second query should be very fast. You can check the plan and details of execution using `EXPLAIN (ANALYZE, BUFFERS)`. Our dataset is tiny (<100k), so the search speed should be ~1ms, and the buffer hit/read numbers ~1000 or less. There are a few tuning options for indexes `pgvector` offers, check out its [README](https://github.com/pgvector/pgvector/blob/master/README.md). @@ -372,7 +372,7 @@ Note that it uses the models `text-embedding-3-small` for embeddings and `gpt-4. 2. Another disadvantage of this approach is that `plpython3u` is not available in some Postgres services (e.g., RDS). -3. Finally, when working with in SQL context, it is quite easy to unintentionally have API calls in a loop. This might +3. Finally, when working within SQL context, it is quite easy to unintentionally have API calls in a loop. This might cause excessive expenses. To avoid it, we need to carefully check the execution plans. 4. For some people, such code is harder to debug. diff --git a/docs/postgres-howtos/advanced-topics/misc/how-to-work-with-metadata.md b/docs/postgres-howtos/advanced-topics/misc/how-to-work-with-metadata.md index d60087e4..5d56ebfb 100644 --- a/docs/postgres-howtos/advanced-topics/misc/how-to-work-with-metadata.md +++ b/docs/postgres-howtos/advanced-topics/misc/how-to-work-with-metadata.md @@ -40,14 +40,14 @@ your work more efficient. We'll cover these topics: In Postgres terminology, tables, indexes, views, materialized views are all called "relations". The metadata about them can be seen in various ways, but the "central" place is the -[pg_class system catalog](https://postgresql.org/docs/current/catalog-pg-class.html). In other words, this is a tables +[pg_class system catalog](https://postgresql.org/docs/current/catalog-pg-class.html). In other words, this is a table that stores information about all tables, indexes, and so on. It has two keys: - PK: `oid` - a number ([OID, object identifier](https://postgresql.org/docs/current/datatype-oid.html)) - UK: a pair of columns `(relname, relnamespace)`, relation name and OID of the schema. -A trick to remember: OID can be quickly converted to relation name, vice versa, using type conversion to `oid` and +A trick to remember: OID can be quickly converted to relation name, and vice versa, using type conversion to `oid` and `regclass` datatypes. Simple examples for a table named `t1`: @@ -76,7 +76,7 @@ So, there is no need to do `select oid from pg_class where relname = ...` – ju ## \? and ECHO_HIDDEN -`psql`'s `\?` command is crucial – this is how you can find description for all commands. For example: +`psql`'s `\?` command is crucial – this is how you can find descriptions for all commands. For example: ``` \d[S+] list tables, views, and sequences @@ -131,7 +131,7 @@ standard: [Docs](https://postgresql.org/docs/current/information-schema.html). W ## pg_stat_activity is not a table -It's essential to remember that when querying metadata, you might deal with something that doesn't behave as normal +It's essential to remember that when querying metadata, you might deal with something that doesn't behave as a normal table even if it looks so. For instance, when you read records from `pg_stat_activity`, you're not dealing with a consistent snapshot of table diff --git a/docs/postgres-howtos/advanced-topics/misc/index.md b/docs/postgres-howtos/advanced-topics/misc/index.md index f879e5cb..fe30829d 100644 --- a/docs/postgres-howtos/advanced-topics/misc/index.md +++ b/docs/postgres-howtos/advanced-topics/misc/index.md @@ -10,15 +10,16 @@ Various advanced PostgreSQL topics including internals, extensions, and other sp ## Topics in this section -### PostgreSQL Internals -- [Understanding how sparsely tuples are stored in a table](/docs/postgres-howtos/advanced-topics/misc/0004-tuple-sparsity) - 5 min *(intermediate)* -- [How to understand LSN values and WAL filenames](/docs/postgres-howtos/advanced-topics/misc/0009-lsn-values-and-wal-filenames) - 5 min *(intermediate)* -- [How many tuples can be inserted in a page](/docs/postgres-howtos/advanced-topics/misc/0066-how-many-tuples-can-be-inserted-in-a-page) - 5 min *(intermediate)* -- [How to estimate the YoY growth of a very large table using row creation timestamps and the planner statistics](/docs/postgres-howtos/advanced-topics/misc/0078-estimate-yoy-table-growth) - 7 min *(advanced)* -- [How to find the best order of columns to save on storage ("Column Tetris")](/docs/postgres-howtos/advanced-topics/misc/0084-how-to-find-the-best-order-of-columns-to-save-on-storage) - 5 min *(intermediate)* - -### Extensions and External Integrations -- [How to use OpenAI APIs right from Postgres to implement semantic search and GPT chat](/docs/postgres-howtos/advanced-topics/misc/0023-how-to-use-openai-apis-in-postgres) - 9 min *(intermediate)* +### Postgres internals +- [Understanding how sparsely tuples are stored in a table](/docs/postgres-howtos/advanced-topics/misc/tuple-sparsity) - 5 min *(intermediate)* +- [How to understand LSN values and WAL filenames](/docs/postgres-howtos/advanced-topics/misc/lsn-values-and-wal-filenames) - 5 min *(intermediate)* +- [How many tuples can be inserted in a page](/docs/postgres-howtos/advanced-topics/misc/how-many-tuples-can-be-inserted-in-a-page) - 5 min *(intermediate)* +- [How to estimate the YoY growth of a very large table using row creation timestamps and the planner statistics](/docs/postgres-howtos/advanced-topics/misc/estimate-yoy-table-growth) - 7 min *(advanced)* +- [How to find the best order of columns to save on storage ("Column Tetris")](/docs/postgres-howtos/advanced-topics/misc/how-to-find-the-best-order-of-columns-to-save-on-storage) - 5 min *(intermediate)* + +### Extensions and external integrations +- [How to use OpenAI APIs right from Postgres to implement semantic search and GPT chat](/docs/postgres-howtos/advanced-topics/misc/how-to-use-openai-apis-in-postgres) - 9 min *(intermediate)* + ### [How to help others](/docs/postgres-howtos/advanced-topics/misc/how-to-help-others) *Difficulty: beginner • Time: 5 min* @@ -31,4 +32,4 @@ Various advanced PostgreSQL topics including internals, extensions, and other sp *Difficulty: beginner • Time: 5 min* -- [How to install Postgres 16 with plpython3u: Recipes for macOS, Ubuntu, Debian, CentOS, Docker](/docs/postgres-howtos/advanced-topics/misc/0047-how-to-install-postgres-16-with-plpython3u) - 5 min *(beginner)* \ No newline at end of file +- [How to install Postgres 16 with plpython3u: Recipes for macOS, Ubuntu, Debian, CentOS, Docker](/docs/postgres-howtos/advanced-topics/misc/how-to-install-postgres-16-with-plpython3u) - 5 min *(beginner)* diff --git a/docs/postgres-howtos/advanced-topics/misc/lsn-values-and-wal-filenames.md b/docs/postgres-howtos/advanced-topics/misc/lsn-values-and-wal-filenames.md index 34474827..fabb8718 100644 --- a/docs/postgres-howtos/advanced-topics/misc/lsn-values-and-wal-filenames.md +++ b/docs/postgres-howtos/advanced-topics/misc/lsn-values-and-wal-filenames.md @@ -26,7 +26,7 @@ LSN – Log Sequence Number, a pointer to a location in the Write-Ahead Log (WAL - [WAL Internals](https://postgresql.org/docs/current/wal-internals.html) - [pg_lsn Type](https://postgresql.org/docs/current/datatype-pg-lsn.html) -LSN is a 8-byte (64-bit) value ([source code](https://gitlab.com/postgres/postgres/blob/4f2994647ff1e1209829a0085ca0c8d237dbbbb4/src/include/access/xlogdefs.h#L17)). It can be represented in the form of `A/B` (more specifically, `A/BBbbbbbb`, see below), where both `A` and `B` are 4-byte values. For example: +LSN is an 8-byte (64-bit) value ([source code](https://gitlab.com/postgres/postgres/blob/4f2994647ff1e1209829a0085ca0c8d237dbbbb4/src/include/access/xlogdefs.h#L17)). It can be represented in the form of `A/B` (more specifically, `A/BBbbbbbb`, see below), where both `A` and `B` are 4-byte values. For example: ``` nik=# select pg_current_wal_lsn(); pg_current_wal_lsn @@ -37,7 +37,7 @@ nik=# select pg_current_wal_lsn(); - `5D` here is higher 4-byte (32-bit) section of LSN - `257E19B0` can, in its turn, be split to two parts as well: - - `25` – lower 4-byte section of LSN (more specifically, only the highest 1 byte of of that 4-byte section) + - `25` – lower 4-byte section of LSN (more specifically, only the highest 1 byte of that 4-byte section) - `7E19B0` – offset in WAL (which is `16 MiB` by default; in some cases, it's changed – e.g., RDS changed it to `64 MiB`) Interesting that LSN values can be compared, and even subtracted one from each other – assuming we use the pg_lsn data type. The result will be in bytes: @@ -64,7 +64,7 @@ nik=# select pg_lsn '5D/257D6780' - '0/0'; ``` ## How to read WAL filenames -Now let's see how the LSN values correspond to WAL file names (files located in `$PGDATA/pg_wal`). We can get WAL file name for any given LSN using function `pg_walfile_name()`: +Now let's see how the LSN values correspond to WAL file names (files located in `$PGDATA/pg_wal`). We can get the WAL file name for any given LSN using the function `pg_walfile_name()`: ``` nik=# select pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn()); pg_current_wal_lsn | pg_walfile_name @@ -73,8 +73,8 @@ nik=# select pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn()); (1 row) ``` -Here `000000010000005D00000025` is WAL filename, it consists of three 4-byte (32-bit) words: -1. `00000001` – timeline ID (TimeLineID), a sequential "history number" that starts with 1 when Postgres cluster is initialized. It "identifies different database histories to prevent confusion after restoring a prior state of a database installation" ([source code](https://gitlab.com/postgres/postgres/blob/4f2994647ff1e1209829a0085ca0c8d237dbbbb4/src/include/access/xlogdefs.h#L50)). +Here `000000010000005D00000025` is the WAL filename, it consists of three 4-byte (32-bit) words: +1. `00000001` – timeline ID (TimeLineID), a sequential "history number" that starts with 1 when a Postgres cluster is initialized. It "identifies different database histories to prevent confusion after restoring a prior state of a database installation" ([source code](https://gitlab.com/postgres/postgres/blob/4f2994647ff1e1209829a0085ca0c8d237dbbbb4/src/include/access/xlogdefs.h#L50)). 2. `0000005D` – higher 4-byte section of sequence number. 3. `00000025` – can be viewed as two parts: - `000000` – 6 leading zeroes, @@ -89,10 +89,10 @@ WAL: 00000001 0000005D 00000025 ``` This can be very helpful if you need to work with LSN values or WAL filenames or with both of them, quickly navigating or comparing their values to understand the distance between them. Some examples when it can be useful to understand: -1. How much bytes the server generates per day -2. How much has passed since replication slot has been created +1. How many bytes the server generates per day +2. How much has passed since a replication slot has been created 3. What's the distance between two backups -4. How much of WAL data needs to be replayed to reach consistency point +4. How much of WAL data needs to be replayed to reach a consistency point ## Good blog posts worth reading besides the official docs: - [Postgres 9.4 feature highlight - LSN datatype](https://paquier.xyz/postgresql-2/postgres-9-4-feature-highlight-lsn-datatype/) diff --git a/docs/postgres-howtos/advanced-topics/misc/tuple-sparsity.md b/docs/postgres-howtos/advanced-topics/misc/tuple-sparsity.md index 494e115b..869c9986 100644 --- a/docs/postgres-howtos/advanced-topics/misc/tuple-sparsity.md +++ b/docs/postgres-howtos/advanced-topics/misc/tuple-sparsity.md @@ -24,20 +24,20 @@ estimated_time: 8 min Today, we'll discuss tuples and their locations in pages – this is quite entry-level material but useful in many cases. -Understanding physical layout of rows in tables may be important in many cases, especially during performance optimization efforts. +Understanding the physical layout of rows in tables may be important in many cases, especially during performance optimization efforts. ## Some terms - Page / buffer / block – unit of storage on disk and in Postgres buffer pool (loaded to RAM unchanged), in most cases 8 KiB (check it: `show block_size;`), it holds a portion of a table or index. - Tuple – physical version of a row in a table. - Tuple header – metadata about a tuple, including transaction ID, visibility info, and more. -- Transaction ID (same as XID, `tid`, `txid`) – unique identifier for a transaction in Postgres: +- Transaction ID (same as XID, `tid`, `txid`) – unique identifier for a transaction in Postgres: - It's allocated for modifying transactions. Read-only ones have "virtualxid" to avoid "wasting" XIDs, since they are still 32-bit as of PG16. There is [work in progress](https://commitfest.postgresql.org/43/3594/) to switch to 64-bit. - - You can get a XID allocated for your transactions calling function `pg_current_xact_id()` or, in PG12 and older, `txid_current()`. + - You can get an XID allocated for your transactions by calling the function `pg_current_xact_id()` or, in PG12 and older, `txid_current()`. Tuple header has interesting "hidden", or "system" columns ([docs](https://postgresql.org/docs/current/ddl-system-columns.html)): -- `ctid` – a hidden (system) column that represents the physical location of tuple in table, it has the form of two integers `(X, Y)`, where: - - `X` is page number starting from 0 - - `Y` is sequential number of tuple inside the page starting from 1 +- `ctid` – a hidden (system) column that represents the physical location of a tuple in a table, it has the form of two integers `(X, Y)`, where: + - `X` is the page number starting from 0 + - `Y` is the sequential number of a tuple inside the page starting from 1 - `xmin`, `xmax` – XIDs of transactions that created this row version (tuple), and deleted it (making this tuple "dead") ## ctid @@ -67,7 +67,7 @@ nik=# select * from t1 where user_id = 101469; (8 rows) ``` -To understand physical locations of these rows, just include `ctid` to the `SELECT` clause of the same query: +To understand the physical locations of these rows, just include `ctid` in the `SELECT` clause of the same query: ``` nik=# select ctid, * from t1 where user_id = 101469; ctid | id | user_id @@ -119,7 +119,7 @@ nik=# select count(*) from t1 where (ctid::text::point)[0] = 1274; (Note, however, that this will be a very slow query for larger tables since it requires a Seq Scan, and we cannot create an index on `ctid` or other system columns. For queries that are aiming to find particular `ctid` (`...where ctid = '(123, 456)'`), though, performance is going to be good thanks to Tid Scan, see https://pgmustard.com/docs/explain/tid-scan). ## ctid & BUFFERS metrics in execution plans -Confirming that the original query, indeed, involves many buffer operations (also see Day 1 where we talked about importance of `BUFFERS`): +Confirming that the original query, indeed, involves many buffer operations (also see Day 1 where we talked about the importance of `BUFFERS`): ``` nik=# explain (analyze, buffers, costs off) select * from t1 where user_id = 101469; QUERY PLAN @@ -141,16 +141,16 @@ nik=# select sum(pg_column_size(t1.*)) from t1 where user_id = 101469; (1 row) ``` -Thus, the Postgres executor must handle 88 KiB to return 317 bytes – this is far from optimal. Since we have an `Index Scan` here, some of those buffer hits are index-related, some – to get data from heap (table). +Thus, the Postgres executor must handle 88 KiB to return 317 bytes – this is far from optimal. Since we have an `Index Scan` here, some of those buffer hits are index-related, some – to get data from the heap (table). ## How to improve? -**Option 0.** Don't do anything but understand what's happening. Perhaps, you don't need to make significant improvements, as none of the options discussed below are perfect. Avoid over-optimization. But understand how sparse tuples are located and be ready to double-check it. In some cases, the fact that the target tuples stored too sparsely can be a significant factor for query performance, leading to timeouts. In this case, do consider the following tactics. +**Option 0.** Don't do anything but understand what's happening. Perhaps, you don't need to make significant improvements, as none of the options discussed below are perfect. Avoid over-optimization. But understand how sparse tuples are located and be ready to double-check it. In some cases, the fact that the target tuples are stored too sparsely can be a significant factor for query performance, leading to timeouts. In this case, do consider the following tactics. **Option 1.** Maintain tables and indexes in good shape: - Table bloat control: bloat is regularly analyzed, prevented by well-tuned autovacuum and regularly removed by `pg_repack`. -- Index maintenance: bloat control as well + regular reindexing, because index health declines over time even if autovacuum is well-tuned (btree health degradation rates improved in PG14, but those optimization does not eliminate the need to reindex on regular basis in heavily loaded systems). -- Partitioning: one of benefits of partitioning is improved data locality. +- Index maintenance: bloat control as well + regular reindexing, because index health declines over time even if autovacuum is well-tuned (btree health degradation rates improved in PG14, but those optimizations do not eliminate the need to reindex on a regular basis in heavily loaded systems). +- Partitioning: one of the benefits of partitioning is improved data locality. **Option 2.** Use index-only scans instead of index scans. This can be achieved by using multi-column indexes or covering indexes, to include all the columns needed for our query. For our example: ``` @@ -172,7 +172,7 @@ nik=# explain (analyze, buffers, costs off) select * from t1 where user_id = 101 **Option 3:** Physically reorganize the table according to the index / column values: This is physical reorganization of the table. It has two downsides: -- You need to choose which index to use for it – only one index. Therefore, it will help only to specific subset of the queries on the workload and can be useless for other queries +- You need to choose which index to use for it – only one index. Therefore, it will help only a specific subset of the queries in the workload and can be useless for other queries - UPDATEs of the rows will move tuples, decreasing the benefits of `CLUSTER`, so it might be needed to repeat it. There are two ways to reorganize the table diff --git a/docs/postgres-howtos/advanced-topics/replication/how-to-convert-a-physical-replica-to-logical.md b/docs/postgres-howtos/advanced-topics/replication/how-to-convert-a-physical-replica-to-logical.md index 11a45bba..81a5e89f 100644 --- a/docs/postgres-howtos/advanced-topics/replication/how-to-convert-a-physical-replica-to-logical.md +++ b/docs/postgres-howtos/advanced-topics/replication/how-to-convert-a-physical-replica-to-logical.md @@ -26,11 +26,11 @@ create a new physical replica first, and then convert it to logical. This approach: -- on the one hand, eliminates the need to execute initial data load step that can be fragile and quite stressful in case - of large, heavily-loaded DB, but -- on another, the logical replica created in such way has everything that the source Postgres instance has. +- on the one hand, eliminates the need to execute the initial data load step that can be fragile and quite stressful in case + of a large, heavily-loaded DB, but +- on the other hand, the logical replica created in such a way has everything that the source Postgres instance has. -So, this method suits better in case when you need all the data from the source be presented in the logical replica +So, this method suits better in the case when you need all the data from the source to be present in the logical replica you're creating, and it is extremely useful if you work with very large, heavily-loaded clusters. The steps below are quite straightforward. In this case, we use a physical replica that replicates data immediately from @@ -72,7 +72,7 @@ Additionally: ## Step 3: stop physical replica -Shut down physical replica and keep it down during the next step. This is needed so its position is guaranteed to be in +Shut down the physical replica and keep it down during the next step. This is needed so its position is guaranteed to be in the past compared to the logical slot we're going to create on the primary. ## Step 4: create publication, logical slot, and remember its LSN @@ -115,12 +115,12 @@ auto-promotes. This can take some time. Once it's done, check it: select pg_is_in_recovery(); ``` -- must return `f`, meaning that this node is now a primary itself (a clone) with position, corresponding to the position +- must return `f`, meaning that this node is now a primary itself (a clone) with a position corresponding to the position of the replication slot on the source node. ## Step 6: create subscription and start logical replication -Now, of the freshly created "clone", create logical subscription with `copy_data = false` and `create_slot = false`: +Now, on the freshly created "clone", create a logical subscription with `copy_data = false` and `create_slot = false`: ```sql create subscription 'my_sub' @@ -143,7 +143,7 @@ select * from pg_replication_slots; ## Finalize -- Wait until the logical replication lags fully caught up (occasional acute spikes are OK). +- Wait until the logical replication lag has fully caught up (occasional acute spikes are OK). - Return `wal_keep_size` (`wal_keep_segments`) to its original value on the primary. ## Additional notes diff --git a/docs/postgres-howtos/advanced-topics/replication/how-to-troubleshoot-streaming-replication-lag.md b/docs/postgres-howtos/advanced-topics/replication/how-to-troubleshoot-streaming-replication-lag.md index b2083338..2fb41a7e 100644 --- a/docs/postgres-howtos/advanced-topics/replication/how-to-troubleshoot-streaming-replication-lag.md +++ b/docs/postgres-howtos/advanced-topics/replication/how-to-troubleshoot-streaming-replication-lag.md @@ -1,7 +1,7 @@ --- title: How to troubleshoot streaming replication lag sidebar_label: troubleshoot streaming replication lag -description: Learn how to how to troubleshoot streaming replication lag +description: Learn how to troubleshoot streaming replication lag keywords: - postgresql - troubleshoot @@ -22,10 +22,10 @@ estimated_time: 5 min Streaming replication in Postgres allows for continuous data replication from a primary server to standby servers, to ensure high availability and balance read-only workloads. However, replication lag can occur, leading to delays in data synchronization. This guide provides steps to troubleshoot and mitigate replication lag. ## Identifying the lag -To start investigation we need to understand where we actually have lag, on which stage of replication: +To start the investigation, we need to understand where we actually have lag, on which stage of replication: - sending WAL stream to replica via network by `walsender` -- receiving WAL stream on replica from network by `walreciever` -- writing WAL on disk on replica by `walreciever` +- receiving WAL stream on replica from network by `walreceiver` +- writing WAL on disk on replica by `walreceiver` - applying (replaying) WAL as a recovery process Thus, streaming replication lag can be categorized into three types: @@ -74,18 +74,18 @@ from pg_stat_replication; ``` ### How to read results -Meaning of those `_lsn` +Meaning of those `_lsn` values: - `sent_lsn`: How much WAL (lsn position) has already been sent over the network - `write_lsn`: How much WAL (lsn position) has been sent to the operating system (before flushing) - `flush_lsn`: How much WAL (lsn position) has been flushed to the disk (written on the disk) - `replay_lsn`: How much WAL (lsn position) has been applied (visible for queries) -So lag is a gap between `pg_current_wal_lsn` and `replay_lsn` (`total_lag_bytes`, and it's a good idea to add it to monitoring, but for troubleshooting purposes we will need all 4 +So lag is a gap between `pg_current_wal_lsn` and `replay_lsn` (`total_lag_bytes`), and it's a good idea to add it to monitoring, but for troubleshooting purposes we will need all 4. - Lag on `sent_lag_bytes` means we have issues with sending the data, i.e. CPU saturated `WALsender` or overloaded network socket on the primary side - Lag on `write_lag_bytes` means we have issues with receiving the data, i.e. CPU saturated `WALreceiver` or overloaded network socket on the replica side - Lag on `flush_lag_bytes` means we have issues with writing the data on the disk on replica side, i.e. CPU saturated or IO contention of `WALreceiver` -- Lag `replay_lag_bytes` means we have issues with applying WAL on replica, usually CPU saturated or IO contention of postgres process +- Lag on `replay_lag_bytes` means we have issues with applying WAL on replica, usually CPU saturated or IO contention of postgres process Once we pinpointed the problem, we need to troubleshoot the process(es) on the OS level to find the bottleneck. diff --git a/docs/postgres-howtos/advanced-topics/replication/zero-downtime-major-upgrade.md b/docs/postgres-howtos/advanced-topics/replication/zero-downtime-major-upgrade.md index de0b33df..324a9a66 100644 --- a/docs/postgres-howtos/advanced-topics/replication/zero-downtime-major-upgrade.md +++ b/docs/postgres-howtos/advanced-topics/replication/zero-downtime-major-upgrade.md @@ -39,7 +39,7 @@ on [zero downtime Postgres upgrades](https://news.ycombinator.com/item?id=386161 [Alexander Sosna](https://twitter.com/xxorde) (GitLab) presented [a talk](https://postgresql.eu/events/pgconfeu2023/schedule/session/4791-how-we-execute-postgresql-major-upgrades-at-gitlab-with-zero-downtime/) explaining how GitLab's large clusters were upgraded under heavy load without any downtime – I highly recommend looking -at that work +at that work. There are many details behind the proper zero downtime upgrade, many challenges to solve, and here I present only a high-level plan. It works well for very large (dozens of TiB) clusters, with many replicas, and working under high TPS @@ -50,7 +50,7 @@ The detailed material will require several separate howtos to be written. The process consists of 2 steps: -1) **UPGRADE:** New cluster is created, running on new Postgres major version. +1) **UPGRADE:** A new cluster is created, running on a new Postgres major version. 2) **SWITCHOVER:** step by step switchover of the traffic. @@ -88,15 +88,15 @@ Steps: 8. Run `pg_upgrade --link` on the new cluster's leader. -9. Use `rsync --hard-links --size-only` on the new cluster's replicas – this is disputable step (see +9. Use `rsync --hard-links --size-only` on the new cluster's replicas – this is a disputable step (see details [here](https://postgresql.org/message-id/flat/CAM527d8heqkjG5VrvjU3Xjsqxg41ufUyabD9QZccdAxnpbRH-Q%40mail.gmail.com)), but this is what most people use for in-place (w/o logical replication) upgrades with `pg_upgrade --link`, and there - is no another fast alternative invented yet. + is no other fast alternative invented yet. 10. Configure new cluster's leader (now primary already) to use logical replication – create subscription, with `copy_data = false` and let it catch up with the working old cluster. -During all these steps the old cluster is up and running, and new cluster is invisible to users. This gives you a huge +During all these steps the old cluster is up and running, and the new cluster is invisible to users. This gives you a huge benefit of testing the whole process right in production (after proper testing in lower environments). ## Step 2: SWITCHOVER @@ -105,7 +105,7 @@ First, it makes sense to switch over the read-only (RO) traffic. If the applicat redirect only part of the RO traffic to new replicas. This would require an advanced replication lag detection in the load balancing code (see: -[How to determine the replication lag - Hybrid case: logical & physical](/docs/postgres-howtos/monitoring-troubleshooting/system-monitoring/how-to-determine-the-replication-lag#hybrid-case-logical-physical). +[How to determine the replication lag - Hybrid case: logical & physical](/docs/postgres-howtos/monitoring-troubleshooting/system-monitoring/how-to-determine-the-replication-lag#hybrid-case-logical--physical)). When it is time to redirect the RW traffic, to achieve zero downtime, one can use PgBouncer's PAUSE/RESUME. If there are multiple PgBouncer nodes (running on separate hosts/ports, or involving `SO_REUSEPORT`), it is important to implement a diff --git a/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-bulk-load.md b/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-bulk-load.md index 7ee03c01..54f79e86 100644 --- a/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-bulk-load.md +++ b/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-bulk-load.md @@ -30,9 +30,9 @@ Use `COPY` to load data, it's optimized for bulk load. Consider increasing `max_wal_size` and `checkpoint_timeout` temporarily. -Changing them does not require restart. +Changing them does not require a restart. -Increased values lead to increased recovery time in case of failure, but benefit is that checkpoints occur less often, +Increased values lead to increased recovery time in case of failure, but the benefit is that checkpoints occur less often, therefore: 1. less stress on disk, 2. less WAL data is written, thanks to decreased number of full page writes of the same pages (when load happens with @@ -64,11 +64,11 @@ state should be done with care. If this is a new table, consider completely avoiding WAL writes during the data load. Two options (both have limitations and require understanding that data can be lost if a crash happens): -- Use unlogged table: `CREATE UNLOGGED TABLE …`. Unlogged tables are not archived, not replicated, they are not persistent (though, they survive normal restarts). However, converting an unlogged table to a normal one takes time (likely, a lot – worth testing), because he data needs to be written to WAL. More about unlogged tables in [this post](https://crunchydata.com/blog/postgresl-unlogged-tables); also, see [this StackOverflow discussion](https://dba.stackexchange.com/questions/195780/set-postgresql-table-to-logged-after-data-loading/195829#195829). +- Use unlogged table: `CREATE UNLOGGED TABLE …`. Unlogged tables are not archived, not replicated, they are not persistent (though, they survive normal restarts). However, converting an unlogged table to a normal one takes time (likely, a lot – worth testing), because the data needs to be written to WAL. More about unlogged tables in [this post](https://crunchydata.com/blog/postgresl-unlogged-tables); also, see [this StackOverflow discussion](https://dba.stackexchange.com/questions/195780/set-postgresql-table-to-logged-after-data-loading/195829#195829). - Use `COPY` with `wal_level ='minimal'`. `COPY` has to be executed inside the transaction that created the table. In this case, due to `wal_level ='minimal'`, `COPY` writes won't be written to WAL - (as of PG16, this is so only if table is unpartitioned). + (as of PG16, this is so only if the table is unpartitioned). Additionally, consider using `COPY (FREEZE)` – this approach also provides a benefit: all tuples are frozen after the data load. Setting `wal_level='minimal'`, unfortunately, requires a restart, and additional changes (`archive_mode = 'off'`, `max_wal_senders = 0`). Of course, this method doesn't work well in most of the @@ -83,9 +83,9 @@ process (e.g., if single-threaded load saturates disk IO, parallelization won't - Partitioned tables and loading into multiple partitions using multiple workers ([Day 20: pg_restore tips](/docs/postgres-howtos/database-administration/backup-recovery/how-to-use-pg-restore)). -- Unpartitioned table and loading in big chunks. Such chunks require preparation of them – it can be CSV split into +- Unpartitioned table and loading in big chunks. Such chunks require preparation – it can be CSV split into pieces, or exported ranges of table data using multiple synchronized `REPEATABLE READ` transactions (working with the - same snapshot via `SET TRANSACTION SNAPSHOT`; see [Day 8: How to speed up pg_dump](/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-pg-dump). + same snapshot via `SET TRANSACTION SNAPSHOT`; see [Day 8: How to speed up pg_dump](/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-pg-dump)). If you use TimescaleDB, consider [timescaledb-parallel-copy](https://github.com/timescale/timescaledb-parallel-copy). diff --git a/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-pg-dump.md b/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-pg-dump.md index 64b8a2c2..54005735 100644 --- a/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-pg-dump.md +++ b/docs/postgres-howtos/database-administration/backup-recovery/how-to-speed-up-pg-dump.md @@ -33,7 +33,7 @@ Speeding up options discussed here: 3. Parallelized `pg_dump` 4. Advanced custom parallelization -## Monitoring Dump Progress +## Monitoring dump progress - **Verbose Output**: Use `pg_dump --verbose` to get detailed information during the dump process. - **Progress Estimation**: While `pg_dump` doesn't provide a progress bar, you can estimate progress by monitoring the size of the output file or, in the case of plain-format dumps, using tools like `pv`. - **System Monitoring**: Monitor system resources (CPU, I/O) to infer activity levels during the dump. @@ -44,7 +44,7 @@ In the cases of weak disks or network, it makes sense to apply compression. Note - **Custom (`-Fc`) and Directory (`-Fd`) Formats:** These formats apply compression by default. The default method is gzip at level 6, unless specified otherwise. - **Plain Format:** No compression is applied by default. To compress, pipe the output through a compression utility, e.g., `pg_dump ... | gzip`. -### Choosing Compression Methods and Levels +### Choosing compression methods and levels PostgreSQL supports multiple compression methods: - `zstd`: Offers the best balance between speed and compression ratio. Levels 1–5 are optimal for most datasets. - `lz4`: Fastest compression, but results in larger dump sizes. @@ -55,7 +55,7 @@ For detailed benchmarks and recommendations, refer to [Best pg_dump compression ## Option 2: Avoid dumping to disk, restore on the fly When the `directory` format is used (option `-Fd`; this format is the most flexible and I usually use it unless I have a specific situation), then compression is applied by default (`gzip` by default, also available `lz4` and `zstd`). -It also makes sense to use `pg_dump -h ... | pg_restore` and avoid writing to disk and restoring the dump "on the fly". Unfortunately, this can be done only when pg_dump is creating a `plain` dump – with `directory` format, it's not working. To solve this problem, there is a 3rd-party tool called [pgcopydb](https://github.com/dimitri/pgcopydb). +It also makes sense to use `pg_dump -h ... | pg_restore` and avoid writing to disk and restoring the dump "on the fly". Unfortunately, this can be done only when pg_dump is creating a `plain` dump – with the `directory` format, it's not working. To solve this problem, there is a 3rd-party tool called [pgcopydb](https://github.com/dimitri/pgcopydb). ## Option 3: pg_dump -j$N For servers with a high number of CPUs, when you deal with multiple tables and create dumps in the `directory` format, parallelization (option `-j$N`) can be very helpful. A single, but partitioned, table is going to behave similarly to multiple tables – because physically, the dumping will be applied to multiple tables (partitions). @@ -118,7 +118,7 @@ pg_dump -Fd -j8 -f ./test_dump test 48.24s user 3.25s system 83% cpu 1:01.83 to This is because `pg_dump` parallelization is working at table level and cannot parallelize dumping a single table. -To parallelize dumping a single large table, a custom solution is needed. To do that, we need to use multiple SQL clients such as psql, each one working with transaction at `REPEATABLE READ` isolation level (`pg_dump` is also using this level when working; see [the docs](https://postgresql.org/docs/current/transaction-iso.html)), and (important!) all of the dumping transactions need to use the same snapshot. +To parallelize dumping a single large table, a custom solution is needed. To do that, we need to use multiple SQL clients such as psql, each one working with a transaction at `REPEATABLE READ` isolation level (`pg_dump` is also using this level when working; see [the docs](https://postgresql.org/docs/current/transaction-iso.html)), and (important!) all of the dumping transactions need to use the same snapshot. The process can be as follows: 1. In one connection (e.g., in one `psql` session), start a transaction at the `REPEATABLE READ` level: @@ -160,10 +160,10 @@ The process can be as follows: --exclude-table-data="pgbench_accounts" \ test ``` -7. Do not forget to close the first transaction when everything is done – long-running transaction are harmful for OLTP workloads. -8. To restore, we need to follow the usual pg_dump order: DDL defining objects except indexes; then data load; and finally, constraint validation and index creation. For this, we can benefit from having dump in the `directory` format and use `pg_restore`'s options `-l` and `-L` to list the objects in the dump and filter them to restore, respectively. +7. Do not forget to close the first transaction when everything is done – long-running transactions are harmful for OLTP workloads. +8. To restore, we need to follow the usual pg_dump order: DDL defining objects except indexes; then data load; and finally, constraint validation and index creation. For this, we can benefit from having the dump in the `directory` format and use `pg_restore`'s options `-l` and `-L` to list the objects in the dump and filter them to restore, respectively. -A good post about dealing with snapshots when making database dumps: ["Postgres 9.5 feature highlight - pg_dump and external snapshots"](https://paquier.xyz/postgresql-2/postgres-9-5-feature-highlight-pg-dump-snapshots/). A very interesting additional consideration in that post is related to a special case of dumping: initialization of logical replicas. It is possible to use custom dumping methods synchronized with the position of logical slot, but creation of such slot has to be done via replication protocol (`CREATE_REPLICATION_SLOT foo3 LOGICAL test_decoding;`), not using SQL (`select * from pg_create_logical_replication_slot(...);`). +A good post about dealing with snapshots when making database dumps: ["Postgres 9.5 feature highlight - pg_dump and external snapshots"](https://paquier.xyz/postgresql-2/postgres-9-5-feature-highlight-pg-dump-snapshots/). A very interesting additional consideration in that post is related to a special case of dumping: initialization of logical replicas. It is possible to use custom dumping methods synchronized with the position of a logical slot, but creation of such a slot has to be done via replication protocol (`CREATE_REPLICATION_SLOT foo3 LOGICAL test_decoding;`), not using SQL (`select * from pg_create_logical_replication_slot(...);`). --- diff --git a/docs/postgres-howtos/database-administration/backup-recovery/how-to-use-pg-restore.md b/docs/postgres-howtos/database-administration/backup-recovery/how-to-use-pg-restore.md index 5cd7aaf4..6c99ab64 100644 --- a/docs/postgres-howtos/database-administration/backup-recovery/how-to-use-pg-restore.md +++ b/docs/postgres-howtos/database-administration/backup-recovery/how-to-use-pg-restore.md @@ -2,7 +2,7 @@ title: How to use pg_restore sidebar_label: use pg_restore description: >- - Today – a few tips on using `pgrestore` to restore databases (or only parts of + Today – a few tips on using `pg_restore` to restore databases (or only parts of them) from dumps. keywords: - postgresql @@ -38,11 +38,11 @@ restore steps. ## Atomic restore By default, pg_restore won't stop on errors. This might be surprising since we got used to more `strict` behavior when -dealing with Postgres. And this also might lead to situations when database restored only partially, but this remained +dealing with Postgres. And this also might lead to situations when the database is restored only partially, but this remained unnoticed. To switch to the `strict` mode, use `-e` (`--exit-on-error`). It can be also helpful to wrap the restoration process into a single transaction, using option `-1` (`--single-transaction`). -## Tracking Restore Progress +## Tracking restore progress - **Verbose Mode**: Use `pg_restore --verbose` to see detailed progress messages. - **TOC List**: Generate a Table of Contents (TOC) with `pg_restore -l backup.dump > toc.list` to view the sequence of objects. @@ -51,7 +51,7 @@ process into a single transaction, using option `-1` (`--single-transaction`). ## Schema vs. data split -You can look at your dumps at two different angles, both offering a way to structure the dump at high level. +You can look at your dumps from two different angles, both offering a way to structure the dump at a high level. First, you can distinguish schema and data – and use options: @@ -60,7 +60,7 @@ First, you can distinguish schema and data – and use options: Interestingly, for a dump, this split – "schema + data" – is not the most efficient in terms of restoration time and the quality of result: indexes are a part of the schema, but if you create them first, and only then load data, the loading -will take longer, and indexes will end up having worse shape than if build after the data load. +will take longer, and indexes will end up having worse shape than if built after the data load. Therefore, there is a second way to look at the dump structure that corresponds to the regular order of the full restore process: @@ -146,7 +146,7 @@ filler | character(84) | | | Partition of: pgbench_accounts FOR VALUES FROM (MINVALUE) TO (625001) ``` -## Handling Errors During Restore +## Handling errors during restore - **Exit on Error**: If `--exit-on-error` is set, the restore stops upon encountering an error. You'll then need to manually restore all subsequent objects using a customized TOC list (`pg_restore -l` and `-L`). @@ -198,7 +198,7 @@ pg_restore \ ## Postscript -Completely forgot (what many people forgot all the time too – it should be default behavior of `pg_restore`, but it's +Completely forgot (what many people forget all the time too – it should be default behavior of `pg_restore`, but it's not): After running `pg_restore`, don't forget: diff --git a/docs/postgres-howtos/database-administration/backup-recovery/index.md b/docs/postgres-howtos/database-administration/backup-recovery/index.md index 708739f6..63b5d678 100644 --- a/docs/postgres-howtos/database-administration/backup-recovery/index.md +++ b/docs/postgres-howtos/database-administration/backup-recovery/index.md @@ -15,7 +15,7 @@ Backup strategies, recovery procedures, and data migration techniques *Difficulty: advanced • Time: 8 min* ### [How to use pg_restore](/docs/postgres-howtos/database-administration/backup-recovery/how-to-use-pg-restore) -Today – a few tips on using `pgrestore` to restore databases (or only parts of them) from dumps. +Today – a few tips on using `pg_restore` to restore databases (or only parts of them) from dumps. *Difficulty: intermediate • Time: 6 min* diff --git a/docs/postgres-howtos/database-administration/configuration/how-to-change-postgres-parameter.md b/docs/postgres-howtos/database-administration/configuration/how-to-change-postgres-parameter.md index a0f25b88..3ca22b2c 100644 --- a/docs/postgres-howtos/database-administration/configuration/how-to-change-postgres-parameter.md +++ b/docs/postgres-howtos/database-administration/configuration/how-to-change-postgres-parameter.md @@ -52,7 +52,7 @@ Two ways to quickly check if a restart is needed: Apply the change in Postgres config files (`postgresql.conf` or its dependencies, if `include` directive is used). It's advisable to `ALTER SYSTEM` unless necessary, because it might lead to confusion in the future (it writes to `postgresql.auto.conf` and later it can be easily overlooked; see also -[this discussion](https://postgresql.org/message-id/flat/CA%2BVUV5rEKt2%2BCdC_KUaPoihMu%2Bi5ChT4WVNTr4CD5-xXZUfuQw%40mail.gmail.com)) +[this discussion](https://postgresql.org/message-id/flat/CA%2BVUV5rEKt2%2BCdC_KUaPoihMu%2Bi5ChT4WVNTr4CD5-xXZUfuQw%40mail.gmail.com)). ## 3) Apply the change diff --git a/docs/postgres-howtos/database-administration/configuration/how-to-perform-postgres-tuning.md b/docs/postgres-howtos/database-administration/configuration/how-to-perform-postgres-tuning.md index 8c9e128b..09850c5f 100644 --- a/docs/postgres-howtos/database-administration/configuration/how-to-perform-postgres-tuning.md +++ b/docs/postgres-howtos/database-administration/configuration/how-to-perform-postgres-tuning.md @@ -51,7 +51,7 @@ perhaps even 95/5 in this case): - [PostgreSQL Configurator](https://pgconfigurator.cybertec.at) - for TimescaleDB users: [timescaledb-tune](https://github.com/timescale/timescaledb-tune) -Additionally, to the official docs, [this resource](https://postgresqlco.nf) is good to use as a reference (it has +In addition to the official docs, [this resource](https://postgresqlco.nf) is good to use as a reference (it has integrated information from various sources, not only official docs) – for example, check the page for [random_page_cost](https://postgresqlco.nf/doc/en/param/random_page_cost/), a parameter which is quite often forgotten. @@ -72,7 +72,7 @@ A general rule here: the more logging, the better. Of course, assuming that you In short my recommendations are (this is worth a separate detailed post): -- turn on checkpoint logging, `log_checkpoints='on'` (fortunately, it's on by default in PG15+), +- turn on checkpoint logging, `log_checkpoints='on'` (fortunately, it's on by default in PG15+), - turn on all autovacuum logging, `log_autovacuum_min_duration=0` (or a very low value) - log temporary files except tiny ones (e.g., `log_temp_files = 100`) - log all DDL statements `log_statement='ddl'` @@ -82,14 +82,14 @@ In short my recommendations are (this is worth a separate detailed post): ## Autovacuum tuning -This is a big topic worth a separate post. In short, the key idea is that default settings don't suit for any modern +This is a big topic worth a separate post. In short, the key idea is that default settings don't suit any modern OLTP case (web/mobile apps), so autovacuum has to be always tuned. If we don't do it, autovacuum becomes a "converter" of large portions of dead tuples to bloat, and this eventually negatively affects performance. -Two areas of tuning needs to be addressed: +Two areas of tuning need to be addressed: 1. Increase the frequency of processing – lowering `**_scale_factor` / `**_threshold` settings, we make autovacuum - workers process tables when quite low value of dead tuples is accumulated + workers process tables when quite a low value of dead tuples is accumulated 2. Allocate more resources for processing: more autovacuum workers (`autovacuum_workers`), more memory (`autovacuum_work_mem`), and higher "quotas" for work (controlled via `**_cost_limit` / `**_cost_delay`). @@ -98,9 +98,9 @@ Two areas of tuning needs to be addressed: Again, it's worth a separate post. But in short, you need to consider raising `checkpoint_timeout` and – most importantly – `max_wal_size` (whose default is very small for modern machines and data volumes, just `1GB`), so checkpoints occur less frequently, especially when a lot of writes happen. However, shifting settings in this direction -mean longer recovery time in case of a crash or recovery from backups – this is a trade-off that needs to be analyzed +means longer recovery time in case of a crash or recovery from backups – this is a trade-off that needs to be analyzed for a particular case. -That's it. Generally, this initial/rough tuning of Postgres config shouldn't take long. For a particular cluster of type +That's it. Generally, this initial/rough tuning of Postgres config shouldn't take long. For a particular cluster or type of clusters, it's a 1-2 day work for an engineer. You don't actually need AI for this, empirical tools work well – unless you do aim to squeeze 5-10% more (you might want it though, e.g., if you have thousands of servers). diff --git a/docs/postgres-howtos/database-administration/configuration/how-to-tune-linux-parameters-for-oltp-postgres.md b/docs/postgres-howtos/database-administration/configuration/how-to-tune-linux-parameters-for-oltp-postgres.md index fa3e7de3..1793a05b 100644 --- a/docs/postgres-howtos/database-administration/configuration/how-to-tune-linux-parameters-for-oltp-postgres.md +++ b/docs/postgres-howtos/database-administration/configuration/how-to-tune-linux-parameters-for-oltp-postgres.md @@ -23,7 +23,7 @@ estimated_time: 5 min Here are general recommendations for basic tuning of Linux to run Postgres under heavy OLTP (web/mobile apps) workloads. -Most of them are default settings used in [postgresql_cluster](https://github.com/vitabaks/postgresql_cluster). +Most of them are default settings used in [Autobase](https://github.com/vitabaks/autobase) (formerly postgresql_cluster). Consider the parameters below as entry points for further study, and values provided as just rough tuning that is worth reviewing for a particular situation. @@ -88,7 +88,7 @@ echo 1 | sudo tee /proc/sys/vm/swappiness also: [PgCookbook - a PostgreSQL documentation project](https://github.com/grayhemp/pgcookbook/blob/master/database_server_configuration.md) by [@grayhemp](https://twitter.com/grayhemp). -## Network Configuration +## Network configuration > 📝 note that below ipv4 settings are provided; > 🎯 **TODO:** ipv6 options @@ -127,7 +127,7 @@ echo 1 | sudo tee /proc/sys/vm/swappiness Improves process scheduling latency for Postgres. -## Filesystem and File Handling +## Filesystem and file handling 15) `fs.file-max = 262144` diff --git a/docs/postgres-howtos/database-administration/configuration/how-to-tune-work-mem.md b/docs/postgres-howtos/database-administration/configuration/how-to-tune-work-mem.md index c1c941d3..7e3ccc6b 100644 --- a/docs/postgres-howtos/database-administration/configuration/how-to-tune-work-mem.md +++ b/docs/postgres-howtos/database-administration/configuration/how-to-tune-work-mem.md @@ -30,17 +30,17 @@ One of the possible approaches is explained here. ## Rough tuning and "safe" values of work_mem First, apply rough optimization as described in -[Rough configuration tuning (80/20 rule; OLTP)](/docs/postgres-howtos/performance-optimization/query-tuning/rough-oltp-configuration-tuning). +[Rough configuration tuning (80/20 rule; OLTP)](/docs/postgres-howtos/database-administration/configuration/rough-oltp-configuration-tuning). A query can "spend" `work_mem` multiple times (for multiple operations). But it is not allocated fully for each operation – an operation can need a lower amount of memory. -Therefore, it is hard to reliably predict, how much memory we'll need to use to handle a workload, without actual +Therefore, it is hard to reliably predict how much memory we'll need to use to handle a workload, without actual observation of the workload. -Moreover, in Postgres 13, new parameter was added, +Moreover, in Postgres 13, a new parameter was added, [hash_mem_multiplier](https://postgresqlco.nf/doc/en/param/hash_mem_multiplier/), adjusting the logic. The default is 2 -in PG13-16. It means that max memory used by a single hash operation is 2 * `work_mem`. +in PG13-16. It means that the max memory used by a single hash operation is 2 * `work_mem`. Worth mentioning, understanding how much memory is used by a session in Linux is very tricky per se – see a great article by Andres Freund: @@ -50,13 +50,13 @@ article by Andres Freund: A safe approach would be: - estimate how much memory is free – subtracting `shared_buffers`, `maintenance_work_mem`, etc., -- then divide the estimated available memory by `max_connections` and additional number such as 4-5 (or more, to be on - even safer side) – assuming that each backend will be using up to 4*`work_mem` or 5*`work_mem`. Of course, this multiplier +- then divide the estimated available memory by `max_connections` and an additional number such as 4-5 (or more, to be on + an even safer side) – assuming that each backend will be using up to 4*`work_mem` or 5*`work_mem`. Of course, this multiplier itself is a very rough estimate – in reality, OLTP workloads usually are much less hungry on average (e.g., having a - lot of PK lookups mean that average memory consumption is very low). + lot of PK lookups means that the average memory consumption is very low). In practice, it can make sense to adjust `work_mem` to a higher value, but this needs to be done after understanding the -behavior of Postgres under certain workload. The following steps are parts of iterative approach for further tuning. +behavior of Postgres under certain workload. The following steps are parts of an iterative approach for further tuning. ## Temp files monitoring @@ -96,11 +96,11 @@ It makes sense to raise it for individual queries. Consider two options: ## Raise work_mem globally Only if the previous steps are not suitable (e.g., it is hard to optimize queries and you cannot tune `work_mem` for parts -of workload), then consider raising `work_mem` globally, evaluating OOM risks. +of the workload), then consider raising `work_mem` globally, evaluating OOM risks. ## Iterate -After some time, review data from monitoring to ensure that situation improved or decide to perform another iteration. +After some time, review data from monitoring to ensure that the situation improved or decide to perform another iteration. ## Extra: pg_get_backend_memory_contexts diff --git a/docs/postgres-howtos/database-administration/configuration/rough-oltp-configuration-tuning.md b/docs/postgres-howtos/database-administration/configuration/rough-oltp-configuration-tuning.md index c036e462..f4190d4b 100644 --- a/docs/postgres-howtos/database-administration/configuration/rough-oltp-configuration-tuning.md +++ b/docs/postgres-howtos/database-administration/configuration/rough-oltp-configuration-tuning.md @@ -26,7 +26,7 @@ estimated_time: 5 min The 80/20 rule (a.k.a. [Pareto principle](https://en.wikipedia.org/wiki/Pareto_principle)) is often enough to achieve a good level of performance for OLTP workloads – it is recommended, in most cases, to start with this approach and focus on query tuning. Especially if your system is rapidly changing – fighting for "the last 20%" just using configuration -tuning, when an overlooked schema-level optimization (e.g., a missing index) can "kill" the performance, making little +tuning, when an overlooked schema-level optimization (e.g., a missing index) can "kill" the performance, makes little sense. However, those 20% make a lot of sense (e.g., budget-wise) to fight for, if workload and DB don't change too fast, or if you have a lot (say, thousands) of Postgres nodes. @@ -34,14 +34,14 @@ That's why simple empirical tuning services such as [PGTune](https://pgtune.leop we consider an example: a server of moderate size (64 vCPUs, 512 GiB RAM) serving moderate OLTP (web/mobile apps) workloads. -The settings below should be considered as starting points and the values as only as rough guidelines – review for your +The settings below should be considered as starting points and the values as only rough guidelines – review for your particular case, verify with experiments in non-production, and monitor all the changes closely. Good resources: - [PGTune](https://pgtune.leopard.in.ua) - [postgresql.conf configurations](https://postgresqlco.nf) -- [postgresql_cluster's defaults](https://github.com/vitabaks/postgresql_cluster/blob/master/vars/main.yml) +- [Autobase defaults](https://github.com/vitabaks/autobase/blob/master/automation/roles/common/defaults/main.yml) (formerly postgresql_cluster) 1) `max_connections = 200` @@ -93,7 +93,7 @@ Using huge pages can improve performance by reducing page management overhead. This is a part of checkpoint tuning. 10GB is quite a large value; however, some may prefer using even larger, which presents a trade-off: -- larger value help handle heavy writes better (lower IO stress), but +- larger values help handle heavy writes better (lower IO stress), but - larger values also lead to longer recovery time in case of crashes. > 🎯 **TODO:** a separate howto on checkpoint tuning. diff --git a/docs/postgres-howtos/database-administration/index.md b/docs/postgres-howtos/database-administration/index.md index d6eeeadb..b6add3a9 100644 --- a/docs/postgres-howtos/database-administration/index.md +++ b/docs/postgres-howtos/database-administration/index.md @@ -8,7 +8,7 @@ description: Essential guides for PostgreSQL database administrators covering ma Essential guides for PostgreSQL database administrators covering maintenance, backups, and configuration. -## Guides by Category +## Guides by category ### Backups, data export/import diff --git a/docs/postgres-howtos/database-administration/maintenance/autovacuum-queue-and-progress.md b/docs/postgres-howtos/database-administration/maintenance/autovacuum-queue-and-progress.md index 1b2afd3b..bda10ce0 100644 --- a/docs/postgres-howtos/database-administration/maintenance/autovacuum-queue-and-progress.md +++ b/docs/postgres-howtos/database-administration/maintenance/autovacuum-queue-and-progress.md @@ -24,10 +24,10 @@ estimated_time: 5 min # Autovacuum "queue" and progress -We know that in some cases, `autovacuum` settings (especially if they are default) need to be adjusted to keep up -with the updates. One of the ways to understand that the existing settings are "not enough" is to compare -[autovacuum_max_workers](https://postgresqlco.nf/doc/en/param/autovacuum_max_workers/) and the number of workers -actually used: +In many workloads, `autovacuum` settings — especially the defaults — need to be tuned to keep up with dead-tuple accumulation. +One way to tell whether the current settings are sufficient is to compare +[autovacuum_max_workers](https://postgresqlco.nf/doc/en/param/autovacuum_max_workers/) with the number of workers +that are actually running: ```sql show autovacuum_max_workers; @@ -41,119 +41,118 @@ where backend_type = 'autovacuum worker' group by state; ``` -👉 If most of the time, we see that the number of workers currently acting reaches `autovacuum_max_workers`, this -is a strong signal that it's time to consider increasing the number of workers (requires a restart) and/or make them -move faster – via [adjusting quotas](https://www.postgresql.org/docs/current/runtime-config-autovacuum.html) -([auto]vacuum_vacuum_cost_limit/[auto]vacuum_vacuum_cost_delay). +👉 If the number of active workers regularly hits `autovacuum_max_workers`, that is a strong signal to consider raising +the limit (which requires a restart) and/or letting workers run faster by +[adjusting quotas](https://www.postgresql.org/docs/current/runtime-config-autovacuum.html) +([auto]vacuum_vacuum_cost_limit / [auto]vacuum_vacuum_cost_delay). -However, we might have a question: how many tables are currently in the "queue" to be processed by `autovacuum`? The -analysis of this "queue" can give an idea how much work the workers need to do and if the current settings are "enough". -The size of the queue compared to the number workers, potentially, can give a metric similar to "load average" for CPU -load. +A related question: how many tables are currently in the "queue" waiting to be processed by `autovacuum`? +Looking at this queue gives a sense of how much work is pending and whether the current settings can keep up. +Queue depth relative to the number of workers acts as a rough analog of "load average" for CPU. -Below is the report ([source](https://gitlab.com/-/snippets/1889668)) that answers it, by looking at: +The report below ([source](https://gitlab.com/-/snippets/1889668)) answers that question by combining: -- current global `autovacuum` settings, -- per-table individual settings for vacuuming, -- numbers of dead tuples for each table. +- the current global `autovacuum` settings, +- per-table vacuum overrides, +- dead-tuple counts per table. -It compares all this and builds the list of tables that need vacuuming. +It cross-references them and produces a list of tables that need vacuuming. -Additionally, it inspects `pg_stat_progress_vacuum` to analyze what's being processed right now. +It also inspects `pg_stat_progress_vacuum` to show what is currently being processed. -The report has been derived from [here](https://github.com/avito-tech/dba-utils/blob/master/munin/vacuum_queue). +The query is adapted from [avito-tech/dba-utils](https://github.com/avito-tech/dba-utils/blob/master/munin/vacuum_queue). -The further development of this query could include: -_analysis of tables "in need of being auto-analyzed"_. +A natural extension is to add the same kind of analysis for tables that need auto-analyze. ```sql with table_opts as ( - select - pg_class.oid, - relname, - nspname, - array_to_string(reloptions, '') as relopts - from pg_class - join pg_namespace ns on relnamespace = ns.oid + select + pg_class.oid, + relname, + nspname, + array_to_string(reloptions, '') as relopts + from pg_class + join pg_namespace as ns on relnamespace = ns.oid ), vacuum_settings as ( - select - oid, - relname, - nspname, - case - when relopts like '%autovacuum_vacuum_threshold%' then - regexp_replace(relopts, '.*autovacuum_vacuum_threshold=([0-9.]+).*', e'\\1')::int8 - else current_setting('autovacuum_vacuum_threshold')::int8 - end as autovacuum_vacuum_threshold, - case - when relopts like '%autovacuum_vacuum_scale_factor%' - then regexp_replace(relopts, '.*autovacuum_vacuum_scale_factor=([0-9.]+).*', e'\\1')::numeric - else current_setting('autovacuum_vacuum_scale_factor')::numeric - end as autovacuum_vacuum_scale_factor, - case - when relopts ~ 'autovacuum_enabled=(false|off)' then false - else true - end as autovacuum_enabled - from table_opts -), p as ( - select * - from pg_stat_progress_vacuum -) -select - coalesce( - coalesce(nullif(vacuum_settings.nspname, 'public') || '.', '') || vacuum_settings.relname, -- current DB - format('[something in "%I"]', p.datname) -- another DB - ) as relation, - round((100 * psat.n_dead_tup::numeric / nullif(pg_class.reltuples, 0))::numeric, 2) as dead_tup_pct, - pg_class.reltuples::numeric, - psat.n_dead_tup, - format ( - 'vt: %s, vsf: %s, %s', -- 'vt' – vacuum_threshold, 'vsf' - vacuum_scale_factor - vacuum_settings.autovacuum_vacuum_threshold, - vacuum_settings.autovacuum_vacuum_scale_factor, - (case when autovacuum_enabled then 'DISABLED' else 'enabled' end) - ) as effective_settings, - case - when last_autovacuum > coalesce(last_vacuum, '0001-01-01') then left(last_autovacuum::text, 19) || ' (auto)' - when last_vacuum is not null then left(last_vacuum::text, 19) || ' (manual)' - else null - end as last_vacuumed, - coalesce(p.phase, '~~~ in queue ~~~') as status, - p.pid as pid, + select + oid, + relname, + nspname, case - when a.query ~ '^autovacuum.*to prevent wraparound' then 'wraparound' - when a.query ~ '^vacuum' then 'user' - when a.pid is null then null - else 'regular' - end as mode, + when relopts like '%autovacuum_vacuum_threshold%' + then regexp_replace(relopts, '.*autovacuum_vacuum_threshold=([0-9.]+).*', e'\\1')::int8 + else current_setting('autovacuum_vacuum_threshold')::int8 + end as autovacuum_vacuum_threshold, case - when a.pid is null then null - else coalesce(wait_event_type || '.' || wait_event, 'f') - end as waiting, - round(100.0 * p.heap_blks_scanned / nullif(p.heap_blks_total, 0), 1) as scanned_pct, - round(100.0 * p.heap_blks_vacuumed / nullif(p.heap_blks_total, 0), 1) as vacuumed_pct, - p.index_vacuum_count, + when relopts like '%autovacuum_vacuum_scale_factor%' + then regexp_replace(relopts, '.*autovacuum_vacuum_scale_factor=([0-9.]+).*', e'\\1')::numeric + else current_setting('autovacuum_vacuum_scale_factor')::numeric + end as autovacuum_vacuum_scale_factor, case - when psat.relid is not null and p.relid is not null then - (select count(*) from pg_index where indrelid = psat.relid) - else null - end as index_count -from pg_stat_all_tables psat - join pg_class on psat.relid = pg_class.oid - left join vacuum_settings on pg_class.oid = vacuum_settings.oid - full outer join p on p.relid = psat.relid and p.datname = current_database() - left join pg_stat_activity a using (pid) + when relopts ~ 'autovacuum_enabled=(false|off)' then false + else true + end as autovacuum_enabled + from table_opts +), progress as ( + select * + from pg_stat_progress_vacuum +) +select + coalesce( + coalesce(nullif(vacuum_settings.nspname, 'public') || '.', '') || vacuum_settings.relname, -- current DB + format('[something in "%I"]', progress.datname) -- another DB + ) as relation, + round((100 * psat.n_dead_tup::numeric / nullif(pg_class.reltuples, 0))::numeric, 2) as dead_tup_pct, + pg_class.reltuples::numeric, + psat.n_dead_tup, + format( + 'vt: %s, vsf: %s, %s', -- 'vt' – vacuum_threshold, 'vsf' – vacuum_scale_factor + vacuum_settings.autovacuum_vacuum_threshold, + vacuum_settings.autovacuum_vacuum_scale_factor, + case when autovacuum_enabled then 'enabled' else 'DISABLED' end + ) as effective_settings, + case + when last_autovacuum > coalesce(last_vacuum, '0001-01-01') then left(last_autovacuum::text, 19) || ' (auto)' + when last_vacuum is not null then left(last_vacuum::text, 19) || ' (manual)' + else null + end as last_vacuumed, + coalesce(progress.phase, '~~~ in queue ~~~') as status, + progress.pid, + case + when activity.query ~ '^autovacuum.*to prevent wraparound' then 'wraparound' + when activity.query ~ '^vacuum' then 'user' + when activity.pid is null then null + else 'regular' + end as mode, + case + when activity.pid is null then null + else coalesce(wait_event_type || '.' || wait_event, 'f') + end as waiting, + round(100.0 * progress.heap_blks_scanned / nullif(progress.heap_blks_total, 0), 1) as scanned_pct, + round(100.0 * progress.heap_blks_vacuumed / nullif(progress.heap_blks_total, 0), 1) as vacuumed_pct, + progress.index_vacuum_count, + case + when psat.relid is not null and progress.relid is not null + then (select count(*) from pg_index where indrelid = psat.relid) + else null + end as index_count +from pg_stat_all_tables as psat +join pg_class on psat.relid = pg_class.oid +left join vacuum_settings on pg_class.oid = vacuum_settings.oid +full outer join progress + on progress.relid = psat.relid + and progress.datname = current_database() +left join pg_stat_activity as activity using (pid) where - psat.relid is null - or p.phase is not null - or ( - autovacuum_vacuum_threshold - + (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples) - < psat.n_dead_tup - ) + psat.relid is null + or progress.phase is not null + or ( + autovacuum_vacuum_threshold + (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples) + < psat.n_dead_tup + ) order by status, relation; ``` -Example of the output (running it in psql with `\gx` instead of `;` in the end): +Example output (run in psql with `\gx` instead of `;` at the end): ![tables to be autovacuumed](/img/postgres-howtos/0067_tables_to_be_autovacuumed_2.png) diff --git a/docs/postgres-howtos/database-administration/maintenance/how-to-deal-with-bloat.md b/docs/postgres-howtos/database-administration/maintenance/how-to-deal-with-bloat.md index 1669432a..b9c16d91 100644 --- a/docs/postgres-howtos/database-administration/maintenance/how-to-deal-with-bloat.md +++ b/docs/postgres-howtos/database-administration/maintenance/how-to-deal-with-bloat.md @@ -63,7 +63,7 @@ Approaches to determine the bloat levels more precisely: - checking DB object sizes on a clone, running `VACUUM FULL` (heavy and blocks queries, thus not for production), and then checking sizes again and comparing before/after -Periodical checks are definitely recommended to control bloat levels and react, when needed. +Periodic checks are definitely recommended to control bloat levels and react, when needed. ## Index bloat mitigation (reactive) @@ -97,7 +97,7 @@ only when high table bloat is detected. * Tune `autovacuum`. * Monitor the `xmin` horizon and don't allow it to be too far in the past -- - [Day 45: How to monitor xmin horizon to prevent XID/MultiXID wraparound and high bloat](/docs/postgres-howtos/performance-optimization/statistics/how-to-monitor-xmin-horizon). + [Day 45: How to monitor xmin horizon to prevent XID/MultiXID wraparound and high bloat](/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-xmin-horizon). * Do not allow unnecessary long-running transactions (e.g., > 1h), neither on the primary, nor on standbys with `hot_standby_feedback` turned on. * If on Postgres 13 or older, consider upgrading to 14+ to benefit from btree index optimizations. diff --git a/docs/postgres-howtos/database-administration/maintenance/how-to-deal-with-long-running-transactions-oltp.md b/docs/postgres-howtos/database-administration/maintenance/how-to-deal-with-long-running-transactions-oltp.md index 01eb10c7..1e587d10 100644 --- a/docs/postgres-howtos/database-administration/maintenance/how-to-deal-with-long-running-transactions-oltp.md +++ b/docs/postgres-howtos/database-administration/maintenance/how-to-deal-with-long-running-transactions-oltp.md @@ -38,7 +38,7 @@ In the OLTP context (e.g., mobile and web apps), long-running transactions are o tuples that are produced by some transaction with `XID > xid1`) cannot be deleted by autovacuum until our transaction finishes. This might lead to bloat and performance degradation. -"Long-running" is a relative term, and, of course, its meaning depends on particular situation. Usually, in +"Long-running" is a relative term, and, of course, its meaning depends on the particular situation. Usually, in heavily-loaded systems – say ~10^5 TPS including RO queries and ~10^3 of XID-consuming TPS (writes) – we consider transactions running longer than 30-60 seconds to be long. This can be translated to 30-60k dead tuples accumulated in a table in the worst case – in the case when all transactions during that time frame produced 1 dead tuple. Of course, @@ -57,15 +57,15 @@ cannot. As of PG16 / 2023, Postgres doesn't provide a way to limit transaction duration (although there is a patch proposed, implementing [transaction_timeout](https://commitfest.postgresql.org/45/4040/) – help test and improve it if you can). -There are two limitation settings that can help reduce chances that a long-running transaction occur, but not -eliminating the risks completely: +There are two limitation settings that can help reduce chances that a long-running transaction occurs, but not +eliminate the risks completely: -1) [statement_timeout](https://postgresqlco.nf/doc/en/param/statement_timeout/) – limits the maximum duration of single +1) [statement_timeout](https://postgresqlco.nf/doc/en/param/statement_timeout/) – limits the maximum duration of a single query. For web/mobile apps, set it to a low value, e.g., 30s or 15 s. You can find in the Postgres docs, that this is "not recommended", but that advice is not practical and I consider it as unproductive. We do need to limit statement_timeout globally for web and mobile apps, to be protected: the - application code is usually limited anyway, and it's not a good situation when application reached a timeout such as + application code is usually limited anyway, and it's not a good situation when the application reached a timeout such as 30s, but Postgres is still processing an orphaned query. And users usually don't wait for more than a few seconds (Read: [What is a slow query?](https://postgres.ai/blog/20210909-what-is-a-slow-sql-query)). Those connections that do need a higher or even unlimited value for statement_timeout, can set it using a simple `SET` in a session (e.g., @@ -74,7 +74,7 @@ eliminating the risks completely: 2) [idle_in_transaction_session_timeout](https://postgresqlco.nf/doc/en/param/idle_in_transaction_session_timeout/) – sets maximum allowed idle time between queries, when in a transaction. Similar recommendations here: set it to a low - value, 15-30s. Sessions that absolutely needed it can override the global value. + value, 15-30s. Sessions that absolutely need it can override the global value. If both of these options are set to low values, it doesn't fully prevent long-running transactions from happening. For example, if we set both of them to 30s, we might still have a transaction running for hours: @@ -85,9 +85,9 @@ example, if we set both of them to 30s, we might still have a transaction runnin - another query lasting < 30s - ... -– in this case, neither of the two thresholds are achieved, but we can have a transaction that hours and even days. +– in this case, neither of the two thresholds is achieved, but we can have a transaction that lasts hours and even days. -While there is no such a setting as `transaction_timeout` yet, we can consider alternative options to fully prevent +While there is no such setting as `transaction_timeout` yet, we can consider alternative options to fully prevent long-running transactions from happening: 1) A cronjob (or `pg_cron` or `pg_timetable`) record to run a "terminator" query that detects all transactions lasting @@ -126,7 +126,7 @@ long-running transaction, to understand what queries it consists of. Without suc of data (queries are fast, they are usually below `log_min_duration_statement`), so we don't see them in logs. In this case, we can apply the method described in #PostgresMarathon -[Ad-hoc monitoring](/docs/postgres-howtos/performance-optimization/statistics/ad-hoc-monitoring) and sample long (> 1min) transactions every 1 +[Ad-hoc monitoring](/docs/postgres-howtos/performance-optimization/monitoring/ad-hoc-monitoring) and sample long (> 1min) transactions every 1 second (might be worth increasing the frequency here): ```sql diff --git a/docs/postgres-howtos/database-administration/maintenance/how-to-enable-data-checksums-without-downtime.md b/docs/postgres-howtos/database-administration/maintenance/how-to-enable-data-checksums-without-downtime.md index 15ef17ba..cd9d3a62 100644 --- a/docs/postgres-howtos/database-administration/maintenance/how-to-enable-data-checksums-without-downtime.md +++ b/docs/postgres-howtos/database-administration/maintenance/how-to-enable-data-checksums-without-downtime.md @@ -43,7 +43,7 @@ Per the [docs](https://postgresql.org/docs/current/app-initdb.html#APP-INITDB-DA However, I strongly recommend enabling data checksums for all clusters. If concerned about the overhead, test it. Example of a [synthetic benchmark](https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/issues/44), which -demonstrated a very low (~2%) of CPU load increased. In my opinion, even if this overhead was higher, it would still be +demonstrated a very low (~2%) CPU load increase. In my opinion, even if this overhead was higher, it would still be worth having them, considering how important it is to promptly detect storage-level corruption. ## How to check if data checksums are enabled in an existing cluster diff --git a/docs/postgres-howtos/database-administration/maintenance/how-to-run-analyze.md b/docs/postgres-howtos/database-administration/maintenance/how-to-run-analyze.md index d836bbe5..8e3dbe19 100644 --- a/docs/postgres-howtos/database-administration/maintenance/how-to-run-analyze.md +++ b/docs/postgres-howtos/database-administration/maintenance/how-to-run-analyze.md @@ -29,9 +29,9 @@ analyze; However, this, being single-threaded, can take a lot of time. ## How to run ANALYZE at full speed -To utilize multiple CPU cores, we can use client program `vacuumdb` with option `--analyze-only` and multiple workers ([docs](https://www.postgresql.org/docs/current/app-vacuumdb.html)). +To utilize multiple CPU cores, we can use the client program `vacuumdb` with option `--analyze-only` and multiple workers ([docs](https://www.postgresql.org/docs/current/app-vacuumdb.html)). -The following runs `ANALYZE` on *all* databases (`--all`; might be not supported in case of managed Postgres such as RDS), using the number of workers matching the number of vCPUs, and limiting overall duration by 2 hours (connection options like `-h`, `-U` are not shown here): +The following runs `ANALYZE` on *all* databases (`--all`; might be not supported in case of managed Postgres such as RDS), using the number of workers matching the number of vCPUs, and limiting overall duration to 2 hours (connection options like `-h`, `-U` are not shown here): ```shell { while IFS= read -r line; do @@ -47,18 +47,18 @@ The following runs `ANALYZE` on *all* databases (`--all`; might be not supported ) | tee -a analyze_all_$(date +%Y%m%d).log ``` -With this snippet, all the commands are going to be also printed and logged, with a timestamps (alternatively, instead of the `while`, one could use `ts` from `moreutils`). +With this snippet, all the commands are going to be also printed and logged, with timestamps (alternatively, instead of the `while`, one could use `ts` from `moreutils`). The number of jobs, `$JOBS`, should be chosen taking into account the number of vCPUs the server has. For example, if we want to go with the full speed, it makes sense to match the number of vCPUs on the server. The client machine can be "good enough" (it makes sense to double-check the client machine for CPU and disk IO saturation, to ensure that it's not a bottleneck). Note that if there are large unpartitioned tables, at some point, only a few workers may remain active. A solution to this problem can be partitioning: with many smaller partitions, it can allow all workers to remain busy, which can speed up the whole operation drastically on machines with a high number of CPU cores. It is highly recommended to run this on a reliable client machine close to server, or right on the server itself, in a `tmux` session, so external network interruptions wouldn't affect the process. -Important: for partitioned table, it is known that `vacuumdb --analyze-only` doesn't update statistics for partitioned tables (parent tables), it only takes care of partitions ([discussion](https://www.postgresql.org/message-id/ZyQgY_ErJszSZTNq%40momjian.us)). However, good news is that this has chances to be fixed in PG19 ([commitfest entry](https://commitfest.postgresql.org/patch/5871/)). Meanwhile, to have parallel gathering of stats for partitioned tables, we need to stick to a single-threaded `ANALYZE` (which will process all partitions one by one, but it will take care of parent table too), or we need another parallelization approach. // TODO: describe alternative approach +Important: for partitioned tables, it is known that `vacuumdb --analyze-only` doesn't update statistics for partitioned tables (parent tables), it only takes care of partitions ([discussion](https://www.postgresql.org/message-id/ZyQgY_ErJszSZTNq%40momjian.us)). However, good news is that this has chances to be fixed in PG19 ([commitfest entry](https://commitfest.postgresql.org/patch/5871/)). Meanwhile, to have parallel gathering of stats for partitioned tables, we need to stick to a single-threaded `ANALYZE` (which will process all partitions one by one, but it will take care of the parent table too), or we need another parallelization approach. // TODO: describe alternative approach ## On overhead Of course, under certain circumstances, this process may take significant time and utilize lots of resources (CPU, disk): -- if database is large, -- if has many options, +- if the database is large, +- if it has many options, - if `default_statistics_target` is increased from default 100 (say, to 1000). @@ -70,15 +70,15 @@ It is crucial to run `ANALYZE` after initial data load to a table, or when conte ## Major upgrade It is *CRUCIAL* to have `ANALYZE` as a mandatory post-upgrade step. It is not automated, as of 2024: - `pg_upgrade` doesn't run it -- major cloud providers, don't automate as well (checked: AWS RDS, GCP CloudSQL, Azure). +- major cloud providers don't automate it as well (checked: AWS RDS, GCP CloudSQL, Azure). This leads to numerous cases when people forget about this crucial step, ending up having critical database incidents on the very first busy day after the upgrade (usually, Mondays). -It is highly recommended including this step in your automation. +It is highly recommended to include this step in your automation. ## On `--analyze-in-stages` The option `--analyze-in-stages` runs three stages of analyze; the first stage uses the lowest possible `default_statistics_target` to produce usable statistics ASAP, and subsequent stages build the full statistics ([docs](https://www.postgresql.org/docs/current/app-vacuumdb.html)). -Often, there is a little value in running `--analyze-in-stages` in OLTP cases (web, mobile apps). Consider two options: -1. In-place upgrades. In this case, we have a downtime window of few minutes allocated already, to run `pg_upgrade --links`. And in most cases, `ANALYZE` (with full processing), being executed using multiple workers (via `vacuumdb`), doesn't increase it too much. Opening the gates too early, without statistics, usually leads to suboptimal performance and even unexpected downtime, because lack of proper statistics is harmful for performance. The idea to start processing traffic with "weak" statistics can lead to such incidents. So it is recommended to have a single iteration of `ANALYZE`, during the maintenance window, right after running `pg_upgrade`, and as much automated fashion as possible. Of course, it is very useful to know the timings – to forecast the duration of overall operation, it is a good idea to test the operation on a clone for the largest clusters, and remember the overall timing. +Often, there is little value in running `--analyze-in-stages` in OLTP cases (web, mobile apps). Consider two options: +1. In-place upgrades. In this case, we have a downtime window of a few minutes allocated already, to run `pg_upgrade --links`. And in most cases, `ANALYZE` (with full processing), being executed using multiple workers (via `vacuumdb`), doesn't increase it too much. Opening the gates too early, without statistics, usually leads to suboptimal performance and even unexpected downtime, because lack of proper statistics is harmful for performance. The idea to start processing traffic with "weak" statistics can lead to such incidents. So it is recommended to have a single iteration of `ANALYZE`, during the maintenance window, right after running `pg_upgrade`, and in as much automated fashion as possible. Of course, it is very useful to know the timings – to forecast the duration of overall operation, it is a good idea to test the operation on a clone for the largest clusters, and remember the overall timing. 1. In the case of zero-downtime upgrades relying on logical replication, it is not needed at all: the `ANALYZE` is to be executed on the target cluster's primary while logical replication is running. diff --git a/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-a-growing-pg-wal-directory.md b/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-a-growing-pg-wal-directory.md index 2e0c5ccb..de628b15 100644 --- a/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-a-growing-pg-wal-directory.md +++ b/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-a-growing-pg-wal-directory.md @@ -56,7 +56,7 @@ Reference doc: [The view pg_replication_slots](https://postgresql.org/docs/curre If `archive_mode` and `archive_command` are configured to archive WALs (e.g., for backup purposes), but `archive_command` is failing (returns non-zero exit code) or lagging (WAL generation rates are higher than the speed of -archiving), then this can be another reason of `pg_wal` growth. +archiving), then this can be another reason for `pg_wal` growth. How to monitor and troubleshoot it: @@ -65,11 +65,11 @@ How to monitor and troubleshoot it: Once the problem is identified, the `archive_command` needs to be either fixed or sped up (e.g., `wal_compression = on`, `max_wal_size` increased to have less WAL data generated; and, at the same time, use lighter compression in the archiver -tool -- this depends on the tool used in `archive_command`; e.g., WAL-G support many options for compression, more or +tool -- this depends on the tool used in `archive_command`; e.g., WAL-G supports many options for compression, more or less CPU intensive). The next two steps are to be considered as additional, since their effects on the `pg_wal` size growth are limited – -they can cause only certain amount of extra WALs being kept in `pg_wal` +they can cause only a certain amount of extra WALs being kept in `pg_wal` (unlike the first two reasons we just discussed). ## Step 3: check `wal_keep_size` @@ -84,7 +84,7 @@ When a successful checkpoint happens, Postgres can delete old WALs. In some case favor of less frequent checkpoints, this can cause more WALs to be stored in `pg_wal` than one could expect. In this case, if it's a problem for disk space (specifically important on smaller servers), reconsider `max_wal_size` and `checkpoint_timeout` to lower values. In some cases, it also can make sense to run an explicit manual `CHECKPOINT`, to -allow Postgres clean up some old files right away. +allow Postgres to clean up some old files right away. ## Summary diff --git a/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-and-speedup-postgres-restarts.md b/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-and-speedup-postgres-restarts.md index b1f1b5eb..f546e08e 100644 --- a/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-and-speedup-postgres-restarts.md +++ b/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-and-speedup-postgres-restarts.md @@ -27,7 +27,7 @@ estimated_time: 5 min Some very popular reasons affecting the duration of the shutdown attempt: 1. There are long-running transactions. -2. A lot of buffers are dirty (changes are applied in memory but not yet synced to disk, waiting for another checkpoint), causing long shutdown checkpoint. +2. A lot of buffers are dirty (changes are applied in memory but not yet synced to disk, waiting for another checkpoint), causing a long shutdown checkpoint. 3. WAL archiving (`archive_command`) is lagging. Below, we discuss each one of these reasons and how to mitigate them. @@ -62,7 +62,7 @@ from pg_buffercache where isdirty; ``` -If the value is large (say, several GiB), at shutdown attempt, Postgres is going to perform so-called "shutdown checkpoint", flushing dirty buffers to disk ([source code](https://gitlab.com/postgres/postgres/blob/ebf76f2753a91615d45f113f1535a8443fa8d076/src/backend/access/transam/xlog.c#L6229)). During this, it won't be processing queries, which affects downtime. Mitigation is simple – an explicit CHECKPOINT right before shutdown/restart attempt: +If the value is large (say, several GiB), at a shutdown attempt, Postgres is going to perform a so-called "shutdown checkpoint", flushing dirty buffers to disk ([source code](https://gitlab.com/postgres/postgres/blob/ebf76f2753a91615d45f113f1535a8443fa8d076/src/backend/access/transam/xlog.c#L6229)). During this, it won't be processing queries, which affects downtime. Mitigation is simple – an explicit CHECKPOINT right before shutdown/restart attempt: ```sql checkpoint; ``` @@ -71,7 +71,7 @@ This is going to help us keep shutdown checkpoint very light, decreasing downtim In some cases, it may make sense to issue 2 explicit CHECKPOINTs in a row, right before the shutdown attempt: if the first CHECKPOINT is heavy, it also takes time, during which new dirty buffers are accumulated due to ongoing writes – and this we mitigate with the second CHECKPOINT, keeping the shutdown checkpoint very light and fast. -To simulate situation described here: +To simulate the situation described here: - make sure `shared_buffers` is large (many GiB; changing it requires a restart) - increase `max_wal_size` and `checkpoint_timeout` (change doesn't require restart): say, `'10GB'` and `'60min'` (make sure there is enough disk space in the `pg_wal` subdirectory) - on a large table t1, perform: `set statement_timeout = '60s'; begin; delete from t1;` (it will be canceled, but lots of dirty buffers will be produced) diff --git a/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-long-startup.md b/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-long-startup.md index 8c6417eb..0fd3a022 100644 --- a/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-long-startup.md +++ b/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-long-startup.md @@ -21,7 +21,7 @@ estimated_time: 8 min -#PostgresMarathon day 3. In [the previous how-to](/docs/postgres-howtos/database-administration/configuration/how-to-troubleshoot-and-speedup-postgres-restarts), we discussed how to quickly stop or restart PostgreSQL. Now it's time to discuss what to do if you are trying to start your server but see this: +#PostgresMarathon day 3. In [the previous how-to](/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-and-speedup-postgres-restarts), we discussed how to quickly stop or restart PostgreSQL. Now it's time to discuss what to do if you are trying to start your server but see this: ``` FATAL: the database system is not yet accepting connections DETAIL: Consistent recovery state has not been yet reached. @@ -65,11 +65,11 @@ One of the biggest causes of frustration can be a lack of understanding of what' ## 2. Understand your settings and workload ### 2a. Check max_wal_size and checkpoint_timeout -The settings that matter the most here are related to checkpoint tuning. If `max_wal_size` and `checkpoint_timeout` are tuned so checkpoints happen less often, Postgres needs more time to reach a consistency point, if shutdown was not clean, without successful shutdown checkpoint (e.g., VM restarted) or if you're restoring from backups. To learn more about this: +The settings that matter the most here are related to checkpoint tuning. If `max_wal_size` and `checkpoint_timeout` are tuned so checkpoints happen less often, Postgres needs more time to reach a consistency point, if shutdown was not clean, without a successful shutdown checkpoint (e.g., VM restarted) or if you're restoring from backups. To learn more about this: - [official docs](https://postgresql.org/docs/current/sql-checkpoint.html) (official docs) - [WAL and checkpoint tuning](https://postgres.fm/episodes/wal-and-checkpoint-tuning) (Postgres.fm podcast) -In other words, if you observe longer startup time, it's probably because the server was tuned to write less to WAL and sync buffers less often during heavy loads at normal times – that tuning comes for the price of longer startup time, and this is exactly what you're dealing with. +In other words, if you observe longer startup time, it's probably because the server was tuned to write less to WAL and sync buffers less often during heavy loads at normal times – that tuning comes at the price of longer startup time, and this is exactly what you're dealing with. ### 2b. Understand the actual checkpoint behavior It is definitely recommended to have `log_checkpoint = on`. Its default is `'off'` in Postgres 14 and older, and `'on'` in PG15+. @@ -166,9 +166,9 @@ nik=# select pg_size_pretty(pg_lsn '45/58000000' - '45/1772EA98'); (1 row) ``` -– the first value is what's left, the second value is what's already done. Here, we see that we've already replayed ~1 GiB, and ~22 GiB are left. Analyzing timestamps in Postgres logs and current time and assuming that REDO is performed at constant speed (this assumption is rough but it's ok for rough estimate, especially if we re-estimating multiple times while observing the process), we can have an estimate of how much left to wait till consistency point and ability for Postgres to accept connections. +– the first value is what's left, the second value is what's already done. Here, we see that we've already replayed ~1 GiB, and ~22 GiB are left. Analyzing timestamps in Postgres logs and current time and assuming that REDO is performed at constant speed (this assumption is rough but it's ok for a rough estimate, especially if we re-estimate multiple times while observing the process), we can have an estimate of how much is left to wait till the consistency point and Postgres is able to accept connections. -If we're dealing with a crashed Postgres, then normally `pg_controldata` doesn't provide `Minimum recovery ending location` (showing `0/0`). In this case, we can check `$PGDATA/pg_wal` to understand how much left to replay, ordering files by creation time. This works under assumption that when crashed, Postgres has WALs that need to be replayed in pg_wal, and the "tail" of those WALs is all we need. For example: +If we're dealing with a crashed Postgres, then normally `pg_controldata` doesn't provide `Minimum recovery ending location` (showing `0/0`). In this case, we can check `$PGDATA/pg_wal` to understand how much is left to replay, ordering files by creation time. This works under the assumption that when crashed, Postgres has WALs that need to be replayed in pg_wal, and the "tail" of those WALs is all we need. For example: ``` ❯ ls -la /opt/homebrew/var/postgresql@15/pg_wal | grep 0000 | tail -3 @@ -177,13 +177,13 @@ If we're dealing with a crashed Postgres, then normally `pg_controldata` doesn't -rw------- 1 nik admin 16777216 Sep 28 11:03 000000010000004A000000E1 ``` -– the latest file is `000000010000004A000000E1`, hence we can take `4A/E100000` as a rough estimate where we'll finish with the REDO process. +– the latest file is `000000010000004A000000E1`, hence we can take `4A/E100000` as a rough estimate of where we'll finish with the REDO process. --- Bonus: how to simulate long startup / REDO time: 1. Increase the distance between checkpoints raising `max_wal_size` and `checkpoint_timeout` (say, `'100GB'` and `'60min'`) 2. Create a large table `t1` (say, 10-100M rows): `create table t1 as select i, random() from generate_series(1, 100000000) i;` -3. Execute a long transaction to data from `t1` (not necessary to finish it): `begin; delete from t1;` +3. Execute a long transaction to delete data from `t1` (not necessary to finish it): `begin; delete from t1;` 4. Observe the amount of dirty buffers with extension `pg_buffercache`: - create extension `pg_buffercache`; - `select isdirty, count(*), pg_size_pretty(count(*) * 8 * 1024) from pg_buffercache group by 1 \watch` diff --git a/docs/postgres-howtos/database-administration/maintenance/how-to-use-subtransactions-in-postgres.md b/docs/postgres-howtos/database-administration/maintenance/how-to-use-subtransactions-in-postgres.md index e8375546..3e5464dd 100644 --- a/docs/postgres-howtos/database-administration/maintenance/how-to-use-subtransactions-in-postgres.md +++ b/docs/postgres-howtos/database-administration/maintenance/how-to-use-subtransactions-in-postgres.md @@ -27,12 +27,12 @@ Don't use subtransactions, unless absolutely necessary. ## What are subtransactions? -A subtransaction, also known as "nested transaction", is a transaction started by instruction within the scope of an +A subtransaction, also known as "nested transaction", is a transaction started by an instruction within the scope of an already started transaction (src: [Wikipedia](https://en.wikipedia.org/wiki/Nested_transaction)). This feature allows users to partially roll back a transaction, which is helpful in many cases: fewer steps need to be repeated to retry the action if some error occurs. -The SQL standard defines two basic instructions describing this mechanism: `SAVEPOINT` and extension to the `ROLLBACK` +The SQL standard defines two basic instructions describing this mechanism: `SAVEPOINT` and an extension to the `ROLLBACK` statement – `ROLLBACK TO SAVEPOINT`. Postgres implements it, allowing slight deviations from the standard syntax – for example, allowing the omission of the word `SAVEPOINT` in the `RELEASE` and `ROLLBACK` statements. @@ -56,7 +56,7 @@ An example: ## Recommendations -The only actual recommendation I have for any project that aims to grow OLTP-like workload (web and mobile apps) is: +The only actual recommendation I have for any project that aims to grow an OLTP-like workload (web and mobile apps) is: > wherever possible, avoid subtransactions diff --git a/docs/postgres-howtos/development-tools/client-tools/how-to-change-ownership-of-all-objects-in-a-database.md b/docs/postgres-howtos/development-tools/client-tools/how-to-change-ownership-of-all-objects-in-a-database.md index ce60c6c7..fcd96864 100644 --- a/docs/postgres-howtos/development-tools/client-tools/how-to-change-ownership-of-all-objects-in-a-database.md +++ b/docs/postgres-howtos/development-tools/client-tools/how-to-change-ownership-of-all-objects-in-a-database.md @@ -21,7 +21,7 @@ estimated_time: 5 min --- -If you need to change ownership of *all* database objects in current database, use this anonymous `DO` +If you need to change ownership of *all* database objects in the current database, use this anonymous `DO` block (or copy-paste from [here](https://gitlab.com/postgres-ai/database-lab/-/snippets/2075222)): ```sql diff --git a/docs/postgres-howtos/development-tools/client-tools/how-to-set-application-name-without-extra-queries.md b/docs/postgres-howtos/development-tools/client-tools/how-to-set-application-name-without-extra-queries.md index ea540370..737e892e 100644 --- a/docs/postgres-howtos/development-tools/client-tools/how-to-set-application-name-without-extra-queries.md +++ b/docs/postgres-howtos/development-tools/client-tools/how-to-set-application-name-without-extra-queries.md @@ -51,7 +51,7 @@ application_name | human_here pid | 93285 ``` -However, having additional query – even a blazing fast one – means an extra RTT +However, having an additional query – even a blazing fast one – means an extra RTT ([round-trip time](https://en.wikipedia.org/wiki/Round-trip_delay)), affecting latency, especially when communicating with a distant server. diff --git a/docs/postgres-howtos/development-tools/client-tools/how-to-use-docker-to-run-postgres.md b/docs/postgres-howtos/development-tools/client-tools/how-to-use-docker-to-run-postgres.md index 0cafd5f8..2a0ed407 100644 --- a/docs/postgres-howtos/development-tools/client-tools/how-to-use-docker-to-run-postgres.md +++ b/docs/postgres-howtos/development-tools/client-tools/how-to-use-docker-to-run-postgres.md @@ -64,7 +64,7 @@ newgrp docker ## Run Postgres in container with persistent PGDATA -Assuming we want the data directory (`PGDATA`) be in `~/pgdata` and container named as `pg16`: +Assuming we want the data directory (`PGDATA`) to be in `~/pgdata` and the container named `pg16`: ```bash sudo docker run \ diff --git a/docs/postgres-howtos/development-tools/index.md b/docs/postgres-howtos/development-tools/index.md index 6a5668c5..3f24d9e1 100644 --- a/docs/postgres-howtos/development-tools/index.md +++ b/docs/postgres-howtos/development-tools/index.md @@ -8,7 +8,7 @@ description: Essential tools and techniques for PostgreSQL developers. Essential tools and techniques for PostgreSQL developers. -## Guides by Category +## Guides by category ### psql @@ -22,7 +22,7 @@ Master the PostgreSQL command-line interface. - [How to make "\\e" work in psql on a new machine](/docs/postgres-howtos/development-tools/psql/how-to-make-e-work-in-psql) - 5 min *(beginner)* - [How to format text output in psql scripts](/docs/postgres-howtos/development-tools/psql/how-to-format-text-output-in-psql-scripts) - 5 min *(beginner)* -### SQL Techniques +### SQL techniques Advanced SQL patterns and best practices. @@ -32,7 +32,7 @@ Advanced SQL patterns and best practices. - [How to generate fake data](/docs/postgres-howtos/development-tools/sql-techniques/how-to-generate-fake-data) - 5 min *(beginner)* - [How to use lib_pgquery in shell to normalize and match queries from various sources](/docs/postgres-howtos/development-tools/sql-techniques/how-to-use-lib-pgquery-in-shell) - 5 min *(beginner)* -### Client Tools +### Client tools Work effectively with PostgreSQL client applications. diff --git a/docs/postgres-howtos/development-tools/psql/how-to-use-variables-in-psql-scripts.md b/docs/postgres-howtos/development-tools/psql/how-to-use-variables-in-psql-scripts.md index b1a8aa62..638b4033 100644 --- a/docs/postgres-howtos/development-tools/psql/how-to-use-variables-in-psql-scripts.md +++ b/docs/postgres-howtos/development-tools/psql/how-to-use-variables-in-psql-scripts.md @@ -23,7 +23,7 @@ estimated_time: 5 min `psql` is a native terminal-based client for PostgreSQL. It is very powerful, available on many platforms, is installed with Postgres (often in a separate package, e.g., `apt install postgresql-client-16` on Ubuntu/Debian). -`psql` supports advanced scripting, and `psql` scripts can be viewed as a superset of Postgres SQL dialect. +`psql` supports advanced scripting, and `psql` scripts can be viewed as a superset of the Postgres SQL dialect. For example, it supports commands like `\set`, `\if`, `\watch`. I usually use extension `.psql` for the scripts that are to be executed by `psql`. @@ -94,7 +94,7 @@ Notes: - These are SQL queries, ending with a semicolon; they can be executed from other clients as well, not only from `psql`. - Custom GUC should be accompanied by a "_namespace_" (`set v1 = 1.23;` won't work – un-prefixed parameters are - considered as standard GUC, such as `shared_buffers`). + considered standard GUC, such as `shared_buffers`). - Working with strings is straightforward (`set myvars.v1 to 'hello';`). Values defined by using `SET` do not persist – they are present only during the ongoing session (or, if `SET LOCAL` is @@ -158,7 +158,7 @@ used, only during the current transaction). For persistence, use either of these ## Server-side variables – how to integrate with SQL This `SET`/`SHOW` syntax is very common. However, it is often inconvenient because neither `SET` nor `SHOW` can be -integrated to other SQL queries such as `SELECT`. To solve this, use alternative methods to set and +integrated into other SQL queries such as `SELECT`. To solve this, use alternative methods to set and access – `set_config(...)` and `current_setting(...)` ([docs](https://postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET)). @@ -242,7 +242,7 @@ Consider that we have a script named `largest_tables.psql`: limit :limit; ``` -Now, we can call it dynamically by setting the value for client-side variable `limit`: +Now, we can call it dynamically by setting the value for the client-side variable `limit`: ```sql ❯ psql -X -f largest_tables.psql -v limit=2 diff --git a/docs/postgres-howtos/development-tools/psql/psql-tuning.md b/docs/postgres-howtos/development-tools/psql/psql-tuning.md index 02abf81d..52041aad 100644 --- a/docs/postgres-howtos/development-tools/psql/psql-tuning.md +++ b/docs/postgres-howtos/development-tools/psql/psql-tuning.md @@ -89,7 +89,7 @@ nik=# select null; (1 row) ``` -To fix it (put it to `~/.psqrc` for persistency): +To fix it (put it to `~/.psqlrc` for persistence): ```sql \pset null 'Ø' diff --git a/docs/postgres-howtos/development-tools/sql-techniques/find-or-insert-using-a-single-query.md b/docs/postgres-howtos/development-tools/sql-techniques/find-or-insert-using-a-single-query.md index 0a353454..05ee70de 100644 --- a/docs/postgres-howtos/development-tools/sql-techniques/find-or-insert-using-a-single-query.md +++ b/docs/postgres-howtos/development-tools/sql-techniques/find-or-insert-using-a-single-query.md @@ -64,7 +64,7 @@ returning * But this leads to performing an `UPDATE` where we need just a `SELECT`, and this is a huge overhead that we definitely should avoid. -Another overhead that both queries have: each time a `ON CONFLICT` collision happens, we have a wasted sequence +Another overhead that both queries have: each time an `ON CONFLICT` collision happens, we have a wasted sequence increment (and this would be the same if we used `GENERATED ALWAYS AS IDENTITY`, because it uses sequences under the hood). @@ -72,7 +72,7 @@ hood). ## Approach 2: Naive CTE with UPDATE-or-SELECT -One might think this using CTE can help here: +One might think that using a CTE can help here: ```sql with val(ts) as ( @@ -121,9 +121,9 @@ pgbench: error: client 0 script 0 aborted in command 0 query 0: ERROR: duplicat DETAIL: Key (ts)=(2023-11-01 01:00:28-07) already exists. ``` -Why errors can happen here? Because between checking the row with sub-`SELECT` and attempting to run an `INSERT` some +Why can errors happen here? Because between checking the row with sub-`SELECT` and attempting to run an `INSERT` some very brief time always exists, during which another session might perform the `INSERT`. So this approach is not working -well in general case. But we can improve it. +well in the general case. But we can improve it. ## Approach 3: improved CTE @@ -191,7 +191,7 @@ query with `now()::timestamptz(0)` and `\watch .2`). from the table. This is a problem. Replacing it with another read attempt won't help. **Solution**: Use `ON CONFLICT DO UPDATE`, which here is acceptable since it comes after a `SELECT` attempt, and the - overhead discussed above hits us only rarely (at same frequency as query failures in the case of "naive CTE"). + overhead discussed above hits us only rarely (at the same frequency as query failures in the case of "naive CTE"). Testing it: diff --git a/docs/postgres-howtos/development-tools/sql-techniques/how-to-format-sql.md b/docs/postgres-howtos/development-tools/sql-techniques/how-to-format-sql.md index fad88be8..03ac68f2 100644 --- a/docs/postgres-howtos/development-tools/sql-techniques/how-to-format-sql.md +++ b/docs/postgres-howtos/development-tools/sql-techniques/how-to-format-sql.md @@ -209,7 +209,7 @@ limit 10; ## 5) Code blocks Root keywords should be on their own line in all cases except when followed by only one dependent word. If there are -more than one dependent words, they should form a column that is left-aligned and indented to the left of the root +more than one dependent word, they should form a column that is left-aligned and indented to the left of the root keyword. ✅ Good: diff --git a/docs/postgres-howtos/development-tools/sql-techniques/how-to-generate-fake-data.md b/docs/postgres-howtos/development-tools/sql-techniques/how-to-generate-fake-data.md index 0133733e..d49d17f7 100644 --- a/docs/postgres-howtos/development-tools/sql-techniques/how-to-generate-fake-data.md +++ b/docs/postgres-howtos/development-tools/sql-techniques/how-to-generate-fake-data.md @@ -54,7 +54,7 @@ Note that per the [docs](https://postgresql.org/docs/current/functions-math.html > uses a deterministic pseudo-random number generator. It is fast but not suitable for cryptographic applications... -We shouldn't use it for tasks as token or password generation (for that, use the library called `pgcrypto`). +We shouldn't use it for tasks such as token or password generation (for that, use the library called `pgcrypto`). But it is okay to use it for pure random data generation (not for obfuscation). Starting with Postgres 16, there is also @@ -236,7 +236,7 @@ There are several options to use Faker for Python: - PL/Python functions. Here, we'll demonstrate the use of the latter approach, with the "untrusted" version of PL/Python, -([Day 47: How to install Postgres 16 with plpython3u](/docs/postgres-howtos/advanced-topics/extensions/how-to-install-postgres-16-with-plpython3u); N/A for +([Day 47: How to install Postgres 16 with plpython3u](/docs/postgres-howtos/advanced-topics/misc/how-to-install-postgres-16-with-plpython3u); N/A for managed Postgres services such as RDS; note that in this case, the "trusted" version should suit too). ```sql diff --git a/docs/postgres-howtos/development-tools/sql-techniques/how-to-import-csv-to-postgres.md b/docs/postgres-howtos/development-tools/sql-techniques/how-to-import-csv-to-postgres.md index f1863d39..67f66099 100644 --- a/docs/postgres-howtos/development-tools/sql-techniques/how-to-import-csv-to-postgres.md +++ b/docs/postgres-howtos/development-tools/sql-techniques/how-to-import-csv-to-postgres.md @@ -106,12 +106,12 @@ psql -c "copy slow_tx_from_csv_2 from '$(pwd)/long_tx_$(date +%Y%m%d).csv' delim Note that here we use SQL command COPY that works on the server side and requires the full path to the CSV file located on the server where Postgres is running. There is also psql's command `\copy` that allows importing CSV file located on -the client's side (if these sides are different, in our case they are not, so we could use the either command). Docs: +the client's side (if these sides are different, in our case they are not, so we could use either command). Docs: - [server-side COPY](https://postgresql.org/docs/current/sql-copy.html) - [psql's \copy](https://postgresql.org/docs/current/app-psql.html#APP-PSQL-META-COMMANDS-COPY) -Note that when working with the "flexible" table format (text-only columns) – `slow_tx_from_csv_2` here – we need not +Note that when working with the "flexible" table format (text-only columns) – `slow_tx_from_csv_2` here – we must not forget about type conversion. For example: ``` @@ -142,7 +142,7 @@ this, we'll be using [file_fdw](https://postgresql.org/docs/current/file-fdw.htm Having a great advantage (live data!), this method has its obvious disadvantages: - It's read-only (although, you can easily create a snapshot using `create table as select from ...`). -- The data is not protected by backup system you (hopefully) have, the storage is not reliable (a file). +- The data is not protected by the backup system you (hopefully) have, the storage is not reliable (a file). - Performance limitations: you cannot create indexes to speed up the queries, so it doesn't work really well for huge data volumes (although, again, you can create a snapshot or a materialized view, and have indexes there). - Some `COPY` options are not supported by `file_fdw`, such as the `FORCE_QUOTE` option. diff --git a/docs/postgres-howtos/development-tools/sql-techniques/how-to-use-lib-pgquery-in-shell.md b/docs/postgres-howtos/development-tools/sql-techniques/how-to-use-lib-pgquery-in-shell.md index 4e9db6d8..250b5c12 100644 --- a/docs/postgres-howtos/development-tools/sql-techniques/how-to-use-lib-pgquery-in-shell.md +++ b/docs/postgres-howtos/development-tools/sql-techniques/how-to-use-lib-pgquery-in-shell.md @@ -31,7 +31,7 @@ estimated_time: 5 min In [Day 12: How to find query examples for problematic pg_stat_statements records](/docs/postgres-howtos/performance-optimization/query-tuning/from-pgss-to-explain--how-to-find-query-examples) it was mentioned that query normalization can be done using [lib_pgquery](https://github.com/pganalyze/libpg_query). This -library builds a tree representation of query text, and also computes so-called "fingerprint" – a hash of the normalized +library builds a tree representation of query text, and also computes a so-called "fingerprint" – a hash of the normalized form of the query (query text where all parameters are removed). This is helpful in various cases, for example: @@ -39,7 +39,7 @@ This is helpful in various cases, for example: - If you need to match normalized queries in `pg_stat_statements` with individual query texts from `pg_stat_activity` or Postgres logs, and you use Postgres version older than 14, where [compute_query_id](https://postgresqlco.nf/doc/en/param/compute_query_id/) was implemented to solve this problem. -- If you use newer version of Postgres, but `compute_query_id` is `off`. +- If you use a newer version of Postgres, but `compute_query_id` is `off`. - If you use query texts from different sources and/or are unsure that the standard `query_id` (aka "`queryid"` - the naming is not unified across tables) can be a reliable way of matching. diff --git a/docs/postgres-howtos/index.md b/docs/postgres-howtos/index.md index 1b7f4990..1b5bcf31 100644 --- a/docs/postgres-howtos/index.md +++ b/docs/postgres-howtos/index.md @@ -15,7 +15,7 @@ A comprehensive collection of practical PostgreSQL how-to guides covering databa Each how-to page includes "Copy for LLM" and "View raw" buttons for easy copying. -## 📚 Guide Categories +## 📚 Guide categories ### [Performance & query optimization](/docs/postgres-howtos/performance-optimization) Master query optimization, indexing strategies, and performance tuning techniques to make your PostgreSQL database blazing fast. @@ -67,7 +67,7 @@ The source files are available at: https://gitlab.com/postgres-ai/docs/-/tree/ma Feel free to submit merge requests! -## Related Resources +## Related resources - [DBLab how-to guides](/docs/dblab-howtos) - Guides for using DBLab Engine - [Reference guides](/docs/reference-guides) - Technical reference documentation diff --git a/docs/postgres-howtos/miscellaneous/how-to-get-into-trouble-using-some-postgres-features.md b/docs/postgres-howtos/miscellaneous/how-to-get-into-trouble-using-some-postgres-features.md index 49eecddf..30c34890 100644 --- a/docs/postgres-howtos/miscellaneous/how-to-get-into-trouble-using-some-postgres-features.md +++ b/docs/postgres-howtos/miscellaneous/how-to-get-into-trouble-using-some-postgres-features.md @@ -60,7 +60,7 @@ useful materials to educate yourself: A couple of tips – how to make your code NULL-safe: -- Consider using expressions like `COALESCE(val, 0)` for replace `NULL`s with some value (usually `0` or `''`). +- Consider using expressions like `COALESCE(val, 0)` to replace `NULL`s with some value (usually `0` or `''`). - For comparison, instead of `=` or `<>`: `IS [NOT] DISTINCT FROM` (check out the `EXPLAIN` plan though). - Instead of concatenation, use: `format('%s %s', var1, var2)`. - Don't use `WHERE NOT IN (SELECT ...)` – use `NOT EXISTS` instead ( @@ -78,7 +78,7 @@ Why you might want to get rid of subtransactions completely: ## int4 PK Zero-downtime conversion of `int4` (a.k.a. int a.k.a. integer) PK to `int8` when the table has 1B rows requires a lot of -efforts. While table `(id int4, created_at timestamptz)` is going to take the same disk space as +effort. While table `(id int4, created_at timestamptz)` is going to take the same disk space as `(id int8, created_at timestamptz)` due to [alignment padding](https://stackoverflow.com/a/7431468/459391). ## (Exotic) SELECT INTO is not you think it is @@ -127,5 +127,5 @@ Read [common db schema change mistakes](https://postgres.ai/blog/20220525-common ## Other "Don't do" articles - [Depesz: Don’t do these things in PostgreSQL](https://depesz.com/2020/01/28/dont-do-these-things-in-postgresql/) -- [PostgreSQL Wiki: Don't Do This](https://wiki.postgresql.org/wiki/Don't_Do_This) +- [PostgreSQL Wiki: Don't Do This](https://wiki.postgresql.org/wiki/Don%27t_Do_This) - [JOOQ: Don't do this](https://jooq.org/doc/latest/manual/reference/dont-do-this/) diff --git a/docs/postgres-howtos/monitoring-troubleshooting/index.md b/docs/postgres-howtos/monitoring-troubleshooting/index.md index 9a0650b4..4ef21989 100644 --- a/docs/postgres-howtos/monitoring-troubleshooting/index.md +++ b/docs/postgres-howtos/monitoring-troubleshooting/index.md @@ -8,16 +8,16 @@ description: Tools and techniques for monitoring PostgreSQL and solving common p Tools and techniques for monitoring PostgreSQL and solving common problems. -## Guides by Category +## Guides by category -### System Monitoring +### System monitoring Track database health, performance metrics, and resource usage. - [How to troubleshoot Postgres performance using FlameGraphs and eBPF (or perf)](/docs/postgres-howtos/monitoring-troubleshooting/system-monitoring/flamegraphs-for-postgres) - 6 min *(beginner)* - [How to determine the replication lag](/docs/postgres-howtos/monitoring-troubleshooting/system-monitoring/how-to-determine-the-replication-lag) - 5 min *(intermediate)* -### Lock Analysis +### Lock analysis Understand and resolve locking issues and deadlocks. diff --git a/docs/postgres-howtos/monitoring-troubleshooting/lock-analysis/how-to-understand-what-is-blocking-ddl.md b/docs/postgres-howtos/monitoring-troubleshooting/lock-analysis/how-to-understand-what-is-blocking-ddl.md index 156f246e..8a6682bc 100644 --- a/docs/postgres-howtos/monitoring-troubleshooting/lock-analysis/how-to-understand-what-is-blocking-ddl.md +++ b/docs/postgres-howtos/monitoring-troubleshooting/lock-analysis/how-to-understand-what-is-blocking-ddl.md @@ -24,14 +24,14 @@ In [day 60](/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-colu low `lock_timeout` and retries (also see: [Zero-downtime Postgres schema migrations need this: lock_timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries)). -There we used `max_attempts` set to `1000` and this is probably too many, but interesting question is: how to understand +There we used `max_attempts` set to `1000` and this is probably too many, but the interesting question is: how to understand what's blocking our DDL, if it doesn't succeed after many retries? The first thing to do is to enable `log_lock_waits`. In this case, after `deadlock_timeout` (`1s` by default; and this setting defines when deadlock detection happens), you'll see some information about our blocked session. -But we don't want to start with `lock_timeout` more than `1s` – it would be too invasive. Solution is to set a lower -`deadlock_timeout` right in session. Example (assuming that there is another session just read from table `t` and keeps +But we don't want to start with `lock_timeout` more than `1s` – it would be too invasive. The solution is to set a lower +`deadlock_timeout` right in the session. Example (assuming that there is another session that just read from table `t` and keeps the transaction open): ```sql @@ -135,9 +135,9 @@ In this case, we can do this: This should be enough for troubleshooting of failing DDL attempts. -A couple of more notes: +A couple more notes: -- Instead of anonymous DO block, it's probably better to wrap PL/pgSQL code into a function, so the `CONTEXT` and +- Instead of an anonymous DO block, it's probably better to wrap PL/pgSQL code into a function, so the `CONTEXT` and `STATEMENT` parts of the log messages don't consume too much space. - Such a function then can be invoked by `pg_cron`, so you have a permanent observability tool. But it should be taken into account that it's a whole backend that runs this, so it might be not wise to have it constantly running – diff --git a/docs/postgres-howtos/monitoring-troubleshooting/system-monitoring/flamegraphs-for-postgres.md b/docs/postgres-howtos/monitoring-troubleshooting/system-monitoring/flamegraphs-for-postgres.md index 2f34b1ee..27101274 100644 --- a/docs/postgres-howtos/monitoring-troubleshooting/system-monitoring/flamegraphs-for-postgres.md +++ b/docs/postgres-howtos/monitoring-troubleshooting/system-monitoring/flamegraphs-for-postgres.md @@ -121,7 +121,7 @@ You may need more packages, for postgres-related software – for example: apt search postgres | grep dbgsym ``` -Note, though, that not every postgres-related package has `postgres` in its name, though – e.g., for pgBouncer, you need `pgbouncer-dbgsym`. +Note, though, that not every postgres-related package has `postgres` in its name – e.g., for pgBouncer, you need `pgbouncer-dbgsym`. Once packages with debug symbols are installed, it is important not to forget to restart Postgres (and our infinite loop with `EXPLAIN .. \watch` in `psql`). @@ -183,7 +183,7 @@ Here is the result for our process running an infinite EXPLAIN loop: -It's very interesting that ~35% of CPU time is spent to analyzing if `Merge Join` is worth using, while eventually the planner picks a `Nested Loop`: +It's very interesting that ~35% of CPU time is spent analyzing if `Merge Join` is worth using, while eventually the planner picks a `Nested Loop`: ``` postgres=# explain (costs off) select from t1 join t2 using (i) @@ -198,9 +198,9 @@ where i between 1000 and 2000; (5 rows) ``` -In this case, the planning time is really low, sub-millisecond – but I encountered with cases, when planning happened to be extremely slow, many seconds or even dozens of seconds. And it turned out (thanks to flamegraphs!) that analyzing the Merge Join paths was the reason, so with "set enable_mergejoin = off" the planning time dropped to very low, sane values. But this is another story. +In this case, the planning time is really low, sub-millisecond – but I encountered cases when planning happened to be extremely slow, many seconds or even dozens of seconds. And it turned out (thanks to flamegraphs!) that analyzing the Merge Join paths was the reason, so with "set enable_mergejoin = off" the planning time dropped to very low, sane values. But this is another story. -## Some good mate +## Some good materials - Brendan Gregg's books: "Systems Performance" and "BPF Performance Tools" - Brendan Gregg's talks – for example, ["eBPF: Fueling New Flame Graphs & more • Brendan Gregg"](https://youtube.com/watch?v=HKQR7wVapgk) (video, 67 min) - [Profiling with perf](https://wiki.postgresql.org/wiki/Profiling_with_perf) (Postgres wiki) diff --git a/docs/postgres-howtos/monitoring-troubleshooting/troubleshooting/how-to-not-get-screwed-as-a-dba.md b/docs/postgres-howtos/monitoring-troubleshooting/troubleshooting/how-to-not-get-screwed-as-a-dba.md index af8d4dd2..22c88baf 100644 --- a/docs/postgres-howtos/monitoring-troubleshooting/troubleshooting/how-to-not-get-screwed-as-a-dba.md +++ b/docs/postgres-howtos/monitoring-troubleshooting/troubleshooting/how-to-not-get-screwed-as-a-dba.md @@ -35,7 +35,7 @@ useful for DBAs/DBREs in large companies. 3. TEST backups. This is the most critical part. An untested backup is a _Schrödinger's Backup_ – the condition of any backup is unknown until a restore is attempted. Automate testing. -4. Partial restore automation and speedup: in some cases, it is necessary to be able to recover a manually deleted data, +4. Partial restore automation and speedup: in some cases, it is necessary to be able to recover manually deleted data, not a whole database. In this case, to speed up recovery, consider options: (a) special delayed replica; (b) frequent cloud snapshots + PITR; (c) DBLab with hourly snapshots and PITR on a clone. @@ -45,12 +45,12 @@ useful for DBAs/DBREs in large companies. Without doubt, backups are the most important topic in database administration. Getting screwed in this area is the worst nightmare of any DBA. Pay maximum attention to backups, learn from other people's mistakes, not yours. -Reliable backup system is, perhaps, one of the biggest reasons why managed Postgres services are preferred in some -organizations. But again: don't trust blindly - study all the details, and test them yourself. +A reliable backup system is, perhaps, one of the biggest reasons why managed Postgres services are preferred in some +organizations. But again: don't trust blindly – study all the details, and test them yourself. ## 2) Corruption control -1. Enable [data checksums](/docs/postgres-howtos/database-administration/backup-recovery/how-to-enable-data-checksums-without-downtime) +1. Enable [data checksums](/docs/postgres-howtos/database-administration/maintenance/how-to-enable-data-checksums-without-downtime) 2. Be careful with OS / `glibc` upgrades – avoid index corruption. diff --git a/docs/postgres-howtos/performance-optimization/benchmarks/how-to-benchmark.md b/docs/postgres-howtos/performance-optimization/benchmarks/how-to-benchmark.md index 1d2f36e1..953febd0 100644 --- a/docs/postgres-howtos/performance-optimization/benchmarks/how-to-benchmark.md +++ b/docs/postgres-howtos/performance-optimization/benchmarks/how-to-benchmark.md @@ -24,7 +24,7 @@ useful, and correct. In this article, we assume that the following principles are followed: -1. **NOT A SHARED ENV:** The whole machine is under our solely use (nobody else is using it), we aim to study the +1. **NOT A SHARED ENV:** The whole machine is under our sole use (nobody else is using it), we aim to study the behavior of Postgres as a whole, with all its components (vs. microbenchmarks such as studying a particular query via using `EXPLAIN` or focusing on underlying components such as disk and filesystem performance). @@ -44,11 +44,11 @@ In this article, we assume that the following principles are followed: ## Benchmark structure Benchmark is a kind of database experiment, where, in general case, we use multiple sessions to DBMS and study the -behavior of the system as a whole, and it's all or particular components (e.g., buffer pool, checkpointer, replication). +behavior of the system as a whole, and its all or particular components (e.g., buffer pool, checkpointer, replication). Each benchmark run should have a well-defined structure. In general, it contains two big parts: -1. **INPUT:** everything we have or define before conducting the database – where we run the benchmark, how the system +1. **INPUT:** everything we have or define before conducting the database experiment – where we run the benchmark, how the system was configured, what DB and workload we use, what change we aim to study (to compare the behavior before and after the change). @@ -64,7 +64,7 @@ recommendations that can help you avoid mistakes and improve the general quality Of course, some of the things can be omitted, if needed. But in general case, it is recommended to automate documentation and artifact collection for all experiments, so it would be easy to study the details later. You can -find [here](https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/issues) +find [here](https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks) some good examples of benchmarks performed for specific purposes (e.g., to study pathological subtransaction behavior or to measure the benefits of enabling `wal_compression`). @@ -108,12 +108,12 @@ some examples: - varying scale: different number of clients working with database or different table sizes - different filesystems -It is not recommended to consider schema changes of changes in SQL queries as "delta" because: +It is not recommended to consider schema changes or changes in SQL queries as "delta" because: - such workload changes usually happen at a very high pace - full-fledged benchmarking is very expensive - it is possible to study schema and query changes in shared environments, focusing on IO metrics (BUFFERS!), achieving - high level of time and cost efficiency (see [@Database_Lab](https://twitter.com/Database_Lab)) + a high level of time and cost efficiency (see [@Database_Lab](https://twitter.com/Database_Lab)) ## OUTPUT: collect artifacts @@ -148,7 +148,7 @@ Some tips (far from being complete): our database system, but we actually observe the behavior of, say, cloud disk throttling or filesystem limitations instead. In such cases we need to think how to tune our input to avoid such bottlenecks, to perform useful experiments. -3. In some cases, it is, vice versa, very desired to reach some kind saturation – for example, if we study the speed of +3. In some cases, it is, vice versa, very desired to reach some kind of saturation – for example, if we study the speed of `pg_dump` or `pg_restore`, we may want to observe our disk system saturated, and we tune the input (e.g. how exactly we `pg_dump` – how many parallel workers we use, is compression involved, is network involved, etc.) so the desired saturation is indeed reached, and we can demonstrate it. diff --git a/docs/postgres-howtos/performance-optimization/benchmarks/pre-and-post-steps-for-benchmark-iterations.md b/docs/postgres-howtos/performance-optimization/benchmarks/pre-and-post-steps-for-benchmark-iterations.md index 9a704c7c..b665059b 100644 --- a/docs/postgres-howtos/performance-optimization/benchmarks/pre-and-post-steps-for-benchmark-iterations.md +++ b/docs/postgres-howtos/performance-optimization/benchmarks/pre-and-post-steps-for-benchmark-iterations.md @@ -21,13 +21,13 @@ estimated_time: 5 min --- -When conducting a Postgres benchmark (see [How to benchmark](/docs/postgres-howtos/performance-optimization/indexing/how-to-benchmark)), quite often we need to +When conducting a Postgres benchmark (see [How to benchmark](/docs/postgres-howtos/performance-optimization/benchmarks/how-to-benchmark)), quite often we need to run multiple benchmark iterations on the same setup. It may be reasonable to perform the same series of unified steps before and after each iteration: 1. Before: flush the caches (or, conversely, warm them up). 2. Before: reset cumulative statistics. -3. After: save statistics and other forms of benchmark artefacts. +3. After: save statistics and other forms of benchmark artifacts. The approach described here allows conducting benchmarks in a unified way. diff --git a/docs/postgres-howtos/performance-optimization/index.md b/docs/postgres-howtos/performance-optimization/index.md index 968c14aa..21c8cc07 100644 --- a/docs/postgres-howtos/performance-optimization/index.md +++ b/docs/postgres-howtos/performance-optimization/index.md @@ -8,9 +8,9 @@ description: Master PostgreSQL performance optimization with practical guides co Master PostgreSQL performance optimization with practical guides covering query tuning, indexing strategies, and system optimization. -## Guides by Category +## Guides by category -### Query Tuning +### Query tuning Learn how to analyze and optimize slow queries using EXPLAIN, pg_stat_statements, and other powerful tools. diff --git a/docs/postgres-howtos/performance-optimization/indexing/how-to-find-redundent-indexes.md b/docs/postgres-howtos/performance-optimization/indexing/how-to-find-redundent-indexes.md index aa590b80..490fd15c 100644 --- a/docs/postgres-howtos/performance-optimization/indexing/how-to-find-redundent-indexes.md +++ b/docs/postgres-howtos/performance-optimization/indexing/how-to-find-redundent-indexes.md @@ -38,8 +38,8 @@ Note that `(a)` is redundant to `(a, b)` but not to `(b, a)`. Accordingly, `(b)` Further, we'll also assume that: -- unique indexes should not be considered in this kind of analysis because they have special purpose; -- indexes on expression follow the same rules – we just consider each expression similarly as columns, and expression +- unique indexes should not be considered in this kind of analysis because they have a special purpose; +- indexes on expressions follow the same rules – we just consider each expression similarly as columns, and expression values should be fully matched; - in case of partial indexes, the conditions should be fully matched (this rule can be adjusted with certain assumptions, but we won't do that); @@ -52,7 +52,7 @@ The same [6 reasons that we discussed for the unused indexes](/docs/postgres-how ## General algorithm of unused indexes cleanup 1. Using the query provided below, identify the sets of redundant indexes. It is enough to analyze only one node in a - cluster – e.g. the primary. It doesn't matter when stats were reset because this analysis is based only the static + cluster – e.g. the primary. It doesn't matter when stats were reset because this analysis is based only on the static information (DB structure). 2. For each index that is considered redundant, perform a manual analysis to avoid mistakes. If not fully sure, remove diff --git a/docs/postgres-howtos/performance-optimization/indexing/how-to-find-unused-indexes.md b/docs/postgres-howtos/performance-optimization/indexing/how-to-find-unused-indexes.md index c6b7c19c..47287d2a 100644 --- a/docs/postgres-howtos/performance-optimization/indexing/how-to-find-unused-indexes.md +++ b/docs/postgres-howtos/performance-optimization/indexing/how-to-find-unused-indexes.md @@ -61,7 +61,7 @@ Thus, having a routine procedure for periodic analysis and cleanup of unused ind 1-3 above. 5. As a result, build a list of indexes that can be reliably named as "unused" – we know that we didn't use them during - significant time, neither on the primary nor replicas, on all production systems we can observe. + a significant time, neither on the primary nor on replicas, on all production systems we can observe. 6. For each index in the list drop it using `DROP INDEX CONCURRENTLY`. diff --git a/docs/postgres-howtos/performance-optimization/indexing/how-to-monitor-index-operations.md b/docs/postgres-howtos/performance-optimization/indexing/how-to-monitor-index-operations.md index 6250392a..068d8efc 100644 --- a/docs/postgres-howtos/performance-optimization/indexing/how-to-monitor-index-operations.md +++ b/docs/postgres-howtos/performance-optimization/indexing/how-to-monitor-index-operations.md @@ -92,7 +92,7 @@ How this query works: presented in the column "phase" of the output. 3. Index name (a temporary one in case of CIC/RC), table name are presented (using the useful trick to convert OIDs to - names – note, e.g., `index_relid::regclass as index_name`). Additionally, the table size which is essential to form + names – note, e.g., `index_relid::regclass as index_name`). Additionally, the table size is presented, which is essential to form expectations of overall duration – the bigger the table is, the longer the index creation is going to take. 4. `pg_stat_activity` (`pgsa`) provides a lot of additional useful information: @@ -101,7 +101,7 @@ How this query works: - the moment when the work has started (`query_start`), allowing us to understand the elapsed time (`query_duration`) - `wait_event_type` & `wait_event` to understand what the process is currently waiting on - - it also used (in a separate sub-query) to get the information of the session that blocks our process, when such an + - it is also used (in a separate sub-query) to get the information of the session that blocks our process, when such an event occurs (`current_locker_pid`, `current_locker_query`) 5. Function `format(...)` is very useful to consolidate data in convenient form without having to worry about `NULL`s, diff --git a/docs/postgres-howtos/performance-optimization/indexing/index-maintenance.md b/docs/postgres-howtos/performance-optimization/indexing/index-maintenance.md index 3217e123..6eeb70ea 100644 --- a/docs/postgres-howtos/performance-optimization/indexing/index-maintenance.md +++ b/docs/postgres-howtos/performance-optimization/indexing/index-maintenance.md @@ -48,7 +48,7 @@ where not indisvalid; A bit more comprehensive query can be found in [Postgres DBA](https://github.com/NikolayS/postgres_dba/). When analyzing this list, keep in mind that an invalid index may be a normal situation if this is an index that is being -built or rebuild by `CREATE INDEX CONCURRENTLY` / `REINDEX CONCURRENTLY`, so it is worth also checking +built or rebuilt by `CREATE INDEX CONCURRENTLY` / `REINDEX CONCURRENTLY`, so it is worth also checking `pg_stat_activity` to identify such processes. The other invalid indexes have to be rebuilt (`REINDEX CONCURRENTLY`) or dropped (`DROP INDEX CONCURRENTLY`). @@ -82,19 +82,19 @@ When searching for unused indexes, be careful and avoid mistakes: next month. 2) Don't forget to analyze all the nodes that receive workload – the primary and all replicas. An index that looks unused on the primary may be needed on a replica. -3) If you have multiple installation of your system, make sure you analyzed all of them or at least representative +3) If you have multiple installations of your system, make sure you analyzed all of them or at least a representative portion of them. Once unused indexes are identified reliably, they need to be dropped using `DROP INDEX CONCURRENTLY`. -Can we soft-drop index ("hide" it from the planner to ensure that planner behavior doesn't change and if so, proceed +Can we soft-drop an index ("hide" it from the planner to ensure that planner behavior doesn't change and if so, proceed with real dropping, otherwise quickly reverting to the original state)? There is no simple answer here, unfortunately: 1) [HypoPG 1.4.0](https://github.com/HypoPG/hypopg/releases/tag/1.4.0) has a feature to "hide" indexes – this is very - useful, but you need to install it and, more importantly, and it might be challenging to use it for whole workload, + useful, but you need to install it and, more importantly, it might be challenging to use it for the whole workload, since you need to call `hypopg_hide_index(oid)` for it. 2) Some people use a trick with setting `indisvalid` to `false` to hide an index from the planner – but there is a - reliable opinion that this is a not safe approach; see + reliable opinion that this is not a safe approach; see [Peter Geoghegan's Tweet](https://twitter.com/petervgeoghegan/status/1599191964045672449): > It's unsafe, basically. Though hard to say just how likely it is to break. Here is one hazard that I know of: in diff --git a/docs/postgres-howtos/performance-optimization/indexing/over-indexing.md b/docs/postgres-howtos/performance-optimization/indexing/over-indexing.md index ab08654c..f0e1fcea 100644 --- a/docs/postgres-howtos/performance-optimization/indexing/over-indexing.md +++ b/docs/postgres-howtos/performance-optimization/indexing/over-indexing.md @@ -83,7 +83,7 @@ What to do: ## Indexes and fastpath=false (LWLock:LockManager contention) -Have you noticed on the pictures above that when we reach index count 15, the nature of the curves changes, showing a +Have you noticed in the pictures above that when we reach index count 15, the nature of the curves changes, showing a worse degradation than the linear trend that was observed before? Let's understand why this happens and how to deal with it. @@ -109,7 +109,7 @@ How to check it – assuming we have a table `t1`: You will see that for N indexes on `t1`, N+1 `AccessShareLock` relation-level locks have been acquired. -For first 16 locks, you'll see `true` in the `pg_locks.fastpath` column. For 17th and further, it is going to be +For the first 16 locks, you'll see `true` in the `pg_locks.fastpath` column. For 17th and further, it is going to be `false`. This threshold is hard-coded in constant [FP_LOCK_SLOTS_PER_BACKEND](https://gitlab.com/postgres/postgres/blob/22655aa23132a0645fdcdce4b233a1fff0c0cf8f/src/include/storage/proc.h#L85). When `fastpath=false`, Postgres lock manager uses a slower, but more comprehensive method to acquire locks. Details can @@ -117,10 +117,10 @@ be found [here](https://gitlab.com/postgres/postgres/blob/22655aa23132a0645fdcdce4b233a1fff0c0cf8f/src/backend/storage/lmgr/README#L70). In a highly concurrent environment, if we have `fastpath=false` locks, we might start observing `LWLock` contention, -sometime a serious one – a lot of active sessions with `wait_event='LockManager'` (or `lock_manager` in PG13 or older) +sometimes a serious one – a lot of active sessions with `wait_event='LockManager'` (or `lock_manager` in PG13 or older) in `pg_stat_activity`. -This might happen both on the primary or on replicas, when two conditions are met: +This might happen both on the primary and on replicas, when two conditions are met: 1. High QPS – say 100 or more (depending on workload and hardware resources) for the observed query 2. `fastpath=false` locks due to more than 16 relations involved (in Postgres, both tables and indexes are considered diff --git a/docs/postgres-howtos/performance-optimization/indexing/rebuild-indexes-without-deadlocks.md b/docs/postgres-howtos/performance-optimization/indexing/rebuild-indexes-without-deadlocks.md index d96ca414..84c96c88 100644 --- a/docs/postgres-howtos/performance-optimization/indexing/rebuild-indexes-without-deadlocks.md +++ b/docs/postgres-howtos/performance-optimization/indexing/rebuild-indexes-without-deadlocks.md @@ -57,10 +57,10 @@ To address this, we can use this approach: 2. Assuming we want to use N reindexing sessions, build the full list of indexes, with the table names they belong to, and "assign" each table to a particular reindexing session. See the query below that does it. -3. Using this "assignment", divide the whole list of indexes to N separate lists, so all the indexes for a particular +3. Using this "assignment", divide the whole list of indexes into N separate lists, so all the indexes for a particular table are present only in a single list – and now we can just run N sessions using these N lists. -For the step 2, here is a query that can help: +For step 2, here is a query that can help: ```sql \set NUMBER_OF_SESSIONS 10 diff --git a/docs/postgres-howtos/performance-optimization/monitoring/ad-hoc-monitoring.md b/docs/postgres-howtos/performance-optimization/monitoring/ad-hoc-monitoring.md index 58fd4844..a0150e17 100644 --- a/docs/postgres-howtos/performance-optimization/monitoring/ad-hoc-monitoring.md +++ b/docs/postgres-howtos/performance-optimization/monitoring/ad-hoc-monitoring.md @@ -28,7 +28,7 @@ In some cases, we need to observe some values in Postgres or the environment it' Being able to organize an "ad hoc" observation on specific metrics is an important skill to master. Here we'll describe some tips and an approach that you can find useful. -In some cases, you can quickly install tools like [Netdata](https://netdata.cloud) (a very powerful modern monitoring that can be quickly installed, has a Postgres plugin or ad-hoc console tools such as [pgCenter](https://github.com/lesovsky/pgcenter) or [pg_top](https://pg_top.gitlab.io). But you still may want to monitor some specific aspects manually. +In some cases, you can quickly install tools like [Netdata](https://netdata.cloud) (a very powerful modern monitoring that can be quickly installed, has a Postgres plugin) or ad-hoc console tools such as [pgCenter](https://github.com/lesovsky/pgcenter) or [pg_top](https://pg_top.gitlab.io). But you still may want to monitor some specific aspects manually. Below, we assume that we are using Linux, but most of the considerations can be applied to macOS or BSD systems as well. @@ -42,7 +42,7 @@ Below, we assume that we are using Linux, but most of the considerations can be - prefer collecting data in a form useful for programmed processing (e.g., CSV) ## An example -Let's assume we need to collect samples `pg_stat_activity` (`pgsa`) to study long-running transactions – those transactions that last longer than 1 minute. +Let's assume we need to collect samples of `pg_stat_activity` (`pgsa`) to study long-running transactions – those transactions that last longer than 1 minute. Here is the recipe – and below we discuss it in detail. @@ -82,7 +82,7 @@ The benefits here are straightforward: if you have internet connectivity issues ## How to use loops / batches Some programs you'll use support batched reporting (examples: `iostat -x 5`, `top -b -n 100 -d 5`), some don't support it. In the latter case, use a `while` loop. -I prefer using an infinite loop like `while sleep 5; do ... ; done` – this approach has a small downside – it starts with sleeping first, and only then perform useful work – but it has a benefit that most of the time, you can interrupt using `Ctrl-C`. +I prefer using an infinite loop like `while sleep 5; do ... ; done` – this approach has a small downside – it starts with sleeping first, and only then performs useful work – but it has a benefit that most of the time, you can interrupt using `Ctrl-C`. ## Use psql options: -X, -A, -t @@ -94,13 +94,13 @@ Best practices of using `psql` for observability-related sampling (and work auto There are several ways to produce a CSV: - use psql's command `\copy` – in this case, results will be saved to file on client's side - `copy (...) to '/path/to/file'` – this will save results on server (there might be permissions issue if path is not writable for OS user under which Postgres is running) -- `psql --csv -F,` – produce a CSV (but there may be issues with escaping values collide with field separator) +- `psql --csv -F,` – produce a CSV (but there may be issues with escaping values that collide with the field separator) - `copy (...) to stdout` – this approach is, perhaps, most convenient for the sampling/logging purposes ## Master log-fu: don't lose STDERR, have timestamps, append For later analysis (who knows what we'll decide to check a couple of hours later?) it is better to save everything to a file. -But it is also critically important not to lose the errors – usually, they are printed to `STDERR`, so we either need to write them to a separate file. We also might want not to lose the existing content of the file so instead of just overwriting (`>`) we want to append (`>>`): +But it is also critically important not to lose the errors – usually, they are printed to `STDERR`, so we need to write them to a separate file. We also might want not to lose the existing content of the file so instead of just overwriting (`>`) we want to append (`>>`): ```shell command 2>>error.log >>messages.log ``` @@ -110,9 +110,9 @@ Or just redirect everything to a single file: command &>>everything.log ``` -If you want to both see everything and log it, use `tee` – or, with append mode, `tee -a` (here, `2>&1` redirects `STDERR` to `STDOUT` first, an then `tee` gets everything from `STDOUT`): +If you want to both see everything and log it, use `tee` – or, with append mode, `tee -a` (here, `2>&1` redirects `STDERR` to `STDOUT` first, and then `tee` gets everything from `STDOUT`): ```shell -commend 2>&1 | tee -a everything.log +command 2>&1 | tee -a everything.log ``` If the output you have lacks timestamps (not the case with the psql snippet we used above though), then use `ts` to prepend each line with a timestamp: @@ -126,7 +126,7 @@ Finally, it is usually wise to name the file with result with some details and c ```shell command 2>&1 \ | ts \ - | tee -a observing_our_comand_$(date +%Y%m%d).log + | tee -a observing_our_command_$(date +%Y%m%d).log ``` One downside of using `tee` is that, in some cases, you might accidentally stop it (e.g., pressing `Ctrl-C` in a wrong `tmux` window/pane). Due to this, some people prefer using `nohup ... &` to run observability actions in background and observing the result using `tail -f`. diff --git a/docs/postgres-howtos/performance-optimization/monitoring/how-to-analyze-heavyweight-locks-part-1.md b/docs/postgres-howtos/performance-optimization/monitoring/how-to-analyze-heavyweight-locks-part-1.md index 74436326..626bc39d 100644 --- a/docs/postgres-howtos/performance-optimization/monitoring/how-to-analyze-heavyweight-locks-part-1.md +++ b/docs/postgres-howtos/performance-optimization/monitoring/how-to-analyze-heavyweight-locks-part-1.md @@ -22,14 +22,14 @@ estimated_time: 5 min Heavyweight locks, both relation- and row-level, are acquired by a query and always held until the end of the -transaction this query belongs to. So, important principle to remember: once acquired, a lock is not released until +transaction this query belongs to. So, an important principle to remember: once acquired, a lock is not released until `COMMIT` or `ROLLBACK`. Docs: [Explicit locking](https://postgresql.org/docs/current/explicit-locking.html). A few notes about this doc: - The title "Explicit Locking" might seem misleading – it actually describes the levels of locks that can be acquired implicitly by any statement, not just explicitly via `LOCK`. -- This page also contains a very useful table, "Conflicting Lock Modes", that helps understand the rules according which +- This page also contains a very useful table, "Conflicting Lock Modes", that helps understand the rules according to which certain locks cannot be acquired due to conflicts and need to wait until the transaction holding such locks finishes, releasing the "blocking" locks. This article has an alternative table that might be also helpful: [PostgreSQL rocks, except when it blocks: Understanding locks](https://citusdata.com/blog/2018/02/15/when-postgresql-blocks/) @@ -145,7 +145,7 @@ Notes: present during our transaction. - Again: **all** indexes are locked with `AccessShareLock`. - In this case, all locks are granted. One might think it is always so with `AccessShareLock`, but it's not – if there - is a granted or **pending** `AccessExclusiveLock` (the "strongest" on), then our attempt to acquire + is a granted or **pending** `AccessExclusiveLock` (the "strongest" one), then our attempt to acquire an `AccessShareLock` will be in the pending state. When might a pending `AccessExclusiveLock` occur? If there is an attempt of `AccessExclusiveLock` (e.g. `ALTER TABLE`), but there is some long-lasting `AccessShareLock` – a "sandwich" situation. This scenario can lead to downtimes when, during an attempt to deploy a very diff --git a/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-transaction-id-wraparound-risks.md b/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-transaction-id-wraparound-risks.md index 299829ec..8e12c5f1 100644 --- a/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-transaction-id-wraparound-risks.md +++ b/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-transaction-id-wraparound-risks.md @@ -120,7 +120,7 @@ limit 25; ## Alerts -If we ages grow above certain threshold (usually 200M, see +If the ages grow above a certain threshold (usually 200M, see [autovacuum_freeze_max_age](https://postgresqlco.nf/doc/en/param/autovacuum_freeze_max_age/)), this is a sign that something is blocking normal autovacuum work. diff --git a/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-xmin-horizon.md b/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-xmin-horizon.md index 9f8beb18..8ff468f8 100644 --- a/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-xmin-horizon.md +++ b/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-xmin-horizon.md @@ -26,12 +26,12 @@ estimated_time: 6 min Previously, we discussed -[how to implement monitoring for the risks of XID (transaction ID) and MultiXID wraparound](/docs/postgres-howtos/performance-optimization/statistics/how-to-monitor-transaction-id-wraparound-risks). +[how to implement monitoring for the risks of XID (transaction ID) and MultiXID wraparound](/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-transaction-id-wraparound-risks). That type of check is critical and a must-have in any monitoring. However, while it helps you understand the risk level, it doesn't reveal the root cause – something that you'll definitely need for your XID wraparound postmortem, when applying the "Five Whys" method (just kidding, we're going to -improve our monitoring and have autovacuum behavior control, so none of us will ever experience a XID wraparound in +improve our monitoring and have autovacuum behavior control, so none of us will ever experience an XID wraparound in production). This problem can be solved with the `xmin` horizon monitoring. And this very check is also helpful in understanding @@ -77,10 +77,10 @@ The "`xmin` horizon" represents the XID of the oldest snapshot of data that must ## What about bloat? -If `xmin` horizon doesn't progress for short period of time, blocking `autovacuum`, it is not a problem – this normally +If `xmin` horizon doesn't progress for a short period of time, blocking `autovacuum`, it is not a problem – this normally happens often. -But if this happens for long period of time, and `xmin` horizon is far in the past, it can cause two big problems: +But if this happens for a long period of time, and `xmin` horizon is far in the past, it can cause two big problems: - XID/MultiXID wraparound, as discussed; - higher bloat growth: inability to delete dead tuples now leads to massive deletes of them later, when `xmin` horizon @@ -122,7 +122,7 @@ Here, the indicators of a problem are: horizon) - `removable cutoff: 784, which was 112449 XIDs old when operation ended` – this tells us that the XID horizon is 784 and its age is 112449 – so, the `xmin` horizon (the data version that is still considered needed) is more than 112k - transaction behind in the past, at the moment when `autovacuum` finished this processing attempt. + transactions behind in the past, at the moment when `autovacuum` finished this processing attempt. This indicates that the `xmin` horizon is far behind the current moment, and something is holding it in the distant past. To understand what it is, we need to check several system views. @@ -131,49 +131,80 @@ past. To understand what it is, we need to check several system views. An example query: + ```sql with bits as ( select + txid_snapshot_xmin(txid_current_snapshot()) as snapshot_xmin, + -- Primary client backend snapshots can hold back cleanup of user table tuples. ( select backend_xmin from pg_stat_activity - order by age(backend_xmin) desc nulls last + where backend_xmin is not null + order by age(backend_xmin) desc limit 1 - ) as xmin_pg_stat_activity, + ) as data_xmin_pg_stat_activity, + -- Replication slot xmin can hold back cleanup of user table tuples. ( select xmin from pg_replication_slots - order by age(xmin) desc nulls last + where xmin is not null + order by age(xmin) desc limit 1 - ) as xmin_pg_replication_slots, + ) as data_xmin_pg_replication_slots, + -- Logical replication slot catalog_xmin can hold back cleanup of system catalog tuples. + ( + select catalog_xmin + from pg_replication_slots + where catalog_xmin is not null + order by age(catalog_xmin) desc + limit 1 + ) as catalog_xmin_pg_replication_slots, + -- Standby feedback can propagate standby snapshots to the primary. ( select backend_xmin from pg_stat_replication - order by age(backend_xmin) desc nulls last + where backend_xmin is not null + order by age(backend_xmin) desc limit 1 - ) as xmin_pg_stat_replication, + ) as data_xmin_pg_stat_replication, + -- Prepared transactions keep their transaction ID until COMMIT/ROLLBACK PREPARED. ( select transaction from pg_prepared_xacts - order by age(transaction) desc nulls last + order by age(transaction) desc limit 1 - ) as xmin_pg_prepared_xacts + ) as data_xmin_pg_prepared_xacts ) select *, - age(xmin_pg_stat_activity) as xmin_pgsa_age, - age(xmin_pg_replication_slots) as xmin_pgrs_age, - age(xmin_pg_stat_replication) as xmin_pgsr_age, - age(xmin_pg_prepared_xacts) as xmin_pgpx_age, + age(data_xmin_pg_stat_activity) as data_xmin_pg_stat_activity_age, + age(data_xmin_pg_replication_slots) as data_xmin_pg_replication_slots_age, + age(catalog_xmin_pg_replication_slots) as catalog_xmin_pg_replication_slots_age, + age(data_xmin_pg_stat_replication) as data_xmin_pg_stat_replication_age, + age(data_xmin_pg_prepared_xacts) as data_xmin_pg_prepared_xacts_age, greatest( - age(xmin_pg_stat_activity), - age(xmin_pg_replication_slots), - age(xmin_pg_stat_replication), - age(xmin_pg_prepared_xacts) - ) as xmin_horizon_age + age(data_xmin_pg_stat_activity), + age(data_xmin_pg_replication_slots), + age(data_xmin_pg_stat_replication), + age(data_xmin_pg_prepared_xacts) + ) as data_horizon_age, + greatest( + age(data_xmin_pg_stat_activity), + age(data_xmin_pg_replication_slots), + age(data_xmin_pg_stat_replication), + age(data_xmin_pg_prepared_xacts), + age(catalog_xmin_pg_replication_slots) + ) as catalog_horizon_age from bits; ``` + Note that the `min(...)` function cannot be applied to XID values directly, because of their nature (32-bit -and `rotation`) – casting XID to `int` doesn't exist for good reason. But the` age(XID)` function is helpful here. So -instead of considering `xmin_horizon` values, we need to deal with `xmin_horizon_age` instead. +and `rotation`) – casting XID to `int` doesn't exist for good reason. But the `age(XID)` function is helpful here. So +instead of considering raw `xmin` values alone, we need to deal with horizon ages. + +The query separates `data_horizon_age` from `catalog_horizon_age`. `pg_replication_slots.xmin` can hold back cleanup of +user table tuples, while `pg_replication_slots.catalog_xmin` can hold back cleanup of system catalog tuples for logical +replication. Keeping these as separate metrics avoids conflating different failure modes and remediation steps. The raw +`snapshot_xmin` column is included as a cross-version anchor for checking what a fresh transaction can currently see. diff --git a/docs/postgres-howtos/performance-optimization/monitoring/how-to-reduce-wal-generation-rates.md b/docs/postgres-howtos/performance-optimization/monitoring/how-to-reduce-wal-generation-rates.md index ec3d2a45..698b6212 100644 --- a/docs/postgres-howtos/performance-optimization/monitoring/how-to-reduce-wal-generation-rates.md +++ b/docs/postgres-howtos/performance-optimization/monitoring/how-to-reduce-wal-generation-rates.md @@ -52,7 +52,7 @@ nik=# select pg_size_pretty('3/ED5F1E0'::pg_lsn - '0/110A1E0'); (1 row) ``` -If your monitoring doesn't have it, you can understand how much of WAL data was generated per hour or day by looking at: +If your monitoring doesn't have it, you can understand how much WAL data was generated per hour or day by looking at: - `pg_wal` directory to see the WAL file names - inspecting the backups (for example, checking the names of two full backups created by @@ -61,7 +61,7 @@ If your monitoring doesn't have it, you can understand how much of WAL data was Both methods should help you to get two LSN values corresponding to two distant points of time. For further details, -see [How to understand the LSN values and WAL file name](/docs/postgres-howtos/advanced-topics/internals/lsn-values-and-wal-filenames). +see [How to understand the LSN values and WAL file name](/docs/postgres-howtos/advanced-topics/misc/lsn-values-and-wal-filenames). ## WAL metrics in query analysis @@ -139,9 +139,9 @@ Below we discuss various ideas that can help you reduce the amount of WAL genera To increase distance, we just need to increase `max_wal_size` (default `1GB`) and checkpoint_timeout (default `5min`). But this needs to be done with understanding of the trade-off: the bigger distance between - checkpoints means more WALs will need to be replayed to achieve consistency point in various situations: + checkpoints means more WALs will need to be replayed to achieve a consistency point in various situations: - - longer recover time after crashes, + - longer recovery time after crashes, - longer time to provision new nodes from backups. Still, this method is a must-have for larger setups, since it gives substantial improvement. @@ -171,6 +171,6 @@ Below we discuss various ideas that can help you reduce the amount of WAL genera 5) **Partitioning** Partitioning of large (100+ GiB) tables improves data locality for writes – for example, `UPDATE`s of a bunch of rows - could be scattered among many pages if table is not partitioned, and with partitioning schema that defines old + could be scattered among many pages if the table is not partitioned, and with a partitioning schema that defines old partitions (that receive almost no writes) and partitions with fresh data, most writes are going to be localized in the fresh partitions, which can help reduce WAL generation rates. diff --git a/docs/postgres-howtos/performance-optimization/monitoring/pg-stat-statements-part-1.md b/docs/postgres-howtos/performance-optimization/monitoring/pg-stat-statements-part-1.md index 2136a43a..73285dbc 100644 --- a/docs/postgres-howtos/performance-optimization/monitoring/pg-stat-statements-part-1.md +++ b/docs/postgres-howtos/performance-optimization/monitoring/pg-stat-statements-part-1.md @@ -29,7 +29,7 @@ There are two big branches of query optimization: Today we focus on how to read and use [pg_stat_statements](https://postgresql.org/docs/current/pgstatstatements.html), starting from basics and proceeding to using the data from it for macro optimization. ## pg_stat_statements basics -Extension `pg_stat_statements` (for short, "pgss") became standard de-facto for macro-analysis. +Extension `pg_stat_statements` (for short, "pgss") became the de-facto standard for macro-analysis. It tracks all queries, aggregating them to query groups – called "normalized queries" – where parameters are removed. @@ -50,9 +50,9 @@ Let's mention some metrics that are usually most frequently used in macro optimi 1. `calls` – how many query calls happened for this query group (normalized query) 2. `total_plan_time` and `total_exec_time` – aggregated duration for planning and execution for this group (again, remember: failed queries are not tracked, including those that failed on `statement_timeout`) 3. `rows` – how many rows returned by queries in this group -4. `shared_blks_hit` and `shared_blks_read` – number if hit and read operations from the buffer pool. Two important notes here: +4. `shared_blks_hit` and `shared_blks_read` – number of hit and read operations from the buffer pool. Two important notes here: - "read" here means a read from the buffer pool – it is not necessarily a physical read from disk, since data can be cached in the OS page cache. So we cannot say these reads are reads from disk. Some monitoring systems make this mistake, but there are cases that this nuance is essential for our analysis to produce correct results and conclusions. - - The names "blocks hit" and "blocks read" might be a little bit misleading, suggesting that here we talk about data volumes – number of blocks (buffers). While aggregation here definitely make sense, we must keep in mind that the same buffers may be read or hit multiple times. So instead of "blocks have been hit" it is better to say "block hits". + - The names "blocks hit" and "blocks read" might be a little bit misleading, suggesting that here we talk about data volumes – number of blocks (buffers). While aggregation here definitely makes sense, we must keep in mind that the same buffers may be read or hit multiple times. So instead of "blocks have been hit" it is better to say "block hits". 5. `wal_bytes` – how many bytes are written to WAL by queries in this group There are many more other interesting metrics, it is recommended to explore all of them (see [the docs](https://postgresql.org/docs/current/pgstatstatements.html)). @@ -75,21 +75,21 @@ Step 3 can be also applied not to particular normalized queries on a single host If your monitoring system supports pgss, you don't need to deal with working with snapshots manually – although, keep in mind that I personally don't know any monitoring that works with pgss perfectly, preserving all kinds of information discussed in this post (and I studied quite a few of Postgres monitoring tools). -Assuming you successfully obtained 2 snapshots of pgss (remembering timestamp when they were collected) or use proper monitoring tool, let's consider practical meaning of the three derivatives we discussed. +Assuming you successfully obtained 2 snapshots of pgss (remembering timestamp when they were collected) or use a proper monitoring tool, let's consider the practical meaning of the three derivatives we discussed. ## Derivative 1. Time-based differentiation -* `dM/dt`, where `M` is `calls` – the meaning is simple. It's QPS (queries per second). If we talk about particular group (normalized query), it's that all queries in this group have. `10,000` is pretty large so, probably, you need to improve the client (app) behavior to reduce it, `10` is pretty small (of course, depending on situation). If we consider this derivative for whole node, it's our "global QPS". +* `dM/dt`, where `M` is `calls` – the meaning is simple. It's QPS (queries per second). If we talk about a particular group (normalized query), it's that all queries in this group have. `10,000` is pretty large so, probably, you need to improve the client (app) behavior to reduce it, `10` is pretty small (of course, depending on situation). If we consider this derivative for whole node, it's our "global QPS". -* `dM/dt`, where `M` is `total_plan_time + total_exec_time` – this is the most interesting and key metric in query macro analysis targeted at resource consumption optimization (goal: reduce time spent by server to process queries). Interesting fact: it is measured in "seconds per second", meaning: how many seconds our server spends to process queries in this query group. *Very* rough (but illustrative) meaning: if we have `2 sec/sec` here, it means that we spend 2 seconds each second to process such queries – we definitely would like to have more than 2 vCPUs to do that. Although, this is a very rough meaning because pgss doesn't distinguish situations when query is waiting for some lock acquisition vs. performing some actual work in CPU (for that, we need to involve wait event analysis) – so there may be cases when the value here is high not having a significant effect on the CPU load. +* `dM/dt`, where `M` is `total_plan_time + total_exec_time` – this is the most interesting and key metric in query macro analysis targeted at resource consumption optimization (goal: reduce time spent by server to process queries). Interesting fact: it is measured in "seconds per second", meaning: how many seconds our server spends to process queries in this query group. *Very* rough (but illustrative) meaning: if we have `2 sec/sec` here, it means that we spend 2 seconds each second to process such queries – we definitely would like to have more than 2 vCPUs to do that. Although, this is a very rough meaning because pgss doesn't distinguish situations when a query is waiting for some lock acquisition vs. performing some actual work in CPU (for that, we need to involve wait event analysis) – so there may be cases when the value here is high not having a significant effect on the CPU load. * `dM/dt`, where `M` is `rows` – this is the "stream" of rows returned by queries in the group, per second. For example, `1000 rows/sec` means a noticeable "stream" from Postgres server to client. Interesting fact here is that sometimes, we might need to think how much load the results produced by our Postgres server put on the application nodes – returning too many rows may require significant resources on the client side. -* `dM/dt`, where `M` is `shared_blks_hit + shared_blks_read` - buffer operations per second (only to read data, not to write it). This is another key metric for optimization. It is worth converting buffer operation numbers to bytes. In most cases, buffer size is 8 KiB (check: show block_size;), so `500,000` buffer hits&reads per second translates to `500000 bytes/sec * 8 / 1024 / 1024 = ~ 3.8 GiB/s` of the internal data reading flow (again: the same buffer in the pool can be process multiple times). This is a significant load – you might want to check the other metrics to understand if it is reasonable to have or it is a candidate for optimization. +* `dM/dt`, where `M` is `shared_blks_hit + shared_blks_read` - buffer operations per second (only to read data, not to write it). This is another key metric for optimization. It is worth converting buffer operation numbers to bytes. In most cases, buffer size is 8 KiB (check: show block_size;), so `500,000` buffer hits&reads per second translates to `500000 bytes/sec * 8 / 1024 / 1024 = ~ 3.8 GiB/s` of the internal data reading flow (again: the same buffer in the pool can be processed multiple times). This is a significant load – you might want to check the other metrics to understand if it is reasonable to have or it is a candidate for optimization. -* `dM/dt`, where `M` is `wal_bytes` – the stream of WAL bytes written. This is relatively new metric (PG13+) and can be used to understand which queries contribute to WAL writes the most – of course, the more WAL is written, the higher pressure to physical and logical replication, and to the backup systems we have. An example of highly pathological workload here is: a series of transactions like `begin; delete from ...; rollback;` deleting many rows and reverting this action – this produces a lot of WAL not performing any useful work. (Note: that despite the `ROLLBACK` here and inability of pgss to tracks failed statements, the statements here are going to be tracked because they are successful inside the transaction.) +* `dM/dt`, where `M` is `wal_bytes` – the stream of WAL bytes written. This is relatively new metric (PG13+) and can be used to understand which queries contribute to WAL writes the most – of course, the more WAL is written, the higher pressure to physical and logical replication, and to the backup systems we have. An example of highly pathological workload here is: a series of transactions like `begin; delete from ...; rollback;` deleting many rows and reverting this action – this produces a lot of WAL not performing any useful work. (Note: that despite the `ROLLBACK` here and inability of pgss to track failed statements, the statements here are going to be tracked because they are successful inside the transaction.) --- -That's it for the part 1 of pgss-related howto, in next parts we'll talk about `dM/dc` and `%M`, and other practical aspects of pgss-based macro optimization. +That's it for the part 1 of pgss-related howto, in the next parts we'll talk about `dM/dc` and `%M`, and other practical aspects of pgss-based macro optimization. Let me know if it was useful, and please share with your colleagues and any people who work with PostgreSQL. diff --git a/docs/postgres-howtos/performance-optimization/query-tuning/from-pgss-to-explain--how-to-find-query-examples.md b/docs/postgres-howtos/performance-optimization/query-tuning/from-pgss-to-explain--how-to-find-query-examples.md index 31a7471e..76b6ed51 100644 --- a/docs/postgres-howtos/performance-optimization/query-tuning/from-pgss-to-explain--how-to-find-query-examples.md +++ b/docs/postgres-howtos/performance-optimization/query-tuning/from-pgss-to-explain--how-to-find-query-examples.md @@ -29,7 +29,7 @@ In a few hours, I'm presenting my "Seamless Postgres query optimization" tutoria ## The problem of jumping from pgss to EXPLAIN -Once a problematic `pgss` record is identified (which is the subject of query macro-optimization that we've discussed on [days 5-7](https://twitter.com/samokhvalov/status/1709069225762095258), the first thing to do is to understand the direction of optimization. +Once a problematic `pgss` record is identified (which is the subject of query macro-optimization that we've discussed on [days 5-7](https://twitter.com/samokhvalov/status/1709069225762095258)), the first thing to do is to understand the direction of optimization. Two most common basic situations of `pgss` records requiring optimization: 1. If `calls` is very high (a lot of QPS, queries per second), the main method to optimize is reduction of this number – this is to be done on client (app) side. @@ -39,7 +39,7 @@ Of course, it's also not uncommon to have a combination of these two basic cases In this post, we won't discuss how to use `EXPLAIN` and `EXPLAIN (ANALYZE, BUFFERS)`. Instead, we'll focus on finding proper materials for `EXPLAIN` – particular query examples that need to be studied and improved. -It is worth remembering that a single `pgss` records can be associated with individual queries that are executed differently – using different plans. A basic example illustrating it: +It is worth remembering that a single `pgss` record can be associated with individual queries that are executed differently – using different plans. A basic example illustrating it: ``` nik=# create table t1 as select 1::int8 as c1; SELECT 1 @@ -65,7 +65,7 @@ nik=# explain select from t1 where c1 = 2; (2 rows) ``` -– both queries here will be registered as `select * from t1 where c1 = $1` in `pgss`. But plans are different, because for `c1 = 1`, we have high selectivity, while for `c1 = 2` it is really bad (targeting all but 1 rows in the table). +– both queries here will be registered as `select * from t1 where c1 = $1` in `pgss`. But plans are different, because for `c1 = 1`, we have high selectivity, while for `c1 = 2` it is really bad (targeting all but 1 row in the table). This means that looking at just `pgss` record demonstrating poor query latency, we cannot quickly jump to using `EXPLAIN` – we need to find particular query samples to work with. @@ -74,7 +74,7 @@ Below, we discuss options to solve this problem. ## Option 1: guessing In some cases, it may be fine to guess. But, I had really bad cases when I lost a lot of time making a mistake with guessing. For example, in one case, dealing with a boolean column, I decided to use the value that had a very bad selectivity, and spent a lot of time optimizing this situation, before realizing that application code never ever is going to use it. -It might be tempting to use `pg_statistic` to improve the guesswork. But unfortunately, in general case, this doesn't work really well, because of lack of multi-column statistic (except when it's created explicitly) – without it, we're going to have unrealistic parameter variants in lots of cases. +It might be tempting to use `pg_statistic` to improve the guesswork. But unfortunately, in the general case, this doesn't work really well, because of a lack of multi-column statistics (except when it's created explicitly) – without it, we're going to have unrealistic parameter variants in lots of cases. So this method is limited and can be used only for simple cases. @@ -82,7 +82,7 @@ So this method is limited and can be used only for simple cases. It is possible to find examples in the Postgres log – of course, if they are logged (usually via the `log_min_duration_statement` parameter or the `auto_explain` extension). To find examples for a given `pgss` record, we need to be able to find association of logged queries and `pgss` records. Two options: 1. For PG14+, option [compute_query_id](https://postgresqlco.nf/doc/en/param/compute_query_id/) can provide the same queryid value that is used in pg_stat_statements, to the log entry. -2. Alternatively, we can use an excellent library [libpg_query](https://github.com/pganalyze/libpg_query; Ruby, Go, Python and other options are also available). It can be applied both to normalized (`pgss` records) and individual queries, producing so-called fingerprint, that can be then used to find the relationships we need. +2. Alternatively, we can use an excellent library [libpg_query](https://github.com/pganalyze/libpg_query; Ruby, Go, Python and other options are also available). It can be applied both to normalized (`pgss` records) and individual queries, producing a so-called fingerprint, that can be then used to find the relationships we need. In general, using Postgres logs to find query examples is a good method, but for heavily-loaded systems, where it is impossible to log all queries, it is going to supply us with very slow examples only – those that exceed [log_min_duration_statement](https://postgresqlco.nf/doc/en/param/log_min_duration_statement/) (usually set to some quite high value, e.g. `500ms`). @@ -98,7 +98,7 @@ This method can be attractive since it doesn't require us to turn on the expensi However, there are two important limitations here. -First, the column pg_stat_statements.query_id, useful to connect samples from `pg_stat_activity` (`pgsa`) with `pgss` records, was added relatively recently, in PG14. For older versions, we would end up using some regular expressions (implementation can be cumbersome/fragile) of libpg_query's fingerprints (meaning that we need to sample all `pgsa` records and then post-process them). So this method is better to use in PG14+. +First, the column pg_stat_statements.query_id, useful to connect samples from `pg_stat_activity` (`pgsa`) with `pgss` records, was added relatively recently, in PG14. For older versions, we would end up using some regular expressions (implementation can be cumbersome/fragile) or libpg_query's fingerprints (meaning that we need to sample all `pgsa` records and then post-process them). So this method is better to use in PG14+. Second, by default, `pg_stat_activity.query` is truncated to 1024 characters – this is defined by [track_activity_query_size](https://postgresqlco.nf/doc/en/param/track_activity_query_size/), which is 1024 by default. It is recommended to increase it significantly – e.g., to 10k, to allow larger queries to be sampled and analyzed. Unfortunately, changing this setting requires a restart. diff --git a/docs/postgres-howtos/performance-optimization/query-tuning/how-to-decide-if-query-too-slow.md b/docs/postgres-howtos/performance-optimization/query-tuning/how-to-decide-if-query-too-slow.md index 2122f686..787335e4 100644 --- a/docs/postgres-howtos/performance-optimization/query-tuning/how-to-decide-if-query-too-slow.md +++ b/docs/postgres-howtos/performance-optimization/query-tuning/how-to-decide-if-query-too-slow.md @@ -27,7 +27,7 @@ estimated_time: 5 min "Slow" is a relative concept. In some cases, we might be happy with query latency 1 minute (or no?), while in other scenarios, even 1 ms might seem to be too slow. -Decision when to apply optimization techniques is important for efficiency – as Donald Knuth famously stated in "The Art +Deciding when to apply optimization techniques is important for efficiency – as Donald Knuth famously stated in "The Art of Computer Programming": > The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at @@ -48,7 +48,7 @@ requires optimization. depending on the case). Of course, non-user-facing queries such as those coming from background jobs, `pg_dump`, and so on, can last longer – assuming that the next principles are met. -2. In the case of OLTP, the second question should be: is this query "read-only" or it changes the data (be it DDL or +2. In the case of OLTP, the second question should be: is this query "read-only" or does it change the data (be it DDL or just writing DML – INSERT/UPDATE/DELETE)? In this case, in OLTP, we shouldn't allow it to run longer than a second or two, unless we are 100% sure that this query won't block other queries for long. For massive writes, consider splitting them in batches so each batch doesn't last longer than 1-2 seconds. For DDL, be careful with lock @@ -64,7 +64,7 @@ requires optimization. progressing slowly, and do not run them often). 4. Finally, even if a query is relatively fast – for instance, 10ms – it might still be considered too slow if its - frequency is high. For example, 10ms query running 1,000 times per second (you can check it via + frequency is high. For example, a 10ms query running 1,000 times per second (you can check it via `pg_stat_statements.calls`), then Postgres needs to spend 10 seconds *every* second to process this group of queries. In this case, if lowering down the frequency is hard, the query should be considered slow, and an optimization attempt needs to be performed, to reduce resource consumption (the goal here is to reduce diff --git a/docs/postgres-howtos/performance-optimization/query-tuning/how-to-imitate-production-planner.md b/docs/postgres-howtos/performance-optimization/query-tuning/how-to-imitate-production-planner.md index 73d90160..b81c0b02 100644 --- a/docs/postgres-howtos/performance-optimization/query-tuning/how-to-imitate-production-planner.md +++ b/docs/postgres-howtos/performance-optimization/query-tuning/how-to-imitate-production-planner.md @@ -36,7 +36,7 @@ to support fast database cloning/branching for query optimization and database t To achieve the planner's prod/non-prod behavior parity, two components are needed: 1) Matching database settings -2) The same or very similar statistic (the content of `pg_statistic`) +2) The same or very similar statistics (the content of `pg_statistic`) ## Matching database settings @@ -63,7 +63,7 @@ Notes: FS or their settings. - The value of `shared_buffers` doesn't matter(!) – it will only affect the executor's behavior and buffer pool's hit/read ratio. What does matter for the planner is `effective_cache_size` and you can set it to a value that - significantly exceed the actual RAM available, "fooling" the planner in a good sense, achieving the goal to match the + significantly exceeds the actual RAM available, "fooling" the planner in a good sense, achieving the goal to match the production planner behavior. So you can have, say, 1 TiB of RAM and `shared_buffers = '250GB'` in production and `effective_cache_size = '750GB'`, and be able to effectively analyze and optimize queries on a small 8-GiB machine with `shared_buffers = '2GB'` and `effective_cache_size = '750GB'`. The planner will assume you have a lot of RAM when @@ -98,13 +98,13 @@ Notes: - The logical method gives the matching row counts, but the size of tables and indexes is going to be different – `relpages` is smaller in a freshly provisioned node, the bloat is not preserved, and tuples, generally, are stored in a different order (we can call this bloat "good" since we want to have it in testing environments to - match the production state). This method still enables quite efficient query optimization workflow, with an additional + match the production state). This method still enables a quite efficient query optimization workflow, with an additional idea that the importance of keeping bloat low in production becomes higher. - After dump/restore you must explicitly run `ANALYZE` (or `vacuumdb --analyze -j `) to initially gather statistics in `pg_statistic`, because `pg_restore` (or `psql`) won't run it for you. - If the database content needs to be changed to remove sensitive data, this most certainly is going to affect the planner behavior. For some queries, the impact may be quite low, but for others it can be critical, making query - optimization virtually impossible. These negative effects are generally grater than those caused by dump/restore + optimization virtually impossible. These negative effects are generally greater than those caused by dump/restore losing bloat because: - dump/restore affects `relpages` (bloat lost) and the order of tuples, but not the content of `pg_statistic` - removal of sensitive data can not only reorder tuples, produce irrelevant bloat ("bad bloat"), but also lose diff --git a/docs/postgres-howtos/schema-design/data-types/how-to-quickly-check-data-type-and-storage-size-of-a-value.md b/docs/postgres-howtos/schema-design/data-types/how-to-quickly-check-data-type-and-storage-size-of-a-value.md index 97e0e402..7ec85651 100644 --- a/docs/postgres-howtos/schema-design/data-types/how-to-quickly-check-data-type-and-storage-size-of-a-value.md +++ b/docs/postgres-howtos/schema-design/data-types/how-to-quickly-check-data-type-and-storage-size-of-a-value.md @@ -24,7 +24,7 @@ estimated_time: 5 min --- -Here is how you can quickly check data type and size of a value, not looking in documentation. +Here is how you can quickly check data type and size of a value, without looking in the documentation. ## How to check data type for a value @@ -89,9 +89,9 @@ nik=# select pg_column_size(true), pg_column_size(false); (1 row) ``` -Remembering the previous howto, [Column Tetris](/docs/postgres-howtos/advanced-topics/internals/how-to-find-the-best-order-of-columns-to-save-on-storage), here we -can conclude that not only we need 1 byte to store a bit (8x space), it becomes 8 bytes if we create a table -(`c1 boolean`, `c2 int8`), due to alignment padding – meaning that it's already 64 bits! So, in such "unfortunate" case, those +Remembering the previous howto, [Column Tetris](/docs/postgres-howtos/advanced-topics/misc/how-to-find-the-best-order-of-columns-to-save-on-storage), here we +can conclude that not only do we need 1 byte to store a bit (8x space), it becomes 8 bytes if we create a table +(`c1 boolean`, `c2 int8`), due to alignment padding – meaning that it's already 64 bits! So, in such an "unfortunate" case, those who store 'true' as text, don't lose anything at all: ```sql @@ -162,7 +162,7 @@ nik=# select pg_column_size(row(1, 2)); ## No need to remember exact function names -When working in psql, there is no need to remember function names – use `\df+` to search function name: +When working in psql, there is no need to remember function names – use `\df+` to search for a function name: ```sql nik=# \df *pg_*type* diff --git a/docs/postgres-howtos/schema-design/data-types/how-to-use-uuid.md b/docs/postgres-howtos/schema-design/data-types/how-to-use-uuid.md index 9fa1f097..0858c941 100644 --- a/docs/postgres-howtos/schema-design/data-types/how-to-use-uuid.md +++ b/docs/postgres-howtos/schema-design/data-types/how-to-use-uuid.md @@ -24,7 +24,7 @@ on [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122). - Docs: [UUID Data Type](https://postgresql.org/docs/current/datatype-uuid.html) - Additional module [uuid-ossp](https://postgresql.org/docs/current/uuid-ossp.html) -A UUID value can be generated using `get_random_uuid()`, it generates UUID version 4 +A UUID value can be generated using `gen_random_uuid()`, it generates UUID version 4 ([source code for PG16](https://github.com/postgres/postgres/blob/03749325d16c4215ecd6d6a6fe117d93931d84aa/src/backend/utils/adt/uuid.c#L405-L423)): ```sql @@ -41,7 +41,7 @@ nik=# select gen_random_uuid(); (1 row) ``` -In standard UUIDs, the version can be understood looking at the first character after the 2nd hyphen: +In standard UUIDs, the version can be understood by looking at the first character after the 2nd hyphen: ``` 08e63fed-f883-4 ... 👈 this means v4 @@ -71,7 +71,7 @@ Good materials explaining performance aspects: - [Identity Crisis: Sequence v. UUID as Primary Key](https://brandur.org/nanoglyphs/026-ids#ulids) by [@brandur](https://twitter.com/brandur) -Since Postgres doesn't support UUID v7 natively yet, there are two options to use them +Since Postgres doesn't support UUID v7 natively yet, there are two options to use them: - generate on client side - implement a helper function in Postgres. @@ -135,7 +135,7 @@ nik=# select uuid_generate_v7(); (1 row) ``` -This function also supports generating UUIDv7 values for artbitrary timestamps, which can be useful in many scenarios: +This function also supports generating UUIDv7 values for arbitrary timestamps, which can be useful in many scenarios: ``` nik=# select uuid_generate_v7('2024-10-15 01:02:03'); uuid_generate_v7 @@ -152,7 +152,7 @@ nik=# select uuid_generate_v7('2024-10-15 01:02:03'); A few notes: -1) If you use these value in the `ORDER BY` clause, the chronological order will persist. +1) If you use these values in the `ORDER BY` clause, the chronological order will persist. 2) For the first 3 values (that we generated during a few seconds) there is a common prefix, `018c1be3-e`, and with the last value that was generated slightly later, there is common prefix `018c1be`. diff --git a/docs/postgres-howtos/schema-design/data-types/uuid-v7-and-partitioning-timescaledb.md b/docs/postgres-howtos/schema-design/data-types/uuid-v7-and-partitioning-timescaledb.md index 8fe2f8d7..c989b517 100644 --- a/docs/postgres-howtos/schema-design/data-types/uuid-v7-and-partitioning-timescaledb.md +++ b/docs/postgres-howtos/schema-design/data-types/uuid-v7-and-partitioning-timescaledb.md @@ -135,7 +135,7 @@ create table my_table ( ); ``` -The default value `00000000-...00` for `id` is "fake" – it will always be replaced in trigger, based on the timestamp: +The default value `00000000-...00` for `id` is "fake" – it will always be replaced in the trigger, based on the timestamp: ```sql create or replace function t_update_uuid() returns trigger @@ -204,7 +204,7 @@ Child tables: _timescaledb_internal._hyper_2_3_chunk, ## Test queries – partition pruning -Now we just need to remember that `uuid_ts` should always participate in queries, to let planner deal with as few +Now we just need to remember that `uuid_ts` should always participate in queries, to let the planner deal with as few partitions as possible – but knowing the `id` values, we can always reconstruct the `uuid_ts` values, using `uuid_v7_to_ts()`. Note that I first disabled `seqscan` as the table `my_table` has too few rows, otherwise PostgreSQL may decide on preferring `seqscan` over index scan: diff --git a/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-check-constraint-without-downtime.md b/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-check-constraint-without-downtime.md index 3693d611..11d34a76 100644 --- a/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-check-constraint-without-downtime.md +++ b/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-check-constraint-without-downtime.md @@ -64,7 +64,7 @@ alter table t validate constraint c_id_is_positive; ``` -This scans whole table, so for a large table, it takes long time – but this query only +This scans the whole table, so for a large table, it takes a long time – but this query only acquires `ShareUpdateExclusiveLock` on the table, not blocking the sessions that run DML queries. However, a lock acquisition attempt is going to be blocked if there is `autovacuum` running in the transaction ID wraparound prevention mode and processing the table, or if there is another session that builds an index on this table or performs diff --git a/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-column.md b/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-column.md index c2705302..53cda0be 100644 --- a/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-column.md +++ b/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-column.md @@ -39,7 +39,7 @@ Two consequences of it: Regarding the latter, it's analyzed in detail in [Zero-downtime Postgres schema migrations need this: lock_timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries). -An example of graceful approach, with low `lock_timeout` and retries: +An example of a graceful approach, with low `lock_timeout` and retries: ```sql do $do$ @@ -71,10 +71,10 @@ end $do$; ``` Note that in this particular example, subtransactions are implicitly used (the `BEGIN/EXCEPTION WHEN/END` block). Which -can be a problem in case of very high XID growth rate (e.g., many writing transactions) and a long-running transaction – +can be a problem in the case of a very high XID growth rate (e.g., many writing transactions) and a long-running transaction – this can trigger SubtransSLRU contention on standbys; see: [PostgreSQL Subtransactions Considered Harmful](https://postgres.ai/blog/20210831-postgresql-subtransactions-considered-harmful). -In this case, implement the retry logic at transaction level. +In this case, implement the retry logic at the transaction level. ## DEFAULT @@ -116,13 +116,13 @@ use another default value for all future rows, we can: - use one `DEFAULT` value at column creation time, - change `DEFAULT` to a different value right after creation. -If you use very old Postgres version (pre-11), consider to use backfilling to avoid long-lasting locking. +If you use a very old Postgres version (pre-11), consider using backfilling to avoid long-lasting locking. ## NOT NULL -Adding a NOT NULL constraint (that is required for a PK [re]definition), generally, requires a full-table scan there is -no support of two-step addition to avoid long-lasting locking. However, when this constraint is needed for a new column, -we can use this trick +Adding a NOT NULL constraint (that is required for a PK [re]definition), generally, requires a full-table scan – there is +no support for two-step addition to avoid long-lasting locking. However, when this constraint is needed for a new column, +we can use this trick: 1) Use some temporary DEFAULT combined with NOT NULL at column creation: @@ -154,7 +154,7 @@ and we still need to backfill. This has to be done in batches, to avoid long-las 1. As usual, for OLTP (web and mobile apps), it is recommended to find batch size so all `UPDATE`s do not exceed 1-2 seconds. 2. To be able to efficiently find the scope for the next batch, we can create an index on the new column and existing - PK (this index may be temporarily, to support efficient batching), and then drop at. This index can be partial. For + PK (this index may be temporary, to support efficient batching), and then drop it. This index can be partial. For example, if our new column is called `id_new` and the `DEFAULT` used at column creation time was `-1`: - Create supporting index: @@ -173,7 +173,7 @@ and we still need to backfill. This has to be done in batches, to avoid long-las ``` - Control the dead tuple counts and autovacuum behavior not to allow dead tuple count to be too high (leading to - bloat) – throttle the frequency `UPDATE`s if needed and/or issue manual `VACUUM` from time to time. + bloat) – throttle the frequency of `UPDATE`s if needed and/or issue manual `VACUUM` from time to time. - If the supporting index is not needed, drop it: ```sql @@ -198,8 +198,8 @@ and we still need to backfill. This has to be done in batches, to avoid long-las (1 row) ``` - And the value stored in `pg_attribute` in `attmissingval` is that one that is used for the rows that existed before - column was created: + And the value stored in `pg_attribute` in `attmissingval` is the one that is used for the rows that existed before + the column was created: ```sql nik=# select attmissingval from pg_attribute where attrelid = 't1'::regclass::oid and attname = 'c2'; diff --git a/docs/postgres-howtos/schema-design/ddl-operations/how-to-drop-a-column.md b/docs/postgres-howtos/schema-design/ddl-operations/how-to-drop-a-column.md index b9d290b6..834430fc 100644 --- a/docs/postgres-howtos/schema-design/ddl-operations/how-to-drop-a-column.md +++ b/docs/postgres-howtos/schema-design/ddl-operations/how-to-drop-a-column.md @@ -34,8 +34,8 @@ Application code needs to stop using this column. It means that it needs to be d ## Risk 2: partial downtime Under heavy load, issuing such an alter without a low `lock_timeout` and retries is a bad idea because this statement -need to acquire AccessExclusiveLock on the table, and if an attempt to acquire it lasts a significant time (e.g. because -of existing transaction that holds any lock on this table - it can be a transaction that read a single row from this +needs to acquire AccessExclusiveLock on the table, and if an attempt to acquire it lasts a significant time (e.g. because +of an existing transaction that holds any lock on this table - it can be a transaction that read a single row from this table, or autovacuum processing this table to prevent transaction ID wraparound), then this attempt can be harmful for all current queries to this table, since it will be blocking them. This causes partial downtime in projects under load. Solution: low `lock_timeout` and retries. An example (more about this and a more advanced example can be found @@ -71,10 +71,10 @@ end $do$; ``` Note that in this particular example, subtransactions are implicitly used (the `BEGIN/EXCEPTION WHEN/END` block). Which -can be a problem in case of very high `XID` growth rate (e.g., many writing transactions) and a long-running +can be a problem in the case of a very high `XID` growth rate (e.g., many writing transactions) and a long-running transaction – this can trigger `SubtransSLRU` contention on standbys (see: [PostgreSQL Subtransactions Considered Harmful](https://postgres.ai/blog/20210831-postgresql-subtransactions-considered-harmful)). -In this case, implement the retry logic at transaction level. +In this case, implement the retry logic at the transaction level. ## Risk 3: false expectations that the data is deleted diff --git a/docs/postgres-howtos/schema-design/ddl-operations/how-to-redefine-a-PK-without-downtime.md b/docs/postgres-howtos/schema-design/ddl-operations/how-to-redefine-a-PK-without-downtime.md index 65ae4365..667c8c41 100644 --- a/docs/postgres-howtos/schema-design/ddl-operations/how-to-redefine-a-PK-without-downtime.md +++ b/docs/postgres-howtos/schema-design/ddl-operations/how-to-redefine-a-PK-without-downtime.md @@ -61,7 +61,7 @@ that are not necessarily related to PK, but they are still relevant. And eventua task. Just bear with me. **Bad news:** unfortunately, adding a NOT NULL constraint to an existing column means that Postgres will need to perform a -long (for large tables) full-table scan, during which it will an `AccessExclusiveLock` acquired by `ALTER TABLE` is +long (for large tables) full-table scan, during which an `AccessExclusiveLock` acquired by `ALTER TABLE` is going to be held. This is not what we want if we need zero-downtime operations. **Good news:** since Postgres 11, we can execute a trick, if we need to add a column with `NOT NULL` – we can benefit from @@ -79,13 +79,13 @@ happens): > > ([PG11 release notes](https://postgresql.org/docs/release/11.0/)) -And since all rows are pre-filled ("virtually", but it doesn't matter), we can have `NOT NULL` right away, avoiding long +And since all rows are pre-filled ("virtually", but it doesn't matter), we can have `NOT NULL` right away, avoiding a long wait. **Bad news:** this works only for new columns. If we deal with an existing column, and still want to add a `NOT NULL` to it, this won't work. -**Good news:** if we just need a "not null", not matter how defined, we can use a `CHECK` constraint. The good thing about +**Good news:** if we just need a "not null", no matter how defined, we can use a `CHECK` constraint. The good thing about `CHECK` constraints is that their definition can be two-phase: - first, we define a constraint `CHECK (col1 IS NOT NULL)` with flag `NOT VALID` – this is fast, not blocking other diff --git a/docs/postgres-howtos/schema-design/index.md b/docs/postgres-howtos/schema-design/index.md index 6819645c..9dbf089c 100644 --- a/docs/postgres-howtos/schema-design/index.md +++ b/docs/postgres-howtos/schema-design/index.md @@ -8,9 +8,9 @@ description: Best practices for designing efficient PostgreSQL schemas and manag Best practices for designing efficient PostgreSQL schemas and managing database objects. -## Guides by Category +## Guides by category -### DDL Operations +### DDL operations Safely perform schema changes with minimal downtime. @@ -19,7 +19,7 @@ Safely perform schema changes with minimal downtime. - [How to add a column](/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-column) - 5 min *(advanced)* - [How to add a CHECK constraint without downtime](/docs/postgres-howtos/schema-design/ddl-operations/how-to-add-a-check-constraint-without-downtime) - 5 min *(intermediate)* -### Data Types +### Data types Choose the right data types and understand their performance implications. diff --git a/docs/postgresai-howtos/how-to-install-mcp.md b/docs/postgresai-howtos/how-to-install-mcp.md index 2b24b799..c9155952 100644 --- a/docs/postgresai-howtos/how-to-install-mcp.md +++ b/docs/postgresai-howtos/how-to-install-mcp.md @@ -42,14 +42,14 @@ postgresai auth ```bash -npx postgresai auth +npx postgresai@latest auth ``` ```bash -bunx postgresai auth +bunx postgresai@latest auth ``` @@ -70,32 +70,59 @@ postgresai mcp install ```bash -npx postgresai mcp install +npx postgresai@latest mcp install ``` ```bash -bunx postgresai mcp install +bunx postgresai@latest mcp install ``` -The CLI will detect supported AI coding tools and configure MCP integration automatically. +Without arguments, `mcp install` prints a numbered menu and asks you to pick a tool: + +``` +Available AI coding tools: + 1. Cursor + 2. Claude Code + 3. Windsurf + 4. Codex +Select your AI coding tool (1-4): +``` + +To skip the prompt, pass the client name as an argument: `postgresai mcp install `, where `` is one of `cursor`, `claude-code`, `windsurf`, or `codex`. + +:::note + +`mcp install` pins the absolute path of the `pgai` binary it was invoked from into the client config. If you ran the command via `npx` or `bunx`, the pinned path points into a per-version package cache that may be garbage-collected. For a stable install, prefer `npm install -g postgresai` (or `brew install postgresai`) before running `mcp install`, or re-run `mcp install` after each CLI upgrade. + +::: ## Verify installation -After installation, your AI coding tool will have access to PostgresAI features: +After installation, restart your AI coding tool. The PostgresAI MCP server exposes 15 tools (see the [`mcp` section of the CLI reference](/docs/reference-guides/postgresai-cli-reference#command-mcp) for full details): + +- **Issues:** `list_issues`, `view_issue`, `create_issue`, `update_issue` — browse and manage issues in the PostgresAI Console. +- **Issue comments:** `post_issue_comment`, `update_issue_comment` — comment on issues. +- **Action items:** `list_action_items`, `view_action_item`, `create_action_item`, `update_action_item` — manage action items on an issue (including the approval workflow). +- **Reports:** `list_reports`, `list_report_files`, `get_report_data` — list and read checkup reports stored in the Console. +- **Files:** `upload_file`, `download_file` — upload local files to PostgresAI storage and download them back. + +To check the install landed, inspect the client config file. For Cursor: + +```bash +cat ~/.cursor/mcp.json +``` -- View and manage issues -- Access database monitoring data -- Get AI-assisted recommendations for database optimization +You should see a `postgresai` entry under `mcpServers` with `command` pointing at the `pgai` binary and `args: ["mcp", "start"]`. ## Manual configuration -If automatic installation doesn't work, you can configure MCP manually. See [PostgresAI CLI reference](/docs/reference-guides/postgresai-cli-reference) for the `mcp` command options. +If you prefer to wire up MCP by hand, edit the client config and add a `postgresai` entry under `mcpServers`. See the [`mcp install` section of the CLI reference](/docs/reference-guides/postgresai-cli-reference#mcp-install) for the exact JSON shape. ## Next steps diff --git a/docs/postgresai-howtos/how-to-work-with-issues.md b/docs/postgresai-howtos/how-to-work-with-issues.md index 1de25380..04bd8bb8 100644 --- a/docs/postgresai-howtos/how-to-work-with-issues.md +++ b/docs/postgresai-howtos/how-to-work-with-issues.md @@ -50,7 +50,7 @@ postgresai issues list postgresai issues view # Post a comment -postgresai issues post_comment "comment" +postgresai issues post-comment "comment" ``` @@ -58,13 +58,13 @@ postgresai issues post_comment "comment" ```bash # List all issues -npx postgresai issues list +npx postgresai@latest issues list # View a specific issue -npx postgresai issues view +npx postgresai@latest issues view # Post a comment -npx postgresai issues post_comment "comment" +npx postgresai@latest issues post-comment "comment" ``` @@ -72,19 +72,25 @@ npx postgresai issues post_comment "comment" ```bash # List all issues -bunx postgresai issues list +bunx postgresai@latest issues list # View a specific issue -bunx postgresai issues view +bunx postgresai@latest issues view # Post a comment -bunx postgresai issues post_comment "comment" +bunx postgresai@latest issues post-comment "comment" ``` -See [PostgresAI CLI reference](/docs/reference-guides/postgresai-cli-reference) for all available commands. +By default, `issues` commands print human-friendly YAML when stdout is a terminal, and switch to JSON when piped or redirected. Force JSON explicitly with `--json` for scripting: + +```bash +postgresai issues list --json | jq '.[] | {id, title}' +``` + +See the [PostgresAI CLI reference](/docs/reference-guides/postgresai-cli-reference#command-issues) for the full `issues` command set (including `create`, `update`, action items, and file attachments). ## Integrate with AI coding tools @@ -100,14 +106,14 @@ Issues can be resolved directly in AI coding tools that support MCP integration: -1. Install MCP: `npx postgresai mcp install` +1. Install MCP: `npx postgresai@latest mcp install` 2. Open the issue in your AI coding tool (Cursor, Claude Code, Windsurf, or Codex) 3. Follow the AI-guided steps to resolve the issue -1. Install MCP: `bunx postgresai mcp install` +1. Install MCP: `bunx postgresai@latest mcp install` 2. Open the issue in your AI coding tool (Cursor, Claude Code, Windsurf, or Codex) 3. Follow the AI-guided steps to resolve the issue diff --git a/docs/postgresai-howtos/install-postgres-ai-monitoring-from-postgresai-console.md b/docs/postgresai-howtos/install-postgres-ai-monitoring-from-postgresai-console.md index 00c497ad..a44a07b5 100644 --- a/docs/postgresai-howtos/install-postgres-ai-monitoring-from-postgresai-console.md +++ b/docs/postgresai-howtos/install-postgres-ai-monitoring-from-postgresai-console.md @@ -48,6 +48,11 @@ On the **Create PostgresAI monitoring managed instance** page: [![PostgresAI Console: Create PostgresAI monitoring managed instance page with billing, project setup, database preparation, and database connection sections](/assets/install-postgres-ai-monitoring-from-postgresai-console/install-postgres-ai-monitoring-from-postgresai-console-2.png)](/assets/install-postgres-ai-monitoring-from-postgresai-console/install-postgres-ai-monitoring-from-postgresai-console-2.png) +:::note +The console screenshot above still shows the retired **Startup** ($128/month) tier, which is no +longer offered. See [Pricing](/pricing) for current plans. +::: + ## Advanced setup (optional) Use **Advanced setup** to adjust access and provisioning details for the monitoring VM: diff --git a/docs/postgresai-howtos/joe-cli.md b/docs/postgresai-howtos/joe-cli.md new file mode 100644 index 00000000..7c4a0ca9 --- /dev/null +++ b/docs/postgresai-howtos/joe-cli.md @@ -0,0 +1,222 @@ +--- +title: Run Joe from the CLI (pgai joe) +sidebar_label: Joe from the CLI +description: Use pgai joe to get query plans, real EXPLAIN ANALYZE results, and test index ideas on ephemeral DBLab clones — right from your terminal. +keywords: + - "pgai joe" + - "postgresai cli" + - "joe bot" + - "explain analyze" + - "query optimization" + - "hypopg" +--- + +[Joe](/docs/joe-bot) is the PostgresAI SQL optimization assistant: it runs your +`EXPLAIN` / `EXPLAIN ANALYZE` requests on an ephemeral [DBLab](/docs/database-lab) +thin clone of your database, so you can analyze and optimize queries with +production-identical plans without touching production. `pgai joe` brings Joe to +the terminal (and to scripts and AI agents): plan a query, get the real +execution plan, build real or hypothetical indexes, and iterate — every result +also lands in the Joe history in the PostgresAI Console. + +:::caution dev channel +`pgai joe` and `pgai projects` ship in CLI 0.16, which is currently published +under the **`dev`** npm dist-tag — that's why the examples below run +`npx pgai@dev …` rather than plain `npx pgai`. Once 0.16 reaches `latest`, the +`@dev` suffix will no longer be needed. +::: + +## Reference + +- [PostgresAI CLI reference — `joe` command](/docs/reference-guides/postgresai-cli-reference#command-joe) +- [PostgresAI CLI reference — `projects` command](/docs/reference-guides/postgresai-cli-reference#command-projects) + +## Prerequisites + +- **Node.js 18+** (or Bun 1.0+) to run the CLI. +- A project in your organization with a **registered, active Joe instance** + (see [Joe setup](/docs/tutorials/joe-setup)). +- Your user must hold the **AllFeaturesUser** or **Admin** role in the + organization — Joe CLI commands are rejected with `403 Forbidden` otherwise. + +The CLI is published as two equivalent npm packages: `postgresai` (canonical) +and `pgai` (short wrapper). `npm install -g postgresai@dev` installs both the +`postgresai` and `pgai` binaries; `npx pgai@dev …` runs without installing. + +## Authenticate + +```bash +npx pgai@dev login +``` + +This opens your browser (OAuth with PKCE), asks you to pick an organization, +and stores the resulting API key in `~/.config/postgresai/config.json`. All +`joe` commands authenticate with this key. See the +[auth reference](/docs/reference-guides/postgresai-cli-reference#command-auth) +for storing a key directly (`--set-key`), useful in CI. + +## Find a project with Joe ready + +```bash +npx pgai@dev projects +``` + +``` +PROJECT_ID ALIAS PROJECT JOE TUNNEL +12 main-db Main DB ready yes +15 analytics Analytics no no +``` + +A `ready` value in the `JOE` column means the project has an active Joe +instance — those projects can be targeted with `--project ` below. +For projects without one, register a Joe instance first, or target a Joe +instance directly with `--instance-id `. + +:::note +Examples below use the short `pgai joe …` form for brevity; while 0.16 is on +the dev channel, run them as `npx pgai@dev joe …` (or install globally with +`npm install -g postgresai@dev`). +::: + +## Get a query plan (no execution) + +`plan` returns the `EXPLAIN` plan **without executing the query** — the fast, +safe default: + +```bash +pgai joe plan "select * from users where email = 'alice@example.com'" \ + --project main-db +``` + +``` +command 3521 · ok +plan: +Seq Scan on users (cost=0.00..1877.10 rows=1 width=142) + Filter: ((email)::text = 'alice@example.com'::text) +⚑ client-side: Seq Scan on users — no index serves this predicate; consider adding one. +``` + +The `⚑` line is a lightweight client-side hint derived from the structured +plan; the full result (plan, statistics, recommendations) is also saved to the +Joe history in the Console. + +## Get the real execution plan + +`explain` runs `EXPLAIN` **and** `EXPLAIN ANALYZE` — the query actually +executes, on the DBLab clone (never on your production database): + +```bash +pgai joe explain "select * from users where email = 'alice@example.com'" \ + --project main-db +``` + +``` +command 3522 · ok +plan: +Seq Scan on users (cost=0.00..1877.10 rows=1 width=142) + Filter: ((email)::text = 'alice@example.com'::text) + +execution plan (EXPLAIN ANALYZE): +Seq Scan on users (cost=0.00..1877.10 rows=1 width=142) (actual time=8.912..8.914 rows=1 loops=1) + Filter: ((email)::text = 'alice@example.com'::text) + Rows Removed by Filter: 99999 +Planning Time: 0.176 ms +Execution Time: 8.987 ms +… +``` + +(Sample output abridged — Joe also returns buffer/timing statistics and +optimization recommendations when available.) Because the clone shares the +production data and planner configuration, plan structure and buffer numbers +match production; timing may differ due to cache state — see +[Joe bot](/docs/joe-bot) for details. + +## Test an index idea + +Clones are writable: build a real index with `exec`, then re-check the plan. + +```bash +pgai joe exec "create index i_users_email on users (email)" --project main-db +pgai joe explain "select * from users where email = 'alice@example.com'" \ + --project main-db +``` + +Or use [HypoPG](https://github.com/HYPOPG/hypopg) hypothetical indexes — no +actual index build, instant even on huge tables (affects `plan` cost estimates +only, not real execution): + +```bash +pgai joe hypo "create index on users (email)" --project main-db +pgai joe plan "select * from users where email = 'alice@example.com'" --project main-db +pgai joe hypo reset --project main-db +``` + +(Every `joe` command except `result` needs a target: pass +`--project ` or `--instance-id ` — or set a default project +once with `pgai set-default-project ` and omit both.) + +To start over from a pristine clone: + +```bash +pgai joe reset --project main-db +``` + +## Inspect and manage clone activity + +```bash +# pg_stat_activity snapshot on the clone +pgai joe activity --project main-db + +# terminate a runaway backend on the clone +pgai joe terminate 12345 --project main-db + +# \d-family metadata: tables, indexes, sizes +pgai joe describe users --project main-db +pgai joe describe users --variant '\d+' --project main-db +``` + +## Long-running commands + +Commands are synchronous: the CLI submits the command and polls for the result +for up to 25 seconds (configurable with `--budget `). If the result +isn't ready in time — e.g. a long `EXPLAIN ANALYZE`, or a cold clone being +provisioned — the CLI exits successfully with a resume handle: + +``` +started 3523 · pending · budget 25s reached — resume: pgai joe result 3523 +``` + +Fetch the result later by command id: + +```bash +pgai joe result 3523 +``` + +## Scripting and agents + +Every `joe` subcommand (and `projects`) accepts `--json` for machine-readable +output, and `--debug` to trace API calls: + +```bash +pgai joe plan "select 1" --project main-db --json | jq '.plan_json' +``` + +Exit codes are script-friendly: `0` for a successful result (and for a +budget-expired one-shot — resume by id), `1` for a failed command or any error. + +## Troubleshooting + +- **`Project not found for id/alias/name '…'`** — run `pgai projects` to see + available projects; the match is case-insensitive on id, alias, and name. +- **`Project '…' has no Joe instance`** — `--project` requires the project to + have a registered, active Joe instance. Register one, or target a Joe + instance directly with `--instance-id `. +- **`403 Forbidden` / "Joe API v2 requires the All Features role"** — ask an + org admin to grant your user the **AllFeaturesUser** (or **Admin**) role. +- **`401`** — your stored API key is missing or expired; re-run `pgai login`. +- **Environments behind Cloudflare Access** (previews, some staging setups) — + the CLI's plain HTTPS calls may be blocked by the access layer; you may need + extra setup (e.g. a service token) or to run from an allowed network. For + non-production API endpoints, see + [environment variables](/docs/reference-guides/postgresai-cli-reference#environment-variables) + (`PGAI_API_BASE_URL`). diff --git a/docs/postgresai-howtos/postgresai-cli.md b/docs/postgresai-howtos/postgresai-cli.md index cfe54019..7bb67206 100644 --- a/docs/postgresai-howtos/postgresai-cli.md +++ b/docs/postgresai-howtos/postgresai-cli.md @@ -46,21 +46,21 @@ Authenticate via browser and store the API key locally: ```bash -postgresai auth +postgresai login ``` ```bash -npx postgresai auth +npx postgresai@latest login ``` ```bash -bunx postgresai auth +bunx postgresai@latest login ``` @@ -81,14 +81,14 @@ postgresai mcp install ```bash -npx postgresai mcp install +npx postgresai@latest mcp install ``` ```bash -bunx postgresai mcp install +bunx postgresai@latest mcp install ``` @@ -109,14 +109,14 @@ postgresai issues list ```bash -npx postgresai issues list +npx postgresai@latest issues list ``` ```bash -bunx postgresai issues list +bunx postgresai@latest issues list ``` @@ -135,14 +135,14 @@ postgresai issues view ```bash -npx postgresai issues view +npx postgresai@latest issues view ``` ```bash -bunx postgresai issues view +bunx postgresai@latest issues view ``` @@ -154,21 +154,21 @@ Post a comment: ```bash -postgresai issues post_comment "" +postgresai issues post-comment "" ``` ```bash -npx postgresai issues post_comment "" +npx postgresai@latest issues post-comment "" ``` ```bash -bunx postgresai issues post_comment "" +bunx postgresai@latest issues post-comment "" ``` diff --git a/docs/questions-and-answers.md b/docs/questions-and-answers.md index 5adeaf81..f1e867a9 100644 --- a/docs/questions-and-answers.md +++ b/docs/questions-and-answers.md @@ -98,7 +98,10 @@ Yes. Monitoring can run in PostgresAI Cloud or in your own infrastructure. We se ## What Postgres versions are supported? -postgres_ai monitoring supports Postgres 14+. DBLab Engine supports Postgres 9.6+. +PostgresAI full monitoring, express-mode checkups, and Console checkup analysis +support Postgres 14 through PostgreSQL 19. PostgreSQL 19 is currently Beta 2, +so use that version for compatibility testing rather than production workloads +until general availability. DBLab Engine supports Postgres 10+. ## Does it work with managed Postgres? @@ -121,5 +124,5 @@ Yes. PostgresAI works with: ## Where can I learn more? - [Vision & roadmap](/docs/roadmap) — The Self-Driving Postgres journey -- [Monitoring areas](/docs/howtos/monitoring-areas) — What PostgresAI monitors +- [postgres_ai monitoring](/docs/monitoring) — Observability and monitoring - [Postgres how-tos](/docs/postgres-howtos) — 100+ practical guides diff --git a/docs/reference-guides/database-lab-engine-api-reference.md b/docs/reference-guides/database-lab-engine-api-reference.md index b2ade16a..a7408484 100644 --- a/docs/reference-guides/database-lab-engine-api-reference.md +++ b/docs/reference-guides/database-lab-engine-api-reference.md @@ -16,7 +16,85 @@ DBLab API (DLE API) is a REST API. It can be used in multiple ways: - indirectly, in browser: [DBLab UI](https://postgres.ai/docs/database-lab/user-interface), being a React application, speaks to the DLE API as well DBLab API reference documentation is available at the following locations: -- [DLE 3.5.x API Reference](https://dblab.readme.io/v3.5.0/) +- [DBLab API Reference (latest)](https://dblab.readme.io/) - [DBLab 4.0.x API Reference](https://dblab.readme.io/v4.0.0/) +- [DLE 3.5.x API Reference](https://dblab.readme.io/v3.5.0/) + +The references are published using the comprehensive ReadMe service, equipped with a developer dashboard and providing code snippets in numerous languages. + +## Authentication + +All API endpoints (except `/healthz` and `/metrics`) require the `Verification-Token` header: + +```bash +curl -H "Verification-Token: YOUR_TOKEN" http://localhost:2345/status +``` + +## Endpoint summary + +### Instance + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/status` | Instance status, info, and list of clones | +| GET | `/healthz` | Health check (no auth required) | +| GET | `/metrics` | Prometheus metrics (no auth required, DLE 4.1+) | +| GET | `/instance/retrieval` | Data refresh status | +| POST | `/full-refresh` | Trigger full data refresh (DLE 4.0+) | + +### Clones + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/clones` | List all clones (DLE 4.0+) | +| POST | `/clone` | Create a clone | +| GET | `/clone/{id}` | Retrieve a clone | +| PATCH | `/clone/{id}` | Update a clone (protection status) | +| DELETE | `/clone/{id}` | Delete a clone | +| POST | `/clone/{id}/reset` | Reset a clone to a snapshot | + +### Snapshots + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/snapshots` | List all snapshots | +| GET | `/snapshot/{id}` | Retrieve a snapshot (DLE 4.0+) | +| POST | `/snapshot` | Create a snapshot (DLE 4.0+) | +| POST | `/snapshot/clone` | Create a snapshot from a clone (DLE 4.0+) | +| DELETE | `/snapshot/{id}` | Delete a snapshot (DLE 4.0+) | +| GET | `/branch/snapshot/{id}` | Retrieve a branch snapshot (DLE 4.0+) | +| POST | `/branch/snapshot` | Create a branch snapshot from clone (DLE 4.0+) | + +### Branches (DLE 4.0+) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/branches` | List all branches | +| POST | `/branch` | Create a branch | +| DELETE | `/branch/{branchName}` | Delete a branch | +| GET | `/branch/{branchName}/log` | Retrieve branch log (snapshot history) | + +### Observation (experimental) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/observation/start` | Start observation session | +| POST | `/observation/stop` | Stop observation session | +| GET | `/observation/summary/{clone_id}/{session_id}` | Get observation summary | +| GET | `/observation/download` | Download observation artifact | + +### Admin + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/admin/config` | Get config (JSON projection) | +| POST | `/admin/config` | Set config | +| GET | `/admin/config.yaml` | Get full config (YAML) | +| POST | `/admin/test-db-source` | Test source database connection | +| GET | `/admin/ws-auth` | WebSocket authentication | + +## New in DBLab Engine 4.1 -The references are published using the comprehensive ReadMe service, equipped with a developer dashboard and provides code snippets in numerous languages. +- **`/metrics` endpoint**: Prometheus metrics for monitoring (no authentication required). See [Prometheus monitoring](/docs/database-lab/prometheus-monitoring). +- **Protection leases**: The `CreateClone` and `UpdateClone` requests now accept a `protectionDurationMinutes` field for time-limited clone protection. The `Clone` response includes `protectedTill` showing when protection expires. See [Clone protection](/docs/dblab-howtos/cloning/clone-protection). +- **`clone_delete` webhook**: A new webhook trigger type for clone deletion events. See [Webhook configuration](/docs/reference-guides/database-lab-engine-configuration-reference#section-webhooks-webhook-configuration). diff --git a/docs/reference-guides/database-lab-engine-components.md b/docs/reference-guides/database-lab-engine-components.md index 2baff039..445083fa 100644 --- a/docs/reference-guides/database-lab-engine-components.md +++ b/docs/reference-guides/database-lab-engine-components.md @@ -14,10 +14,12 @@ User-defined. - Manages all other containers - Handles data retrieval and snapshot creation - Offers an HTTP API to manage snapshots and clones +- Exposes operational endpoints such as `/healthz` and `/metrics` - Generates an internal DLE RuntimeID on each start to mark related components #### How to manage Operates as a Docker container. See the [guide](/docs/dblab-howtos/administration/engine-manage) for administering DLE. +For API and metrics details, see [DBLab API reference](/docs/reference-guides/database-lab-engine-api-reference) and [Prometheus monitoring](/docs/database-lab/prometheus-monitoring). --- @@ -58,7 +60,7 @@ Container names include a DLE RuntimeID, such as `dblab_sync_bt48bvi9c0h0`. #### How to manage Automatically starts and stops. -To activate a sync instance, use the `syncInstance` option for a physical restore job in the DLE configuration file. +To activate a sync instance, set `sync.enabled: true` for the `physicalRestore` job in the DBLab Engine configuration file. See [Job `physicalRestore`](/docs/reference-guides/database-lab-engine-configuration-reference#job-physicalrestore) for the full set of `sync` options. --- @@ -179,7 +181,7 @@ Container names include a DLE RuntimeID, such as `dblab_embedded_ui_bt48bvi9c0h0 - `dblab_engine_name`: `` #### Responsibility -- provides a visual user interface (UI) for interacting with the DLE. +- Provides a visual user interface (UI) for interacting with the DLE #### How to manage Automatically starts and stops based on configuration settings in the `embeddedUI` section. diff --git a/docs/reference-guides/database-lab-engine-configuration-reference.md b/docs/reference-guides/database-lab-engine-configuration-reference.md index 17b5615d..ad8ea322 100644 --- a/docs/reference-guides/database-lab-engine-configuration-reference.md +++ b/docs/reference-guides/database-lab-engine-configuration-reference.md @@ -10,7 +10,7 @@ DBLab Engine behavior can be controlled using the main configuration file that h DBLab Engine supports [YAML 1.2](https://yaml.org/spec/1.2/spec.html) including anchors, aliases, tags, map merging. ::: -Example config files can be found here: https://gitlab.com/postgres-ai/database-lab/-/tree/v4.0.3/engine/configs. +Example config files can be found here: https://gitlab.com/postgres-ai/database-lab/-/tree/v4.1.3/engine/configs. You may store configuration files in any suitable location. The recommended location of configuration files for DBLab Engine is `~/.dblab/engine/configs`. @@ -22,8 +22,8 @@ Make sure that the file name is `server.yml` and its directory is mounted to `/h ::: Useful guides that help manage DBLab Engine: -- [How to configure and start DBLab Engine](/docs/dblab-howtos/administration/engine-manage#configure-and-start-a-database-lab-engine-instance) -- [Reconfigure DBLab Engine without downtime](/docs/dblab-howtos/administration/engine-manage#reconfigure-database-lab-engine) +- [How to configure and start DBLab Engine](/docs/dblab-howtos/administration/engine-manage#configure-and-start-a-dblab-engine-instance) +- [Reconfigure DBLab Engine without downtime](/docs/dblab-howtos/administration/engine-manage#reconfigure-dblab-engine) :::tip The configuration of DBLab Engine can be reloaded without downtime: @@ -34,18 +34,18 @@ docker logs --since 1m dblab_server ``` ::: -## YAML Anchors and Configuration Patterns +## YAML anchors and configuration patterns DBLab Engine configuration extensively uses YAML anchors and aliases to reduce repetition and maintain consistency across different configuration sections. This approach allows you to define common configuration patterns once and reuse them throughout the configuration file. -### Basic YAML Anchors Syntax +### Basic YAML anchors syntax - `&anchor_name` - defines an anchor (creates a reusable reference) - `*anchor_name` - uses an anchor (references the defined anchor) - `<<: *anchor_name` - merges an anchor into the current mapping (inheritance) -### Common Configuration Patterns +### Common configuration patterns -#### Database Container Configuration (`databaseContainer`) +#### Database container configuration (`databaseContainer`) This pattern defines common Docker container settings used across multiple jobs: ```yaml @@ -70,7 +70,7 @@ retrieval: dumpLocation: "/var/lib/dblab/dblab_pool/dump" ``` -#### Database Configuration Parameters (`databaseConfigs`) +#### Database configuration parameters (`databaseConfigs`) This pattern defines PostgreSQL configuration parameters that should be consistent across jobs: ```yaml @@ -96,14 +96,14 @@ retrieval: preprocessingScript: "" ``` -### Best Practices for YAML Anchors +### Best practices for YAML anchors 1. **Define anchors at the top level** of your configuration file for better readability 2. **Use descriptive names** that clearly indicate the purpose (e.g., `&db_container`, `&db_configs`) 3. **Combine anchors when needed** - you can use multiple `<<:` merge operators in the same section 4. **Override specific values** - anchor merging allows you to override individual values while keeping the rest -### Example: Combining Multiple Anchors +### Example: combining multiple anchors ```yaml # Define multiple anchors databaseContainer: &db_container @@ -141,7 +141,7 @@ Here is how the configuration file is structured: | `embeddedUI` | Refers to the DBLab Engine UI. | | `poolManager` | Manages filesystem pools or volume groups. | | `provision` | Describes how thin cloning and database branching are organized. | -| `retrieval` | Defines the data flow: a series of "jobs" for initial retrieval of the data, and, optionally, continuous data synchronization with the source, snapshot creation and retention policies. The initial retrieval may be either "logical" (dump/restore) or "physical" (based on replication or restoration from a archive). | +| `retrieval` | Defines the data flow: a series of "jobs" for initial retrieval of the data, and, optionally, continuous data synchronization with the source, snapshot creation and retention policies. The initial retrieval may be either "logical" (dump/restore) or "physical" (based on replication or restoration from an archive). | | `cloning` | Thin cloning policies. | | `platform` | PostgresAI Platform integration (provides GUI, advanced features such as user management, logs). | | `observer` | CI Observer configuration. CI Observer helps verify database schema changes (database migrations) automatically, in CI/CD pipelines. Available on the PostgresAI Platform. | @@ -160,7 +160,7 @@ Here is how the configuration file is structured: ## Section `server`: DBLab Engine API server - `verificationToken` (string, required) - the token that is used to work with Database Lab API - `host` (string, optional) - The host which the DBLab Engine API server accepts HTTP connections from. An empty string (default) means "all available addresses". -- `port` (string, required, default: 2345) - HTTP server port +- `port` (integer, required, default: 2345) - HTTP server port - `disableConfigModification` (boolean, optional, default: false) - disable modifying configuration via UI/API; when enabled, configuration changes can only be made by editing the config file directly ## Section `embeddedUI`: DBLab Engine user interface @@ -183,8 +183,8 @@ Here is how the configuration file is structured: - `from` (integer, required) - the lowest port value in the pool - `to` (integer, required) - the highest port value in the pool - `dockerImage` (string, required) - Postgres Docker image to be used for cloning. IMPORTANT: Postgres version of this image should match the source's Postgres version. For logical mode, it is a recommendation. For physical mode, it is a *requirement*. -- `useSudo` (boolean, optional, default: false) - use sudo for ZFS/LVM and Docker commands if Database Lab server running outside a container -- `keepUserPasswords` (bool, optional, default: "false") - By default, in addition to creating a new user with administrative privileges, DBLab Engine resets passwords for all existing users. This is done for security reasons. If this behavior is undesirable and you want to keep the ability authenticate for the existing users with their unchanged passwords, then set the value of the variable to `true`. +- `useSudo` (boolean, optional, default: false) - use sudo for ZFS/LVM and Docker commands if Database Lab server is running outside a container +- `keepUserPasswords` (bool, optional, default: "false") - By default, in addition to creating a new user with administrative privileges, DBLab Engine resets passwords for all existing users. This is done for security reasons. If this behavior is undesirable and you want to keep the ability to authenticate for the existing users with their unchanged passwords, then set the value of the variable to `true`. - `containerConfig` (key-value, optional) - options to pass custom parameters to clone containers - `cloneAccessAddresses` (string, optional, default: "127.0.0.1") - IP addresses that can be used to access clones. By default, use a loop-back to accept only local connections. The empty string means "all available addresses". The option supports multiple IPs (using comma-separated format) and IPv6 addresses (for example, `[::1]`) @@ -216,7 +216,7 @@ Note, that all jobs are optional. For example, all the following approaches defi Dumps a PostgreSQL database from a provided source to an archive or to the DBLab Engine instance. Options: -- `dumpLocation` (string, required) - specifies the location to store dump files (or directories, for directory-format archives), it will be automatically created on the host machine. DBLab Engine deletes all files and directories in this directory before creating new dumps. +- `dumpLocation` (string, required) - specifies the location to store dump files (or directories, for directory-format archives) — it will be automatically created on the host machine. DBLab Engine deletes all files and directories in this directory before creating new dumps. - `dockerImage` (string, required) - specifies the Docker image containing the dump-required tool - `containerConfig` (key-value, optional) - options to pass custom parameters to logicalDump container. Supports standard Docker container configuration options such as memory limits, CPU limits, volumes, etc. Can be inherited using YAML anchors (see `databaseContainer` pattern above) - Example: `"memory": "2gb"`, `"cpus": "1.5"`, `"shm-size": "1gb"` @@ -230,12 +230,12 @@ Options: - `password` (string, optional, default: "") - defines username password to connect to the database; the environment variable PGPASSWORD can be used instead of this option; the environment variable has a higher priority - `rdsIam` (key-value, optional) - contains options specific for RDS IAM source type - `awsRegion` (string, required) - AWS Region where RDS is located - - `dbInstanceIdentifier` (string, required) - RDS instance Identifier + - `dbInstanceIdentifier` (string, required) - RDS instance Identifier. This value is also exposed through the `/admin/config` projection as `RDSIAMDBInstance`. - `sslRootCert` (string, required) - path on the host machine to the SSL root certificate. You can download it from https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem - `parallelJobs` (integer, optional, default: 1) - defines the number of concurrent jobs using the `pg_dump` option `jobs`. This option can dramatically reduce the time to dump a large database - `databases` (key-value, optional) - defines options for specifying the database list that must be copied. By default, DBLab Engine dumps and restores all available databases. Do not specify the databases section to take all databases. Available options for each database: `tables` - `tables` (list of strings, optional) - dumps definition and/or data of only the listed tables. Do not specify the tables section to dump all available tables - - `excludeTables` (list of strings, optional) - excludes all tables matching any of the patterns from the dump. Accept specific schemas and tables or will allow for wildcards (*) for more flexibility. + - `excludeTables` (list of strings, optional) - excludes all tables matching any of the patterns from the dump. Accepts specific schemas and tables, or wildcards (*) for more flexibility. - `customOptions` (list of strings, optional) - defines one or multiple `pg_dump` options. See available options in [the official PostgreSQL documentation](https://www.postgresql.org/docs/current/app-pgdump.html). Common examples: - `"--no-publications"` - exclude publications (useful for replica databases) - `"--no-subscriptions"` - exclude subscriptions @@ -279,6 +279,12 @@ Options: Prepares a snapshot for logical restored PostgreSQL database. Options: +- `databaseRename` (key-value, optional) - rename databases before finalizing the snapshot. Runs after `preprocessingScript`. Each entry maps the original database name to the new name. This is useful when you want clones to use different database names than production (e.g., renaming `mydb_prod` to `mydb_dev`). Supported since DBLab Engine 4.1. See [Rename databases during snapshot creation](/docs/dblab-howtos/administration/data/database-rename). + ```yaml + databaseRename: + mydb_prod: mydb_dev + analytics_production: analytics_dblab + ``` - `dataPatching` (key-value, optional) - defines SQL queries for data patching. This allows you to run custom SQL queries against the restored database before creating the snapshot, useful for data masking, test data setup, or schema modifications - `dockerImage` (string, optional) - specifies the Docker image to run a data patching container. Can be inherited using YAML anchors (see `databaseContainer` pattern above) - `containerConfig` (key-value, optional) - options to pass custom parameters to data patching container. Supports standard Docker options like memory/CPU limits @@ -336,6 +342,12 @@ Options: - `configs` (key-value, optional) - applies PostgreSQL configuration parameters to the promotion instance - `sysctls` (key-value, optional) - allows configuring namespaced kernel parameters (sysctls) of Docker container for a promotion stage of taking a snapshot. See supported parameters: https://docs.docker.com/reference/cli/docker/container/run/#sysctl - `preprocessingScript` (string, optional) - path on the host machine to a pre-processing script +- `databaseRename` (key-value, optional) - rename databases before finalizing the snapshot. Runs after `preprocessingScript`. Each entry maps the original database name to the new name. Supported since DBLab Engine 4.1. See [Rename databases during snapshot creation](/docs/dblab-howtos/administration/data/database-rename). + ```yaml + databaseRename: + example_production: example_dblab + analytics_prod: analytics_dblab + ``` - `configs` (key-value, optional) - applies PostgreSQL configuration parameters to snapshot. These parameters are inherited by all clones. See also: [How to configure PostgreSQL used by DBLab Engine](/docs/dblab-howtos/administration/postgresql-configuration) - `envs` (key-value, optional) - passes custom environment variables to the promotion Docker container - `scheduler` (key-value, required) - contains tasks which run on a schedule: @@ -348,6 +360,9 @@ Options: ## Section `cloning`: thin cloning policies - `accessHost` (string, required) - the host that will be specified in the database connection string to inform users about how to connect to database clones. This should match one of the addresses specified in `provision.cloneAccessAddresses` or be a hostname that resolves to one of those addresses. Use public IP address if database connections are allowed from outside, or "localhost"/private IP for local-only access. - `maxIdleMinutes` (integer, optional, default: 120) - automatically delete clones after the specified minutes of inactivity, 0 is being used to disable this feature. Inactivity means no active sessions (queries being processed) and no recently logged queries in the query log. +- `protectionLeaseDurationMinutes` (integer, optional, default: 1440) - default protection lease duration in minutes when a clone is marked as protected. When a clone is protected with a lease, it will automatically become unprotected after this duration elapses. Use `0` for infinite protection (no automatic expiration). Supported since DBLab Engine 4.1. +- `protectionMaxDurationMinutes` (integer, optional, default: 10080) - maximum allowed protection duration in minutes. Users cannot request a protection duration longer than this value. Use `0` to remove the limit. Supported since DBLab Engine 4.1. +- `protectionExpiryWarningMinutes` (integer, optional, default: 1440) - send a warning webhook notification the specified number of minutes before a protection lease expires. Supported since DBLab Engine 4.1. ## Section `platform`: PostgresAI Platform integration - `url` (string, optional, default: "https://postgres.ai/api/general") - Platform API URL @@ -362,12 +377,12 @@ CI Observer helps verify database schema changes (database migrations) automatic - `replacementRules` (key-value, optional) - set up rules based on regular expressions (a pair of values `"regexp":"replace"`; to check syntax, use [this document](https://github.com/google/re2/wiki/Syntax )) for Postgres logs that will be sent to the Platform when running Observed Sessions; this helps ensure that sensitive data is masked properly and it doesn't leave the origin -### Log Fields Affected +### Log fields affected Replacement rules apply to the following PostgreSQL log fields: `message`, `detail`, `hint`, `internal_query`, `query` -### Common Replacement Patterns +### Common replacement patterns -#### Masking Numeric Values +#### Masking numeric values ```yaml observer: replacementRules: @@ -376,7 +391,7 @@ observer: "\\b\\d{3,}\\b": "***" # Numbers with 3+ digits ``` -#### Masking Email Addresses +#### Masking email addresses ```yaml observer: replacementRules: @@ -386,7 +401,7 @@ observer: "[a-z0-9._%+\\-]+@[a-z0-9.\\-]+\\.[a-z]{2,4}": "user@example.com" ``` -#### Masking SQL Values +#### Masking SQL values ```yaml observer: replacementRules: @@ -398,7 +413,7 @@ observer: "\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b": "XXX.XXX.XXX.XXX" ``` -#### Complete Example +#### Complete example ```yaml observer: replacementRules: @@ -414,7 +429,7 @@ observer: "\\b\\d{3}[.-]?\\d{3}[.-]?\\d{4}\\b": "XXX-XXX-XXXX" ``` -### Security Considerations +### Security considerations - **Test regex patterns** carefully to ensure they match the intended data - **Use capture groups** (like `$1`) to preserve necessary parts of matched text - **Order matters** - more specific patterns should come before general ones @@ -429,13 +444,60 @@ Webhooks provide a way to notify external systems about clone lifecycle events. - `trigger` (list of strings, required) - specifies which clone events should trigger this webhook. Available trigger types: - `clone_create` - triggered when a new clone is created - `clone_reset` - triggered when an existing clone is reset to a different snapshot + - `clone_delete` - triggered when a clone is deleted. Supported since DBLab Engine 4.1. + - `clone_protection_expiring` - triggered when a clone's protection lease is about to expire (based on `protectionExpiryWarningMinutes`). Supported since DBLab Engine 4.1. + - `clone_protection_expired` - triggered when a clone's protection lease has expired and protection has been automatically removed. Supported since DBLab Engine 4.1. + - `snapshot_create` - triggered when a new snapshot is created. Supported since DBLab Engine 4.1. + - `snapshot_delete` - triggered when a snapshot is deleted. Supported since DBLab Engine 4.1. + - `branch_create` - triggered when a new branch is created. Supported since DBLab Engine 4.1. + - `branch_delete` - triggered when a branch is deleted. Supported since DBLab Engine 4.1. ### Webhook payload format -Webhook requests are sent as HTTP POST with JSON payload containing: -- Event type (matching the trigger) -- Clone information (ID, port, connection details) -- Timestamp of the event -- Instance information +Webhook requests are sent as HTTP `POST` with a JSON body. If `secret` is configured, DBLab Engine also sends the `DBLab-Webhook-Token` HTTP header. + +Payload shape depends on the event type: +- Basic events (`snapshot_create`, `snapshot_delete`, `branch_create`, `branch_delete`) include: + - `event_type` + - `entity_id` +- Clone lifecycle events (`clone_create`, `clone_reset`, `clone_delete`) include: + - `event_type` + - `entity_id` + - `host` + - `port` + - `username` + - `dbname` + - `container_name` +- Clone protection events (`clone_protection_expiring`, `clone_protection_expired`) include all clone lifecycle fields plus: + - `protected_till` + - `expires_in_hours` + +### Example payload: `clone_create` +```json +{ + "event_type": "clone_create", + "entity_id": "clone-1", + "host": "localhost", + "port": 5432, + "username": "user1", + "dbname": "postgres", + "container_name": "dblab_clone_5432" +} +``` + +### Example payload: `clone_protection_expiring` +```json +{ + "event_type": "clone_protection_expiring", + "entity_id": "clone-1", + "host": "localhost", + "port": 5432, + "username": "user1", + "dbname": "postgres", + "container_name": "dblab_clone_5432", + "protected_till": "2027-01-15T14:00:00Z", + "expires_in_hours": 24 +} +``` ### Example configuration ```yaml @@ -468,46 +530,46 @@ The section has been removed in DBLab Engine 3.4.0 - `profilingInterval` (string, optional, default: 10ms) - time interval of samples taken by the profiler - `sampleThreshold` - (integer, optional, default: 20) - the minimum number of samples sufficient to display the estimation results -## Environment Variables +## Environment variables DBLab Engine supports several environment variables that can override configuration file settings or provide sensitive data like passwords. Environment variables have higher priority than configuration file values. -### Supported Environment Variables +### Supported environment variables -#### Database Connection +#### Database connection - `PGPASSWORD` - PostgreSQL password for source database connections. Overrides `password` in job configurations - `PGUSER` - PostgreSQL username. Can override `username` in job configurations - `PGHOST` - PostgreSQL hostname. Can override `host` in job configurations - `PGPORT` - PostgreSQL port. Can override `port` in job configurations - `PGDATABASE` - PostgreSQL database name. Can override `dbname` in job configurations -#### AWS/Cloud Integration +#### AWS/cloud integration - `AWS_ACCESS_KEY_ID` - AWS access key for S3/RDS access - `AWS_SECRET_ACCESS_KEY` - AWS secret key - `AWS_SESSION_TOKEN` - AWS session token (for temporary credentials) - `AWS_REGION` - AWS region (can override `awsRegion` in RDS IAM configuration) -#### WAL-G Configuration +#### WAL-G configuration - `WALG_S3_PREFIX` - S3 prefix for WAL-G backups - `WALG_COMPRESSION_METHOD` - compression method for WAL-G - `WALG_S3_STORAGE_CLASS` - S3 storage class -#### Platform Integration +#### Platform integration - `DLE_PLATFORM_ACCESS_TOKEN` - Platform access token (overrides `platform.accessToken`) - `DLE_VERIFICATION_TOKEN` - API verification token (overrides `server.verificationToken`) -### Priority Order +### Priority order When the same parameter is defined in multiple places, DBLab Engine uses this priority order: 1. **Environment variables** (highest priority) 2. **Configuration file values** 3. **Default values** (lowest priority) -### Security Best Practices +### Security best practices - **Use environment variables for sensitive data** like passwords and tokens - **Avoid putting credentials in configuration files** in production - **Use Docker secrets or Kubernetes secrets** to manage environment variables securely - **Rotate credentials regularly** and update environment variables accordingly -### Example Usage +### Example usage ```yaml # Configuration file - no sensitive data retrieval: diff --git a/docs/reference-guides/db-migration-checker-configuration-reference.md b/docs/reference-guides/db-migration-checker-configuration-reference.md index f3b3bb9f..856485cd 100644 --- a/docs/reference-guides/db-migration-checker-configuration-reference.md +++ b/docs/reference-guides/db-migration-checker-configuration-reference.md @@ -26,13 +26,13 @@ Here is how the configuration file is structured: | `runner` | How execution of DB migrations is organized | ## Section `app`: DB Migration Checker API server -- `host` (string, optional, default: `""`) - the host to which the DB Migration Checker server accepts HTTP connections -- `port` (string, required) - HTTP server port +- `host` (string, optional, default: `""`) - the host on which the DB Migration Checker server accepts HTTP connections; the empty string means "all available addresses" +- `port` (integer, optional, default: 2500) - HTTP server port - `verificationToken` (string, required) - token that is used to work with DB Migration Checker API -- `debug` - allows seeing more in the DBLab Engine logs; WARNING: in this mode, sensitive data (such as passwords) can be printed to logs +- `debug` (boolean, optional, default: false) - allows seeing more in the DB Migration Checker logs; WARNING: in this mode, sensitive data (such as passwords) can be printed to logs ## Section `dle`: DBLab Engine API integration -- `url` (string, required) - the URL to which the Database Lab server receives HTTP requests +- `url` (string, required) - URL of the DBLab Engine API server to which DB Migration Checker sends requests (for example, `https://dblab.domain.com`) - `verificationToken` (string, required) - the token that is used to work with Database Lab API ## Section `platform`: Postgres.ai Platform integration diff --git a/docs/reference-guides/dblab-client-cli-reference.md b/docs/reference-guides/dblab-client-cli-reference.md index f31d5ca8..e8b47141 100644 --- a/docs/reference-guides/dblab-client-cli-reference.md +++ b/docs/reference-guides/dblab-client-cli-reference.md @@ -52,7 +52,7 @@ To list available commands, either run `dblab` with no parameters or with flag ` The environment variable `DBLAB_CLI_FORWARDING_LOCAL_PORT` can be used as well. The flag `--forwarding-local-port` overrides config/env settings. -- `--identity-file` (string, default: "") - select a file from which the identity (private key) for public key authentication is read". +- `--identity-file` (string, default: "") - select a file from which the identity (private key) for public key authentication is read. The environment variable `DBLAB_CLI_IDENTITY_FILE` can be used as well. The flag `--identity-file` overrides config/env settings. @@ -81,7 +81,7 @@ DBLAB_INSTANCE_URL="http://127.0.0.1:2345" DBLAB_VERIFICATION_TOKEN="SECRET_TOKE If you register a Database Lab instance on the Postgres.ai Platform through the Platform server tunnel, it means that to use Database Lab API and CLI, your users need to be able to reach your infrastructure somehow. Consider use of VPN or custom SSH [port forwarding](https://en.wikipedia.org/wiki/Port_forwarding). ::: -## Command Overview +## Command overview ``` COMMANDS: init initialize Database Lab CLI @@ -93,6 +93,7 @@ COMMANDS: clone create, update, delete, reset, or retrieve clone instance display instance info snapshot create, retrieve, or delete snapshot + teleport Teleport integration commands (DLE 4.1+) config configure CLI environments help, h shows a list of commands or help for one command ``` @@ -110,12 +111,12 @@ dblab init [command options] [arguments...] **Options** - `--environment-id` (string, required) - an arbitrary environment ID of Database Lab instance's API - `--url` (string, required) - URL of Database Lab instance's API - - `--token` (string, required) - verification token of Database Lab instance + - `--token` (string, optional) - verification token of Database Lab instance - `--insecure` (boolean, optional, default: false) - allow insecure server connections when using SSL - `--request-timeout` (string, optional, default: "") - change requests timeout - `--forwarding-server-url` (string, optional) - forwarding server URL of Database Lab instance. For example: `ssh://user@remote.host:22` - `--forwarding-local-port` (string, optional) - local port for forwarding to the Database Lab instance - - `--identity-file` (string, optional) - select a file from which the identity (private key) for public key authentication is read" + - `--identity-file` (string, optional) - select a file from which the identity (private key) for public key authentication is read **Example** ```bash @@ -214,7 +215,7 @@ dblab commit [command options] [arguments...] **Options** - `--clone-id` (string, required) - clone ID -- `--message` (string, optional) - use the given message as the commit message +- `--message`, `-m` (string, optional) - use the given message as the commit message **Arguments** - `CLONE_ID` (string, required) - an ID of the Database Lab clone @@ -239,7 +240,7 @@ dblab log BRANCH_NAME **Example** -Display snapshot logs logs of branch `test`. +Display snapshot logs of branch `test`. ```bash dblab log test ``` @@ -302,8 +303,8 @@ dblab clone create [command options] - `--db-name` (string, optional) - database available to the user with restricted permissions - `--id` (string, optional) - clone ID - `--snapshot-id` (string, optional; DLE 4.0+) - snapshot ID -- `--branch` (string, optional; DLE 4.0+) - branch name -- `--protected` , `-p` (boolean, default: false) - mark instance as protected from deletion +- `--branch` (string, optional; DLE 4.0+) - branch name. If omitted, DBLab uses the default branch `main`. +- `--protected` , `-p` (string, optional) - enable deletion protection. Accepts: `true` for default lease duration; a number of minutes (e.g. `480`) or a Go-style duration string (`30m`, `2h`, `7d`) for a custom lease; or `0` for infinite protection (no expiry). When omitted, the clone is not protected. DLE 4.1+ supports time-limited protection leases — see [Protection leases](/docs/dblab-howtos/cloning/clone-protection). - `--async` , `-a` (boolean, default: false) - run the command asynchronously - `--extra-config` (string, optional) set an extra database configuration for the clone. An example: statement_timeout='1s' - `--help` , `-h` (boolean, default: false) - show help @@ -313,6 +314,16 @@ dblab clone create [command options] dblab clone create --username someuser --password SomePassword --branch main --id test ``` +Create a clone with protection for 8 hours (using a duration suffix): +```bash +dblab clone create --username someuser --password SomePassword --branch main --id test --protected 8h +``` + +Or equivalently, in minutes: +```bash +dblab clone create --username someuser --password SomePassword --branch main --id test --protected 480 +``` + --- ### Subcommand `update` Update the specified clone. @@ -325,12 +336,24 @@ dblab clone update [command options] CLONE_ID - `CLONE_ID` (string, required) - an ID of the Database Lab clone to update parameters **Options** -- `--protected` , `-p` (boolean, optional) - mark instance as protected from deletion +- `--protected` , `-p` (string, optional) - manage deletion protection. Accepts: `true` for default lease duration; a number of minutes (e.g. `1440`) or a Go-style duration string (`30m`, `24h`, `7d`) for a custom lease; `0` for infinite protection; or `false` to remove protection. DLE 4.1+ supports time-limited protection leases — see [Protection leases](/docs/dblab-howtos/cloning/clone-protection). - `--help` , `-h` (boolean, default: false) - show help **Example** + +Protect a clone with default lease duration: +```bash +dblab clone update --protected true TestCloneID +``` + +Protect a clone for 24 hours (using a duration suffix): ```bash -dblab clone update --protected TestCloneID +dblab clone update --protected 24h TestCloneID +``` + +Remove protection from a clone: +```bash +dblab clone update --protected false TestCloneID ``` --- @@ -383,7 +406,7 @@ dblab clone destroy TestCloneID ### Subcommand `start-observation` :::note 🚧 Experimental -This is an experimental feature (its working title: "CI Observer"). If you have questions, suggestions, or bug reports, please open an issue in the [DBLab Engine issue tracker](https://gitlab.com/postgres-ai/database-lab/-/issues) and/or raise a discussion in one of [the Database Lab Community channels](https://postgres.ai/docs/database-lab#more). +This is an experimental feature (its working title: "CI Observer"). If you have questions, suggestions, or bug reports, please open an issue in the [DBLab Engine issue tracker](https://github.com/postgres-ai/database-lab-engine/issues) and/or raise a discussion in one of [the Database Lab Community channels](https://postgres.ai/docs/database-lab#more). ::: Start clone state monitoring. @@ -631,7 +654,7 @@ Delete a snapshot. **Usage** ```bash -dblab snapshot delete SNAPSHOT_ID +dblab snapshot delete [command options] SNAPSHOT_ID ``` **Example** @@ -640,6 +663,10 @@ dblab snapshot delete SNAPSHOT_ID dblab snapshot delete "dblab_pool/dataset_1@snapshot_20241028174127" ``` +:::tip +Force deletion of snapshots with dependent clones is available through the API (`DELETE /snapshot/{id}?force=true`) or the UI, but is not currently supported via the CLI. +::: + --- ### Subcommand `help` , `h` Show help for the command. @@ -650,6 +677,55 @@ dblab snapshot help ``` +## Command: `teleport` +:::note +Requires DBLab 4.1 or higher +::: +Teleport integration commands. The `teleport serve` subcommand runs a sidecar process that automatically registers and deregisters DBLab clones as Teleport database resources, enabling secure access to clones through Teleport's access control. + +For a full setup guide, see the [Teleport integration howto](/docs/dblab-howtos/administration/teleport-integration). + +**Usage** +```bash +dblab teleport command [command options] [arguments...] +``` + +**Subcommands** +- `serve` - start the Teleport sidecar + +--- +### Subcommand `serve` +Start the Teleport integration sidecar. This process listens for clone lifecycle webhooks from DBLab Engine and automatically registers/deregisters clones as Teleport database resources. + +**Usage** +```bash +dblab teleport serve [command options] +``` + +**Options** +- `--environment-id` (string, required) - environment identifier used in Teleport resource names +- `--teleport-proxy` (string, required) - Teleport Auth Server or Proxy address (e.g., `teleport.example.com:3025`) +- `--teleport-identity` (string, required) - path to the Teleport bot identity file for authentication +- `--listen-addr` (string, optional, default: "localhost:9876") - address and port to listen for incoming webhooks. Use `0.0.0.0:9876` if the sidecar needs to be reachable from Docker containers. +- `--dblab-url` (string, optional, default: "http://localhost:2345") - DBLab API URL +- `--dblab-token` (string, required) - DBLab verification token (or via env var `DBLAB_TOKEN`) +- `--webhook-secret` (string, required) - shared secret that DBLab Engine sends in the `DBLab-Webhook-Token` header for webhook payload verification (or via env var `WEBHOOK_SECRET`) +- `--tctl-path` (string, optional, default: `tctl`) - path to the `tctl` binary if it is not on `$PATH` + +**Example** +```bash +dblab teleport serve \ + --environment-id production \ + --teleport-proxy teleport.example.com:3025 \ + --teleport-identity /etc/teleport/dblab-identity \ + --listen-addr 0.0.0.0:9876 \ + --dblab-url http://localhost:2345 \ + --dblab-token "$DBLAB_TOKEN" \ + --webhook-secret "$WEBHOOK_SECRET" +``` + +--- + ## Command: `config` Configure CLI environments. diff --git a/docs/reference-guides/index.md b/docs/reference-guides/index.md index bf1343cc..487a55bf 100644 --- a/docs/reference-guides/index.md +++ b/docs/reference-guides/index.md @@ -1,5 +1,5 @@ --- -title: Database Lab reference guides +title: PostgresAI reference guides sidebar_label: Overview slug: /reference-guides --- diff --git a/docs/reference-guides/joe-bot-configuration-reference.md b/docs/reference-guides/joe-bot-configuration-reference.md index 69dcf474..0eddc8f3 100644 --- a/docs/reference-guides/joe-bot-configuration-reference.md +++ b/docs/reference-guides/joe-bot-configuration-reference.md @@ -207,7 +207,7 @@ channelMapping: # Enterprise Edition options – only to use with active Postgres.ai Platform EE # subscription. Changing these options you confirm that you have active # subscription to Postgres.ai Platform Enterprise Edition. -# See more: https://postgres.ai/docs/platform/postgres-ai-platform-overview +# See more: https://postgres.ai/docs/platform/ enterprise: quota: # Limit request rates. Works in pair with "interval" value. Default: 10. @@ -221,8 +221,8 @@ enterprise: enabled: false dblab: - # Limit the number of available Database Lab instances. Default: 1. - instanceLimit: 1 + # Limit the number of available Database Lab instances. Default: 2. + instanceLimit: 2 ``` @@ -248,13 +248,13 @@ enterprise: ### `JOE_PLATFORM_HISTORY_ENABLED` - (boolean, default: `true`), enable sending command history to Postgres.ai Platform for collaboration and visualization. Requires setting proper `JOE_PLATFORM_TOKEN`. See the [Joe Bot Tutorial](/docs/tutorials/joe-setup#step-2a-set-up-joe-in-postgresai-console-web-ui) for the token. -### `JOE_DEBUG` +### `JOE_APP_DEBUG` - (boolean, default: `false`), enable debug mode; WARNING: in this mode, sensitive data (such as passwords) can be printed to logs --- ## Enterprise environment variables -Changing these options you confirm that you have active subscription to [Postgres.ai Platform](https://postgres.ai/console/) Enterprise Edition) +Changing these options you confirm that you have an active subscription to [Postgres.ai Platform](https://postgres.ai/console/) Enterprise Edition. ### `EE_QUOTA_LIMIT` - (integer, default: `10`), limits request rates, works in pair with `EE_QUOTA_INTERVAL` @@ -265,5 +265,5 @@ Changing these options you confirm that you have active subscription to [Postgre ### `EE_AUDIT_ENABLED` - (boolean, default: `false`), enable command logging for audit purposes -### `EE_DBLAB_INSTANCE_LIMIT` -- (integer, default: `1`), limit the number of Database Lab instances. Joe Bot CE supports working with only 1 Database Lab instance +### `EE_DBLAB_INSTANCE_LIMIT` +- (integer, default: `2`), limit the number of Database Lab instances. Joe Bot CE supports working with only 1 Database Lab instance diff --git a/docs/reference-guides/postgres-ai-bot-reference.md b/docs/reference-guides/postgres-ai-bot-reference.md index 12d69e96..f8b22577 100644 --- a/docs/reference-guides/postgres-ai-bot-reference.md +++ b/docs/reference-guides/postgres-ai-bot-reference.md @@ -53,7 +53,7 @@ Visualize some data such as benchmark results. This function uses QuickChart; se | **data** | `array` | Array of numbers representing the data points. | N/A | `[10, 20, 30, 40]` | ## Tool `fetch_whole_web_page` -Fetch the content of a web page. As of July 2024, this feature is limited to these domains: +Fetch the content of a web page. The feature is limited to these domains: - `github.com` - `gitlab.com` - `postgresql.org` @@ -71,7 +71,7 @@ Fetch the content of a web page. As of July 2024, this feature is limited to the Database experiment in dedicated environment: creates a PostgreSQL cluster in Hetzner Cloud and executes a series of experiment runs on it using a GitLab CI pipeline. -When started, provides pipeline URL. Once experiment is finished, either succesfully or with errors, +When started, provides pipeline URL. Once experiment is finished, either successfully or with errors, the user is informed of the results. JSON configuration example: @@ -143,7 +143,7 @@ Access and analyze experiment's results, or get pipeline's job statuses if the e | **pipeline_id** (required) | `string` | Pipeline ID. | N/A | `12345` | ## Tool `sql_execute` -Connects to Postgres database and executes SQL query. If database credentials are provided, they are used to establish Postgres connection. Otherwise, a new DBLab clone is created using `create_dblab_clone` and then clone's credentials are used. +Connects to Postgres database and executes SQL query. If database credentials are provided, they are used to establish Postgres connection. Otherwise, a new DBLab clone is created using `create_dblab_clone` and then the clone's credentials are used. ### Input parameters @@ -164,7 +164,7 @@ Connects to Postgres database and executes SQL query. If database credentials ar ## Tool `create_dblab_clone` -Create a new DBLab clone for a specific Postgres major version (`16` by default). This function is called when user wants to execute a SQL query but hasn't provided DB connection information. +Create a new DBLab clone for a specific Postgres major version (`16` by default). This function is called when a user wants to execute a SQL query but hasn't provided DB connection information. ### Input parameters diff --git a/docs/reference-guides/postgres-ai-monitoring-reference.md b/docs/reference-guides/postgres-ai-monitoring-reference.md index f71d43bf..2b059862 100644 --- a/docs/reference-guides/postgres-ai-monitoring-reference.md +++ b/docs/reference-guides/postgres-ai-monitoring-reference.md @@ -2,7 +2,7 @@ title: postgres_ai monitoring reference documentation sidebar_label: postgres_ai monitoring keywords: - - "postgres_ai monitoringreference" + - "postgres_ai monitoring reference" - "Monitoring reference" --- @@ -10,6 +10,12 @@ keywords: ## Metrics +:::note + +This page lists the user-facing metric groups. The pgwatch collector ships with additional groups that are emitted but not yet documented here (for example, `pg_stat_slru`, `pg_statio_all_tables`, `pg_statio_all_indexes`, `multixact_size`, `pg_index_pilot`, `stats_reset`, `table_size_detailed`). To see the full set of metrics emitted by your monitoring instance, query the Prometheus / VictoriaMetrics endpoint directly (e.g. `/api/v1/label/__name__/values`). + +::: + ### Common labels Most metrics include these standard labels: @@ -25,9 +31,9 @@ Most metrics include these standard labels: ### Metric-specific labels Additional labels are available for specific metric types: -- **Query metrics** (`pg_stat_statements`): `queryid`, `user` -- **Table metrics** (`table_stats`, `pg_stat_user_tables`): `schema`, `table_name`, `table_full_name`, `table_size_cardinality_mb` -- **Index metrics** (`pg_stat_user_indexes`): `schemaname`, `relname`, `indexrelname` +- **Query metrics** (`pg_stat_statements`): `queryid`, `datname` +- **Table metrics** (`table_stats`, `pg_stat_all_tables`): `schema`, `table_name`, `table_full_name`, `table_size_cardinality_mb` +- **Index metrics** (`pg_stat_all_indexes`): `schemaname`, `relname`, `indexrelname` - **Lock metrics** (`locks_mode`): `lockmode` - **Wait events** (`wait_events`): `wait_event`, `wait_event_type` - **Replication metrics**: `application_name`, `client_info`, `usename` @@ -95,12 +101,12 @@ Collected every 15-30 seconds | `db_stats_sessions_killed` | Sessions killed | - | | `db_size_size_b` | Database size in bytes | Bytes | | `db_size_catalog_size_b` | Catalog schema size in bytes | Bytes | -| `pg_stat_activity_count` | Count of sessions by state | - | -| `pg_stat_activity_max_tx_duration` | Maximum transaction duration | Seconds | +| `pg_stat_activity_count` | Count of sessions by state (additional labels: `state`, `application_name`) | - | +| `pg_stat_activity_max_tx_duration` | Maximum transaction duration (additional labels: `state`, `application_name`) | Seconds | ### Query performance (`pg_stat_statements`) Collected every 30 seconds -**Additional Labels:** `queryid` (query identifier), `user` (database user) +**Additional Labels:** `queryid` (query identifier), `datname` (database name) | Metric | Description | Units | |--------|-------------|-------| @@ -129,15 +135,40 @@ Collected every 30 seconds |--------|-------------|-------| | `locks_mode_count` | Number of locks held by mode type | - | +### Lock waits (`lock_waits`) +Collected every 30 seconds + +Detailed blocked / blocking pairs for lock-contention root cause analysis. New in 0.15: +the lock-wait metrics carry session PIDs as labels, so the blocker can be identified directly +in Grafana / PromQL without running the manual `pg_locks` join. Used by +[Dashboard 13 — Lock contention](/docs/monitoring/dashboards/lock-contention). + +**Additional Labels:** `blocked_pid` (PID of the waiting backend), `blocker_pid` (PID of the +blocking backend), `blocked_user` / `blocker_user`, `blocked_appname` / `blocker_appname`, +`blocked_table` / `blocker_table` (affected relation), `blocked_query_id` / `blocker_query_id`, +and `datname`. Note: `blocked_mode` / `blocker_mode` and `blocked_locktype` / `blocker_locktype` +are emitted as plain (non-`tag_`) value columns in the metric definition, so they are **not** +Prometheus labels. + +| Metric | Description | Units | +|--------|-------------|-------| +| `pgwatch_lock_waits_blocked_ms` | Time the blocked backend has been waiting | Milliseconds | +| `pgwatch_lock_waits_blocker_tx_ms` | Age of the blocking backend's transaction | Milliseconds | + +:::tip Terminating a blocker +Use the `blocker_pid` label directly: `select pg_terminate_backend();`. No manual +blocking-chain query is required to find the PID. +::: + ### Wait events (`wait_events`) Collected every 15 seconds -**Additional Labels:** `wait_event` (specific wait event), `wait_event_type` (wait category), `query_id` (associated query) +**Additional Labels:** `wait_event` (specific wait event), `wait_event_type` (wait category), and on PostgreSQL 14+ `query_id` (associated query) | Metric | Description | Units | |--------|-------------|-------| | `wait_events_total` | Count of processes experiencing wait event | - | -### Table statistics (`table_stats`, `pg_stat_user_tables`) +### Table statistics (`table_stats`, `pg_stat_all_tables`) Collected every 30 seconds **Additional Labels:** `schema` (table schema), `table_name` (table name), `table_full_name` (schema.table), `table_size_cardinality_mb` (size category) @@ -167,15 +198,15 @@ Collected every 30 seconds | `table_stats_seconds_since_last_analyze` | Seconds since last analyze | Seconds | | `table_stats_seconds_since_last_vacuum` | Seconds since last vacuum | Seconds | -### Index statistics (`pg_stat_user_indexes`) +### Index statistics (`pg_stat_all_indexes`) Collected every 30 seconds **Additional Labels:** `schemaname` (schema name), `relname` (table name), `indexrelname` (index name) | Metric | Description | Units | |--------|-------------|-------| -| `pg_stat_user_indexes_idx_scan` | Index scans performed | - | -| `pg_stat_user_indexes_idx_tup_read` | Index entries returned | - | -| `pg_stat_user_indexes_idx_tup_fetch` | Table rows fetched via index | - | +| `pg_stat_all_indexes_idx_scan` | Index scans performed | - | +| `pg_stat_all_indexes_idx_tup_read` | Index entries returned | - | +| `pg_stat_all_indexes_idx_tup_fetch` | Table rows fetched via index | - | ### WAL and replication metrics (`wal`, `replication`, `replication_slots`, `pg_stat_replication`, `pg_stat_wal_receiver`, `pg_archiver`, `archive_lag`, `pg_xlog_position`) Collected every 15-30 seconds @@ -201,6 +232,93 @@ Collected every 15-30 seconds | `archive_lag_archived_count` | Total archived WAL files | - | | `archive_lag_failed_count` | Failed archive attempts | - | | `pg_archiver_pending_wal_count` | Number of WAL files pending archive | - | +| `pg_wal_size_bytes` | Total size of regular files in the `pg_wal` directory (via `pg_ls_waldir()`; excludes subdirectories such as `pg_wal/archive_status`). New in 0.15. Not emitted when `pg_wal_size_status_code` > 0. | Bytes | +| `pg_wal_size_status_code` | `pg_wal` size collection status: `0` = success, `1` = `pg_ls_waldir()` not available, `2` = monitoring role lacks EXECUTE privilege. New in 0.15. | - | + +:::note Interpreting `pg_wal_size` +`pg_wal` growth that is not matched by archive or replica progress points to disk-fill risk — +typically a stuck WAL archiver, an inactive replication slot retaining WAL, or sustained high +WAL generation. Cross-reference with the archiver and replication-slot metrics above, and see +[How to troubleshoot a growing pg_wal directory](/docs/postgres-howtos/database-administration/maintenance/how-to-troubleshoot-a-growing-pg-wal-directory). +::: + +### xmin horizon (`xmin_horizon`) +Collected every 30 seconds. Instance-level, primary only. New in 0.15. + +Tracks the current xmin horizon age split by blocker class and horizon type. Component +`*_age_tx` / `*_count` columns emit `0` (never NULL) when that component has no active blocker. +Used by [Dashboard 07 — Autovacuum and xmin horizon](/docs/monitoring/dashboards/autovacuum). + +| Metric | Description | Units | +|--------|-------------|-------| +| `xmin_horizon_data_horizon_age_tx` | Age of the data horizon (worst of client-backend, slot, standby, prepared-xact blockers) | Transactions | +| `xmin_horizon_catalog_horizon_age_tx` | Age of the catalog horizon (data horizon plus catalog `catalog_xmin` blockers) | Transactions | +| `xmin_horizon_snapshot_xmin` | Raw snapshot xmin anchor (`txid_snapshot_xmin(txid_current_snapshot())`) | - | +| `xmin_horizon_pg_stat_activity_age_tx` | Oldest client-backend `backend_xmin` age | Transactions | +| `xmin_horizon_pg_stat_activity_count` | Number of client backends holding a horizon | - | +| `xmin_horizon_pg_replication_slots_age_tx` | Oldest replication-slot `xmin` age | Transactions | +| `xmin_horizon_pg_replication_slots_count` | Number of slots holding the data horizon | - | +| `xmin_horizon_pg_replication_slots_catalog_age_tx` | Oldest replication-slot `catalog_xmin` age | Transactions | +| `xmin_horizon_pg_replication_slots_catalog_count` | Number of slots holding the catalog horizon | - | +| `xmin_horizon_pg_stat_replication_age_tx` | Oldest standby-feedback `backend_xmin` age | Transactions | +| `xmin_horizon_pg_stat_replication_count` | Number of standbys holding a horizon | - | +| `xmin_horizon_pg_prepared_xacts_age_tx` | Oldest prepared-transaction age | Transactions | +| `xmin_horizon_pg_prepared_xacts_count` | Number of prepared transactions holding a horizon | - | + +### xmin horizon blockers (`xmin_horizon_blockers`) +Collected every 30 seconds. Instance-level, primary only. New in 0.15. + +Captures the single oldest (top) blocker for each currently active component as a separate +labeled series, with that blocker's xmin age in transactions. Emits one series per active +component and no series for an empty component, so cardinality varies between 0 and 5 per +scrape. The monitoring role's own sessions are excluded. + +**Additional Labels:** `component` (`pg_stat_activity`, `pg_replication_slots`, +`pg_replication_slots_catalog`, `pg_stat_replication`, `pg_prepared_xacts`), `horizon_type` +(`data` / `catalog`), `blocker_database`, `blocker_user`, `blocker_appname`, `blocker_state`, +`queryid` (for activity blockers), `slot_name` / `slot_type` / `slot_plugin` / +`slot_xmin_source` / `slot_status` / `slot_wal_status` (for slot blockers), `standby_name` +(for replication blockers), `prepared_gid` / `owner` (for prepared-transaction blockers). + +| Metric | Description | Units | +|--------|-------------|-------| +| `xmin_horizon_blockers_age_tx` | xmin age of the top blocker for the labeled component | Transactions | + +:::note Query text is not a label +Query text is intentionally not emitted as a Prometheus label. Use the `queryid` label to look +up the query text in pgwatch query storage. +::: + +### I/O statistics (`pg_stat_io`, PostgreSQL 16+) {#io-statistics-pg_stat_io-postgresql-16} +Collected every 30 seconds. Instance-level. New in 0.15. + +Collects I/O statistics from the PostgreSQL `pg_stat_io` view (PostgreSQL 16+). On PostgreSQL +15 and earlier this group emits nothing. Values are aggregated by backend type with a `total` +row added via `ROLLUP`. Used by +[Dashboard 14 — I/O statistics](/docs/monitoring/dashboards/io-statistics). + +**Additional Labels:** `backend_type` (e.g. `client backend`, `autovacuum worker`, +`background writer`, `checkpointer`, `walwriter`, or `total` for the rollup row). + +| Metric | Description | Units | +|--------|-------------|-------| +| `pg_stat_io_reads` | Read operations | - | +| `pg_stat_io_read_bytes_mb` | Data read | MiB | +| `pg_stat_io_read_time_ms` | Time spent reading | Milliseconds | +| `pg_stat_io_writes` | Write operations | - | +| `pg_stat_io_write_bytes_mb` | Data written | MiB | +| `pg_stat_io_write_time_ms` | Time spent writing | Milliseconds | +| `pg_stat_io_writebacks` | Writeback operations | - | +| `pg_stat_io_writeback_bytes_mb` | Data written back | MiB | +| `pg_stat_io_writeback_time_ms` | Time spent on writebacks | Milliseconds | +| `pg_stat_io_fsyncs` | fsync operations | - | +| `pg_stat_io_fsync_time_ms` | Time spent on fsyncs | Milliseconds | +| `pg_stat_io_extends` | Relation extend operations | - | +| `pg_stat_io_extend_bytes_mb` | Data added by extends | MiB | +| `pg_stat_io_hits` | Blocks found in shared buffers | - | +| `pg_stat_io_evictions` | Buffers evicted to make room | - | +| `pg_stat_io_reuses` | Buffers reused directly (e.g. ring buffers) | - | +| `pg_stat_io_stats_reset_s` | Seconds since `pg_stat_reset_shared('io')` | Seconds | ### Bloat analysis metrics (`pg_table_bloat`, `pg_btree_bloat`, `unused_indexes`, `rarely_used_indexes`, `redundant_indexes`, `pg_invalid_indexes`) Collected every 2-3 hours diff --git a/docs/reference-guides/postgresai-cli-reference.md b/docs/reference-guides/postgresai-cli-reference.md index 4982a36d..0e5998fe 100644 --- a/docs/reference-guides/postgresai-cli-reference.md +++ b/docs/reference-guides/postgresai-cli-reference.md @@ -3,10 +3,15 @@ title: PostgresAI CLI reference sidebar_label: PostgresAI CLI keywords: - "postgresai cli" + - "pgai cli" - "postgres_ai cli" - "postgres_ai monitoring cli" - "mcp" - "issues" + - "checkup" + - "prepare-db" + - "pgai joe" + - "joe bot" --- import Tabs from '@theme/Tabs'; @@ -14,11 +19,26 @@ import TabItem from '@theme/TabItem'; ## Description -PostgresAI Command Line Interface (`postgresai`) is a tool for working with postgres_ai monitoring, including authentication, MCP integration, and issue management. +PostgresAI Command Line Interface is a tool for working with PostgresAI: +preparing databases for monitoring, running local monitoring stacks, +generating health-check reports, browsing and managing issues in the +PostgresAI Console, running [Joe](/docs/joe-bot) SQL optimization commands +on ephemeral DBLab clones, and exposing PostgresAI tools to AI coding +clients over MCP. + +The CLI is published as the `postgresai` npm package and ships two +equivalent binaries: `postgresai` (canonical) and `pgai` (short alias). +Both names accept exactly the same commands and options; this page uses +`postgresai` throughout. + +## Requirements + +The CLI requires **Node.js 18+ (or Bun 1.0+)**. Older Node versions fail fast with a clear +error rather than breaking partway through a command. ## Getting started -To install and authenticate, see [PostgresAI CLI](/docs/postgresai-howtos/postgresai-cli). +To install and authenticate, see the [PostgresAI CLI how-to](/docs/postgresai-howtos/postgresai-cli). ## Synopsis @@ -27,44 +47,68 @@ To install and authenticate, see [PostgresAI CLI](/docs/postgresai-howtos/postgr ```bash postgresai [global options] [command options] [arguments...] +# or, equivalently: +pgai [global options] [command options] [arguments...] ``` ```bash -npx postgresai [global options] [command options] [arguments...] +npx postgresai@latest [global options] [command options] [arguments...] ``` ```bash -bunx postgresai [global options] [command options] [arguments...] +bunx postgresai@latest [global options] [command options] [arguments...] ``` -Run `postgresai --help` to list available commands and global options. For command-specific help, run `postgresai --help`. +Run `postgresai --help` to list available commands and global options. +For command-specific help, run `postgresai --help` (works for +nested subcommands too, e.g. `postgresai mon targets --help`). + +## Global options + +These options apply to every command and override the corresponding +environment variables and configuration file values: + +- `--api-key ` — API key (overrides `PGAI_API_KEY`). +- `--api-base-url ` — API base URL for backend RPC (overrides `PGAI_API_BASE_URL`; default `https://postgres.ai/api/general/`). +- `--ui-base-url ` — UI base URL for browser routes (overrides `PGAI_UI_BASE_URL`; default `https://console.postgres.ai`). +- `--storage-base-url ` — Storage base URL for file uploads (overrides `PGAI_STORAGE_BASE_URL`). + +Configuration is stored in `~/.config/postgresai/config.json`. ## Command overview ``` COMMANDS: - auth authenticate via browser and store API key locally - init create a monitoring role, required view(s), and grant permissions - mon manage monitoring services - issues manage issue reports in PostgresAI Console - mcp MCP server integration for AI coding tools - add-key store API key locally - show-key show the current API key (masked) - remove-key remove the stored API key + prepare-db prepare a database for monitoring (idempotent) + unprepare-db remove monitoring setup from a database + checkup generate health-check reports directly from PostgreSQL + mon manage the local monitoring stack + login authenticate via browser or store an API key directly + auth authenticate and manage the local API key + login top-level alias for `auth login` + joe Joe — plan/EXPLAIN/exec queries on ephemeral DBLab clones + projects list the org's projects (shows which have Joe ready) + issues manage issues, comments, and action items in PostgresAI Console + reports list and download checkup reports stored in PostgresAI Console + mcp MCP server integration for AI coding tools + set-default-project store the default project for checkup uploads + set-storage-url store the storage base URL for file uploads + help show help ``` -## Command: `auth` +## Command: `prepare-db` -Authenticate via browser and store the API key locally. +Prepare a database for monitoring: create the monitoring user, the +required view(s), and grant permissions. The command is idempotent. **Usage** @@ -72,405 +116,971 @@ Authenticate via browser and store the API key locally. ```bash -postgresai auth +postgresai prepare-db [conn] [options] ``` ```bash -npx postgresai auth +npx postgresai@latest prepare-db [conn] [options] ``` ```bash -bunx postgresai auth +bunx postgresai@latest prepare-db [conn] [options] ``` -**Notes** +`[conn]` is an optional positional admin connection string. Both URL +form (`postgresql://admin@host:5432/dbname`) and libpq key/value form +(`"dbname=dbname host=host user=admin"`) are accepted; psql-like +options (`-h`, `-p`, `-U`, `-d`) are also supported. -- Configuration is stored in `~/.config/postgresai/config.json`. +**Examples** -## Command: `init` +```bash +postgresai prepare-db postgresql://admin@host:5432/dbname +postgresai prepare-db "dbname=dbname host=host user=admin" +postgresai prepare-db -h host -p 5432 -U admin -d dbname -Create or update the monitoring role, required view(s), and grant required permissions (idempotent). +# Verify only (no changes) +postgresai prepare-db postgresql://admin@host:5432/dbname --verify -**Usage** +# Dry run: print SQL plan +postgresai prepare-db postgresql://admin@host:5432/dbname --print-sql - - +# Reset only the monitoring role password +postgresai prepare-db postgresql://admin@host:5432/dbname \ + --reset-password --password 'new_password' + +# Supabase mode (uses Management API instead of direct connection) +SUPABASE_ACCESS_TOKEN=... SUPABASE_PROJECT_REF=... \ + postgresai prepare-db --supabase +``` + +**Connection options** + +- `-h, --host ` — PostgreSQL host (psql-like). +- `-p, --port ` — PostgreSQL port (psql-like). +- `-U, --username ` — PostgreSQL admin user (psql-like). +- `-d, --dbname ` — PostgreSQL database name (psql-like). +- `--admin-password ` — admin password (otherwise uses `PGPASSWORD` if set). +- `--db-url ` — admin connection URL (deprecated; pass it as the positional `[conn]` argument). + +**Monitoring role options** + +- `--monitoring-user ` — monitoring role name to create or update (default: `postgres_ai_mon`). +- `--password ` — monitoring role password (overrides `PGAI_MON_PASSWORD`). If neither is provided, a strong password is generated. +- `--print-password` — print the generated monitoring password (dangerous in CI logs). +- `--skip-optional-permissions` — skip optional permissions (RDS / self-managed extras). +- `--provider ` — database provider (e.g. `supabase`); affects which steps run. + +**Modes** + +- `--verify` — verify that the monitoring role and permissions are in place; make no changes. +- `--reset-password` — reset only the monitoring role password. +- `--print-sql` — print the SQL plan and exit; apply no changes. +- `--json` — output the result as machine-readable JSON. + +**Supabase mode** + +- `--supabase` — use the Supabase Management API instead of a direct PostgreSQL connection. +- `--supabase-access-token ` — Supabase Management API token (or `SUPABASE_ACCESS_TOKEN` env var). Tokens can be created on the [Supabase access tokens page](https://supabase.com/dashboard/account/tokens). +- `--supabase-project-ref ` — Supabase project reference (or `SUPABASE_PROJECT_REF` env var). Auto-detected from a Supabase database URL when one is supplied as `[conn]`. + +## Command: `unprepare-db` + +Reverse `prepare-db`: drop the monitoring user, views, schema, and +revoke permissions. + +**Usage** ```bash -postgresai init +postgresai unprepare-db [conn] [options] ``` - - +**Options** + +- `-h, --host`, `-p, --port`, `-U, --username`, `-d, --dbname`, + `--admin-password`, `--db-url ` (deprecated) — admin connection + parameters, same as `prepare-db`. +- `--monitoring-user ` — monitoring role to remove (default: `postgres_ai_mon`). +- `--keep-role` — keep the monitoring role; only revoke permissions and drop objects. +- `--provider ` — database provider (affects which steps run). +- `--print-sql` — print the SQL plan and exit; apply no changes. +- `--force` — skip the confirmation prompt. +- `--json` — output the result as machine-readable JSON. + +## Command: `checkup` + +Generate health-check reports directly from PostgreSQL ("express mode") +and optionally upload them to the PostgresAI Console. + +Express mode supports PostgreSQL 14 through PostgreSQL 19. For PostgreSQL 19 +beta releases, it preserves version labels such as `19beta2` while using +`server_version_num` (`190000`) for version-aware metric selection. PostgreSQL +19 remains a pre-release; use it for compatibility testing rather than +production workloads until general availability. + +**Usage** ```bash -npx postgresai init +# Run all checks +postgresai checkup + +# Run a specific check (CHECK_ID matches /^[A-Z]\d{3}$/i — case-insensitive, e.g. H002 or h002) +postgresai checkup ``` - - +`` accepts the same URL / libpq / psql-like forms as +`prepare-db`. + +**Options** + +- `--check-id ` — specific check to run (or `ALL`). Equivalent to passing the check ID as the first positional argument. +- `--node-name ` — node name embedded in reports (default: `node-01`). +- `--output ` — write per-check JSON results to this directory. Only the report payload is written; progress and error messages go to stderr, so the output files stay clean. +- `--upload` / `--no-upload` — upload JSON results to PostgresAI Console (requires API key). Default depends on whether an API key is configured. +- `--project ` — project name or ID for the upload (used with `--upload`). Defaults to the value stored by [`set-default-project`](#command-set-default-project); a project is auto-generated on first run if needed. +- `--json` — print JSON to stdout. +- `--markdown` — print Markdown to stdout. + +:::tip stdout vs stderr +Progress and error messages are written to **stderr**; **stdout** carries only the report +payload. This means `--json` / `--markdown` can be piped safely, for example: ```bash -bunx postgresai init +postgresai checkup --json | jq '.checks[] | select(.id == "H002")' ``` +::: - - +**Available checks** -**Examples** +Run `postgresai checkup --help` to see the full list of check IDs and titles bundled with your +CLI version. The express-mode checks span the A (general / version / cluster), D (logging and +`pg_stat_statements` settings), F (autovacuum / bloat), G (memory and timeouts), H (index), and I +(I/O) groups — there are no K (query) checks in the express-mode CLI. In addition to the index +(`H00x`) checks, 0.15 ships the estimated bloat checks: - - +| Check | Finds | +|-------|-------| +| `F004` | Autovacuum: heap bloat (estimated) | +| `F005` | Autovacuum: index bloat (estimated) | ```bash -postgresai init postgresql://admin@host:5432/dbname -postgresai init "dbname=dbname host=host user=admin" -postgresai init -h host -p 5432 -U admin -d dbname +# Run a single bloat check +postgresai checkup F004 ``` - - +:::note Bloat checks need the monitoring role +The bloat estimation checks read catalog-level statistics that require the monitoring role +created by [`prepare-db`](#command-prepare-db). When the connection lacks the required +privileges, the check prints a hint: + +``` +Hint: Run "postgresai prepare-db " to create required objects. +``` + +Run `prepare-db` (or connect with a sufficiently privileged role) and re-run the check. +::: + +## Command: `mon` + +Manage the local monitoring stack (Docker Compose-based: collectors, +VictoriaMetrics, Grafana, …). + +**Usage** ```bash -npx postgresai init postgresql://admin@host:5432/dbname -npx postgresai init "dbname=dbname host=host user=admin" -npx postgresai init -h host -p 5432 -U admin -d dbname +postgresai mon [options] ``` - - +**Subcommands** + +- `local-install` — install the local monitoring stack: generate `.env`, configure services, and start them. +- `start` — start monitoring services. Runs `docker compose up -d` **only when the stack is not already running**: if any Grafana/pgwatch container is already up, it prints `Monitoring services are already running` and exits **without** running `up -d` (suggesting `mon restart`). When it does run, `up -d` creates or recreates containers as needed, so it applies a newly pulled image and triggers a `config-init` reseed when the image version no longer matches the config-volume marker. It uses plain `docker compose up -d` (not `--force-recreate`). A full-stack `up -d --force-recreate` is used by `mon local-install`; `mon targets add`/`remove` also force-recreate, but only the two pgwatch collector containers (`pgwatch-prometheus`, `pgwatch-postgres`). On an **already-running** stack a bare `mon start` is therefore a no-op — to apply a pulled image or recreate `config-init` on a live stack, run `docker compose up -d` directly, or `mon stop` then `mon start`. +- `stop` — stop monitoring services. +- `restart [service]` — restart all services or a specific one (`docker compose restart [service]`). Restarts the **existing** containers in place: it does **not** recreate them, does **not** apply a newly pulled image, and does **not** trigger a `config-init` reseed. To apply a new image or changed container env vars on a running stack, recreate the containers with `docker compose up -d` (a bare `mon start` no-ops while the stack is running, since it short-circuits with `Monitoring services are already running`). +- `status` — show services status. +- `health` — check that services are up and healthy. +- `logs [service]` — show logs for all services or a specific one. +- `config` — show monitoring configuration. +- `update-config` — apply configuration changes after editing `.env`: migrates `.env` additively (preserving existing values), refreshes the CLI-owned `docker-compose.yml` for non-git installs, and regenerates the pgwatch `sources.yml` (`docker compose run --rm sources-generator`). It does **not** regenerate the Grafana datasources, reseed the config volume, or restart any service. +- `update` — update the monitoring stack: migrates `.env` additively (preserving existing values), refreshes the repo/compose, and pulls the pinned images for the current tag (`docker compose pull`). It does **not** restart, recreate, or `up` any service — afterward you must recreate the containers to apply the new images. On a running stack do this with `docker compose up -d` directly (or `mon stop` then `mon start`, or re-run `mon local-install`, which uses `up -d --force-recreate`). The command prints a hint to run `mon restart`, but `docker compose restart` restarts containers in place on the old image and does not apply a pulled image; and a bare `mon start` is a **no-op** while the stack is running (it short-circuits with `Monitoring services are already running`), so it will not apply the new image on its own either. See [Upgrading the monitoring stack](/docs/monitoring/getting-started/upgrade) for the full upgrade flow (including the required `VM_AUTH_*` keys in 0.15). +- `reset [service]` — reset all services or a specific one (removes data). +- `clean` — clean up monitoring artifacts (stops services and removes volumes). +- `check` — system readiness check. +- `shell ` — open an interactive shell in a monitoring service container. +- `targets` — manage databases to monitor (see below). +- `generate-grafana-password` — generate a new Grafana password. +- `show-grafana-credentials` — show Grafana credentials. + +### Subcommand: `mon local-install` + +Install (or re-install) the local monitoring stack. Replaces the older +`mon quickstart` name. ```bash -bunx postgresai init postgresql://admin@host:5432/dbname -bunx postgresai init "dbname=dbname host=host user=admin" -bunx postgresai init -h host -p 5432 -U admin -d dbname +postgresai mon local-install [options] ``` - - +**Options** -**Common options** +- `--demo` — demo mode with a sample database (for testing; cannot be combined with `--api-key`). +- `--api-key ` — PostgresAI API key for automated report uploads. +- `--db-url ` — PostgreSQL connection URL to monitor (form: `postgresql://user:pass@host:port/db`). +- `--tag ` — Docker image tag to use (e.g. `0.15.0`, `0.15.0-dev.33`). +- `--project ` — project name. Used as the Docker Compose project name (default: `postgres_ai`). When an `--api-key` is supplied (non-demo install), it is **also** used as the project name when registering this monitoring instance with the PostgresAI Console; the registration default is `postgres-ai-monitoring`. +- `-y, --yes` — accept all defaults and skip interactive prompts. -- `--verify`: verify that monitoring role/permissions are in place (no changes). -- `--reset-password`: reset monitoring role password only. -- `--print-sql`: print SQL plan and exit (no changes applied). -- `--skip-optional-permissions`: skip optional permissions (managed and self-managed extras). +`local-install` writes `.env` in the monitoring directory, preserving +existing `REPLICATOR_PASSWORD` and `VM_AUTH_*` values or generating new +random ones when missing. `VM_AUTH_USERNAME` defaults to `vmauth` when +absent. The replication password is used by the demo PostgreSQL standby, +and the VM auth credentials are required before Docker Compose can +provision Grafana datasources. To rotate VM auth credentials manually, +run `VM_AUTH_PASSWORD="$(openssl rand -base64 18)" ./scripts/rotate-vm-auth.sh` +from the monitoring directory. -## Command: `mon` +### Subcommand: `mon health` -Manage monitoring services. +```bash +postgresai mon health [--wait ] +``` -**Usage** +- `--wait ` — wait up to `` for services to become healthy (default: `0`, i.e. check once and return). - - +### Subcommand: `mon logs` ```bash -postgresai mon [options] +postgresai mon logs [service] [options] ``` - - +- `-f, --follow` — follow logs. +- `--tail ` — number of trailing lines (default: `all`). + +### Subcommand: `mon clean` ```bash -npx postgresai mon [options] +postgresai mon clean [--keep-volumes] ``` - - +- `--keep-volumes` — keep data volumes (only stop and remove containers). + +### Subcommand group: `mon targets` + +Manage databases monitored by the local stack. ```bash -bunx postgresai mon [options] +postgresai mon targets [args] ``` - - +**Subcommands** + +- `list` — list configured monitoring targets. +- `add [conn-string] [name]` — add a Postgres instance to monitor. Both arguments are optional; missing values are prompted for interactively. +- `remove ` — remove a monitoring target. +- `test ` — test connectivity to a configured target. + +## Command: `login` + +Authenticate via browser (OAuth) or store an API key directly. This is +the shortest form of `postgresai auth login`; both commands use the same +options and behavior. + +**Usage** + +```bash +postgresai login # OAuth via browser +postgresai login --set-key # store an API key directly +postgresai login --port 7777 --debug # use a fixed callback port with debug output +``` + +**Options** + +- `--set-key ` — store an API key directly without going through the OAuth flow. +- `--port ` — local callback server port (default: random). +- `--debug` — enable debug output. + +The browser flow opens your default browser, prompts for organization +selection, and writes the resulting API key to +`~/.config/postgresai/config.json`. + +## Command: `auth` + +Authentication and API-key management. `auth` is a command group; the +default subcommand is `login`, so plain `postgresai auth` triggers an +OAuth flow. + +**Usage** + +```bash +postgresai auth [subcommand] [options] +``` **Subcommands** -- `quickstart`: complete setup (generate config and start services). -- `start`: start monitoring services. -- `stop`: stop monitoring services. -- `restart [service]`: restart all services or a specific service. -- `status`: show services status. -- `health`: check that services are up and healthy. -- `targets`: manage databases to monitor. -- `logs [service]`: show logs for all or a specific service. -- `config`: show monitoring configuration. -- `update-config`: apply configuration changes (generate sources). -- `update`: update monitoring stack. -- `reset [service]`: reset all or a specific service data. -- `clean`: cleanup artifacts. -- `check`: system readiness check. -- `shell `: open a shell in a monitoring service container. -- `generate-grafana-password`: generate a new Grafana password. -- `show-grafana-credentials`: show Grafana credentials. - -### Subcommand: `quickstart` - -Complete setup (generate config and start monitoring services). +- `login` (default) — authenticate via browser (OAuth) or store an API key directly. +- `show-key` — show the current API key, masked. +- `remove-key` — remove the stored API key. + +### Subcommand: `auth login` + +```bash +postgresai auth # OAuth via browser +postgresai auth --set-key # store an API key directly +postgresai auth login --port 7777 --debug # explicit form +``` + +For a shorter equivalent, use the top-level [`login`](#command-login) +command. + +**Options** + +- `--set-key ` — store an API key directly without going through the OAuth flow. +- `--port ` — local callback server port (default: random). +- `--debug` — enable debug output. + +The browser flow opens your default browser (OAuth with PKCE), prompts +for organization selection, and writes the resulting API key to +`~/.config/postgresai/config.json`. + +`postgresai login` is also available as a top-level alias for +`postgresai auth login` (same options). + +## Command: `joe` + +Run [Joe](/docs/joe-bot) SQL optimization commands on ephemeral +[DBLab](/docs/database-lab) thin clones. See the +[Joe from the CLI how-to](/docs/postgresai-howtos/joe-cli) for a +task-oriented walkthrough. + +:::caution dev channel +The `joe` and `projects` commands ship in CLI 0.16, currently published +under the **`dev`** npm dist-tag: run them via `npx pgai@dev …` / +`npx postgresai@dev …` (or install with `npm install -g postgresai@dev`) +until 0.16 reaches `latest`. +::: **Usage** - - +```bash +postgresai joe [arguments] [options] +``` + +**How it works** + +Every `joe` subcommand is synchronous: the CLI submits one raw Joe +command (the same text you could type at Joe in the Console or in chat), +then polls for the result until it is ready or the one-shot poll budget +(default 25 seconds) is exhausted. On budget expiry the CLI exits `0` and +prints a resume handle — fetch the result later with +`postgresai joe result `. Each invocation starts a fresh +Joe command; the command and its full result (plans, statistics, +recommendations) are stored in the Joe history in the PostgresAI +Console. + +Running Joe commands requires the token owner to hold the +**AllFeaturesUser** or **Admin** role in the organization; other roles +receive `403 Forbidden`. + +**Targeting.** Every `joe` subcommand (except `result`) needs a target. +Provide it in one of three ways: pass `--instance-id`, pass `--project`, +or configure a default project once with +[`set-default-project `](#command-set-default-project) and omit +both flags. Resolution order is `--instance-id`, then `--project`, then +the stored default project — an explicit flag always wins. With no flag +**and** no default project configured, the command errors and prompts you +to supply `--instance-id` (or `--project`). + +**Shared options** (every subcommand except `result`) + +- `--instance-id ` — target the Joe instance id directly (skips `--project` resolution). Takes precedence over `--project` and the default project. +- `--project ` — target a project by numeric id OR alias/name (case-insensitive; resolved via the projects API — see [`projects`](#command-projects)). Requires the project to have a registered, active Joe instance. When omitted, falls back to the default project set by [`set-default-project`](#command-set-default-project). +- `--budget ` — one-shot poll budget in seconds (default: `25`). +- `--debug` — enable debug output. +- `--json` — output the full result row as raw JSON (includes `plan_text`, structured `plan_json`, `plan_execution_text`, `plan_execution_json`, `stats`, `recommendations`, `queryid`). + +### `joe plan` + +`plan ` — plan a query (`EXPLAIN`, plan-only — **no execution**; the fast/safe default). ```bash -postgresai mon quickstart [--demo] [--api-key ] [--db-url ] [-y] +postgresai joe plan "select * from users where email = 'x@y.com'" --project main-db ``` - - +### `joe explain` + +`explain ` — `EXPLAIN` + `EXPLAIN ANALYZE` a query (**executes** on the DBLab clone). ```bash -npx postgresai mon quickstart [--demo] [--api-key ] [--db-url ] [-y] +postgresai joe explain "select * from users where email = 'x@y.com'" --project 12 ``` - - +### `joe exec` + +`exec ` — run arbitrary DDL/DML on the clone (e.g. `create index`, `analyze`, `set` planner parameters). ```bash -bunx postgresai mon quickstart [--demo] [--api-key ] [--db-url ] [-y] +postgresai joe exec "create index i_users_email on users (email)" --instance-id 34 ``` - - +### `joe hypo` -### Subcommand group: `targets` +`hypo ` — [HypoPG](https://github.com/HYPOPG/hypopg) hypothetical indexes (e.g. `hypo "create index on users (email)"`, `hypo desc`, `hypo reset`). -Manage databases to monitor. +### `joe activity` -**Usage** +`activity` — running-activity snapshot (`pg_stat_activity`) on the clone. - - +### `joe terminate` + +`terminate ` — `pg_terminate_backend(pid)` on the clone. The pid must be a bare positive integer; anything else is rejected client-side before any API call. + +### `joe reset` + +`reset` — reset/recreate the session's thin clone. + +### `joe describe` + +`describe ` — `\d`-family schema/relation/index metadata. Takes +`--variant ` to select the `\d`-family form (default `\d`). +Supported variants: `\d`, `\d+`, `\dt`, `\dt+`, `\di`, `\di+`, `\l`, +`\l+`, `\dv`, `\dv+`, `\dm`, `\dm+`. ```bash -postgresai mon targets [args] +postgresai joe describe users --variant '\d+' --project main-db ``` - - +### `joe result` + +`result ` — fetch a Joe command's output by id (resume a +budget-expired one-shot; accepts only `--debug` / `--json`). ```bash -npx postgresai mon targets [args] +postgresai joe result 3523 ``` - - +### Output and exit codes for `joe` + +Human-readable output prints `command · ok` followed by whichever +sections the result contains: the response text, `plan:`, client-side +plan flags (`⚑ …`, e.g. flagging a Seq Scan), `execution plan (EXPLAIN +ANALYZE):`, `stats:`, `recommendations:`, and the `queryid`. + +Exit codes: + +- `0` — terminal `ok` result, or budget expired (resume by id). +- `1` — terminal `error` result, `result` on a still-pending command, or any other failure. + +## Command: `projects` + +List the organization's projects, showing which ones have Joe ready. +This is org-level discovery (not a Joe endpoint): it powers +`--project ` resolution for [`joe`](#command-joe) commands. + +**Usage** ```bash -bunx postgresai mon targets [args] +postgresai projects [--json] [--debug] ``` - - +**Output columns** -**Subcommands** +- `PROJECT_ID` — numeric project id (usable as `--project `). +- `ALIAS` — project alias (usable as `--project `; `-` if not set). +- `PROJECT` — human-readable project name. +- `JOE` — `ready` when the project has an active Joe instance targetable by `joe` commands; `no` otherwise. +- `TUNNEL` — whether the project's DBLab tunnel is connected. -- `list`: list configured monitoring targets. -- `add [name]`: add a Postgres instance to monitor. -- `remove `: remove a monitoring target. -- `test `: test connectivity to a configured target. +With `--json`, each row also includes `instance_id` (the Joe instance id +that `joe` commands target — usable as `--instance-id`) and +`dblab_instance_id` (the project's active DBLab instance, not used by +`joe` commands). + +``` +PROJECT_ID ALIAS PROJECT JOE TUNNEL +12 main-db Main DB ready yes +15 analytics Analytics no no +``` ## Command: `issues` -Manage issue reports in PostgresAI Console. +Manage issues, comments, and action items in the PostgresAI Console. **Usage** - - - ```bash postgresai issues [options] ``` - - +All `issues` subcommands accept `--debug` (enable debug output) and +`--json` (force raw JSON output instead of the default human-friendly +YAML). When stdout is not a TTY (e.g. piped or redirected), JSON is +selected automatically. + +**Subcommands** + +- `list` — list issues. +- `view ` — view issue details and comments. +- `create [options]` — create a new issue. +- `update <issueId> [options]` — update an existing issue. +- `post-comment <issueId> <content> [options]` — post a comment. +- `update-comment <commentId> <content> [options]` — update an existing comment. +- `files upload <path>` — upload a file to storage and print a markdown link. +- `files download <url> [-o <path>]` — download a file from storage. +- `action-items <issueId>` — list action items for an issue. +- `view-action-item <id> [<id> ...]` — view one or more action items in detail. +- `create-action-item <issueId> <title> [options]` — create an action item. +- `update-action-item <actionItemId> [options]` — update an action item. + +### `issues list` ```bash -npx postgresai issues <subcommand> [options] +postgresai issues list [--status <status>] [--limit <n>] [--offset <n>] ``` -</TabItem> -<TabItem value="bunx" label="bunx"> +- `--status <status>` — filter by status: `open`, `closed`, or `all` (default: `all`). +- `--limit <n>` — maximum number of issues to return (default: `20`). +- `--offset <n>` — number of issues to skip (default: `0`). + +### `issues view` ```bash -bunx postgresai issues <subcommand> [options] +postgresai issues view <issueId> ``` -</TabItem> -</Tabs> +### `issues create` -**Subcommands** +```bash +postgresai issues create <title> [options] +``` -- `list`: list issues. -- `view <issue_id>`: view issue details (and comments). -- `post_comment <issue_id> <content>`: post a comment to an issue. +- `--org-id <id>` — organization ID (defaults to the configured `orgId`). +- `--project-id <id>` — project ID. +- `--description <text>` — issue description (use `\n` for newlines). +- `--label <label>` — issue label; repeat to add multiple. +- `--attach <path>` — attach a local file (uploads to storage and appends a markdown link to the description); repeatable. -**Examples** +### `issues update` -<Tabs groupId="cli-runner" queryString> -<TabItem value="cli" label="Installed CLI" default> +```bash +postgresai issues update <issueId> [options] +``` + +- `--title <text>` — new title (use `\n` for newlines). +- `--description <text>` — new description (use `\n` for newlines). +- `--status <value>` — `open`, `closed`, `0`, or `1`. +- `--label <label>` — set labels; repeatable. If provided, replaces existing labels. +- `--clear-labels` — set labels to an empty list. +- `--attach <path>` — attach a file; appends a markdown link to `--description`. If `--description` is omitted, the existing description is fetched and the link appended to it. + +### `issues post-comment` ```bash -postgresai issues list -postgresai issues view <issue_id> -postgresai issues post_comment <issue_id> "comment" +postgresai issues post-comment <issueId> <content> [options] ``` -</TabItem> -<TabItem value="npx" label="npx"> +- `--parent <uuid>` — parent comment ID (for threaded replies). +- `--attach <path>` — attach a file; appends a markdown link to the comment body. Repeatable. + +### `issues update-comment` ```bash -npx postgresai issues list -npx postgresai issues view <issue_id> -npx postgresai issues post_comment <issue_id> "comment" +postgresai issues update-comment <commentId> <content> [options] ``` -</TabItem> -<TabItem value="bunx" label="bunx"> +- `--attach <path>` — attach a file; appends a markdown link to `<content>`. Repeatable. + +### `issues files` ```bash -bunx postgresai issues list -bunx postgresai issues view <issue_id> -bunx postgresai issues post_comment <issue_id> "comment" +# Upload a local file; prints the storage URL and a ready-to-paste markdown link. +postgresai issues files upload <path> + +# Download a file from storage; without -o, derives the filename from the URL. +postgresai issues files download <url> [-o <output_path>] ``` -</TabItem> -</Tabs> +#### Attaching files to issues and comments (`--attach`) -## Command: `mcp` +`create`, `update`, `post-comment`, and `update-comment` accept a +repeatable `--attach <path>` flag. Each file is uploaded to PostgresAI +storage and a markdown link is appended to the comment body or issue +description. Image extensions (`.png`, `.jpg`, `.jpeg`, `.gif`, +`.webp`, `.svg`, `.bmp`, `.ico`) render inline as `![](url)`; other +files render as `[](url)`. Multiple `--attach` flags preserve order; +each link goes on its own line. -MCP server integration for AI coding tools. +```bash +# Attach a screenshot to a new comment +postgresai issues post-comment <issueId> "Saw this in prod" --attach screenshot.png -**Usage** +# Attach multiple files to a new issue +postgresai issues create "Slow query" --org-id 4 \ + --description "Plan attached" --attach plan.txt --attach flame.svg -<Tabs groupId="cli-runner" queryString> -<TabItem value="cli" label="Installed CLI" default> +# Attach a file to an existing issue without changing the description +postgresai issues update <issueId> --attach trace.log +``` + +### `issues action-items` ```bash -postgresai mcp <subcommand> [options] +postgresai issues action-items <issueId> +postgresai issues view-action-item <actionItemId> [<actionItemId> ...] ``` -</TabItem> -<TabItem value="npx" label="npx"> +### `issues create-action-item` ```bash -npx postgresai mcp <subcommand> [options] +postgresai issues create-action-item <issueId> <title> [options] ``` -</TabItem> -<TabItem value="bunx" label="bunx"> +- `--description <text>` — detailed description (use `\n` for newlines). +- `--sql-action <sql>` — SQL command to execute. +- `--config <json>` — config change as JSON, e.g. `'{"parameter":"work_mem","value":"64MB"}'`. Repeatable. + +### `issues update-action-item` ```bash -bunx postgresai mcp <subcommand> [options] +postgresai issues update-action-item <actionItemId> [options] ``` -</TabItem> -</Tabs> +- `--title <text>`, `--description <text>` — update title or description. +- `--done` / `--not-done` — mark as done or not done. +- `--status <value>` — `waiting_for_approval`, `approved`, or `rejected`. +- `--status-reason <text>` — reason for the status change. +- `--sql-action <sql>` — update the SQL command (use `""` to clear). +- `--config <json>` — replace config changes; repeatable. +- `--clear-configs` — remove all config changes. -**Subcommands** +### Output format for `issues` commands + +By default, `issues` commands print human-friendly YAML to a terminal. +For scripting: -- `start`: start the MCP server in stdio mode. -- `install [client]`: install MCP client configuration for a supported tool. +- Pass `--json` to force JSON output: -## Command: `add-key` + ```bash + postgresai issues list --json | jq '.[] | {id, title}' + ``` -Store an API key locally. +- Or rely on auto-detection: when stdout is not a TTY, output is JSON + automatically: + + ```bash + postgresai issues view <issueId> > issue.json + ``` + +## Command: `reports` + +List and download checkup reports stored in the PostgresAI Console. **Usage** -<Tabs groupId="cli-runner" queryString> -<TabItem value="cli" label="Installed CLI" default> +```bash +postgresai reports <subcommand> [options] +``` + +**Subcommands** + +- `list [options]` — list checkup reports. +- `files [reportId] [options]` — list files (metadata only) of a checkup report. +- `data [reportId] [options]` — fetch report file contents (markdown / JSON). + +### `reports list` ```bash -postgresai add-key <key> +postgresai reports list [options] ``` -</TabItem> -<TabItem value="npx" label="npx"> +- `--project-id <id>` — filter by project ID. +- `--limit <n>` — maximum number of reports to return (default: `20`, max: `100`). +- `--before <date>` — show reports created before this date (`YYYY-MM-DD`, `DD.MM.YYYY`, etc.). +- `--all` — fetch all reports (paginated automatically). Mutually exclusive with `--before`. +- `--json` — output raw JSON. + +### `reports files` ```bash -npx postgresai add-key <key> +postgresai reports files [reportId] [options] ``` -</TabItem> -<TabItem value="bunx" label="bunx"> +Either `reportId` or `--check-id` is required. + +- `--type <type>` — filter by file type: `json` or `md`. +- `--check-id <id>` — filter by check ID (e.g. `H002`). +- `--json` — output raw JSON. + +### `reports data` ```bash -bunx postgresai add-key <key> +postgresai reports data [reportId] [options] ``` -</TabItem> -</Tabs> +- `--type <type>` — filter by file type: `json` or `md`. +- `--check-id <id>` — filter by check ID (e.g. `H002`). +- `--formatted` — render markdown with ANSI styling (experimental). +- `-o, --output <dir>` — save files to a directory (using their original filenames). +- `--json` — output raw JSON. -## Command: `show-key` +## Command: `mcp` -Show the currently configured API key (masked). +MCP (Model Context Protocol) server integration for AI coding tools. **Usage** -<Tabs groupId="cli-runner" queryString> -<TabItem value="cli" label="Installed CLI" default> - ```bash -postgresai show-key +postgresai mcp <subcommand> [options] ``` -</TabItem> -<TabItem value="npx" label="npx"> +**Subcommands** + +- `start` — start the MCP stdio server, exposing PostgresAI tools. +- `install [client]` — install MCP client configuration for a supported AI coding tool. + +### `mcp start` ```bash -npx postgresai show-key +postgresai mcp start [--debug] ``` -</TabItem> -<TabItem value="bunx" label="bunx"> +Starts an MCP server over stdio. Intended to be launched by an MCP +client (e.g. Cursor, Claude Code) rather than invoked directly. + +### `mcp install` ```bash -bunx postgresai show-key +postgresai mcp install [client] ``` -</TabItem> -</Tabs> +Installs an `mcpServers.postgresai` entry pointing at the **absolute +path of the `pgai` binary that invoked `mcp install`**, with `mcp +start` as its arguments. -## Command: `remove-key` +`client` may be one of: -Remove the stored API key. +- `cursor` — writes to `~/.cursor/mcp.json`. +- `claude-code` — runs `claude mcp add -s user postgresai <pgai> mcp start`. +- `windsurf` — writes to `~/.windsurf/mcp.json`. +- `codex` — writes to `~/.codex/mcp.json`. -**Usage** +If `client` is omitted, you are prompted to choose interactively +(1=Cursor, 2=Claude Code, 3=Windsurf, 4=Codex). -<Tabs groupId="cli-runner" queryString> -<TabItem value="cli" label="Installed CLI" default> +:::note + +The pinned `command` path is the absolute path resolved at install +time. When `mcp install` is run via `npx` or `bunx`, that path points +into the package cache and may be garbage-collected. For a stable +install, run `mcp install` from a globally installed CLI +(`npm install -g postgresai` or `brew install postgresai`), or re-run +`mcp install` after each CLI upgrade. + +::: + +A typical Cursor entry written by `mcp install` looks like: + +```json +{ + "mcpServers": { + "postgresai": { + "command": "<absolute-path-to-pgai>", + "args": ["mcp", "start"] + } + } +} +``` + +The `command` value is the absolute path resolved by `mcp install` at +install time. Typical values: + +- `/opt/homebrew/bin/pgai` — Homebrew on Apple Silicon macOS +- `/usr/local/bin/pgai` — Homebrew on Intel macOS or `npm install -g` on Linux/macOS +- `~/.nvm/versions/node/<version>/bin/pgai` — `npm install -g` under nvm +- `~/.npm/_npx/<hash>/node_modules/.bin/pgai` — invoked via `npx` (ephemeral; see the note above) + +To point the server at a non-production endpoint, add an `env` block +manually: + +```json +"env": { + "PGAI_API_BASE_URL": "https://v2.postgres.ai/api/general/", + "PGAI_UI_BASE_URL": "https://console-dev.postgres.ai" +} +``` + +**MCP tools exposed** + +The 0.15 MCP server registers 15 tools in four groups. + +*Issues:* + +- `list_issues` — same JSON as `postgresai issues list`. +- `view_issue` — view a single issue with its comments. +- `create_issue` — create a new issue. +- `update_issue` — update title / description / status / labels. +- `post_issue_comment` — post a comment. +- `update_issue_comment` — update an existing comment. + +*Action items:* + +- `list_action_items` — list action items for an issue. +- `view_action_item` — view one or more action items with full detail. +- `create_action_item` — create an action item (title, description, optional `sql_action` and config changes) for an issue. +- `update_action_item` — mark done / not done, approve / reject, or edit an action item. + +*Reports:* (new in 0.15 — the only place the reports capability is exposed to AI agents) + +- `list_reports` — list checkup reports (metadata: id, project, status, timestamps; supports `before_date` filtering). +- `list_report_files` — list files in a report (per-check `json` / `md` files; filter by `report_id`, `type`, or `check_id`). +- `get_report_data` — fetch report file content (`type=md` for analysis, `type=json` for raw check data). + +*Files:* + +- `upload_file` — upload a local file and return the storage URL plus a ready-to-paste markdown link. +- `download_file` — download a file from storage. + +The issue / comment tools accept an optional `attachments: string[]` of +local file paths. Each file is uploaded to PostgresAI storage and the +resulting markdown link is appended to the comment body or issue +description, using the same image-extension rules as the `--attach` +CLI flag. + +For `post_issue_comment` and `update_issue_comment`, either `content` or +`attachments` must be non-empty (attachments alone are allowed). For +`update_issue` with `attachments` but no `description`, the existing +description is fetched first and the new links are appended to it. + +#### MCP threat model + +The MCP server runs in your local user account with your PostgresAI API +key. It treats the connected MCP client (the LLM agent) as **trusted** — +the same way the CLI treats you when you type a command. In particular: + +- `upload_file` and the `attachments: string[]` parameter on the issue / + comment tools read **any local file the CLI process can read**, + including secrets like `~/.ssh/id_rsa`, `~/.aws/credentials`, or + `~/.config/postgresai/config.json` (which contains your own API + key). The file's bytes are uploaded to PostgresAI storage and the + resulting URL becomes visible to anyone with read access to the + issue or comment it ends up in. +- `download_file` writes to **any path the CLI process can write to** + when `output_path` is supplied (`~/.ssh/authorized_keys`, + `~/.bashrc`, etc. are all fair game). When `output_path` is omitted, + downloads are restricted to the current working directory. + +This is fine when the agent and the upstream context the agent is +reading are trusted. It is **not** safe to run this MCP server against +an agent that is processing untrusted text (issue bodies, comments, web +pages, third-party docs) without additional sandboxing — a +prompt-injection in any input the agent reads could be used to +exfiltrate local secrets or write arbitrary files. If you need to +expose this MCP server to such an agent, run the agent (and this +server) in a container or restricted user account that has no access +to anything sensitive. + +## Command: `set-default-project` + +Store the default project used for `checkup` uploads and other +project-scoped operations. ```bash -postgresai remove-key +postgresai set-default-project <project> ``` -</TabItem> -<TabItem value="npx" label="npx"> +## Command: `set-storage-url` + +Store the storage base URL used for file uploads. Equivalent to setting +`PGAI_STORAGE_BASE_URL` permanently in the configuration file. ```bash -npx postgresai remove-key +postgresai set-storage-url <url> ``` -</TabItem> -<TabItem value="bunx" label="bunx"> +## Configuration + +The CLI stores configuration in `~/.config/postgresai/config.json`, +including: + +- API key +- API / UI / storage base URLs +- Organization ID +- Default project + +### Configuration priority + +API key resolution order: + +1. Command-line option (`--api-key`). +2. Environment variable (`PGAI_API_KEY`). +3. User config file (`~/.config/postgresai/config.json`). +4. Legacy project config (`.pgwatch-config`). + +Base URL resolution order: + +- API base URL (`apiBaseUrl`): + 1. Command-line option (`--api-base-url`). + 2. Environment variable (`PGAI_API_BASE_URL`). + 3. User config file (`baseUrl` in `~/.config/postgresai/config.json`). + 4. Default: `https://postgres.ai/api/general/`. +- UI base URL (`uiBaseUrl`): + 1. Command-line option (`--ui-base-url`). + 2. Environment variable (`PGAI_UI_BASE_URL`). + 3. Default: `https://console.postgres.ai`. +- Storage base URL (`storageBaseUrl`): + 1. Command-line option (`--storage-base-url`). + 2. Environment variable (`PGAI_STORAGE_BASE_URL`). + 3. Value stored by `postgresai set-storage-url`. + 4. Default: `https://postgres.ai/storage`. + +A single trailing `/` is stripped from URL values to ensure consistent +path joining. + +### Environment variables + +- `PGAI_API_KEY` — API key for PostgresAI services. +- `PGAI_API_BASE_URL` — API endpoint for backend RPC (default: `https://postgres.ai/api/general/`). +- `PGAI_UI_BASE_URL` — UI endpoint for browser routes (default: `https://console.postgres.ai`). +- `PGAI_STORAGE_BASE_URL` — storage endpoint for file uploads. +- `PGAI_MON_PASSWORD` — default password for the monitoring role created by `prepare-db`. +- `PGPASSWORD` — admin password used by `prepare-db` / `unprepare-db` when `--admin-password` is not given. +- `SUPABASE_ACCESS_TOKEN`, `SUPABASE_PROJECT_REF` — credentials for `prepare-db --supabase`. + +### Examples + +For production (uses default URLs): ```bash -bunx postgresai remove-key +postgresai auth --debug ``` -</TabItem> -</Tabs> +For staging / development environments: + +```bash +# Linux / macOS (bash, zsh) +export PGAI_API_BASE_URL=https://v2.postgres.ai/api/general/ +export PGAI_UI_BASE_URL=https://console-dev.postgres.ai +postgresai auth --debug +``` + +```powershell +# Windows PowerShell +$env:PGAI_API_BASE_URL = "https://v2.postgres.ai/api/general/" +$env:PGAI_UI_BASE_URL = "https://console-dev.postgres.ai" +postgresai auth --debug +``` + +Via CLI options (overrides environment variables): + +```bash +postgresai auth --debug \ + --api-base-url https://v2.postgres.ai/api/general/ \ + --ui-base-url https://console-dev.postgres.ai +``` diff --git a/docs/roadmap.md b/docs/roadmap.md index 6756167d..36653a0c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -43,7 +43,7 @@ We're not there yet. Today, **PostgresAI** watches, diagnoses, and prepares pull │ └── PostgresAI Assistant (AI chat) │ │ -2025 │ POSTGRESAI ◄── WE ARE HERE +2025-26 │ POSTGRESAI ◄── WE ARE HERE │ │ AI watches, diagnoses, suggests │ Expert validation on every recommendation @@ -88,7 +88,7 @@ We don't believe in "trust us, we're AI." PostgresAI starts with human approval The core monitoring tool (postgres_ai) is Apache 2.0 licensed. We believe in transparency and community-driven development. The building blocks are open; the intelligence layer is how we sustain the business. -## Current milestone: PostgresAI (2025) +## Current milestone: PostgresAI (2026) Here's how PostgresAI works today: @@ -347,7 +347,7 @@ Logical provisioning: native support of DB provisioning for managed Postgres dat - [x] Basic support for masking and obfuscation - [x] custom scripts - [x] parallel execution of custom scripts - - [x] [postgres_anonymizer](https://postgresql-anonymizer.readthedocs.io/en/stable/masking_functions.html) + - [x] [postgres_anonymizer](https://postgresql-anonymizer.readthedocs.io/en/stable/masking_functions/) - [x] [kitchen-sync](https://github.com/willbryant/kitchen_sync) - [ ] [pgsync](https://github.com/ankane/pgsync) - [ ] Hybrid setup: raw and obfuscated/masked clones on the same DBLab instance diff --git a/docs/tutorials/database-lab-tutorial-amazon-rds.md b/docs/tutorials/database-lab-tutorial-amazon-rds.md index 44022eb1..d455e863 100644 --- a/docs/tutorials/database-lab-tutorial-amazon-rds.md +++ b/docs/tutorials/database-lab-tutorial-amazon-rds.md @@ -19,7 +19,7 @@ Currently, the AWS Marketplace version of DLE focuses on the "logical" data prov Compared to traditional RDS clones, Database Lab clones are instant. RDS cloning takes several minutes, and, depending on the database size, additional dozens of minutes or even hours may be needed to "warm up" the database (see ["Lazy load"](https://docs.amazonaws.cn/en_us/AWSEC2/latest/WindowsGuide/ebs-creating-volume.html#ebs-create-volume-from-snapshot)). Obtaining a new DLE clone takes as low as a few seconds, and it does not increase storage and instance bill at all. -A single DLE instance can be used by dozens of engineers or CI/CD pipelines – all of them can work with dozens of thin clones located on a single instance and single storage volume. [RDS Aurora clones](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Clone.html) are also "thin" by nature, which could be great for development and testing. However, each Aurora clone requires a provisioned instance, increasing the "compute" part of the bill; IO-related charges can be significant as well. This makes Aurora clones less attractive for the use in non-production environments. The use of DLE clones doesn't affect the bill anyhow – both "compute" and "storage" costs remain constant regardles of the number clones provisioned at any time. +A single DLE instance can be used by dozens of engineers or CI/CD pipelines – all of them can work with dozens of thin clones located on a single instance and single storage volume. [RDS Aurora clones](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Clone.html) are also "thin" by nature, which could be great for development and testing. However, each Aurora clone requires a provisioned instance, increasing the "compute" part of the bill; IO-related charges can be significant as well. This makes Aurora clones less attractive for the use in non-production environments. The use of DLE clones doesn't affect the bill anyhow – both "compute" and "storage" costs remain constant regardless of the number of clones provisioned at any time. ## Typical "pilot" setup Timeline: @@ -46,7 +46,7 @@ Outcome: ## Step 1. Install DLE from the AWS Marketplace First steps to install DLE from the AWS Marketplace are trivial: -- Log in into AWS: https://console.aws.amazon.com/ +- Log in to AWS: https://console.aws.amazon.com/ - Open the DBLab on [AWS Marketplace page](https://aws.amazon.com/marketplace/pp/prodview-wlmm2satykuec) And press the "View purchase options" button: @@ -91,7 +91,7 @@ Now, it is time to fill the form that defines the AWS resources that we need: Next, on the same page: - define the size of EBS volume that will be created (you can find pricing calculator here: ["Amazon EBS pricing"](https://aws.amazon.com/ebs/pricing/)): - put as many GiB as roughly your database has (it is always possible to add more space without downtime), - - define how many snapshots you'll be needed (minimum 2); + - define how many snapshots you'll need (minimum 2); - define secret token (at least 9 characters are required!) – it will be used to communicate with DBLab API, CLI, and UI. Then, press "Next". @@ -112,7 +112,7 @@ Once you've pressed "Submit", the process begins. You need to wait a few minutes while all resources are being provisioned. Check out the "Outputs" section periodically. Once DLE API and UI are ready, you should see the ordered list of instructions on how to connect to UI and API. ## Step 2. Configure and launch the DBLab Engine -Enter the verification token, you have created earlier. You can also find it in the "Outputs" section. +Enter the verification token you have created earlier. You can also find it in the "Outputs" section. <p align="center"> <img src="/assets/dle-aws/DLE_config_step1.png" alt="DBLab Engine configuration: step 1" /> @@ -153,7 +153,7 @@ If data provisioning fails, you can always: - adjust the configuration in the "Configuration" tab, and - perform a new attempt to initialize DLE. -If something went south in general and you need a fresh start, go back to AWS CloudFormation and delete your stack; then start from the very beginning of this tutorial +If something went south in general and you need a fresh start, go back to AWS CloudFormation and delete your stack; then start from the very beginning of this tutorial. ## Getting support With DLE installed from AWS Marketplace, the guaranteed vendor support is included – please use [one of the available ways to contact](https://postgres.ai/contact). @@ -189,7 +189,7 @@ With DBLab, you can create safe, instant copies of your database: perfect for te ##### Connect to a clone 1. From the **Database Lab clone** page under section **Connection info**, copy the **psql connection string** field contents by clicking the **Copy** button. ![Database Lab clone page / psql connection string](/assets/dle-aws/AWS_DLE_connect_clone1.png) -1. Here we assume that you have `psql` installed on your working machine. In the terminal, type `psql` and paste the **psql connection string** field contents. Change the database name `DBNAME` parameter, you can always use `postgres` for the initial connection. +1. Here we assume that you have `psql` installed on your working machine. In the terminal, type `psql` and paste the **psql connection string** field contents. Change the database name `DBNAME` parameter — you can always use `postgres` for the initial connection. 1. Run the command and type the password you've set during the clone creation. 1. Test established connection by listing tables in the database using `\d`. ![Terminal / psql](/assets/guides/connect-clone-2.png) @@ -378,5 +378,5 @@ dblab branch --snapshot-id SNAPSHOT_ID my_first_branch For more, see [the full client CLI reference](/docs/reference-guides/dblab-client-cli-reference). :::info Have questions? -[Reach out to the PostgresAI team](https://postgres.ai/contact), we'll be happy to help! +[Reach out to the PostgresAI team](https://postgres.ai/contact) — we'll be happy to help! ::: diff --git a/docs/tutorials/database-lab-tutorial.md b/docs/tutorials/database-lab-tutorial.md index c142ac0d..859ec078 100644 --- a/docs/tutorials/database-lab-tutorial.md +++ b/docs/tutorials/database-lab-tutorial.md @@ -5,7 +5,7 @@ keywords: - "DBLab tutorial" - "Start using DBLab Engine" - "PostgresAI tutorial" -description: In this tutorial, we are going set up a DBLab Engine in the Cloud. DBLab is used to boost software development and testing processes via enabling ultra-fast provisioning of databases of any size. +description: In this tutorial, we are going to set up a DBLab Engine in the Cloud. DBLab is used to boost software development and testing processes via enabling ultra-fast provisioning of databases of any size. --- DBLab Engine is used to boost software development and testing processes by enabling ultra-fast provisioning of databases of any size. @@ -43,7 +43,7 @@ If your cloud vendor is not supported by Option 1 or if you are using an on-prem In both scenarios, your data remains securely within your infrastructure. -## Step 1. Deploying DBLab in Cloud +## Step 1. Deploying DBLab in cloud ### Prerequisites - Sign up for an account at https://console.postgres.ai/, using one of four supported methods: Google, LinkedIn, GitHub, GitLab - [Create](https://console.postgres.ai/addorg) a new organization @@ -113,7 +113,7 @@ Review the specifications of the virtual machine, and click "Create DBLab": <img src="/assets/dle-platform/Platform_DLE_step9.v2.png" alt="DBLab Engine in DBLab Platform: step 9" width="50%"/> </p> -Select the installation method and follow the instructions to create server and install DBLab SE: +Select the installation method and follow the instructions to create a server and install DBLab SE: <p align="center"> <img src="/assets/dle-platform/Platform_DLE_step10.v3.png" alt="DBLab Engine in DBLab Platform: step 10" /> </p> @@ -122,7 +122,7 @@ Select the installation method and follow the instructions to create server and To perform the initial deployment, a new temporary SSH key will be generated and added to the Cloud. After the deployment is completed, this key will be deleted and the SSH key that was specified in the "ssh_public_keys" variable will be added to the server. ::: -After running the deployment command, You need to wait a few minutes, while all resources are provisioned and DBLab setup is complete. Check out the "usage instructions" – once DBLab API and UI are ready, you'll see the ordered list of instructions on how to connect to UI and API. +After running the deployment command, you need to wait a few minutes, while all resources are provisioned and DBLab setup is complete. Check out the "usage instructions" – once DBLab API and UI are ready, you'll see the ordered list of instructions on how to connect to UI and API. Example: @@ -144,7 +144,7 @@ ok: [root@5.161.212.233] => { "", "5) DBLab CLI:", " - CLI ('dblab') setup:", - " export DBLAB_CLI_VERSION=4.0.3", + " export DBLAB_CLI_VERSION=4.1.1", " curl -sSL dblab.sh | bash", " dblab init --environment-id=dblab-demo --token=edlhYHOgBPkr4ix1qP3YvQMytfK2JSxH --url=http://127.0.0.1:2346/api", " - CLI docs: https://cli-docs.dblab.dev/", @@ -188,7 +188,7 @@ Now UI should be available at http://127.0.0.1:2346 Currently, configuring DBLab in UI allows config changes only for the "logical" mode of data retrieval (dump/restore) – the only available method for managed PostgreSQL cloud services such as RDS Postgres, RDS Aurora Postgres, Azure Postgres, or Heroku. "Physical" mode is not yet supported in UI but is still possible (through SSH connection and [editing DBLab config file directly](/docs/dblab-howtos/administration/engine-manage)). More about [various data retrieval options for DBLab](/docs/dblab-howtos/administration/data). ::: -Enter the verification token, you have created earlier. +Enter the verification token you have created earlier. <p align="center"> <img src="/assets/dle-platform/DLE_config_step1.png" alt="DBLab Engine configuration: step 1" /> @@ -264,7 +264,7 @@ You also can click the "Enable deletion protection" box. When enabled no one can # Replace with your server IP and clone port ssh -N -L 6000:127.0.0.1:6000 ubuntu@35.183.123.243 ``` -3. Here we assume that you have `psql` installed on your working machine. In the terminal, type `psql` and paste the **psql connection string** field contents. Change the database name `DBNAME` parameter, you can always use `postgres` for the initial connection. +3. Here we assume that you have `psql` installed on your working machine. In the terminal, type `psql` and paste the **psql connection string** field contents. Change the database name `DBNAME` parameter — you can always use `postgres` for the initial connection. 4. Run the command and type the password you've set during the clone creation. 5. Test established connection by listing tables in the database using `\dt`. ```bash @@ -495,7 +495,7 @@ To troubleshoot: - Use SSH to connect to the DBLab server - Check the containers that are running: `sudo docker ps` - Check the DBLab container's logs: `sudo docker logs dblab_server` -- If needed, check Postgres logs for the main branch. They are located in `/var/lib/dblab/dblab_pool/dataset_1/data/log` for the first snapshot of the database, in ``/var/lib/dblab/dblab_pool/dataset_2/data/log` for the second one (if it's already fetched); if you've configured DBLab to have more than 2 snapshots, check out the other directories too (`/var/lib/dblab/dblab_pool/dataset_$N/data/log`, where `$N` is the snapshot number, starting with `1`) +- If needed, check Postgres logs for the main branch. They are located in `/var/lib/dblab/dblab_pool/dataset_1/data/log` for the first snapshot of the database, in `/var/lib/dblab/dblab_pool/dataset_2/data/log` for the second one (if it's already fetched); if you've configured DBLab to have more than 2 snapshots, check out the other directories too (`/var/lib/dblab/dblab_pool/dataset_$N/data/log`, where `$N` is the snapshot number, starting with `1`) ## Getting support With DBLab installed from DBLab Platform, guaranteed vendor support is included – please use [one of the available ways to contact](https://postgres.ai/contact). diff --git a/docs/tutorials/joe-setup.md b/docs/tutorials/joe-setup.md index 0ef5583e..066b065b 100644 --- a/docs/tutorials/joe-setup.md +++ b/docs/tutorials/joe-setup.md @@ -14,7 +14,7 @@ keywords: description: Learn how to use Joe bot to build a swift workflow of PostgreSQL query optimization running EXPLAIN commands on ultra-fast thin clones. --- -[↵ Back to Guides](/docs/guides/) +[↵ Back to DBLab how-to guides](/docs/dblab-howtos) ## Step 1. Requirements - Set up [DBLab Engine](/docs/tutorials/database-lab-tutorial) (e.g., running on address https://dblab.domain.com) before configuring Joe Bot @@ -28,7 +28,7 @@ There are two available types of communication with Joe: - Web UI powered by [PostgresAI Console](https://postgres.ai/console/) - Slack -You can use both of them in parallel. If you can develop in Go language, feel free to implement more types of communication: see [communication channels issues](https://gitlab.com/postgres-ai/joe/-/issues?label_name%5B%5D=Communication+channel). +You can use both of them in parallel. If you can develop in Go language, feel free to implement more types of communication: see the [Joe Bot issue tracker](https://github.com/postgres-ai/joe/issues). We need to define where to store the configuration file. We will use `~/.dblab/joe/configs/joe.yml`. @@ -43,7 +43,7 @@ curl -fsSL https://gitlab.com/postgres-ai/joe/-/raw/0.10.0/configs/config.exampl Then, configure ways of communication with Joe. ### Step 2a. Set up Joe in PostgresAI Console ("Web UI") -If you don't need Web UI and prefer working with Joe only in messengers (such as Slack), comment out `channelMapping: communicationTypes: webui` subsection in Jog config, and proceed to the next step. +If you don't need Web UI and prefer working with Joe only in messengers (such as Slack), comment out `channelMapping: communicationTypes: webui` subsection in Joe config, and proceed to the next step. Before configuring Web UI make sure you have a PostgresAI account. @@ -66,17 +66,17 @@ Configure a new Slack App in order to use Joe in Slack and add the app to your t 1. Create `#db-lab` channel in your Slack Workspace (You can use another channel name). 1. [Create a new Slack App](https://api.slack.com/apps?new_app=1). - * Choose *From an app manifest* option in popup. + * Choose *From an app manifest* option in the popup. ![Slack App - create app from app manifest](/assets/joe/tutorial-slack-create-app.png) - * Paste next yaml. + * Paste the following YAML. ```yaml _metadata: major_version: 1 minor_version: 1 display_information: name: Joe Bot - description: PostgreSQL query optimization assistent + description: PostgreSQL query optimization assistant background_color: "#2b2c30" features: app_home: @@ -148,7 +148,7 @@ and we are ready to run Joe Bot. sudo docker logs -f joe_bot ``` - Need you to reconfigure or upgrade, you can stop and remove the container any time using `sudo docker stop joe_bot` and `sudo docker rm joe_bot` and then launching it again as described above. + If you need to reconfigure or upgrade, you can stop and remove the container any time using `sudo docker stop joe_bot` and `sudo docker rm joe_bot`, and then launch it again as described above. 1. Make a publicly accessible HTTP(S) server port specified in the configuration to receive requests from communication channels Request URL (e.g., http://35.200.200.200:2400, https://joe.dev.domain.com). @@ -172,5 +172,5 @@ Instead of working using insecure HTTP, you can set up NGINX with SSL enabled an See available configuration options [here](/docs/reference-guides/joe-bot-configuration-reference). :::info Have questions? -Reach out to our team [here](https://postgres.ai/contact/), we'll be happy to help! +Reach out to our team [here](https://postgres.ai/contact/) — we'll be happy to help! ::: diff --git a/docusaurus.config.js b/docusaurus.config.js index 2b8954af..4c538648 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -35,7 +35,7 @@ module.exports = { tagline: SITE_SLOGAN, url: URL, // Your website URL. baseUrl: BASE_URL, // Base URL for your project. - onBrokenLinks: 'warn', //'throw', + onBrokenLinks: 'warn', //'throw' — tracked in #189 (fix pre-existing broken links first) favicon: '/favicon.svg', organizationName: 'postgres-ai', projectName: 'docs', @@ -44,6 +44,7 @@ module.exports = { // Files with <placeholder> text need HTML entities: <placeholder> markdown: { format: 'mdx', + mermaid: true, }, customFields: { @@ -429,6 +430,15 @@ module.exports = { { from: '/docs/platform/how-to-install-mcp', to: '/docs/postgresai-howtos/how-to-install-mcp' }, { from: '/docs/platform/how-to-work-with-issues', to: '/docs/postgresai-howtos/how-to-work-with-issues' }, + // Performance optimization redirects (moved from statistics to monitoring / other categories) + { from: '/docs/postgres-howtos/performance-optimization/statistics', to: '/docs/postgres-howtos/performance-optimization/monitoring' }, + { from: '/docs/postgres-howtos/performance-optimization/statistics/index', to: '/docs/postgres-howtos/performance-optimization/monitoring' }, + { from: '/docs/postgres-howtos/performance-optimization/statistics/ad-hoc-monitoring', to: '/docs/postgres-howtos/performance-optimization/monitoring/ad-hoc-monitoring' }, + { from: '/docs/postgres-howtos/performance-optimization/statistics/how-to-monitor-transaction-id-wraparound-risks', to: '/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-transaction-id-wraparound-risks' }, + { from: '/docs/postgres-howtos/performance-optimization/statistics/how-to-monitor-xmin-horizon', to: '/docs/postgres-howtos/performance-optimization/monitoring/how-to-monitor-xmin-horizon' }, + { from: '/docs/postgres-howtos/performance-optimization/statistics/how-to-troubleshoot-streaming-replication-lag', to: '/docs/postgres-howtos/advanced-topics/replication/how-to-troubleshoot-streaming-replication-lag' }, + { from: '/docs/postgres-howtos/performance-optimization/statistics/how-to-run-analyze', to: '/docs/postgres-howtos/database-administration/maintenance/how-to-run-analyze' }, + // DBLab how-tos redirects (moved from /docs/how-to-guides to /docs/dblab-howtos) { from: '/docs/how-to-guides', to: '/docs/dblab-howtos' }, @@ -548,7 +558,6 @@ module.exports = { { from: '/docs/guides/platform', to: '/docs/dblab-howtos' }, { from: '/docs/tutorials/onboarding', to: '/docs/dblab-howtos/platform/onboarding' }, { from: '/support', to: '/contact/' }, - { from: '/careers/dba', to: '/careers/dbe' }, { from: '/docs/how-to-guides/administration/machine-setup', to: '/docs/dblab-howtos/administration/install-dle-manually' @@ -621,6 +630,22 @@ module.exports = { description: '', // default to `${siteConfig.title} Blog` copyright: SITE_NAME, language: undefined, // possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes + createFeedItems: async ({ + blogPosts, + siteConfig, + outDir, + defaultCreateFeedItems, + }) => { + const items = await defaultCreateFeedItems({ + blogPosts: blogPosts.slice(0, 20), + siteConfig, + outDir, + }); + return items.map((item) => ({ + ...item, + content: undefined, + })); + }, }, }, theme: { @@ -638,4 +663,3 @@ module.exports = { ], ], } - diff --git a/package.json b/package.json index 4099d355..0b36ef5d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "build": "bun run build:copy-md && bun run build:copy-rules && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", - "serve": "docusaurus serve" + "serve": "docusaurus serve", + "test": "bun test src/components" }, "dependencies": { "@docusaurus/core": "3.9.2", diff --git a/sidebars.js b/sidebars.js index a9c25e2d..58cafa55 100644 --- a/sidebars.js +++ b/sidebars.js @@ -3,6 +3,7 @@ module.exports = { Overview: ['get-started', 'questions-and-answers', 'roadmap'], "PostgresAI how-tos": [ "postgresai-howtos/postgresai-cli", + "postgresai-howtos/joe-cli", "postgresai-howtos/how-to-install-mcp", "postgresai-howtos/how-to-work-with-issues", "postgresai-howtos/install-postgres-ai-monitoring-from-postgresai-console", @@ -20,6 +21,8 @@ module.exports = { "monitoring/getting-started/installation-helm", "monitoring/getting-started/installation-cloud", "monitoring/getting-started/quickstart-supabase", + "monitoring/getting-started/quickstart-rds-privatelink", + "monitoring/getting-started/upgrade", ], }, { @@ -40,6 +43,7 @@ module.exports = { "monitoring/dashboards/single-index", "monitoring/dashboards/slru", "monitoring/dashboards/lock-contention", + "monitoring/dashboards/io-statistics", "monitoring/dashboards/self-monitoring", ], }, @@ -83,6 +87,8 @@ module.exports = { "monitoring/advanced/index", "monitoring/advanced/multi-cluster", "monitoring/advanced/architecture", + "monitoring/advanced/security", + "monitoring/advanced/telemetry", ], }, ], @@ -90,6 +96,7 @@ module.exports = { "database-lab/index", "database-lab/supported-databases", "database-lab/user-interface", + "database-lab/prometheus-monitoring", "database-lab/masking", "database-lab/db-migration-checker", "database-lab/telemetry", @@ -126,6 +133,7 @@ module.exports = { "dblab-howtos/administration/engine-manage", "dblab-howtos/administration/joe-manage", "dblab-howtos/administration/engine-secure", + "dblab-howtos/administration/teleport-integration", "dblab-howtos/administration/logical-full-refresh", "dblab-howtos/administration/ci-observer-postgres-log-masking", "dblab-howtos/administration/add-disk-space-to-zfs-pool", @@ -133,6 +141,8 @@ module.exports = { "Data sources": [ "dblab-howtos/administration/data/index", "dblab-howtos/administration/data/rds", + "dblab-howtos/administration/data/rds-refresh", + "dblab-howtos/administration/data/database-rename", "dblab-howtos/administration/data/dump", "dblab-howtos/administration/data/wal-g", "dblab-howtos/administration/data/pgbackrest", diff --git a/src/components/BlogContactForm/index.tsx b/src/components/BlogContactForm/index.tsx index 43adf87c..f8fce845 100644 --- a/src/components/BlogContactForm/index.tsx +++ b/src/components/BlogContactForm/index.tsx @@ -96,7 +96,7 @@ export const BlogContactForm = () => { required disabled={isSubmitting} className={styles.textarea} - placeholder="Tell us about your company and what you expect" + placeholder="Tell us about your company and what you're looking for" rows={4} /> </div> diff --git a/src/components/BotSample/SignInBanner/index.tsx b/src/components/BotSample/SignInBanner/index.tsx index 09fe7627..de0695b4 100644 --- a/src/components/BotSample/SignInBanner/index.tsx +++ b/src/components/BotSample/SignInBanner/index.tsx @@ -19,7 +19,7 @@ export const SignInBanner = (props: SignInBannerProps) => { return ( <div className={styles.container}> <div className={styles.content}> - <p className={styles.description}>To continue, please Sign In or Register</p> + <p className={styles.description}>To continue, please sign in or register.</p> {saveConversationIdOnSignInClick && threadId ? <button onClick={onSignInClick} className="btn btn1">Sign In</button> : <a href="/signin" className="btn btn1">Sign In</a> diff --git a/src/components/DbLabBanner/index.tsx b/src/components/DbLabBanner/index.tsx index a82c6620..29bdf9ea 100644 --- a/src/components/DbLabBanner/index.tsx +++ b/src/components/DbLabBanner/index.tsx @@ -15,7 +15,7 @@ export const DbLabBanner = () => { height="170px" /> <div className={styles.content}> - <h6 className={styles.title}>DBLab Engine 4.0</h6> + <h6 className={styles.title}>DBLab Engine</h6> <p className={styles.desc}> Instant database branching with O(1) economics. </p> diff --git a/src/components/LaunchWeekPreview/index.tsx b/src/components/LaunchWeekPreview/index.tsx index 2725ed88..52e8c080 100644 --- a/src/components/LaunchWeekPreview/index.tsx +++ b/src/components/LaunchWeekPreview/index.tsx @@ -132,7 +132,7 @@ function LaunchWeekPreview() { <div className={styles.footer}> <Link to="/launch-week" className={styles.viewAllButton}> - View Full Schedule → + View full schedule → </Link> </div> diff --git a/src/components/PostgresCity/README.md b/src/components/PostgresCity/README.md new file mode 100644 index 00000000..6797e812 --- /dev/null +++ b/src/components/PostgresCity/README.md @@ -0,0 +1,60 @@ +# PostgresCity + +An animated, simplified PostgreSQL cluster for the front page. Plain canvas 2D +and TypeScript — no runtime dependency, no WebGL, no network. + +## What it is + +The scene is a projection of a real city plan, not a fresh drawing. Positions, +district ordering and the semantic palette come from +[PGSimCity](https://github.com/NikolayS/PGSimCity), which is the same cluster in +three dimensions. Behind it runs a small deterministic model of PostgreSQL, and +everything that moves is a consequence of that model's state: + +- Statements travel a connection's own duct to its own backend process. The + postmaster forks a backend **per connection**, not per statement. +- A read that misses in the buffer pool goes to storage; the clock sweep picks + the victim frame, and a dirty victim is written out first. +- A write dirties a page in shared memory and produces a WAL record. The commit + waits for that record to reach durable storage — and the data page is still + dirty in memory when the commit returns. +- Dirty pages reach the data directory later: at a checkpoint, through the + background writer, or because another backend needed the frame. +- Replication ships the log. No data page crosses the wire. + +The rates are scaled so a person can watch them; the counts are reduced. Both +are disclosed in the caption under the figure, which is load-bearing content +and must not be dropped at any breakpoint. + +## Files + +| File | Role | +|---|---| +| `plan.ts` | Geography, structures and routes, in PGSimCity's coordinates | +| `sim.ts` | The model. Seeded, fixed-timestep, allocation-free after construction | +| `render.ts` | Canvas painter. All screen-space geometry precomputed once | +| `palette.ts` | The two semantic palettes, light and dark | +| `index.tsx` | React wrapper: sizing, theme, motion, visibility, keyboard | + +## Tests + +```shell +bun test src/components +``` + +The tests assert the claims the drawing makes — that no structure floats over +the excavation, that storage renders below memory, that each backend has its +own connection duct, that `usage_count` caps at 5, that pages stay dirty across +a commit, and that WAL volume climbs after a checkpoint re-arms full-page +writes. They assert durable properties, not this calibration's numbers: if a +change makes the model more correct and a test goes red, the assertion is what +was wrong. + +## Attribution + +Derived from PGSimCity, copyright 2026 Nikolay Samokhvalov, licensed under +Apache-2.0. PGSimCity is an independent, non-commercial educational +visualization and is not affiliated with, sponsored by, or endorsed by the +PostgreSQL project, the PostgreSQL Global Development Group, or the PostgreSQL +Community Association of Canada. PostgreSQL is a trademark of the PostgreSQL +Community Association of Canada. diff --git a/src/components/PostgresCity/index.tsx b/src/components/PostgresCity/index.tsx new file mode 100644 index 00000000..424c530f --- /dev/null +++ b/src/components/PostgresCity/index.tsx @@ -0,0 +1,338 @@ +/** + * PostgresCity — an animated, simplified PostgreSQL cluster for the front page. + * + * Derived from PGSimCity (github.com/NikolayS/PGSimCity, Apache-2.0), which is + * the same city in three dimensions. This one keeps that project's plan, its + * semantic palette and its rule that a drawing is a claim, and drops the + * renderer: it is plain canvas 2D with no runtime dependency, so it costs the + * front page a few kilobytes rather than a WebGL engine. + * + * Behaviour the page depends on: + * - It never animates off-screen, in a hidden tab, or under + * `prefers-reduced-motion`. A reader in that last case still gets the + * composed city, and an explicit control to start it. + * - It follows the site's light and dark themes through `data-theme`, using + * two separately tuned palettes rather than one dimmed set. + * - Every district is reachable by keyboard and readable by a screen reader, + * and the full description exists as text whether or not canvas renders. + */ + +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { DISTRICTS, type DistrictId, type Pt } from './plan' +import { paletteFor } from './palette' +import { anchorAt, buildScene, draw, fitView, type Scene, type View } from './render' +import { createCity, type City } from './sim' +import styles from './styles.module.css' + +const PGSIMCITY_URL = 'https://nikolays.github.io/PGSimCity/' + +/** Wall-clock seconds a single animation step advances the model. */ +const STEP = 1 / 60 +/** Never let a backgrounded tab's catch-up run the model for minutes. */ +const MAX_CATCHUP = 0.25 + +type ThemeMode = 'light' | 'dark' + +function readTheme(): ThemeMode { + if (typeof document === 'undefined') return 'dark' + return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light' +} + +interface HitTarget { + id: DistrictId + x: number + y: number +} + +export interface PostgresCityProps { + /** Rendered above the scene. Omit for a bare figure. */ + title?: string + className?: string +} + +export default function PostgresCity({ title, className }: PostgresCityProps): JSX.Element { + const wrapRef = useRef<HTMLDivElement | null>(null) + const canvasRef = useRef<HTMLCanvasElement | null>(null) + const sceneRef = useRef<Scene | null>(null) + const cityRef = useRef<City | null>(null) + const viewRef = useRef<View | null>(null) + const activeRef = useRef<DistrictId | null>(null) + const rafRef = useRef<number>(0) + const accRef = useRef<number>(0) + const lastRef = useRef<number>(0) + + const [theme, setTheme] = useState<ThemeMode>('dark') + const [active, setActive] = useState<DistrictId | null>(null) + const [reduced, setReduced] = useState(false) + const [playing, setPlaying] = useState(true) + const [targets, setTargets] = useState<HitTarget[]>([]) + const [onScreen, setOnScreen] = useState(true) + const [tabActive, setTabActive] = useState(true) + const visible = onScreen && tabActive + + activeRef.current = active + + /* ---- one-time construction ------------------------------------------ */ + if (sceneRef.current === null) sceneRef.current = buildScene() + if (cityRef.current === null) cityRef.current = createCity() + + /* ---- theme ------------------------------------------------------------ */ + useEffect(() => { + setTheme(readTheme()) + const obs = new MutationObserver(() => setTheme(readTheme())) + obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }) + return () => obs.disconnect() + }, []) + + /* ---- reduced motion --------------------------------------------------- */ + useEffect(() => { + const mq = window.matchMedia('(prefers-reduced-motion: reduce)') + const apply = (): void => { + setReduced(mq.matches) + setPlaying(!mq.matches) + } + apply() + mq.addEventListener('change', apply) + return () => mq.removeEventListener('change', apply) + }, []) + + /* ---- size ------------------------------------------------------------- */ + const resize = useCallback((): void => { + const wrap = wrapRef.current + const canvas = canvasRef.current + const scene = sceneRef.current + if (!wrap || !canvas || !scene) return + const rect = wrap.getBoundingClientRect() + if (rect.width < 1 || rect.height < 1) return + const dpr = Math.min(2, window.devicePixelRatio || 1) + canvas.width = Math.round(rect.width * dpr) + canvas.height = Math.round(rect.height * dpr) + canvas.style.width = `${rect.width}px` + canvas.style.height = `${rect.height}px` + const view = fitView(scene, rect.width, rect.height, dpr) + viewRef.current = view + + const p: Pt = { x: 0, y: 0 } + setTargets( + DISTRICTS.map((d, i) => { + anchorAt(scene, view, i, p) + return { id: d.id, x: p.x / dpr, y: p.y / dpr } + }), + ) + }, []) + + useEffect(() => { + resize() + const wrap = wrapRef.current + if (!wrap) return + const ro = new ResizeObserver(resize) + ro.observe(wrap) + return () => ro.disconnect() + }, [resize]) + + /* ---- pause when nobody is looking ------------------------------------- + * + * Two independent conditions, tracked separately: whether the figure is on + * screen and whether the tab is in front. Folding them into one flag lets a + * tab regaining focus restart an animation that is scrolled far out of + * view — which is exactly the case a front page cannot afford. */ + useEffect(() => { + const wrap = wrapRef.current + if (!wrap) return + const io = new IntersectionObserver((entries) => setOnScreen(entries[0]?.isIntersecting ?? true), { + threshold: 0.01, + }) + io.observe(wrap) + const onVis = (): void => setTabActive(!document.hidden) + onVis() + document.addEventListener('visibilitychange', onVis) + return () => { + io.disconnect() + document.removeEventListener('visibilitychange', onVis) + } + }, []) + + /* ---- the loop --------------------------------------------------------- */ + const paint = useCallback((): void => { + const canvas = canvasRef.current + const scene = sceneRef.current + const city = cityRef.current + const view = viewRef.current + if (!canvas || !scene || !city || !view) return + const ctx = canvas.getContext('2d') + if (!ctx) return + draw(ctx, { + scene, + view, + pal: paletteFor(theme), + city, + active: activeRef.current, + }) + }, [theme]) + + useEffect(() => { + if (!(playing && visible)) { + /* Compose one frame anyway, so a paused or reduced-motion reader is + * looking at the city rather than at nothing. */ + paint() + return + } + + lastRef.current = performance.now() + const frame = (now: number): void => { + const city = cityRef.current + if (!city) return + const dt = Math.min(MAX_CATCHUP, (now - lastRef.current) / 1000) + lastRef.current = now + accRef.current += dt + let guard = 0 + while (accRef.current >= STEP && guard < 16) { + city.step(STEP) + accRef.current -= STEP + guard++ + } + paint() + rafRef.current = requestAnimationFrame(frame) + } + rafRef.current = requestAnimationFrame(frame) + return () => cancelAnimationFrame(rafRef.current) + }, [playing, visible, paint]) + + /* Repaint on theme change or hover even while paused. */ + useEffect(() => { + if (!playing || !visible) paint() + }, [theme, active, targets, playing, visible, paint]) + + const describedBy = active ? `pgcity-blurb-${active}` : undefined + const current = DISTRICTS.find((d) => d.id === active) ?? null + + return ( + <figure className={[styles.figure, className].filter(Boolean).join(' ')}> + {title ? <figcaption className={styles.title}>{title}</figcaption> : null} + + <div className={styles.stage} ref={wrapRef}> + <canvas + ref={canvasRef} + className={styles.canvas} + role="img" + aria-label="A simplified PostgreSQL cluster drawn as a city: clients and the postmaster to the north, a row of backend processes, the shared buffer pool at the centre over the data directory, the write-ahead log to the east, maintenance processes to the west, and a streaming standby to the south." + aria-describedby={describedBy} + /> + + {/* Real focusable controls, positioned over the scene. Hover and + keyboard focus drive the same highlight, so the mouse and the Tab + key see identical behaviour. */} + <div className={styles.hits}> + {targets.map((t) => { + const d = DISTRICTS.find((x) => x.id === t.id) + if (!d) return null + return ( + <button + key={t.id} + type="button" + className={[styles.hit, active === t.id ? styles.hitOn : ''].join(' ')} + style={{ left: `${t.x}px`, top: `${t.y}px` }} + onMouseEnter={() => setActive(t.id)} + onMouseLeave={() => setActive((cur) => (cur === t.id ? null : cur))} + onFocus={() => setActive(t.id)} + onBlur={() => setActive((cur) => (cur === t.id ? null : cur))} + onClick={() => setActive((cur) => (cur === t.id ? null : t.id))} + aria-pressed={active === t.id} + > + <span className={styles.srOnly}> + {d.label} — {d.sub}. {d.blurb} + </span> + </button> + ) + })} + </div> + + <button + type="button" + className={styles.motionToggle} + onClick={() => setPlaying((p) => !p)} + aria-label={playing ? 'Pause the animation' : 'Start the animation'} + > + {playing ? '❙❙ pause' : '▶ play'} + </button> + + <div className={styles.blurbSlot} aria-live="polite"> + {current ? ( + <p className={styles.blurb} id={`pgcity-blurb-${current.id}`}> + <strong>{current.label}</strong> + <span className={styles.blurbSub}> {current.sub}</span> + <br /> + {current.blurb} + </p> + ) : ( + <p className={styles.blurbHint}> + {reduced && !playing + ? 'Motion is off, matching your system setting. Press play to run the cluster.' + : 'Hover or tab through a district to see what it does.'} + </p> + )} + </div> + </div> + + <div className={styles.legend}> + <span className={styles.key} data-c="wal"> + write-ahead log + </span> + <span className={styles.key} data-c="dirty"> + dirty page + </span> + <span className={styles.key} data-c="clean"> + clean page + </span> + <span className={styles.key} data-c="checkpoint"> + checkpoint + </span> + <span className={styles.key} data-c="bgwriter"> + background writer + </span> + <span className={styles.key} data-c="vacuum"> + autovacuum + </span> + <span className={styles.key} data-c="replication"> + replication + </span> + <span className={styles.key} data-c="storage"> + storage + </span> + </div> + + {/* Load-bearing, not chrome: this qualification stays at every width. + If the viewport cannot hold both the picture and this sentence, the + picture is what goes. */} + <p className={styles.disclosure}> + A model, not a monitor. This runs a scaled simulation of PostgreSQL in your browser — it is + not live data from any database. Twelve backends stand in for a full connection set, 48 + frames for the whole buffer pool, and the rates are slowed so the mechanisms are watchable.{' '} + <a href={PGSIMCITY_URL} target="_blank" rel="noopener noreferrer"> + Explore the full 3D city + </a> + . + </p> + + {/* The city as text. Present for screen readers and for anyone whose + browser never draws the canvas at all. */} + <details className={styles.textAlt}> + <summary>Read this as text</summary> + <ul> + {DISTRICTS.map((d) => ( + <li key={d.id}> + <strong>{d.label}</strong> ({d.sub}) — {d.blurb} + </li> + ))} + </ul> + <p> + A statement travels client → backend → buffer pool. A change is made to a page in shared + memory and its WAL record is written; the transaction commits only once that record is + flushed to durable storage. The changed page itself is still in memory at that point, and + reaches the data directory later — at a checkpoint, through the background writer, or + because another backend needed its frame. The standby receives the log, not the pages. + </p> + </details> + </figure> + ) +} diff --git a/src/components/PostgresCity/palette.test.ts b/src/components/PostgresCity/palette.test.ts new file mode 100644 index 00000000..f21ebf0b --- /dev/null +++ b/src/components/PostgresCity/palette.test.ts @@ -0,0 +1,182 @@ +/** + * Colour is semantic here: a hue names a PostgreSQL mechanism. These check + * that the two palettes stay a translation of each other rather than drifting + * into two different vocabularies, and that neither one hides a meaning it is + * supposed to carry. + * + * Run with `bun test`. + */ + +import { describe, expect, it } from 'bun:test' +import { DAY, NIGHT, paletteFor, type Palette } from './palette' + +/** CIE L*a*b*, D65. Perceptual distance is the right instrument here: these + * palettes separate meanings by hue at deliberately matched lightness, so a + * luminance-only floor would condemn a set that reads perfectly well. */ +function lab(hex: string): [number, number, number] { + const n = parseInt(hex.slice(1), 16) + const f = (c: number): number => { + const s = c / 255 + return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4) + } + const r = f((n >> 16) & 255) + const g = f((n >> 8) & 255) + const b = f(n & 255) + const X = (0.4124 * r + 0.3576 * g + 0.1805 * b) / 0.95047 + const Y = 0.2126 * r + 0.7152 * g + 0.0722 * b + const Z = (0.0193 * r + 0.1192 * g + 0.9505 * b) / 1.08883 + const k = (t: number): number => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116) + const [fx, fy, fz] = [k(X), k(Y), k(Z)] + return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)] +} + +function deltaE(a: string, b: string): number { + const A = lab(a) + const B = lab(b) + return Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]) +} + +/** Relative luminance, WCAG's definition. */ +function luminance(hex: string): number { + const m = /^#([0-9a-f]{6})$/i.exec(hex.trim()) + if (!m) return NaN + const n = parseInt(m[1], 16) + const lin = (c: number): number => { + const s = c / 255 + return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4) + } + return 0.2126 * lin((n >> 16) & 255) + 0.7152 * lin((n >> 8) & 255) + 0.0722 * lin(n & 255) +} + +function hue(hex: string): number { + const n = parseInt(hex.slice(1), 16) + const r = ((n >> 16) & 255) / 255 + const g = ((n >> 8) & 255) / 255 + const b = (n & 255) / 255 + const max = Math.max(r, g, b) + const min = Math.min(r, g, b) + const d = max - min + if (d === 0) return 0 + let h: number + if (max === r) h = ((g - b) / d) % 6 + else if (max === g) h = (b - r) / d + 2 + else h = (r - g) / d + 4 + return (h * 60 + 360) % 360 +} + +/** Shortest distance around the colour wheel, in degrees. */ +function hueGap(a: string, b: string): number { + const d = Math.abs(hue(a) - hue(b)) % 360 + return d > 180 ? 360 - d : d +} + +function contrast(a: string, b: string): number { + const la = luminance(a) + const lb = luminance(b) + const hi = Math.max(la, lb) + const lo = Math.min(la, lb) + return (hi + 0.05) / (lo + 0.05) +} + +/** The mechanisms whose colour is doing the teaching. */ +const MEANINGS: (keyof Palette)[] = [ + 'client', + 'postmaster', + 'backend', + 'shmem', + 'bufClean', + 'bufDirty', + 'bufPinned', + 'wal', + 'archive', + 'storage', + 'index', + 'checkpoint', + 'bgwriter', + 'vacuum', + 'replication', + 'ok', +] + +describe('the two palettes', () => { + it('name exactly the same things', () => { + expect(Object.keys(NIGHT).sort()).toEqual(Object.keys(DAY).sort()) + }) + + it('are chosen for their own background, not one dimmed for both', () => { + /* Reusing night values on a light page is the failure both sets exist to + * avoid; if any meaning were identical in both, that is what happened. */ + const shared = MEANINGS.filter((k) => NIGHT[k] === DAY[k]) + expect(shared).toEqual([]) + }) + + it('resolve by theme', () => { + expect(paletteFor('dark')).toBe(NIGHT) + expect(paletteFor('light')).toBe(DAY) + }) +}) + +describe('legibility', () => { + /* + * Floors, not aspirations. Each is set just under what the palettes + * inherited from PGSimCity actually measure, so the test cannot be + * satisfied by a set that has quietly got worse: + * + * night — closest meanings index/bgwriter, ΔE76 9.6 + * day — closest meanings client/backend, ΔE76 12.2 + * both — every meaning is ≥ 36 from the ground it stands on + */ + const MEANING_FLOOR = 9 + const SURFACE_FLOOR = 25 + + for (const [name, pal] of [ + ['night', NIGHT], + ['day', DAY], + ] as [string, Palette][]) { + it(`keeps every meaning distinct from every other in ${name}`, () => { + const tight: string[] = [] + for (let i = 0; i < MEANINGS.length; i++) { + for (let j = i + 1; j < MEANINGS.length; j++) { + const d = deltaE(pal[MEANINGS[i]], pal[MEANINGS[j]]) + if (d < MEANING_FLOOR) tight.push(`${MEANINGS[i]}/${MEANINGS[j]}:${d.toFixed(1)}`) + } + } + expect(tight).toEqual([]) + }) + + it(`keeps every meaning off the surfaces it is painted on in ${name}`, () => { + /* A mechanism that sinks into the ground or into the matte structure it + * stands on has stopped carrying its meaning, whatever its hue is. */ + const lost: string[] = [] + for (const k of MEANINGS) { + for (const surface of ['ground', 'matTop', 'skyTop'] as (keyof Palette)[]) { + const d = deltaE(pal[k], pal[surface]) + if (d < SURFACE_FLOOR) lost.push(`${k} on ${surface}:${d.toFixed(1)}`) + } + } + expect(lost).toEqual([]) + }) + } + + it('keeps label ink readable in both themes', () => { + /* The plates are translucent over an unknown scene, so measure against + * the opaque extremes they sit between. */ + expect(contrast(NIGHT.ink, NIGHT.skyTop)).toBeGreaterThan(7) + expect(contrast(DAY.ink, '#ffffff')).toBeGreaterThan(7) + }) + + it('separates clean pages from dirty ones by hue, in both themes', () => { + /* Clean and dirty are the pool's whole vocabulary, and they are separated + * on the colour wheel rather than by lightness: both palettes put them + * about 140° apart at closely matched luminance. That is deliberate — the + * pool must not read as a brightness gradient — but it means colour alone + * carries no information on a monochrome display or to a reader with a + * colour-vision deficiency, which is why `drawPool` also raises dirty + * frames above the deck. If that redundant channel is ever removed, this + * test is the record of why it was there. */ + for (const pal of [NIGHT, DAY]) { + expect(hueGap(pal.bufClean, pal.bufDirty)).toBeGreaterThan(90) + expect(contrast(pal.bufClean, pal.bufDirty)).toBeLessThan(1.6) + } + }) +}) diff --git a/src/components/PostgresCity/palette.ts b/src/components/PostgresCity/palette.ts new file mode 100644 index 00000000..b4079783 --- /dev/null +++ b/src/components/PostgresCity/palette.ts @@ -0,0 +1,164 @@ +/** + * Semantic palette for the Postgres city. + * + * Ported from PGSimCity (github.com/NikolayS/PGSimCity, `src/core/themes.ts`), + * which tunes two independent palettes rather than dimming one: a dark set for + * a night scene where meaning glows against matte structure, and a light set + * where saturation and value carry meaning under daylight. Reusing the dark + * values on a light page is the failure mode both sets exist to avoid. + * + * A hue names a PostgreSQL mechanism. It is never chosen because it looks + * good next to its neighbour, and the same hue means the same mechanism + * everywhere in the scene. + */ + +export interface Palette { + /** Scene backdrop, top and bottom of the sky gradient. */ + skyTop: string + skyBottom: string + /** The ground plane the districts stand on, and its survey grid. */ + ground: string + groundEdge: string + grid: string + /** The cut faces of the excavation, and the air below it. */ + pitWall: string + underground: string + + /* Processes and memory. */ + client: string + postmaster: string + backend: string + shmem: string + + /* Buffer-pool frame states. */ + bufClean: string + bufDirty: string + bufPinned: string + bufFree: string + + /* Durability. */ + wal: string + walDim: string + archive: string + storage: string + index: string + + /* Maintenance. */ + checkpoint: string + bgwriter: string + vacuum: string + replication: string + + /* Status and type. */ + ok: string + ink: string + inkDim: string + + /** Structure faces get their own ramp so meaning stays the only bright thing. */ + matTop: string + matLeft: string + matRight: string + /** Backing plate behind a label, so type never fights the scene under it. */ + plate: string + /** The flare on a frame a backend just touched. Brighter in both themes — + * using the ink colour darkens it in daylight, which reads as a different + * page state rather than as a page being used. */ + flash: string +} + +/** + * Night. Structure is matte, meaning is the only thing that emits. Values are + * PGSimCity's NIGHT_PALETTE unchanged, so a reader who follows the link from + * here into the full city finds the same colours meaning the same things. + */ +export const NIGHT: Palette = { + skyTop: '#04060c', + skyBottom: '#0a1120', + ground: '#15243c', + groundEdge: '#3d68a0', + grid: '#20334f', + pitWall: '#0b1526', + underground: '#050810', + + client: '#8ecae6', + postmaster: '#9db4ff', + backend: '#5ad1ff', + shmem: '#7b6cff', + + bufClean: '#3fa7ff', + bufDirty: '#ff4d6d', + bufPinned: '#ffd166', + bufFree: '#1b2740', + + wal: '#ffb03a', + walDim: '#7a5312', + archive: '#c9a227', + storage: '#55d6a0', + index: '#64ffda', + + checkpoint: '#ff7ac6', + bgwriter: '#4fe3c1', + vacuum: '#b57bff', + replication: '#ff9c1c', + + ok: '#57e389', + ink: '#e8f1ff', + inkDim: '#8fa5c4', + + matTop: '#243449', + matLeft: '#151e2d', + matRight: '#1b2738', + plate: 'rgba(4, 8, 16, 0.72)', + flash: '#ffffff', +} + +/** + * Day. The same call sites, a different rendering model: no glow at all, so + * hue and lightness have to do the whole job. Values are PGSimCity's + * DAY_PALETTE, whose closest pair measures ΔE2000 7.0 — daylight separates + * these meanings more strictly than night does. + */ +export const DAY: Palette = { + skyTop: '#cfe0ee', + skyBottom: '#eef2f5', + ground: '#9c9583', + groundEdge: '#5f5a4d', + grid: '#8b8573', + pitWall: '#6f6a5b', + underground: '#5f5b4e', + + client: '#5f96c4', + postmaster: '#6a63d9', + backend: '#0089b5', + shmem: '#4b2fd0', + + bufClean: '#1d5fcb', + bufDirty: '#e02b46', + bufPinned: '#efbc16', + bufFree: '#acaeb2', + + wal: '#b8720a', + walDim: '#8c7444', + archive: '#7d6018', + storage: '#17954f', + index: '#05a47e', + + checkpoint: '#c42d92', + bgwriter: '#0e8f8c', + vacuum: '#8b2bc0', + replication: '#e2690d', + + ok: '#3f9c22', + ink: '#18222e', + inkDim: '#5d6b7a', + + matTop: '#e8e5dc', + matLeft: '#aca697', + matRight: '#c9c4b7', + plate: 'rgba(255, 255, 255, 0.82)', + flash: '#ffffff', +} + +export function paletteFor(mode: 'light' | 'dark'): Palette { + return mode === 'dark' ? NIGHT : DAY +} diff --git a/src/components/PostgresCity/plan.test.ts b/src/components/PostgresCity/plan.test.ts new file mode 100644 index 00000000..4a6d7335 --- /dev/null +++ b/src/components/PostgresCity/plan.test.ts @@ -0,0 +1,250 @@ +/** + * The plan makes factual claims about a PostgreSQL cluster, and a drawing can + * teach a falsehood more persuasively than the caption next to it can teach + * the truth. These assert the claims that the geometry itself is making. + * + * Run with `bun test`. + */ + +import { describe, expect, it } from 'bun:test' +import { + DECK, + DISTRICTS, + N_BACKENDS, + N_FRAMES, + PIT, + ROUTES, + STORAGE_Y, + backendX, + buildBoxes, + buildUnderBoxes, + conduitX, + depth, + forkRoute, + frameCentre, + project, + queryRoute, + resultRoute, + routeOf, + type Box, +} from './plan' + +function overlaps(a0: number, a1: number, b0: number, b1: number): boolean { + return a0 < b1 && b0 < a1 +} + +function footprint(b: Box): [number, number, number, number] { + return [b.x - b.w / 2, b.x + b.w / 2, b.z - b.d / 2, b.z + b.d / 2] +} + +describe('the excavation', () => { + it('has no unsupported structure standing over the hole', () => { + /* A building at grade inside the pit has nothing under it. The two legal + * exceptions are the plaza itself, which is meant to hang over the cut, + * and anything standing on the plaza — wal_buffers is shared memory and + * belongs up there with the pool. + * + * This has broken twice: once when the excavation grew wide enough to + * swallow the entire backend row, and once when wal_buffers sat just off + * the deck's east edge with the pit underneath it. */ + const onDeck = (b: Box): boolean => { + const [x0, x1, z0, z1] = footprint(b) + return ( + x0 >= -DECK.w / 2 && x1 <= DECK.w / 2 && z0 >= -DECK.d / 2 && z1 <= DECK.d / 2 + ) + } + const offenders = buildBoxes() + .filter((b) => b.y < 6 && b.district !== 'pool' && !onDeck(b)) + .filter((b) => { + const [x0, x1, z0, z1] = footprint(b) + return overlaps(x0, x1, PIT.x0, PIT.x1) && overlaps(z0, z1, PIT.z0, PIT.z1) + }) + .map((b) => `${b.district}@(${b.x},${b.z})`) + expect(offenders).toEqual([]) + }) + + it('puts every part of the data directory below ground', () => { + for (const b of buildUnderBoxes()) { + expect(b.y + b.h).toBeLessThanOrEqual(0) + } + }) + + it('keeps the data directory inside the hole, where it can be seen', () => { + for (const b of buildUnderBoxes()) { + const [x0, x1, z0, z1] = footprint(b) + expect(x0).toBeGreaterThanOrEqual(PIT.x0) + expect(x1).toBeLessThanOrEqual(PIT.x1) + expect(z0).toBeGreaterThanOrEqual(PIT.z0) + expect(z1).toBeLessThanOrEqual(PIT.z1) + } + }) + + it('floats the buffer pool over the excavation rather than beside it', () => { + /* The plaza hanging over the cut is the whole reason a reader can see + * that memory is above and storage is below. */ + expect(overlaps(-DECK.w / 2, DECK.w / 2, PIT.x0, PIT.x1)).toBe(true) + expect(overlaps(-DECK.d / 2, DECK.d / 2, PIT.z0, PIT.z1)).toBe(true) + }) + + it('draws storage lower on screen than the memory above it', () => { + const deck = { x: 0, y: DECK.h, z: 0 } + const heap = buildUnderBoxes().find((b) => b.accent === 'storage' && b.h > 6) + expect(heap).toBeDefined() + const a = project(deck.x, deck.y, deck.z, { x: 0, y: 0 }) + const b = project(heap!.x, heap!.y + heap!.h, heap!.z, { x: 0, y: 0 }) + expect(b.y).toBeGreaterThan(a.y) + }) +}) + +describe('the plan', () => { + it('keeps PostgreSQL’s compass: WAL east, maintenance west, standby south', () => { + const by = (id: string) => DISTRICTS.find((d) => d.id === id)! + expect(by('wal').x).toBeGreaterThan(0) + expect(by('maintenance').x).toBeLessThan(0) + expect(by('standby').z).toBeGreaterThan(0) + expect(by('clients').z).toBeLessThan(0) + /* The archive is downstream of pg_wal, further from the server. */ + expect(by('archive').x).toBeGreaterThan(by('wal').x) + /* Storage is the only district below grade. */ + expect(by('storage').y).toBeLessThan(0) + }) + + it('puts the client further from the pool than the backends are', () => { + const by = (id: string) => DISTRICTS.find((d) => d.id === id)! + expect(Math.abs(by('clients').z)).toBeGreaterThan(Math.abs(by('backends').z)) + expect(Math.abs(by('backends').z)).toBeGreaterThan(Math.abs(by('pool').z)) + }) + + it('gives every district a structure to name', () => { + const drawn = new Set(buildBoxes().concat(buildUnderBoxes()).map((b) => b.district)) + for (const d of DISTRICTS) { + expect(drawn.has(d.id)).toBe(true) + } + }) + + it('names the excavation depth consistently', () => { + expect(STORAGE_Y).toBeLessThan(0) + for (const b of buildUnderBoxes()) expect(b.y).toBeLessThanOrEqual(STORAGE_Y) + }) +}) + +describe('connections', () => { + it('gives each backend its own duct, never a shared lane', () => { + /* One process per connection is the single most misunderstood thing about + * PostgreSQL. If two backends shared a route, the drawing would be making + * the wrong claim. */ + const q = new Set<number>() + const r = new Set<number>() + const f = new Set<number>() + for (let i = 0; i < N_BACKENDS; i++) { + q.add(queryRoute(i)) + r.add(resultRoute(i)) + f.add(forkRoute(i)) + } + expect(q.size).toBe(N_BACKENDS) + expect(r.size).toBe(N_BACKENDS) + expect(f.size).toBe(N_BACKENDS) + }) + + it('keeps the ducts out of the postmaster’s avenue', () => { + /* The postmaster forks a backend and then leaves the data path. No + * connection may be drawn touching it. */ + for (let i = 0; i < N_BACKENDS; i++) { + const pts = ROUTES[queryRoute(i)].pts + for (const [x, , z] of pts) { + if (z < -200 && z > -260) expect(Math.abs(x)).toBeGreaterThanOrEqual(24) + } + } + }) + + it('lands each duct on the backend it belongs to', () => { + for (let i = 0; i < N_BACKENDS; i++) { + const pts = ROUTES[queryRoute(i)].pts + expect(pts[0][0]).toBeCloseTo(conduitX(i), 5) + expect(pts[pts.length - 1][0]).toBeCloseTo(backendX(i), 5) + } + }) +}) + +describe('routes', () => { + it('are all well formed and have length', () => { + for (const r of ROUTES) { + expect(r.pts.length).toBeGreaterThanOrEqual(2) + let len = 0 + for (let i = 1; i < r.pts.length; i++) { + const a = r.pts[i - 1] + const b = r.pts[i] + len += Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]) + } + expect(len).toBeGreaterThan(0) + expect(r.speed).toBeGreaterThan(0) + } + }) + + it('sends the write-ahead log to durable storage, not the pool', () => { + /* wal.fsync must descend. A flat or rising route would draw a commit that + * never left memory. */ + const pts = ROUTES[routeOf('walFsync')].pts + expect(pts[pts.length - 1][1]).toBeLessThan(pts[0][1]) + expect(pts[pts.length - 1][1]).toBeLessThanOrEqual(STORAGE_Y + 20) + }) + + it('reads pages up out of storage and writes them back down', () => { + const read = ROUTES[routeOf('pageRead')].pts + expect(read[0][1]).toBeLessThan(read[read.length - 1][1]) + const write = ROUTES[routeOf('pageWrite')].pts + expect(write[0][1]).toBeGreaterThan(write[write.length - 1][1]) + }) + + it('routes WAL through wal_buffers before pg_wal', () => { + /* A record is staged in shared memory first; it is not written straight + * to the segment by the backend that produced it. */ + const ins = ROUTES[routeOf('walIns')].pts + const flush = ROUTES[routeOf('walFlush')].pts + const insEnd = ins[ins.length - 1] + expect(Math.abs(insEnd[0] - flush[0][0])).toBeLessThan(12) + expect(Math.abs(insEnd[2] - flush[0][2])).toBeLessThan(12) + /* And it ends up east of where it started. */ + expect(flush[flush.length - 1][0]).toBeGreaterThan(ins[0][0]) + }) + + it('streams to the standby from pg_wal, never from the buffer pool', () => { + const stream = ROUTES[routeOf('stream')].pts + expect(stream[0][0]).toBeGreaterThan(140) + expect(stream[stream.length - 1][2]).toBeGreaterThan(120) + }) +}) + +describe('projection', () => { + it('is a consistent depth ordering: south and west are nearer', () => { + expect(depth(0, 100)).toBeGreaterThan(depth(0, -100)) + expect(depth(-100, 0)).toBeGreaterThan(depth(100, 0)) + }) + + it('lifts height up the screen', () => { + const low = project(0, 0, 0, { x: 0, y: 0 }) + const high = project(0, 40, 0, { x: 0, y: 0 }) + expect(high.y).toBeLessThan(low.y) + }) +}) + +describe('the buffer pool grid', () => { + it('lays every frame on the deck', () => { + const c = { x: 0, z: 0 } + for (let i = 0; i < N_FRAMES; i++) { + frameCentre(i, c) + expect(Math.abs(c.x)).toBeLessThan(DECK.w / 2) + expect(Math.abs(c.z)).toBeLessThan(DECK.d / 2) + } + }) + + it('gives every frame a distinct position', () => { + const seen = new Set<string>() + const c = { x: 0, z: 0 } + for (let i = 0; i < N_FRAMES; i++) { + frameCentre(i, c) + seen.add(`${c.x.toFixed(3)},${c.z.toFixed(3)}`) + } + expect(seen.size).toBe(N_FRAMES) + }) +}) diff --git a/src/components/PostgresCity/plan.ts b/src/components/PostgresCity/plan.ts new file mode 100644 index 00000000..94c4bf27 --- /dev/null +++ b/src/components/PostgresCity/plan.ts @@ -0,0 +1,674 @@ +/** + * The city plan. + * + * Coordinates are PGSimCity's, unchanged: Y is up, north is -Z, east is +X, + * one unit is about one metre. Every position below is read from that + * project's `src/world/layout.ts` ANCHOR table, so this scene is a true + * projection of the same plan rather than a redrawing of it — the buffer pool + * really is at the centre, `pg_wal` really is east of it, the standby really + * is south, and storage really is underneath. + * + * What is simplified is population, not geography: 12 backends stand in for + * however many connections a cluster has, 48 frames for the whole buffer pool, + * 5 tables for the catalog, 7 silos for `pg_wal`. Those counts are disclosed + * in the caption, because a reader who counts them would otherwise learn a + * wrong number. + * + * Distance is compressed. The outermost districts — the archive estate and + * the standby's site — sit closer to the centre than they do in PGSimCity, so + * the whole cluster fits one frame. The excavation is also wider and the + * storage layer shallower than that project's: from a camera that cannot move, + * a deeper pit puts the data directory behind its own near wall and the plaza + * hides what is left. Direction and ordering are exact; absolute separation is + * not, and no distance should be read off this scene. + */ + +/* -------------------------------------------------------------------------- + * Projection: plan rotated 30° about Y, then flattened by the camera pitch. + * The rotation is what puts the data path on the reading diagonal — clients + * top-left, the pool at the centre, the standby bottom-right. + * ------------------------------------------------------------------------*/ + +const YAW = Math.PI / 6 +const COS_YAW = Math.cos(YAW) +const SIN_YAW = Math.sin(YAW) +/** Camera pitch. 1 would be a plan view; 0 would be an elevation. */ +const TILT = 0.52 +/** How much a metre of height moves a point up the screen. */ +const LIFT = 0.86 + +export interface Pt { + x: number + y: number +} + +/** Project a world point into unscaled scene space. Writes into `out`. */ +export function project(x: number, y: number, z: number, out: Pt): Pt { + const rx = x * COS_YAW + z * SIN_YAW + const rz = -x * SIN_YAW + z * COS_YAW + out.x = rx + out.y = rz * TILT - y * LIFT + return out +} + +/** Painter's-algorithm depth. Larger draws later, i.e. nearer the camera. */ +export function depth(x: number, z: number): number { + return -x * SIN_YAW + z * COS_YAW +} + +/* -------------------------------------------------------------------------- + * Districts. + * + * `blurb` is what a reader gets on hover, focus, and in the text alternative. + * Each sentence has to be true of PostgreSQL and true of what this scene + * actually draws; a caption cannot correct a misleading building. + * ------------------------------------------------------------------------*/ + +export type DistrictId = + | 'clients' + | 'postmaster' + | 'backends' + | 'pool' + | 'wal' + | 'archive' + | 'maintenance' + | 'storage' + | 'standby' + +export interface District { + id: DistrictId + label: string + sub: string + /** Anchor the label and the hit target hang from, in world space. */ + x: number + y: number + z: number + /** Hit-target radius in scene units. */ + r: number + /** Where the label sits relative to the anchor, in scene units. */ + lx: number + ly: number + accent: string + blurb: string +} + +export const DISTRICTS: readonly District[] = [ + { + id: 'clients', + lx: -10, + ly: -30, + label: 'CLIENTS', + sub: 'your application', + x: 0, + y: 14, + z: -300, + r: 62, + accent: 'client', + blurb: + 'Connections arrive from outside the server. Each one is a socket held for the whole session, not a request that comes and goes.', + }, + { + id: 'postmaster', + lx: 68, + ly: -22, + label: 'POSTMASTER', + sub: 'the supervisor', + x: 0, + y: 36, + z: -215, + r: 42, + accent: 'postmaster', + blurb: + 'The supervisor process forks one backend per accepted connection, then steps out of the data path. It never reads or writes your tables itself.', + }, + { + id: 'backends', + lx: -122, + ly: -26, + label: 'BACKENDS', + sub: 'one process per connection', + x: 0, + y: 26, + z: -130, + r: 120, + accent: 'backend', + blurb: + 'One operating-system process serves one connection. It parses, plans and executes your statement, and it is the process that writes WAL for its own transaction.', + }, + { + id: 'pool', + lx: -150, + ly: 19, + label: 'BUFFER POOL', + sub: 'shared_buffers', + x: 0, + y: 10, + z: 0, + r: 96, + accent: 'bufClean', + blurb: + 'Shared memory holding fixed-size page frames. Every read and every write goes through a frame here; a page changed in memory is dirty until something writes it back to storage.', + }, + { + id: 'wal', + lx: 18, + ly: -46, + label: 'WAL', + sub: 'pg_wal', + x: 168, + y: 20, + z: 0, + r: 64, + accent: 'wal', + blurb: + 'The write-ahead log. A change is durable once its WAL record is flushed here — before the changed data page has gone anywhere. That is the rule the whole design rests on.', + }, + { + id: 'archive', + lx: 32, + ly: -38, + label: 'ARCHIVE', + sub: 'completed segments', + x: 232, + y: 14, + z: -66, + r: 48, + accent: 'archive', + blurb: + 'Finished WAL segments are copied off the machine. Together with a base backup they are what makes point-in-time recovery possible.', + }, + { + id: 'maintenance', + lx: -54, + ly: 32, + label: 'MAINTENANCE', + sub: 'checkpointer · bgwriter · autovacuum', + x: -170, + y: 18, + z: 0, + r: 84, + accent: 'checkpoint', + blurb: + 'The checkpointer writes every dirty page at a checkpoint; the background writer trickles some out ahead of it; autovacuum reclaims row versions no transaction can still see.', + }, + { + id: 'storage', + lx: -64, + ly: 42, + label: 'STORAGE', + sub: 'the data directory', + x: -46, + y: -34, + z: 84, + r: 110, + accent: 'storage', + blurb: + 'Heap files and indexes on disk. Memory ends and durable storage begins here, which is why the ground is cut away above it.', + }, + { + id: 'standby', + lx: 24, + ly: -28, + label: 'STANDBY', + sub: 'streaming replica', + x: 120, + y: 16, + z: 258, + r: 90, + accent: 'replication', + blurb: + 'A second server replaying the primary’s WAL as it arrives. Replication ships the log, never the buffer pool, so the replica rebuilds the same pages from the same records.', + }, +] + +/* -------------------------------------------------------------------------- + * Structures. A box is (centre x, z), footprint (w, d), base y and height. + * ------------------------------------------------------------------------*/ + +export interface Box { + x: number + z: number + w: number + d: number + y: number + h: number + /** Palette key for the lit top face; absent means matte structure. */ + accent?: string + /** 0..1 — how much the accent bleeds onto the side faces. */ + glow?: number + /** 0..1 — how much of the top face the accent takes. A big plane at 1 is a + * wash of colour that drowns everything meaningful standing on it. */ + tint?: number + district: DistrictId +} + +export const N_BACKENDS = 12 +export const BUF_COLS = 8 +export const BUF_ROWS = 6 +export const N_FRAMES = BUF_COLS * BUF_ROWS +export const N_WAL_SEGMENTS = 7 +export const N_TABLES = 5 + +/** World Y of the storage layer, and of the kernel cache slab above it. */ +export const STORAGE_Y = -52 +export const OS_CACHE_Y = -24 + +export const DECK = { w: 156, d: 124, y: 0, h: 5 } +const BUF_PITCH_X = 17 +const BUF_PITCH_Z = 18.5 +export const BUF_TILE = 12.5 +/** + * The excavation. The ground is cut away here so the storage layer below is + * visible, and nothing that stands at ground level may be placed inside it. + * It runs from under the plaza's southern half out to open ground before the + * standby's site: the plaza floats over its northern end, and the rest is the + * cutaway a reader looks down into. A symmetric hole centred on the plaza + * cannot work from a fixed camera — the plaza would cover the half of the + * floor that its own near wall did not already hide. + */ +export const PIT = { x0: -124, x1: 108, z0: -30, z1: 180 } + +export function backendX(i: number): number { + const span = 232 + return -span / 2 + (i * span) / (N_BACKENDS - 1) +} + +export function frameCentre(i: number, out: { x: number; z: number }): void { + const col = i % BUF_COLS + const row = (i / BUF_COLS) | 0 + out.x = -((BUF_COLS - 1) * BUF_PITCH_X) / 2 + col * BUF_PITCH_X + out.z = -((BUF_ROWS - 1) * BUF_PITCH_Z) / 2 + row * BUF_PITCH_Z +} + +/** The standby's own buffer frames: fewer, because it is a smaller claim. */ +export const STANDBY = { x: 120, z: 284, cols: 4, rows: 3, pitch: 15, tile: 10.5 } +export const N_STANDBY_FRAMES = STANDBY.cols * STANDBY.rows + +export function standbyFrameCentre(i: number, out: { x: number; z: number }): void { + const col = i % STANDBY.cols + const row = (i / STANDBY.cols) | 0 + out.x = STANDBY.x - ((STANDBY.cols - 1) * STANDBY.pitch) / 2 + col * STANDBY.pitch + out.z = STANDBY.z - ((STANDBY.rows - 1) * STANDBY.pitch) / 2 + row * STANDBY.pitch +} + +export function walSegmentZ(i: number): number { + const pitch = 13 + return -((N_WAL_SEGMENTS - 1) * pitch) / 2 + i * pitch +} + +export function tableX(i: number): number { + const pitch = 42 + return -((N_TABLES - 1) * pitch) / 2 + i * pitch +} + +/** Static structures, in no particular order; the renderer depth-sorts them. */ +export function buildBoxes(): Box[] { + const boxes: Box[] = [] + + // Client terminal, outside the server boundary. + boxes.push({ x: 0, z: -300, w: 118, d: 30, y: 0, h: 13, district: 'clients' }) + boxes.push({ x: -34, z: -300, w: 20, d: 16, y: 13, h: 6, accent: 'client', glow: 0.5, district: 'clients' }) + boxes.push({ x: 34, z: -300, w: 20, d: 16, y: 13, h: 6, accent: 'client', glow: 0.5, district: 'clients' }) + + // Postmaster: one tower on the centre line, alone, because the conduits are + // forbidden to touch it. + boxes.push({ x: 0, z: -215, w: 30, d: 26, y: 0, h: 30, district: 'postmaster' }) + boxes.push({ x: 0, z: -215, w: 20, d: 17, y: 30, h: 5, accent: 'postmaster', glow: 0.7, district: 'postmaster' }) + + // Backend row. Heights are animated, so only the plinth is static here. + for (let i = 0; i < N_BACKENDS; i++) { + boxes.push({ x: backendX(i), z: -130, w: 13, d: 14, y: 0, h: 2, district: 'backends' }) + } + + // The shared-memory deck the buffer pool sits on. + boxes.push({ + x: 0, z: 0, w: DECK.w, d: DECK.d, y: DECK.y, h: DECK.h, + accent: 'shmem', glow: 0.3, tint: 0.16, district: 'pool', + }) + // wal_buffers: shared memory too, at the deck's east edge, where WAL is + // staged before any of it reaches pg_wal. + boxes.push({ x: 66, z: 0, w: 14, d: 46, y: DECK.h, h: 9, accent: 'wal', glow: 0.35, district: 'wal' }) + + // pg_wal: the segment vault, east. + boxes.push({ x: 128, z: -34, w: 26, d: 24, y: 0, h: 18, district: 'wal' }) + boxes.push({ x: 168, z: 0, w: 30, d: 104, y: 0, h: 4, district: 'wal' }) + // The walsender: the process that reads pg_wal and ships it south. + boxes.push({ x: 206, z: 46, w: 22, d: 20, y: 0, h: 16, district: 'standby' }) + + // Archive estate, further east and off the server's own ground. + boxes.push({ x: 200, z: -48, w: 20, d: 18, y: 0, h: 14, district: 'archive' }) + boxes.push({ x: 232, z: -66, w: 40, d: 30, y: 0, h: 9, district: 'archive' }) + + // Maintenance yard, west. + boxes.push({ x: -140, z: -40, w: 30, d: 28, y: 0, h: 22, district: 'maintenance' }) + boxes.push({ x: -140, z: 34, w: 28, d: 26, y: 0, h: 16, district: 'maintenance' }) + boxes.push({ x: -196, z: 0, w: 26, d: 24, y: 0, h: 19, district: 'maintenance' }) + + // Standby: its own walreceiver, its own startup process, its own deck. Its + // buffer frames are drawn from replay state, not placed here. + boxes.push({ x: 120, z: 212, w: 22, d: 20, y: 0, h: 15, district: 'standby' }) + boxes.push({ x: 120, z: 244, w: 24, d: 22, y: 0, h: 20, district: 'standby' }) + boxes.push({ x: 120, z: 284, w: 74, d: 54, y: 0, h: 3, district: 'standby' }) + + return boxes +} + +/** + * Everything below the excavation. Kept separate from the surface because the + * ground has a hole cut in it and these have to be painted before that hole + * exists — otherwise the data directory ends up drawn on top of the city it + * is supposed to be underneath. + */ +export function buildUnderBoxes(): Box[] { + const boxes: Box[] = [] + /* The data directory floor. It stays inside the excavation, because the + * only thing making the memory/storage boundary legible is that you are + * looking down through a hole at it. */ + boxes.push({ + x: (PIT.x0 + PIT.x1) / 2, z: (PIT.z0 + PIT.z1) / 2, + w: PIT.x1 - PIT.x0 - 8, d: PIT.z1 - PIT.z0 - 8, + y: STORAGE_Y - 6, h: 6, accent: 'storage', glow: 0.08, tint: 0.06, district: 'storage', + }) + /* Heap files, then the indexes on them nearer the viewer. Both stand clear + * of the plaza's footprint, which is the only part of the floor a reader + * can see down onto. */ + for (let i = 0; i < N_TABLES; i++) { + boxes.push({ x: tableX(i), z: 84, w: 30, d: 24, y: STORAGE_Y, h: 18, accent: 'storage', glow: 0.45, district: 'storage' }) + boxes.push({ x: tableX(i), z: 118, w: 17, d: 18, y: STORAGE_Y, h: 16, accent: 'index', glow: 0.5, district: 'storage' }) + } + return boxes +} + +/* -------------------------------------------------------------------------- + * Routes. Every animated packet travels one of these; the id names the real + * mechanism, and the order they fire in is the order PostgreSQL does the work. + * + * The connection routes are per backend, not shared. That is not decoration: + * a PostgreSQL connection is a process and a socket held for the whole + * session, so it gets its own duct from the client terminal to one backend + * and keeps it. Sharing one lane between all eight would draw the thing + * people already wrongly believe — that a statement is handed to whichever + * worker is free. + * ------------------------------------------------------------------------*/ + +export type RouteId = + | 'conn' + | 'bufReq' + | 'pageRead' + | 'walIns' + | 'walFlush' + | 'walFsync' + | 'ckptSweep' + | 'bgwSweep' + | 'pageWrite' + | 'archiveShip' + | 'stream' + | 'replay' + | 'vacGo' + +export interface RouteDef { + /** Named routes carry their id; per-backend variants carry the family name. */ + id: string + pts: readonly (readonly [number, number, number])[] + accent: string + /** World units per simulated second. */ + speed: number + /** Draw a faint road under it. */ + road?: boolean + /** Set on per-backend routes: which backend, and whether the road is a + * connection duct that should only appear while that backend exists. */ + backend?: number + conduit?: boolean +} + +const CONDUIT_Y = 7 +const TERMINAL_FACE = -286 +const BACKEND_FACE = -140 +/** No duct may come closer than this to the centre line — the postmaster's. */ +const CLEAR_X = 24 + +/** X of connection duct i where it leaves the client terminal. */ +export function conduitX(i: number): number { + const half = N_BACKENDS / 2 + const inner = 24 + const step = 12 + const west = i < half + const k = west ? half - 1 - i : i - half + const x = inner + k * step + return west ? -x : x +} + +const routes: RouteDef[] = [] +function addRoute(def: RouteDef): number { + routes.push(def) + return routes.length - 1 +} + +/* --- named routes -------------------------------------------------------- */ + +const named: Partial<Record<RouteId, number>> = {} +function named_(id: RouteId, def: Omit<RouteDef, 'id'>): void { + named[id] = addRoute({ id, ...def }) +} + +/* A new connection walks the arrivals avenue to the postmaster's door. This + * happens once per connection, not once per statement. */ +named_('conn', { + accent: 'client', + speed: 170, + road: true, + pts: [ + [0, 2, -282], + [0, 2, -252], + [0, 2, -224], + ], +}) + +named_('bufReq', { + accent: 'backend', + speed: 300, + road: true, + pts: [ + [0, 10, -122], + [0, 11, -80], + [0, 8, -46], + ], +}) + +named_('pageRead', { + accent: 'storage', + speed: 220, + road: true, + pts: [ + [-34, STORAGE_Y + 20, 80], + [-22, OS_CACHE_Y, 42], + [-12, 6, 2], + ], +}) + +named_('walIns', { + accent: 'wal', + speed: 360, + road: true, + pts: [ + [16, 11, -126], + [48, 14, -76], + [64, 11, -26], + [66, 10, -4], + ], +}) + +named_('walFlush', { + accent: 'wal', + speed: 330, + road: true, + pts: [ + [68, 10, -8], + [104, 12, -28], + [128, 11, -34], + [160, 8, -18], + [168, 6, -4], + ], +}) + +/* fsync: the record reaches durable media. Commit waits for this, and for + * nothing downstream of it. */ +named_('walFsync', { + accent: 'wal', + speed: 250, + pts: [ + [168, 4, 14], + [150, -18, 54], + [98, STORAGE_Y + 14, 92], + ], +}) + +named_('ckptSweep', { + accent: 'checkpoint', + speed: 230, + road: true, + pts: [ + [-124, 12, -40], + [-92, 10, -26], + [-52, 8, -10], + ], +}) + +named_('bgwSweep', { + accent: 'bgwriter', + speed: 230, + road: true, + pts: [ + [-124, 10, 34], + [-92, 9, 26], + [-52, 8, 14], + ], +}) + +named_('pageWrite', { + accent: 'bufDirty', + speed: 200, + road: true, + pts: [ + [10, 6, 10], + [8, OS_CACHE_Y, 48], + [6, STORAGE_Y + 20, 84], + ], +}) + +named_('archiveShip', { + accent: 'archive', + speed: 240, + road: true, + pts: [ + [176, 8, -34], + [200, 9, -48], + [232, 8, -64], + ], +}) + +named_('stream', { + accent: 'replication', + speed: 320, + road: true, + pts: [ + [178, 7, 26], + [206, 8, 46], + [200, 3, 110], + [166, 3, 166], + [126, 6, 202], + ], +}) + +named_('replay', { + accent: 'replication', + speed: 220, + pts: [ + [120, 9, 224], + [120, 10, 244], + [120, 7, 268], + ], +}) + +named_('vacGo', { + accent: 'vacuum', + speed: 150, + road: true, + pts: [ + [-186, 8, 6], + [-156, -12, 40], + [-122, STORAGE_Y + 20, 76], + [-88, STORAGE_Y + 20, 84], + ], +}) + +/* --- per-backend routes -------------------------------------------------- */ + +const forkRoutes = new Int16Array(N_BACKENDS) +const queryRoutes = new Int16Array(N_BACKENDS) +const resultRoutes = new Int16Array(N_BACKENDS) + +for (let i = 0; i < N_BACKENDS; i++) { + const bx = backendX(i) + const cx = conduitX(i) + /* Hold the duct out of the postmaster's avenue while it crosses the yard. */ + const raw = cx + (bx - cx) * 0.3 + const mx = Math.abs(raw) < CLEAR_X ? Math.sign(raw || 1) * CLEAR_X : raw + + forkRoutes[i] = addRoute({ + id: 'fork', + accent: 'postmaster', + speed: 260, + backend: i, + pts: [ + [0, 3, -202], + [bx * 0.35, 3, -178], + [bx * 0.8, 3, -154], + [bx, 5, BACKEND_FACE], + ], + }) + + queryRoutes[i] = addRoute({ + id: 'query', + accent: 'client', + speed: 340, + road: true, + conduit: true, + backend: i, + pts: [ + [cx, CONDUIT_Y, TERMINAL_FACE], + [cx, CONDUIT_Y, -250], + [mx, CONDUIT_Y, -212], + [bx, CONDUIT_Y, -168], + [bx, CONDUIT_Y, BACKEND_FACE], + ], + }) + + resultRoutes[i] = addRoute({ + id: 'result', + accent: 'ok', + speed: 370, + backend: i, + pts: [ + [bx + 3, CONDUIT_Y, BACKEND_FACE], + [bx + 3, CONDUIT_Y, -168], + [mx + 3, CONDUIT_Y, -212], + [cx + 3, CONDUIT_Y, -250], + [cx + 3, CONDUIT_Y, TERMINAL_FACE], + ], + }) +} + +export const ROUTES: readonly RouteDef[] = routes +export function routeOf(id: RouteId): number { + return named[id] as number +} +export function forkRoute(i: number): number { + return forkRoutes[i] +} +export function queryRoute(i: number): number { + return queryRoutes[i] +} +export function resultRoute(i: number): number { + return resultRoutes[i] +} diff --git a/src/components/PostgresCity/render.ts b/src/components/PostgresCity/render.ts new file mode 100644 index 00000000..2b61c7aa --- /dev/null +++ b/src/components/PostgresCity/render.ts @@ -0,0 +1,966 @@ +/** + * Canvas painter for the Postgres city. + * + * Everything screen-space is computed once, in `buildScene`, and reused: the + * per-frame path only reads simulation state and paints. Nothing in `draw` + * allocates, because a hero animation that makes garbage on a phone is a + * hero animation that stutters on a phone. + * + * Only meaning is allowed to be bright. Structure is painted from the + * palette's three matte faces; a district gets a lit face when its state + * means something, and a particle glows because it is carrying data. + */ + +import type { Palette } from './palette' +import { + BUF_TILE, + DECK, + DISTRICTS, + N_BACKENDS, + N_FRAMES, + N_TABLES, + N_WAL_SEGMENTS, + OS_CACHE_Y, + PIT, + ROUTES, + STORAGE_Y, + N_STANDBY_FRAMES, + STANDBY, + backendX, + buildBoxes, + buildUnderBoxes, + depth, + frameCentre, + project, + standbyFrameCentre, + tableX, + walSegmentZ, + type Box, + type DistrictId, + type Pt, +} from './plan' +import { + BE_COMMIT_WAIT, + BE_IDLE, + BE_IO_WAIT, + CONN_NONE, + CONN_OPEN, + FRAME_CLEAN, + FRAME_DIRTY, + PK_HEAVY, + type City, +} from './sim' + +const MONO = + '"JetBrains Mono Variable", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace' + +/** Look a semantic colour up by name. The plan stores names, not values, so a + * district cannot drift out of step with the palette it is painted from. */ +function accent(pal: Palette, key: string | undefined): string { + return (key && (pal as unknown as Record<string, string>)[key]) || pal.ink +} + +/* -------------------------------------------------------------------------- + * Static scene: everything that depends on geometry but not on state. + * ------------------------------------------------------------------------*/ + +interface SceneBox { + top: Float32Array + left: Float32Array + right: Float32Array + d: number + accent?: string + glow: number + tint: number + district: DistrictId +} + +export interface Scene { + boxes: SceneBox[] + /** Painted before the ground, so the hole in the ground reveals them. */ + under: SceneBox[] + /** The two inner faces of the excavation you can actually see into. */ + pitWallN: Float32Array + pitWallE: Float32Array + deckShadow: Float32Array + /** Survey lines on the ground plane, so the plane reads as a surface. */ + grid: Float32Array + standbyTiles: Float32Array + standbyOrder: Int16Array + /** Buffer-pool frames, back to front. */ + tileOrder: Int16Array + tiles: Float32Array + /** Storage-side anchor of each table, for marking autovacuum's target. */ + tables: Float32Array + /** Ground plane outline and the excavation cut through it. */ + ground: Float32Array + pit: Float32Array + /** Route polylines in scene space, with the world-space arclength fraction + * at each vertex so a particle's progress maps exactly. */ + routePts: Float32Array[] + routeFrac: Float32Array[] + /** District anchors, and where their labels sit. */ + anchors: Float32Array + labels: Float32Array + bounds: { minX: number; minY: number; maxX: number; maxY: number } +} + +const scratch: Pt = { x: 0, y: 0 } + +function quad( + out: Float32Array, + o: number, + ax: number, + ay: number, + az: number, + bx: number, + by: number, + bz: number, + cx: number, + cy: number, + cz: number, + dx: number, + dy: number, + dz: number, +): void { + project(ax, ay, az, scratch) + out[o] = scratch.x + out[o + 1] = scratch.y + project(bx, by, bz, scratch) + out[o + 2] = scratch.x + out[o + 3] = scratch.y + project(cx, cy, cz, scratch) + out[o + 4] = scratch.x + out[o + 5] = scratch.y + project(dx, dy, dz, scratch) + out[o + 6] = scratch.x + out[o + 7] = scratch.y +} + +/** + * Build the three visible faces of a box. With the plan rotated 30°, the + * faces that face the camera are the south (+z) and west (-x) ones; they land + * on the right and the left of the silhouette respectively. + */ +function boxFaces(b: Box): SceneBox { + const x0 = b.x - b.w / 2 + const x1 = b.x + b.w / 2 + const z0 = b.z - b.d / 2 + const z1 = b.z + b.d / 2 + const yb = b.y + const yt = b.y + b.h + + const top = new Float32Array(8) + quad(top, 0, x0, yt, z0, x1, yt, z0, x1, yt, z1, x0, yt, z1) + + const right = new Float32Array(8) + quad(right, 0, x0, yt, z1, x1, yt, z1, x1, yb, z1, x0, yb, z1) + + const left = new Float32Array(8) + quad(left, 0, x0, yt, z0, x0, yt, z1, x0, yb, z1, x0, yb, z0) + + return { + top, + left, + right, + d: depth(b.x, b.z) + b.y * 0.001, + accent: b.accent, + glow: b.glow ?? 0, + tint: b.tint ?? 1, + district: b.district, + } +} + +export function buildScene(): Scene { + const boxes = buildBoxes().map(boxFaces) + boxes.sort((a, b) => a.d - b.d) + const under = buildUnderBoxes().map(boxFaces) + under.sort((a, b) => a.d - b.d) + + /* Buffer frames: eight faces each would be noise at this size, so a frame is + * a flat plate on the deck whose colour is its state. */ + const tiles = new Float32Array(N_FRAMES * 8) + const tileDepth = new Float32Array(N_FRAMES) + const c = { x: 0, z: 0 } + for (let i = 0; i < N_FRAMES; i++) { + frameCentre(i, c) + const h = BUF_TILE / 2 + quad( + tiles, + i * 8, + c.x - h, DECK.h + 0.6, c.z - h, + c.x + h, DECK.h + 0.6, c.z - h, + c.x + h, DECK.h + 0.6, c.z + h, + c.x - h, DECK.h + 0.6, c.z + h, + ) + tileDepth[i] = depth(c.x, c.z) + } + const tileOrder = new Int16Array(N_FRAMES) + for (let i = 0; i < N_FRAMES; i++) tileOrder[i] = i + tileOrder.sort((a, b) => tileDepth[a] - tileDepth[b]) + + const tables = new Float32Array(N_TABLES * 2) + for (let i = 0; i < N_TABLES; i++) { + project(tableX(i), STORAGE_Y + 18, 84, scratch) + tables[i * 2] = scratch.x + tables[i * 2 + 1] = scratch.y + } + + /* The ground plane, and the rectangle cut out of it so the storage layer + * below is visible. The plaza floats over that hole. */ + const G = 470 + const ground = new Float32Array(8) + quad(ground, 0, -G, 0, -G, G, 0, -G, G, 0, G, -G, 0, G) + const pit = new Float32Array(8) + quad(pit, 0, PIT.x0, 0, PIT.z0, PIT.x1, 0, PIT.z0, PIT.x1, 0, PIT.z1, PIT.x0, 0, PIT.z1) + + /* Looking down into a hole, the faces you see are its far ones: the north + * wall and the east wall. Without them the excavation reads as a painted + * rectangle instead of a cut. */ + const pitWallN = new Float32Array(8) + quad( + pitWallN, 0, + PIT.x0, 0, PIT.z0, + PIT.x1, 0, PIT.z0, + PIT.x1, STORAGE_Y - 6, PIT.z0, + PIT.x0, STORAGE_Y - 6, PIT.z0, + ) + const pitWallE = new Float32Array(8) + quad( + pitWallE, 0, + PIT.x1, 0, PIT.z0, + PIT.x1, 0, PIT.z1, + PIT.x1, STORAGE_Y - 6, PIT.z1, + PIT.x1, STORAGE_Y - 6, PIT.z0, + ) + + const deckShadow = new Float32Array(8) + quad( + deckShadow, 0, + -DECK.w / 2, OS_CACHE_Y, -DECK.d / 2, + DECK.w / 2, OS_CACHE_Y, -DECK.d / 2, + DECK.w / 2, OS_CACHE_Y, DECK.d / 2, + -DECK.w / 2, OS_CACHE_Y, DECK.d / 2, + ) + + /* A survey grid on the ground. Flat colour at this size reads as a void; + * the lines are what make it a surface with the city standing on it. */ + const GRID_STEP = 50 + const gridLines: number[] = [] + for (let g = -G; g <= G; g += GRID_STEP) { + project(g, 0, -G, scratch) + gridLines.push(scratch.x, scratch.y) + project(g, 0, G, scratch) + gridLines.push(scratch.x, scratch.y) + project(-G, 0, g, scratch) + gridLines.push(scratch.x, scratch.y) + project(G, 0, g, scratch) + gridLines.push(scratch.x, scratch.y) + } + const grid = new Float32Array(gridLines) + + /* The standby's own frames. It rebuilds these from the log it receives, so + * they are drawn in the same page colours as the primary's pool. */ + const standbyTiles = new Float32Array(N_STANDBY_FRAMES * 8) + const standbyDepth = new Float32Array(N_STANDBY_FRAMES) + for (let i = 0; i < N_STANDBY_FRAMES; i++) { + standbyFrameCentre(i, c) + const h = STANDBY.tile / 2 + quad( + standbyTiles, + i * 8, + c.x - h, 3.6, c.z - h, + c.x + h, 3.6, c.z - h, + c.x + h, 3.6, c.z + h, + c.x - h, 3.6, c.z + h, + ) + standbyDepth[i] = depth(c.x, c.z) + } + const standbyOrder = new Int16Array(N_STANDBY_FRAMES) + for (let i = 0; i < N_STANDBY_FRAMES; i++) standbyOrder[i] = i + standbyOrder.sort((a, b) => standbyDepth[a] - standbyDepth[b]) + + /* Routes: scene-space vertices plus the world-space arclength fraction at + * each, so a particle at t maps onto the same physical point regardless of + * how the projection stretches a leg. */ + const routePts: Float32Array[] = [] + const routeFrac: Float32Array[] = [] + for (const r of ROUTES) { + const n = r.pts.length + const pts = new Float32Array(n * 2) + const frac = new Float32Array(n) + let total = 0 + for (let i = 0; i < n; i++) { + const p = r.pts[i] + project(p[0], p[1], p[2], scratch) + pts[i * 2] = scratch.x + pts[i * 2 + 1] = scratch.y + if (i > 0) { + const q = r.pts[i - 1] + total += Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2]) + } + frac[i] = total + } + for (let i = 0; i < n; i++) frac[i] = total > 0 ? frac[i] / total : i / (n - 1) + routePts.push(pts) + routeFrac.push(frac) + } + + const anchors = new Float32Array(DISTRICTS.length * 2) + const labels = new Float32Array(DISTRICTS.length * 2) + DISTRICTS.forEach((d, i) => { + project(d.x, d.y, d.z, scratch) + anchors[i * 2] = scratch.x + anchors[i * 2 + 1] = scratch.y + labels[i * 2] = scratch.x + d.lx + labels[i * 2 + 1] = scratch.y + d.ly + }) + + /* Fit to whatever the structures and labels actually occupy. */ + let minX = Infinity + let minY = Infinity + let maxX = -Infinity + let maxY = -Infinity + const consider = (x: number, y: number): void => { + if (x < minX) minX = x + if (x > maxX) maxX = x + if (y < minY) minY = y + if (y > maxY) maxY = y + } + for (const b of boxes.concat(under)) { + for (const face of [b.top, b.left, b.right]) { + for (let i = 0; i < 8; i += 2) consider(face[i], face[i + 1]) + } + } + /* Reserve the plate around each label anchor. Fitting to the bare point + * clips the topmost label off the frame. */ + const LABEL_PAD_X = 52 + const LABEL_PAD_Y = 26 + for (let i = 0; i < labels.length; i += 2) { + consider(labels[i] - LABEL_PAD_X, labels[i + 1] - LABEL_PAD_Y) + consider(labels[i] + LABEL_PAD_X, labels[i + 1] + LABEL_PAD_Y) + } + + return { + boxes, + under, + pitWallN, + pitWallE, + deckShadow, + grid, + standbyTiles, + standbyOrder, + tileOrder, + tiles, + tables, + ground, + pit, + routePts, + routeFrac, + anchors, + labels, + bounds: { minX, minY, maxX, maxY }, + } +} + +/* -------------------------------------------------------------------------- + * Painting. + * ------------------------------------------------------------------------*/ + +export interface View { + /** Scale and offset from scene space to device pixels. */ + s: number + ox: number + oy: number + /** Device pixels. */ + w: number + h: number + /** CSS pixels, which is what type size and the compact cutover key off. */ + cssW: number + cssH: number + dpr: number + /** Below this width the scene drops its secondary labels. */ + compact: boolean +} + +export function fitView(scene: Scene, cssW: number, cssH: number, dpr: number): View { + const w = cssW * dpr + const h = cssH * dpr + const b = scene.bounds + const padX = (cssW < 560 ? 14 : 30) * dpr + const padTop = 30 * dpr + /* The explanation strip overlays the bottom of the canvas; the scene has to + * end above it or a district label ends up underneath the prose. */ + const padBottom = 62 * dpr + const sw = b.maxX - b.minX + const sh = b.maxY - b.minY + const s = Math.min((w - padX * 2) / sw, (h - padTop - padBottom) / sh) + return { + s, + ox: w / 2 - ((b.minX + b.maxX) / 2) * s, + oy: (padTop + (h - padBottom)) / 2 - ((b.minY + b.maxY) / 2) * s, + w, + h, + cssW, + cssH, + dpr, + compact: cssW < 620, + } +} + +/** Append a quad as a subpath. The caller owns `beginPath`, because the + * ground needs two subpaths in one path to cut the excavation out of it. */ +function subPoly(ctx: CanvasRenderingContext2D, p: Float32Array, o: number, v: View): void { + ctx.moveTo(p[o] * v.s + v.ox, p[o + 1] * v.s + v.oy) + ctx.lineTo(p[o + 2] * v.s + v.ox, p[o + 3] * v.s + v.oy) + ctx.lineTo(p[o + 4] * v.s + v.ox, p[o + 5] * v.s + v.oy) + ctx.lineTo(p[o + 6] * v.s + v.ox, p[o + 7] * v.s + v.oy) + ctx.closePath() +} + +function poly(ctx: CanvasRenderingContext2D, p: Float32Array, o: number, v: View): void { + ctx.beginPath() + subPoly(ctx, p, o, v) +} + +/** Cheap tint: mix a colour toward a target by `k` using globalAlpha layers. */ +function fillQuad( + ctx: CanvasRenderingContext2D, + p: Float32Array, + o: number, + v: View, + color: string, + alpha = 1, +): void { + poly(ctx, p, o, v) + ctx.globalAlpha = alpha + ctx.fillStyle = color + ctx.fill() + ctx.globalAlpha = 1 +} + +/** A tower: four corners of a footprint extruded to `h`, drawn in place. */ +function tower( + ctx: CanvasRenderingContext2D, + v: View, + pal: Palette, + cx: number, + cz: number, + w: number, + d: number, + y0: number, + h: number, + accent: string, + lit: number, +): void { + const x0 = cx - w / 2 + const x1 = cx + w / 2 + const z0 = cz - d / 2 + const z1 = cz + d / 2 + const yt = y0 + h + + const buf = towerBuf + quad(buf, 0, x0, yt, z0, x1, yt, z0, x1, yt, z1, x0, yt, z1) + quad(buf, 8, x0, yt, z1, x1, yt, z1, x1, y0, z1, x0, y0, z1) + quad(buf, 16, x0, yt, z0, x0, yt, z1, x0, y0, z1, x0, y0, z0) + + fillQuad(ctx, buf, 16, v, pal.matLeft) + fillQuad(ctx, buf, 8, v, pal.matRight) + if (lit > 0) { + fillQuad(ctx, buf, 8, v, accent, lit * 0.45) + fillQuad(ctx, buf, 16, v, accent, lit * 0.28) + } + fillQuad(ctx, buf, 0, v, lit > 0.12 ? accent : pal.matTop, lit > 0.12 ? Math.min(1, 0.45 + lit) : 1) +} +const towerBuf = new Float32Array(24) + +export interface DrawOpts { + scene: Scene + view: View + pal: Palette + city: City + /** District under the pointer or keyboard focus, if any. */ + active: DistrictId | null +} + +/** + * Paint one frame. There is no time argument on purpose: everything that + * moves is a consequence of model state, so a paused scene is a real instant + * of the cluster rather than an animation holding still. + */ +export function draw(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { view: v, pal } = o + + /* Sky. */ + const g = ctx.createLinearGradient(0, 0, 0, v.h) + g.addColorStop(0, pal.skyTop) + g.addColorStop(1, pal.skyBottom) + ctx.fillStyle = g + ctx.fillRect(0, 0, v.w, v.h) + + /* Order is the argument. Storage is painted first and the ground is then + * painted over it with the excavation cut out, so the only reason you can + * see the data directory at all is that you are looking down a hole at it. + * Paint it after the ground and the picture says the opposite. */ + drawUnderground(ctx, o) + drawVacuumTarget(ctx, o) + drawGround(ctx, o) + drawRoads(ctx, o) + drawStructures(ctx, o) + drawPool(ctx, o) + drawStandbyPool(ctx, o) + drawBackends(ctx, o) + drawWalSegments(ctx, o) + drawParticles(ctx, o) + drawLabels(ctx, o) +} + +function drawUnderground(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { scene, view: v, pal, active } = o + const dim = active && active !== 'storage' ? 0.5 : 1 + + /* Two inner walls of the cut. Everything below ground gets one value + * darker than the ground above it, so depth is carried by value and the + * data directory can still be the brightest thing down there. */ + fillQuad(ctx, scene.pitWallN, 0, v, pal.pitWall, dim) + fillQuad(ctx, scene.pitWallE, 0, v, pal.underground, dim) + + /* The plaza's footprint dropped onto the pit floor. Without it the deck + * reads as resting on the floor rather than hanging over it. */ + fillQuad(ctx, scene.deckShadow, 0, v, '#000000', 0.34 * dim) + + for (const b of scene.under) { + fillQuad(ctx, b.left, 0, v, pal.matLeft, dim) + fillQuad(ctx, b.right, 0, v, pal.matRight, dim) + const c = accent(pal, b.accent) + if (b.tint < 1) fillQuad(ctx, b.top, 0, v, pal.matTop, dim) + fillQuad(ctx, b.top, 0, v, b.accent ? c : pal.matTop, dim * (b.accent ? b.tint : 1)) + if (b.glow > 0) { + fillQuad(ctx, b.right, 0, v, c, b.glow * 0.4 * dim) + fillQuad(ctx, b.left, 0, v, c, b.glow * 0.25 * dim) + } + } +} + +function drawGround(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { scene, view: v, pal } = o + + /* Ground with the excavation cut out of it, so the storage layer below + * stays visible and the plaza reads as floating over a hole. */ + ctx.beginPath() + subPoly(ctx, scene.ground, 0, v) + subPoly(ctx, scene.pit, 0, v) + ctx.fillStyle = pal.ground + ctx.fill('evenodd') + + /* Survey lines, clipped to the ground so they never cross the hole. */ + ctx.save() + ctx.clip('evenodd') + ctx.strokeStyle = pal.grid + ctx.lineWidth = Math.max(1, 1 * v.dpr) + ctx.globalAlpha = 0.65 + ctx.beginPath() + const g = scene.grid + for (let i = 0; i < g.length; i += 4) { + ctx.moveTo(g[i] * v.s + v.ox, g[i + 1] * v.s + v.oy) + ctx.lineTo(g[i + 2] * v.s + v.ox, g[i + 3] * v.s + v.oy) + } + ctx.stroke() + ctx.restore() + ctx.globalAlpha = 1 + + poly(ctx, scene.pit, 0, v) + ctx.strokeStyle = pal.groundEdge + ctx.lineWidth = Math.max(1.5, 2 * v.dpr) + ctx.globalAlpha = 0.9 + ctx.stroke() + ctx.globalAlpha = 1 +} + +function drawRoads(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { scene, view: v, pal, city } = o + ctx.lineWidth = Math.max(1, 1.4 * v.dpr) + ctx.lineCap = 'round' + for (let r = 0; r < ROUTES.length; r++) { + const def = ROUTES[r] + if (!def.road) continue + /* A connection duct only exists while its backend does. The road is the + * connection, not a lane that statements are dispatched into. */ + if (def.conduit && city.backendConn[def.backend as number] === CONN_NONE) continue + const pts = scene.routePts[r] + ctx.beginPath() + ctx.moveTo(pts[0] * v.s + v.ox, pts[1] * v.s + v.oy) + for (let i = 2; i < pts.length; i += 2) { + ctx.lineTo(pts[i] * v.s + v.ox, pts[i + 1] * v.s + v.oy) + } + ctx.globalAlpha = def.conduit ? 0.34 : 0.28 + ctx.strokeStyle = accent(pal, def.accent) + ctx.stroke() + } + ctx.globalAlpha = 1 +} + +function drawStructures(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { scene, view: v, pal, active } = o + for (const b of scene.boxes) { + const dim = active && b.district !== active ? 0.45 : 1 + ctx.globalAlpha = dim + fillQuad(ctx, b.left, 0, v, pal.matLeft, dim) + fillQuad(ctx, b.right, 0, v, pal.matRight, dim) + if (b.accent) { + const c = accent(pal, b.accent) + if (b.tint < 1) fillQuad(ctx, b.top, 0, v, pal.matTop, dim) + fillQuad(ctx, b.top, 0, v, c, dim * b.tint) + if (b.glow > 0) { + fillQuad(ctx, b.right, 0, v, c, b.glow * 0.4 * dim) + fillQuad(ctx, b.left, 0, v, c, b.glow * 0.25 * dim) + } + } else { + fillQuad(ctx, b.top, 0, v, pal.matTop, dim) + } + } + ctx.globalAlpha = 1 +} + +const frameCentreScratch = { x: 0, z: 0 } + +function drawPool(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { scene, view: v, pal, city, active } = o + const dim = active && active !== 'pool' ? 0.45 : 1 + + for (let k = 0; k < scene.tileOrder.length; k++) { + const i = scene.tileOrder[k] + const st = city.frameState[i] + let color = pal.bufFree + let a = dim + if (st === FRAME_CLEAN) color = pal.bufClean + else if (st === FRAME_DIRTY) color = pal.bufDirty + else a = dim * 0.75 + if (city.framePinned[i] > 0) color = pal.bufPinned + + /* Dirty frames stand proud of the deck. Blue and red are far apart in hue + * but close in luminance, so on a monochrome display or to a reader with + * a colour-vision deficiency the pool would otherwise say nothing at all. + * Height is the redundant channel — and it is the right metaphor: a dirty + * page is work the cluster still owes its storage. */ + frameCentre(i, frameCentreScratch) + const h = st === FRAME_DIRTY ? 3.4 : 0.9 + tower( + ctx, + v, + pal, + frameCentreScratch.x, + frameCentreScratch.z, + BUF_TILE, + BUF_TILE, + DECK.h, + h, + color, + a, + ) + + /* A frame a backend just touched flares briefly, so a reader can see that + * one page was hit rather than the pool changing as a block. `towerBuf` + * still holds that tile's top face. */ + const hot = city.frameHot[i] + if (hot > 0) fillQuad(ctx, towerBuf, 0, v, pal.flash, Math.min(0.55, hot) * dim) + + /* The clock sweep's hand: the next frame it will consider. */ + if (i === city.sweepHand % N_FRAMES) { + poly(ctx, towerBuf, 0, v) + ctx.strokeStyle = pal.ink + ctx.globalAlpha = 0.75 * dim + ctx.lineWidth = Math.max(1, 1.4 * v.dpr) + ctx.stroke() + ctx.globalAlpha = 1 + } + } +} + +/** + * The standby's buffer frames. They come and go with replay, because that is + * what a replica does: it reads the log and reconstructs pages of its own. No + * page ever crosses the wire. + */ +function drawStandbyPool(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { scene, view: v, pal, city, active } = o + const dim = active && active !== 'standby' ? 0.45 : 1 + const replayed = city.replayedBytes + for (let k = 0; k < scene.standbyOrder.length; k++) { + const i = scene.standbyOrder[k] + /* Each frame lights when replay has passed its slot; the wave running + * across the deck is the startup process working through the log. */ + const phase = (replayed / 90 + i * 0.37) % 1 + const lit = phase < 0.34 + fillQuad(ctx, scene.standbyTiles, i * 8, v, lit ? pal.bufDirty : pal.bufClean, dim * (lit ? 0.95 : 0.7)) + } +} + +function drawBackends(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { view: v, pal, city, active } = o + const dim = active && active !== 'backends' ? 0.45 : 1 + ctx.globalAlpha = dim + for (let i = 0; i < N_BACKENDS; i++) { + if (city.backendConn[i] === CONN_NONE) continue + const opening = city.backendConn[i] !== CONN_OPEN + const st = city.backendState[i] + const load = city.backendLoad[i] + const h = 8 + load * 16 + let accent = pal.backend + let lit = 0.15 + load * 0.75 + if (opening) { + accent = pal.postmaster + lit = 0.5 + } else if (st === BE_COMMIT_WAIT) { + /* Waiting for its own WAL flush. Amber, because what it is waiting for + * is the write-ahead log, not the storage its page will land on. */ + accent = pal.wal + lit = 1 + } else if (st === BE_IO_WAIT) { + accent = pal.storage + lit = 0.9 + } else if (st === BE_IDLE) { + lit = 0.18 + } + tower(ctx, v, pal, backendX(i), -130, 11, 12, 2, opening ? 5 : h, accent, lit) + } + ctx.globalAlpha = 1 +} + +function drawWalSegments(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { view: v, pal, city, active } = o + const dim = active && active !== 'wal' ? 0.45 : 1 + ctx.globalAlpha = dim + for (let i = 0; i < N_WAL_SEGMENTS; i++) { + const fill = city.segments[i] + const current = i === city.segmentHead % N_WAL_SEGMENTS + const h = 3 + fill * 17 + tower(ctx, v, pal, 168, walSegmentZ(i), 20, 9, 4, h, current ? pal.wal : pal.walDim, current ? 0.9 : 0.5) + } + ctx.globalAlpha = 1 +} + +/** + * The table autovacuum is currently working on. Marking the target is the + * only way the reader can tell that vacuum visits a specific relation rather + * than sweeping the whole data directory. + */ +function drawVacuumTarget(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { scene, view: v, pal, city } = o + const t = city.vacuumTable + if (t < 0 || t >= N_TABLES || city.pulseVacuum <= 0.02) return + const x = scene.tables[t * 2] * v.s + v.ox + const y = scene.tables[t * 2 + 1] * v.s + v.oy + ctx.globalAlpha = Math.min(0.8, city.pulseVacuum) + ctx.strokeStyle = pal.vacuum + ctx.lineWidth = Math.max(1, 1.6 * v.dpr) + ctx.beginPath() + ctx.ellipse(x, y, 18 * v.s * 0.9, 18 * v.s * 0.9 * 0.52, 0, 0, Math.PI * 2) + ctx.stroke() + ctx.globalAlpha = 1 +} + +const partPos: Pt = { x: 0, y: 0 } + +/** Position a particle at fraction t along a route, in device pixels. */ +function pointOnRoute(scene: Scene, v: View, r: number, t: number, out: Pt): void { + const pts = scene.routePts[r] + const frac = scene.routeFrac[r] + const n = frac.length + let i = 1 + while (i < n - 1 && frac[i] < t) i++ + const f0 = frac[i - 1] + const f1 = frac[i] + const k = f1 > f0 ? (t - f0) / (f1 - f0) : 0 + const x = pts[(i - 1) * 2] + (pts[i * 2] - pts[(i - 1) * 2]) * k + const y = pts[(i - 1) * 2 + 1] + (pts[i * 2 + 1] - pts[(i - 1) * 2 + 1]) * k + out.x = x * v.s + v.ox + out.y = y * v.s + v.oy +} + +function drawParticles(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { scene, view: v, pal, city, active } = o + /* Size a packet in world units so it stays the same size relative to the + * city, with a floor so it does not vanish on a phone. Clamping to a + * constant device size instead made a packet a blob two frames wide at + * 390px. */ + const base = Math.max(2 * v.dpr, 1.5 * v.s) + + for (let i = 0; i < city.pAlive.length; i++) { + if (!city.pAlive[i]) continue + const r = city.pRoute[i] + const def = ROUTES[r] + if (active && !routeTouches(def.id, active)) { + ctx.globalAlpha = 0.22 + } else { + ctx.globalAlpha = 1 + } + pointOnRoute(scene, v, r, city.pT[i], partPos) + const color = accent(pal, def.accent) + const rad = city.pKind[i] === PK_HEAVY ? base * 1.35 : base + + /* Two circles rather than a shadow blur: a halo the eye reads as glow, + * at a fraction of the cost on a software rasteriser. */ + ctx.fillStyle = color + ctx.globalAlpha *= 0.18 + ctx.beginPath() + ctx.arc(partPos.x, partPos.y, rad * 2.6, 0, Math.PI * 2) + ctx.fill() + ctx.globalAlpha = ctx.globalAlpha / 0.18 + ctx.beginPath() + ctx.arc(partPos.x, partPos.y, rad, 0, Math.PI * 2) + ctx.fill() + } + ctx.globalAlpha = 1 +} + +/** Which districts a route belongs to, for dimming everything else. */ +function routeTouches(id: string, d: DistrictId): boolean { + switch (d) { + case 'clients': + return id === 'query' || id === 'result' || id === 'conn' + case 'postmaster': + return id === 'conn' || id === 'fork' + case 'backends': + return id === 'query' || id === 'result' || id === 'fork' || id === 'bufReq' || id === 'walIns' + case 'pool': + return ( + id === 'bufReq' || + id === 'pageRead' || + id === 'pageWrite' || + id === 'ckptSweep' || + id === 'bgwSweep' + ) + case 'wal': + return id === 'walIns' || id === 'walFlush' || id === 'walFsync' || id === 'archiveShip' + case 'archive': + return id === 'archiveShip' + case 'maintenance': + return id === 'ckptSweep' || id === 'bgwSweep' || id === 'vacGo' + case 'storage': + return id === 'pageRead' || id === 'pageWrite' || id === 'walFsync' || id === 'vacGo' + case 'standby': + return id === 'stream' || id === 'replay' + default: + return false + } +} + +/* -------------------------------------------------------------------------- + * Type. Monospace, because that is the page this lives on, and because a + * district label is a name, not a caption. + * ------------------------------------------------------------------------*/ + +function drawLabels(ctx: CanvasRenderingContext2D, o: DrawOpts): void { + const { scene, view: v, pal, city, active } = o + const size = Math.max(9, Math.min(12.5, v.cssW / 62)) * v.dpr + const subSize = size * 0.84 + + for (let i = 0; i < DISTRICTS.length; i++) { + const d = DISTRICTS[i] + const x = scene.labels[i * 2] * v.s + v.ox + const y = scene.labels[i * 2 + 1] * v.s + v.oy + const ax = scene.anchors[i * 2] * v.s + v.ox + const ay = scene.anchors[i * 2 + 1] * v.s + v.oy + const on = active === d.id + if (v.compact && !on && d.id !== 'pool' && d.id !== 'wal' && d.id !== 'storage') continue + + const readout = readoutFor(d.id, city) + ctx.font = `600 ${size}px ${MONO}` + ctx.textAlign = 'center' + ctx.textBaseline = 'alphabetic' + const wLabel = ctx.measureText(d.label).width + ctx.font = `400 ${subSize}px ${MONO}` + const wSub = v.compact ? 0 : ctx.measureText(d.sub).width + const wRead = readout ? ctx.measureText(readout).width : 0 + const w = Math.max(wLabel, wSub, wRead) + 12 * v.dpr + const lines = 1 + (v.compact ? 0 : 1) + (readout ? 1 : 0) + const h = size * 1.25 * lines + 8 * v.dpr + + /* A leader line, so a label that had to move out of the way still names + * the thing it is naming. */ + ctx.beginPath() + ctx.moveTo(x, y - h / 2) + ctx.lineTo(ax, ay) + ctx.strokeStyle = on ? accent(pal, d.accent) : pal.inkDim + ctx.globalAlpha = on ? 0.7 : 0.35 + ctx.lineWidth = Math.max(1, 1 * v.dpr) + ctx.stroke() + ctx.globalAlpha = 1 + + ctx.fillStyle = pal.plate + ctx.globalAlpha = on ? 1 : 0.92 + roundRect(ctx, x - w / 2, y - h, w, h, 3 * v.dpr) + ctx.fill() + if (on) { + ctx.strokeStyle = accent(pal, d.accent) + ctx.lineWidth = Math.max(1, 1 * v.dpr) + ctx.stroke() + } + ctx.globalAlpha = 1 + + let ty = y - h + size * 1.05 + 3 * v.dpr + ctx.font = `600 ${size}px ${MONO}` + ctx.fillStyle = on ? accent(pal, d.accent) : pal.ink + ctx.fillText(d.label, x, ty) + if (!v.compact) { + ty += size * 1.15 + ctx.font = `400 ${subSize}px ${MONO}` + ctx.fillStyle = pal.inkDim + ctx.fillText(d.sub, x, ty) + } + if (readout) { + ty += size * 1.15 + ctx.font = `500 ${subSize}px ${MONO}` + ctx.fillStyle = accent(pal, d.accent) + ctx.fillText(readout, x, ty) + } + } +} + +/** + * The three figures worth putting on screen. All are dimensionless on + * purpose: a ratio cannot be mistaken for somebody's throughput, and this + * scene has no business implying it measured one. + */ +function readoutFor(id: DistrictId, city: City): string | null { + switch (id) { + case 'pool': + return `hit ${(city.hitRatio * 100).toFixed(1)}% dirty ${Math.round(city.dirtyRatio * 100)}%` + case 'standby': + return `replay lag ${city.replayLag < 0.02 ? 'none' : bar(city.replayLag)}` + default: + return null + } +} + +const BAR = '▁▂▃▄▅▆▇' +function bar(v: number): string { + const n = Math.max(0, Math.min(BAR.length - 1, Math.round(v * (BAR.length - 1)))) + return BAR[n].repeat(3) +} + +function roundRect( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + w: number, + h: number, + r: number, +): void { + ctx.beginPath() + ctx.moveTo(x + r, y) + ctx.arcTo(x + w, y, x + w, y + h, r) + ctx.arcTo(x + w, y + h, x, y + h, r) + ctx.arcTo(x, y + h, x, y, r) + ctx.arcTo(x, y, x + w, y, r) + ctx.closePath() +} + +/** Screen position of a district anchor, for placing the DOM hit targets. */ +export function anchorAt(scene: Scene, v: View, i: number, out: Pt): void { + /* The label, not the district: the plate is the visible affordance, so it + * is where a pointer and a Tab stop both expect the target to be. */ + out.x = scene.labels[i * 2] * v.s + v.ox + out.y = scene.labels[i * 2 + 1] * v.s + v.oy - 10 * v.dpr +} diff --git a/src/components/PostgresCity/sim.test.ts b/src/components/PostgresCity/sim.test.ts new file mode 100644 index 00000000..3f74435b --- /dev/null +++ b/src/components/PostgresCity/sim.test.ts @@ -0,0 +1,269 @@ +/** + * What the model is allowed to teach. + * + * These assert durable properties of PostgreSQL that the animation is making + * claims about, not the particular numbers this calibration happens to + * produce. If a change makes the model more correct and one of these goes + * red, the assertion is what was wrong — moving a knob to keep a test green + * is how a model quietly stops being true. + * + * Run with `bun test`. + */ + +import { describe, expect, it } from 'bun:test' +import { BE_COMMIT_WAIT, CONN_NONE, CONN_OPEN, FRAME_DIRTY, createCity, type City } from './sim' +import { N_BACKENDS, N_FRAMES } from './plan' + +const STEP = 1 / 60 + +function run(city: City, seconds: number, each?: (c: City) => void): void { + const n = Math.round(seconds / STEP) + for (let i = 0; i < n; i++) { + city.step(STEP) + each?.(city) + } +} + +describe('the buffer pool', () => { + it('caps usage_count at 5, as the clock sweep does', () => { + const city = createCity() + let max = 0 + run(city, 120, (c) => { + for (let i = 0; i < N_FRAMES; i++) if (c.frameUsage[i] > max) max = c.frameUsage[i] + }) + /* Must reach the cap and must not pass it: below, the sweep is not being + * exercised; above, the cap is not being applied. */ + expect(max).toBe(5) + }) + + it('reports the hit ratio by PostgreSQL’s own formula', () => { + const city = createCity() + run(city, 30) + expect(city.hitRatio).toBeCloseTo(city.blksHit / (city.blksHit + city.blksRead), 10) + }) + + it('keeps the hit ratio in a range a real OLTP database reaches', () => { + /* A front page reporting 40% would be teaching that a healthy database + * misses half its reads. What this pins is the working set, not the + * arithmetic. */ + const city = createCity() + let sum = 0 + let n = 0 + run(city, 180, (c) => { + sum += c.hitRatio + n++ + }) + expect(sum / n).toBeGreaterThan(0.88) + expect(sum / n).toBeLessThan(1) + }) + + it('releases every pin it takes', () => { + /* A leaked pin permanently removes a frame from the clock sweep's reach. */ + const city = createCity() + run(city, 240) + let pinned = 0 + for (let i = 0; i < N_FRAMES; i++) pinned += city.framePinned[i] + expect(pinned).toBeLessThan(N_BACKENDS + 1) + }) +}) + +describe('the write-ahead rule', () => { + it('leaves the changed page dirty in memory after the commit returns', () => { + /* The claim the whole drawing rests on: a commit makes the WAL record + * durable, not the data page. If commits cleaned pages, the pool would + * run clean under write load and the checkpointer would have nothing to + * do — which is the opposite of why checkpoint tuning matters. */ + const city = createCity() + run(city, 90) + expect(city.commits).toBeGreaterThan(0) + + let sawDirty = 0 + let samples = 0 + run(city, 120, (c) => { + samples++ + if (c.dirtyRatio > 0) sawDirty++ + }) + expect(sawDirty).toBe(samples) + }) + + it('produces write-ahead log before anything is committed', () => { + const city = createCity(0xc0ffee) + let walAtFirstCommit = -1 + run(city, 60, (c) => { + if (walAtFirstCommit < 0 && c.commits > 0) walAtFirstCommit = c.walBytes + }) + expect(walAtFirstCommit).toBeGreaterThan(0) + }) + + it('parks a backend in commit_wait while its WAL is flushed', () => { + /* The wait has to be a state a reader can catch, not an instant + * transition. A commit that never waits is not a commit. */ + const city = createCity() + let seen = 0 + run(city, 120, (c) => { + for (let i = 0; i < N_BACKENDS; i++) if (c.backendState[i] === BE_COMMIT_WAIT) seen++ + }) + expect(seen).toBeGreaterThan(0) + }) + + it('floods the log with full-page images after a checkpoint begins', () => { + /* full_page_writes re-arms at every checkpoint, so the first change to + * each page afterwards carries the whole page into the log. It is why WAL + * volume — and replication lag with it — climbs right after a checkpoint + * starts, and it is the mechanism behind a whole class of "why did my + * replica fall behind on a schedule" questions. */ + const city = createCity() + const WINDOW = 4 + const ringLen = Math.round(WINDOW / STEP) + const ring = new Float64Array(ringLen) + let head = 0 + let filled = 0 + let t = 0 + let prevActive = false + let pending: { at: number; atEdge: number; windowAgo: number } | null = null + const events: { before: number; after: number }[] = [] + + run(city, 400, (c) => { + t += STEP + const windowAgo = ring[head] + ring[head] = c.walBytes + head = (head + 1) % ringLen + if (filled < ringLen) filled++ + + if (c.checkpointActive && !prevActive && filled >= ringLen && !pending) { + pending = { at: t, atEdge: c.walBytes, windowAgo } + } + prevActive = c.checkpointActive + + if (pending && t - pending.at >= WINDOW) { + events.push({ + before: pending.atEdge - pending.windowAgo, + after: c.walBytes - pending.atEdge, + }) + pending = null + } + }) + + expect(events.length).toBeGreaterThanOrEqual(2) + const mean = (xs: number[]) => xs.reduce((a, b) => a + b, 0) / xs.length + const before = mean(events.map((e) => e.before)) + const after = mean(events.map((e) => e.after)) + expect(after).toBeGreaterThan(before) + }) +}) + +describe('connections', () => { + it('forks a backend per connection, not per statement', () => { + /* Connections change on their own slow clock. If a slot went from unused + * to used on every statement, the drawing would be teaching a + * process-per-query database, which is the misconception this scene most + * needs not to reinforce. */ + const city = createCity() + let transitions = 0 + const prev = new Uint8Array(N_BACKENDS) + prev.set(city.backendConn) + run(city, 180, (c) => { + for (let i = 0; i < N_BACKENDS; i++) { + if (c.backendConn[i] !== prev[i]) transitions++ + prev[i] = c.backendConn[i] + } + }) + expect(city.commits).toBeGreaterThan(0) + expect(city.commits / Math.max(1, transitions)).toBeGreaterThan(1) + }) + + it('keeps at least one connection open and never exceeds the row', () => { + const city = createCity() + let minOpen = N_BACKENDS + let maxUsed = 0 + run(city, 240, (c) => { + let open = 0 + let used = 0 + for (let i = 0; i < N_BACKENDS; i++) { + if (c.backendConn[i] === CONN_OPEN) open++ + if (c.backendConn[i] !== CONN_NONE) used++ + } + if (open < minOpen) minOpen = open + if (used > maxUsed) maxUsed = used + }) + expect(minOpen).toBeGreaterThanOrEqual(1) + expect(maxUsed).toBeLessThanOrEqual(N_BACKENDS) + }) + + it('never shows work on a slot with no process behind it', () => { + const city = createCity() + let violations = 0 + run(city, 180, (c) => { + for (let i = 0; i < N_BACKENDS; i++) { + if (c.backendConn[i] === CONN_NONE && c.backendLoad[i] > 0.5) violations++ + } + }) + expect(violations).toBe(0) + }) +}) + +describe('replication', () => { + it('measures lag as the gap between what is sent and what is replayed', () => { + const city = createCity() + run(city, 120) + expect(city.replayedBytes).toBeLessThanOrEqual(city.sentBytes) + expect(city.sentBytes).toBeLessThanOrEqual(city.walBytes) + }) + + it('lets lag both build and drain rather than pinning at either end', () => { + /* A lag readout stuck at zero teaches that replication is free; one stuck + * at the maximum teaches that a replica can never catch up. */ + const city = createCity() + let min = Infinity + let max = -Infinity + run(city, 300, (c) => { + if (c.replayLag < min) min = c.replayLag + if (c.replayLag > max) max = c.replayLag + }) + expect(min).toBeLessThan(0.35) + expect(max).toBeGreaterThan(0.02) + expect(max).toBeLessThan(1) + }) +}) + +describe('the model as a machine', () => { + it('is deterministic for a given seed', () => { + const a = createCity(1234) + const b = createCity(1234) + run(a, 60) + run(b, 60) + expect(a.commits).toBe(b.commits) + expect(a.walBytes).toBe(b.walBytes) + expect(Array.from(a.frameState)).toEqual(Array.from(b.frameState)) + }) + + it('differs for a different seed', () => { + const a = createCity(1) + const b = createCity(2) + run(a, 60) + run(b, 60) + expect(Array.from(a.frameState)).not.toEqual(Array.from(b.frameState)) + }) + + it('never exhausts its fixed particle pool', () => { + /* The pool is preallocated so the animation loop allocates nothing. If it + * saturates, particles are silently dropped and the causality the scene + * is showing stops being complete. */ + const city = createCity() + let peak = 0 + run(city, 300, (c) => { + if (c.pCount > peak) peak = c.pCount + }) + expect(peak).toBeGreaterThan(0) + expect(peak).toBeLessThan(city.pAlive.length * 0.75) + }) + + it('holds a steady state rather than drifting to all-clean or all-dirty', () => { + const city = createCity() + run(city, 600) + let dirty = 0 + for (let i = 0; i < N_FRAMES; i++) if (city.frameState[i] === FRAME_DIRTY) dirty++ + expect(dirty).toBeGreaterThan(0) + expect(dirty).toBeLessThan(N_FRAMES) + }) +}) diff --git a/src/components/PostgresCity/sim.ts b/src/components/PostgresCity/sim.ts new file mode 100644 index 00000000..e563294a --- /dev/null +++ b/src/components/PostgresCity/sim.ts @@ -0,0 +1,766 @@ +/** + * A small, honest model of a PostgreSQL cluster. + * + * It is a model, not an emulator: no PostgreSQL code runs here and the rates + * are scaled so a person can watch them. What it does preserve is the order + * and the causality, because those are the whole lesson: + * + * - A backend that misses in the buffer pool reads the page from storage + * before it can use it, and if the frame it takes was dirty it writes that + * page out first — the clock sweep picks the victim, not a queue. + * - A write dirties a page in shared memory and produces a WAL record. The + * commit waits for that record to reach durable storage and for nothing + * else. The wait is a state the backend is visibly in. + * - The dirty data page is still in memory after the commit returns. It + * reaches storage later: at a checkpoint, through the background writer, + * or because somebody needed its frame. This is structural here — no path + * marks a frame clean at commit time. + * - Replication ships WAL. Nothing in this model sends a data page to the + * standby. + * + * Determinism is deliberate: one seeded generator, a fixed timestep, and no + * allocation after construction, so the same elapsed time always produces the + * same city and the animation loop never makes garbage. + */ + +import { + N_BACKENDS, + N_FRAMES, + N_TABLES, + N_WAL_SEGMENTS, + ROUTES, + forkRoute, + queryRoute, + resultRoute, + routeOf, +} from './plan' + +/* --- frame states --------------------------------------------------------*/ +export const FRAME_FREE = 0 +export const FRAME_CLEAN = 1 +export const FRAME_DIRTY = 2 + +/* --- backend states ------------------------------------------------------*/ +export const BE_IDLE = 0 +export const BE_ACTIVE = 1 +/** Waiting for its own WAL to be flushed. This is `commit_wait`, not work. */ +export const BE_COMMIT_WAIT = 2 +/** Waiting on storage: a page fault, or writing out a dirty victim. */ +export const BE_IO_WAIT = 3 + +/* --- connection lifecycle ------------------------------------------------*/ +/** No connection, so no process: PostgreSQL has nothing here at all. */ +export const CONN_NONE = 0 +/** The postmaster has been asked and is forking. */ +export const CONN_OPENING = 1 +export const CONN_OPEN = 2 + +/* --- particle kinds (drawing hints only) ---------------------------------*/ +export const PK_NORMAL = 0 +export const PK_HEAVY = 1 + +const MAX_PARTICLES = 320 +const MAX_TXNS = 48 + +/** Pages in the modelled working set. Larger than the pool, so misses are real. */ +const N_PAGES = N_TABLES * 44 +/** The hot set: what an OLTP workload actually touches, and what a + * correctly sized pool is expected to hold. */ +const HOT_PAGES = 30 +const HOT_SHARE = 0.95 +/** Model WAL bytes that fill one segment. Scaled: a real segment is 16 MiB. */ +const SEGMENT_BYTES = 900 +/** WAL a modelled write produces, before any full-page image. */ +const WAL_PER_WRITE = 5.5 +/** A full-page image: the first change to a page after a checkpoint. */ +const WAL_FULL_PAGE = 26 + +const CKPT_PERIOD = 21 +/** checkpoint_completion_target: spread the writes over most of the interval. */ +const CKPT_TARGET = 0.85 +const BGW_PERIOD = 0.34 +const VAC_PERIOD = 13 +const STREAM_PERIOD = 0.3 +/** What the standby's startup process can replay, in model WAL bytes/second. + * Set a little above the average write rate, so lag drains between bursts + * and builds during them rather than sitting at a constant. */ +const REPLAY_RATE = 105 +const ARRIVAL_RATE = 9 +/** How often the city considers opening or closing a connection. */ +const CONN_PERIOD = 3.5 + +/* --- transaction stages --------------------------------------------------*/ +const TX_FREE = 0 +const TX_ARRIVING = 1 +const TX_LOOKUP = 2 +const TX_IO = 3 +const TX_WAL = 4 +const TX_FLUSH = 5 +const TX_RETURN = 6 + +/* --- what a particle does when it lands ----------------------------------*/ +const ON_NOTHING = 0 +const ON_TX_BUFREQ = 1 +const ON_TX_IO_DONE = 2 +const ON_TX_WAL_FLUSH = 3 +const ON_TX_COMMIT = 4 +const ON_TX_DONE = 5 +const ON_WRITE_PAGE = 6 +const ON_PAGE_CLEAN = 7 +const ON_VACUUM = 8 +const ON_FORKED = 9 + +function xorshift32(seed: number): () => number { + let s = seed | 0 || 0x9e3779b9 + return () => { + s ^= s << 13 + s |= 0 + s ^= s >>> 17 + s ^= s << 5 + s |= 0 + return (s >>> 0) / 4294967296 + } +} + +export interface City { + /* Buffer pool. */ + frameState: Uint8Array + frameTag: Int32Array + frameUsage: Uint8Array + framePinned: Uint8Array + /** Seconds of "just touched" highlight left on the frame. */ + frameHot: Float32Array + sweepHand: number + + /* Backends. A slot with no connection has no process behind it at all. */ + backendConn: Uint8Array + backendState: Uint8Array + backendLoad: Float32Array + + /* Particles, structure-of-arrays so the loop allocates nothing. */ + pRoute: Int8Array + pT: Float32Array + pRate: Float32Array + pKind: Uint8Array + pAlive: Uint8Array + pCount: number + + /* Districts under load, 0..1 — a pulse a reader can actually see. */ + pulseCheckpoint: number + pulseWal: number + pulseVacuum: number + pulseFork: number + + /* WAL. */ + walBytes: number + segmentFill: number + segmentHead: number + /** Fill level of each visible segment silo, 0..1. */ + segments: Float32Array + + /* Replication, in model WAL bytes. Lag is the gap between what the + * walsender has shipped and what the standby has replayed. */ + sentBytes: number + replayedBytes: number + /** That gap, normalised against one segment, for the readout. */ + replayLag: number + + /* Maintenance. */ + checkpointActive: boolean + checkpointProgress: number + vacuumTable: number + + /* Counters. */ + blksHit: number + blksRead: number + commits: number + + /* Derived each step, for the readouts. */ + hitRatio: number + dirtyRatio: number + + t: number + step(dt: number): void +} + +export function createCity(seed = 0x5eed1e): City { + const rng = xorshift32(seed) + + const frameState = new Uint8Array(N_FRAMES) + const frameTag = new Int32Array(N_FRAMES).fill(-1) + const frameUsage = new Uint8Array(N_FRAMES) + const framePinned = new Uint8Array(N_FRAMES) + const frameHot = new Float32Array(N_FRAMES) + /* Bumped every time a frame is dirtied. A write-back only cleans the frame + * if the stamp has not moved since the write started — PostgreSQL's + * BM_JUST_DIRTIED, without which a page changed during its own write-out + * would be silently lost. */ + const frameStamp = new Uint16Array(N_FRAMES) + + const backendConn = new Uint8Array(N_BACKENDS) + const backendState = new Uint8Array(N_BACKENDS) + const backendLoad = new Float32Array(N_BACKENDS) + + const pRoute = new Int8Array(MAX_PARTICLES) + const pT = new Float32Array(MAX_PARTICLES) + const pRate = new Float32Array(MAX_PARTICLES) + const pKind = new Uint8Array(MAX_PARTICLES) + const pOnArrive = new Uint8Array(MAX_PARTICLES) + const pOwner = new Int16Array(MAX_PARTICLES) + const pArg = new Int16Array(MAX_PARTICLES) + const pStamp = new Uint16Array(MAX_PARTICLES) + const pAlive = new Uint8Array(MAX_PARTICLES) + const freeP = new Int16Array(MAX_PARTICLES) + let freePTop = MAX_PARTICLES + for (let i = 0; i < MAX_PARTICLES; i++) freeP[i] = MAX_PARTICLES - 1 - i + + const txState = new Uint8Array(MAX_TXNS) + const txWrite = new Uint8Array(MAX_TXNS) + const txBackend = new Int8Array(MAX_TXNS) + const txFrame = new Int16Array(MAX_TXNS) + const txPage = new Int32Array(MAX_TXNS) + /** Blocks this statement still has to touch. Real statements touch many. */ + const txBlocks = new Uint8Array(MAX_TXNS) + const freeT = new Int16Array(MAX_TXNS) + let freeTTop = MAX_TXNS + for (let i = 0; i < MAX_TXNS; i++) freeT[i] = MAX_TXNS - 1 - i + + /** Pages already given a full-page image since the last checkpoint. */ + const fpiDone = new Uint8Array(N_PAGES) + + /** Frames the checkpointer still has to write in this checkpoint. */ + const ckptQueue = new Int16Array(N_FRAMES) + let ckptCount = 0 + let ckptCursor = 0 + let ckptRelease = 0 + + /* Route lengths in world units, so particle speed is screen-independent. */ + const routeLen = new Float32Array(ROUTES.length) + for (let r = 0; r < ROUTES.length; r++) { + const pts = ROUTES[r].pts + let len = 0 + for (let i = 1; i < pts.length; i++) { + const dx = pts[i][0] - pts[i - 1][0] + const dy = pts[i][1] - pts[i - 1][1] + const dz = pts[i][2] - pts[i - 1][2] + len += Math.sqrt(dx * dx + dy * dy + dz * dz) + } + routeLen[r] = len || 1 + } + + const city: City = { + frameState, + frameTag, + frameUsage, + framePinned, + frameHot, + sweepHand: 0, + backendConn, + backendState, + backendLoad, + pRoute, + pT, + pRate, + pKind, + pAlive, + pCount: 0, + pulseCheckpoint: 0, + pulseWal: 0, + pulseVacuum: 0, + pulseFork: 0, + walBytes: 0, + segmentFill: 0, + segmentHead: 0, + segments: new Float32Array(N_WAL_SEGMENTS), + sentBytes: 0, + replayedBytes: 0, + replayLag: 0, + checkpointActive: false, + checkpointProgress: 0, + vacuumTable: -1, + blksHit: 40, + blksRead: 3, + commits: 0, + hitRatio: 0.93, + dirtyRatio: 0, + t: 0, + step, + } + + /* ---- particles ------------------------------------------------------- */ + + function emit( + r: number, + onArrive: number, + owner: number, + arg: number, + kind: number, + stamp = 0, + ): void { + if (freePTop === 0) return + const i = freeP[--freePTop] + pRoute[i] = r + pT[i] = 0 + pRate[i] = ROUTES[r].speed / routeLen[r] + pKind[i] = kind + pOnArrive[i] = onArrive + pOwner[i] = owner + pArg[i] = arg + pStamp[i] = stamp + pAlive[i] = 1 + city.pCount++ + } + + function release(i: number): void { + pAlive[i] = 0 + freeP[freePTop++] = i + city.pCount-- + } + + /* ---- buffer pool ----------------------------------------------------- */ + + function findResident(tag: number): number { + for (let i = 0; i < N_FRAMES; i++) if (frameTag[i] === tag) return i + return -1 + } + + function markDirty(f: number): void { + frameState[f] = FRAME_DIRTY + frameStamp[f] = (frameStamp[f] + 1) & 0xffff + } + + /** + * PostgreSQL's clock sweep. Each pass decrements a frame's usage count and + * only an unpinned frame at zero can be taken. The count is capped at 5, + * which is why a page read repeatedly survives several passes. + */ + function clockSweep(): number { + for (let guard = 0; guard < N_FRAMES * 6; guard++) { + const i = city.sweepHand + city.sweepHand = (i + 1) % N_FRAMES + if (framePinned[i]) continue + if (frameState[i] === FRAME_FREE) return i + if (frameUsage[i] > 0) { + frameUsage[i]-- + continue + } + return i + } + return city.sweepHand + } + + /* ---- transactions ---------------------------------------------------- */ + + /* A hot set plus a long cold tail. A uniform pick would make the hit ratio + * a function of pool size alone, and would put a number on screen that no + * healthy OLTP database has ever reported. */ + function pickPage(): number { + return rng() < HOT_SHARE + ? (rng() * HOT_PAGES) | 0 + : Math.min(N_PAGES - 1, (rng() * N_PAGES) | 0) + } + + function idleBackend(): number { + const start = (rng() * N_BACKENDS) | 0 + for (let k = 0; k < N_BACKENDS; k++) { + const i = (start + k) % N_BACKENDS + if (backendConn[i] === CONN_OPEN && backendState[i] === BE_IDLE) return i + } + return -1 + } + + /** + * Open a connection. The postmaster forks one backend for it, and that + * backend then serves every statement on that connection until it closes. + * This happens per connection — never per statement, which is the thing + * everyone assumes and the reason it is drawn separately here. + */ + function openConnection(): void { + let slot = -1 + for (let i = 0; i < N_BACKENDS; i++) { + if (backendConn[i] === CONN_NONE) { + slot = i + break + } + } + if (slot < 0) return + backendConn[slot] = CONN_OPENING + city.pulseFork = 1 + emit(routeOf('conn'), ON_NOTHING, -1, 0, PK_NORMAL) + emit(forkRoute(slot), ON_FORKED, -1, slot, PK_HEAVY) + } + + function closeConnection(): void { + let open = 0 + for (let i = 0; i < N_BACKENDS; i++) if (backendConn[i] === CONN_OPEN) open++ + if (open <= 4) return + for (let i = N_BACKENDS - 1; i >= 0; i--) { + if (backendConn[i] === CONN_OPEN && backendState[i] === BE_IDLE) { + backendConn[i] = CONN_NONE + backendLoad[i] = 0 + return + } + } + } + + function beginTxn(): void { + if (freeTTop === 0) return + const be = idleBackend() + if (be < 0) return + const tx = freeT[--freeTTop] + txState[tx] = TX_ARRIVING + txWrite[tx] = rng() < 0.34 ? 1 : 0 + txBackend[tx] = be + txFrame[tx] = -1 + txPage[tx] = pickPage() + /* One statement touches several blocks — an index descent and its heap + * fetches are already three or four. Drawing one buffer access per + * statement would understate the pool's job by about that much. */ + txBlocks[tx] = 3 + ((rng() * 5) | 0) + backendState[be] = BE_ACTIVE + emit(queryRoute(be), ON_TX_BUFREQ, tx, 0, PK_NORMAL) + } + + function endTxn(tx: number): void { + const be = txBackend[tx] + if (be >= 0) backendState[be] = BE_IDLE + const f = txFrame[tx] + if (f >= 0 && framePinned[f] > 0) framePinned[f]-- + txState[tx] = TX_FREE + txFrame[tx] = -1 + freeT[freeTTop++] = tx + } + + /** The statement has reached its backend; ask shared memory for the page. */ + function requestBuffer(tx: number): void { + txState[tx] = TX_LOOKUP + emit(routeOf('bufReq'), ON_TX_IO_DONE, tx, 0, PK_NORMAL) + } + + function lookup(tx: number): void { + const tag = txPage[tx] + let f = findResident(tag) + if (f >= 0) { + city.blksHit++ + if (frameUsage[f] < 5) frameUsage[f]++ + frameHot[f] = 0.55 + framePinned[f]++ + txFrame[tx] = f + usePage(tx) + return + } + + city.blksRead++ + f = clockSweep() + /* A dirty victim is written by the backend that needed the frame. Under + * the write-ahead rule its WAL is flushed first, so this is never cheap — + * it is the price a too-small pool actually charges. */ + if (frameState[f] === FRAME_DIRTY) emit(routeOf('pageWrite'), ON_NOTHING, -1, f, PK_HEAVY) + frameTag[f] = tag + frameState[f] = FRAME_CLEAN + frameUsage[f] = 1 + framePinned[f]++ + txFrame[tx] = f + txState[tx] = TX_IO + backendState[txBackend[tx]] = BE_IO_WAIT + emit(routeOf('pageRead'), ON_TX_IO_DONE, tx, f, PK_NORMAL) + } + + /** The page is in a frame and pinned; do what the statement asked for. */ + function usePage(tx: number): void { + const f = txFrame[tx] + frameHot[f] = 0.55 + backendState[txBackend[tx]] = BE_ACTIVE + + if (txWrite[tx]) { + /* Changed in memory now. Nothing below marks it clean: it stays dirty + * until a checkpoint, the background writer, or an eviction writes it. */ + markDirty(f) + + const page = txPage[tx] + let bytes = WAL_PER_WRITE + if (!fpiDone[page]) { + /* full_page_writes: the first change to a page after a checkpoint + * carries the whole page into the WAL. It is why WAL floods just + * after a checkpoint begins, and why replay falls behind there. */ + bytes += WAL_FULL_PAGE + fpiDone[page] = 1 + } + city.walBytes += bytes + city.pulseWal = Math.min(1, city.pulseWal + bytes / 90) + /* One WAL record per changed block, staged into wal_buffers. None of + * them is flushed yet — the commit below is what waits. */ + emit(routeOf('walIns'), ON_NOTHING, -1, 0, bytes > WAL_PER_WRITE ? PK_HEAVY : PK_NORMAL) + } + + /* Done with this block: unpin it and move to the next one. */ + if (framePinned[f] > 0) framePinned[f]-- + txFrame[tx] = -1 + txBlocks[tx]-- + + if (txBlocks[tx] > 0) { + txPage[tx] = pickPage() + txState[tx] = TX_LOOKUP + emit(routeOf('bufReq'), ON_TX_IO_DONE, tx, 0, PK_NORMAL) + return + } + + if (!txWrite[tx]) { + txState[tx] = TX_RETURN + emit(resultRoute(txBackend[tx]), ON_TX_DONE, tx, 0, PK_NORMAL) + return + } + + /* The commit record. Everything this transaction wrote becomes durable + * when this one reaches disk, and not before. */ + city.walBytes += WAL_PER_WRITE + txState[tx] = TX_WAL + emit(routeOf('walIns'), ON_TX_WAL_FLUSH, tx, 0, PK_HEAVY) + } + + /* ---- arrivals -------------------------------------------------------- */ + + function arrive(what: number, tx: number, arg: number, stamp: number): void { + switch (what) { + case ON_TX_BUFREQ: + if (txState[tx] === TX_ARRIVING) requestBuffer(tx) + break + + case ON_TX_IO_DONE: + if (txState[tx] === TX_LOOKUP) lookup(tx) + else if (txState[tx] === TX_IO) usePage(tx) + break + + case ON_TX_WAL_FLUSH: + if (txState[tx] === TX_WAL) { + txState[tx] = TX_FLUSH + /* Asleep in commit_wait. The backend is not doing work and is not + * holding up anything but itself and its client. */ + backendState[txBackend[tx]] = BE_COMMIT_WAIT + emit(routeOf('walFlush'), ON_TX_COMMIT, tx, 0, PK_HEAVY) + } + break + + case ON_TX_COMMIT: + if (txState[tx] === TX_FLUSH) { + city.commits++ + /* Durable. The WAL record is on disk; the data page it describes is + * still dirty in shared memory, and that is correct. */ + emit(routeOf('walFsync'), ON_NOTHING, -1, 0, PK_NORMAL) + txState[tx] = TX_RETURN + backendState[txBackend[tx]] = BE_ACTIVE + emit(resultRoute(txBackend[tx]), ON_TX_DONE, tx, 0, PK_NORMAL) + } + break + + case ON_TX_DONE: + if (txState[tx] !== TX_FREE) endTxn(tx) + break + + case ON_WRITE_PAGE: + /* The sweep reached the pool; now the page itself travels to storage, + * carrying the stamp it had when the write was scheduled. */ + emit(routeOf('pageWrite'), ON_PAGE_CLEAN, -1, arg, PK_NORMAL, frameStamp[arg]) + break + + case ON_PAGE_CLEAN: + if (frameState[arg] === FRAME_DIRTY && frameStamp[arg] === stamp) { + frameState[arg] = FRAME_CLEAN + } + break + + case ON_VACUUM: + city.vacuumTable = arg + city.pulseVacuum = 1 + break + + case ON_FORKED: + /* The forked backend exists now and will serve this connection until + * it closes. The postmaster takes no further part. */ + backendConn[arg] = CONN_OPEN + backendState[arg] = BE_IDLE + break + + default: + break + } + } + + /* ---- background activity --------------------------------------------- */ + + function beginCheckpoint(): void { + ckptCount = 0 + for (let i = 0; i < N_FRAMES; i++) if (frameState[i] === FRAME_DIRTY) ckptQueue[ckptCount++] = i + ckptCursor = 0 + ckptRelease = 0 + city.checkpointActive = ckptCount > 0 + city.checkpointProgress = 0 + city.pulseCheckpoint = 1 + /* A checkpoint re-arms full_page_writes: the next change to any page + * carries a full-page image again. */ + fpiDone.fill(0) + } + + function driveCheckpoint(dt: number): void { + if (!city.checkpointActive || ckptCount === 0) return + /* Spread the writes across checkpoint_completion_target of the interval + * rather than dumping them: that smoothing is the point of the setting. */ + const spread = CKPT_PERIOD * CKPT_TARGET + ckptRelease += (ckptCount / spread) * dt + while (ckptRelease >= 1 && ckptCursor < ckptCount) { + ckptRelease -= 1 + const f = ckptQueue[ckptCursor++] + if (frameState[f] === FRAME_DIRTY) emit(routeOf('ckptSweep'), ON_WRITE_PAGE, -1, f, PK_NORMAL) + } + city.checkpointProgress = ckptCursor / ckptCount + if (ckptCursor >= ckptCount) city.checkpointActive = false + } + + /** + * The background writer trickles dirty pages out ahead of the checkpointer, + * scanning from the clock-sweep position so it writes the frames most likely + * to be reused next. + */ + function runBgwriter(): void { + let written = 0 + for (let k = 0; k < 16 && written < 3; k++) { + const f = (city.sweepHand + k) % N_FRAMES + if (frameState[f] === FRAME_DIRTY && !framePinned[f]) { + emit(routeOf('bgwSweep'), ON_WRITE_PAGE, -1, f, PK_NORMAL) + written++ + } + } + } + + /* ---- the step -------------------------------------------------------- */ + + let txAccum = 0 + let bgwAccum = 0 + let vacAccum = 0 + let streamAccum = 0 + let ckptAccum = 0 + let connAccum = 0 + let lastWalBytes = 0 + + function step(dt: number): void { + city.t += dt + + /* Connections come and go on their own clock, far slower than statements + * do. Watch the postmaster: it fires here and nowhere else. */ + connAccum += dt + if (connAccum >= CONN_PERIOD) { + connAccum = 0 + if (rng() < 0.62) openConnection() + else closeConnection() + } + + /* Arrivals. The rate is scaled for watching, not benchmarked. */ + txAccum += ARRIVAL_RATE * dt + while (txAccum >= 1) { + txAccum -= 1 + beginTxn() + } + + /* Particles. Read the landing payload before releasing the slot, because + * `arrive` can emit and immediately reuse it. */ + for (let i = 0; i < MAX_PARTICLES; i++) { + if (!pAlive[i]) continue + pT[i] += pRate[i] * dt + if (pT[i] >= 1) { + const what = pOnArrive[i] + const owner = pOwner[i] + const arg = pArg[i] + const stamp = pStamp[i] + release(i) + arrive(what, owner, arg, stamp) + } + } + + ckptAccum += dt + if (ckptAccum >= CKPT_PERIOD) { + ckptAccum = 0 + beginCheckpoint() + } + driveCheckpoint(dt) + + bgwAccum += dt + if (bgwAccum >= BGW_PERIOD) { + bgwAccum = 0 + runBgwriter() + } + + vacAccum += dt + if (vacAccum >= VAC_PERIOD) { + vacAccum = 0 + emit(routeOf('vacGo'), ON_VACUUM, -1, (rng() * N_TABLES) | 0, PK_NORMAL) + } + + /* WAL segments fill in order; a finished one is shipped to the archive. */ + const produced = city.walBytes - lastWalBytes + lastWalBytes = city.walBytes + city.segmentFill += produced + while (city.segmentFill >= SEGMENT_BYTES) { + city.segmentFill -= SEGMENT_BYTES + city.segments[city.segmentHead % N_WAL_SEGMENTS] = 1 + city.segmentHead++ + emit(routeOf('archiveShip'), ON_NOTHING, -1, 0, PK_HEAVY) + } + city.segments[city.segmentHead % N_WAL_SEGMENTS] = city.segmentFill / SEGMENT_BYTES + + /* Streaming replication. The walsender ships WAL as it is generated; the + * standby's startup process replays it at a finite rate. Lag is the gap + * between the two — which is why it climbs when full-page images flood + * the log after a checkpoint, and drains when writes go quiet. Nothing + * here sends a data page: a replica rebuilds its own pages from the log. */ + city.sentBytes = city.walBytes + city.replayedBytes = Math.min(city.sentBytes, city.replayedBytes + REPLAY_RATE * dt) + const lagBytes = city.sentBytes - city.replayedBytes + city.replayLag = Math.min(1, lagBytes / SEGMENT_BYTES) + + streamAccum += dt + if (streamAccum >= STREAM_PERIOD) { + streamAccum = 0 + if (produced > 0) emit(routeOf('stream'), ON_NOTHING, -1, 0, PK_NORMAL) + if (lagBytes > 1) emit(routeOf('replay'), ON_NOTHING, -1, 0, PK_NORMAL) + } + + /* Decay: pulses, frame highlights, backend afterglow. */ + const decay = Math.min(1, dt * 1.8) + city.pulseCheckpoint -= city.pulseCheckpoint * decay + city.pulseWal -= city.pulseWal * decay * 1.4 + city.pulseVacuum -= city.pulseVacuum * decay * 0.5 + city.pulseFork -= city.pulseFork * decay * 2 + for (let i = 0; i < N_FRAMES; i++) { + if (frameHot[i] > 0) frameHot[i] = Math.max(0, frameHot[i] - dt) + } + for (let i = 0; i < N_BACKENDS; i++) { + const target = backendState[i] === BE_IDLE ? 0 : 1 + backendLoad[i] += (target - backendLoad[i]) * Math.min(1, dt * 7) + } + + /* Readouts. `blks_hit / (blks_hit + blks_read)` is PostgreSQL's own + * formula; the counters decay so the ratio tracks the recent workload + * instead of freezing at a lifetime average. */ + const halfLife = Math.pow(0.5, dt / 6) + city.blksHit *= halfLife + city.blksRead *= halfLife + const reads = city.blksHit + city.blksRead + city.hitRatio = reads > 0 ? city.blksHit / reads : 1 + + let dirty = 0 + for (let i = 0; i < N_FRAMES; i++) if (frameState[i] === FRAME_DIRTY) dirty++ + city.dirtyRatio = dirty / N_FRAMES + } + + /* A database a reader arrives at has been up for a while: some connections + * are already established, and the pool is already populated. Start there, + * then run the model forward so the first painted frame is a working + * cluster rather than a cold start. */ + for (let i = 0; i < 8; i++) { + backendConn[i] = CONN_OPEN + backendState[i] = BE_IDLE + } + for (let i = 0; i < 1800; i++) step(1 / 60) + + return city +} diff --git a/src/components/PostgresCity/styles.module.css b/src/components/PostgresCity/styles.module.css new file mode 100644 index 00000000..9d7ad623 --- /dev/null +++ b/src/components/PostgresCity/styles.module.css @@ -0,0 +1,333 @@ +/* + * The scene owns its own light: it draws its own sky, so it sits on either + * page background without a frame fighting it. Everything outside the canvas + * is the page's own monospace voice. + */ + +.figure { + margin: 0; + width: 100%; +} + +.title { + font-family: var(--ifm-font-family-base); + font-size: 0.95rem; + font-weight: 500; + letter-spacing: 0.02em; + margin: 0 0 10px 0; + text-align: center; +} + +.stage { + position: relative; + width: 100%; + /* 16:9 down to a taller crop on a phone, where a wide band would leave the + districts too small to name. */ + aspect-ratio: 16 / 9; + min-height: 240px; + border-radius: 6px; + overflow: hidden; + border: 1px solid #d4d9de; + background: #eef2f5; +} + +html[data-theme='dark'] .stage { + border-color: #23293a; + background: #04060c; +} + +.canvas { + display: block; + width: 100%; + height: 100%; +} + +/* --- district hit targets ------------------------------------------------ */ + +.hits { + position: absolute; + inset: 0; + pointer-events: none; +} + +.hit { + position: absolute; + width: 62px; + height: 44px; + transform: translate(-50%, -50%); + padding: 0; + border: 1px solid transparent; + border-radius: 4px; + background: transparent; + cursor: pointer; + pointer-events: auto; + transition: border-color 120ms ease, background-color 120ms ease; +} + +.hit:hover, +.hitOn { + border-color: rgba(255, 97, 18, 0.55); + background: rgba(255, 97, 18, 0.07); +} + +.hit:focus-visible { + outline: 2px solid #ff6112; + outline-offset: 2px; +} + +.srOnly { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* --- motion control ------------------------------------------------------ */ + +.motionToggle { + position: absolute; + right: 8px; + top: 8px; + z-index: 2; + font-family: var(--ifm-font-family-base); + font-size: 11px; + line-height: 1; + padding: 5px 8px; + border-radius: 3px; + border: 1px solid rgba(120, 130, 145, 0.4); + background: rgba(255, 255, 255, 0.72); + color: #2b3440; + cursor: pointer; +} + +html[data-theme='dark'] .motionToggle { + background: rgba(6, 10, 18, 0.7); + border-color: rgba(120, 145, 190, 0.35); + color: #cfdcf0; +} + +.motionToggle:focus-visible { + outline: 2px solid #ff6112; + outline-offset: 2px; +} + +/* --- the explanation slot ------------------------------------------------ */ + +/* + * Fixed height. The text under the scene changes on hover, and a box that + * grows and shrinks would shove the rest of the page around while somebody is + * reading it. + */ +.blurbSlot { + position: absolute; + left: 0; + right: 0; + bottom: 0; + min-height: 62px; + display: flex; + align-items: flex-end; + padding: 10px 14px; + background: linear-gradient(to top, rgba(238, 242, 245, 0.9) 30%, rgba(238, 242, 245, 0)); + pointer-events: none; +} + +html[data-theme='dark'] .blurbSlot { + background: linear-gradient(to top, rgba(4, 6, 12, 0.88) 30%, rgba(4, 6, 12, 0)); +} + +.blurb, +.blurbHint { + font-family: var(--ifm-font-family-base); + font-size: 12px; + line-height: 1.45; + margin: 0; + max-width: 78ch; +} + +.blurb strong { + letter-spacing: 0.06em; +} + +.blurbSub { + opacity: 0.62; +} + +.blurbHint { + opacity: 0.6; +} + +/* --- legend -------------------------------------------------------------- */ + +.legend { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 6px 16px; + margin: 12px 0 0 0; + font-family: var(--ifm-font-family-base); + font-size: 11px; + line-height: 1.3; + opacity: 0.85; +} + +.key { + display: inline-flex; + align-items: center; + white-space: nowrap; +} + +.key::before { + content: ''; + width: 9px; + height: 9px; + border-radius: 2px; + margin-right: 6px; + background: currentColor; +} + +/* Day values, matching the palette the canvas paints with in light mode. */ +.key[data-c='wal']::before { + background: #b8720a; +} +.key[data-c='dirty']::before { + background: #e02b46; +} +.key[data-c='clean']::before { + background: #1d5fcb; +} +.key[data-c='checkpoint']::before { + background: #c42d92; +} +.key[data-c='bgwriter']::before { + background: #0e8f8c; +} +.key[data-c='vacuum']::before { + background: #8b2bc0; +} +.key[data-c='replication']::before { + background: #e2690d; +} +.key[data-c='storage']::before { + background: #17954f; +} + +/* Night values. Same meanings, separately tuned — not the day set dimmed. */ +html[data-theme='dark'] .key[data-c='wal']::before { + background: #ffb03a; +} +html[data-theme='dark'] .key[data-c='dirty']::before { + background: #ff4d6d; +} +html[data-theme='dark'] .key[data-c='clean']::before { + background: #3fa7ff; +} +html[data-theme='dark'] .key[data-c='checkpoint']::before { + background: #ff7ac6; +} +html[data-theme='dark'] .key[data-c='bgwriter']::before { + background: #4fe3c1; +} +html[data-theme='dark'] .key[data-c='vacuum']::before { + background: #b57bff; +} +html[data-theme='dark'] .key[data-c='replication']::before { + background: #ff9c1c; +} +html[data-theme='dark'] .key[data-c='storage']::before { + background: #55d6a0; +} + +/* --- disclosure ---------------------------------------------------------- */ + +/* + * This paragraph is content, not decoration. No breakpoint below may hide it, + * shrink it below the legend, or move it out of the reader's path: the claim + * above it is only honest while this is next to it. + */ +.disclosure { + font-family: var(--ifm-font-family-base); + font-size: 11.5px; + line-height: 1.5; + margin: 10px auto 0 auto; + max-width: 86ch; + text-align: center; + opacity: 0.72; +} + +.disclosure a { + text-decoration: underline; +} + +/* --- text alternative ---------------------------------------------------- */ + +.textAlt { + font-family: var(--ifm-font-family-base); + font-size: 12px; + line-height: 1.5; + margin: 10px auto 0 auto; + max-width: 86ch; + text-align: left; +} + +.textAlt summary { + cursor: pointer; + opacity: 0.7; + text-align: center; +} + +.textAlt ul { + margin: 10px 0; + padding-left: 1.2em; +} + +.textAlt li { + margin-bottom: 6px; +} + +/* --- responsive ---------------------------------------------------------- */ + +@media (max-width: 996px) { + .stage { + aspect-ratio: 3 / 2; + } + + .hit { + width: 54px; + height: 40px; + } +} + +@media (max-width: 620px) { + /* The scene is a wide band, so a tall box just adds empty sky. Keep it near + the scene's own proportions and let the caption do the vertical work. */ + .stage { + aspect-ratio: 4 / 3; + min-height: 260px; + } + + .hit { + width: 48px; + height: 38px; + } + + .blurbSlot { + min-height: 74px; + padding: 8px 10px; + } + + .legend { + gap: 4px 12px; + font-size: 10px; + } +} + +@media (prefers-reduced-motion: reduce) { + .hit { + transition: none; + } +} diff --git a/src/components/signupForm/signupForm.tsx b/src/components/signupForm/signupForm.tsx index ad8055a4..aa0fcfed 100644 --- a/src/components/signupForm/signupForm.tsx +++ b/src/components/signupForm/signupForm.tsx @@ -71,7 +71,7 @@ export default function SignupForm() { <form onSubmit={onSubmit} className={styles.container}> <h3 className={styles.formTitle}>Get early access</h3> <p className={styles.formDescription}> - Currently in preview. Please use your work email address + Currently in preview. Please use your work email address. </p> <div className={styles.inputWrapper}> <input diff --git a/src/config/authors.ts b/src/config/authors.ts index 1b63d0da..09a62f9d 100644 --- a/src/config/authors.ts +++ b/src/config/authors.ts @@ -22,7 +22,7 @@ export const anatoly = { avatarUrl: '/assets/author/anatoly.jpg', name: 'Anatoly Stansler', gitlabUrl: 'https://gitlab.com/anatolystansler', - githubUrl: 'https://github.com/anatolystansler', + githubUrl: 'https://github.com/astansler', linkedinUrl: 'https://www.linkedin.com/in/anatoly-stansler-37265514a', } @@ -44,9 +44,9 @@ export const bogdan = { } export const denis = { - avatarUrl: '/assets/author/denis.jpeg', + avatarUrl: '/assets/author/denis.png', name: 'Denis Morozov', - role: 'Lead Engineer at', + role: 'Staff Engineer at', gitlabUrl: 'https://gitlab.com/Sarumyan9999' } @@ -60,11 +60,11 @@ export const tanya = { export const dmitry = { avatarUrl: '/assets/author/dmitry.jpeg', name: 'Dmitry Fomin', - role: 'Sr. Postgres engineer at', + role: 'Sr. Postgres Engineer at', } export const dementii = { avatarUrl: '/assets/author/dementii.png', name: 'Dementii Priadko', - role: 'Postgres engineer at', + role: 'Postgres Engineer at', } \ No newline at end of file diff --git a/src/css/custom.css b/src/css/custom.css index c12a6292..b6ffd117 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -9,6 +9,13 @@ @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono+Variable:ital,wght@0,100..800;1,100..800&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap"); +/* Prevent mobile browsers from inflating font sizes in code blocks */ +html { + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + text-size-adjust: 100%; +} + /* You can override the default Infima variables here. */ :root { --ifm-color-primary: #ff6111; @@ -18,16 +25,19 @@ --ifm-color-primary-light: #ff732c; --ifm-color-primary-lighter: #ff7c3a; --ifm-color-primary-lightest: #ff9763; - --ifm-code-font-size: 90%; + --ifm-code-font-size: 14px; --ifm-font-family-base: "JetBrains Mono Variable", "JetBrains Mono", "Fira Code", "Source Code Pro", Consolas, monospace; --ifm-font-size-base: 14px; --ifm-pre-line-height: 1.3; } -/* Code blocks: 14px font, normal line height */ +/* Code blocks: 14px font everywhere including mobile */ pre, pre code, .prism-code { font-size: 14px !important; line-height: 1.5 !important; + -webkit-text-size-adjust: 100% !important; + text-size-adjust: 100% !important; + max-width: 100%; } /* ASCII diagrams: no line gaps for proper character alignment */ @@ -344,7 +354,7 @@ html[data-theme="dark"] .blog-sec { /* Blog post titles - same size as body, just bold */ header h1 { - font-size: 14px !important; + font-size: 22px !important; font-weight: 700 !important; } @@ -354,10 +364,32 @@ header h2 { } article h2 { - font-size: 14px; + font-size: 18px; font-weight: 700; } +article h3 { + font-size: 16px; + font-weight: 700; +} + +article h4 { + font-size: 14px; + font-weight: 600; +} + +.teleport-logo-dark { + display: none; +} + +[data-theme='dark'] .teleport-logo-light { + display: none; +} + +[data-theme='dark'] .teleport-logo-dark { + display: inline; +} + .table-of-contents { font-size: 14px; } @@ -1267,6 +1299,26 @@ html body > div:first-child { font-size: 1rem !important; line-height: 1.5 !important; } + + /* Code blocks: enforce 14px on mobile — cover all Docusaurus/Prism selectors */ + pre, + pre code, + code, + .prism-code, + .prism-code *, + article pre, + article pre code, + article .prism-code, + article .prism-code *, + [class*="codeBlock"], + [class*="codeBlock"] *, + [class*="codeBlockContent"], + [class*="codeBlockContent"] *, + [class*="language-"], + [class*="language-"] * { + font-size: 14px !important; + line-height: 1.5 !important; + } /* Assistant Widget mobile fixes */ [class*="AssistantWidget_container"] { diff --git a/src/data/authors.ts b/src/data/authors.ts index aec87fc4..6c7bbdf2 100644 --- a/src/data/authors.ts +++ b/src/data/authors.ts @@ -32,7 +32,7 @@ const authors: { [key: string]: Author } = { avatarUrl: '/assets/author/anatoly.jpg', name: 'Anatoly Stansler', gitlabUrl: 'https://gitlab.com/anatolystansler', - githubUrl: 'https://github.com/anatolystansler', + githubUrl: 'https://github.com/astansler', linkedinUrl: 'https://www.linkedin.com/in/anatoly-stansler-37265514a', }, artyom: { diff --git a/src/data/blog.js b/src/data/blog.js index f446d0c8..782df3b7 100644 --- a/src/data/blog.js +++ b/src/data/blog.js @@ -1,10 +1,17 @@ // Duplicates data from the blog. const blog = [ + { + link: 'blog/20260408-dblab-engine-4-1-released', + date: '2026-04-08 00:00:00', + title: 'DBLab 4.1: protection leases, Teleport, Prometheus, and more', + description: 'DBLab Engine 4.1 brings protection leases for safe clone management, Teleport integration, RDS/Aurora-safe refresh, Prometheus metrics, ARM64 support, and database rename.', + image: '/assets/thumbnails/dblab-4.1-blog.png', + }, { link: 'blog/20241003-how-does-planning-time-depend-on-number-of-partitions', date: '2024-10-03 21:45:12', title: 'AI-assisted benchmark: number of partitions vs. planning time', - description: 'How does planning time depends on the number of partitions? Let\'s explore with the PostgresAI assistant', + description: 'How does planning time depend on the number of partitions? Let\'s explore with the PostgresAI assistant', image: '/assets/thumbnails/ai_db_experiment_plannting_time_vs_partition_count.jpg', }, { diff --git a/src/data/careers.js b/src/data/careers.js deleted file mode 100644 index 5ce20d5a..00000000 --- a/src/data/careers.js +++ /dev/null @@ -1,40 +0,0 @@ -const careers = [ - { - title: 'Senior Database Engineer | Postgres', - descriptions: [ - '3+ years of experience running PostgreSQL in large production environments', - 'Strong skills in performance optimization of large and heavily loaded databases (>1TiB, >10k TPS)', - 'Readiness to dive deep into PostgreSQL internals', - ], - link: '/careers/dba', - }, - { - title: 'Senior Full Stack Developer | React | Go', - descriptions: [ - '3+ years experience in client-side development using React', - 'Strong HTML, CSS, JavaScript skills', - 'Readiness to work with backend code', - ], - link: '/careers/fullstack', - }, - { - title: 'Senior Software Engineer | Go', - descriptions: [ - '3+ years experience developing server applications using Go', - 'Deep understanding of HTTP protocol, data structures, JSON', - 'REST API development experience', - ], - link: '/careers/godeveloper', - }, - { - title: 'Senior Frontend Engineer | React', - descriptions: [ - '3+ years experience in client-side development using React', - 'Excellent HTML, CSS, JavaScript skills – you understand not only how to build the data, but how to make it look great too', - 'Strong experience in all aspects of client-side performance optimization', - ], - link: '/careers/frontend', - }, -]; - -export default careers; diff --git a/src/data/resources.js b/src/data/resources.js index 6c5007a7..273efa48 100644 --- a/src/data/resources.js +++ b/src/data/resources.js @@ -1,7 +1,7 @@ const resources = [ { title: 'GitLab: How GitLab iterates on SQL performance optimization workflow to reduce downtime risks', - description: 'Learn how SaaS company can improve database management processes.', + description: 'Learn how a SaaS company can improve its database management processes.', preview: '/assets/thumbnails/case-study-gitlab.png', link: '/resources/case-studies/gitlab', internalLink: true, diff --git a/src/dynamicPages/chats/ChatsContent/index.tsx b/src/dynamicPages/chats/ChatsContent/index.tsx index 783a69fa..bba9f8a0 100644 --- a/src/dynamicPages/chats/ChatsContent/index.tsx +++ b/src/dynamicPages/chats/ChatsContent/index.tsx @@ -237,7 +237,7 @@ export const Chatscontent = () => { <div className={styles.errorContainer}> <h2>Chat not found</h2> <p> - No chat found with the id <strong>{id}</strong> + No chat found with the ID <strong>{id}</strong> </p> <p> <span onClick={handleFetchChat} className={styles.tryAgain}> diff --git a/src/pages/bot/index.tsx b/src/pages/bot/index.tsx index 00cd9f50..963dbdbc 100644 --- a/src/pages/bot/index.tsx +++ b/src/pages/bot/index.tsx @@ -14,7 +14,7 @@ const textSequence = [ 4000, "Source code interaction: \"talk to the source code\" to understand how exactly things are implemented, when the docs are not enough.", 3000, - "Real database experiments conducted by bot: study Postgres behavior in action, run benchmarks, optimize query performance.", + "Real database experiments conducted by the bot: study Postgres behavior in action, run benchmarks, and optimize query performance.", 3000, ] diff --git a/src/pages/careers/dbe.md b/src/pages/careers/dbe.md deleted file mode 100644 index 89f7f9b2..00000000 --- a/src/pages/careers/dbe.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: Senior Database Engineer -requirements: - - 3+ years of experience in running PostgreSQL in large production environments - - Strong skills in performance optimization of large and heavily loaded databases (>1TiB, >10k TPS) - - Readiness to dive deep into PostgreSQL internals ---- - -# Senior Database Engineer - -As a Senior Database Engineer, you will be working on state-of-the-art solutions, involving the following topics: - -- Сapacity planning -- Database experiments -- Incident troubleshooting -- Database and CI/CD -- Cloud infrastructures -- Partitioning -- Sharding -- Database change management -- Performance optimization - -## Requirements - -- 3+ years of experience in running PostgreSQL in large production environments -- Strong skills in performance optimization of large and heavily loaded databases (>1TiB, >10k TPS) -- Readiness to dive deep into PostgreSQL internals -- A proven track record of increasing responsibility in the field of databases -- Experience and skills in the field of systems performance (PostgreSQL and Linux monitoring, troubleshooting, tuning) -- Self-motivation and strong desire to achieve the highest levels of expertise -- Solid skills of reading and using EXPLAIN command to troubleshoot and optimize SQL queries -- Technical English -- Advanced SQL and PL/pgSQL knowledge - -## Nice-to-haves - -- Experience in partitioning -- Experience in sharding -- Experience in implementation of SQL optimization workflow, involving `pg_stat_statements`, log-based analysis, etc. -- Experience in development/tuning of advanced PostgreSQL monitoring setups either from scratch or based on existing components -- Deep understanding of file systems -- ZFS experience -- Go and/or C -- Python and/or Ruby -- Oracle or SQL Server scalability and performance optimization experience -- Knowledge of basic machine learning algorithms and experience/with popular machine learning frameworks -- Cloud experience (AWS, GCP) -- Experience in working with logical decoding and replication -- Experience in working in a distributed team -- Good verbal/written skills in English - -## Responsibilities - -- Development of new open-source to automate: - - SQL query optimization - - Database experiments to verify DB migrations, complex changes, etc - - PostgreSQL configuration tuning -- SQL performance troubleshooting and optimization -- DB schema design to store data securely and efficiently -- Code reviews and interaction with development teams -- Setting up SQL optimization workflow in various development teams -- Direct work with our clients to help them scale and optimize their PostgreSQL databases -- Assisting the development team to develop brand new tools that solve database scalability and performance problems - -## Benefits - -- Development of game-changing tools for software engineers -- Interesting and challenging tasks, basis for constant learning of new technologies -- Team of professionals and a supportive atmosphere -- Extremely competitive pay depending on experience and skills -- Flexible working hours/home-office - -Send us your CV to join@postgres.ai diff --git a/src/pages/careers/frontend.md b/src/pages/careers/frontend.md deleted file mode 100644 index b88621c6..00000000 --- a/src/pages/careers/frontend.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Senior Frontend Developer -requirements: - - 3+ years experience in client-side development using React - - Excellent HTML, CSS, JavaScript skills – you understand not only how to build the data, but how to make it look great too - - Strong experience in all aspects of client-side performance optimization ---- - -# Senior Frontend Developer - -As a Senior Frontend Developer, you will be building components of the [PostgresAI Platform](https://postgres.ai/docs). - -## Job Details - -- Employment type: full-time/ part-time remote -- Company: PostgresAI, headquartered in California - -## Requirements - -- 3+ years experience in client-side development using React -- Excellent HTML, CSS, JavaScript skills – you understand not only how to build the data, but how to make it look great too -- Strong experience in all aspects of client-side performance optimization -- Deep understanding of HTTP protocol, data structures, JSON -- Technical English -- Experience in working remotely - - -## Nice-to-haves - -None of the following is a requirement, yet having any of these items increases your chances to be a perfect match for the PostgresAI team. - -- Rest API development experience -- PostgreSQL experience is a plus -- Advanced knowledge of CI/CD tools -- Contributions to Open Source projects -- Understanding of containerization concepts and tools, Docker specifically -- Good command of English -- Solid knowledge of Computer Science fundamentals including the following topics: - - Data Structures - - Algorithms, and - - System Optimization - -## Benefits - -- Development of game-changing tools for software engineers -- Interesting and challenging tasks, basis for constant learning of new technologies -- Team of professionals and a supportive atmosphere -- Extremely competitive pay depending on experience and skills -- Flexible working hours/home-office - - -Send us your CV to join@postgres.ai - diff --git a/src/pages/careers/fullstack.md b/src/pages/careers/fullstack.md deleted file mode 100644 index 1bcbbdb6..00000000 --- a/src/pages/careers/fullstack.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Senior Full Stack Developer | React | Go -requirements: - - 3+ years experience in client-side development using React - - Solid skills of basic SQL (SQL-92) - - Excellent HTML, CSS, JavaScript skills – you understand not only how to build the data, but how to make it look great too ---- - -# Senior Full Stack Developer | React | Go - -As a Senior Full Stack Developer, you will be building components of the [PostgresAI Platform](https://postgres.ai/docs). - -## Job Details - -- Employment type: full-time, remote -- Company: PostgresAI, headquartered in California - -## Requirements - -- 3+ years experience in client-side development using React -- Solid skills of basic SQL (SQL-92) -- Excellent HTML, CSS, JavaScript skills – you understand not only how to build the data, but how to make it look great too -- Strong experience in all aspects of client-side performance optimization -- Deep understanding of HTTP protocol, data structures, JSON -- Rest API development experience - -## Nice-to-haves - -None of the following is a requirement, yet having any of these items increases your chances to be a perfect match for the PostgresAI team. - -- Experience in developing server applications using Go, or readiness to learn Go -- PostgreSQL experience is a big plus -- Advanced knowledge of CI/CD tools -- Contributions to Open Source projects -- Deep understanding of containerization concepts and tools, Docker specifically -- Kubernetes experience is a big plus -- Good command of English -- Solid knowledge of Computer Science fundamentals including the following topics: - - Data Structures - - Algorithms, and - - System Optimization -- Experience in working remotely - -## Benefits - -- Development of game-changing tools for software engineers -- Interesting and challenging tasks, basis for constant learning of new technologies -- Team of professionals and a supportive atmosphere -- Extremely competitive pay depending on experience and skills -- Flexible working hours/home-office - - -Send us your CV to join@postgres.ai diff --git a/src/pages/careers/godeveloper.md b/src/pages/careers/godeveloper.md deleted file mode 100644 index a6944f01..00000000 --- a/src/pages/careers/godeveloper.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Senior Software Engineer | Go -requirements: - - 3+ years experience developing server applications using Go. - - Deep understanding of HTTP protocol, data structures, JSON. - - REST API development experience. - - Solid skills of basic SQL (SQL-92). ---- - -# Senior Software Engineer | Go - -As a Senior Go Developer, you will be building components of the [PostgresAI Platform](https://postgres.ai/docs/). - -## Job Details - -- Employment type: full-time, remote. -- Company: PostgresAI, headquartered in the San Francisco Bay Area. - -## Requirements - -- 3+ years experience developing server applications using Go. -- Deep understanding of HTTP protocol, data structures, JSON. -- Rest API development experience. -- Solid skills of basic SQL (SQL-92). -- Understanding of concepts of distributed systems engineering. -- Cloud experience (AWS, GCP). - -## Nice-to-haves - -None of the following is a requirement, yet having any of these items increases your chances to be a perfect match for the PostgresAI team. - -- PostgreSQL experience is a big plus. -- Deep understanding of containerization concepts and tools, Docker specifically. -- Kubernetes experience is a big plus, especially if you: - - participated in development of k8s operators, - - have experience of working with databases managed by k8s. -- Deep understanding of file systems. -- ZFS experience. -- Experience in development of observability tools. -- Advanced knowledge of CI/CD tools. -- Contributions to Open Source projects. -- Good command of English. -- Solid knowledge of Computer Science fundamentals including the following topics: - - Data Structures, - - Algorithms, and - - System Optimization. -- Experience working remotely. - -## Benefits - -- Development of game-changing tools for software engineers. -- Interesting and challenging tasks, basis for constant learning of new technologies. -- Team of professionals and a supportive atmosphere. -- Extremely competitive pay depending on experience and skills. -- Flexible working hours/home-office. -- Health care payment. -- Online IT / Business English course. - -Send us your CV to join@postgres.ai diff --git a/src/pages/careers/index.js b/src/pages/careers/index.js deleted file mode 100644 index 72976c2b..00000000 --- a/src/pages/careers/index.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import clsx from 'clsx'; -import Layout from '@theme/Layout'; - -import careers from '../../data/careers'; - -const TITLE = 'Careers'; - -function Careers() { - return ( - <Layout title={TITLE}> - <main className="container margin-vert--lg"> - <div className="text--center margin-bottom--m"> - <h1>{TITLE}</h1> - </div> - <div className="card__row"> - {careers.map((job) => ( - <div key={job.title} className="col col--4 margin-bottom--lg"> - <div className={clsx('card', 'showcaseUser', 'careers')}> - <div className="card__body"> - <div className="avatar"> - <div className="avatar__intro margin-left--none"> - <h4 className="avatar__name">{job.title}</h4> - <small className="avatar__subtitle"> - {job.descriptions && job.descriptions.map((desc) => ( - <span>- {desc}<br /></span> - ))} - </small> - </div> - </div> - </div> - {(job.link) && ( - <div className="card__footer"> - <div className="button-group button-group--block"> - {job.link && ( - <a - className="button button--small button--secondary button--block" - href={job.link}> - More details - </a> - )} - </div> - </div> - )} - </div> - </div> - ))} - </div> - </main> - </Layout> - ); -} - -export default Careers; diff --git a/src/pages/consulting-landing.tsx b/src/pages/consulting-landing.tsx index 387bc0bb..4c8ea2ed 100644 --- a/src/pages/consulting-landing.tsx +++ b/src/pages/consulting-landing.tsx @@ -110,7 +110,7 @@ const ConsultingLandingPage: React.FC = () => { {/* Why engage */} <section className={classNames('container', styles.section)}> - <h2 className={styles.sectionTitle}>Why engage Postgres AI?</h2> + <h2 className={styles.sectionTitle}>Why engage PostgresAI?</h2> <ul className={styles.bullets}> <li>You're hitting database bottlenecks as usage accelerates.</li> <li>You’re growing fast but have limited in‑house database expertise.</li> @@ -190,7 +190,7 @@ const ConsultingLandingPage: React.FC = () => { <footer className={styles.quoteFooter}><a className={styles.accent} href="https://www.linkedin.com/in/oliver-r-9a16b943/" target="_blank" rel="noreferrer">Oliver Rice, Ph.D</a> — Head of Engineering at <span className={styles.accent}>Supabase</span>, USA</footer> </blockquote> <blockquote className={styles.quote}> - “When you're powering thousands of developer apps, database downtime isn't an option—PostgresAI's expertise over the years culminated in a flawless zero-downtime Postgres upgrade that kept our platform running seamlessly while we scaled for the future.” + “When you're powering thousands of developer apps, database downtime isn't an option — PostgresAI's expertise over the years culminated in a flawless zero-downtime Postgres upgrade that kept our platform running seamlessly while we scaled for the future.” <footer className={styles.quoteFooter}><a className={styles.accent} href="https://www.linkedin.com/in/harrybrundage/" target="_blank" rel="noreferrer">Harry Brundage</a> — Co-founder & CTO at <span className={styles.accent}>Gadget</span>, Canada</footer> </blockquote> <blockquote className={styles.quote}> diff --git a/src/pages/consulting.mdx b/src/pages/consulting.mdx index 6cbd966b..3574ca19 100644 --- a/src/pages/consulting.mdx +++ b/src/pages/consulting.mdx @@ -18,7 +18,7 @@ You need us if you're a fast-growing startup on PostgreSQL (whether it's managed ## Our approach -We break down complex database problems into clear, actionable steps. Through practical experimentation and early testing, your team learns to solve performance and scalability challenges quickly. No guesswork—just proven solutions based on real-world experience. +We break down complex database problems into clear, actionable steps. Through practical experimentation and early testing, your team learns to solve performance and scalability challenges quickly. No guesswork — just proven solutions based on real-world experience. --- @@ -119,7 +119,7 @@ Our engagement is typically structured like this: --- <div className="row justify-content-center align-items-center"> - <a className="btn btn1 cta-button" href="mailto:consulting@postgres.ai" target="_blank"> - Get help with Postgres now + <a className="btn btn1 cta-button" href="mailto:consulting@postgres.ai"> + {'Get help with Postgres now'} </a> </div> diff --git a/src/pages/contact/index.tsx b/src/pages/contact/index.tsx index 941bc02a..4cdce3b0 100644 --- a/src/pages/contact/index.tsx +++ b/src/pages/contact/index.tsx @@ -14,9 +14,9 @@ const ContactPage: React.FC = () => { { name: 'Community on Slack', description: - 'Join other PostgresAI users to get help and latest news', + 'Join other PostgresAI users to get help and the latest news', icon: '/assets/contact/slack.svg', - buttonText: 'Join the Community', + buttonText: 'Join the community', onButtonClick: () => openLink('https://slack.postgres.ai/'), }, { @@ -41,7 +41,7 @@ const ContactPage: React.FC = () => { }, []) return ( - <Layout title="PostgreSQL Contact us"> + <Layout title="Contact PostgresAI"> <main className="banner text-center"> <section className={classNames('container padding-vert--xl', styles.container)} @@ -87,7 +87,7 @@ const ContactPage: React.FC = () => { </a> {' '}or{' '} <a - href="https://gitlab.com/postgres-ai/database-lab/-/issues" + href="https://github.com/postgres-ai/database-lab-engine/issues" target="_blank" className={styles.link} > @@ -116,7 +116,7 @@ const ContactPage: React.FC = () => { <a className={styles.link} href="/docs" target="_blank"> Documentation section </a>{' '} - containing tutorials and how-tos for most popular use cases. + containing tutorials and how-tos for the most popular use cases. </div> <div className={styles.footerText}> It's a good idea to start with the{' '} diff --git a/src/pages/customer-advisory-group.md b/src/pages/customer-advisory-group.md index 10a27a56..3b787761 100644 --- a/src/pages/customer-advisory-group.md +++ b/src/pages/customer-advisory-group.md @@ -30,7 +30,7 @@ Companies who are accepted into the Customer Advisory Group will make the follow Free for 6 months. * Shape our product roadmap to the needs of your organization. * _Added Bonus:_ We will run a free PostgreSQL Query Optimization Training course for your whole team based - on the #1 voted talk from this year's [PGConf.Online](https://pgconf.ru/en/2021). + on the #1 voted talk from this year's [PGConf.Online](https://pgconf.ru/2021/en). ## Who is qualified? diff --git a/src/pages/index.module.css b/src/pages/index.module.css index dcbb3a57..58f6e7e8 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -32,6 +32,12 @@ margin: 32px 0 0 0; } +.cityContainer { + max-width: 100%; + margin: 44px auto 8px auto; + text-align: left; +} + .videoContainer { max-width: 100%; margin: 48px auto 32px auto; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ced90fe6..3fe6cb4d 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,6 +2,8 @@ import React from 'react' import Layout from '@theme/Layout' import useDocusaurusContext from '@docusaurus/useDocusaurusContext' +import PostgresCity from '../components/PostgresCity' + import styles from './index.module.css' import { TRUSTED_BY_COMPANIES } from './pricing' import { SITE_NAME, SITE_SLOGAN, SITE_SUBTITLE } from '../config/site' @@ -16,7 +18,7 @@ const testimonials = [ location: "USA", }, { - quote: "When you're powering thousands of developer apps, database downtime isn't an option—PostgresAI's expertise over the years culminated in a flawless zero-downtime Postgres upgrade that kept our platform running seamlessly while we scaled for the future.", + quote: "When you're powering thousands of developer apps, database downtime isn't an option — PostgresAI's expertise over the years culminated in a flawless zero-downtime Postgres upgrade that kept our platform running seamlessly while we scaled for the future.", name: "Harry Brundage", title: "Co-founder & CTO", company: "Gadget", @@ -64,6 +66,12 @@ function IndexPage() { </a> </div> + {/* What a Postgres cluster is actually doing, drawn from a + running model. Derived from PGSimCity. */} + <div className={styles.cityContainer}> + <PostgresCity title="A PostgreSQL cluster, running" /> + </div> + {/* Video Container */} <div className={styles.videoContainer}> <div className={styles.videoEmbed}> diff --git a/src/pages/postgres-check/index.tsx b/src/pages/postgres-check/index.tsx index 0f983152..077fbe57 100644 --- a/src/pages/postgres-check/index.tsx +++ b/src/pages/postgres-check/index.tsx @@ -255,7 +255,7 @@ const platforms = [ const faqItems = [ { question: 'Is this really free?', - answer: 'All PostgreSQL health check reports are free forever - no credit card, no trials, no locked features. Paid plans unlock continuous monitoring and expert recommendations that turn findings into clear, prioritized actions.', + answer: 'All Postgres health check reports are free forever - no credit card, no trials, no locked features. Paid plans unlock continuous monitoring and expert recommendations that turn findings into clear, prioritized actions.', }, { question: 'Is it safe to connect my database?', diff --git a/src/pages/pricing/index.tsx b/src/pages/pricing/index.tsx index 738fa2b3..be558cd8 100644 --- a/src/pages/pricing/index.tsx +++ b/src/pages/pricing/index.tsx @@ -342,21 +342,12 @@ const plans: Plan[] = [ cta: 'Get started', ctaLink: 'https://console.postgres.ai/get-started?plan=express', }, - { - name: 'Starter', - price: '$128', - period: '/cluster/mo', - description: 'For small production databases', - keyFeature: 'Full monitoring with 7-days history', - cta: 'Get started', - ctaLink: 'https://console.postgres.ai/get-started?plan=starter', - }, { name: 'Scale', price: '$512', period: '/cluster/mo', description: 'For growing teams with critical workloads', - keyFeature: '6-months history + 1 business day SLA', + keyFeature: '6-month history + 1 business day SLA', cta: 'Get started', ctaLink: 'https://console.postgres.ai/get-started?plan=scale', highlight: true, @@ -380,32 +371,33 @@ interface FeatureRow { const features: FeatureRow[] = [ { category: 'Checkup reports', feature: '', values: [] }, - { feature: 'AI-friendly database checkup reports', values: [true, true, true, true, true] }, - { feature: 'Nodes included', values: ['Primary only', 'Primary only', 'Primary + 2 replicas', 'Unlimited replicas', 'Unlimited replicas'] }, - { feature: 'Issues with detailed action plans', values: [false, true, true, true, true] }, - { feature: 'Full AI-driven workflow (MCP, Cursor, Claude Code)', values: [false, true, true, true, true] }, + { feature: 'AI-friendly database checkup reports', values: [true, true, true, true] }, + { feature: 'Nodes included', values: ['Primary only', 'Primary only', 'Unlimited replicas', 'Unlimited replicas'] }, + { feature: 'Issues with detailed action plans', values: [false, true, true, true] }, + { feature: 'Full AI-driven workflow (MCP, Cursor, Claude Code)', values: [false, true, true, true] }, { category: 'Monitoring', feature: '', values: [] }, - { feature: 'Full monitoring stack', values: [false, false, true, true, true] }, - { feature: 'Monitoring retention', values: ['—', '—', '7 days', '6 months', 'Custom'] }, - { feature: 'Monitoring data for the past, ready for RCAs', values: [false, false, true, true, true] }, - { feature: 'Trend analysis and risk detection', values: [false, false, false, true, true] }, - { feature: 'Advanced reports and workflows', values: [false, false, true, true, true] }, + { feature: 'Full monitoring stack', values: [false, false, true, true] }, + { feature: 'Private RDS monitoring over AWS PrivateLink', values: [false, false, true, true] }, + { feature: 'Monitoring retention', values: ['—', '—', '6 months', 'Custom'] }, + { feature: 'Monitoring data for the past, ready for RCAs', values: [false, false, true, true] }, + { feature: 'Trend analysis and risk detection', values: [false, false, true, true] }, + { feature: 'Advanced reports and workflows', values: [false, false, true, true] }, { category: 'Alerts', feature: '', values: [] }, - { feature: 'Email alerts', values: [false, false, true, true, true] }, - { feature: 'Slack alerts', values: [false, false, true, true, true] }, + { feature: 'Email alerts', values: [false, false, true, true] }, + { feature: 'Slack alerts', values: [false, false, true, true] }, { category: 'Support', feature: '', values: [] }, - { feature: 'Community support', values: [true, true, true, true, true] }, - { feature: 'Async support (email, Slack)', values: [false, false, true, true, true] }, - { feature: 'Support SLA', values: ['—', '—', '—', '1 business day', 'Custom'] }, - { feature: 'Dedicated support channel', values: [false, false, false, false, true] }, + { feature: 'Community support', values: [true, true, true, true] }, + { feature: 'Async support (email, Slack)', values: [false, false, true, true] }, + { feature: 'Support SLA', values: ['—', '—', '1 business day', 'Custom'] }, + { feature: 'Dedicated support channel', values: [false, false, false, true] }, { category: 'Advanced', feature: '', values: [] }, - { feature: 'Kubernetes & Terraform supported', values: [false, false, false, false, true] }, - { feature: 'Custom workflows', values: [false, false, false, false, true] }, - { feature: 'On-prem deployment', values: [false, false, false, false, true] }, + { feature: 'Kubernetes & Terraform supported', values: [false, false, false, true] }, + { feature: 'Custom workflows', values: [false, false, false, true] }, + { feature: 'On-prem deployment', values: [false, false, false, true] }, ] const Pricing = () => { @@ -414,7 +406,7 @@ const Pricing = () => { const { apiUrlPrefix } = customFields const [isLoading, setIsLoading] = useState(true) const [tableData, setTableData] = useState([]) - const [selectedPlanIndex, setSelectedPlanIndex] = useState(3) // Default to Scale + const [selectedPlanIndex, setSelectedPlanIndex] = useState(2) // Default to Scale const [isDblabPricingExpanded, setIsDblabPricingExpanded] = useState(false) useEffect(() => { @@ -632,7 +624,7 @@ const Pricing = () => { <li>One-time package delivering Enterprise-level support</li> <li>Unlimited Slack communications</li> <li>1 hour live training (Zoom)</li> - <li>Custom-built docker images upon request</li> + <li>Custom-built Docker images upon request</li> <li>Max. response time: 1 business day</li> </ul> <div className={styles.addonPricing}> @@ -763,7 +755,6 @@ const Pricing = () => { <ul> <li><strong>Hobby:</strong> You're learning Postgres or running a pet project with no real users yet.</li> <li><strong>Express:</strong> You're shipping fast (solo or small team) and want to make sure your database survives the first users.</li> - <li><strong>Starter:</strong> You're in production with a small database and need full visibility when things break.</li> <li><strong>Scale:</strong> You have critical workloads, need longer history, and want faster support.</li> <li><strong>Enterprise:</strong> You need stricter SLAs, dedicated support, or on-prem deployment.</li> </ul> diff --git a/src/pages/pricing/styles.module.css b/src/pages/pricing/styles.module.css index c95ca644..d2d90c0e 100644 --- a/src/pages/pricing/styles.module.css +++ b/src/pages/pricing/styles.module.css @@ -35,7 +35,7 @@ .plansGrid { display: grid; - grid-template-columns: repeat(5, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 1rem; } diff --git a/src/pages/privacy.md b/src/pages/privacy.md index 2436143b..8cfd1e79 100644 --- a/src/pages/privacy.md +++ b/src/pages/privacy.md @@ -24,7 +24,7 @@ This Policy applies to our customers and users who have visited our website and - **Your personal details** – name, address, phone number, email address when you submit web forms on our website, including opportunities to sign up for and agree to receive email communications from us. We also may ask you to submit such personal information if you choose to use interactive features of our website, including participation in surveys, contests, promotions, sweepstakes, requesting customer support, registration for attendance at an event sponsored by PostgresAI or otherwise communicating with us. - **Log Files** – certain information including but is not limited to Internet Protocol (IP) addresses, system configuration information, URLs of referring pages, and locale and language preferences when you visit and interact with most websites and services delivered via the Internet, when you visit our website and interactive areas offered by it. - **Cookies and Other Tracking Technologies** – we may use cookies to provide you with a user-friendly interface, features and general access to our website. You can control how our website use cookies by configuring your browser's privacy settings (please refer to your browser's help function to learn more about cookie controls). Note that if you disable cookies entirely, our website may not function properly. The purpose of the use of cookies on our website is site improvement, as well as the analysis of our customer-provider relationships and marketing intentions. We do not use third-party marketing or analytics services (such as Google Analytics or Facebook Pixel). Our analytics are self-hosted, and any tracking cookies we use (such as for marketing attribution) remain first-party and are not shared with external advertising platforms. Because we use only strictly necessary first-party cookies and do not employ third-party tracking, no cookie consent banner is required under current EU guidance. If our cookie practices change, we will update this section and implement appropriate consent mechanisms. For more information about the cookies PostgresAI uses, please see our Cookie Policy. -- **Interactive Areas** – publicly accessible blogs, chats, community forums, comments sections, discussion forums, or other interactive features (“Interactive Areas”). If you choose to participate in any of these Interactive Areas, please be aware that that any information that you post in an Interactive Area might be read, collected, and used by others who access it. If you wish to remove your personal information from any of our Interactive Areas, please contact us at: privacy@postgres.ai. +- **Interactive Areas** – publicly accessible blogs, chats, community forums, comments sections, discussion forums, or other interactive features (“Interactive Areas”). If you choose to participate in any of these Interactive Areas, please be aware that any information that you post in an Interactive Area might be read, collected, and used by others who access it. If you wish to remove your personal information from any of our Interactive Areas, please contact us at: privacy@postgres.ai. ## HOW WE USE INFORMATION WE COLLECT PostgresAI only processes personal information in a way that is compatible with and relevant for the purpose for which it was collected or authorized. As a general matter, for all categories of data we collect, we may use the information we collect to: diff --git a/src/pages/products/data-masking.md b/src/pages/products/data-masking.md index 49ef1e5d..ccb3163b 100644 --- a/src/pages/products/data-masking.md +++ b/src/pages/products/data-masking.md @@ -39,7 +39,7 @@ Learn more about masking techniques with our recommended tool: ## Clear and auditable rules DBLab Engine recommends a declarative approach to data masking using -[PostgreSQL Anonymizer](https://postgresql-anonymizer.readthedocs.io/en/stable/declare_masking_rules.html). +[PostgreSQL Anonymizer](https://postgresql-anonymizer.readthedocs.io/en/stable/declare_masking_rules/). Masking rules are declared as security labels within the schema itself. diff --git a/src/pages/products/dblab_engine/index.tsx b/src/pages/products/dblab_engine/index.tsx index 834f8c4e..9e5cf163 100644 --- a/src/pages/products/dblab_engine/index.tsx +++ b/src/pages/products/dblab_engine/index.tsx @@ -9,7 +9,10 @@ const DBLAB_START_URL = const DBLabEngine = () => { return ( - <Layout> + <Layout + title="DBLab Engine: instant database branching for Postgres" + description="DBLab Engine delivers instant, thin database branching and cloning for any Postgres database, with fixed storage and compute costs for development, testing, and CI/CD." + > <section className="banner position-relative text-center"> <div className="container"> <div className="row justify-content-center align-items-center"> @@ -208,7 +211,7 @@ const DBLabEngine = () => { <br /> <br /> <a href="/products/joe">Joe Bot</a>, our virtual DBA, runs on - top of Database Lab to help you find and fix bottlenecks. + top of DBLab to help you find and fix bottlenecks. <br /> <br /> Iterate as many times as you want to achieve the best results. diff --git a/src/pages/products/how-it-works.md b/src/pages/products/how-it-works.md index 514f44b2..6ec769c3 100644 --- a/src/pages/products/how-it-works.md +++ b/src/pages/products/how-it-works.md @@ -48,7 +48,7 @@ If you run your infrastructure on AWS, you can follow our [Getting Started Guide for RDS](/docs/tutorials/database-lab-tutorial-amazon-rds). Here's what you can expect when setting up the DBLab Engine: -* Experienced engineers can setup the DBLab Engine in less than 1 hour +* Experienced engineers can set up the DBLab Engine in less than 1 hour * The DBLab Engine host instance should have a disk 30% larger than the production DB * [Sensitive data masking](/products/data-masking) can be achieved with a set of declarative rules diff --git a/src/pages/products/postgres-ai-zdu.md b/src/pages/products/postgres-ai-zdu.md index ba11f198..dfdaded4 100644 --- a/src/pages/products/postgres-ai-zdu.md +++ b/src/pages/products/postgres-ai-zdu.md @@ -25,7 +25,7 @@ Traditional Postgres major version upgrades require: - **Hard to test and verify** procedures for various issues like incompatibilities - **Risks of plan flips** and performance regressions after upgrade -## Our solution: Four-component approach +## Our solution: four-component approach A complete system: @@ -57,7 +57,8 @@ This solution is **fully developed and battle-tested**. We're offering preview a textDecoration: 'none', padding: '12px 24px', borderRadius: '6px', - display: 'inline-block', + display: 'inline-flex', + alignItems: 'center', fontWeight: '500' }} > diff --git a/src/pages/products/realistic-test-environments.md b/src/pages/products/realistic-test-environments.md index 36578bfd..9aac4aee 100644 --- a/src/pages/products/realistic-test-environments.md +++ b/src/pages/products/realistic-test-environments.md @@ -7,7 +7,7 @@ description: Give your developers and QA team realistic environments A single instance of the DBLab Engine can generate many fully isolated database clones. These clones can replace the need -to setup, maintain, and pay for standalone development and staging databases. +to set up, maintain, and pay for standalone development and staging databases. [Learn how the DBLab Engine works](/products/how-it-works). ## Your staging server doesn't give you the full picture diff --git a/src/pages/rules/index.md b/src/pages/rules/index.md index fc957f2d..a3164b2d 100644 --- a/src/pages/rules/index.md +++ b/src/pages/rules/index.md @@ -15,7 +15,7 @@ These rules are designed to be used as instructions for AI coding assistants: 1. **Cursor**: use slash commands to create rules ([docs](https://docs.cursor.com/context/rules)) 2. **Claude Code**: use `CLAUDE.md` ([docs](https://docs.anthropic.com/en/docs/claude-code/memory)) -3. **GitHub Copilot**: configure coding guidelines ([docs](https://docs.github.com/en/copilot/how-tos/agents/copilot-code-review/configuring-coding-guidelines)) +3. **GitHub Copilot**: configure coding guidelines ([docs](https://docs.github.com/en/copilot/concepts/code-review/coding-guidelines)) 4. **Other AI tools**: see your tool docs, looking for "rules", "guidelines", "memory" or similar instructions. Each rule page includes "Copy for LLM" and "View raw" buttons for easy copying. \ No newline at end of file diff --git a/src/pages/supabase-check/index.tsx b/src/pages/supabase-check/index.tsx index 40480123..01d9a13f 100644 --- a/src/pages/supabase-check/index.tsx +++ b/src/pages/supabase-check/index.tsx @@ -56,7 +56,7 @@ const steps = [ const faqItems = [ { question: 'Is this really free?', - answer: 'All PostgreSQL health check reports are free forever — no credit card, no trials, no locked features. Paid plans unlock continuous monitoring and expert recommendations that turn findings into clear, prioritized actions.', + answer: 'All Postgres health check reports are free forever — no credit card, no trials, no locked features. Paid plans unlock continuous monitoring and expert recommendations that turn findings into clear, prioritized actions.', }, { question: 'Is it safe to connect my database?', diff --git a/static/assets/blog/20260408-clone-protection-dropdown.png b/static/assets/blog/20260408-clone-protection-dropdown.png new file mode 100644 index 00000000..a559bbb9 Binary files /dev/null and b/static/assets/blog/20260408-clone-protection-dropdown.png differ diff --git a/static/assets/blog/not-exists-vs-exists-benchmark/run.sh b/static/assets/blog/not-exists-vs-exists-benchmark/run.sh new file mode 100644 index 00000000..bc37f8de --- /dev/null +++ b/static/assets/blog/not-exists-vs-exists-benchmark/run.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# Benchmark: NOT EXISTS(deleted) vs EXISTS(NOT deleted) — partial indexes +# +# Full reproducible script. Tested on PG18, CCX33 (8 vCPU, 32 GiB), Hetzner NBG1. +# Results: https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/issues/74 +# Blog post: https://postgres.ai/blog/20260306-not-exists-vs-exists-partial-index +# +# Usage: +# sudo bash run.sh +# +# Requirements: +# - PostgreSQL 14+ (tested on PG18) +# - pg_prewarm extension (postgresql-contrib) +# - Dedicated benchmark VM (not the machine you work on) +# - Run as root or a user with sudo + pg_ctlcluster access + +set -Eeuo pipefail +IFS=$'\n\t' + +PG_SUPERUSER="${PG_SUPERUSER:-postgres}" +DB="bench" +ROWS=50000000 +TAG_ROWS=500000 + +log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*"; } + +psql_bench() { sudo -u "$PG_SUPERUSER" psql -d "$DB" "$@"; } +psql_root() { sudo -u "$PG_SUPERUSER" psql "$@"; } + +drop_caches() { + log "Dropping OS page cache + restarting PostgreSQL..." + systemctl stop postgresql + echo 3 > /proc/sys/vm/drop_caches + systemctl start postgresql + sleep 2 +} + +run_explain() { + local label="$1" sql="$2" + log "Running: ${label}" + echo "" + echo "### ${label}" + echo "\`\`\`" + psql_bench -c "explain (analyze, buffers, verbose, settings, format text) ${sql}" + echo "\`\`\`" + echo "" +} + +# ── Setup ───────────────────────────────────────────────────────────────────── + +log "Applying PostgreSQL settings..." +psql_root -c "alter system set shared_buffers = '8GB';" +psql_root -c "alter system set effective_cache_size = '24GB';" +psql_root -c "alter system set work_mem = '64MB';" +psql_root -c "alter system set random_page_cost = 1.1;" +psql_root -c "alter system set track_io_timing = on;" +psql_root -c "select pg_reload_conf();" + +log "Creating database and tables..." +psql_root -c "drop database if exists ${DB};" 2>/dev/null || true +psql_root -c "create database ${DB};" + +psql_bench <<SQL +-- autovacuum disabled: VM state must be fully deterministic +create table posts ( + post_id bigint primary key, + deleted boolean not null default false, + content text not null default repeat('x', 200) +) with (autovacuum_enabled = false); + +create table post_tags ( + tag_id int not null, + post_id bigint not null, + primary key (tag_id, post_id) +) with (autovacuum_enabled = false); +SQL + +log "Inserting ${ROWS} rows into posts (2% deleted)..." +psql_bench -c " +insert into posts (post_id, deleted) +select g, (random() < 0.02) +from generate_series(1, ${ROWS}) g;" + +log "Creating partial indexes..." +psql_bench -c "create unique index posts_not_deleted_id_key on posts (post_id) where not deleted;" +psql_bench -c "create unique index posts_deleted_id_key on posts (post_id) where deleted;" + +log "Inserting ${TAG_ROWS} rows into post_tags..." +psql_bench -c " +insert into post_tags (tag_id, post_id) +select (g % 1000) + 1, (random() * $((ROWS - 1)) + 1)::bigint +from generate_series(1, ${TAG_ROWS}) g +on conflict do nothing;" + +# ── Correct VM state ────────────────────────────────────────────────────────── +# Step 1: vacuum analyze — fill visibility map cleanly +log "Running vacuum analyze posts..." +psql_bench -c "vacuum analyze posts;" +psql_bench -c "vacuum analyze post_tags;" + +# Step 2: controlled dirty update — simulate active production table +log "Dirtying 10% of pages..." +psql_bench -c "update posts set content = repeat('y', 200) where post_id % 10 = 0;" + +# Step 3: analyze only — update stats, do NOT re-clean the VM +psql_bench -c "analyze posts;" + +# Verify: last_autovacuum must be NULL +log "Verifying autovacuum never ran..." +psql_bench -c " +select relname, last_autovacuum, last_autoanalyze +from pg_stat_user_tables +where relname in ('posts', 'post_tags') +order by relname;" + +# Sizes +log "Table and index sizes:" +psql_bench -c " +select indexrelname, pg_size_pretty(pg_relation_size(indexrelid)) as size +from pg_stat_user_indexes +where relname = 'posts' +order by indexrelname;" +psql_bench -c "select pg_size_pretty(pg_relation_size('posts')) as heap_size;" + +log "PG config:" +psql_bench -c "show shared_buffers; show effective_cache_size; show work_mem; show random_page_cost; show track_io_timing;" + +# ── Queries ─────────────────────────────────────────────────────────────────── + +q1() { local s=$1; echo "select pt.* +from post_tags pt +where pt.tag_id = any(array(select generate_series(1,${s}))) + and exists ( + select + from posts + where posts.post_id = pt.post_id + and not deleted + );"; } + +q2() { local s=$1; echo "select pt.* +from post_tags pt +where pt.tag_id = any(array(select generate_series(1,${s}))) + and not exists ( + select + from posts + where posts.post_id = pt.post_id + and deleted + );"; } + +# ── Cold cache: all scales ───────────────────────────────────────────────────── + +echo "" +echo "================================================================" +echo "COLD CACHE" +echo "================================================================" + +for scale in 50 250 1000; do + drop_caches + run_explain "Q1 EXISTS(NOT deleted) — ${scale} tag_ids — COLD" "$(q1 $scale)" + drop_caches + run_explain "Q2 NOT EXISTS(deleted) — ${scale} tag_ids — COLD" "$(q2 $scale)" +done + +# ── Hot cache: pg_prewarm, 250 tag_ids ──────────────────────────────────────── + +echo "" +echo "================================================================" +echo "HOT CACHE — 250 tag_ids (pg_prewarm indexes + heap)" +echo "================================================================" + +log "Prewarming..." +psql_bench <<SQL +create extension if not exists pg_prewarm; +select 'posts_not_deleted_id_key', pg_prewarm('posts_not_deleted_id_key'); +select 'posts_deleted_id_key', pg_prewarm('posts_deleted_id_key'); +select 'posts (heap)', pg_prewarm('posts'); +SQL + +run_explain "Q1 EXISTS(NOT deleted) — 250 tag_ids — HOT" "$(q1 250)" +run_explain "Q2 NOT EXISTS(deleted) — 250 tag_ids — HOT" "$(q2 250)" + +echo "" +log "Done." diff --git a/static/assets/blog/not-exists-vs-exists-chart-reads.png b/static/assets/blog/not-exists-vs-exists-chart-reads.png new file mode 100644 index 00000000..0b3c0015 Binary files /dev/null and b/static/assets/blog/not-exists-vs-exists-chart-reads.png differ diff --git a/static/assets/blog/not-exists-vs-exists-chart-time.png b/static/assets/blog/not-exists-vs-exists-chart-time.png new file mode 100644 index 00000000..ab3f69f0 Binary files /dev/null and b/static/assets/blog/not-exists-vs-exists-chart-time.png differ diff --git a/static/assets/blog/not-exists-vs-exists-chart.png b/static/assets/blog/not-exists-vs-exists-chart.png new file mode 100644 index 00000000..e63343ee Binary files /dev/null and b/static/assets/blog/not-exists-vs-exists-chart.png differ diff --git a/static/assets/blog/teleport-logo-white.svg b/static/assets/blog/teleport-logo-white.svg new file mode 100644 index 00000000..342cd8b0 --- /dev/null +++ b/static/assets/blog/teleport-logo-white.svg @@ -0,0 +1,21 @@ +<svg viewBox="0 0 182.79993 39.33508" xmlns="http://www.w3.org/2000/svg" id="Layer_1"> + <defs> + <style> + .cls-1 { + fill: #ffffff; + stroke-width: 0px; + } + </style> + </defs> + <polygon points="67.409 6.53009 46.71149 6.53009 46.71149 11.23505 54.03839 11.23505 54.03839 32.104 60.08057 32.104 60.08057 11.23505 67.409 11.23505 67.409 6.53009" class="cls-1"></polygon> + <polygon points="128.2962 13.55127 128.29309 13.55127 128.29486 13.55139 128.2962 13.55127" class="cls-1"></polygon> + <path d="M130.73498,16.00952c-.5589-.7644-1.28772-1.38971-2.12775-1.82672-.82098-.42133-1.71295-.63116-2.68042-.63141-1.67169-.02295-3.30035.52216-4.62177,1.54401-.30573.23773-.60071.49414-.88019.77051l-.27332.28406-.40918-1.33411c-.07721-.27948-.25165-.5235-.49261-.68555-.20996-.13129-.44928-.2085-.69788-.22083l-.16827-.00775h-3.43109v23.97729h5.54028v-6.9856c.5343.45544,1.13806.82294,1.78815,1.0885.65314.26868,1.46381.40308,2.43042.40308,1.11646.01385,2.22046-.23315,3.22565-.72119.9682-.47705,1.82214-1.15961,2.50311-1.99957.72107-.89868,1.27081-1.92249,1.62439-3.0188.3891-1.15814.58063-2.42267.58063-3.79077,0-1.46387-.16833-2.7702-.508-3.92371-.33972-1.15198-.80609-2.12622-1.40216-2.92145ZM126.66009,25.41638c-.18066.70251-.42621,1.27386-.74274,1.71234-.31659.43701-.68866.75671-1.11798.95587-.44165.20221-.9234.30261-1.41132.29797-.53583,0-1.04846-.08954-1.53638-.27167-.44318-.17151-.84467-.43396-1.17822-.77063l-.18066-.18524v-7.72375c.22241-.26398.46021-.51263.71497-.74573.23315-.21313.49567-.39233.7782-.5343.27802-.14209.58374-.25018.91107-.32434.32733-.07715.6933-.11584,1.0979-.11584.44318,0,.84155.08801,1.19818.26263.35675.17755.66559.46631.92188.86926.25482.4046.45557.9342.59753,1.58893.14209.65619.21466,1.46228.21466,2.42279,0,1.00671-.08954,1.8606-.26709,2.56171Z" class="cls-1"></path> + <path d="M79.80353,15.81647c-.75049-.72418-1.64606-1.28156-2.62817-1.63367-1.08081-.38452-2.2204-.57446-3.36774-.56049-1.41901,0-2.69141.23932-3.81708.7196-1.08087.44928-2.05988,1.11017-2.87823,1.94708-.79364.82147-1.41284,1.79431-1.82208,2.86121-.42615,1.1026-.64081,2.27606-.63464,3.45892,0,1.61365.24554,3.0296.74274,4.2464.49414,1.21667,1.16742,2.23584,2.01971,3.0542.85242.81995,1.8468,1.43604,2.9848,1.85297,1.13647.41382,2.35632.62231,3.65344.62231.63312,0,1.29547-.0448,1.99347-.13129.69641-.08801,1.39282-.24548,2.09076-.47552.69794-.22711,1.37732-.54205,2.03821-.94666.56677-.34424,1.10095-.77979,1.60278-1.30164l.24713-.26868-1.60742-1.94867c-.22858-.31659-.56055-.47565-1.0022-.47565-.3335,0-.65161.07721-.95422.2301-.30573.15131-.63928.32117-1.00214.508-.40002.20233-.81537.37213-1.24304.50958-.46484.15289-1.0191.22852-1.6615.22852-1.20441,0-2.18805-.34277-2.94928-1.02679-.71179-.63928-1.16888-1.64301-1.37274-3.01111l-.03864-.29797h10.68695c.25171,0,.45557-.02942.61609-.08649.16064-.05865.29803-.16986.38605-.31653.10651-.18835.1745-.39844.19611-.61456.03552-.25787.05408-.58984.05408-1.00061,0-1.31091-.20691-2.4845-.61768-3.51904-.37982-.98676-.96356-1.8808-1.71552-2.62354ZM70.25,20.81793c.19147-1.11176.58521-1.96411,1.17969-2.55396.59607-.59143,1.42365-.88635,2.4845-.88635.59448,0,1.10095.10028,1.51941.2995.39221.1792.73657.44629,1.00989.77985.25787.32428.44623.698.55438,1.09784.11267.41235.1698.83691.16827,1.26312h-6.91614Z" class="cls-1"></path> + <path d="M109.43842,15.815c-.75043-.72424-1.64606-1.28009-2.62811-1.6322-1.08087-.38452-2.22198-.57446-3.36926-.56049l-.00159-.00159c-1.41748,0-2.68982.24091-3.81555.72119-1.07928.44928-2.05829,1.11169-2.87665,1.94708-.7937.82147-1.4129,1.79431-1.82208,2.86121-.42316,1.08862-.63617,2.2406-.63617,3.45892,0,1.61365.2486,3.0296.74268,4.24786.49567,1.21832,1.16742,2.23438,2.01978,3.05273.85382.82147,1.8714,1.45148,2.98627,1.85297,1.17206.41998,2.40887.63153,3.65503.62231.63153,0,1.29395-.0448,1.99188-.13129.71027-.09113,1.41138-.2486,2.09229-.47406.69641-.22693,1.37433-.54352,2.03674-.94812.58673-.36279,1.12567-.79987,1.60126-1.30164l.2486-.26868-1.60895-1.94867c-.22705-.31659-.55902-.47412-1.00214-.47412-.33203,0-.65167.07568-.95435.23016-.30414.14972-.6377.31958-1.00208.508-.3999.20221-.81531.37207-1.24152.508-.46478.15289-1.0191.22705-1.6615.22705-1.20435,0-2.18805-.33978-2.94922-1.02533-.71344-.63928-1.17047-1.64301-1.37433-3.01111l-.03864-.29797h10.68848c.21002.0061.41846-.02319.61609-.08649.16064-.05865.29651-.16986.38452-.31653.10651-.18835.1745-.39844.19611-.61456.03857-.25787.05402-.58984.05402-1.00061,0-1.31091-.20691-2.4845-.61609-3.51904-.41071-1.03467-.98358-1.9101-1.71552-2.625ZM99.88489,20.81793c.18994-1.11176.58368-1.96259,1.17969-2.55396.59454-.5899,1.42224-.88483,2.48297-.88483.5976,0,1.10254.09875,1.51941.29645.41846.19922.75354.45868,1.01147.78137.25629.32117.44.68866.55432,1.09784.11273.41071.16833.82916.16833,1.26312h-6.9162Z" class="cls-1"></path> + <path d="M150.68982,16.13153c-.85083-.82153-1.86536-1.45459-2.97552-1.86066-1.15656-.43243-2.43671-.64856-3.84186-.64856-1.41901,0-2.7099.21613-3.87109.64856-1.11951.40607-2.14172,1.04071-3.00336,1.86066-.85236.82751-1.51788,1.8313-1.94714,2.94-.45856,1.15192-.68707,2.44897-.68707,3.88812,0,1.44989.22852,2.75781.68707,3.92358.45862,1.16278,1.1087,2.15094,1.94714,2.96631.86163.82617,1.88385,1.46545,3.00336,1.87616,1.16272.44,2.45209.6593,3.87109.6593,1.40515,0,2.68683-.2193,3.84186-.6593,1.11176-.41071,2.12622-1.04999,2.97552-1.87616.82764-.81537,1.4715-1.80353,1.93011-2.96631.46014-1.16425.68872-2.47369.68872-3.92358,0-1.43915-.22858-2.73621-.68872-3.88812-.42773-1.10559-1.08551-2.10773-1.93011-2.94ZM146.68591,27.02222c-.60065.8833-1.54108,1.32642-2.81348,1.32642-1.31091,0-2.27142-.44159-2.87823-1.32642-.6084-.88324-.91101-2.22504-.91101-4.02698,0-1.80212.30261-3.14392.91101-4.01947.6084-.87708,1.56732-1.31714,2.87823-1.31714,1.2724,0,2.21283.43848,2.81348,1.31714.60217.87555.90173,2.21735.90173,4.01947,0,1.80194-.29956,3.14374-.90173,4.02698Z" class="cls-1"></path> + <path d="M181.11993,28.2251c-.10809-.16376-.21155-.28259-.31348-.35822-.11737-.08185-.25781-.12201-.40149-.11584-.10651-.00153-.2146.01855-.31189.06183-.09113.04163-.1853.08795-.28717.1405-.26721.13892-.56677.20837-.86786.20227-.39221,0-.6933-.13898-.90021-.41235-.17908-.23615-.28259-.52655-.30731-.86768l-.00616-.17767v-9.04083h4.50421v-3.71979h-4.50421v-5.31805h-2.89679c-.61151,0-1.00214.23627-1.17352.70721l-.04016.13446-1.09167,4.44238-2.94769.56207v2.12164c0,.35199.10034.6192.30261.7998.16522.14984.37213.24097.59296.26416l.13892.00763h1.57349v9.40839c-.00928.74585.11121,1.48846.35828,2.19421.22083.62842.57904,1.19977,1.04535,1.67529.48022.47723,1.06079.84167,1.69702,1.07172.67474.25012,1.45148.37677,2.33313.37677.95428,0,1.87616-.13281,2.77173-.39532.7597-.21924,1.48083-.55438,2.14014-.99286l.27179-.19147-1.67999-2.58026Z" class="cls-1"></path> + <rect height="26.2749" width="5.53882" y="5.8291" x="85.4967" class="cls-1"></rect> + <path d="M163.83496,14.4469c-.80145.58051-1.4978,1.29083-2.05988,2.10455l-.22852.33356-.3042-1.70007c-.03864-.20374-.0957-.40302-.16986-.59753-.05865-.15448-.15753-.29041-.28571-.39539-.13739-.10498-.29645-.17908-.46472-.21918-.16064-.03864-.32587-.06024-.49103-.06488l-.19769-.00464h-3.28583v18.20068h5.54028v-11.16412c.8338-1.521,1.97803-2.28064,3.43103-2.28064.41846,0,.76434.02631,1.03766.07874.27332.05255.51111.08032.7149.08032.23785,0,.42932-.04938.57135-.14972.12506-.0943.2146-.22858.25323-.37994l.034-.13739.71338-4.00238c-.51422-.39838-1.19208-.5976-2.03821-.5976-1.00067,0-1.9256.2995-2.7702.89563Z" class="cls-1"></path> + <path d="M37.65826,25.22339l-3.83563-3.26593.07104-.44318c.08032-.59912.10657-1.22443.10657-1.8421,0-.76752-.04016-1.55652-.17761-2.28534l3.83563-3.26117.0849-.07104c.10193-.10034.17145-.23157.19763-.37213.02783-.14209.00928-.28876-.04938-.42004-.7829-2.36249-2.13397-4.61072-3.79541-6.47455-.23474-.26703-.56677-.36438-.87866-.22845l-4.83282,1.64905-.40149-.31036c-1.10034-.81836-2.30737-1.48224-3.58759-1.97339l-.95721-4.91333-.02777-.10193c-.04773-.13898-.1322-.26099-.24463-.35516-.11243-.09424-.24829-.15594-.39294-.17755l-.00171-.00159c-2.50476-.50024-5.08356-.50024-7.58813,0-.1626.02472-.31348.09888-.43256.21161-.11908.1142-.20026.26093-.23303.42303l-.95703,4.91345-.43433.17291c-1.28314.52502-2.48163,1.23529-3.55646,2.11078l-4.83154-1.65063-.10657-.03394c-.14117-.034-.28888-.02637-.42621.02008-.13745.04785-.25836.13275-.349.24542C2.19269,8.65173.84393,10.89996.06213,13.26245c-.06702.14825-.08032.31506-.03748.4726.04272.15747.13873.29639.27185.39062l3.83521,3.2627-.07214.44318c-.08032.59912-.10504,1.22766-.10504,1.84064-.01062.76587.04852,1.53027.17719,2.28528l-3.8335,3.26593-.08521.0694c-.10254.10187-.17175.23169-.19843.37366-.02655.14209-.0094.28882.04926.41998.7818,2.36407,2.13055,4.60931,3.79407,6.46991.23608.26874.56873.36597.88019.23169l4.83154-1.65228.35236.28418c1.11066.83691,2.33612,1.51172,3.63843,2.00116l.95703,4.9165.02631.10187c.04803.13745.13293.26105.24567.35529.11255.09412.2486.1543.39362.17755,1.25214.22852,2.50427.36279,3.79559.36279,1.28992,0,2.54211-.13428,3.79425-.36279.16223-.02637.31299-.1004.43188-.21313.1189-.11432.20044-.26093.23346-.42157l.95874-4.9165.47858-.19305c1.2652-.53424,2.4502-1.23834,3.51068-2.09229l4.83112,1.65228.10498.03552c.28107.07568.56519-.02783.77521-.26721,1.66455-1.8606,3.01251-4.10583,3.79388-6.46991.06793-.14819.08032-.31647.03857-.47406-.04315-.15741-.13892-.29486-.27173-.38898ZM18.97711,31.17743c-6.36896,0-11.53162-5.15271-11.53162-11.50836,0-6.35529,5.159-11.5094,11.53076-11.50989-.00031,0-.00055-.00006-.00085-.00006h.00171c-.00031,0-.00055.00006-.00085.00006,6.37018.00049,11.53271,5.1546,11.53271,11.50989,0,6.35565-5.16296,11.50836-11.53186,11.50836Z" class="cls-1"></path> + <path d="M18.97693,11.80157c-4.34424.00031-7.87878,3.52948-7.87878,7.86707s3.53455,7.86639,7.87909,7.86639,7.87903-3.52881,7.87903-7.86639-3.53479-7.86676-7.87933-7.86707ZM18.97723,24.29132c-2.55603,0-4.63538-2.07361-4.63538-4.62268,0-2.54938,2.07904-4.62329,4.6344-4.62329,2.55634,0,4.63629,2.07391,4.63629,4.62329,0,2.54907-2.07928,4.62268-4.63531,4.62268Z" class="cls-1"></path> +</svg> \ No newline at end of file diff --git a/static/assets/blog/teleport-logo.svg b/static/assets/blog/teleport-logo.svg new file mode 100644 index 00000000..afb92c58 --- /dev/null +++ b/static/assets/blog/teleport-logo.svg @@ -0,0 +1,21 @@ +<svg viewBox="0 0 182.79993 39.33508" xmlns="http://www.w3.org/2000/svg" id="Layer_1"> + <defs> + <style> + .cls-1 { + fill: #512fc9; + stroke-width: 0px; + } + </style> + </defs> + <polygon points="67.409 6.53009 46.71149 6.53009 46.71149 11.23505 54.03839 11.23505 54.03839 32.104 60.08057 32.104 60.08057 11.23505 67.409 11.23505 67.409 6.53009" class="cls-1"></polygon> + <polygon points="128.2962 13.55127 128.29309 13.55127 128.29486 13.55139 128.2962 13.55127" class="cls-1"></polygon> + <path d="M130.73498,16.00952c-.5589-.7644-1.28772-1.38971-2.12775-1.82672-.82098-.42133-1.71295-.63116-2.68042-.63141-1.67169-.02295-3.30035.52216-4.62177,1.54401-.30573.23773-.60071.49414-.88019.77051l-.27332.28406-.40918-1.33411c-.07721-.27948-.25165-.5235-.49261-.68555-.20996-.13129-.44928-.2085-.69788-.22083l-.16827-.00775h-3.43109v23.97729h5.54028v-6.9856c.5343.45544,1.13806.82294,1.78815,1.0885.65314.26868,1.46381.40308,2.43042.40308,1.11646.01385,2.22046-.23315,3.22565-.72119.9682-.47705,1.82214-1.15961,2.50311-1.99957.72107-.89868,1.27081-1.92249,1.62439-3.0188.3891-1.15814.58063-2.42267.58063-3.79077,0-1.46387-.16833-2.7702-.508-3.92371-.33972-1.15198-.80609-2.12622-1.40216-2.92145ZM126.66009,25.41638c-.18066.70251-.42621,1.27386-.74274,1.71234-.31659.43701-.68866.75671-1.11798.95587-.44165.20221-.9234.30261-1.41132.29797-.53583,0-1.04846-.08954-1.53638-.27167-.44318-.17151-.84467-.43396-1.17822-.77063l-.18066-.18524v-7.72375c.22241-.26398.46021-.51263.71497-.74573.23315-.21313.49567-.39233.7782-.5343.27802-.14209.58374-.25018.91107-.32434.32733-.07715.6933-.11584,1.0979-.11584.44318,0,.84155.08801,1.19818.26263.35675.17755.66559.46631.92188.86926.25482.4046.45557.9342.59753,1.58893.14209.65619.21466,1.46228.21466,2.42279,0,1.00671-.08954,1.8606-.26709,2.56171Z" class="cls-1"></path> + <path d="M79.80353,15.81647c-.75049-.72418-1.64606-1.28156-2.62817-1.63367-1.08081-.38452-2.2204-.57446-3.36774-.56049-1.41901,0-2.69141.23932-3.81708.7196-1.08087.44928-2.05988,1.11017-2.87823,1.94708-.79364.82147-1.41284,1.79431-1.82208,2.86121-.42615,1.1026-.64081,2.27606-.63464,3.45892,0,1.61365.24554,3.0296.74274,4.2464.49414,1.21667,1.16742,2.23584,2.01971,3.0542.85242.81995,1.8468,1.43604,2.9848,1.85297,1.13647.41382,2.35632.62231,3.65344.62231.63312,0,1.29547-.0448,1.99347-.13129.69641-.08801,1.39282-.24548,2.09076-.47552.69794-.22711,1.37732-.54205,2.03821-.94666.56677-.34424,1.10095-.77979,1.60278-1.30164l.24713-.26868-1.60742-1.94867c-.22858-.31659-.56055-.47565-1.0022-.47565-.3335,0-.65161.07721-.95422.2301-.30573.15131-.63928.32117-1.00214.508-.40002.20233-.81537.37213-1.24304.50958-.46484.15289-1.0191.22852-1.6615.22852-1.20441,0-2.18805-.34277-2.94928-1.02679-.71179-.63928-1.16888-1.64301-1.37274-3.01111l-.03864-.29797h10.68695c.25171,0,.45557-.02942.61609-.08649.16064-.05865.29803-.16986.38605-.31653.10651-.18835.1745-.39844.19611-.61456.03552-.25787.05408-.58984.05408-1.00061,0-1.31091-.20691-2.4845-.61768-3.51904-.37982-.98676-.96356-1.8808-1.71552-2.62354ZM70.25,20.81793c.19147-1.11176.58521-1.96411,1.17969-2.55396.59607-.59143,1.42365-.88635,2.4845-.88635.59448,0,1.10095.10028,1.51941.2995.39221.1792.73657.44629,1.00989.77985.25787.32428.44623.698.55438,1.09784.11267.41235.1698.83691.16827,1.26312h-6.91614Z" class="cls-1"></path> + <path d="M109.43842,15.815c-.75043-.72424-1.64606-1.28009-2.62811-1.6322-1.08087-.38452-2.22198-.57446-3.36926-.56049l-.00159-.00159c-1.41748,0-2.68982.24091-3.81555.72119-1.07928.44928-2.05829,1.11169-2.87665,1.94708-.7937.82147-1.4129,1.79431-1.82208,2.86121-.42316,1.08862-.63617,2.2406-.63617,3.45892,0,1.61365.2486,3.0296.74268,4.24786.49567,1.21832,1.16742,2.23438,2.01978,3.05273.85382.82147,1.8714,1.45148,2.98627,1.85297,1.17206.41998,2.40887.63153,3.65503.62231.63153,0,1.29395-.0448,1.99188-.13129.71027-.09113,1.41138-.2486,2.09229-.47406.69641-.22693,1.37433-.54352,2.03674-.94812.58673-.36279,1.12567-.79987,1.60126-1.30164l.2486-.26868-1.60895-1.94867c-.22705-.31659-.55902-.47412-1.00214-.47412-.33203,0-.65167.07568-.95435.23016-.30414.14972-.6377.31958-1.00208.508-.3999.20221-.81531.37207-1.24152.508-.46478.15289-1.0191.22705-1.6615.22705-1.20435,0-2.18805-.33978-2.94922-1.02533-.71344-.63928-1.17047-1.64301-1.37433-3.01111l-.03864-.29797h10.68848c.21002.0061.41846-.02319.61609-.08649.16064-.05865.29651-.16986.38452-.31653.10651-.18835.1745-.39844.19611-.61456.03857-.25787.05402-.58984.05402-1.00061,0-1.31091-.20691-2.4845-.61609-3.51904-.41071-1.03467-.98358-1.9101-1.71552-2.625ZM99.88489,20.81793c.18994-1.11176.58368-1.96259,1.17969-2.55396.59454-.5899,1.42224-.88483,2.48297-.88483.5976,0,1.10254.09875,1.51941.29645.41846.19922.75354.45868,1.01147.78137.25629.32117.44.68866.55432,1.09784.11273.41071.16833.82916.16833,1.26312h-6.9162Z" class="cls-1"></path> + <path d="M150.68982,16.13153c-.85083-.82153-1.86536-1.45459-2.97552-1.86066-1.15656-.43243-2.43671-.64856-3.84186-.64856-1.41901,0-2.7099.21613-3.87109.64856-1.11951.40607-2.14172,1.04071-3.00336,1.86066-.85236.82751-1.51788,1.8313-1.94714,2.94-.45856,1.15192-.68707,2.44897-.68707,3.88812,0,1.44989.22852,2.75781.68707,3.92358.45862,1.16278,1.1087,2.15094,1.94714,2.96631.86163.82617,1.88385,1.46545,3.00336,1.87616,1.16272.44,2.45209.6593,3.87109.6593,1.40515,0,2.68683-.2193,3.84186-.6593,1.11176-.41071,2.12622-1.04999,2.97552-1.87616.82764-.81537,1.4715-1.80353,1.93011-2.96631.46014-1.16425.68872-2.47369.68872-3.92358,0-1.43915-.22858-2.73621-.68872-3.88812-.42773-1.10559-1.08551-2.10773-1.93011-2.94ZM146.68591,27.02222c-.60065.8833-1.54108,1.32642-2.81348,1.32642-1.31091,0-2.27142-.44159-2.87823-1.32642-.6084-.88324-.91101-2.22504-.91101-4.02698,0-1.80212.30261-3.14392.91101-4.01947.6084-.87708,1.56732-1.31714,2.87823-1.31714,1.2724,0,2.21283.43848,2.81348,1.31714.60217.87555.90173,2.21735.90173,4.01947,0,1.80194-.29956,3.14374-.90173,4.02698Z" class="cls-1"></path> + <path d="M181.11993,28.2251c-.10809-.16376-.21155-.28259-.31348-.35822-.11737-.08185-.25781-.12201-.40149-.11584-.10651-.00153-.2146.01855-.31189.06183-.09113.04163-.1853.08795-.28717.1405-.26721.13892-.56677.20837-.86786.20227-.39221,0-.6933-.13898-.90021-.41235-.17908-.23615-.28259-.52655-.30731-.86768l-.00616-.17767v-9.04083h4.50421v-3.71979h-4.50421v-5.31805h-2.89679c-.61151,0-1.00214.23627-1.17352.70721l-.04016.13446-1.09167,4.44238-2.94769.56207v2.12164c0,.35199.10034.6192.30261.7998.16522.14984.37213.24097.59296.26416l.13892.00763h1.57349v9.40839c-.00928.74585.11121,1.48846.35828,2.19421.22083.62842.57904,1.19977,1.04535,1.67529.48022.47723,1.06079.84167,1.69702,1.07172.67474.25012,1.45148.37677,2.33313.37677.95428,0,1.87616-.13281,2.77173-.39532.7597-.21924,1.48083-.55438,2.14014-.99286l.27179-.19147-1.67999-2.58026Z" class="cls-1"></path> + <rect height="26.2749" width="5.53882" y="5.8291" x="85.4967" class="cls-1"></rect> + <path d="M163.83496,14.4469c-.80145.58051-1.4978,1.29083-2.05988,2.10455l-.22852.33356-.3042-1.70007c-.03864-.20374-.0957-.40302-.16986-.59753-.05865-.15448-.15753-.29041-.28571-.39539-.13739-.10498-.29645-.17908-.46472-.21918-.16064-.03864-.32587-.06024-.49103-.06488l-.19769-.00464h-3.28583v18.20068h5.54028v-11.16412c.8338-1.521,1.97803-2.28064,3.43103-2.28064.41846,0,.76434.02631,1.03766.07874.27332.05255.51111.08032.7149.08032.23785,0,.42932-.04938.57135-.14972.12506-.0943.2146-.22858.25323-.37994l.034-.13739.71338-4.00238c-.51422-.39838-1.19208-.5976-2.03821-.5976-1.00067,0-1.9256.2995-2.7702.89563Z" class="cls-1"></path> + <path d="M37.65826,25.22339l-3.83563-3.26593.07104-.44318c.08032-.59912.10657-1.22443.10657-1.8421,0-.76752-.04016-1.55652-.17761-2.28534l3.83563-3.26117.0849-.07104c.10193-.10034.17145-.23157.19763-.37213.02783-.14209.00928-.28876-.04938-.42004-.7829-2.36249-2.13397-4.61072-3.79541-6.47455-.23474-.26703-.56677-.36438-.87866-.22845l-4.83282,1.64905-.40149-.31036c-1.10034-.81836-2.30737-1.48224-3.58759-1.97339l-.95721-4.91333-.02777-.10193c-.04773-.13898-.1322-.26099-.24463-.35516-.11243-.09424-.24829-.15594-.39294-.17755l-.00171-.00159c-2.50476-.50024-5.08356-.50024-7.58813,0-.1626.02472-.31348.09888-.43256.21161-.11908.1142-.20026.26093-.23303.42303l-.95703,4.91345-.43433.17291c-1.28314.52502-2.48163,1.23529-3.55646,2.11078l-4.83154-1.65063-.10657-.03394c-.14117-.034-.28888-.02637-.42621.02008-.13745.04785-.25836.13275-.349.24542C2.19269,8.65173.84393,10.89996.06213,13.26245c-.06702.14825-.08032.31506-.03748.4726.04272.15747.13873.29639.27185.39062l3.83521,3.2627-.07214.44318c-.08032.59912-.10504,1.22766-.10504,1.84064-.01062.76587.04852,1.53027.17719,2.28528l-3.8335,3.26593-.08521.0694c-.10254.10187-.17175.23169-.19843.37366-.02655.14209-.0094.28882.04926.41998.7818,2.36407,2.13055,4.60931,3.79407,6.46991.23608.26874.56873.36597.88019.23169l4.83154-1.65228.35236.28418c1.11066.83691,2.33612,1.51172,3.63843,2.00116l.95703,4.9165.02631.10187c.04803.13745.13293.26105.24567.35529.11255.09412.2486.1543.39362.17755,1.25214.22852,2.50427.36279,3.79559.36279,1.28992,0,2.54211-.13428,3.79425-.36279.16223-.02637.31299-.1004.43188-.21313.1189-.11432.20044-.26093.23346-.42157l.95874-4.9165.47858-.19305c1.2652-.53424,2.4502-1.23834,3.51068-2.09229l4.83112,1.65228.10498.03552c.28107.07568.56519-.02783.77521-.26721,1.66455-1.8606,3.01251-4.10583,3.79388-6.46991.06793-.14819.08032-.31647.03857-.47406-.04315-.15741-.13892-.29486-.27173-.38898ZM18.97711,31.17743c-6.36896,0-11.53162-5.15271-11.53162-11.50836,0-6.35529,5.159-11.5094,11.53076-11.50989-.00031,0-.00055-.00006-.00085-.00006h.00171c-.00031,0-.00055.00006-.00085.00006,6.37018.00049,11.53271,5.1546,11.53271,11.50989,0,6.35565-5.16296,11.50836-11.53186,11.50836Z" class="cls-1"></path> + <path d="M18.97693,11.80157c-4.34424.00031-7.87878,3.52948-7.87878,7.86707s3.53455,7.86639,7.87909,7.86639,7.87903-3.52881,7.87903-7.86639-3.53479-7.86676-7.87933-7.86707ZM18.97723,24.29132c-2.55603,0-4.63538-2.07361-4.63538-4.62268,0-2.54938,2.07904-4.62329,4.6344-4.62329,2.55634,0,4.63629,2.07391,4.63629,4.62329,0,2.54907-2.07928,4.62268-4.63531,4.62268Z" class="cls-1"></path> +</svg> \ No newline at end of file diff --git a/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-1.png b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-1.png new file mode 100644 index 00000000..5477a9b1 Binary files /dev/null and b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-1.png differ diff --git a/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-2.png b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-2.png new file mode 100644 index 00000000..433ffb96 Binary files /dev/null and b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-2.png differ diff --git a/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-3.png b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-3.png new file mode 100644 index 00000000..dc7aeaa2 Binary files /dev/null and b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-3.png differ diff --git a/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-4.png b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-4.png new file mode 100644 index 00000000..4a194071 Binary files /dev/null and b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-4.png differ diff --git a/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-5.png b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-5.png new file mode 100644 index 00000000..682f70b0 Binary files /dev/null and b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-5.png differ diff --git a/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-6.png b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-6.png new file mode 100644 index 00000000..98f1655f Binary files /dev/null and b/static/assets/rds-privatelink-monitoring/rds-privatelink-monitoring-6.png differ diff --git a/static/assets/thumbnails/dblab-4.1-blog.png b/static/assets/thumbnails/dblab-4.1-blog.png new file mode 100644 index 00000000..52f6a168 Binary files /dev/null and b/static/assets/thumbnails/dblab-4.1-blog.png differ diff --git a/static/supabase/index.html b/static/supabase/index.html new file mode 100644 index 00000000..f1fb6989 --- /dev/null +++ b/static/supabase/index.html @@ -0,0 +1,258 @@ +<!DOCTYPE html> +<html lang="en" data-theme="system"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<meta name="color-scheme" content="light dark"> +<title>Postgres.AI for Supabase + + + + +
+
+ Postgres.AI × Supabase +
+ + + +
+
+
+ +
+
+

Improve the health of your Supabase Postgres

+

Find and fix what's slowing your database down — continuous health checks and a prioritized list of what to act on, from the team that helps keep Postgres healthy for companies like GitLab, Gadget, Suno, WorkOS, and Orb.

+

Included at no extra charge — covered by your Supabase plan, offered through the Supabase × Postgres.AI partnership.

+
+
+ +
+ +
+

What you get

+
+
DB Health Matrix
Your database's health across settings, bloat, indexes, locks and workload — tracked over time.
+
Actionable issues
Prioritized findings with a detailed action plan — ready to execute by your team or an AI agent. Not just charts.
+
Expert dashboards
Active Session History, query analysis, locks, WAL, autovacuum, I/O and more.
+
Expertise on tap
The Postgres.AI team reviews your database's health and answers your questions — helping you postpone or avoid hiring a dedicated DBA.
+
+
+ +
+

What's collected

+

Can you see my data? No. Monitoring is read-only and metadata-only — we never read the contents of your tables.

+
    +
  • A least-privilege user postgres_ai_mon with the built-in pg_monitor role — reads system catalogs and statistics, not your rows.
  • +
  • Collected: server settings, schema/index metadata, query statistics (pg_stat_statements), wait events, and size/bloat estimates.
  • +
  • Light footprint (observer effect): collection queries are time-bounded — statement_timeout ≈ 15s, lock_timeout = 100ms, so they never block your workload. We rely on pg_stat_statements and built-in stat views you most likely already run — nothing extra to install, negligible added overhead.
  • +
  • No query values. Query text comes from pg_stat_statements in normalized form — literal values are never captured.
  • +
  • You choose where it's stored. Collected metadata is stored in an AWS region of your choice.
  • +
  • Open source — inspect everything. The setup is open source: it creates the monitoring user, sets permissions, and adds two read-only helper functions (explain_generic, table_describe). Review it before anything runs: npx pgai@latest prepare-db --print-sql
  • +
+ +
+ +
+

See it in action

+
+
+
PostgresAI for Supabase — a quick tour of health checks, actionable issues, and expert dashboards.
+
+
+ DB Health Matrix +
DB Health Matrix — health across categories, tracked over 10 days / 4 weeks / 3 months.
+
+
+ Issue detail with before/after +
Inside an issue — evidence, before/after, and the fix.
+
+
+ Actionable issues list +
Actionable issues — prioritized findings, each with a checklist.
+
+
+ Active Session History dashboard +
Active Session History — time spent by waiting on each wait event.
+
+
+ Query performance analysis dashboard +
Query performance analysis — top queries by calls & time.Live demo of the dashboards (Grafana only): demo.postgres.ai — login demo / demo.
+
+
+ +
+

How to get started

+
    +
  1. Review & decide

    See exactly what we access — metadata only, never your data — and give go / no-go. Your security team can run a full audit: documentation · source code.

  2. +
  3. Approve & install

    Give consent to your Supabase contact, pick the database to monitor and click Install — setup runs automatically and you'll get an email invite.

  4. +
  5. Get results

    First health results appear in the Console within ~30 minutes, with deeper findings and trends over the following days.

  6. +
+
+ +
+ + + + + + + + \ No newline at end of file