dubbo启动服务:Dubbo-go 源码笔记 一
dubbo启动服务:Dubbo-go 源码笔记 一$ cd go-server/app进入目录后可看到四个文件夹,分别支持 go 和 java 的 client 以及 server,我们尝试运行一个 go 的 server。进入 app 子文件夹内,可以看到里面保存了 go 文件。1)开启一个 go-server 服务$ git clone https://github.com/dubbogo/dubbo-samples.git$ cd dubbo-samples/golang/helloworld/dubbo

导读:随着微服务架构的流行,许多高性能 rpc 框架应运而生,由阿里开源的 dubbo 框架 go 语言版本的 dubbo-go 也成为了众多开发者不错的选择。本文将介绍 dubbo-go 框架的基本使用方法,以及从 export 调用链的角度进行 server 端源码导读,希望能引导读者进一步认识这款框架。下周将发表本文的姊妹篇:《从 client 端源码导读 dubbo-go 框架》。
当拿到一款框架之后,一种不错的源码阅读方式大致如下:从运行最基础的 helloworld demo 源码开始 —> 再查看配置文件 —> 开启各种依赖服务(比如zk、consul) —> 开启服务端 —> 再到通过 client 调用服务端 —> 打印完整请求日志和回包。调用成功之后,再根据框架的设计模型,从配置文件解析开始,自顶向下递阅读整个框架的调用栈。
对于 C/S 模式的 rpc 请求来说,整个调用栈被拆成了 client 和 server 两部分,所以可以分别从 server 端的配置文件解析阅读到 server 端的监听启动,从 client 端的配置文件解析阅读到一次 invoker Call 调用。这样一次完整请求就明晰了起来。
运行官网提供的 helloworld-demo1. 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)常见问题
- 当日志开始部分出现 profiderInit 和 ConsumerInit 均失败的日志,检查环境变量中配置路径是否正确,配置文件是否正确。
 - 当日志中出现 register 失败的情况,一般为向注册中心注册失败,检查注册中心是否开启,检查配置文件中关于 register 的端口是否正确。
 - sample 的默认开启端口为 20000,确保启动前无占用。
 
2. 配置环境变量
export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"
    
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() 优雅地结束一个服务的启动过程。
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 注册和调用过程。
5. 自定义配置文件(非环境变量)方法
1)服务端自定义配置文件
- var providerConfigStr = xxxxx// 配置文件内容,可以参考 log 和 client。在这里你可以定义配置文件的获取方式,比如配置中心,本地文件读取。
 
log 地址:https://github.com/dubbogo/dubbo-samples/blob/master/golang/helloworld/dubbo/go-client/profiles/release/log.yml
client 地址:https://github.com/dubbogo/dubbo-samples/blob/master/golang/helloworld/dubbo/go-client/profiles/release/client.yml
- 在 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 {
    }
}
    
2)客户端自定义配置文件
- var consumerConfigStr = xxxxx// 配置文件内容,可以参考 log 和 clien。在这里你可以定义配置文件的获取方式,比如配置中心,本地文件读取。
 - 在 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)
}Server 端
    
服务暴露过程涉及到多次原始 rpcService 的封装、暴露,网上其他文章的图感觉太过笼统,在此,简要地绘制了一个用户定义服务的数据流图:

1. 加载配置
1)框架初始化
在加载配置之前,框架提供了很多已定义好的协议、工厂等组件,都会在对应模块 init 函数内注册到 extension 模块上,以供接下来配置文件中进行选用。
其中重要的有:
- 默认函数代理工厂:common/proxy/proxy_factory/default.go
 
func init() {
    extension.SetProxyFactory("default"  NewDefaultProxyFactory)
}
    
它的作用是将原始 rpc-service 进行封装,形成 proxy_invoker,更易于实现远程 call 调用,详情可见其 invoke 函数。
- 注册中心注册协议:
registry/protocol/protocol.go 
func init() {
    extension.SetProtocol("registry"  GetProtocol)
}
    
它负责将 invoker 暴露给对应注册中心,比如 zk 注册中心。
- zookeeper 注册协议:registry/zookeeper/zookeeper.go
 
func init() {
    extension.SetRegistry("zookeeper"  newZkRegistry)
}
    
它合并了 base_resiger,负责在服务暴露过程中,将服务注册在 zookeeper 注册器上,从而为调用者提供调用方法。
- dubbo 传输协议:protocol/dubbo/dubbo.go
 
func init() {
    extension.SetProtocol(DUBBO  GetProtocol)
}
    
它负责监听对应端口,将具体的服务暴露,并启动对应的事件 handler,将远程调用的 event 事件传递到 invoker 内部,调用本地 invoker 并获得执行结果返回。
- filter 包装调用链协议:protocol/protocolwrapper/protocol_filter_wrapper.go
 
func init() {
    extension.SetProtocol(FILTER  GetProtocol)
}
    
它负责在服务暴露过程中,将代理 invoker 打包,通过配置好的 filter 形成调用链,并交付给 dubbo 协议进行暴露。
上述提前注册好的框架已实现的组件,在整个服务暴露调用链中都会用到,会根据配置取其所需。
2)配置文件
服务端需要的重要配置有三个字段:services、protocols、registries。
profiles/dev/server.yml:
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 需要设置用户名和密码,也可以在配置中写好。
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() 函数代码如下:




