阿里云故障演练平台获得可信云最高等级认证,为企业数字韧性能力保驾护航

alicloudnative阅读(55)评论(0)

7月27日,2021可信云大会在北京召开。会上,阿里云故障演练平台入选可信云最佳技术实践,并首批通过可信云混沌工程平台能力要求最高等级-先进级认证。同时,由信通院牵头,阿里云计算有限公司联合多家企业共同发起的“混沌工程实验室”宣布成立。

lALPDgfLSAcLsDLNBTXNB9Y_2006_1333.png_720x720g.jpg

双项认证,阿里云故障演练平台获可信云最高等级认证

随着企业对于云计算的理解和实践不断深入,基于云计算的分布式架构成为越来越多企业应用构建的首选方案,如何通过混沌工程提升云原生系统稳定性和保障业务连续性成为业内普遍关注的话题。

混沌工程是主要通过故障注入的方式,提前发现系统稳定性等问题,旨在提升系统和组织韧性,打造韧性的架构,保障业务连续性。在信通院可信云混沌工程平台测评中,阿里云故障演练平台以最高分成绩通过资源支持、故障场景、场景管理、实验流程、实验防护、实验度量、权限管理、安全审计等8项能力测评,并入选2021可信云最佳技术实践,双项认证,再一次证明了阿里云在混沌工程领域的技术和产品实力。

图片 1.png

故障演练随着阿里巴巴系统架构从微服务,到容器化,再到云原生一起发展,内部已有近10年的混沌工程落地实践经验。阿里云故障演练平台将阿里巴巴内部的实践经验以产品化的方式对外输出,提供丰富的实验场景和专家经验库、领域化的解决方案,满足用户的故障场景需求,在灵活的流程编排和开放的集成能力下,提供监控、报告等实现混沌工程实施闭环,通过权限管控和演练防护来控制故障演练的风险,帮助企业在云迁移、云就绪、云原生过程中提升系统稳定性和业务连续性。

2.png

自混沌工程理论提出以来,很多企业在探索和实践,但落地形式不同,阿里云故障演练平台有何不同?

  • 灵活的流程编排:制订了一套标准化的演练流程,在此基础上可以添加所需的流程节点。同时支持多场景的运行方式。
  • 可视化故障演练:与架构感知集成,在架构拓扑可视化的基础上,实现故障注入,同时可以配合架构巡检,发现系统风险点,使用故障演练进行验证。
  • 多样的专家经验库:将阿里巴巴内部多年的故障演练经验沉淀到演练模板中,具备演练场景的真实性和实用性,极大的提升演练创建的效率,同时解决用户上手混沌工程难的问题。
  • 领域化的解决方案:提供对服务组件、系统架构等稳定性验证的产品化解决方案,通过架构感知、依赖分析等动态识别组件和架构,自动生成演练方案,达到快、准、全的演练目的。

使用故障演练平台做混沌工程,可以衡量微服务的容错能力,估算系统容错红线,衡量系统容错能力。并且,故障演练平台可以验证容器编排配置是否合理,测试PaaS层是否健壮,验证监控告警的时效性,提升监控告警的准确和时效性。通过故障突袭,随机对系统注入故障,考察相关人员对问题的应急能力,以及问题上报、处理流程是否合理,达到以战养战,锻炼人定位与解决问题的能力。通过故障注入的方式,提前发现系统稳定性等问题,旨在提升系统和组织韧性,打造韧性的架构,保障业务连续性。

阿里云故障演练平台自2019年商业化以来,通过多样化的实验工具,自动化的工具部署,多维度的演练方式,灵活的流程编排,丰富的故障场景,实用的演练模板,专业的解决方案,安全的演练防护,深度的云产品集成,已经拥有近千个企业客户,服务了包括华泰证券、比心科技、亲宝宝等客户,助力企业在云原生时代构建数字韧性能力。

推动标准统一,打造ChaosBlade 开源项目,缩短构建混沌工程路径

近几年,越来越多的企业开始关注并探索混沌工程,渐渐成为测试系统高可用,构建对系统信息不可缺少的工具。但混沌工程领域目前还处于一个快速演进的阶段,最佳实践和工具框架没有统一标准。实施混沌工程可能会带来一些潜在的业务风险,经验和工具的缺失也将进一步阻止 DevOps 人员实施混沌工程。混沌工程领域目前也有很多优秀的开源工具,分别覆盖某个领域,但这些工具的使用方式千差万别,其中有些工具上手难度大,学习成本高,混沌实验能力单一,使很多人对混沌工程领域望而却步。

阿里巴巴集团在混沌工程领域已经实践多年,为了帮助企业更好地构建混沌工程路径,阿里巴巴在2019年开源了混沌工程项目 ChaosBlade,并在今年成为 CNCF Sandbox 项目。将”自研技术”、”开源项目”、”商业产品”形成统一的技术体系,阿里云通过三位一体的正向循环,实现了技术价值的最大化。

ChaosBlade 是一款遵循混沌工程原理的开源工具,包含混沌工程实验工具 chaosblade 和混沌工程平台 chaosblade-box,旨在通过混沌工程帮助企业解决云原生过程中高可用问题。实验工具 chaosblade 支持 3 大系统平台,4 种编程语言应用,共涉及 200 多个实验场景,3000 多个实验参数,可以精细化地控制实验范围。ChaosBlade 已成为阿里云故障演练平台基础能力底座服务众多企业客户。

3.png

未来,ChaosBlade 将继续以云原生为基础,提供面向多集群、多环境、多语言的混沌工程平台和混沌工程实验工具;后续会托管更多的混沌实验工具和兼容主流的平台,实现场景推荐,提供业务、系统监控集成,输出实验报告,在易用的基础上完成混沌工程操作闭环。

业内首个混沌工程实验室正式成立,推动混沌工程实践落地

在数字化产业对系统稳定性和云计算高可用要求越来越高的大背景下,由中国信通院牵头,阿里云等众多企业共同参与的混沌工程实验室正式成立。混沌工程实验室将推动混沌工程在各领域典型应用场景中的实践落地,联动云计算上下游企业来共同推进混沌工程快速发展。

阿里云拥有国内最丰富的混沌工程实践经验,并致力于打造云原生时代的混沌工程标准体系。阿里云在海量互联网服务以及历年双11场景的实践过程中,沉淀出了包括全链路压测、线上流量管控、故障演练等高可用核心技术,并通过开源和云上服务的形式对外输出,以帮助企业用户和开发者享受技术红利,提高开发效率,缩短业务的构建流程。

BoCloud博云:ESB老旧力不能支,微服务独立自治强势替代

BoCloud阅读(224)评论(0)

在微服务化建设中,应该考虑到 ESB ,也必须考虑到 ESB 。那么同样是面向服务的架构, ESB 与微服务相比差别在哪儿?微服务化的建设应该如何安置 ESB ?在新型架构终将取代腐化传统架构的背景下,如何迁移、替代,保证业务的可用性?接下来的内容里,将给大家做一个详细的分析。

SOA与ESB

技术架构的发展有其规律可循,从单体到垂直拆分、再到面向服务、最后到分布式微服务,从技术架构上来看是化整为零的,从服务管理角度来看是从单一的管理到集中式的管理。在服务化以后,也就是提出面向服务的架构以后,除了微服务以外,SOA 和 ESB 也是大家熟知的架构:

  • SOA架构:其实是一种设计理念,没有架构和技术的依赖,只是多个服务以独立的形式部署运行,服务间通过网络调用,最终提供一系列完整的功能。

  • ESB:企业服务总线,用来连接各个服务节点,服务间通信都通过 ESB 做路由和转发。最主要的是,不同应用可能有不同的协议和报文,ESB 最大的功能是解决不同的服务间互联互通。所以,ESB 其实也算是 SOA 的一种实现形式

微服务的特点

看着上面 SOA 的描述,是不是感觉跟微服务架构也差不多?或者说微服务是 SOA 的升华。那么微服务与 SOA、ESB 有什么不同呢?

我们先来看看微服务优势介绍:

1、 独立、专注、自治

  • 可以独立的部署、独立的运行,使用单独的数据库,甚至使用单独的前端。这样可以将故障影响面降低,不会出现牵一发动全身的事情。
  • 专注于自身的业务,比如报表服务,只关注于报表,也就能聚焦于报表业务,不需要考虑监控、告警和日志,只需要输入数据,生成报表。
  • 研发团队自治,可以拉起一个三五人的小团队,只做报表相关功能和业务,效率高、专业程度高、产生质量和价值高。

2、 分布式,高可用,扩缩容,资源分配

  • 微服务框架,支持分布式的部署和运行方式,解决了服务的高可用问题。
  • 通过注册中心的服务发现,实现服务的动态扩缩容,可根据业务并发量,自动调整横向的扩缩容量。
  • 根据不同的业务模块,调整服务的资源配比。例如在财务系统中,用户管理模块(用户增删改)使用频率较低,可以部署一个资源少、单副本的用户服务;但是账务模块使用极其频繁,每天达到上百单,可能分配一个资源多、多副本的账务服务。但是如果非微服务架构的话,就需要做系统的整体扩容,以满足最大业务量的模块,会造成其他模块的资源浪费。

3、 服务解耦、服务化

  • 拆分了微服务以后,服务间的依赖都会通过网络传输的方式提供。也就是说一个服务只需要提供所需的功能接口,就可以满足整体的业务实现。因此,微服务使用什么编程语言,如何实现业务逻辑都可以不用考虑,只需要提供该有的功能接口(只关注于结果)。这样服务在开发、管理、使用的时候,可以实现真正的解耦,不会受到开发框架、开发模式的制约。
  • 微服务化以后,服务的业务能力,不只是该系统可以使用,其他有需要的部门或者系统也可以通过接口调用该微服务,提供给其业务处理能力。

ESB对比微服务

1、 独立而不自由。相对于微服务,SOA架构可以独立部署业务服务,但是不够自由,不能像微服务那样自由互通,只能通过 ESB 做服务通信,所有服务通信都需要 ESB 转发,在 ESB 处集中管理,配置权限、配置策略。

2、 非分布式解决方案,服务的高可用无法通过框架整体解决,只能通过传统方式解决。所以,更无法实现横向动态的扩缩容。

3、 ESB 尽管也是面向服务的框架,但是却无法真正的实现服务化。服务化,应该是自消费的模式,微服务的服务能力提供以后,使用者可以实现订阅使用。但是 ESB 中,服务能力提供出来只是给特定场景的某几个服务使用,其他服务如果想使用,需要通过定制化的增加。

4、 与自消费的道理相同,服务提供者不能实现自动化的服务提供,ESB中需要增加对应的接口以便于服务提供相应的业务能力。所以在ESB建设中,会提前制定好需要暴露的指定接口,如果有新增的需求,需要针对性的定制化。而微服务提供的则是一个微服务平台,只要遵循特定的协议,无论是服务的提供还是使用,都可以自由的上下线和增减。

5、 微服务架构是新型的服务化架构,而 ESB 架构很容易腐化,且维护困难,技术核心非开源,受厂商绑定。微服务基本上都是基于开源的新型的技术,不存在腐化,且可以自主掌握核心技术

6、 ESB 是集中化的服务管理模式,ESB 的性能将决定集团整体的通信性能,且由于过于“集中”导致一旦总线出现问题,整个集团的信息化系统将面临“瘫痪”的风险。而微服务则不同,微服务采用去中心化方案,任何一个组件出现问题都不会影响全局的业务

 

ESB终将被替代

业务持续增长,所以需要技术不断革新,促进技术框架也不断进步。一切变化的源头都在业务的变化,业务消费量成几何倍增长,所以要求服务可以自由的扩缩容;新型业务的不断增加,要求服务支持动态的运营和管理。而 ESB 的架构过于死板,已经完全不适合云原生理念下的服务治理

老旧的系统架构逐渐力不能支,新型的微服务架构应运而生,在这个趋势的指引下,我们开始做微服务化的建设,但是在微服务化建设期间 ESB 如何安置,是一个比较大的问题。企业中多数系统的互联互通都依赖于 ESB 的服务总线,但同时改造所有 ESB 接入的系统,工程量会很巨大,容错率会很低,危险性也极高。因此 ESB 的替代尽管势在必行,却也需要慎重的规划

通常 ESB 的改造需要采用逐步改造加逐步迁移的方式,根据不同的系统现状,做相应的方式处理:

  • 接受深度改造的系统,通过拆分改造成微服务系统,那么之前的调用方式可以直接修改成微服务的调用方式,这部分在我们的微服务化建设当中,需根据实际情况做好相应的建设规划;
  • 支持轻度改造的系统,不做微服务化拆分,但是可以改造调用或访问的方式和地址。这部分系统可以将调用地址直接换成 API 网关的地址,通过 API 网关提供原 ESB 的如协议转换、路由转发、认证控制等功能,替换掉 ESB 的代理;
  • 完全不接受改造的系统,或者对 ESB 依赖极强的系统,在做深度改造之前,ESB 不能被取代,那么仍需要保留 ESB 。但是需要考虑新改造的,或者增量的微服务系统,与 ESB、与老旧系统之间的通信,因此需要对运行中的 ESB 做兼容和纳管。

在微服务建设的初期,ESB 因在企业中起到的关键作用,并不能完全替换或取代,因此需要有个过渡期:通过微服务管理台,兼容纳管 ESB 的服务;通过API 网关转换报文,路由转发 ESB 的接口,以保证存量的系统与增量或改造的系统兼容管理、互联互通。

其实 ESB 的功能,路由、转发,与 API 网关的功能极其相似。接下来,我们将会重点分析 API 网关在微服务中的作用与 ESB 的区别和优劣,敬请期待。

Rainbond Helm 应用商店对接管理实现分析

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

本文作者:Rainbond 项目维护者:黄润豪

Rainbond 是一个完全开源,简单易用的云原生应用管理平台。除了支持内置的本地组件库, 云原生应用商店, 还支持 Helm 应用商店. 用户把常用的 Helm 仓库对接到 Rainbond 后, 可以简单, 方便地对Helm应用进行配置和安装。

Rainbond Helm 应用商店的功能

添加 Helm 应用商店

当前版本支持基于Helm v3协议添加外部应用商店。

添加 Helm 应用商店

Helm 应用列表

Helm 应用列表

搜索 Helm 应用

Helm 应用列表

Helm 应用商店实现

在开始介绍基本原理时, 我们先简单介绍下 Helm 仓库, 让大家对 Helm 仓库有基础的理解.

Helm 仓库

Helm 仓库的应用模板列表索引会放在 index.yaml 里面,可以通过 <仓库地址>/index.yaml 获取。

比如: https://openchart.goodrain.com/goodrain/rainbond/index.yaml

