dubbo_go

dubbo_go 查看完整档案

北京编辑上海财经大学  |  计算机科学技术 编辑apache  |  dubbo-go 编辑 github.com/apache/dubbo-go 编辑
编辑

于雨(github @AlexStocks),dubbogo 社区负责人,一个有十多年服务端基础架构和中间件研发一线工作经验的程序员,陆续参与和改进过 Redis/Pika/Muduo/dubbo-go/Sentinel-go 等知名项目,目前在蚂蚁金服可信原生部从事容器编排工作。

个人动态

dubbo_go 发布了文章 · 2月9日

云原生中间件的下一站

作者:于雨

于雨(github @AlexStocks),dubbogo 社区负责人,一个有十年服务端基础架构和中间件研发一线工作经验的程序员,陆续参与和改进过 Redis/Pika/Muduo/dubbo-go/Sentinel-go 等知名项目,目前在蚂蚁金服可信原生部从事容器编排工作。

自从以 2013 年开源的 docker 为代表的的容器技术和以 2014 年开源的 K8s 为代表的容器编排技术登上舞台之后,相关技术从业人员从认知和体感上接受,云原生时代真的到来了。

当然也有很多资深技术人员认为,云原生时代要从 2010s 时以 OpenStack 为代表的虚机编排时代开始。 当然,也有人说其实云原生技术诞生很早,可以从巨型机时代在巨型机上虚拟出若干小型机开始追溯。

在云原生时代,不变镜像作为核心技术的 docker 定义了不可变的单服务部署形态,统一了容器编排形态的 k8s 则定义了不变的 service 接口,二者结合定义了服务可依赖的不可变的基础设施。有了这种完备的不变的基础设置,就可以定义不可变的中间件新形态 -- 云原生中间件。

云原生时代的中间件,包含了不可变的缓存、通信、消息、事件(event) 等基础通信设施,应用只需通过本地代理即可调用所需的服务,无需关心服务能力来源。

1 微服务框架


从最早的单体应用时代到分布式技术时代,流行的是微服务技术。微服务时代各大公司都沉淀出了具有代表性的一些服务通信框架,如 Google 的 gRPC,阿里的 Dubbo 和 HSF,百度的 bRPC 等等。多个服务通信框架之间的竞争,基本上是在大公司之间进行角力。

站在使用者的角度,当然期待一个网络框架在进化过程中能够保持向前兼容性,多个框架之间保持互通性。

1.1 服务框架的向后兼容性

通信框架的基础是通信协议和序列化协议,这其中很重要的问题就是新版本协议对旧版本的向后兼容性。在一个组织中一般都使用统一的通信框架,但现实中可能因为各种原因,同一个框架的序列化协议或者通信协议的向后兼容能力差会,导致使用不同版本通信框架的各个服务之间的异构化。如采用了 pb v2 和 pb v3 的通信框架不兼容,不遑多让的 Thrift 0.8.x 与Thrift 0.9.x 之间也不兼容。

不过 Protobuf v3 或者 Protobuf v2 的各个子版本之间的向前和先后兼容性还是不错的,但还是有一些弱鸡公司的内部序列化协议无论是否采用 TLV 形式,其协议各个版本的之间还是无法兼容,进而导致各个子版本的服务框架相互异构,最终导致使用了不同版本的服务框架的业务背上大量包袱无法快速演进,有些新版本的业务中存在各种神逻辑可能不是为了兼容旧版本的业务逻辑,而是为了兼容旧版本框架的通信协议。

1.2 多框架之间的互通性

一个常识是,组织规模膨胀到足够大的程度后,不可能有一个通用的框架适用于所有的场景,一些大经济实体随着业务体量变大,业务类型变得庞杂,不可避免地存在一些重复的轮子,这些庞大规模的组织因为其规模效应,任何一个适用于特定场景的框架只要能在内部找到若干落地应用场景就足以让其开发维护成本变得可负担且收益甚大。

公司内部各个分公司之间可能存在不同服务框架导致各个服务之间通信异构化越来越严重,如阿里内部早前存在异构的 Dubbo 和 HSF(目前阿里内部 HSF 和 Dubbo 已经开始融合, HSF 已经采用 Dubbo 作为内核以 Dubbo 插件的形式存在),如当下的阿里的 HSF 和各个收来的新公司之间的网络通信,其间通信可能不得不借助于 Proxy 形式的通信网关。

每个服务框架各个子版本之间需要保持向后兼容,各个服务框架之间的兼容可能需要网关代理。

1.3 多语言框架之间的互联

除了序列化协议、通信框架的异构外,还普遍存在着因为各个不同语言技术栈之间差异导致的异构:每种语言都有个各自的序列化协议和通信框架。如 Java 世界就存在着 Spring Cloud 和 Dubbo 两大服务治理框架,Spring Cloud 尚无多语言版本实现,相比之下 Dubbo 的多语言工作稍好一些。

1.4 打通打平不同的技术体系

同一实体内部不同公司之间有两三个不同服务框架的情况已经算是很好了,大公司组织内部可以容忍适量的重复造轮子,但大量的重复造轮子就过犹不及了。据说有巨头内部不同部门之间存在各自的 RPC 服务框架 ,其总体服务框架超 100+!

随着国内互联网行业发展由于头部效应趋向性明显,在大鱼吃小鱼的时代背景下,大公司与收购来的公司之间的技术体系异构也是云原生时代中间件面临的一个问题,二者之间因为业务规模不同带来的服务注册中心、配置中心、语言技术栈、服务鉴权、缓存和存储等诸多技术不统一。有的刚收来的公司甚至使用了大公司的竞争对手的云平台,这就又带来了平台级的差异,诸如此类的异构问题不一而足。

借助网络代理技术,可以初步快速打通不同技术体系之间的异构差异。

1.5 通信代理的必要性

除了南北向通信的网络接入层代理外,微服务时代使用同一通信框架的各个服务实体之间直接进行通信,很少听说各个服务实体之间通过代理 Proxy 进行通信,其根由是各个通信实体之间通信框架同构。

或者说,网络通信代理被各个强悍的通信框架给消化掉,以 Proxyless SDK 的形式存在,如服务发现、服务路由、容灾限流、负载均衡等功能都存在于各个服务框架的 SDK 中。

但随着多协议多语言等各种因素导致的各个框架之间的各种异化,大家普遍相信:没有什么差异不是一层 Proxy 解决不了的。

1.6 Service Mesh

2016 年之后兴起的 Service Mesh 技术区分为 Proxy Service Mesh 和 Proxyless Service Mesh,至于二者之间的差异可参见本文 2019 年的一篇文章 Service Mesh 形态刍议。目前比较流行的 Service Mesh 技术形式是 Proxy Service Mesh,其比较有代表性的组件有数据面的 envoy 和控制面的 Istio。

一些 Service Mesh 技术文档宣称,将服务框架的序列化协议、通信能力、服务治理能力沉淀到服务网格的代理(如 envoy 这类数据面 sidecar)中,可有如下收益:

  • 1 服务框架 SDK 会变的非常轻量,甚至完全沉淀到 sidecar 中。
  • 2 多语言、多协议和多种通信方式之间的差异将被磨平。
  • 3 统一流量控制,提升系统的弹性。
  • 4 统一监控设施,提高可观测性。
  • 5 统一安全可信认证,提升安全性。
  • 6 升级过程业务无感,做到平滑升级,提升可靠性。
  • 7 提升业务版本迭代速度。
  • 8 快速打通不同技术治理体系之间的差异。
  • 9 在 Mesh 和 非Mesh 模式之间快速切换。

有人可能据此误以为 Service Mesh 技术可将业务和服务框架的复杂性消灭于无形,将 Istio + sidecar 形式为代表的服务网格可以定义为 Service Mesh 的终极形态。

2 Sidecar 与中间件


Proxy Service Mesh 的数据面的 sidecar 仅具备通信能力,业务应用和 sidecar 之间仍然存在一个 gap:微服务时代应用所使用的中间件系统的能力需要沉淀到 sidecar 中。

2.1 sidecar 的能力

Proxy Service Mesh 中 sidecar 的一个典型代表是 envoy,其本质是一个具备通信能力的 local proxy,具备如下数据面能力:

  • 流量控制
  • 序列化协议转换
  • 通信协议转换
  • 数据路由控制

实际上, envoy 仅仅是提供了这些能力的接口,至于具体的序列化协议转换、通信协议转换插件等工作还是需要相关的基础设施开发人员去做的。

除了序列化协议和通信协议转换,中间件的其他能力向 Proxy 下沉的过程中,都需要做大量的 envoy filter 层面的工作。相对于 Proxyless 形式的 Service Mesh,Proxy Service Mesh 成本没有任何变化,其对业务的侵入性也没有减轻更多。

2.2 协议下沉

如果说 Proxyless Service Mesh 形态下的 SDK 升级需要业务层面做很多改造,Proxy Service Mesh 形态下的业务从 Proxyless 向 Proxy 形态升级过程中改造成本也不可谓不小。如果通信协议采用了 Protobuf V3,Proxy Serivce Mesh 形态下的业务升级协议时可能做到平滑升级:只升级 Proxy 不用升级业务服务实体。但现实是一些公司内部的私有协议根本做不到向后兼容,其成本就是升级协议时服务网格的 Proxy 和业务实体一起升级,至于期望的平滑升级就很难做到了。

诸般问题的核心是 Local Proxy 没有统一通信协议和序列化协议,仅仅注重于流量劫持。

2.3 有状态的应用

Service Mesh 技术的一些鼓吹者假设服务实体是一个无状态的服务,其代理也当然是一个无状态的 sidecar,然后宣传 Service Mesh 时代的各种红利。

至于那些有状态的应用,大公司有人有钱耗费巨量人日成本后,把状态转移到 Proxy 中去,让应用无状态,但业务无感知的平滑升级是就难做到了。这个成本对大经济实体来说当然是可负担且收益甚大的,但不是一些中小厂家所能承受的,他们在踩完各种坑后,可能神奇的发现他们根本没有实力实现有状态的 sidecar。

2.4 复杂性守恒

总结一番,基于 sidecar 搭建一个 Proxy Service Mesh 可能存在如下工作:

  • 不同序列化协议转换
  • 不同通信协议之间的转换
  • 同一序列化协议多版本之间的转换
  • 私有和公开通用的序列化协议之间的转换
  • 对单体时代或者微服务时代的业务进行改造升级
  • 业务应用和 sidecar 同时升级带来的额外运营维护
  • 有状态的 Sidecar 的开发测试与维护
  • 应用和代理的优雅退出和平滑升级
  • 不同技术体系之间的互通
  • Mesh 模式和非 Mesh 模式之间的切换

面对这些难度层层递进的工作,中小厂家很有可能就会退缩到微服务技术体系。其最根本的原因是:同等级别的业务形态下,技术复杂性守恒,总成本守恒。一如微服务技术至于单体技术:各个单一服务都被拆分的足够简单,但单体技术的复杂性转换为了巨量服务之间的复杂性。

3 另一种 Mesh


以 Service Mesh 技术为代表的的云原生技术体系根植于以 k8s 为代表的的云原生基础设施之上。

3.1 协议标准化

云原生中间件 Proxy 的下一站除了被动地去兼容各种协议,更应该主动的去统一序列化协议,如自身直接支持 Protobuf v3 协议,业务也采用这种可保证向后兼容的协议即可。

除了被动地去兼容各种通信框架做互联互通,可以更主动地向事实上的通信框架 gRPC 或者通信协议 HTTP 靠拢。gRPC 有各种语言的 SDK 库。各种语言自身会主动提供 HTTP 通信协议库,且 HTTP2.0 向后兼容 HTTP 1.1。例如微软的 Dapr,就直接提供 gRPC 和 HTTP 两种通信协议的 API,上层业务需要做的是选择其中一种通信协议的 API 进行开发。

3.2 Service Proxy

