比最差的API(ETW)更差的API(LTTng)是如何煉成的, 談如何寫一個好的接口


最近這幾天在幫檸檬看她的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, 原因包括:

  • 事件類型使用了位標志(最多只能有32個), 沒有考慮到未來的情況
  • 不同的接口共用一個大的構造體, 接口的輸入和輸出不明確
  • 讓調用者寫無論怎么看都是多余的代碼
  • 讓調用者使用魔法數字(而不是提供一個枚舉值)
  • 命名帶有誤導性
  • 返回值的意義不統一
  • 使用過於復雜, 沒有先想好用例
  • 文檔里面沒有完整的示例代碼, 只能從零碎的代碼拼湊

然而Casey Muratori的文章對我幫助很大, 我只用了1天時間就寫出了使用ETW收集.Net運行時事件的示例代碼.
之后我開始看如何使用LTTng收集這些事件, 按照我以往的經驗linux上的類庫api通常會比windows的好用, 但LTTng是個例外.

我第一件做的事情是去查找怎樣在c程序里面LTTng的接口, 我打開了他們的文檔然后開始瀏覽.
很快我發現了他們的文檔只談了如何使用代碼發送事件, 卻沒有任何說明如何用代碼接收事件, 我意識到我應該去看源代碼.

初始化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_create_session函數可以通過引用lttng.h調用.
再過了幾分鍾我寫出了第一行代碼

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會返回成功了, 但是又發現了新的問題,
創建的會話是由一個單獨的服務管理的, 即使當前進程退出會話也會存在, 第二次創建的時候會返回一個已存在的錯誤.
這個問題和ETW的問題一模一樣, 解決方法也一模一樣, 在創建會話前關閉它就可以了.

於是代碼變成了這樣:

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獲取兩種有用的數據:

  • 元數據(metadata), 定義了跟蹤數據的格式
  • 跟蹤數據(trace), 包含了事件信息例如GC開始和結束等

獲取元數據使用的是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變得復雜, 應該想辦法去抽象它.

考慮到未來的擴展

因為需求會不斷變化, 設計API的時候應該為未來的變化預留空間, 保證向后兼容性.
例如ETW中監聽的事件類型使用了位標記, 也就是參數是32位時最多只能有32種事件, 考慮到未來有更多事件應該把事件類型定義為連續的數值並提供額外的API啟用事件.
現在有很多接口在設計時會考慮到版本, 例如用v1和v2區分, 這是一個很好的策略.

明確接口的輸入和輸出

不要為了節省代碼去讓一個接口接收或者返回多余的信息.
在ETW中很多接口都共用了一個大構造體EVENT_TRACE_PROPERTIES, 調用者很難搞清楚接口使用了構造體里面的哪些值, 又影響了哪些值.
設計API時應該明確接口的目的, 讓接口接收和返回必要且最少的信息.

提供完整的示例代碼

對調用者來說, 100行的示例代碼通常比1000行的文檔更有意義.
因為接口的設計者和調用者擁有的知識量通常不對等, 調用者在沒有看到實際的例子之前, 很可能無法理解設計者編寫的文檔.

不要使用魔法數字

這是很多接口都會犯的錯誤, 例如ETW中決定事件附加的信息時, 1表示時間戳, 2表示系統時間, 3表示CPU周期計數.
如果你需要傳遞具有某種意義的數字給接口, 請務必在SDK中為該數字定義枚舉類型.

我從LTTng中吸收到的教訓有:

寫文檔

99%的調用者沒有看源代碼的興趣或者能力, 不寫文檔沒有人會懂得如何去調用你的接口.
現在有很多自動生成文檔的工具, 用這些工具可以減少很多的工作量, 但是你仍然應該手動去編寫一個入門的文檔.

不要輕易的去創造一個協議

創造一個新的協議意味着需要編寫新的代碼去解析它, 而且每個程序語言都要重新編寫一次.
除非你很有精力, 可以為主流的程序語言都提供一個SDK, 否則不推薦這樣做.
很多項目都提供了REST API, 這是很好的趨勢, 因為幾乎每個語言都有現成的類庫可以方便地調用REST API.

謹慎的去定義二進制協議

定義一個好的二進制協議需要很深的功力, LTTng定義的協議明顯考慮的太少.
推薦的做法是明確區分請求和回應, 請求和回應都應該有一個帶有長度的頭, 支持全雙工通信.
如果你想設計一個二進制協議, 強烈建議參考Cassandra數據庫的協議文檔, 這個協議無論是設計還是文檔都是一流的水平.
但是如果你沒有對傳輸性能有很苛刻的要求, 建議使用現成的協議加json或者xml.

不要去創造一個DSL(Domain-specific language)

這里我沒有寫輕易, 如果你有一個數據結構需要表示成文本, 請使用更通用的格式.
LTTng表示元數據時使用了一個自己創造的DSL, 但里面的內容用json表示也不會增加多少體積, 也就是說創造一個DSL沒有任何好處.
解析DSL需要自己編寫詞法分析器, 即使是經驗老道的程序員編寫一個也需要不少時間(包含單元測試更多), 如果使用json等通用格式那么編寫解析的代碼只需要幾分鍾.

寫在最后

雖然這篇文章把LTTng批評了一番, 但這可能是目前全世界唯一一篇提到如何通過代碼調用LTTng和接收事件數據的文章.
希望看過這篇文章的設計API時多為調用者着想, 你偷懶省下幾分鍾往往會導致別人浪費幾天的時間.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM