《Kubernetes 与云原生应用》专栏是 InfoQ 向轻元科技首席架构师王昕约稿的系列文章。本专栏包含 8 篇内容,将会从介绍和分析 Kubernetes 系统以及云原生应用入手,逐步推出基于 Kubernetes 的容器设计模式实践案例,希望对计划应用 Kubernetes 的朋友有所帮助。本文是该专栏的第四篇,阅读本系列全部内容请在细说云计算微信公众号(CloudNote)回复 K8s。
1. Kubernetes 系统架构与设计理念
2. 云原生应用的设计理念与挑战
3. Kubernetes 与云原生应用的容器设计模式
4. Kubernetes 容器设计模式实践案例 - 单节点多容器模式
5. Kubernetes 容器设计模式实践案例 - 多节点选举模式
6. Kubernetes 容器设计模式实践案例 - 工作队列模式
7. Kubernetes 容器设计模式实践案例 - 分散收集模式
8. 云原生应用的容器设计模式综述与展望
K8s 与容器设计模式
目前 K8s 社区推出的容器设计模式主要分为三大类:
第一类,单容器管理模式;
第二类,单节点多容器模式;
第三类,多节点多容器模式;
一类比一类更复杂。根据复杂性的不同,本系列文章给出不同篇幅的实践案例介绍。
对于第一类,只在本文中用一小节给与介绍;对于第二类,在本文中,针对每一种典型设计模式分一个小节给与介绍;对于较复杂的第三类,每一种典型设计模式将用一篇文章给与介绍。
单容器管理模式
K8s 的最大特色是支持多容器的微服务实例。当然,单容器的模式也是支持的,只不过这种模式并不能突出 K8s 的特色和强大。很多人对 K8s 一直以来的印象是:功能强大,但入门较难。其实,单单就启动一个单容器微服务实例,K8s 的命令行操作跟 Docker 原生命令一样简单。
运行一个 nginx 容器
[root@demo-k8s ~]# kubectl run nginx --image=nginx deployment "nginx" created [root@demo-k8s ~]# kubectl get deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE nginx 1 1 1 1 24s [root@demo-k8s ~]# kubectl get rs NAME DESIRED CURRENT AGE nginx-3137573019 1 1 1m
由上面的例子可以看到,K8s 只要一个命令既可以启动以 nginx 为镜像的一个微服务实例。与此同时,K8s 的强大之处在于,方便用户用一个命令的同时,仍然保证了 K8s 应用体系的完整性和规范性。也就是说,虽然用户只运行了一个命令,但 K8s 为用户自动创建了四种 API 对象,包括:Deployment,ReplicaSet(RS),Pod 和 Container 要想扩展伸缩同一服务的实例个数也非常简单。
[root@demo-k8s ~]# kubectl scale deployment nginx --replicas=3 deployment "nginx" scaled [root@demo-k8s ~]# kubectl get rs NAME DESIRED CURRENT AGE nginx-3137573019 3 3 22m
依靠这种兼顾易用性和模型一致性的设计理念,K8s 使自己既适合简单场景也适合复杂场景。
单节点多容器模式
从单节点多容器模式开始的容器设计模式,是真正体现 K8s 设计特点的地方,也就是基于多容器微服务模型的分布式应用模型。在 K8s 体系中,Pod 是一个轻量级的节点,同一个 Pod 中的容器可以共享同一块存储空间和同一个网络地址空间,这使得我们可以实现一些组合多个容器在同一节点工作的模式。既然 Pod 的特点是共享存储空间和网络地址,那么单节点多容器模式一定是利用这两种特性的。
挎斗模式(Sidecar pattern)
第一种单节点多容器模式是挎斗模式。这种模式主要是利用在同一 Pod 中的容器可以共享存储空间的能力。
一个典型的挎斗应用场景如图所示:一个工具容器写文件到共享的文件目录,应用主容器从共享的文件目录读文件。例如,我们可以用 Nginx 构建一个代码发布仓库,简单的将代码放到某个本地目录即可。为了保持同步,我们同时用一个装有 Git 客户端的容器定时到原始代码仓库同步下拉最新的代码。这种模式的好处是,工具容器的镜像,也就是打包有 Git 客户端的镜像可以重用,而不需要跟应用的容器打包在一起。同样的应用,应用主容器不用 Nginx 也可以用 Apache Httpd,都可以跟工具容器组合起来形成微服务。
图:工具容器写文件应用容器读文件
另一个典型的挎斗模式如图所示:一个工具容器读文件,应用容器写文件。例如:一个基于 Nginx 的 Web 应用向系统文件系统写入日志,而一个收集日志的容器从共享目录读出日志,并输出到集群的日志系统。这一模式的好处在于,工具容器的镜像是可以重用的,不需要在每次更新应用容器打包的时候,把工具容器的执行文件打包进去。
图:工具容器读文件应用容器写文件
外交官模式 (Ambassador pattern)
第二种单节点多容器模式是外交官模式。这种模式主要利用同一 Pod 中的容器可以共享网络地址空间的特性。如图所示,在一个 Pod 中给应用容器搭配一个工具容器作为代理服务器。工具容器帮助应用容器访问外部服务,使得应用容器访问服务时不需要使用外网的 IP 地址,而只需要用 localhost 访问本地服务。在这种模式下,作为代理服务器的工具容器好像外部服务派驻在 Pod 中的“外交官”,使得应用容器办理业务时只需要跟本 Pod 的外交官打交道,而不需要出国了,因此而得名。
图:外交官模式的逻辑结构
基于外交官模式的 Redis 访问演示案例
我们这里用一个访问 Redis 服务的简单案例,来实践体验一下 Ambassador 模式和 K8s 单节点多容器模式的应用细节。本案例的文件清单在 Github 上。
图:基于外交官模式的 Redis 访问演示案例架构
1. 创建一个初始的 Redis Master 实例
先创建一个 Redis Master 节点的 Pod 用于初始化 Redis 集群。
kubectl create -f examples/redis/redis-master.yaml
2. 创建 redis 的服务
创建一个 redis 的服务,这个服务可以在前段作为多个 redis 的 Pod 节点的负载均衡。
kubectl create -f examples/redis/redis-service.yaml
3. 创建 redis 的 replication controller
创建控制多个 redis 服务 Pod 的 RC,当然也可以用 Deployment 或 ReplicaSet 来创建。
kubectl create -f examples/redis/redis-controller.yaml
[root@demo-k8s ~]# kubectl get rc NAME DESIRED CURRENT AGE redis 1 1 19h
创建完后可以用 kubectl get 命令查看 rc 和 Pod,会发现并没有产生新的 Redis Pod,这是因为原来的 Pod redis-master 已经满足了 replica=1 的要求。
4. 创建 redis-sentinel 的服务
创建 redis sentinel 服务。
kubectl create -f examples/redis/redis-sentinel-service.yaml
5. 将 redis-sentinel 的 replication controller
创建控制多个 redis sentinel 服务 Pod 的 RC。
kubectl create -f examples/redis/redis-sentinel-controller.yaml
6. 将 redis 实例和 redis-sentinel 实例扩展成 3 个
kubectl scale rc redis --replicas=3 kubectl scale rc redis-sentinel --replicas=3
7. 删除掉手工启动的 redis 实例 redis-master
删除掉我们已开始手工创建的 redis master 的 Pod,redis 的 rc 会自动启动新的 redis 以满足 replicas=3 的要求。同时,Redis sentinel 节点会选举出一个新的节点作为 master 节点。
kubectl delete pods redis-master
8.Redis 集群的验证方法
查询 redis 集群中所有 redis 实例的 IP。
[root@demo-k8s ~]# kubectl describe pod -l name=redis | grep IP IP: 10.120.44.3 IP: 10.120.63.3 IP: 10.120.80.5
我们知道,这 3 个 redis 实例中,只有一个是 Master 节点,是可写的,可以调用 SET 命令和 GET 命令;其他两个节点是 Slave 节点,是只读的,只能调用 GET 命令。我们可以用下面的命令测试三个 redis 节点。
[root@demo-k8s ~]# echo -e "SET test1 SET test1 8\r\nQUIT\r\n" | curl telnet://10.120.44.3:6379 -READONLY You can't write against a read only slave. +OK [root@demo-k8s ~]# echo -e "SET test1 8\r\nQUIT\r\n" | curl telnet://10.120.63.3:6379 -READONLY You can't write against a read only slave. +OK [root@demo-k8s ~]# echo -e "SET test1 8\r\nQUIT\r\n" | curl telnet://10.120.80.5:6379 +OK +OK
通过上面的测试我们可以知道只有 IP 为 10.120.80.5 的 redis pod 是 Master 节点,其他两个是 slave 节点。
对于只读的操作,我们可以利用 redis 的 service IP,通过 K8s 的 kube-proxy 来访问,如下,我们得到 redis 的 CLUSTER-IP 为 10.123.248.129,可以用这个 IP 来读取 Redis 数据。那么,如果用这个 IP 来写数据将怎么样呢,后面将看到。
[root@demo-k8s ~]# kubectl get svc -l name=redis NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE redis 10.123.248.129 <none> 6379/TCP 5d [root@demo-k8s ~]# echo -e "GET test1\r\nQUIT\r\n" | curl telnet://10.123.248.129:6379 $1 8 +OK
9. 制作 Ambassador 容器镜像
截至目前,我们还没有用到外交官模式。下面我们用 Haproxy 制作一个外交官代理,用来访问 Redis 服务,使得跟该容器在同一个 Pod 里的容器,在访问 Redis 读写服务的时候,都只需要访问本地 localhost 服务。本例中的文件在 Github 上可以找到。
Dockerfile 文件清单:Dockerfile
FROM haproxy:1.5 COPY tmpl-haproxy.cfg / COPY starthaproxy.sh / CMD ["sh"Node.js "-c"Node.js "/starthaproxy.sh"]
启动 haproxy 的文件清单:starthaproxy.sh
echo "Proxying localhost ${INPUT_PORT} to ${TARGET_IP}: ${TARGET_PORT}"
cat tmpl-haproxy.cfg | sed -e "s/INPUT_PORT/${INPUT_PORT}/" -e "s/TARGET_IP/${TARGET_IP}/" - e "s/TARGET_PORT/${TARGET_PORT}/" > /haproxy.cfg haproxy -f /haproxy.cfg
通过这里的 Dockerfile 构建的代理服务器容器,根据输入的环境变量 INPUT_PORT、TARGET_IP 和 TARGET_PORT,可以将发向本地 INPUT_PORT 的服务请求转发到目标 TARGET_IP:TARGET_PORT 上去。
因此这个容器可以作为一个简单方便的本地代理服务器使用。
10. 使用 Ambassador 代理服务器的 pod
使用 Ambassador 代理服务的 Pod 文件清单:demo-redis-amb-centos.yaml
apiVersion: v1 kind: Pod metadata: labels: name: demo-redis-amb-centos name: demo-redis-amb-centos spec: containers: - name: app-main-container image: centos:7 args: - sleep - "1000000" - name: redis-amb-read image: xwangqingyuan/port-forward-haproxy env: - name: INPUT_PORT value: "6379" - name: TARGET_IP value: "10.123.248.129" - name: TARGET_PORT value: "6379" ports: - containerPort: 6379 - name: redis-amb-write image: xwangqingyuan/port-forward-haproxy env: - name: INPUT_PORT value: "16379" - name: TARGET_IP value: "10.120.80.5" - name: TARGET_PORT value: "6379" ports: - containerPort: 16379 volumes: - name: data emptyDir: {}
在这个 Pod 中,我们用一个 CentOS 容器作为应用的主容器来使用 Ambassador 工具容器,当然也可以用实际的应用容器 Tomcat、Node.js、PHP 等等,因为这里只是为了演示测试 Ambassador 代理,我们用 CentOS 作为应用容器。其中我们有两个外交官容器,一个用来从 Redis 集群读数据,容器的名字为 redis-amb-read,它将发向本地 6379 端口的请求转发到 redis 服务的 CLUSTER-IP,最终会轮训地发送给任意一个 redis 实例;另一个用来向 Redis 集群写数据,容器的名字为 redis-amb-write,它将发向本地 16379 端口的请求转发到 redis master Pod 的 IP。
11. 测试一个使用 Ambassador 模式的 pod
创建一个使用 Ambassador 代理的 Pod,并登陆到主容器进行测试。
kubectl create -f /home/centos/worktemp/redis/demo-redis-amb-centos.yaml kubectl exec -it demo-redis-amb-centos /bin/bash
测试对读容器本地 Ambassador 的访问。
echo -e "SET test1 10\r\nQUIT\r\n" | curl telnet://localhost:6379 -READONLY You can't write against a read only slave. +OK echo -e "SET test1 10\r\nQUIT\r\n" | curl telnet://localhost:6379 -READONLY You can't write against a read only slave. +OK echo -e "SET test1 10\r\nQUIT\r\n" | curl telnet://localhost:6379 +OK +OK echo -e "GET test1\r\nQUIT\r\n" | curl telnet://localhost:6379 $2 10 +OK echo -e "GET test1\r\nQUIT\r\n" | curl telnet://localhost:6379 $2 10 +OK echo -e "GET test1\r\nQUIT\r\n" | curl telnet://localhost:6379 $2 10 +OK
从测试结果可以发现,对读容器执行 GET 操作时,操作总是成功的,说明 3 个 redis pod 都可以读取数据。而对读容器执行 SET 操作时,3 个操作只有一个是成功的,也就是说只有负载均衡将请求发给 redis master pod 时,操作能够成功。
测试对写容器本地 Ambassador 的访问。
echo -e "SET test1 10\r\nQUIT\r\n" | curl telnet://localhost:16379 +OK +OK echo -e "SET test1 10\r\nQUIT\r\n" | curl telnet://localhost:16379 +OK +OK echo -e "SET test1 10\r\nQUIT\r\n" | curl telnet://localhost:16379 +OK +OK
从测试结果可以发现,对写容器执行 SET 操作时,操作总是成功的,说明 redis-amb-write 容器将所有请求都转发给了 redis master pod。
适配器模式(Adapter pattern)
第三种单节点多容器模式是适配器模式。这种模式对于监控和管理分布式系统尤为重要。对分布式系统的一种理想设计目标,就是能够实现“分布地执行和存储,统一的监控和管理”。要想实现“统一的监控和管理”,应用和监控管理交互的接口需要是统一的,而且其接口是依照“统一的监控服务”的接口模式来实现。这和面向对象设计模式中的“适配器模式”也非常相似。
图:统一监控服务和监控适配器模式
图:利用 Prometheus 作为监控服务的分布式系统
一个典型的可以采用适配器模式的系统,是利用 Prometheus 作为监控服务的分布式系统。在 Prometheus 周边项目中,有诸多适用于不同应用系统的监控数据输出器(Exporter),负责收集跟特定应用相关的监控数据,使得 Prometheus 服务可以以统一的数据模式收集不同应用系统的监控数据,每个 Exporter 同时也都是一个适配器模式的实现。
总结
本文主要介绍了 K8s 集群中,单节点单容器模式和基于 Pod 模型所支持的几种单节点多容器模式,包括挎斗模式、外交官模式和适配器模式。并且对于外交官模式,利用一个 redis 集群案例演示了如何利用外交官模式以访问本地服务的模式访问网络服务。
后续,K8s 社区肯定还会发展出其他的容器设计模式,但不论如何,单节点多容器模式主要是利用同一 Pod 中的容器可以共享存储空间和网络地址空间的特点。本文案例中的代码只能用来演示,还比较简单,有很多的地方可以增强。例如:用 Replication Controller 的地方,可以用 Deployment 或 Replica Set 来部署,这是 K8s 社区更为推荐的方式;对于案例中的外交官容器,Master Pod 的 IP 和 Redis 服务的 IP 未必需要写死,而是可以通过脚本从指定的服务读取;此外,演示中所用的代理服务器容器也可以增强为支持多个端口映射的代理服务器。
答疑环节
问:考虑到性能,分布式文件存储可能不会适合,K8s 有没有针对 database 的略微完美的部署方案?
王昕:完美的应该没有。有两个 K8s 功能可能跟 database 的存储有关,一个是 nodeAffinity 一个是 PetSet。
nodeAffinity 的作用是让你可以为 Pod 选定特定的节点运行,例如你想要的高性能的挂载了 SSD 的节点。PetSet 的作用是让 Pod 可用于有状态服务,可以固定跟某个存储绑定。这两个功能都处于非常早的阶段还很不成熟,但可以肯定这些功能是 K8s 社区专门解决有状态服务和存储问题的方案。
在 nodeAffinity 和 PetSet 设计稳定之前,还有一个 nodeSelector 也是可以利用的。它可以让用户选定特定的 node 部署 Pod,同样,你可以选定挂有高性能存储的节点。
问:希望更深层次了解一下 Overlay 的网络原理,能否介绍一下?
王昕:Overlay 也就是覆盖网络,跟隧道技术是紧密相关的,通过网络封装技术模拟二层网络,跟 VPN 所采用的技术是类似的。
简单说,比如 A 和 B 之间是没有网络直接用以太网连接的,就是所谓的二层不连通。但是通过三层的 IP 层可以连通,也就可以建立四层的连接,比如 UDP 可以连通,也就是四层连通,二层不连通。但是我可以,通过四层的连通传输二层以太网的数据包,模拟出一个二层连通的以太网来。这个模拟出来的连通的二层网络就叫 Overlay,下面真实的物理网络叫做 Underlay。
所以说,Overlay 的技术每一层封装会增加一些开销,会影响一定性能。此外,各个层次对网络丢包延迟的问题处理不好,也会影响性能。K8s 比较流行的基于 Overlay 的组网技术有 Flannel 和 Weave,Weave 目前看性能是比较差的。Flannel 有两种隧道封装技术,UDP 和 VxLAN,一般来说 VxLAN 性能比 UDP 好很多。Overlay 有一定的性能劣势,也有优势,主要是降低对下面物理网络的依赖,很容易搭建。
感谢魏星对本文的策划和审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论