标签 PostgreSQL 下的文章

这两年数据库圈有点像3年前的云原生圈:"分布式"、"新一代内核"、"重构存储引擎"这些词突然又密集起来了。

前几天刷群,看到有人转了 OpenTeleDB 的开源消息,说是"基于 PostgreSQL 的新一代内核"。说实话,我第一反应是:又一个魔改 PG?

但看到里面提到一个点:原位更新 + Undo 引擎(XStore),我还是没忍住下了源码。 因为这恰好戳中我这些年被 PG 折磨得最狠的痛点:

表膨胀、autovacuum 抽风、性能像心电图一样忽高忽低。

所以这次我没看 PPT,也没看宣传稿,直接跑到机器上拆了半天,想看看它究竟动了 PG 的哪根"老筋"。

一、先说结论:XStore 不是快,而是"稳"

image.png

我装的是 OpenTeleDB 的 17.6 内核版。 创建方式很直观:

SELECT relname, amname
FROM pg_class c
JOIN pg_am a ON c.relam = a.oid
WHERE relname = 'test_xstore';

image.png

这一步其实就已经很有意思了------它不是 fork 了一套新引擎,而是作为插件挂进去的。 这个思路我很认可:

  • 不绑死 PG 版本
  • 能跟着大版本升级
  • 出问题可以随时回退

像 Citus、openHalo 这些"成功插件化路线"的项目,本质都是这个思路。

image.png

二、打开数据目录,我第一次意识到:它真不是换皮

$PGDATA 下面,多了一个非常显眼的目录:

drwx------ 2 postgres postgres  4096 Nov  3 20:15 undo

这就是 XStore 的核心: 它不是靠多版本链来维护 MVCC,而是靠 Undo 日志回滚。

这点和 Oracle、MySQL InnoDB 的逻辑更像。

也正是它敢说"原位更新"的底气来源。

三、插入测试:它不快,但很"诚实"

我用同样的参数,在同一台机器上跑了两组:

INSERT INTO test_xstore (name, value)
SELECT md5(random()::text), (random()*1000)::int
FROM generate_series(1,10000000);
INSERT INTO test_heap (name, value)
SELECT md5(random()::text), (random()*1000)::int
FROM generate_series(1,10000000);

image.png

结果是:

image.png

写慢了将近一倍。这点我反而觉得真实:因为 XStore 在写数据页的同时,还要写一份 Undo。物理写入翻倍,吞吐下降是必然的。如果一个系统告诉你"原位更新 + Undo 还更快",那我反而会不太信。

四、创新实验:模拟1千万数据的存储膨胀对比

我设计了一项创新实验:在 1000 万条级别的大数据量下,评估 XStore 与 Heap 表在高频更新下的空间膨胀、索引稳定性以及查询性能表现。该实验主要有两个创新点:

大规模数据模拟

  1. 使用 generate_series(1,10000000) 生成 1000 万条数据,保证数据量级对存储膨胀影响明显。
  2. 初始数据包括 idnamevalueupdated_at 四列,与前期实验一致,但数据量增加十倍,以模拟真实大规模 OLTP 系统负载。

多维度空间分析

  1. 不仅监控表总大小,还分别统计索引占用和 TOAST 表空间。
  2. 每轮更新后,通过 pg_relation_sizepg_total_relation_sizepg_indexes_size 获取精细化指标。
  3. 引入 可视化趋势分析,绘制表空间增长曲线,以直观展示 XStore 与 Heap 的差异。

image.png

4.1 实验设计

表结构

CREATE TABLE xstore_large (
    id SERIAL PRIMARY KEY,
    name TEXT,
    value INT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) USING XSTORE;

