网易传媒经过了一年多的探索及建设,已经将所有核心业务部署在容器环境内了。本文将以笔者在网易传媒整个容器的规划、设计、实施为内容,分享传媒在整个容器建设过程中的经验和教训,希望能给正在容器建设道路上前行的朋友们提供一些指导和帮助,避免走一些弯路。

1 什么是云原生

前两章为云原生的介绍和传媒的基础架构介绍,如果读者对这两块内容不感兴趣,可以直接跳到第三章

云原生的产生

云原生英文为Cloud Native,这个概念是Pivotal公司在2013年的时候首次提出,2015年,Pivotal公司的Matt Stine撰写了一本名为《迁移到云原生应用架构》的手册,对云原生架构的几个主要特征做了阐述:符合十二因素的应用、面向微服务、自助服务的敏捷基础架构、基于API协作和抗脆弱性。2017年,Matt Stine在接受InfoQ采访的时候,对云原生定义做了小幅调整,将云原生架构定义为以下六个特征:模块化(Modularity)、可观测性(Observability)、可部署性(Deployability)、可测试性(Testability)、可处理性(Disposability)以及可替换性(Replaceability)。Pivotal也对云原生概括为4个要点:DevOps、持续交付、微服务和容器化。

什么是云原生容器化平台 云原生 容器_IP

CNCF在2015年由Google联合Linux基金会成立,主要工作是统一云计算接口和相关标准,推动云计算和服务的发展。2018年6月11日CNCF明确了云原生定义1.0版本:云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。

什么是云原生容器化平台 云原生 容器_什么是云原生容器化平台_02

云原生本身不能称为是一种架构,它首先是一种基础设施,运行在其上的应用称作云原生应用,只有符合云原生设计哲学的应用架构才叫云原生应用架构。

云原生系统的设计理念如下:

  • 面向分布式设计(Distribution):容器、微服务、API 驱动的开发;
  • 面向配置设计(Configuration):一个镜像,多个环境配置;
  • 面向韧性设计(Resistancy):故障容忍和自愈;
  • 面向弹性设计(Elasticity):弹性扩展和对环境变化(负载)做出响应;
  • 面向交付设计(Delivery):自动拉起,缩短交付时间;
  • 面向性能设计(Performance):响应式,并发和资源高效利用;
  • 面向自动化设计(Automation):自动化的 DevOps;
  • 面向诊断性设计(Diagnosability):集群级别的日志、metric 和追踪;
  • 面向安全性设计(Security):安全端点、API Gateway、端到端加密;

容器技术

我们现在聊容器技术避不开云原生,聊云原生也避不开容器技术,它们就是一对双螺旋体,容器技术催生了云原生思潮,云原生生态推动了容器技术发展。

随着云原生的普及,容器技术逐渐被人们熟知并迅速席卷全球,颠覆了应用的开发、交付和运行模式,在云计算、互联网等领域得到了广泛应用,在容器技术中,使用最为广泛的就是Docker和Kubernetes了,它两长期占据着容器技术的霸主地位。

容器  VS VM

首先强调一点:Docker是容器的一种实现,不过目前Docker引擎的使用最为广泛,以至于容易将容器和Docker混淆。

虚拟化和容器最大的差别就在于虚拟化是强隔离技术,容器是弱隔离技术,虚拟化通过Hypervisor实现了VM与底层硬件的解耦,而容器技术使用了Linux内核提供的Cgroup、Namespace等技术,将应用程序及其运行依赖环境打包封装到标准化、强移植的镜像中,通过容器引擎提供进程隔离、资源可限制的运行环境,实现应用与OS平台及底层硬件的解耦。

什么是云原生容器化平台 云原生 容器_什么是云原生容器化平台_03

通过上图可以看到,VM中包含了Hypervisor、Guest OS层,资源占用很重,而容器并没有这两层,更加的轻量。在此注意:我们进入虚拟机环境后,运行的TOP命令、Java、C、Python等语言运行时所拿到的CPU核数和内存是虚拟机本身的,而进入容器实例环境后,我们运行的TOP命令、Java、C、Python等语言运行时拿到的CPU核数和内存时物理机本身的,这就是因为容器本身是基于Cgroup文件隔离,TOP命令和各种语言如果不对容器做适配,还是拿的是物理机资源信息。

容器和VM相比,更适用于云原生的快速交付、快速迭代,是微服务的最佳载体。

下表出了容器和VM的差别

对比项

虚拟机VM

容器

镜像大小

包含GuestOS,一般在G以上 

仅包含应用程序运行的Bin/Lib,一般在M以上

资源要求

CPU和内存资源分配按核,按G分配

CPU和内存资源分配可以小到0.5核,几M分配

启动时间

分钟级

毫秒/秒级

可移植性

跨物理机迁移

跨OS平台迁移

性能

较弱

接近原生

系统支持量

一般几十个

单机支持上千个容器

隔离策略

OS、系统级

Cgroup、进程、内核级别

