Home | 简体中文 | 繁体中文 | 杂文 | Github | 知乎专栏 | 51CTO学院 | CSDN程序员研修院 | OSChina 博客 | 腾讯云社区 | 阿里云栖社区 | Facebook | Linkedin | Youtube | 打赏(Donations) | About
知乎专栏多维度架构

2.12. Kubernetes & Docker 实施中你会遇到的问题

在项目中实施容器技术,你可以遇到下列问题。



真的需要容器吗?
技术人员强上容器的问题?
使用容器技术会遇到哪些问题?
如何解决遇到的问题?

2.12.1. 真的需要容器吗?

请仔细思考以下几个问题

  • 非用不可的理由是什么?仅仅是趋势吗?
  • 容器能为企业带来哪些价值?能降本增效?
  • 目前团队对容器技术掌握程度?包括架构师,开发人员,测试人员,运维人员
  • 技术转型的成本有多高?是否需要停服升级?
  • 人力成本呢?是否要为此招聘新的员工?
  • 转到容器后故障降低了吗?转型失败有回撤方案吗?
  • 从产品角度解决了用户的哪些痛点?用户体验改善了吗?

2.12.2. 镜像会遇到的问题

目前docker 镜像,没有统一标准,体现在以下几个方面。

2.12.2.1. 镜像使用的OS发行版不统一

在使用过程中会遇到过各种版本的 OS。包括 alpine, debian, ubuntu, centos, oraclelinux, redhat 等等……

经过裁剪的 OS 面目全非,不完整

即使是镜像采用 CentOS 母版,很多镜像制作者会给操作系统减肥。经过优化后,已经不是官方版本,在使用过程中你会遇到各种麻烦。例如调试的时候需要 curl,wget,telnet,nslookup 等工具在镜像中没有。甚至 ps, top, free, find, netstat, ifconfig 命令都没有。

很多容器都不带 iptables 所以,即使带有iptables 在容器中修改规则也很麻烦。

2.12.2.2. 安装位置不统一

传统OS 以 CentOS为例,有严格的安装规范,例如:

通常安装位置是

  • /etc/example 配置文件
  • /bin/sbin 二进制文件
  • /var/lib/example 数据文件
  • /var/log/example 日志文件
  • /var/run/example PID 文件
  • /etc/sysconfig/example 启动参数文件
  • /etc/system.d/example 启动脚本

或者被安装在

  • /usr/local/etc 配置文件
  • /usr/local/bin 可执行文件
  • /usr/local/share 文档

最后一种是独立安装在:

/usr/local/example

容器镜像那可是五花八门,没有统一标准,如果不看 Dockerfile 根本不知道作者将文件安装到了哪里。

常常存储目录被放置在根目录。例如 /data

2.12.2.3. 时区遇到的问题

很多外国人制作的镜像,根本没有考虑国际化问题,包括时区,日期格式等等。

例如监控系统 Prometheus 和 Alertmanager 默认是 UTC 时区,由于镜像制作并不规范,无论你怎么修改,你都无法将时区改为 CST,最终你只能自己制作镜像。

2.12.2.4. Linux 系统也存在BUG

在我的20年职业生涯中是遇到过 Linux 系统有BUG的,还向 Redhat 提交过 BUG。如果你采用的镜像有BUG,你想过怎么去debug 吗?

2.12.3. 容器会遇到的问题

2.12.3.1. 程序启动的区别

在Linux是一般是采用守护进程方式启动。启动后进入后台,启动采用 systemd 。

容器中启动通常是直接运行,这样的运行方式,相当于你在linux的Shell 终端直接运行一样,是在前台运行,随时 CTRL + C 或者关闭终端窗口,程序就会退出。容器采用这种方式启动,就是为了让 docker 管理容器,docker 能够感知到容器的当前状态,如果程序退出,docker 将会重新启动这个容器。

