Cloudpods是齐全自研的一套云平台,Golang是该平台的次要后端开发语言。本文介绍咱们在平台开发迭代过程中对于Golang的教训以及在Golang上积攒的框架和库,包含积攒的Golang工具库,以及基于这些工具库实现的开发框架。

1、背景介绍

Cloudpods(云联壹云)从2017年开始迭代开发。过后企业的IT环境曾经不仅仅是本地的虚拟机以及裸金属,不少企业曾经逐渐驳回多云。所以Cloudpods平台作为新一代的云平台,须要不仅能治理本地IT环境中的虚拟机和裸金属,还能治理其余的云平台的资源,特地是私有云。实现所有的资源在一个平台上对立运维,操作,起到升高运维复杂度并进步企业IT运维效率的目标。

为了实现一个对立的多云平台,咱们采纳了最适宜开发云原生利用的 Golang作为后端开发语言。前端则采纳Vue框架。整个平台基于微服务框架。服务之间的认证鉴权基于OpenStack Keystone的框架(咱们用Golang从新实现了Keystone)。

2、Cloudpods Golang技术栈

云联壹云的Golang技术栈蕴含两局部:

首先是一个Golang的服务框架,所有的服务组件都基于同一套服务框架来开发,这个服务框架针对云平台的特点做出优化,适宜疾速开发云服务的API和异步工作逻辑。

其次是四个次要的Golang工具:

3、Golang框架

大部分Cloudpods服务都基于同一套服务框架开发,此框架的特点是针对云服务开发进行了适配和优化。

Cloudpods的服务框架能够简略地认为是一套不便CRUD的脚手架。云服务次要是对云资源的操作,比方云资源的创立、删除、更新等。服务框架内置了云资源的CRUD API的根底框架,再加上异步工作机制实现对云资源的简单操作以及资源模型的批改。

除CRUD脚手架外,服务框架把认证,权限、配额等云平台特有的性能内置集成,使得认证和权限成为云平台默认的标配,同时简化相应性能的实现。

首先,服务组件之间的通信基于OpenStack Keystone认证,咱们将keystone认证加到框架中,使得服务组件开发者不需关注keystone认证流程如何实现,就能人造反对keystone认证。其次,每一个API申请都受到权限的管制,因而把权限管制也集成到框架中。开发者在实现REST API时,根本不用为权限管制实现相应的代码,就可能人造地将权限管制集成到API中。

与此同时,每一个服务都有相应的配置参数,如何不便地治理各个服务的配置,容许对配置更新并同步到相应的组件使其失效,此过程绝对简单。咱们将服务配置的性能集成到框架中,开发者采纳框架不用思考配置的存储、更新、服务器读取更新并使配置失效,这些简单事宜已在框架中解决。

另外,还有异步工作的治理性能。平台是一个分布式的零碎,云控制器须要去操作和治理数据计算节点、裸金属的治理节点。协调组件之间的简单操作,例如将虚拟机、裸金属创立起来,这些都是分布式的工作治理,在平台中也嵌入了异步工作治理框架。如此即可较为不便地实现异步工作。

4、CRUD脚手架原理

在平台中,每一种资源,例如主机,在底层对应到数据库MySQL表,资源的状态、相应的属性都记录到了MySQL表中。

用户通过调用API对数据进行操作,在数据操作的同时也能调用异步的工作去实现相应性能。落到底层代码中就是一种资源对应到一张MySQL表。

为了比拟不便地实现对数据库MySQL记录的操作,针对每一个资源,都会对应到一对ModelManager和Model的数据结构。ModelManager数据结构对实现一类资源的汇合操作,例如创立资源或者列表,而针对单个资源的操作,则通过Model来实现,实现对单个具体资源的更新、删除的操作。

每个资源的Model对应到Golang的一个构造体,该构造体有若干字段,每个字段代表资源的属性,例如有一个用户的资源蕴含用户的id、extra属性,用户是否enabled,用户何时创立,归属的域等。一个属性就是Golang构造体的一个字段,通过构造体字段的Tag属性定义每个字段在MySQL的数据库中对应的schema的定义。

例如Id这个字段 ,属性中有width:"64" charset:"ascii" nullable:"false" primary:"true"

这些tags定义了Id这个字段是数据库外面的一个VARCHAR(64)的字段,并且他的字符集是ascii码,不能为空,并且是主键。所以通过tag将构造体的字段映射到了MySQL的schema的字段中,如此,每一个model通过字段的定义就可能清晰地映射到MySQL的数据表中。这样实现了Model的字段和MySQL的数据表定义的严格同步,每次程序启动时都会进行schema的同步查看,如果Model 的定义和数据表的定义不统一,就会执行相应的SQL的变更操作,将表的schema定义和Model的定义变更为统一。

例如咱们将Id的宽度从64改成80,在程序重启时就可能发现这个变动,而后将数据表该晓得的宽度变更成80。如此实现通过代码定义的Model 和数据库中的表定义的严格同步。(另外,也反对离线变更数据库Schema,即只检测数据库Schema的变更,并且输入对应的SQL语句,通过专门的离线数据库schema变更工具实现数据库的变更。)

与此同时,每类资源都会提供一系列的API,此处列出了对一个资源会实现的九类API,包含创立、删除、更新、执行操作、获取详情、列表等操作。

每一个操作对应一个REST API,每一个REST API对应到后端代码中就对应到了每一个资源对应的ModalManager或者Model的办法。

例如获取资源详情的REST API是: GET /resources/<res_id>

调用这个REST API其实就映射到相应的Model的GetDetails办法。为了实现获取资源的详情只须要去实现Model中的GetDetails的办法。

通过框架简化了实现云资源REST API的流程,只须要把相应的Model和ModelManager的办法依据输出实现相应逻辑,而后把正确的输入返回回去,这个REST API的性能即可实现。

如此诸如鉴权 、认证、配置同步等周边的工作在框架中实现,从而大大晋升了开发效率并升高在开发过程中犯错的几率。

5、Golang 工具库

上面介绍在Cloudpods开发过程中积攒的Golang工具库。

  • jsonutils

jsonutils是一个JSON序列化和反序列的工具库。

Golang 的规范库中带的JSON库是encoding/json,encoding/json是一个十分弱小、十分高效的JSON序列化和反序列的工具库。encoding/json实现的是Golang的数据结构和对应json的字符串之间的互相转换。能够把Golang中的构造体通过Marshal的形式生成一个Json的字符串,或者把Json的字符串通过Unmarshal放到相应的构造体中的各个字段,这样即可拜访构造体去取得json中的这些值。

jsonutils与encoding/json相比的显著区别是两头减少了一个两头态,在jsonutils库外面实现一个两头态的通用类型的数据类型JSONObject。咱们能够把数据结构Marshal(s)成JSONObject,JSONObject是Golang的interface,该interface能够进一步地序列化成json字符串。

这个两头态就是应用jsonutils的重要起因,通过通用类型的JSONObject就能够实现任意的构造体都能够Marshal(s)成JSONObject而后能够把JSONObject作为函数参数进行传递。

Golang是一个严格类型查看的动态语言,它的每一个变量都有相应的类型,咱们的框架可能解决任意API的输入输出,如果没有两头的构造体,在解决API的输入输出时,输出是json字符串, 为了在程序上拜访它, 就必须把它反序列化成严格的有类型构造体,这样一来就无奈将框架变成通用框架。

如果引入通用的JSONObject,在框架中输出了json字符串, 先把它反序列化成JSONObject,这个JSONObject是通用类型的,这样就能够将JSONObject作为参数再进一步的向下传递,直到传递到具体相应的Model或者ModelManager相应的办法中,而后进一步把它转换成相应的构造体。这样就容许框架中应用曾经反序列化好的JSONObject 并进行操作,能够实现比拟通用的框架。

平台采纳jsonutils最次要的起因是不便实现通用的框架。同时jsonutils还有其它特别之处,JSONObject 不仅可能转换成JSON字符串,也能够转换成QueryString 或者是把QueryString 反序列化成JSONObject 或者能够序列化成YAML的字符串。

这样能够实现更不便的性能,比方对于列表 GET这种读取的这种API,它的参数通常作为QueryString 嵌入到URL中。如此咱们能够将QueryString参数在框架中反序列化成JSONObject,把它作为JSONObject输出参数传入到框架中。

对于其余类型的申请,能够把申请参数放在HTTP申请的body中的JSON字符串,咱们同样能够把申请参数解析成JSONObject。这样,能够以同样的逻辑去解决嵌入到URL中的QueryString的参数以及嵌入到body 中的JSON的参数,能够做到对立解决出入参数的逻辑。

另外jsonutils 还有还有一些针对云平台API做的一些特地的一些解决。较为特地的一点是反对构造体字段的版本变更。

以下为举例说明:

