Pomelo 概览 - 设计动机、框架、工具和库等

 2017年08月05日    311     声明


游戏服务器不同于Web服务器,其在逻辑复杂度、消息量、实时性等方面有更高的要求。本文参考官方文档的Overview部分,进行简单的汇总整理,对Pomelo的设计动机、Pomelo 框架及相关工具和库等进行介绍。

  1. 设计动机
  2. Pomelo 框架概览
  3. Pomelo 工具与库
  4. 客户端平台支持

1. 设计动机

最初,Pomelo 被设计为一个游戏服务器,但在设计和开发完成后,发现其可做为一个通用的分布式、实时应用程序开发框架。在此,通过分析游戏服务器需求来说明一下Pomelo的设计动机。


1.1 什么是游戏服务器

没有做过游戏服务器开发的人,可能会觉得游戏服务器很神秘。但实际上,它并不比Web服务器复杂,它只是为客户端的网络请求提供服务。本质上它只是基于长连接的socket服务器。相比Web服务器来说,游戏服务器在逻辑复杂度、消息量、实时性等方面要求更高。

以下是游戏服务器和Web服务器之间的一些差异:

复杂的Socket服务器

我们可以将Web服务器做为一个HTTP服务器看待,而将游戏服务器做为一个原始的Socket服务器。其通过Socket通讯来处理服务器和客户端之间的交互,因此许多游戏服务器直接基于原生Socket(即TCP)来实现。

相比简单的Socket服务器,游戏服务器的任务更为繁重,主要体现在以下几个方面:

  • 后端服务器必须处理的极其复杂的游戏逻辑
  • 网络流量更大,且实时性要求高
  • 通常会使用集群提供服务,以提高并发量

长连接及时实性

Web服务器使用基于连接的响应/请求模式,而游戏服务器使用长连接,因此Web服务器所持有和需要的资源要远远少于游戏服务器。基于HTTP使用短连接可以大大Web服务器的伸缩性。

Web服务器可以使用短连接是因为:

  • 单向通信 - 典型的Web应用程序只需要支持“拉取”模式
  • 低实时性要求 - 一般来说,Web服务器在3秒内做出响应,就不会明显影响用户体验

而游戏服务器只能使用长连接,其原因如下:

  • 双向通信 - 游戏服务器不仅需要“拉取”模式,还需要支持“推送”模式。更重要的时,数务器推送的数据量要远远超过客户端拉取的数据量。
  • 高实时性要求 - 一般来说,服务器推送消息或响应客户端请示的时间应该小于100ms。

分区策略和负载均衡

一般来说,Web应用之间没有交互的概念,所有用户之间的交互是平等的。在Web应用中,交互频率与用户的地理位置无关,而在游戏应用中则相反。游戏服务器中,玩家的义互频率与玩家的位置(区域)密切相关。如,两个相邻的玩家会互相攻击或者组队来攻击怪物,他们之间的交互会非常频繁且实时性要求很高。这也意味着,两个玩家需要被分配到同一区域服务器进程中,以减少跨进程成本。

因此,游戏应用应该根据区域的不同,而有一个分区策略。这也是与Web应用的不同之处,如下所示:

服务器进程可能位于一个区域或多个区域,因此游戏服务器的可伸缩性会受区域的限制。如果一个区域太忙,超出了它的容量,那么整个游戏服务器就会被阻塞或关闭。区域服务器是有状态的,来自特定玩家的请求必须发送到同一区域服务器。

有状态的服务器会给我们带来了很多问题,会导致该区域服务器在可扩展性和可用性方面不如Web服务器。通常,我们必须通过隔离游戏服务器来缓解这些问题。

Web应用可以以常规方式来划分负载均衡,而游戏应用会基于区域策略进行划分,使同一区域内的玩家能够在同一区域的服务器进程中运行,以减少跨进程成本。

可扩展性与分布式

无论是Web应用程序还是游戏应用程序,可伸缩性都是最重要评估指标之一。同时这也是要解决的难题之一,会涉及到运行架构及各种优化策略。可以通过一个可扩展设计,以保证在线玩家数及响应时间。在传统游戏服务器的架构中,会使用一个单进程模型来处理所有逻辑,在这种架构下,线上没有大量玩家时可以使用。但随着玩家数量的增加,会带来巨大很大问题。因此,分布式、多进程的游戏服务器架构就成为必然的选择。

以下说明了Web服务器和游戏服务构架的不同之处:

