万字干货 | K8S 技术落地实践上海站分享

Kubernetes 已经成为事实上的编排平台的领导者,下一代分布式架构的王者,其在自动化部署、扩展性、以及管理容器化的应用中已经体现出独特的优势,在企业中应用落地已经成为一种共识。

本次「K8S 技术落地实践」上海站,青云QingCloud 软件工程师朱晓扬、王欣、宋雪涛就以下主题进行了分享:

  • Operator:以 K8S 原生的方式去管理 K8S 原生应用

  • 企业级核心存储平台为 Kubernetes 赋能

  • NetDevOps in Kubernetes

1Operator:以 K8S 原生的方式去管理 K8S 原生应用

kubernetes介绍及不足

K8S 是谷歌团队发起并维护的开源容器编排系统,其代码被托管在 GitHub 上,有超过 2.9 万名关注者和 1400 个贡献者,超过 53000 个 Star 和 18000 Fork,每周上百个 PR 被提交和合并,可见 K8S 的受欢迎程度之高。

在 K8S 上管理无状态应用非常方便,但对于有状态应用,K8S 的能力是比较欠缺的。像数据库、缓存、监控系统等如何进行升级、伸缩。当数据丢失或者不可用时如何进行重新配置等等,K8S 比较缺乏这些能力。

传统的方式可能会选择使用运维脚本帮助我们实现自动化运维,但传统运维脚本有先天不足,比如运维脚本本身也会出现异常,而且不能借助 K8S 的能力自动恢复,还有运维脚本的重用成本大、配置文件的管理等等问题。传统运维脚本游离在 K8S 之外,并不能很好的跟 K8S 进行融合。

Operator 的出现

为了解决以上问题,Operator 便出现了。Operator 旨在简化复杂有状态应用管理,它是一个感知应用状态的控制器,通过扩展 Kubernetes API 来自动创建、管理和配置应用实例。

2016 年,CoreOS 最早推出 Operator。2019 年,推出了集中式存储库 OperatorHub,上面存储的容量很多,大家可以上传和下载。

Operator 作为 K8S 第三方组件,它主要以 CRD 为基础,根据 CRD 资源的变化情况,执行一系列的操作。这一系列的操作以代码的形式固定下来,通过代码帮助我们实现自动化运维。最后实现 K8S 对复杂的有状态应用进行简单的配置和管理。

K8S API 可以说是 K8S 的核心,它主要有以下特点,一是声明式,用户只需要描述他希望最后达成的状态,不需要关心 K8S 如何达成这个状态;二是基于级别的,工作队列背后由一个 queue 和 set ,一个 key 的多次添加会被合并成一次操作;三是异步,所有的 API 都是异步的。

这是内置资源,自定义资源(CRD),Kube-controller-manager 和 Operator 之间的逻辑关系。

Operator 到底如何协调工作?首先,这是一个 etcd Operator,它首先会 watch API server,当有更新操作时,然后会分析当前状态是否与预期的状态相符,如果不符的话,会执行对应的操作来保证达到预期状态。

当我们实现 Operator 之后,它到底可以达到什么样的效果?假设一个集群现在出现了故障,Operator 会自动的进行灾难恢复,不需要人工介入。备份和伸缩也是一样,用户只需要做简单的配置,剩下的工作都可以交给 Operator 来完成。这就极大的简化了用户自行运维的成本。

实现定时任务Operator

接下来我们讲一下如何实现定时任务 Operator。我们的目的是实现一个自定义的定时任务,通过创建定时任务的 crd 实例,可定时输出“hello world”。我们主要用到的工具 Kubebuilder,项目地址是:https://github.com/kubernetes-sigs/kubebuilder。

首先我们初始化项目结构:

kubebuilder init --domain tutorial.kubebuilder.io

接下来我们需要初始化 API:

kubebuilder create api --group batch --version v1 --kind CronJob

现在的项目结构会是这样:

|──go.mod 依赖管理|──config 部署的yaml文件,manifests|──Dockerfile |──Makefile|──PROJECT|──api 定义API资源对象 |──v1 |—cronjob_types.go|──controller operator具体的逻辑|── |cronjob_controller.go|──main.go 入口函数

接下来我们在 api/v1/cronjob_types.go 文件中定义 CRD 资源结构体

然后执行 make install,在 config 目录下生成 crd 的 yaml,并部署 manifests

之后定义对资源的权限管理,在 controllere/cronjob_controller.go 中的 Reconcile 函数中添加业务逻辑代码。

Operator 趋势及总结

Operator 主要是为了解决维护服务必须有足够的专业知识与学习成本过高之间的矛盾。它的优势是对资源进行了封装,赋予 K8S 新的能力。方便用户管理复杂的应用,而不需要了解具体的执行步骤。缺点是运维人员需要去学习并开发 Operator,上手难度高,对于应用的各种各样的问题难以考虑全面,但这也是所有自动化运维都会面临的问题,只有不断的去完善更新 Operator, 才能够不断满足项目发展的需求。

2企业级核心存储平台为 Kubernetes 赋能

我们先说一下 K8S 的存储插件,然后介绍如何开发存储插件,将企业的核心存储系统与 Kubernetes 对接,为 Kubernetes 赋能。最后介绍我们做的插件质量如何管理。

背景

当前,容器技术是发展十分迅速的技术,大家都会使用容器技术封装自己的应用。容器技术提供了运行时环境,让大家以一个镜像的方式打包自己的应用,实现依赖封装、DevOps,也可以提高资源利用效率。

现在大家应用都是微服务架构的应用,这样需要很多容器进行协作,构建成一个关键的系统,所以就需要一种容器编排技术能够快速创建、部署微服务应用。

在容器编排系统领域中,Kubernetes 毫无疑问现在的领导者。大家在初次使用 Kubernetes 时,可以上手做例子,搭建 K8S 时会提供标准的容器技术,如 Docker。也会提供一个标准的网络插件,供大家搭建网络。

但是存储,大家未必会接触到。当用户逐渐深入使用 Kubernetes 容器编排技术时,将容器编排技术落地时,不可避免地需要一些持久化的存储来支撑用户的微服务应用,这需要 Kubernetes 与后端的持久化存储相对接。

K8S 如何与后端存储服务相对接?Kubernetes 是通过存储插件对接后端的存储系统。

在 Kubernetes 里,存储插件有 In-tree 和 Out-of-tree 两种。对于 In-tree 存储插件,它是随着 K8S 核心组件部署而部署,它运行在 K8S 核心组件之内。如Kubernetes 可以调用 In-tree 的 AWS 存储插件来对接后端的 AWS 存储服务。另一种是 Out-of-tree 存储插件,顾名思义是存储插件的运行与 Kubernetes 核心组件相独立。

接下来我们比较一下两种存储插件。In-tree 存储插件在功能性上比较精简,主要是做存储卷的创建、删除、挂载或者卸载的操作。Out-of-tree 存储插件在功能性上比较丰富一些,Out-of-tree 存储插件除了支持存储卷的创建、删除、挂载或卸载的功能外,还支持快照的创建或者删除。

对支持的存储类型上,In-tree 存储插件支持的后端存储系统比较有限,这样会限制住用户的选择。Out-of-tree 存储插件比较多样,可以由存储系统的提供商来开发相应的存储插件。

最后一个方面是 In-tree 存储插件的代码和 K8S 核心组件的代码一起发布,插件代码在 K8S 代码仓库内,不易维护。另外它运行在 K8S 核心组件之上,如果 In-tree 存储插件出现问题,也会导致 K8S 核心组件出现问题。

