分布式系统设计之路

Scroll Down

一个交易网站说开去

一个小的交易网站web系统中计算商品交易 一个小的ecs就可以办到,一个应用服务器 一台数据库 以这个来展开当业务体量很小单机单用户的时候,也是可以适用的,但是当业务规模变大,机器性能就会成为网站发展的至关重要的点。当价格价格不变,每过去18个月,集成电路上的晶体管的数据就会扩大一倍,性能也会提上一倍,摩尔定律告诉我们是单位价格的购买的机器的性能是在不断地提升,那么理论上理解就是花钱堆性能,单机也是可以承受的超大量的访问,但是实际上单机的瓶颈也摆在那里,一味通过机器来实现性能的提升是建立在不在乎成本的前提。而且其稳定性和可用性也是极差,一旦机器宕机,接踵而来的的就是系统的不可用,就会损失大量的效益转换。这是一个公司不愿意看到的。而且单机的C10K问题也是其瓶颈所在。所以在业务体量增长到一定程度,一般的重构策略都会转向分布式系统,来抵抗业务访问的激增。虽然分布式有很多优点,但是其天然问题也很可能成为程序致命的稻草。

分布式的天然问题

没有全局时钟,没有事务,集群故障排查,集群内部宕机如何恢复

并发编程

说道分布式系统,有一个前提不得不说,并发编程,也就是俗称的多线程编程,直白点说就是一个人干一件事花费20天,那么我找20个人话费1天甚至半天就可以搞完,例子中的人就相当于程序中的线程,说是那么说,一个人干20天我们不需要干预他,如果要20个人同事处理一件事,那我就要让他们的处理有序,而且不能出现同时处理一个问题的情况,还得让他们每个人做的东西彼此都能知道,要实现这个目标呢,在程序中其实就是依赖于,锁,同步关键字Synchronized,volatile JNI的CAS,等手法,去控制他们并发访问的正确性,对于并发访问的存储容器也需要颇为考究,如在互斥访问是下可以使用加锁来处理,如果是读多写少的情况,直接使用CopyonWrite开头的容器即可,对于写多读少的情况可以使用Concurrent开头的容器,在并发情况下,要注意,线不能随便创建,建议使用线程池,锁不能乱用,因为线程是奢侈的东西,锁的乱用导致的挂死,或者死锁,进而导致程序瘫痪。多线程是程序的内丹,一定要让其安稳有序的运作,不然内丹爆裂,修为就没了,这里只是提一下并发观念,具体的可以写一本书,这里就不在展开说,在后续的JVM文章会系列会讲述,因为大型网站一定是离不开多线程的,根据阿姆达尔定律可知道,程序中可并行的代码的比例决定了处理器能带来速度提升的上限。

IO模型

提完了多线程,就是I/O了,我们都知道IO不外乎就是输入输出,在java中常见的BIO,NIO,AIO(在nio2中提供了三个api),BIO就是传统的阻塞io,请求发完了在哪里wait着,当响应返回了,再去返回数据,真个请求过程取决于响应的处理过程的耗时,实际呢就是一个线程处理一个socket连接,想要扩大规模只能添加线程的数量,这无疑是有瓶颈的,而且在linux上内核默认最大的打开数目也就1024个,采取该种方式下想要扩大处理的数量,得重新编译内核。这成本不切实际了。NIO呢事件轮询也叫Reactor模式,则是一个线程处理多个socket,采取轮询的方式只去处理就绪了状态的socket,这也就是linux的上的select/poll的方式,在2002年linux上的epoll这是对此方式进一步优化,每次处理的是只返回有变化的socket,针对轮询内容做出了改进,但是epoll只能在linux上使用。AIO呢是完全的异步,nio是返回给你的时候是表面可以操作,而AIO则是把handle逻辑作为参数传递,返回的时候就是结果标识已经完成。说着这几种io模型。在java服务端一般bio和nio较多,aio使用较少。

演变这个网站

