Edooon.com

此篇权当抛砖引玉,大家一起讨论!

重点从技术角度去阐述我理解的整个后端系统的架构设计,当然任何技术的选型不能抛开具体业务场景,所以是在具体业务场景下来思考,如有不正确,欢迎指正!

应用特点分析

总体上来说,edooon是以移动APP为主要数据采集方式的互联网体育类应用,pc端页面访问量不大,其主要功能是运动记录的展现,同时包括部分sns社交功能操作,如圈子、好友管理等等,所以在pc端页面,读远比写频繁;而在app端,写主要是运动记录的上传,读操作主要是个人运动信息,好友运动信息,以及福利、圈子、夺宝以及附件的人等功能,所以app端也是读大于写操作,所以整体来说,edooon是属于一个读大于写的互联网应用。

这种系统特点决定了APP与后端系统之间的交互是整个应用体验的一个重要指标,特别是运动数据的同步,以及其状态的一致性,当然APP本身的设计非常重要,因为其涉及到采集数据的准确性,我们这里只侧重谈后端系统。

存储

目前系统使用mysql存储所有数据,同时结合redis来存储部分数据;mysql存储是独立的,只有redis目前还和应用服务器共用一台,但是就目前redis存储的数据量只有50+M,对应用服务器内存占用不大,但是基于redis的应用一旦在整个系统中使用,就需要将redis独立出来。 mysql是整个edooon的主要存储容器,目前单表的最大记录数已经达到500w条,而且有好几张表,最要命的是很多sql是在这些大表之间做关联查询,全表扫描是常有的事,导致应用响应慢;且整个mysql库是没有备份库的,一旦出现问题,后果可想而知。所以目前最要紧的是将mysql的架构进行重构,主要从如下几个方面进行。

主从备份

现在数据库备份是通过脚本凌晨进行自动备份,这种属于冷备份,存在数据丢失的风险,所以要建立热备份机制,那就是mysql的主从备份机制,建立主库,即将目前的库备份一份完整的出来,此备份库作为从库,而原来的库作为主库,备份完毕后主库和从库的数据完全一致。 利用mysql自带的Replication方案,将主库中需要写的表同步到从库中,这个方案其实有一定的局限性,就是同步可能会有延迟,导致读的时候数据不一致。目前教成熟的方案是使用keepalive结合Replication方案来实现主从备份。

读写分离

要让数据库资源能更好的为应用层提供服务,就需要使用读写分离,所有的写操作,要完全落到主库上,而读操作完全落到从库上,在访问高峰期,2个读库会有效的分散整个后端数据库的读压力,上面提到过,整个系统是读大于写的,所以,这里我们建立的从库,应该是2个,而不是一个,在上面的Replication方案中,主库同步数据应该是同步到2个从库上,读写分离方案可以有效的减少对单机数据库连接资源的使用。 在原有的应用中,是不支持读写分离的,所以需要进行重构这部分机制。具体就是通过应用层分离读写连接的数据库,具体就是构建一层数据访问层(DAL),在该层中可以根据sql自动路由到相应的库,其实阿里有开源的这部分框架TDDL,但是考虑到其源代码没有完全开放,而目前公司开发DAL组件也比较耗时,所以建议就是通过在业务层通过spring来动态配置数据源。

剪裁表

目前的主库中,包括了很多和主业务无关的表,比如福利表,专题表等,这些表需要独立出来放在一个独立的库中,在它上面涉及到的应用也需要独立出来,不能放在原有应用中,如果涉及到和原有主库中的表之间的关联查询,可以通过应用层获取数据,再存入这些表中,因为这些福利或者专题是有时效性,一段时间后即会下线。

mysql中大表的处理

