目录

Dubbo Go HelloWorld

Dubbo-go HelloWorld

1. 简介

随着微服务架构的流行,许多高性能 rpc 框架应运而生,dubbo是一个高性能的rpc框架,dubbo-go是阿里开源的dubbo框架的go语言版本,可以实现java和go服务之间的互相调用,本教程将介绍dubbo-go的基础用法,以及相关概念。

2 学习目标

  • 了解配置文件的使用
  • 掌握使用dubbo-go编写微服务应用
  • 了解微服务架构的相关概念

3. 详细内容

  • 快速上手
  • 运行注册中心
  • 代码解读
  • 平滑关闭

4. 准备工作

4.1 获取代码

将demo获取下来

git clone https://github.com/dubbogo/dubbo-samples.git

4.2 安装注册中心

在微服务架构中,注册中心是最核心的基础服务之一,各个服务将自己的网络地址等信息注册到注册中心,注册中心存储这些数据。服务的消费者从注册中心查询服务提供者的地址,并通过该地址调用服务提供者的接口。

不使用注册中心也可以实现微服务,但是带来的问题是如果某一个服务提供者的ip变动,或者实例有增加或销毁,服务调用者需要再手动修改提供者的网络地址。注册中心提供了服务注册与发现、服务检查等功能。服务调用者只需要通过注册中心便可以调用到服务提供者。常见的注册中心有zookeeper、consul、etcd、nacos等,本案例使用zookeeper,推荐使用docker安装,下面演示一下如何使用docker拉取镜像及运行zookeeper。

# 拉去zookeeper景象
docker pull zookeeper
# 运行
docker run -d -p 2181:2181 --name zookeeper-container --restart always zookeeper

4.3 目录结构

demo的文件目录如下:

.
├── app
├── assembly
└── profiles

app是代码目录

assembly是在不同的环境中的执行脚本

profiles是配置文件所在的文件夹

4.4 设置配置文件路径

配置文件在dubbo-go中非常重要,注册中心的网络地址、传输协议、日志表等信息都需要写在配置文件中,在项目启动的时候会先加载配置文件。因此在启动配置文件的时候,需要指定一下配置文件的位置,目前支持设置环境变量及添加启动参数的方式来指定路径。对于server端来说有两个变量必须需要设置:CONF_PROVIDER_FILE_PATH、APP_LOG_CONF_FILE,分别应该指向服务端配置文件、日志配置文件。

环境变量的使用方法如下:

$ export CONF_PROVIDER_FILE_PATH="../profiles/dev/server.yml" 
$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"

client也类似,客户端环境变量的使用方法如下

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

1.5.6版本之后可以使用命令行的方式来指定配置文件的路径

server端

$ go run . -proConf ../profiles/dev/server.yml -logConf ../profiles/dev/log.yml

client端

$ go run . -conConf ../profiles/dev/client.yml -logConf ../profiles/dev/log.yml

4.5 hessian介绍

在微服务架构中,少不了rpc,rpc即远程过程调用,使用rpc框架可以向调用方屏蔽各种复杂性,向服务提供方也屏蔽各类复杂性,使用rpc带来的好处有如下:

  • 调用方感觉就像调用本地函数一样

  • 服务提供方感觉就像实现一个本地函数一样来实现服务

下图可以更生动的描述rpc框架所起到的作用

https://cdn.cjpa.top/cdnimages/image-20210306150132695.png

rpc协议并不是一个具体的协议,可以由开发者自行设计,开发者可以自己对rpc进行封装和注册,对于数据的传输格式以及顺序也可以自己定制,对数据格式的处理需要使用到打解包工具,常见的打解包工具有Protobuf,json,hessian2。dubbo使用的是hessian。

作者在之前的项目中使用过grpc框架,grpc进行序列化需要借助proto文件,在使用的时候需要先用protoc生成开发者所需要的语言的文件,然后再进行调用这样做的好处是,以通过共用一个文件来保证各服务的一致性,但是使用起来并不是那么方便。dubbo/hession就比较简单,但是需要严格控制各个服务之间的数据格式

hessian使用非常简单,真正提供了类似于调用本地方法一样调用接口的功能 ,返回的参数不需要二次解析并且足够轻量。

5. 启动server端

在按照4.4中的说明设置好server端之后,可以执行

go run .

便可启动server端

5.1 server端配置文件讲解

服务端需要的重要配置有三个字段:services、protocols、registries。

profiles/dev/server.yml:

registries :
	# 注册中心的名字
  "demoZk":
  	# 注册中心协议
    protocol: "zookeeper"
    # 健康检查超时时间
    timeout    : "3s"
    # zookeeper的地址及端口
    address: "127.0.0.1:2181"
