KubeEdge CounterDemo 源码分析【超详细】【P1】
Kubeedge是一个基于Kubernetes构建的开源容器化应用编排与设备管理部署在边端主机的系统。本文作者编写此文时仍在学习过程中,作为学习笔记分享,文章内容可能会出现部分逻辑错误、分析错误,故此文仅供参考。若有问题,请各位开发者们评论区指出,共同学习。
在进行源码分析时建议已成功部署CounterApp且对于相关的知识已经有了初步的了解。
一、目录结构
counter-mapper(边端设备)
device -> device.go
main.go
crds(部署时使用的yaml文件)
kubeedge-counter-instance.yaml
kubeedge-counter-model.yaml
kubeedge-pi-counter-app.yaml
kubeedge-web-controller-app.yaml
web-controller-app(云端控制)
controller -> trackController.go
static
css -> …
fonts -> …
js
bootstrap.min.js
bootstrap-select.js
service.js
track.js
utils
constants.go
crdclient.go
kubeclient.go
views
content.html
head.html
layout.html
main.go
二、web-controller-app
(一)main.go
先从入口函数分析:
func main() {
beego.Router("/", new(controllers.TrackController), "get:Index")
beego.Router("/track/control/:trackId", new(controllers.TrackController), "get,post:ControlTrack")
beego.Run(":80")
}
Beego是一个应用开发框架,beego.Router(...)
是用来注册路由(将URL映射到Controller,第三个参数是用来设置对应httpMethod到函数名,即自定义方法)
例如:
beego.Router("/", &IndexController{}, "*:Index")
- 注意
- * 表示任意的httpMethod都会执行Index函数
- 格式
httpMethod:funcname
- httpMethod通过" , “分割,funcname通过” ; "分割
所以源码中URL为"/"时映射TrackController处理器,当请求为Get时会调用Index方法。
戳到TrackController处理器内部:
type TrackController struct {
beego.Controller
}
TrackController完全继承Beego内的Controller处理器。
戳到TrackController处理器内部Index方法(/webcontroller/controller/trackController)
// Index is the initial view
func (controller *TrackController) Index() {
log.Println("Index Start")
controller.Layout = "layout.html"
controller.TplName = "content.html"
controller.LayoutSections = map[string]string{}
controller.LayoutSections["PageHead"] = "head.html"
log.Println("Index Finish")
}
controller.Layout
用来指定模板文件(一般是layout.html),这个html文件是整个页面的框架。
controller.Tplname
用来指定个性化页面文件,Beego会先解析Tplname指定的文件,获取的内容赋值给LayoutContent,最后再渲染layout.html文件并展示。需要替换的位置需要写{{.LayoutContent}}
进行渲染。
controller.LayoutSections = map[string]string{}
用来定义Layout其他部分。下一行的PageHead定义了页面的静态JS部分。
<!DOCTYPE html>
<html lang="en">
<head>
...
{{.PageHead}}
</head>
<body>
{{.LayoutContent}}
{{.Modal}}
</body>
</html>
再回到入口函数
func main() {
beego.Router("/", new(controllers.TrackController), "get:Index")
beego.Router("/track/control/:trackId", new(controllers.TrackController), "get,post:ControlTrack")
beego.Run(":80")
}
总结一下:第一行注册了/
并映射到了TrackController处理器,在收到Get请求后会执行Index函数,Index用来初始化网页View(加载模板Layout、个性化页面Tplname)
第二行则是注册了/track/control/:trackId
并映射到了TrackController处理器,在收到Get或者Post请求会执行ControlTrack函数。
戳进ControlTrack方法内部进一步分析。
// Control
func (controller *TrackController) ControlTrack() {
// Get track id
params := struct {
TrackID string `form:":trackId"`
}{controller.GetString(":trackId")}
resultCode := 0
status := map[string]string{}
log.Printf("ControlTrack: %s", params.TrackID)
// update track
if params.TrackID == "ON" {
UpdateDeviceTwinWithDesiredTrack(params.TrackID)
resultCode = 1
} else if params.TrackID == "OFF" {
UpdateDeviceTwinWithDesiredTrack(params.TrackID)
resultCode = 2
} else if params.TrackID == "STATUS" {
status = UpdateStatus()
resultCode = 3
}
// response
controller.AjaxResponse(resultCode, status, nil)
}
Golang中定义结构体字段时,除了字段名与数据类型名外还可以使用反引号为结构体字段声明元信息(Tags)
type User struct{
Id int `json:"id,-"`
Name string `json:"name"`
}
以上的例子使用的是encoding/json包编码或解码结构体时使用的Tag信息。Tag结构:key:"value"
键值对组成
// 还有例如
Id int `json:"id" gorm:"AUTO_INCREMENT"`
在ControlTrack方法中处理器会获取输入Key为:TrackId
的值并赋值到params结构体中的TrackID字段,可以通过params.TrackID
访问。之后初始化resultCode
与status
(空map)。
TrackID一共有三种值:ON、OFF、STATUS。如果TrackID为ON或OFF执行UpdateDeviceTwinWithDesiredTrack函数并分别设置resultCode(执行结果标志)1、2,Status则执行UpdateStatus函数,设置resultCode 3。
现详细分析3种TrackID对应的函数到底干了什么
首先是TrackID为ON或者OFF时执行的UpdateDeviceTwinWithDesiredTrack
// The default status of the counter
var originCmd = "OFF"
...
// cmd: 浏览器中输入的期望状态
// UpdateDeviceTwinWithDesiredTrack patches the desired state of
// the device twin with the command.
func UpdateDeviceTwinWithDesiredTrack(cmd string) bool {
if cmd == originCmd {
return true
}
status := buildStatusWithDesiredTrack(cmd)
deviceStatus := &DeviceStatus{Status: status}
body, err := json.Marshal(deviceStatus)
if err != nil {
log.Printf("Failed to marshal device status %v", deviceStatus)
return false
}
result := crdClient.Patch(utils.MergePatchType).Namespace(namespace).Resource(utils.ResourceTypeDevices).Name(deviceID).Body(body).Do(context.TODO())
if result.Error() != nil {
log.Printf("Failed to patch device status %v of device %v in namespace %v \n error:%+v", deviceStatus, deviceID, namespace, result.Error())
return false
} else {
log.Printf("Turn %s %s", cmd, deviceID)
}
originCmd = cmd
return true
}
根据注释可知UpdateDeviceTwinWithDesiredTrack是通过命令($ kubectl patch
)来更新DeviceTwin的期望状态(即WebController目前的开关状态)。
分析函数内部,传入的TrackID的值赋值到cmd中,如果cmd的值与Counter默认值(originCmd = "OFF"
)相同直接返回True(不需要更新DT期望状态)。注意:在Kubeedge源码中DeviceTwin常被简写为DT。
如果cmd与Counter默认值不同将执行DT期望状态更新。首先出现buildStatusWithDesiredTrack
...
// The twin value map
var statusMap = map[string]string{
"ON": "1",
"OFF": "0",
}
...
status := buildStatusWithDesiredTrack(cmd)
...
// cmd: 浏览器中输入的期望状态
func buildStatusWithDesiredTrack(cmd string) devices.DeviceStatus {
metadata := map[string]string{
"timestamp": strconv.FormatInt(time.Now().Unix()/1e6, 10),
"type": "string",
}
twins := []devices.Twin{{PropertyName: "status", Desired: devices.TwinProperty{Value: cmd, Metadata: metadata}, Reported: devices.TwinProperty{Value: statusMap[cmd], Metadata: metadata}}}
devicestatus := devices.DeviceStatus{Twins: twins}
return devicestatus
}
buildStatusWithDesiredTrack后主要包装Twin结构体,这里的Twin结构体在Kubeedge/cloud/pkg/apis/devices/v1alpha即Kubeedge Device部分提供的接口中。进入接口中如下:
...
// DeviceStatus reports the device state and the desired/reported values of twin attributes.
type DeviceStatus struct {
// A list of device twins containing desired/reported desired/reported values of twin properties..
// Optional: A passive device won't have twin properties and this list could be empty.
// +optional
Twins []Twin `json:"twins,omitempty"`
}
// Twin provides a logical representation of control properties (writable properties in the
// device model). The properties can have a Desired state and a Reported state. The cloud configures
// the `Desired`state of a device property and this configuration update is pushed to the edge node.
// The mapper sends a command to the device to change this property value as per the desired state .
// It receives the `Reported` state of the property once the previous operation is complete and sends
// the reported state to the cloud. Offline device interaction in the edge is possible via twin properties for control/command operations.
// Twin 提供可以向DeviceModel写入参数(Desired state-期望状态、Reported state-汇报状态)的代理。设备Property的期望状态和配置信息会推送给边端。
// Mapper会通过命令来修改设备的Property值,Mapper接受先前操作是否完成的报告并发送状态给云端。在边端的离线设备的活动可以通过Twin内命令控制操作实现。
type Twin struct {
// Required: The property name for which the desired/reported values are specified.
// This property should be present in the device model.
PropertyName string `json:"propertyName,omitempty"`
// Required: the desired property value.
Desired TwinProperty `json:"desired,omitempty"`
// Required: the reported property value.
Reported TwinProperty `json:"reported,omitempty"`
}
// TwinProperty represents the device property for which an Expected/Actual state can be defined.
// 可以理解为是一种属性设置格式
type TwinProperty struct {
// Required: The value for this property.
Value string `json:"value,"`
// Additional metadata like timestamp when the value was reported etc.
// +optional
Metadata map[string]string `json:"metadata,omitempty"`
}
...
综合Demo源码与KubeEdge Device Instance部分考虑。buildStatusWithDesiredTrack首先会建立元信息metadata,用于组成下面twins(包含Twin的各项属性设置:期望、反馈)。twins封装了PropertyName属性名、期望内容、反馈内容。Demo源码中PropertyName设置为status,期望值为cmd(解析浏览器输入后的数据),Metadata设置为一时间戳。报告内容Reported值为状态Map内Key为cmd的值"1"或"0",Metadata值同上。之后使用包装好的twins来创建DeviceStatus并返回给UpdateDeviceTwinWithDesiredTrack函数中的status中。
再返回到UpdateDeviceTwinWithDesiredTrack函数中继续分析。
// DeviceStatus is used to patch device status
type DeviceStatus struct {
Status devices.DeviceStatus `json:"status"`
}
// The device id of the counter
var deviceID = "counter"
// The default namespace in which the counter device instance resides
var namespace = "default"
...
// The CRD client used to patch the device instance.
var crdClient *rest.RESTClient
...
func UpdateDeviceTwinWithDesiredTrack(cmd string) bool {
if cmd == originCmd {
return true
}
status := buildStatusWithDesiredTrack(cmd)
deviceStatus := &DeviceStatus{Status: status}
body, err := json.Marshal(deviceStatus)
if err != nil {
log.Printf("Failed to marshal device status %v", deviceStatus)
return false
}
result := crdClient.Patch(utils.MergePatchType).Namespace(namespace).Resource(utils.ResourceTypeDevices).Name(deviceID).Body(body).Do(context.TODO())
if result.Error() != nil {
log.Printf("Failed to patch device status %v of device %v in namespace %v \n error:%+v", deviceStatus, deviceID, namespace, result.Error())
return false
} else {
log.Printf("Turn %s %s", cmd, deviceID)
}
originCmd = cmd
return true
}
在定义好status后,声明deviceStatus,使用status初始化。其中DeviceStatus结构体定义如上,内部包含一个数据类型为KubeEdge接口内DeviceStatus的成员,之后将deviceStatus结构转换为json字符串。
之后进行Patch资源更新。一开始进行一系列的设置:crdClient是Kubernetes提供的接口RESTful结构体。使用RESTful下Patch方法,在Utils下Constants中定义了Patch类型(Merge-patch + Json)与资源类型devices。Namespace为default,Name为counter。Patch Body为Json转换后的deviceStatus。最后Do进行执行,返回一个Result。Do是传给一个Context.TODO()。(空的Context)成功更新资源后再更新oringinCmd(可以理解为上次浏览器输入),UpdateDeviceTwinWithDesiredTrack函数结束。
总结UpdateDeviceTwinWithDesiredTrack函数的工作内容:以Merge-patch + Json的方式进行更新资源。最后更新originCmd。
所以现在ControlTrack处理器内部两种TrackID(ON、OFF)如何更新资源状态已经清晰,还剩下一种TrackID(STATUS),当浏览器发送的TrackID为STATUS时执行UpdateStatus函数。
戳入UpdateStatus函数内部
func UpdateStatus() map[string]string {
result := DeviceStatus{}
raw, _ := crdClient.Get().Namespace(namespace).Resource(utils.ResourceTypeDevices).Name(deviceID).DoRaw(context.TODO())
status := map[string]string{
"status": "OFF",
"value": "0",
}
_ = json.Unmarshal(raw, &result)
for _, twin := range result.Status.Twins {
status["status"] = twin.Desired.Value
status["value"] = twin.Reported.Value
}
return status
}
首先会初始化DeviceStatus,之后直接发起Get请求,其中会设置Namespace、Resource、Name,没有设置Body,所以需要使用Content中DoRaw方法。json.Unmarshal()
参数中第一个参数是JsonData被解析内容,第二个参数为解析结果。所以再初始化status后会将raw结果进行Json解析并储存在result中。
之后遍历result结构体内DeviceStatus内的Twins,将twin结构体内的Desired成员内的Value写入status内Key为status的值中,status map中Key为value的值写入twin内的Reported成员结构体内的Value的值。(这里直接看比较简洁)最后返回status。
总结UpdateStatus函数的工作内容:以Get的方式获取当前状态,将每个Twin内的Desired与Reported存储到status并返回
至此3个TrackID分析完毕,处理器内部最后还有一个响应部分controller.AjaxResponse(resultCode, status, nil)
这里调用了TrackController的AjaxResponse方法。
戳入AjaxResponse内部
// AjaxResponse returns a standard ajax response.
func (Controller *TrackController) AjaxResponse(resultCode int, resultString map[string]string, data interface{}) {
response := struct {
Result int
ResultString map[string]string
ResultObject interface{}
}{
Result: resultCode,
ResultString: resultString,
ResultObject: data,
}
Controller.Data["json"] = response
Controller.ServeJSON()
}
根据注释可知此方法返回一个标准的Ajax响应。此方法需要传入resultCode(对应每个TrackID),resultString(仅当TrackID为STATUS时,resultCode为3,有内容,否则空),data(为nil)。根据传入的参数初始化response后,将其赋值给处理器Data(Beego内部Controller结构体内负责处理ContextData中的Data成员)Key为json的值中。最后ServeJSON来发送已编码的Json格式进行响应。
至此Demo main入口函数第二行分析完毕。
接下来第三行beego.Run
来运行此Beego应用,监听8080端口。beego.Run
效果看似监听了8080端口,实际上内部已经进行了一系列的配置:
- 解析配置文件(解析conf目录下的配置文件app.conf,内部定义了监听的端口、是否开启session对话、应用名等信息)
- 执行用户的hookfunc,默认已经注册mime(通过函数
AddAPPStarHook
注册自己的启动函数) - 是否开启session(根据配置文件,如果开启session那么就会初始化全局session)
- 将views目录下的模版进行预编译,存入map中
- 是否启动文档功能(根据EnableDocs配置,决定是否开启内置文档路由功能)
- 是否启动管理模块(应用内监控模块,在8088端口进行内部监听,可通过这个端口查询QPS每秒查询数、CPU、内存、GC垃圾回收、goroutine、thread线程数等统计信息)
- 监听服务端口