维护开源 C 库:从 librdkafka 吸取的教训

维护开源 C 库:从 librdkafka 吸取的教训

原文:https://medium.com/hackernoon/maintaining-an-open-source-c-library-lessons-learnt-from-librdkafka-8666ad49c9f5

大约四年来,我一直维护着开源 Apache Kafka C/C++客户端库- librdkafka ,这篇博客文章概述了我从它小小的开源旅程中学到的东西,涵盖了从位级兼容性到翻译英语的一切。

无数(如果你真的不擅长计数,像我一样)应用程序是用 librdkafka 编写的,社区在它的基础上为几乎所有流行的编程语言编写了语言绑定,比如我为汇合 : 汇合-kafka-python汇合-kafka-go 编写的两个。更多信息,请参见此有点完整的列表

让我们言归正传。

开源社区

开源是伟大的,所有这些都是无情的。在过去的三年里,我平均每天花 1-2 小时为在线开源社区提供免费支持。大部分是回答问题,解释概念,指出已经在官方文档里了。然后是不可避免的和无数的故障排除和用户的假设不符合应用库合同的指导。最后,最不重要也是最重要的一点是:为库中的实际 bug 提交的不同质量的问题。

没有太多的选择,这就是开源的工作方式。

如果我能给寻求帮助的用户一些建议,那就是:

  • 你是程序员,我是程序员,请给我足够的信息来理解和调试你的问题。给我日志,配置,代码片段和回溯。请告诉我日志中哪里出错了(我收到了千兆字节大小的日志文件,但没有指出要查找什么)。我很擅长猜测,但我从来没猜对过。对该问题进行故障诊断需要哪些信息?
  • 请始终用库的最新版本再现您的问题。但是如果您不能复制,那也没关系,而且一次性事件的日志也很有用。
  • 我会花时间调查你的问题,我会花时间免费帮助你和其他人。请在你的问题上投入一些努力,这将帮助我们所有人,尤其是你。
  • 提交问题之前:阅读常见问题,阅读文档,搜索现有的(开放和关闭的)问题。如果你还是运气不好;将问题归档。
  • 回馈:改进文档,写博客,回答 StackOverflow 问题,回答其他用户在 GitHub 上的问题。
  • 反馈:正如任何支持渠道一样,当事情按计划进行时,你永远不会听到成功的故事。每个人都喜欢听到他们的项目进入实际应用,所以请发送电子邮件简要概述您的用例,如果您对该项目感到满意,这将使我的一天非常愉快。

但是,对于用户来说,这可能是错误的媒介。

因此,作为开源库的维护者,这里有一些指导方针:

  • 要有礼貌,尊重他人,把每一个用户都当成付费客户——如果你这样做了,他们很有可能有一天也会成为付费客户(不管这在你的情况下意味着什么)。
  • 语言障碍——你会看到很多英语极差的问题。如果你不是英语教师,你对此无能为力,这些问题的价值不亚于你那篇铺张的美国论文。深呼吸,开始吃吧。
  • 为你的工作感到自豪。您的项目质量在实际运行时特性、代码质量和社区交流质量之间平均分布。如果你不倾向于问题,或者你告诉人们他们是白痴(直接或间接),那么你的项目将会遭受损失,这是理所当然的。莱纳斯可能会逃脱,但他真的不应该。
  • 很多公司,尤其是大公司,不允许公开讨论内部系统,因此你应该在项目的 GitHub 页面上提供你的邮箱。出于跟踪的目的,你最终还是会想把这个问题放到 GitHub 上,而忽略掉任何可以识别用户或公司的信息。
  • 代码风格指南——写一个,它可能真的很糟糕,但它仍然比你代码中的混合风格要好。确保没有公关是不遵守它的,包括你自己的。
  • 写文档——只有你真正了解你的库是如何工作的。确保你的假设是书面的,确保申请图书馆的合同是清楚的。这是一项单调乏味的工作,但它是值得的,并不是说大多数用户会在提交问题之前阅读您的文档,这太费力了,但它允许您快速回复您的文档的相关部分的链接。如果你需要解释一个问题中的东西,要养成更新文档的习惯,否则你一定会再做一次。
  • 没有好的内联代码文档框架,只需选择一个并保持一致。
  • 互动——有些问题更容易实时发现——加入一个 IRC 频道或获得一个免费的 Gitter 频道来互动地支持你的用户。这里是自由卡夫卡吉特频道

