作者

李腾飞,腾讯容器技术研发工程师,腾讯云TKE后台研发,SuperEdge核心开发成员。

杜杨浩,腾讯云高级工程师,热衷于开源、容器和Kubernetes。目前主要从事镜像仓库,Kubernetes集群高可用&备份还原,以及边缘计算相关研发工作。

SuperEdge 介绍

SuperEdge 是 Kubernetes 原生的边缘容器方案,它将 Kubernetes 强大的容器管理能力扩展到边缘计算场景中,针对边缘计算场景中常见的技术挑战提供了解决方案,如:单集群节点跨地域、云边网络不可靠、边缘节点位于 NAT 网络等。这些能力可以让应用很容易地部署到边缘计算节点上,并且可靠地运行,可以帮助您很方便地把分布在各处的计算资源放到一个 Kubernetes 集群中管理,包括但不限于:边缘云计算资源、私有云资源、现场设备,打造属于您的边缘 PaaS 平台。SuperEdge 支持所有 Kubernetes 资源类型、API 接口、使用方式、运维工具,无额外的学习成本,也兼容其他云原生项目,如:Promethues,使用者可以结合其他所需的云原生项目一起使用。项目由以下公司共同发起:腾讯、Intel、VMware、虎牙直播、寒武纪、首都在线和美团。

云边隧道的架构与原理

在边缘场景中,很多时候都是单向网络,即只有边缘节点能主动访问云端。云边隧道主要用于代理云端访问边缘节点组件的请求,解决云端无法直接访问边缘节点的问题。

架构图如下所示:

实现原理为:

  • 边缘节点上 tunnel-edge 主动连接 tunnel-cloud service,tunnel-cloud service根据负载均衡策略将请求转到 tunnel-cloud pod

  • tunnel-edgetunnel-cloud 建立 gRPC 连接后,tunnel-cloud 会把自身的podIp和 tunnel-edge 所在节点的 nodeName 的映射写入 tunnel-dnsgRPC 连接断开之后,tunnel-cloud 会删除相关 podIp 和节点名的映射

而整个请求的代理转发流程如下:

  • apiserver 或者其它云端的应用访问边缘节点上的 kubelet 或者其它应用时,tunnel-dns 通过 DNS 劫持(将 HTTP Request 中的 host 中的节点名解析为 tunnel-cloud 的podIp)把请求转发到 tunnel-cloud 的pod上

  • tunnel-cloud 根据节点名把请求信息转发到节点名对应的与 tunnel-edge 建立的 gRPC 连接上

  • tunnel-edge 根据接收的请求信息请求边缘节点上的应用

Tunnel 内部模块数据交互

在介绍完 Tunnel 的配置后,下面介绍 Tunnel 的内部数据流转:

上图标记出了 HTTPS 代理的数据流转,TCP 代理数据流转和 HTTPS 的类似,其中的关键步骤:

  • HTTPS Server -> StreamServer:HTTPS Server 通过 Channel将 StreamMsg 发送给 Stream Server,其中的 Channel 是根据 StreamMsg.Node 字段从 nodeContext 获取 node.Channel

  • StreamServer -> StreamClient: 每个云边隧道都会分配一个 node 对象,将 StreamClient 发送到 node 中的 Channel 即可把数据发往 StreamClient

  • StreamServer -> HTTPS Server: StreamServer 通过 Channel 将 StreamMsg 发送给 HTTPS Server,其中的 Channel 是根据 StreamMsg.Node从nodeContext 获取 node,通过 StreamMsg.Topic 与 conn.uid 匹配获取 HTTPS 模块的 conn.Channel

nodeContext 和 connContext 都是做连接的管理,但是 nodeContext 管理 gRPC 长连接的和 connContext 管理的上层转发请求的连接(TCPHTTPS)的生命周期是不相同的,因此需要分开管理

Tunnel 的连接管理

Tunnel 管理的连接可以分为底层连接(云端隧道的 gRPC 连接)和上层应用连接(HTTPS 连接和 TCP 连接),连接异常的管理的可以分为以下几种场景:

gRPC 连接正常,上层连接异常

以 HTTPS 连接为例,tunnel-edge 的 HTTPS Client 与边缘节点 Server 连接异常断开,会发送 StreamMsg (StreamMsg.Type=CLOSE) 消息,tunnel-cloud 在接收到 StreamMsg 消息之后会主动关闭 HTTPS Server与HTTPS Client 的连接。

