【编者按】在2013年初马化腾被问及“过去两年腾讯在海外投资中最成功的案例是什么”时,他毫无疑问的回答:“投资美国的Riot Games,做出《英雄联盟》。”在那个时候,《英雄联盟》这款游戏仅上市3年,却以500万同时在线(日活跃用户1200万)玩家数量横扫全球,成为全世界第一大线上游戏。而值得一提的是,一年后(2014年),该游戏的日活跃玩家数量已超过2700万,最高同时在线玩家也达到了750万。
回顾《英雄联盟》的发展无疑是一个高速成长的光辉史,然而这个光辉史赖以生存的基础设施却不得不克服一次又一次的挑战,历经一次又一次的迭代,就比如下面我们要说的聊天服务。近日,HighScalability创始人Todd Hoff总结了Riot Games公司Michal Ptaszek在 Strange Loop 2014会议上的演讲“Scaling League of Legends Chat to 70 million Players”( PPT下载),简述了该游戏聊天服务“Make it work. Make it right. Make it fast.”理念的实现途径。
以下为译文
如上图所示,该游戏的聊天服务需要支撑750万并发用户,2700万日活跃用户,每秒钟需要处理的消息上万条,每台服务器每天处理消息达十亿条。
对于对战类型游戏,团队间交流直接影响到了比赛的胜负。为了帮助完成这一目标,聊天服务初始就使用了XMPP特性,就如WhatsApp一样。在小规模下实现并没有什么难度,可以说是开箱即用,然而当用户快速增长时,挑战也随之而来。为了更快和更好的实现这一点,WhatsApp和LOL(英雄联盟)不得不定制化Erlang VM,对其进行优化并添加多个监控功能,只为解决大规模下的性能瓶颈。
纵观整个服务架构,Riak CRDTs(commutative replicated data types,可交换多副本数据类型)应用无异是最大的亮点,通过零可变贡献实现大规模线性横向扩展。时至今日,CRDTs的使用范围仍然很小,甚至大多数人都没听说过这项技术,但是它将来必将成为众多机构的宠儿,在写操作上另辟奇径。下面我们一起看LOL如何打造支撑超过7千万玩家的聊天系统:
状态
- 月6700万的独立访问玩家,不包括其他使用这个系统的服务
- 日活跃玩家2700万
- 750万的并发玩家
- 每台服务器每天路由10亿个事件,值得一提的是,CPU和内存使用率只有20-30%
- 每秒处理1.1万条消息
- 世界范围内部署的chat服务器达数百台,负责运维人员只有3个
- 99%的可用率
平台
- Ejabberd (Erlang based) XMPP server
- Riak
- Load Balancer
- Graphite
- Zabbix
- Nagios
- Jenkins
- Confluence
Chat
1. 支持私聊和群聊
2. Chat拥有独立的界面,同时还支持好友列表。你可以查看好友的连接状态(在线或离线)、游戏状态、游戏时间,以及获得过的奖项。
3. REST APIs让chat可以作为其他LoL服务的后端服务。举个例子,store会与chat通信来验证好友关系。Leagues会使用chat的社交图谱将新玩家组织到一起。这样一来,这些新玩家就可以交到一些志同道合的朋友,从而增加在线时间。
4. Chat必须稳定的保持在一个低延时环境,chat的宕机会拉低整个游戏的用户体验。
5. 选择XMPP作为协议,提供消息、状态信息并且负责通讯列表维护。
6. 基于性能和新功能等原因,他们不得不偏离核心XMPP协议。
7. Chat服务打造时就选择了Ejabberd作为服务器。Erlang同样非常棒,拥有更好的错误隔离和可追溯性。同时,它还支持代码的热加载,如此一来,给bug打补丁时就不需要再重启服务。
8. 目标是零共享以实现线性横向扩展,同时零共享还更有益于错误隔离及追溯。在零共享实现上,系统刚还有一些提升空间。
9. 运维人员只有3人,这样就对chat服务器容错提出了非常高的要求,同时也意味着不是每个故障都需要人力介入。
10. 让它崩溃。不要试图从一个严重的故障中做缓慢的恢复。取而代之,从一个已知的状态下重启更加适合。举个例子,当大量数据库查询积压时,重启可以让新的查询实时完成,队列中的查询则另选恰当时间进行。
11. 每台服务器上都运行了Ejabberd和Riak,Riak作为服务器使用。在需要时,可添加服务器对系统进行横向扩展。Ejabberd和Riak运行在不同的集群中。
12. Riak服务器使用了多数据中心备份机制,它们还会提供数据给第二Riak集群。类似社交图等昂贵的ETL查询都运行在第二集群上,从而避免主集群受到影响。备份操作同样会在第二集群上进行。
13. 扩展性、性能和容错机制是个长期奋斗目标,大部分的Ejabberd代码都已经被重写。
- 重写以匹配自己的需求。举个例子,LoL中只存在双向好友关系,但是XMPP机制却允许不一致的好友关系。也就说是,基于XMPP建立好友列表需求16条客户端与服务器之间的消息(对于数据库来说这是一个非常重的负载),而重写后的协议完成这个操作只需要3条消息。
- 移除不必要及不期望的代码。
- 优化协议的本身。
- 编写测试以避免崩溃。
14. 消除明显的瓶颈。
15. 避免共享可能会变化的状态,这样可以更容易的进行大规模线性扩展。
- Multi User Chat(MUC)。每个Chat服务器都可以支撑数百万连接数。每个用户连接中都包含了一个会话进程,当用户期望修改状态或者给一个房间发送消息时,事件则会被传送到一个被称为MUC路由器的单进程,然后MUC会将消息传递给相关的群聊。这是一个很明显的瓶颈,解决的方法是并发路由。优化之后,群聊房间的寻找会放在用户会话中,从而利用所有的核心。
- 每个Ejabberd服务器都包含了会话列表的一个副本,它是用户ID和会话之间的映射。发送消息需要查找用户会话在集群中的位置,随后消息会被写入会话列表。通过校验会话是否存在、优先级以及一些其他的查询,写入操作的数量可以降低96%。这些校验对系统的提升非常大,可以大幅度减少用户登陆操作及状态修改时间。
16. 增加功能以获得生产环境中代码的能见度。让代码可以在涉及到同一事务的多个服务器上同时升级。
17. 优化Erlang VM中的服务器调试功能。获得会话内存使用情况,以更好地进行内存使用优化。
18. 项目开始时就考虑到了数据库扩展性。开始时选择的MySQL造成了性能、可靠性、扩展性等多方面的问题。比如,数据模式的修改速度匹配不了代码的变更。
- 因此他们选择了Riak。Riak是个分布式的高容错键值存储。无主的机制让它可以避免单点故障,即使两台服务器同时发生故障也不会影响服务或丢失数据。
- 需要在chat服务器上投入大量的精力以实现最终一致。实现了一个Ejabberd CRDT库处理所有的写入冲突。尝试将对象转换到一个稳定的状态。
- CRDT是如何工作的?取代给好友列表直接添加一个新层,CRDT中为对象维护了一个操作日志,日志中记录的格式类似“Add Player 1”和“Add Player 2”。下一次对象会以请求的方式读取日志,从而解决了任何冲突。提供给对象的日志是无序的,因为这里并不需要去关心顺序。这样操作保证了好友列表的一致性。这里的理念是在合适时即对值进行修改,而不是为对象建立一个冗长的操作日志,并且只在对象的读取时完成操作。
- Riak是个非常大的成功,它提供了几乎线性的扩展性,鉴于对象可以被非常快的修改还提供了不错的模式灵活性。
- 这是一个非常大的观念变革,它改变了服务测试和工具建立的方式。
监视
- Chat服务建立了500个以上的计数器,每分钟都会对结果进行收集并传送给监视系统(Graphite、Zabbix、Nagios)。
- 为计数器设定了阈值,在超过警戒线时会进行提醒。因此,在影响用户体验或者系统发生问题之前,问题就会被定位。
- 举个例子,最近有一次客户端升级造成了无限广播用户状态的问题。着眼Graphite,工程师很快定位到服务器因新客户端上线(带来的新特性)而崩溃。
实现Feature Toggles(功能表示)
- 在无需重启服务的情况下实现新和功能上线下线。
- 新功能开发时就会被添加Toggles(on/off )特性,如果一个功能导致问题产生,工程师可以立刻关闭它。
- 部分部署。新代码可以只对某些特定的用户开放,或者只是某些特定的用户可以激活新代码,这允许在某个范围内测试风险较高的功能。一旦该功能通过测试,它就会被发布到所有用户。
动态代码重载
- Erlang的一大特性就是动态热加载新的代码。
- 在第三方客户端(比如 pidgin)并没有经过良好的测试时,比如它会发送与官方客户端不同类型的事件,补丁在无需重启整个chat服务器时就可以快速被部署并集成到chat服务器,从而显著的减少玩家宕机。
日志
- 记录所有异常情况,比如错误和警报。
- 服务器同样提供了健康检查报告,这样就可以查看日志(登陆用户数量、接受新的连接数以及好友列表修改情况)并决定这个服务器是否运行良好。
- 为调试模式嵌入选择用户会话功能。如果存在可以或者测试用户(生产服务器上的QA测试),即使chat服务器上存在10万个会话,需要加载的会话也只有一个。日志只包括XML流量、事件以及度量,这将节省大量的日志空间。
- 通过整合功能标识、部分部署和日志选择功能,系统已经完成了给部分用户推送新功能的准备;同时,系统还可以在没有其他用户干扰的情况下收集和分析日志。
加载测试代码
- 每天晚上,自动校验系统都会在测试环境中部署所有改变,并进行一连串的负载测试。
- 测试过程中,服务器健康状态会被监控,度量会被取出并分析。系统会建立一个Confluence页面来记录所有度量和测试结果,测试结果概要会通过邮件发送。
- 可以做功能发布前后的对比,这样很容易对比代码改变产生的影响,也很容易定位造成灾难或者提升内存消耗X个百分比的问题。
未来的工作
- 弃用MySQL。
- 将chat服务扩展到游戏外,这样玩家在不登陆游戏的情况下就可以与好友交互。
- 通过社交图来提升体验。分析玩家关系,并找出影响游戏兴趣的原因。
- 计划将游戏内chat迁移到游戏外服务器。
学到的知识
1. 故障肯定会产生,你不需要做到完全控制。即使你的代码不存在bug,也可能存在一个ISP路由器以及10万个用户同时丢失的情况。做好万全之策。确保系统可以承担同时丢失一半用户的情况,或者在丢失1/4 chat服务器的情况下不会影响到性能。
2. 规模扩大小概率事件变常态。如果某个bug发生的概率是十亿分之一,但是在LoL的规模,这个bug可能每天都会发生一次。即使某些完全不可能发生的事件都有可能发生。
3. 成功的关键是清楚系统发生的所有事情。必须清楚你系统是健康的或者濒临崩溃。
4. 指定一个策略。LoL为其chat服务选择了横向扩展策略。为了支撑这个策略,他们选择了一个不同的途径来支撑这个策略。他们不仅选择了Riak这个NoSQL数据库,同时还挑战了CRDTs这个途径,只为了横向扩展能尽可能的无缝和强大。
5. 可用。贯穿开始和衍变。他们开始于Ejabberd,这并不一定代表着Ejabberd更容易开始,但是Ejabberd绝对可以更匹配他们的需求。
6. 让一切更可见。增加追踪、警报、监视、同样一级一切有意义的东西。
7. 让系统可运维。LoL给软件更新添加了事务特性,还给系统添加了功能标识、热更新、自动化测试加载、高可配置日志等级等功能,这一切都只是为了更容易管理。
8. 减少无用协议。定制系统所需的功能。如果你的系统只存在双向好友关系,那么你不再需要那个通用的昂贵协议。
9. 避免可变状态共享。这条已众所周知,但是仍然有系统会共享可变状态,从而导致横向扩展时的各种问题。
10. 利用好你的社交图。Chat服务提供了一个原生的社交图。这些信息可以被用于提升用户体验,以及开发更有意思的新功能。
原文链接: How League Of Legends Scaled Chat To 70 Million Players - It Takes Lots Of Minions. (翻译/童阳 责编/仲浩)