服务更新的思考

转载请附本文链接:https://blog.csdn.net/maxlovezyy/article/details/100199461

本篇文章总结一下个人所思考的关于分布式下怎么去比较好地热更新一个服务,其中会涉及到一些关于分布式下一致性的思考以及升级和数据迁移的一些思路。下面进入正题。

分类

  • 从更新的对象角度看,涉及两个主要方面:一个是程序二进制可执行文件的更新,一个是程序执行时的配置文件的更新。

  • 从更新所造成的可用性影响上看,有冷和热两种方式

  1. 冷更新:停服务,更新,恢复服务。从流程上看,是有一段时间服务处于不可用状态的,这对于游戏或者一些可用性要求较高的公有服务其实是不可接受的。
  2. 热更新:服务正常运行,不需要停服务更新。是提升用户体验的一个非常有效的手段, 小到单机服务,大到分布式服务。一旦你支持热更新,那么就意味着更新过程中用户可以基本无感知或者说感受不到失败(可能会感受到一些延迟)。

面对的问题

由于冷更新很简单,也不适用于互联网等要求可用性较高的方向,就不多说了。本文主要讨论在分布式的情况下,无状态的业务层怎么做热更新。

这里我们拿对象存储业务来讨论,由于其涉及存储和一致性问题,所以相对来讲也比较具有一般性,普适性。一旦掌握理解了,对其他系统是具有良好的参考意义甚至直接拿来主义的。

业务架构

在这里插入图片描述
架构上看都很明显,只说明一下数据层的route layer,这一层的作用就是把数据层做一个抽象,数据层后端即便是多异构,对于业务层来说也是透明的,使用起来和一套同构的没有任何区别。包括滚动升级、数据迁移等等都是透明的,agent层服务负责管理路由信息,用户都是无感知的。Agent实例本身对于业务来说是无状态的,所有数据部分的状态都是在下面的数据库中,但是其自身逻辑的路由信息是有状态的,需要管理。

对于一个写请求,大致流程为:用户通过应用发起请求,服务侧的接入层进行分流和导流,进一步请求进入到业务层,业务层收到请求后,对于每一个data block通过data client经data agent写入到数据服务,成功后得到一个block id,之后将block id作为metadata通过meta client经meta agent写入到元数据服务,之后后返回给用户结果。

对于整个架构和流程,可以看出route layer是至关重要的,它是易用性、一致性、可用性上非常重要的一层。下面着重讨论元数据部分,其他的都可以通过这方面的讨论类比分析。

一个业务很容易初期采用简单粗暴的方式快速部署,无论从成本还是易用性角度,都很可能选择mysql作为其metadata服务。而假如随着业务的发展,其储量和流量可能越来越大,不是一个mysql能hold住的了,就需要扩容或者迁移到一个分布式数据库上(这里忽略proxy的sharding方式,个人不是很喜欢proxy的方式,不太可控,提供的一致性语义相对较弱,比如说事务,而且维护复杂)。一旦你有了这样的需求,就需要定制一个可靠的计划做这件事,那么这里假设是基于上述架构来做这件事。很容易看出,我们只需把route layer做好,对于meta来说即把meta agent做好就可以了,更进一步,本质就是做好meta agent的状态管理即可。这个状态管理的终极目标就是保证数据的一致性、服务的易用性和可用性。下面通过几个常用的方式去讨论具体的作法。

具体做法

1. 蓝绿部署

