在文章《系统架构-性能篇章2(系统拆分1)》有提及到过关于系统在什么情况下会拆分,拆分的目之类的问题,本文会阐述一些关于拆分过程中遇到的各种各样的常见问题进行分析,和上一个文章中提及到的一样,讲解的目录如下:
1、负载均衡设备的问题。
2、不同系统之间的通信问题。
3、数据写入和查找的问题。
4、跨数据库事务问题。
5、跨数据库序列问题。
6、不同应用的本地缓存问题。
7、系统之间的直接依赖和间接依赖问题。
8、独立模块面临的单点问题。
9、各类批量分组、切换、扩展的问题。
10、统一监控和恢复问题。
进入正题:
一、负载均衡设备的问题:
负载均衡设备就是当系统被拆分为多个节点进行发布后,前端应用系统访问的过程中,还是应当有一个被一个统一认识的整体,也就是有一个统一的入口,而不能让客户端来记住每一个节点的地址来输入;如输入:www.google.com,谷歌的后台就有非常多的服务器来为我们服务,但是我们访问的是一个目录地址。
付:负载均衡设备目前是狭义的一个公司内部的入口或者一套系统的入口,不过很多广义的负载均衡还包含链路的负载均衡(网络层的解析)、同一台机器上进程之间的负载均衡,不过都可以理解为节点之间的负载均衡。
我们在经过网络层解析后,访问到目标地址后,然后再开始找对应的应用系统平台,然后找到可执行对应应用的主机信息,开始进行负载均衡的操作动作;什么是应用自己的平台呢?那就是URL上面的子目录,或主目录本身也有可能。
负载均衡也有硬件负载和软件负载,硬件负载均衡器一般比较贵,受限制于硬件本身,在一定程度上,它较软件负载均衡更加有效,但是在要求更好的负载下并且要求低成本的情况,软件负载又具有了更好的优势,也就是高集成度的东西始终处于中间的那个位置,但是两者都是可以并存的。
目前我们讨论的主要是软件负载均衡,在Weblogic中有一个自带的集群配置策略,通过Proxy进行代理,Admin节点管理多个Managed节点,不过这个负载均衡比较挫,数量稍微多一点就会死掉,这个节点也做得不好,在系统的负载一般的情况下,选择这种方式会比较简单一些,配置的方法也是weblogic提供的傻瓜式下一步就可以了;不过注意的一点,不要在proxy节点和admin节点去发布什么应用啥的,这种节点要是再配置些应用就更费了,qps就更低了。
其次就是我们注明的apache,其广泛应用于很多领域,也是目前全球使用范围最广的负载均衡,而且提供了很多web模块直接编译一些语言(如php模块),一般在这种负载均衡器下,qps可以达到3000以上甚至于更多,不过有些时候要看反馈路径是否经过apache本身以及每次请求数据的大小,在大部分的应用下,使用它已经可以注意支撑起来;很多时候这种代理也称反向代理服务器,apache里头也拥有非常丰富的第三方模块,这方面它甚至于超越了nginx,在安全性方面也较好,技术资料较为健全。
世界级大型反向代理服务器nginx(Engine-X),可支持Http反向代理、负载均衡、FastCGI支持、Rewrite、缓存等等,邮件相关的各种协议,其QPS可以支持3万以上,经过改进后的nginx更加强大;这是一位俄罗斯的著名程序员(Igor Sysoev,自称为一个高级的系统管理员)编写的,目前全球排位第四位,不过它的性能是最强大的,也是高并发的互联网公司借鉴的标准,并且它轻量、开源、稳定、高性能、低CPU开销、低内存开销、缓存压力甚至于抵挡常见的攻击;nginx发展为2001年开始,04年第一个public release版本出来,至今已经有10年历史,其接受第三方模块较少,虽然在第三方的支持上较少,但是其代码非常的干净利落,非常高的代码质量,并且更新很快;另外nginx在热部署上远远超越于apache,不过要在上面做扩展比较困难,技术资料相对较少一点(关于nginx的一些细节,后面有文章单独说明,因为他的确太强大了)。
其次全球还有很多的负载均衡设备,如:IIS(微软支持的)、google等,各自有自己的应用场景需求。
负载均衡主要需要做几件事情:
1、需要根据信息找到目标主机进行负载。
2、多个目标节点,需要均衡的负载。
3、记住统一个session前一次访问的主机,下次还会访问这个机器(看模式,有些是无状态session)
4、某个节点失败后,需要进行相应的切换操作。
5、在指定的配置下,可能会负责反馈结果,不过在高并发应用下,这里可能会涉及到多层负载均衡的情况,最顶层的负载均衡设备一般不干这事。
6、必要时需要在切换时发生session的复制(在有状态session下,需要将session的内容复制到另外的机器上,要求session中保存的内容是可以被序列化的)
7、在增加节点时,高性能的要求是在这种情况下不能出现负载偏向,这种依赖于负载均衡设备的算法,一般我们采用一致性hash可以达到这个目的,不过最初的一致性hash算法还存在很多问题(Hash的KEY通常是一些URL或者参数的内容),并不能真正解决这些问题,后来出现了很多变种的一致性hash算法的确可以很大程度上降低负载偏向的问题,其次:一致性hash并不适合于解决数据层的问题,也就是数据层具有持久性的,用一致性hash并不能解决其动态扩展的问题,虽然一致性hash为了解决这个问题做出了很多算法的变动,不过仍然存在很多版本问题。
二、不同系统之间的通信问题
上一篇文章中提及到了,系统能做到一起,我们做到一起最简单,因为模块之间调用就直接用对象直接就可以引用到,拆分成多个系统就不一样了,系统之间的调用就成了进程和进程之间的通信了;
有关通信就说到早期的socket,这个socket虽然是最古老的技术,不过它也算是目前所有网络技术衍生的基石,网络的交互的不断优化过程就是socket特征不断变化的过程。
说到socket就是编写程序比较麻烦,双方调用和接受都需要编写单独的程序去通信,要求传输的内容都可以被序列化,写的一方有点类似于写文件,读的一方有点类似读文件,不过它也是使用IO本身的一种方法去通信;很多程序员初手在调用完了也会搞忘把socket关掉。
面对java想要把底层封装的,而且尽量减少程序员的错误,所以RMI诞生了,远程方法调用诞生了,RMI是jvm自己封装的一种远程方法调用的协议,中间通过对象序列化来完成,调用方需要有远程的接口,由于RMI本身调用过程中配置比较麻烦,但是又有了这种技术,于是EJB诞生了,EJB在RMI的基础上衍生出标准化的分布式编程模型,不过它都是基于RMI来编写的,主要目的是将业务和VIEW分解开,不过它是物理上将其分开了,在部署和调试程序上相对比较困难,最头痛的就是RMI里头在调用完后会自己做一个System.gc()方法,将会导致Full GC,于此同时衍生出不同厂商不同系统的通信方法也是沿用EJB,是的各个工程里头都有和自己不想管的系统的代码和jar包,Perm区域增加的开销暂时忽略掉,不过系统的移植性和产品化就出现了很多困难(如果在另一个地方要发布同样的系统,但是这个系统中有很多外部调用需要,要么最后工程鱼龙混杂,要么要把这些东西分解出来是很困难的事情,甚至于会报一些诡异的错误)。
后来基于RPC的webservice出来了,它跨语言,因为它传递过程中你可以认为它不是传递对象(后来EJB也把它融进去了),不过我们很多时候还是喜欢用spring来集成它,高版本的webservice可以通过注解完成大部分的工作,不过tomcat发布这个玩意一直不是很好用;另外Spring里头也提供了对于RMI本身的封装的支持,以及spring hessian也是非常不错的交互框架,而且spring是轻量级的,越来越多的人开始选择spring了,因为EJB很多功能它都有,没有的大部分东西也不想要,用了EJB还有各种各样的问题。
最最简单的交互方法,HttpClient,这种是apache组织提供的一种非常轻量级的交互方法,其实也是基于socket写的,因为浏览器本身和服务器交互也是一种socket,只是建立了协议包头和一些短连接机制;所以HttpClient就是使用socket模拟的一个浏览器客户端发起的一次提交操作,可以发起Get和POST的请求,也可以控制参数的传递的字符集等等,传递和结果信息由双方决定;服务方只需要通过正常的response输出数据即可,只是这个时候不是输出一个页面,而是输出一些客户端可以被解释的数据,如json结构或xml结构的字符串信息。
总之,系统一旦拆分,通信是避免不了,从这里也可以看出,并不是系统想怎么拆分就怎么拆分的,要尽量减少相互之间的通信,就需要了解系统,做到系统的低耦合、高内聚,减少外部依赖,不然系统大部分时间就在通信了,没有做其他的事情,不过也不排除有这种情况,那就是有些系统是专门用来做通信的,这种系统可以例外,它处理的核心就是通信处理,做中间转换。其余的业务系统尽量做到减少通信的模块数量。
三、数据写入和查找的问题
关于数据级别被拆分后,尤其是再数据库级别被拆分后,就会面临数据写入和读取的问题,那么在写入的数据就需要能够读取出来,那么这里就需要相同的规则进行读写操作,此时数据库拆分后,我们更多的是将数据库作为一种类似NoSQL的目标机器,或者说是一种存储引擎的分布式部署方法,要实现读写的一致性就要保证一样的规则(当然你说你可以用不一样的规则,除非你中间用了十分复杂的数学算法来做,的确有这种可能,不过考虑到业务数据的准确性,我也一直不敢尝试这些经验,所以本文约定,写入的规则和读出的规则是一致的)
上一篇关于拆分的文章中提及到了数据的拆分方法,这里就不更多的提及了,总之数据的拆分方法就那些,写入和读取就按照这种规则。
在这种设计下,读数据,如果要做表关联成为一种困难的事情,所以它只是解决了一些问题,在并发的系统中,我们在这种情况下更多想将数据库目标作为存储引擎来做,对这类分布式的表读操作基本都是单表,降低数据库的压力,根据读的数据,再去检索其他的信息,相关性的静态数据可以适当用缓存来处理以提高性能。
其次,如果是对于非常大的表,如果还有扩展表,如有一个人类表,每个人类都有很多不同种类的属性,扩展属性都是动态的。所以扩展属性也数不胜数,由于人类这个表本身就很大,扩展属性就更加可怕,要是两个表做关联,结果可想而知,但是我们发现人类很多属性是具有共性的,也就是类型是相同的(如:肤色、学历、婚姻,至少属性名是相同的,是否可以只存储一份?),再考虑,这些属性都是可以被枚举的,或者说可以被数清楚的,即使要增加也不会像业务数据那么多;那么OK,我们就将这种属性和属性值单独存放起来,这里可以将这些数据放在缓存中;而这些扩展属性可以作为原表中的一个大字段来存放,如json结构,为了节约空间可以适当对K和V做一些压缩,这样就在OLTP下要查找数据,适应了扩展性的问题。
而在OLAP中,这样肯定是不行的,因为他们需要其他的维度的数据,所以OLAP更多的是清理和整理计算数据,一般这类海量数据我们是通过一些分布式平台去计算,增量信息也可以,也就是一些关联的结果需要被分析才能被统计以及被搜索;如需要统计具有硕士学历二婚以上的人(每个城市取前面几名),正常的数据库,一个GROUP BY就搞定了,但如果在海量分布式下就没那么容易了,需要通过计算成较为好计算的数据才方便使用;再例如:在搜索中输入一个黑色,那么就应当将相关黑色的内容搜索出来,而不仅仅在主表中存储了黑色的代码(如0代表了黑色);在这类计算中,OLAP就需要计算过滤处理数据了。
OK,这部分暂时就说到这里,关于拆分的方法,上一篇文章中有说明。
四、跨数据库事务问题
从这一章开始,后面都是些细节问题,相对字眼要少点,呵呵。
跨了数据库,最大的困难就是数据库的事务一致性,类似多个库,如果要做大绝对一致性几乎不太可能,除非网络各方面因素非常好,JTA本身提供了多数据源一致性操作,存储过程也可以远程操作其他的数据库(不过跨数据库类型应该还有问题),不过他们或多或少的都存在一些小问题,如果你要用最简单的方法来实现一个一致性事务,可以开辟多个数据源,然后分别用不同的数据源去操作,操作完成后,几个数据源一起做commit,或一起做rollback,虽然说这个未必能完全保证一致性,但是很大程度上是可以保证的,因为数据执行完了,commit和rollback失败的概率很低,除非网络级别和数据库级别发生不可预知的异常。
很多时候我们更加愿意用最终一致性来考虑这种问题,或者最终一致性与绝对一致性结合的方法,如上面的问题,可能我们会在和核心库中写入数据,然后将要写入其他某个库中的数据放入到一个中间件中,这个可能是一个服务器,可能是一个文件,也可能是一张表,总之在写入时,写入到本库结束,就认为成功或失败了,远程的服务,异步同步数据,如果出现什么问题,报警出来由人为处理(这种只要程序没有问题,出现问题的概率非常低),错误后自己需要有一些机制进行重试,总之不会因为某一个远程没有写入或者网络而卡住,导致一些看不懂的错误。
与之结合的方法无非是先去尝试绝对一致性,如果成功当然最好,失败的话,就采用最终一致性来完成,最大程度上保证绝对一致性,各别数据由一个小的后台线程完成异步同步,这样这个小的后台异步程序压力也会降低很多,关于一致性的细节概念还有很多很多,这里就不再屡了,不然越屡越乱。
五、跨数据库序列问题
跨了数据库,序列就有问题,不论是MySQL的索引还是Oracle的序列都会有问题,当然对于MySQL自动增长列,你可以在里头设置增长值来控制多个库之间的相同表不会被交叉,这也算是一种方法;不过如果是Oracle就不是很好办了,因为都是序列,此时这种序列应当建立统一的公共服务,类似于序列,也就是是全局唯一的,那么这个服务体系就是需要一个公共来源,我们先假设是一个数据库的表来存储这些内容。
那么这个公共服务的压力将会非常大,对程序来说也是不可信任的,而由于你为了保证锁,也就是每个时候只有一个请求能拿到序列号,那么不得不做的事情就是做一些update操作或内存直接锁掉,那么这就出现了严重的系统瓶颈,也就是系统访问上升时,这个地方自然就上去了。
OK,那么我们如何来解决这种问题,首先,通过UPDATE table SET NUM=?+100 WHERE NUM = ? AND ID=?;这条SQL语句,在一个瞬间只有一个线程可以被执行成功,也就是多个主机之间只会有一个得到最新的序号,其余的SQL执行返回的影响行数都是0(因为前面哪前一个执行完以后,NUM已经被修改,所以其余的都不会得到修改),只有这个是1,所以它此时得到它的线程会得到当前的NUM和NUM+100的值,那么它自己在这个范围内去循环分配编号,在Java中为了保证一致性可以使用AtomicInteger相关的变量来执行incrementAndGet操作活得最新的数据,保证一致性,注意这里虽然可能会造成序列的浪费,也就是数据库中的序列跳跃,但是数据库本身也是存在这些现象的,对于海量数据,我们不用纠结这些细节。
从上面的理论提出,你应该看到如果请求过大,这里虽然做了100的增长,在并发量极高的时候,也是会出现问题的,为了降低压力,我们想用两个序列,但是又想保证一致性,没办法吗?不是,办法稍微变通下,就是每个序列每次增长200,然后两个序列交叉100偏移量,如:A序列、B序列,应用按照某种规则分别负载到A或者B,A从0开始,B从100开始,A第一次取的时候,A被变成200,但是获取到A的应用只能使用A0-100之间的数据,也就是A当前值 ~ A当前值 + 偏移量;同理B也是这样,两者不重复,但是也可以负载压力,去做操作的时候基本都是行级锁,所以A和B的压力自然被分开了。
序列的问题就先说到这里,有问题再说。
六、不同应用的本地缓存问题
很多系统,为了让自己的系统跑得快一点,就初始化的时候加载一些烂七八糟的数据,首先,这个东西不要乱用,尤其是再java语言中,不要用其他所有语言的缓存思想来理解java的所有,虽然你可能测试效果是要好一些,但是可能你的测试没有出现过什么大GC或者大缓存,如果现场的数据并不适合这样做,要有办法能扯开,否则java的缓存就会成为一个累赘。
同时,这种缓存当跨越到分布式,多个系统之间,当对缓存进行修改时,其他的节点都得不到相应,此时需要定时同步、客户端指定目标去刷新等等方法去做,但是都是很挫的,而且这样做本身就不合适,除非你只是缓存一点点小数据,应用本身也不存在很大的并发量。
我们考虑的是在分布式的环境下应当采用分布式的缓存机制来完成这些工作,分布式缓存,我们非常注明就是MemCached,其实它本身是独立部署的,也就是将缓存部分和应用部分独立开了,由于它操作的独立性,所以在网络层面可以认为是分布式的,不过谈及到真正的分布式缓存还是有很多台机器组成的缓存集群组,这里的分解规则就很像数据库了,不过最大的区别就是数据库全部都是基于持久化的,缓存默认情况下是使用内存,当然也有可以支持持久化的。
此时通信缓存可能会本地基于路由规则去获取,或者直接通过某个中间件统一获取。
七、系统之间的直接依赖和间接依赖问题
系统做多了,依赖关系越来越复杂,相互之间调用越来越看不懂,越来越趋向于网络,不过网络还可以通过单纯的访问来统计,但是系统就很困难了,一个系统动了可能一串系统出现问题,尤其是数据库修改一个表可能导致一片系统不能运行,在这种情况下依赖关系分析非常重要,我们要做修改前发现这是有影响的,或者影响谁的,要统计之间的依赖,没有太好的办法,要么看源码(这个是土办法,适合代码量少,但是系统多了不可能代码量少),系统和系统之间的依赖,可以通过类似agent去跟踪代码之间的调用,包含了上述的各种RMI、RPC、HTTP等调用方法;其次对数据库的调用需要分析SQL或一些日志信息。
OK,上面这个步骤其实看了已经比较有难度了,主要是实施起来有难度,但是在必要情况下也得做,那么更有难度的是间接依赖,最终你可能会递归死在里头,因为一旦间接依赖引发了,这种相互之间的依赖将会无穷无尽,这种信息的分析和搜集必须是准确的或者说是非常精确的,否则不如不做。
这算是在一个数百个系统的相互调用中,必然存在的问题,因为代码每天在变化,会面临各种问题的挑战,变更是必然的,而且是快速的,所以依赖很重要。
八、独立模块面临的单点问题
上面有提及到缓存、统一序列的问题,这些服务虽然是一些小动作,但是当外部请求越来越多的时候,那么就不是一个服务可以搞定的了,那么此时他们也会面临单点问题,一旦他们出现问题,如果调用方没有任何容错处理,那么就会导致大面积的宕机,这是我们不想看到的,在解决模块单点问题上我们有一些常见的手段:
方法1:通过集群负载均衡来解决,一般我不愿意看到这样,因为这样投入的成本会更多,这种服务模式更多是比较集中式的大型服务中心。
方法2:一主一备,服务中的信息,日志之间是通信的,当前主宕机后,备机马上启动服务,备份机器平时可以用来做其他的工作,只需要简单监控目标是否还活着,当负载较高的时候开始。
方法3:管理者模式,也就是类似于企业,有老总、有各种副总、还有部门老大,还有员工;观察者负责去协调资源,进行分配,应用方先从观察者哪里拿到可以到那些机器上去拿数据,那些机器上可以拿什么数据,他们有没有主备关系以及状态,并缓存在本地,应用方去尝试拿他们的数据,当失败后,可以采取两种策略,一种是观察者察觉到一个切换,将配置信息改掉,应用重新获取配置信息或由观察者统一推送给应用方;另一种模式就是由应用方自己去完成主备之间的尝试,除非全部失败,否则下次就用当前尝试成功的哪一个。两者不能说谁好谁坏,各自有自己的场景。
九、各类批量分组、切换、扩展的问题
在应用、数据库、主机很多的时候,我们操作任何动作都会操作类似的动作,所以我们开始想要有批量这些动作,其实和应用系统中批量操作最大的区别就是应用系统基本是做一个update操作,可以通过数据库本身的一致性来完成,但是如果类似这样批量切换和分组落实到实际中,有可能会发生中途失败,就没有失败回滚的那么简单了。
这种分组呢,我们还比较好说,可以通过统一的绑定,将某些目标节点绑定为一个业务层次中,方便做扩展;
切换就是在同一个分组下做批量的切换动作,能不能完全成功不好说,因为策略不同,但是要保证切换的动作和最终的反馈要保持一致,批量只是提供一个平台,不过能不能完全自动化未必能保证,但是需要的是自动化的重试,提醒和容错等处理。
扩展的问题就是当业务发展过程中,主机性能或磁盘容量等已经无法满足需求了,还有就是现在中国式的热点式访问(因为中国喜欢瞎起哄看热闹,所以有啥新鲜事、新鲜人、活动什么的自然就成为),付:还有一类是瞬间高峰,这类解决方案比较特殊,需要很多预热过程,否则什么系统也扛不住,因为各类cache和系统内部各种资源都是临时分配的,而违背了系统本身的局部化原理,要让他局部化需要提前对这个系统提前预热和做相应的特殊处理才可以;
在这些情况下,我们需要扩展了,应用扩展比较方便,更多的压力排到了负载均衡设备上,数据库的扩展就不一样了,涉及到历史数据的迁移,因为扩展后就需要新的数据规则,我们一般在这些层面上,可能是因为热点数据,可能是因为容量,可能是因为压力,他们需要去扩展,如果使用时间点来记录两套规则的切换点,这个倒是一种貌似简单的方法,但是这个实施起来倒是存在很多的问题,而且对于容量不够的情况,始终还是需要迁移数据,所以这个时候需要后台做全量和增量的结合迁移,业务还在运行并写入,全量迁移中,记录增量log,最后通过log开始做增量,如果是完全迁移,最后就将原来数据truncate,如果不是,就看迁移规则是否是分区信息,是分区也可以truncate掉分区,如果不是,就只有用最慢的delete了,很恐怖,系统此时压力可能会上来,做delete的同时由于两遍都已经有了数据,所以就可以将规则进行切换,中间可能有那么很少量的数据会出现问题,不过这些数据几乎瞬间就可以搞定。
关于热点偏向,一般局部化处理它,或者说要么不要因为他影响一大堆的内容,或者不要因为其他的内容影响它,所以我们可以独立他们,偏向处理除了本身的规则外,还需要更多的信息来处理他们,因为独立后,索引可能对他们就没有任何效果了,那个表可能绝大部分内容都是他,要查询更多的业务信息可以通过自定义的业务规范来定义。
10、统一监控和恢复问题
最后,一个大型,好的分布式系统,需要有完善的监控体系,包含对应用系统各种性能指标、数据库的各种性能指标、网络、稳定性,健康状况,等做出实时监控,并且对某些关键性数据进行实时统计运算。
这类监控系统目前还没有什么太好的开源软件,因为这类大型分布式没有多少公司有,有的公司也是通过多年积淀下来的,不会一次性透露出来,因为这里的监控会涉及到多个方面,一般不是一个软件就可以搞定的问题,在大的公司内部,这些软件由于管理的内容非常庞大,而且也要求实时,所以可能会导致本身也会出现性能问题,也可能需要集群来运算,不过这不是关键,关键的是需要用它来做什么。
最后,系统出现问题,怎么恢复,除了应用上的负载均衡,数据库级别的主备切换,还涉及到更多的灾难性恢复,也就是数据丢失或者错误如何恢复的问题,并且是在线恢复,对于灾难性恢复,更多的大家会选择一种跨地区备份的策略,而在线恢复就要更多考验一些经验信息了。
更多的监控粒度是可以根据键控制预知未来的某些信息,根据趋势图预测未来的内容,按照一些数学模型建立起自动化的余量统计和预算工作,其次,就是对问题的发生具有预先判定的能力,也就是根据性能趋势图,提前预知机器可能会发生问题;
对于恢复更高的境界也是如此,当监控自动化的完成预知的时候,那么恢复也希望绝大部分是可以被自动化的,因为主机成千万上万的时候人为很难管理,再说人需要睡觉,所以运维的朋友经常睡不着,但是很多恢复的工作,我们可以抽象出百分之八十以上的经验出来是具有很多共性的,要相信,生在IT行业,要做得更好,就首先要为自己铺路,共性的东西我们基本都是可以通过软件来实现的,因为软件本身就是为了解决重复和简单,这些内容一旦被自动化(注意,这和人工智能还是有很大区别的,人工智能讲究各种模型来模拟人理方法,这里是自动通过计算机来解决我们的简单和重复,让我们做更加有意义的事情),资源的动态调配工作很多由计算机去完成,那么这就逐步迈向我们所谓的云了。
OK,本文先介绍到这里!
联系客服