手擼一個ingress controller來打通dubbo+k8s網路

mokeyWie發表於2021-11-16

背景

由於公司內部所有服務都是跑在阿里雲 k8s 上的,然後 dubbo 提供者預設向註冊中心上報的 IP 都是Pod IP,這意味著在 k8s 叢集外的網路環境是呼叫不了 dubbo 服務的,如果本地開發需要訪問 k8s 內的 dubbo 提供者服務的話,需要手動把服務暴露到外網,我們的做法是針對每一個提供者服務暴露一個SLB IP+自定義埠,並且通過 dubbo 提供的DUBBO_IP_TO_REGISTRYDUBBO_PORT_TO_REGISTRY環境變數來把對應的SLB IP+自定義埠註冊到註冊中心裡,這樣就實現了本地網路和 k8s dubbo 服務的打通,但是這種方式管理起來非常麻煩,每個服務都得自定義一個埠,而且每個服務之間埠還不能衝突,當服務多起來之後非常難以管理。

於是我就在想能不能像nginx ingress一樣實現一個七層代理+虛擬域名來複用一個埠,通過目標 dubbo 提供者的application.name來做對應的轉發,這樣的話所有的服務只需要註冊同一個SLB IP+埠就可以了,大大的提升便利性,一方調研之後發現可行就開擼了!

專案已開源:https://github.com/monkeyWie/dubbo-ingress-controller

技術預研

思路

  1. 首先 dubbo RPC 呼叫預設是走的dubbo協議,所以我需要先去看看協議裡有沒有可以利用做轉發的報文資訊,就是尋找類似於 HTTP 協議裡的 Host 請求頭,如果有的話就可以根據此資訊做反向代理虛擬域名的轉發,在此基礎之上實現一個類似nginxdubbo閘道器
  2. 第二步就是要實現dubbo ingress controller,通過 k8s ingress 的 watcher 機制動態的更新dubbo 閘道器的虛擬域名轉發配置,然後所有的提供者服務都由此服務同一轉發,並且上報到註冊中心的地址也統一為此服務的地址。

架構圖

dubbo 協議

先上一個官方的協議圖:

可以看到 dubbo 協議的 header 是固定的16個位元組,裡面並沒有類似於 HTTP Header 的可擴充套件欄位,也沒有攜帶目標提供者的application.name欄位,於是我向官方提了個issue,官方的答覆是通過消費者自定義Filter來將目標提供者的application.name放到attachments裡,這裡不得不吐槽下 dubbo 協議,擴充套件欄位竟然是放在body裡,如果要實現轉發需要把請求報文全部解析完才能拿到想要報文,不過問題不大,因為主要是做給開發環境用的,這一步勉強可以實現。

k8s ingress

k8s ingress 是為 HTTP 而生的,但是裡面的欄位夠用了,來看一段 ingress 配置:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: user-rpc-dubbo
  annotations:
    kubernetes.io/ingress.class: "dubbo"
spec:
  rules:
    - host: user-rpc
      http:
        paths:
          - backend:
              serviceName: user-rpc
              servicePort: 20880
            path: /

配置和 http 一樣通過host來做轉發規則,但是host配置的是目標提供者的application.name,後端服務是目標提供者對應的service,這裡有一個比較特殊的是使用了一個kubernetes.io/ingress.class註解,這個註解可以指定此ingress對哪個ingress controller生效,後面我們的dubbo ingress controller就只會解析註解值為dubbo的 ingress 配置。

開發

前面的技術預研一切順利,接著就進入開發階段了。

消費者自定義 Filter

前面有提到如果請求裡要攜帶目標提供者的application.name,需要消費者自定義Filter,程式碼如下:

@Activate(group = CONSUMER)
public class AddTargetFilter implements Filter {