Out-of-tree 存储插件在易维护性上好于 In-tree 存储插件。Out-of-tree 存储插件运行不会影响 Kubernetes 核心组件,他们互相独立。Out-of-tree 存储插件的代码仓库与 Kubernetes 核心组件相独立,它可以更好地独立维护、发布、部署。我们现在通常选择开发 Out-of-tree 存储插件让用户使用。使用 Out-of-tree 存储插件是一种趋势。

在 Out-of-tree 存储插件中也分为两类:一类是 Flex-Volume,这一类存储插件是 Kubernetes 特有的,从 1.2 开始支持。Flex-Volume 的存储插件部署比较复杂,功能比较有限。

另外一类就是现在用比较新的存储插件方式 CSI,CSI 的全称是 Container Storage Interface,这是容器平台领域的工业标准。Kubernetes 是从 1.9 版本开始支持 CSI 的存储插件。CSI 存储插件部署简便,可以支持容器化部署。另一方面,CSI 的功能比较强大,除了支持存储卷的创建、删除功能,还有一些高级功能,比如快照管理功能。

青云开发了相关的存储插件,称之为 QingCloud CSI 存储插件,它对接青云云平台的块存储,也就是青云云平台的硬盘。另一种插件称之为 NeonSAN CSI,可以将 K8S 与后台的企业级分布式块存储系统 NeonSAN 相对接。

开发存储插件

接下来,我们以开发 NeonSAN CSI 存储插件为例,跟大家说明如何开发 CSI 的存储插件。左侧是 Kubernetes 集群;最右侧是存储系统,这里画的是 NeonSAN 的存储系统;中间可以通过 CSI 插件对接。CSI 插件需要实现 CSI 规范规定的一系列接口。Kubernetes 与 CSI 插件相对接是由 Kubernetes 原生支持的,Kubernetes 团队开发了若干个容器,我们 CSI 插件支撑,这样就实现了让 Kubernetes 使用我们的存储服务的功能。

刚刚谈到开发一个 CSI 存储插件要实现一些接口,它的接口分为 Identify 类、Controller 类和 Node 类。

对于 Identify 类接口主要描述的是插件的基本信息和检测插件状态的接口,比如 GetPluginInfo,这个接口能得到插件的版本信息。Probe 接口,检测插件是否正常运行。GetPluginCapabilities 接口可以得到插件所实现的功能。

第二类接口称之为 Controller 类接口,主要负责向存储服务端发送指令,对存储卷进行管理。插件接口可以看到 CreateVolume,DeleteVolume 等接口,它是负责创建或者删除存储卷的。还有一些接口是 ControllerPublishVolume,ControllerUnpublishVolume 这些是在我们主机上挂载或卸载存储卷。Controller 类接口负责直接向存储服务端发送请求来操作底层的存储资源。

最后一类接口称之为 Node 接口,Node 类接口主要负责对主机上的存储卷进行管理。Node 类接口,包括比如 NodeGetInfo。这些接口负责存储卷的操作,还有其他的接口得到主机上的信息。

CSI 插件部署时会划分为 Controller 和 Node 这两类服务。现在我们以实现 CreateVolume 这个接口为例,让大家看看我们具体是如何实现一个 CSI 接口的。

实现 CSI 接口,首先要认真理解、阅读 CSI 的接口规范。第一是实现接口负责的业务逻辑,像 CreateVolume 这个接口,从字面意义来看是创建存储卷的接口。但是创建存储卷有不同类型的划分,比如你可能创建一个全新的存储卷,也有可能从基于快照来创建一个存储卷。这些业务逻辑都需要在接口中实现。首先,我们要实现相关接口,负责业务逻辑,这是最核心的。

光去实现业务核心在这个接口还不够,我们开发这样一个接口还要符合 CSI 规范的要求。比如 Create Volume 这个接口输入字段有必填项,我们需要检测其输入参数是否填写必填项。输出也会有一些要求,对于异常情况下,CSI 有不同的返回码,要求插件开发者返回出来。支持 CSI 接口的技术平台,比如 Kubernetes 对特定的返回码作出相应的反应,有时候要重试调用接口,有时候不会重试调用或者需要作出其他的反应,这要以 CSI 的规范来定。