要设计一个交易网站,首先要有交易模块,用户模块, 商品模块作为依托,这三个模块我们就不往下细分了,加上web容器和数据存储 ,这个网站我们就设计完成了,慢慢的我们这个网站盈利了,用户群变得越来越大,我们的机器苦不堪言,访问卡顿,间歇性不能访问,等等条件层出不群,单点问题被放大了,我们不得不正视系统的瓶颈了,想了方案一: 先加机器横向扩展,增加负载均衡设备来将请求分发到不同的机器上,代理设备处于请求接受的入口,一旦负载均衡设备的单点情况依旧无法改变,先不说这种方案带来的延迟,因为增加了一层代理,如果数据量大了之后流量也是增加很明显的,问题是存在,但是不影响这是一种解决方案,方案二:我们在请求发起端去做这个负载均衡,通过访问一个其他服务得到的服务端的的名称,或者匹配规则,根据该规则或者服务端名称去调用,在配置默认调用的情况下,就可以避免了单纯负载均衡设备的单点故障了,也起到了一定得服务端的分流,也节省了原来负载均衡设备和服务端的带宽消耗,但是这样一搞,代码的升级重构就变得很繁琐。应为每次升级都要在代码中考虑网路因素导致的调用不可用。方案三:针对上述的情况实际上把对获取请求服务端地址的事情可以看做是一个任务,可以采用Master+Worker的方式,有master接受任务。有worker去处理,一旦master挂了重新再worker中选取一个主节点同步任务,继续分发。这三种方案我们可以适用于应用服务器,数据存储的扩展上。这样我们就从一个单机拓展到了分布式。

应用服务器的拓展

我们的处理方案的当然是逐步过渡,上述的方案可以解决,但是罗马不是一步成型,所以逐步过户的切换才是项目实际的发展进程,首先我们发现了单机负载告警了,一般要将数据存储和应用服务器在同一台机器上拆分出来。在我们拆分出来应用服务器和数据存储服务器之后这样就很舒服了,应为我们的应用服务器是业务请求的入口,迫切需要提升来应对用户的访问,所以最直接的就属于方案一了,引入负载均衡设备(硬/软件负载),前部没有细致解释这回话同步问题,直白的说就是session共享,加入用户A登录请求负载将请求到服务器X上,那么当付款请求负载到服务器Y上是后,就拿不到A用户的session信息,这样就得用户没有回话信息就得退出了,,这情况我们得改进一下怎么处理呢;方案一:将其每次需要session处理的都转向到X机器,Y机器不做含有session回话的处理,很显然X机器宕机,我们系统整个不可用,方案二:可以在X,Y机器上都存储session,通过一个session同步的模块去同步。这方案同样还是有问题,就是不能避免同步session带来的额外带宽开销,而且冗余的存储,如果同时有很多用户存在,那么session的存储开销也是很大的。方案三:我们将回话session存放在cookie中,每次将session传递到服务端,这种方式最大的一点是不安全,cookie直接暴露在浏览器,而且cookie数据大小也是有限制的,如果session中的数据很多,那么是存不下就是一个很大的问题,而且传输也是会影响带宽的消耗,方案四:针对session同步的情况,可以让session集中存储,放在redis中使用Spring-Session来做,当然集中存储session会有单点问题,读写session的网络消耗,但是相对前三种方案而言,集中存储session还是不错的方案。方案五,这个呢就是一个单独的认证服务器,用于存储所有的用户回话信息,是对方案4的一种补存。

数据存储的拓展

应用服务器已经拓展了一次了,过去的数据存储架构就显得过时了,单数据源 单点问题也是应该考虑的,目前的主流数据库都有相应的容灾和数据保护,故障恢复的手段,作为开发人员合理的利用这些技术,就可以拓展我们的数据存储,以mysql为例,我们一般的处理方案为增加从库,并且咋程序中使用动态数据源,读取操作去查询slave库,而写入操作使用master库,master和slave的同步依赖数据库的异步复制,而且mysql数据库的主存复制是完全镜像式的,不考虑复制延迟的情况下,最大程度保证了主从的数据一致,虽然从数据库方面来说,具备一定能力的容灾,主库一般执行写操作,从库提供读取服务,但是如果单位时间没大量的写操作出现,这种数据的延迟在生产环境是不可避免的,所以单纯数据库层面的远远不够,而且主从数据库的设计不能盲目,要根据具体的业务下进行设计,读写分离呢,在大面上来说是要给原有得读写组件增加读源,并且要解决这个新增的读源的数据同步问题。当然采取数据库作为读源是最基础的,再此之上,我们建立搜索集群是一个非常不错的方式,一系列的大型网站的站内搜索,最初也和我们的小型交易网站一样 出发点是DB,后续的扩展呢回会引入搜索引擎基于lucene的solr和ElasticSearch之流的组件,基于向量空间映射的倒排索引可以很快的提升检索的速度,以Elasticsearch为例,我们查询数据的操作可以理解为索引文档,也就是搜索索引真实数据之后生成为文档,索引真实数据就是我们从数据库数据复制的过程,但是什么数据走数据库,什么数据走Elasticsearch这个没有统一的规定,具体业务方会根据自己的业务规模和热点数据密集程度来处理,使用上也比较方便和原来的数据库使用没有太大的区别,也就是api方式的处理有所不同,在于建立索引的方式,一种是全量一种为增量,全量方式一般在数据初始也就是第一次的时候使用,也可能在重建的时候,增量就是在原有的基础上进行补充服务,一般是把每天库里新增的数据进行增量索引,有实时好和非实时的方式,实时呢是数据库有更新,索引就reindex,(笔者公司采用的实时监控主库的ddl语句进行增量更新数据)用于热点数据,非实时就是每晚业务峰值下降了,进行增量一般用于非热点数据。