CREATE TABLE heap_large (
    id SERIAL PRIMARY KEY,
    name TEXT,
    value INT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

image.png

初始化 1000 万条数据

INSERT INTO xstore_large (name, value)
SELECT 'name_' || g, g
FROM generate_series(1, 10000000) AS g;

INSERT INTO heap_large (name, value)
SELECT 'name_' || g, g
FROM generate_series(1, 10000000) AS g;

image.png

先对现在存入1000w数据的空间监控与记录一下如下。

image.png

SELECT
    pg_size_pretty(pg_total_relation_size('xstore_large')) AS xstore_total,
    pg_size_pretty(pg_indexes_size('xstore_large')) AS xstore_index,
    pg_size_pretty(pg_total_relation_size('heap_large')) AS heap_total,
    pg_size_pretty(pg_indexes_size('heap_large')) AS heap_index;
 xstore_total | xstore_index | heap_total | heap_index
--------------+--------------+------------+------------
 985 MB       | 388 MB       | 789 MB     | 214 MB

多轮全表更新

  • 连续 5 轮更新,每轮更新 valueupdated_at,模拟写入密集场景:
UPDATE xstore_large
SET value = value + 1, updated_at = CURRENT_TIMESTAMP;

UPDATE heap_large
SET value = value + 1, updated_at = CURRENT_TIMESTAMP;

image.png

空间监控与记录

SELECT
    pg_size_pretty(pg_total_relation_size('xstore_large')) AS xstore_total,
    pg_size_pretty(pg_indexes_size('xstore_large')) AS xstore_index,
    pg_size_pretty(pg_total_relation_size('heap_large')) AS heap_total,
    pg_size_pretty(pg_indexes_size('heap_large')) AS heap_index;

第一轮:

image.png

 xstore_total | xstore_index | heap_total | heap_index
--------------+--------------+------------+------------
 985 MB       | 388 MB       | 1578 MB    | 428 MB

第五轮:

image.png

 xstore_total | xstore_index | heap_total | heap_index
--------------+--------------+------------+------------
 985 MB       | 388 MB       | 1628 MB    | 428 MB

4.2 千万数据更新膨胀可视化

image.png

image.png

五、实验结论

这组 1000 万级数据 + 多轮全表更新的实验,其实把 PG 传统 Heap 表的"老问题"放大得非常清楚。

最核心的对比结果只有一句话:

XStore 的空间是线性的、可预测的;Heap 表的空间是失控的、不可预测的。

具体来看:

  1. 表空间膨胀

a. Heap 表在第一次更新后,表体空间直接翻倍,从 789MB 飙到 1578MB。

b. 之后每一轮更新,虽然增长幅度趋缓,但空间再也回不到初始状态。

c. XStore 从头到尾不变: 985MB → 985MB → 985MB

  1. 索引体积稳定性

a. Heap 表索引从 214MB 膨胀到 428MB,且在后续更新中保持"高位横盘"。

b. XStore 的索引尺寸始终维持在 388MB 左右,没有明显漂移。

  1. 更新行为本质差异

a. Heap:每一次 UPDATE,本质都是 DELETE + INSERT → 老版本残留 → 表膨胀 → 索引碎片 → autovacuum 压力。

b. XStore:真正的原位更新 → 历史版本进 Undo → 主表物理页不变 → 无膨胀。

  1. 长期可运维性

a. 在 Heap 表上,如果你不 VACUUM,它一定会慢; 如果你 VACUUM,系统一定会抖。

b. 在 XStore 上,这两件事都不再是必选项。

这意味着什么?

它不是让你飞起来,而是让你不再塌方

六、我的心得

说实话,这几年我已经对"新一代数据库内核"这类说法有点免疫了。大多数项目,要么是在 PG 上糊一层分布式壳; 要么就是换个名字,重新卖一遍 MVCC。而 XStore 给我的感觉不一样。它没有试图掩盖代价。写入更慢, IO 更多,架构更复杂。

但它正面承认了一个事实:

PostgreSQL 的 MVCC,在高频更新场景下已经接近物理极限。

这不是参数调优能解决的事,也不是加机器能扛住的事,而是存储模型本身的问题。这些年我见过太多系统:白天 QPS 很稳,半夜 autovacuum 开始清垃圾,延迟突然拉长,业务报警,DBA 开始手工 VACUUM / REINDEX / CLUSTER,第二天继续循环。

这不是运维水平的问题,而是模型在和现实硬扛。XStore 让我第一次意识到:原来 PG 也可以选择不走这条老路。它没有追求"更快",而是选择了一个更难、但更稳的方向:

  • 用 Undo 换空间可控
  • 用写放大换性能平滑
  • 用工程复杂度换系统长期可预期性

如果你是写多、更新密集型 OLTP 系统,如果你被表膨胀、索引碎片、autovacuum 抽风折磨过,那你会和我一样---不一定立刻用它,但你会开始认真看它。这大概就是我这次拆源码、跑实验,最大的收获。

使用托管数据库部署 Coreflux MQTT 代理

MQTT 代理 通过发布-订阅消息模式连接物联网设备和应用程序,使其成为现代 物联网 基础设施的重要组成部分。Coreflux 是一个 低代码 MQTT 代理,增加了实时数据处理和转换功能,让你可以直接与 DigitalOcean 托管数据库(包括 MongoDBPostgreSQLMySQLOpenSearch)集成,而无需编写自定义集成代码。

你将学到什么: 本教程将引导你部署一个完整的物联网数据管道——从在 DigitalOcean 上设置托管数据库集群和 Coreflux MQTT 代理,到配置安全的 VPC 网络、使用 Coreflux 的物联网语言 (LoT) 构建数据转换模型,以及自动将处理后的物联网数据存储到你选择的数据库中。最终你将获得一个可用于生产环境的设置,能够处理物联网应用的实时消息传递和持久存储。

关键要点

在深入了解分步部署过程之前,以下是你将学到的关键点:

  • 在 DigitalOcean 上部署托管数据库集群(PostgreSQL、MongoDB、MySQL 或 OpenSearch),用于可扩展的物联网数据存储。
  • 使用 Marketplace 镜像或 Docker 在 DigitalOcean Droplet (DigitalOcean的VPC)上设置 Coreflux MQTT 代理。
  • 创建安全的 VPC 网络以连接你的 MQTT 代理和数据库,无需公开暴露。
  • 使用 Coreflux 的物联网语言 (LoT) 构建实时数据管道,实现低代码物联网自动化。
  • 自动转换和存储物联网数据,从 MQTT 主题到数据库表、集合或索引。
  • 验证端到端数据流,从模拟传感器通过转换模型到数据库存储。

本教程为需要实时消息传递结合持久数据存储以及搜索、分析或关系查询等高级功能的物联网应用提供了一个可用于生产环境的基础。

你将构建什么

在本教程结束时,你将得到:

  • 一个用于可扩展存储托管数据库集群(PostgreSQLMongoDBMySQLOpenSearch
  • 一台运行 Coreflux MQTT 代理DigitalOcean Droplet
  • 一个用于安全 物联网通信的虚拟私有云 (VPC) 网络
  • 使用 LoT Notebook 扩展进行的实时数据模拟
  • 低代码数据转换模型和数据库集成路由
  • 用于 物联网自动化 的完整 数据集成与转换 管道

Coreflux 与 DigitalOcean 合作

Coreflux 通过物联网语言编程语言在 DigitalOcean 云平台上提供轻量级 MQTT 代理和数据管道工具,以实现高效的物联网通信。

什么是 MQTT?

MQTT(消息队列遥测传输)是一种轻量级的、发布-订阅网络协议,在物联网生态系统中被广泛采用。专为受限设备和低带宽、高延迟或不稳定的网络设计,MQTT 能够在带宽受限的环境中实现高效、实时的消息传递。

关于 Coreflux

Coreflux 提供了一个轻量级 MQTT 代理,以促进物联网设备与应用程序之间的高效、实时通信,包括每个用例所必需的实时数据转换功能。为可扩展性和可靠性而构建,Coreflux 专为低延迟和高吞吐量至关重要的环境量身定制。

无论你是构建一个小型物联网项目还是部署工业监控系统,Coreflux 都能处理设备之间的消息路由和数据流。

在 DigitalOcean 云平台上使用 Coreflux,你将获得:

数据处理: 在你的数据所在之处集中处理你的数据处理需求,确保实时数据处理。
数据集成: 轻松与其他 DigitalOcean 服务(如托管数据库 PostgreSQL、MongoDB、MySQL 或 OpenSearch)集成,确保为你的所有数据需求提供一个单一且简单的生态系统。
可扩展性: 轻松处理不断增长的数据和设备数量,而不会影响性能。
可靠性: 确保在所有连接的设备之间进行一致且可靠的消息传递。

Coreflux MQTT 和托管数据库架构概述

准备工作

在开始本 MQTT 代理 部署教程之前,你需要:

  • 一个 DigitalOcean 帐户,可在DigitalOcean.com注册,支持绑定信用卡、支付宝或数字货币
  • 了解 MQTT 协议概念和 物联网 架构
  • Visual Studio Code(用于 LoT Notebook 扩展)

预计时间: 本教程大约需要 30-45 分钟完成,具体取决于数据库配置时间(通常每个数据库集群需要 1-5 分钟)。

步骤 1 — 为物联网自动化创建网络基础设施

为安全的 MQTT 通信创建 VPC 网络

首先,你将创建一个虚拟私有云 (VPC),以确保你的 物联网 服务和 MQTT 代理 之间的安全通信,无需公开访问。

  1. 登录你的 DigitalOcean 控制面板
  2. 从左侧导航栏进入 网络VPC
  3. 点击 创建 VPC 网络

DigitalOcean VPC 创建屏幕

  1. 物联网自动化配置你的 VPC:

    • 名称:coreflux-integrations-vpc(或你的 VPC 名称)
    • 数据中心区域:选择法兰克福(或你首选的区域)
    • IP 范围:使用默认值或根据需要配置
    • 描述:为你的 MQTT 代理和数据库 网络添加有意义的描述
  2. 点击 创建 VPC 网络

VPC 将为你所有的物联网资源提供隔离的网络,确保 Coreflux MQTT 代理托管数据库 之间的安全通信。有关 VPC 配置的更多详细信息,请参阅我们关于创建 VPC 网络的教程。

步骤 2 — 为可扩展存储设置托管数据库

根据你的物联网应用需求,选择以下数据库选项之一:

  • PostgreSQL:适用于需要关系查询、ACID 合规性和复杂关系的结构化数据
  • MySQL:适用于结构化工作负载和具有强一致性及广泛工具支持的事务性查询
  • MongoDB:适用于具有可变模式的灵活文档存储和快速开发
  • OpenSearch:适用于高级搜索、分析、日志分析和时间序列数据可视化

设置 PostgreSQL 托管数据库

当你的物联网工作负载需要关系模式强一致性高级 SQL 分析,并由自动备份、监控和维护支持时,DigitalOcean 上的托管 PostgreSQL 是一个很好的选择。

DigitalOcean 托管 PostgreSQL 集群设置

  1. DigitalOcean 控制面板,导航到 数据库
  2. 点击 创建数据库集群
  3. 物联网自动化配置你的 PostgreSQL 集群:

    • 数据库引擎:选择 PostgreSQL
    • 版本:选择最新的稳定版本
    • 数据中心区域:选择法兰克福(与你的 VPC 相同)
    • VPC 网络:选择你创建的 coreflux-integrations-vpc
    • 数据库集群名称:postgresql-coreflux-test
    • 项目:选择你的目标项目
  4. 根据你的 物联网 需求选择你的计划:

    1. 对于开发:基础 计划,1 GB RAM
    2. 对于生产:通用型 或更高,用于可扩展存储
  5. 点击 创建数据库集群

托管数据库 创建过程通常需要 1-5 分钟。完成后,你将被重定向到数据库概览页面,在那里你可以查看连接详细信息并执行管理操作。

为 MQTT 代理集成配置 PostgreSQL 数据库访问

系统将提示你进行入门步骤,显示你的连接详细信息,你可以配置入站访问规则(建议限制为你的 IP 和仅 VPC)。

  1. 点击 开始使用 来配置你的 PostgreSQL 数据库
  2. (可选操作)限制入站连接:

    • 添加你本地计算机的 IP 以进行管理访问
    • droplet 将通过 VPC 网络自动获得允许

PostgreSQL 入站访问和 VPC 规则

对于连接详细信息,你将看到两个选项 - 公共网络和 VPC 网络。第一个用于像 DBeaver 这样的工具进行外部访问,而第二个将由 Coreflux 服务用于访问数据库。

PostgreSQL 公共和 VPC 连接详细信息

  1. 记下提供的连接详细信息,包括公共访问和 VPC 访问(每种都有不同的详细信息):

    • 主机:你的数据库主机名
    • 用户:默认管理员用户
    • 密码:自动生成的安全密码
    • 数据库:身份验证数据库名称
测试 PostgreSQL 数据库连接

你可以使用提供的连接参数,使用公共访问凭证通过 DBeaver 测试 PostgreSQL 连接:

在 DBeaver 中测试 PostgreSQL 连接

创建 PostgreSQL 应用程序数据库和用户(可选)

为了更好的安全性和组织性,为你的 物联网自动化 应用程序创建一个专用用户和数据库。这也可以通过 DBeaver 或 CLI 完成,但 DigitalOcean 提供了一种用户友好的方法:

  1. 转到你的 托管数据库 集群中的 用户与数据库 选项卡
  2. 创建用户

    • 用户名:coreflux-broker-client
    • 密码:自动生成
  3. 创建数据库

    • 数据库名称:coreflux-broker-data

注意: 你可能需要更改数据库内的用户权限,以便能够创建表、插入和选择数据。对于 PostgreSQL,使用 GRANT CREATE, INSERT, SELECT ON DATABASE coreflux-broker-data TO coreflux-broker-client; 授予必要的权限。对于 MySQL,使用 GRANT CREATE, INSERT, SELECT ON coreflux-broker-data.* TO 'coreflux-broker-client'@'%';。

设置 MySQL 托管数据库

当你想要熟悉的 SQL、广泛的生态系统支持以及处理备份、更新和监控的完全托管服务时,DigitalOcean 上的托管 MySQL结构化、事务性物联网数据的理想选择。

DigitalOcean 托管 MySQL 集群设置

  1. DigitalOcean 控制面板,导航到 数据库
  2. 点击 创建数据库集群
  3. 物联网自动化配置你的 MySQL 集群:

    • 数据库引擎:选择 MySQL
    • 版本:选择最新的稳定版本
    • 数据中心区域:选择法兰克福(与你的 VPC 相同)
    • VPC 网络:选择你创建的 coreflux-integrations-vpc
    • 数据库集群名称:mysql-coreflux-test
    • 项目:选择你的目标项目
  4. 根据你的 物联网 需求选择你的计划:

    • 对于开发:基础 计划,1 GB RAM
    • 对于生产:通用型 或更高,用于可扩展存储
  5. 点击 创建数据库集群

托管数据库 创建过程通常需要 1-5 分钟。完成后,你将被重定向到数据库概览页面,在那里你可以查看连接详细信息并执行管理操作。

为 MQTT 代理集成配置 MySQL 数据库访问

系统将提示你进行入门步骤,显示你的连接详细信息,你可以配置入站访问规则(建议限制为你的 IP 和仅 VPC)。

  1. 点击 开始使用 来配置你的 MySQL 数据库
  2. (可选操作)限制入站连接:

    • 添加你本地计算机的 IP 以进行管理访问
    • droplet 将通过 VPC 网络自动获得允许

MySQL 入站访问和 VPC 规则

对于连接详细信息,你将看到两个选项 - 公共网络和 VPC 网络。第一个用于像 DBeaver 这样的工具进行外部访问,而第二个将由 Coreflux 服务用于访问数据库。

MySQL 公共和 VPC 连接详细信息

  1. 记下提供的连接详细信息,包括公共访问和 VPC 访问(每种都有不同的详细信息):

    • 主机:你的数据库主机名
    • 用户:默认管理员用户
    • 密码:自动生成的安全密码
    • 数据库:身份验证数据库名称
测试 MySQL 数据库连接

你可以使用提供的连接参数,使用公共访问凭证通过 DBeaver 测试 MySQL 连接。

注意: 你可能需要更改 DBeaver 的驱动程序设置——设置 allowPublicKeyRetrieval = true。

在 DBeaver 中测试 MySQL 连接

创建 MySQL 应用程序数据库和用户(可选)

为了更好的安全性和组织性,为你的 物联网自动化 应用程序创建一个专用用户和数据库。这也可以通过 DBeaver 或 CLI 完成,但 DigitalOcean 提供了一种用户友好的方法:

  1. 转到你的 托管数据库 集群中的 用户与数据库 选项卡
  2. 创建用户

    • 用户名:coreflux-broker-client
    • 密码:自动生成
  3. 创建数据库

    • 数据库名称:coreflux-broker-data

设置 MongoDB 托管数据库

托管 MongoDB 非常适合灵活或不断演变的物联网负载,让你能够存储异构的传感器文档,而无需严格模式,同时平台处理复制、备份和监控。

创建托管 MongoDB 集群

  1. DigitalOcean 控制面板,导航到 数据库
  2. 点击 创建数据库集群
  3. 物联网自动化配置你的 MongoDB 集群:

    • 数据库引擎:选择 MongoDB
    • 版本:选择最新的稳定版本
    • 数据中心区域:选择法兰克福(与你的 VPC 相同)
    • VPC 网络:选择你创建的 coreflux-integrations-vpc
    • 数据库集群名称:mongodb-coreflux-test
    • 项目:选择你的目标项目
  4. 根据你的 物联网 需求选择你的计划:

    • 对于开发:基础 计划,1 GB RAM
    • 对于生产:通用型 或更高,用于可扩展存储
  5. 点击 创建数据库集群

托管数据库 创建过程通常需要 1-5 分钟。完成后,你将被重定向到数据库概览页面,在那里你可以查看连接详细信息并执行管理操作。

为 MQTT 代理集成配置 MongoDB 数据库访问

系统将提示你进行入门步骤,显示你的连接详细信息,你可以配置入站访问规则(建议限制为你的 IP 和仅 VPC)。

  1. 点击 开始使用 来配置你的 MongoDB 数据库
  2. (可选)限制入站连接:

    • 添加你本地计算机的 IP 以进行管理访问
    • droplet 将通过 VPC 网络自动获得允许

为 MQTT 代理集成配置数据库访问

对于连接详细信息,你将看到两个选项:公共网络和 VPC 网络。第一个用于像 MongoDB Compass 这样的工具进行外部访问,而第二个将由 Coreflux 服务用于访问数据库。

MongoDB 连接详细信息

  1. 记下提供的连接详细信息,包括公共访问和 VPC 访问(每种都有不同的详细信息):

    • 主机:你的数据库主机名
    • 用户:默认管理员用户
    • 密码:自动生成的安全密码
    • 数据库:身份验证数据库名称
测试 MongoDB 数据库连接

你可以使用 MongoDB Compass 或提供的连接字符串,使用公共访问凭证测试 MongoDB 连接:

mongodb://username:password@mongodb-host:27017/defaultauthdb?ssl=true

测试数据库连接

创建 MongoDB 应用程序数据库和用户(可选)

为了更好的安全性和组织性,为你的 物联网自动化 应用程序创建一个专用用户和数据库。这也可以通过 MongoDB Compass 或 CLI 完成,但 DigitalOcean 提供了一种用户友好的方法:

  1. 转到你的 托管数据库 集群中的 用户与数据库 选项卡
  2. 创建用户

    • 用户名:coreflux-broker-client
    • 密码:自动生成
  3. 创建数据库

    • 数据库名称:coreflux-broker-data

设置 OpenSearch 托管数据库

托管 OpenSearch 专为高容量物联网数据的搜索、日志分析和时间序列仪表板而设计,该服务为你管理集群健康、扩展和索引存储。

创建托管 OpenSearch 集群

  1. DigitalOcean 控制面板,导航到 数据库
  2. 点击 创建数据库集群
  3. 物联网自动化配置你的 OpenSearch 集群:

    • 数据库引擎:选择 OpenSearch
    • 版本:选择最新的稳定版本
    • 数据中心区域:选择法兰克福(与你的 VPC 相同)
    • VPC 网络:选择你创建的 coreflux-integrations-vpc
    • 数据库集群名称:opensearch-coreflux-test
    • 项目:选择你的目标项目
  4. 根据你的 物联网 需求选择你的计划:

    1. 对于开发:基础 计划,1 GB RAM
    2. 对于生产:通用型 或更高,用于可扩展存储
  5. 点击 创建数据库集群

托管数据库 创建过程通常需要 1-5 分钟。完成后,你将被重定向到数据库概览页面,在那里你可以查看连接详细信息并执行管理操作。

为 MQTT 代理集成配置 OpenSearch 数据库访问

系统将提示你进行入门步骤,显示你的连接详细信息,你可以配置入站访问规则(建议限制为你的 IP 和仅 VPC)。

  1. 点击 开始使用 来配置你的 OpenSearch 数据库
  2. (可选)限制入站连接:

    • 添加你本地计算机的 IP 以进行管理访问
    • droplet 将通过 VPC 网络自动获得允许

配置数据库访问

对于连接详细信息,你将看到两个选项:公共网络和 VPC 网络。第一个用于工具的外部访问,而第二个将由 Coreflux 服务用于访问数据库。你还将看到访问 OpenSearch 仪表板的 URL 和参数。

连接详细信息

  1. 记下提供的连接详细信息,包括公共访问和 VPC 访问(每种都有不同的详细信息):

    • 主机:你的数据库主机名
    • 用户:默认管理员用户
    • 密码:自动生成的安全密码
    • 数据库:身份验证数据库名称
测试 OpenSearch 数据库连接

你可以使用提供的凭证通过 OpenSearch 仪表板测试 OpenSearch 连接:

测试数据库连接

步骤 3 — 在 DigitalOcean Droplet 上部署 Coreflux MQTT 代理

创建 DigitalOcean Droplet

  1. 在你的 DigitalOcean 控制面板中导航到 Droplets
  2. 点击 创建 Droplet

创建新的 DigitalOcean Droplet

  1. MQTT 代理 部署配置你的 droplet

    • 选择区域:法兰克福(与你的托管数据库相同)
    • VPC 网络:选择 coreflux-integrations-vpc
    • 选择镜像:转到 Marketplace 选项卡
    • 搜索 “Coreflux” 并从 Marketplace 中选择 Coreflux

从 Marketplace 选择 Coreflux

  1. 为你的 物联网 工作负载选择大小

    • 对于开发:基础 计划,2 GB 内存
    • 对于生产:基础通用型 计划,4+ GB 内存以获得可扩展性能
  2. 选择身份验证方法

    • SSH 密钥:推荐用于提高安全性

      1. 可以使用 ssh-keygen 在本地创建密钥
    • 密码:备选方案
  3. 最终确定详细信息

    • 主机名:coreflux-test-broker
    • 项目:选择你的项目
    • 标签:为 DevOps 组织添加相关标签
  4. 点击 创建 Droplet
  5. 查看 Droplet 主页并等待其完成部署

Droplet 部署进行中

替代方案 - 在Docker镜像Droplet上使用Docker安装Coreflux MQTT代理

采用与Coreflux Droplet相同的方法,选择Docker作为市场应用镜像。

一旦你的droplet运行起来,通过已定义的认证方法或Droplet主页上提供的Web控制台,使用SSH连接到它:

ssh root@your-droplet-ip

SSH连接到Coreflux droplet

使用Docker运行Coreflux MQTT代理

docker run -d \
  --name coreflux \
  -p 1883:1883 \
  -p 1884:1884 \
  -p 5000:5000 \
  -p 443:443 \
  coreflux/coreflux-mqtt-broker-t:1.6.3

这个Docker命令:

  • 以分离模式运行容器 (-d)
  • 将容器命名为 coreflux
  • 暴露MQTT和Web界面所需的端口
  • 使用最新的Coreflux镜像

验证MQTT代理是否在运行:

docker ps

你应该看到一个正在运行的容器:

Docker中运行的Coreflux容器

通过使用默认值连接到MQTT代理来验证部署

你可以通过MQTT客户端(如MQTT Explorer)访问MQTT代理,以验证对代理的访问,无论采用何种部署方法。

MQTT Explorer连接到Coreflux代理

步骤4 — 为安全的物联网通信配置防火墙规则(可选)

对于生产环境的物联网自动化部署,配置防火墙规则以限制访问:

  1. 导航到网络防火墙
  2. 点击创建防火墙
  3. 配置MQTT代理安全的入站规则:

    • SSH:来自你IP的端口22
    • MQTT:来自你的物联网应用程序源的端口1883
    • 带TLS的MQTT:用于安全的带TLS的MQTT的端口1884
    • WebSocket:用于通过WebSocket的MQTT的端口5000
    • 带TLS的WebSocket:用于通过带TLS的WebSocket的MQTT的端口443
  4. 将防火墙应用到你的droplet

关于详细的防火墙配置,请参考DigitalOcean的防火墙快速入门教程。生产提示: 将MQTT端口1883限制在特定的源IP或VPC范围,并且对于外部设备连接,优先使用端口1884(带TLS的MQTT)。如果你需要额外的安全层,请考虑使用带有私有网络的DigitalOcean应用平台。

步骤5 — 使用Coreflux的Language of Things设置物联网数据集成

安装LoT Notebook扩展

用于Visual Studio Code的LoTLanguage of ThingsNotebook扩展提供了一个集成的低代码开发环境,用于MQTT代理编程和物联网自动化。了解更多关于Coreflux的Language of Things (LoT)用于低代码物联网自动化的信息。

  1. 打开Visual Studio Code
  2. 转到扩展(Ctrl+Shift+X)
  3. 搜索"LoT Notebooks"
  4. 安装由Coreflux提供的LoT VSCode Notebooks扩展

Visual Studio Code中的LoT Notebook扩展

连接到你的MQTT代理

配置与你的Coreflux MQTT代理的连接,当在顶部栏提示时或通过点击底部左侧栏的MQTT按钮时,使用默认凭据:

  • 用户:root
  • 密码:coreflux

假设没有错误,你将在底部左侧栏看到与代理的MQTT连接状态。

VS Code中的Coreflux MQTT连接状态

步骤6 — 通过Actions在MQTT代理中创建数据

对于这个用例,我们将通过一个转换管道将原始数据集成到数据库中。然而,由于在演示中没有连接到任何MQTT设备,我们将利用LoT的能力,并使用一个Action来模拟设备数据。

在LoT中,Action是一种可执行的逻辑,由特定事件触发,例如定时间隔、主题更新或其他操作或系统组件的显式调用。Actions允许与MQTT主题、内部变量和负载进行动态交互,促进复杂的物联网自动化工作流。

因此,我们可以使用一个以定义的时间间隔在特定主题中生成数据的Action,然后由我们将在下面定义的管道的其余部分使用。

你可以下载包含示例项目的github仓库。

生成模拟物联网数据

使用低代码LoTLanguage of Things)界面创建一个Action来生成模拟传感器数据:

DEFINE ACTION RANDOMIZEMachineData
ON EVERY 10 SECONDS DO
    PUBLISH TOPIC "raw_data/machine1" WITH RANDOM BETWEEN 0 AND 10
    PUBLISH TOPIC "raw_data/station2" WITH RANDOM BETWEEN 0 AND 60

在提供的Notebook中,你还有一个Action可以执行递增计数器来模拟数据,作为提供Action的替代方案。

运行LoT操作以生成模拟物联网数据

当你运行这个Action时,它将:

  • 自动部署到MQTT代理
  • 每10秒生成一次模拟的物联网传感器数据
  • 实时数据发布到特定的MQTT主题
  • LoT Notebook界面中显示同步状态

    • 此状态显示LoT Notebook上的代码是否与代理中运行的代码不同,或者是否完全缺失

步骤7 — 为实时处理创建数据转换模型

使用Language of Things定义数据模型

Coreflux中的模型用于转换、聚合和计算来自输入MQTT主题的值,并将结果发布到新主题。它们是创建适用于你多个数据源的UNS - 统一命名空间 - 的基础。

因此,通过该模型,你可以定义原始物联网数据的结构与转换方式,适用于单个设备,也支持同时处理多个设备(借助通配符+实现)。模型还作为用于可扩展存储托管数据库的关键数据模式。

DEFINE MODEL MachineData WITH TOPIC "Simulator/Machine/+/Data"

    ADD "energy" WITH TOPIC "raw_data/+" AS TRIGGER

    ADD "energy_wh" WITH (energy * 1000)

    ADD "production_status" WITH (IF energy > 5 THEN "active" ELSE "inactive")

    ADD "production_count" WITH (IF production_status EQUALS "active" THEN (production_count + 1) ELSE 0)

    ADD "stoppage" WITH (IF production_status EQUALS "inactive" THEN 1 ELSE 0)

    ADD "maintenance_alert" WITH (IF energy > 50 THEN TRUE ELSE FALSE)

    ADD "timestamp" WITH TIMESTAMP "UTC"

这个低代码模型:

  • 使用通配符+自动应用到所有机器
  • 通过乘以1000将能量转换为瓦时(energy_wh)
  • 根据能量阈值确定生产状态
  • 跟踪生产计数和停机事件
  • 向所有实时数据点添加时间戳
  • 从主题结构中提取机器ID
  • 将结构化数据发布到Simulator/Machine/Data主题(将+替换为每个匹配触发器/源数据格式的主题)

由于我们使用Action生成了两个模拟传感器/机器,我们可以看到模型结构自动应用于两者,同时生成了一个json对象和各个单独的主题。

Coreflux模型发布的转换后的MQTT数据

步骤8 — 为可扩展存储设置数据库集成

选择与你在步骤2中选择的数据库相匹配的数据库集成部分。

PostgreSQL集成

在本节中,你将学习如何将处理后的物联网数据存储到DigitalOcean上的PostgreSQL托管数据库中。

要将处理后的物联网数据存储到PostgreSQL托管数据库中,你需要在Coreflux中定义一个Route。Route使用简单、低代码的配置指定数据如何从你的MQTT代理发送到你的PostgreSQL集群:

DEFINE ROUTE PostgreSQL_Log WITH TYPE POSTGRESQL

    ADD SQL_CONFIG

        WITH SERVER "db-postgresql.db.onmyserver.com"

        WITH PORT 25060

        WITH DATABASE "defaultdb"

        WITH USERNAME "doadmin"

        WITH PASSWORD "AVNS_pass"

        WITH USE_SSL TRUE

        WITH TRUST_SERVER_CERTIFICATE FALSE

使用来自DigitalOcean的你自己的PostgreSQL连接详细信息替换,并在你的LoT Notebook中运行该Route重要提示: 为了更好的安全性和更低的延迟,请使用VPC连接详细信息(而非公共连接)。VPC主机名和端口与公共连接字符串不同 - 请检查你的数据库集群的连接详细信息页面以获取这两个选项。

为PostgreSQL数据库存储更新模型

修改你的LoT模型以使用数据库路由进行可扩展存储,通过将此添加到模型的末尾:

STORE IN "PostgreSQL_Log"

    WITH TABLE "MachineProductionData"

此外,添加一个带有主题的参数,以便在你的托管数据库中为每个条目提供唯一标识符。

DEFINE MODEL MachineData WITH TOPIC "Simulator/Machine/+/Data"

    ADD "energy" WITH TOPIC "raw_data/+" AS TRIGGER

    ADD "device_name" WITH REPLACE "+" WITH TOPIC POSITION 2 IN "+"

    ADD "energy_wh" WITH (energy * 1000)

    ADD "production_status" WITH (IF energy > 5 THEN "active" ELSE "inactive")

    ADD "production_count" WITH (IF production_status EQUALS "active" THEN (production_count + 1) ELSE 0)

    ADD "stoppage" WITH (IF production_status EQUALS "inactive" THEN 1 ELSE 0)

    ADD "maintenance_alert" WITH (IF energy > 50 THEN TRUE ELSE FALSE)

    ADD "timestamp" WITH TIMESTAMP "UTC"

    STORE IN "PostgreSQL_Log"

        WITH TABLE "MachineProductionData"

部署此更新后的操作后,所有数据在更新时应自动存储在数据库中。

MySQL集成

MySQL是一种广泛使用的关系数据库管理系统,非常适合大规模存储和分析物联网数据。在本节中,你将学习如何将你的Coreflux MQTT代理连接到DigitalOcean上的托管MySQL数据库,以便你的实时设备数据能够安全可靠地持久化,用于分析、报告或与其他应用程序集成。

要启用此集成,你必须在Coreflux的LoT(Language of Things)中定义一个Route,指示处理后的数据应该发送到哪里以及如何发送。下面是路由数据到MySQL数据库所需的低代码格式。请务必根据需要替换你自己的连接详细信息:

DEFINE ROUTE MySQL_Log WITH TYPE MYSQL
    ADD SQL_CONFIG
        WITH SERVER "db-mysql.db.onmyserver.com"
        WITH PORT 25060
        WITH DATABASE "defaultdb"
        WITH USERNAME "doadmin"
        WITH PASSWORD "AVNS_pass"
        WITH USE_SSL TRUE
        WITH TRUST_SERVER_CERTIFICATE FALSE

使用来自DigitalOcean的你自己的MySQL连接详细信息替换,并在你的LoT Notebook中运行该Route重要提示: 为了更好的安全性和更低的延迟,请使用VPC连接详细信息(而非公共连接)。如果你遇到连接问题,请验证TRUST_SERVER_CERTIFICATE是否已为你的MySQL版本正确设置 - 某些版本需要TRUE,而其他版本则使用FALSE

为MySQL数据库存储更新模型

修改你的LoT模型以使用数据库路由进行可扩展存储,通过将此添加到模型的末尾:

STORE IN "MySQL_Log"
    WITH TABLE "MachineProductionData"

此外,添加一个带有主题的参数,以便在你的托管数据库中为每个条目提供唯一标识符。

DEFINE MODEL MachineData WITH TOPIC "Simulator/Machine/+/Data"
    ADD "energy" WITH TOPIC "raw_data/+" AS TRIGGER
    ADD "device_name" WITH REPLACE "+" WITH TOPIC POSITION 2 IN "+"
    ADD "energy_wh" WITH (energy * 1000)
    ADD "production_status" WITH (IF energy > 5 THEN "active" ELSE "inactive")
    ADD "production_count" WITH (IF production_status EQUALS "active" THEN (production_count + 1) ELSE 0)
    ADD "stoppage" WITH (IF production_status EQUALS "inactive" THEN 1 ELSE 0)
    ADD "maintenance_alert" WITH (IF energy > 50 THEN TRUE ELSE FALSE)
    ADD "timestamp" WITH TIMESTAMP "UTC"
    STORE IN "MySQL_Log"
        WITH TABLE "MachineProductionData"

部署此更新后的操作后,所有数据在更新时应自动存储在数据库中。

MongoDB集成

MongoDB是一种NoSQL数据库,非常适合存储和查询具有灵活模式的物联网数据。在本节中,你将学习如何将你的Coreflux MQTT代理连接到DigitalOcean上的托管MongoDB数据库,以便你的实时设备数据能够安全可靠地持久化,用于分析、报告或与其他应用程序集成。

要启用此集成,你必须在Coreflux的LoT(Language of Things)中定义一个Route,指示处理后的数据应该发送到哪里以及如何发送。下面是路由数据到MongoDB数据库所需的低代码格式。请务必根据需要替换你自己的连接详细信息:

DEFINE ROUTE mongo_route WITH TYPE MONGODB
    ADD MONGODB_CONFIG
        WITH CONNECTION_STRING "mongodb+srv://<username>:<password>@<cluster-uri>/<database>?tls=true&authSource=admin&replicaSet=<replica-set>"
        WITH DATABASE "admin"

使用来自DigitalOcean的你自己的MongoDB连接详细信息替换,并在你的LoT Notebook中运行该Route。重要提示: 当可用时,请使用VPC连接字符串格式。连接字符串应包括tls=trueauthSource=admin参数。有关MongoDB连接故障排除,请参阅我们关于连接MongoDB的教程。

为MongoDB数据库存储更新模型

修改你的LoT模型以使用数据库路由进行可扩展存储,通过将此添加到模型的末尾:

STORE IN "mongo_route"
    WITH TABLE "MachineProductionData"

此外,添加一个带有主题的参数,以便在你的托管数据库中为每个条目提供唯一标识符。

DEFINE MODEL MachineData WITH TOPIC "Simulator/Machine/+/Data"
    ADD "energy" WITH TOPIC "raw_data/+" AS TRIGGER
    ADD "device_name" WITH REPLACE "+" WITH TOPIC POSITION 2 IN "+"
    ADD "energy_wh" WITH (energy * 1000)
    ADD "production_status" WITH (IF energy > 5 THEN "active" ELSE "inactive")
    ADD "production_count" WITH (IF production_status EQUALS "active" THEN (production_count + 1) ELSE 0)
    ADD "stoppage" WITH (IF production_status EQUALS "inactive" THEN 1 ELSE 0)
    ADD "maintenance_alert" WITH (IF energy > 50 THEN TRUE ELSE FALSE)
    ADD "timestamp" WITH TIMESTAMP "UTC"
    STORE IN "mongo_route"
        WITH TABLE "MachineProductionData"

部署此更新后的操作后,所有数据在更新时应自动存储在数据库中。

OpenSearch集成

OpenSearch是一种分布式搜索和分析引擎,专为大规模数据处理和实时分析而设计。在本节中,你将学习如何将你的Coreflux MQTT代理连接到DigitalOcean上的托管OpenSearch数据库,以便你的实时设备数据能够安全可靠地持久化,用于分析、报告或与其他应用程序集成。

要启用此集成,你必须在Coreflux的LoT(Language of Things)中定义一个Route,指示处理后的数据应该发送到哪里以及如何发送。下面是路由数据到OpenSearch数据库所需的低代码格式。请务必根据需要替换你自己的连接详细信息:

DEFINE ROUTE OpenSearch_log WITH TYPE OPENSEARCH
    ADD OPENSEARCH_CONFIG
        WITH BASE_URL "https://my-opensearch-cluster:9200"
        WITH USERNAME "myuser"
        WITH PASSWORD "mypassword"
        WITH USE_SSL TRUE
        WITH IGNORE_CERT_ERRORS FALSE

使用来自DigitalOcean的你自己的OpenSearch连接详细信息替换,并在你的LoT Notebook中运行该Route。重要提示: 当可用时,请使用VPC基础URL(而非公共URL)。基础URL格式通常为https://your-cluster-hostname:9200。对于OpenSearch仪表板访问,请使用数据库集群详细信息中提供的单独的仪表板URL。有关更多详细信息,请参阅我们的OpenSearch快速入门。

为OpenSearch数据库存储更新模型

修改你的LoT模型以使用数据库路由进行可扩展存储,通过将此添加到模型的末尾:

STORE IN "OpenSearch_Log"
    WITH TABLE "MachineProductionData"

此外,添加一个带有主题的参数,以便在你的托管数据库中为每个条目提供唯一标识符。

DEFINE MODEL MachineData WITH TOPIC "Simulator/Machine/+/Data"
    ADD "energy" WITH TOPIC "raw_data/+" AS TRIGGER
    ADD "device_name" WITH REPLACE "+" WITH TOPIC POSITION 2 IN "+"
    ADD "energy_wh" WITH (energy * 1000)
    ADD "production_status" WITH (IF energy > 5 THEN "active" ELSE "inactive")
    ADD "production_count" WITH (IF production_status EQUALS "active" THEN (production_count + 1) ELSE 0)
    ADD "stoppage" WITH (IF production_status EQUALS "inactive" THEN 1 ELSE 0)
    ADD "maintenance_alert" WITH (IF energy > 50 THEN TRUE ELSE FALSE)
    ADD "timestamp" WITH TIMESTAMP "UTC"
    STORE IN "OpenSearch_Log"
        WITH TABLE "MachineProductionData"

部署此更新后的操作后,所有数据在更新时应自动存储在数据库中。

步骤9 — 验证完整的物联网自动化管道

监控实时数据流

  1. MQTT Explorer:使用MQTT客户端验证实时数据发布
  2. 数据库客户端:连接以验证数据的存储(PostgreSQL使用DBeaver,MongoDB使用MongoDB Compass,OpenSearch使用OpenSearch Dashboards)

验证PostgreSQL存储

使用DBeaver连接到你的PostgreSQL托管数据库以验证可扩展存储

  1. 使用来自你的DigitalOcean数据库的连接字符串
  2. 导航到 coreflux-broker-data 数据库(或你为数据库指定的名称)
  3. 检查 MachineProductionData 表中存储的记录

显示存储的物联网记录的PostgreSQL表

正如我们之前看到的,所有数据都可在MQTT代理中用于其他用途和集成。

带有实时机器数据的MQTT主题

验证MongoDB存储

使用MongoDB Compass连接到你的MongoDB托管数据库以验证可扩展存储

  1. 使用来自你的DigitalOcean数据库的连接字符串
  2. 导航到 coreflux-broker-data 数据库(或你为数据库指定的名称)
  3. 检查 MachineProductionData 集合中存储的文档

检查数据库存储

你应该看到具有类似结构的实时数据文档:

{
  "_id": {
    "$oid": "68626dc3e8385cbe9a1666c3"
  },
  "energy": 36,
  "energy_wh": 36000,
  "production_status": "active",
  "production_count": 31,
  "stoppage": 0,
  "maintenance_alert": false,
  "timestamp": "2025-06-30 10:58:11",
  "device_name": "station2"
}

正如我们之前看到的,所有数据都可在MQTT代理中用于其他用途和集成。

验证MySQL存储

使用DBeaver连接到你的MySQL托管数据库以验证可扩展存储:

  1. 使用来自你的DigitalOcean数据库的连接字符串
  2. 导航到coreflux-broker-data数据库(或你为数据库指定的名称)
  3. 检查MachineProductionData表中存储的记录

验证MySQL中存储的物联网记录

与其他集成一样,所有数据也可在MQTT代理中用于其他用途和下游集成。

监控实时数据流

验证OpenSearch存储

使用提供的URL和凭据打开OpenSearchDashboards

  1. 打开菜单并选择索引管理选项

    1. 在菜单中选择索引选项,查看你的表名是否出现在列表中

检查数据库存储

  1. 返回主页并在菜单中选择发现选项

    1. 按照提供的步骤创建索引模式
    2. 返回到发现页面,你应该会看到你的数据

检查数据库存储

正如我们之前看到的,所有数据都可在MQTT代理中用于其他用途和集成。

检查数据库存储

步骤10 - 扩展你的用例和集成

测试LoT能力

  • 发布示例数据:使用MQTT Explorer将示例数据集发布到你的Coreflux代理。尝试不同的负载结构和不同的模型/操作,查看它们如何处理并存储到你选择的数据库中。
  • 数据验证:验证你数据库中的数据与你发布的有效负载是否匹配。使用你的数据库客户端(PostgreSQL使用DBeaver,MongoDB使用MongoDB Compass,OpenSearch使用OpenSearch Dashboards)检查一致性和准确性,确保你的物联网自动化集成按预期工作。比较时间戳、字段转换和数据类型,以验证你的实时数据管道。
  • 实时监控:使用另一个MQTT数据源(例如具有MQTT连接功能的简单传感器)设置连续的实时数据馈送。观察Coreflux和你的数据库如何处理传入的物联网数据流,并探索数据检索和查询的响应时间。

构建分析和可视化

  • 创建仪表板:与Grafana等可视化工具集成,创建显示你的物联网数据的仪表板,从实时MQTT主题和历史数据库查询中提取数据。跟踪指标,如设备正常运行时间、传感器读数、生产计数或来自你自动化系统的维护警报。了解如何使用我们的教程设置DigitalOcean托管数据库与Prometheus和Grafana的监控。对于实时仪表板,直接订阅MQTT主题;对于历史趋势和聚合,查询你的数据库。
  • 趋势分析:利用你数据库的能力来分析随时间变化的趋势:

    • PostgreSQL:使用SQL查询进行复杂的关系分析
    • MongoDB:使用聚合框架进行基于文档的分析
    • OpenSearch:使用高级分析和搜索能力进行全文搜索和时间序列分析
  • 多数据库集成:探索集成其他托管数据库,如用于非结构化数据的MongoDB,用于关系数据的PostgreSQL,用于结构化查询的MySQL,或用于高级分析和搜索的OpenSearch。使用Coreflux路由将数据同时发送到多个目的地。

优化和扩展你的物联网基础设施

  • 负载测试:使用LoT Notebook或自动化脚本通过同时发布多条消息来模拟高流量。监控你的Coreflux MQTT代理和数据库集群如何处理负载,并识别你的数据管道中的任何瓶颈。
  • 扩展DigitalOcean提供垂直和水平扩展选项。随着你的物联网数据需求增长,增加droplet资源(CPU、RAM或存储)。扩展你的托管数据库集群以处理更大的数据集,并配置自动扩展警报,以便在接近资源限制时通知你。

常见问题解答

1. 如何将Coreflux MQTT代理与托管数据库集成?

你通过定义指向目标服务(PostgreSQL、MySQL、MongoDB或OpenSearch)的LoTRoute来将Coreflux MQTT代理与托管数据库集成。每个路由使用适当的连接参数(服务器或连接字符串、端口、数据库名称、用户名、密码和SSL/TLS选项),并自动将MQTT消息有效负载持久化到表、集合或索引中。一旦定义好路由,你就使用STORE IN指令将其附加到Model,这样每个处理后的消息都会被写入你选择的数据库。

2. 我能否在不编写自定义集成代码的情况下将MQTT数据直接保存到数据库?

可以。Coreflux设计为一个低代码集成层,因此你无需编写应用程序代码或外部ETL作业来持久化数据。对于每种数据库类型,你配置一个LoT路由(例如,PostgreSQL_LogMySQL_Logmongo_routeOpenSearch_Log),然后使用STORE IN "<route_name>" WITH TABLE "MachineProductionData"扩展你的模型。Coreflux处理连接池、重试和错误处理,因此你可以专注于建模主题和转换,而不是样板数据库代码。

3. 我应该为MQTT物联网数据存储选择哪种托管数据库?

你的MQTT物联网数据的最佳托管数据库取决于你的数据结构、查询需求和分析目标。使用下面的比较表来帮助你决定:

数据库最适合示例用例
PostgreSQL强一致性、关系模式、复杂的SQL查询工业传感器网络、事务性事件、需要跨连接数据集的分析
MySQL关系数据、结构化查询、广泛的兼容性库存系统、生产指标、传统业务记录
MongoDB灵活、不断演进的模式;文档存储具有可变负载的互联设备、具有变化格式的物联网遥测
OpenSearch全文搜索、分析、仪表板、日志索引时间序列分析、监控、事件日志、物联网搜索和可视化

提示: 你可以通过配置多个Coreflux路由同时使用多个托管数据库。这使得可以从同一个MQTT流中,将结构化的物联网数据存储在PostgreSQL或MySQL中,在OpenSearch中聚合日志和指标,并在MongoDB中收集非结构化或无模式数据。

4. 这种架构如何处理实时和历史分析?

Coreflux将所有处理后的值保留在MQTT主题上,供实时消费、仪表板或额外管道使用,而Routes则将相同的建模数据持久化到你的数据库中,用于历史查询。在实践中,你可以订阅主题以进行即时反应(警报、控制回路),并查询PostgreSQL/MySQL/MongoDB/OpenSearch以进行聚合、趋势和长期分析。这种双路径设计反映了MQTT和物联网数据集成教程中的常见模式,其中代理提供实时消息传递,而数据库提供持久存储和分析。

5. Coreflux和托管数据库之间的连接有多安全?

当部署在DigitalOcean上时,你可以使用VPC网络来保持Coreflux MQTT代理和数据库之间的所有通信私密。VPC将你的资源与公共互联网访问隔离开来,并且DigitalOcean托管数据库支持连接的TLS加密。此外,你可以为你的Coreflux应用程序创建具有有限权限的专用数据库用户,遵循最小权限原则。

6. 这个设置是否适用于生产环境物联网部署?

是的。这种架构反映了生产环境中MQTT和数据库集成所使用的模式,其中代理前端处理设备流量,而托管数据库层提供持久性和分析。DigitalOcean托管数据库提供自动备份、高可用性和监控,而Coreflux MQTT代理可以水平扩展以处理高消息吞吐量。对于生产环境,你还应该配置防火墙规则、使用强凭据、为MQTT和数据库连接启用TLS,并根据预期的消息量来调整你的droplet和集群大小。

7. 我能否在没有公共互联网访问的情况下,或在混合环境中运行MQTT代理?

可以。MQTT代理通常部署在私有网络或边缘环境中,公共资源一致指出,只要客户端可以访问代理,MQTT就可以在没有公共互联网的情况下工作。使用DigitalOcean,你可以将Coreflux和你的数据库保持在VPC内部,并且只暴露绝对必要的内容(例如,VPN、堡垒主机或有限的防火墙规则)。如果你需要混合或多站点架构,你还可以将选定的主题与其他代理或云区域同步。

8. 在物联网数据中使用MQTT和数据库是否存在任何限制或权衡?

MQTT针对轻量级、事件驱动的消息传递进行了优化;数据库则针对存储和查询进行了优化。存储每一条原始消息可能会变得昂贵或嘈杂,因此最佳实践建议仔细建模数据(例如,聚合指标、过滤主题或降采样)。极低功耗设备或超受限网络可能难以维持持久连接或处理TLS开销,在这种情况下,你可能需要调整QoS级别、批处理和保留策略。只要你在设计中考虑到这些权衡,MQTT加上托管数据库对于大多数物联网场景都能很好地工作。

9. 我如何为我的物联网项目在PostgreSQL、MySQL、MongoDB和OpenSearch之间做出选择?

你应该根据物联网数据结构、可扩展性以及你希望如何查询设备数据来选择托管数据库。下表总结了每个选项的优势:

数据库当...时最佳典型用例关键优势
PostgreSQL你需要复杂的关系查询、强一致性和事务完整性(ACID支持)。工业传感器网络、将设备数据与生产相关联、需要对连接的数据集进行分析关系模式、高级SQL、一致性
MySQL你的工作负载是结构化的,具有广泛的工具和兼容性需求。库存跟踪、传统业务系统、生产指标更简单的关系需求、广泛支持
MongoDB你的设备负载和模式不断演变,或者你希望使用灵活的、基于文档的存储进行快速原型设计。具有可变格式的物联网遥测、快速开发、半结构化数据灵活的模式、易于扩展、快速原型设计
OpenSearch你需要分析、搜索或对大容量的物联网数据(日志、时间序列、事件)进行仪表板展示。搜索传感器数据、日志分析、可视化、基于关键字/时间的查询搜索、全文、分析、快速聚合

结论

将Coreflux MQTT代理与DigitalOcean的托管数据库服务(PostgreSQL、MongoDB、MySQL或OpenSearch)集成,为你提供了实时物联网数据处理和存储的完整设置。按照本教程,你已经使用低代码开发实践构建了一个收集、处理和存储物联网数据的自动化管道。

借助Coreflux的架构和你选择的数据库的存储特性,你可以处理大量的实时数据并查询它以获取洞察。无论你是监控工业系统、跟踪环境传感器还是管理智慧城市基础设施,这种设置都让你能够基于实时MQTT主题和历史数据库查询做出数据驱动的决策。

了解更多关于DigitalOcean托管数据库的信息,以及DigitalOcean 针对 IoT行业的产品服务支持,可咨询 DigitalOcean 中国区独家战略合作伙伴卓普云AI Droplet(aidroplet.com)

你可以尝试提供的用例或使用Coreflux和DigitalOcean实现你自己的用例。你还可以在DigitalOcean Droplet市场或通过Coreflux网站获取免费的Coreflux MQTT代理

摘要:
OceanBase联合成都信息工程大学的数据库缺陷实证研究,被软件工程顶刊IEEE TKDE录用。研究聚焦复杂工作负载下数据库参数自动调优的效率与泛化能力问题,提出一种分层协作的多智能体框架,通过创新的训练机制与协同策略,有效破解传统调优方法的瓶颈,为数据库性能优化和预测提供了兼具理论创新性与工程实用性的技术方案。

近日,成都信息工程大学与 OceanBase 研发团队合作完成的研究《CMA+DB: How to Automatically Tune Database Parameters through Collaborative Multi-Agents》,被《IEEE Transactions on Knowledge and Data Engineering》(TKDE,CCF A 类、SCI 一区)录用。

研究聚焦复杂工作负载下数据库参数自动调优的效率与泛化能力问题,提出一种分层协作的多智能体框架,通过创新的训练机制与协同策略,有效破解传统调优方法的瓶颈,为数据库性能优化和预测提供了兼具理论创新性与工程实用性的技术方案。

以下为论文介绍:

研究背景与挑战

随着分布式与云计算技术的发展,数据库工作负载日趋多样化、复杂化。从高并发的在线交易场景(TPC-C),到随机读写的云服务场景(YCSB),再到高实时性的社交互动场景(Twitter),不同场景对参数配置的需求差异显著。

传统调优方式逐渐难以适配这些实际需求:人工调优高度依赖 DBA 的专业经验,不仅需要耗费大量时间梳理参数关系,而且无法应对动态变化的工作负载,往往出现 “调优即过时” 的问题;搜索式调优方法依赖启发式规则,在简单场景下表现尚可,但面对多参数交互的复杂场景时,搜索空间急剧扩大,调优效率大幅下降;贝叶斯优化方法需要手动筛选关键参数,若参数定义不全面,难以找到最优配置;现有强化学习调优方法多采用单智能体设计,仅能对参数进行粗粒度调优,无法充分挖掘不同类型参数间的深层交互影响,调优精度和泛化能力受限。

如何实现参数调优的自动化、精准化与高效化,成为数据库领域亟待解决的关键问题。


图1 数据库参数调优主要步骤

核心理论创新:CMA+DB 多智能体协作框架

CMA+DB 框架以 “分类协作、分层训练” 为核心设计理念,构建了三级递进式训练机制,整合单智能体预训练模型 SAPM、多智能体联合训练模型 MATM 与基于概率选择的联合训练模型 PJTM,既保障了单个参数类别内的调优深度,又实现了跨类别参数的协同优化。

三个子模型并非孤立存在,而是以级联方式递进工作,前一阶段的训练成果直接作为后一阶段的输入,形成 “基础专精—交互探索—精准强化” 的完整调优链路,确保框架在参数交互捕捉、调优效率与泛化能力之间实现最优平衡。


图2 CMA+DB 模型工作原理

在技术实现上,CMA+DB 基于深度确定性策略梯度(DDPG)与多智能体深度确定性策略梯度(MADDPG)构建核心算法架构,采用 Actor-Critic 网络结构实现智能体的决策与优化。每个智能体都具备独立的观测空间与动作空间,通过与数据库环境的持续交互获取反馈,不断优化参数调优策略。


图3 多智能体 Actor 和 Critic 网络结构

在第一阶段,提出单智能体预训练模型 SAPM。训练初期,每个 Agent 负责调优一类功能或参数级别相似的参数,目的是探究单个 Agent 对数据库性能的影响,进而优化其网络,使得其神经网络偏向于调节重要参数。该过程 Agent 之间相互不影响。与传统的单智能体模型相比,SAPM 模型的优点在于更深入地探究相似参数之间的关系,也更容易识别出一些重要参数。

这一阶段对于识别关键参数和实现快速收敛至关重要。通过使智能体能够初步掌握参数调整策略,SAPM 为后续更复杂的多智能体训练奠定了坚实的基础,并减少后续阶段的耦合干扰,从而提高了整体效率和效果。

在第二阶段,提出多智能体联合训练模型 MATM,在 MATM 模型训练阶段,使用组合算法将多个 Agent 的预测动作组合并映射为一组数据库的推荐配置,要求 Agent 之间不能存在相同的数据库参数。

这种协作方法不仅增加了可调参数的数量,还显著增强了模型的表现力和调整能力。此阶段虽然存在耦合,但通过联合训练,能够让智能体逐步适应彼此的行为模式,实现协同优化。

在第三阶段,提出了联合动作概选模型 PJTM,在 SAPM 和 MATM 模型训练之后进行训练,算法为每个 Agent 设置了一个概选因子 Pi, Agent 根据 Pi,随机地参与联合动作推荐。当一个 Agent 控制的数据库参数对数据库性能的提升微乎其微时,就可以根据 Pi 减小该 Agent 的动作参与,进而降低其对其它 Agent 所做动作的负面影响。这一机制有效减少了低影响智能体对整体调优过程的干扰,缓解了耦合带来的负面影响。

基于上述三种方式,提出协作型多智能体模型 CMA+DB,其整合 SAPM、MATM 和 PJTM 模型进行分阶段训练,实现分功能、分级别地对数据库参数进行调优。探究了相同类型参数之间的相互影响以及不同类型参数之间的协作关系。有效地解决了离散环境和异步反馈的挑战,确保多个智能体的动作是协调的,并针对数据库性能进行了优化,提高了数据库参数的调优数量,提升了数据库参数调优的效率。

关键验证成果

相关研究成果已在 PostgreSQL 数据库环境下,在 TPC-C、YCSB、Twitter 三种典型工作负载上,与主流调优方法进行全面对比验证,核心性能指标表现突出且稳定。

在调优效率方面,CMA+DB 的收敛速度优势显著。在 TPC-C 高并发交易场景中,这一优势尤为明显,CMA+DB 的平均收敛速度(达到最大吞吐量时)优于其他三种算法。能帮助数据库更快脱离性能波动期,进入最优稳定状态,大幅缩短调优耗时,降低系统上线或扩容后的性能适配成本。

在性能提升方面,CMA+DB 展现出持续优化的能力。经过 SAPM 预训练后,框架的吞吐量已优于现有部分主流方法;后续经 MATM 的跨类别参数协同优化,以及 PJTM 的精准强化,在 TPC-C 场景下,其延迟显著低于 OtterTune、CDBTune+ 等方法,有效保障了系统在峰值压力下的响应稳定性,避免了因参数配置不当导致的延迟突增问题。

在泛化能力方面,CMA+DB 表现出极强的适应性。实验结果表明,不同参数规模与智能体数量的组合测试显示,CMA+DB 能够根据实际场景灵活调整,无需人工干预即可适配不同规模的数据库参数调优需求,解决了传统方法在参数规模变化时泛化能力下降的问题。

总结与展望

作为一种兼具理论深度与工程价值的数据库参数自动调优方案,CMA+DB 框架通过多智能体分类协作与分层训练的创新设计,有效解决了传统方法在参数交互探索、调优效率与泛化能力上的短板,为 AI 赋能数据库优化领域提供了新的技术思路。

该框架的核心优势在于,不但实现了参数调优的自动化与精准化,而且无需依赖大量人工干预与领域知识,降低了数据库性能优化的门槛,具备较强的应用价值。

未来研究将进一步优化框架结构,通过算法改进降低计算复杂度,提升框架在超大规模参数场景下的运行效率;同时,将尝试适配 MySQL、OceanBase 等更多主流 DBMS,针对不同数据库的内核特性调整智能体的参数分类与协作策略,推动相关技术在金融、电商、社交等实际生产环境中的广泛应用,为数据库系统的性能优化提供更高效、更通用的解决方案。

欢迎访问 OceanBase 官网获取更多信息:https://www.oceanbase.com/

墨天轮社区举办的 【文档悬赏令】系列活动,每次聚焦一个主题,征集真实、可操作的第一手文档,希望能扩充社区文档资源、为更多人提供有用的参考资料,让技术学习少走弯路!

第2号悬赏令活动主题:数据库巡检方案实践

数据库巡检是保障业务稳定运行的核心运维环节,更是DBA日常工作的重中之重。当前数据库类型、场景十分丰富,多数从业者需耗费大量时间整理巡检清单、调试巡检流程。墨天轮社区第二期【文档悬赏令】活动特围绕这一主题发起有奖征集活动,诚邀您上传实用、可落地的数据库巡检文档,和社区众位DBA们互帮互助,让巡检工作更高效、更标准!

一、活动时间

2026 年 1 月 26 日 - 3 月 28 日

二、文档要求

1、主题范围

本次征集聚焦 “数据库巡检” 核心主题,覆盖国内外主流数据库,基础巡检、专项巡检、自动化巡检等多类实用方案等均可,具体范围如下:

巡检主题细分(包含但不限于):

  • 基础巡检:日常运维巡检表/巡检清单/巡检手册
  • 专项巡检:性能监控与优化巡检/灾备状态巡检/单机or集群巡检
  • 方式:手动巡检标准化文档/自动化巡检/脚本命令/巡检工具

数据库不限

  • 商业数据库:Oracle、SQL Server等
  • 国产数据库:达梦DM8、人大金仓KingbaseES、OceanBase等国产库
  • 开源数据库:MySQL、PostgreSQL、Redis等开源库

2、合格要求与格式

文档内容需明确巡检目的、适用场景、巡检流程等,包含巡检相关代码(部分敏感信息可隐去)。

  • 页数要求:≥5 页
  • 必加标签:上传时需在 “标签” 栏填写 “数据库巡检” + 数据库种类(如 Oracle、OceanBase) 两个标签
  • 支持格式:优先推荐 .doc、.pdf、.ppt、.md、.txt 格式(可支持前 5 页预览,下载率更高);也可上传 .zip (需附内容说明);
以下文档将被判为不合格:
1)主题无关:非“数据库巡检”相关主题
2)内容搬运:直接上传电子书或产品官方文档、他人演讲PPT,或直接复制抄袭、全文搬运网站其他文章/文档内容(已发布的文章内容不可以同时上传成文档参与活动,但如果是您曾发在其他网站的内容则可以上传参与)
3)流水账或凑字数:文档页数需≥5页,但不可通过凑字数、使用大量无关图片占篇幅等
4)作者刷量:不可刷数据-我们鼓励作者自发宣传自己的文档,但不可使用人工刷量、注册小号刷量等方式提高下载量;不可将文档拆分多份上传,导致单份内容无实际意义、缺乏完整性
5)重复文档:重新上传以前发布的文档系统会自动识别判定为重复仅自己可见,也不可删除旧文档后重新上传

