聊聊kubernetes

从一个系统的上线说起 即使是一个最简单的系统,从代码到线上,从运维到升级都要经历以下生命周期

  • 前端代码编译(不同线上环境选择不同配置)
  • 前端代码打包
  • 后端代码编译(不同线上环境选择不同配置)
  • 后端代码打包
  • 搭建nginx部署前端代码
  • 搭建tomcat部署后端代码
  • 前端应用集群
  • 后端应用集群
  • 配置负载均衡
  • 应用监控
  • 故障重启
  • 滚动升级
  • 应用回滚
  • 弹性伸缩
  • 系统运维交接
这些步骤如果都是人工来做,每一步都存在风险
  • 代码编译(编译程序版本不一致,忘记修改配置文件,生产跑测试的配置)
  • 环境搭建(不同环境受限网络环境,操作系统版本等差异,同样的程序在不同环境中可能表现不一致)
  • 负载配置(添加节点需要人工操作,节点故障无法进行故障转移)
  • 应用监控(人工监控不现实)
  • 故障重启(无法进行人工操作,人工滞后于故障)
  • 滚动升级(无法进行人工操作)
  • 应用回滚(需要在方案层面上预留回滚机制,比如保留之前版本的部署包)
  • 弹性伸缩(无法进行人工操作)
  • 运维交接(每个系统都需要交接,代码仓库,打包方法,部署步骤,重启步骤,升级步骤等)
我们在运维交接过程中碰到非常多代码与线上运行不一致,存在代码丢失问题,有可能是之前运维的同事部署后没有提交最新代码,时间久了代码就找不到,特别是对于长时间没有更新的系统,我们无法保证目前的代码就是最新的代码,代码不一致对运维是灾难性的,总之一句话就是
人是不可靠的
理想的情况下,开发提交完代码,一切就与他无关
  • 不用由开发人员个人电脑进行编译,编译应该是统一的平台,统一的环境,统一的标准
  • 环境不能搭建两次,就像你写两边得帆,就算写上几千几万次,你无法找到两次完全一样的
  • 集群是自动化的,什么意思呢,集群应该是一个配置项在那,而不是我们需要针对单机和集群采取完全不一样的安装方式,部署方式
  • 监控是自动化,多样化,可配置,借助图表能够快速判断系统问题
  • 弹性伸缩也要自动化,能够根据系统负载情况自动进行伸缩
  • 故障重启,应用回滚都是自动化,能够自动重启,部署失败能够自动回滚
  • 交接是标准化,所有系统的交接都是一个模板
kubernetes解决了什么问题
  • 保证环境完全一致
  • 提供集群配置
  • 故障重启
  • 弹性伸缩
jenkins解决了什么问题
  • 线上编译,打包,部署
prometheus解决了什么问题
  • 监控
  • 预警
  • 通知
如果能做到以上这些自动化,那么交接的内容就是,如果启动这个流程
  • 某项目流水线
聊聊kubernetes
文章图片

一切的改变源于docker 任何事物的发展都需要标准化,古人早在两千多年前就意识到标准化的重要性,秦始皇统一六国后实行车同轨书文同文就是一种标准化,自此中华名族才实现了真正意义上的大一统。聪明的IT人也在标准化上提出了各种方案
  • W3C标准制定了互联网标准,从此互联网进入高速发展阶段
  • JDBC标准让程序员从适配不同数据库的噩梦中解脱
  • J2EE标准让java成为了最流行的后端开发语言
  • TCP/IP协议奠定了互联网的基础
  • 等等
标准说白了就是对事物的极致抽象,那么一个应用的抽象是什么呢,应用有太多属性了,拍脑袋就可以列举很多
  • 编程语言不一样
  • 运行环境不一样
  • 依赖不同的第三方系统
  • 打包编译的方式不一样
  • 部署方式不一样
  • 重启的方式不一样,systemctl想实现这个目标
  • 等等