蓝绿部署是一个很常见的业务更新方式,其核心思想是一半一半做。先下掉一半,更新,之后上线,另一半重复上述步骤。其有两个主要问题:

  1. 一致性
    对于一致性要求不高的服务,简单的按照上述做法,只要数据层client来点重试,那么业务层除了latency,是没有其他感知的。但这样做其实背后隐藏着一致性问题,因为假设不加任何保护措施,在下掉一半的时候其上有一个用户写请求A,假如client立刻重试A到了另一半并成功,之后用户又发起了一个写请求B,覆盖了刚才A的数据,这时候被下掉的一半其实已经发出了之前的请求A,又执行了且成功了,这时候用户后面的请求B就被覆盖了,破坏了线性一致性。那么狠容易想到,我可以让被下掉的一般以gracefully的方式把doing的请求做完再下掉啊,期间不接受新请求,这样client就不会因为FIN/RST原因重试了啊。这样的问题就是,如果crush了呢?甚至于说网络问题导致非FIN/RST而是timeout了呢?Client一样会重试。
  2. 可用性
    由于需要下掉一半,那么意味着可提供服务的能力也下降了一半,这样的话对于持续高压的业务是没办法接受的,因为一个是你找不到这么一个时间点流量低去升级,一个是没那么土豪搞那么多机器去临时扩容,当然这些都比较极端,可能绝大部分业务没这样的问题。但还有一个问题,就是这样实现,那么下掉的流程基本上就是a.先向S1的所有服务实例发送停止接收新请求的命令,b.之后向服务发现发送请求摘掉这一半agent S1,都成功且处于空闲状态后,c. 进行更新。这里a和b不可以调换,否则可能会有线性一致性问题,比如我这个更新是切库,先一个请求通过更新后的agent把数据写入到了新集群,后一个请求通过还未更新的agent读取数据,读不到的。另外上述流程看似没问题,其实这里隐藏了一个问题,那就是可能影响服务的可用性。因为业务层都是有缓存的,也就是说不是每个请求都去服务发现那里问一下都有哪些agent(且不说服务发现能不能扛得住,latency也比较难以接受),既然有缓存,那么就可能存在缓存不一致,就存在某一个client多次重试都定向到了被下掉的S1中,最终请求失败了。当然可以通过一定的策略大概率上避免这个问题,就是几次失败后刷新缓存,而不是等到缓存失效,甚至服务发现具有通知机制通知更新。但其实还隐藏着另外一个问题,假设第二步向S1所有实例发送停止接受新请求时某一个失败了呢?比如说它跟administrator所在的操作节点网络隔离了呢?这些异常情况都需要处理,否则就可能出现数据不一致。

凡此种种,是不是看起来也很麻烦?假如更新binary,这没有办法,必须重启服务(不要想不是能动态加载动态库中的函数么,没必要非的重启吧?且不说这样编程有多麻烦,你需要一个可靠机制去通知配置变更了,而且其本质上等价于重启)。假如只是更新配置呢?这么折腾一番是不是看起来很麻烦?有没有一种方式更轻便一些呢?看下面。

2. 配置中心

对于只想做配置更新而非binary的话,通过蓝绿部署是很重的,而且对于超大的集群来说,是很费时费力的,检查验证都是很繁重的任务。不过可以通过引入配置中心,去相对地解决这个问题。比如所有agent都缓存着集群的配置信息,所有配置都持久化在配置中心,变更也通过配置中心的接口做。所有agent都向配置中心订阅本集群的配置变更消息,一旦配置有所变更,向所有的agent发送事件(当然agent也有超时重新获取机制),所有agent都重新获取配置,更新完毕。显而易见,这里有一致性问题,各个agent不可能“同时”做到配置变更,那么就有可能出现配置不同导致的线性一致性问题。比较naive的解决方式是让每一个收到变更事件的agent都wait 1s或更多hang住请求,等待其他agent同步,不过这依然可能会因为时间不够或者网络问题导致不一致。那么有没有一种办法能做到完美呢?既能做到热更新,又能做到简单、高可用、线性一致呢?看下面。

3. 带lease的配置项

上面的配置中心方案具有一致性问题,怎么解决?我们可以通过lease来解决这个问题。我们假设时钟漂移是有界(ε)的,这是完全可以假定的且实际可以成立的,比如通过NTP来做,一旦agent检测到NTP在一个有界时间内没有更新了,则判定时钟失效,实例不对外提供服务。基于有界时钟,我们可以通过改造配置中心,使其具有下发带有效期(deadline)的配置项的功能即可。各个agent获取到配置项之后,可认为在[deadline - ε]之前的时间里配置项是有效的且集群一致的。配置中心在更新此种配置项的时候不可以随意更新,需要等到[max_deadline + ε]之后再更新,这样就保证了agents配置的一致性。不过这里存在一个问题,那就是配置中心在等待更新期间可能会有某一个agent A的 deadline较大,在等它失效之前其他agent是无法进行服务的,这样会导致流量都流向了A甚至不断重试最终失败了,进而影响可用性。这里实现上可以通过配置中心发起主动失效请求的方式来加速配置更新的过程。配置中心收到此种配置项的配置变更请求之后先向所有agent发起失效请求(可以有重试),如果全部成功则立即更新配置,否则等待[max_deadline - ε]之后再更新。另外还可以有一个小优化,那就是配置中心在下放lease的时候不是针对每一个生成一个,而是一批一个,这样的话在这么短的时间周期内服务器的晶体振荡器的时钟漂移也不可能导致配置失效点那么不一致进而出现更新配置过程中的最后单点问题。

