1. 程式人生 > 實用技巧 >Eureka和Ribbon,中心架構圖原理

Eureka和Ribbon,中心架構圖原理

一、Eureka 註冊中心架構原理

Eureka 架構圖

Register(服務註冊):把自己的 IP 和埠註冊給 Eureka。

Renew(服務續約):傳送心跳包,每 30 秒傳送一次。告訴 Eureka 自己還活著。

Cancel(服務下線):當 provider 關閉時會向 Eureka 傳送訊息,把自己從服務列表中刪除。防止 consumer 呼叫到不存在的服務。

Get Registry(獲取服務註冊列表):獲取其他服務列表。

Replicate(叢集中資料同步):eureka 叢集中的資料複製與同步。

Make Remote Call(遠端呼叫):完成服務的遠端呼叫。

二、基於分散式 CAP 定理,分析註冊中心兩大主流框架:Eureka與 Zookeeper 的區別

1 什麼是 CAP 原則

CAP 原則又稱 CAP 定理,指的是在一個分散式系統中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分割槽容錯性),三者不可兼得。

CAP 由 Eric Brewer 在 2000 年 PODC 會議上提出。該猜想在提出兩年後被證明成立,成為我們熟知的 CAP 定理。

分散式系統 CAP 定理

資料一致性 (Consistency):也叫做資料原子性

系統在執行某項操作後仍然處於一致的狀態。在分散式系統 中,更新操作執行成功後所有的使用者都應該讀到最新的值,這樣的系統被認為是具有強一致性的。等同於所有節點訪問同一份最新的資料副本。

服務可用性 (Availablity):

每一個操作總是能夠在一定的時間內返回結果,這裡需 要注意的是"一定時間內"和"返回結果"。一定時間內指的是,在可以容忍的範圍內返回結果,結果可以是成功或者是失敗。

分割槽容錯性 (Partition-torlerance):

在網路分割槽的情況下,被分隔的節點仍能正常對外提供服務(分散式叢集,資料被分佈儲存在不同的伺服器上,無論什麼情況,伺服器都能正常被訪問)

2 Zookeeper 與 Eureka 的區別

三、Eureka 優雅停服

1 在什麼條件下,Eureka 會啟動自我保護?

什麼是自我保護模式

1)自我保護的條件

一般情況下,微服務在 Eureka 上註冊後,會每 30 秒傳送心跳包,Eureka 通過心跳來判斷服務時候健康,同時會定期刪除超過 90 秒沒有傳送心跳服務。

2)有兩種情況會導致 Eureka Server 收不到微服務的心跳

a.是微服務自身的原因

b.是微服務與 Eureka 之間的網路故障

通常(微服務的自身的故障關閉)只會導致個別服務出現故障,一般不會出現大面積故障,而(網路故障)通常會導致 Eureka Server 在短時間內無法收到大批心跳。

考慮到這個區別,Eureka 設定了一個閥值,當判斷掛掉的服務的數量超過閥值時, Eureka Server 認為很大程度上出現了網路故障,將不再刪除心跳過期的服務。

3)那麼這個閥值是多少呢?

15 分鐘之內是否低於 85%;

Eureka Server 在執行期間,會統計心跳失敗的比例在 15 分鐘內是否低於 85% 這種演算法叫做 Eureka Server 的自我保護模式。

2.為什麼要自我保護

1)因為同時保留"好資料"與"壞資料"總比丟掉任何資料要更好,當網路故障恢復後,這個 Eureka 節點會退出"自我保護模式"。

2)Eureka 還有客戶端快取功能(也就是微服務的快取功能)。即便 Eureka 叢集中所有節點都宕機失效,微服務的 Provider 和 Consumer 都能正常通訊。

3)微服務的負載均衡策略會自動剔除死亡的微服務節點。

3 如何關閉自我保護

修改 Eureka Server 配置檔案

#關閉自我保護:true 為開啟自我保護,false 為關閉自我保護
eureka.server.enableSelfPreservation=false
#清理間隔(單位:毫秒,預設是 60*1000)
eureka.server.eviction.interval-timer-in-ms=60000

4.如何優雅停服

不需要再 Eureka Server 中配置關閉自我保護

需要再服務中新增 actuator.jar 包

<?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 https://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.5.13.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.sxt</groupId>
    <artifactId>springcloud-eureka-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springcloud-eureka-provider</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-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</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>Dalston.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.application.name=eureka-provider
server.port=9090

