- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
我们知道微服务彼此间独立部署、具有清晰的边界,服务间通过远程调用来构建复杂的业务功能。而服务册中心在微服务项目中扮演着非常重要的角色,那么注册中心又是什么,使用服务注册中心可以解决微服务中的哪些问题呢?
注册中心是微服务架构中的纽带,类似于“通讯录”,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址并进行调用。注册中心本质上是为了解耦服务提供者和服务消费者。对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的,更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。因此,原本在单体应用阶段常用的静态LB机制就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。
(1)屏蔽、解耦服务之间相互依赖的细节:
服务之间的远程调用必须要知道对方IP、端口。但是该调用方式存在明显的问题,如被调用的IP、端口变化后,调用方也要同步修改。通过服务发现,将服务之间IP与端口的依赖转化为服务名的依赖,服务名可以根据具体微服务业务来做标识。
(2)对服务进行动态管理:
在微服务架构中,服务数量多且依赖错综复杂,无论是服务主动停止、意外挂掉,还是因为流量增加对服务扩容,这些服务状态上的动态变化,都需要尽快的通知到被调用方,被调用方才采取相应的措施。所以,对于服务注册中心要实时管理服务的数据与状态,包括服务的注册上线、服务主动下线,异常服务的剔除。
(3)降低服务端负载均衡中间件的压力:
当服务越来越多时,服务 URL 配置管理变得非常困难,服务端的负载均衡中间件,比如 F5、Nginx 压力也越来越大。通过服务注册中心,就可以实现动态地注册和发现服务,使服务的位置透明,并通过在消费方获取服务提供方地址列表,实现软负载均衡和 Failover,降低对服务端的负载均衡中间件,也能减少部分成本。
上面提到,硬件的 F5、软件的 Nginx 也可以实现服务的发现,那么这与注册中心的服务发现有什么区别呢?这其实是服务发现与注册的两种实现模式:服务端的发现模式 和 客户端的发现模式。F5、Nginx 属于服务端的发现模式,服务注册中心属于客户端的发现模式,两种模式各有优缺点,也适用于不同的场景,对于大型应用一般会有多层负载,外层用服务器端负载均衡,内部用客户端负载均衡。接下来我们就具体看看两种服务发现模式是怎么样的:
(1)服务端的发现模式:
服务端的发现模式是通过使用一个中间的服务器,来屏蔽被调用服务的复杂性与变动性,当有新的服务加入或老服务剔除时,只需要修改中间服务器上的配置即可,此模式的显著特点是:引入独立的中间代理服务器来屏蔽真实服务的具体细节。
如下图所示:当服务A要调用服务B时,先通过 DNS 域名解析找到 Nginx 服务器,然后将请求发送给Nginx,因为在 Nginx 上配置了服务B的真实访问地址,Nginx 收到请求后根据负载均衡算法,将请求转发到某个真实的服务B,服务B将请求结果返回给 Nginx,Nginx 再将返回结果给服务A,整个请求流程结束。当然中间服务器不一定非得是 Nginx,还可以是基于硬件的 F5,也可以是工作在传输层的 IP 负载均衡等。
该模式的优点是:配置集中在独立的中间服务器端完成,对代码没有任何入侵,也不存在跨平台跨语言的问题。但缺点也很明显,因为所有请求都需要穿透中间服务器,所以中间服务器会成为一个单点,对性能也会有所影响。
(2)客户端的发现模式:
我们再看看客户端的发现模式,服务A调用服务B时,不需要通过中间服务器,而是在自己进程内维护了服务B的信息,再通过负载算法选择一个服务B直接调用。那服务A具体是怎么维护服务B的信息呢?为此引入了服务注册中心的概念,当服务B启动时向注册中心注册自己(将自己的信息发送到注册中心的注册表里),服务A再从注册中心获取所有注册的服务,这就是客户端模式的基本原理。
客户端模式因为在进程内直接调用服务,也叫做进程内负载,由于不需要穿透中间服务器,所以客户端模式的性能损耗比较小。但是,需要在服务内部维护服务注册信息,负载算法等,有一定的代码入侵性,对于跨平台,跨语言的支持不太友好。
微服务架构中,所有的服务启动后都通过注册中心来注册自己,同时把注册中心里面的服务信息拉回本地,后续调用时就直接检查本地的服务和节点信息来进行服务节点的调用。每个服务节点都会来注册中心进行服务注册,那注册信息是如何在服务端保存的呢,其实就是注册表,服务注册的时候把自己的信息上报上来,然后注册中心把注册表,返回给客户端,那服务之间就知道要调用服务的节点了。
服务注册表需要高可用而且随时更新。客户端能够缓存从服务注册表中获取的服务地址,然而,这些信息最终会过时,客户端也就无法发现服务实例。因此,服务注册表会包含若干服务端,并使用复制协议保持一致性。服务注册表不能是单点,否则存在单点故障,当服务注册表有多台服务器的时需要考虑服务注册表的信息在多台机器上的实时同步和一致。
(1)Zookeeper 和 Consul 遵循 CP 原则,保证了强一致性和分区容错性,放弃可用性,在分布式环境中,如果涉及数据存储的场景,数据一致性应该是首先被保证的,但对于服务发现来说,可用性才是最核心的,针对同一个服务,即使注册中心的不同节点保存的服务提供者信息不相同,也并不会造成灾难性的后果。因为对于服务消费者来说,能消费才是最重要的,消费者拿到不正确的服务实例信息后尝试消费一下,也胜过因为无法获取实例信息而不去消费而导致系统异常
(2)Eureka 遵循 AP 原则,保证可用性,放弃数据一致性,基本能满足注册中心所需的核心功能,但 Eureka 2.x 版本已停止开发,并且宣布如果继续使用的话,风险自负。
(3)Nacos 同时支持 AP 与 CP,默认是 AP,同时功能更丰富,与 SpringCloud Alibaba 的兼容性更好,使用更简单灵活,可以满足更多的业务场景,且支持 K8S 的集成。
不同的服务注册中心组件的应用场景不同,读者可以根据自己的业务情况进行选型。但下文我们主要以 Nacos 注册中心为例进行介绍,其他几种注册中心读者自行上网查阅
我们先去 Nacos 的 Github(Tags · alibaba/nacos · GitHub)下载我们所需的 Nacos 版本,可以选择 windows 或者 Linux,如下图:
由于当时在搭建项目的时候,考虑到与 SpringBoot 和 SpringCloud 的版本对应问题,我这里是下载了 2.0.0 的版本进行搭建,读者可以根据自己的情况选择对应的 Nacos 版本。
下载并解压 nacos-server-2.0.0.zip,解压完成后进入 /bin 目录,可以看到下面两个脚本:
windows 环境直接运行 startup.cmd 启动项目,出现以下界面则启动完成:
在浏览器输入 http://localhost:8848/nacos 进入Nacos的登录界面,用户名与密码默认都是 nacos,登录成功后界面如下:
Nacos 在 Linux 环境下的启停跟在 windows 环境的启停基本一致,先下载 nacos-server-2.0.0.tar.zip 压缩包,然后上传到 Linux 服务器上进行解压(解压命令:tar -zxvf nacos-server-2.0.0.tar.gz),解压完成后同样进入 /bin 目录执行启动命令(单机模式启动命令:sh startup.sh -m standalone),启动完成后再访问 nacos 控制台地址(http://服务器ip地址:8848/nacos/index.html)验证是否成功启动即可。
我们首先看一下 nacos 的简单架构图:
参照上面的架构图,我们分别创建两个模块,分别是 cloud-producer-server(服务提供者)、cloud-consumer(服务消费者),职责如下:
创建这两个模块前,我们先声明项目的版本信息:
<properties>
<spring-boot.version>2.3.2.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
</properties>
<!-- 只声明依赖,不引入依赖 -->
<dependencyManagement>
<dependencies>
<!-- 声明springBoot版本 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 声明springCloud版本 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 声明 springCloud Alibaba 版本 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
(1)引入maven依赖:
<!-- 引入阿里的nacos作为服务注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
(2)添加 nacos 相关的配置信息:
在 application.properties 配置文件指定服务名称、端口号、nacos-server 的地址信息,如下:
spring.application.name = cloud-producer-server
server.servlet.context-path = /${spring.application.name}
server.port=9000
# nacos注册中心配置
spring.cloud.nacos.discovery.server-addr = localhost:8848
spring.cloud.nacos.discovery.namespace = 91b5489b-d009-4725-86fa-534f760b4d04
spring.cloud.nacos.discovery.register-enabled = true
(3)开启服务注册发现的功能:
在主 Application 启动类加入 @EnableDiscoveryClient 注解开启服务注册发现的功能,如下:
/**
* SpringBoot启动类
* @EnableDiscoveryClient 开启服务注册发现的功能
*/
@EnableDiscoveryClient
@SpringBootApplication
public class ProducerApplication
{
public static void main(String[] args)
{
SpringApplication.run(ProducerApplication.class, args);
}
}
(4)实现个演示功能:
cloud-producer-server 作为服务提供者注册到 nacos 中,肯定需要提供个服务来供消费者 cloud-consumer 调用,下面简单写一个演示接口:
@RestController
@RequestMapping (value = "/")
public class CloudController
{
@PostMapping ("getSum")
public String getSum(@RequestParam (value = "num1") Integer num1, @RequestParam (value = "num2") Integer num2)
{
return "success:两数求和结果=" + (num1 + num2);
}
}
(5)启动项目:
启动项目之后,我们进入 nacos 控制台,在 nacos 的 “服务管理->服务列表” 的 “91b5489b-d009-4725-86fa-534f760b4d04” 空间中将会发现注册进入的 cloud-producer-server 这个服务,如下图:
服务消费者的创建步骤与服务提供者基本一致
(1)引入maven依赖:
<!-- 引入阿里的nacos作为服务注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
(2)添加 nacos 相关的配置信息:
spring.application.name = cloud-consumer
server.port=9001
# nacos注册中心配置
spring.cloud.nacos.discovery.server-addr = localhost:8848
spring.cloud.nacos.discovery.namespace = 91b5489b-d009-4725-86fa-534f760b4d04
spring.cloud.nacos.discovery.register-enabled = true
(3)开启服务注册发现的功能:
/**
* SpringBoot启动类
* @EnableDiscoveryClient 开启服务注册发现的功能
*/
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication
{
public static void main(String[] args)
{
SpringApplication.run(ConsumerApplication.class, args);
}
}
(4)调用服务提供方的演示功能:
cloud-producer-server 服务提供方提供一个演示功能,那我们如何调用该功能呢?其实 Nacos 集成了 Ribbon(有关 Ribbon 的详细介绍请参考这篇文章:https://blog.csdn.net/a745233700/article/details/122916856),因此我们便能使用 Ribbon 的负载均衡来调用服务,步骤如下:
① 创建 RestTemplate,使用 @LoadBalanced 注解标注开启负载均衡:
@Configuration
public class RestConfig
{
/**
* 创建restTemplate对象。
* LoadBalanced注解表示赋予restTemplate使用Ribbon的负载均衡的能力(一定要加上注解,否则无法远程调用)
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
② 通过 RestTemplate 请求远程服务地址并接收返回值
@RestController
@RequestMapping (value = "api/invoke")
public class InvokeController
{
@Autowired
private RestTemplate restTemplate;
/**
* 使用 RestTemplate 进行远程服务调用,并且使用 Ribbon 进行负载均衡
*/
@ApiOperation (value = "RestTemplate", notes = "使用RestTemplate进行远程服务调用,并使用Ribbon进行负载均衡")
@GetMapping ("getByRestTemplate")
public String getByRestTemplate(Integer num1, Integer num2)
{
//第一个cloud-producer-server代表在nacos注册中心中的服务名,第二个cloud-producer-server代表contextPath配置的项目路径
String url = "http://cloud-producer-server/cloud-producer-server/getSum";
MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
params.add("num1", num1);
params.add("num2", num2);
//通过服务名的方式调用远程服务(非ip端口)
return restTemplate.postForObject(url, params, String.class);
}
}
(5)启动测试,查看nacos注册中心控制面板情况
启动成功后将会在 nacos 中的服务列表中看到 cloud-consumer,如下图:
那么接下来就测试下服务能否调的通?访问服务消费方的 api/invoke/getByRestTemplate接口,可以看到请求结果如下:
前面的介绍中,我们并未对 Nacos 服务端做任何特殊的配置,一切均以默认的单机模式运行,但是单机运行模式仅适用于学习与测试环境,对于有高可用要求的生产环境显然是不合适的。那我们怎么搭建支持高可用的集群环境呢?
在搭建 Nacos 集群前,我们需要先修改 Nacos 的数据持久化配置为 MySQL 存储。默认情况下,Nacos 使用内嵌的数据库 Derby实现数据的存储,这种情况下,如果启动多个默认配置下的 Nacos 节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos 采用了集中式存储的方式来支持集群化部署,但目前 Nacos 只支持 MySQL 的存储,且版本要求:5.6.5+
(1)初始化 MySQL 数据库:
首先在 MySQL 中新建一个数据库 nacos-config(名称随意),然后执行 Nacos 中的SQL脚本,该脚本是 Nacos-server 的 conf 文件夹中的 nacos-mysql.sql,如下图:
执行该脚本,将会自动创建表,如下图:
(2)修改 conf/application.properties 配置文件:
Nacos-server 也是一个Spring Boot 项目,想要连接自己的数据库,当然要配置数据源了,配置文件同样在 Nacos-server 中的 conf 目录下,如下图:
只需要将 application.properties 中的 Mysql 配置成自己的数据源并重启 Nacos-server 即可 ,如下:
# 此项一定要启用,默认是注释掉的
spring.datasource.platform=mysql
# 注意MySQL8.0以上版本指定url时一定要带入serverTimezone参数
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_config?serverTimezone=Asia/Shanghai&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456
# 可选启用配置
nacos.cmdb.dumpTaskInterval=3600
nacos.cmdb.eventTaskInterval=10
nacos.cmdb.labelTaskInterval=300
nacos.cmdb.loadDataAtStart=false
Nacos 官方推荐在生产环境使用集群模式部署,这样可以避免单点故障,集群化部署的架构图如下:
请求先通过 Nginx 集群进行转发到 Nacos 集群中,当然为了保持高可用,数据库也需要是集群模式。那么接下来我们就演示下搭建 Nacos 集群的方法。
由于条件限制,我们仅在一台服务器上启动三个Nacos服务演示。Nacos的端口分别为8848、8849、8850。
(1)修改端口号:
Nacos 默认的端口号是 8848,那么如何修改端口呢?只需要修改 conf 目录下的 application.properties 中的 server.port 即可,如下图:
(2)修改集群配置:
那么如何配置集群呢?在 conf 目录下有一个 cluster.conf.example 文件,如下图:
只需要将 cluster.conf.example 这个文件复制一份为 cluster.conf 放在 conf 目录下,其中配置的内容如下:
172.16.1.84:8848
172.16.1.84:8849
172.16.1.84:8850
(3)修改数据源:
这个在持久化的那里已经讲过了,只需要将 application.properties 中的数据源替换掉,如下:
# 此项一定要启用,默认是注释掉的
spring.datasource.platform=mysql
# 注意MySQL8.0以上版本指定url时一定要带入serverTimezone参数
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_config?serverTimezone=Asia/Shanghai&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456
# 可选启用配置
nacos.cmdb.dumpTaskInterval=3600
nacos.cmdb.eventTaskInterval=10
nacos.cmdb.labelTaskInterval=300
nacos.cmdb.loadDataAtStart=false
(4)启动Nacos:
经过上述的步骤 Nacos 集群已经配置好了,启动 Nacos 成功后,访问任意一个端口的 Nacos 服务,在 “集群管理->节点列表” 中将会看到自己搭建的三个节点,如下图:
至此,Nacos集群算是搭建完成了
(5)Nginx 中的配置:
此处就不演示Nginx集群搭建了,直接在单机的Nginx中配置。直接修改nginx的conf文件,内容如下:
upstream nacos{
server 172.16.1.84:8848;
server 172.16.1.84:8849;
server 172.16.1.84:8850;
}
server{
listen 80;
location / {
proxy_pass http://nacos;
}
}
(6)项目中配置 server-addr:
既然搭建了集群,那么项目中也要配置一下,有两种方式,下面分别介绍:
第一种:通过直连的方式配置,如下:
spring:
application:
## 指定服务名称,在nacos中的名字
name: cloud-producer-server
cloud:
nacos:
discovery:
# nacos的服务地址,nacos-server中IP地址:端口号
server-addr: 172.16.1.84:8848,172.16.1.84:8849,172.16.1.84:8850
第二种:直接连接Nginx,如下:
spring:
application:
## 指定服务名称,在nacos中的名字
name: cloud-producer-server
cloud:
nacos:
discovery:
# nacos的服务地址,nacos-server中IP地址:端口号
server-addr: 172.16.1.84:80
Nacos 集群搭建非常简单,唯一的配置就是在 cluster.conf 中设置三个 Nacos 服务的地址。
参考文章:
五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?
最近我在用 RestSharp消耗我的 Restful 资源。并期望在服务器和客户端之间与 JSon 交换数据。下面是我的 C# 代码。 var client = new RestSharp.Rest
我正在阅读 Bartosz Milewski 的一篇文章,其中他定义了以下函数: instance Applicative Chan where pure x = Chan (repeat x)
‘…' 其实是go的一种语法糖。 它的第一个用法主要是用于函数有多个不定参数的情况,可以接受多个不确定数量的参数。 第二个用法是slice可以被打散进行传递。 实例:
前言 在算face_track_id map有感: 开始验证 data={"state":[1,1,2,2,1,2,2,2],"pop":[&quo
本文实例讲述了php访问数组最后一个元素的函数end()用法。分享给大家供大家参考。具体分析如下: end()函数在PHP中用于检索数组中的最后一个元素。end()函数需要一个数组作为其唯一参数,
我使用的是 jdk1.8.0_92。我的虚拟机如下所示。 $java -version java version "1.8.0_92" Java(TM) SE Runtime Environment
我的情况是我需要将所有匹配 http://mywebsite.com/portfolio/[anyname] 的请求定向到 http://mywebsite.com/portfolio.php?用户名
我正在尝试在 NLTK 中使用语音标记并使用了以下命令: >>> text = nltk.word_tokenize("And now for something completely differe
#include typedef QList IntList; qRegisterMetaType("IntList"); error C2909: 'qRegisterMetaType':
来自 here我知道 BN_CTX 是一个保存 BIGNUM 临时变量的结构。这些 BIGNUM 变量什么时候会进入 BN_CTX 的 BN_POOL?如果我有一个 bignum_ctx BN_CTX
尝试为 ABPersonRef 创建对象例子:ABpersonRef 引用; 已包含Addressbook和AddressBookUI框架即使这样,当我编译时,它仍显示“ABPersonRef”未声明
我无法使用 GetAltTabInfo。可能是一个愚蠢的错误,但这有什么问题呢? HWND taskSwitcher = FindWindow(L"TaskSwitcherWnd", L"Task S
JSLint4Java 是 JSLint 的 Java 包装器。我需要这样的东西在我的 GWT 项目中使用,但使用 JSLint4Java 的唯一方法似乎是从命令行或通过 ANT 任务。有谁知道是否有
我有一个持久化实体对象的方法 persistData() 。我有另一个方法 findData() ,它对同一实体类执行 find() 操作以获取持久的主键值。当我在实体类的@PostPersist中调
下面是我的代码。请查看。 1. bool isUnavailable = db.Deploys.Where(p => p.HostEnvironmentId == Guid.Parse(h
这个问题已经有答案了: Why can't a Generic Type Parameter have a lower bound in Java? (6 个回答) 已关闭 9 年前。 我试图理解为什
我正在尝试使用 scala 编译器 Y 警告,但我认为我做得不对。在下面的示例中,nums 未使用,因此我希望 -Ywarn-value-discard 打印一个警告。有两个 if 条件,一个嵌套在另
用户被要求从某个给定的集合中选择一个 ID。我检查该 ID 是否存在于我的集合中,如果不存在,我会抛出 IndexOutOfBoundsException 并稍后捕获它。我实际上可以使用该异常来达到这
我正在尝试减少从 OSM 路径数据生成的形状文件。我正在使用 VTS 的 DouglasPeuckerSimplifier 实现。我想为特定 GTFS(通用交通提要规范)构建路线图的 geojson。
我明白了?!是排除某个模式,例如 a(?!b) 表示如果“a”后面没有“b”,它将匹配“a”。我的问题是,假设我有一个包含以下内容的文件: a cat is a cat, a dog is a dog
我是一名优秀的程序员,十分优秀!