PPmoney基于Kubernetes的DevOps实践

大家好,我叫冯旭松,现是PPmoney架构部平台组高级运维开发工程师,主要工作是研发云平台的建设以及推动线上业务容器化。今天给大家分享的内容是我司基于Kuberntes上的DevOps实践。

云平台介绍

先给大家看看我们研发云1.x时候的一个包含部分微服务的关系图。

这边使用的主流语言是Java,Spring全家桶做后端服务,Python做数据相关开发,Node主要做前端应用。在此前还有dotnet的soa服务。如果采用传统虚拟机部署一整套开发环境的话,因为环境不一致,配置不规范等因素,可能需要运维同学花费一整周的时间来部署并调通环境。所以快速部署的需求是非常迫切的,于是我们开始打造内部的基于K8s的容器研发云,代号“基拉祈”,又名“许愿星”,来源于Pokemon(我们架构部的很多基础框架应用都取名来自于此),希望能够通过Jirachi平台提供各种类型的问题的解决方案。

大致架构图

K8s集群部署

基于kubeadm部署工具,现在新版本功能越来越完善,而且可以通过[alpha ]phase将初始化各阶段操作解耦。对于集群自动化操作,配合ansible playbook食用更佳,推荐参考 kubespray ,如果是想对K8s各组件有更深了解的话可以参考 kubernetes-the-hard-way

刚入坑的同学可能遇到的一些比较常见的问题:

  • 关于墙的问题,可以是先在能科学上网的机器上Pull再Push到内网的镜像仓库(我们使用的harbor,主要后续IDC、云以及内网之间可以通过 镜像复制功能 进行镜像同步),或者是使用 registry.cn-hangzhou.aliyuncs.com/google_containers/gcr.azk8s.cn/google_containers/ 这些公有的镜像仓库, 然后通过指定 --image-repository 参数或者是config文件中的 imageRepository 字段来初始化/升级集群。
  • 集群的证书期限问题,可以通过renew subcommand实现证书续期,当然改动源码将默认一年期改成更长的时间也是个方法,但是其实K8s每个版本都有一个支持期,在升级版本的时候kubeadm工具会检查证书期限并执行renew操作。

说说我们的网络方案选型,因为在这边没有专业的网络研发的同学,所以只能用社区的开源方案。入门的话推荐选择配置相对较少的flannel,熟悉了之后可以看看calico/kube-ovn这样子的方案。

我们想使用calico的主要原因是需要通过BGP协议来实现打通原有网络以及容器内部网络,场景是集群内外的服务发现调用。但是在测试的时候遇到一个难题,集群外部client无法调用集群内部的TCP类型服务,具体 issue ,如对这方面有经验的同学望指点一二。

集群外部网络访问流量走向是 client -> LVS -> nginx-ingress-controller -> endpoints

LVS这里我们实现一个简单的operator去listWatch带有标识的endpoints变化然后动态CRUD virtual_server,后面nginx-ingress-controller以共享主机网络方式运行,同时这里会添加一个sidecar去做LVS DR模式中real_server在start/stop时做的一些例如assign ip/del ip/route add/route del操作。

Jenkins pipeline

在云平台导入/创建项目,按提示填写表单之后,会在项目下生成Dockerfile/Jenkinsfile,对应的Jenkins pipeline job,以及创建gitlab webhook

项目类型当前是手动选择,之后会考虑使用 github-linguist / buildpack 类似的开源工具来实现autodetect。

可以看出来,git hook的event是先发到云平台做过滤后再触发Jenkins的pipelinejob。这里主要核心是Jenkinsfile,其中引入share-library,封装了一些基础的groovy方法,import之后可直接使用。

缺点:不够云原生,master/slave的运行还是依赖于外部的虚拟机实例,使用jenkins的kubernetes插件在启动slave调度Job时耗时太长,封装方法内执行的操作对于使用者不够友好,改造成本较大。目前有在对比gitlab-ci/drone/tektonci等。

helm应用管理

helm是一个应用包管理工具。没有helm之前我们是怎么做的?

通过类似上图这些模板文件,使用scm管理,云平台会通过hook或定期同步这些文件。每添加一种应用支持,就需要“手写”这些yaml文件,然后经过测试才能“上线”使用。相关K8s对象的逻辑也是使用硬编码。于是我们调研了helm,如果我们需要支持一个新的中间件应用类型,例如rabbitmq/mssql等,我们可以直接引用官方/社区维护完善的chart即可。

至于我们的服务应用,我们只需要写一个通用的basechart即可,然后所有的应用都引入这个做为dependency。然后发现这种做法存在的问题是,如果basechart更新了,那么每个应用都需要更新生成一个新的chart(如果有需要的话)。于是又抽象出一个应用商店的概念。例如Java应用会使用springboot的chart,然后在这个上面更新一些相关的默认值存在云平台的数据库(存放在configmap中也是一个选项),每次部署的时候只需要将用户指定或者CI参数merge到这个默认值values,最后部署/更新即可。

同时我们在chart中添加了 类似 JSONschema(其实并不是)这样子的东西来自动生成部署应用的表单。毕竟用户大多数时候是使用云平台的webUI来管理应用。helm3也会使用JSONschema来做values的校验,同样webUI前端可以通过这个文件来做form-rendering。不同应用暴露在schema文件中的可配置选项是不同的。

部署页面

服务列表(相对比较简陋)

环境复制需求,我们同样通过helm实现将例如整个dev分支上的服务复制到staging分支,有状态服务通过Job的形式去将数据导出导入,后面可以集成 Velero 工具来实现。

