一、Spring Cloud Kubernetes
随着云计算和微服务架构的不断发展,k8s
和Spring Cloud
成为了当今技术领域的两大热门话题。k8s
作为一个开源的容器编排平台,已经在自动化部署、扩展和管理方面取得了巨大的成功,而Spring Cloud
则以其丰富的生态和语言亲和性,为Java
开发者提供了便利的微服务框架支持。在现如今的技术趋势下,两者融合已经成为了必要的方向。
首先,k8s
的火热程度不言而喻。它不仅支持多种编程语言和框架,还提供了强大的自动化部署、扩展和管理能力,使得开发者可以根据自己的需求进行定制。此外,k8s
的强大自动化能力也为开发、测试和运维团队带来了便利。
而Spring Cloud
作为微服务框架,提供了丰富的组件和工具,如服务注册与发现、负载均衡、熔断器等,可以帮助开发者快速构建微服务应用。并且针对Java
开发者,提供了许多开箱即用的功能。然而,Spring Cloud
主要适用于Java
语言,对其他语言的支持有限,且在某些情况下可能显得有些重量级。
在面对这样的技术选择时,将k8s
和Spring Cloud
融合使用是一个理想的选择。充分发挥它们各自的优势,构建更加稳定和高效的微服务架构。利用k8s
进行容器的编排和部署,实现自动化管理。例如,通过k8s
的Service
机制,可以实现服务发现和负载均衡;通过k8s
的ConfigMap
和Secrets
,可以实现配置管理;通过k8s
的Ingress
或 Gateway
,可以实现智能路由。
在如此背景下 Sping Cloud
提供了 Spring Cloud Kubernetes
子项目,旨在将Spring Cloud
和k8s
结合起来。Spring Cloud Kubernetes
在保持原有开发模式的同时,几乎无感的做到对接 k8s
,使得在k8s
环境中运行Spring Cloud
应用程序变得更加容易和高效。例如:允许应用程序自动发现k8s
集群中的其他服务,从而使得服务间通信更加便捷,更加高效实现服务发现与注册。还支持从k8s
的ConfigMap
或Secrets
中读取配置信息,并自动更新应用程序中的配置属性。
官方文档如下:
https://docs.spring.io/spring-cloud-kubernetes/reference/3.1-SNAPSHOT/index.html
下面使用 Spring Cloud Kubernetes
搭建两个 SpringBoot
服务,分别作为服务提供者、服务消费者实现服务的注册和发现,并且在消费者端使用 ConfigMap
作为动态配置。
角色 | replicas | 类型 | 动态配置 |
---|---|---|---|
provider | 2 | Deployment | consumerconfig |
consumer | 2 | Deployment |
新建一个 Maven
项目作为父组件,在该项目中再创建两个SpringBoot Model
。
首先在父 pom
中增加依赖,声明SpringBoot
、 StringCloud
的版本:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>nacos-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>service-parent</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>
<modules>
<module>provider-service</module>
<module>consumer-service</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
二、服务提供者搭建
新建 SpringBoot model
,修改 pom
内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>provider-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>provider-service</name>
<description>provider-service</description>
<parent>
<groupId>org.example</groupId>
<artifactId>nacos-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.example.providerservice.ProviderServiceApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
修改 application.yml
:
server:
port: 8091
spring:
application:
name: provider
启动类增加 @EnableDiscoveryClient
注解:
@EnableDiscoveryClient
@SpringBootApplication
public class ProviderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderServiceApplication.class, args);
}
}
创建一个测试接口,后面在服务消费端通过 OpenFeign
调用:
@RestController
public class ProviderController {
/**
* 提供服务接口
*/
@GetMapping("/rpc")
public String rpc() {
return "provider from : " + getCurrentHostName();
}
/**
* 获取当前系统名称
*/
private String getCurrentHostName(){
try {
InetAddress inetAddress = InetAddress.getLocalHost();
return inetAddress.getHostName();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return null;
}
}
建立 Dockerfile
文件,内容如下:
FROM java:8
MAINTAINER bxc
WORKDIR /app
ADD target/provider-service-0.0.1-SNAPSHOT.jar /app/app.jar
CMD ["java", "-jar", "app.jar"]
三、服务消费者搭建
消费者端通过 OpenFeign
调用服务提供者端的接口信息,并使用 ConfigMap
动态配置。
新建 SpringBoot model
,修改 pom
内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>consumer-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>consumer-service</name>
<description>consumer-service</description>
<parent>
<groupId>org.example</groupId>
<artifactId>nacos-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-config</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.example.consumerservice.ConsumerServiceApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
修改配置文件,注意不要写在application.yml
中,新建一个bootstrap.yml
文件,用来在程序引导时执行:
server:
port: 8080
spring:
application:
name: consumer
cloud:
kubernetes:
config:
name: ${CONFIG_NAME:consumerconfig}
namespace: ${NAMESPACE:cloud}
reload:
# 是否自动更新
enabled: true
# 监控configMap变化
monitoring-config-maps: true
# 监控secrets变化
monitoring-secrets: true
# 加载策略
# refresh:只重新加载用@ConfigurationProperties或@RefreshScope注释的配置bean。此重新加载级别利用了Spring Cloud Context的刷新功能。
# restart_context:整个Spring ApplicationContext被优雅地重新启动。使用新配置重新创建bean。为了使重启上下文功能正常工作,您必须启用并公开restart端点
# shutdown:关闭Spring ApplicationContext以激活容器的重新启动。使用此级别时,请确保所有非守护进程线程的生命周期都绑定到ApplicationContext,并且已配置复制控制器或副本集以重新启动pod。
strategy: restart_context
#更新配置信息的模式:polling:主动拉取,event:事件通知
mode: event
#如果主动拉取,拉取的时间间隔,单位毫秒
#period: 500
management:
endpoint:
restart:
enabled: true
endpoints:
web:
exposure:
include: restart # 如果 strategy 是 restart_context ,这个端点需要开启
这里配置文件的名称和命名空间通过环境变量动态传递进来。
启动类增加 @EnableDiscoveryClient
和 @EnableFeignClients
注解:
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceApplication.class, args);
}
}
使用 FeignClient
声明服务提供者的接口:
@Component
@FeignClient(value = "provider")
public interface ProviderClient {
@GetMapping("/rpc")
String rpc();
}
创建动态配置类:
@Component
@ConfigurationProperties(prefix = "param")
@Data
@RefreshScope
public class ParamProperties {
private String text;
private String name;
}
创建测试接口,分别测试 discovery
、rpc
调用、动态配置:
@RestController
@RefreshScope
public class ConsumerController {
@Resource
ProviderClient providerClient;
@Value("${param.param1:tmp}")
private String param;
@Resource
private DiscoveryClient discoveryClient;
@Resource
ParamProperties paramProperties;
/**
* 查看 Discovery 信息
*/
@GetMapping("/discoveryInfo")
public Map<String, List<ServiceInstance>> getDiscoveryInfo() {
Map<String, List<ServiceInstance>> result = new HashMap<>();
discoveryClient.getServices().forEach(service -> {
List<ServiceInstance> instances = discoveryClient.getInstances(service);
result.put(service,instances);
});
return result;
}
/**
* 测试RPC调用
*/
@GetMapping("/rpc")
public String rpc() {
String providerData = providerClient.rpc();
return "sunncess: consumer from : " + getCurrentHostName() + " ; " + providerData;
}
/**
* 测试动态配置
*/
@GetMapping("/config")
public String getConfig() {
return "success: current config : " + param+": "+paramProperties.toString();
}
/**
* 获取当前系统名称
*/
private String getCurrentHostName() {
try {
InetAddress inetAddress = InetAddress.getLocalHost();
return inetAddress.getHostName();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return null;
}
}
建立 Dockerfile
文件,内容如下:
FROM java:8
MAINTAINER bxc
WORKDIR /app
ADD target/consumer-service-0.0.1-SNAPSHOT.jar /app/app.jar
CMD ["java", "-jar", "app.jar"]
四、部署到 K8s
4.1 打包上传镜像
这里我将镜像打包后上传至私有的 harbor
仓库中,如果没有 harbor
仓库也可以将镜像上传至每个 k8s node
节点中,然后使用 docker load
到本地镜像仓库中。
# 打包成 jar 包
mvn clean package
# 构建镜像
cd consumer-service
docker build -t consumer-service:1.0 .
# 上传至 harbor
docker tag consumer-service:1.0 11.0.1.150/image/consumer-service:1.0 .
docker push 11.0.1.150/image/consumer-service:1.0
# 构建镜像
cd ../provider-service
docker build -t provider-service:1.0 .
# 上传至 harbor
docker tag provider-service:1.0 11.0.1.150/image/provider-service:1.0 .
docker push 11.0.1.150/image/provider-service:1.0
4.2 k8s 部署服务
首先创建 cloud
命名空间,后续所有服务都建立在该命名空间下:
kubectl create ns cloud
由于默认的 ServiceAccount
是没有权限访问K8S
的API Server
资源的,直接启动服务读取 ConfigMap
会报错 Forbidden!Configured service account doesn't have access.
,所以在启动服务前,先创建一个分配权限的 ServiceAccount
,但是需要注意:由于指定了 ServiceAccount
,该 ServiceAccount
同时也需要给 Services
的权限,不然 discoveryInfo
读取不到 Service
的信息:
vi role.yml
apiVersion: v1
kind: ServiceAccount
metadata:
name: config-reader
namespace: cloud
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: cloud
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods","configmaps","secrets","endpoints","services"]
verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: config-reader-rolebinding
namespace: cloud
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: pod-reader
namespace: cloud
subjects:
- kind: ServiceAccount
name: config-reader
namespace: cloud
kubectl apply -f role.yml
查看:
下面部署服务,注意:服务需要指定上面创建的 ServiceAccount
,不然没有权限获取 ConfigMap
配置以及 Service
信息。
vi cloud.yml
apiVersion: v1
kind: Service
metadata:
name: consumer
namespace: cloud
labels:
app: consumer
spec:
type: NodePort
ports:
- port: 8080
name: client
nodePort: 31880
targetPort: 8080
selector:
app: consumer
---
kind: ConfigMap
apiVersion: v1
metadata:
name: consumerconfig
namespace: cloud
data:
application.yml: |-
param:
param1: abc
text: textabc
name: nameabc
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: consumer
namespace: cloud
spec:
replicas: 2
selector:
matchLabels:
app: consumer
template:
metadata:
labels:
app: consumer
spec:
serviceAccountName: config-reader
containers:
- name: consumer
image: consumer-service:1.0
ports:
- containerPort: 8080
name: server
env:
- name: CONFIG_NAME
value: consumerconfig
- name: NAMESPACE
value: cloud
---
apiVersion: v1
kind: Service
metadata:
name: provider
namespace: cloud
labels:
app: provider-svc
spec:
type: ClusterIP
ports:
- port: 8091
targetPort: 8091
selector:
app: provider
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: provider
namespace: cloud
spec:
replicas: 2
selector:
matchLabels:
app: provider
template:
metadata:
labels:
app: provider
spec:
serviceAccountName: config-reader
containers:
- name: provider
image: provider-service:1.0
ports:
- containerPort: 8091
name: server
注意这里配置信息 ConfigMap
的名字为 consumerconfig
,需要传递到服务消费者服务中,并且记住此时的配置内容:
启动服务:
kubectl apply -f cloud.yml
查看 Pod
:
kubectl get pods -n cloud
都处于 Running
状态,查看其中一个的启动日志,看是否正常启动:
kubectl logs consumer-77c674cc96-l58jv -n cloud
服务启动正常。
五、测试
5.1 服务注册发现测试
调用 http:{node ip}:31880/discoveryInfo
查看服务信息:
结果完整信息如下:
{
"provider": [
{
"instanceId": "47d501db-646c-401a-9d84-63ef5909cd10",
"serviceId": "provider",
"host": "10.233.111.243",
"port": 8091,
"uri": "http://10.233.111.243:8091",
"secure": false,
"metadata": {
"app": "provider-svc",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"provider-svc\"},\"name\":\"provider\",\"namespace\":\"cloud\"},\"spec\":{\"ports\":[{\"port\":8091,\"targetPort\":8091}],\"selector\":{\"app\":\"provider\"},\"type\":\"ClusterIP\"}}\n"
},
"namespace": null,
"scheme": "http"
},
{
"instanceId": "493dad3f-76b4-46a2-b882-3607ac3751f4",
"serviceId": "provider",
"host": "10.233.111.244",
"port": 8091,
"uri": "http://10.233.111.244:8091",
"secure": false,
"metadata": {
"app": "provider-svc",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"provider-svc\"},\"name\":\"provider\",\"namespace\":\"cloud\"},\"spec\":{\"ports\":[{\"port\":8091,\"targetPort\":8091}],\"selector\":{\"app\":\"provider\"},\"type\":\"ClusterIP\"}}\n"
},
"namespace": null,
"scheme": "http"
}
],
"consumer": [
{
"instanceId": "a21109cd-ca86-4b0d-a9ca-b8c0efa336c8",
"serviceId": "consumer",
"host": "10.233.111.245",
"port": 8080,
"uri": "http://10.233.111.245:8080",
"secure": false,
"metadata": {
"app": "consumer",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"consumer\"},\"name\":\"consumer\",\"namespace\":\"cloud\"},\"spec\":{\"ports\":[{\"name\":\"client\",\"nodePort\":31880,\"port\":8080,\"targetPort\":8080}],\"selector\":{\"app\":\"consumer\"},\"type\":\"NodePort\"}}\n",
"port.client": "8080"
},
"namespace": null,
"scheme": "http"
},
{
"instanceId": "591e43a8-6d30-4395-915e-570275ee0336",
"serviceId": "consumer",
"host": "10.233.111.246",
"port": 8080,
"uri": "http://10.233.111.246:8080",
"secure": false,
"metadata": {
"app": "consumer",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"consumer\"},\"name\":\"consumer\",\"namespace\":\"cloud\"},\"spec\":{\"ports\":[{\"name\":\"client\",\"nodePort\":31880,\"port\":8080,\"targetPort\":8080}],\"selector\":{\"app\":\"consumer\"},\"type\":\"NodePort\"}}\n",
"port.client": "8080"
},
"namespace": null,
"scheme": "http"
}
]
}
可以看到服务提供者和服务消费的基本信息都获取到了,其中的 ip
地址就是 pod
的地址:
kubectl get pods -n cloud -o wide
调用 http:{node ip}:31880/rpc
测试 RPC
远程调用,并且多次调用:
经过多次调用可以看到负载均衡效果。
5.2 服务动态配置测试
调用 http:{node ip}:31880/config
查看配置信息:
正常读取到 ConfigMap
中的配置信息,下面修改 ConfigMap
信息:
kubectl edit configmap consumerconfig -n cloud
修改信息如下:
然后保存后再次调用查看配置接口:
配置已生效。