花钱的年华

江南白衣,公众号:春天的旁边

服务化之-负载均衡与路由的设计

| Filed under 技术

从这章开始,各个服务化框架之间要贴身肉搏的比较了。

以Java体系的,有被真实地大量使用的,能接触到源码的这三条作标准,选择了这些学习对象: Dubbo与Dubbox兄弟,新浪微博的Motan,美团点评的Pigeon,还有SpringCloud/Netflix家的一应物件。嗯,当然还有唯品会自家出品的OSP。

推家的Finagle,谷家的GRPC,还有Netty作者写的Armeria更偏重于RPC框架,这章先不出场。

1. 服务注册与发现

时代在进步,服务注册与发现简单讲讲就行。

1.1 几桩好处

与Apache,Nginx,HAProxy,LVS这些传统的Load Balancer对比,服务的自注册自发现+智能客户端有几桩好处:

一、绕过了中央的代理节点,少了一层网络节点。

二、避免了中央的代理节点自身的处理速度和网卡带宽的瓶颈,以及LB自身的高可用。

传统的LB节点,需要KeepAlived之类的方案来做一个主备。然后前面再放一个DNS轮询之类的方案来做扩容。考虑到DNS的TTL设置,一旦主从LB一起失效,还要等待比如10分钟的过期时间,或紧急绑定客户机的/etc/host。

三、往日的LB,更改所代理的集群实例还要再重启一把。不过现在好像不需要了,比如Docker里的Bamboo方案,就可以动态注册Docker容器到HAProxy中。这桩好处便大大削弱了,只剩这注册可以不假于外物。

四、智能客户端上可以做更多的服务治理逻辑,比往Nginx上挂Lua脚本强。

 

1.2 大致功能

框架启动时,应用等完成初始预热了,才注册迎客。
框架退出时,先把自己从注册中心下线,稍等所有客户端都收到这个通知,再完成手头剩下的请求了,才优雅的转身退出。

还应提供API,一来是运维摘流量的一种方式,二来应用程序自己,可以随时将某个实例临时下线,比如应用做一些缓存刷新的工作时,便不希望接客。

另外一件要紧事情,注册中心虽然也会与服务端进行定时心跳。但这毕竟走的是注册中心与服务端之间的特殊通道,而不像传统LB的Health Check URL那样直接与服务端口进行交互,所以两者效用不可等同,还得有别的健康检查措施。

注册中心的DashBoard?觉得可以融合在服务治理中心里,单独的DashBoard意义不大。

 

1.3 种种实现

服务注册中心可以有zk,etcd,consul,我自己只用过ZK,其他没什么发言权,但和配置中心一样,etcd看着不错,有些框架可以同时支持其中的二三者。

Netflix家也有个Eureka,目前的版本基于RESTFul的API, 所以推送能力比前几家弱,靠着默认定时30秒的刷新,Server间要数据同步,Client与Server要数据同步,文档里说最多两分钟才保证通知到所有客户端,与ZK们秒级的推送速度相差甚远。对于脑裂的情况,Eureka的态度是宁愿保留坏数据,不要丢失好数据,见仁见智。

 

2. 负载均衡

2.1 算法一览

1.Round Robbin(轮询)

最古老的算法最可信的算法,缺点是有状态,必须在并发之下记住上一次到谁了。

2.Random(随机)

最简单的,无状态的算法。

3. Least Load(最小负载)

可以实现某种程度上的智能调节。具体的实现包括最少在途请求数(发出去了还没收到应答的请求),最少并发连接数,最小平均响应时间等等。

4. Hash (参数哈希)

根据参数进行哈希,一来有分区的效果(参见单元化架构,可改善数据库连接,缓存热度等),二来可以Stick Session(可本机进行防刷运算等)。

 

2.2 衡量标准

2.2.1 权重支持

传统LB都有权重设置, 机器(Docker)硬件配置的不一致需要权重,压测引流需要权重。

灰度发布也需要权重。假如这个应用只有3台服务器,灰度发布1台,万一有错,那就是33%的失败率了。如果信心不足,可以增加一个灰度批次,先把第一台的权重降到原来的1%。

还有,机器刚启动时,需要一点预热的时间(如Class Loading),如果一开始就把一堆请求压给它,可能会有批量的超时,所以最好刚启动时只放一些流量过去,等运行一段时间才递增到正常流量(Dubbo是10分钟,Pigeon大约30分钟)

另外调权重为0,也是一种摘流量的方式。

唯独Netflix家的Ribbon,完全不支持权重。

 

2.2.2 性能,静态与动态实例列表

每家的实现算法,对性能都有影响,尤其是如何支持权重。 像Ribbon这种完全不支持权重的就首先退场了。

