基於Predix雲遠程控制邊緣節點的實踐

基於Predix雲遠程控制邊緣節點的實踐

在工業互聯網的核心場景中,除了使機器擁有感知,不斷上傳自身的運行數據外,通過雲端遠程控制邊緣節點則是另一個非常重要的場景。因爲,只有實現了從雲端到設備端反向通信和控制,才能將雲端經由大數據運算、優化過的推薦策略反饋到邊緣設備端,從而達到優化運營管理、提高設備使用率、降低設備的非計劃性停機時間等用戶期待的結果。

Predix EdgeManager - 雲端遠程控制邊緣節點的解決方案

Predix EdgeManager是Predix平臺中雲端遠程控制邊緣節點的全生命週期解決方案,它可以和Predix Machine完美集成,完成如下功能,

  • 用戶權限管理
  • 提供設備註冊與分組管理
  • 設備配置、算法的下發
  • 設備狀態監控,報警
  • 設備端命令執行
  • 設備端軟件依賴管理
  • 設備端軟件版本管理

Predix EdgeManager能提供全生命週期的解決方案,但是,目前只針對Predix北美用戶開放,中國Predix試用計劃的用戶還沒法申請、試用Predix EdgeManager的服務。

如果只需要從雲端遠程控制邊緣節點的功能,我們完全可以自己實現一個WebSocket服務器推送到Predix平臺,從雲端下發指令給設備端執行。本文就詳細介紹一下,如何基於Predix雲通過WebSocket協議遠程控制邊緣節點的實踐

WebSocker服務器軟件架構

實現一個WebSocket服務器一點都不困難,因爲各種高級語言都有相應的開發框架,我們可以在幾分鐘內就能完成。真正需要注意的是,當WebSocket服務器已經連接了多個客戶端的場景下,如何傳遞命令到相應的WebSocket客戶端,而不是廣播給所有的客戶端。下圖就是我們的一個示例,用Go語言實現的WebSocket服務器。

WebSocket服務器架構如下圖所示,


我們可以看到,

  1. WebSocket服務器運行在Predix雲平臺上。
  2. 客戶端通過WebSocket協議與Predix雲平臺上的WebSocket服務器通信。
  3. 服務器端實現了agentbrokerREST Interface幾個核心組件。
  4. 服務器爲每個WebSocket連接創建一個agent實例。agent實例一方面負責同WebSocket客戶端進行雙向通信,另一方面,agent還接收broker下發的命令並轉發到客戶端。
  5. 服務器創建一個broker實例。該broker實例負責註冊每個agent實例,並在接收到具體的命令後,轉發給相應的agent實例。也就是說,我們通過broker實例實現了傳遞命令到相應的WebSocket客戶端,而不是廣播給所有客戶端的功能。
  6. REST Interface實現了/agentsGET方法。因此,用戶可以通過瀏覽器發送相應的請求給broker實例,而broker實例則會轉發命令給相應的agent實例。
  7. 同時,REST Interface會爲每個請求創建一個callback管道,agent實例將客戶端執行命令的結果通過該callback管道返回給REST Interface。因此,用戶在瀏覽器中就可以看到邊緣節點執行命令的結果。

Go語言代碼實例

介紹完架構設計,讓我們看看代碼實現。

下載代碼示例

git clone https://github.com/pxie/go.in.practices.git
cd go.in.practices/websocket/

實現WebSocket服務器

首先,我們需要實現一個運行在Predix平臺的WebSocket服務器,而且實現兩個URL的處理函數,

  1. 處理WebSocket客戶端通過wss://<主機名>/register註冊到服務器的請求
  2. 處理用戶通過瀏覽器訪問https://<主機名>/agents獲取客戶端系統信息的請求

注:嚴格意義上說,我們實現的服務器同時提供WebSocket接口和RESTful接口,不只是WebSocket服務器。

// server.go
func main() {
	...
	http.HandleFunc("/", home)

	http.HandleFunc("/agents", func(w http.ResponseWriter, r *http.Request) {
		agentsHandler(broker, w, r)
	})

	// create agent, and register it to broker
	http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
		serveWs(broker, w, r)
	})

