分类: Mysql/postgreSQL
2017-01-29 20:42:47
从这章开始,各个服务化框架之间要贴身肉搏的比较了。
以Java体系的,有被真实地大量使用的,能接触到源码的这三条作标准,选择了这些学习对象: Dubbo与Dubbox兄弟,新浪微博的Motan,美团点评的Pigeon,还有SpringCloud/Netflix家的一应物件。嗯,当然还有唯品会自家出品的OSP。
推家的Finagle,谷家的GRPC,还有Netty作者写的Armeria更偏重于RPC框架,这章先不出场。
时代在进步,服务注册与发现简单讲讲就行。
与Apache,Nginx,HAProxy,LVS这些传统的Load Balancer对比,服务的自注册自发现+智能客户端有几桩好处:
一、绕过了中央的代理节点,少了一层网络节点。
二、避免了中央的代理节点自身的处理速度和网卡带宽的瓶颈,以及LB自身的高可用。
传统的LB节点,需要KeepAlived之类的方案来做一个主备。然后前面再放一个DNS轮询之类的方案来做扩容。考虑到DNS的TTL设置,一旦主从LB一起失效,还要等待比如10分钟的过期时间,或紧急绑定客户机的/etc/host。
三、往日的LB,更改所代理的集群实例还要再重启一把。不过现在好像不需要了,比如Docker里的Bamboo方案,就可以动态注册Docker容器到HAProxy中。这桩好处便大大削弱了,只剩这注册可以不假于外物。
四、智能客户端上可以做更多的服务治理逻辑,比往Nginx上挂Lua脚本强。
框架启动时,应用等完成初始预热了,才注册迎客。
框架退出时,先把自己从注册中心下线,稍等所有客户端都收到这个通知,再完成手头剩下的请求了,才优雅的转身退出。
还应提供API,一来是运维摘流量的一种方式,二来应用程序自己,可以随时将某个实例临时下线,比如应用做一些缓存刷新的工作时,便不希望接客。
另外一件要紧事情,注册中心虽然也会与服务端进行定时心跳。但这毕竟走的是注册中心与服务端之间的特殊通道,而不像传统LB的Health Check URL那样直接与服务端口进行交互,所以两者效用不可等同,还得有别的健康检查措施。
注册中心的DashBoard?觉得可以融合在服务治理中心里,单独的DashBoard意义不大。
服务注册中心可以有zk,etcd,consul,我自己只用过ZK,其他没什么发言权,但和配置中心一样,etcd看着不错,有些框架可以同时支持其中的二三者。
Netflix家也有个Eureka,目前的版本基于RESTFul的API, 所以推送能力比前几家弱,靠着默认定时30秒的刷新,Server间要数据同步,Client与Server要数据同步,文档里说最多两分钟才保证通知到所有客户端,与ZK们秒级的推送速度相差甚远。对于脑裂的情况,Eureka的态度是宁愿保留坏数据,不要丢失好数据,见仁见智。
1.Round Robbin(轮询)
最古老的算法最可信的算法,缺点是有状态,必须在并发之下记住上一次到谁了。
2.Random(随机)
最简单的,无状态的算法。
3. Least Load(最小负载)
可以实现某种程度上的智能调节。具体的实现包括最少在途请求数(发出去了还没收到应答的请求),最少并发连接数,最小平均响应时间等等。
4. Hash (参数哈希)
根据参数进行哈希,一来有分区的效果(参见单元化架构,可改善数据库连接,缓存热度等),二来可以Stick Session(可本机进行防刷运算等)。
传统LB都有权重设置, 机器(Docker)硬件配置的不一致需要权重,压测引流需要权重。
灰度发布也需要权重。假如这个应用只有3台服务器,灰度发布1台,万一有错,那就是33%的失败率了。如果信心不足,可以增加一个灰度批次,先把第一台的权重降到原来的1%。
还有,机器刚启动时,需要一点预热的时间(如Class Loading),如果一开始就把一堆请求压给它,可能会有批量的超时,所以最好刚启动时只放一些流量过去,等运行一段时间才递增到正常流量(Dubbo是10分钟,Pigeon大约30分钟)
另外调权重为0,也是一种摘流量的方式。
唯独Netflix家的Ribbon,完全不支持权重。
每家的实现算法,对性能都有影响,尤其是如何支持权重。 像Ribbon这种完全不支持权重的就首先退场了。
传统的LB,实例列表是静态的,所以一致性哈希环,按权重分配的Random环,RoundRobbin环之类的都可以预先生成。
而像我们家,需要经过动态的路由规则,最后才知道可选的实例列表,那什么什么环做起来就不容易了。
后来看了Motan的实现,一言惊醒梦中人,原来只要使劲,也可以根据当前机器的IP,预先进行机房路由的计算,预先得到每个可选目标分组的环,然后再来侦听实例的变化(注册变化,路由变化,熔断变化)。
随便设计的LB算法可能会引起莫名的意外,比如RoundRobbin 和 最少在途请求数,如何与权重结合一定要谨慎。我们之前就设计过看起来可以,但跑起来压力都到了一台机上的算法。
Dubbo首先检查权重是否相等,如果相等就退化为无权重设置。其实日常99%的时段可能都是无权重的,区分快慢路径,为99%的情况作优化,是设计算法时的重要思路。
如果有权重,则每次生成个环,比对上一次的状态,在每个节点上调用完自己的权重数后,再往下一个节点走。
这里就有个问题,如果客户把三台服务器的权重分别设为100,10000,100, 本来只想表达个1:100:1的权重关系,结果。。。。连续一万个请求都压在第二台机上。
Pigeon的算法做了改进,会把权重求一个公约数(不过每个请求算一次也是累),真正达到1:100:1的效果。
我们家OSP嘛,与Dubbo有着同样的问题,这打击还没恢复过来,所以暂时主打Random,不支持RoundRobbin了。
要重新支持的话,会融合Motan的静态列表与Pigeon的公约数,直接生成一个静态环,每个实例按公约后的权重数,在环上插入若干个点,然后不断遍历这个环就行。
这里有块测试项目新鲜程度的试金石:有没有用无锁的JDK8 ThrealLocalRandom 或者其移植类。
结果好像只有我们家OSP用了。
Dubbo同样先判断一下权重是否一致。如果不一致,则要遍历所有节点得到总权重,然后以总权重为上限生成一个随机数,然后再遍历每个节点,击鼓传花的减去其权重,直到数字小于0,那就是它了。
Pigeon和OSP的算法,和Dubbo一模一样。
Motan则像前面设计的完美版RoundRobbin一样,预先生成一个环,每个实例按公约后的权重数来插入若干个点,然后随机选一个点即可。OSP以后也要见贤思齐。
最小负载的表达,最直接还是最少在途请求数(如果是同步请求,退化为最小并发数)。最小平均响应时间什么的,受不同方法不同的参数什么的影响太多。
最小负载最好就不要扯上权重了,因为说不清。Dubbo的做法,如果有在途请求数相等的节点,则再来按权重随机那套,但好像复杂了。
Motan做得有意思,考虑到服务实例的列表可能有一百几十台,全部比较一次谁的在途请求数最少太浪费时间。所以它会在列表里随机取其中一段,10个实例之间比较一下即可。
一致性哈希的基本算法:
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算法。
路由规则的定制,可以演化出无数的用法,如分流与隔离(读写分离,将非重要调用者分流到小集群,为重要调用者保留机器等等),AB测试,黑白名单等等。
经常听到的各种热词,都能靠路由规则支持。能用来做什么,只受限于配置员的想象力。
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来自定义规则。
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
我们的框架,原本也专门用Scala来解析的DSL,得益于Scala的强大(在祥威武),只用很少语句就很简洁实现了。
但后来我们老板觉得,还是需要一种更规范化,结构化,容易扩展的表达方式,所以选择了JSON(其实就是把前面DSL语法解析后的Java对象JSON化表达出来),并且独立出“目标集群”的概念。
条件集是多个条件的“与”的组合。
按顺序匹配每个条件集,中了就去到它的目标服务集群。
如果都没符合的,则走到“默认集群”去。
规则集 与 目标集群 是多对多的关系。 一个规则集可以对应多个有顺序的目标集群,用于满足HA的需求,如果集群A中的所有服务实例均不可用,则按顺序访问集群B中的实例。
目标服务集群,可以一个个IP定义,可以子网掩码或范围定义。注意Docker化之后,IP都是动态的,要么用范围定义,要么用Label。权重什么的也在里面顺便标明了。