news 2026/10/19 11:24:44

MySQL Errcode 13排查:从权限到secure_file_priv的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL Errcode 13排查:从权限到secure_file_priv的完整指南

那天晚上我正在给一张订单流水表做离线对账,表里攒了四千多万行数据,直接SELECT ... INTO OUTFILE导出成 CSV 是最省事的方案。结果定时任务日志里出现了一段让我有点来火的报错:

ERROR 1 (HY000) at line 1: Can't create/write to file '/data/export/orders_20240115.csv' (Errcode: 13 - Permission denied)

我第一反应是登录账号权限不够,赶紧切到 root 再跑了一次,照样被挡回来。同一个目录,我在终端里用系统账号touch一个测试文件都好好的,偏偏 MySQL 写不进去。这个 Errcode: 13 在网络上已经被讨论过无数次,几乎所有答案都指向“改目录权限、改属主”,可真正遇到复杂环境时,你会发现事情远没有这么简单。

我花了几个小时才把事情彻底解决,中间还走过一条弯路。这篇文章就把完整的排查思路、涉及的关键配置和最终验证方法写下来,碰到类似问题的朋友可以直接照着走,不用再被“明明有权限却写不了”折腾到怀疑人生。

1. 被 Errcode: 13 拦下的一次数据导出:真实场景回顾

1.1 这个报错到底长什么样

MySQL 的 Errcode: 13 一般不会出现在客户端连接阶段,而是出现在服务端尝试写文件的时候。最常见的两个触发点是:

  • SELECT * FROM 表 INTO OUTFILE '文件路径'
  • LOAD DATA INFILE '文件路径' INTO TABLE 表

这两个操作都是服务端行为,文件路径写在哪个机器上,就由哪台机器上的 MySQL 进程去读写。这一点非常容易搞混,特别是你根本不知道连接的是远端实例,客户端 Shell 噼里啪啦敲完命令,实际写文件发生在另一台服务器上。所以排查 Errcode: 13 的第一条铁律,就是搞清楚:文件到底写到哪台机器、哪个目录。

报错信息里通常会带两个部分:

  • Errcode: 13:这是操作系统返回的错误码,在 Linux 上对应errno 13,也就是EACCES,含义是“权限不足”。
  • Permission denied:顶层的提示文案,MySQL 只是把操作系统的错误原样带了出来。

有时候错误日志里的文案还可能是:

[ERROR] [MY-010128] Can't create/write to file '/data/export/orders_20240115.csv' (Errcode: 13 - Permission denied)

反正关键就一条:MySQL 进程在写文件时,操作系统拒绝了它的写入请求。

1.2 为什么网络上常见的“改权限大法”经常失效

很多教程看完,第一步是让你chmod 777,第二步是让你chown mysql:mysql,第三步就让你重启 MySQL。表面上看很有道理,但如果你遇到的是下面这种环境,这些操作会全部失效:

  • 目录本身属主和权限都没问题,但挂载点被标记为只读。
  • 目录没问题,但 SELinux 上下文不对,操作系统仍然拒绝服务进程写入。
  • MySQL 服务端配置了secure_file_priv,把可写路径圈死在一个特定目录里,你写到哪里都碰壁。
  • 写入目标是已有文件,MySQL 出于安全机制不会覆盖任何已存在的文件,报错会被误以为是权限问题。

我那次遇到的问题,就是典型的“目录权限看起来没问题,实际好几层限制叠在一起”。如果网上随便找个答案就照着改,大概率是给系统留下了一个很不安全的 777 权限,问题却依旧没解决。

2. 拆开 Errcode: 13:卡在几个层面上的权限断点

2.1 错误码的真实身份:从 errno 到 MySQL

先说清楚 errno 13 是哪来的。在 Linux 源码里,EACCES的数值就是 13,它代表的含义是“操作被拒绝,因为你没有足够的权限”。这个错误和EPERM(号码 1)不一样:EPERM更多表示“这个操作不允许做”,比如你试图对一个只读文件执行删除;EACCES则表示“权限不够,物理上没法访问”。

