Skip to content

Welcome dubbo enters istio service mesh

2022-06-10

此处掌声!!!

Istio Service Mesh Group

http 1.0/1.1/2

gRPC

Why Dubbo Not Yet

我们在服务网格中面临着一些挑战:

  • Istio和其他主流服务网格产品仅对HTTP和gRPC支持,其他7层协议缺支持的非常有限;
  • Envoy的RDS(路由服务发现)仅仅是针对HTTP设计的,其他协议例如Dubbo、Thrift只能使用在线的监听器进行流量管理,而这种管理会使当路由变化时断开连接,这种断开会导致服务偶现地请求失败,却难以排查;
  • 引入一个7层的专有协议到服务网格要花费非常多的精力。我们需要写一个Envoy filter来处理数据平面的流量,同时也需要有个控制平面来管理这些Envoy的代理。

这使得如Dubbo等在微服务框架中广泛使用的7层协议,引入到服务网格变得非常困难。但也并非不可能,本文将会介绍如何将下列的协议引入到服务网格的方案:

  • 远程方法调用: HTTP, gRPC, Thrift, Dubbo
  • 消息: Kafka, RabbitMQ
  • 缓存: Redis, Mencached
  • 数据库:MySQL,PostgreSQL, MongoDB

How to Introduce Dubbo into Group

为了能够解决上述的问题,引入了腾讯云原生团队开源的Aeraki方案,它提供了一个可信赖的、可扩展的方法引入到服务网格,来管理上述的7层协议流量。

3.1 architecture

如上述架构图所示,Aeraki mesh包括以下几个组件:

  • Aeraki: Aeraki提供了用户友好的流量管理规则操作手段,并将这些流量管理规则“翻译”成为envoy filter配置,然后通过EnvoyFilter接口,将这些配置推送到业务服务的边车代理的envoy上。
  • MetaProtocol Proxy: MetaProtocol Proxy为这些7层协议提供了 通用能力,例如负载均衡,熔断,路由,限流,故障注入和认证。7层协议可以建立在MetaProtocol之上。新增一种新协议到服务网格,只需要实现编码器的接口和一些配置行。如果新协议还需要有特殊的需求,且MetaProtocol proxy内无法满足,它也提供了一种应用级别的过滤器机制,允许用户能够自定义的7层协议逻辑到MetaProtocol Proxy中。数据平面如下图所示:

  • X2Istio: X2Istio其中X表示协议名称,对于dubbo该组件名为Dubbo2Istio。Dubbo2istio 将 Dubbo 服务注册表中的 Dubbo 服务自动同步到 Istio 服务网格中,目前已经支持 ZooKeeper,Nacos 和 Etcd。

Dubbo已经基于MetaProtocol实现了编码器的接口。整个Dubbo在服务网格里面服务发现的流程如下:

画板

更具体地

3.1.1 Provider (Downstream)

提供者应用启动服务,注入了如下服务com.shuwen.ops.helloworld.api.AppService

<dubbo:registry protocol="zookeeper" check="false" file="false"/>
<dubbo:protocol name="dubbo">
  <dubbo:parameter key="aeraki_meta_app_namespace" value="${AERAKI_META_APP_NAMESPACE}" />
  <dubbo:parameter key="aeraki_meta_app_service_account" value="${AERAKI_META_APP_SERVICE_ACCOUNT}" />
  <dubbo:parameter key="aeraki_meta_app_version" value="${AERAKI_META_APP_VERSION}" />
  <dubbo:parameter key="aeraki_meta_workload_selector" value="${AERAKI_META_WORKLOAD_SELECTOR}" />
</dubbo:protocol>
<dubbo:monitor protocol="registry"/>

<dubbo:service interface="com.shuwen.ops.helloworld.api.AppService"  ref="appServiceImpl"  protocol="dubbo" version="0.0.1" timeout="3000"/>

其中 parameter的4个key,最终会带到zk里面,如下图所示:

# zk 路径
/dubbo/com.shuwen.ops.helloworld.api.AppService/providers

