【核心技术】现代软件构建之旅:功夫核心库七年开发实践与洞见

董可人/CEO

我们维护的功夫核心库https://libkungfu.cc是一个开源的工业级极速低延迟计算框架,目前主要应用在对延迟性能极为敏感,有着近乎变态要求的量化交易行业。项目是2017年发布,至今已有7年的生命和众多的专业机构用户(仅在国内就有至少上百家):

GitHub: Kungfu​github.com/kungfu-origin/kungfu

由于功夫核心库主要的用户很多是专业的投资机构,除了对性能要求极其严格,他们对稳定性的要求也是非常高的,对于代码 bug 几乎是零容忍。在这种压力下,还要选择开源的形式,肯定要承受用户对于代码质量的苛刻检验。面对这种挑战,我们是通过如下的工作来确保项目质量的。

代码结构

由于核心库需要应用在很多种不同的场景下,例如跨平台(支持 Win/Linux/MacOS), GUI/TUI、Headless 都需要,还有很多不同的业务模块组合,首先需要有一个精心设计的框架性结构来把功能解耦合。而且因为本身对于性能的极致要求,核心的部分选择了用 C++ 来编写;但是在业务层面,首先用户很大程度上有刚需使用 Python 来进行各种数据处理和科学计算,其次我们也希望能利用 Web 前端技术方面的开发和人才优势,所以还选择了基于 JS 的 Vue 框架来做 UI,这就产生了一种 C++/Python/JS 的混合式开发模式,也对框架的整合能力有很高的要求。

经过权衡,我们最终选择了这样几个核心原则来设计整个框架:

以 C++ 为核心开发内核来确保关键性能指标

作为老牌的“零成本抽象”开发语言,C++在性能上的优势是无需多言的。虽然作为远古语言 C++ 背负了很多历史包袱,但是在我们的场景里,因为使用了一系列诸如零拷贝、状态机复制、单线程模型等设计思想,那些负面的包袱很少真正影响到我们。并且考虑下面要讲到的粘合剂作用,相比时代新秀 Rust 我们最终还是选择使用 C++ 来进行内核开发。关于核心库的设计思路,可以参考我专栏的几篇文章:

 
 

以 C++ Binding 的方式来作为语言粘合剂

通过模板元编程技术实现了一套跨语言的类型定义机制,支持一套数据类型能够无缝使用在 C++/Python/JS 里。可能有的读者会想到诸如 Protobuf 这一类工具库也能解决类似问题,但是考虑到如果使用这种工具,需要在编译步骤里额外的引入代码生成代码的环节,这种负担对于一个需要多人协作的复杂项目来说有不小的副作用:

  • 需要在编译流程里增加额外的工具调用,这需要增加开发者的学习成本;
  • 对自动生成的代码进行版本管理会带来这样一个两难选择:
    • 把生成的代码加入到版本追踪 – 这就很难避免一些开发者无意中手动修改这些代码而带来意外的问题;
    • 把生成的代码加入某个临时目录 – 这会给代码阅读带来额外负担,任何想要阅读完整代码的人首先都必须成功执行编译流程

如果项目的协作者比较少,上面这种问题比较容易控制,大家都是熟人打个招呼就能沟通;但是对于一个长生命周期的项目来说,需要考虑到随着时间发展,不断会有新人加入到项目开发中,这些埋藏在代码或文档细节里的额外工作必然会带来更多的培训和管理成本,老鸟们也会很快失去对新人的耐心。

我们最终选择使用 C++ 的模板元编程技术来自行设计开发了一套类型机制,虽然代价是核心代码的技巧性和理解难度都相当高,但是好处是能完全避免额外工具的引入和代码管理上的不便。并且,这个代价主要集中在类型的跨语言转化部分,在实际定义或修改类型数据的时候并不需要关心那些底层机制。这样就带来了很好的开发友好度,使得业务工作者也有非常大的灵活度来自行维护相关数据类型。

以 NPM 的包管理机制为基础来建立模块间的聚合框架

作为跨语言开发的混合项目,我们实际在项目中用到了多种包管理机制,甚至某些情况下会使用多种方案混合:

  • C++ 的 Conan
  • Python 的 Pipenv、Poetry、PDM
  • NodeJS 的 NPM/Yarn

虽然实际上这些包管理工具都支持很大的灵活性和开放性,选择其中任何一个作为项目整体的骨干框架都有可行性,但是如果考虑到在线服务的成熟度,那么 NPM 方案就是首选。作为连接各个业务模块的骨干框架,除了需要支持必要的参数化的模块管理,还需要能比较容易的发布到在线服务上并在后续的工作中灵活引用。除了基于 package.json 的简单配置机制外,NPM 最重要的加成在于我们使用的代码托管服务 GitHub 本身就支持完整的在线 npm-registry,结合 GitHub Actions 就能得到一个非常成熟的包管理服务。

