在K8s上运维Java和GC的经验教训 - Coufal


在Wandera,大多数微服务都是用Java编写的,并且都在全球众多的Kubernetes集群中运行。不久前,我们的一个团队注意到网络间歇性问题,这会导致我们两个边缘服务之间的通信失败。
解决方法很简单,将受影响的服务的流量故障转移到运行状况良好的实例上,并在一段时间后进行恢复过来,但是却无法及时发现问题,在线程转储中找不到任何暗示潜在问题的信息,后端团队引入了一个度量标准和一个警报,以检测边缘服务之间的断路(CB),以便至少我们可以及时做出反应而不会影响我们的客户。
在Wandera,我们不仅每天以连续交付的方式多次独立发布服务,而且还可以通过使用内部内置的功能标记系统以类似的方式为客户启用功能。该系统的一部分是我们的大多数Java服务使用的通用库,这些库用于检查是否为特定客户/管理员/设备/其他启用了特定功能。库是在一段时间之前编写的,并且在过去的几年中没有进行任何重大更改,因此您可以称其为经过验证的库。但是,就像所有内容一样,魔鬼会隐藏得很详细,环境或输入参数的变化可能会导致令人惊讶的结果。这将在以后发挥作用。
 
警报是在圣诞节之前引入的,相对安静,直到一月初。在这一点上,警报开始定期触发,并相应地将其上报给On-Call工程师(正确的做法是)。这似乎只发生在我们当时最繁忙的美国特区中。我们尝试更改在边缘服务旁边运行的本地数据库,并向群集的工作节点中添加了更多资源,但是即使服务的响应时间开始缩短,断路仍在发生。
我们有一些理论,但没有确凿的证据。例如,一种理论认为问题可能与新的数据库后端(从嵌入式到外部)有关,这带来了额外的延迟,并且无法很好地处理负载。我们的Prometheus指标显示了负载下数据库的延迟增加,而DB指标却没有任何增加。
尽管问题是断断续续的,并且影响相对较小,但我们还是决定召集一个专门tiger团队几天来共同关注该问题。

  • 团队可以将问题隔离到我们的网络服务之一
  • 它以某种方式与服务的发布或带来的额外负载有关
  • 锁定后,该服务将无法恢复,除非完全从负载中取出负载

在Tiger团队对问题进行调查时,产品和后端团队为我们的客户启用了几个功能,这些功能正在增加此服务的负载。得出结论并不难-可以观察到因果关系。启用功能标志会导致服务上的更多负载,进而导致锁定。
最简单(最讨厌)的解决方案是增加分配给服务的计算资源,问题消失了,甚至显示DB调用延迟的指标也更好了。
尽管如此,Tiger团队坚持不懈地工作,并继续分析增加资源的影响……然后问题又发生了。产品团队为另一批美国客户启用了某些功能,这些功能在启用该标记的确切时刻在我们的一个欧洲DC中触发了相同的警报。但是,由于受影响的客户不在欧盟运行其设备,因此DC的负载没有增加。
 
为何世界另一端的客户启用某些功能会如何影响另一端的服务?您还记得我前面提到的库(我们用来控制功能标记的库)吗?这是恶作剧调用出错的地方。
们通过对边缘服务进行详细的分析发现了这些特征库,但是现在为什么会出现问题?以及它与Kubernetes的联系如何?
 
GC死亡
在Kubernetes中,您可以选择检查容器的资源使用情况,这称为容器限制。如果在CPU上设置了限制,则会在特定的时间窗口内为容器提供CPU时间的有限部分。每当容器达到极限时,它就会从进一步的CPU周期中剥离,直到窗口关闭。这样做是为了减少嘈杂邻居对其他容器的影响,并保护在同一节点上运行的其他服务。
另一方面,Java具有这种垃圾回收过程,可以清理内存中未使用的对象,尽管运行速度(通常)很快会非常昂贵。它可能会非常昂贵,以至于可能在特定的时间范围内耗尽整个CPU配额,而且发生如此之快,以至于使用常规指标很难观察到。
由于两者的结合以及库中的故障,我们的服务成为了GC导致死亡的受害者。
  • FF检查会产生大量垃圾(并且在解析时会占用宝贵的CPU周期)
  • 为了使Java能够应对,必须调用GC
  • 更多的GC意味着更高的服务延迟
  • 更高的延迟意味着处理请求需要更多的线程
  • 更多线程→更多上下文切换
  • 更多上下文切换→需要更多CPU周期
  • CPU达到容器限制
  • 容器被节流
  • 没有足够的CPU进行垃圾收集
  • 内存回收速度不够快,垃圾堆积
  • 需要更多的垃圾收集意味着需要更多的CPU
  • CPU不足→CPU节流
  • CPU节流→延迟增加
  • 需要更多线程和GC
  • 恶性循环现在已经完成
  • 服务现在(几乎)无响应

 
解决方法
为了解决这种情况,团队分析了库并确定了需要修复的地方。最后,这只是分析的一点微小抽象:类似java.util.Properties成java.util.HashMap导致该问题。
有了生产中的修复程序,就可以测量结果了。
  • 检查FF所花费的CPU骤降了97%
  • 每秒分配的Java堆内存减少了80%
  • 最后,服务的平均响应时间缩短了60%

 
结论
  • 花费几个小时或几天的时间全神贯注于此问题可能是有效的
  • 在Kubernetes中对容器设置了限制后,始终会发出有关CPU节流的警报
  • 代码中的每个抽象都有一个代价,衡量其效果,以免失控
  • 遥测(日志,指标,跟踪)很重要,但在某些情况下还不够
  • 在生产中测量profile服务很重要