Rainbond v5.2.0-beta1发布,众多变化请查看详情

好雨云阅读(459)评论(0)

下载安装

安装文档参考: https://v5.2-doc.rainbond.com/docs/quick-start/rainbond_install/

版本变更

安装与运维

  • Rainbond系统安装和运维管理重构为Operator模式,运行于Kubernetes集群内部。

  • 解除对Kubernetes的强依赖关系,Rainbond不再维护Kubernetes集群安装脚本,推荐使用 easzup

  • Rainbond-Operator安装采用Helm包管理工具安装。

  • Rainbond系统安装提供UI界面,实时把控安装进度,后续版本UI提供系统运维、升级等功能。

  • 安装提供多种参数可选配置,包括镜像仓库、数据库、ETCD集群等关键配置。

  • 系统组件生命周期由Kubernetes和Rainbond-Operator共同维护和管理。

一句话,你有Kubernetes集群(1.13及以上)就可以试试Rainbond带来的不一样的体验。

应用存储

  • Rainbond 组件存储抽象支持存储类型支持通过Kubernetes StorageClass 扩展,通过增加集群中的StorageClass即可扩充Rainbond支持的存储类型,目前测试接入的存储类型包括阿里云盘、Ceph块设备等

  • 组件存储模型增加容量、挂载状态属性。

  • 应用分享安装、跨集群迁移等用例中基于简要算法选择合适的存储类型,后续版本中将基于存储特性指标更加智能选择。

应用网关

  • 重构TCP/UDP类访问策略的负载均衡机制,Upstream的更新机制由过去生成Nginx配置文件并Reload修改为Lua控制的动态更新,无需触发Reload。

  • HTTP访问策略默认支持X-Forwarded-Proto X-Scheme等参数 #591

  • 新增对Rainbond数据中心API,控制台UI等外网控制入口的代理,集群所有请求统一由网关组件进入。

源码构建

  • 重构源代码构建任务运行模式,由管理节点运行变更为Kubernetes Job任务,在集群计算节点运行,进而支持高并发构建任务。

  • Golang语言Buildingpack升级,增加对Go mod模式依赖包管理的支持,支持Go 1.12 1.13 #613

  • Java相关语言Buildingpack升级,支持JDK 11 12 13, Maven 3.5.4 3.6.2

  • PHP语言Buildingpack升级,支持php 7.2.26 7.3.13 版本

  • NodeJS/NodeJS前端 两种语言类型支持UI设置构建参数

其他变更

  • 所有系统组件对ETCD的通信默认支持TLS认证

  • grctl命令行变更安装方式,新增grctl gateway grctl envoy 等功能辅助运维。

  • 组件支持使用privileged模式运行 #333

    移除功能

    • 移除命令行扩充集群节点功能,改由easzup 扩充Kubernetes集群后Rainbond节点自动扩充。

    • 移除“全局共享存储”存储类型的自动化安装(无权限操作宿主机),改由用户使用简化命令行工具安装。

    • rainbond-ansible 项目仅用于V5.1版本。

基于Jenkins打造符合DevOps能力成熟度三级标准的持续集成流水线

JFrogchina阅读(1775)评论(0)

DevOps的核心是自动化,自动化的核心是标准化。而DevOps最重要的一环节是持续交付,持续交付中建设的重点是流水线,所以如何打造标准的持续交付流水线则为DevOps建设中最重要的一环,也是评估DevOps能力的一个重要的打分点。

本文内容参照《研发运营一体化(DevOps)能力成熟度模型 第3部分:持续交付》,基于jenkins,对持续集成流水线建设的一些关键点进行技术应答,带领大家把方法论落地到具体的技术点上。

 

文中涉及到的几个名词解释:

  • 流水线:pipeline,一个应用程序从构建、部署、测试和发布这个过程实现自动化
  • 制品:构建过程的输出物,包括软件包、测试报告、应用配置文件等。
  • 制品库:存储全语言制品的仓库,提供依赖解析及文件存储能力。
  • 元数据:软件生命周期全过程数据,如需求id、代码提交信息、构建环境、静态扫描结果、测试通过率、安全扫描结果等。

 

文章中涉及到的一些技术详解:见文章《打造企业级pipeline服务的18个疑问》

 

下面,我们来看一下持续集成流水线建设中的配置管理、构建与持续集成、测试管理、部署与发布管理、环境管理、数据管理、度量与反馈的七个维度的三级标准是如何定义的,并且哪些指标需要在jenkins流水线中体现,如何使用jenkins流水线达到此标准。

 

  • 配置管理
三级标准 Jenkins流水线落地建议方案
版本控制 版本控制系统 1)将配置文件、构建和部署等自动化脚本纳入版本控制系统管理。
2)有健全的版本控制系统管理机制,包括:代码库命名规范备份与可用性保障机制权限模型专人专岗管理。
流水线内容(Jenkinsfile)需要纳入版本管理
流水线的命名需要有明确规范
流水线应明确权限,开发人员应只有可读权限,模版由专门团队编写
技术点:可使用jenkins的Share library特性,由专门团队在源码仓库中统一管理流水线,
分支管理 短周期分支分支频繁地向主干合并 非流水线内容
制品管理 1)将依赖组件纳入制品库管理
2)将所有交付制品纳入制品库管理,比如:测试报告
3)制品库读写有清晰的权限管控制度
建设统一制品库,如Artifactory。设置完整的权限。
收集构建流水线过程中的所有工具的结果数据,并将此类数据定义成元数据,与制品绑定。如需求、代码提交信息、构建环境信息、依赖信息、静态扫描信息、单元测试信息、安全扫描信息等。
技术点:可采用商用制品库、如Artifactory。也可自行开发元数据管理系统,收集构建中过程数据。
单一可信数据源 版本控制系统和制品库作为单一可信数据源,覆盖生产部署环节 建立统一制品库,在jenkinsfile中指明制品库地址,构建时不使用pom文件中的依赖解析地址,而由其他方式修改依赖解析仓库到唯一可信仓库中
技术点:使用Artifactory统一管理制品库,保证唯一可信源
变更管理 变更过程 1)所有配置项变更由变更管理系统触发
2)针对每次变更内容进行评审,并使用自动化手段
不涉及流水线、注意配置与应用分离、及配置中心管理
变更追溯 实现版本控制系统和变更管理系统的自动化关联,信息双向同步和实时可追溯 不涉及流水线
变更回滚 1)实现变更管理系统和版本控制系统的同步回滚,保证状态的一致性
2)回滚操作实现自动化
不涉及流水线,

 

  • 构建与持续集成
三级标准 Jenkins流水线落地建议方案
构建实践 构建方式 1)定义结构化构建脚本,实现模块级共享复用
2)构建脚本由专人专岗统一维护
技术点:使用Jenkins ShareLibrary实现构建模块化管理,并实现全局共享
构建环境 1)构建环境配置实现标准化
2)有独立的构建资源池
打造少量固定的标准化构建节点作为独立的构建资源池,并用k8s集群创建动态构建节点作为动态资源池。
技术点:jenkins主从架构、jenkins on k8s
构建计划 1)实现定期自动执行构建和代码提交触发构建
2)明确定义构建计划和规则,并在研发团队内共享
技术点:jenkins触发器,可实现定时构建、轮询源码构建或webhook触发构建
构建职责 构建工具和环境由专门团队维护并细分团队人员职责 jenkins主从节点或构建镜像由统一团队维护。业务部门只使用,不能修改。
持续集成 集成服务 组建专门的持续集成团队,负责优化持续集成系统和服务 统一团队构建流水线模版与持续集成环境,供开发人员选择
技术点:可以通过jenkins on k8s方式,打造多种构建环境镜像,开发人员提交构建任务时定义所需环境。
集成频率 研发人员至少每天向代码主干集成一次 不涉及流水线
集成方式 每次代码提交触发自动化构建,构建问题通自动分析精准推送相关人员处理 每次提交代码触发jenkins进行构建,并在构建过程中执行完整的静态扫描、单元测试等步骤
技术点:jenkins的触发器功能,可以设置轮训scm或git的webhook触发
反馈周期 集成问题反馈和解决可以在几个小时内完成 jenkins pipeline中要通知构建工作完成或失败状态,发邮件或webhook给运维团队和业务团队

 

  • 测试管理
三级标准 Jenkins流水线落地建议方案
测试分层策略 分层方法 1)采用代码级测试对模块的函数或类方法进行覆盖全面的单元测试;
2)系统全面的进行性能、容量、稳定性、可靠性、易用性、兼容性、安全性等非功能性测试
在流水线中进行单元测试,收集单元测试通过率作为元数据与制品绑定。
分层策略 1)测试设计以对接口/服务级测试为主,兼顾用户/业务级测试辅以少量的代码级测试
2)对非功能性测试进行全面系统的设计
在流水线中可以集成接口测试,并收集接口测试通过率作为元数据与制品绑定。
测试时机 1)测试在持续交付过程中的介入时间提前到开发的编码阶段
2)代码级测试在模块的函数或类方法开发完成后进行
提高单元测试覆盖率。
代码质量管理 质量规约 1)建立组织级代码质量规约
2)建立完整的质量规约,将安全漏洞检查、合规检查纳入规约
3)建立强制执行的质量门禁体系
4)建立规约固定更新机制
需要在jenkins流水线中增加安全扫描步骤,并对扫描测试结果设置质量关卡。
技术点:Xray作为安全扫描工具集成在流水线中、通过制品元数据作为质量门禁判断构建产物是否达标
检查方式 代码质量检查完全自动化,不需要手工干预 流水线集成sonar扫描工具,每次代码提交自动触发构建、自动化进行源码扫描,并将代买坏味道数量、代码重复率等结果数据以元数据方式回写制品库。
技术点:sonarqube代码静态扫描
反馈处理 根据代码质量检查结果反馈及时处理,根据质量规约维持一定的技术债 代码静态扫描结果与制品绑定,回写到制品库。通过制品携带的元数据是否通过质量门禁,来判断制品质量。
自动化测试 自动化设计 1)对接口/服务级测试进行自动化设计
2)对代码级测试进行自动化设计
jenkins 流水线增加接口测试及服务测试
自动化开发 1)建立统一的自动化测试框架,统一管理自动化测试用例
2)自动化测试脚本开发采用数据驱动、关键字驱动等方法;
不涉及流水线
自动化执行 1)对接口/服务级与代码级测试采用自动化测试
2)自动化测试由流水线自动化触发
在流水线中进行所需测试
自动化分析 对自动化测试结果具备较强的自动判断能力,误报少 流水线中收集各项测试结果,作为元数据与交付物关联,保障每个制品都能获取到完整的测试结果。

 

  • 部署与发布管理
三级标准 Jenkins流水线落地建议方案
部署与发布模式 部署方式 部署和发布实现全自动化 部署过程作为流水线的必要步骤
技术点:对接如saltstack、ansible等工具在流水线中部署
部署过程 1)使用相同的过程和工具完成所有环境部署
2)一次部署过程中使用相同的构建产物
为确保发布内容为测试过的内容,要实现一次构建多次部署。通过元数据与仓库名标识制品成熟度。流水线中要将制品在不同成熟度仓库移动,并收集各个环境中的结果数据作为元数据存储。
技术点:应用配置分离、Artifactory元数据及promotion功能
部署策略 1)采用定期部署策略,具备按天进行部署的能力
2)应用和环境整体作为部署的最小单位
3)应用和配置进行分离
不涉及流水线
部署质量 1)部署失败率低
2)部署活动集成自动化测试功能,并以测试结果为部署前置条件
3)每次部署活动提供变更范围报告和测试报告
部署后会在流水线中进行简单验证,收集验证结果数据。测试结果收集到元数据中,部署时验证元数据,判断是否通过质量门禁,来实现部署。               技术点:Artifactory元数据

 

持续部署流水线 协作模式 通过定义完整的软件交付过程和清晰的交付规范,保证团队之间交付的有序 标准化工具链及持续集成流水线,收集个阶段结果数据作为元数据,用元数据标识制品的质量标准,供各个团队间进行使用。
流水线过程 软件交付过程中的各个环节建立自动化能力以提升处理效率 不涉及流水线
过程可视化 1)交付过程在团队内部可见,信息在团队间共享
2)交付状态可追溯
流水线中收集整个构建过程结果数据,与制品绑定,供所有团队查看。
技术点:Artifactory元数据

 

  • 环境管理
三级标准 Jenkins流水线落地建议方案
环境管理 环境类型 建立标准的研发环境 不涉及流水线
环境构建 1)环境的构建通过自服务的资源交付平台来完成
2)环境准备时间小时级
可在流水线中自动创建所需环境。
技术点:使用k8s的helm自动拉起整套环境,helm是最佳的实现方式
环境依赖于配置管理 以应用为中心,有服务级依赖的配置管理能力,比如:依赖的关联服务,数据库服务、缓存服务、关联应用服务等等 不涉及流水线

 

  • 数据管理
三级标准 Jenkins流水线落地建议方案
测试数据管理 数据来源 导出部分生产环境数据并清洗敏感信息后形成基准的测试数据集 不涉及流水线
数据覆盖 建立体系化测试数据,进行数据依赖管理,覆盖全部测试分层策略要求的测试类型 不涉及流水线
数据独立性 1)每个测试用例拥有专属的测试数据有明确的测试初始状态
2)测试用例的执行不依赖其他测试用例执行所产生的数据
不涉及流水线
数据变更管理 变更过程 将数据变更纳入持续部署流水线,经人工确认后自动完成 流水线与审批系统集成。
兼容回滚 每次数据变更同时提供明确的回滚机制,并实现进行变更测试,如:提供升级和回滚两个自动化脚本 不涉及流水线
数据监控 针对不同环境和危险程度对数据变更建立分级监控机制 不涉及流水线

 

  • 度量与反馈
三级标准 Jenkins流水线落地建议方案
度量指标 度量指标定义 建立跨组织度量指标,进行跨领域综合维度的度量 不涉及流水线
度量指标类型 度量指标覆盖过程指标,客观反映组织研发现状 流水线中需要收集元数据,作为后续度量指标
度量数据管理 持续收集度量数据历史度量数据有明确的管理规则 流水线中需要收集元数据,作为后续度量指标
度量指标更新 1)度量指标可以按照需求定期更新
2)度量指标的优先级在团队内部达成一致
不涉及流水线
度量驱动改进 内容和生成方式 度量报告进行分类分级并按需生成内容 流水线中需要收集元数据,作为后续度量指标。对元数据进行二次清晰,生成报告
数据时效性 通过可视化看板实时展示数据 看板需要展示流水线状态,如构建时间、通过率、故障率等
覆盖范围 全部团队成员均可查看报告 不涉及流水线
反馈改进 度量反馈问题纳入研发迭代的待办事项列表,作为持续改进的一部分 不涉及流水线

 

通过上述数据及分析,可以看出,打造出一个标准的流水线服务可以匹配到60%的三级标准。那么我们可以在整个DevOps的建设中投入较大的力量来打造流水线。一套标准的流水线服务和稳定的工具链将会成为DevOps建设的一个基石,并且成为贯穿你的整个建设周期中。

 

 

学习更多技术知识可以关注我们的在线课堂

关注微信公众号:JFrog杰蛙DevOps, 获取课程通知

科技战“疫”,Rainbond助力咸阳市疫情管控应用快速交付,支撑大用户并发

好雨云阅读(574)评论(0)

2020年的春节与往年不同,突如其来的新型冠状病毒肺炎自华中始,并向全国各地蔓延,冲淡了原有年味儿的喜庆热闹。从23号武汉封城,到多地纷纷效仿,最后到一级响应预警,全国各地都是严阵以待。面对严峻疫情防控形势,社会各界齐心协力抗击疫情,好雨科技也积极投入到抗击疫情的实际行动中。

咸阳市大数据管理局是咸阳市政府下属机构,负责咸阳全市信息化建设、大数据管理和信息网络运行维护等工作。2019年,咸阳大数据管理局以Rainbond为基座,建设咸阳市的智慧社会操作系统,智慧社会操作系统的主要任务是连接资源、连接应用、连接数据、连接用户,2019年底已经完成智慧社会操作系统的主体建设工作。

挑战

2月4日,由于复工返岗高峰的到来,大规模的人口流动重新启动,为遏制疫情蔓延扩散,做好外来返工人员的防控和服务工作,咸阳市大数据需要用最短的时候完成咸阳市疫情登记应用和相关管控应用的开发和上线,并在3天内完成整个咸阳市130万人信息上报和管控服务。

在这过程中面对两大个挑战:

  1. 咸阳大数据管理局的工作人员也在家办公,疫情防控应用必须在短时间开发完成,需要高效的远程协作,并支撑应用快速迭代上线和业务不间断升级。

  2. 应用要支撑130万人短时间访问要求,在某些时间点会有大量并发,需要快速完成性能优化,支撑大并发。

通过Rainbond实现远程协作、快速迭代开发和持续交付

疫情来的非常突然,导致整个业务的开发上线时间非常短暂。在几天内完成开发,上线,并且立刻接受大压力的考验,君知其难也。不得不承认,业务上线前的压力测试结果并不尽人意。但是任务就在那里,必须被完成。负责智慧社会操作系统的所有工程师都拿出了自己的看家本领,力保线上业务正常运行。

“咸阳市外来人口登记业务”是一个前后端分离的业务系统。主要包含了前端页面、后台服务、缓存、数据库、短信业务5个服务组件。

image-20200213124830983

1,通过Rainbond实现远程协作

疫情的到来,迫使很多企业的员工必须在家远程办公,如何远程协作才能在如此短暂的时间内完成业务的开发并上线,对于工程师们是一项艰巨的挑战。

通过将开发——测试——上线全流程所需的资源全部部署于Rainbond,这样,通过Rainbond提供的应用控制台,即可完成全流程远程协作。这一切只需要工程师们可以在家里通过VPN访问到Rainbond应用控制台即可。

远程协助

整个流程的简要描述:

  • 开发测试人员均可以通过vpn访问到Rainbond控制台。

  • 开发人员向部署在Rainbond内部的Gitlab管理需求并远程提交代码。

  • 通过代码部署业务应用并上线。

  • 测试人员通过部署在Rainbond内部的自动化测试工具对业务应用进行测试。

  • 测试人员提交issue到Gitlab来反馈BUG。

  • 开发人员获取反馈,更新迭代代码。

  • 通过提交信息中包含的关键字,触发应用自动构建,自动将更新上线。

  • 上线后如发现新的问题隐患,通过Rainbnond版本管理功能一键回滚到上个稳定版本。

2,快速上线与回滚

业务的开发没办法一蹴而就,任何一个业务都需要代码的迭代更新,统计业务的开发人员通过基于Webhook实现的自动构建功能,将代码快速部署到线上。

快速上线

开发人员在提交代码时,将指定的触发关键字加入commit信息,即可触发Rainbond自动构建该服务组件。Rainbond将自动完成代码拉取、构建、滚动更新上线的全流程。整个过程更加智能、更加自动化。

然而并非所有的代码更新都是正向的,一旦发现代码更新后线上业务出现了问题,那么如何快速回滚到上一个稳定版本,就变成了一个问题。在这次实战中,统计业务的开发人员利用了Rainbond自带的版本管理功能实现一键快速回滚。

回滚

3,服务组件图形化编排

当开发人员部署好所有的服务组件之后,如何让它们彼此之间能够通过依赖关系正常通信,互相调用呢?实战中非常好的一个实践是借助于Rainbond平台基于拓扑图实现的图形化服务组件编排。

抛弃以往复杂的配置方式,在web界面上简单的拖拉拽即可实现服务组件的编排。对于开发人员而言,这样的方式既感性又方便。

  • 切换到应用拓扑图界面。

  • 点击“切换到编辑模式”。

  • 从下游服务组件的六角形拖拽一条线到上游服务组件即可。

编排

通过多次的拼装,在简单的服务组件之间就会形成复杂的拓扑,组件间彼此就可以相互调用,正常运作了。

应用实时性能监控和优化,支持5000+并发

想要在3天内完成整个咸阳市130万人信息上报和管控服务,可以预见到这将会是一个高并发场景。这个场景,将对平台网关的抗并发能力、容器平台的承载能力、统计业务的处理能力、数据库性能四个方面提出较高水平的需求。如何探测整条链路的瓶颈,并加以处理以提高综合吞吐能力,就成了打赢这次“实战”的关键点。

  • 负载均衡降低网关压力

  • 支持业务实例自动故障迁移。

  • 实例数量自动伸缩。

  • 业务升级滚动更新不间断。

  • 服务组件间通信通过插件治理。

  • 性能分析插件迅速发现业务瓶颈。

1,网关节点和计算节点伸缩

网关节点集群的流量被全局负载均衡器所负载,这是业内处理并发最常用的手段之一。根据Nginx的轮询算法,将流量均匀分配到所有网关节点,大大降低了单一网关节点的压力。使其处理更高层的 Http 协议更加从容不迫。

Rainbond容器云平台的计算节点,支持分布式部署。这样的部署方式对于抗并发最大的意义在于:一旦某台计算节点由于压力过大而崩溃,Rainbond健康检测机制会在短时间内将故障节点上运行的业务容器实例进行自动迁移,始终保持业务正常运行。

2,通过应用市场快速复制应用,用户分流

项目负责人早在业务上线前,就已经预料到大并发场景。所以在规划开始阶段,就将咸阳市所有区县进行分组,最终决定部署4套相同的业务来为不同的分组提供服务,这样可以在业务处理能力这个层面进行人为的分流。大幅度降低单个统计业务系统的压力。

在这里,Rainbond内部市场功能发挥了作用,开发人员只需要手动构建部署第一套业务系统,然后发布到内部应用市场,即可快速复制出另外3个业务系统。而且一旦业务需要升级,只需要将源业务系统升级后再次发布,Rainbond内部市场提供的版本管理功能会自动识别升级,其它复制业务系统可以基于提示一键升级。

整个过程和以往人力部署的情况相比,人员的投入、操作的复杂程度、操作耗时都成倍的下降了。

3,应用根据用户量弹性伸缩

Rainbond提供服务组件的伸缩功能,只需要一键,就可以为当前服务组件快速伸缩出多个实例,并且自动提供负载均衡。这将大幅度降低单个实例处理业务的压力。

在“咸阳市外来人口登记业务”的所有组件中,我们为前端页面、后台服务这两个服务组件都伸缩了最多5个实例,这两个服务组件也是经常进行实时更新的组件,基于多个实例,Rainbond提供滚动更新的功能,使业务的升级不会影响到线上的业务运行。

image-20200213112244995

上图中,显示的就是一次构建完成后的滚动更新过程。

为了能够让业务流量过大时,可以自动扩展实例数量,我们还设置了基于内存使用率来触发的自动伸缩功能。在运维层面更加自动化。

4,专门针对数据库进行优化

在如同“咸阳市外来人口登记业务”这样一个业务系统中,数据库的吞吐能力直接影响整个业务系统的综合吞吐能力。在这个层面,我们做了这样几件事情来提高数据库性能:

  • Mysql本身的性能优化:添加 max_connections 参数,提供Mysql所能提供的最大连接数。

  • 为后台服务安装出口网络治理插件,设置TCP最大连接数,从ServiceMesh微服务治理的层面,提高了后台服务到Mysql数据库之间依赖关系的通信能力。需要注意的一点,是该插件默认内存设置太小,我们需要点击更新内存来提高设置。

image-20200213114537579

  • 提高Mysql读写磁盘的能力:Rainbond应用市场默认提供的Mysql应用,均使用了共享存储类型的持久化存储来挂载Mysql数据目录。在这里,我们将其更改为本地存储类型。这样做的目的在于使用宿主机节点的本地磁盘代替Rainbond系统使用的共享存储,牺牲Mysql故障迁移的能力来换取性能的大幅度提升。这样的交易是否合理,取决于我们的数据库是否已经由于磁盘IO性能而达到了瓶颈。

5,通过实时性能分析插件,监控和优化应用

为了更好的监控“咸阳市外来人口登记业务”各个服务组件的压力情况,我们为前端页面、后台服务、数据库分别安装了Rainbond自带的服务实时性能分析插件。业务运行期间,这个插件为我们带来很多的有用信息,多次帮助开发人员发现业务系统的不足之处,使开发人员可以在业务雪崩宕机之前修正代码并上线。

