SPRING CLOUD微服務DEMO-上篇


1. 微服務架構

系統架構的演變從單應用,到根據每個模塊垂直拆分,到分布式服務,SOA,到目前進化成了微服務形態。

微服務的特點

  • 單一職責:微服務中每一個服務都對應唯一的業務能力,做到單一職責
  • 微:微服務的服務拆分粒度很小,例如一個用戶管理就可以作為一個服務。每個服務雖小,但“五臟俱全”。
  • 面向服務:面向服務是說每個服務都要對外暴露服務接口API。並不關心服務的技術實現,做到與平台和語言無關,也不限定用什么技術實現,只要提供Rest的接口即可。
  • 自治:自治是說服務間互相獨立,互不干擾
    • 團隊獨立:每個服務都是一個獨立的開發團隊,人數不能過多。
    • 技術獨立:因為是面向服務,提供Rest接口,使用什么技術沒有別人干涉
    • 前后端分離:采用前后端分離開發,提供統一Rest接口,后端不用再為PC、移動段開發不同接口
    • 數據庫分離:每個服務都使用自己的數據源
    • 部署獨立,服務間雖然有調用,但要做到服務重啟不影響其它服務。有利於持續集成和持續交付。每個服務都是獨立的組件,可復用,可替換,降低耦合,易維護

2. 遠程調用方式

2.1 RPC/RMI

RPC:Remote Produce Call遠程過程調用,類似的還有RMI。自定義數據格式,基於原生TCP通信,速度快,效率高。早期的webservice,現在熱門的dubbo,都是RPC的典型

2.2 Http

Http:http其實是一種網絡傳輸協議,基於TCP,規定了數據傳輸的格式。現在客戶端瀏覽器與服務端通信基本都是采用Http協議。也可以用來進行遠程服務調用。缺點是消息封裝臃腫。

2.3 如何選擇

既然兩種方式都可以實現遠程調用,我們該如何選擇呢?

  • 速度來看,RPC要比http更快,雖然底層都是TCP,但是http協議的信息往往比較臃腫,不過可以采用gzip壓縮。
  • 難度來看,RPC實現較為復雜,http相對比較簡單
  • 靈活性來看,http更勝一籌,因為它不關心實現細節,跨平台、跨語言。RPC方式需要在API層面進行封裝,限制了開發的語言環境。

因此,兩者都有不同的使用場景:

  • 如果對效率要求更高,並且開發過程使用統一的技術棧,那么用RPC還是不錯的。
  • 如果需要更加靈活,跨語言、跨平台,顯然http更合適

微服務,更加強調的是獨立、自治、靈活。而RPC方式的限制較多,因此微服務框架中,一般都會采用基於Http的Rest風格服務。

3. Http客戶端工具

可以使用一些流行的開源Http客戶端工具請求Rest接口

  • HttpClient
  • OKHttp
  • URLConnection

3.1 RestTemplate

使用Http客戶端工具請求Rest接口,得到數據后反序列化成對象。這樣做比較麻煩,Spring提供了一個RestTemplate模版工具類,對Http客戶端進行了封裝(默認使用URLConnection),實現了對象和Json的序列化和反序列化,方便得很。下面搭建一個Spring Boot工程,同時演示RestTemplate如何使用。

4. Spring Boot 搭建項目

微服務的一個重要特點是每個服務都是一個可以獨立運行的項目,要是按照以前的SSM的搭建方式,無法做到服務的快速搭建和部署。於是Spring Boot應運而生,它的理念就是約定大於配置,你選好模塊,自動幫你搭建好環境,非常便捷。

接下來用Spring Boot搭建兩個簡單的用戶微服務:user-serviceuser-consume,實現的功能是:user-consume使用RestTemplate調用user-service服務

具體參考這篇筆記(子文章)SPRING BOOT搭建兩個微服務模塊

5. Spring Cloud簡介

Spring Cloud是實現微服務架構的技術。它集成了Netflix公司的一些微服務組件。(如果你不知道這家公司,那你肯定不愛看美劇。)

Netflix微服務架構圖

  • Eureka:注冊中心
  • Zuul:服務網關
  • Ribbon:負載均衡
  • Feign:服務調用
  • Hystix:熔斷器

6. 微服務場景模擬

在第四節我們已經搭建好了user-serviceuser-consume兩個微服務。

consume使用RestTemplate調用service提供的rest接口,獲得json數據,反序列化成User對象返回到前台。其中存在着一些問題:

  • consume中調用的rest接口的地址是硬編碼的,不方便維護。
  • 如果service的rest接口變更或關閉了,consume並不知情,最終什么都得不到。
  • service只有1台,一旦宕機,整個服務就不可用了。如果擴展多台,那consume又要自己考慮負載均衡。

接下來介紹的幾個組件就是為解決這些問題而生的。

7. Eureka注冊中心

7.1 簡介

Eureka負責微服務的管理。

如果一個項目有數十個微服務,調用者想要自己找到一個適合的,可用的微服務是很麻煩的一件事。

就比如你想要坐車出門,自己上街攔出租車就很麻煩,要么司機拒載,要么車里已經有人了,要么干脆就沒有車來...后來就出現了滴滴,做為一個網約車的“注冊中心”,可以為你分配離你最近的空閑出租車。

Eureka就好比是滴滴,負責管理、記錄服務提供者的信息。服務調用者無需自己尋找服務,而是把自己的需求告訴Eureka,然后Eureka會把符合你需求的服務告訴你。

同時,服務提供方與Eureka之間通過“心跳”機制進行監控,當某個服務提供方出現問題,Eureka自然會把它從服務列表中剔除。

這就實現了服務的自動注冊、發現、狀態監控。

7.2 原理圖

  • EurekaServer:注冊中心,可以是多個Eureka的集群,對外暴露自己的地址。
  • 服務提供者:啟動后在注冊中心中注冊,成功后定期使用http方法向注冊中心發送心跳包,表明自己還活着。
  • 客戶端消費者:向注冊中心訂閱服務。注冊中心會向消費者發送合適的服務提供者列表,並且定期更新。需要使用某個服務時,可以從列表中找到並調用。

7.3 搭建注冊中心

搭建過程和第4節說的一樣,注意選擇Eureka模塊即可。

7.3.1 代碼和配置文件

  • 啟動類

@EnableEurekaServer注解表示這是一個注冊中心

@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}
  • 配置文件

現在我們只啟動一個eureka作為注冊中心。

這是Spring官方文檔中的Standalone Eureka Server的建議配置。

注意eureka下的client配置,這個配置意思是eureka應用作為一個客戶端,對注冊中心的動作。

其中service-url的配置必須要填寫,內容是注冊中心的地址,如果有多個,逗號隔開。

defaultZone路徑后面必須加上/eureka后綴,別問我為啥。

server:
  port: 10086 # 端口
spring:
  application:
    name: eureka-server # 應用名稱,會在Eureka中顯示
eureka:
  client:
    register-with-eureka: false # 是否注冊自己的信息到EurekaServer,默認是true
    fetch-registry: false # 是否拉取服務列表,默認是true
    service-url: # EurekaServer的地址,現在是自己的地址,如果是集群,需要加上其它Server的地址。
      defaultZone: http://127.0.0.1:${server.port}/eureka

訪問http://localhost:10086即可看到注冊中心的內容,eureka只有一個,服務列表為空:

7.4 將user-service注冊到eureka

7.4.1 添加依賴

eureka客戶端依賴

<!-- Eureka客戶端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Spring Cloud的依賴,注意我這里的版本是Greenwich.SR1

    <!-- SpringCloud的依賴 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <!-- Spring的倉庫地址 -->
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

7.4.2 開啟EurekaClient功能

添加@EnableDiscoveryClient注解

@SpringBootApplication
@EnableDiscoveryClient // 開啟EurekaClient功能
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

7.4.3 配置eureka客戶端屬性

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/XXXXX?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: XXXXX
    password: XXXXX
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
  application:
    name: user-service # 應用名稱
mybatis:
  type-aliases-package: com.vplus.demo.userservice.pojo
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 當調用getHostname獲取實例的hostname時,返回ip而不是host名稱
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的話會自己尋找

7.4.4 效果

7.5 user-consume從eureka中獲取服務

7.5.1 添加依賴

同上7.4.1

7.5.2 開啟EurekaClient功能

同上7.4.2

7.5.3 配置eureka客戶端屬性

server:
  port: 8082
spring:
  application:
    name: user-consume # 應用名稱
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 當其它服務獲取地址時提供ip而不是hostname
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的話會自己尋找

7.5.4 修改UserService

  • 之前是調用UserDao,UserDao使用RestTemplate請求遠程接口得到數據。
  • 現在我們在service層,從eureka中拉取服務列表,得到接口地址后請求數據。
@Service
public class UserService {
    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;// Eureka客戶端,可以獲取到服務實例信息

    public List<User> queryUserByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        // String baseUrl = "http://localhost:8081/user/";
        // 根據服務名稱,獲取服務實例
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        // 因為只有一個UserService,因此我們直接get(0)獲取
        ServiceInstance instance = instances.get(0);
        // 獲取ip和端口信息
        String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/";
        ids.forEach(id -> {
            // 我們測試多次查詢,
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次間隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }

}

7.6 eureka集群

7.6.1 搭建集群

Eureka可以集群搭建形成高可用的注冊中心。多個Eureka Server之間也會互相注冊為服務,當服務提供者注冊到Eureka Server集群中的某個節點時,該節點會把服務的信息同步給集群中的每個節點,從而實現數據同步。因此,無論客戶端訪問到Eureka Server集群中的任意一個節點,都可以獲取到完整的服務列表信息。

我們要兩個Eureka,端口號分別是10086、10087

可以使用Idea的啟動器復制功能復制eureka的啟動器。

將應用A的啟動器復制出一個A2,A啟動后,修改配置再啟動A2,這樣我們就可以得到兩個配置不同的應用了。

先把原來的eureka配置信息改為

server:
  port: 10086 # 端口
spring:
  application:
    name: eureka-server # 應用名稱,會在Eureka中顯示
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10087/eureka

啟動EurekaApplication,啟動起來后再將配置改為

server:
  port: 10087 # 端口
spring:
  application:
    name: eureka-server # 應用名稱,會在Eureka中顯示
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

再啟動EurekaApplication2,此時兩個注冊中心就形成了集群。

兩個eureka相互注冊,10086的defaultZone的url地址的端口號為10087,注意這一點。

再將兩個客戶端的Eureka相關配置改為

defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka

訪問http://localhost:10086http://localhost:10087都可以看到效果:

7.7 Eureka相關配置

7.7.1 服務提供者

服務提供者主要做兩個動作:服務注冊服務續約

  • 服務注冊的對應配置,true表示服務啟動后會向注冊中心發送注冊請求,默認就是開着的。
eureka
	client
		register-with-erueka: true
  • 服務續約的對應配置,這些都是默認值
eureka:
  instance:
    #服務失效時間,默認值90秒
    lease-expiration-duration-in-seconds: 90
    #服務續約(renew)的間隔,默認為30秒
    lease-renewal-interval-in-seconds: 30

默認情況下每個30秒服務會向注冊中心發送一次心跳,證明自己還活着。如果超過90秒沒有發送心跳,EurekaServer就會認為該服務宕機,會從服務列表中移除,注意,這是服務提供者告訴注冊中心我每30秒續約一次,90秒沒有續約,就表示我失效了,是配置在服務提供者上的,不是配置在注冊中心上的。

我實驗的時候,關閉一個服務,eureka幾乎是瞬間就知道服務down了

試了好幾次都是這樣,可能我的關閉動作出發了什么東西吧,時間有限,先不管這個問題,留個坑將來看。

后來我注意到服務關閉后會輸出一句:Unregistering ...,推測服務正常關閉時會自己通知注冊中心。

  • 實例ID名的修改

在頁面上,服務提供者實例ID顯示為:localhost:user-service:8081,

格式為:${hostname} + ${spring.application.name} + ${server.port},對應配置為

eureka:
  instance:
    instance-id: ${spring.application.name}:${server.port}

修改后啟動,變成了

7.7.2 服務消費者

消費者需要拉取服務列表,拉取時間間隔默認為30秒1次,對應配置為,如果是開發環境可適當縮小方便開發

eureka:
  client:
    registry-fetch-interval-seconds: 30

7.7.3 失效剔除和自我保護

  • 失效剔除:每個一段時間剔除掉失效的服務,默認為60s,為了方便開發設置為1s。

    服務器怎么知道失效了?看看上面服務提供者的配置,注意這幾個配置的聯系。

  • 自我保護:生產環境中,由於網絡延遲等原因,失效服務並不一定是真正失效了。如果被標記為失效的服務太多,超過了85%,此時eureka會把這些服務保護起來,先不剔除,保證大多數服務還可用。在開發中將自我保護模式關掉,方便開發。

eureka:
  server:
    enable-self-preservation: false # 關閉自我保護模式(默認為打開)
    eviction-interval-timer-in-ms: 1000 # 	掃描失效服務的間隔時間為1s(默認為60s)

8. Robbin負載均衡

8.1 開啟兩個user-service

具體操作參照7.6.1,這里設置兩個service的端口為8080和8081

8.2 開啟負載均衡

注意,是在調用端即consume上開啟負載均衡,Eureka中已經集成了Ribbon,無需引入新的依賴,只需要在調用端的RestTemplate的注冊Bean方法上添加注解:@LoadBalanced即可。這個方法位於UserConsumerApplication里。

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }

具體的調用方式也需要修改,將UserService中的queryUserByIds方法修改成這樣。

public List<User> queryUserByIds(List<Long> ids) {
       List<User> users = new ArrayList<>();
        // 地址直接寫服務名稱即可
        String baseUrl = "http://user-service/user/";
        ids.forEach(id -> {
            // 我們測試多次查詢,
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次間隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }
}

8.3 默認負載均衡策略分析

org.springframework.cloud.client.loadbalancer包里面有一個LoadBalancerInterceptor,它就是實現負載均衡的攔截器。

跟蹤源碼,找到了RibbonLoadBalancerClient,用consume多次請求接口,斷點調試

execute(String serviceId, LoadBalancerRequest<T> request, Object hint)方法:

就是輪詢

8.4 修改負載均衡策略

一個配置即可修改,有多種配置規則,這里使用隨機規則。

注意格式,是以服務名稱開頭的。

server:
  port: 8082
spring:
  application:
    name: user-consume # 應用名稱
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
    registry-fetch-interval-seconds: 5
  instance:
    prefer-ip-address: true # 當其它服務獲取地址時提供ip而不是hostname
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的話會自己尋找
###################################負載均衡配置###############################################
user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

8.5 重試機制

如果有一台服務突然掛掉了,而eureka還來不及將其清除出服務列表,或者消費者拉取的服務列表還有緩存,一旦請求到這台掛掉的服務就會報錯。雖然多次請求后結果也能出來,但體驗非常不好。

Ribbon的重試機制就是解決這個問題的。

引入依賴

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

開啟Spring Cloud的重試機制並配置

server:
  port: 8082
spring:
  application:
    name: user-consume # 應用名稱
  cloud:
    loadbalancer:
      retry:
        enabled: true # 開啟Spring Cloud的重試功能
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
    registry-fetch-interval-seconds: 5
  instance:
    prefer-ip-address: true # 當其它服務獲取地址時提供ip而不是hostname
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的話會自己尋找

user-service:
  ribbon:
    ConnectTimeout: 250 # Ribbon的連接超時時間
    ReadTimeout: 1000 # Ribbon的數據讀取超時時間
    OkToRetryOnAllOperations: true # 是否對所有操作都進行重試
    MaxAutoRetriesNextServer: 1 # 切換實例的重試次數
    MaxAutoRetries: 1 # 對當前實例的重試次數

就好了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM