2024-05-29 14:47:39 +08:00
2024-05-08 16:35:49 +08:00
2024-05-30 00:57:25 +08:00
2024-05-29 14:47:39 +08:00
2024-03-06 21:03:22 +08:00
2024-04-10 15:10:07 +08:00
2023-12-10 20:07:34 +08:00
2024-05-29 14:47:39 +08:00
2024-02-23 20:11:59 +08:00

Frontier是一个go开发的全双工开源长连接网关旨在让微服务直达边缘节点或客户端反之边缘节点或客户端也同样直达微服务。对于两者提供了全双工的单双向RPC调用消息发布和接收以及点对点流的功能。Frontier符合云原生架构可以使用Operator快速部署一个集群具有高可用和弹性轻松支撑百万边缘节点或客户端在线的需求。

特性

  • RPC 微服务和边缘可以Call对方的函数提前注册并且在微服务侧支持负载均衡
  • 消息 微服务和边缘可以Publish对方的Topic边缘可以Publish到外部MQ的Topic微服务侧支持负载均衡
  • 多路复用/流 微服务可以直接在边缘节点打开一个流(连接),可以封装例如文件上传、代理等,天堑变通途
  • 上线离线控制 微服务可以注册边缘节点获取ID、上线离线回调当这些事件发生Frontier会调用这些函数
  • API简单 在项目api目录下分别对边缘和微服务提供了封装好的sdk可以非常简单的基于这个sdk做开发
  • 部署简单 支持多种部署方式(docker docker-compose helm以及operator)来部署Frontier实例或集群
  • 水平扩展 提供了Frontiter和Frontlas集群在单实例性能达到瓶颈下可以水平扩展Frontier实例或集群
  • 高可用 支持集群部署支持微服务和边缘节点永久重连sdk在当前实例宕机情况时切换新可用实例继续服务
  • 支持控制面 提供了gRPC和rest接口允许运维人员对微服务和边缘节点查询或删除删除即踢除目标下线

架构

组件Frontier

  • Service End:微服务侧的功能入口,默认连接
  • Edge End:边缘节点或客户端侧的功能入口
  • Publish/Receive:发布和接收消息
  • Call/Register:调用和注册函数
  • OpenStream/AcceptStream:打开和接收点到点流(连接)
  • 外部MQFrontier支持将从边缘节点Publish的消息根据配置的Topic转发到外部MQ

Frontier需要微服务和边缘节点两方都主动连接到FrontierService和Edge的元信息接收TopicRPCService名等可以在连接的时候携带过来。连接的默认端口是

  • :30011 提供给微服务连接获取Service
  • :30012 提供给边缘节点连接获取Edge
  • :30010 提供给运维人员或者程序使用的控制面

功能

功能 发起方 接收方 方法 路由方式 描述
Messager Service Edge Publish EdgeID+Topic 必须Publish到具体的EdgeID默认Topic为空Edge调用Receive接收接收处理完成后必须调用msg.Done()或msg.Error(err)保障消息一致性
Edge Service或外部MQ Publish Topic 必须Publish到Topic由Frontier根据Topic选择某个Service或MQ
RPCer Service Edge Call EdgeID+Method 必须Call到具体的EdgeID需要携带Method
Edge Service Call Method 必须Call到Method由Frontier根据Method选择某个的Service
Multiplexer Service Edge OpenStream EdgeID 必须OpenStream到具体的EdgeID
Edge Service OpenStream ServiceName 必须OpenStream到ServiceName该ServiceName由Service初始化时携带的service.OptionServiceName指定

主要遵守以下设计原则:

  1. 所有的消息、RPC和Stream都是点到点的传递
    • 从微服务到边缘一定要指定边缘节点ID
    • 从边缘到微服务Frontier根据Topic和Method路由最终哈希选择一个微服务或外部MQ默认根据edgeid哈希,你也可以选择randomsrcip
  2. 消息需要接收方明确结束
    • 为了保障消息的传达语义接收方一定需要msg.Done()或msg.Error(err),保障传达一致性
  3. Multiplexer打开的流在逻辑上是微服务与边缘节点的直接通信
    • 对方接收到流后所有在这个流上功能都会直达对方不会经过Frontierd的路由策略

使用

示例

目录examples/chatroom下有简单的聊天室示例仅100行代码实现一个的聊天室功能可以通过

make examples

在bin目录下得到chatroom_servicechatroom_egent可执行程序,运行示例:

https://github.com/singchia/frontier/assets/15531166/18b01d96-e30b-450f-9610-917d65259c30

