业务系统错误设计

最近和同事讨论了几句错误设计的问题,感觉有必要写写自己的看法。

举几个例子,一般你的系统在运行的时候可能会有下面这些种类的错误/失败发生:

依赖组件挂了,可能是 db,可能是 mq,可能是 cache

依赖服务挂了,可能是别人给你提供的 http/rpc 服务挂了

可能是你的依赖方超时了

可能是调用方的参数有问题

可能是调用方的参数无法正确地通过校验

可能是用户的某种操作在业务逻辑上不具有合理性,不能够接着让他执行下去(例如你就给他一天一次抽奖机会,他不知道从哪里拿来的链接或者api又向你发送请求

还可能是程序自身出错了,比如数组越界,对字符串和数字进行加和操作,或者是把 null 当成了某种合法的数据结构,通过点或者下标来获取某种属性

上面这些情况都是有很大概率发生的,当这种情况发生的时候,如果用户向你反馈了问题,你要怎么进行跟踪呢?

可能很多程序员已经被大公司完善的日志系统惯坏了,给出的大约是下面这样的答案:

从用户侧的反馈信息里拿到用户 id,然后拿用户 id(或者内部携带的 trace id)去专门的 trace 系统里查询这个 bad case

那如果是没有 trace 系统的时候呢,你该怎么做?

上线 grep

好像也没什么错,但是你要 grep 什么呢?是不是也一头雾水。如果你的机器有几十上百台,那 grep 很可能也会变成很麻烦的事情。想想都崩溃(虽然你也可以写个 ssh 到所有机器然后 grep 获取结果的脚本)。

这是技术维度上的错误问题。换个角度,互联网公司的产品的用户在你的产品出了什么问题的时候,其实希望的也是能够看到你的系统明确地给出了某种有意义有价值的错误信息。譬如你需要告诉他,您的网络条件不佳,可以稍后重试,或者说我们的服务繁忙,暂时处理不过来,请稍后重试,或者更差一点,出张小人图卖个萌,说页面好像不见了呀,你要不要两秒以后再重试一下。

但是从这半年多的经历来看,互联网公司有不少系统可能连这个基本的要求都达不到。在用户的网络啊,或者系统内部出了点什么问题(无论是兼容性问题,还是别的什么技术问题)的时候,能展现给用户的是啥啊?

是白屏啊。。或者稍微好一点,一个 nginx 504 Gateway Timeout。

我特么哪里知道您这 nginx 504 Gateway Timeout 是什么鬼啊。

还有更荒谬的,某系统设计了一套权限系统,不管从权限中心拿到的是什么样的权限,都会进行后端缓存,而这个缓存过期时间少则十分钟,多则数天。在用户进入系统第一次判断认为用户没有权限时,则给了用户一个没有权限的页面。但是问题是,却没有在用户获取到权限之后进行权限缓存清空的交互逻辑!也就是说,如果你这次来的时候没有权限,那你就慢慢等缓存过期(几天后)吧。

所以啊,无数系统的产品经理和研发们压根儿就没有考虑过你的系统还有很多特殊情况的逻辑根本就走不通的啊。真不知道这样的系统是怎么上得了线的。

上面这些现实情况先按下不表了。我们来谈谈,怎么样的错误、异常情况处理才能够让人舒心。

对用户

对于产品的用户来讲,希望的是无论任何情况下都要有一个明确的反馈,正常情况下自不用说。而特殊情况下,我也应该看得到你的系统到底出了啥问题,是我网络不行了就告诉我是我的网络问题,别给我整一堆莫名其妙的英文。

在各种异常情况下,你要保证我能够恢复到正常使用中去。别因为你的程序一次运行错误就永远跑不起来了。

不要给我显示我看不懂的任何信息。不要什么都不给我显示(白屏)。

用户的想法会要求你在端上就有完善的错误兜底。而不是写完正常的业务逻辑就完事了。

对研发

实际上就是要有调用链的错误存储逻辑,比如对端的 api,你的errors 应该是能够一路把上游的错误串下来的,而不是直接只存储当前这一级出了什么问题。我只看 empty array reading 这种错误,怎么可能知道是上游某个地方查 mysql 的时候 sql 拼错了?

我们还可以参考一下 unix 系操作系统的设计,为每种错误都返回一个特定的错误码。错误码有啥好处呢?最大的好处大概就是能够按照错误码建立自己的业务错误字典,这个字典你甚至可以在客户端侧进行存储,当用户使用报错的时候可以直接弹出错误原因自查选项以及恢复建议(你看现在常见的硬件厂商一般都会这么做,比如sony,微软,任天堂)。错误码对于用户和客服,客服和技术人员之间沟通也有很大的好处,比如我司现在用户把问题反馈过来,你需要把客服按照不同的技能进行分组,如果是他们解决不了的问题,还要根据用户在电话里的描述,再对公司的公共知识库里的知识进行对应。如果有完善的错误码系统,至少在软件使用和技术方面上的沟通成本会下降很多。

当然了,上面是我的YY。

我们可以看看现在很多系统的错误处理所带来的痛苦:

大多数系统在出错的时候,只会返回一个没有规律的非零值,而错误信息寥寥可数,例如 Param Error!或者 Business Error!这类完全没有意义的信息,在自己调试出问题的时候,只从调用侧看到了这样的信息也没有办法进行判断,还要去代码里找在什么位置返回了上述错误内容。然后再用上各种单步调试手段进行调试,或者还有些脚本程序员喜欢用二分插 log,来判断是在什么位置出的错。如果是在线上报了错,那更是要狗日地上线上 debug 了对不对。如果你没有改线上代码的权限(php),也不能进行远程调试(编译型)怎么办?只能加 log 上线再上线了对不对。

不好的设计一定会变成你痛苦的根源。

当然了,错误码这样的信息和其它的配置类信息都有一个麻烦的地方,就是数量膨胀时候的管理问题,这个问题我也一直在考虑。大概只能通过进行目录/分类化把粒度划细了。

使用不同的错误码的同时,能够给人可读的错误信息也是很重要的。如果你设计的是开放式 api,在参数校验错误的时候至少要告诉别人是哪个参数错了吧。不要只给一个 Param Error。同时错误信息也应该区分开,哪些是给用户看的,而另一些是给自己看的。

在自己看到错误码或者错误信息的时候,能马上找到代码的位置,那你的设计就已经成功了一半了~

说了一堆有的没的,还是以一个例子结尾吧:

POST /user/detail/9932

Response
{
    "errorCode" : 82193823,
    "errors" : [
        "business logic error, user not exist"
    ],
    "userError" : "用户不存在",
    "info" : {}
}


POST /post/create

Response
{
    "errorCode" : 84723423,
    "errors" : [
        "sql parse error, invalid param in pos 22",
        "db query error, create post failed"
    ],
    "userError" : "内部错误,创建用户失败",
}

根据错误码,我可以直接知道发生了什么事情。在哪里发生的问题。

但到了实际的开发场景,发生错误的现场可能还是需要你去特定的系统里查询。但优秀的错误设计至少可以让你迅速地定位到发生错误的代码位置。这一点还是很有意义的。

当然,实际设计不会有例子这么简单,有些错误信息不适合暴露给端。还要考虑各种安全因素。这里只是举例~

一个好的系统,即使在出错的时候,也应该是对用户友好,且对 debug 友好的~