阅读完需:约 25 分钟
K8S
Kubernetes是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes的目标是让部署容器化的应用简单并且高效(powerful),Kubernetes提供了应用部署,规划,更新,维护的一种机制。
Kubernetes一个核心的特点就是能够自主的管理容器来保证云平台中的容器按照用户的期望状态运行着(比如用户想让apache一直运行,用户不需要关心怎么去做,Kubernetes会自动去监控,然后去重启,新建,总之,让apache一直提供服务),管理员可以加载一个微型服务,让规划器来找到合适的位置,同时,Kubernetes也系统提升工具以及人性化方面,让用户能够方便的部署自己的应用(就像canary deployments)。
Spring Cloud Kubernetes
Spring Cloud Kubernetes 提供了众所周知的 Spring Cloud 接口的实现,允许开发人员在 Kubernetes 上构建和运行 Spring Cloud 应用程序。虽然这个项目在构建云原生应用程序时可能对您有用,但在 Kubernetes 上部署 Spring Boot 应用程序也不是必需的。如果您刚刚开始在 Kubernetes 上运行 Spring Boot 应用程序,那么您只需一个基本的 Spring Boot 应用程序和 Kubernetes 本身就可以完成很多工作。
之前的spring cloud项目都是用的eurake作的注册中心,但是在项目部署到kubernetes中时如果想用k8s特有的功能,往往会达不到预期的效果
感觉spring cloud和kubernetes中有很多组件是类似的,比如spring cloud 中的eurake与k8s中etcd的类似,spring cloud 中zuul和gateway与k8s 中ingress或istio 的类似,spring cloud config与k8s configmap的类似等,对于许多类似的功能组件其实只用一个就行了,比如注册中心只需要用k8s的etcd就可以了,如果再用上eurake部署在k8s环境中就确实感觉有点没有必要
鉴于目前的部署环境都是kubernetes,为了不让组件重复,我决定将spring cloud项目改造成spring cloud kubernetes项目,为了方便,就以之前的项目spring boot cloud项目来改造
官网:https://docs.spring.io/spring-cloud-kubernetes/docs/2.1.3/reference/html/#why-do-you-need-spring-cloud-kubernetes
测试k8s版本是1.18
基本概念
在kubernetes
环境中,pod、service
这些资源的数据都存储在etcd
,任何服务想要增删改查etcd
的数据,都只能通过向API Server发起RestFul请求的方式来完成,咱们的DiscoveryController
类获取所有service也是发请求到API Server,由API Server从etcd中取得service的数据返回给DiscoveryController
。
服务发现和轮询
使用spring-cloud-kubernetes
框架,该框架可以调用kubernetes
的原生能力来为现有SpringCloud
应用提供服务,架构如下图所示:
上图表明,Web-Service
应用在调用Account-Service
应用的服务时,会用okhttp向API Server请求服务列表,API Server收到请求后会去etcd取数据返回给Web-Service应用,这样Web-Service就有了Account-Service的信息,可以向Account-Service的多个Pod轮询发起请求。之所以具有这个能力,是因为spring-cloud-kubernetes框架通过service拿到了Account-Service对应的所有Pod信息。
SpringCloud Gateway
原有的Gateway
在基于SpringCloud的微服务环境中,外部请求会到达SpringCloud Gateway应用,该应用对请求做转发、过滤、鉴权、熔断等前置操作。
和kubernetes
结合后,运行在kubernetes
环境的SpringCloud Gateway
应用,如果使用了spring-cloud-kubernetes
框架就能得到kubernetes的service列表,因此可以承担网关的角色,将外部请求转发至kubernetes内的service上,最终到达对应的Pod。
Spring Cloud Config
这是kubernetes提供的基本服务之一,创建一个configmap资源,对应着一份配置文件,可以将该资源通过数据卷的形式映射到Pod上,这样Pod就能用上这个配置文件了,如下图:
spring-cloud-starter-kubernetes-config
是spring-cloud-starter-kubernetes
框架下的一个库,作用是将kubernetes
的configmap
与SpringCloud Config
结合起来,通过spring-cloud-starter-kubernetes-config
,我们的应用就像在通过SpringCloud Config取得配置信息,只不过这里的配置信息来自kubernetes的configmap,而不是SpringCloud Config server。
去掉eurake注册中心
去掉eurake
注册中心,使用kubernetes
的etcd
来替换。当然不可能真的直接连kubernetes
的etcd
,而是用DiscoveryClient for Kubernetes
来替换
也就是直接将原来的eurake项目删掉,在以前的eurake客户端的项目中换成下面这个引用:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client</artifactId>
<version>2.1.2</version>
</dependency>
在网上有很多个版本的依赖大多都是1.X版本的,这里采用的是官方的依赖,这里并没有使用Fabric8
的依赖与Fabric8
的插件,因为这里我们可以手动的去打包部署服务,不需要Fabric8
插件。
这里值得注意的是要完全去掉eurake
的服务,包括了client与server服务
在项目中添加Controller
来测试获取K8S上的服务
@RestController
public class DiscoveryController {
@Autowired
private DiscoveryClient discoveryClient;
/**
* 探针检查响应类
* @return
*/
@RequestMapping("/health")
public String health() {
return "health";
}
/**
* 返回远程调用的结果
* @return
*/
@RequestMapping("/getservicedetail")
public String getservicedetail(
@RequestParam(value = "servicename", defaultValue = "") String servicename) {
return "Service [" + servicename + "]'s instance list : " + JSON.toJSONString(discoveryClient.getInstances(servicename));
}
/**
* 返回发现的所有服务
* @return
*/
@RequestMapping("/services")
public String services() {
return this.discoveryClient.getServices().toString()
+ ", "
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
}
以下所有的操作都是在同级目录下执行的
将项目通过DockerFile
打包
FROM openjdk:8-jre-alpine
ARG PREFIX=/opt/java
COPY /dist/. ${PREFIX}/
COPY /run.sh ${PREFIX}/
COPY /version ${PREFIX}/
RUN ls ${PREFIX}/
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add --no-cache bash
WORKDIR ${PREFIX}
CMD ["sh", "./run.sh"]
EXPOSE 40300
测试项目的目录结构
测试服务的dist是因为修改了Maven的打包方式,将lib包提取出来啦
<finalName>${project.artifactId}-${project.version}</finalName>
<resources>
<!--指定src/main/resources资源要 过滤-->
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<!-- maven资源文件复制插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy-resources</id>
<!-- here the phase you need -->
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/dist/</outputDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<exclude>**/*.xml</exclude>
<exclude>**/*.conf</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.yml</exclude>
<exclude>**/*.sh</exclude>
<exclude>**/*.keystore</exclude>
<exclude>**/*.txt</exclude>
<exclude>**/*.sql</exclude>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main</directory>
<includes>
<include>webapp/**</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
<encoding>UTF-8</encoding>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>keystore</nonFilteredFileExtension>
<nonFilteredFileExtension>cer</nonFilteredFileExtension>
<nonFilteredFileExtension>woff</nonFilteredFileExtension>
<nonFilteredFileExtension>eot</nonFilteredFileExtension>
<nonFilteredFileExtension>ttf</nonFilteredFileExtension>
<nonFilteredFileExtension>svg</nonFilteredFileExtension>
<nonFilteredFileExtension>xls</nonFilteredFileExtension>
<nonFilteredFileExtension>xlsx</nonFilteredFileExtension>
<nonFilteredFileExtension>key</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</execution>
</executions>
</plugin>
<!-- 依赖包插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/dist/lib</outputDirectory>
<!-- 是否不包含间接依赖 -->
<excludeTransitive>false</excludeTransitive>
<!-- 忽略版本 -->
<stripVersion>false</stripVersion>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<!--不打包依赖的jar,把依赖的jar copy到lib目录,和生成的jar放在同一级目录下-->
<manifestEntries>
<Class-Path>.</Class-Path>
</manifestEntries>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>${main.class}</mainClass>
</manifest>
</archive>
<excludes>
<exclude>**/*.xml</exclude>
<exclude>**/*.conf</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.yml</exclude>
<exclude>**/*.sh</exclude>
<exclude>**/*.keystore</exclude>
<exclude>**/*.txt</exclude>
<exclude>**/*.sql</exclude>
</excludes>
<outputDirectory>${project.build.directory}/dist</outputDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<skip>true</skip>
<skipTests>true</skipTests>
</configuration>
</plugin>
至于 run.sh
与 version
,只是为了启动服务
version
1.0.0
run.sh
# !/bin/sh
trap 'kill -TERM $PID' TERM INT
# Uncomment below lines if the SVC is the owner of DB
#echo "Running migrations ..."
#for i in $(seq 1 30); do
# ./migrations/bin/migrate up > /dev/null 2>&1
# [ $? = 0 ] && break
# echo "Reconnecting db $i ..." && sleep 1
#done
#
#[ $? != 0 ] && echo "Failed to connect db, aborted!" && sleep 1 && exit 1
if [ -f javaopt.conf ]; then
source javaopt.conf
fi
MSMEM=${MSMEM:-1024m}
MXMEM=${MXMEM:-1024m}
# Set it in javaopt.conf if needed
# CLASS=${ENTRY_CLASS:-com.linktopa.app.main}
export VERSION=`cat version`_$REV
echo "Starting service [$VERSION] ..."
java -javaagent:/opt/java/lib/core-x-2.0.3.jar ${JAVA_OPTS} -Xms$MSMEM -Xmx$MXMEM \
-Djava.security.egd=file:/dev/./urandom \
-Duser.timezone=GMT+8 \
-Dglobal.config.path=/opt/java \
-jar `ls /opt/java/*-*.jar` $CLASS
tail -f /dev/null &
PID=$!
# Now blocking ...
wait ${PID}
# Here a TERM/INT signal must be received
trap - TERM INT
echo "Service exited!"
执行Dockerfile
打包镜像
docker build -f Dockerfile -t vehcloud:1.0.0 .
编写kubernetes
部署文件
apiVersion: apps/v1 #kubectl api-versions 可以通过这条指令去看版本信息
kind: Deployment # 指定资源类别
metadata: #资源的一些元数据
name: ent-cloud-k8s #deloyment的名称
labels:
app: ent-cloud-k8s #标签
spec:
replicas: 1 #创建pod的个数
selector:
matchLabels:
app: ent-cloud-k8s #满足标签为这个的时候相关的pod才能被调度到
template:
metadata:
labels:
app: ent-cloud-k8s
spec:
nodeSelector:
kubernetes.io/hostname: "210node"
containers:
- name: ent-cloud-k8s
image: entcloud:1.0.1
imagePullPolicy: IfNotPresent #当本地有镜像的时候优先选用本地,没有才选择网络拉取
ports:
- containerPort: 40900 #开放8080
---
apiVersion: v1
kind: Service
metadata:
labels:
app: ent-cloud-k8s
name: ent-cloud-k8s
spec:
ports:
- name: "40900"
port: 40900
targetPort: 40900
type: NodePort
selector:
app: ent-cloud-k8s #满足标签为这个的时候相关的pod才能被调度到
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: service-reader
rules:
- apiGroups: [""]
resources: ["services", "endpoints", "pods"]
verbs: ["get", "list", "watch"]
执行K8S命令部署
kubectl create -f ./k8s-boot-app-deployment.yml
其中最为关键是对于用户角色的配置问题
部署完成后需要执行
kubectl create clusterrolebinding service-reader-pod --clusterrole=service-reader --serviceaccount=default:default
完成用户角色的权限
这里和官方有点不同需要注意一下
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: YOUR-NAME-SPACE
name: namespace-reader
rules:
- apiGroups: [""]
resources: ["configmaps", "pods", "services", "endpoints", "secrets"]
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: namespace-reader-binding
namespace: YOUR-NAME-SPACE
subjects:
- kind: ServiceAccount
name: default
apiGroup: ""
roleRef:
kind: Role
name: namespace-reader
apiGroup: ""
这是官网添加账号权限的地方
因为我们必须要有以下资源的get
,list
和权限:watch
["configmaps", "pods", "services", "endpoints", "secrets"]
才可以访问K8S上的服务
之后就可以访问K8S上部署的服务了
可以通过简单的命令查看服务状态
kubectl create -f ./k8s-boot-app-deployment.yml #部署k8s应用
kubectl get deployments #查看deployment信息
kubectl expose deployment k8s-boot-app-deployment --type=NodePort #开放端口访问
kubectl get services #查看services信息,如端口映射情况
kubectl delete service nginx-service #删除service
kubectl get pods #获取部署的prod列表信息
kubectl logs -f springboot-k8s-template-deployment-687f8bf86d-lcq5p #查看某个pod的日志
kubectl delete -f ./k8s-boot-app-deployment.yml #删除服务
访问接口可以获得服务名称列表
[kubernetes, veh-cloud-k8s], 2022-05-26 15:46:24
得到服务细节
Service [veh-cloud-k8s]'s instance list :
[
{
"host":"10.100.96.60",
"instanceId":"a129eab0-c58d-4fdb-adc9-7670370820bc",
"metadata":{
"app":"veh-cloud-k8s"
},
"port":40300,
"scheme":"http",
"secure":false,
"serviceId":"veh-cloud-k8s",
"uri":"http://10.100.96.60:40300"
}
]
如果在本地测试时没有K8S环境可以暂时关闭K8S集成
spring.cloud.kubernetes.enabled= false
Spring Cloud Feign
当项目部署到K8S上时,接下来就是服务间的远程调用,对于服务的远程调用我们也需要对pom依赖和项目做一些修改
早期的Spring-Cloud-Kubernetes
是采用ribbon
来做服务间的负载均衡,但是新版的Spring-Cloud-Kubernetes
是采用了loadbalancer
来代替的,做一个负载均衡
这里值得注意的是对于OpenFeign
的依赖需要去掉ribbon
才可以
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
采用官方依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-loadbalancer</artifactId>
<version>2.1.2</version>
</dependency>
要启用基于 Kubernetes 服务名称的负载平衡,请使用以下属性。然后负载均衡器会尝试使用地址调用应用程序,例如service-a.default.svc.cluster.local
可以在application.yml
中添加一个配置
Spring:
cloud:
kubernetes:
loadbalancer:
mode: service
在官方代码中是可以选择pod与service
/**
* Kubernetes load balancer mode enum.
*
* @author Piotr Minkowski
*/
public enum KubernetesLoadBalancerMode {
/**
* using pod ip and port.
*/
POD,
/**
* using kubernetes service name and port.
*/
SERVICE
}
查看spring cloud kubernetes
的源码可知,loadbalancer
的默认mode
为POD
方式。
/**
* {@link KubernetesLoadBalancerMode} setting load balancer server list with ip of pod
* or service name. default value is POD.
*/
private KubernetesLoadBalancerMode mode = KubernetesLoadBalancerMode.POD;
其中pod方式与service方式的主要区别是:
service方式的负载均衡请求服务时,是直接向kubernetes集群中对应的service(svc)发起请求的;
pod方式的负载均衡请求服务时,是先由服务调用者通过调用kubernetes的api先获取到对应的服务的pod列表,之后再保存到一个list中,再使用spring cloud项目所配置的loadbalancer负载均衡策略进行请求。
也就是说,当负载均衡方式mode为service时,spring cloud项目中所配置的关于loadbalance的配置是无效的,因为它将是通过k8s中的service进行访问的。 当负载均衡方式mode为pod时,可以利用spring cloud项目中的loadbance配置进行负载。
service方式(负载均衡时的压力在k8s端)
pod方式(负载均衡时的压力在spring cloud项目端)
至于具体选择哪个,需要根据项目的情况进行选择。
一般来说,如果k8s集群资源比较多为了方便省事,选service方式即可。
如果想要做到更加灵活,并减少一些k8s集群的压力,可以选择pod方式。
要跨所有命名空间启用负载平衡,请使用以下属性。spring-cloud-kubernetes-discovery
尊重模块的属性。
spring.cloud.kubernetes.discovery.all-namespaces=true
负载均衡pod和service方式式的平滑访问
servcie模式下服务平滑访问
如果spring cloud kubernetes项目配置的是service方式,想要做到服务的平滑访问,其主要配置pod的两个探针即可,它们分别是:
就绪探针(readinessProbe)
存活探针(livenessProbe)
具体可以看下spring boot官方的说明,其链接如下:
对项目中的Feign
做修改
@FeignClient(name = "ent-cloud-k8s")
public interface EnterpriseClient {
.........
}
这里的name是项目部署的时候自己定义的其他服务名称
修改Controller
来测试
@RestController
public class DiscoveryController {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
@Qualifier("restTemplates")
private RestTemplate restTemplate;
@Autowired
private EnterpriseClient enterpriseClient;
/**
* 探针检查响应类
* @return
*/
@RequestMapping("/healths")
public String health() {
String responseEntity = enterpriseClient.getenterpriseinfo();
JSONObject JSObject = JSON.parseObject(responseEntity);
return JSObject.getString("data");
}
@RequestMapping("/ent")
public String demo() {
String url="http://ent-cloud-k8s:40900/enterprise/enterprise/info/base/info";
System.out.println(url);
String forObject = restTemplate.getForObject(url, String.class);
System.out.println(forObject);
return forObject;
}
/**
* 返回远程调用的结果
* @return
*/
@RequestMapping("/getservicedetail")
public String getservicedetail(
@RequestParam(value = "servicename", defaultValue = "") String servicename) {
try {
List<ServiceInstance> instances = discoveryClient.getInstances(servicename);
System.out.println("Service [" + servicename + "]'s instance list : " + JSON.toJSONString(discoveryClient.getInstances(servicename)));
String url = instances.get(0).getUri() + "/enterprise/enterprise/info/base/info";
System.out.println(url);
String forObject = restTemplate.getForObject(url, String.class);
System.out.println(forObject);
}catch (Exception e){
System.out.println(e.getMessage());
}
return "Service [" + servicename + "]'s instance list : " + JSON.toJSONString(discoveryClient.getInstances(servicename));
}
/**
* 返回发现的所有服务
* @return
*/
@RequestMapping("/services")
public String services() {
return this.discoveryClient.getServices().toString()
+ ", "
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
}
依照上面的部署文件重新打包部署即可访问其他项目的接口
官网:
GateWay
K8s上的gateway写起来与普通的gateway没有什么区别,就是依赖的区别
部署测试的方式和上面的一样
首先先调整依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
最关键的还是服务发现的依赖
随后写一下yml的配置
server:
port: 50201
servlet:
context-path: /
tomcat:
threads:
max: {MAX_THREAD:800}
min-spare: {MIN_THREAD:100}
spring:
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 2048MB
application:
name: smartpark-govern-gateway
redis:
host: ${REDIS_HOST:10.4.2.53}
port: ${REDIS_PORT:36379}
timeout: 5000 #连接超时 毫秒
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
lower-case-service-id: true # 服务名称小写
routes:
- id: veh-cloud-k8s
uri: lb://veh-cloud-k8s
predicates:
- Path=/**
logging:
level:
org.springframework.cloud.gateway: debug
yml的配置方式与普通的gateway配置没什么区别
启动类也是没什么区别,其他的过滤器也都是正常写
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayK8sApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayK8sApplication.class, args);
}
}
然后就可以部署测试了。
问题记录
在我调整Cloud为K8S-Cloud时发生了一些奇怪的问题,特此记录一下
当时我测试的K8S环境是1.18版本,测试了Gateway的服务转发,与服务远程调用,当时一切正常,正当我准备把全部服务都调整时,发生了问题,当时K8S的版本升级了1.23版本,那1.23版本下服务远程调用一直失败。
通过name服务名称来调用就失败
@FeignClient(ServiceList.SMARTPARK_SERVICE_ENTERPRISE)
通过URL来调用就可以成功
@FeignClient(name =ServiceList.SMARTPARK_SERVICE_ENTERPRISE+"s",url = "http://"+ServiceList.SMARTPARK_SERVICE_ENTERPRISE+":40900")
在k8s环境中1.18两种方式都正常,只有1.22与1.23第一种不正常,当两个java后端服务进行http远程调用接口时会出现找不到服务接口的访问错误。
当时得出的结论是在K8S的版本问题。
在K8S的1.18版本是可以互相调用接口,在K8S1.23版本无法相互调用,在K8S1.22版本无法相互调用,在K8S1.23和1.22版本无法调用的原因是无法获取java后端服务的端口号。
首先我对spring-cloud-starter-kubernetes-client
与spring-cloud-starter-kubernetes-client-loadbalancer
这两个依赖是否能正常获取到服务地址与端口,答案是可以的。
这里的两个依赖是版本都是2.1.2版本,为了验证是否更新依赖版本可以解决,我将K8S相关的依赖都更新到了最新的2.1.3版本,显然还是不行。
这时还有一个疑问?
在调整服务pom依赖时,我将所有的Ribbon
都去掉了包括openfeign
的,并且加上了kubernetes-client-loadbalancer
,那么这个K8S相关的依赖是如果和普通的openfeign
依赖包结合的呢?
原来答案很简单,kubernetes-client-loadbalancer
这个依赖包只有四个类,但是它依赖原本的spring-cloud-starter-loadbalancer
,只是一个关于K8S的补充,而openfeign
也依赖了spring-cloud-starter-loadbalancer
,所以原本的loadbalancer
就是他们的桥梁。
那么问题很有可能是出现在openfeign
上面。
那么如何去定位问题所在,我想到了可以远程Debug程序,这个是IDEA提供的功能。
这里简要记录一下如何远程调试。
本地虚拟机与远程虚拟机相互通信,远程虚拟机监控自身的栈帧,方法调用等运行信息,本地虚拟机通过Java API提供的可使用的调试接口,向远程虚拟机发送调试命令,并接受显示调试结果。
远程调试的核心:JPDA(Java Platform Debugger Architecture
)框架。
注意:注意端口别被占用。后续这个端口是用来跟远程的java进程通信的。这个端口要与tomcat的端口不同。
可以注意到:切换不同的jdk版本,生成的脚本不一样
- 选择 jdk1.4,则为
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=50055
- 选择 jdk 5-8,则为
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=50055
- 选择 jdk9以上,则为
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:50055
-
transport=dt_socket
:JPDA front-end
和back-end
之间的传输方法。dt_socket
表示使用套接字传输。 -
address=50055
:远程JVM在50055端口上监听请求。 -
server=y
:y表示启动的JVM是被调试者。如果为n,则表示启动的JVM是本机。 -
suspend=n
:n表示调试时会暂停远程虚拟机。
最后和正常程序一样打断点启动就可以了。
开始探索openfeign
找问题
在openfeign
里我直奔主题在loadbalancer
文件夹下开始找,因为这里是处理负载均衡的,直接找DefaultFeignLoadBalancerConfiguration
类。
这类里有两个负载均衡器客户端,一个是阻塞的一个是非阻塞的,而loadbalancer
是依赖于reactor-extra
与webflux
写的,所以一定是非阻塞的,直接看第二个方法就可以了。
K8S上获取的地址例如http://enterprise.default.svc.cluster.local:40900
enterprise.default.svc.cluster.local
是K8S里服务的地址信息拼接成的域名地址
最后拼接后的样子就是http://enterprise.default.svc.cluster.local:40900/enterprise/enterprise/info/base/info
拼接后进入来到这里LoadBalancerUtils
。
我发现在这里会一直报错,那么问题一定出现在try/catch上面,进入feignClient.execute(feignRequest, options);
方法
这里有三个实现类,我们就去第一个也就是默认的实现类里,其实就是这个类本身的方法
找到这里基本就可以了,我们开始调用接口去复现问题。
最后发现并不是无法获取java后端服务的端口号,在K8S1.18版本与K8S1.22版本(K8S1.22与K8S1.23版本的现象一致,所以采取其中之一来测试)中都可以获取java后端服务的端口号。
至于为何无法正常调用,原因是当openfeign
在K8S上获取服务的地址和端口号后会进行拼接URL,比如http://enterprise.default.svc.cluster.local:40900/enterprise/enterprise/info/base/info
,该 URL 中的enterprise.default.svc.cluster.local
是通过 enterprise
这个K8S的serviceId
在K8S上获取服务信息,40900端口也是如此。当openfeign
成功拼接完URL后会对该URL进行一次连接,获取连接实例,也就是说会在真正调用该URL获取信息前会进行一次调用,判断该URL是否正常。
原因就出在这里。在K8S1.23与1.22版本中判断该URL是否正常的那次前置调用失败了,该接口无法访问K8S获取Http的code状态码,所以就导致了服务间访问的错误。
对照测试:通过 enterprise
这个K8S的serviceId
获取的信息拼接成的URL无法访问K8S,所以就将URL写死,例如 http://enterprise:40900/enterprise/enterprise/info/base/info
,经过测试该接口在K8S1.18版本与1.22版本访问正常,两组接口唯一的区别就是 enterprise
与 enterprise.default.svc.cluster.local
的不同,前者是写死的,后者是获取的信息。
得出的结论是在K8S1.23版本与1.22版本中,K8S对于这种形式的 enterprise.default.svc.cluster.local
的URL地址无法访问获取Http的code状态码,对于仅仅只有ServiceID
进行请求的可以正常请求。
到这里就可以去找K8S的问题了。
K8S地址解析问题
就关于该地址解析问题。在官网文档上找到了相关内容 https://github.com/kubernetes/dns/blob/master/docs/specification.md#23—records-for-a-service-with-clusterip 能够正常解析的地址域名应为:enterprise.default.svc.cluster.local.
该格式类型为k8s的FQDN(Fully qualified domain name)
与无法解析地址仅差最后的一点
经测试在k8s的1.18版本上,是否带最后的一点都能够正常解析,但是在k8s的1.22+版本上,只有正确域名地址才能正常解析。
查找了该官方文档的history,发现在第一个版本(2017.1.10)中。该解析地址就是最后会带一点。所以不知道其中变迁具体原因是如何。
通过了解k8s的域名解析流程,以及容器中/etc/resolv.conf
相关内容。可以通过修改,k8s部署文件的相关配置:在所有使用服务配置文件中,添加几行配置
dnsConfig:
options:
- name: ndots
value: "2"
https://www.jianshu.com/p/9b34ee879bcb
最后问题就解决啦!