在微服务架构中,整个系统会按照功能划分为多个服务,通过服务之间协作来实现复杂的业务目标。这样在我们的代码中免不了要进行服务之间的远程调用,为了完成一次服务消费方调用服务生产方的请求,服务消费方需要知道服务生产方的具体网络位置,即 IP地址和端口号。
通常,我们最容易想到的办法是在服务消费方的配置文件中配置服务生产方的网络地址,如下图:
然后通过网络工具库发起调用。下面我们将通过 Spring Boot 创建两个简单的服务来演示服务生产和服务消费之间的调用,项目结构如下图:
父项目的 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>com.hxstrive.nacos</groupId> <artifactId>springcloud_alibaba_nacos</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>nacos_spring_cloud4</artifactId> <name>nacos_spring_cloud4</name> <packaging>pom</packaging> <modules> <module>service1</module> <module>service2</module> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencyManagement> <dependencies> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.3.RELEASE</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>
服务生产将提供一个简单的 get 接口,该接口将返回一个包含当前服务端口的字符串信息,依赖信息如下:
<?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>com.hxstrive.nacos</groupId> <artifactId>nacos_spring_cloud3</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>service1</artifactId> <name>service1</name> <dependencies> <!-- Spring Boot Begin --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Boot End --> </dependencies> </project>
配置信息(application.yaml)如下:
server: port: 8081 spring: application: name: service1
启动类如下:
package com.hxstrive.nacos; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * 入口类 * @author hxstrive.com */ @SpringBootApplication @RestController public class Service1App { public static void main(String[] args) { SpringApplication.run(Service1App.class, args); } @Value("${server.port}") private String port; // 提供一个 get 接口,获取端口信息 @GetMapping("/get") public String get() { return toString(); } @Override public String toString() { return "Service1App{" + "port='" + port + '\'' + '}'; } }
服务消费将在配置文件中配置服务生产的网络地址,然后通过 RestTemplate 工具类发起网络调用,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>com.hxstrive.nacos</groupId> <artifactId>nacos_spring_cloud3</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>service2</artifactId> <name>service2</name> <dependencies> <!-- Spring Boot Begin --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Boot End --> </dependencies> </project>
配置信息(application.yaml)如下:
server: port: 8082 spring: application: name: service2 # 服务1的地址 service1_url: http://127.0.0.1:8081
启动类如下:
package com.hxstrive.nacos; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.net.URI; import java.net.URISyntaxException; /** * 入口类 * @author hxstrive.com */ @SpringBootApplication @RestController public class Service2App { public static void main(String[] args) { SpringApplication.run(Service2App.class, args); } @Value("${service1_url}") private String url; // 通过 RestTemplate 调用服务生产 @GetMapping("/get") public String get() { try { RestTemplate restTemplate = new RestTemplate(); return "Service2App: " + restTemplate.getForObject(new URI(url + "/get"), String.class); } catch (URISyntaxException e) { throw new RuntimeException(e); } } }
分别启动服务生产和服务消费两个服务,然后使用浏览器访问服务生产,如下图:
再访问服务消费,输出如下图:
从上图可知,成功调用到了服务生产。
虽然上面示例成功的进行了服务间调用/协调,但是有很多问题:假如我们有上百个服务,那么是不是需要再配置文件中配置上百个服务的网络地址?如果相同的服务部署了多个,我们该怎么处理?并且,每次调用服务都需要我们自己拼接服务调用 URL 字符串等等问题。为了在微服务框架中解决这个问题,提出了服务发现的概念。
服务发现是一种计算机网络领域的概念,用于帮助分布式应用程序或微服务架构中的不同组件或服务相互定位和通信。在这种架构中,许多小型服务通常以独立的方式运行,它们需要相互协作以执行复杂的任务。服务发现有助于这些服务相互发现和交流,从而构建弹性和可伸缩的系统。
服务发现的主要目标是:
(1)服务注册: 当新的服务实例启动时,它们向服务发现系统注册自己,提供自己的网络地址和元数据信息,如: IP 地址、端口号、服务类型、版本号等。
(2)服务查询: 其他服务或应用程序可以向服务发现系统查询特定服务的位置和信息,以便与之通信。这使得服务之间的通信更加动态,因为它们不需要硬编码对方的地址或配置。
(3)负载均衡: 服务发现系统可以管理多个服务实例,并为请求选择合适的目标服务实例,以实现负载均衡和高可用性。这有助于提高系统的性能和稳定性。
(4)故障处理: 当服务实例发生故障或下线时,服务发现系统可以自动检测并从服务列表中移除不可用的实例,以确保请求不会发送到不可用的服务上。
(5)动态配置: 服务发现可以用于动态配置管理,以便根据环境的变化自动更新服务配置。
目前,常见的服务发现工具包括 Consul、Eureka、ZooKeeper、Nacos 等等,这些服务发现工具可以帮助开发人员更容易地构建和管理复杂的分布式系统,支持微服务架构的需求。
下图就是平时经常用到的服务发现产品的对比:
服务健康检查:Euraka 使用时需要显式配置健康检查支持;Zookeeper、Etcd 则在失去了和服务进程的连接情况下任务不健康,而 Consul 相对更为详细点,比如内存是否已使用了90%,文件系统的空间是不是快不足了。
多数据中心:Consul 和 Nacos 都支持,其他的产品则需要额外的开发工作来实现。
KV 存储服务:除了 Eureka,其他几款都能够对外支持 k-v 的存储服务,所以后面会讲到这几款产品追求高一致性的重要原因。而提供存储服务,也能够较好的转化为动态配置服务。
CAP 理论的取舍:
Eureka 是典型的 AP,Nacos可以配置为 AP,作为分布式场景下的服务发现的产品较为合适,服务发现场景的可用性优先级较高,一致性并不是特别致命。
而Zookeeper、Etcd、Consul则是 CP 类型牺牲可用性,在服务发现场景并没太大优势;
Watch的支持:Zookeeper 支持服务器端推送变化,其它都通过长轮询的方式来实现变化的感知。
自身集群的监控:除了 Zookeeper 和 Nacos,其它几款都默认支持 metrics,运维者可以搜集并报警这些度量信息达到监控目的。
Spring Cloud 的集成:目前都有相对应的 boot starter,提供了集成能力。
CAP 是分布式计算和数据库系统中的一个重要理论概念,它代表了三个核心属性:一致性(Consistency)、可用性(Availability)、分区容忍性(Partition Tolerance)。这个概念最早由计算机科学家埃里克·布鲁尔(Eric Brewer)在 2000 年提出。
以下是对 CAP 的三个属性的解释:
一致性(Consistency): 这意味着在分布式系统中的所有节点上,数据的状态应该保持一致。无论客户端向任何节点发出请求,都应该看到相同的数据视图。这意味着如果一个节点接受了一次写请求,那么所有其他节点在稍后的读取请求中应该返回该写操作的结果。
可用性(Availability): 可用性指的是系统应该对请求保持响应,即使出现部分故障或节点故障的情况下也是如此。在分布式系统中,某些节点可能无法响应请求,但系统仍然应该继续处理其他请求,而不是完全宕机。
分区容忍性(Partition Tolerance): 分区容忍性是指在面临网络分区(例如,某些节点之间的通信故障)的情况下,系统仍然能够继续运行。这意味着即使网络中的某些部分不可达,系统也应该能够处理请求。
CAP理论提出了一个悖论:在分布式系统中,无法同时满足这三个属性,最多只能同时满足其中两个。这就意味着在设计分布式系统时,必须做出权衡,选择满足哪些属性更重要,具体取决于应用程序的需求和使用情况。
例如,一些系统可能会优先保证一致性和分区容忍性,即使牺牲一些可用性。而其他系统可能更注重可用性和分区容忍性,愿意牺牲一致性,以确保系统在面临网络分区时仍然能够提供服务。这种权衡通常取决于应用程序的特点和可接受的业务风险。