通过gNMI来订阅服务器内存使用率信息,订阅形式为sample,每10秒向客户端推送一次,1分钟后自动关闭推送通道
gNMI和gRPC的关系
- gRPC 是一个高性能、开源的远程过程调用(RPC)框架,它使用 Protocol Buffers(protobuf)作为接口定义语言(IDL)。
- gNMI(gRPC Network Management Interface)是基于 gRPC 的网络管理接口
- gNMI 是使用 gRPC 进行网络管理的一种具体实现。它定义了一组消息和服务,用于网络设备和网络管理系统之间的交互。
- gNMI 提供了一种标准化的方式来配置、查询和订阅网络设备的状态和数据。
- gNMI定义了自己的proto,用户也可以按照自己的需求做扩展
前期准备
- 创建文件夹并初始化项目
admin@hpc-1:~/go$ mkdir my_gNMI
admin@hpc-1:~/go$ cd my_gNMI
admin@hpc-1:~/go/my_gNMI$ go mod init my_gNMI
- 下载需要的包
admin@hpc-1:~/go/my_gNMI$ go get google.golang.org/grpc
admin@hpc-1:~/go/my_gNMI$ go get github.com/shirou/gopsutil/mem
admin@hpc-1:~/go/my_gNMI$ go get github.com/openconfig/gnmi/proto/gnmi
- proto不必另外写了,就利用gnmi现有的proto,注意到定义了四种rpc,后续这四种rpc都
必须定义
service gNMI {
// Capabilities allows the client to retrieve the set of capabilities that
// is supported by the target. This allows the target to validate the
// service version that is implemented and retrieve the set of models that
// the target supports. The models can then be specified in subsequent RPCs
// to restrict the set of data that is utilized.
// Reference: gNMI Specification Section 3.2
rpc Capabilities(CapabilityRequest) returns (CapabilityResponse);
// Retrieve a snapshot of data from the target. A Get RPC requests that the
// target snapshots a subset of the data tree as specified by the paths
// included in the message and serializes this to be returned to the
// client using the specified encoding.
// Reference: gNMI Specification Section 3.3
rpc Get(GetRequest) returns (GetResponse);
// Set allows the client to modify the state of data on the target. The
// paths to modified along with the new values that the client wishes
// to set the value to.
// Reference: gNMI Specification Section 3.4
rpc Set(SetRequest) returns (SetResponse);
// Subscribe allows a client to request the target to send it values
// of particular paths within the data tree. These values may be streamed
// at a particular cadence (STREAM), sent one off on a long-lived channel
// (POLL), or sent as a one-off retrieval (ONCE).
// Reference: gNMI Specification Section 3.5
rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeResponse);
}
实现
- 定义获取内存使用率的path:/system/memory
- server的源码
centec@hpc-1:~/go/my_gNMI/server$
package main
import (
"fmt"
"log"
"net"
"time"
"context"
"github.com/shirou/gopsutil/mem"
"github.com/openconfig/gnmi/proto/gnmi"
"google.golang.org/grpc"
)
type server struct{}
//获取内存使用率的函数
func get_memory_util() (float64, error) {
memory, err := mem.VirtualMemory()
if err != nil {
fmt.Println("Error:", err)
return 0,err
}
usagePercent := memory.UsedPercent
return usagePercent,nil
}
func (s *server) Capabilities(context.Context, *gnmi.CapabilityRequest) (*gnmi.CapabilityResponse, error) {
// 返回服务器支持的 CapabilityResponse,只占位,具体实现先空着
return &gnmi.CapabilityResponse{}, nil
}
func (s *server) Get(context.Context, *gnmi.GetRequest) (*gnmi.GetResponse, error) {
// 返回服务器的 GetResponse,只占位,具体实现先空着
return &gnmi.GetResponse{}, nil
}
func (s *server) Set(context.Context, *gnmi.SetRequest) (*gnmi.SetResponse, error) {
// 处理 SetRequest 并返回 SetResponse,只占位,具体实现先空着
return &gnmi.SetResponse{}, nil
}
func (s *server) Subscribe(stream gnmi.GNMI_SubscribeServer) error {
// 记录客户端是否取消订阅
ctx := stream.Context()
done := ctx.Done()
for {
req, err := stream.Recv()
if err != nil {
return err
}
if req.GetSubscribe() != nil {
// 处理订阅请求
go handleSubscription(stream, done, req.GetSubscribe())
}
}
}
func handleSubscription(stream gnmi.GNMI_SubscribeServer, done <-chan struct{}, sub *gnmi.SubscriptionList) {
var memoryUsage float64
var err error
// 获取采样间隔
sampleInterval := sub.GetSubscription()[0].GetSampleInterval()
for {
select {
case <-done:
return
default:
// 构建更新消息
memoryUsage, err = get_memory_util()
if err != nil {
fmt.Println("Get memory util failure:", err)
return
}
update := &gnmi.SubscribeResponse_Update{
//下面":"前面都在gnmi proto生成的代码中定义过的
Update: &gnmi.Notification{
Timestamp: time.Now().UnixNano(),
Update: []*gnmi.Update{
{
Path: &gnmi.Path{
Elem: []*gnmi.PathElem{
{
Name: "system",
},
{
Name: "memory",
},
},
},
Val: &gnmi.TypedValue{
Value: &gnmi.TypedValue_DoubleVal{
DoubleVal: memoryUsage,
},
},
},
},
},
}
// 发送更新消息给客户端
err := stream.Send(&gnmi.SubscribeResponse{
Response: update,
})
if err != nil {
log.Println("Failed to send update:", err)
return
}
fmt.Println(sampleInterval)
// 等待采样间隔
<-time.After(time.Duration(sampleInterval) * time.Second)
}
}
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
gnmi.RegisterGNMIServer(s, &server{})
log.Println("Server running on port 50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
centec@hpc-1:~/go/my_gNMI/server$
- client源码
centec@hpc-1:~/go/my_gNMI/client$ cat client.go
package main
import (
"context"
"log"
"time"
"github.com/openconfig/gnmi/proto/gnmi"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
c := gnmi.NewGNMIClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Hour)
defer cancel()
// 设置订阅参数
subscribe := &gnmi.SubscriptionList{
Subscription: []*gnmi.Subscription{
{
Path: &gnmi.Path{
Elem: []*gnmi.PathElem{
{
Name: "system",
},
{
Name: "memory",
},
},
},
Mode: gnmi.SubscriptionMode_SAMPLE,
SampleInterval: 10, // 设置采样间隔为 10 秒
},
},
Mode: gnmi.SubscriptionList_STREAM,
}
// 创建订阅请求
subReq := &gnmi.SubscribeRequest{
Request: &gnmi.SubscribeRequest_Subscribe{
Subscribe: subscribe,
},
}
stream, err := c.Subscribe(ctx)
if err != nil {
log.Fatalf("Failed to subscribe: %v", err)
}
err = stream.Send(subReq)
if err != nil {
log.Fatalf("Failed to send subscribe request: %v", err)
}
// 启动一个go routine,等待 1 分钟后cancel context
go func() {
<-time.After(1 * time.Minute)
cancel()
log.Println("Unsubscribed")
}()
log.Println("Subscribed")
// 接收服务器发送的更新消息
for {
res, err := stream.Recv()
if err != nil {
log.Fatalf("Failed to receive update: %v", err)
}
update := res.GetUpdate()
if update != nil {
// 处理接收到的更新消息
log.Printf("Received update: %+v", update)
}
}
}
centec@hpc-1:~/go/my_gNMI/client$
验证
- 启动server,保持一直运行
centec@hpc-1:~/go/my_gNMI/server$ go run server.go
2024/04/25 17:19:02 Server running on port 50051
- 另开一个终端,启动client,一分钟后自动cancel
centec@hpc-1:~/go/my_gNMI/client$ go run client.go
2024/04/25 17:20:00 Subscribed
2024/04/25 17:20:00 Received update: timestamp:1714036800991972077 update:{path:{elem:{name:"system"} elem:{name:"memory"}} val:{double_val:25.807159341843345}}
2024/04/25 17:20:11 Received update: timestamp:1714036811001969660 update:{path:{elem:{name:"system"} elem:{name:"memory"}} val:{double_val:25.865417470678985}}
2024/04/25 17:20:21 Received update: timestamp:1714036821011881474 update:{path:{elem:{name:"system"} elem:{name:"memory"}} val:{double_val:25.864675080036548}}
2024/04/25 17:20:31 Received update: timestamp:1714036831021878936 update:{path:{elem:{name:"system"} elem:{name:"memory"}} val:{double_val:25.865183031528744}}
2024/04/25 17:20:41 Received update: timestamp:1714036841022197657 update:{path:{elem:{name:"system"} elem:{name:"memory"}} val:{double_val:25.863620103860452}}
2024/04/25 17:20:51 Received update: timestamp:1714036851031841235 update:{path:{elem:{name:"system"} elem:{name:"memory"}} val:{double_val:25.86430388471533}}
2024/04/25 17:21:01 Unsubscribed
2024/04/25 17:21:01 Failed to receive update: rpc error: code = Canceled desc = context canceled
exit status 1
centec@hpc-1:~/go/my_gNMI/client$