三、上传步骤

  1. 登录上传:登录墨天轮账号后,点击链接直达上传页:https://www.modb.pro/docUpload,或在首页下拉框点击 “传文档”;

  1. 填写信息

    • 标题:建议明确标注 “数据库种类 + 巡检场景”(如《MySQL数据库巡检手册》《Oracle数据库常规巡检项目和命令》),帮助他人快速判断主题
    • 标签:上传时需在 “标签” 栏填写 “数据库巡检” + 数据库种类(如 Oracle、OceanBase) 两个标签
    • 墨值设置:支持免墨值 / 5 墨值 / 10 墨值 / 25 墨值 / 50 墨值 / 100 墨值,设置墨值后,他人下载时支付的墨值将实时计入您的账户(ps:一般来说墨值越低下载门槛越低,被下载可能更高)

  1. 提交:确认信息无误后点击 “提交文档”即可。于 “控制台-内容管理-我的文档” 处可查看您所有文档明细,若您遇到“仅自己可见”“转换失败”等状态可询问墨天轮小助手(VX:modb666)询问具体原因。

四、奖励设置

本次活动奖励分为 “合格奖”“有效助人奖”“巡检先锋奖” 三类

1、合格奖

每位用户每日首次上传≥5 页的合格文档,可自动触发 “每日任务” 获 5 墨值。此外,本次活动奖励额外可叠加,根据用户合格文档数量发放不同等级的墨值奖励,具体如下:

合格数量墨值
0-3个10墨值/份
3个以上15墨值/份

2、有效助人奖

根据单个文档下载数发放不同等级的墨值奖励,每个用户最多可有5个文档获得本项奖励:

被下载数墨值
30<被下载数 ≤ 5050墨值
50<被下载数 ≤ 100100墨值
100<下载数 ≤ 150200墨值
150<下载数 ≤ 200300墨值
下载数>200500墨值

3、巡检先锋奖

将综合评估用户上传的合格文档的内容质量及总下载量,评选出 “既多产、又优质” 的核心贡献者,发予相应奖励:

奖项等级奖励内容
第 1 名1000 墨值 + 100元内数据库实体书一本
第 2 名500墨值 + 爱国者U盘(128GB 双接口)
第 3-5 名笔记本电脑支架(可旋转)

说明:三类奖项单独评奖、每位用户可重复获得。其中新版本先锋奖数量将根据实际参与情况灵活调整,若参与情况佳则可能增设、反之亦然。

五、奖励公布与发放

  1. 进度公示:为了让大家更加了解自己的参与进度,将在活动期间不定期在活动原文评论区公布最新合格情况。若发现违规行为也欢迎向工作人员反馈,一经核实将取消其参与资格。
  2. 结果公布:活动结束后 3 个工作日内公布所有获奖名单;
  3. 奖励发放:墨值将在结果公布后 1-2 个工作日内发放至账户;实物奖励将通过私信收集地址,10 个工作日内寄出。

常见问题(Q&A)

Q1:如何让我的安装文档下载率更高?
A:建议在标题、简介中写明 “适用版本 + 核心亮点”,若上传的是.zip 等无法预览的格式,可在评论区附关键步骤截图或内容大纲,帮助他人判断价值。

Q2:墨值可以干什么?
A:墨值是墨天轮社区的“通用货币”,可以用来下载付费文档、赞赏他人文章或进行提问赞赏、直播打赏、兑换商品、参与墨值拍卖等。具体可查看使用说明:https://www.modb.pro/db/446902

Q3:我的文档会被展示在哪里?
A:文档上传成功后可在您的控制台、个人主页找到相应链接。管理员也会择优推荐到网站首页及文档页面,为您的文档增加更多曝光。若您觉得您的文档优秀,欢迎您转发分享或自荐给工作人员,我们将为您争取首页推荐、转发等更多曝光。


无论是你是初学者还是资深开发者,欢迎你加入本次文档悬赏活动,分享你的优质文档,与更多朋友一起学习进步!未来我们也将针对更多技术主题推出悬赏活动,如果你有推荐的主题也不妨告诉我们!乐知乐享、共同成长!

往期活动导航
【文档悬赏令】第1号:数据库新版本的安装实操


欲了解更多可浏览墨天轮社区,围绕数据人的学习成长提供一站式的全面服务,打造集新闻资讯、在线问答、活动直播、在线课程、文档阅览、资源下载、知识分享及在线运维为一体的统一平台,持续促进数据领域的知识传播和技术创新。

关注官方公众号: 墨天轮、 墨天轮平台、墨天轮成长营、数据库国产化 、数据库资讯

2025 年下半年,存储价格又一次成为行业聚焦点。

多家市场机构统计显示,2025 年三季度跟四季度,DRAM 和 NAND 价格一路攀升。根据 Tom's Hardware 披露的数据,2025 年 DRAM 合同价同比上涨幅度高达 171.8%,创下历史新高。此轮上涨跟 AI 数据中心建设拓展、服务器需求集中释放紧密相联,还直接引发企业 IT 基础设施采购成本上升。

对于依赖自建数据中心或中小 IDC 的企业来说,这种变化带来的冲击尤为剧烈。硬件采购从一次性预算问题,演变为难以预测的长期成本风险。服务器、SSD 和内存条的价格不再稳定,交付周期也更不确定。企业在扩容时不得不承担高价买入、供货延迟的双重压力。

因此,将硬件采购压力转化为按需付费的运营支出,把价格波动风险转移给云服务商,正在成为越来越多企业的选择。

但问题并未因此结束。

随着业务迁移到云端,企业发现云账单中存储与内存的占比仍在持续上升,即便算力配置并未明显升级,总体成本依旧水涨船高。部分团队开始反思:问题是否仅和数据量增多有关,还是资源使用方式本身就存在不合理的地方?

目前,多数云实例依旧按固定的 CPU 与内存配比来交付,诸如 2 核 4GB、4 核 8GB 的规格。早期,这种设计可简化资源管理,推动了云计算普及,但如今业务形态有所改变,企业系统一般得同时支撑多样业务,各业务对于算力、内存的消耗不一样,固定规格愈发难以契合实际需求。这导致企业要么部分资源长期闲置,要么不得不面对业务在高峰阶段出现性能瓶颈的风险。

当内存价格进入上行周期,这种规格错配带来的浪费被进一步放大:闲置的不再只是资源本身,而是越来越昂贵的成本

正是基于这样的背景,云基础设施走到新的路径分岔口:是继续就资源本身实施配置,还是转变方向围绕应用需求设计算力供给方式?

在近期面向中国区合作伙伴召开的发布会上,华为云对 Flexus 云服务器系列规格及性能进行更新,并且展示了其在各种业务负载下的运行表现。该实例基于华为云首创的柔性算力技术,打破 CPU 与内存的固定绑定关系,使企业能够按真实业务需求配置资源,从源头减少内存浪费,并结合智能调度与应用级加速改善长期运行稳定性与算力资源投入产出比。本文将从行业环境变化与技术实现等层面,剖析这种模式背后的思路,以及它所代表的云服务器演进方向。

云服务器,开始不太“合身”了

云服务器长期采用固定 CPU 与内存的配比,是工程上的一种取舍考量。早期云平台首先得解决的是规模化交付和稳定调度的问题,采用固定规格利于资源池管理,同样便于容量规划及计费设计。当业务形态呈现相对单一阶段,这样的方式尚可接纳。但究其本质它是从平台管理成本角度设计的,并非从业务负载的角度出发。

如今业务已不再是单一模式,电商、内容分发、数据库、缓存、AI 推理在一套系统中同步协同运行,对 CPU 以及内存的需求差别明显,固定规格无法精准对应实际负载,企业只能采用超出实际所需的实例型号。云服务器规格跟应用需求普遍不匹配,用户往往被迫去为用不到的算力和内存付费,引发大量资源的闲置浪费。

资源浪费只不过是表象罢了,更深层的问题体现为性能优化的复杂度。现实的业务部署不仅涉及操作系统选定,还包含网络参数、系统参数以及应用配置参数。数量往往达到数千级别,缺少专家经验积累,难以达成稳定的最优配置。单是内核跟应用层的参数组合,就已超出普通团队可控范围,调优所用的周期漫长,效果也难以把控。

从较长的时间阶段看,云服务器本身一直在不断演变,最初的资源虚拟化阶段,是把物理服务器标准化成可租借的实例;紧接着进入弹性规模阶段,采取自动伸缩的方式去应对流量变化,这两个阶段处理的是存不存在以及是否充足的问题,当下已经迈入第三阶段,关注焦点转向使用是否高效。过去,固定实例曾是工程优势,如今却愈发像是一件穿着不合身的衣服。

柔性算力:从“卖规格”到“卖能力”

怎样让资源本身更贴近应用?在 Flexus 云服务器 X 实例产品的设计里,华为云引入了柔性算力这一概念。

在 Flexus X 实例里,柔性算力首先体现在规格形态的调整变化上。传统实例一般仅仅可在少量固定比例中选择 CPU 跟内存配置,而该实例支持按业务需求实施更精细的组合配置。发布会现场提到,所有 X 实例均支持多种非常规的 CPU/ 内存配比,包括 3:1、2:5、3:7 等组合。这可减少由规格不一致引起的资源闲置,让用户更接近按实际负载付费。

然而规格数量增加,并非表示问题自动就解决了,其关键是系统如何判断哪种配置更合适。传统调度大多依据节点上剩余的 CPU 与内存。新方式需要领会业务负载本身,涵盖资源使用结构,以及随时间的变化趋势。Flexus X 实例本质上不再是调度 CPU,而是实际的业务场景。

就工程实现而言,这种转变依赖底层架构的支撑,Flexus X 实例借助华为云自研的擎天 QingTian 架构和瑶光云脑调度系统得以实现,经由计算、存储和网络资源的解耦操作,提高了资源组合的自由度,也增强了非标准规格运行状态下的稳定性。

此外,柔性算力还意味着配置不再是一次性决定,实例运行时会一直对资源使用状况进行评估,系统会判断当前配置跟负载是否相符,进而给出调整建议,而且还支持算力规格热升降的独家能力。从这个层面看,Flexus X 实例的转变不只是规格数量增多,它更像是把算力从提前打包好的商品,变成可持续优化的能力,实现“应用驱动算力”的最优体验。

关键应用加速:算力之外的第二条性能曲线

Flexus X 实例不单单改变了资源形态,还进一步深入应用执行层,解决了算力配置合理系统却依旧不稳定的问题。

此次规格升级,华为云为数据库以及中间件类的负载引入专属应用级加速机制。Flexus X 实例针对 PostgreSQL、Memcached、MySQL、Redis、Nginx 提供了独立的一键加速能力,由 X-Turbo 应用加速引擎统一驱动。此类优化不会对用户的使用途径做出改变,实例创建结束之后即可启用,平台会把调优工作完成,用户无需插手复杂参数的配置。发布会现场,华为云对该能力实测演示,在 PostgreSQL 的使用场景下,Flexus X 实例的吞吐量达到 2.1 万 + TPS,大概为同规格业界旗舰型实例的 3.4 倍

就数据库这类系统而言,峰值性能仅仅属于一方面,更为关键的是高负载持续状态下的稳定输出能力。业务系统更易受诸如延迟抖动、连接堆积等问题的干扰,而不是单次压测形成的成绩。X-Turbo 的设计目标之一正是实现性能优化长期运行状态下的吞吐与响应稳定性。

跟应用级优化同步进行的是,实例规模的进一步扩展。新一代 Flexus X2e 实例的 x86 规格从原本的 32U128G 提升至 64U256G,多核算力提升了约 30%;新增 Flexus KX1 鲲鹏实例,最高可达 80U320G,以覆盖大数据处理、内存数据库这类资源密集型场景。这意味着应用加速机制不再受中小规格环境约束,能在规模更大的资源池里发挥作用。

这一系列的变化显示出云服务器性能边界正在转移。过去,性能更多由 CPU 规格和内存容量决定。而如今,应用执行路径、参数组合的方法及调度策略成为同等要紧的变量,在固定规格的时代里,这些优化由用户自己承担,而于 Flexus X 实例中,它们被纳入到算力交付范畴,正是从这一意义出发,云服务器竞争不再只是资源规模大小的比拼,而是发展为聚焦运行效率的系统工程。

从工程能力到真实落地:柔性算力如何进入生产系统

一项新的算力供给方式,能否切实进入生产系统,首要取决于它是否具备充足的稳定性与可用性。Flexus X 实例可靠性设计向华为云旗舰级云服务器标准看齐,实现单 AZ 99.975% 的可用水平,还有跨 AZ 99.995% 的可用性。这暗示柔性算力没有以牺牲稳定性为交换代价,而是可直接承受核心业务负载的基础设施形态。

除了稳定性这一点,规模化使用还取决于运维体系自身是否具有确定性,Flexus X 实例在华为云既有的 SRE 运维体系框架内运行,强调借助标准化变更、容量预测与故障演练减少系统行为的不确定性,实现大规模实例并发运行的可控性。

从行业落地的实际来看,柔性算力最先进入的并非那种单一业务场景,而是负载结构繁杂、资源使用波动大的系统类型。其已经在医疗电商平台迁移、连锁零售系统、医药行业信息化平台、游戏服务器迁移等场景大规模部署,用以承载数据库、中间件及核心交易服务。

中软国际智能集团云业务部副总经理王春玉在发布会上分享,团队为某大型生物医药集团搭建系统的时候,引入 Flexus X 实例作为数据库及业务服务的主要承载环境,在原有系统架构未改变的情形下完成迁移,而且在性能满足要求的前提下,达成约 30% 的综合成本下降。王春玉还谈到,其团队服务的一家专业酒水直营连锁品牌,把部分核心业务迁移到 Flexus X 实例而后,通过规格按需匹配与资源利用率优化,实现整体云资源成本约 15% 的下降。这些亮眼的结果主要源于两方面:一是实例规格跟业务负载的匹配度有所提升,降低了长期闲置资源的数量;二是借助应用级加速与调度优化,降低了单位业务量所需的算力规模。

从这些真实的实际部署案例能看出,Flexus X 实例的用户一般有几个共同特性:业务负载呈现明显波动,系统结构相对复杂,然而运维及架构团队的规模较为有限,同时对长期云资源的成本敏感度较高。Flexus X 实例在未对业务形态本身作出改变的情况下,却降低了基础设施对业务扩展所施加的约束强度,让按照业务形态去配置算力成为可践行的工程实践。

可以预见,未来企业买的不再是服务器,而是业务效率。Flexus X 实例凸显了云服务器设计思路的一次转向:由“卖规格”过渡到“交付能力”,从“静态资源”过渡到“智能算力”,在 AI 成为主流计算负载的未来,此种转变大概率不会再是差异化优势,而是云基础设施的必要门槛。

本次版本 新增函数对象转换能力,扩展了达梦等多数据库迁移适配范围,并提升了批量转换的处理效率,进一步降低企业级数据库迁移的复杂度与成本。

一、核心特性

支持函数对象迁移

函数对象可随存储过程的迁移任务一键同步转换​。该能力的加入,让 SQLShift[1] 从一款“​存储过程迁移工具​”升级为“​核心业务逻辑对象全量迁移工具​”。随之也带来三重提升:

  1. 降低迁移风险与人工成本

    避免 函数对象 需人工逐个改写与反复校验,大幅减少因语法差异、返回值不一致引发的运行期错误。

  2. 提升非表对象整体迁移效率

    函数对象与存储过程 可在同一时间中完成迁移与校验,缩短整体迁移周期。

  3. 保障业务逻辑完整性与可用性

避免 函数对象 缺失导致上层存储过程等对象无法编译或运行的问题,有效降低迁移后集中调试与返工压力,提升割接与上线的稳定性。

函数对象迁移任务

<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=115970207123078&bvid=BV1Gg6LBAEf3&cid=35655845383&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>

新增数据库迁移组合

本次升级,SQLShift 扩展了多项数据库迁移组合。

SQLShift 支持迁移链路

  • 新增 Oracle / OceanBase → 达梦

    降低了迁移至达梦数据库的复杂度和人工成本,帮助企业快速完成数据库替换或国产化改造。

  • 新增 PostgreSQL → OceanBase(Oracle 模式)

    减少了跨数据库迁移中的人工调整工作量,加快了从 PostgreSQL 向 OceanBase 的迁移进程。

二、其他更新

批量处理能力提升

支持同时上传多个 SQL 文件进行转换,提升大规模迁移场景下的处理效率。

免费试用限时开放!

👉 点击领取 你的转换额度,立即体验 SQLShift 智能化迁移带来的飞跃效率!

🧩 SQL 方言再多,转换也能一步到位,SQLShift 为你搞定!

SQLShift介绍

如题,传统 php 网站,mysql 数据库,一些 crud 等,pdf 、excel 等处理

准备重构:
后端 Rust+python 微服务处理 pdf 、excel 等
前端 astro+solid
数据库 PostgreSQL

部署 Podman

大家觉得如何,有没有更优化的组合。

前几天我还在用 Go ,但是,因为感觉很多人倾向于 Rust 替代 Go ,所有把 Go 替换成 Rust 在试一试

整理 | 华卫

 

用一个 PostgreSQL 主库和 50 个只读副本,就顶住了 ChatGPT 上的 8 亿用户!

 

近日,OpenAI 的工程师们不仅爆出了这一惊人消息,还直接把 Codex 的“大脑”给扒了个精光。在 OpenAI 官方工程博客主页,OpenAI 工程师、Technical Staff 成员 Michael Bolin 发布了一篇文章,以“揭秘 Codex 智能体循环”为题,深入揭秘了 Codex CLI 的核心框架:智能体循环(Agent Loop),并详细讲解了 Codex 在查询模型时如何构建和管理其上下文,以及适用于所有基于 Responses API 构建智能体循环的实用注意事项和最佳实践。

 

这些消息传出后,在 Hacker News 等技术论坛及社交平台上获得了高度关注。“看似平淡的技术最终会胜出。OpenAI 正在证明,优秀的架构远胜于花哨的工具。”

 

值得一提的是,有网友透露,前不久 Anthropic 的一位工程师称“他们用于 Claude Code UI 的架构糟糕且效率低下”。而就在刚刚,X 上出现一条爆料:Codex 已接管 OpenAI 100%的代码编写工作。

 

对于“你们有多少百分比的编码工作是基于 OpenAI 模型进行”的问题,roon 表示,“100%,我不再写代码了。”而此前,Sam Altman 曾公开发帖称,“roon 是我的小号。”

 

Codex 的“大脑”揭秘

“每个人工智能智能体的核心都是 Agent Loop,负责协调用户、模型以及模型调用以执行有意义的软件工作的工具之间的交互。”

 

据介绍,在 OpenAI 内部,“Codex”涵盖了一系列软件智能体产品,包括 Codex CLI、Codex Cloud 和 Codex VS Code 插件,而支撑它们的框架和执行逻辑是同一个。

 

Agent Loop 的简化示意图

 

首先,智能体会从用户那里接收输入,并将其纳入为模型准备的文本指令集,该指令集被称为提示词。下一步是通过向模型发送指令并要求其生成响应来查询模型,这个过程称为推理。推理过程中,文本提示词首先被转换为一系列输入 token,随后被用于对模型进行采样,生成新的输出 token 序列。输出 token 会被还原为文本,成为模型的回复。由于 token 是逐步生成的,该还原过程可与模型的运行同步进行,这也是众多基于大语言模型的应用支持流式输出的原因。实际应用中,推理功能通常封装在文本 API 后方,从而抽象化词元化的细节。

 

推理步骤完成后,模型会产生两种结果:(1)针对用户的原始输入生成最终回复;(2)要求智能体执行某项工具调用操作。若为第二种情况,智能体将执行该工具调用并将工具输出结果附加至原始提示词中。该输出结果会被用于生成新的输入内容,再次对模型进行查询;智能体随后会结合这些新信息,重新尝试完成任务。这一过程会不断重复,直至模型停止发出工具调用指令,转而生成面向用户的消息(在 OpenAI 的模型中,该消息被称为助手消息)。多数情况下,这条消息会直接解答用户的原始请求,也可能是向用户提出的跟进问题。

 

由于智能体可执行能对本地环境进行修改的工具调用,其 “输出” 并不仅限于助手消息。在很多场景下,软件智能体的核心输出是在用户设备上编写或编辑的代码。但无论何种情况,每一轮交互最终都会以一条助手消息收尾,该消息是智能体循环进入终止状态的信号。从智能体的角度来看,其任务已完成,操作控制权将交还给用户。

 

多轮智能体循环

 

这意味着,对话内容越丰富,用于模型采样的提示词长度也会随之增加。而所有模型都存在上下文窗口限制,即其单次推理调用可处理的 token 最大数量,智能体可能在单次对话轮次中发起数百次工具调用,这有可能耗尽上下文窗口的容量。因此,上下文窗口管理是智能体的多项职责之一。

这套智能体循环如何运行?

据介绍,Codex 正是借助响应 API 来驱动这套智能体循环的,博文曝出许多背后的实际运行细节,包括:

  • Codex 不会把用户的话直接给大模型用,而是会主动“拼接”出一整套精心设计的提示词结构,且涵盖多个角色的指令、用户输入的一句话在结尾才出现。

  • 模型推理与工具调用之间可能会进行多轮迭代,提示词的内容会持续增加。

构建初始提示词

作为终端用户,在调用响应 API 时无需逐字指定用于模型采样的提示词,只需在查询中指定各类输入类型,由响应 API 服务器决定如何将这些信息组织为模型可处理的提示词格式。在初始提示词中,列表中的每个条目均关联一个角色。该角色决定了对应内容的权重占比,优先级从高到低分为以下几类:系统、开发者、用户、助手。

 

响应 API 接收包含多个参数的 JSON 负载,其中三个核心参数有:

  • 指令:插入模型上下文的系统(或开发者)消息

  • 工具:模型生成回复过程中可调用的工具列表

  • 输入:向模型传入的文本、图片或文件输入列表

 

在 Codex 中,若已配置,指令字段的内容会从~/.codex/config.toml 配置文件中的模型指令文件读取;若未配置,则使用与该模型关联的基础指令。模型专属指令存储在 Codex 代码仓库中,并被打包至命令行工具中。工具字段为符合响应 API 定义的模式的工具定义列表。对于 Codex 而言,该列表包含三部分工具:Codex 命令行工具自带的工具、响应 API 提供且开放给 Codex 使用的工具,以及通常由用户通过 MCP 服务器提供的自定义工具。JSON 负载的输入字段为一个条目列表。在添加用户消息前,Codex 会先向该输入中插入以下条目:

 

1. 一条角色为开发者(role=developer)的消息,用于描述仅适用于工具部分中定义的 Codex 内置 Shell 工具的沙箱环境。也就是说,其他工具(如由 MCP 服务器提供的工具)并不受 Codex 的沙箱限制,需自行负责实施自身的防护规则。该消息基于模板构建,核心内容均来自打包在 Codex 命令行工具中的 Markdown 代码片段。

2.一条角色为开发者的消息,其内容为从用户的 config.toml 配置文件中读取的 developer_instructions 配置值。

3.一条角色为用户的消息,其内容为用户指令;该内容并非来源于单个文件,而是从多个数据源聚合而来。一般而言,表述越具体的指令,排序越靠后:

  • 加载 $CODEX_HOME 目录下 AGENTS.override.md 和 AGENTS.md 文件的内容

  • 在默认 32 千字节的大小限制内,从当前工作目录对应的 Git / 项目根目录(若存在)向上遍历至当前工作目录本身,加载任意 AGENTS.override.md、AGENTS.md 文件的内容,或加载 config.toml 配置文件中 project_doc_fallback_filenames 参数指定的任意文件内容

  • 若已配置相关技能,则补充以下内容:关于技能的简短引言、各技能对应的技能元数据、技能使用方法说明章节。

4. 一条角色为用户的消息,用于描述智能体当前的运行本地环境,其中会明确当前工作目录及用户所使用的终端 Shell 信息。

 

当 Codex 完成上述所有计算并完成输入初始化后,会追加用户消息以启动对话。需注意的是,输入中的每一个元素都是一个 JSON 对象,包含类型、角色和内容三个字段。当 Codex 构建好要发送至响应 API 的完整 JSON 负载后,会根据~/.codex/config.toml 中响应 API 端点的配置方式,携带授权请求头发起 HTTP POST 请求(若有指定,还会添加额外的 HTTP 请求头和查询参数)。当 OpenAI 响应 API 服务器接收到该请求后,会使用 JSON 数据来推导出模型的提示信息,(需要说明的是,Responses API 的自定义实现可能会采用不同的方法)。

 

可见,提示词中前三项的顺序由服务器决定,而非客户端。也就是说,这三项里仅系统消息的内容同样由服务器控制,工具与指令则均由客户端决定。紧随其后的是 JSON 负载中的输入内容,至此提示词拼接完成。

模型采样

提示词准备就绪后,模型才开始进行进行采样。

 

第一轮交互:此次向响应 API 发起的 HTTP 请求,将启动 Codex 中对话的第一轮交互。服务器会以服务器发送事件(SSE)流的形式进行响应,每个事件的数据均为一个 JSON 负载,其 type 字段以 response 开头。Codex 接收该事件流并将其重新发布为可供客户端调用的内部事件对象。`response.output_text.delta`这类事件用于为用户界面实现流式输出功能,而`response.output_item.added`等其他事件则会被转换为对象,附加至输入内容中,为后续的响应 API 调用所用。

 

若首次向响应 API 发起的请求返回两个`response.output_item.done`事件,一个类型为推理(reasoning),一个类型为函数调用(function_call),那么当结合工具调用的返回结果再次向模型发起查询时,这些事件必须在 JSON 的输入字段中进行体现。后续查询中用于模型采样的最终提示词结构如下:

 

需要特别注意的是,旧提示词是新提示词的完整前缀。这一设计是有意为之的,因为它能让用户借助提示词缓存提升后续请求的效率。

 

