仅供Go使用的gob比JSON性能提高80倍 - ksred

22-04-12 banq

我们主要产品是股票交易规则引擎:策略是你创建的一套股票或加密货币的规则,这个引擎会跟踪你创建的策略中所做的所有交易,从中你可以看到哪些交易效果好,哪些交易效果不好,你每天都会收到一封包含交易机会的电子邮件。

这个规则引擎的一个关键部分是确保在回测期间有效的数据访问和处理:通过放弃JSON,改用gob,我们在读取方面提高了84倍,在写入方面提高了20倍

规则引擎功能的核心是回测。单一股票回测的工作方式如下。
  • 创建一个具有N个进入和N个退出标准的规则
  • 回溯测试的周期被设定。范围从1天到5年(约1,300天)。
  • 该规则从最早的日期一直运行到当前日期
  • 对于每个运行的日期,每个标准都被检查,看是否符合标准
  • 如果某一天的所有进入标准都得到满足,就会开出一笔交易(如果还没有交易)。
  • 如果所有的退出标准被满足,或者如果止损被触发,任何未平仓的交易就会被关闭。

标准对象可以相当复杂。通过举例,我们将说明各种级别的复杂性。
  • 如果价格在一天内超过100美元
  • 如果价格在两天内增加了5%。
  • 如果价格在五天内连续上涨

对于这些标准中的每一项,都必须为所要求的数值(1、2和5)检索价格,并检查标准。这发生在给定的回测中的每一天。

缓存的重要性
由于给定数据集的读取数量,可能是价格或数量,我实现了多层次的缓存,以确保高效的数据检索。

对于每个数据请求(每个标准),其流程如下:

  • 向存储库函数请求N条数据记录
  • 如果N大于默认的缓存记录数,则直接从数据库中获取它们
  • 如果N小于默认的缓存记录数,检查缓存
  • 首先,我们检查内存中的缓存,并响应所需的记录子集(N条记录被后面的测试日抵消)。
  • 如果这些记录不在内存中,我们检查redis(同样,N条记录被后面的测试日抵消)。
  • 如果N小于默认的缓存记录数,并且没有存储在任何一个缓存中,我们将为后续请求同时缓存redis和内存中的记录。


内存缓存工作得很好,因为单个股票的每个回测都在单个实例上运行。在回测结束时,内存被释放(对于给定的股票/加密货币,内存缓存被删除),以确保我们不会不断地分配内存。

由于被缓存的记录是一个结构片,我在使用JSON对记录进行相应的Marshal和Unmarshal。性能还算不错,但我觉得还是太慢了。在浏览了应用程序的流程之后,我没有发现有什么大的效率提高,所以决定做一个pprof,看看引擎盖下发生了什么。

Gob来拯救
我喜欢Go的最大原因之一是它的工具性。我所要做的就是在本地公开生产集群,运行回测查询,针对本地端口运行pprof命令,这是一个类似JProfiler的性能定位调试攻击,然后看一下输出。在这个例子中,我主要对CPU使用率感兴趣。

该引擎中有三个主要领域可以作为焦点:

  • JSON读取
  • JSON写入
  • 垃圾收集

快速搜索后,gob似乎是替代品的明显选择:
它是基于二进制的,与structs配合得非常好,而且缓存不需要是人类可读的。

// Using JSON
err = json.Unmarshal(obj, result)
if err != nil {
    return
}

// Using gob
buf := bytes.NewBuffer(obj)
dec := gob.NewDecoder(buf)
err = dec.Decode(&result)
if err != nil {
    return
}


这是为redis和内部映射的get/delete/fetch函数做的。

我运行了我的测试套件,在本地部署并测试,然后推送到生产。我重新进行了同样的回测:
我还让垃圾收集器的攻击性降低了一些:
编码和解码的时间大大减少了。

结果
在高层次上,我们可以看到以下结果。两者的剖析时间总计为31秒。

  • 写JSON的时间(Marshal): 10.31s
  • 花在JSON读取上的时间(Unmarshal):5.09s

gob显示了一些显著的收益。
  • gob写(Encode)花费的时间:0.51s
  • 读取gob的时间(解码):0.06s

因此,综上所述,我们有以下结果:
  1. 写作速度提高了20.2倍
  2. 读取速度增加了84.8倍

需要提醒的是--这两项测试都运行了相同的查询,当时没有运行其他查询,并且使用了相同的集群/硬件。

这意味着3个标准规则在一个月前的测试中需要1.5秒,而在一年前的测试中需要10秒。如果你知道引擎盖下的所有逻辑,这就是闪电般的速度。