刘华:戏说Docker和K8s,一文让你成为懂王

 一文让你成为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程序的接口

创建镜像:

docker build . -t sample-app:latest

通过Docker build命令构建一个镜像,并以sample-app:latest给它标签(latest是版本)。

试运行:

docker run -p 80: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的服务器上运行和迁移。镜像的好处是:

  1. 简化了构建、打包和部署过程。一般来说,全量变更比增量变更要简单得多,因为你不需要关心两个版本间的差异。Docker镜像部署就是全量变更(虽然Docker镜像是分层的,只有有变动的层会被拉取或推送,以节约时间和带宽,但这个过程由Docker管理,开发者不需要关心);

  2. 由于Docker镜像包含应用运行的整个环境并且可以运行在所有支持它的Linux服务器上,对底层的服务器和OS没有依赖关系。保证了环境的隔离性和一致性,并完美地切合了不可变基础设施的原则

  3. 通过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
 
kubectl get nodes
 
kubectl describe svc [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服务。

另外有一点要特别指出的是,容器的本质就是线程,一个Docker容器虽然自带OS,但运行起来是单线程的,并不能模拟一台虚拟机。而K8s的Pod能为部署在里面的容器提供不同的线程和共享的网络和存储,它才是一台虚拟机的代表,并能水平扩展

当然,K8s是复杂的,特别对于初学者,光是看到那些新概念就晕。所以,只有当你真的有一个繁杂的微服务架构需要管理,才需要考虑K8s。

03

总结

在Docker和K8s的加持下,我们可以通过以下方式部署和运行我们的应用程序:

  1. 在本地开发电脑上安装Docker Desktop。

  2. 为应用创建一个Docker镜像,并推送到镜像仓库;

  3. 在服务器上,安装Docker。然后从镜像仓库拉取镜像并运行。

  4. 可以通过docker-compose运行一组的镜像(应用);

  5. 如果需要管理繁杂的微服务架构,K8s将是首选之一。

番外

关于Docker与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等论坛发表主题演讲

  • 阿里云、谷歌云认证架构师

  • 著有《猎豹行动:硝烟中的敏捷转型之旅》一书和专栏《软件交付那些事儿》

关注公众号看其他原创作品

敏于思 捷于行 

坚持原创高质量软件交付相关文章

觉得好看,点个“点赞”、“在看”或转发给朋友们,欢迎你留言

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值