对于前端页面、后台服务这样的基于Http协议提供服务的组件,插件将提供平均响应时间、吞吐率、在线人数三项实时数据,以及最近5分钟耗时URL排行、历史数据等持续性数据。

  • 通过分析排行中请求所有资源或接口的平均时间, 可以很方便的分析出当前业务系统的瓶颈在哪里,并据此调优代码。

image-20200213120511540

image-20200213120530981

  • 根据历史数据,我们发现当前组件在24小时内响应时间一直正常,在线人数峰值1600人同时在线,吞吐率根据人类作息时间正常起伏波动。

image-20200213120547158

而对于Mysql数据库而言,服务实时性能分析插件提供的信息最大不同,在于最近5分钟排行将替代为Mysql数据查询、更新等操作的排行。

  • 一次真实的经历是,一段时间内Mysql的平均响应时间不断在上升,后台服务处理能力随之急剧下降。危机边缘,开发人员立刻分析5分钟耗时排行,发现耗时排在第一位的查询语句引用了临时表,是一个慢查询语句,并且已经在Mysql内产生了堆积阻塞查询的现象。发现这个现象后立刻采取行动建立了索引,Mysql的响应时间缓慢的情况随之缓解。

image-20200213121956132

6, 其它优化

除了上述这些优化, 我们还做了其它许多事情来提高系统的抗并发能力。

  • 优化代码参数,很多开发框架天生带有最大连接数管理机制,例如对于Spring Boot项目而言,就需要调节server.tomcat.max-connections server.tomcat.max-threads

  • 优化短信验证业务流程,在大并发情况下,业务系统请求外部资源(例如短信业务)时,也需要考量该外部业务本身的并发限制。尽量以异步任务的方式处理,不要陷入外部资源一旦请求超限,不再响应导致了自身业务流程受阻,最后阻塞了业务的运行。

  • 关注出口网络治理插件与服务实时性能分析插件的内存设置,避免内存过小导致插件OOM,并随之影响业务服务组件的运行。

  • 关注业务服务组件实例内存占用,避免内存分配过小导致的OOM。

总结

现在 咸阳市疫情填报应用 已经度过了流量高峰期。整个填报期间,4套业务系统平均在线人数保持在4000人以上,峰值达到5000+。智慧社会操作系统没有出现一次宕机情况,始终稳定承载填报应用正常运行。后续,智慧社会操作系统将继续承载“重点人员监控系统”、“疫情可视化”等其他疫情管控系统,持续为咸阳市抗击“新冠”疫情作出自己的贡献。

五分钟学会使用 go modules(含在家办公使用技巧)

alicloudnative阅读(1380)评论(0)

作者 | 孙健波(天元)  阿里巴巴技术专家

导读go modules 是 golang 1.11 新加的特性。如今 1.13 都已经发布了第 7 个小版本了,几乎所有大项目均已开始使用,这自然也包括 Kubernetes 生态中的众多项目。笔者在开发 OAM 相关项目的时候,却发现 modules 的各项功能看似简单,却并没有那么好用,于是便想给大家分享一下使用心得,希望大家也能在最短时间内学会 modules 的使用,避免踩坑。

modules 是什么?

简单说就是包管理,Golang 的包管理素来以混乱著称,以前是依赖 $GOPATH,只要你的代码放在指定路径下就好了,完全没有“包管理”的概念。被社区吐槽了很久以后开始搞 vendor 机制,简单来说就是代码不光是可以放到指定路径,还可以放在项目自身路径的 vendor 文件夹。这个解决的问题是:你引用的代码包上游变更不会直接影响你的项目,这显然是开始关心“包版本”了。遗憾的是依旧没有解决包管理的问题,比如不同的包依赖了同一个包的不同版本怎么办?版本间代码冲突怎么办?vendor 机制并没有解决,于是围绕 vendor/ 社区就出了几十个包管理工具,一时间百花齐放、百家争鸣、各有所长,导致 golang 的包管理生态变得有些混乱。对这段历史感兴趣的可以阅读下笔者曾经写的文章《Go 包管理的前世今生》

更有意思的是,在 go 官方社区看到包管理工具的乱象后,也做了个功能类似的工具 dep,原理与其他各类依靠vendor/ 机制的包管理工具类似,准备对包管理做统一。当大家对 dep 工具报以期望并纷纷开始切换到 dep 工具管理依赖包的时候,go 官方又发布了现在的 modules 机制,完全放弃了之前的 dep 工具与 vendor 机制。这样的操作在社区引起了巨大的争议,modules 与 go getgo build 等官方工具生态有很好的集成,官方的意图自然是希望抛开原有的历史包袱,通过全新的方式拯救世界。然而实际体验下来,却依旧不尽如人意。

总的来说大趋势已经是用 modules,go1.13 也对 modules 机制做了不少工作。

言归正传,本文的目标是希望能用 5~10 分钟时间带您学会使用 go modules,然后通过 QA 的形式,描述一些常见的问题。如果希望详细理解相关内容,也可以参考官方文档

初始化

modules 机制在 go 里面的子命令是 go mod

modules 机制是 go 1.11 才引入的,所以开始用之前先检查下自己的 go 版本 go version,建议使用最新的 1.13 版本,涵盖了 module 机制相关的较多更新和功能。

在保证 go 版本至少在 1.11 或以上之后,就要开启一下环境变量 GO111MODULE,启用 module 机制。

export GO111MODULE=on

或者设置为 auto 模式,这样在 GOPATH 路径下的项目可以不使用 module 机制。

export GO111MODULE=auto

建议加到 ~/.bashrc~/.zshrc  等配置中启动便自动生效。

如果您的项目之前已经用 modules 管理了,那么到此为止你本地的环境已经完成初始化了。

如果项目里之前没有使用 modules,切换过来也很简单,删除原先的 vendor 文件夹(保险起见可以移动到项目之外的地方),在项目里执行一下初始化命令即可。

go mod init [module名称]

包名跟以前一样,还是跟 go path 强关联的,比如我们的项目一般是在 http://github.com/oam-dev/oam-go-sdk,那么你的包名就是这个了。

go mod init github.com/oam-dev/oam-go-sdk

初始化完成后就会看见项目里有个 go.mod 文件。

然后通过  go mod download 就可以下载所有原先 vendor 中的依赖。

日常包管理

使用了 go module 以后,你的许多命令就会与包管理集成,比如 go getgo buildgo run 都会自动查找并在go.mod 里面更新依赖。

所以按照 Go 官方团队的意思,一般情况下,你根本不用关心包管理的问题了。( 当然,这真的纯粹只是官方在 YY )

所以到这里你就已经学会 go modules 了,没超过5分钟吧?

FAQ

实际因为 go 官方对包管理重视的太晚,各种包都没有版本的概念,随随便便就会出现各种冲突,而且由于 go modules 实在是太自动了,所以就算你学会了怎么用 modules,最后还是会比较头疼。

下面我们就以 FAQ 的形式回答项目中遇到各种问题怎么办。

有些包由于特定网络原因无法访问怎么办?

Go module 加入了代理的机制,只要设置一个代理地址,就可以提供代理访问。阿里云就提供了这样一个 go 的代理 ,是完全免费的服务。公共代理仓库会代理并缓存go模块,你可以利用该代理来避免 DNS 污染或其他问题导致的模块拉取缓慢或失败的问题,加速你的项目构建。

设置的方式非常简单,只需要设置如下环境变量即可,执行命令:

export GOPROXY=https://mirrors.aliyun.com/goproxy/

go1.13 加入了 mirror 机制,可以通过 go env -w 设置 mirror,其实就是之前的 GOPROXY 做的更到位一些,执行命令:

go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct

逗号后面可以增加多个 proxy,最后的 direct 则是在所有 proxy 都找不到的时候,直接访问,代理访问不到的私有仓库就可以正常使用了。

这个功能基本上是在家远程办公的必备工具了。

公司通过 gitlab 搭建了私有库,二方依赖库下载不下来怎么办?

这个几乎是最常见的问题,比较简单的解决方案是 hack 一下 git 配置:

git config --global url."git@gitlab.your-company.com:<your>/<package>.git".insteadOf "https://gitlab.your-company.com/<your>/<package>.git"

这个方案依赖你本地的 ~/.ssh/id_rsa, 这样你就可以正常 go get 了。

Dockerfile 中构建镜像怎么解决私有库的依赖包问题?

  • 方案一:上述方式通过修改 git config ,却依赖你本地的  ~/.ssh/id_rsa,在构建时可以通过 multistage-build 把私钥 add 到 stage 0 里面 build,然后用后面新的 stage 生成镜像,这样构建的镜像就不会包含私钥;
  • 方案二: 更为安全的方式是,在每次构建 Docker 镜像之前,先在本地用 go mod vendor 把包缓存下来,在 Dockerfile 构建镜像过程中还是用 GOPATH 和 Vendor 机制来管理依赖。

依赖包怎么更新指定版本?

先查看版本:

$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99

再更新:

$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok      example.com/hello    0.022s

某些依赖包的地址变更导致无法找到了怎么办?

go 的依赖与项目名直接相关,这就导致如果我们使用了 github 上的项目,然后项目的维护人员突发奇想改个项目名称,就会导致所有依赖它的项目都无法找到依赖。

还好有 replace 的机制:

replace golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a => github.com/golang/crypto v0.0.0-20190313024323-a1f597ede03a

版本冲突怎么办?

这就要梳理版本了,是最没有捷径的。一个比较简单的办法是把所有 go.mod 里不需要指定版本的包全部删掉,仅指定必要的包版本,然后通过 go build 让项目自动构建依赖包的版本。

通过 go mod graph 可以查看具体依赖路径:

$ go mod graph
github.com/oam-dev/oam-go-sdk github.com/go-logr/logr@v0.1.0
github.com/oam-dev/oam-go-sdk github.com/onsi/ginkgo@v1.10.1
github.com/oam-dev/oam-go-sdk github.com/onsi/gomega@v1.7.0
github.com/oam-dev/oam-go-sdk github.com/stretchr/testify@v1.4.0
github.com/oam-dev/oam-go-sdk golang.org/x/net@v0.0.0-20191004110552-13f9640d40b9
github.com/oam-dev/oam-go-sdk k8s.io/api@v0.17.0
github.com/oam-dev/oam-go-sdk k8s.io/apimachinery@v0.17.0
github.com/oam-dev/oam-go-sdk k8s.io/client-go@v0.17.0
github.com/oam-dev/oam-go-sdk sigs.k8s.io/controller-runtime@v0.4.0
...

左边是项目包,右边是被依赖的包和版本。

如果确实存在两个需要指定版本的包互相冲突,那就要做取舍,修改代码,升级或降级某个包了。

本地包如何引用?

如果在代码调试过程中,涉及到修改其他依赖项目代码,这时候就要引用本地包,也可以采用 replace 机制:

require (
    golang.org/x/crypto v0.0.0
)
replace golang.org/x/crypto v0.0.0 => ../crypto

后面这个就是个相对项目路径的本地依赖所在路径。

解决了上面的这些问题,基本上你就可以愉快的使用 module 功能啦。

go mod 命令一览

go mod 里还有一些其他功能,也在此列举,方便大家查阅:

1.png

最后

OAM(Open Application Model)开放应用模型是阿里联合微软针对云原生应用的模型,第一次对“以应用为中心”的基础设施和构建规范进行了完整的阐述。应用管理者只要遵守这个规范,就可以编写出一个自包含、自描述的“应用定义文件”。

OAM 将应用划分为应用组件和应用特征两部分,应用组件是应用本身的逻辑,而应用特征则是云上的各种通用能力(如扩缩容、监控、灰度等等),大大提升了应用构建时模块化复用能力,将云上的各类资源和能力都转化为了标准化的可“声明”对象。

同时 OAM 强调关注点分离,通过标准化的模型将应用开发不同阶段的 API 进行分层,流程上先由研发定义应用组件,再由运维配置云上的各种策略,最后由基础设施团队统一提供各类模块化的能力。OAM 则在其中起着彼此协作的粘合剂作用,大大提高了应用交付的效率。

OAM 相关内容在 github 上完全开源,同时我们也为 Go 生态编写了 oam-go-sdk 方便快速实现 OAM。

目前,阿里巴巴团队正在上游贡献和维护这套技术,如果大家有什么问题或者反馈,也非常欢迎与我们在上游或者钉钉联系。

参与方式:

  • 钉钉扫码进入 OAM 项目中文讨论群

2.png

招聘

我们也在招聘,感兴趣的同学欢迎加入我们。在这里,既有 CNCF TOC 和 SIG 联席主席,也有 etcd 创始人、K8s Operator 创始人与 Kubernetes 核心维护成员组成的、国内最顶尖的 Kubernetes 技术团队。在这里,你将同来自全球的云原生技术领域专家们(比如 Helm 项目的创始人、Istio 项目的创始人)密切合作,在独一无二的场景与规模中从事 Kubernetes、Service Mesh、Serverless、Open Application Model ( OAM )等云计算生态核心技术的研发与落地工作,在业界标杆级的平台上既赋能阿里巴巴全球经济体,更服务全世界的开发者用户。

简历投递至 jianbo.sjb@alibaba-inc.com

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术圈。”

你的K8s 运行时环境安全吗? KubeXray帮你保护K8s环境及应用

JFrogchina阅读(654)评论(0)

引言

大多数安全措施都是为了防止漏洞逃跑而设计的, 在此之前,我们也分享了一些第三方安全扫描的文章(请移步到历史文章中查看),尽早识别应用程序的风险意味着您可以防止或限制它部署到您的系统中(安全左移策略)。有了这些知识或工具,容器中任何可能造成损坏的漏洞都可以安全地留在由您的安全策略围栏后面。

 

但是,当这些漏洞已经逃跑时,我们能做什么呢? 如何确保已经在Kubernetes pods中运行的容器和应用程序符合您当前的风险和策略?

 

背景(运行时安全管控)

由于大多数应用程序严重依赖于包管理器和开源存储库,因此它们很容易受到来自这些源的恶意或不安全代码的攻击。想象我们交付的软件 Application 是一张饼,我们自己开发的代码仅占其中很小一部分,见下图:

 

 

最近,当Javascript社区得知npm module中流行的事件流包被一个针对比特币钱包平台的恶意包更新时,他们非常愤怒。在被发现和报道之前的三个月里,这个包被下载了近800万次。

虽然来自社区包管理器的此类事件并不常见,但并不少见。一年前,npm发现并删除了39个恶意包。所以很多包在我们安全策略发现之前可能已经进入到了生产环境

 

解决方案

在介绍如何对运行时进行安全控制之前,先回顾一下常见漏洞扫描工具的原理:这里以JFrog  Xray 为例:

通用二进制分析工具和策略引擎JFrog Xray,会实时扫描Artifactory制品库中的容器镜像,war包,以及Npm module 等二进制制品,执行深度递归扫描,逐层检查应用程序的所有组件,并与多个漏洞数据源(已知漏洞数据库)进行一一对比,从而判断是否存在已知漏洞 或License许可证策略问题,同时为被扫描文件(Docker 镜像,Npm Module)添加相关元数据。

 

Xray 漏洞扫描平台分析

DevOps管理员可以根据Xray扫描平台所发现的风险级别,配置策略来限制或阻止Kubernetes部署这些Docker 镜像。但是可以发现仅仅使用Xray,只能将漏洞限制在运行时之前。

 

为了解决这个问题,JFrog提供了KubeXray 组件,这是一个开源软件项目,它将通用二进制安全分析工具Xray的安全性扩展到Kubernetes pods运行时。

使用Xray扫描容器映像生成的元数据,KubeXray可以对已经部署的内容(容器镜像等)进行安全策略管控

KubeXray监控所有活动Kubernetes Pod资源,以帮助您:

1. 捕捉当前在所有Kubernetes吊舱中运行的应用程序中最新报告的风险或漏洞

2. 对正在运行的应用程序强制执行当前策略,即使您已经更改了这些策略

3. 对未被Xray扫描且风险未知的正在运行的应用程序执行策略

 

通过这种方式,KubeXray可以帮助您将逃逸的漏洞进行安全的控制。

 

 

KubeXray 是什么?

 

在Kubernetes将容器镜像部署到pods之前,Xray检测风险并将策略应用于容器镜像,KubeXray检测风险并将策略应用于已经运行或即将运行的Kubernetes pod。

KubeXray监视来自Kubernetes服务器和Xray的安全事件,并为Kubernetes运行的所有pods执行当前的安全策略。KubeXray监听这些事件流:

1. 部署新服务(Pod)

2. 升级现有服务

3. 新的许可证策略,例如某个License许可证类型不允许在运行时使用

4. 一个新的安全问题

 

当检测到问题时,KubeXray会根据您设置的当前策略进行响应。您可以选择以下可能的操作之一:

 

  •  Scaledown为直到0。所需的服务状态更新为0,使其在仍然可以查询时处于非活动状态
  •  删除漏洞容器镜像的相应Kubernetes资源
  •  忽略它,让pod继续运行

 

KubeXray还了解不同Kubernetes资源(状态集和部署)之间的差异,并允许对每种资源应用不同的策略操作。

 

 

虽然KubeXray主要是将Xray的深度扫描安全性扩展到运行Kubernetes pods,但它也为未被Xray扫描的pods提供了一些策略控制,例如从存储库(而不是Artifactory)部署的容器映像。对于没有经过x射线扫描的pod,因此其风险是未知的,您可以指定要采取的单独策略操作。

 

KubeXray 工作原理

KubeXray监听Kubernetes集群中运行的每个pod,并使用Xray元数据(何时可用以及是否可用)来确定安全策略控制。

1. 对于Kubernetes上的每个pod(运行或计划运行),KubeXray检查Xray元数据中的漏洞或License许可证策略问题。如果发现任何风险,KubeXray将采取相应的控制操作。

 

2. 如果Kubernetes pod中的任何容器镜像(正在运行或计划运行)没有被Xray识别——因为它没有被扫描,或者因为它没有从Artifactory 下载——那么KubeXray将以未知风险来应用当前的策略集。

 

每当在Xray上添加或更新新策略,或报告新漏洞时,KubeXray都会检测到此更改,并检查现有pod是否存在问题。如果发现任何风险,KubeXray将立即根据当前安全策略进行安全控制。

 

如下图所式: 显示对漏洞pod的每个策略操作过程(忽略/删除/缩容)。

 

 

上面提到:KubeXray根据发现的风险和DevOps管理员配置的策略应用策略操作。

策略操作是在一个 values.yaml 文件中设置。您可以为以下条件配置策略操作(缩容、删除或忽略):

 

  •  未扫描——未被 Xray 扫描deployments ,您还可以指定命名空间的白名单;使用这些命名空间的deployments 将不应用安全策略操作。
  •  安全性——由于漏洞而存在安全问题的deployments 。
  •  License许可证——许可证不符合策略的deployments 。

 

上述每种条件都为Deployments 和StatefulSets提供了单独的策略操作设置。

 

 

KubeXray安装使用

