Google Code Prettify

2021年2月15日 星期一

consul: getting started

2020 年底,Spring Cloud 正式以 consul 取代 Eureka,成為 Spring Cloud 微服務架構中,服務註冊、查詢的元件。關於 consul 和 Eureka 的優劣,可以透過 google 得到不少訊息,在這裡就不多說,這裡整理的,主要是從程式開發的角度看, 當改用 consul,要怎麼開發、測試?

Spring Cloud 要部署的元件不少,我把它們部署在 Windows 10 Docker Desktop 裡的 Kubernetes 上,下面整理的資料會有一些 K8s 部署需要的 yaml 檔。

1. 部署 consul

為了讓 Kubernetes 外可以 consul 存取,要有一個 service,內容如下:

apiVersion: v1
kind: Service
metadata:
  name: consul-svc
  labels:
    app: consul
spec:
  type: LoadBalancer
  ports:
    - name: port8301
      protocol: TCP
      port: 8301
      targetPort: 8301
    - name: port8302
      protocol: TCP
      port: 8302
      targetPort: 8302
    - name: port8300
      protocol: TCP
      port: 8300
      targetPort: 8300
    - name: ui
      port: 8500
      targetPort: 8500
    - name: port8600
      protocol: TCP
      port: 8600
      targetPort: 8600
  selector:
    app: consul

因為是 Docker Desktop 單節點的 Kubernetes,EXTERNAL-IP 會是 localhost,從本機透過 8500 即可存取。接著要部署 consul,官網建議部署 consul 最好是 3 個或 5 個成為 cluster,這裡單純是要測試用,只部署一個,因為是成為各服務註冊的元件,部署時選擇 StatefulSet,yaml 檔如下。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: consul
spec:
  serviceName: consul-svc
  podManagementPolicy: "Parallel"
  replicas: 1
  template: 
    metadata:
      labels:
        app: consul
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: consul
        image: consul:latest
        args:
             - "agent"
             - "-server"
             - "-bootstrap-expect=1"
             - "-ui"
             - "-data-dir=/consul/data"
             - "-bind=0.0.0.0"
             - "-client=0.0.0.0"
             - "-advertise=$(PODIP)"
        env:
            - name: PODIP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
        ports:
            - containerPort: 8500
              name: ui-port
            - containerPort: 8400
              name: alt-port
            - containerPort: 53
              name: udp-port
            - containerPort: 8443
              name: https-port
            - containerPort: 8080
              name: http-port
            - containerPort: 8301
              name: serflan
            - containerPort: 8302
              name: serfwan
            - containerPort: 8600
              name: consuldns
            - containerPort: 8300
              name: server
  selector:
    matchLabels:
      app: consul

部署好之後,以 kubectl get statefulset 檢查看看有沒有成功,應該可以看到如下的結果。



打開瀏覽器輸入 http://localhost:8500,應該可以看到如下內容,consul 正常運作。


2. 註冊一個服務到 consul

假設有個財富管理系統,命名為 wealth-system,裡面提供了一個服務 /customer/{idno},可以透過身分證字號查詢客戶基本資料,要如何註冊到 consul ? 首先在 build.gradle 中引入相關的 jar 檔,在 dependencies 中加入如下內容:

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-config'

接著在 applicatoin.yml 中設定如下內容:

management:
  server:
    base-path: /
    port: 5001
  endpoint:
    health:
      probes:
        enabled: true
      show-details: always

spring:
  application:
    name: wealth-system
  profiles:
    active: ${SPRING_PROFILES_ACTIVE}
  main:
    banner-mode: off
  cloud:
    consul:
      enabled: true
      host: consul-svc
      port: 8500
      discovery:
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${random.value}
        service-name: ${spring.application.name}
        register-health-check: true
        health-check-path: /actuator/health/liveness
        health-check-interval: 60s

  • 第 1 ~ 9 行,設定 management,這是設定監控工具 Actuator 傾聽 5001 port,其中第 6 ~ 8 行指出要提供 health check 所需要的探針, 第 28 行即是指出探針的網址。
  • 同一個服務部署到 consul,會有好幾個 instance,以避免一個 instance 掛了,就無法提供服務,所以第 25 行指出 instance 的名稱,並且後面以亂數來表示,這樣每個 instance 的 intance id 自然就會不同。
  • 第 21、22 行,指出 consul 所在的 IP 和 port。

程式中怎麼啟用 consul? 在 @SpringBootApplication 所在的 class 上加入 @EnableDiscoveryClient。

Controller 程式這裡就不多做說明,基本上就是要提供一個網址為 /customer/{idno} 的服務,可以透過身分證字號查詢客戶基本資料。

compile 好之後,要 push 到 docker hub,方法如下:

(1) 製作 image

Dockerfile 內容如下:

FROM openjdk:11
ADD wealth-system.jar wealth-system.jar
ENV SPRING_PROFILES_ACTIVE sit
EXPOSE 80/tcp
EXPOSE 5001/tcp
ENTRYPOINT ["java","-jar","/wealth-system.jar"]

第 3 行傳入的參數,代表這是一個 sit 環境。第 4、5 行則是對外開放的 port。

在 Dockerfile 所在目錄下指令將 image push 到 docker hub,我在 docker hub 有一個帳號是 twleader 所以第 1 行產生 image 時指定的 image 名稱前有 twleader/。
docker build -t twleader/wealth-system .
docker push

