2020年值得关注的混合云计算趋势

从混合多云的兴起到混合云策略中边缘计算和超融合基础设施的使用增加,到2020年将见证数个主导趋势,这些趋势将塑造混合云的未来。

全球混合云市场:概述

混合云是指结合了私有云和公有云网络的计算平台。公司通过将私有云的速度和可靠性以及公有云的经济性和可扩展性相结合,来实现混合云,以此来提高业务流程效率。两种云网络的结合使公司可以利用两者的优势。因此,对于那些努力在私有云的帮助下降低运营成本的公司而言,混合云已成为一种更可行的解决方案。

全球混合云市场:机遇与挑战

推动混合云市场的一些关键因素包括,人们越来越意识到通过混合云实现业务流程中的高效率,业务数据量不断增加以及跨行业实施物联网的情况有所增加。大数据管理工具在组织中的日益普及也导致混合云的合并增加。通过使用灵活且兼容的云网络,可以增强通过大数据技术实现的业务运营增值,从而可以将数据平稳地吸收到公司的运营数据库中。

云计算行业以惊人的速度发展,每年都有新的趋势和发展。 但是,有一件事已经很清楚了-云的未来是混合的。 混合云将私有云随附的任务关键型应用程序的更高安全性与公有云提供的灵活性和可扩展性相结合,可为组织带来两全其美的承诺。

有了这些优势,混合云已经成为首选的实施模型,它将推动云计算行业的发展。 研究公司Gartner的研究结果证实了这一趋势,该发现表明,计划,实施或升级云技术的企业全球基础架构决策者中有77%表示他们处于混合云环境中。

随着混合云空间的不断成熟,它正在快速发展。 让我们看一下确定未来一年混合云未来的一些明确趋势:

一致的混合云体验

关于混合云成为未来的讨论已经进行了一段时间。 2020年将变成现实。 借助混合云空间中的新创新,组织将能够确保在整个环境中获得无缝体验,而不必将公有云和本地或私有云视为单独的部分。 企业将能够克服延迟挑战,并体验真正一致的混合体验。

混合云正在崛起

跨行业的组织正在根据其特定的业务需求从多个云提供商选择和混合技术和服务,以避免供应商锁定并从同类最佳功能中受益。 随着多个云提供商被添加到混合云的公共领域,混合多云将成为新的IT常态。 通过混合使用来自多个提供商的本地和/或私有/公有云,混合多云为组织提供了自由和灵活性,可以在企业内部或云上运行其工作负载,甚至在需要时更改云提供商。 此外,混合多云方法使组织能够在整个环境中采用通用的管理和软件开发功能。 到2020年,混合多云将成为主导趋势,并将影响云产业。 根据麦肯锡公司(McKinsey&Company)的数据,到2022年,混合云将成为1.2万亿美元的市场机会。

边缘计算在混合云战略中越来越重要

边缘计算是一种模型,在该模型中,计算应尽可能靠近数据和内容的源和汇,这将成为混合云策略的关键要素。 混合云和边缘计算的组合功能为组织带来了巨大的价值,其中混合云生态系统可用于聚合最相关的数据和后端功能,而边缘则可支持处理和实时分析。 2020年,随着物联网设备数量的增加,越来越多的企业将利用混合云模型利用边缘优势来发现关键业务见解。

集装箱化持续上升

不管是在内部部署还是在一个或多个云上部署工作负载,行业专家都普遍认为容器是有效的混合云模型的核心,因为它们具有提供一致性的功能。 容器简化了与混合云相关的部署,管理和运营问题,从而帮助组织从其混合云战略中最大化业务价值。 到2020年,虽然集装箱将不再成为主流,但我们可以期望采用率更高。 随着每家技术巨头引入平台以简化集群的部署和管理,组织将考虑利用容器的功能来实现混合云异构和工作负载不可知。

用于混合云的超融合基础架构越来越受欢迎

超融合基础架构(HCI)逐渐成为支持混合环境的最佳选择,因为它解决了对环境的最大关注-复杂性的增加。 HCI使组织无需再将计算,存储和网络资源作为单独的层进行管理,从而使组织能够将云集成到其环境中。 组织获得了从一个单一窗格管理所有内容的能力。 此外,HCI解决方案的预集成,整合的计算和存储资源使云实施能够更快地运行,扩展更大并响应更快。 随着混合云的发展,HCI解决方案也在不断发展,以适应混合云世界的需求。 到2020年,预计会有越来越多的组织在HCI上运行其混合云,根据行业观察家的说法,它将最终成为混合云的首选基础架构平台。

灾难恢复和备份要求将刺激混合云的采用

在当今始终在线的业务场景中,有效的灾难恢复和备份对于确保业务连续性和数据安全比以往任何时候都变得更加重要。 提供可扩展性,灵活性和成本效益,灾难恢复和备份的混合云方法可以证明对企业极为有利。 DR是一项复杂且成本和资源密集的活动,因此基于云的DR和备份正在成为一种可行的选择。 使用混合云模型,组织可以在云中拥有二级异地备份位置,这比传统存储更有效,同时可以灵活地在专用网络上托管敏感数据并满足合规性要求。 不断增加的灾难恢复和备份要求将继续推动混合云在2020年之前的采用。

对混合云托管服务的吸收更强

2020年,许多组织将与云服务提供商合作,以定义和确定最佳的云管理方法。 组织将选择提供商以处理与混合云相关的复杂性,并有效地管理跨云提供商的实例,并托管在各种部署模型(私有云和内部部署)上。 提供强大的云管理平台和托管服务相结合的云提供商将凭借其帮助组织确保其混合基础架构的统一视图和整体无缝体验的能力而成为首选合作伙伴。

随着我们进入新的十年,云无疑比以往任何时候都更加勇敢和强大。 混合云将继续增长和发展,为组织提供更多的灵活性和一致性,并帮助他们加快应用程序部署周期。


相关链接:

2020年值得关注的7大新兴混合云计算趋势

混合云市场-2016年全球行业分析,规模,份额,增长,趋势和预测-2024

PaaS平台概述

什么是容器PaaS

平台即服务(PaaS)是一种云计算模型,其中第三方提供商通过互联网向用户提供硬件和软件工具(通常是应用程序开发所需的工具)。 PaaS提供商在其自己的基础架构上托管硬件和软件。 因此,PaaS使开发人员免于必须安装内部硬件和软件来开发或运行新应用程序的麻烦。

PaaS如何运作

如上所述,PaaS不会取代公司用于软件开发的整个IT基础架构。 它是通过云服务提供商的托管基础结构提供的,用户最常通过Web浏览器访问产品。 PaaS可以通过公有,私有和混合云交付,以交付诸如应用程序托管和Java开发之类的服务。

其他PaaS服务包括:

  • 与开发团队合作
  • 应用程序设计与开发
  • 应用程序测试和部署
  • Web服务集成
  • 信息安全
  • 数据库整合

用户按使用量付费使用PaaS。 但是,某些提供商对访问平台及其应用程序收取固定的月租费。

PaaS的优缺点

PaaS的主要好处是为用户提供了简便性-PaaS提供商提供了许多基础架构和其他IT服务,用户可以通过Web浏览器在任何地方访问。按使用付费的能力使企业可以消除传统上用于本地硬件和软件的资本支出。

但是,服务可用性或弹性可能是PaaS所关注的问题。如果提供商遇到服务中断或其他基础设施中断,则可能对客户产生不利影响,并导致生产力损失惨重。

供应商锁定是另一个常见的问题,因为用户无法轻松地将许多服务以及通过一个PaaS产品生成的许多数据迁移到另一种竞争产品。用户在选择PaaS提供商时必须评估服务停机和供应商锁定的业务风险。

PaaS产品的内部更改也是一个潜在问题。例如,如果PaaS提供商停止支持某种编程语言或选择使用其他开发工具集,则对用户的影响可能是困难且具有破坏性的。用户必须遵循PaaS提供商的服务路线图,以了解提供商的计划

PaaS通常不会取代一家企业,这会影响其环境和能力。

PaaS体系结构具有整个IT基础架构。相反,它倾向于合并各种底层的云基础架构组件,例如操作系统,服务器,数据库,中间件,网络设备和存储服务。这些功能均由服务提供商拥有,操作,配置和维护。 PaaS还提供其他资源,包括数据库管理系统,编程语言,库和各种开发工具。
PaaS提供商构建并提供了一个弹性和优化的环境,用户可以在该环境上安装应用程序和数据集。用户可以专注于创建和运行应用程序,而不是构建和维护基础架构和服务。

许多PaaS产品都面向软件开发。这些平台提供了计算和存储基础结构,以及文本编辑,版本管理,编译和测试服务,可帮助开发人员更快,更高效地创建新软件。 PaaS产品还可以使开发团队进行协作,而无论他们身在何处。

PaaS体系结构使其基础结构对开发人员和其他用户隐藏。结果,该模型类似于无服务器计算和功能即服务的架构,其中云服务提供商管理和运行服务器并控制资源的分配。

PaaS的类型

当前,开发人员可以使用各种类型的PaaS。 他们是:

  • 公有PaaS
  • 专用PaaS
  • 混合PaaS
  • 通讯PaaS
  • 移动PaaS
  • OpenPaaS

公有PaaS最适合在公有云中使用。公有PaaS允许用户控制软件的部署,而云提供商则管理托管应用程序所需的所有其他主要IT组件的交付,包括操作系统,数据库,服务器和存储系统网络。

公有PaaS供应商提供了中间件,该中间件使开发人员可以设置,配置和控制服务器和数据库,而无需设置基础结构。结果,公有PaaS和基础架构即服务(IaaS)一起运行,而PaaS在供应商的IaaS基础架构之上运行,同时利用了公有云。不幸的是,这意味着用户只能使用他们可能不想使用的单个公有云选项。

一些中小型企业已采用公有PaaS,但是较大的组织和企业由于与公有云的紧密联系而拒绝接受它。这主要是由于公有云内的企业应用程序开发涉及大量法规和合规性问题而导致的。

