前言 Kubernetes(K8s)脱胎于Google的Borg系统,在2015年发布以来,已经是当下最流行的容器编排工具(没有之一)。在本文中,笔者会使用上一篇文章中的demo,模拟把代码部署到K8s的过程,同时也会在这个过程中讨论一下部署方案的设计思路。
本文内容主要都是应用发布相关,为了简化步骤,使用minikube进行演示。
Demo代码介绍 这次使用的demo代码如下,模拟一个典型的flask http应用,通过项目路径中的配置文件获取必要的业务配置,得到内网中的Redis连接信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import timeimport redisimport jsonfrom flask import Flaskconfig_json = json.load(open ('./config/config.json' , "rb" )) redis_config = config_json['redis_config' ] show_message = config_json['show_message' ] app = Flask(__name__) cache = redis.Redis(host=redis_config['redis_host' ], port=redis_config['redis_port' ], db=redis_config['redis_db' ], password=redis_config['redis_password' ]) def get_hit_count (): retries = 5 while True : try : return cache.incr('hits' ) except redis.exceptions.ConnectionError as exc: if retries == 0 : raise exc retries -= 1 time.sleep(0.5 ) @app.route('/' ) def hello (): count = get_hit_count() return '{}! I have been seen {} times.\n' .format (show_message,count) @app.route('/health' ) def health (): return "i am fine" if __name__ == "__main__" : app.run(host = '0.0.0.0' )
其中,配置文件内容如下:
1 2 3 4 5 6 7 8 9 { "redis_config" : { "redis_host" : "192.168.0.251" , "redis_port" : 6379 , "redis_db" : 0 , "redis_password" : "aaaa1111" }, "show_message" : "this is a python-redis demo" }
传统部署方案的痛点 对于这种python flask应用,传统的部署方案是使用supervisor托管python进程,封装成一个服务,通过Nginx或者其他同类产品实现负载均衡,架构图如下:
这种传统部署方案多用在IDC机房时代或者是云计算刚刚开始流行的早期,除了费钱之外,最大的问题在于不够灵活:
扩缩容不灵活,就算是使用了云平台或者虚拟化,还是逃不过服务节点创建和注销的繁琐流程。
部署不灵活,需要为应用量身定制一套部署流程,维护主机列表去进行批量的文件发布(如果你觉得已经比手动好很多了,那就当我没说 ;-) )。
配置修改不灵活,在运行的过程中,如果需要临时对配置内容进行调整,都需要一套严谨的方案保障所有的节点使用的配置内容的一致性,避免业务故障。
架构笨重,从架构图上就能看出来,为了保障服务的高可用,不可避免地需要多台服务器进行负载均衡,而单单是为了一个应用的运行就需要一个完整的操作系统,显然是一件很麻烦的事情。
总结下来,传统部署方案的诸多不便,其实是架构决定的。也正因为传统部署方案存在着诸多的不便,后来才有了容器技术。硬件的虚拟化促成了云平台的诞生,实现了硬件的复用,降低了设备成本;而内核的虚拟化又刺激了容器技术的蓬勃发展,使得系统内核可得以带着应用服务轻装上路,这就是这些年来的技术趋势。
在接下来的内容中,我们会一步一步把这个demo部署到K8s上。
容器镜像构建 在应用部署到K8s之前,我们需要先把代码封装成一个容器镜像,并上传到镜像仓库。
项目路径 项目路径的内容如下,配置文件存放在本地。
1 2 3 4 5 6 python-redis-demo ├── Dockerfile ├── app.py ├── config │ └── config.json └── requirements.txt
Dockerfile 1 2 3 4 5 6 7 8 9 10 11 FROM python:3.10 -alpineWORKDIR /code ENV FLASK_APP=app.pyENV FLASK_RUN_HOST=0.0 .0.0 RUN apk add --no-cache gcc musl-dev linux-headers COPY requirements.txt requirements.txt RUN pip install -r requirements.txt EXPOSE 5000 COPY . . CMD ["flask" , "run" ]
使用docker build构建容器镜像 在开发任务结束并且确认功能正常后,我们就可以把代码封装成一个容器镜像了,步骤和上一篇文章 类似:
1 2 3 4 5 6 7 8 9 10 cd python-redis-demodocker build -t rondochen/python-redis-demo:v1.2 . docker build --build-arg "HTTPS_PROXY=http://xxx.xxx.xxx.xxx:xxxx" -t rondochen/python-redis-demo:v1.2 . docker push rondochen/python-redis-demo:v1.2
笔者已把这个镜像上传到Dockerhub,供大家参考:
1 docker push rondochen/python-redis-demo:v1.2
(顺带一提)关于K8s不再支持Docker的新闻 在K8s 1.20版本的changelog里面,有提过说未来将会放弃对docker的支持,当时还引起了广泛的讨论,甚至很多人都以为后面就不能在K8s里面使用docker镜像了。
后来在K8s的官方博客里面还专门重新解释了一下这个变更的原因。简单来说,K8s只是在未来不再支持docker的运行环境,而我们通过docker build生成的镜像并不是只能运行在docker环境中,只要镜像文件是符合OCI(Open Container Initiative)标准的,就可以在K8s中运行,哪怕是使用不同的容器技术(如containerd或CRI-O)。
也就是说,我们可以继续使用docker build去构建容器镜像并部署到K8s中,但是如果是有一些docker运行环境专属的功能,比如说一些依赖/var/run/docker.sock的场景,就不再支持。
部署到minikube(Kubernetes) 如果你刚刚接触K8s,或者是希望有一个K8s环境可以调试应用的开发者,又或者是一个懒得去部署一整套K8s集群但又想介绍一个K8s方案的撰稿人, Minikube就是你的好朋友。
Minikube可以很轻易地帮你生成出一个足够迷你但是功能又刚好够用的K8s集群,以支持你进行常用的K8s功能测试。
https://kubernetes.io/docs/tutorials/hello-minikube/
安装minikube 可以参考官方文档完成Minikube的安装:https://minikube.sigs.k8s.io/docs/start/
如果是使用Linux系统安装,建议选择带有图形界面的版本,这样在使用minikube dashboard的时候可以方便一点。
另外,如果因为网络问题无法使用Google的镜像库,可以设置国家或者地区,从而在启动的时候就指定镜像的下载地址:
1 minikube start --image-mirror-country='cn'
在接下来的演示中,我们都在dev这个命名空间下进行。
1 2 kubectl create namespace dev
通过deployment部署pod 集群启动好了之后,我们就可以把pod部署起来了。尽管我们可以直接用kubectl以命令行的方式创建pod,比如说:
1 kubectl run nginx --image=nginx:latest --port=80 --namespace dev
但我们一般不会这样做。
在生产环境中,对于本文demo的这种无状态服务,更常见和规范的做法,是通过deployment完成。 在下面的yaml文件中,对通过deployment这个pod控制器,对pod进行了如命名空间、pod节点数以及使用的镜像等配置,并赋予了app: mydemo这个标签属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 apiVersion: apps/v1 kind: Deployment metadata: name: mydemo namespace: dev spec: replicas: 2 selector: matchLabels: app: mydemo template: metadata: labels: app: mydemo spec: containers: - image: rondochen/python-redis-demo:v1.2 name: mydemo ports: - name: mydemo-port containerPort: 5000 protocol: TCP
把配置文件提交到k8s集群后,就可以自动完成pod的创建:
1 2 3 4 5 6 7 8 9 kubectl apply -f deploy-mydemo.yaml [root@minikube yaml] NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR mydemo 2/2 2 2 4d1h mydemo rondochen/python-redis-demo:v1.2 app=mydemo
查看pod状态 我们可以看到,有两个pod节点已经正常运行:
1 2 3 4 5 6 7 8 9 [root@minikube yaml] NAME READY STATUS RESTARTS AGE mydemo-59d5b8c44c-95jbt 1/1 Running 0 46h mydemo-59d5b8c44c-gpssx 1/1 Running 0 46h [root@minikube yaml] NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES mydemo-59d5b8c44c-95jbt 1/1 Running 0 46h 172.17.0.4 minikube <none> <none> mydemo-59d5b8c44c-gpssx 1/1 Running 0 46h 172.17.0.3 minikube <none> <none>
再回看前文提到的传统部署方案中对于扩容的不便,这里就可以产生明显的对比了。在K8s中,只需要简单的replicas配置,即可快速完成服务的扩缩容操作。
pod间访问 在K8s的设计网络设计理念中,最特别的一点是,所有的pod都有一个唯一的IP地址,并且所有的pod都可以通过IP地址互访。
在上面的输出中,我们已知pod运行在172.17.0.3和172.17.0.4两个IP下,我们可以使用kubectl debug命令创建一个临时的pod请求一下mydemo所在的IP地址,检查pod是否在正常运行。
1 2 3 4 5 6 7 8 9 10 [root@minikube yaml] Creating debugging pod node-debugger-minikube-zb8s8 with container debugger on node minikube. If you don't see a command prompt, try pressing enter. [root@minikube /]# curl http://172.17.0.3:5000 this is a python-redis demo! I have been seen 1047 times. [root@minikube /]# curl http://172.17.0.3:5000 this is a python-redis demo! I have been seen 1048 times. [root@minikube /]# curl http://172.17.0.3:5000/health i am fine[root@minikube /]# exit exit
创建service 显然,光有pod是不够的,我们还需要一个类似传统部署方案中的Nginx那样的入口,同时肩负起负载均衡和服务暴露的功能。在K8s集群中,我们通过Service实例实现这个需求。
在service的配置文件中,通过selector选择了带有app: mydemo标签的pod作为自己的后端服务,并且完成了80:5000的端口映射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 kind: Service metadata: name: svc-mydemo namespace: dev labels: app: mydemo spec: selector: app: mydemo ports: - protocol: TCP port: 80 targetPort: 5000
提交配置到K8s后,service即可创建成功:
1 kubectl apply -f service-mydemo.yaml
集群内访问service 我们可以看到,在默认情况下,创建出来的service是ClusterIP类型,有CLUSTER-IP,但没有EXTERNAL-IP,所以只能在集群内访问。
1 2 3 4 5 6 7 8 9 [root@minikube yaml]# kubectl get service -n dev -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR svc-mydemo ClusterIP 10.100.164.207 <none> 80/TCP 4d app=mydemo [root@minikube yaml]# kubectl debug -n dev node/minikube -it --image=centos Creating debugging pod node-debugger-minikube-hzlrm with container debugger on node minikube. If you don't see a command prompt, try pressing enter. [root@minikube /]# [root@minikube /]# curl http://10.100.164.207/ this is a python-redis demo! I have been seen 1051 times.
集群外访问service 如果想要在集群外都能访问Servcie,需要把Service设置成NodePort类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Service metadata: name: svc-mydemo namespace: dev labels: app: mydemo spec: selector: app: mydemo ports: - protocol: TCP port: 80 targetPort: 5000 type: NodePort
完成修改后,再看service的信息,我们会发现,类型已经更改成NodePort,而且可以在集群外被访问:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@minikube yaml]# kubectl get service -n dev NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE apigateway ExternalName <none> apigateway.dev.svc.cluster.local <none> 47h svc-mydemo NodePort 10.100.164.207 <none> 80:32098/TCP 4d4h [root@minikube yaml]# minikube service list |---------------|------------------------------------|--------------|---------------------------| | NAMESPACE | NAME | TARGET PORT | URL | |---------------|------------------------------------|--------------|---------------------------| | default | kubernetes | No node port | | dev | svc-mydemo | 80 | http://192.168.49.2:32098 | | kube-system | apigateway | No node port | |---------------|------------------------------------|--------------|---------------------------| [root@minikube yaml]# curl http://192.168.49.2:32098 this is a python-redis demo! I have been seen 1052 times.
因为这个K8s集群是建立在一个内网服务器上的minikube,没有公网地址或者是向云平台上的LoadBalance,所以最后我们就算是把服务暴露到集群外了,也无法模拟完整的使用场景。假如是在功能完整的K8s集群中,一个类型为NodePort的Service,会绑定到一个Node服务器的IP地址上,我们从内网或者公网都可以通过Node的IP:Port访问服务。
Kubernetes的高级应用 经过上面的步骤,我们已经初步完成了这个demo应用的容器化改造,也通过了deployment和service功能成功打通了访问路径,可以说,这个应用已经上线了。但是从运维的角度来看,我们不会只满足于把一段代码部署到线上,我们还需要考虑其他的运维需求。
配置分离 在真实的项目中,容器镜像里面的配置文件往往是开发环境或者是测试环境的内容,在我们要给一个容器镜像部署到生产环境之前,都需要重新修改配置内容。在Docker Host方案中,我们可以使用挂载Volume的方式替换掉容器里面的配置文件,但如果再遇到多节点负载均衡的情况,我们又要花额外的精力去保障所有DockerHost中配置文件的一致性,显然不是一个优雅的方案。
在K8s,我们可以使用K8s的配置管理工具ConfigMap完成配置分离。
创建ConfigMap 我们可以把ConfigMap简单理解为一个特殊的Volume,也是通过一个yaml文件定义ConfigMap的属性和配置内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: v1 kind: ConfigMap metadata: name: mydemo-config namespace: dev labels: app: mydemo data: config.json: | { "redis_config": { "redis_host": "192.168.0.251", "redis_port": 6379, "redis_db": 0, "redis_password": "aaaa1111" }, "show_message": "this is a python-redis demo with configmap" }
最后使用命令kubectl apply -f configmap-mydemo.yaml 提交更改。
Pod挂载ConfigMap 正如前面所说,我们把ConfigMap看做一个特殊的Volume,创建ConfigMap之后,还需要把ConfigMap挂载到Pod中,deployment的yaml文件需要添加如下的内容:
1 2 3 4 5 6 7 8 9 10 11 12 ... containers: - image: rondochen/python-redis-demo:v1.2 name: mydemo volumeMounts: - name: config-volumn mountPath: /code/config volumes: - name: config-volumn configMap: name: mydemo-config ...
(顺带一提)配置文件动态加载功能 在腾讯游戏的《可运营规范》里面,有要求服务必须支持动态加载配置的功能,即配置文件变更后,不需要重启服务进程就能生效。在ConfigMap乃至K8s其他的配置管理工具中,当我们成功提交了配置内容变更的指令后,Pod中的配置都是无需重启就能更新的。至于Pod中的服务进程是否有做到动态加载变化的配置文件,还是需要在业务代码中实现。
之所以突然想到这个,是想提醒大家,业务上的功能不能依赖操作系统或者运维工具实现 ;-)。
资源管理 在K8s集群中,一个Node节点的CPU/内存或者其他硬件资源是有限的,我们要怎么给里面的各个容器分配资源呢?
同样,我们在deployment中可以给Pod中的每个容器分配资源。举例说,我要为这个容器分配0.5核的CPU和0.5G的内存空间,具体yaml示例如下:
1 2 3 4 5 6 7 8 9 10 containers: - image: rondochen/python-redis-demo:v1.2 name: mydemo resources: limits: cpu: 500m memory: 512Mi requests: cpu: 500m memory: 512Mi
健康检查 使用K8s的探针功能,定期请求demo中的健康检查URI(/health),判断服务是否处在就绪状态:
1 2 3 4 5 6 7 8 9 containers: - image: rondochen/python-redis-demo:v1.2 name: mydemo livenessProbe: httpGet: path: /health port: 5000 initialDelaySeconds: 10 periodSeconds: 10
在这个例子中,假如其中一个Pod不在就绪状态,Service就会把流量分配到其他正常的Pod。
(扩展内容)使用Ingress实现URI的rewrite 在微服务的设计理念中,所有的服务都通过一个统一的入口接入,再根据不同的URI进入到不同的服务中,所以就有了前面题图里面的Ingress模块。
为了便于理解,可以把Ingress理解为一个Nginx(实际上就是Nginx),通过location匹配把不同的URI分类反向代理到不同的服务中。为了实现准确的反向代理,Ingress还需要使用一种类似DNS的功能,在反向代理的时候能准确找到各个服务的IP。
需要注意的是,下面的操作Ingress部分只是在Minikube环境中特有的。在完整的K8s集群中,并不适用下面的步骤。
官方文档: https://minikube.sigs.k8s.io/docs/handbook/addons/ingress-dns/
创建ingress实例 在本例中,笔者在集群中创建了一个域名为apigateway.test的服务,通过URI路径反向代理到flask demo, 使用的yaml内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: apigateway namespace: dev annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2 spec: ingressClassName: nginx rules: - host: apigateway.test http: paths: - path: /mydemo(/|$)(.*) pathType: Prefix backend: service: name: svc-mydemo port: number: 80 --- apiVersion: v1 kind: Service metadata: name: apigateway namespace: dev spec: type: ExternalName externalName: apigateway.dev.svc.cluster.local
集群外访问 请求示例如下:
1 2 3 4 [root@minikube yaml] this is a python-redis demo with configmap! I have been seen 1058 times . [root@minikube yaml] i am fine[root@minikube yaml]
配置汇总 最后来汇总一下deployment中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 apiVersion: apps/v1 kind: Deployment metadata: name: mydemo namespace: dev labels: app: mydemo spec: replicas: 2 selector: matchLabels: app: mydemo template: metadata: labels: app: mydemo spec: containers: - image: rondochen/python-redis-demo:v1.2 name: mydemo ports: - name: mydemo-port containerPort: 5000 protocol: TCP livenessProbe: httpGet: path: /health port: 5000 initialDelaySeconds: 10 periodSeconds: 10 volumeMounts: - name: config-volumn mountPath: /code/config resources: limits: cpu: 500m memory: 500Mi requests: cpu: 500m memory: 500Mi volumes: - name: config-volumn configMap: name: mydemo-config
总结 最初笔者是想通过一个demo去介绍K8s的实现原理和大概的编排思路,但是在撰文的过程中却反过来觉得被这个demo局限了思路。到了总结阶段,回头发现一个小小的demo已经引出了这么多的思考。
尽管笔者已经尽可能地把大部分常见运维场景都覆盖到,但一个小小的demo显然撑不起这个野心,现在能想到的就还有镜像瘦身、性能监控、日志收集和Pod调度这些内容没有提到。(吐槽again,总不能把公司的项目拿上来说吧)
诚然,K8s里面的功能可远远不止这些,单单说是服务管理,除了Deployment还有Job和DaemonSet;在应用配置管理,除了ConfigMap还有Secret和ServiceAccount。想要对这诸多功能和feature一一详细介绍显然是不可能的,笔者也无意照搬官方文档对K8s进行面面俱到的指引,只是希望可以通过这个基于http的无状态服务部署到K8s过程,为读者带来一些思考和启发。
后面会再抽时间介绍一下Helm或者是容器编排的一些场景,拭目以待。
扩展阅读 https://github.com/kubernetes/ingress-nginx/blob/main/docs/examples/rewrite/README.md https://minikube.sigs.k8s.io/docs/handbook/addons/ingress-dns/ https://kubernetes.io/docs/tasks/debug-application-cluster/ https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/ https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/