• VIPServer:阿里智能地址映射及环境管理系统详解


    http://geek.csdn.net/news/detail/110586

    作者: 
    周遥,阿里技术专家,花名玄胤,毕业于四川大学。六年大型分布式与中间件系统经验,三项国家专利,参加过多次“双十一”。2013年从零开始带出VIPServer,目前已成为集团环境管理与路由的标准。 
    王建伟,阿里巴巴工程师,花名正己,西北工业大学计算机学院硕士毕业。目前在阿里中间件技术部软负载小组负责VIPServer系统。 
    本文为《程序员》原创文章,未经允许不得转载,更多精彩文章请订阅2016年《程序员》

    VIPServer是阿里内部使用最广的服务地址映射及环境管理系统。本文主要阐述VIPServer的项目背景、设计目的、架构演变及内部详细实现。

    背景

    寻址,意味着什么?当系统比较简单时,模块都集中在同一台服务器,调用都在内部——大家同住一个屋檐下,直接调用其接口便行。但在大型软件架构中,分布式占据了极其重要位置,不同的系统被分配到了不同的服务器上。首先相互发现便成了问题,因此业界诞生了许多配置服务器,类似的有阿里的ConfigServer或外界的ZooKeeper(使用其配置同步功能)。

    但生产环境的地址映射并不是一个简单的反向代理,还需要考虑许多环境及路由策略问题。例如,阿里内部将开发环境分为了日常、预发与线上三套环境,不同环境之间的服务需要做到隔离(如图 1所示),即日常的终端不能拿到其它环境(如预发)的服务地址。与此同时,线上的服务始终是一个动态的服务,可能因为各种原因进行调整,如压测需要引流,灰度发布需要流程隔离等。

    我们设计VIPServer,初衷仅仅是为了替换硬件负载均衡设备如F5(基于硬件的网络负载均衡设备,早期售价高达上千美元)或者阿里内部的LVS,主要原因如下:

    1. 无论LVS还是F5都是代理的形式,必然存在网络瓶颈,对网络RT也有影响(需要中途转发一次)。
    2. LVS、F5都需要实体机器支撑,不利于快速部署,需要预算、采购、安装、调试等诸多流程。
    3. 它们都需要大量的资金来购买设备(虽然LVS相比F5成本已经小了很多)。
    4. LVS、F5的服务面仅在当前网络,在需要跨地域、跨区域服务挂载时会变得非常困难。
    5. LVS、F5是分散在各个应用中的,日常的管理也是由应用自己的系统工程维护的,不利于统一协调、管理。

    图片描述

    图1 不同环境下的服务不能错调

    开始时,用户并不认可VIPServer,因为当时LVS的管理流程与功能已经相当完备。仅出于成本或者减少网络延迟考虑并不能支撑一次底层迁移。不过,随着业务量发展,集团内的环境变得越来越复杂,单元化、隔离环境、准备环境层出不穷,上述LVS弊端便慢慢显现。后面我们为VIPServer加入了更多环境管理相关功能并逐渐改造架构——去掉所有(二方及三方)系统依赖与服务下沉成为基础中的基础产品,这使得VIPServer如今成为了环境认识、变更与维护的权威。

    架构

    初始架构

    最早我们的首要目的是去除LVS及F5这类网关类型的反向代理结点,使内部应用调用都是以直连的形式进行。在这种构想下,终端向一个服务发起请求有以下步骤:

    1. 终端依赖VIPServer客户端;
    2. 向VIPServer客户端提供服务标识;
    3. 客户端向服务发起查询并定期更新此标识对应的数据以保证服务地址的状态正确;
    4. 客户端根据标识策略性地返回一个康健的地址给终端,这里康健与否由VIPServer服务端检测;
    5. 终端根据地址直接发起服务调用,完成整个请求。

    上述过程涉及四个模块:客户端、服务地址管理(添加、删除、存储)、服务状态检测以及服务地址返回策略。

    客户端

    客户端本不知道服务端地址,因此向服务端的请求本身也是个服务发现过程,存在“先有蛋还是先有鸡”的问题。为了解决这个循环依赖,我们引入了一个称为“地址服务器”的模块,其本质就是将一个静态包含VIPServer服务端IP地址列表的文件放至于一个Web服务上(我们使用的是Nginx),再申请一个DNS域名,用于发现此Web服务器地址,这样客户端便能得到VIPServer服务端的地址列表。我们不能简单使用DNS,因此VIPServer本身也需要区分各种环境,在Web服务上,我们会根据请求客户端的IP地址列表来返回对应环境的服务端地址列表。

    服务端

    服务端是管理服务地址与状态的地方。首先VIPServer本身也是集群应用,因此数据如何在集群内同步并保持一致性便是个很大的问题。我们选择了内部的Diamond(阿里的持久配置中心,采用RESTful接口,支持订阅、通知与按标识聚合数据,在集团内部已广泛使用)作为VIPServer的“NOSQL数据库”,原因是:

    1. 地址数据并不是经常变动且查询条件简单,适合NOSQL数据库。
    2. Diamond能够向集群服务进行同步数据并提供“最终一致性”保证。
    3. 服务与服务地址是个聚合与被聚合关系,Diamond本身提供这个功能,免去关联查询的操作。
    4. Diamond本身支持非结构化数据。

    相比之下,服务地址的状态则会变化相当频繁,比如系统发布、机器故障、A/B测试等等都会造成服务状态改变而且这种数据是具有时效性的,因此我们没有存储与同步地址状态数据,而是让服务端进行实时检测。在1.0架构中,状态数据如果通过Diamond进行同步则会给其造成很大的压力,外加上前期我们挂载的地址数量不多,因此我们选择让每台服务器都进行全量检测,如图2所示。

    图片描述

    图2 早期采用全量检测的方式

    演进架构

    初始架构虽然确实能实现最基本的需求,但随着挂载应用的增加,全量检测便引出一个非常重要的难题:无法横向扩容来提高服务机器挂载数量。另外我们在推进客户端接入时,也发现用户不愿意以通过修改代码的方式来接入,因为以前LVS通过提供一个VIP(Virtual IP Address,类似网关IP,终端通过调用这个IP地址,LVS就会把流量均匀地分配到后端挂载的机器上),使用方只要像调用普通机器一样调用LVS一样就可以,至于流量的转发、目标机的故障情况都不用关心。所以在中期,我们重点做了两件事:分量检测与DNS-F客户端研发。

    分量检测

    如果每台服务器都进行全量检测,确实是一个简单易行的方式,在这种情况服务器之间不需要同步状态数据,当一台机器挂掉后也不需要进行迁移,因为每台机器都是对等的。不过,随着挂载机器的增多,如果一台机器已经没有能力检测所有挂载机器,那么所有其它服务器也会遇到同样的结果,而这样的性能瓶颈是不能通过扩充机器解决的。

    我们通过将挂载机器的检测任务进行切分来解决这个问题。简单来说就是将n个检测任务平分到m台机器上,每台机器负责n/m个任务。还必须考虑到以下要素:

    1. 分配的任务尽量平均分配。
    2. 当一台机器宕机时,检测任务能平滑再分配到其它机器。
    3. VIPServer服务器的扩缩容都能自动感知并重新进行检测任务分配。
    4. 在已有架构上进行最小变更。

    我们通过将标识列表按服务器数量取模以散列至所有服务器上,同时每台服务器定期向Diamond指定标识(Diamond称为DataID)发送自己的IP地址与当前时间截,这个DataID被我们配置成聚合数据,也就是说每台服务器发送的IP地址与时间截都会被聚合成一个列表,服务端通过这个列表中的时间截与当前时间的时间差来判断其它服务器是否存活。然后将存活的IP地址按自然顺序排序便能得到自己在列表中的位子,假设为p。那么如果在所有域名集合Ω={D1,D2,D3……Dn} 中,若某域名D∈Ω对应的序列为i,即Di。若 i mod m = p,则此域名因由本机负责检测,若不是则由其它机器检测,这台机器不用关注。

    代码1 分量检测算法逻辑

    set m=VIPServer机器数量
    set n=sizeof(所有标识集合Ω) 
    set list=sort(接收到的存活的机器列表)
    set p=list.indexof(当前机器地址)
    
    for i=0 till i>=n do
    if i mod m = p then
    checkDomain(Ω.get(i));
    else 
    // do nothing
    end
    end

    由于每台服务器定期更新自己的时间截,那么当有新机器加入时列表就会更新;而有机器宕机时,时间差就会大于预设值。通过以上方法,我们便实现了对检测域名的动态分量检测,如果检测达到瓶颈,我们只需要简单的加机器就能解决问题。

    最后,每台机器的检测结果我们仍使用Diamond来同步到其它机器。

    DNS-F

    前面提过用户希望以最小的成本从原有的LVS上迁移至VIPServer,而LVS采用的是VIP方式。我们还发现VIP并不是直接使用,而是通过传统的DNS进行映射的。因此我们考虑这个DNS是不是能返回我们提供的地址,这样一来,DNS解析过程就相当于VIPServer客户端的地址请求过程。因此我们设计了DNS-F,即DNS Filter来拦截用户的DNS请求,当发现请求的域名存在于VIPServer系统中时,便优先返回其中的地址数据。这个拦截过程是通过向“/etc/resolv.conf”文件注入一个本地DNS地址127.0.0.1并设置其为首先DNS,如代码2所示。

    代码2 DNS配置文件内容示例

    search tbsite.net aliyun.com
    options attempts:1 timeout:1
    nameserver 127.0.0.1
    nameserver 10.195.29.17
    nameserver 10.195.29.33

    这样设计有诸多巧妙之处:首先如果VIPServer出现故障,我们可以优雅地容灾到原有的LVS上,因为DNS解析在超时设置的timeout还没有收到返回消息时就会自动重试下一个DNS服务器,也就是说会走到原来的逻辑;其实用户不需要改变原来的使用逻辑,我们透明地将VIP替换成了真实的IP地地址。不过这样的设计也存在一些问题:首先是用户需要运行一个单独的进程提供本地DNS服务(即我们的DNS-F程序);其次对“/etc/resolv.conf”会影响到所有进程,这个问题后期我们会考虑将DNS-F做成Linux内核模块,只对特定的进程与域名起作用。事实证明DNS-F是个极成功的构想,现在其安装量为VIPServer第二大客户端。

    图片描述

    图3 DNS-F工作原理示意图

    类似的原理,Google的Kubernetes至少半年后才出现。

    高阶架构

    VIPServer发展到后期,我们已经面临10万级上的机器挂载量,并且分布在世界各个机房。之前的设计构架并没有考虑到跨地域跨国家这种问题,检测虽然分布但都是集中式的。一些检测由于距离太远而出现了检查不准的问题,另一方面,断网演练的时候如果断的是VIPServer所处的机房,那么所有机房的服务健康检测都会失败,即使此次断网并未影响到它们。

    区域化检测

    我们引入了区域化的概念,即每个区域都有一个VIPServer集群专门负责检测,同时也会尽力检测其它区域的部分域名,之所以还需要检测其它区域的是因为某些特定场景下存在跨区域调用,同时还要求客户端优先连接本区域的VIPServer集群,这样一来,客户端得到的总是最准确检测数据,因为访问与检测链路是相同,如图4所示。

    图片描述

    图4 区域化检测模型

    在区域下模型下,挂载机的状态在每个区域是独立的,也就是说如果存在A、B、C三个区域,其中A与B断网,那么A对B中挂载机的检测结果为故障,B由于并未与C断网,因此结果需要为正常。这情况下,检测结果的同步也需要区域化,因此原来使用Diamond来进行全局同步的便不再适合了。由于检测状态只需要在区域内部同步,鉴于其量小、延迟小的特点,我们使用了“Gossip一致性协议”(Gossip的同步原理就像“八卦新闻”,每个人都将自己获得的八卦传递给周围其它人以最终获得同步,优点是简单易懂,缺点则是收敛时间不能控制,当然目前已经存在诸多优化变种)来进行同步。Gossip是一种轻量及最终一致性同步协议,最大的优点在于实现算法简单,每个结点只需要周期性地向其它结点广播自己的数据就可以了,顺序以时间截为准,虽然不是很精准但我们对顺序的要求并不高。试想一下,如果一台机器收到了错误的状态,由于检测是一直在进行,同时检测机也在不停的向外发送正确状态,因此即便是某次状态错了,接下来也会逐渐纠正过来。

    去依赖

    随着环境与区域的增加,VIPServer的集群部署变得越来越频繁,很多区域都是独立或者隔离的,并没有我们需要的依赖,因此如果我们希望VIPServer向最基础的“环境管理及路由”方向发展,我们不能依赖应用,因为我们是环境搭建第一要素。去Diamond是我们首先要做的,因为不少环境,如“私有云”并没有它。之前我们已经将检测结果同步从其中分享出来并使用Gossip来解决,这里我们还需要将挂载机器的配置信息也独立出来。

    这里我们使用的是“Raft一致性协议”(Raft的诞生就是为了解决Paxos过于复杂且难以实现的难题,这里有个很好的说明动画:http://thesecretlivesofdata.com/raft/)并针对VIPServer的场景做了裁剪。我们之所有不使用Gossip是因为其无法保证顺序操作,由于机器的挂载与下线都是一次性的,没有机会修正。在Raft协议中,所有操作必须在Master上进行,变更均由Master同步至其它服务器,就样就能保证顺序,然后我们将同步的数据都持久化到磁盘上,这样的好处在于每台机器都有全量的数据,具有很高的容灾能力。

    下沉

    后期由于环境的大量增加,造成调用关系越来越复杂:“同机房”、“同网段”、“同城”、“冷备隔离”、“小流量隔离”等等层出不穷。鉴于此我们提出VIPServer下沉,承担更多类似SDN的责任。为了支持更多网络层的路由,我们开放了环境标识导入接口,以标识每个挂载的机器的各种属性——如所在机房、城市、网络、使用类型等等——以确定其在网络的中角色与位置。 如此一来,用户想要的任何路由规则只要对应的标识是存在的,我们都可以计算出来。例如我们想“同机房”调用,每次在返回服务地址列表时只需要将调用者的机房信息与服务提供方的机房信息进行简单的对比即可。如此一来,整个网的调用链就变得相当灵活。例如“冷备环境”,平时我只需要返回标签为正常环境的机器列表,只有当正常环境的健康机器比下降到一定程度(如20%)时,才返回“冷备环境”的机器列表;又例如做“灰度发布”,只需要简单调整权重,便可以只把少量流量分配的新版本的服务器上。

    数据结构&存储

    VIPServer维护的就的就是服务地址映射关系,因此基础数据就是每个地址的信息,这里包括:IP、端口,权重以及若干机器环境相关信息(如机房名、所在城市等)。我们将每个地址的信息以非结构化数据的方式存储,原因是服务的附加属性是复杂多变的:随着环境的增加,地址配置、标签会越来越多。如“初始架构”一节所述,前期我们使用Diamond的聚合数据功能来存储地址与服务信息,后期我们使用直接存磁盘的方式,因此每个聚合维度便变成了一个文件,即一个文件就是一个服务,里面的每一行就是一个地址信息。

    这样设计有诸多好处,首先写入时不会影响到其它服务目录;然后因为以文件的形式存在,备份是一件相当容易的事,只需要复制整个目录即可;最后排查问题也方便,如果想检视服务数据,只需要简单地将文件打印出来即可。

    图片描述

    图5 VIPServer数据存储结构

    每台服务器都存全量数据,它们之间的数据同步通过Raft进行,构成完整的存储体系。这样做的好处在于数据不依赖于任何一台服务器,只要有一台数据还在,整个VIPServer体系的数据就在,因此具有很高的容灾特性。

    实现细节

    权重计算

    权重计算经历两个阶段的发展,整数阶段和浮点数阶段。在整数阶段,标识中的服务地址权重是整型的,其计算方式是在列表中按权重展开,这样一来权重大的便有较多的机会被选中,例如有两个地址为“A1、A2”,如果A1的权重为1,A2的权重为2,展开后的列表便为“A1、A2、A2”,然后最终再随机选择一个地址,这样A2被选中的概率就高些,当然这是个很简单的实现。到了后期,其不灵活的问题就越来越明显了,例如如果我想把一个地址的流量切换成总流量的0.1%,按原来的方式,得将其它地址的权重都设置成1000才行,先不说要如何才能更改这么多地址的权重,关键的问题在于展开的地址扩大了多少倍,如果有10个地址,那么调整后展开的大小即为:9*1000 +1=9001,扩大了近100倍,如果列表中有100个地址,那显然内存会溢出。所以后期我们设计了“浮点权重”,其计算算法为:

    1. 对所有地址(ip)的权重求和,即: 
      图片描述
    2. 那么每个地址的权重就把sum划分成了一个一的区间Di。
    3. 在[0,sum]间随机取浮点值,f = random(0, sum)。
    4. 查找满足条件的地址m,使得m ∈Di即可。

    这种算法最大的优点在于如果我们想把一个地址的流量切为原来的10%,只需要将其权重变成10%即可。

    图片描述

    图6 基于数列分布的权重原理示意图

    容灾手段

    路由信息在调用链中是至关重要的角色,如果获取不到则会直接导致调用失败,容灾工作首要的目标就是保证用户在最差的情况下都有路由信息可用。

    为此,我们在服务端与客户端都放置了容灾逻辑。服务端方面,有以下措施:

    1. 每台机器都据有全量数据,当一台机器宕机时客户端可以随时切换到另一台。
    2. 每台服务器都需要定期向其它服务器发送心跳,以确保其仍然正常。
    3. 当其中一台心跳失效时,按“清单 1 分量检测算法逻辑”对检测任务进行重新分配。
    4. 增设各类阈值进行保护,如正常服务器比例下降到一定程度时停止健康检测(因为此时每台服务器分担的检测任务比正常情况下大太多),又如当标识对应的机器列表中正常机器小于配置的比例(如0.3)时便返回所有服务地址。
    5. 使用异步Servlet将所有 API接口异步化并配置隔离请求队列,这样当一个API慢时不会影响到其它。
    6. Raft协议会在Master失去响应时重新进行选举,保证可以随时进行机器挂载及其它操作。
    7. 增设各类开关,可以随时关闭非核心功能,进行降级保护(如机器列表同步)。
    8. 客户端方面则有以下措施:
    9. 每次更新地址后都需要向磁盘写入缓存,在不能连接或者更新时使用。
    10. 客户端的更新线程与API处理线程隔离,做到不能因为任何情况而阻塞业务线程。
    11. 客户端每次更新以轮询的方式向服务端请求更新数据,这样做不但有利于服务端的负载平衡,还保证客户端不会受部分服务端宕机影响。
    12. 如果客户端收到空数据,则拒绝更新,这个我们称为“推空保护”。

    未来工作

    由于VIPServer毕竟不同于传统网关类似的负载均衡设备,因此我们认为其重点不在单个应用的负载均衡。未来我们将投入更多精力在网络调用治理上,形成了VIPServer为基础的SDN平台。现代大型企业应用中,整套生产环境是非常复杂的,它包含了众多细分环境与调用关系,所以在部署一个新环境时,首要头痛的问题便是环境的搭建。如果整个环境都运行在以SDN为基础的网络上,那么最终的形态将是所有的环境都浮在云端,不与任何物理设备挂钩,可以随意将一个“机房”移动另一个区域,所有的环境变更操作都可以瞬间执行完成,这对产品的运维的帮助是巨大的 ,也是云上环境最需要。

    参考资料

      1. In Search of an Understandable Consensus Algorithmhttps://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf
      2. Linux Virtual Server https://github.com/alibaba/LVS
      3. Paxos Made Simple http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf
      4. Gossip Algorithm http://www.inf.u-szeged.hu/~jelasity/cikkek/gossip11.pdf
  • 相关阅读:
    ORM执行原生sql, Python脚本调用Django环境, ORM事务, sql模式说明
    ORM多表更新删除 查询
    ORM多表操作
    Java BigDecimal类型的数据运算方法
    js获取表格中的数据转化为json字符串
    在threamleaf中使用循环遍历输出list集合
    sql中使用cast转化数据格式(整数或者小数)
    mybatis的xml中使用模糊搜索查询
    k8s挂载ceph
    kubernetes HPA
  • 原文地址:https://www.cnblogs.com/linkenpark/p/8637111.html
Copyright © 2020-2023  润新知