  @Override
  public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
    String targetApplication = StringUtils.isBlank(invoker.getUrl().getRemoteApplication()) ?
        invoker.getUrl().getGroup() : invoker.getUrl().getRemoteApplication();
    // 目標提供者的application.name放入attachment
    invocation.setAttachment("target-application", targetApplication);
    return invoker.invoke(invocation);
  }
}

這裡又要吐槽一下,dubbo 消費者首次訪問時會發起一個獲取 metadata 的請求,這個請求通過invoker.getUrl().getRemoteApplication()是拿不到值的,通過invoker.getUrl().getGroup()才能拿到。

dubbo 閘道器

這裡就要開發一個類似nginxdubbo閘道器,並實現七層代理和虛擬域名轉發,程式語言直接選擇了 go,首先 go 做網路開發心智負擔低,另外有個 dubbo-go 專案,可以直接利用裡面的解碼器,然後 go 有原生的 k8s sdk 支援,簡直完美!

思路就是開啟一個TCP Server,然後解析 dubbo 請求的報文,把attachment裡的target-application屬性拿到,再反向代理到真正的 dubbo 提供者服務上,核心程式碼如下:

routingTable := map[string]string{
  "user-rpc": "user-rpc:20880",
  "pay-rpc":  "pay-rpc:20880",
}

listener, err := net.Listen("tcp", ":20880")
if err != nil {
  return err
}
for {
  clientConn, err := listener.Accept()
  if err != nil {
    logger.Errorf("accept error:%v", err)
    continue
  }
  go func() {
    defer clientConn.Close()
    var proxyConn net.Conn
    defer func() {
      if proxyConn != nil {
        proxyConn.Close()
      }
    }()
    scanner := bufio.NewScanner(clientConn)
    scanner.Split(split)
    // 解析請求報文,拿到一個完整的請求
    for scanner.Scan() {
      data := scanner.Bytes()
      // 通過dubbo-go提供的庫把[]byte反序列化成dubbo請求結構體
      buf := bytes.NewBuffer(data)
      pkg := impl.NewDubboPackage(buf)
      pkg.Unmarshal()
      body := pkg.Body.(map[string]interface{})
      attachments := body["attachments"].(map[string]interface{})
      // 從attachments裡拿到目標提供者的application.name
      target := attachments["target-application"].(string)
      if proxyConn == nil {
        // 反向代理到真正的後端服務上
        host := routingTable[target]
        proxyConn, _ = net.Dial("tcp", host)
        go func() {
          // 原始轉發
          io.Copy(clientConn, proxyConn)
        }()
      }
      // 把原始報文寫到真正後端服務上,然後走原始轉發即可
      proxyConn.Write(data)
    }
  }()
}

func split(data []byte, atEOF bool) (advance int, token []byte, err error) {
    if atEOF && len(data) == 0 {
        return 0, nil, nil
    }

    buf := bytes.NewBuffer(data)
    pkg := impl.NewDubboPackage(buf)
    err = pkg.ReadHeader()
    if err != nil {
        if errors.Is(err, hessian.ErrHeaderNotEnough) || errors.Is(err, hessian.ErrBodyNotEnough) {
            return 0, nil, nil
        }
        return 0, nil, err
    }
    if !pkg.IsRequest() {
        return 0, nil, errors.New("not request")
    }
    requestLen := impl.HEADER_LENGTH + pkg.Header.BodyLen
    if len(data) < requestLen {
        return 0, nil, nil
    }
    return requestLen, data[0:requestLen], nil
}

dubbo ingress controller 實現

前面已經實現了一個dubbo閘道器,但是裡面的虛擬域名轉發配置(routingTable)還是寫死在程式碼裡的,現在要做的就是當檢測到k8s ingress有更新時,動態的更新這個配置就可以了。

首先先簡單的說明下ingress controller的原理,拿我們常用的nginx ingress controller為例,它也是一樣通過監聽k8s ingress資源變動,然後動態的生成nginx.conf檔案,當發現配置發生了改變時,觸發nginx -s reload重新載入配置檔案。

裡面用到的核心技術就是informers,利用它來監聽k8s資源的變動,示例程式碼:

// 在叢集內獲取k8s訪問配置
cfg, err := rest.InClusterConfig()
if err != nil {
  logger.Fatal(err)
}
// 建立k8s sdk client例項
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
  logger.Fatal(err)
}
// 建立Informer工廠
factory := informers.NewSharedInformerFactory(client, time.Minute)
handler := cache.ResourceEventHandlerFuncs{
  AddFunc: func(obj interface{}) {
    // 新增事件
  },
  UpdateFunc: func(oldObj, newObj interface{}) {
    // 更新事件
  },
  DeleteFunc: func(obj interface{}) {
    // 刪除事件
  },
}
// 監聽ingress變動
informer := factory.Extensions().V1beta1().Ingresses().Informer()
informer.AddEventHandler(handler)
informer.Run(ctx.Done())

通過實現上面的三個事件來動態的更新轉發配置,每個事件都會攜帶對應的Ingress物件資訊過來,然後進行對應的處理即可:

ingress, ok := obj.(*v1beta12.Ingress)
if ok {
  // 通過註解過濾出dubbo ingress
  ingressClass := ingress.Annotations["kubernetes.io/ingress.class"]
  if ingressClass == "dubbo" && len(ingress.Spec.Rules) > 0 {
    rule := ingress.Spec.Rules[0]
    if len(rule.HTTP.Paths) > 0 {
      backend := rule.HTTP.Paths[0].Backend
      host := rule.Host
      service := fmt.Sprintf("%s:%d", backend.ServiceName+"."+ingress.Namespace, backend.ServicePort.IntVal)
      // 獲取到ingress配置中host對應的service,通知給dubbo閘道器進行更新
      notify(host,service)
    }
  }
}

docker 映象提供

k8s 之上所有的服務都需要跑在容器裡的,這裡也不例外,需要把dubbo ingress controller構建成 docker 映象,這裡通過兩階段構建優化,來減小映象體積:

FROM golang:1.17.3 AS builder
WORKDIR /src
COPY . .
ENV GOPROXY https://goproxy.cn
ENV CGO_ENABLED=0
RUN go build -ldflags "-w -s" -o main cmd/main.go

FROM debian AS runner
ENV TZ=Asia/shanghai
WORKDIR /app
COPY --from=builder /src/main .
RUN chmod +x ./main
ENTRYPOINT ["./main"]

yaml 模板提供

由於要在叢集內訪問 k8s API,需要給 Pod 進行授權,通過K8S rbac進行授權,並以Deployment型別服務進行部署,最終模板如下:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: dubbo-ingress-controller
  namespace: default

---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: dubbo-ingress-controller
rules:
  - apiGroups:
      - extensions
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: dubbo-ingress-controller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: dubbo-ingress-controller
subjects:
  - kind: ServiceAccount
    name: dubbo-ingress-controller
    namespace: default

---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: dubbo-ingress-controller
  labels:
    app: dubbo-ingress-controller
spec:
  selector:
    matchLabels:
      app: dubbo-ingress-controller
  template:
    metadata:
      labels:
        app: dubbo-ingress-controller
    spec:
      serviceAccountName: dubbo-ingress-controller
      containers:
        - name: dubbo-ingress-controller
          image: liwei2633/dubbo-ingress-controller:0.0.1
          ports:
            - containerPort: 20880

後期需要的話可以做成Helm進行管理。

後記

至此dubbo ingress controller實現完成,可以說麻雀雖小但是五臟俱全,裡面涉及到了dubbo協議TCP協議七層代理k8s ingressdocker等等很多內容,這些很多知識都是在雲原生越來越流行的時代需要掌握的,開發完之後感覺受益匪淺。

關於完整的使用教程可以通過github檢視。

參考連結:

我是MonkeyWie,歡迎掃碼??關注!不定期在公眾號中分享JAVAGolang前端dockerk8s等乾貨知識。

wechat

相關文章