Pixie Stirling ThriftMux 测试容器完全指南:构建、运行与 Mux 协议追踪验证

原创2026-10-07 23:18:381,925 阅读
文章标签:可观测性云原生

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 可以看到镜像的完整构建链路:

  1. TLS 证书生成:certs genrule 调用 //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 目录;
  2. 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 等)打包;
  3. 镜像组装: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"
}

这里有两个值得注意的细节:

  1. 输出断言:测试用成员 thriftmux_client_output = "StringString" 断言客户端输出,与 query("String") 的服务端实现(x + x)完全对应,形成端到端闭环;
  2. 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 流量发生器用于调试,都可以按本文的构建与运行步骤直接上手。

登录后查看全文
pixie