前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Service Mesh 是如何做到对业务代码无侵入的透明代理?Istio 中通过 iptables 做流量拦截

Service Mesh 是如何做到对业务代码无侵入的透明代理?Istio 中通过 iptables 做流量拦截

作者头像
hugo_lei
发布2021-08-16 17:26:20
1.1K0
发布2021-08-16 17:26:20
举报

目录

1 传统微服务MicroService的问题:侵入式 Client 端服务发现+LoadBalance

1.1 Client 端服务发现+负载均衡

2 Istio 是如何实现流量劫持的?

2.1 要做哪些事?

2.2 透明代理

2.3 Sidecar

2.4 iptables

2.5 Init 容器

3 问题:如何判断目标服务的类型?

3.1 Cluster IP


1 传统微服务MicroService的问题:侵入式 Client 端服务发现+LoadBalance

1.1 Client 端服务发现+负载均衡

传统微服务,服务发现+负载均衡的代码,是和业务代码耦合在一起的,并且在运行过程中,也是和业务跑在同一个进程里。

例如 Springboot 项目启动的 Tomcat 服务,业务逻辑跑在这个 tomcat 里,同时服务发现的代码,及服务发现后的负载均衡代码,也跑在这个 tomcat 里。

那么能不能将业务代码和框架代码解耦呢?

能不能实现 tomcat 服务器里只跑业务代码,而服务发现+负载均衡交给其他进程去实现?

答案是可以的,将服务发现+负载均衡放在单独的 sidecar 进程中,与业务代码解耦,同时通过流量劫持来实现对于服务流量的 proxy。

Istio 的项目中有一个亮点就是可以将旧的应用无缝接入到 Service Mesh 的平台上来,不用修改一行代码。实现这个功能,目前主要是通过 iptables 来截获流量转发给 proxy。

2 Istio 是如何实现流量劫持的?

参考 Istio 的实现方式,我们可以自己设计一个简单的流量劫持的方案。

2.1 要做哪些事?

  • 首先要有一个支持透明代理的 proxy,处理被劫持的流量,能够获取到连接建立时的原来的目的地址。在 k8s 中这个 proxy 采用 sidecar 的方式和要劫持流量的服务部署在一个 Pod 中。
  • 通过 iptables 将我们想要劫持的流量劫持到 proxy 中。proxy 自身的流量要排除在外。
  • 要实现零侵入,最好不修改服务的镜像,在 k8s 中可以采用 Init 容器的方式在应用容器启动之前做 iptables 的修改。

2.2 透明代理

proxy 作为一个透明代理,对于自身能处理的流量,会经过一系列的处理逻辑,包括重试,超时,负载均衡等,再转发给对端服务。对于自身不能处理的流量,会直接透传,不作处理。

通过 iptables 将流量转发给 proxy 后,proxy 需要能够获取到原来建立连接时的目的地址。在 Go 中的实现稍微麻烦一些,需要通过 syscall 调用来获取,

示例代码:

代码语言:javascript
复制
package redirect

import (
    "errors"
    "fmt"
    "net"
    "os"
    "syscall"
)

const SO_ORIGINAL_DST = 80

var (
    ErrGetSocketoptIPv6 = errors.New("get socketopt ipv6 error")
    ErrResolveTCPAddr   = errors.New("resolve tcp address error")
    ErrTCPConn          = errors.New("not a valid TCPConn")
)

// For transparent proxy.
// Get REDIRECT package's originial dst address.
// Note: it may be only support linux.
func GetOriginalDstAddr(conn *net.TCPConn) (addr net.Addr, c *net.TCPConn, err error) {
    fc, errRet := conn.File()
    if errRet != nil {
        conn.Close()
        err = ErrTCPConn
        return
    } else {
        conn.Close()
    }
    defer fc.Close()

    mreq, errRet := syscall.GetsockoptIPv6Mreq(int(fc.Fd()), syscall.IPPROTO_IP, SO_ORIGINAL_DST)
    if errRet != nil {
        err = ErrGetSocketoptIPv6
        c, _ = getTCPConnFromFile(fc)
        return
    }

    // only support ipv4
    ip := net.IPv4(mreq.Multiaddr[4], mreq.Multiaddr[5], mreq.Multiaddr[6], mreq.Multiaddr[7])
    port := uint16(mreq.Multiaddr[2])<<8 + uint16(mreq.Multiaddr[3])
    addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", ip.String(), port))
    if err != nil {
        err = ErrResolveTCPAddr
        return
    }

    c, errRet = getTCPConnFromFile(fc)
    if errRet != nil {
        err = ErrTCPConn
        return
    }
    return
}

func getTCPConnFromFile(f *os.File) (*net.TCPConn, error) {
    newConn, err := net.FileConn(f)
    if err != nil {
        return nil, ErrTCPConn
    }

    c, ok := newConn.(*net.TCPConn)
    if !ok {
        return nil, ErrTCPConn
    }
    return c, nil
}

通过 GetOriginalDstAddr 函数可以获取到连接原来的目的地址。

这里需要格外注意的是,当启用 iptables 转发后,proxy 如果接收到直接访问自己的连接时,会识别到自身不能处理,会再去连接此目的地址(就是自己绑定的地址),这样就会导致死循环。所以在服务启动时,需要将目的地址为自身 IP 的连接直接断开。

2.3 Sidecar

使用 Sidecar 模式部署服务网格时,会在每一个服务身边额外启一个 proxy 去接管容器的部分流量。在 kubernetes 中一个 Pod 可以有多个容器,这多个容器可以共享网络,存储等资源,从概念上将服务容器和 proxy 容器部署成一个 Pod,proxy 容器就相当于是 sidecar 容器。