一些的云原生时代的事实上的标准通信设施有:2008 年开源的 protobuf v2 和 2014 年开源的 protobuf v3 统一了序列化协议;2016 年 gRPC 发布 v1 之后逐渐成了跨语言首选的通信库。

除了序列化协议和通信协议,微服务时代的中间件体系大概有如下技术栈:

  • RPC,其代表是 Dubbo/Spring Cloud/gRPC 等。
  • 限流熔断等流控,如 hystrix/sentinel 等。
  • Cache,其代表是 Redis。
  • MQ,其代表有 kafka/rocketmq 等。
  • 服务跟踪,如兼容 Opentracing 标准的各种框架。
  • 日志收集,如 Flume/Logtail 等。
  • 指标收集,如 prometheus。
  • 事务框架,如阿里的 seata。
  • 配置下发,如 apollo/nacos。
  • 服务注册,如 zookeeper/etcd 等。
  • 流量控制,如 hystrix/sentinel 等。
  • 搜索,如 ElasticSearch。
  • 流式计算,如 spark/flink。

把各种技术栈统一到一种事实上的技术标准,才能反推定义出不可变的中间件设施的终态。把上面这些事实上的中间件梳理一番后,整体工作即是:

  • 统一定义各服务的标准模型
  • 定义这些标准模型的可适配多种语言的 API
  • 一个具备通信和中间件标准模型 API 的 Proxy
  • 适配这些 API 的业务

上图定义一种不同于 istio + Envoy 的另一种可能的 Proxy Service Mesh 进化路径。该 Service Mesh 模型下的 sidecar 不仅仅是一个 Local Proxy,更应该是一个提供各个中间件技术栈标准能力的 Service Proxy。

Service Proxy 可能是一个集状态管理、event 传递、消息收发、分布式追踪、搜索、配置管理、缓存数据、旁路日志传输等诸多功能于一体的 Proxy, 也可能是分别提供部分服务的多个 Proxy 的集合,但对上提供的各个服务的 API 是不变的。

3.3 Application Mesh

或者更进一步,可以把 Service Proxy 拆分为两个 Proxy:

  • 仍然以现有的以劫持网络流量的 sidecar 形式存在的 Local Proxy。
  • 另一个专门提供各个 Service API 的 Application Proxy。

Application Proxy 可以是一个独立存在的进程或者容器,也可以是一个可被应用调用嵌入式的 SDK 库。无论是 Proxy 形式还是 Proxyless 形式,这都是一种新形态的 Service Mesh,可被称之为 Application Mesh。

Application Proxy 可以依赖于 Local Proxy,亦可不依赖。如人们常说的三级缓存其实也是一种 Application Mesh 形态,从进程内的 local cache 到本机(容器、虚拟机 or 物理机)cache proxy 一直回溯到 cache cluster, 应用只需要从 local cache 处获取数据即可。

当然,Application Mesh 亦可不依赖于特定的基础设施平台,包括 k8s,本文就不展开讨论了。

除了 Service Mesh 技术带来的收益,Application Mesh 具有如下更多的收益:

  • 更好的扩展性与向后兼容性

    基于标准的 API,其服务的不变性得到极大改善,不变性可以确保中间件的向后兼容与更好的扩展能力。基于标准 API,第三方服务商可以在云厂商提供的基础设施之上扩展出更多形态的中间件设施。

  • 与语言无关

    统一序列化协议和通信协议后,无特定语言依赖是很自然的事情。主流语言都会支持 HTTP 协议,gRPC 库自身能够提供主流语言的支持。无特定语言绑定带来的另一个间接好处是更好的可移植性。

  • 与云平台无关

    通过标准的服务让应用做到无云平台依赖,统一了中间件技术栈的平台提供的技术能力相同的,云平台使用者没有被云服务提供商绑架的危险。

  • 应用与中间件更彻底地解耦

    通过标准协议和标准 API 让应用与中间件彻底解耦,让开发者在开发阶段对新类型 Proxy 的有感,做到业务上线后业务对 Proxy及其底层系统的升级过程无感知,即更好地平滑升级。

  • 应用开发更简单

    基于标准 API,应用开发者甚至可以用单体时代的服务框架开发分布式应用,提升应用版本迭代速度。

  • 更快的启动速度

    状态从应用转移到代理后,可以通过提前预热代理,让应用瞬时批量启动。

4 未来的收益


任何技术都不是没有代价的。诚如微服务技术带来了服务数量的剧增,Service Mesh 技术带来了吞吐的降低和延时的增加,但下一站的云原生中间件形态会带来的是另一种新的价值形态,相比而言这个代价是可以接受的。

4.1 业务价值

就其浅显的技术价值而言,做到基础中间件技术的统一后,即可做到无关语言,无关各个具体的中间件实体。减轻业务开发人员负担,使其专注于业务逻辑,做到真正的快速迭代与平滑升级,提升研发效率的同时降低各种成本。

4.2 云平台无关

新形态带来的商业价值就是无云平台依赖,各平台间相互之间的竞争就不会停留在某种独有的核心技术优势上,而是在同一技术体系下不断降低服务成本,提供更好的用户体验、更强的服务能力与更亲民的价格。

能够且愿意实现这种终态 proxy 的组织当然不是各中小型业务厂家,所以统一了这些标准服务 API 的 Proxy 之下的应该是提供这些标准服务的各大云厂商。越早向统一服务模型靠拢的云厂商越快得利,越相信自己私有服务能力的云厂商越孤立。

4.3 初创公司的机会

基于大厂提供的基础设施,可以孕育出一个独立的 service proxy 生态:一些第三方的初创厂家专职提供云原生中间件解决方案。

基于新形态的中间件方案,Low Code 或者 No Code 技术才能更好落地。单体时代的 IDE 才能更进一步 -- 分布式时代的 IDE,基于各种形态中间件的标准 API 之对这些中间件的能力进行组合,以 WYSIWYG 方式开发出分布式应用。

4.4 打破大厂内部藩篱

对一些大厂组织内部而言,可借助真正形态的 Service Mesh 技术栈可以统一大经济实体内部技术栈,打破人为的各种异构隔离,提升研发运维效率和物理资源利用效率,降低整体人力与资源的交付运维成本。

4.5 走向新时代

以统一技术形态的 Service Mesh 为基础的云原生中间件技术体系真正发起起来,在其之上的 Serverless 才有更多的落地场景,广大中小企业才能分享云原生时代的技术红利,业务开发人员的编码工作就会越来越少,编程技术也会越来越智能--从手工作坊走向大规模机器自动生产时代。

欢迎对 apache/dubbo-go 项目有兴趣的同学欢迎钉钉扫码加入交流群【或搜索钉钉群号 31363295】:

查看原文

赞 8 收藏 1 评论 0

dubbo_go 发布了文章 · 2020-11-07

Dubbo-go Server端开启服务过程

导读

导读:随着微服务架构的流行,许多高性能 rpc 框架应运而生,由阿里开源的 dubbo 框架 go 语言版本的 dubbo-go 也成为了众多开发者不错的选择。本文将介绍 dubbo-go 框架的基本使用方法,以及从 export 调用链的角度进行 server 端源码导读,希望能引导读者进一步认识这款框架。下周将发表本文的姊妹篇:《从 client 端源码导读 dubbo-go 框架》。

序言

近日阅读了部分dubbo-go源码
https://github.com/dubbogo/dubbo-go

当拿到一款框架之后,一种不错的源码阅读方式大致如下:从运行最基础的helloworld demo 源码开始,再查看配置文件,开启各种依赖服务(比如zk、consul),开启服务端,再到通过client调用服务端,打印完整请求日志和回包。调用成功之后,再根据框架的设计模型,从配置文件解析开始,自顶向下递阅读整个框架的调用栈。
对于C/S模式的rpc请求来说,整个调用栈被拆成了client和server两部分,所以可以分别从server端的配置文件解析阅读到server端的监听启动,从client端的配置文件解析阅读到一次invoker Call 调用。这样一次完整请求就明晰了起来。

1. 运行官网提供的helloworld-demo

官方demo

1.1 dubbo-go 2.7版本 QuickStart

1. 开启一个go-server服务

  • 将仓库clone 到本地
$ git clone https://github.com/dubbogo/dubbo-samples.git
  • 进入dubbo目录
$ cd dubbo-samples/golang/helloworld/dubbo

进入目录后可看到四个文件夹,分别支持go和java的client以及server,我们尝试运行一个go的server
进入app子文件夹内,可以看到里面保存了go文件。

$ cd go-server/app
  • sample文件结构:

可在go-server里面看到三个文件夹:app、assembly、profiles
其中app文件夹下保存go源码,assembly文件夹下保存可选的针对特定环境的build脚本,profiles下保存配置文件。对于dubbo-go框架,配置文件非常重要,没有文件将导致服务无法启动。

  • 设置指向配置文件的环境变量

由于dubbo-go框架依赖配置文件启动,让框架定位到配置文件的方式就是通过环境变量来找。对于server端需要两个必须配置的环境变量:CONF_PROVIDER_FILE_PATH、APP_LOG_CONF_FILE,分别应该指向服务端配置文件、日志配置文件。
在sample里面,我们可以使用dev环境,即profiles/dev/log.yml profiles/dev/server.yml 两个文件。
在app/下,通过命令行中指定好这两个文件:

$ export CONF_PROVIDER_FILE_PATH="../profiles/dev/server.yml"
$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
  • 设置go代理并运行服务
$ go run .

如果提示timeout,则需要设置goproxy代理

$ export GOPROXY="http://goproxy.io"

再运行go run 即可开启服务

2. 运行zookeeper

安装zookeeper,并运行zkServer, 默认为2181端口

3. 运行go-client调用server服务

  • 进入go-client的源码目录
$ cd go-client/app
  • 同理,在/app下配置环境变量
$ export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"
$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"

配置go代理

$ export GOPROXY="http://goproxy.io"
  • 运行程序
$ go run .

即可在日志中找到打印出的请求结果

response result: &{A001 Alex Stocks 18 2020-10-28 14:52:49.131 +0800 CST}

同样,在运行的server中,也可以在日志中找到打印出的请求:

req:[]interface {}{"A001"}
rsp:main.User{Id:"A001", Name:"Alex Stocks", Age:18, Time:time.Time{...}

恭喜!一次基于dubbo-go的rpc调用成功。

4. 常见问题

    1. 当日志开始部分出现profiderInit和ConsumerInit均失败的日志。检查环境变量中配置路径是否正确,配置文件是否正确。
    1. 当日志中出现register失败的情况,一般为向注册中心注册失败,检查注册中心是否开启,检查配置文件中关于register的端口是否正确。
    1. sample的默认开启端口为20000,确保启动前无占用

1.2 配置环境变量

export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"

1.3 服务端源码

1. 目录结构

dubbo-go框架的example提供的目录如下:

  • app/ 文件夹下存放源码,可以自己编写环境变量配置脚本buliddev.sh
  • assembly/ 文件夹下存放不同平台的构建脚本
  • profiles/ 文件夹下存放不同环境的配置文件
  • target/ 文件夹下存放可执行文件

2. 关键源码

源码放置在app/文件夹下,主要包含server.go 和user.go 两个文件,顾名思义,server.go用于使用框架开启服务以及注册传输协议,user.go则定义了rpc-service结构体,以及传输协议的结构。
user.go

func init() {
    config.SetProviderService(new(UserProvider))
    // ------for hessian2------
    hessian.RegisterPOJO(&User{})
}
type User struct {
    Id   string
    Name string
    Age  int32
    Time time.Time
}
type UserProvider struct {
}
func (u *UserProvider) GetUser(ctx context.Context, req []interface{}) (*User, error) {

可以看到,user.go中存在init函数,是服务端代码中最先被执行的部分。User为用户自定义的传输结构体,UserProvider为用户自定义的rpc_service。
包含一个rpc函数,GetUser。
当然,用户可以自定义其他的rpc功能函数。
在init函数中,调用config的SetProviderService函数,将当前rpc_service注册在框架config上。
可以查看dubbo官方文档提供的设计图

service层下面就是config层,用户服务会逐层向下注册,最终实现服务端的暴露。
rpc-service注册完毕之后,调用hessian接口注册传输结构体User。
至此init函数执行完毕
server.go

// they are necessary:
//      export CONF_PROVIDER_FILE_PATH="xxx"
//      export APP_LOG_CONF_FILE="xxx"
func main() {
    hessian.RegisterPOJO(&User{})
    config.Load()
    initSignal()
}
func initSignal() {
    signals := make(chan os.Signal, 1)
    ...

之后执行main函数
main函数中只进行了两个操作,首先使用hessian注册组件将User结构体注册(与之前略有重复),从而可以在接下来使用getty打解包。
之后调用config.Load函数,该函数位于框架config/config_loader.go内,这个函数是整个框架服务的启动点,下面会详细讲这个函数内重要的配置处理过程。执行完Load()函数之后,配置文件会读入框架,之后根据配置文件的内容,将注册的service实现到配置结构里,再调用Export暴露给特定的registry,进而开启特定的service进行对应端口的tcp监听,成功启动并且暴露服务。
最终开启信号监听initSignal()优雅地结束一个服务的启动过程。

1.4 客户端源码

客户端包含client.go和user.go两个文件,其中user.go与服务端完全一致,不再赘述。
client.go

// they are necessary:
//      export CONF_CONSUMER_FILE_PATH="xxx"
//      export APP_LOG_CONF_FILE="xxx"
func main() {
    hessian.RegisterPOJO(&User{})
    config.Load()
    time.Sleep(3e9)
    println("\n\n\nstart to test dubbo")
    user := &User{}
    err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)
    if err != nil {
  panic(err)
    }
    println("response result: %v\n", user)
    initSignal()
}

main函数和服务端也类似,首先将传输结构注册到hessian上,再调用config.Load()函数。在下文会介绍,客户端和服务端会根据配置类型执行config.Load()中特定的函数loadConsumerConfig()和loadProviderConfig(),从而达到“开启服务”、“调用服务”的目的。
加载完配置之后,还是通过实现服务,增加函数proxy,申请registry,reloadInvoker指向服务端ip等操作,重写了客户端实例userProvider的对应函数,这时再通过调用GetUser函数,可以直接通过invoker,调用到已经开启的服务端,实现rpc过程。
下面会从server端和client端两个角度,详细讲解服务启动、registry注册、调用过程:

1.5 自定义配置文件(非环境变量)方法

1.5.1 服务端自定义配置文件

  1. var providerConfigStr = xxxxx// 配置文件内容,可以参考logclient

    • 在这里你可以定义配置文件的获取方式,比如配置中心,本地文件读取
  2. config.Load()之前设置配置,例如:
func main() {
    hessian.RegisterPOJO(&User{})
    providerConfig := config.ProviderConfig{}
    yaml.Unmarshal([]byte(providerConfigStr), &providerConfig)
    config.SetProviderConfig(providerConfig)
    defaultServerConfig := dubbo.GetDefaultServerConfig()
    dubbo.SetServerConfig(defaultServerConfig)
    logger.SetLoggerLevel("warn") // info,warn
    config.Load()
    select {
    }
}

1.5.2 客户端自定义配置文件

  1. var consumerConfigStr = xxxxx// 配置文件内容,可以参考logclient

    • 在这里你可以定义配置文件的获取方式,比如配置中心,本地文件读取
  2. config.Load()之前设置配置,例如:
func main() {
     p := config.ConsumerConfig{}
     yaml.Unmarshal([]byte(consumerConfigStr), &p)
     config.SetConsumerConfig(p)
     defaultClientConfig := dubbo.GetDefaultClientConfig()
     dubbo.SetClientConf(defaultClientConfig)
     logger.SetLoggerLevel("warn") // info,warn
     config.Load()

     user := &User{}
     err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)
     if err != nil {
         log.Print(err)
         return
     }
  log.Print(user)
}

2. server端:

服务暴露过程涉及到多次原始rpcService的封装、暴露,网上其他文章的图感觉太过笼统,我简要的绘制了一个用户定义服务的数据流图

2.1 加载配置

2.1.1 框架初始化

在加载配置之前,框架提供了很多已定义好的协议、工厂等组件,都会在对应模块init函数内注册到extension模块上,以供接下来配置文件中进行选用。
其中重要的有:

  1. 默认函数代理工厂
    common/proxy/proxy_factory/default.go
func init() {
    extension.SetProxyFactory("default", NewDefaultProxyFactory)
}

他的作用是将原始rpc-service进行封装,形成proxy_invoker,更易于实现远程call调用,详情可见其invoke函数。

  1. 注册中心注册协议
    registry/protocol/protocol.go
func init() {
    extension.SetProtocol("registry", GetProtocol)
}

他负责在将invoker暴露给对应注册中心,比如zk注册中心。

  1. zookeeper注册协议
    registry/zookeeper/zookeeper.go
func init() {
    extension.SetRegistry("zookeeper", newZkRegistry)
}

他合并了base_resiger,负责在服务暴露过程中,将服务注册在zookeeper注册器上,从而为调用者提供调用方法。

  1. dubbo传输协议
    protocol/dubbo/dubbo.go
func init() {
    extension.SetProtocol(DUBBO, GetProtocol)
}

他负责监听对应端口,将具体的服务暴露,并启动对应的事件handler,将远程调用的event事件传递到invoker内部,调用本地invoker并获得执行结果返回。

  1. filter包装调用链协议
    protocol/protocolwrapper/protocol_filter_wrapper.go
func init() {
    extension.SetProtocol(FILTER, GetProtocol)
}

他负责在服务暴露过程中,将代理invoker打包,通过配置好的filter形成调用链,并交付给dubbo协议进行暴露。
上述提前注册好的框架已实现的组件,在整个服务暴露调用链中都会用到,会根据配置取其所需。

2.1.2 配置文件

服务端需要的重要配置有三个字段:services、protocols、registries
profiles/dev/server.yaml:

registries :
  "demoZk":
    protocol: "zookeeper"
    timeout    : "3s"
    address: "127.0.0.1:2181"
services:
  "UserProvider":
    # 可以指定多个registry,使用逗号隔开;不指定默认向所有注册中心注册
    registry: "demoZk"
    protocol : "dubbo"
    # 相当于dubbo.xml中的interface
    interface : "com.ikurento.user.UserProvider"
    loadbalance: "random"
    warmup: "100"
    cluster: "failover"
    methods:
    - name: "GetUser"
      retries: 1
      loadbalance: "random"
protocols:
  "dubbo":
    name: "dubbo"
    port: 20000

其中service指定了要暴露的rpc-service名("UserProvider),暴露的协议名("dubbo"),注册的协议名("demoZk"),暴露的服务所处的interface,负载均衡策略,集群失败策略,调用的方法等等。
其中中间服务的协议名需要和registries下的mapkey对应,暴露的协议名需要和protocols下的mapkey对应
可以看到上述例子中,使用了dubbo作为暴露协议,使用了zookeeper作为中间注册协议,并且给定了端口。如果zk需要设置用户名和密码,也可以在配置中写好。

2.1.3 配置文件的读入和检查

config/config_loader.go:: Load()
在上述example的main函数中,有config.Load()函数的直接调用,该函数执行细节如下:

// Load Dubbo Init
func Load() {
    // init router
    initRouter()
    // init the global event dispatcher
    extension.SetAndInitGlobalDispatcher(GetBaseConfig().EventDispatcherType)
    // start the metadata report if config set
    if err := startMetadataReport(GetApplicationConfig().MetadataType, GetBaseConfig().MetadataReportConfig); err != nil {
  logger.Errorf("Provider starts metadata report error, and the error is {%#v}", err)
  return
    }
    // reference config
    loadConsumerConfig()
    // service config
    loadProviderConfig()
    // init the shutdown callback
    GracefulShutdownInit()
}

在本文中,我们重点关心loadConsumerConfig()和loadProviderConfig()两个函数
对于provider端,可以看到loadProviderConfig()函数代码如下:

前半部分是配置的读入和检查,进入for循环后,是单个service的暴露起始点。
前面提到,在配置文件中已经写好了要暴露的service的种种信息,比如服务名、interface名、method名等等。在图中for循环内,会将所有service的服务依次实现。
for循环的第一行,根据key调用GetProviderService函数,拿到注册的rpcService实例,这里对应上述提到的init函数中用户手动注册的自己实现的rpc-service实例:

这个对象也就成为了for循环中的rpcService变量,将这个对象注册通过Implement函数写到sys(ServiceConfig类型)上,设置好sys的key和协议组,最终调用了sys的Export方法。
此处对应流程图的部分:

至此,框架配置结构体已经拿到了所有service有关的配置,以及用户定义好的rpc-service实例,他触发了Export方法,旨在将自己的实例暴露出去。这是Export调用链的起始点。

2.2 原始service封装入proxy_invoker

config/service_config.go :: Export()
接下来进入ServiceConfig.Export()函数:
这个函数进行了一些细碎的操作,比如为不同的协议分配随机端口,如果指定了多个中心注册协议,则会将服务通过多个中心注册协议的registryProtocol暴露出去,我们只关心对于一个注册协议是如何操作的。还有一些操作比如生成调用url和注册url,用于为暴露做准备。

2.2.1 首先通过配置生成对应registryUrl和serviceUrl

registryUrl是用来向中心注册组件发起注册请求的,对于zookeeper的话,会传入其ip和端口号,以及附加的用户名密码等信息
这个regUrl目前只存有注册(zk)相关信息,后续会补写入ServiceIvk,即服务调用相关信息,里面包含了方法名,参数等...

2.2.2 对于一个注册协议,将传入的rpc-service实例注册在common.ServiceMap

这个Register函数将服务实例注册了两次,一次是以Interface为key写入接口服务组内,一次是以interface和proto为key写入特定的一个唯一的服务。
后续会从common.Map里面取出来这个实例。

2.2.3 获取默认代理工厂,将实例封装入代理invoker

// 拿到一个proxyInvoker,这个invoker的url是传入的regUrl,这个地方将上面注册的service实例封装成了invoker
// 这个GetProxyFactory返回的默认是common/proxy/proxy_factory/default.go
// 这个默认工厂调用GetInvoker获得默认的proxyInvoker,保存了当前注册url
invoker := extension.GetProxyFactory(providerConfig.ProxyFactory).GetInvoker(*regUrl)
// 暴露出来 生成exporter,开启tcp监听
// 这里就该跳到registry/protocol/protocol.go registryProtocol 调用的Export,将当前proxyInvoker导出
exporter = c.cacheProtocol.Export(invoker)

这一步的GetProxyFactory("default")方法获取默认代理工厂,通过传入上述构造的regUrl,将url封装入代理invoker。
可以进入common/proxy/proxy_factory/default.go::ProxyInvoker.Invoke()函数里,看到对于common.Map取用为svc的部分,以及关于svc对应Method的实际调用Call的函数如下:

到这里,上面GetInvoker(*regUrl)返回的invoker即为proxy_invoker,他封装好了用户定义的rpc_service,并将具体的调用逻辑封装入了Invoke函数内。

为什么使用Proxy_invoker来调用?
我认为,通过这个proxy_invoke调用用户的功能函数,调用方式将更加抽象化,可以在代码中看到,通过ins和outs来定义入参和出参,将整个调用逻辑抽象化为invocation结构体,而将具体的函数名的选择,参数向下传递,reflect反射过程封装在invoke函数内,这样的设计更有利于之后远程调用。我认为这是dubbo Invoke调用链的设计思想。

至此,实现了图中对应的部分:

2.3 registry协议在zkRegistry上暴露上面的proxy_invoker

上面,我们执行到了exporter = c.cacheProtocol.Export(invoker)
这里的cacheProtocol为一层缓存设计,对应到原始的demo上,这里是默认实现好的registryProtocol
registry/protocol/protocol.go:: Export()
这个函数内构造了多个EventListener,非常有java的设计感。
我们只关心服务暴露的过程,先忽略这些监听器。

2.3.1 获取注册url和服务url

2.3.2 获取注册中心实例zkRegistry

一层缓存操作,如果cache没有需要从common里面重新拿zkRegistry

2.3.3 zkRegistry调用Registry方法,在zookeeper上注册dubboPath

上述拿到了具体的zkRegistry实例,该实例的定义在
registry/zookeeper/registry.go

该结构体组合了registry.BaseRegistry结构,base结构定义了注册器基础的功能函数,比如Registry、Subscribe等,但在这些默认定义的函数内部,还是会调用facade层(zkRegistry层)的具体实现函数,这一设计模型能在保证已有功能函数不需要重复定义的同时,引入外层函数的实现,类似于结构体继承却又复用了代码。
这一设计模式我认为值得学习。
我们查看上述registry/protocol/protocol.go:: Export()函数,直接调用了:

// 1. 通过zk注册器,调用Register()函数,将已有@root@rawurl注册到zk上
    err := reg.Register(*registeredProviderUrl)

将已有RegistryUrl注册到了zkRegistry上。
这一步调用了baseRegistry的Register函数,进而调用zkRegister的DoRegister函数,进而调用:

在这个函数里,将对应root创造一个新的节点

并且写入具体node信息,node为url经过encode的结果,包含了服务端的调用方式。
这部分的代码较为复杂,具体可以看baseRegistry的 processURL()函数
至此,将服务端调用url注册到了zookeeper上,而客户端如果想获取到这个url,只需要传入特定的dubboPath,向zk请求即可。目前client是可以获取到访问方式了,但服务端的特定服务还没有启动,还没有开启特定协议端口的监听,这也是registry/protocol/protocol.go:: Export()函数接下来要做的事情:

2.3.4 proxy_invoker 封装入wrapped_invoker,得到filter调用链

// invoker封装入warppedInvoker
wrappedInvoker := newWrappedInvoker(invoker, providerUrl)
// 经过为invoker增加filter调用链,再使用dubbo协议Export,开启service并且返回了Exporter 。
// export_1
cachedExporter = extension.GetProtocol(protocolwrapper.FILTER).Export(wrappedInvoker)

新建一个WrappedInvoker,用于之后链式调用
拿到提前实现并注册好的ProtocolFilterWrapper,调用Export方法,进一步暴露。
protocol/protocolwrapped/protocol_filter_wrapper.go:Export()

protocol/protocolwrapped/protocol_filter_wrapper.go:buildInvokerChain

可见,根据配置的内容,通过链式调用的构造,层层将proxy_invoker包裹在调用链的最底部,最终返回一个调用链invoker。
对应图中部分:

至此,我们已经拿到filter调用链,期待将这个chain暴露到特定端口,用于相应请求事件

2.3.5 通过dubbo协议暴露wrapped_invoker

protocol/protocolwrapped/protocol_filter_wrapper.go:Export()

// 通过dubbo协议Export  dubbo_protocol调用的 export_2
    return pfw.protocol.Export(invoker)

回到上述Export函数的最后一行,调用了dubboProtocol的Export方法,将上述chain真正暴露。
该Export方法的具体实现在
protocol/dubbo/dubbo_protocol.go: Export()

这一函数做了两个事情:构造触发器、启动服务

  1. 将传入的Invoker调用chain进一步封装,封装成一个exporter,再将这个export放入map保存。注意!这里昂exporter放入了SetExporterMap中,在下面服务启动的时候,会以注册事件监听器的形式将这个exporter取出!
  2. 调用dubboProtocol的openServer方法,开启一个针对特定端口的监听

如上图所示,一个Session被传入,开启对应端口的事件监听。
至此构造出了exporter,完成图中部分:

2.4 注册触发动作

上述只是启动了服务,但还没有看到触发事件的细节,点进上面的s.newSession可以看到,dubbo协议为一个getty的session默认使用了如下配置

其中很重要的一个配置是EventListener,传入的是dubboServer的默认rpcHandler
protocol/dubbo/listener.go:OnMessage()
rpcHandler有一个实现好的OnMessage函数,根据getty的API,当client调用该端口时,会触发OnMessage

// OnMessage notified when RPC server session got any message in connection
func (h *RpcServerHandler) OnMessage(session getty.Session, pkg interface{}) {

这一函数实现了在getty session接收到rpc 调用后的一系列处理:

  • 传入包的解析

  • 根据请求包构造请求url

  • 拿到对应请求key,找到要被调用的exporter

  • 拿到对应的Invoker

  • 构造invocation

  • 调用

  • 返回

整个被调过程一气呵成。实现了从getty.Session的调用事件,到经过层层封装的invoker的调用。
至此,一次rpc调用得以正确返回。

3. 小结

  • 关于Invoker的层层封装
    能把一次调用抽象成一次invoke,能把一个协议抽象成针对invoke的封装,能把针对一次invoke所做出的特定改变封装到invoke函数内部,可以降低模块之间的耦合性。层层封装逻辑更加清晰
  • 关于URL的抽象
    关于dubbo的统一化请求对象URL的极度抽象是我之前没有见过的... 我认为这样封装能保证请求参数列表的简化和一致。但我认为在开发的过程中,滥用极度抽象的接口可能造成...debug的困难?以及不知道哪些字段是当前已经封装好的,哪些字段是无用的。
  • 关于协议的理解
    之前理解的协议还是太过具体化了,而关于dubbo-go对于dubboProtocol的协议,我认为是基于getty的进一步封装,他定义了客户端和服务端,对于getty的session应该有哪些特定的操作,从而保证主调和被调的协议一致性,而这种保证也是一种协议的体现,是由dubbo协议来规范的。

如果你有任何疑问,欢迎钉钉扫码加入交流群【钉钉群号 23331795】:

作者简介

李志信 (GitHubID LaurenceLiZhixin),中山大学软件工程专业在校学生,擅长使用 Java/Go 语言,专注于云原生和微服务等技术方向。

查看原文

赞 0 收藏 0 评论 0

dubbo_go 发布了文章 · 2020-09-19

解构 Dubbo-go 的核心注册引擎 Nacos

近几年,随着Go语言社区逐渐发展和壮大,越来越多的公司开始尝试采用Go搭建微服务体系,也涌现了一批Go的微服务框架,如go-micro、go-kit、Dubbo-go等,跟微服务治理相关的组件也逐渐开始在Go生态发力,如Sentinel、Hystrix等都推出了Go语言版本,而作为微服务框架的核心引擎--注册中心,也是必不可缺少的组件,市面已经有多款注册中心支持Go语言,应该如何选择呢?我们可以对目前主流的支持Go语言的注册中心做个对比。

                   图1

根据上表的对比我们可以从以下几个维度得出结论:

  • 生态:各注册中心对Go语言都有支持,但是Nacos、 Consul、Etcd 社区活跃,zookeeper和Eureka社区活跃度较低;
  • 易用性:Nacos、Eureka、Consul都有现成的管控平台,Etcd、zookeeper本身作为kv存储,没有相应的管控平台,Nacos支持中文界面,比较符合国人使用习惯;
  • 场景支持:CP模型主要针对强一致场景,如金融类,AP模型适用于高可用场景,Nacos可以同时满足两种场景,Eureka主要满足高可用场景,Consul、Zookeepr、Etcd主要满足强一致场景,此外Nacos支持从其它注册中心同步数据,方便用户注册中心迁移;
  • 功能完整性:所有注册中心都支持健康检查,Nacos、Consul支持的检查方式较多,满足不同应用场景,Zookeeper通过keep alive方式,能实时感知实例变化;Nacos、Consul和Eureka都支持负载均衡策略,Nacos通过Metadata selector支持更灵活的策略;此外,Nacos、Eureka都支持雪崩保护,避免因为过多的实例不健康对健康的实例造成雪崩效应。

综合上面各维度的对比,可以了解到Nacos作为注册中心有一定的优势,那么它对Go微服务生态的集成做得如何?接下来我们首先探索下Nacos是如何与Dubbo-go集成。

引言

Dubbo-go目前是Dubbo多语言生态中最火热的一个项目,从2016年发布至今,已经走过5个年头。最近,Dubbo-go发布了v1.5版本,全面兼容Dubbo 2.7.x版本,支持了应用维度的服务注册与发现,和主流的注册模型保持一致,标志着Dubbo-go向云原生迈出了关键的一步。作为驱动服务运转的核心引擎--注册中心,在切换到应用维度的注册模型后,也需要做相应的适配,本文将解析如何以Nacos为核心引擎实现应用维度的服务注册与发现,并且给出相应的实践案例。此外,本文代码基于Dubbo-go v1.5.1,Nacos-SDK-go v1.0.0和Nacos v1.3.2。

服务注册与发现架构

从架构中,我们可以看到,与接口级别的服务注册发现不同的是,Dubbo-go的provider启动后会调用Nacos-go-sdk的RegisterInstance接口向Nacos注册服务实例,注册的服务名即为应用名称,而不是接口名称。Conusmer启动后则会调用Subscribe接口订阅该应用的服务实例变化,并对的实例发起服务调用。

                        图2

服务模型

图3是我们Dubbo-go的应用维度服务发现模型,主要有服务和实例两个层级关系,服务实例的属性主要包含实例Id、主机地址、服务端口、激活状态和元数据。图4为Nacos的服务分级存储模型,包含服务、集群和实例三个层次。两者对比,多了一个集群维度的层级,而且实例属性信息能够完全匹配。所以在Dubbo-go将应用服务实例注册到Nacos时,我们只需要将集群设置为默认集群,再填充服务和实例的相关属性,即可完成服务模型上的匹配。此外Nacos可以将服务注册到不同的Namespace下,实现多租户的隔离。

                           图3

                           图4

服务实例心跳维持

Dubbo-go的Provider在向Nacos注册应用服务实例信息后,需要主动上报心跳,让Nacos服务端感知实例的存活与否,以判断是否将该节点从实例列表中移除。维护心跳的工作是在Nacos-SDK-go完成的,从图5代码中可以看到,当Dubbo-go调用RegisterInstance注册一个服务实例时,SDK除了调用Nacos的Register API之外,还会调用AddBeatInfo,将服务实例信息添加到本地缓存,通过后台协程定期向Nacos发送服务实例信息,保持心跳。当服务下线时,可以通过调用DeRegisterInstance执行反注册,并移除本地的心跳保持任务,Nacos实例列表中也会将该实例移除。

                        图5

订阅服务实例变化

Dubbo-go的Consumer在启动的时候会调用Nacos-SDK-go的Subscribe接口,该接口入参如图6,订阅的时候只需要传递ServiceName即应用名和回调函数SubscribeCallback,Nacos在服务实例发生变化的时候即可通过回调函数通知Dubbo-go。Nacos-SDK-go是如何感知Nacos的服务实例变化的呢?主要有两种方式:

  • Nacos服务端主动推送,Nacos-SDK-go在启动的时候会监听一个UDP端口,该端口在调用Nacos Register API的时候作为参数传递,Nacos会记录Ip和端口,当服务实例发生变化时,Nacos会对所有监听该服务的Ip和端口发送UDP请求,推送变化后的服务实例信息。
  • Nacos-SDK-go定期查询,SDK会对订阅的服务实例定时调用查询接口,如果查询有变化则通过回调接口通知Dubbo-go。作为兜底策略保证Nacos服务端推送失败后,仍能感知到变化。

                                      图6
    

此外Nacos-SDK-go还支持推空保护,当Nacos推送的实例列表为空时,不更新本地缓存,也不通知Dubbo-go变更,避免Consumer无可用实例调用,造成故障。同时,SDK还支持服务实例信息本地持久化存储,可以保证在Nacos服务故障过程中,Consumer重启也能获取到可用实例,具备容灾效果。

范例实践

环境准备

dubbo-go samples代码下载:https://github.com/apache/dub...,基于Nacos注册中心的应用级服务发现的hello world代码目录在 registry/servicediscovery/nacos。

                                图7

Nacos服务端搭建,参考官方文档:https://nacos.io/zh-cn/docs/q...,或者使用官方提供的公共Nacos服务:http://console.nacos.io/nacos...:nacos,仅供测试),或者购买阿里云服务:https://help.aliyun.com/docum...

Server端搭建

进入registry/servicediscovery/nacos/go-server/profiles文件,可以看到有dev、release和test三个文件夹,分别对应开发、测试和生产配置。我们使用dev配置来搭建开发环境,dev文件下有log.yml和server.yml文件,下面对server.yml配置进行修改。

remote配置,这里使用公共的Nacos服务,address支持配置多个地址,用逗号分割。params参数配置nacos-sdk的日志目录。

remote:
  nacos:
    address: "console.nacos.io:80"
    timeout: "5s"
    params:
        logDir: "/data/nacos-sdk/log"
configCenter配置
config_center:
  protocol: "nacos"
  address: "console.nacos.io:80"

配置server端环境变量

export CONF_PROVIDER_FILE_PATH=server端的server.yml文件路径
export APP_LOG_CONF_FILE=server端的log.yml文件路径

进入registry/servicediscovery/nacos/go-server/app,运行server.go的main方法,可以从Nacos的控制台(http://console.nacos.io/nacos...

看到,应用user-info-server已经注册成功。

Client端搭建

client的配置文件在registry/servicediscovery/nacos/go-server/profiles目录下,需要修改的地方跟server端一样,这里不赘述。

配置client端环境变量

export CONF_CONSUMER_FILE_PATH=client端的server.yml文件路径
export APP_LOG_CONF_FILE=client端的log.yml文件路径

进入registry/servicediscovery/nacos/go-client/app,运行client.go的main方法,看到如下日志输出,表示调用server端成功。

作者:李志鹏

Github账号:Lzp0412,Nacos-SDK-go作者,Apache/Dubbo-go Contributor。现就职于阿里云云原生应用平台,主要参与服务发现、CoreDNS、ServiceMesh相关工作,负责推动Nacos Go微服务生态建设。

相关链接
Nacos-SDK-go项目地址:https://github.com/nacos-grou...
Nacos golang生态交流群:23191211
Nacos项目地址:https://nacos.io/
Nacos社区交流群:30438813

Dubbo-go 项目地址:https://github.com/apache/dub...
Dubbo-go社区交流群:23331795

查看原文

赞 0 收藏 0 评论 0

dubbo_go 发布了文章 · 2020-09-17

What’s New in Dubbo-go-hessian2 v1.7.0

发版人: 望哥

Dubbo-go-hessian2 v1.7.0已发布,详见 https://github.com/apache/dub..., 以下对这次更新内容进行详细整理。

另外v1.6.3 将 attachment 类型由 map[string]stiring 改为map[string]interface{} 导致版本不兼容问题,这部分已还原,后续的计划是将dubbo协议的request/response对象整体迁移到dubbogo项目中进行迭代修改, hessian2中将不再改动到request/response对象。

1. New Features

1.1 add GetStackTrace method into Throwabler and its implements. #207

contributed by https://github.com/cvictory

go语言client请求java语言服务时,如果java语言抛出了异常,异常对应的堆栈信息是被保存在StackTraceElement中。

这个异常信息在日志中最好能被打印出来,以方便客户端排查问题,所以在Throwabler和对应子类中增加了StackTraceElement的获取。

注:其实还有一种更好的方法,所有的具体的异常类型都包含java_exception/exception.go的Throwable struct。这样只需要在Throwable中增加GetStackTrace方法就可以了。但是这种方式需要更多的测试验证,改动的逻辑相对会复杂一些。但是代码会更整洁。 这里先不用这种方法。

1.2 catch user defined exceptions. #208

contributed by https://github.com/cvictory

golang中增加一个java中Exception对象的序列化输出方法:

func JavaException() []byte {
    e := hessian.NewEncoder()
    exception := java_exception.NewException("java_exception")
    e.Encode(exception)
    return e.Buffer()
}

在output/output.go 提供调用入口:添加如下函数初始化声明

func init() {
    funcMap["JavaException"] = testfuncs.JavaException
}

java代码中增加调用go方法序列化结果:
说明: Assert.assertEquals 不能直接比较Exception对象是否相等

    /**
     * test java java.lang.Exception object and go java_exception Exception struct
     */
    @Test
    public void testException() {
        Exception exception = new Exception("java_exception");
        Object javaException = GoTestUtil.readGoObject("JavaException");
        if (javaException instanceof Exception) {
            Assert.assertEquals(exception.getMessage(), ((Exception) javaException).getMessage());
        }
    }

1.3 support java8 time object. #212, #221

contributed by https://github.com/willson-chen, https://github.com/cyb-code

golang中增加一个java8对象的序列化输出方法:

// test java8 java.time.Year
func Java8TimeYear() []byte {
    e := hessian.NewEncoder()
    year := java8_time.Year{Year: 2020}
    e.Encode(year)
    return e.Buffer()
}

// test java8 java.time.LocalDate
func Java8LocalDate() []byte {
    e := hessian.NewEncoder()
    date := java8_time.LocalDate{Year: 2020, Month: 9, Day: 12}
    e.Encode(date)
    return e.Buffer()
}

在output/output.go 提供调用入口:添加函数初始化声明

func init() {
    funcMap["Java8TimeYear"] = testfuncs.Java8TimeYear
    funcMap["Java8LocalDate"] = testfuncs.Java8LocalDate
}

java代码中增加调用go方法序列化结果:

/**
 * test java8 java.time.* object and go java8_time/* struct
 */
@Test
public void testJava8Year() {
    Year year = Year.of(2020);
    Assert.assertEquals(year
            , GoTestUtil.readGoObject("Java8TimeYear"));
    LocalDate localDate = LocalDate.of(2020, 9, 12);
    Assert.assertEquals(localDate, GoTestUtil.readGoObject("Java8LocalDate"));
}

1.4 support test golang encoding data in java. #213

contributed by https://github.com/wongoo

为了更好的测试验证hessian库,原来已经支持在golang中测试java的序列化数据,现在增加在java中测试golang的序列化数据,实现双向测试验证。

golang中增加序列化输出方法:

func HelloWorldString() []byte {
    e := hessian.NewEncoder()
    e.Encode("hello world")
    return e.Buffer()
}

将该方法注册到output/output.go中

 // add all output func here
 func init() {
     funcMap["HelloWorldString"] = testfuncs.HelloWorldString
}

output/output.go 提供调用入口:

func main() {
    flag.Parse()

    if *funcName == "" {
        _, _ = fmt.Fprintln(os.Stderr, "func name required")
        os.Exit(1)
    }
    f, exist := funcMap[*funcName]
    if !exist {
        _, _ = fmt.Fprintln(os.Stderr, "func name not exist: ", *funcName)
        os.Exit(1)
    }

    defer func() {
        if err := recover(); err != nil {
            _, _ = fmt.Fprintln(os.Stderr, "error: ", err)
            os.Exit(1)
        }
    }()
    if _, err := os.Stdout.Write(f()); err != nil {
        _, _ = fmt.Fprintln(os.Stderr, "call error: ", err)
        os.Exit(1)
    }
    os.Exit(0)
}

java代码中增加调用go方法序列化结果:

public class GoTestUtil {

    public static Object readGoObject(String func) {
        System.out.println("read go data: " + func);
        try {
            Process process = Runtime.getRuntime()
                    .exec("go run output/output.go -func_name=" + func,
                            null,
                            new File(".."));

            int exitValue = process.waitFor();
            if (exitValue != 0) {
                Assert.fail(readString(process.getErrorStream()));
                return null;
            }

            InputStream is = process.getInputStream();
            Hessian2Input input = new Hessian2Input(is);
            return input.readObject();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private static String readString(InputStream in) throws IOException {
        StringBuilder out = new StringBuilder();
        InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
        char[] buffer = new char[4096];

        int bytesRead;
        while ((bytesRead = reader.read(buffer)) != -1) {
            out.append(buffer, 0, bytesRead);
        }

        return out.toString();
    }
}

增加java测试代码:

@Test
public void testHelloWordString() {
    Assert.assertEquals("hello world"
            , GoTestUtil.readGoObject("HelloWorldString"));
}

1.5 support java.sql.Time & java.sql.Date. #219

contributed by https://github.com/zhangshen023

增加了 java 类 java.sql.Time, java.sql.Date 支持,分别对应到hessian.Time 和 hessian.Date, 详见 https://github.com/apache/dub...

2. Enhancement

2.1 Export function EncNull. #225

contributed by https://github.com/cvictory

开放 hessian.EncNull 方法,以便用户特定情况下使用。

3. Bugfixes

3.1 fix enum encode error in request. #203

contributed by https://github.com/pantianying

原来在 dubbo request 对象中没有判断 enum 类型的情况,此pr增加了判断是不是POJOEnum类型。详见 https://github.com/apache/dub...

3.2 fix []byte field decoding issue. #216

contributed by https://github.com/wongoo

v1.7.0 之前如果 struct中包含[]byte字段时无法反序列化, 报错“error list tag: 0x29”,主要原因是被当做list进行处理,对于这种情况应该按照binary数据进行处理即可。

type Circular struct {
    Num      int
    Previous *Circular
    Next     *Circular
    ResponseDataBytes    []byte // <---- 
}

func (Circular) JavaClassName() string {
    return "com.company.Circular"
}

3.3 fix decoding error for map in map. #229

contributed by https://github.com/wongoo

v1.7.0 之前嵌套map无法正确解析,主要原因是对应的map对象被当做一个数据类型却未被自动加到类引用列表中,而嵌套map类信息是同一类型的引用,去类引用列表找,找不到就报错了。 解决这个问题的方法就是遇到map类对象,也将其加入到类引用列表中即可。 问题详细参考 #119.

3.4 fix fields name mismatch in Duration class. #234

contributed by https://github.com/skyao

这个 PR 解决了Duration对象中字段错误定义,原来是"second/nano", 应该是"seconds/nanos"。

同时改善了测试验证数据。之前使用0作为int字段的测试数据,这是不准确的,因为int类型默认值就是0.

如有疑问,欢迎加入 dubbogo 社区【钉钉群号 23331795】讨论。

钉钉群二维码:
image

查看原文

赞 0 收藏 0 评论 0

dubbo_go 发布了文章 · 2020-09-12

What's new in Dubbo-go v1.5.1

近期我们发布了 dubbo-go v1.5.1,虽然是 v1.5 的一个子版本,但相比于 v1.5.0, 社区还是投入了很大人力添加了如下重大改进。

1 应用维度注册模型


在新模型 release 后,我们发现 Provider 每个 URL 发布元数据都会注册 ServiceInstance,影响性能需要优化。


我们的优化方案是:
去除 ServiceDiscoveryRegistry 中注册 ServiceInstance 的代码,在 config_loader 中的loadProviderConfig 方法的最后注册 ServiceInstance
具体步骤:
1、获取所有注册的 Registry,过滤出 ServiceDiscoveryRegistry,拿取所有 ServiceDiscovery。
2、创建 ServiceInstance。
3、每个 ServiceDiscovery 注册 ServiceInstance。


保证 Provider 在注册成功之后,才暴露元数据信息。

2 支持基于 Seata 的事务


基于 Seata 扩展实现。通过增加过滤器,在服务端接收  xid 并结合 seata-golang 达到支持分布式事务的目的。 从而使 Dubbo-go 在分布式场景下,让用户有更多的选择,能适应更多的个性化场景。


我们在 dubbo-samples 中给出了 事务测试用例

3 多注册中心集群负载均衡


对于多注册中心订阅的场景,选址时的多了一层注册中心集群间的负载均衡:


在 Cluster Invoker 这一级,我们支持的选址策略有:

  • 指定优先级
  • 同 zone 优先
  • 权重轮询

3 传输链路安全性


该版本在传输链路的安全性上做了尝试,对于内置的 Dubbo getty Server 提供了基于 TLS 的安全链路传输机制。


为尽可能保证应用启动的灵活性,TLS Cert 的指定通过配置文件方式,具体请参见 Dubbo-go 配置读取规则与 TLS 示例:

4 路由功能增强


本次路由功能重点支持了 动态标签路由 和 应用/服务级条件路由。

4.1 动态标签路由


标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。


标签主要是指对 Provider 端应用实例的分组,目前有两种方式可以完成实例分组,分别是动态规则打标静态规则打标,其中动态规则相较于静态规则优先级更高,而当两种规则同时存在且出现冲突时,将以动态规则为准。

4.1.1 动态规则打标


可随时在服务治理控制台下发标签归组规则

# governance-tagrouter-provider应用增加了两个标签分组tag1和tag2
# tag1包含一个实例 127.0.0.1:20880
# tag2包含一个实例 127.0.0.1:20881
---
  force: false
  runtime: true
  enabled: true
  key: governance-tagrouter-provider
  tags:
    - name: tag1
      addresses: ["127.0.0.1:20880"]
    - name: tag2
      addresses: ["127.0.0.1:20881"]
 ...

4.1.2 静态规则打标


可以在 server 配置文件的 tag 字段里设置

services:
  "UserProvider":
    registry: "hangzhouzk"
    protocol : "dubbo"
    interface : "com.ikurento.user.UserProvider"
    loadbalance: "random"
    warmup: "100"
    tag: "beijing"
    cluster: "failover"
    methods:
    - name: "GetUser"
      retries: 1
      loadbalance: "random"


consumer  添加 tag 至 attachment 即可

ctx := context.Background()
attachment := make(map[string]string)
attachment["dubbo.tag"] = "beijing"
ctx = context.WithValue(ctx, constant.AttachmentKey, attachment)
err := userProvider.GetUser(ctx, []interface{}{"A001"}, user)


请求标签的作用域为每一次 invocation,使用 attachment 来传递请求标签,注意保存在 attachment 中的值将会在一次完整的远程调用中持续传递,得益于这样的特性,我们只需要在起始调用时,通过一行代码的设置,达到标签的持续传递。

4.1.3 规则详解

格式

  • Key明确规则体作用到哪个应用。必填
  • enabled=true 当前路由规则是否生效,可不填,缺省生效。
  • force=false 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 false
  • runtime=false 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 false
  • priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0
  • tags 定义具体的标签分组内容,可定义任意n(n>=1)个标签并为每个标签指定实例列表。必填

    • name, 标签名称
  • addresses, 当前标签包含的实例列表

降级约定

  1. request.tag=tag1 时优先选择 标记了 tag=tag1 的 provider。若集群中不存在与请求标记对应的服务,默认将降级请求 tag 为空的 provider;如果要改变这种默认行为,即找不到匹配 tag1 的 provider 返回异常,需设置request.tag.force=true
  2. request.tag 未设置时,只会匹配 tag 为空的 provider。即使集群中存在可用的服务,若 tag 不匹配也就无法调用,这与约定 1 不同,携带标签的请求可以降级访问到无标签的服务,但不携带标签/携带其他种类标签的请求永远无法访问到其他标签的服务。

4.2 应用/服务级条件路由


您可以在路由规则配置中配置多个条件路由及其粒度


Sample:

# dubbo router yaml configure file
routerRules: 
  - scope: application
    key: BDTService
    priority: 1
    enable: false
    force: true
    conditions : ["host = 192.168.199.208 => host = 192.168.199.208 "]
  - scope: service
    key: com.ikurento.user.UserProvider
    priority: 1
    force: true
    conditions : ["host = 192.168.199.208 => host = 192.168.199.208 "]

4.2.1 规则详解

各字段含义

  • scope表示路由规则的作用粒度,scope的取值会决定key的取值。必填。

    • service 服务粒度
    • application 应用粒度
  • Key明确规则体作用在哪个服务或应用。必填。

    • scope=service时,key取值为[{group}/]{service}[:{version}]的组合
    • scope=application时,key取值为application名称
  • enabled=true 当前路由规则是否生效,可不填,缺省生效。
  • force=false 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 false。
  • runtime=false 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 false。
  • priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。
  • conditions 定义具体的路由规则内容。必填。


5 回顾与展望


Dubbo-go 处于一个比较稳定成熟的状态。目前新版本正处于往云原生方向的尝试,应用服务维度注册是首先推出的功能,这是一个和之前模型完全不一样的新注册模型。该版本是我们朝云原生迈进新一步的关键版本。除此之外,包含在该版本也有一些之前提到的优化。


下一个版本 v1.5.2,本次的关注重点以通信模型改进为主,除此之外,与 2.7.x 的兼容性、易用性及质量保证也是本次关注的信息。**


服务发现,会支持更加多的方式,如:文件、Consul。 从而使 Dubbo-go 在服务发现场景下,让用户有更多的选择,能适应更多的个性化场景。


另外 易用性及质量保证,主要关注的是 samples 与自动化构建部分。可降低用户上手 Dubbo-go 的难度,提高代码质量。


目前下一个版本正在紧锣密鼓的开发中,具体规划及任务清单[1] ,都已经在 Github 上体现。


[1] : https://github.com/apache/dubbo-go/projects/10

image

查看原文

赞 0 收藏 0 评论 0

dubbo_go 发布了文章 · 2020-09-12

Dubbo-go应用维度注册模型


本文作者: 白泽(蒋超),Github ID @Patrick0308,开源爱好者。

Dubbo 3.0 将至。其最重要的一点就是服务自省,其基础即是应用维度的注册模型,作为目前与 Dubbo 在功能上完全对齐的 Dubbo-go,已于 本年【2020 年】7 月份发布了其 v1.5.0 版本,实现了该模型,为年底实现与 Dubbo 3.0 对齐的新版本奠定了基础。


Dubbo-go 作为 Dubbo 的 Go 语言版本,因跨语言之故,二者针对同一模型的实现必然有较大差异,故本文注重讨论 Dubbo-go 社区自身对该模型的理解和实现,以及其与 Dubbo 之间的差异。

1 引语


在 v1.5 以前,Dubbo-go 注册模型都是以服务为维度的,直观的理解可认为其是接口维度。譬如注册信息,按照服务维度模型其示例如下:

"com.xxx.User":[
  {"name":"instance1", "ip":"127.0.0.1", "metadata":{"timeout":1000}},
  {"name":"instance2", "ip":"127.0.0.2", "metadata":{"timeout":2000}},
  {"name":"instance3", "ip":"127.0.0.3", "metadata":{"timeout":3000}}, 
]


这种模式的好处是不言而喻的,简单直观,提供了细粒度的服务控制手段。


而近两年,随着云时代的到来,这种模式就暴露了不足:

  1. 主流的注册模型都是应用维度的;
  2. 以服务维度来注册,那么规模与服务数量成正比,大规模集群之下,注册中心压力非常大;

2 Dubbo-go v1.5.0 的新注册模型


这次 Dubbo-go 支持了新的注册模型,也就是应用维度的注册模型。简单而言,在应用维度注册下,其注册信息类似:

"application1": [
  {"name":"instance1", "ip":"127.0.0.1", "metadata":{}},
  {"name":"instance2", "ip":"127.0.0.2", "metadata":{}},
  {"name":"instanceN", "ip":"127.0.0.3", "metadata":{}}
]


在此模式之下,可以看到注册信息将会大幅度减少,集群规模只与实例数量相关。


与此同时,在实现这一个功能的时候,Dubbo-go 还希望保持两个目标:

  1. 对用户完全兼容,用户迁移无感知;
  2. 保持住原本服务粒度上精细控制的能力——即保留现有的服务维度的元数据;


因此 Dubbo-go 要着力解决以下几点:

  1. 目前 Consumer 的配置是以接口为准的,如何根据接口找到该接口对应的应用?例如,用户配置了 com.xxx.User 服务,那么,Dubbo-go 怎么知道这个服务是由哪个应用来提供的呢?
  2. 在知道了是哪个应用之后,可以从注册中心拿到应用的注册信息,如实例信息等;那怎么知道 com.xxx.User 服务自身的元数据呢?


为了解决这两个问题,在已有的注册模型的基础上,Dubbo-go 引入两个额外的组件:ServiceNameMapping 和 MetadataService。


前者用于解决服务-应用之间的映射,后者用于获取服务的元数据。


由此,Dubbo-go 的应用维度注册模型就变为:


image.png

2.1 ServiceNameMapping

ServiceNameMapping 并不复杂。考虑到一般人在 Consumer 侧想要调用一个服务,其十有八九是知道这个服务是哪个应用提供的,于是 Dubbo-go 引入了新的配置项 provideBy


image.png


当然,所谓 “十有八九”就是说有些时候确实不知道是服务是谁提供的,所以 Dubbo-go 还支持了基于配置中心的 ServiceNameMapping 实现。Dubbo-go 会用服务名作为 Key 从配置中心里面读出对应的应用名。这意味着, Provider 启动的时候,也会在配置中心将自身的 服务-应用名映射 写入配置中心。

2.2 MetadataService


MetadataService 稍微要复杂一点,有 remotelocal 两种模式。


类似于前面的 ServiceNameMapping,Dubbo-go 提供了基于配置中心的 MetadataService 的实现,即 remote 模式。Provider 启动的时候,就会将服务的元数据写进去。


另外一种模式是 local 模式。Dubbo-go 可以直接将 MetadataService 看做是一个普通的微服务,而后由 Provider 所提供。类似于:


image.png


由此带来一个问题:


既然 Dubbo-go 将 MetadataService 看做是一个普通的服务,那么 MetadataService 的元数据,Consumer 该怎么获得呢?这是一个典型的鸡生蛋蛋生鸡的问题。


Dubbo-go 的方案非常简单粗暴,Provider 启动的时候,不仅仅往注册中心里面写入应用本身的信息,还要把它的 MetadataService 信息写入。


这是一个应用的注册信息:

image.png


本质上来说,应用维度注册信息 + 服务元数据 = 服务维度注册信息。或者说,应用维度注册,只是一种重新组织这些信息的方式。

3 差异与改进


Dubbo-go v1.5.x 对标 Dubbo 2.7.5,可以认为是参照 Dubbo 2.7.5 直接实现其 Go 源码,但是考虑到 Java 和 Go 之间的语言差异,导致二者之间的实现不可能完全对等。

3.1 修订版本号revision比对

Dubbo v2.7.x 在 MetadataService 注册时,会对其 provider 应用的所有服务接口的 hash 值做为修订版本号写入元数据中心,此 revision 是对所有接口的方法以及其参数总体的计算结果。其目的是减少 consumer 端到注册中心的拉取次数。


在Go中用的计算 revision 的 hash 算法与 Java 是不一致的,而且 Go 与 Java 的方法签名信息是不相同的,所以计算出来的 hash 值一定是不一样的。


此不一致会导致如果Go应用和Java应用同时发布同一个服务的时候,Go服务和Java服务的修订版本号必定是不相同的,Consumer需要分别缓存这两个修订版本的元数据。

3.2 应用注册时机


Dubbo-go v1.5.0 实现时,其中一个考量是全面向后兼容 v1.4.x。Dubbo-go v1.5.x 应用 consumer 既可以调用 Dubbo-go v1.4.x 应用的服务,也可以调用 Dubbo v2.6.x 应用的服务,当然也可以调用其对标的 v2.7.x 应用的服务。


为了达到兼容性,Dubbo-go v1.5.x 实现时面临一个问题:Dubbo-go provider 应用启动时有一个服务启动成功,把应用信息注册到元数据中心之后,就会把实例注册到注册中心,而 Dubbo 2.7.x 的 provider 应用则是在其所有服务接口的信息注册到元数据中心后才会注册实例!


这个问题的后果就是:Dubbo-go v1.5.0 的 provider 每次发布接口到元数据中心的同时,都会触发Dubbo-go v1.5.0 / Dubbo v2.7.x 的 consumer 应用拉取 Dubbo-go v1.5.0 应用信息,当provider 发布的服务过多时 consumer 侧性能损耗非常明显!


Dubbo-go 在 v1.5.1 中已经修复了这个问题,provider 在启动时先将其全部服务接口发布到元数据中心,然后注册实例到注册中心,减少了 consumer 拉取元数据的次数。

image

查看原文

赞 0 收藏 0 评论 0

dubbo_go 发布了文章 · 2020-07-27

What's New in Dubbo-go v1.5

What's New in Dubbo-go v1.5

引语

计算机技术浪潮每 10 年都有一次技术颠覆,相关知识体系最迟每 5 年都会革新一次,大概每两年贬值一半,在应用服务通信框架领域亦然。凡是有长期生命的通信框架,大概有 5 年的成长期和 5 年的稳定成熟期。每个时代都有其匹配的应用通信框架,在 20 年前的 2G 时代,强跨语言跨平台而弱性能的 gRPC 是不会被采用的。

每个通信框架,不同的人从不同角度看出不同的结论:初学者看重易用易学,性能测评者注重性能,应用架构师考虑其维护成本,老板则考虑则综合成本。一个应用通信框架的性能固然重要,其稳定性和进化能力更重要,得到有效维护的框架可在长时间单位内降低其综合成本:学习成本、维护成本、升级成本和更换成本。

什么是 Dubbo-go?第一,它是 Dubbo 的 Go 语言版本,全面兼容 Dubbo 是其第一要义。第二,它是一个 Go 语言应用通信框架,会充分利用作为云原生时代第一语言---Go 语言的优势,扩展 dubbo 的能力。

2008 年诞生的 Dubbo 已有十多年历史,依靠阿里和其社区,历久弥新。2016 年发布的 Dubbo-go 也已进入第五个年头,如今全面兼容 Dubbo v2.7.x 的 Dubbo-go v1.5 终于发布了。

回首过往,Dubbo-go 已经具备如下能力:

  • 互联互通:打通了 gRPC 和 Spring Cloud 生态;
  • 可观测性:基于 OpenTracing  和 Prometheus,使得其在 Logging、Tracing 和 Metrics 方面有了长足进步;
  • 云原生:Dubbo-go 实现了基于 Kubernetes API Server 为注册中心的通信能力,做到了升级成本最低。

毋庸讳言,相较于现有成绩,发展阶段的 Dubbo-go 对未来有更多的期待之处:

  • 易用性:Dubbo-go 的入门成本并不低,把很多感兴趣者挡在了门外。但好消息是,随着 Dubbo-go 在阿里内部的逐步推开,阿里中间件团队对其进行了进一步的封装,经生产环境检验后会开放给社区使用。
  • 云原生:目前的 Dubbo-go 的基于 kubernetes 的方案,从技术分层角度来看, Kubernetes API Server 终究是系统的运维态组件,不应该暴露给应用层,否则会造成 APIServer 自身通信压力过大,且系统整体风险很高:应用层使用不当,或者框架自身的流量方面的 bug,可能会把 APiServer 打垮,后果就是造成整体后端服务能力的瘫痪!所以应用层需要感知的是 kubernetes 提供给应用层的 Operator,不断进化的 Dubbo-go 计划在 v1.6 版本中发布 Dubbo-go Operator。

雄关漫道真如铁,而今迈步从头越。Dubbo-go 社区【钉钉群 23331795】与 Dubbo-go 同在。

应用维度注册模型

经过一段时间的努力之后,我们终于完成了应用维度的服务注册与发现。和原本已有的接口维度的注册模型比起来,新的注册模型有两个突出特点:

  1. 和主流的注册模型保持一致。目前的主流做法都是按照应用为基本单位来进行注册的,如Spring Cloud。在支持应用维度注册之后,对于接下来的云原生支持,奠定了基础;
  2. 大幅度减轻对注册中心的压力。在该模型之下,从注册中心的视角看过去,集群规模只和实例数量成正比,而不是现有的和服务数量成正比;

当然,我们在设计的时候就考虑到了用户的迁移成本。要迁移到新的注册模型,只需要将现有使用的注册中心换成新的 ServiceDiscoveryRegistry 就可以。

ServiceDiscoveryRegistry 是支持多种实现的。目前来说,我们支持:

  1. nacos;
  2. etcd;
  3. zookeeper;

我们提倡新上线的业务尽量使用 nacos 和 etcd 这种更可靠稳定的注册中心。

Metadata Report 元数据中心

v1.5 版本在支持应用维度注册模型时,有很重要的一个问题需要解决,即接口维度的元数据存储。服务维度的注册模型和应用维度的注册模型,本质的区别是往注册中心注册的数据维度的不一致。虽然我们在应用维度注册模型中,将接口维度的数据从注册中心中剔除了,但是在rpc的框架中,一个 consumer 要想真正找到想要调用的服务地址,就必须得到 provider 端开放的服务信息。这部分数据,在 v1.5 版本中,我们将它们存储到了元数据中心中。
元数据中心,是一个接口定义。泛指一块存储区域,可以对接口级别的元数据进行存储、读取,provider 端调用存储,consumer 端调用读取。元数据中心中的数据需要保持准确性、实时性。
目前元数据中心,有两个父类(Go 中没有继承,此处说的父子类,单纯指子类对父类的组合关系)实现,一个是 local 实现,一个是 remote 实现。local 实现是将 provider 的内存作为虚拟元数据中心,remote 实现是指依赖 ZooKeeper、etcd、nacos 等注册中心作为元数据中心。目前 remote 有 zookeeper、nacos、etcd 和 consul 的子类实现。即用户可以将元数据信息,通过以上的第三方注册中心进行数据存储和分发。

Invocation 接口支持 attribute 属性

invocation 结构中新增 attribute 属性支持,用于流程内部的属性存储。和 attachment 不同点在于,attachment会从 consumer 传递到 provider,但 attribute 属性不会。

k8s注册中心

  在 v1.5 版本之前,k8s 注册中心的实现是通过直接使用 [k8s client](https://github.com/kubernetes/client-go) 中Pod对象的 List&&Watch 接口。在本次迭代中引入了 k8s informer。这样做的原因在于两点,首先一定的程度上来讲 dubbo-go 的 k8s 注册中心也是一个 k8s controller,使用 informer 的模式更加 k8s native。更重要的是社区计划后续向 CRD+Operator 的模式演进,informer 模式是对后续的演进的探索。除了这个铺垫之外,本次迭代还对跨 namespace 的服务发现做了支持。再有就是为了减少对 kube-apiserver List&&Watch 的压力,对 provider 和 consumer 的行为进行了区分,provider 不再进行 Watch 而仅对 kube-apiserver 进行写操作。

优化路由模型

在 1.5 版本之前,Router 模型中属性是包含:优先级与路由属性,Router Chain 只包含路由属性。从中能识别出其实 Router Chain 也是一种特殊 Router。1.5 版本之后,使 Router 更抽象,分离出其优先级属性,新增 Priority Router、Chain 继承 Router 使其变为特殊的 Router,使关系上看起来更加清晰。如下图:

回顾与展望

Dubbo-go 处于一个比较稳定成熟的状态。目前新版本正处于往云原生方向的尝试,应用服务维度注册是首先推出的功能,这是一个和之前模型完全不一样的新注册模型。该版本是我们朝云原生迈进新一步的关键版本。除此之外,包含在该版本也有一些之前提到的优化。

下一个版本 v1.5.1,虽然仍是以兼容 Dubbo 2.7.x 为主要任务,但在分布式能力的增强上,也是我们关注的重点。

分布式事务方面,有一个重要的基于 Seata 扩展实现。通过增加过滤器,在服务端接收 xid 并结合 seata-golang[2] 达到支持分布式事务的目的。 从而使 Dubbo-go 在分布式场景下,让用户有更多的选择,能适应更多的个性化场景。

与此同时,在传输链路安全性上,TLS 安全传输链路是该版本重要功能之一。通过提供统一入口,未来能引入更多的与传输链路安全性相关的功能,适应用户不一样的使用场景。

注册中心模型上,支持多注册中心集群负载均衡。业务部署假设是双注册中心(图 1 ),从原来双注册中心中所有 Provider 一起选址。优化成选址时的多了一层注册中心集群间的负载均衡(图 2 )。

(图 1 )

(图 2 )

以前的 dubbo-go RPC 层直接复用了 getty 框架 的 RPC[3],未能实现协议和应用通信地址的隔离。阿里中间件展图同学重构了 dubbo-go RPC 层,实现了连接复用:可以实现 consumer 与 provider 端的同一个 TCP 连接上进行多协议通信。相关 PR 业已合并,会在 dubbo-go v1.5.1 中发布。

目前下一个版本正在紧锣密鼓的开发中,具体规划及任务清单[1] ,都已经在 Github 上体现。

[1] : https://github.com/apache/dubbo-go/projects/8

[2] : https://github.com/seata-golang/seata-golang

[3] : https://github.com/AlexStocks/getty/tree/master/rpc

查看原文

赞 0 收藏 0 评论 0

dubbo_go 发布了文章 · 2020-06-16

涂鸦智能 dubbo-go 亿级流量的实践与探索

涂鸦智能 dubbo-go 亿级流量的实践与探索

dubbo 是一个基于 Java 开发的高性能的轻量级 RPC 框架,dubbo 提供了丰富的服务治理功能和优秀的扩展能力。而 dubbo-go 在 java 与 golang 之间提供统一的服务化能力与标准,是涂鸦智能目前最需要解决的主要问题。本文分为实践和快速接入两部分,分享在涂鸦智能的 dubbo-go 实战经验,意在帮助用户快速接入 dubbo-go RPC 框架,希望能让大家少走些弯路。

另外,文中的测试代码基于 dubbo-go版本 v1.4.0

dubbo-go 网关实践

image.png

dubbo-go 在涂鸦智能的使用情况如上图,接下来会为大家详细介绍落地细节,希望这些在生产环境中总结的经验能够帮助到大家。

背景

在涂鸦智能,dubbo-go 已经作为了 golang 服务与原有 dubbo 集群打通的首选 RPC 框架。其中比较有代表性的 open-gateway 网关系统(下文统一称 gateway,开源版本见 https://github.com/dubbogo/dubbo-go-proxy)。该 gateway 动态加载内部 dubbo 接口信息,以HTTP API 的形式对外暴露。该网关意在解决上一代网关的以下痛点。

  • 通过页面配置 dubbo 接口开放规则,步骤繁琐,权限难以把控。
  • 接口非 RESTful 风格,对外部开发者不友好。
  • 依赖繁重,升级风险大。
  • 并发性能问题。

架构设计

针对如上痛点,随即着手准备设计新的 gateway 架构。首先就是语言选型,golang 的协程调用模型使得 golang 非常适合构建 IO 密集型的应用,且应用部署上也较 java 简单。经过调研后我们敲定使用 golang 作为 proxy 的编码语言,并使用 dubbo-go 用于连接 dubbo provider 集群。provider 端的业务应用通过使用 java 的插件,以注解形式配置 API 配置信息,该插件会将配置信息和 dubbo 接口元数据更新到元数据注册中心(下图中的 redis )。这样一来,配置从管理后台页面转移到了程序代码中。开发人员在编码时,非常方便地看到 dubbo 接口对外的 API 描述,无需从另外一个管理后台配置 API 的使用方式。

实践

从上图可以看到,网关能动态加载 dubbo 接口信息,调用 dubbo 接口是基于 dubbo 泛化调用。泛化调用使 client 不需要构建 provider 的 interface 代码,在 dubbo-go 中表现为无需调用 config.SetConsumerService 和 hessian.RegisterPOJO 方法,而是将请求模型纯参数完成,这使得 client 动态新增、修改接口成为可能。在 apache/dubbo-sample/golang/generic/go-client 中的有泛化调用的演示代码。

func test() {
    var appName = "UserProviderGer"
    var referenceConfig = config.ReferenceConfig{
        InterfaceName: "com.ikurento.user.UserProvider",
        Cluster:       "failover",
        Registry:      "hangzhouzk",
        Protocol:      dubbo.DUBBO,
        Generic:       true,
    }
    referenceConfig.GenericLoad(appName) // appName is the unique identification of RPCService

    time.Sleep(3 * time.Second)
  
    resp, err := referenceConfig.GetRPCService().(*config.GenericService).
        Invoke([]interface{}{"GetUser", []string{"java.lang.String"}, []interface{}{"A003"}})
    if err != nil {
        panic(err)
    }
}

泛化调用的实现其实相当简单。其功能作用在 dubbo 的 Filter 层中。Generic Filter 已经作为默认开启的 Filter 加入到 dubbo Filter 链中。其核心逻辑如下:

func (ef *GenericFilter) Invoke(ctx context.Context, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result {
    if invocation.MethodName() == constant.GENERIC && len(invocation.Arguments()) == 3 {
        oldArguments := invocation.Arguments()

        if oldParams, ok := oldArguments[2].([]interface{}); ok {
            newParams := make([]hessian.Object, 0, len(oldParams))
            for i := range oldParams {
                newParams = append(newParams, hessian.Object(struct2MapAll(oldParams[i])))
            }
            newArguments := []interface{}{
                oldArguments[0],
                oldArguments[1],
                newParams,
            }
            newInvocation := invocation2.NewRPCInvocation(invocation.MethodName(), newArguments, invocation.Attachments())
            newInvocation.SetReply(invocation.Reply())
            return invoker.Invoke(ctx, newInvocation)
        }
    }
    return invoker.Invoke(ctx, invocation)
}

Generic Filter 将用户请求的结构体参数转化为统一格式的 map(代码中的 struct2MapAll ),将类( golang 中为 struct )的正反序列化操作变成 map 的正反序列化操作。这使得无需 POJO 描述通过硬编码注入 hessain 库。

从上面代码可以看到,泛化调用实际需要动态构建的内容有 4 个,ReferenceConfig 中需要的 InterfaceName 、参数中的 method 、ParameterTypes、实际入参 requestParams。

那么这些参数是如何从 HTTP API 匹配获取到的呢?

这里就会用到上文提到的 provider 用于收集元数据的插件。引入插件后,应用在启动时会扫描需要暴露的 dubbo 接口,将 dubbo 元数据和 HTTP API 关联。插件使用方法大致如下,这里调了几个简单的配置作为示例,实际生产时注解内容会更多。

image-20200609112413216.png

最终获得的 dubbo 元数据如下:

{
    "key": "POST:/hello/{uid}/add",
    "interfaceName": "com.tuya.hello.service.template.IUserServer",
    "methodName": "addUser",
    "parameterTypes": ["com.tuya.gateway.Context", "java.lang.String", "com.tuya.hello.User"],
    "parameterNames": ["context", "uid", "userInfo"],
    "updateTimestamp": "1234567890",
    "permissionDO":{},
    "voMap": {
        "userInfo": {
            "name": "java.lang.String",
            "sex": "java.lang.String",
            "age": "java.lang.Integer"
        }
    },
    "parameterNameHumpToLine": true,
    "resultFiledHumpToLine": false,
    "protocolName": "dubbo",
  .......
}

Gateway 从元数据配置中心订阅到以上信息,就能把一个 API 请求匹配到一个 dubbo 接口。再从 API 请求中抓取参数作为入参。这样功能就完成了流量闭环。

以上内容,大家应该对此 gateway 的项目拓扑结构有了清晰的认知。我接着分享项目在使用 dubbo-go 过程中遇到的问题和调优经验。19 年初,当时的 dubbo-go 项目还只是构建初期,没有什么用户落地的经验。我也是一边参与社区开发,一边编码公司内部网关项目。在解决了一堆 hessain 序列化和 zookeeper 注册中心的问题后,项目最终跑通了闭环。但是,作为一个核心应用,跑通闭环离上生产环境还有很长的路要走,特别是使用了当时稳定性待测试的新框架。整个测试加上功能补全,整整花费了一个季度的时间,直到项目趋于稳定,压测效果也良好。单台网关机器( 2C 8G )全链路模拟真实环境压测达到 2000 QPS。由于引入了比较重的业务逻辑(单个请求平均调用 3 个 dubbo 接口),对于这个压测结果,是符合甚至超出预期的。

总结了一些 dubbo-go 参数配置调优的经验,主要是一些网络相关配置。大家在跑 demo 时,应该会看到配置文件最后有一堆配置,但如果对 dubbo-go 底层网络模型不熟悉,就很难理解这些配置的含义。目前 dubbo-go 网络层以 getty 为底层框架,实现读写分离和协程池管理。getty 对外暴露 session 的概念,session 提供一系列网络层方法注入的实现,因为本文不是源码解析文档,在这里不过多论述。读者可以简单的认为 dubbo-go 维护了一个 getty session池,session 又维护了一个 TCP 连接池。对于每个连接,getty 会有读协程和写协程伴生,做到读写分离。 这里我尽量用通俗的注释帮大家梳理下对性能影响较大的几个配置含义:

protocol_conf:
  # 这里是协议独立的配置,在dubbo协议下,大多数配置即为getty session相关的配置。
  dubbo:
      # 一个session会始终保证connection_number个tcp连接个数,默认是16,
    # 但这里建议大家配置相对小的值,一般系统不需要如此多的连接个数。
    # 每隔reconnect_interval时间,检查连接个数,如果小于connection_number,
    # 就建立连接。填0或不填都为默认值300ms
    reconnect_interval: 0
    connection_number: 2
    # 客户端发送心跳的间隔
    heartbeat_period: "30s"
    # OnCron时session的超时时间,超过session_timeout无返回就关闭session
    session_timeout: "30s"
    # 每一个dubbo interface的客户端,会维护一个最大值为pool_size大小的session池。
    # 每次请求从session池中select一个。所以真实的tcp数量是session数量*connection_number,
    # 而pool_size是session数量的最大值。测试总结下来一般程序4个tcp连接足以。
    pool_size: 4
    # session保活超时时间,也就是超过session_timeout时间没有使用该session,就会关闭该session
    pool_ttl: 600
    # 处理返回值的协程池大小
    gr_pool_size: 1200
    # 读数据和协程池中的缓冲队列长度,目前已经废弃。不使用缓冲队列
    queue_len: 64
    queue_number: 60
    getty_session_param:
      compress_encoding: false
      tcp_no_delay: true
      tcp_keep_alive: true
      keep_alive_period: "120s"
      tcp_r_buf_size: 262144
      tcp_w_buf_size: 65536
      pkg_wq_size: 512
      tcp_read_timeout: "1s"  # 每次读包的超时时间
      tcp_write_timeout: "5s" # 每次写包的超时时间
      wait_timeout: "1s" 
      max_msg_len: 102400     # 最大数据传输长度
      session_name: "client"

dubbo-go 快速接入

前文已经展示过 dubbo-go 在涂鸦智能的实践成果,接下来介绍快速接入 dubbo-go 的方式。

第一步:hello world

dubbo-go 使用范例目前和 dubbo 一致,放置在 apache/dubbo-samples 项目中。在 dubbo-sample/golang 目录下,用户可以选择自己感兴趣的 feature 目录,快速测试代码效果。

tree dubbo-samples/golang -L 1
dubbo-samples/golang
├── README.md
├── async
├── ci.sh
├── configcenter
├── direct
├── filter
├── general
├── generic
├── go.mod
├── go.sum
├── helloworld
├── multi_registry
└── registry

我们以 hello world 为例,按照 dubbo-samples/golang/README.md 中的步骤,分别启动 server 和 client 。可以尝试 golang 调用 java 、 java 调用 golang 、golang 调用 golang 、java 调用 java。dubbo-go 在协议上支持和 dubbo 互通。

我们以启动 go-server 为例,注册中心默认使用 zookeeper 。首先确认本地的 zookeeper 是否运行正常。然后执行以下命令,紧接着你就可以看到你的服务正常启动的日志了。

export ARCH=mac
export ENV=dev
cd dubbo-samples/golang/helloworld/dubbo/go-server
sh ./assembly/$ARCH/$ENV.sh
cd ./target/darwin/user_info_server-2.6.0-20200608-1056-dev/
sh ./bin/load.sh start

第二步:在项目中使用 dubbo-go

上面,我们通过社区维护的测试代码和启动脚本将用例跑了起来。接下来,我们需要在自己的代码中嵌入 dubbo-go 框架。很多朋友往往是在这一步遇到问题,这里我整理的一些常见问题,希望能帮到大家。

1. 环境变量

目前 dubbo-go 有 3 个环境变量需要配置。

  • CONF_CONSUMER_FILE_PATH : Consumer 端配置文件路径,使用 consumer 时必需。
  • CONF_PROVIDER_FILE_PATH:Provider 端配置文件路径,使用 provider 时必需。
  • APP_LOG_CONF_FILE :Log 日志文件路径,必需。
  • CONF_ROUTER_FILE_PATH:File Router 规则配置文件路径,使用 File Router 时需要。
2. 代码注意点
  • 注入服务 : 检查是否执行以下代码
# 客户端
func init() {
    config.SetConsumerService(userProvider)
}
# 服务端
func init() {
    config.SetProviderService(new(UserProvider))
}
  • 注入序列化描述 :检查是否执行以下代码
    hessian.RegisterJavaEnum(Gender(MAN))
    hessian.RegisterJavaEnum(Gender(WOMAN))
    hessian.RegisterPOJO(&User{})
3. 正确理解配置文件
  • `references/services 下的 key ,如下面例子的 "UserProvider" 需要和服务 Reference() 返回值保持一致,此为标识改接口的 key。
references:
  "UserProvider":
    registry: "hangzhouzk"
    protocol : "dubbo"
    interface : "com.ikurento.user.UserProvider"
    cluster: "failover"
    methods :
    - name: "GetUser"
      retries: 3
  • `注册中心如果只有一个注册中心集群,只需配置一个。多个 IP 用逗号隔开,如下:
registries :
  "hangzhouzk":
    protocol: "zookeeper"
    timeout    : "3s"
    address: "172.16.120.181:2181,172.16.120.182:2181"
    username: ""
    password: ""
4. java 和 go 的问题
  • go 和 java 交互的大小写 :golang 为了适配 java 的驼峰格式,在调用 java 服务时,会自动将 method 和属性首字母变成小写。很多同学故意将 java 代码写成适配 golang 的参数定义,将首字母大写,最后反而无法序列化匹配。

第三步:拓展功能

dubbo-go 和 dubbo 都提供了非常丰富的拓展机制。可以实现自定义模块代替 dubbo-go 默认模块,或者新增某些功能。比如实现 Cluster、Filter 、Router 等来适配业务的需求。这些注入方法暴露在 dubbo-go/common/extension 中,允许用户调用及配置。

本文作者:
潘天颖,Github ID @pantianying,开源爱好者,就职于涂鸦智能。

欢迎加入 dubbo-go 社区

有任何 dubbo-go 相关的问题,可以加我们的钉钉群 23331795 询问探讨,我们一定第一时间给出反馈。

最新活动

Dubbo-go ASoC 相关题目 ,参加详情 请点击

查看原文

赞 0 收藏 0 评论 1

dubbo_go 关注了标签 · 2020-06-16

运维

即运行维护的简称。大部分情况下这个角色包含了服务器管理员、数据库管理员、网络管理员和员工管理员。。。。。

关注 1395

认证与成就

  • 获得 8 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2020-06-16
个人主页被 1.4k 人浏览