KubeXray工具是一个开源软件项目,可以在Github存储库中找到并安装它(https://github.com/jfrog/kubexray)。

 

 

要使用KubeXray,您必须具备:

  •  一个已获授权及正在运行Artifactory 服务
  •  一个已获授权及正在运行Xray服务
  •  一个 正在运行的Kubernetes集群
  •  客户端Kubectl
  •  Helm客户端以及Helm服务端配置(Tiler)

 

快速安装KubeXray:

JFrog Helm仓库中提供的一个Helm Chart,可以快速安装或升级JFrog KubeXray到正在运行的Kubernetes集群。要自定义KubeXray的配置,请参阅Github Readme文档。

 

安装KubeXray后,可以在values.yaml中设置前文提到的策略操作。让JFrog KubeXray监视您的Kubernetes pod,控制Kubernetes 集群运行时存在的安全漏洞或License 许可证问题。

 

 

总结

常见的第三方漏洞安全监管工具一般只在控制运行时之前进行安全控制,在运行时未能做到相应的监管控制,KubeXray可以帮助我们快速对运行时资源进行安全管控,并且其作为一个开源软件项目,我们期待着继续增强KubeXray以获得更健壮的操作和特性,并欢迎开发人员在社区提出改进意见和提交代码。

 

更多技术分享请关注公众号:JFrog杰蛙DevOps

也可以添加官方微信号:JFrogjiewachina

 

2月13日在线课堂:《容器持续交付流水线最佳实践》

课程介绍

作为运维、工程效能、sre

你是否对开发人员代码质量参差不齐而感到愤恨?

你是否对开发人员使用的不同开发语言而眼花缭乱?

你是否对不同技术栈的质量度量标准无从下手?

你是否曾经因为开发一句“环境问题,和代码无关”而莫名背锅?

听完这节课,教你如何度量开发人员的代码质量

 

 

课程内容

SonarQube 是一代码质量管理的工具,它主要用于度量源代码的质量。可以支持多种开发语言,比如 java,C, C#, go,C++, Python, Groovy 等。帮助你发现代码的漏洞,Bug,坏味道以及技术债务等信息。

本节课会基于SonarQube,讲解运维、工程效能、Sre团队如何自动化度量开发人员代码质量,如何在DevOps体系中集成代码质量扫描,提供最佳实践及平台建设思路,课程中也会提供实操演示环节。

 

 

本期话题

1. SonarQube介绍

2. SonarQube使用详解

3. SonarQube最佳实践

4. 运维人员如何评估业务代码质量

 

课堂活动 

本期课堂讲师会在结束前进行抽奖活动

第一名:小爱蓝牙耳机

第二名:JFrog新版T恤

第三名:JFrog新版T恤

 

报名链接:https://www.bagevent.com/event/6360865

调度系统设计精要

alicloudnative阅读(1108)评论(0)

作者 | Draveness

导读:本文作者写这篇文章前前后后大概 2 个月的时间,全文大概 2w 字,建议收藏后阅读或者通过电脑阅读。

调度是一个非常广泛的概念,很多领域都会使用调度这个术语,在计算机科学中,调度就是一种将任务(Work)分配给资源的方法。任务可能是虚拟的计算任务,例如线程、进程或者数据流,这些任务会被调度到硬件资源上执行,例如:处理器 CPU 等设备。

·1.png

图 1 – 调度系统设计精要

本文会介绍调度系统的常见场景以及设计过程中的一些关键问题,调度器的设计最终都会归结到一个问题上 — 如何对资源高效的分配和调度以达到我们的目的,可能包括对资源的合理利用、最小化成本、快速匹配供给和需求。

2.png

图 2 – 文章脉络和内容

除了介绍调度系统设计时会遇到的常见问题之外,本文还会深入分析几种常见的调度器的设计、演进与实现原理,包括操作系统的进程调度器,Go 语言的运行时调度器以及 Kubernetes 的工作负载调度器,帮助我们理解调度器设计的核心原理。

设计原理

调度系统其实就是调度器(Scheduler),我们在很多系统中都能见到调度器的身影,就像我们在上面说的,不止操作系统中存在调度器,编程语言、容器编排以及很多业务系统中都会存在调度系统或者调度模块。

这些调度模块的核心作用就是对有限的资源进行分配,以实现最大化资源的利用率或者降低系统的尾延迟,调度系统面对的就是资源的需求和供给不平衡的问题。

3.png

图 3 – 调度器的任务和资源

我们在这一节中将从多个方面介绍调度系统设计时需要重点考虑的问题,其中包括调度系统的需求调研、调度原理以及架构设计。

1. 需求调研

在着手构建调度系统之前,首要的工作就是进行详细的需求调研和分析,在这个过程中需要完成以下两件事:

  • 调研调度系统的应用场景,深入研究场景中待执行的任务(Work)和能用来执行任务的资源(Resource)的特性;
  • 分析调度系统的目的,可能是成本优先、质量优先、最大化资源的利用率等,调度目的一般都是动态的,会随着需求的变化而转变;

应用场景

调度系统应用的场景是我们首先需要考虑的问题,对应用场景的分析至关重要,我们需要深入了解当前场景下待执行任务和能用来执行任务的资源的特点。我们需要分析待执行任务的以下特征:

  • 任务是否有截止日期,必须在某个时间点之前完成;
  • 任务是否支持抢占,抢占的具体规则是什么;
  • 任务是否包含前置的依赖条件;
  • 任务是否只能在指定的资源上运行;

而用于执行任务的资源也可能存在资源不平衡,不同资源处理任务的速度不一致的问题。

资源和任务特点的多样性决定了调度系统的设计,我们在这里举几个简单的例子帮助各位读者理解调度系统需求分析的过程。

4.jpeg

图 4 – Linux 操作系统

在操作系统的进程调度器中,待调度的任务就是线程,这些任务一般只会处于正在执行或者未执行(等待或者终止)的状态;而用于处理这些任务的 CPU 往往都是不可再分的,同一个 CPU 在同一时间只能执行一个任务,这是物理上的限制。简单总结一下,操作系统调度器的任务和资源有以下特性:

  • 任务 —— Thread。状态简单:只会处于正在执行或者未被执行两种状态;优先级不同:待执行的任务可能有不同的优先级,在考虑优先级的情况下,需要保证不同任务的公平性;
  • 资源 —— CPU 时间。资源不可再分:同一时间只能运行一个任务;

在上述场景中,待执行的任务是操作系统调度的基本单位 —— 线程,而可分配的资源是 CPU 的时间。Go 语言的调度器与操作系统的调度器面对的是几乎相同的场景,其中的任务是 Goroutine,可以分配的资源是在 CPU 上运行的线程。

5.png

图 5 – 容器编排系统 Kubernetes

除了操作系统和编程语言这种较为底层的调度器之外,容器和计算任务调度在今天也很常见,Kubernetes 作为容器编排系统会负责调取集群中的容器,对它稍有了解的人都知道,Kubernetes 中调度的基本单元是 Pod,这些 Pod 会被调度到节点 Node 上执行:

  • 任务 —— Pod。优先级不同:Pod 的优先级可能不同,高优先级的系统 Pod 可以抢占低优先级 Pod 的资源;有状态:Pod 可以分为无状态和有状态,有状态的 Pod 需要依赖持久存储卷;
  • 资源 —— Node。类型不同:不同节点上的资源类型不同,包括 CPU、GPU 和内存等,这些资源可以被拆分但是都属于当前节点;不稳定:节点可能由于突发原因不可用,例如:无网络连接、磁盘损坏等;

调度系统在生活和工作中都很常见,除了上述的两个场景之外,其他需要调度系统的场景包括 CDN 的资源调度、订单调度以及离线任务调度系统等。在不同场景中,我们都需要深入思考任务和资源的特性,它们对系统的设计起者指导作用。

调度目的

在深入分析调度场景后,我们需要理解调度的目的。我们可以将调度目的理解成机器学习中的成本函数(Cost function),确定调度目的就是确定成本函数的定义,调度理论一书中曾经介绍过常见的调度目的,包含以下内容:

  • 完成跨度(Makesapan) — 第一个到最后一个任务完成调度的时间跨度;
  • 最大延迟(Maximum Lateness) — 超过截止时间最长的任务;
  • 加权完成时间的和(Total weighted completion time)— 权重乘完成时间的总和;

这些都是偏理论的调度的目的,多数业务调度系统的调度目的都是优化与业务联系紧密的指标 — 成本和质量。如何在成本和质量之间达到平衡是需要仔细思考和设计的,由于篇幅所限以及业务场景的复杂,本文不会分析如何权衡成本和质量,这往往都是需要结合业务考虑的事情,不具有足够的相似性。

2. 调度原理

性能优异的调度器是实现特定调度目的前提,我们在讨论调度场景和目的时往往都会忽略调度的额外开销,然而调度器执行时的延时和吞吐量等指标在调度负载较重时是不可忽视的。本节会分析与调度器实现相关的一些重要概念,这些概念能够帮助我们实现高性能的调度器:

  • 协作式调度与抢占式调度;
  • 单调度器与多调度器;
  • 任务分享与任务窃取;

协作式与抢占式

协作式(Cooperative)与抢占式(Preemptive)调度是操作系统中常见的多任务运行策略。这两种调度方法的定义完全不同:

  • 协作式调度允许任务执行任意长的时间,直到任务主动通知调度器让出资源;
  • 抢占式调度允许任务在执行过程中被调度器挂起,调度器会重新决定下一个运行的任务;

6.png

图 6 – 协作式调度与抢占式调度

任务的执行时间和任务上下文切换的额外开销决定了哪种调度方式会带来更好的性能。如下图所示,图 7 展示了一个协作式调度器调度任务的过程,调度器一旦为某个任务分配了资源,它就会等待该任务主动释放资源,图中 4 个任务尽管执行时间不同,但是它们都会在任务执行完成后释放资源,整个过程也只需要 4 次上下文的切换。

7.png

图 7 – 协作式调度

图 8 展示了抢占式调度的过程,由于调度器不知道所有任务的执行时间,所以它为每一个任务分配了一段时间切片。任务 1 和任务 4 由于执行时间较短,所以在第一次被调度时就完成了任务;但是任务 2 和任务 3 因为执行时间较长,超过了调度器分配的上限,所以为了保证公平性会触发抢占,等待队列中的其他任务会获得资源。在整个调度过程中,一共发生了 6 次上下文切换。

8.png

图 8 – 抢占式调度

如果部分任务的执行时间很长,协作式的任务调度会使部分执行时间长的任务饿死其他任务;不过如果待执行的任务执行时间较短并且几乎相同,那么使用协作式的任务调度能减少任务中断带来的额外开销,从而带来更好的调度性能。

因为多数情况下任务执行的时间都不确定,在协作式调度中一旦任务没有主动让出资源,那么就会导致其它任务等待和阻塞,所以调度系统一般都会以抢占式的任务调度为主,同时支持任务的协作式调度。

单调度器与多调度器

使用单个调度器还是多个调度器也是设计调度系统时需要仔细考虑的,多个调度器并不一定意味着多个进程,也有可能是一个进程中的多个调度线程,它们既可以选择在多核上并行调度、在单核上并发调度,也可以同时利用并行和并发提高性能。

9.png

图 9 – 单调度器调度任务和资源

不过对于调度系统来说,因为它做出的决策会改变资源的状态和系统的上下文进而影响后续的调度决策,所以单调度器的串行调度是能够精准调度资源的唯一方法。单个调度器利用不同渠道收集调度需要的上下文,并在收到调度请求后会根据任务和资源情况做出当下最优的决策。

随着调度器的不断演变,单调度器的性能和吞吐量可能会受到限制,我们还是需要引入并行或者并发调度来解决性能上的瓶颈,这时我们需要将待调度的资源分区,让多个调度器分别负责调度不同区域中的资源。

10.png

图 10 – 多调度器与资源分区

多调度器的并发调度能够极大提升调度器的整体性能,例如 Go 语言的调度器。Go 语言运行时会将多个 CPU 交给不同的处理器分别调度,这样通过并行调度能够提升调度器的性能。

上面介绍的两种调度方法都建立在需要精准调度的前提下,多调度器中的每一个调度器都会面对无关的资源,所以对于同一个分区的资源,调度还是串行的。

11.png

图 11 – 多调度器粗粒度调度

使用多个调度器同时调度多个资源也是可行的,只是可能需要牺牲调度的精确性 — 不同的调度器可能会在不同时间接收到状态的更新,这就会导致不同调度器做出不同的决策。负载均衡就可以看做是多线程和多进程的调度器,因为对任务和资源掌控的信息有限,这种粗粒度调度的结果很可能就是不同机器的负载会有较大差异,所以无论是小规模集群还是大规模集群都很有可能导致某些实例的负载过高。

工作分享与工作窃取

这一小节将继续介绍在多个调度器间重新分配任务的两个调度范式 — 工作分享(Work Sharing)和工作窃取(Work Stealing)。独立的调度器可以同时处理所有的任务和资源,所以它不会遇到多调度器的任务和资源的不平衡问题。在多数的调度场景中,任务的执行时间都是不确定的,假设多个调度器分别调度相同的资源,由于任务的执行时间不确定,多个调度器中等待调度的任务队列最终会发生差异 — 部分队列中包含大量任务,而另外一些队列不包含任务,这时就需要引入任务再分配策略。

工作分享和工作窃取是完全不同的两种再分配策略。在工作分享中,当调度器创建了新任务时,它会将一部分任务分给其他调度器;而在工作窃取中,当调度器的资源没有被充分利用时,它会从其他调度器中窃取一些待分配的任务,如下图所示:

12.png

图 12 – 工作窃取调度器

这两种任务再分配的策略都为系统增加了额外的开销,与工作分享相比,工作窃取只会在当前调度器的资源没有被充分利用时才会触发,所以工作窃取引入的额外开销更小。工作窃取在生产环境中更加常用,Linux 操作系统和 Go 语言都选择了工作窃取策略。

3. 架构设计

本节将从调度器内部和外部两个角度分析调度器的架构设计,前者分析调度器内部多个组件的关系和做出调度决策的过程;后者分析多个调度器应该如何协作,是否有其他的外部服务可以辅助调度器做出更合理的调度决策。

调度器内部

当调度器收到待调度任务时,会根据采集到的状态和待调度任务的规格(Spec)做出合理的调度决策,我们可以从下图中了解常见调度系统的内部逻辑。

13.png

图 13 – 调度器做出调度决策

常见的调度器一般由两部分组成 — 用于收集状态的状态模块和负责做决策的决策模块。

  • 状态模块

状态模块会从不同途径收集尽可能多的信息为调度提供丰富的上下文,其中可能包括资源的属性、利用率和可用性等信息。根据场景的不同,上下文可能需要存储在 MySQL 等持久存储中,一般也会在内存中缓存一份以减少调度器访问上下文的开销。

  • 决策模块

决策模块会根据状态模块收集的上下文和任务的规格做出调度决策,需要注意的是做出的调度决策只是在当下有效,在未来某个时间点,状态的改变可能会导致之前做的决策不符合任务的需求,例如:当我们使用 Kubernetes 调度器将工作负载调度到某些节点上,这些节点可能由于网络问题突然不可用,该节点上的工作负载也就不能正常工作,即调度决策失效。

调度器在调度时都会通过以下的三个步骤为任务调度合适的资源:

  1. 通过优先级、任务创建时间等信息确定不同任务的调度顺序;
  2. 通过过滤和打分两个阶段为任务选择合适的资源;
  3. 不存在满足条件的资源时,选择牺牲的抢占对象。

14.png

图 14 – 调度框架

上图展示了常见调度器决策模块执行的几个步骤,确定优先级、对闲置资源进行打分、确定抢占资源的牺牲者,上述三个步骤中的最后一个往往都是可选的,部分调度系统不需要支持抢占式调度的功能。

调度器外部

如果我们将调度器看成一个整体,从调度器外部看架构设计就会得到完全不同的角度 — 如何利用外部系统增强调度器的功能。在这里我们将介绍两种调度器外部的设计,分别是多调度器和反调度器(Descheduler)。

  • 多调度器

串行调度与并行调度一节已经分析了多调度器的设计,我们可以将待调度的资源进行分区,让多个调度器线程或者进程分别负责各个区域中资源的调度,充分利用多和 CPU 的并行能力。

  • 反调度器

反调度器是一个比较有趣的概念,它能够移除决策不再正确的调度,降低系统中的熵,让调度器根据当前的状态重新决策。

15.png

图 15 – 调度器与反调度器

反调度器的引入使得整个调度系统变得更加健壮。调度器负责根据当前的状态做出正确的调度决策,反调度器根据当前的状态移除错误的调度决策,它们的作用看起来相反,但是目的都是为任务调度更合适的资源。

反调度器的使用没有那么广泛,实际的应用场景也比较有限。作者第一次发现这个概念是在 Kubernetes 孵化的descheduler 项目中,不过因为反调度器移除调度关系可能会影响正在运行的线上服务,所以 Kubernetes 也只会在特定场景下使用。

操作系统

调度器是操作系统中的重要组件,操作系统中有进程调度器、网络调度器和 I/O 调度器等组件,本节介绍的是操作系统中的进程调度器。

有一些读者可能会感到困惑,操作系统调度的最小单位不是线程么,为什么这里使用的是进程调度。在 Linux 操作系统中,调度器调度的不是进程也不是线程,它调度的是 task_struct 结构体,该结构体既可以表示线程,也可以表示进程,而调度器会将进程和线程都看成任务,我们在这里先说明这一问题,避免读者感到困惑。我们会使用进程调度器这个术语,但是一定要注意 Linux 调度器中并不区分线程和进程。

Linux incorporates process and thread scheduling by treating them as one in the same. A process can be viewed as a single thread, but a process can contain multiple threads that share some number of resources (code and/or data).

接下来,本节会研究操作系统中调度系统的类型以及 Linux 进程调度器的演进过程。

1. 调度系统类型

操作系统会将进程调度器分成三种不同的类型,即长期调度器、中期调度器和短期调度器。这三种不同类型的调度器分别提供了不同的功能,我们将在这一节中依次介绍它们。

长期调度器

长期调度器(Long-Term Scheduler)也被称作任务调度器(Job Scheduler),它能够决定哪些任务会进入调度器的准备队列。当我们尝试执行新的程序时,长期调度器会负责授权或者延迟该程序的执行。长期调度器的作用是平衡同时正在运行的 I/O 密集型或者 CPU 密集型进程的任务数量:

  • 如果 I/O 密集型任务过多,就绪队列中就不存在待调度的任务,短期调度器不需要执行调度,CPU 资源就会面临闲置;
  • 如果 CPU 密集型任务过多,I/O 等待队列中就不存在待调度的任务,I/O 设备就会面临闲置;

长期调度器能平衡同时正在运行的 I/O 密集型和 CPU 密集型任务,最大化的利用操作系统的 I/O 和 CPU 资源。

中期调度器

中期调度器会将不活跃的、低优先级的、发生大量页错误的或者占用大量内存的进程从内存中移除,为其他的进程释放资源。

16.png

图 16 – 中期调度器

当正在运行的进程陷入 I/O 操作时,该进程只会占用计算资源,在这种情况下,中期调度器就会将它从内存中移除等待 I/O 操作完成后,该进程会重新加入就绪队列并等待短期调度器的调度。

短期调度器

短期调度器应该是我们最熟悉的调度器,它会从就绪队列中选出一个进程执行。进程的选择会使用特定的调度算法,它会同时考虑进程的优先级、入队时间等特征。因为每个进程能够得到的执行时间有限,所以短期调度器的执行十分频繁。

2. 设计与演进

本节将重点介绍 Linux 的 CPU 调度器,也就是短期调度器。Linux 的 CPU 调度器并不是从设计之初就是像今天这样复杂的,在很长的一段时间里(v0.01 ~ v2.4),Linux 的进程调度都由几十行的简单函数负责,我们先了解一下不同版本调度器的历史:

  • 初始调度器 · v0.01 ~ v2.4。由几十行代码实现,功能非常简陋;同时最多处理 64 个任务;
  • 调度器 · v2.4 ~ v2.6。调度时需要遍历全部任务当待执行的任务较多时,同一个任务两次执行的间隔很长,会有比较严重的饥饿问题;
  • 调度器 · v2.6.0 ~ v2.6.22。通过引入运行队列和优先数组实现  的时间复杂度;使用本地运行队列替代全局运行队列增强在对称多处理器的扩展性;引入工作窃取保证多个运行队列中任务的平衡;
  • 完全公平调度器 · v2.6.23 ~ 至今。引入红黑树和运行时间保证调度的公平性;引入调度类实现不同任务类型的不同调度策略;

这里会详细介绍从最初的调度器到今天复杂的完全公平调度器(Completely Fair Scheduler,CFS)的演变过程。

初始调度器

Linux 最初的进程调度器仅由 sched.h 和 sched.c 两个文件构成。你可能很难想象 Linux 早期版本使用只有几十行的 schedule 函数负责了操作系统进程的调度

void schedule(void) {
    int i,next,c;
    struct task_struct ** p;
    for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
       ...
    }
    while (1) {
        c = -1;
        next = 0;
        i = NR_TASKS;
        p = &task[NR_TASKS];
        while (--i) {
            if (!*--p) continue;
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
                c = (*p)->counter, next = i;
        }
        if (c) break;
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
            if (*p)
                (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
    }
    switch_to(next);
}

无论是进程还是线程,在 Linux 中都被看做是 task_struct 结构体,所有的调度进程都存储在上限仅为 64 的数组中,调度器能够处理的进程上限也只有 64 个。

17.png

图 17 – 最初的进程调度器

上述函数会先唤醒获得信号的可中断进程,然后从队列倒序查找计数器 counter 最大的可执行进程,counter 是进程能够占用的时间切片数量,该函数会根据时间切片的值执行不同的逻辑:

  • 如果最大的 counter 时间切片大于 0,调用汇编语言的实现的 switch_to 切换进程;
  • 如果最大的 counter 时间切片等于 0,意味着所有进程的可执行时间都为 0,那么所有进程都会获得新的时间切片;

Linux 操作系统的计时器会每隔 10ms 触发一次 do_timer 将当前正在运行进程的 counter 减一,当前进程的计数器归零时就会重新触发调度。

O(n)调度器

调度器是 Linux 在 v2.4 ~ v2.6 版本使用的调度器,由于该调取器在最坏的情况下会遍历所有的任务,所以它调度任务的时间复杂度就是 。Linux 调度算法将 CPU 时间分割成了不同的时期(Epoch),也就是每个任务能够使用的时间切片。

我们可以在 sched.h 和 sched.c 两个文件中找到调度器的源代码。与上一个版本的调度器相比, 调度器的实现复杂了很多,该调度器会在 schedule 函数中遍历运行队列中的所有任务并调用 goodness 函数分别计算它们的权重获得下一个运行的进程:

asmlinkage void schedule(void){
    ...
still_running_back:
    list_for_each(tmp, &runqueue_head) {
        p = list_entry(tmp, struct task_struct, run_list);
        if (can_schedule(p, this_cpu)) {
            int weight = goodness(p, this_cpu, prev->active_mm);
            if (weight > c)
                c = weight, next = p;
        }
    }
    ...
}

在每个时期开始时,上述代码都会为所有的任务计算时间切片,因为需要执行 n 次,所以调度器被称作  调度器。在默认情况下,每个任务在一个周期都会分配到 200ms 左右的时间切片,然而这种调度和分配方式是  调度器的最大问题:

  • 每轮调度完成之后就会陷入没有任务需要调度的情况,需要提升交互性能的场景会受到严重影响,例如:在桌面拖动鼠标会感觉到明显的卡顿;
  • 每次查找权重最高的任务都需要遍历数组中的全部任务;
  • 调度器分配的平均时间片大小为 210ms,当程序中包含 100 个进程时,同一个进程被运行两次的间隔是 21s,这严重影响了操作系统的可用性.

正是因为调度器存在了上述的问题,所以 Linux 内核在两个版本后使用新的  调度器替换该实现。

O(1)调度器

调度器在 v2.6.0 到 v2.6.22 的 Linux 内核中使用了四年的时间,它能够在常数时间内完成进程调度,你可以在sched.h 和 sched.c 中查看  调度器的源代码。因为实现和功能复杂性的增加,调度器的代码行数从  的 2100 行增加到 5000 行,它在调度器的基础上进行了如下的改进

  • 调度器支持了  时间复杂度的调度;
  • 调度器支持了对称多处理(Symmetric multiprocessing,SMP)的扩展性;
  • 调度器优化了对称多处理的亲和性。

数据结构

调度器通过运行队列 runqueue 和优先数组 prio_array 两个重要的数据结构实现了  的时间复杂度。每一个运行队列都持有两个优先数组,分别存储活跃的和过期的进程数组:

struct runqueue {
    ...
    prio_array_t *active, *expired, arrays[2];
    ...
}
struct prio_array {
    unsignedint nr_active;
    unsignedlong bitmap[BITMAP_SIZE];
    struct list_head queue[MAX_PRIO];
};

优先数组中的 nr_active 表示活跃的进程数,而 bitmap 和 list_head 共同组成了如下图所示的数据结构:

18.png

图 18 – 优先数组

优先数组的 bitmap 总共包含 140 位,每一位都表示对应优先级的进程是否存在。图 17 中的优先数组包含 3 个优先级为 2 的进程和 1 个优先级为 5 的进程。每一个优先级的标志位都对应一个 list_head 数组中的链表。 调度器使用上述的数据结构进行如下所示的调度:

  • 调用 sched_find_first_bit 按照优先级分配 CPU 资源;
  • 调用 schedule 从链表头选择进程执行;
  • 通过 schedule 轮训调度同一优先级的进程,该函数在每次选中待执行的进程后,将进程添加到队列的末尾,这样可以保证同一优先级的进程会依次执行(Round-Robin);
  • 计时器每隔 1ms 会触发一次 scheduler_tick 函数,如果当前进程的执行时间已经耗尽,就会将其移入过期数组;
  • 当活跃队列中不存在待运行的进程时,schedule 会交换活跃优先数组和过期优先数组;

上述的这些规则是  调度器运行遵守的主要规则,除了上述规则之外,调度器还需要支持抢占、CPU 亲和等功能,不过在这里就不展开介绍了。

本地运行队列

全局的运行队列是  调度器难以在对称多处理器架构上扩展的主要原因。为了保证运行队列的一致性,调度器在调度时需要获取运行队列的全局锁,随着处理器数量的增加,多个处理器在调度时会导致更多的锁竞争,严重影响调度性能。 调度器通过引入本地运行队列解决这个问题,不同的 CPU 可以通过 this_rq 获取绑定在当前 CPU 上的运行队列,降低了锁的粒度和冲突的可能性。

#define this_rq()        (&__get_cpu_var(runqueues))

19.png

图 19 – 全局运行队列和本地运行队列

多个处理器由于不再需要共享全局的运行队列,所以增强了在对称对处理器架构上的扩展性,当我们增加新的处理器时,只需要增加新的运行队列,这种方式不会引入更多的锁冲突。

优先级和时间切片

调度器中包含两种不同的优先级计算方式,一种是静态任务优先级,另一种是动态任务优先级。在默认情况下,任务的静态任务优先级都是 0,不过我们可以通过系统调用 nice 改变任务的优先级; 调度器会奖励 I/O 密集型任务并惩罚 CPU 密集型任务,它会通过改变任务的静态优先级来完成优先级的动态调整,因为与用户交互的进程时 I/O 密集型的进程,这些进程由于调度器的动态策略会提高自身的优先级,从而提升用户体验。

完全公平调度器

完全公平调度器(Completely Fair Scheduler,CFS)是 v2.6.23 版本被合入内核的调度器,也是内核的默认进程调度器,它的目的是最大化 CPU 利用率和交互的性能。Linux 内核版本 v2.6.23 中的 CFS 由以下的多个文件组成:

  • include/linux/sched.h
  • kernel/sched_stats.h
  • kernel/sched.c
  • kernel/sched_fair.c
  • kernel/sched_idletask.c
  • kernel/sched_rt.c

通过 CFS 的名字我们就能发现,该调度器的能为不同的进程提供完全公平性。一旦某些进程受到了不公平的待遇,调度器就会运行这些进程,从而维持所有进程运行时间的公平性。这种保证公平性的方式与『水多了加面,面多了加水』有一些相似:

  • 调度器会查找运行队列中受到最不公平待遇的进程,并为进程分配计算资源,分配的计算资源是与其他资源运行时间的差值加上最小能够运行的时间单位;
  • 进程运行结束之后发现运行队列中又有了其他的进程受到了最不公平的待遇,调度器又会运行新的进程;

调度器算法不断计算各个进程的运行时间并依次调度队列中的受到最不公平对待的进程,保证各个进程的运行时间差不会大于最小运行的时间单位。

数据结构

虽然我们还是会延用运行队列这一术语,但是 CFS 的内部已经不再使用队列来存储进程了,cfs_rq 是用来管理待运行进程的新结构体,该结构体会使用红黑树(Red-black tree)替代链表:

struct cfs_rq {
    struct load_weight load;
    unsignedlong nr_running;
    s64 fair_clock;
    u64 exec_clock;
    s64 wait_runtime;
    u64 sleeper_bonus;
    unsignedlong wait_runtime_overruns, wait_runtime_underruns;
    struct rb_root tasks_timeline;
    struct rb_node *rb_leftmost;
    struct rb_node *rb_load_balance_curr;
    struct sched_entity *curr;
    struct rq *rq;
    struct list_head leaf_cfs_rq_list;
};

红黑树(Red-black tree)是平衡的二叉搜索树,红黑树的增删改查操作的最坏时间复杂度为 ,也就是树的高度,树中最左侧的节点 rb_leftmost 运行的时间最短,也是下一个待运行的进程。

注:在最新版本的 CFS 实现中,内核使用虚拟运行时间 vruntime 替代了等待时间,但是基本的调度原理和排序方式没有太多变化。

调度过程

CFS 的调度过程还是由 schedule 函数完成的,该函数的执行过程可以分成以下几个步骤:

  • 关闭当前 CPU 的抢占功能;
  • 如果当前 CPU 的运行队列中不存在任务,调用 idle_balance 从其他 CPU 的运行队列中取一部分执行;
  • 调用 pick_next_task 选择红黑树中优先级最高的任务;
  • 调用 context_switch 切换运行的上下文,包括寄存器的状态和堆栈;
  • 重新开启当前 CPU 的抢占功能。

CFS 的调度过程与  调度器十分类似,当前调度器与前者的区别只是增加了可选的工作窃取机制并改变了底层的数据结构。

调度类

CFS 中的调度类是比较有趣的概念,调度类可以决定进程的调度策略。每个调度类都包含一组负责调度的函数,调度类由如下所示的 sched_class 结构体表示:

struct sched_class {
    struct sched_class *next;
    void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup);
    void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep);
    void (*yield_task) (struct rq *rq, struct task_struct *p);
    void (*check_preempt_curr) (struct rq *rq, struct task_struct *p);
    struct task_struct * (*pick_next_task) (struct rq *rq);
    void (*put_prev_task) (struct rq *rq, struct task_struct *p);
    unsigned long (*load_balance) (struct rq *this_rq, int this_cpu,
            struct rq *busiest,
            unsigned long max_nr_move, unsigned long max_load_move,
            struct sched_domain *sd, enum cpu_idle_type idle,
            int *all_pinned, int *this_best_prio);
    void (*set_curr_task) (struct rq *rq);
    void (*task_tick) (struct rq *rq, struct task_struct *p);
    void (*task_new) (struct rq *rq, struct task_struct *p);
};

