代码改变世界

聊天室服务分析设计

2013-03-05 09:52  轩脉刃  阅读(16099)  评论(25编辑  收藏

如果你需要写一个简单的聊天室的服务,那么我想很多网上的demo都可以直接拿来用。但是如果你要做的是给线上百万甚至千万级用户用的服务,那么,整个结构和聊天室Demo是必然不一样的。本文就从设计一个大用户量的聊天室服务的角度出发来思考。

分布式?

首先用户量大必然先考虑的问题是服务是单进程还是多进程,单机器还是多机器,单进程代表的是单机上跑一个服务,单机器代表的是单机上跑一个或者多个服务,这两种方案都是不可行的。理由是考虑下面几个方面:

1 单进程或单机器对机器性能要求较高:由于一台机器上的一个进程直接服务于这么多用户,因此在内存,带宽,CPU等要求上是非常高的。

2 单进程必然导致高内存,高内存的内存回收机制会对服务稳定性产生影响

3 单进程的进程容错性不好。一旦这个进程挂了,那么整个服务就会处于瘫痪

4 单进程的扩展性不好。由于服务用户必然是从少到多,我们需要的是一个可以不断增加机器就能增加抗压性的服务。

 

于是考虑使用到多机器多进程(分布式服务)。

谈到多进程的架构设计,避免不了的是进程间通信的问题。两个进程进行相互通信分为两种情况:同机器两个进程的相互通信,不同机器的两个进程的相互通信

同机器两个进程间的通信机制有:Unix管道,信号(比如我们平常使用的Ctrl + C),共享存储(包括把需要通信的信息放在文件或者内存中)

不同机器的两个进程通信机制就要使用到socket通信了。通过TCP从一个进程发送信息给另一个在监听端口的进程,等候监听端口的进程会返回需要的结果的整个模型就是RPC模型(远程服务调用)。RPC模型也有两种,PRC via TCP或者RPC via HTTP。RPC via TCP相较于后者传输的数据更少(少了HTTP包的那些部分),所以效率更高,但是却实际上不适合在Internet这样存在着危险的环境中使用。所以RPC via TCP适合在内网的两个进程间交互使用。

 

对于聊天室的需求,服务器一般是假设在内部网络上,不需要对外进行服务提供,所以应该选在RPC via TCP的方式最好。

如何保证实时?

对于一个聊天室服务,聊天的实时性是最硬性的需求。这里该考虑使用什么技术来完成实时性的需求了。

一般有几种方法能做到实时性或者是近实时性:

1 HTTP轮询。

客户端每隔一段时间(较短的时间)对服务端进行HTTP请求,询问是否有发送给自己的聊天消息。如果有则返回具体消息,如果没有就返回空消息。

这个方法的缺点有几个:

a 不实时:较短的时间再短也不能说是实时的。

b 浪费请求:必然是空消息的请求次数大大多于有消息的请求次数。那么意味着大多数的请求实际上都是没有用的。造成的结果就是浪费了流量,增加了服务端的压力。

2 HTTP  Long Polling

算是第一种方法的优化。具体表现就是发送了HTTP Request之后不立刻返回HTTP Response,而是把HTTP请求hold住在服务端,等到有消息返回的时候,再返回HTTP Response。

这种实现方法就将第一种的两个缺点给解决了。老王这篇文章写了一个nginx+lua的实现:http://huoding.com/2012/09/28/174

3 TCP长连接

客户端发起TCP的连接,与服务器建立连接后,客户端和服务器都不关闭连接。如果有消息到达,服务端就会主动推送消息给客户端。这就是“推”的机制。

这个方法看起来是最美好的。因为它能完全保证消息的实时性,也能最小限度的节约请求。但是放到具体的环境中就有几个问题了:

首先是在BS架构中,浏览器充当客户端,这个方法就需要浏览器能发送TCP请求。在HTML5出现之前浏览器是只能发送HTTP请求的。但是好在HTML5推出了websocket协议,它使用HTTP握手+TCP传输的方式来实现了浏览器与服务器的TCP连接。具体的协议细节可以看http://www.cnblogs.com/yjf512/archive/2013/02/18/2915171.html

其次,如果你有客户端,比如QQ,手机或者flash等,你就需要自己定义一套可扩展的协议了。比如最简单的设计可能是:前32个bit是代表内容byte长度,后面是json化的内容。

 

还有就是考虑到安全性,如何保证TCP网络包的安全,你可能会用到签名,加密(对称、非对称)等方法。

 

对于聊天室的需求,websocket是最佳选择。

要提供哪些分布式服务呢?

下面进行服务组件设计。

connector服务

使用长连接,必然有个服务组件专门服务于长连接的建立,维护,接收和发送消息。我们可以称之为connector。

这个服务需要是分布式的,有多少用户就会有多少个长连接,维护这么多长连接需要使用的是多个机器的服务。而且如果允许的话,建议connector能监听在80端口,以避免客户端的防火墙对端口的限制。

chat服务

connector是暴露在服务器前端的,后端必然有个服务能处理聊天的业务逻辑,其功能包括:

A 管理聊天室的用户

B 接收connector发送的群聊或者单聊的消息(通过进程间通信)

C 发送群聊和单聊的消息给对应的connector(通过进程间通信)

这个业务逻辑服务我们称之为chat

 

这里就有几种设计了

管理聊天室的用户?

这实际就是一种存储的功能,那么能不能使用现有的存储服务来做呢?nosql(比如redis)?

这种设计是完全可以的,redis的存储是放在内存中,存取的效率是有保证的,唯一无法保证的可能就是网络传输了。

缺点很明显:

A 业务逻辑“获取聊天室用户”的请求量一定非常大,这个请求如果是通过网络传输来请求的话,那么对网络和存储服务的稳定性等要求较高。一旦网络不稳定,那么服务必然受到影响(但或许想想,什么服务不是这样呢的)。

B 存储服务是否要分布式的呢?估计也是需要的,那么可能还需要建议一个hash算法来定位和保证扩展性。

使用存储服务的好处当然也不可忽略,比如可以使用现成的配套服务(比如redis的持久化),现成的数据结构存储(比如redis的zset等结构),以及监控等服务。

在这个聊天室需求里面,我们可能更倾向于不使用存储功能,直接把管理聊天室用户的功能放在chat服务中。那么我们就需要存储每个用户在哪个connector中存储的情况了,就是保存用户session信息。

第二个问题:chat是分布式的,那么一个聊天室是分布在一个chat进程中还是分布在多个chat进程中呢?

从效率角度上来说,一个聊天室之在一个chat进程中是最为好的方法。因为减少了进程间通信。聊天室内部的消息传递只要在进程内部进行交互就行了。

但是考虑下这么个极端情况,某天搞一个活动,一个聊天室人数非常多,另一个聊天室人数却很少,两个聊天室是分布在不同的进程中的,那么就会导致的情况可能就是一个进程占用非常多资源,另一个进程却非常空闲。这个时候多么希望空闲的进程能够分担繁忙进程的服务!

其实作为聊天室的需求,第二种情况是比较极端的了,所以可以不用考虑。但是或许在其他的场景中(游戏等)这种情况就需要重点考虑了。

 

下面考虑一些配套服务组件

gate服务

首先是connector的分配问题,我们不应该直接将connector的选择抛给客户端,由客户端决定连接哪个connector,因为这样很容易会造成connector的负载不平衡的问题。

所以我们设计在connector之前使用一个gate服务,算是一种负载均衡服务,它的行为是:

客户端先连接gate,gate告知客户端应该连接哪个connector,并将connector的地址端口等信息返回,后客户端连接指定的connector

但是这里其实也是存在安全隐患的,如果攻击性的客户端跳过gate的负载均衡,直接连接connector,那么可能导致某个connector崩溃,可能会出现雪崩效应了。安全的问题,深究下去是个很复杂的工程。

master服务

分布式系统最头疼的可能是服务管理了,不管是进程的启动,关闭,更新等操作,如果一台一台机器一个一个进程进行操作很容易导致操作者的崩溃。所以就需要一个服务能一次性进行所有机器的启动,关闭,重启等操作。

我们希望达到的最终结果是启动了主服务器的master进程,本地和远程的connector,chat,gate进程都会根据配置文件进行启动。当然本地的进程启动相对容易,不管是什么语言都应该会有command.exec类似的命令来启动,远程的进程启动则稍微麻烦些:

归结起来有两种方法:

1 ssh进行远程机器调用命令。这个就要求进行ssh的帐号在其他机器上有存在,并且在~/.ssh下存储了对应的RSA等密钥信息。那么就可以使用这个帐号来调用远端机器的进程了。

这个方法有个不好的地方,就是帐号权限,一般出于安全考虑,这个ssh的帐号不可能是root帐号。但是不用root帐号启动,就可能遇到各种限制(比如无法占用80端口来启用connector)

2 在每个机器上启动一个启动和关闭聊天服务的服务,我们叫做deploy。这个deploy对master提供RPC服务,master通过RPC调用来发送“启动/关闭/更新 进程”的命令。

这个方法的好处就是可以使用root来启动deploy了,但是不好的地方是deploy只能由手动启动了...

例子

chatOfPomelo是在网易的Pomelo游戏服务框架上搭建的一个聊天服务。使用这个例子,我们就能很好的了解分布式聊天服务器的架构

在官网上是有个架构图的,https://github.com/NetEase/pomelo/wiki/pomelo%E6%9E%B6%E6%9E%84%E6%A6%82%E8%A7%88,原图稍微复杂了点,我这里做一下修改:

Image

联系到上面的分析可能就更好理解这个图了。

后记

任何设计都是存在弊端的,但是只要这个弊端不会在需求中被体现,即能满足需求,就是一个好设计,所以说在不明确的需求面前,任何设计都不能说是正确的。