随着分布式、微服务的火爆,跨系统的服务调用也变得常见起来。这使得我们在线上追查问题的时候,常常要查阅多个系统的日志。

这时候,问题就出现了。如何确定服务A中的某条日志,对应的是服务B中的一个操作呢?

我们的开发人员提出了一个简单的方案:每次服务调用时,调用方都将一些技术性的数据封装在header中;服务方从header中获取到数据后,记录到日志中(或者做其它必要的操作)。

初版

这个方案的思路无疑是正确的。不过其最初的实现方式么,我实在不敢苟同。因为它要求每次调用服务,都按这个格式来封装数据:

{
    "header":{        
        "traceId":xxxx,// 一次请求的唯一键
        "timestampe":zzzz, // 发起 请求的时间戳
        "fromSystem":systemA,
        "requestId":yyyy, // 业务数据的唯一键
        "others":aaaa // 一些其它数据
    }, 
    "body":{
        // 业务数据
    }
}

这个方案的确解决了问题。但是同时它又引入了新的问题。

第一个问题出在requestId字段上。按照方案要求,它应该是业务数据的唯一键,一般就是数据库主键。最主要的问题是,不管用什么样的唯一键,这个字段都应属于业务数据,而非请求头中的技术数据——前者对于业务逻辑往往必不可少,后者则对业务可有可无、为技术上锦上添花。 将业务数据放入请求头中,混淆了业务与技术的边界,为后续的变更、扩展带来不小的麻烦。

这套方案中“混淆业务与技术边界”的地方还不止这一处。它将header与body全部放在请求体中——例如http的RequestBody,或JMS的MessageBody中,使得每一段调用服务的业务代码中,都要处理header与body。

这次,应当由技术框架承担的功能被写到业务代码中,技术与业务的边界再一次混淆。这次混淆不仅增加了业务代码的代码量和重复率,还导致大量的老代码无法顺利的接入,更为以后的扩展埋下了天坑——如果我们需要统一地再加一个字段怎么办?

说真的,这个方案让我回忆起很久以前在jsp页面上写代码的感觉。HTML标签、CSS样式、数据库事务、业务代码混杂在一起,熬成一锅大杂烩。虽然这样也能够实现业务功能,但是再想改动任何一个点,都难于上青天。

再版

第二套方案中,我们首先去掉了header中的requestId字段。这样,避免了业务代码对技术框架的入侵。

第二步,是避免技术框架入侵业务代码。我们的做法是将header字段放到http的请求头(header)或者JMS消息属性(MessageProperties)中。这样,业务代码仍如以前一样,只需要处理body内的数据。由技术框架提供header的写入和读取功能。这样,通过一些配置(如RestTemplate的拦截器等),就可以把旧代码全部按新规范进行处理。

完成这两步改造后,后续再修改、添加header字段时,只需要对框架代码进行修改即可。

后记

我与身边的开发同事讨论设计原则、设计模式时,常常被投以不理解的眼光:这种东西太理论/理想化了,实践中根本用不上、没必要用、性价比太低。

然而初版方案中,“单一职责”原则被无情的践踏——业务代码背负了技术框架的职责,技术框架也背上了业务代码的包袱。这是其低扩展性问题的根源。

但是,这个观点并没有得到同事们的认同。实际上,再版方案也并没有被通过施行——我们匆匆忙忙的上线了初版方案。

唉……