记录下最近看分布式系统设计的一些感想(上)

正式内容开始前的一点题外话:

去年毕业,到上个月13号工作正式满一年了,在工作开始之初就想着要多写博客,后来慢慢发现,自己没啥积累,即使写也是CV别人的,没多大意义,于是就放弃了写博客,专心提升自己。直到最近看了一些关于分布式系统设计的文章,才想好好总结总结,于是有了这篇难产的博文。

下面就开始胡诌了。

1、什么是分布式系统?为什么会有分布式系统?

我理解分布式系统就是将多台服务器按某种规则组合在一起,以期望获得更大的请求处理能力,让系统更加的稳健,不会因为某些故障导致系统的不可用。

在分布式系统出现之前,我们的系统都会面临两个问题,一个单机瓶颈,还有一个是单机故障:

  • 单机瓶颈就是一台机器的处理能力是有限的,随着业务的发展,更多用户的访问使得单机无法处理更多的请求,由此造成用户体验的下降、用户流失;
  • 单机故障就是当我们将服务部署到一台机器上后,万一服务器所在区域停电了、服务器网线断了,总之因为各种各样的原因,导致服务器宕机、网络不通,这样都会导致服务不可用

解决以上两种问题的办法就是加机器,对于第二种情况除了加机器之外,我们可能还需要多机房灾备、更甚至的就是异地多活,博主所在公司去年就在主推异地多活。将多台服务器通过网络组合在一起,形成一个分布式系统。

而我们根据服务器功能的不同可以将这些分布式系统细分为:分布式服务系统(包括微服务、消息服务、搜索服务等)、分布式缓存、分布式存储等等。

2、分布式系统带来的挑战

但是任何事情都是两面的,分布式系统给我们带来好处的同时,也带来了更多的挑战。例如:

  • 分布式环境下用户请求路由问题、以及连带的分布式session共享问题
  • 多个服务之间的分布式事务问题
  • 并发访问中涉及到的分布式锁问题
  • 。。。。。。

以上只是列举了部分问题,还有更多问题等待解决。

2.1、分布式系统带来的会话问题

当在单机环境下,用户的请求一直都落在同一台机器上,这样不会存在获取不到session的问题,但是当我们部署多台机器之后,利用硬件负载或者软件负载都可能会导致用户的请求落在不同的机器上,此时各个机器上的session如何实现共享,是我们首先需要解决的问题,一般采用的方法有:

  • 通过负载算法,根据用户的信息进行负载,让同一用户的请求落到同一台机器,这种是最简单也是最方便的方法,但是此方法存在一个很严重的问题,当服务器进行扩容时,会出现部分session丢失的问题
  • session复制:即同步每台服务器上的session,使得每台服务器上拥有相同的session,这样不管用户请求到哪一台服务器,都不会有问题,但是session的同步是与风险的,而且全量复制也浪费资源
  • session统一存储:将用户的session用一个数据库存储起来,所有服务器的session通过统一的session服务去获取,可以是关系型数据库,也可以是NOSQL

我们通常说到的单点登录也是分布式会话引出的一个分支。

2.2、分布式微服务框架

近年来,微服务大行其道,得力于优秀的开源框架Spring Cloud,很多公司都可以非常方便的搭建一个分布式微服务系统。对于微服务,我理解的就是服务拆分,让系统模块化,以达到解耦的目的,只有这样才能满足业务的多变化需求。

那一般微服务框架是如何搭建的,都由哪些组成呢。

  • 注册中心:注册中心是微服务框架的核心,给服务的提供方和消费方搭起了一座桥梁。主要负责服务发现、服务治理
  • 服务提供者:具体服务的实现方,一般提供各种业务需要的基础服务。
  • 服务消费者:完成用户请求时需要调用各种服务进行业务逻辑处理。

注册中心一般分为两部分,一个是服务端,主要是记录服务的信息,例如:服务提供者的ip地址和端口号、服务名称、服务参数(分组信息、超时时间、重试次数等)、方法信息等;还有一个就是客户端,客户端主要负责对提供方的服务进行注册,以及检测当前提供方是否存活,以及对消费方提供拉取服务信息的功能。

一个服务在启动的工程中,先由客户端分别向注册中心注册和拉取相应的服务信息,消费方会缓存一份服务信息在本地避免进行频繁的网络请求;当服务方出现新节点或者节点出现故障时,会告知客户端服务的变更,常见的服务注册中心有三个:

  • zookeeper:利用其节点的特性,通过临时节点和节点变更的监听机制,使客户端可以及时的发现服务的变更,但是zk作为服务注册中心也存在问题,由于在CAP(一致性、可用性、分区容错性)理论中,zk保证的是CP,这会导致,当存在极多节点时,由于需要保证一致性,在处理事务请求,各个节点进行数据同步时需要时间,会导致短暂的服务不可用。
  • eureka:eureka并没有像zk那样保证数据的强一致性,它只保证数据的最终一致性,通过eureka client组件每隔30秒向eureka server拉取最新服务信息以及发送心跳检测。集群模式下多个节点的数据一致,服务注册时先写到一个节点上,然后再异步同步给其他节点,不像zk那样有leader,每个节点平等。这样会存在一个问题,当服务刚注册到一个节点,还没来得及同步,这个节点挂掉了,造成服务的丢失。同时由于客户端每隔30秒才刷新一次服务信息,导致可能会调用到已经挂掉的服务,而且每个节点保存的相同数据也造成了资源的浪费。另外,当服务极多时u,每个服务给注册中心发送心跳检测对注册中心也是极大的压力。
  • consul:由于eureka2.X已不在开源,注册中心急需一个替代品,这个替代品就是consul,consul和zk一样也是保证的CP,consul分为 consul agent和consul seerver,服务由agent进行注册,需要注意的是,agent是安装在客户端(提供者和消费者)的一个程序,它会不断发请求检查服务是否存活,若不存活,则上报给server服务不可用,相对于eureka直接向server发送心跳检测,这一点的改良极大的降低了server的压力

