1. 程式人生 > >SpringCloud服務發現註冊Eureka +Ribbon + Feign

SpringCloud服務發現註冊Eureka +Ribbon + Feign

release 平衡 get 微服務 enc 2個 baidu hostname snapshot

在本期將學習以下知識點:
  • 什麽是服務註冊和發現?
  • 基於Eureka的註冊服務器
  • 服務生產者
  • 結合Ribbon服務消費者
  • 結合Feign的服務生產者和消費者

什麽是服務註冊和發現

   假設有2個微服務A和B分別在端點http:// localhost:8181 /和http:// localhost:8282 /上運行,如果想要在A服務中調用B服務,那麽我們需要在A服務中鍵入B服務的url,這個url是負載均衡器分配給我們的,包括負載平衡後的IP地址,那麽很顯然,B服務與這個URL硬編碼耦合在一起了,如果我們使用了服務自動註冊機制,就可以使用B服務的邏輯ID,而不是使用特定IP地址和端口號來調用服務。

   我們可以使用Netflix Eureka Server創建Service Registry服務器,並將我們的微服務同時作為Eureka客戶端,這樣一旦我們啟動微服務,它將自動使用邏輯服務ID向Eureka Server註冊。然後,其他微服務(同樣也是Eureka客戶端)就可以使用服邏輯務ID來調用REST端點服務了。

   Spring Cloud使用Load Balanced RestTemplate創建Service Registry並發現其他服務變得非常容易。

   除了使用Netflix Eureka Server作為服務發現,也可以使用Zookeeper,但是根據CAP定理,在需要P網絡分區容忍性情況下,強一致性C和高可用性A只能選擇一個,Zookeeper是屬於CP,而Eureka是屬於AP,在服務發現方面,高可用性才是更重要,否則無法完成服務之間調用,而服務信息是否一致則不是最重要,A服務發現B服務時,B服務信息沒有及時更新,可能發生調用錯誤,但是調用錯誤總比無法連接到服務註冊中心要強。否則,服務註冊中心就成為整個系統的單點故障,存在極大的單點風險,這是我們為什麽需要分布式系統的首要原因。

基於Eureka的註冊服務器

  讓我們使用Netflix Eureka創建一個Service Registry,它只是一個帶有Eureka Server啟動器的SpringBoot應用程序。

  使用Intellij的Idea開發工具是非常容易啟動Spring cloud的:

技術分享圖片

可以從https://start.spring.io/網址,選擇相應組件即可。

由於我們需要建立一個註冊服務器,因此選擇Eureka Server組件即可,通過這些自動工具實際上是能自動生成Maven的配置:

<dependency>

????<groupId>org.springframework.cloud</groupId>

????<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>

</dependency>

我們需要給SpringBoot啟動類添加@EnableEurekaServer註釋,以使我們的SpringBoot應用程序成為基於Eureka Server的Service Registry。

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer

@SpringBootApplication

public class ServiceRegistryApplication {

????public static void main(String[] args) {

????????SpringApplication.run(ServiceRegistryApplication.class, args);

????}

}

默認情況下,每個Eureka服務器也是Eureka客戶端,客戶端一定會需要一個服務器URL來定位,否則就會不斷報錯,由於我們只有一個Eureka Server節點(獨立模式),我們將通過在application.properties文件中配置以下屬性來禁用此客戶端行為。

SpringCloud有properties和YAML兩種配置方式,這兩種配置方式其實只是形式不同,properties配置信息格式是a.b.c,而YAML則是a:b:c:,兩者本質是一樣的,只需要其中一個即可,這裏以properties為案例:

spring.application.name=jdon-eureka-server
server.port=1111
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

現在運行ServiceRegistryApplication並訪問http:// localhost:1111,如果不能訪問,說明沒有正常啟動,請檢查三個環節:pom.xml是否配置正確?需要Eureka和配置

SpringBoot的註釋@EnableEurekaServer是否增加了?

最後,application.properties是否配置?

SpringCloud其實非常簡單,約定大於配置,默認只要配置服務器端口就可以了,然後是一條註釋@EnableEurekaServer,就能啟動Eurek服務器了。

服務器準備好後,我們就要準備服務生產者,向服務器裏面註冊自己,服務消費者則是從服務器中發現註冊的服務然後調用。

服務生產者

   服務生產者其實首先是Eureka的客戶端,生產者將自己註冊到前面啟動的服務器當中,引如果是idea的導航,選擇CloudDiscovery的EurekaDiscovery,如果是 Maven則引入包依賴是:

<dependency>

????<groupId>org.springframework.cloud</groupId>

????<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

</dependency>

這樣,spring-cloud-starter-netflix-eureka-client這個jar包就放入我們系統的classpath,為了能夠正常使用這個jar包,還需要配置,只需要在application.properties中配置eureka.client.service-url.defaultZone屬性即可自動註冊Eureka Server:

eureka.client.service-url.defaultZone=http://localhost:1111/eureka/

當我們的服務在Eureka Server註冊時,它會持續發送一定時間間隔的心跳。如果Eureka服務器沒有從任何服務的實例接收到心跳,它將認為這個服務實例已經關閉並從自己的池中剔除它。

以上是服務生產者註冊服務的過程,比較簡單,為了使我們的服務生產者能的演示代碼夠運行起來,我們還需要新建一個服務生產者代碼:

@RestController
public class ProducerService {

    @GetMapping("/pengproducer")
    public String sayHello(){
        return "hello world";
    }
}

這段代碼是將服務暴露成RESTful接口,@RestController是聲明Rest接口,/pengproducer是REST的訪問url,通過get方式能夠獲得字符串:hello world

因為REST屬於WEB的一種接口,因此需要在pom.xml中引入Web包:

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

然後在application.properties中加入有關REST接口的配置:

spring.application.name=PengProducerService
server.port=2111

指定我們的生產者服務的名稱是PengProducerService,REST端口開在2111。

現在可以在idea中啟動我們的應用了,這樣我們啟動這個項目,就可以在http://127.0.0.1:2111/ 訪問這個REST服務。同時,因為我們之前已經啟動了註冊服務器,訪問http://localhost:1111/你會發現PengProducerService出現在服務列表中:

技術分享圖片

上面啟動應用服務是在idea編輯器中,我們還可以通過命令行啟動我們的服務生產者:

java -jar -Dserver.port=2112 producer-0.0.1-SNAPSHOT.jar

這個是在端口2112開啟我們的服務端點了。現在再問http://localhost:1111/,你會看到可用節點Availability Zones下面已經從(1)變為(2),現在我們的服務生產者已經有兩個實例在運行,當服務的消費者訪問這個兩個實例時,它可以根據負載平衡策略比如輪詢訪問其中一個服務生產者實例。

總結一下,為了讓服務生產者註冊到Euraka服務器中,只需要兩個步驟:

  • 1. 引入spring-cloud-starter-netflix-eureka-client包
  • 2. 配置Eurake服務器的地址

請註意,spring-cloud-starter-netflix-eureka-client包是Spring Cloud升級後最新的包名,原來是spring-cloud-starter-eureka,裏面沒有netflix,這是過去版本,Spring Boot 1.5以後都是加入了netflix的,見Spring Cloud Edgware Release Notes

另外,這裏不需要在SpringBoot主代碼中再加入@enablediscoveryclient 或 @enableeurekaclient,只要eureka的client包在maven中配置,也就會出現在系統的classpath中,這樣就會默認自動註冊到eureka服務器中了。

這部分×××:百度網盤。

下面我們準備訪問這個服務生產者PengProducerService的消費者服務:

結合Ribbon的服務消費者

   上個章節我們已經啟動了兩個服務生產者實例,如何通過負載平衡從兩個中選擇一個訪問呢?這時就需要Ribbon,為了使用Ribbon,我們需要使用@LoadBalanced元註解,那麽這個註解放在哪裏呢?一般有兩個DiscoveryClient 和 RestTemplate,這兩個的區別是:

1. DiscoveryClient可以獲得服務提供者(生產者)的多個實例集合,能讓你手工決定選擇哪個實例,這裏負載平衡的策略比如round robin輪詢就不會派上,實際就沒有使用Ribbon:

List<ServiceInstance> instances=discoveryClient.getInstances("PengProducerService");
ServiceInstance serviceInstance=instances.get(0);

2.RestTemplate則是使用Ribbon的負載平衡策略,使用@LoadBalanced註釋resttemplate並使用zuul代理服務器作為邊緣服務器。那麽對zuul邊緣服務器的任何請求將默認使用Ribbon進行負載平衡,而resttemplate將以循環方式路由請求。這部分代碼如下:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.client.RestTemplate;

@Controller
public class ConsumerService {

    @Autowired
    private RestTemplate restTemplate;

    public String callProducer() {
        ResponseEntity<String> result =
                this.restTemplate.getForEntity(
                        "http://PengProducerService/pengproducer",
                        String.class,
                        "");
        if (result.getStatusCode() == HttpStatus.OK) {
            System.out.printf(result.getBody() + " called in callProducer");
            return result.getBody();
        } else {
            System.out.printf(" is it empty");
            return " empty ";
        }
    }
}

RestTemplate是自動註射進這個控制器,在這控制器,我們調用了服務生產者http://PengProducerService/pengproducer,然後獲得其結構。

這個控制器的調用我們可以在SpringBoot啟動函數裏調用:

@SpringBootApplication
public class ConsumerApplication {

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

    public static void main(String[] args) {
        ApplicationContext ctx  =  SpringApplication.run(ConsumerApplication
                .class, args);
        ConsumerService consumerService = ctx.getBean(ConsumerService.class);
        System.out.printf("final result RestTemplate=" + consumerService
                .callProducer() + " \n");
    }
}

註意到@LoadBalanced是標註在RestTemplate上,而RestTemplate是被註入到ConsumerService中的,這樣通過調用RestTemplate對象實際就是獲得負載平衡後的服務實例。這個可以通過我們的服務提供者裏面輸出hashcode來分辨出來,啟動兩個服務提供者實例,每次運行ConsumerService,應該是依次打印出不同的hashcode:

hello world1246528978 called in callProducerfinal result RestTemplate=hello world1246528978

再次運行結果:

hello world1179769159 called in callProducerfinal result RestTemplate=hello world1179769159

hellow world後面的哈希值不同,可見是來自不同的服務提供者實例。

如果系統基於https進行負載平衡,那麽只需要兩個步驟:

1.application.properties中激活ribbon的https:

ribbon.IsSecure=true

2.代碼中RestTemplate初始化時傳入ClientHttpRequestFactory對象:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    CloseableHttpClient httpClient = HttpClientUtil.getHttpClient();
    HttpComponentsClientHttpRequestFactory clientrequestFactory = new HttpComponentsClientHttpRequestFactory();
    clientrequestFactory.setHttpClient(httpClient);
    RestTemplate restTemplate = new RestTemplate(clientrequestFactory);
    return restTemplate;
}

這部分×××:百度網盤

結合Feign的服務生產者和消費者

   上篇是使用Ribbon實現對多個服務生產者實例使用負載平衡的方式進行消費,在調用服務生產者時,返回的是字符串類型,如果返回是各種自己定義的對象,這些對象傳遞到消費端是通過JSON方式,那麽我們的消費者需要使用Feign來訪問各種Json對象。

   需要註意的是:Feign = Eureka +Ribbon + RestTemplate,也就是說,使用Feign訪問服務生產者,無需前面章節那麽關於負載平衡的代碼了,前面我們使用RestTemplate進行負載平衡訪問,代碼還是挺復雜  

   現在我們開始Feign的實現:首先我們在服務的生產者那邊進行修改,讓我們生產者項目變得接近實戰中項目,增加領域層、服務層和持久層。

   假設新增Article領域模型對象,我們就需要倉儲保存,這裏我們使用Spring默認約定,使用JPA訪問h2數據庫,將Article通過JPA保存到h2數據庫中:

要啟用JPA和h2數據庫,首先只要配置pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

Article領域模型對象作為需要持久的實體對象:配置實體@Entity和@Id主鍵即可:

@Entity
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    private String body;
    private Date startDate;

然後我們建立一個空的Article倉儲接口即可:

@Repository
public interface ArticleRep extends JpaRepository<Article,Long> {
}

這樣,關於Article的CRUD實現就已經有了,不需要自己再編寫任何SQL語句。這樣我們編寫一個Service就可以提供Article對象的CRUD方法,這裏只編寫插入和查詢批量兩個方法:

@Service
public class ArticleService {

    @Autowired
    ArticleRep articleRep;

    public List<Article> getAllArticles(){
        return articleRep.findAll();
    }

    public void insertArticle(Article article){
        articleRep.save(article);
    }
}

我們在REST接口中暴露這兩種方法:

  • 1. get /articles是查詢所有對象
  • 2. post /article是新增

    @RestController
    public class ProducerService {
    
    @Autowired
    ArticleService articleService;
    
    @GetMapping("/articles")
    public List<Article> getAllArticles(){
        return articleService.getAllArticles();
    }
    
    @GetMapping("/article")
    public void publishArticle(@RequestBody Article article){
        articleService.insertArticle(article);
    }

上面服務的生產者提供了兩個REST url,我們在消費者這邊使用/articles以獲得所有文章:

@FeignClient(name="PengProducerService")
public interface ConsumerService {

    @GetMapping("/articles")
    List<Article> getAllArticles();
}

這是我們消費者的服務,調用生產者 /articles,這是一個接口,無需實現,註意需要標註FeignClient,其中寫入name或value微服務生產者的application.properties配置:

spring.application.name=PengProducerService

當然,這裏會直接耦合PengProducerService這個名稱,我們以後可以通過配置服務器更改,這是後話。

然後需要在應用Application代碼加入@EnableFeignClients:

@SpringBootApplication
@EnableFeignClients
public class FeignconsumerApplication {

    public static void main(String[] args)  {
        ApplicationContext context = SpringApplication.run(FeignconsumerApplication
                        .class, args);
        ConsumerService consumerService = context.getBean(ConsumerService
                .class);
        System.out.printf("#############all articles ok" + consumerService
                .getAllArticles());
    }

在FeignconsumerApplication我們調用了前面接口ConsumerService,而ConsumerService則通過負載平衡調用另外一個生產者微服務,如果我們給那個生產者服務加入一些Articles數據,則這裏就能返回這些數據:

#############all articles ok[[email protected], [email protected]]

說明調用成功。

在調試過程中,曾經出現錯誤:

Load balancer does not have available server for client:PengProducerService

經常排查是由於生產者項目中pom.xml導入的是spring-cloud-starter-netflix-eureka-client,改為pring-cloud-starter-netflix-eureka-server就可以了,這是SpringBoot 2.0發現的一個問題。

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

本章的代碼下載:百度網盤

總結

   通過這個項目學習,我們如同蠶絲剝繭層層搞清楚了Spring Cloud的微服務之間同步調用方式,發現基於REST/JSON的調用代碼最少,也是最方便,Feign封裝了Ribbon負載平衡和Eureka服務器訪問以及REST格式處理。

SpringCloud服務發現註冊Eureka +Ribbon + Feign