本章主要讲解调试模式的相关操作,如何快速排查和定位开发过程中所遇到的问题和线上发生的故障。

温馨提示:以下内容基于PhalApi 1.4.0 版本及以上。

2.13.1 开启调试调试

开启调试模式很简单,主要有两种方式:

  • 单次请求开启调试:默认添加请求参数&debug=1
  • 全部请求开启调试:把配置文件./Config/sys.php文件中的配置改成'debug' => true,
    请特别注意,在实际项目中,调试参数不应使用默认的调试参数,而应各自定义,使用更复杂的参数,从而减少暴露敏感或者调试信息的风险。例如:

  • 不推荐的做法:&debug=1

  • 一般的做法:&phalapi_debug=1
  • 更好的做法:&phalapi_debug=202cb962ac59075b964b07152d234b70

    2.13.2 调试信息有哪些?

温馨提示:调试信息仅有当在开启调试模式后,才会返回。

正常响应的情况下,当开启调试模式后,会返回多一个debug字段,里面有相关的调试信息。如下所示:

  1. {
  2. "ret": 200,
  3. "data": {
  4. },
  5. "msg": "",
  6. "debug": {
  7. "stack": [ // 自定义埋点信息
  8. ],
  9. "sqls": [ // 全部执行的SQL语句
  10. ]
  11. }
  12. }

在发生异常时,最初的框架的处理方式是直接报500错误。现在调整为,当开启调试模式后,会将发生的异常转换为对应的结果按结果格式返回,即其结构会变成以下这样:

  1. {
  2. "ret": 0, // 异常时的错误码
  3. "data": [],
  4. "msg": "", // 异常时的错误信息
  5. "debug": {
  6. "exception": [ // 异常时的详细堆栈信息
  7. ],
  8. "stack": [ // 自定义埋点信息
  9. ],
  10. "sqls": [ // 全部执行的SQL语句
  11. ]
  12. }
  13. }

(1) debug.sqls全部执行的SQL语句

所执行的全部SQL语句,会由框架自动搜集并统计。最后显示的信息格式是:

  1. [序号 - 当前SQL的执行时间ms]所执行的SQL语句及参数列表

示例:

  1. [1 - 0.32ms]SELECT * FROM tbl_user WHERE (id = ?); -- 1

表示是第一条执行的SQL语句,消耗了0.32毫秒,SQL语句是SELECT * FROM tbl_user WHERE (id = ?);,其中参数是1。

又如,假设我们编写一个这样获取用户总人数的示例接口。

  1. // $ vim ./Demo/Api/User.php
  2. class Api_User extends PhalApi_Api {
  3. public function amount() {
  4. return DI()->notorm->user->count('id');
  5. }
  6. }

然后通过/?service=User.Amount&debug=1请求该接口,可以看到类似这样的返回结果。

  1. {
  2. "ret": 200,
  3. "data": "49", // 共49个用户
  4. "msg": "",
  5. "debug": {
  6. "stack": [
  7. "[#0 - 0ms]/home/apps/projects/PhalApi/Public/index.php(6)"
  8. ],
  9. "sqls": [
  10. "[1 - 0.3ms]SELECT COUNT(id) FROM tbl_user;" // 本次所执行的SQL语句
  11. ]
  12. }
  13. }
温馨提示:这只是一个示例。实际项目中,不推荐直接在Api直接操作数据库,也不推荐返回非数组格式的data。

(2) debug.stack自定义埋点信息