在 Codex 命令行工具中,会将助手消息展示给用户,并聚焦输入编辑区,以此提示用户轮到其继续对话。若用户做出回应,上一轮的助手消息以及用户的新消息均需附加至响应 API 请求的输入字段中,从而开启新一轮对话。同样,由于对话处于持续进行的状态,发送至响应 API 的输入内容长度也会不断增加。

 

弃用简单参数费力做优化,就为了用户隐私?

“在对话过程中,发送至响应 API 的 JSON 数据量,是否会让智能体循环的时间复杂度达到二次方级别?”答案是肯定的。

 

据悉,尽管响应 API 支持通过可选的 previous_response_id 参数缓解这一问题,但目前 Codex 并未启用该参数,主要是为了保证请求完全无状态,并兼容零数据保留(ZDR) 配置,即不存储用户对话数据。

 

取而代之的,是两套需投入大量研发精力、涉及复杂实施流程的技术策略。文中,OpenAI 详细介绍了这两项硬核优化的具体方案。

 

通常,模型采样的开销远高于网络传输的开销,采样环节会成为优化效率的核心目标,这也是提示词缓存至关重要的原因,它能复用前一次推理调用的计算结果。当缓存命中时,模型采样的时间复杂度将从二次方降至线性。OpenAI 相关的提示词缓存文档对这一机制有更详细的说明:仅当提示词存在完全匹配的前缀时,才有可能实现缓存命中。为充分发挥缓存的优势,需将指令、示例等静态内容置于提示词开头,而将用户专属信息等可变内容放在末尾。这一原则同样适用于图片和工具,且其内容在各次请求中必须保持完全一致。

 

基于这一原则,Codex 中可能有以下导致缓存未命中的操作:

  1. 在对话过程中修改模型可调用的工具列表;

  2. 更换响应 API 请求的目标模型(实际场景中,这会改变原始提示词中的第三项内容,因该部分包含模型专属指令);

  3. 修改沙箱配置、审批模式或当前工作目录。

 

因此,Codex 团队在为命令行工具开发新功能时,必须审慎考量,避免新功能破坏提示词缓存机制。例如,他们最初对 MCP 工具的支持曾出现一个漏洞:工具的枚举顺序无法保持一致,进而导致缓存未命中。需要注意的是,MCP 工具的处理难度尤为突出,因为 MCP 服务器可通过 notifications/tools/list_changed 通知,动态修改其提供的工具列表。若在长对话过程中响应该通知,极易引发高成本的缓存未命中问题。

 

在可能的情况下,针对对话过程中发生的配置变更,他们会通过在输入中追加新消息的方式体现变更,而非修改已有的早期消息:

  • 若沙箱配置或审批模式发生变更,我们会插入一条新的role=developer消息,格式与原始的条目保持一致;

  • 若当前工作目录发生变更,我们会插入一条新的role=user消息,格式与原始的条目保持一致。

 

据介绍,为保障性能,OpenAI 在实现缓存命中方面投入了大量精力。除此之外,他们还重点管理了一项核心资源:上下文窗口。

 

其规避上下文窗口耗尽的通用策略是:一旦词元数量超过某个阈值,就对对话进行压缩。具体来说,会用一个更精简、且能代表对话核心内容的新条目列表替代原有输入,让智能体在继续执行任务时仍能理解此前的对话过程。早期的压缩功能实现方案,需要用户手动调用/compact 命令,该命令会结合现有对话内容和自定义的摘要生成指令,向响应 API 发起查询;Codex 则会将返回的、包含对话摘要的助手消息,作为后续对话轮次的新输入。

 

此后,响应 API 不断迭代,新增了专用的/responses/compact 端点,能以更高效率完成压缩操作。该端点会返回一个条目列表,可替代原有输入继续对话,同时释放出更多的上下文窗口空间。该列表中包含一个特殊的 type=compaction 条目,其附带的 encrypted_content 加密字段为透明化设计,可保留模型对原始对话的潜在理解。

 

现在,当词元数量超过 auto_compact_limit 自动压缩阈值时,Codex 会自动调用该端点对对话内容进行压缩。

极限扩容:用 1 个数据库扛住了 8 亿用户

在另一篇技术博文中,OpenAI 工程师 Bohan Zhang 介绍, OpenAI 通过严苛的技术优化与扎实的工程实践,对单个数据库 PostgreSQL 进行深度扩容,实现以单套体系支撑 8 亿用户、每秒数百万次查询的访问需求。

 

据称,多年来,PostgreSQL 一直是支撑 ChatGPT、OpenAI API 等核心产品的核心底层数据系统之一。过去一年,公司 PostgreSQL 的负载增长超 10 倍,且这一增长趋势仍在持续加速。OpenAI 称,PostgreSQL 的横向扩展能力远超此前行业普遍认知,能够稳定支撑规模大得多的读密集型工作负载。“这套最初由加州大学伯克利分校的科学家团队研发的系统,助力我们通过单主节点 Azure PostgreSQL 弹性服务器实例,搭配分布在全球多个区域的近 50 个只读副本,承接了海量的全球访问流量。”

 

而且,OpenAI 表示,其扩容在实现规模提升的同时,始终将延迟控制与可靠性优化放在核心位置:生产环境中,客户端 99 分位延迟稳定保持在十几毫秒的低水平,服务可用性达到五个九标准;过去 12 个月内,PostgreSQL 仅出现过一次零级严重故障,该故障发生在 ChatGPT 图像生成功能爆红上线期间,一周内超 1 亿新用户注册导致写流量突发暴涨超 10 倍。

 

尽管 PostgreSQL 的扩容成果已达预期,OpenAI 仍在持续探索其性能极限。目前,他们已将可分片的写密集型业务负载迁移至 CosmosDB 等分片式数据库系统;对于分片难度更高的剩余写密集型负载,相关迁移工作也在积极推进,以此进一步减轻 PostgreSQL 主节点的写压力。同时,OpenAI 正与微软 Azure 团队展开合作,推动级联复制功能落地,实现只读副本的安全、大规模扩容。随着基础设施需求的持续增长,其将继续探索更多扩容方案,包括基于 PostgreSQL 的分片架构改造、引入其他分布式数据库系统等。

 

有网友评价道,“这招的高明之处,就在于极简。他们没用什么花里胡哨的冷门技术,不过是把最佳实践做到了极致。过去十年,行业里全是 ‘一切皆分片、拥抱 NoSQL、全面分布式,为 CAP 定理折腰’ 的论调,而 OpenAI 倒好, 服务十亿级用户的解法,居然只是一句‘试过加只读副本吗?’”

 

参考链接:

https://openai.com/index/unrolling-the-codex-agent-loop/

https://openai.com/index/scaling-postgresql/

熟悉 Spring Boot 3 的开发者,都知道它在简化开发流程、提高开发效率方面的出色表现吧!但是,在实际业务场景中,大家肯定都碰到过这样的棘手问题:订单数据存放在 MySQL 里,库存数据在 PostgreSQL 中,用户数据又保存在 MongoDB 中,当多种数据源同时存在时,想要实现统一查询简直比登天还难。

所以呢,今天我就亮出我的“终极大招”——Apache Calcite,着重给大家讲讲它怎样与 Spring Boot 3 实现无缝集成,还会分享一些可以直接拿来使用的经典应用场景。掌握了这一招,多数据源查询的难题就能轻松解决啦!

一、核心认知:Apache Calcite 为何是多数据源查询的利器?

在动手集成前,咱们先把核心逻辑搞明白:为啥 Calcite 能成为多数据源查询的“万能钥匙”?它的核心优势到底在哪?

1.1 不止是查询引擎:Calcite 的核心定位

Apache Calcite 本质是一个动态数据管理框架,而非传统的数据库。它最核心的价值在于“解耦”——将数据存储与数据查询分离,无论数据存在哪里、是什么格式,都能通过统一的 SQL 接口进行查询。

说通俗点,Calcite 就像个“超级数据翻译官”——不管数据藏在哪个数据源里、是什么格式,你只要写一套标准 SQL,它就能翻译成对应数据源能懂的指令,最后把结果整理成统一格式返回。这也是它能搞定多数据源查询的核心秘诀!

1.2 Calcite 的核心能力拆解

统一 SQL 接口:支持标准 SQL,无论底层是关系型数据库(MySQL、PostgreSQL)、非关系型数据库(MongoDB、Redis),还是文件(CSV、Parquet)、大数据引擎(Hive、Spark),都能通过同一套 SQL 查询。

  1. 强大的查询优化:内置基于规则和成本的查询优化器,能自动优化 SQL 执行计划,提升查询效率,尤其是在复杂多表关联、跨数据源查询场景下,优化效果明显。
  2. 灵活的数据源适配:通过“适配器(Adapter)”机制适配不同数据源,社区已提供大量现成适配器,也支持自定义开发,适配特殊数据源。
  3. 轻量级集成:核心依赖体积小,无复杂依赖,可轻松集成到 Spring Boot、Spring Cloud 等主流 Java 开发框架中,无需单独部署独立服务(也支持独立部署)。

    二、重点实战:Spring Boot 3 集成 Calcite 核心步骤

    既然大家都熟悉 Spring Boot 3 的基础操作,我就不啰嗦项目搭建这些常规步骤了,直接聚焦 Calcite 集成的核心环节,每一步都附完整代码和避坑提醒,跟着做就能成!

2.1 核心依赖引入

第一步先引依赖,在 pom.xml 里加好 Calcite 核心包、对应数据源的适配器,再配上 MyBatis Plus 的核心依赖(替换掉原来的 Jdbc 依赖就行),具体如下:

<!-- Calcite 核心依赖 -->
<dependency>
    <groupId>org.apache.calcite</groupId>
    <artifactId>calcite-core</artifactId>
    <version>1.36.0</version> 
</dependency>

<!-- MySQL 适配器(用于适配 MySQL 数据源) -->
<dependency>
    <groupId>org.apache.calcite</groupId>
    <artifactId>calcite-mysql</artifactId>
    <version>1.36.0</version>
</dependency>

<!-- MongoDB 适配器(用于适配 MongoDB 数据源) -->
<dependency>
    <groupId>org.apache.calcite</groupId>
    <artifactId>calcite-mongodb</artifactId>
    <version>1.36.0</version>
</dependency>

<!-- Spring Boot 与 MyBatis Plus 集成核心依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.5</version> <!-- 适配 Spring Boot 3 的稳定版 -->
</dependency>

<!-- 数据库连接池依赖(MyBatis Plus 需连接池支持) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.20</version>
</dependency>

这里有 3 个避坑点必须强调下:

  1. Calcite 所有组件版本要统一,不然容易出现类加载异常;
  2. MyBatis Plus 得选适配 Spring Boot 3 的版本(3.5.3+);
  3. 一定要加连接池依赖,不然 Calcite 数据源没法被 MyBatis Plus 正常管理。

    2.2 核心配置:Calcite 模型文件编写

    模型文件是 Calcite 识别数据源的关键,一般用 JSON 格式,放在 resources 目录下命名为 calcite-model.json 就行。下面给大家一个适配 MySQL 和 MongoDB 双数据源的示例,直接改改连接信息就能用:

{
  "version": "1.0",
  "defaultSchema": "ecommerce",
  "schemas": [
    {
      "name": "ecommerce",
      "type": "custom",
      "factory": "org.apache.calcite.adapter.jdbc.JdbcSchema$Factory",
      "operand": {
        "jdbcUrl": "jdbc:mysql://localhost:3306/ecommerce_order?useSSL=false&serverTimezone=UTC",
        "username": "root",
        "password": "123456",
        "driver": "com.mysql.cj.jdbc.Driver"
      }
    },
    {
      "name": "user_mongo",
      "type": "custom",
      "factory": "org.apache.calcite.adapter.mongodb.MongoSchema$Factory",
      "operand": {
        "host": "localhost",
        "port": 27017,
        "database": "user_db",
        "collection": "user_info"
      }
    }
  ]
}

几个关键配置给大家解释清楚,避免踩坑:

  1. defaultSchema:默认查询的 Schema,可省略,查询时需指定 Schema 名称(如 ecommerce.order、user_mongo.user_info)。
  2. factory:对应数据源的适配器工厂类,Calcite 已为主流数据源提供现成工厂,自定义数据源需实现自己的 Factory。
  3. operand:数据源连接参数,根据数据源类型不同配置不同参数(如 MySQL 的 jdbcUrl、MongoDB 的 host/port)。

    2.3 Spring Boot 集成 Calcite + MyBatis Plus 核心配置

    这一步是核心,主要分两步走:

    配置好 Calcite 数据源;
    让 MyBatis Plus 用上这个数据源,顺便把 mapper 扫描、分页插件这些基础参数配好。直接上配置类代码:

    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import org.apache.calcite.jdbc.CalciteConnection;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.util.Properties;
    
    @Configuration
    // MyBatis Plus  mapper 接口扫描(指定 mapper 包路径)
    @MapperScan(basePackages = "com.example.calcite.mapper")
    public class CalciteMybatisPlusConfig {
    
     // 1. 配置 Calcite 数据源(核心,与原逻辑一致)
     @Bean
     public DataSource calciteDataSource() throws Exception {
         Properties props = new Properties();
         props.setProperty("model", "classpath:calcite-model.json");
         Connection connection = DriverManager.getConnection("jdbc:calcite:", props);
         CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class);
         return calciteConnection.getDataSource();
     }
    
     // 2. 配置 MyBatis Plus 的 SqlSessionFactory,指定使用 Calcite 数据源
     @Bean
     public SqlSessionFactory sqlSessionFactory(DataSource calciteDataSource) throws Exception {
         MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
         // 注入 Calcite 数据源
         sessionFactory.setDataSource(calciteDataSource);
         // 配置 mapper.xml 文件路径(如果使用 XML 方式编写 SQL)
         sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                 .getResources("classpath:mapper/*.xml"));
         // 配置 MyBatis Plus 全局参数(可选)
         org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
         configuration.setMapUnderscoreToCamelCase(true); // 下划线转驼峰
         sessionFactory.setConfiguration(configuration);
         // 注入 MyBatis Plus 插件(如分页插件)
         sessionFactory.setPlugins(mybatisPlusInterceptor());
         return sessionFactory.getObject();
     }
    
     // 3. MyBatis Plus 分页插件(可选,复杂查询分页用)
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
         interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 适配 Calcite 兼容的 MySQL 语法
         return interceptor;
     }
    
     // 4. 配置事务管理器(可选,需要事务支持时添加)
     @Bean
     public PlatformTransactionManager transactionManager(DataSource calciteDataSource) {
         return new DataSourceTransactionManager(calciteDataSource);
     }
    }

    核心逻辑给大家捋一捋:先通过 Calcite 创建统一的数据源,再把它注入到 MyBatis Plus 的 SqlSessionFactory 里。这样一来,咱们后续写代码就完全是 MyBatis Plus 的熟悉风格了,不管是 Mapper 接口还是 XML 映射文件,都能直接用,跨数据源查询的复杂逻辑全交给 Calcite 处理。

2.4 核心查询实现(MyBatis Plus 风格)

接下来就是大家最熟悉的查询实现环节了,我用 MyBatis Plus 最常用的“Mapper 接口+注解”和“XML”两种方式来演示,还是以 MySQL 订单表和 MongoDB 用户表的关联查询为例,大家可以根据自己的习惯选:

(1)定义实体类(对应跨数据源查询结果,可使用 lombok 简化代码)

import lombok.Data;

@Data
public class UserOrderVO {
    private String orderId;      // 订单 ID(来自 MySQL)
    private String orderTime;    // 下单时间(来自 MySQL)
    private BigDecimal amount;   // 订单金额(来自 MySQL)
    private String userName;     // 用户名(来自 MongoDB)
    private String phone;        // 手机号(来自 MongoDB)
    private String userId;       // 用户 ID(关联字段)
}

(2)定义 Mapper 接口(MyBatis Plus 风格,无需编写实现类)

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;

// 继承 BaseMapper,获得 MyBatis Plus 基础 CRUD 能力
public interface UserOrderMapper extends BaseMapper<UserOrderVO> {
    // 注解方式编写跨数据源关联 SQL
    @Select("SELECT " +
            "o.order_id AS orderId, o.order_time AS orderTime, o.amount, " +
            "u.user_name AS userName, u.phone, o.user_id AS userId " +
            "FROM ecommerce.order o " +  // ecommerce:MySQL 的 Schema;order:订单表
            "JOIN user_mongo.user_info u " +  // user_mongo:MongoDB 的 Schema;user_info:用户表
            "ON o.user_id = u.user_id " +
            "WHERE o.user_id = #{userId}")
    List<UserOrderVO> queryUserOrderByUserId(@Param("userId") String userId);

}

(3)编写 Service 层

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserOrderServiceImpl extends ServiceImpl<UserOrderMapper, UserOrderVO> implements UserOrderService {
    @Override
    public List<UserOrderVO> getUserOrderByUserId(String userId) {
        // 调用 Mapper 接口方法,实现跨数据源查询
        return baseMapper.queryUserOrderByUserId(userId);
        // 若使用 XML 方式:return baseMapper.queryUserOrderByUserIdWithXml(userId);
    }
}

(4)编写 Controller 层

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
public class CrossDataSourceQueryController {
    @Autowired
    private UserOrderService userOrderService;

    @GetMapping("/user/order/{userId}")
    public List<UserOrderVO> queryUserOrder(@PathVariable String userId) {
        // 调用 Service 方法,返回跨数据源查询结果
        return userOrderService.getUserOrderByUserId(userId);
    }
}

最后再划 3 个重点,确保大家少走弯路:

  1. 实体类字段要和查询结果列名对应,用别名适配下划线转驼峰更省心;
  2. Mapper 接口继承 BaseMapper 后,MyBatis Plus 的分页、条件构造器这些功能都能直接用,复杂查询也能轻松搞定;
  3. 咱们写的都是标准 SQL,Calcite 会自动解析适配不同数据源,完全不影响大家原来的开发习惯。

    三、深度解析:Calcite 的经典使用场景

    讲完了集成步骤,再跟大家深度拆解下 Calcite 的经典落地场景。毕竟技术最终要服务于业务,这些场景都是我在实际项目中常用到的,拿来就能用!

第一个经典场景是多系统数据融合查询,这也是企业级中台的核心需求。做企业级中台的小伙伴肯定深有体会,大型企业里数据都是分散的——订单系统用 MySQL,用户系统用 MongoDB 存行为数据,库存系统用 PostgreSQL。要是想做“用户-订单-库存”全链路分析,传统做法得分别调三个系统的接口,再在业务层手动整合数据,不仅效率低,还容易出错。用 Calcite 分别适配这三个数据源后,只要写一套标准 SQL 就能实现跨数据源关联查询,咱们用 Spring Boot 3 搭好接口服务,业务层完全不用管数据存在哪,专注核心业务逻辑就行,亲测开发效率能提升 50%以上,再也不用写重复的接口调用和数据整合代码,而且 Calcite 的查询优化器会自动优化关联逻辑,查询效率也能跟上。

第二个场景是实时数据与离线数据联动查询,做电商的小伙伴应该经常遇到这类需求。比如实时订单数据存在 Kafka 里,历史订单数据存在 Hive 里,运营需要实时查看“今日订单+近 30 天历史订单”的汇总数据来做实时监控和决策。这种情况不用麻烦地把 Kafka 数据同步到 Hive,也不用把 Hive 数据同步到实时库,直接用 Calcite 的 Kafka 适配器(calcite-kafka)和 Hive 适配器(calcite-hive),就能把实时流数据和离线数据放到同一个查询体系里,写一条 SQL 就能实现“实时+离线”数据的联合查询,既省了大量数据同步成本,又能兼顾实时性和准确性,还支持增量查询。

第三个场景是自定义数据源适配,主要解决特殊格式数据查询的难题。企业里总有很多 CSV、Excel、Parquet 格式的文件数据,传统做法是先把这些文件导入数据库才能查询,步骤又多又耗时,尤其是临时做数据分析的时候,导入数据库的成本太高了。而 Calcite 内置了文件适配器(calcite-file),支持直接查询这些文件数据,根本不用导入数据库。咱们再结合 Spring Boot 3 的文件上传功能,还能实现“文件上传后直接用 SQL 查询”的需求,临时分析数据超方便。如果有企业内部的特殊格式文件,比如自定义的二进制文件,也可以自己实现 Calcite 的 SchemaFactory 和 TableFactory 接口,写个自定义适配器,就能适配这些特殊数据源了。

四、避坑指南:集成注意事项与优化建议

4.1 这些坑一定要避开!

  • 适配器版本要统一:Calcite 核心依赖和各数据源适配器的版本必须一致,不然很容易出现类加载异常,这个坑我踩过,大家一定要注意。
  • 模型文件配置要规范:Schema 名称、表名要清晰,别重复;数据源的地址、端口、账号密码这些连接参数一定要准确,错一个就会连接失败。
  • 要考虑数据源性能:跨数据源查询的性能取决于最慢的那个数据源,所以要确保每个数据源自身性能没问题,不然会拖慢整个查询。

    4.2 优化小技巧,查询更快更稳

  • 启用 Calcite 缓存:配置一下 Calcite 的元数据缓存和查询计划缓存,能减少重复解析和元数据查询的时间,提升查询效率。
  • 优化 SQL 写法:尽量避免复杂的多表关联,能把过滤条件下推到数据源的就尽量下推。虽然 Calcite 会自动优化,但手动优化后的效果会更好。
  • 自定义优化规则:如果是特别复杂的业务场景,可以自己实现 Calcite 的 OptimizerRule 接口,写自定义的查询优化规则,进一步提升查询效率。

    五、本文总结

    最后总结一下,对于熟悉 Spring Boot 3 的咱们来说,集成 Calcite 的关键就是理解它“统一查询”的核心思想,把模型文件写对、核心 Bean 配置好,就能快速实现多数据源查询能力了。https://mybj123.com/28732.html

前言

前阵子写的日志分析工具NginxPulse,自开源以来,已过去 2 周时间,目前 GitHub 已收获 1.5k 的 star 。收到了不少用户的反馈建议,花了点时间将这些问题都处理了下。

本文就跟大家分享下新版本都解决了哪些问题,优化了哪些内容,欢迎各位感兴趣的开发者阅读本文。

抛弃 SQLite

有不少用户反馈说日志文件很大的时候( 10G+),解析速度非常慢,需要解析好几个小时,解析完成之后数据看板的查询也比较慢(接口响应在 5 秒左右)。

于是,我重写了日志解析策略(解析阶段不做 IP 归属地查询,仅入库其他数据,将日志中 IP 记录起来),日志解析完毕后,将记录的 IP 做去重处理,随后去做归属地的查询处理(优先本地的 ip2region 库,远程的 API 调用查询做兜底),最后将解析到的归属地回填至对应的数据库表中,这样一套下来就可以大大提升日志的解析速度。

数据库的数据量大了之后,SQLite 的表现就有点差强人意了,请教了一些后端朋友,他们给了我一些方案,结合我自身的实际场景后,最后选定了 PostgreSQL 作为新的数据库选型。

这套方案落地后,用户群的好兄弟说:他原先需要解析 1 个小时的日志,新版只需要 10 多分钟。

6c1c8781ddb810d57c9f508fdaf47025

UI 配置可视化使用

有一部分用户反馈说他非专业人士,这些晦涩的配置对他来说使用门槛太高了,希望能有一个 UI 配置页面,他只需要点一点、敲敲键盘,就能完成这些配置。

我将整个配置流程做成了 4 步,同时也准备一个[演示视频](NginxPulse 支持 UI 配置化了) - https://www.bilibili.com/video/BV1hqzyBVEU9:

  • 配置站点
  • 配置数据库
  • 配置运行参数
  • 确认最终配置

image-20260125235847464

新增 wiki 文档

因为配置过于庞大,仓库主页浏览 README.md 比较费劲,希望能整理一份 wiki 文档发上去。

花了点时间,简化了下 README ,整理了一份: https://github.com/likaia/nginxpulse/wiki

image-20260126000555725

访问明细模块优化

有部分用户反馈说希望增加更多的筛选条件以及导出 Excel 功能,现在它来了:

image-20260126001010068

概况模块优化

概况页面的日期筛选之前放在趋势分析卡片的上方,但是他的切换影响的维度还包含了指标,于是我就调整了下它的位置,新版如下图所示:

image-20260126001325265

项目地址

写在最后

至此,文章就分享完毕了。

我是神奇的程序员,一位前端开发工程师。

如果你对我感兴趣,请移步我的个人网站,进一步了解。

Ruby on Rails 后端工程师(远程 )

职位概述

我们正在寻找一名 Ruby on Rails 工程师,参与实际业务系统的开发与维护。
你将以 远程办公 的形式加入项目,按实际工时结算薪资,合作方式灵活,适合自由职业者或希望长期远程合作的开发者。


工作内容

  • 使用 Ruby on Rails 进行 Web 后端功能开发与维护
  • 参与业务需求的技术实现,与产品/设计协作落地功能
  • 编写结构清晰、可维护的代码,并进行必要的调试与优化
  • 根据项目需要,参与 API 设计、数据库建模等工作
  • 通过文档、Issue 或英文技术资料理解并解决问题


任职要求

  • 熟悉 Ruby on Rails,具备实际项目开发经验

  • 掌握基本的 Web 开发知识( HTTP 、RESTful 、数据库等)

  • 熟悉常见数据库(如 PostgreSQL / MySQL )

  • 具备 基础英文读写能力

  • 能阅读英文技术文档、Issue 、报错信息

  • 能进行简单的英文技术沟通(不要求口语流利)

  • 具备良好的自我管理能力,能够适应远程协作

  • 对代码质量负责,有持续学习和改进的意识


加分项(非必须)

  • 有远程工作或自由职业经验
  • 熟悉 RSpec 、Sidekiq 、Redis 、API 设计等 Rails 常见生态
  • 有性能优化、系统重构经验
  • 熟悉 Git / GitHub 协作流程


合作方式 & 薪资

  • 工作方式:远程办公

  • 计薪方式:按工时结算

  • 薪资范围80 – 140 元人民币 / 小时

  • 具体根据技术水平、合作稳定性综合确定

  • 工时与任务透明,长期合作优先


我们能提供

  • 灵活、尊重专业的合作方式
  • 清晰的需求沟通,不无意义加班
  • 有技术挑战、能真正落地的项目
  • 长期合作机会(不是一次性外包)

如有意向,请投递简历至 [email protected]

“我的笔记本是 16G 内存的 M3 Pro ,为什么我还需要一台只有 4 核 8G 的服务器?”

在 Reddit 的 r/indiehackers 板块,这是新手最常问的问题之一。在 Serverless (如 Vercel )和 PaaS (如 Supabase )横行的今天,VPS ( Virtual Private Server ,虚拟专用服务器)似乎显得有些“老派”。

但现实是:真正能跑通商业闭环、实现长期盈利的独立开发者,手里一定攥着几台 VPS 。

本文将从独立开发的 7 个核心痛点出发,深度解析为什么 VPS 是你迈向专业化、摆脱“代码玩具”的必经之路。


1. 摆脱“本地焦虑”:解决 node_modules 与 Docker 的空间黑洞

独立开发者最昂贵的资产是笔记本,而最廉价的则是笔记本硬盘。这波 AI 编程大部分都是 NextJS ,这也就带来了 node_modules 灾难。其实还有 cc 居然也喜欢拉 bb 。如果观察 cc 的执行过程,会发现它一直要写东西去 /tmp 目录

  • 痛点:硬盘与性能的双重榨干


    • node_modules 爆炸:同时维护 10 个项目,node_modules 能吃掉 50GB 以上的 SSD 。
    • Docker 镜像堆积:在本地运行容器会让系统响应迟滞,风扇咆哮。
    • 计算占用:本地运行 PostgreSQL 或 Redis 等中间件会显著拖慢 IDE 的响应速度。
  • 解决方案:VPS 作为“重型计算中心”
    你只需在本地保留一个轻量的 VS Code + Cursor,通过 Remote SSH 连接 VPS 。所有的重型依赖和环境都在云端运行,笔记本只负责显示 UI 。

图 1:本地开发负载 vs. VPS 远程卸载对比

2. 拒绝“SaaS 账单勒索”:从商业逻辑看成本控制

独立开发最怕的不是没用户,而是用户还没付钱,SaaS 账单先爆了。最近几年做 AI 编程,难免会接触到 supabase ,clerk 等工具,其实包括 vercel 也一样,用下来会发现一开始很爽,然后爽着爽着,账单就爆炸了。vercel 有个很有意思的坑,就是 Image 组件,编译的时候会提示最好用 <Image 组件,听起来很贴心对吧?但这个组件默认走 Vercel 的图片优化服务——每优化一张图就计费一次。流量大的站点,光图片优化费用就能超过主机费用。

Vercel 的 Hobby 免费套餐非常诱人——部署、CDN 、SSL 全包。但一旦你的项目有了流量,噩梦就开始了。

超额收费一览

资源 Pro 套餐包含 超出后收费
带宽 1 TB/月 $0.15/GB(即 $150/TB )
Edge Requests 1000 万/月 $2/百万
Serverless 执行时间 40 小时/月 $5/小时
图片优化 5000 张/月 $5/1000 张
  • 痛点:被绑架的扩展成本


    • PaaS 陷阱:Firebase 的免费额度诱人,但一旦涉及复杂备份或高并发,价格呈指数级增长。
    • 身份验证收费:Clerk 等按月活用户收费,对高频低客单价应用是噩梦。
  • 解决方案:全栈自建( Self-hosting )
    在 $5/月 的 VPS 上,你可以利用 Docker 跑满性能,同时运行:数据库( PostgreSQL )、验证系统( PocketBase )和统计系统( Umami )。

图 2:SaaS 订阅 vs. VPS 固定成本曲线对比

💡 公平地说:自建服务确实需要一定的运维能力。但最近很多海外开发者分享了自己维护 PostgreSQL 的经验——比想象中简单得多,尤其是有了 Docker 和自动备份脚本之后。后面我会详细讲怎么做。

3. 真正的 CI/CD:构建“一人 IT 部门”的自动化流水线

独立开发者的核心竞争力在于迭代速度。部署到 vercel 、cloudflare 、Netfily 等 servless 平台在早期验证需求的时候,是非常好的,但是这些平台的问题是,它们的 node 实现是不完备的,一些长时间的任务就没法跑。以前本地打包机器就开始呼啸,通过 github 的 action ,这个事不用操心了,弄好就是 docker 镜像,然后,起飞了。

  • 执行时间限制:Serverless 函数通常有 10-60 秒的超时限制,一般默认是 10s

  • 无持久进程:WebSocket 、长连接、后台任务都很别扭

  • 冷启动延迟:首次请求可能需要等待数秒

  • 痛点:手动部署的低效与错误
    如果你还在用手动执行 git pull,你不仅在浪费生命,还在增加生产事故的概率。

  • 解决方案:基于 VPS 的轻量自动化
    利用 VPS 运行 GitHub Actions Runner


    1. Git Push 触发流水线。
    2. VPS 自动拉取代码并构建 Docker 镜像。
    3. Docker Compose 自动重启容器,实现零停机更新。