例如有一个输出参数的构造体称为Input,有一个字段是TenantId,用来标识用户的租户ID。

随着版本的降级,心愿将TenantId对立改名为ProjectId,这种降级如果不做任何解决,将可能呈现接口兼容性的问题,在变更之前这个字段是TenantId,变更之后这个字段就只能是ProjectId。降级后,应用TenantId的客户端就不能正确拜访这个接口。

在这个构造体中,咱们减少了特地的tag :yunion-deprecated-by,把这个构造体input降级为新的input的构造之后,减少了ProjectId的字段,用它来代表新的TenantId的属性。

旧的TenantId依然保留,然而在构造体的tag中加了名称为yunion-deprecated-by的tag,这个Tag的值是ProjectId。表明TenantId的字段曾经被ProjectId这个字段deprecated。

代码在解决时,如果旧客户端的参数中只有TenantId,此时框架就会将TenantId的值依据yunion-deprecated-by这个tag 指引同时copy到ProjectId的字段中。

这样,如果旧的客户端去拜访新的接口,在新的接口中同样能够用ProjectId这个字段去获取这个值。这样保障了旧的客户端能够拜访降级后的接口。

  • sqlchemy

Sqlchemy是后面介绍的Golang服务框架中实现Model和数据库表映射的底层实现。

Sqlchemy实现了Golang的数据结构到MySQL表单向同步,可能依据构造体字段的定义以及字段中tag的定义,生成准确的MySQL的schema。

Sqlchemy能保障数据库中schema总是严格地和构造体定义保持一致,如果不统一,可能主动地变更数据库,将数据库Schema和构造体的定义变更为统一。

Sqlchemy另一个重要个性是可能实现结构化的数据库查问语句。

 

这里举一个简略的例子。如上图所示,咱们要对UserTable表进行一个查问。首先实例化UserTable实例,而后调用它的Query接口返回一个SQuery实例,再调用Equals办法,进行过滤。这样就示意在查问user表,并且要求domain_id 这个字段要等于指定的值。Sqlchemy在执行时会把构造体的结构化查问变成SQL的查问语句,并送到MySQL进行执行。

应用这种代码化的数据库查问形式的长处是防止了人工拼凑SQL容易呈现的问题,并且Golang是一个严格的动态语言。能够通过Golang的语法查看保障查问语句的正确性。

另一个益处是因为把SQL的查问代码化,这样就能够把数据库查问的逻辑进行肯定水平的代码复用,这样就不必反复同样的查问语句,而是调用一个办法,这个办法中就嵌入了SQL的查问逻辑。

框架中所有数据库的操作查问都用到sqlchemy,在肯定水平上保障框架代码中SQL语句的正确性以及执行效率。

  • structarg

还有一个比拟重要一个库是structarg,这个库的作用是将程序的命令行参数或者配置文件信息和代码中的构造体做严格的映射。

这样能够基于构造体主动生成程序输出参数的提醒,并且可能依据命令行参数将参数中的值反序列化放到构造体中, 或者是将对应配置文件中的参数反序列化到构造体中,在程序中就能够通过拜访构造体的字段去拜访相应参数的值。

如此即可比拟不便地在程序中应用配置的信息,这里举个例子。

假如定义了UserOption的构造体,这个构造体tag中蕴含一个help的tag,这个tag的定义了构造体的每个字段的帮忙信息。将其初始化并进行程序编译之后,如果命令行中执行带help的参数的话,其将会把这些帮忙信息展示进去。

同时,用户也能够在命令行参数中去用这些参数把值传递到程序中,在程序代码中即可通过拜访构造体去拜访这些参数。

同时,也反对配置文件的输出,而且配置文件同时反对Key=Value格局和YAML格局。

这样能够将程序的配置文件的配置信息通过构造体定义下来 ,程序就可能主动地辨认配置文件的信息,而后将信息放到构造体中拜访。

框架中对配置管理的根底就是structarg的性能,利用该性能将每一个服务的配置用构造体来定义,并且配置的信息会存到数据库中,在数据库的信息进行批改之后,框架会把数据库中的配置信息拉取下来,而后把它反序列化到构造体中,程序中可能感知到配置的变更,并做出相应的配置变更解决。

  • pkg

除去三个比拟重要的库之外, 还有一些其余辅助的工具库在pkg这个库中。包含模拟python的prettytable的格式化输入数据的库,错误处理库,保障key程序的map的实现等。这里就不一一介绍,感兴趣的读者能够浏览相干源码。