点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言

本文发布于 2016 年 3 月,但其中的设计技巧仍然有效。

我们的团队需要编写非常快速的缓存服务。目标非常明确,但可以通过多种方式实现。最后,我们决定尝试一些新的东西,并在 Go 中实现该服务。本文描述了我们是如何做到的以及由此产生的价值。

目录

  1. 需求

  2. 为什么用 Go

  3. 缓存

    1. 并发
    2. 移除
    3. 避免 GC
    4. BigCache
  4. HTTP 服务器

  5. JSON 反序列化

  6. 最终结果

  7. 总结

需求

根据需求,我们的服务应:

  • 使用 HTTP 协议处理请求
  • 支持 10K RPS (5k 写,5k 读)
  • cache 对象至少保持 10 分钟
  • 响应时间平均 5ms,99.9% 以外 10ms, 99.999% 以外 400ms
  • 处理包含 JSON 消息的 POST 请求,消息包含:
    • 包含 ID 和内容
    • 不超过 500 字节
  • 通过 POST 请求添加记录后,立即检索记录并通过 GET 请求返回 int,也就是保证一致性

简而言之,我们的任务是编写带有到期时间和 REST 接口的快速字典。

为什么是 Go?

我们公司中的大多数微服务都是用 Java 或另一种基于 JVM 的语言编写的,有些是用 Python 编写的。我们也有一个用 PHP 编写的单体式旧平台,但是除非有必要,否则我们不会碰它。我们已经知道这些技术,但是我们愿意探索一种新技术。我们的任务可以用任何语言实现,因此我们决定用 Go 编写。

在一家大公司(谷歌)和一个不断增长的用户社区的支持下,Go 发布已有一段时间了。它是一门编译,并发,命令式,结构化的编程语言。它还具有自动内存管理,因此比 C/C++ 看起来更安全,更容易使用。我们在用 Go 编写工具方面拥有相当丰富的经验,因此决定在此处使用它。我们已经有了一个 Go 的开源项目[1],现在我们想知道 Go 如何处理大流量。我们相信整个项目用 Go 仅需不到 100 行的代码即可完成任务,并且速度足以满足我们的需求。

缓存

为了满足需求,缓存本身需要:

  • 即使有数以百万计的记录也非常快
  • 提供并发访问
  • 在预定的时间后移除记录

基于第一点,我们决定放弃外部缓存,例如 Redis,Memcached 或 Couchbase,主要是因为网络上需要更多时间。因此,我们专注于内存中的缓存。在 Go 中已经有这种类型的缓存,即 LRU groupcache[2],go-cache[3],ttlcache[4],freecache[5] 等。只有 freecache 满足我们的需求。接下来的子章节会揭示为什么我们决定还是坚持自己实现,并描述了如何实现上述需求。

并发

我们的服务将同时接收许多请求,因此我们需要提供对缓存的并发访问。最简单的方法是将 sync.RWMutex 放在缓存访问功能的前面,以确保一次只能通过一个 goroutine 对其进行修改。但是,其他想对其进行修改的 goroutine 将被阻塞,使其成为瓶颈。为了消除此问题,可以应用 shards(分片)技术。分片背后的想法很简单。创建 N 个分片的数组,每个分片包含自己的带有锁的缓存实例。当需要缓存具有唯一 key 的记录时,首先通过函数 hash(key)%N 选择一个分片。在此之后,获取缓存锁并对缓存进行写入。记录读取是类似的。当分片的数量相对较多并且哈希函数返回的数字较分散时,锁争用几乎可以降至零。这就是我们决定在缓存中使用分片的原因。

移除(Eviction)

从缓存中移除元素的最简单方法是将其与 FIFO 队列一起使用。将记录添加到高速缓存后,将执行两个附加操作:

  • 包含 key 和创建时间戳的记录被添加到队列的末尾。
  • 从队列中读取最早的记录。将其创建时间戳与当前时间进行比较。如果晚于收回时间,则将队列中的元素及其在缓存中的对应记录一起删除。

