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×tamp=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
,即Inbound
和Outbound
其中outbound
会带有ServiceEntry
的addresses
接下来看下Inbound
和Outbound
的具体内容:
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
生成的ServiceEntry
的host
:
<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 Mesh
和Out
。