實現broker,並初始化一個實例

其次,我們需要實現brokerbroker實例通過register管道接收新創建的agent實例,接着存儲到conns鍵值對中完成agent註冊功能。在WebSocket服務器的初始化過程中,通過goroutine來運行一個broker的實例。在本例中,我們使用Go語言的內部類型map存儲所有的鍵值對。如果要實現分佈式無狀態的橫向擴展,我們可以非常方便的將agent實例的信息存儲到外部存儲服務中,例如Predix雲平臺的Redis服務。

// broker.go
type broker struct {
	// register channel to add new websocket connect to broker
	register chan map[string]*agent

	// channel to get message from http server to broker
	httpCh chan *message

	// record all websocket conns
	conns map[string]*agent
}

// server.go
func main() {
	broker := newBroker()
	go broker.start()

實現agent

再次,我們需要實現agent。每當有新的WebSocket客戶端註冊到服務器端,就會創建一個agent實例。接着,agent實例會通過register管道把自己註冊到broker中。最後,agent會啓動一個goroutine接收broker下發的命令,並完成與WebSocket客戶端的交互。

// agent.go
type agent struct {
	// broker to know every agent
	b *broker

	// connect to remote client via websocket
	conn *websocket.Conn

	// channel get message from broker
	agentCh chan *message
}

func serveWs(b *broker, w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)

	ag := &agent{b: b, conn: conn, agentCh: make(chan *message, 256)}
	agentID := uuid.NewV4().String()

	// build map to register agent
	reg := make(map[string]*agent)
	reg[agentID] = ag
	b.register <- reg

	// one goroutine to handle communication between agent to remote client, and broker
	go ag.start()
}

實現WebSocket客戶端

最後,我們需要實現WebSocket客戶端,它首先完成向WebSocket服務器的註冊,然後就不斷監聽WebSocket服務器下發的消息,並根據消息的內容,執行相應的處理函數,最終將處理結果通過WebSocket連接返回給WebSocket服務器。

// client/client.go
func main() {
	addr := "websocket-server.run.aws-jp01-pr.ice.predix.io"
	u := url.URL{Scheme: "wss", Host: addr, Path: "/register"}

	log.Printf("connect to %s", u.String())
	conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
	if err != nil {
		log.Fatal("connect to websocker server error.", err)
	}
	defer conn.Close()

	ticker := time.NewTicker(10 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			_, msg, err := conn.ReadMessage()
			if err != nil {
				log.Fatalln("read message from websocket error.", err)
			}
			exec(string(msg), conn)
		}
	}
}

推送到Predix雲平臺

和推送其他應用一樣,我們用cf push就可以完成WebSocket服務器的推送。

推送完成的結果如下所示,

requested state: started
instances: 1/1
usage: 170M x 1 instances
urls: websocket-server.run.aws-jp01-pr.ice.predix.io   <-- 推送app後獲得的URL
last uploaded: Tue Sep 26 04:20:50 UTC 2017
stack: cflinuxfs2
buildpack: https://github.com/cloudfoundry/go-buildpack.git

     state     since                    cpu    memory         disk           details
#0   running   2017-09-26 12:21:52 PM   0.0%   5.5M of 170M   7.7M of 350M

獲取當前所有已註冊的agent信息

我們可以通過瀏覽器訪問https://websocket-server.run.aws-jp01-pr.ice.predix.io/agents來獲取當前已註冊的agent信息。和預期的一樣,因爲沒有任何WebSocket客戶端連接到服務器端,所以返回的結果是沒有agent已經註冊。

註冊WebSocket客戶端

因爲,每個人推送到Predix雲平臺上WebSocket服務器的URL都是不一樣的。因此,我們需要修改客戶端的代碼,指向正確的地址並重新編譯。

// client/client.go
func main() {
	// 修改爲正確的地址
	addr := "websocket-server.run.aws-jp01-pr.ice.predix.io"

}