# 路径下的value
dubbo%3A%2F%2F10.11.17.79%3A20880%2Fcom.shuwen.ops.helloworld.api.AppService%3Faeraki_meta_app_namespace%3Dxhzy-pe%26aeraki_meta_app_service_account%3Ddefault%26aeraki_meta_app_version%3Dv1.223681%26aeraki_meta_workload_selector%3Dwsn-20210907%26anyhost%3Dtrue%26application%3Dshaman%26dubbo%3D2.8.7%26generic%3Dfalse%26interface%3Dcom.shuwen.ops.helloworld.api.AppService%26methods%3DgetHelloWorld%26organization%3Dshuwen%26owner%3Dwangshaonan%26pid%3D1%26revision%3D1.0-SNAPSHOT%26side%3Dprovider%26timeout%3D3000%26timestamp%3D1654402639085%26version%3D0.0.1

# value url decode
dubbo://10.11.17.79:20880/com.shuwen.ops.helloworld.api.AppService?aeraki_meta_app_namespace=xhzy-pe&aeraki_meta_app_service_account=default&aeraki_meta_app_version=v1.223681&aeraki_meta_workload_selector=wsn-20210907&anyhost=true&application=shaman&dubbo=2.8.7&generic=false&interface=com.shuwen.ops.helloworld.api.AppService&methods=getHelloWorld&organization=shuwen&owner=wangshaonan&pid=1&revision=1.0-SNAPSHOT&side=provider&timeout=3000&timestamp=1654402639085&version=0.0.1

3.1.2 Dubbo2Istio

  • 接下来会被Dubbo2Istio监听到,代码如下:
// Run starts the ProviderWatcher until it receives a message over the stop channel
// This method blocks the caller
func (w *ProviderWatcher) Run(stop <-chan struct{}) {
    providers, eventChan := watchUntilSuccess(w.path, w.conn)
    w.syncServices2Istio(w.service, providers)

    callback := func() {
        w.mutex.Lock()
        defer w.mutex.Unlock()
        w.syncServices2Istio(w.service, providers)
    }
    debouncer := debounce.New(debounceAfter, debounceMax, callback, stop)
    for {
        select {
            case <-eventChan:
            w.mutex.Lock()
            providers, eventChan = watchUntilSuccess(w.path, w.conn)
            w.mutex.Unlock()
            debouncer.Bounce()
            case <-stop:
            return
        }
    }
}

必须带这4个key才能同步生成ServiceEntry如下代码:

// ConvertServiceEntry converts dubbo provider znode to a service entry
func ConvertServiceEntry(registryName string,
    instances []DubboServiceInstance) map[string]*v1alpha3.ServiceEntry {
    serviceEntries := make(map[string]*v1alpha3.ServiceEntry)

    for _, instance := range instances {
        ...
        serviceAccount := dubboAttributes["aeraki_meta_app_service_account"]
        if serviceAccount == "" {
            serviceAccount = defaultServiceAccount
        }

        ns := dubboAttributes["aeraki_meta_app_namespace"]
        if ns == "" {
            log.Errorf("can't find aeraki_meta_app_namespace parameter, ignore provider %v,  ", dubboAttributes)
            continue
        }
        ...

        // What is this for? Authorization?
        selector := dubboAttributes["aeraki_meta_workload_selector"]
        if selector == "" {
            log.Errorf("can't find aeraki_meta_workload_selector parameter for provider %v,  ", dubboAttributes)
        }
        ...
        locality := strings.ReplaceAll(dubboAttributes["aeraki_meta_locality"], "%2F", "/")

        ...
        version := dubboAttributes["aeraki_meta_app_version"]
        if version != "" {
            labels["version"] = version
        }
        ...
    }
}

最终生成的ServiceEntry为:

apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  annotations:
    interface: com.shuwen.ops.helloworld.api.AppService
    workloadSelector: wsn-20210907
  labels:
    manager: aeraki
    registry: dubbo2istio
  name: aeraki-com-shuwen-ops-helloworld-api-appservice-0-0-1
  namespace: dubbo