在这个示例你可以看到上线离线通知消息Publish等功能。

微服务如何使用

微服务侧获取Service

package main

import (
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/service"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30011")
	}
	svc, err := service.NewService(dialer)
	// 开始使用service
}

微服务接收获取ID、上线/离线通知

package main

import (
	"context"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/service"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30011")
	}
	svc, _ := service.NewService(dialer)
	svc.RegisterGetEdgeID(context.TODO(), getID)
	svc.RegisterEdgeOnline(context.TODO(), online)
	svc.RegisterEdgeOffline(context.TODO(), offline)
}

// service可以根据meta分配id给edge
func getID(meta []byte) (uint64, error) {
	return 0, nil
}

// edge上线
func online(edgeID uint64, meta []byte, addr net.Addr) error {
	return nil
}

// edge离线
func offline(edgeID uint64, meta []byte, addr net.Addr) error {
	return nil
}

微服务发布消息到边缘节点

前提需要该Edge在线否则会找不到Edge

package main

import (
	"context"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/service"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30011")
	}
	svc, _ := service.NewService(dialer)
	msg := svc.NewMessage([]byte("test"))
	// 发布一条消息到ID为1001的边缘节点
	err := svc.Publish(context.TODO(), 1001, msg)
	// ...
}

微服务声明接收Topic

package main

import (
	"context"
	"fmt"
	"io"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/service"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30011")
	}
	// 在获取svc时声明需要接收的topic
	svc, _ := service.NewService(dialer, service.OptionServiceReceiveTopics([]string{"foo"}))
	for {
		// 接收消息
		msg, err := svc.Receive(context.TODO())
		if err == io.EOF {
			// 收到EOF表示svc生命周期已终结不可以再使用
			return
		}
		if err != nil {
			fmt.Println("receive err:", err)
			continue
		}
		// 处理完msg后需要通知调用方已完成
		msg.Done()
	}
}

微服务调用边缘节点的RPC

package main

import (
	"context"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/service"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30011")
	}
	svc, _ := service.NewService(dialer)
	req := svc.NewRequest([]byte("test"))
	// 调用ID为1001边缘节点的foo方法前提是边缘节点需要预注册该方法
	rsp, err := svc.Call(context.TODO(), 1001, "foo", req)
	// ...
}

微服务注册方法以供边缘节点调用

package main

import (
	"context"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/service"
	"github.com/singchia/geminio"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30011")
	}
	svc, _ := service.NewService(dialer)
	// 注册一个"echo"方法
	svc.Register(context.TODO(), "echo", echo)
	// ...
}

func echo(ctx context.Context, req geminio.Request, rsp geminio.Response) {
	value := req.Data()
	rsp.SetData(value)
}

微服务打开边缘节点的点到点流

package main

import (
	"context"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/service"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30011")
	}
	svc, _ := service.NewService(dialer)
	// 打开ID为1001边缘节点的新流同时st也是一个net.Conn前提是edge需要AcceptStream接收该流
	st, err := svc.OpenStream(context.TODO(), 1001)
}

基于这个新打开流,你可以用来传递文件、代理流量等。

微服务接收流

package main

import (
	"fmt"
	"io"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/service"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30011")
	}
	// 在获取svc时声明需要微服务名在边缘打开流时需要指定该微服务名
	svc, _ := service.NewService(dialer, service.OptionServiceName("service-name"))
	for {
		st, err := svc.AcceptStream()
		if err == io.EOF {
			// 收到EOF表示svc生命周期已终结不可以再使用
			return
		} else if err != nil {
			fmt.Println("accept stream err:", err)
			continue
		}
		// 使用stream这个stream同时是个net.Conn你可以Read/Write/Close同时也可以RPC和消息
	}
}

基于这个新打开流,你可以用来传递文件、代理流量等。

消息、RPC和流一起来吧

package main

import (
	"context"
	"fmt"
	"io"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/service"
	"github.com/singchia/geminio"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30011")
	}
	// 在获取svc时声明需要微服务名在边缘打开流时需要指定该微服务名
	svc, _ := service.NewService(dialer, service.OptionServiceName("service-name"))

	// 接收流
	go func() {
		for {
			st, err := svc.AcceptStream()
			if err == io.EOF {
				// 收到EOF表示svc生命周期已终结不可以再使用
				return
			} else if err != nil {
				fmt.Println("accept stream err:", err)
				continue
			}
			// 使用stream这个stream同时是个net.Conn你可以Read/Write/Close同时也可以RPC和消息
		}
	}()

	// 注册一个"echo"方法
	svc.Register(context.TODO(), "echo", echo)

	// 接收消息
	for {
		msg, err := svc.Receive(context.TODO())
		if err == io.EOF {
			// 收到EOF表示svc生命周期已终结不可以再使用
			return
		}
		if err != nil {
			fmt.Println("receive err:", err)
			continue
		}
		// 处理完msg后需要通知调用方已完成
		msg.Done()
	}
}

