almost 2 years ago

Microservice 其實不是很好管理,可想而知會有非常多路由、組態、監控等問題要搞,但是如果你團隊都是用Java的話,基本上 SpringCloud 提供非常多組件,讓你使用一些簡單設定檔跟 Annotation 就可以搞定 Discovery、Synchronize Settings、Proxy、LoadBalance、Realtime Dashboards、LogAnalyzer 等機制,例如下圖。

選擇組件的話到這邊http://start.spring.io/勾選需要的組件後下載專案即可
這次練習選擇使用 SpringBoot1.3 & Gradle

使用 SpringBoot 建立 RestAPI
建立統一管理的 Config Server
新增自動發現服務
新增 Proxy 機制
增加 LoadBalance 機制
透過 redis 來轉發請求
增加服務中斷時的回覆訊息
即時監控
Log 收集
References

使用 SpringBoot 建立 RestAPI

先建立一個 reservation-service 專案
使用到的組件如下

Web Data Cloud Config Cloud Discovery Cloud Tracing Cloud Messaging Database Ops
Web JPA Config Client Eureka Discovery Zipkin Stream Redis H2 Actuator
Rest Repositories - - - - - - -

首先先把下面這幾個依賴註解起來,因為暫時用不到

build.gradle
dependencies {
    /*
    compile('org.springframework.cloud:spring-cloud-starter-config')
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    compile('org.springframework.cloud:spring-cloud-starter-zipkin')
    compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
    compile('org.springframework.boot:spring-boot-starter-actuator')
    */
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-data-rest')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('com.h2database:h2')
    testCompile('org.springframework.boot:spring-boot-starter-test') 
}
application.properties
server.port=8025
ReservationServiceApplication.java
package com.example;

import java.util.Arrays;
import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;

@SpringBootApplication
public class ReservationServiceApplication {
    
    //起動的時候預先塞測試資料

    @Bean
    CommandLineRunner runner(ReservationRepository rr){
        return args -> {
            Arrays.asList("Dr. rod,Dr. Syer,Juergen,ALL THE COMMUNITY,Josh".split(","))
            .forEach( x -> rr.save(new Reservation(x)));;
            rr.findAll().forEach( System.out::println);
        };
    }

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

//這個註解是把你的Repository直接變成 RESTful API

@RepositoryRestResource
interface ReservationRepository extends JpaRepository<Reservation, Long>{

    @RestResource(path = "by-name")
    Collection<Reservation> findByReservationName( @Param("rn") String rn);
}

@Entity
class Reservation{
    @Id
    @GeneratedValue
    private Long id;

    private String reservationName;
    public Reservation(){}

    public Reservation(String reservationName) {
        this.reservationName = reservationName;
    }

    public Long getId() {
        return id;
    }

    public String getReservationName() {
        return reservationName;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("Reservation{");
        sb.append("id=").append(id);
        sb.append(", reservationName='").append(reservationName).append("'}");
        return sb.toString();
    }
}

這是產生器幫我們建立的測試範本

ReservationServiceApplicationTests.java
package com.example;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ReservationServiceApplication.class)
@WebAppConfiguration
public class ReservationServiceApplicationTests {

    @Test
    public void contextLoads() {
    }

}

接著使用GET 向 http://localhost:8025/reservations 取得資料,得到這樣的結果