调度类中包含任务的初始化、入队和出队等函数,这里的设计与面向对象中的设计稍微有些相似。内核中包含 SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE、SCHED_FIFO 和 SCHED_RR 调度类,这些不同的调度类分别实现了 sched_class 中的函数以提供不同的调度行为。

3. 小结

本节介绍了操作系统调度器的设计原理以及演进的历史,从 2007 年合入 CFS 到现在已经过去了很长时间,目前的调度器也变得更加复杂,社区也在不断改进进程调度器。

我们可以从 Linux 调度器的演进的过程看到主流系统架构的变化,最初几十行代码的调度器就能完成基本的调度功能,而现在要使用几万行代码来完成复杂的调度,保证系统的低延时和高吞吐量。

由于篇幅有限,我们很难对操作系统的调度器进行面面俱到的分析,你可以在 这里 找到作者使用的 Linux 源代码,亲自动手分析不同版本的进程调度器。

4. 延伸阅读

Go 语言

Go 语言是诞生自 2009 年的编程语言,相信很多人对 Go 语言的印象都是语法简单,能够支撑高并发的服务。语法简单是编程语言的顶层设计哲学,而语言的高并发支持依靠的是运行时的调度器,这也是本节将要研究的内容。

对 Go 语言稍微有了解的人都知道,通信顺序进程(Communicating sequential processes,CSP)影响着 Go 语言的并发模型,其中的 Goroutine 和 Channel 分别表示实体和用于通信的媒介。

20.png

图 20 – Go 和 Erlang 的并发模型

『不要通过共享内存来通信,我们应该使用通信来共享内存』不只是 Go 语言鼓励的设计哲学,更为古老的 Erlang 语言其实也遵循了同样的设计,但是 Erlang 选择使用了Actor 模型,我们在这里就不介绍 CSP 和 Actor 的区别和联系的,感兴趣的读者可以在推荐阅读和应引用中找到相关资源。

1. 设计与演进

今天的 Go 语言调度器有着非常优异的性能,但是如果我们回过头重新看 Go 语言的 v0.x 版本的调度器就会发现最初的调度器非常简陋,也无法支撑高并发的服务。整个调度器经过几个大版本的迭代才有了今天的优异性能。

  • 单线程调度器 · 0.x – 源代码。只包含 40 多行代码;只能单线程调度,由 G-M 模型组成;
  • 多线程调度器 · 1.0 – 源代码。引入了多线程调度;全局锁导致竞争严重;
  • 任务窃取调度器 · 1.1 – 源代码。引入了处理器 P,构成了目前的 G-M-P 模型;在处理器 P 的基础上实现了基于工作窃取的调度器;在某些情况下,Goroutine 不会让出线程造成饥饿问题;时间过长的程序暂停(Stop-the-world,STW)会导致程序无法工作;
  • 抢占式调度器 · 1.2 ~ 至今 – 源代码。实现基于信号的真抢占式调度;垃圾回收对栈进行扫描时会触发抢占调度;抢占的时间点不够多,还不能覆盖全部的边缘情况;通过编译器在函数调用时插入检查指令,实现基于协作的抢占式调度;GC 和循环可能会导致 Goroutine 长时间占用资源导致程序暂停;协作的抢占式调度器 – 1.2 ~ 1.13;抢占式调度器 – 1.14 ~ 至今;
  • 非均匀存储访问调度器 · 提案。对运行时中的各种资源进行分区;实现非常复杂,到今天还没有提上日程;

除了多线程、任务窃取和抢占式调度器之外,Go 语言社区目前还有一个非均匀存储访问(Non-uniform memory access,NUMA)调度器的提案,将来有一天可能 Go 语言会实现这个调度器。在这一节中,我们将依次介绍不同版本调度器的实现以及未来可能会实现的调度器提案。

单线程调度器

Go 语言在 0.x 版本调度器中只包含表示 Goroutine 的 G 和表示线程的 M 两种结构体,全局也只有一个线程。我们可以在 clean up scheduler 提交中找到单线程调度器的源代码,在这时 Go 语言的 调度器 还是由 C 语言实现的,调度函数 schedule 中也只包含 40 多行代码 :

static void scheduler(void) {
    G* gp;
    lock(&sched);
    if(gosave(&m->sched)){
        lock(&sched);
        gp = m->curg;
        switch(gp->status){
        case Grunnable:
        case Grunning:
            gp->status = Grunnable;
            gput(gp);
            break;
        ...
        }
        notewakeup(&gp->stopped);
    }
    gp = nextgandunlock();
    noteclear(&gp->stopped);
    gp->status = Grunning;
    m->curg = gp;
    g = gp;
    gogo(&gp->sched);
}

该函数会遵循如下所示的过程执行:

  • 获取调度器的全局锁;
  • 调用 gosave 保存栈寄存器和程序计数器;
  • 调用 nextgandunlock 获取下一个线程 M 需要运行的 Goroutine 并解锁调度器;
  • 修改全局线程 m 上要执行的 Goroutine;
  • 调用 gogo 函数运行最新的 Goroutine。

这个单线程调度器的唯一优点就是能跑,不过从这次提交中我们能看到 G 和 M 两个重要的数据结构,它建立了 Go 语言调度器的框架。

多线程调度器

Go 语言 1.0 版本在正式发布时就支持了多线程的调度器,与上一个版本完全不可用的调度器相比,Go 语言团队在这一阶段完成了从不可用到可用。我们可以在 proc.c 中找到 1.0.1 版本的调度器,多线程版本的调度函数 schedule 包含 70 多行代码,我们在这里保留了其中的核心逻辑:

static void schedule(G *gp) {
    schedlock();
    if(gp != nil) {
        gp->m = nil;
        uint32 v = runtime·xadd(&runtime·sched.atomic, -1<<mcpuShift);
        if(atomic_mcpu(v) > maxgomaxprocs)
            runtime·throw("negative mcpu in scheduler");
        switch(gp->status){
        case Grunning:
            gp->status = Grunnable;
            gput(gp);
            break;
        case ...:
        }
    } else {
        ...
    }
    gp = nextgandunlock();
    gp->status = Grunning;
    m->curg = gp;
    gp->m = m;
    runtime·gogo(&gp->sched, 0);
}

整体的逻辑与单线程调度器没有太多区别,多线程调度器引入了 GOMAXPROCS 变量帮助我们控制程序中的最大线程数,这样我们的程序中就可能同时存在多个活跃线程。

多线程调度器的主要问题是调度时的锁竞争,Scalable Go Scheduler Design Doc 中对调度器做的性能测试发现 14% 的时间都花费在 runtime.futex 函数上,目前的调度器实现有以下问题需要解决:

  • 全局唯一的调度器和全局锁,所有的调度状态都是中心化存储的,带来了锁竞争;
  • 线程需要经常互相传递可运行的 Goroutine,引入了大量的延迟和额外开销;
  • 每个线程都需要处理内存缓存,导致大量的内存占用并影响数据局部性(Data locality);
  • 系统调用频繁阻塞和解除阻塞正在运行的线程,增加了额外开销。

这里的全局锁问题和 Linux 操作系统调度器在早期遇到的问题比较相似,解决方案也都大同小异。

任务窃取调度器

2012 年 Google 的工程师 Dmitry Vyukov 在 Scalable Go Scheduler Design Doc 中指出了现有多线程调度器的问题并在多线程调度器上提出了两个改进的手段:

  • 在当前的 G-M 模型中引入了处理器 P;
  • 在处理器 P 的基础上实现基于工作窃取的调度器。

基于任务窃取的 Go 语言调度器使用了沿用至今的 G-M-P 模型,我们能在 runtime: improved scheduler 提交中找到任务窃取调度器刚被实现时的源代码,调度器的 schedule 函数到现在反而更简单了:

static void schedule(void) {
    G *gp;
 top:
    if(runtime·gcwaiting) {
        gcstopm();
        goto top;
    }
    gp = runqget(m->p);
    if(gp == nil)
        gp = findrunnable();
    ...
    execute(gp);
}
  • 如果当前运行时在等待垃圾回收,调用 gcstopm 函数;
  • 调用 runqget 和 findrunnable 从本地的或者全局的运行队列中获取待执行的 Goroutine;
  • 调用 execute 函数在当前线程 M 上运行 Goroutine。

当前处理器本地的运行队列中不包含 Goroutine 时,调用 findrunnable 函数会触发工作窃取,从其他的处理器的队列中随机获取一些 Goroutine。

运行时 G-M-P 模型中引入的处理器 P 是线程 M 和 Goroutine 之间的中间层,我们从它的结构体中就能看到 P 与 M 和 G 的关系:

struct P {
    Lock;
    uint32  status;  // one of Pidle/Prunning/...
    P*  link;
    uint32  tick;   // incremented on every scheduler or system call
    M*  m;  // back-link to associated M (nil if idle)
    MCache* mcache;
    G** runq;
    int32   runqhead;
    int32   runqtail;
    int32   runqsize;
    G*  gfree;
    int32   gfreecnt;
};

处理器 P 持有一个运行队列 runq,这是由可运行的 Goroutine 组成的数组,它还反向持有一个线程 M 的指针。调度器在调度时会从处理器的队列中选择队列头的 Goroutine 放到线程 M 上执行。如下所示的图片展示了 Go 语言中的线程 M、处理器 P 和 Goroutine 的关系。

21.png

图 21 – G-M-P 模型

基于工作窃取的多线程调度器将每一个线程绑定到了独立的 CPU 上并通过不同处理器分别管理,不同处理器中通过工作窃取对任务进行再分配,提升了调度器和 Go 语言程序的整体性能,今天所有的 Go 语言服务的高性能都受益于这一改动。

抢占式调度器

对 Go 语言并发模型的修改提升了调度器的性能,但是在 1.1 版本中的调度器仍然不支持抢占式调度,程序只能依靠 Goroutine 主动让出 CPU 资源。Go 语言的调度器在1.2 版本中引入了基于协作的抢占式调度解决下面的问题

  • 单独的 Goroutine 可以一直占用线程运行,不会切换到其他的 Goroutine,造成饥饿问题;
  • 垃圾回收需要暂停整个程序(Stop-the-world,STW),如果没有抢占可能需要等待几分钟的时间,导致整个程序无法工作。

然而 1.2 版本中实现的抢占式调度是基于协作的,在很长的一段时间里 Go 语言的调度器都包含一些无法被抢占的边缘情况,直到 1.14 才实现了基于信号的真抢占式调度解决部分问题。

基于协作的抢占式调度

我们可以在 proc.c 文件中找到引入抢占式调度后的调度器实现。Go 语言会在当前的分段栈机制上实现抢占式的调度,所有的 Goroutine 在函数调用时都有机会进入运行时检查是否需要执行抢占。基于协作的抢占是通过以下的多个提交实现的:

  • runtime: mark runtime.goexit as nosplit
  • runtime: add stackguard0 to G。为 Goroutine 引入 stackguard0 字段,当该字段被设置成 StackPreempt 时,Goroutine 会被抢占;
  • runtime: introduce preemption function (not used for now)。引入抢占函数 preemptone 和 preemptall,这两个函数会设置 Goroutine 的 StackPreempt;引入抢占请求 StackPreempt;
  • runtime: preempt goroutines for GC。在垃圾回收调用的 runtime·stoptheworld 中调用 preemptall 函数设置所有处理器上 Goroutine 的 StackPreempt;在 runtime·newstack 函数中增加抢占的代码,当 stackguard0 等于 StackPreempt 时触发调度器的抢占;
  • runtime: preempt long-running goroutines。在系统监控中,如果一个 Goroutine 的运行时间超过 10ms,就会调用 retake 和 preemptone;
  • runtime: more reliable preemption。修复 Goroutine 因为周期性执行非阻塞的 CGO 或者系统调用不会被抢占的问题。

从上述一系列的提交中,我们会发现 Go 语言运行时会在垃圾回收暂停程序、系统监控发现 Goroutine 运行超过 10ms 时提出抢占请求 StackPreempt;因为编译器会在函数调用中插入 runtime.newstack,所以函数调用时会通过 runtime.newstack 检查 Goroutine 的 stackguard0 是否为 StackPreempt 进而触发抢占让出当前线程。

这种做法没有带来运行时的过多额外开销,实现也相对比较简单,不过增加了运行时的复杂度,总体来看还是一种比较成功的实现。因为上述的抢占是通过编译器在特定时机插入函数实现的,还是需要函数调用作为入口才能触发抢占,所以这是一种协作式的抢占式调度。

基于信号的抢占式调度

协作的抢占式调度实现虽然巧妙,但是留下了很多的边缘情况,我们能在 runtime: non-cooperative goroutine preemption 中找到一些遗留问题:

  • runtime: tight loops should be preemptible #10958
  • An empty for{} will block large slice allocation in another goroutine, even with GOMAXPROCS > 1 ? #17174
  • runtime: tight loop hangs process completely after some time #15442

Go 语言在 1.14 版本中实现了非协作的抢占式调度,在实现的过程中我们对已有的逻辑进行重构并为 Goroutine 增加新的状态和字段来支持抢占。Go 团队通过下面提交的实现了这一功能,我们可以顺着提交的顺序理解其实现原理:

  • runtime: add general suspendG/resumeG。挂起 Goroutine 的过程是在栈扫描时完成的,我们通过 runtime.suspendG 和 runtime.resumeG 两个函数重构栈扫描这一过程;调用 runtime.suspendG 函数时会将运行状态的 Goroutine 的 preemptStop 标记成 true;调用 runtime.preemptPark 函数可以挂起当前 Goroutine、将其状态更新成 _Gpreempted 并触发调度器的重新调度,该函数能够交出线程控制权;
  • runtime: asynchronous preemption function for x86。在 x86 架构上增加异步抢占的函数 runtime.asyncPreempt 和 runtime.asyncPreempt2;
  • runtime: use signals to preempt Gs for suspendG。支持通过向线程发送信号的方式暂停运行的 Goroutine;在 runtime.sighandler 函数中注册了 SIGURG 信号的处理函数 runtime.doSigPreempt;runtime.preemptM 函数可以向线程发送抢占请求;
  • runtime: implement async scheduler preemption。修改 runtime.preemptone 函数的实现,加入异步抢占的逻辑。

目前的抢占式调度也只会在垃圾回收扫描任务时触发,我们可以梳理一下触发抢占式调度的过程:

  • 程序启动时,在 runtime.sighandler 函数中注册了 SIGURG 信号的处理函数 runtime.doSigPreempt;
  • 在触发垃圾回收的栈扫描时会调用 runtime.suspendG 函数挂起 Goroutine。将 _Grunning 状态的 Goroutine 标记成可以被抢占,即 preemptStop 设置成 true;调用 runtime.preemptM 函数触发抢占;
  • runtime.preemptM 函数会调用 runtime.signalM 向线程发送信号 SIGURG;
  • 操作系统会中断正在运行的线程并执行预先注册的信号处理函数 runtime.doSigPreempt;
  • runtime.doSigPreempt 函数会处理抢占信号,获取当前的 SP 和 PC 寄存器并调用 runtime.sigctxt.pushCall;
  • runtime.sigctxt.pushCall 会修改寄存器并在程序回到用户态时从 runtime.asyncPreempt 开始执行;
  • 汇编指令 runtime.asyncPreempt 会调用运行时函数 runtime.asyncPreempt2;
  • runtime.asyncPreempt2 会调用 runtime.preemptPark 函数;
  • runtime.preemptPark 会修改当前 Goroutine 的状态到 _Gpreempted 并调用 runtime.schedule 让当前函数陷入休眠并让出线程,调度器会选择其他的 Goroutine 继续执行;

上述 9 个步骤展示了基于信号的抢占式调度的执行过程。我们还需要讨论一下该过程中信号的选择,提案根据以下的四个原因选择 SIGURG 作为触发异步抢占的信号

  • 该信号需要被调试器透传;
  • 该信号不会被内部的 libc 库使用并拦截;
  • 该信号可以随意出现并且不触发任何后果;
  • 我们需要处理多个平台上的不同信号。

目前的抢占式调度也没有解决所有潜在的问题,因为 STW 和栈扫描时更可能出现问题,也是一个可以抢占的安全点(Safe-points),所以我们会在这里先加入抢占功能,在未来可能会加入更多抢占时间点。

非均匀内存访问调度器

非均匀内存访问(Non-uniform memory access,NUMA)调度器目前只是 Go 语言的提案,因为该提案过于复杂,而目前的调度器的性能已经足够优异,所以暂时没有实现该提案。该提案的原理就是通过拆分全局资源,让各个处理器能够就近获取本地资源,减少锁竞争并增加数据局部性。

在目前的运行时中,线程、处理器、网络轮训器、运行队列、全局内存分配器状态、内存分配缓存和垃圾收集器都是全局的资源。运行时没有保证本地化,也不清楚系统的拓扑结构,部分结构可以提供一定的局部性,但是从全局来看没有这种保证。

22.png

图 22 – Go 语言 NUMA 调度器

如上图所示,堆栈、全局运行队列和线程池会按照 NUMA 节点进行分区,网络轮训器和计时器会由单独的处理器持有。这种方式虽然能够利用局部性提高调度器的性能,但是本身的实现过于复杂,所以 Go 语言团队还没有着手实现这一提案。

2. 小结

Go 语言的调度器在最初的几个版本中迅速迭代,但是从 1.2 版本之后调度器就没有太多的变化,直到 1.14 版本引入了真正的抢占式调度解决了自 1.2 以来一直存在的问题。在可预见的未来,Go 语言的调度器还会进一步演进,增加抢占式调度的时间点减少存在的边缘情况。

本节内容选择《Go 语言设计与实现》一书中的 Go 语言调度器实现原理,你可以点击链接了解更多与 Go 语言设计与实现原理相关的内容。

3. 延伸阅读

Kubernetes

Kubernetes 是生产级别的容器调度和管理系统,在过去的一段时间中,Kubernetes 迅速占领市场,成为容器编排领域的实施标准。

23.png

图 23 – 容器编排系统演进

Kubernetes 是希腊语『舵手』的意思,它最开始由 Google 的几位软件工程师创立,深受公司内部Borg 和 Omega 项目的影响,很多设计都是从 Borg 中借鉴的,同时也对 Borg 的缺陷进行了改进,Kubernetes 目前是 Cloud Native Computing Foundation (CNCF) 的项目,也是很多公司管理分布式系统的解决方案

调度器是 Kubernetes 的核心组件,它的主要功能是为待运行的工作负载 Pod 绑定运行的节点 Node。与其他调度场景不同,虽然资源利用率在 Kubernetes 中也非常重要,但是这只是 Kubernetes 关注的一个因素,它需要在容器编排这个场景中支持非常多并且复杂的业务需求,除了考虑 CPU 和内存是否充足,还需要考虑其他的领域特定场景,例如:两个服务不能占用同一台机器的相同端口、几个服务要运行在同一台机器上,根据节点的类型调度资源等。

这些复杂的业务场景和调度需求使 Kubernetes 调度器的内部设计与其他调度器完全不同,但是作为用户应用层的调度器,我们却能从中学到很多有用的模式和设计。接下来,本节将介绍 Kubernetes 中调度器的设计以及演变。

1. 设计与演进

Kubernetes 调度器的演变过程比较简单,我们可以将它的演进过程分成以下的两个阶段:

  • 基于谓词和优先级的调度器 · v1.0.0 ~ v1.14.0
  • 基于调度框架的调度器 · v1.15.0 ~ 至今

Kubernetes 从 v1.0.0 版本发布到 v1.14.0,总共 15 个版本一直都在使用谓词和优先级来管理不同的调度算法,知道 v1.15.0 开始引入调度框架(Alpha 功能)来重构现有的调度器。我们在这里将以 v1.14.0 版本的谓词和优先级和 v1.17.0 版本的调度框架分析调度器的演进过程。

谓词和优先级算法

谓词(Predicates)和优先级(Priorities)调度器是从 Kubernetes v1.0.0 发布时就存在的模式,v1.14.0 的最后实现与最开始的设计也没有太多区别。然而从 v1.0.0 到 v1.14.0 期间也引入了很多改进:

  • 调度器扩展 · v1.2.0 – Scheduler extension。通过调用外部调度器扩展的方式改变调度器的决策;
  • Map-Reduce 优先级算法 · v1.5.0 – MapReduce-like scheduler priority functions。为调度器的优先级算法支持 Map-Reduce 的计算方式,通过引入可并行的 Map 阶段优化调度器的计算性能;
  • 调度器迁移 · v1.10.0 – Move scheduler code out of plugin directory。从 plugin/pkg/scheduler 移到 pkg/scheduler;kube-scheduler 成为对外直接提供的可执行文件;

谓词和优先级都是 Kubernetes 在调度系统中提供的两个抽象,谓词算法使用 FitPredicate 类型,而优先级算法使用 PriorityMapFunction 和 PriorityReduceFunction 两个类型:

type FitPredicate func(pod *v1.Pod, meta PredicateMetadata, nodeInfo *schedulernodeinfo.NodeInfo) (bool, []PredicateFailureReason, error)
type PriorityMapFunction func(pod *v1.Pod, meta interface{}, nodeInfo *schedulernodeinfo.NodeInfo) (schedulerapi.HostPriority, error)
type PriorityReduceFunction func(pod *v1.Pod, meta interface{}, nodeNameToInfo map[string]*schedulernodeinfo.NodeInfo, result schedulerapi.HostPriorityList) error

因为 v1.14.0 也是作者刚开始参与 Kubernetes 开发的第一个版本,所以对当时的设计印象也非常深刻,v1.14.0 的 Kubernetes 调度器会使用 PriorityMapFunction 和 PriorityReduceFunction 这种 Map-Reduce 的方式计算所有节点的分数并从其中选择分数最高的节点。下图展示了,v1.14.0 版本中调度器的执行过程:

24.png

图 24 – 谓词和优先级算法

如上图所示,我们假设调度器中存在一个谓词算法和一个 Map-Reduce 优先级算法,当我们为一个 Pod 在 6 个节点中选择最合适的一个时,6 个节点会先经过谓词的筛选,图中的谓词算法会过滤掉一半的节点,剩余的 3 个节点经过 Map 和 Reduce 两个过程分别得到了 5、10 和 5 分,最终调度器就会选择分数最高的 4 号节点。

genericScheduler.Schedule 是 Kubernetes 为 Pod 选择节点的方法,我们省略了该方法中用于检查边界条件以及打点的代码:

func (g *genericScheduler) Schedule(pod *v1.Pod, nodeLister algorithm.NodeLister) (result ScheduleResult, err error) {
    nodes, err := nodeLister.List()
    if err != nil {
        return result, err
    }
    iflen(nodes) == 0 {
        return result, ErrNoNodesAvailable
    }
    filteredNodes, failedPredicateMap, err := g.findNodesThatFit(pod, nodes)
    if err != nil {
        return result, err
    }
    ...
    priorityList, err := PrioritizeNodes(pod, g.nodeInfoSnapshot.NodeInfoMap, ..., g.prioritizers, filteredNodes, g.extenders)
    if err != nil {
        return result, err
    }
    host, err := g.selectHost(priorityList)
    return ScheduleResult{
        SuggestedHost:  host,
        EvaluatedNodes: len(filteredNodes) + len(failedPredicateMap),
        FeasibleNodes:  len(filteredNodes),
    }, err
}
  • 从 NodeLister 中获取当前系统中存在的全部节点;
  • 调用 genericScheduler.findNodesThatFit 方法并行执行全部的谓词算法过滤节点。谓词算法会根据传入的 Pod 和 Node 对节点进行过滤,这时会过滤掉端口号冲突、资源不足的节点;调用所有调度器扩展的 Filter 方法辅助过滤;
  • 调用 PrioritizeNodes 函数为所有的节点打分。以 Pod 和 Node 作为参数并发执行同一优先级的 PriorityMapFunction;Pod 和优先级返回的 Node 到分数的映射为参数调用 PriorityReduceFunction 函数;调用所有调度器扩展的 Prioritize 方法;将所有分数按照权重相加后返回从 Node 到分数的映射;
  • 调用 genericScheduler.selectHost 方法选择得分最高的节点。

这就是使用谓词和优先级算法时的调度过程,我们在这里省略了调度器的优先队列中的排序,出现调度错误时的抢占以及 Pod 持久存储卷绑定到 Node 上的过程,只保留了核心的调度逻辑。

调度框架

Kubernetes 调度框架是 Babak Salamat 和 Jonathan Basseri 2018 年提出的最新调度器设计,这个提案明确了 Kubernetes 中的各个调度阶段,提供了设计良好的基于插件的接口。调度框架认为 Kubernetes 中目前存在调度(Scheduling)和绑定(Binding)两个循环:

  • 调度循环在多个 Node 中为 Pod 选择最合适的 Node;
  • 绑定循环将调度决策应用到集群中,包括绑定 Pod 和 Node、绑定持久存储等工作。

除了两个大循环之外,调度框架中还包含 QueueSort、PreFilter、Filter、PostFilter、Score、Reserve、Permit、PreBind、Bind、PostBind 和 Unreserve 11 个扩展点(Extension Point),这些扩展点会在调度的过程中触发,它们的运行顺序如下:

25.png

图 25 – Kubernetes 调度框架

我们可以将调度器中的 Scheduler.scheduleOne 方法作为入口分析基于调度框架的调度器实现,每次调用该方法都会完成一遍为 Pod 调度节点的全部流程,我们将该函数的执行过程分成调度和绑定两个阶段,首先是调度器的调度阶段:

func (sched *Scheduler) scheduleOne(ctx context.Context) {
    fwk := sched.Framework
    podInfo := sched.NextPod()
    pod := podInfo.Pod
    state := framework.NewCycleState()
    scheduleResult, _ := sched.Algorithm.Schedule(schedulingCycleCtx, state, pod)
    assumedPod := podInfo.DeepCopy().Pod
    allBound, _ := sched.VolumeBinder.Binder.AssumePodVolumes(assumedPod, scheduleResult.SuggestedHost)
    if err != nil {
        return
    }
    if sts := fwk.RunReservePlugins(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost); !sts.IsSuccess() {
        return
    }
    if err := sched.assume(assumedPod, scheduleResult.SuggestedHost); err != nil {
        fwk.RunUnreservePlugins(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
        return
    }
    ...
}
  • 调用内部优先队列的 MakeNextPodFunc 返回的函数从队列中获取下一个等待调度的 Pod,用于维护等待 Pod 的队列会执行 QueueSort 插件;
  • 调用 genericScheduler.Schedule 函数选择节点,该过程会执行 PreFilter、Filter、PostFilter、Score 四个扩展点的插件;
  • 调用 framework.RunReservePlugins 函数运行 Reserve 插件用于保留资源并进入绑定阶段(绑定阶段运行时间较长,避免资源被抢占)。如果运行失败执行,调用 framework.RunUnreservePlugins 函数运行 Unreserve 插件。

因为每一次调度决策都会改变上下文,所以该阶段 Kubernetes 需要串行执行。而绑定阶段就是实现调度的过程了,我们会创建一个新的 Goroutine 并行执行绑定循环:

func (sched *Scheduler) scheduleOne(ctx context.Context) {
    ...
    gofunc() {
        bindingCycleCtx, cancel := context.WithCancel(ctx)
        defer cancel()
        fwk.RunPermitPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
        if !allBound {
             sched.bindVolumes(assumedPod)
        }
        fwk.RunPreBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
        if err := sched.bind(bindingCycleCtx, assumedPod, scheduleResult.SuggestedHost, state); err != nil {
            fwk.RunUnreservePlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
        } else {
            fwk.RunPostBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
        }
    }()
}
  • 启动一个 Goroutine 并调用 framework.RunPermitPlugin 异步运行 Permit 插件,这个阶段可以用来实现批调度器;
  • 调用 Scheduler.bindVolumes 将卷先绑定到 Node 上;
  • 调用 Scheduler.bind 函数将 Pod 绑定到 Node 上完成调度,绑定的过程会执行 PreBind、Bind 和 PostBind 三个扩展点的插件。

目前的调度框架在 Kubernetes v1.17.0 版本中还是 Alpha 阶段,很多功能还不明确,为了支持更多、更丰富的场景,在接下来的几个版本还可能会做出很多改进,不过调度框架在很长的一段时间中都会是调度器的核心。

2. 小结

本节介绍了 Kubernetes 调度器从 v1.0.0 到最新版本中的不同设计,Kubernetes 调度器中总共存在两种不同的设计,一种是基于谓词和优先级算法的调度器,另一种是基于调度框架的调度器。

很多的业务调度器也需要从多个选项中选出最优的选择,无论是成本最低还是质量最优,我们可以考虑将调度的过程分成过滤和打分两个阶段为调度器建立合适的抽象,过滤阶段会按照需求过滤掉不满足需求的选项,打分阶段可能会按照质量、成本和权重对多个选项进行排序,遵循这种设计思路可以解决很多类似问题。

目前的 Kubernetes 已经通过调度框架详细地支持了多个阶段的扩展方法,几乎是调度器内部实现的最终形态了。不过随着调度器功能的逐渐复杂,未来可能还会遇到更复杂的调度场景,例如:多租户的调度资源隔离、多调度器等功能,而 Kubernetes 社区也一直都在为构建高性能的调度器而努力。

3. 延伸阅读

总结

从操作系统、编程语言到应用程序,我们在这篇文章中分析了 Linux、Go 语言和 Kubernetes 调度器的设计与实现原理,这三个不同的调度器其实有相互依赖的关系:

26.png

图 26 – 三层调度器

如上图所示,Kubernetes 的调度器依赖于 Go 语言的运行时调度器,而 Go 语言的运行时调度器也依赖于 Linux 的进程调度器,从上到下离用户越来越远,从下到上越来越关注具体业务。我们在最后通过两个比较分析一下这几个调度器的异同:

  • Linux 进程调度器与 Go 语言调度器;
  • 系统级调度器(Linux 和 Go)与业务调度器(Kubernetes)。

这是两种不同层面的比较,相信通过不同角度的比较能够让我们对调度器的设计有更深入的认识。

1. Linux 和 Go

首先是 Linux 和 Go 语言调度器,这两个调度器的场景都非常相似,它们最终都是要充分利用机器上的 CPU 资源,所以在实现和演进上有很多相似之处:

  • 调度器的初始版本都非常简单,甚至很简陋,只能支持协作式的调度;
  • 按照运行队列进行分区,通过工作窃取的方式平衡不同 CPU 或者线程上的运行队列;
  • 最终都通过某些方式实现了基于信号的抢占式调度,不过 Go 语言的实现并不完善。

因为场景非常相似,所以它们的目的也非常相似,只是它们调度的任务粒度会有不同,Linux 进程调度器的最小调度单位是线程,而 Go 语言是 Goroutine,与 Linux 进程调度器相比,Go 语言在用户层建立新的模型,实现了另一个调度器,为使用者提供轻量级的调度单位来增强程序的性能,但是它也引入了很多组件来处理系统调用、网络轮训等线程相关的操作,同时组合多个不同粒度的任务导致实现相对复杂。

Linux 调度器的最终设计引入了调度类的概念,让不同任务的类型分别享受不同的调度策略以此来调和低延时和实时性这个在调度上两难的问题。

Go 语言的调度器目前刚刚引入了基于信号的抢占式调度,还有很多功能都不完善。除了抢占式调度之外,复杂的 NUMA 调度器提案也可能是未来 Go 语言的发展方向。

2. 系统和业务

如果我们将系统调度器和业务调度器进行对比的话,你会发现两者在设计差别非常大,毕竟它们处于系统的不同层级。系统调度器考虑的是极致的性能,所以它通过分区的方式将运行队列等资源分离,通过降低锁的粒度来降低系统的延迟;而业务调度器关注的是完善的调度功能,调度的性能虽然十分重要,但是一定要建立在满足特定调度需求之上,而因为业务上的调度需求往往都是比较复杂,所以只能做出权衡和取舍。

正是因为需求的不同,我们会发现不同调度器的演进过程也完全不同。系统调度器都会先充分利用资源,降低系统延时,随后在性能无法优化时才考虑加入调度类等功能满足不同场景下的调度,而 Kubernetes 调度器更关注内部不同调度算法的组织,如何同时维护多个复杂的调度算法,当设计了良好的抽象之后,它才会考虑更加复杂的多调度器、多租户等场景。

3. 最后

这种研究历史变化带来的快乐是很不同的,当我们发现代码发生变化的原因时也会感到欣喜,这让我们站在今天重新见证了历史上的决策,本文中的相应章节已经包含了对应源代码的链接,各位读者可以自行阅读相应内容,也衷心希望各位读者能够有所收获。

延伸阅读

云原生网络研讨会邀您参加

27.png

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术圈。”

数据管理方案Portworx是如何帮助有状态应用做容灾的?

Portworx阅读(714)评论(0)

对于企业用户来说,在容器上跑有状态应用(Stateful Application)的生产系统,容灾备份是不可或缺的。

下面的视频简要介绍了全球领先的容器数据管理解决方案Portworx是如何在Kubernetes上做容灾的。

视频链接:

https://v.qq.com/x/page/m30648ptszh.html

更多相关安装文档请访问:

  • https://docs.portworx.com/portworx-install-with-kubernetes/disaster-recovery/
  • https://docs.portworx.com/portworx-install-with-kubernetes/disaster-recovery/px-metro/
  • https://docs.portworx.com/portworx-install-with-kubernetes/disaster-recovery/async-dr/
  • Disaster Recovery Strategies for Kubernetes/

微服务构建持久API的7大规则

JFrogchina阅读(472)评论(0)

前言

近年来,微服务架构发展迅速,SparkPost就是早期落地微服务架构公司之一,他们发现落地微服务过程中,不光需要考虑服务发现、服务注册、服务调用跟踪链等等架构问题,也需要重视微服务API的变更管理。微服务的一大特性就是独立发布,快速迭代,但前提是足够稳定,他们在使用微服务构建API的过程中就遇到很多问题:

  1. 客户(微服务使用方)经常反馈API 升级变更后不可用,有时影响范围不可控,导致该微服务上线延期,甚至线上故障,违背了微服务初衷
  2. API参数变化或返回结果变化而导致客户端行为不一致,依赖客户端需要大量重构,团队不能专注在创新型工作
  3. API 易用性差, 使用方技术栈不统一,各自进行API抽象及封装,容易出错
  4. 缺少文档及使用引导,需要大量支持工作
  5. 闭门造车,产出微服务往往不能满足需求,运行一段时间就会逐渐废弃

SparkPost经过多年的探索与实践,总结了大量最佳实践,指导他们构建持久稳定的微服务API。现如今,它们的API被成千上万的客户使用,包括Pinterest、Zillow和Intercomto,每月发送超过150亿封电子邮件。

在这篇文章中,我将回顾几个选择和最佳实践。

 

 

七大原则

一、Restful是最好的,但要实用,不需要学究式

首先,也是最重要的一步,我们采取的步骤是决定使用REST作为API。我们的理念是选择以下三个要素作为我们的API的基础:。

  1. HTTP : 这包括响应代码和操作符。操作符包括POST、GET、PUT和DELETE,它们可以映射到基本CRUD(创建、读取、更新、删除)操作。
  2. resources : 这些是HTTP操作人员执行的实体。
  3. JSON (JavaScript对象表示法) : 这是一种通用的数据交换格式。

这三个元素提供了实用REST API所需的一切,包括简单性、可移植性、互操作性和可修改性。在构建了API之后,用户可以轻松地对其进行集成,而不考虑他们的编程语言,包括C#、PHP和Node。Js, Java,甚至是Shell中的CURL。他们可以不用担心潜在的技术发展,包括多种微服务。

当我们创建SparkPost API时,我们试着不要太过学究式地使用纯粹的REST模型,而是选择易于使用。下面是两个可能不遵循RESTful最佳实践的示例:

  1. GET /api/v1/account?include=usage
  2. POST/api/v1/sending-domains/example.domain.com/verify

第一个示例使用查询字符串参数来过滤实体中返回的内容。在第二个示例中,我们在终端名称中使用“verify”这个动词,这可能不符合Restful。我们会讨论每个新的用例,并尽力确保它的一致性和易于使用。

 

二、发展进化并管理变化

我们有许多开发人员和团队在使用我们的API的微服务,并在持续的变更。当工程师确定它已经通过了我们的测试时,我们就会自动将变更部署到生产中。

我们很早就决定让我们的API在使用惯例和如何管理变更方面保持一致。我们建立了一个治理小组,其中包括代表每个团队的工程师、产品管理组的成员和CTO。这个组建立了并强制我们遵守的API约定,并且是完全文档化的。

文档化的约定让我们可以减少不一致,并且更容易定义每个新的端点。以下是我们建立的一些约定:

  • 在单词命名时,URL路径是带有连字符的小写字母,并且区分大小写。
  • URL查询参数和JSON字段也是小写的下划线,并且是大小写敏感的。
  • 请求主体中的非预期查询参数和JSON字段应该被忽略。

治理组还为如何进行更改以及允许哪些类型的更改设置了基本规则。有一些很好的API更改对用户是有益的,并且不会破坏它们的集成,包括:

  • 一个新的API资源、端点或现有资源上的操作。
  • 一个新的可选参数或JSON字段。
  • 在JSON响应主体中返回的新字段。

相反,一个破坏性的变化包括任何可能破坏用户集成的东西,比如:

  • 更改字段的数据类型。
  • 一个新的必需参数或JSON 字段。
  • 删除现有端点或请求方法。
  • 现有资源方法的实质性行为差异,例如将选项的默认值改为“true”

 

三、做任何修改时不要制造破坏 

即使它们是修复bug或不一致的结果,也应该避免发生修改。通常在这种特殊的情况下运行比破坏与客户端的集成风险更大。如果变化是多样的,我们会非常谨慎,寻找其他方法来实现我们的目标。有时可以通过简单地允许用户通过帐户设置或API参数更改其行为来实现。

然而,总会有一种情况引入变化对我们用户的利益胜过任何潜在的不利因素,将引入的变化。但是在这些情况下,我们遵循了这些最佳实践:

  • 我们分析了API日志,以了解更改可能会影响多少用户。
  • 我们给用户至少30到60天的提前警告。
  • 我们发了一封邮件或发表了一篇博客文章,里面包含了关于改变的详细信息以及我们为什么要做这些改变。
  • 我们在API文档中提供了升级指导。

 

四、“一个版本”规则

在过去的三年里,我们对API进行了数千次的修改,现在仍然是第一个版本。我们很早就决定不将API的版本超过第一个版本,因为这样做会增加不必要的复杂性,从而减慢用户对我们最新和最强大功能的使用。对API的版本控制也会减缓开发和测试,让监控变得复杂,让用户文档变得混乱。

另外,我们的API没有版本控制,这意味着我们可以避免围绕主题的争论。有三种方法可以实现API的版本,所有这些都有潜在的缺陷:

  • 把这个版本放到URL中: 容易做,但是从语义的角度来看是一个不好的选择,因为这个实体在v1和v2之间没有变化。
  • 添加一个自定义的标题: 也很容易做,但是语义不强。
  • 在accept标头中放置这个版本: 语义强但是最复杂的方法。

 

五、使用客户端库来帮助非javascript用户

我们的一些用户更喜欢Python、c#、Java或PHP而不是JavaScript。我们通过维护客户端库(为其代码提供易于使用的函数库)将API集成到应用程序中,使其快速进行集成。

随着时间的推移,我们的客户库已经发生了变化,我们也做了相应的版本。我们已经了解到,在包装一个不断增长的API时,抽象是很困难的,所以我们专注于提供一层薄薄的抽象,并使用一些语法快捷方式来简化我们API的使用。这样做可以让我们的用户快速地访问我们任何API,并且具有许多灵活性

 

六、“文档优先”的策略

我们将我们的文档视为代码,并在编写或更改一个API代码行之前使用它来记录我们的API更改。这样做可以帮助我们执行我们的约定,使所有事情保持一致,并保持良好的客户体验。它还削减了支持成本。

我们在GitHub中维护我们的文档,这使得技术和非技术用户可以很容易地做出更改。我们还发现,更容易审查变更的方式。我们使用API Blueprint Markdown格式和Jekyll生成HTML文档,以及一个名为Algolia的强大搜索服务。这样做让我们能够提供更好的客户体验,包括移动设备。

对于那些不想“滚动升级自己”文档的人来说,我们推荐OpenAPI(以前称为“Swagger”)、Apiary和API Blueprint。避免使用不适合REST API文档的工具是很重要的。我们建议在文档中包含一个亮橙色的“在Postman中运行”的按钮,这样可以很容易地试用一个API,以及成功和失败场景的例子。

 

七、听取用户的意见

最后,我们建议所有开发人员注意他们的用户的反馈。SparkPost有一个社区Slack的频道,成千上万的用户可以方便地联系我们的产品、支持、工程和执行管理团队的成员。我们也有一个专门的开发人员关系团队,他们专注于与开发人员社区的合作。所有这些都让我们能更好倾听用户的意见,并将他们的反馈整合到我们的API中。

 

总结

随着微服务架构的发展,微服务快速增长,有的企业内部运维了超过1000的微服务,且仍在不断增长,每个微服务包含数十API,如何持续管理微服务API 变化将成为企业的关注点,SparkPost 根据这些规则和最佳实践,为他们的业务从提供现场电子邮件基础设施到以完全基于云计算的电子邮件发送服务提供了坚实的基础。

 

原文链接:

https://devops.com/7-principles-for-using-microservices-to-build-an-api-that-lasts/

 

 

更多技术分享请关注公众号:JFrog杰蛙DevOps

 

211日在线课堂:《容器持续交付流水线最佳实践》

课堂收益:

1.基于 Jenkins Pipeline,搭建 Docker 容器自动化持续交付流水线。

  1. 通过各阶段自动化及手工测试的结果,形成多维度质量关卡,保障发布包质量。
  2. 开发测试镜像和生产镜像分库管理,通过质量关卡的镜像升级到生产库,生产库的镜像权限得到保障,避免镜像被篡改。

4.为 Docker 镜像构建进行持续的深度漏洞扫描,保证线上应用服务的安全。

  1. 为应用依赖的数据库等有状态服务提供自动化地数据库升级流程,规范自动化 CI/CD 流水线。

报名链接:https://www.bagevent.com/event/6358324

数据管理方案Portworx在K8S上是如何支撑有状态应用的?

Portworx阅读(685)评论(0)

在Kubernetes上运行Portworx,支撑有状态应用(Stateful Application)的基本工作原理
视频链接:
https://v.qq.com/x/page/q30632nf9fo.html

本视频介绍了Portworx作为Kubernetes上最领先的数据管理解决方案,是如何在Kubernetes上工作的。

Portworx安装的详细文档请访问:

https://docs.portworx.com/portworx-install-with-kubernetes/

https://www.katacoda.com/portworx/scenarios/deploy-px-k8s

https://install.portworx.com/

https://docs.portworx.com/cloud-references/auto-disk-provisioning/

从零开始入门 K8s | Kubernetes 存储架构及插件使用

alicloudnative阅读(3924)评论(0)

作者 | 阚俊宝  阿里巴巴高级技术专家

本文整理自《CNCF x Alibaba 云原生技术公开课》第 21 讲。
关注“阿里巴巴云原生”公众号,回复关键词“入门”,即可下载从零入门 K8s 系列文章 PPT。

导读:容器存储是 Kubernetes 系统中提供数据持久化的基础组件,是实现有状态服务的重要保证。Kubernetes 默认提供了主流的存储卷接入方案(In-Tree),同时也提供了插件机制(Out-Of-Tree),允许其他类型的存储服务接入 Kubernetes 系统服务。本文将从 Kubernetes 存储架构、存储插件原理、实现等方面进行讲解,希望大家有所收获。

一、Kubernetes 存储体系架构

引例: 在 Kubernetes 中挂载一个 Volume

首先以一个 Volume 的挂载例子来作为引入。

如下图所示,左边的 YAML 模板定义了一个 StatefulSet 的一个应用,其中定义了一个名为 disk-pvc 的 volume,挂载到 Pod 内部的目录是 /data。disk-pvc 是一个 PVC 类型的数据卷,其中定义了一个 storageClassName。

因此这个模板是一个典型的动态存储的模板。右图是数据卷挂载的过程,主要分为 6 步:

1.png

  • 第一步:用户创建一个包含 PVC的 Pod;
  • 第二步:PV Controller 会不断观察 ApiServer,如果它发现一个 PVC 已经创建完毕但仍然是未绑定的状态,它就会试图把一个 PV 和 PVC 绑定;

PV Controller 首先会在集群内部找到一个适合的 PV 进行绑定,如果未找到相应的 PV,就调用 Volume Plugin 去做 Provision。Provision 就是从远端上一个具体的存储介质创建一个 Volume,并且在集群中创建一个 PV 对象,然后将此 PV 和 PVC 进行绑定;

  • 第三步:通过 Scheduler 完成一个调度功能;

我们知道,当一个 Pod 运行的时候,需要选择一个 Node,这个节点的选择就是由 Scheduler 来完成的。Scheduler 进行调度的时候会有多个参考量,比如 Pod 内部所定义的 nodeSelector、nodeAffinity 这些定义以及 Volume 中所定义的一些标签等。

我们可以在数据卷中添加一些标签,这样使用这个 pv 的 Pod 就会由于标签的限制,被调度器调度到期望的节点上。

  • 第四步:如果有一个 Pod 调度到某个节点之后,它所定义的 PV 还没有被挂载(Attach),此时 AD Controller 就会调用 VolumePlugin,把远端的 Volume 挂载到目标节点中的设备上(如:/dev/vdb);
  • 第五步:当 Volum Manager 发现一个 Pod 调度到自己的节点上并且 Volume 已经完成了挂载,它就会执行 mount 操作,将本地设备(也就是刚才得到的 /dev/vdb)挂载到 Pod 在节点上的一个子目录中。同时它也可能会做一些像格式化、是否挂载到 GlobalPath 等这样的附加操作。
  • 第六步:绑定操作,就是将已经挂载到本地的 Volume 映射到容器中。

Kubernetes 的存储架构

接下来,我们一起看一下 Kubernetes 的存储架构。

2.png

  • PV Controller: 负责 PV/PVC 的绑定、生命周期管理,并根据需求进行数据卷的 Provision/Delete 操作;
  • AD Controller:负责存储设备的 Attach/Detach 操作,将设备挂载到目标节点;
  • Volume Manager:管理卷的 Mount/Unmount 操作、卷设备的格式化以及挂载到一些公用目录上的操作;
  • Volume Plugins:它主要是对上面所有挂载功能的实现;

PV Controller、AD Controller、Volume Manager 主要是进行操作的调用,而具体操作则是由 Volume Plugins 实现的。

  • Scheduler:实现对 Pod 的调度能力,会根据一些存储相关的的定义去做一些存储相关的调度;

接下来,我们分别介绍上面这几部分的功能。

PV Controller

首先我们先来回顾一下几个基本概念:

  • Persistent Volume (PV): 持久化存储卷,详细定义了预挂载存储空间的各项参数;

例如,我们去挂载一个远端的 NAS 的时候,这个 NAS 的具体参数就要定义在 PV 中。一个 PV 是没有 NameSpace 限制的,它一般由 Admin 来创建与维护;

  • Persistent Volume Claim (PVC):持久化存储声明;

它是用户所使用的存储接口,对存储细节无感知,主要是定义一些基本存储的 Size、AccessMode 参数在里面,并且它是属于某个 NameSpace 内部的。

  • StorageClass:存储类;

一个动态存储卷会按照 StorageClass 所定义的模板来创建一个 PV,其中定义了创建模板所需要的一些参数和创建 PV 的一个 Provisioner(就是由谁去创建的)。

PV Controller 的主要任务就是完成 PV、PVC 的生命周期管理,比如创建、删除 PV 对象,负责 PV、PVC 的状态迁移;另一个任务就是绑定 PVC 与 PV 对象,一个 PVC 必须和一个 PV 绑定后才能被应用使用,它们是一一绑定的,一个 PV 只能被一个 PVC 绑定,反之亦然。

接下来,我们看一下一个 PV 的状态迁移图。

3.png

创建好一个 PV 以后,我们就处于一个 Available 的状态,当一个 PVC 和一个 PV 绑定的时候,这个 PV 就进入了 Bound 的状态,此时如果我们把 PVC 删掉,Bound 状态的 PV 就会进入 Released 的状态。

一个 Released 状态的 PV 会根据自己定义的 ReclaimPolicy 字段来决定自己是进入一个 Available 的状态还是进入一个 Deleted 的状态。如果 ReclaimPolicy 定义的是 “recycle” 类型,它会进入一个 Available 状态,如果转变失败,就会进入 Failed 的状态。

相对而言,PVC 的状态迁移图就比较简单。

4.png

一个创建好的 PVC 会处于 Pending 状态,当一个 PVC 与 PV 绑定之后,PVC 就会进入 Bound 的状态,当一个 Bound 状态的 PVC 的 PV 被删掉之后,该 PVC 就会进入一个 Lost 的状态。对于一个 Lost 状态的 PVC,它的 PV 如果又被重新创建,并且重新与该 PVC 绑定之后,该 PVC 就会重新回到 Bound 状态。

下图是一个 PVC 去绑定 PV 时对 PV 筛选的一个流程图。就是说一个 PVC 去绑定一个 PV 的时候,应该选择一个什么样的 PV 进行绑定。

5.png

  • 首先它会检查 VolumeMode 这个标签,PV 与 PVC 的 VolumeMode 标签必须相匹配。VolumeMode 主要定义的是我们这个数据卷是文件系统 (FileSystem) 类型还是一个块 (Block) 类型;
  • 第二个部分是 LabelSelector。当 PVC 中定义了 LabelSelector 之后,我们就会选择那些有 Label 并且与 PVC 的 LabelSelector 相匹配的 PV 进行绑定;
  • 第三个部分是 StorageClassName 的检查。如果 PVC 中定义了一个 StorageClassName,则必须有此相同类名的 PV 才可以被筛选中。

这里再具体解释一下 StorageClassName 这个标签,该标签的目的就是说,当一个 PVC 找不到相应的 PV 时,我们就会用该标签所指定的 StorageClass 去做一个动态创建 PV 的操作,同时它也是一个绑定条件,当存在一个满足该条件的 PV 时,就会直接使用现有的 PV,而不再去动态创建。

  • 第四个部分是 AccessMode 检查。

AccessMode 就是平时我们在 PVC 中定义的如 “ReadWriteOnce”、”RearWriteMany” 这样的标签。该绑定条件就是要求 PVC 和 PV 必须有匹配的 AccessMode,即 PVC 所需求的 AccessMode 类型,PV 必须具有。

  • 最后一个部分是 Size 的检查。

一个 PVC 的 Size 必须小于等于 PV 的 Size,这是因为 PVC 是一个声明的 Volume,实际的 Volume 必须要大于等于声明的 Volume,才能进行绑定。

接下来,我们看一个 PV Controller 的一个实现。

PV Controller 中主要有两个实现逻辑:一个是 ClaimWorker;一个是 VolumeWorker。

ClaimWorker 实现的是 PVC 的状态迁移。

6.png

通过系统标签 “pv.kubernetes.io/bind-completed” 来标识一个 PVC 的状态。

  • 如果该标签为 True,说明我们的 PVC 已经绑定完成,此时我们只需要去同步一些内部的状态;
  • 如果该标签为 False,就说明我们的 PVC 处于未绑定状态。

这个时候就需要检查整个集群中的 PV 去进行筛选。通过 findBestMatch 就可以去筛选所有的 PV,也就是按照之前提到的五个绑定条件来进行筛选。如果筛选到 PV,就执行一个 Bound 操作,否则就去做一个 Provision 的操作,自己去创建一个 PV。

再看 VolumeWorker 的操作。它实现的则是 PV 的状态迁移。

7.png

通过 PV 中的 ClaimRef 标签来进行判断,如果该标签为空,就说明该 PV 是一个 Available 的状态,此时只需要做一个同步就可以了;如果该标签非空,这个值是 PVC 的一个值,我们就会去集群中查找对应的 PVC。如果存在该 PVC,就说明该 PV 处于一个 Bound 的状态,此时会做一些相应的状态同步;如果找不到该 PVC,就说明该 PV 处于一个绑定过的状态,相应的 PVC 已经被删掉了,这时 PV 就处于一个 Released 的状态。此时再根据 ReclaimPolicy 是否是 Delete 来决定是删掉还是只做一些状态的同步。

以上就是 PV Controller 的简要实现逻辑。

AD Controller

AD Controller 是 Attach/Detach Controller 的一个简称。

它有两个核心对象,即 DesiredStateofWorld 和 ActualStateOfWorld。

  • DesiredStateofWorld 是集群中预期要达到的数据卷的挂载状态;
  • ActualStateOfWorld 则是集群内部实际存在的数据卷挂载状态。

它有两个核心逻辑,desiredStateOfWorldPopulator 和 Reconcile。

  • desiredStateOfWorldPopulator 主要是用来同步集群的一些数据以及 DSW、ASW 数据的更新,它会把集群里面,比如说我们创建一个新的 PVC、创建一个新的 Pod 的时候,我们会把这些数据的状态同步到 DSW 中;
  • Reconcile 则会根据 DSW 和 ASW 对象的状态做状态同步。它会把 ASW 状态变成 DSW 状态,在这个状态的转变过程中,它会去执行 Attach、Detach 等操作。

下面这个表分别给出了 desiredStateOfWorld 以及 actualStateOfWorld 对象的一个具体例子。

  • desiredStateOfWorld 会对每一个 Worker 进行定义,包括 Worker 所包含的 Volume 以及一些试图挂载的信息;
  • actualStateOfWorl 会把所有的 Volume 进行一次定义,包括每一个 Volume 期望挂载到哪个节点上、挂载的状态是什么样子的等等。

8.png

下图是 AD Controller 实现的逻辑框图。

从中我们可以看到,AD Controller 中有很多 Informer,Informer 会把集群中的 Pod 状态、PV 状态、Node 状态、PVC 状态同步到本地。

在初始化的时候会调用 populateDesireStateofWorld 以及 populateActualStateofWorld 将 desireStateofWorld、actualStateofWorld 两个对象进行初始化。

在执行的时候,通过 desiredStateOfWorldPopulator 进行数据同步,即把集群中的数据状态同步到 desireStateofWorld 中。reconciler 则通过轮询的方式把 actualStateofWorld 和 desireStateofWorld 这两个对象进行数据同步,在同步的时候,会通过调用 Volume Plugin 进行 attach 和 detach 操作,同时它也会调用 nodeStatusUpdater 对 Node 的状态进行更新。

9.png

以上就是 AD Controller 的简要实现逻辑。

Volume Manager

Volume Manager 实际上是 Kubelet 中一部分,是 Kubelet 中众多 Manager 的一个。它主要是用来做本节点 Volume 的 Attach/Detach/Mount/Unmount 操作。

它和 AD Controller 一样包含有 desireStateofWorld 以及 actualStateofWorld,同时还有一个 volumePluginManager 对象,主要进行节点上插件的管理。在核心逻辑上和 AD Controller 也类似,通过 desiredStateOfWorldPopulator 进行数据的同步以及通过 Reconciler 进行接口的调用。

这里我们需要讲一下 Attach/Detach 这两个操作:

之前我们提到 AD Controller 也会做 Attach/Detach 操作,所以到底是由谁来做呢?我们可以通过 “–enable-controller-attach-detach” 标签进行定义,如果它为 True,则由 AD Controller 来控制;若为 False,就由 Volume Manager 来做。

它是 Kubelet 的一个标签,只能定义某个节点的行为,所以如果假设一个有 10 个节点的集群,它有 5 个节点定义该标签为 False,说明这 5 个节点是由节点上的 Kubelet 来做挂载,而其它 5 个节点是由 AD Controller 来做挂载。

下图是 Volume Manager 实现逻辑图。

10.png

我们可以看到,最外层是一个循环,内部则是根据不同的对象,包括 desireStateofWorld, actualStateofWorld 的不同对象做一个轮询。

例如,对 actualStateofWorld 中的 MountedVolumes 对象做轮询,对其中的某一个 Volume,如果它同时存在于 desireStateofWorld,这就说明实际的和期望的 Volume 均是处于挂载状态,因此我们不会做任何处理。如果它不存在于 desireStateofWorld,说明期望状态中该 Volume 应该处于 Umounted 状态,就执行 UnmountVolume,将其状态转变为 desireStateofWorld 中相同的状态。

所以我们可以看到:实际上,该过程就是根据 desireStateofWorld 和 actualStateofWorld 的对比,再调用底层的接口来执行相应的操作,下面的 desireStateofWorld.UnmountVolumes 和 actualStateofWorld.AttachedVolumes 的操作也是同样的道理。

Volume Plugins

我们之前提到的 PV Controller、AD Controller 以及 Volume Manager 其实都是通过调用 Volume Plugin 提供的接口,比如 Provision、Delete、Attach、Detach 等去做一些 PV、PVC 的管理。而这些接口的具体实现逻辑是放在 VolumePlugin 中的

根据源码的位置可将 Volume Plugins 分为 In-Tree 和 Out-of-Tree 两类:

  • In-Tree 表示源码是放在 Kubernetes 内部的,和 Kubernetes 一起发布、管理与迭代,缺点及时迭代速度慢、灵活性差;
  • Out-of-Tree 类的 Volume Plugins 的代码独立于 Kubernetes,它是由存储商提供实现的,目前主要有 Flexvolume 和 CSI 两种实现机制,可以根据存储类型实现不同的存储插件。所以我们比较推崇 Out-of-Tree 这种实现逻辑。

从位置上我们可以看到,Volume Plugins 实际上就是 PV Controller、AD Controller 以及 Volume Manager 所调用的一个库,分为 In-Tree 和 Out-of-Tree 两类 Plugins。它通过这些实现来调用远端的存储,比如说挂载一个 NAS 的操作 “mount -t nfs *“,该命令其实就是在 Volume Plugins 中实现的,它会去调用远程的一个存储挂载到本地。

11.png

从类型上来看,Volume Plugins 可以分为很多种。In-Tree 中就包含了 几十种常见的存储实现,但一些公司的自己定义私有类型,有自己的 API 和参数,公共存储插件是无法支持的,这时就需要 Out-of-Tree 类的存储实现,比如 CSI、FlexVolume。

12.png

Volume Plugins 的具体实现会放到后面去讲。这里主要看一下 Volume Plugins 的插件管理。

Kubernetes会在 PV Controller、AD Controller 以及 Volume Manager 中来做插件管理。通过 VolumePlguinMg 对象进行管理。主要包含 Plugins 和 Prober 两个数据结构。

Plugins 主要是用来保存 Plugins 列表的一个对象,而 Prober 是一个探针,用于发现新的 Plugin,比如 FlexVolume、CSI 是扩展的一种插件,它们是动态创建和生成的,所以一开始我们是无法预知的,因此需要一个探针来发现新的 Plugin。

下图是插件管理的整个过程。

13.png

PV Controller、AD Controller 以及 Volume Manager 在启动的时候会执行一个 InitPlugins 方法来对 VolumePluginsMgr 做一些初始化。

它首先会将所有 In-Tree 的 Plugins 加入到我们的插件列表中。同时会调用 Prober 的 init 方法,该方法会首先调用一个 InitWatcher,它会时刻观察着某一个目录 (比如图中的 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/),当这个目录每生成一个新文件的时候,也就是创建了一个新的 Plugins,此时就会生成一个新的 FsNotify.Create 事件,并将其加入到 EventsMap 中;同理,如果删除了一个文件,就生成一个 FsNotify.Remove 事件加入到 EventsMap 中。

当上层调用 refreshProbedPlugins 时,Prober 就会把这些事件进行一个更新,如果是 Create,就将其添加到插件列表;如果是 Remove,就从插件列表中删除一个插件。

以上就是 Volume Plugins 的插件管理机制。

Kubernetes 存储卷调度

我们之前说到 Pod 必须被调度到某个 Worker 上才能去运行。在调度 Pod 时,我们会使用不同的调度器来进行筛选,其中有一些与 Volume 相关的调度器。例如 VolumeZonePredicate、VolumeBindingPredicate、CSIMaxVolumLimitPredicate 等。

VolumeZonePredicate 会检查 PV 中的 Label,比如 failure-domain.beta.kubernetes.io/zone 标签,如果该标签定义了 zone 的信息,VolumeZonePredicate 就会做相应的判断,即必须符合相应的 zone 的节点才能被调度。

比如下图左侧的例子,定义了一个 label 的 zone 为 cn-shenzhen-a。右侧的 PV 则定义了一个 nodeAffinity,其中定义了 PV 所期望的节点的 Label,该 Label 是通过 VolumeBindingPredicate 进行筛选的。

14.png

存储卷具体调度信息的实现可以参考《从零开始入门 K8s | 应用存储和持久化数据卷:存储快照与拓扑调度》,这里会有一个更加详细的介绍。

二、Flexvolume 介绍及使用

Flexvolume 是 Volume Plugins 的一个扩展,主要实现 Attach/Detach/Mount/Unmount 这些接口。我们知道这些功能本是由 Volume Plugins 实现的,但是对于某些存储类型,我们需要将其扩展到 Volume Plugins 以外,所以我们需要把接口的具体实现放到外面。

在下图中我们可以看到,Volume Plugins 其实包含了一部分 Flexvolume 的实现代码,但这部分代码其实只有一个 “Proxy”的功能。

比如当 AD Controller 调用插件的一个 Attach 时,它首先会调用 Volume Plugins 中 Flexvolume 的 Attach 接口,但这个接口只是把调用转到相应的 Flexvolume 的Out-Of-Tree实现上。

Flexvolume是可被 Kubelet 驱动的可执行文件,每一次调用相当于执行一次 shell 的 ls 这样的脚本,都是可执行文件的命令行调用,因此它不是一个常驻内存的守护进程。

Flexvolume 的 Stdout 作为 Kubelet 调用的返回结果,这个结果需要是 JSON 格式。

Flexvolume默认的存放地址为 “/usr/libexec/kubernetes/kubelet-plugins/volume/exec/alicloud~disk/disk”。

15.png

下面是一个命令格式和调用的实例。

16.png

Flexvolume 的接口介绍

Flexvolum 包含以下接口:

  • init: 主要做一些初始化的操作,比如部署插件、更新插件的时候做 init 操作,返回的时候会返回刚才我们所说的 DriveCapabilities 类型的数据结构,用来说明我们的 Flexvolume 插件有哪些功能;
  • GetVolumeName: 返回插件名;
  • Attach: 挂载功能的实现。根据 –enable-controller-attach-detach 标签来决定是由 AD Controller 还是 Kubelet 来发起挂载操作;
  • WaitforAttach: Attach 经常是异步操作,因此需要等待挂载完成,才能需要进行下面的操作;
  • MountDevice:它是 mount 的一部分。这里我们将 mount 分为 MountDevice 和 SetUp 两部分,MountDevice 主要做一些简单的预处理工作,比如将设备格式化、挂载到 GlobalMount 目录中等;
  • GetPath:获取每个 Pod 对应的本地挂载目录;
  • Setup:使用 Bind 方式将 GlobalPath 中的设备挂载到 Pod 的本地目录;
  • TearDownUnmountDeviceDetach 实现的是上面一些借口的逆过程;
  • ExpandVolumeDevice:扩容存储卷,由 Expand Controller 发起调用;
  • NodeExpand: 扩容文件系统,由 Kubelet 发起调用。

上面这些接口不一定需要全部实现,如果某个接口没有实现的话,可以将返回结果定义成:

{
    "status": "Not supported",
    "message": "error message"
}

告诉调用者没有实现这个接口。此外,Volume Plugins 中的 Flexvolume 接口除了作为一个 Proxy 外,它也提供了一些默认实现,比如 Mount 操作。所以如果你的 Flexvolume 中没有定义该接口,该默认实现就会被调用。

在定义 PV 时可以通过 secretRef 字段来定义一些 secret 的功能。比如挂载时所需的用户名和密码,就可以通过 secretRef 传入。

Flexvolume 的挂载分析

从挂载流程和卸载流程两个方向来分析 Flexvolume 的挂载过程。

17.png

我们首先看 Attach 操作,它调用了一个远端的 API 把我们的 Storage 挂载到目标节点中的某个设备上去。然后通过 MountDevice 将本地设备挂载到 GlobalPath 中,同时也会做一些格式化这样的操作。Mount 操作(SetUp),它会把 GlobalPath 挂载 PodPath 中,PodPath 就是 Pod 启动时所映射的一个目录。

下图给出了一个例子,比如我们一个云盘,其 Volume ID 为 d-8vb4fflsonz21h31cmss,在执行完 Attach 和 WaitForAttach 操作之后,就会将其挂载到目标节点上的 /dec/vdc 设备中。执行 MountDevice 之后,就会把上述设备格式化,挂载到一个本地的 GlobalPath 中。而执行完 Mount 之后,就会将 GlobalPath 映射到 Pod 相关的一个子目录中。最后执行 Bind 操作,将我们的本地目录映射到容器中。这样完成一次挂载过程。

18.png

卸载流程就是一个逆过程。上述过程描述的是一个块设备的挂载过程,对于文件存储类型,就无需 Attach、MountDevice操作,只需要 Mount 操作,因此文件系统的 Flexvolume 实现较为简单,只需要 Mount 和 Unmount 过程即可。

Flexvolume 的代码示例

19.png

其中主要实现的是 init()、doMount()、doUnmount() 方法。在执行该脚本的时候对传入的参数进行判断来决定执行哪一个命令。

在 Github 上还有很多 Flexvolume 的示例,大家可以自行参考查阅。阿里云提供了一个 Flexvolume 的实现,有兴趣的可以参考一下。

Flexvolume 的使用

下图给出了一个 Flexvolume 类型的 PV 模板。它和其它模板实际上没有什么区别,只不过类型被定义为 flexVolume 类型。flexVolume 中定义了 driver、fsType、options。

  • driver 定义的是我们实现的某种驱动,比如图中的是 aliclound/disk,也可以是 aliclound/nas 等;
  • fsType 定义的是文件系统类型,比如 “ext4″;
  • options 包含了一些具体的参数,比如定义云盘的 id 等。

我们也可以像其它类型一样,通过 selector 中的 matchLabels 定义一些筛选条件。同样也可以定义一些相应的调度信息,比如定义 zone 为 cn-shenzhen-a。

20.png

下面是一个具体的运行结果。在 Pod 内部我们挂载了一个云盘,其所在本地设备为 /dev/vdb。通过 mount | grep disk 我们可以看到相应的挂载目录,首先它会将 /dev/vdb 挂载到 GlobalPath 中;其次会将 GlobalPath 通过 mount 命令挂载到一个 Pod 所定义的本地子目录中去;最后会把该本地子目录映射到 /data 上。

21.png

三、CSI 介绍及使用

和 Flexvolume 类似,CSI 也是为第三方存储提供数据卷实现的抽象接口。

有了 Flexvolume,为何还要 CSI 呢?

Flexvolume 只是给 kubernetes 这一个编排系统来使用的,而 CSI 可以满足不同编排系统的需求,比如 Mesos,Swarm。

其次 CSI 是容器化部署,可以减少环境依赖,增强安全性,丰富插件的功能。我们知道,Flexvolume 是在 host 空间一个二进制文件,执行 Flexvolum 时相当于执行了本地的一个 shell 命令,这使得我们在安装 Flexvolume 的时候需要同时安装某些依赖,而这些依赖可能会对客户的应用产生一些影响。因此在安全性上、环境依赖上,就会有一个不好的影响。

同时对于丰富插件功能这一点,我们在 Kubernetes 生态中实现 operator 的时候,经常会通过 RBAC 这种方式去调用 Kubernetes 的一些接口来实现某些功能,而这些功能必须要在容器内部实现,因此像 Flexvolume 这种环境,由于它是 host 空间中的二进制程序,就没法实现这些功能。而 CSI 这种容器化部署的方式,可以通过 RBAC 的方式来实现这些功能。

CSI 主要包含两个部分:CSI Controller Server 与 CSI Node Server。

  • Controller Server 是控制端的功能,主要实现创建、删除、挂载、卸载等功能;
  • Node Server 主要实现的是节点上的 mount、Unmount 功能。

下图给出了 CSI 接口通信的描述。CSI Controller Server 和 External CSI SideCar 是通过 Unix Socket 来进行通信的,CSI Node Server 和 Kubelet 也是通过 Unix Socket 来通信,之后我们会讲一下 External CSI SiderCar 的具体概念。

22.png

下图给出了 CSI 的接口。主要分为三类:通用管控接口、节点管控接口、中心管控接口。

  • 通用管控接口主要返回 CSI 的一些通用信息,像插件的名字、Driver 的身份信息、插件所提供的能力等;
  • 节点管控接口的 NodeStageVolume 和 NodeUnstageVolume 就相当于 Flexvolume 中的 MountDevice 和 UnmountDevice。NodePublishVolume 和 NodeUnpublishVolume 就相当于 SetUp 和 TearDown 接口;
  • 中心管控接口的 CreateVolume 和 DeleteVolume 就是我们的 Provision 和 Delete 存储卷的一个接口,ControllerPublishVolume 和 ControllerUnPublishVolume 则分别是 Attach 和 Detach 的接口。

23.png

CSI 的系统结构

CSI 是通过 CRD 的形式实现的,所以 CSI 引入了这么几个对象类型:VolumeAttachment、CSINode、CSIDriver 以及 CSI Controller Server 与 CSI Node Server 的一个实现。

24.png

在 CSI Controller Server 中,有传统的类似 Kubernetes 中的 AD Controller 和 Volume Plugins,VolumeAttachment 对象就是由它们所创建的。

此外,还包含多个 External Plugin组件,每个组件和 CSI Plugin 组合的时候会完成某种功能。比如:

  • External Provisioner 和 Controller Server 组合的时候就会完成数据卷的创建与删除功能;
  • External Attacher 和 Controller Server 组合起来可以执行数据卷的挂载和操作;
  • External Resizer 和 Controller Server 组合起来可以执行数据卷的扩容操作;
  • External Snapshotter 和 Controller Server 组合则可以完成快照的创建和删除。

25.png

CSI Node Server 中主要包含 Kubelet 组件,包括 VolumeManager 和 VolumePlugin,它们会去调用 CSI Plugin 去做 mount 和 unmount 操作;另外一个组件 Driver Registrar 主要实现的是 CSI Plugin 注册的功能。

以上就是 CSI 的整个拓扑结构,接下来我们将分别介绍不同的对象和组件。

CSI 对象

我们将介绍 3 种对象:VolumeAttachment,CSIDriver,CSINode。

VolumeAttachment 描述一个 Volume 卷在一个 Pod 使用中挂载、卸载的相关信息。例如,对一个卷在某个节点上的挂载,我们通过 VolumeAttachment 对该挂载进行跟踪。AD Controller 创建一个 VolumeAttachment,而 External-attacher 则通过观察该 VolumeAttachment,根据其状态来进行挂载和卸载操作。

下图就是一个 VolumeAttachment 的例子,其类别 (kind) 为 VolumeAttachment,spec 中指定了 attacher 为 ossplugin.csi.alibabacloud.com,即指定挂载是由谁操作的;指定了 nodeName 为 cn-zhangjiakou.192.168.1.53,即该挂载是发生在哪个节点上的;指定了 source 为 persistentVolumeName 为 oss-csi-pv,即指定了哪一个数据卷进行挂载和卸载。

status 中 attached 指示了挂载的状态,如果是 False, External-attacher 就会执行一个挂载操作。

26.png

第二个对象是 CSIDriver,它描述了集群中所部署的 CSI Plugin 列表,需要管理员根据插件类型进行创建。

例如下图中创建了一些 CSI Driver,通过 kuberctl get csidriver 我们可以看到集群里面创建的 3 种类型的 CSI Driver:一个是云盘;一个是 NAS;一个是 OSS。

在 CSI Driver 中,我们定义了它的名字,在 spec 中还定义了 attachRequired 和 podInfoOnMount 两个标签。

  • attachRequired 定义一个 Plugin 是否支持 Attach 功能,主要是为了对块存储和文件存储做区分。比如文件存储不需要 Attach 操作,因此我们将该标签定义为 False;
  • podInfoOnMount 则是定义 Kubernetes 在调用 Mount 接口时是否带上 Pod 信息。

27.png

第三个对象是 CSINode,它是集群中的节点信息,由 node-driver-registrar 在启动时创建。它的作用是每一个新的 CSI Plugin 注册后,都会在 CSINode 列表里添加一个 CSINode 信息。

例如下图,定义了 CSINode 列表,每一个 CSINode 都有一个具体的信息(左侧的 YAML)。以 一 cn-zhangjiakou.192.168.1.49 为例,它包含一个云盘的 CSI Driver,还包含一个 NAS 的 CSI Driver。每个 Driver 都有自己的 nodeID 和它的拓扑信息 topologyKeys。如果没有拓扑信息,可以将 topologyKeys 设置为 “null”。也就是说,假如有一个有 10 个节点的集群,我们可以只定义一部分节点拥有 CSINode。

28.png

CSI 组件之 Node-Driver-Registrar

Node-Driver-Registrar 主要实现了 CSI Plugin 注册的一个机制。我们来看一下下图中的流程图。

29.png

  • 第 1 步,在启动的时候有一个约定,比如说在 /var/lib/kuberlet/plugins_registry 这个目录每新加一个文件,就相当于每新加了一个 Plugin;

启动 Node-Driver-Registrar,它首先会向 CSI-Plugin 发起一个接口调用 GetPluginInfo,这个接口会返回 CSI 所监听的地址以及 CSI-Plugin 的一个 Driver name;

  • 第 2 步,Node-Driver-Registrar 会监听 GetInfo 和 NotifyRegistrationStatus 两个接口;
  • 第 3 步,会在 /var/lib/kuberlet/plugins_registry 这个目录下启动一个 Socket,生成一个 Socket 文件 ,例如:”diskplugin.csi.alibabacloud.com-reg.sock”,此时 Kubelet 通过 Watcher 发现这个 Socket 后,它会通过该 Socket 向 Node-Driver-Registrar 的 GetInfo 接口进行调用。GetInfo 会把刚才我们所获得的的 CSI-Plugin 的信息返回给 Kubelet,该信息包含了 CSI-Plugin 的监听地址以及它的 Driver name;
  • 第 4 步,Kubelet 通过得到的监听地址对 CSI-Plugin 的 NodeGetInfo 接口进行调用;
  • 第 5 步,调用成功之后,Kubelet 会去更新一些状态信息,比如节点的 Annotations、Labels、status.allocatable 等信息,同时会创建一个 CSINode 对象;
  • 第 6 步,通过对 Node-Driver-Registrar 的 NotifyRegistrationStatus 接口的调用告诉它我们已经把 CSI-Plugin 注册成功了。

通过以上 6 步就实现了 CSI Plugin 注册机制。

CSI 组件之 External-Attacher

External-Attacher 主要是通过 CSI Plugin 的接口来实现数据卷的挂载与卸载功能。它通过观察 VolumeAttachment 对象来实现状态的判断。VolumeAttachment 对象则是通过 AD Controller 来调用 Volume Plugin 中的 CSI Attacher 来创建的。CSI Attacher 是一个 In-Tree 类,也就是说这部分是 Kubernetes 完成的。

当 VolumeAttachment 的状态是 False 时,External-Attacher 就去调用底层的一个 Attach 功能;若期望值为 False,就通过底层的 ControllerPublishVolume 接口实现 Detach 功能。同时,External-Attacher 也会同步一些 PV 的信息在里面。

30.png

CSI 部署

我们现在来看一下块存储的部署情况。

之前提到 CSI 的 Controller 分为两部分,一个是 Controller Server Pod,一个是 Node Server Pod。

我们只需要部署一个 Controller Server,如果是多备份的,可以部署两个。Controller Server 主要是通过多个外部插件来实现的,比如说一个 Pod 中可以定义多个 External 的 Container 和一个包含 CSI Controller Server 的 Container,这时候不同的 External 组件会和 Controller Server 组成不同的功能。

而 Node Server Pod 是个 DaemonSet,它会在每个节点上进行注册。Kubelet 会直接通过 Socket 的方式直接和 CSI Node Server 进行通信、调用 Attach/Detach/Mount/Unmount 等。

Driver Registrar 只是做一个注册的功能,会在每个节点上进行部署。

31.png

文件存储和块存储的部署情况是类似的。只不过它会把 Attacher 去掉,也没有 VolumeAttachment 对象。

32.png

CSI 使用示例

和 Flexvolume 一样,我们看一下它的定义模板。

可以看到,它和其它的定义并没什么区别。主要的区别在于类型为 CSI,里面会定义 driver,volumeHandle,volumeAttribute,nodeAffinity 等。

  • driver 就是定义是由哪一个插件来去实现挂载;
  • volumeHandle 主要是指示 PV 的唯一标签;
  • volumeAttribute 用于附加参数,比如 PV 如果定义的是 OSS,那么就可以在 volumeAttribute 定义 bucket、访问的地址等信息在里面;
  • nodeAffinity 则可以定义一些调度信息。与 Flexvolume 类似,还可以通过 selector 和 Label 定义一些绑定条件。

中间的图给出了一个动态调度的例子,它和其它类型的动态调度是一样的。只不过在定义 provisioner 的时候指定了一个 CSI 的 provisioner。

33.png

下面给出了一个具体的挂载例子。

Pod 启动之后,我们可以看到 Pod 已经把一个 /dev/vdb 挂载到 /data 上了。同理,它有一个 GlobalPath 和一个 PodPath 的集群在里面。我们可以把一个 /dev/vdb 挂载到一个 GlobalPath 里面,它就是一个 CSI 的一个 PV 在本节点上唯一确定的目录。一个 PodPath 就是一个 Pod 所确定的一个本地节点的目录,它会把 Pod 所对应的目录映射到我们的容器中去。

34.png

CSI 的其它功能

除了挂载、卸载之外,CSI 化提供了一些附加的功能。例如,在定义模板的时候往往需要一些用户名和密码信息,此时我们就可通过 Secret 来进行定义。之前我们所讲的 Flexvolume 也支持这个功能,只不过 CSI 可以根据不同的阶段定义不同的 Secret 类型,比如挂载阶段的 Secret、Mount 阶段的 Secret、Provision 阶段的 Secret。

Topology 是一个拓扑感知的功能。当我们定义一个数据卷的时候,集群中并不是所有节点都能满足该数据卷的需求,比如我们需要挂载不同的 zone 的信息在里面,这就是一个拓扑感知的功能。这部分在第 10 讲已有详细的介绍,大家可以进行参考。

Block Volume 就是 volumeMode 的一个定义,它可以定义成 Block 类型,也可以定义成文件系统类型,CSI 支持 Block 类型的 Volume,就是说挂载到 Pod 内部时,它是一个块设备,而不是一个目录。

Skip Attach 和 PodInfo On Mount 是刚才我们所讲过的 CSI Driver 中的两个功能。

35.png

CSI 的近期 Features

36.png

CSI 还是一个比较新的实现方式。近期也有了很多更新,比如 ExpandCSIVolumes 可以实现文件系统扩容的功能;VolumeSnapshotDataSource 可以实现数据卷的快照功能;VolumePVCDataSource 实现的是可以定义 PVC 的数据源;我们以前在使用 CSI 的时候只能通过 PVC、PV 的方式定义,而不能直接在 Pod 里面定义 Volume,CSIInlineVolume 则可以让我们可以直接在 Volume 中定义一些 CSI 的驱动。

阿里云在 GitHub 上开源了 CSI 的实现,大家有兴趣的可以看一下,做一些参考。

四、本文总结

本文主要介绍了 Kubernetes 集群中存储卷相关的知识,主要有以下三点内容:

  • 第一部分讲述了 Kubernetes 存储架构,主要包括存储卷概念、挂载流程、系统组件等相关知识;
  • 第二部分讲述了 Flexvolume 插件的实现原理、部署架构、使用示例等;
  • 第三部分讲述了 CSI 插件的实现原理、资源对象、功能组件、使用示例等;

希望上述知识点能让各位同学有所收获,特别是在处理存储卷相关的设计、开发、故障处理等方面有所帮助。

云原生网络研讨会邀您参加

网研会议题

Kubernetes SIG-Cloud-Provider-Alibaba 介绍

举办日期

2020 年 2 月 12 日 10:00(时区:北京)

会议语言

中文

讲师介绍

3.png

议题介绍

SIG Cloud Provider 是 Kubernetes 的重要兴趣小组,致力于推动所有云厂商以标准的能力提供 Kubernetes 服务。SIG-Cloud-Provider-Alibaba 是 SIG Cloud Provider 在国内唯一的子项目。

本次研讨会将首次完整介绍阿里云对 Kubernetes 社区的布局。在产品层面,阿里云提供了完整的容器产品家族;在开源领域,阿里云也围绕 Kubernetes 提供了十个类别,20 多个开源项目,提供了完整的 Kubernetes 生命周期管理。阿里云会依托 SIG-Cloud-Provider-Alibaba,寻求和开发者更密切的互动,也号召更多的开发者一起贡献。

参会收益

  • 透明可控:对于研究性质的开发者,可以基于提供插件自行搭建 Kubernetes 集群;对于容器服务 ACK 的用户,也可以更加透明的了解相关实现。
  • 共建协同:对阿里云上使用 Kubernetes 的计算、网络、存储等领域有需求的开发者,可以提 Issue 或参与到开源组件开发中一起贡献,并参与 RoadMap 的制定。
  • 平滑演进:阿里云 Kubernetes 开源插件提供了 Day 1 的部署能力,但是对企业的运维、升级、稳定性掌控等提出了更高的要求。如果需要 Day 2 的持续升级、高可用保障、纠错推荐等专家服务,可以平滑演进到容器服务 ACK。

如何参与?

点击注册报名参会:
https://zoom.com.cn/webinar/register/8015799062779/WN_dIrSRs1zQ-uXNXmuAThuog

 

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术圈。”

2020 年,Serverless 将给大前端带来什么样的变化?

alicloudnative阅读(1004)评论(0)

作者 | 杜欢(阿里巴巴高级前端技术专家)、王文婧

导读:云 + 端模式成为当前前端开发的新风向,由此而来的 Serverless 正帮助前端工程师提升开发能力和效率。近日在 2019 ArchSummit 全球架构师峰会北京站,阿里巴巴高级前端技术专家杜欢(风驰)接受了 InfoQ 记者的采访,为我们详细梳理了阿里巴巴近两年使用云 + 端的 Serverless 来探索前端演进过程的经验和体会。

Question:杜老师,您好!请您介绍一下您的从业经历,以及目前在阿里云战略 & 合作部负责的工作。

杜欢(风驰):目前我在阿里云战略合作部,负责阿里云的开发者业务,更多的是在考虑怎么在云的时代帮助整个广大的开发者社区和生态能够在成为云时代原住民开发者的状态下,有个更好的开发环境。

Question:您从事前端工作多久了?对这个行业有过哪些困惑与思考?

杜欢(风驰):我其实进入到前端行业还是很有趣的一个过程,我最早是在 2001 年左右开始接触到 Web 开发。那个时候,就是做网站,做网站前端、后端、数据库,然后发布运维都要做。那个时候其实也没有现在这么多岗位,基本上就一个岗位——开发,所有的事情都做。

后来随着公司业务的拓展,开始去接一些 Browser 端的工作,当时有一个词叫做 BS,它和 CS 是对应的,CS 叫 Client Side,就是客户端。Client 和 Server,就是客户端和 Server。BS 是 Browser 和 Server。从那个时候开始,这种 BS 结构的应用出现,这种结构的出现其实当时是为了解决开发成本和部署成本的问题。就是有些企业想做一个系统,这个系统可以很容易地让整个企业内部不同的团队、不同的角色很好地利用,部署的成本不要那么高,开发成本也不要那么高。

所以那个时候开始有这种业务类型出现,这种业务类型操作的主要界面就在 Browser 端,那 Browser 端就会遇到一个很大的挑战,也就是说,你的操作行为、表现、习惯跟原来传统的客户端软件开发的那种操作体验是不太一样的。因为 Browser 是浏览器。浏览器里边就是很有限的几个元素 API。然后主要客户就会提一些要求说,你需要帮我把传统的那种体验交互保留下来。因为对我而言,我只是换了一个软件提供商,但是我和我的同事在使用的时候不能有什么感知,对他们来讲应该是一样的。

那个时候遇到的挑战是,在浏览器里如何实现和传统的开发软件里的 UI、组件一样的行为。举个例子,比如说你在搜索框里输入任何一个字符,它会有下拉提示,这是非常常见的一个 UI,但在那个时候是没有的。这个 UI 至今也是没有原生提供的,都是前端去模拟出来的。

所以那个时候我做的就是这些事情。做着做着,我发现挑战非常大,相当于你要完全模拟出一套传统的开发体系里的整个 UI 体系。那个时候就想,我能不能把这个做得更好一点。所以慢慢地开始加入到前端的一些社区。认识了当时的一些朋友。就这样进入到前端行业,一直做到今天。

Question:前端的发展很快,在研发体系的升级上,阿里云是如何部署的?

杜欢(风驰): 前端升级确实是让人又爱又恨的。而且这种升级,在我看来,比如说框架层,它可能要解决的是一些新的研发形态,但是对业务而言,它其实并没有很大规模地解决上一个阶段遇到的问题。

举个研发效率的例子,比如我们现在做工程化、框架的演进,和最早用 jquery 的时候,相对业务而言,有什么变化吗?没有什么变化。而且有时候反而使你的整个协同成本、交付成本、人力成本在一定程度上变高了。因为你引入了工程的概念,你就要去做工程化,工程化不是所有人都能做得很深入。因为工程化本身就是一个领域,所以你又得为了把工程化做好去准备一些特定的侯选人,组建一个团队。相对来讲,你又多了一批做业务的人,业务流程又要变慢。原来可能你在 UI 上做完,JS、HTML、CSS,你怎么做马上就可以看到。现在你是看不到的,你写完之后要编译,工程化和编译完之后,你可能才能看到。

我想表达的是,前端不断地在演进,它其实是更精细化了,质量更有保障,在一定程度上效率可能也有提升。但是从更宏观的角度,从业务的角度来看,它可能不一定真正解决了业务痛点。就比如说今天我们提到,有的业务期望是,人一进来马上就能干活,干完活马上就能上线。从业务的视角来看,前端这几年的演进可能还不是一个终态,它处在一个摸索的阶段。

Question:所以就像您说的,工程化现在还没有达到它的预期效果?

杜欢(风驰): 我们认为,工程化的出现和持续演进未来一定是能帮到业务的,但是它还在摸索的阶段。本身做工程化需要消耗人力、资源,包括流程的新增,这些其实在这个阶段是会降低业务交付效率的。所以我们也不能说它不对,因为它毕竟有一个发展的过程。只是在它还没有到达终态之前,不管是框架还是这种工程化的这种演进,相对来讲都是比较痛苦的。

但是未来如果终态来临,随着未来结合云原生 Serverless,从写代码到最终发布一体化的时代来临,可能所有的问题就迎刃而解了。

Question:我之前采访过一个专家,他说,前端工程化就是在做“消灭”自己的工作,您怎么看?

杜欢(风驰):我是这么理解,如果是消灭自己,那意味着,前端这个岗位目前做的事情未来会有一个东西替代它。
那今天前端的岗位在做什么事情呢?核心是在做用户交互行为的开发,在普遍的基础上,如果加上业务的特性,用户交互行为就会有很多定制化的东西。再加上,因为每一个业务都要差异化才能生存,尤其是 to C 的产品类型,它一定会在用户侧寻找和竞品的差异化,用户侧更多的表现就是怎么让用户看起来更舒服,操作起来更舒服,整个体验更好。这些往往会表现在真正的用户交互行为上的差异。

这里有一个矛盾的点,抽象出来的那些东西,通过工程化确实能以一定的手段来替代,但是差异化的东西怎么来做,是不是能够完全替代,这个还很难说,至少今天还没有一个大家都觉得可信的方案说能够替代掉。就像今天的企业级定制开发也是类似,之所以叫定制开发,就是它至少在提定制的这个时间点,没有一个可抽象、可覆盖它的一个通用的东西,要不然它就不需要定制了,就用通用的就好了。

所以我觉得工程化能够消灭那种通用抽象的东西,但是定制的东西至少目前来看还不能,除非未来机器学习演进到能够理解真正不同的需求,并且能够把这种需求跟现有的技术体系、科学体系完整地链接起来的时候,那我觉得是有机会的。

Question:阿里经济体的前端技术架构是什么样的?它经历了哪些发展阶段,可否提取几个重要的时间节点谈谈?

杜欢(风驰):阿里经济体的前端在一定程度上,至少能代表国内的前端行业发展的阶段。首先,据我所知,在国内,前端这个岗位最早就是在阿里出现的。那个时候为什么会出现前端?已经从原来的所有的应用由一个人开发变成一种用户需求导向,用户觉得你这个应用虽然好,但是操作起来很差,或者整个体验不好,所以能不能有人把这块做得更好?所以在业务的需求下产生了职业精细化的要求。这个精细化的需求在前端岗位诞生的时候,它的核心是把结构、表现、行为这三者做精细化的处理或演进。这是这个岗位诞生之初阿里前端在做的事情。

后来随着业务体量逐渐增大,开始覆盖到的人群,以及人群所在的地理位置都不太一样的时候,越来越多的来自网络比较差的环境的用户会说,打开特别慢,体验不好,那个时候又经历了做性能优化的时代。性能优化主要的目的是,让不同的地理位置的用户都能够以最好的速度访问到我们的业务,让大家的体验尽量是最好的。

第三个阶段,Node.js 的出现,为我们前面谈到的工程化提供了基础。因为做工程化意味着你要去做编译、文件处理,操作一些事情,这些东西需要有一种能力让它能够跑在本地,跑在系统里面,不只是在 Web 页面上。Node.js 当时帮助前端有能力做这件事情,然后开始演进出前端如何进一步地把行为、样式、结构分离,如何做模块化的设计、模块化的开发。拆开之后,这个页面你就看不到了,你想看到,怎么把拆开的东西重新聚合起来?那个时候就是通过 Node.js 做这种整体的工程化。

第一更精细化,第二更精细化之后,能够把它编译在一起,能够看到,同时去解决或优化和原来后端的协同方式。其实在这个阶段之前,前端和后端的协同方式是比较粗暴的,是那种交接式的。就是前端做完页面,然后把产物交接给后端,后端拿着前端做的页面,在那些特定的区域操作,比如说一个表格,表格里面应该有数据,前端会填一些假的数据在里面占位,后端再把真实的数据塞在里面,那是最早的阶段。有了工程化体系之后,前端和后端的衔接就可以通过 API 的方式来做。在有 API 之前,前端可以去模拟这个假数据,通过约定的 API 规范之类。这是第三个阶段,就是工程化带来的这种更精细化的设计、模块化的设计,以及这种前后端协同的演进。

再往后的演进就是无线时代,前端开始向混合开发模式演进,比如几个框架的诞生。阿里内部也诞生了一些框架,比如大家知道的 Weex,最近的 Rax 等等。这是在 all in 无线业务背景下前端的演进。阿里内部有很多中后台的业务,它有很多相对固定的结构形态,其实我们在工程化上又进一步演进了,就是诞生了这种中后台的研发模式。这种低代码的研发模式,更多地体现在页面的搭建,当我们有足够多的设计资源,已经抽象好的、比较通用的、设计好的模块,那么就可以简单地通过一些框架,把这些模块组装在一起,而不用写代码,或者写很少的代码。这是中后台的演进。

现在,结合云的到来,企业希望通过云去提高效率。这只是一个愿望,一定要经过一个技术的演进才能落地。相应地,从今年年初到“双 11”,我们整个阿里再一次演进了自己的技术体系,升级到了 Serverless 的研发体系。它不仅可以帮助前端完成面向用户交互的开发,还能够完成整个应用的开发,整个应用的开发基于云计算的实时弹性的能力能够快速做好,并且能够真实地在线上服务好“双 11”这么大的流量,真正帮助企业实现用云来快速商业化、节约成本的初衷。

Question:阿里是什么时候开始采用 Serverless 的?

杜欢(风驰):2017 年,阿里就开始讨论这个事情,正式启动是在 2018 年。阿里内部由于开发环境、网络的客观原因,暂时不能直接使用阿里云的公共资源,所以我们要内部实现一套公共云上有的 Serverless 的能力,所以我们在 2018 年自己建设了这么一套能力。2019 年,我们开始做上层研发的架构和模型,到今年“双十一”我们正式投入使用。

Question:云 + 端是一个老生常谈的话题,阿里云的云 + 端和其他企业的云 + 端有哪些不同之处?

杜欢(风驰):为什么今天云出现了这么久,大家提云 + 端也提了这么久,提 Serverless 也提了有一段时间,但是真正的实践那么少呢?因为在研发实践当中还是需要很挑战的一些东西去帮助它推动。

第一个是顶层的设计,因为你是研发生态,而不是简单地利用云的能力去做一个任务,这是不一样的两件事情。如果是利用云的能力去完成一个任务,这个很简单,很多人都在用。但是现在真正利用云 + 端,利用 Serverless 的能力去帮助自己提升研发能力是没有的。问题就在于大家都缺乏对整个研发架构的改变。因为你要真正利用它,研发模型要发生改变,研发的流程链路也要发生改变,这个大家没有参考。

今天阿里作为前期的实践者,愿意分享自己的设计,为大家提供参考,未来真正要在自己的研发体系里实践,大概要怎么设计,有哪些环节,哪些关键节点,哪些特征等等。第二个是真正的实践,如果阿里巴巴也只是停留在设计上,没有拿自己的业务去实践,我相信大家也缺乏信心,也可能会认为这只是我们的想象,但是今天我们真正地通过“双十一”这个很大的场景来考验。

其实我相信这能够给到整个行业一些信息,我们不仅在思考和设计整个 Serverless,整个云 + 端落到真正的研发模式上,同时我们也通过自己的业务去验证了我们的设计是可行的。最后我们也希望,不仅是分享我们的架构设计,未来我们自己内部的整个研发平台能有机会通过阿里云开放给整个行业,让外面整个开放的生态也能够使用。大家都使用一样的方式、一样的平台、一样的架构。

Question:正如您所说,今年“双 11”是 Serverless 在阿里的第一次大检验,取得了振奋人心的效果,但是这个过程当中肯定会有一些坎坷,您能分享一下这方面的经验吗?

杜欢(风驰):最痛苦的还是 Serverless 底座的建设。我花了比较多的时间和大家讲为什么不要去自建这一层的原因是,因为落地和实践 Serverless,不是一个技术诉求,而是一个业务诉求。为什么?因为云本身是帮助企业用低成本高效快速地实现商业化,技术只是为了让这个业务诉求落地,是这样的一个关系,所以说如果没有 Serverless 底座是很痛苦的一件事。并且如果它的能力不行,基本上也是不可用的,因为落地 Serverless 意味着你的所有服务都是跑在上面的。如果它挂了,你的业务也挂了,没有人愿意这样。

Question:所以说,小企业可能不太适合做 Serverless?

杜欢(风驰):小企业最好不要自己去建设 Serverless 底座资源能力。第一,存在技术上的挑战;第二,存在资源规模化的挑战。因为 Serverless 的核心要素是,它是按量使用的,按量使用意味着如果今天的量很小,你就用很少的资源;如果今天的量很大,就会给你调很多资源。“双十一”的时候,流量都是亿级的流量,如果你的企业内部没有按亿级做单位的这种流量的机器资源,你怎么去调度这些资源给他人使用呢?你没办法实现按量调度。所以小企业,或者不具备这种资源规模化的企业,不需要去自建 Serverless 能力,不是说不能去实践 Serverless,可以用公共云,比如说用阿里云或其他的云。

我们遇到的最困难的也是这个事情,就是内部研发网络环境和生产运行网络的问题。我们也是不互通的,我们内部也很难直接在公共云环境去使用阿里云这些已有的能力。我们其实花了一年的时间,在阿里内部推动不同的团队去建设 Serverless 底座。这是第一个我认为比较挑战的点。

第二,整个研发模型对研发体系带来了挑战。其实很多时候这种东西一出来,看起来是帮助前端拓展了边界,拓展了价值能力,但是相应来讲,后端同学可能第一反应就是,那这是不是把我革命了?我就不需要干活了?其实不是这样的。比如阿里的导购业务就是取数据展示的场景。这种事情让一个后端来做,没有任何技术价值、技术沉淀、技术成长,但是现有的研发模式就是需要有后端同学进来开发。所以其实对他们来讲,Serverless 研发模式的演进有助于帮助他们往更底层演进,让他们聚焦于真正需要做技术研究的部分。比如,这些数据的能力、服务的能力,怎么做得更好、更扎实,这是我们期望看到的。但是这个研发模式乍一看,如果大家没有深入了解的话,就会认为对整个研发模式、研发流程挑战很大,那么就需要去和大家沟通、布道,讲它对每个岗位会带来的价值。

第三,回到前端来讲,这个东西虽然看起来很美好,但如果你真正下决心要进去,对每一个前端来讲,是撕裂的成长。因为我们要开始知道这个业务是什么,为什么要做这个业务,这个业务到底服务谁,关键的指标是什么,怎么做。这个时候他已经从前端变成一个业务的功能,整个业务都是他去开发、交付。

这是从技术准备、研发体系的协同,到前端岗位的挑战三个层面的难点,是我觉得印象比较深刻的,可能是未来大家在实践当中都会遇到的。

Question:我在网上了解到,有人说 Serverless 存在不适合长时间的运行应用,完全依赖第三方服务,缺乏调试,还有构建复杂等缺点。您认同这些观点吗?对于那些还没有涉足 Serverless 的人,您可以帮助他们辨清这些概念吗?

杜欢(风驰):我觉得没有什么对错。它只是提到了一些特征,但是我也想从特征的角度给大家鼓鼓劲。比如说今天 CNCF,就是云原生在推的事情,核心就是 Serverless。Serverless 的核心特征是什么呢?第一,按量。也就是说,先不要站在技术的角度去看,站在业务角度,它是按量的,按量就意味着,对于业务而言,它是最好的资源使用方式,既不会带来浪费,也不会不足。第二,计费方式。现在很多的方式是你买的多浪费,买的少就不够,而且需要再补买,很难把它和你的应用扩容上去。Serverless 的计费方式是按量走的,用多少付多少。另外,它是平台承载的,因为平台的实时弹性,帮助了用户实现按量诉求。

第二,关于技术实践上的复杂,我觉得也只是一个阶段性的现状而已。今天整个行业还没有一个开箱即用,或者说比较成熟的研发框架或研发体系、研发平台出来,大家都在一个摸索的阶段,就包括我们自己也是刚刚摸索实践出来,并且也还不算是成熟,我们也还遇到很多要去继续推动解决的问题。所以我是这么看,先从业务的角度去看,它一定是一个最佳的路径。阶段性的痛苦肯定是有的,所以没有什么对错。

Question:目前国内外 Serverless 实践存在怎样的差距?

杜欢(风驰):相对来讲,国外的整个开发生态就时间上要比国内领先一点,原因在于国外的主流云厂商对整个 IT 行业,对整个开发生态的布道做了很多工作。国外的开发生态对云原生,对 Serverless 的接受度和实践比我们要好很多,并且也早很多。对他们而言,这是一个先发优势。提供的早,就意味着实践的多,然后大家对整个 Serverless 的通用性的东西,比如通用的研发环节能够去做一些沉淀和抽象,所以诞生了一些像 Serverless.com 这些 Serverless 的开发框架。他们更多地是站在一个第三方的公共框架的角度来看,你可能既可以用这个云厂商,也可以用那个云厂商,基于我的框架可以快速地去做,基于我的框架,框架自然会有些约束,你跟着这个框架的要求去做一些动作,然后你可以去实践,真正实践这个 Serverless 在业务里面落地,这是一些现状。

那我们今天在做的也有点类似这个事情,但是我们可能不仅仅是一个开发框架,而是希望把整个开发平台都开放出来。所以大家不仅仅是说云层面,函数层面可以按照我们提的建议去做,你甚至可以直接在我们上面去做,我们希望是这样。

Question:明年 Serverless 有哪些更细粒度的技术值得关注?

杜欢(风驰):当 Serverless 整个研发模式大概成形之后,接下来就是实践。在实践的过程当中,对渲染层、服务层、函数运行时、框架这几层可能会有一个更深入的实践,产生更细节的一些需求。我理解从明年开始,可能就是非常 detail 的垂直分层演进了,可能会有更多的这类内容产生,比如服务编排是如何演进的,函数运行时是如何演进的,性能是怎样提升的,稳定性是怎样进一步保障好的,就是又会回到一个大的运维架构演进的阶段。

Question:最后一个问题,您预测未来 5 年,前端行业会有什么变化?您所在团队目前有没有针对这些技术判断做出一些布局?

杜欢(风驰):今年阿里经济体的其他几个大的方向,比如前端智能、搭建等,这些都有可能串联起来,成为影响整个前端行业发展趋势的一些因素。但我今天讲的更多的可能是整个研发生态的变化,未来的研发模式使前端可以供整个业务,具体到每一个环节,比如前端可能通过 UI 的智能化,让自己释放出来,通过一些成熟的视觉物料、前端物料,以及服务的物料,通过 AI 的辅助,快速地把一些原本需要前端去开发的一些模式化页面模块,通过 AI 的方式自主生成。

运维这块可能随着云原生能力的不断增强,工程化能力的补充,未来有可能进入到 NoOps ,就是不需要运维,只需要关注好一些数据。因为整个弹性会跟这些数据运行的实时数据关联起来,去做不同的变化。

所以整体而言,未来五年对前端而言是能力价值进一步放大的五年,云上 Serverless 开发能力将成为前端的“金手指”,企业愿意去组建一个由云端的应用用开发工程师构成的研发团队,通过研发团队结合整个云的研发体系,快速地交付它的业务。同时在这个过程当中,结合智能化进一步提高生产效率,可能是这样一个趋势。

如果你对于 Serverless 和函数计算感兴趣的话,欢迎钉钉扫码进入交流群。
阿里计算.png

作者介绍:
杜欢(风驰),阿里云战略 & 合作部 / 高级前端技术专家。目前在阿里云”战略 & 合作部”负责阿里云开发者业务,阿里巴巴经济体前端技术委员会委员,阿里巴巴经济体前端 Serverless 研发升级项目负责人。此前就职于雅虎、思科等公司。

招聘

TL;DR

阿里云 – 云原生应用平台 – 基础软件中台团队(原容器平台基础软件团队)诚邀 Kubernetes/容器/ Serverless/应用交付技术领域专家( P6-P8 )加盟。

工作年限:建议 P6-7 三年起,P8 五年起,具体看实际能力。
工作地点:

  • 国内:北京,杭州,深圳;
  • 海外:旧金山湾区、西雅图

简历立刻回复,2~3 周出结果。节后入职。

工作内容

基础产品事业部是阿里云智能事业群的核心研发部门,负责计算、存储、网络、安全、中间件、系统软件等研发。而云原生应用平台基础软件终态团队致力于打造稳定、标准、先进的云原生应用系统平台,推动行业面向云原生技术升级与革命。

在这里,既有 CNCF TOC 和 SIG 联席主席,也有 etcd 创始人、K8s Operator 创始人与 Kubernetes 核心维护成员组成的、国内最顶尖的 Kubernetes 技术团队。

在这里,你将同来自全球的云原生技术领域专家们(如 Helm 项目的创始人、Istio 项目的创始人)密切合作,在独一无二的场景与规模中从事 Kubernetes、Service Mesh、Serverless、Open Application Model ( OAM )等云计算生态核心技术的研发与落地工作,在业界标杆级的平台上,既赋能阿里巴巴全球经济体,更服务全世界的开发者用户。

  1. 以 Kubernetes 为核心,推动并打造下一代 “以应用为中心” 的基础技术体系;在阿里经济体场景中,研发和落地“以应用为中心”的基础设施架构和基于 Open Application Model ( OAM )的下一代 NoOps 体系,让 Kubernetes 与云原生技术栈发挥出真正的价值和能量;
  1. 研发多环境复杂应用交付核心技术;结合阿里与生态中的核心业务场景,打造多环境复杂应用交付的业界标准与核心依赖(对标 Google Cloud Anthos 和 Microsoft Azure Arc );
  1. 云原生应用平台核心产品及后端架构设计与开发工作;在生态核心技术与前沿架构的加持下,在世界级云厂商的平台场景中,用技术打造持续的云产品生命力与竞争力;
  1. 持续推动阿里经济体应用平台架构演进,包括 Serverless 基础设施、标准云原生标准 PaaS 构建、新一代应用交付体系构建等核心技术工作。

技术要求:Go/Rust/Java/C++,Linux,分布式系统

简历提交

lei.zhang AT alibaba-inc.com

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术圈。”

操作指南:通过Rancher在K8S上运行PostgreSQL数据库

Portworx阅读(2074)评论(0)

通过Rancher Kubernetes Engine运行高可用 PostgreSQL

这篇是我们关于在Kubernetes上运行PostgreSQL系列文章的其中一篇。下面是相关文章和链接。

  • 在亚马逊AWS Elastic Container Service for Kubernetes (EKS) 上运行高可用 PostgreSQL:

(https://portworx.com/postgresql-amazon-eks/)

  • 在微软Azure Kubernetes Service (AKS) 上运行高可用 PostgreSQL:

(https://portworx.com/ha-postgresql-azure-aks/)

  • 在Google Kubernetes Engine (GKE) 上运行高可用 PostgreSQL:

(https://portworx.com/run-ha-postgresql-gke/)

  • 在RedHat OpenShift 上运行高可用 PostgreSQL:

(https://portworx.com/run-ha-postgresql-red-hat-openshift/)

  • 在IBM Cloud Kubernetes Service (IKS) 上运行高可用 PostgreSQL:

(https://portworx.com/run-ha-postgresql-ibm-cloud-kubernetes-service/)

  • 在IBM 私有云上运行高可用 PostgreSQL:

(https://portworx.com/run-ha-postgresql-ibm-cloud-private/)

Rancher Kubernetes Engine (RKE)是一个轻量级的Kubernetes 安装程序,支持在裸金属和虚拟机上安装Kubernetes。RKE解决了Kubernetes安装的复杂性问题。通过RKE安装是比较简单的,而跟下层的操作系统无关。

 

Portworx是一个云原生的存储和数据管理平台,来支撑Kubernetes上持久性的工作负载。通过Portworx,用户能够管理不同基础架构上的、不同容器调度器上的数据库。它为所有的有状态服务(Stateful Service)提供了一个单一的数据管理层。

 

本文列出了操作步骤:通过RancherKubernetes Engine (RKE),在AWS的Kubernetes集群上,部署和管理高可用PostgreSQL集群。

 

总结来说,在Amazon上运行高可用PostgreSQL,需要:

  1.   通过Rancher KubernetesEngine安装一个Kubernetes集群
  2.  安装云原生存储解决方案Portworx,作为Kubernetes的一个DaemonSet。
  3.   建立一个存储类来定义你的存储要求,比如,复制因子,快照策略和性能情况
  4.  使用Kubernetes部署PostgreSQL
  5.  通过killing或者cordoning集群中的节点,来测试故障恢复
  6. 可能的话,动态的调整PG Volume的大小,快照和备份Postgres到S3
如何通过RKE来创建一个Kubernetes集群

RKE是一个安装和配置Kubernetes的工具。可以支持的环境包括裸金属,虚拟机或者IaaS。在本文中,我们会在AWS EC2上创建一个3节点的Kubernetes集群。

更为详细的步骤,可以参考这篇tutorial from The New Stack. (https://thenewstack.io/run-stateful-containerized-workloads-with-rancher-kubernetes-engine-and-portworx/)

做完这些操作,我们会创建一个1 master 和 3 worker 节点的集群。

在Kubernetes上安装Portworx

在RKE的Kubernetes 上安装Portworx,跟在Kubernetes集群上通过Kops安装没什么不同。Portworx有详细的文档,列出每步的操作 (https://docs.portworx.com/portworx-install-with-kubernetes/cloud/aws/),来完成在AWS环境的Kubernetes上运行Portworx集群。

The New Stacktutorial(https://thenewstack.io/run-stateful-containerized-workloads-with-rancher-kubernetes-engine-and-portworx/) 也包含了在Kubernetes部署Portworx DaemonSet的所有操作步骤。

Kubernetes集群运行起来,Portworx安装和配置完成,我们就开始部署一个高可用的PostgreSQL数据库。

创建一个Postgres 存储类

通过存储类对象,一个Admin可以定义集群中不同的Portworx卷的类。这些类在动态的卷的部署过程中会被用到。存储类本身定义了复制因子,IO情况(例如数据库或者CMS),以及优先级(比如SSD或者HDD)。这些参数影响着工作负载的可用性和输出,因此参数可以被根据每个卷分别设置。这很重要,因为对生产系统的数据库的要求,跟研发测试系统是完全不一样的。

 

在下面的例子里,我们部署的存储类,它的复制因子是3,IO情况设定成“db”,优先级设定成“high”。这意味着存储会被优化为适合低传输速率的数据库负载(Postgres),并且自动的部署在集群具备最高性能的存储里。

$ kubectl create -f https://raw.githubusercontent.com/fmrtl73/katacoda-scenarios-1/master/px-k8s-postgres-all-in-one/assets/px-repl3-sc.yaml

storageclass "px-repl3-sc" created

创建一个Postgres PVC

我们现在可以基于存储类创建一个PersistentVolume Claim (PVC)。动态部署的优势就在于,claims能够在不需要显性部署持久卷Persistent Volume (PV)的情况下被创建。

$ kubectl create -f https://raw.githubusercontent.com/fmrtl73/katacoda-scenarios-1/master/px-k8s-postgres-all-in-one/assets/px-postgres-pvc.yaml

persistentvolumeclaim "px-postgres-pvc" created

PostgreSQL的密码会被创建成Secret。运行下面的命令来用正确的格式创建Secret。

$ echo postgres123 > password.txt
$ tr -d '\n' .strippedpassword.txt && mv .strippedpassword.txt password.txt
$ kubectl create secret generic postgres-pass --from-file=password.txt
secret "postgres-pass" created

在Kubernetes上部署PostgreSQL

最后,让我们创建一个PostgreSQL实例,作为一个Kubernetes部署对象。为了简单起见,我们只部署一个单独的Postgres Pod。因为Portworx提供同步复制来达到高可用。因此一个单独的Postgres实例,是Postgres数据库的最佳部署方式。Portworx也支持多节点的Postgres部署方式,看你的需要。

$ kubectl create -f https://raw.githubusercontent.com/fmrtl73/katacoda-scenarios-1/master/px-k8s-postgres-all-in-one/assets/postgres-app.yaml

deployment "postgres" created

确保Postgres的Pods是在运行的状态。

$ kubectl get pods -l app=postgres -o wide --watch

等候直到Postgres pod变成运行状态。

我们可以通过使用与PostgresPod一起运行的pxctl工具,来检查Portworx卷。

$ VOL=`kubectl get pvc | grep px-postgres-pvc | awk '{print $3}'`
$ PX_POD=$(kubectl get pods -l name=portworx -n kube-system -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec -it $PX_POD -n kube-system -- /opt/pwx/bin/pxctl volume inspect ${VOL}

命令的输出信息,确认了支撑PostgreSQL数据库实例的卷已经被创建完成了。

PostgreSQL的错误恢复

让我们为数据库填充5百万行的样例数据。

我们首先找到运行PostgreSQL的Pod,来访问shell。

$ POD=`kubectl get pods -l app=postgres | grep Running | grep 1/1 | awk '{print $1}'`
$ kubectl exec -it $POD bash

现在我们进入了Pod,我们能够连接到Postgres并且创建数据库。

# psql
pgbench=# create database pxdemo;
pgbench=# \l
pgbench=# \q

默认状态下,Pgbench会创建4张表:(pgbench_branches,pgbench_tellers,pgbench_accounts,pgbench_history),在主pgbench_accounts表里会有10万行。这样我们创建了一个简单的16MB大小的数据库。

 

使用-s选项, 我们可以增加在每张表中的行的数量。在上面的命令中,我们在“scaling”上填写了50,这样pgbench就会创建一个50倍默认大小的数据库。

 

我们的pgbench_accounts现在有5百万行了。这样我们的数据库变成了800MB (50*16MB)

# pgbench -i -s 50 pxdemo;

等待直到pgbench完成表的创建。我们接着来确认一下

pgbench_accounts现在有500万行的填充。

# psql pxdemo
\dt
select count(*) from pgbench_accounts;
\q
exit

现在,我们来模拟PostgreSQL正在运行的节点的失效,

$ NODE=`kubectl get pods -l app=postgres -o wide | grep -v NAME | awk '{print $7}'`
$ kubectl cordon ${NODE}

node "ip-172-20-57-55.ap-southeast-1.compute.internal" cordoned

执行kubectl get nods, 确认了其中一个节点的排程已经失效了。

$ kubectl get nodes

我们继续删除这个PostgreSQLpod。

$ POD=`kubectl get pods -l app=postgres -o wide | grep -v NAME | awk '{print $1}'`
$ kubectl delete pod ${POD}

pod "postgres-556994cbd4-b6ghn" deleted

一旦删除完成。Portworx STorageORchestrator for Kubernetes (STORK)(https://portworx.com/stork-storage-orchestration-kubernetes/),会把pod重置来创建有数据复制集的节点。

一旦Pod被删除,它会被重置到有数据复制集的节点上。Portworx STorageORchestrator for Kubernetes (STORK) https://portworx.com/stork-storage-orchestration-kubernetes/)- Portworx的客户存储排程器,允许在数据所在节点上放置多个pod,并且确保正确的节点能够被选择来用来排程Pod。

 

让我们运行下面的命令验证一下。我们会发现一个新的pod被创建了,并且被排程在了一个不同的节点上。

$ kubectl get pods -l app=postgres

让我们把之前的节点重新部署回来。

$ kubectl uncordon ${NODE}

node "ip-172-20-57-55.ap-southeast-1.compute.internal" uncordoned

最后,我们验证一下数据仍然是可用的。

我们来看下容器里的pod名称和exec。

$ POD=`kubectl get pods -l app=postgres | grep Running | grep 1/1 | awk '{print $1}'`
$ kubectl exec -it $POD bash

现在用psql来确保我们的数据还在。

# psql pxdemo
pxdemo=# \dt
pxdemo=# select count(*) from pgbench_accounts;
pxdemo=# \q
pxdemo=# exit

我们看到数据库表都还在,并且所有的内容都是正确的。

在Postgres进行存储管理

测试了端到端的数据库错误恢复后,我们在Kubernetes集群上来运行StorageOps。

完全无停机下,扩充卷

我们现在来演示一下,在空间将满的情况下,如何简单的、动态的为卷添加空间。

在容器内打开一个shell,

$ POD=`kubectl get pods -l app=postgres | grep Running | awk '{print $1}'`
$ kubectl exec -it $POD bash

让我们来用pgbench来运行一个baseline transaction benchmark,它将尝试增加卷容量到1Gib,并且没能成功。

$ pgbench -c 10 -j 2 -t 10000 pxdemo
$ exit

在运行上面命令的时候,可能会有多种错误产生。第一个错误提示Pod已经没有空间了。

PANIC: could not write to file "pg_xlog/xlogtemp.73": No space left on device

Kubernetes并不支持在PVC创建后进行修改。我们在Portworx上用pxctl CLI工具来进行操作。

 

我们来获取卷的名称,用pxctl工具来查看。

 

SSH到节点里,运行下面的命令

POD=`/opt/pwx/bin/pxctl volume list --label pvc=px-postgres-pvc | grep -v ID | awk '{print $1}'`

$ /opt/pwx/bin/pxctl v i $POD

注意到卷还有10%就要满了。让我们用下面的命令来扩充。

$ /opt/pwx/bin/pxctl volume update $POD --size=2

Update Volume: Volume update successful for volume 834897770479704521

为卷做快照,并且恢复数据库

Portworx支持为Kubernetes PVCs创建快照。让我们为之前创建的Postgres PVC来创建一个快照。

$ kubectl create -f https://github.com/fmrtl73/katacoda-scenarios-1/raw/master/px-k8s-postgres-all-in-one/assets/px-snap.yaml

volumesnapshot "px-postgres-snapshot" created

可以通过下面的命令来看所有的快照。

$ kubectl get volumesnapshot,volumesnapshotdata

有了快照,我们来删掉数据库。

$ POD=`kubectl get pods -l app=postgres | grep Running | grep 1/1 | awk '{print $1}'`
$ kubectl exec -it $POD bash
$ psql
drop database pxdemo;
\l
\q
exit

快照就跟卷是一样的,我们可以使用它来创建一个新的PostgreSQL实例。让我们恢复快照数据,来创建一个新的PostgreSQL实例。

$ kubectl create -f https://raw.githubusercontent.com/fmrtl73/katacoda-scenarios-1/master/px-k8s-postgres-all-in-one/assets/px-snap-pvc.yaml

persistentvolumeclaim "px-postgres-snap-clone" created

从新的PVC,我们创建一个PostgreSQL Pod,

$ kubectl create -f https://raw.githubusercontent.com/fmrtl73/katacoda-scenarios-1/master/px-k8s-postgres-all-in-one/assets/postgres-app-restore.yaml

deployment "postgres-snap" created

确认这个pod是在运行状态。

$ kubectl get pods -l app=postgres-snap

最后,让我们访问由benchmark工具创建的数据。

$ POD=`kubectl get pods -l app=postgres-snap | grep Running | grep 1/1 | awk '{print $1}'`
$ kubectl exec -it $POD bash
$ psql pxdemo
\dt
select count(*) from pgbench_accounts;
\q
exit

我们发现表和数据都是正常的。如果我们想要在另一个Amazon区域创建一个容灾备份,我们可以把快照推送到Amazon S3。Portworx快照支持所有的S3兼容存储对象,所以备份也可以是其他的云或者是本地部署的数据中心。

_
小结

Portworx可以通过RKE很容易的部署,用来运行Kubernetes上生产系统中有状态的工作负载。通过跟STORK的整合,DevOps和StorageOps团队能够无缝的在Kubernetes上运行数据库集群。他们也可以为云原生应用运行传统的操作,比如扩充卷,快照,备份,容灾恢复。