私有PaaS旨在提供公有PaaS的敏捷性,同时保持私有数据中心的安全性,合规性,收益并可能降低成本。私有PaaS通常作为设备或软件在用户防火墙内交付,该防火墙通常在公司的本地数据中心中维护。私有PaaS可以在任何类型的基础架构上开发,并且可以在公司的特定私有云中工作。

私有PaaS使组织可以更好地为开发人员提供服务,改善内部资源的使用并减少许多公司面临的昂贵的云计算蔓延。此外,私有PaaS允许开发人员部署和管理其公司的应用程序,同时还遵守严格的安全性和隐私要求。

混合PaaS将公有PaaS和私有PaaS结合在一起,为公司提供了公有PaaS提供的无限容量的灵活性以及在私有PaaS中拥有内部基础架构的成本效率。混合PaaS利用混合云。

Communication PaaS(CPaaS)是一个基于云的平台,允许开发人员在不使用后端基础结构和接口的情况下向其应用程序添加实时通信。通常,实时通信发生在专门为这些功能构建的应用程序中。示例包括Skype,FaceTime,WhatsApp和传统手机。

CPaaS提供了用于创建实时通信功能的完整开发框架,而无需开发人员构建自己的框架,包括基于标准的应用程序编程接口,软件工具,预构建的应用程序和示例代码。

CPaaS提供程序还通过提供支持和产品文档帮助用户整个开发过程。一些提供商还提供软件开发工具包以及可以帮助在不同的台式机和移动平台上构建应用程序的库。选择使用CPaaS的开发团队可以节省基础架构,人力资源和上市时间。

移动PaaS(MPaaS)是使用付费集成开发环境来配置移动应用程序。在mPaaS中,不需要编码技能。 MPaaS通过Web浏览器交付,通常支持公有云,私有云和本地存储。该服务通常按月租用,价格随所含设备和支持功能的数量而异。

MPaaS通常提供一个面向对象的拖放界面,通过直接访问设备的GPS,传感器,照相机和麦克风等功能,用户可以简化HTML5或本机应用程序的开发。它通常支持各种移动操作系统。

公司经常使用MPaaS来创建将提供内部使用和面向客户的应用程序。此实现可促进BYOD环境和生产力应用程序的发展,而无需移动应用程序开发人员或额外的IT支持。

OpenPaaS是一个免费的,开源的,面向业务的协作平台,在所有设备上都很有吸引力,并提供有用的Web应用程序,包括日历,联系人和邮件应用程序。 OpenPaaS旨在允许用户快速部署新应用程序,其目的是开发一种致力于企业协作应用程序的PaaS技术,特别是在混合云上部署的应用程序。

PaaS的使用

PaaS解决方案经常用于移动应用程序的开发中。但是,许多开发人员和公司也使用PaaS来构建跨平台应用程序,因为它提供了灵活而动态的解决方案,能够创建几乎可以在任何设备上运行的应用程序。

PaaS的另一种用法是在DevOps工具中。 PaaS提供了应用程序生命周期管理功能以及适合公司产品开发方法的特定功能。该模型还允许DevOps团队插入基于云的持续集成工具,这些工具可以添加更新而不会造成停机。此外,遵循瀑布式模型的公司可以使用与日常管理相同的控制台来部署更新。

PaaS还可以通过自动执行或完全消除内务处理和维护任务来减少应用程序的上市时间。此外,PaaS可以通过减轻管理可伸缩基础架构的负担来减少基础架构管理。 PaaS消除了负载平衡,扩展和分发新的依赖服务的复杂性。 PaaS提供者无需承担开发人员控制这些任务的责任。

此外,借助PaaS现在提供的更新编程语言和技术(例如无服务器功能和容器)的支持,开发人员可以使用该模型引入技术发展的新渠道。这尤其适用于技术变革缓慢的行业,例如银行业和制造业。 PaaS使这些组织可以适应最新的产品,而无需完全更改其业务流程。

平台即服务示例

PaaS提供程序的许多示例提供了在云中构建企业应用程序所需的工具和服务。 领先的提供商包括:

  • Google
  • Microsoft
  • Amazon Web Services (AWS)
  • com
  • IBM
  • Red Hat
  • Pivotal
  • Oracle
  • Heroku
  • Mendix
  • Engine Yard

Google App Engine支持使用Java,Python,PHP和Go的分布式Web应用程序。 Red Hat OpenShift是一种Pa​​aS产品,用于使用多种语言,数据库和组件来创建开源应用程序。 Heroku PaaS提供Unix样式的容器计算实例,这些实例在隔离的环境中运行进程,同时支持Ruby,Python,Java,Scala,Clojure和Node.js等语言。

Microsoft Azure支持.NET,Node.js,PHP,Python,Java和Ruby中的应用程序开发,并允许开发人员使用软件开发人员工具包和Azure DevOps创建和部署应用程序。

AWS Elastic Beanstalk允许用户在Apache,Nginx,Passenger和IIS等常见服务器上创建,部署和扩展使用Java,.NET,PHP,Node.js,Python,Ruby,Go和Docker开发的Web应用程序和服务。

尽管许多PaaS提供商都提供类似的服务,但是每个提供商都有其细微的差别和局限性。用户必须测试潜在的提供商,以确保其服务满足任何业务或技术要求,例如支持的语言和服务可用性,这一点很重要。

PaaS,IaaS和SaaS之间的区别

PaaS是云计算服务的三个主要类别之一。 另外两个是软件即服务(SaaS)和基础架构即服务(IaaS)。

比较云计算服务模型:
下表提供了三种云计算服务类别的描述

借助IaaS,提供商可以提供基本的计算,存储和网络基础结构以及虚拟机管理程序-虚拟化层。 然后,用户必须创建虚拟机,安装操作系统,支持应用程序和数据,并处理与这些任务相关的所有配置和管理。

借助PaaS,提供商可以提供比IaaS解决方案更多的应用程序堆栈,并将操作系统,中间件(例如数据库)和其他运行时添加到云环境中。

借助SaaS,提供商可以提供整个应用程序堆栈。 用户只需登录并使用完全在提供商的基础架构上运行的应用程序即可。


相关链接

平台即服务(PaaS)

大数据容器化-基于Kubernetes构建现代大数据系统

Apache Spark

在大数据处理与分析领域,Apache Spark无疑占据着重要地位。它的特点是基于内存计算,支持各类资源管理平台,其中以YARN最为常见,同时又与Hadoop平台集成,在集群节点以HDFS作为分布式文件存储系统。

我们可以先看一下搭建一个常见的Apache Spark大数据平台需要哪些步骤:

  1. 安装Hadoop集群
  2. 配置HDFS
  3. 配置YARN
  4. 安装Spark
  5. 配置Spark与YARN集成

事实上如果参阅官方文档,还有更多细节检查与配置,有过大数据相关领域从业经验的人都知道,要搭建一套可用的大数据环境并不容易,再加上后期维护,就更吃力了,而一套稳定的大数据平台正是进行大数据应用开发的基础。根据笔者了解,有不少公司正是因为大数据平台搭建及配置的复杂性等原因,不得不在多个测试环境中,共用一套大数据平台,这种方式长期看维护成本较高,也可能存在安全隐患。

大数据领域需要一些变化,而Kubernetes的出现则提供了契机。

Kubernete(以下简称k8s)是容器集群管理系统,是一个开源的平台,可以实现容器集群的自动化部署、自动扩缩容、维护等功能。通过Kubernetes你可以:

  • 快速部署应用
  • 快速扩展应用
  • 无缝对接新的应用功能
  • 节省资源,优化硬件资源的使用

大数据社区

随着K8s社区的发展壮大,微服务及容器化被越来越多的公司应用到生产环境。与此同时,K8s也成为容器编排的首选平台。大数据社区在容器化进程中当然也是不甘落后的。

Spark自2.3开始官方支持K8s
Flink自1.9开始官方支持K8s
Hue官方Helm chart包
Hive以MR3为执行引擎支持K8s
Airflow自1.10开始支持K8s
Presto支持K8s
…… 

可以看到整个大数据社区也在积极支持容器化,但大数据的容器化并不是生硬地将各个组件搬到K8s上,以Spark on YARN为例,核心组件YARN作为资源调度器,其结构如下图所示

下图讲述了Apache Spark on YARN的工作方式:

YARN ResourceManager的功能为:

负责集群中所有资源的统一管理和分配,它接收来自各个节点(NodeManager)的资源汇报信息,并把这些信息按照一定的策略分配给各个应用程序

了解K8s的同学可以看出YARN的功能其实与K8s Scheduler的功能非常类似

Kubernetes 调度器是一个策略丰富、拓扑感知、工作负载特定的功能,调度器显著影响可用性、性能和容量。调度器需要考虑个人和集体的资源要求、服务质量要求、硬件/软件/政策约束、亲和力和反亲和力规范、数据局部性、负载间干扰、完成期限等。

所以与其将YARN生搬到K8s中(早期确实是这样做的),何不用K8s调度器替换掉YARN,使得Spark适应K8s呢? 事实上社区确实是在这个方向努力尝试,并且自Spark 2.3开始,实验性支持使用K8s原生Scheduler替代YARN。

spark on k8s:

在该方案中

  1. 客户端通过`spark-submit`将任务提交到K8s集群中,并在集群中启动一个Spark Driver Pod;
  2. Spark Driver启动相应的Executor Pod, 组成一个Spark Application集群并执行作业任务;
  3. 任务执行完成后,Executor Pod会被销毁, 而Driver Pod会持久化相关日志,并保持在’completed’状态,直到用户手清理或被K8s集群的垃圾回收机制回收.

Spark原生支持K8s的好处也是很明显的:可以更好的利用K8s的集群资源,通过K8s赋能,更好的进行资源的隔离。这个方案不太友好的地方在于:`spark-submit`在K8s集群之外,使用非声明式的提交接口,实际使用起来不够友好。

将Spark应用迁移到K8s环境中

Spark Operator是Google基于Operator模式开发的一款的工具, 用于通过声明式的方式向K8s集群提交Spark作业,并且负责管理Spark任务在K8s中的整个生命周期,其工作模式如下图所示:

我们可通过Hem安装`spark-operator`

$ helm repo add incubator http://storage.googleapis.com/kubernetes-charts-incubator
$ helm install incubator/sparkoperator --namespace spark-operator