可以看出,Web服务器可以通过单个负载均衡器将请求重定向到任何进程,因此它的运行结构相对简单,也很少需要分布式。而游戏服务器使用了蜘蛛式网络架构,每个进程都有自己的职责,这些进程又相互交织在一起完成一项任务。因此,游戏服务器是一种典型的分布式体系结构。


1.2 难点

通过以上分析我们可以知道,游戏服务器使用了蜘蛛式网络架构,这也给游戏开发带来了一些困难。包括:

实时性保证

对于一个游戏服务器来说,会包含一些实时性任务。包括:

实时tick

通常,游戏服务器需要定时tick来执行定时任务。为了实现实时的行为,这tick定时器会在100ms。这些任务一般包含以下逻辑:

  • 遍历区域中的玩家、怪物等实体,并对它们进行一些操作,如:移动、复活、消失等。
  • 在区域中部分怪物被干掉后,再定时制造一些怪物
  • 定时的AI逻辑,如:怪物攻击、逃跑及其它逻辑

由于定时器间隔在100ms以内,所以,上面任务的处理时间也应该控制在100ms以内。

广播

当一个玩家做某一动作后,必须实时通知同一区域内的其它玩家。这时就需要广播,这也使得游戏应用的网络需求远远高于Web应用。

广播在游戏中的开销很大。由于玩家间输入/输出信息的不对称,如:玩家只是稍微移动,服务器就要把这个动作传递给所有其他玩家,以使他们可以在同一个区域看到这个玩家。当一个区域内的玩家数量较少,广播消息数量不多,但如果玩家数量达到较高水平,广播消息数量将成倍增长。如下所示:

如上,当某一区域中有1000个玩家,每个玩家做一个动作,服务器需要通知区域内的所有玩家,这时广播的消息将达到10000000个,足以阻塞服务器而放弃其它任何操作。

分布式

我们可能会在很多地方看到这一观点:分布式开发是困难的。主要体现在以下几点:

多进程(服务)管理

游戏服务器通常采用多进程模型。这些进程间又会有相互,因此这些进程的管理非常困难。

如果没有对服务器(进程)的统一抽象和管理,在开发环境中启动这些服务器会非常复杂,也会很影响开发效率。更重要的是,重量级进程消耗了大量的机器资源,一般用于开发的服务器无法承受这么多处理,而多服务器又会带来进程间调试的困难。

RPC调用

RPC调用方案已经出来很多年了,但开发效率仍没有显著提高。

以下我们通过一个流行的RPC框架thrift,来演示RCP调用流程。在这个流程中会包含以下步骤:

  • 编写一个.thrift文件
  • 使用.thrift文件编译生成一些源码:
    thrift - gen  
  • 使用生成的源码进行开发

而当所定义的接口发生改变时,我们就需要重复以上过程。在不稳定的开发环境中,这种RPC调用方式会严重影响开发效率,因此我们需要一种更灵活的方式。

分布式事务&异步操作

尽管我们试图把相关逻辑放到一个进程中,分布式事务仍然是不可避免的。在使用普通编程语言时,分布式异步提交操作不是一件容易的事。

负载均衡&高可用

由于游戏服务器是有状态的,所以特定玩家的请求需要通过路由规则路由到同一服务器。我们可以很轻松的将请求路由负载到无状态Web服务器,而对于有状态的服务器,使其高可用是非常困难的。但也有方法可以做到这一点,这里介绍以下两种方法:

  • 将状态保存在外部存储中 - 如,我们可以将玩家状态保存在Redis或类似存储中,这样服务器就不需要再状态,从而变成无状态的。但这状态相关操作将会经过Redis,这也会导致性能的损失。
  • 服务器冗余 - 服务器的状态可以通过日志备份到另一个冗余服务器,但服务器切换时可能会出现短暂的数据丢失问题,并最终导致数据的不一致。

在Pomelo V0.5中提供了高可用机制,通过zookeeperredis可以解决一些服务器(如:主服务器)高可用问题,但在实际复杂应用中还需要由应用自己来处理。

原生Socket开发中的问题

我们可以基于原生Socket进行开发,但使用原生Socket也会带来一些问题:

  • 低级抽象 - 低级的Socket抽象不易用,许多处理机制需要由开发人员来实现,如:Session、过滤器、请求、广播等。
  • 可伸缩性 - 可伸缩性取决于奶多方面,如:消息密度、存储策略、服务器架构及其他因素。如果需要提高原生Socket的可扩展性,开发者需要在架构设计上花费大量精力。
  • 服务器监控管理- 服务器状态监控是很必要的,如:消息密度、在线玩家数、机器状态、网络压力等。如果使用原生Socket套接字这样,这也有很大的工作量。


1.3 基于框架的解决方案