埋点信息的格式如下:

  1. [#序号 - 距离最初节点的执行时间ms - 节点标识]代码文件路径(文件行号)

示例:

  1. [#0 - 0ms]/home/apps/projects/PhalApi/Public/index.php(6)

表示,这是第一个埋点(由框架自行添加),执行时间为0毫秒,所在位置是文件/home/apps/projects/PhalApi/Public/index.php的第6行。即第一条的埋点发生在框架初始化时:

  1. // $ vim ./Public/init.php
  2. if (DI()->debug) {
  3. // 启动追踪器
  4. DI()->tracer->mark();
  5. }

与SQL语句的调试信息不同的是,自定义埋点则需要开发人员根据需要自行纪录,可以使用全球追踪器DI()->tracer进行纪录,其使用如下:

  1. // 添加纪录埋点
  2. DI()->tracer->mark();
  3. // 添加纪录埋点,并指定节点标识
  4. DI()->tracer->mark('DO_SOMETHING');

通过上面方法,可以对执行经过的路径作标记。你可以指定节点标识,也可以不指定。对一些复杂的接口,可以在业务代码中添加这样的埋点,追踪接口的响应时间,以便进一步优化性能。当然,更专业的性能分析工具推荐使用XHprof。

参考资料:XHprof扩展类库

继续上面的示例,在进行数据库操作前后,我们添加相应的操作埋点。

  1. public function amount() {
  2. DI()->tracer->mark('开始读取数据库');
  3. $rs = DI()->notorm->user->count('id');
  4. DI()->tracer->mark('读取完毕');
  5. return $rs;
  6. }

再次请求,会看到类似以下的返回结果。

  1. {
  2. "ret": 200,
  3. "data": "49",
  4. "msg": "",
  5. "debug": {
  6. "stack": [
  7. "[#0 - 0ms]/home/apps/projects/PhalApi/Public/index.php(6)",
  8. "[#1 - 5.5ms - 开始读取数据库]/home/apps/projects/PhalApi/Demo/Api/User.php(74)",
  9. "[#2 - 6.4ms - 读取完毕]/home/apps/projects/PhalApi/Demo/Api/User.php(78)"
  10. ],
  11. "sqls": [
  12. "[1 - 0.3ms]SELECT COUNT(id) FROM tbl_user;"
  13. ]
  14. }
  15. }

可以看出,在“开始读取数据库”前消耗了5.5毫秒,以及相关的代码位置。

(3) debug.exception异常堆栈信息

当有未能捕捉的接口异常时,开启调试模式后,框架会把对应的异常转换成对应的返回结果,而不是像最初那样直接500,页面空白。这些是由框架自动处理的。

继续上面的示例,让我们故意制造一些麻烦,手动抛出一个异常。

  1. public function amount() {
  2. ... ...
  3. throw new Exception('这是一个演示异常调试的示例', 501);
  4. return $rs;
  5. }

再次请求后,除了上面的SQL语句和自定义埋点信息外,还会看到这样的异常堆栈信息。

  1. {
  2. "ret": 501,
  3. "data": [],
  4. "msg": "这是一个演示异常调试的示例",
  5. "debug": {
  6. "exception": [
  7. {
  8. "function": "amount",
  9. "class": "Api_User",
  10. "type": "->",
  11. "args": []
  12. },
  13. ... ...
  14. ],
  15. "stack": [
  16. ... ...
  17. ],
  18. "sqls": [
  19. ... ...
  20. ]
  21. }
  22. }

然后便可根据返回的异常信息进行排查定位问题。

(4) 添加自定义调试信息

当需要添加其他调试信息时,可以使用DI()->response->setDebug()进行添加。

如:

  1. $x = 'this is x';
  2. $y = array('this is y');
  3. DI()->response->setDebug('x', $x);
  4. DI()->response->setDebug('y', $y);

请求后,可以看到:

  1. "debug": {
  2. "x": "this is x",
  3. "y": [
  4. "this is y"
  5. ]
  6. }

2.13.3 一个错误的接口开发

有时,在进行接口开发时,会需要进行批量获取的功能,如列表。但很多开发的同学可能会因为时间赶或者没有意识去对SQL查询进行优化,或者甚至不知道自己的接口背后隐藏着多少问题。下面是一个错误的开发示例。

(1)新增的批量获取接口

假设我们在开发一个国际的项目,并且运行良好,BOSS说因业务需要,要加多一个接口以支持批量获取用户的基本信息,提供给国外某知名的社交平台调用。

于是乎,我们很快就根据原来的单个获取接口实现了新的接口:

  1. //$vim ./Demo/Api/User.php
  2. <?php
  3. class Api_User extends PhalApi_Api {
  4. public function getRules() {
  5. return array(
  6. //...
  7. 'getMultiBaseInfo' => array(
  8. 'user_ids' => array('name' => 'user_ids', 'type' => 'array', 'format' => 'explode', 'require' => true),
  9. ),
  10. );
  11. }
  12. //...
  13. public function getMultiBaseInfo() {
  14. $rs = array('code' => 0, 'msg' => '', 'list' => array());
  15. $domain = new Domain_User();
  16. foreach ($this->user_ids as $userId) {
  17. $rs['list'][] = $domain->getBaseInfo($userId);
  18. }
  19. return $rs;
  20. }
  21. }

(2)运行调用一下

显然,我们可以很清楚地调用新增的接口:

  1. http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&user_ids=1,2,3

可返回:

  1. {
  2. "ret": 200,
  3. "data": {
  4. "code": 0,
  5. "msg": "",
  6. "list": [
  7. {
  8. "id": "1",
  9. "name": "dogstar",
  10. "note": "oschina"
  11. },
  12. {
  13. "id": "2",
  14. "name": "Tom",
  15. "note": "USA"
  16. },
  17. {
  18. "id": "3",
  19. "name": "King",
  20. "note": "game"
  21. }
  22. ]
  23. },
  24. "msg": ""
  25. }

假设我们已经有了这样的数据库表数据:

  1. INSERT INTO `tbl_user` VALUES ('1', 'dogstar', 'oschina');
  2. INSERT INTO `tbl_user` VALUES ('2', 'Tom', 'USA');
  3. INSERT INTO `tbl_user` VALUES ('3', 'King', 'game');

(3)这样的问题?

这样的问题,在对外黑盒调用的客户端同学是发现不了的,对于测试人员来说也是无法感知的。但所犯的错误也是显然易见的,就是没有进行SQL的批量查询优化,造成了很多不必要的重复查询。这里,根据前面学习的调试方式,则我们可以快速发现存在的问题:

  1. http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&user_ids=1,2,3&__debug__=1

如下返回,我们看到了很多重复类似的查询语句。

  1. {
  2. ... ...
  3. "debug": {
  4. .... ...
  5. "sqls": [
  6. "[1 - 0.34ms]SELECT * FROM tbl_user WHERE (id = ?); -- 1",
  7. "[2 - 0.16ms]SELECT * FROM tbl_user WHERE (id = ?); -- 2",
  8. "[3 - 0.16ms]SELECT * FROM tbl_user WHERE (id = ?); -- 3"
  9. ]
  10. }
  11. }

(4)如何改进?

这是一个很基本的问题,当然在实际项目中不会普通存在,这里只是作为一个示例加以说明。但让人失望的是,实际项目确实存在为数不少的这样的情况。可能是新人的技术和意识问题,也有可能是老同学的态度问题。所以,优化这么一个接口的批量SQL查询不难,难的是如何才能让新、老同学都注重这块的SQL查询优化呢?而不是等到线上服务器异常崩溃后再来推托责任。具体的代码改进,留给读者自己实践了。毕竟,看了,实践了,才会真正深刻地掌握。

2.13.4 由此引申

  • 这里不专门讲述SQL的优化,但也顺便提供一些SQL查询优化的建议:
  • 使用批量查询,而不是N次循环查询!
  • 重复的数据,不要重复获取;
  • 根据需要,按需要获取表字段,而不是SELECT *;
  • 针对频繁的搜索字段,建立必要的索引,以加快查询速度;
  • 使用关联查询,而不是粗暴地类似:where uid IN (… 这里是成千上W个用户ID …);
  • 针对单条SQL语句执行时间超过1秒的,重点优化;

搞定,收工,开饭!

原文: https://www.phalapi.net/wikis/2-13.html