index.yaml 详情如下:

apiVersion: v1
entries:
 nfs-client-provisioner:
 - apiVersion: v1
 appVersion: 3.1.0
 created: "2021-07-08T08:20:40Z"
 description: nfs-client is an automatic provisioner that used your *already configured*
 NFS server, automatically creating Persistent Volumes.
 digest: 1197847e6ee3720e4f7ee493985fce3703aa42813fa409dc6c0ebaa4fef3f175
 home: https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client
 keywords:
 - nfs
 - storage
 maintainers:
 - email: bart@verwilst.be
 name: verwilst
 name: nfs-client-provisioner
 sources:
 - https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client
 urls:
 - charts/nfs-client-provisioner-1.2.8.tgz
 version: 1.2.8
 rainbond-console:
 - apiVersion: v2
 appVersion: 1.16.0
 created: "2021-07-08T08:36:05Z"
 description: Goodrain Rainbond-console Helm chart for Kubernetes
 digest: 5ecba097ee7425d16a1a4512c6bb82ca3e7d04b8491d098226c4a9517e3507be
 home: https://github.com/goodrain/rainbond
 icon: https://www.rainbond.com/images/rainbondlog.png
 name: rainbond-console
 sources:
 - https://github.com/goodrain/rainbond/
 type: application
 urls:
 - charts/rainbond-console-5.3.1.tgz
 version: 5.3.1
generated: "2021-07-08T08:36:12Z"
serverInfo: {}

可以看到 index.yaml 记录了应用模板列表, 每个应用模板的元数据, 包括 版本号(version), 说明(description), charts 包地址(urls) 等等.

基本原理

┌────────────────┐        ┌───────────────────────┐
│                │        │                       │
│   index.yaml   ├───────►│ application templates │
│                │        │                       │
└────────────────┘        └───────────────────────┘

添加 Helm 应用商店

添加Helm 应用商店需要把 Helm 应用商店的元数据记录下来, 持久化到数据库中. 在正式添加前, 还需要检查商店的可用心.

代码如下:

func (a *appStoreRepo) Create(ctx context.Context, appStore *domain.AppStore) error {
 // Check the availability of the app store.
 if err := a.isAvailable(ctx, appStore); err != nil {
 return err
 }
 return a.appStoreDao.Create(&model.AppStore{
 Name: appStore.Name,
 URL: appStore.URL,
 Branch: appStore.Branch,
 })
}

Helm 应用列表

Helm 应用商店的主要逻辑:

  • 建立 HTTP 请求去获取 index.yaml 文件

  • 将 HTTP 响应反序列化到结构体 IndexFile 中. IndexFile 是 Helm 官方定义的结构体.

  • 遍历 IndexFile 的 Entries, 生成我们想要的应用模板列表(appTemplates)

代码如下:

func (h *helmAppTemplate) fetch(ctx context.Context, appStore *domain.AppStore) ([]*domain.AppTemplate, error) {
 // 建立 HTTP 请求去访问 index.yaml
 req, err := http.NewRequest("GET", appStore.URL+"/index.yaml", nil)
 ...
 body, err := ioutil.ReadAll(resp.Body)
 ...
 jbody, err := yaml.YAMLToJSON(body)
 ...
 // 解析返回结果, 反序列化到结构体 hrepo.IndexFile 中
 var indexFile hrepo.IndexFile
 if err := json.Unmarshal(jbody, &indexFile); err != nil {
 return nil, errors.Wrap(err, "read index file")
 }
 ...
 // 解析成应用列表
 var appTemplates []*domain.AppTemplate
 for name, versions := range indexFile.Entries {
 appTemplate := &domain.AppTemplate{
 Name: name,
 Versions: versions,
 }
 appTemplates = append(appTemplates, appTemplate)
 }
 return appTemplates, nil
}

应用列表的缓存

为了加快响应速度, 应用模板列表会被缓存起来, 目前支持的缓存是内存。

代码如下:

appTemplates, err := s.appTemplater.Fetch(ctx, appStore)
if err != nil {
 return nil, err
}
appStore.AppTemplates = appTemplates
s.store.Store(appStore.Key(), appStore)

防止缓存击穿

使用缓存时, 我们还应该考虑高并发情况下缓存击穿的问题. 缓存击穿的意思是在某个数据过期后, 有大量的并发访问该数据, 从而导致数据库压力剧增. 当然, 我们获取应用列表不是去访问数据库, 而是发出 HTTP 请求, 获取 index.yaml。

为了解决防止缓存击穿, 引入了 singleflight. singleflight 会确保同一个 key, 同一时刻, 只会有一个请求在执行. 也就是说, 对于同一个 Helm 应用商店, 在同一个时刻, 只会有一个获取 index.yaml 文件的 HTTP 请求。

代码如下:

type helmAppTemplate struct {
 singleflight.Group
}
func (h *helmAppTemplate) Fetch(ctx context.Context, appStore *domain.AppStore) ([]*domain.AppTemplate, error) {
 // single flight to avoid cache breakdown
 v, err, _ := h.Do(appStore.Key(), func() (interface{}, error) {
 return h.fetch(ctx, appStore)
 })
 appTemplates := v.([]*domain.AppTemplate)
 return appTemplates, err
}

Charts 包的缓存

每次安装应用时, Helm 会先拉取应用的 Charts 包, 缓存到本地存储中.

Helm 对 Charts 包的缓存

在 Mac 中, Helm 缓存 charts 包的路径是 $HOME/Library/Caches/helm/repository:

.
├── bitnami-charts.txt
├── bitnami-index.yaml
├── mysql-8.5.1.tgz
├── nginx-8.8.3.tgz
├── phpmyadmin-8.2.4.tgz
├── rainbond-charts.txt
├── rainbond-index.yaml
├── rainbond-operator-1.3.0.tgz
├── redis-cluster-5.0.1.tgz

所有 Helm 仓库的 chart 包都会缓存到该目录下. 需要注意的是, 该目录下的 charts 包是不区分仓库的. 也就是说, 如果 bitnami 和 rainbond 仓库都有版本为 8.5.1 的 mysql 应用, 那么 bitnami 的 mysql-8.5.1.tgz 就可能会被 rainbond 的 mysql-8.5.1.tgz 覆盖掉, 导致 bitnami 仓库的缓存失效. 详情请查看 #4618

Rainbond 对 Charts 包的缓存

Rainbond 没有直接沿用 Helm 对 Charts 包的缓存方式, 而是做了一些扩展, 使其支持区分仓库的 Charts 包的缓存:

  • 构造 Charts 包的缓存路径, 格式为 主缓存路径/仓库名/应用名/应用版本/.

  • 构造应用的包名 应用名-版本号.tgz.

  • 检查存储中时候已经有目标 Charts 包, 并检查他们的 hash 是否一样.

  • 如果缓存红没有目标 Charts 包, 则进行下载, 将Charts 包缓存起来.

代码如下:

func (h *Helm) locateChart(chart, version string) (string, error) {
 repoAndName := strings.Split(chart, "/")
 ...
 // Charts 包的路径为: 主缓存路径/仓库名/应用名/应用版本/
 chartCache := path.Join(h.settings.RepositoryCache, chart, version)
 cp := path.Join(chartCache, repoAndName[1]+"-"+version+".tgz")
 if f, err := os.Open(cp); err == nil {
 defer f.Close()
 // 检查 hash
 // check if the chart file is up to date.
 hash, err := provenance.Digest(f)
 ...
 // get digiest from repo index.
 digest, err := h.getDigest(chart, version)
 ...
 if hash == digest {
 return cp, nil
 }
 }
 cpo := &ChartPathOptions{}
 cpo.Version = version
 settings := h.settings
 // 下载 Charts 包
 cp, err := cpo.LocateChart(chart, chartCache, settings)
 ...
 return cp, err
}

接下来

目前的实现还有非常大的优化的空间, 比如应用列表缓存插件化, 应用列表缓存可过期, 清理 Charts 包缓存.

应用列表缓存插件化

应用列表缓存目前只支持内存. 随着 Helm 商店数量的越来越多, 应用模板的数量也将越来越多, 这对缓存的大小的要求将会比较高. 这场景下, 内存显然不是合适选择. 所以, 有必然将缓存抽象出来, 与内存解耦, 变得插件化, 可以轻松得对接其它的缓存, 比如 redis.

应用列表缓存可过期

细心的同学可能已经发现, 目前的内存缓存是不支持过期的, 也就是说, 在不重启应用的情况下, 缓存会越来越大. 所以, 不管是缓存在内存中, 还是缓存在 redis 中, 都非常有必要支持缓存过期. 缓存的过期策略可以是 LRU, LFU 等等, 同时结合过期时间, 清理长时间不被使用的缓存.

另外, 减少应用模板中不必要的字段, 从而减小每个应用模板的大小, 也是个不错的优化点.

清理 Charts 包缓存

Charts 包缓存在磁盘上, 不会过期, 目前也没有清理的机制, 这可能会使得磁盘的占用非常大. 接下来, 需要设计合适的方案对其进行定期的清理.

总结

本文结合代码和大家一起分析了Rainbond Helm 应用商店对接实现.

Reference

  1. Helm: https://github.com/helm/helm

  2. What is cache penetration, cache breakdown and cache avalanche?: https://www.pixelstech.net/article/1586522853-What-is-cache-penetration-cache-breakdown-and-cache-avalanche

  3. singleflight: https://pkg.go.dev/golang.org/x/sync/singleflight

社区

如果您对Rainbond项目感兴趣,如果您有一些疑问,如果您对云原生、Kubernetes等技术感兴趣,欢迎加入Rainbond 社区钉钉群。

dingding-group.png

KubeSphere 3.1.1 发布,可以接入集群已有的 Prometheus

KubeSphere阅读(549)评论(0)

KubeSphere 作为一款面向应用的开源容器平台,经过 3 年的发展和 10 个版本的迭代,收获了两百多位开源贡献者,超过十万次下载,并有数千名社区用户用 KubeSphere 作为企业容器平台。

7 月 7 日,KubeSphere 3.1.1 版本正式发布,现在部署 KubeSphere 时可以指定 Kubernetes 集群中已有的 Prometheus 啦!!!

鉴于最近有很多 KubeSphere 社区用户询问 KubeSphere 3.1.1 的发布进度,本文将会详细介绍 KubeSphere 3.1.1 比较重要的新特性。

KubeSphere 3.1.1 不是大版本更新,只是针对 v3.1.0 的 patch 版本,也就是修修补补的工作。本文只列出优化增强的功能,问题修复请查看完整的 Release Notes。

安装与升级

优化增强的功能

用户体验

  • 删除工作负载时支持批量删除关联资源 #1933
  • 优化页面弹框 #2016
  • 允许在 system-workspace 下的项目中使用容器终端 #1921

可观测性

  • 优化了通知设置中端口号的格式限制#1885
  • 支持安装时指定使用已有的 Prometheus. #1528

微服务治理

  • 优化 trace 页面增加时间选择器 #2022

DevOps

  • 支持 GitLab 多分支流水线按分支名筛选过滤 #2077
  • 更改了 b2i 页面的“重新执行”按钮为“执行”#1981

多集群管理

  • 优化了 member 集群配置错误时的提示信息 #2084 #1965

计量计费

  • 计量计费部分的 UI 调整 #1896
  • 修改了计量计费按钮的颜色 #1934

应用商店

安全

  • 切换 jwt-go 的分支,用于修复 CVE-2020-26160 #3991
  • 升级 protobuf 版本至 v1.3.2 用于修复 CVE-2021-3121 #3944
  • 升级 crypto 至最新版用于修复 CVE-2020-29652 #3997
  • 移除了 yarn.lock 文件以避免一些 CVE 漏洞误报 #2024

存储

  • 提升了 s3 uploader 并发性能 #4011
  • 增加预置的 CSI Provisioner CR 配置 #1536

KubeEdge 集成

下载与升级

大家可以到 KubeSphere GitHub 主页查看完整的 英文版 KubeSphere 3.1.1 Release Notes,了解更多与升级有关的注意事项。国内用户也可以访问下方链接来查看完整的 Release Notes:

关于 KubeSphere