spec:
  addresses:
  - 240.240.0.5
  endpoints:
  - address: 10.5.0.56
    labels:
      anyhost: "true"
      application: shaman
      dubbo: 2.8.7
      generic: "false"
      interface: com.shuwen.ops.helloworld.api.AppService
      methods: getHelloWorld
      organization: shuwen
      owner: wangshaonan
      pid: "1"
      registryName: default
      revision: 1.0-SNAPSHOT
      side: provider
      timeout: "3000"
      timestamp: "1655107164223"
      version: v2
    ports:
      tcp-dubbo: 20880
    serviceAccount: default
  hosts:
  - com.shuwen.ops.helloworld.api.appservice-0-0-1
  location: MESH_INTERNAL
  ports:
  - name: tcp-dubbo
    number: 20880
    protocol: tcp
    targetPort: 20880
  resolution: STATIC

3.1.3 Aeraki

Aeraki watch K8S的ServiceEntry,然后根据ServiceEntry生成EnvoyFilter,代码如下:

func (c *Controller) pushEnvoyFilters2APIServer() error {
    // 先去生成 EnvoyFilters的metadata
    generatedEnvoyFilters, err := c.generateEnvoyFilters()
    ...

    // 然后对每一个EnvoyFilter的metadata,向apiserver提交请求创建
    for _, wrapper := range generatedEnvoyFilters {
        _, err = c.istioClientset.NetworkingV1alpha3().EnvoyFilters(configRootNS).Create(context.TODO(), c.toEnvoyFilterCRD(wrapper,
            nil),
            v1.CreateOptions{FieldManager: aerakiFieldManager})
        ...
    }
    return err
}

接下来看下generateEnvoyFilters实现:

func (c *Controller) generateEnvoyFilters() (map[string]*model.EnvoyFilterWrapper, error) {
    envoyFilters := make(map[string]*model.EnvoyFilterWrapper)

    // 先去list有哪些serviceEntry
    serviceEntries, err := c.configStore.List(collections.IstioNetworkingV1Alpha3Serviceentries.Resource().
        GroupVersionKind(), "")

    // 对每一个serviceEntry
    for _, config := range serviceEntries {
        service, ok := config.Spec.(*networking.ServiceEntry)
        ...
        context := &model.EnvoyFilterContext{
            ServiceEntry: &model.ServiceEntryWrapper{
                Meta: config.Meta,
                Spec: service,
            },
            ...
        }
        // 对每一个port,都会找是否属于专用协议的端口,例如dubbo->20880,mysql->3306。如果有,就创建一个envoyFilter
        for _, port := range service.Ports {
            instance := protocol.GetLayer7ProtocolFromPortName(port.Name)
            if generator, ok := c.generators[instance]; ok {
                controllerLog.Infof("found generator for port: %s", port.Name)
                // 实际生成EnvoyFilter的metadata动作
                envoyFilterWrappers, err := generator.Generate(context)
                ...
                break
            }
        }
    }
    return envoyFilters, nil
}

生成的EnvoyFilter代码如下:

// Generate create EnvoyFilters for Dubbo services
func (g *Generator) Generate(context *model.EnvoyFilterContext) ([]*model.EnvoyFilterWrapper, error) {
    return envoyfilter.GenerateReplaceNetworkFilter(
        context.ServiceEntry,
        context.ServiceEntry.Spec.Ports[0],
        buildOutboundProxy(context),
        buildInboundProxy(context, g.client),
        "envoy.filters.network.dubbo_proxy",
        "type.googleapis.com/envoy.extensions.filters.network.dubbo_proxy.v3.DubboProxy"), nil
}

要生成两个EnvoyFilter,即InboundOutbound

画板

其中outbound会带有ServiceEntryaddresses

