这一周,主要的工作是针对某个 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 中包含了从就绪通道中进行正 确操作的必要参数。
目前经过阅读代码,从客户端侧并未发现有什么明显的问题。所使用的方案基本上是成熟且 稳定的,那有可能问题出现在服务端,这还有待后续的熟梳理。