而且这种差异性都不是小问题,都是刀刀要命的大问题,如果不解决应用都无法正常运行,如果真的硬要抽象那么只会导致应用变得更加的复杂,聪明的程序员从集装箱中找到了灵感,货物和应用有很多共通的地方
  • 大小不一样
  • 体积不一样
  • 运输工具不一样
  • 装载方式不一样
  • 卸载方式不一样
生物的多样性才能让我们这个星球变得生机勃勃,但多样性也导致了标准化的困难,人类很聪明,设计出了集装箱系统
我们只运集装箱,你们自己想办法把获取装到集装箱
聊聊kubernetes
文章图片

我们把应用装到docker中就跟我们把货物装到集装箱,那么对应用的管理就变为对docker的管理,跟集装箱一样,对货物的管理就变为对集装箱的管理,事情就变得简单多了。
事实上,早在docker之前,我们就在探索标准化,比如虚拟机,拷贝虚拟机也能实现类似docker的功能,那为什么这种方案没有流行起来呢,因为虚拟机太重了,货车装羽毛,效率太低了,docker相比虚拟机有很多优势
  • 轻量级,docker本身占系统资源极少
  • 启动快,指的是docker本身启动很快,相比虚拟机而言
  • 易迁移,docker本质就是一个dockerfile的文本文件,虚拟机动不动就是好几个G
  • 好管理,只需掌握少量命令就能管理应用
对于没有接触过docker的同学,你可以理解为超轻量级虚拟机,虽然docker和虚拟机有本质的区别,但可以让你对docker有个初步的印象。
为什么还有kubernetes 实际上,在没有kubernetes出现之前,虽然docker热度很高,但是很少有企业在生产中真正用起来,因为docker解决了大问题,但小问题还有很多
  • 官方没有管理界面,不友好
  • 无法跨主机通信,导致无法大规模使用
  • 没有监控机制,无法监控应用状态
  • 缺少上层服务,docker只提供基础设施
  • 缺少编排服务,即一个系统往往需要编写多个dockerfile,虽然后面有composer,但这是后话
  • 缺少大公司背书
【聊聊kubernetes】说到底,docker解决了应用部署,应用运维这两个大问题,但是应用管理,应用编排,应用监控,弹性伸缩等等这些和应用相关的问题还没解决,kubernetes的出现才真正意义上完成了大一统,自此docker才真正发挥出其巨大威力。
聊聊kubernetes
文章图片

从这张图可以看出,kubernetes就是一个docker的管理系统,在架构上处于docker之上,应用之下,往上可以为应用赋能,提供应用多种IT资源,往下,可以调度docker,实现应用的统一管理。kubernetes能为应用提供什么呢,其实就是开篇提到的那些应用开发过程中的问题都能在kubernetes中找到解决方案,比如
  • 提供Deployment解决应用部署的问题
  • 提供Service解决应用负载的问题
  • 提供ConfigMap解决不同环境配置的问题
  • 提供Ingress解决应用访问的问题
  • 提供PersistentVolume解决应用存储问题
  • 等等
基本你能想到在开发中碰到的非业务问题都能在kubernetes中找到解决方案,kubernetes就像一个应用的小镇一样,为应用提供各种所需资源
五分钟搭建一个kubernetes环境 这部分内容主要目的是消除大家对kubernetes的恐惧,不要觉得kubernetes很复杂,kubernetes刚发布时确实安装起来比较麻烦,但是近几年有很多方案让kubernetes安装变得非常简单,只需一行命令就能完成安装
  • 设置主机名
#配置主机名 hostnamectl set-hostname k1 #添加hosts 172.16.8.49 k1 172.16.8.50 k2#创建用户 groupadd docker useradd -g docker rke passwd rke#配置互信 ssh-keygen ssh-copy-id rke@k2

  • 安装docker
yum install docker systemctl start docker

  • 安装kubernetes
