分布式系统架构特别是进入微服务架构后,服务治理的重要性愈发变得不可缺少而且处于重要地位。缺乏服务治理的的分布式系统架构,很难正式投入生产。那么服务治理包括哪些方面呢?主要包括服务发现,负载均衡,限流,熔断,超时,重试,服务跟踪等。下面展开讲。
侵入式服务治理
1.服务发现
服务发现是指使用一个注册中心来记录分布式系统中全部服务的信息,以便让其他服务能够快速找到这些已注册的服务。服务发现需要服务注册,服务查找,服务健康检查及服务变更通知等关键功能。
最早的服务发现应用就是DNS,通过使用域名,让访问者不必关心服务在哪里。其实DNS的服务发现应用,在原来公司做大型嵌入式软件时,虽然是单体系统,但是是一个分时的多进程系统,由于系统非常庞大,不同的进程间通信采用的是IPC进程间通信。为了便于不同的进程间通信,我们当时设计在IPC模块内部提供了一个小型的服务发现功能,所有需要通过IPC通信的系统,在模块启动时都需要调用IPC提供的服务注册功能,这样当模块间通信时,就不用关注这个IPC通信是设备的板间通信还是板内通信。这里简单解释
下,因为设备形态存在控制板(主要负责协议计算和路由)和接口板(主要负责数据报文转发,可能多个接口板),因此当我们在考虑IPC时提供了类似的设计。
可以看到关于服务发现,关键是要有一个服务注册中心。具体的服务发现机制
2.注册中心与服务提供者无法保持心跳时需要剔除服务
3.服务消费者可以从注册中心获取服务提供者的最新信息,可以采取定期拉和事件通知的两种方式
可以看到服务注册中心在分布式服务架构中的关键位置,因此需要具备高可用。基于CAP定理的,服务注册中心的最佳选择是一个AP的系统。但实际中我们常用的注册中心有zooKeeper,Eureka,etcd,consul,还有阿里的Nacos。
zooKeeper我们知道它本质其实是一个CP模型,基于其自己的Zab协议
(基于Paxos裁剪)来保证注册中心多个节点之间的数据一致性。
Zab协议规定,消息传递要遵循可靠传递,完全有序,因果有序的特性.
在zooKeeper中,主节点故障下,Zab协议通过主节点快速选举,初始化,同步从节点、广播几个阶段来保证数据一致性和主节点选举的高效。
集群角色:主节点,从节点,观察者节点。主节点负责数据写入服务,从节点提供读数据。观察者同样提供读但是不参与选举投票。在集群中,服务器数量是奇数被认为是一个最佳实践。集群对外工作的必要条件是超过半数的服务器可以对外正常工作。
ZooKeeper's Hierarchical Namespace
另外有个集群宕机容忍度的衡量,2台服务器的容忍度为0,3台,4台容忍度都是1
会话:在zooKeeper,客户端和服务端之间是有TCP长连接的。基于长连接发送心跳。会话超时时间需要
根据生产环境的网络状况合理设定。
数据节点:数据节点称zNode,通过/分割父节点和子节点。zNode分为持久节点和临时节点 服务注册就是注册在临时节点上。
PERSISTENT
PERSISTENT_SEQUENTIAL(持久序列/test0000000019 )
EPHEMERAL
EPHEMERAL_SEQUENTIAL
监听:即watcher,客户端可以在感兴趣的zNode上进行监听,当zNode状态变化时,服务端会通知客户端。
ZooKeeper's Hierarchical Namespace
zooKpeer 原生提供了命令行客户端及Java,c的客户端。原生客户端开发效率低,且存在一些不足(不支持递归,需要重复注册监听器,需要自行解决会话超时,对常用的分布式锁,选举,分布式计数等都需要二次开发)。
zkClient和Curator是两个常见的三方库。
zkClient提供递归删除创建节点,会话超时处理,监听器反复等,简化了原生API的使用。
Curator是Netflix开源的,已经被Apache基金会收录。除了支持zkClient的功能,还支持选举,分布式锁等场景
zooKeeper是最广为广泛的分布式协调组件,由于存在当主节点因为网络与其他节点失去联系导致整个系统重新选举时,集群是不可用的(这也是它作为CP模型,不能应用于大规模分布式系统注册中心的考虑。),因此已经不是服务发现的最好选择了,其主要优势在选举和分布式锁。Curator提供的缓存能力,能够让zooKeeper可用性增强,同时由于缓存使得其从CP发生向AP的转化
-
Eureka采用了去中心化的设计,整个集群由对等节点组成,不存在zk(zooKpeer以下简称zk)的选举问题。服务端之间相互注册彼此同步信息来实现高可用性。客户端可以连接任一服务端进行注册。但这样会存在数据的一致性问题,也就是客户端查询的信息不一定是最新的。
Eureka也是Netflix的开源项目,可以和SpringCloud很好的整合。Eureka是一个 war包,需要部署到一个web服务器中。另外Eureka提供一个保护机制,就是当超过85%的节点在15分钟内没有心跳时,就会启动,此时
1.不再从注册列表删除长时间没有心跳的服务
2.不会接受新的户注册及同步数据
3.网络恢复稳定后再接受新注册及同步信息
Eureka提供了java,Python, Node.js, .net的客户端,对于没有客户端的语言可以通过Restful API交互。
-
Etcd
Etcd和zk具有类似的架构,采用 Raft算法代替了 zab,从CAP来分析,也是一个CP系统,通过TTL来实现类似zk临时节点的功能
-
Consul
Consul是一个商业产品(Raft),有一个开源版本。除了服务发现,还通过内存,磁盘使用的细粒度状态检测及服务配置的键值存储功能。
-
Nacos
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务.它同时支持AP和CP模式,可以通过配置切换。
2.负载均衡
负载均衡是实现系统高可用,网络流量疏导和扩容的重要手段。通过合理的算法把请求分摊到后端的多个服务节点,关键在于均匀分发请求。
小规模的系统可以采用DNS来做,通过为同一主机名配置多个IP地址,DNS应答时通过轮询的方式返回不同的主机IP。
四层负载均衡 通过IP+Port的方式决定流量的负载分担。以TCP为例,通过修改报文中目标IP地址的方式,进行分发到不同的后端。客户端是和真实的服务器间建立连接,此时负载均衡器只是作为路由器进行报文转发。
四层负载均衡的性能强于七层(需要解析报文应用层内容[参考OSI七层网络模型]),常用的产品有F5和LVS。
七层负载均衡通过解析报文中,进行请求转发。还以TCP来说,此时负载均衡器会分别与服务器和客户端建立连接,因为只有这样负载均衡器才能解析应用层。常用的产品是Nginx.
服务端负载均衡优势,对于业务本身没有侵入性
,对代码本身无影响,应用只需要保证无状态性。
劣势就是 负载均衡会成为瓶颈,一般会部署多个节点
客户端负载均衡
服务端负载均衡针对服务器列表不经常变化的场景是可以的,但是分布式架构特别是微服务架构,服务动态伸缩,上下线会经常变化,此时就需要客户端做动态负载均衡了。如图
客户端负载均衡是在客户端内部,由客户端通过动态获取服务列表后,按照负载均衡算法进行的负载分担。这样实现了动态的负载均衡,同时也给代码带了较大的侵入性。目前常见的实现方案就是NetFlix的Ribbon,他有5个核心接口
-
-
-
-
-
ServerListFilter
这五个接口的实现类课题通过属性文件注入,可以自定义自己的负载策略
Ribbo还提供了连接超时和重试的能力,当Ribbon和 Eureka一起应用时,Ribbon会通过Eureka获取服务列表。一个典型的Ribbon应用架构如下
合理使用负载均衡,是提高系统性能和高可用性的有效手段,需要结合具体的场景进行选择搭配。
3.限流
限流 Traffic Shaping 又谓之流量整形,主要是针对突发流量的整形,实现网络流量的平滑,防止后端服务被突然的流量洪峰冲垮。限流是一旦流量达到阀值,就启动限流处理,主要有下面三种处理
2.排队等待,主要是秒杀,抢购的场景,针对稀缺资源。
1.计时器限流算法。使用计数器统计一算时间内的请求数量来限流。适用于服务器端资源请求的限流,不适合用来控制用户请求的限流。
2.漏桶算法 Leaky Bucket 使用一个固定容量的桶,桶底部有一个洞,请求可以任意速率流入,但是只能固定速率处理,一旦超速桶就会溢出,新的请求会被丢弃。
3.令牌桶算法 和漏桶算法不同的是,使用有一个有固定容量令牌的桶,按照固定速率往桶内添加令牌,请求处理会先取令牌,没有申请到令牌则进入排队或直接拒绝。可以看出令牌桶,允许处理瞬间的大量突然请求。
限流方案主要分为下面几种,实际架构方案中,需要综合几种来共同处理,来实现更佳的限流效果
通过限制客户端发出请求来限制的。为避免单个客户端对服务的过度使用,可以在客户端进行限流,可以很好的控制全网的流量。但是由于客户端处理,应用会比较复杂,虽然可以通过统一的控制中心或配置中心做一些控制,但是算法升级会很麻烦。
这里可以采用Google开源的Guava类库中的RateLimiter来实现。它提供一个基于令牌桶的限流方案。
Ratelimiter.cretae(速率) 0.5 表示每2秒放入一个令牌
Ratelimiter.cretae(速率,预热时间,时间单位) 通过预热时间,调节令牌的生成速率,实现令牌数量的自然平稳增长。
Ratelimiter.acquire();请求令牌
客户端限流可以控制单个客户端,但是不了解服务端全部状态,一个服务端可能同时服务多个客户端,因此还需要服务端限流。
服务端一般通过框架或者中间件来提供服务,如Tomcat,Dubbo等都提供了过载限流能力。
比如Tomcat的server.xml 的<Connector>项可以配置限流参数,MySQL,MongoDB,Redis等有类似能力
最后NetFlix提供的zuul组件也可以提供丰富的限流机制。
如图APP的接入端是流量的入口,一般会接负载均衡服务器。我们知道四层负载和七层负载的区别,所以四层的限流主要针对紧急情况的限流,比如后端服务会被峰值流量冲跨。通常更多是在七层负载均衡做限流,这里主要是使用Nginx,它提供了连接数限流和请求限流两个模块。
Ngx_http_limit_conn_module
Ngx_http_limit_req_module
这里的Ngx_http_limit_req_module采用的是基于漏桶算法进行的限流,具体配置
http{
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
Server{
location /{ limit_req zone=one burst=5 nodally
}
}
}
虽然Nginx内置了限流的标准化
策略,但是无法满足业务应用的各种个性化需求,针对多个nginx的分布式限流策略,推荐可以使用openResty,它通过lua脚本可以非常方便让我们进行定制化策略开发。
另外openResty本身是一个基于nginx+lua的高性能web平台,并内部集成了
大量扩展性强的web应用,服务和动态网关。
最后说说限流的维度和粒度。维度就是说限流的基准或者参考是什么,常用的有访问者IP,请求URL,用户令牌,用户组,设备信息等。这里要说下基于IP地址虽然简单有效,但是当网络中有nginx或者公司使用同一个IP出口防伪外网,此时就不灵了。这个有具体的方案,不知道的话,可以百度下。粒度就是限流的对象是集群还是服务,还是接口。集群粒度一般作为兜底手段,接口级别太小。所以面向服务的粒度最常见。当然对于接口,可以通过添加分组,然后基于分组做限流也是一种选择。
限流用于保证服务质量,限流的具体方案很多,一套健全的限流方案需要在各个部分都要考虑,综合上面三种,基本可以搭建出很难被流量压垮的系统。
4.熔断
熔断circuit breaker 类似股票市场的熔断机制或者电路的过载保险熔断,达到保护后端服务的有效手段,属于流量调控的范畴。熔断是完全禁止客户端访问。熔断的目的是为了防止单点超时阻塞,进而形成雪崩的有效方法。关于熔断的实现熔断器有三种状态
1.Closed(关闭) 熔断未开启,就是正常状态
2.Open(开启) 熔断开启,应用请求会立即返回错误或者失败,但是不会一直熔断,而是设置超时阀值,一旦到达阀值
,会进入半开启状态
3.Half-Open(半开启) 此时允许少量请求通过,如果请求符合预期,则会修复熔断,进入关闭;否则再次进入开启并重新计算超时。半开启模式能够有效探测服务状态,做到保护。
这块最知名的就是Netflix开源的Hystrix 它提供了熔断,隔离(分为默认进程隔离和信号量隔离),失效转移和监控功能等。
国内有阿里的Sentinel哨兵方案。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
-
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
-
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。