由于在写入缓存期间已获取了锁,因此顺带执行移除操作。

避免 GC

在 Go 中,对于 map,垃圾收集器(GC)将在标记(mark)和扫描(scan)阶段遍历该 map 的每个条目。当 map 足够大(包含数百万个对象)时,这可能会对应用程序性能产生巨大影响。

我们对服务进行了一些测试,在该服务中向缓存添加了数百万个记录,然后我们开始将请求发送到一些不相关的 REST 端点(endpoint),仅执行静态 JSON 序列化(根本不涉及缓存)。缓存为空时,此端点的最大响应延迟为 10ms,10k rps。当缓存被填满时,它在 99 的百分位上有超过一秒钟的延迟。度量标准表明,堆中有超过 4000 万个对象,GC 标记和扫描阶段花费了四秒钟。该测试向我们表明,如果要满足与响应时间有关的要求,则需要跳过 GC 以获取缓存项。我们该怎么做?好吧,有三个选择。

GC 仅限于堆,因此第一个选择是堆外。有一个项目可以帮助解决这个问题,称为 offheap[6]。它提供了自定义函数 Malloc()和 Free() 来管理堆外部的内存。但是,将需要实现依赖于那些功能的缓存。

第二种方法是使用 freecache[7]。Freecache 通过减少指针数量实现了具有零 GC 开销的 map。它将键和值保留在环形缓冲区中,并使用索引切片查找条目。

为缓存条目避免 GC 的第三种方式与 Go 1.5 版本修复的一个 issue 有关(issue 9477[8])。此优化表明,如果你使用的是 key 和 value 中没有指针的 map,则 GC 将忽略其内容。这是用堆,但为 map 中的条目避免 GC 的一种方法。但是,这并不是最终的解决方案,因为 Go 中的所有内容基本上是基于指针构建的:结构,切片甚至数组。只有诸如 int 或 bool 之类的基本类型才不是指针。那我们可以用 map[int]int 做什么?由于我们已经生成了 hashed key,以便从缓存中选择适当的分片(在并发中进行了描述),因此我们可以将它们重新用作 map[int]int 中的键。但是 int 类型的值呢?我们可以将哪些信息保留为 int?我们可以保留条目的偏移量。另一个问题是如何保留这些条目以便再次避免 GC?可以分配大量的字节数组,并且可以将条目序列化为字节并保留在其中。为此,map[int]int 中的值可能指向一个偏移量,该偏移量是条目在目标数组中开始的位置。而且由于 FIFO 队列用于保留条目并控制其移除(在 Eviction 中进行了描述),因此可以基于一个巨大的字节数组重新构建它,该 map 中的值也将指向该数组。

在所有以上指出的方案中,都需要条目(反)序列化。最终,我们决定尝试第三种解决方案,因为我们想知道它是否会起作用,并且实际中我们会有大量元素—hashed key(在分片选择阶段计算)和条目队列。

BigCache

为了满足本章开头提出的需求,我们实现了自己的缓存并将其命名为 BigCache。BigCache 提供分片,移除(即淘汰),并且避免了缓存条目的 GC 问题。结果,即使对于大量记录,它也是非常快的缓存。

Freecache 是 Go 中唯一提供这种功能的可用内存中缓存之一。Bigcache 是它的替代解决方案,并以不同的方式减少了 GC 开销,因此我们决定分享它:bigcache[9]。有关 freecache 和 bigcache 比较的更多信息,请参见 GitHub[10]

HTTP Server

内存探查器(profiler)向我们展示了在请求处理期间分配了一些对象。我们知道 HTTP 处理程序将成为我们系统的热点。我们的 API 非常简单。我们仅接受 POST 和 GET 从缓存中上传和下载元素。我们实际上仅支持一个 URL 模板,因此不需要功能齐全的路由器。我们通过剪切前 7 个字母从 URL 中提取了 ID,它对我们来说很好用。

