个人博客原文地址:http://www.ltang.me/2017/04/12/Call-local-service-in-one-thread/
dapeng容器目前的模式,每个请求都需要以调用远程服务的方式请求服务,即使它请求的服务运行在本地同一个容器内。打个比方,客户端调用A服务,A服务内需要调用B服务,这时候,对于A服务来说,它是B服务的客户端,它请求B服务的方式与标准的前端没有任何区别,都需要经过各种过滤链,走网络传输,然后调用到本地(或其他节点)的服务。
这样做的优点是,对于所有请求,模式是统一的,每一个请求都是完整的服务调用。
缺点在于:
1. 如果一个节点有很多服务,一个请求链的互相调用过程中,即使每次都是调用本节点,也依然有大量的网络传输过程开销;
2. 对一个完整的服务调用来说,每个请求处理需要占用一个新的线程,如果一个请求中调用了十次本节点的服务,那么就至少占用了本节点10个线程,并且大多数都处于等待状态;
3. 在分布式服务的调用过程中,如果后面某个服务调用异常,前面成功的服务调用无法自动回滚,但如果都是在同一个线程同一个事务,后面的异常可以使前面的数据库操作回滚。
因此,提出了这么一种需求:“如果本节点存在即将调用的服务,那么,不经过网络传输,直接用当前线程调用本地服务”,以此来解决上述的3个问题。
在实现这个特性的过程中,遇到了一个问题,即,在同一个节点(容器)中运行的多个服务,它们的classLoader是不一致的,在服务A中试图通过反射去调用服务B是行不通的,参数没法通过反射传递。我选择的解决方式是将请求参数和请求结果通过序列化成java基础类型,另一个classLoader再将序列化后的参数反序列化,这样就能绕过classLoader不同导致的无法转换问题。这样做的优点是同一个容器里的各个服务可以在同一个线程内互相调用,缺点是要经历一次序列化和反序列化,不过我认为当前实际使用情况下(一个容器存在多个服务),前者更为重要。
此外,还需要考虑以下三个问题:
1. 可选择
这个特性应该可以选择,不强制使用,可以通过soa.call.local.enable=true
开启
2. context出入栈
在客户端与服务端,都有很多上下文信息(context)存放在threadLocal中,在之前的模式中没有问题,但是使用本特性时,线程在前一个时间点是客户端,在下一个时间点变成服务端,获取结果后又变成了客户端,这个过程中,threadLocal中的context信息需要正确地出入栈,以防请求信息错乱。
3. 与全局事务管理器的配合
1. 如果即将调用的本地服务是一个全局事务,那么开始全局事务是有意义的,因为服务中可能调用另一个节点的子事务,这时候需要全局事务管理器来管理全局事务与子事务,判断是否需要前进或回滚并执行;
2. 如果即将调用的本地服务是一个子事务过程,那么需要考虑此刻是否存在一个全局事务,如果存在,也需要开启子事务。比如全局事务A调用了服务B,服务B使用“同线程本地调用”的方式调用了子事务服务C,这时候,子事务C和全局事务A其实并不在同一个事务中,因此也需要被全局事务管理器管理。
20170423更新
经过讨论后,发现上面的思路有一些问题。考虑到系统目前微服务的设计,主要通过不同域对服务进行划分;即使同一个容器里面,不同的服务也是对不同库进行操作,那么即使在同一个线程,不同库操作仍然处于不同事务。那么,同线程调用本地服务,主要针对的对象,应该是那些同一个服务里面,针对同一个库的不同操作接口,希望将这些操作在同一个线程里面完成,主要是为了减少分布式事务问题,而现阶段网络开销并不是我们关注的重点。
因此,上面的实习需要做一定的修改,主要更改点为:
1. 同一个容器内的服务同线程互相调用,应该修改为同一个领域服务内;同一个容器内的不同领域服务,应该走标准的网络请求方式;
2. 对于同一个领域内的同线程调用,不需要再经过全局事务管理器进行管理,因为它的主要目的也是为了解决事务不一致问题,不需要重复。