通常情況下,我們的線上的服務在遷移到k8s環境下的時候,都是采用平滑遷移的方案。服務治理與注冊中心等都是采用原先的組件。比如spring cloud應用,在k8s環境下還是用原來的一套注冊中心(如eureka),服務治理(hystrix,ribbon)等。但是當我們開發新的應用時,我們是可以借助於sping-cloud-kubernetes組件為我們提供的服務發現、負載均衡等來擯棄像eureka這樣的注冊中心。本文主要通過構建兩個spring cloud 服務來演示spring-cloud-kubernetes組件如何做服務的發現,負載均衡等。
一、使用spring-cloud-kubernetes做服務發現
分別開發兩個服務,服務提供者:product_infra_service, 並通過FeignClient的方式對外提供接口,供消費者調用。服務消費者:product_infra_consumer.
首先新建一個module:product-infra-service-api:該項目中配置pom文件的相關依賴如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--Service Interface API--> <!--Feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.1.3.RELEASE</version> </dependency> </dependencies>
該項目中定義接口如下:
@FeignClient(name="${feign.product-infra.name:product-infra-service}",fallback = PropertyClientFallback.class)
public interface PropertyClient { @GetMapping(value="properties") List<String> getProperties(); }
hystrix的fallback類定義如下:
@Component public class PropertyClientFallback implements PropertyClient{ @Override public List<String> getProperties() { return new ArrayList<String>(); } }
接下來,新建服務提供者 product-infra-service項目,在該項目中實現PropertyClient接口。pom文件依賴配置如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <!--Service Interface API--> <dependency> <groupId>com.maidao.center.product_infra</groupId> <artifactId>product-infra-service-api</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--SpringBoot Actuator--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>
定義controller 類如下:
@RestController @RequestMapping public class PropertyController implements PropertyClient{ @GetMapping("properties") public List<String> getProperties(){ ArrayList<String> properties = new ArrayList<>(); properties.add("properties1"); properties.add("properties2"); return properties; } }
編譯打包成鏡像后將項目部署到k8s環境,並在k8s環境下定義服務pod實例所關聯到的service:本例中定義k8s service name 為 product-infra-service(與定義的應用名相同).
新建消費者項目:product-infra-consumer,pom文件配置依賴如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-core</artifactId> <version>${springcloud.kubernetes.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-discovery</artifactId> <version>${springcloud.kubernetes.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId> <version>${springcloud.kubernetes.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-commons</artifactId> <version>${springcloud.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${springcloud.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> <version>${springcloud.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> <version>${springcloud.version}</version> </dependency> <!--Service Interface API--> <dependency> <groupId>com.maidao.center.product_infra</groupId> <artifactId>product-infra-service-api</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> </dependencies>
應用配置如下:
server:
port: 8080 product-infra-service: ribbon: KubernetesNamespace: ${namespace} (k8s namespace,實際根據自己服務部署的namespace配置對應的名字) backend: ribbon: eureka: enabled: false client: enabled: true ServerListRefreshInterval: 5000 hystrix: command: BackendCall: execution: isolation: thread: timeoutInMilliseconds: 5000 threadpool: BackendCallThread: coreSize: 5 feign: hystrix: enabled: true
新建ProductInfraConsumerController類:通過PropertyClient的引用調用提供者提供的接口。
@RestController
@RequestMapping
public class ProductInfraConcumerController {
@Autowired
private PropertyClient propertyClient;
@GetMapping("properties")
public List<String> getProductProperties(){
return propertyClient.getProperties();
}
}
可以看到代碼沒有變化,調用方式沒有變化,在消費者項目中去掉了原先的EurekaClient發現服務的調用。服務發現機制采用的是,spring-cloud-kubernetes-discovery。負載均衡組件采用的是:spring-cloud-starter-kubernetes-ribbon。
最后將product-infra-consumer編譯打包成鏡像后,部署到k8s環境下。
最后通過curl 命令在k8s環境下調用product-infra-consumer的controller方法來測試服務之間的通信:如下所示:
[root@iZbp174tf9563rykf3bbjjZ ~]# curl http://172.30.28.100:8080/properties
["properties1","properties2"]
停掉product-infra-service服務,再次發起請求,降級fallback方法生效:
[root@iZbp174tf9563rykf3bbjjZ ~]# curl http://172.30.28.98:8080/properties []
可以看到在沒有eureka注冊中心的情況下,定義的兩個服務能夠正常通信,且代碼不需要做任何變動,只需要在消費者服務中引入spring-cloud-kubernetes相關依賴即可,spring-cloud-kubernetes組件起到了服務發現以及負載均衡的作用。
二、spring-cloud-kubernetes 服務發現流程
spring-cloud-kubernetes框架提供了調用kubernetes的原生能力來為現有SpringCloud應用提供服務,架構如下圖所示:
從上圖可以看出product-infra-consumer在調用product-infra-service時,通過FeignClient組件拿到service name信息,最底層通過ok-http3,根據service name 調用 api server 獲取該service下對應的Pod信息,拿到Pod信息后通過,輪詢的方式向這些pod發送請求。spring-cloud-starter-kubernetes-ribbon組件中的KubernetesServerList 繼承了 ribbon-loaderbanlancer組件中的AbstractServerList以及實現了 ServerList類中的方法,並通過 KubernetesClient提供的能力向k8s api server 發送請求信息。通過服務名獲取pod信息的源碼如下:
public List<Server> getUpdatedListOfServers() { Endpoints endpoints = this.namespace != null ? this.client.endpoints().inNamespace(this.namespace) .withName(this.serviceId).get() : this.client.endpoints().withName(this.serviceId).get(); List<Server> result = new ArrayList<Server>(); if (endpoints != null) { if (LOG.isDebugEnabled()) { LOG.debug("Found [" + endpoints.getSubsets().size() + "] endpoints in namespace [" + this.namespace + "] for name [" + this.serviceId + "] and portName [" + this.portName + "]"); } for (EndpointSubset subset : endpoints.getSubsets()) { if (subset.getPorts().size() == 1) { EndpointPort port = subset.getPorts().get(FIRST); for (EndpointAddress address : subset.getAddresses()) { result.add(new Server(address.getIp(), port.getPort())); } } else { for (EndpointPort port : subset.getPorts()) { if (Utils.isNullOrEmpty(this.portName) || this.portName.endsWith(port.getName())) { for (EndpointAddress address : subset.getAddresses()) { result.add(new Server(address.getIp(), port.getPort())); } } } } } } else { LOG.warn("Did not find any endpoints in ribbon in namespace [" + this.namespace + "] for name [" + this.serviceId + "] and portName [" + this.portName + "]"); } return result; }