Docker除了使用Linux的NameSpace、Cgroup等内核进行隔离外,笔者认为Docker还有一个核心的能力:容器镜像,容器镜像类似于我们的Java程序发布后的Jar包,这些Jar包通过JVM可以在不同的操作系统中运行一样,Docker打完的容器镜像也可以通过Docker Engine在不同的操作系统中运行。容器镜像这种新型的应用打包、分发和运行机制,它将应用运行环境,包括代码、依赖库、工具、资源文件和元信息等,打包成一种操作系统发行版无关的不可变更软件包,并且将这个镜像能推送到中心镜像仓库中,从而实现“build once, run anywhere”。容器镜像打包了整个容器运行依赖的环境,以避免依赖运行容器的服务器的操作系统容器,镜像一旦构建完成,就变成read only,成为不可变基础设施的一份子(不知道大家还记得VM或物理机时代,部署应用,升级应用的时候常常出现的文件冲突、版本冲突、依赖冲突等等头疼的问题吗)。

Kubernetes

Docker解决了应用运行和隔离的问题,但在一个大数据中心里,如何将应用比较好的进行调度,如何将Docker实例运行调度到合适的宿主机上,这些都是Kubernetes需要解决的问题,它主要提供如下能力:

  • 应用的快速部署、扩缩容。
  • 基于不同策略的应用调度、升级、故障自动恢复。
  • 应用集群的负载均衡、服务发现。
  • 提供统一的扩展接口以支持自定义插件。

用户直接将自己所期望的部署状态告诉kubernetes,kubernetes会比较当前状态和期望状态的差异,并执行一系列操作把集群调整到与用户预期状态相一致,这一过程我们通常把它称之为同步(sync)或协调(reconcile)。kubernetes定义了大量的API资源提供给用户,用户通过创建不同的API资源来告诉kubernetes“所期望的部署状态”。

Kubernetes 的目标不仅仅是一个编排系统,而是提供一个规范用以描述集群的架构,定义服务的最终状态,使系统自动地达到和维持该状态。Kubernetes 作为云原生应用的基石,相当于一个云原生操作系统。

接下来笔者描述Kubernetes的几个基础核心概念:

  • Namespace:用来对资源做隔离划分,你可以在一个kubernetes集群中拥有多个逻辑上完全隔离的Namespace,对于每个Namespace可以独立管理,设置不同的Quota(资源配额)。
  • Node:可以是一台物理机,也可以是一台虚拟机,它们都被统一抽象成Node这个资源,容器最终会被调度到不同的Node上运行。
  • Pod:集群管理的最小单元,一个Pod可以理解kubernetes中应用程序的单个实例,一个Pod可以包含多个容器,kubernetes对容器的管理调度其实就是对Pod的调度,Pod是归属于Namespace,每个Pod可以有自己的独立的网络协议栈,被分配唯一的IP地址,也可以直接使用宿主机的网络,同一个Pod中的所有容器会共享同一个网络协议栈,在网络层面上我们可以将Pod理解为是一台独立的主机,Pod中的所有容器也可以共享挂载,使用持久化的存储。
  • Deployment:能够快速部署一组相同的Pod,Deployment支持应用集群的扩缩容、滚动升级、回滚等,保证服务的高可用。
  • StatefulSet:能自动管理一组Pod,但StatsfulSet用于部署有状态的应用,这类应用通常需要唯一不变的网络标识,需要提供稳定持久的存储,需要有序的升级扩缩容。
  • Service:应用服务的抽象,由于Pod的生命周期是无法保证的,随时有可能终止,重启,重新调度,因此Pod的IP等都是动态变化的,对于需要对外暴露服务的应用来说,需要提供一个固定的访问地址,Service通过筛选包含了一组符合条件的Pod,并为它们提供了简单的负载均衡。

Kubernetes的逻辑架构图和核心组件描述如下:

什么是云原生容器化平台 云原生 容器_什么是云原生容器化平台_04

  • etcd:保存集群数据的数据库。
  • kube-apiserver:负责提供kubernetes API服务的核心组件,访问kubernetes集群本质上访问的就是集群的kube-apiserver,它集成了认证、授权、准入控制等API管理功能。
  • kube-scheduler:调度器,通过监听Pod资源,对未被调度的Pod,按照既定的调度算法为Pod分配Node。
  • kube-controller-manager:控制器,其中运行了一系列controller来负责维护集群的状态。
  • kubelet:集群中运行在每个Node上的代理服务,kubelet从kube-apiserver中获取被调度到该Node上的所有Pod的信息, 并管理这些Pod的生命周期。
  • kube-proxy:每个集群上运行的网络代理,以实现kubernetes的Sercive网络模型。

2 网易传媒基础框架

