文档/ 数据库/ 连接字符串

连接字符串

sh0 不会替你把数据库接入应用。你需要读取连接信息,为应用创建数据库和用户,并自行设置 DATABASE_URL。本页列出每一步,命令均为实际执行结果。

Warning
不会自动注入任何变量。创建数据库服务器不会给你的应用添加 DATABASE_URL、REDIS_URL 或其他任何变量,无论应用属于哪个项目。期待 DATABASE_URL 的应用在你设置之前什么也拿不到。

查找连接信息

在控制台中打开 Databases,再打开服务器:Connection Info 显示 Internal Host、端口、Root Password 以及可复制的 Connection URL。

使用 CLI 时,sh0 db connection-info 需要服务器 ID(创建时输出,或通过 sh0 db list 查看):

Terminal
$ sh0 db create --name my-pg --engine postgres
Database server 'my-pg' created (id: e8afafa4-2ff1-4088-8276-fccb839734a8, engine: postgres 16)

$ sh0 db connection-info e8afafa4-2ff1-4088-8276-fccb839734a8
Connection Info:
  Host:     my-pg.internal
  Port:     32962
  Username: postgres
  Password: <password>
  Internal: postgresql://postgres:<password>@my-pg.internal:5432/
  External: postgresql://postgres:<password>@localhost:32962/

应用中请使用 Internal URL。External URL 指向服务器上绑定在 127.0.0.1 的端口,只能从服务器本机访问(见“从你的电脑连接”)。

每个应用一个数据库和一个用户

新建的服务器没有任何数据库,只有管理员账户。请为每个应用创建独立的数据库和用户,而不是使用管理员密码。控制台中依次点击 Create Database、Create User、Grant Access。使用 CLI:

Terminal
$ sh0 db create-database e8afafa4-2ff1-4088-8276-fccb839734a8 --name appdb
Database 'appdb' created (id: 503a447c-f9f5-44b8-87cf-22c0905f075c).

$ sh0 db create-user e8afafa4-2ff1-4088-8276-fccb839734a8 --username appuser --password '<password>'
User 'appuser' created (id: 86de6179-7e97-463a-93d8-dfd89b1da6e2).

$ sh0 db grant e8afafa4-2ff1-4088-8276-fccb839734a8 \
    --user 86de6179-7e97-463a-93d8-dfd89b1da6e2 \
    --database 503a447c-f9f5-44b8-87cf-22c0905f075c --privileges all
Access granted.
  • 密码规则 -- 至少包含一个大写字母和一个符号等;不符合的密码会被拒绝,并提示违反的规则。控制台的 Generate 按钮可生成合规密码;请立即保存,之后不会再显示。
  • 权限 -- all 或 readonly,其他值会被拒绝。

在应用上设置 DATABASE_URL

用内部主机名、引擎端口、用户和数据库拼出 URL,保存为应用的环境变量。变量在部署时生效,因此需要重新部署:

Terminal
$ sh0 env my-api set "DATABASE_URL=postgresql://appuser:<password>@my-pg.internal:5432/appdb"
[ok] Set DATABASE_URL for 'my-api'

$ sh0 deploy my-api
[*] Deploying my-api...
  Deployment ID: 64492c39-dad0-409f-ad6d-29b71f79bab3
  Status:        queued

各引擎的 URL 格式(把名称换成你自己的;内部端口是引擎默认端口,而不是 Port 下显示的端口):

  • PostgreSQL -- postgresql://appuser:<password>@my-pg.internal:5432/appdb
  • MySQL 和 MariaDB -- mysql://appuser:<password>@my-mysql.internal:3306/appdb
  • Redis -- redis://:<password>@my-redis.internal:6379
Note
同一项目的多个应用共用一个数据库?在项目上设置一次变量即可:项目变量会下发到项目内所有应用,应用上的同名变量优先。密码中的特殊字符(@、:、/、#…)在 URL 中必须进行百分号编码。定时任务不会收到这些变量 -- 见定时任务。

内部网络

应用和数据库服务器共享 sh0 的 Docker 网络。应用通过 <服务器名>.internal 加引擎端口访问服务器 -- 在应用容器中实测:到 my-pg.internal:5432 的 TCP 连接成功建立,appuser 使用上述 URL 登录 appdb。

Note
这些流量只在服务器的 Docker 网络内传输,不经过 TLS 加密。对于与应用在同一台机器上的数据库,这是常规做法;一旦开启外部访问,情况就不同了。

从你的电脑连接

要从工作站(pgAdmin、TablePlus、DBeaver、psql)访问数据库,有两种方式,优先使用第一种。

SSH 隧道(推荐)

Port 下显示的端口(此处为 32962)在服务器上绑定于 127.0.0.1。通过 SSH 转发即可:不会向互联网暴露任何端口,访问权限即你的 SSH 密钥权限。

Your machine
ssh -N -L 5432:127.0.0.1:32962 root@your-server
# in another terminal
psql "postgresql://appuser:<password>@127.0.0.1:5432/appdb"

外部访问

在服务器上点击 Enable External Access 会通过 sh0 的代理开放一个公网端口。已发布版本的代码强制执行以下规则:

  • 必须填写允许的 IP -- 单个地址或 CIDR 段;0.0.0.0/0 和 ::/0 会被拒绝。
  • 默认要求 TLS。
  • 有效期 -- 1 小时、6 小时、24 小时、7 天、30 天或永久。
  • 端口 -- 10000-10999 范围内第一个空闲端口;你的防火墙需要放行它。
Warning
本节依据代码描述,未在我们的演示服务器上实际操作:我们有意没有把数据库开放到互联网。优先使用 SSH 隧道;如果确实要开放,请使用尽量窄的 IP 列表并设置有效期。

连接池

每个数据库都有连接数上限(PostgreSQL 默认 100)。请调整连接池大小,使所有副本加起来低于该上限。

  • 使用框架自带的连接池 -- 大多数 ORM 和驱动都提供。
  • 在副本之间分配上限 -- 应用有 3 个副本、上限为 100 时,每个副本约 30 个连接,再减去其他应用的占用。
  • 设置空闲超时,让闲置连接释放名额。
Node.js pool configuration
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,              // Maximum connections in the pool
  idleTimeoutMillis: 30000,  // Close idle connections after 30s
  connectionTimeoutMillis: 5000, // Fail if connection takes > 5s
});

常见框架示例

下面的每个示例都读取你在应用上设置的 DATABASE_URL。

Ruby on Rails (config/database.yml)
production:
  url: <%= ENV['DATABASE_URL'] %>
Django (settings.py, dj-database-url)
import dj_database_url

DATABASES = {"default": dj_database_url.config(conn_max_age=600)}
Rust / SQLx
let pool = PgPoolOptions::new()
    .max_connections(20)
    .connect(&std::env::var("DATABASE_URL")?)
    .await?;
Go / pgx
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))