二零二二年十二月第三周技术回顾

这一周,主要的工作是针对某个Java服务进行优化。该服务一直存在CPU占用率无法提升上来的问题。首先考虑的问题是,该服务是否存在工作线程不足的问题。后面发现,并非CPU占用无法提升,而是CPU占用提升后会导致较多的超时问题。该服务很久以前就有反馈性能不足,不建议继续使用。所以我感觉,这个问题来自于框架,而非业务代码。

经过对框架代码进行阅读和梳理,该框架使用netty作为NIO服务器框架,并且会在执行业务逻辑的时候分发业务处理任务到工作线程。然后工作线程来处理业务逻辑。之前框架有个问题就是,工作线程数太少,难以应对高IO的情况。而现在的情况下,推断并非上次一样的问题,从日志来看工作线程数是足够的。会不会是客户端的问题。在整个微服务架构下,该服务会作为客户端去调用其他服务的接口。

这可能是一个切入点。通过阅读代码,发现该服务框架通过实现Java的InvocationHandler接口来生成代理类ObjectProxy,该ObjectProxy接管对其他服务的RPC调用。业务代码中,通过RPC的方式发起对其他服务接口的调用时,ObjectProxy会通过它关联的ProtocolInvoker来获取目标服务对应有效节点列表(这个有效节点列表每隔30s刷新一次)。而后,通过将列表传入负载均衡器LoadBalancer获得本次调用的目标节点。然后,就通过具体协议对应的Invoker类向目标节点发起调用。Invoker负责管理与目标服务之间的长连接,在调用的时候会选取一个连接来发送请求并接收响应。具体的请求方式与此次调用类型是异步还是同步有关。

客户端需要调用的每个目标服务由多个节点组成。对于每个节点,该框架都会默认创建两个I/O线程负责网络I/O传输(NIO模式)。另外,该框架会为每个节点分别默认创建和处理器个数相当的TCP连接。每个I/O线程包含有一个Selector对连接的相关的事件进行轮询监听。而在每个TCP连接上会建立一个TCPSession,每当需要发送一次请求时,会建立一个Ticket来跟踪这次请求及其关联响应。对于同步请求来说,发送请求后会被阻塞,直到响应到来为止。对弈异步请求来说,会在Ticket中填入callback函数,当响应到来时,会通过TicketNumber(Ticket的唯一索引)找到对应的Ticket并调用其中事先填入的callback函数,进行后续的处理。

该框架对NIO的操作,在底层用到了Java提供的NIO库的能力。上面所提到的TCP链接,其实就是NIO库中的SocketChannel。那么该框架如何分割各个数据包呢,该框架通过Buffer来存储目前已经读到的数据,而我们常用的RPC协议,会将该包的字节数存在包的首部,只需要比较Buffer与字节数的大小即可知道包是否读全了。如果包没有读全,就继续等待后续的数据到来。如果包读全了,则按照字节数规定的大小从Buffer中分割出相应的数据来处理即可。在该框架中,会额外从一个线程池中取出工作线程来进行有关于Ticket的后续处理。目前从现有的代码来看,这个工作线程池只用来做这件事情,它的默认工作线程个数与核心数相同,最大线程数是核心数的两倍。

根据Java NIO库的定义,Channel中规定了几个IO操作,Selector会轮询检查这些操作是否就绪,如果就绪就会返回SelectionKey。SelectionKey中包含了从就绪通道中进行正确操作的必要参数。

目前经过阅读代码,从客户端侧并未发现有什么明显的问题。所使用的方案基本上是成熟且稳定的,那有可能问题出现在服务端,这还有待后续的熟梳理。