张健伟 分布式实验室 


Kubernetes作为当下最火热的容器编排技术,宜信借鉴开源社区和商业PaaS平台产品,基于Kubernetes + Docker自研了一套容器云管理平台。本次文主要介绍宜信如何结合容器技术实现快速满足业务需求,以及在构建容器云过程中遇到的问题和解决方案,希望通过阅读本文,可以给大家在构建自己的容器云时带来一点思路。
宜信自研Kubernetes管理平台

Kubernetes在宜信的落地实践_Java


项目背景
由于之前大部分服务都运行在虚拟机上,服务器资源利用率不高,并且还有昂贵的License费用。在2016年,容器技术尤其是Docker迅速流行起来,公司内部开始尝试将业务放到容器内运行,虽然通过容器解决了服务发布问题,但很多容器的运维仍然让运维捉襟见肘。宜信是一家金融科技公司,在引入开源组件的时候,稳定可靠是作为考量的最重要标准,在2017年初Kubernetes慢慢成熟,成为容器的管理标准,并且被国内外很多公司采用,在这种背景下,宜信借鉴开源社区和商业PaaS平台产品,基于Kubernetes自研了一套容器管理平台。目前80%的服务都是运行在容器平台,其他业务也都正在向容器云平台迁移。
Kubernetes在宜信的落地实践_Java_02
宜信容器云经历了两个版本:
容器云管理平台1.0
容器云管理平台1.0,使用的Java语言开发,我们使用的Kubernetes 1.6版本,基本上解决了一些运维的痛点问题,实现了CI/CD流程,但是同时也给我们带来进一步的问题。业务申请资源以及资源初始化,均需要运维人员去配置,有大量的重复工作。
容器云管理平台2.0
容器云管理平台2.0,我们基于Kubernetes 1.13版本,使用Go语言重新设计容器云平台,重点解决了在1.0版本中遇到的一些问题,服务上线申请资源,运维的同学只需要在CMDB创建数据,授权给RD或QA,后续操作均有RD或QA人员自行配置,这样极大的解放了运维的生产力,下图是整个容器云平台的功能模块图:Kubernetes在宜信的落地实践_Java_03

服务管理中主要包括,服务创建、监控、灰度发布、升级、配置,具体页面展示如下:Kubernetes在宜信的落地实践_Java_04

  • 监控,监控页面集成Pod的CPU、内存、流量监控等。



  • Kubernetes在宜信的落地实践_Java_05
  • Codeflow,创建代码发布,用户只需要填写Git地址,选择对应的编译环境,填写编译脚本、编译结果,即可完成创建。

Kubernetes在宜信的落地实践_Java_06


  • 负载均衡,负载均衡使用Tengine,用户可以自行修改backend server,添加别名,添加rewrite规则。


Kubernetes在宜信的落地实践_Java_07



  • 存储管理,支持NFS、NAS、CephFS多种后端存储,私有只允许单个应用挂载,公共则支持项目下所有应用挂载。


Kubernetes在宜信的落地实践_Java_08



  • 告警,支持单个容器资源使用告警和服务使用资源告警,以及Pod异常重启告警。告警方式支持短信、邮件、蜜蜂(公司内部使用IM工具)

Kubernetes在宜信的落地实践_Java_09


  • 用户权限,支持四级权限:运维管理员、管理员、普通用户、游客权限。运维同学将项目负责人授权为管理员,管理员可以给普通用户授权。


容器云平台带来的收益
节省资源
相比与直接使用物理机或者虚拟机,使用容器可以带来更高的资源利用率,目前生产环境平均单台服务器运行60个容器。
提高效率和资源利用率
  • 效率提升,在此之前,服务上线需要先申请虚拟机,然后由运维人员配置运行环境和发布系统,使用容器云平台只需要联系运维人员授权,便可以实现自助部署服务。

  • 快速扩缩容,在流量高峰期,容器云平台可以秒级扩容,流量低峰期,可以缩容将资源提供给跑批、离线计算等服务使用。


减轻运维负担
容器云平台上线后,服务资源申请、部署上线、全部有开发或者测试的同学自助操作,在此之前所有的部署上线活动都是运维来做,现在开发人员可以直接在程序完成之后将其制作成镜像,自己就可以进行部署。
自研组件扩展容器云平台Kubernetes在宜信的落地实践_Java_10



