Docker跑多个PHP项目共享一个MySQL容器连接池够不够用
Docker跑多个PHP项目,共享一个MySQL容器,连接池到底够不够用
多个PHP项目放在同一台Docker宿主机上,MySQL也跑一个容器,这种部署在中小业务里很常见。比如一个官网、一个后台、几个API服务、一个定时任务项目,全都通过Docker network访问同一个mysql:8.0容器。刚开始访问量不大,看起来没问题;业务跑一段时间后,偶尔出现Too many connections、SQL变慢、PHP请求卡住,这时才会怀疑是不是连接池不够。
这里要先把概念捋清楚:MySQL本身不是像Java应用那样天然有一个业务侧连接池。MySQL有max_connections、thread cache、连接线程管理;PHP这边如果是传统PHP-FPM,大多数情况下是请求来了建立连接,请求结束释放连接。只有开启PDO persistent connection、mysqli persistent connection,或者前面挂ProxySQL、MySQL Router、Swoole连接池,才更接近大家口中的“连接池”。
所以问题不能只问“一个MySQL容器连接池够不够”,更准确的问法是:所有PHP-FPM worker在峰值并发时,可能同时占用多少MySQL连接,MySQL容器的CPU、内存、IO能不能扛住这些连接背后的SQL压力。
先看连接数,不要只看项目数量
实际使用中发现,项目数量经常会误导判断。5个PHP项目不一定比1个项目压力大,关键看每个项目的PHP-FPM进程数、请求并发、是否长连接、有没有慢SQL和后台任务。
一个很粗但有用的估算方式是:最大数据库连接数 ≈ 所有PHP项目的pm.max_children之和 × 每个请求同时打开的数据库连接数。
比如有4个PHP项目,每个项目php-fpm配置pm.max_children=20,代码里每个请求只连一个MySQL库,那么理论峰值就是80个MySQL连接。如果再有Laravel queue、ThinkPHP定时任务、爬虫入库脚本、后台导出任务,额外预留20到40个连接并不过分。
常见配置可以这样看:
场景:3个小型PHP站点;每个项目pm.max_children=10;理论PHP连接上限约30;MySQL max_connections设151;通常够用。
场景:5个PHP项目,其中2个有后台任务;每个项目pm.max_children=20;理论PHP连接上限约100,加任务约20;MySQL max_connections设151;能跑,但高峰时空间不大。
场景:8个项目混在一台机器,每个项目pm.max_children=30;理论PHP连接上限约240;MySQL max_connections还是默认151;大概率会遇到Too many connections。
场景:开启PDO持久连接,6个项目,每个项目pm.max_children=30;每个FPM子进程可能长期占着连接;MySQL连接数容易长期维持在100到180以上;低访问时也不一定释放。
这里补充一点,MySQL默认max_connections一般是151,不同镜像和配置可能有调整。很多人看到连接数到140就开始慌,其实不是140这个数字危险,而是连接背后的SQL如果大量等待锁、等待磁盘IO、等待CPU,连接数会堆得很快。
PHP-FPM的pm.max_children才是连接数入口
Docker里跑PHP项目,很多compose文件只限制了容器内存,却没认真调PHP-FPM。一个项目复制一份默认配置,pm.max_children随手写成50,五个项目就是250个潜在并发执行单元。每个执行单元只要访问数据库,就可能占一个MySQL连接。
PHP-FPM常见配置里,pm.max_children决定同时处理多少请求。pm.start_servers、pm.min_spare_servers、pm.max_spare_servers影响空闲进程数量,但连接数上限主要还是看max_children。
如果是pm=dynamic,访问低的时候进程少一些,连接压力也低。访问一上来,FPM子进程拉满,MySQL连接数会跟着上去。如果业务代码使用普通短连接,请求结束后连接释放;如果使用持久连接,连接可能留在对应FPM子进程里,下次复用,但也可能长期占着MySQL连接名额。
实际线上更推荐先按CPU和内存把每个项目的pm.max_children压住,而不是给每个PHP容器都开很大。比如2C4G机器上同时放多个项目,单个PHP容器pm.max_children写50并不现实,CPU上下文切换和MySQL等待会把响应时间拉得很难看。
MySQL容器不是连接数越大越稳
有些处理方式是直接把max_connections从151改成500或1000。短期看Too many connections少了,但后面可能换成更隐蔽的问题:内存抖动、swap、查询变慢、容器被OOM kill。
MySQL每个连接都有线程栈和连接相关开销,thread_stack、net_buffer、连接上下文这些是基础消耗。更麻烦的是sort_buffer_size、join_buffer_size、read_buffer_size、tmp_table_size这类缓冲,不是每个连接固定全量占用,但SQL一旦触发排序、join、临时表,活跃连接多了以后内存会被快速吃掉。
举个实际一点的数:一台4G内存宿主机,MySQL容器限制2G,innodb_buffer_pool_size设置1G,如果max_connections拉到500,表面上能接很多连接,但只要几十个连接同时跑复杂查询,内存就会明显紧张。再叠加PHP容器、Nginx容器、Redis容器,宿主机剩余内存不够,Docker层面也救不了。
更稳的做法是先看MySQL状态:
SHOW VARIABLES LIKE 'max_connections'; 看上限。
SHOW STATUS LIKE 'Threads_connected'; 看当前连接数。
SHOW STATUS LIKE 'Max_used_connections'; 看历史峰值连接数。
SHOW STATUS LIKE 'Threads_running'; 看真正正在执行的线程数。
Threads_connected高但Threads_running低,可能只是连接占着,未必有压力。Threads_running长期高,CPU、锁、IO、慢SQL就要一起查。
Docker网络不会帮你做连接池
Docker Compose里多个PHP服务通过同一个bridge network访问mysql容器,服务名写mysql,端口3306。这只是网络可达和DNS解析,不会自动复用连接,也不会帮业务做排队。
同一个宿主机内走Docker bridge,网络延迟一般不是主要矛盾,单次RTT通常很低。真正要关注的是MySQL容器资源限制、宿主机磁盘IO、PHP-FPM并发和SQL质量。
如果MySQL容器没有设置资源限制,它可能和PHP容器抢CPU、抢内存。如果设置了限制,比如cpus: 1.5、mem_limit: 2g,那么MySQL在高峰时会被硬性卡住,连接数堆积更明显。生产环境里,数据库容器和应用容器混跑可以,但要清楚这是资源共享,不是资源隔离。
短连接、持久连接、ProxySQL的取舍
传统PHP-FPM项目默认短连接并不丢人。很多中小业务,短连接配合MySQL thread_cache_size,性能已经够用。MySQL建立连接确实有成本,但同机房、同宿主机、低并发场景下,这个成本通常不是瓶颈。
持久连接适合连接建立成本明显、请求频繁、SQL很轻的场景。但它也有坑:FPM子进程不退出,连接就可能一直占用;项目多、FPM worker多时,空闲连接也会堆在MySQL里。更麻烦的是会话级状态,比如临时表、变量、事务未清理,代码不规范时容易出诡异问题。
ProxySQL更适合连接数明显膨胀、读写分离、连接复用需求比较强的场景。它可以在PHP和MySQL之间做连接复用、路由、限流、SQL规则。但引入ProxySQL后也多了一个组件,配置、监控、故障切换都要管。小项目没必要一上来就加,连接数长期打满、MySQL连接建立频繁、业务开始拆库读写分离时再考虑更合适。
多项目共享MySQL容器时,比较稳的容量估法
可以按峰值请求来估,而不是按“现在访问量感觉不大”来估。
假设有6个PHP项目,其中2个是主要业务,4个是低频后台。主业务每个pm.max_children=30,后台项目每个pm.max_children=8,那么PHP-FPM理论并发执行数是30×2+8×4=92。
如果每个请求只使用一个MySQL连接,预估连接峰值就是92。再预留定时任务、人工后台操作、迁移脚本、监控连接,给30个余量,总计约122。MySQL max_connections设到180或200,通常比直接设500更稳。
但这里还有一个现实问题:如果92个请求同时打MySQL,数据库CPU和IO不一定扛得住。连接数够,不代表数据库吞吐够。实际压测时要看QPS、慢查询、InnoDB buffer pool命中率、磁盘await、CPU steal、容器throttle情况。
多说一句,很多“连接池不够”的报警,最后查出来是慢SQL导致连接释放慢。比如一个列表页没有合适索引,单次查询从20ms变成2s。原来每秒50个请求只占很少连接,现在每个连接占着2秒,连接数马上翻几十倍。这个时候把连接数加大,只是让更多请求一起慢。
配置示例:不要让每个容器都无限长大
PHP-FPM可以按项目重要性分配worker。核心API项目给pm.max_children=30,后台管理给10,低频站点给5到8。这样MySQL连接入口就被限制住了。
MySQL侧可以从这些配置入手:max_connections设置为预估峰值的1.3到1.8倍;thread_cache_size设置到64或128,减少线程创建销毁;innodb_buffer_pool_size按容器内存的50%到70%设置;slow_query_log打开,long_query_time可以先设1s,排查期设0.2s也可以。
如果MySQL容器内存是4G,innodb_buffer_pool_size设2G到2.5G比较常见。max_connections如果业务估算峰值是120,可以设200。不要在4G MySQL容器里随便设1000,除非SQL非常轻、缓冲参数也经过压测。
Docker Compose里建议给MySQL单独挂载数据盘,别把数据写在容器层。生产环境至少要有明确的volume、备份目录、binlog策略。MySQL容器重建不等于数据安全,容器只是进程包装,数据库还是数据库。
什么时候一个MySQL容器已经不适合继续共享
共享MySQL容器适合项目规模小、团队能统一管理SQL、备份和发布节奏一致的场景。一旦出现资源争抢,就要重新拆。
比较典型的信号是:某个项目的慢SQL拖垮所有项目;后台导出影响前台接口;一个项目需要升级MySQL版本但其他项目不敢动;备份窗口越来越长;binlog增长很快;Max_used_connections经常接近max_connections;Threads_running在高峰期长期超过CPU核心数很多。
比如4C8G宿主机上,MySQL容器、6个PHP容器、Redis、Nginx都放一起,业务峰值时CPU跑到90%以上,iowait超过10%,这时候继续调max_connections意义不大。该拆数据库就拆数据库,该上独立云服务器就上独立云服务器。
如果你也在找这种能承载多个PHP项目、MySQL容器或者独立数据库部署的云服务器,可以看看129云。像金华电信-C型这种16C、16G DDR4 ECC、1TB硬盘、100Mbps独享带宽、100Gbps防御的配置,更适合放国内业务、后台系统和需要一定DDoS防护的项目。香港多IP站群-C型走CN2精品线路,适合SEO站群和香港访问场景。客服热线400-9177118,选配置前把项目数量、PHP-FPM并发、MySQL数据量说清楚,机器规格会更容易对上。
压测时重点看连接占用时间
判断够不够用,不要只看连接上限。压测时更关键的是连接占用时间。一个请求如果从拿到连接到释放连接只用30ms,100个连接能支撑的吞吐很可观;如果一次请求占用连接800ms,同样100个连接很快就满。
可以在应用侧记录SQL耗时,也可以在MySQL侧开slow log。Laravel、Symfony、ThinkPHP这些框架都能加SQL日志,但线上不要长期打印全量SQL,日志IO会反过来拖慢服务。排查期打开,定位后关闭或采样。
压测数据可以这样读:并发50时,Threads_connected稳定在40以内,Threads_running在5到10,P95响应时间低于200ms,说明MySQL还有余量。并发100时,Threads_connected冲到150,Threads_running长期60以上,P95超过2s,那不是单纯连接不够,数据库执行已经拥堵。
如果压测里出现大量Sleep连接,要看PHP是否开启了持久连接,或者代码有没有连接后长时间做非数据库操作。比如请求一开始连库,然后去调用第三方API,等外部接口返回后再查库,这段时间连接一直被占着,就很浪费。更好的写法是需要查库时再拿连接,查完尽快释放,不要把数据库连接当全局资源挂整段请求。
后台任务比前台请求更容易吃满连接
Web请求通常有Nginx、PHP-FPM并发限制,入口还算可控。后台任务就不一样了,Supervisor一开就是十几个worker,队列堆积时所有worker同时跑,每个worker都连MySQL,连接数瞬间上去。
实际使用中发现,Too many connections经常不是白天用户访问打出来的,而是凌晨任务打出来的。比如统计脚本、订单归档、日志清洗、批量同步第三方数据,全都安排在凌晨1点到3点,任务之间互相不知道,最后一起把MySQL打满。
后台任务要单独限并发。Laravel queue的--max-jobs、--sleep、进程数量,Crontab脚本的flock锁,批处理每批条数,都要控制。批量更新不要一次十万行,分批500到2000行更容易观察,也不容易把undo、redo、binlog打爆。
共享MySQL容器的账号也别混用
多个PHP项目连同一个MySQL容器,很多人图省事用同一个root账号或者同一个业务账号。短期方便,后面排查很难。哪个项目占连接、哪个项目慢查询多、哪个项目权限误操作,都不好分。
建议每个项目单独MySQL用户,权限只给对应库。这样看processlist时能通过user和db区分来源。慢查询日志里也更容易按库名、账号、SQL指纹分析。
连接数也可以按账号观察。MySQL原生对单用户连接限制有MAX_USER_CONNECTIONS,可以给低优先级项目限制连接数,避免一个边缘项目把主业务拖死。比如后台报表项目限制30个连接,主API不限制或给更高上限。
连接池够不够用,现场判断看这组数
在线上机器执行SHOW STATUS LIKE 'Max_used_connections'; 如果最大使用连接数长期不到max_connections的50%,一般不是连接上限问题。
SHOW PROCESSLIST里如果大量Sleep,优先查持久连接、FPM worker数量、连接释放时机。
如果大量Sending data、Creating tmp table、Copying to tmp table、Waiting for lock,那要查SQL、索引、锁等待、临时表。
如果Threads_running长期很高,同时CPU满,说明MySQL正在硬算。加连接数只会让更多SQL排队执行。
如果Threads_connected接近max_connections,但Threads_running不高,应用侧连接持有策略更可疑,尤其是PHP持久连接和后台worker。
如果容器层面出现memory limit、OOM、CPU throttling,先把资源问题处理掉。数据库在被限流的CPU和紧张内存下运行,连接池参数调得再细也会抖。
比较推荐的部署边界
小流量多项目可以共享一个MySQL容器,前提是每个PHP项目限制pm.max_children,MySQL max_connections按估算设置,slow log打开,备份和监控到位。
中等流量项目可以继续共享MySQL实例,但核心项目和边缘项目最好拆库、拆账号、拆任务并发。MySQL可以仍在同一容器或同一台机器上,但资源占用要能看清楚。
核心业务开始有稳定收入、数据增长快、后台任务重,就不建议MySQL继续和一堆PHP容器混跑。MySQL独立机器或独立云数据库会更省心,应用服务器和数据库服务器之间走内网,备份、监控、扩容、故障处理都清楚很多。
如果还在单机Docker阶段,先把MySQL max_connections从默认151调整到合理区间,比如200或300,同时把PHP-FPM总max_children控制在连接上限以内。不要PHP侧理论能开500个数据库连接,MySQL侧只给151;也不要MySQL侧给1000,PHP和SQL完全不控。连接入口和数据库承载能力要对齐。