因此,我们需要一个游戏开发框架。除了游戏逻辑外,其它工作都可以由框架完成,服务器的抽象性、可伸缩性、可扩展性都可以由框架解决,以避免重复开发。

Pomelo 是基于Node.js的使用MIT开源许可证的开源框架,它旨在提供一个高性能、可伸缩、轻量级的游戏服务器框架。与其他类似的框架相比,它的主要有以下优点:

  • 可以快速开发,基于约定优于配置的原则,使代码更简单易用
  • 提供了高可用性和可扩展性,使得扩展应用程序非常方便
  • 轻量级的,分布式体系结构,可以快速启动,且只需要很少的资源


2. Pomelo 框架概览

一个可扩展的游戏服务器的运行时架构必须是多进程的,因为单进程扩展受限。谷歌的gritsgame和Mozilla的Browserquest都是使用Node.js作为游戏服务器平台,但他们都是一个单进程模型,这意味着他们的在线用户数是有限的,且缺乏扩展性。

相比来说,Pomelo具有明显的优势,其特点如下。

2.1 典型的多进程架构

如下所示,是一个典型的多进程MMO游戏服务器的运行时架构:

在以上架构中:

  • 图中每个矩形代表一个进程,其可以认为是概念上的“服务器”
  • 客户端通过websocket或socket连接到connector服务器
  • Connector被视为前端服务器,它们不负责任何游戏逻辑,只是把客户机的请求转发给后端服务器
  • 后端服务器包括areachatstatus及其它服务器,在实际情况中可能还会有许多其他类型的服务器,它们负责各自的业务逻辑。后端服务器会将逻辑处理的响应发送给Connector,然后Connectorg再通过广播/响应将消息推给客户端
  • 主服务器(Master)负责管理所有这些服务器,包括启动、监视和停止等


2.2 Pomelo 框架介绍

Pomelo 框架中的组件

Pomelo 框架中包含以下组件:

  • Server Management

    Pomelo采用多进程模型,因此管理服务器尤为重要。框架中对服务器的抽象,使得服务器管理非常简单

  • Network

    Pomelo 框架中的网络包括两部分:前端服务器和客户的通讯、服务器集群间通信。包括请求/响应、广播、会话管理、RPC调用,以及所有由这些通信构建游戏逻辑流。

  • Application

    松散耦合的体系结构是至关重要的,应用被视为一个全局上下文来支持组件的生命周期、应用程序DSL等,也使得Pomelo框架可插入并易于扩展。


2.3 设计目标

  • 服务器(进程)抽象

    在Web应用程序中,服务器是无状态的、松散耦合的,也就是说不需要用框架来管理所有这些服务器。但是游戏服务器不同于Web。不同的游戏可以包括各种服务器类型,不同类型的服务器的数量也不同。所有这些都需要框架来支持服务器抽象和解耦。

  • 抽象请求/响应、广播

    与Web应用相比,请求/响应在游戏应用中是相似的,但是游戏基于长连接,因此需要一个通用的请求/广播机制。由于广播是游戏应用程序中最频繁的操作,因此框架需要提供一个方便的API,并使其尽可能高效。

  • 服务器之间的RPC调用

    服务器之间需要会话,尽管我们尝试避免,但进程间通信是不可避免的,所以需要一个用户友好的RPC框架。

  • 松散耦合的、可插拔应用

    应用扩展是非常重要的,Pomelo 框架支持扩展,你可定义自已的组件、路由规则、请求筛选器、admin模块,并将这些插入到Pomelo框架中。


服务器抽象

服务器类型

Pomelo中有两类服务器:前端服务器、后端服务器。如下所示:

其中,前端服务器负责:

  • 处理客户端连接
  • 维护客户端的Session信息
  • 定向客户端请求到服务端
  • 将后端服务器的广播/响应消息发送到客户端

而后端服务器负责:

  • 处理业务逻辑,包括Rpc请求及来自前端服务器的请求
  • 推送响应消息到前端服务器


鸭式服务器

“鸭式”是面向对象(OOP)动态编程语言中的一个常用概念,这一概念也可以被用于服务器抽象。我们只需要定义两种服务器接口:一种是处理来自客户端的请求,被称为Hanldler;另一个处理RPC调用,称为Remote

这样,只要我们为服务器定义了remote和handler,就可以确定服务器的类型及其功能。


服务器抽象的实现

实现服务器的最简单的方法是使服务器代码的组织结构对应相应的目录。如下所示:

如,我们可以定义一个名为"area"的服务器,服务器的具体行为由"handler""remote"中的代码决定。对于开发人员来说,只要和handler和remote中编写对应的代码即可。

