基於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服務器架構如下圖所示,
我們可以看到,
- WebSocket服務器運行在Predix雲平臺上。
- 客戶端通過WebSocket協議與Predix雲平臺上的WebSocket服務器通信。
- 服務器端實現了
agent
,broker
和REST Interface
幾個核心組件。 - 服務器爲每個WebSocket連接創建一個
agent
實例。agent
實例一方面負責同WebSocket客戶端進行雙向通信,另一方面,agent
還接收broker
下發的命令並轉發到客戶端。 - 服務器創建一個
broker
實例。該broker
實例負責註冊每個agent
實例,並在接收到具體的命令後,轉發給相應的agent
實例。也就是說,我們通過broker
實例實現了傳遞命令到相應的WebSocket客戶端,而不是廣播給所有客戶端的功能。 REST Interface
實現了/agents
的GET
方法。因此,用戶可以通過瀏覽器發送相應的請求給broker
實例,而broker
實例則會轉發命令給相應的agent
實例。- 同時,
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的處理函數,
- 處理WebSocket客戶端通過
wss://<主機名>/register
註冊到服務器的請求 - 處理用戶通過瀏覽器訪問
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,並初始化一個實例
其次,我們需要實現broker
。broker
實例通過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客戶端,並將執行的結果反饋回雲端的全過程。
小結
通過本文我們瞭解到,
- Predix EdgeManager提供了雲端遠程控制邊緣節點的全生命週期解決方案,目前只對北美用戶開放。
- 基於Predix雲,通過WebSocket協議實現遠程控制邊緣節點是非常簡單的。
- 在用Go語言實現的版本中,我們大量的使用了Go語言內置的管道和goroutine,做到通過消息通信來共享內存,而不是通過共享內存來完成消息通信。
- 本示例只完成了
agent
註冊和雲端命令下發到客戶端的功能,並沒有實現WebSocket持久連接和agent
註銷功能,大家可以自行完成。
作者:謝品,上海創新坊首席架構師,GE數字集團
專注於工業互聯網,雲計算,大數據,高性能分佈式存儲領域,對Cloud Foundry和傳統應用向雲端,特別是向Predix遷移有豐富的經驗,曾供職於VMware,EMC,Autodesk等知名軟件公司雲計算部門。