KubeSphere (https://kubesphere.io)是在 Kubernetes 之上构建的开源容器混合云,提供全栈的 IT 自动化运维的能力,简化企业的 DevOps 工作流。

KubeSphere 已被 Aqara 智能家居、杭州数跑科技、本来生活、新浪、华夏银行、四川航空、国药集团、微众银行、紫金保险、中通、中国人保寿险、中国太平保险、中移金科、Radore、ZaloPay 等海内外数千家企业采用。KubeSphere 提供了开发者友好的向导式操作界面和丰富的企业级功能,包括多云与多集群管理、Kubernetes 资源管理、DevOps (CI/CD)、应用生命周期管理、微服务治理 (Service Mesh)、多租户管理、监控日志、告警通知、审计事件、存储与网络管理、GPU support 等功能,帮助企业快速构建一个强大和功能丰富的容器云平台。

 ✨ GitHub:https://github.com/kubesphere
 💻 官网(中国站):https://kubesphere.com.cn
 👨‍💻‍ 微信群:请搜索添加群助手微信号 kubesphere

基于Rancher安装Rainbond v5.3.1控制台

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

本文适用于正在使用 Rancher 或对 Rancher 有所了解的用户。本文作者:好雨云交付工程师 张齐。

本文将介绍:

  • 如何在Rancher中安装Rainbond控制台。

  • Rainbond如何对接Rancher管理的Kubernetes集群。

  • 演示安装完整流程供用户参考。

参考视频:https://www.bilibili.com/video/BV1a54y1n7Ch/

前提要求

  1. 具有一套稳定可用的 Rancher 环境,若还没有可参考 Rancher 安装部署文档

  2. Kubernetes 集群具有至少 4GB 以上的空闲调度内存

  3. Kubernetes 版本在 1.13 ~1.19区间

  4. Kubernetes 集群至少有一个 80⁄443 端口未被占用的节点,用于Rainbond网关部署。

  5. Kubernetes 集群存在可用的 StorageClass

    如果集群内没有 StorageClass,可以参考 https://artifacthub.io/packages/helm/kvaps/nfs-server-provisioner

  6. StorageClass是NFS,则需要在集群内安装NFS 客户端。如果没有安装,可以参考:

    # CentOS 系统
    yum install -y nfs-utils
    # Ubuntu/Debian 系统
    apt install -y nfs-common

开始安装

添加 Rainbond Console 到应用商店

将 Rainbond Console 添加到 Rancher 的应用商店中。

  1. 应用商店页面中,单击添加应用商店

  2. 输入名称(比如 Rainbond-Console)和 商店 URL 地址 输入 https://gitee.com/rainbond/rainbond-console.git

  3. 分支选择masterHelm 版本选择Helm v3

  4. 单击创建完成应用商店添加。

image-20210708163003499

  1. 回到应用商店页面中,单击启动

  2. 单击刷新,等 Rancher 同步完后,就可以看到刚才新加的 Rainbond Console 了。

安装 Rainbond Console

在 Rancher 商店中启动 Rainbond Console

  1. 单击识别出的 rainbond-console Chart,开始 Chart 的安装。

  2. 选择命名空间或新创建命名空间。

  3. 修改一些启动配置:

    • 可选使用默认镜像或自定义镜像。

    • 可选择手动填写或自动生成Mysql、Redis密码。

    • 选择存储类。

    • 设置Rainbond-Console NodePort端口。

image-20210708170910377

  1. 填写完成后,点击启动,创建Rainbon-console。

  2. 等待所有POD启动完成后,访问上面设置的Rainbond-Console NodePort端口,默认是 30707

访问 Rainbond-Console,对接已有集群

根据页面提示完成账号注册,进入集群安装页面,选择 接入Kubernetes集群,填写集群的kubeconfig文件内容,开始Rainbond集群的安装。

image-20210712174937793

在安装之前,如需要配置集群初始化参数,则点击下图红框中的内容填写初始化参数配置。

image-20210712175145269

集群初始化配置参考示例:

集群初始化详细参数配置请参考文档

Rainbond集群初始化默认会安装 NFS-Server,集群中已存在 storageClassName 可以复用此存储。

 metadata:
 name: rainbondcluster
spec:
 #设置Rainbond对外网关IP,可以是公网IP或负载均衡
 gatewayIngressIPs:
 - 39.103.228.113
 #设置网关节点IP
 nodesForGateway:
 #内网IP
 - internalIP: xxxx
 # 节点hostname
 name: xxxx
 #指定集群中的 storageClass
 rainbondVolumeSpecRWX:
 storageClassName: nfs-server

了解 Rancher 用户使用 Rainbond 的优势

  • 无需深入学习 Kubernetes 各类资源的使用方式

Rainbond 使用云原生应用模型的方式提供给用户智能化、简单的应用开发管理模式。不管是简单应用还是复杂的微服务架构,整个开发部署过程无需开发者深入学习 Kubernetes 相关知识。

  • 标准的云原生 12 要素应用管理模式

你或许听说过云原生 12 要素,作为目前推荐的云原生应用开发模式。Rainbond 应用模型对云原生 12 要素进行了充分的实践,使用 Rainbond,天然地使你的代码满足云原生要求。

  • 从源代码到云端

常用的开发语言(Java、PHP、Python、Golang、NodeJS、.NetCore)无需定义 Dockerfile、无需定义 Kubernetes 部署方式即可完成持续构建、持续部署。

  • 标准应用多集群交付

Rainbond 提供多种方式便于开发者在多个集群,多个环境中快速交付应用,获取 SaaS 化应用交付体验。

  • 微服务架构

Rainbond 内置 ServiceMesh 微服务架构治理框架,所有部署组件按照微服务的治理思路进行管理,微服务治理功能开箱即用的。

常见问题

  • Rancher 已经部署的应用能否直接由 Rainbond 接管

这个问题是大多数用户的疑问,我们希望达成 Rainbond 可以自动化的接管 Rancher 部署的应用。然而遗憾的是由于 Rancher 即同类型平台部署应用时目前都不会遵循标准规范(比如OAM),导致我们很难 100% 兼容的转换 Rancher 已经部署的应用成为 Rainbond 应用模型。因此目前我们还是推荐用户直接使用 Rainbond 提供的基于源代码、基于镜像快速的重新部署应用(相对于部分转化后再进行人工干预优化更节省时间)。同时也便于用户在这个过程中了解 Rainbond 应用管理的机制和流程。

  • Rainbond 部署的应用是否可以从 Rancher 视图中进行管理

Rainbond 部署到 Kubernetes 集群中的资源都是由 Rainbond 控制器进行创建、升级和回收,使用 Rainbond 定义的资源创建规范。我们并不推荐用户在 Rancher 中直接对这些资源进行修改。但可以进行观测,比如日志观测、资源监控观测等等。

  • 通过Rancher安装Rainbond-console POD一直处于不可用状态

首先排查POD之间是否可以通信,如不能通信则需做些优化,比如:开启内核转发、关闭Firewalld、关闭交换分区等等。

(更多…)

KubeSphere Helm 应用仓库源码分析

KubeSphere阅读(328)评论(0)

作者:蔡锡生,LStack 平台研发工程师,近期专注于基于 OAM 的应用托管平台落地。

背景介绍

KubeSphere 应用商店简介

作为一个开源的、以应用为中心的容器平台,KubeSphere 在 OpenPitrix 的基础上,为用户提供了一个基于 Helm 的应用商店,用于应用生命周期管理。OpenPitrix 是一个开源的 Web 平台,用于打包、部署和管理不同类型的应用。KubeSphere 应用商店让 ISV、开发者和用户能够在一站式服务中只需点击几下就可以上传、测试、部署和发布应用。

KubeSphere 中的 Helm 仓库功能

  • KubeSphere Helm 仓库添加

  • Helm repo list

  • KubeSphere Helm 仓库中的应用模版查询

Helm 仓库简介

Helm charts 是存放 K8s 应用模版的仓库,该仓库由 index.yaml 文件和 .tgz 模版包组成。

[root@ningbo stable]# ls -al 
总用量 400
drwxr-xr-x. 26 root root   4096 6  22 17:01 .
drwxr-xr-x.  4 root root     86 6  22 16:37 ..
-rw-r--r--.  1 root root  10114 6  22 17:12 index.yaml
-rw-r--r--.  1 root root   3803 6   8 2020 lsh-cluster-csm-om-agent-0.1.0.tgz
-rw-r--r--.  1 root root   4022 6   8 2020 lsh-mcp-cc-alert-service-0.1.0.tgz
-rw-r--r--.  1 root root   4340 6   8 2020 lsh-mcp-cc-sms-service-0.1.0.tgz
-rw-r--r--.  1 root root   4103 6   8 2020 lsh-mcp-cpm-metrics-exchange-0.1.0.tgz
-rw-r--r--.  1 root root   4263 6   8 2020 lsh-mcp-cpm-om-service-0.1.0.tgz
-rw-r--r--.  1 root root   4155 6   8 2020 lsh-mcp-csm-om-service-0.1.0.tgz
-rw-r--r--.  1 root root   3541 6   8 2020 lsh-mcp-deploy-service-0.1.0.tgz
-rw-r--r--.  1 root root   5549 6   8 2020 lsh-mcp-iam-apigateway-service-0.1.0.tgz
  • index.yaml 文件
apiVersion: v1
entries:
  aliyun-ccm:
  - apiVersion: v2
    appVersion: addon
    created: "2021-06-21T08:59:58Z"
    description: A Helm chart for Kubernetes
    digest: 6bda563c86333475255e5edfedc200ae282544e2c6e22b519a59b3c7bdef9a32
    name: aliyun-ccm
    type: application
    urls:
    - charts/aliyun-ccm-0.1.0.tgz
    version: 0.1.0
  aliyun-csi-driver:
  - apiVersion: v2
    appVersion: addon
    created: "2021-06-21T08:59:58Z"
    description: A Helm chart for Kubernetes
    digest: b49f128d7a49401d52173e6f58caedd3fabbe8e2827dc00e6a824ee38860fa51
    name: aliyun-csi-driver
    type: application
    urls:
    - charts/aliyun-csi-driver-0.1.0.tgz
    version: 0.1.0
  application-controller:
  - apiVersion: v1
    appVersion: addon
    created: "2021-06-21T08:59:58Z"
    description: A Helm chart for application Controller
    digest: 546e72ce77f865683ce0ea75f6e0203537a40744f2eb34e36a5bd378f9452bc5
    name: application-controller
    urls:
    - charts/application-controller-0.1.0.tgz
    version: 0.1.0
  • .tgz 解压缩后的文件目录
[root@ningbo stable]# cd mysql/
[root@ningbo mysql]# ls -al
总用量 20
drwxr-xr-x.  3 root root   97 5  25 2020 .
drwxr-xr-x. 26 root root 4096 6  22 17:01 ..
-rwxr-xr-x.  1 root root  106 5  25 2020 Chart.yaml
-rwxr-xr-x.  1 root root  364 5  25 2020 .Helmignore
-rwxr-xr-x.  1 root root   76 5  25 2020 index.yaml
drwxr-xr-x.  3 root root  146 5  25 2020 templates
-rwxr-xr-x.  1 root root 1735 5  25 2020 values.yaml
  • Chart.yaml
[root@ningbo mysql]# cat Chart.yaml 
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for Kubernetes
name: mysql
version: 0.1.0

添加 Helm 仓库代码介绍

接口实现分析

  1. 路由注册
  2. handler,参数解析,调用 models 方面
  3. models ,调用 models 方法
  4. crd client,调用 K8s api 存储
    webservice.Route(webservice.POST("/repos").
        To(handler.CreateRepo). // 跟进
        Doc("Create a global repository, which is used to store package of app").
        Metadata(restfulspec.KeyOpenAPITags, []string{constants.OpenpitrixTag}).
        Param(webservice.QueryParameter("validate", "Validate repository")).
        Returns(http.StatusOK, api.StatusOK, openpitrix.CreateRepoResponse{}).
        Reads(openpitrix.CreateRepoRequest{}))
func (h *openpitrixHandler) CreateRepo(req *restful.Request, resp *restful.Response) {

    createRepoRequest := &openpitrix.CreateRepoRequest{}
    err := req.ReadEntity(createRepoRequest)
    if err != nil {
        klog.V(4).Infoln(err)
        api.HandleBadRequest(resp, nil, err)
        return
    }

    createRepoRequest.Workspace = new(string)
    *createRepoRequest.Workspace = req.PathParameter("workspace")

    user, _ := request.UserFrom(req.Request.Context())
    creator := ""
    if user != nil {
        creator = user.GetName()
    }
    parsedUrl, err := url.Parse(createRepoRequest.URL)
    if err != nil {
        api.HandleBadRequest(resp, nil, err)
        return
    }
    userInfo := parsedUrl.User
    // trim credential from url
    parsedUrl.User = nil

    repo := v1alpha1.HelmRepo{
        ObjectMeta: metav1.ObjectMeta{
            Name: idutils.GetUuid36(v1alpha1.HelmRepoIdPrefix),
            Annotations: map[string]string{
                constants.CreatorAnnotationKey: creator,
            },
            Labels: map[string]string{
                constants.WorkspaceLabelKey: *createRepoRequest.Workspace,
            },
        },
        Spec: v1alpha1.HelmRepoSpec{
            Name:        createRepoRequest.Name,
            Url:         parsedUrl.String(),
            SyncPeriod:  0,
            Description: stringutils.ShortenString(createRepoRequest.Description, 512),
        },
    }

    if strings.HasPrefix(createRepoRequest.URL, "https://") || strings.HasPrefix(createRepoRequest.URL, "http://") {
        if userInfo != nil {
            repo.Spec.Credential.Username = userInfo.Username()
            repo.Spec.Credential.Password, _ = userInfo.Password()
        }
    } else if strings.HasPrefix(createRepoRequest.URL, "s3://") {
        cfg := v1alpha1.S3Config{}
        err := json.Unmarshal([]byte(createRepoRequest.Credential), &cfg)
        if err != nil {
            api.HandleBadRequest(resp, nil, err)
            return
        }
        repo.Spec.Credential.S3Config = cfg
    }

    var result interface{}
    // 1. validate repo
    result, err = h.openpitrix.ValidateRepo(createRepoRequest.URL, &repo.Spec.Credential)
    if err != nil {
        klog.Errorf("validate repo failed, err: %s", err)
        api.HandleBadRequest(resp, nil, err)
        return
    }

    // 2. create repo
    validate, _ := strconv.ParseBool(req.QueryParameter("validate"))
    if !validate {
        if repo.GetTrueName() == "" {
            api.HandleBadRequest(resp, nil, fmt.Errorf("repo name is empty"))
            return
        }
        result, err = h.openpitrix.CreateRepo(&repo) //跟进
    }

    if err != nil {
        klog.Errorln(err)
        handleOpenpitrixError(resp, err)
        return
    }

    resp.WriteEntity(result)
}
func (c *repoOperator) CreateRepo(repo *v1alpha1.HelmRepo) (*CreateRepoResponse, error) {
    name := repo.GetTrueName()

    items, err := c.repoLister.List(labels.SelectorFromSet(map[string]string{constants.WorkspaceLabelKey: repo.GetWorkspace()}))
    if err != nil && !apierrors.IsNotFound(err) {
        klog.Errorf("list Helm repo failed: %s", err)
        return nil, err
    }

    for _, exists := range items {
        if exists.GetTrueName() == name {
            klog.Error(repoItemExists, "name: ", name)
            return nil, repoItemExists
        }
    }

    repo.Spec.Description = stringutils.ShortenString(repo.Spec.Description, DescriptionLen)
    _, err = c.repoClient.HelmRepos().Create(context.TODO(), repo, metav1.CreateOptions{}) // 跟进
    if err != nil {
        klog.Errorf("create Helm repo failed, repo_id: %s, error: %s", repo.GetHelmRepoId(), err)
        return nil, err
    } else {
        klog.V(4).Infof("create Helm repo success, repo_id: %s", repo.GetHelmRepoId())
    }

    return &CreateRepoResponse{repo.GetHelmRepoId()}, nil
}
// Create takes the representation of a HelmRepo and creates it.  Returns the server's representation of the HelmRepo, and an error, if there is any.
func (c *HelmRepos) Create(ctx context.Context, HelmRepo *v1alpha1.HelmRepo, opts v1.CreateOptions) (result *v1alpha1.HelmRepo, err error) {
    result = &v1alpha1.HelmRepo{}
    err = c.client.Post().
        Resource("Helmrepos").
        VersionedParams(&opts, scheme.ParameterCodec).
        Body(HelmRepo).
        Do(ctx).
        Into(result)
    return
}

查询 Helm 仓库应用模版代码介绍

接口实现

  1. 路由注册
  2. handler,参数解析,调用 models 方面
  3. models ,调用 models 方法
  4. crd client,调用 K8s api 存储
webservice.Route(webservice.GET("/apps").LiHui, 6 months ago: • openpitrix crd
        Deprecate().
        To(handler.ListApps).  // 跟进
        Doc("List app templates").
        Param(webservice.QueryParameter(params.ConditionsParam, "query conditions,connect multiple conditions with commas, equal symbol for exact query, wave symbol for fuzzy query e.g. name~a").
            Required(false).
            DataFormat("key=%s,key~%s")).
        Param(webservice.QueryParameter(params.PagingParam, "paging query, e.g. limit=100,page=1").
            Required(false).
            DataFormat("limit=%d,page=%d").
            DefaultValue("limit=10,page=1")).
        Param(webservice.QueryParameter(params.ReverseParam, "sort parameters, e.g. reverse=true")).
        Param(webservice.QueryParameter(params.OrderByParam, "sort parameters, e.g. orderBy=createTime")).
        Metadata(restfulspec.KeyOpenAPITags, []string{constants.OpenpitrixTag}).
        Returns(http.StatusOK, api.StatusOK, models.PageableResponse{}))
func (h *openpitrixHandler) ListApps(req *restful.Request, resp *restful.Response)
    limit, offset := params.ParsePaging(req)
    orderBy := params.GetStringValueWithDefault(req, params.OrderByParam, openpitrix.CreateTime)
    reverse := params.GetBoolValueWithDefault(req, params.ReverseParam, false)
    conditions, err := params.ParseConditions(req)

    if err != nil {
        klog.V(4).Infoln(err)
        api.HandleBadRequest(resp, nil, err)
        return
    }

    if req.PathParameter("workspace") != "" {
        conditions.Match[openpitrix.WorkspaceLabel] = req.PathParameter("workspace")
    }

    result, err := h.openpitrix.ListApps(conditions, orderBy, reverse, limit, offset)  // 跟进

    if err != nil {
        klog.Errorln(err)
        handleOpenpitrixError(resp, err)
        return
    }

    resp.WriteEntity(result)
}
func (c *applicationOperator) ListApps(conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error) {

    apps, err := c.listApps(conditions)  // 重点跟进
    if err != nil {
        klog.Error(err)
        return nil, err
    }
    apps = filterApps(apps, conditions)

    if reverse {
        sort.Sort(sort.Reverse(HelmApplicationList(apps)))
    } else {
        sort.Sort(HelmApplicationList(apps))
    }

    totalCount := len(apps)
    start, end := (&query.Pagination{Limit: limit, Offset: offset}).GetValidPagination(totalCount)
    apps = apps[start:end]
    items := make([]interface{}, 0, len(apps))

    for i := range apps {
        versions, err := c.getAppVersionsByAppId(apps[i].GetHelmApplicationId())
        if err != nil && !apierrors.IsNotFound(err) {
            return nil, err
        }
        ctg, _ := c.ctgLister.Get(apps[i].GetHelmCategoryId())
        items = append(items, convertApp(apps[i], versions, ctg, 0))
    }
    return &models.PageableResponse{Items: items, TotalCount: totalCount}, nil
}

