L4 Gateway

1. 介绍

etcd的gateway是一个简单的tcp的转发代理服务。这个网关服务是无状态且透明的,它只做转发服务,不会对客户端请求和服务器的响应作任何的改变。
该转发服务用了一个简单轮训算法策略去支持多服务转发,并且支持简单的错误处理。
网关服务主要用来集群动态更新的问题,类似于一个nginx的代理功能。

2. 主方法介绍

etcd gateway的主要方法是从 startGateway 去实现的:

func startGateway(cmd *cobra.Command, args []string) {
    srvs := discoverEndpoints(gatewayDNSCluster, gatewayCA, gatewayInsecureDiscovery)
    if len(srvs.Endpoints) == 0 {
        // no endpoints discovered, fall back to provided endpoints
        srvs.Endpoints = gatewayEndpoints
    }
    // Strip the schema from the endpoints because we start just a TCP proxy
    srvs.Endpoints = stripSchema(srvs.Endpoints)
    if len(srvs.SRVs) == 0 {
        for _, ep := range srvs.Endpoints {
            h, p, err := net.SplitHostPort(ep)
            if err != nil {
                plog.Fatalf("error parsing endpoint %q", ep)
            }
            var port uint16
            fmt.Sscanf(p, "%d", &port)
            srvs.SRVs = append(srvs.SRVs, &net.SRV{Target: h, Port: port})
        }
    }

    if len(srvs.Endpoints) == 0 {
        plog.Fatalf("no endpoints found")
    }

    l, err := net.Listen("tcp", gatewayListenAddr)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    tp := tcpproxy.TCPProxy{
        Listener:        l,
        Endpoints:       srvs.SRVs,
        MonitorInterval: getewayRetryDelay,
    }

    // At this point, etcd gateway listener is initialized
    notifySystemd()

    tp.Run()
}

上面这一方法,从逻辑上,主要就是从以下几个步骤去执行:

  • 获取gatway代理的服务列表 discoverEndpoint
  • 监听网关端口 net.Listen("tcp", gatewayListenAddr)
  • 生成 tp := tcpproxy.TCPProxy 服务
  • 执行 tp.Run()

2.1 获取代理的服务地址

discoverEndpoint是其本质是利用 net包下的 lookupSRV 这个方法:对指定的service,protocol 以及domain的名称,进行srv查询。具体的net包下的用法请参考这个文章:
https://blog.csdn.net/chenbaoke/article/details/42782571

SRV 记录是一个域名系统 (DNS) 资源记录,用于标识承载特定服务的计算机。

通过对DNS的轮训或者指定服务的端口,启动代理。

2.2 监听网关端口生成proxy服务

通过使用 net.Listen("tcp", gatewayListenAddr) 监听服务端口,同时生成一个TCPProxy的结构。

type TCPProxy struct {
    Listener        net.Listener //网关监听地址
    Endpoints       []*net.SRV //SRV地址
    MonitorInterval time.Duration //这个函数主要是用于定时的重连remotes中坏链接。

    donec chan struct{} //用于优雅的关闭服务

    mu        sync.Mutex //读写锁
    remotes   []*remote//代理服务列表
    pickCount int // 用于轮训代理服务(round robin)
}

2.3 TCPProxy.Run

这个是整个网关服务中最核心的方法,其根本上通过对于对接入的client链接,采用 pick方法(round robin)去获取一个remote通过 io.copy 去透明的传输双芳的字节数据。

func (tp *TCPProxy) Run() error {
    tp.donec = make(chan struct{})
    if tp.MonitorInterval == 0 {
        tp.MonitorInterval = 5 * time.Minute
    }
    for _, srv := range tp.Endpoints { //初始化remotes
        addr := fmt.Sprintf("%s:%d", srv.Target, srv.Port)
        tp.remotes = append(tp.remotes, &remote{srv: srv, addr: addr})
    }

    eps := []string{}
    for _, ep := range tp.Endpoints {
        eps = append(eps, fmt.Sprintf("%s:%d", ep.Target, ep.Port))
    }
    plog.Printf("ready to proxy client requests to %+v", eps)

    go tp.runMonitor() //启动坏链接监控
    for {
        in, err := tp.Listener.Accept() //开启tcp accept 服务
        if err != nil {
            return err
        }

        go tp.serve(in) //处理客户端链接
    }
}