守护进程方式需要记录 pid 即父进程ID,用于后面管理该进程,例如可以实现 HUP 信号处理。也就是 reload 操作,不用退出当前程序实现配置文件刷新。处理 HUP 信号,无需关闭 Socker 端口,也不会关闭线程或进程,用户体验更好。

容器是直接运行(前台运行),所以没有 PID 也不能实现 reload 操作。 配置文件更新需要重新启动容器,容器启动瞬间TCP Socker 端口关闭,此时用户会 timeout。甚至该服务可能会引起集群系统的雪崩效应。

很多镜像制作者更趋向使用环境变量传递启动参数。

当然你也可以在容器中使用 systemd ,这样做容器不能直接感知到容器的运行状态,systemctl stop example 后,容器仍然正常。需要做存活和健康检查。通过健康状态判断容器的工作情况。如果处于非健康状态,将该节点从负载均衡节点池中将它踢出去。

Linux 启动一个应用远远比docker 启动一个容器速度要快。因为物理机或者虚拟机的Linux操作系统已经启动,虚拟机也分配了资源,运行可执行文件基本上是瞬间启动。而 docker 启动容器,要分配资源(分配内存和CPU资源,新建文件系统),相当于创建一个虚拟机的过程,最后载入约200MB左右的镜像,并将镜像运行起来,所以启动所需时间较长,有时不可控,尤其是Java应用更为突出。

2.12.3.2. 存储面临的问题

传统 Linux 直接操作本地硬盘,IO性能最大化。

私有云还好办公有云处处受限。

自建的 Docker 或 Kubrnetes 可以使用宿主主机资源,公有云只能使用网络文件系统和分布式系统。

这也是我的架构中 KVM,Docker,Kubernetes,物理机混合使用的原因,根据业务场景的需要来选择哪种方案。

物理机上部署 docker 可以分配宿主主机的所有资源,适合做有状态的服务的存储持久化的需求。

私有云 Kubernetes 适合做 CPU密集型运算服务,虽然通过local 卷和 hostPath 可以绑定,但是管理起来不如 Docker 更方便。

NFS 基本是做实验用的,不能用在生产环境。我20年的职业生涯遇到过很多奇葩,例如 NFS 卡顿,NFS 用一段时间后访问不了,或者可以访问,文件内容是旧的等等。

无论是NFS是更先进的分布式文件系统,如果不是 10G以太网,基本都不能用在生产环境。10年前我用4电口1G网卡做端口聚合勉强可以用于生产环境,不过10年前的互联网生态跟当今不同,那时还是以图文为主,确切的说是文字为主,配图还很少。

所以涉及到存储使用分布式文件系统的前提是必须是 10G以上以太网或者8G以上的FC 存储。这样才不会有IO瓶颈。任何分布式文件系统都不可能比本地文件系统稳定,除了速度还有延迟等等。

10GB 电口,光口以太网已经出来十几年了,相对比较便宜,可以使用 4光口 10G网卡,然后做端口聚合,变成 40G 网口。

现在 40G光口交换机都在10-20万之间。一个40G的交换口可以分出四个10GB口。

如果使用40GB以上的以太网,那么总成本可能会超过物理机+虚拟机的解决方案。

2.12.3.3. 内部域名DNS

由于在集群环境中容器名称是随机,IP地址是不固定的,甚至端口也是动态的。为了定位到容器的节点,通常集群中带有DNS功能,为每个节点分配一个域名,在其他容器中使用域名即可访问到需要的容器。

看似没有问题,我的职业生涯中就遇到过DNS的问题,bind,dnsmseq 我都用过,都出现过事故。解析卡顿,ping www.domain.com 后迟迟解析不出IP。最长一次用了几分钟才解析到IP地址。

所以后面就非常谨慎,配置文件中我们仍然使用域名,因为修改配置文件可能需要 reload 应用,或者重新部署等等。域名写入配置,方便IP地址变更。例如 db.host=db.netkiller.cn 同时我们会在 /etc/hosts 中增加 xxx.xxx.xxx.xxx db.netkiller.cn 。这样主要使用 /etc/hosts 做解析,一旦漏掉 /etc/hosts 配置 DNS 还能工作。

故障分析,DNS 使用 UDP 协议 53 端口,UDP 在网络中传输不会返回状态,有无数种可能导致 DNS 解析失败。例如内部的交换机繁忙,背板带宽不够(用户存储转发数据包,你可以理解就是交换机的内存),路由的问题等等……

2.12.3.4. 容器与网络

相比传统网络,容器中的网络环境是十分复杂的。传统网络中一个数据包仅仅经过路由器,交换机,达到服务器,最多在服务前在增加一些防火墙,负载均衡等设备。

容器网络部分实现方式SDN(软件定义网络)相比物理机(路由器、交换机、无服务)实现相对复杂。容器里面使用了IP转发,端口转发,软路由,lvs,7层负载均衡等等技术…… 调试起来非常复杂。docker 的 iptables 规则很头痛。

例如一个TCP/IP 请求,需要经过多层虚拟网络设备(docker0,bridge0,tun0……)层层转发,再经过4层和7层的各种应用拆包,封包,最终到达容器内部。

有兴趣你可以测试一下对比硬件设备,容器的网络延迟和吞吐量。

容器的任何操作都是占用宿主主机的资源的,所以存在更多不可控的因素。例如CPU负载瞬态变化可以影响虚拟网络,如UDP丢包。而物理机+交换机+路由器的方案不会有任何影响。

在我学习Cisco的时候一直想不通,三层交换机都能启用路由了,为什么不在增加NAT功能?路由器都提供了ACL为什么不增加防火墙功能?为什么要分成三个设备?

为什么在高端的箱式插卡设备里,交换,路由,防火墙都能实现。仅仅是因为价格?

电信级别的设备 24口交换机只会配满足24口交换的CPU和背板,卖出24口的价格。如果增加NAT功能,不仅会影响交换能力,网络设备本身价格透明且敏感。

箱式网络设备定位园区,城域网,骨干网,不是用于接入服务器。价格空间很大,厂家采用堆料方式开发,功能要满足各种用户需求和各种业务的需要。用途比IDC接入更复杂。

2.12.3.5. 容器的管理

传统服务可以通过键盘和显示器本地管理,OpenSSH 远程管理,通过配置还能使用串口。

容器的管理让你抓狂 docker exec 和 kubectl exec 进入后与传统Linux差异非常大,这是镜像制作者造成了。

控制台环境

  • 有些镜像没有初始化 shell 只有一个 $ 符号
  • 没有彩色显示
  • 可能不支持 UTF-8,中文乱码
  • 可能不是标准 ANSI/XTerm 终端
  • 键盘定义五花八门,可能不是美式104键盘
  • 国家和时区并不是东八区,上海
  • HOME 目录也是不是 /root

OS 不完整

  • 想查看端口情况,发现 netstat 和 ss 命令没有。
  • 想查看IP地址,发现 ifconfig, ip 命令没有。
  • 想测试IP地址是否畅通,发现 ping, traceroute 没有。
  • 想测试URL,发现 curl , wget 没有。

有些镜像 dnf,yum,apk,apt 可以使用,有些镜像把包管理也给阉割了,你想安装上述工具都安装不了。

卧槽!!! 一万匹草泥马

然后就自己用 Dockerfile 编译,整出200MB的镜像,卧槽这么大。

你还会发现你编译的程序没有yum/dnf官方制作的好,它们做了编译器的优化。你如果不相信你可以编译一个 nginx 然后看看bin/nginx这个文件的大小,你再使用 dnf install nginx 安装,比较以下两个二进制文件。你会发现你编译出来的二进制文件足有8M大小,而dnf安装的可能只有4M左右。

2.12.3.6. 容器与安全

网络安全

很多容器的镜像中是不包含 iptables 的,所以无法做颗粒度很细的容器内部网络安全设置。即使你制作的镜像带有iptables ,多数容器的侧咯,IP地址和端口是随机变化的。绑定IP地址又带了容器的复杂性。

一旦攻入一个容器,进入容器后,容器与容器间基本是畅通无阻。

挂马风险

在容器中藏一个后门比物理机更容易,如上文所说很多容器中没有调试相关命令,限制了你排查后门的难度。所以Dockerfile 制作镜像,最好使用官方镜像衍生出你的镜像。

隔离安全

容器间是隔离安全的,kubernetes 还有明明空间和RBAC等等。但是......

有时我们为了产生持久化会使用本地卷,或将容器目录挂载到节点宿主主机中。这也带来一个安全问题,一是可能大家使用了相同的文件或目录名,相互覆盖对方的文件。二是不小心将敏感信息写入了到宿主主机,存在信息泄露风险。

除此之外 local volume, hostPath 以及没有用户认证的网络文件系统,都存在这种风险。例如NFS,因为Pod是随机IP地址,NFS服务器无法通过IP地址设置访问规则。

用户认证

虚拟主机与物理机是一样的,进入虚拟机只能通过终端或OpenSSH两个入口,必须输入正确的用户和密码。

容器没有用户认证机制,从宿主主机可以直接进入容器内部。例如:

				
docker exec 执行容器 shell 进入容器内部
docker cp 从容器中复制文件
				
				

只要控制宿主主机就能控制所有容器。

2.12.3.7. 容器与监控

谈到监控,跳不开 prometheus(普罗米修斯),它并不能覆盖到所有监控。

我曾经写过一篇文章《监控的艺术》网上可以搜到。

2.12.3.8. 容器与CI/CD

在DevOps场景中,使用 docker 或 kubernetes 做 CI/CD 是很扯淡的。

当 git 产生提交后,gitlab/jenkins 启动容器,下载代码,编译,打包,测试,产生构建物,编译 Dockerfile ,上传 docker 镜像到 registry,最后部署到容器执行。

卧槽!!!速度能急死你。

于是乎,我们做了 Cache。 不用每次都 pull 镜像,缓存 Maven 的 .m2 库,不再清理代码(mvn clean)提速不少,测试环境凑合用吧。 注意不mvn clean 有时会编译出错

至于生产环境,我就不说了,有多少人真用CD部署生产环境。

2.12.3.9. 宿主主机与容器的参数设置问题

虚拟机是 100% 隔离的,容器是半隔离的。容器的有些配置是继承来自宿主主机的,一些可以修改,一些则不允许修改,有些配置会造成容器与宿主主机相互覆盖,例如 sysctl, ulimit 以及 时区时间等等,下面我来演示一下。

首先是 sysctl

			
宿主主机查看 vm.overcommit_memory 默认是 0 :

[root@localhost redis]# sysctl vm.overcommit_memory
vm.overcommit_memory=0

容器中查看 vm.overcommit_memory 默认也是 0 :

[root@localhost redis]# docker exec -it redis sh
/data # sysctl vm.overcommit_memory
vm.overcommit_memory=0

现在修改容器的 vm.overcommit_memory 配置
/data # sysctl -w vm.overcommit_memory=1
vm.overcommit_memory=1
/data # sysctl vm.overcommit_memory
vm.overcommit_memory=1
/data # exit

回到宿主主机查看 vm.overcommit_memory:
[root@localhost redis]# sysctl vm.overcommit_memory
vm.overcommit_memory=1

宿主主机的 vm.overcommit_memory 被覆盖了
			
			

再举一个例子日期

			
查看宿主主机日期和时间
[www@localhost ~]$ date
Mon Oct 10 18:59:18 CST 2011

查看容器的日期和时间
[www@localhost ~]$ sudo docker exec -it redis sh
/data $ date
Mon Oct 10 18:59:22 CST 2011
/data $ exit

修改宿主主机的日期和
[www@localhost ~]$ sudo date -s "1991-2-1 06:30:00"
Fri Feb  1 06:30:00 CST 1991

在进入容器查看,日期也被修改了
[www@localhost ~]$ sudo docker exec -it redis sh
/data $ date
Fri Feb  1 06:30:04 CST 1991
			
			

2.12.4. 运维会遇到的问题

由于 Docker 采用守护进程统一管理,当我们运行大量容器的时候,常常出现 docker 命令卡死,无响应。

docker-compose 无响应更是家常便饭

工作中还遇到多启动一组容器,其中 A 容器会访问 B 容器,此时 B 容器停止,重启 B 后 A 无法连接。找不出原因,重启 A 之后正常。

总之很多各种各样的问题等着你,这是此前在物理机和虚拟机中从未有过的。

2.12.5. 人员的问题

现实中真正精通容器应用的人很少,容器实在太复杂。Google 将 Kubernetes 设计成大而全系统,想用 Kubernetes 解决所有问题。它涵盖了几大块。

操作系统,虚拟化,软件定义网络,存储,容器管理,用户体系,权限体系……

我们的大学教育是本科教育专科化,本科教育本应该重视通识教育,我们的教育却按照专科标准教育。本科是面向学术的起点,专科是面向工作,解决实际问题。

你问一个中国大学生他会什么,他会说:我会Java,我会Linux……

反应到工作上,就是程序猿不懂运维知识,运维攻城狮不会写程序。员工更趋向深耕一个领域,很类似现在的医生教育和医院体系,专科化,割裂化,导致很多跨科的疾病难以诊断。

于是我提出了「多维度架构」。

2.12.6. 当 kubernetes 遇上微服务

kubernetes + spring cloud 能碰撞出什么火花?

大家好今天是2022年正月十五元宵节,我一直想写这篇文章,拖到今天。这篇文章我会详细讲讲我们在实施 kubernetes + spring cloud 过程中遇到的问题。

在此前我们使用单体服务 springboot 足以完美解决我们的需求,即客户满意,产品满意,运营满意……

但是……技术不满意,呵呵~~

2.12.6.1. docker + springboot 完美解决方案

我们项目采用 docker + springboot 单体运行方案,使用传统的负载均衡等技术解决高可用。一台干净的主机,部署 docker 容器,然后所有应用都在容器中跑。

其中java项目会使用同一个镜像(OpenJDK8),然后将 jar 文件放在指定目录,挂载到容器中运行。系统升级,持续部署(Gitlab runner)只需将 jar 复制到宿主主机,然后重启容器即可。我们并没有为Java项目制作自己的镜像,那样增加了复杂度,升级需要更新镜像。

2.12.6.2. 升级到 kubernetes + spring cloud 遇到的问题。

2.12.6.3. 链接池遇到的问题

当我们将实例拆分成微服务后,还有遇到一个问题,就是数据库、缓存、搜索引擎、MongoDB等等服务的链接开销问题。

单体服务只需要链接一次数据库,如果单体程序拆分成10个微服务,这个10个服务都需要与数据库建立链接,这种开销是成指数级增长的。如果单体服务器联系池配置128个链接,那么10个微服务呢?是 128*10 = 1280 也就10个微服务都需要与数据库建立链接,这时你会发现数据库的最大链接数已经不够,必须做调整。

同理,其他服务例如 缓存 Redis, MongoDB, Elasticsearch 等所有服务都需要同时调整最大链接数限制。

2.12.7. 最后总结

使用物理机,虚拟机,学习成本,试错成本,部署成本远远低于容器技术,对于事故排查可能容器需要更多的时间。Google 官方也曾经说过,未来 kubernetes 重点可能会转向虚拟机。

掌握容器技术需要很多跨界知识,对人员的要求非常高,深耕自己领域程序猿或运维工程狮都无法全面掌握容器技术,冒然跟风蹭热度,项目只会变为成为员工打怪升级,点亮技能树的试炼场。

我个人认为容器更适合CPU密集型的业务,前期制作符合你需求的镜像,可能需要花费很长时间,我的架构中 KVM,Docker,Kubernetes,物理机混合使用,根据业务场景的需要来选择最佳方案。