// line 601
func (c *applicationOperator) listApps(conditions *params.Conditions) (ret []*v1alpha1.HelmApplication, err error) {
    repoId := conditions.Match[RepoId]
    if repoId != "" && repoId != v1alpha1.AppStoreRepoId {
        // get Helm application from Helm repo
        if ret, exists := c.cachedRepos.ListApplicationsByRepoId(repoId); !exists {
            klog.Warningf("load repo failed, repo id: %s", repoId)
            return nil, loadRepoInfoFailed
        } else {
            return ret, nil
        }
    } else {
        if c.backingStoreClient == nil {
            return []*v1alpha1.HelmApplication{}, nil
        }
        ret, err = c.appLister.List(labels.SelectorFromSet(buildLabelSelector(conditions)))
    }

    return
}
func (c *cachedRepos) ListApplicationsByRepoId(repoId string) (ret []*v1alpha1.HelmApplication, exists bool) {
    c.RLock()
    defer c.RUnlock()

    if repo, exists := c.repos[repoId]; !exists {
        return nil, false
    } else {
        ret = make([]*v1alpha1.HelmApplication, 0, 10)
        for _, app := range c.apps {
            if app.GetHelmRepoId() == repo.Name { // 应用的仓库ID相同则追加
                ret = append(ret, app)
            }
        }
    }
    return ret, true
}

既然 app template 是从缓存中获取的,那么缓存中的数据又是什么时候录入的呢?

  1. 创建全局缓存变量
  2. 添加新 Helm 仓库,K8s 中已安装 crd 控制器 HelmRepoController 发现有新的 HelmRepo 创建,更新 .Status.Data 内容
  3. informer 发现有更新,同时更新缓存

缓存更新的实现

  1. 创建全局变量,通过 init 函数初始化
  2. 通过 HelmRepo 的 informer 实现缓存同步更新
  3. 在每次调用接口的时候,hanlder 类中包换了缓存变量

创建接口类 openpitrix.Interface

type openpitrixHandler struct {
    openpitrix openpitrix.Interface
}

func newOpenpitrixHandler(ksInformers informers.InformerFactory, ksClient versioned.Interface, option *openpitrixoptions.Options) *openpitrixHandler {
    var s3Client s3.Interface
    if option != nil && option.S3Options != nil && len(option.S3Options.Endpoint) != 0 {
        var err error
        s3Client, err = s3.NewS3Client(option.S3Options)
        if err != nil {
            klog.Errorf("failed to connect to storage, please check storage service status, error: %v", err)
        }
    }

    return &openpitrixHandler{
        openpitrix.NewOpenpitrixOperator(ksInformers, ksClient, s3Client),
    }
}

NewOpenpitrixOperator

  • 通过在 informer 中添加通知函数,执行缓存更新
  • once.Do 只执行一次
var cachedReposData reposcache.ReposCache
var HelmReposInformer cache.SharedIndexInformer
var once sync.Once


func init() {
    cachedReposData = reposcache.NewReposCache() // 全局缓存
}

func NewOpenpitrixOperator(ksInformers ks_informers.InformerFactory, ksClient versioned.Interface, s3Client s3.Interface) Interface {

    once.Do(func() {
        klog.Infof("start Helm repo informer")
        HelmReposInformer = ksInformers.KubeSphereSharedInformerFactory().Application().V1alpha1().HelmRepos().Informer()
        HelmReposInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
            AddFunc: func(obj interface{}) {
                r := obj.(*v1alpha1.HelmRepo)
                cachedReposData.AddRepo(r) // 缓存更新, 点击跟进
            },
            UpdateFunc: func(oldObj, newObj interface{}) {
                oldR := oldObj.(*v1alpha1.HelmRepo)
                cachedReposData.DeleteRepo(oldR)
                r := newObj.(*v1alpha1.HelmRepo)
                cachedReposData.AddRepo(r)
            },
            DeleteFunc: func(obj interface{}) {
                r := obj.(*v1alpha1.HelmRepo)
                cachedReposData.DeleteRepo(r)
            },
        })
        go HelmReposInformer.Run(wait.NeverStop)
    })

    return &openpitrixOperator{
        AttachmentInterface:  newAttachmentOperator(s3Client),
    // cachedReposData used
        ApplicationInterface: newApplicationOperator(cachedReposData, ksInformers.KubeSphereSharedInformerFactory(), ksClient, s3Client),
    // cachedReposData used
        RepoInterface:        newRepoOperator(cachedReposData, ksInformers.KubeSphereSharedInformerFactory(), ksClient),
    // cachedReposData used
        ReleaseInterface:     newReleaseOperator(cachedReposData, ksInformers.KubernetesSharedInformerFactory(), ksInformers.KubeSphereSharedInformerFactory(), ksClient),
        CategoryInterface:    newCategoryOperator(ksInformers.KubeSphereSharedInformerFactory(), ksClient),
    }
}

缓存更新逻辑

// 缓存结构体
type cachedRepos struct {
    sync.RWMutex

    chartsInRepo  map[workspace]map[string]int
    repoCtgCounts map[string]map[string]int

    repos    map[string]*v1alpha1.HelmRepo
    apps     map[string]*v1alpha1.HelmApplication
    versions map[string]*v1alpha1.HelmApplicationVersion
}
  • ByteArrayToSavedIndex:将 repo.Status.Data 转换为 SavedIndex 数组对象
  • 遍历 SavedIndex.Applications
  • 保存(app.ApplicationId:HelmApplication)到 cachedRepos.apps
func (c *cachedRepos) AddRepo(repo *v1alpha1.HelmRepo) error {
    return c.addRepo(repo, false)
}

//Add new Repo to cachedRepos
func (c *cachedRepos) addRepo(repo *v1alpha1.HelmRepo, builtin bool) error {
    if len(repo.Status.Data) == 0 {
        return nil
    }
    index, err := Helmrepoindex.ByteArrayToSavedIndex([]byte(repo.Status.Data))
    if err != nil {
        klog.Errorf("json unmarshal repo %s failed, error: %s", repo.Name, err)
        return err
    }
    ...

    chartsCount := 0
    for key, app := range index.Applications {
        if builtin {
            appName = v1alpha1.HelmApplicationIdPrefix + app.Name
        } else {
            appName = app.ApplicationId
        }

        HelmApp := v1alpha1.HelmApplication{
        ....
        }
        c.apps[app.ApplicationId] = &HelmApp

        var ctg, appVerName string
        var chartData []byte
        for _, ver := range app.Charts {
            chartsCount += 1
            if ver.Annotations != nil && ver.Annotations["category"] != "" {
                ctg = ver.Annotations["category"]
            }
            if builtin {
                appVerName = base64.StdEncoding.EncodeToString([]byte(ver.Name + ver.Version))
                chartData, err = loadBuiltinChartData(ver.Name, ver.Version)
                if err != nil {
                    return err
                }
            } else {
                appVerName = ver.ApplicationVersionId
            }

            version := &v1alpha1.HelmApplicationVersion{
            ....
            }
            c.versions[ver.ApplicationVersionId] = version
        }
        ....
    }
    return nil
}

HelmRepo 协调器

HelmRepo.Status.Data 加载流程

  1. LoadRepoIndex: convert index.yaml to IndexFile
  2. MergeRepoIndex: merge new and old IndexFile
  3. savedIndex.Bytes(): compress data with zlib.NewWriter
  4. 将 savedIndex 数据存入 CRD(HelmRepo.Status.Data)

关键结构体

// HelmRepo.Status.Data == SavedIndex 压缩后的数据
   type SavedIndex struct {
       APIVersion   string                  `json:"apiVersion"`
       Generated    time.Time               `json:"generated"`
       Applications map[string]*Application `json:"apps"`
       PublicKeys   []string                `json:"publicKeys,omitempty"`

       // Annotations are additional mappings uninterpreted by Helm. They are made available for
       // other applications to add information to the index file.
       Annotations map[string]string `json:"annotations,omitempty"`
   }

   // IndexFile represents the index file in a chart repository
   type IndexFile struct {
       APIVersion string                   `json:"apiVersion"`
       Generated  time.Time                `json:"generated"`
       Entries    map[string]ChartVersions `json:"entries"`
       PublicKeys []string                 `json:"publicKeys,omitempty"`
   }

代码位置

func (r *ReconcileHelmRepo) syncRepo(instance *v1alpha1.HelmRepo) error {
       // 1. load index from Helm repo
       index, err := Helmrepoindex.LoadRepoIndex(context.TODO(), instance.Spec.Url, &instance.Spec.Credential)

       if err != nil {
           klog.Errorf("load index failed, repo: %s, url: %s, err: %s", instance.GetTrueName(), instance.Spec.Url, err)
           return err
       }

       existsSavedIndex := &Helmrepoindex.SavedIndex{}
       if len(instance.Status.Data) != 0 {
           existsSavedIndex, err = Helmrepoindex.ByteArrayToSavedIndex([]byte(instance.Status.Data))
           if err != nil {
               klog.Errorf("json unmarshal failed, repo: %s,  error: %s", instance.GetTrueName(), err)
               return err
           }
       }

       // 2. merge new index with old index which is stored in crd
       savedIndex := Helmrepoindex.MergeRepoIndex(index, existsSavedIndex)

       // 3. save index in crd
       data, err := savedIndex.Bytes()
       if err != nil {
           klog.Errorf("json marshal failed, error: %s", err)
           return err
       }

       instance.Status.Data = string(data)
       return nil
   }

Question:

Q1:Helm 仓库发包时如何进行 Helm release 版本控制

A:修改 Charts.yaml 中的字段 version,然后 Helm package, 等于新增一个 .tgz 包,老版本的不要删除,这时候执行 index 的时候会吧所有的 .tgz 包包含在内。

$ Helm repo index stable --url=xxx.xx.xx.xxx:8081/
  $ cat index.yaml
  ....
  redis:
  - apiVersion: v1
    appVersion: "1.0"
    created: "2021-06-22T16:34:58.286583012+08:00"
    description: A Helm chart for Kubernetes
    digest: fd7c0d962155330527c0a512a74bea33302fca940b810c43ee5f461b1013dbf5
    name: redis
    urls:
    - xxx.xx.xx.xxx:8081/redis-0.1.1.tgz
    version: 0.1.1
  - apiVersion: v1
    appVersion: "1.0"
    created: "2021-06-22T16:34:58.286109049+08:00"
    description: A Helm chart for Kubernetes
    digest: 1a23bd6d5e45f9d323500bbe170011fb23bfccf2c1bd25814827eb8dc643d7f0
    name: redis
    urls:
    - xxx.xx.xx.xxx:8081/redis-0.1.0.tgz
    version: 0.1.0

Q2:KubeSphere 版本同步功能有缺失?用户添加完 Helm 仓库后,如果有新的应用发布,查询不到

A:解决方案:使用 3 种同步策略

  • 定时同步 Helm 仓库(HelmRepo 设置一个定时协调的事件)
  • 企业仓库,用户可以设置 hook,发布新版本的时候主动触发更新
  • 用户主动更新 charts

Q3:index.yaml 缓存位置

A:某些仓库的 index.yaml 比较大,如果 1000 个用户,1000 个 charts 会太吃内存。建议常用 index.yaml 的放在内存中,不常用的 index.yaml 存储在本地磁盘。

统一服务门户,让运维不再成为“背锅侠”和“救火队”

BoCloud阅读(357)评论(0)

当前企业基本已具备较为完整的运维服务团队以及运维专业工具,但是企业管理者普遍认为运维就是“被动救火队”,对其重视程度非常低,一说到运维就会想到“背锅侠”、“救火队”,如何把运维工作从压力转化为价值,从运维工作转型到运营,是每个运维团队管理者一直在思考的痛点问题。

01

运维和运营工作的区别在哪里?

运维更多的是被动式“维持”,面向基础设施、面向软硬件,保障系统的“稳定”、“安全”和“可靠”。运营更多的是主动式“经营”,面向业务、服务和用户,关注的是提供服务的“体验”、“效率”和“效益”。