网易传媒对基础架构的能力和要求做了一些说明,主要分为以下几个方面:

  • 稳定可靠:这是基本的要求,基础架构要为业务提供稳定可靠的基础资源、公共服务、基础组件。
  • 流程化、标准化、自动化:提供申请、使用、操作资源的标准流程,在这些流程中,实现基础操作的自动执行,自动验证,自动通知申请人。
  • 提升整体资源利用率:在保证业务稳定可靠的前提下,能够提升整体的资源利用率。
  • 弹性:应对突发流程能快速扩容资源(包括集群内资源和云厂商资源),快速启动服务,流量过去后能自动缩容资源。
  • 安全:能防刷、防DDos攻击,当出现网络攻击的时候,快速通知,快速响应。
  • 可监控:提供丰富的监控手段,让业务快速定位问题。
  • 规范化:提供统一的开发框架及规范,提供基础组件,减少业务方的研究时间,避免重复造轮子,避免大家都踩坑。

为了能应对业务的需求,传媒基础团队整理了整体的基础框架,框架如下图所示:

什么是云原生容器化平台 云原生 容器_IP_05

  • 最底层为资源层:为传媒整个业务提供基础资源。
  • 容器基础层:基于Docker+Kubernetes为基础,并为以后能够扩容共有云资源做准备
  • 技术中台:为业务提供基础、公共的中台服务。
  • 监控组件:从资源层、业务层、日志层、链路层提供完整的监控服务。
  • 开发规范:定义代码规范、日志规范和分支规范,业务使用统一的规范,避免代码的不统一。
  • 完成的测试服务:从功能、性能、接口等测试能力,到线上的全链路测试及混沌测试。
  • 运维管理工具组件:从发布到安全,从成本到容量,为整个传媒的业务提供完整的运维工具组件。
  • 网关、门户:最上层为业务方提供了统一的网关入口及门户入口,对外部流量进行限制、监控及告警。

3 容器建设方案

以上对云原生、容器、调度及传媒的基础架构做了描述,接下来笔者将详细描述传媒在容器集群建设方面所做的工作。

建设背景

在传媒进行决定容器化之前,已经建成了自己的专属云,但在资源使用率、应对突发流量、资源回收等方面都有比较大的挑战。

  • 研发工程师:
  • 应用上线、扩容等操作的时候,都需要向领导申请资源(邮件方式)。
  • 资源到位后,需要手动在CMDB认领资源,并将资源加到集群中,才能进行应用的发布操作。
  • 应用下线,需要手动缩容,将资源释放,在CMDB中将资源删除,并通知运维进行回收。
  • 为了应对突发流量,线上应用资源都是按最大流量进行申请,对于资源的使用造成了比较大的浪费。
  • 当前资源不足以应对突发流量时,打电话,发消息,找运维手动创建VM,严重拖慢了应对突发事件的时间。
  • 运维工程师:
  • 需要定期查看专属云资源池的整体资源使用情况,并通知资源使用率较低的业务进行缩容操作。
  • 业务将资源释放后,需要手动将VM删除。
  • 研发人员资源使用结束后,一般也不会释放资源,运维人员需要定期查看占用资源不使用的业务,并督促让业务做下线操作,耗时耗力。
  • 突发流量,需要手动扩容虚拟机,并通知业务方进行认领、发布操作。
  • 管理者:
  • 经常要收到常规性的资源申请邮件并进行处理。
  • 资源成本持续增加,资源的有效利用率不是很高,资源的申请也不太好拒绝。
  • 业务应对突发流量的稳定性堪忧。

基于以上的问题,传媒决定将资源池话,并通过Kubernetes进行资源的整体调度,部署到资源池的应用可以弹性扩缩容,做到随用随创建,后期也计划和云计算提供商(阿里、腾讯等)合作,使用它们的算力资源,当传媒资源池的资源不够使用的情况下,可以弹性扩展云计算提供商的资源,使用时开始计费,使用结束停止计费。从而实现资源调度自动化的目的,免去研发申请资源,手动扩缩容的操作,免去运维手动维护资源的操作,也对突发流量的应对提供了资源的支撑。

集群部署规划

在集群搭建之前,调研了每个业务团队,对他们目前的业务架构和业务特性进行了详细的沟通,总结下来,主要有几方面

  • 大部分应用是无状态的,对于容器的重调度可以接受,需要能保证重调度对于业务是无影响的,能够做到优雅的下线及上线。
  • 部分应用加载时间较长(模型计算类服务、预读缓存类服务),尽量保证不重调度,如果要重调度,也要保证数据加载完成后才对外提供服务。
  • 服务调用的协议比较多,有HTTP、gRPC、Dubbo、Thrfit,注册中心有Consul、Zookeeper、Eureka,业务希望调用方式尽量保持,不希望改动。
  • 有部分应用的业务逻辑使用IP Hash。
  • 有部分业务使用了Nginx做流量转发,也在Nginx使用了Lua脚本语言,希望这些Nginx能够保留。
  • 大部分服务都会连接MySQL数据库,数据库有白名单限制,大部分业务的白名单可以取消,但一些敏感业务对IP白名单限制有要求。
  • 部分业务为有状态服务,需要读取、写入本地数据。
  • 部分业务对IP可达性有要求(Dubbo、Thrfit服务),希望POD的IP地址在K8S集群外也可达。