创建服务用户及绑定权限

$ kubectl create serviceaccount spark 
$ kubectl create clusterrolebinding spark-role --clusterrole=edit --serviceaccount=default:spark --namespace=default

一个典型的Spark应用在K8s中的资源描述文件`spark-pi.yaml`如下所示

apiVersion: "sparkoperator.k8s.io/v1beta2"
kind: SparkApplication
metadata:
  name: spark-pi
  namespace: default
spec:
  type: Scala
  mode: cluster
  image: "gcr.io/spark-operator/spark:v2.4.4"
  imagePullPolicy: Always
  mainClass: org.apache.spark.examples.SparkPi
  mainApplicationFile: "local:///opt/spark/examples/jars/spark-examples_2.11-2.4.4.jar"
  sparkVersion: "2.4.4"
  restartPolicy:
    type: Never
  volumes:
    - name: "test-volume"
      hostPath:
        path: "/tmp"
        type: Directory
  driver:
    cores: 1
    coreLimit: "1200m"
    memory: "512m"
    labels:
      version: 2.4.4
    serviceAccount: spark
    volumeMounts:
      - name: "test-volume"
        mountPath: "/tmp"
  executor:
    cores: 1
    instances: 1
    memory: "512m"
    labels:
      version: 2.4.4
    volumeMounts:
      - name: "test-volume"
        mountPath: "/tmp"

部署运行

$ kubectl apply -f spark-pi.yaml

计算与存储分离

计算与存储耦合存在的问题:

  1. 当存储或计算其中一方资源不足时,只能同时对两者进行扩容,导致扩容的经济效率比较低(另一种扩容的资源被浪费了);
  2. 在云计算场景下,不能实现真正的弹性计算,因为计算集群中也有数据,关闭闲置的计算集群会丢失数据。

因为耦合导致的以上这些问题,导致很多公司不得不考虑这种耦合的必要性。而Hadoop的架构设计正是计算与存储耦合,这种设计并不适合云原生架构。而作为大数据存储的基石-HDFS,目前并无官方的K8s解决方案,不过在K8s社区本身就有许多优秀的存储解决方案-MINIO

MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。而且实验数据表明,其性能丝毫不逊色于HDFS

安装MINIO也非常容易

$ helm install stable/minio

我们以WordCount,数据读写使用minio存储系统(兼容亚马逊S3云存储服务接口)

JavaRDD<String> textFile = sc.textFile("s3a://...");
JavaPairRDD<String, Integer> counts = textFile
    .flatMap(s -> Arrays.asList(s.split(" ")).iterator())
    .mapToPair(word -> new Tuple2<>(word, 1))
    .reduceByKey((a, b) -> a + b);
counts.saveAsTextFile("s3a://..."); 

由于兼容亚马逊S3云存储服务接口这一优势,minio也同样可以作为Hive数据仓库的可选存储系统。

<property>
<name>fs.s3a.path.style.access</name>
<value>true</value>
<description>Enable S3 path style access.</description>
</property>
<property>
<name>hive.metastore.warehouse.dir</name>
<value>s3a://hive/warehouse</value>
</property>

总结

通过以上论述,在K8s集群上搭建Spark大数据平台,相比传统YARN调度方式而言更为简洁,MINIO可作为大数据的存储系统,在保证数据的持久性的同时,也实现了大数据计算系统与存储系统的解耦。

参考

GPU在容器云中的方案及使用

问题根源需求

在使用容器云调度资源场景下,我们可以请求并限制特定配额的CPU及内存供于容器创建使用,K8S调度器会将Pod绑定到资源合适的节点上;但对于现实使用场景原生资源的调度能力仍然不能满足现有的用户,其他特定资源例如GPU、IB卡、硬加密狗等也是迫切需要的,用户希望特定资源也可以被调度工具发现、监管、分配并最终使用。

GPU卡作为重要的计算资源不管是在算法训练还是预测上都不可或缺,而对于常见的算法训练平台或智能业务平台都有往容器迁移演进的趋势,此时如何更好的利用GPU资源成了容器云平台需要解决的问题。

所以可以看出需要解决的问题主要是一下三个方面:

1.资源管理:调度器可以发现并调度GPU资源;

2.资源限制隔离:运行在同一张卡上的GPU资源可以限制在配额之内;

3.资源算力损耗较少:同等算力的GPU资源计算能力不出现明显衰减;

解决方案

目前K8S官方对于如何共享单张GPU资源没有很好的解决方案,而对于使用多张GPU也停留在整张GPU卡作为调度颗粒度方式,应付复杂的使用场景譬如集群内存在多种GPU类型时仍有不足。处理复杂使用场景的GPU调度方案需要将资源多维度的标注,针对这种需求可以做如下处理:

1.以显存为单位上报资源并调度使用,用于应对共享单张GPU的场景;

2.以卡为单位上报资源并调度使用,用于应对多卡加速计算场景;

3.以类型为标签标注节点,尽量在同一节点安装相同GPU;

同时用户对于使用特型资源都不太愿意修改K8S原生代码,我们可以利用K8S现有的机制来避免对主干代码的侵入,如下:

1.Extended Resource机制:用于定义GPU资源;

2.Scheduler Extender机制:用于对GPU资源进行调度;

3.Device Plugin机制:用于上报、监管和分配GPU资源;

方案设计迭代

计划分三步迭代:

一、初步卡共享可行

已实现以显存/卡作为调度颗粒的资源上报使用,用户可以在该方案下在单张GPU上运行多个Pod和将多卡同时供单个Pod使用。

二、多类型GPU集群调度

需要支持多类型GPU调度使用,根据请求类型调度到特定节点上运行;同一节点上显存资源及卡资源联动,避免被以显存分配的卡重分配给卡单位的调度请求。

完成以上两部迭代后,主要架构如下:

三、GPU卡内配额限制

对于共享单张卡都无可避免的会出现计算效率下降的问题,此时可以利用Nvida官方提供的MPS接口,开启该功能可以运行多个进程在GPU上叠加提供利用率,减少了GPU上下文存储与切换,降低了调度带来的开销。需要注意的是容器内使用MPS需要GPU架构高于volta,runc默认为nvidia。对于配额的限制前期还是建议利用应用程序自己的机制来实现。在容器环境中开启MPS功能将根据一下来实现:

需要注意的时Nvidia在Volta 架构引入了新的 MPS 功能。与 Volta GPU 前的 MPS 相比,Volta MPS 提供了一些关键改进:

  • Volta MPS 客户端直接向 GPU 提交工作,而无需通过 MPS 服务器。
  • 每个 Volta MPS 客户端都拥有自己的 GPU 地址空间,而不是与所有其他 MPS 客户端共享 GPU 地址空间。
  • Volta MPS 支持为服务质量 (QoS) 提供有限的执行资源资源。

Volta前架构与Volta机构GPU使用MPS对比:

GPU在K8S中使用的全流程演示

要求:

1.节点有GPU资源

2.docker >=1.12

3.K8S >=1.10

一、安装Nvidia驱动及CUDA

驱动是应用使用GPU资源的前提

按照自己的需求安装特定版本的驱动(需要>=384.81),例如在ubuntu1604上安装CUDA10.2的驱动可以参考一下:

$ wget http://developer.download.nvidia.com/compute/cuda/10.2/Prod/local_installers/cuda_10.2.89_440.33.01_linux.run
$ sudo sh cuda_10.2.89_440.33.01_linux.run

安装完成后,可以用一下命令确认:

$ nvidia-smi

二、部署安装nvidia-docker2

NVIDIA 容器工具包允许用户构建和运行 GPU 加速 Docker 容器。该工具包包括一个容器运行时和实用程序,用于自动配置容器以利用 NVIDIA GPU。

可以参考一下安装:

distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

安装完成后验证是否已成功可以使用:

docker run --gpus all nvidia/cuda:9.0-base nvidia-smi

三、切换runtime为nvidia-container-runtime

通过修改Docker的Runtime为nvidia runtime工作,当我们执行 nvidia-docker create 或者 nvidia-docker run 时,它会默认加上 --runtime=nvidia 参数。将runtime指定为nvidia。为了方便使用,可以直接修改Docker daemon 的启动参数,修改默认的 Runtime为:

cat /etc/docker/daemon.json
{
    "default-runtime": "nvidia",
    "runtimes": {
        "nvidia": {
            "path": "/usr/bin/nvidia-container-runtime",
            "runtimeArgs": []
        }
    }
}

然后重启docker

四、部署GPU-Scheduler及开启K8S相关功能

使用GPU-Scheduler需要更改原生调度启动参数,利用K8S的扩展机制,在全局调度器筛选绑定的时候查找某个节点的特定GPU卡是否能够提供足够的显存,并且在绑定时将GPU分配结果通过annotation记录到Pod Spec以供后续检查分配结果。添加以下:

- --policy-config-file=/etc/kubernetes/scheduler-policy-config.json

scheduler-policy-config.json的具体内容为:

{
  "kind": "Policy",
  "apiVersion": "v1",
  "extenders": [
    {
      "urlPrefix": "http://127.0.0.1:32766/gpushare-scheduler",
      "filterVerb": "filter",
      "bindVerb":   "bind",
      "enableHttps": false,
      "nodeCacheCapable": true,
      "managedResources": [
        {
          "name": "aliyun.com/gpu-mem",
          "ignoredByScheduler": false
        }
      ],
      "ignorable": false
    }
  ]
}

待调度组件正常启动后,再部署GPU-Scheduler:

---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: gpushare-schd-extender
rules:
- apiGroups:
  - ""
  resources:
  - nodes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - update
  - patch
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - bindings
  - pods/binding
  verbs:
  - create
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gpushare-schd-extender
  namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: gpushare-schd-extender
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gpushare-schd-extender
subjects:
- kind: ServiceAccount
  name: gpushare-schd-extender
  namespace: kube-system