MySQL 进程本身就是一个普通系统进程,它去写目录、写文件的时候,内核会基于三套规则判断:

  1. 文件系统权限:目录的属主、属组、权限位,以及进程的运行身份是否匹配;
  2. 访问控制扩展:包括 SELinux、AppArmor 这类强制访问控制机制,即便 DAC 权限放开了,它们仍然可以拒绝;
  3. 挂载属性:文件系统以ro方式挂载,或者指定了nosuid、nodev等特殊选项,也会影响写入。

这三层叠加在一起,就构成了 Errcode: 13 的完整“案发现场”。只看其中任何一层,都可能误判。

2.2 常见根因速查表

我把实际遇到过的、以及身边同事踩过的坑整理成下面这张表格,方便排查时对照:

表现常见根因关键检查项
目标目录权限非常宽松,仍然报错进程运行身份和目录属主不匹配,或父目录没有执行权限ps -ef查看 mysqld 进程用户
目录权限正确,重启后又报错systemd 重启后服务身份变化,或 mounted 目录在启动后才挂载systemctl status mysqld
文件能 touch,但 MySQL 写入报错SELinux 上下文不对,或设置了强制访问控制getenforce、ls -Z
提示路径是/tmp/下的临时文件MySQL tmpdir 指向的目录不可写,或 systemd 的私有 /tmp 隔离SHOW VARIABLES LIKE 'tmpdir'
导出路径不在 secure_file_priv 范围内MySQL 服务端限制了导入导出文件的可用目录SHOW VARIABLES LIKE 'secure_file_priv'
要写入的文件已经存在MySQL 不允许覆盖已有文件,避免数据损坏风险先ls -l 目标文件确认是否存在

这张表不会覆盖所有情况,但已经能帮你把 90% 的问题定位到大方向上。接下来就沿着这个方向,一步步做验证。

2.3 顺带提醒:和 Errcode 13 相似的两个报错

排查的时候不要被误导,和文件读写相关的错误码还有 28 和 1:

  • Errcode: 28表示磁盘空间不足(ENOSPC),有时候也会以 “No space left on device” 的形式出现。磁盘满了之后,MySQL 写任何文件都会失败,很多人会误以为是权限问题。
  • Errcode: 1表示不是权限不够,而是操作不被允许。比如目标文件是一个只读目录,或者路径中某一段被设置为不可删除。

我建议你在看报错时先记录完整错误码,再往权限方向靠。如果错误码是 28,费半天劲去查属主和 SELinux,方向就完全错了。

3. 完整排查链路:从进程身份一路查到 SELinux 与 secure_file_priv

3.1 第一步:先向 mysqld 进程确认真实写入身份

我首先做的一步,不是去看目录权限,而是找出 MySQL 进程到底是以哪个系统身份运行的。这一步是整个排查链条的地基,错在这里,后面的判断全都会偏。

执行:

ps -ef | grep mysqld
ps -ef | grep mysqld | grep -v grep

正常情况下会看到类似:

mysql 12345 1 0 09:30 ? 00:00:00 /usr/sbin/mysqld --daemonize

第一列mysql就是进程的属主。无论你客户端登录账号是 root 还是普通账号,写入文件的系统身份都由这个进程属主决定。也就是说,你 Linux 登录用户对目录有写权限没用,关键要看mysql用户对目标目录有没有写权限。

如果显示为 root,那就要留意了——很多从源码包编译安装的实例,如果不小心用 root 直接启动,后续排查逻辑会完全不一样。最好确认一下服务管理方式,是 systemd 托管、还是手动 nohup 启动、还是容器方式运行。

3.2 第二步:检查目标目录与路径每一级权限

确认了进程身份后,下一步检查目标文件所经过的所有路径层级。这里我推荐一个叫namei的工具,可以一次看完整绝对路径的每一级权限:

namei -l /data/export/orders_20240115.csv

输出会像这样:

