11.1 注冊中心Eureka
1.1.1 注冊中心
注意它的特點,結構類似於MessageQueue消息隊列,服務(提供者、消費者)先都注冊到注冊中心。它的特點在於,不會每次都去注冊中心獲取,而是有本地緩存,加快訪問性能。內部含有心跳機制,當注冊中心信息改變,自動快速獲取新的信息到本地。心跳機制還保證分布式環境下,某個服務失敗后,自動列表從注冊中心移除。注冊中心中保證所有可用的鏈接。
1.1.2 拓展:CAP定理
CAP原則又稱CAP定理,指的是在一個分布式系統中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區容錯性),三者不可得兼。它是分布式系統中最核心最重要的理論。
分布式系統的CAP理論:理論首先把分布式系統中的三個特性進行了如下歸納:
l 一致性(C):在分布式系統中的所有數據備份,在同一時刻是否同樣的值。(等同於所有節點訪問同一份最新的數據副本)
l 可用性(A):在集群中一部分節點故障后,集群整體是否還能響應客戶端的讀寫請求。(對數據更新具備高可用性)
l 分區容錯性(P):以實際效果而言,分區相當於對通信的時限要求。系統如果不能在時限內達成數據一致性,就意味着發生了分區的情況,必須就當前操作在C和A之間做出選擇。
CAP理論就是說在分布式系統中,最多只能實現上面的兩點。而由於當前的網絡硬件肯定會出現延遲丟包等問題,所以分區容忍性是我們必須需要實現的。所以我們只能在一致性和可用性之間進行權衡,要么選擇CP要么選擇AP,沒有分布式系統能同時保證這三點。
1.1.3 ZooKeeper和Eureka對比
Eureka本身是Netflix開源的一款提供服務注冊和發現的產品,並且提供了相應的Java封裝。在它的實現中,節點之間相互平等,部分注冊中心的節點掛掉也不會對集群造成影響,即使集群只剩一個節點存活,也可以正常提供發現服務。哪怕是所有的服務注冊節點都掛了,Eureka Clients(客戶端)上也會緩存服務調用的信息。這就保證了我們微服務之間的互相調用足夠健壯。
Zookeeper主要為大型分布式計算提供開源的分布式配置服務、同步服務和命名注冊。曾經是Hadoop項目中的一個子項目,用來控制集群中的數據,目前已升級為獨立的頂級項目。很多場景下也用它作為Service發現服務解決方案。
對比
根據著名的CAP定理(C-數據一致性;A-服務可用性;P-服務對網絡分區故障的容錯性CAP這三個特性在任何分布式系統中不能同時滿足,最多同時滿足兩個CP或者AP)。
ZooKeeper
Zookeeper是基於CP來設計的,即任何時刻對Zookeeper的訪問請求能得到一致的數據結果,同時系統對網絡分割具備容錯性,但是它不能保證每次服務請求的可用性。從實際情況來分析,在使用Zookeeper獲取服務列表時,如果zookeeper正在選主,或者Zookeeper集群中半數以上機器不可用,那么將無法獲得數據。所以說,Zookeeper不能保證服務可用性。
誠然,在大多數分布式環境中,尤其是涉及到數據存儲的場景,數據一致性應該是首先被保證的,這也是zookeeper設計成CP的原因。但是對於服務發現場景來說,情況就不太一樣了:針對同一個服務,即使注冊中心的不同節點保存的服務提供者信息不盡相同,也並不會造成災難性的后果。因為對於服務消費者來說,能消費才是最重要的——拿到可能不正確的服務實例信息后嘗試消費一下,也好過因為無法獲取實例信息而不去消費。(嘗試一下可以快速失敗,之后可以更新配置並重試)所以,對於服務發現而言,可用性比數據一致性更加重要——AP勝過CP。
Eureka
而Spring Cloud Netflix在設計Eureka時遵守的就是AP原則。Eureka Server也可以運行多個實例來構建集群,解決單點問題,但不同於ZooKeeper的選舉leader的過程,Eureka Server采用的是Peer to Peer對等通信。這是一種去中心化的架構,無master/slave區分,每一個Peer都是對等的。在這種架構中,節點通過彼此互相注冊來提高可用性,每個節點需要添加一個或多個有效的serviceUrl指向其他節點。每個節點都可被視為其他節點的副本。
如果某台Eureka Server宕機,Eureka Client的請求會自動切換到新的Eureka Server節點,當宕機的服務器重新恢復后,Eureka會再次將其納入到服務器集群管理之中。當節點開始接受客戶端請求時,所有的操作都會進行replicateToPeer(節點間復制)操作,將請求復制到其他Eureka Server當前所知的所有節點中。
一個新的Eureka Server節點啟動后,會首先嘗試從鄰近節點獲取所有實例注冊表信息,完成初始化。Eureka Server通過getEurekaServiceUrls()方法獲取所有的節點,並且會通過心跳續約的方式定期更新。默認配置下,如果Eureka Server在一定時間內沒有接收到某個服務實例的心跳,Eureka Server將會注銷該實例(默認為90秒,通過eureka.instance.lease-expiration-duration-in-seconds配置)。當Eureka Server節點在短時間內丟失過多的心跳時(比如發生了網絡分區故障),那么這個節點就會進入自我保護模式。
總結
ZooKeeper基於CP,不保證高可用,如果zookeeper正在選主,或者Zookeeper集群中半數以上機器不可用,那么將無法獲得數據。Eureka基於AP,能保證高可用,即使所有機器都掛了,也能拿到本地緩存的數據。作為注冊中心,其實配置是不經常變動的,只有發版(發布新的版本)和機器出故障時會變。對於不經常變動的配置來說,CP是不合適的,而AP在遇到問題時可以用犧牲一致性來保證可用性,既返回舊數據,緩存數據。
所以理論上Eureka是更適合作注冊中心。而現實環境中大部分項目可能會使用ZooKeeper,那是因為集群不夠大,並且基本不會遇到用做注冊中心的機器一半以上都掛了的情況。所以實際上也沒什么大問題。
1.1.4 拓展:分布式對關系型數據庫的沖擊
對於web網站來說,關系數據庫的很多主要特性卻往往無用武之地
l 數據庫事務一致性需求
很多web實時系統並不要求嚴格的數據庫事務,對讀一致性的要求很低,有些場合對寫一致性要求並不高。允許實現最終一致性。
l 數據庫的寫實時性和讀實時性需求
對關系數據庫來說,插入一條數據之后立刻查詢,是肯定可以讀出來這條數據的,但是對於很多web應用來說,並不要求這么高的實時性,比方說發一條消息之后,過幾秒乃至十幾秒之后,我的訂閱者才看到這條動態是完全可以接受的。如:MQ消息隊列機制,意義,可以解決瞬時的高並發,消峰填谷作用。
l 對復雜的SQL查詢,特別是多表關聯查詢的需求
任何大數據量的web系統,都非常忌諱多個大表的關聯查詢,以及復雜的數據分析類型的報表查詢,特別是SNS類型的網站,從需求以及產品設計角度,就避免了這種情況的產生。往往更多的只是單表的主鍵查詢,以及單表的簡單條件分頁查詢,SQL的功能被極大的弱化了。
SNS:全稱Social Networking Services,專指社交網絡服務,包括了社交軟件和社交網站。例如:臉譜facebook、騰訊QQ、微信等。
1.1.5 自我保護模式
什么是自我保護模式?默認配置下,如果Eureka Server每分鍾收到心跳續約的數量低於一個閾值(instance的數量(60/每個instance的心跳間隔秒數)自我保護系數),並且持續15分鍾,就會觸發自我保護。在自我保護模式中,Eureka Server會保護服務注冊表中的信息,不再注銷任何服務實例。當它收到的心跳數重新恢復到閾值以上時,該Eureka Server節點就會自動退出自我保護模式。它的設計哲學前面提到過,那就是寧可保留錯誤的服務注冊信息,也不盲目注銷任何可能健康的服務實例。該模式可以通過eureka.server.enable-self-preservation = false來禁用,同時eureka.instance.lease-renewal-interval-in-seconds可以用來更改心跳間隔。
1.1.6 調用關系圖
常見的服務發現產品:Eureka/Consul/ZooKeeper,Eureka輕量級。
1.2 Eureka服務端
1.2.1 創建Maven工程
父工程spring-cloud
spring-cloud工程的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>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wood</groupId>
<artifactId>spring-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud</name>
<description>Demo project for Spring Boot</description>
<!--Maven項目可以繼承,三個子工程-->
<modules>
<module>eureka-server</module>
<module>provider-a</module>
<module>provider-b</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<!--公共依賴包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
子工程eureka-server
子工程服務提供者A provider-a
子工程服務提供者B provider-b
1.2.2 eureka-server服務的 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>
<!--父工程不在是spring-boot,而是spring-cloud-->
<parent>
<groupId>com.wood</groupId>
<artifactId>spring-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wood</groupId>
<artifactId>eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-server</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
1.2.3 application.properties
spring.application.name=eureka-server
server.port=6001
#eureka
# 注冊中心配置
eureka.instance.preferIpAddress=true
eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port}
eureka.instance.lease-renewal-interval-in-seconds=5
eureka.instance.lease-expiration-duration-in-seconds=20
eureka.client.serviceUrl.defaultZone=http://localhost:6001/eureka
eureka.client.registry-fetch-interval-seconds=10
#不要向注冊中心注冊自己
eureka.client.register-with-eureka=false
#禁止檢索服務
eureka.client.fetch-registry=false
#打印日志(開發模式)
logging.level.root=DEBUG
1.2.4 EurekaServerApplication.java
package com.wood.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
1.2.5 監控地址
地址:http://localhost:6001
啟動后形成一個控制台
1.3 服務提供者
1.3.1 創建Maven工程 provider-a
1.3.2 pom.xml
注意提供者是作為Eureka的客戶端,這樣啟動后自動注冊到EurekaServer中。
<?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.wood</groupId>
<artifactId>spring-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wood</groupId>
<artifactId>provider-a</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>provider-a</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
1.3.3 application.properties
注意多個提供端的差異在於端口不同,將來同一個業務服務多實例如何標識呢?就是通過端口來標識。
server.port=8081
spring.application.name=provider-user
eureka.client.serviceUrl.defaultZone=http://localhost:6001/eureka
1.3.4 HelloController.java
package com.wood.providera.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return "服務提供者A:"+name;
}
}
1.3.5 ProviderRunApp.java
package com.wood.providera;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class ProviderAApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderAApplication.class, args);
}
}
1.4 服務提供者2
1.4.1 創建Maven工程
創建項目,復制文件進行修改。主要
1.4.2 application.properties
server.port=8082
spring.application.name=provider-user
eureka.client.serviceUrl.defaultZone=http://localhost:6001/eureka
1.4.3 HelloController.java
修改打印的頁面展現的值,方便觀察是哪個提供者響應的。
package com.wood.providerb.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return "服務提供者B:"+name;
}
}
1.4.4 Provider2RunApp.java
修改類名
package com.wood.providerb;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class ProviderBApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderBApplication.class, args);
}
}
附上demo
spring-cloud.zip
https://pan.baidu.com/s/1aEg6SBim9xlPhSToBFOKrw