在大部分互联网公司中业务和技术是这样的关系,公司是业务驱动型的,技术是服务于业务的,在不同阶段技术承担着业务背后的守门员角色。
互联网系统系统发展到一定规模之后需要面对多个层面不同的压力,从资源压力到数据访问瓶颈都需要持续进行解决。
比如早期的互联网业务更多的是为了进行业务试错,所以通过单体系统实现一个MVP架构。随着业务落地之后用户规模的增加,会进入分布式阶段,通过大量无状态的服务水平扩展解决大用户量带来的压力。之后进入业务和数据高度丰富的阶段,这个解决需要解决的除了性能问题还有数据安全性和业务的可用性,一般通过异地多活的方式实现容灾。再之后是一系列的精细化运营,主要是降低平台成本,通过工具化自动化等方案实现降本增效。
对于互联网架构,一般需要解决的问题是高并发场景下的高性能,高可用,以及高扩展。
高性能的标准是低延迟,高可用的标准是几个9,高扩展的标准是开发成本低。
我们假借支付宝的发展看下在业务和数据发展到一定规模之后,互联网系统架构应如何设计。
比如在数据量发展到一定规模之后,我们可以进行数据库拆分,可以进行分表,一般采用水平拆分解决写瓶颈或是数据量问题。也会按照业务垂直的拆分到多个库中,上层是一套套的微服务,经过分库分表和微服务拆分基本可以支撑TPS在万级甚至更高的访问量了。
随着微服务应用数量的越来越多,数据库连接数量也增长巨大,让数据库本身资源成为了瓶颈。这个问题发生的根源在于所有应用无差别的访问数据库,这样数据库连接数就变成了笛卡尔积了。
本质上这种是因为资源隔离处理的不够彻底,需要解决的方式是逻辑上移,从数据层,到服务层,到API层进行分离,也就是一套套的独立部署单元,也就是单元化的雏形。
这样我可以将整套单元进行独立打包部署,同类请求进入一个单元内被消化,由于这种彻底的隔离性,整个单元可以轻松部署到任意到机房并且保证逻辑上的统一。
强一致性场景,比如金融可以做三地五中心部署:
支付宝是一个金融场景,也是一款国民应用,日访问的TPS可以达到几十万上百万,所以早就做了单元化方案,去解决容量上的问题和实现异地容灾的需求。
支付宝采用三地五中心保障整体系统的可用性,其单元化分成三类(CRG架构):
“写读时间差现象”情况是,大部分数据写入后,都会需要持续的读取,比如我们办完银行卡后,可能很长时间才会存进去一笔钱,这样在我们真实世界中对于数据一致性的要求是远低于数据复制的一致性的。所以异地延迟100ms对于业务上也不会造成营销,所以只需要CZone中写入后100ms后可以用到这个数据,就可以将数据和服务放到CZone中。
为什么需要这三种部署单元方式,背后是因为业务上对于不同性质数据的要求不同。我们经常用UserId来进行分库分表操作,围绕此类系统数据可以分为两类:
我们可以将上面提到的数据划分到RZone和GZone中,RZone包含具体用户数据及服务,GZone包含用户共享数据及服务。
在支付宝单元化架构中,是没有CZone的。GZone只能单地部署,因为数据要被共享,多地部署会再来异地延迟引起不一致,比如实时风控数据,如果多地部署,每个RZone只访问本地数据很容易获取旧的数据和规则这样很危险。
那么所有数据都不支持延迟吗?
其实大部分场景是写后不需要实时读取的,也就是对于数据强一致性要求不高的。对于这部分数据我们允许每个地区RZone服务直接访问本地,为了支持这些RZone的数据访问,也就提出了CZone概念,在CZone场景下,写请求一般从GZone写入公共数据的主库,然后同步到OB集群,然后由CZone提取数据,比如支付宝会员服务。
单元化之后,异地多活只是多地部署而已,支付宝对单元化的基本要求是每个单元都具备服务于用户的能力,而且支付宝的单元化和用户关系的配置是可以动态修改的,这样异地双活的单元还起到了彼此备份的作用。
spanner是nginx的代理网关,有些请求可以通过反向代理转发到其他IDC的spanner而无需进入后端服务。对于应该在本IDC处理的请求,直接映射到对应的RZ即可。
进入单元内的请求如果只需要访问用户流水型数据,一般不会在进行路由了。而有些场景可能需要访问其他用户共享的数据,那么可能会涉及到再次路由,这种处理有两个结果:跳转到其他IDC或跳转到本IDC的其他RZone。
RZone到DB数据分区的访问是实现配置好的,比如RZ和DB的关系为:
- RZ0* --> a
- RZ1* --> b
- RZ2* --> c
- RZ3* --> d
比如杭州用户访问了支付宝域名,默认按照地域路由流量,根据IP将域名解析到杭州IDC的IP。之后请求达到IDC-1的spanner集群服务器上,spanner从内存读取到了路由配置,知道了请求主体用户所属的RZ3*不在本IDC,于是直接转发到IDC-2处理。之后根据流量配比规则分发到RZ3B进行处理。RZ3B得到请求后对数据分区C进行访问,处理完成后原路返回。
为了解决跨地域路由问题,可以将决策迁移到客户端,比如每个IDC机房有自己的域名,如IDC-1对应域名1,IDC-2对应域名2,APP中可以直接通过rest访问不同的域名,后面对于用户行为的解析都在域名之上发生,避免了走一遍IDC-1带来的延迟。
对于容灾的处理的前提是,当发生灾难后通用的方法是把陷入灾难的单元的流量打到正常的单元上去,这个过程叫做切流量。支付宝架构灾备有三个层次:
1. 同机房单元间灾备
2. 同城机房间灾备
3. 异地机房间灾备
对于灾难来说,最小的灾难是某个单元由于插座断开,线路老化,人为误操作等导致宕机,由于每组RZ都有A,B两个单元,可以用来做同机房灾备,并且AB之间是双活双备,正常AB两个单元共同分担所有请求,一旦A挂了,B将自动承担A单元流量份额,是默认灾备方案。
如果因为机房光缆被挖断,或者机房维护任何操作事务,需要人工定制切流量。
切流量流程如下:
这样在切流量期间的用户请求会失败,并发起重试提示。真实情况下,并不是在发生灾难时才进行切流量,而是事先配置好预案,推送给客户端,或者集成到spanner。
异地机房容灾方案和同机房灾备方案基本一致。
分布式系统解决的最大痛点就是单机系统可用性问题,要想高可用,必须分布式。比如我们会启动多个服务层水平扩展,底层可能还是一个数据库。一方面解决了单点导致的低可用性问题,另一方面无论这些水平扩展的机器间网络是否出现分区,这些服务器都可以各自提供服务,因为机器之间是无状态的,不需要考虑数据一致性问题。
为什么分布式系统里面不推崇采用数据库事务呢?因为用了事务数据库就变成了单点和瓶颈了。
所以从CAP角度来说,分布式系统如果满足了CP,A不出色是常态,也就有另一个BASE定理了,也就是最终一致性。在分布式系统里面,一致性一般选择退让。
一般系统应用层是无状态的水平扩展,数据一致性处理一般交给数据层,那么如果想要在应用层做到一致性,有什么好方法吗?做好一致性需要处理应用之间的状态同步,大家可以参考下ZK的方案。
对于数据层,我们可以采用读写分离,一主多从缓解读可用性问题,写可用性问题的解决方案可以通过keepalived的HA框架保证主库是活着的,这种方案带来性能上的可用性提示,至少不会因为某个实例挂了就不可用了。
主从之间进行通信,通过心跳保证只有一个Master,一旦发生分区,每个分区会选择一个新的Master,这样出现脑裂,常见的主从数据库解决方案,没有自带的脑裂解决方案。可以考虑引入仲裁机制,从而实现最终一致性,自动规则无法合并的情况只能依赖于人工处理了。
异地多活的意思是,每一地都可能产生数据写入,异地之间偶尔网络延迟,网络故障都会导致网络产生分区,一个机房里面很少产生网络分区,但是异地这个概率会很高。所以支付宝的三地五中心需要考虑这个问题,应对网络带来的分区问题,做好数据一致性处理。
支付宝的每个单元由两部分组成:复制业务逻辑计算的应用和负责数据持久的数据库。应用层不对写一致性负责,这个任务下沉到数据库。所以关键在数据库。
支付宝自研了OceanBase,可以解决分区之后脑裂的数据不一致问题。
分区容忍性可以分为“可用性分区容忍”和“一致性分区容忍”。
可用性分区容忍的关键在于不让一个事务到所有节点,一个事务没必要让所有节点都参与。
一致性分区容忍的意思是说,既然分区了还谈什么事实一致性,但要保证最终一致性也不是那么容易,如果出现分区,如何保证同一时刻只有一份协议呢?如何保证脑裂后只有一个脑呢?
简单的脑的理解是可以处理写的,那些非脑就是只处理读的。
Paxos是唯一解决分布式场景下一致性的解决方案。Paxos逃离了CAP的约束。所以我们可以任务Paxos是唯一一种保障分布式系统最终一致性共识算法,通过过半投票保障大家结果一致。
我们想一下Paxos是怎么处理的。
Paxos要求任何一个提议,需要超过半数以上节点认可,才认为可信。如果已经完成了多数节点认可,但是系统宕机了。重启后仍然可以通过一次投票知道哪些值是合法的。这样可以解决分区下的共识问题。
一旦产生分区,我们要求一个分区内节点数量超过半数,这样的设计可以巧妙的避开脑裂问题。当然MySql集群脑裂问题可以通过其他方式解决,比如同时ping一个公共服务ip,成功者继续为脑,但是会制造另一个单点。
Paxos的一致性贡献在于,集群中有部分节点保有了本次事务的正确结果,正确的结果随后以异步方式同步到其他节点上,从而保证了最终一致性。
在回过头说下支付宝的OB如何做到数据库层面的一致性的。在OB体系下,每个数据库实例都具备读写能力,具体的读写可以动态配置。对于某一类数据比如用户号段内的数据,在任意一刻只有一个单元负责写入节点,其他节点要么实时同步数据,要么异步复制数据。
OB采用了Paxos共识,实时库之间同步节点个数至少需要(n/2)+1个,这样可以解决分区容忍性问题。
比如用户最开始请求到A单元,但是这时A单元网络断开产生了分区,我们将A单元某个号段数据交给了B单元,这次用户进行了重试,但是网络断开之前的请求已经进入了A,这样A,B单元里都有了数据,导致了不一致。
在OB体系下这种情况不会发生,因为A单元与其他节点之间失去了联系,其他节点对于它来说是不可达的,A知道自己分区了,所以这个提议被丢弃,之后B会接替A完成写入任务。之后B同步了自己的提议,大部分节点响应了,最终的数据是B写入的数据,A从来没机会写入。
OB节点之间相互通信,进而完成数据同步,如果出现分区问题,OB通过同步部分节点来保证可用性,OB解决了分区容错。
OB事务只需要同步到(n/2)+个节点,允许剩余一小半节点分区,只要(n/2)+1节点存活就是可用的。
OB不能解决强一致性问题,因为分区情况下,部分节点失联了,但是通过共识算法,保障同一时刻只有一个合法值,并且最终会通过节点间同步达到最终一致性。
所以OB没有逃离CAP魔咒,产生分区时,牺牲了C,整体来说还是AP的。
回过头再来看下支付宝在大规模用户请求下做的架构设计:
基于用户分组的RZone设计,让每个用户可以独占一个单元完成请求,解决了系统容量带来的挑战。
RZone在网络分区或灾备切换时,OB的防脑裂设计(Paxos),我们知道RZone是单脑的(读写都在一个单元对应的库),而网络分区时可能产生多个脑,OB解决了此种情况下的共识问题。
基于CZone的本地读设计,很大程度上保障了写读时间差的现象,提升了本地读速度。
剩下的一点情况不能本地读取,只能实时访问GZone的公共配置,也不会造成太大的问题,比如实时库存数据,可以通过“页面展示查询走应用层缓存”+“实际下单时再校验”的方式减少对于GZone的依赖和调用量。
当然除了单元化和OB的方案,支付宝还做了其他一些事情,比如双十一的缓存预热,削峰运营数据,压测容量规划等技术手段。其他互联网公司可以借鉴下。