经典案例复盘——运维专家讲述如何实现 K8S 落地

阅读数:3 2019 年 1 月 2 日

话题:运维最佳实践Kubernetes

背景介绍

运满满自开始微服务改造以来,线上线下已有数千个微服务的 Java 实例在运行中。这些 Java 实例部署在数百台云服务器或虚机上,除少数访问量较高的关键应用外,大部分实例均混合部署。

这些实例的管理,采用自研平台结合开源软件的方式,已实现通过平台页面按钮菜单执行打包、部署、启动、停止以及回滚指定的版本等基本功能,取得了不错的效果。但仍然存在如下几个痛点:

  1. 实例间资源隔离,尤其在高峰期或故障期间,单服务器上不同实例间 CPU 和内存资源的争抢特别明显。
  2. 线上某个应用实例异常时需要人工干预,导致较长的故障时间。
  3. 大批服务端应用新版上线后,如网站关键功能故障,需要针对每个应用,选择对应的版本,执行回滚操作,整个过程耗时较长。
  4. 线下 DEV/QA 环境频繁发布,每次发布都需要先停止老的版本再发布新的版本,会影响到日常测试。

运满满飞速发展的业务,对系统稳定性的要求越来越高,我们急需解决如上问题。

技术调研、选型

最初吸引我们的是容器技术良好的隔离和水平扩展等特性,而 Docker 的口碑以及几年前参与的一些 Docker 项目经验,使得采用 Docker 容器技术成了我们的不二选择。

但我们仍然需要一套容器编排系统,来实现自动化管理 Docker 容器,大致了解下来有 3 个选项:Kubetnetes(K8S)、swarm、mesos

这 3 个我们都不熟悉,而这个项目的节奏很紧迫,不允许我们对这 3 个系统深入了解后再做选择。好在 Github 有一个统计功能,我们在 Github 上查到了这 3 个开源项目的一些基本情况,如下图:



根据这份统计数据,以及拥有 Google 公司的光环,我们在很短的时间内确定了使用 K8S 作为容器编排管理系统。K8S,这个开源项目号称可以自动部署、扩展和管理容器应用,并且能解决如下核心问题:

负载均衡 - 一个应用运行多个同样的容器,内部 Service 提供了统一的访问定义,以负载均衡的方式来提供访问。

服务发现 - Service 和 Kube-DNS 结合,只需要通过固定的 Service 名称就可以访问到对应的容器,不需要独立寻找使用服务发现组件。

高可用 - K8S 会检查服务的健康状态,发现异常时会自动尝试重新启动服务,保障正常运行。

滚动升级 - 在升级过程中 K8S 会有规划的挨个容器滚动升级,把升级带来的影响降低到最小。

自动伸缩 - 可以配置策略当容器资源使用较高会自动增加新的容器来分担压力, 当资源使用率降低会回收容器。

快速部署 - 编写好对应的编排脚本, 可以在极短的时间部署一套环境。

资源限制 - 对程序限制最大资源使用量避免抢占资源遇到事故或压力也能从容保障基础服务不受影响。

进一步深入了解 K8S 之后,我们大致确定了会用到如下组件、相关技术和系统:

  • 应用部署 K8S Deployment,HPA;
  • 少量基础服务 K8S Daemonset, kube-dns;
  • 对外服务暴露 K8S Ingress, Traefik, Service;
  • 网络插件 Flannel;
  • 监控告警 Heapster, InfluxDB, Grafana, Prometheus;
  • 管理界面 Kubectl, Dashboard, 自研发布管理系统;
  • 制作镜像 Jenkins, Maven, Docker;
  • 镜像仓库 Harbor;
  • 日志收集 Filebeat, Kafka, ELK。

难点和基本原则

  1. 线上服务必须在不间断提供服务的情况下迁移,每个应用按比例切分流量,在确保稳定性的前提下迁移到 K8S 集群中。
  2. DEV 环境可批量上线,QA 和 Production 环境上线需要考虑各应用的版本依赖关系。
  3. 初期只上无状态的应用。
  4. 对研发 /QA 的影响最小化(尽量不给繁忙的研发 /QA 同学增加工作量)。

落地过程剖析

Docker 化前后应用发布流程对比

从下图中可以看到 2 个明显的变化:

  1. 之前部署的是 war 包、jar 包,之后部署的是 Docker 镜像(镜像中包含 war 包、jar 包)。
  2. 之前是先停止再启动应用进程,发布过程中服务会中断,之后是先启动新版本容器,再停止旧版本容器,发布过程中应用一直在提供服务。

迁移中的系统架构

当前业务应用主要分为 2 种,仅供内部应用调用的 RPC 服务(Pigeon 框架)和对外提供服务的 REST API,REST API 可进一步细分为 2 种,已接入 API 网关和未接入 API 网关。其中 RPC 服务和已接入 API 网关的应用均有自己的注册中心,迁移步骤相对简单,在 K8S 集群中启动对应的应用即可。未接入 API 网关的应用采用 K8S Ingress 插件提供对外服务入口,需要一些配置。系统架构如下图,最终目标是要实现将图中下方的两个框内的应用全部迁入 K8S 集群中。

Master 集群的高可用

由于公有云的限制,我们主要结合服务商提供的 SLB 来实现,示意图如下:

K8S 集群内应用对外提供服务

由于集群内 POD 的 IP 地址动态变化,我们采用 Traefik+Ingress+Nginx+SLB 的方式,来提供一个对外服务的统一入口。Traefik 根据 HTTP 请求的域名和路径路由到不同的应用服务,Nginx 则执行一些复杂的诸如 rewrite 等操作,SLB 提供高可用。架构示意图如下:

容器内应用初始化

为了实现同一个镜像可以兼容运行在 DEV、QA、Production 等各种环境,必须编写一个初始化脚本,该脚本被存放在镜像中。当容器启动时,从 Env 变量中读取当前所在的环境,并创建一系列软链到各环境对应的配置文件以及设置日志目录等其他初始化操作,随后 fork 一个新进程用于检测和设置该容器内应用是否已完成正常启动(配合容器 readiness 探针使用),同时调用应用启动脚本。
下图为容器内通过软链指向不同的环境配置文件:

下图为容器内通过软链设置日志目录:

K8S 日志收集

当前应用日志均以文件形式存放,且单个实例对应多个日志文件,无法采用 K8S 官方推荐的日志方案。同时由于容器的无状态化,我们必须另想其他办法保存日志。目前采用的是将 Node 上的固定目录作为存储卷挂载到容器内,在容器启动时通过初始化脚本按照应用名 + 容器 IP 生成该容器特定的日志路径。为了便于查看日志,我们提供 3 种途径:

  1. 容器内启用 SSH 服务端,发布管理系统中实现 WEBSSH,正常情况下可通过 WEB 页面进入容器命令行查看日志,由于其便利性,推荐首选此方式。
  2. 有些情况下容器会启动失败,此时无法进入命令行,可在发布管理系统中找到日志的链接地址,下载到本机后再查看。
  3. 此外,我们在所有 Node 上各运行一个 Filebeat 容器,将 Node 上收集到的日志实时发送到 Kafka 集群中,经过处理后存储到 ES 集群,以便日后检索。

下图为 Node 服务器上的日志目录结构:

下图为 Node 服务器上共享的日志下载路径:

K8S 监控

采用 Heapster+InfluxDB+Grafana 组合,需要注意的是其中 InfluxDB 用于存放监控数据,需要将数据持久化。在 Grafana 上制作了不同维度的 dashboard,可根据 Namespace、Node、应用名进行检索,可按照 CPU、内存、网络带宽、硬盘使用量筛选应用,方便故障排查和日常优化。(当然,更好的监控系统是 Prometheus,已经在上线的路上。)

下图为监控大盘:

下图为监控菜单:

下图为某应用的监控图:

Harbor 镜像仓库

Harbor 我们目前采用的是一主多从结构,主库与打包 Jenkins 都在线下网络中,镜像上传到主库后会被自动同步到线下另一个从库以及线上的从库中,如下图所示:

镜像树

我们的规划是构建一颗镜像树,所有的应用都基于这颗树上的基础镜像来构建应用镜像,各应用构建时选择最相似的基础镜像,再增加应用的特殊需求即可。基于此镜像树,我们 95% 以上的应用均无需在 Gitlab 里放置 Dockerfile,Dockerfile 在打包时根据变量自动生成即可,例如:

下图为脚本自动生成的某应用 Dockerfile:

镜像树结构示意图如下:

当前状态

容器化:DEV/QA 环境的应用已完成 Docker 化,产品环境中应用约 98% 已完成 Docker 化。

系统自愈:应用 OOM 或其他 Crash 时,系统能够自动拉起新的节点以替换故障节点,高级健康检查暂未开启(需其他方面配合)。

弹性伸缩:关键应用全部开启弹性伸缩,访问量高峰期观察到的效果很好。

滚动发布:可按指定的比例分批次部署更新应用版本,先更新一批,成功后销毁一批,依次滚动。

快速回滚:当前仅支持单应用快速回滚,后期如需要增加事务级回滚能力,采用 K8S 的 rollout 功能可以方便实现。

一些踩过的坑和建议

  1. 底层操作系统采用 CentOS7.x 版本,会比较省事。
  2. 阿里云经典网络中的 ECS 无法访问容器 IP,需要先迁移到 VPC 环境,其他公有云情况类似,重点是能自主添加路由。
  3. 如果有应用级监控的话,从容器内部采集到的 Memory,Load Average 等信息是底层操作系统的,而不是容器的,这些指标可以依赖专门的容器监控系统。
  4. 要注意 ulimit 的限制,容器中并没有对它进行隔离,设置过小的话会遇到一些莫名其妙的问题。
  5. 容器中的 root 用户用 netstat 命令可能看不到其他用户所创建的进程的 owner,如果有一些老式的脚本可能会遇到类似问题。
  6. 如果有一些内部系统需要直接访问容器的特定端口,headless service 挺好用。
  7. Zookeeper 有一个单 IP 连接数 60 的默认限制,如果没修改过该参数的话应用迁移到 K8S 之后可能会遇到此问题。
  8. 产品环境中的某个访问量大的应用往 K8S 迁移时,可以先分配较多数量的容器,确保能吃下所有流量,之后再根据监控,用弹性伸缩功能来减掉多余的容器。
  9. 如果想提前知道 K8S 集群的性能,部署好应用之后做一次压测很有必要。

作者简介

王春林,就职于运满满技术保障部,关注容器、DevOPS 等领域。