引入搜索引擎之后,我们的小型交易网站已经大变样了,检索速度快的飞起,但是这还是不太完美。例如单用户的热数据我们目前而言只可以从数据源读取,应为这些热数据是无法存储在搜索引擎的,这个时候加速神器cache就出现了,当缓存命中就不用去数据库进行查询,缓存数据的写入有很多种方式,主动和被动的处理,主动方式是数据库发生更改就去刷新缓存,被动呢就是缓存命中失败之后,去查库,同时做一个写入缓存的操作,当然除了数据缓存,在我们的小型交易系统中,一些的页面的数据我们不必要频繁的去服务器上load,可以使用页面缓存来处理使用apache或者nginx之类的均可以。在数据库方面我们还是有瓶颈的,于是分布式存储就应运而生,hadoop之流的分布式文件系统就是被广为使用。尽管增加一系列骚操作 ,解决了读写存储问题,但是随着我们的业务体量的扩大,数据库的压力还是存在,这时候就需要对数据表进行垂直/水平拆分进行处理。

所谓垂直拆分就是专库专用,把我们原来的商品 订单 用户数据独立放如不同的数据库,应用上以多数据源的形式处理,当然专库专用之后,单机事务就不生效了,为了保证一致性我们可以采取分布式事务,或者干脆不适用事务,出现脏数据之后由程序托管,尽管我们拆分了专库专用,但是业务再大的话,库里的表就会变成一张无形的大表,深不可测,或者说就是该表已经达到了该库单表的最大值,这个时候就要进行水平拆分,水平拆分呢就是大表变小表,这样呢虽然解决了存储但是唯一主键这个就被打破了,该数据表的唯一主键就不能使用原来的自增序列了,可以依托于别的方式如分布式唯一id 或者zookeeper的临时有序节点,或者uuid均可以。当然我们最大的问题还是分页,就涉及到跨表了。这是一个很大需要考虑的问题。

应用拆为服务的拓展

在进行上述的拆分之后,我们的业务系统已经具备了不错的容错能力了,我们经历了应用服务器,数据库 缓存,搜索引擎的填充,整个网站变得非常的庞大,应用服务器的水平拓展在一定范围内业务体量来说是没有问题,完全可以cover住,随着越来越大就好考虑如何不让应用持续增长,一些固有的功能就可以抽离出来,从而一变多,来化解应用不断变大的问题,从业务特性也就是功能上来划分,比如用户,订单 商品 就单拎出来,原来一个分成三个系统,但是这么搞的话依旧是还有一些应用上的缺陷比如相同的交互代码(如 Connection DB)还是会存在各个系统中,不是很彻底,同时这个多个系统的调用无非还得是http方式来调用,就得维护一个各业务线的url资源文件,每次被调用方修改其余隔断都需要修改接口,维护性极差,不过这个方式来讲依旧是一种解决方案,沿着这个演变的趋势,再细粒度的拆分就是让应用走服务化的方案,面向用户的web层,web层面向各个服务中心,不同的服务交互不同的业务数据库,这样处理之后各个层负责的事情更加专一,职能更加着重业务,同时还降低了应用发布的风险,各端发布各端的 都也不影响彼此,各个服务之间初期可以使用http调用,进一步的优化可以带入一些分布式的消息中间件,这样不同系统的调用可以不用同步等待,可以异步化,这也是很好地解耦,不用关心对方的影响,至于对方消费的成功与否只是对方和消息中间件的关系,如果消息确认则offset递增,没有确认那么该消息依旧存在于消息中间件中,至于排队还是其他的操作那有对方自己处理了。

#### 总结

从当初的单机程序到我们各层划分,优化演变进而变为了可以支撑大体量的qps,具备了大型业务网站应有的健壮,当然这个不是所有网站演变的规范,就是会在每一个模块会遇到的问题和常规性的解决方案,具体业务规模到一定程度再做一定的事情,微服务最近是个时髦词汇,但是不是所有的业务都适合,这个道理都明白,人家本来业务就不大 几个应用服务器水平扩展完了就完美处理,走服务化反而是更加的臃肿,所以技术要在合适的场景下使用就是好技术,上面就是对于一个分布式系统搭建的一点小总结。