写作背景
在这之前,已经有了单体工程CRUD、分布式SOA、微服务SpringCloud前两个版本的使用经验。
参考资料如下:
SpringCloud官方文档:http://cloud.spring.io/spring-cloud-static/Edgware.RELEASE/single/spring-cloud.html
以下内容大量使用了what-why-how的写法,从篇幅上来讲前期作为带入环境,从文字中可以直接感受到非用微服务不可以及相见恨晚的感觉,然后会慢慢介绍SpringCloud是如何实现微服务以及为什么这么实现......
待续...
带入环境
假设你现在已经会使用ssm框架在mysql或者redis中实现增删改查,并且你也会使用tomcat进行项目部署,也用过git或者svn进行代码版本管理。你正接受了一份项目需求文档,刷刷的设计好了数据库和表,建了一个spring web环境,你美滋滋的敲着crud的代码,敲完之后就提交了,然后通知测试人员进行测试,测试人员测试通过之后会通知运维人员进行项目部署,也许是tomcat,也许是nginx+tomcat并实现动静分离,第一个里程碑版本就这样上线了,需求文档的文档的确定需要时间,数据库设计需要时间,使用java开发后台crud需要时间,测试需要时间,部署需要时间,如果是单纯的使用tomcat实现部署,网站并发量只有300-500,使用nginx反向代理服务器实现部署也只有3w-5w并发量,这是前期的并发量,单系统单工程,到中期了,用户数据开始增长,mysql撑不住了,你眼珠一转、灵机一动,马上使用开源组件mycat搭建了mysql集群并实现了读写分离,使用keepalive实现了集群双活,数据层查询时间瞬间就上去了,再过了一段时间之后,你开发的系统还真的厉害,很多用户使用,比如说,比如说,就说首页模块、订单模块,本来是好事的,但是个别模块实在是太成功了,导致整个系统处于“卡顿”状态,现在连用户登录和注册都已经要等个好几分钟才有响应,这个时候能优化的地方都已经优化了,但是整个系统的性能还是被那个别的模块给卡住,连重启都已经完全没有效果,因为产生卡顿的原因是个别模块占用资源过大,这种现象在任何系统都是存在的,是无法解决的,但是你灵机一动,我把那些卡顿的模块给单独出去,或者将资源消耗不大的模块给单独出来,于是“分布式‘诞生了,优点很明显,分布式系统中的任意两个模块不会产生非常直接的性能影响,除非是部署到一台服务器中,当你以为解决这样的性能问题之后就是成功的时候,其实灾难才刚刚开始,用户访问的时候会有多个模块在多台服务器上运行,这些模块之间的交互如何定义,像java的restTemplate一样直接调用其他系统?如果直接这样实现,访问的调用网络间的资源需要明确的url,而url包括通信协议、通信ip、通信端口、映射路径、调用参数[get]等等,而且可能会需要指定post参数和header等等,哪怕是一次简单的调用都是那么的麻烦,调用返回的数据如何解析、header信息如何判定,这些在之前单体应用的时候是不需要考虑的问题,之前是调用其他service层的时候返回一个对象数据即可,而这里会有很多问题,url中的ip如何获取,被依赖方改变之后如何做到能够简单修改,写到配置文件中?其实还有一个问题,那些特别出色的模块要是单独出来之后还是过了一段时间之后还是自身性能出现瓶颈了怎么办呢?一个是再次切分,但是总有一个极限,但是那个极限下还是出现性能瓶颈了呢?你突然间想起,web工程不就是处理数据么?一个系统处理数据100份,那么两个系统是否就能处理数据200份呢?对头,确实可以,按照这个思路,你将这个系统直接再次部署了一份,部署完之后,你大呼“终于解决了”,但是呢?其他模块的调用者就惨了,因为你这是多个ip,而且ip的数量还随着你性能的瓶颈率攀升,调用方就惨了,需要不断的去维护这些ip列表,而且有一点,如何去合理的将所有的调用机会去适配到这些具体的ip背后的系统中去呢?轮着来?确实是可以,现在还有一个问题,假设这个列表中的一台服务器挂掉了,在单体系统中,如果是宕机了,运维估计得加班加点的进行重启,问题是还有别的模块在调用这个模块呢?那别的模块就几乎等于是已经处于不可用状态了,如果是调用关系很密切,整个系统都会因为这个模块的宕机导致全部无法正常响应,也就是说一旦出现一个哪怕很小的问题都有可能导致整个系统都是不可用的,而且这样很可能会出现一个很严重的问题,以前是维护一台服务器,现在是要维护一个集群的服务器,运维成本上升,可用率明显降低,当然你也可以用redis做个可用情况记录,比如说用url作为key,当不可用时将value的值进行+1,当这个value达到一定数量值的时候换其他ip,当发现现在已经没有可用ip的时候发送一条调用短信服务接口发送一条短信给运维组,让运维组诊断集群,对该模块进行水平扩容,这样也确实可以.其实分布式的做法跟这个差不多,通常分布式工程会用一个开源组件Zookeeper作为服务注册和发现,上面说的ip列表是当模块启动时主动定时向zk注册的、此时如果是第一次注册则是创建、以后每次都是修改,当其他模块需要该模块的时候会请求下载并保存到本地,当zk一定时间收不到某ip的注册信号时就会将该ip删掉、则为修改,相当于一个简单的一张表的crud,这张表维持的是ip列表和模块名,当然其实并不是用到数据库的表实现的.zk作为组件是需要部署的,部署者通常都是运维,而且上面的如何获取和注册可用ip列表是通过RPC框架dubbo实现的,dubbo也需要部署,dubbo是阿里云开源的,在分布式系统上使用是非常简单的,通过配置文件和接口即可调用远程接口,当调用失败时会重试三次,关于上面说的轮着调用,专业术语为负载均衡,负载均衡有硬负载均衡和软负载均衡,这里属于是软负载。这个时候整个系统的瓶颈就直接出现在ZK注册中心上,当然负载均衡策略也是很重要的。其实分布式还可以通过消息队列进行实现,当消息队列起到了中间人的作用,始终会造成不必要的性能浪费,在使用RocketMQ或者Kafka的时候,RocketMQ可靠性比Kafka高、效率不如Kafka高,在传统项目中用RocketMQ更多,而Kafka在大数据领域作为中间件的情况更多。但是这个java工程中,如果是其他语言呢?比如说nodejs、go语言、c#、c++,dubbo只能说sorry,因为暂时还不支持多语言,当然这也不是关键,问题是:dubbo只是做到了服务注册发现、zk只做到了分布式服务治理,这些只是分布式的一部分而已,当拆分到足够细粒度的时候,外部访问的url如何确定呢?单独写一个工程对内部所有的url进行转发?其实也是可以的,明明是一个系统的东西,当拆分为多个系统之后的配置文件如何作为统一呢?这是分布式尚未解决的,还有一个问题是调用可用ip列表的具体ip时出错了怎么办呢?dubbo是默认重试三次,这是非常恐怖的,比如说一旦转账操作出错之后又重新转了3次,分布式的数据库还是在一起的,只是将处理数据部分单独抽取出来,性能还是难以提升,当然你也可以将某些表给单独出来,当然你可以试着看后期这样的改动是否麻烦。
我相信你现在已经体会到分布式的甜头了,也相信你已经知道了分布式实施的不容易;其实上面的分布式只是分布式的一部分,也可以叫做微服务的一部分;如果你肯定了上面的实现思路并开始实施,你还需要为这样的架构集成很多东西,甚至需要一套非常规范的大局的框架来集中实现它,上面的只是做到了服务注册与发现、服务治理,你还需要:1、分布式的/版本化的配置 2、路由,网关 3、不同语言间的服务对服务调用 4、负载均衡 5、断路器 6、全局锁 7、领导选举和集群状态 8、分布式消息等等,你大可继续使用dubbo并集成很多很多的其他工程,可是你要知道用很多不同公司或者组织的框架时会遇到各种冲突问题,现在我们推荐使用springCloud,springCloud是Spring推出的适用于微服务的框架,其中集成了面对分布式环境下的微服务框架的很多解决方案,现在已经有19个之多,以后还会更多,如果你觉得购买昂贵的商用服务器也没什么?购买f5这种昂贵的硬负载设备也觉得没什么?大可暂时抛弃微服务,不过一旦系统大到一定程度之后还是需要分布式那一套的,逃不掉的,你要想想淘宝、京东这种公司。
进入主题
引入:构建分布式系统不需要变得很复杂且容易出错。Spring Cloud为最常见的分布式系统模式提供了简单易用的编程模型,帮助开发人员构建灵活,可靠和协调的应用程序。Spring Cloud建立在Spring Boot的基础之上,使开发人员能够快速入门并且快速生产。
SpringCloud官方架构图:
名词翻译:
Iot:物联网
百度百科: 新一代信息技术的重要组成部分,也是“信息化”时代的重要发展阶段。其英文名称是:“Internet of things(IoT)”。顾名思义,物联网就是物物相连的互联网。这有两层意思:其一,物联网的核心和基础仍然是互联网,是在互联网基础上的延伸和扩展的网络;其二,其用户端延伸和扩展到了任何物品与物品之间,进行信息交换和通信,也就是物物相息。物联网通过智能感知、识别技术与普适计算等通信感知技术,广泛应用于网络的融合中,也因此被称为继计算机、互联网之后世界信息产业发展的第三次浪潮。物联网是互联网的应用拓展,与其说物联网是网络,不如说物联网是业务和应用。因此,应用创新是物联网发展的核心,以用户体验为核心的创新2.0是物联网发展的灵魂。
物联网是物理世界和虚拟世界的桥梁,通过物理世界采集数据,虚拟世界加工数据,形成信息,产生决策,做出反馈。将各种信息传感设备与互联网结合起来,进行信息交换和通讯,以实现智能化识别、定位、跟踪、监控和管理的一种网络。物联网从本质上可以看作是互联网的延伸。我们平时所说的互联网主要是通过网络来实现不同硬件如计算机和服务器的数据连接和处理。物联网的终端则更加多样化,它使得“世界上所有的物体都可以通过网络主动进行信息交换,实现任何时刻、任何地点、任何物体之间的互联、无所不在的网络和无所不在的计算。
mobile:移动APP
智能手机的第三方应用程序,也就是第三方应用软件。
随着移动互联网的兴起,越来越多的互联网企业、电商平台将APP作为销售的主战场之一。数据表明,APP既给手机电商带来的流量远远超过了传统互联网(PC端)的流量,通过APP进行盈利也是各大电商平台的发展方向。事实表明,各大电商平台向移动APP的倾斜也是十分明显的,原因不仅仅是每天增加的流量,更重要的是由于手机移动终端的便捷,为企业积累了更多的用户,更有一些用户体验不错的APP使得用户的忠诚度、活跃度都得到了很大程度的提升,从而为企业的创收和未来的发展起到了关键性的作用。
browser:浏览器
可以显示网页服务器或者文件系统的HTML文件(标准通用标记语言的一个应用)内容,并让用户与这些文件交互的一种软件。可以显示网页服务器或者文件系统的HTML文件(标准通用标记语言的一个应用)内容,并让用户与这些文件交互的一种软件。
好处:1、导入流量,增加用户的粘性,和品牌的认知度。 对于网站来讲,流量非常重要。2、抢占用户桌面,用浏览器来代替桌面,不管是pc还是android ios等。3、随着云计算的发展,将来的web应用将慢慢的取代本地应用,便于部署自己的产品和服务。4、浏览器本身就可以作为一种平台,粘住开发人员。
API Gateway:API网关
API网关是一个服务器,是系统的唯一入口,跨一个或多个内部API提供单个统一的API,入口点URL地址则不应当变化,即前端用户始终请求一个固定地址,实际调用是将URL路由到后端相应的业务服务器,对微服务进行编排。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、控流、监控、负载均衡、缓存、请求分片与管理、静态响应处理。API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务。
API网关负责服务请求路由、组合及协议转换。客户端的所有请求都首先经过API网关,然后由它将请求路由到合适的微服务。API网管经常会通过调用多个微服务并合并结果来处理一个请求。它可以在Web协议(如HTTP与WebSocket)与内部使用的非Web友好协议之间转换。API网关还能为每个客户端提供一个定制的API。通常,它会向移动客户端暴露一个粗粒度的API。例如,考虑下产品详情的场景。API网关可以提供一个端点(/productdetails?productid=xxx),使移动客户端可以通过一个请求获取所有的产品详情。API网关通过调用各个服务(产品信息、推荐、评论等等)并合并结果来处理请求。
breaker dashboard:断路器仪表板
由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。
在微服务架构中,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
service registry:服务注册
注册中心为消费者下的多个提供者实现弹性扩缩容,尽管提供者的数量和分布在动态变化,对于消费者来说都是无感知的。
进行远程调用时需要知道一个服务实例的网络地址(IP地址和端口)。这些运行在物理硬件上的服务实例的网络地址是相对静态的,实现中可以从一个很少更新的配置文件中读取网络地址。服务实例的网络地址是动态分配的。而且,由于自动扩展,失败和更新,服务实例的配置也经常变化。这样一来,客户端代码需要一套更精细的服务发现机制。有两种主要的服务发现模式:客户端服务发现(client-side discovery)和服务器端服务发现(server-side discovery)。
客户端服务发现:使用客户端服务发现的时候,客户端负责决定可用的服务实例的网络地址,以及围绕他们的负载均衡。客户端向服务注册表(service registry)发送一个请求,服务注册表是一个可用服务实例的数据库。客户端使用一个负载均衡算法,去选择一个可用的服务实例,来响应这个请求。一个服务实例第一次启动时会向注册注册表写入网络ip并在以后每次启动时会再次刷新网络ip,如果该服务实例已经正常停下会从注册表中删除属于该实例的网络ip,如果是非正常情况下实例销毁,注册中心会有检测机制确保实例已经消亡或者实例会使用心跳机制定时向注册中心声明当前实例仍在存活期而不会被误删。
服务器端服务发现:客户端通过负载均衡器向一个服务发送请求,这个负载均衡器会查询服务注册表,并将请求路由到可用的服务实例上。通过客户端的服务发现,服务实例在服务注册表上被注册和注销。ELB(Elastic Load Blancer)就是一个服务器端服务发现路由器。一个ELB通常被用来均衡来自互联网的外部流量,也可以用ELB去均衡流向VPC(Virtual Private Cloud)的流量。一个客户端通过ELB发送请求(HTTP或TCP)时,使用的是DNS,ELB会均衡这些注册的EC2实例或ECS(EC2 Container Service)容器的流量。没有另外的服务注册表,EC2实例和ECS容器也只会在ELB上注册。 好处在于服务发现的细节被从客户端中抽象出来,客户端只需要向负载均衡器发送请求,不需要为服务客户端使用的每一种语言和框架,实现服务发现逻辑;另外,这种模式也有一些问题,除非这个负载均衡器是由部署环境提供的,又是另一个高需要启动和管理的可用的系统组件。
config dashboard:配置仪表板
在单体应用中起初所有配置跟源代码一起放在代码仓库中;之后出于安全性的考虑,将配置文件从代码仓库中分离出来,或者放在CI服务器上通过打包脚本打入应用包中,或者直接放到运行应用的服务器的特定目录下,剩下的非文件形式的关键配置则存入数据库中。而在微服务架构下,虽然也可以这样做,但是面对数量十分庞大的子服务(数量*扩容倍数),这样实现起来会有障碍,特别是在修改配置文件的时候保证配置一致时会有延时性,对系统造成毁灭性的意料之外的损失。为了保证配置文件的实时性和以及复杂情况下的可用性我们通常会使用配置中心——统一管理各种应用配置的基础服务组件。
配置包括哪些以及如何分类:1、按配置的来源划分,主要有源代码(俗称hard-code),文件,数据库和远程调用。2、按配置的适用环境划分,可分为开发环境,测试环境,预发布环境,生产环境等。3、按配置的集成阶段划分,可分为编译时,打包时和运行时。编译时,最常见的有两种,一是源代码级的配置,二是把配置文件和源代码一起提交到代码仓库中。打包时,即在应用打包阶段通过某种方式将配置(一般是文件形式)打入最终的应用包中。运行时,是指应用启动前并不知道具体的配置,而是在启动时,先从本地或者远程获取配置,然后再正常启动。4、按配置的加载方式划分,可分为单次加载型配置和动态加载型配置。
为了管理上面这些多样化的配置文件,通常我们需要做到以下几点:1、非开发环境下应用配置的保密性,避免将关键配置写入源代码2、不同部署环境下应用配置的隔离性,比如非生产环境的配置不能用于生产环境3、同一部署环境下的服务器应用配置的一致性,即所有服务器使用同一份配置4、分布式环境下应用配置的可管理性,即提供远程管理配置的能力。
具体可用实现的有Spring Cloud Config和disconfig.Spring Cloud Config像是为Spring量身定做的轻量级配置中心,巧妙的将应用运行环境映射为profile,应用版本映射为label。在服务端,基于特定的外部系统(Git、文件系统或者Vault)存储和管理应用配置;在客户端,利用强大的Spring配置系统,在运行时加载应用配置;而disconfig是国内开源框架,在服务端提供了完善的操作界面管理各种运行环境,应用和配置文件;在客户端,深度集成Spring,通过Spring AOP实现应用配置的自动加载和刷新。
distributed tracing:分布式追踪
对于希望监视复杂的微服务架构系统的组织,分布式追踪正在快速成为一种不可或缺的工具。
以前在单应用下的日志监控很简单,在微服务架构下却成为了一个大问题,如果无法跟踪业务流,无法定位问题,我们将耗费大量的时间来查找和定位问题,在复杂的微服务交互关系中,我们就会非常被动。另一方面很多技术会对实际生活产生直接影响,系统的可靠性至关重要,但这一切都离不开“可观测性”这一前提。传统的监视工具,例如度量值和分布式日志依然发挥着自己的作用,但这类工具往往无法提供跨越不同服务的能见度,分布式追踪应运而生。
分析网络请求在各个分布式系统之间的调用情况,从而得到处理请求的调用链上的入口URL、应用服务的调用关系,从而找到请求处理瓶颈,定位错误异常的根源位置。
业务流调用流程跟踪、可视化的监控界面、业务分析。
microservices:微服务
可以在“自己的程序”中运行,并通过“轻量级设备与HTTP型API进行沟通”。关键在于该服务可以在自己的程序中运行。通过这一点我们就可以将服务公开与微服务架构(在现有系统中分布一个API)区分开来。在服务公开中,许多服务都可以被内部独立进程所限制。如果其中任何一个服务需要增加某种功能,那么就必须缩小进程范围。在微服务架构中,只需要在特定的某种服务中增加所需功能,而不影响整体进程。
对外是系统,对内是服务,在抽象之后针对业务系统实现尽可能的抽取成细粒度的工程以达到业务复用和高扩展等等,由于是理论上的整体和实际上的分布式,所以在实现过程中如何保证去“网络传输弊病”和最大限度挖掘性能造就了微服务服务自治和集合。
马丁大神曾说,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
在传统的IT行业软件大多都是各种独立系统的堆砌,这些系统的问题总结来说就是扩展性差,可靠性不高,维护成本高。到后面引入了SOA服务化,但是,由于 SOA 早期均使用了总线模式,这种总线模式是与某种技术栈强绑定的,比如:J2EE。这导致很多企业的遗留系统很难对接,切换时间太长,成本太高,新系统稳定性的收敛也需要一些时间。最终 SOA 看起来很美,但却成为了企业级奢侈品,中小公司都望而生畏。
微服务与单体工程:
在以往,单体架构在规模比较小的情况下工作情况良好,但是随着系统规模的扩大,它暴露出来的问题也越来越多,主要有以下几点:1、复杂性逐渐变高:比如有的项目有几十万行代码,各个模块之间区别比较模糊,逻辑比较混乱,代码越多复杂性越高,越难解决遇到的问题。2、技术债务逐渐上升:公司的人员流动是再正常不过的事情,有的员工在离职之前,疏于代码质量的自我管束,导致留下来很多坑,由于单体项目代码量庞大的惊人,留下的坑很难被发觉,这就给新来的员工带来很大的烦恼,人员流动越大所留下的坑越多,也就是所谓的技术债务越来越多。3、部署速度逐渐变慢:单体架构模块非常多,代码量非常庞大,导致部署项目所花费的时间越来越多,曾经有的项目启动就要一二十分钟,非常漫长,启动几次项目一天的时间就过去了,留给开发者开发的时间就非常少了。4、阻碍技术创新:比如以前的某个项目使用struts2写的,由于各个模块之间有着千丝万缕的联系,代码量大,逻辑不够清楚,如果现在想用spring mvc来重构这个项目将是非常困难的,付出的成本将非常大,所以更多的时候公司不得不硬着头皮继续使用老的struts架构,这就阻碍了技术的创新。5、无法按需伸缩:比如说电影模块是CPU密集型的模块,而订单模块是IO密集型的模块,假如我们要提升订单模块的性能,比如加大内存、增加硬盘,但是由于所有的模块都在一个架构下,因此我们在扩展订单模块的性能时不得不考虑其它模块的因素,因为我们不能因为扩展某个模块的性能而损害其它模块的性能,从而无法按需进行伸缩。
而在现在,单体架构所有的模块全都耦合在一块,代码量大,维护困难,微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。单体架构所有的模块都共用一个数据库,存储方式比较单一,微服务每个模块都可以使用不同的存储方式(比如有的用redis,有的用mysql等),数据库也是单个模块对应自己的数据库。单体架构所有的模块开发所使用的技术一样,微服务每个模块都可以使用不同的开发技术,开发模式更灵活。
微服务与SOA区别: 微服务从本质意义上看,还是 SOA 架构。但内涵有所不同,微服务并不绑定某种特殊的技术,在一个微服务的系统中,可以有 Java 编写的服务,也可以有 Python编写的服务,他们是靠Restful架构风格统一成一个系统的。所以微服务本身与具体技术实现无关,扩展性强。
详细对比,如下表:
微服务架构模式 SOA架构模式
服务自治,服务间通过轻量级的通信机制通信,没有集中式的总线架构 企业级服务总线,统一规范,集中式的服务架构
服务单独部署,灰度发布和灰度升级,服务独立上线,无相互依赖和互影响 通常是单块系统架构,相互依赖,需要联动升级,部署复杂
基于微服务的应用集成相对简单(HTTP/REST/JSON) 基于企业级通用平台的集成,比较厚重(ESB/WS/SOAP)
服务的划分粒度细,6~8人负责一个微服务构建 服务由多个子系统构成,通常还共用组件,服务粒度划分粗
微服务的特点:
1、易于开发和维护,可以实现并行开发:由于微服务单个模块就相当于一个项目,开发这个模块我们就只需关心这个模块的逻辑即可,代码量和逻辑复杂度都会降低,从而易于开发和维护。
2、启动更快,部署更流畅:这是相对单个微服务来讲的,相比于启动单体架构的整个项目,启动某个模块的服务速度明显是要快很多的,在部署项目时出现的问题更加细节,而且可以并行解决,而如果是单体架构,异常会从上到下一层一层的剥开,在解决这些异常的时候需要逐步联系各个模块的开发人员,实现的是1+(1+1)+(1+1+1)+1的时间消耗,而微服务仅仅是1+1+1即可。
3、局部修改容易部署软件版本粒度更小更完善、回环周期更快:在开发中发现了一个问题,如果是单体架构的话,我们就需要重新发布并启动整个项目,非常耗时间,但是微服务则不同,哪个模块出现了bug我们只需要解决那个模块的bug就可以了,解决完bug之后,我们只需要重启这个模块的服务即可,部署相对简单,不必重启整个项目从而大大节约时间。在需求速度大于开发速度的时代,在过去常常是将版本的更新一拖再拖,因为工程已经足够大到发布一次就会花费巨大的时间抵不上一次小更新带来的好处,而在微服务中却不需要,因为整体不会受到影响,一个特别形象的比喻是修改一次数据库在单体工程中是读出来写进去覆盖掉实现升级,而微服务中可以针对想要修改什么字段就直接将该字段直接修改,其效率很明显就可以比较出来。
4、技术栈不受限:比如订单微服务和电影微服务原来都是用java写的,现在我们想把电影微服务改成开发速度更快的nodeJs技术或者并发效果更强的go语言,这是完全可以的,而且由于所关注的只是电影的逻辑而已,因此技术更换的成本也就会少很多。
5、按需伸缩:我们上面说了单体架构在想扩展某个模块的性能时不得不考虑到其它模块的性能会不会受影响,对于我们微服务来讲,完全不是问题,电影模块通过什么方式来提升性能不必考虑其它模块的情况。
在看到这么多的微服务的鲜明的特点之后,微服务的本质又是什么呢?
1、能够容载并适配一套基础的架构的系统,这种架构的系统使得子微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等;
2、微服务的目的是有效的拆分应用,实现敏捷开发和快速持续部署并应用;
3、微服务提倡的理念团队间应该是 inter-operate, not integrate(非集成的网络交互) 。inter-operate是定义好系统的边界和接口,在一个团队内全栈,让团队自治,原因就是因为如果团队按照这样的方式组建,将沟通的成本维持在系统内部,每个子系统就会更加内聚,彼此的依赖耦合能变弱,跨系统的沟通成本也就能降低。
实践过程中,什么样的系统适合于微服务呢?
1、能够利用网络与外界进行交互且模块分工明显2、内部数据共享要求不是很高3、业务量足够大
实践过程中,有哪些设计原则?
1、单一职责原则:每个微服务只需要实现自己的业务逻辑就可以了,比如订单管理模块,它只需要处理订单的业务逻辑就可以了,其它的不必考虑。
2、服务自治原则:每个微服务从开发、测试、运维等都是独立的,包括存储的数据库也都是独立的,自己就有一套完整的流程,我们完全可以把它当成一个项目来对待。不必依赖于其它模块。
3、轻量级通信原则:首先是通信的语言非常的轻量,第二,该通信方式需要是跨语言、跨平台的,之所以要跨平台、跨语言就是为了让每个微服务都有足够的独立性,可以不受技术的钳制。
4、接口明确原则:由于微服务之间可能存在着调用关系,为了尽量避免以后由于某个微服务的接口变化而导致其它微服务都做调整,在设计之初就要考虑到所有情况,让接口尽量做的更通用,更灵活,从而尽量避免其它模块也做调整。
任何事情都有缺点,微服务也不例外:
运维要求较高;开发、测试、运行所必须要的资源更多;接口调整成本高,一旦用户微服务的接口发生大的变动,那么所有依赖它的微服务都要做相应的调整,由于微服务可能非常多,那么调整接口所造成的成本将会明显提高;
微服务实践过程中的整体脉络图(仅供参考):
运行流程图:
message brokers:消息中间件
什么是消息?即一次对话所传递的内容。
什么是中间价?在网络环境下实现松耦合、高复用、互操作的软件;常指网络环境下处于操作系统、数据库等系统软件和应用软件之间的一种起连接作用的分布式软件,主要解决异构网络环境下分布式应用软件的互连与互操作问题,提供标准接口、协议,屏蔽实现细节,提高应用系统易移植性;主要为网络分布式计算环境提供通信服务、交换服务、语义互操作服务等系统之间的协同集成服务,解决系统之间的互连互通问题。形象地说就是所谓左右之间的中间。其特点有:平台化(能够独立运行并自主存在,为其所支撑的上层系统和应用提供运行所依赖的环境)、应用支撑(从模型、结构、互操作以及开发方法方面解决了上层应用系统的问题)、组件复用(使用明确职责的组件)、松耦合(在由网络连接—数据转换—业务逻辑组成的系统中,通过抽取对象将“连接逻辑”进行击碎分离、消息中间件将“连接逻辑”进行连接处理实现)、互操作性(不同标准的实现体的通用)。
在了解消息中间件之前需要明白服务之间是如何通信/调用?
因为所有的微服务都是独立的Java进程跑在独立的虚拟机上,所以服务间的通信就是IPC(inter process communication),具体实现可有:REST(JAX-RS,Spring Boot)、RPC(Thrift, Dubbo)、异步消息调用(Kafka, Notify,RocketMQ)等等。Spring Boot的RestTemplate直接通过URL(Uniform Resource Locator:统一资源定位符)直接对目标服务器发起请求;Dubbo的RPC(Remote Procudure Call 远程服务调用)在通过网络调用远程服务之前将形参值序列化为请求参数、在远程返回数据之后将响应数据反序列化为内存中存在的方法返回值;而RocketMQ是基于推拉(pull-push)或者是发布-订阅(publish-subscribe)的数据模型实现数据交换、达到远程服务调用。
消息中间件的实现是可靠且异步高效的(调用方非阻塞)。相比于数据库来说,其更像是强流动性、强消费可靠性的数据库,而传统数据库是静态的。其也可称消息队列,管理基于消息队列的分布式集群更加容易,因为不再需要注册中心对被调用者的协调,其对性能的浪费也是非常大的,系统瓶颈此时就会明显集中于消息队列集群,属于IO密集模块,其一般用于部分模块为保证调用可靠的一种有效实现方式。
消息中间件适用于需要可靠的数据传送的分布式环境。采用消息中间件机制的系统中,不同的对象之间通过传递消息来激活对方的事件,完成相应的操作。发送者将消息发送给消息服务器,消息服务器将消息存放在若干队列中,在合适的时候再将消息转发给接收者。消息中间件能在不同平台之间通信,它常被用来屏蔽掉各种平台及协议之间的特性,实现应用程序之间的协同,其优点在于能够在客户和服务器之间提供同步和异步的连接,并且在任何时刻都可以将消息进行传送或者存储转发,这也是它比远程过程调用更进一步的原因。
databases:数据库
是什么?在计算机上按照数据结构来组织、存储和管理数据建立的仓库。其特点为长期储存在计算机内、有组织的、可共享的数据集合。数据库中的数据指的是以一定的数据模型组织、描述和储存在一起、具有尽可能小的冗余度、较高的数据独立性和易扩展性的特点并可在一定范围内为多个用户共享。
实现底层和上层开发分离,将后台独立出来,这样保证后台数据的完整性、安全性和可迁移,通过sql语句调用,实现对数据操作错误的可控性。
工程=逻辑+数据结构,一个计算机软件的运行本质上是数据流动的过程,同时以业务为重时数据库只是辅助存取。保证数据存取的精确性,避免并发操作带来的数据异常,特定操作需要事务,易管理;对数据进行高度结构化、完整性、安全性,海量数据的快速检索。数据库就是使用了高度抽象概念实现数据管理的软件。
官方解释:
服务发现:
一个动态目录,启用客户端负载平衡和智能路由;
断路器微服务:
容错与监控仪表板;
配置服务器:
分散式应用程序的动态集中配置管理;
API网关:
API使用者(浏览器,设备,其他API)的单一入口点;
分布式跟踪:
分布式系统的自动化应用程序检测和操作可见性;
OAuth2:
支持单点登录,令牌中继和令牌交换;
消费者驱动契约:
服务演进模式,支持基于HTTP和基于消息的API;
功能实践
CentralConfig:SpringConfigClient
配置文件加密:
yml文件中:'{cipher} FKSAJDFGYOS8F7GLHAKERGFHLSAJ'
properties文件中:{cipher} FKSAJDFGYOS8F7GLHAKERGFHLSAJ
对称加密时设置配置属性: encrypt.key=密码
依赖声明:
<properties>
<spring-cloud.version>Edgware.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<!--spring cloud config client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
配置声明:
spring:
cloud:
config:
uri: http://localhost:8888
fail-fast: true #快速失败
server:
port: 8080
一旦导入SpringCloudConfigClient,默认就会从指定server地址为http://localhost:8888
可访问资源有”/env”
如果server端使用了spring security ,需要在uri中添加user:secret@以确保连接成功
CentralConfig:SpringConfigServer
依赖声明:
<properties>
<spring-cloud.version>Edgware.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<!--spring cloud config server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
配置声明:
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/spring-cloud-samples/config-repo
username: trolley
password: strongpassword
force-pull: true #强制从远程git库中拉取
访问资源格式:
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
ServiceDiscovery: EurekaClient
Eureka通过自动配置绑定Spring环境和其他Spring编程模式惯用语法,为SpringBoot程序提供了NetflixOSS集成。带有一些简单注解,你就可以在你的应用部分快速启用和配置公共模式并且构建有着实战检验过的Netflix组件的大型分布式系统。这种模式下提供了包含服务发现、熔断器、智能路由和客户端的负载均衡。
集成所需要依赖的文件:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
需要配置的属性:
server:
port: 8081 #外部访问端口
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:8761/eureka/ #注册服务器eurekaServer的地址
当客户端向尤里卡注册时,它提供关于其本身的元数据,例如主机和端口,健康指示符URL,主页等。Eureka Server接收来自属于服务的每个实例的心跳消息。如果心跳在可配置的时间表上失败,则通常从注册表中删除实例。有一个问题注册延时问题,一个服务默认在30秒内延迟3次心跳,虽然也可以调整,但是建议不要修改,因为内部设计到一些数据的自动计算。
对外声明REST接口:
服务提供方的的rest接口在springBoot-web中是通过这些进行声明的:
@RestController/@Controller
@RequestMapping/@GetMapping/@PostMapping/@PutMapping
自定义ServiceId:
一个服务声明是通过service ID作为辨别的(来自于多个ip+端口),通过以下参数可以进行自定义:
spring.application.name
server.port
安全连接注册中心:
服务连接注册中心往往是需要安全保障的,通过这样进行设置:
http://user:password@localhost:8761/eureka
获取运行状态和健康指标信息:
运行状态信息页面和健康指标信息是通过这些url进行获取的,
/info --->状态信息
/health --->健康指标
当然,如果是项目中加入了根路径,比如说server.servletPath=/foo或者management.contextPath=/admin的时候可以这样配置:
eureka:
instance:
statusPageUrlPath: ${management.context-path}/info
healthCheckUrlPath: ${management.context-path}/health
配置https:
eureka.instance.[nonSecurePortEnabled,securePortEnabled]=[false,true]
比如说: eureka.instance.securePortEnabled=true
同时需要配置信息展示Url,如:
eureka:
instance:
statusPageUrl: https://${eureka.hostname}/info
healthCheckUrl: https://${eureka.hostname}/health
homePageUrl: https://${eureka.hostname}/
改变实例Id:
eureka:
instance:
instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}
ServiceRigistry: EurekaServer
所需依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
独立模式的配置属性:
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false #服务方不要注册
fetchRegistry: false #强制注册设置false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
同级模式的配置属性:
---
spring:
profiles: peer1
eureka:
instance:
hostname: peer1
client:
serviceUrl:
defaultZone: http://peer2/eureka/
---
spring:
profiles: peer2
eureka:
instance:
hostname: peer2
client:
serviceUrl:
defaultZone: http://peer1/eureka/
CircuitBreaker:HystrixClient
什么是Hystrix?
在SpringCloud中实现了熔断模式的库.
参考资料链接: https://github.com/Netflix/Hystrix/wiki/Configuration
在这样的微架构风格下,如图:
在低级别的服务中有一个服务调用失败之后对于所有用户来说会造成级联失败。
保护措施如下图:
使用这个开放的熔断器去终止级联失败并且对负载过高或者失败的服务进行修复。回调被其他hystrix调用保护,回调可以是静态数据或者是合理的数据。多个回调可以被连接,所以这个第一个回调使得 一些其他的业务调用选择改变返回为一个静态数据。
内部运行机制如下图:
依赖的库文件:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
实际使用中:
@HystrixCommand(fallbackMethod = "")
public String test(String par1){
return LocalDateTime.now().toString();
}
public String testFallbackMethod(String par1){
return LocalDateTime.now().toString();
}
/**
* 通过@HystrixCommand注解为受保护的方法集成熔断模式,通过fallbackMethod指定熔断之后跳转的方法,
* 熔断方法与实际调用方法保持一致,即返回类型、形参列表等等。
*/
开启熔断器的条件为10秒钟内20个请教的失败率大于50%,分别通过circuitBreaker.requestVolumeThreshold自定义次数、通过circuitBreaker.errorThresholdPercentage自定义失败百分率、通过metrics.rollingStats.timeInMilliseconds自定义统计时间。
修改属性
//比如说修改传播属性,使之更加安全
@HystrixCommand(fallbackMethod = "stubMyService",
commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
}
)
//同样的可以通过配置文件进行修改,hystrix.shareSecurityContext的值为true
如何导出hystrix的状态?
通过/health,导出样例如下:
{
"hystrix": {
"openCircuitBreakers": [
"StoreIntegration::getStoresByLocationLink"
],
"status": "CIRCUIT_OPEN"
},
"status": "UP"
}
如何获取tystrix的运行情况?
需要集成以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
访问uri为: /hystrix.stream
这样就可以集成Hystrix Metrics Stream了
如何在网页中优雅的观测工程中的hystrix端点:使用Hystrix Dashboard
效果如下图:
集成Dashboard所依赖的文件:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>
使用Turbine对集群信息进行汇总:
但是Hystrix Dashboard对于整个项目的健康观测并不适用,我们使用Turbine,而什么是Turbine呢?Turebine像是一个应用程序,它的访问uri为 /turbine.stream ,实际中是将所有相关的/hystrix.stream端点汇总为一个组合并展示在用于Hystrix仪表板,一个具体而又典型的例子就是使用Turbine来汇总来自100或1000台机器的数据。实现对整个公司的多个系统的仪表板的监控快照,如下图:
集成Turbine需要的依赖如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>
Turbine通过Eureka从各个服务暴露的/hystrix.stream获取数据。通常访问端口是可以自定义的,例如manage端口,自定义如下:
eureka:
instance:
metadata-map:
management.port: ${management.port:8081}
甚至可以配置需要去搜索展示的服务的列表,配置如下:
turbine:
aggregator:
clusterConfig: CUSTOMERS
appConfig: customers
访问时可能是可以这样:http://my.turbine.sever:8080/turbine.stream?cluster=CLUSTERNAME,其默认参数是default,甚至是不需要传入。appConfig配置的值即为会被Turbine收集的所在的子微服务。
另一种模型实现汇聚数据:
在某些情况下,从各个子微服务中通过访问hystrix而集中到turbine的方法可能会更加难以实现,鉴于这一点,我们使用通过各子微服务推送数据到turbine上,实现这种模型,我们需要为之集成 "Turbine Stream":
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine-stream</artifactId>
</dependency>
Turbine默认访问端口为 8989,在配置类或者启动类上标注了@EnableTurbineStream即可开启收集。
ClientSideLoadBalancer:Ribbon
参考链接:
https://cloud.spring.io/spring-cloud-static/Edgware.RELEASE/single/spring-cloud.html#spring-cloud-ribbon
负载均衡是什么?
负载均衡就是使得内部多台机器按照合适的访问比例被外部访问;
机器内部负载标准线达到怎么办?
hystrix熔断器,限流(个人保留意见为转移流量),自动化扩容(docker+k8s);
负载均衡具体是怎么做到的?将无状态请求按照算法进行完整分发,分别有软负载和硬负载,硬负载有f5等等,软负载有nginx等等,硬负载由于其价格昂贵等原因导致使用者更加偏爱于软负载,软负载通常是可以使在了解集群情况下对平均请求到每一个子微服务中的每一个实例中,或者是随机请求到每一个实例中,或者是进行试探性分发请求以及等等,在SpringCloud中,从eureka server中获取可用服务ip列表并非难事,其具体实现者为Ribbon.
Ribbon的官方定义为"客户端负载均衡器",值得一提的是SpringCloud的Feign是默认集成Ribbon的.
每个子微服务根据ApplicationContext 中客户端的配置创建一个新的集合 RibbonClientConfiguration,其中由 ILoadBalancer 、 RestClient和 ServerListFilter 组成。也就是说每个子微服务的负载均衡在使用Ribbon实现时是可各自不相同的,但是如果是已经集成Ribbon但是未需要配置负载均衡,其默认的策略为轮询,所以有些人觉得每个子微服务都要写一套负载均衡策略是非常浪费时间的其实是不是那样的,但是有些人觉得每个客户端都要集成Ribbon是很麻烦的,确实,在SpringCloud中是默认提供了Eureka Server和Consul以及Zookepper的,如果你觉得麻烦,你大可使用ZK即可。
集成所依赖的文件:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
在使用@RibbonClient时,如何配置达到全局修改该注解的配置?
@RibbonClient(name =“foo”,configuration = FooConfiguration.class)
public class TestConfiguration {
}
//这样 由@Configuration标注的FooConfiguration类会覆盖RibbonClientConfiguration
// TODO 待续...
我们需要了解需要自定义哪些部分,下面是Ribbon源码(组成):
IClientConfig ribbonClientConfig: DefaultClientConfigImpl
IRule ribbonRule: ZoneAvoidanceRule
IPing ribbonPing: DummyPing
ServerList<Server> ribbonServerList: ConfigurationBasedServerList
ServerListFilter<Server> ribbonServerListFilter: ZonePreferenceServerListFilter
ILoadBalancer ribbonLoadBalancer: ZoneAwareLoadBalancer
ServerListUpdater ribbonServerListUpdater: PollingServerListUpdater
ribbonClientConfig:客户端配置,ribbonRule负载规则,ribbonPing通讯,ribbonServerList服务器列表,ribbonServerListFilter服务器列表过滤器,ribbonLoadBalancer负载均衡,ribbonServerListUpdater服务器列表更新。可见SpringCloud的框架的开发者是的java水平是非常高的,代码封装非常漂亮,职责单一,功能完善。其实SpringCloud以及分布式何尝不是这样,在漫长的演进过程中,大量开发人员发现大量代码其实都是一样的思想,人们就直接抽象化抽出了组件,随着越来越多组件的抽出,系统的轮廓就逐渐就出来了,由之前的需要什么造什么导致不同业务逻辑下多套代码难以维护变成现在的一套模板实现整个系统。
通过@Bean注解实现响应接口的实现类达到自定义,一个例子的代码如下:
@Configuration //注意这里是必须
protected static class FooConfiguration {
@Bean
public ZonePreferenceServerListFilter serverListFilter() {
ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
filter.setZone("myTestZone");
return filter;
}
@Bean
public IPing ribbonPing() {
return new PingUrl();
}
}
当然,在实际使用中是需要明白每个接口是如何进行实现的,所以需要查看接口以及其实现类的源码,在IDEA中,通过双击shift搜索类名即可查看源码,在查看接口源码的过程中,我首先是看接口类上的注释内容,上面写有接口作用及其实现类,通过点入即可查看实现类。
但是如何修改默认的全局配置呢?(上面是通过自定义配置对默认配置进行覆盖,当再有配置时会再次对上面配置进行覆盖,最终配置难以确定以及统一),一个示例代码如下:
@RibbonClients(defaultConfiguration = DefaultRibbonConfig.class)
public class RibbonClientDefaultConfigurationTestsConfig {
public static class BazServiceList extends ConfigurationBasedServerList {
public BazServiceList(IClientConfig config) {
super.initWithNiwsConfig(config);
}
}
}
@Configuration
class DefaultRibbonConfig {
@Bean
public IRule ribbonRule() {
return new BestAvailableRule();
}
@Bean
public IPing ribbonPing() {
return new PingUrl();
}
@Bean
public ServerList<Server> ribbonServerList(IClientConfig config) {
return new RibbonClientDefaultConfigurationTestsConfig.BazServiceList(config);
}
@Bean
public ServerListSubsetFilter serverListFilter() {
ServerListSubsetFilter filter = new ServerListSubsetFilter();
return filter;
}
}
其中@RibbonClient和@RibbonClinets的源码为:
@Configuration
@Import(RibbonClientConfigurationRegistrar.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RibbonClient {
以及
@Configuration
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClients {
到这里为止已经知道了功能以及如何自定义,下面总结一下:
提供软件负载平衡器与服务器群集进行通信。负载均衡器提供以下基本功能:
1.向通信客户端提供单个服务器的公共DNS名称或IP
2.根据一定的逻辑在服务器列表之间轮换
某些负载均衡器也可以提供高级功能:
1、通过将客户端和服务器划分为区域(如数据中心内的机架)来建立客户端和服务器之间的关联,并且倾向于在同一个区域内使用服务器
2.保持服务器的统计数据,并避免使用高延迟或频繁失败的服务器
3.保持区域的统计数据,避免可能出现故障的区域
使用高级功能需要使用Ribbon提供的客户端之一,因为它与负载平衡器集成,并向负载平衡器统计提供输入。
负载平衡器的组件的介绍:
1.Rule - 确定从列表返回哪个服务器的逻辑组件
2.Ping - 在后台运行的组件,以确保服务器的活跃性
3.ServerList - 这可以是静态的或动态的。如果它是动态的(被使用DynamicServerListLoadBalancer),后台线程将刷新并按一定的时间间隔过滤列表
自定义方式实现的组件可以以硬编码方式指定,也可以是客户端配置属性的一部分,并通过反射创建,分别有:
NFLoadBalancerClassName: should implement ILoadBalancer
NFLoadBalancerRuleClassName: should implement IRule
NFLoadBalancerPingClassName: should implement IPing
NIWSServerListClassName: should implement ServerList
NIWSServerListFilterClassName: should implement ServerListFilter
在属性配置文件(例如application.yml)中,一个具体的例子如下,(对应的规则为< client >.ribbon.*):
users:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
非常重要的一点,到底有哪些负载均衡的实现规则?如下:
1、RoundRobinRule
这个规则只是通过循环来选择服务器。它通常用作默认规则或更高级规则的后备。
2、AvailabilityFilteringRule
此规则将跳过被视为“电路跳闸”或高并发连接数的服务器。
默认情况下,如果RestClient最近三次连接失败,则会触发一个实例。一旦电路跳闸,在电路被认为再次闭合之前,它将保持在这个状态30秒。但是,如果连接失败,则会再次成为“电路跳闸”,等待“电路闭合”的时间将成倍地增加到连续的失败次数。
以下属性可以通过Archaius设置ConfigurationManager:
# successive connection failures threshold to put the server in circuit tripped state, default 3
niws.loadbalancer.<clientName>.connectionFailureCountThreshold
# Maximal period that an instance can remain in "unusable" state regardless of the exponential increase, default 30
niws.loadbalancer.<clientName>.circuitTripMaxTimeoutSeconds
# threshold of concurrent connections count to skip the server, default is Integer.MAX_INT
<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit
3、WeightedResponseTimeRule
对于这个规则,每个服务器根据其平均响应时间被赋予权重。响应时间越长,重量就越小。规则随机挑选服务器,其中可能性由服务器的权重确定。
要启用WeightedResponseTimeRule,请通过API将其设置为负载平衡器,或者设置以下属性
<clientName>.<clientConfigNameSpace>.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.WeightedResponseTimeRule
4、等等
另外,服务器列表如何获取的?如下
特设静态服务器列表
可以使用API以BaseLoadBalancer或其子类设置静态的服务器列表 BaseLoadBalancer.setServersList()
ConfigurationBasedServerList
负载均衡器的默认ServerList实现。
使用Archaius ConfigurationManager将服务器列表设置为属性。例如
sample-client.ribbon.listOfServers=www.microsoft.com:80,www.yahoo.com:80,www.google.com:80
如果该属性动态更改,则服务器列表也将更改为负载平衡器。
DiscoveryEnabledNIWSServerList
此ServerList实现从Eureka客户端获取服务器列表。服务器群集必须通过属性中的VipAddress标识。例如
myClient.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
# the server must register itself with Eureka server with VipAddress "myservice"
myClient.ribbon.DeploymentContextBasedVipAddresses=myservice
ServerListFilter
ServerListFilter是一个组件,用于DynamicServerListLoadBalancer过滤从ServerList实现返回的服务器。功能区中有两个ServerListFilter实现:
ZoneAffinityServerListFilter
筛选出与客户端不在同一区域的服务器,除非客户端区域中没有可用的服务器。可以通过指定以下属性(假设客户端名称为“myclient”,客户端属性名称空间为“功能区”)来启用此过滤器:
myclient.ribbon.EnableZoneAffinity=true
ServerListSubsetFilter
这个过滤器确保客户端只能看到由ServerList实现返回的整个服务器的一个固定的子集。它也可以定期用新的服务器替换服务器的可用性不佳。要启用此过滤器,请指定以下属性
myClient.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
# the server must register itself with Eureka server with VipAddress "myservice"
myClient.ribbon.DeploymentContextBasedVipAddresses=myservice
myClient.ribbon.NIWSServerListFilterClassName=com.netflix.loadbalancer.ServerListSubsetFilter
# only show client 5 servers. default is 20.
myClient.ribbon.ServerListSubsetFilter.size=5
Ribbon通常就是为服务服务消费者提供的,解决负载均衡的问题。在微服务中进行服务消费是需要进行服务发现调用的,而SpringCloud中的服务消费调用是通过Eureka Client实现的,所以在集成Eureka 和Ribbon时我们通常需要弄清楚一些东西:ribbonServerList 覆盖的扩展DiscoveryEnabledNIWSServerList 将填充来自Eureka的服务器列表。它还取代了代表Eureka 的 IPing接口,NIWSDiscoveryPing以确定服务器是否启动。该ServerList由默认安装的是一个DomainExtractingServerList和这样做的目的是使身体的元数据提供给负载平衡器,而无需使用AWS AMI元数据(这是Netflix的依赖)。默认情况下,服务器列表将使用实例元数据中提供的“区域”信息构建(所以在远程客户端集合上 eureka.instance.metadataMap.zone),如果缺少,可以使用服务器主机名中的域名作为区域的代理(如果approximateZoneFromHostname 标志被设置),一旦区域信息可用,它可以用于一个 ServerListFilter。默认情况下,它将用于定位与客户端位于同一区域的服务器,因为默认为一个 ZonePreferenceServerListFilter。客户端区域的默认方式与远程实例相同,即通过eureka.instance.metadataMap.zone。
之前我们说了那么久的服务发现调用,然后却没有出现一句代码,那是因为我们在实际开发中因为使用麻烦并不直接使用Eureka Client的RestTemplate,顺便回忆一下Eureka Client, Eureka是一种便捷的方式去抽象的发现远程的服务,所以不需要在调用客户端去硬编码访问时的url。
如何在Ribbon的集成中不使用Eureka 从远程注册中心获取服务列表?在配置文件(application.yml)中设置:
stores:
ribbon:
listOfServers: example.com,google.com
或者是直接禁用掉Eureka Client:
ribbon:
eureka:
enabled: false
与之带来的更加麻烦的使用方式:
public class MyClass {
@Autowired
private LoadBalancerClient loadBalancer;
public void doStuff() {
ServiceInstance instance = loadBalancer.choose("stores");
URI storesUri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort()));
// ... do something with the URI
}
}
每个名为客户端的功能区都有一个Spring Cloud维护的对应的子应用程序上下文,这个应用程序上下文在第一个请求到客户端的时候被延迟加载。通过指定功能区客户端的名称,可以将此延迟加载行为更改为在启动时切换加载这些子应用程序上下文。修改配置(application.yml)如下:
ribbon:
eager-load:
enabled: true
clients: client1, client2, client3
在划分粒度不大的时候,在同一个工程中可能会有不同负载的服务,为了提高并发效应下的稳定性,我们需要通过Ribbon修改Hystrix线程池的配置属性,因为默认的实现为所有路由的HystrixCommands将在相同的Hystrix线程池中执行,例如:假如线程阻塞了,这个时候后续有大量的其他请求过来,那么容器中的线程数量则会持续增加直致CPU资源耗尽到100%,整个服务对外不可用,集群环境下就是雪崩,我们可以通过
zuul:
threadPool:
useSeparateThreadPools: true
threadPoolKeyPrefix: zuulgw
修改为每个路由的Hystrix线程池中执行HystrixCommands,另外一点就是默认的HystrixThreadPoolKey与每个路由的服务ID相同,要为HystrixThreadPoolKey添加前缀如上,修改threadPoolKeyPrefix的值即可。
如果要进行金丝雀测试时需要出传递某些信息给IRule的choose方法是可以实现的,首先我们要明白IRule接口的定义:
public interface IRule{
public Server choose(Object key);
:
:
:
可以通过实现IRule接口实现一些特定的功能,如下:
RequestContext.getCurrentContext()
.set(FilterConstants.LOAD_BALANCER_KEY, "canary-test");
如果RequestContext用键把任何对象放入FilterConstants.LOAD_BALANCER_KEY,它将被传递给实现的choose方法IRule。上面的代码必须在执行前RibbonRoutingFilter 执行,Zuul的前置过滤器是最好的地方。您可以通过RequestContext预过滤器轻松访问HTTP标头和查询参数,因此可用于确定LOAD_BALANCER_KEY将传递给功能区。如果你不用LOAD_BALANCER_KEYin RequestContext,那么null将作为choose 方法的参数传递。
DeclarativeRESTClient: Feign
Feign是一个声明式的web服务客户端。这使得编写web服务代码更加容易,要使用Feign就要创建一个接口并注解它,它有可插拔的注解支持,包括Feign和JAX-RS注解。它同样支持可插拔的编码器和解码器。Spring Cloud增加了对SpringMVC注解的支持,并使用HttpMessageConvertersSpring Web中默认使用的注释。Spring Cloud整合Ribbon和Eureka,在使用Feign时提供负载均衡的http客户端。
集成Feign所需要的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
一个使用的例子为,以StoreClient.java为例:
@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
}
使用@FeignClient注解在类上,其中的值指向子微服务在服务注册中心上暴露的名称,在方法上使用@RequestMapping映射uri,其值method指明访问方式、name指向具体uri(服务id)、url值为访问的具体url、通过形参指定调用需要传递的参数、返回值为响应结果。其使用与SpringMVC类似,但是相反的是本地是通过@AutoWired 进行注入,使用@Feign注解的接口类,通过访问本地访问的形式去访问远程接口,其思想与RPC类似。
@FeignClient的源码如下:
package org.springframework.cloud.netflix.feign;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
@AliasFor("name")
String value() default "";
@Deprecated
String serviceId() default "";
@AliasFor("value")
/**
* The service id with optional protocol prefix. Synonym for {@link #value() value}.
*/
String name() default "";
String qualifier() default "";
/**
* An absolute URL or resolvable hostname (the protocol is optional).
*/
String url() default "";
boolean decode404() default false;
Class<?>[] configuration() default {};
Class<?> fallback() default void.class;
Class<?> fallbackFactory() default void.class;
String path() default "";
boolean primary() default true;
}
如何为SpringCloudFeign配置配置文件?
SpringCloudFeign支持中的一个中心概念是指定的客户端。每个feign客户端都是组件的一部分,这些组件一起工作以便根据需要连接远程服务器,并且该集合具有作为使用@FeignClient注释的应用程序开发人员提供的名称。Spring Cloud根据ApplicationContext 为每个指定客户端的需求创建一个新的集合FeignClientsConfiguration。这包含(除其他外)一个 feign.Decoder,一个 feign.Encoder和一个 feign.Contract。Spring Cloud允许您通过声明额外的配置(在之上FeignClientsConfiguration)来完全控制客户端@FeignClient 。例如:
@FeignClient(name = "stores", configuration = FooConfiguration.class)
public interface StoreClient {
//..
}
在这种情况下,客户端是由已经FeignClientsConfiguration在一起的任何组件FooConfiguration(后者将覆盖前者)组成的
FooConfiguration不需要注释@Configuration。但是,如果是的话,照顾到任何排除@ComponentScan,否则将包括此配置,它将成为默认信号源feign.Decoder 、feign.Encoder 、feign.Contract规定等。这可以通过把它在一个单独的、非重叠的包从任何被避免@ComponentScan或@SpringBootApplication,或者它可以被明确地排除在@ComponentScan。该serviceId属性现在不赞成使用该name属性。以前,使用该url属性,不需要 name属性, name现在需要使用。
在name和url属性中支持占位符。例如:
@FeignClient(name =“$ {feign.name}”,url =“$ {feign.url}”)
public interface StoreClient {
// ...
}
如何自定义默认feign类?我们需要理解feign的默认组成:
Decoder————feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder)
Encoder————feignEncoder: SpringEncoder
Logger————feignLogger: Slf4jLogger
Contract————feignContract: SpringMvcContract
Feign.Builder————feignBuilder: HystrixFeign.Builder
Client————feignClient: if Ribbon is enabled it is a LoadBalancerFeignClient, otherwise the default feign client is used.
另外在实际应用网络访问库中,OkHttpClient和ApacheHttpClient的feign客户端可以通过设置使用feign.okhttp.enabled或feign.httpclient.enabled以true分别和具有它们的类路径上。可以通过ClosableHttpClient在使用Apache或OkHttpClient使用OK HTTP 时提供一个bean来自定义HTTP客户端。Spring Cloud Netflix 默认情况下不提供以下bean,但仍然从应用程序上下文中查找这些类型的bean来创建feign客户端:
Logger.Level
Retryer
ErrorDecoder
Request.Options
Collection<RequestInterceptor>
SetterFactory
创建这些类型的bean并将其放入@FeignClient配置中(FooConfiguration如上所述),可以覆盖所描述的每个bean。例如:
@Configuration
public class FooConfiguration {
@Bean
public Contract feignContract(){
return new feign.Contract.Default();
}
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor(){
return new BasicAuthRequestInterceptor( “user”, “password”);
}
}
//这将替换SpringMvcContract用feign.Contract.Default,并增加了RequestInterceptor对集合RequestInterceptor
同时也可以使用属性文件的形式进行配置(application.yml中):
feign:
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
默认配置可以在@EnableFeignClients属性defaultConfiguration中以与上述类似的方式指定。不同的是,这个配置将适用于所有feign客户端。例如:
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
如果我们同时在代码和配置文件中配置了,结果是配置文件中的会生效,即将代码中配置的属性值给覆盖掉。但是这并不是写死的,通过修改feign.client.default-to-properties 为 false 可以达到相反的效果。
如果你需要使用ThreadLocal绑定变量在你的请求拦截器上,你将为了Hystrix需要同样的设置线程隔离策略为SEMAPHORE,或者是在Feign中禁用Hystrix。例如:
# To disable Hystrix in Feign 在Feign中禁用Hystrix(考虑到配置文件中可能会有强制转码的可能性,这里保留英文注释)
feign:
hystrix:
enabled: false
# To set thread isolation to SEMAPHORE 设置线程隔离级别为SEMAPHORE
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE
如何去手动的创建一个Feign客户端?
为什么需要手动创建呢?不是有注解不就够了吗?因为在某些情况下,可能需要以不可能使用上述方法的方式自定义的Feign客户端。在这种情况下,可以使用Feign Builder API创建客户端 。下面是一个例子,它创建了两个具有相同接口的Feign客户端,但是用一个单独的请求拦截器来配置每个客户端:
@Import(FeignClientsConfiguration.class)
class FooController {
private FooClient fooClient;
private FooClient adminClient;
@Autowired
public FooController(
Decoder decoder, Encoder encoder, Client client) {
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "http://PROD-SVC");
this.adminClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "http://PROD-SVC");
}
}
在上面的例子中 FeignClientsConfiguration.class 通过SpringCloudNetflix作为默认的配置提供者
PROD-SVC 是服务客户端们的名称将被作为请求到达。
Feign与Hystrix的共存状态?如果Hystrix在类路径上并且feign.hystrix.enabled=true,Feign将用断路器包装所有的方法。返回的 com.netflix.hystrix.HystrixCommand 同样也是可用的。这就使得你可以使用反应模式(也就是调用.toObservable()或.observe()或异步使用(调用去 .queue())。但是,要以每个Feign客户端为基础禁用Hystrix支持,为每个Feign客户端,创建一个带有"prototype"范围的 Feign.Builder,例如:
@Configuration
public class FooConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}
值得注意的一点,在Spring Cloud Dalston发布之前,如果Hystrix在类路径上,Feign默认情况下会将所有方法封装在断路器中。Spring Cloud Dalston改变了这种默认行为,采用选择加入的方式。
上面我们已经写过,hystrix在熔断时会有会有后备"fallback",但是在与Feign集成时会变成什么样呢?而且我们也知道了Hystrix是加入的Feign的行为的。在熔断或出现错误时执行。为给@FeignClient 中的 fallback属性设置fallback,将属性设置为实现回退的类名称。还需要将您的实现声明为Spring bean而不能直接书写,例如:
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}
static class HystrixClientFallback implements HystrixClient {
@Override
public Hello iFailSometimes() {
return new Hello("fallback");
}
}
//与之前一致的是,此wrap接口中只能指定服务id+后备方法+映射路径+限定访问类型+形参列表+返回值类型
为了进行一些操作获取熔断时的具体情况(回退触发器的原因),比如说日志记录等,需要通过在@FeignClient中指定fallbackFactory属性值进行指定:
@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}
@Component
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
@Override
public HystrixClient create(Throwable cause) {
return new HystrixClient() {
@Override
public Hello iFailSometimes() {
return new Hello("fallback; reason was: " + cause.getMessage());
}
};
}
}
//该类需要继承FallbackFactory<HystrixClient> 重写create方法,接受Throwable形参,该形参是异常的顶级接口,返回值为HystrixClient,即需要创建HystrixClient对象,使用匿名内部类的形式,重写iFailSometimes方法,返回feign接口中方法返回的数据类型,同时可以将Throwable中的信息取出。
注意,在Feign中后备的实现和Hystrix的后备是如何工作是有限制的。在当前版本中Fallbacks不支持返回 com.netflix.hystrix.HystrixCommand and rx.Observable 。
当使用Feign和Hystrix回退时,ApplicationContext同一类型中有多个bean 。这将导致@Autowired 无法工作,因为没有一个bean,或标记为主要的一个。为了解决这个问题,Spring Cloud Netflix将所有Feign实例标记为@Primary,所以Spring Framework将知道注入哪个bean。在某些情况下,这可能并不通用。要关闭此行为,需要将primary属性设置@FeignClient为false。代码如下:
@FeignClient(name = "hello", primary = false)
public interface HelloClient {
// methods here
}
如何充分发挥Feign声明式的接口的作用?Feign通过单继承接口支持样板。这允许将通用操作分组为方便的基础接口。例如:
UserService.java.
public interface UserService { //Feign接口
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
User getUser(@PathVariable("id") long id);
}
UserResource.java.
@RestController
public class UserResource implements UserService { //Controller层<-提供方的
}
UserClient.java.
package project.user;
@FeignClient("users")
public interface UserClient extends UserService { //拓展feign接口
}
服务器和客户端之间共享一个接口问题:
但是,值得一说的是一般不建议在服务器和客户端之间共享一个接口。它引入了紧密的耦合,而且实际上并不以当前的形式用于Spring MVC(方法参数映射不被继承)。在很早的soa中,在使用dubbo这种rpc框架时,通常会对Service进行抽取接口,通过RPC模式在远程进行方法调用,这种方式在早期开发中起到了加速开发的作用,但是后来我们发现这种接口强制声明的方式会对接口升级起到抑制作用,甚至无法为局部升级。如果是Controller层抽取接口,效果与Dubbo一致,但是由于是微服务,子微服务自治,其升级却是畅通无阻的。实际开发中觉得为类抽取接口是件麻烦事情,但是通过Ide工具,如idea,抽取接口是非常容易的,在多个微服务之间通过nexus私有库进行共享对外接口接口,可以实现开发的更加清晰,但是弊端也是有的,在服务升级时可能会加重升级代价。
在网络服务器上通常可以通过算法对HTML文档或者其他形式的文件进行压缩,再到调用端进行解压,这样可以减少来自带宽上的压力从而带来更大的访问量,同时也会加重服务的CPU和内存的负载,但是其实我们都是推荐开启的,因为这点硬件负载还是承担的起的,Feign作为网络服务器客户端,其实其作用已经是可以进行请求/响应压缩了。这些通过配置文件进行配置即可:
# 是否启用请求/响应压缩
feign.compression.request.enabled=true
feign.compression.response.enabled=true
# 设置对压缩的媒体类型和最小请求阈值长度(数据太小没必要压缩,浪费调用时间,而且有些数据或许已经压缩了)
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
Feign的日志:
为每个创建的Feign客户端创建一个记录器。默认情况下,记录器的名称是用于创建Feign客户端的接口的完整类名称。Feign日志记录只响应该DEBUG级别。通常需要在application.yml中配置logging.level.project.user.UserClient:DEBUG 其中 Logger.Level 指定Feign需要记录哪些内容。通常可选级别为:
NONE,不记录(默认值)。
BASIC只记录请求方法和URL以及响应状态码和执行时间。
HEADERS记录基本信息以及请求和响应头。
FULL,为请求和响应记录标题,正文和元数据。
同时也可以通过代码对级别进行配置:
@Configuration
public class FooConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
ExternalConfiguration: Archaius
Archaius是Netflix客户端配置库。这是所有Netflix OSS组件用于配置的库。Archaius是Apache Commons Configuration项目的扩展。它允许更新配置,方法是一方面轮询源配制中心是否更改一方面为资源更改到客户端。Archaius使用Dynamic <Type> Property类作为属性的句柄。下面是手动编写代码的例子:
class ArchaiusTest {
DynamicStringProperty myprop = DynamicPropertyFactory
.getInstance()
.getStringProperty("my.prop");
void doSomething() {
OtherClass.someMethod(myprop.get());
}
}
Archaius有自己的一套配置文件和加载优先级。Spring应用程序通常不应该直接使用Archaius,而是需要本地配置Netflix工具。Spring Cloud有一个Spring环境桥接,所以Archaius可以从Spring环境读取属性。这允许Spring Boot项目使用正常的配置工具链,同时允许他们配置Netflix工具,即使用@Value。
RouterAndFilter: Zuul
路由是微服务架构的重要组成部分。例如,/可能会映射到Web应用程序根路径对应的内容(很可能是首页),/api/users映射到用户服务以及 /api/shop映射到商店服务。Zuul是由Netflix提供的基于JVM的路由器和服务器端负载均衡器。
Zuul通常包含以下功能:
认证:Authentication
洞察(监察):Insights
压力测试:Stress Testing
金丝雀测试:Canary Testing
动态路由:Dynamic Routing
服务迁移:sService Migration
加载脱落:Load Shedding
安全:Security
静态响应处理:Static Response handling
活跃及活跃流量管理:Active/Active traffic management
Zuul的规则引擎允许规则和过滤器以任何基于JVM的语言编写,内置对Java和Groovy的支持。
值得一提的是,配置属性zuul.max.host.connections已被取代的两个新的属性,zuul.host.maxTotalConnections并且zuul.host.maxPerRouteConnections它的缺省值分别200和20。 所有路由的默认Hystrix隔离模式(ExecutionIsolationStrategy)是SEMAPHORE。zuul.ribbonIsolationStrategy如果这种隔离模式是首选的,可以更改为THREAD。
Zuul是从设备和网站到Netflix应用程序后端的所有请求的前门。作为边缘服务应用程序,Zuul的建立是为了实现动态路由,监控,弹性和安全性。它还能够根据需要将请求路由到多个云服务器自动扩容组。
为什么要在微服务架构添加Zull组件?
Netflix API流量的数量和多样性有时会导致生产问题迅速出现,而没有任何警告。我们需要一个能让我们快速改变行为的系统,以便对这些情况作出反应。Zuul使用一系列不同类型的过滤器,使我们能够快速,灵活地将功能应用于我们的边缘服务。这些过滤器帮助我们执行以下功能:
身份验证和安全性 - 识别每个资源的身份验证要求并拒绝不满足要求的请求。
见解和监测 - 在边缘跟踪有意义的数据和统计数据,以便为我们提供准确的生产视图。
动态路由 - 根据需要将请求动态路由到不同的后端群集。
压力测试 - 逐渐增加到群集的流量,以衡量表现。
加载脱落- 为每种类型的请求分配容量,并删除超出限制的请求。
静态响应处理 - 直接在边缘建立一些响应,而不是将它们转发到内部群集
多区域弹性 - 跨云服务区域的路由请求,以使我们的ELB使用多样化,并使我们的边缘更接近我们的成员。
Zuul组件如何工作以及组成?
zuul-core - 包含编译和执行过滤器的核心功能的库
zuul-simple-webapp - webapp,其中显示了如何使用zuul-core构建应用程序的简单示例
zuul-netflix - 将其他NetflixOSS组件添加到Zuul的库 - 使用功能区来执行路由请求。
zuul-netflix-webapp - webapp把zuul-core和zuul-netflix组合成一个易于使用的软件包
如何集成Zuul到我们的SpringBoot项目中?在文件库依赖上需要添加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
zuul如何做到门面工作的呢?其实就是嵌入式反向代理,由此来缓解UI应用程序希望将调用委托给一个或多个后端服务的非常常见的用例的开发。此功能对于用户界面代理所需的后端服务非常有用,避免了独立管理所有后端的CORS和身份验证问题。要启用它,用Spring注释一个Spring Boot主类 @EnableZuulProxy,并且将本地调用转发给适当的服务。按照惯例,具有“用户ID”的服务将接收来自位于/users(用剥离的前缀)的代理的请求。代理使用功能区找到要通过发现转发的实例,并且所有请求都在hystrix命令中执行 ,因此失败将显示在Hystrix度量标准中,一旦电路打开,代理将不会尝试连接服务。
注意:Zuul starter不包含发现客户端,所以对于基于服务ID的路由,我们也需要在类路径中提供其中的一个(例如Eureka是一种选择)
一个非常重要一点是:并非所有的都是需要被代理转发的,如何在zuul中配置其不被转发?配置如下:
zuul:
ignoredServices: '*' # 需要排除的服务id
routes:
users: /myusers/** # 需要映射的服务id,优于上面的排除列表
上面的配置表示:所有的服务都被忽略,除了 “用户”。
实际上开发中是不断从中不断添加映射的,通过修改以下配置文件:
zuul:
routes:
users: /myusers/**
这样配置表示 /myusers/101 的请求将会直接转发到/myusers服务中去处理。
为了更好的控制路由(当路径与服务id不匹配时),也可以单独指定路径和serviceId,配置如下:
zuul:
routes:
users:
path: /myusers/**
serviceId: users_service
这意味着http调用“/ myusers”被转发到“users_service”服务。路由必须有一个“路径”,可以指定为蚂蚁样式,因此“/ myusers / *”只匹配一个级别,但“/ myusers / **”分层次匹配。
如果服务自动发现注册可能并不适用,同样可以直接映射指定到指定服务器中(这样的配置与单体工程可以形成有利结合,同样可以导致多域名的时候,目前很流行),配置如下:
zuul:
routes:
users:
path: /myusers/**
url: http://example.com/users_service
但是,这些简单的url路由不会被执行,HystrixCommand也不会使用Ribbon来负载多个URL。为了达到这个目的,可以指定serviceId一个静态的服务器列表。如下:
zuul:
routes:
echo:
path: /myusers/**
serviceId: myusers-service
stripPrefix: true
hystrix:
command:
myusers-service:
execution:
isolation:
thread:
timeoutInMilliseconds: ...
myusers-service:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
ListOfServers: http://example1.com,http://example2.com # 这种配置之前也写过
ConnectTimeout: 1000
ReadTimeout: 3000
MaxTotalHttpConnections: 500
MaxConnectionsPerHost: 100
另一种方法是指定一个服务路由并为serviceId配置一个Ribbon客户端(这需要在功能区中禁用Eureka支持),这就是以前很常用的DNS分配模式,配置如下:
zuul:
routes:
users:
path: /myusers/**
serviceId: users
ribbon:
eureka:
enabled: false
users:
ribbon:
listOfServers: example.com,google.com
另外一种实现思路是,使用正则映射在服务ID(serviceId)和路由之间提供约定。它使用正则表达式命名组从serviceId中提取变量并将它们注入路由模式。例如:
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
这意味着服务id “myusers-v1”将被映射为路由/ v1 / myusers / **。任何正则表达式都被接受,但是所有的命名组都必须出现在服务模式(servicePattern)和路由模式(routePattern)中。如果服务模式(servicePattern)与服务id(serviceId)不匹配,则使用默认行为。 在上面的示例中,服务id “myusers”将被映射为路由/ myusers / **(未检测到任何版本)此功能在默认情况下处于禁用状态,仅适用于发现的服务。
要为所有映射添加前缀,请设置zuul.prefix一个值,如 /api。在请求被默认转发之前,代理前缀将从请求中剥离出来(关闭该行为 zuul.stripPrefix=false)。我们也可以关闭从单个路由中删除特定于服务的前缀,例如:
zuul:
routes:
users:
path: /myusers/**
stripPrefix: false
zuul.stripPrefix只适用于设置的前缀zuul.prefix。它对给定路由中定义的前缀没有任何影响path 。
在这个例子中,对“/ myusers / 101”的请求将被转发到“users”服务上的“/ myusers / 101”。
这些zuul.routes条目实际上绑定到一个类型的对象ZuulProperties。如果你看看这个对象的属性,你会看到它也有一个“可重试”的标志。将该标志设置为“true”以使功能区客户端自动重试失败的请求(如果需要,可以使用功能区客户端配置修改重试操作的参数)。 X-Forwarded-Host标头被添加到默认转发的请求。把它关掉设置zuul.addProxyHeaders = false。前缀路径在默认情况下被剥离,并且对后端的请求在上面的例子中选择一个头部“X-Forwarded-Prefix”(“/ myusers”)。 @EnableZuulProxy如果您设置了默认路由(“/”),则应用程序可以充当独立服务器,例如zuul.route.home: /将所有流量(即“/ **”)路由到“家庭”服务。如果需要更细粒度的忽略,则可以指定要忽略的特定模式。这些模式在路由定位过程开始时被评估,这意味着模式中应包含前缀以保证匹配。忽略的模式跨越所有服务并取代任何其他路由规范。
如何过滤指定服务?例如,/ myusers / 101”的所有调用都将被转发到“用户”服务上的“/ 101”。但“/ admin /”的调用不会转发。配置如下:
zuul:
ignoredPatterns: /**/admin/**
routes:
users: /myusers/**
如果您需要您的路线保留其订单,则需要使用YAML文件,因为使用属性文件会丢失订单。一个具体的例子就是如果以下配置文件是配置文件而不会YAML文件就会导致legacy路径可能会最终在users 路径前面呈现users路径不可访问。配置文件内容如下:
zuul:
routes:
users:
path: /myusers/**
legacy:
path: /**
关于zuul是如何发起请求的?zuul使用的默认HTTP客户端现在由Apache HTTP Client支持,而不是弃用的功能区RestClient。要使用RestClient或使用okhttp3.OkHttpClient ,需要分别设置ribbon.restclient.enabled=true或ribbon.okhttp.enabled=true。如果您想自定义Apache HTTP客户端,或者确定HTTP客户端提供类型为ClosableHttpClient 或者 OkHttpClient。
关于Cookie和请求头(header)的敏感问题:
在同一系统中的服务之间共享header是可以的,但是你可能不希望敏感的标题向下游泄漏到外部服务器中。您可以指定一个忽略header列表作为路由配置的一部分。由于Cookie在浏览器中具有明确的语义,因此Cookie扮演着特殊的角色,并且始终将其视为敏感。如果代理的用户是浏览器,那么下游服务的cookie也会给用户带来问题,因为他们都混乱了(所有的下游服务看起来都是来自同一个地方)。
如果对服务的设计非常小心,例如,如果只有一个下游服务设置了cookie,那么可能会让它们从后端一路流向调用者。此外,如果代理设置了Cookie,并且所有后端服务都是同一系统的一部分,那么简单地共享它们(例如使用Spring Session将它们链接到某个共享状态)可能是很自然的。除此之外,任何由下游服务设置的cookie对于调用者来说可能都不是很有用,所以建议至少把“Set-Cookie”和“Cookie”制作成敏感的头文件,不属于域名的一部分。即使是属于域名的路由,
敏感请求头可以配置为每个路由的逗号分隔列表,例如
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders: Cookie,Set-Cookie,Authorization
url: https://downstream
注意:这是默认值sensitiveHeaders,所以你不需要设置它,除非你希望它是不同的。注意,这是Spring Cloud Netflix 1.1中的新功能(在1.0版中,用户无法控制请求头和所有的Cookie都在两个方向上流动。
这个sensitiveHeaders是一个黑名单,默认不是空的,所以为了让Zuul发送所有头(除了“被忽略”的头),你必须明确地将它设置为空列表。如果您想将Cookie或授权标头传递给您的后端,这是必要的。例:
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders:
url: https://downstream
其实敏感header也可以通过设置全局设置zuul.sensitiveHeaders。如果sensitiveHeaders 在路由上设置,则将覆盖全局sensitiveHeaders设置。
如何忽略请求头?
除了每个路由敏感请求头外,还可zuul.ignoredHeaders以为与下游服务交互期间应丢弃的值(包括请求和响应)设置一个全局值。默认情况下,如果Spring Security不在类路径中,它们是空的,否则它们被初始化为Spring Security指定的一组众所周知的“security”头(例如涉及缓存)。在这种情况下的假设是下游服务可能也会添加这些头,我们希望来自代理的值。为了以防Spring安全不放弃这些众所周知的安全头部是可以设置的类路径 zuul.ignoreSecurityHeaders 为 false。如果您在Spring Security中禁用了HTTP安全性响应头并且需要下游服务提供的值,这可能会很有用。
如何管理端点?如果您正在使用@EnableZuulProxySpring Boot Actuator,则将启用(默认情况下)两个附加端点:1、路由(Routes) 2、过滤器(Filters)。
在访问路由端点的GET方式的 /routes url时将返回映射路由的列表,如下所示:
{
/stores/**: "http://localhost:8081"
}
在默认实现中可以添加请求参数?format=details 得到更加详细的内容,如下所示:
url:GET /routes?format=details
响应结果:
{
"/stores/**": {
"id": "stores",
"fullPath": "/stores/**",
"location": "http://localhost:8081",
"path": "/**",
"prefix": "/stores",
"retryable": false,
"customSensitiveHeaders": false,
"prefixStripped": true
}
}
值得注意的是POST将强制刷新现有的路由(例如,如果服务目录中有更改)。当然也可以通过设置endpoints.routes.enabled为false 禁用此端点。按理来说路由应该自动响应服务目录中的改变,但是POST /路由是强制立即发生改变的一种方式,在一种方面是一种后门,但是如果泄露,后果也是很严重的。
过滤(filter)端点如何使用?通过GET /filters将按类型返回Zuul过滤器的映射。对于地图中的每个过滤器类型,您将找到该类型的所有过滤器的列表及其详细信息。
迁移现有的应用程序或API时,常见的模式是“扼杀”旧的端点,慢慢地用不同的实现替换它们。Zuul代理是一个有用的工具,因为您可以使用它来处理来自旧端点的客户端的所有流量,但将一些请求(forward)重定向到新端点。但是我们如何设置扼杀模式和本地转发呢?可以通过修改配置文件的方式,例如:
zuul:
routes:
first:
path: /first/**
url: http://first.example.com
second:
path: /second/**
url: forward:/second
third:
path: /third/**
url: forward:/3rd
legacy:
path: /**
url: http://legacy.example.com
在上面这个例子中,我们正在扼杀映射到不匹配其他模式的所有请求的“遗留”应用程序。路径 /first/**已经被提取到一个带有外部URL的新服务中。路径/second/**被转发,因此可以在本地进行处理,例如使用普通的Spring @RequestMapping。路径 /third/**也被转发,但具有不同的前缀(即被/third/foo转发到/3rd/foo)。注意最后那个,被忽略的模式并不完全被忽略,它们只是不被代理处理(所以它们也被有效地本地转发)。
由于zuul是流量转发,对于大文件上传可能会导致一些问题:默认情况下如果标注@EnableZuulProxy 后可以使用代理路径来上传文件,只要文件很小就可以工作。对于大文件,DispatcherServlet在“/ zuul / *”中有一条绕过Spring (避免多部分处理)的替代路径。也就是说,如果 zuul.routes.customers=/customers/** 就可以将大文件发布到“/ zuul / customers / *”。servlet路径通过外部化zuul.servletPath。如果代理路由引导您通过ribbon负载平衡器,则极大的文件也需要提升超时设置。配置如下:
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000 # 大文件连接超时
ReadTimeout: 60000 # 大文件读取超时
为了使流式文件处理大型文件,您需要在请求中使用分块编码(某些浏览器默认不会这样做),一个命令行的例如如下:
$ curl -v -H "Transfer-Encoding: chunked" \
-F "[email protected]" localhost:9999/zuul/simple/file
关于查询字符串编码:在处理传入的请求时,查询参数被解码,因此可以在Zuul过滤器中进行可能的修改。当在路由过滤器中构建后端请求时,它们将被重新编码。例如,如果使用Javascript encodeURIComponent()方法编码,结果可能与原始输入不同。虽然这在大多数情况下不会引起任何问题,但某些Web服务器可能会对复杂的查询字符串进行编码。要强制查询字符串的原始编码,可以传递一个特殊的标志,ZuulProperties以便查询字符串按照以下HttpServletRequest::getQueryString方法进行:
zuul:
forceOriginalQueryStringEncoding: true
注意:这个特殊的标志只适用于SimpleHostRoutingFilter,RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)因为查询字符串现在直接在原文中获取,所以你很容易覆盖查询参数HttpServletRequest。
同样可以简单的就应用直接嵌入zuul,可以在没有代理的情况下运行一个Zuul服务器,或者如果你使用@EnableZuulServer(而不是@EnableZuulProxy)来选择代理平台的一部分。您添加到类型应用程序的任何bean ZuulFilter 都将自动安装,因为它们是自动添加的@EnableZuulProxy,但没有任何代理过滤器被自动添加。在这种情况下,通过配置zuul.routes.*来指定进入Zusul服务器的路由,但是没有服务发现和代理,所以“serviceId”和“url”设置被忽略。例如:
zuul:
routes:
api: /api/**
将/api/**中的所有路径映射到Zuul过滤器链。
关于如何禁用过滤器?Zuul为Spring Cloud提供了ZuulFilter ,默认情况下在代理和服务器模式下启用的一些bean。如果你想禁用一个,只需设置zuul.<SimpleClassName>.<filterType>.disable=true。按照惯例,之后的包 filters是Zuul过滤器类型。例如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter设置 zuul.SendResponseFilter.post.disable=true。
在zuul中如何为路由提供Hystrix回退?当Zuul中给定路由的电路跳闸时,可以通过创建一个类型的bean来提供回退响应ZuulFallbackProvider。在这个bean中,你需要指定后备的路由ID,并提供一个ClientHttpResponse返回值作为后备。这是一个非常简单的ZuulFallbackProvider实现。例如:
class MyFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "customers";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
配置文件如下:
zuul:
routes:
customers: /customers/**
如果您想为所有路由提供默认回退,则可以创建一个类型为的bean ZuulFallbackProvider ,并使getRoute方法返回*或null ,例如:
class MyFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
如果您想根据FallbackProvider将ZuulFallbackProvder在未来版本中替换的失败原因选择响应,请参考以后下面代码:
class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(final Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return fallbackResponse();
}
}
@Override
public ClientHttpResponse fallbackResponse() {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {
return status.value();
}
@Override
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
总结一下关于zuul超时:
如果要配置套接字超时并通过Zuul代理请求读取超时,则有两个选项基于您的配置。
如果Zuul使用服务发现比你需要通过功能区属性来配置这些超时, ribbon.ReadTimeout和ribbon.SocketTimeout。
如果您通过指定URL来配置Zuul路由,则需要使用 zuul.host.connect-timeout-millis和zuul.host.socket-timeout-millis。
重写Location 请求头:
网关作为全局唯一的对外服务器,有一个问题就非常重要——重写Location 请求头,如果Zuul面向Web应用程序,那么Location当Web应用程序通过http状态代码3XX重定向时,可能需要重写头,否则浏览器将最终重定向到Web应用程序的url而不是Zuul url。一个LocationRewriteFilterZuul器可被配置来重新写Location头到Zuul的URL,它也增加了返回的跳过的全局和路由的具体前缀。过滤器可以通过一个Spring配置文件以下面的方式添加:
import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter;
...
@Configuration
@EnableZuulProxy
public class ZuulConfig {
@Bean
public LocationRewriteFilter locationRewriteFilter() {
return new LocationRewriteFilter();
}
}
很重要的一点,Zuul是如何工作的呢?
过滤器概述
Zuul的中心是一系列过滤器,能够在HTTP请求和响应的路由过程中执行一系列操作。
以下是Zuul滤波器的主要特性:
1、类型:通常在应用过滤器时定义路由流程中的阶段(尽管它可以是任何自定义字符串)
2、执行顺序:在类型中应用,定义跨多个过滤器的执行顺序
3、标准:执行过滤器所需的条件
4、行动:符合条件时要执行的行动
Zuul提供了一个动态读取,编译和运行这些过滤器的框架。过滤器不直接相互通信 - 而是通过每个请求唯一的RequestContext共享状态。
虽然Zuul支持任何基于JVM的语言,但是过滤器目前是用Groovy编写的。每个过滤器的源代码被写入到Zuul服务器上的一组指定的目录中,这些目录将被定期轮询以进行更改。更新的过滤器从磁盘读取,动态编译到正在运行的服务器中,并由Zuul为每个后续请求调用。
过滤器类型
有几种标准的过滤器类型对应于请求的典型生命周期:
1、PRE:过滤器在路由到原点之前执行。示例包括请求身份验证,选择原始服务器以及记录调试信息。
2、ROUTING:过滤器处理将请求路由到原点。这是使用Apache HttpClient或Netflix Ribbon构建和发送原始HTTP请求的地方。
3、POST:过滤器在请求被路由到原点后执行。示例包括向响应中添加标准HTTP头,收集统计信息和度量标准,以及将响应从源发送到客户端。
4、ERROR:过滤器在其他阶段发生错误时执行。
除了默认的Filter流程外,Zuul允许我们创建自定义的过滤器类型并明确执行它们。例如,我们有一个自定义的STATIC类型,它在Zuul中生成一个响应,而不是将请求转发到一个源。我们有一些用例,其中之一是包含关于特定Zuul实例的调试数据的内部端点。
请求到达Zuul的生命周期,如下图:
Zuul被实现为一个Servlet。对于一般情况,Zuul被嵌入到Spring Dispatch机制中。这允许Spring MVC控制路由。在这种情况下,Zuul被配置为缓冲请求。如果需要在没有缓冲请求的情况下通过Zuul(例如对于大文件上传),Servlet也安装在Spring Dispatcher之外。默认情况下,这是位于/zuul。该路径可以随着zuul.servlet-path属性而改变。
要在过滤器之间传递信息,Zuul使用一个 RequestContext。其数据保存在ThreadLocal (本地线程绑定)每个请求的特定位置。在哪里路由请求,错误和实际信息 都在HttpServletRequest,并通过HttpServletResponse存储在那里。RequestContext 继承了 ConcurrentHashMap,所以什么都可以存储在上下文。FilterConstants包含由Spring Cloud Netflix安装的过滤器使用的键。
关于@EnableZuulProxy与@EnableZuulServer 与的区别:Spring Cloud Netflix安装了许多基于哪个注释被用来启用Zuul的过滤器。@EnableZuulProxy是一个超集@EnableZuulServer。换句话说,@EnableZuulProxy包含安装的所有过滤器@EnableZuulServer。“代理”中的附加过滤器启用路由功能。如果你想要一个“空白”Zuul,你应该使用@EnableZuulServer。
@EnableZuulServer过滤器的实现:
创建一个SimpleRouteLocator从Spring Boot配置文件加载路由定义。
安装以下过滤器(与普通的Spring Bean一样):
预过滤器:
1、ServletDetectionFilter:检测请求是否通过Spring Dispatcher。用键设置布尔值FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY。
2、FormBodyWrapperFilter:解析表单数据并为下游请求重新编码。
3、DebugFilter:如果debug请求的参数设置,该过滤器设置RequestContext.setDebugRouting()和RequestContext.setDebugRequest()为true。
路线过滤器:
4、SendForwardFilter:这个过滤器使用Servlet转发请求RequestDispatcher。转发位置存储在RequestContext属性中FilterConstants.FORWARD_TO_KEY。这对于转发到当前应用程序中的端点很有用。
后过滤器:
5、SendResponseFilter:将代理请求的响应写入当前响应。
错误过滤器:
6、SendErrorFilter:前进到/error (默认)如果RequestContext.getThrowable()不为空。默认转发路径(/error),可以通过设置error.path属性进行更改。
@EnableZuulProxy 过滤器的实现:
创建一个DiscoveryClientRouteLocator从DiscoveryClient(如Eureka),以及从属性加载路由定义。为每个serviceId从路由创建DiscoveryClient。随着新服务的添加,路由将被刷新。
除了上述过滤器外,还安装了以下过滤器(如正常的Spring Bean):
预过滤器:
1、PreDecorationFilter:此过滤器根据提供的内容确定在哪里以及如何路由RouteLocator。它还为下游请求设置了各种与代理相关的头文件。
路线过滤器:
2、RibbonRoutingFilter:此过滤器使用Ribbon,Hystrix和可插入的HTTP客户端发送请求。服务ID在RequestContext属性中找到FilterConstants.SERVICE_ID_KEY。此筛选器可以使用不同的HTTP客户端。他们是:
1、apche HttpClient。这是默认的客户端。
2、Squareup OkHttpClientv3。这是通过com.squareup.okhttp3:okhttp在类路径和设置上的库启用的ribbon.okhttp.enabled=true。
3、Netflix功能区HTTP客户端。这是通过设置启用ribbon.restclient.enabled=true。这个客户端有一些限制,例如它不支持PATCH方法,但也有内置的重试。
3、 SimpleHostRoutingFilter :这个过滤器通过Apache HttpClient发送请求给预定的URL。在中找到网址 ·RequestContext.getRouteHost()。
关于自定义Zuul过滤器,参考链接:https://github.com/spring-cloud-samples/sample-zuul-filters
如何编写Pre过滤器:
前置过滤器用于设置RequestContext下游过滤器中的数据。主要的用例是设置路由过滤器所需的信息。例子如下:
public class QueryParamPreFilter extends ZuulFilter {
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
}
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
&& !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (request.getParameter("foo") != null) {
// put the serviceId in `RequestContext`
ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
}
return null;
}
}
上面的过滤器SERVICE_ID_KEY从foo请求参数中填充。实际上,做这种直接映射不是一个好主意,但是服务id应该从foo的值中查找。现在这SERVICE_ID_KEY是填充,PreDecorationFilter不会运行,并RibbonRoutingFilter会。如果想要转到完整的网址,请调用ctx.setRouteHost(url)。要修改路由过滤的路劲将转发的路径,请设置REQUEST_URI_KEY。
如何编写Route过滤器:
路由过滤器在预过滤之后运行,并用于向其他服务发出请求。这里的大部分工作是将请求和响应数据翻译到客户端所需的模型。例子如下:
public class OkHttpRoutingFilter extends ZuulFilter {
@Autowired
private ProxyRequestHelper helper;
@Override
public String filterType() {
return ROUTE_TYPE;
}
@Override
public int filterOrder() {
return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return RequestContext.getCurrentContext().getRouteHost() != null
&& RequestContext.getCurrentContext().sendZuulResponse();
}
@Override
public Object run() {
OkHttpClient httpClient = new OkHttpClient.Builder()
// customize
.build();
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String method = request.getMethod();
String uri = this.helper.buildZuulRequestURI(request);
Headers.Builder headers = new Headers.Builder();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
String value = values.nextElement();
headers.add(name, value);
}
}
InputStream inputStream = request.getInputStream();
RequestBody requestBody = null;
if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
MediaType mediaType = null;
if (headers.get("Content-Type") != null) {
mediaType = MediaType.parse(headers.get("Content-Type"));
}
requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
}
Request.Builder builder = new Request.Builder()
.headers(headers.build())
.url(uri)
.method(method, requestBody);
Response response = httpClient.newCall(builder.build()).execute();
LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
responseHeaders.put(entry.getKey(), entry.getValue());
}
this.helper.setResponse(response.code(), response.body().byteStream(),
responseHeaders);
context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
return null;
}
}
上面的过滤器将Servlet请求信息转换成OkHttp3请求信息,执行HTTP请求,然后将OkHttp3响应信息转换为Servlet响应。请注意:此过滤器可能有错误,无法正常工作。
如何编写Post过滤器?
后过滤器通常操纵响应。在下面的过滤器中,我们添加一个随机UUID作为X-Foo标题。其他操作(如转换响应主体)则要复杂得多,而且需要大量计算。例如如下:
public class AddResponseHeaderFilter extends ZuulFilter {
@Override
public String filterType() {
return POST_TYPE;
}
@Override
public int filterOrder() {
return SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse();
servletResponse.addHeader("X-Foo", UUID.randomUUID().toString());
return null;
}
}
Zuul错误如何工作的呢?如果在Zuul过滤器生命周期的任何部分中抛出异常,则会执行错误过滤器。SendErrorFilter只有在RequestContext.getThrowable()没有的情况下才运行null。然后javax.servlet.error.*在请求中设置特定的属性,并将请求转发到Spring Boot错误页面。
Zuul Eager应用上下文加载(懒加载问题)?
Zuul在内部使用Ribbon来调用远程URL,而Ribbon客户端默认在第一次调用时被Spring Cloud懒加载。使用以下配置可以为Zuul更改此行为,并且会导致在应用程序启动时急切地加载子功能区相关的应用程序上下文:
zuul:
ribbon:
eager-load:
enabled: true
如何使用Sidecar实现多语种支持?
如果没有非jvm语言情况下使用eureka、feign和configServer?Netflix Prana的灵感来源于Netflix的Spring Cloud Netflix 。它包含一个简单的http API来获取给定服务的所有实例(即主机和端口)。还可以通过嵌入式Zuul代理服务器来获取来自Eureka的路由条目。Spring Cloud Config Server可以通过主机查找或通过Zuul Proxy直接访问。非jvm应用程序应执行健康检查,以便Sidecar可以向应用程序启动或关闭时向eureka报告。集成Sidecar参考以下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
使用@EnableSidecar 启用Sidecar。源码如下:
package org.springframework.cloud.netflix.sidecar;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Import;
@EnableCircuitBreaker // 熔断器
@EnableZuulProxy // zuul代理网关
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(SidecarConfiguration.class)
public @interface EnableSidecar {} //实际却是没有其他内容的
可以在application.yml 中配置 sidecar.port和sidecar.health-uri 从而配置Sidecar。该sidecar.port属性是非jvm应用程序正在侦听的端口。这是Sidecar可以正确地向Eureka注册应用程序。这sidecar.health-uri 是一个可以在非jvm应用程序上使用的模拟Spring Boot健康指示器的uri。它应该返回一个像下面这样的json文件():
健康-URI情况如下:
{
“status”:“UP”
}
下面是一个Sidecar应用程序的示例application.yml:
server:
port: 5678
spring:
application:
name: sidecar
sidecar:
port: 8000
health-uri: http://localhost:8000/health.json
该DiscoveryClient.getInstances()方法的api 是/hosts/{serviceId}。这是一个示例响应/hosts/customers,返回不同主机上的两个实例。这个API是非jvm应用程序(如果Sidecar在端口5678)访问http://localhost:5678/hosts/{serviceId} , /hosts/customers结果如下:
[
{
"host": "myhost",
"port": 9000,
"uri": "http://myhost:9000",
"serviceId": "CUSTOMERS",
"secure": false
},
{
"host": "myhost2",
"port": 9000,
"uri": "http://myhost2:9000",
"serviceId": "CUSTOMERS",
"secure": false
}
]
Zuul代理自动为每个已知的eureka服务添加路由 /<serviceId>,所以客户服务可在/customers。非jvm应用程序可以通过http://localhost:5678/customers (假设Sidecar正在监听端口5678)访问客户服务。如果配置服务器向Eureka注册,则非jvm应用程序可以通过Zuul代理访问它。如果ConfigServer的serviceId是configserver ,并且Sidecar在端口5678上,那么可以通过http:// localhost:5678 / configserver。非jvm应用程序可以利用Config服务器返回YAML文档的能力。例如,对http://sidecar.local.spring.io:5678/configserver/default-master.yml的调用 可能会产生如下所示的YAML文档:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
password: password
info:
description: Spring Cloud Samples
url: https://github.com/spring-cloud-samples
21
参考文献
优步分布式追踪技术再度精进——http://www.infoq.com/cn/articles/evolving-distributed-tracing-at-uber-engineering
微服务架构下,如何实现分布式跟踪?——http://www.infoq.com/cn/articles/how-to-realize-distributed-tracking
什么是微服务——http://blog.csdn.net/wuxiaobingandbob/article/details/78642020?locationNum=1&fps=1
浅析深究什么是中间件——https://kb.cnblogs.com/page/196448/
SpringCloud官方文档——http://cloud.spring.io/spring-cloud-static/Edgware.RELEASE/single/spring-cloud.html
Hystrix的wiki文档——https://github.com/Netflix/Hystrix/wiki/Configuration
Turbine的github文档——https://github.com/Netflix/Turbine
Turbine的wiki文档——https://github.com/Netflix/Turbine/wiki
Ribbon的wiki文档——https://github.com/Netflix/ribbon/wiki/Working-with-load-balancers#components-of-load-balancer
Hystrix线程隔离技术解析-线程池——https://www.jianshu.com/p/df1525d58c20
Netflix开源类库archaius(一)概述——http://blog.csdn.net/zhangfb95/article/details/48297907
Zull的SpringCloud文档——https://cloud.spring.io/spring-cloud-static/Edgware.RELEASE/single/spring-cloud.html#_router_and_filter_zuul
Netflix/zuul的wiki文档——https://github.com/Netflix/zuul/wiki
Zuul如何工作的wiki文档——https://github.com/Netflix/zuul/wiki/How-it-Works
Spring Cloud Netflix的自定义Zuul 过滤器示例——https://github.com/spring-cloud-samples/sample-zuul-filters