MoreRSS

site iconSunZhongWei | 孙仲维 修改

博客名「大象笔记」,全干程序员一名,曾在金山,DNSPod,腾讯云,常驻烟台。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

SunZhongWei | 孙仲维 的 RSS 预览

MySQL 连接数:为什么你的服务器总“Too many connections”?连接池的配置和优化

2026-02-02 17:26:29

查看 MySQL 的当前连接数是排查性能瓶颈和配置连接池时的基本功。 所以记录一下可能用到的查询命令。以下是基于 MySQL 8.0 的测试:

当前有多少个客户端连着数据库

当前打开的连接数:

> SHOW STATUS LIKE 'Threads_connected';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 17    |
+-------------------+-------+
1 row in set (0.01 sec)

查看详细的连接列表

> SHOW FULL PROCESSLIST;
+-------+-----------------+-----------------+----------------+---------+----------+------------------------+-----------------------+
| Id    | User            | Host            | db             | Command | Time     | State                  | Info                  |
+-------+-----------------+-----------------+----------------+---------+----------+------------------------+-----------------------+
|     5 | event_scheduler | localhost       | NULL           | Daemon  | 98589253 | Waiting on empty queue | NULL                  |
| 20740 | user1           | localhost:54994 | database1      | Sleep   |      470 |                        | NULL                  |
| 20741 | user1           | localhost:55450 | database1      | Sleep   |      470 |                        | NULL                  |
| 20918 | user1           | localhost:49080 | database3      | Sleep   |      470 |                        | NULL                  |
| 21858 | user1           | localhost:47582 | database2      | Sleep   |      470 |                        | NULL                  |
| 21859 | user1           | localhost:47734 | database2      | Sleep   |      470 |                        | NULL                  |
| 24081 | user1           | localhost       | db5            | Query   |        0 | init                   | SHOW FULL PROCESSLIST |
+-------+-----------------+-----------------+----------------+---------+----------+------------------------+-----------------------+
18 rows in set (0.00 sec)

字段说明:

  • Command: 连接状态(如 Sleep 表示空闲,Query 表示正在查)
  • Time: 该状态持续的时间(秒)。

可以看到目前的大部分连接处于 Sleep 状态,说明这些连接是空闲的,没有在执行查询。

而且每个独立的数据库都有两个 sleep 状态的连接,说明我的 golang gorm 默认配置是 MaxIdleConns 为 2。 即使没有查询任务,连接池也会保持 2 个空闲连接。但是对于高并发的场景,2 个空闲连接可能不够用。 原因是在高并发结束后,多出来的连接会被立即关闭,等下次请求来时又要重新握手。

当前正在执行查询的连接数

通常远小于 connected,如果这个值很高,说明数据库压力极大

> SHOW STATUS LIKE 'Threads_running';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Threads_running | 2     |
+-----------------+-------+
1 row in set (0.00 sec)

最大连接数限制

> SHOW VARIABLES LIKE 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 151   |
+-----------------+-------+
1 row in set (0.00 sec)

默认是 151,可以根据需要调整这个值,避免连接数过多导致拒绝服务。

同时也要对应的限制 golang gorm 的最大连接数,当流量激增时,程序会尝试开启成千上万个连接,直接超出数据库限制,导致报错 Too many connections。

历史最高连接数(峰值)

这个值记录了自 MySQL 启动以来的历史最大并发连接数。如果这个值远低于你的连接池总和,说明你的连接池设置得太大了,浪费资源。

> SHOW STATUS LIKE 'Max_used_connections';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 18    |
+----------------------+-------+
1 row in set (0.00 sec)

一个 mysql 连接会占用多少内存

  • 空闲 (Idle)连接状态:2MB ~ 5MB,仅包含线程栈和基础缓冲区。
  • 普通查询连接状态:5MB ~ 10MB,包含了一些基础的读取缓冲区。
  • 重度查询连接状态(排序/大表关联):32MB ~ 128MB+,如果 SQL 写得烂,且 sort_buffer 设置得大,内存会迅速飙升。

批量修改 AWS S3 挂载目录下文件权限

2026-01-19 22:04:15

先交代一下背景,在服务器上挂载了一个 S3 类似的对象存储,用于存放网站的图片。 现在需要把这个挂载目录下的所有文件权限改为 644。但是执行命令:

find . -maxdepth 1 -type f -print0 | xargs -0 sudo chmod 644

服务器的系统负载很高,而 CPU 和内存占用都很低。这个目录下有 8 万多个文件。

