背景

因为公司外部所有服务都是跑在阿里云 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/v1beta1kind: Ingressmetadata:  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 builderWORKDIR /srcCOPY . .ENV GOPROXY https://goproxy.cnENV CGO_ENABLED=0RUN go build -ldflags "-w -s" -o main cmd/main.goFROM debian AS runnerENV TZ=Asia/shanghaiWORKDIR /appCOPY --from=builder /src/main .RUN chmod +x ./mainENTRYPOINT ["./main"]

yaml 模板提供

因为要在集群内拜访 k8s API,须要给 Pod 进行受权,通过K8S rbac进行受权,并以Deployment类型服务进行部署,最终模板如下:

apiVersion: v1kind: ServiceAccountmetadata:  name: dubbo-ingress-controller  namespace: default---kind: ClusterRoleapiVersion: rbac.authorization.k8s.io/v1beta1metadata:  name: dubbo-ingress-controllerrules:  - apiGroups:      - extensions    resources:      - ingresses    verbs:      - get      - list      - watch---kind: ClusterRoleBindingapiVersion: rbac.authorization.k8s.io/v1beta1metadata:  name: dubbo-ingress-controllerroleRef:  apiGroup: rbac.authorization.k8s.io  kind: ClusterRole  name: dubbo-ingress-controllersubjects:  - kind: ServiceAccount    name: dubbo-ingress-controller    namespace: default---apiVersion: apps/v1kind: Deploymentmetadata:  namespace: default  name: dubbo-ingress-controller  labels:    app: dubbo-ingress-controllerspec:  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查看。

参考链接:

  • dubbo 协定
  • dubbo-go
  • 应用多个-ingress-控制器
  • 应用 Golang 自定义 Kubernetes Ingress Controller

我是MonkeyWie,欢送扫码关注!不定期在公众号中分享JAVAGolang前端dockerk8s等干货常识。