简单来说,就是一个tcp服务的三板斧,从 listen -- accept -- serve,同时对坏链接监控 runMonitor,这部分的源码比较简单。

我主要想说一下,在serve里面的处理过程:

func (tp *TCPProxy) serve(in net.Conn) {
    var (
        err error
        out net.Conn
    )

    for {
        tp.mu.Lock()
        remote := tp.pick()
        tp.mu.Unlock()
        if remote == nil {
            break
        }
        // TODO: add timeout
        out, err = net.Dial("tcp", remote.addr)
        if err == nil {
            break
        }
        remote.inactivate()
        plog.Warningf("deactivated endpoint [%s] due to %v for %v", remote.addr, err, tp.MonitorInterval)
    }

    if out == nil {
        in.Close()
        return
    }

    go func() {
        io.Copy(in, out)
        in.Close()
        out.Close()
    }()

    io.Copy(out, in)
    out.Close()
    in.Close()
}

上面的代码中最核心的就是一个 tp.pick()io.Copy 这两个方法。

tp.pick这个方法很简单,就是从remotes中列表获取一个remote,这个pick很有意思,从其方法中有可能取到空链接,和坏链接,只是几率很小罢了。

io.Copy上面这两个方法很有意思,实际上我认为这个是golang的设计精髓之一,即采用非阻塞的方式来处理文件描述符,元无需关注回调。(我有个朋友这个地方有些疑惑,是为什么把tcp也叫文件?在unix的设计中,一切都是文件)。

在这个上面, io.Copy和Close两个函数都是阻塞的,具体可以认为是非阻塞的,我们可以实验一下:

    go func() {
        time.Sleep(5*time.Second)
        io.Copy(in, out)
        err1 := in.Close()
        fmt.Printf("in1:%v \n", err1)
        err1 = out.Close()
        fmt.Printf("out1:%v \n", err1)
    }()

    io.Copy(out, in)
    err2 := out.Close()
    fmt.Printf("out2:%v \n", err2)
    err2 = in.Close()
    fmt.Printf("in2:%v \n", err2)

改写上面的方法之后,调用etcd自带的测试用例,返回的数据是:

out2:<nil>
in2:close tcp 127.0.0.1:62229->127.0.0.1:62231: use of closed network connection
in1:<nil>
--- PASS: TestUserspaceProxy (0.00s)
PASS
out1:close tcp 127.0.0.1:62232->127.0.0.1:62230: use of closed network connection

很有意思,也很明显的是,返回值跟预期的逻辑并不一致,主要是由于阻塞。

3. 使用参数描述

使用命令 etcd gateway start -h:

start the gateway

Usage:
  etcd gateway start [flags]

Flags:
      --discovery-srv string     DNS domain used to bootstrap initial cluster
      --endpoints stringSlice    comma separated etcd cluster endpoints (default [127.0.0.1:2379])
      --insecure-discovery       accept insecure SRV records
      --listen-addr string       listen address (default "127.0.0.1:23790")
      --retry-delay duration     duration of delay before retrying failed endpoints (default 1m0s)
      --trusted-ca-file string   path to the client server TLS CA file.

可以看出gateway只有几个参数。

  • --discovery-srv 用于服务的dns发现。
  • --endpoints 指定服务地址列表(注意只有当 discovery-srv未指定,或者没有srv信息的时候,这个参数才会生效,默认一个地址是 127.0.0.1:2379)
  • --insecure-discovery 是否接受不安全的SRV 默认是false
  • --listen-addr string 服务的监听地址,默认是127.0.0.1:23790
  • -- retry-delay 这个参数就是指在 runMonitor 中的 for 的轮训时间,默认是1分钟
  • --trusted-ca-file CA 文件地址,验证服务地址的安全性。默认为空。

3. 使用场景

gateway并不推荐单独使用,主要是由于它不能自动发现服务节点,必须在初始化的时候指定端口或dns信息。
虽然官方文档中说,etcd gateway的使用场景就是在有多个相同服务的时候,客户端无需知道所有的服务地址,并且在服务地址有所更新的时候,只需要更新gateway的endpoint列表,但是我并不推荐使用它,因为在阅读源码的过程中,并没有看到任何可以更新服务地址列表的API提供。

猜你喜欢

转载自www.cnblogs.com/songjingsong/p/9227557.html