当我们开始开发时,Go 1.6 发布了 RC 版。我们减少请求处理时间的第一个尝试是将其更新到最新的 RC 版本。在我们的场景中,性能几乎相同。我们开始寻找更有效的方法,然后找到了 fasthttp[11]。它是一个提供零分配 HTTP 服务器的库。根据文档,在综合测试中,它通常比标准 HTTP 处理程序快 10 倍。在我们的测试过程中,结果证明它仅快 1.5 倍,但还是更好!

fasthttp 通过减少 HTTP Go 程序包完成的工作来提升其性能。例如:

  • 将请求生存期限制为实际处理的时间
  • header 是延迟解析的(我们真的不需要 header)

不幸的是,fasthttp 并不是标准 http 的真正替代。它不支持路由或 HTTP/2,并声称不能支持所有 HTTP 边缘情况。对于具有简单 API 的小型项目而言,这很好,因此对于正常(非超级性能)项目,我们会坚持使用默认 HTTP。

fasthttp vs nethttp

JSON 反序列化

在对应用程序进行性能分析时,我们发现该程序在 JSON 反序列化上花费了大量时间。内存探查器还报告说 json.Marshal 处理了大量数据。这并不令我们感到惊讶。对于 10k rps,每个请求 350 个字节对于任何应用程序来说都是重要的有效负载(payload)。尽管如此,我们的目标是速度,所以我们对其进行了调研。

我们听说 Go JSON 序列化程序的速度不如其他语言快。大多数基准测试是在 2013 年完成的,因此这是在 1.3 版之前做的测试。当我们看到 issue-5683[12] 声称 Go JSON 比 Python 慢 3 倍,而邮件列表[13]说它比 Python simplejson[14] 慢 5 倍时,我们开始寻找更好的解决方案。

如果需要速度,基于 HTTP 的 JSON 绝对不是最佳选择。不幸的是,我们所有的服务都以 JSON 相互通信,因此采用新协议超出了此任务的范围(但我们正在考虑使用 avro[15],就像我们对 Kafka[16] 所做的那样)。我们决定坚持使用 JSON。通过快速搜索找到了一个名为 ffjson 的解决方案。(polaris 注:此文是 2016 年写的,一方面标准库性能更好了,另一方面,也有更多 JSON 库可供选择。)

ffjson 文档声称它比标准 json.Unmarshal 快 2-3 倍,并且使用的内存更少。

json 16154 ns/op 1875 B/op 37 allocs/op
ffjson 8417 ns/op 1555 B/op 31 allocs/op

我们的测试证实 ffjson 比内置 unmarshaler 快了近 2 倍,并且内存分配的次数更少。它是如何做到这一点的?

首先,为了从 ffjson 的所有功能中受益,我们需要为我们的结构生成一个 unmarshaller。生成的代码实际上是一个解析器,它扫描字节并用数据填充对象。如果您看一下 JSON 语法,您会发现它确实很简单。ffjson 充分利用了结构的外观,仅解析结构中指定的字段,并在发生错误时快速失败。标准库的 marshaler 使用昂贵的反射调用来在运行时获取结构定义。另一个优化是减少不必要的错误检查。json.Unmarshal 将无法更快地执行较少的分配,并跳过反射调用。

json (invalid json) 1027 ns/op 384 B/op 9 allocs/op
ffjson (invalid json) 2598 ns/op 528 B/op 13 allocs/op

有关 ffjson 如何工作的更多信息,请参见此处[17]。基准测试在这里[18]

最终结果

最后,对于最耗时的请求,我们将时间从 2.5 秒以上缩短到了 250 毫秒以下。这些时间仅发生在我们的场景中。我们相信,对于大量的写入操作或更长的移除(淘汰)周期,对标准缓存的访问可能会花费更多的时间,但是对于 bigcache 或 freecache 来说,访问时间可以保持在毫秒级,因为消除了长时间 GC 暂停这个根源。

