在 Istio 服务网格内连接外部 MySQL 数据库
为了方便理解,以 Istio 官方提供的 Bookinfo 应用示例为例,利用 ratings 服务外部 MySQL 数据库。Bookinfo应用的架构图如下:其中,包含四个单独的微服务:productpage:调用 details 和 reviews 两个服务,用来生成页面。details:包含了书籍的信息。reviews:包含了书籍相关的评论。它还会调用 ratings 微服务。rating:包含了由书籍评价组成的评级信息。其中,reviews 服务有 3 个版本:v1 版本不会调用 ratings 服务。v2 版本会调用 ratings 服务,并使用 1 到 5 个黑色星形图标来显示评分信息。v3 版本会调用 ratings 服务,并使用 1 到 5 个红色星形图标来显示评分信息。准备 MySQL 数据库创建一个名为 test 数据库,执行以下SQL创建表和数据:DROP TABLE IF EXISTS `ratings`;
CREATE TABLE `ratings` (
`ReviewID` int(11) NOT NULL,
`Rating` int(11) NULL DEFAULT 0,
PRIMARY KEY (`ReviewID`) USING BTREE
) ENGINE = InnoDB;
INSERT INTO ratings (ReviewID, Rating) VALUES (1, 2);
INSERT INTO ratings (ReviewID, Rating) VALUES (2, 4);创建ServiceEntry执行以下命令创建ServiceEntry:kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: mysqldb
spec:
hosts:
- mysqldb.svc.remote
ports:
- number: 3306
name: mysql
protocol: MySQL
location: MESH_EXTERNAL
resolution: STATIC
endpoints:
- address: 192.168.1.116
ports:
mysql: 3306
EOF其中,192.168.1.116是 MySQL 数据库的IP,3306是 MySQL 数据库的端口。创建ratings服务首先,执行以下命令,获取密码的Base64编码:echo -n 'OneMoreSociety' | base64其中,OneMoreSociety是连接 MySQL 数据库的密码。然后,执行以下命令,创建 ratings 服务:kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: mysql-credentials
type: Opaque
data:
dbpasswd: T25lTW9yZVNvY2lldHk=
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ratings-v2-mysql
labels:
app: ratings
version: v2-mysql
spec:
replicas: 1
selector:
matchLabels:
app: ratings
version: v2-mysql
template:
metadata:
labels:
app: ratings
version: v2-mysql
spec:
containers:
- name: ratings
image: docker.io/istio/examples-bookinfo-ratings-v2:1.16.2
imagePullPolicy: IfNotPresent
env:
- name: DB_TYPE
value: "mysql"
- name: MYSQL_DB_HOST
value: mysqldb.svc.remote
- name: MYSQL_DB_PORT
value: "3306"
- name: MYSQL_DB_USER
value: root
- name: MYSQL_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-credentials
key: dbpasswd
ports:
- containerPort: 9080
securityContext:
runAsUser: 1000
EOF其中,T25lTW9yZVNvY2lldHk=是连接 MySQL 数据库的密码的Base64编码。修改路由规则执行以下命令,把对 reviews 服务的调用全部路由到 v2 版本上:kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v2
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
subsets:
- labels:
version: v1
name: v1
- labels:
version: v2
name: v2
- labels:
version: v3
name: v3
EOF执行以下命令,把对 ratings 服务的调用全部路由到 v2-mysql 版本上:kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: ratings
spec:
hosts:
- ratings
http:
- route:
- destination:
host: ratings
subset: v2-mysql
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: ratings
spec:
host: ratings
subsets:
- labels:
version: v1
name: v1
- labels:
version: v2-mysql
name: v2-mysql
EOF效果访问 productpage 页面,可以看到 Reviewer1 显示2星, Reviewer2 显示4星,和数据库中的数据一致,如下图:在Kiali中也可以看到对应的拓扑结构,如下图:流量转移访问 MySQL 数据库时,所有流量都路由到v1版本,具体配置如下:kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: mysqldb
spec:
hosts:
- mysqldb.svc.remote
ports:
- number: 3306
name: tcp
protocol: TCP
location: MESH_EXTERNAL
resolution: STATIC
endpoints:
- address: 192.168.1.116
ports:
tcp: 3306
labels:
version: v1
- address: 192.168.1.118
ports:
tcp: 3306
labels:
version: v2
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: mysqldb
spec:
hosts:
- mysqldb.svc.remote
tcp:
- route:
- destination:
host: mysqldb.svc.remote
subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: mysqldb
spec:
host: mysqldb.svc.remote
subsets:
- labels:
version: v1
name: v1
- labels:
version: v2
name: v2
EOF访问 MySQL 数据库时,把50%流量转移到v2版本,具体配置如下:kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: mysqldb
spec:
hosts:
- mysqldb.svc.remote
tcp:
- route:
- destination:
host: mysqldb.svc.remote
subset: v1
weight: 50
- destination:
host: mysqldb.svc.remote
subset: v2
weight: 50
EOF访问 MySQL 数据库时,所有流量都路由到v2版本,具体配置如下:kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: mysqldb
spec:
hosts:
- mysqldb.svc.remote
tcp:
- route:
- destination:
host: mysqldb.svc.remote
subset: v2
EOF最后,感谢你这么帅,还给我点赞。
云原生系列六:容器和Docker
最近云原生领域热火朝天,那么云原生是什么?何为云原生?云原生用来干什么的?今天学长带领大家走进云原生时代~~何为云?技术的变革,一定是思想先行,云原生是一种构建和运行应用程序的方法,是一套技术体系和方法论。云原生(CloudNative)是一个组合词,Cloud+Native。Cloud表示应用程序位于云中,而不是传统的数据中心;Native表示应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳姿势运行,充分利用和发挥云平台的弹性+分布式优势。叶秋学长理解的云原生就是用来降本增效的,如下图:编辑云原生的应用程序是什么?叶秋学长认为符合云原生架构的应用程序应该是:采用开源堆栈(K8S+Docker)进行容器化,基于微服务架构提高灵活性和可维护性,借助敏捷方法、DevOps支持持续迭代和运维自动化,利用云平台设施实现弹性伸缩、动态调度、优化资源利用率。容器和Docker是什么?虚拟化与容器在容器技术之前,业界的网红是虚拟机。虚拟机技术的代表是VMware和OpenStack。很多人都用过虚拟机,就是在操作系统里安装一个软件,然后通过这个软件,再模拟一台甚至多台“子电脑”出来。在“子电脑”里,可以和正常电脑一样运行程序,例如微信、Word。“子电脑”和“子电脑”之间,相互隔离互不影响。虚拟机虽然可以隔离出很多“子电脑”,但占用空间大,启动慢,虚拟机软件可能还要花钱(例如VMware)。而容器技术恰好没有这些缺点,它不需要虚拟出整个操作系统,只需要虚拟一个小规模的环境(类似“沙箱”),启动时间很快,几秒钟就能完成。而且,它对资源的利用率很高(一台主机可以同时运行几千个Docker容器)。此外它占的空间很小,虚拟机一般要几GB到几十GB的空间,而容器只需要MB级甚至KB级。虚拟机和以Docker为代表的容器都是虚拟化技术,不过容器属于轻量级的虚拟化。下面是两者的主要对比。编辑 Docker的源起我们再来看看Docker,Docker本身并不是容器,它是创建容器的工具,是应用容器引擎。虽然Docker 把容器技术推向了巅峰,但容器技术却不是Docker发明的。实际上,容器技术连新技术都算不上,因为它的诞生和使用有些年头了,像最早的容器LXC发布于2008年。Docker本来是做PaaS的公司,原来叫做DotCloud,成立于2010年。但比起Pivotal、Red Hat等著名企业,DotCloud运营并不成功。眼看就要失败的时候,2013年DotCloud决定开源自己的容器项目Docker。但是短短几个月,Docker迅速崛起,吸引大量的开发者使用。随着Docker在开发者中越来越流行,2013年10月,DotCloud公司正式更名为Docker,2014年8月,Docker 宣布把PaaS业务出售,开始专心致志做Docker。Docker一词意为码头工人,而它的logo则是一个托着许多集装箱的鲸鱼,非常形象:Docker是鲸鱼,而集装箱则是一个个的容器。在Docker的官网上,对于容器有一个一句话的解释“A standardized unit of software”,即“软件的一个标准化单元”。Docker的核心概念Docker技术的三大核心概念,分别是:• 镜像(Image)• 容器(Container)• 仓库(Repository)上面的例子里,设计出来的模板就是Docker镜像,生产(复制)出来的构件就是Docker容器,而Docker仓库则是集中放置管理Docker镜像的地方。Docker镜像是一个特殊的文件系统。它除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的配置参数(例如环境变量)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。每一种模板(镜像)能够创建出一种构件,但是模板可以由不同的设计师来设计,提供不同用途、不同风格,例如斜顶式阳台、嵌入式阳台、包豪斯风格、蒙德里安风格等等,所有人相互之间可以共享,这就形成了大的公共仓库。Docker官方提供了Docker Hub来维护管理所有的镜像,只是对于免费用户而言,只能创建一个私有仓库。Docker Hub里提供了大量高质量的官方镜像,例如Oracle、MySQL、redis、Ubuntu、Nginx、python、Docker(Docker in Docker!)等等,开发人员需要一个环境的时候,可以直接到Docker镜像仓库去查找,减少了大量无谓的环境安装工作。Docker的好处Docker给我们带来的好处非常多,下面简单列举几点:• 更高效的利用系统资源有了Docker,我们可以在一台服务器上运行很多应用,充分利用硬件资源。例如现在我们有一台Linux服务器,可以构建不同版本的Ubuntu镜像启动,并且为不同的用户分配不同的容器。这样用一台服务器就能虚拟出许多运行不同操作系统的虚拟服务器,而对于用户来说,这些都是透明的。许多公有云采用了容器技术为用户提供服务,所以虚拟化与容器共同成为了现代云计算的基石。• 更快速的启动时间传统的虚拟机技术启动应用服务往往需要数分钟,而Docker容器应用,由于直接运行于宿主内核,无须启动完整的操作系统,因此可以做到秒级甚至毫秒级的启动时间,大大的节约了开发、测试、部署的时间。• 保证环境一致性开发过程中常见的问题之一是环境一致性问题,由于开发环境、测试环境、生产环境不一致,导致有些bug并未在开发过程中被发现,而Docker的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,再也不会有在线下开发环境中运行正常,而部署到线上有各种错误的情况了。• 持续交付和部署对于开发和运维人员来说,最希望的是一次创建或配置,可以在任意地方正常运行。开发者可以使用一个标准的镜像来构建一套开发容器,开发完成之后,运维人员可以直接使用这个容器来部署代码,无论在多少台服务器中部署都是如此。Docker可以快速创建容器,快速迭代应用程序,并让整个过程全程可见。• 更轻松的迁移由于Docker确保了执行环境的一致性,使得应用的迁移更加容易,Docker可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,其运行结果是一致的,因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。• 提升复用性,降低耦合性,维护和扩展更轻松Docker使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。安装Docker后,我们可以从Docker Hub上获取各种各样的操作系统镜像,这个操作很简单,只需要拉取相应的镜像到本地然后运行即可。另外我们可以将数据库、Web服务器、缓存服务器运行在不同的容器中,降低了各个服务之间的耦合性、便于扩展,Docker Hub上有各种各样的优秀镜像,我们可以直接拿来使用,不需要自己搭建,应用的部署就像搭积木一样简单。• 实现沙盒机制,提高了安全性由于应用运行在容器中,与操作系统隔离开,从而使操作系统基本不可能受到破坏。另外如果应用因为攻击而瘫痪,并不需要重启服务器,直接重启容器或者再启动一个镜像就可以了。容器与微服务容器是微服务和云原生架构的最佳实现载体。微服务与容器几乎是完美的搭配。单体式架构(Monolithic)变成微服务架构(Microservices),相当于一个全能型变成N个专能型,每个专能型分配一个隔离的容器,赋予了最大程度的灵活。编辑本期分享到此为止,关注博主不迷路,叶秋学长带你上高速~~~
语音交友系统开发,服务当拆则拆不受其乱
单体架构和微服务架构是语音交友系统开发常用的两种架构类型,在系统运行初期其实应该是以单体架构为先的,但由于用户数量的不断累积,数据量会大幅度增加,单体架构便逐渐丧失其优势,直至无法支撑系统的正常运行。这时微服务架构便更具优势,要想在语音交友系统开发中实现微服务架构就得对服务进行拆分,服务拆分应该逐步进行、持续演进,当拆则拆才能不受其乱。在语音交友系统开发中实现服务拆分需要遵循一些原则。一、闭包原则在语音交友系统开发中拆分后的各个微服务应该实现闭包性,当对某一微服务进行修改时,不需要且不会影响到其他微服务。二、单一服务内部功能高内聚低耦合拆分后的各个微服务仅需完整自身职责内的任务,对于不在自身职责内的任务则不进行处理,而是交由其他对应的服务器进行任务处理。三、避免环形依赖与双向依赖如果在拆分后的微服务中存在环形依赖或双向依赖,则证明服务拆分不明确,各个功能的边界没能实现清晰地划分。四、服务接口的定义要具备可扩展性在语音交友系统开发中为保证服务间跨进程网络通信的实现,需要让服务接口的定义具备可扩展性。五、服务自理、接口隔离原则服务拆分后应该尽可能消除对其他服务的强依赖,这样才能提升系统的稳定性,即便要对接其他服务也应该实现标准的接口隔离,这样才能实现语音交友系统开发以服务为单位持续交付。六、持续演进原则由于语音交友系统开发并不是一成不变的,所以微服务的拆分也并不是一次性完成的,为了实现更好的拆分效果,应该保证服务拆分的持续演进性,避免服务数量的骤然增加。七、阶段性合并在语音交友系统开发中,如果服务拆分边界由于某种原因出现了不清晰的情况,则需要重新梳理该服务的领域边界并进行纠正,以保证服务拆分的合理性。在语音交友系统开发中,微服务拆分并不是随时都可以的,需要考虑合适的时机点,而且服务拆分的落地还需要提前准备好配套的基础设置,这样才能实现更好地服务拆分效果。关于语音交友系统开发,还有很多需要我们了解和掌握的知识点。声明:本文由云豹科技原创,转载请注明作者名及原文链接,否则视为侵权
Nacos配置中心之动态感知
Nacos配置中心之动态感知我们通过控制台或API的方式修改了配置后,如何实时通知?这里利用到了事件处理机制,配置文件被修改后调用了nacos服务端的/v1/cs/configs接口,接口中发布了ConfigDataChangeEvent事件LongPollingService 监听事件类LongPollingService继承AbstractEventListener,AbstractEventListener是事件抽象类,它有一个onEvent抽象方法,而LongPollingService实现了这个方法@Override
public void onEvent(Event event) {
if (isFixedPolling()) {
// ignore
} else {
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}LongPollingService可以看到LocalDataChangeEvent事件,这个事件是服务端的配置数据发生变化时发布的一个事件。onEvent方法中通过线程池来执行一个DataChangeTask任务DataChangeTask线程DataChangeTask是一个线程,实现了Runnable接口,对应的run()如下:class DataChangeTask implements Runnable {
@Override
public void run() {
try {
ConfigService.getContentBetaMd5(groupKey);
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
if (clientSub.clientMd5Map.containsKey(groupKey)) {
// 如果beta发布且不在beta列表直接跳过
if (isBeta && !betaIps.contains(clientSub.ip)) {
continue;
}
// 如果tag发布且不在tag列表直接跳过
if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
continue;
}
getRetainIps().put(clientSub.ip, System.currentTimeMillis());
iter.remove(); // 删除订阅关系
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
(System.currentTimeMillis() - changeTime),
"in-advance",
RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),
"polling",
clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
} catch (Throwable t) {
LogUtil.defaultLog.error("data change error:" + t.getMessage(), t.getCause());
}
}
DataChangeTask(String groupKey) {
this(groupKey, false, null);
}
DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps) {
this(groupKey, isBeta, betaIps, null);
}
DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps, String tag) {
this.groupKey = groupKey;
this.isBeta = isBeta;
this.betaIps = betaIps;
this.tag = tag;
}
final String groupKey;
final long changeTime = System.currentTimeMillis();
final boolean isBeta;
final List<String> betaIps;
final String tag;
}遍历allSubs中的客户端长轮询请求。比较每个客户端长轮询请求携带的groupKey,如果服务端变更的配置和客户端请求关注的配置一致,则直接返回,调用clientSub.sendResponse()方法将变更信息返回总结现在大家明白为什么nacos会进行实时动态感知,大体流程就是当在配置中心修改配置后,会发布事件ConfigDataChangeEvent,EventDispatcher触发事件,通知监听者,LongPollingService就是监听者之一,它要做的操作就是通过线程池开启定时线程,线程中遍历客户端的所有长轮询的请求,对比客户端携带的groupKey,与服务端变更的groupKey就返回客户端变更信息,从而实现客户端的动态感知这就是Nacos配置中心的动态感知的原理,基于事件监听机制实现,大家知道了吗,喜欢的朋友们欢迎在评论区留言或提出不同意见。这样的话我们对nacos源码的解读差不多就到这里了,包括它的两大模块,注册中心和配置中心,后续我们讲解一下RocketMQ的知识,RocketMQ在如今微服务畅行的环境之下也是一个非常重要的技术点,接下来跟着我一起学习吧~
Spring Cloud Zuul 基础搭建
Spring Cloud Zuul API服务网关一、Zuul 介绍二、构建Spring Cloud Zuul网关构建网关请求路由传统路由方式面向服务的路由请求过滤一、Zuul 介绍通过前几篇文章的介绍,我们了解了Spring Cloud Eureka 如何搭建注册中心,Spring Cloud Ribbon 如何做负载均衡,Spring Cloud Hystrix 断路器如何保护我们的服务,以防止雪崩效应的出现,Spring Cloud Feign进行声明式服务调用都有哪些应用,相比Ribbon和Hystrix都有哪些改善。可以说,以上几个组件都是搭建一套微服务架构所必须的。通过以上思路,能够梳理出下面这种基础架构: 无服务网关的架构图在此架构中,我们的服务集群是内部ServiceA 和 ServiceB,他们都会向Eureka Server集群进行注册与订阅服务。而OpenService是一个对外的Restful API 服务,它通过F5,Nginx等网络设备或工具软件实现对各个微服务的路由与负载,公开给外部客户端调用那么上述的架构存在什么问题呢?从运维的角度来看,当客户端单机某个功能的时候往往会发出一些请求到后端,这些请求通过F5,Nginx等设施的路由和负载均衡分配后,被转发到各个不同的实例上,而为了让这些设施能够正确的路由与分发请求,运维人员需要手动维护这些实例列表,当系统规模增大的时候,这些看似简单的维护回变得越来越不可取。从开发的角度来看,为了保证服务的安全性,我们需要在调用内部接口的时候,加一层过滤的功能,比如权限的校验,用户登陆状态的校验等;同时为了防止客户端在请求时被篡改等安全方面的考虑,还会有一些签名机制的存在。正是由于上述架构存在的问题,API网关被提出,API网关更像是一个智能的应用服务器,它的定义类似于设计模式中的外观模式,它就像是一个门面的角色,结婚时候女方亲属堵门时候的角色,我去参加婚礼当伴郎的时候去村子里面见新娘,女方亲属会把鞋子藏起来,有可能藏在屋子里有可能藏在身上,这得需要你自己去寻找,找到了鞋子之后,你才能够给新娘穿上才能正式的会见家长。API网关真正实现的功能有请求路由,负载均衡,校验过滤,请求转发的熔断机制,服务的聚合等一系列功能。 Spring Cloud Zuul通过与Spring Cloud Euerka进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有的微服务的实例信息。者可以通过使用Zuul来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口。下面我们就来搭建一下Spring Cloud Zuul服务网关二、构建Spring Cloud Zuul网关下面我们就来实际搭建一下Zuul网关,来体会一下网关实际的用处构建网关在实现各种API网关服务的高级功能之前,我们先来启动一下前几章搭建好的服务server-provider,feign-consumer,eureka-server,虽然之前我们一直将feign-consumer视为消费者,但是在实际情况下,每个服务既时服务消费者,也是服务提供者,之前我们访问的http://localhost:9001/feign-consumer等一系列接口就是它提供的服务。这里就来介绍一下详细的构建过程创建一个Spring Boot功能,命名为api-gateway,并在Pom.xml文件中引入如下内容<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.api.gateway</groupId>
<artifactId>api-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>api-gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>对于spring-cloud-starter-zuul 依赖,可以通过查看依赖配置了解到,它不仅包含了Netflix Zuul的核心依赖zuul-core,还包括了下面这些网关的重要依赖spring-cloud-starter-hystrix: 该依赖用在网关服务中实现对微服务转发时候的保护机制,通过线程隔离和断路器,防止因为微服务故障引发的雪崩效应spring-cloud-starter-ribbon: 该依赖用在实现网关服务进行负载均衡和请求重试spring-cloud-starter-actuactor: 该依赖用来提供常规的微服务管理端点。另外,Spring Cloud Zuul 中还特别提供了/routes端点来返回当前的路由规则在ApiGatewayApplication 主入口中添加@EnableZuulProxy注解开启服务网关功能@EnableZuulProxy @SpringBootApplication public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } }在application.properties 中配置Zuul应用的基础信息,包括应用名,端口号,具体如下spring.application.name=api-gateway server.port=5555请求路由下面,我们通过一个简单的示例来为上面构建的网关增加请求路由的功能,为了演示请求路由的功能,我们先将之前的Eureka服务注册中心和微服务应用都启动起来。观察下面的服务列表,可以看到两个微服务应用已经注册成功了传统路由方式使用Spring Cloud Zuul实现路由功能非常简单,只需要对api-gateway服务增加一些关于路由的配置规则,就能实现传统路由方式zuul.routes.api-a-url.path=/api-a-url/** # 映射具体的url路径 zuul.routes.api-a-url.url=http://localhost:8080/该配置定义了发往API网关服务的请求中,所有符合/api-a-url/** 规则的访问都将被路由转发到 http://localhost:8080 的地址上,也就是说,当我们访问http://localhost:5555/api-a-url/hello 的时候,API网关服务会将该请求路由到http://localhost:8080/hello 提供的微服务接口中。其中,配置属性zuul.routes.api-a-url.path 中的api-a-url部分为路由的名字,可以任意定义,但是一组path和url映射关系的路由名要相同面向服务的路由很显然,传统的配置方式对我们来说并不友好,他同样需要运维人员花费大量的时间维护各个路由path 和url的关系。为了解决这个问题,Spring Cloud Zuul实现了与Spring Cloud Eureka的无缝衔接,我们可以让路由的path不是映射具体的url,而是让它映射到具体的服务,而具体的url则交给Eureka的服务发现机制去自动维护为了实现与Eureka的整合,我们需要在api-gateway的pom.xml中引入spring-cloud-starter-eureka依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>在api-gateway服务中对应的application.properties文件中加入如下代码zuul.routes.api-a.path=/api-a/** zuul.routes.api-a.serviceId=server-provider zuul.routes.api-b.path=/api-b/** zuul.routes.api-b.serviceId=feign-consumer eureka.client.service-url.defaultZone=http://localhost:1111/eureka/针对我们之前准备的两个微服务应用server-provider和feign-consumer,在上面的配置中分别定义了api-a 和 api-b 的路由来映射它们。然后这个api-gateway的默认注册中心是默认注册中心地址完成上述配置后,我们可以将四个服务启动起来,分别是eureka-server, server-provider, feign-consumer, api-gateway服务,启动完毕,会在eureka-server信息面板中看到多了一个api-gateway网关服务。http://localhost:5555/api-a/hello: 这个接口符合 /api-a/的规则,由api-a 路由负责转发,该路由映射的serviceId 为 server-provider,所以最终/hello请求会被发送到server-provider服务的某个实例上去http://localhost:9001/api-b/feign-consumer: 这个接口符合 /api-b/的规则,由api-b 进行路由转发,实际的地址由Eureka负责映射,该路由的serviceId是feign-consumer, 所以最终 /feign-consumer 请求会被路由到 feign-consumer 服务上。请求过滤在实现了请求路由功能之后,我们的微服务应用提供的接口就可以通过统一的API网关入口被客户端访问到了,但是,每个客户端用户请求微服务应用提供的接口时,它们的访问权限往往都有一定限制。为了实现客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。但是,这样的方法并不可取,因为同一个系统中会有很多校验逻辑相同的情况,最好的方法是将这些校验逻辑剥离出去,构成一个独立的服务。对于上面这种问题,更好的做法是通过前置的网关服务来完成非业务性质的校验。为了在API网关中实现对客户端请求的校验,我们将继续介绍Spring Cloud Zuul的另外一个核心功能:请求过滤,实现方法比较简单,我们只需要继承ZuulFilter抽象类并实现它定义的4个抽象函数即可下面的代码定义了一个简单的Zuul过滤器,它实现了在请求被路由之前检查HttpServletRequest中是否带有accessToken参数public class AccessFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(AccessFilter.class); /** * 过滤器的执行时序 * @return */ @Override public String filterType() { return "pre"; } /** * 过滤器的执行顺序 * @return */ @Override public int filterOrder() { return 0; } /** * 判断过滤器是否应该执行 * @return */ @Override public boolean shouldFilter() { return true; } /** * 过滤器的具体执行逻辑 * @return */ @Override public Object run() { RequestContext rc = RequestContext.getCurrentContext(); HttpServletRequest request = rc.getRequest(); log.info("send {} request to {}", request.getMethod(),request.getRequestURL().toString()); String accessToken = request.getParameter("accessToken"); if(null == accessToken){ log.warn("access token is null"); rc.setResponseStatusCode(401); rc.setSendZuulResponse(false); } log.info("access token ok"); return null; } }在上面实现的过滤器代码中,我们通过继承ZuulFilter 抽象类并重写了四个方法filterType : 过滤器类型,它决定过滤器的请求在哪个生命周期中执行,这里定义为pre,意思是在请求前执行filterOrder : 过滤器的执行顺序,当请求在一个阶段存在多个过滤器时,需要根据方法的返回值来判断过滤器的执行顺序shouldFilter: 过滤器是否需要执行,这里直接返回true,因为该过滤器对所有的请求都生效run: 过滤器的具体逻辑,这里我们通过rc.setResponseStatusCode(401)设置失效的标志,rc.setSendZuulResponse(false)令Zuul过滤该请求在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的Bean才能启动该过滤器。@EnableZuulProxy @SpringBootApplication public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } @Bean public AccessFilter filter(){ return new AccessFilter(); } }在对api-gateway服务完成了上面的改造之后,我们可以重新启动它,并发起下面的请求,对上面的过滤器做一个验证输入 http://localhost:5555/api-a/hello : 返回 401错误输入 http://localhost:5555/api-a/hello?accessToken=token,正确路由到server-provider的/hello 接口,并返回Hello World。到这里,对于API网关的快速入门示例就搭建完成了,通过对Spring Cloud Zuul 网关的搭建,我们能认知到网关的重要性,可以总结如下:它作为系统的统一入口, 屏蔽了系统内部各个微服务的细节。它可以与服务治理框架结合,实现自动化的服务实例维护以及负载均衡的路由转发。它可以实现接口权限校验与微服务业务逻辑的解耦。通过服务网关中的过滤器, 在各生命周期中去校验请求的内容, 将原本在对外服务层做的校验前移, 保证了微服务的无状态性, 同时降低了微服务的测试难度, 让服务本身更集中关注业务逻辑的处理。 </div>
Spring Cloud Feign 声明式服务调用
一、Feign是什么?二、Feign的快速搭建三、Feign的几种姿态 参数绑定 继承特性四、其他配置 Ribbon 配置 Hystrix 配置一、Feign是什么?通过对前面Spring Cloud Ribbon和 Spring Cloud Hystrix ,我们已经掌握了开发微服务应用时的两个重磅武器,学会了如何在微服务框架中进行服务间的调用和如何使用断路器来保护我们的服务,这两者被作为基础工具类框架广泛的应用在各个微服务框架中。既然这两个组件这么重要,那么有没有更高层次的封装来整合这两个工具以简化开发呢?Spring Cloud Feign就是这样的一个工具,它整合了Spring Cloud Ribbon 和 Spring Cloud Hystrix 来达到简化开发的目的。我们在使用Spring Cloud Ribbon时,通常都会使用RestTemplate的请求拦截来实现对依赖服务的接口调用,而RestTemplate已经实现了对Http请求的封装,形成了一套模板化的调用方法。在之前Ribbon的例子中,我们都是一个接口对应一个服务调用的url,那么在实际项目开发过程中,一个url可能会被复用,也就是说,一个接口可能会被多次调用,所以有必要把复用的接口封装起来公共调用。Spring Cloud Feign在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务的接口定义。二、Feign的快速搭建我们通过一个示例来看一下Feign的调用过程,下面的示例将继续使用之前的server-provider服务,这里我们通过Spring Cloud Feign提供的声明式服务绑定功能来实现对该服务接口的调用首先,搭建一个SpringBoot项目,取名为feign-consumer,并在pom.xml文件中引入spring-cloud-starter-eureka和spring-cloud-starter-feignn依赖,具体内容如下:<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.feign.consumer</groupId>
<artifactId>feign-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>feign-consumer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>搭建完成pom.xml之后,我们在feign-consumer的启动类上添加如下注解@EnableDiscoveryClient @EnableFeignClients @SpringBootApplication public class FeignConsumerApplication { public static void main(String[] args) { SpringApplication.run(FeignConsumerApplication.class, args); } }@EnableDiscoveryClient : 这个注解和@EnableEurekaClient 用法相同,表明这是一个Eureka客户端@EnableFeignClients : 这个注解表明这个服务是一个Feign服务,能够使用@FeignClient 实现远程调用新建一个HelloService接口,在接口上加上@FeignClient注解,表明这个接口是可以进行远程访问的,也表明这个接口可以实现复用的接口,它提供了一些远程调用的方法,也相当于制定了一些规则。// 此处填写的是服务的名称 @FeignClient(value = "server-provider") public interface HelloService { @RequestMapping(value = "hello") String hello(); }@FeignClient 后面的value值指向的是提供服务的服务名,这样就能够对spring.application.name = server.provider 的服务发起服务调用新建一个Controller,提供外界访问的入口,调用HelloService,完成一系列的服务请求-服务分发-服务调用@RestController public class ConsumerController { @Autowired HelloService helloService; @RequestMapping(value = "/feign-consumer", method = RequestMethod.GET) public String helloConsumer(){ return helloService.hello(); } }最后,为feign-consumer指定服务的端口号,服务的名称,并向注册中心注册自己spring.application.name=feign-consumerserver.port=9001eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/测试验证:像之前一样,启动四个服务: eureka-server, server-provider(8081,8082), feign-consumer,启动http://localhost:9000/eureka/ 主页,发现主页上注册了四个服务访问http://localhost:9001/feign-consumer 端口,发现 "Hello World" 能够返回三、Feign的几种姿态参数绑定在上一节的事例中,我们使用Spring Cloud Feign搭建了一个简单的服务调用的示例,但是实际的业务场景中要比它复杂很多,我们会在HTTP的各个位置传入不同类型的参数,并且返回的也是一个复杂的对象结构,下面就来看一下不同的参数绑定方法首先扩展一下server-provider中HelloController的内容@RequestMapping(value = "/hello1", method = RequestMethod.GET) public String hello1(@RequestParam String name){ return "Hello " + name; } @RequestMapping(value = "/hello2", method = RequestMethod.GET) public User hello2(@RequestHeader Integer id,@RequestHeader String name){ return new User(id,name); } @RequestMapping(value = "/hello3",method = RequestMethod.POST) public String hello3(@RequestBody User user){ return "Hello " + user.getId() + ", " + user.getName(); }User 对象的定义入下,省略了get和set方法,需要注意的是,这里必须要有User的默认构造函数,否则反序列化的时候,会报Json解析异常public class User { private Integer id; private String name; public User(){} public User(Integer id, String name) { this.id = id; this.name = name; } get and set...}在feign-consumer中的HelloService中声明对服务提供者的调用@FeignClient(value = "server-provider") public interface HelloService { @RequestMapping(value = "hello") String hello(); @RequestMapping(value = "/hello1", method = RequestMethod.GET) String hello1(@RequestParam("name") String name); @RequestMapping(value = "/hello2", method = RequestMethod.GET) User hello2(@RequestHeader("id") Integer id,@RequestHeader("name") String name); @RequestMapping(value = "/hello3", method = RequestMethod.POST) String hello3(@RequestBody User user); }hello1 方法传递了一个参数为name的请求参数,它对应远程调用server-provider服务中的hello1方法hello2 方法传递了一个请求头尾id 和 name的参数,对应远程调用server-provider服务中的hello2方法hello3 方法传递了一个请求体为user的参数,对应远程调用呢server-provider服务中的hello3方法下面在ConsumerController类中定义一个helloConsumer1的方法,分别对hello1,hello2,hello3方法进行服务调用@RestController public class ConsumerController { @Autowired HelloService helloService; @RequestMapping(value = "/feign-consumer", method = RequestMethod.GET) public String helloConsumer(){ return helloService.hello(); } @RequestMapping(value = "/feign-consumer2", method = RequestMethod.GET) public String helloConsumer1(String name){ StringBuilder builder = new StringBuilder(); builder.append(helloService.hello()).append("\n"); builder.append(helloService.hello1("lx")).append("\n"); builder.append(helloService.hello2(23,"lx")).append("\n"); builder.append(helloService.hello3(new User(24,"lx"))).append("\n"); return builder.toString(); } }上面的helloConsumer1方法,分别调用了HelloServcie接口中的hello、hello1、hello2、hello3方法,传递对应的参数,然后对每一个方法进行换行测试验证在完成上述的改造之后,启动服务注册中心、两个 server-provider 服务以及我们改造过的feign-consumer。通 过发送GET请求到, 触发 HelloService对新增接口的调用。最终,我们会获得如下输出,代表接口绑定和调用成功。继承特性通过上述的示例,我们能够发现能够从服务提供方的Controller中依靠复制操作,构建出相应的服务客户端绑定接口。既然存在很多复制操作,我们自然考虑能否把公用的接口抽象出来?事实上也是可以的,Spring Cloud Feign提供了通过继承来实现Rest接口的复用,下面就来演示一下具体的操作过程首先为了演示Spring Cloud Feign的继承特性,我们新建一个maven 项目,名为feign-service-api,我们需要用到Spring MVC的注解,所以在pom.xml 中引入spring-boot-starter-web依赖,具体内容如下:<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.feign</groupId> <artifactId>feign-service-api</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.7.RELEASE</version> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies></project>将User 对象复制到feign-service-api 中,如下public class User { private Integer id; private String name; // 必须加上 public User(){} public User(Integer id, String name) { this.id = id; this.name = name; } get and set... }创建HelloService接口,并在接口中定义如下三个方法:@RequestMapping(value = "/refactor") public interface HelloService { @RequestMapping(value = "/hello4", method = RequestMethod.GET) String hello(@RequestParam("name")String name); @RequestMapping(value = "/hello5", method = RequestMethod.GET) User hello(@RequestHeader("id")Integer id,@RequestHeader("name")String name); @RequestMapping(value = "/hello6", method = RequestMethod.POST) String hello(@RequestBody User user); }定义完成后,使用idea 右侧的maven 工具,依次执行mvn clean ,mvn install,把feign-service-api打成jar包之后,现在切换项目至 server-provider ,并让server-provider依赖这个maven项目server-providerserver-provider 的pom.xml 添加 feign-service-api打包后的依赖<dependency> <groupId>com.feign</groupId> <artifactId>feign-service-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>创建RefactorHelloController 实现feign-service-api中的HelloService 方法@RestController public class RefactorHelloController implements HelloService { // 注解没有带过来,这是自己加的 @Override public String hello(@RequestParam("name") String name) { return "Hello " + name; } @Override public User hello(@RequestHeader("id") Integer id, @RequestHeader("name") String name) { return new User(id,name); } @Override public String hello(@RequestBody User user) { return "Hello " + user.getId() + ", " + user.getName(); } }这里有一个问题,当继承了HelloService 之后,@RestController,@RequestParam,@RequestHeader,@RequestBody 注解都没有带过来, 但是书上说是只有 @RestController 注解是带不过来的,余下三个都是可以的。这里未查明是何原因 ……feign-consumer在完成了对server-provoder的构建之后,下面来构建feign-consumer服务,像server-provider 一样,在pom.xml 中添加对feign-service-api的依赖<dependency> <groupId>com.feign</groupId> <artifactId>feign-service-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>创建RefactorHelloService 接口,继承feign-service-api中的 HelloService接口@FeignClient(value = "server-provider") public interface RefactorHelloService extends HelloService {}在ConsumerController类注入 RefactorHelloService,并测试 feign-service-api 中的方法,远程调用server-provider 中的 /hello4 /hello5 /hello6方法。@RequestMapping(value = "/feign-consumer3", method = RequestMethod.GET) public String helloConsumer3(String name){ StringBuilder builder = new StringBuilder(); builder.append(refactorHelloService.hello("lx")).append("\n"); builder.append(refactorHelloService.hello(new com.feignservice.api.User(24,"lx"))).append("\n"); builder.append(refactorHelloService.hello(23,"lx")).append("\n"); return builder.toString(); }测试验证依次启动服务服务注册中心,server-provider的两个实例,feign-consumer服务,在http://localhost:1111/ 主页能够发现如下几个服务访问 http://localhost:9001/feign-consumer3( 使用Postman 访问),发现能够显示出来如下内容Hello lxHello 24, lxcom.feignservice.api.User@5865261优点和缺点使用Spring Cloud Feign的优点很多,可以将接口的定义从Controller 中剥离,同时配合Maven 构建就能轻易的实现接口的复用,实现在构建期的接口绑定,从而有效的减少服务客户端的绑定配置。但是这种配置使用不当也会带来副作用就是:你不能忽略频繁变更接口带来的影响。所以,如果团队打算采用这种方式来构建项目的话,最好在开发期间就严格遵守面向对象的开闭原则。避免牵一发而动全身,造成不必要的维护量。四、其他配置Ribbon 配置由于Spring Cloud Feign的客户端负载均衡是通过Spring Cloud Ribbon实现的,所以我们可以通过配置Spring Cloud Feign 从而配置 Spring Cloud Ribbon 。全局配置全局配置的方法很简单,我们可以使用如下配置来设置全局参数ribbon.ConnectTimeout=5000 ribbon.ReadTimeout=5000指定服务配置大多数情况下,我们对于服务的调用时间可能会根据实际服务特性来做一些调整,所以仅仅依靠全局的配置是不行的,因为Feign 这个组件是整合了 Ribbon和 Hystrix的,所以通过设置Feign的属性来达到属性传递的目的。在定义Feign 客户端的时候,我们使用了@FeignClient()注解,其实在创建@FeignClient(value = server-provider)的时候,同时也创建了一个名为server-provider的ribbon 客户端,所以我们就可以使用@FeignClient中的nane 和value 值来设置对应的Ribbon 参数。# 使用feign-clients 中的注解的value值设置如下参数 # HttpClient 的连接超时时间 server-provider.ribbon.ConnectTimeout=500 # HttpClient 的读取超时时间 server-provider.ribbon.ReadTimeout=2000 # 是否可以为此客户端重试所有操作 server-provider.ribbon.OkToRetryOnAllOperations=true # 要重试的下一个服务器的最大数量(不包括第一个服务器) server-provider.ribbon.MaxAutoRetriesNextServer=2 # 同一个服务器上的最大尝试次数(不包括第一个) server-provider.ribbon.MaxAutoRetries=1重试机制Spring Cloud Feign 中实现了默认的请求重试机制,我们可以通过修改server-provider中的示例做一些验证:在server-provider应用中的/hello接口实现中,增加一些随机延迟,比如@RequestMapping(value = "hello", method = RequestMethod.GET) public String hello() throws Exception{ ServiceInstance instance = discoveryClient.getLocalServiceInstance(); log.info("instance.host = " + instance.getHost() + "instance.service = " + instance.getServiceId() + "instance.port = " + instance.getPort()); log.info("Thread sleep ... "); int sleepTime = new Random().nextInt(3000); log.info("sleepTime = " + sleepTime); Thread.sleep(sleepTime); System.out.println("Thread awake"); return "Hello World"; }在feign-consumer 应用中增加上文提到的重试配置参数,来解释一下上面的配置MaxAutoRetriesNextServer 设置为2 表示的是下一个服务器的最大数量,也就是说如果调用失败,会更换两次实例进行重试,MaxAutoRetries设置为1 表示的是每一个实例会进行一次调用,失败了再换为其他实例。OKToRetryOnAllOperations的意义是无论是请求超时或者socket read timeout都进行重试,这里需要注意一点,Ribbon超时和Hystrix超时是两个概念,为了让上述实现有效,我们需要 让Hystrix的超时时间大于Ribbon的超时时间, 否则Hystrix命令超时后, 该命令直接熔断,重试机制就没有任何意义了。Hystrix 配置在Spring Cloud Feign中,除了引入Spring Cloud Ribbon外,还引入了服务保护工具Spring Cloud Hystrix,下面就来介绍一下如何使用Spring Cloud Feign配置Hystrix属性实现服务降级。全局配置对于Hystrix全局配置同Spring Cloud Ribbon 的全局配置一样,直接使用默认前缀 hystrix.command.default 就可以进行配置,比如设置全局的超时hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000另外,在对Hystrix进行配置之前,我们需要确认feign.hystrix.enable参数没有设置为false,否则该参数设置会关闭Feign客户端的Hystrix支持。// 关闭Hystrix 功能(全局关闭) feign.hystrix.enabled=false // 关闭熔断功能 hystrix.command.default.execution.timeout.enabled=false禁用hystrix如果不想全局地关闭Hystrix支持,而只想针对某个服务客户端关闭Hystrix支持,需要通过使用@Scope("prototype")注解为指定的客户端配置Feign.Builder 实例构建一个关闭Hystrix的配置类@Configuration public class DisableHystrixConfiguration { @Bean @Scope("prototype") public Feign.Builder builder(){ return new Feign.Builder(); } }在HelloService的@FeignClient注解中,通过Configuration参数引入上面实现的配置@FeignClient(value = "server-provider", fallback = DisableHystrixConfiguration.class) public interface RefactorHelloService extends HelloService {}服务降级配置Hystrix 提供的服务降级是服务容错的重要功能,之前我们开启Ribbon的服务降级是通过使用@HystrixCommand(fallbackMethod = "hystrixCallBack")开启的,Feign对Ribbon进行了封装,所以Feign 也提供了一种服务降级策略。下面我们就来看一下Feign 如何使用服务降级策略。我们在feign-consumer中进行改造服务降级逻辑的实现只需要为Feign客户端的定义接口编写一个具体的接口实现类,比如为server-provider接口实现一个服务降级类 HelloServiceFallback,其中每个重写方法的逻辑都可以用来定义相应的服务降级逻辑,具体代码如下@Component public class FeignServiceCallback implements FeignService{ @Override public String hello() { return "error"; } @Override public String hello(@RequestParam("name") String name) { return "error"; } @Override public User hello(@RequestHeader("id") Integer id, @RequestHeader("name") String name) { return new User(0,"未知"); } @Override public String hello(@RequestBody User user) { return "error"; } }在服务绑定接口中,通过@FeignClient注解的fallback 属性来指定对应的服务降级类@FeignClient(value = "server-provider",fallback = FeignServiceCallback.class) public interface FeignService { @RequestMapping(value = "/hello") String hello(); @RequestMapping(value = "/hello1", method = RequestMethod.GET) String hello(@RequestParam("name") String name); @RequestMapping(value = "/hello2", method = RequestMethod.GET) User hello(@RequestHeader("id") Integer id,@RequestHeader("name") String name); @RequestMapping(value = "/hello3", method = RequestMethod.POST) String hello(@RequestBody User user); }测试验证下面我们来验证一下服务降级逻辑,启动注册中心Eureka-server,服务消费者feign-consumer,不启动server-provider,发送GET 请求到http://localhost:9001/feign-consumer2,该接口会分别调用FeignService中的四个接口,因为feign-consumer没有启动,会直接触发服务降级,使用Postman调用接口的返回值如下error error error com.feign.consumer.pojo.User@5ac0702f后记:Spring Cloud Feign 声明式服务调用就先介绍到这里,下一篇介绍Spring Cloud Zuul服务网关 </div>
Spring Cloud Hystrix 断路器
Spring Cloud Hystrix 服务容错保护一、Hystrix 是什么 雪崩效应 雪崩效应产生场景 常见解决方案二、Hystrix断路器搭建三、断路器优化一、Hystrix 是什么在微服务架构中,我们将系统拆分成了若干弱小的单元,单元与单元之间通过HTTP或者TCP等方式相互访问,各单元的应用间通过服务注册与订阅的方式相互依赖。由于每个单元都在不同的进程中运行,依赖远程调用的方式执行,这样就可能引起因为网速变慢或者网络故障导致请求变慢或超时,若此时调用方的请求在不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最终导致自身服务的瘫痪。 Hystrix 是Netflix 中的一个组件库,它隔离了服务之间的访问点,阻止了故障节点之间可能会引起的雪崩效应,并提供了后备选项。在微服务架构中,存在着许多的服务单元,若单一节点的故障,就很容易因为依赖关系而引发故障的蔓延,最终导致整个生态系统的瘫痪。为了解决这样的问题,产生了断路器等一系列的保护机制措施。在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。雪崩效应雪崩效应就像是水滴石穿,蝴蝶效应一样,是指微小的事物随着时间的推移,会变得越来越巨大,从而对整个环境造成影响的现象。例如:在生态系统中,某一类物种的灭绝可能对整个生态系统造成不了太大的损失,但是这类物种的灭绝可能会引发其他物种的死亡,其他物种的灭绝又会影响另外一种物种的灭亡,就像雪球越滚越大,最终会导致整个生态系统的崩溃。如上图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。雪崩效应产生场景流量激增: 比如异常流量,用户重试导致系统负载升高;缓存刷新: 假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会造成B系统崩溃连接未释放: 代码循环调用的逻辑问题,资源未释放引起的内存泄漏等问题;硬件故障: 比如宕机,机房断电等线程同步等待: 系统间经常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。如果一个核心业务线程调用非核心业务线程,这个非核心线程交由第三方系统完成,当第三方系统本身出现问题,导致核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的,最终这条线程将断掉,也可能引发雪崩;常见解决方案针对上述的雪崩问题,每一条都有一个自己的解决方案,但是任何一个解决方案能够应对所有场景针对流量激增,采用自动扩容以应对流量激增,或者在负载均衡器上安装限流模块针对缓存刷新,参考Cache应用的服务过载案例研究针对硬件故障,采用多机房灾备,跨机房路由针对同步等待,采用线程隔离,熔断器等机制通过实践发现,线程同步等待是最常见引发的雪崩效应的场景。二、Hystrix断路器搭建在开始使用Spring Cloud Hystrix断路器之前,我们先用之前实现的一些内容作为基础,构建一个如下图所示的服务调用关系:如图所示,上面需要的角色有三个,服务有四个ribbon-connsumer: ribbon消费者,消费server-provider提供的服务server-provider: 服务提供者,提供服务供消费者消费(有点像父母默默的付出一样),启动两个实例,还记得怎么启动吗?—server.port 启动eureka-server: eureka注册中心,提供最基本的订阅发布功能。消费者和服务提供者都需要往注册中心注册自己依次启动上面的四个服务,发现注册中心已经成功注册了四个服务(包括自己)调用http://localhost:9000/ribbon-consumer 发现能够通过Ribbon进行远端调用在未加入断路器之前,关闭ribbon-consumer 的连接,再次调用http://localhost:9000/ribbon-consumer,发现服务无法提供(使用Postman 测试)下面开始引入Hystrix在ribbon-consumer 工程的pom.xml的dependency节点下引入spring-cloud-starter-hystrix依赖在ribbon-consumer 工程的主加载类中添加@EnableCircuitBreaker开启断路器的功能注意:这里也可以使用@SpringCloudApplication注解来修饰应用主类,具体定义如下@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {}SpringCloudApplication 注解上有@EnableCircuitBreaker 注解,用来开启断路器的功能,其他主要注解是@SpringBootApplication ,这个注解是SpringBoot的启动类注解, @EnableDiscoveryClient该注解可以发现Eureka注册中心改造消费方式,新增HystrixService类,并且注入RestTemplate实例,然后,将在RibbonController中对RestTemplate 的使用迁移到hystrixService方法中,最后,在hystrixService上添加@HystrixCommand注解来指定回掉方法。// HystrixService @Service public class HystrixService { @Resource RestTemplate restTemplate; // 指定回掉方法是下面的hystrixCallback @HystrixCommand(fallbackMethod = "hystrixCallBack") public String hystrixService(){ return restTemplate.getForEntity("http://server-provider/hystrix";,String.class).getBody(); } public String hystrixCallBack(){ return "error"; } }服务提供者的业务非常简单,具体代码如下@RequestMapping(value = "/hystrix", method = RequestMethod.GET) public String hystrix(){ return "hystrix"; }下面来验证一下通过断路器的回掉实现,重启之前关闭的8081端口,恢复成为四个服务的状态,并确保http://localhost:9000/ribbon-consumer/ 能够提供服务,并且以轮询的方式循环访问8081 和 8082 端口的服务。此时断开8081端口,发现页面上展示的不再是 hystrix ,而是"error",而另一个服务是正常能够打印。三、断路器优化经过以上服务的搭建,相信你已经能够搭建出来最基本的Hystrix熔断器,并且实现了服务熔断机制,下面就来对断路器做一下简单的优化,来模拟服务阻塞(长时间未响应)的情况。优化server-provider代码如下:@RequestMapping(value = "/hystrix", method = RequestMethod.GET) public String hystrix() throws InterruptedException { ServiceInstance serviceInstance = discoveryClient.getLocalServiceInstance(); // 让线程等待几秒钟 int sleepTime = new Random().nextInt(3000); Thread.sleep(sleepTime); System.out.println("weak up!!!"); log.info("sleepTime = " + sleepTime); return "hystrix"; }依次启动所有的服务,在主页上访问 http://localhost:9000/ribbon-consumer ,多次刷新主页,发现error 和 hystrix 是交替出现的,这是为何?因为hystrix断路器的默认超时时间是2000毫秒,所以这里采用了0 - 3000 的随机数,也就是访问请求在 0 -2000 毫秒内是不超时的,不会触发断路器,而> 2000 毫秒是超市的,默认会触发断路器。 </div>
Spring Cloud Ribbon负载均衡
Spring Cloud Ribbon负载均衡Spring Cloud Ribbon负载均衡一、简介二、客户端负载均衡三、RestTemplate详解GET请求POST请求PUT请求DELETE请求一、简介 Spring Cloud Ribbon是一个基于HTTP 和 TCP的客户端负载工具,它基于Netflix Ribbon实现,我们可以使用它来进行远程服务负载均衡的调用。它不像Zuul 和 Eureka 等可以独立部署,它虽然是一个工具类框架,但是几乎所有的Spring Cloud微服务架构和基础设施都离不开它,包括后面所介绍的Feign 远程调用,也是基于Ribbon实现的工具二、客户端负载均衡 负载均衡是在一个架构中非常重要,而且不得不去实施的内容。_因为负载均衡对系统的高可用,网络压力的缓解和处理能力扩容的重要手段之一。通常负载均衡分为两种:硬件负载均衡 和 软件负载均衡,硬件负载均衡一般是通过硬件来实现,在_服务器节点之间安装特定的负载均衡设备_,比如F5。 而软件负载均衡是采用软件控制的手段实现的,它实在_服务器之间安装某种特定功能的软件来完成特定的请求分开工作,比如Nginx等。无论硬件负载还是软件负载,只要是服务端负载均衡都能以下图的架构方式构建起来:硬件负载均衡的设备和软件负载均衡的模块都会维护一个下挂可用的服务清单,通过心跳检测来剔除故障的服务节点以保证清单中都是可以访问的服务端节点。当客户发送请求到负载均衡的设备时。设备按照服务负载均衡的算法(随机访问,轮询访问,权重访问,最少访问次数算法)来找到对应的服务端。而客户端负载均衡和服务端负载均衡最大的不同点在于上面所提到服务清单的存储位置。在客户端负载均衡中,所有客户端节点都维护着自己要访问的服务清单,而这些服务清单都来自注册中心,比如我们上一章介绍的Eureka服务端。通过Spring Cloud Ribbon的封装,我们在微服务架构中使用负载均衡就比较简单,只需要下面两步:服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心服务消费者直接调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。三、RestTemplate详解在上一章中,我们已经引入了Spring Cloud Ribbon实现了客户端负载均衡的一个简单的实例,其中,我们使用了一个非常有用的对象RestTemplate。该对象会使用Ribbon的自动化配置,同时通过配置@LoadBalanced开启客户端负载均衡。下面我们将详细介绍RestTemplate 针对几种不同的请求类型和参数类型的服务调用实现。准备工作在上一篇博客中,我们搭建了一个注册中心,一个服务提供者,一个ribbon消费者客户端,现在我们也需要这三个组件来做Ribbon 服务消费GET请求在RestTemplate中,对GET请求可以通过如下两个方法进行调用实现。第一种:getForEntity()函数,该方法返回的是ResponseEntity,该对象是Spring对HTTP请求响应的封装,其中主要存储了HTTP的几个重要元素,比如HTTP请求状态码的枚举对象HttpStatus(常用的404,500这些错误),在它的父类HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型集合的请求体对象。它的一般形式有三种:/*
* url是远程服务端的路径,responseType是返回值类型,urlVariables是可变参数,给服务端传递的参数
*/
getForEntity(String url, Class<T> responseType, Object... urlVariables)
/*
* 可以使用Map封装参数传递给客户端
*/
getForEntity(String url, Class<T> responseType, Map<String, ?> urlVariables)
/*
* 也是一直接使用uri地址
*/
getForEntity(URI url, Class<T> responseType) throws RestClientException
/*
* getForObject 用法和getForEntity基本相同
*/
getForObject(String url, Class<T> responseType, Object... urlVariables) throws RestClientException
getForObject(String url, Class<T> responseType, Map<String, ?> urlVariables) throws RestClientException
getForObject(URI url, Class<T> responseType) throws RestClientExceptionURI 和 URL 的关系:URI : 统一资源标志符:URL: 统一资源定位符URN : 统一资源名称三者之间的关系:一般用法getForEntityRibbon 消费者/** * 文章基于spring-boot-starter-parent 1.3.7 版本 * 如果读者使用1.5.9 以上的版本,可以用GetMapping * @return */ @RequestMapping(value = "/ribbon-consumer1", method = RequestMethod.GET) public ResponseEntity<String> helloUser(){ // 返回值是String类型,所以对应第一个逗号后面的类型 // /user/{1} 中的{1}表示的是第一个参数,传的值是didi // 也可以用getForEntity().getBody() 方法,此时返回值就只是一个String类型 return restTemplate.getForEntity("http://server-provider/user/{1}",String.class,"didi"); } @RequestMapping(value = "/ribbon-consumer2", method = RequestMethod.GET) public ResponseEntity<User> helloUser2(){ // 返回值是一个User类型 // 多个参数之间用& 隔开 return restTemplate.getForEntity("http://server-provider/user2?id=;001&name=didi",User.class); } // 传递一个Map类型的对象 @RequestMapping(value = "/ribbon-consumer3", method = RequestMethod.GET) public ResponseEntity<String> helloUser3(){ Map params = new HashMap(); params.put("name","data"); // {name}表示的是params中的key return restTemplate.getForEntity("http://server-provider/user3?name=;{name}", String.class,params); } // 其实最核心的就是通过uri进行调用,上面所有的写法都会转换为下面这种写法 // 也就是说下面这种写法是最根本的。 @RequestMapping(value = "/ribbon-consumer4", method = RequestMethod.GET) public ResponseEntity<String> helloUser4(){ UriComponents uriComponents = UriComponentsBuilder.fromUriString( "http://server-provider/user4?name=;{name}") .build() .expand("lx") .encode(); URI uri = uriComponents.toUri(); return restTemplate.getForEntity(uri,String.class); }User 对象public class User { private Integer id; private String name; public User(){} public User(Integer id, String name) { this.id = id; this.name = name; } get and set...}服务提供者来看一下服务提供者的代码:// 返回的类型是String @RequestMapping(value = "/user/{name}", method = RequestMethod.GET) public String helloUser(@PathVariable("name") String name){ return "Hello " + name; } // 返回的类型是User @RequestMapping(value = "/user2", method = RequestMethod.GET) public User helloUser(User user){ return user; } @RequestMapping(value = "/user3", method = RequestMethod.GET) public String helloUser1(@RequestParam("name") String name){ return "Hello " + name; } @RequestMapping(value = "/user4", method = RequestMethod.GET) public String helloUser2(@RequestParam("name") String name){ return "Hello " + name; }getForObject()Ribbon 消费者@RequestMapping(value = "/ribbonGet", method = RequestMethod.GET) public String ribbonGet(){ // {1} 和 {2} 都是占位符,分别代表着 001 和 lx的值 return restTemplate.getForObject("http://server-provider/ribbon?id=;{1}&name={2}",String.class, new Object[]{"001","lx"}); } // 和上面用法基本相同 @RequestMapping(value = "/ribbonGet2", method = RequestMethod.GET) public String ribbonGet2(){ Map params = new HashMap(); params.put("id","001"); params.put("name","lx"); return restTemplate.getForObject("http://server-provider/ribbon?id=;{id}&name={name}",String.class, params); } @RequestMapping(value = "/ribbonGet3", method = RequestMethod.GET) public String ribbonGet3(){ UriComponents uriComponents = UriComponentsBuilder.fromUriString( "http://server-provider/ribbon?id=;{id}&name={name}") .build() .expand("001","lx") .encode(); URI uri = uriComponents.toUri(); return restTemplate.getForObject(uri,String.class); }服务提供者// 上面所有的url共用下面一个方法 @RequestMapping(value = "/ribbon", method = RequestMethod.GET) public String acceptRibbon(@RequestParam("id")String id, @RequestParam("name") String name){ System.out.println("id = " + id + "name = " + name); return "Hello " + id + " World " + name; }POST请求了解完GET请求后,再来看一下POST请求:在RestTemplate中,POST请求可以用一下几种方式来实现// postForEntitypostForEntity(String url, Object request, Class<T> responseType, Object... uriVariables) postForEntity(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)postForEntity(URI url, Object request, Class<T> responseType) throws RestClientException// postForObjectpostForObject(String url, Object request, Class<T> responseType, Object... uriVariables)postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)postForObject(URI url, Object request, Class<T> responseType) throws RestClientException// postForLocationpostForLocation(String url, Object request, Object... urlVariables) throws RestClientExceptionpostForLocation(String url, Object request, Map<String, ?> urlVariables) throws RestClientException postForLocation(URI url, Object request) throws RestClientExceptionRibbon服务端/** * 文章基于spring-boot-starter-parent 1.3.7 版本 * 如果读者使用1.5.9 以上的版本,可以用 PostMapping * @return */ @RequestMapping(value = "/ribbonPost", method = RequestMethod.POST) public User ribbonPost(){ User user = new User(001,"lx"); return restTemplate.postForEntity("http://server-provider/rpost";,user,User.class) .getBody(); } @RequestMapping(value = "/ribbonPost2", method = RequestMethod.POST) public User ribbonPost2(){ User user = new User(001,"lx"); UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://server-provider/location";) .build() .expand(user) .encode(); URI uri = uriComponents.toUri(); return restTemplate.postForEntity(uri,user,User.class).getBody(); } @RequestMapping(value = "/ribbonPost3", method = RequestMethod.POST) public String ribbonPost3(){ User user = new User(001,"lx"); // 占位符石str, 服务端可以用 @PathVariable获取 return restTemplate.postForEntity("http://server-provider/rbPost/{str}",user,String.class,"hello") .getBody(); } @RequestMapping(value = "/ribbonPost4", method = RequestMethod.POST) public String ribbonPost4(){ Map<String,String> params = new HashMap<>(); params.put("id","001"); params.put("name","lx"); return restTemplate.postForEntity("http://server-provider/mapPost";,params,String.class).getBody(); } /** * restTemplate.postForObject()方法与上面用法几乎相同 * postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables) * postForEntity(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) * postForEntity(URI url, Object request, Class<T> responseType) * *. postForLocation 也相似,这里就不再举例说明了 */服务提供者@RequestMapping(value = "/rpost", method = RequestMethod.POST) public User accpetRibbonPost(@RequestBody User user){ log.info("id = " + user.getId() + " name = " + user.getName()); return user; } @RequestMapping(value = "/location", method = RequestMethod.POST) public User acceptRibbonPost2(@RequestBody User user){ log.info("id = " + user.getId() + " name = " + user.getName()); return user; } @RequestMapping(value = "/rbPost/{str}", method = RequestMethod.POST) public String accpetRibbonPost3(@PathVariable String str, @RequestBody User user){ log.info("str = " + str); log.info("id = " + user.getId() + " name = " + user.getName()); return str + " " + user.getId() + " " + user.getName(); } @RequestMapping(value = "/mapPost", method = RequestMethod.POST) public String acceptRibbonPost4(@RequestBody Map map){ String id = (String)map.get("id"); String name = (String)map.get("name"); return "id = " + id + " name = " + name; }PUT请求Restful中的put请求经常用来修改某些属性的值,他和POST请求相似一般形式/** 它的形式比较少,只有一种比较形式*/put(String url, Object request, Object... urlVariables) throws RestClientExceptionput(String url, Object request, Map<String, ?> urlVariables) throws RestClientExceptionput(URI url, Object request) throws RestClientExceptionRibbon服务端@RequestMapping(value = "/putRibbon", method = RequestMethod.PUT) public void putRibbon(){ restTemplate.put("http://server-provider/ribbonPut";,new User(21,"lx")); }这里只采用了一种简单形式,用法和Post很相似,没有再详细说明PUT请求没有返回值,可以理解为只把需要的值传过去就可以,修改成功不成功与我没有关系服务提供者@RequestMapping(value = "/ribbonPut", method = RequestMethod.PUT) public void acceptRibbonPut(@RequestBody User user){ log.info("user.id = " + user.getId() + " user.name = " + user.getName()); }DELETE请求delete请求在Restful API中一般用于根据id删除某条信息,用法也比较简单,没有返回值一般形式delete(String url, Object... urlVariables) throws RestClientExceptiondelete(String url, Map<String, ?> urlVariables) throws RestClientExceptiondelete(URI url) throws RestClientExceptionRibbon服务端@RequestMapping(value = "/deleteRibbon", method = RequestMethod.DELETE) public void deleteUser(){ User user = new User(21,"lx"); restTemplate.delete("http://server-provider/ribbonDelete/{1}",user.getId()); }服务提供者@RequestMapping(value = "/ribbonDelete/{id}", method = RequestMethod.DELETE) public void deleteRibbon(@PathVariable Integer id){ log.info("delete user " + id); }以上就是Ribbon介绍的全部内容,其实关于Ribbon的源码分析根本没有这么简单,敬请期待…… </div>
Spring Cloud Eureka 详述(一)(下)
注册服务提供者在完成了上述搭建之后,接下来我们尝试将一个既有的SpringBoot应用加入Eureka服务治理体系去。使用上一小节的快速入门工程进行改造,将其作为一个微服务应用向服务注册中心发布注册自己pom.xml配置如下:<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>配置完pom.xml,我们需要在启动类上加入@EnableDiscoverClient注解,用于开启eureka-client客户端在application.properties中加入如下内容# 这个名字就是Eureka注册中新的实例名称spring.application.name=server-provider# 向注册中心注册自己eureka.client.service-url.defaultZone=http://localhost:1111/eureka/配置完上面两个之后,在package文件夹下新建HelloController类,具体代码如下@RestController public class HelloController { private final Logger log = LoggerFactory.getLogger(HelloController.class); @Resource private DiscoveryClient discoveryClient; @RequestMapping(value = "hello", method = RequestMethod.GET) public String hello(){ ServiceInstance instance = discoveryClient.getLocalServiceInstance(); log.info("instance.host = " + instance.getHost() + " instance.service = " + instance.getServiceId() + " instance.port = " + instance.getPort()); return "Hello World"; }}启动服务提供者,启动完成后,会出现如下表示启动成功。访问http://localhost:1111/ ,主页上显示eureka-provider注册到了注册中心此处的Status 中的内容也就包括上面配置的spring.application.name=server-provider在主页访问 http://localhost:8080/hello ,发现页面上 输出了Hello World,控制台打印出来了c.s.provider.controller.HelloController : instance.host = macliu instance.service = server-provider instance.port = 8080注意事项上面注册到注册中心的图,你会发现这样一行红色的文字EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.这是Eureka的一种自我保护机制,Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会将当前的实例注册信息保护起来,同时提示这个警告。Eureka server和client之间每隔30秒会进行一次心跳通信,告诉server,client还活着把上面的server-provider服务停止之后,会出现如下状态这个表示server-provider 已经标记为下线,也就是 DOWN 状态,再次重新上线后,发现Status又变为了UP状态。把上面的配置文件中自我保护功能关闭后,出现如下状态高可用配置中心在微服务架构这样的分布式环境中,需要充分考虑到发生故障的情况,所以在生产环境中必须对各个组件进行高可用部署,对于微服务是如此,对于注册中心也一样。Eureka Server的设计就充分考虑了高可用问题,在Eureka的服务治理体系中,所有的节点既是服务提供方,也是服务的消费者,服务注册中心也不例外,不同的注册中心在向其他注册中心提供节点列表的时候,也在向其他注册中心获取节点列表。高可用的配置中心就是向其他注册中心注册自己,同时把服务列表提供给其他注册中心,从而达到注册中心列表同步,达到高可用的效果。通过下面两个配置来实现eureka.client.register-with-eureka=trueeureka.client.fetch-registry=true下面就在单节点的基础之上创建一下高可用的配置中心(双节点注册中心)首先,创建两个配置文件,分别是application-peer1.properties 和 application-peer2.properties,内容分别如下application-peer1.propertiesspring.application.name=eureka-serverserver.port=1111eureka.instance.hostname=peer1eureka.client.register-with-eureka=trueeureka.client.fetch-registry=trueeureka.client.service-url.defaultZone=http://peer2:1112/eureka/application-peer2.propertiesspring.application.name=eureka-serverserver.port=1112eureka.instance.hostname=peer2eureka.client.register-with-eureka=trueeureka.client.fetch-registry=trueeureka.client.service-url.defaultZone=http://peer1:1111/eureka/在本地修改配置文件/etc/hosts ,Windows下面是C:\Winows\System32\drivers\etc\hosts。添加如下内容127.0.0.1 peerl127.0.0.1 peer2如下:首先在idea 或者eclipse 使用mvn clean 和 mvn install命令,会直接打包,这里注意,一定要在pom.xml中配置如下,否则使用java -jar会报没有主清单属性的错误。<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins></build>打包完成后,切换到eureka-server项目,再切换到target目录下,此时有mvn install 的jar包,使用java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1和 java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2 两个命令,启动两个例程。起来过后分别访问 http://localhost:peer1/eureka/ 和 http://localhost:peer2/eureka/ 主页,发现对应的注册中心分别注册进去了,而且分片也处于可用分片状态。到现在为止,我们已经让两个注册中心分别注册各自的服务了,还记得上面还有一个server-provider服务吗?我们也让server-provider分别注册到这两个注册中心。在server-provider中修改对应的配置文件eureka.client.service-url.defaultZone=http://peer1:1111/eureka, http://peer2:1112/eureka/ 启动程序,发现http://localhost:1111/ 和 http://localhost:1112/ 中都注册了server-provider服务访问http://localhost:8080/hello,你会发现页面上显示出来hello world,断开其中的任意一个注册中心,hello world也能够显示出来。也就是说,server-provider 分别对两个注册中心分别注册了各自的服务,由两个注册中心以轮询的方式提供服务。断开其中一个注册中心,还有另外一个注册中心可以提供服务,这也是Eureka 高可用的体现。 注意事项如果application-peer1.properties 和 application-peer2.properties中的eureka.instance.hostname与 本地hosts文件中的名称不一致的话,那么注册中心启动后,会使分片处于不可用的状态, spring.application.name 表示的是实例的名称,也就是如下的地方当server-provider注册进来的时候,高可用配置的注册中心会以轮询的方式提供服务,每次提供服务是哪个注册中心是不可预知的。如我们不想使用主机名来定义注册中心的地址,也可以使用IP地址的形式, 但是需要在配置文件中增加配置参数eureka.instance.prefer-ip-address=true, 该值默认为false。服务发现与消费通过上述的内容介绍与实践,我们已经搭建起来微服务架构中的核心组件— 服务注册中心(包括单节点模式和高可用模式)。并用server-provider注册双节点,在页面上发起一个url请求时,注册中心找到server-provider,并有两个节点以轮询的方式提供服务。下面就来构建一个消费者,它主要完成两个目标:发现服务和消费服务。其中,服务发现的任务由Eureka客户端完成,消费服务的任务由Ribbon来完成。先来熟悉一下什么是Ribbon:Ribbon是客户端负载均衡器,可以让您对HTTP和TCP客户端的行为进行控制。 Feign已经使用了Ribbon,如果你使用了@FeignClient,那么Ribbon也适用。Ribbon可以在通过客户端中配置的ribbonServerList服务端列表去轮询访问以达到负载均衡的效果。当ribbon与Eureka联合使用时,Ribbon的服务实例清单RibbonServerList会被DiscoveryEnabledNIWSServerList重写,扩展成从Eureka注册中心中获取服务端列表。同时它也会用NIWSDiscoveryPing来取代Ping,它将职责委托给Eureka来确定服务端是否启动,我们目前不细致探讨Ribbon的细节问题。下面通过一个简单的实例,看看Eureka的服务治理体系下如何实现服务的发现与消费。首先,先做一些准备工作,启动之前实现的服务注册中心eureka-server以及server-provider服务,为了实现ribbon的负载均衡功能,我们通过java -jar命令行的方式来启动两个不同端口的server-provider启动一个eureka-server即可使用java -jar service-provider-0.0.1-SNAPSHOT.jar --server.port=8081和 java -jar service-provider-0.0.1-SNAPSHOT.jar --server.port=8082 来启动两个server-provider 进程启动完成后,可见注册中心注册了两个server-provider 实例新创建一个SpringBoot 工程,命名为ribbon-consumer,相较于之前pom.xml,我们新增了spring-cloud-starter-ribbon<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Brixton.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>创建完pom.xml,在ribbon-consumer 启动类加上@EnableDiscoveryClient注解,让该注解注册为Eureka客户端,以获得服务发现的能力,同时,创建RestTemplate对象,加上@LoadBalance注解开启负载均衡。@EnableDiscoveryClient @SpringBootApplication public class RibbonConsumerApplication { @Bean @LoadBalanced RestTemplate restTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(RibbonConsumerApplication.class, args); }}在src目录下新建一个RibbonController类,注入@RestTemplate,构造一个方法来调用server-provider中的/hello 方法。代码如下@RestController public class RibbonController { @Autowired RestTemplate restTemplate; @RequestMapping(value = "/ribbon-consumer",method = RequestMethod.GET) public String helloConsumer(){ return restTemplate.getForEntity("http://server-provider/hello";,String.class).getBody(); } </div>
Spring Cloud Eureka 详述(一)(上)
服务治理: Spring Cloud Eureka服务治理: Spring Cloud Eureka一、简介二、 功能概述服务治理Netflix Eureka搭建服务注册中心注册服务提供者高可用配置中心服务发现与消费一、简介Spring cloud eureka是Spring cloud netfilx中的一部分,它基于Netflix Eureka做了二次封装,主要职责完成Eureka 中的服务治理功能本篇主要探讨如下:服务治理和Eureka简介构建服务注册中心服务注册与服务发现Eureka 基础架构Eureka 的服务治理机制Eureka 的配置二、 功能概述服务治理 服务治理可以是说微服务架构中最为核心的基础模块,它主要用来实现各个微服务实现的自动化注册与发现。在开始的时候微服务系统的服务可能并不多,我们需要一些配置来完成服务的调用。服务注册: 在服务治理框架中,通常会构建一个注册中心,由各个服务提供者来向注册中心登记并提供服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按照服务名分类组织服务清单。服务名位置服务A192.168.1.101:8000, 192.168.1.102:8000服务B192.168.1.103:9000,192.168.1.104:9000,192.168.1.105:9000比如我们有两个提供服务A 的进程分别位于192.168.1.101:8000, 192.168.1.102:8000 上,另外还有三个提供服务B 的进程分别位于192.168.1.103:9000,192.168.1.104:9000,192.168.1.105:9000 进程上,那么你向服务中心注册过后,服务中心就会有一个这样的服务列表,服务中心向各个注册的服务发送心跳机制,来检验服务是否可用,若不可用就会把服务剔除,来达到故障排除的效果。服务发现: 由于在服务治理框架下运作,服务间的调用不再通过指定的Ip:端口号这种方式来实现 ,而是向服务名发起请求实现。所以,在服务调用方在调用服务提供方接口的时候,并不知道具体服务的位置。因此,服务调用方需要向服务中心获取服务列表,以实现对具体服务的访问。比如一个服务调用者C想要获取服务A的ip来完成接口的调用,那么他首先应该去服务中心发起咨询你服务的请求,由注册中心的服务列表将A的位置发送给调用者C,如果按照上面服务A地址的话,那么调用者C会由两个服务A的地址来提供服务,当服务C需要调用的时候,便从服务A中的清单中采用轮询的方式取出一个位置来服务调用,这个过程也被称为负载均衡。Netflix EurekaEureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。 Eureka包含两个组件: Eureka Server 和 Eureka ClientEureka Server 简称Eureka 服务端, 主要提供服务注册功能,其实也就相当于是注册中心,和其他服务注册中心一样,提供高可用的配置,同时也支持集群部署,当集群中某一个节点发生故障时,那么Eureka就会进入自我保护模式,它允许故障的节点继续提供服务的发现与注册,当故障分片恢复运行时,集群中的其他分片会把他们的状态再同步回来。Eureka Client:简称Eureka 客户端,主要处理服务的注册与发现。客户端通过注解和参数配置的方式,Eureka 客户端向注册中心注册自身的服务并周期性的发送心跳机制来更新服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态。搭建服务注册中心Spring Cloud Eureka 是采用SpringBoot 进行项目的快速搭建的,如果不太了解SpringBoot的话,可以了解一下SpringBoot 入门实例。首先创建SpringBoot工程,命名为Eureka-server,也就是Eureka服务端,创建完成后在pom.xml文件中增加如下maven依赖,完整的文件如下:<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.eureka.server</groupId>
<artifactId>eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-server</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
• 在SpringBoot启动类,也就是@SpringBootApplication修饰的主方法中加入如下注解@EnableEurekaServer。
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}加入这个注解也就标识着这是一个Eureka的服务端,可以启动服务了,但是启动服务会报错,因为你没有添加注册中心的相关配置。在application.properties文件中加入如下内容server.port=8000eureka.instance.hostname=localhosteureka.client.register-with-eureka=falseeureka.client.fetch-registry=falseeureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/server.port 就代表着注册中心的端口号eureka.client.service-url.defaultZone :eureka客户端默认服务urleureka.client.register-with-eureka : 表示注册中心是否向其他注册中心注册自己,单节点注册中心不需要,设置为falseeureka.client.fetch-registry: 表示注册中心是否主动去检索服务,并不需要检索服务,设置为false其他配置:# 项目contextPath,一般在正式发布版本中,我们不配置# 避免加上更目录:Cannot execute request on any known server# 加上根目录也需要在注册地址上加入根server.context-path=/eureka81# 错误页,指定发生错误时,跳转的URL。请查看BasicErrorController源码便知server.error.path=/error# 通过spring.application.name属性,我们可以指定微服务的名称后续在调用的时候只需要使用该名称就可以进行服务的访问。spring.application.name=eureka-server# eureka是默认使用hostname进行注册,可通过一下项自动获取注册服务IP或者直接通过eureka.instance.ip-address指定IP# eureka.instance.prefer-ip-address=true# SpringBoot 在启动的时候会读配置文件,会把prefer-ip-address 默认转换为preferIpAddress驼峰命名eureka.instance.preferIpAddress=true# 设为false,关闭自我保护eureka.server.enable-self-preservation=false# 清理间隔(单位毫秒,默认是60*1000eureka.server.eviction-interval-timer-in-ms=6000# 开启健康检查(需要spring-boot-starter-actuator依赖)eureka.client.healthcheck.enabled=false# 续约更新时间间隔(默认30秒)eureka.instance.lease-renewal-interval-in-seconds=10# 续约到期时间(默认90秒)eureka.instance.lease-expiration-duration-in-seconds=30没有加入 eureka.instance.preferIpAddress=true 之前,默认本地为注册中心加入 eureka.instance.preferIpAddress=true 之后,圈出来的ip即为eureka.client.service-url.defaultZone指定的 ip。在完成了上述配置之后,应用程序启动并访问http://localhost:1111/ 可以看到如下图所示的信息版,其中Instances curently registered with Eureka 是空的,表明还没有任何服务提供者提供服务。 </div>