f: /data/export/orders_20240115.csv dr-xr-xr-x root root / drwxr-xr-x root root data drwxr-xr-x root root export - - mysql mysql orders_20240115.csv

重点看两点:路径中的每一级目录都得有x执行权限,否则进程无法进入该目录;目标文件路径的最终父目录需要有w写权限,因为 MySQL 要在这里创建新文件。

我那次检查后发现,/data目录权限是drwxr-xr-x,属主是 root,export子目录权限是drwxrwxr-x,属主也是 root。按说 mysql 用户应该能写进去,但我再用一个更贴近真实的办法验证:

sudo -u mysql touch /data/export/.write_test

这条命令如果能成功,说明系统层面的 DAC 权限是放开的。结果我这个命令也成功了,问题还没有暴露。这就说明权限问题也许并不在这一层,而是被更高层级的 SELinux 拦住了。

3.3 第三步:看看 SELinux 和 AppArmor 有没有插手

如果你的环境启用了 SELinux,就算目录权限全部放开,写入照样可能失败。检查当前 SELinux 状态:

getenforce

如果输出Enforcing,那么 MySQL 能不能写入,还要看目标路径的 SELinux 标签是否为 mysql 服务器可写的类型。

查看目标目录的安全上下文:

ls -Z /data/export

正常 MySQL 数据目录的标签通常类似:

system_u:object_r:mysqld_db_t:s0

而/data/export如果显示的是:

system_u:object_r:default_t:s0

那 MySQL 的写请求大概率就被 SELinux 拦截了。这时候可以用ausearch查一下审计日志确认:

ausearch -m avc -ts recent | grep mysql

如果能看到denied { write }和comm="mysqld"的关键字,那基本就实锤了。

如果你用的是带 AppArmor 的 Linux 发行版,排查方式类似,重点看对应发行版下 mysql 服务配置文件里有没有对路径的限制,必要时需要把导出目录加进允许列表。

3.4 第四步:确认 secure_file_priv 的边界

权限和 SELinux 都没问题,但 MySQL 里依然报错,这时要查服务端变量secure_file_priv。它控制 MySQL 允许在哪些目录执行LOAD DATA INFILE和INTO OUTFILE。

登录 MySQL 执行:

SHOW VARIABLES LIKE 'secure_file_priv';

结果有三种可能:

值含义
NULL完全禁止导入导出文件操作,任何路径都不允许
空字符串不限制路径,可以写到任意目录
具体路径只允许写到该路径及其子目录下

如果输出是/var/lib/mysql-files/,而你刚才写的是/data/export/xxx.csv,那么即使操作系统层面权限完全正常,MySQL 也会拒绝。这里要注意一个微妙的区别:直接写错路径时,MySQL 报的错误通常是ERROR 1290,而 Errcode: 13 一般还是系统权限问题。但实际运维中,两者常常交叉出现:你把目录权限和 SELinux 都搞定了,再回头检查secure_file_priv把导出目录修正到白名单里,整个链路才算彻底通畅。

4. 修复与验证:四类根因对应的收口操作

4.1 修正目录属主与权限位的标准做法

如果你的确认结果是目录权限不匹配,最稳妥的做法不是chmod 777,而是把目标目录的属主改成 mysql 运行用户,并保持目录权限合理:

mkdir -p /data/export chown -R mysql:mysql /data/export chmod 750 /data/export

这里给出权限位是 750 而不是 777,意思是:mysql 用户对目录有完整读写执行权限,mysql 同组用户只读可执行,其他用户无法访问。这能避免把目录完全暴露给所有系统用户,安全性会好很多。

改完之后再用sudo -u mysql touch验证一下:

sudo -u mysql sh -c 'touch /data/export/.write_test && rm -f /data/export/.write_test'

如果这条命令能通过,操作系统层面已经允许 mysql 写入了。

4.2 调整 secure_file_priv 并正确重启实例

如果你的secure_file_priv是 NULL,或者白名单路径和你想要的路径不一致,就需要修改 MySQL 配置文件。

编辑 MySQL 配置文件,在[mysqld]段下面增加:

[mysqld] secure_file_priv = /data/export

然后重启 MySQL 服务:

systemctl restart mysqld

重启后在 MySQL 里再次确认:

SHOW VARIABLES LIKE 'secure_file_priv';

输出结果应该是/data/export/而不是 NULL。

值得说明的是,secure_file_priv是一个启动时读取的变量,不支持在会话里用SET GLOBAL动态修改,改配置文件重启是最直接的办法。还有一点要特别小心:如果系统上有其他业务依赖原白名单目录,比如定时备份脚本用的就是/var/lib/mysql-files/,你把白名单一刀切改成导出目录,备份任务就会开始报错。稳妥的做法是多建一个目录,并用分号?不行,这个参数只支持一个路径,不能同时写多个。所以最好规划单一目录,把导入导出文件都统一收纳进去。

4.3 SELinux 下的正确处理而不是直接关闭

遇到 SELinux 拦截,很多人的第一反应是干脆把 SELinux 禁了:

setenforce 0

这能临时解决问题,但系统重启后 SELinux 又会恢复为 Enforcing,而且整个系统安全防护被降级,在生产环境里是非常危险的操作。更合理的做法是给导出目录打上正确上下文。

如果只是临时测试,可以用:

chcon -R -t mysqld_db_t /data/export

chcon打印一个新的安全上下文。如果是正式环境,更推荐用semanage fcontext写入白名单以实现持久化,再执行restorecon恢复上下文:

semanage fcontext -a -t mysqld_db_t "/data/export(/.*)?" restorecon -Rv /data/export

这样即使系统做 relabel,导出目录也能保持正确标签。之后再用ls -Z确认:

ls -Zd /data/export

只要显示的不再是default_t,而变成了mysqld_db_t,MySQL 写入就有了通行证。

如果系统用的是 AppArmor,则要对应修改服务配置,把/data/export加进允许写入的路径列表,改完后执行 reload 让配置生效。

4.4 验证清单:能写入、能导全量、能清理

改动全部完成后,不要急着跑全量任务。我先按从易到难的顺序做了三轮验证:

第一轮,测试工具文件:

sudo -u mysql sh -c 'echo test > /data/export/.write_test && rm -f /data/export/.write_test'

第二轮,测试 MySQL 导出:

SELECT * FROM 表名 INTO OUTFILE '/data/export/tmp_verify.csv' FIELDS TERMINATED BY ',';

如果这条语句成功,说明 MySQL 服务端已经能够往目标目录写文件。这里特别提醒一个坑:目标文件不能预先存在。MySQL 出于安全考量,不会覆盖任何已存在的文件,如果tmp_verify.csv已经存在,即使权限完全正常,它也会拒绝写入。所以导出前先确认一下文件不存在,或者用一个全新的文件名。

第三轮,验证全量导出和内容完整性:

wc -l /data/export/orders_20240115.csv

对比数据库里的行数,确认数据没有缺失。

5. 几次踩坑后的体会:权限问题真正应该这样预防

5.1 为什么我坚决不建议“临时放开 777”

网络上有太多灾难性的建议,让你直接chmod -R 777某个目录,然后问题立刻消失。问题解决是解决了,代价是把整个目录的访问控制全部废掉。任何能登录这台机器的系统用户都可以读、写、删除这个目录里的文件,如果里面是客户数据导出文件,这就是一个潜在的泄露点。

正确的权限哲学是:只给 MySQL 进程所属用户最小必要权限。目录 750 就够了,不需要 777;属主设置为 mysql 就够了,不需要给 root;SELinux 保持 Enforcing,用正确标签适配就够了,不需要关闭。多花十分钟配置正确的权限,比留一个安全隐患在线上要划算得多。

5.2 给导出目录一个尽量“顺理成章”的位置

经过这几次折腾,我的习惯是:建好专门的数据交换目录,而不是随便扔在业务代码目录或者 /tmp 下面。比如:

mkdir -p /data/mysql_exports chown mysql:mysql /data/mysql_exports chmod 750 /data/mysql_exports

