深入理解POD的工作原理(下)

到目前为止,我们在部署的应用程序,POD中只有一个容器,但是如我们前边多次提到,对于关系紧密的多个容器来说,比如需要通过localhost来通信,或者需要通过IPC来进行数据交互,那么这些容器就应该被部署到相同的POD中。今天我们就来通过实际的业务场景,来看看如何在同一个POD中运行多个容器实例。

安全是非功能需求中非常特殊的一项,每个项目都需要考虑安全,并且还必须体系化的考虑安全。说到安全,我们一般围绕安全的黄金法则AAAA(Authentication,Authorization, Audit和Accounting)来体系化的防范,我们会讨论认证,授权,加密,web安全,容器安全,基础设施安全等等。本质上这些安全措施都属于表象,笔者认为大部分系统或者应用最本质的东西其实是数据,而评价一个系统的安全体系是否做的好,标准也很简单:构建的安全防御体系是否围绕核心的业务数据。终端用户使用我们提供的产品和应用过程中,本质上就是在企业进行数据交换,比如用户在淘宝上搜索商品,下单,用户使用支付宝进行交易,都是以数据的形式在进行资产的转移。因此从某个层面来说,安全的本质就是保护数据被合法的使用。那如何理解被合法的使用这个词呢,其实就是我们说的:机密性,完整性和可用性(CIA)这三个方面。机密性,完整性和可用性是系统安全最基础的三原则。比如我们需要面对的DDOS攻击,本质上就是可用性受到了影响,可用性有很多内涵,大家需要注意安全三要素的这个维度。

由于安全性是一个体系性的工程,因此我们不可能一蹴而就,通常情况下最基本的一些安全措施要到位,比如数据传输和存储的机密性。特别对于传输过程的敏感数据来讲,加密属于常规操作,而加密和数据的完整性两个特征都需要保证,因为无论是数据被破解或者,数据被篡改,我们希望能够尽早的发现。HTTPS是保证数据在传输过程中的机密性和完整性的常规操作,接下来笔者会结合给我们的SpringCloud应用增加HTTPS访问支持,来介绍如何在POD中部署多个容器实例。虽然我们可以通过修改k8ssample应用程序的代码来达到相同的效果,但是通过在POD中增加一个容器实例这种部署方式,我们实现了一种称作是组合模式的面向对象设计模式。如果你阅读过笔者关于容器深入理解的前边两篇文章,你应该对边车模式有印象,其实这种通过增加一个容器来扩展应用程序能力的方式,就是边车模式(sidecar)。

注:边车模式并不是一项新的技术,这是一项Kubernetes集群本身就有的能力,我们在介绍Kuberntes体系结构的时候,特别是详细讨论如何解决多个容器具有紧密关系这种场景下部署问题的时候,提到过POD这种抽象的必要性,而POD要做的就是让运行在其中的多个容器实例共享一些命名空间。比如说网络命名空间,这样容器之间就可以通过localhost或者IPC来进行通信。而为了解决谁先hold住这些命名空间让POD中的应用容器实例能够加入进来,Kubernetes提供了一种叫infras的容器,这个容器是最先被创建起来,并hold住命名空间,后续启动的业务应用容器实例加入到命名空间即可。这种方式解决来多个业务实例之间可能形成的拓扑关系,提升了容器实例的独立性,而这个infras容器其实就是边车模式的体现,希望大家能注意到这点。

接下来我们来规划一下如何来给k8ssample应用增加HTTPS的处理能力,首先我们需要运行一个反向代理的容器(sidecar容器),这个反向代理容器实例接受HTTPS流量,然后基于配置的证书进行必要的安全检查,如果通过,就通过http协议来调用我们的k8ssample提供的hello接口,并将返回结果通过反向代理容器和客户端之间的https链路返回给客户端。要说能提供这种反向代理能力的组件,非Envoy莫属,这是一个高性能的开源服务代理组件,起源于Lyft公司,目前已经是CNCF(云原生基金会)的毕业项目,因此选择Envoy是最佳的方案,另外这业务笔者后续关于Istio主题的文章打下基础。