# 下载rke https://github.com/rancher/rke/releases# cluster.yml nodes: - address: k1 internal_address: k1 role: [controlplane,etcd] hostname_override: k1 user: rke - address: k2 internal_address: k2 role: [worker] hostname_override: k2 user: rke services: kubelet: extra_args: max-pods: "10000" kube-api: service_node_port_range: "1-65535" authentication: strategy: x509 authorization: mode: rbac# 执行rke up ./rke_linux-amd64 up

  • 安装kubectl
wget https://storage.googleapis.com/kubernetes-release/release/v1.20.5/bin/linux/amd64/kubectlinstall -o rke -g docker -m 0755 kubectl /usr/local/bin/kubectl$ kubectl get nodes NAMESTATUSROLESAGEVERSION k1Readycontrolplane,etcd6m53sv1.20.8 k2Readyworker6m52sv1.20.8

  • 安装rancher
docker run --privileged -d --restart=unless-stopped -p 8080:80 -p 4443:443 rancher/rancher

kubernetes架构 总架构
聊聊kubernetes
文章图片

组件
  • master:k8s主节点,负责资源的管理和应用的调度,通常不部署应用
  • node:k8s从节点,负责运行应用
  • API Server:资源控制接口,对k8s所有资源的操作都需要通过API Server,API Server只运行在master节点
  • etcd:集群数据库,集群的数据都保存在etcd中,etcd是一个高性能分布式key-value数据库
  • kubelet:kubelet运行在每一个node上的,负责接收master的调度信息,将应用部署到node上,并且定时上报应用到状态和节点状态为master调度决策提供参考数据
  • kube-proxy:你可以理解为一个nginx(实际上底层实现就是用nginx),负责应用的代理
  • Kube scheduler:调度器,负责决定pod运行在哪个节点上
  • controller manager:保证应用能达到用户期望水平,比如应用设置副本为2,那么controller确保该应用随时随刻都有2个副本
这些组件确保了应用在kubernetes上
  • 最合理的部署
  • 挂了能重启
  • 资源不够可扩展
常用资源
POD
  • Kubernetes 管理的最小单元
  • 一个POD就是一个docker的镜像实例
  • 所有的kuberntes操作都是围绕POD进行的
  • 如果把docker管理的应用形容为乌合之众,一群散兵,那么kubernetes就是正规军,有制度,有阶级,有分工,
  • 够把这群散兵管理好,一个POD就是一个散兵,不管分工再怎么复杂,命令终归是要到达底层。
  • 可以定义各种策略,比如必须保证同一时间多少个POD运行,保证应用“死不了”,kubernetes会自动部署新的POD以满足要求
Deployment 聊聊kubernetes
文章图片

  • 多个POD集合
  • 可以理解为一个系统所需所有POD的集合
  • 方便管理,可以对一组POD进行操作
Service
  • 对外提供统一地址
  • 作用类似于Nginx,可以作为反向代理
ConfigMap
  • 系统的配置文件
  • 不同环境间应该仅有配置环境的差异,configmap的设计实现了以上目标
  • configmap最终是给POD使用的
Ingress
  • 使应用能通过域名进行路由,作用类似nginx servername
kubectl基本命令
# 查看所有节点 kubectl get nodes NAMESTATUSROLESAGEVERSION k1Readycontrolplane,etcd22hv1.20.8 k2Readyworker22hv1.20.8# 查看所有的pod kubectl get pods --all-namespaces# 查看指定命名空间下pod kubectl get pods -n poc# 获取所有资源 kubectl get all --all-namespaces# 获取所有支持的资源类型kubectl api-resources

一个spring boot部署示例 代码
@RestController @RequestMapping("example") public class ExampleController {@GetMapping("header") public Map header(HttpServletRequest request) { Enumeration headerNames = request.getHeaderNames(); Map headers = new HashMap<>(); while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); headers.put(name, request.getHeader(name)); } return headers; } }

