本章主要讲解调试模式的相关操作,如何快速排查和定位开发过程中所遇到的问题和线上发生的故障。
温馨提示:以下内容基于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
字段,里面有相关的调试信息。如下所示:
{
"ret": 200,
"data": {
},
"msg": "",
"debug": {
"stack": [ // 自定义埋点信息
],
"sqls": [ // 全部执行的SQL语句
]
}
}
在发生异常时,最初的框架的处理方式是直接报500错误。现在调整为,当开启调试模式后,会将发生的异常转换为对应的结果按结果格式返回,即其结构会变成以下这样:
{
"ret": 0, // 异常时的错误码
"data": [],
"msg": "", // 异常时的错误信息
"debug": {
"exception": [ // 异常时的详细堆栈信息
],
"stack": [ // 自定义埋点信息
],
"sqls": [ // 全部执行的SQL语句
]
}
}
(1) debug.sqls全部执行的SQL语句
所执行的全部SQL语句,会由框架自动搜集并统计。最后显示的信息格式是:
[序号 - 当前SQL的执行时间ms]所执行的SQL语句及参数列表
示例:
[1 - 0.32ms]SELECT * FROM tbl_user WHERE (id = ?); -- 1
表示是第一条执行的SQL语句,消耗了0.32毫秒,SQL语句是SELECT * FROM tbl_user WHERE (id = ?);
,其中参数是1。
又如,假设我们编写一个这样获取用户总人数的示例接口。
// $ vim ./Demo/Api/User.php
class Api_User extends PhalApi_Api {
public function amount() {
return DI()->notorm->user->count('id');
}
}
然后通过/?service=User.Amount&debug=1
请求该接口,可以看到类似这样的返回结果。
{
"ret": 200,
"data": "49", // 共49个用户
"msg": "",
"debug": {
"stack": [
"[#0 - 0ms]/home/apps/projects/PhalApi/Public/index.php(6)"
],
"sqls": [
"[1 - 0.3ms]SELECT COUNT(id) FROM tbl_user;" // 本次所执行的SQL语句
]
}
}
温馨提示:这只是一个示例。实际项目中,不推荐直接在Api直接操作数据库,也不推荐返回非数组格式的data。
(2) debug.stack自定义埋点信息
埋点信息的格式如下:
[#序号 - 距离最初节点的执行时间ms - 节点标识]代码文件路径(文件行号)
示例:
[#0 - 0ms]/home/apps/projects/PhalApi/Public/index.php(6)
表示,这是第一个埋点(由框架自行添加),执行时间为0毫秒,所在位置是文件/home/apps/projects/PhalApi/Public/index.php
的第6行。即第一条的埋点发生在框架初始化时:
// $ vim ./Public/init.php
if (DI()->debug) {
// 启动追踪器
DI()->tracer->mark();
}
与SQL语句的调试信息不同的是,自定义埋点则需要开发人员根据需要自行纪录,可以使用全球追踪器DI()->tracer
进行纪录,其使用如下:
// 添加纪录埋点
DI()->tracer->mark();
// 添加纪录埋点,并指定节点标识
DI()->tracer->mark('DO_SOMETHING');
通过上面方法,可以对执行经过的路径作标记。你可以指定节点标识,也可以不指定。对一些复杂的接口,可以在业务代码中添加这样的埋点,追踪接口的响应时间,以便进一步优化性能。当然,更专业的性能分析工具推荐使用XHprof。
参考资料:XHprof扩展类库
继续上面的示例,在进行数据库操作前后,我们添加相应的操作埋点。
public function amount() {
DI()->tracer->mark('开始读取数据库');
$rs = DI()->notorm->user->count('id');
DI()->tracer->mark('读取完毕');
return $rs;
}
再次请求,会看到类似以下的返回结果。
{
"ret": 200,
"data": "49",
"msg": "",
"debug": {
"stack": [
"[#0 - 0ms]/home/apps/projects/PhalApi/Public/index.php(6)",
"[#1 - 5.5ms - 开始读取数据库]/home/apps/projects/PhalApi/Demo/Api/User.php(74)",
"[#2 - 6.4ms - 读取完毕]/home/apps/projects/PhalApi/Demo/Api/User.php(78)"
],
"sqls": [
"[1 - 0.3ms]SELECT COUNT(id) FROM tbl_user;"
]
}
}
可以看出,在“开始读取数据库”前消耗了5.5毫秒,以及相关的代码位置。
(3) debug.exception异常堆栈信息
当有未能捕捉的接口异常时,开启调试模式后,框架会把对应的异常转换成对应的返回结果,而不是像最初那样直接500,页面空白。这些是由框架自动处理的。
继续上面的示例,让我们故意制造一些麻烦,手动抛出一个异常。
public function amount() {
... ...
throw new Exception('这是一个演示异常调试的示例', 501);
return $rs;
}
再次请求后,除了上面的SQL语句和自定义埋点信息外,还会看到这样的异常堆栈信息。
{
"ret": 501,
"data": [],
"msg": "这是一个演示异常调试的示例",
"debug": {
"exception": [
{
"function": "amount",
"class": "Api_User",
"type": "->",
"args": []
},
... ...
],
"stack": [
... ...
],
"sqls": [
... ...
]
}
}
然后便可根据返回的异常信息进行排查定位问题。
(4) 添加自定义调试信息
当需要添加其他调试信息时,可以使用DI()->response->setDebug()
进行添加。
如:
$x = 'this is x';
$y = array('this is y');
DI()->response->setDebug('x', $x);
DI()->response->setDebug('y', $y);
请求后,可以看到:
"debug": {
"x": "this is x",
"y": [
"this is y"
]
}
2.13.3 一个错误的接口开发
有时,在进行接口开发时,会需要进行批量获取的功能,如列表。但很多开发的同学可能会因为时间赶或者没有意识去对SQL查询进行优化,或者甚至不知道自己的接口背后隐藏着多少问题。下面是一个错误的开发示例。
(1)新增的批量获取接口
假设我们在开发一个国际的项目,并且运行良好,BOSS说因业务需要,要加多一个接口以支持批量获取用户的基本信息,提供给国外某知名的社交平台调用。
于是乎,我们很快就根据原来的单个获取接口实现了新的接口:
//$vim ./Demo/Api/User.php
<?php
class Api_User extends PhalApi_Api {
public function getRules() {
return array(
//...
'getMultiBaseInfo' => array(
'user_ids' => array('name' => 'user_ids', 'type' => 'array', 'format' => 'explode', 'require' => true),
),
);
}
//...
public function getMultiBaseInfo() {
$rs = array('code' => 0, 'msg' => '', 'list' => array());
$domain = new Domain_User();
foreach ($this->user_ids as $userId) {
$rs['list'][] = $domain->getBaseInfo($userId);
}
return $rs;
}
}
(2)运行调用一下
显然,我们可以很清楚地调用新增的接口:
http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&user_ids=1,2,3
可返回:
{
"ret": 200,
"data": {
"code": 0,
"msg": "",
"list": [
{
"id": "1",
"name": "dogstar",
"note": "oschina"
},
{
"id": "2",
"name": "Tom",
"note": "USA"
},
{
"id": "3",
"name": "King",
"note": "game"
}
]
},
"msg": ""
}
假设我们已经有了这样的数据库表数据:
INSERT INTO `tbl_user` VALUES ('1', 'dogstar', 'oschina');
INSERT INTO `tbl_user` VALUES ('2', 'Tom', 'USA');
INSERT INTO `tbl_user` VALUES ('3', 'King', 'game');
(3)这样的问题?
这样的问题,在对外黑盒调用的客户端同学是发现不了的,对于测试人员来说也是无法感知的。但所犯的错误也是显然易见的,就是没有进行SQL的批量查询优化,造成了很多不必要的重复查询。这里,根据前面学习的调试方式,则我们可以快速发现存在的问题:
http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&user_ids=1,2,3&__debug__=1
如下返回,我们看到了很多重复类似的查询语句。
{
... ...
"debug": {
.... ...
"sqls": [
"[1 - 0.34ms]SELECT * FROM tbl_user WHERE (id = ?); -- 1",
"[2 - 0.16ms]SELECT * FROM tbl_user WHERE (id = ?); -- 2",
"[3 - 0.16ms]SELECT * FROM tbl_user WHERE (id = ?); -- 3"
]
}
}
(4)如何改进?
这是一个很基本的问题,当然在实际项目中不会普通存在,这里只是作为一个示例加以说明。但让人失望的是,实际项目确实存在为数不少的这样的情况。可能是新人的技术和意识问题,也有可能是老同学的态度问题。所以,优化这么一个接口的批量SQL查询不难,难的是如何才能让新、老同学都注重这块的SQL查询优化呢?而不是等到线上服务器异常崩溃后再来推托责任。具体的代码改进,留给读者自己实践了。毕竟,看了,实践了,才会真正深刻地掌握。
2.13.4 由此引申
- 这里不专门讲述SQL的优化,但也顺便提供一些SQL查询优化的建议:
- 使用批量查询,而不是N次循环查询!
- 重复的数据,不要重复获取;
- 根据需要,按需要获取表字段,而不是SELECT *;
- 针对频繁的搜索字段,建立必要的索引,以加快查询速度;
- 使用关联查询,而不是粗暴地类似:where uid IN (… 这里是成千上W个用户ID …);
- 针对单条SQL语句执行时间超过1秒的,重点优化;
搞定,收工,开饭!