By Li Zhixin
Dubbo-go ecosystem includes sub-projects (such as Dubbo-go v3.0, v1.5, and pixiu). It provides flexible customization methods in terms of scalability.
As we all know, High-Speed Service Framework (HSF) is the benchmark in the RPC/service governance field of Alibaba Group. HSF-go is an HSF framework implemented in the Go language, which is maintained by the middleware team. Due to the characteristics of the Go language, the cloud-native component integration service proxy plays an important role in cross-language call scenarios. Currently, it has Dapr Binding implementation and generates value in Function Compute (FC) scenarios, cross-cloud scenarios, and independent deployment scenarios. It also has applied to scenarios in DingTalk, Lazada, AutoNavi, and other technical teams. HSF-go is a part of the Dubbo-go ecosystem and is a customized implementation of the open-source project Dubbo-go.
Throughout the series of HSF-go scenarios related to service proxy, I would like to share my practice and principles when using it as a service proxy, but you are welcome to share your experience.
First of all, let's take a look at the generic call of Dubbo. It can be called to downstream services by passing in the method name, method signature, and parameter value without relying on the two-party package.
Golang's generic call is slightly different from that of Java, which is related to language features. Go does not support class inheritance and method overloading and does not have the concept of a two-party package. Java's two-party package can be abstracted into a set of interface information agreed upon by the client and the server, including the interface name, method name, parameter list, and specific parameter definition. These basic concepts are necessary for any RPC scenario, but their manifestations are different. For Java, it is a two-party package; for gRPC, it is the proto file and compilation product; for Dubbo-go compatible with Dubbo protocol, it is the Java-compatible Hessian serialization interface. The adaptation of using Go to write the Hessian interface brings some troubles. Go developers have difficulty writing the POJO structure and interface stub corresponding to the Java version.
The following is an example of a Java-compatible Go client that uses Hessian serialization in the Dubbo-go ecosystem's custom writing method.
// UserProvider Client Stub Class
type UserProvider struct {
// The dubbo label is used to adapt to the uppercase method name of the client on the Go side -> the lowercase method name on the Java side. Only the dubbo protocol client needs it.
GetUser func(ctx context.Context, req int32) (*User, error) `dubbo:"getUser"`
}
func init(){
// Register the client stub class to the framework and instantiate the client interface pointer userProvider.
config.SetConsumerService(userProvider)
}
// The field must correspond to the Java side, with the first letter in uppercase.
type User struct {
UserID string
UserFullName string `hessian:"user_full_name"`
UserAge int32 // default convert to "userAge"
Time time.Time
}
func (u *User) JavaClassName() string {
return "org.apache.dubbo.User" // Must correspond to the user class name on the Java side.
}
Compared with Java, which supports method overloading, Go is less dependent on the metadata information of the interface and can easily locate the destination method to initiate calls. However, in essence, the agreed interface information mentioned above is needed to ensure that downstream methods can be hit correctly and parameter resolution is correct.
In the scenario of a generic call, there is no need to introduce a two-party package in the code. While the freedom is increased, the restriction of the two-party package interface is lost. Therefore, the client needs to be careful when passing parameters in the generic call to ensure that the passed parameters completely correspond to the interface provided by the server to call correctly.
Generic calls are server generic and client generic calls. The client generic is that the intermediate proxy is served as the reverse proxy on the consumer side, and the server generic is that the intermediate proxy is served as the forward proxy on the service provider side and forwards requests to the real backend service provider. Developers do not need to declare specific parameters when writing services in the server generic. The framework parses the request into a generic method name and parameter list array and passes it to the user layer. The code written by developers needs to use these dynamic data directly. Please refer to the example at the end of this article. However, the client generic is used more. The client does not get the interface dependency provided by the server at the code level but generates the generic call request by the framework by passing in the method name and parameters, achieving the same effect as calling through the real interface.
Generic call requests' method name is often $invoke and contains three parameters:
Let’s take an HSF-go generic call request as an example:
// A HSF-go client generic call.
genericService.Invoke(context.TODO(),
"getUser",
[]string{(&GoUser{}).JavaClassName(), (&GoUser{}).JavaClassName()},
[]interface{}{&GoUser{Name: "laurence"}, &GoUser{Age: 22}}
)
After receiving these three parameters, the framework constructs a generic request and sends it to the server.
When the server receives the generic request, it filters out the request with the method name of $invoke in a filter layer, constructs a real request structure, and passes it to the upper layer to complete the call and return it.
It is a general implementation of the Dubbo generic call. However, if you design it from the perspective of the Go language, you do not need to pass the parameter list type. The server can locate the method simply by the method name and then deserialize the parameter array to obtain the real parameters.
The application of generic calls is wide. The generic calls the group's developers are most exposed to may be the service testing capabilities provided by the MSE/HSF-ops platform.
The MSE O&M platform used in the group is powerful for HSF service governance. It can configure O&M, service governance capabilities, service testing, stress testing, and traffic playback of commercial versions of MSE on the platform. The service testing capability it provides depends on the HSF generic calls. When a developer initiates a test for an interface method on the platform, a JSON parameter list will be passed in. The platform will convert the JSON parameter list into a Hessian object and serialize it, construct the three parameters mentioned above, and initiate a call to the destination machine to get the test return value. HSF services support generic calls by default.
In addition to service testing, you can use generic calls to develop service gateways, service probes, and cli service testing tools.
There are many common serialization protocols, such as the default Hessian2 of Dubbo/HSF, the extensive JSON, and the protobuf (PB) natively supported by gRPC.
The three typical serialization schemes mentioned have similar effects but are slightly different in implementation and development. PB cannot directly generate memory objects from the serialized byte stream while Hessian and JSON can. The deserialization of the latter two does not rely on two-party packages, which can be said to be stubs. A better understanding is that PB can be understood as a symmetric encryption protocol. The client and server must have stubs to parse the object, while Hessian and JSON do not rely on stubs, which determines that the compression effect of PB is better.
This can explain why the Triple (Dubbo3) protocol that uses PB serialization is not supported by the test function of our commonly used service O&M platform. The preceding generic call model can only construct serialized types that can be parsed out of thin air.
If you want to generalize the call PB serialization service, there are still some solutions. Use symmetric encryption as an example. As long as you get the same key as the server, you can construct the structure that can be parsed by the other party, initiating the generic call. This is the principle of gRPC reflection service, which allows the client to obtain the proto interface definition file before initiating the call, obtaining the key of symmetric encryption. Based on this key, fill in the parameter fields, and the call can be initiated like a normal client.
We mainly talked about the generic call model of the Dubbo system. As mentioned before, there are many application scenarios of generic calls, which has become one of the bases for Dapr landing. Dapr is a CNCF incubation project jointly developed by Alibaba Cloud and opened source by Microsoft. It integrates many advanced concepts (such as standardized API, component extensible SPI mechanism, sidecar architecture, and Serverless). There are many production landing scenarios in Alibaba Group, such as FC and cross-cloud.
The Dapr standardized API concept is novel and practical. The Bindings building block is the foundation of our service call solution.
The most intuitive understanding of Bindings is a layer of traffic middleware between the user application runtime and the network.
The preceding figure can explain the entire Binding-based call process. The user application runtime calls the Dapr standardized interface to initiate the call. The Dapr runtime hands traffic to the extensible Binding building block. Dapr can easily support the switching of multiple protocols and activate on demand with this unified interface and extensible capability. HSF and Dubbo are supported, as shown in the figure.
For example, the activated HSF-go building block will take over the request, parse the standardized request header and request body from the application, and generate an HSF protocol request. The Dapr sidecar does not have a downstream service two-party package, so this request must be a generic call request.
The service discovery process was completed before the request was issued, which was insensitive to users and applications. It was taken over and encapsulated by Dapr. The generic request can be sent to the destination machine IP after the service discovery is completed and is received and processed by the HSF-go implementation of the downstream Inbound Binding. This downstream component corresponds to the server generic call mentioned before, which accepts any HSF request. The downstream parses the HSF protocol, standardizes the parameters from the three parameters of the generic call to normal request parameters, and passes the parameters to the application runtime through the callback mechanism provided by Dapr.
Generic calls play an important role in this process. The client is responsible for initiating generic calls of the HSF protocol for outbound traffic, and the server is responsible for parsing and passing generic calls of inbound traffic.
In my opinion, the network protocol model of Dapr is the embodiment of the further abstraction of the RPC protocol. All RPC protocols are abstracted into metadata and body. The user application/SDK side only needs to care about these two parts. Once this abstract request structure is handed over to Dapr, the specific protocol is generated by the specific activated building block. This is a delicate service call abstract design provided by Dapr.
The inbound and outbound traffic components are both implementations of generic calls. If you look at them carefully, you will find they are not the traditional generic calls we mentioned in the first section.
The input parameter of a traditional generic call is a structure, and the calling process involves serialization. In a sidecar scenario like Dapr, a complete RPC call will introduce at least six serialization/deserialization, which is costly.
Therefore, the standard generic call is not used in the design. The serialization process is omitted, and only one serialization on the application side is retained. The Dapr sidecar only performs pass-through for the parameter part. As such, unnecessary consumption is reduced.
The implementation of Outbound on the client side accomplishes the use of the following generic call interface.
// The args parameter is a serialized byte array.
ProxyInvokeWithBytes(ctx context.Context, methodName string, argsTypes []string, args [][]byte) ([]byte, error)
The implementation of Inbound on the server side has become a generic call for byte array type parameters.
// inbound input parameter
type RawDataServiceRequest struct {
RequestContext *core.RequestContext
Method string
ArgsTypes []string
Args [][]byte // The args parameter is a serialized byte array.
Attachment map[string]interface{}
RequestProps []byte
}
This is equivalent to deleting the serialization based on the generic call and passing the request parameters through.
The DingTalk Team has many Go language landing scenarios and has provided much help and practice during the development of the Dubbo-go ecological projects.
In cross-cluster communication solutions, proxy gateways are essential, and most gateways require O&M personnel to configure traffic manually. Some gateways have requirements for network protocols, such as envoy. One of the reasons why the middleware team launched the Dubbo3 (Triple) protocol based on Http2 is to adapt to gateways.
Protocol conversion is not required at the gateway layer in the cross-cluster RPC scenario. Serialization or deserialization is not required, and service governance capabilities are integrated within the gateway, which reduces resource consumption and O&M costs.
This raises a demand. In cross-cloud scenarios within the group, we need to establish a proxy gateway that supports the native HSF protocol. This allows clients outside the cluster to switch requests to the internal cluster without awareness. The gateway receives HSF requests from the outside and dynamically performs the service discovery to forward the request traffic to the corresponding service provider in the cluster. It is conceivable that generic calls will play an important role in this process.
Following the previous approach of Dapr, as shown in the preceding figure, let us shift the perspective from the entire call process to a single instance. It shows that one instance can either accept or initiate a generic request, and the serialization process is not involved in the generic process. The instance we are concerned about is the abstraction of the gateway.
With such a gateway, we can realize cross-cluster calls without client awareness. If necessary, proxy registration can be performed in the environment where the client is located.
Such a gateway is one-way and can handle traffic from the outside to the inside. If you want to open up in both directions, a unified registry across clusters will be necessary. As such, the gateway needs to query the information of multiple registries based on traffic to ensure the processes are correct.
HSF is the benchmark in the field of RPC/service governance in Alibaba Group. The Go language has broad development prospects and practical scenarios due to its high concurrency and cloud-native characteristics. The service proxy model is only a landing scenario. In addition, there are more applications worthy of our exploration and study in the process of research and development.
Li Zhixin, is the Head of Apache Dubbo PMC and Dubbo-go 3.0 and a Dapr contributor. He focuses on the R&D and open-source of cloud-native middleware and works hard on edge computing.
The ZEEKR App System's Cloud-Native Architecture Transformation Practice
508 posts | 49 followers
FollowAlibaba Cloud Native Community - July 2, 2021
Alibaba Developer - May 20, 2021
Alibaba Cloud Native Community - December 30, 2021
Alibaba Cloud Native Community - January 26, 2024
Alibaba Developer - February 4, 2021
Alibaba Cloud Native Community - April 23, 2023
508 posts | 49 followers
FollowA PaaS platform for a variety of application deployment options and microservices solutions to help you monitor, diagnose, operate and maintain your applications
Learn MoreA PaaS platform for a variety of application deployment options and microservices solutions to help you monitor, diagnose, operate and maintain your applications
Learn MoreElastic and secure virtual cloud servers to cater all your cloud hosting needs.
Learn MoreAlibaba Cloud Function Compute is a fully-managed event-driven compute service. It allows you to focus on writing and uploading code without the need to manage infrastructure such as servers.
Learn MoreMore Posts by Alibaba Cloud Native Community