4. 蓝绿+lease配置项

我们可以通过蓝绿部署和lease配置项的结合,简化和完善蓝绿的更新过程。比如说一半服务是一个开关配置项,另一半是另一个,需要下掉某一半的时候,通过配置中心关闭其开关flag即可。这样他们一定会在某一个deadline时间之后全部处于offline状态。进而基于这种机制进行更细腻的下掉的一半的配置更新、binary更新等动作。

不确定结果的确定

分布式系统的话,stale的请求是再所难免的,总可能会因为网络、服务hang或者bug等原因导致的stale的请求出现。分布式的情况下一般请求的响应有两种可能,一种是可预期的错误(服务里定义的确定性错误),一种是不可预期的错误(服务里定义的未知错误、发起端判断请求超时或者网络错误等)。一般对于分布式存储来说,一旦出现不可预期错误(known error)的时候,就需要等待,等到某一个时间点,系统状态稳定了,也就是stale请求结束了,等到这么一个一致的时间点之后,再继续操作,就能保证线性一致性了。不过这里就有一个问题了,怎么能确定一个一致的时间点?这里有几个可行的方法:

  1. 通过一个中心服务管理器跟踪和管理请求,整个请求的生命周期都被严格跟踪。每一个请求都需要通过管理器来决议执行。显而易见,这个实现非常麻烦。
  2. 通过给每一个请求赋予一个可比较的id,保存每一条数据的最后因果请求id,且保证只能更大的id才可以应用,小的id不可以应用,通过这种机制来消除stale请求的影响。比如系统的设计是幂等的(复杂状态机可以通过事务就可以保证,简单状态机比如kv本身就是幂等的),当出现unknown error时,用户只需要重试请求即可,一旦成功了,就说明得到了一个确定性结果了,因为用户串行的情况下后发起的重试请求的id一定比之前得到unknown error请求id大,一旦成功之前的stale请求必定失败。而假设之前的请求成功了,由于本请求是幂等的,也不会影响状态的一致性。
  3. 通过CAS机制,每一个modify动作都是一个RMW(Read-Modify-Write)过程,先拿到一个cookie,之后带着cookie去write。这样如果确定地修改一次一个key,就能确定之前stale的请求再也不能成功了,因为其cookie已经失效了。

一个基于lease配置项的KV状态机迁移设计

前提假设

DB-A为mysql集群,DB-B为TiKV集群。假设当前数据都存储在DB-A中,现在要迁移到DB-B中。

设计实现

假设不考虑双写验证和容错(比如复写MQ防止目标存储不可靠)。
设计思想就是在某一时刻,把所有业务流量都切到DB-B中,DB-A变为只读。这时候所有的旧数据都在DB-A中,新数据在DB-B中。所有的数据获取都需要在DB-A和DB-B之间做归并,DB-B的所有删除请求都必须使用标记删除。读取数据的时候先看DB-B中是否有记录(包括删除),如果没有则访问DB-A,如果有则直接返回。后面的话再通过分布式锁或者CAS等机制把DB-A的数据导入到DB-B中。

映射到上面的架构图,整个过程都是数据层特别是meta agent在参与,业务层完全无感知。那么问题就是怎么能让meta agent在某一时刻路由信息的配置项一致?答案就是通过带lease的配置项。当然也可以通过上述的蓝绿方式,只不过会复杂、麻烦一些。如果对于一致性要求特别高,还需要结合不确定结果的确定这种方式来保证线性一致性。

转载请附链接:https://blog.csdn.net/maxlovezyy/article/details/100199461

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值