(2) 部署 image 到 Kubernetes

consul 需要固定的 IP,採用 StatefulSet 方式部署,一般的服務每次產生時 IP 可以不一樣,採用 Deployment 方式部署,用這種方式部署會先產生 ReplicaSet 再產 Pod,yaml 檔內容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wealth 
spec:
  replicas: 2
  selector:
    matchLabels:
      app: wealth
  template:
    metadata:
      name: wealth
      labels:
        app: wealth 
    spec:
      containers:
      - image: twleader/wealth-system:latest
        name: wealth
        ports:
        - containerPort: 80
          name: api-port
        - containerPort: 5001
          name: management-port
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "sit"
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        volumeMounts:
        - name: my-pvc
          mountPath: /mnt/logs
      volumes:
      - name: my-pvc
        persistentVolumeClaim:
          claimName: my-pvc
  • 第 6 行指出會產生兩個 Pod
  • 第 31~37 行,這支程式產生的 log 會放在 /mnt/logs 目錄下,透過這樣的設定,可以讓 log 不會因為 Pod 消失而不見 (會儲存到 PV 所指定的地方)。
部署之後,應該可以在 consul ui 看到如下的內容,一開始可能會是紅色的,需過一段時間,讓 consul 透過 health check 確認服務已經 ready,才會轉為綠色。

3. client

上面已經將服務註冊到 consul 了,那麼其它程式或服務要怎麼存取這些服務呢?

(1) build.gradle

在 dependencies 中加入如下的依賴:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'

(2) application.yml

management:
  server:
    base-path: /
    port: 15001
  endpoint:
    health:
      probes:
        enabled: true
      show-details: always

spring:
  application:
    name: consul-client
  profiles:
    active: ${SPRING_PROFILES_ACTIVE}
  main:
    banner-mode: off
  cloud:
    consul:
      enabled: true
      host: consul-svc
      port: 8500
      discovery:
        instance-id: ${spring.application.name}:${random.value}
        service-name: ${spring.application.name}
        register-health-check: true
        health-check-path: /actuator/health/liveness
        health-check-interval: 60s

(3) RestTemplate

@Configuration
public class RestConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
第 4 行的 @LoadBalanced 很重要! 這樣子才可以透過 consul 上註冊的服務名稱存取到服務。

(4) Controller

@RestController
@RequestMapping("/wealth")
@Slf4j
public class WealthController {
    @Autowired
    RestTemplate rest;
	
    @GetMapping("/customer/{idno}")
    public ResponseEntity<?> findCustomerByIdno(@PathVariable String idno) {
		
        log.info("idno = " + idno);
		
        try {
	    ResponseEntity<CustomerModel> response = rest.getForEntity("http://wealth-system/customer/" + idno, CustomerModel.class);
	    CustomerModel customer = response.getBody();
			
	    log.info(customer.toString());
		
	    return new ResponseEntity<CustomerModel>(customer, HttpStatus.OK);
	}
	catch (Exception e) {
	    log.error(e.getMessage(), e);
		
	    return new ResponseEntity<Exception>(e, HttpStatus.INTERNAL_SERVER_ERROR);
	}
    }
}
  • 看第 5、6 行,RestTemplate 不能直接 new,需要用 @Autowired (註)。 
  • 第 14 行,要對一個服務 request 時,不需要指出該服務的網址或IP,而是直接對服務名稱 wealth-system。

(5) 產生 image

Dockerfile 內容如下:
FROM openjdk:11
ADD consul-client.jar consul-client.jar
ENV SPRING_PROFILES_ACTIVE sit
EXPOSE 18080/tcp
EXPOSE 15001/tcp
ENTRYPOINT ["java","-jar","/consul-client.jar"]
於 Dockerfile 所在目錄下底下的指令:
docker build -t twleader/consul-client .
docker push twleader/consul-client

(6) 部署到 Kubernetes

先為 client 建立一個 service,方便測試,yaml 檔如下:
apiVersion: v1
kind: Service
metadata:
  name: consul-client-svc
spec:
  type: LoadBalancer
  ports:
  - name: consul-client
    port: 18080
    targetPort: 18080
  - name: consul-client-management
    port: 15001
    targetPort: 15001
  selector:
    app: consul-client
接著當然要部署 POD, 使用如下的 deployment yaml file:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: consul-client
spec:
  replicas: 2
  selector:
    matchLabels:
      app: consul-client
  template:
    metadata:
      name: consul-client
      labels:
        app: consul-client 
    spec:
      containers:
      - image: twleader/consul-client:latest
        name: consul-client
        ports:
        - containerPort: 18080
          name: api-port
        - containerPort: 15001
          name: management-port
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "sit"
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        volumeMounts:
        - name: my-pvc
          mountPath: /mnt/logs
      volumes:
      - name: my-pvc
        persistentVolumeClaim:
          claimName: my-pvc

一切部署妥當,應該可以在 consul ui 中看到如下內容,這次要使用到的服務都已經在上面了!


打開瀏覽器,輸入 http://localhost:18080/wealth/customer/E243350588 可以得到傳回的 JSON format 資料如下:
{"idno":"E243350588","name":"Nancy","phoneNo":"0909000911","email":"nancy@yahoo.com"}

沒有留言:

張貼留言