gRPC 连接异常

gRPC 连接异常,Stream 模块会根据与 gPRC 连接绑定的 node.connContext,向 HTTPS 和 TCP 模块发送 StreamMsg(StreamMsg.Type=CLOSE),HTTPS 或 TCP 模块接收消息之后主动断开连接。

Stream (gRPC云边隧道)

func (stream *Stream) Start(mode string) {
context.GetContext().RegisterHandler(util.STREAM_HEART_BEAT, util.STREAM, streammsg.HeartbeatHandler)
if mode == util.CLOUD {
...
//启动gRPC server
go connect.StartServer()
...
//同步coredns的hosts插件的配置文件
go connect.SynCorefile()
} else {
//启动gRPC client
go connect.StartSendClient()
...
}
...
}

tunnel-cloud 首先调用 RegisterHandler 注册心跳消息处理函数 HeartbeatHandler

SynCorefile 执行同步 tunnel-coredns 的 hosts 插件的配置文件,每隔一分钟(考虑到 configmap 同步 tunnel-cloud 的 pod 挂载文件的时间)执行一次 checkHosts,如下:

func SynCorefile() {
for {
...
err := coreDns.checkHosts()
...
time.Sleep(60 * time.Second)
}
}

而 checkHosts 负责 configmap 具体的刷新操作:

func (dns *CoreDns) checkHosts() error {
nodes, flag := parseHosts()
if !flag {
return nil
}
...
_, err = dns.ClientSet.CoreV1().ConfigMaps(dns.Namespace).Update(cctx.TODO(), cm, metav1.UpdateOptions{})
...
}

checkHosts 首先调用 parseHosts 获取本地 hosts 文件中边缘节点名称以及对应 tunnel-cloud podIp 映射列表,对比 podIp 的对应节点名和内存中节点名,如果有变化则将这个内容覆盖写入 configmap 并更新:

另外,这里 tunnel-cloud 引入 configmap 本地挂载文件的目的是:优化托管模式下众多集群同时同步 tunnel-coredns 时的性能

tunnel-edge 首先调用 StartClient 与 tunnel-edge 建立 gRPC 连接,返回 grpc.ClientConn

func StartClient() (*grpc.ClientConn, ctx.Context, ctx.CancelFunc, error) {
...
opts := []grpc.DialOption{grpc.WithKeepaliveParams(kacp),
grpc.WithStreamInterceptor(ClientStreamInterceptor),
grpc.WithTransportCredentials(creds)}
conn, err := grpc.Dial(conf.TunnelConf.TunnlMode.EDGE.StreamEdge.Client.ServerName, opts...)
...
}

在调用 grpc.Dial 时会传递grpc.WithStreamInterceptor(ClientStreamInterceptor) DialOption,将 ClientStreamInterceptor 作为 StreamClientInterceptor 传递给 grpc.ClientConn,等待 gRPC 连接状态变为 Ready,然后执行 Send 函数。streamClient.TunnelStreaming 调用StreamClientInterceptor 返回 wrappedClientStream 对象

func ClientStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
...
opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{AccessToken: clientToken})))
...
return newClientWrappedStream(s), nil
}

ClientStreamInterceptor 会将边缘节点名称以及 token 构造成 oauth2.Token.AccessToken 进行认证传递,并构建 wrappedClientStream

stream.Send 会并发调用 wrappedClientStream.SendMsg 以及 wrappedClientStream.RecvMsg 分别用于 tunnel-edge 发送以及接受,并阻塞等待

注意:tunnel-edge 向 tunnel-cloud 注册节点信息是在建立 gRPC Stream 时,而不是创建 grpc.connClient 的时候

整个过程如下图所示:

相应的,在初始化 tunnel-cloud 时,会将grpc.StreamInterceptor(ServerStreamInterceptor)构建成 gRPC ServerOption,并将 ServerStreamInterceptor 作为 StreamServerInterceptor 传递给 grpc.Server:

func StartServer() {
...
opts := []grpc.ServerOption{grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp), grpc.StreamInterceptor(ServerStreamInterceptor), grpc.Creds(creds)}
s := grpc.NewServer(opts...)
proto.RegisterStreamServer(s, &stream.Server{})
...
}