代码逻辑很简单,将请求头信息全部打印出来,没有数据库连接
Dockerfile
FROM openjdk:8-jdk-alpine MAINTAINER definesys.com VOLUME /tmp ADD kubernetes-demo-1.0-SNAPSHOT.jar app.jar RUN echo "Asia/Shanghai" > /etc/timezone EXPOSE 8080 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dfile.encoding=UTF-8","-Duser.timezone=Asia/Shanghai", "-jar","app.jar"]

构建镜像
docker build -t 172.16.81.92:8000/poc/kubernetes-example:v1.0 . docker push 172.16.81.92:8000/poc/kubernetes-example:v1.0

Deployment
apiVersion: apps/v1 kind: Deployment metadata: labels: app: kubernetes-example tier: backend name: kubernetes-example namespace: poc spec: replicas: 1 selector: matchLabels: app: kubernetes-example tier: backend strategy: rollingUpdate: maxSurge: 25% maxUnavailable: 25% type: RollingUpdate template: metadata: labels: app: kubernetes-example tier: backend spec: containers: - image: 172.16.81.92:8000/poc/kubernetes-example:v2.0 imagePullPolicy: IfNotPresent name: kubernetes-example ports: - containerPort: 8080 protocol: TCP resources: {} dnsPolicy: ClusterFirst schedulerName: default-scheduler status: {}

通过kubectl进行部署
kubectl apply -f app-deployment.yml

如果只有deployment的话,只能在容器内进行访问,我们可以启动一个busybox的容器,在该容器内用curl命令进行测试
? curlhttp://10.42.1.30:8080/example/header {"host":"10.42.1.30:8080","user-agent":"curl/7.30.0","accept":"*/*"}

busybox是一些linux工具集的容器
这种访问方式存在很多问题
  • 在k8s中,ip是变化的,因为k8s会根据环境的变化对应用进行调度,所以就算你不升级,应用也可能会被重新部署
  • 单点问题,如果有多个副本就无法通过单个应用ip进行访问
  • 负载问题,如果有多个副本就必然面临负载问题
那么解决上面这些问题的方案就是Service
Service 在传统开发中,我们如果部署了多个副本,也就是集群,那么我们会在集群前面部署一个反向代理,比如nginx,不仅可以做代理还可以做负载均衡,在k8s中,我们不需要单独再去搭建这么一个反向代理服务器和负载均衡,k8s中的Service资源就可以实现需求。
apiVersion: v1 kind: Service metadata: name: kubernetes-example-svc namespace: poc spec: ports: - name: app-port port: 8080 protocol: TCP targetPort: 8080 selector: app: kubernetes-example tier: backend type: ClusterIP

通过kubectl进行部署
kubectl apply -f app-svc.yaml

注意到Service的类型type: ClusterIP表明这还是一个集群内部的ip,无法通过外部进行访问,但这个Service解决了负载均衡和反向代理的问题,在其他应用中可以通过名称service名称进行访问,比如
? curl http://kubernetes-example-svc:8080/example/header {"host":"kubernetes-example-svc:8080","user-agent":"curl/7.30.0","accept":"*/*"}

那如何解决外部访问的问题呢,有两种方案,我们先介绍第一种
apiVersion: v1 kind: Service metadata: name: kubernetes-example-svc namespace: poc spec: ports: - name: app-port port: 8080 protocol: TCP targetPort: 8080 nodePort: 18080 selector: app: kubernetes-example tier: backend type: NodePort

NodePort类型的Service可以在主机上直接开端口,作用类似docker中的-p参数,通过指定nodeport可以在外部通过主机ip:nodePort方式进行访问
? curl http://k2:18080/example/header {"host":"k2:18080","user-agent":"curl/7.29.0","accept":"*/*"}