# deployment yaml
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: gpushare-schd-extender
  namespace: kube-system
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: gpushare
        component: gpushare-schd-extender
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ''
    spec:
      hostNetwork: true
      tolerations:
      - effect: NoSchedule
        operator: Exists
        key: node-role.kubernetes.io/master
      - effect: NoSchedule
        operator: Exists
        key: node.cloudprovider.kubernetes.io/uninitialized
      serviceAccount: gpushare-schd-extender
      containers:
        - name: gpushare-schd-extender
          image: registry.cn-hangzhou.aliyuncs.com/acs/k8s-gpushare-schd-extender:1.11-d170d8a
          env:
          - name: LOG_LEVEL
            value: debug
          - name: PORT
            value: "12345"

# service.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: gpushare-schd-extender
  namespace: kube-system
  labels:
    app: gpushare
    component: gpushare-schd-extender
spec:
  type: NodePort
  ports:
  - port: 12345
    name: http
    targetPort: 12345
    nodePort: 32766
  selector:
    # select app=ingress-nginx pods
    app: gpushare
    component: gpushare-schd-extender

五、简单部署GPUshare-device-plugin确认可行

GPUshare-device-plugi是开源的GPU资源上报组件,利用Device Plugin机制,由Kubelet负责调度GPU卡分配,依据GPU-Scheduler分配结果执行。可以用来简单的部署测试下是否已经可使用:

kind: DaemonSet
metadata:
  name: gpushare-device-plugin-ds
  namespace: kube-system
spec:
  template:
    metadata:
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ""
      labels:
        component: gpushare-device-plugin
        app: gpushare
        name: gpushare-device-plugin-ds
    spec:
      serviceAccount: gpushare-device-plugin
      hostNetwork: true
      nodeSelector:
        gpushare: "true"
      containers:
      - image: registry.cn-hangzhou.aliyuncs.com/acs/k8s-gpushare-plugin:v2-1.11-aff8a23
        name: gpushare
        command:
          - gpushare-device-plugin-v2
          - -logtostderr
          - --v=5
          - --memory-unit=GiB
          - --mps=true
        resources:
          limits:
            memory: "300Mi"
            cpu: "1"
          requests:
            memory: "300Mi"
            cpu: "1"
        env:
        - name: KUBECONFIG
          value: /etc/kubernetes/kubelet.conf
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]
        volumeMounts:
          - name: device-plugin
            mountPath: /var/lib/kubelet/device-plugins
      volumes:
        - name: device-plugin
          hostPath:
            path: /var/lib/kubelet/device-plugins
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: gpushare-device-plugin
rules:
- apiGroups:
  - ""
  resources:
  - nodes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - update
  - patch
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - nodes/status
  verbs:
  - patch
  - update
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gpushare-device-plugin
  namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: gpushare-device-plugin
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gpushare-device-plugin
subjects:
- kind: ServiceAccount
  name: gpushare-device-plugin
  namespace: kube-system
$ kubectl create -f gpu.yml

使用kubectl命令确认节点内已经有GPU资源:aliyun.com/gpu-mem 后就可以尝试部署一个服务看是否正常:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-1
  labels:
    app: gpu-1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gpu-1
  template:
    metadata:
      labels:
        app: gpu-1

    spec:
      containers:
      - name: gpu-1
        image: bvlc/caffe:gpu
        command: ["/bin/sh"]
        args: ["-c","while true;do echo hello;sleep 1;done"]
        resources:
          limits:
            # GiB
            aliyun.com/gpu-mem: 1

如果Pod调度成功且可以在容器内正常调用GPU说明已经可以使用。

总结

方案在已成功验证步骤一情况下,结合现有资料说明发展路径是可行,后续的步骤是具体实现上的精力花费。

参考:

魔方云钉钉告警服务

用户可以在魔方云中定义告警,当告警被触发时,魔方云。这样的告警流程是基于Prometheus和Alertmanager的,具体的流程如图1所示。

图1 告警流程

其中Prometheus是监控系统,负责对集群的监控。Alertmanager负责告警的发送。

用户在魔方云中设置了具体的告警规则,每个告警规则对应接收对象,魔方云会把告警规则写入Prometheus的配置文件,并把告警规则对应的接收对象的信息写入Alertmanager。Prometheus会监控告警规则描述的值,当告警被触发时,Prometheus将告警的内容发送给Alertmanager,Alertmanager则将告警信息与的接受者信息对应起来,将信息发送给接收者。

Alertmanager本身支持以下几种类型的接收对象:

  • 电子邮件
  • Slack
  • PagerDuty
  • 微信
  • Webhook

其中前4种是主流的IT服务对象。Webhook是通用接收对象,可以用于扩展其他原本不支持的服务对象,钉钉的告警服务就是通过webhook来扩展的。扩展的思路为:首先编写一个http服务端,用于接收钉钉的告警信息。随后在魔方云中添加一个webhook配置,指向部署的服务端的地址,并把钉钉告警的配置作为参数添加在url中。告警触发后按照上图的流程由alertmanager转发到部署的服务端,服务端接收到告警信息后,读取url中的相关参数,最后将告警发送至钉钉。图2是添加了告警转发服务后的流程图。

图2 钉钉告警流程

钉钉告警扩展方法

1.编写钉钉告警转发服务端程序

服务端首先需要做的事是接收魔方云发送的webhook告警信息,并从URL中读取钉钉告警的配置:钉钉webhook、需要at用户的账号和是否at所有人。

对于webhook告警,alertmanager会以json形式发送如下的结构体

type Alert struct {
    Status       string            `json:"status"`
    Labels       map[string]string `json:"labels"`
    Annotations  map[string]string `json:"annotations"`
    StartsAt     time.Time         `json:"startsAt"`
    EndsAt       time.Time         `json:"endsAt"`
    GeneratorURL string            `json:"generatorURL"`
}

type Message struct {
    Version           string            `json:"version"`
    GroupKey          string            `json:"groupKey"`
    Status            string            `json:"status"`
    Receiver          string            `json:"receiver"`
    GroupLabels       map[string]string `json:"groupLabels"`
    CommonLabels      map[string]string `json:"commonLabels"`
    CommonAnnotations map[string]string `json:"commonAnnotations"`
    ExternalURL       string            `json:"externalURL"`
    Alerts            []Alert           `json:"alerts"`
}

服务端接收告警信息并读取url中的参数。

func ReceiveAndSend(w http.ResponseWriter, req *http.Request) {
    log.SetFlags(log.LstdFlags | log.Lshortfile)

    body, err := ioutil.ReadAll(req.Body)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        _, _ = fmt.Fprint(w, err)
        log.Printf("[ERROR] %s", err)
        return
    }

    alertMessage := Message{}
    _ = json.Unmarshal(body, &alertMessage)

    err = req.ParseForm()
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        _, _ = fmt.Fprint(w, err)
        return
    }

    if _, ok := req.Form["webhook"]; !ok {
        log.Print("[ERROR] url argument \"webhook\" is null")
        return
    }
    if _, ok := req.Form["atmobiles"]; !ok {
        log.Print("[ERROR] url argument \"atmobiles\" is null")
        return
    }
    if _, ok := req.Form["isatall"]; !ok {
        log.Print("[ERROR] url argument \"isatall\" is null")
        return
    }
    webhook := req.Form["webhook"][0]
    atmobiles := req.Form["atmobiles"]
    isatall, _ := strconv.ParseBool(req.Form["isatall"][0])

    err = SendToDingtalk(alertMessage, webhook, atmobiles, isatall)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        _, _ = fmt.Fprint(w, err)
        log.Printf("[ERROR] %s", err)
        return
    }

    _, _ = fmt.Fprint(w, "Alert sent successfully")
}

向从url中读取的地址发送钉钉告警信息。

type At struct {
    AtMobiles []string `json:"atMobiles"`
    IsAtAll   bool     `json:"isAtAll"`
}

type DingTalkMarkdown struct {
    MsgType  string   `json:"msgtype"`
    At       At       `json:"at"`
    Markdown Markdown `json:"markdown"`
}

type Markdown struct {
    Title string `json:"title"`
    Text  string `json:"text"`
}

const layout = "Jan 2, 2006 at 3:04pm (MST)"

func SendToDingtalk(alertMessage Message, webhook string, atMobiles []string, isAtAll bool) error {
    groupKey := alertMessage.CommonLabels["group_id"]
    status := alertMessage.Status

    message := fmt.Sprintf("### 通知组:%s(状态:%s)\n\n", groupKey, status)

    if _, ok := alertMessage.CommonLabels["alert_type"]; !ok {
        return errors.New("alert type is null")
    }

    var description string
    switch alertMessage.CommonLabels["alert_type"] {
    case "event":
        if _, ok := alertMessage.CommonLabels["event_type"]; !ok {
            return errors.New("event_type is null in commonLabels")
        }
        if _, ok := alertMessage.GroupLabels["resource_kind"]; !ok {
            return errors.New("resource kind is null in groupLabels")
        }
        description = fmt.Sprintf("\n > %s event of %s occuored\n\n", alertMessage.CommonLabels["event_type"], alertMessage.GroupLabels["resource_kind"])
    case "systemService":
    // ...
    default:
        return errors.New("invalid alert type")
    }

    message += description

    for _, alert := range alertMessage.Alerts {
        if alert.Status != "firing" {
            continue
        }
        message += "-----\n"

        for k, v := range alert.Labels {
            message += fmt.Sprintf("- %s : %s\n", k, v)
        }
        message += fmt.Sprintf("- 起始时间:%s\n", alert.StartsAt.Format(layout))
    }

    dingtalkText := DingTalkMarkdown{
        MsgType: "markdown",
        At: At{
            AtMobiles: atMobiles,
            IsAtAll:   isAtAll,
        },
        Markdown: Markdown{
            Title: fmt.Sprintf("通知组:%s(当前状态:%s)", groupKey, status),
            Text:  message,
        },
    }

    data, err := json.Marshal(dingtalkText)
    if err != nil {
        return err
    }

    req, err := http.NewRequest(http.MethodPost, webhook, bytes.NewBuffer(data))
    if err != nil {
        return err
    }

    req.Header.Set("Content-Type", "application/json")
    tr := &http.Transport{
        TLSClientConfig:    &tls.Config{
            InsecureSkipVerify:        true,
        },
    }
    client := http.Client{Transport:tr}

    resp, err := client.Do(req)
    if err != nil {
        return err
    }

    if resp.StatusCode != 200 {
        log.Printf("[ERROR] %s", resp.Header)
    }

    log.Printf("[INFO] Alert message sent to %s successfully", webhook)
    _ = resp.Body.Close()
    return nil
}

