自建类 Supabase Serverless 平台:K8s + Knative + PostgreSQL 完全本地化方案

背景

Supabase 是 Firebase 的开源替代,但生产环境中你需要:

  • 数据不出机房:所有组件跑在自己的 K8s 集群内
  • 零依赖外部服务:不依赖 Supabase 官方托管、不上云
  • Serverless 弹缩:函数按请求扩缩容,闲时缩零节省资源
  • SQL 即 API:写 PostgreSQL 查询自动暴露 REST 接口

本文基于已部署的 3 节点 K8s 1.34 集群(Ubuntu 24.04 ARM64、containerd、Calico eBPF),实现完全自建的 Serverless 应用平台。

总体架构

用户请求 (HTTPS)
    │
    ▼
Kourier Ingress (Knative 网络层)
    │  域名路由、TLS 终止、自动缩零
    ▼
Knative Serving (Serverless 运行时)
    │  冷启动、流量灰度、版本管理
    ▼
┌──────────────────────────┐
│  自定义函数容器 (Deno)     │  ← Serverless 函数(缩零)
└──────────────────────────┘
    │ REST API 调用
    ▼
PostgREST ────▶ PostgreSQL  ────▶ MinIO
(SQL→REST)    (主数据库)       (S3 对象存储)

全部组件运行在 K8s 集群内,不依赖任何外部服务。

组件清单