云端 gRPC 服务在接受到 tunnel-edge 请求(建立 Stream 流)时,会调用 ServerStreamInterceptor,而 ServerStreamInterceptor 会从gRPC metadata 中解析出此 gRPC 连接对应的边缘节点名和token,并对该 token 进行校验,然后根据节点名构建 wrappedServerStream 作为与该边缘节点通信的处理对象(每个边缘节点对应一个处理对象),handler 函数会调用 stream.TunnelStreaming,并将 wrappedServerStream 传递给它(wrappedServerStream 实现了proto.Stream_TunnelStreamingServer 接口)

func ServerStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
md, ok := metadata.FromIncomingContext(ss.Context())
...
tk := strings.TrimPrefix(md["authorization"][0], "Bearer ")
auth, err := token.ParseToken(tk)
...
if auth.Token != token.GetTokenFromCache(auth.NodeName) {
klog.Errorf("invalid token node = %s", auth.NodeName)
return ErrInvalidToken
}
err = handler(srv, newServerWrappedStream(ss, auth.NodeName))
if err != nil {
ctx.GetContext().RemoveNode(auth.NodeName)
klog.Errorf("node disconnected node = %s err = %v", auth.NodeName, err)
}
return err
}

而当 TunnelStreaming 方法退出时,就会执 ServerStreamInterceptor 移除节点的逻辑ctx.GetContext().RemoveNode

TunnelStreaming 会并发调用 wrappedServerStream.SendMsg 以及 wrappedServerStream.RecvMsg 分别用于 tunnel-cloud 发送以及接受,并阻塞等待:

func (s *Server) TunnelStreaming(stream proto.Stream_TunnelStreamingServer) error {
errChan := make(chan error, 2)
go func(sendStream proto.Stream_TunnelStreamingServer, sendChan chan error) {
sendErr := sendStream.SendMsg(nil)
...
sendChan <- sendErr
}(stream, errChan)
go func(recvStream proto.Stream_TunnelStreamingServer, recvChan chan error) {
recvErr := stream.RecvMsg(nil)
...
recvChan <- recvErr
}(stream, errChan)
e := <-errChan
return e
}

SendMsg 会从 wrappedServerStream 对应边缘节点 node 中接受 StreamMsg,并调用 ServerStream.SendMsg 发送该消息给 tunnel-edge

func (w *wrappedServerStream) SendMsg(m interface{}) error {    if m != nil {        return w.ServerStream.SendMsg(m)    }    node := ctx.GetContext().AddNode(w.node)    ...    for {        msg := <-node.NodeRecv()        ...        err := w.ServerStream.SendMsg(msg)        ...    }}

而 RecvMsg 会不断接受来自 tunnel-edge 的 StreamMsg,并调用 StreamMsg.对应的处理函数进行操作

小结:

  • Stream 模块负责建立 gRPC连接以及通信(云边隧道)
  • 边缘节点上 tunnel-edge 主动连接云端 tunnel-cloud service,tunnel-cloud service 根据负载均衡策略将请求转到tunnel-cloud pod
  • tunnel-edgetunnel-cloud 建立 gRPC 连接后,tunnel-cloud 会把自身的 podIp 和 tunnel-edge 所在节点的 nodeName 的映射写入tunnel-corednsgRPC 连接断开之后,tunnel-cloud 会删除相关 podIp 和节点名的映射
  • tunnel-edge 会利用边缘节点名以及 token 构建 gRPC 连接,而 tunnel-cloud 会通过认证信息解析 gRPC 连接对应的边缘节点,并对每个边缘节点分别构建一个 wrappedServerStream 进行处理(同一个 tunnel-cloud 可以处理多个 tunnel-edge 的连接)
  • tunnel-cloud 每隔一分钟(考虑到 configmap 同步 tunnel-cloud 的 pod 挂载文件的时间)向 tunnel-coredns 的 hosts 插件的配置文件对应 configmap 同步一次边缘节点名以及 tunnel-cloud podIp 的映射;另外,引入 configmap 本地挂载文件优化了托管模式下众多集群同时同步 tunnel-coredns 时的性能
  • tunnel-edge 每隔一分钟会向 tunnel-cloud 发送代表该节点正常的心跳 StreamMsg,而 tunnel-cloud 在接受到该心跳后会进行回应(心跳是为了探测 gRPC Stream 流是否正常)
  • StreamMsg 包括心跳,TCP 代理以及 HTTPS 请求等不同类型消息;同时 tunnel-cloud 通过 context.node 区分与不同边缘节点 gRPC 连接隧道