然后把 MySQL 配置里的secure_file_priv指向这个目录。这样后续所有导入导出都走同一路径,权限、SELinux 上下文、备份策略都围绕这一个目录来设计,不会再出现今天往这里写、明天往那里写,每个目录都要重新排查一遍权限的情况。

另外建议让这个目录和数据目录尽量保持相似的上下文属性,避免因为标签差异导致“这边能写、那边不能写”的诡异问题。

5.3 同类问题的举一反三

Errcode: 13 不只出现在INTO OUTFILE中。MySQL 在创建临时表、排序溢出、redo/undo 扩容时,也可能在tmpdir或数据目录下报同样的错误。遇到这类情况,排查思路完全一致:先确认进程身份,再检查目标路径每一级的 DAC 权限,然后核对 SELinux/AppArmor,最后看 MySQL 变量里有没有路径限制。

如果哪天出现了一个奇怪情况——既有INTO OUTFILE报错,又同时看到错误日志里带有/tmp/#sql...这样的临时文件,记得查一下 MySQL 的tmpdir变量,把它也加到排查范围里。权限问题本身不可怕,可怕的是对着错误码瞎猜,把系统越改越乱。沿着进程身份 → 目录权限 → SELinux → secure_file_priv这条链走下来,大多数 Errcode: 13 都能干干净净地收口。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 11:24:28

Windows内核I2C驱动开发:KMDF+SPB+ACPI教学闭环

简介:本资源是一套面向嵌入式与Windows内核开发初学者的实践型源码项目,聚焦I2C设备在Windows平台下的全栈通信实现,适用于高校电子/计算机专业学生、驱动开发入门者及对KMDF和SPB总线机制感兴趣的工程师。项目完整覆盖用户态WinRT API调用、…

作者头像 李华
网站建设 2026/10/19 11:24:10

OSPF网络类型全解析:从原理到排障,一篇掌握五种类型

搞网络这一行,只要绕不开OSPF,就绕不开网络类型这四个字。我前阵子帮人排查一个邻居起不来的问题,折腾到后半夜,根因就是接口上的OSPF网络类型两端设置不一致。OSPF网络类型决定了Hello报文用什么方式发出去、要不要选DR/BDR、邻居…

作者头像 李华
网站建设 2026/10/19 11:24:00

基于Spring Boot的车辆综合服务平台开发实战:从需求拆分到落地部署

大学毕业设计做了一圈管理系统,唯独车辆相关的项目特别多,但大多数质量堪忧。前阵子帮朋友梳理一个基于Spring Boot的车辆综合服务平台,从需求拆分到落地部署完整走了一遍,发现这类项目真正难的不是CRUD,而是业务边界、…

作者头像 李华
网站建设 2026/10/19 11:23:49

Java开发者转型AI:避开认知误区,发挥工程化优势跑通实战

Java开发者这两年应该都有一种微妙的感觉:身边越来越多人在讨论Python、PyTorch、模型训练,好像一夜之间“不会AI”就成了落伍信号。但真让一个写了五六年Java的人去转型AI,第一反应往往是发怵——“我数学早忘了”“Python语法都不熟”“神经…

作者头像 李华
网站建设 2026/10/19 11:21:09

统计学习方法第六章:逻辑斯蒂回归与最大熵模型推导全过程详解

李航老师的《统计学习方法》第二版,第六章“逻辑斯蒂回归与最大熵模型”,是很多人在学习时最先感觉到“难度跳变”的一章。前面几章你可能还在看感知机的几何直觉、朴素贝叶斯的独立性假设,到了这一章,模型突然变成了一堆概率公式…

作者头像 李华
网站建设 2026/10/19 11:19:53

diagram-design指南:从架构图到流程图的视觉传达设计

diagram-design这个词,我最早是在整理架构文档时意识到的。当时某项目上线后需要做技术交底,面对一张画得满满当当、却谁都看不懂的网络拓扑图,我才发现:会画图是一回事,把图设计得让读者一眼抓住重点,是另…

作者头像 李华