02

在多云系统架构下的服务运营工作企业主要存在哪些问题?

  • 云资源申请等工作线下操作,缺乏自助服务,用户体验差;
  • 服务审批流程与云管理平台割裂,资源交付线下手工完成,未实现自动交付,资源供给效率低;
  • 未对服务进行标准化,无法实现服务的计量计费,无法有效管控效益;
  • 缺失服务考核评价体系,服务满意度低。

如何在当前企业多云系统架构下实现从运维到运营转型?

我们基于多年云管理平台建设经验,以及对企业客户服务运营需求的理解,认为云管理平台的“统一服务门户”是企业从运维到服务运营的重要抓手,通过将云服务自助化、流程化、自动化、标准化服务满意度评价管理来规范日常服务运营工作,提高服务效率和服务质量,提升用户服务体验,有效管控效益。

  • 服务自助化-各业务部门通过“统一服务门户”自助申请计算、网络、存储、运维操作等云服务;
  • 服务流程化-各种云服务申请、变更、退订等自动匹配相应审批流程,直观展现审批各环节信息;
  • 服务自动化-服务申请审批通过,平台自动执行并交付计算、网络、存储资源及运维操作结果;
  • 服务标准化-标准化计算、网络、存储、运维操作等产品,以服务形式发布到“统一服务门户”;
  • 服务满意度评价-服务评价管理对各类服务能力的使用情况进行评价,打造一个“客观、完整、详实”的考核评价体系。

 

通过“统一服务门户”提高基础设施资源的利用率,实现按需调配、即需即用、有效共享,解决了各业务部门碎片化服务的痛点。打造“四化一评价”的服务运营全生命周期闭环管控的完整服务体系,助力企业从运维到运营成功转型。

关于BeyondCMP云管理平台:

BeyondCMP 云管理平台针对多云和混合云环境的云基础设施等资源管理,提供“云管理+云服务+云运营+云监控”四位一体的中立云管理服务,以统一化方式帮助企业用户实现多云资源的调配和管理,自动化和自助化的服务交付,精细化和可视化的成本运营分析,帮助企业从传统环境平滑迁移到多云环境,保持企业云架构系统的持续优化。

注:内容由BoCloud博云原创,未经允许不得转发

Rainbond 5.3.1 发布,支持100+组件一键云原生交付

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

2021年7月5日,Rainbond 5.3.1 正式发布。

Rainbond 是云原生且易用的应用管理平台。云原生应用交付的最佳实践。专注于以应用为中心的理念。赋能企业搭建云原生开发云、云原生交付云。

对于企业: Rainbond 是开箱即用的云原生平台,借助 Rainbond 可以快速完成企业研发和交付体系的云原生转型。

对于开发者: 基于 Rainbond 开发、测试和运维企业业务应用,开箱即用的获得全方位的云原生技术能力。包括但不仅限于持续集成、服务治理、架构支撑、多维度应用观测、流量管理。

对于项目交付: 基于 Rainbond 搭建产品版本化管理体系,搭建标准化客户交付环境,使传统的交付流程可以自动化、简单化和可管理。

近一年,使用Rainbond 云原生应用交付流程(见下图)的开源用户成为主流,面对不同用户的业务复杂性,对Rainbond交付流程的性能提出了新的要求。从 5.3.0 版本发布以来4个月的时间,Rainbond 开发者以交付链路的性能优化为主要迭代方向。

2bdevops

在工业互联网、园区建设、智慧城市建设等等领域中,一个应用解决方案大多具有50-100个服务组件。在这些行业中,通常会有多家应用厂商来合作完成一个解决方案。Rainbond 在这个过程中提供多项能力:

(1)标准化应用交付模型,统一各个应用厂商的应用交付标准,使行业集成商低成本集成解决方案。

(2)统一交付环境,一键实现应用交付。大大降低销售过程中的演示场景、POC场景和成本和最终交付的成本。

(3)智能应用运维管理,客户维度运维低成本。

关键变更

支持100+组件规模应用交付

基于上述的Rainbond应用交付流程,当前版本在应用模型发布、应用模型安装、应用升级、应用生命周期管理四个维度进行性能优化。以应用模型安装为例,100个组件的安装过程需要调用大量的计算资源,控制组件的启动顺序,控制微服务系统注册和配置分发。

多组件应用安装和升级演示

Helm应用安装与管理(Beta)

Rainbond 不支持Helm应用安装和管理早已是一个痛点。Rainbond 的产品形态是以管理自定义的应用规范为核心的云原生应用管理。与其他容器化平台不一样,我们不提供Kubernetes原生的资源管理面板。因此在面对灵活的Helm应用包,我们没有很好的方式将所有Helm应用转化为Rainbond的应用规范。在OAM规范的实现模式中,有一个思路是把每一个Helm应用定义为自定义的组件类型,对其进行资源类型识别从而实现一些运维能力注入。我们认为这种模式精细化管理有优势,但用户落地成本很大。

当前版本我们采用一种新的模式来安装和管理Helm应用。我们将其定位为 Rainbond 原生应用的补充,作为部署一些中间件的载体,所以我们主要要考虑解决以下问题:

(1)Helm安装的应用如何接入Rainbond ServiceMesh微服务架构,实现原生应用可调用Helm应用。

(2)Helm安装的应用如何接入Rainbond 网关,实现外部访问及流量治理。

(3)Helm应用管理能力支持多少。

我们定义了Kubernets自定义资源HelmApp,实现HelmApp的多集群部署。用户对接上Helm应用商店后,即可选择应用进行安装。安装过程中完全支持Helm应用的配置规范进行应用配置,同时也支持 Rancher 定义的配置表单规范,实现配置表单自动生成。Helm应用部署后自动识别其Service资源,进行服务的注册,实现Rainbond微服务和网关体系的接入。

逐步适配OAM应用规范

从5.3.1版本开始,我们开始逐步适配OAM应用规范,提升Rainbond的可扩展性。在当前版本中我们基于OAM规范,重新实现第三方组件类型,定义了ThirdComponent 作为第一个 ComponentDefinition,并在产品中实现对ComponentDefinition的基础管理机制。接下来Rainbond中现有的两种内置组件类型逐步基于ComponentDefinition定义实现。然后开放用户自行扩展的能力,Rainbond中提供整个支撑体系,包括通用运维特征能力注入、配置UI化、通用的微服务治理和流量管理、应用打包交付等。

image-20210705114112352

详细变更点

新增功能

  • 【应用商店】支持Helm应用仓库对接;

  • 【应用管理】支持Helm应用安装和配置;

  • 【微服务治理】支持通过网关或内部组件依赖两种方式访问Helm安装的应用;

  • 【微服务治理】新增GRPC协议的服务治理能力;

  • 【微服务治理】新增对组件下容器启动顺序控制,实现mesh容器先于业务容器启动;

  • 【组件管理】新增基于kubernetes service服务发现类型的第三方组件;

  • 【源码构建】Go语言新增对Go 1.14、1.15、1.16 版本Runtime的支持;

  • 【源码构建】Go语言新增对构建模块和启动命令的配置;

  • 【源码构建】Java、Go、PHP等语言新增pre_build、post_build构建时shell hook的支持;

  • 【企业管理】用户管理中新增对用户所在团队及角色的批量管理能力;

  • 【企业管理】团队管理中新增开通集群的功能入口;

  • 【集群安装】支持RKE集群配置,实现集群节点配置的灵活调整;

优化功能

  • 【性能】应用升级体系优化,支持100+组件批量升级;

  • 【性能】从应用商店安装组件实现优化,支持100+组件批量安装;

  • 【性能】改进拓扑图加载逻辑,加速大应用下拓扑图加载速度;

  • 【性能】优化在大量组件情况下的应用级生命周期操作API的性能;

  • 【稳定性】应用网关优化,解决异常应用访问导致网关内存泄露的故障;

  • 【组件管理】支持空值的环境变量和配置组变量;

  • 【监控报警】移除错误的节点健康检测报警策略;

  • 【组件管理】重构组件本地存储类型的实现,支持使用本地存储组件复用集群的调度策略;

  • 【内部组件库】新增应用模型版本管理,支持在发布页展示版本介绍;

  • 【组件管理】支持组件设置自定义主机名解析记录;

BUG修复

  • 【源码构建】修复 .netcore 源码构建任务无法结束的故障;

  • 【控制台】修复网关策略搜索功能不可用故障;

  • 【源码构建】修复Maven 配置删除后源码构建无法执行的故障;

  • 【稳定性】修复错误的网关策略参数导致网关故障;

  • 【组件管理】修复组件实例数不一致的故障;

  • 【稳定性】修复rbd-worker系统组件由于etcd不稳定异常重启的故障;

  • 【应用管理】修复对接非HTTPS镜像仓库时应用备份不可用的故障;

  • 【集群安装】修复集群镜像仓库证书不一致的故障;

社区

如果您对Rainbond项目感兴趣,如果您有一些疑问,如果您对云原生、Kubernetes等技术感兴趣,欢迎加入Rainbond 社区钉钉群。

安装使用请参考文档:快速安装

从5.3.0升级到5.3.1: 升级参考文档

2021值得考虑的一类新型微服务架构:ServiceMesh

BoCloud阅读(303)评论(0)

本期与大家共同探讨微服务当下最新的框架 —— ServiceMesh 。

微服务框架
微服务,简单而言,就是将原有的一个整体的应用,拆成多个细粒度的小应用,同时具备分布式的特点。化整为零后当然会带来一系列的问题,就好比一个开小卖铺的后来开成了连锁店一样,一定会带来很多跟进货、卖货没关系的问题。那么微服务带来的这一系列问题,就需要通过微服务框架来解决。
通常狭义的“微服务”仅指的是 SpringCloud、Dubbo 这一类的传统微服务框架,也是国内使用较多的开源微服务框架。另外一类新型的微服务框架就是服务网格(ServiceMesh),也在近两年逐渐变得热门,比如 Linkerd、Consul、Istio 等都属于服务网格的框架。

ServiceMesh的特点
做微服务首要解决的问题是通信问题:服务寻址、访问控制、流量限流、熔断降级等等,都是通信上的问题,而微服务、服务网格的框架就是用来解决这类问题的。但是在解决的方式上,传统的微服务框架、新型的服务网格框架则有所不同,而且是大有不同。两者之间的区别我们可以看一下这个图:

传统微服务框架以 SpringCloud 为例,开发的时候需要引入 SpringCloud 相关的所有依赖,也就是将通信部分包到了引入的依赖(SDK)里,当然这也考验开发人员的能力,需要开发人员熟悉 SpringCloud 框架才能用,并且目前只支持 Java 语言。服务网格类的框架,说来也简单,就是把上面的 SDK 拿出来,单独运行,只要能实现该 SDK 的功能,目的也就达到了,而抽出来单独运行的 SDK 称之为 Sidecar,在服务网格的管理中属于数据面的管理。
当然 Sidecar 也有一系列需要解决的问题,比如 Sidecar 要全权代理其通信,即流量劫持;服务发现、服务健康检测要有相应的机制;策略下发和统一管理也都需要考虑。在服务网格的管理中属于控制面的管理。
与传统微服务框架相比,服务网格确实是有很多优势。服务的业务与通信拆成两个部分,业务开发人员不用关心微服务框架、微服务治理的事情,开发的时候也不会涉及框架的学习,甚至不用关心使用哪种开发语言;运维管理组或者架构组,也不用担心自己在通信架构上的优化,很难升级到运行中的系统上。用专业的术语来说就是将业务与治理解耦,微服务治理能力下沉至运维层,降低开发难度,在架构上更容易实现层次化、规范化、体系化。

性能是不是服务网络最大的挑战?
我们知道当单体应用拆分成微服务以后,本来进程内的调用却变成了网络间的调用,性能自然是不如从前。那么同样的道理,现在是将每个微服务又拆成了两个运行的程序,同样需要通过一个网络的调用,这岂不是又加重了性能的问题?如果我们把每一次服务间的调用,都比做是搭乘公交车,原本 A 服务调用 B 服务,就好似搭乘了一趟公交,那么带上 Sidecar 的服务调用就是 A 服务->Sidecar->Sidecar->B 服务,这样好比是中间换乘了两次公交,这样算来延迟可增加不少,几近于原来的三倍?
其实仔细分析一下会发现,从处理时间上来说,ServiceMesh 其实并没有增加通信性能的消耗。举个例子,在没有引入 ServiceMesh 时,A 服务调用 B 服务,消耗的时间主要是:
● A发出信息之前,首先会经过自身的框架(SDK)做处理,因为传统的微服务框架基本上都是客户端负载均衡,所以在这里会过一些负载均衡、算法选址、熔断降级等治理功能,耗时在1ms左右;
● A->B的网络通信时间,网络畅通的情况下,不超过0.1ms;
● B接收到数据后,也首先是框架(SDK)处理,比如访问权限、黑白名单等,姑且叫做治理功能处理,大概也是会消耗1ms;
● 治理相关处理完以后,就是业务处理了,首先是协议编解码,然后是业务代码的处理,一个不是很复杂的业务服务处理,应该是在10ms左右。

这样算下来,A->B的微服务调用,从 A 出口算起到 B 处理完成,粗略的估计是12.1ms,我们再来看A->Sidecar->Sidecar-B的业务处理,大概需要花费的时间:
● A服务发出通信信息,到A服务的 Sidecar 上,这个网络传输的时间一定在0.1ms以内,通常都是主机内的本地调用,因此也不占据网络资源;
● A服务的 Sidecar 接收信息后,会做类似于之前的 SDK 一样的处理,也就是熔断、限流、访问控制就在这里做好了,处理时间也类似大概1ms。注意服务网格的访问控制、黑白名单也是在调用端处理的;
● A服务的Sidecar将信息传输到B服务 Sidecar ,与传统微服务的A到B的传输相同,传输时间也相同,姑且算0.1ms;
● 信息到B服务的 Sidecar 以后,如果不是加密传输,那么B服务的 Sidecar 不用做任何处理,因为治理相关的功能已经全部放在了调用端,所以B服务的 Sidecar 直接透传,几乎不消耗时间;
● B服务的 Sidecar 将信息传送到B服务,网络也按照0.1ms算;
● 最后B服务的协议编解码、业务处理,也是将近10ms。
统计一下服务网格平均一次调用的消耗大概是11.3ms。延迟的消耗不增反减,这也是前期我们经过很多次的压测以后发现的一个规律。

意料之外,情理之中
经过上面的性能分析,结果大出我们的意料之外,本以为将近三倍的延时,却发现几乎没有太大变化,这是我们有意抬高 ServiceMesh ,还是其本身架构就是向着性能优化的方向走的呢?
其实了解 Sidecar 的处理机制以后,也就能明白这个意料之外的事情其实并不奇怪。
首先 Sidecar 不是一个服务,不处理报文体中的详细业务内容,只做转发。拿到报文头中的目标信息以后,原报文体不做任何处理,其次在转发的时候执行负载策略、熔断限流、路由策略等,然后就是通信信息的记录,以便于监控、日志等管理。为了实现 API 粒度的管理,比如 API 粒度的访问控制、熔断限流,Sidecar 还需要有 URL 的路由策略功能,通过不同的 URL 策略,实现细粒度的 API 或 API 组的治理,这也就是 Sidecar 整体的处理流程。
说到这里,大家有没有发现 Sidecar 跟一个 API 网关的功能、作用好像差不太多?实际上 Sidecar 就是通过 API 网关来实现的,为每一个微服务实例配置一个 API 网关实例,全权接管其流量和通信,以达到透明化的治理。所以在性能方面,CPU、内存的消耗是必然,而通信延迟上却并不足为虑。

总结
服务网格从 2010 年就被提出,到 2017、2018 年的时候就普遍受到了技术人员的关注,2020 年也被叫做是服务网格落地的元年。了解了服务网格的原理,也就普遍能被市场所接受了,毕竟核心的技术也就在于 API 网关上,而 API 网关又是一个已经完全成熟的一个技术。所以如果现在做微服务化的转型,ServiceMesh 也是值得考虑的一种技术规范。
在接下来的文章中,我们会将 API 网关、Mesh、Sidecar 做一个统一的介绍和对比,敬请期待~

【GOTC 预告】王思宇:从 OpenKruise 看云原生应用负载发展趋势

alicloudnative阅读(191)评论(0)

头图.png

2021 年 7 月 9 日至 10 日,GOTC 全球开源技术峰会(The Global Opensource Technology Conference)上海站即将拉开大幕。大会由开源中国和 Linux 软件基金会(The Linux Foundation)联合发起,全球头部开源公司和顶级开源项目将一起亮相,覆盖云原生、大数据、人工智能、物联网、区块链、DevOps、开源治理等多个技术领域,为开发者带来全球最新、最纯粹的开源技术,同时传播开源文化和理念,推动开源生态的建设和发展。

7 月 10 日, CNCF Sandbox 项目 OpenKruise 作者 & 社区负责人、阿里云技术专家王思宇将在 GOTC 上海站 「开源云原生计算时代论坛」专题论坛带来主题分享。OpenKruise 是由阿里开源的云原生应用自动化扩展套件,它在完全兼容标准的 Kubernetes 之上,围绕云原生应用场景提供多种丰富的自动化能力。目前 OpenKruise 在 Github 上已经有 2300+ star, 50+ 贡献者,已登记生产使用的用户包括来自国内外的阿里、蚂蚁、携程、苏宁、OPPO、有赞、斗鱼TV、申通、小红书、Lyft、Spectro Cloud 等 25+ 企业。

在本次论坛上,王思宇将分享哪些在社区中观察到的云原生趋势,又将介绍哪些值得关注的 OpenKruise 最新动态?让我们先睹为快。

GOTC 讲师王思宇:从 OpenKruise 看云原生应用负载发展趋势

1.png

讲师简介

王思宇,OpenKruise 作者&负责人,阿里云技术专家,Kubernetes、OAM 社区贡献者。长期从事云原生、容器、调度等领域研发;阿里巴巴百万容器调度系统核心研发成员,多年支撑阿里双十一超大规模容器集群经验。

议题介绍

云原生的应用负载从 Kubernetes 原生的 workloads(Deployment、StatefulSet)为人所熟知,但在另一方面,我们也看到从中小型的创业公司到大型互联网公司,越是大规模的应用场景下这些原生的 workloads 越是无法满足复杂的业务部署诉求。因此,不少公司都自研了适用于自身场景的自定义 workload,但其中真正在通用化、全面性、稳定性等多方面做到成熟的开源组件,只有阿里云开源的、已经成为 CNCF Sandbox 项目的 OpenKruise。

本次分享中,我们将从 Kubernetes 原生 workloads 开始介绍云原生应用负载的职责、实现基础,而后分析在超大规模业务场景下对应用负载的真实诉求,OpenKruise 是通过什么样的方式来满足这些需求,以及后续开源生态下的发展趋势,包括:

  1. 云原生应用部署的问题与挑战。
  2. K8s 原生应用负载的基本能力。
  3. OpenKruise 如何满足超大规模业务场景下的部署发布诉求。
  4. 以阿里巴巴应用场景为例,介绍使用 OpenKruise 做应用管理的实践。
  5. 未来云原生下,应用负载领域的发展趋势与方向。

如果对 OpenKruise 项目感兴趣,或有任何问题,欢迎大家通过钉钉扫码,加入 OpenKruise 社区交流群:
2.png

点击https://gotc.oschina.net/,查看 GOTC 大会详情

同程旅行基于 RocketMQ 高可用架构实践

alicloudnative阅读(231)评论(0)

头图.jpeg

背景介绍

为何选择 RocketMQ

我们在几年前决定引入 MQ 时,市场上已经有不少成熟的解决方案,比如 RabbitMQ , ActiveMQ,NSQ,Kafka 等。考虑到稳定性、维护成本、公司技术栈等因素,我们选择了 RocketMQ :

  • 纯 Java 开发,无依赖,使用简单,出现问题能 hold ;
  • 经过阿里双十一考验,性能、稳定性可以保障;
  • 功能实用,发送端:同步、异步、单边、延时发送;消费端:消息重置,重试队列,死信队列;
  • 社区活跃,出问题能及时沟通解决。

使用情况

  • 主要用于削峰、解耦、异步处理;
  • 已在火车票、机票、酒店等核心业务广泛使用,扛住巨大的微信入口流量;
  • 在支付、订单、出票、数据同步等核心流程广泛使用;
  • 每天 1000+ 亿条消息周转。

下图是 MQ 接入框架图

由于公司技术栈原因,client sdk 我们提供了 java sdk ;对于其他语言,收敛到 http proxy ,屏蔽语言细节,节约维护成本。按照各大业务线,对后端存储节点进行了隔离,相互不影响。

1.png

MQ 双中心改造

之前单机房出现过网络故障,对业务影响较大。为保障业务高可用,同城双中心改造提上了日程。

为何做双中心

  • 单机房故障业务可用;​
  • 保证数据可靠:若所有数据都在一个机房,一旦机房故障,数据有丢失风险;
  • 横向扩容:单机房容量有限,多机房可分担流量。

双中心方案

做双中心之前,对同城双中心方案作了些调研,主要有冷(热)备份、双活两种。(当时社区 Dledger 版本还没出现,Dledger 版本完全可做为双中心的一种可选方案。)

1)同城冷(热)备份

两个独立的 MQ 集群, 用户流量写到一个主集群,数据实时同步到备用集群,社区有成熟的 RocketMQ Replicator 方案,需要定期同步元数据,比如主题,消费组,消费进度等。

2.png

2)同城双活

两个独立 MQ 集群,用户流量写到各自机房的 MQ 集群,数据相互不同步。

平时业务写入各自机房的 MQ 集群,若一个机房挂了,可以将用户请求流量全部切到另一个机房,消息也会生产到另一个机房。

3.png

对于双活方案,需要解决 MQ 集群域名。

1)若两个集群用一个域名,域名可以动态解析到各自机房。此方式要求生产、消费必须在同一个机房。假如生产在 idc1 ,消费在 idc2 ,这样生产、消费各自连接一个集群,没法消费数据。

2)若一个集群一个域名,业务方改动较大,我们之前对外服务的集群是单中心部署的,业务方已经大量接入,此方案推广较困难。

为尽可能减少业务方改动,域名只能继续使用之前的域名,最终我们采用一个 Global MQ 集群,跨双机房,无论业务是单中心部署还是双中心部署都不影响;而且只要升级客户端即可,无需改动任何代码。

双中心诉求

  • 就近原则:生产者在 A 机房,生产的消息存于 A 机房 broker ; 消费者在 A 机房,消费的消息来自 A 机房 broker 。
  • 单机房故障:生产正常,消息不丢。
  • broker 主节点故障:自动选主。

就近原则

简单说,就是确定两件事:

  • 节点(客户端节点,服务端节点)如何判断自己在哪个 idc;
  • 客户端节点如何判断服务端节点在哪个 idc。

如何判断自己在哪个 idc?

1) ip 查询
节点启动时可以获取自身 ip ,通过公司内部的组件查询所在的机房。

2)环境感知
需要与运维同学一起配合,在节点装机时,将自身的一些元数据,比如机房信息等写入本地配置文件,启动时直接读写配置文件即可。

我们采用了第二个方案,无组件依赖,配置文件中 logicIdcUK 的值为机房标志。

修改图.jpg
客户端节点如何识别在同一个机房的服务端节点?

客户端节点可以拿到服务端节点的 ip 以及 broker 名称的,因此:

  • ip 查询:通过公司内部组件查询 ip 所在机房信息;
  • broker 名称增加机房信息:在配置文件中,将机房信息添加到 broker 名称上;
  • 协议层增加机房标识:服务端节点向元数据系统注册时,将自身的机房信息一起注册。

相对于前两者,实现起来略复杂,改动了协议层, 我们采用了第二种与第三种结合的方式。

就近生产

基于上述分析,就近生产思路很清晰,默认优先本机房就近生产;

若本机房的服务节点不可用,可以尝试扩机房生产,业务可以根据实际需要具体配置。
5.png

就近消费

优先本机房消费,默认情况下又要保证所有消息能被消费。

队列分配算法采用按机房分配队列

  • 每个机房消息平均分给此机房消费端;
  • 此机房没消费端,平分给其他机房消费端。

伪代码如下:

Map<String, Set> mqs = classifyMQByIdc(mqAll);
Map<String, Set> cids = classifyCidByIdc(cidAll);
Set<> result = new HashSet<>;
for(element in mqs){
                     result.add(allocateMQAveragely(element, cids, cid)); //cid为当前客户端
}

消费场景主要是消费端单边部署与双边部署。

单边部署时,消费端默认会拉取每个机房的所有消息。
6.png

双边部署时,消费端只会消费自己所在机房的消息,要注意每个机房的实际生产量与消费端的数量,防止出现某一个机房消费端过少。
7.png

单机房故障

  • 每组 broker 配置

一主两从,一主一从在一机房,一从在另一机房;某一从同步完消息,消息即发送成功。

  • 单机房故障

消息生产跨机房;未消费消息在另一机房继续被消费。

故障切主

在某一组 broker 主节点出现故障时,为保障整个集群的可用性,需要在 slave 中选主并切换。要做到这一点,首先得有个broker 主故障的仲裁系统,即 nameserver(以下简称 ns )元数据系统(类似于 redis 中的哨兵)。

ns 元数据系统中的节点位于三个机房(有一个第三方的云机房,在云上部署 ns 节点,元数据量不大,延时可以接受),三个机房的 ns 节点通过 raft 协议选一个leader,broker 节点会将元数据同步给 leader, leader 在将元数据同步给 follower 。

客户端节点获取元数据时, 从 leader,follower 中均可读取数据。

8.png

切主流程

  • 若 nameserver leader 监控到 broker 主节点异常, 并要求其他 follower 确认;半数 follower 认为 broker 节点异常,则 leader 通知在 broker 从节点中选主,同步进度大的从节点选为主;
  • 新选举的 broker 主节点执行切换动作并注册到元数据系统;
  • 生产端无法向旧 broker 主节点发送消息。

流程图如下

9.pngimage.png

切中心演练

用户请求负载到双中心,下面的操作先将流量切到二中心—回归双中心—切到一中心。确保每个中心均可承担全量用户请求。

先将用户流量全部切到二中心

10.png
image.png
流量回归双中心,并切到一中心

11.png

回顾

  • 全局 Global 集群
  • 就近原则
  • 一主二从,写过半消息即及写入成功
  • 元数据系统 raft 选主
  • broker 主节点故障,自动选主

MQ 平台治理

即使系统高性能、高可用,倘若随便使用或使用不规范,也会带来各种各样的问题,增加了不必要的维护成本,因此必要的治理手段不可或缺。

目的

​让系统更稳定

  • 及时告警
  • 快速定位、止损

治理哪些方面

主题/消费组治理

  • 申请使用

生产环境 MQ 集群,我们关闭了自动创建主题与消费组,使用前需要先申请并记录主题与消费组的项目标识与使用人。一旦出现问题,我们能够立即找到主题与消费组的负责人,了解相关情况。若存在测试,灰度,生产等多套环境,可以一次申请多个集群同时生效的方式,避免逐个集群申请的麻烦。

  • 生产速度

为避免业务疏忽发送大量无用的消息,有必要在服务端对主题生产速度进行流控,避免这个主题挤占其他主题的处理资源。

  • 消息积压

对消息堆积敏感的消费组,使用方可设置消息堆积数量的阈值以及报警方式,超过这个阈值,立即通知使用方;亦可设置消息堆积时间的阈值,超过一段时间没被消费,立即通知使用方。

  • 消费节点掉线

消费节点下线或一段时间无响应,需要通知给使用方。

客户端治理

  • 发送、消费耗时检测

监控发送/消费一条消息的耗时,检测出性能过低的应用,通知使用方着手改造以提升性能;同时监控消息体大小,对消息体大小平均超过 10 KB 的项目,推动项目启用压缩或消息重构,将消息体控制在 10 KB 以内。

  • 消息链路追踪

一条消息由哪个 ip 、在哪个时间点发送,又由哪些 ip 、在哪个时间点消费,再加上服务端统计的消息接收、消息推送的信息,构成了一条简单的消息链路追踪,将消息的生命周期串联起来,使用方可通过查询msgId或事先设置的 key 查看消息、排查问题。

  • 过低或有隐患版本检测

随着功能的不断迭代,sdk 版本也会升级并可能引入风险。定时上报 sdk 版本,推动使用方升级有问题或过低的版本。

服务端治理

  • 集群健康巡检

如何判断一个集群是健康的?定时检测集群中节点数量、集群写入 tps 、消费 tps ,并模拟用户生产、消费消息。

  • 集群性能巡检

性能指标最终反映在处理消息生产与消费的时间上。服务端统计处理每个生产、消费请求的时间,一个统计周期内,若存在一定比例的消息处理时间过长,则认为这个节点性能有问题;引起性能问题的原因主要是系统物理瓶颈,比如磁盘 io util 使用率过高,cpu load 高等,这些硬件指标通过夜鹰监控系统自动报警。

  • 集群高可用

