ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP断点调试:PhpStorm + Xdebug 配置与排错指南

PHP断点调试:PhpStorm + Xdebug 配置与排错指南 写 PHP 这几年var_dump配exit这套组合拳我用得比谁都熟直到有一次要追一个跑在队列消费进程里的空值问题——打出来的东西淹没在几万行日志里改一次代码还得重启 worker 等半天。那次之后我才老老实实把 PhpStorm 和 Xdebug 这条调试链路彻底打通。这篇就是我把这件事从头到尾走一遍的完整记录PHP 端怎么装 Xdebug、php.ini里到底要写哪几行、PhpStorm 那边 Servers 和 Debug 面板怎么配、断点为什么命不中、命中了为什么停在不该停的地方全部按我自己踩过的顺序写下来。配置本身不复杂难的是出错时不知道从哪一层开始查所以排查链路我也一并铺开让刚上手的人能照着抄也让配过很多次的人在出问题时能快速定位到是 PHP 端还是 IDE 端的问题。1. 在动手配置之前先把这几个概念理清楚很多人把 Xdebug 配不起来不是因为操作步骤难而是因为对谁在跟谁说话没有概念。Xdebug 是装在 PHP 里的一个扩展它负责在代码执行到某一行时停下来并把当前的状态通过一个网络连接发给监听方PhpStorm 是监听方它开了个端口等消息收到之后把文件、行号、变量值映射到自己的编辑器界面上。这里有两个映射特别关键文件路径的映射以及请求到项目的映射。之所以强调这一点是因为后面九成的故障都出在这两处而不是出在扩展装没装上。还有一个容易被忽略的点Xdebug 是装给某一个具体的 PHP 进程用的。你在命令行里php -m能看到 Xdebug不代表 Nginx 后面的 php-fpm 也加载了它——这两个进程读的可能是完全不同的php.ini。我见过太多人对着 CLI 的配置改了半天结果浏览器请求走的是 FPM配置压根没生效。所以在动手之前先明确你要调试的是命令行脚本还是网页请求这两个场景的配置点不一样。最后提醒一句心态问题。第一次配 Xdebug一次成功是运气来回折腾两三个小时是常态。把每一层单独验证——PHP 端装没装上、连接有没有发出去、IDE 有没有收到、路径映射对不对——比反复重启服务要高效得多。1.1 断点调试相对于打印日志省下来的到底是什么用打印调试最大的问题是你得先猜。你猜错在哪一行就得去那一行加var_dump跑一遍发现不对删掉再换一处。整个过程是串行试错而且每次都要改动源码、重新加载。断点不一样代码执行到你标记的位置会自动停下来你可以看到那一刻整个作用域里所有变量的真实值包括$this上挂着的一堆属性、闭包里捕获的外部变量这些东西如果用打印方式光写输出语句就得写十几行。更实际的一点是不侵入。生产问题很多时候不好复现你不想为了看一眼中间状态就往代码里塞一堆临时输出更不想改完忘了删。断点是在 IDE 侧管理的源码里不留痕迹调试完直接关掉监听就行。还有几个打印做不到的能力条件断点让代码只在满足某个表达式时才停日志断点让它在不停下来的情况下把值写进日志异常断点让程序在抛出某类异常的那一刻冻结。这几个能力配合起来排查问题时能省掉大量反复加代码的动作。当然打印也不是没用。对于线上环境、对于跑在别人机器上的代码、对于没有调试扩展的容器打印仍然是唯一手段。我的习惯是本地开发一律用断点线上排错用结构化日志加日志级别的临时调整。两者不是替代关系是场景分工。1.2 Xdebug 3 的几个 mode选错了会很痛苦Xdebug 从 2 升到 3 最大的变化之一是把一堆布尔开关收拢成了一个xdebug.mode配置项用逗号分隔可以同时开多个模式。常用的有这么几个debug断点调试能力也就是这篇要用的核心模式。develop美化var_dump输出、增强错误页面的可读性很多人的默认模式就是这个。coverage代码覆盖率统计给 PHPUnit 这类测试框架用。trace函数调用追踪输出到文件适合看一个请求到底调了哪些函数。profile性能剖析输出能被 KCachegrind 之类的工具解析的文件。这里有个坑需要单独拎出来说。默认情况下 Xdebug 3 装好后mode是develop也就是说扩展是加载了的但断点调试能力并没有打开。你在 PhpStorm 里点调试没反应第一反应会去怀疑 IDE 配置实际上根本原因可能是xdebug.mode里少了debug。我第一次遇到这个问题时对着 Servers 面板折腾了四十分钟最后是看php --ri xdebug的输出才发现mode里压根没debug这一项。另外提醒一句性能账develop模式对性能有可见影响trace和profile的影响更大。本地开发开着无所谓如果要压测或者跑大批量数据处理记得把mode临时关掉或者改成只留必要项。这个影响不是玄学是实打实每一次函数调用都要多走一层处理逻辑。1.3 PHP 版本、Xdebug 版本、PhpStorm 版本的对齐关系这三者的版本关系如果对不上会出现各种诡异现象。Xdebug 2 和 Xdebug 3 的配置项名字是完全不同的两套Xdebug 2 用的是xdebug.remote_enable、xdebug.remote_host、xdebug.remote_portXdebug 3 全部改成了xdebug.mode、xdebug.client_host、xdebug.client_port。如果你拿着一篇 Xdebug 2 时代的教程去配 Xdebug 3会发现配置项写了但完全不起作用因为旧名字在新版本里已经被移除了不会报错只是被静默忽略。端口也是一个高频混乱点。Xdebug 2 默认端口是 9000Xdebug 3 改成了 9003。PhpStorm 里默认监听的是 9003如果你 PHP 端写的是 9000两边就对不上了。另外 9000 这个端口经常被 php-fpm 或者别的服务占用这也是官方改默认值的原因之一。版本对应上Xdebug 3 系列需要 PHP 7.2 及以上具体的小版本对应关系在官方文档的兼容性表格里写得很清楚每个 PHP 小版本都有明确支持的 Xdebug 版本区间装之前先去核对别凭感觉下 dll 或者编译。PhpStorm 这边2018.3 之后的版本对 Xdebug 3 支持得都比较好更老的版本可能需要手动改端口配置。用一个对照表把这三者列出来装之前先对一眼能省掉很多来回组件关键信息常见坑PHP用php -v确认确切小版本CLI 和 FPM 版本可能不同Xdebug3.x 对应 PHP 7.2各小版本有明确支持表装了不匹配的版本会加载失败PhpStorm2018.3 默认端口 9003老版本默认 9000需要手动调端口Xdebug 3 默认 90039000 常被 php-fpm 占用2. PHP 端装 Xdebug 和写 php.ini逐行拆开看这一部分是我建议花最多时间的地方因为 PHP 端配好了IDE 端基本就是点点鼠标的事PHP 端有问题你在 IDE 里怎么调都是白费劲。核心动作只有两个把扩展装进正确的 PHP、把配置写进正确的php.ini。听起来简单但正确这两个字里藏着一堆细节。先说一个判断原则后面会反复用到一切以phpinfo()的输出为准而不是以你记得的路径为准。很多服务器上装了好几个 PHP 版本/etc/php/8.2/下面有 CLI、FPM、Apache 三套配置你改的那套不一定就是正在跑的那套。打开phpinfo()页面看顶部 Loaded Configuration File 那一行指向的文件那才是真正生效的php.ini。命令行场景则用php --ini看输出的第一行就是 CLI 加载的文件。配置写完之后别急着去 IDE 里试先在 PHP 端做一次独立验证。验证的目标就一个确认 Xdebug 加载了、模式对了、目标地址和端口写对了。这一步确认下来后面出问题基本可以断定是 IDE 侧或者网络侧的事排查范围一下子缩小一半。2.1 先搞清楚你的 PHP 是 CLI、FPM 还是 Apache 模块这一步看起来是废话但它决定了你后面要改哪个文件。判断方法很简单网页请求的 SAPI 用phpinfo()页面里的 Server API 那一栏看通常会显示FPM/FastCGI或者Apache 2.0 Handler命令行脚本直接跑php -r echo php_sapi_name();输出一般是cli。确认之后对应的配置文件位置大致是CLI 通常是/etc/php/8.x/cli/php.iniFPM 是/etc/php/8.x/fpm/php.iniApache 模块是/etc/php/8.x/apache2/php.ini。这几个文件互相独立各自加载各自的扩展列表。你在 CLI 里pecl install xdebug装好并加了zend_extension之后FPM 那边还需要单独启用一次否则网页请求依然没有调试能力。Ubuntu 系的发行版包管理装法一般是apt install php8.2-xdebug它会把扩展放进扩展目录但还需要在对应 SAPI 的配置目录里做个软链接或者写一行配置来启用。我自己的习惯是把这几个 SAPI 的配置都确认一遍因为日常既有跑脚本的需求也有跑网页的需求两边都能调省得来回切。确认方式就是分别跑php --ri xdebug和打开一个phpinfo()页面看 Xdebug 段落两边都出现版本信息才算真的装对了。2.2 三种安装方式按你的环境选一种安装方式主要看你手上的环境是什么形态没有必要追求某一种能装上并且可复现就行。第一种是pecl。在 Linux 或者 macOS 上如果 PHP 是源码编译或者通过 Homebrew 装的pecl install xdebug是最直接的路径。它会下载源码、编译、把扩展放进扩展目录然后提示你在php.ini里加一行zend_extensionxdebug.so。这个方式的优点是版本可以自己指定比如pecl install xdebug-3.3.2装一个特定版本缺点是需要编译环境缺php-dev或者编译工具链的话会失败。第二种是用系统包管理器。Debian/Ubuntu 上是apt install php8.2-xdebugCentOS 系上常见的是yum install php-xdebug。这种方式最省事包管理器会自动处理依赖和扩展目录但版本会滞后于官方发布而且不同源里的版本质量参差。装完之后记得看对应 SAPI 的conf.d目录里有没有生成启用文件。第三种是直接下预编译二进制或者 dll。Windows 上基本只能走这条路从 Xdebug 官方下载页选对应 PHP 版本、线程安全模式、架构的 dll丢进 PHP 的ext目录然后写zend_extensionphp_xdebug.dll。下之前一定要用页面上的版本检测工具确认因为 Windows 上的 PHP 分 NTS 和 TS、分 x64 和 x86选错了会直接加载失败。注意不要用extensionxdebug.so必须用zend_extension。Xdebug 是需要挂在 Zend 引擎层的扩展写错了不会生效而且有些 PHP 版本会直接报错退出。2.3 php.ini 里那几行到底在干什么把配置逐行拆开讲是因为照抄容易出问题的时候不知道哪一行在起作用就麻烦了。Xdebug 3 的典型配置是这样zend_extensionxdebug.so xdebug.modedebug xdebug.start_with_requesttrigger xdebug.client_host127.0.0.1 xdebug.client_port9003 xdebug.idekeyPHPSTORM xdebug.log/tmp/xdebug.log xdebug.log_level7xdebug.modedebug是开关决定 Xdebug 提供哪些能力前面已经说过。这里想补一句的是debug可以和别的模式组合比如xdebug.modedebug,develop同时拿到调试能力和好看的var_dump输出。xdebug.start_with_requesttrigger是最值得展开的一行。它有三个取值yes表示每个请求都尝试连接调试器no表示从不主动连接trigger表示只有请求里带了触发标识时才连接。日常开发推荐trigger因为yes会让你每一个页面请求都卡一下去等调试连接即使你根本没打算调。触发标识可以是 URL 参数、Cookie 或者 HTTP 头名字就是XDEBUG_SESSION值对应idekey。浏览器插件做的就是帮你自动带上这个 Cookie 这件事。xdebug.client_host是连到哪台机器的哪个端口本地开发写127.0.0.1就行。这里是本地回环地址Xdebug 在 PHP 进程里往这个地址发连接请求PhpStorm 在那个地址上监听。容器场景下这个值得改后面单独说。xdebug.idekeyPHPSTORM是给连接打一个标签PhpStorm 收到后会按这个 key 来匹配。这个名字不是必须叫 PHPSTORM但它和 PhpStorm 的默认设置一致改它没有额外好处除非你要在同一台机器上区分多个调试会话。xdebug.log和xdebug.log_level是排查神器。把日志打开Xdebug 会把我拿到请求了我尝试连接 127.0.0.1:9003连接失败了这些信息写进文件。调试配不通的时候先看这个文件能省掉大量猜测。2.4 装完之后的三步自检一步都不能省第一步php -m | grep -i xdebug确认扩展列表里有 xdebug。如果没有说明zend_extension那行没生效或者路径写错了。第二步php --ri xdebug这个命令会把 Xdebug 的所有配置项和当前值全部列出来。重点看xdebug.mode是不是包含debug、xdebug.client_port是不是 9003、xdebug.start_with_request是不是你要的值。如果用 FPM这一步要用网页版的phpinfo()来做CLI 的输出不反映 FPM 的配置。第三步给一个真实请求打一次触发观察xdebug.log里有没有连接记录。这一步是在验证连接有没有真的发出去比在 IDE 里瞎点更有针对性。日志里出现尝试连接的记录但 IDE 没反应问题就在 IDE 侧或者网络侧日志里压根没有尝试连接的记录问题就在 PHP 端的触发条件上。自检步骤命令或方式判断标准扩展加载php -m找 xdebug列表里有 xdebug配置生效php --ri xdebugmode 含 debug端口对得上连接触发看xdebug.log有尝试连接的记录3. PhpStorm 侧Servers、端口和路径映射PHP 端确认无误之后IDE 侧其实只有几个地方要配但每一个都容易配错。我的建议是先把 PhpStorm 的调试监听开起来再去浏览器里触发请求顺序反了会看到一堆无意义的连接失败记录。PhpStorm 里跟调试相关的主要有三块Settings PHP Debug管端口和监听行为Settings PHP Servers管主机名和路径映射工具栏上的电话图标管现在要不要接电话。很多人把前两块配得很认真却忘了点电话图标结果请求发过来没人接自然什么都不发生。这个细节后面在故障排查里还会展开。3.1 Servers 配置和路径映射九成断点不生效都出在这路径映射解决的问题是PHP 在执行时告诉调试器我停在了/var/www/html/app/Service/UserService.php第 88 行而 PhpStorm 需要把这个路径翻译成你本地项目里的app/Service/UserService.php。如果这个翻译关系不存在或者写错了IDE 收到断点信息也找不到对应的文件只能忽略掉。配置的位置在Settings PHP Servers新建一个 Server填几个关键字段Name随便起但后面运行配置里要引用它Host要和你浏览器里访问的域名或 IP 完全一致这个匹配是字符串级别的localhost和127.0.0.1会被当成两个不同的主机Port填网页访问的端口80 或 443 或者你自定义的Debugger选 Xdebug。然后勾选Use path mappings在下面的文件树里把本地项目根目录和服务器上的绝对路径对应起来。Docker 场景下这个映射尤其重要。你的项目在宿主机上是/Users/you/project容器里挂在/var/www/html那映射关系就是左边/Users/you/project对应右边/var/www/html。写错一个字母断点就是不生效而且 IDE 不会给你明确的报错只是安静地什么都不做。如果本地直接跑 PHP 内置服务器或者 FPM路径完全相同映射关系就是左右一样但依然要勾上这个选项并写出来因为有些版本的 PhpStorm 在没有映射信息时会拒绝处理断点。3.2 Debug 面板里那几项每一笔都有讲究Settings PHP Debug里最核心的是Debug portXdebug 3 下填 9003和 PHP 端的xdebug.client_port保持一致。这个不一致是最典型的两边都配了但连不上。Can accept external connections这个选项只在 PHP 进程和 IDE 不在同一台机器上时才需要勾——比如 PHP 跑在虚拟机或者容器里。勾上之后 PhpStorm 会监听所有网卡而不只是本地回环容器才能连进来。如果只是本地开发不勾更安全。Force break at first line when no path mapping specified和Force break at first line when a script is outside the project这两个选项的作用是找不到映射关系时先在入口文件停一下方便你确认连接到底通没通。调试初期可以都勾上等映射关系理顺了再取消否则每个请求都会在入口多停一次反而烦。Notify about breakpoints in PHP 4.4 and older之类的老版本兼容选项现代项目直接忽略就行。这个面板里还有一个很多人不知道的功能Validate按钮。点开它会给你一段可直接访问的 URL你把它贴到浏览器里跑一次PhpStorm 会在同一页面上告诉你哪一层出了问题——扩展有没有装、端口通不通、路径映射对不对。配不通的时候这个自检工具比任何猜测都靠谱建议养成习惯先在它这里过一遍。3.3 调试命令行脚本、PHPUnit、后台进程的差异网页请求走的路径是用户访问页面PHP 处理请求时触发调试连接命令行脚本没有这个请求的概念所以触发方式不一样需要用环境变量主动告诉 Xdebug 这次要调试。最直接的方式是在运行命令时带上环境变量XDEBUG_SESSIONPHPSTORM XDEBUG_MODEdebug php artisan some:command或者在php.ini里把xdebug.start_with_request临时改成yes跑完之后再改回来。这个方案简单但容易忘改完忘了改回去会让后面的所有命令执行都变慢因为每个命令都在尝试连接一个可能没开的调试端口。更规范的做法是在 PhpStorm 里建一个PHP Script运行配置勾上Use debugger相关选项PhpStorm 启动进程时会自动注入必要的环境变量和调试标识。PHPUnit 的调试同理在Run Configuration里给 PHPUnit 配置勾上调试器或者直接在测试方法上右键选Debug。后台队列消费进程通常是常驻的调试方式是启动时带上调试标识然后在消费任务的分发代码里打断点任务被消费到的时候就会停下来。注意常驻进程的调试连接只在启动那一刻建立一次调试结束、进程退出之后连接就断了。如果你调试完让进程继续跑它不会反复重连需要重新启动进程才能再次调试。这个特性对长时间运行的服务来说有点反直觉我第一次踩的时候以为是配置坏了。3.4 四种断点各管一类问题行断点是最基本的点一下行号旁边的空白处就出来了。它的局限是每次执行到这一行都会停如果这一行在一个循环里或者被高频调用你按断点继续的手会按到酸。条件断点解决上面的问题。右键断点在Condition里写一个 PHP 表达式比如$orderId 12345或者$user null只有表达式为真时才停下来。这一招在排查某个特定 ID 的数据有问题这类场景时非常高效可以把断点留在公共方法里而不会打扰其它请求。异常断点让你在抛出某类异常的那一刻冻结而不是等到异常被 catch 之后。PhpStorm 里通过Run View Breakpoints在PHP Exception Breakpoints那一栏配置可以按异常类型、按 Notice/Warning 等级来设。排查某个地方抛了一个被吞掉的异常时这个特别有用因为平时你根本看不到它的存在。日志断点是不停下来的断点。右键断点把Suspend取消勾选勾上Evaluate and log写一个表达式比如用户ID: . $userId。执行到这里时程序不会停但会把表达式的结果写进 IDE 的调试控制台。它适合那种我只想看几个关键值但不想打断执行流的场合也能在一定程度上替代临时日志输出而且改起来比改代码快得多。4. 真正连上之后调试界面上每个按钮在做什么第一次成功命中断点时面对工具栏那一排图标人是懵的。这一节把每个按钮的含义讲清楚配合我自己的使用顺序让第一步不至于卡在界面上。调试窗口一般由几个面板组成左侧是Frames也就是调用栈中间是Variables当前作用域的变量右侧是Watches你手动加的监控表达式。工具栏在顶部控制执行流程。理解这几个面板的定位之后操作逻辑就顺了你在某个栈帧上看这个帧里的变量改一改然后决定下一步往哪走。需要提醒一个心态上的事调试工具的价值不是能停下来而是能任意回看和前进。停下来只是手段真正解决问题靠的是你在各个栈帧之间来回切换、观察状态变化的能力。如果只是单纯停车看两眼还不如直接打印。4.1 单步工具栏每个图标对应什么操作先把默认快捷键列出来这些键位是可以改的但默认值用熟了之后效率很高操作默认快捷键含义Step OverF8执行当前行遇到函数调用不进入Step IntoF7执行当前行遇到函数调用进入函数体Step OutShift F8执行完当前函数并跳出到调用方Force Step IntoAlt Shift F7进入包括内部函数在内的所有调用Run to CursorAlt F9一直执行到光标所在行Resume ProgramF9继续执行直到下一个断点Evaluate ExpressionAlt F8打开表达式求值窗口Step Over和Step Into的区别是使用频率最高的一对。当前行如果是个方法调用你关心这个方法内部怎么走的用Step Into你不关心只想看它返回什么用Step Over。判断标准很简单这个方法是框架代码或者第三方库Step Over是你自己写的、正在怀疑的代码Step Into。Force Step Into值得单独说。默认情况下PHP 的一些内部函数比如数组处理函数是进不去的按Step Into会直接跳过。用这个强制进入的变体可以钻进一些平时进不去的地方排查那些看起来没问题但结果不对的内置行为时很有用。Run to Cursor是我用得最多的一个。在函数末尾打一个断点然后按这个键程序会一路执行到那里如果中间出了错或者卡住你立刻就知道问题在中间某处。这招适合快速确认这一段代码跑下来了没有比一步步按Step Over高效得多。4.2 Variables、Watches 和 Evaluate Expression 的实战用法Variables面板显示的是当前栈帧作用域里的所有变量展开对象可以看到它的属性展开数组可以看到每一项。有个很好用的细节右键一个变量可以选Set Value直接改掉它的值然后继续执行。这在测试边界条件时特别好用比如你想看看$count为 0 时的行为不用去构造数据直接在面板里改成 0继续执行就看到了结果。Watches面板用来挂你关心的表达式。它和Variables的区别是后者只能看你当前帧里已经存在的变量前者可以写任意表达式包括调用方法。比如你想持续观察$order-getTotal()的值变化就把它加到 Watch 列表里每次停下来它都会重新求值。注意这个方法会被真实调用如果方法有副作用比如写日志、修改状态观察本身会影响程序行为这一点要留心。Evaluate ExpressionAlt F8是最强的一个。它打开了当前执行上下文你可以在里面写任意合法的 PHP 代码并立刻执行。常见的用法是临时调一个方法看看返回什么$repository-findById(1)或者拼一个字符串看看输出或者检查isset($array[key])。它相当于把你临时打印的需求现场解决掉用完就走不留在代码里。我经常用它来验证是不是这个查询没查到数据比在代码里加一行日志再跑一遍快得多。提示Evaluate Expression里执行的方法是有副作用的别在里面调删除、写入这类方法。观察行为不等于安全行为。4.3 调用栈 Frames 和并发请求下的调试Frames面板展示的是当前请求的调用链从最外层一直排到当前停下来的一帧。每一帧都记录着当时这个函数里的局部变量你点任意一帧Variables面板就会切换成那一帧的状态。这是排查参数是在哪一层被改坏的最有效的手段从当前帧往上点一层层看参数怎么变的很快就能定位到出问题的那一次修改。有个操作容易忽略Drop Frame不是所有场景都可用通常出现在较新的 PhpStorm 里。它的作用是把当前栈帧丢掉让执行回到调用方相当于给了一次重来的机会。配合Set Value使用可以在不改代码的情况下把参数改对重新走一遍前面的逻辑。这个能力在调试复杂条件分支时能省下大量重启请求的时间。并发场景下的调试要单独说。假设你调试的是一个队列消费进程同时跑了多个 worker或者你在浏览器里同时开多个标签访问页面那么调试连接会是多个。PhpStorm 会为每个连接开一个单独的会话标签页你在顶部标签栏能看到它们。这时候要注意别在错误的会话里操作也不要同时让多个任务停在断点上——它们会互相占着连接让排查变得混乱。我的习惯是并发调试时把 worker 数量临时降到 1跑通逻辑之后再恢复。4.4 用日志断点做到看得见但不打断前面提到过日志断点这里展开讲一下它的实战价值。有些代码路径不能停一停请求就超时或者一停就把整条链路堵死了。这种情况下日志断点几乎是唯一的选择。配置方式是在断点上右键取消Suspend勾上Evaluate and log然后在输入框里写你要输出的表达式。比如你想确认某个循环跑了多少次、每次的参数是什么可以写第 . $i . 次参数 . json_encode($params, JSON_UNESCAPED_UNICODE)。程序继续跑输出会出现在Debugger Console面板里按时间顺序排列。它比临时写日志强在哪强在不用改代码、不用重新请求、改表达式只是改一个输入框。而且它输出的内容会带调用栈信息你能看到这一次输出是从哪一行、哪个函数发出来的。缺点是它只在调试会话活着的时候有效会话结束日志也就没了。如果要把结果留下来得用Watches或者干脆写文件。我通常的用法是先用日志断点快速扫一遍数据流大概定位到问题区间后再在那里加一个会停下来的行断点精查。两步走比一上来就单步跟踪效率高得多。5. 出问题了怎么查一条从外到内的排查链路配置这种东西一次成功的概率不高大部分时间花在排查上。这一节我要强调的重点不是最终答案是什么而是按什么顺序查。顺序对了通常三五个动作就能定位到问题层顺序不对会在无关的地方反复折腾。排查的核心思路是二分法先确定问题在PHP 端还是IDE 端再在确定的那一半里继续二分。判断问题在哪一端最快的办法就是看xdebug.log日志里有尝试连接的记录说明 PHP 端一切正常问题在连接对接或者 IDE 侧日志里什么都没有说明请求压根没触发调试问题在触发条件上。5.1 点了调试没有任何反应按这个顺序查第一步看 PhpStorm 工具栏上的电话图标有没有变绿。这个图标控制是否监听调试连接没点开的话请求送过来也没人接。这是最常被忽略的一步因为它不在设置面板里而在主界面上。第二步确认端口一致。PHP 端的xdebug.client_port和 IDE 的Debug port必须一样Xdebug 3 的约定值是 9003。顺手可以确认一下这个端口没有被别的进程占用用系统命令查一下比较放心。第三步确认触发条件。如果xdebug.start_with_requesttrigger那请求里必须带XDEBUG_SESSION参数或者 Cookie。浏览器插件装了吗插件的 idekey 设成 PHPSTORM 了吗临时验证可以手动在 URL 后面加?XDEBUG_SESSIONPHPSTORM能看到断点命中就说明插件那边有问题。第四步看xdebug.log。日志里出现Tried to connect之类的记录但 IDE 没反应问题在 IDE 侧日志里只有请求记录没有连接尝试说明触发条件没满足日志文件压根没生成说明xdebug.log的路径不可写或者扩展没加载。第五步用 PhpStorm 自带的Validate工具过一遍。它会逐项告诉你哪里不对比自己瞎猜快。把这五步做完绝大多数点了没反应的情况都能定位到具体位置。5.2 断点命中了但停在了意料之外的文件里这种情况通常有两个原因。最常见的是路径映射配错PHP 报的路径和 IDE 项目的路径没能正确对应IDE 就退而求其次停到了它能找到的位置通常是入口文件。判断方式是看停下来时Frames面板里的文件路径如果是index.php的第一行八成就是映射问题。第二个原因是断点打在了 vendor 目录或者框架的自动加载代码里而这些位置在很多配置下会被 IDE 忽略。PhpStorm 会在断点上显示一个小提示说明它被当作不在项目内的断点处理了。解决办法是在Settings PHP Debug里调整是否忽略未映射的断点这个选项或者明确地把 vendor 目录加进项目路径。还有一种隐蔽的情况是 opcache 缓存。某些环境下修改了代码但 opcache 没刷新调试时看到的行号和实际执行的行对不上表现为断点停的位置比源码偏了几行。如果遇到位置诡异的情况先把 opcache 关了或者清一次再说。注意Force break at first line这类选项在调试初期很有用但它会让你每个请求都先停在入口如果发现每次都停在 index.php先去检查是不是这个选项开着。5.3 请求变慢、超时甚至 502Xdebug 对性能有影响这是既定事实但正常的调试不应该慢到超时的程度。如果开了调试之后请求明显变慢或者直接报网关错误通常是几个原因叠加。一个原因是 PHP 端在尝试连接一个没在监听的调试端口每次连接都要等超时。Xdebug 有个连接超时配置xdebug.connect_timeout_ms默认值不算大但如果每个请求都等一次累积起来也很可观。解决方式就是不要把xdebug.start_with_request设成yes而不在 IDE 里监听用trigger模式按需开启。第二个原因是xdebug.mode里开了develop或者coverage这种对性能影响更大的模式。排查一下是不是忘了关。第三个原因出现在容器或者远程场景网络往返本身有延迟加上调试连接的握手整体响应时间被拉长。这种情况下网关比如 Nginx 的fastcgi_read_timeout的超时值可能需要临时调大但这是临时措施根本还是要缩短调试会话的持续时间把断点打在更精确的位置。我踩过最典型的一次是队列消费任务开了调试之后任务处理时间从几百毫秒涨到十几秒原因是每个任务处理时都在尝试连接一个已经关掉的调试会话等超时。后来改成只在需要调试时通过环境变量临时打开问题就没了。5.4 容器、WSL 和远程主机里的调试要点本地直跑是最简单的情况路径一致、网络回环、没什么坑。一旦进了容器或者虚拟环境就多出网络怎么打通和路径怎么映射两件麻烦事。容器场景下PHP 跑在容器里本机指的是容器自己127.0.0.1指向的是容器内部而 IDE 跑在宿主机上所以xdebug.client_host必须指向宿主机的地址。Docker Desktop 环境macOS 和 Windows通常可以用一个内置的主机名来指代宿主机Linux 上则需要用容器的默认网关地址或者启动容器时加上 host 映射参数。同时 PhpStorm 那边要把Can accept external connections勾上让它监听所有网卡。路径映射在容器场景下是必配项因为项目在容器里的路径和宿主机的路径不一样前面已经详细说过。比较稳妥的做法是把映射关系写清楚之后用Validate工具跑一遍确认。WSL2 场景下PHP 通常在 WSL 里跑IDE 在 Windows 侧跑。xdebug.client_host需要指向 Windows 主机的地址这个地址在 WSL 的 DNS 配置里能查到。另外要注意 Windows 防火墙可能拦截来自 WSL 的连接调试连不上的时候先排除这个因素。远程主机场景下通常的做法是通过连接转发把远端端口映射到本地让client_host看起来还是127.0.0.1。这样配置最简单网络链路也最可控。具体用什么工具做端口映射不重要重要的是理解让 Xdebug 认为调试器就在本地这个思路。6. 把调试环境管起来别让它反过来拖慢你调试能力配好之后怎么管理它就成了新问题。我见过不少人因为图方便把调试相关的配置在生产环境也留着结果性能下滑一大截还找不到原因。这一节聊几个习惯都是踩过坑之后形成的。核心原则是调试能力应该是按需开启、用完即关的而不是一直开着、随时可用。前者对性能影响可控后者等于给每个请求都背了一个包袱。6.1 生产环境里的调试扩展代价比你想的大Xdebug 挂载在 Zend 引擎上每次函数调用、每次变量赋值它都有机会介入即使你只是开了最基本的模式性能开销也是实打实的。开了debug模式而没人在监听时每个请求都要等一次连接超时开了coverage模式跑测试执行时间翻几倍是常见的。生产环境一旦不小心留着这些东西表现就是服务器没改什么但响应就是在变慢而且很难通过看代码发现。我的做法是从环境镜像层面就不装调试扩展。开发用的镜像和线上用的镜像分开构建需要调试的时候在开发镜像里加上。如果因为某些原因必须在同一套镜像上切换那就用环境变量控制把它作为启动参数的一部分而不是写死在配置文件里。还有一种情况是排查线上问题不得不开调试。这时候的原则是只在单台实例上临时开、只开搭好触发条件的trigger模式、排查完立刻关掉、同时盯紧这台实例的响应时间。整个过程要当成一次有风险的操作来对待而不是随手改个配置。6.2 用环境变量和触发参数控制开关XDEBUG_MODE这个环境变量可以覆盖php.ini里的xdebug.mode设置这是容器化环境里控制调试开关最灵活的方式。启动容器时不传这个变量Xdebug 就是常规模式需要调试时把XDEBUG_MODEdebug传进去重启容器调试能力就打开了。这样配置文件本身不用改切换只体现在启动命令上不容易忘。同理XDEBUG_SESSION环境变量是给命令行场景用的跑脚本时带上它这次执行就会尝试连接调试器。这比临时改php.ini再改回来干净得多。浏览器场景下触发的方式主要是 Cookie 和 URL 参数。开发插件负责在前端自动加上触发标识我一般把插件的默认状态设成关闭需要调试的页面手动点开。这样日常浏览不会触发调试连接只有明确要调的时候才开。听起来是多了一步操作但省下来的是每个请求都在等连接的等待时间。提示如果你在团队里维护开发容器可以在文档里写清楚怎么打开调试和记得关掉比每个人都记住一堆配置项靠谱。6.3 用日志断点和 xdebug.log 留下排查证据调试会话是短暂的你从断点里看到的信息在关闭会话之后就没了。有些问题需要事后分析这时候要么把关键值写进日志要么用日志断点把输出落到能留存的地方。xdebug.log本身是排查配置问题用的但它偶尔也能帮你确认某个请求到底有没有触发调试连接有没有被拒绝这些信息在排查配置问题时非常直接。日志级别调高一点能看到更详细的连接过程包括握手、失败原因等。平时可以把它关掉减少 I/O需要排查时再打开。日志断点的输出除了出现在调试控制台也可以配置成写到文件。对于那种跑一次要很久、中途不能停的任务把感兴趣的变量用日志断点输出到文件跑完之后再分析比反复重跑要省时间。我处理大批量数据脚本时基本都用这个套路先在几个关键节点布上日志断点跑一轮拿到数据流再根据数据流的异常点缩小范围最后才用行断点精查。这套流程走下来调试不再是出问题了才手忙脚乱去配而是工具箱里随时能拿出来的一个常规手段。配一次、管好、按需开关剩下的时间就可以留给真正的问题本身了。我自己后来把这些配置整理成了一个开发容器的启动脚本新同事入职直接拉起来就能用省掉了每个人各自踩一遍坑的过程。
返回列表