下表比较了优化服务前后的响应时间。在测试期间,我们发送了 10k rps,从中写入 5k,读取另外 5k。移除时间设置为 10 分钟。测试时间为 35 分钟。

response times before and after optimizations

使用上述相同的设置进行单独测试(读写单独)的最终结果如下。

final results

总结

如果您不需要高性能,请使用标准库。确保维护性,因为它们具有向后兼容性,因此升级 Go 版本会很顺利。

我们用 Go 编写的缓存服务终于满足了我们的需求。大多数时候,我们花时间弄清楚 GC 暂停可能会对应用程序的响应性产生巨大影响,因为它控制着数百万个对象。幸运的是,像 bigcache 或 freecache 这样的缓存可以解决此问题。

原文链接:https://allegro.tech/2016/03/writing-fast-cache-service-in-go.html

作者:Łukasz Drumiński、Tomasz Janiszewski

参考资料

[1]

开源项目: https://github.com/allegro/marathon-consul/#marathon-consul-

[2]

LRU groupcache: https://github.com/golang/groupcache/tree/master/lru

[3]

go-cache: https://github.com/patrickmn/go-cache

[4]

ttlcache: https://github.com/diegobernardes/ttlcache

[5]

freecache: https://github.com/coocood/freecache

[6]

offheap: https://godoc.org/github.com/glycerine/offheap

[7]

freecache: https://github.com/coocood/freecache

[8]

issue 9477: https://github.com/golang/go/issues/9477

[9]

bigcache: https://github.com/allegro/bigcache

[10]

GitHub: https://github.com/allegro/bigcache#bigcache-vs-freecache

[11]

fasthttp: https://github.com/valyala/fasthttp

[12]

issue-5683: https://github.com/golang/go/issues/5683

[13]

邮件列表: https://groups.google.com/forum/#!topic/golang-nuts/zCBUEB_MfVs

[14]

simplejson: https://pypi.org/project/simplejson/

[15]

avro: https://avro.apache.org/

[16]

Kafka: https://allegro.tech/2015/08/spark-kafka-integration.html

[17]

此处: https://journal.paul.querna.org/articles/2014/03/31/ffjson-faster-json-in-go/

[18]

在这里: https://gist.github.com/janisz/8b20eaa1197728e09d6a

推荐阅读

  • 官方不推荐使用 Goroutine ID,但它自己却使用了:原来是这么做的

  • 【每日一库】一个零 GC 的缓存库:freecache

  • 从Go开源项目BigCache学习加速并发访问和避免高额的GC开销

  • 妙到颠毫:你应该学会的 bigcache 优化技巧


喜欢本文的朋友,欢迎关注“Go语言中文网”:

Go语言中文网启用微信学习交流群,欢迎加微信:274768166,投稿亦欢迎

