DockOne微信分享(一九三):容器化落地实践的一个案例


【编者的话】随着公司业务的不断发展和微服务化的改造,项目越来越多,每天的发布也越来越多, 传统运维体系效率低下,难以满足业务的需求。近年来Docker发展非常迅速,构建一次,到处运行的理念也给运维带来了一丝曙光,Kubernetes平台的出现更是加速了容器化在企业落地的进度。本文主要分享了Kubernetes在的一个落地实践案例。

容器化的背景介绍

随着公司业务规模的不断变大,为了能够快速迭代,公司的后端架构也逐渐地从传统架构模型转向了微服务架构。公司主要开发语言是Java、Golang、PHP,大部分的服务都运行在公有云的虚拟主机上。

在没有容器化之前,我们遇到的一些主要问题:

流程繁琐

后端架构转成微服务以后服务的数量变得更多,每个服务都需要开通虚拟机,配置监控,配置Jenkins,配置ELK,配置白名单等。不同的开发语言有些配置还不同,这些琐碎的事情消耗了运维的很多精力。

没有稳定的测试环境

由于测试环境之间也需要互相调用,经常联调,由于一些历史原因,不是所有的服务都有稳定的测试环境,这给测试人员带来了不少的麻烦,经常会用线上的部分机器进行调试,存在不少的潜在风险。

资源利用率低

为了方便管理,每个服务都是分配的单独的服务器进行部署,由于产品用户的使用习惯,公司大部分的服务的资源使用都有高峰低估的特点,为了保障服务的资源充足,我们核心的服务高峰的时候也会控制资源使用率,所以大部分服务在平时的资源使用率都是很低的,造成了资源的大量浪费。

扩容/缩容不及时

业务高峰期的时候经常需要扩容机器,扩容需要开发申请,运维审批,部署服务,测试确认等很多环节,一个服务的扩容最快也得需要半个小时才能完成,半个小时对于很对关键服务来说其实是很长的,是无法忍受的。

部署系统的不足

后端系统在向微服务演进的过程中,不同的后端小团队出现了编程语言,框架等的多元化,之前存在的部署系统不能充分满足多语言多框架的部署需求(之前大部分是PHP)。

整体架构

01.png

为了解决上面提到的问题,同时也看到很多大公司已经在生产环境采用了Kubernetes,我们也把我们的服务逐渐迁移到Kubernetes容器集群上。

各个模块的落地细节

容器的接入

这一部分主要跟大家分享的是我们怎样把运行在传统虚拟机/物理机上的服务逐渐接入到Kubernetes上的。

在进行容器化改造之前,我们的运维平台已经具有了代码发布的功能,我们为不同编程语言的项目制定了部署规范,比如部署路径,日志路径,启停方式,回调脚本等。每一个服务要接入部署系统都需要在平台上提工单申请,工单信息大致如下:
02.png

通过上面的工单,足够可以收集运维关心的服务信息,这些信息都会记录到我们的运维平台上。容器集群搭建完成以后,开发人员只需要为该项目申请容器部署即可,构建的过程可以复用之前的,不用做任何改动,只需要在构建完成以后制作相应的镜像推送到内部的Docker仓库中,后面会详细说这些,下面是开发申请项目接入Kubernetes的主要选项(有分页不便截图):
  • 项目名称
  • 环境选择,线上/测试
  • 容器副本数量
  • HPA扩容范围,比如3-10
  • 资源配置(request),比如1核1G
  • 集群内访问/负载均衡,如果连接该项目的服务都已经部署到了容器中,那么该服务选择集群内访问的方式就行,也就是采用clusterIP的方式,如果该服务是对公网的,或者访问该服务的客户端部署在传统VM/物理机上,这时候就需要一个负载均衡了。这里的负载均衡是公有云提供的LoadBalancer。


开发人员提交工单,运维人员处理完工单以后这个项目基本上就可以通过部署系统部署到容器了。运维平台通过调用Kubernetes集群的接口来创建相应的Service,Deployment,HPA等。当然这只是大概的流程,具体实施过程中还需要做好开发人员的容器知识的培训和编写相关基础文档。

容器CICD

03.png

这里简单说一下上图的具体步骤:

1、推送代码,代码仓库使用的是内部搭建的GitLab仓库。

2、运维平台是以项目为中心而设计开发的,上面说了每个项目要想接入部署系统都会提交发布申请工单,提交完以后项目和该项目的代码仓库的绑定关系就会存储到平台上,然后开发就能从平台上提交容器的发布工单,如下图是开发者提交发布申请的表单。
04.png


3、我们的项目构建采用的是Jenkins,跟大部分使用Jenkins的方式不太一样,我们通过Jenkins的API来触发构建,Jenkins对于开发人员是不可见的。我们会在Jenkins上提前创建不同编程语言的任务模板,开发提交发布工单以后,运维平台会通过调用Jenkins的API复制模板创建一个和该项目在运维平台上名字一致的Job,并配置好相关的参数信息,比如Git地址,分支/tag名称,构建的shell等,然后触发构建任务。这里有2个点需要给大家说明一下,一个就是我们之前对服务的标准化以后,开发人员不需要编写Dockerfile,而是采用通用的Dockerfile,替换一下相关的变量即可,这些工作在构建的时候自动触发;二是我们采用GitLab上的tag来作为每次部署的版本号。我们没有采用commitID是因为它可读性较差,开发每次提交发布以后,我们平台会用时间戳+用户ID去GitLab上创建tag,同时这个tag也会作为该服务镜像的一部分。
# 构建shell样例
mvn clean;
mvn package -Dmaven.test.skip;
sed  s/service-name.jar/${JOB_NAME}/g /template/Dockerfile > Dockerfile
cp target/*.jar ${JOB_NAME}
docker build -t inner.hub.com/${JOB_NAME}:#tag# .
docker push inner.hub.com/${JOB_NAME}:#tag#

4、Jenkins配置了GitLab的互信密钥,可以拉取所有项目的代码。

5、平台触发Jenkins构建任务以后,就会把该次构建的容器image推送到内部的Hub中。

6、测试的Kubernetes集群开发和测试可以随意发布, 线上的是需要测试人员确认通过以后才可以上线的, 这里有一个点是线上环境上线的镜像是已经上线到测试环境的相同镜像.

7、上线到Kubernetes,开发人员从运维平台上点击按钮触发,运维平台调用Kubernetes的接口通过替换容器的image地址来实现发布/回滚。

容器的CICD大致就是这样的流程。

容器的管理

这里主要跟大家分享的是我们的运维平台上提供的容器相关的功能。我们的运维平台是以项目为中心来设计的,所有的其他资源都会跟项目绑定,比如服务器,数据库,容器资源,负责人等。此外平台还提供了权限的管理,不同的人员对于项目有不同的权限,基本上每个开发对于项目的常规操作我们都会集成到平台上。容器这块我们没有使用官方的Dashboard,也是基于API来集成到运维平台的。
05.png


  • 编辑配置,这个功能主要是给运维人员开放的,方便给项目调整参数。

  • 基本信息,包括容器信息,服务信息,HPA信息等,方便开发查看。

  • 监控信息,这些信息也是通过直接使用的Prometheus和Heapster的接口来获取的。

  • Web终端,这里也是通过调用Kubernetes相关的接口并写了一个WebSocket服务来实现的,方便开发人员紧急处理问题。


日志系统

在没有采用容器之前,我们已经用ELK来解决日志的问题了,我们主要有2种使用方式:
  • 当服务的日志量不是很大的时候我们采用的是程序直接以json的形式写日志到Redis的list中,然后我们使用Logstash发送到ES中,然后从Kibana上就可以查看了。
  • 当服务的日志量较大的时候,我们采用写文件的方式,然后采用Filebeat发送到Kafka集群,然后采用Logstash发送到ES集群。


06.png

采用Kubernetes容器以后,由于磁盘限制这块不是很完善,我们要求所有的服务都采用输出到Redis的方式或者标准输出的方式来打日志,这样本地磁盘也就不用考虑限制了。这里有一点要说明一下:
  • 由于所有服务都直接采用Redis的方式后,Redis很容易成为瓶颈,如果Redis挂了很可能会影响线上业务,基于这一点,我们让公司的Golang工程师帮忙开发了一个Redis -> Kafka的一个代理服务,这样的话业务不用修改任何代码,还是用Redis的方式输出日志,只不过后面其实是输出到了Kafka集群了,代理服务是无状态的,可以无限扩容,这样性能问题也就解决了。


监控系统

在接入容器之前我们已经采用了Prometheus来监控我们的服务器和业务,Kubernetes对Prometheus的支持很完美,所以我们这块能很快适应,业务代码也不用怎么修改,这里有几个点:
  • 我们在Kubernetes上部署了Prometheus来专门监控所有业务的Pod状态,比如Pod的HPA使用率,Pod的不可用数量等。
  • 由于业务监控需要采集的数据巨大,多个服务如果公用一个Prometheus抓取端的话会有性能问题,我们采用了为每一个服务启动一个Prometheus抓取端的办法来解决这个问题,每个服务单独的Prometheus抓取端通过配置文件中的正则匹配来控制只抓取该服务的数据。
  • 看板采用的是Grafna。服务注册到运维平台的时候也会自动通过Grafna的接口创建相应的看板项目。
  • 报警发送的话我们自己有封装的钉钉/企业微信/邮件/短信等功能的接口。


服务发现

公司用到服务发现的主要是Java的Spring项目,所以采用的是Eureka,我们之前在没有采用容器的时候就已经有了一套Eureka注册中心,由于Pod的网络和虚拟机的网络是直接互通的,服务迁移到容器中以后我们依然采用的之前的一套注册中心。

配置中心

服务迁移到容器的时候我们就要求开发在开发代码的时候生产和测试是同一个jar包,根据启动参数的不同来选择环境,这样的话就可以实现构建一次,生产/测试到处运行了。配置中心这块我们采用的是Apollo,这块也是通过调用Apollo本身的API来集成到运维平台的,做到了从平台上修改/编辑配置等。

踩过的坑

initialDelaySeconds参数导致Pod循环重启问题

这个参数是Kubernetes配置服务的健康检查的时候使用的,我们知道,Kubernetes的健康检查有2种,一种是存活检查,当这个检查失败的时候,Kubernetes会尝试重启该Pod,另一种是就绪检查,当这个检查失败的时候,Kubernetes会把该Pod从Service上摘下来,还有一个点是很重要的,比如我在Kubernetes上部署了一个Java项目,这个项目启动时间大概是30秒,那么我就需要配置上面的这个参数,Pod启动的时候让健康检查的探针30秒以后再执行检查,实际场景中,一个普通的Spring Boot项目启动要花费40秒左右(limit为4核),所以这个值对于Java来说至少需要配置成60才可以,不然经常会出现新部署的Pod一直重启。

使用PreStop Hook保证服务安全退出

在实际生产环境中使用Spring框架,由于服务更新过程中,服务容器被直接终止,由于Eureka Server有缓存,部分请求仍然被分发到终止的容器,虽然有重试机制,但是高并发情况下也经常会遇到接口调用超时/失败的情况,为了减少这些错误,可以在容器退出前主动从Eureka上注销这个节点,等待2倍左右的Eureka检测时间(2*5),这需要开发提供接口并将调用接口的脚本添加到PreStop中,参考:curl http://127.0.0.1/debug/stop/eurekaClient;sleep 10

健康检查超时时间相关配置

实际工作中遇到过一个服务不管怎么配置,总是会莫名奇怪地出现重启的现象,看日志也没有看出来,后来问题找到了,是健康检查配置的问题,最开始使用Kubernetes的时候我们给每个服务配置健康检查的超时时间是1秒,失败1次就算失败;后来跟业务沟通了一下,他们的服务本来就是响应慢,好吧,我们只好修改了一下超时时间,调大了一点,失败次数也改成了3次,连续3次失败才算失败。

Java获取宿主机CPU/内存的问题

由于JVM获取的是宿主机系统的参数,所以导致默认情况下JVM获取的GC线程数和分配的HeapSize不是我们想要的,我们是通过调整JVM启动参数来解决这些问题的,参考:java -server -XX:ParallelGCThreads=4 -Xmx2g -Xms2g -jar service.jar,当然也可以通过其他的方式来修改。

结束语

以上是容器化落地的一个实践案例,以及在容器化过程中碰到的一些问题和解决方案,希望这次分享能对大家有所帮助,我们生产环境的容器化还处于开始阶段,还在逐渐推进中,以后可能会遇到更多的问题,希望能跟大家相互学习,共同进步。

Q&A

Q:PreStop Hook的参考地址能给个外网地址看嘛?

A:这个看官方的文档就行吧,我这个场景里只是用了一个curl,让开发提供一个接口就行。具体PreStop Hook官方文档上有详细的举例。
Q:Apollo配置中心,配置怎么落到服务里的,或者容器里?

A:我们这边大部分是Java项目,使用的官方提供的SDK,具体可以看下Apollo的使用文档。
Q:日志怎么采集和展示?用什么方案?

A:ELK,采集日志主要是程序直接输出到Redis,这点有些不一样。
Q:CD的配置是怎么管理的?

A:相关的上线配置都是存在运维平台上,服务的配置使用的Apollo配置中心。
Q:Kubernetes的HPA组件是原生的吗,只根据CPU内存来进行伸缩,有没有出现过什么问题?

A:是原生的,出现过的问题是,之前都只采用了一个纬度的扩容(CPU),后来发现该Pod经常OOM,后来加上了内存的扩容,Java服务例外。
Q:Prometheus数据怎么保存的,每个实例都存在本地目录吗?

A:我们有专门的Node节点来跑Prometheus Pod通过Node Label调度,采用的本地SSD磁盘,每个服务一个目录,大概这个样子。
Q:还有就是日志部分现在Redis是瓶颈吗,Redis也是集群?

A:分享的时候提到了,Redis是瓶颈,后来公司Golang工程师搞了一个Reids--> Kafka的代理服务,采用的Redis协议,但是直接写入到了Kafka,解决了性能问题。
Q:Prometeus也是Kubernetes管理吗,配置文件他的配置文件怎么管理的?

A:这块我们写了一个简单的服务部署到了Kubernetes的Master节点上,当一个服务接入Kubernetes上以后,运维平台会去掉这个服务的接口,就会创建一个Prometheus Server专门抓取该服务的监控数据,通过Prometheus的配置可以做到只匹配该服务,我们这边是每个服务配置一个单独的Prometheus Server抓取端。
以上内容根据2018年11月20日晚微信群分享内容整理。分享人吴飞群,一下科技运维工程师。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册