1. 这需求是怎么来的:先弄清楚禅道的上传限制卡在哪了
做运维或者当项目管理工具的管理员,迟早会撞上这个问题。团队用禅道管项目,跑了一段时间,同事突然反馈说附件传不上去了,一看提示才知道是文件太大。去禅道后台翻了半天,发现设置界面里根本没有“允许上传大小”这个选项。这时候才意识到,禅道的上传限制压根就不是它自己说了算,而是被底层的 PHP 配置和 Web 服务器配置双重卡住的。
我在实际排查过程中发现,绝大多数人第一次遇到这个问题时,都会钻进禅道的源码或者数据库配置表里找答案,其实方向完全错了。禅道本身对上传大小确实有一些控制,但那是基于 PHP 层接收到的请求参数做二次校验,真正决定能不能传上去的,核心在 PHP 的upload_max_filesize、post_max_size,以及 Nginx 或 Apache 的请求体大小限制这三层。这三层只要有一层没放开,你就算把禅道后台翻个底朝天也没用。
所以这篇文章想把整个链路讲清楚:为什么你改了 PHP 配置还是传不了大文件,Nginx 那层到底应该怎么配,以及改完之后有哪些细节容易忽略。按我给出的顺序走一遍,基本能解决 99% 的上传限制问题。
顺带说一句,这套方法不仅适用于禅道,其它跑在 Linux + PHP + Nginx(或 Apache)环境下的 Web 应用,比如部分开源网盘、表单系统、企业 Wiki,原理都一样。你把这个思路吃透了,以后碰到任何“改了配置但是上传限制没变”的怪问题,都能套用这套排查逻辑。
2. 上传限制的完整链路:PHP、Nginx、禅道各自管什么
2.1 三层限制模型:缺一层就前功尽弃
要改禅道的文档上传限制,首先得在心里建立一张“链路图”。一次文件上传请求从浏览器发出,到最终存到禅道的data/upload目录下,会依次经过三到四道关卡,每一道关卡都设置了自己的上限:
第一道是 Web 服务器层。如果你用的是 Nginx,它有一个client_max_body_size参数,默认值是 1m,也就是 1MB。这个参数限制的是整个 HTTP 请求体的大小,不仅仅是文件本身,还包括 POST 请求里的表单字段、JSON 数据等。这一层是最容易忽略的,因为它在 Nginx 配置里,很多人改完 PHP 就以为完事了,结果 Nginx 直接返回 413 Request Entity Too Large,一脸懵。
第二道是 PHP 层。PHP 有两个关键参数,一个是upload_max_filesize,单独限制单个上传文件的大小,PHP 默认值是 2M;另一个是post_max_size,限制整个 POST 请求体的总大小,默认值是 8M。这两个参数必须一起看,因为就算你把upload_max_filesize改成了 20M,但post_max_size没动,PHP 会认为整个 POST 数据超过了 8M,一样会拒绝接收。更坑的是,PHP 在这种情况下返回的错误信息往往不够明确,让你以为是文件格式问题或者禅道的问题,实际上纯粹是post_max_size没改够。
第三道是 PHP 脚本执行时间和内存限制。当一个文件从磁盘读入 PHP 内存再写入目标目录时,会占用脚本执行时间和内存。如果文件太大,PHP 的max_execution_time不够用,脚本会在处理过程中被强制终止,表现就是上传进度走完了,但最终提示超时失败。另外memory_limit如果设置得太低,大文件在上传过程中也可能触发内存耗尽错误。
第四道才是禅道自身的应用层校验。禅道在接收到上传请求后,会读取 PHP 的配置值来生成前端提示,并且在后端对文件大小、扩展名做二次校验。如果文件太大,哪怕 PHP 和 Nginx 都放行了,禅道自己的校验逻辑也会拒绝。但异步上传场景下,一部分远程文件或大分块文件会绕过一些校验,这里先不展开,后面在常见问题里再详细说。
2.2 为什么只改一处往往会翻车
网上很多教程只让你改 PHP 的upload_max_filesize,改完重启php-fpm就结束了,结果实测下来根本没生效。原因很简单:Nginx 那一层的 1MB 限制还死死挡在前面。你上传一个 5MB 的文件,Nginx 直接返回 413,PHP 层见都没见到这个请求,你改 PHP 有什么意义?
反过来,如果只改 Nginx 不放宽 PHP,请求能到 PHP 层了,但 PHP 的upload_max_filesize还是 2M,照样拒绝。所以正确的思路是从外到内,逐层放开,每一层设置的数值还要保持一定的递进关系,避免两层之间出现新的瓶颈。
我个人习惯的做法是:Nginx 的client_max_body_size比 PHP 的post_max_size稍大一点,PHP 的post_max_size又比upload_max_filesize大一点。原因在于,upload_max_filesize只管单个文件,但一个请求可能同时传多个文件,加上表单里的其它文本字段,整个 POST 体积会比单个文件大。如果post_max_size只是刚好等于upload_max_filesize,那么一次传两个大文件的时候就会失败。遵循“Nginx >= PHP POST >= PHP 单文件”的递进原则,能避免很多莫名其妙的边界问题。
还有一个衍生问题:PHP 的post_max_size如果设置得很大,意味着 PHP 允许接收大体积的 POST 请求,这会带来一定的安全风险,比如恶意用户构造超大请求体造成服务器资源被占满。所以这层配置并不是越大越好,而是根据实际业务需求取一个合理的值。正常项目团队传文档、传压缩包,单文件几 MB 到几十 MB 是常态,可以设到 100M 或 200M;如果还有视频、镜像包这类超大文件,那就得考虑分块上传或者单独走对象存储了。
3. 实操之前的准备工作:先搞清楚几份配置文件在哪
3.1 定位 PHP 配置文件
在动手修改之前,第一步是找到 PHP 的配置文件。不同发行版、不同安装方式,配置文件的位置差异很大。我见过用源码编译安装的 PHP,配置文件在/usr/local/php/etc/php.ini;用 yum 或 apt 安装的,一般在/etc/php.ini;还有的发行版会拆分成/etc/php/7.x/cli/php.ini和/etc/php/7.x/fpm/php.ini,注意区分 CLI 和 FPM 用的是不同的配置文件。
最靠谱的方法不是靠猜,而是执行命令查当前加载的配置文件路径。在终端运行:
php --ini或者查询当前生效的上传相关参数值:
php -i | grep -E "upload_max_filesize|post_max_size"如果是通过 php-fpm 运行的,还可以创建一个临时 PHP 文件,输出phpinfo()来查看实际加载的配置,但更推荐用命令行直接查。
这里有一个容易踩的坑:有些环境里php --ini查到的是 CLI 的配置路径,而 Web 请求实际走的是 php-fpm 或 Apache 的libphp模块,两者加载的php.ini可能不是同一个。比如某次我的模块化部署里,CLI 走/etc/php.ini,而 php-fpm 却加载了/etc/php-fpm.d/php.ini下面覆盖的参数。仅改 CLI 的配置根本不影响 Web 侧。所以最好先确认你的 Web 环境是php-fpm还是mod_php,再对应找配置。
还有一个更隐蔽的情况:php-fpm的 pool 配置里(通常在/etc/php-fpm.d/www.conf)可以直接用php_admin_value或php_value形式覆盖upload_max_filesize、post_max_size等参数。这个优先级高于php.ini,但又低于目录下的.htaccess(如果允许用php_flag)。如果php.ini里已经改成 200M,但www.conf里还写着一行php_value upload_max_filesize = 8M,那么实际生效的就是 8M。我在排查时遇到过不止一次这种情况,所以每次改配置前,都要全盘搜索一遍关键词:
grep -r "upload_max_filesize" /etc/php* /usr/local/php*把每一处出现的地方都看清,才能确定真正的配置入口。
3.2 定位禅道运行目录和 Nginx 配置
禅道本身的安装位置也值得确认。一般禅道企业版的默认安装路径是/opt/zbox或者/www/zentaopms,开源版通过源码部署的话常常放在 Web 根目录下,比如/data/www/zentaopms。如果你不确定,可以看 Nginx 的配置里的root指令指向哪里,或者访问禅道登录页,用浏览器的“查看源代码”看静态资源路径,大致能推断出来。
Nginx 配置文件的查找方式相对固定。在终端输入:
nginx -t可以校验配置语法,同时会输出主配置文件路径。一般是/etc/nginx/nginx.conf。然后看它 include 了哪些子配置文件,常见的模式是/etc/nginx/conf.d/*.conf以及/etc/nginx/sites-enabled/*。禅道对应的站点配置,通常在某个以域名命名的 conf 文件里,或者在/usr/local/nginx/conf/vhost/目录下。
找到之后,重点看server块内的配置,client_max_body_size一般放在server或location块内。如果你找不到这个参数,说明它没被显式设置,那么默认值就是 1m。
4. 核心实操步骤:从 PHP 到 Nginx 再到禅道逐层放开
4.1 修改 PHP 的 upload_max_filesize 和 post_max_size
确定好配置文件的路径之后,用你习惯的编辑器打开php.ini,搜索以下三个参数:
upload_max_filesize = 200M post_max_size = 220M max_execution_time = 600 memory_limit = 512M参数的具体数值不需要完全照搬,而是要根据你的实际业务来定。我自己的习惯是:单文件上限设为 200M,post 总上限设为 220M,留 20M 的空间给表单字段和其它文本数据。max_execution_time从默认的 30 秒改成 600 秒,因为大文件上传在服务器处理时需要一定的时间,尤其是在磁盘 I/O 较慢或文件需要做安全检查的场景下。memory_limit至少提到 512M,PHP 在接收和写入大文件时,如果开了某些扩展(比如做图片处理、解压校验),内存消耗会明显上升。
这里有个细节:post_max_size必须大于upload_max_filesize。否则当用户一次上传多个文件时,总大小可能超过post_max_size,请求会被 PHP 直接拒绝。如果你不确定这两个值的倍数关系,可以直接把post_max_size定为upload_max_filesize的 1.1 倍或 1.2 倍。
修改完成后,需要重启 php-fpm 才能生效。重启命令根据发行版不同有所区别:
systemctl restart php-fpm # 或者 service php-fpm restart如果你使用的是禅道自带的集成环境,比如 zbox,那么修改的不是系统 php-fpm,而是禅道内置的 PHP 服务。这时候重启方式要换成禅道的控制脚本。切到 zbox 目录下:
./zbox restart或者用它提供的service.sh脚本。有人会忽略这一步,改完php.ini就想当然地认为生效了,实际上内置 PHP 的进程还跑着旧配置,上传限制一点没变。
4.2 修改 Nginx 的 client_max_body_size
PHP 改完之后,接着解决 Nginx 层。在禅道对应的站点配置文件的server块内添加或修改一行:
client_max_body_size 220M;如果原本已经有这行配置,直接改数字即可。注意单位,m是小写,表示兆字节;也可以写成k、g。我的建议是放在server层级而不是某个location层级,这样该站点下所有上传路径都覆盖到了。如果只放在某个location里,比如location /api/,其它路径的请求仍然受默认 1M 限制影响,容易漏掉。
改完配置后用命令检查语法:
nginx -t看到syntax is ok再执行重载:
nginx -s reload # 或者 systemctl reload nginx不要用nginx -s stop再启动这种方式,会导致瞬间断连;reload是平滑重载,生产环境更安全。
这里我再强调一个容易忽视的点:如果禅道是跑在 Apache 环境下,Nginx 那层就不存在,对应的是 Apache 的LimitRequestBody或mod_security的规则。但绝大多数生产部署都是 Nginx + php-fpm,所以本文以 Nginx 为主线。
4.3 禅道自身的配置文件与 UI 层提示
到了这一层,才轮到禅道自身。禅道并非没有上传大小限制,只是它的限制值并不是从数据库读的,而是直接读 PHP 的upload_max_filesize。换句话说,你改完 PHP 后禅道后台的提示内容会自动跟着变。
但有一个特殊情况:禅道的配置文件/www/zentaopms/config/my.php里,存在一个叫maxSize的配置项。某些版本的禅道,把这个值写死在配置里,前端 JS 在进行文件校验时会优先读取它。如果这个值没有同步更新,就会出现一个诡异的现象:大文件实际上传到了服务器,但前端在上传开始前就拦截了,提示“文件超大”,实际上后端并没有拒绝。
这种案例我遇到过好几次。解决方案是在my.php中找到类似这样的配置:
$config->file->maxSize = 200;数字单位是 MB。将它调整到和 PHP 的upload_max_filesize一致或略小,保存后刷新页面。纯开源版在某些版本中这个值可能不存在,那就不用加,只靠 PHP 配置即可。
还有一点涉及附件存储路径的权限问题。禅道的附件默认存放在www/data/upload目录下。这个目录的归属用户必须和 php-fpm 运行用户一致,否则上传的文件无法写入,各个版本的禅道还会返回各自特有的错误提示。常见做法是设为 777:
chmod -R 777 /www/zentaopms/www/data/upload但要注意,chmod -R 777只适合局域网或测试环境,公网部署建议改为组权限或使用chown指定用户,避免不必要的安全暴露。
4.4 校验修改是否真的生效
改完所有配置后,不要急着让同事去试,先自己验证一遍。最直接的方法是去禅道的“后台-系统设置-附件设置”页面,看看显示的上传上限是否已经变成你期望的值。如果界面上的值没变,说明某个配置没改对或者没重启成功。
更严谨的做法是用命令行模拟请求。先生成一个测试文件:
dd if=/dev/zero of=testfile.bin bs=1M count=150然后通过 curl 上传到这个测试文件,观察响应码和响应时间:
curl -F "file=@testfile.bin" http://你的禅道域名/index.php?m=file&f=upload如果返回正常,说明上传链路已经通了。如果返回 413,说明 Nginx 层没改对;如果返回 500 或 PHP 警告,说明 PHP 层配置有问题;如果禅道返回业务错误码,则可能卡在前端校验或者my.php的maxSize没改。
这一步非常关键,它能帮你快速定位是哪一层还没打开,避免靠肉眼猜。
5. 常见问题与排查技巧实录
5.1 改了配置却提示“文件太大”或 413
这是出现频率最高的问题。我已经在上文中提到了主要原因:Nginx 的client_max_body_size没改,或者没重启 php-fpm。但还有一种相对少见的原因:有反向代理层在请求前面,比如用了 CDN、云 WAF 或者另一层 Nginx。这种情况下,你改的只是后端 Nginx,前端代理层还有自己的请求体限制。排查方法是用 curl 直接打后端 IP(绕过代理层)测试,如果绕过代理能正常上传,问题就在代理层,需要到代理的配置里同样放开client_max_body_size。
还有一次我踩过一个奇葩坑:服务器上有两个 Nginx 实例,一个监听 80 端口做转发,另一个监听 8080 端口处理 PHP。我改了后者的client_max_body_size,但前者的转发层没改,结果上传大文件时总是被前端的 Nginx 拦截。这种多实例场景在容器化部署里尤其常见,nginx ingress 控制器、API 网关、负载均衡器都可能夹在中间,每一层都需要检查。
5.2 上传 200M 文件时提示“连接被重置”
这个现象容易误导人。表面看是网络问题,实际原因往往是 PHP 的max_execution_time超时。当服务器接收大文件后开始处理(比如做安全检查、转移文件、生成缩略图),这个处理过程耗时会超过max_execution_time的默认值,PHP 会直接终止脚本。表现就是请求进行到一半时被重置,浏览器无法收到正常响应。
解决方案是把max_execution_time调大,同时关注php-fpm的request_terminate_timeout参数。这个参数在www.conf或 pool 配置里,默认可能是 0(表示不限制),但有些发行版会设置成 30 或 60 秒。如果你改了 PHP 的max_execution_time但没动这个参数,它仍然会在超时后强制杀掉请求。我的建议是把request_terminate_timeout也设置成和max_execution_time相同或稍大一点,避免因为这两个超时机制不同步而出现问题。
5.3 内网上传没问题,外网总失败
内外网表现不一致,大概率是代理层或防火墙的问题。外网请求经过的路径更长,可能经过 CDN、云防火墙、网关等节点,其中任何一层对请求体大小有限制,都会导致上传失败。还有一种可能是链路中存在 TCP 超时或连接重置的节点,大文件上传耗时较长,更容易被中间节点切断。
排查思路是分段测试:先用内网 IP 直接上传,确认应用层没问题;再用公网域名上传,看失败点在哪;如果公网失败而内网成功,借助抓包工具看是哪个节点返回的 TCP RST,或者到 CDN/防火墙控制台看访问日志,找到具体的拦截原因。这类问题没有统一的开配置指令,只能顺着链路一层层排查。
5.4 上传成功后文件损坏或只有部分内容
这种问题在跨版本升级禅道或者迁移服务器后容易出现。原因一般是服务器本身的 PHP 配置文件没有错误,但上传过程中因为 PHP 的post_max_size小于upload_max_filesize造成的。当post_max_size比文件实际体积小得多时,PHP 不会完整接收报文体,而是截断处理,导致禅道拿到不完整的数据。表现出来就是文件能上传,但传上去的文件大小比原本小不少。
建议总是把post_max_size设置成大于upload_max_filesize的 1.2 倍,这样就算用户同时上传多个大文件,总和也不会轻易超过 POST 限制。
5.5 修改后无任何变化的速查清单
为了解决“改了什么但看起来没改”的情况,我把自己的排查顺序做成了一张清单,每次遇到这类问题就按顺序走一遍,效率高很多:
- PHP 配置文件路径是否正确(CLI 和 FPM 是否共用同一个
php.ini)。 - 是否在
www.conf或php-fpm.d下面存在php_value或php_admin_value覆盖了php.ini的值。 - Nginx 站点配置里的
client_max_body_size是否放在了正确的server或location块。 - 有没有反向代理、CDN、负载均衡器在前面拦了一道。
- 禅道
config/my.php里的maxSize是否同步更新。 - 改完配置后是否重启了正确的服务,重启的是不是禅道内置的 PHP。
- 附件上传目录的写权限是否正常。
这七步走完,绝大多数上传限制问题都能找到原因。我见过有人在第一步就卡住,因为他的环境里php -i查到的是一个不存在于 PHP 运行进程里的路径,配置改错了地方,后面全白费。
6. 关于调大上传限制的补充思考
把限制调大只是第一步,真正上线前还应该考虑后续运维的问题。上传文件大了,磁盘空间消耗速度会明显加快,尤其是团队频繁交换安装包、视频、设计稿的场景。建议给上传目录配置配额监控,或者定期做清理策略。禅道自带文件管理功能,可以对历史附件做梳理,但大文件是否要保留在本地,还是迁移到对象存储,也需要根据实际情况决策。
另外,大文件上传会让 php-fpm 进程长时间被占用,每个进程在处理大文件时都会持有较多内存,并发上来之后,服务器内存可能吃紧。如果团队经常有超过 100M 的附件,我更倾向于建议在 Nginx 层直接开启client_body_timeout和超时设置来保护应用层,或者在应用层做分块上传和断点续传的改造,避免把压力全压在 PHP 上。
我在实际部署中还遇到过另一个细节:如果你把上传限制改得很大,但服务器磁盘分区容量不足,用户上传时会发现文件显示上传成功,实际写入时因为磁盘满而失败。禅道在这种场景下的报错不一定明确,甚至可能只显示“上传失败”而没有具体原因。所以在调大限制的同时,也应该检查一下磁盘剩余空间,确保上传目录所在分区的容量能满足新需求。
至于“到底设成多少合适”,我的建议是不要盲目追求大。你需要先统计一下团队日常文件的体积分布。如果基本都是 PDF、Word、Excel,三五十兆就够用;如果有安装包、日志包、视频演示,那可以考虑 200M 到 500M。上限设得越大,服务器面临的负载和风险也越高。曾经有位同事为了图省事直接把upload_max_filesize改成 1G,结果没过多久测试同事传了一堆视频,磁盘直接告警,最后又得花时间清理。这种事发生过一次之后,我就养成了“按需配置”的习惯,绝不再拍脑袋设一个夸张的值。
另外,如果你的禅道里有好几个项目同时在使用,建议在改动之前先和相关同事做一次沟通,明确他们最需要的上传文件类型和最大体积,避免为了极个别需求把整体限制抬得太高。上线之后,也可以定期关注系统日志里的上传记录,动态调整数值配置,而不是一劳永逸地设置一次就再也不管。
7. 用这套思路能复用到哪些场景
如果你手上管的不只是禅道,还有其它 PHP 应用,这套思路完全可以复用。我平时排查“某个 Web 系统上传不了大文件”时,永远先按“Nginx —— PHP —— 应用配置”这个顺序来检查,而不是一上来就猜是应用系统自身的 bug。
举个实际中的例子,有一次某内部系统用的是另一个 PHP 开发框架,上传文件时永远提示 413。我按这套链路排查,发现 Nginx 的client_max_body_size确实被设置为 0(不限制)了,但 PHP 的upload_max_filesize只有 2M,并且 php-fpm 还没重启,改完重启之后问题立刻消失。再往后,这个系统的前端 JS 还有一个手动校验文件大小的逻辑,必须同步改掉。这种多层联动的排查思路,比死磕某一个配置文件要高效得多。
再补充一个冷门场景:禅道部署在容器里时,修改配置的要点和物理机一致,但需要额外注意容器重启后配置文件是否会被镜像覆盖。如果你的php.ini是通过 Dockerfile 写进去的,而不是挂载卷持久化的,那么每次容器重建都会丢失修改。这种环境里,我建议先把配置目录用卷持久化,再加一个初始化脚本确保容器启动时自动修改参数,避免每次部署都要手工操作一遍。
我在实际项目中用过这个方案:在启动脚本里加入一行sed命令,自动修改php.ini中的上传参数,并把client_max_body_size写入 Nginx 配置。这样不管是自动扩容还是手动重启,配置都不会丢。这个方法同样适用于非容器环境,你可以用部署脚本来代替手动改配置,减少人为失误。
最后提一句,改完权限后,如果禅道页面上传还报错,记得看服务器端错误日志。我经常会看到有人反复改配置但不去看日志,结果问题其实一直是别的原因。/var/log/nginx/error.log和/var/log/php-fpm/www-error.log是最直观的排查入口,里面通常直接记录了失败原因,比自己在配置里瞎猜快得多。