func echo(ctx context.Context, req geminio.Request, rsp geminio.Response) {
	value := req.Data()
	rsp.SetData(value)
}

边缘节点/客户端如何使用

边缘节点侧获取Edge

package main

import (
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/edge"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30012")
	}
	eg, _ := edge.NewEdge(dialer)
	// 开始使用eg ...
}

边缘节点发布消息到Topic

Service需要提前声明接收该Topic或者在配置文件中配置外部MQ。

package main

import (
	"context"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/edge"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30012")
	}
	eg, _ := edge.NewEdge(dialer)
	// 开始使用eg
	msg := eg.NewMessage([]byte("test"))
	err := eg.Publish(context.TODO(), "foo", msg)
	// ...
}

边缘节点接收消息

package main

import (
	"context"
	"fmt"
	"io"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/edge"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30012")
	}
	eg, _ := edge.NewEdge(dialer)
	for {
		// 接收消息
		msg, err := eg.Receive(context.TODO())
		if err == io.EOF {
			// 收到EOF表示eg生命周期已终结不可以再使用
			return
		}
		if err != nil {
			fmt.Println("receive err:", err)
			continue
		}
		// 处理完msg后需要通知调用方已完成
		msg.Done()
	}
	// ...
}

边缘节点调用微服务RPC

package main

import (
	"context"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/edge"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30012")
	}
	eg, _ := edge.NewEdge(dialer)
	// 开始使用eg
	req := eg.NewRequest([]byte("test"))
	// 调用echo方法Frontier会查找并转发请求到相应的微服务
	rsp, err := eg.Call(context.TODO(), "echo", req)
}

边缘节点注册RPC

package main

import (
	"context"
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/edge"
	"github.com/singchia/geminio"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30012")
	}
	eg, _ := edge.NewEdge(dialer)
	// 注册一个"echo"方法
	eg.Register(context.TODO(), "echo", echo)
	// ...
}

func echo(ctx context.Context, req geminio.Request, rsp geminio.Response) {
	value := req.Data()
	rsp.SetData(value)
}

边缘节点打开微服务的点到点流

package main

import (
	"net"
	"github.com/singchia/frontier/api/dataplane/v1/edge"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30012")
	}
	eg, _ := edge.NewEdge(dialer)
	st, err := eg.OpenStream("service-name")
	// ...
}

基于这个新打开流,你可以用来传递文件、代理流量等。

边缘节点接收流

package main

import (
	"net"
	"fmt"
	"io"
	"github.com/singchia/frontier/api/dataplane/v1/edge"
)

func main() {
	dialer := func() (net.Conn, error) {
		return net.Dial("tcp", "127.0.0.1:30012")
	}
	eg, _ := edge.NewEdge(dialer)
	for {
		stream, err := eg.AcceptStream()
		if err == io.EOF {
			// 收到EOF表示eg生命周期已终结不可以再使用
			return
		} else if err != nil {
			fmt.Println("accept stream err:", err)
			continue
		}
		// 使用stream这个stream同时是个net.Conn你可以Read/Write/Close同时也可以RPC和消息
	}
}

错误处理

错误 描述和处理
io.EOF 收到EOF表示流或连接已关闭需要退出Receive、AcceptStream等操作
io.ErrShortBuffer 发送端或者接收端的Buffer已满可以设置OptionServiceBufferSize或OptionEdgeBufferSize来调整
apis.ErrEdgeNotOnline 表示该边缘节点不在线,需要检查边缘连接
apis.ServiceNotOnline 表示微服务不在线,需要检查微服务连接信息或连接
apis.RPCNotOnline 表示Call的RPC不在线
apis.TopicOnline 表示Publish的Topic不在线
其他错误 还存在Timeout、BufferFull等

需要注意的是如果关闭流在流上正在阻塞的方法都会立即得到io.EOF如果关闭入口Service和Edge则所有在此之上的流阻塞的方法都会立即得到io.EOF。

控制面

Frontier控制面提供gRPC和Rest接口运维人员可以使用这些api来确定本实例的连接情况gRPC和Rest都由默认端口:30010提供服务。