我们通过一个 Deployment 来演示,这个 Deployment 的 yaml 配置中包括了 test 和 proxy 两个 container,它们共享网络,所以登录 test 容器后,通过 127.0.0.1:30000 可以访问到 proxy 容器。

代码语言:javascript
复制
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
  namespace: default
  labels:
    app: test
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: test
    spec:
      containers:
      - name: test
        image: {test-image}
        ports:
          - containerPort: 9100
      - name: proxy
        image: {proxy-image}
        ports:
          - containerPort: 30000    

为每一个服务都编写 sidecar 容器的配置是一件比较繁琐的事情,当架构成熟后,我们就可以利用 kubernetes 的 MutatingAdmissionWebhook 功能,在用户创建 Deployment 时,主动注入 sidecar 相关的配置。

例如,我们在 Deployment 的 annotations 中加入如下的字段:

代码语言:javascript
复制
annotations:
  xxx.com/sidecar.enable: "true"
  xxx.com/sidecar.version: "v1"

表示在此 Deployment 中需要注入 v1 版本的 sidecar。当我们的服务收到这个 webhook 后,就可以检查相关的 annotations 字段,根据字段配置来决定是否注入 sidecar 配置以及注入什么版本的配置,如果其中有一些需要根据服务改变的参数,也可以通过这种方式传递,极大地提高了灵活性。

2.4 iptables

通过 iptables 我们可以将指定的流量劫持到 proxy,并将部分流量排除在外。

代码语言:javascript
复制
iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner 9527 -j RETURN
iptables -t nat -A OUTPUT -p tcp -d 172.17.0.0/16 -j REDIRECT --to-port 30000

上面的命令,表示将目标地址是 172.17.0.0/16 的流量 REDIRECT 到 30000 端口(proxy 所监听的端口)。但是 UID 为 9527 启动的进程除外。172.17.0.0/16 这个地址是 k8s 集群内部的 IP 段,我们只需要劫持这部分流量,对于访问集群外部的流量,暂时不劫持,如果劫持全部流量,对于 proxy 不能处理的请求,就需要通过 iptables 的规则去排除。

2.5 Init 容器

前文说过为了实现零侵入,我们需要通过 Init 容器的方式,在启动用户服务容器之前,就修改 iptables。这部分配置也可以通过 kubernetes 的 MutatingAdmissionWebhook 功能注入到用户的 Deployment 配置中。

将前面 sidecar 的配置中加上 Init 容器的配置:

代码语言:javascript
复制
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
  namespace: default
  labels:
    app: test
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: test
    spec:
      initContainers:
      - name: iptables-init
        image: {iptables-image}
        imagePullPolicy: IfNotPresent
        command: ['sh', '-c', 'iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner 9527 -j RETURN && iptables -t nat -A OUTPUT -p tcp -d 172.17.0.0/16 -j REDIRECT --to-port 30000']
        securityContext:
          capabilities:
            add:
            - NET_ADMIN
          privileged: true
      containers:
      - name: test
        image: {test-image}
        ports:
          - containerPort: 9100
      - name: proxy
        image: {proxy-image}
        ports:
          - containerPort: 30000    

这个 Init 容器需要安装 iptables,在启动时会执行我们配置的 iptables 命令。

需要额外注意的是 securityContext 这个配置项,我们加了 NET_ADMIN 的权限。它用于定义 Pod 或 Container 的权限,如果不配置,则 iptables 执行命令时会提示错误。

3 问题:如何判断目标服务的类型?

我们将 172.17.0.0/16 的流量都劫持到了 proxy 内部,那么如何判断目标服务的协议类型?如果不知道协议类型,就不能确定如何去解析后续的请求。

在 kubernetes 的 service 中,我们可以为每一个 service 的端口指定一个名字,这个名字的格式可以固定为 {name}-{protocol},例如 {test-http},表示这个 service 的某个端口是 http 协议。

代码语言:javascript
复制
kind: Service
apiVersion: v1
metadata:
  name: test
  namespace: default
spec:
  selector:
    app: test
  ports:
    - name: test-http
      port: 9100
      targetPort: 9100
      protocol: TCP

proxy 通过 discovery 服务获取到 service 对应的 Cluster IP 和端口名称,这样通过目标服务的 IP 和 port 就可以知道这个连接的通信协议类型,后面就可以交给对应的 Handler 去处理。

3.1 Cluster IP

在 kubernetes 中创建 Service,如果没有指定,默认采用 Cluster IP 的方式来访问,kube-proxy 会为此创建 iptables 规则,将 Cluster IP 转换为以负载均衡的方式转发到 Pod IP。

当存在 Cluster IP 时,service 的 DNS 解析会指向 Cluster IP,负载均衡由 iptables 来做。如果不存在,DNS 解析的结果会直接指向 Pod IP。

proxy 依赖于 service 的 Cluster IP 来判断用户访问的是哪一个服务,所以不能设置为 clusterIP: None。因为 Pod IP 是有可能会经常变动的,当增减实例时,Pod IP 的集合都会改变,proxy 并不能实时的获取到这些变化。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-06-24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 传统微服务MicroService的问题:侵入式 Client 端服务发现+LoadBalance
    • 1.1 Client 端服务发现+负载均衡
    • 2 Istio 是如何实现流量劫持的?
      • 2.1 要做哪些事?
        • 2.2 透明代理
          • 2.3 Sidecar
            • 2.4 iptables
              • 2.5 Init 容器
              • 3 问题:如何判断目标服务的类型?
                • 3.1 Cluster IP
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档