最后一点,我们实现接口时要符合 CSI 接口和规定的其他要求。比如我们实现 CreateVolume 接口时,我们需要注意幂等的要求。

幂等的含义是以相同的参数调用接口一次或者多次结果是一样的。比如 CreateVolume 接口,如果我们没有实现幂等性,容器平台第一次要用 CreateVolume 接口发送创建一个存储卷的请求给后端的服务系统,我们称之为存储卷 A。假如此时创建了存储卷 A,返回给容器平台时发生了意外情况,导致存储卷 A 的信息并没有及时返回容器平台,这时候容器平台会尝试以相同的参数重新调用 CSI 插件的 CreateVolume,假如 CSI 插件没有实现幂等性的话,存储系统会新建一个存储卷 B,并把 B 的信息返回给容器平台。这时候存储卷 A 就泄漏了,而存储卷泄露的情况不被允许,它可能会造成资源的浪费或者其他问题。

那我们是如何实现幂等性的呢?

在创建存储卷之前,我们以这个参数在系统中查询一遍是否有相应存储卷的存在。如果有相应存储卷的存在,直接返回存储卷的信息给容器平台。以刚才的例子为例,我们在第二次容器平台调用 CreateVolume 接口时,这个 CreateVolume 接口会在系统中查询一遍,是否有相应的存储卷存在。查询到有存储卷 A 的存在后,就会直接给容器平台返回存储卷 A 的信息,这样就不会有存储卷泄露的情况发生。

刚才介绍了如何实现 CSI 插件,接下来再看 CSI 插件如何与 Kubernetes 协作,让 K8S 通过 CSI 插件对接到后端的存储系统。

我们需要 Kubernetes CSI 开发的 Sidecar 容器与业务单元共同组成 Pod,提供一个完整的服务。K8S CSI Sidecar 容器前面介绍了,这是 K8S 团队开发的 Sidecar 容器,帮助 CSI 插件开发并与 K8S 平台相结合。

我们常见的 Sidecar 容器有 Driver Registrar,它可以将 CSI 插件注册到某个 K8S 节点中。External-Provisioner 容器负责监听 K8S 集群内存储卷的创建、删除的状态,相应的调用 CSI 插件的接口,完成存储卷创建、删除的工作。External-Attacher 容器,这个容器会监听 K8S 集群关于存储卷挂载相关资源对象的创建、删除等变化的状态,调用 CSI 插件的接口,完成存储卷挂载到某个节点上,或者从某个节点上卸载的操作。

还有一种是 External-Snapshotter 容器,它是负责监听 K8S 集群内关于快照的资源对象和创建删除的操作。听到资源快照相关资源对象的创建或删除后,它会相应地调用 CSI 插件的接口,实现存储服务端的快照创建和删除。

接下来介绍 NeonSAN CSI 如何部署到 K8S 集群中。这是一张部署架构图,从上往下看有一个 Master 和三个 Node 的 K8S 集群。K8S 核心组件是浅灰色表明,划上了我们关注的 K8S 的组件。比如在 Master 上,我们划出 API Server 这样的 K8S 核心组件,在 Node 上我们划出的像 Kubelet 这样的 K8S 核心组件。绿色标志的是我们存储厂商开发的 CSI 存储插件。

右侧是 Controller 容器组合,Controller 的容器是通过 K8S StatefulSet 创建。Controller 包含四个容器,分别是 External Provisioner,External Attacher,External Snapshotter 和 CSI 存储插件,他们会监听 K8S 资源对象并调用 CSI 插件的相关接口,完成对我们后端存储系统的操作。

另一类是 Node 类服务,要求在 K8S 每个节点中都要创建。从 Node 类服务中,我们集成了 K8S 开发的 Driver Registrar 容器,这个容器负责将存储插件注入到 K8S 节点中。

质量管理

前面介绍的 CSI 插件的开发和部署,接下来我们介绍 CSI 插件如何做相应的质量管理。我们开发 CSI 存储插件时,引入了持续集成的理念。我们开发完代码后,首先会进行单元测试,通过单元测试后,我们会通过预先编辑好的代码,一键式构建我们 CSI 的容器镜像。构建完镜像后会使用我们 K8S 资源定义文件持续部署 K8S CSI 插件到 Kubernetes 集群里。

部署完 CSI 插件后,我们会运行一个称之为 Kubernetes CSI Test 的测试。在该测试成功后,我们会运行集成测试,模拟用户使用的场景来创建资源对象,测试 CSI 插件能否正常运作。通过集成测试后,我们才会提交代码到主线分支。通过持续集成的流水线,我们开发时可以及早发现并修复问题,这样大家最后发布时会非常轻松。

接下来看看前面谈到的 Kubernetes CSI Test 是什么项目。Kubernetes CSI Test 是 Kubernetes CSI 开发的开源项目,其目的是测试 CSI 插件是否符合 CSI 官方规范。NeonSAN CSI 插件也是在 K8S 环境中通过相应的 CSI Test。

后续我们会开发监控的功能,企业级用户使用初始化工作负载时,存储卷的已用量、可用量等都需要知道,当存储卷容量不足时,我们需要及早对存储卷进行扩容,存储卷扩容功能将会在之后 CSI 版本中提供。

在 K8S 集群中,每个节点访问存储系统权限可能会不尽相同,我们也会开发相关的拓扑感知功能,帮助 K8S 集群调度 Pod 到正确的节点上,保证 Pod 正常的创建和运行。

我们开发的插件在 GitHub 上可搜索到,欢迎大家使用并提出建议。

3NetDevOps in Kubernetes

背景

首先聊一下我们为什么需要 NetDevOPS。NetDevOPS 之所以发展起来,是因为 SDN 的快速发展和 K8S 的兴起要求我们有更高效的方法去实施网络运维,然而传统的 telnet 和 ssh 运维的方式满足不了我们的需要。

NetDevOPS 不是工具的使用,也不是编程框架的堆砌,它是一种文化,主要包含以下几个方面。第一是重视测试,包括单元测试,集成测试,E2E 测试等,这些测试都需要能够自动化的进行,用测试用例来保证质量,而不是重复的手动测试。

第二是重视版本控制,敬畏版本,而不是隔三差五交出一个正式版本并废弃前几天刚交付给用户的版本。

第三是部署微服务架构,组件之间用 API 沟通。网络工程师通过 API 和网络设备沟通而不是一次又一次敲入大量的命令行。

第四是用自动化解决重复单调的工作,用机器代替人工,避免一些人为的误操作。

CloudNative 是当今比较流行的话题,相比于传统的网络开发,云原生开发更为方便,更为强大,同时迭代也更快。云原生中需要处理更多的设备,云原生中也涌现了更多的新技术,网络工程师需要提高效率的同时,写一个健壮的网络应用,这就更需要NetDevOPS。

我作为一个网络工程师,我对自动化的粗浅理解主要是以下三点,第一是用 API 和网络设备对话,第二是用云平台管理网络设备,第三是拥抱容器,拥抱 K8S。

经验分享

接下来分享一下我在实施 NetDevOPS 时的一些经验。

作为网络工程师要主动的去拥抱容器,其实 docker 相关的操作都非常简单,我们常用的有以下操作。感受一个应用程序的快速启动可以用 docker run, 制作一个自己需要的镜像可以用 docker build,将镜像推送到远端仓库可以用 docker push。此外我们还需要了解一下 Linux Net Namespaces,每个容器中都有一个独立的网络空间,有自己的 eth0 。

分享一些容器的妙用:

比如想跑测试但是发现自己的笔记本不会 Linux 的时候,可以用以下快速启动一个容器来运行测试。

docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp -e GOOS=linux -e GOARCH=AMD64 golang:1.11 go test -v -cover

需要一个支持 BGP 的路由器时,可以用:

docker run -d -v `pwd` /gobgp:/etc/gobgp:rw -p 179:179 pierky/gobgp

想要模拟在一个小型网络中的三台虚机,可以用:

docker network create test-networkdocker run -itd --name=test1 --net=test-network busybox /bin/shdocker run -itd --name=test2 --net=test-network busybox /bin/shdocker run -itd --name=test3 --net=test-network busybox /bin/sh

分享一些学习资料:

除此以外,我们还需要掌握一些基础的编程知识。除了最基本的命令行之外,至少需要熟悉一个高级语言,比如 Python,Golang 等。我们在开发的时候尤其需要注意解耦。网络工程师写的代码往往需要和多种外部环境打交道,例如 iptables,netlink,数据库以及硬件设备等。软件需要一定逻辑来维护和这些外部环境的关系。但如果代码写的耦合度过高,就会发现给代码写单元测试非常困难。我们通常会把这些交互都暴露成接口。

调用外部资源的时候,只要满足自己的需求即可。即使是外部资源有接口,我们也应该定义一套需要的接口。因为我们大部分时间不会用到对方所有的接口。定义自己需要的接口是为了方便引入其他实现,比如 mock。对外提供的接口需要谨慎协商,给外部调用的接口应该在实现时一起讨论,订好约定,避免出现代码升级对方无法编译的尴尬。

此外我们可以用工具生成接口的 mock,比如 gomock,pythonmock 等等。

最后提一些开发中的小建议,一是团队使用同一套框架。除非非常厉害,建议用框架。并且一个 Team 也要用同一个框架,暴露的 API 应该要遵守一些 REST 的公约。

二是不要手动维护 API 文档,API 文档应该是由程序生成的而不是人工维护的,很多框架有生成文档的功能。用良好的注释代替手动维护。

三是对接网络设备的 API,现在越来越多网络设备都有自己的 API 服务,用调用 API 的方式来避免多次 telnet 或者 ssh。

四是使用 CRD,如果程序需要在 kubernetes 中运行,那么暴露 API 最好的方式就是 CRD。

我们在网络中常用的开发 Repo 有如下这些:

有了良好的测试,下面需要就是团队协作。我们目前的工作流程是:

自动化工具

NetDevOps 本身不是开发工具,而是开发文化的一次革新,为了能够成功地实施 NetDevOps,需要借助一些工具。我们常用的工具有 Jenkins,Puppet,Ansible,Docker, Git,GNS3 等。

Prow 是 k8s 的原生 CI/CD 系统,用于 K8S 的代码的开发和维护。值得注意的是,Prow 运行在 K8S 上,即所谓的 “Build kubernetes with kubernetes on kubernetes ”。Prow 最为人所知的就是 Tide 机器人。关于Prow我们后续会有一个单独的演讲,有兴趣的同学可以关注一下。

网络开发中,往往需要在不同环境中测试,kubectx 可以用于切换不同集群。而 kubens 则可以用于切换默认 namespace。写网络插件的时候,往往需要将默认 namespace 切换为 kube-system。

在 Kubernetes 环境中,针对 Pod 进行抓包是个常规操作,在 Pod 中、在 Node 中都能够完成,抓出文件之后现场查看或者拷贝回来喂给 Wireshark 也都不难。Ksniff 工具的作用是,把这些常规步骤组织起来,用一个简单的 kubectl 插件命令,就能完成这一系列的操作。

点击阅读原文,一键下载资料。

-FIN-

7 月 25 日, 秉承科技前沿与企业实践并重的 CIC 大会再度来袭,为大家奉上一场科技盛宴。依旧硬核依旧信息过载,干货满满,绝对让你不虚此行。长按识别下面二维码,即刻报名!

发表评论
留言与评论(共有 0 条评论)
   
验证码:

相关文章

推荐文章

'); })();