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)
字段说明:
可以看到目前的大部分连接处于 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)
2026-01-19 22:04:15
先交代一下背景,在服务器上挂载了一个 S3 类似的对象存储,用于存放网站的图片。 现在需要把这个挂载目录下的所有文件权限改为 644。但是执行命令:
find . -maxdepth 1 -type f -print0 | xargs -0 sudo chmod 644
服务器的系统负载很高,而 CPU 和内存占用都很低。这个目录下有 8 万多个文件。
S3 是对象存储,不是真正的硬盘。当执行 chmod 时,挂载工具(如 s3fs 或 goofys)必须为每个文件发送一个网络请求(通常是 COPY 请求以更新元数据)。8 万个文件意味着 8 万次网络往返。
CPU 在等待网络响应,所以负载(Load Average)很高,但并没在运算。
最佳的解决方案是修改挂载参数。即,修改 /etc/fstab 中的挂载选项,添加 umask=022,并指定 uid 和 gid,这样所有文件在 Linux 看来都是 644。
由于,我还在 docker 中映射了这个目录,所以需要先停掉相关的 docker 容器,然后卸载目录,修改挂载参数,重新挂载,最后再启动 docker 容器。
docker compose stop xxx
umount /var/www/some_directory
例如,如果是 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 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 容器的运行?
重新挂载(Unmount & Remount)宿主机的目录,一定会影响正在运行的 Docker 容器。
如果不重启容器,容器内部通常会看到一个空目录或者遇到 Stale file handle(失效的文件句柄) 错误。
Docker 的容器卷挂载(Bind Mount)在容器启动时,是基于宿主机路径的 Inode(索引节点) 或当时的挂载点建立的连接。
断开连接: 当在宿主机执行 umount 时,宿主机内核会撤销该路径的挂载。虽然容器还在运行,但它原本引用的文件系统指针已经“断了”。
无法自动跟随: 当在宿主机执行新的 mount 后,虽然路径没变,但宿主机内核为它分配了新的挂载 ID。已经运行的容器不会自动同步这个新挂载,它会继续盯着那个已经变成空目录的原始挂载点。
在 S3 挂载的目录下执行 chmod,权限信息并不存在于一个类似 Linux Inode 的本地索引中,而是直接保存在 S3 对象的元数据(Metadata) 里。
如果使用的是最常用的 s3fs,权限信息被存储在 S3 对象的 HTTP 自定义元数据头中。
存储位置: AWS S3 对象的 Metadata 部分。
具体的 Key:
如果你登录 AWS 控制台,随便找一个文件点击“属性(Properties)”,在“元数据”一栏就能看到这些以 x-amz-meta- 开头的参数。
2026-01-15 22:19:46
参考前文,Ubuntu 安装 Oracle Instant Client, 并测试 Golang Gorm 读取 Oracle 数据库 搞定了 Oracle Instant Client 和 GORM 连接 Oracle 数据库的环境后,我以为马上就能开始写逻辑了。但是,测试了一下,发现了一个严重的问题。GORM 默认的实现方案,并不支持低版本 Oracle 的分页语法,准确的说是不支持 Oracle 11g 及更低版本。
在 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 框架来做。
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 于 2007 年发布,我也是服了,公司买这套 MES 系统,也是祖传架构了。。。
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 分页兼容解决方案
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 也类似。
因为 Ubuntu 上不能使用 rpm 包安装,所以直接下载 zip 包解压使用。(那些 rpm 包是针对 Oracle Linux 的)
https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html
版本选择: 下载与你的 Oracle 数据库版本一致或更高的客户端版本(例如,连接 19c 数据库建议使用 19c 或更高版本的客户端)。
确认当前 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 编译的程序。
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。这里不记录怎么配置了,我只用了第一种方式。
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:
否则会报错:
dpiStmt_execute: ORA-00942: table or view does not exist
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。
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-01-13 15:33:55
我发现上周末开始微信小程序审核变慢,而且奇慢无比,不知道是发什么疯,2026年前后是秒过,现在变成了等四天都没动静😂
上周六提交的审核,已经这周二了,一点消息也没有。应该不是违规,就是正常的小程序更新,走的正常审核流程,没有加急。
我以为只是我的个例,在某书上发了牢骚,发现很多小程序开发者都在抱怨这个问题,原来是普遍现象。。。
我猜测前几个月审核秒过,应该是用 AI 审核替代了人工审核,估计是伤到了外包审核公司的利益,估计被不可抗力又强制让外包公司去人工审核了。以后搞不好,就是跟软著一个德行了,硬拖你到审核周期的上限。而实际上根本没有排队,只是为了让你交钱加急而已。
哎,国内的这些种种。。。