Pixie Stirling ThriftMux 测试容器完全指南:构建、运行与 Mux 协议追踪验证
Pixie Stirling ThriftMux 测试容器完全指南:构建、运行与 Mux 协议追踪验证
导读
本文围绕 Pixie 开源项目(Instant Kubernetes-Native Application Observability)中位于 src/stirling/source_connectors/socket_tracer/testing/containers/thriftmux/ 的测试容器展开。该容器封装了一个基于 Twitter Finagle ThriftMux 的 Scala 客户端/服务端,是 Stirling 的 socket tracer 对 ThriftMux(Mux over Thrift)协议进行 eBPF 追踪与断言验证的关键实验载体。读完本文,你将掌握:如何用 Bazel 构建并启动该容器、如何以 docker exec 手动运行内置客户端、如何通过 --use-tls 开启 TLS 通道,以及该容器如何被 mux_trace_bpf_test.cc 等 BPF 集成测试引用以验证协议解析正确性。
容器概览:一个容器,两种角色
根据 README 的说明,thriftmux 目录下的构建产物是一个可同时充当 ThriftMux 服务端 与 ThriftMux 客户端 的容器镜像:
- 服务端:默认入口(entrypoint),启动后监听 loopback 地址的 8080 端口;
- 客户端:不设默认入口,需要通过
docker exec覆盖默认入口并显式指定Client主类来运行。
该容器之所以存在,是因为 Stirling 的 socket tracer 需要对多种应用层协议(含 ThriftMux/Mux)做无侵入的字节流解析,而协议解析器的正确性必须由真实协议流量来验证——这正是该容器被设计为可独立运行的"协议流量发生器"的原因。
目录结构与构建产物
容器构建所需的全部文件位于 thriftmux 目录 下,结构如下:
thriftmux/
├── BUILD.bazel # Bazel 构建定义:镜像、scrooge 代码生成、TLS 证书
├── README.md # 使用说明(本文主体)
└── src/
├── main/
│ ├── scala/
│ │ ├── client/Client.scala # ThriftMux 客户端入口
│ │ └── server/Server.scala # ThriftMux 服务端入口
│ └── thrift/
│ └── testservice.thrift # 测试服务接口定义
从 BUILD.bazel 可以看到镜像的完整构建链路:
- TLS 证书生成:
certsgenrule 调用//src/common/testing/test_utils/cert_generator生成ca.crt、client.crt、client.key、server.crt、server.key五份证书/密钥(--secret_key_type pkcs8),随后通过ssl_keys(pkg_tar)以 0755 权限装入镜像的/etc/ssl目录; - Thrift 接口编译:
thrift_library收集**/*.thrift,再由thriftmux_scrooge(scrooge_scala_library)生成 Scrooge Scala 服务代码,依赖scrooge_core_with_finagle、scrooge_jars等将 finagle 各模块(com_twitter_finagle_core_2_13、com_twitter_finagle_mux_2_13、com_twitter_finagle_thriftmux_2_13等)打包; - 镜像组装:
thriftmux_base(container_image)以DEFAULT_JAVA_BASE(OpenJDK)为基础,装入libc6、libcrypt1与ssl_keys;server_image(scala_image)在此基础上以Server为主类、并设置 JVM 参数-Dio.netty.native.deleteLibAfterLoading=false(保留 Netty 原生库以便追踪)。
快速上手:构建并运行服务端
默认(非 TLS)模式
按 README 中的命令,直接以 Bazel 运行即可启动服务端(默认入口):
$ bazel run src/stirling/source_connectors/socket_tracer/testing/containers/thriftmux:server_image
该命令会构建 server_image 并在容器内以 Server 主类启动服务。从 Server.scala 的源码可以看到服务端的实际行为:
val addr = new InetSocketAddress(InetAddress.getLoopbackAddress, 8080)
val testSvc = new TestService.MethodPerEndpoint {
def query(x: String): Future[String] = Future.value(x + x)
def question(y: String): Future[String] = Future.value(y + y)
def inquiry(z: String): Future[String] = Future.value(z + z)
}
var server = if (useTls) {
ThriftMux.server.withTransport.tls(sslConfig).serveIface(addr, testSvc)
} else {
ThriftMux.server.serveIface(addr, testSvc)
}
Await.ready(server)
要点解析:
- 服务监听 loopback 地址的 8080 端口,而不是 0.0.0.0,流量被约束在容器内部,便于 BPF 测试聚焦;
- 三个接口方法(
query/question/inquiry)的返回值均为入参重复拼接(如query("String")返回"StringString"),这一简单而确定性的行为让测试断言变得非常可靠; - 是否启用 TLS 由命令行参数
--use-tls控制(解析逻辑为args.grouped(2).toList.collect { case Array("--use-tls", tls) => useTls = tls.toBoolean })。
带 TLS 运行服务端
如需验证加密通道下的协议追踪,在 Bazel run 时追加参数(Bazel 会把 -- 之后的参数透传给容器入口):
$ bazel run src/stirling/source_connectors/socket_tracer/testing/containers/thriftmux:server_image -- --use-tls true
TLS 模式下,服务端通过 SslServerConfiguration 加载镜像内置的 /etc/ssl/server.crt 与 /etc/ssl/server.key(即 BUILD 中 certs genrule 生成的证书对),并调用 withTransport.tls(sslConfig) 为 ThriftMux 传输层启用 TLS。
在容器内运行客户端
定位容器 ID 并手动运行
server_image 的默认入口是服务端,因此要运行客户端,必须用 docker exec 直接覆盖入口执行 Java 主类:
$ docker exec -it ${container_id} /usr/bin/java -cp @/app/px/src/stirling/source_connectors/socket_tracer/testing/containers/thriftmux/server_image.classpath Client
其中:
${container_id}是正在运行的 thriftmux 容器 ID(可由docker ps获取);@/app/px/.../server_image.classpath是 Java 的 classpath 参数文件(@前缀),由 Bazel 的scala_image规则在镜像内生成,指向全部运行时依赖 JAR 列表;- 末尾的
Client是主类名。
执行后,客户端会连接 localhost:8080 并调用一次 query("String"),随后打印返回值。对照 Client.scala 的实现:
val svcPerEndpoint = stackClient
.methodBuilder("inet!localhost:8080")
.servicePerEndpoint[TestService.ServicePerEndpoint]
val svc = ThriftMux.Client.methodPerEndpointTestService.ServicePerEndpoint, TestService.MethodPerEndpoint
val r = Await.result(svc.query("String"))
println(r)
客户端逻辑:
- 使用
ThriftMux.client.withLabel("thriftmux_example")构建栈客户端; - 通过
methodBuilder("inet!localhost:8080")绑定目标地址,inet!表示显式使用InetSocketAddress解析(而非 DNS); - 调用
query("String"),按服务端实现应得到输出StringString。
客户端开启 TLS
由于客户端同样通过 --use-tls 开关控制 TLS,而 Java 信任库(keystore)默认不包含自签 CA,因此在 TLS 场景下需要先把 CA 证书导入 JVM 的 cacerts(README 中的关键步骤):
$ docker exec -it ${container_id} /usr/lib/jvm/java-11-openjdk-amd64/bin/keytool \
-importcert -keystore /etc/ssl/certs/java/cacerts \
-file /etc/ssl/ca.crt -noprompt -storepass changeit
之后即可运行启用 TLS 的客户端:
$ docker exec -it ${container_id} /usr/bin/java \
-cp @/app/px/src/stirling/source_connectors/socket_tracer/testing/containers/thriftmux/server_image.classpath \
Client --use-tls true
其中:
keytool -importcert:将容器内的ca.crt导入/etc/ssl/certs/java/cacerts,-noprompt跳过交互确认,-storepass changeit是 OpenJDK 默认 cacerts 口令;- Java 路径
java-11-openjdk-amd64与 README 中DEFAULT_JAVA_BASE的 OpenJDK 11 基础镜像一致; - 客户端 TLS 配置由
SslClientConfiguration加载/etc/ssl/client.crt与/etc/ssl/client.key完成双向认证所需的客户端证书与密钥。
测试服务的 Thrift 接口定义
容器内所有调用均围绕 testservice.thrift 定义的服务展开:
exception InvalidQueryException {
1: i32 errorCode
}
service TestService {
string query(1: string x) throws (1: InvalidQueryException ex)
string question(1: string y) throws (1: InvalidQueryException ex)
string inquiry(1: string z) throws (1: InvalidQueryException ex)
}
service FanoutTestService {
list<string> query(1: list<string> x)
}
接口设计的测试友好性在于:三个方法签名对称(单字符串入参、字符串返回),且都声明了可抛出的 InvalidQueryException,既覆盖了正常请求/响应路径,也为将来验证异常序列化预留了接口。此外还定义了 FanoutTestService 用于测试 list<string> 批量参数的场景。
集成测试场景:mux_trace_bpf_test.cc
README 末尾明确指出"See mux_container_bpf_test.cc for use case",对应的实际测试文件是 mux_trace_bpf_test.cc。该测试通过 ThriftMuxServerContainer(定义于 thrift_mux_server_container.h,加载 server_image.tar)启动容器,再在容器内以 podman exec 运行客户端:
std::string classpath =
"@/app/px/src/stirling/source_connectors/socket_tracer/testing/containers/thriftmux/"
"server_image.classpath";
StatusOr<int32_t> RunThriftMuxClient() {
std::string cmd =
absl::StrFormat("podman exec %s /usr/bin/java -cp %s Client & echo $! && wait",
server_.container_name(), classpath);
PX_ASSIGN_OR_RETURN(std::string out, px::Exec(cmd));
// ...解析 client_pid,并断言输出包含 "StringString"
}
这里有两个值得注意的细节:
- 输出断言:测试用成员
thriftmux_client_output = "StringString"断言客户端输出,与query("String")的服务端实现(x + x)完全对应,形成端到端闭环; - BPF 资源调配:测试的
Init()会强制开启FLAGS_stirling_enable_mux_tracing,并同时关闭 CQL(FLAGS_stirling_enable_cass_tracing)与 NATS(FLAGS_stirling_enable_nats_tracing)追踪,注释说明这是因为老内核只有 4096 条 BPF 指令上限,需要为 Mux 解析器腾出指令空间——这解释了该容器测试在真实内核环境下的资源约束前提。
除 mux_trace_bpf_test.cc 外,netty_tls_trace_bpf_test.cc 也在 TLS 场景中复用同一 classpath 路径(第 89 行),用于验证 TLS 加密流量下的解析,与 README 中 --use-tls true 的用法相互印证。
常见问题与注意事项
- 客户端必须用
docker exec运行:server_image的默认入口是Server,客户端没有独立镜像,需要覆盖入口并以 classpath 参数文件方式启动; - TLS 场景必须先导入 CA:客户端 JVM 默认不信任自签 CA,
keytool -importcert步骤不可省略,否则握手会因证书校验失败而中断; - 端口固定为 loopback:8080:服务端绑定
InetSocketAddress(InetAddress.getLoopbackAddress, 8080),客户端用inet!localhost:8080连接,二者必须一致; - 测试环境以 podman 而非 docker 驱动:从
mux_trace_bpf_test.cc可见集成测试通过podman exec注入客户端命令,因此本地复现时可优先使用 podman 兼容环境; - 构建产物为容器镜像而非裸二进制:运行依赖 Bazel 的
scala_image/container_image规则与@io_bazel_rules_docker、@io_bazel_rules_scala外部依赖,需在配置好 Bazel 工作区的 Pixie 仓库环境中执行。
总结
thriftmux 测试容器是 Pixie Stirling socket tracer 验证 ThriftMux 协议解析的最小闭环:一份 Thrift 接口定义 + 一份 Scala 服务端/客户端实现 + 一份 Bazel 镜像定义,即可在任何 Linux 内核上生成可预期的 Mux 流量,供 eBPF 探针抓取并断言。无论你是想复现 mux_trace_bpf_test.cc 的端到端流程,还是需要一套独立的 ThriftMux 流量发生器用于调试,都可以按本文的构建与运行步骤直接上手。