为何系统负载飙高

S3 是对象存储,不是真正的硬盘。当执行 chmod 时,挂载工具(如 s3fsgoofys)必须为每个文件发送一个网络请求(通常是 COPY 请求以更新元数据)。8 万个文件意味着 8 万次网络往返。 CPU 在等待网络响应,所以负载(Load Average)很高,但并没在运算。

解决方案

最佳的解决方案是修改挂载参数。即,修改 /etc/fstab 中的挂载选项,添加 umask=022,并指定 uid 和 gid,这样所有文件在 Linux 看来都是 644。

由于,我还在 docker 中映射了这个目录,所以需要先停掉相关的 docker 容器,然后卸载目录,修改挂载参数,重新挂载,最后再启动 docker 容器。

停掉相关 Docker

docker compose stop xxx

卸载挂载的目录

umount /var/www/some_directory

确认服务器用户的 uid gid

例如,如果是 www 用户:

> id www
uid=1000(www) gid=1000(www) groups=1000(www),27(sudo),100(users)

修改挂载参数

由于使用的是类似 S3 存储的 Linode Object Storage,所以挂载参数如下:

xxx.sunzhongwei.com /var/www/some_directory fuse.s3fs _netdev,allow_other,umask=022,uid=1000,gid=1000,use_path_request_style,nonempty,url=https://us-east-1.linodeobjects.com/ 0 0

umask 的全称是 User File-Creation Mask(用户文件创建掩码)。它的作用是:规定“不允许”出现哪些权限。

在 Linux 中,权限是通过从“满权限”中减去 umask 的值来计算的。

执行重新挂载

mount -a

参考:s3fs-fuse 将 Linode Object Storage 挂载到 Ubuntu Server 本地文件系统

启动相关 Docker

docker compose start xxx

确认

> ls -lah /var/www/some_directory
total 543K
drwxr-xr-x 1 www www    0 Jan 16 06:08  .
drwxr-xr-x 1 www www    0 Dec 13  2024  ..
-rwxr-xr-x 1 www www 330K Jan 16 06:11  a.jpg
-rwxr-xr-x 1 www www 214K Jan 17 03:04  b.jpg

到此搞定。但是,我还是有一些其他疑问的,所以继续下面的整理。

新建文件的权限

设置了 umask=022 后,如果是 root 用户在这个 s3 目录下新建一个文件,文件的所有人是否会改变。

umask 只管“权限数字”,不管“所有人(Owner)”。 但是,在 S3 挂载(FUSE)的环境下,新建文件的所有人到底是谁,取决于你挂载时的参数设置,而不是 umask。

当挂载时指定了固定的 uid 和 gid,即在挂载命令或 /etc/fstab 中设置了 -o uid=1000,gid=1000。

无论你用 root 还是普通用户新建文件,在这个目录下看到的文件的所有人永远是 UID 1000 的用户。

原因: FUSE 驱动会“劫持”所有权信息。即使 root 写入了对象,挂载工具也会在显示时把文件强行显示为 UID 1000。

修改挂载参数对 Docker 容器的影响

修改挂载参数后重新挂载, 如果这个目录也挂载到了某个 docker 容器中,重新挂载是否会影响 docker 容器的运行?

重新挂载(Unmount & Remount)宿主机的目录,一定会影响正在运行的 Docker 容器。

如果不重启容器,容器内部通常会看到一个空目录或者遇到 Stale file handle(失效的文件句柄) 错误。

Docker 的容器卷挂载(Bind Mount)在容器启动时,是基于宿主机路径的 Inode(索引节点) 或当时的挂载点建立的连接。

断开连接: 当在宿主机执行 umount 时,宿主机内核会撤销该路径的挂载。虽然容器还在运行,但它原本引用的文件系统指针已经“断了”。

无法自动跟随: 当在宿主机执行新的 mount 后,虽然路径没变,但宿主机内核为它分配了新的挂载 ID。已经运行的容器不会自动同步这个新挂载,它会继续盯着那个已经变成空目录的原始挂载点。

对于 s3 的文件,在服务器本地执行 chmod 后,权限信息是保存在哪里?

在 S3 挂载的目录下执行 chmod,权限信息并不存在于一个类似 Linux Inode 的本地索引中,而是直接保存在 S3 对象的元数据(Metadata) 里。

如果使用的是最常用的 s3fs,权限信息被存储在 S3 对象的 HTTP 自定义元数据头中。

