游戏开发论坛

 找回密码
 立即注册
搜索
查看: 15868|回复: 0

揭秘拳头公司的游戏API: 充分发挥ZUUL的性能

[复制链接]

1万

主题

1万

帖子

3万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
32132
发表于 2016-10-11 11:51:04 | 显示全部楼层 |阅读模式
  本文Riot游戏API系列文章的第三篇。我们第二篇中提到,在API前端架设了Netflix公司开发的Zuul代理服务器。我们选择Zuul是因为Netflix最初构建这个项目就是为了处理庞大的日流量,因此我们有理由相信,这些代码是经过实战考验和生产环境加固过的。然而,自从两年前我们第一次启动这些API,我们始终难以从我们的Zuul实例中获取预期的负载容量,虽然我们知道,问题并不在Zuul本身。

  在这篇文章中,我们会大概讲述我们是如何追踪一部分影响我们容器容量的配置问题,网络以及Jetty。首先我会讲一些导致瓶颈的socket管理的问题,然后我会讨论另一种表现看起来是相同的socket问题、相同的症状,以及我们如何解决的。最后,我会大概讲一下我们对这些影响我们的问题的理解,他们如何捆绑在一起的,以及我们的容量现在在一个什么程度。

  容量问题

  我们最初启动API时,我们部署的Zuul实例每分钟可以处理大概1万次请求(RPM,request per minute),更多就会挂掉(后面会多一点)。对期望的容量我们也没有很好的理解,但是这么低的RPM让我们很奇怪,Zuul怎么有效的为Netflix如此大的流量工作。贯穿API平台的整个生命周期,我们看到一些有问题的现象模式,超过了特定的RPM或者缓慢的代理请求都会导致Zuul实例变得无法响应。当Zuul实例无法响应的时候,API调用失败,这会影响使用我们API的第三方。大流量的站点,比如 LoLKing.com 和 OP.GG,最后会做缓存,所以即使API宕机他们依然可以运作,只是数据不实时了。然而大部分站点在API平台无法响应的情况下都无法提供相关功能。因此,找到并解决API平台的这类问题,被列为重中之重。

  本文全文将会讨论API基础设施的各个部分,以及Riot的网络。下面的图表会帮你从视觉上理解后面阅读的各个模块交互的内容。

70.png

  SOCKET 管理

  启动API不久,我们注意到一种现象模式,每过几个小时,Zuul会变的非常慢,进而对调用停止响应,然后自己恢复。通常经过一天左右的震荡模式,这个问题将会自己重现,直到几个月后解决了这个问题。

  我们第一次见这种行为的时候,调查显示请求处理消耗了异常长的时间。最终,这些慢请求占据了Jetty所有的工作线程然后其他后续进来的请求堆积在了Jetty的接收队列中。当接收队列变得足够大之后,请求在队列中等待一个工作线程的处理,在等待的过程中最后超时。当足够多的请求在接收队列中还没有被处理就被拒绝后,迟钝现象就清空了,请求又能被及时处理了。然后几个小时以后,这个模式又会重来一遍。

  观察这个循环几次后,我们断定一个从代理后的服务返回的缓慢的请求是个潜在的元凶。如果足够多的流量通过Zuul代理到了那个服务,它会触发下降的漩涡,进而使整个边界宕机。为了缓和这种风险,我们增加了配置连接和读超时的能力,这样就允许我们合理的调整每个代理服务的配置,来保证整个系统不被几个迟钝的服务带宕机。

  但即使适当的解决了这个问题,而且确认按照期望工作,这种行为还是又一次突然出现了。另一项调查显示当Zuul开始慢下来的时候,容器上有大量的socket是CLOSE_WAIT状态。我们曾看到过一次明显的变慢的模式,当时CLOSE_WAIT状态的socket有数千个。虽然很显然有个问题是socket没有被正确的清理,但理论上,这个容器应该能处理比当时耗尽时多得多的socket,而没有任何性能问题。这种模式很容易预见,因此我们写了个脚本,如果机器上开启的socket数量超过4000就重启Jetty,这只是个应急处理的方式,直到我们可以找到并解决socket溢出的问题。

  最终,我们发现当一个请求超时的时候,代码没有正确的清理链接,然后发布了一个修复补丁,解决了最直接的问题。但是仅仅在部署那个补丁几个月之后,这个恐怖的现象又回来了。这次并没有socket处于CLOSE_WAIT状态,但是模式和之前典型的模式是一样的,而且我们看到打开的socket数量飙升。我们决定修改实例允许的最大打开sockets的数量以及我们Jetty线程池的容量,配置如下:

  添加至 /etc/sysctl.conf:
  1. # increase max open files
  2. fs.file-max = 65536