用例——或者,你不知道的

当您决定创建您的项目时,您可能有一个用例,并且它很可能会相应地影响项目的设计和界面。

但是你不仅仅是在为自己制作这个软件,你还希望其他人也能使用它。适应毕竟是开源项目的目标。

如果我能告诉你一件关于用户用例的事情,那就是你不知道它们是什么,当然——它们可能会不时地出现在问题和电子邮件中,但你只是看到了冰山一角。

考虑到这一点,您必须尽最大努力不要限制软件的使用:

  • 不要对性能或延迟做任何假设——尽最大努力实现你能想到的最快、最有效的实现。慢速库对于大多数初始用户来说是可以的,但是对于高要求的用例来说就不行了。一个快速高效的图书馆将为每个人服务,从零开始。基准测试是一个非常方便的卖点——每个人都喜欢快速。
  • 节约 CPU 和内存不是你的。写绿色代码。或者如谢丽尔所说:要有品味,不要浪费。
  • 如果你的库混洗了很多用户数据,在适当的地方提供可选的零拷贝接口。
  • 写一个功能性的 API(不像 FP 那样,而是关注功能性,而不是技术性)。抽象出潜在的困难和协议或实现细节,提供一个做正确事情的 API。你是领域专家,而不是你的用户,他们只是想给他们的程序增加一点功能。
  • 不要求用户理解线程。使库成为线程安全的,这使得它在线程和非线程环境中都可用。例如:允许从不同的用户线程调用方法(在有意义的地方),避免从内部线程调用回调,总是从用户线程调用它们(通过服务轮询调用),否则你将迫使用户理解并正确实现并发性——这很难。
  • 拯救用户自己。你是一个了不起的程序员,你的一些用户可能不是——所以为了使库可访问——远离复杂的 API 或范例——不要求用户理解低级线程原语、分布式系统等,只要给他们他们想要的并保证他们的安全。
  • 不做同步或者阻塞接口,就是不做。它们是我们珍贵花园里的蛇。例子:相当多的 librdkafka 用户要求一个同步/阻塞的 produce() API,它产生一个消息,等待传递,然后返回。这非常实用,你只需调用 produce(),当它返回时,你就知道一切正常了。所以你在你的开发机器上试了一下,它看起来表现很好,延迟在亚毫秒级。你可以每秒钟发出数千个呼叫,这没问题,让我们投入生产吧。生产机器更强大了,现在运转得更快了。然后,负载增加,延迟开始增加,网络出现问题,突然之间,您之前非常快速的 produce()调用需要 10 到 50 毫秒才能完成。您的吞吐量从 3000 msgs/s 骤降至 30 msgs/s,背压沿着您的数据管道向上流动,队列开始堆积。数据源停止接受新的输入,你的系统逐渐停止,辅助设备像多米诺骨牌一样在你的平台上倒下。相反,如果您提供了一个带有未来或回调的 异步 API *来指示状态,并允许同时进行多个操作,那么您就不会受到延迟增加的影响,您的吞吐量也不会受到影响。由于这个原因,我决定不提供 sync produce(),而是选择解释为什么这是不好的,并把用户引向正确的方向,使他们免于未来的问题。如果你知道得更多,分享这些知识*。现在,对于那些确实理解问题并且仍然想做的人(他们真的不应该这么做),提供一个可行的替代方案(例如,我提供了一个带有示例的 wiki 页面)。

API 和 ABI 保证

任何名副其实的 C 库都必须保证 API 和 ABI 的稳定性。SONAME 碰撞是不被允许的,必须尽可能避免,因为它可能会破坏现有的应用程序。

API 稳定性意味着用新版本的库重新编译现有程序将会成功,并且程序与库的交互保持不变。

ABI 稳定性意味着不重新编译程序也是一样的。虽然 API 稳定性允许改变公共类型等,但 ABI 稳定性不允许——该库必须在所有公共接口的位级上与以前的版本完全二进制兼容。

  • 让你所有的结构都私有,是的,所有的结构都私有,我还没有添加一个最终不需要扩展或修改的公共结构,如果不破坏现有的应用程序,你根本无法做到这一点。
  • 根据需要向您的私有类型、getters 和 setters 添加访问器方法。
  • 不要泄漏内部符号,使用一个链接器脚本只暴露你的公共符号。librdkafka 为其公共头文件使用定义良好的格式,并使用一个小的 Python 脚本来提取所有公共函数并将它们写入一个链接器脚本文件,然后在库编译时传递给链接器(如果支持(gcc,clang))。
  • 定义也是最终的,对任何可能改变的东西使用全局 getter 函数。一个例子是 RD_KAFKA_DEBUG_CONTEXTS,这是一个很好的包含 librdkafka 支持的调试上下文的 CSV 字符串,例如:“主题,代理,cgrp,元数据,…”。我认为通过 define 以字符串形式提供它会很方便,但结果是这打破了 ABI——使用此 define 用旧版本编译的程序在库被替换后不会获得新的调试上下文(但程序不会重新编译),它现在也提供了一个方法。唯一值得注意的例外是库版本,例如# define RD KAFKA VERSION 0x 00090300,您必须提醒您的用户仅在编译期间使用该版本进行 API 发现。
  • 提供向后兼容和面向未来的接口的一种方便的方法是使用 var-arg 函数,该函数采用以标记终止的元组列表。每个元组由一个元组 id 和该令牌的特定参数组成。为了便于使用,提供每个元组的宏,这些宏接受预期数量的参数,可选地(并且依赖于工具链支持(gcc,clang))验证参数的类型,并且最重要的是,将提供的参数转换为它们预期的类型(因为在函数中未转换的 int 不能被正确地读取为 int64_t)。更多信息见 rdkafka.h

配置

设计您的配置属性,使它们反映期望的行为,而不是如何实现。让他们成为合同

轻便

可移植性剥夺了编程的乐趣。

可移植性实际上意味着:

  • 你不能使用过去 15 年左右添加的任何语言功能。是的,人们被困在旧企业系统的旧工具链上。你会在 RHEL 2.1 上看到 13 年前 gcc 的问题。你必须在某个地方画一条线,librdkafka 的模糊线在 C89 和 C99 之间,没有明确支持供应商工具链(但它们可能会工作)。
  • POSIX 很棒而且大部分都是便携的,坚持最小的功能集,坐在船上。
  • 使用自动配置(但是看在上帝的份上,不要 autoconf )。 mklove 是我的非臃肿的选择(mklove 是“做爱,不要战争”,但用它的同义词 autoconf 代替了战争)。
  • Win32 支持——唉,真是一团糟。微软在他们的(某种)POSIX 兼容 API 中几乎做错了所有的事情。怪异或错误的行为,下划线前缀一切,添加 _s 以表明事情是安全的(什么?!).好主意!这实际上意味着您应该从一开始就抽象网络和 IO 代码,以使 Win32 移植更容易。对于线程,我强烈建议使用 tinycthread ,它是一个类似 C11 的线程抽象,适用于 POSIX 和 Win32。尽管 Visual Studio 非常好。
  • 使用 CIs 获得关于可移植性问题的快速反馈。 Travis -CI 提供 Linux 和 OSX 版本, AppVeyor 提供 Win32。还要看看 Suse 开放构建服务,它允许你为几乎任何 Linux 发行版进行构建(但是与现代网络的集成很差,例如 Github)。安德烈亚斯·斯马斯的 CI 项目看起来也很有希望。

为了达到最佳效果,请在美国东部时间周二上午发布。

请你喝一杯啤酒。

对于小团队来说,维护多个发布是很难的,所以尽量避免带有反向移植修复的多个发布分支,增加的测试矩阵和维护成本是没有回报的。

一些真相

  • gettimeofday()在 Linux 上非常便宜(内核映射内存,没有 syscall)。
  • 不要检查 malloc(constant size) (et.al)返回值是否为 NULL,除非您分配的是大块(兆字节)或可变大小(您事先不知道大小,比如 st.st_size)。现代平台过度使用内存,当虚拟内存需要映射到物理页面时,失败的不是 malloc 调用,而是后来的页面错误。
  • 注意计算的 malloc 大小,例如 malloc(n m)不会换行。*
  • 无竞争互斥在 Linux 上很便宜。
  • 另一方面,原子有时比预期的更昂贵,但不比互斥锁差。

半成品

这篇文章还在进行中,随着时间的推移会添加更多的注释(除非这真的是开源项目的全部,那我就完了)。


本站为非盈利网站,作品由网友提供上传,如无意中有侵犯您的版权,请联系删除