{
  "_embedded": {
    "reservations": [
      {
        "reservationName": "Dr. rod",
        "_links": {
          "self": {
            "href": "http://localhost:8025/reservations/1"
          },
          "reservation": {
            "href": "http://localhost:8025/reservations/1"
          }
        }
      },
      {
        "reservationName": "Dr. Syer",
        "_links": {
          "self": {
            "href": "http://localhost:8025/reservations/2"
          },
          "reservation": {
            "href": "http://localhost:8025/reservations/2"
          }
        }
      },
      {
        "reservationName": "Juergen",
        "_links": {
          "self": {
            "href": "http://localhost:8025/reservations/3"
          },
          "reservation": {
            "href": "http://localhost:8025/reservations/3"
          }
        }
      },
      {
        "reservationName": "ALL THE COMMUNITY",
        "_links": {
          "self": {
            "href": "http://localhost:8025/reservations/4"
          },
          "reservation": {
            "href": "http://localhost:8025/reservations/4"
          }
        }
      },
      {
        "reservationName": "Josh",
        "_links": {
          "self": {
            "href": "http://localhost:8025/reservations/5"
          },
          "reservation": {
            "href": "http://localhost:8025/reservations/5"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8025/reservations"
    },
    "profile": {
      "href": "http://localhost:8025/profile/reservations"
    },
    "search": {
      "href": "http://localhost:8025/reservations/search"
    }
  },
  "page": {
    "size": 20,
    "totalElements": 5,
    "totalPages": 1,
    "number": 0
  }
}

簡單幾行就可以把資料庫轉成 RESTful API 主要是靠這個 @RepositoryRestResource

建立統一管理的 Config Server

建立一個 config-server 專案

使用到的組件如下

Cloud Config
Config Server

主要依賴是 spring-cloud-config-server

build.gradle
dependencies {
    compile('org.springframework.cloud:spring-cloud-config-server')
    testCompile('org.springframework.boot:spring-boot-starter-test') 
}
application.properties
spring.cloud.config.server.git.uri=D:/springcloud/config-repo

server.port=8888
  • spring.cloud.config.server.git.uri 設定應用起動時從哪裡讀取設定檔,可以從Github,也可以從本地Git檔案

D:/springcloud/config-repo資料夾放的檔案

application.yml
server.port: ${PORT:${SERVER_PORT:0}}

info.id: ${spring.application.name}

debug: true

spring.sleuth.log.json.enabled: true

logging.pattern.console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([trace=%X{X-Trace-Id:-},span=%X{X-Span-Id:-}]){yellow} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex"
reservation-service.properties
server.port=${PORT:8000}

message=HELLO world!

spring.cloud.stream.bindings.input=reservations

起動程式

ConfigServerApplication.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

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

記得加上@EnableConfigServer就可以啟動ConfigServer的功能了

起動 Config-Server 後可以訪問 http://localhost:8888/reservation-service/master 就可以取得設定相關資料

{
  "name": "reservation-service",
  "profiles": [
    "master"
  ],
  "label": null,
  "version": "b017cbcb47700df4ffd7e824614532dd18128040",
  "propertySources": [
    {
      "name": "D:/springcloud/config-repo/reservation-service.properties",
      "source": {
        "server.port": "${PORT:8000}",
        "spring.cloud.stream.bindings.input": "reservations",
        "message": "HELLO world!"
      }
    },
    {
      "name": "D:/springcloud/config-repo/application.yml",
      "source": {
        "server.port": "${PORT:${SERVER_PORT:0}}",
        "info.id": "${spring.application.name}",
        "debug": true,
        "spring.sleuth.log.json.enabled": true,
        "logging.pattern.console": "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([trace=%X{X-Trace-Id:-},span=%X{X-Span-Id:-}]){yellow} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex"
      }
    }
  ]
}

修改 reservation-service

把原先得設定檔改成依靠 Config-Server 提供的

把原先註解掉的依賴加回去 spring-cloud-starter-config 跟 spring-boot-starter-actuator

build.gradle
dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-config')
    /*
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    compile('org.springframework.cloud:spring-cloud-starter-zipkin')
    compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
    */
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-data-rest')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('com.h2database:h2')
    testCompile('org.springframework.boot:spring-boot-starter-test') 
}

記得更新一下依賴

把原本的application.properties重新命名為bootstrap.properties並改成以下內容

bootstrap.properties
spring.application.name=reservation-service

spring.cloud.config.uri=http://localhost:8888
  • spring.application.name 應用自己的名稱,到時候可以從介面上看到,也必須對應到設定檔的名稱
  • spring.cloud.config.uri Config-Server的位置

增加個控制器可以顯示從Condif-Server得到的資料

@RefreshScope
@RestController
class MessageRestControler{  
    @Value("${message}")
    private String message;
    
    @RequestMapping("/message")
    String message(){
        return this.message;
    }    
}

只要啟動後可以在 http://localhost:8000/message 取得 HELLO world! 的資料
注意 Port 變了喔,因為一開始就從 Config-Server 取得 reservation-service.properties 的內容,也取得了 message=HELLO world! 的內容來呈現。

加上 @RefreshScope 用意是當設定檔有變更時,你可以透過 URL 來觸發更新

curl -X POST 'http://localhost:8000/refresh'

但是要怎麼隨時保持同步請看另外一篇研究
使用 SpringCloud 同步所有節點設定

新增自動發現服務

建立一個 eureka-server 專案

使用到的組件如下

Cloud Config Cloud Discovery
Config Client Eureka Server

新增 bootstrap.properties 然後把不需要的 application.properties 刪除,因為組態檔我們使用 Config Server 提供的

bootstrap.properties
spring.application.name=eureka-service

spring.cloud.config.uri=http://localhost:8888

新增 eureka-service.properties 到 Config-Server 設定檔路徑底下

eureka-service.properties
server.port=${PORT:8761}

eureka.client.register-with-eureka=false

eureka.client.fetch-registry=false

#eureka.client.enabled=false
eureka.instance.hostname=localhost

#eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

其實 hostname 理論上是可以不用加的,但是會出現以下的錯誤訊息,不過網路上是說這沒關係是沒有Client的錯誤

2015-11-09 14:53:30.685 ERROR 21880 --- [trace=,span=] [nio-8761-exec-1] c.n.eureka.resources.StatusResource      : Could not determine if the replica is available 

java.lang.NullPointerException: null
    at com.netflix.eureka.resources.StatusResource.isReplicaAvailable(StatusResource.java:87)
    at com.netflix.eureka.resources.StatusResource.getStatusInfo(StatusResource.java:67)
    at org.springframework.cloud.netflix.eureka.server.EurekaController.status(EurekaController.java:68)

主程式部份

EurekaServerApplication.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

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

起動後就可以從 http://localhost:8761/ 觀察到目前有哪些服務

修改 reservation-service 新增自動發現的客戶端

把 spring-cloud-starter-eureka 依賴加回去

build.gradle
dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-config')
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    /*
    compile('org.springframework.cloud:spring-cloud-starter-zipkin')
    compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
    */
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-data-rest')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('com.h2database:h2')
    testCompile('org.springframework.boot:spring-boot-starter-test') 
}

記得更新依賴後在起動類別加上 @EnableDiscoveryClient

@EnableDiscoveryClient
@SpringBootApplication
public class ReservationServiceApplication {
    
    //起動的時候預先塞測試資料

    @Bean
    CommandLineRunner runner(ReservationRepository rr){
        return args -> {
            Arrays.asList("Dr. rod,Dr. Syer,Juergen,ALL THE COMMUNITY,Josh".split(","))
            .forEach( x -> rr.save(new Reservation(x)));;
            rr.findAll().forEach( System.out::println);
        };
    }

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

起動後再回去 http://localhost:8761/ 觀察,就可以發現在 DS Replicas Instances currently registered with Eureka 列表中多了一個 RESERVATION-SERVICE 的應用名稱

新增 Proxy 機制

建立一個 reservation-client 專案

使用到的組件如下

Web Cloud Config Cloud Discovery Cloud Routing Cloud Circuit Breaker Cloud Tracing Cloud Messaging Ops
HATEOAS Config Client Eureka Discovery Zuul Hystrix Zipkin Stream Redis Actuator

先暫時將 zipkin 跟 hateoas 註解起來練習 Proxy 機制

build.gradle
dependencies {
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.cloud:spring-cloud-starter-config')
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    compile('org.springframework.cloud:spring-cloud-starter-hystrix')
    /*
    compile('org.springframework.boot:spring-boot-starter-hateoas')
    compile('org.springframework.cloud:spring-cloud-starter-zipkin')
    */
    compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
    compile('org.springframework.cloud:spring-cloud-starter-zuul')
    testCompile('org.springframework.boot:spring-boot-starter-test') 
}

把範例的 application.properties 刪掉並新增 bootstrap.properties

bootstrap.properties
spring.application.name=reservation-client

spring.cloud.config.uri=http://localhost:8888

起動程式

ReservationClientApplication.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class ReservationClientApplication {

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

在原先的範例程式上增加 @EnableZuulProxy 跟 @EnableDiscoveryClient
起動後即可 http://localhost:8050/reservation-service/reservations 取得原本 reservation-service 上的資料如下,可以很明顯的得出來多了一層 reservation-service 代理路徑

{
  "_embedded": {
    "reservations": [
      {
        "reservationName": "Dr. rod",
        "_links": {
          "self": {
            "href": "http://localhost:8050/reservation-service/reservations/1"
          },
          "reservation": {
            "href": "http://localhost:8050/reservation-service/reservations/1"
          }
        }
      },
      {
        "reservationName": "Dr. Syer",
        "_links": {
          "self": {
            "href": "http://localhost:8050/reservation-service/reservations/2"
          },
          "reservation": {
            "href": "http://localhost:8050/reservation-service/reservations/2"
          }
        }
      },
      {
        "reservationName": "Juergen",
        "_links": {
          "self": {
            "href": "http://localhost:8050/reservation-service/reservations/3"
          },
          "reservation": {
            "href": "http://localhost:8050/reservation-service/reservations/3"
          }
        }
      },
      {
        "reservationName": "ALL THE COMMUNITY",
        "_links": {
          "self": {
            "href": "http://localhost:8050/reservation-service/reservations/4"
          },
          "reservation": {
            "href": "http://localhost:8050/reservation-service/reservations/4"
          }
        }
      },
      {
        "reservationName": "Josh",
        "_links": {
          "self": {
            "href": "http://localhost:8050/reservation-service/reservations/5"
          },
          "reservation": {
            "href": "http://localhost:8050/reservation-service/reservations/5"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8050/reservation-service/reservations"
    },
    "profile": {
      "href": "http://localhost:8050/reservation-service/profile/reservations"
    },
    "search": {
      "href": "http://localhost:8050/reservation-service/reservations/search"
    }
  },
  "page": {
    "size": 20,
    "totalElements": 5,
    "totalPages": 1,
    "number": 0
  }
}

增加 LoadBalance 機制

修改 reservation-client 專案

先把依賴 hateoas 加回去

build.gradle
dependencies {
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.cloud:spring-cloud-starter-config')
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    compile('org.springframework.cloud:spring-cloud-starter-hystrix')
    compile('org.springframework.boot:spring-boot-starter-hateoas')
    /*
    compile('org.springframework.cloud:spring-cloud-starter-zipkin')
    */
    compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
    compile('org.springframework.cloud:spring-cloud-starter-zuul')
    testCompile('org.springframework.boot:spring-boot-starter-test') 
}

然後新增一個控制器

@RestController
@RequestMapping("/reservations")
class ReservationApiGatewayRestController{

    @Autowired
    @LoadBalanced
    private RestTemplate restTemplate;

    @RequestMapping("names")
    public Collection<String> getReservationNames(){

        ParameterizedTypeReference<Resources<Reservation>> ptr = 
                new ParameterizedTypeReference<Resources<Reservation>>(){};

                ResponseEntity<Resources<Reservation>> responseEntity = 
                        this.restTemplate.exchange("http://reservation-service/reservations", 
                                HttpMethod.GET, null, ptr);
                
                Collection<String> nameList = responseEntity
                        .getBody()
                        .getContent()
                        .stream()
                        .map(Reservation::getReservationName)
                        .collect(Collectors.toList());

                return nameList;
    }
}

這邊其實猜得出來它的使用方式,然後下面的部分是Java8的語法喔。

再啟動後從 http://localhost:8050/reservations/names 嘗試抓取資料,結果是可以取得預期的姓名清單

[
  "Dr. rod",
  "Dr. Syer",
  "Juergen",
  "ALL THE COMMUNITY",
  "Josh"
]

透過 redis 來轉發請求

修改 reservation-client 專案

先修改 build.gradle 把 spring-cloud-starter-stream-redis 加進來

build.gradle
dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-config')
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    /*
    compile('org.springframework.cloud:spring-cloud-starter-zipkin')
    */
    compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-data-rest')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('com.h2database:h2')
    testCompile('org.springframework.boot:spring-boot-starter-test') 
}

主程式上方新增 @EnableBinding (Source.class)

@EnableZuulProxy
@EnableBinding (Source.class)
@EnableDiscoveryClient
@SpringBootApplication
public class ReservationClientApplication {

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

在 Controller 中加上

@Autowired
@Output(Source.OUTPUT)
private MessageChannel messageChannel;

@RequestMapping( method = RequestMethod.POST)
public void write(@RequestBody Reservation r){
    this.messageChannel.send(MessageBuilder.withPayload(r.getReservationName()).build());
}

修改 reservation-service

修改 build.gradle 把 spring-cloud-starter-stream-redis 加進來

build.gradle
dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-config')
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    /*
    compile('org.springframework.cloud:spring-cloud-starter-zipkin')
    */
    compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-data-rest')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('com.h2database:h2')
    testCompile('org.springframework.boot:spring-boot-starter-test') 
}

然後在主程式加上 @EnableBinding(Sink.class)

@EnableBinding(Sink.class)
@EnableDiscoveryClient
@SpringBootApplication
public class ReservationServiceApplication {
    
    //起動的時候預先塞測試資料

    @Bean
    CommandLineRunner runner(ReservationRepository rr){
        return args -> {
            Arrays.asList("Dr. rod,Dr. Syer,Juergen,ALL THE COMMUNITY,Josh".split(","))
            .forEach( x -> rr.save(new Reservation(x)));;
            rr.findAll().forEach( System.out::println);
        };
    }

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

再增加一個訊息接入點

@MessageEndpoint
class MessageReservationReceiver{
    @Autowired
    private ReservationRepository reservationRepository;
    
    @ServiceActivator(inputChannel = Sink.INPUT)
    public void acceptReservation(String rn){
        this.reservationRepository.save(new Reservation(rn));
    }
}

然後再回到 Config-Server 的設定檔資料夾加上 spring.redis.host ,因為是透過 redis 來收送所以當然是要給位置才能用

application.yml
spring.redis.host: "localhost"

這邊測試而已,還要裝個 redis 就太大費周章了,直接用 Docker 來跑吧

Vagrantfile.proxy
VAGRANTFILE_API_VERSION = "2"
 
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|  
  config.vm.box = "ubuntu/trusty64"
  config.vm.provision "docker"
  config.vm.provision "shell", inline:
    "ps aux | grep 'sshd:' | awk '{print $2}' | xargs kill"
  
  config.vm.provider :virtualbox do |vb|
    vb.name = "redis"
    vb.gui = $vm_gui
    vb.memory = $vm_memory
    vb.cpus = $vm_cpus
  end
 
  config.vm.network :forwarded_port, guest: 6379, host: 6379
  
  config.ssh.username = "vagrant" 
  config.ssh.password = "vagrant" 
end
Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

# Specify Vagrant version and Vagrant API version
#Vagrant.require_version ">= 1.6.0"
VAGRANTFILE_API_VERSION = "2"
ENV['VAGRANT_DEFAULT_PROVIDER'] = 'docker'

$vm_gui = false
$vm_memory = 2048
$vm_cpus = 2

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.synced_folder ".", "/vagrant", disabled: true
  
  config.vm.define "redis" do |v|
    v.vm.provider "docker" do |d|
      d.name = "redis"
      d.image = "redis"
      d.ports = ["6379:6379"]
      d.vagrant_vagrantfile = "./Vagrantfile.proxy"
    end
  end
end

兩個檔案放同一個資料夾後接著執行

vagrant up redis --provider=docker

好啦,程式跟環境都好了,接著把程式都叫起來,接著透過 POST 新增資料從 reservation-client -> redis -> reservation-service 寫入資料庫

curl -X POST -H "Accept: application/json" -H "Content-Type: application/json" -d '{"reservationName":"Red Johnson"}' 'http://localhost:8050/reservations'

再重新查看 http://localhost:8050/reservations/names 就可以看到多了 Red Johnson 的名字

為什麼要這樣做?我覺得是為了突發量,有時就算你的應用可以水平拓展,但是來不及拓也是GG...

增加服務中斷時的回覆訊息

有時候還是會有意外,但是出現問題如果跑出奇怪的錯有可能讓前端措手不及,或是稍微偽裝一下,可以讓客戶端無感異常

修改 reservation-client 專案

起動程式增加 @EnableCircuitBreaker ,然後在需要此功能的方法上增加 @HystrixCommand(fallbackMethod = "getReservationNamesFallback") 當失敗時他就會使用你指定的方法 getReservationNamesFallback 來回覆前端

@EnableZuulProxy
@EnableBinding(Source.class)
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class ReservationClientApplication {

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

@RestController
@RequestMapping("/reservations")
class ReservationApiGatewayRestController{

    @Autowired
    @LoadBalanced
    private RestTemplate restTemplate;
    
    @Autowired
    @Output(Source.OUTPUT)
    private MessageChannel messageChannel;
    
    @RequestMapping( method = RequestMethod.POST)
    public void write(@RequestBody Reservation r){
        this.messageChannel.send(MessageBuilder.withPayload(r.getReservationName()).build());
    }
    
    public Collection<String> getReservationNamesFallback(){
        return Collections.emptyList();
    }
    
    @HystrixCommand(fallbackMethod = "getReservationNamesFallback")
    @RequestMapping("names")
    public Collection<String> getReservationNames(){

        ParameterizedTypeReference<Resources<Reservation>> ptr = 
                new ParameterizedTypeReference<Resources<Reservation>>(){};

                ResponseEntity<Resources<Reservation>> responseEntity = 
                        this.restTemplate.exchange("http://reservation-service/reservations", 
                                HttpMethod.GET, null, ptr);
                
                Collection<String> nameList = responseEntity
                        .getBody()
                        .getContent()
                        .stream()
                        .map(Reservation::getReservationName)
                        .collect(Collectors.toList());

                return nameList;
    }
}

reservation-client 起動後,把 reservation-service 關掉,這麼一來通常應用程式就會發生異常回傳 500 之類的,但是你可以試著呼叫 http://localhost:8050/reservations/names,你可以發現你只是得到一個空集合,不影響你的前端程式

[]

即時監控

建立一個 hystrix-dashboard 專案

使用到的組件如下

Cloud Config Cloud Discovery Cloud Circuit Breaker
Config Client Eureka Discovery Hystrix Dashboard

一樣移除 application.properties,因為主要設定我們現在都依靠 Config-Server 的提供,再新增bootstrap.properties

bootstrap.properties
spring.application.name=hystrix-dashboard

spring.cloud.config.uri=http://localhost:8888

然後主程式啟用 @EnableHystrixDashboard

HystrixDashboardApplication.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

@EnableHystrixDashboard
@SpringBootApplication
public class HystrixDashboardApplication {

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

然後在 Config-Server 設定檔資料夾中新增

hystrix-dashboard.properties
server.port=${PORT:8010}

然後起動 hystrix-dashboard ,接著訪問

http://localhost:8050/hystrix.stream
你可以看到我們 reservation-client 一直在吐資料

然後把上面網址貼在下面網頁中間欄位
http://localhost:8010/hystrix.html

然後按下 Monitor Stream 按鈕,你就可以看到一個監控的介面
當後端執行成功或是失敗你都可以即時的發現到


Log 收集

有時知道錯在哪一個環節,但是沒有記錄還是很難找問題, spring-cloud-starter-zipkin 就可以記錄每一個 service 之間的資料傳遞,官網 https://twitter.github.io/zipkin/Quickstart.html

這東西 twitter 做的,裝起來應該也是很煩人,改天再試看看,再研究一下怎麼用 Docker 頂一下
https://github.com/joshlong/cloud-native-workshop/blob/master/bin/zipkin/docker-compose.yml

先說程式部分
把 reservation-service 、 reservation-client 原先註解掉的依賴 spring-cloud-starter-zipkin 加回去
然後這兩個專案內註冊 @Bean ,程式端就完成了

@Bean
AlwaysSampler alwaysSampler(){
    return new AlwaysSampler();
}

zipkin 這東西再研究看看吧。

待補

https://github.com/openzipkin/zipkin


References:
Getting started with Spring Cloud by Josh Long

This lab is references YouTube Getting Started with Spring Cloud, Thank you Dr. Dave Syer, Josh Long.

Dive into Eureka – nobodyiam's blog

← Android內WebView除錯 easy use JQuery fileDownload →
 
comments powered by Disqus