复制代码

  Added to /etc/security/limits.conf:
  1. *               soft    nproc65535
  2. *               hard    nproc65535
  3. *               soft    nofile65535
  4. *               hard    nofile65535
复制代码

  添加至 /usr/share/jetty/start.ini:
  1. #increase thread pool size
  2. threadPool.minThreads = 50
复制代码

  但是这些这些修改并没有让我们看到什么提升。在那个时间点,我们也没有什么其它可以尝试的,最后在我们的集群中增加了一些Zuul的实例,然后问题解决了。暂时当我们的平均吞吐量增长到一定程度的时候,就添加新的实例。每次RPM增长到10k这个点的时候,这种行为一起复现。新实例总是能解决这个问题。

  同样的模式,不同的原因

  三月的时候,我们重构了Zuul,使用Hermes客户端(一个原厂框架,我之前的文章讨论过)来代替Ribbon作为客户端软件实现几个终端间的负载均衡。同时,我们重构了一起其它的基础服务,从dropwizard应用迁移到Hermes服务应用。我们在QA中测试了所有这些变化,而且在staging环境中跑了几个月。2016年4月11号开始的那周,我们开始一系列的操作在左右VPC上来部署我们的新项目,包括Zuul,ESRL,API 度量,以及配置服务。

  在4月16日中午左右,Zuul开始表现出与老模式相同的问题。起初,我们花费了一些时间,尝试找出我们做出的诸多修改的哪一个会让这个问题重生,但是却没有明显的祸源。下一步需要检查什么似乎已经很明显了。可以确定的是,当Zuul实例不响应的时候,容器上的打开的socket超过12k。像之前一样,我们觉得一个容器的应该有能力处理这个数量个socket,没什么问题,但我们的经历告诉我们——不能。我们想知道,我们的流量是否又到达了10k每个容器的极限,然后导致了这个现象的回归。我们检查了我们的New Relic监控,确定我们的流量并没超过之前几个月,因此我们排除了这点,但是又很多打开的sockets这个症状是一致的。

  我们的New Relic监控同时确认所有容器中的JVM不存在内存问题。我们注意到受影响的节点都在,不过我们不知道这是不是个巧合。我们也注意到我们的容器的内核版本并不相同。过去,我们见过由于内核版本变更导致的问题。例如,有一次内核升级,IP包的TTL变化了,导致了一些丢包问题并堵塞我们的代理对特定后方服务的访问。为了确保所有跟内核的交互以及其他的软件在我们的所有容器上的一致性,我们在所有的容器上执行了一遍yum update,然后重启,但是这个现象并没有改变。我们也在Zuul反应迟缓的时候做了一些测试,来看看有没有ping值飙升或延迟的问题,但好像也不是这个原因。

  排除了其它原因,我们回到大量的打开的socket上。我们始终觉得一个容器处理几千个打开的socket应该没什么问题,我们过去知道这个症状总是伴随着类似的现象。我们团队的一个新成员,之前遇到类似问题的时候还没来,他看过系统socket相关设置后,提出了如下修改建议。

  添加至 /etc/sysctl.conf:
  1. # increase max open files
  2. fs.file-max = 1000000
  3. # increase the port range (to allow more sockets to be opened)
  4. net.ipv4.ip_local_port_range = 900065535
  5. # allows OS to reuse sockets in TIME_WAIT state for new connections when it is safe from a protocol viewpoint.
  6. net.ipv4.tcp_fin_timeout = 15
  7. net.ipv4.tcp_tw_reuse = 1
  8. net.ipv4.tcp_tw_recycle = 1
复制代码

  添加至 /etc/security/limits.conf:
  1. *               soft    nproc1000000
  2. *               hard    nproc1000000
  3. *               soft    nofile1000000
  4. *               hard    nofile1000000
复制代码

  添加至 /etc/pam.d/su:
  1. session    required   pam_limits.so
复制代码

  虽然我并不确定增长的打开状态的socket是不是起因或者症结,但我们准备先走出这一步,按这些来配置一下试试。不论如何,它们看起来是有用的,我们也很好奇,它们是不是真的有用。然而,问题继续……

  我们继续我们的调查,使用JVisualVM观察工作线程,通过JMX/MBeans展示出了一些令人吃惊的东西。虽然我们一直认为我们的工作线程的最大数量被设置为2000,但JVisualVM显示,这个最大值实际只有200。我们做了一些深入挖掘,发现这个最小/最大线程的相关的配置也会从默认的配置文件/usr/share/jetty/etc/jetty.xml中获取,这个会复写我们在start.ini中的配置。我们修改了一下jetty.xml中的配置,然后重启了Zuul实例。

  添加至 /usr/share/jetty/etc/jetty.xml:
  1. <Set name="minThreads" type="int"><Property name="jetty.threadPool.minThreads" deprecated="threads.min" default="10"/></Set>
  2. <Set name="maxThreads" type="int"><Property name="jetty.threadPool.maxThreads" deprecated="threads.max" default="2000"/></Set>
复制代码

  但现象仍未改变。

  DDOS保护以及边缘问题

  我们知道一定有一个解决方案。我们继续跟踪调查,最后问题回到我们对观察者服务代理请求上,观战服务每天有数万的请求(每24小时67K RPM,1/3的总流量)。我们回顾了Zuul实例响应迟缓的这段时间观战服务的响应时间,发现了一个直接相关。观战服务代理的请求要消耗很长时间,经常3秒,在一些特殊情况下甚至40秒。数千个需要消耗3秒以上的请求阻塞了可用的工作线程队列。因此千其它请求,包括健康检查等,在30秒后被Jetty超时。失败的健康检查请求会导致整个集群域起伏震荡行为。失败的健康检查会导致一个服务的所有终端被移走。然后,当一个单独的实例的健康检查通过后,ELB会重新加入这个节点,然后这个实例会承受这个服务的所有负载,然后马上挂掉。临时的解决方案是停掉观战服务的终端,导致Zuul认为这些终端变得无效,然后对这些请求马上返回一个503错误。这样会排除慢请求,然后使健康检查的请求通过,进而使集群恢复。一旦终端禁用,集群马上恢复并保持健康状态。这让我们思考为什么几年前我们添加这段代码的时候为什么没有触发这种超时或响应缓慢的问题。当我们在New Relic监控查看从Zuul向观战服务的终端发起的慢请求事务,如下图显示,HTTP客户端请求消耗的时间比配置的1秒要长。

71.png

  我们不明白为什么我们的配置的超时没有起作用,但是坚持集中先解决为什么观战服务的请求响应缓慢,并尽快得到一个解决方案。解决问题的同时,我们注意到所有从一个有效域发出的外访的请求使用同一个IP。贯穿于我们的基础服务当中,我们拥有最佳实践的保护服务,因此我们开始分流内部和其他团队的流量,来看看我们的Zuul实例是否触及了什么限制。结果表明我们的流量达到了某些防御检查,因此没有新的请求可以通过它到达观战服务,这个结果就是Zuul实例变得无法响应。一旦我们的Zuul实例停止对请求的响应,由于对观战服务的请求慢慢的超时,并回到请求队列,它们慢慢开始自己恢复。当Zuul实例不响应的时候我们的防御检查开始下降,这时进入的旁观请求会被即时的响应。由于流量是以此种方式循环的,我们会看到我们的Zuul实例的健康状态上上下下。经过一些进一步的内部调整,我们最后修正了这个问题,我们的Zuul实例恢复了并一直保持健康。

  实现稳定

  一次Riot Direct把我们的IP加入到了他们的白名单中,我们决定通过观战请求压测一下我们生产环境的一个Zuul实例集群,进而确定问题是不是真的解决了。我们知道如果我们负载测试用的Zuul实例挂了,另外的15个Zuul实例也能正常处理负载。我们发现,不论我们扔给它多少负载,它都能完美处理,最终在一个单独容器中处理的RPM比以往任何时候都高。我们导入了所有北加州VPC的流量,大概共有150K RPM,这是我们所有区域API负载的50%,很可靠的运行在一个8核的实例上。在这之前,北加州的流量跑在16个4核的容器上,每个使用4G的堆空间。

  我们知道我们找到了数年来我们一起追求的稳定,现在可以开始更新了。我们在每个VPC中循环开启Zuul实例的所有新容器,以保证我们对容器做的这些在线更新以及监控和测试中的JMX不会导致雪崩。我们给每个Jetty进程配备 c4.xlarge (4核) 容器以及6G堆空间。我们在每个有效域内放置2个容器,这意味着每个VPC有4个容器,一共12个容器。在这之前我们使用30个容器来支撑我们的流量。由于我们看到Zuul容器上系统socket配置修改带来了如此好的结果,我们对ESRL做了相同的修改和并度量了一下他们容器来帮助优化他们的吞吐量,由于所有对Zuul的请求最终是对这些服务的请求。在那之后的几个月里,我们没有遇到什么重大事故,Jetty的工作线程池保持40个上下的工作中线程。

  最后一个转折,在我即将结束这篇文章的时候,我突然想到了之前配置的超时时间没有生效的问题。我当时通过一个本地运行Zuul服务连接一个测试服务,因此我本地的Zuul无法达到那个服务。配置的1秒超时,而那个请求却用了3分钟才超时。现在我们不需要急着恢复在线服务,我决定花点时间来查明白为什么。我花了几小时调查研究,最后找到了问题的根。Hermes在Jersey的ClientConfig类中配置PROPERTY_READ_TIMEOUT (socket超时) 和PROPERTY_CONNECT_TIMEOUT (连接超时),但是使用是配置选择的客户端实现的。我发现Jersey虽然把前者的属性完整的拷贝到了它的Apache客户端实现的Apache HttpParams对象中,但是它没拷贝后者。因此Apache使用默认值0,这意味着无尽的等待。PROPERTY_CONNECT_TIMEOUT 在Jersey的其它客户端实现里会用到,但是Apache不会。Hermes将为Apache Http Client设置正确值的责任委托给了Jersey,但是Jersey只设置了socket 超时。我为HermesClient提交了一个修正,来明确的在HttpParams中设置这个值,来代替依赖Jersey作为一个中介。我已经确认了这个修正是没有问题,这个配置的超时时间对一个无法到达的服务正常生效了。

  结论

  我们估计从30到12个容器的变化大概每年能为我们节省$25,000。更重要的是,最终我们一定程度上能用Zuul来承载符合我们认知容量的负载,但是永远解释不了过去为什么不能。Netflix使用这个代理服务器来处理庞大的流量和带宽,因此没有理由搞不定这个规模。其实Zuul从来也不是问题真正症结。

  我们知道当API刚上线的时候DDoS防御不是问题的元凶,因为那时候防御还没上线,但是旁观服务第一年也没有通过旁观服务代理。我们也知道连接超时问题也不是元凶,因为这个只是当Zuul迁移至HermesClient的时候引入的。当我们推出Zuul和HermesClient组合的时候,存在着不实现连接超时的bug,当DDoS保护开始限制我们的流量的时候,它触发了整个系统级的崩溃。我们相信为了解决缓慢问题我们对系统socket的配置以及Jetty工作线程所做的那些修改最终解决了我们经历数年的问题模式。然而当我们最初应用这些修改的时候没有看到性能上的变化,是由于观战服务流量的DDoS保护造成了一切的瓶颈掩盖了这些修改提供的所有益处。一旦我们的IP加入了白名单,结束了慢响应,然后我们重新载入测试,我们的流量以从没有过的形式完美提升,是因为Jetty和系统配置的修改已经生效了。现在,由于链接超时的bug解决了,我们以后保护代理服务免受类似DDoS防御导致的这种慢响应的问题。

  除了性能的提升之外,我们第一次真正的理解了我们服务的容量。我们判定每个CPU可以处理40K的RPM。因此,使用4核的容器,我们可以期待一个Zuul可以处理160k 的RPM。我们给Jetty进程分配了6G内存,但它始终徘徊的2.5GB左右,而不是内存限制。现在是CPU限制了我们的负载,这对我们是第一位的。我们之前Zuul表现的缓慢或不响应的问题,从没有导致过CPU的限制,都是不正常socket行为导致的。现在我们可以高兴的说,我们现在是被CPU限制的。

  在调查解决最初搞挂Zuul问题的过程中,我们学到了很多的技术点,最后一劳永逸的解决了Zuul的容量问题。他用掉了我们超过一周的紧急的未预期的工作,但非常值。整个过程的中一个惊人的部分是他对团队合作的影响。我们的团队有各个领域的专家,为问题的调查和解决作出了许多贡献。最后,我们有了一个稳定,可靠,可伸缩的边缘,我们放在了一个非常好的位置,可以专注于我们的功能集和范围。

via:杰微刊

相关阅读:揭秘《英雄联盟》的自动化测试

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-5-15 19:34

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表