接下来看下InboundOutbound的具体内容:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  labels:
    manager: Aeraki
  name: aeraki-inbound-com.shuwen.ops.helloworld.api.appservice-0-0-1-20880
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      listener:
        filterChain:
          destinationPort: 20880
          filter:
            name: envoy.filters.network.tcp_proxy
        name: virtualInbound
    patch:
      operation: REPLACE
      value:
        name: envoy.filters.network.dubbo_proxy
        typed_config:
          '@type': type.googleapis.com/udpa.type.v1.TypedStruct
          type_url: type.googleapis.com/envoy.extensions.filters.network.dubbo_proxy.v3.DubboProxy
          value:
            dubboFilters:
            - name: envoy.filters.dubbo.router
            routeConfig:
            - interface: '*'
              name: inbound|20880||
              routes:
              - match:
                  method:
                    name:
                      safeRegex:
                        googleRe2: {}
                        regex: .*
                route:
                  cluster: inbound|20880||
            statPrefix: inbound|20880||
  workloadSelector:
    labels:
      app: wsn-20210907
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  labels:
    manager: Aeraki
  name: aeraki-outbound-com.shuwen.ops.helloworld.api.appservice-0-0-1-240.240.0.5-20880
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      listener:
        filterChain:
          filter:
            name: envoy.filters.network.tcp_proxy
        name: 240.240.0.5_20880
    patch:
      operation: REPLACE
      value:
        name: envoy.filters.network.dubbo_proxy
        typed_config:
          '@type': type.googleapis.com/udpa.type.v1.TypedStruct
          type_url: type.googleapis.com/envoy.extensions.filters.network.dubbo_proxy.v3.DubboProxy
          value:
            dubboFilters:
            - name: envoy.filters.dubbo.router
            routeConfig:
            - interface: com.shuwen.ops.helloworld.api.AppService
              name: outbound|20880||com.shuwen.ops.helloworld.api.appservice-0-0-1
              routes:
              - match:
                  method:
                    name:
                      safeRegex:
                        googleRe2: {}
                        regex: .*
                route:
                  cluster: outbound|20880||com.shuwen.ops.helloworld.api.appservice-0-0-1
            statPrefix: outbound|20880||com.shuwen.ops.helloworld.api.appservice-0-0-1

通过边车的cluster接口,

➜  ~ kubectl exec -it wsn-20210907-0 -ndubbo -c wsn-20210907 bash
[admin@wsn-20210907-0 ~]$ curl 127.0.0.1:15000/clusters

可以看到上游服务的一些信息:

3.1.4 Dubbo Protocol Proxy

我们目前istio的版本是1.7.6,如下图。根据aeraki mesh和dubbo protocol的部署要求,Aeraki官方声明需要istio >= 1.10的版本才能满足。如果要将istio升级到1.10,K8S版本至少要升级到 1.18,但升级K8S又是一大难题,也影响服务的稳定性。

画板

为了能够不升级K8S版本,且又能使用到dubbo protocol proxy的功能,则必须要做一些适配。目前K8S 1.16版本最高能够适配istio 1.8.6,而istio又分为数据平面和控制平面,而此协议主要是在数据平面上,所以可以将控制平面升级并维持到1.8.6,而数据平面使用 1.10的版本。具体来说,将sidecar proxyv2升级版本到

xhzy-registry.cn-hangzhou.cr.aliyuncs.com/share/proxyv2:1.8.6.1,而Dockerfile如下:

ARG SIDECAR=envoy

FROM docker.io/istio/proxyv2:1.10.6 as envoybase

FROM docker.io/istio/proxyv2:1.8.6

# Install Envoy.
COPY --from=envoybase /usr/local/bin/$SIDECAR /usr/local/bin/$SIDECAR

画板

3.1.5 Consumer (Upstream)

消费者在application.xml文件里面设置需要访问的接口url,这个url是Provider生成的ServiceEntryhost

<dubbo:reference id="appService" interface="com.shuwen.ops.helloworld.api.AppService" url="dubbo://com.shuwen.ops.helloworld.api.appservice-0-0-1:20880"
                     version="0.0.1" timeout="3000"/>

3.2 dubbo-mesh-sdk

综上所述,Provider和Consumer都需要添加一些配置,其中

  • Provider添加了一些Parameter
  • Consumer添加了直连的端点。

如果是在迁移阶段,当消费者添加了端点,发布上线,但是生产者并没有添加Parameter发布上线时,就不会生成ServiceEntry,这样就会出现直连失败的问题。所以需要解决这个问题,针对直连url进行判断,如果url有效,可以链接,那就直连,如果url无法telnet通,则还是走原来的从ZK获取端点的方式。

为了解决这个问题,且让研发尽量无感,故需要开发一个dubbo-mesh-sdk的jar,开发集成这个jar后,不需要修改任何配置,就可以让他的Dubbo RPC调用是无缝切换In Istio Service MeshOut

Reference