就目前用户量来说,mysql的物理连接数设置为1000,还是够用的,但是为什么出现应用访问缓慢的现象?问题是出在应用的sql上,应用的数据库连接池已经换成阿里的druid了,这个连接池在性能上已经被大量的应用证明完全没有问题,即使我们设置最大连接数目达到500甚至600,也还是有被占满的时候,那是因为某个sql一旦对数据量特别大的表进行查询时,就会导致该连接被长期占用不释放,后面的请求继续消耗连接池中的连接资源,一旦这种耗时的sql在访问高峰期时被频繁使用,就会导致连接池中连接资源被消耗完,而此时数据库的物理连接仍然够用,极端情况是这种耗时的sql查询导致mysql数据库服务器的cpu资源被耗尽,此时任何连接过来都无法得到响应,返回超时,解决办法只能重启数据库。

所以针对大表,我建议解决办法有2种:

Redis集群

使用redis是因为其具有数据持久化功能,且数据结构丰富,适用很多业务场景,部署结构为一主一从,使用独立的2台服务器,要求内存大,读写都在主服务器上,从服务器只做备份功能,并且要求一旦主服务器挂掉,可以立即自动切换到从服务器上,通过配置redis.conf文件即可实现。redis并没有在我们的应用中充分利用起来,后期可以考虑一些应用场景,比如排名,好友动态等,这里是以前写的一篇关于Redis使用场景的文章。当后期其数据量达到一定量,规模扩大时,会存在主从复制延迟,当机后不能迅速恢复数据等问题,就需要建立一套完备的redis集群的机制,保证高可用性。

队列

在后台记录数据同步(写操作)时,有许多相关业务逻辑要执行,比如一些专题活动等,会即时调用这些记录数据,这部分代码混杂在主逻辑中,为了不影响主逻辑,需要使用异步队列来存储数据同步消息,即记录数据同步成功后,在队列中放入这一消息,所有订阅了此消息的消费者获取该消息后,调用自己的业务逻辑,这不仅将主业务逻辑和非主业务逻辑进行了解耦,而且对性能的提高也有很大帮助。 再比如对于记录数据上传时,需要进行有效性检查和清理,这部分逻辑也是异步执行。

回到同步记录数据的主逻辑,当客户端装机量达到一定规模,同步数据的操作,也就是写操作,就会变得非常消耗服务端资源,所有客户端都在线等待同步结果,界面会停止响应,或者现象为同步数据超时,或者因为同步过程中网络中断,导致同步结果和反馈结果不一致,此场景也可以通过消息队列来解决,我们把这个步骤拆分成2步,第一步客户端发起数据同步请求,服务端将这些请求放入队列并立即返回,第二步消费者进程异步读取队列,然后执行写库操作,写成功或者失败,都将通过推送系统把消息推送到客户端;消息队列的性能远比数据库高,所以对于并发量大的同步数据操作完全可以胜任,而且异步消息队列是可以集群部署的,性能可以线性扩展。

结合目前我们应用情况,推荐使用一个轻量级的消息队列,那就是Redis,使用List数据结构可以轻松实现一个队列,FIFO,有个前提是存入数据时大小应该尽量小于10k,否则会影响入队列性能。业务组件可以使用一个worker进程轮询消息队列进行业务逻辑处理,此功能组件需要单独开发。

缓存

现在互联网应用性能的提高,首要手段就是缓存,我们应用中目前的缓存只是一个本地的HashMap,其性能和可用性在高并发场景下,没有测试过,更谈不上扩展性,所以需要使用一个成熟的缓存系统,毋庸置疑,肯定是memcached;对于一个读大于写的应用,使用缓存才是保证性能的王道,memcached是一个存储简单key和value值的缓存系统,其本身不并不支持分布式,也就是不能做到集群内单点之间数据的自动复制同步,所以需要从客户端来实现这些功能,好在这部分工作已经有现成的实现,而且也很成熟,所以只需要拿来根据我们目前应用的情况做适当修改即可。

缓存系统将会是应用中使用最多的一部分,所以其应该是独立部署的集群,根据需要可以动态扩容,并且可以防止单点故障,可以使用阿里文初写的memchaced客户端系统,其实这部分工作我在以前已经实现过,拿来即用,具体可以参考以前写的一篇文章。

应用分离

