调用链跟踪系统的首秀—查找打印配置未正确加载的bug

背景

问题

连续两天都有商家反馈无法打印,远程查看后都发现打印配置加载的有问题,以前设置好的打印机都没显示出来——显示的是 store 级别的打印配置(打印配置从store细化到entity_store后,如果请求没有entity_store_id,会去查找store级别的打印配置)。

思路

调用链跟踪系统,记录了每次api请求的url、params、返回数据、时间戳、ua、x-forwarded-for、埋点标记 等有用信息,足够我们用来回溯当时的程序运行路径,展示当时程序的部分状态。

操作

  1. 首先从庞大的日志里筛选出需要的所有调用链;
    1.1 根据商家反馈,早上从未成功打印过,我们先确定了一个较小的时间范围:从商家第一次登陆,到打印配置获取完成;
    1.2 我们先在记录里找商家store_id第一次出现的位置,再根据这条记录的ua找到了商家ip,根据此ip筛选出第一批数据,也找到了大概的时间范围;
    1.3 把所有记录复制出来,分组排序(按TraceId分组,组内按RpcId排序,组间按每组内第一条记录的At排序),方便分析;
    1.4 根据TraceId和RpcId补足中间缺失的记录(RpcId是有序的,中间缺失可以看出来)
    1.5 最终筛选出来106条记录,时间跨度约5秒,其中seller端的cash/get/lists接口产生了11个Api调用,相关记录就占了46条);
  2. 找到“获取打印配置”这个接口的请求参数和返回值,发现接口传参不正确,获取到的数据也不正确;
  3. 结合记录和代码,推测程序运行路径,找出问题的原因。

效果

最后找出来一个bug:

check接口和返回值:


PS: 接口没返回entity_store_id,后面却使用了entity_store_id去获取打印配置。

结语

  1. 最终张跃还在js端找出一个bug:打印配置的获取 写在了 setCookie方法 前面,导致获取配置的时候没有取到ntity_store_id(所以也没传)。
  2. 打印配置的获取有三个地方:打印js加载后自动获取、doLogin时获取、checkLogin时获取,所以出bug后也只有部分商户无法打印。
  3. 调用链跟踪记录查阅起来还不是很方便,主要问题有:
    3.1 商家没有固定的身份标识,在记录里难以找出所有的商家相关记录;
    3.2 日志记录无序,还需要自己排一遍序;
    3.3 如果能像数据库一样select筛选数据更好;
  4. 针对上面的问题,分别有一些解决方法:
    4.1 在客户端里获取商家电脑的硬件信息,作为商家的身份标识;
    4.2 日志记录排好序再写到log里,可以保证组内绝对有序,组间相对有序;
    4.3 找一些开源工具。。

mysql 如何恢复数据?

问题:

尝试还原数据库,之后提示 table doesn`t exist。

分析:

类型:MyISAM
数据:Table.frm,Table.MYD,Table.MYI
位置:/data/$databasename/目录中
说明:直接复制到mysql中data目录中,便可以使用

类型:InnoDB
数据文件:存储在/$innodb_data_home_dir/中的ibdata1文件中
结构文件:结构文件存在于/data/table_name.frm中
说明:不可以直接使用,并报错:table doesn`t exist

解决方法:

1、 停止 apache 和 mysql服务
2、 拷贝相应文件到/data/目录,在数据库引擎类型为InnoDB时,拷贝数据文件的同时还需要拷贝ibdata1。
3、 将根目录下的ib_logfile*文件全部删除掉

备注:

1、 正常的数据导出恢复,最好用工具,不要在data文件层面去恢复
2、 测试环境在windows下

参考文档:

1、 mysql 直接从date 文件夹备份表,还原数据库之后提示 table doesn`t exist的原因和解决方法

在api里写可以平滑关闭的进程

背景

对于无关紧要的进程,我们可以随时kill掉再重启,但是有时候不能这样粗暴地操作。

假设我们正在处理一个从消息系统得到的消息,而消息是 非持久化/自动ack 的,如果进程被打断,这条消息就可能丢失。

或者我们在处理一个耗时特别长的任务,这时别人不小心kill掉了我们的进程,那我们前面的成果就付诸东流了。

针对上面这些情况,我们在Api进程里加入了平滑关闭的特性,你可以捕获进程关闭指令,自己决定何时关闭。

下面是一些使用方法:

使用

使用方法一:闭包

class Cront extends MY_Controller
{
    public function run()
    {
        $ready_exit = false;
        $this->regSig(SIGTERM, function() use (&$ready_exit) {
            $ready_exit = true;
            echo "准备退出!";
        });
        while (true) {
            {
                /**
                 * 括号里是业务处理代码
                 */
                echo "正在处理业务...\n";
                sleep(3);
                echo "业务处理完毕!\n";
            }
            $this->catchSig();
            if ($ready_exit) {
                echo "exit now!\n";
                exit;
            }
        }
    }
}

使用方法二:对象方法

class Cront extends MY_Controller
{
    public $ready_exit = false;
    public function handSig($sig, $my_var1, $my_var2)
    {
        switch ($my_var1) {
            case 'run':
                $this->ready_exit = true;
                echo "exit ready\n";
                break;
        }
        var_dump(func_get_args());
    }

    public function run()
    {
        $this->regSig(SIGTERM, array($this, 'handSig'), 'run', 'myvar');
        while (true) {
            {
                /**
                 * 括号里是业务处理代码
                 */
                echo "正在处理业务...\n";
                sleep(3);
                echo "业务处理完毕!\n";
            }
            $this->catchSig();
            if ($this->ready_exit) {
                echo "exit now!\n";
                exit;
            }
        }
    }
}

注意

  1. 进程捕获关闭指令后,用 kill -9 仍然可以杀死进程;
  2. api里的 regSig 不仅可以捕获 SIGTERM 信号,还可以捕获其他信号;
  3. 如果调用了 regSig 而没有调用 catchSig ,程序仍然捕获不到信号;
  4. 不用 declare(ticks = 1); 是考虑到性能;