Spring Cloud封神之旅-微服务与springcloud
Posted gonghaiyu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud封神之旅-微服务与springcloud相关的知识,希望对你有一定的参考价值。
微服务
微服务架构的提出者 Martin Fowler 在其文章Microservices中定义了包括服务组件化、去中心化、基础设施自动化在内的多个微服务架构特点。
微服务架构三大要素,即如下图所示的业务建模、技术体系和研发过程。
微服务架构的第一要素:业务建模
为什么我们首先需要考虑的是这个要素呢?因为微服务架构与传统 SOA 等技术体系有本质区别,就是其服务的粒度和服务本身的面向业务和组件化特性。针对服务建模,我们首先需要明确服务的类别,以及服务与业务之间的关系,尽可能明确领域的边界。
针对服务建模,推荐使用领域驱动设计(Domain Driven Design,DDD)方法,通过识别领域中的各个子域、判断这些子域是否独立、考虑子域与子域的交互关系,从而明确各个界限上下文(Boundary Context)之间的边界。
对于领域的划分,业界主流的分类方法认为,系统中的各个子域可以分成核心子域、支撑子域和通用子域三种类型,其中系统中的核心业务属于核心子域,专注于业务某一方面的子域称为支撑子域,可以作为某种基础设施的功能可以归到通用子域。下面以电商系统为例子。
务围绕业务能力建模,而业务能力往往体现的是一种分层结构。按照我的经验,我们可以把业务体系中的服务分成如下几种类型:基础服务、通用服务、定制服务和其他服务等。这里,我们同样给出基于电商场景的业务服务分层示例图,如下所示:
每个行业、每个公司具有不同的业务体系和产品形态,我无意对业务建模的应用场景做过多展开。但在课程的后续内容中,我们会基于 DDD 设计思想,并通过一个具体的案例来介绍如何完成对系统的业务建模,以帮助你在日常开发过程中掌握如何使用 DDD 来完成对业务系统的领域建模的系统方法。
微服务架构的第二要素:技术体系
在本课程中,我也基于目前业界主流的微服务实现技术提炼了八大技术体系,包括服务通信、服务治理、服务路由、服务容错、服务网关、服务配置、服务安全和服务监控。
服务通信
对于微服务架构而言,我们关注的是网络连接模式、I/O 模型和服务调用方式。
我们知道基于TCP 协议的网络连接有两种基本方式,也就是通常所说的长连接和短连接。 Dubbo 框架就采用的是长连接,而本课程中要介绍的 Spring Cloud 则采用了短连接。
服务之间通信的另一个关注点是 I/O 模型。I/O 模型也有阻塞式 I/O 和非阻塞式 I/O 等多种实现方式。以服务网关而言,像Netflix 的 Zuul就是阻塞式 I/O,而Spring 自研的 Spring Cloud Gateway则采用的是非阻塞式 I/O。
服务通信的另一个主题是调用方式,这方面同样存在同步调用和异步调用两大类实现机制。为了简化开发人员的使用过程,通常都会采用异步转同步的实现机制,也就是说开发人员使用同步的方式进行方法调用,而框架本身会基于 Future 等机制实现异步的远程处理。
服务治理
服务注册中心是保存服务调用所需的路由信息的存储仓库,也是服务提供者和服务消费者进行交互的媒介,充当着服务注册和发现服务器的作用。诸如 Dubbo、Spring Cloud 等主流的微服务框架都基于 Zookeeper、Eureka 等分布式系统协调工具构建了服务注册中心。
服务路由
Spring Cloud 等主流的微服务框架也都内置了 Ribbon 等客户端负载均衡组件。
另一方面,负载均衡的出发点更多的是提供服务分发而不是解决路由问题,常见的静态、动态负载均衡算法也无法实现精细化的路由管理。这时候我们就可以采用路由规则。路由规则常见的实现方案是白名单或黑名单,即把需要路由的服务地址信息(如服务 IP)放入可以控制是否可见的路由池中进行路由。同样,路由规则也是微服务开发框架的一项常见功能。
服务容错
业界存在一批与服务容错相关的技术组件,包括以失效转移 Failover 为代表的集群容错策略,以线程隔离、进程隔离为代表的服务隔离机制,以滑动窗口、令牌桶算法为代表的服务限流机制,以及服务熔断机制。而从技术实现方式上看,在 Spring Cloud 中,这些机制部分包含在下面要介绍的服务网关中,而另一部分则被提炼成单独的开发框架,例如专门用于实现服务熔断的 Spring Cloud Circuit Breaker 组件。
服务网关
服务网关的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。
在功能设计上,服务网关在完成客户端与服务器端报文格式转换的同时,它可能还具有身份验证、监控、缓存、请求管理、静态响应处理等功能。另一方面,也可以在网关层制定灵活的路由策略。针对一些特定的 API,我们需要设置白名单、路由规则等各类限制。在本课程中,我们会基于 Netflix Zuul 和 Spring Cloud Gateway 这两种网关对这些功能分别展开介绍。
服务配置
在微服务架构中,考虑到服务数量和配置信息的分散性,一般都需要引入配置中心的设计思想和相关工具。与注册中心一样,配置中心也是微服务架构中的基础组件,其目的也是对服务进行统一管理,区别在于配置中心管理的对象是配置信息而不是服务的实例信息。
为了满足以上要求,配置中心通常需要依赖分布式协调机制,即通过一定的方法确保配置信息在分布式环境中的各个服务中能得到实时、一致的管理。可以采用诸如 Zookeeper 等主流的开源分布式协调框架来构建配置中心。当然,像 Spring Cloud 也提供了专门的配置中心实现工具 Spring Cloud Config。
服务安全
一般意义上的访问安全性,都是围绕认证和授权这两个核心概念来展开的。也就是说我们首先需要确定用户身份,然后再确定这个用户是否有访问指定资源的权限。站在单个微服务的角度讲,我们系统每次服务访问都能与授权服务器进行集成以便获取访问 Token。站在多个服务交互的角度讲,我们需要确保 Token 在各个微服务之间的有效传播。另一方面,服务内部,我们可以使用不同的访问策略限制服务资源的访问。
在实现微服务安全访问上,我们通常使用 OAuth2 协议来实现对服务访问的授权机制,使用 JWT 技术来构建轻量级的认证体系。Spring 家族也提供了 Spring Security 和 Spring Cloud Security 框架来完整这些组件的构建。
服务监控
在微服务架构中,当服务数量达到一定量级时,我们难免会遇到两个核心问题。一个是如何管理服务之间的调用关系?另一个是如何跟踪业务流的处理过程和结果?这就需要构建分布式服务跟踪机制。
分布式服务跟踪机制的建立需要完成调用链数据的生成、采集、存储及查询,同时也需要对这些调用链数据进行运算和可视化管理。这些工作不是简单一个工具和框架能全部完成,因此,在开发微服务系统时,我们通常会整合多个开发框架进行链路跟踪。例如,在 Spring Cloud 中,就提供了 Spring Cloud Sleuth 与 Zipkin 的集成方案。
微服务架构的第三要素:研发过程
Martin Fowler 在介绍微服务架构时,同样也提出了围绕“业务功能”组织团队的研发管理理念。
当寻找把一个大的应用程序进行拆分的方法时,研发过程通常都会围绕产品团队、项目管理、大前端和服务器端团队而展开,这些团队也就是通常所说的职能团队。任何一个需求,无论大小,都将导致跨团队协作,从而增加沟通和协作成本。而微服务架构则倾向围绕业务功能的组织来分割服务,而不是面向某项技术能力。因此,团队是跨职能的特征团队,每个服务都围绕着业务进行构建,并且能够被独立部署到生产环境。这部分内容并不是本课程的重点,我们不做进一步展开。
Spring Cloud介绍
Spring Cloud 所基于的 Spring Boot,已经成为 Java EE 领域中最流行的开发框架,用来简化 Spring 应用程序的框架搭建和开发过程。
在设计思想上,Spring Boot 充分利用约定优于配置(Convention over Configuration)的自动化配置机制。与传统的 Spring 应用程序相比, Spring Boot 在启动依赖项自动管理、简化部署并提供应用监控等方面对开发过程做了优化。
Spring Cloud 中的组件非常多,我们无意对所有组件都进行详细展开,而是梳理了开发一个微服务系统所必需的八大核心组件。如下图所示。
案例驱动
在物联网和智能穿戴式设备日益发达的当下,试想一下这样的日常场景,患者通过智能手环、便携式脉诊仪等一些智能穿戴式设备检测自身的各项健康信息,然后把这些健康信息实时上报到云平台,云平台检测到用户健康信息中的异常情况时会通过人工或自动的方式进行一定的健康干预,从而确保用户健康得到保证。这是大健康领域非常典型的一个业务场景,也是我们案例的来源。
领域驱动
从领域建模的角度进行分析,我们可以把该系统分成三个子域,即:
- 用户(User)子域,用于用户管理,用户可以通过注册成为系统用户,同时也可以修改或删除用户信息,并提供用户信息有效性验证的入口。
- 设备(Device)子域,用于设备管理,医护人员可以查询某个用户的某款穿戴式设备以便获取设备的详细信息,同时基于设备获取当前的健康信息。
- 健康干预(Intervention)子域,用于健康干预管理,医护人员可以根据用户当前的健康信息生成对应的健康干预。当然,也可以查询自己所提交健康干预的当前状态。
从子域的分类上讲,用户子域比较明确,显然应该作为一种通用子域。而健康干预是 SpringHealth 的核心业务,所以应该是核心子域。至于设备子域,在这里比较倾向于归为支撑子域,如下图所示:
基于以上分析,我们可以把 SpringHealth 划分成三个微服务,即 user-service、device-service 和 intervention-service。下图展示了 SpringHealth 的基本架构,在图中,intervention-service 需要基于 REST 风格完成与 user-service 和 device-service 服务之间的远程交互。
服务设计
服务列表
当我们采用 Spring Cloud 构建完整的微服务技术解决方案时,部分技术组件需要通过独立服务的形式进行运作,具体包括:
(1)注册中心服务。我们将这个服务命名为 eureka-server。
(2)配置中心服务。我们将这个服务命名为 config-server。
(3)API网关服务。针对 Zuul 和 Spring Cloud Gateway 这两款工具,我们建立两个独立的 zuul-server 和 gateway-server 服务,并根据需要分别采用其中的一个服务进行运行。
(4)安全授权服务。我们把这个服务命名为 auth-server。
(5)案例中最后一个基础设施类服务是 Zipkin 服务,这个服务并不是必需的,而是取决于我们是否需要对服务访问链路进行可视化展示。因此将构建一个独立的 zipkin-server 服务。
以上这种划分只是一种场景。
服务数据
在案例中,我们针对三个业务服务,也将建立独立的三个数据库,数据库的访问信息通过配置中心进行集中管理,如下图所示:
使用 Spring Cloud 实现服务治理
服务治理
从架构设计上讲,状态变更管理可以采用发布-订阅模式,体现在服务提供者可以根据服务定义发布服务,而服务消费者则通过对自己感兴趣的服务进行订阅并获取包括服务地址在内的各项元数据。发布-订阅功能还体现在状态变更推送,即当注册中心服务定义发生变化时,主动推送变更到该服务的消费者。
基于发布-订阅设计思想,就诞生了一种服务监听机制。服务监听机制确保服务消费者能够实时监控服务更新状态,是一种被动接收变更通知的实现方案,通常采用监听器以及回调机制,如下图所示。
服务注册
基于 Eureka 构建注册中心(不建议使用,厂家放弃更新)
构建单点 Eureka 服务器
新建 Maven 工程并命名为 eureka-server。同时我们引入了 spring-cloud-starter-eureka-server 依赖,该依赖是 Spring Cloud 中实现 Spring Cloud Netflix Eureka 功能的主体 jar 包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
创建 Spring Boot 的启动类 EurekaServerApplication,代码如下所示。包含 @EnableEurekaServer 注解的服务意味着就是一个 Eureka 服务器组件。
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Eureka 也为开发人员提供了一系列的配置项。这些配置项可以分成三大类,一类用于控制 Eureka 服务器端行为,以 eureka.server 开头;一类则是从客户端角度出发考虑配置需求,以 eureka.client 开头;而最后一类则关注于注册到 Eureka 的服务实例本身,以 eureka.instance 开头。请注意,Eureka 除了充当服务器端组件之外,实际上也可以作为客户端注册到 Eureka 本身,这时候它使用的就是客户端配置项。
现在,我们尝试在 eureka-server 工程的 application.yml 文件中添加了如下配置信息。我们不希望 Eureka 服务对自身进行注册,registerWithEureka、fetchRegistry都设置为false。
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:8761
构建 Eureka 服务器集群
我们通常都需要构建一个 Eureka 服务器集群来确保注册中心本身的可用性。与传统的集群构建方式不同,如果我们把 Eureka 也视为一个服务,也就是说 Eureka服务自身也能注册到其他 Eureka 服务上,从而实现相互注册,并构成一个集群。在 Eureka中,这种实现高可用的部署方式被称为 Peer Awareness 模式。
现在我们准备两个 Eureka 服务实例 eureka1 和 eureka2。在 Spring Boot 中,我们分别提供 application-eureka1.yml 和 application-eureka2.yml 这两个配置文件来设置相关的配置项。其中 application-eureka1.yml 配置文件的内容如下:
server:
port: 8761
eureka:
instance:
hostname: eureka1
client
serviceUrl
defaultZone: http:// eureka2:8762/eureka/
对应的,application-eureka2.yml 配置文件的内容如下:
server:
port: 8762
eureka:
instance:
hostname: eureka2
client
serviceUrl
defaultZone: http://eureka1:8761/eureka/
构建 Eureka 集群模式的关键点在于使用客户端配置项 eureka.client.serviceUrl.defaultZone 用于指向集群中的其他 Eureka 服务器。所以 Eureka 集群的构建方式实际上就是将自己作为服务并向其他注册中心注册自己,这样就形成了一组互相注册的服务注册中心以实现服务列表的同步。显然,这个场景下 registerWithEureka 和 fetchRegistry配置项应该都使用其默认的 true 值,所以我们不需要对其进行显式的设置。
如果你尝试使用本机搭建集群环境,显然 eureka.instance.hostname 配置项中的 eureka1 和 eureka2 是无法访问的,所以需要在本机hosts 文件中添加以下信息。
127.0.0.1 eureka1
127.0.0.1 eureka2
理解 Eureka 服务器实现原理
理解 Eureka 服务器实现原理
服务注册(Register)是服务治理的最基本概念,内嵌了 Eureka 客户端的各个微服务通过向 Eureka 服务器提供 IP 地址、端点等各项与服务发现相关的基本信息完成服务注册操作。
因为 Eureka 客户端与服务器端通过短连接完成交互,所以在服务续约(Renew)中,Eureka 客户端需要每隔一定时间主动上报自己的运行时状态,从而进行服务续约。
服务取消(Cancel)的意思就是 Eureka 客户端主动告知 Eureka 服务器自己不想再注册到 Eureka 中。当Eureka客户端连续一段时间没有向 Eureka 服务器发送服务续约信息时,Eureka 服务器就会认为该服务实例已经不再运行,从而将其从服务列表中进行剔除(Evict)。
Eureka 服务存储源码解析
对于一个注册中心而言,我们首先需要关注它的数据存储方法。在 Eureka 中,我们发现 InstanceRegistry 接口及其实现类(位于 com.netflix.eureka.registry 包中)承接了这部分职能。InstanceRegistry 的类层结构如下所示:
从上图中,不难看出 Spring Cloud 中同样存在一个 InstanceRegistry(位于 org.springframework.cloud.netflix.eureka.server 包中),它实际上是基于 Netflix 中 InstanceRegistry 实现的一种包装。我们在上图中 InstanceRegistry 接口的实现类 AbstractInstanceRegistry 中发现了 Eureka 用于保存注册信息的数据结构,如下所示:
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
可以看到这是一个双层的 HashMap,采用的是 JDK 中线程安全的 ConcurrentHashMap。其中第一层的 ConcurrentHashMap 的 Key 为 spring.application.name,也就是服务名,Value 为一个 ConcurrentHashMap;而第二层的 ConcurrentHashMap 的 Key 为 instanceId,也就是服务的唯一实例 ID,Value 为 Lease 对象。Eureka 采用 Lease(租约)这个词来表示对服务注册信息的抽象,Lease 对象保存了服务实例信息以及一些实例服务注册相关的时间,如注册时间 registrationTimestamp、最新的续约时间 lastUpdateTimestamp 等。如果用图形化的表达方式来展示这种数据结构,可以参考下图:
而对于 InstanceRegistry 本身,它也继承了 Eureka 中两个非常重要的接口,即LeaseManager 接口和 LookupService 接口。其中 LeaseManager 接口定义如下:
public interface LeaseManager<T> {
void register(T r, int leaseDuration, boolean isReplication);
boolean cancel(String appName, String id, boolean isReplication);
boolean renew(String appName, String id, boolean isReplication);
void evict();
}
显然 LeaseManager 做的事情就是 Eureka 注册中心模型中的服务注册、服务续约、服务取消和服务剔除等核心操作,关注于对服务注册过程的管理。而 LookupService 接口定义如下,关注于对应用程序与服务实例的管理:
public interface LookupService<T> {
Application getApplication(String appName);
Applications getApplications();
List<InstanceInfo> getInstancesById(String id);
InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);
}
在内部实现上,实际上对于注册中心服务器而言,服务注册、续约、取消和剔除等不同操作所执行的工作流程基本一致,即都是对服务存储的操作,并把这一操作同步到其他 Eureka 节点。我们这里选择用于服务注册操作的 register 方法进行展开,register 方法非常长,我们对源码进行裁剪,得出如下所示的核销处理流程:
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
//从已存储的 registry 获取一个服务定义
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
//初始化一个 Map<String, Lease<InstanceInfo>> ,并放入 registry 中
}
//根据当前注册的 ID 找到对应的 Lease
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
if (existingLease != null && (existingLease.getHolder() != null)) {
//如果 Lease 能找到,根据当前节点的最新更新时间和注册节点的最新更新时间比较
//如果前者的时间晚于后者的时间,那么注册实例就以已存在的实例为准
} else {
//如果找不到,代表是一个新注册,则更新其每分钟期望的续约数量及其阈值
}
//创建一个新 Lease 并放入 Map 中
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
gMap.put(registrant.getId(), lease);
//处理服务的 InstanceStatus
registrant.setActionType(ActionType.ADDED);
//更新服务最新更新时间
registrant.setLastUpdatedTimestamp();
//刷选缓存
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
}
}
Eureka 服务缓存源码解析
Eureka 服务器端组件的另一个核心功能是提供服务列表。为了提高性能,Eureka 服务器会缓存一份所有已注册的服务列表,并通过一定的定时机制对缓存数据进行更新。
我们知道为了获取注册到 Eureka 服务器上具体某一个服务实例的详细信息,可以访问如下地址:
http://<eureka-server-ip>:8761/eureka/apps/<APPID>
该地址代表的就是一个普通的 HTTP GET 请求。Eureka 中所有对服务器端的访问都是通过RESTful 风格的资源(Resource) 进行获取,ApplicationResource 类(位于com.netflix.eureka.resources 包中)提供了根据应用获取注册信息的入口。我们来看该类的 getApplication 方法,核心代码如下所示:
Key cacheKey = new Key(
Key.EntityType.Application,
appName,
keyType,
CurrentRequestVersion.get(),
EurekaAccept.fromString(eurekaAccept)
);
String payLoad = responseCache.get(cacheKey);
if (payLoad != null) {
logger.debug("Found: {}", appName);
return Response.ok(payLoad).build();
} else {
logger.debug("Not Found: {}", appName);
return Response.status(Status.NOT_FOUND).build();
}
可以看到这里是构建了一个 cacheKey,并直接调用了 responseCache.get(cacheKey) 方法来返回一个字符串并构建响应。其中最核心的就是这里的 get 方法:
public interface ResponseCache {
void invalidate(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress);
AtomicLong getVersionDelta();
AtomicLong getVersionDeltaWithRegions();
String get(Key key);
byte[] getGZIP(Key key);
}
从类层关系上看,ResponseCache 只有一个实现类 ResponseCacheImpl,我们来看它的 get 方法,发现该方法使用了如下处理策略:
Value getValue(final Key key, boolean useReadOnlyCache) {
Value payload = null;
try {
if (useReadOnlyCache) {
final Value currentPayload = readOnlyCacheMap.get(key);
if (currentPayload != null) {
payload = currentPayload;
} else {
payload = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, payload);
}
} else {
payload = readWriteCacheMap.get(key);
}
} catch (Throwable t) {
logger.error("Cannot get value for key : {}", key, t);
}
return payload;
}
可以看到上述代码中有两个缓存,一个是 readOnlyCacheMap,一个是 readWriteCacheMap。其中 readOnlyCacheMap 就是一个 JDK 中的 ConcurrentMap,而 readWriteCacheMap 使用的则是 Google Guava Cache 库中的 LoadingCache 类型。在创建 LoadingCache过程中,缓存数据的来源是调用 generatePayload 方法来生成。而在这个 generatePayload 方法中,就会调用前面介绍的 AbstractInstanceRegistry 中的 getApplications 方法获取应用信息并放到缓存中。这样我们就实现了把注册信息与缓存信息进行关联。
这里有一个设计和实现上的技巧。把缓存设计为一个只读的 readOnlyCacheMap 以及一个可读写的 readWriteCacheMap,可以更好地分离职责。但因为两个缓存中保存的实际上是同一份数据,所以,我们在不断更新 readWriteCacheMap 时,也需要确保 readOnlyCacheMap 中的数据得到同步。为此 ResponseCacheImpl 提供了一个定时任务 CacheUpdateTask,如下所示:
private TimerTask getCacheUpdateTask() {
return new TimerTask() {
@Override
public void run() {
for (Key key : readOnlyCacheMap.keySet()) {
try {
CurrentRequestVersion.set(key.getVersion());
Value cacheValue = readWriteCacheMap.get(key);
Value currentCacheValue = readOnlyCacheMap.get(key);
if (cacheValue != currentCacheValue) {
readOnlyCacheMap.put(key, cacheValue);
}
} catch (Throwable th) {
}
}
}
};
}
显然,这个定时任务主要是从 readWriteCacheMap 更新数据到 readOnlyCacheMap。
Eureka 高可用源码解析
我们已经在前面的内容中了解到 Eureka 的高可用部署方式被称为 Peer Awareness 模式。对应的,我们在 InstanceRegistry 的类层结构中也已经看到了它的一个扩展接口 PeerAwareInstanceRegistry 以及该接口的实现类 PeerAwareInstanceRegistryImpl。
我们还是围绕服务注册这个场景展开讨论,在 PeerAwareInstanceRegistryImpl 中同样存在一个 register 方法,如下所示:
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
super.register(info, leaseDuration, isReplication);
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
我们在这里看到了一个非常重要的replicateToPeers 方法,该方法作就是用来实现服务器节点之间的状态同步。replicateToPeers 方法的核心代码如下所示:
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
//如何该 URL 代表主机自身,则不用进行注册
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl(干货分享微服务spring-cloud(7.配置中心spring-cloud-config)
向 Eureka 服务器注册 Spring Cloud 微服务