eureka.client.serviceUrl.defaultZone=http://eureka1:8761/eureka/,http://eureka2:8761/eureka/

#啟用 shutdown
endpoints.shutdown.enabled=true
#禁用密碼驗證
endpoints.shutdown.sensitive=false

傳送一個關閉服務的 URL 請求

public class HttpClientUtil {

	public static String doGet(String url, Map<String, String> param) {

		// 建立Httpclient物件
		CloseableHttpClient httpclient = HttpClients.createDefault();

		String resultString = "";
		CloseableHttpResponse response = null;
		try {
			// 建立uri
			URIBuilder builder = new URIBuilder(url);
			if (param != null) {
				for (String key : param.keySet()) {
					builder.addParameter(key, param.get(key));
				}
			}
			URI uri = builder.build();

			// 建立http GET請求
			HttpGet httpGet = new HttpGet(uri);

			// 執行請求
			response = httpclient.execute(httpGet);
			// 判斷返回狀態是否為200
			if (response.getStatusLine().getStatusCode() == 200) {
				resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if (response != null) {
					response.close();
				}
				httpclient.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return resultString;
	}

	public static String doGet(String url) {
		return doGet(url, null);
	}

	public static String doPost(String url, Map<String, String> param) {
		// 建立Httpclient物件
		CloseableHttpClient httpClient = HttpClients.createDefault();
		CloseableHttpResponse response = null;
		String resultString = "";
		try {
			// 建立Http Post請求
			HttpPost httpPost = new HttpPost(url);
			// 建立引數列表
			if (param != null) {
				List<NameValuePair> paramList = new ArrayList<>();
				for (String key : param.keySet()) {
					paramList.add(new BasicNameValuePair(key, param.get(key)));
				}
				// 模擬表單
				UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList,"utf-8");
				httpPost.setEntity(entity);
			}
			// 執行http請求
			response = httpClient.execute(httpPost);
			resultString = EntityUtils.toString(response.getEntity(), "utf-8");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				response.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		return resultString;
	}

	public static String doPost(String url) {
		return doPost(url, null);
	}
	
	public static String doPostJson(String url, String json) {
		// 建立Httpclient物件
		CloseableHttpClient httpClient = HttpClients.createDefault();
		CloseableHttpResponse response = null;
		String resultString = "";
		try {
			// 建立Http Post請求
			HttpPost httpPost = new HttpPost(url);
			// 建立請求內容
			StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
			httpPost.setEntity(entity);
			// 執行http請求
			response = httpClient.execute(httpPost);
			resultString = EntityUtils.toString(response.getEntity(), "utf-8");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				response.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		return resultString;
	}
	
	public static void main(String[] args) {
		String url ="http://localhost:9090/shutdown";
		//該url必須要使用dopost方式來發送
HttpClientUtil.doPost(url);
	}
}

四、如何加強 Eureka 註冊中心的安全認證

1 在 Eureka Server 中新增 security 包

 <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
 </dependency>

2.修改 Eureka Server 配置檔案

spring.application.name=eureka-server
server.port=8761

#設定 eureka 例項名稱,與配置檔案的變數為主
eureka.instance.hostname=eureka2
#設定服務註冊中心地址,指向另一個註冊中心
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/

#開啟 http basic 的安全認證
security.basic.enabled=true 
security.user.name=user
security.user.password=123456

3 修改訪問叢集節點的 url

eureka.client.serviceUrl.defaultZone=http://user:123456@eureka2:8761/eureka/

4 修改微服務的配置檔案新增訪問註冊中心的使用者名稱與密碼

spring.application.name=eureka-provider
server.port=9090

eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/

#啟用 shutdown
endpoints.shutdown.enabled=true
#禁用密碼驗證
endpoints.shutdown.sensitive=false

如果微服務中未新增訪問註冊中心的使用者名稱和密碼,將會無法訪問註冊中心進行註冊。會報以下錯誤。

五、負載均衡 Ribbon

1、Ribbon 在微服務中的作用

什麼是 Ribbon

1)Ribbon 是一個基於 Http 和 TCP 的客服端負載均衡工具,它是基於 Netflix Ribbon 實現的。

2)它不像 spring cloud 服務註冊中心、配置中心、API 閘道器那樣獨立部署,但是它幾乎存在於每個spring cloud 微服務中。包括 feign 提供的宣告式服務呼叫也是基於該 Ribbon實現的。

3)ribbon 預設提供很多種負載均衡演算法,例如 輪詢、隨機 等等。甚至包含自定義的負載均衡演算法。

Ribbon 解決了什麼問題

他解決並提供了微服務的負載均衡的問題。

2、 集中式與程序內負載均衡的區別

負載均衡解決方案的分類

目前業界主流的負載均衡方案可分成兩類:

第一類:集中式負載均衡, 即在 consumer 和 provider 之間使用獨立的負載均衡設施(可以是硬體,如 F5, 也可以是軟體,如 nginx), 由該設施負責把 訪問請求 通過某種策略轉發至 provider;

第二類:程序內負載均衡,將負載均衡邏輯整合到 consumer,consumer 從服務註冊中心獲知有哪些地址可用,然後自己再從這些地址中選擇出一個合適的 provider。

Ribbon 就屬於後者,它只是一個類庫,集成於 consumer 程序,consumer 通過它來獲取 到 provider 的地址。

兩種負載均衡方式結構圖

3、Ribbon 的入門案例

Ribbon 中對於叢集的服務採用的負載均衡的策略預設的是輪詢

Consumer

@Service
public class UserService {
    @Autowired
    private LoadBalancerClient balancerClient;//ribbon 負載均衡器

    public List<User> getUser(){
        //選擇呼叫的服務的名稱
        //ServiceInstance 封裝了服務的基本資訊,如 IP,埠
        ServiceInstance si = this.balancerClient.choose("eureka-provider");
        //拼接訪問服務的URL
        StringBuffer sb = new StringBuffer();
        //http://localhost:9090/user
        sb.append("http://").append(si.getHost()).append(":").append(si.getPort()).append("/user");
        System.out.println(sb.toString());
        //springMVC RestTemplate
        RestTemplate rt = new RestTemplate();
        ParameterizedTypeReference<List<User>> type = new ParameterizedTypeReference<List<User>>() {};
        //ResponseEntity封裝了返回值的資訊
        ResponseEntity<List<User>> response = rt.exchange(sb.toString(), HttpMethod.GET,null,type);
        List<User> list = response.getBody();
        return list;
    }
}

Consumer 的配置檔案

spring.application.name=eureka-consumer
server.port=9091

eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/

Provider 的叢集部署

將 provider 打包。部署到 linux 環境中

建立啟動指令碼 server.sh

#!/bin/bash
cd `dirname $0`
 
CUR_SHELL_DIR=`pwd`
CUR_SHELL_NAME=`basename ${BASH_SOURCE}`
 
JAR_NAME="springcloud-eureka-server-ha-0.0.1-SNAPSHOT.jar"
JAR_PATH=$CUR_SHELL_DIR/$JAR_NAME
 
#JAVA_MEM_OPTS=" -server -Xms1024m -Xmx1024m -XX:PermSize=128m"
JAVA_MEM_OPTS=""

#如果是多環境配置需要在該選項中指定profile
#SPRING_PROFILES_ACTIV="-Dspring.profiles.active=配置檔案profile名稱"
#如果沒有多環境配置將 SPRING_PROFILES_ACTIV註釋掉,將SPRING_PROFILES_ACTIV=""釋放開
SPRING_PROFILES_ACTIV=""
LOG_DIR=$CUR_SHELL_DIR/logs
LOG_PATH=$LOG_DIR/${JAR_NAME%..log
 
echo_help()
{
    echo -e "syntax: sh $CUR_SHELL_NAME start|stop"
}
 
if [ -z $1 ];then
    echo_help
    exit 1
fi
 
if [ ! -d "$LOG_DIR" ];then
    mkdir "$LOG_DIR"
fi
 
if [ ! -f "$LOG_PATH" ];then
    touch "$LOG_DIR"
fi
 
if [ "$1" == "start" ];then
 
    # check server
    PIDS=`ps --no-heading -C java -f --width 1000 | grep $JAR_NAME | awk '{print $2}'`
    if [ -n "$PIDS" ]; then
        echo -e "ERROR: The $JAR_NAME already started and the PID is ${PIDS}."
        exit 1
    fi
 
    echo "Starting the $JAR_NAME..."
 
    # start
    nohup java $JAVA_MEM_OPTS -jar $SPRING_PROFILES_ACTIV $JAR_PATH >> $LOG_PATH 2>&1 &
 
    COUNT=0
    while [ $COUNT -lt 1 ]; do
        sleep 1
        COUNT=`ps  --no-heading -C java -f --width 1000 | grep "$JAR_NAME" | awk '{print $2}' | wc -l`
        if [ $COUNT -gt 0 ]; then
            break
        fi
    done
    PIDS=`ps  --no-heading -C java -f --width 1000 | grep "$JAR_NAME" | awk '{print $2}'`
    echo "${JAR_NAME} Started and the PID is ${PIDS}."
    echo "You can check the log file in ${LOG_PATH} for details."
 
elif [ "$1" == "stop" ];then
 
    PIDS=`ps --no-heading -C java -f --width 1000 | grep $JAR_NAME | awk '{print $2}'`
    if [ -z "$PIDS" ]; then
        echo "ERROR:The $JAR_NAME does not started!"
        exit 1
    fi
 
    echo -e "Stopping the $JAR_NAME..."
 
    for PID in $PIDS; do
        kill $PID > /dev/null 2>&1
    done
 
    COUNT=0
    while [ $COUNT -lt 1 ]; do
        sleep 1
        COUNT=1
        for PID in $PIDS ; do
            PID_EXIST=`ps --no-heading -p $PID`
            if [ -n "$PID_EXIST" ]; then
                COUNT=0
                break
            fi
        done
    done
 
    echo -e "${JAR_NAME} Stopped and the PID is ${PIDS}."
else
    echo_help
    exit 1
fi

啟動Consumer

六、Ribbon 的常見負載均衡策略

1.輪詢策略(預設)

策略對應的類名:RoundRobinRule

實現原理:輪詢策略表示每次都順序取下一個 provider,比如一共有 5 個provider,第 1 次取第 1 個,第 2 次取第 2 個,第 3 次取第 3 個,以此類推

2.權重輪詢策略

策略對應的類名:WeightedResponseTimeRule

實現原理:

1)根據每個 provider 的響應時間分配一個權重,響應時間越長,權重越小,被選中的可能性越低。

2)原理:一開始為輪詢策略,並開啟一個計時器,每 30 秒收集一次每個 provider 的平均響應時間,當資訊足夠時,給每個 provider 附上一個權重,並按權重隨機選擇provider,高權越重的 provider會被高概率選中。

3.隨機策略

策略對應的類名:RandomRule

實現原理:從 provider 列表中隨機選擇一個 provider

4.最少併發數策略

策略對應的類名:BestAvailableRule

實現原理:選擇正在請求中的併發數最小的 provider,除非這個provider 在熔斷中。

5.在“選定的負載均衡策略”基礎上進行重試機制

策略對應的類名:RetryRule

實現原理:

1)“選定的負載均衡策略”這個策略是輪詢策略RoundRobinRule

2)該重試策略先設定一個閾值時 間段,如果在這個閾值時間段內當選擇 provider 不成功,則一直嘗 試採用“選定的負載均衡策略:輪詢策略”最後選擇一個可用的provider。

6.可用性敏感策略

策略對應的類名:AvailabilityFilteringRule

實現原理:過濾效能差的 provider,有 2 種:

第一種:過濾掉在 eureka 中處於一直連線失敗 provider

第二種:過濾掉高併發的 provider

7.區域敏感性策略

策略對應的類名:ZoneAvoidanceRule :

實現原理:

1)以一個區域為單位考察可用性,對於不可用的區域整個丟棄,從剩下區域中選可用的provider .

2)如果這個ip區域內有一個或多個例項不可達或響應變慢,都會降低該ip區域內其他ip被選中的權重。

七、Ribbon指定其他負載均衡策略

修改程式碼更換負載均衡策略

建立專案

在啟動類中新增建立負載均衡策略物件的方法

@EnableEurekaClient
@SpringBootApplication
public class SpringcloudEurekaConsumerApplication {
    
    @Bean
    public RandomRule createRule(){
        return new RandomRule();
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringcloudEurekaConsumerApplication.class, args);
    }

}

修改配置檔案更換負載均衡策略

#設定負載均衡策略 eureka-provider 為呼叫的服務的名稱
eureka-provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

八、Ribbon 的點對點直連

建立專案

去掉 Eureka 的座標新增 Ribbon 座標

 <!-- ribbon 座標 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>

修改配置檔案去掉與 Eureka 相關的配置,新增新配置項

spring.application.name=eureka-consumer-direct
server.port=9091

#禁用 eureka
ribbon.eureka.enabled=false
#指定具體的服務例項清單
eureka-provider.ribbon.listOfServers=eureka1:9090

修改啟動類去掉報錯程式碼

@SpringBootApplication
public class SpringcloudEurekaConsumerApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SpringcloudEurekaConsumerApplication.class, args);
    }
}