服務(wù)注冊與發(fā)現(xiàn)——Netflix Eureka

(git上的源碼:https://gitee.com/rain7564/spring_microservices_study/tree/master/second-discovery-euraka)

何為服務(wù)發(fā)現(xiàn)

在許多分布式系統(tǒng)架構(gòu)中,都需要去獲取機(jī)器的物理地址(微服務(wù)實(shí)例部署的服務(wù)器地址及端口)。這一認(rèn)知在分布式系統(tǒng)架構(gòu)開始的時候就已經(jīng)存在,而等到分布式計(jì)算出現(xiàn)的時候,被正式稱為服務(wù)發(fā)現(xiàn)(service discovery)。

服務(wù)發(fā)現(xiàn)可以做一些簡單的事情,比如維護(hù)一個帶有所有遠(yuǎn)程服務(wù)地址的屬性文件,或一個UDDI(Universal Description, Discovery and Integration)存儲庫。

服務(wù)發(fā)現(xiàn)對微服務(wù)、分布式應(yīng)用/基于云的應(yīng)用至關(guān)重要。有兩個主要原因:

  1. 它為應(yīng)用程序開發(fā)團(tuán)隊(duì)提供了快速擴(kuò)展的能力,以及縮減在一個環(huán)境中運(yùn)行的服務(wù)實(shí)例數(shù)量。
    通過服務(wù)發(fā)現(xiàn),服務(wù)消費(fèi)者(service consumers)可以從服務(wù)提供者(service provider)的物理地址中抽象出來。因?yàn)橄M(fèi)者并不知道提供者的實(shí)例的實(shí)際物理地址(多個實(shí)例就有多個地址),新的服務(wù)實(shí)例也可以添加到"可用服務(wù)池",失效的服務(wù)也會被移除,即消費(fèi)者根本沒辦法具體知道會消費(fèi)服務(wù)提供者哪個的實(shí)例,服務(wù)提供者實(shí)例的物理地址對消費(fèi)者來說是透明的。
    以往,龐大的單體應(yīng)用要擴(kuò)大業(yè)務(wù)處理能力,只能通過將應(yīng)用部署到更大更好的機(jī)器上(縱向擴(kuò)展),這種辦法取得的效果與成本的比值越來越低,而且也無法無限擴(kuò)展;而服務(wù)發(fā)現(xiàn)能讓開發(fā)團(tuán)隊(duì)從這一困境走出,因?yàn)榉?wù)發(fā)現(xiàn)使將服務(wù)部署到更多的廉價機(jī)器上(橫向擴(kuò)展)成為可能。
  2. 服務(wù)發(fā)現(xiàn)使整體應(yīng)用更有彈性。當(dāng)機(jī)器出現(xiàn)異常使正在運(yùn)行的服務(wù)實(shí)例處理能力變?nèi)?頻繁報錯)或不可用,服務(wù)發(fā)現(xiàn)會將該實(shí)例從可用服務(wù)列表中移除,外部或應(yīng)用內(nèi)的其他微服務(wù)不會再"消費(fèi)"這個服務(wù)實(shí)例。這樣,因意外導(dǎo)致部分服務(wù)實(shí)例不可用,服務(wù)發(fā)現(xiàn)會將對該服務(wù)的"消費(fèi)"路由到其他可用的服務(wù)實(shí)例,繞過了已衰掉的實(shí)例,該服務(wù)還是能正常被"消費(fèi)",從而將影響降到了最低。

服務(wù)發(fā)現(xiàn)的實(shí)現(xiàn)方案

我們已經(jīng)初步了解服務(wù)發(fā)現(xiàn)帶來的好處。那么有沒有好的方案來實(shí)現(xiàn)"服務(wù)發(fā)現(xiàn)"?例如DNS或負(fù)載均衡器(此處指服務(wù)端負(fù)載均衡,還有客戶端負(fù)載均衡。注意:后文分析DNS+負(fù)載均衡模式,都是指服務(wù)端負(fù)載均衡)。
開發(fā)過程中,經(jīng)常會有一種情況——從第三方獲取資源,在獲取的時候就必須定位這些資源所在的物理地址。若不是基于云架構(gòu)的應(yīng)用,大都采用DNS和負(fù)載均衡器實(shí)現(xiàn)。大體架構(gòu)如下圖所示:


image.png

應(yīng)用調(diào)用第三方服務(wù)過程中,通過DNS將域名解析得到一個商業(yè)負(fù)載均衡器的物理地址,然后將請求轉(zhuǎn)發(fā)到給負(fù)載均衡器,負(fù)載均衡器維護(hù)了一張路由表,然后根據(jù)這張路由表將請求路由給正確的服務(wù)。
為了獲得高可用,會有一個處于空閑狀態(tài)的次負(fù)載均衡器一直發(fā)送ping請求來確認(rèn)主負(fù)載均衡器是否正常運(yùn)行,若已經(jīng)衰掉,次負(fù)載均衡器會被激活然后接管主負(fù)載均衡器。
然而這種解決方案并不適合基于云的微服務(wù)應(yīng)用,原因如下:

  • 單點(diǎn)故障:負(fù)載均衡會成為整體架構(gòu)的一個單點(diǎn)故障。首先,如果負(fù)載均衡器衰掉,那么所有依賴負(fù)載均衡器的服務(wù)都會受到牽連。雖然主次負(fù)載均衡器能實(shí)現(xiàn)高可用,但當(dāng)請求劇增時,會成為整體架構(gòu)的性能瓶頸,單位時間內(nèi)能處理的請求數(shù)有限。
  • 限制水平擴(kuò)展能力:通過負(fù)載均衡器將服務(wù)集中到單個集群,限制了負(fù)載均衡架構(gòu)水平擴(kuò)展的能力。大多數(shù)商業(yè)負(fù)載平衡器使用冗余的熱交換模型,所以只有一臺服務(wù)器來處理負(fù)載,而故障轉(zhuǎn)移的次負(fù)載平衡器只有在停機(jī)的情況下才成為主負(fù)載均衡器。
  • 靜態(tài)托管:許多商業(yè)負(fù)載均衡器并不支持為服務(wù)快速注冊/注銷。他們將路由規(guī)則存儲在一個集中式數(shù)據(jù)庫中,而且添加新路由規(guī)則通常需要通過供應(yīng)商提供的專有API。
  • 復(fù)雜:因?yàn)樨?fù)載均衡器只是服務(wù)的一個代理,服務(wù)消費(fèi)者的請求必須通過它才能定位到服務(wù)提供者的物理地址。整個應(yīng)用架構(gòu)因?yàn)槎嗔素?fù)載均衡這一轉(zhuǎn)換層變得更復(fù)雜,因?yàn)樗新酚梢?guī)則都是通過人工定義和發(fā)布的。