为了让服务器运行起来,需要在servers.json文件中进行一些简单的配置:

{
  "development": {
    "connector": [
      {"id": "connector-server-1", "host": "127.0.0.1", "port": 3150, "clientPort": 3010, "frontend": true},
      {"id": "connector-server-2", "host": "127.0.0.1", "port": 3151, "clientPort": 3011, "frontend": true}
    ] ,
    "area": [
      {"id": "area-server-1", "host": "127.0.0.1", "port": 3250, "area": 1},
      {"id": "area-server-2", "host": "127.0.0.1", "port": 3251, "area": 2},
      {"id": "area-server-3", "host": "127.0.0.1", "port": 3252, "area": 3}
   ] ,
    "chat": [
      {"id": "chat-server-1", "host": "127.0.0.1", "port": 3450}
    ]
  }
}


请求/响应、广播

虽然在游戏中我们会使用长连接,但是请求/响应的API也类似于Web。如:

Pomelo中请求/响应API很像“Ajax”,但它使用是长连接。在如上所示的请求路由"chat.chatHandler.send"中,其中chat表示服务器类型、chatHandler表示处理器(handler)、而send表示具体的请求处理方法。基于“约定优于配置”的原则,这里不需要任何配置,即可完成客户端请求的处理及响应。

除此之外,Pomelo还提供了过滤器(filter)、广播/多播机制、及频道(channel)支持等。


RPC调用抽象

Pomelo 提供了非常简单的rpc框架。它能够根据路由规则路自动选择并调用目标服务器,且不需要任何配置。示例如下:

如,在chatRemote.js文件中,有一个如下的接口定义:

chatRemote.kick = function (uid, player, cb) {
}

我们可以RPC客户端通过如下方式调用:

app.rpc.chat.chatRemote.kick (session, uid, player, function (data) {
}) ;

注意,在在请求参数中session用于路由请求,框架将基于路由规则自动确定所要调用的服务器。


可插拔组件

在Pomelo中组件是可插拔的模块。开发者可以定义自己的组件,并将其插入到Pomelo中。Pomelo的核心功能全部由内置组件实现,换句话说,Pomelo框架只是它的组件的容器。

组件的生命周期类似如下:

开发者自定义的组件中应实现:startafterStartstop等接口,然后就可以app.js文件中像下面这样加载组件:

app.load([name], comp, [opts])

除组件外,Pomelo 还支持插件,插件同样是基于组件实现,可以认为是组件的集合。插件是一个独立的npm模块,使用插件,开发者可以很容易地扩展框架,而不会对框架核心功能产生任何影响。

Pomelo 提供了一些内置插件如下:


3. Pomelo 工具与库

Pomelo 提供了一系列开发工具和库,以帮助开发者开发、调试、部暑。这些工具和库的提供了许多功能,包括服务器管理控制、压力测试及一些常用功能库。


3.1 Pomelo Command-Line Tool

该工具可以帮助开发人员更方便、高效地开发应用程序,其功能包括创建项目、启动应用、停止应用、关闭应用等。详见:


3.2 Pomelo-cli

Pomelo-cli是一个用于管理服务器集群的命令行客户端,它应该首先连接并注册到主服务器,然后就可以通过它向服务器集群发送一些管理命令,如:动态启动服务器、查看服务器状态等。详见:


3.3 Pomelo-robot

Pomelo-robot是一个用于运行基准测试和压力测试的框架。详见:


3.4 Pomelo-daemon

Pomelo-daemon提供了守护进程服务,可以使用此服务进行分布式部署和日志收集。详见:


3.5 Pomelo-admin-web

Pomelo-admin-web是一个基于Pomelo-admin实现的服务器集群监控Web客户端。可以使用它通过浏览器监控服务器集群的运行状态、性能、日志和其他信息。详见:


3.6 Pomelo-sync

Pomelo-sync是一个用于管理数据同步的模块,它可以用于内存和外部存储系统之间的数据同步。详见:


3.7 Pomelo-protobuf

Pomelo-protobuf是一个对google protobuf的变体实现,该模块通过一个json文件代替了原本的.proto文件。它可以动态的解析json文件,从而避免编译.proto文件。在Pomelo框架中,它用于通讯消息的压缩。详见:


4. 客户端平台支持

客户端和服务器之间的协议是开放、可定制的,理论上讲,你可以在任何协议、任何平台上连接到Pomelo游戏服务器。

为方便开发者,Pomelo官方还提供了一些常用平台的客户端SDK,包括:Web、iOS java & android、unity3d、flash、及C语言库等。

以下是SDK列表: