最近这几天在帮 柠檬 看她的 APM 系统要如何收集. Net 运行时的各种事件, 这些事件包括线程开始, JIT 执行, GC 触发等等.
.Net 在 windows 上 (NetFramework, CoreCLR) 通过 ETW(Event Tracing for Windows), 在 linux 上 (CoreCLR) 是通过 LTTng 跟踪事件.
ETW 的 API 设计已经被很多人诟病, 微软推出的类库 krabsetw 中 直指 ETW 是最差的 API 并且把操作 ETW 的文件命名为 噩梦. hpp .
而且 这篇文章 中, Casey Muratori 解释了为什么 ETW 是最差的 API, 原因包括:
然而 Casey Muratori 的文章对我帮助很大, 我只用了 1 天时间就写出了使用 ETW 收集. Net 运行时事件的 示例代码 .
之后我开始看如何使用 LTTng 收集这些事件, 按照我以往的经验 linux 上的类库 api 通常会比 windows 的好用, 但 LTTng 是个例外.
我第一件做的事情是去查找怎样在 c 程序里面 LTTng 的接口, 我打开了他们的 文档 然后开始浏览.
很快我发现了他们的文档只谈了如何使用代码发送事件, 却没有任何说明如何用代码接收事件, 我意识到我应该去看源代码.
使用 LTTng 跟踪事件首先需要创建一个会话, 启用事件和添加上下文参数, 然后启用跟踪, 在命令行里面是这样的调用:
- lttng create --live
- lttng enable-event --userspace --tracepoint DotNETRuntime:GCStart_V2
- lttng add-context --userspace --type vpid
- lttng add-context --userspace --type vtid
- lttng start
lttng 这个命令的源代码在 github 上, 通过几分钟的查找我发现 lttng 的各个命令的实现都是保存在 这个文件夹 下的.
打开 create.c 后又发现了创建会话调用的是
函数, 而
- lttng_create_session
函数可以通过引用 lttng.h 调用.
- lttng_create_session
- int
- ret = lttng_create_session_live(
- "example-session"
- ,
- "net://127.0.0.1"
- ,
- 1000000
- );
运行后立刻就报错了, 错误是 "No session daemon is available".
原因是 lttng-sessiond 这个程序没有启动, lttng 是通过一个独立服务来管理会话的, 而这个服务需要手动启动.
使用独立服务本身没有错, 但是 lttng-sessiond 这个程序提供了很多参数,
如果一个只想跟踪用户事件的程序启动了这个服务并指定了忽略内核事件的参数, 然后另外一个跟踪内核事件的程序将不能正常运作.
正确的做法是使用 systemd 来启动这个服务, 让系统管理员决定用什么参数, 而不是让调用者去启动它.
解决这个问题只需要简单粗暴的两行, 启动时如果已经启动过新进程会失败, 没有任何影响:
- system("lttng-sessiond --daemonize");
- std: :this_thread: :sleep_for(std: :chrono: :seconds(1));
现在
会返回成功了, 但是又发现了新的问题,
- lttng_create_session_live
于是代码变成了这样:
- system("lttng-sessiond --daemonize");
- std: :this_thread: :sleep_for(std: :chrono: :seconds(1));
- lttng_destroy_session(SessionName);
- int ret = lttng_create_session_live("example-session", "net://127.0.0.1", 1000000);
经过一段时间后, 我用代码实现了和命令行一样的功能:
- // start processes, won't replace exists
- system("lttng-sessiond --daemonize");
- std: :this_thread: :sleep_for(std: :chrono: :seconds(1));
- // create new session
- lttng_destroy_session(SessionName);
- int ret = lttng_create_session_live(SessionName, SessionUrl, LiveSessionInterval);
- if (ret != 0) {
- std: :cerr << "lttng_create_session: " << lttng_strerror(ret) << std: :endl;
- return - 1;
- }
- // create handle from session
- lttng_domain domain = {};
- domain.type = LTTNG_DOMAIN_UST;
- lttng_handle * handle = lttng_create_handle(SessionName, &domain);
- if (handle == nullptr) {
- std: :cerr << "lttng_create_handle: " << lttng_strerror(ret) << std: :endl;
- return - 1;
- }
- // enable event
- lttng_event event = {};
- event.type = LTTNG_EVENT_TRACEPOINT;
- memcpy(event.name, EventName.c_str(), EventName.size());
- event.loglevel_type = LTTNG_EVENT_LOGLEVEL_ALL;
- event.loglevel = -1;
- ret = lttng_enable_event_with_exclusions(handle, &event, nullptr, nullptr, 0, nullptr);
- if (ret < 0) {
- std: :cerr << "lttng_enable_event_with_exclusions: " << lttng_strerror(ret) << std: :endl;
- return - 1;
- }
- // add context
- lttng_event_context contextPid = {};
- contextPid.ctx = LTTNG_EVENT_CONTEXT_VPID;
- ret = lttng_add_context(handle, &contextPid, nullptr, nullptr);
- if (ret < 0) {
- std: :cerr << "lttng_add_context: " << lttng_strerror(ret) << std: :endl;
- return - 1;
- }
- // start tracing
- ret = lttng_start_tracing(SessionName);
- if (ret < 0) {
- std: :cerr << "lttng_start_tracing: " << lttng_strerror(ret) << std: :endl;
- return - 1;
- }
到这里为止是不是很简单? 尽管没有文档, 但是这些 api 都是非常简单的 api, 看源代码就可以推测如何调用.
在告诉 LTTng 启用跟踪后, 我还需要获取发送到 LTTng 的事件, 在 ETW 中获取事件是通过注册回调获取的:
- EVENT_TRACE_LOGFILE trace = {};
- trace.LoggerName = (char * ) mySessionName.c_str();
- trace.EventRecordCallback = (PEVENT_RECORD_CALLBACK)(StaticRecordEventCallback);
- trace.BufferCallback = (PEVENT_TRACE_BUFFER_CALLBACK)(StaticBufferEventCallback);
- trace.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD | PROCESS_TRACE_MODE_REAL_TIME;
- TRACEHANDLE sessionHandle = ::OpenTrace( & trace);
- if (sessionHandle == INVALID_PROCESSTRACE_HANDLE) {
- // ...
- }
- ULONG processStatus = ::ProcessTrace( & sessionHandle, 1, nullptr, nullptr);
我寻思 lttng 有没有这样的机制, 首先我看到的是 lttng.h 中的
函数, 这个函数的注释如下:
- lttng_register_consumer
- This call registers an "outside consumer"
- for a session and an lttng domain.No consumer will be spawned and all fds / commands will go through the socket path given(socket_path).
翻译出来就是给会话注册一个外部的消费者, 听上去和我的要求很像吧?
这个函数的第二个参数是一个字符串, 我推测是 unix socket, lttng 会通过 unix socket 发送事件过来.
于是我写了这样的代码:
- ret = lttng_register_consumer(handle, "/tmp/custom-consumer");
一执行立刻报错, 错误是 Command undefined, 也就是命令未定义, 服务端不支持这个命令.
经过搜索发现 lttng 的源代码中没有任何调用这个函数的地方, 也就是说这个函数是个装饰.
看起来这个办法行不通.
经过一番查找, 我发现了 live-reading-howto 这个文档, 里面的内容非常少但是可以看出使用 lttng-relayd 这个服务可以读取事件.
读取事件目前只支持 TCP, 使用 TCP 传输事件数据不仅复杂而且效率很低, 相对 ETW 直接通过内存传递数据这无疑是个愚蠢的办法.
虽然愚蠢但是还是要继续写, 我开始看这 TCP 传输用的是什么协议.
对传输协议的解释文档在 live-reading-protocol.txt , 这篇文档写的很糟糕, 但总比没有好.
和 lttng-relayd 进行交互使用的是一个 lttng 自己创造的半双工二进制协议, 设计如下:
客户端发送命令给 lttng-relayd 需要遵从以下的格式
- [data_size: unsigned 64 bit big endian int, 命令体大小]
- [cmd: unsigned 32 bit big endian int, 命令类型]
- [cmd_version: unsigned 32 bit big endian int, 命令版本]
- [命令体, 大小是data_size]
发送命令的设计没有问题, 大部分二进制协议都是这样设计的, 问题在于接收命令的设计.
接收命令的格式完全依赖于发送命令的类型, 例如
这个命令发送过去会收到以下的数据:
- LTTNG_VIEWER_CONNECT
- [viewer_session_id: unsigned 64 bit big endian int, 服务端指定的会话ID]
- [major: unsigned 32 bit big endian int, 大版本]
- [minor: unsigned 32 bit big endian int, 中版本]
- [type: 客户端的类型]
可以看出接收的数据没有数据头, 没有数据头如何决定接收多少数据呢? 这就要求客户端定义的回应大小必须和服务端完全一致, 一个字段都不能漏.
服务端在以后的更新中不能给返回数据随意添加字段, 返回多少字段需要取决于发送过来的 cmd_version, 保持 api 的兼容性将会非常的麻烦.
目前在 lttng 中 cmd_version 是一个预留字段, 也就是他们没有仔细的想过 api 的更新问题.
正确的做法应该是返回数据也应该提供一个数据头, 然后允许客户端忽略多出来的数据.
看完协议以后, 我在想既然使用了二进制协议, 应该也会提供一个 sdk 来减少解析的工作量吧?
经过一番查找找到了一个头文件 lttng-viewer-abi.h , 包含了和 lttng-relayd 交互使用的数据结构体定义.
这个头文件在源代码里面有, 但是却不在 LTTng 发布的软件包中, 这意味着使用它需要复制它到项目里面.
复制别人的源代码到项目里面 不能那么随便 , 看了一下 LTTng 的开源协议, 在 include/lttng/* 和 src/lib/lttng-ctl/* 下的文件是 LGPL, 其余文件是 GPL,
也就是上面如果把这个头文件复制到自己的项目里面, 自己的项目必须使用 GPL 协议开源, 不想用 GPL 的话只能把里面的内容自己一行行重新写, 还不能写的太像.
既然是测试就不管这么多了, 把这个头文件的代码复制过来就开始继续写, 首先是连接到 lttng-relayd:
- int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- if (fd < 0) {
- perror("socket");
- return - 1;
- }
- sockaddr_in address = {};
- address.sin_addr.s_addr = inet_addr("127.0.0.1");
- address.sin_family = AF_INET;
- address.sin_port = htons(5344);
- ret = connect(fd, (sockaddr * ) & address, sizeof(address));
- if (ret < 0) {
- perror("connect");
- return - 1;
- }
连接成功以后的交互流程在阅读上面的协议文档以后可以整理如下:
- 初始化
- 客户端发送命令 LTTNG_VIEWER_CLIENT_COMMAND + 构造体 lttng_viewer_connect
- 服务端返回构造体 lttng_viewer_connect
- 客户端发送命令 LTTNG_VIEWER_CREATE_SESSION + 构造体 lttng_viewer_create_session_response
- 服务端返回构造体 lttng_viewer_create_session_response
- 列出会话
- 客户端发送命令 LTTNG_VIEWER_LIST_SESSIONS, 不带构造体
- 服务端返回构造体 lttng_viewer_list_sessions + 指定长度的 lttng_viewer_session
- 附加到会话
- 客户端发送命令 LTTNG_VIEWER_ATTACH_SESSION + 构造体 lttng_viewer_attach_session_request
- 服务端返回构造体 lttng_viewer_attach_session_response + 指定长度的 lttng_viewer_stream
- 循环 {
- 如果需要获取新的流 {
- 客户端发送命令 LTTNG_VIEWER_GET_NEW_STREAMS + 构造体 lttng_viewer_new_streams_request
- 服务端返回构造体 lttng_viewer_new_streams_response + 指定长度的 lttng_viewer_stream
- }
- 如果需要获取新的元数据(metadata) {
- 枚举现存的metadata流列表 {
- 客户端发送命令 LTTNG_VIEWER_GET_METADATA + 构造体 lttng_viewer_get_metadata
- 服务端返回构造体 lttng_viewer_metadata_packet + 指定长度的payload
- }
- }
- 枚举现存的trace流列表 {
- 客户端发送命令 LTTNG_VIEWER_GET_NEXT_INDEX + 构造体 lttng_viewer_get_next_index
- 服务端返回构造体 lttng_viewer_index
- 检查返回的 index.flags, 如果服务端出现了新的流或者元数据, 需要先获取新的流和元数据才可以继续
- 客户端发送命令 LTTNG_VIEWER_GET_PACKET + 构造体 lttng_viewer_trace_packet
- 服务端返回构造体 lttng_viewer_trace_packet + 指定长度的payload
- 根据metadata packet和trace packet分析事件的内容然后记录事件
- }
- }
是不是觉得很复杂?
因为协议决定了服务端发给客户端的数据没有数据头, 所以服务端不能主动推送数据到客户端, 客户端必须主动的去进行轮询.
如果你注意到构造体的名称, 会发现有的构造体后面有 request 和 response 而有的没有, 如果不看上下文只看构造体的名称很难猜到它们的作用.
正确的做法是所有请求和返回的构造体名称末尾都添加 request 和 response, 不要去省略这些字母而浪费思考的时间.
为了发送命令和接收构造体我写了一些帮助函数, 它们并不复杂, 使用 TCP 交互的程序都会有类似的代码:
- int sendall(int fd, const void * buf, std: :size_t size) {
- std: :size_t pos = 0;
- while (pos < size) {
- auto ret = send(fd, reinterpret_cast < const char * >(buf) + pos, size - pos, 0);
- if (ret <= 0) {
- return - 1;
- }
- pos += static_cast < std: :size_t > (ret);
- }
- return 0;
- }
- int recvall(int fd, void * buf, std: :size_t size) {
- std: :size_t pos = 0;
- while (pos < size) {
- auto ret = recv(fd, reinterpret_cast < char * >(buf) + pos, size - pos, 0);
- if (ret <= 0) {
- return - 1;
- }
- pos += static_cast < std: :size_t > (ret);
- }
- return 0;
- }
- template < class T > int sendcmd(int fd, std: :uint32_t type, const T & body) {
- lttng_viewer_cmd cmd = {};
- cmd.data_size = htobe64(sizeof(T));
- cmd.cmd = htobe32(type);
- if (sendall(fd, &cmd, sizeof(cmd)) < 0) {
- return - 1;
- }
- if (sendall(fd, &body, sizeof(body)) < 0) {
- return - 1;
- }
- return 0;
- }
初始化连接的代码如下:
- lttng_viewer_connect body = {};
- body.major = htobe32(2);
- body.minor = htobe32(9);
- body.type = htobe32(LTTNG_VIEWER_CLIENT_COMMAND);
- if (sendcmd(fd, LTTNG_VIEWER_CONNECT, body) < 0) {
- return - 1;
- }
- if (recvall(fd, &body, sizeof(body)) < 0) {
- return - 1;
- }
- viewer_session_id = be64toh(body.viewer_session_id);
后面的代码比较枯燥我就省略了, 想看完整代码的可以看 这里 .
进入循环后会从 lttng-relayd 获取两种有用的数据:
获取元数据使用的是 LTTNG_VIEWER_GET_METADATA 命令, 获取到的元数据内容如下:
- Wu@"Jtf@oe/* CTF 1.8 */
- typealias integer { size = 8; align = 8; signed = false; } := uint8_t;
- typealias integer { size = 16; align = 8; signed = false; } := uint16_t;
- typealias integer { size = 32; align = 8; signed = false; } := uint32_t;
- typealias integer { size = 64; align = 8; signed = false; } := uint64_t;
- typealias integer { size = 64; align = 8; signed = false; } := unsigned long;
- typealias integer { size = 5; align = 1; signed = false; } := uint5_t;
- typealias integer { size = 27; align = 1; signed = false; } := uint27_t;
- trace {
- major = 1;
- minor = 8;
- uuid = "a3df4090 - 0722 - 4a74 - 97a4 - 81e066406f03 ";
- byte_order = le;
- packet.header := struct {
- uint32_t magic;
- uint8_t uuid[16];
- uint32_t stream_id;
- uint64_t stream_instance_id;
- };
- };
- env {
- hostname = "ubuntu - virtual - machine ";
- domain = "ust ";
- tracer_name = "lttng - ust ";
- tracer_major = 2;
- tracer_minor = 9;
- };
- clock {
- name = "monotonic ";
- uuid = "f397e532 - 4837 - 402b - 8cc9 - 700ed92a339d ";
- description = "Monotonic Clock ";
- freq = 1000000000; /* Frequency, in Hz */
- /* clock value offset from Epoch is: offset * (1/freq) */
- offset = 1514336042565610080;
- };
- typealias integer {
- size = 27; align = 1; signed = false;
- map = clock.monotonic.value;
- } := uint27_clock_monotonic_t;
- typealias integer {
- size = 32; align = 8; signed = false;
- map = clock.monotonic.value;
- } := uint32_clock_monotonic_t;
- typealias integer {
- size = 64; align = 8; signed = false;
- map = clock.monotonic.value;
- } := uint64_clock_monotonic_t;
- struct packet_context {
- uint64_clock_monotonic_t timestamp_begin;
- uint64_clock_monotonic_t timestamp_end;
- uint64_t content_size;
- uint64_t packet_size;
- uint64_t packet_seq_num;
- unsigned long events_discarded;
- uint32_t cpu_id;
- };
- struct event_header_compact {
- enum : uint5_t { compact = 0 ... 30, extended = 31 } id;
- variant <id> {
- struct {
- uint27_clock_monotonic_t timestamp;
- } compact;
- struct {
- uint32_t id;
- uint64_clock_monotonic_t timestamp;
- } extended;
- } v;
- } align(8);
- struct event_header_large {
- enum : uint16_t { compact = 0 ... 65534, extended = 65535 } id;
- variant <id> {
- struct {
- uint32_clock_monotonic_t timestamp;
- } compact;
- struct {
- uint32_t id;
- uint64_clock_monotonic_t timestamp;
- } extended;
- } v;
- } align(8);
- stream {
- id = 0;
- event.header := struct event_header_compact;
- packet.context := struct packet_context;
- event.context := struct {
- integer { size = 32; align = 8; signed = 1; encoding = none; base = 10; } _vpid;
- integer { size = 32; align = 8; signed = 1; encoding = none; base = 10; } _vtid;
- };
- };
- event {
- name = "DotNETRuntime: GCStart_V2 ";
- id = 0;
- stream_id = 0;
- loglevel = 13;
- fields := struct {
- integer { size = 32; align = 8; signed = 0; encoding = none; base = 10; } _Count;
- integer { size = 32; align = 8; signed = 0; encoding = none; base = 10; } _Depth;
- integer { size = 32; align = 8; signed = 0; encoding = none; base = 10; } _Reason;
- integer { size = 32; align = 8; signed = 0; encoding = none; base = 10; } _Type;
- integer { size = 16; align = 8; signed = 0; encoding = none; base = 10; } _ClrInstanceID;
- integer { size = 64; align = 8; signed = 0; encoding = none; base = 10; } _ClientSequenceNumber;
- };
- };"
这个元数据的格式是 CTF Metadata , 这个格式看上去像 json 但是并不是, 是 LTTng 的公司自己创造的一个文本格式.
babeltrace 中包含了解析这个文本格式的代码, 但是没有开放任何解析它的接口, 也就是如果你想自己解析只能写一个词法分析器.
这些格式其实可以使用 json 表示, 体积不会增加多少, 但是这公司硬是发明了一个新的格式增加使用者的负担.
写一个词法分析器需要 1 天时间和 1000 行代码, 这里我就先跳过了.
接下来获取跟踪数据, 使用的是 LTTNG_VIEWER_GET_NEXT_INDEX 和 LTTNG_VIEWER_GET_PACKET 命令.
LTTNG_VIEWER_GET_NEXT_INDEX 返回了当前流的 offset 和可获取的 content_size, 这里的 content_size 单位是位 (bit), 也就是需要除以 8 才可以算出可以获取多少字节,
关于 content_size 的单位 LTTng 中没有任何文档和注释说明它是位, 只有一个测试代码里面的某行写了 / CHAR_BIT.
使用 LTTNG_VIEWER_GET_PACKET 命令, 传入 offset 和 content_size/8 可以获取跟踪数据 (如果不 / 8 会获取到多余的数据或者返回 ERR).
实际返回的跟踪数据如下:
- 000000: c1 1f fc c1 29 82 6b fe 24 10 4c 6b 97 91 4d c3 ....).k.$.Lk..M.
- 000010: ed d4 41 8f 00 00 00 00 03 00 00 00 00 00 00 00 ..A.............
- 000020: 92 91 49 96 08 0a 00 00 07 a0 58 b9 08 0a 00 00 ..I.......X.....
- 000030: 50 05 00 00 00 00 00 00 00 80 00 00 00 00 00 00 P...............
- 000040: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 000050: 03 00 00 00 1f 00 00 00 00 92 91 49 96 08 0a 00 ...........I....
- 000060: 00 e1 1b 00 00 03 00 00 00 02 00 00 00 01 00 00 ................
- 000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1f ................
- 000080: 00 00 00 00 4d ae a7 af 08 0a 00 00 e1 1b 00 00 ....M...........
- 000090: 04 00 00 00 02 00 00 00 01 00 00 00 00 00 00 00 ................
- 0000a0: 00 00 00 00 00 00 00 00 00 00 ..........
跟踪数据的格式是 CTF Stream Packet , 也是一个自定义的二进制格式, 需要配合元数据解析.
babeltrace 中同样没有开放解析它的接口 (有 python binding 但是没有解析数据的函数), 也就是需要自己写二进制数据解析器.
操作 LTTng + 和 relayd 通讯 + 元数据词法分析器 + 跟踪数据解析器全部加起来预计需要 2000 行代码, 而这一切使用 ETW 只用了 100 多行代码.
糟糕的设计, 复杂的使用, 落后的文档, 各种各样的自定义协议和数据格式, 不提供 SDK 把 LTTng 打造成了一个比 ETW 更难用的跟踪系统.
目前在 github 上 LTTng 只有 100 多星而 babeltrace 只有 20 多, 也印证了没有多少人在用它们.
我不清楚为什么 CoreCLR 要用 LTTng, 但欣慰的是 CoreCLR 2.1 会有新的跟踪机制 EventPipe , 到时候可以更简单的实现跨平台捕获 CoreCLR 跟踪事件.
我目前写的调用 ETW 的代码放在了 这里 , 调用 LTTng 的代码放在了 这里 , 有兴趣的可以去参考.
最差的 API(ETW) 和更差的 API(LTTng) 都看过了, 那么应该如何避免他们的错误, 编写一个好的 API 呢
Casey Muratori 提到的教训有:
设计一个 API 时, 首先要做的是站在调用者的立场, 想想调用者需要什么, 如何才能最简单的达到这个需求.
编写一个简单的用例代码永远是设计 API 中必须的一步.
不要过多的去想内部实现, 如果内部实现机制让 API 变得复杂, 应该想办法去抽象它.
因为需求会不断变化, 设计 API 的时候应该为未来的变化预留空间, 保证向后兼容性.
例如 ETW 中 监听的事件类型 使用了位标记, 也就是参数是 32 位时最多只能有 32 种事件, 考虑到未来有更多事件应该把事件类型定义为连续的数值并提供额外的 API 启用事件.
现在有很多接口在设计时会考虑到版本, 例如用 v1 和 v2 区分, 这是一个很好的策略.
不要为了节省代码去让一个接口接收或者返回多余的信息.
在 ETW 中很多接口都共用了一个大构造体
, 调用者很难搞清楚接口使用了构造体里面的哪些值, 又影响了哪些值. 设计 API 时应该明确接口的目的, 让接口接收和返回必要且最少的信息.
- EVENT_TRACE_PROPERTIES
对调用者来说, 100 行的示例代码通常比 1000 行的文档更有意义.
因为接口的设计者和调用者拥有的知识量通常不对等, 调用者在没有看到实际的例子之前, 很可能无法理解设计者编写的文档.
这是很多接口都会犯的错误, 例如 ETW 中决定事件附加的信息时, 1 表示时间戳, 2 表示系统时间, 3 表示 CPU 周期计数.
如果你需要传递具有某种意义的数字给接口, 请务必在 SDK 中为该数字定义枚举类型.
我从 LTTng 中吸收到的教训有:
99% 的调用者没有看源代码的兴趣或者能力, 不写文档没有人会懂得如何去调用你的接口.
现在有很多自动生成文档的工具, 用这些工具可以减少很多的工作量, 但是你仍然应该手动去编写一个入门的文档.
创造一个新的协议意味着需要编写新的代码去解析它, 而且每个程序语言都要重新编写一次.
除非你很有精力, 可以为主流的程序语言都提供一个 SDK, 否则不推荐这样做.
很多项目都提供了 REST API, 这是很好的趋势, 因为几乎每个语言都有现成的类库可以方便地调用 REST API.
定义一个好的二进制协议需要很深的功力, LTTng 定义的协议明显考虑的太少.
推荐的做法是明确区分请求和回应, 请求和回应都应该有一个带有长度的头, 支持全双工通信.
如果你想设计一个二进制协议, 强烈建议参考 Cassandra 数据库的 协议文档 , 这个协议无论是设计还是文档都是一流的水平.
但是如果你没有对传输性能有很苛刻的要求, 建议使用现成的协议加 json 或者 xml.
这里我没有写轻易, 如果你有一个数据结构需要表示成文本, 请使用更通用的格式.
LTTng 表示元数据时使用了一个自己创造的 DSL, 但里面的内容用 json 表示也不会增加多少体积, 也就是说创造一个 DSL 没有任何好处.
解析 DSL 需要自己编写词法分析器, 即使是经验老道的程序员编写一个也需要不少时间 (包含单元测试更多), 如果使用 json 等通用格式那么编写解析的代码只需要几分钟.
虽然这篇文章把 LTTng 批评了一番, 但这可能是目前全世界唯一一篇提到如何通过代码调用 LTTng 和接收事件数据的文章.
希望看过这篇文章的设计 API 时多为调用者着想, 你偷懒省下几分钟往往会导致别人浪费几天的时间.
来源: https://www.cnblogs.com/zkweb/p/8126303.html