iPaaS
容器云平台的核心服务,主要是负责和kube-apisever交互,调用kube-apiserver接口,创建Deployment、Service、ConfigMap、PV/PVC等对象。
虽然Kubernetes本身存在采用高可用的部署架构,避免单点故障,但这远远还不够,一方面是因为单个Kubernetes集群部署在一个机房,如果发生机房级别的故障,将会导致服务中断,另一方面由于单个Kubernetes集群本身故障,如集群的网络配置错误导致整个网络故障等,都将会影响业务的正常使用。
在宜信将Kubernetes部署在多个机房内,机房之间通过专线互连,Kubernetes Federation功能暂时不够成熟,多集群的管理将成为主要难点:
第一是如何分配资源,当用户选择多集群部署后,iPaaS会在每个集群创建同名的Service和Deployment,每个集群Deployment副本数取决于当前集群资源使用情况,并且保证每个集群至少有一个容器。应用自动伸缩时,也会按照相同的逻辑创建和回收容器。
第二是故障迁移,如图中的集群控制器主要为了解决多集群的自动伸缩和集群故障时的容器迁移,控制器定时检测集群的多个节点,如果多次失败后将触发集群容器迁移的操作,保障服务可靠运行。下图是多集群管理架构图:
Kubernetes在宜信的落地实践_Java_11
Nginx-mgr
Nginx-mgr主要是负责接收Nginx配置请求,并更新到etcd,nginx-mgr 通过watch etcd生成Nginx配置并且在管理节点检查Nginx配置正确性,推送到Nginx集群后再次检测配置文件是否合法,最后reload配置。每个服务都配置了健康检查,这样能够保障在后端故障中自动切换。
Kubernetes在宜信的落地实践_Java_12
由于部分业务场景的需要session,在Nginx中配置了session sticky,但是通过nodePort访问的时候,经过的多层负载均衡,session sticky就失效了,为了解决该问题,支持Nginx直连Pod IP,实现机制如下:
  1. nginx-mgr watch k8s apiserver,发现Pod变更,写入etcd集群。

  2. 重新生成Nginx配置,下发至目标Nginx集群,加载生效。


builder
主要是负责CI/CD,后端是基于Jenkins,当用户创建Codeflow后,会在Jenkins中创建对应的Job,并且将用户设置的脚本更新到Jenkins中,当用户启动构建任务时,首先判断代码是否是第一次下载,如果非第一次,只需要增量更新代码(支持SVN/Git),当拉取代码后,Jenkins会启动用户编译脚本,执行编译命令,打包镜像并推送到Harbor,调用iPaaS接口,完成滚动升级。
Cluster-mgr
主要负责多集群的切换、计费,通过watch k8s api,获取容器的生命周期,从而按照小时粒度对容器进行计费。
多集群切换Cluster-mgr会从数据库中获取应用的完整配置,从而在备用集群中创建应用。
Dolphinsync
负责同步公司CMDB的项目名-服务名数据。
Kubernetes在宜信的落地沉思

Kubernetes在宜信的落地实践_Java


镜像管理
镜像管理使用的是VMware开源的Harbor作为私有镜像仓库,Harbor支持权限隔离,Harbor后端使用的是Ceph存储,原生Harbor查询镜像仓库时会关联tag,在镜像非常多的情况下性能较差,我们在查镜像时去除了tag关联。Harbor架构图如下:
Kubernetes在宜信的落地实践_Java_14
  • 部署多个Harbor实例,实现服务的高可用;

  • 挂载CephFS共享存储的方式实现镜像数据高可用;

  • 登陆接入LDAP,方便管理用户;

  • Harbor使用的配置数据和关系数据放在外部(External)数据库集群中,保证数据高可用和实时一致性;

  • 通过外部Redis集群实现UI组件的session共享。