高可用主要针对 broker 中 master 节点由于软硬件故障无法正常工作,slave 节点自动被切换为 master ,适合消息顺序、集群完整性有要求的场景。

部分后台操作展示

主题与消费组申请

12.png

生产,消费,堆积实时统计

13.png

集群监控
修改图2.jpg

踩过的坑

社区对 MQ 系统经历了长时间的改进与沉淀,我们在使用过程中也到过一些问题,要求我们能从深入了解源码,做到出现问题心不慌,快速止损。

  • 新老消费端并存时,我们实现的队列分配算法不兼容,做到兼容即可;
  • 主题、消费组数量多,注册耗时过长,内存 oom ,通过压缩缩短注册时间,社区已修复;
  • topic 长度判断不一致,导致重启丢消息,社区已修复;
  • centos 6.6 版本中,broker 进程假死,升级 os 版本即可。

MQ 未来展望

目前消息保留时间较短,不方便对问题排查以及数据预测,我们接下来将对历史消息进行归档以及基于此的数据预测。

  • 历史数据归档
  • 底层存储剥离,计算与存储分离
  • 基于历史数据,完成更多数据预测
  • 服务端升级到 Dledger ,确保消息的严格一致

了解更多 RocketMQ 信息,可加入社区交流群,下面是钉钉群,欢迎大家加群留言。

15.jpg

深度解读畅捷通云原生架构转型实战历程

alicloudnative阅读(182)评论(0)

新的头图.png
在信通院 2021 年云原生产业大会上,畅捷通获得 2021 年度云原生优秀案例。

畅捷通公司是用友集团旗下的成员企业,专注于服务国内小微企业的财务和管理服务。一方面,畅捷通将自己的产品、业务、技术架构互联网化;另一方面,畅捷通推出了畅捷通一站式云服务平台,面向小微企业提供以数智财税、数智商业为核心的平台服务、应用服务、业务服务及数据增值服务,致力于建立“小微企业服务生态体系”。

根据易观国际的报告,目前畅捷通在国内小微企业云服务市场的覆盖率保持第一。有超过 130 万家企业与机构通过使用畅捷通的软件及服务,实现降本提效、持续创新、数智化转型。

早期,畅捷通的业务应用都是构建在公司自主研发的 CSP 平台之上,包括客户管家、易代账、好会计和好生意等,每个企业分配一个独立的虚机,资源完全隔离,初期该平台方案极大简化了开发的复杂度,提高了开发效率,满足公司向云服务发展的最初需求。

新的需求

随着用户规模的增多,现有为每个用户分配一个独立虚机的方案产生的问题很突出。首先体现在虚机的数量日益庞大,在客户转换率不高的情况下,容易造成资源的浪费,同时运维成本也偏高;其次,不同的用户软件迭代的版本不尽相同,还需要专门设计维护多套版本的补丁更新系统;第三,产品需要停服上线,业务会出现短暂不可用的情况;最后,如果若干用户的业务出现高峰期,不能快速弹性扩容,资源的利用率不高。

在此背景下,畅捷通研发团队决定尝试当前业界比较通用的云原生架构设计方案,利用云上的基础设施,共享计算资源和存储资源,采用容器化方案,快速弹性扩容,利用微服务架构,按应用更新产品,彻底解决当前运维成本偏高、弹性不足、资源利用不均衡的问题。

小微企业的特点是数量多、单个企业业务量相对较小、企业 IT 能力有限。以畅捷通好生意产品为例,采用现有的云原生架构,能够方便畅捷通快速开发出一款应对大规模小型用户,支持弹性可扩展的 SaaS 应用。同时通过服务的编排,又能快速搭建出其他产品线,如智+。目前已经有超过 20 万的付费用户正在使用畅捷通提供的云原生架构企业云服务,每天产生的业务数据达百 G 以上。

云原生应用架构的驱动力

国内云计算产品快速发展,企业应用往云端迁移趋势明显,加上政府部门鼓励企业上云推出补贴政策,企业上云已成为大势所趋。

尤其在疫情阶段下,商业模式的变革,消费方式的转变,只有企业上云才能更有利于推动企业加快数字化、智能化的转型,更有效的帮助企业实现“客户在线、业务在线、人员在线、管理在线”。而现在的云原生技术带来的价值能更好的帮助企业进行数智转型。

1. 实时在线

采用智能接入网关,就近接入云企业网,在 VPC 间、VPC 与本地数据中心间搭建私网通信通道,通过自动路由分发及学习,提高网络的快速收敛和跨网络通信的质量和安全性,保证用户全球范围实时在线,加速实现整个客群的线上线下一体化。

2. 数智化

通过云端廉价的存储、强大的算力资源以及众多的算法模型,畅捷通用极低的成本存储海量数据并在线实时分析,为用户提供更多的商业决策。

3. 快速响应市场需求

采用 DDD 设计思想,利用微服务架构,快速开发高内聚、低耦合的应用。这些应用通过服务的编排,能快速组合更多的应用,满足不同行业和领域的客户群体,达到快速上线、迭代优化的效果。

4. 稳定高可靠

利用容器和微服务架构,可以快速构建和运行可弹性扩展的应用。系统出现故障或者性能瓶颈的时候,通过镜像可以秒级恢复受损应用,保障了系统的高可用性。利用云原生技术的红利,畅捷通可以只关注业务的开发,一方面加速构建新应用,另一方面也可以优化现有应用并在云原生架构中集成,达到奔跑中更换轮子的效果,去更方便地让历史存量的客户升级到云上。

云原生应用架构设计

云原生应用架构设计路线

原有产品是部署在物理 IDC 中,通过对 cloudfoundry 云平台的二开,实现每个租户间虚机的隔离。但由于每个租户独享容器+数据库,当用户量达到几十万级时,数据库的升级效率、容器部署成本、硬件运维复杂度都明显提升。通过应用的微服务化、上云来解决降本提效的问题迫在眉睫。

畅捷通通过基础设施上云、数据库上云、技术框架和业务框架的重构,实现了在多租户之间容器部署、应用共享、DB 共享,产品基于 EDAS 及集成在其上的阿里云容器服务 Kubernetes 版 ACK。希望通过云原生的技术红利,解决当前运维成本高、系统弹性不足、产品迭代交付周期长的问题。

应用架构的改造

1. 微服务架构

将复杂应用按照业务的视角切分为高内聚、低耦合模块,这些模块独立开发、独立发布。业务领域一共分为四层,即核心领域服务层、业务领域服务层、应用服务层和接口服务层。其中核心领域服务层包括授权、UOM、组织(Party)、产品、计价、促销和存量模型模块,主要提供核心领域知识、能力服务;业务领域服务层是提供好生意业务的业务功能,包括采购、库存管理和销售领域服务;应用服务层基于具体应用场景,调用领域服务,解决应用中具体的业务问题。每层服务都是一个单独的微服务,基于 EDAS 进行服务的全生命周期管理,通过引入 Spring Cloud 实现服务的注册发现以及治理。

此外,由于 EDAS 无缝集成了 ACK ,支持以容器的形式托管应用到阿里云 Kubernetes 集群或混合云集群(其他云域或IDC内自建集群),因此能够与畅捷通底层K8s集群打通,实现了 K8s 集群的定时弹性能力和基于 CPU/RT/Load 等某一监控指标的自动弹性能力。

2. 数据一致性

传统的单一服务架构,所有领域的功能都聚合在一个进程内运行,可以通过数据库的事务来保证业务强一致性。但是畅捷通现在按分布式微服务架构设计,不同领域模块会建成独立运行的微服务,微服务之间需要按照最终一致性方案来保证数据的一致。对于实时性要求高的场景,畅捷通采用 TCC 模型;对于实时性要求不高,可长过程处理的,畅捷通采用消息队列的方式进行服务的解耦,达到最终一致性。

技术架构的改造

1. 容器化管理

核心应用部署在容器中,通过 Kubernetes 进行统一编排和运行调度。对于秒杀场景或者耗算力的异步任务,通过函数计算来按需构建。

2. 服务治理

引入微服务架构后,服务管理尤为复杂,为此畅捷通引入 Spring Cloud 一站式解决方案,使得开发只需要专注业务的发展,不去关注技术细节。通过 Spring Cloud 完成服务发现注册、配置管理、限流降级、服务调用、数据监控等,实现降本提效,也降低了运维成本。

3. Gitops 流水线

基于 Gitlab、Jenkins、Rundeck、K8s,搭建自研的 DevOps 流水线,按照微应用独立构建、微服务自由组包、按照容器化进行发布部署,保证了研发、测试、线上各阶段的运行环境均保持不变。

4. 数据库层面的改造

所有租户共享数据库,按照应用、数据量等因素进行分库分表;引入 OLAP 数据库,分离交易库和分析库,避免大查询拖累用户的交易。

云原生应用技术框架


图片 1.png

系统分为前端展现层、业务中台、技术平台、运维中台以及基础设施层。

前端展现层:使用基于微前端应用集成思想形成的 H5 前端开发框架,使用 qiankun 实现同构应用和异构应用的集成,支持多端开发。

业务中台:采用微服务架构的设计思想,基于 EDAS 平台搭建而成,在应用服务层可以实现弹性伸缩、限流降级、流量监控等;业务模型通过元数据驱动并支持 GraphQL 查询;利用 RocketMQ 实现了业务的解耦。

技术中台:包括容器管理、DevOps 流水线、微服务治理、链路追踪等,是企业业务快速交付、系统稳定运行,业务快速创新的基石。

基础设施层:包括数据存储层以及中间件。数据基于关系型数据库存储,如MySQL、PolarDB 等,通过 Tenant 标识在数据库表级别实现多租户共享数据库;数据库也支持读写分离,查询操作只需要访问读数据库即可。其余中间件均由技术平台进行了二次封装,对业务屏蔽了具体的技术细节。

前台采用微前端框架

图片 2.png**

前端应用由于参与的人员增多、产品的应用模块随着需求不断增加,已经从一个普通单体应用演变成一个巨石应用,好生意、智+、微商城等产品前端开发工程都在一起开发,相互影响,导致功能开发、迭代上线无法按照应用单独部署。再加上前期业务和技术分层不清晰,导致公共组件抽象不够,业务和技术强耦合,技术想单独更新换代异常艰难。

畅捷通需要设计一套框架,确保畅捷通的业务代码能平滑的迁移,且还能确保畅捷通能在不影响业务在线的情况下,进行技术的迭代更新。

畅捷通只需要在主系统构造一个足够轻量的基座,再加上公共可复用的组件(包括基础技术组件、基础业务组件、公共业务服务等),然后让各子应用按照共同的协议去实现即可。这个协议包括,主应用应该如何加载子应用,以及子应用如何被主应用感知、调度,应用之间如何通信等。同时畅捷通这个系统还应该有提供统一的 build 工程,让各子应用不再关注配置、依赖、构建、发布等操作。这样,各个微应用之间就可以独立发布部署,不同应用之间的弱耦合,也降低了开发的复杂度。

技术平台-容器管理

快速发布环节:

基于从 git 的源码管理平台和配置管理平台的联动,实现了容器镜像的快速自动生成和管理,基于环境变量的区分和配置中心的统一管控,能实现一个容器镜像多环境部署模式,并且能对每次的代码进行安全和一致性扫描,保障代码环节的安全稳定。

闭环管理环节:

发布到线上后,基于阿里云 Prometheus 监控,异常信息发送到消息中心中,并且在消息中心数据汇聚和策略编排,形成了工单流的模式,实现有效数据的闭环管理。

业务保障环节:

在容器弹性伸缩方面,畅捷通借助 K8s 的 HPA 机制,基于阿里云容器服务 ACK 最大化利用资源的能力以及业务层自定义指标,实现面对直播、秒杀、在线考试等突发流量下微服务的快速扩缩容。

技术平台-DevOps 流水线

图片 3.png

  • 采用管道方式,将原本独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排。自动化构建、测试和发布过程可轻松测试每次代码更改并捕获易于修复的错误。
  • 通过构建 DevOps 工具链,实现从需求下发、到代码提交与编译,测试与验证到部署与运维的全过程支撑,打通软件交付的完整路径,提供软件研发端到端支持。

技术平台-微服务治理

随着业务的快速发展,畅捷通对原有的IT系统进行了大量的微服务化改造,以适应互联网大型应用快速迭代以及频繁发布的需求。由于 SaaS 化企业管理云服务具备用户量大、业务复杂、调用链路长、与第三方应用系统深度集成等特点,给微服务化改造工作带来了非常大的挑战。畅捷通必须提升整体的微服务治理能力与监控能力,在频繁的版本迭代中才能确保系统的稳定健壮。

图片 4.png

最终畅捷通的微服务部署到阿里云提供的企业级分布式应用服务 EDAS 上,运行在 EDAS 上的 Spring Cloud 应用,可以享受到应用生命周期管理、无损下线、全链路流控等一系列针对微服务治理领域的能力增强。特别在应用发布的流程中,EDAS 所提供的平滑上下线以及灰度机制极大程度的提升了系统在版本更新期间的稳定性,降低了应用发布所带来的风险。

技术平台-全链路监控

海恩法则指出:每一起严重事故背后,必然有 29 次轻微事故和 300 起未遂先兆以及 1000 起事故隐患。

图片 5.png

尤其是畅捷通采用了微服务架构后,由于 SaaS 产品所涉及到的业务链路极为复杂,当用户反馈系统 Bug 或者性能存在问题之后,技术团队需要耗费非常长的时间在错综复杂的链路之间定位故障源以及分析性能瓶颈。畅捷通也使用了一些 APM 工具类的产品,包括应用性能监控,用户体验监控,链路追踪,问题诊断等,但是这类工具仅能定位框架级的问题,对于自定义函数以及进程中的异步处理均无法做到链路追踪,在分布式应用架构中这类 APM 发挥的作用就更少了。

由于畅捷通的应用是托管在阿里云的 EDAS 中,EDAS 集成了应用实时监控服务 ARMS,可以监控微服务的健康状态和关键指标,并针对监控指标设置告警,及时发现并处理可能存在的异常或故障,以保障应用的健康和可用性,所以畅捷通只需要将业务操作与系统日志、系统日志和 ARMS 打通,就可以实现从业务出发,贯穿整个业务的生命周期,快速定位应用的性能瓶颈以及异常故障的位置。

