Practical Guide: Integrating go-doudou with dubbo-go via gRPC
In our practice and exchange of Go language microservices, we've learned that some companies or technology teams that previously used Java are now developing microservices with the dubbo-go framework that, together with legacy Java services, form a heterogeneous system. Some technology teams also want to use the go-doudou microservice framework for agile development and rapid service delivery. But the question arises: Can go-doudou interoperate with existing dubbo ecosystem services and join the existing microservice architecture? Since version v2.0.8, go-doudou has implemented a zookeeper-based service registration and discovery mechanism that allows services written with the dubbo framework to interoperate via the gRPC protocol. This article demonstrates how to get started with the go-doudou microservice framework and achieve interoperability with services written in dubbo-go through a simple case. Example code repository address: https://github.com/unionj-cloud/go-doudou-tutorials/tree/master/dubbodemo
Project Structure Explanation
.
├── README.md
├── docker-compose.yml
├── dubbo
│ ├── go.mod
│ ├── go.sum
│ └── rpc
│ └── grpc
│ ├── go-client # dubbo gRPC service consumer
│ ├── go-server # dubbo gRPC service provider
│ ├── protobuf
│ └── service-b
├── service-a # go-doudou RESTful service a
└── service-b # go-doudou gRPC service b
2
3
4
5
6
7
8
9
10
11
12
13
14
This demo project consists of three microservices and one client program.
The three microservices are:
- service-a: A RESTful service using the go-doudou framework, demonstrating how go-doudou calls dubbo-go's gRPC service through its interface;
- service-b: A gRPC service using the go-doudou framework, used to demonstrate being called by dubbo-go's client;
- go-server: A gRPC service using the dubbo-go framework, used to demonstrate being called by go-doudou's client;
The client program is:
- go-client: A client program using the dubbo-go framework, used to demonstrate dubbo-go calling go-doudou's gRPC service;
Starting Zookeeper
We first need to start a three-node zookeeper cluster through docker-compose, by executing the command docker-compose -f docker-compose.yml up -d --remove-orphans
.
# docker-compose.yml
version: '3.1'
services:
zoo1:
image: zookeeper
restart: always
hostname: zoo1
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
zoo2:
image: zookeeper
restart: always
hostname: zoo2
ports:
- 2182:2181
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
zoo3:
image: zookeeper
restart: always
hostname: zoo3
ports:
- 2183:2181
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
After starting, we can connect to localhost:2181 using prettyZoo to view the nodes. Currently, no services are registered yet.
Starting service-b
Go to the service-b path and execute the command go run cmd/main.go
. When you see the three lines of log output in the red box below, it indicates that the service has started. Now if we look at prettyZoo again, we can see that the cloud.unionj.ServiceB_grpc service has been registered.
The node after providers,
grpc%3A%2F%2F192.168.189.126%3A50051%2Fcloud.unionj.ServiceB_grpc%3Fgroup%3Dgroup%26rootPath%3D%26version%3Dv2.2.2%26weight%3D1
, is a URL-escaped string. Before escaping, the content is grpc://192.168.189.126:50051/cloud.unionj.ServiceB_grpc?group=group&rootPath=&version=v2.2.2&weight=1
. The content and formatting rules of this node are compatible with the dubbo ecosystem, so services can discover each other. Further explanation is as follows:
grpc://
: Indicates the communication protocol, which is the gRPC protocol here. go-doudou currently only supports http and gRPC;192.168.189.126
: Indicates the service registration host, by default taking the host's private IP. This can be customized through the environment variableGDD_REGISTER_HOST
;50051
: Indicates the gRPC service port number, default 50051. This can be customized through the environment variableGDD_GRPC_PORT
;cloud.unionj.ServiceB_grpc
: Indicates the service name, formed by the user-configured service name + underscore + communication protocol. Since the go-doudou framework supports starting the same set of code to provide both http protocol RESTful services and gRPC protocol RPC services, the underscore + communication protocol is needed for distinction. In this example, the service name configured by the user through the environment variableGDD_SERVICE_NAME
is cloud.unionj.ServiceB, and go-doudou added_grpc
;group
: Indicates the service group name, which can be customized through the environment variableGDD_SERVICE_GROUP
;version
: Indicates the service version, which can be customized through the environment variableGDD_SERVICE_VERSION
. The service name + service group name + service version together uniquely identify a service, and if any one does not match, the service cannot be called;rootPath
: Indicates the interface path prefix, only valid under the http protocol;weight
: Indicates the weight of the service instance, used for client load balancing, default 1. This can be customized through the environment variableGDD_WEIGHT
;
Let's look at the RPC interface provided by ServiceB.
// svc.go
package service
import (
"context"
"service-b/dto"
)
//go:generate go-doudou svc http -c
//go:generate go-doudou svc grpc
type ServiceB interface {
GetDeptById(ctx context.Context, deptId int) (dept dto.DeptDto, err error)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
From the svc.go file, we can see that ServiceB service defines only one RPC interface, with the department ID as input and the department DTO and error as output. Let's see how the interface is implemented.
// svcimpl.go
func (receiver *ServiceBImpl) GetDeptByIdRpc(ctx context.Context, request *pb.GetDeptByIdRpcRequest) (*pb.DeptDto, error) {
return &pb.DeptDto{
Id: request.DeptId,
Name: "Test Department",
StaffTotal: 10,
}, nil
}
2
3
4
5
6
7
8
The implementation logic is very simple, the returned department name is always "Test Department", and the department ID takes the value passed in.
Starting go-server
Go to the dubbo/rpc/grpc/go-server
path and execute the command go run cmd/server.go
.
The log output when dubbo-go service starts is quite long, but seeing the log output in the screenshot above indicates that the service has started and registered successfully.
We can also see the nodes registered by dubbo-go through prettyZoo.
Regarding the usage of dubbo-go, colleagues who have used or are using dubbo-go do not need to be introduced, and it is not the focus of this article. Open the server.go file, let's look at the interface implementation provided by go-server.
type GreeterProvider struct {
pb.GreeterProviderBase
}
func (g *GreeterProvider) SayHello(ctx context.Context, req *pb.HelloRequest) (reply *pb.HelloReply, err error) {
fmt.Printf("req: %v", req)
return &pb.HelloReply{Message: "this is message from reply"}, nil
}
2
3
4
5
6
7
8
Very simple, just an RPC interface called SayHello.
Starting service-a
Go to service-a and execute the command go run cmd/main.go
.
When you see the log output as shown in the figure above, it indicates that the service has started successfully. Let's look at the service registration node through prettyZoo again.
cloud.unionj.ServiceA_rest is the node that service-a registers to zookeeper.
Let's look at the RESTful interface provided by service-a.
package service
import (
"context"
"service-a/dto"
)
//go:generate go-doudou svc http -c
//go:generate go-doudou svc grpc
type ServiceA interface {
GetUserById(ctx context.Context, userId int) (user dto.UserDto, err error)
GetRpcUserById(ctx context.Context, userId int) (user dto.UserDto, err error)
GetRpcSayHello(ctx context.Context, name string) (reply string, err error)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Let's focus on the two interfaces with the GetRpc
prefix, which are interfaces used as clients to call gRPC services. GetRpcUserById
calls the service-b service, and GetRpcSayHello
calls the go-server service. Let's continue to look at the interface implementation code of ServiceA.
var _ ServiceA = (*ServiceAImpl)(nil)
type ServiceAImpl struct {
conf *config.Config
bClient client.IServiceBClient
grpcClient pb.ServiceBServiceClient
greeterClient protobuf.GreeterClient
}
2
3
4
5
6
7
8
The ServiceA interface implementation structure ServiceAImpl has two gRPC client member variables:
- grpcClient: service-b's gRPC client
- greeterClient: go-server's gRPC client
These two clients are injected in the main.go file:
// Establish a gRPC connection to ServiceB based on zk, with built-in smooth weighted load balancing
grpcConn := zk.NewSWRRGrpcClientConn(zk.ServiceConfig{
Name: "cloud.unionj.ServiceB_grpc",
Group: "group",
Version: "v2.2.2",
}, dialOptions...)
defer grpcConn.Close()
// Connect to ServiceB's gRPC server using the gRPC connection
grpcClient := pb.NewServiceBServiceClient(grpcConn)
// Similarly, establish a gRPC connection to go-server based on zk, with built-in smooth weighted load balancing
dubbo := zk.NewSWRRGrpcClientConn(zk.ServiceConfig{
Name: "org.apache.dubbo.sample.GreeterProvider",
Group: "group",
Version: "v2.2.2",
}, dialOptions...)
defer dubbo.Close()
// Connect to go-server's gRPC server using the gRPC connection
greeterClient := protobuf.NewGreeterClient(dubbo)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
So far, the go-doudou service client has successfully connected to both the go-doudou gRPC service and the dubbo-go gRPC service. Let's look at the implementation code for GetRpcUserById
and GetRpcSayHello
:
func (receiver *ServiceAImpl) GetRpcUserById(ctx context.Context, userId int) (user dto.UserDto, err error) {
// Use the gRPC client to call service-b through the gRPC protocol
output, err := receiver.grpcClient.GetDeptByIdRpc(ctx, &pb.GetDeptByIdRpcRequest{
DeptId: int32(userId),
})
if err != nil {
return
}
user = dto.UserDto{
Id: int(output.Id),
Name: "test",
DeptId: int(output.Id),
DeptName: output.Name,
}
return
}
func (receiver *ServiceAImpl) GetRpcSayHello(ctx context.Context, name string) (reply string, err error) {
// Use the gRPC client to call go-server through the gRPC protocol
output, err := receiver.greeterClient.SayHello(ctx, &protobuf.HelloRequest{
Name: name,
})
if err != nil {
return
}
reply = output.Message
return
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Testing service-a's implementation call to service-b:
curl -X 'GET' \
'http://localhost:6060/v1/rpcdept?userId=1' \
-H 'accept: application/json'
2
3
And we get the response:
{
"id": 1,
"name": "test",
"deptId": 1,
"deptName": "Test Department"
}
2
3
4
5
6
Now, let's test service-a's implementation call to go-server:
curl -X 'GET' \
'http://localhost:6060/v1/rpchello?name=yongchang' \
-H 'accept: application/json'
2
3
And we get the response:
"this is message from reply"
Starting go-client
Go to the dubbo/rpc/grpc/go-client
path and execute the command go run cmd/client.go
:
When you see the log output as shown in the figure above, it indicates that the client has started successfully, successfully called service-b, and got the return value {Id:1 Name:Test Department StaffTotal:10}
.
Let's look at the code implementation of go-client:
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/common/logger"
"dubbo.apache.org/dubbo-go/v3/config"
_ "dubbo.apache.org/dubbo-go/v3/imports"
_ "github.com/dubbogo/gost/log/logrus"
"github.com/unionj-cloud/go-doudou-tutorials/dubbodemo/dubbo/rpc/grpc/service-b"
)
type Client struct {
ServiceBClientImpl service_b.ServiceBClientImpl
}
func main() {
config.SetConsumerService(&Client{})
if err := config.Load(); err != nil {
panic(err)
}
logger.Info("start to test dubbo")
client := &Client{}
for i := 0; i < 10; i++ {
deptDto, err := client.ServiceBClientImpl.GetDeptById(context.TODO(), 1)
if err != nil {
fmt.Printf("error: %v", err)
return
}
fmt.Printf("response result: %v\n", deptDto)
time.Sleep(1 * time.Second)
}
initSignal()
}
func initSignal() {
signals := make(chan os.Signal, 1)
// It is not possible to block SIGKILL or syscall.SIGSTOP
signal.Notify(signals, os.Interrupt, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM)
for {
sig := <-signals
logger.Infof("get signal %s", sig.String())
switch sig {
case syscall.SIGHUP:
// reload()
default:
time.AfterFunc(time.Duration(int(3e9)), func() {
logger.Warnf("app exit now")
os.Exit(0)
})
// The program exits normally or timeout forcibly exits.
fmt.Println("provider app exit now")
return
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Looking at the ServiceBClientImpl interface definition and the Dubbo configuration file, we can understand how a Dubbo client calls a go-doudou service:
// dubbo/rpc/grpc/service-b/service.go
type ServiceBClientImpl struct {
GetDeptById func(ctx context.Context, id int32) (*DeptDto, error) `dubbo:"GetDeptByIdRpc"`
}
func (u *ServiceBClientImpl) Reference() string {
return "ServiceBClientImpl"
}
2
3
4
5
6
7
8
# dubbo/rpc/grpc/go-client/conf/dubbogo.yml
dubbo:
registries:
demoZK:
protocol: zookeeper
address: localhost:2181,localhost:2182,localhost:2183
consumer:
references:
ServiceBClientImpl:
protocol: grpc
interface: cloud.unionj.ServiceB_grpc
group: group
version: v2.2.2
retries: 3
cluster: failover
2
3
4
5
6
7
8
9
10
11
12
13
14
15
From these configuration files, we can see that dubbo-go is configured to call the cloud.unionj.ServiceB_grpc service registered in ZooKeeper, with group name "group" and version "v2.2.2". The reference field in the ServiceBClientImpl interface corresponds to the RPC service name, and the dubbo:"GetDeptByIdRpc"
annotation maps the local method name GetDeptById to the remote RPC method name GetDeptByIdRpc.
Summary
From the demo we've analyzed, we can see:
- go-doudou can call dubbo-go services through gRPC.
- dubbo-go can also call go-doudou services through gRPC.
- The implementation is very simple, and the interoperability based on ZooKeeper is complete, using the gRPC protocol as a bridge.
This case verifies that go-doudou, as a new-generation Go microservice framework, can seamlessly integrate with the traditional dubbo ecosystem, supporting hybrid deployments and gradual migrations.
For more detailed information, please refer to the example code repository: go-doudou-tutorials/dubbodemo