为保证系统能够承受越来越多的用户的访问,除了前面说明的存储和缓存都要进行集群部署外,应用也需要进行分离,历史原因,现在后台和前台网站以及API都在一个应用中,首要的任务就是需要将API这部分独立出来作为一个应用(具体看下面的API一节),等所有APP都切到新的API应用后,再将老应用中的api这部分逻辑去掉,网站和后台暂时不动,继续服务。这样API应用和原来的应用通过数据库进行关联。

独立的API应用需要支持集群部署,目前可以使用4台机器部署API应用(这个看具体访问情况),前端使用Nginx进行负载均衡,保证APP接口调用性能,因为API应用是无状态的(用户鉴权和session信息都调用原来的老系统),所以不考虑session问题,随着用户量增长,可以动态增加机器即可。同时Nginx本身也需要考虑单点问题,需要同时部署2台。

原来老系统,继续使用,但是其性能肯定不能支持后期的大量用户,所以也需要进行集群部署,但是其是有状态的,这里我们可以使用tomcat的session复制功能,部署4台机器,session同步复制,这种规模的集群,session复制性能问题可以忽略。 而系统中的后台管理,以及特定的专题活动等,可以独立出来作为一个应用进行部署。其次,老系统中的部分功能可以考虑独立出来作为一个公共服务,比如用户登陆鉴权,session管理,好友关系等,这个看人力情况。

API

移动服务端的API设计,行业内并没有统一标准,但是根据目前主流互联网应用api的设计思路,Restful风格,http协议,json格式数据交互,这些都是必选的,而我们原有系统中的API大体上也是这种规则。目前系统所有的API都是servlet,安全验证不够严谨,API的Restful风格不符合,急需要进行重构,在技术选型上,如果保留现有的servlet机制,缺点明显,如果选择springMVC对Restful的支持,也是可以,但是既然是后端的独立API服务,springMVC显得过重,我们只需要json数据输出即可,这样客户端既可以是APP,也可以是web,可以复用,为了保持应用的简洁性,推荐使用基于java的开源Restful框架Jersey,其是对JAX-RS的一个扩展实现,具体可以参考官网介绍;为了兼容老版本的APP,需要重构原来的API实现方式,接口和返回参数保持不变即可。 在设计API应用的时候,需要考虑对接口调用的统计和阀值设定以及报警功能的设计。

APP应用主要是通过API来和后端交互,API的性能和可用性至关重要,重构以后独立的API应用保持无状态,直接连接数据库,支持集群部署;接口中需要安全验证和获取状态的流程直接访问原来老系统的登陆流程即可。 独立后的API应用在将来的角色应该是扩展为整个edooon.com的核心业务应用,所有外围系统都将根据一定的权限通过其来获取有价值的业务数据,而不是直接连接数据库获取数据。

安全验证,使用OAuth2.0协议。

web应用的可扩展性

历史原有,网站应用的代码高度耦合,每个功能模块没有很好的集中管理,有些散落在各个其他模块中,当需要修改或者新增一项功能时导致要修改很多地方,前端和后端业务逻辑耦合严重,web技术使用不统一,不利于开发人员的开发效率,对扩展带来极大不便,前期Bug问题没有完全解决。所以个人觉得,部分重要功能模块需要进行检查和重构,比如用户登陆安全验证模块,session管理等功能,因为这些都涉及到后期的集群部署,其他模块可以根据实际的人力情况在决定。 另外一个重点就是对于数据库读写分离和大表分片的支持,应用如果要在这块做的灵活的话,还是需要花时间去研发,如果只从业务层动态设置数据源,也要对业务规则理解透彻的基础上进行设计。

统一技术栈,固定开发流程,注意积累公共组件和文档,总的目标就是提高开发效率,节约成本。

系统监控

硬件资源监控可以参考使用 Grafana+collectd+influxDB,应用的监控就比较广泛了,看具体需要再进行设计,这个不擅长,不多谈。

系统架构图

以上泛泛之谈,很多细节没有涉及,具体研发过程中,许多坑需要去填,最后的架构图是这样的。