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 檢查看看有沒有成功,應該可以看到如下的結果。
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 所指定的地方)。
3. client
(1) build.gradle
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 中看到如下內容,這次要使用到的服務都已經在上面了!
{"idno":"E243350588","name":"Nancy","phoneNo":"0909000911","email":"nancy@yahoo.com"}
沒有留言:
張貼留言