图 3:基于 VPS 的自动化 CI/CD 流水线示意图

不知道是不是这个原因,现在 cloudflare 也不咋推 pages 了,又回到 worker ,感觉挺难用的,你怎么看?

4. 解决“网络壁垒”:从静默爬虫到跨境访问

很多项目在本地跑不通,不是代码问题,而是网络环境问题。开发用都的很多 npm 包,或者其他的资源,常常会因为网络,把人给气死,累死,折腾死,烦死。

  • 痛点:变动的 IP 与受限的出口


    • 固定 IP 需求:对接 Stripe 、PayPal 或银行 API 时,通常需要固定的公网 IP 做白名单。家庭宽带的动态 IP 根本没法用。
    • 网络环境问题:开发时用到的很多 npm 包、Docker 镜像、GitHub 资源,经常因为网络问题把人折腾得够呛。
    • 反爬虫封禁:如果你在做数据采集相关的项目,家庭宽带 IP 极易被反爬策略封禁。
  • 解决方案:VPS 作为全局网络枢纽


    • 固定身份标识:为业务提供永久的公网 IP ,Stripe Webhook 、OAuth 回调都能稳定工作。
    • 反向代理中心:一个 VPS 配合 Nginx 或 Caddy ,可以管理 10+ 个域名并映射到不同的本地端口。
    • 开发环境加速:npm install 、docker pull 都在 VPS 上执行,下载速度飞快,不再受本地网络限制。

image.png

和 nginx proxy manager 有仇,已经好几次了,弄它的 Docker ,能占 10 来 G 的空间,完全不理解,caddy 就小巧很多。

5. 守护“睡后收入”:24/7 监控与容灾

独立开发最痛苦的时刻,是早上醒来发现服务已经挂了一整晚,而你毫无察觉。(希望是伪命题,真来钱的项目,还是很上心的!)

痛点:缺乏哨兵

  • 本地电脑会休眠,没法做持续监控
  • 免费的外部监控工具检测频率太低(如 5 分钟/次),发现问题时用户早就流失了
  • 很多问题是"偶发性"的,等你手动检查时一切正常

解决方案:自建监控站

在 VPS 上部署 Uptime Kuma(或类似工具),每 30-60 秒检测一次全球访问状况。一旦挂掉,立即通过 Telegram 、Discord 或邮件通知。

监控清单建议

监控项 检测频率 告警方式
HTTP 状态码 60 秒 Telegram 即时通知
SSL 证书到期 每天 提前 14 天预警
服务器资源 5 分钟 CPU/内存超 80% 告警
数据库连接 60 秒 连接失败立即通知

进阶玩法

  • Uptime Kuma 做可用性监控
  • BezelNetdata 做服务器资源监控,Bezel 还挺好用的。Netdata 稍微重点。
  • 两者结合,形成完整的监控闭环

图 4:全天候监控与即时告警闭环

6. 数据主权:独立开发的“最后防线”

  • 痛点:平台依赖风险

    如果你的数据全在 Firebase ,某天账号因为合规问题被封,你的所有努力将瞬间清零。

  • 解决方案:VPS 本地化存储 + 异地备份


    • 数据隔离:数据库文件完全属于你。
    • 自动化备份:编写一个简单的 Cron 任务,每天定时将数据加密并同步到 S3 或你的本地存储。

image.png

7. 独立开发者的资源规划:“1 + N” 策略

针对 2026 年的典型开发场景,我们建议采用以下阵列:

类型 规格建议 核心作用
1 台主领地 2 核 4G 或 4 核 8G 运行 Nginx 、核心数据库、核心产品。
N 台哨兵机 1 核 1G 或更低 运行 Uptime Kuma 监控、小型爬虫、测试环境。
为什么需要分开?
  • 监控服务不应该和被监控的服务在同一台机器——否则机器挂了你也收不到告警
  • 测试环境和生产环境隔离,避免误操作
  • 多台小机器比一台大机器更有弹性

image.png

Reddit 上 Hetzner 被反复提及为"性价比之王":同样的价格,配置通常是美国云服务商的 2-3 倍。缺点是机房主要在欧洲,亚洲访问延迟较高。

咋说呢? 数据库还是很重要的,如果精力有限,就还是用 neon 或者 supabase 之类的。

总结:从“玩票”到“专业”的入场券

拥有 VPS 的那一刻起,你就不再只是一个“写代码的人”,而是一个 “系统的掌控者”。它为你提供了:

  • 确定性:不再受本地环境变化的干扰。
  • 连续性:产品 24 小时独立生存。
  • 商业性:以最低的边际成本支撑业务增长。

正如独立开发圈子里流传的一句话:“你的第一个服务器 IP ,就是你产品的第一张名片。”(我编的)

VPS 入门:为什么独立开发者需要一台 VPS ?( 2026 深度版)

1. 这是个什么东西?

这是个数据库万能连接器的 MCP,可以使用支持 MCP 协议的工具(例如:Claude Desktop、Cherry Studio 等)直接连接你的数据库,用自然语言查询和分析数据。

2. 有什么作用?

  • 临时数据分析 :想快速查看生产数据库的某些指标,但是不想写 SQL
  • 问题排查 :需要跨多个表关联查询,但记不清表结构
  • AI 辅助开发 :希望 Claude 能直接理解你的数据库结构,生成准确的查询
  • 生成可视化大屏分析:通过自然语言描述,自动生成可视化大屏分析
    这个 MCP 连接了具有 MCP 协议的客户端和数据库,只要模型够给力,有一堆想不到的能力等你自己探索。

3. 有什么特性?

自然语言查询 - 用中文描述需求,Claude 自动生成并执行 SQL
智能表结构理解 - 自动获取数据库 Schema,提供精准建议
多数据库支持 - MySQL、PostgreSQL、Redis 一键切换 (后续还会增加)
安全第一 - 默认只读模式,防止误操作删库
开箱即用 - 无需复杂配置,一行命令启动

4. 简单的效果预览:

以 MySQL 为例,有以下几个表数据:

  • users 表:
  • categories 表
  • products 表
  • orders 表
  • order_items 表

4.1 Claude Desktop 效果



【开源自荐 5】MCP 数据库万能连接器:用自然语言查询和分析数据9
【开源自荐 5】MCP 数据库万能连接器:用自然语言查询和分析数据15

4.2 Cherry Studio 效果




5. 如何使用?

只要是支持 MCP 协议的工具都可以使用,这里只介绍 Claude Desktop 和 Cherry Studio 的配置,配置都类似。

5.1 配置 Claude Desktop

编辑 Claude Desktop 配置文件:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json

添加以下配置:

MySQL 使用示例

基础配置(只读模式)

{ "mcpServers": { "mysql-db": { "command": "npx", "args": [ "universal-db-mcp", "--type", "mysql", "--host", "localhost", "--port", "3306", "--user", "root", "--password", "your_password", "--database", "myapp_db" ] } } } 

启用写入模式(谨慎使用)

{ "mcpServers": { "mysql-dev": { "command": "npx", "args": [ "universal-db-mcp", "--type", "mysql", "--host", "localhost", "--port", "3306", "--user", "dev_user", "--password", "dev_password", "--database", "dev_database", "--danger-allow-write" ] } } } 

PostgreSQL 使用示例

基础配置

{ "mcpServers": { "postgres-db": { "command": "npx", "args": [ "universal-db-mcp", "--type", "postgres", "--host", "localhost", "--port", "5432", "--user", "postgres", "--password", "your_password", "--database", "myapp" ] } } } 

连接远程数据库

{ "mcpServers": { "postgres-prod": { "command": "npx", "args": [ "universal-db-mcp", "--type", "postgres", "--host", "db.example.com", "--port", "5432", "--user", "readonly_user", "--password", "secure_password", "--database", "production" ] } } } 

Redis 使用示例

基础配置(无密码)

{ "mcpServers": { "redis-cache": { "command": "npx", "args": [ "universal-db-mcp", "--type", "redis", "--host", "localhost", "--port", "6379" ] } } } 

带密码和数据库选择

{ "mcpServers": { "redis-session": { "command": "npx", "args": [ "universal-db-mcp", "--type", "redis", "--host", "localhost", "--port", "6379", "--password", "redis_password", "--database", "1" ] } } } 

启动使用

  1. 重启 Claude Desktop
  2. 在对话中直接询问:
  • “帮我查看 users 表的结构”
  • “统计最近 7 天的订单数量”
  • “找出消费金额最高的 10 个用户”

Claude 会自动调用数据库工具完成查询!

同时连接多个数据库

你可以在 Claude Desktop 中同时配置多个数据库连接:

{ "mcpServers": { "mysql-prod": { "command": "npx", "args": [ "universal-db-mcp", "--type", "mysql", "--host", "prod-db.example.com", "--port", "3306", "--user", "readonly", "--password", "prod_password", "--database", "production" ] }, "postgres-analytics": { "command": "npx", "args": [ "universal-db-mcp", "--type", "postgres", "--host", "analytics.example.com", "--port", "5432", "--user", "analyst", "--password", "analytics_password", "--database", "warehouse" ] }, "redis-cache": { "command": "npx", "args": [ "universal-db-mcp", "--type", "redis", "--host", "cache.example.com", "--port", "6379", "--password", "cache_password" ] } } } 

重启 Claude Desktop 后,你可以在对话中指定使用哪个数据库:

  • “在 MySQL 生产库中查询…”
  • “从 PostgreSQL 分析库获取…”
  • “检查 Redis 缓存中的…”

5.2 配置 Cherry Studio

6. 开源地址

universal-db-mcp
如果这个项目对你有帮助,请给个 Star 支持一下!
如果这个项目对你有帮助,请给个 Star 支持一下!
如果这个项目对你有帮助,请给个 Star 支持一下!
希望大家帮忙多多 star!!!


📌 转载信息
原作者:
anarkh
转载时间:
2026/1/23 12:02:03

Kite:Kotlin/Java 通用的全自动 ORM 框架

Kite 是一个高效的轻量级 ORM 框架,基于 Kotlin 编写,开箱即用,内置分页查询、增删改查等常用功能,支持多表操作。它支持 PostgreSQL、MySQL、Derby 等多种数据库,旨在通过简化数据库操作,减少代码量,提升开发效率。

框架特点

  • 全自动映射:无需手动编写 SQL,Kite 会自动根据实体类生成相应的数据库操作语句
  • 支持自定义 SQL:在需要时,可以编写自定义 SQL 语句,满足复杂查询需求,还可以像写代码一样写流程控制语句
  • 多数据库支持:支持 PostgreSQL、MySQL、Derby 等主流关系型数据库
  • Kotlin/Java 双语言支持:既可以在 Kotlin 项目中使用,也可以在 Java 项目中无缝集成
  • 轻量级设计:无过多依赖,性能优秀
  • 丰富的 API:提供简洁直观的 API,支持各种复杂查询和操作
  • Spring Boot 集成:提供 Spring Boot Starter,便于在 Spring Boot 项目中快速集成

使用方法(Spring Boot 集成示例)

Maven 中央仓库: kite-spring-boot-starter
  1. 向项目添加以下依赖:
  • Maven
<dependency>
   <groupId>io.github.tangllty</groupId>
   <artifactId>kite-spring-boot-starter</artifactId>
   <version>${kite.version}</version>
</dependency>
  • Gradle
implementation("io.github.tangllty:kite-spring-boot-starter:${kite.version}")
  1. 在数据库中创建表
使用 MySQL 演示
create table account (
  id          bigint not null auto_increment,
  username    varchar(32)     default '',
  password    varchar(32)     default '',
  balance     decimal(10,2)   default '0.00',
  create_time datetime        default null,
  update_time datetime        default null,
  primary key (`id`)
);

insert into account (username, password, create_time, balance) values
('admin', 'admin123', '2020-01-01 12:00:00', 1000.10),
('user', 'user123', '2024-05-02 8:30:00', 101.00),
('guest', 'guest123', '2022-03-03 15:00:00', 10.00),
('tang', 'tang123', '2019-06-01 21:30:30', 1.88),
('jeo', 'jeo123', '2024-07-01 5:59:59', 0.10);
  1. application.yml 文件中配置数据库连接信息
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/kite-test
    username: root
    password: password
  1. account 表创建模型类
  • Java
import com.tang.kite.annotation.id.Id;
import com.tang.kite.annotation.id.IdType;
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class Account {

    @Id(type = IdType.AUTO)
    private Long id;
    private String username;
    private String password;
    private BigDecimal balance;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    // Getters and Setters
}
  • Kotlin
import com.tang.kite.annotation.id.Id
import com.tang.kite.annotation.id.IdType
import java.math.BigDecimal
import java.time.LocalDateTime

class Account (

    @Id(type = IdType.AUTO)
    var id: Long? = null,
    var username: String? = null,
    var password: String? = null,
    var balance: BigDecimal? = null,
    var createTime: LocalDateTime? = null,
    var updateTime: LocalDateTime? = null

)
  1. 继承 BaseMapper 接口创建 Mapper 接口
  • Java
import com.tang.kite.mapper.BaseMapper;
import com.tang.kite.spring.annotation.Mapper;

@Mapper
public interface AccountMapper extends BaseMapper<Account> {
}
  • Kotlin
import com.tang.kite.mapper.BaseMapper
import com.tang.kite.spring.annotation.Mapper

@Mapper
interface AccountMapper : BaseMapper<Account>
  1. 在 Spring Boot 应用类上添加 @MapperScan 注解
  • Java
import com.tang.kite.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.tang.application.mapper")
@SpringBootApplication
public class KiteApplication {

    public static void main(String[] args) {
        SpringApplication.run(KiteApplication.class, args);
    }

}
  • Kotlin
import com.tang.kite.spring.annotation.MapperScan
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@MapperScan(["com.tang.application.mapper"])
@SpringBootApplication
class KiteApplication

fun main(args: Array<String>) {
    runApplication<KiteApplication>(*args)
}
  1. 测试 Mapper 接口
  • Java
import com.tang.demo.mapper.AccountMapper;
import com.tang.kite.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.tang.application.mapper")
@SpringBootApplication
public class KiteApplication {

    public static void main(String[] args) {
        var context = SpringApplication.run(KiteApplication.class, args);
        var accountMapper = context.getBean(AccountMapper.class);
        var accounts = accountMapper.select();
        accounts.forEach(System.out::println);
    }

}
  • Kotlin
import com.tang.demo.mapper.AccountMapper
import com.tang.kite.spring.annotation.MapperScan
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@MapperScan(["com.tang.application.mapper"])
@SpringBootApplication
class KiteApplication

fun main(args: Array<String>) {
    val context = runApplication<KiteApplication>(*args)
    val accountMapper = context.getBean(AccountMapper::class.java)
    val accounts = accountMapper.select()
    accounts.forEach { println(it) }
}

文档与社区

官方文档

详细的使用文档请参考:

源码

Kite 的源码托管在 GitHub 和 Gitee 上,您可以在以下地址查看和贡献:

总结

Kite 是一个功能强大、易于使用的 ORM 框架,它通过全自动映射和简洁的 API,大大简化了数据库操作的开发工作。无论是在 Kotlin 项目还是 Java 项目中,都能提供高效、便捷的数据库访问体验。

如果您正在寻找一个轻量级、高性能的 ORM 框架,Kite 绝对值得一试!

关于作者:

Nickyoung,数据库领域从业者。PostgreSQL ACE,IvorySQL专家顾问委员会成员。

公众号 “ 👉 PostgreSQL 运维之道 ”。

给大家分享一个有趣的案例,同一个 sql,索引扫描比全表顺序扫描获取的数据更少。本篇我们深入分析一起索引排序规则损坏的案例,并 debug 验证索引扫描的主要过程。

问题现象

走索引扫描查询到 1 条数据。

testidx=# explain analyze select *  from user_info where userid ='1230005998';
                                                          QUERY PLAN                                                           
-------------------------------------------------------------------------------------------------------------------------------
 Index Scan using index_userid on user_info  (cost=0.28..35.61 rows=9 width=57) (actual time=0.030..0.032 rows=1 loops=1)
   Index Cond: ((userid)::text = '1230005998'::text)
 Planning Time: 0.118 ms
 Execution Time: 0.057 ms
(4 rows)

testidx=# select ctid,userid,region_id from user_info where userid ='1230005998';
  ctid  | userid    | region_id 
--------+----------------------+-----------
 (4,39) | 1230005998 | abc
(1 row)

不走索引顺序扫描查询到 11 条数据。

testidx=# set enable_indexscan to off;
SET
testidx=# explain analyze select * from user_info where userid ='1230005998';
                                                QUERY PLAN                                                
----------------------------------------------------------------------------------------------------------
 Seq Scanon user_info  (cost=0.00..51.50rows=9 width=57) (actual time=0.093..0.460rows=11 loops=1)
   Filter: ((userid)::text = '1230005998'::text)
   Rows Removed by Filter: 1309
 Planning Time: 0.116 ms
 Execution Time: 0.478 ms
(5rows)

testidx=# select ctid,userid,region_id from user_info where userid ='1230005998';
  ctid   | userid    | region_id 
---------+----------------------+-----------
 (4,39)  | 1230005998 | abc
 (9,14)  | 1230005998 | abc
 (9,32)  | 1230005998 | abc 
 (10,32) | 1230005998 | abc
 (12,5)  | 1230005998 | abc
 (26,23) | 1230005998 | abc
 (27,4)  | 1230005998 | abc
 (27,9)  | 1230005998 | abc
 (27,11) | 1230005998 | abc
 (34,38) | 1230005998 | abc
 (34,39) | 1230005998 | abc
(11rows)

testidx=#

对比两次查询结果,可以看到走索引扫描时,仅查询到第一条匹配的数据,对应 ctid 为(4,39)。索引损坏了?

问题分析

当我们怀疑索引损坏时,可以使用 amcheck 插件对索引进行扫描分析,检查是否存在异常。

可以看到 leaf page 8 的 itemoffset 24 和 25 违反了条目顺序不变性规则。即按照升序原则 24 号索引槽位对应的键值要小于等于 25 槽位,但经检查是大于的,所以排序规则混乱了。

testidx=# select * from bt_index_check('index_userid',true);
DEBUG:  StartTransaction(1) name: unnamed; blockState: DEFAULT; state: INPROGRESS, xid/subid/cid: 0/1/0
DEBUG:  verifying level 1 (true root level)
DEBUG:  verifying 7 items on internal block 3
DEBUG:  verifying level 0 (leaf level)
DEBUG:  verifying 207 items on leaf block 1
DEBUG:  verifying 204 items on leaf block 2
DEBUG:  verifying 204 items on leaf block 4
DEBUG:  verifying 204 items on leaf block 5
DEBUG:  verifying 204 items on leaf block 6
DEBUG:  verifying 235 items on leaf block 7
DEBUG:  verifying 78 items on leaf block 8
ERROR:  item order invariant violated for index "index_userid"
DETAIL:  Lower index tid=(8,24) (points to heap tid=(4,14)) higher index tid=(8,25) (points to heap tid=(9,14)) page lsn=1/331E9F98.
testidx=# 

使用 pageinspect 扩展,查看 leaf page 8 有 78 条记录,其中 itemoffset 24 和 25 对应的键值,24 的键值为'31 09 xxx',25 的键值为'2b 4c xxx',前者大,确实是有问题的。

testidx=# select * from bt_page_stats('index_userid',8);
 blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags 
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
     8 | l    |         78 |          0 |            31 |      8192 |      5356 |         7 |         0 |    0 |          1
(1 row)

testidx=# 
testidx=# select * from  bt_page_items('index_userid',8) where itemoffset in (22,23,24,25);
 itemoffset |  ctid  | itemlen | nulls | vars |                                  data                                   
------------+--------+---------+-------+------+-------------------------------------------------------------------------
         22 | (4,39) |      32 | f     | t    | 2b 4c 54 34 33 36 32 35 31 33 34 00 00 00 00 00 00 00 00 00 00 00 00 00
         23 | (4,9)  |      32 | f     | t    | 31 09 0d 0a 4c 54 34 33 36 32 35 31 33 34 37 33 36 30 30 37 39 30 30 32
         24 | (4,14) |      32 | f     | t    | 31 09 0d 0a 4c 54 34 33 36 32 35 31 33 34 37 33 36 30 30 37 39 30 30 32
         25 | (9,14) |      32 | f     | t    | 2b 4c 54 34 33 36 32 35 31 33 34 00 00 00 00 00 00 00 00 00 00 00 00 00
(4 rows)

testidx=#

明显的索引损坏了,怎么损坏的呢?

可能是 BUG 或者系统异常导致数据库 crash 等写坏, 还有一个glibc 版本差异导致索引损坏的场景,特别是 glibc 2.28 之前和之后的版本。

经过排查这次异常就是 glibc 差异导致的,glibc 版本从 2.17 到 2.28。

当遇到这样的索引损坏场景时,建议 reindex 对应的索引来修复。

这个问题基本分析清楚了,不过老杨不打算到此为止。 借此机会证实下索引扫描的逻辑,也搞清楚为什么仅扫描一条数据就结束。感兴趣的朋友可以继续往下看。

原理分析

btree 想必大家都很熟悉了(其实我很讨厌面试中对于 btree 的八股文,haha...)

再来回顾下结构,细节可以参考灿灿的书中btree 章节

01.png

检索的时候,从 root page 开始检索,在 leaf page 中找到键值匹配的 heap ctid,通过 ctid 去 heap 中 fetch 对应的数据。这里借用德哥画的图,来自github 博客

02.png

另外 postgrespro 的博客btree 章节,对于检索过程描述的不错,推荐大家去看看。

例如查找等于 49 的数据,标黄部分及蓝色箭头描述了检索过程:从 root 节点出发,找到第一个匹配的 leaf 节点,顺着 leaf 节点的链表一直查找,直到检索完所有匹配的 leaf 节点。

03.png

简单回顾一些概念和原理后,我们上手 debug 来证实检索过程。

我们的检索条件为userid ='1230005998'

1. 先确定 first leaf page

btgettuple函数中首次扫描走_bt_first函数逻辑。

通常 leaf page 会有多个,扫描时通过二分查找,先找到键值匹配的目标 leaf page。 在\_bt_first 函数中,调用\_bt_search 函数,再调用\_bt_binsrch 函数进行二分查找。

初始的 low 为 1,high 为 8 对应 index_userid 这个索引的 leaf block 1 和 8

\_bt_compare 函数进行 key 匹配,这里 userid 为 text 类型,因此使用的比较函数为 bttextcmp

04.png

我们省去二分查找的过程,最终 high=low=8,确定目标数据在 leaf page 8

05.png

2、确定 first item

开始扫描目标 leaf page,同样采用二分查找,找到第一条匹配的 item。

\_bt_first 函数走到 offnum = \_bt_binsrch(rel, &inskey, buf),在\_bt_binsrch 函数中初始 high 为 78,low 为 1(因为 leaf page 8 有 78 条 item)。

06.png

在多轮二分查找后,mid 为 22 时\_bt_compare 匹配到了预期数据。bttextcmp 函数中可以看到 text_cmp 入参 arg1, arg2 相同,都为 1230005998,result 为 0。

07.png

因此,low 为 22,high 为 22,找到了 first item。

08.png

3、遍历页面元组,设置扫描边界

while (offnum <= maxoff)循环,offnum 为 22,maxoff 为 78。

从 first item 即 offnum=22 开始遍历,\_bt_readpage 中调用\_bt_checkkeys 首次比较结果相同,itemIndex++为 1,continuescan 为 true,offnum 延顺到 Next 即 23。

09.png

循环中再次调用\_bt_checkkeys 进行比较,实际的比较函数为 texteq,offnum 为 23 时 key 值明显和检索条件的长度不同,值肯定是不同的,result 为 false。

10.png

result 传递给 test,因此*continuescan = false,\_bt_checkkeys 返回 false。

11.png

continuescan 为 false,因此 so->currPos.moreRight=false,so->currPos.firstItem = 0, so->currPos.lastItem = 1 - 1, so->currPos.itemIndex = 0;

就是这几个属性决定了扫描边界。 firstItem 和 lastItem 相同都为 0,说明扫描的范围就是 first Item 这一条数据。

12.png

index_getnext_slot 函数中根据 ctid(4,39)调用 index_fetch_heap 获取 heap 数据。

13.png

4、获取 next Item

btgettuple 函数中,后续扫描调用\_bt_next 函数。

so->currPos.moreRight 为 false,\_bt_readnextpage 函数 return false,因此\_bt_steppage 函数 return false

14.png

因此\_bt_next 函数返回 false,btgettuple 返回 false

15.png

index_getnext_tid 函数返回 NULL

16.png

tid 为 NULL,index_getnext_slot 函数返回 NULL

17.png

至此扫描结束。

从这个过程中可以看到,itemoffset 22 即记录 ctid(4,39)这条索引键值和检索条件匹配,但 23 不匹配,因此导致索引扫描结束,只扫描了一条数据。

从 seqscan 结果看,ctid (4,39)下一条符合条件的数据为(9,14),对应到索引 itemoffset 25。从 bt_page_items 的结果来看,23 和 24 的键值是一样的,都比 25 大,因此索引排序规则是错乱的。

小结

本篇我们深入分析了一起索引排序规则损坏的案例,当出现类似问题时,可以利用 amcheck 和 pageinspect 扩展来分析解决。同时也 debug 证实了下索引扫描的一些关键过程。


HOW 2026 议题招募中

2026 年 4 月 27-28 日,由 IvorySQL 社区联合 PGEU(欧洲 PG 社区)、PGAsia(亚洲 PG 社区)共同打造的 HOW 2026(IvorySQL & PostgreSQL 技术峰会) 将再度落地济南。届时,PostgreSQL 联合创始人 Bruce Momjian 等顶级大师将亲临现场。

自开启征集以来,HOW 2026 筹备组已感受到来自全球 PostgreSQL 爱好者的澎湃热情。为了确保大会议题的深度与广度,我们诚邀您在 2026 年 2 月 27 日截止日期前,提交您的技术见解。

投递链接:https://jsj.top/f/uebqBc

我复现一下上面的操作说 zq-platform初始数据写不到数据库

第一次执行

alembic revision --autogenerate -m "init tables"

报错

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.ERROR [
            alembic.util.messaging]
           Target database is not up to date.FAILED: Target database is not up to date.

意思是:你的数据库当前版本 (current) 落后于 Alembic 迁移脚本所定义的最新版本 (head) cnblogs.com+1。这就好比你手里拿着的是第3版的说明书,但产品已经更新到第5版了

要解决这个问题,核心思路就是将数据库的当前版本 (current) 更新到与最新的迁移脚本版本 (head) 一致。

1.查看当前数据库状态:首先,确认一下版本差异。在项目根目录下打开终端,依次运行:

    # 查看数据库当前记录的版本    alembic current    # 查看所有可用的迁移脚本版本(head)    alembic heads

你通常会看到 current 的版本号比 heads 的版本号要旧,或者 heads 显示了多个分支(这通常意味着存在多个分支迁移需要合并)。

分别显示

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.

这证实了问题所在:数据库当前停留在一个空版本,并没有处于最新状态,所以 Alembic 拒绝你生成新的迁移脚本。

请直接运行下面这条命令来解决这个问题:

alembic upgrade head

这个命令会扫描 alembic/versions 文件夹,找到所有脚本,并依次在数据库中执行它们。

执行结果:

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.INFO  [
            alembic.runtime.migration]
           Running upgrade  -> b6a31168d666, init tablesINFO  [
            alembic.runtime.migration]
           Running upgrade b6a31168d666 -> a79453452d83, add page design

数据库已经成功升级到最新版本了。从输出 Running upgrade b6a31168d666 -> a79453452d83 可以看到:数据库已经更新到了 a79453452d83,

打开数据库可以看到当前版本号:

图片

重新生成迁移:现在可以再次尝试运行 alembic revision --autogenerate -m “init tables”。

alembic revision --autogenerate -m "init tables"

显示结果:

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.Generating F:\下载程序与源码\★★★可执行项目收集★★★\zq-platform\backend-fastapi\alembic\versions\588
            bd64ec92e_init_tables.py
           ...  done

输出显示:Generating ... 588bd64ec92e\_init\_tables.py ... done

这表示 Alembic 成功扫描了你的代码和数据库,并发现它们之间的差异,自动生成了一个新的迁移脚本文件: 588bd64ec92e\_init\_tables.py。

接下来的步骤:让修改生效

现在脚本文件只是生成了,但数据库里的表结构还没变。你需要执行最后一步:

1. 查看生成的脚本(可选,但推荐)

你可以打开 alembic\versions\ 9abfcdbaff03\_init\_tables.py 看一眼。检查 upgrade() 函数里是否包含了 op.create\_table(…) 等语句。如果那里是空的,说明 Alembic 没检测到你的模型变化,或者模型没有被正确导入。

2. 更新数据库(必须执行)

在终端运行:

alembic upgrade head

这将会把刚才生成的脚本应用到你的 fastapi\_db 数据库中,创建对应的表结构。

显示

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.INFO  [
            alembic.runtime.migration]
           Running upgrade a79453452d83 -> 588bd64ec92e, init tables

数据库迁移已经完全成功了!🎉

从输出 Running upgrade a79453452d83 -> 588bd64ec92e 可以确认:

你的 fastapi\_db 数据库已经成功应用了 init tables 脚本,所有定义的表结构现在都已经真实存在于数据库中了。

验证成果

如果你想确认表真的创建好了,可以:

1.打开 pgAdmin(如果你安装了的话)。

2.连接到 fastapi\_db。

3.展开 Schemas -> public -> Tables。

4.你应该能看到刚才定义的所有数据表(比如 users 等)。

或者,你可以直接在 Python 代码中试着向数据库写入一条数据,看是否报错。

再执行python scripts/ loaddata.py db\_init.json,导入数据,看到

导入完成:  成功: 38 条  失败: 0 条

先不要激动!!!日志最后一句“导入完成: 成功 38 条 / 失败 0 条”是脚本自己打印的统计,并不真实——

只要发生 ROLLBACK,整个事务就被回滚,数据库里一条新数据也没有写进去。

真正的失败原因就是这一条:


            asyncpg.exceptions.DataError:
            invalid input for query argument $4: '2026-01-11T19:44:39.752685'  (expected a 
            datetime.date
           or 
            datetime.datetime
           instance, got 'str')

也就是 core\_user.last\_login 字段传的是 字符串,而数据库列类型是 timestamp without time zone,异步驱动 asyncpg 不接受字符串隐式转换。

如何修复

def parse_datetime(value):    """解析日期时间字符串"""    if isinstance(value, str):        # 尝试多种日期时间格式        formats = [            "%Y-%m-%dT%H:%M:%S.%f",  # ISO 格式带微秒            "%Y-%m-%dT%H:%M:%S",      # ISO 格式不带微秒            "%Y-%m-%d %H:%M:%S.%f",   # 带微秒的空格分隔格式            "%Y-%m-%d %H:%M:%S",      # 不带微秒的空格分隔格式            "%Y-%m-%d",               # 仅日期格式        ]                for fmt in formats:            try:                return 
            datetime.strptime(value,
           fmt)            except ValueError:                continue                # 如果以上格式都不匹配,尝试 fromisoformat        try:            return 
            datetime.fromisoformat(value.replace(
          "Z", "+00:00"))        except ValueError:            pass                # 如果所有尝试都失败,返回原始值        return value    return value    ......    # 转换日期时间字段                for key, value in 
            fields.items():
                              if isinstance(value, str):                        # 检查是否为日期时间格式的字符串                        parsed_value = parse_datetime(value)                        # 如果成功解析且返回的是 datetime 对象,则替换原值                        if isinstance(parsed_value, datetime):                            fields[key] = parsed_value

再执行python scripts/ loaddata.py db\_init.json,直至这些数据都导入完成。

当看到

从文件导入数据: 
            db_init.json
          读取到 38 条记录2026-01-20 17:07:52,224 INFO 
            sqlalchemy.engine.Engine
           select 
            pg_catalog.version()
          2026-01-20 17:07:52,225 INFO 
            sqlalchemy.engine.Engine
           [raw sql] ()......2026-01-20 17:07:52,276 INFO 
            sqlalchemy.engine.Engine
           COMMIT导入完成:  成功: 38 条  失败: 0 条

·脚本成功读取了 db\_init.json 文件,识别出包含 38 条待导入的记录

·SQLAlchemy 引擎成功连接到 PostgreSQL 数据库(日志中出现 pg\_catalog.version() 是 PostgreSQL 特有的查询)

数据库验证

出现账号数据即为数据导入成功。

图片

启动服务

python main.py或使用 uvicornuvicorn main:app --reload --host 0.0.0.0 --port 8000

这样初始数据写不到数据库问题就可以得到根本解决。

PostgreSQL 在各行各业的关键应用中具有极高适用性。尽管 PostgreSQL 提供了良好的性能,但仍存在一些用户不太关注但对整体效率与速度至关重要的问题。多数人认为增加 CPU 核数、更快的存储、更大内存即可提升性能,但还有同样重要的因素需要关注——那就是延迟。

延迟意味着什么?

数据库执行查询操作的耗时,仅占应用程序接收查询结果总耗时的极小部分。下图可直观呈现该过程的内在逻辑:

1.png

客户端应用发送请求后,驱动程序通过网络向 PostgreSQL 发送消息(a),数据库执行查询(b),并将结果集返回给应用程序(c)。关键问题在于:相较于查询执行时间(b),网络传输时间(a 与 c)是否具有显著影响。通过实验可以加以验证。

首先,使用 pgbench 初始化一个简单的测试数据库。对于本次测试,小规模数据库已足够:

cybertec$ pgbench -i blog
dropping old tables...
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
vacuuming...
creating primary keys...
done in 0.19 s (drop tables 0.00 s, create tables 0.02 s, client-side generate 0.13 s, vacuum 0.02 s, primary keys 0.02 s).

随后进行第一次基础测试:建立单个 UNIX Socket 连接,运行 20 秒(只读测试):

cybertec$ pgbench -c 1 -T 20 -S blog
pgbench (17.5)
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 20 s
number of transactions actually processed: 1035095
number of failed transactions: 0 (0.000%)
latency average = 0.019 ms
initial connection time = 2.777 ms
tps = 51751.287839 (without initial connection time)

关键指标如下:

  • 平均延迟:0.019 毫秒
  • 每秒事务处理量(TPS):51751

该数据表现对于单连接场景而言已属良好水平。

下一步执行相同查询测试,但将连接方式从 UNIX 套接字更换为指向本地主机(localhost)的 TCP 连接(非远程连接):

cybertec$ pgbench -c 1 -T 20 -S blog -h localhost
pgbench (17.5)
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 20 s
number of transactions actually processed: 583505
number of failed transactions: 0 (0.000%)
latency average = 0.034 ms
initial connection time = 3.290 ms
tps = 29173.916752 (without initial connection time)

结果出现明显变化,关键指标如下:

  • 平均延迟:0.034 毫秒
  • 每秒事务数(TPS):29173

吞吐量下降约 44%。下图对此进行了直观展示:

2.png

值得注意的是,延迟仅从 0.019 毫秒上升至 0.034 毫秒,变化幅度极小。但由于查询本身执行速度极快,即便如此微小的延迟也会带来显著影响。执行计划可以说明这一点:

blog=# explain analyze SELECT *
      FROM   pgbench_accounts
WHERE  aid = 434232;
                         QUERY PLAN
------------------------------------------------------------
 Index Scan using pgbench_accounts_pkey on pgbench_accounts
   (cost=0.29..8.31 rows=1 width=97)
   (actual time=0.015..0.016 rows=0 l                                                                                                                  oops=1)
   Index Cond: (aid = 434232)
 Planning Time: 0.227 ms
 Execution Time: 0.047 ms
(4 rows)

执行计划中的关键数值为 0.016,表示索引扫描在表中定位记录所需的时间。将该数值与额外引入的网络延迟进行对比,即可理解微小变化为何会造成巨大差异。

真实网络环境中的延迟

在实际场景中,应用程序与数据库通常部署在不同的机器上。测试前,先查看 traceroute 的输出结果:

different_box$ traceroute 10.1.139.53
traceroute to 10.1.139.53 (10.1.139.53), 30 hops max, 60 byte packets
 1  _gateway (10.0.0.1)  0.212 ms  0.355 ms  0.378 ms
 2  cybertec (10.1.139.53)  0.630 ms  0.619 ms *

可以看到,从运行 pgbench 的主机到数据库服务器的路径较短,仅通过内部网络完成通信。

再次运行相同测试,结果如下:

different_box$ pgbench -h 10.1.139.53 -S -c 1 -T 20 blog
pgbench (17.5)
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 20 s
number of transactions actually processed: 47540
number of failed transactions: 0 (0.000%)
latency average = 0.420 ms
initial connection time = 9.727 ms
tps = 2378.123901 (without initial connection time)

关键指标为:

  • 平均延迟:0.420 毫秒
  • 每秒事务数(TPS):2378

即便延迟仅为 0.420 毫秒,吞吐量已从 5 万 TPS 降至 2378 TPS。虽然该测试仍为单连接,但原因十分清晰:网络传输所消耗的 0.4 毫秒,与索引读取所需的 0.016 毫秒相比,已是数量级上的差距。

下图展示了吞吐量变化情况:

3.png

可确定的是,若网络架构中增加更多网络层级,吞吐量数据将进一步显著下降。该问题在云计算环境中尤为突出,每一层负载均衡、每一次网络跳转、每一台路由设备、每一条防火墙规则,均会增加网络延迟,进而降低应用程序运行效率。对于执行耗时极短的查询操作而言,网络延迟产生的额外开销占比越高,查询操作本身的执行耗时占比则越低,其对整体性能的影响程度也随之下降。

并发机制:可行的解决方案?

上述实验展示了极端情况,适用于单一应用在应用与数据库间频繁交互的场景。而在负载较高的业务系统中,通常存在多用户并发访问的情况。若增加并发连接数,系统性能可呈现较为理想的表现:

cybertec$ pgbench -c 4 -j 4 -T 20 -S blog -h localhost
pgbench (17.5)
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 4
number of threads: 4
maximum number of tries: 1
duration: 20 s
number of transactions actually processed: 1639827
number of failed transactions: 0 (0.000%)
latency average = 0.049 ms
initial connection time = 5.637 ms
tps = 82007.653121 (without initial connection time)

提取关键数据如下:

  • 平均延迟:0.429 毫秒
  • 每秒事务数(TPS):82007

使用 4 个并发连接,TPS 达到 82,000,增加更多并发可进一步提升。在现代服务器上,每秒超过 100 万次操作完全可行。但前提是数据库与查询来源距离接近,网络延迟不构成瓶颈。

更快的 CPU 是否有帮助?

常见疑问:增加 CPU 核数或提升单核性能是否有意义?对比如下:

  • 索引查找:0.016 毫秒
  • 网络延迟:0.490 毫秒

即便 CPU 更快,优化的仅为 0.016 毫秒,占总耗时约 3%,剩余 97% 时间不受影响。本质上,这与吞吐量关系不大,而是延迟问题。对于极短查询,延迟累积可能导致严重性能下降,尤其在云环境下网络复杂度更高。

对于执行时间较长的查询,延迟影响较小;但对于超快小查询,网络延迟可能成为主要性能瓶颈。

总结

延迟在高频、短时查询场景中具有决定性影响。单连接环境下,微小的网络延迟即可导致吞吐量大幅下降;通过并发可以在一定程度上缓解这一问题,但网络距离和拓扑结构仍是关键约束因素。相比之下,单纯提升 CPU 性能对以网络延迟为主导的场景改善有限。在云环境与分布式架构中,延迟问题需要在系统设计阶段予以重点关注。

原文链接:

https://www.cybertec-postgresql.com/en/postgresql-performance...

作者:Hans-Jürgen Schönig


HOW 2026 议题招募中

2026 年 4 月 27-28 日,由 IvorySQL 社区联合 PGEU(欧洲 PG 社区)、PGAsia(亚洲 PG 社区)共同打造的 HOW 2026(IvorySQL & PostgreSQL 技术峰会) 将再度落地济南。届时,PostgreSQL 联合创始人 Bruce Momjian 等顶级大师将亲临现场。

自开启征集以来,HOW 2026 筹备组已感受到来自全球 PostgreSQL 爱好者的澎湃热情。为了确保大会议题的深度与广度,我们诚邀您在 2026 年 2 月 27 日截止日期前,提交您的技术见解。

投递链接:https://jsj.top/f/uebqBc

PostgreSql 基于 Pacemaker+Corosync+pcs 的高可用实践

简介

在 PostgresSql HA 方案中,流复制方案集性能,可靠性,部署成本低等优点,也是目前被普遍采用的方案
在流复制 HA 集群管理工具中,Pacenmaker+Corosysnc 是相对程序可靠的

功能特性

  • 快速故障转移
  • 支持多节点集群
  • 支持同步和异步复制
  • 提供读写 vip 和只读 vip

基础架构 / 原理

  1. Pacemaker + Corosync 作为集群基础软件,Corosync 负责集群通信和成员关系管理,Pacemaker 负责资源管理。
  2. 集群用到资源包括 PostgreSQL 和 VIP 等,PostgreSQL 对应的 Resource Agent (RA) 为 expgsql,expgsql 负责实施 PostgreSQL 的起停,监视,failover 等操作。
  3. 集群初始启动时 expgsql 通过比较所有节点的 xlog 位置,找出 xlog 最新的节点作为 Master,其它节点作为 Slave 通过读写 VIP 连接到 Master 上进行 WAL 复制。
  4. 集群启动后 expgsql 不断监视 PostgreSQL 的健康状况,当 expgsql 发现 PostgreSQL 资源故障时报告给 Pacemaker,由 Pacemaker 实施相应动作。
    如果是 PostgreSQL 进程故障,原地重启 PostgreSQL,并且该节点上的 fail-count 加 1。
    fail-count 累加到 3 时不再分配 PostgreSQL 资源到这个节点。如果该节点为 Master,会提升一个 Slave 为 Master,即发起 failover。
  5. Corosync 发现节点故障 (主机或网络故障) 时,Pacemaker 也根据情况实施相应动作。
    对多节点集群,未包含过半节点成员的分区将主动释放本分区内的所有资源,包括 PostgreSQL 和 VIP。
    合法的分区中如果没有 Master,Pacemaker 会提升一个 Slave 为 Master,即发起 failover。
  6. Master 上的 expgsql 会不断监视 Slave 的复制健康状况,同步复制下会选定一个 Slave 作为同步 Slave。
  7. 当同步 Slave 出现故障时,Master 上的 expgsql 会临时将同步复制切换到异步复制,防止 Master 上的写操作被 hang 住。如果故障 Slave 恢复或存在另一个健康的 Slave,再切换到同步复制。
  8. 为防止集群分区后,Slave 升级为新 Master 而旧 Master 切换到异步复制导致脑裂和数据双写,引入分布式锁服务进行仲裁。Slave 升级为新 Master 和旧 Master 切换到异步复制前必须先取得锁,避免这两件事同时发生。失去锁的 Master 会主动停止 PostgreSQL 进程,防止出现双主。
  9. 如果分布锁服务发生故障而所有 PostgreSQL 节点都是健康的,expgsql 会忽视锁服务,即不影响集群服务。但在分布锁服务故障期间,Master 发生节点故障 (注意区分节点故障和资源故障),集群将无法正常 failover。
  10. 同步复制下只有同步 Slave 才有资格成为候选 Master,加上有分布式锁的防护,可以确保 failover 后数据不丢失。
  11. 集群初始启动和每次 failover 时通过 pg_ctl promote 提升 Slave 为 Master 并使时间线加 1,同时记录 Master 节点名,时间线和切换时的 xlog 位置到集群 CIB。
  12. 集群重启时根据集群 CIB 中记录的信息确定 Master 节点,并保持时间线不变。
  13. expgsql 启动 PostgreSQL 前会检查该节点的时间线和 xlog,如果和集群 CIB 中记录的信息有冲突,将报错。需要人工通过 cls_repair_by_pg_rewind 等手段修复。
  14. 读写 VIP 和 Master 节点绑定,只读 VIP 和其中一个 Slave 绑定,应用只需访问 VIP,无需关心具体访问哪个节点。

集群常规命令

pcs status //查看集群状态
pcs resource show //查看资源
pcs resource create ClusterIP IPaddr2 ip=192.168.0.120 cidr_netmask=32 //创建一个虚拟IP资源
pcs resource cleanup //xx表示虚拟资源名称,当集群有资源处于unmanaged的状态时,可以用这个命令清理掉失败的信息,然后重置资源状态
pcs resource list //查看资源列表
pcs resource restart //重启资源
pcs resource enable //启动资源
pcs resource disable //关闭资源
pcs resource delete //删除资源
crm_mon -Arf -1 //查看同步状态和资源 

pg_ctl 常用命令

Usage:
  pg_ctl init[db] [-D DATADIR] [-s] [-o OPTIONS]
  pg_ctl start      [-D DATADIR] [-l FILENAME] [-W] [-t SECS] [-s] [-o OPTIONS] [-p PATH] [-c]
  pg_ctl stop       [-D DATADIR] [-m SHUTDOWN-MODE] [-W] [-t SECS] [-s]
  pg_ctl restart    [-D DATADIR] [-m SHUTDOWN-MODE] [-W] [-t SECS] [-s] [-o OPTIONS] [-c]
  pg_ctl reload     [-D DATADIR] [-s]
  pg_ctl status     [-D DATADIR]
  pg_ctl promote    [-D DATADIR] [-W] [-t SECS] [-s]
  pg_ctl logrotate  [-D DATADIR] [-s]
  pg_ctl kill       SIGNALNAME PID

集群实践

环境介绍

操作系统版本

CentOS Linux release 7.9.2009 (Core)

软件版本

pgsql:(14)
PCS 相关版本:

[root@k8s-master01 pgsql_cluster]#  rpm -qa|grep pacemaker
pacemaker-cli-1.1.23-1.el7_9.1.x86_64
pacemaker-cluster-libs-1.1.23-1.el7_9.1.x86_64
pacemaker-1.1.23-1.el7_9.1.x86_64
pacemaker-libs-1.1.23-1.el7_9.1.x86_64

[root@k8s-master01 pgsql_cluster]#  rpm -qa|grep pcs
pcs-0.9.169-3.el7.centos.3.x86_64

[root@k8s-master01 pgsql_cluster]# rpm -qa|grep corosync
corosync-2.4.5-7.el7_9.2.x86_64
corosynclib-2.4.5-7.el7_9.2.x86_64
地址信息
192.168.28.151   pg-151
192.168.28.152   pg-152
192.168.28.153   pg-153
192.168.28.41    vip-master
192.168.28.141   vip-slave

基础环境准备

配置 hostname [all service]
cat /etc/hosts
192.168.28.151   pg-151
192.168.28.152   pg-152
192.168.28.153   pg-153
关闭防火墙.selinux [all service]
#systemctl disable firewalld #systemctl stop firewalld #systemctl status firewalld #sestatus

SELinux status:                 disabled
服务器时间同步
ntpdate ntp1.aliyun.com

集群软件安装

安装 pcs
yum -y install libsmb*

yum install -y pacemaker pcs corosync

yum install  -y autoconf automake libtool

yum install   -y docbook-style-xsl

yum install   -y gcc-c++ glib2-devel

需要注意的是,安装时可能会报 Missing Dependency :kernel-header
安装 安装 kernel-headers 即可解决问题,如下

wget http://mirror.centos.org/centos/7/updates/x86_64/Packages/kernel-headers-3.10.0-1160.102.1.el7.x86_64.rpm
rpm -ivh kernel-headers-3.10.0-1160.102.1.el7.x86_64.rpm
pacemaker resource-agents 更新 [all servers]
wget  https://github.com/ClusterLabs/resource-agents/releases/tag/v4.12.0
unzip resource-agents-4.12.0.zip
./autogen.sh
/configure
make && make install

确认支持 PG12 以上版本
/usr/lib/ocf/resource.d/heartbeat/pgsql 文件,1918 行,包含 ocf_version_cmp “$version” “12”

 if is_replication || [ "$OCF_RESKEY_rep_mode" = "slave" ]; then if [ `printf "$version\n9.1" | sort -n | head -1` != "9.1" ]; then
            ocf_exit_reason "Replication mode needs PostgreSQL 9.1 or higher." return $OCF_ERR_INSTALLED fi
        ocf_version_cmp "$version" "12"
        rc=$?
        if [ $rc -eq 1 ]||[ $rc -eq 2 ]; then # change the standby method for PosrgreSQL 12 or later. 
启动服务 [all service]
systemctl start pcsd
systemctl enable pcsd
systemctl enable corosync
systemctl enable pacemaker

集群设置

1. 设置 hacluster 用户密码 [all service]
echo hacluster|passwd hacluster --stdin
2. 集群认证 (任意节点)
pcs cluster auth -u hacluster -p hacluster pg-151 pg-152 pg-153 
3. 同步配置 (任意节点)
pcs cluster setup --last_man_standing=1 --name pgcluster pg-151 pg-152 pg-153 --force 

注意 --force 会强行覆盖原有集群配置

4. 启动集群 (任意节点)
#启动集群
pcs cluster start --all
#查看集群状态
pcs status --full 

安装 PostgresSQl14 [all service]

yum 安装
sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo yum install -y postgresql14-server
sudo /usr/pgsql-14/bin/postgresql-14-setup initdb
sudo systemctl enable postgresql-14
sudo systemctl start postgresql-14
数据库目录创建 [all servers]

mkdir -p /data/postgresql/pgdata/pg-5432
mkdir -p /data/postgresql/pg_archive/pg-5432
chown postgres:postgres /data/postgresql
chmod 700 /data/postgresql

数据库用户环境变量配置 [all servers]
su - postgres
-bash-4.2$ cat .bash_profile
[ -f /etc/profile ] && source /etc/profile
PGDATA=/data/postgresql/pgdata/pg-5432
export PGDATA
export PATH=/usr/pgsql-14/bin:$PATH # If you want to customize your settings, # Use the file below. This is not overridden # by the RPMS.
[ -f /var/lib/pgsql/.pgsql_profile ] && source /var/lib/pgsql/.pgsql_profile
主节点数据库 (pg-151) 配置
  1. 初始化数据库
initdb -D /data/postgresql/pgdata/pg-5432/ 
  1. 修改配置文件和 hba
    postgresql.conf

pg_hba.conf (用于设置主机访问)

local all all                                     trust
host   all all 192.168.28.0/24          md5
# IPv4 local connections:
host    all all 127.0.0.1/32            trust
# IPv6 local connections:
host    all all             ::1/128                 trust
# Allow replication connections from localhost, by a user with the
# replication privilege.
local   replication     all                                     trust
host    replication     repluser        192.168.28.0/24         md5
host    replication     all 127.0.0.1/32            trust
host    replication     all             ::1/128                 trust
创建复制用户 (master 节点)

先运行 matser 数据库
pg_ctl start
再创建复制用户

psql (14.10)
Type "help" for help.
postgres=# create user repluser with replication password 'repluser'; 
创建 slave 节点 (pg-152 pg-152)
pg_basebackup -h pg-151 -U repluser -p 5432  -D /data/postgresql/pgdata/pg-5432/ -X stream -P 
停止 master 节点 (pg-151)

pg_ctl stop

配置 pgsql 集群

1. 设置 cluster_setup.sh 脚本
pcs cluster cib pgsql_cfg
pcs -f pgsql_cfg property set no-quorum-policy="ignore"
pcs -f pgsql_cfg property set stonith-enabled="false"
pcs -f pgsql_cfg resource defaults resource-stickiness="INFINITY"
pcs -f pgsql_cfg resource defaults migration-threshold="1"
pcs -f pgsql_cfg resource create vip-master IPaddr2 \
   ip="192.168.28.41" \
   nic="eth0" \
   cidr_netmask="24" \
   op start   timeout="60s" interval="0s"  on-fail="restart" \
   op monitor timeout="60s" interval="10s" on-fail="restart" \
   op stop    timeout="60s" interval="0s"  on-fail="block"
pcs -f pgsql_cfg resource create vip-slave IPaddr2 \
   ip="192.168.28.141" \
   nic="eth0" \
   cidr_netmask="24" \
   meta migration-threshold="0" \
   op start   timeout="60s" interval="0s"  on-fail="stop" \
   op monitor timeout="60s" interval="10s" on-fail="restart" \
   op stop    timeout="60s" interval="0s"  on-fail="ignore"
pcs -f pgsql_cfg resource create pgsql pgsql \
   pgctl="/usr/pgsql-14/bin//pg_ctl" \
   psql="/usr/pgsql-14/bin//psql" \
   pgdata="/data/postgresql/pgdata/pg-5432" \
   config="/data/postgresql/pgdata/pg-5432/postgresql.conf" \
   rep_mode="async" \
   node_list="pg-151 pg-152 pg-153" \
   master_ip="192.168.28.41" \
   repuser="repluser" \
   primary_conninfo_opt="password=repluser keepalives_idle=60 keepalives_interval=5 keepalives_count=5" \
   restart_on_promote='true' \
   op start   timeout="60s" interval="0s"  on-fail="restart" \
   op monitor timeout="60s" interval="4s"  on-fail="restart" \
   op monitor timeout="60s" interval="3s"  on-fail="restart" role="Master" \
   op promote timeout="60s" interval="0s"  on-fail="restart" \
   op demote  timeout="60s" interval="0s"  on-fail="stop" \
   op stop    timeout="60s" interval="0s"  on-fail="block" \
   op notify  timeout="60s" interval="0s"
pcs -f pgsql_cfg resource master msPostgresql pgsql \
   master-max=1 master-node-max=1 clone-max=5 clone-node-max=1 notify=true
pcs -f pgsql_cfg resource group add master-group vip-master
pcs -f pgsql_cfg resource group add slave-group vip-slave
pcs -f pgsql_cfg constraint colocation add master-group with master msPostgresql INFINITY
pcs -f pgsql_cfg constraint order promote msPostgresql then start master-group symmetrical=false score=INFINITY
pcs -f pgsql_cfg constraint order demote  msPostgresql then stop  master-group symmetrical=false score=0
pcs -f pgsql_cfg constraint colocation add slave-group with slave msPostgresql INFINITY
pcs -f pgsql_cfg constraint order promote msPostgresql then start slave-group symmetrical=false score=INFINITY
pcs -f pgsql_cfg constraint order demote  msPostgresql then stop  slave-group symmetrical=false score=0
pcs cluster cib-push pgsql_cfg

运行脚本

chmox +x cluster_setup.sh
sh cluster_setup.sh 
2. 如何修改集群配置
cibadmin --query > tmp.xml
//将当前集群配置信息保存到tmp.xml文件中 cibadmin --query > tmp.xml
vi tmp.xml
//使用编辑器对XML文件进行修改  vim tmp.xml
cibadmin --replace --xml-file tmp.xml
//将修改后的XML文件替换掉当前集群的配置信息 cibadmin --replace --xml-file tmp.xml 

cibadmin 是用于操作 Heartbeat CIB 的低级管理命令。它可以用来转储、更新或修改所有或部分 CIB,删除整个 CIB 或执行各种 CIB 管理操作。

集群配置信息是 Pacemaker 集群中 CIB 信息的关键组成部分,Pacemaker 的集群配置信息决定了集群最终应该如何工作以及集群最终的运行状态,因为只有一个正确的集群配置才能驱动集群资源运行在正常的状态。通常情况下,集群的配置信息由集群配置选项 (crm_config)、集群节点 (nodes)、集群资源 (resources) 和资源约束 (constraints) 四个配置段组成,通过 cibadmin --query 可查。

3. 集群状态检查

1. 启动集群并设置开机自启动
数据库无需配置自启动,由集群软件自动拉起

pcs cluster start --all
pcs cluster enable --all 

2. 查看集群状态

[root@k8s-master01 pgsql_cluster]# pcs status Cluster name: pgcluster Stack: corosync Current DC: pg-153 (version 1.1.23-1.el7_9.1-9acf116022) - partition with quorum Last updated: Tue Nov 21 16:47:19 2023 Last change: Tue Nov 21 16:38:42 2023by root via crm_attribute on pg-151 3 nodes configured 7 resource instances configured Online: [ pg-151 pg-152 pg-153 ]

Full list of resources: Master/Slave Set: msPostgresql [pgsql]
     Masters: [ pg-151 ]
     Slaves: [ pg-152 pg-153 ]
 Resource Group: master-group vip-master (ocf::heartbeat:IPaddr2): Started pg-151 Resource Group: slave-group vip-slave  (ocf::heartbeat:IPaddr2): Started pg-152 Daemon Status: corosync: active/enabled pacemaker: active/enabled pcsd: active/enabled 

3、查看本机集群状态及资源状态
crm_mon -Afr -1

[root@k8s-master01 pgsql_cluster]# crm_mon -Afr -1
Stack: corosync
Current DC: pg-153 (version 1.1.23-1.el7_9.1-9acf116022) - partition with quorum
Last updated: Tue Nov 21 16:47:54 2023
Last change: Tue Nov 21 16:38:42 2023 by root via crm_attribute on pg-151

3 nodes configured
7 resource instances configured

Online: [ pg-151 pg-152 pg-153 ]

Full list of resources:

 Master/Slave Set: msPostgresql [pgsql]
     Masters: [ pg-151 ]
     Slaves: [ pg-152 pg-153 ]
 Resource Group: master-group
     vip-master (ocf::heartbeat:IPaddr2):       Started pg-151
 Resource Group: slave-group
     vip-slave  (ocf::heartbeat:IPaddr2):       Started pg-152

Node Attributes:
* Node pg-151:
    + master-pgsql                      : 1000      
    + pgsql-data-status                 : LATEST    
    + pgsql-master-baseline             : 00000000220000A0
    + pgsql-status                      : PRI       
* Node pg-152:
    + master-pgsql                      : -INFINITY 
    + pgsql-data-status                 : STREAMING|ASYNC
    + pgsql-status                      : HS:async  
* Node pg-153:
    + master-pgsql                      : -INFINITY 
    + pgsql-data-status                 : STREAMING|ASYNC
    + pgsql-status                      : HS:async  

Migration Summary:
* Node pg-153:
* Node pg-152:
* Node pg-151:

4. 查看主节点流复制状态 (master)

su - postgres
-bash-4.2$ psql
psql (14.10)
Type "help" for help.

postgres=# select client_addr,sync_state from pg_stat_replication;
  client_addr   | sync_state 
----------------+------------
 192.168.28.153 | async 192.168.28.152 | async
(2 rows)

5. 验证 vip [all service]

ipconfig
验证数据同步

在主节点上创建一个新表

postgres=# create table ysl(id int);
CREATE TABLE
postgres=# insert into ysl values(1);
INSERT 0 1
postgres=# select * from ysl;
 id 
----
  1
(1 row)

在 slave 节点上查看

-bash-4.2$ psqlpsql (14.10)
Type "help" for help.

postgres=# select * from ysl; id 
----
  1
(1 row)

故障模拟

停掉主库 pg-151,备库 pg-152 提升为主节点

-bash-4.2$ pg_ctl stop
waiting for server to shut down.... done

[root@k8s-master01 pgsql_cluster]# pcs status
Cluster name: pgcluster
Stack: corosync
Current DC: pg-153 (version 1.1.23-1.el7_9.1-9acf116022) - partition with quorum
Last updated: Tue Nov 21 16:59:30 2023
Last change: Tue Nov 21 16:59:29 2023 by root via crm_attribute on pg-152

3 nodes configured
7 resource instances configured

Online: [ pg-151 pg-152 pg-153 ]

Full list of resources:

 Master/Slave Set: msPostgresql [pgsql]
     Masters: [ pg-152 ]
     Slaves: [ pg-153 ]
     Stopped: [ pg-151 ]
 Resource Group: master-group
     vip-master (ocf::heartbeat:IPaddr2):       Started pg-152
 Resource Group: slave-group
     vip-slave  (ocf::heartbeat:IPaddr2):       Started pg-153

Failed Resource Actions:
* pgsql_monitor_3000 on pg-151 'not running' (7): call=92, status=complete, exitreason='',
    last-rc-change='Tue Nov 21 16:58:59 2023', queued=0ms, exec=0ms

Daemon Status:
  corosync: active/enabled
  pacemaker: active/enabled
  pcsd: active/enabled

注意 master 节点挂掉之后,使用 crm_mon -Arf -1 命令看到末尾有 "You have to remove /var/lib/pgsql/tmp/PGSQL.lock file to force start." 字样,master 宕机启动时,需要删除临时锁文件方可进行集群角色转换。即执行 rm -rf /var/lib/pgsql/tmp/PGSQL.lock
拉起数据库不需要执行 pg_ctl start, 只需要 pcs resource cleanup

[root@k8s-master01 pgsql_cluster]# pcs resource cleanup
Cleaned up all resources on all nodes
Waiting for 1 reply from the CRMd. OK
[root@k8s-master01 pgsql_cluster]# pcs status
Cluster name: pgcluster
Stack: corosync
Current DC: pg-153 (version 1.1.23-1.el7_9.1-9acf116022) - partition with quorum
Last updated: Tue Nov 21 17:01:57 2023
Last change: Tue Nov 21 17:01:26 2023 by hacluster via crmd on pg-151

3 nodes configured
7 resource instances configured

Online: [ pg-151 pg-152 pg-153 ]

Full list of resources:

 Master/Slave Set: msPostgresql [pgsql]
     Masters: [ pg-152 ]
     Slaves: [ pg-153 ]
     Stopped: [ pg-151 ]
 Resource Group: master-group
     vip-master (ocf::heartbeat:IPaddr2):       Started pg-152
 Resource Group: slave-group
     vip-slave  (ocf::heartbeat:IPaddr2):       Started pg-153

Failed Resource Actions:
* pgsql_start_0 on pg-151 'unknown error' (1): call=127, status=complete, exitreason='My data may be inconsistent. You have to remove /var/lib/pgsql/tmp/PGSQL.lock file to force start.',
    last-rc-change='Tue Nov 21 17:01:28 2023', queued=1ms, exec=615ms

Daemon Status:
  corosync: active/enabled
  pacemaker: active/enabled
  pcsd: active/enabled

此时可以看到 pg-153,pg-151 成为 pg-152 的备机

postgres=# select * from pg_stat_replication;
  pid  | usesysid | usename  | application_name |  client_addr   | client_hostname | client_port |     
    backend_start         | backend_xmin |   state   |  sent_lsn  | write_lsn  | flush_lsn  | replay_ls
n | write_lag | flush_lag | replay_lag | sync_priority | sync_state |          reply_time           
-------+----------+----------+------------------+----------------+-----------------+-------------+-----
--------------------------+--------------+-----------+------------+------------+------------+----------
--+-----------+-----------+------------+---------------+------------+-------------------------------
 13234 |    16384 | repluser | pg-153           | 192.168.28.153 |                 |       60624 | 2023
-11-21 08:59:27.197402+00 |        12918 | streaming | 0/2D004748 | 0/2D004748 | 0/2D004748 | 0/2D00474
8 |           |           |            |             0 | async      | 2023-11-21 09:04:16.90261+00
 31795 |    16384 | repluser | pg-151           | 192.168.28.151 |                 |       44678 | 2023
-11-21 09:04:05.741284+00 |        12918 | streaming | 0/2D004748 | 0/2D004748 | 0/2D004748 | 0/2D00474
8 |           |           |            |             0 | async      | 2023-11-21 09:04:15.822551+00
(2 rows)

参考:

PgSQL Replicated Cluster:


📌 转载信息
原作者:
xxdtb
转载时间:
2026/1/19 17:53:11

介绍
发布和订阅使用了 pg 的逻辑复制功能,通过发布端创建 publication 与表绑定,订阅端创建 subscription 同时会在发布端创建逻辑复制槽实现逻辑复制功能

逻辑复制基于 发布(Publication) 与 订阅(Subscription)模型

一个 发布者(Publisher) 上可以有多个发布,一个 订阅者(Subscriber) 上可以有多个 订阅 。
一个发布可被多个订阅者订阅,一个订阅只能订阅一个发布者,但可订阅同发布者上的多个不同发布。

典型用法

迁移,跨 PostgreSQL 大版本,跨操作系统平台进行复制。
CDC,收集数据库(或数据库的一个子集)中的增量变更,在订阅者上为增量变更触发触发器执行定制逻辑。
分拆,将多个数据库集成为一个,或者将一个数据库拆分为多个,进行精细的分拆集成与访问控制。

复制标识

一个被纳入发布中的表,必须带有复制标识(Replica Identity),只有这样才可以在订阅者一侧定位到需要更新的行,完成 UPDATE 与 DELETE 操作的复制。
默认情况下,主键 (Primary Key)是表的复制标识,非空列上的唯一索引 (UNIQUE NOT NULL)也可以用作复制标识。
如果没有任何复制标识,可以显式将复制标识设置为 FULL,也就是把整个行当作复制标识
使用 FULL 模式的复制标识效率很低(因为每一行修改都需要在订阅者上执行全表扫描,很容易把订阅者拖垮),所以这种配置只能是保底方案。
使用 FULL 模式的复制标识还有一个限制,订阅端的表上的复制身份所包含的列,要么与发布者一致,要么比发布者更少

创建发布订阅

环境介绍

version: pg16
发布端: 5433
订阅端: 5434
主机 IP: 192.168.28.11

创建发布订阅

在发布者端,wal_level 必须被设置为 logical,而 max_replication_slots 中设置的值必须至少是预期要连接的订阅数加上保留给表同步的连接数。max_wal_senders 应该至少被设置为 max_replication_slots 加上同时连接的物理复制体的数量。
订阅者必须配置 max_replication_slots。它必须设置为至少是订阅者数,加上一些用于表同步的预留。max_logical_replication_workers 必须至少被设置为订阅数加上保留给表同步的连接数。此外,可能需要调整 max_worker_processes 以容纳复制工作者,至少为 (max_logical_replication_workers + 1)。注意,一些扩展和并行查询也会从 max_worker_processes 中取得工作者槽。

发布端
  1. 修改配置文件
vim postgres.conf
wal_level = logical 
max_wal_senders = 100 #max_wal_senders大于max_replication_slots 
vim pg_hba.conf
host    all    repl    订阅者IP/32    md5

重启 pgsql ,配置生效

su postgres
pg_ctl restart -D /data/postgresql/pgdata/pg-5433/ 
  1. 发布者节点创建用户,赋权
su postgres
psql -P 5433
psql (16.9 (Ubuntu 16.9-0ubuntu0.24.10.1))
Type "help" for help.
#创建用户
postgres=# create user repuser password 'repuser' replication;
CREATE ROLE
#在postgres库中创建表,并插入一条数据
postgres=# create table test1(id int primary key,name varchar(10) );
postgres=# insert into test1 values(1,'a'); # 赋予复制用户select表权限
postgres=# grant select on table test1 to repuser; 
  1. 创建表 test1 的发布
create publication pub_test01 for table test1;
订阅端
  1. 修改 posetgres.conf
vim postgres.conf
max_replication_slots=8
max_logical_replication_workers=10
max_worker_processes=8

max_logical_replication_workers 逻辑复制进程数,应大于订阅节点的数量,并且给表同步预留一些进程数量
2. 重启 pgsql

pg_ctl restart -D /data/postgresql/pgdata/pg-5434/ 
  1. 创建订阅
CREATE SUBSCRIPTION subs_test01  CONNECTION 'host=192.168.28.11 dbname=postgres port=5433 user=repuser password=repuser' PUBLICATION pub_test01;
  1. 查看表是否同步
    在发布端执行 test1 表的 insert、delete、update、truncate 操作,看订阅端是否能同步

新加表同步

发布端
  1. 新建表
    create table test3 (id int,name varchar(10));
  2. 设置复制标识
    alter table test3 REPLICA IDENTITY full;
  3. 将表加入发布
    ALTER PUBLICATION pub1 ADD TABLE test3;
订阅端

在订阅端执行刷新
ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION ;
在发布端执行 test3 表的 insert、delete、update、truncate 操作,看订阅端是否能同步

发布订阅查询状态语句

发布端
select * from pg_replication_slots; --查看所有复制槽 select * from pg_stat_replication; --查看复制槽的同步状态 select * from pg_publication; --查看所有发布 select * from pg_publication_tables; --查看所有发布对应的表 select usename,a.pubname,c.*,pubinsert,pubupdate,pubdelete,pubtruncate from pg_publication a,pg_user b,pg_publication_tables c where a.pubowner=b.usesysid and c.pubname=a.pubname;
订阅端
select * from pg_subscription; --查看所有订阅 select srrelid::regclass from pg_subscription_rel; --查看订阅的所有表 select usename,datname,subname,srrelid::regclass,srsublsn,subconninfo from pg_subscription a,pg_user b,pg_subscription_rel c ,pg_database d where a.oid=c.srsubid and a.subowner=b.usesysid and a.subdbid=d.oid;

删除发布订阅

删除订阅

订阅在运行的情况下,需要一下操作:

ALTER SUBSCRIPTION pub_test01;
##首先需要停止当前的订阅 ALTER SUBSCRIPTION subs_test01 (slot_name =NONE);
##然后将订阅的复制槽设置成空
drop subscription subs_test01;
##最后就可以删除订阅了。 ##另外一个问题,复制订阅中,如果订阅的服务停止,或无法再次连接的情况下,需要关注 发布端的数据wal log 无法清理以及膨胀的问题。 ##所以在复制订阅中的订阅停止后,如果确认订阅无法再次恢复,或者不确认多长时间恢复,则需要删除复制槽 select * from pg_replication_slots;

select pg_drop_replication_slot(slot_name) from pg_replication_slots where slot_name = 'pub_test01';

📌 转载信息
原作者:
xxdtb
转载时间:
2026/1/19 17:52:23

概述
使用 postgresql + etcd + patroni + haproxy + keepalived 可以实现 PG 的高可用集群,其中,以 postgresql 做数据库,Patroni 监控本地的 PostgreSQL 状态,并将本地 PostgreSQL 信息 / 状态写入 etcd 来存储集群状态,所以,patroni 与 etcd 结合可以实现数据库集群故障切换(自动或手动切换),而 haproxy 可以实现数据库读写分离 + 读负载均衡(通过不同端口实现),keepalived 实现 VIP 跳转,对 haproxy 提供了高可用,防止 haproxy 宕机。

Patroni 介绍

Patroni 是一个基于 Python 的用于实现 PostgreSQL HA 解决方案的框架。为了最大程度的兼容性,它支持多种分布式配置存储,包括 ZooKeeper、etcd、Consul 或 Kubernetes。旨在帮助数据库工程师、DBA、DevOps 工程师和 SRE 快速部署数据中心(或任何地方)的 HA PostgreSQL 环境。

当前支持的 PostgreSQL 版本从 9.3 到 16。支持自动化故障转移、物理复制和逻辑复制、提供 RESTful API 接口,允许外部应用或运维工具直接操作 PostgreSQL 集群,进行如启停、迁移等操作,与 Linux watchdog 集成,以避免脑裂现象。

项目地址: GitHub - patroni/patroni: A template for PostgreSQL High Availability with Etcd, Consul, ZooKeeper, or Kubernetes

ETCD 介绍

etcd 是一个分布式键值存储数据库,支持跨平台,拥有强大的社区。etcd 的 Raft 算法,提供了可靠的方式存储分布式集群涉及的数据。etcd 广泛应用在微服务架构和 Kubernates 集群中,不仅可以作为服务注册与发现,还可以作为键值对存储的中间件。从业务系统 Web 到 Kubernetes 集群,都可以很方便地从 etcd 中读取、写入数据。
etcd 完整的 cluster(集群)至少有三台,这样才能选举出一个 master 节点,两个 slave 节点。如果小于 3 台则无法进行选举,造成集群不可用。Etcd 使用 2379 和 2380 端口。
2379 端口:提供 HTTP API 服务,和 etcdctl 交互
2380 端口:集群中节点间通讯
项目地址:[[GitHub - etcd-io/etcd: Distributed reliable key-value store for the most critical data of a distributed system]]

环境说明

服务器信息
服务器名ip 地址os数据库读写端口只读端口组件组件端口
k8s-mater01192.168.28.11ubuntu 24.101543325433PostgreSQL,Patroni、Etcd,haproxy、keepalived8008
k8s-mater02192.168.28.12ubuntu 24.101543325433PostgreSQL,Patroni、Etcd,haproxy、keepalived8008
k8s-mater01192.168.28.13ubuntu 24.101543325433PostgreSQL,Patroni、Etcd,haproxy、keepalived8008
vip192.168.28.10PostgreSQL,Patroni、Etcd,haproxy、keepalived8008
软件信息
软件名版本
Patroni4.0.6
Etcd3.5.16
Keepalived2.3.1
Haproxy2.9.10-1ubuntu1.2
PostgreSQL16.9
watchdog5.16
python3.12.7
架构图

这个架构中,PostgreSQL 提供数据服务,Patroni 负责主从切换,etcd 提供一致性存储,HAProxy 提供访问路由,Keepalived 提供网络 VIP 高可用,Watchdog 提供节点存活及脑裂防护机制。 六者协同组成一个企业级高可用数据库集群

预先准备

网络设置
防火墙设置

关闭防火墙 (相对简单,但是不安全)
放行对应的端口(安全)

安装 PostgreSQL

通过 apt 安装 PostgreSQL (所有节点)
sudo apt install curl ca-certificates  
sudo install -d /usr/share/postgresql-common/pgdg  
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc  
. /etc/os-release  
sudo sh -c "echo 'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $VERSION_CODENAME-pgdg main' > /etc/apt/sources.list.d/pgdg.list" sudo apt update  
sudo apt -y install postgresql
数据库目录设置 (所有节点)
mkdir -p /data/postgresql/pgdata/
mkdir -p /data/postgresql/pg_archive/
chown -R postgres:postgres /data/postgresql/
postgres 用户设置
设置家目录
mkdir -p /home/postgres/
chown -R postgres:postgres /home/postgres/
vim /etc/passwd
#修改postgres 家目录为/home/postgres postgres:x:114:113:PostgreSQL administrator,,,:/home/postgres:/bin/bash 
设置环境变量
vim /home/postgres/.bashrc

[ -f /etc/profile ] && source /etc/profile
export PATH=/usr/lib/postgresql/16/bin:$PATH # If you want to customize your settings, # Use the file below. This is not overridden # by the RPMS.
[ -f /var/lib/pgsql/.pgsql_profile ] && source /var/lib/pgsql/.pgsql_profile
设置 sudo 免密
vim /etc/sudoers
#行末新增
postgres  ALL=(ALL)       NOPASSWD: ALL 

安装 ETCD 集群 (所有节点)

安装 ectd
下载 etcd
wget https://github.com/etcd-io/etcd/releases/download/v3.6.4/etcd-v3.6.4-linux-amd64.tar.gz

tar -xf etcd-v3.6.4-linux-amd64.tar.gz --strip-components=1 -C /usr/local/bin etcd-v3.6.4-linux-amd64/etcd etcd-v3.6.4-linux-amd64/etcdctl


mkdir -p /etc/etcd/
mkdir -p /data/etcd/

编辑 etcd 配置文件
touch /etc/etcd/etcd-pg.config.yml

#节点1配置文件
vim /etc/etcd/etcd-pg.config.yml

#节点名
name: pg-etcd01
#数据目录 
data-dir: /data/etcd
snapshot-count: 5000
#选举和心跳参数
heartbeat-interval: 100
election-timeout: 1000
#存储新能优化
quota-backend-bytes: 8589934592
max-request-bytes: 10485760
max-concurrent-requests: 5000
#自动压缩与碎片整理
auto-compaction-mode: periodic
auto-compaction-retention: "2h"
#集群通信配置
listen-peer-urls: "http://192.168.28.11:12380"
listen-client-urls: "http://192.168.28.11:12379,http://127.0.0.1:12379"
max-snapshots: 3
max-wals: 5
cors:
initial-advertise-peer-urls: "http://192.168.28.11:12380"
advertise-client-urls: "http://192.168.28.11:12379"
discovery:
discovery-fallback: 'proxy'
discovery-proxy:
discovery-srv:
initial-cluster: "pg-etcd01=http://k8s-etcd01:12380,pg-etcd02=http://k8s-etcd02:12380,pg-etcd03=http://k8s-etcd03:12380"
initial-cluster-token: 'etcd-cluster-pg'
initial-cluster-state: 'new'
strict-reconfig-check: false
enable-v2: true
enable-pprof: true
proxy: 'off'
proxy-failure-wait: 5000
proxy-refresh-interval: 30000
proxy-dial-timeout: 1000
proxy-write-timeout: 5000
proxy-read-timeout: 0


#节点2配置文件
name: pg-etcd02
data-dir: /data/etcd

snapshot-count: 5000
#选举和心跳参数
heartbeat-interval: 100
election-timeout: 1000
#存储新能优化
quota-backend-bytes: 8589934592
max-request-bytes: 10485760
max-concurrent-requests: 5000
#自动压缩与碎片整理
auto-compaction-mode: periodic
auto-compaction-retention: "2h"
#集群通信配置
listen-peer-urls: "http://192.168.28.12:12380"
listen-client-urls: "http://192.168.28.12:12379,http://127.0.0.1:12379"
max-snapshots: 3
max-wals: 5
cors:
initial-advertise-peer-urls: "http://192.168.28.12:12380"
advertise-client-urls: "http://192.168.28.12:12379"
discovery:
discovery-fallback: 'proxy'
discovery-proxy:
discovery-srv:
initial-cluster: "pg-etcd01=http://k8s-etcd01:12380,pg-etcd02=http://k8s-etcd02:12380,pg-etcd03=http://k8s-etcd03:12380"
initial-cluster-token: 'etcd-cluster-pg'
initial-cluster-state: 'new'
strict-reconfig-check: false
enable-v2: true
enable-pprof: true
proxy: 'off'
proxy-failure-wait: 5000
proxy-refresh-interval: 30000
proxy-dial-timeout: 1000
proxy-write-timeout: 5000
proxy-read-timeout: 0


#节点3配置文件
name: pg-etcd03
data-dir: /data/etcd
snapshot-count: 5000
#选举和心跳参数
heartbeat-interval: 100
election-timeout: 1000
#存储新能优化
quota-backend-bytes: 8589934592
max-request-bytes: 10485760
max-concurrent-requests: 5000
#自动压缩与碎片整理
auto-compaction-mode: periodic
auto-compaction-retention: "2h"
#集群通信配置
listen-peer-urls: "http://192.168.28.13:12380"
listen-client-urls: "http://192.168.28.13:12379,http://127.0.0.1:12379"
max-snapshots: 3
max-wals: 5
cors:
initial-advertise-peer-urls: "http://192.168.28.13:12380"
advertise-client-urls: "http://192.168.28.13:12379"
discovery:
discovery-fallback: 'proxy'
discovery-proxy:
discovery-srv:
initial-cluster: "pg-etcd01=http://k8s-etcd01:12380,pg-etcd02=http://k8s-etcd02:12380,pg-etcd03=http://k8s-etcd03:12380"
initial-cluster-token: 'etcd-cluster-pg'
initial-cluster-state: 'new'
strict-reconfig-check: false
enable-v2: true
enable-pprof: true
proxy: 'off'
proxy-failure-wait: 5000
proxy-refresh-interval: 30000
proxy-dial-timeout: 1000
proxy-write-timeout: 5000
proxy-read-timeout: 0
创建 etcd 服务
vim /etc/systemd/system/etcd-pg.service 
[Unit]
Description=Etcd Server
After=network.target
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/local/bin/etcd --config-file=/etc/etcd/etcd-pg.config.yml
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

启动 etcd-pg 服务

systemctl daemon-reload
systemctl start etcd-pg.service
systemctl enable etcd-pg.service
检查 etcd 集群健康状态
root@k8s-master01:~# etcdctl --endpoints="k8s-etcd01:12379,k8s-etcd02:12379,k8s-etcd03:12379" member list  -w=table

+------------------+---------+-----------+-------------------------+----------------------------+------------+
|        ID        | STATUS  |   NAME    |       PEER ADDRS        |        CLIENT ADDRS        | IS LEARNER |
+------------------+---------+-----------+-------------------------+----------------------------+------------+
| 2bb79737c88dd84d | started | pg-etcd03 | http://k8s-etcd03:12380 | http://192.168.28.13:12379 |      false |
| 354b7a6aa8551f4a | started | pg-etcd02 | http://k8s-etcd02:12380 | http://192.168.28.12:12379 |      false |
| 84101e54de967367 | started | pg-etcd01 | http://k8s-etcd01:12380 | http://192.168.28.11:12379

为了简化命令,可以通过 alisa 配置

cd ~
vim .profile
alias  etcdctlpg="etcdctl --endpoints="k8s-etcd01:12379,k8s-etcd02:12379,k8s-etcd03:12379" " source .profile

root@k8s-master01:~# etcdctlpg member list -w=table
+------------------+---------+-----------+-------------------------+----------------------------+------------+
|        ID        | STATUS  |   NAME    |       PEER ADDRS        |        CLIENT ADDRS        | IS LEARNER |
+------------------+---------+-----------+-------------------------+----------------------------+------------+
| 2bb79737c88dd84d | started | pg-etcd03 | http://k8s-etcd03:12380 | http://192.168.28.13:12379 |      false |
| 354b7a6aa8551f4a | started | pg-etcd02 | http://k8s-etcd02:12380 | http://192.168.28.12:12379 |      false |
| 84101e54de967367 | started | pg-etcd01 | http://k8s-etcd01:12380 | http://192.168.28.11:12379 |      false |

Etcd 可视化工具

安装 watchdog (所有节点)

watchdog 防止脑裂。Patroni 支持通过 Linux 的 watchdog 监视 patroni 进程的运行,当 patroni 进程无法正常往 watchdog 设备写入心跳时,由 watchdog 触发 Linux 重启。

# 安装软件,linux内置功能 sudo apt install -y watchdog
# 初始化watchdog字符设备 sudo modprobe softdog
# 修改/dev/watchdog设备权限 sudo chmod 666 /dev/watchdog
sudo chown postgres:postgres /dev/watchdog
# 启动watchdog服务 sudo systemctl start watchdog
sudo systemctl enable watchdog


安装 Patroni (所有节点)

安装
1. pip3 install --break-system-packages  psycopg2-binary
2. pip3 install --break-system-packages  patroni[etcd]
3. pip3 install --break-system-packages python-json-logger   
4. mkdir -p /etc/patroni 
创建配置文件
创建 Patroni 服务
cat /etc/systemd/system/patroni-5433.service 
[Unit]
Description=Patroni high-availability PostgreSQL
After=syslog.target network.target etcd.service
Requires=etcd-pg.service

[Service]
Type=simple
User=postgres
Group=postgres
# 使用watchdog进行服务监控 ExecStartPre=-/usr/bin/sudo /sbin/modprobe softdog ExecStartPre=-/usr/bin/sudo /bin/chown postgres /dev/watchdog
PermissionsStartOnly=true
WorkingDirectory=/home/postgres/
ExecStart=/usr/local/bin/patroni /etc/patroni/patroni-5433.yaml
ExecReload=/bin/kill -HUP 
KillMode=process
Restart=always
RestartSec=10
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

systemctl daemon-reload
配置免密登录
su postgres
cd ~
#备库从主库同步WAL日志使用,主备倒换后,主库降备库,新备库使用,所以备库也配置
touch .pgpass
vim .pgpass
192.168.28.11:5433:*:replica:replica 192.168.28.12:5433:*:replica:replica 192.168.28.13:5433:*:replica:replica 
创建 日志目录
sudo mkdir -p /var/log/patroni/
sudo chown -R postgres:postgres /var/log/patroni/
启动 Patroni

根据节点依次启动 Patroni

sudo systemctl daemon-reload
sudo systemctl restart patroni-5433.service
sudo systemctl enable patroni-5433.service
sudo systemctl status patroni-5433.service

查看服务状态,默认情况下,根据配置文件中,initdb 内容,Patroni 会自动对数据库进行初始化操作,并创建用户,配置文件,拉起数据库并建立主从关系及流复制

查看状态
root@k8s-master01:~# patronictl  -c /etc/patroni/patroni-5433.yaml list
+ Cluster: pg_patroni_etcd () ---+-----------+----+-----------+ | Member | Host               | Role    | State     | TL | Lag in MB | + | pg_patroni_5433_01 | 192.168.28.11:5433 | Leader  | running | 17 | | | pg_patroni_5433_02 | 192.168.28.12:5433 | Replica | streaming | 17 | 0 | | pg_patroni_5433_03 | 192.168.28.13:5433 | Replica | streaming | 17 | 0 | + 

通过 alisa 设置简易命令

alias  patr5433="patronictl  -c /etc/patroni/patroni-5433.yaml" 

patroni 维护命令 (所有节点)

列出节点信息
patronictl -c /etc/patroni/patroni-5433.yaml list
重做备库

reinit 先是移除了整个 data 目录。然后选择正确的节点进行备份恢复。

patronictl -c /etc/patroni/patroni-5433.yaml reinit [nodename]
查看配置
patronictl -c /etc/patroni/patroni-5433.yaml show-config
更改参数
patronictl -c /etc/patroni/patroni-5433.yaml edit-config
#重载参数
patronictl -c /etc/patroni/patroni-5433.yaml reload [nodename]
重启节点 / 关闭节点
  1. 仅重启当前节点
patronictl -c /etc/patroni/patroni-5433.yaml restart [clustername] [nodename] 
  1. 如果节点是 pending 状态的,才会执行重启操作
patronictl -c /etc/patroni/patroni-5433.yaml restart [clustername] --pending 
  1. 重启所有成员
patronictl -c /etc/patroni/patroni-5433.yaml restart [clustername]
维护模式 脱离 patroni 的集群管理
patronictl pause

patronictl pause 暂时将 Patroni 集群置于维护模式并禁用自动
在某些情况下,Patroni 需要暂时退出集群管理,同时仍然在 DCS 中保留集群状态。可能的用例是集群上不常见的活动,例如主要版本升级或损坏恢复。在这些活动期间,节点经常因为 Patroni 不知道的原因而启动和停止,有些节点甚至可以暂时提升,这违反了只运行一个主节点的假设。因此,Patroni 需要能够与正在运行的集群 “分离”,在 Pacemaker 中实现与维护模式相当的功能。

patronictl resume

patronictl resume 将使 Patroni 集群退出维护模式,并重新启用自动故障转移。
自动拉起所有数据库

switchover 主备切换
patronictl switchover
# Switchover
root@k8s-master01:~# patr5433   switchover
Current cluster topology
+ Cluster: pg_patroni_etcd (7540584003720074567) ---+-----------+----+-----------+
| Member             | Host               | Role    | State     | TL | Lag in MB |
+--------------------+--------------------+---------+-----------+----+-----------+
| pg_patroni_5433_01 | 192.168.28.11:5433 | Leader  | running   | 22 |           |
| pg_patroni_5433_02 | 192.168.28.12:5433 | Replica | streaming | 22 |         0 |
| pg_patroni_5433_03 | 192.168.28.13:5433 | Replica | streaming | 22 |         0 |
+--------------------+--------------------+---------+-----------+----+-----------+
Primary [pg_patroni_5433_01]: 
Candidate ['pg_patroni_5433_02', 'pg_patroni_5433_03'] []: pg_patroni_5433_02
When should the switchover take place (e.g. 2025-08-26T12:26 )  [now]: now
Are you sure you want to switchover cluster pg_patroni_etcd, demoting current leader pg_patroni_5433_01? [y/N]: y 
2025-08-26 11:26:32.31189 Successfully switched over to "pg_patroni_5433_02"
+ Cluster: pg_patroni_etcd (7540584003720074567) ---+---------+----+-----------+
| Member             | Host               | Role    | State   | TL | Lag in MB |
+--------------------+--------------------+---------+---------+----+-----------+
| pg_patroni_5433_01 | 192.168.28.11:5433 | Replica | stopped |    |   unknown |
| pg_patroni_5433_02 | 192.168.28.12:5433 | Leader  | running | 22 |           |
| pg_patroni_5433_03 | 192.168.28.13:5433 | Replica | running | 22 |         0 |
+--------------------+--------------------+---------+---------+----+-----------+
root@k8s-master01:~# patr5433   list
+ Cluster: pg_patroni_etcd (7540584003720074567) ---+-----------+----+-----------+
| Member             | Host               | Role    | State     | TL | Lag in MB |
+--------------------+--------------------+---------+-----------+----+-----------+
| pg_patroni_5433_01 | 192.168.28.11:5433 | Replica | streaming | 23 |         0 |
| pg_patroni_5433_02 | 192.168.28.12:5433 | Leader  | running   | 23 |           |
| pg_patroni_5433_03 | 192.168.28.13:5433 | Replica | streaming | 23 |         0 |
+--------------------+--------------------+---------+-----------+----+-----------+
接口切换
 数据库从 pg_patroni_5433_01 switchover 到 pg_patroni_5433_02
[root@pgtest1 ~]# curl -s http://192.168.28.11:8008/switchover -XPOST -d '{"leader":"pg_patroni_5433_01","candidate":"pg_patroni_5433_02"}'
Successfully switched over to "pg_patroni_5433_02" 
failover 切换

patronictl failover

# Failover
[postgres@pgtest1 ~]$ patronictl -c /etc/patroni/patroni-5433.yaml  failover
Candidate ['pg_patroni_5433_01', 'pg_patroni_5433_02','pg_patroni_5433_03'] []: pg_patroni_5433_01
Current cluster topology
... ...
Are you sure you want to failover cluster pg_cluster, demoting current master pg_patroni_5433_02? [y/N]: y
2021-10-28 03:47:56.13486 Successfully failed over to "pg_patroni_5433_01"
... ...

获取主节点 dsn 信息
root@k8s-master01:~# patronictl -c /etc/patroni/patroni-5433.yaml  dsn
host=192.168.28.12 port=5433 

安装 Haproxy (所有节点)

安装 Haproxy
sudo apt install haproxy
编辑 Haproxy 配置文件
sudo vim /etc/haproxy/haproxy.cfg 

global
 maxconn 2000
 ulimit-n 16384
 log 127.0.0.1 local0 err
 stats timeout 30s

defaults
 log global
 mode http
 option httplog
 timeout connect 5000
 timeout client 50000
 timeout server 50000
 timeout http-request 15s
 timeout http-keep-alive 15s

listen status_page
    bind *:8888
    stats enable
    stats uri /haproxy-status
    stats auth    admin:admin
    stats realm "Welcome to the haproxy load balancer status page of k8s-master"

frontend monitor-in
 bind *:33305
 mode http
 option httplog
 monitor-uri /monitor
# 主库读写端口
listen master
    bind *:15433
    mode tcp
    option tcplog
    balance roundrobin
    option httpchk OPTIONS /master
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server pgtest1 192.168.28.11:5433 maxconn 1000 check port 8008 inter 5000 rise 2 fall 2
    server pgtest2 192.168.28.12:5433 maxconn 1000 check port 8008 inter 5000 rise 2 fall 2
    server pgtest3 192.168.28.13:5433 maxconn 1000 check port 8008 inter 5000 rise 2 fall 2

