Kubernetes 应用容器化

非容器化平台到容器平台中间我们需要面临的调整和解决方案。如何将应用迁移至k8s平台。

目标


虚拟化是将物理机切分为不同的虚拟机,每一个虚拟机是一个独立的操作系统,应用在虚拟化部署的时候没什么太多的变化。

我们应用即使运行在不同的namespace,有了cgroup的控制,它的很多系统级别的命令其实就是在主机的层面去运行了这些命令,它看到的信息就是和主机相关的,比如在任何的namespace里面去执行top,它看到的是整机的进程资源。

在任何namespace里面去执行df,它看到的是整机的磁盘情况。

应用在迁移到容器化的时候,我要去感知到容器里面,有些针对系统级别的操作就可能不使用传统的方式去做了。

应用容器化对单个实例,第一要考虑应用如何去适配容器技术栈。第二单个应用实例本身需要的资源需求,需要多少资源,然后配置管理如何做,如何将代码和配置分离。

比如应用要去连接远端的服务,这个远端服务用户名密码是什么?这些东西是不能构建到容器镜像里面的,肯定要将其分离出来,所以需要配置管理。

这些应用可能要保存一些数据,这些数据存在哪?也是我们需要思考的。

应用也是要做监控的,这个指标除了标准容器技术汇报出去的cpu memeory的用量,这可能是统一平台就能够采走了,同时可能会有一些业务指标,还要去监考网络请求的处理延时,这些指标也是需要去收集的。

  

应用容器化的思考


除了要将系统的可维护性变高以外,我们还追求应用快速启动,如果生产系统要替换实例的时候,那么启动时间特别长,那么很多的实例,那就需要等很久很久这个问题才能恢复。或者滚动升级,同样需要很长时间才能够完成一次发布,所以发布一次需要很长时间,这就需要运维人员花很长时间去盯着。

如果应用秒级启动,那么应用发布的速度会非常快,替换,扩容缩容也会非常的快。

所以要考虑应用多久可以提供服务,还能够优化启动速度吗,不能优化的原因是什么,是要加载缓存吗,很多java应用去数据库里面加载数据,然后构建缓存,那么就要去看拉取的数据是不是你需要的,能不能做细粒度的架构优化,减少对资源的浪费,提升启动速度。

优雅终止和优雅启动,优雅终止是如何去处理sigterm这个信号量,所谓优雅启动就是如何去设置健康检查。这样应用真正就绪了之后才能够接受用户流量。

以java为例,要配置java的heap size,堆大小,容器化之后就要考虑配置是不是需要发生变更。

基础镜像越小越好,毫无疑问,因为意味着启动速度快,消耗的资源小,也不是越小越好,要考虑在做调试的时候需要什么工具,如果容器里面没有这些工具,你可以登入到节点上面,用节点上面工具去查看。

k8s后面版本有调试的container,可以将调试的container注入到这个pod里面,把工具放到那个container里面去调试。

容器里面有多少个进程,很多时候会去鼓励一个容器一个进程,这样使得这个容器的微服务力度很细,但是很多传统应用往容器化搬迁的时候,我们发现这不是事实,它可能有个主应用,但是还有一堆守护进程,还有很多辅助进程,这就制约我们在容器化的时候不一定是一个进程对应一个容器的。

一个容器对应多个进程,那么就要知道哪个是主进程,谁来影响整个进程的退出。容器还要控制进程的数量,因为操作系统的进程数量是有限的,如果某个容器一直在启动新的进程,那么可能将整个机器所有的进程号都占满,那么这个节点就会失效,这个pod就会被驱逐,这样就会将局部的问题带到整个集群。

只要这个进程在集群里面乱窜,那么整个集群都会被它毁掉,在早期的k8s里面经常会出现这个问题,后期提供了对pid的限制,它可以控制某个容器最多建立多少个进程来消除这种破坏力。

还有应用程序配置和代码分离,如何管理这些配置是通过环境变量去管理还是通过配置文件去管理,如果是配置文件就可能需要通过configmap或者secret来mount到容器当中作为一个文件在那,然后由应用去加载,这些我们都需要去规划好。

容器层数越多,那么最后会有一些性能的损耗。

最后就是entry point如何控制,定义了容器在启动的时候到底要启动哪个应用,entrypoint才是使得基础架构和应用进程产生关联关系这样的核心点。

容器额外开销和风险


任何容器技术都会有自己的日志转储系统,比如docker有自己的日志driver,docker logs看到的容器日志是容器里面的进程通过stdout和stderr输出出来的一个标准日志。

docker本身会提供自己的log driver,将控制台日志转储到本地文件,用来持久化,最后可以汇报给一个集中的日志采集系统。