有同学可能会问,如果有多台主机,是不是还要做一层负载,答案是的,你可以用F5也可以用nginx,下面介绍另外一种方式Ingress
Ingress
通过nodePort暴露服务的方式存在几个严重的问题
  • 端口难以管理,如果服务多,每个服务一个端口,还要自行维护端口和服务的映射关系,非常麻烦
  • 通过在主机上挖孔方式,在实现上就不优雅
为了解决NodePort的问题,于是有了Ingress,Ingress通过域名进行路由,也就是可以给每个service指定域名,通过域名进行访问就能路由到相应的service上,所有的流量都是通过kube-proxy进行转发,因此无需在主机上开端口
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: annotations: nginx.ingress.kubernetes.io/proxy-body-size: 500m nginx.ingress.kubernetes.io/proxy-connect-timeout: "300" nginx.ingress.kubernetes.io/proxy-read-timeout: "600" nginx.ingress.kubernetes.io/proxy-send-timeout: "600" name: kubernetes-example-svc-ingress namespace: poc spec: rules: - host: kubernetes-example.definesys.com http: paths: - backend: serviceName: kubernetes-example-svc servicePort: app-port

kubernetes-example.definesys.com指定了域名,通过serviceName匹配service名称,通过Ingress我们可以直接用域名访问,当然,这个域名需要加到dns系统中,如果有多个节点,可以做dns负载也可以再做一层nginx负载,如果是自己本地测试,可以直接配置hosts文件就可以直接访问
ConfigMap ConfigMap是kubernetes中非常天才的设计方案,介绍ConfigMap之前我们想一个问题生产环境的war包和测试环境的war包区别在什么,答案是配置文件,配置文件决定了应用的环境属性,应用可以分为两部分,一部分是程序,一部分是配置文件,按照docker的原则,测试和生产只能是配置文件不一样,程序应该是完全一样的,也就是说,同一个镜像,在测试环境运行的时候应该用的是测试环境的配置文件,在生产环境运行时应该用的是生产环境的配置文件,如果是docker,我们可以通过挂载卷实现
#测试 docker run -v /data/dev/application.properties:/u01/webapps/application.properties -d app #生产 docker run -v /data/prod/application.properties:/u01/webapps/application.properties -d app

在kubernetes中,可以通过ConfigMap实现,ConfigMap也是一个key-value结构的文件
一个jenkins的configmap文件部分内容
apiVersion: v1 data: apply_config.sh: |- mkdir -p /usr/share/jenkins/ref/secrets/; echo "false" > /usr/share/jenkins/ref/secrets/slave-to-master-security-kill-switch; cp -n /var/jenkins_config/config.xml /var/jenkins_home; cp -n /var/jenkins_config/jenkins.CLI.xml /var/jenkins_home; cp -n /var/jenkins_config/hudson.model.UpdateCenter.xml /var/jenkins_home; config.xml: |- 0 NORMAL .... hudson.model.UpdateCenter.xml: |- default https://updates.jenkins.io/update-center.jsonjenkins.CLI.xml: |- false plugins.txt: "" kind: ConfigMap metadata: name: jenkins namespace: poc

data部分定义的就是ConfigMap的数据部分,注意到这个文件data中的key都是文件名,是的,每个key都可以以文件的形式挂载到容器内,文件的内容就是value,我们将之前的代码修改下
@RestController @RequestMapping("example") public class ExampleController { @Value("${kubernetes.demo.env.name:}") private String envName; @GetMapping("header") public Map header(HttpServletRequest request) { Enumeration headerNames = request.getHeaderNames(); Map headers = new HashMap<>(); while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); headers.put(name, request.getHeader(name)); } headers.put("envName", envName); return headers; } }

代码注入了配置文件里的配置项目kubernetes.demo.env.name
  • 准备configMap.yaml
apiVersion: v1 data: application.properties: kubernetes.demo.env.name=dev kind: ConfigMap metadata: name: example-configmap namespace: poc

  • kubectl导入到kubernetes中
kubectl apply -f configMap.yaml

  • 修改应用的Dockerfile选择从指定路径下读取配置文件