【通过Envoy代理扩展SpringCloud应用的功能】

作为系统设计人员或者架构师,我们必须时刻有清晰的系统部署结构图,因此咱先来看看,如果引入这个Envoy的代理,我们的部署结构长什么样。如下图所示,k8ssample应用程序的POD实例中会有两个容器,一个是SpringCloud应用本身,一个是新增加的Envoy代理。SpringCloud应用没有任何变化,会继续对外提供HTTP服务,而如果我们通过HTTPS协议来访问,请求会先被Envoy代理容器处理。具体来说,对于每个HTTPS请求,Envoy代理会创建一个新的HTTP请求并发送给SpringCloud应用,而由于两个容器处于同一个POD中,因此共享了网络命名空间,就可以通过localhost来进行高效的通信。这一点希望大家特别关注,正是由于这个通过localhost通信机制的存在,这种通过增加一个容器来扩展系统能力的方式才不会造成不可接受的性能开销。因为从访问链路上看,增加Envoy容器代理是访问链路变长,因此应用的性能势必受到影响,但是通过localhost的方式,这种额外的开销就回到可以接受的范围了。

《图1.1 增加Envoy代理后SpringCloud应用的部署架构图》

如上图所示,Envoy除了提供对HTTPS能力的支持,也提供一套管理接口,通过这套管理接口,我们就可以实现整个横向能力的管理(比如说HTTPS的证书更新等)。而这个叫Envoy的容器,是用C++写的高性能的服务代理,是服务网格所依赖的核心组件,而服务网格可以说是Kubernetes平台提供的扩展能力集大成者,大家可以期待一下笔者后续关于服务网格(Service Mesh)的系列文章。

接下来我们来创建YAML文件,来在同一个POD中运行两个容器,一个是SpringCloud,一个是Envoy代理。幸运的是,Envoy代理的作者已经在Dockerhub上共享了Envoy镜像,我们可以直接从Dockerhub上拉取这个镜像,当然你如果属于那种对任何事情的运行原理都好奇的同学,也可以下载源代码,自行打包。当然有了Envoy这个镜像,我们还需要配置证书,为了让这个学习的过程体感更好,笔者提前准备好了容器镜像,并且已经上传到了Dockerhub上,笔者后续关于Envoy的介绍,都会基于这个叫qigaopan/yunpan-ssl-proxy-image的容器镜像展开。大家可以自行docker pull这个容器镜像,当前版本是v1.0。

稍微介绍一下Envoy代理的配置,由于我们使用的是HTTPS协议,因此我们必须有private key(私钥)和证书的配置信息,HTTPS流量通过443端口监听,当HTTPS请求进入Envoy代理后,Envoy代理会结束TLS连接,然后将请求以HTTP协议的方式发送给localhost的8085端口,这个端口上运行着SpringCloud的hello服务。另外Envoy代理也配置了管理接口,运行在9901端口上。有了上边的这些铺垫,我们来创建应用部署的YAML文件,如下图所示:

《1.2 运行边车容器的SpringCloud应用部署YAML文件》

如上图所示,我们定义了POD的YAML文件,POD的名称叫yunpan-ssl,包含连个容器yunpan和envoy,虽然说这个YAML文件比起来我们在(中)篇中介绍的YAML文件,但是其实变化也不算太大,读者如果看过前边的文章,应该不难理解。废话不多说,咱直接在本地的集群把这个新的YAML文件给部署上去,在对应的文件夹中运行kubectl apply -f yunpan-ssl.yaml,然后我们可以通过kubectl get和kubectl describe命令来查看POD会否被成功的创建并运行起来。如下边的输出所示,POD已经成功运行起来了:

➜  kubernetes kubectl get pod -o wide

NAME        READY  STATUS    RESTARTS  AGE    IP            NODE      NOMINATED NODE  READINESS GATES

yunpan      1/1    Running  2          16h    172.17.0.12  minikube           

yunpan-ssl  2/2    Running  0          105s  172.17.0.13  minikube           

通过Ready列的信息我们可以看到,POD中有两个容器准备好了,基于我们的YAML文件配置,我们可以推断容器的镜像被成功的运行。但是我们只能推断,为了验证应用真的运行起来,并且同时对外提供了HTTP和HTTPS服务,我们需要来实际的访问一下,请继续阅读。

【访问新版本的SpringCloud服务】

接下里我们来访问服务提供的接口,由于yunpan-ssl这个POD提供了三个对外的端口,因此我们使用port-forward(port forward是最方便用来调试的访问容器服务的方法,但是也是访问链路最长的一种,很容易不同。笔者提供了三种访问容器服务的方法,具体可以参考上篇文章)的时候,需要把三个端口都在本地代理,具体的命令是:kubectl port-forward yunpan-ssl 8085 8443 9901,在自己本地macOS上运行这个命令,就可以访问通过这些端口号提供的服务了。

首先,我们来验证一下HTTP协议还是继续工作,新开一个命令行窗口,运行: curl http://localhost:8085/v1/yunpan/k8s/hello,在笔者的macOS上输出如下:

➜  ~ curl http://localhost:8085/v1/yunpan/k8s/hello

被访问机器的Host:yunpan-ssl  ,IP地址是:172.17.0.13%

从输出结果看,符合预期,我们的应用继续对外提供HTTP的服务,接着我们来测试一下新增加的功能,通过HTTPS来访问服务,因为HTTPS流量会先到达Envoy代理,并且通过Envoy代理结束TLS连接,并发送HTTP请求给服务实例,这个过程比直接访问要复杂,我们先来验证一下,如下是笔者本地输出的结果:

➜  ~ curl https://localhost:8443/v1/yunpan/k8s/hello --insecure

被访问机器的Host:yunpan-ssl  ,IP地址是:172.17.0.13%

从输出的记过可以看到,我们的SpringCloud应用现在支持HTTPS协议访问了,并且通过增加容器的这种扩展方式,不对原有代码做任何改变,是组合模式这种设计模式的生动体现。细心的读者可能注意到了,上边的命令使用了--insecure这个选项,我们来详细介绍一下。

由于我们这边文章中使用的证书属于self-signed类型的证书,并且这个证书笔者签发给example.com这个域名,但是我们访问服务是通过本地的由port-forward创建的代理,使用的是localhost这个域名,因此严格来说,我们访问的是域名和证书签发的域名不一致,因此如果你不加这个选项的话,TLS机制会报错。

我们部署的叫yunpan-ssl的pod成功运行起来,并且我们也验证过服务按照预期的方式工作。这个时候你可能会问,如果我要访问容器的日志,由于POD中有连个容器,我该如何访问呢?不用担心,Kubernetes的客户端工具已经提供对应的选项,我们可以通过命令:kubectl logs yunpan-ssl -c envoy来查看代理容器的日志输入,下面是在笔者本地的环境的输出:

➜  ~ kubectl logs yunpan-ssl -c envoy

[2021-09-03 03:33:25.936][1][info][main] [source/server/server.cc:255] initializing epoch 0 (hot restart version=11.104)

[2021-09-03 03:33:25.936][1][info][main] [source/server/server.cc:257] statically linked extensions:

[2021-09-03 03:33:25.936][1][info][main] [source/server/server.cc:259]  envoy.transport_sockets.downstream: envoy.transport_sockets.alts, envoy.transport_sockets.raw_buffer, envoy.transport_sockets.tap, envoy.transport_sockets.tls, raw_buffer, tls

[2021-09-03 03:33:25.936][1][info][main] [source/server/server.cc:259]  envoy.retry_priorities: envoy.retry_priorities.previous_priorities

[2021-09-03 03:33:25.936][1][info][main] [source/server/server.cc:259]  envoy.dubbo_proxy.serializers: dubbo.hessian2

[2021-09-03 03:33:25.936][1][info][main] [source/server/server.cc:259]  envoy.grpc_credentials: envoy.grpc_credentials.aws_iam, envoy.grpc_credentials.default, envoy.grpc_credentials.file_based_metadata

(...省略若干)

如果你要访问的是应用容器的日志,只需要替换-c后边容器的名称即可,是不是很方便。当然我们也可以把POD中所有容器的日志都打印出来,使用命令:kubectl logs kubia-ssl --all-containers即可,读者可以在自己的机器上验证。

在一个POD中运行多个容器的情况下,如果我们要在某而过容器中运行一个命令,该如何实现呢?我们可以使用:kubectl exec -it yunpan-ssl -c envoy -- bash,来登陆到这个叫envoy的容器中,如下是笔者本地机器上的输出:

➜  ~ kubectl exec -it yunpan-ssl -c envoy -- bash

root@yunpan-ssl:/# ps -aux

USER        PID %CPU %MEM    VSZ  RSS TTY      STAT START  TIME COMMAND

root          1  0.7  1.9 176312 39020 ?        Ssl  03:33  0:06 envoy -c /etc/envoy/envoy.yaml

root          19  0.0  0.1  18256  3248 pts/0    Ss  03:47  0:00 bash

root          30  0.0  0.1  34428  2784 pts/0    R+  03:49  0:00 ps -aux

从输出的信息可以看出,我们成功登陆到了这个Envoy的代理容器实例中,并且打印出了在容器中正在运行进程,你可以看到envoy容器是容器的第一个进程,也就是1号进程,符合预期。好了,关于在一个POD中运行多个容器实例就这么多了,接下来我们来聊一个很有意思的话题,在POD的启动的时候,运行初始化的容器实例。

【在POD启动的时候,运行初始化容器实例】

我们必须清楚的一个事实是,同一个POD中的多个容器是并行启动的,Kubernetes并没有提供一种机制来保证谁先谁后。但是Kubernetes提供了一种叫init容器的机制,来保证这些初始化容器会先于应用容器实例启动,我们接下来详细的了解一下。

在POD的YAML文件中,我们可以配置多个初始容器,这些容器会先于我们的应用容器启动,这些初始化容器的目的是初始化pod的运行环境,因此我们把这种容器叫init containers。如下图所示,初始化容器会一个接一个的运行,并且只有当所有的初始化容器运行成功后,业务应用容器实例才会开始启动。

《图1.3 初始容器和应用容器的启动关系和时序》

如上图所示,init容器和普通容器最大的不同是,init容器按配置的顺序启动,并且每次只能运行一个初始化容器,而普通容器都是并行启动的,相互之间没有明确的先后关系。

接下来我们看看,初始化容器具体能干啥:

1,初始化普通的业务容器运行所需要的数据,包括但不限于获取证书,获取私钥,以及配置文件,初始化数据下载等。

2,初始POD的网络环境,由于所有运行在POD中的容器实例和POD共享相同的网络命名空间,因此网络设备的配置都可以通过初始化容器来做,并且会直接影响后边启动的应用容器的执行。

3,延缓业务容器的启动,直到某些前置条件满足。举个例子,如果因公程序依赖于某个服务启动并可以被访问到,那么可以在初始容器中检查这个服务是否ready,来延缓主应用容器的启动。

4,发送通知给外部系统,在特殊的场景下,运维人员需要知道有哪些容器启动了,因此需要得到通知,我们可以在初始化容器中,将这个通知发送出去,告诉外部系统有个新的应用实例正在启动。

虽然上边描述的4个场景本质上都可以在主容器中处理,但是使用初始化容器在某些情况下会优于在主容器中处理,我们来探讨一下这个”某些情况“具体啥意思。

使用初始化容器不需要主容器的镜像进行任何修改,因此我们可以最大限度的复用通用的初始逻辑,在多个POD中使用。这个在处理一些横向微服务治理的场景下特别有用,我们只需要将这部分代码编写到初始化容器中,所有配置了这个容器的POD都可以复用这部分能力。另外初始化容器可以保证我们的初始化工作一定是限于主容器执行,确保应用在启动的时候,有完整的运行环境。

另外一个使用初始化容器的场景安全,我们把涉及到敏感信息的操作移动到初始化容器,这样即便是主容器被攻破,我们的敏感数据也不会被轻易拿到。举个例子,POD在启动的时候需要注册到外部系统,一般需要安全令牌来访问外部以系统,如果注册的过程是通过主容器来进行,那么我们就必须在主容器里挂载令牌和证书等,这样才能访问外部的系统。如果恶意攻击者攻破了主容器,那么就能轻而易举的拿到敏感信息,但是如果我们从初始化容器注册到外部系统,那么即便是主容器被攻破,黑客也很难拿到敏感信息。

接下来,我们来给POD中增加初始容器,由于我们只是通过离子来说明这种机制,因此笔者后续介绍的初始化容器基本不会做实质性的工作。我们会向yunpan-ssl这个YAML文件中增加两个初始化容器,第一个初始化容器模拟初始化的第一步操作,运行5秒钟后,打印一条消息到标准输出(最终会体现到日志中),而第二初始化容器测试网络情况,通过ping命令来检查特定的IP地址从本地可达。由于初始化容器也是容器,需要镜像文件,因此笔者提前准备好了这两个镜像文件。编写完成的YAML文件如下图所示:

《图1.4 增加初始化容器的应用部署YAML文件》

从上图可以看到,给POD增加初始容器非常的简单和直接,这里需要注意的是,容器的名称必须唯一,由于初始化容器也是容器,因此所有容器的命名都必须唯一。在我们实际创建这个新的POD之前,我们可以先打开一个命令行窗口,运行命令:kubectl get pods -w来查看POD的状态是如何变化的。另外我们也可通打开另外一个窗口来查看事件的输出:kubectl get events -w。接下来我们就来部署这个增加了两个初始容器的POD部署文件。运行kubectl apply命令后,我们从pod的监控窗口可以看到如下的输出:

➜  kubernetes git:(master) kubectl get pods -w

NAME    READY  STATUS    RESTARTS  AGE

yunpan  1/1    Running  2          18h

yunpan-init  0/2    Pending  0          0s

yunpan-init  0/2    Pending  0          0s

yunpan-init  0/2    Init:0/2  0          0s

yunpan-init  0/2    Init:0/2  0          1s

yunpan-init  0/2    Init:1/2  0          6s

yunpan-init  0/2    PodInitializing  0          7s

yunpan-init  2/2    Running          0          8s

从上图可以看到初始化容器和应用容器是按顺序执行的,如果我们再看看事件的输入,这个就更加明显了:

0s          Normal    Scheduled  pod/yunpan-init  Successfully assigned default/yunpan-init to minikube

0s          Normal    Pulled      pod/yunpan-init  Container image "qigaopan/yunpan-init-demo:v1.0" already present on machine

0s          Normal    Created    pod/yunpan-init  Created container init-demo

0s          Normal    Started    pod/yunpan-init  Started container init-demo

0s          Normal    Pulled      pod/yunpan-init  Container image "qigaopan/yunpan-network-check:v1.0" already present on machine

0s          Normal    Created    pod/yunpan-init  Created container network-check

0s          Normal    Started    pod/yunpan-init  Started container network-check

0s          Normal    Pulled      pod/yunpan-init  Container image "qigaopan/k8ssample:v1.1" already present on machine

0s          Normal    Created    pod/yunpan-init  Created container yunpan

0s          Normal    Started    pod/yunpan-init  Started container yunpan

0s          Normal    Pulled      pod/yunpan-init  Container image "qigaopan/yunpan-ssl-proxy:v1.0" already present on machine

0s          Normal    Created    pod/yunpan-init  Created container envoy

0s          Normal    Started    pod/yunpan-init  Started container envoy

最后,我们可以通过查看日志的方式来分析初始化容器运行的情况,笔者在自己的机器上运行命令:kubectl kogs yunpan-init -c network-check,输出如下:

➜ kubectl logs yunpan-init -c network-check

简单到主机的网络连接 1.1.1.1 ...

主机可达!

好了,通过三篇关于深入理解POD文章的学习,笔者希望大家能知其然,知其所以然,POD是整个Kubenetes平台的基石,因此我们花费了大量的笔墨来让大家了解这个对象的方方面面。接下来,笔者会介绍POD的状态,敬请期待!

你可能感兴趣的:(深入理解POD的工作原理(下))