有些应用在启动的过程当中输出了大量的debug log,可能几分钟就输出了几百兆的日志,那么就会导致标准输出的buffer满,这个标准输出本身是有buffer的,buffer由应用这边写入,由log driver读出去并且持久化。

docker的默认的driver是blocking模式的,也就是log driver在读取日志的时候,如果没有及时落盘,它会阻塞新的日志写入,那就意味着应用那边想输出日志,发现缓冲区满了,那么它的日志输出就卡在那了,那么就会导致应用启动慢了,因为日志那行代码过不去,那么就会导致原来10分钟启动的应用花了1-2个小时还没有起来。

应用就是慢,这个慢可能是上下游服务慢,有可能是访问数据库慢,最后发现可能和日志有关,然后将日志等级关掉,然后就正常了。

后面docker才提出了none blocking driver模式,如果发生这种情况,如果缓冲区满了,但是还没有写,log driver还没来得及写,这个时候可以设置none blocking mode,就是应用可以继续输入,我 log driver来不及转储,所有那些在缓冲区被覆盖的日志直接被丢掉,所以后期都设置为none blocking mode的log driver。但是你还是需要知道,频繁写日志就会面临这样的情况,打debug log就会丢失日志,但是应用可以起来。

由于容器技术不是一个一个点虚拟机,所有的进程共用kernel,这就意味着kernel的一些参数和环境变量配置是所有容器进程共享的,比如fd的数量,磁盘,参数配置,这些都是共享的。

容器化应用的资源监控


如果应用里面通过top这个命令,你看主机资源并且基于主机资源做一些配置的话,那么这里面就可以产生一些问题,有些云应用是java应用,这个java应用启动的时候会去设置concurrent GC,concurrent GC去看这个cpuinfo,cpuinfo里面看到的更多的是主机资源,所以在容器世界里面它看到的可能是80个核数,那它的concurrent GC Thread就设置错了,虽然我主机是80个核,但是应用只给了你4个核,但最终的目的其实是希望你分配4个核数,我就用4个去配置concurrent GC,但是通过proc cpuinfo去读取到的值是不对的,这样就会造成它的一个错误的配置。

后续造成的结果是什么,那么它的性能就和之前不一样了,因为concurrent GC直接影响到它不同负载情况下去做gc的效率,这些系统主机资源,无论哪一个namespace去运行,它看到的都是主机的资源,如果你要依赖于这样的命令或者配置文件去读取系统配置,那么在虚拟机世界和容器世界里面返回的结果就不一样了。

在容器里面是通过cgroup来加载所有的进程,在容器里面查看pid为1的容器进程,查看它的cgroup的信息就可以知道自己是不是运行在k8s之上,最早期是以docker为关键字的,后面k8s逐渐将docker剥离出去了,所以它以kubepod为关键字,所以可以看到proc/1的进程它的cgroup路径里面有kubepod,那么说明它是运行在k8s基础之上。

假设要找当前容器容器进程配置的内存上线,就去看内存子系统里面limit_in_bytes,如果要看用量就看memeory_usage_in_byets,假设应用里面去看这些信息就可以通过cgroup去解读这些信息。

cpu要看两个值,一个是period一个是quota,如果容器被分配了一个CPU,那么quota是10w,如果2个cpu就是20w,cpu数量就是period/quota,通过这个就可以知道cpu的上线。

-1代表besteffort pod,也就是没有为其设置资源上线。

所以在容器世界里面,要去抓真实用量的话,那么就需要去读取这些文件,然后在容器里面再去利用这些数值,但是这些数值由监控cadvisor统一了,包括Prometheus的一些adapter,那么就不需要从容器内部抓取这些指标数据。

kata本身是另外的一个runtime,它和runc不一样,它有自己的kata runtime,kata runtime就会启动一个虚拟机,在虚拟机里面再启动一个容器,首先在外层有虚拟机,它是为了让每一个应用实例是有一个真正操作系统层面的隔离,可能有些公司对安全有要求,不让使用容器技术,kata就是在容器之外使用独立操作系统去封装,这就可能满足一些公司的额外要求。

有了容器技术使得后面运维监控标准化了,从应用层面还是一个一个容器,只不过在运行的时候有个虚拟机,虚拟机本身是有开销的,所以kata会在原始cpu的基础上再加1个cpu,这个cpu就是给kata这个操作系统去使用的,所以会造成资源的浪费。

对java老说是current gc,默认配置就不对了,特别是老的jvm没有适配容器技术。 

上面都是对容器环境没有做很好的适配。

猜你喜欢

转载自blog.csdn.net/qq_34556414/article/details/126987299