go post 参数_用 Go 编写能存数百万条记录仍非常快的缓存服务
点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言
本文发布于 2016 年 3 月,但其中的设计技巧仍然有效。
我们的团队需要编写非常快速的缓存服务。目标非常明确,但可以通过多种方式实现。最后,我们决定尝试一些新的东西,并在 Go 中实现该服务。本文描述了我们是如何做到的以及由此产生的价值。
目录
需求
为什么用 Go
缓存
- 并发
- 移除
- 避免 GC
- BigCache
HTTP 服务器
JSON 反序列化
最终结果
总结
需求
根据需求,我们的服务应:
- 使用 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 分钟。
使用上述相同的设置进行单独测试(读写单独)的最终结果如下。
总结
如果您不需要高性能,请使用标准库。确保维护性,因为它们具有向后兼容性,因此升级 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 编写能存数百万条记录仍非常快的缓存服务相关推荐
- mysql获取删除的条数_如何从mysql表中删除数百万条记录而不会减速
有没有一种很好的方法来删除很多记录而不会减慢网站的速度? 我需要从没有索引和主键的MySQL表中删除数百万条记录.我阅读了SO和网上的各种教程,基本策略是限制删除查询,在删除之间休眠一两秒钟,然后重复 ...
- python3正则表达式替换_在Python 3中加速数百万的正则表达式替换
TLDR 如果您想要最快的基于正则表达式的解决方案,请使用此方法. 对于类似于OP的数据集,它比接受的答案快大约1000倍. 如果您不关心正则表达式,请使用此基于集合的版本,这比正则表达式联合快200 ...
- java insert方法_【Oracle/Java】以Insert ALL方式向表中插入百万条记录,耗时9分17秒...
packagecom.hy;importjava.sql.Connection;importjava.sql.DriverManager;importjava.sql.ResultSet;import ...
- java 一次最多insert多少条数据_万J,一次Insert/Update数十万条记录,有什么办法提高效率么?...
我现在没有环境了,搞了个 pg 测试: create table t(id serial primary key, x float, y float) insert 100000 rows exhau ...
- java读取百万条记录出错_如何实现导出百万条数据到EXCEL中不报OOM异常?
Java项目中使用POI导出百万条数据到Excel中,但是会出现内存溢出异常. 存在以下问题需要考虑POI导出条数限制6w+ 数据量大的话会导致内存溢出 现在的做法是每6w条数据做一次分割,创建一个新 ...
- mysql提取前两个数据_各种数据库提取表的前几条记录的方法
前一阵子去了个面试,其中有个道考题是写出取某张表前10条记录的sql语句.一头雾水,回来问了同学也没有结果,近来上网搜了一下,收获如下: 原来不同的DBMS对此操作的sql语句也不同. (取出TABL ...
- mysql更新10000条记录_如何快速更新数据库中的百万条数据
环境:ubuntu 14.04,MySQL 5.6,Python 2.7. 本文使用python的MySQLdb这个库,MySQL Python tutorial这个教程很赞. MySQLDBdb处理 ...
- mysql记录和字段_科学网—MySQL学习笔记:计算同一条记录内各字段之和,并判断是否归一 - 丁祥欢的博文...
★计算同一记录中不同字段之和 背景说明:MainDB表内有几十个字段用于存放配方数据,需要将它们加起来看看是否归一. 由于MySQL没有类似高版本Access那样的计算字段,因为需要用SQL语法来处理 ...
- mysql查询前5条记录_各个数据库中,查询前n条记录的方法
SQL查询前10条的方法为: 1.select top X * from table_name --查询前X条记录,可以改成需要的数字,比如前10条. 2.select top X * from ...
最新文章
- ORM武器:NHibernate(三)五个步骤+简单对象CRUD+HQL
- C 一样快,Ruby 般丝滑的 Crystal 发布 1.0 版本,你看好吗?
- 图解|什么是高并发利器NoSQL
- 经常玩电脑正确的坐姿_初级茶艺师工作技能培训:第一节礼仪(正确的坐姿)...
- nacos如何做配置中心?自带自动刷新配置功能?这一篇文章让你明明白白!
- 算法 | 最速降线问题与最小旋转面问题(变分法)
- SwiftUI优秀文章经典案例制作简易的新闻列表Demo
- oracle 9i hwm,Oracle 10g HWM原理及性能优化
- [转]在VS2010 VC++项目中引用Lib静态库(以Openssl为例)
- 项目中查询数据和模糊查询
- Joomla css类后缀
- python 识别图形验证码_python爬虫20 | 小帅b教你如何用python识别图片验证码
- 脉动风时程matlab程序,脉动风时程matlab程序.docx
- 软件著作权申请流程和费用
- python学习笔记30(利用turtle绘制最基本的几何图形)
- 目前三款国内最靠谱最良心的杀毒软件推荐 | 国内杀毒软件哪个好用?威航软件园诚意推荐这三个
- java8历史版本下载地址
- Monash call (莫纳什来电) -开篇
- 网络打印机怎么和计算机连接不上,网络打印机无法连接怎么办?网络打印机设置步骤...
- windows2012取消自动ipv4_Win10删除自动配置ipv4|Win10自动配置ip地址怎么关闭
热门文章
- 埃夫特机器人离线编程软件_埃夫特ER-Factory数字化工厂软件重磅升级
- unity 检测电脑摄像头_Unity调用PC摄像头
- linux0.12-12-fs
- Linux命令lsscsi详解
- .net core创建区域(Areas)的使用
- 为国庆献礼:无人智能系统,守护祖国海空长城
- 2018CCPC吉林赛区 The Hermit
- 程序员不愿意加班的真相......
- Maya2015安装配置和学习
- javascript 简繁体字转换脚本