通常把跟客户端直连的服务器称为接入服务器,一个或多个接入服务器构成的接入层。接入层有以下功能:
- 维护与客户端之间的网络连接,管理客户端的网络状态。
- 接收客户端请求,将请求转发到业务层,转发业务层发给客户端的数据。
- 就近接入,负载均衡,优化网络体验。
这里可以发现,如果把接入层跟业务层合并也可以实现以上的功能,而且节省了实现功能2需要的工作量,根据简单性原则,接入层不应该被独立出来。对于一个功能单一,用户少,并发小的系统,接入层的确没有必要独立出来。但对于一个复杂的系统来说,如果把也接入层和业务层合并势必导致某一业务模块的代码代码复杂,如下图所示:
这是一个电商系统架构的一部分,用户模块中合并了接入层。一般来说,后台系统都有一个从简单到复杂的演进过程,在个过程中会经历多次版本迭代,每次都会有开发-测试-发布上线,每次发布上线都会重启服务器。拿着里的用户模块来说,第一个版本一般很简单,只有用户注册,登录,查询用户信息这几个功能,为了提升转转化率,很快就会加速未注册用户购物购物流程,就需要用模块增加管理临时用户的功能。随着用户量增加,用户模块还需要设计成分布。这样用户模块就需要经常因为发布上线而重启,重启时就会因想用户体验,更严重的是如果用户正在进行像支付这样的跟钱有关的操作时,很有可能造成直接的经济损失。同样地,接入层的功能迭代是也会影响到新用户注册和老用户登录。如果把接入层独立出来,则可以有效的减少这样的故障。
功能越简单,改动越少的服务器,越容易做得稳定。相对于业务层模块,接入服务器功能相对简单很多,也很少有变化。也就是说,接入服务器比业务层模块更容易做得稳定。接入服务器是直接更用户连接的,它直接影响用户体验。接入服务器故障或者是重启肯定会影响到用户,而其他业务模块故障或者重启则不一定会影响用户体验。从这一点上说,应该尽量避免其他模块对接入服务器的不良影响。
接入层独立出来有下好处:
- 降低接入层与业务耦合度,减少地稳定度模块对高稳定度模块的不良影响。
- 使接业务层专注于业务处理,降低业务层设计的复杂度。
- 接入层专注于消息转发,可以有效降低消息的丢失率,从而提高系统的稳定性。
- 接入层以较小的代价大幅提高用户接入体验。
架构设计的终极目标是满足业务需求,接入层设计也不能例外。设计接入层时需要搞清楚这样几个问题:网络延迟有什么要求?并发有多大?但消息平均长度多少?用户规模多少?用户地域分布是什么情况?用户的网络环境怎样?对这几个问题有了清醒的认识就能就能设计出恰到好处的接入层架构。
接入层的架构是随着用户量增加,流量增大,访问并发增大由简单到复杂演进的,如下图所示
1. 单个IP地址接入
2. 多个IP地址随机接入
3. DNS根据用户位置和用户的网络运营商返回接入地址
4. DNS根据用户位置和网络运营商返回二级引导服务器地址,二级引导服务器根据根据负载情况和业务需求返回接入地址
以上四种架构中,第1中可以用开源的DNS服务器bind架设。第2,3,4中架构需要自己开发一个DNS服务器,这里面的关键是需要一个IP库,这个IP中记录了不同的IP地址段所属的地理区域及运营商。
客户端状态管理
管理客户端的状态是接入层的另一个主要自责,当客户端连接到接入服务器后,这个连接会有不同的状态,如游客状态,登录状态等。对于不同类型的连接,管理状态发方法也不一样。一般来说,客户端与服务器之间可以有两种不同的连接类型:
1. 长连接。使用TCP协议,TCP连接建立成功后需要尽可能地保持,通过心跳保持连接,实现重连。一般用在客户端需要被动接受数据的场景。
2. 端连接。TCP,UDP皆可,一个请求--返回为一次数据交换,完成之后如果是TCP会断开连接。
消息识别
管理客户端状态,第一步必须要识别客户端的消息。假如有A,B两个客户端连接上了服务器,服务器收到了m1, m2两个消息,处理这两个消息需要建立如A->m1, B->m2这样的对应关系。现在的做法是使用session来识别消息,每个session都有一个的session ID, session ID由服务器生成,服务器要保证它的唯一性,客户端与服务器器之间通讯过程中,每一条消息都要带上这它。 session有过期时间,过期后需要重新建立session。
session存储
session数据有一下几个特点:
1. 单条数据量小, 每条session数据一般在256Byte以内。
2. 数据总规模增加速度慢,每100w个session也就300MB左右。
3. 读写频率高。相对于其他数据的来说,session的读写平率很有可能是最高的,因为每一条消息都会触发至少一次的session读-写。
基于以上3个特点发现,把session放在内存中是最划算的,占用内存空间不大,读写速度快。假如session的过期时间是1天,系统的日活跃有100W用户,存储session所需要的空间是300MB,这个量级服务器表示很轻松。笔者这里推荐使用redis存储session,既能满足要求,又简单。
用户状态管理
在一个系统中,用户一般有两种:匿名用户和注册用户。
用户进入系统后没登录的就是匿名用户,这个时候需要使用session来标示用户的临时数据。例如一个电商网站,在用没有登录的情况下也可以往购物车里添加商品,此时购物车里的数据就是临时数据,当session过期后,这些数据会被删除。
用户进入系统登录后,此时这个用户就是注册用户,每个注册用户都有一个唯一的用户ID,通过这个用户ID,把用户和session绑定。这样就可以通过session找到用户ID,通过用户ID找到用户的私有数据,进而向用户提供各种服务。有时候,还有通过用户ID找到session, 这就要求在把用户ID和session绑定的时候同时建立用户ID到session的key-value关系。
消息转发
如果有需要接入层还要负责消息转发,消息转发有两个过程:生成消息,推送消息。消息可以由用户生成,如用户的聊天消息;也可以有系统生成,如你在你在使用一个APP时收到的广告。消息生成之后接下来就是要把消息推送给用户,用户能够收到消息的前提条件是客户端与服务器之间保持一个长连接。一条消息可以定向推送到特定的一个用户,也可以推送到多个用户,这两种场景都很常见。成功地把一条消息推送给用户,首先要确定消息需要推送给哪些用户,然后再找出这些用户与哪些接入服务器建立了长连接,然后把消息消息推送到相应的接入服务器,剩下的就是接入服务器的事了。
接入服务器收到需要推送的消息后,会根据消息头中目标用户ID找到session, 从session中取出长连接的socket文件描述符,发送消息。这样做理论上没问题,但实际上很容易出错。在linux上,socket文件描述符是循环使用的。如果用户A 的连接短开了,如果用A重连之前,用户B建立了连接,这时用户B的连接文件描述符与用户先前用户A的是同一个。这个时候试图推送给A的消息会推送给B。这个文件的解决方案是每个连接建立的时候,给文件描述符生成一个在一个较长时间内不会重复的流水号,使用这个流水号来查找文件描述符。