Files
neon/docs/rfcs/024-extension-loading.md
Clarence 3d1b08496a Update words in docs for better readability (#6600)
## Problem
 Found typos while reading the docs

## Summary of changes
Fixed the typos found
2024-02-03 00:59:39 +00:00

8.0 KiB

Supporting custom user Extensions (Dynamic Extension Loading)

Created 2023-05-03

Motivation

There are many extensions in the PostgreSQL ecosystem, and not all extensions are of a quality that we can confidently support them. Additionally, our current extension inclusion mechanism has several problems because we build all extensions into the primary Compute image: We build the extensions every time we build the compute image regardless of whether we actually need to rebuild the image, and the inclusion of these extensions in the image adds a hard dependency on all supported extensions - thus increasing the image size, and with it the time it takes to download that image - increasing first start latency.

This RFC proposes a dynamic loading mechanism that solves most of these problems.

Summary

compute_ctl is made responsible for loading extensions on-demand into the container's file system for dynamically loaded extensions, and will also make sure that the extensions in shared_preload_libraries are downloaded before the compute node starts.

Components

compute_ctl, PostgreSQL, neon (extension), Compute Host Node, Extension Store

Requirements

Compute nodes with no extra extensions should not be negatively impacted by the existence of support for many extensions.

Installing an extension into PostgreSQL should be easy.

Non-preloaded extensions shouldn't impact startup latency.

Uninstalled extensions shouldn't impact query latency.

A small latency penalty for dynamically loaded extensions is acceptable in the first seconds of compute startup, but not in steady-state operations.

Proposed implementation

On-demand, JIT-loading of extensions

Before postgres starts we download

  • control files for all extensions available to that compute node;
  • all shared_preload_libraries;

After postgres is running, compute_ctl listens for requests to load files. When PostgreSQL requests a file, compute_ctl downloads it.

PostgreSQL requests files in the following cases:

  • When loading a preload library set in local_preload_libraries
  • When explicitly loading a library with LOAD
  • When creating extension with CREATE EXTENSION (download sql scripts, (optional) extension data files and (optional) library files)))

Summary

Pros:

  • Startup is only as slow as it takes to load all (shared_)preload_libraries
  • Supports BYO Extension

Cons:

  • O(sizeof(extensions)) IO requirement for loading all extensions.

Alternative solutions

  1. Allow users to add their extensions to the base image

    Pros:

    • Easy to deploy

    Cons:

    • Doesn't scale - first start size is dependent on image size;
    • All extensions are shared across all users: It doesn't allow users to bring their own restrictive-licensed extensions
  2. Bring Your Own compute image

    Pros:

    • Still easy to deploy
    • User can bring own patched version of PostgreSQL

    Cons:

    • First start latency is O(sizeof(extensions image))
    • Warm instance pool for skipping pod schedule latency is not feasible with O(n) custom images
    • Support channels are difficult to manage
  3. Download all user extensions in bulk on compute start

    Pros:

    • Easy to deploy
    • No startup latency issues for "clean" users.
    • Warm instance pool for skipping pod schedule latency is possible

    Cons:

    • Downloading all extensions in advance takes a lot of time, thus startup latency issues
  4. Store user's extensions in persistent storage

    Pros:

    • Easy to deploy
    • No startup latency issues
    • Warm instance pool for skipping pod schedule latency is possible

    Cons:

    • EC2 instances have only limited number of attachments shared between EBS volumes, direct-attached NVMe drives, and ENIs.
    • Compute instance migration isn't trivially solved for EBS mounts (e.g. the device is unavailable whilst moving the mount between instances).
    • EBS can only mount on one instance at a time (except the expensive IO2 device type).
  5. Store user's extensions in network drive

    Pros:

    • Easy to deploy
    • Few startup latency issues
    • Warm instance pool for skipping pod schedule latency is possible

    Cons:

    • We'd need networked drives, and a lot of them, which would store many duplicate extensions.
    • UNCHECKED: Compute instance migration may not work nicely with networked IOs

Idea extensions

The extension store does not have to be S3 directly, but could be a Node-local caching service on top of S3. This would reduce the load on the network for popular extensions.

Extension Storage implementation

The layout of the S3 bucket is as follows:

5615610098 // this is an extension build number
├── v14
│   ├── extensions
│   │   ├── anon.tar.zst
│   │   └── embedding.tar.zst
│   └── ext_index.json
└── v15
    ├── extensions
    │   ├── anon.tar.zst
    │   └── embedding.tar.zst
    └── ext_index.json
5615261079
├── v14
│   ├── extensions
│   │   └── anon.tar.zst
│   └── ext_index.json
└── v15
    ├── extensions
    │   └── anon.tar.zst
    └── ext_index.json
5623261088
├── v14
│   ├── extensions
│   │   └── embedding.tar.zst
│   └── ext_index.json
└── v15
    ├── extensions
    │   └── embedding.tar.zst
    └── ext_index.json

Note that build number cannot be part of prefix because we might need extensions from other build numbers.

ext_index.json stores the control files and location of extension archives. It also stores a list of public extensions and a library_index

We don't need to duplicate `extension.tar.zst`` files. We only need to upload a new one if it is updated. (Although currently we just upload every time anyways, hopefully will change this sometime)

access is controlled by spec

More specifically, here is an example ext_index.json

{
    "public_extensions": [
        "anon",
        "pg_buffercache"
    ],
    "library_index": {
        "anon": "anon",
        "pg_buffercache": "pg_buffercache"
        // for more complex extensions like postgis
        // we might have something like:
        // address_standardizer: postgis
        // postgis_tiger: postgis
    },
    "extension_data": {
        "pg_buffercache": {
            "control_data": {
                "pg_buffercache.control": "# pg_buffercache extension \ncomment = 'examine the shared buffer cache' \ndefault_version = '1.3' \nmodule_pathname = '$libdir/pg_buffercache' \nrelocatable = true \ntrusted=true"
            },
            "archive_path": "5670669815/v14/extensions/pg_buffercache.tar.zst"
        },
        "anon": {
            "control_data": {
                "anon.control": "# PostgreSQL Anonymizer (anon) extension \ncomment = 'Data anonymization tools' \ndefault_version = '1.1.0' \ndirectory='extension/anon' \nrelocatable = false \nrequires = 'pgcrypto' \nsuperuser = false \nmodule_pathname = '$libdir/anon' \ntrusted = true \n"
            },
            "archive_path": "5670669815/v14/extensions/anon.tar.zst"
        }
    }
}

How to add new extension to the Extension Storage?

Simply upload build artifacts to the S3 bucket. Implement a CI step for that. Splitting it from compute-node-image build.

How do we deal with extension versions and updates?

Currently, we rebuild extensions on every compute-node-image build and store them in the prefix. This is needed to ensure that /share and /lib files are in sync.

For extension updates, we rely on the PostgreSQL extension versioning mechanism (sql update scripts) and extension authors to not break backwards compatibility within one major version of PostgreSQL.

Alternatives

For extensions written on trusted languages we can also adopt dbdev PostgreSQL Package Manager based on pg_tle by Supabase. This will increase the amount supported extensions and decrease the amount of work required to support them.