FROM openjdk:8-jdk-alpine MAINTAINER definesys.com VOLUME /tmp ADD kubernetes-demo-1.0-SNAPSHOT.jar app.jar RUN echo "Asia/Shanghai" > /etc/timezone EXPOSE 8080 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dfile.encoding=UTF-8","-Duser.timezone=Asia/Shanghai", "-Dspring.config.location=/app/", "-jar","app.jar"]

增加了--spring.config.location=/u01/config/application.properties启动参数
重新构建镜像
docker build -t 172.16.81.92:8000/poc/kubernetes-example-configmap:v1.0 .

  • 修改Deployment,将ConfigMap挂载到容器内
apiVersion: apps/v1 kind: Deployment metadata: labels: app: kubernetes-example tier: backend name: kubernetes-example namespace: poc spec: replicas: 1 selector: matchLabels: app: kubernetes-example tier: backend strategy: type: RollingUpdate template: metadata: labels: app: kubernetes-example tier: backend spec: containers: - image: 172.16.81.92:8000/poc/kubernetes-example-configmap:v1.0 imagePullPolicy: IfNotPresent name: kubernetes-example volumeMounts: - mountPath: /app/ name: configmap-data ports: - containerPort: 8080 protocol: TCP resources: {} dnsPolicy: ClusterFirst volumes: - name: configmap-data configMap: name: example-configmap schedulerName: default-scheduler status: {}

Jenkins 一个应用的部署需要至少准备以下几个yaml配置文件
  • Deployment
  • Service
  • Ingress
如果是手写不仅效率低,易于出错,也难以管理,也不建议将配置文件放在项目中,原因有以下几个
  • 每个项目都需要copy一份配置进行修改,繁琐
  • 如果不懂k8s会增加其学习成本
  • 如果k8s升级,配置文件有可能需要跟着变化,就需要修改所有的配置文件
我们需要一个工具帮助我们完成配置文件的生成并且部署到kubernetes环境中,Jenkins可以帮助我们完成一系列的自动化操作
#!/bin/bashset -ev_app_name=kubernetes-example v_module=. v_app_host=kubernetes-example.definesys.com v_k8s_namespace=poc #v_app_name=$appName #v_module=$module #v_app_host=${v_app_name}.fuyaogroup.com #v_k8s_namespace='fone-application' v_app_version=`date +"%Y%m%d%H%M%S"` v_harbor_prefix='172.16.81.92:8000/poc/'if [ "$v_app_host" == "" ]; then v_app_host=${v_app_name}.definesys.com fiecho "app name====>"$v_app_name echo "app version ====>"$v_app_version echo "module====>"$v_module echo "workspace====>"$WORKSPACE echo "profile====>"$v_profile echo "app host====>"$v_app_hostv_workspace=$WORKSPACE/workspacemkdir -p $v_workspace cd $v_modulemvn clean package -Dmaven.test.skip#临时存放jar包目录 v_build_directory_name=build v_build_directory=$v_workspace/$v_build_directory_name v_app_jar=target/*.jarv_app_jar=`basename target/*.jar`rm -rf $v_build_directory mkdir -p $v_build_directorycp -rf target/$v_app_jar $v_build_directory cd $v_build_directory#v_app_name=${v_app_jar%.*} #v_app_name=${v_app_name%-*}echo "app jar name=====>"$v_app_jar echo "app name=====>"$v_app_name#docker镜像构建 v_image_tag=$v_harbor_prefix$v_app_name:$v_app_version cat 1>Dockerfile < /etc/timezone EXPOSE 8080 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dfile.encoding=UTF-8","-Duser.timezone=Asia/Shanghai", "-jar","app.jar"] EOF docker build -t $v_image_tag . -f Dockerfile docker push $v_image_tag docker rmi -f $v_image_tag#部署kubernetes cat 1>app-deployment.yaml <

    推荐阅读