// 保存client.go文件
// 運行go build命令完成編譯

當編譯完成後,我們就可以運行client/路徑下的./client可執行文件,將agent註冊到broker

這時,我們再次訪問/agents頁面就會得到agent相關的信息。

[{"AgentID":"298ee2a5-d43b-426c-b42c-04c14a6aae41","RemoteAddr":"10.120.3.10:8446"}]

查詢WebSocket客戶端的系統信息

最後,我們可以通過瀏覽器訪問/agents?id=<AgentID>來查詢WebSocket客戶端的系統信息,包括執行WebSocket客戶端的操作系統,CPU信息和主機信息。通過這個請求,我們從瀏覽器發送命令到指定的agent實例,然後agent將命令轉發給WebSocket客戶端,客戶端執行相應的命令,並把命令執行的結果返回給瀏覽器展示。

> GET /agents?id=bc93384d-e46b-4508-ab7e-18ec55d9652d HTTP/1.1
> Host: websocket-server.run.aws-jp01-pr.ice.predix.io
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 1073
< Content-Type: text/plain; charset=utf-8
< Date: Tue, 26 Sep 2017 04:57:14 GMT
< X-Vcap-Request-Id: 9cba4b18-e4ba-42fb-5cea-afca45385fd0
<
OS: darwin
CPU: [{"cpu":0,"vendorId":"GenuineIntel","family":"6","model":"70","stepping":1,"physicalId":"","coreId":"","cores":4,"modelName":"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz","mhz":2500,"cacheSize":256,"flags":["fpu","vme","de","pse","tsc","msr","pae","mce","cx8","apic","sep","mtrr","pge","mca","cmov","pat","pse36","clfsh","ds","acpi","mmx","fxsr","sse","sse2","ss","htt","tm","pbe","sse3","pclmulqdq","dtes64","mon","dscpl","vmx","smx","est","tm2","ssse3","fma","cx16","tpr","pdcm","sse4.1","sse4.2","x2apic","movbe","popcnt","aes","pcid","xsave","osxsave","seglim64","tsctmr","avx1.0","rdrand","f16c","smep","erms","rdwrfsgs","tsc_thread_offset","bmi1","avx2","bmi2","invpcid","fpu_csds","syscall","xd","1gbpage","em64t","lahf","lzcnt","rdtscp","tsci"],"microcode":""}]
host: {"hostname":"C02S8EWGG8WP","uptime":14451,"bootTime":1506387382,"procs":308,"os":"darwin","platform":"darwin","platformFamily":"","platformVersion":"10.11.6","kernelVersion":"15.6.0","virtualizationSystem":"","virtualizationRole":"","hostid":"b370db23-7244-3441-b95d-384442442565"}

注:/agents?id=<AgentID>獲取的是WebSocket客戶端的系統信息,而不是Predix雲平臺上agent實例的系統信息。

通過這個示例,我們完整演示瞭如何從雲端傳遞命令到相應的WebSocket客戶端,並將執行的結果反饋回雲端的全過程。

小結

通過本文我們瞭解到,

  1. Predix EdgeManager提供了雲端遠程控制邊緣節點的全生命週期解決方案,目前只對北美用戶開放。
  2. 基於Predix雲,通過WebSocket協議實現遠程控制邊緣節點是非常簡單的。
  3. 在用Go語言實現的版本中,我們大量的使用了Go語言內置的管道和goroutine,做到通過消息通信來共享內存,而不是通過共享內存來完成消息通信。
  4. 本示例只完成了agent註冊和雲端命令下發到客戶端的功能,並沒有實現WebSocket持久連接和agent註銷功能,大家可以自行完成。

作者:謝品,上海創新坊首席架構師,GE數字集團

專注於工業互聯網,雲計算,大數據,高性能分佈式存儲領域,對Cloud Foundry和傳統應用向雲端,特別是向Predix遷移有豐富的經驗,曾供職於VMware,EMC,Autodesk等知名軟件公司雲計算部門。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章