组件 角色 最小资源 容器镜像
PostgreSQL 17 主数据库 512MB 内存 postgres:17-alpine
PostgREST 12 SQL → REST API 128MB postgrest/postgrest:v12.2
Knative Serving 1.16 Serverless 运行时 200MB gcr.io/knative-releases/*
Kourier 1.16 Knative 网络层 100MB gcr.io/knative-releases/*
MinIO S3 兼容对象存储 256MB minio/minio:latest
自定义函数 业务逻辑 128MB 自定义

总计增量开销:集群已有基础上增加约 1.5GB,现有 3 节点(Master 4GB + 2 Worker 各 2GB)完全够用。

第一步:部署 PostgreSQL(集群内自建)

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pg-data
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 10Gi
---
apiVersion: v1
kind: Secret
metadata:
  name: pg-secret
stringData:
  POSTGRES_PASSWORD: "your-strong-password"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    spec:
      containers:
      - name: postgres
        image: postgres:17-alpine
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: pg-secret
              key: POSTGRES_PASSWORD
        - name: POSTGRES_DB
          value: appdb
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: pg-data
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  ports:
  - port: 5432
  selector:
    app: postgres

初始化 Schema

-- 用户表
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- 项目表
CREATE TABLE projects (
  id SERIAL PRIMARY KEY,
  user_id INT REFERENCES users(id),
  name TEXT NOT NULL,
  data JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT now()
);

-- 文件表
CREATE TABLE files (
  id SERIAL PRIMARY KEY,
  project_id INT REFERENCES projects(id),
  filename TEXT NOT NULL,
  size BIGINT,
  storage_path TEXT,
  uploaded_at TIMESTAMPTZ DEFAULT now()
);

关键配置:wal_level = logical

PostgreSQL 需要开启逻辑复制以支持实时订阅:

# 在 Deployment 的 args 中添加
args:
- "-c"
- "wal_level=logical"
- "-c"
- "max_replication_slots=5"
- "-c"
- "max_wal_senders=5"

这不影响普通查询性能,仅增加少量 WAL 写入(约 5-10%)。

第二步:部署 PostgREST(SQL 即 API)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgrest
spec:
  replicas: 2
  selector:
    matchLabels:
      app: postgrest
  template:
    spec:
      containers:
      - name: postgrest
        image: postgrest/postgrest:v12.2
        ports:
        - containerPort: 3000
        env:
        - name: PGRST_DB_URI
          value: "postgres://authenticator:password@postgres:5432/appdb"
        - name: PGRST_DB_SCHEMAS
          value: "public"
        - name: PGRST_DB_ANON_ROLE
          value: "anonymous"
        - name: PGRST_JWT_SECRET
          value: "your-jwt-secret-min-32-chars"
        - name: PGRST_SERVER_PORT
          value: "3000"
---
apiVersion: v1
kind: Service
metadata:
  name: postgrest
spec:
  ports:
  - port: 3000
  selector:
    app: postgrest

PostgreSQL 角色设置

-- 创建认证角色
CREATE ROLE authenticator NOINHERIT LOGIN PASSWORD 'password';
CREATE ROLE anonymous NOLOGIN;

-- anonymous 角色只读权限
GRANT anonymous TO authenticator;
GRANT USAGE ON SCHEMA public TO anonymous;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anonymous;

效果

部署后,直接通过 REST 访问数据库:

# 获取所有用户(无需编写任何 API 代码!)
curl http://postgrest:3000/users

# 带过滤的查询
curl "http://postgrest:3000/users?email=eq.admin@example.com"

# 跨表关联查询
curl "http://postgrest:3000/projects?select=id,name,users(email)"

PostgREST 自动将 PostgreSQL 的表/视图/函数暴露为 RESTful API,支持:

  • 增删改查 CRUD
  • 嵌套资源关联
  • 分页、排序、过滤
  • JWT 认证
  • OpenAPI 自动文档

第三步:部署 Knative(Serverless 基座)

# 1. Knative Serving CRDs
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.16.0/serving-crds.yaml

# 2. Knative Serving 核心
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.16.0/serving-core.yaml

# 3. Kourier 网络层(比 Istio 轻 10 倍)
kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.16.0/kourier.yaml

# 4. 设为默认网络层
kubectl patch configmap/config-network -n knative-serving \
  --type merge -p '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'

# 5. 配置 Kourier 为 LoadBalancer(配合 MetalLB 可用 VIP)
kubectl patch svc kourier -n kourier-system \
  -p '{"spec":{"type":"NodePort","ports":[{"name":"http2","port":80,"nodePort":30081}]}}'

Knative 核心概念

Knative Service = Deployment + Service + Ingress + Autoscaler 的声明式封装

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: hello-world
spec:
  template:
    spec:
      containers:
      - image: denoland/deno:alpine-2.1.9
        command: ["deno", "run", "-A", "--watch", "main.ts"]

一个 YAML 搞定:容器打包、路由、扩缩容、缩零、流量管理。

第四步:编写第一个 Serverless 函数

// main.ts - Deno 函数,查询 PostgreSQL
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";

const PG_URL = "http://postgrest:3000";

serve(async (req: Request) => {
  const url = new URL(req.url);

  // GET /users → 查询所有用户
  if (url.pathname === "/users" && req.method === "GET") {
    const res = await fetch(`${PG_URL}/users`, {
      headers: { "Accept": "application/json" }
    });
    return new Response(await res.text(), {
      headers: { "Content-Type": "application/json" }
    });
  }

  // POST /users → 创建用户
  if (url.pathname === "/users" && req.method === "POST") {
    const body = await req.json();
    const res = await fetch(`${PG_URL}/users`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Prefer": "return=representation"
      },
      body: JSON.stringify(body)
    });
    return new Response(await res.text(), {
      status: res.status,
      headers: { "Content-Type": "application/json" }
    });
  }

  return new Response("Not Found", { status: 404 });
});
# Dockerfile
FROM denoland/deno:alpine-2.1.9
WORKDIR /app
COPY main.ts .
CMD ["deno", "run", "-A", "main.ts"]

部署为 Knative Service

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: my-api
spec:
  template:
    spec:
      containers:
      - image: my-registry/my-api:v1
        env:
        - name: DENO_ENV
          value: production

Knative 自动提供的能力

特性 零配置
域名 my-api.default.10.211.55.6.sslip.io
扩缩容 请求量驱动,默认 0 → N
缩零 60 秒无请求自动缩到 0
冷启动 eBPF 加速,300-800ms
流量灰度 kubectl apply 新版本自动分流

第五步:部署 MinIO(S3 存储)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio
spec:
  replicas: 1
  selector:
    matchLabels:
      app: minio
  template:
    spec:
      containers:
      - name: minio
        image: minio/minio:latest
        args: ["server", "/data", "--console-address", ":9001"]
        ports:
        - containerPort: 9000
          name: api
        - containerPort: 9001
          name: console
        env:
        - name: MINIO_ROOT_USER
          value: "minioadmin"
        - name: MINIO_ROOT_PASSWORD
          value: "minioadmin123"
        volumeMounts:
        - name: data
          mountPath: /data
      volumes:
      - name: data
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: minio
spec:
  ports:
  - port: 9000
    name: api
  - port: 9001
    name: console
  selector:
    app: minio

第六步:实时订阅(Supabase Realtime 替代)

Supabase 的实时订阅基于 PostgreSQL 逻辑复制。自建方案使用 WAL 监听:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: realtime
spec:
  template:
    spec:
      containers:
      - image: my-realtime-server:v1   # 监听 PG WAL ,推送 WebSocket
        env:
        - name: DATABASE_URL
          value: "postgres://appuser:password@postgres:5432/appdb"

核心逻辑(伪代码):

// 1. 连接 PG logical replication slot
const slot = await pg.createReplicationSlot("realtime_slot");

// 2. 监听 WAL 变更
slot.on("change", (change) => {
  // 3. 推送到 WebSocket 客户端
  wsServer.clients.forEach(client => {
    client.send(JSON.stringify({
      table: change.table,
      action: change.action,  // INSERT / UPDATE / DELETE
      data: change.new
    }));
  });
});

完整拓扑

┌─────────────────────────────────────────────────────────────────┐
│                        K8s 集群 (3 节点)                         │
│                                                                   │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                   │
│  │ Master   │    │ Worker1  │    │ Worker2  │                   │
│  │ 4GB      │    │ 2GB      │    │ 2GB      │                   │
│  └──────────┘    └──────────┘    └──────────┘                   │
│       │               │               │                         │
│       └───────────────┼───────────────┘                         │
│                       │                                         │
│    ┌──────────────────┼──────────────────┐                      │
│    │         Calico eBPF Network          │                      │
│    └──────────────────┼──────────────────┘                      │
│                       │                                         │
│  ┌────────────────────┼────────────────────┐                    │
│  │         Kourier Ingress :30081           │                    │
│  │         Knative Serving                  │                    │
│  │  ┌─────────┐ ┌──────────┐ ┌──────────┐ │                    │
│  │  │ my-api  │ │ realtime │ │ uploader │ │  ← Serverless      │
│  │  │ (Deno)  │ │  (WS)    │ │ (Deno)   │ │    Functions        │
│  │  └────┬────┘ └────┬─────┘ └────┬─────┘ │                    │
│  │       │           │            │        │                    │
│  │       ▼           ▼            ▼        │                    │
│  │  ┌────────┐ ┌──────────┐ ┌─────────┐   │                    │
│  │  │PostgRST│ │PostgreSQL│ │ MinIO   │   │  ← 数据层          │
│  │  │  :3000 │ │  :5432   │ │ :9000   │   │                    │
│  │  └────────┘ └──────────┘ └─────────┘   │                    │
│  └─────────────────────────────────────────┘                    │
└─────────────────────────────────────────────────────────────────┘

外部访问: http://10.211.55.x:30081/my-api

资源预算

组件 副本 内存/副本 总内存
PostgreSQL 17 1 512MB 512MB
PostgREST 12 2 128MB 256MB
Knative Serving 1 200MB 200MB
Kourier 2 100MB 200MB
MinIO 1 256MB 256MB
Serverless 函数 0-3 128MB 0-384MB
合计(闲时) 约 1.4GB
合计(3 函数活跃) 约 1.8GB

现有 Master 4GB + 2 Worker 各 2GB,总可用约 6GB(扣除系统开销),跑这套方案绰绰有余。

与 Supabase 官方对比

能力 Supabase 官方 本方案
数据库 托管 PG 自建 PG 17
REST API PostgREST(同) PostgREST 12
认证 GoTrue JWT + PostgREST(可选 GoTrue)
实时订阅 Supabase Realtime WAL listener + WebSocket
存储 S3 MinIO
Edge Functions Deno Deploy Deno + Knative
缩零 ✅ Knative
冷启动 ~200ms ~500ms (eBPF)
数据留存 云端 本地,完全掌握
成本 按量计费 无额外成本
离线使用 ✅ 完全离线可用

优势

  1. 完全自建:不依赖任何云服务商或外部 API
  2. 数据主权:所有数据留存在你的 K8s 集群内
  3. Serverless 原生:函数按需运行,闲时零资源消耗
  4. SQL 即 API:定义 schema 即有 REST 接口,零样板代码
  5. eBPF 网络:已部署的 Calico eBPF 直接加速 Knative 冷启动
  6. 可扩展:每个 Knative Service 独立扩缩,互不影响

快速开始(基于现有 K8s 集群)

# 1. 部署 PostgreSQL
kubectl apply -f postgres.yaml

# 2. 部署 PostgREST
kubectl apply -f postgrest.yaml

# 3. 安装 Knative + Kourier
kubectl apply -f serving-crds.yaml
kubectl apply -f serving-core.yaml
kubectl apply -f kourier.yaml

# 4. 部署 MinIO
kubectl apply -f minio.yaml

# 5. 编写并部署第一个函数
docker build -t my-api:v1 .
kubectl apply -f knative-service.yaml

# 6. 访问
curl http://10.211.55.6:30081/my-api/users

本文是 K8s 1.34 集群部署实录 的进阶篇。集群部署完成后,可直接基于本文构建 Serverless 应用平台。