经过对业务规模、业务特性及业务需求的调研分析后,初步评估整个集群的规模达到百台以上的节点,POD上万,属于比较大的容器集群,同时,除了容器集群外,对不能进入容器的业务,也要保留虚拟机及物理机资源,并且这些业务提供的服务都要可达,基于以上的业务需求,对集群部署架构做了如下设计

什么是云原生容器化平台 云原生 容器_Docker_06

传媒的所有核心业务都部署在VPC内,VPC内包括虚拟机和容器,VPC内的所有IP都可达(包括VM和容器),由于物理机还未进VPC,有部分业务还依赖物理机,这部分服务需要通过DGW网关互通,入口机统一使用NLB进入VPC,VPC内的服务通过SNAT的方式访问外网。

物理机初始化

传媒容器集群统一使用物理机做为计算节点,在物理机上架前,做如下装机配置

  • OS使用集团提供的Debian10。
  • CPU开启performance模式。
  • 预装proton-access-agent、nagent4docker、dnsmasq。
  • ulimit配置为655360,关闭swap配置,关闭transparent_hugepage配置,关闭kmem cgroup配置。
  • 磁盘分为系统盘和数据盘,系统盘安装Debian10 OS,数据盘双盘Raid1,保存Docker容器实例数据。
  • 网卡中断绑定分散到所有CPU,rps_cpus设置为默认值0,rss设置为和cpu一致数目的队列数,且需要hash分发到最多的队列。

部署管控节点

传媒使用的Kubernetes版本是杭研定制版本:v1.11.9-netease,Docker版本为18.06.3,管控面部署在云内物理机,主要包括K8S控制面组件和一些系统组件

  • kube-apiserver:kubernetes的核心组件,对外提供API接口,属于无状态服务,通过static pod部署,在3台管控物理机节点上部署3副本,通过域名实现高可用。
  • kube-controller:监控集群状态,努力使其达到用户所期望的状态,属于无状态服务,通过static pod部署,在3台管控物理机节点上部署3副本,通过K8S内置的LeaderElection实现高可用。
  • kube-scheduler:POD调度器,属于无状态服务,通过static pod部署,在3台管控物理机节点上部署3副本,通过K8S内置的LeaderElection实现高可用。
  • etcd:存储元数据,属于有状态服务,通过static pod部署,在3台ETCD物理机节点上部署3副本,使用raft做一致性协议,单台故障不影响。

除了Kubernetes的组件外,集群内还部署了一些必要的稳定性组件

  • dns:容器内使用coredns,物理机部署dnsmasq,在3台CoreDNS物理机节点上部署3副本,单台机器故障不影响。
  • prometheus:容器性能监控组件,单副本,部署到数据节点。
  • alertmanager:监控告警组件,单副本,部署到数据节点。

对容器内的每个节点都设置了一些公共标签,标签如下:

kubernetes.io/arch=amd64
kubernetes.io/instance-type=bareMetal
kubernetes.io/os=linux
kubernetes.io/hostname=xxx.xx.163.org
network.netease.com/zone=xxx
network.netease.com/single-subnet-id=xxxxxxxx

设置管控节点的Role为master,为了防止业务POD调度到管控节点,我们对管控节点设置标签和污点。

标签如下:

node-role.kubernetes.io/master=

污点标记如下:

node-role.kubernetes.io/master:NoSchedule

部署计算节点

计算节点部署在云内物理机,主要包括K8S控制面组件和一些系统组件。

  • kubelet:计算节点容器管理控制器,以service方式启动。
  • kube-proxy:k8s service与POD的路由管理,使用iptables的方式进行管理,通过DaemonSet的方式部署到每台节点上。

除了Kubernetes的组件外,集群内还部署了一些必要的稳定性组件。

  • cleanlog:日志清理组件,通过DaemonSet的方式部署到每台节点上。
  • node-problem-detector:节点故障检测组件,通过DaemonSet的方式部署到每台节点上。
  • logagent:容器日志采集组件,通过DaemonSet的方式部署到每台节点上。
  • kraken-agent:镜像P2P加速组件,通过DaemonSet的方式部署到每台节点上。

传媒将计算资源按使用方式划分为不同的资源池,每个资源池都会有一组计算节点供特定的业务方使用,整体资源池规划如下图所示:

什么是云原生容器化平台 云原生 容器_Pod_07

在每个计算节点上都打了Label

node.netease.com/node-pool=app
node-role.kubernetes.io/node=
network.netease.com/kube-proxy=lb-xxxxxx
topology.netease.com/rack-group=XXXXXXXXXXXX

计算节点可用的资源包括:CPU、内存和最大POD数量,为了能在每个节点上部署更多的POD,对部分节点设置了超售,超售比例包括2倍和4倍,通过设置节点的capacity实现,设置allocatable的值限定了POD消耗的资源量。

在启动kubelet的时候,设置了系统保留机kubelet自身保留的CPU和内存容量。

节点运行状态,使用conditions字段描述,主要包括如下内容:

条件项

描述

OutOfDisk

True 表示节点的空闲空间不足以用于添加新pods, 否则为False

Ready

表示节点是健康的并已经准备好接受 pods;False 表示节点不健康而且不能接受 pods;Unknown 表示节点控制器在最近 40 秒内没有收到节点的消息

MemoryPressure

True 表示节点存在内存压力 – 即节点内存用量低,否则为 False

PIDPressure

True 表示节点存在进程压力 – 即进程过多;否则为 False

DiskPressure

True 表示节点存在磁盘压力 – 即磁盘可用量低,否则为 False

NetworkUnavailable

True 表示节点网络配置不正确;否则为 False

计算节点的磁盘分为系统盘和数据盘,数据盘使用双盘Raid1,数据盘挂载目录为/data,将docker目录的/var/lib/docker直接挂载到数据盘的/data/docker下,将kubelet目录的/var/lib/kubelet挂载到/data/kubelet目录下。

计算节点上线时预设的内核全局配置如下:

kernel.pid_max = 2821360
kernel.threads-max = 2821360
net.netfilter.nf_conntrack_checksum = 0
net.ipv4.netfilter.ip_conntrack_tcp_be_liberal = 1
net.ipv4.ip_local_port_range = 35000   60999
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
vm.max_map_count = 5642720
fs.inotify.max_queued_events = 1048576
fs.inotify.max_user_instances = 1048576
fs.inotify.max_user_watches = 1048576
net.core.somaxconn = 1024

容器网络设计

传媒的基础网络都在VPC内,使用了 OVS网络,使用杭研自研的CNI插件,从Proton SDN中获取可用的IP地址,初始化到POD网络中,除此之外,使用杭研自研的Scud网络IP管理方案,实现了容器IP地址的跨节点能力。

整体网络架构如下图所示:

什么是云原生容器化平台 云原生 容器_Docker_08

传媒从集团申请了10.XXX.XX.XX/XX的网段,在这个大网段下,又划分为若干子网,这些子网包括:管理网段、出外网网段、不出外网网段。设置了出外网和不出外网两个路由表,将不同的网段关联到不同的路由表中。

什么是云原生容器化平台 云原生 容器_Pod_09

从上图可以看到在VPC的网段下又划分了很多的子网网段,用于不同的需求

除此之外,传媒使用了杭研自研的Scud网络IP管理方案,Scud是由杭研自研的容器IP管理解决方案,是一套基于kubebuilder开发的operator,主要解决了IP选择、IP保持、IP漂移的问题,实现了IP可以跨节点漂移(业界大部分容器网络方案为了减少路由条目、提高性能和方便管理,都会将pod的IP范围按node划分,每个node有一个cidr)。

Scud将整个“可漂移网络方案”中的各种概念抽象为CRD,包括以下几类:

  • Subnet:表示集群里pod可以用的一个大的cidr。
  • IPAllocation:表示一个Subnet下的被使用的IP地址。
  • IPRange:表示一个Subnet下的小规格cidr。
  • IPPool:表示多个IPRange的集合,是pod的直接引用对象。
  • PodStickyIP:表示pod与IP的关联关系,用于做IP保持。

容器POD设计

POD是可以在Kubernetes中创建和管理的最小可部署计算单元,它是一组容器,共享着存储、网络机运行这些容器的声明,传媒将POD定义为一个最小的计算单元,类似于一个虚拟机,将Deployment定义为一个集群,业务操作对象都是一个个集群。

我们已经将资源划分为若干资源池,应用在发布的时候,统一发布到APP资源池,在POD的yaml中,设置如下标签

nodeSelector:
    node.netease.com/node-pool: app

为了保证集群的POD都能打散到不同的机柜上,需要设置POD的机柜反亲和性,设置标签如下

topologyKey: topology.netease.com/rack-group

在应用启动及停止过程中,Kubernetes提供了livenessProbe和readinessProbe探针,livenessProbe用于判断应用是否存活,如果发现应用未存活,则会重新调度这个POD,readinessProbe用于判断POD是否可以接受流量,如果POD已经Ready了,则Kubernetes会将这个POD的IP加载到EndPoint里,即可对外提供服务。

传媒使用这两个探针来保证业务的可用性,设置的标签如下:

livenessProbe:
  failureThreshold: 15
  httpGet:
    path: /healthz/status
    port: 8080
    scheme: HTTP
  initialDelaySeconds: 100
  periodSeconds: 10
  successThreshold: 1
  timeoutSeconds: 50
readinessProbe:
  failureThreshold: 15
  httpGet:
    path: healthz/online
    port: 8080
    scheme: HTTP
  initialDelaySeconds: 75
  periodSeconds: 10
  successThreshold: 1
  timeoutSeconds: 50

在POD中,PID为1的进程扮演了十分重要的角色,容器启动后由1号进程+派生的整个进程树,当Docker停止一个容器的时候,发送stop命令,其实就是发送一个SIGTERM信号给容器内的1号进程,1号进程接收到信号后退出,这时,1号进程需要通知业务进程退出。有些业务进程是以后台的方式运行的,业务进程启动后,由于1号进程退出,Kubernetes会认为这个POD已经Completed,为此,传媒容器实例在启动POD的时候,会预加载一个appCtrl脚本,用于拉起业务进程,判断业务进程的状态,如果业务进程退出,则快速拉起。

appCtrl脚本启动方式如下(截取了部分关键内容):

- args:
    - -a
    - deploy
    - -C
    - '{"deploy":"....."}'
    command:
    - /home/xxx/xxxx/ctrl.sh
    env:
    - name: com_cmdb_clustername
      value: toutiao-xxxx-grab_online
    - name: com_cmdb_appname
      value: toutiao-xxxx-grab
    - name: NAGENT_CLUSTER_NAME
      value: toutiao-xxxx-grab_online

在Kubernetes销毁前,会执行preStop设置的脚步,传媒通过appCtrl脚本在preStop的时候执行一些必要的检查和通知动作,脚本如下:

lifecycle:
  preStop:
    exec:
      command:
      - /home/xxx/xxxx/ctrl.py
      - -a
      - prestop
      - -C
      - '{"request":"..."}'

在POD启动的时候,需要将本机的时区以volumnMount的方式绑定到Docker里,绑定方式如下

volumeMounts:
- name: localtime
  mountPath: /etc/localtime
volumes:
- name: localtime
  hostPath:
    path: /etc/localtime
    type: ""

Kubernetes在Namespace下创建的POD数没有限制,但如果创建过多的POD,当通过API拉取指定Namespace下的POD时,会导致API Server的压力较大,建议每个Namespace下的POD数量不要超过2000,过多的POD可以再新建Namespace。

在初始化计算节点的时候,我们通过Allocatable和Capacity参数的调整实现节点的超售,我们在创建POD的时候,通过设置request和limit也实现了节点资源的超售,超售比例目前按2倍超售(只是对CPU资源做了超售,内存资源属于独享资源,未做超售),设置脚本如下:

resources:
  limits:
    cpu: "4"
    memory: 16G
  requests:
    cpu: "2"
    memory: 16G

我们在集群中创建了一个default IPPool,POD在创建启动的时候,通过CNI获取IP地址,大部分的业务都会在这个Pool里获取可以使用的IP,如果有业务对IP有特殊限制,可以根据业务需求创建独立的IPPool。在创建POD的时候增加如下标签可以选择使用的IPPool

annotations:
  network.netease.com/kubernetes.ippool.name: public-default

容器服务设计

在Kubernetes里,POD的生命周期是不稳定的,它的IP地址也是不固定的,所以Kubernetes提出了Service的概念,service定义了一个服务的入口地址,它通过label selector 关联后端的pod,service会被自动分配一个ClusterIP,service的生命周期是稳定的,它的ClusterIP也不会发生改变,用户通过访问service的ClusterIP来访问后端的pod,所以,不管后端pod如何扩缩容、如何删除重建,客户端都不需要关心。

什么是云原生容器化平台 云原生 容器_容器_10

Service中有几个关键字段

  • spec.selector: 通过该字段关联属于该service的pod。
  • spec.clusterIP: k8s自动分配的虚拟ip地址。
  • spec.ports: 定义了监听端口和目的端口。用户可以通过访问clusterip:监听端口来访问后端的pod。

当用户创建一个service时,kube-controller-manager会自动创建一个跟service同名的endpoints资源,endpoints资源中,保存了该service关联的pod列表,这个列表是kube-controller-manager自动维护的,当发生pod的增删时,这个列表会被自动刷新。

k8s在每个node上运行了一个kube-proxy组件,kube-proxy会watch service和endpoints资源,通过配置iptables规则来实现service的负载均衡,当用户访问clusterip:port时,iptables会通过iptables DNAT 均衡的负载均衡到后端pod。

kubernetes是有自己的域名解析服务,域名格式为:${ServiceName}.${Namespace}.svc.${ClusterDomain}. 其中${ClusterDomain}的默认值是cluster.local。

传媒的Service使用iptables来管理路由,主要考虑到以下几个方面的原因

  • 业务使用内网域名(HTTP协议)的方式访问服务比较多  (下一步完善)。
  • Dubbo服务有自己的注册中心,SpringCloud有Eureka的服务注册中心   (下一步完善)。
  • 下一步将要推广ServiceMesh,不需要iptalbes路由。
  • ipvs虽然在路由表上有很大优势,但ipvs本身也有很多不兼容的地方,使用ipvs的收益不是很明显。

传媒内部推荐使用service的服务才创建,否则就创建POD和Deployment即可,不需要创建Service,对于已经接入Mesh的服务,只创建Service和EndPoint,但不下发iptables路由,具体yaml配置如下

spec:
  template:
    metadata:
      labels:
        service.kubernetes.io/service-proxy-name: istio

LoadBalancer Service使用杭研NLB服务开发的,组件名称为cloud-controller-manager

什么是云原生容器化平台 云原生 容器_IP_11

cloud-controller-manager组件会list watch services和endpoints资源:

  • 当watch到LoadBalancer类型的service创建时,会调用NLB接口创建NLB实例,并且将NLB实例的VIP设置到svc.status.loadBalancer字段。
  • 当watch到LoadBalancer service下纳管的pod发生变更(即endpoints资源发生变化时),会调用NLB接口自动刷新后端实例列表。
  • 当watch到LoadBalancer service被删除时,cloud-controller-manager会调用NLB接口删除NLB实例。

监控与告警

传媒容器集群监控方案使用社区成熟的 Prometheus 方案,以下是整体架构图:

什么是云原生容器化平台 云原生 容器_Pod_12

Prometheus 主要由 Prometheus、Alertmanager、Grafana、Node Exporter、Kube State Metrics 等组件组成,集群的核心监控指标数据由 Kubelet、APIServer、Controller Manager、Scheduler、Etcd 等组件提供。

Prometheus的主要特性描述如下:

  • Prometheus 以键值对的形式对监控指标进行存储,并对这些监控指标打上标签进行管理。
  • Prometheus 内置实现了一个时间序列数据库对监控数据进行存储到本地或TSDB中。
  • Prometheus 的监控数据收集是基于拉取模式的,被监控的组件通过特定的 URL 输出 Prometheus 格式的数据,Prometheus 每隔一段时间从该接口拉取一次监控数据并存储到服务端。
  • Prometheus 支持 Kubernetes 的服务发现功能,用户可以通过在 Prometheus 配置文件中添加相关项将 Service、Pod、Node 等注册到 Prometheus 中去。
  • Prometheus 定义了 PromQL 查询语法,通过 PromQL 编写监控以及报警的 Rule 可以定义 Prometheus 的监控细项以及触发报警的条件。

Prometheus 部署包括以下步骤:

  • 为监控节点打标签。
kubectl label node $NODE_NAME node-role.kubernetes.io/monitoring=
  • 创建用于访问 Etcd 的 客户端证书 Secret。
kubectl -n monitoring create secret generic \
  kube-etcd-client-certs \
  --dry-run \
  --from-file=etcd-client-ca.crt=/etc/kubernetes/pki/ca.crt \
  --from-file=etcd-client.crt=/etc/kubernetes/pki/apiserver-etcd-client.crt \
  --from-file=etcd-client.key=/etc/kubernetes/pki/apiserver-etcd-client.key \
  -o yaml > kube-etcd-client-certs.yaml
kubectl apply -f kube-etcd-client-certs.yaml
  • 部署 Kube Prometheus。
  • 检查 Prometheus 监控状态。
  • 访问该集群的 Prometheus 页面查看监控状态,任意节点的 Kubernetes 集群注册 IP 加 30900 端口可访问。
  • 部署 Alertmanager Webhook Server。

最佳实践

传媒在Kubernetes迁移过程中,遇到了很多问题,也总结了一些最佳实践,篇幅所限,笔者仅列出部分。

优雅关闭

在容器的概念出现之前,大多数应用都直接在物理机或虚拟机上运行,所以恢复故障的应用需要花费很多时间。如果你只有一两台机器,那么恢复的时长就不太能让人接受了,取而代之的是通过一些守护者进程来监视应用,重启应用。一旦应用异常终止,守护者进程会捕获exit code并立即重启应用。

随着诸如kubernetes之类系统的出现,我们不再需要守护进程来监视系统,kubernetes会自行处理崩溃的应用。kubernetes通过事件循环来保证资源(比如Pod和Node)的健康,这也就意味着你不需要人工运行任何守护进程,如果某一实例的健康检查失败了,kubernetes会自动替换新的副本。

优雅关闭就意味着你的应用能够处理SIGTERM信号并在收到信号后启动该关闭流程。在该流程中,你可能需要保存需要保存的数据,关闭网络连接,完成那些剩余的工作等。

一旦kubernetes决定终止你的Pod,会执行如下步骤

  • Pod被设置为Terminating状态并从Service的Endpoint中移除。
  • 执行preStop Hook。
  • SIGTERM信号被发送到Pod。
  • kubernetes为优雅关闭等待一段时间。
  • SIGKILL信号发送到Pod并真正删除Pod。

appCtrl脚本接收到SIGTERM信号后调用业务提供的Hook方法,业务开始执行退出操作

我们需要保证应用程序能够被优雅的关闭,因为Kubernetes会以各种理由终止POD的运行。

配置健康检查

在Kubernetes集群中,健康检查时必需的,业务应用需要提供Readiness和Liveness的HTTP接口,能让kubernetes和appCtrl脚本定期检查业务的健康状态

  • Readiness

假设你的应用从容器运行开始需要几分钟才能够完成初始化,那么尽管容器的进程已经启动了,你的Service在应用的容器初始化完成之前并不能正常工作。还有一种情况是你对Deployment进行扩容,新的副本在完全准备好之前不应该接收流量,但默认情况下kubernetes会在进程启动的那一刻起就开始发送流量。通过使用就绪探针,kubernetes就会等待健康检查通过再发送流量。

  • Liveness

假设你的应用出现了死锁,导致进程无限挂起也就无法处理请求。因为容器的进程依然存活,所以默认情况kubernetes会认为容器工作一切正常并继续发送流量。通过使用存活探针,kubernetes会监测到应用的这种情况,停止发送流量并重启有问题的Pod。

配置容器内获取正确的可用CPU和内存数据

我们在容器里使用top、free命令查看资源的时候,会发现查询到的资源并不是容器里设置的资源,而是整个宿主机的资源大小,系统对于使用资源的查询,基本查询方式有如下几种:

  • procfs,主要是/proc/cpuinfo和/proc/meminfo这两个文件;
  • 通过sysfs获取,主要是/sys/devices/system/cpu/, /sys/devices/system/memory/,/sys/devices/system/node 这几个目录下的由内核导出的数据,这里可以获取真实的硬件信息,真实的硬件拓扑。
  • 通过系统调用sysinfo, sysinfo数据来源内核中的资源探知。
  • 通过posix标准函数sysconf,glibc中的sysconf的实现是通过上面的多个数据源获取的数据,会进行各种检测和fallback。

在使用容器的场景下,默认的这些工具/语言获取到的还是同样的系统的资源信息,只是由于cgroup的限制,这些资源并不能完全使用到。容器场景下需要做cgroup的感知,从cgroup中获取到该容器实际能使用的资源。

在传媒内部,主要语言栈是Java,在Java语言环境下如何正确的获取信息

  • 针对hotspot jvm,低于1.8的版本,使用sysinfo和sysconf获取,可以通过对glibc进行hack实现。
  • 对于jvm 1.8,低于1.8u131以下版本,同低于1.8的版本。
  • java 9以及java 8u使用选项-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap,开启读取cgroup的信息作为可用值。
  • java 10+,使用-XX:+UseContainerSupport 这个参数是默认打开的,会自动探测是不是容器环境,并自动处理。并将之前jvm 9,jvm 8的实现选项做了deprecate。
  • java 11+, 增加了-XX:+ActiveProcessorCount参数,并把UseCGroupMemoryLimitForHeap移除。
  • 对于java 12, 默认不用处理,就是开启UseContainerSupport支持的。

top/free等命令如何获取正确的资源信息,可以通过lxcfs方式,lxcfs是通过对容器内的procfs以及少量sysfs的内容进行覆盖,将其中的值用cgroup中的值进行替代来假冒的。这种方式对于很多业务查询可用资源都是有效的,比如free/top之类的使用方式。但是对于一些业务通过sysinfo/sysconf等方式获取可用资源的方式,无法支持,还会引起混乱。

Request和Limit

资源的Request(请求)和Limit(限制)是kubernetes用来控制CPU和内存资源的,Request是容器被保证能够享有的资源量,如果容器对某一资源有Request,那么kubernetes只会把Pod调度到能够满足Request的节点上,Limit是容器运行过程中资源量的使用上限,kubernetes确保容器不会使用超过Limit的资源,Request不能超过Limit,否则创建资源会报错。

Request和Limit是容器粒度的,因此每个容器都可单独配置,大多数Pod只运行一个容器,如果Pod包含多个容器,那么调度Pod时kubernetes考虑的Request和Limit就是所有内部容器配置的总和。

CPU资源的单位是毫核,也就是说如果你需要2个CPU的资源,那么值就是2000m,如果你只需要0.5个CPU,那么值就是500m。

如果你设置的CPU的Request值大于集群内单个节点能提供的最大值,那么你的Pod将永远无法被调度,对于单核应用(Redis、Nodejs),你应该尽可能的将CPU的Request设置在1或者更小,然后通过扩大副本数来满足性能需求,这样可以使系统更灵活可靠。可以通过设置CPU的Request和Limit实现超售,但对于Java语言来说,启动需要占用2000m,如果实际使用过程中,不需要2000m的话,可以将Limit设置为2000m,保证服务能够正常启动。

CPU可以被视为一种可压缩的资源,因此当你的应用达到Limit,kubernetes会限制你的应用继续占用更多CPU资源,所以你的应用性能可能会下降。kubernetes并不会终止或驱逐这些应用,但是你可以通过健康检查来监测应用的性能是否受到影响。

内存资源的单位为字节,通常情况下,内存是以MiB(可以粗略的视为兆字节MB)来衡量的,同样,如果你设置的内存的Request值大于集群内单个节点能提供的最大值,那么你的Pod将永远无法被调度。与CPU不同,内存无法被压缩,因为你无法人为控制内存的使用,所以如果容器内存的使用量超过Limit,容器会被终止(OOMKilled)。如果你的Pod是由kubernetes提供的一些工作负载自动创建的,比如Deployment,DaemonSet,那么控制器会自动替换新的Pod。