GRPC 详见Protobuf定义

service ControlPlane {
    rpc ListEdges(ListEdgesRequest) returns (ListEdgesResponse);
    rpc GetEdge(GetEdgeRequest) returns (Edge);
    rpc KickEdge(KickEdgeRequest) returns (KickEdgeResponse);
    rpc ListEdgeRPCs(ListEdgeRPCsRequest) returns (ListEdgeRPCsResponse);
    rpc ListServices(ListServicesRequest) returns (ListServicesResponse);
    rpc GetService(GetServiceRequest) returns (Service);
    rpc KickService(KickServiceRequest) returns (KickServiceResponse);
    rpc ListServiceRPCs(ListServiceRPCsRequest) returns (ListServiceRPCsResponse);
    rpc ListServiceTopics(ListServiceTopicsRequest) returns (ListServiceTopicsResponse);
}

REST Swagger详见Swagger定义

例如你可以使用下面请求来踢除某个边缘节点下线:

curl -X DELETE http://127.0.0.1:30010/v1/edges/{edge_id} 

或查看某个微服务注册了哪些RPC

curl -X GET http://127.0.0.1:30010/v1/services/rpcs?service_id={service_id}

注意gRPC/Rest依赖dao backend有两个选项buntdbsqlite都是使用的in-memory模式为性能考虑默认backend使用buntdb并且列表接口返回字段count永远是-1当你配置backend为sqlite3时会认为你对在Frontier上连接的微服务和边缘节点有强烈的OLTP需求例如在Frontier上封装web此时count才会返回总数。

Frontier配置

如果需要更近一步定制你的Frontier实例可以在这一节了解各个配置是如何工作的。

最小化配置

简单起,你可以仅配置面向微服务和边缘节点的服务监听地址:

servicebound:
  listen:
    network: tcp
    addr: 0.0.0.0:30011
edgebound:
  listen:
    network: tcp
    addr: 0.0.0.0:30012
  edgeid_alloc_when_no_idservice_on: true

TLS

tls:
  enable: false
  mtls: false
  ca_certs:
  - ca1.cert
  - ca2.cert
  certs:
  - cert: edgebound.cert
    key: edgebound.key
  insecure_skip_verify: false

Frontier部署

在单Frontier实例下可以根据环境选择以下方式部署你的Frontier实例。

docker

docker run -d --name frontier -p 30011:30011 -p 30012:30012 singchia/frontier:1.0.0

docker-compose

git clone https://github.com/singchia/frontier.git
cd dist/compose
docker-compose up -d frontier

helm

如果你是在k8s环境下可以使用helm快速部署一个实例

git clone https://github.com/singchia/frontier.git
cd dist/helm
helm install frontier ./ -f values.yaml

集群

Frontier + Frontlas架构

新增Frontlas组件用于构建集群Frontlas同样也是无状态组件并不在内存里留存其他信息因此需要额外依赖Redis你需要提供一个Redis连接信息给到Frontlas支持 redis sentinelredis-cluster

  • Frontier:微服务和边缘数据面通信组件
  • Frontlas集群管理组件将微服务和边缘的元信息、活跃信息记录在Redis里

Frontier需要主动连接Frontlas以上报自己、微服务和边缘的活跃和状态默认Frontlas的端口是

  • :40011 提供给微服务连接代替微服务在单Frontier实例下连接的30011端口
  • :40012 提供给Frontier连接上报状态

使用

分布式

当部署多个Frontier实例时跨实例的微服务和边缘节点寻址一定需要分布式存储如果没有Frontlas这部分的存储工作

高可用

水平扩展

k8s

Operator

开发

路线图

详见 ROADMAP

贡献

如果你发现任何Bug请提出Issue项目Maintainers会及时响应相关问题。

如果你希望能够提交Feature更快速解决项目问题满足以下简单条件下欢迎提交PR

  • 代码风格保持一致
  • 每次提交一个Feature
  • 提交的代码都携带单元测试

测试

流功能测试

许可证

Released under the Apache License 2.0

Description
The first open source cloud-native tcp gateway for edges. 首个开源云原生tcp/udp长连接网关,支持rpc、消息和流,微服务和边缘节点/客户端互相直达!完美匹配mesh vpn、内网穿透、边缘管理、iot网关等场景
Readme Apache-2.0 15 MiB
Languages
Go 95.8%
Makefile 3.2%
Lua 0.6%
Dockerfile 0.3%
Shell 0.1%