Web 推送服务优化之路

  • lovebing 
  • 未分类

最近一段时间,对我负责的 Web 推送服务(基于 Netty-socketio)进行了优化。

目标:

  • 合理使用资源,减少 CPU 和内存使用率
  • 提高稳定性

主要从以下几方面着手:

部署优化

部署到 k8s(Kubernetes),可以很方便地调整 CPU 和内存配置。由于 k8s pod 的会话保持比较麻烦,所以 Socket.IO Client 不能使用 Upgrade 的连接方法。

代码优化

减少中间缓存

之前使用了 Guava 的 LocalCache,以客户端的 SessionID 作为 key。

LocalCache 的数据淘汰策略,只有两种:expireAfterWrite 和 expireAfterAccess。

  /** Returns true if the entry has expired. */
  boolean isExpired(ReferenceEntry entry, long now) {
    checkNotNull(entry);
    if (expiresAfterAccess() && (now - entry.getAccessTime() >= expireAfterAccessNanos)) {
      return true;
    }
    if (expiresAfterWrite() && (now - entry.getWriteTime() >= expireAfterWriteNanos)) {
      return true;
    }
    return false;
  }

在实际的业务中,用户重新登录后,SessionID 会重新生成一个,所以在 channelInActive 时,需要清除相应 SessionID 的缓存。这个操作被我忽略了,过一段时间服务就发生 OOM。后来发现 Netty-socketio 提供了 store 的管理机制,就不用 LocalCache 了。

减少 JSON 操作

使用阿里云的消息服务 MNS 作为消息队列,之前目标用户的 UID 放在消息体里,每次消费都要做 json 反序列化。优化之后,UID 存在 Message Tag 里,而 Message Body 直接透传到客户端。

使用 Netty 重新实现 Socket.IO Server 简化版

Netty-socketio 在存以下问题:

  1. 当服务端重启时,有时候会发生端口被占用的情况。
  2. 根据消息的类型决定发送普通消息还是二进制消息,如果是 byte[] 类型,就发送二进制消息,否则发送文档消息;不够灵活。

所以重新实现了 Socket.IO Server 的最小化版

处理 CPU 使用率过高的问题

做了以上几个优化后,发现还存在一个问题:每过一段时间,CPU 使用率就居高不下。然后写了一个脚本获取 CPU 使用率最高的 Java 线程

发现以下线程 CPU 使用率很高:

在网上查了一些资料,可能是线程池的问题。分析了一下代码,可能是以下原因:Jetty 线程池设置得过大

因为之前使用 Spring WebSocket 实现 Socket.IO Server 时,考虑到连接数比较多,将 Jetty 的最大线程数设置得很大。测试后发现性能不行, 于是放弃了 Spring WebSocket,但却忘记了把配置还原。

通过 Grafana 查看 Prometheus 的监控数据,发现 jvm_threads_live_threads 数据在持续增加。

由于 Prometheus 会定期通过 HTTP 采集监控数据,Jetty 的线程数据就一直增加。通过 top_cup_java_thread.sh 发现,随着请求每次的增加,每次请求的CPU 使用率都比较高。

然后把 Jetty 最大线程数改为默认的 200,再也没有发生 CPU 使用率居高不下的情况。EatWhatYouKill 偶尔排到第一位,CPU 使用率也比较低了。