而服务调用的发起就是消费方通过本地的服务存根,根据某种负载均衡算法确定一台服务提供方后发起网络请求,服务提供方通过动态代理执行方法,返回结果。

在Spring Cloud中,也对应有相应的组件:

  • Ribbon:服务负载均衡,默认是轮询
  • Feign:动态代理的具体实施者
  • Hystrix:主要是针对服务调用的容错机制,例如:熔断和降级
  • Zuul:网关,可以统一不同来源的请求,为PC、APP等提供统一的流量入口

再具体到细节,例如编码解码、序列化与反序列化、协议的定义等也都是很关键的步骤,但此处不具体赘述,感兴趣的可以私下讨论。

另外我们还需要关注的是我们应该如何优雅的设计一个服务,笔者前两天遇到一个情况,在调用其他部门的查询服务时,服务提供方给返回了一个未知异常,线上环境,返回未知异常,着实把笔者给吓了一跳,立马让兄弟部门给查下是什么原因,结果说是查询的结果是空,查询是空,返回了未知异常。在这里有两个点:一个是他将异常直接抛了出来;另一个是业务异常也返回未知异常,这样的接口设计其实是很糟糕的,对于服务调用方而言,他们只关注结果,所有我们在设计接口时对于异常处理需要谨慎,不然别人就会骂人了。

2.3、分布式消息服务

消息队列现在已经成为分布式系统中不可或缺的一部分,它不仅能够实现系统解耦、异步调用,而且还能达到削峰填谷的作用。但同时消息队列的引入也给我们带来了挑战:

  • 系统稳定性降低:只要往系统里面引入一个工具都会导致整个系统稳定性的降低,对于MQ就是消息丢失、重复消费等
  • 系统可用性降低:只要我们想想我们目前的系统,如果他的消息系统挂了,将会发生啥应该就明白了
  • 分布式系统数据一致性:根据BASE理论,我们可以通过某种中间中间状态来达到最终一致,但是如果这个时候出了问题就会导致数据不一致

那前面已经说了MQ带来的问题,该如何解决它呢?

首先应该如何保证消息不丢失呢,我们知道在发送一条消息的时候,消息中间件都会去落磁盘,因为只有存储到磁盘上,才是最安全的,不会丢失。当消息的服务端接收到发送的消息之后,都会给消息发送方一个ack确认指令,表示该消息已经收到了,但是此时如果在消息服务中心返回ack确认之后,还没来得及把消息刷到磁盘,宕机了,此时,消息发送方收到确认后不会再重发消息,而消息服务中心宕机了,这条消息就丢失了。所以在发送ack确认之前,需要先把消息落磁盘,但是我们知道写磁盘是一个极其费时的操作,如果来一条消息就写一次磁盘,而且一般发送消息是在内存里面,比写磁盘快很多倍,此时所有线程都阻塞在等待写磁盘操作返回ack这里,导致线程被打满,服务不可用,这是不允许的。

所以首先要优化,异步确认机制,即发送方发完消息就返回,接着发送下一条,每个消息的ack确认异步返回,这样可以让发送消息线程正常工作,但是写磁盘的压力任然存在,很多优秀的开源框架都采用了优化方法,例如kafka采用操作系统的页缓存机制,也有利用先写缓存,然后异步刷磁盘的机制等。利用两块缓冲区,消息先写其中一块,写满之后进行交换,后台另起线程将写满的缓冲区刷到磁盘。此时也会存在问题就是刷到磁盘始终是比写缓冲区要慢的,此处任然会有阻塞写的可能。

发送消息和消息落盘都确保消息不丢失之后,就只剩消息消费保证消息不丢失了,也是一样,在消费消息后,需要手动确认消息是否消费,而不是自动确认,这样就能保证消息不丢了,而消息的重复消费就需要消费方对接口做幂等处理。

下面一个问题是该如何保证消息系统的高可用,目前各个消息系统都有自己的高可用机制,例如kafka通过给每个partition进行备份,ActiveMQ针对不同的持久化机制也有不同的集群搭建方式,但是我们还是需要视系统的重要程度去考虑整个系统的高可用方案。如果MQ集群直接宕机了,整个集群不可用,此时我们应该如何降级,首先我们需要MQ服务宕机的发现机制,当发现MQ宕机后,我们需要自动切换降级服务,将消息写入到另外的存储服务,考虑到消息的并发情况,我们可以选择数据库或者redis等,利用redis的队列数据结构来进行消息发送和消费。而在使用redis的过程中我们又需要注意大key和热key的产生,你会发现不管干啥总会有新的挑战。

而对于最后一个问题,如何保证数据一致性的问题,这个就需要我们在设计的时候,要有容错机制,去主动发现不一致的数据,然后进行解决,当然最直接有效的方法就是用worker了,worker大法好,专治一切花里胡哨。通过worker去定时轮询数据库中的数据状态,当然此处我们可以将轮询的对象选择为从库,这样可以减轻主库的压力,这些都可以视情况而定。

先写这些吧,剩余的下次再写!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值