使用helm2感受到的缺点:整个集群当作一个大的租户,用户权限都由云平台(架构图里边的almonds应用)来管理的,所以整个集群中只有一个tiller服务端(绑定了cluster-admin角色)来管理所有的release。需要保证releaseName的唯一性,且当release数量增多超过300个左右的时候(当前已部署1700 个release,因为服务依赖没有使用helm chart来做,所以导致release超多),每次list操作都会比较耗时,看过tiller源码的同学应该知道,每次list都会在具体driver实现中获取所有的K8s内置对象例如cm/secret,之后再decode所有的release,最后再把结果返回,而通常的情况下我们会使用一些filter,例如通过release的namespace,chartName,name中的subpattern来做过滤,那每次query都要经过这么久的list操作对用户体验是非常不友好的。所以我们catalog又添加了inmemorycache的实现来加速list查询。

有状态应用部署

上面说到我们会支持mysql/pg/redis这类有状态应用服务,那么就存在一个问题,数据存储在哪里?这里我们使用了ceph rbd。因为是内部的测试环境,对读写的性能要求并不会太高,所以我们从开始就确定了使用开源的分布式网络存储。

rbd集群部署一开始是使用官方的 ceph-deploy 工具,后面由于集群故障(整个ceph的所有mon节点都完全起不来),切换到了新的使用rook部署的集群,秉着专业的事情让专业的人做,operator这种模式让管理复杂集群服务工作变得更简单。

说到operator,例如etcd/es/redis这类有状态服务,仅仅使用官方的chart,部署成sts类型应用是远远不够的,于是我们使用 awesome-operators 里边的部分operator来简化管理我们的有状态服务。例如创建一个es kibana集群服务(当然首先需要部署operator)

apiVersion: elasticsearch.k8s.elastic.co/v1alpha1

kind: Elasticsearch

metadata:

name: quickstart

spec:

http:

service:

  metadata: {}

  spec: {}

tls: {}

image: elasticsearch/elasticsearch:7.1.0

nodes:

- config:

  node.data: true

  node.ingest: true

  node.master: true

nodeCount: 3

updateStrategy: {}

version: 7.1.0

apiVersion: kibana.k8s.elastic.co/v1alpha1

kind: Kibana

metadata:

name: quickstart

spec:

elasticsearch:

auth:

secret:

key: kibana-user

name: quickstart-kibana-user

caCertSecret: quickstart-es-ca

url: https://quickstart-es.default.svc.cluster.local:9200

elasticsearchRef:

name: quickstart

http:

service:

metadata: {}

spec: {}

tls: {}

image: kibana/kibana:7.1.0

nodeCount: 1

version: 7.1.0

日志收集

云平台本身提供简单的查看日志的功能。我们发生过一起事件,因查看日志功能导致云平台频繁OOM。CRI或者是kubelet(早期版本)默认没有对容器的日志文件大小做限制,logs接口会将整个容器日志文件读进内存。所以配置集群时建议对容器的日志文件大小做合理的规划限制。

日志收集是使用比较典型的efk方案,其中 f 现在是filebeat。filebeat以DaemonSet类型应用部署运行在所有节点上,将 /var/log 目录以hostPath volume形式挂载到filebeat Pod的文件系统中。

容器日志收集比较麻烦的是多行日志的处理,例如Java的exception,golang的panic stack,可以使用multiline来处理,一个input里只能定义一个multiline的配置,所有的容器日志都属于同一个input,但是并不是所有的应用都是使用同一个pattern来输出日志的。以及通常情况下我们会使用 add_kubernetes_metadata 这个 processor 将关于Pod的元数据也一并放在日志信息中,例如lables中存在 app:demo ,那么filebeat发出来的一条日志事件中就会包含 ...lables: {app: demo}... ,此时如果有另外一个Pod包含了 app.kubernetes.io:demo 这个标签,那么输出的日志事件会是 ...labels: {app: {kubernetes: {io: demo}}}... ,这样子的话,第一个app是string类型,另外一个是map[string]string类型,在索引到es时就会报401错误,导致日志条目丢失。

解决这个问题,filebeat的autodiscover方案是个不错的选择,可以基于hints做autodiscover,可以给不同的Pod类型添加 multiline.* 在labels中,filebeat会自动对Pod的日志输出做处理。同时kubernetes provider可以使用 labels.dedot / annotations.dedot 配置解决上述的 app 字段类型不同引起的问题。 但是 ,我们在测试过程中发现即使当前最新的版本启用该配置的话也会引起memory leak,报错 issue

多行日志的问题我们更建议在应用层面解决,于是我们又提出要求,对接云平台的应用需要接我们实现的多行日志方案(其实就是封装log4j实现JSON日志输出)。

从filebeat抓取日志后会输出到例如kafka的消息队列中,之后再由logstash indexer来处理并入到es,最终kibana做用户界面查询展示。

监控

监控方案使用了 prometheus-operator 全家桶。这个推荐直接使用 helm chart 安装部署。(这里涉及的组件有点多就先略过了…)

可能遇到的问题会是某个Job中target个数是 0 (通过sd),这里排查需要了解的知识点是servicemonitor如何对应到endpoints,以及如何使用relabeling。

其它的一些小功能

  • 在命名空间之上支持环境空间概念,将多个namespace包含在所属的业务团队空间中
  • 支持简单的混沌测试功能,即通过开关随机删除应用实例来测试自愈功能以及上下游服务的熔断机制

未来工作

  • 云平台使用rbac来支持权限控制
  • 优化CI/CD流程,引入云原生解决方案
  • 线上业务容器化,提升DEVOPS能力(直接关系到所有人的幸福感)
  • 引入服务网格,将服务治理能力降到基础设施层面,让开发更专注于业务
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章