版本管理

当我们谈一个复杂项目的“版本管理”时,并不仅仅是能熟练使用版本控制工具例如 git 就算是成功上岸的。特别是对一个跨平台、支持独立安装部署的产品来说,有这样几个关键问题需要考虑:

管理发布流程

发布一个固定的版本号(诸如 1.2.3)需要一个完整的“请求-测试-审核-发布”流程,这其中需要不同角色的人员来执行不同的操作。对于一些功能点能精确限定的产品来说,这个过程可能比较容易全部的自动化实现;但是对于功夫核心库这种功能点极其宽泛的产品来说,全自动化的难度非常大,实际上直到今天我们仍然在流程里非常依赖手工的测试和确认。例如说,想要验证交易报单的准确性,需要有真实的外部环境提供接入才能进行,这种环境会经常发生各种变化,以至于完全代码化的管理是几乎不可能的;只能通过测试人员手动的进行管理。

这样,对于测试角色来说,如果缺乏系统工程的支持,工作量会非常大,例如对于提测的代码,需要自行进行编译才能开展测试,这样会有很多繁琐的问题:

  • 测试人员需要自己配置维护编译环境,对于全平台(Win/MacOS/Linux)来说这个工作量还需要 X3;
  • 复杂项目全流程编译时间很长(普通机器需要30-60分钟),产生大量等待时间;
  • 开发人员失误导致的编译失败问题也会带来时间浪费;

这些问题全都指向需要通过自动化的 CI/CD Pipeline 来减少人员的工作量。但是理想很丰满,骨感现实的问题是,CI/CD 也是需要花钱的。如果自己维护用于 CI/CD 的服务器,除去购买服务器的硬件成本,还有日常维护相关软件、编译环境的烦恼,如果还要长期稳定(以年计)的维护,基本上至少需要一个全职 DevOp。除非项目能商业化运作且大到不差钱,这种成本是一般的开源开发者很难承担的。

我们在这个问题上找到的解决方案,是大力的挖掘 GitHub 的在线服务,尽可能把编译动作通过 GitHub-hosted runner来实现。但是熟悉的朋友会知道,GitHub Actions 的触发事件整体还是围绕 git 流程来设计的,即使考虑封装过的 Pull Requests,距离我们上面的核心需求还是有一定偏差。为此,我们专门开发了一套 GitHub workflow 来适配到能满足需求的流程:

 

这里主要的特性是通过参数化的形式,来完成编译环境的构建以及编译流程的代码化,具体可以参见 .github/workflows/.release-verify.yml 的代码。GitHub 的配套设施做的非常不错,基本上能够完全解决编译相关的软硬件维护问题,比如可以很轻松的支持多平台编译环境:

                                                                几行代码实现多平台编译环境准备

顺便一提,一套合理的流程必须具备非常好的抽象性和普适性,一个简单的判断标准就是流程项目本身也可以用自己来进行流程管理,我们也正是基于这个原则首先把这套流程应用在其自身的版本发布上,可以从 Pull requests · kungfu-trader/workflows 以及 Tags · kungfu-trader/workflows 这里看到这个项目的版本发布历史记录。

最终实现的效果如下图:

                                                                准备版本发布时,首先需要发起 PR 请求
                                                                自动执行的 CI/CD 检查及编译(含多平台同时编译)
                                                                CI/CD 生成的编译产物可在 PR 页直接下载使用

给代码打标签

已经发布的版本号需要能精确的对应代码版本(例如 git tag),这虽然看起来是一个很简单的需求,但是如果结合上面所说的发布流程,你会发现其中核心的问题是:必须在审核通过之后,才能对刚刚审核过的代码打标签;而这其实是一个细思极恐的问题,因为:

  • “打标签”需要是一个事后的动作,即审核通过才能执行;
  • “打标签”所产生的 git tag,并不等于写在代码里的版本号,如果要保证这两者完全一致,还需要一次额外的 git commit,而这会给“审核-发布”这个流程增加非常多的不确定性;

特别是,对于工作中的未审核代码,我们希望其中含有的写在代码里的“版本号”就是一个“未发布”的状态,不论在包管理还是产物下载的记录里都不应该存在对应的记录,否则就会产生很多不必要的麻烦。

上述问题最终导向了,我们必须有一个能够在流程中自动执行代码升级及给 git 打标签的一个程序化脚本,因此我们专门开发了一个 GitHub Action 来实现这个目的:

 

由于期望流程能够完全的可视化,并且避免命令行操作,我们基于 Pull Request 机制来设计,通过自定义的这个 action,来自动过滤掉不合格的 PR,并且在代码通过审核之后,自动对代码进行版本升级及打标签。

同样的,这个 action 本身也是基于前述的版本发布管理流程来进行开发管理的,可以在 Pull requests · kungfu-trader/action-bump-version 及 Tags · kungfu-trader/action-bump-version 看到它的历史版本发布记录。

追溯历史版本

已经发布的版本,其所对应的编译产物需要长期可见。除了通过 CI/CD 来实现自动化编译以外,这个需求还需要我们准备一些服务器资源来存储生成的编译产物。这次我们选择了 AWS S3 来作为存储服务,并同样是通过一个自定义的 GitHub Action 来完成编译产物的上传发布:

 

最终的成果可以在 功夫核心库历史版本 (libkungfu.cc) 这个页面看到,例如最新的发布版 kungfu v2.7.1 (libkungfu.cc) 下载页截图:

                                                                              发布版本的编译产物下载

版本发布说明(Release Notes)

更进一步的版本控制工作涉及到,对每一次的版本升级,需要提前规划出需要增加的特性和待解决的问题,以便进行合理的规划;同时这些信息也需要能反映在发布页面上,让用户有直观的理解。对于项目早期阶段,简单的写在 Markdown 文档里就可以;但是到了项目比较成熟,协作人员数量较多的时候,就必须借助专业的项目管理工具来进行工作。

到了这个阶段,虽然说比如 GitHub 自身也提供相关服务,但是考虑到团队中不同角色(产品、开发、测试)的工作特点,我们还希望能够尽量让大家都使用各自领域的专业工具,而非明显面向研发人员的 GitHub 工具集。

这方面在国内也有很多优秀的产品可以选择,但是如果考虑到整个 SaaS 环境生态的互联互通,国内产品链的可用性就远远不及海外产品了。我们需要的不仅仅是单点上一个可用的产品,更需要能够通过 API 串联起一系列服务,以便能够把数据汇总到一起来进行处理。

这里我们选择了 Monday 作为产品进度管理工具:

                                                                                   产品进度管理

并通过 Airtable、Zapier、Hookdeck 等一系列的 SaaS 服务把数据流联通,使得每次的版本发布都能自动化的汇总对应的进度项。当然这其中也需要自行进行一些必要的开发工作,来把各家的 API 对接起来。最终我们可以实现到的效果体现在这个自动生成的发布页上:

                                                                         版本发布 Release Notes

文档

对于长期维护(LTS)的版本线,同样需要配套的文档版本管理,及每个 LST 版本的文档也需要长期可见及可维护。但使人头大的是,这看起来非常普遍的需求,在开源世界中却很难找到完整的工具或解决方案,仍然需要撸起袖子自己写代码来解决。

文档渲染我们选择了老牌的 Sphinx 以及 Read the Docs Sphinx Theme 作为基础工具,然后基于 NPM 的包管理制作了能够同时维护多版本的文档项目:

 

最终的效果可以在我们的在线文档页面看到:

 

多版本在页面左下角可以切换:

                                      简单的文档多版本支持也需要自己写代码来实现,泪目

总结

对于需要支持多人协作的开源项目,必须建立一套完整的管理机制,从代码提交,到版本发布,再到成品的(编译产物、文档)的供应,每个环节都需要精心设计来减少以及避免人工操作带来的失误。对于今天的开发者来说,可以从这几个方面来提高自身的项目管理能力:

  • 正确的使用成熟技术方案,利用现代化软件工具,例如包管理、胶水技术、专业框架等来获得最佳实践,避免自己重复造轮子的同时尽量使用其他开发者贡献的高效工具来提高代码质量;
  • 多利用开放式的 SaaS 服务,在线上技术服务生态里有非常丰富的各种在线服务解决各种各样的常见痛点问题,选择其中最有性价比的方案,并且尽可能让数据在各家 API 之间流转起来,简化流程终端的操作;
  • 在以上环节之间暂时无法覆盖到的缝隙里,投入自己的精力来打磨,所谓磨刀不误砍柴工。

我们过去七年维护功夫核心库https://libkungfu.cc)的体验是,软件开发的世界里没有银弹,尽管优秀的现代开发工具层出不穷,仍然需要自己投入大量的精力开发流程工具和配套的基础设施,才能在质量控制上得到比较满意的效果。这些在“核心代码”之外的“繁琐之事”,也是不可忽视的必要工作,只有仔细打磨每个细节,才能让你的代码生命长青。

最后附上我们的公众号和微信群,欢迎对高频因子研究感兴趣的朋友关注,加入(微信搜索 功夫量化,关注后可扫码入群 ),对于产品的使用有任何疑问,可以在公众号后台直接回复,功夫小伙伴们会第一时间为你解答: