网易传媒经过了一年多的探索及建设,已经将所有核心业务部署在容器环境内了。本文将以笔者在网易传媒整个容器的规划、设计、实施为内容,分享传媒在整个容器建设过程中的经验和教训,希望能给正在容器建设道路上前行的朋友们提供一些指导和帮助,避免走一些弯路。
1
什么是云原生
前两章为云原生的介绍和传媒的基础架构介绍,如果读者对这两块内容不感兴趣,可以直接跳到第三章
云原生的产生
云原生英文为Cloud Native,这个概念是Pivotal公司在2013年的时候首次提出,2015年,Pivotal公司的Matt Stine撰写了一本名为《迁移到云原生应用架构》的手册,对云原生架构的几个主要特征做了阐述:符合十二因素的应用、面向微服务、自助服务的敏捷基础架构、基于API协作和抗脆弱性。2017年,Matt Stine在接受InfoQ采访的时候,对云原生定义做了小幅调整,将云原生架构定义为以下六个特征:模块化(Modularity)、可观测性(Observability)、可部署性(Deployability)、可测试性(Testability)、可处理性(Disposability)以及可替换性(Replaceability)。Pivotal也对云原生概括为4个要点:DevOps、持续交付、微服务和容器化。
CNCF在2015年由Google联合Linux基金会成立,主要工作是统一云计算接口和相关标准,推动云计算和服务的发展。2018年6月11日CNCF明确了云原生定义1.0版本:云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。
云原生本身不能称为是一种架构,它首先是一种基础设施,运行在其上的应用称作云原生应用,只有符合云原生设计哲学的应用架构才叫云原生应用架构。
云原生系统的设计理念如下:
面向分布式设计(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平台及底层硬件的解耦。
通过上图可以看到,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的逻辑架构图和核心组件描述如下:
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攻击,当出现网络攻击的时候,快速通知,快速响应。
可监控:提供丰富的监控手段,让业务快速定位问题。
规范化:提供统一的开发框架及规范,提供基础组件,减少业务方的研究时间,避免重复造轮子,避免大家都踩坑。
为了能应对业务的需求,传媒基础团队整理了整体的基础框架,框架如下图所示:
最底层为资源层:为传媒整个业务提供基础资源。
容器基础层:基于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上万,属于比较大的容器集群,同时,除了容器集群外,对不能进入容器的业务,也要保留虚拟机及物理机资源,并且这些业务提供的服务都要可达,基于以上的业务需求,对集群部署架构做了如下设计
传媒的所有核心业务都部署在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的方式部署到每台节点上。
传媒将计算资源按使用方式划分为不同的资源池,每个资源池都会有一组计算节点供特定的业务方使用,整体资源池规划如下图所示:
在每个计算节点上都打了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地址的跨节点能力。
整体网络架构如下图所示:
传媒从集团申请了10.XXX.XX.XX/XX的网段,在这个大网段下,又划分为若干子网,这些子网包括:管理网段、出外网网段、不出外网网段。设置了出外网和不出外网两个路由表,将不同的网段关联到不同的路由表中。
从上图可以看到在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如何扩缩容、如何删除重建,客户端都不需要关心。
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
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 方案,以下是整体架构图:
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。
热门跟贴