2.部署服务端

将服务端程序制作成docker镜像,上传至镜像仓库。在魔方云的helm包中添加一个依赖charts,使用刚才制作的docker镜像。在用户添加了告警规则后,钉钉告警转发服务就会自动启动。

钉钉告警使用流程

1.添加钉钉通知

首先在钉钉群中添加一个自定义机器人,并复制该机器人的webhook。

进入集群页面,点击侧边栏的“通知”,然后点击右边的“添加通知”按钮。

选择“dingtalk”,并填写相关信息。可以点击“测试”按钮来测试填写的信息是否正确,如果没有错误,对应的钉钉账号会收到一条测试消息。确认无误后点击下方的”添加“按钮。

2.添加告警规则

点击侧边栏的”告警“进入告警页面,然后点击右边的”添加告警组“按钮,配置告警规则,最好降低告警触发的条件,便于测试,然后在接收者栏中选择钉钉,可以在“Notifier”中填写要at的用户的手机号码,用英文逗号分隔。在这里添加的at用户会覆盖通知中的相应用户。最后点击”创建“按钮。此时一条告警规则已经创建完毕,当告警触发时会向钉钉发送告警信息。

3.等待告警触发

等待告警触发后,相应告警的状态会变成红色字体的“Alerting”。

相应的钉钉账户就会收到一条消息。

Istio入门

一、背景

随着单片应用程序向分布式微服务架构过渡 ,特别是服务之间呈现拓扑状的复杂关系,service mesh的提出就是为了简化管理微服务之间的通信问题。为了实现微服务 Service Mesh 模式和诸多理念,Google , IBM 和 Lyft 这三家公司协同研发,并于 2017 年 6 月 8 日( 根据 Github 最后一次提交的时间 )发布了 Istio 的第一个发行版——Istio 0.1 版本。

二、istio架构

istio分为控制面和数据面,架构如下图所示。

