上一章 文档首页 下一章


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

温馨提示:以下内容基于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秒的,重点优化;

搞定,收工,开饭!


上一章 文档首页 下一章

还有疑问?欢迎到社区提问!

Fork me on GitHub