services:
	# 要暴露的rpc-service名
  "UserProvider":
    # 可以指定多个registry,使用逗号隔开;不指定默认向所有注册中心注册
    registry: "demoZk"
    # 暴露的协议名
    protocol : "dubbo"
    # 暴露的服务所处的 interface,相当于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

其中,中间服务的协议名需要和 registries 下的 mapkey 对应,暴露的协议名需要和 protocols 下的 mapkey 对应。

上述例子中,使用了 dubbo 作为暴露协议,使用了 zookeeper 作为中间注册协议,并且给定了端口。如果 zk 需要设置用户名和密码,也可以在配置中写好。

5.2 user.go解读

user.go定义了rpc-service结构体以及传输的数据结构

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) {
	gxlog.CInfo("req:%#v", req)
	rsp := User{"A001", "Alex Stocks", 18, time.Now()}
	gxlog.CInfo("rsp:%#v", rsp)
	return &rsp, nil
}

ser 为用户自定义的传输结构体,UserProvider 为用户自定义的 rpc_service;包含一个 rpc 函数,GetUser。

在 init 函数中,调用 config 的 SetProviderService 函数,将当前 rpc_service 注册在框架 config 上,hessian则提供了如5.2所描述的二进制编码的功能,注册传输结构体 User。

5.3 server.go解读

server.go

func main() {
   hessian.RegisterPOJO(&User{})
   config.Load()
   initSignal()
}

main 函数中只进行了两个操作,首先使用 hessian 注册组件将 User 结构体注册,从而可以在接下来使用 getty 打解包。

之后调用 config.Load 函数,该函数位于框架 config/config_loader.go 内,这个函数是整个框架服务的启动点, config.Load会从环境变量或启动参数中获取consumer的位置,然后从配置文件中把配置读入框架。

最终开启信号监听 initSignal() 优雅地结束一个服务的启动过程。后文会介绍。

6. client端

6.1 client端配置文件解读

registries :
  "demoZk":
    protocol: "zookeeper"
    timeout  : "3s"
    address: "127.0.0.1:2181"
    username: ""
    password: ""
references:
  "UserProvider":
    # 可以指定多个registry,使用逗号隔开;不指定默认向所有注册中心注册
    registry: "demoZk"
    # 协议
    protocol : "dubbo"
    # 暴露的服务所处的 interface,相当于dubbo.xml中的interface
    interface : "com.ikurento.user.UserProvider"
    cluster: "failover"
    # 调用的方法
    methods :
    - name: "GetUser"
      retries: 3

client端的配置文件和server端大体一致,其中refrences字段和server端不太一样, refrences 的字段就是对当前服务要主调的服务的配置,其中详细说明了调用协议、注册协议、接口 id、调用方法、集群策略等

6.2 user.go解读

client端的use.go的代码和server端基本一致

func init() {
	config.SetConsumerService(userProvider)
	hessian.RegisterPOJO(&User{})
}

type User struct {
	Id   string
	Name string
	Age  int32
	Time time.Time
}

type UserProvider struct {
	GetUser func(ctx context.Context, req []interface{}, rsp *User) error
}

其中User结构体一定要保持一致,如果不保持一致会导致传输失败,

6.3 client.go解读

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

client作为调用方,锁执行的步骤要稍微复杂一些,

首先也是将使用 hessian 注册组件将 User 结构体注册,然后加载配置。

userProvider中的GetUser方法可以实现对server端远程服务的调用,可以看到,有了hessian提供打解包,dubbo-go服务之间的调用非常方便。

initSignal()是平滑关闭函数,下文会有介绍。

7. 平滑关闭

initSignal

func initSignal() {
	signals := make(chan os.Signal, 1)
	// It is not possible to block SIGKILL or syscall.SIGSTOP
	signal.Notify(signals, os.Interrupt, os.Kill, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
	for {
		sig := <-signals
		logger.Infof("get signal %s", sig.String())
		switch sig {
		case syscall.SIGHUP:
			// reload()
		default:
			time.AfterFunc(time.Duration(survivalTimeout), func() {
				logger.Warnf("app exit now by force...")
				os.Exit(1)
			})
			// The program exits normally or timeout forcibly exits.
			fmt.Println("provider app exit now...")
			return
		}
	}
}

在server和client中都是用到了平滑关闭。

平滑关闭的实现原理很简单,第一步是定义一个chanel,然后监听系统中断的信号,如果收到了中断的信号,就把这个信号传递到chanel中。在接收到chanel之前,会一直循环检测,直到chanel中收到了中断参数,然后系统会执行关闭的相关函数。