![](/tp/5de067fab481934666d3bb44090fadad/istio

数据面:由一组sidecar组成,对应具体的组件为envoy;通过给每用启动一个轻量级的网络代理,来执行对网络通信的控制和调整,Sidecar和外围代理,实现客户端和服务器之间的安全通信;

控制面:负责管理和配置代理流量。具体通过mixer组件下发策略给envoy,执行策略并对各个sidecar收集数据。Citadel用于密钥和证书管理;pilot将身份验证策略和安全命名信息分发到代理;mixer用于管理授权和审核。

三、核心功能

流量管理:

下图显示了pilot的服务发现过程。

stio根据Kubernetes的适配器发现并注册service后,流量规则会由pilot解析成envoy理解的格式传送给Sidecar,进而控制服务间的流量和 API 调用。Istio 简化了断路器、超时和重试等服务级别属性的配置,并且可以轻松设置 A/B 测试、金丝雀部署和基于百分比的流量分割的分阶段部署等重要任务。

安全:

Istio提供底层安全通信通道,使开发人员可以专注于应用程序级别的安全,并提供大规模管理服务通信的身份验证、授权和加密。使用Istio,服务通信在默认情况下是安全的,可以跨不同的协议和运行时一致地实施策略,所有这些都只需很少或根本不需要更改应用程序。

安全涉及的几个组件及架构如下图所示:

Citadel:用于密钥和证书管理。

Sidecar和外围代理:实现客户端和服务器之间的安全通信。

pilot:将身份验证策略和安全命名信息分发到代理。

mixer:用于管理授权和审核。

策略定制:为应用程序配置自定义策略,以在运行时强制执行规则,如动态限制服务的通信量,通过名单限制对服务的访问,也可以创建自己的策略适配器,添加自定义授权行为。

可观察性:Istio强大的跟踪、监控和日志记录功能可让人深入了解服务网格的部署。通过Istio的监控功能,可以真正了解服务性能对上下游的影响,同时其定制的仪表盘可查看所有服务的性能,并了解该性能对其他流程有何影响。

四、基本功能验证

本环境基于Kubernets1.14和istio1.13版本进行验证。其中下面的例子都在官方链接的samples目录中,官方链接[1]istio官方例子

1、流量管理:

为了填充自己的服务注册表,Istio连接到服务发现系统,而在Kubernetes集群上安装了Istio,Istio会自动检测该集群中的服务和端点,使用此服务注册表,代理就可以将流量定向到相关服务。默认情况下同一工作负载多个实例,流量会平均分发,而作为A/B测试的一部分,也可以将特定百分比的流量定向到服务的新版本,或者对特定服务实例子集的流量应用不同的负载平衡策略。还可以对进出Mesh的流量应用特殊规则,或者将Mesh的外部依赖项添加到服务注册表。

以官方的bookinfo为例,使用对同一程序多版本的流量管理,具体配置如下:

自注入使能

kubectl label namespace default istio-injection=enabled

部署bookinfo到default namespaces,bookinfo服务之间默认的调用关系如下图:

可创建virtualService全部流量导向reviews-v1,yaml文件中host指向的是reviews service,只指定了v1版本,因此流量全导向reviews v1。

virtualService yaml如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1

结果如下图,可以看到图中绿色部分就是当前流量的走向,全部走向previews v1:

2、安全,主要提供服务网格之间安全访问,这里以enable TLS为例。

创建meshPolicy全局enable tls

kubectl apply -f - <<EOF
apiVersion: "authentication.istio.io/v1alpha1"
kind: "MeshPolicy"
metadata:
  name: "default"
spec:
  peers:
  - mtls: {}
EOF

因为使能了TLS,所以不带证书访问会报错,直接http访问结果如下:

for from in "foo" "bar"; do for to in "foo" "bar"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
sleep.foo to httpbin.foo: 503
sleep.foo to httpbin.bar: 503
sleep.bar to httpbin.foo: 503
sleep.bar to httpbin.bar: 503

创建 destination rules使能TLS,目标是所有的集群内部的服务,然后服务之间就可以正常的访问了,使能TLS的操作如下:

kubectl apply -f - <<EOF
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "default"
  namespace: "istio-system"
spec:
  host: "*.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
EOF

根据destination rules,访问所有集群内部服务都会带上TLS证书进行访问,使能TLS的访问结果:

for from in "foo" "bar"; do for to in "foo" "bar"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
sleep.foo to httpbin.foo: 200
sleep.foo to httpbin.bar: 200
sleep.bar to httpbin.foo: 200
sleep.bar to httpbin.bar: 200

除了全局指定tls,也可以单独指定namespace使能TLS,操作如下:

kubectl apply -f - <<EOF
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "default"
  namespace: "foo"
spec:
  peers:
  - mtls: {}
EOF

kubectl apply -f - <<EOF
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "default"
  namespace: "foo"
spec:
  host: "*.foo.svc.cluster.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
EOF

指定特定service tls,操作如下:

cat <<EOF | kubectl apply -n bar -f -
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "httpbin"
spec:
  targets:
  - name: httpbin
  peers:
  - mtls: {}
EOF
cat <<EOF | kubectl apply -n bar -f -
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "httpbin"
spec:
  host: "httpbin.bar.svc.cluster.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
EOF

3、策略

这里还是以官方的bookinfo为例,指定应用拒绝访问。

首先修改istio configmap修改disablePolicyChecks为false,使能policy;然后制定策略拒绝v3版本的访问版本,匹配源为reviews v3和目的ratings制定rule对应handler为拒绝访问,yaml如下:

 code: 7
      message: Not allowed
---
apiVersion: "config.istio.io/v1alpha2"
kind: instance
metadata:
  name: denyreviewsv3request
spec:
  compiledTemplate: checknothing
---
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
  name: denyreviewsv3
spec:
  match: destination.labels["app"] == "ratings" && source.labels["app"]=="reviews" && source.labels["version"] == "v3"
  actions:
  - handler: denyreviewsv3handler
    instances: [ denyreviewsv3request ]

4、可观察性

Istio为网格内的所有服务通信生成详细的telemetry信息。此telemetry提供服务行为的可观察性,使运维人员能够对应用程序进行故障排除、维护和优化, 具体通过三个方面表现,第一个是metrics即指标,Istio根据监控的四个维度(延迟、流量、错误和饱和度)生成一组服务指标,暴露给proetheus。第二个是访问日志,当流量流入网格内的服务时,Istio可以生成每个请求的完整记录,包括源和目标元数据。此信息使操作员能够审核服务行为,直至各个工作负载实例级别。第三个是分布式跟踪, Istio提供了一种通过监视流经网格的各个请求来监视和了解行为的方法,了解服务网状网内的服务依赖关系和延迟来源。

以bookinfo为例,配置istio自动收集服务指标,每次调用网格内的服务,都会有相应的指标生成。

配置收集metrics的yaml文件如下:

# Configuration for metric instances
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
  name: doublerequestcount
  namespace: istio-system
spec:
  compiledTemplate: metric
  params:
    value: "2" # count each request twice
    dimensions:
      reporter: conditional((context.reporter.kind | "inbound") == "outbound", "client", "server")
      source: source.workload.name | "unknown"
      destination: destination.workload.name | "unknown"
      message: '"twice the fun!"'
    monitored_resource_type: '"UNSPECIFIED"'
---
# Configuration for a Prometheus handler
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
  name: doublehandler
  namespace: istio-system
spec:
  compiledAdapter: prometheus
  params:
    metrics:
    - name: double_request_count # Prometheus metric name
      instance_name: doublerequestcount.instance.istio-system # Mixer instance name (fully-qualified)
      kind: COUNTER
      label_names:
      - reporter
      - source
      - destination
      - message
---
# Rule to send metric instances to a Prometheus handler
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: doubleprom
  namespace: istio-system
spec:
  actions:
  - handler: doublehandler
    instances: [ doublerequestcount ]

在prometheus graph界面搜索istio_double_request_count,结果如下:

日志功能,使用资源instance,handler,rule创建,具体内容如下:

# Configuration for logentry instances
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
  name: newlog
  namespace: istio-system
spec:
  compiledTemplate: logentry
  params:
    severity: '"warning"'
    timestamp: request.time
    variables:
      source: source.labels["app"] | source.workload.name | "unknown"
      user: source.user | "unknown"
      destination: destination.labels["app"] | destination.workload.name | "unknown"
      responseCode: response.code | 0
      responseSize: response.size | 0
      latency: response.duration | "0ms"
    monitored_resource_type: '"UNSPECIFIED"'
---
# Configuration for a stdio handler
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
  name: newloghandler
  namespace: istio-system
spec:
  compiledAdapter: stdio
  params:
    severity_levels:
      warning: 1 # Params.Level.WARNING
    outputAsJson: true
---
# Rule to send logentry instances to a stdio handler
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: newlogstdio
  namespace: istio-system
spec:
  match: "true" # match for all requests
  actions:
   - handler: newloghandler
     instances:
     - newlog

访问productpage,可以看到有对应的日志生成,操作如下:

kubectl logs -n istio-system -l istio-mixer-type=telemetry -c mixer | grep "newlog" | grep -v '"destination":"telemetry"' | grep -v '"destination":"pilot"' | grep -v '"destination":"policy"' | grep -v '"destination":"unknown"'
{"level":"warn","time":"2019-12-16T16:45:53.950607Z","instance":"newlog.instance.istio-system","destination":"ratings","latency":"1.494269ms","responseCode":200,"responseSize":48,"source":"reviews","user":"unknown"}

分布式跟踪,使用jaeger进行trace, 默认采样率为1%。至少需要发送100个请求,才能看到一个跟踪,访问productpage操作:

for i in `seq 1 100`; do curl -s -o /dev/null http://$GATEWAY_URL/productpage; done

可以在jaeger dashboard看到对应跟踪信息,如下图:

DevOps功能实现解析

概述

过去传统的开发模式是开发团队研发了产品,后期的部署运维交给单独的运维团队负责。这种开发模式经常会导致一些混乱的问题,比如,前期开发时由于缺乏后面测试和部署时的及时反馈,一些小问题没有及时发现,导致后面错误累积,甚至积重难返,需要返工重做;也有可能前期开发时没有出现任何问题,但是到后面部署运维时一些基础环境变了,导致很多冲突产生,运维或开发团队又需要在短时间内解决该问题,耗时耗力,甚至可能拖延产品的上线日期。这种传统的开发模式虽然有分工明确,各司其职的优点,但是正因为如此,开发、测试和运维团队之间严重脱节,缺乏密切的合作,很多前期没有发现的小问题会在后期部署运维时集中爆发,大大提高了开发的成本以及延长了产品的迭代周期。针对现代软件越来越复杂,需求变化越来越快的趋势,人们提出了DevOps(Development&Operations)开发模式,它不是一种工具集,而是一套方法论,主张开发、测试和运维团队之间进行沟通、协作、集成和自动化,以综合协作的工作方式改善整个团队在交付软件过程中的速度和质量。

如果说DevOps是一种开发理念,那么CI/CD(持续集成、持续部署)管道就是其中的一种实践方式,它代表着发布流程自身的一个循环,从编写代码、构建镜像、测试代码、部署代码到后面生产环境重新测试和部署等,这是一个持续的过程,反复的过程。它使开发、测试和运维团队等一开始就以综合协作的方式绑定在一起,解决了前期开发得不到及时的测试、部署反馈以及运维在后期才开始介入等问题,避免了错误累积以及能够快速响应新的需求变化。常用的CI/CD工具有Jenkins、Drone和GitLab CI等等,由于系统平台使用的是Jenkins,我们就以Jenkins为例简单介绍下其相关概念。Jenkins是一个用java语言编写的开源工具,其CI/CD功能是通过一个叫pipeline的插件完成的。顾名思义,pipeline就是一套运行在Jenkins上的流水线框架,类似于工厂中的流水线作业。它将代码编译、脚本运行、镜像构建、测试、部署等功能集成在一起,为了清楚地划分不同的功能逻辑,一个pipeline被划分成了若干个Stage(称之为阶段),每个Stage代表一组相关的操作,比如:“build”,“Test”,“Deploy”等。其中每个Stage内又划分了多个Step(称之为步骤),它是最基本的操作单元,比如:创建目录、构建镜像、部署应用等等,由各类Jenkins插件提供具体功能。pipeline示意图如下所示:

魔方云DevOps实现

简单地说,魔方云是基于Kubernetes管理多种云的云平台管理系统,内部功能的设计基本上均是通过Kubernetes(以下简称k8s)提供的自定义controller功能来实现的,其基本逻辑就是根据业务需要抽象出多个CRD(Custom Resource Definition,自定义资源对象),并编写对应的controller来实现业务逻辑。

为了实现CI/CD功能,我们抽象出了多个CRD,跟pipeline相关的3个CRD:

  • pipeline:记录pipeline运行时状态、仓库的认证信息、钩子触发配置以及项目代码地址等信息。
  • pipelineExecution:每次pipeline运行时产生的执行实例(以下简称执行实例),运行完记录结果信息;
  • pipelineSetting:整个项目下pipeline运行的设置信息,比如内存、CPU的限制,最大流水线并行运行个数等等。

跟源代码仓库(以下以gitlab为例)相关的3个CRD:

  • sourceCodeProviderConfig:记录代码仓库的application id 和secret授权信息;
  • sourceCodeCredential:记录代码仓库的oauth认证信息;
  • sourceCodeRepository:记录代码仓库的每个具体的项目信息。

除了抽象出对应的CRD外,我们还需要编写对应的controller代码实现对应的业务逻辑,比如当pipeline运行时,我们需要产生pipeline执行实例,并实时同步其运行的状态信息等等。文章下面会详细介绍。

pipeline功能步骤有很多种类型,包括运行脚本、构建发布镜像、发布应用模板、部署YAML、部署应用等等。为了提供这些功能,我们采用Jenkins作为底层的CI/CD工具,docker registry 作为镜像仓库中心,minio作为日志存储中心等等。这些服务是运行在pipeline所在项目的命名空间下。综上,我们设计的CI/CD系统功能的实现逻辑如图所示:

如上,当触发pipeline执行逻辑时,会产生一个pipelineExecution crd,以记录本次运行pipeline的状态信息。当goroutine(syncState)发现有新的执行实例产生时,就会通过Jenkins引擎接口启动Jenkins server端流水线作业的运行,Jenkins server 端收到信息后会启动单独的一个Jenkins slave pod进行流水线作业的响应。同时,goroutine(syncState)会不断地通过引擎接口轮询pipeline执行实例的运行情况进而更新 pipelineExecution crd的状态,比如,运行成功或失败等等。当pipeline执行实例发生状态变化时,就会触发其对应的controller业务逻辑,进而通过Jenkins引擎接口与Jenkins server 通信进行不同的操作,比如,暂停流水线的运行,运行完清除不需要的资源等等。当流水线作业发生状态变化时,又会通过goroutine(syncState)更改pipeline执行实例的状态,进而又触发对应的controller业务代码进行不同的业务逻辑处理,往复循环,直到流水线运行结束。这就是整个pipeline执行时的一个逻辑流程。我们把整个pipeline模块的实现划分成了3个部分:

  • crd:即上述提到的6个crd类型;
  • 基础接口定义:即与Jenkins server通信的客户端,与代码仓库交互的客户端等等基础模块;
  • controller:实现逻辑功能的业务代码。

CRD

我们首先介绍下3个与源代码仓库相关的crd。当我们首次配置pipeline时会进行代码仓库授权设置,填写的gitlab Application Id和secret等信息会被存入到sourceCodeProviderConfig这个crd中,下面摘取了一些主要的字段信息并添加了详细注释,敏感信息使用了‘*’代替。

apiVersion: project.cubepaas.com/v3
metadata:
  name: gitlab
  namespace: p-qqxs7
clientId: 89d840b****** // Application Id
clientSecret: a69657b****** // secret
enabled: true
hostname: gitlab.******.cn // 代码仓库的地址
projectName: c-llqwv:p-qqxs7
redirectUrl: https://******/verify-auth
type: gitlabPipelineConfig // 仓库类型

当配置完授权信息后,平台就会与源代码仓库进行交互,获取到仓库的oauth认证信息并填充到sourceCodeCredential crd中,同时会获取该仓库下所有的项目代码的信息并填充到sourceCodeRepository crd中。如下所示:

apiVersion: project.cubepaas.com/v3
metadata:
  name: p-qqxs7-gitlab-******
spec:
  accessToken: 65b4e90****** // 访问token
  displayName: ****** // 代码仓库的显示昵称
  gitLoginName: oauth2
  loginName: ****** // 代码仓库的登入用户名
  projectName: c-llqwv:p-qqxs7
  sourceCodeType: gitlab // 代码仓库类型, gitlab github ...
apiVersion: project.cubepaas.com/v3
kind: SourceCodeRepository
metadata:
  labels:
    cubepaas.com/creator: linkcloud
  name: 89c129ff-f572-489e-98f7-3f111b3056f7
  namespace: user-lwckv
spec:
  defaultBranch: master // 默认代码分支
  projectName: c-llqwv:p-qqxs7
  sourceCodeCredentialName: user-lwckv:p-qqxs7-gitlab-****** // 指向上述 sourceCodeCredential crd
  sourceCodeType: gitlab
  url: https://gitlab.netbank.cn/******/testpipeline.git // 项目的url地址

这样就将仓库的oauth认证信息以及仓库的项目信息保存到了2个不同的crd中。

下面详细看下与pipeline相关的3个crd结构信息。

sh-4.4# kubectl get crds | grep pipeline
pipelineexecutions.project.cubepaas.com                            2019-12-18T10:32:10Z
pipelines.project.cubepaas.com                                     2019-12-18T10:32:11Z
pipelinesettings.project.cubepaas.com                              2019-12-18T10:32:10Z

pipelinesetting crd 保存着整个项目下所有的pipeline的运行设置信息,比如CPU、内存资源限额,最多可同时运行多少个pipeline等等,不同功能的配置信息保存在多个crd下。

p-qqxs7     executor-cpu-limit          19d // cpu限制配置
p-qqxs7     executor-cpu-request        19d // cpu预留配置
p-qqxs7     executor-memory-limit       19d // 内存限制配置
p-qqxs7     executor-memory-request     19d // 内存预留配置
p-qqxs7     executor-quota              19d // 最多可同时运行多少个pipeline
p-qqxs7     registry-signing-duration   19d // 用于设置docker镜像仓库证书的有效时长

// 比如,看下executor-quota详细信息
apiVersion: project.cubepaas.com/v3
metadata:
  name: executor-quota
  namespace: p-qqxs7
default: "2" // 默认最多可同时运行2个pipeline
projectName: c-llqwv:p-qqxs7
value: "3" // 自定义设置,最多可同时运行3个pipeline,没有值会取上面默认值

下面是pipeline crd 的详细字段信息,主要是保存了上次执行pipeline时的结果信息、仓库的认证信息、钩子信息以及项目代码地址信息等。

apiVersion: project.cubepaas.com/v3
kind: Pipeline
metadata:
  name: p-2qz4b
  namespace: p-qqxs7
spec:
  projectName: c-llqwv:p-qqxs7
  repositoryUrl: https://gitlab.******.cn/******/testpipeline.git // 项目代码地址
  sourceCodeCredentialName: user-lwckv:p-qqxs7-gitlab-****** // 指向对应的用户认证信息(sourceCodeCredential crd,见下面 status ---> sourceCodeCredential 字段)
  triggerWebhookPush: true // 当push代码时触发该流水线执行
status:
  lastExecutionId: p-qqxs7:p-2qz4b-1 // 最新一次运行的执行实例对应的id
  lastRunState: Success // 最新一次运行的最后结果
  nextRun: 2 // 下次运行时执行实例对应的序号
  pipelineState: active // 该流水线处于激活状态
  sourceCodeCredential: // 上述已介绍,此处不再赘述
    apiVersion: project.cubepaas.com/v3
    kind: SourceCodeCredential
    metadata:
      name: p-qqxs7-gitlab-******
      namespace: user-lwckv
    spec:
      displayName: ******
      gitLoginName: oauth2
      loginName: ******
      projectName: c-llqwv:p-qqxs7
      sourceCodeType: gitlab
      userName: user-lwckv
    status: {}
  token: a8c70ff2-a87a-4ef5-b677-73fc6c65fe69 // 访问token
  webhookId: "241" // 通过gitlab webhook触发流水线执行逻辑

pipelineExecution crd 结构的字段信息:

apiVersion: project.cubepaas.com/v3
kind: PipelineExecution
metadata:
  name: p-2qz4b-1 // 执行实例的命名规则是:pipeline crd 的名字加序号,运行一次pipeline,序号加1
  namespace: p-qqxs7
spec:
  branch: master // 代码仓库分支
  commit: 6f67ae4****** // git commit id
  event: push // push代码触发流水线执行
  message: Update .cubepaas-devops.yml file
  pipelineConfig: // 以下是pipeline具体的stage和step的配置信息,每次运行时从代码仓库的配置文件(.cubepaas-devops.yml)拉取下来填充该结构
    notification: {}
    stages:
    - name: Clone // 克隆代码
      steps:
      - sourceCodeConfig: {}
    - name: one // 构建发布镜像
      steps:
      - publishImageConfig:
          buildContext: .
          dockerfilePath: ./Dockerfile
          registry: 127.0.0.1:34844
          tag: test:001
    - name: two // 运行脚本
      steps:
      - runScriptConfig:
          image: golang:latest
          shellScript: go env
    timeout: 60 // 超时设置
  pipelineName: p-qqxs7:p-2qz4b
  projectName: c-llqwv:p-qqxs7
  ref: refs/heads/master // 拉取的是master分支
  repositoryUrl: https://gitlab.netbank.cn/******/testpipeline.git // 项目代码地址
  run: 1 // 此次运行序号
  triggeredBy: webhook // 如何触发
status: // 以下记录着pipeline每个stage和step的运行结果信息
  executionState: Success
  stages:
  - ended: 2020-01-07T08:06:36Z
    state: Success
    steps:
    - ended: 2020-01-07T08:06:36Z
      state: Success
  ...
  ...

以上就是实现CI/CD系统功能所抽象出的6个主要的CRD。

基础接口定义

pipeline 涉及到的功能模块很多,从源代码仓库拉取代码、设置钩子操作、仓库触发流水线操作以及与 Jenkins server 端通信的 client 等等。为了方便以后的扩展,关键数据结构都被定义成了接口,如下所示:

这是与Jenkins server端通信的client接口定义。

type PipelineEngine interface {
    // 检查jenkins运行条件是否满足
    PreCheck(execution *v3.PipelineExecution) (bool, error)
    // 运行pipeline
    RunPipelineExecution(execution *v3.PipelineExecution) error
    // 重新运行pipeline
    RerunExecution(execution *v3.PipelineExecution) error
    // 停止pipeline的运行
    StopExecution(execution *v3.PipelineExecution) error
    // 获取详细的日志信息
    GetStepLog(execution *v3.PipelineExecution, stage int, step int) (string, error)
    // 同步运行状态
    SyncExecution(execution *v3.PipelineExecution) (bool, error)
}

这是代码仓库触发流水线操作的接口定义:

// 当代码仓库触发流水线操作时,由该接口响应请求并启动流水线执行
type Driver interface {
    Execute(req *http.Request) (int, error)
}

以 gitlab 为例

func (g GitlabDriver) Execute(req *http.Request) (int, error) {
    ...

    event := req.Header.Get(GitlabWebhookHeader)
    if event != gitlabPushEvent && event != gitlabMREvent && event != gitlabTagEvent {
        return http.StatusUnprocessableEntity, fmt.Errorf("not trigger for event:%s", event)
    }

    ...

    // 根据不同的触发条件获取请求参数中对应的信息
    info := &model.BuildInfo{}
    if event == gitlabPushEvent {
        info, err = gitlabParsePushPayload(body)
        ...
    } else if event == gitlabMREvent {
        info, err = gitlabParseMergeRequestPayload(body)
        ...
    } else if event == gitlabTagEvent {
        info, err = gitlabParseTagPayload(body)
        ...
    }

    // 验证并创建pipeline执行实例,开始流水线的运行
    return validateAndGeneratePipelineExecution(g.PipelineExecutions, g.SourceCodeCredentials, g.SourceCodeCredentialLister, info, pipeline)
}

除此之外,还有与代码仓库进行交互的客户端(获取代码分支信息、克隆代码、拉取pipeline配置文件等)、与minio server通信的客户端(保存、获取日志信息)等结构,不再赘述。

controller

当触发流水线执行逻辑时,会通过pipeline crd和代码仓库中的pipeline配置文件产生一个pipelineExecution crd,这时会触发pipelineExecution对应的controller运行业务逻辑。如下所示:

func (l *Lifecycle) Create(obj *v3.PipelineExecution) (runtime.Object, error) {
    return l.Sync(obj)
}

func (l *Lifecycle) Updated(obj *v3.PipelineExecution) (runtime.Object, error) {
    return l.Sync(obj)
}

当创建或更新pipelineExecution时会调用Sync同步函数进行业务处理。下面只摘取重要的代码逻辑,如下所示:

func (l *Lifecycle) Sync(obj *v3.PipelineExecution) (runtime.Object, error) {

    ...

    // 如果 pipeline 执行实例被暂停,则会停止流水线作业
    if obj.Status.ExecutionState == utils.StateAborted {
        if err := l.doStop(obj); err != nil {
            return obj, err
        }
    }

    // 如果 pipeline 执行实例运行完毕,则会清理流水线作业的一些资源
    // 比如,产生的Jenkins slave pod
    if obj.Labels != nil && obj.Labels[utils.PipelineFinishLabel] == "true" {
        return l.doFinish(obj)
    }

    // 如果 pipeline 执行实例正在运行中,则直接返回,无操作
    if v3.PipelineExecutionConditionInitialized.GetStatus(obj) != "" {
        return obj, nil
    }

    // 判断流水线作业是否超出资源限额
    exceed, err := l.exceedQuota(obj)
    if err != nil {
        return obj, err
    }
    // 如果超出资源限额,则会设置当前 pipeline 执行实例为阻塞状态
    if exceed {
        obj.Status.ExecutionState = utils.StateQueueing
        obj.Labels[utils.PipelineFinishLabel] = ""

        if err := l.newExecutionUpdateLastRunState(obj); err != nil {
            return obj, err
        }

        return obj, nil
    } else if obj.Status.ExecutionState == utils.StateQueueing {
        obj.Status.ExecutionState = utils.StateWaiting
    }

    // 更新 pipeline 执行实例的状态: 比如运行序号+1
    if err := l.newExecutionUpdateLastRunState(obj); err != nil {
        return obj, err
    }
    v3.PipelineExecutionConditionInitialized.CreateUnknownIfNotExists(obj)
    obj.Labels[utils.PipelineFinishLabel] = "false"

    // 在数据面部署pipeline功能所需资源
    if err := l.deploy(obj.Spec.ProjectName); err != nil {
        obj.Labels[utils.PipelineFinishLabel] = "true"
        obj.Status.ExecutionState = utils.StateFailed
        v3.PipelineExecutionConditionInitialized.False(obj)
        v3.PipelineExecutionConditionInitialized.ReasonAndMessageFromError(obj, err)
    }

    // 将 configMap 存储的docker镜像仓库端口信息同步到pipeline执行实例中去.
    if err := l.markLocalRegistryPort(obj); err != nil {
        return obj, err
    }

    return obj, nil
}

其中,deploy函数的逻辑就是第一次运行时通过判断数据面中是否存在pipeline的命名空间,如果存在就代表基础资源已经配置完成,直接走reconcileRb函数,该函数的逻辑见下面;如果不存在,就会在数据面中初始化必要的基础资源,比如:pipeline命名空间, Jenkins docker minio服务, 配置configMap, secret等等。

func (l *Lifecycle) deploy(projectName string) error {
    clusterID, projectID := ref.Parse(projectName)
    ns := getPipelineNamespace(clusterID, projectID)
    // 如果该pipeline的namespace已经有了,说明下面的资源部署已经完成了,则直接走reconcileRb流程
    // 否则走下面的资源部署流程
    if _, err := l.namespaceLister.Get("", ns.Name); err == nil {
        return l.reconcileRb(projectName)
    } else if !apierrors.IsNotFound(err) {
        return err
    }

    // 创建pipeline对应的命名空间,如p-qqxs7-pipeline
    if _, err := l.namespaces.Create(ns); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the pipeline namespace")
    }

    ...

    // 随机产生一个token,用于配置下面的secret
    token, err := randomtoken.Generate()

    nsName := utils.GetPipelineCommonName(projectName)
    ns = getCommonPipelineNamespace()
    // 创建用于部署docker镜像仓库的代理服务的命名空间
    if _, err := l.namespaces.Create(ns); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the cattle-pipeline namespace")
    }

    // 在 pipeline namespace 内创建secret : pipeline-secret
    secret := getPipelineSecret(nsName, token)
    l.secrets.Create(secret);

    ...

    // 获取管理面项目的系统用户token
    apikey, err := l.systemAccountManager.GetOrCreateProjectSystemToken(projectID)

    ...

    // 在 pipeline namespace 内创建secret: pipeline-api-key,用于数据面与管理面通信的凭证
    secret = GetAPIKeySecret(nsName, apikey)
    l.secrets.Create(secret);

    // 调谐 docker 镜像仓库的证书配置(在控制面中)
    if err := l.reconcileRegistryCASecret(clusterID); err != nil {
        return err
    }

    // 将控制面中的 docker 镜像仓库的证书配置同步到数据面中
    if err := l.reconcileRegistryCrtSecret(clusterID, projectID); err != nil {
        return err
    }

    // 在 pipeline namespace 内创建 serviceAccount : jenkins
    sa := getServiceAccount(nsName)
    if _, err := l.serviceAccounts.Create(sa); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating a pipeline service account")
    }

    ...

    // 在 pipeline namespace 内创建 service: jenkins
    jenkinsService := getJenkinsService(nsName)
    if _, err := l.services.Create(jenkinsService); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the jenkins service")
    }

    // 在 pipeline namespace 内创建 deployment: jenkins
    jenkinsDeployment := GetJenkinsDeployment(nsName)
    if _, err := l.deployments.Create(jenkinsDeployment); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the jenkins deployment")
    }

    // 在 pipeline namespace 内创建 service: docker-registry
    registryService := getRegistryService(nsName)
    if _, err := l.services.Create(registryService); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the registry service")
    }

    // 在 pipeline namespace 内创建 deployment: docker-registry
    registryDeployment := GetRegistryDeployment(nsName)
    if _, err := l.deployments.Create(registryDeployment); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the registry deployment")
    }

    // 在 pipeline namespace 内创建 service: minio
    minioService := getMinioService(nsName)
    if _, err := l.services.Create(minioService); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the minio service")
    }

    // 在 pipeline namespace 内创建 deployment: minio
    minioDeployment := GetMinioDeployment(nsName)
    if _, err := l.deployments.Create(minioDeployment); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the minio deployment")
    }

    // 调谐 configMap: proxy-mappings,用于配置docker镜像仓库代理服务的端口信息
    if err := l.reconcileProxyConfigMap(projectID); err != nil {
        return err
    }

    // 创建secret: devops-docker-registry,存储访问docker仓库的认证信息
    if err := l.reconcileRegistryCredential(projectName, token); err != nil {
        return err
    }

    // 创建 daemonset: registry-proxy,每个节点部署一套docker镜像仓库的nginx代理服务
    // 可以在任意一个节点上通过不同的端口即可访问到不同的docker镜像仓库
    nginxDaemonset := getProxyDaemonset()
    if _, err := l.daemonsets.Create(nginxDaemonset); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the nginx proxy")
    }

    return l.reconcileRb(projectName)
}

reconcileRb函数的功能就是遍历所有namespace, 对其调谐rolebindings, 目的是让 pipeline serviceAccount(jenkins) 对该project下的所有namespace具有所需要的操作权限,这样Jenkins server才能够在数据面中正常提供CI/CD基础服务。

func (l *Lifecycle) reconcileRb(projectName string) error {

    ...

    var namespacesInProject []*corev1.Namespace
    for _, namespace := range namespaces {
        parts := strings.Split(namespace.Annotations[projectIDLabel], ":")
        if len(parts) == 2 && parts[1] == projectID {
            // 过滤出属于该project下的所有namespace
            namespacesInProject = append(namespacesInProject, namespace)
        } else {
            // 对非该project下的namespace, 清除有关该 pipeline 的 rolebinding
            if err := l.roleBindings.DeleteNamespaced(namespace.Name, commonName, &metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
                return err
            }
        }
    }

    for _, namespace := range namespacesInProject {
        // 对属于该project下的namespace, 创建 rolebinding: 对 jenkins serviceAccount 绑定角色
        // 即赋予 jenkins serviceAccount 对该project下的所有namespace所需要的操作权限
        rb := getRoleBindings(namespace.Name, commonName)
        if _, err := l.roleBindings.Create(rb); err != nil && !apierrors.IsAlreadyExists(err) {
            return errors.Wrapf(err, "Error create role binding")
        }
    }

    // 赋予 jenkins serviceAccount 在 cluster 内创建和修改 namespace 的权限
    // 当部署应用时可以指定创建新的命名空间
    clusterRbs := []string{roleCreateNs, projectID + roleEditNsSuffix}
    for _, crbName := range clusterRbs {
        crb := getClusterRoleBindings(commonName, crbName)
        if _, err := l.clusterRoleBindings.Create(crb); err != nil && !apierrors.IsAlreadyExists(err) {
            return errors.Wrapf(err, "Error create cluster role binding")
        }
    }

    return nil
}

我们通过kubernator ui查看下数据面k8s中部署的有关pipeline的资源,如图所示:

pipeline-secret用于配置Jenkins、docker-registry、minio服务的auth信息,pipeline-api-key用于数据面与管理面通信的凭证,registry-crt用于配置docker镜像仓库的证书。

goroutine(syncState)的代码逻辑比较简单,当产生新的pipeline执行实例时就会启动Jenkins server端流水线作业的运行并实时同步其运行状态到pipeline执行实例中。代码逻辑如下:

func (s *ExecutionStateSyncer) syncState() {
    set := labels.Set(map[string]string{utils.PipelineFinishLabel: "false"})
    allExecutions, err := s.pipelineExecutionLister.List("", set.AsSelector())
    executions := []*v3.PipelineExecution{}
    // 遍历该cluster下的 pipeline 执行实例
    for _, e := range allExecutions {
        if controller.ObjectInCluster(s.clusterName, e) {
            executions = append(executions, e)
        }
    }

    for _, execution := range executions {
        if v3.PipelineExecutionConditionInitialized.IsUnknown(execution) {
            // 检查数据面k8s中 Jenkins pod 是否正常,正常则运行该 pipeline job
            s.checkAndRun(execution)
        } else if v3.PipelineExecutionConditionInitialized.IsTrue(execution) {
            e := execution.DeepCopy()
            // 如果已经启动了,则同步运行状态
            updated, err := s.pipelineEngine.SyncExecution(e)
            if updated {
                // 更新最新的状态到 pipelineExecution crd 中
                s.updateExecutionAndLastRunState(e);
            }
        } else {
            // 更新最新的状态到 pipelineExecution crd 中
            s.updateExecutionAndLastRunState(execution);
        }
    }

    logrus.Debugf("Sync pipeline execution state complete")
}

除此之外,系统为了内部的docker镜像仓库的安全性,会启动一个goroutine每隔一段时间就更新下docker镜像仓库的证书。同时为了方便访问docker镜像仓库,系统在数据面k8s中启动了一个daemonset类型的代理服务,内部采用的是nginx代理,通过不同的端口指向不同的docker镜像仓库,这样就可以在任意一个节点通过不同的端口方便地访问到不同的docker镜像仓库。通过kubectl在数据面中获取到的configMap(proxy-mappings)信息如下所示:

apiVersion: v1
items:
- apiVersion: v1
  data:
    mappings.yaml: |
      mappings: '{"p-ljzgf":"34380","p-qqxs7":"34844"}'
  kind: ConfigMap
  metadata:
    labels:
      cubepaas.com/creator: linkcloud
    name: proxy-mappings
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""

我们可以看到现在系统上有两个pipeline的docke镜像仓库端口信息。我们可以通过不同的端口访问到对应pipeline的docker镜像仓库。如:

michael@work1:~$ docker login 127.0.0.1:34844
Username: admin
Password: 
WARNING! Your password will be stored unencrypted in /home/michael/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

魔方云DevOps使用

通常,流水线第一步是从代码仓库克隆代码,然后才是编译、测试和部署等一系列功能的设置,因此我们首先需要授权平台能够访问代码仓库。我们以gitlab仓库为例,将配置好的Application Id、secret和仓库地址填入到对应的文本框中,点击授权,这样我们就完成了仓库的授权配置。如图所示:

配置完成后,我们就可以激活pipeline功能了,我们简单地设置pipeline配置文件( .cubepaas-devops.yml)如下:

stages:
- name: one
  steps:
  - publishImageConfig:
      dockerfilePath: ./Dockerfile
      buildContext: .
      tag: test:001
      registry: 127.0.0.1:34844
- name: two
  steps:
  - runScriptConfig:
      image: golang:latest
      shellScript: go env
timeout: 60
notification: {}

如上,简单地配置了两个阶段,一个是构建发布镜像,一个是运行脚本,你可以按照自己项目的发布流程配置好pipeline,最后点击完成,我们就可以看到该pipeline自动运行了。pipeline的配置文件会推送到代码仓库中,这样就可以实现pipeline历史版本的回溯,支持从代码库直接读取pipeline配置文件,从而实现了Pipeline as Code的理念。点击查看日志,你可以看到pipeline各个阶段运行的详细日志信息。如图所示。

【注意】首次运行pipeline时系统会从网络下载Jenkins、docker、minio以及其他pipeline-tools镜像,如果长时间未运行,请查看网络是否有问题。