传统的LB,实例列表是静态的,所以一致性哈希环,按权重分配的Random环,RoundRobbin环之类的都可以预先生成。

而像我们家,需要经过动态的路由规则,最后才知道可选的实例列表,那什么什么环做起来就不容易了。

后来看了Motan的实现,一言惊醒梦中人,原来只要使劲,也可以根据当前机器的IP,预先进行机房路由的计算,预先得到每个可选目标分组的环,然后再来侦听实例的变化(注册变化,路由变化,熔断变化)。

 

2.2.3 可预测性

随便设计的LB算法可能会引起莫名的意外,比如RoundRobbin 和 最少在途请求数,如何与权重结合一定要谨慎。我们之前就设计过看起来可以,但跑起来压力都到了一台机上的算法。

 

2.3 实现的比较

2.3.1 轮询

Dubbo首先检查权重是否相等,如果相等就退化为无权重设置。其实日常99%的时段可能都是无权重的,区分快慢路径,为99%的情况作优化,是设计算法时的重要思路。

如果有权重,则每次生成个环,比对上一次的状态,在每个节点上调用完自己的权重数后,再往下一个节点走。

这里就有个问题,如果客户把三台服务器的权重分别设为100,10000,100, 本来只想表达个1:100:1的权重关系,结果。。。。连续一万个请求都压在第二台机上。

Pigeon的算法做了改进,会把权重求一个公约数(不过每个请求算一次也是累),真正达到1:100:1的效果。

我们家OSP嘛,与Dubbo有着同样的问题,这打击还没恢复过来,所以暂时主打Random,不支持RoundRobbin了。

要重新支持的话,会融合Motan的静态列表与Pigeon的公约数,直接生成一个静态环,每个实例按公约后的权重数,在环上插入若干个点,然后不断遍历这个环就行。

 

2.3.2 随机

这里有块测试项目新鲜程度的试金石:有没有用无锁的JDK8 ThrealLocalRandom 或者其移植类。

结果好像只有我们家OSP用了。

Dubbo同样先判断一下权重是否一致。如果不一致,则要遍历所有节点得到总权重,然后以总权重为上限生成一个随机数,然后再遍历每个节点,击鼓传花的减去其权重,直到数字小于0,那就是它了。

Pigeon和OSP的算法,和Dubbo一模一样。

Motan则像前面设计的完美版RoundRobbin一样,预先生成一个环,每个实例按公约后的权重数来插入若干个点,然后随机选一个点即可。OSP以后也要见贤思齐。

 

2.3.3 最小负载

最小负载的表达,最直接还是最少在途请求数(如果是同步请求,退化为最小并发数)。最小平均响应时间什么的,受不同方法不同的参数什么的影响太多。

最小负载最好就不要扯上权重了,因为说不清。Dubbo的做法,如果有在途请求数相等的节点,则再来按权重随机那套,但好像复杂了。

Motan做得有意思,考虑到服务实例的列表可能有一百几十台,全部比较一次谁的在途请求数最少太浪费时间。所以它会在列表里随机取其中一段,10个实例之间比较一下即可。

 

2.3.4 参数哈希

一致性哈希的基本算法:

1. 每个服务实例在一个以整数最大值为范围的环上,各占据错落有致不连续的若干个点。每个实例的点的数量可以固定如160,也可以按着权重来。

2. 然后随机一个整数,看它落在环上靠近的点是属于哪个实例的,就选择哪个实例。

与哈希取模相比,如果一台服务器挂了,只会让本来属于这台服务器的数据去到其他服务器,而原本属于其他服务器的数据则不会变。而哈希取模,模7 和 模8,几乎所有数据的落位都变了。

如果服务实例有一百个,每次生成一万六千个点的环,成本还是蛮大的。所以Dubbo采用了预生成的方式,只要实例列表的SystemIdentityHashCode不变,就继续用下去。

刀刀说Dubbo有个Bug,用可变的FullUrl 而不是固定的Host来生成点,会起不到一致性哈希的效果,另外用md5而不是新潮的murmurhash,也是历史感的体现。

Motan的类名虽然叫一致性哈希,但实现上好像不是.......

OSP因为成本关系,暂时是直接取模的,日后也要学习Motan静态化服务列表,按权重构造一致性哈希环。

最后,拿什么来哈希呢?Dubbo和Motan哈希了全部请求参数(Dubbo也可以配置只哈希第1-N个参数),考虑到这样哈希成本非常不可控,及Proxy(支持多语言所用,以后再谈)要避免做反序列化,OSP采用了让客户端自己往Header中放分区键的方式。

Ribbon和Pigeon不支持Hash算法。

 

3. 路由规则定制

