老板:把系统从单体架构升级到集群架构!
本文针对工作中实际经验,整理了把一个单体架构的系统升级成集群架构需要做的准备工作,以及为集群架构的升级做指导方针。
本文首先分析了单体架构存在的问题,然后介绍了集群架构(好处、注意的问题、架构图),接着分析了目前系统的主要功能以及集群后需要做哪些调整,然后对集群架构涉及的技术做横向对比,最后确定技术选型。从这几个方面介绍了从单体架构到集群架构的改造过程,希望对你有帮助。
背景
单机存在单点故障的隐患
Jvm内存频繁在某时段报警
单体架构存在的问题
项目目前的架构是单体垂直架构,只有一个服务节点,存在一些问题,以下是对存在问题的分析:
1、服务可用性差
单机部署只有一个节点提供服务,如果服务进程挂掉或服务器宕机导致服务不可用,将会影响用户的正常使用,如果服务重新上线的时间很长,将会严重公司业务开展,对于下单等常规业务带来的损失无法估量。这个问题就是我们常说的单点故障。
2、服务性能存在瓶颈
单机所能承载的读写压力、请求数都是有限的,当系统业务增长到一定程度的时候,单机的硬件资源将无法满足你的业务需求,增加服务器配置所带来的的性能提升与昂贵的成本不成正比,性价比不高。
3、不可伸缩性
单体架构的弊端之一就是伸缩性不强。随着需求和负荷的增长,单体架构的性能满足不了现有需求时,增加服务器资源的手段收效甚微,服务的性能可扩展性低是单体架构的致命缺点。
4、代码量庞大,系统臃肿,牵一发动全身
随着业务功能增加,系统代码量不断增加,系统变得庞大而臃肿,开发人员修改一处代码往往担心会牵涉到其他功能。
据统计,项目后端代码行数高达12万行(Java),前端代码行数高达34万行(html+css+js),日常维护、版本迭代、发版上线的成本也相应增加。每次项目上线前都要对系统进行回归测试,上线时要把项目进行全量打包发布,上线后监测系统日志是否正常,担心会不会影响其他功能模块。
可规划系统拆分。
集群架构
3.1为什么要集群部署?
1、单点故障单机部署很容易出现服务挂了之后,没有备用节点,从而影响用户使用。单机对外提供服务,风险很大,服务器任何故障都可能引起整个服务的不可用。
2、性能瓶颈单机遇到资源瓶颈时,要想支持更大的用户量,性能有较大的提升,一般是优化业务和增加服务器配置。然而这么做只能是杯水车薪,成本巨大并且效果非常有限。而集群部署通过部署多个服务节点水平扩展服务的性能,成倍的增加服务器性能,而且支持动态扩展。
3.2集群的好处
1、高可用性。提高服务的可用性,只要有一个服务可用就能对外提供服务。高可用性是指,在不需要操作者干预的情况下,防止系统发生故障或从故障中自动恢复的能力。通过把故障服务器上的应用程序转移到备份服务器上运行,集群系统能够把正常运行时间提高到大于99.9%,大大减少服务器和应用程序的停机时间。
2、吞吐量。增加吞吐量,并发量,支持更大的用户量。
3、易扩展。也叫可伸缩性,可伸缩性体现在节点数量的调整,在预知流量增大的情况下,可以提前增加节点。
3.3集群部署需要注意的问题
1、负载均衡问题。
请求应该由哪个节点处理?--应用集群需要有一个组件来管理请求的分发。--常见的如Nginx
2、session失效问题。
登录请求被节点1处理后session存储在节点1,后续的请求分发到节点2则需要重新登录。
--集群环境下session需要同步。方案:Tomcat自带的session复制、springsession+redis实现分布式session
3、定时任务执行问题。
定时任务分配给哪台机器执行?
确保不会重复执行,最简单的办法就是定时任务拆分出来,只部署一个节点。方案:分布式job框架、分布式锁实现job的分配。
4、缓存一致性问题。
原来缓存在本地的数据,需要保证数据的一致性,实现共享,只保存一份。(比如两个节点各缓存了一份不同版本的数据,就会出现同一个页面刷新,交替展示不一样的数据直到缓存失效)。--Redis
3.4集群架构
站点部署多个节点,集群前面架设Nginx做负载均衡,Tomcat之间通过Redis实现session共享。
前后端分离
系统功能点及集群部署后需作何调整
4.1功能点
主要分为这几个大类:用户登录登出、权限控制、业务功能、定时任务。
4.2用户登录登出的处理
登录涉及到用户信息的存储,目前单机部署,session交给web容器Tomcat管理,存储在内存中。集群环境多个Tomcat,当同一个用户的多次请求被分发到不同的服务器上,假设第一次请求访问的A服务器,创建了一个session,但是第二次请求访问到了B服务器,这时就会出现取不到session的情况,认为用户没有登录,跳到登录页再次让用户登录,如此反复。于是,集群环境中,session共享就成了一个必须要解决的问题。
解决方案:
1.不要有session:大家可能觉得我说了句废话,但是确实在某些场景下,是可以没有session的,在很多接口类系统当中,都提倡;也就是每一次的接口访问,都不依赖于session、不依赖于前一次的接口访问;
2.存入cookie中:将session存储到cookie中,但是缺点也很明显,例如:每次请求都得带着session;session数据存储在客户端本地,是有风险的;
3.session同步:多个服务器之间同步session,这样可以保证每个服务器上都有全部的session信息。不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
4.粘滞会话:使用Nginx(或其他负载均衡软硬件)中的ip绑定策略,同一个ip只能在指定的同一个机器访问,但是这样做风险也比较大,而且也失去了负载均衡的意义;
5.session分布式存储:把session放到Redis中存储,虽然架构上变得复杂,并且需要多访问一次Redis,但是这种方案带来的好处也是很大的:实现session共享,可以水平扩展(增加Redis服务器);服务器重启session不丢失(不过也要注意session在Redis中的刷新/失效机制);不仅可以跨服务器session共享,甚至可以跨平台(例如网页端和APP端)。
4.3权限控制的处理
系统中权限控制基于数据库权限表实现,无需调整。
4.4业务功能的处理
梳理出哪些业务功能会受到影响?影响包括变量的使用。
通过梳理系统中代码,发现目前系统中对数据的缓存其实是缓存在本地jvm内存中的,自己实现了一套缓存过期机制,但是这种方式并未对缓存占据内存大小进行控制,这样缓存使用的内存有无限增长的可能,甚至导致内存泄漏。
这些变量用做本地缓存,存在jvm中,若集群部署,则在各自的jvm进程中都会存一份,不能共享,可能存在如下问题:第一次请求,由服务器A处理,其查询后存了一份数据V1,第二次请求由服务器B处理,刚好数据发生变化,查询后存了一份数据V2,后续请求如果均匀的分发到AB服务器,那么用户看到的数据将一会儿是V1一会儿是V2(在缓存未过期时),这样就造成了数据不一致。
4.5定时任务的处理
集群部署的初衷是解决JVM内存频繁告警,内存告警的原因可能是定时任务比较耗内存,本次讨论不展开jvm内存频繁告警的问题。另外,系统一旦做集群部署就需要考虑集群环境下的定时任务不能重复执行。
1.代码拆分
JOB任务耗时耗内存占用服务器资源,对用户操作有一定的影响,将定时任务从项目中拆分出来,单独做个站点跑定时任务,采用单机/集群部署。
拆分的好处:由定时任务耗时耗内存引起的内存告警,可能会影响正常业务进行,拆分的好处之一就是业务隔离。若不拆分,集群部署只能将同一时间的定时任务分散到不同节点执行,分摊内存压力。但若要彻底解决定时任务引起的内存报警,光靠集群部署是不能彻底解决的,因为有可能某一时刻的定时任务都由同一个节点执行,这样又回到单机的状态,还是会发生内存告警问题。若要彻底解决,首先是拆分定时任务独立运行,观察内存情况后再做后续优化。
2.任务防重复执行
如果将定时任务代码拆分且集群部署或不拆分(原系统集群部署),那么定时执行的任务,需要控制同一个任务触发时只有一个节点执行,可用分布式锁实现、或quartz框架自身支持。
分布式锁方式:执行前先尝试获取锁,获取到则执行,否则不执行。缺点:需引入分布式锁,修改现有业务代码。
quartz自带集群功能的支持:需修改配置文件,同时数据库导入quartz官方的11张表。优点:自带功能,对外透明,不需要改代码,对现有业务影响较小。
集群部署涉及的技术方案对比
5.1负载均衡方案
在服务器集群中,需要有一台服务器充当调度者的角色,用户的所有请求都会首先由它接收,调度者再根据每台服务器的负载情况将请求分配给某一台后端服务器去处理。
那么在这个过程中,调度者如何合理分配任务,保证所有后端服务器都将性能充分发挥,从而保持服务器集群的整体性能最优,这就是负载均衡问题。
负载均衡种类:DNS、硬件、软件
参考:
负载均衡种类及优缺点
高并发解决方案之一——负载均衡
5.2session共享
多个服务器之间同步session,这样可以保证每个服务器上都有全部的session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
实现方式参考:Tomcat集群和Session复制说明
springsession+redis实现分布式session
简介:将Session存入分布式缓存集群中的某台机器上,当用户访问不同节点时先从缓存中拿Session信息使用场景:集群中机器数多、网络环境复杂优点:可靠性好缺点:实现复杂、稳定性依赖于缓存的稳定性、Session信息放入缓存时要有合理的策略写入
把session放到Redis中存储,虽然架构上变得复杂,并且需要多访问一次Redis,但是这种方案带来的好处也是很大的:实现session共享,可以水平扩展(增加Redis服务器),应用服务器重启session不丢失(不过也要注意session在Redis中的刷新/失效机制),不仅可以跨服务器session共享,甚至可以跨平台(例如网页端和APP端)。
5.3定时任务防重复执行
指定某一个节点执行通过特定IP限制,在定时任务的代码上加一段逻辑:仅某个ip的服务器能运行该定时任务。
优点:解决方法容易理解,部署简单,不需要多套代码。
缺点:
需要修改现有代码;
存在单点问题,只能规定一台服务器运行,发生故障时需要人工介入。
通过锁控制锁的性质需满足悲观、独占、非自旋、分布式。
在定时任务业务逻辑执行前先尝试获取锁,谁获取到谁执行,拿不到锁就直接放弃,或者进行其他的处理逻辑。
优点:解决单点问题
缺点:无法故障转移
利用quartz集群分布式(并发)部署解决方案
quartz自身提供了集群分布式(并发)部署的一套解决方案,主要解决思路是通过数据库锁的方式实现。
实现原理和解决方案和Quartz-cluster最佳实践特性:
1.持久化任务:当应用程序停止运行时,所有调度信息不被丢失,当你重新启动时,调度信息还存在,这就是持久化任务(保存到数据库表中)。
2.集群和分布式处理:当在集群环境下,当有配置Quartz的多个客户端时(节点),采用Quartz的集群和分布式处理时,我们要了解几点好处
一个节点无法完成的任务,会被集群中拥有相同的任务的节点取代执行。
2)Quartz调度是通过触发器的类别来识别不同的任务,在不同的节点定义相同的触发器的类别,这样在集群下能稳定的运行,一个节点无法完成的任务,会被集群中拥有相同的任务的节点取代执行。
3)分布式体现在当相同的任务定时在一个时间点,在那个时间点,不会被两个节点同时执行。
有两点需要注意:
1)集群配置文件quartz.properties部署的时候必须要一致
2)集群建立起来之后,如果运行过程中需要修改quartz调度器的策略,例如:原来每5天执行一次任务,现在要改成每半个月执行一次,这个时候要修改所有的配置文件,并且要重新执行数据库脚本,或者手动修改数据库中存的corn表达式(容易改错),或找到那条记录删除(多表主外键关联)。
实现步骤:
1)修改quartz.properties配置文件
配置文件详解和quartz配置详解
org.quartz.jobStore.misfireThreshold=org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTXorg.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegateorg.quartz.jobStore.tablePrefix=QRTZ_org.quartz.jobStore.dataSource=myDSorg.quartz.jobStore.isClustered=trueorg.quartz.jobStore.acquireTriggersWithinLock=trueorg.quartz.jobStore.clusterCheckinInterval=org.quartz.jobStore.maxMisfiresToHandleAtATime=1
2)导入表结构,
quartz-2.3.0.jar!\org\quartz\impl\jdbcjobstore\tablesmysqlinnodb.sql
DROPTABLEIFEXISTSQRTZ_FIRED_TRIGGERS;DROPTABLEIFEXISTSQRTZ_PAUSED_TRIGGER_GRPS;DROPTABLEIFEXISTSQRTZ_SCHEDULER_STATE;DROPTABLEIFEXISTSQRTZ_LOCKS;DROPTABLEIFEXISTSQRTZ_SIMPLE_TRIGGERS;DROPTABLEIFEXISTSQRTZ_SIMPROP_TRIGGERS;DROPTABLEIFEXISTSQRTZ_CRON_TRIGGERS;DROPTABLEIFEXISTSQRTZ_BLOB_TRIGGERS;DROPTABLEIFEXISTSQRTZ_TRIGGERS;DROPTABLEIFEXISTSQRTZ_JOB_DETAILS;DROPTABLEIFEXISTSQRTZ_CALENDARS;......
图片
优点:
1)框架自带功能,实现方式对外透明
2)不需要改代码,对现有业务影响较小。
缺点:
1)后期如果修改定时任务的执行时间,数据库不会刷新,需手动改库。
2)后期想停掉定时任务需要从数据库中删除或修改下次执行时间为无穷大(只在代码中注掉定时任务的注册不起作用)
5.4缓存(可选)
第4.4章节中,使用jvm内存缓存了一些基础数据,当集群部署后,这些数据会在每个jvm都缓存一份,无法做到数据唯一性。
RedisRedis是完全开源的,遵守BSD协议,是一个高性能的key-value数据库。
Redis与其他key-value缓存产品有以下三个特点:
Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
Redis支持数据的备份,即master-slave模式的数据备份。
Redis优势
性能极高–Redis能读的速度是次/s,写的速度是次/s。
丰富的数据类型–Redis支持二进制案例的Strings,Lists,Hashes,Sets及OrderedSets数据类型操作。
原子–Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
丰富的特性–Redis还支持publish/subscribe,通知,key过期等等特性。
Redis与其他key-value存储有什么不同?
Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
MemcacheMemcache特点Memcache是一套开放源代码的分布式高速缓存系统。Memcache通过在内存里维护一个统一的巨大的hash表,它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。简单的说就是将数据调用到内存中,然后从内存中读取,从而大大提高读取速度。1、协议简单:Memcached的服务器客户端通信使用简单的基于文本的协议。2、基于libevent的事件处理:libevent是个程序库,他将Linux的epoll、BSD类操作系统的kqueue等时间处理功能封装成统一的接口,能在Linux、BSD、Solaris等操作系统上发挥其高性能。3、内置内存存储方式:Memcached的数据都存储在内置的内存存储空间中,因此重启Memcached,重启操作系统会导致全部数据消失。另外,内容容量达到指定的值之后Memcached会自动删除不适用的缓存。4、两阶段哈希结构:Memcached就像一个巨大的、存储了很多对的哈希表,客户端可以把数据存储在多台memcached上。查询数据时,客户端首先计算出阶段一哈希,选中一个节点;客户端将请求发送给选中的节点,然后memcached节点通过计算出阶段二哈希,查找真正的数据(item)并返回给客户端。从实现的角度看,Memcached是一个非阻塞的、基于事件的服务器程序。5、不互通信的分布式:服务器端并没有分布式功能,不会互相通信以共享信息。分布式是通过客户端实现。
Redis和Memcache区别,优缺点对比图片
1、Redis和Memcache都是将数据存放在内存中,都是内存数据库。不过memcache还可用于缓存其他东西,例如图片、视频等等。2、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构的存储。3、分布式–设定memcache集群,利用magent做一主多从;redis可以做一主多从。都可以一主一从4、存储数据安全–memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化)5、灾难恢复–memcache挂掉后,数据不可恢复;redis数据丢失后可以通过aof恢复6、Redis支持数据的备份,即master-slave模式的数据备份。
redis和memecache的不同在于:1、存储方式:memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小redis有部份存在硬盘上,这样能保证数据的持久性,支持数据的持久化(有快照和AOF日志两种持久化方式,在实际应用的时候,要特别注意配置文件快照参数,要不就很有可能服务器频繁满载做dump)。2、数据支持类型:redis在数据支持上要比memecache多的多。3、使用底层模型不同:新版本的redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。4、运行环境不同:redis目前官方只支持LINUX上运行,从而省去了对于其它系统的支持,这样的话可以更好的把精力用于本系统环境上的优化,虽然后来微软有一个小组为其写了补丁,但是没有放到主干上。
总结一下,有持久化需求或者对数据结构和处理有高级要求的应用,选择redis,其他简单的key/value存储,选择memcache。
5.5分布式锁(可选)
用于控制定时任务由哪个节点执行,若4.5节中采用quartz自带功能解决,则不需引入分布式锁。
分布式锁三种实现方式:
1、基于数据库实现分布式锁;
2、基于缓存(Redis等)实现分布式锁;
3、基于Zookeeper实现分布式锁。
数据库实现分布式锁基于数据库实现的分布式锁,是最容易理解的。但是,因为数据库需要落到硬盘上,频繁读取数据库会导致IO开销大,因此这种分布式锁适用于并发量低,对性能要求低的场景。
基于数据库实现分布式锁比较简单,绝招在于创建一张锁表,为申请者在锁表里建立一条记录,记录建立成功则获得锁,消除记录则释放锁。
该方法依赖于数据库,优点就是容易理解,实现简单,但缺点也很明显:
1)单点故障问题。一旦数据库不可用,会导致整个系统崩溃。
2)死锁问题。数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。倘若已获得共享资源访问权限的进程突然挂掉、或者解锁操作失败,使得锁记录一直存在数据库中,无法被删除,而其他进程也无法获得锁,从而产生死锁现象。
Redis(缓存)实现分布式锁基于缓存实现分布式锁的方式,也就是说把数据存放在计算机内存中,不需要写入磁盘,减少了IO读写。
使用Redis实现分布式锁,通常可以使用setnx(key,value)函数来实现分布式锁,当进程通过setnx函数返回1时,表示已经获得锁。排在后面的进程只能等待前面的进程主动释放锁,或者等到时间超时才能获得锁。
相对于基于数据库实现分布式锁的方案来说,基于缓存实现的分布式锁的优势表现在以下几个方面:
1)性能更好。数据被存放在内存,而不是磁盘,避免了频繁的IO操作。
2)很多缓存可以跨集群部署,避免了单点故障问题。
3)使用方便。很多缓存服务都提供了可以用来实现分布式锁的方法,比如Redis的setnx和delete方法等。
4)可以直接设置超时时间(例如expirekeytimeout)来控制锁的释放,因为这些缓存服务一般支持自动删除过期数据。
缺点:
1)锁删除失败、过期时间不好控制
ZK分布式锁实现ZooKeeper基于树形数据存储结构实现分布式锁,来解决多个进程同时访问同一临界资源时,数据的一致性问题。ZooKeeper基于临时顺序节点实现了分布锁。
临时节点(EPHEMERAL):当客户端与Zookeeper连接时临时创建的节点。与持久节点不同,当客户端与ZooKeeper断开连接后,该进程创建的临时节点就会被删除。
临时顺序节点(EPHEMERAL_SEQUENTIAL):就是按时间顺序编号的临时节点。
可以解决前两种方法提到的各种问题,比如单点故障、不可重入、死锁等问题。但该方法实现较复杂,且需要频繁地添加和删除节点,所以性能不如基于缓存实现的分布式锁。
缺点:性能不如Redis分布式锁实现。
图片
这里的实现复杂性,是针对同样的分布式锁的实现复杂性,与之前提到的基于数据库的实现非常简易不一样。
基于数据库实现的分布式锁存在单点故障和死锁问题,仅仅利用数据库技术去解决单点故障和死锁问题,是非常复杂的。而ZooKeeper已定义相关的功能组件,因此可以很轻易地解决设计分布式锁时遇到的各种问题。所以说,要实现一个完整的、无任何缺陷的分布式锁,ZooKeeper是一个最简单的选择。
总结来说,ZooKeeper分布式锁的可靠性最高,有封装好的框架,很容易实现分布式锁的功能,并且几乎解决了数据库锁和缓存式锁的不足,因此是实现分布式锁的首选方法。
总结
技术选择
负载均衡:使用哪种负载均衡策略不需要我们关心,由运维支持,告知需负载均衡即可。(若公司需自己搞,推荐Nginx)
session共享:springsession+redis
定时任务防重复执行:quartz-cluster
缓存:Redis(公司大面积使用)
分布式锁:Redis(公司提供了Redis分布式锁实现)
说的再好,不如行动。不怕慢,就怕站。