在Golang中设置网络名称空间后运行GRPC时是否存在任何已知限制?

时间:2019-03-13 21:23:06

标签: go grpc

在Golang项目中切换网络名称空间后,我遇到了GRPC客户端与服务器之间进行通信的问题。为了解决这个问题,我相应地修改了GRPC示例程序Hello World,结果是同样的问题。在golang应用程序中切换名称空间后使用GRPC时,是否存在任何已知的限制?

我已经阅读了有关在Golang中切换名称空间的问题,但是我想这是否会导致问题取决于GRPC行为。

  1. GRPC客户端是否会生成任何其他goroutine?由于名称空间切换,此类goroutine不一定会在与原始goroutine锁定线程的相同名称空间中执行。

  2. 我假设GRPC服务器为每个客户端生成goroutine,但是在生成新的goroutine之前,它是否在原始goroutine中创建了套接字?这个问题还与产生的goroutine不必在调用grpc.Serve()的名称空间中执行相同(命名空间开关需要runtime.LockOSThread())。

在服务器和客户端在匹配的名称空间(ip netns exec ...)中启动的情况下,通信有效,但是在客户端内部执行名称空间切换的情况下,通信失败。当在服务器内部执行名称空间切换时,通信也可以进行,因此问题应该出在客户端。

greeter_client / main.go:

package main

import (
    "fmt"
    "log"
    "os"
    "runtime"
    "syscall"
    "time"

    pb "grpctest/helloworld/helloworld"

    "github.com/vishvananda/netns"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
)

const (
    defaultName    = "world"
    defaultAddress = "localhost:50051"
    nsEnv          = "NAMESPACE"
    addressEnv     = "ADDRESS"
    blockEnv       = "DIALBLOCK"
)

func main() {
    fmt.Printf("* Client thread id before runtime.LockOSThread(): %d\n", syscall.Gettid())
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()
    fmt.Printf("* Client thread id after runtime.LockOSThread(): %d\n", syscall.Gettid())

    var dialOpts []grpc.DialOption
    dialOpts = append(dialOpts, grpc.WithInsecure())
    _, ok := os.LookupEnv(blockEnv)
    if ok == true {
        dialOpts = append(dialOpts, grpc.WithBlock())
        fmt.Printf("* Dial in blocked mode\n")
    } else {
        fmt.Printf("* Dial in unblocked mode\n")
    }

    address, ok := os.LookupEnv(addressEnv)
    if ok == false {
        address = defaultAddress
    }

    fmt.Printf("* Talk to server at %s\n", address)

    var origns netns.NsHandle
    namespace, ok := os.LookupEnv(nsEnv)
    if ok {
        fmt.Printf("* Switch namespace to %s\n", namespace)
        origns, err := netns.Get()
        if err != nil {
            log.Fatal("failed to get current namespace")
        }

        defer origns.Close()
        newns, err := netns.GetFromName(namespace)
        if err != nil {
            log.Fatalf("failed to get new namespace: %s", namespace)
        }

        err = netns.Set(newns)
        if err != nil {
            log.Fatalf("failed to set new namespace: %s", namespace)
        }

        defer newns.Close()
    }

    fmt.Printf("* Client thread id before grpc.Dial(): %d\n", syscall.Gettid())
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, dialOpts...)
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    fmt.Printf("* Client thread id before pb.NewGreeterClient(): %d\n", syscall.Gettid())
    c := pb.NewGreeterClient(conn)
    fmt.Printf("* Client thread id after pb.NewGreeterClient(): %d\n", syscall.Gettid())

    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) > 1 {
        name = os.Args[1]
    }

    r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
    if err != nil {
        fmt.Printf("could not greet: %v", err)
        select {}
        log.Fatalf("could not greet: %v", err)
    }
    fmt.Printf("* Client thread id after c.SayHello(): %d\n", syscall.Gettid())

    log.Printf("Greeting: %s", r.Message)
    time.Sleep(5 * time.Second)

    if namespace != "" {
        netns.Set(origns)
    }

    fmt.Printf("* Client thread id at exit: %d\n", syscall.Gettid())
}

greeter_server / main.go:

package main

import (
    "fmt"
    "log"
    "net"
    "os"
    "runtime"
    "syscall"

    pb "grpctest/helloworld/helloworld"

    "github.com/vishvananda/netns"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

const (
    port  = ":50051"
    nsEnv = "NAMESPACE"
)

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    fmt.Printf("* RPC call server thread id: %d\n", syscall.Gettid())
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()
    var origns netns.NsHandle

    namespace := os.Getenv(nsEnv)
    if namespace != "" {
        fmt.Printf("* Switch namespace to %s\n", namespace)
        origns, err := netns.Get()
        if err != nil {
            log.Fatal("failed to get current namespace")
        }

        defer origns.Close()
        newns, err := netns.GetFromName(namespace)
        if err != nil {
            log.Fatalf("failed to get new namespace: %s", namespace)
        }

        err = netns.Set(newns)
        if err != nil {
            log.Fatalf("failed to set new namespace: %s", namespace)
        }

        defer newns.Close()
    }

    fmt.Printf("* Main server thread id: %d\n", syscall.Gettid())
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    // Register reflection service on gRPC server.
    reflection.Register(s)

    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }

    if namespace != "" {
        netns.Set(origns)
    }

    fmt.Printf("* Main server exits (thread id: %d)\n", syscall.Gettid())
}

1 个答案:

答案 0 :(得分:1)

我遇到了完全相同的问题,无论是否锁定OS线程,netns都会在每个新的goroutine上切换回原始名称空间。

我发现只要服务器在正确的网络中启动(我放弃尝试以编程方式执行此操作,而只是通过dialOpts = append(dialOpts, grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { netns.Set(newns) return net.DialTimeout("tcp", addr, timeout) })) 生成了服务器进程),则只需为该服务器设置自定义拨号程序GRPC客户端在连接到正确的netns期间还原goroutine:

{{1}}