使用minikube模拟基于Kubernetes平台的容器化改造
前言
Kubernetes(K8s)脱胎于Google的Borg系统,在2015年发布以来,已经是当下最流行的容器编排工具(没有之一)。在本文中,笔者会使用上一篇文章中的demo,模拟把代码部署到K8s的过程,同时也会在这个过程中讨论一下部署方案的设计思路。
本文内容主要都是应用发布相关,为了简化步骤,使用minikube进行演示。
Demo代码介绍
这次使用的demo代码如下,模拟一个典型的flask http应用,通过项目路径中的配置文件获取必要的业务配置,得到内网中的Redis连接信息:
1 | import time |
其中,配置文件内容如下:
1 | { |
传统部署方案的痛点
对于这种python flask应用,传统的部署方案是使用supervisor托管python进程,封装成一个服务,通过Nginx或者其他同类产品实现负载均衡,架构图如下:
这种传统部署方案多用在IDC机房时代或者是云计算刚刚开始流行的早期,除了费钱之外,最大的问题在于不够灵活:
扩缩容不灵活,就算是使用了云平台或者虚拟化,还是逃不过服务节点创建和注销的繁琐流程。
部署不灵活,需要为应用量身定制一套部署流程,维护主机列表去进行批量的文件发布(如果你觉得已经比手动好很多了,那就当我没说 ;-) )。
配置修改不灵活,在运行的过程中,如果需要临时对配置内容进行调整,都需要一套严谨的方案保障所有的节点使用的配置内容的一致性,避免业务故障。
架构笨重,从架构图上就能看出来,为了保障服务的高可用,不可避免地需要多台服务器进行负载均衡,而单单是为了一个应用的运行就需要一个完整的操作系统,显然是一件很麻烦的事情。
总结下来,传统部署方案的诸多不便,其实是架构决定的。也正因为传统部署方案存在着诸多的不便,后来才有了容器技术。硬件的虚拟化促成了云平台的诞生,实现了硬件的复用,降低了设备成本;而内核的虚拟化又刺激了容器技术的蓬勃发展,使得系统内核可得以带着应用服务轻装上路,这就是这些年来的技术趋势。
在接下来的内容中,我们会一步一步把这个demo部署到K8s上。
容器镜像构建
在应用部署到K8s之前,我们需要先把代码封装成一个容器镜像,并上传到镜像仓库。
项目路径
项目路径的内容如下,配置文件存放在本地。
1 | python-redis-demo |
Dockerfile
1 | # syntax=docker/dockerfile:1 |
使用docker build构建容器镜像
在开发任务结束并且确认功能正常后,我们就可以把代码封装成一个容器镜像了,步骤和上一篇文章类似:
1 | # 构建docker 镜像 |
笔者已把这个镜像上传到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 | # 创建命名空间 |
通过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 | # deploy-mydemo.yaml |
把配置文件提交到k8s集群后,就可以自动完成pod的创建:
1 | # 提交配置文件 |
查看pod状态
我们可以看到,有两个pod节点已经正常运行:
1 | [root@minikube yaml]# kubectl get pods -n dev -l app=mydemo |
再回看前文提到的传统部署方案中对于扩容的不便,这里就可以产生明显的对比了。在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 | [root@minikube yaml]# kubectl debug -n dev node/minikube -it --image=centos |
创建service
显然,光有pod是不够的,我们还需要一个类似传统部署方案中的Nginx那样的入口,同时肩负起负载均衡和服务暴露的功能。在K8s集群中,我们通过Service实例实现这个需求。
在service的配置文件中,通过selector选择了带有app: mydemo
标签的pod作为自己的后端服务,并且完成了80:5000
的端口映射:
1 | # service-mydemo.yaml |
提交配置到K8s后,service即可创建成功:
1 | kubectl apply -f service-mydemo.yaml |
集群内访问service
我们可以看到,在默认情况下,创建出来的service是ClusterIP类型,有CLUSTER-IP,但没有EXTERNAL-IP,所以只能在集群内访问。
1 | [root@minikube yaml]# kubectl get service -n dev -o wide |
集群外访问service
如果想要在集群外都能访问Servcie,需要把Service设置成NodePort类型。
1 | # service-mydemo-NodePort.yaml |
完成修改后,再看service的信息,我们会发现,类型已经更改成NodePort
,而且可以在集群外被访问:
1 | [root@minikube yaml]# kubectl get service -n dev |
因为这个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 | # configmap-mydemo.yaml |
最后使用命令kubectl apply -f configmap-mydemo.yaml
提交更改。
Pod挂载ConfigMap
正如前面所说,我们把ConfigMap看做一个特殊的Volume,创建ConfigMap之后,还需要把ConfigMap挂载到Pod中,deployment的yaml文件需要添加如下的内容:
1 | ... |
(顺带一提)配置文件动态加载功能
在腾讯游戏的《可运营规范》里面,有要求服务必须支持动态加载配置的功能,即配置文件变更后,不需要重启服务进程就能生效。在ConfigMap乃至K8s其他的配置管理工具中,当我们成功提交了配置内容变更的指令后,Pod中的配置都是无需重启就能更新的。至于Pod中的服务进程是否有做到动态加载变化的配置文件,还是需要在业务代码中实现。
之所以突然想到这个,是想提醒大家,业务上的功能不能依赖操作系统或者运维工具实现 ;-)。
资源管理
在K8s集群中,一个Node节点的CPU/内存或者其他硬件资源是有限的,我们要怎么给里面的各个容器分配资源呢?
同样,我们在deployment中可以给Pod中的每个容器分配资源。举例说,我要为这个容器分配0.5核的CPU和0.5G的内存空间,具体yaml示例如下:
1 | containers: |
健康检查
使用K8s的探针功能,定期请求demo中的健康检查URI(/health),判断服务是否处在就绪状态:
1 | containers: |
在这个例子中,假如其中一个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 | apiVersion: networking.k8s.io/v1 |
集群外访问
请求示例如下:
1 | [root@minikube yaml]# curl http://apigateway.test/mydemo |
配置汇总
最后来汇总一下deployment中的内容:
1 | apiVersion: apps/v1 |
总结
最初笔者是想通过一个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/