一個(gè) Dubbo Triple 協(xié)議的 "Bug"
問題描述
近期涂鴉智能的筒子反饋:dubbogo 客戶端使用 Triple 協(xié)議發(fā)往 dubbo (Java) 服務(wù)端的某些 attachment 會(huì)發(fā)送丟失現(xiàn)象,并報(bào)了 issue:
https://github.com/apache/dubbo-go/issues/2752
問題簡述:服務(wù)端無法從附件中獲取鍵名為 remote.application 的值。
具體來說,在使用 Dubbo-go 客戶端調(diào)用 Dubbo-java 服務(wù)時(shí),發(fā)現(xiàn)通過 context 設(shè)置的 attachment 中,"remote.application" 鍵值對(duì)在服務(wù)端無法獲取,而 "remote.application1" 和 "remote.application2" 可以正常獲取。
這里先給出結(jié)論:這不是一個(gè) bug,之所以 attachment 中 key 為 "remote.application" 的 item 在 dubbo server 端被過濾掉,是因?yàn)樵?key 是 dubbo(Java) attachment 中的保留字段,不允許用戶使用。
下面給出問題復(fù)現(xiàn),以及問題分析過程。
環(huán)境準(zhǔn)備
- 服務(wù)端:Dubbo-Java v3.3.0
- 客戶端:Dubbo-go v3.2.0-rc2
- 協(xié)議:triple
- 注冊(cè)中心:Zookeeper
參考:https://github.com/apache/dubbo-go-samples
- 代碼:Dubbo java and go interoperability, protobuf and triple protocol
- 文檔:基于 protobuf 實(shí)現(xiàn) triple 協(xié)議互通(適用于兩邊都用 protobuf 開發(fā)的場景)
Client 端
先下載 dubbo-go-samples 工程,在工程根目錄下執(zhí)行如下命令,以更新 dubbo-go-sample 依賴的 dubbogo 版本。
$ go get dubbo.apache.org/dubbo-go/v3@v3.2.0-rc2
java_interop/protobuf-triple/go/go-client/cmd/client.go:
package main
import (
"context"
"dubbo.apache.org/dubbo-go/v3/client"
"dubbo.apache.org/dubbo-go/v3/common/constant"
_ "dubbo.apache.org/dubbo-go/v3/imports"
greet "github.com/apache/dubbo-go-samples/java_interop/protobuf-triple/go/proto"
"github.com/dubbogo/gost/log/logger"
)
func main() {
cli, err := client.NewClient(
client.WithClientURL("127.0.0.1:50052"),
)
if err != nil {
panic(err) // If there's an error, it's handled immediately by panicking.
}
svc, err := greet.NewGreeter(cli)
if err != nil {
panic(err) // Same here, handle the error immediately.
}
ctx := context.Background()
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
"remote.application": "appname",
"remote.application1": "appname",
"remote.application2": "appname",
})
resp, err := svc.SayHello(ctx, &greet.HelloRequest{Name: "hello world"})
if err != nil {
logger.Error(err)
return // Now, we explicitly handle the error by logging it and then returning from the function.
}
logger.Infof("Greet response: %s", resp.Message)
}
Server 端
java_interop/protobuf-triple/java/java-server/pom.xml:
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<dubbo.version>3.3.0</dubbo.version>
</properties>
java_interop/protobuf-triple/java/java-server/src/main/java/org/apache/dubbo/sample/GreeterImpl.java:
package org.apache.dubbo.sample;
import com.alibaba.fastjson2.JSON;
import org.apache.dubbo.rpc.RpcContext;
import java.util.Map;
public class GreeterImpl extends DubboGreeterTriple.GreeterImplBase {
@Override
public HelloReply sayHello(HelloRequest request) {
Map<String, Object> serverAttachments = RpcContext.getServerAttachment().getObjectAttachments();
System.out.println("ContextService serverAttachments:" + JSON.toJSONString(serverAttachments));
return HelloReply.newBuilder()
.setMessage(request.getName())
.build();
}
}
運(yùn)行
Go client 調(diào)用 java server
- java-server 目錄中:
$ ./run.sh
...
Dubbo triple java server started
- go-client 目錄中:
$ go run cmd/client.go
- 結(jié)果輸出
服務(wù)端:
2月 12, 2025 6:26:30 下午 org.apache.dubbo.common.logger.jdk.JdkLogger info
信息: [DUBBO] The connection [id: 0x156ed855, L:/127.0.0.1:50052 - R:/127.0.0.1:56961] of 127.0.0.1:56961 -> 127.0.0.1:50052 is established., dubbo version: 3.3.0, current host: 10.60.200.103
ContextService serverAttachments:{"retries":"","remote.application1":"appname","remote.application2":"appname"}
2月 12, 2025 6:26:30 下午 org.apache.dubbo.common.logger.jdk.JdkLogger info
信息: [DUBBO] The connection [id: 0x156ed855, L:/127.0.0.1:50052 ! R:/127.0.0.1:56961] of 127.0.0.1:56961 -> 127.0.0.1:50052 is disconnected., dubbo version: 3.3.0, current host: 10.60.200.103
2月 12, 2025 6:26:30 下午 org.apache.dubbo.common.logger.jdk.JdkLogger warn
警告: [DUBBO] All clients has disconnected from /127.0.0.1:50052. You can graceful shutdown now., dubbo version: 3.3.0, current host: 10.60.200.103, error code: 99-0. This may be caused by unknown error in remoting module, go to https://dubbo.apache.org/faq/99/0 to find instructions.
2月 12, 2025 6:26:30 下午 org.apache.dubbo.common.logger.jdk.JdkLogger info
信息: [DUBBO] The connection [id: 0x156ed855, L:/127.0.0.1:50052 ! R:/127.0.0.1:56961] of 127.0.0.1:56961 -> 127.0.0.1:50052 is disconnected., dubbo version: 3.3.0, current host: 10.60.200.103
客戶端:
2025-02-12 18:26:29 INFO logger/logging.go:42 URL specified explicitly 127.0.0.1:50052
2025-02-12 18:26:30 INFO logger/logging.go:42 [TRIPLE Protocol] Refer service: tri://127.0.0.1:50052/org.apache.dubbo.sample.Greeter?app.version=&application=dubbo.io&async=false&bean.name=org.apache.dubbo.sample.Greeter&cluster=failover&config.tracing=&environment=&generic=&group=&interface=org.apache.dubbo.sample.Greeter&loadbalance=&metadata-type=local&module=sample&name=dubbo.io&organization=dubbo-go&owner=dubbo-go&peer=true&provided-by=&reference.filter=cshutdown?istry.role=0&release=dubbo-golang-3.2.0&remote.timestamp=&retries=&serialization=protobuf&side=consumer&sticky=false×tamp=1739355989&version=
2025-02-12 18:26:30 INFO logger/logging.go:42 Greet response: hello world
注意到 java-server 無法從 go-server 傳遞的 attachment 獲取 "remote.application" 鍵。
問題分析
在這個(gè)場景中涉及到 ClientAttachment 和 ServerAttachment:
- ClientAttachmen:Go 客戶端寫入?yún)?shù)
- ServerAttachment:Java 服務(wù)端讀取參數(shù)
調(diào)用流程:
- client 端:Method Invoke 發(fā)起調(diào)用 $\rightarrow$ 寫入 RpcClientAttachment $\rightarrow$ 封裝進(jìn) Invocation $\rightarrow$ 序列化傳輸
- server 端:解析 Invocation $\rightarrow$ 生成 Method Invoke 參數(shù)和 RpcServerAttachment $\rightarrow$ 真實(shí)調(diào)用
$\rightarrow$ 處理后生成 Result(包含 Response 和 Context)$\rightarrow$ 序列化返回。
參考 官方文檔:
Triple 協(xié)議會(huì)將 attachment 轉(zhuǎn)換為 HTTP header 傳輸,使用 Wireshark 抓包分析,確認(rèn) "remote.application" 確實(shí)被正確封裝在 HTTP header 中,說明 Go 客戶端的 ClientAttachment → Invocation → 網(wǎng)絡(luò)傳輸鏈路是正常的。
關(guān)注 Java 服務(wù)端的 org.apache.dubbo.rpc.protocol.tri.h12.AbstractServerTransportListener 服務(wù)端是請(qǐng)求處理的核心類,負(fù)責(zé)接收和處理 HTTP 請(qǐng)求,并構(gòu)建 RPC 調(diào)用對(duì)象。
// AbstractServerTransportListener.java
protected RpcInvocation buildRpcInvocation(RpcInvocationBuildContext context) {
// ...
// 關(guān)鍵轉(zhuǎn)換點(diǎn):將 HTTP Header 轉(zhuǎn)為 RPC Attachment
inv.setObjectAttachments(StreamUtils.toAttachments(httpMetadata.headers()));
// ...
}
調(diào)用 StreamUtils.toAttachments() 之前,Header 中是包含 "remote.application" 的。
調(diào)用 StreamUtils.toAttachments() 之后,RpcInvocation 類型對(duì)象 inv 的成員變量 Map<String,Object>attachments 不包含 "remote.application"。
StreamUtils#toAttachments(HttpHeaders headers):
/**
* Parse and convert headers to attachments. Ignore Http2 PseudoHeaderName and internal name
*
* @param headers the headers
* @return the attachments
*/
public static Map<String, Object> toAttachments(HttpHeaders headers) {
if (headers == null) {
return Collections.emptyMap();
}
Map<String, Object> attachments = CollectionUtils.newHashMap(headers.size());
for (Map.Entry<CharSequence, String> entry : headers) {
String key = entry.getKey().toString();
String value = entry.getValue();
int len = key.length() - TripleConstants.HEADER_BIN_SUFFIX.length();
if (len > 0 && TripleConstants.HEADER_BIN_SUFFIX.equals(key.substring(len))) {
try {
putAttachment(attachments, key.substring(0, len), value == null ? null : dec
} catch (Exception e) {
LOGGER.error(PROTOCOL_FAILED_PARSE, "", "", "Failed to parse response attach
}
} else {
putAttachment(attachments, key, value);
}
}
// try converting upper key
String converted = headers.getFirst(TripleHeaderEnum.TRI_HEADER_CONVERT.getKey());
if (converted == null) {
return attachments;
}
String json = TriRpcStatus.decodeMessage(converted);
Map<String, String> map = JsonUtils.toJavaObject(json, Map.class);
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
Object value = attachments.remove(key);
if (value != null) {
putAttachment(attachments, entry.getValue(), value);
}
}
return attachments;
}
StreamUtils#putAttachment(Map<String, Object> attachments, String key, Object value):
進(jìn)一步查看 TripleHeaderEnum :
為了防止業(yè)務(wù)層誤用或覆蓋這個(gè)鍵值,Dubbo 將其加入了排除列表,導(dǎo)致即使在 attachment 中設(shè)置了這個(gè)鍵值對(duì),在轉(zhuǎn)換時(shí)也會(huì)被過濾掉。