我们根据业务的服务类型,运维同学构建了基础镜像,Tomcat、Jetty、Python、Nginx、NodeJS等,运维同学只需要维护这几个基础镜像即可。当前我们使用的是1.5.2的版本,这个版本使用的是数据库是MySQL,未来打算升级到Harbor的最新版本,对应的后端数据库需要切换到PostgreSQL,而且新版的Harbor支持在线GC。
网络方案
Flannel的介绍和基本原理,官方文档和大量的博客做了非常详细的介绍,此处不再赘述。下面介绍下宜信在使用Flannel的一些经验:
暴露Pod IP给集群外部访问
在服务迁移至容器云平台过程中,服务提供方部署在Kubernetes上,将Pod IP注册到注册中心,服务调用方访问注册中心,拿到提供方的 IP 后,却发现这个IP并不可达,因此严重影响了业务方接入Kubernetes。
解决方案:我们在三层交换机上针对Flannel加了聚合路由,将Pod IP段下一跳指向物理机IP,把Pod和物理机、虚拟机放在同一平面下,实现了Pod IP和物理机、虚拟机互通。
多机房互通
为了实现跨机房的互通,两个集群的Flannel连接到同一个etcd集群,这样保障网络配置的一致性。老版本的Flannel存在很多问题,包括:路由条数过多,ARP表缓存失效等问题。建议修改成网段路由的形式,并且设置ARP规则永久有效,避免因为etcd等故障导致集群网络瘫痪。
修改租约
Flannel的使用还需要注意一些配置优化,默认情况下每天都会申请etcd的租约,如果申请失败会删除etcd网段信息。为了避免网段变化,我们将etcd数据节点的ttl置为0(永不过期),设置ttl的命令如下:

etcdctl --endpoint=https://10.100.139.246:2379 set -ttl 0 /coreos.com/network/subnets/10.254.50.0-24 $(etcdctl --endpoint=http://10.100.139.246:2379 set -ttl 0 /coreos.com/network/subnets/10.254.50.0-24)


日志采集
日志采集有三种方案:

  1. 在宿主机上实现日志采集

  2. 在容器镜像中添加采集Agent

  3. 基于Sidecar日志采集方式


三种方式相比较而言,第一种是开销最小,对应用侵入最小。由于原生的kubelet不支持单个容器自定义日志输出路径,我们二次开发kubelet,通过设置环境变量KUBERNETES_FILELOGS,在容器启动时读取变量,根据变量映射到宿主机/logs/PODNAME下,日志统一落盘在宿主机上,使用公司自研的watchdog日志系统,每台宿主机上通过DaemonSet方式部署日志采集Agent,Agent通过Docker API获取需要采集的容器和日志路径,采集日志并发送到日志中心,日志中心基于Elasticsearch开发,提供多维度日志检索和导出。
CI/CD
流程如下图所示:
Kubernetes在宜信的落地实践_Java_15
实现如下:
  1. 用户创建Codeflow后,用户选择对应的镜像版本,以及代码编译需要的Maven命令,默认会生成Dockerfile。

  2. 用户可以自定义Dockerfile,Dockerfile优先级:代码根目录中Dockerfile > 上传至平台中的Dockerfile > 创建Codeflow生成的Dockerfile。

  3. 基于安全角度考虑,使用非ROOT用户构建。


灰度发布
灰度发布是在Kubernetes中新建一个Service(NodePort),将更新的Pod打上灰度标签,Service(NodePort)根据Label筛选出来灰度的Pod,最后通过Nginx配置导入新创建的灰度服务,从而实现灰度发布。
基于Label的方式筛选:
Kubernetes在宜信的落地实践_Java_16
基于Header、URL、源IP分发流量:
Kubernetes在宜信的落地实践_Java_17
示例:如果希望请求头中通过foo=bar的客户端路由到新版本,我们修改Nginx的配置文件如下:

  set $upserver test.com;  set $hdupserver test.com-grey;  if ($http_foo = "bar") {set $upserver $hdupserver;  }


监控
资源监控
容器本身资源监控的性能监控通过cAdvisor + Prometheus的方式。
APM
容器内业务的监控集成开源的APM监控系统uav,完成应用的性能监控。uav的链路跟踪基于JavaAgent技术,如果用户部署应用勾选了使用uav监控,通过Init Container在启动时,将Agent拷贝至容器内,并修改启动参数。
监控实践

  • 基于单个容器的告警。由于业务部署为多副本,每次出现问题并不是所有副本都出现不可用的情况,可能某些问题(特定请求触发的bug)只会在单个容器中出现,所以针对单个容器增加告警;

  • Prometheus数据去除Sandbox监控采集,节省Prometheus空间;

  • 扩展blackbox_exporter增加了站点监控和机房之间的ping监控。


站点监控示例:Kubernetes在宜信的落地实践_Java_18

ping监控示例:Kubernetes在宜信的落地实践_Java_19

监控详细部分可以参考之前陈晓宇的分享:《Prometheus架构与实践分享》。
debug容器
为了提供给开发人员排查问题,我们基于kubectl-debug插件启动了一个包含众多排查工具的容器(strace、tcpdump、traceroute、iperf、mtr、redis-cli、jstack、net-tools等工具),加入到业务容器的namespaces中,启动的时候挂载到业务容器中,效果类似于:

 docker run -it --network=container:<container_ID> --pid=container:<container_ID> --ipc=container :<container_ID> -v /log/container _ID:/debugviewlogs <image>


kubectl-debug容器启动效果如下:
Kubernetes在宜信的落地实践_Java_20
实现原理:
Kubernetes在宜信的落地实践_Java_21
将debug-agent在全部Kubernetes集群的所有节点中以DaemonSet的形式部署,并且挂载了宿主机的/var/docker/docker.sock,实现与Docker Daemon通信。如上图的步骤:

  1. Web端提供Pod的cluster、namespace、podname,向后端服务Backend server发起websocket请求;


  2. 后端服务Backend Server接收到请求后,向Api-server验证该Pod是否存在,并返回Pod所在的宿主机Node和Pod的容器信息,根据状态判断是否可以debug;


    注意:如果Pod的状态reason是CrashLoopBackOff,那么Backend Server将会请求Api-server,让kubelet复制一个改写了启动命令(sleep)、去掉label及健康检查的Pod,那么后续是对复制后的Pod进行debug。


  3. Backend Server传递debug的Pod信息,发起debug请求,同样是升级的SPDY请求,映射了WS的标准流,实现交互。debug-agent收到请求后,开始拉取debug工具镜像,进而创建一个debug容器,并将debug容器的各个namespace设置为目标业务容器APP的namespace。创建完debug容器后,debug-agent将Backend Server的SPDY请求中继到debug容器。debug容器将SPDY的标准流attach到业务容器中。如此,web端便可与debug容器实现交互。debug操作结束后,debug容器便由debug-agent清理回收。


遇到的问题
Docker使用的是devicemapper存储,devicemapper限制单个容器大小为10G,避免因为单个Pod的写数据,导致整个Node节点不可用。
etcd需要定时备份,避免由于etcd故障导致集群不可用的问题。
业务在测试过程中,由于某些场景的,需要调整容器内的时间。libfaketime主要是拦截了程序调用获取当前时间的系统调用。然后会将你修改(假的)后的时间返回给这些程序。这样的话你可以单独修改一个程序的获取的时间,而不用修改整个系统的时间。Dockerfile如下:

RUN git clone http://gitlab.creditease.corp/paas/faketime.gitWORKDIR /faketime/srcRUN make installCMD ["/bin/sh", "-c", "LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1 FAKETIME_NO_CACHE=1 FAKETIME_DONT_FAKE_MONOTONIC=1 /app/tomcat/bin/catalina.sh start 2>&1"]


服务平滑升级过程中存在如下问题:
流量分发到Terminating的Pod
在使用Spring Cloud框架时,服务更新过程中,某个老的容器被终止时,由于Eureka Server是通过心跳检测,并不能及时发现服务容器下线,部分请求仍然被分发到终止的容器,虽然有重试机制,但是高并发情况下也经常会遇到接口调用超时/失败的情况,为了减少这些错误,在Pod销毁前需要在Eureka中摘除节点,这需要开发提供接口并将调用接口的脚本添加到PreStop中,具体如下:
Kubernetes在宜信的落地实践_Java_22
流量打到未启动容器
健康检查、运行实例数保证,应用可以在无人工干预下自动修复,确保线上服务不中断。

Kubernetes在宜信的落地实践_Java_23

  • 设置JVM获取的GC线程数-XX:ParallelGCThreads=4;

  • Java 7,Java 8,根据容器的Limit值*80%设置Xmx;

  • 在Java 8u131+及Java 9,需要加上-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap才能使得Xmx感知Docker的memory limit。


容器云未来展望

Kubernetes在宜信的落地实践_Java_24


  1. 网络策略隔离

  2. 基于流量的扩缩容

  3. 服务画像,搜集应用CPU/IO使用、服务高峰期,为应用调度部署做支撑

  4. 引入服务网格,增加对服务治理