“ 一文让你成为Docker和K8s的懂王!”
最近,K8s和Docker的离婚案闹得大伙心慌慌的。两位改变世界的神仙打架,咱码农会不会被误伤呢?万幸的是,Docker生的娃(镜像)K8s还是会继续照料,只要Docker没变心(继续遵循Open Container Initiative,OCI标准),我们还是可以继续安心用Docker生成镜像,然后部署在K8s上。
两位神仙的恩恩怨怨暂且不表,留待最后再谈。先来跟大伙科普一下这俩主是干嘛的,为啥改变了世界,对我们又有啥用呢?
01 Docker能咋伺候咱?还记得咱小时候家里不富裕,买个电脑都是找人把配件东凑凑、西凑凑的,拼成一台兼容机,然后装上个Windows的。那个时候我们的记忆是,拼装硬件几十分钟搞掂,但装个Windows弄个好几小时。后来有了Ghost,结果就鬼那么快把Windows装好了。
Ghost是个什么鬼,居然有这等魔力?Ghost的法术就是镜像(Image)。把别人装好系统的硬盘拷贝压缩成镜像文件,然后在新的电脑上解压拷贝,搞掂。还能把别人电脑里的好东西一起搬过来。这就是镜像的魔力。
Docker就是掌握了镜像的魔力!我们开发出来的应用程序,是需要一个运行环境的。在这个运行环境中,OS、用户、用户组、权限、基础软件、证书等等样样都得有,这个配置过程往往并不简单,而且应用程序经常要搬家,开发镇、测试镇、生产镇、灾备镇每天可能得跑几回。
没有Docker的时候,应用部署到不同环境就像带着行李入住各个镇的酒店,每次都要收拾打包,经常丢三落四的。每个镇的酒店的摆设和条件都不一样,也要不断调整和适应。上云以后,搬家更频繁了,适应和收拾也要更快了,想想头都大。
因为这个过程是很重复的,所以我们发明了一些自动化打包和部署工具,试图让电脑帮我们把这些繁杂的过程搞掂。但自动化工具只会干计划好的重复的活,条件不一样,就得再弄新的脚本,结果脚本越来越多。而且如果过程比较复杂的时候,自动化也不好使,有时也会闹脾气,半路撩杆子。
有了Docker,就像开着房车四处逛,应用软件所需要的所有细软都在车上,到哪都不需要重新打包收拾,到每个镇上借点水电就好。每天跑多少趟都不嫌累。
Docker就是能让你把应用程序和运行环境完整地封装在一起,可以把运行环境带在身上到处走,不怕水土不服。
这房车怎样布置呢?开发人员只要设计好了Dockerfile,Docker就会照着布置。下面是个样例:
FROM openjdk:8-alpine
ARG NAME
ARG VERSION
ARG JAR_FILE
LABEL name=$NAME \
version=$VERSION
# 设定时区
ENV TZ=Asia/Shanghai
RUN set -eux; \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime; \
echo $TZ > /etc/timezone
# 新建用户java-app
RUN set -eux; \
addgroup --gid 1000 java-app; \
adduser -S -u 1000 -g java-app -h /home/java-app/ -s /bin/sh -D java-app; \
mkdir -p /home/java-app/lib /home/java-app/etc /home/java-app/jmx-ssl /home/java-app/logs /home/java-app/tmp /home/java-app/jmx-exporter/lib /home/java-app/jmx-exporter/etc; \
chown -R java-app:java-app /home/java-app
# 导入启动脚本
COPY --chown=java-app:java-app docker-entrypoint.sh /home/java-app/docker-entrypoint.sh
# 导入JAR
COPY --chown=java-app:java-app target/${JAR_FILE} /home/java-app/lib/app.jar
USER java-app
ENTRYPOINT ["/home/java-app/docker-entrypoint.sh"]
EXPOSE 8080
这个Dockerfile就是告诉Docker怎么制作一个镜像,它包含了以下内容:
1. 指定一个基础镜像(含OS和基础软件,如JDK)
2. 设定正确的时区
3. 创建用户和配置权限
4. 设置JVM参数、Java System Properties、程序自定义的参数
5. 上传应用jar文件
6. 启动应用
7. 指定Web程序的接口
创建镜像:
dockerbuild.-tsample-app:latest
通过Docker build命令构建一个镜像,并以sample-app:latest给它标签(latest是版本)。
试运行:
dockerrun-p80:8080 sample-app:latest
通过Docker run命令运行镜像中的应用,并把容器里的8080端口通过80端口暴露出来,现在可以通过http://localhost来访问该应用了。
推送到镜像仓库:
docker push sample-app:latest
通过Docker push命令把镜像从本地推送到公共镜像仓库。
从镜像仓库拉取并运行:
docker pull sample-app:latest
docker run -p 80:8080 sample-app:latest
在任何可运行Docker的服务器,通过Docker pull从公共镜像仓库拉取镜像,并运行。
所以整个过程就是:
在本地(含有已能成功运行应用的开发环境):构建Docker镜像 -> 在本地运行镜像进行验证 -> 把镜像推送到镜像仓库;
在服务器:从镜像仓库拉取镜像 -> 运行镜像。
就这么简单!
如果我们有几个应用要一起运行,还可以组个车队:
version: '3'
services:
web:
build:nginx
ports:
- "80:8080"
redis:
image:redis
通过编写以上的docker-compose.yml配置,可以把若干个Docker容器组装在一起运行,在这个例子中,有web(镜像是nginx,容器端口8080映射为80)和redis(镜像是redis)两个容器一起运行。通过docker-compose up命令启动,docker-compose down命令停止。
容器技术其实并不新鲜,所谓容器就是特殊的线程,分配给一组程序独立运行,和其他的线程完全隔离。Docker的最大优势是引入了镜像(Image),让开发者可以把应用运行所需要的环境,包括OS打包到一个镜像中,在所有支持Docker的服务器上运行和迁移。镜像的好处是:
它简化了构建、打包和部署过程。一般来说,全量变更比增量变更要简单得多,因为你不需要关心两个版本间的差异。Docker镜像部署就是全量变更(虽然Docker镜像是分层的,只有有变动的层会被拉取或推送,以节约时间和带宽,但这个过程由Docker管理,开发者不需要关心);
由于Docker镜像包含应用运行的整个环境并且可以运行在所有支持它的Linux服务器上,对底层的服务器和OS没有依赖关系。保证了环境的隔离性和一致性,并完美地切合了不可变基础设施的原则。
通过Docker镜像,可以以秒级这样的速度在新的服务器上配置、部署和运行应用,确保了水平扩展的能力,这也是云原生的要求。
在Windows上安装Docker最简单的方式就是下载和安装Docker Desktop。
02 K8s又是何方神圣?Docker那么厉害,咋后来名声又都给了K8s了呢?听说Kubernetes在希腊语是领航员的意思,它要带咱去哪飞啊?
过去,开发人员觉得只生一个娃好,只需要开发一个单体应用,把所有精力、钱财和资源都给了这个应用,让它过关斩将,一夫当关,万夫莫开,应对各类妖怪(俗称业务需求和用户请求)。但时代不一样了,妖怪的种类和数量都越来越多,一个应用单枪匹马,纵然长出了三头六臂都难以招架。
开发人员就把原来的单体应用按业务服务类型给拆了,而且用docker-compose让各应用组个队,一起上,但还是招架不住,恨不得有分身术,以一化十,以十化百。
开发人员决定开发出不同技能的更小粒度的应用程序,然后让它们组个大兵团,不同技能的应用精心修炼好一门绝技,各司其职,负责打不同的怪物,而且掌握分身术,随时变出更多的分身,应对更多怪物。
这个时候,事情就复杂了,需要有个大统领指挥,收放自如。这差事,本来Docker也想揽,但没揽住。docker-compose管个小家庭凑合。这么大的营生,只能望而却步。
后来半路*出个程咬金,K8s横空出世,当了这个大统领。人多是不是,活杂是不是,都不在话下。引无数码农竞折腰。
K8s咋那么神呢?咱先来看看大神有啥五脏六腑(这张图对于理解K8s很重要!):
- Pod - 给容器们住的房子,可以几个容器一起住,也可以一个容器占个房子,每个容器身怀自己的应用程序——含该应用的Docker镜像。在同一间房子里,容器们一起吃住,共用一套WIFI(共享网络),也可以给对方打免费的内部电话(通过localhost访问不同容器的port端口)唠嗑,共用储物柜(共享存储)。容器的孪生兄弟们来了,不够住,大统领随时给安排更多的房子(水平扩展多个Pod的副本)。
- Deployment - 给容器的房子装修的施工队,我们画张图纸(包含房子Pod的名字,标签,要部署的Docker镜像,房子Pod的副本数量Replicas等),Deployment拉上施工队就干活,保质保量。
- Service - 大统领日理万机,要分配活给容器们,总不能一间间房子找人。于是找些楼长来管管事。楼长知道哪个妖怪来了找哪些房子的容器(某个微服务),他掌握了和容器的接头暗号(房子Pod的标签和容器的Port端口)。
- Ingress - 每个楼长都跑出来太乱了,大统领又找了个大内总管,哪个妖怪来了都把大内总管推到前面(统一的URL访问入口),大内总管有所有楼长的电话,知道哪个妖怪来了找谁(通过URL不同的path把请求发到相应的Service中)。可以理解为Services的Service。
- Node - 房子所在大楼的桩,桩越多,大楼越结实,可以安排更多的房子(一个Node就是一台服务器,通过增加Node可以加强整个集群的能力)。
K8s大统领还有一个大招,就是咱要干啥只需要给他吩咐,他就帮咱干好,不需要告诉他咋做。比方说,咱要他盖房子:
Deployment (示例文件名:deployment.yaml):
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-sample-app
labels:
app: k8s-sample-app
spec:
selector:
matchLabels:
app: k8s-sample-app
replicas: 5
template:
metadata:
labels:
app: k8s-sample-app
spec:
containers:
- name: k8s-test
image: k8s-test:latest"
imagePullPolicy: Always
ports:
- containerPort: 5000
protocol: TCP
文件里的关键要素:
- 这是一个Deployment文件,用来创建Pod的模板。
- 它的标签(label)是app: k8s-sample-app。Service会用这个标签来找Pod,并把请求转给这些Pod。
- 用来部署和允许的容器镜像是k8s-test:latest。
- 容器的端口是5000。
- 水平扩展(replicas)是5,意味着将有5个Pod副本运行。
安排个楼长:
Service (示例文件名:service.yaml):
apiVersion: v1
kind: Service
metadata:
name: k8s-sample-svc
annotations:
cloud.google.com/neg: '{"ingress": true}'
spec:
ports:
- name: host1
port: 80
protocol: TCP
targetPort: 5000
selector:
app: k8s-sample-app
type: NodePort
文件中的关键要素:
- 这是类型为NodePort的Service(Service有分NodePort、ClusterIP和Load Balance三类,这里不展开了)。它将把Pod容器的端口暴露出来,供外界访问。
- 它的名字是k8s-sample-svc。
- 它会把80端口的访问转发到内部容器端口5000。
- 它会把请求转发给所有带k8s-sample-app标签的Pod。
安排个大内总管:
Ingress (示例文件名:ingress.yaml):
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: ilb-k8s-ingress
annotations:
ingress.gcp.kubernetes.io/pre-shared-cert: "self-signed"
kubernetes.io/ingress.class: "gce-internal"
kubernetes.io/ingress.allow-http: "false"
spec:
rules:
- http:
paths:
- backend:
serviceName: k8s-sample-svc
servicePort: 80
文件中的关键要素:
- 它是一个Ingress。
- 它的名字是ilb-k8s-ingress。
- 它的对外端口是80。
- 它将把请求转发到名为k8s-sample-svc的Service。
我们可以通过以下命令创建以上的所有元素:
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml
通过这些配置,外部请求的路径将是Ingress -> Service -> Pods。
如果Node的数量是3的话,那么意味着底层会有3台服务器在支撑整个集群。这5个Pod将由K8s自动分配到这些Node上(用前面的图做示例,很可能是2个在Node A、2个在Node B、1个在Node C)。
从这一点我们可以看到Pod的水平扩展和Node没有直接的关系(当然我们也可以要求某些Pod只运行或不允许运行在某个Node上,这里就不展开了)。
我们可以随时为K8s集群增减Node,而不影响正在运行的Pod(应用)。
从这个过程我们也能看到,我们只需要通过yaml文件告诉K8s我们想要的最终状态,K8s就会帮我们创建和调整,我们不需要告诉K8s怎么做,这也是声明式配置的力量(就像SQL,我们只需要说要什么,不需要管怎么做到)。
现在我们来看看房子建得咋样了:
kubectl get pods
它会罗列所有的Pod及其状态。
窥探房子里面的情况,包括它建在哪个Node上:
kubectl describe pod [POD_NAME]
查看容器的日志:
kubectl logs [POD_NAME]
也可以对Service、Ingress和Node进行类似的操作:
kubectl get svc
kubectl get ingress
kubectlgetnodes
kubectldescribesvc[SERVICE-NAME]
kubectl describe node [NODE-NAME]
K8s隔离了应用和服务器。我们可以在K8s集群上部署数十个甚至上百个微服务,K8s会帮我们妥善管理这么一个繁杂架构,包括某些服务的伸缩。
这里面涉及的复杂的网络、部署、集群、状态、版本升级、储存等都统统交给了K8s。这也是把简单留给用户,把复杂留给自己的设计。作为应用开发者,可以把更多精力放在开发上。这也是K8s大受欢迎的原因。
K8s提供以下能力:
1. 跨越多台服务器的集群管理。
2. Pod为应用提供了稳健和隔离的运行环境。
3. Pod的伸缩。
4. 应用版本升级的滚动发布。
5. Pod之间的负载均衡。
6. 为不同服务提供单一访问入口。
如果你的应用比较简单,只是拆成了若干个服务,那么docker-compose可以解决问题。如果你的应用是真正的微服务架构,有数十个甚至上百个服务,某些服务又需要水平扩展和伸缩,需要服务发现和负载均衡,那么K8s就能帮你管好这么一个繁杂的架构。
K8s可以帮你在不需要关心底层服务器配置和部署的情况下,让你的繁杂架构有序运作,并充分利用底层服务器的资源,它本身就是一层操作系统。目前各主流云厂商都提供了K8s服务,提供介于IaaS和PaaS之间的能力。
另外有一点要特别指出的是,容器的本质就是线程,一个Docker容器虽然自带OS,但运行起来是单线程的,并不能模拟一台虚拟机。而K8s的Pod能为部署在里面的容器提供不同的线程和共享的网络和存储,它才是一台虚拟机的代表,并能水平扩展。
当然,K8s是复杂的,特别对于初学者,光是看到那些新概念就晕。所以,只有当你真的有一个繁杂的微服务架构需要管理,才需要考虑K8s。
03 总结在Docker和K8s的加持下,我们可以通过以下方式部署和运行我们的应用程序:
- 在本地开发电脑上安装Docker Desktop。
- 为应用创建一个Docker镜像,并推送到镜像仓库;
- 在服务器上,安装Docker。然后从镜像仓库拉取镜像并运行。
- 可以通过docker-compose运行一组的镜像(应用);
- 如果需要管理繁杂的微服务架构,K8s将是首选之一。
最后讲讲K8s和Docker“离婚”的事情,其实并不是K8s和Docker的直接恩怨,而是Docker和整个PaaS江湖的恩怨。
容器技术并不是什么新鲜事物,它本质上就是隔离的线程,基础是Linux的Cgroups、Namespace技术,存在多时。
而能帮助大家把应用程序以“沙盒”这样的隔离形式托管在不同运行环境的PaaS能力,一直是各大厂商的兵家必争之地。云兴起以后,这样的需求变得更加急切。
Docker的横空出世,一举成名就在于它以镜像方式创建容器,大大简化了应用托管过程。由于得到业内的迅速追捧,Docker也想乘胜追击,拿下PaaS霸主的地位。毕竟,Docker如果仅仅是一个镜像打包工具,它无法商业化(zuan qian),它必须争夺容器编排市场。它也在打造和收购自己的容器编排工具,前文提到的docker-compose就是其中一员,还有Swarm。
K8s出来后,一举成为容器编排的一哥。早期它的编排能力也是依托在Docker的运行时(runtime)引擎。由于Docker也想争夺这个市场,它的运行时架构变得越来越复杂,也不支持通用的容器运行时接口(ORI),影响了K8s的架构和效率。
于是K8s选择了抛弃Docker的运行时引擎。
由于K8s对容器镜像的调用早已经从Docker实现换成更通用的OCI(Open Container Initiative)接口,所以只要Docker继续遵循OCI,通过Docker打包的镜像在K8s上部署和运行就不会受到影响,这也是为什么说这次“离婚”案对开发人员没有影响。
觉得文章不错,顺手转发给朋友们吧。
近期必读:
关于作者
刘华(Kenneth)
- 就职于世界500强银行,负责基金服务业务软件开发与交付
- 敏捷、精益、DevOps专家
- 公众号“敏于思 捷于行”博主
- 精通极限编程、Scrum、看板方法、测试驱动开发、持续集成、行为驱动开发、DevOps工具栈
- 曾在GDevOps、DevOpsDays Meetup、中国软件技术大会、ArchSummit、Top 100等论坛发表主题演讲
- 阿里云、谷歌云认证架构师
- 著有《猎豹行动:硝烟中的敏捷转型之旅》一书和专栏《软件交付那些事儿》