为此,畅捷通实现了一套机制,将业务操作按照 Timeline 进行抽象建模,并结合系统日志、阿里云 ARMS 系统形成三位一体的全链路跟踪机制。

  • 原则上,除了读操作之外的写权限点所对应的交互都应当视作一次 BI。所以畅捷通可以简单地认为每个写操作的权限点就是一个 BI。这就反过来要求后端提供的 REST api 必须是面向场景,每个API对应一个权限点。
  • BI 与 REST api 的 request_id 之间需要建立关联,以便追踪业务操作与系统日志之间的关系。
  • WebFilter 拦截所有 RESTful API的Request 和 Response ,获取到请求和应答信息,通过交互协议中的取值公式从拦截到的数据中解析出具体的特征、角色、关系等数据。在 Request 请求中增加 Create 操作将实体的原值自动记录下来,在 Response 返回时额增加 Complete 操作,负责把新值写入,并记录前后变化。两者在接口的调用开始和调用结束时配对使用。
  • 在后台日志里面,记录了 user_req_id 和阿里云的 arms 的 uber-trace-ID 的对照关系。

通过全链路跟踪机制,畅捷通将业务的交互操作与阿里云 ARMS 应用监控关联起来,尤其是业务中还存在一些通过消息队列进行解耦的操作,畅捷通都可以通过 BI 来进行追踪,为畅捷通的微服务体系更进一步的提供了监控能力。在接入 ARMS 之后,通过全链路信息排查以及应用实时诊断等工具,将定位系统故障源以及性能瓶颈的工作量降低到了之前的 50% 以下,极大程度的提升了 IT 团队的工作效率。

技术平台-灰度发布

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行 A/B testing ,即让一部分用户继续用产品特性 A ,一部分用户开始用产品特性 B 。

灰度期:新特性在灰度环境发布一直到新特性部署到线上正式环境的这一段时间,称为灰度期。对于 2C 的应用,是以用户作为灰度的基本单位进行分流。而对于 2B 的应用,则是以租户为基本单位进行分流。

灰度环境包括数据库灰度和应用程序的灰度。若在数据库层面支持灰度,则需要新建一个灰度 DB ,把参与灰度的客户数据导入到灰度 DB 中,灰度结束后再把数据清洗合并到正式生产 DB 中。这个过程所需操作较多且成本较高,鉴于此,数据库层面不考虑灰度。基于这个设定,需要遵循以下几个约束:

  • 灰度客户的量控制在较小范围,以尽可能缩小数据修复的范围;
  • 模型设计严格遵从兼容原则,如:只增不减,字段避免复用;
  • 对于影响业务逻辑的系统数据需要有评估。比如增加了某个系统级的枚举值,只对灰度客户可见,可以考虑针对灰度客户复制一份系统数据,灰度后删除,系统后端代码逻辑优先取租户数据,取不到再获取系统级数据。

应用程序的灰度因为后端对外提供的服务都是 REST API ,可以采用支持调用外部服务的负载均衡或反向代理获取灰度用户名单后,对用户进行分流。后端接口的兼容性需要保证,当无法保证兼容性的情况下,需要强制前端一同升级。

图片 6.png

基础设施层-数据库读写分离

初期云服务上线的时候,用户规模小,数据量也小,所以线上环境 MySQL 没有实现读写分离方案。在平稳运行一段时间,随着用户规模导致 PV 和 UV 增加,给数据库带来了巨大压力,主要体现在如下三点:

  • 复杂业务,多表联查效率低下,导致业务功能响应不够及时甚至查询超时;
  • 业务高峰期传统的数据库无法快速升配,只能等待业务高峰过去或者做流控;
  • 业务代码对主从延迟敏感,无法充分利用从库。

初期采用 Sharding-JDBC 实现读写分离,但现有产品读写操作互绕,不易将读操作分离,且读库和写库的同步延迟较大,不太满足业务需要。调研 PolarDB 后发现,其集群地址直接支持读写分离,且与 MySQL 语法 100% 的兼容,对业务无影响。通过 PolarDB 的分钟级弹升能力,能快速升配;其特有的并行查询能力也可以降低复杂查询的响应时间,同时提升系统的并发能力。

所以畅捷通将数据库从 MySQL 迁移到 PolarDB,并采用了一写多读的模式,并对耗时的报表查询业务单独指定读节点,降低了耗时操作对其他业务的影响,保障了用户的正常交易操作。

运维中台-监控体系

传统的报警机制,以消息为载体,尽量简洁,更倾向于报警即故障模式,涵盖信息少,对后期判断往往起到一个提醒开始的状态。

随着运维模式的提高,大量高效工具的结合,自愈,时序事件等相关系统不断涌现,同时近年来兴起的云原生模式,将实现智能预警的老大难问题:信息标准化问题,给出了优质的解决方案,也加速了升级监控结构的步伐,报警的单一模式已不再适应需求,运维人员更希望报警有意义,有过程、有结论。

畅捷通长期致力于以客户体验为基础的立体化监控架构,从客户性能与体验损失的纬度,对监控信息分级,分权重,通过业务链中各环节的客户画像,特征模型,精准监控用户体验。可在用户未感知的情况下预警并进入处理流程,进入流程的事件在内部消息中心扭转,关联计算,分析根因以及自愈方案。并根据事件处理模型响应事件,发送报警,报警涵盖事件的关键信息与结论。在提高了预警有效行的同时,也避免的传统故障处理模型中的各种效率损失。

云原生应用服务特点

DevOps 工具-故障检修中心

众所周知,分布式系统遵从 CAP 理论,畅捷通通常选择满足 AP 而放弃 C(一致性)。按照墨菲定律,畅捷通最终是需要通过人工干预来保证数据的最终一致性。那么检查发现不一致的数据、区别错误场景并按规程修复,就需要有一套系统来辅助进行。

不同于系统运维,业务检修面向的是业务和服务,通过租户和租户数据的监控来发现和解决业务问题。系统运行中,除数据不一致之外,还有其他一些常见故障,也需要为支持和解决一些较为普遍有共性的客户问题提供便利,以及提供部分分析视图帮助研发或运维掌握了解租户状态和趋势,这些诉求的响应也都会因为生产环境的物理隔离和安全性要求,而变得低效,只有通过固化到系统中形成工具集才能更好地解决。

所以,畅捷通提供了一套业务检修系统,负责业务数据的监控、问题诊断,故障修复、态势预警等。整体设计如下:

图片 7.png

目前系统针对开通失败、定时任务失败、死信、数据稽核违规、导入超时、TCC 失败六类故障进行定时检查,并与运维的 Midas 消息系统集成,及时发现问题,并向具体负责人发出钉钉告警;负责人通过故障展示的看板和明细列表来查看具体原因;针对具体故障,还提供查看上下文日志、重新开通、死信重放、数量帐重算、重提交等辅助手段解决故障。

安全服务-内容安全

畅捷通的产品会涉及到协同中的聊天信息,电商类的商品评价,这些内容可能会被外人恶意使用,将一些非法广告、网络诈骗、恐怖信息等内容录入畅捷通的产品中并传播出去,所以畅捷通的运维安全部门需要增加内容安全的监控,保障公司产品的纯净、健康运行,避免网络犯罪行为发生在畅捷通公司的产品和用户之中。

图片 8.png

畅捷通借用系统的操作日志和消息队列,将内容安全检测和业务功能进行解耦,借助函数计算的能力,按需调用云服务厂商提供的安全检测能力(文字类、图片类等),去识别可分享的业务,内容是否符合安全法,检测结果还增加了人工审核及误判赦免。

数用分离

图片 9.png

统计分析类数据,畅捷通每天通过 DTS 将原始数据从关系型数据库增量同步到数据仓库里,在固定时间进行数据清洗、汇总统计分析后,再将这部分数据传输 Elasticsearch 。同时畅捷通也利用 MaxCompute 的多任务计算能力,进行业务数据的标签计算,计算结果也传递给 Elasticsearch ,业务系统通过 Elasticsearch 的 REST API 访问这些数据,并展现给最终用户。

全链路流量控制技术+端云联调

由于微服务间的依赖,开发人员无法在本地完成开发和测试,必须依赖一套测试开发环境。

研发效率方面:当上百人的开发人员共享一套环境,开发态的代码频繁变更,质量较低,服务之间相互影响,开发环境经常中断,调试困难,严重影响了开发效率,研发希望每个项目能提供一套环境,如图:

图片 10.png

研发成本方面:如果为每个项目提供全量环境,计算资源成本很高,运维成本也会激增。(在开发态有近百个微服务,按 20 个项目并行开发计算,需要近 2000 个POD/ECS 的计算资源成本和运维成本)产品在成长期,并行的项目和服务数都在增加。

综合效率和成本方面的因素,畅捷通引入网关、全链路流量精确控制技术、端云联调技术,用基线环境+增量修改的应用叠加出项目开发环境:在入口处根据企业 Id 进行流量打标(根据企业 ID 在请求中注入环境标识),如:https://cloud.chanjet.com/req?orgId =企业 ID ,环境标识随请求在整个执行流程中流转(http 请求,rpc 请求,定时任务,消息等),通过环境标识,对微服务调用/消息进行精确控制。如图:

图片 11.png

畅捷通在基线环境部署最新上线的代码,在项目环境中只部署涉及修改的应用:

(1)为项目研发提供稳定的独立项目环境,使研发能按“小规模,多批次”(DevOps 最佳实践)的方式快速协作;
(2)节省了资源、降低了运维成本。(按每个项目修改 5% 的应用,在开发态有近百个微服务,按 20 个项目并行开发计算,将节省 2000*95%=1900 个ECS/POD);
(3)使用端云联调技术,方便开发本地PC加入到项目环境调试。如图所示:

图片12.png

云原生带来的技术价值

高弹性可扩展

系统可以在应用服务层和数据库层支持横向扩展。

应用方面:通过微服务架构,容器化部署,技术平台可以感知服务器负载,弹性伸缩服务节点,实现集群扩/缩容,快速补充计算能力。

数据库方面:数据库支持快速扩容读节点,以支持更多并发请求,同时租户实现数据隔离,并可根据负载实现跨库迁移。

微服务化

在微服务架构和设计阶段:

  • 引入 MDD 领域驱动方法论进行应用架构设计和领域模型设计;
  • 按照微服务的设计原则进行服务的拆分和职责、关系定义,确定服务接口和领域模型;

作为一个复杂的 ToB 应用,畅捷通按照如下原则进行微服务的拆分:

  • 可用 – 拆分的模块能够满足应用的需要;
  • 好用 – 拆分的模块能够通过比较简单、清晰的方式组成应用,服务应用价值;
  • 美 – 用最简单的方式表达复杂问题;业务人员容易理解。

Back-end For Front-end

服务器端采用微服务架构之后,畅捷通需要在前后端之间增加 BFF 层,简化前后端人员的协同开发复杂度。BFF 层基于 Node 实现,畅捷通选择了 Egg.js ,并基于Egg.js 做了分层封装,在 Node 自身技术升级的情况下可以不影响业务开发。为了提高效率,畅捷通还在 Node 层接入了缓存中间件 Redis ,并利用 GraphQL 做聚合查询。

端云联调方案

采用微服务架构后,每套环境部署需要 30+ 的微服务容器,如果开发阶段存在多特性并行,为每个特性分支单独部署环境,资源消耗太大。为此,畅捷通引入了网关、全链路流量打标控制技术、端云联调技术,用基线环境+增量修改的应用叠加出项目开发环境,以最小代价快速拉起一套完整的开发调试环境,降低了开发成本。同时开发人员也可以将本地电脑直接注册到微服务环境中去,在本地完成上下游业务的验证和测试,极大地提高了开发人员的工作效率以及提交代码的质量。

离线数据分析

早期,畅捷通的数据库只有 OLTP ,数据存储在 MySQL 里,随着数据量的增多,统计分析类的查询不仅自身查询时间长,还消耗了在线交易库的资源,影响在线交易的响应时间。仅仅通过云原生数据库 PolarDB ,进行读写分离,只能缓解一部分查询压力。

针对系统中一些实时性要求不高的分析,畅捷通引入了数据仓库进行离线数据的处理,通过 DataWorks 工具进行 ETL 和统一调度,在 MaxCompute 中进行大数据指标计算和标签计算。

全链路灰度方案

畅捷通的系统,实现了从前端到后端服务、消息队列、定时任务的全链路灰度。
前端静态资源:区分灰度静态资源和正式环境静态资源。登录后由登录信息里的租户,确定应该路由到哪个环境。
消息队列:区分灰度消息队列和正式消息队列。根据环境变量进行消费
定时任务:定时任务的任务执行方,通过环境变量和租户名单,来决定是否执行。
后端 Rest 接口:在 Nginx 中通过 lua 脚本,解析 url 路径,根据灰度名单中的租户信息进行路由。

云原生带来的业务价值

容器化运维管理:

基于 EDAS+ACK 模式的容器化部署和管理,实现了应用发布的提升,并且在弹性伸缩和容量管理,也可以加快异常识别,故障自愈。并且在 2(异常识别)-5(快速定位)-10(自愈止损)的目标上不断攀升。

可观测性:

基于日志平台和数据中台的结合分析,实现用户画像和用户轨迹的展示,并且对数据的不断挖掘,可以实现用户体验百分数量化,产品质量百分数量化,从而在服务团队对接上提供数字化保障。

成本方面:

从 IDC 机房容器模式到云平台容器化的升级,不必要投入到硬件设备的大额投入和替换设备上,从而在设备成本和人力成本上大幅度节省了很多,并且新的需求和想法都可以按量投入,弹性伸缩。

DevOps:
对接云原生环境的 devops ,在研发需求支持上,节省人员 50%,构建及部署效率上提升了 4 倍。其中 2020 全年更新次数 12734 次,成功率 91.8%。

多环境资源:

通过多特性灰度方案的模式,7 套开发环境合并为1套。其中每套环境 90 个微服务,可以实现灵活性的动态调整和扩展。节省 450 个节点。

云原生容器化
容器化 EDAS+K8s 部署模式,可以实现节点的弹性伸缩,特别是支撑了畅捷通直播活动,秒杀场景。不仅仅节省了人力的部署等环节投入,也节省了秒级伸缩的成本。

多云平台:

通过 DevOps 和容器化模式的建立,满足了用户多云场景的需求,并且节省人力50%,实现多云环境的自动化运维模式。

数据库升级:

好生意从 MySQL 升级到 PolarDB 后,其中处理性能提升 20%~40% ,并成功的将无法支持的低端手持 pda 适配工作成功复活。抽样回访客户,满意度也大幅提升。

稳定性:

​基于云原生基础组建的对接,云产品可以实现 5 个 9 的 SLA 服务,这样避免了自身搭建开源组件的稳定性和安全风险。并且在组件功能上也能享受到各种业务需求功能的福利。对接云原生后,可以提出 2(告警)-5(定位)-10(止损)的稳定性保障目标,从而在用户满意度上不断提升。