go post 参数_用 Go 编写能存数百万条记录仍非常快的缓存服务相关推荐

  1. mysql获取删除的条数_如何从mysql表中删除数百万条记录而不会减速

    有没有一种很好的方法来删除很多记录而不会减慢网站的速度? 我需要从没有索引和主键的MySQL表中删除数百万条记录.我阅读了SO和网上的各种教程,基本策略是限制删除查询,在删除之间休眠一两秒钟,然后重复 ...

  2. python3正则表达式替换_在Python 3中加速数百万的正则表达式替换

    TLDR 如果您想要最快的基于正则表达式的解决方案,请使用此方法. 对于类似于OP的数据集,它比接受的答案快大约1000倍. 如果您不关心正则表达式,请使用此基于集合的版本,这比正则表达式联合快200 ...

  3. java insert方法_【Oracle/Java】以Insert ALL方式向表中插入百万条记录,耗时9分17秒...

    packagecom.hy;importjava.sql.Connection;importjava.sql.DriverManager;importjava.sql.ResultSet;import ...

  4. java 一次最多insert多少条数据_万J,一次Insert/Update数十万条记录,有什么办法提高效率么?...

    我现在没有环境了,搞了个 pg 测试: create table t(id serial primary key, x float, y float) insert 100000 rows exhau ...

  5. java读取百万条记录出错_如何实现导出百万条数据到EXCEL中不报OOM异常?

    Java项目中使用POI导出百万条数据到Excel中,但是会出现内存溢出异常. 存在以下问题需要考虑POI导出条数限制6w+ 数据量大的话会导致内存溢出 现在的做法是每6w条数据做一次分割,创建一个新 ...

  6. mysql提取前两个数据_各种数据库提取表的前几条记录的方法

    前一阵子去了个面试,其中有个道考题是写出取某张表前10条记录的sql语句.一头雾水,回来问了同学也没有结果,近来上网搜了一下,收获如下: 原来不同的DBMS对此操作的sql语句也不同. (取出TABL ...

  7. mysql更新10000条记录_如何快速更新数据库中的百万条数据

    环境:ubuntu 14.04,MySQL 5.6,Python 2.7. 本文使用python的MySQLdb这个库,MySQL Python tutorial这个教程很赞. MySQLDBdb处理 ...

  8. mysql记录和字段_科学网—MySQL学习笔记:计算同一条记录内各字段之和,并判断是否归一 - 丁祥欢的博文...

    ★计算同一记录中不同字段之和 背景说明:MainDB表内有几十个字段用于存放配方数据,需要将它们加起来看看是否归一. 由于MySQL没有类似高版本Access那样的计算字段,因为需要用SQL语法来处理 ...

  9. mysql查询前5条记录_各个数据库中,查询前n条记录的方法

    SQL查询前10条的方法为: 1.select top X *  from table_name --查询前X条记录,可以改成需要的数字,比如前10条. 2.select top X *  from  ...

最新文章

  1. ORM武器:NHibernate(三)五个步骤+简单对象CRUD+HQL
  2. C 一样快,Ruby 般丝滑的 Crystal 发布 1.0 版本,你看好吗?
  3. 图解|什么是高并发利器NoSQL
  4. 经常玩电脑正确的坐姿_初级茶艺师工作技能培训:第一节礼仪(正确的坐姿)...
  5. nacos如何做配置中心?自带自动刷新配置功能?这一篇文章让你明明白白!
  6. 算法 | 最速降线问题与最小旋转面问题(变分法)
  7. SwiftUI优秀文章经典案例制作简易的新闻列表Demo
  8. oracle 9i hwm,Oracle 10g HWM原理及性能优化
  9. [转]在VS2010 VC++项目中引用Lib静态库(以Openssl为例)
  10. 项目中查询数据和模糊查询
  11. Joomla css类后缀
  12. python 识别图形验证码_python爬虫20 | 小帅b教你如何用python识别图片验证码
  13. 脉动风时程matlab程序,脉动风时程matlab程序.docx
  14. 软件著作权申请流程和费用
  15. python学习笔记30(利用turtle绘制最基本的几何图形)
  16. 目前三款国内最靠谱最良心的杀毒软件推荐 | 国内杀毒软件哪个好用?威航软件园诚意推荐这三个
  17. java8历史版本下载地址
  18. Monash call (莫纳什来电) -开篇
  19. 网络打印机怎么和计算机连接不上,网络打印机无法连接怎么办?网络打印机设置步骤...
  20. windows2012取消自动ipv4_Win10删除自动配置ipv4|Win10自动配置ip地址怎么关闭

热门文章

  1. 埃夫特机器人离线编程软件_埃夫特ER-Factory数字化工厂软件重磅升级
  2. unity 检测电脑摄像头_Unity调用PC摄像头
  3. linux0.12-12-fs
  4. Linux命令lsscsi详解
  5. .net core创建区域(Areas)的使用
  6. 为国庆献礼:无人智能系统,守护祖国海空长城
  7. 2018CCPC吉林赛区 The Hermit
  8. 程序员不愿意加班的真相......
  9. Maya2015安装配置和学习
  10. javascript 简繁体字转换脚本