存储位置: AWS S3 对象的 Metadata 部分。

具体的 Key:

  • x-amz-meta-mode: 存储权限位(如 33188 代表 644)。
  • x-amz-meta-uid: 存储所有者的 UID。
  • x-amz-meta-gid: 存储组的 GID。

如果你登录 AWS 控制台,随便找一个文件点击“属性(Properties)”,在“元数据”一栏就能看到这些以 x-amz-meta- 开头的参数。

Oracle 11g 的 Golang GORM 分页兼容解决方案

2026-01-15 22:19:46

参考前文,Ubuntu 安装 Oracle Instant Client, 并测试 Golang Gorm 读取 Oracle 数据库 搞定了 Oracle Instant Client 和 GORM 连接 Oracle 数据库的环境后,我以为马上就能开始写逻辑了。但是,测试了一下,发现了一个严重的问题。GORM 默认的实现方案,并不支持低版本 Oracle 的分页语法,准确的说是不支持 Oracle 11g 及更低版本。

FETCH NEXT xx ROWS ONLY

在 admin 管理后台,实现分页查询是非常常见的需求。但是,如果使用 GORM 的分页语法:

db.Select("*").
	Limit(limit).
	Offset((page - 1) * limit).
	Find(&items)

会报错:

dpiStmt_execute: ORA-00933: SQL command not properly ended
[1.344ms] [rows:0] SELECT * FROM "xxx"."some_table" ORDER BY id desc FETCH NEXT 20 ROWS ONLY

FETCH NEXT 20 ROWS ONLY 是 Oracle 12c 及更高版本才引入的语法。Oracle 11g,无法识别这段代码,从而报出 ORA-00933。

我尝试了让 AI 写一个分页查询的 SQL,我发现太麻烦了,而且可读性也很差。我感觉还是得用 ORM 框架来做。

纯 Golang 实现的 godoes/gorm-oracle

https://github.com/godoes/gorm-oracle

这个库文档里明确说明了支持 Oracle 11g。可以看到配置里有个专门的配置:

// RowNumberAliasForOracle11 is the alias for ROW_NUMBER() in Oracle 11g,
// defaulting to ROW_NUM
RowNumberAliasForOracle11: "ROW_NUM",

看了一下 github 上的代码实现:

https://github.com/godoes/gorm-oracle/blob/main/oracle.go#L290

里面的 RewriteLimit11 方法,重写了 GORM 的分页语法,改成了 Oracle 11g 支持的分页语法。

// RewriteLimit11 rewrite the LIMIT clause in the query to accommodate pagination requirements for Oracle 11g and lower database versions
//
// # Limit and Offset
//
//	SELECT * FROM (SELECT T.*, ROW_NUMBER() OVER (ORDER BY column) AS ROW_NUM FROM table_name T)
//	WHERE ROW_NUM BETWEEN offset+1 AND offset+limit
//
// # Only Limit
//
//	SELECT * FROM table_name WHERE ROWNUM <= limit ORDER BY column
//
// # Only Offset
//
//	SELECT * FROM table_name WHERE ROWNUM > offset ORDER BY column
func (d Dialector) RewriteLimit11(c clause.Clause, builder clause.Builder) {
	limit, ok := c.Expression.(clause.Limit)
	if !ok {
		return
	}
	offsetRows := limit.Offset
	hasOffset := offsetRows > 0
	limitRows, hasLimit := d.getLimitRows(limit)
	if !hasOffset && !hasLimit {
		return
	}

	var stmt *gorm.Statement
	if stmt, ok = builder.(*gorm.Statement); !ok {
		return
	}

	if hasLimit && hasOffset {
		// 使用 ROW_NUMBER() 和子查询实现分页查询
		if d.RowNumberAliasForOracle11 == "" {
			d.RowNumberAliasForOracle11 = "ROW_NUM"
		}
		subQuerySQL := fmt.Sprintf(
			"SELECT * FROM (SELECT T.*, ROW_NUMBER() OVER (ORDER BY %s) AS %s FROM (%s) T) WHERE %s BETWEEN %d AND %d",
			d.getOrderByColumns(stmt),
			d.RowNumberAliasForOracle11,
			strings.TrimSpace(stmt.SQL.String()),
			d.RowNumberAliasForOracle11,
			offsetRows+1,
			offsetRows+limitRows,
		)
		stmt.SQL.Reset()
		stmt.SQL.WriteString(subQuerySQL)
	} else if hasLimit {
		// 只有 Limit 的情况
		d.rewriteRownumStmt(stmt, builder, " <= ", limitRows)
	} else {
		// 只有 Offset 的情况
		d.rewriteRownumStmt(stmt, builder, " > ", offsetRows)
	}
}

测试了一下,果然可以正常分页查询了!

而 gorm 默认的 Oracle 驱动,是不支持 Oracle 11g 的分页语法的:

github.com/oracle-samples/gorm-oracle

从这个山寨的名字就感觉不太靠谱。

大小写问题

在 Oracle 中,当你给标识符(表名、列名、Schema 名)加上双引号时,Oracle 会变得严格区分大小写。因为 GORM 默认会给表名和列名加上双引号,所以你会遇到下面的问题:

dpiStmt_execute: ORA-00942: table or view does not exist

[14.539ms] [rows:0] SELECT count(*) FROM "some_schema"."some_table"

解决方法就是,把表名和 Schema 名都改成大写。

Oracle 11g 是哪年发布的?

Oracle 11g 于 2007 年发布,我也是服了,公司买这套 MES 系统,也是祖传架构了。。。

Ubuntu 安装 Oracle Instant Client, 并测试 Golang Gorm 读取 Oracle 数据库

2026-01-15 11:13:16

准备用 Golang 二次开发一个 MES 系统,需要连接 Oracle 数据库读取数据。 Gorm 官方文档有 Oracle 数据库的连接说明:

It is built on top of the Go Driver for Oracle (Godror)

https://gorm.io/zh_CN/docs/connecting_to_the_database.html#Oracle-Database

而 Godror 则依赖 ODPI-C 和 Oracle Instant Client, 我发现在 Ubuntu 上安装 Oracle Instant Client 没啥参考文档 (官方文档基本都是基于 Oracle Linux 和 Red Hat 的),所以把摸索的过程记录下来,造福宇宙。

⚠️:注意,Gorm 这个方案,并不支持 Oracle 11g 分页处理,所以如果想使用这个方案,请确保 oracle 版本 > 12。如果是 11g 版本,请参考我另一篇笔记: Oracle 11g 的 Golang GORM 分页兼容解决方案

为何要安装 Oracle Instant Client

To use ODPI-C with Godror, you’ll need to install the Oracle Instant Client on your system.

即,要使用 Gorm 连接 Oracle 数据库,必须安装 Oracle Instant Client。

ODPI-C 是 Oracle Database Programming Interface for C 的缩写,是一个 C 语言的库,简化了 C/C++ 应用程序访问 Oracle 数据库的过程。它是 Oracle Call Interface (OCI) 的一个封装,使得应用程序和语言接口更容易开发。

Oracle Instant Client 支持 Windows、Linux 和 macOS。

这里以 Ubuntu 20.04 为例,介绍安装步骤。更高版本的 Ubuntu 也类似。

下载 oracle instant client ZIP 包

因为 Ubuntu 上不能使用 rpm 包安装,所以直接下载 zip 包解压使用。(那些 rpm 包是针对 Oracle Linux 的)

https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html

版本选择: 下载与你的 Oracle 数据库版本一致或更高的客户端版本(例如,连接 19c 数据库建议使用 19c 或更高版本的客户端)。

glibc 版本确认

确认当前 Ubuntu 系统的 glibc 版本。

$ ldd --version
ldd (Ubuntu GLIBC 2.31-0ubuntu9.2) 2.31
Copyright (C) 2020 Free Software Foundation, Inc.

我选择了 19.29 版本的 instant client,因为它支持的最低 glibc 版本是 2.17,符合要求。 新版本 glibc 可以运行为旧版本 glibc 编译的程序。

解压 instant client

sudo mkdir -p /opt/oracle
cd /opt/oracle
sudo unzip instantclient-basic-linux.x64-19.29.0.0.0dbru.zip

确认解压后的目录名,例如 instantclient_19_29

$ ls instantclient_19_29
BASIC_LICENSE    libccme_base.so           libclntsh.so.10.1  libclntshcore.so.19.1  libocci.so       libocci.so.19.1        liboramysql19.so  uidrvci
BASIC_README     libccme_base_non_fips.so  libclntsh.so.11.1  libcryptocme.so        libocci.so.10.1  libocci_gcc53.so       libtfojdbc1.so    xstreams.jar
adrci            libccme_ecc.so            libclntsh.so.12.1  libipc1.so             libocci.so.11.1  libocci_gcc53.so.19.1  network
genezi           libccme_ecc_non_fips.so   libclntsh.so.18.1  libmql1.so             libocci.so.12.1  libociei.so            ojdbc8.jar
libccme_asym.so  libclntsh.so              libclntsh.so.19.1  libnnz19.so            libocci.so.18.1  libocijdbc19.so        ucp.jar

/opt 目录是 Linux 系统中放置第三方软件的常用目录,opt 是 optional 的缩写,表示可选的意思。 删除时也方便,直接删除 /opt/oracle 目录即可。

安装系统依赖

Oracle Instant Client 需要 libaio 库才能运行。根据你的 Ubuntu 版本运行:

sudo apt update
# Ubuntu 22.04 及以前版本
sudo apt install libaio1
# Ubuntu 24.04+ 版本 (可能需要 libaio1t64)
sudo apt install libaio1t64

如果是使用的 instant client 19 版本,还需要安装 libnsl

sudo apt install libnsl-dev

但是报错:

E: Unable to locate package libnsl-dev

我最后也没搞定 libnsl-dev 的安装,不过并没有影响 gorm 读取 oracle 数据库 😅

配置动态链接库

配置动态链接库,让系统可以找到 Oracle Instant Client 的库文件

sudo sh -c "echo /opt/oracle/instantclient_19_29 > /etc/ld.so.conf.d/oracle-instantclient.conf"
sudo ldconfig

注意:将路径替换为你实际的解压路径。

另外一种方式是设置环境变量 LD_LIBRARY_PATH。这里不记录怎么配置了,我只用了第一种方式。

golang 代码

import (
  "github.com/oracle-samples/gorm-oracle/oracle"
  "gorm.io/gorm"
)

dataSourceName := `user="test" password="test"
                   connectString="dbhost:1521/orclpdb1"`
db, err := gorm.Open(oracle.Open(dataSourceName), &gorm.Config{})

测试读取一个表

为了防止意外,我创建了一个测试用的只读权限的 Oracle 用户,参考 (Oracle 创建只读用户

var items []map[string]interface{}
db.Raw("SELECT * FROM another_user.some_table").Scan(&items)
log.Println("Items from Oracle some_table:", items)

哈哈,果然测试成功了。✌

需要注意的是,如果你连接的是用户 A,但表属于用户 B,你需要显式指定 Schema:

  • 错误做法: SELECT * FROM some_table(如果当前用户不是所有者)。
  • 正确做法: SELECT * FROM another_user.some_table(加上所有者前缀)。

否则会报错:

dpiStmt_execute: ORA-00942: table or view does not exist

CGO 问题

godror 里的说明:

Important: because this is a CGO enabled package, you are required to set the environment variable CGO_ENABLED=1 and have a gcc compile present within your path.

因为 CGO_ENABLED 默认是 1,所以一般不需要设置这个环境变量。

只是要注意,不要手动把 CGO_ENABLED 设为 0。

参考

  • https://odpi-c.readthedocs.io/en/latest/user_guide/installation.html#oracle-instant-client-zip-files

Oracle 创建只读用户

2026-01-14 16:07:45

创建只读用户

-- 创建用户
CREATE USER readonly_user IDENTIFIED BY "password";
-- 授予连接权限
GRANT CONNECT TO readonly_user;
-- 授予只读权限(可查询所有表)
GRANT SELECT ANY TABLE TO readonly_user;

判断用户是否创建成功

SELECT * FROM all_users WHERE username = 'READONLY_USER';

能查询到用户即创建成功。

微信小程序审核变慢,2026年后四天了还没消息

2026-01-13 15:33:55

我发现上周末开始微信小程序审核变慢,而且奇慢无比,不知道是发什么疯,2026年前后是秒过,现在变成了等四天都没动静😂

上周六提交的审核,已经这周二了,一点消息也没有。应该不是违规,就是正常的小程序更新,走的正常审核流程,没有加急。

我以为只是我的个例,在某书上发了牢骚,发现很多小程序开发者都在抱怨这个问题,原来是普遍现象。。。

我猜测前几个月审核秒过,应该是用 AI 审核替代了人工审核,估计是伤到了外包审核公司的利益,估计被不可抗力又强制让外包公司去人工审核了。以后搞不好,就是跟软著一个德行了,硬拖你到审核周期的上限。而实际上根本没有排队,只是为了让你交钱加急而已。

哎,国内的这些种种。。。