在《搭建 K8s 容器化应用的开发调试环境》一文里,我们基于 K3s、Telepresence 和 Skaffold,搭建了一个基本的 K8s 环境,这里就来实操一下,在 K8s 里部署 Spring Cloud 的服务发现 Eureka Server。
1、Clone 代码
GitHub: https://github.com/xiaoboey/from-zero-to-n
from-zero-to-n 是笔者写微服务相关文章的项目,为了方便大家结合文章阅读,尽量把各个主题的代码分开存放,没有从一个简单的项目逐渐演化。本文使用的 from-zero-to-n/dev-in-k8s 基于 from-zero-to-n/two 的代码进行开发。
git clone https://github.com/xiaoboey/from-zero-to-n.git
cd from-zero-to-n/dev-in-k8s
复制代码
查看 from-zero-to-n/dev-in-k8s 下的 eureka-server 项目,目录结构如下:
这个项目的代码非常简单,原本只有 pom.xml、EurekaServerApplication.java 和 application.yml 这三个文件。为了结合 Skaffold 来把它部署到 K8s,增加了 skaffold.yaml、Dockerfile 和 k8s/k8s-*.yaml 等文件,并且把 application.yml 进行拆分重组,dev 和 k8s 两种场景分别配置,这些调整只是增加和修改了配置文件,java 代码没做任何修改。这大概也是大多数 Coder 不关心 K8s 的一个原因了,埋头肝代码就是了,其它由运维去操心吧😂
2、部署到 K8s 要解决的问题
结合原版的 application.yml 文件,我们来分析一下,要把这个应用容器化并部署到 K8s,需要解决哪些问题。
server:
port: 8761
servlet:
context-path: /
spring:
profiles:
active: dev
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
management:
endpoints:
web:
exposure:
include: ["health","info", "shutdown"]
endpoint:
health:
show-details: always
shutdown:
enabled: true
server:
port: 7761
复制代码
主要问题如下:
像 server.port 这类直接给参数指定固定值的问题,Docker 的镜像机制已经解决了,在构建镜像的时候通过 EVN 设置环境变量,实例化镜像时用-e 参数赋值。
Spring Boot 对环境变量也提供了支持,在 application.yml 文件中可以用${环境变量名}
的方式取环境变量的值,还可以指定默认值:${环境变量名:默认值}
。
至于 Pod 地址会变,无法设置 defaultZone 的问题,在互联网世界早已有成熟的方案,那就是域名。只要域名固定下来,域名指向的 ip 地址发生变化,不会影响终端用户的访问。K8s 也提供了 DNS 服务,在创建 Service 时提供了一个 Headless 的机制(clusterIP: None),不给 cluster 分配具体的 ip 地址,通过域名访问服务。
在 application-k8s.yml 文件里,使用环境变量,并按集群的方式配置 Eureka:
eureka:
instance:
hostname: ${EUREKA_INSTANCE_HOSTNAME}
prefer-ip-address: false
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: ${EUREKA_INSTANCE_LIST}
复制代码
3、Dockerfile
先从应用的容器化开始,也就是把应用打包为 Docker 镜像。
Dockerfile 是 Docker 用来构建容器镜像的源文件,此处尽量注解说明了每一条命令,更多 Dockerfile 的知识请自行搜索其它资料进行学习。
# 基础镜像
FROM openjdk:8-jdk-alpine
# 作者信息
LABEL maintainer="xiaoqb@foxmail.com"
# 修改alpine的源为阿里云的源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 更新
RUN apk upgrade --available && sync
# 创建文件夹
RUN mkdir /app
# 设置当前的工作路径,后续的操作在这个路径下进行
WORKDIR /app
# 把编译后的jar复制到容器里
ADD ./target/*.jar eureka-server.jar
# 设置环境变量(这类变量可以在启动容器时设置,对应docker run的-e参数)
ENV SPRING_PROFILES_ACTIVE=dev
ENV HTTP_PORT=8761
ENV ACTUATOR_PORT=7761
# 容器开放端口
EXPOSE $HTTP_PORT
EXPOSE $ACTUATOR_PORT
# 容器启动执行命令
# -XX:+UseContainerSupport 让JVM在容器环境里分配合理的堆内存
# -XX:MaxRAMPercentage 取值范围0.0到100.0,默认值25.0
ENTRYPOINT java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -jar eureka-server.jar --spring.profiles.active=$SPRING_PROFILES_ACTIVE
复制代码
Alpine 是因 Docker 才火起来的轻量 Linux 发行版,够小够安全,制作出来的镜像比较小。我们这个 Eureka Server 制作出来的镜像大小大概是 200M 左右。更改 Alpine 的源和进行系统升级,都是可选操作,在开发环境,为了提高构建速度,可以注释略过,生产环境则建议进行升级操作。
容器环境的 Java 内存参数,稍微有点不一样,UseContainerSupport 这个参数是 Java 适应容器化部署扩展出来的一个能力。就 K8s 而言,大致的意思是 Java 可以感知到 K8s 对资源的限制。不然的话,Java 参数这里限制了具体的内存,然后 K8s 做容器编排调整资源参数(这里主要是 memory 和 cpu),怎么调整也没用,或者 java 参数的内存值高于 K8s 分配的资源,直接就打爆内存。
手工用 Docker 测试一下这个 Dockerfile 是否配置正确:
# 打开PowerShell,先整体编译,后续可以只编译eureka-server
PS > cd from-zero-to-n\dev-in-k8s>
PS > mvn clean install -DskipTests
...
[INFO] Reactor Summary for from-zero-to-two 0.0.1-SNAPSHOT:
[INFO]
[INFO] from-zero-to-two ................................... SUCCESS [ 0.778 s]
[INFO] eureka-server ...................................... SUCCESS [ 10.242 s]
[INFO] gateway ............................................ SUCCESS [ 1.485 s]
[INFO] auth-server ........................................ SUCCESS [ 3.289 s]
[INFO] service-one ........................................ SUCCESS [ 1.567 s]
[INFO] service-two ........................................ SUCCESS [ 1.234 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
# 进入Eureka Server
PS > cd from-zero-to-n\dev-in-k8s\eureka-server
# 构建Docker镜像
PS > docker build -t eureka-server:v1 .
[+] Building 0.8s (11/11) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 32B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for docker.io/library/openjdk:8-jdk-alpine
=> [internal] load build context
=> => transferring context: 89B
=> [1/6] FROM docker.io/library/openjdk:8-jdk-alpine
=> CACHED [2/6] RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
=> CACHED [3/6] RUN apk upgrade --available && sync
=> CACHED [4/6] RUN mkdir /app
=> CACHED [5/6] WORKDIR /app
=> CACHED [6/6] ADD ./target/*.jar eureka-server.jar
=> exporting to image
=> => exporting layers
=> => writing image sha256:162305b3d7cde6e7f5e3ea4ea78048c71c313c5632d74dd1fd3e0a4a822c47b1
=> => naming to docker.io/library/eureka-server:v1
# 查看构建结果
PS > docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
eureka-server v1 162305b3d7cd 25 minutes ago 253MB
...
# 用刚才构建的镜像启动容器化部署的EurekaServer
# --rm 结束后自动删除容器实例
# --it 运行后进入交互界面并使用标准输入,可以ctl+c终止运行
PS > docker run --rm -it eureka-server:v1
...
... The following profiles are active: dev
... Tomcat initialized with port(s): 8761 (http)
...
# 指定环境变量参数
# 这里只是测试环境变量是否生效,没有给application-k8s.yml文件里需要的两个关键环境变量,启动会失败
PS > docker run -e SPRING_PROFILES_ACTIVE=k8s -e SERVER_PORT=9871 --rm -it eureka-server:v1
...
... The following profiles are active: k8s
... Tomcat initialized with port(s): 9871 (http)
...
复制代码
4、准备 K8s 部署文件
Eureka Server 的单实例部署没什么好说的了,跟 bootcamp 的部署类似,但要部署为多个实例的集群,要涉及到 K8s 的 StatefulSet 和 Headless。因为 Eureka Server 集群需要相互注册并协同工作,要保持一定的状态(SatefulSet),并且需要其它微服通过 DNS 来找到 Eureka Server 并向 Eureka Server 注册,这需要部署 Headless 的 Service 来实现。
部署 eureka-server 的 StatefulSet 是这个文件:k8s/k8s-sts.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
namespace: default
name: eureka
labels:
app: eureka
spec:
serviceName: "eureka"
replicas: 3
selector:
matchLabels:
app: eureka
template:
metadata:
labels:
app: eureka
spec:
terminationGracePeriodSeconds: 10
imagePullSecrets:
- name: my-registry-key
containers:
- name: eureka
image: eureka-server:v1
ports:
- containerPort: 8761
- containerPort: 7761
env:
- name: SPRING_PROFILES_ACTIVE
value: k8s
- name: EUREKA_INSTANCE_HOSTNAME
value: ${HOSTNAME}.eureka
- name: EUREKA_INSTANCE_LIST
value: "http://eureka-0.eureka.default.svc.cluster.local:8761/eureka/,http://eureka-1.eureka.default.svc.cluster.local:8761/eureka/,http://eureka-2.eureka.default.svc.cluster.local:8761/eureka/"
# 健康检查
livenessProbe:
httpGet:
path: /actuator/health
port: 7761
initialDelaySeconds: 20
timeoutSeconds: 5
复制代码
这里 env 传递了 3 个环境变量,SPRING_PROFILES_ACTIVE 是在 Dockerfile 里启动 Spring Boot 应用时指定 profile,另两个则是在 application-k8s.yml 里使用。
StatefulSet 里的 ${HOSTNAME}是以 ${metadata.name}-n 这样的格式取名,n 是从 0 开始的序列,此处就是 eureka-0、eureka-1、eureka-2,不但始终从 0 开始,还保证序号为 0 的实例先就绪,再依次是 1、2...,http://eureka-0.eureka.default.svc.cluster.local里的 eureka 是 headless Service 的名称,default 是 k8s 的 namespace。
因为要用 Skaffold 来部署,所以还需要配置访问镜像仓库的 Secret:
# 下述指令里的***和邮件,请替换你自己的信息
# 如果是linux请把行尾的`换成\
kubectl create secret `
docker-registry my-registry-key `
--docker-server=registry.cn-chengdu.aliyuncs.com/*** `
--docker-username=*** `
--docker-password=*** `
--docker-email=a@b.com
复制代码
此操作同时隐含了对容器镜像仓库的准备,请参看《搭建 K8s 容器化应用的开发调试环境》
部署 Headless Service 是这个文件:k8s/k8s-svc.yaml
kind: Service
apiVersion: v1
metadata:
name: eureka
namespace: default
spec:
clusterIP: None
ports:
- name: http
port: 8761
protocol: TCP
targetPort: 8761
selector:
app: eureka
复制代码
Headless 的标志是那句“clusterIP: None”
部署一个负载均衡来提供统一的访问入口:k8s/k8s-lb.yaml
kind: Service
apiVersion: v1
metadata:
name: eureka-lb
namespace: default
spec:
type: LoadBalancer
ports:
- name: http
port: 8761
protocol: TCP
targetPort: 8761
selector:
app: eureka
复制代码
重点是那句“type: LoadBalancer”。从配置文件看来,Headless 和 LoadBalancer 都是一种 Service,感觉 K8s 的水比较深啊
5、配置 Skaffold
Skaffold 的配置文件就很简单了:skaffold.yaml
apiVersion: skaffold/v2beta26
kind: Config
build:
artifacts:
- image: eureka-server
deploy:
kubectl:
manifests:
- k8s/k8s-*
复制代码
k8s/k8s-*表示让 skaffold 应用 k8s 文件夹下 k8s-为前缀的那些 yaml 文件进行部署,本来想把 Dockerfile 和 skaffold.yaml 这两文件也放在 k8s 文件夹里,显得整齐一些,无赖 Dockerfile 里的 COPY 和 ADD 操作,只对该文件同级或者子路径的文件有效,暂时无解只能这样了。
6、构建、部署与测试
6.1 部署
用 Skaffold 进行构建与部署(这里按步骤列出了相关的操作,重复操作可以视情况忽略):
# PowerShell下进入项目路径
PS > cd from-zero-to-n\dev-in-k8s\eureka-server
# 编译
PS > mvn clean install -DskipTests
# 登录镜像仓库
PS > docker login <你的镜像仓库地址>
# 确保k8s环境已经启动
PS > multipass list
PS > multipass start k3s-1
# multipass重启k3s-1后,虚拟机的地址可能会变,所以还需要修改~/.kube/config文件里的k8s集群地址
# 验证一下kubectl是否配置正确
PS > kubectl get pod
# 构建Docker镜像并部署到k8s
# 注意这里的镜像仓库地址,是eureka-server镜像地址的上一级,否则可能导致无法推送镜像到仓库
PS > skaffold run --default-repo=<你的镜像仓库地址>
复制代码
查看最后的部署情况:
PS > kubectl get pod
NAME READY STATUS RESTARTS AGE
svclb-eureka-lb-lsgxf 1/1 Running 1 23h
eureka-2 1/1 Running 0 2m55s
eureka-1 1/1 Running 0 2m25s
eureka-0 1/1 Running 0 2m15s
PS > kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
eureka-lb LoadBalancer 10.43.252.142 172.30.144.82 8761:31080/TCP 38m
eureka ClusterIP None <none> 8761/TCP,7761/TCP 38m
...
复制代码
可以看出来 StatefulSet 部署产生的主机名称是固定的,eureka-0、eureka-1 和 eureka-2 也是依次启动。
6.2 调试技巧
出现 Pod 部署失败,运行异常,或者反复重启等情况,大概有这些手段去定位错误:
用 kubectl logs 命令查看容器里应用的日志:
eureka-0 是 Pod 的名称,-f 则表示持续查看后续的日志输出
用 kubectl describe 查看 Pod 的信息及日志:
kubectl describe pod eureka-0
复制代码
describe 命令可以查看所有的 k8s 资源,pod、svc、secret 等
或者 kubectl exec 用 shell 连接 Pod 进行操作:
kubectl exec -it eureka-0 sh
复制代码
跟 docker 的 exec 类似
6.3 测试
这里就不劳 telepresence 的大驾了,直接用 kubectl 的端口转发(port-foward):
PS > kubectl port-forward --address=0.0.0.0 Pod/eureka-0 8761:8761
Forwarding from 0.0.0.0:8761 -> 8761
复制代码
telepresence 开启代理成功后,可能会影响虚拟机对外网的访问,表现为域名解析失败,所以建议测试完后,记得及时 quit,退出 telepresence 的代理状态。或者尽量少用吧🤦♂️
在浏览器访问: http://localhost:8761
当然,通过 LoadBalance 访问,也是可以的:
PS > kubectl port-forward --address=0.0.0.0 Service/eureka-lb 8761:8761
Forwarding from 0.0.0.0:8761 -> 8761
复制代码
6.4 加入 gateway
把 dev-in-k8s/gateway 也部署到 K8s,测试一下服务注册和发现。Gateway 的配置这里就不贴了,比较简单,不需要 StatefulSet 或者 Headless,只需要传一个环境变量给 eureka.client.service-url.defaultZone:
7、总结与体会
在 Mulitpass 启动的虚拟机里跑 K3s,然后向 K8s 集群部署 Eureka Server,一切顺利的话还是比较快的,就怕中间出什么幺蛾子^_^
虚拟机的 K3s 感觉不够稳定,有时行有时不行,可能是内存、cpu 资源这些的限制,也可能是本机环境的变化导致,也不排除 K8s 本身的问题。这比较烦人,需要折腾的时候多思考,卡壳的时候重启虚拟机,甚至重启本机😢
Telepresence 在 Windows 上确实有些渣,基本上可以稳定重现的一个问题,就是 Telepresence 启动代理后,会影响 K8s 集群里的 Pod 拉取互联网上镜像仓库里的镜像。
Skaffold 反而让人比较惊喜,可以稳定的工作,大厂出的产品在稳定性上还是有保障一些。
评论