HTTPS 代理

HTTPS 模块负责建立云边的 HTTPS 代理,将云端组件(例如:kube-apiserver)的 https 请求转发给边端服务(例如:kubelet)

func (https *Https) Start(mode string) {    context.GetContext().RegisterHandler(util.CONNECTING, util.HTTPS, httpsmsg.ConnectingHandler)    context.GetContext().RegisterHandler(util.CONNECTED, util.HTTPS, httpsmsg.ConnectedAndTransmission)    context.GetContext().RegisterHandler(util.CLOSED, util.HTTPS, httpsmsg.ConnectedAndTransmission)    context.GetContext().RegisterHandler(util.TRANSNMISSION, util.HTTPS, httpsmsg.ConnectedAndTransmission)    if mode == util.CLOUD {        go httpsmng.StartServer()    }}

Start 函数首先注册了 StreamMsg 的处理函数,其中 CLOSED 处理函数主要处理关闭连接的消息,并启动 HTTPS Server。

当云端组件向 tunnel-cloud 发送 HTTPS 请求时,serverHandler 会首先从 request.Host 字段解析节点名,若先建立 TLS 连接,然后在连接中写入 HTTP 的 request 对象,此时的 request.Host 可以不设置,则需要从 request.TLS.ServerName 解析节点名。HTTPS Server 读取 request.Body 以及 request.Header 构建 HttpsMsg 结构体,并序列化后封装成 StreamMsg,通过 Send2Node 发送 StreamMsg 放入 StreamMsg.node 对应的 node 的 Channel 中,由 Stream 模块发送到 tunnel-edge

func (serverHandler *ServerHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {    var nodeName string    nodeinfo := strings.Split(request.Host, ":")    if context.GetContext().NodeIsExist(nodeinfo[0]) {        nodeName = nodeinfo[0]    } else {        nodeName = request.TLS.ServerName    }    ...    node.Send2Node(StreamMsg)}

tunnel-edge 接受到 StreamMsg,并调用 ConnectingHandler 函数进行处理:

func ConnectingHandler(msg *proto.StreamMsg) error {    go httpsmng.Request(msg)    return nil}func Request(msg *proto.StreamMsg) {    httpConn, err := getHttpConn(msg)    ...    rawResponse := bytes.NewBuffer(make([]byte, 0, util.MaxResponseSize))    rawResponse.Reset()    respReader := bufio.NewReader(io.TeeReader(httpConn, rawResponse))    resp, err := http.ReadResponse(respReader, nil)    ...    node.BindNode(msg.Topic)    ...    if resp.StatusCode != http.StatusSwitchingProtocols {        handleClientHttp(resp, rawResponse, httpConn, msg, node, conn)    } else {        handleClientSwitchingProtocols(httpConn, rawResponse, msg, node, conn)    }}

ConnectingHandler 会调用 Request 对该 StreamMsg 进行处理。Reqeust 首先通过 getHttpConn 与边缘节点 Server 建立的 TLS 连接。解析 TLS 连接中返回的数据获取 HTTP Response,Status Code 为200,将 Response 的内容发送到 tunnel-cloud,Status Code 为101,将从TLS 连接读取 Response 的二进制数据发送到 tunnel-cloud,其中 StreamMsg.Type为CONNECTED。

tunnel-cloud 在接受到该 StreamMsg 后,会调用 ConnectedAndTransmission 进行处理:

func ConnectedAndTransmission(msg *proto.StreamMsg) error {    conn := context.GetContext().GetConn(msg.Topic)    ...    conn.Send2Conn(msg)    return nil}

通过 msg.Topic(conn uid) 获取 conn,并通过 Send2Conn 将消息塞到该 conn 对应的管道中

云端 HTTPS Server 在接受到云端的 CONNECTED 消息之后,认为HTTPS 代理成功建立。并继续执行 handleClientHttp or handleClientSwitchingProtocols 进行数据传输,这里只分析 handleClientHttp 非协议提升下的数据传输过程,HTTPS Client 端的处理逻辑如下:

func handleClientHttp(resp *http.Response, rawResponse *bytes.Buffer, httpConn net.Conn, msg *proto.StreamMsg, node context.Node, conn context.Conn) {    ...    go func(read chan *proto.StreamMsg, response *http.Response, buf *bytes.Buffer, stopRead chan struct{}) {        rrunning := true        for rrunning {            bbody := make([]byte, util.MaxResponseSize)            n, err := response.Body.Read(bbody)            respMsg := &proto.StreamMsg{                Node:     msg.Node,                Category: msg.Category,                Type:     util.CONNECTED,                Topic:    msg.Topic,                Data:     bbody[:n],            }            ...            read <- respMsg        }        ...    }(readCh, resp, rawResponse, stop)    running := true    for running {        select {        case cloudMsg := <-conn.ConnRecv():            ...        case respMsg := <-readCh:            ...            node.Send2Node(respMsg)            ...        }    }    ...}

这里 handleClientHttp 会一直尝试读取来自边端组件的数据包,并构建成 TRANSNMISSION 类型的 StreamMsg 发送给 tunnel-cloudtunnel-cloud 在接受到StreamMsg 后调用 ConnectedAndTransmission 函数,将 StreamMsg 放入 StreamMsg.Type 对应的 HTTPS 模块的 conn.Channel 中

func handleServerHttp(rmsg *HttpsMsg, writer http.ResponseWriter, request *http.Request, node context.Node, conn context.Conn) {    for k, v := range rmsg.Header {        writer.Header().Add(k, v)    }    flusher, ok := writer.(http.Flusher)    if ok {        running := true        for running {            select {            case <-request.Context().Done():                ...            case msg := <-conn.ConnRecv():                ...                _, err := writer.Write(msg.Data)                flusher.Flush()                ...            }        }    ...}

handleServerHttp 在接受到 StreamMsg 后,会将 msg.Data,也即边端组件的数据包,发送给云端组件。整个数据流是单向的由边端向云端传送,如下所示:

而对于类似kubectl exec的请求,数据流是双向的,此时边端组件 (kubelet) 会返回 StatusCode 为101的回包,标示协议提升,之后 tunnel-cloud 以及 tunnel-edge 会分别切到 handleServerSwitchingProtocols 以及 handleClientSwitchingProtocols 对 HTTPS 底层连接进行读取和写入,完成数据流的双向传输。

架构如下所示:



总结HTTPS模块如下:

小结

  • HTTPS:负责建立云边 HTTPS 代理(eg:云端 kube-apiserver <-> 边端 kubelet),并传输数据
  • 作用与 TCP 代理类似,不同的是 tunnel-cloud 会读取云端组件 HTTPS 请求中携带的边缘节点名,并尝试建立与该边缘节点的 HTTPS 代理;而不是像 TCP 代理一样随机选择一个云边隧道进行转发
  • 云端 apiserver 或者其它云端的应用访问边缘节点上的 kubelet 或者其它应用时,tunnel-dns 通过DNS劫持(将 Request host 中的节点名解析为 tunnel-cloud 的 podIp)把请求转发到 tunnel-cloud 的pod上,tunnel-cloud 把请求信息封装成 StreamMsg 通过与节点名对应的云边隧道发送到 tunnel-edge,tunnel-edge 通过接收到的 StreamMsg 的 Addr 字段和配置文件中的证书与边缘端 Server 建立 TLS 连接,并将 StreamMsg 中的请求信息写入 TLS 连接。tunnel-edgeTLS 连接中读取到边缘端 Server 的返回数据,将其封装成 StreamMsg 发送到 tunnel-cloudtunnel-cloud 将接收到数据写入云端组件与 tunnel-cloud 建立的连接中。

TCP

TCP 模块负责在多集群管理中建立云端管控集群与边缘独立集群的一条 TCP 代理隧道:

func (tcp *TcpProxy) Start(mode string) {    context.GetContext().RegisterHandler(util.TCP_BACKEND, tcp.Name(), tcpmsg.BackendHandler)    context.GetContext().RegisterHandler(util.TCP_FRONTEND, tcp.Name(), tcpmsg.FrontendHandler)    context.GetContext().RegisterHandler(util.CLOSED, tcp.Name(), tcpmsg.ControlHandler)    if mode == util.CLOUD {    ...        for front, backend := range Tcp.Addr {            go func(front, backend string) {                ln, err := net.Listen("tcp", front)                ...                for {                    rawConn, err := ln.Accept()                    ....                    fp := tcpmng.NewTcpConn(uuid, backend, node)                    fp.Conn = rawConn                    fp.Type = util.TCP_FRONTEND                    go fp.Write()                    go fp.Read()                }            }(front, backend)        }    }

Start 函数首先注册了 StreamMsg 的处理函数,其中 CLOSED 处理函数主要处理关闭连接的消息,之后在云端启动 TCP Server。

在接受到云端组件的请求后,TCP Server 会将请求封装成 StremMsg 发送给 StreamServer,由 StreamServer 发送到 tunnel-edge,其中 StreamMsg.Type=FrontendHandler,StreamMsg.Node 从已建立的云边隧道的节点中随机选择一个。

tunnel-edge 在接受到该StreamMsg 后,会调用 FrontendHandler 函数处理

func FrontendHandler(msg *proto.StreamMsg) error {    c := context.GetContext().GetConn(msg.Topic)    if c != nil {        c.Send2Conn(msg)        return nil    }    tp := tcpmng.NewTcpConn(msg.Topic, msg.Addr, msg.Node)    tp.Type = util.TCP_BACKEND    tp.C.Send2Conn(msg)    tcpAddr, err := net.ResolveTCPAddr("tcp", tp.Addr)    if err != nil {    ...    conn, err := net.DialTCP("tcp", nil, tcpAddr)    ...    tp.Conn = conn    go tp.Read()    go tp.Write()    return nil}

FrontendHandler 首先使用 StreamMsg.Addr 与 Edge Server 建立 TCP 连接,启动协程异步对 TCP 连接 Read 和 Write,同时新建 conn 对象(conn.uid=StreamMsg.Topic),并 eamMsg.Data 写入 TCP 连接。tunnel-edge 在接收到 Edge Server 的返回数据将其封装为 StreamMsg(StreamMsg.Topic=BackendHandler) 发送到 tunnel-cloud

整个过程如图所示:

小结

  • TCP:负责在多集群管理中建立云端与边端的 TCP 代理
  • 云端组件通过 TCP 模块访问边缘端的 Server,云端的 TCP Server 在接收到请求会将请求封装成 StreamMsg 通过云边隧道(在已连接的隧道中随机选择一个,因此推荐在只有一个 tunnel-edge 的场景下使用 TCP 代理)发送到 tunnel-edgetunnel-edge 通过接收到 StreamMag 的Addr字段与边缘端 Server 建立TCP* 连接,并将请求写入 TCP 连接。tunnel-edgeTCP 连接中读取边缘端 Server 的返回消息,通过云边缘隧道发送到tunnel-cloud,tunnel-cloud 接收到消息之后将其写入云端组件与 TCP Server 建立的连接

展望

  • 支持更多的网络协议(已支持 HTTPSTCP)
  • 支持云端访问边缘节点业务 pod server
  • 多个边缘节点同时加入集群时,多副本 tunnel-cloud pod 在更新 tunnel-coredns 的 hosts 插件配置文件对应 configmap 时没有加锁,虽然概率较低,但理论上依然存在写冲突的可能性

Refs

一文读懂 SuperEdge 云边隧道的更多相关文章

  1. 一文读懂 SuperEdge 边缘容器架构与原理

    前言 superedge是腾讯推出的Kubernetes-native边缘计算管理框架.相比openyurt以及kubeedge,superedge除了具备Kubernetes零侵入以及边缘自治特性, ...

  2. 一文读懂SuperEdge拓扑算法

    前言 SuperEdge service group 利用 application-grid-wrapper 实现拓扑感知,完成了同一个 nodeunit 内服务的闭环访问 在深入分析 applica ...

  3. SuperEdge 云边隧道新特性:从云端SSH运维边缘节点

    背景 在边缘集群的场景下边缘节点分布在不同的区域,且边缘节点和云端之间是单向网络,边缘节点可以访问云端节点,云端节点无法直接访问边缘节点,给边缘节点的运维带来很大不便,如果可以从云端SSH登录到边缘节 ...

  4. [转帖]一文读懂 HTTP/2

    一文读懂 HTTP/2 http://support.upyun.com/hc/kb/article/1048799/ 又小拍 • 发表于:2017年05月18日 15:34:45 • 更新于:201 ...

  5. kubernetes基础——一文读懂k8s

    容器 容器与虚拟机对比图(左边为容器.右边为虚拟机)   容器技术是虚拟化技术的一种,以Docker为例,Docker利用Linux的LXC(LinuX Containers)技术.CGroup(Co ...

  6. 一文读懂数仓中的pg_stat

    摘要:GaussDB(DWS)在SQL执行过程中,会记录表增删改查相关的运行时统计信息,并在事务提交或回滚后记录到共享的内存中.这些信息可以通过 "pg_stat_all_tables视图& ...

  7. 一文读懂HTTP/2及HTTP/3特性

    摘要: 学习 HTTP/2 与 HTTP/3. 前言 HTTP/2 相比于 HTTP/1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作,当然兼容问题以及如何 ...

  8. 一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现

    一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现 导读:近日,马云.马化腾.李彦宏等互联网大佬纷纷亮相2018世界人工智能大会,并登台演讲.关于人工智能的现状与未来,他们提出了各自的观点,也引 ...

  9. 一文读懂高性能网络编程中的I/O模型

    1.前言 随着互联网的发展,面对海量用户高并发业务,传统的阻塞式的服务端架构模式已经无能为力.本文(和下篇<高性能网络编程(六):一文读懂高性能网络编程中的线程模型>)旨在为大家提供有用的 ...

随机推荐

  1. 利用flex解决input定位的问题

    用简单的布局搞定input框使用fixed下输入的问题 最近在做移动端H5聊天应用发现,当input框在最底部并且使用 position:fixed 属性的时候在苹果手机中会出现不兼容的情况 ​

  2. ResNet的个人总结

    ResNet可以说是我认真读过的第一篇paper,据师兄说读起来比较简单,没有复杂的数学公式,不过作为经典的网络结构还是有很多细节值得深究的.因为平时不太读英文文献,所以其实读的时候也有很多地方不是很 ...

  3. effective解读-第一条 静态工厂创建对象代替构造器

    好处 有名称,能见名知意.例如BigInteger的probablePrime方法 享元模式.单例模式中使用 享元模式:创建对象代价很高,重复调用已有对象,例如数据库连接等.享元模式是单例模式的一个拓 ...

  4. [React Hooks长文总结系列一]初出茅庐,状态与副作用

    写在开头 React Hooks在我的上一个项目中得到了充分的使用,对于这个项目来说,我们跳过传统的类组件直接过渡到函数组件,确实是一个不小的挑战.在项目开发过程中也发现项目中的其他小伙伴(包括我自己 ...

  5. Android 之 TableLayout 布局详解

    TableLayout简介 •简介 Tablelayout 类以行和列的形式对控件进行管理,每一行为一个 TableRow 对象,或一个 View 控件. 当为 TableRow 对象时,可在 Tab ...

  6. 不想eject,还咋修改create-react-app的配置?

    一.先抛问题 许多刚开始接触create-react-app框架的同学,不免都会有个疑问:如何在不执行eject操作的同时,修改create-react-app的配置.今天胡哥就来带大家一起来看看这个 ...

  7. Polly-故障处理和弹性应对很有一手

    前言 对于运行中的系统,可以说百分百的小伙伴会经常遇见以下问题: 网络不通,突然又好了: 服务器宕机了: 调用服务接口超时了: 调用接口报错啦: 通讯信息发送失败需要重发: 以上只是列举了一些常遇到的 ...

  8. Spring Boot 2.3 新特性优雅停机详解

    什么是优雅停机 先来一段简单的代码,如下: @RestController public class DemoController { @GetMapping("/demo") p ...

  9. 【项目】手写FTP服务器-C++实现FTP服务器

    X_FTP_server 手写FTP服务器-C++实现FTP服务器 项目Gitee链接:https://gitee.com/hsby/ftp_Server 简介 一个基于libevent的高并发FTP ...

  10. win10美化,让你的win10独一无二,与众不同!

    2020.06.23 更新 1 原则 美化之前,得先有一个目标对不对,笔者是一个喜欢简单的人,因此美化本着三大原则:简单,干净,整洁. 呃....好像很抽象的样子,上图吧.反正没图没真相. 怎么样,还 ...