云联壹云是完全自研的一套融合云平台,Golang是主要的开发语言,本文主要介绍介绍在迭代过程中关于Golang的经验以及在Golang上积累的框架和库。
在开发过程中,我们也积累了Golang的库函数,并基于这些库函数去开发框架以及平台,当然还有库的特点,实现库的原因及其优点。
背景介绍
融合云平台—云联壹云是从2017年开始逐步迭代开发,平台在17年时是私有云,能够管理在用户部署在本地物理机上的KVM,同时也能管理裸金属的服务器。
当时企业的IT环境并不仅仅是本地的虚拟机以及裸金属,企业的IT基础设施已经逐步采纳多云的技术。
所以平台不仅能管理本地IT环境中的虚拟机和裸金属。还能管理其他云,特别是能够帮助企业管理公有云的资源, 做到所有的资源在一个平台上统一纳管,运维,操作,起到降低运维复杂度并提高企业IT运维效率的目的。
平台后端采用的是Golang,目前为止已有60万行代码,前端采用的是Vue框架,整个平台是基于微服务的框架,每个服务之间的认证鉴权是基于Keystone组件。
Golang积累
首先是Golang的服务框架,所有的组件都是基于这个服务框架来开发,服务框架的特点比较适合在我们平台开发,并且针对平台的特点做出优化,适合快速开发服务。
基本所有服务都是基于这个服务框架开发,此框架是比较方便做CRUD的脚手架框架。
因为服务主要是对云资源的操作,比如云资源的创建、删除、更新等。
因为云的资源非常多,通过脚手架能够比较方便地实现资源的CRUD操作,再加上其他机制实现对云资源的复杂操作以及信息回复。
除CRUD脚手架外,其实它把平台的特别功能加进去,首先组件之间是基于keystone认证,所以在将keystone认证加到框架中,则开发不需关注keystone认证,只要代码是在框架中实现的,天生就集成了keystone认证。
每一个API都受到权限的控制,权限控制也集成到框架中,每一个开发者在开发平台相应的restapi时,不必为权限写相应的代码,能够天然地将权限控制集成到API中。
微服务框架的每一个服务都有相应的配置,如何方便地管理服务配置,并进行更新 ,同步到相应的组件使其生效,此过程相对复杂,我们将服务配置的功能集成到框架中,开发者采用框架不必考虑配置的存储、更新、服务器读取更新并使配置生效,这些复杂事宜已在框架中解决。
还有异步任务的管理功能,平台可以被认为是一个分布式的系统,云控制器需要去操作和管理数据计算节点、裸金属的管理节点。协调组件之间的复杂操作,例如将虚拟机、裸金属创建起来,这些都是分布式的任务管理,在平台中也嵌入了异步任务管理框架。如此即可较为方便地实现异步任务。
CRUD脚手架原理
在平台中,每一种资源,例如主机,在底层对应到数据库MySQL表,资源的状态、相应的属性都记录到了MySQL表中。
用户通过调用API对数据进行操作,在数据操作的同时也能做额外的异步的task,然后去实现相应功能。落到底层代码中就是一种资源对应到一张MySQL表。
为了去比较方便地实现对数据库MySQL记录的操作,针对每一个资源,都会对应到ModelManager和Model的一对数据结构。
ModelManager数据结构是对应到Golang的structs,这个结构可以实现这一类资源的集合操作,例如创建资源或者列表,而针对单个资源的操作,则通过Model来实现,实现对某个资源的更新、删除的操作。
Model对应到Golang的结构体,结构体有若干字段,每个字段代表资源的属性,例如此处有一个用户的资源 ,用户的id、Extra属性,用户是否enabled ,用户何时创建,归属的域,都是这个用户资源相应的属性。
这个属性就是Golang结构体的字段,通过结构体字段的Tag属性,如此即可定义每个字段在MySQL的数据库中对应的schema的定义。
例如Id这个字段,属性中有width:"64" charset:"ascii" nullable:"false" primary:"true"
这定义了在Id这个字段是数据库里面的一个varchar(64)的字段,并且他的字符集是ascii码。所以通过tag将结构体的字段映射到了MySQL的schema的字段中,如此,每一个model通过字段的定义就能够清晰地映射到MySQL的数据表中。
这样我们就实现了Model的字段和MySQL的数据表定义的严格同步,每次程序启动时都会检查,如果Model 的定义和数据表的定义不太一致,然后就会执行相应的SQL的变更操作,将表的定义和Model的定义变更为一致。
例如我们将Id的宽度从64改成80,在程序重启时就能够发现这个变化,然后将数据表的宽度也变更成80。
如此即可实现通过代码定义的Model 和数据库中的表精确地映射上。
每个 Model、资源都会提供一系列的API,此处已经列出对一个资源会实现的九类API。
例如创建、删除、更新、执行操作、获取详情、列表等操作。
每一个操作对应restapi,每一个restapi 对应到后端代码中就对应到了每一个资源对应的Manager或者Model的方法。
例如我们要获取资源详情的方法就是他restapi路径就是GETresources,resources 的Id,调用这个restapi 就映射到相应的Model的GetDetails的方法。
为了实现获取资源的详情只需要去实现Model中的GetDetails的方法的内容,如此即可实现restapi的功能。
通过框架简化了实现restapi的流程,只需要把相应的Model和ModelManager的方法根据输入实现相应逻辑,然后把正确的输出返回回去,这个restapi的功能即可实现。
如此诸如健全 、认证、配置、同步等周边的工作即可在框架中实现,从而大大提升开发效率并降低在开发过程中犯错的几率。
框架中包含许多内容,包括认证、权限、配置变更管理 、配额管理等。
Golang 库
下面介绍在过去开发过程中积累的Golang库。有利于更加方便 、高效地实现需要的功能。
jsonutils是一个JSON序列化和反序列的工具库。
Golang 的标准库中带的库是encoding/json,encoding/json也是一个非常强大、非常高效的序列化和反序列的工具库,下面为大家介绍为何抛开encoding/json另外实现jsonutils的库。
encoding/json实现的是Golang的数据结构和对应json的字符串之间的相互转换。
我们可以把Golang中的结构体通过Marshal的方式生成一个Json的字符串,或者把Json的字符串通过Unmarshal放到相应的结构体中的各个字段,这样即可访问结构体去获得json中的这些值。
jsonutils与encoding/json相比的明显区别是中间增加了一个中间态,在jsonutils库里面实现JSONObject。
这是中间态的无类型的数据,我们可以把数据结构Marshal(s)成JSONObject,JSONObject是Golang的interface。
下层是一个结构体,interface可以进一步地序列化成json字符串。
这样是在Golang的结构体和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 嵌入到UR。
如此我们可以将这种参数在框架中反序列化成JSONObject,把它作为JSONObject输入参数传入到框架中。
如果是对于把参数作为body中的JSON字符串,我们同样可以把它解析成JSONObject,可以以同样的逻辑去处理嵌入到UR中的QueryString的参数 以及嵌入到body 中的Jason的参数,可以做到统一处理的逻辑。
另外jsonutils 还有还有一些比较特别的地方,就是针对我们平台做的一些特别的一些处理。较为重要的一点是支持结构体字段的版本变更。
以下为举例:
\
例如有一个输入参数的结构体称为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,这个库是CRUD的框架中实现Model和数据库映射的底层实现。
在sqlchemy中实现了Golang的数据结构到MySQL表严格单向同步,能够根据结构体字段的定义以及字段中tag的定义,严格地生成精确的MySQL的schema。
保证数据库中schema总是严格地和结构体定义保持一致,如果不一致,即可去自动变更数据库,然后将它和结构体的定义变更为一致。
另一个重要特性是能够实现结构化的数据库查询语句。
这里举一个简单的例子,就是我们把刚才的User表进行一个查询 ,我们这个查询在Golang中并不会实现SQL的语句,而是会有结构化的查询的方法。
图中上面框中的语句就是首先实例化UserTable实例,然后调用它的Query接口返回一个Query。
然后再调用图中所示的q.Equals(“domain_id一个值。
这样就表示在查询user的这个表,并且要求domain_id 这个字段要等于这个值。
通过sqlchemy的库在执行时就会把结构体查询变成真正的SQL的查询语句,然后把它送到MySQL进行执行。
使用这种结构化的数据库查询方式的优点是避免了人工拼凑SQL容易出现的问题,并且Golang是一个严格的静态语言。可以通过Golang的语法检查保证查询语句的正确性。
另一个好处是因为把SQL的查询代码化,这样就可以把数据库查询的逻辑进行一定程度的代码复用,这就不用重复同样的查询语句,而是调用一个方法,这个方法中就嵌入了SQL的查询逻辑。
框架中所有数据库的操作查询都用到sqlchemy,在一定程度上保证框架代码中SQL语句的正确性以及执行效率。
另一个比较重要的库是structarg,这个库的作用是能够将程序的命令行参数或者配置文件、信息、代码中的结构体做严格的映射。
这样可以基于结构体自动生成程序输入参数的提示,并且能够根据命令行参数将参数中的值反序列化放到结构体中, 或者是将对应配置文件中的参数反序列化到结构体中,在程序中就可以通过访问结构体的字段去访问相应参数的值。
如此即可比较方便地在程序中使用配置的信息,这里举个例子是我们定义了UserOption的结构体,这个结构体tag中包含每一个结构体的每个字段就是一个命令行,一个参数或者是配置文件的一个参数,这个参数的含义 Help message就放到了tag中,通过对这个结构体,将其初始化并进行程序编译之后,如果命令行中执行带help的参数的话,其将会把这些参数以易懂的方式告诉用户其参数格式。
同时,用户也可以在命令行参数中去用这些参数把值传递到这个程序中,在这个程序代码中即可通过访问结构体去访问这些参数。
同时,也支持配置文件的输入,而且配置文件同时支持KV格式 和YAML格式。
这样可以将程序的配置文件的配置信息通过结构体定义下来 ,程序就能够自动地识别配置文件的信息,然后将信息放到结构体中访问。
框架中对配置管理的基础就是structarg的功能,利用该功能将每一个服务的配置用结构体来定义,并且配置的信息会存到数据库中,在数据库的信息进行修改之后,框架会把数据库中的配置信息拉取下来,然后把它反序列化到结构体中,程序中能够感知到配置的变更,并做出相应的配置变更处理。
除去三个比较重要的库之外, 还有小的方法库放在pkg这个库中。