路由规则的定制,可以演化出无数的用法,如分流与隔离(读写分离,将非重要调用者分流到小集群,为重要调用者保留机器等等),AB测试,黑白名单等等。

经常听到的各种热词,都能靠路由规则支持。能用来做什么,只受限于配置员的想象力。

 

3.1 Dubbo的DSL

Dubbo的DSL语言:

比如:为重要调用者保留机器

application != kylin => host != 172.22.3.95,172.22.3.96

比如读写分离:

method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96

条件包括applicaiton,method, host, port
判断包括 ==,与 !=,
值可以是 ","分隔的列表,"*"代表的后缀匹配。

还可以用Java支持的脚本语言如JRuby,Groovy来自定义规则。

 

3.2 Motan的DSL

Motan的DSL简单很多,基本只支持两端的服务名作条件,再配上两端的IP地址,也能玩出不少花样了。

服务名的定义:

#匹配com.weibo下以User开头的不包括UserMapping的所有服务,或以Status开头的所有服务
(com.weibo.User* & !com.weibo.UserMapping) | com.weibo.Status*
以 to 分隔的两端IP:
* to 10.75.1.*
10.75.2.* to 10.73.1.*
* to !10.75.1.1

 

3.3 唯品会OSP的JSON化界面定义

我们的框架,原本也专门用Scala来解析的DSL,得益于Scala的强大(在祥威武),只用很少语句就很简洁实现了。

但后来我们老板觉得,还是需要一种更规范化,结构化,容易扩展的表达方式,所以选择了JSON(其实就是把前面DSL语法解析后的Java对象JSON化表达出来),并且独立出“目标集群”的概念。

3.3.1 整体语法

条件集是多个条件的“与”的组合。
按顺序匹配每个条件集,中了就去到它的目标服务集群。
如果都没符合的,则走到“默认集群”去。

规则集 与 目标集群 是多对多的关系。 一个规则集可以对应多个有顺序的目标集群,用于满足HA的需求,如果集群A中的所有服务实例均不可用,则按顺序访问集群B中的实例。

目标服务集群,可以一个个IP定义,可以子网掩码或范围定义。注意Docker化之后,IP都是动态的,要么用范围定义,要么用Label。权重什么的也在里面顺便标明了。

 

3.3.2 条件语法

参数列表:

  • 客户端的应用名称
  • 客户端的IP地址
  • 服务的版本号,方法名
  • 由客户端主动放入到Header中的上下文属性(绕过了复杂的参数提取,改为客户端自己放入),从这里玩出万千花样。

条件判断则包括了!=, ==, 正则匹配,逗号分隔的列表等,还有针对某些特定类型的,如针对IP的子网掩码匹配。

 

3.3.3 运维友好的配置界面

用go.js做的,贴一张过时的图出来:

 

 

4. 机房路由

4.1 距离优先路由

虽然有爱因斯坦相对论的加持,从广州跑到北京的机房,几十毫秒的延时还是有的,所以,最好完全按距离远近来决定路由。

优先调用同机房的服务。但如果本机房的该服务的所有实例都挂了,或者服务只部署在其他机房,就调用离得最近的那个机房。

做起来其实也简单,一般按服务器IP的前两位,就能区分出机房来。然后做个配置,把每个机房之间的距离粗略量化出来。至于根据网络状态的动态选路,暂时不需要这么高大上的东西。

好像只有美团点评的Pigeon和我们自己家的OSP实现了。

 

4.2 其他方式

另外几家,虽然没有优先路由,但机房仍然是个跨不过的概念。

Motan的实现,通过前面提到的DSL语法,还能手工做流量容灾切换。

Dubbo的实现里,注册中心是按机房部署的,每个服务可以注册到一个或多个注册中心。

Netflix的Robbin,则有一种SameAvaiableZoneRule的负载均衡方式。不过它把机房路由也当成一种负载均衡,因此选了它的话,就硬编码了使用Random做均衡。

 

5. 小结

最后,还应该有一种静态配置文件的方式(类似Motan的DSL),在开发联调时,绕过上面所有所有的路由和负载均衡,直达目标测试服务器。

一通比较下来,感觉自家的OSP虽然还有种种待改进的地方,但整体完成度还可以。

Netflix家的东西,基本上没什么亮点,处处敬陪末席。

还有,开源真好,太多让人眼前一亮的代码。

在博客上,这个扫可乐钱的二维码总是存在的。

 

最后,欢迎微信公众号jnby1978。

版权声明:本文版权归 江南白衣@唯品会 所有。任何形式的转载,必须获得作者授权。

转载请保留原文链接: http://calvin1978.blogcn.com/articles/routing.html

有关的...

发表评论

您的电子邮箱不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>