以上四個原因并不是想說明服務(wù)端負(fù)載均衡架構(gòu)不好,而是為了說明不適合基于云的微服務(wù)應(yīng)用。因?yàn)榉?wù)端負(fù)載均衡能很好的作用于應(yīng)用的大小、規(guī)模能通過集中式網(wǎng)絡(luò)架構(gòu)支撐的情況。
然而,基于云的應(yīng)用中,需要處理大量的事務(wù)和數(shù)據(jù)信息,集中式網(wǎng)絡(luò)架構(gòu)并不能很好支撐這種情況,因?yàn)檫@種架構(gòu)擴(kuò)展能力弱,擴(kuò)展成本效益低。下面開始介紹如何實(shí)現(xiàn)一個健壯的擴(kuò)展性強(qiáng)的適用于云微服務(wù)應(yīng)用的服務(wù)發(fā)現(xiàn)。

適用于云的服務(wù)發(fā)現(xiàn)

基于云的微服務(wù)環(huán)境的服務(wù)發(fā)現(xiàn)解決方案,必須具備以下特征:

  • 高可用:服務(wù)發(fā)現(xiàn)需要具備支持"熱"集群環(huán)境的能力。即服務(wù)的信息可以在服務(wù)發(fā)現(xiàn)集群的多個節(jié)點(diǎn)中共享。當(dāng)集群中的某個節(jié)點(diǎn)不可用,其它節(jié)點(diǎn)可以完全接管。
  • 所有節(jié)點(diǎn)對等:集群中的所有節(jié)點(diǎn)共享所有注冊到服務(wù)發(fā)現(xiàn)的服務(wù)實(shí)例的狀態(tài)信息。
  • 負(fù)載均衡:服務(wù)發(fā)現(xiàn)需要動態(tài)地、均衡地將請求分配到所有注冊到服務(wù)發(fā)現(xiàn)的服務(wù)實(shí)例。當(dāng)然,服務(wù)發(fā)現(xiàn)可以根據(jù)不同的分配策略進(jìn)行分配,比如輪詢、隨機(jī)等。服務(wù)發(fā)現(xiàn)取代了類似上文提及的靜態(tài)的、需要人工配置的負(fù)載均衡器。
  • 彈性:服務(wù)發(fā)現(xiàn)的客戶端(服務(wù)發(fā)現(xiàn)分服務(wù)端和客戶端,后文會詳細(xì)說明)應(yīng)該在本地緩存服務(wù)實(shí)例信息。這樣做有很多好處,比如:不用每次遠(yuǎn)程請求都去服務(wù)發(fā)現(xiàn)的服務(wù)端獲取目標(biāo)服務(wù)的信息、當(dāng)服務(wù)發(fā)現(xiàn)服務(wù)端衰掉了,短時間內(nèi)還可以依賴本地緩存繼續(xù)正常運(yùn)行等。
  • 容錯:服務(wù)發(fā)現(xiàn)需要及時發(fā)現(xiàn)那些不可用的服務(wù)實(shí)例并將其從可用服務(wù)列表中移除。即可以自動發(fā)現(xiàn)并處理而不用認(rèn)為干預(yù)。

至此,應(yīng)該對適用于云的服務(wù)發(fā)現(xiàn)有一定的了解,接下來將會對如下幾個方面進(jìn)行分析:

  • 基于云的服務(wù)發(fā)現(xiàn)代理是如何運(yùn)作
  • 當(dāng)服務(wù)發(fā)現(xiàn)代理不可用時,客戶端負(fù)載均衡是如何能繼續(xù)運(yùn)作
  • 如何使用Spring Cloud和Netflix Eureka實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)代理
服務(wù)發(fā)現(xiàn)架構(gòu)

在討論服務(wù)發(fā)現(xiàn)架構(gòu)之前,需要了解四個概念,這四個概念在所有服務(wù)發(fā)現(xiàn)架構(gòu)都會涉及到的,如下:

  • 服務(wù)注冊:服務(wù)是怎樣把自己注冊到服務(wù)發(fā)現(xiàn)代理?
  • 客戶端獲取服務(wù)地址:客戶端是怎樣從服務(wù)發(fā)現(xiàn)代理獲取其他同樣注冊到服務(wù)發(fā)現(xiàn)代理的服務(wù)的信息?
  • 信息共享:服務(wù)發(fā)現(xiàn)代理集群之間是如何共享服務(wù)注冊信息?
  • 健康監(jiān)控:服務(wù)客戶端是怎樣通知服務(wù)發(fā)現(xiàn)代理自己的狀態(tài)信息?

下圖中序號1-4分別對應(yīng)上面所說的四個概念。

image.png

當(dāng)服務(wù)啟動后,它們會將自己注冊到一個或多個服務(wù)發(fā)現(xiàn)代理,即將物理地址、端口等信息發(fā)送。雖然某個服務(wù)的所有實(shí)例有不同的IP和端口號,但它們都會注冊在同一個服務(wù)ID(上文已經(jīng)提到的邏輯名稱,application name)下,這個服務(wù)ID只是被服務(wù)發(fā)現(xiàn)代理用來給不同服務(wù)分組而已,即相同服務(wù)的不同實(shí)例,它們的服務(wù)ID是相同的。

一般情況下,一個服務(wù)只需要注冊到一個服務(wù)發(fā)現(xiàn)代理。因?yàn)榇蠖喾?wù)發(fā)現(xiàn)實(shí)現(xiàn)都采用數(shù)據(jù)共享模型,即一個服務(wù)發(fā)現(xiàn)集群的所有節(jié)點(diǎn)會共享它們維護(hù)的數(shù)據(jù)。

當(dāng)一個服務(wù)實(shí)例注冊到服務(wù)發(fā)現(xiàn)代理,就意味著它隨時可以被其他應(yīng)用或服務(wù)調(diào)用,注冊到服務(wù)發(fā)現(xiàn)的所有服務(wù)之間的調(diào)用是相互的??蛻舳朔?wù)在"發(fā)現(xiàn)"服務(wù)上有多種模式。比如:客戶端服務(wù)的每次遠(yuǎn)程調(diào)用,都依靠服務(wù)發(fā)現(xiàn)引擎去解析得到目標(biāo)服務(wù)的地址。這種模式(服務(wù)端服務(wù)發(fā)現(xiàn),如DNS+負(fù)載均衡器)是非常脆弱的,因?yàn)榭蛻舳朔?wù)的每一個遠(yuǎn)程調(diào)用完全依賴于服務(wù)發(fā)現(xiàn)引擎,所以需要有一個更好、更健壯的模式——客戶端負(fù)載均衡。下圖說明了何為客戶端負(fù)載均衡:


image.png

另外,當(dāng)服務(wù)調(diào)用失敗,客戶端會讓本地緩存失效,然后重新從服務(wù)發(fā)現(xiàn)代理獲取新的服務(wù)注冊信息。

使用Netflix Eureka實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)

接下來,我們會創(chuàng)建一個服務(wù)發(fā)現(xiàn)代理并向該代理注冊兩個服務(wù)。然后其中的一個服務(wù)使用從服務(wù)發(fā)現(xiàn)代理獲取的服務(wù)信息去調(diào)用另一個服務(wù)。Netflix Eureka的服務(wù)發(fā)現(xiàn)引擎可以實(shí)現(xiàn)服務(wù)發(fā)現(xiàn),而客戶端負(fù)載均衡的實(shí)現(xiàn)則使用Netflix的Ribbon庫。

當(dāng)然,Spring Cloud為實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)提供了多種解決方案。下文也會分析各自的優(yōu)劣勢。

在上一節(jié),我們已經(jīng)創(chuàng)建了一個微服務(wù)應(yīng)用,該應(yīng)用中只有l(wèi)icense一個微服務(wù)?,F(xiàn)在我們會在現(xiàn)有應(yīng)用的基礎(chǔ)上,再創(chuàng)建一個organization服務(wù)和一個服務(wù)發(fā)現(xiàn)代理,并將license服務(wù)和organization服務(wù)注冊到服務(wù)發(fā)現(xiàn)代理。

假設(shè)當(dāng)license服務(wù)被調(diào)用,它會去調(diào)用organization服務(wù),organization服務(wù)再根據(jù)organization ID獲取對應(yīng)的organization信息并返回。在從organization服務(wù)獲取數(shù)據(jù)前,必須先知道organization服務(wù)可用的實(shí)例信息,這些信息是organization服務(wù)啟動的時候就注冊到服務(wù)發(fā)現(xiàn)代理并由它維護(hù)的。而license服務(wù)在調(diào)用organization服務(wù)時,會從本地獲取organization服務(wù)實(shí)例的信息。下圖說明了這個過程:

image.png

在上一節(jié)創(chuàng)建的應(yīng)用的基礎(chǔ)上,再創(chuàng)建一個organization微服務(wù),由于創(chuàng)建步驟與創(chuàng)建license服務(wù)時大同小異,這里就不貼代碼出來,需要的可以去git上查看。(由于需要將license服務(wù)和organization服務(wù)注冊到Eureka上,實(shí)現(xiàn)這一功能的代碼會在下文貼出)

接下來我們開始進(jìn)入本節(jié)的重頭戲,服務(wù)發(fā)現(xiàn)代理的搭建。

搭建Spring Eureka服務(wù)

我們會使用Spring Boot搭建Eureka服務(wù),因?yàn)榉?wù)發(fā)現(xiàn)本質(zhì)上也是一個微服務(wù)。

  1. pom文件
    首先,把注意力放在pom.xml文件上,代碼如下:
    eureka服務(wù):
<?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>

    <groupId>cn.study.microservice</groupId>
    <artifactId>discovery-eureka</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>discovery-eureka</name>
    <description></description>

    <parent>
        <groupId>cn.study.microservice</groupId>
        <artifactId>second-discovery-euraka</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>
    </dependencies>

</project>

可以看到,pom文件中的依賴很少,只有一個"spring-cloud-starter-eureka-server",這是因?yàn)镾pring Cloud的Eureka server啟動依賴,其中包含多個必須的jar包,如"spring-cloud-netflix-eureka-server"、"spring-cloud-starter-ribbon"等。

同樣的,因?yàn)閘icense服務(wù)和organization服務(wù)會注冊到Eureka上,所以對應(yīng)的pom文件也需要作出修改,如下:
license服務(wù):

<?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>

    <groupId>cn.study.microservice</groupId>
    <artifactId>license-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>license-service</name>
    <description></description>

    <parent>
        <groupId>cn.study.microservice</groupId>
        <artifactId>second-discovery-euraka</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <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</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>

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

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

</project>

organization服務(wù):

<?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>

    <groupId>cn.study.microservice</groupId>
    <artifactId>organization-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>organization-service</name>
    <description></description>

    <parent>
        <groupId>cn.study.microservice</groupId>
        <artifactId>second-discovery-euraka</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <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</artifactId>
        </dependency>

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

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

</project>

上面兩個pom文件中,可以看到都引入了一個與eureka相關(guān)的依賴——"spring-cloud-starter-eureka",與服務(wù)發(fā)現(xiàn)引入的依賴——"spring-cloud-starter-eureka-server"略有不同,多了一個"-server",那么不同在哪里呢?可以打開idea提供的"Maven Project"視圖,打開方法如下:


image.png

打開后可以看到類似如下圖所示:

image.png

可以看到,"spring-cloud-starter-eureka-server"會引入"spring-cloud-netflix-eureka-server",而"spring-cloud-starter-eureka"會引入"spring-cloud-netflix-eureka-client",分別代表eureka服務(wù)端和客戶端。

另外,license、organization兩個服務(wù)還添加"spring-boot-starter-data-jpa"、"com.h2database.h2"。即數(shù)據(jù)庫使用的是h2,因?yàn)橐_保教程的代碼能開箱即用,如果使用mysql等其他開源數(shù)據(jù)庫,讀者還要去新建數(shù)據(jù)庫,如果數(shù)據(jù)庫賬戶密碼不同,還需要去改配置文件;數(shù)據(jù)訪問層框架使用的是"spring data jpa","spring-boot-starter-data-jpa"是spring boot提供的啟動依賴,里邊包含spring data jpa和hibernate-core等jar包,若之前未了解過spring data jpa,建議先去熟悉下。

最后,license服務(wù)pom文件中還多出了一個依賴:"spring-cloud-starter-feign"。該依賴是spring cloud提供的啟動依賴,主要包含了Netflix的一個開源項(xiàng)目feign項(xiàng)目的jar包,feign主要功能是對上文提及的實(shí)現(xiàn)了客戶端負(fù)載均衡的Ribbon的封裝,下文會詳細(xì)說明。

  1. 配置文件

eureka服務(wù)的application.yml配置文件:

#默認(rèn)端口號為8761
server:
  port: 8761

eureka:
  client:
    #由于該應(yīng)用為注冊中心,所以設(shè)置為false,代表不向注冊中心注冊自己
    registerWithEureka: false
    #由于注冊中心的職責(zé)就是維護(hù)服務(wù)實(shí)例,它并不需要去檢索服務(wù),所以也設(shè)置為false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  server:
    #關(guān)閉自我保護(hù)模式。自我保護(hù)模式是指,出現(xiàn)網(wǎng)絡(luò)分區(qū)、eureka在短時間內(nèi)丟失過多客戶端時,會進(jìn)入自我保護(hù)模式。
    #自我保護(hù):一個服務(wù)長時間沒有發(fā)送心跳包,eureka也不會將其刪除,默認(rèn)為true。
    enable-self-preservation: false
    #在Eureka服務(wù)器獲取不到集群里對等服務(wù)器上的實(shí)例時,需要等待的時間,單位為毫秒,默認(rèn)為1000 * 60 * 5
    wait-time-in-ms-when-sync-empty: 0

license服務(wù)的application.yml配置文件:

server:
  port: 10000

#服務(wù)發(fā)現(xiàn)客戶端
eureka:
  instance:
    #向Eureka注冊時,是否使用IP地址+端口號作為服務(wù)實(shí)例的唯一標(biāo)識。推薦設(shè)置為true
    prefer-ip-address: true

    #============分界線================
    #以下為基本不需要配置的屬性,屬性的值為默認(rèn)值

    #服務(wù)續(xù)約的調(diào)用時間間隔,默認(rèn)30秒
    lease-renewal-interval-in-seconds: 30
    #服務(wù)失效的時間,默認(rèn)90秒
    lease-expiration-duration-in-seconds: 90
    #非安全的通信端口號
    non-secure-port: 80
    #安全的通信端口號
    secure-port: 443
    #是否啟用非安全的通信端口號
    non-secure-port-enabled: true
    #是否啟用安全的通信端口號
    secure-port-enabled: false

  client:
    #是否將自身的實(shí)例信息注冊到Eureka服務(wù)端
    register-with-eureka: true
    #是否拉取并緩存其他服務(wù)注冊表副本到本地
    fetch-registry: true
    #注冊到哪個Eureka服務(wù)實(shí)例
    service-url:
      defaultZone: http://localhost:8761/eureka/

    #============分界線================
    #以下為基本不需要配置的屬性,屬性的值為默認(rèn)值

    #更新其他服務(wù)注冊表時間間隔,默認(rèn)30秒
    registry-fetch-interval-seconds: 30
    #更新實(shí)例信息的變化到Eureka服務(wù)端的間隔時間,單位為秒
    instance-info-replication-interval-seconds: 30
    #初始化實(shí)例信息到Eureka服務(wù)端的間隔時間,單位為秒
    initial-instance-info-replication-interval-seconds: 40
    #輪詢Eureka服務(wù)端地址更改的間隔時間,單位為秒。
    #當(dāng)我們與Sping Cloud Config配合,動態(tài)刷新Eureka的service url地址時需要關(guān)注該參數(shù)
    eureka-service-url-poll-interval-seconds: 300
    #讀取Eureka Server信息的超時時間,單位為秒
    eureka-server-read-timeout-seconds: 8
    #連接Eureka Server的超時時間,單位為秒
    eureka-server-connect-timeout-seconds: 5
    #從Eureka客戶端到所有Eureka服務(wù)端的連接總數(shù)
    eureka-server-total-connections: 200
    #從Eureka客戶端到每個Eureka服務(wù)端主機(jī)的連接總數(shù)
    eureka-server-total-connections-per-host: 50
    #Eureka服務(wù)連接的空閑關(guān)閉時間,單位為秒
    eureka-connection-idle-timeout-seconds: 30
    #心跳連接池的初始化線程數(shù)
    heartbeat-executor-thread-pool-size: 2
    #心跳超時重試延遲時間的最大乘數(shù)值
    heartbeat-executor-exponential-back-off-bound: 10
    #緩存刷新線程池的初始化線程數(shù)
    cache-refresh-executor-thread-pool-size: 2
    #緩存刷新重試延遲時間的最大乘數(shù)值
    cache-refresh-executor-exponential-back-off-bound: 10
    #使用DNS來獲取Eureka服務(wù)端的service url
    use-dns-for-fetching-service-urls: false
    #是否優(yōu)先使用處于相同Zone的Eureka服務(wù)端
    perfer-same-zone-eureka: true
    #獲取實(shí)例時是否過濾,僅保留UP狀態(tài)的實(shí)例
    filter-only-up-instances: true

#數(shù)據(jù)源的配置
spring:
  datasource:
    platform: h2
    schema: classpath:schema.sql
    data: classpath:data.sql
    driver-class-name: org.h2.Driver
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none

organization服務(wù)的application.yml配置文件:

server:
  port: 11000

eureka:
  instance:
    preferIpAddress: true
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

spring:
  datasource:
    platform: h2
    schema: classpath:schema.sql
    data: classpath:data.sql
    driver-class-name: org.h2.Driver
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none

每一個注冊到eureka的服務(wù)實(shí)例,eureka中都有兩個屬性與它關(guān)聯(lián)起來,分別為:application ID、instance ID。application ID是用來標(biāo)識某一個服務(wù)的實(shí)例集合,即同一個服務(wù)的不同實(shí)例,它們的application ID都是相同的;基于Spring Boot的微服務(wù),可以通過設(shè)置屬性spring.application.name來定義application ID(一般在bootstrap.yml中配置),上面的配置,license服務(wù)的application ID為licensingservice,organization服務(wù)的為organizationservice;instance ID默認(rèn)是一個隨機(jī)數(shù),用來定位服務(wù)的實(shí)例集合中某一實(shí)例,通常會配置eureka.instance.instance-id,如${spring.cloud.client.ipAddress}:${server.port},上面給出的配置并沒有配置該屬性,讀者可自行配置。

下面介紹幾個主要的屬性的作用:

  • 屬性eureka.instance.preferIpAddress:會通知eureka使用服務(wù)實(shí)例的IP地址進(jìn)行注冊,而不是它的主機(jī)名(hostname)。一般情況下,該屬性會被設(shè)置為true,其中一個原因是:基于云的微服務(wù)的生存周期很短暫且是無狀態(tài)的,這些微服務(wù)可以隨意啟動和關(guān)閉。因此使用IP地址更適合這種微服務(wù)。

  • 屬性eureka.client.registerWithEureka:代表該服務(wù)是否將自己注冊到eureka中;屬性eureka.client.fetchRegistry表明該服務(wù)是否從eureka服務(wù)端獲取其他服務(wù)的注冊信息到本地,將該屬性設(shè)置為true,就不用在每一次調(diào)用其他服務(wù)的接口時都要去eureka獲取目標(biāo)服務(wù)的信息。另外,若eureka.client.fetchRegistry設(shè)置為true,服務(wù)每隔30s會從eureka刷新服務(wù)信息到本地。

  • 屬性eureka.client.serviceUrl.defaultZone:代表服務(wù)會注冊到哪個eureka服務(wù)端實(shí)例。實(shí)現(xiàn)eureka的高可用需要關(guān)注這個屬性,這里不過多說明,需要了解更多關(guān)于eureka高可用,請自行Google或百度。

配置文件中的大多數(shù)屬性的作用都已在文件中給出注釋,就不過多贅述了。

另外license、organization兩個服務(wù)還多出一個bootstrap.yml文件,該文件主要配置微服務(wù)的application.name和profiles.active,該文件會在application.yml之前被加載。

  1. 啟動類

eureka服務(wù):

@SpringBootApplication
@EnableEurekaServer
public class Application {

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

觀察上面的代碼,可以看出相比普通的微服務(wù)啟動類,只多出了一個注解——@EnableEurekaServer,該注解表明該微服務(wù)會成為一個Eureka服務(wù),即服務(wù)發(fā)現(xiàn)的服務(wù)端。
啟動該服務(wù),可以看到控制臺會打印如下字符串:

Started Eureka Server
Tomcat started on port(s): 8761 (http)

表明eureka服務(wù)已成功啟動,并且在端口8761啟動,之前在配置文件中配置的server.port: 8761已經(jīng)起作用了。

接著在瀏覽器訪問: http://localhost:8761/,如下圖所示:

image.png

上圖圈中的區(qū)域是一個注冊到該eureka服務(wù)實(shí)例的所有服務(wù)實(shí)例列表,因?yàn)榇藭r尚未有任何服務(wù)注冊,所以列表為空。

organization服務(wù):

//該注解表明該類是項(xiàng)目(微服務(wù))的啟動類
@SpringBootApplication
@EnableEurekaClient
public class Application {
    //運(yùn)行該方法,會啟動整個Spring Boot服務(wù)
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

觀察上面organization服務(wù)啟動類的代碼,與eureka服務(wù)的相比,@EnableEurekaServer注解變成了@EnableEurekaClient,使用該注解的服務(wù),會在服務(wù)啟動的時候?qū)⒆约鹤缘絜ureka服務(wù)端。如果查看@EnableEurekaClient的源碼,如下:

@EnableDiscoveryClient
public @interface EnableEurekaClient {
}

可以看到,該注解其實(shí)是Netflix Eureka對注解@EnableDiscoveryClient的封裝,注解@EnableDiscoveryClient可以說是服務(wù)可以使用DiscoveryClient和Ribbon庫的觸發(fā)器,并將自己注冊到服務(wù)發(fā)現(xiàn)代理。所以你也可以選擇使用@EnableDiscoveryClient代替@EnableEurekaClient。

啟動organization服務(wù),可以看到控制臺有類似下圖的輸出:

image.png

此時若再次訪問http://localhost:8761/,可以看到類似如下圖的界面:

image.png

若出現(xiàn)的界面與上圖類似,說明到此為止應(yīng)用的搭建還算順利??梢钥吹缴蠄D的注冊到eureka服務(wù)的所有服務(wù)實(shí)例列表不為空,多出了一行,這一行列出的信息代表有一個application Id為"ORGANIZATIONSERVICE",instance ID為"10.10.1.216:organizationservice:11000"的服務(wù)實(shí)例注冊到eureka,"UP(1)"則代表該服務(wù)只有一個實(shí)例正在運(yùn)行。

license服務(wù):
此時先不考慮license服務(wù)會消費(fèi)organization服務(wù),所以啟動類的代碼跟organization服務(wù)的相同,這里就不貼出來。

啟動license服務(wù),然后刷新http://localhost:8761/,可以看到向eureka服務(wù)注冊的服務(wù)列表中多出了一行,說明剛剛啟動的license服務(wù)也注冊成功。

image.png
使用服務(wù)發(fā)現(xiàn)

接下來我們會通過三種方式來實(shí)現(xiàn)服務(wù)的發(fā)現(xiàn)。分別如下:

  • Spring Discovery client
  • Netflix Ribbon client
  • Netflix Feign client

首先,把license和organization服務(wù)關(guān)掉,eureka無所謂;然后把organization服務(wù)的其他相關(guān)代碼加上,如controller包、repository包、service包,詳細(xì)代碼請參考git上的代碼。我們把重點(diǎn)放在license服務(wù)代碼編寫上。下面開始進(jìn)入正題。

(License和Organization實(shí)體類請讀者自行加上)

  • Spring Discovery client
    首先創(chuàng)建一個服務(wù)發(fā)現(xiàn)客戶端類——OrganizationDiscoveryClient,路徑:cn.study.microservice.license.client.OrganizationDiscoveryClient,如下:
@Component
public class OrganizationDiscoveryClient {

    //當(dāng)服務(wù)啟動類加上@EnableEurekaClient或@EnableDiscoveryClient注解后,會自動注入實(shí)現(xiàn)DiscoveryClient接口的對象。此處會注入EurekaDiscoveryClient對象
    @Autowired
    private DiscoveryClient discoveryClient;

    public Organization getOrganization(String organizationId) {
        RestTemplate restTemplate = new RestTemplate();
        //會從eureka服務(wù)端獲取organizationservice服務(wù)的所有實(shí)例集合
        List<ServiceInstance> instances = discoveryClient.getInstances("organizationservice");

        if (instances.size()==0) return null;
        String serviceUri = String.format("%s/v1/organizations/%s",instances.get(0).getUri().toString(), organizationId);
        System.out.println("!!!! SERVICE URI:  " + serviceUri);

        ResponseEntity< Organization > restExchange =
                restTemplate.exchange(
                        serviceUri,
                        HttpMethod.GET,
                        null, Organization.class, organizationId);

        return restExchange.getBody();
    }
}

上面代碼中,涉及到一個類:RestTemplate,該類是Spring用于客戶端http同步訪問的主要類。具體用法,請讀者自行了解。

接著,創(chuàng)建cn.study.microservice.license.service.LicenseService,如下:

@Service
public class LicenseService {

    @Autowired
    private LicenseRepository licenseRepository;

    @Autowired
    private OrganizationDiscoveryClient organizationDiscoveryClient;

    private Organization retrieveOrgInfo(String organizationId, String clientType){
        Organization organization = null;

        switch (clientType) {
            case "discovery":
                System.out.println("I am using the discovery client");
                organization = organizationDiscoveryClient.getOrganization(organizationId);
                break;
            default:
                organization = organizationDiscoveryClient.getOrganization(organizationId);
        }

        return organization;
    }

    public License getLicense(String organizationId, String licenseId, String clientType) {
        License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);

        Organization org = retrieveOrgInfo(organizationId, clientType);

        return license
                .withOrganizationName( org.getName())
                .withContactName( org.getContactName())
                .withContactEmail( org.getContactEmail() )
                .withContactPhone( org.getContactPhone() )
                .withComment("");
    }

}

最后,編寫controller類,如下:

@RestController
@RequestMapping(value="v1/organizations/{organizationId}/licenses")
public class LicenseServiceController {
    @Autowired
    private LicenseService licenseService;

    @RequestMapping(value="/{licenseId}/{clientType}",method = RequestMethod.GET)
    public License getLicensesWithClient( @PathVariable("organizationId") String organizationId,
                                          @PathVariable("licenseId") String licenseId,
                                          @PathVariable("clientType") String clientType) {

        return licenseService.getLicense(organizationId,licenseId, clientType);
    }
}

到此,代碼編寫完畢,啟動服務(wù),然后使用postman訪問http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/discovery,結(jié)果如下:

image.png

可以看到,返回的結(jié)果是licenseId為"f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a"的License對象的json字符串,其中"organizationName"、"contactName"、"contactPhone"和"contactEmail"都是根據(jù)organizationId訪問organization服務(wù)提供的接口:/v1/organizations/{organizationId},然后返回一個Organization對象,最后將對象中的信息賦值到License對象中。實(shí)現(xiàn)這一功能的核心代碼如下:

...
ResponseEntity< Organization > restExchange =
        restTemplate.exchange(
                serviceUri,
                HttpMethod.GET,
                null, Organization.class, organizationId);

return restExchange.getBody();
...

exchange方法的方法簽名為:

public <T> ResponseEntity<T> exchange(
    String url, //需要訪問的資源的url
    HttpMethod method, //HTTPMethod枚舉,分別代表http定義的各種安全方法,如:GET、POST、PUT、DELETE等。
    HttpEntity<?> requestEntity, //請求實(shí)體,即請求攜帶的數(shù)據(jù)。為空時,賦null
    Class<T> responseType, //返回對象的Class類型
    Object... uriVariables //url路徑上變量對應(yīng)的值
)

此處,可以看成是license服務(wù)調(diào)用了organization服務(wù)的接口。該接口在organization服務(wù)的OrganizationServiceController類中被定義。讀者可以自行驗(yàn)證,在控制臺會答應(yīng)出變量"serviceUri"的值:http://10.10.1.216:11000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a,請讀者將"10.10.1.216"換成自己的IP或"localhost",然后用postman訪問。

至此,第一種實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)方式完成。原理是:使用DiscoveryClient從服務(wù)端獲取對應(yīng)服務(wù)的所有實(shí)例列表,然后取其中的一個實(shí)例,并獲取該實(shí)例的物理地址,最后調(diào)用該服務(wù)的接口。看到這里,讀者應(yīng)該對服務(wù)發(fā)現(xiàn)有更深的理解。license服務(wù)在調(diào)用organization服務(wù)的接口時,沒有使用硬編碼也行不通,因?yàn)樵诖a編寫階段,根本不知道服務(wù)會被部署到哪個服務(wù)器的哪個端口,而通過服務(wù)發(fā)現(xiàn),則可以動態(tài)地獲取目標(biāo)服務(wù)實(shí)例的物理地址(前提是目標(biāo)服務(wù)也注冊到eureka),進(jìn)而調(diào)用該服務(wù)的接口。

  • Netflix Ribbon client
    在編寫服務(wù)發(fā)現(xiàn)客戶端類前,先修改啟動類,添加一下代碼:
//該注解告知Spring Cloud創(chuàng)建一個基于Ribbon的RestTemplate,才可以實(shí)現(xiàn)客戶端負(fù)載均衡
@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
    return new RestTemplate();
}

接著,編寫服務(wù)發(fā)現(xiàn)客戶端類——OrganizationRestTemplateClient,如下:

@Component
public class OrganizationRestTemplateClient {
    @Autowired
    RestTemplate restTemplate;

    public Organization getOrganization(String organizationId){
        ResponseEntity<Organization> restExchange =
                restTemplate.exchange(
                        "http://organizationservice/v1/organizations/{organizationId}",
                        HttpMethod.GET,
                        null, Organization.class, organizationId);

        return restExchange.getBody();
    }
}

然后,修改LicenseService類,注入OrganizationRestTemplateClient、修改方法retrieveOrgInfo,如下:

...
@Autowired
private OrganizationRestTemplateClient organizationRestClient;
...
switch (clientType) {
    case "discovery":
        System.out.println("I am using the discovery client");
        organization = organizationDiscoveryClient.getOrganization(organizationId);
        break;
    case "rest":
        System.out.println("I am using the rest client");
        organization = organizationRestClient.getOrganization(organizationId);
        break;
    default:
        organization = organizationDiscoveryClient.getOrganization(organizationId);
}
...

至此,代碼修改完畢,啟動服務(wù)。使用postman調(diào)用:http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/rest。正確返回后,可以發(fā)現(xiàn)返回結(jié)果與第一種方式返回的結(jié)果相同。接下來,開始分析第二種方式是如何內(nèi)部工作的:

查看服務(wù)發(fā)現(xiàn)客戶端類OrganizationRestTemplateClient,可以發(fā)現(xiàn)同樣是使用RestTemplate的exchange方法來調(diào)用organization服務(wù)的接口,唯一不同的是方法的第一個參數(shù)值,這里硬編碼為:"http://organizationservice/v1/organizations/{organizationId}"。為什么這里可以這樣直接寫死在程序里,而且是使用"organizationservice"代替常規(guī)的服務(wù)實(shí)例物理地址。還記得在啟動類中注入的RestTemplate嗎,我們在注入的時候另外加了一個注解@LoadBalanced,答案就在這里。

加了注解@LoadBalanced后,代表在注入RestTemplate時,Ribbon會對其進(jìn)行加工,加工后的RestTemplate的能力有:

  1. 在調(diào)用其他服務(wù)的接口時,會從訪問的url中截取得到目標(biāo)服務(wù)的application ID,此處為"organizationservice",然后根據(jù)該服務(wù)獲取對應(yīng)的實(shí)例,最后正常訪問;
  2. 實(shí)現(xiàn)客戶端負(fù)載均衡

讀者可以自行驗(yàn)證,把注解去掉,然后重啟服務(wù),再訪問一次,結(jié)果肯定是報錯。如下:

{
  "timestamp": 1509695097081,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "org.springframework.web.client.ResourceAccessException",
  "message": "I/O error on GET request for \"http://organizationservice/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a\": organizationservice; nested exception is java.net.UnknownHostException: organizationservice",
  "path": "/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/rest"
}

可以看出,報的錯是未知主機(jī)名:organizationservice。如果將"organizationservice"換成"localhost:11000"就能正常訪問。

最后,我們來對可能是大家最關(guān)心的問題進(jìn)行驗(yàn)證,客戶端負(fù)載均衡。

  • 打包項(xiàng)目
    打開idea的Maven Projects,如下:
image.png
  • 啟動多個organization服務(wù)
    使用快捷鍵組合:Alt + F12,打開終端控制臺,然后進(jìn)入organization項(xiàng)目的target目錄,最后使用命令:
java -jar organization-service-0.0.1-SNAPSHOT.jar --server.port=11000

執(zhí)行該命令后,會在端口11000啟動一個organization服務(wù)。

接著按照同樣的方式在不同端口啟動一個或多個服務(wù)。首先點(diǎn)擊終端控制臺左上角的"+"圖標(biāo),創(chuàng)建一個新的終端,然后進(jìn)入target目錄,使用命令:

java -jar organization-service-0.0.1-SNAPSHOT.jar --server.port=11001

啟動多個organization服務(wù)后,多次訪問:http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/rest

觀察兩個終端控制臺,會出現(xiàn)如下圖所示的輸出:

image.png
image.png

由上圖可知客戶端負(fù)載均衡驗(yàn)證成功。license服務(wù)多次調(diào)用organization服務(wù)的接口,會隨機(jī)的訪問organization服務(wù)的某一實(shí)例,從而避免頻繁訪問某一個實(shí)例,達(dá)到負(fù)載均衡的目的。

使用Netflix Ribbon client可以很方便地實(shí)現(xiàn)對其它服務(wù)接口的調(diào)用,但每個接口的調(diào)用都要寫類似OrganizationRestTemplateClient.getOrganization方法的代碼,雖然不多,但有沒有更方便、簡介的實(shí)現(xiàn)方法呢?下面開始講如何使用"Netflix Feign client"實(shí)現(xiàn)。

  • Netflix Feign client

在編寫服務(wù)發(fā)現(xiàn)客戶端類前,向啟動類添加一個類注解:@EnableFeignClients。

image.png

接著,編寫服務(wù)發(fā)現(xiàn)客戶端類——OrganizationFeignClient,其實(shí)是一個接口:

@FeignClient("organizationservice")
public interface OrganizationFeignClient {
    @RequestMapping(
            method= RequestMethod.GET,
            value="/v1/organizations/{organizationId}",
            consumes="application/json")
    Organization getOrganization(@PathVariable("organizationId") String organizationId);
}

最后修改,LicenseService類,注入OrganizationRestTemplateClient、修改方法retrieveOrgInfo。如下:

...
@Autowired
OrganizationFeignClient organizationFeignClient;
...
switch (clientType) {
    case "discovery":
        System.out.println("I am using the discovery client");
        organization = organizationDiscoveryClient.getOrganization(organizationId);
        break;
    case "rest":
        System.out.println("I am using the rest client");
        organization = organizationRestClient.getOrganization(organizationId);
        break;
    case "feign":
        System.out.println("I am using the feign client");
        organization = organizationFeignClient.getOrganization(organizationId);
        break;
    default:
        organization = organizationDiscoveryClient.getOrganization(organizationId);
}
...

代碼修改完成,重啟license服務(wù)。然后訪問http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/feign。返回的結(jié)果與之前兩種方式一樣。
下面分析接口OrganizationFeignClient:
可以看到該接口上方添加了一個注解——FeignClient,注解的value值為:organizationservice,看到這里,應(yīng)該能猜出organizationservice代表的是需要調(diào)用的接口所屬的服務(wù)的application ID,而該接口的作用是:用于通知Feign組件對該接口進(jìn)行代理,而不需要編寫任何邏輯代碼。在服務(wù)啟動時,F(xiàn)eign會掃描標(biāo)有@FeignClient注解的接口,生成代理,并注冊到Spring容器中,因此可通過@Autowired注入。
接著就是接口中的方法的編寫??梢钥吹椒椒╣etOrganization添加了一個@RequestMapping注解,注解的value值就是目標(biāo)服務(wù)的接口。如果與第二種方式調(diào)用RestTemplate.exchange方法做對比,服務(wù)application ID+@RequestMapping的value值與exchange的第一個參數(shù)url對應(yīng);@RequestMapping的method與第二個 參數(shù)對應(yīng);接口方法的返回值與第四個參數(shù)對應(yīng);接口方法的所有帶@PathVariable注解的參數(shù)合并成一個數(shù)組與第五個參數(shù)對應(yīng);至于第三個參數(shù),由于該接口的請求體為空,所以在方法getOrganization的簽名中沒體現(xiàn)出來。

假設(shè)啟動多個organization服務(wù)實(shí)例,那么多次訪問http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/feign,也會負(fù)載均衡, 其負(fù)載均衡的默認(rèn)實(shí)現(xiàn)是基于 Netflix Ribbon。

完!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容