#从库读端口
listen replicas
    bind *:25433
    mode tcp
    option tcplog
    balance roundrobin
    option httpchk OPTIONS /replica
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server pgtest1 192.168.28.11:5433 maxconn 1000 check port 8008 inter 5000 rise 2 fall 2
    server pgtest2 192.168.28.12:5433 maxconn 1000 check port 8008 inter 5000 rise 2 fall 2
    server pgtest3 192.168.28.13:5433 maxconn 1000 check port 8008 inter 5000 rise 2 fall 2
启动 haproxy
sudo systemctl enable haproxy
sudo systemctl start haproxy
HAProxy 监控页面

登录地址:[[http://192.168.28.11:8888/haproxy-status]] (也可以通过各个节点 IP + 端口登录)

默认用户密码:admin/admin

安装 Keepalived (所有节点)

安装
sudo apt install -y keepalived
配置文件
主服务配置文件
vim /etc/keepalived/keepalived.conf

global_defs {

router_
id LVS_DEVEL00

script_
user root enable_script_security } vrrp_script check_haproxy { script "/etc/keepalived/check_haproxy.sh" interval 2 weight 5 fall 3 rise 5 timeout 2 } vrrp_instance VI_1 { state Master interface ens18 virtual_router_id 80 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 12345 } virtual_ipaddress { 192.168.28.10/24 } track_script { check_haproxy } }
备库节点 1 配置文件
vim /etc/keepalived/keepalived.conf
global_defs {

router_
id LVS_DEVEL01

script_
user root enable_script_security } vrrp_script check_haproxy { script "/etc/keepalived/check_haproxy.sh" interval 2 weight 5 fall 3 rise 5 timeout 2 } vrrp_instance VI_1 { state BACKUP interface ens18 virtual_router_id 80 priority 90 advert_int 1 authentication { auth_type PASS auth_pass 12345 } virtual_ipaddress { 192.168.28.10/24 } track_script {

check_
haproxy } }
备库节点 2 配置文件
vim /etc/keepalived/keepalived.conf

global_defs {

router_
id LVS_DEVEL02

script_
user root enable_script_security } vrrp_script check_haproxy { script "/etc/keepalived/check_haproxy.sh" interval 2 weight 5 fall 3 rise 5 timeout 2 } vrrp_instance VI_1 { state BACKUP interface ens18 virtual_router_id 80 priority 80 advert_int 1 authentication { auth_type PASS auth_pass 12345 } virtual_ipaddress { 192.168.24.15/24 } track_script {

check_
haproxy } }
检查脚本
vim /etc/keepalived/check_haproxy.sh

#!/bin/bash
count=`ps aux | grep -v grep | grep haproxy | wc -l`
if [ $count -eq 0 ]; then exit 1
else exit 0
fi 

赋予执行权限

chmod +x /etc/keepalived/check_haproxy.sh
依次启动
sudo systemctl start keepalived
sudo systemctl enable keepalived

📌 转载信息
原作者:
xxdtb
转载时间:
2026/1/19 17:52:08

pgBackRest
简介

pgBackRest 旨在提供一个简单可靠,容易纵向扩展的 PostgreSQL 备份恢复系统。pgBackRest 并不依赖像 tar 和 rsync 这样的传统备份工具,而是通过在内部实现所有备份功能,并使用自定义协议来与远程系统进行通信。 消除对 tar 和 rsync 的依赖可以更好地解决特定于数据库的备份问题。 自定义远程协议提供了更多的灵活性,并限制执行备份所需的连接类型,从而提高安全性。

相关网站

pgBackRest 主页:http://pgbackrest.org
手册:pgBackRest User Guide - RHEL
pgBackRest
Github 主页:GitHub - pgbackrest/pgbackrest: Reliable PostgreSQL Backup & Restore

pgbackRest 特征
  • 并行备份和还原
  • 本地或远程操作
  • 完整,增量和差异备份
  • 备份轮换和存档到期
  • 备份完整性
  • 页面校验和
  • 备份恢复
  • 流压缩和校验和
  • 增量还原
  • 并行,异步 WAL Push&Get
  • 表空间和链接支持
  • S3、Azure 和 GCS 兼容对象存储支持
  • 加密
  • 与 PostgreSQL > = 8.3 的兼容性
pgbackRest 安装
ip软件角色
192.168.1.11postgres,pgbackrestprimary
192.168.1.12postgres,pgbackreststandby
192.168.1.13postgres,standby
192.168.1.16pgbackrest远程备份工具端

ubuntu (所有节点) :

sudo apt-get install pgbackrest
创建所需目录并赋予权限
su - root
mkdir -p -m 770 /var/log/pgbackrest
chown postgres.postgres /var/log/pgbackrest/
chown postgres.postgres -R /etc/pgbackrest/
主从 pgbackrest 配置文件
su postgres

vim /etc/pgbackrest.conf

[pg_5433]
pg1-port=5433
pg1-path=/data/postgresql/pgdata/pg-5433/
pg1-socket-path=/var/run/postgresql/

[global]
repo1-host=192.168.28.14
repo1-host-user=postgres
repo1-path=/var/lib/pgbackrest
repo1-retention-full=2
process-max=3

[global:archive-push]
compress-level=3 

主库参数解释:

[pg_5433] 部分
这个 pg_5433 是备份集群的名字,多个备份集群可以添加 比如:[test-1],[test-2] 等等
pg1-path=/data/postgresql/pgdata/pg-5433/: 指定 PostgreSQL 数据库实例的数据目录路径。这是 pgBackRest 需要备份的主要内容所在的位置。
pg1-socket-path=/var/run/postgresql/: 指定 PostgreSQL 服务器的套接字文件(socket file)路径。pgBackRest 使用这个路径来通过 UNIX 套接字连接到数据库。
pg1-user=postgres: 定义 pgBackRest 连接到 PostgreSQL 实例时应该使用的用户名。这个用户需要有足够的权限来读取数据库文件和执行备份相关的操作。
[global] 部分
这部分的配置适用于 pgBackRest 的全局设置,影响所有备份和恢复操作。
repo1-host=192.168.28.14: 指定远程备份仓库的主机地址。这表明备份数据将被存储在指定 IP 地址的服务器上。pgBackRest 支持多个备份仓库,这里的 repo1 表示第一个仓库。
repo1-host-user=postgres: 定义访问远程备份仓库主机时使用的用户名。postgres 将以这个用户的身份在远程主机上执行操作。
log-level-file=detail: 设置文件日志记录的详细级别。detail 级别会记录更详细的操作信息,有助于故障排查和监控备份过程。
log-path=/var/log/pgbackrest: 指定日志文件的存储路径。pgBackRest 会将运行日志写入这个目录下,便于后续的日志分析和问题定位。

远程备份工具端配置文件
su postgres
vim /etc/pgbackrest.conf

[pg_5433]
pg1-path=/data/postgresql/pgdata/pg-5433/
pg1-port=5433
pg1-socket-path=/var/run/postgresql/
pg1-host-config=/etc/pgbackrest.conf
pg1-user=postgres
pg1-host=192.168.28.11
pg1-host-port=22
pg1-host-user=postgres

pg2-path=/data/postgresql/pgdata/pg-5433/
pg2-port=5433
pg2-socket-path=/var/run/postgresql/
pg2-host-config=/etc/pgbackrest.conf
pg2-user=postgres
pg2-host=192.168.28.12
pg2-host-port=22
pg2-host-user=postgres

pg3-path=/data/postgresql/pgdata/pg-5433/
pg3-port=5433
pg3-socket-path=/var/run/postgresql/
pg3-host-config=/etc/pgbackrest.conf
pg3-user=postgres
pg3-host=192.168.28.13
pg3-host-port=22
pg3-host-user=postgres

[global]
backup-standby=y
process-max=3
start-fast=y
repo1-path=/var/lib/pgbackrest
repo1-retention-full=2
backup-user=postgres
log-level-console=info
log-level-file=debug
buffer-size=16MiB
compress-type=gz 

[global:archive-push]
compress-level=3 

工具端参数解释

[pg_5433]
这个 pg_5433 是备份集群的名字,多个备份集群可以添加 比如:[test-1],[test-2] 等等
pg1-path :指定了数据库实例的数据目录路径。
pg1-port :定义了实例的端口号,通常 PostgreSQL 默认端口是 5432。
pg1-socket-path:指定了 UNIX 套接字文件的路径,用于本地连接。
pg1-user :定义 pgBackRest 连接到各个 PostgreSQL 实例时使用的用户名。
pg1-host-config-path :指定远程主机上 pgBackRest 配置文件的路径。
pg1-host:定义了各实例所在的主机地址。
pg1-host-port :指定了用于 SSH 连接的端口号,默认为 22。
pg1-host-user:定义了 SSH 连接时使用的用户名。

[global] 部分
这部分定义了全局备份策略和行为。
backup-standby=y:启用备份从备用服务器进行,这有助于减少对生产数据库的性能影响。
process-max=3:定义了 pgBackRest 同时执行任务的最大进程数。
start-fast=y:启用快速启动模式,尝试减少备份期间的停机时间。
repo1-path:指定了备份仓库的路径。
repo1-retention-full=2:定义了完整备份的保留数量,超过这个数量的旧备份将被删除。
backup-user:指定执行备份操作的用户。
log-level-console:设置控制台日志级别。
log-level-file:设置文件日志的详细级别。
buffer-size:定义了缓冲区大小,用于优化性能。
compress-type=gz:指定了压缩类型,这里使用的是 gzip。
[global:archive-push] 部分
这部分专门用于配置归档推送操作。

compress-level=3:定义了压缩级别,数值越高,压缩效果越好,但需要更多的 CPU 资源。
ssh 免密登陆配置

在各个服务器上,生成 postgres 的 ssh 密钥

su postgres
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N "" 

在主从服务器上 postgres 用户 配置备份服务器 postgres 的用户公钥
登录备份服务器 192.168.28.14

su postgres
cd ~
cat ./.ssh/id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDnvcOPSQgi2zKWqNHsFjKC0zp4X5+yG1eNf5fEr25r2+NlBGMRrBFh2pONh2pWSLiglbhOZA5Pr1ILpllwP34eiGNjxTp0ys0U1YjnOuvgY7iwwR+xkXJmywDb0g0ALSEi3TS0lu5z3u4mBcW03q4m/oS++Fi+7ieDinyQAZOXXOyvj8k7g7/NiUXzONN83do/+KC5htVm9Q77A2DrDmZWQGbypMKQYPY66RjcWvApPOVYbrUxHlndq3fU4IhHPOVwiAdpHm5bh8dyb9k1FWcIS9sxLVm4KsUbt99VeDC8ri7iglMKen+gcktIyo80rGRoIdzJrD6JPP8cTlhpTwV/uW42kWgS9lZ8I/Ahk7lWoDdiF/pVNkMiiTOgZ2/YGV88CE0khpOtRl3nPHFlUZHi1QLdfH9omI0FZWeLYAuQbKWBGZ8GgfAweKjEtMy/J43NO5qGK6JZ0KB2ve03JowCGbW65cmTuPQgz3Hwo5I0fv3YEy88LK9nVnLub44zunGqJ4JBAc2H/WrmSqLYtLtljo/5EuKmc34SS75WimY9wh1nTmhVPODuLzurXjz28zx245tkcLeImbn4C8Gge4I7TgtPj8VkWTXC6WlrTTLjebLuMjYR3qFfuGqfD2vuLEHU4CBGHAnpDCG51v96gBpw+m9Cman6f9KvA3iZRBOXHw== postgres@k8s-node01

在主从服务器上添加备份服务器的公钥

root@k8s-master01:~# su postgres
postgres@k8s-master01:/root$cd ~     
postgres@k8s-master01:~$vim ./.ssh/authorized_keys

同样的在备份服务器的 postgres 的用户上配置主从服务器上 postgres 用户的公钥

设置 postgresql 归档

在主库上修改 archive 归档

patronictl -c /etc/patroni/patroni-5433.yaml edit-config archive_mode = on archive_command = 'pgbackrest --stanza=pg_5433 archive-push %p'
max_wal_senders = 3 

stanza 名需要和配置文件一致
修改完之后,通过 patronictl 重启所有数据库

patronictl -c /etc/patroni/patroni-5433.yaml restart pg_patroni_etcd pg_patroni_5433_01
patronictl -c /etc/patroni/patroni-5433.yaml restart pg_patroni_etcd pg_patroni_5433_02
patronictl -c /etc/patroni/patroni-5433.yaml restart pg_patroni_etcd pg_patroni_5433_03
初始化备份

在备份服务器上执行

su postgres
pgbackrest --stanza=pg_5433 --log-level-console=info stanza-create

#删除方式
pgbackrest --stanza=pg_5433 stop
pgbackrest --stanza=pg_5433 stanza-delete --force 
检查配置
pgbackrest --stanza=pg_5433 --log-level-console=info check

#执行结果如下
postgres@k8s-node01:~$ pgbackrest --stanza=pg_5433  --log-level-console=info check
2025-10-27 15:40:57.655 P00   INFO: check command begin 2.50: --backup-standby --buffer-size=16MiB --exec-id=3943474-ea8cda56 --log-level-console=info --log-level-file=debug --pg1-host=192.168.28.25 --pg1-host-config=/etc/pgbackrest.conf --pg1-host-port=22 --pg1-host-user=postgres --pg1-path=/data/postgresql/pgdata/pg-5433/ --pg1-port=5433 --pg1-user=postgres --repo1-path=/var/lib/pgbackrest --stanza=pg_5433
WARN: option 'backup-standby' is enabled but standby is not properly configured
2025-10-27 15:41:00.980 P00   INFO: check repo1 configuration (primary)
2025-10-27 15:41:01.387 P00   INFO: check repo1 archive for WAL (primary)
2025-10-27 15:41:06.703 P00   INFO: WAL segment 000000230000000000000059 successfully archived to '/var/lib/pgbackrest/archive/pg_5433/16-1/0000002300000000/000000230000000000000059-0c80b7903193612ee31642ac12c2548bba36bc1b.gz' on repo1
2025-10-27 15:41:06.806 P00   INFO: check command end: completed successfully (9159ms)

完全备份
在备份机上操作即可
pgbackrest --stanza=pg_5433 --log-level-console=info backup --type=full
增量备份
pgbackrest pgbackrest --stanza=pg_5433 --log-level-console=info backup --type=incr
差异备份
pgbackrest pgbackrest --stanza=pg_5433 --log-level-console=info backup --type=diff
查看备份信息
备份恢复

这个恢复操作的一台新的 postgresql 主机上操作,并配置好 pgbackrest.conf 文件 和 备份机可以免密登陆 ssh 登陆

 # 全量恢复
$ pgbackrest --stanza=pg_5433 restore --pg1-path=/data/postgresql/pgdata/pg-5433/
 
 
 
# 指定某个备份恢复
pgbackrest --stanza=pg_5433 --set=20251027-134949F restore
 
# 基于lsn恢复 # 指定备份策略,获取对应的lsn:lsn start/stop
pgbackrest --stanza=pg_5433 --set=20250713-195747F_20250713-195909I info
pgbackrest --stanza=pg_5433 --type=lsn --target="0/41000028" restore
 
# 基于时间点恢复

pgbackrest --stanza=pg_5433 --delta --type=time "--target=2025-10-27 14:11:02+08" restore

常用命令
# 创建存储库
pgbackrest --stanza=pg_5433 stanza-create
# 删除存储库
pgbackrest --stanza=pg_5433 stanza-delete
# 更新存储库
pgbackrest --stanza=pg_5433 stanza-upgrade
 
# 启用备份
pgbackrest --stanza=pg_5433 start
# 停用备份
pgbackrest --stanza=pg_5433 stop
 
# 备份数据
pgbackrest --stanza=pg_5433 backup
# 恢复备份
pgbackrest --stanza=pg_5433 restore
# 查看备份
pgbackrest --stanza=pg_5433 info
 
# 检查备份是否过期
pgbackrest expire --stanza=pg_5433
pgbackrest expire --set=20250713-195747F_20250713-195909I --stanza=demo
 
# 检查配置
pgbackrest --stanza=pg_5433 check
 
# 获取存储库信息 # 疑似有bug,执行报错:ERROR: [032]: unable to determine cipher passphrase for ''
pgbackrest --stanza=pg_5433 --config=/pg14/pgbackrest/etc/pgbackrest.conf repo-get /pg14/pgbackrest/lib
pgbackrest_exporter 监控备份状态

一般我们可以通过 pgbackrest info 去查看备份状态,但是这样并不直观
所以可以借助 exporter 获取 pgbackrest 的状态,从而及时监控到备份信息
安装 pgbackrest_exporter
pgbackrest_exporter 支持二进制运行,同时也支持 docker 运行,此处用二进制服务运行的方式
下载对应服务器版本的二进制包

wget https://ghfast.top/https://github.com/woblerr/pgbackrest_exporter/releases/download/v0.21.0/pgbackrest_exporter-0.21.0-linux-x86_64.tar.gz
tar -zxvf pgbackrest_exporter-0.21.0-linux-x86_64.tar.gz
mv pgbackrest_exporter-0.21.0-linux-x86_64/pgbackrest_exporter /usr/local/bin/pgbackrest_exporter
chown postgres:postgres /usr/local/bin/pgbackrest_exporter

创建 pgbackrest_exporter.service

touch /usr/lib/systemd/system/pgbackrest-exporter.service 
vim /usr/lib/systemd/system/pgbackrest-exporter.service
[Unit]
Description=pgbackrest-exporter Service
After=network.target
[Service]
Type=simple
User=postgres
ExecStart=/usr/local/bin/pgbackrest_exporter --backrest.config=/etc/pgbackrest.conf
Restart=on-failure
RestartSec=10
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Alias=pgbackrest-exporter.service                                 

启动 pgbackrest_exporter.service

systemctl daemon-reload 
systemctl start pgbackrest-exporter.service
systemctl enable pgbackrest-exporter.service

通过 grafana 展示页面
grafana id:17709

ON_Backs 通知 脚本

#!/usr/bin/env python3 """
Patroni钉钉通知脚本 - 增强诊断和错误修复版
主要解决:日志显示发送成功但实际未收到消息的问题
作者:资深SRE/数据库高可用架构师
"""
import os import string import subprocess import sys import json import logging import requests import socket import traceback from datetime import datetime import time # 新增时间模块用于重试 import patroni # 钉钉Webhook配置 - 添加详细的验证逻辑 DINGTALK_WEBHOOK = 'https://oapi.dingtalk.com/robot/send?access_token=********' if not DINGTALK_WEBHOOK: print("错误:钉钉Webhook环境变量未设置,请配置 DINGTALK_WEBHOOK_URL", file=sys.stderr) sys.exit(1) # 高级日志配置 - 确保诊断信息完整 logger = logging.getLogger('patroni-dingtalk') logger.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s') # 文件日志 file_handler = logging.FileHandler("/var/log/patroni/dingtalk_diagnostic.log") file_handler.setFormatter(formatter) logger.addHandler(file_handler) # 控制台日志 stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(formatter) logger.addHandler(stdout_handler) def verify_dingtalk_network(): """验证网络连通性到钉钉服务器""" target_host = "oapi.dingtalk.com" target_port = 443 try: # 创建socket连接检查 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(3) result = sock.connect_ex((target_host, target_port)) sock.close() if result == 0: logger.info(f"网络连通性检查: 可访问 {target_host}:{target_port}") return True else: logger.error(f"无法连接钉钉服务器 {target_host}:{target_port}, 错误代码: {result}") return False except socket.gaierror: logger.error("DNS解析故障 - 无法解析钉钉服务器地址") return False except Exception as e: logger.exception(f"网络检查异常: {str(e)}") return False def send_with_retry(message_data): """带重试机制的消息发送函数 - 解决偶发网络问题""" headers = {'Content-Type': 'application/json'} max_retries = 3 retry_delay = 2 # seconds for attempt in range(max_retries): try: # 详细记录实际发送的内容 logger.debug( f"发送请求 (尝试 #{attempt + 1}):\nURL: {DINGTALK_WEBHOOK}\nBody: {json.dumps(message_data, indent=2)}") response = requests.post( DINGTALK_WEBHOOK, json=message_data, headers=headers, timeout=(3, 5) # 连接超时3秒,读取超时5秒 ) # 详细记录响应信息 logger.debug(f"钉钉响应状态码: {response.status_code}") logger.debug(f"钉钉响应体: {response.text[:500]}...") # 只记录前500字符 # 钉钉成功响应格式: {"errcode":0,"errmsg":"ok"} if response.status_code == 200: json_response = response.json() if json_response.get('errcode') == 0: logger.info(f"钉钉API确认消息已发送! (尝试#{attempt + 1})") return True else: # 钉钉API业务错误 logger.error(f"钉钉API错误: [{json_response.get('errcode')}] {json_response.get('errmsg')}") # 特殊处理常见错误码 if json_response.get('errcode') == 130101: logger.error("常见原因: 钉钉机器人关键词不匹配 - 检查消息中是否包含设置的keyword") elif json_response.get('errcode') in [310000, 310001]: logger.error("常见原因: 被钉钉限流 - 请降低通知频率或添加特殊关键词") else: logger.warning(f"HTTP错误码: {response.status_code}") except requests.exceptions.RequestException as e: logger.exception(f"网络请求异常 (尝试 #{attempt + 1}): {str(e)}") except json.JSONDecodeError: logger.error(f"响应JSON解析失败: {response.text[:200]}") # 如果不是最后一次尝试则延时重试 if attempt < max_retries - 1: logger.info(f"{retry_delay}秒后将重试...") time.sleep(retry_delay) return False def parse_event_args(): """解析Patroni回调参数-增强容错""" if len(sys.argv) < 4: logger.error(f"错误: 参数不足! 期望至少4个参数,实际收到 {len(sys.argv)}") logger.info(f"完整参数列表: {sys.argv}") return None try: # 解析基础参数 ([0]=脚本路径, [1]=事件类型, [2]=角色, [3]=集群名) PATRONI_LEADER=subprocess.run("/usr/local/bin/patronictl -c /etc/patroni/patroni-5433.yaml dsn -r leader", shell=True, capture_output=True, text=True).stdout.strip() PATRONI_MEBERS=json.loads(subprocess.run("/usr/local/bin/patronictl -c /etc/patroni/patroni-5433.yaml list -f json", shell=True, capture_output=True, text=True).stdout.strip()) PATRONI_MEBERS_HOST=[] for i in PATRONI_MEBERS: PATRONI_MEBERS_HOST.append(i.get('Host')) PATRONI_MEBERS_HOST=str(PATRONI_MEBERS_HOST) len_members=len(PATRONI_MEBERS) leader_num=0 replica_num=0 for i in PATRONI_MEBERS: if i.get('Role')=="Leader": i.get('State')=="running" leader_num+=1 elif i.get('Role')=="Replica": i.get('State')=="streaming" replica_num+=1 else: continue if len_members>=2 and leader_num==1 and replica_num==len_members: PATRONI_HA_STATE='green' elif leader_num==1 and replica_num>=1 and replica_num < len_members: PATRONI_HA_STATE='yellow' else: PATRONI_HA_STATE='red' event_info = { 'script_path': sys.argv[0], 'event_type': sys.argv[1], 'node_role': sys.argv[2], 'cluster_name': sys.argv[3], 'leader': PATRONI_LEADER, 'mebers': PATRONI_MEBERS_HOST, 'len_members': len_members, 'hostname': socket.gethostname(), 'ipaddress': socket.gethostbyname(socket.gethostname()), 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 'ha_state': PATRONI_HA_STATE, 'old_role': os.getenv('PATRONI_OLD_ROLE', None) # 角色变更时存在 } logger.debug(f"解析事件成功: {json.dumps(event_info, indent=2)}") return event_info except Exception as e: logger.exception(f"参数解析错误: {str(e)}") return None def build_safe_message(event): """构建安全的消息格式 - 兼容钉钉要求""" if not event: logger.error("无法构建消息: 事件数据为空") return None # 确保包含关键词 (避免钉钉关键词检查失败) # 根据事件类型设置不同的标题和颜色 if event['event_type'] in ['stop', 'failover']: title= f"**<font color={'#FF0000'}>**🔴 Patroni故障事件**</font>**" elif event['event_type'] in ['start', 'promote']: title = f"**<font color={'#008000'}>**🟢 Patroni恢复事件**</font>**" elif event['event_type'] in ['reload', 'restart']: title = f"**<font color={'#FFA500'}>**🟡 Patroni维护事件**</font>**" else: title = f"**<font color={'#0000FF'}>**🔔 Patroni状态变更**</font>**" # 使用最简单的markdown格式确保兼容性 message_content = f"""

### {title}

- **事件类型**: {event['event_type']}

- **集群名称**: {event['cluster_name']}

- **集群当前Leader**: {event['leader']}

- **集群当前成员**: {event['mebers']}

- **集群成员数量**: {len(event['mebers'].split(','))}

- **集群状态**: **<font color={'#008000' if event['ha_state'] == 'green' else '#FFFF00' if event['ha_state'] == 'yellow' else '#FF0000'}>{event['ha_state']}</font>**

- **当前节点角色**: {event['node_role']}

- **当前主机名称**: {event['hostname']}

- **当前主机IP**: {event['ipaddress']}

- **集群状态**: {event['ha_state']}

- **发生时间**: {event['timestamp']}

"""
# 如果是角色变更事件,添加额外信息 if event.get('old_role'): message_content += f"- **变更前角色**: {event['old_role']}\n" # 必须包含关键字"Patroni"两次以上避免误过滤 message_content += "\n> **Patroni数据库高可用系统**" return { "msgtype": "markdown", "markdown": { "title": title, "text": message_content }, "at": { "isAtAll": False # 不@所有人 } } if __name__ == "__main__": logger.info("=" * 60) logger.info("🚦 Patroni钉钉通知脚本启动 | 诊断模式开启") logger.info(f"收到参数: {sys.argv}") logger.info(f"环境变量 WEBHOOK={DINGTALK_WEBHOOK[:20]}...") # 部分展示避免泄露 # 步骤1: 网络连通性验证 if not verify_dingtalk_network(): logger.critical("网络诊断失败! 消息无法发送 - 请检查网络连接或防火墙设置") sys.exit(10) # 步骤2: 解析事件 event_data = parse_event_args() if not event_data: logger.error("无法解析事件数据,消息发送中止") sys.exit(20) # 步骤3: 构建消息(安全格式) message_payload = build_safe_message(event_data) if not message_payload: logger.error("消息体构建失败") sys.exit(30) # 步骤4: 发送消息(带重试) logger.info("尝试发送消息到钉钉...") success = send_with_retry(message_payload) if success: logger.info("✅ 消息发送确认成功") else: logger.error("❌ 消息发送失败 - 请查看上述诊断信息") logger.info("=" * 60) sys.exit(0 if success else 40) # 非0退出码便于外部监控

📌 转载信息
原作者:
xxdtb
转载时间:
2026/1/19 17:45:53

各中转站对 RPM 有限制,同时禁止分发,本教程只给出自用模式

看到群里不少佬友想在 newapi 中对接中转站,恰好我在用 RightCode,所以以 rightcode 为例,写(水)一篇教程吧。

前者要求

  • git (用于克隆仓库)
  • windows docker desktop 或者 linux docker (建议有足够的内存 + 硬盘存储)
  • 配置好 docker compose

windows docker desktop 建议选择 wsl2 作为 backend

安装部署

克隆仓库 & 启动

先克隆仓库

git clone https://github.com/QuantumNous/new-api.git

修改 docker-compose.yml

本教程采用 postgresql

```diff
version: '3.4' # For compatibility with older Docker versions

services:
  new-api:
    image: calciumion/new-api:latest
    container_name: new-api
    restart: always
    command: --log-dir /app/logs
    ports:
-     - "3000:3000" +     - "3003:3000"
    volumes:
      - ./data:/data
      - ./logs:/app/logs
    environment:
-     - SQL_DSN=postgresql://root:123456@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production! +     - SQL_DSN=postgresql://root:idkpassword@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production!
#       - SQL_DSN=root:123456@tcp(mysql:3306)/new-api  # Point to the mysql service, uncomment if using MySQL
      - REDIS_CONN_STRING=redis://redis
      - TZ=Asia/Shanghai
      - ERROR_LOG_ENABLED=true # 是否启用错误日志记录 (Whether to enable error log recording)
      - BATCH_UPDATE_ENABLED=true  # 是否启用批量更新 (Whether to enable batch update)

    depends_on:
      - redis
      - postgres
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3

  redis:
    image: redis:latest
    container_name: redis
    restart: always

  postgres:
    image: postgres:15
    container_name: postgres
    restart: always
    environment:
      POSTGRES_USER: root
-     POSTGRES_PASSWORD: 123456  # ⚠️ IMPORTANT: Change this password in production! +     POSTGRES_PASSWORD: idkpassword  # ⚠️ IMPORTANT: Change this password in production!
      POSTGRES_DB: new-api
    volumes:
      - pg_data:/var/lib/postgresql/data

volumes:
  pg_data:
#  mysql_data:

编辑完保存,继续执行命令

docker compose up -d

等待 n 秒(取决于你的网速~)

出现以下字样,拉去镜像和启动成功

[+] Running 5/5
 ✔ Network new-api_default   Created                                                                                                                   0.1s
 ✔ Volume "new-api_pg_data"  Created                                                                                                                   0.0s
 ✔ Container redis           Started                                                                                                                   0.7s
 ✔ Container postgres        Started                                                                                                                   0.7s
 ✔ Container new-api         Started                                                                                                                   1.0s

NewAPI 配置

打开浏览器,输入 http://localhost:3003/ 后,会出现配置页

如果你的数据库检查没有错误,继续下一步,填写管理员账号和密码

下一步,选择使用模式

最后 初始化系统 即可

配置渠道

打开 控制台

依次点击 渠道管理 添加渠道,并填入 类型 / 名称 / 密钥

填入 API 地址为 https://www.right.codes/codex

因为 rightcode 支持了模型列表接口,点获取模型列表即可获取可用的模型

随后确定,并提交即可

模型管理

按照图中的内容,切换刀模型管理,依次点击 同步 → 下一步 → 确定

配合 CC Switch 使用

现在 控制台 -> 令牌管理 生成令牌,并填入刀 cc switch 中

最终的 config.toml

model_provider = "custom" model = "gpt-5.2" model_reasoning_effort = "xhigh" disable_response_storage = true [model_providers.custom] name = "custom" wire_api = "responses" requires_openai_auth = false base_url = "http://localhost:3003/v1" 

保存好以后,切换供应商,可以开始 coding 了


📌 转载信息
原作者:
unsafe
转载时间:
2026/1/15 18:30:28