Redis在中国有成熟的社区,大多数企业都或多或少用到Redis, 但我相信大家也会发现Redis有很多不尽如人意的地方,如果有一款K-V数据库能给您带来更快的读写速度,更可靠的安全保障以及更低的总体拥有成本,为什么不多了解一下Aerospike呢?

今天我想给大家分享一个来自我们的客户 travel audience 的案例!

Travel audience 是一个数字广告平台,这意味着我们将在全球范围内不断投放大量广告。在本文中,我想谈一谈我们投标部门面临的一些挑战。

我们的竞标者参加了实时广告环境中网页上广告位的拍卖。我们每秒处理约60–70k个出价请求,每个请求的处理时间应少于75ms。为了处理系统上的高负载,我们一直在使用Postgres在后台刷新内存中的数据,并在Redis中使用实时数据。(技术公司中通常使用的数据库集包括RDBMS,例如Postgres,MySQL以及缓存解决方案。)

来自SSP(供应方平台)的每个请求都会导致对缓存的多个查询。吞吐量(每秒处理的事务)和延迟(响应时间)是重要的指标,此缓存必须满足RTB应用程序的需要。

随着时间的流逝,广告系列的数量在增加,我们投放广告的用户数量也在增加,预算也在增加。这就是为什么我们必须保持灵活性,以便根据负载扩展我们的系统。

挑战

我们的Redis缓存层由运行在不同端口上的几个Redis数据库(5-6)组成,每个数据库都有1个主节点和大约10个从属节点。从某个时候开始,我们注意到使用Redis时越来越多的问题。这是我们面临的一些问题:

  • 一个主设备,多个从设备,“写”吞吐量受运行主节点的一台服务器的限制。
  • Redis是单线程的,这意味着我们在CPU方面没有垂直可扩展性。
  • 实时主从同步问题:由于在主节点上进行了大量写入操作,因此所有更改都必须与从节点同步。由于无法同步大量数据并同时将数据提供给来自RTB应用程序的读取请求,导致从节点必须脱机进行同步。
  • 在同一个数据库中没有方便的方法来存储多种不同类型的数据。我们不得不将不同的实体存储在不同的Redis实例(instance)中,这样我们就必须处理不同端口上的多个连接。

解决方案

我试图找到一种可以满足我们所有需求的解决方案,同时仍要保持灵活性和易用性。

经过广泛的研究,我深刻的了解Aerospike可以真正满足我们的需求,我在上一份工作中已经有了一些使用Aerospike的经验。因此,我们在这个项目上开始采用Aerospike。

Aerospike的好处包括:

  • 分区:默认情况下,它具有4096个分区,分布在群集中的所有节点上。这对于我们 “写”吞吐量有很大提升。
  • Aerospike是多线程的,可以最有效地利用我们的资源。
  • 主副本同步没有停机时间,您可以配置“写”规则(policy),以便在副本创建确认后将“写”请求视为“完成”。
  • 命名空间:所有不同类型的数据都可以存储在同一群集中的不同命名空间下,就是这样的层次结构:命名空间 > 集 > 记录。
  • SSD或内存存储:Aerospike有两种模式:SSD与内存。Redis仅在内存中,这意味着大规模扩展变得非常昂贵,而Aerospike可以通过使用SSD存储提供具有竞争力的性能。

基准测试

我对Redis和Aerospike进行了测试比较。首先,吞吐量是最重要的参数,因为我们每秒有来自投标人的数十万个请求。

我在GCE中使用Debian 9创建了一个具有16个vCPU和14.4GB内存的虚拟机(大多数travel audience基础设施都是托管在Google Cloud Platform上),并在那里设置了Redis和Aerospike。

我按照以下链接设置了Aerospike和基准测试:

Install on Debian This tutorial covers installing Aerospike on a Debian system.

Benchmarks Use the Aerospike Go client benchmark tool to generate load on an Aerospike cluster and calculate performance metrics.

我还下载了Go基准测试,因为我们所有的RTB应用程序都是用Go编写的。

这是安装Redis的链接:

Redis Quick Start - Redis The suggested way of installing Redis is compiling it from sources, as Redis has no dependencies other than a working…

然后,我只运行了redis-server并能够使用redis-benchmark工具。

我决定分别运行“写入”和“读取”基准。基准测试写入/读取100万行,将8个字节写入值(int64)和50个并行的客户端连接。

“写”基准:

Redis:

redis-benchmark -t set -n 1000000 -d 8 -c 50
====== SET ======
1000000 requests completed in 7.66 seconds
50 parallel clients
8 bytes payload
keep alive: 1
99.92% <= 1 milliseconds
100.00% <= 2 milliseconds
100.00% <= 2 milliseconds
130497.20 requests per second

Aerospike:

./benchmark -w I -c 50
2018/01/17 14:05:33 Setting number of CPUs to use: 16
2018/01/17 14:05:33 benchmark.go:181: hosts: 127.0.0.1
2018/01/17 14:05:33 benchmark.go:182: port: 3000
2018/01/17 14:05:33 benchmark.go:183: namespace: test
2018/01/17 14:05:33 benchmark.go:184: set: testset
2018/01/17 14:05:33 benchmark.go:185: keys/records: 1000000
2018/01/17 14:05:33 benchmark.go:186: object spec: I, size: 0
2018/01/17 14:05:33 benchmark.go:187: random bin values false
2018/01/17 14:05:33 benchmark.go:188: workload: Initialize 100% of records
2018/01/17 14:05:33 benchmark.go:189: concurrency: 50
2018/01/17 14:05:33 benchmark.go:190: max throughput unlimited
2018/01/17 14:05:33 benchmark.go:191: timeout 0 ms
2018/01/17 14:05:33 benchmark.go:192: max retries 2
2018/01/17 14:05:33 benchmark.go:193: debug: false
2018/01/17 14:05:33 benchmark.go:194: latency: 0:0
2018/01/17 14:05:33 benchmark.go:137: Nodes Found: [BB90512440A0142]
2018/01/17 14:05:34 benchmark.go:712: write(tps=436881 timeouts=0 errors=0 totalCount=436881)
2018/01/17 14:05:35 benchmark.go:712: write(tps=442889 timeouts=0 errors=0 totalCount=879770)
2018/01/17 14:05:36 benchmark.go:712: write(tps=120230 timeouts=0 errors=0 totalCount=1000000)

“读”基准:

Redis:

redis-benchmark -t get -n 1000000 -d 8 -c 50
====== GET ======
1000000 requests completed in 7.47 seconds
50 parallel clients
8 bytes payload
keep alive: 1
99.90% <= 1 milliseconds
100.00% <= 1 milliseconds
133850.89 requests per second

Aerospike:

./benchmark -w RU:100 -c 50
2018/01/17 14:03:55 Setting number of CPUs to use: 16
2018/01/17 14:03:55 benchmark.go:181: hosts: 127.0.0.1
2018/01/17 14:03:55 benchmark.go:182: port: 3000
2018/01/17 14:03:55 benchmark.go:183: namespace: test
2018/01/17 14:03:55 benchmark.go:184: set: testset
2018/01/17 14:03:55 benchmark.go:185: keys/records: 1000000
2018/01/17 14:03:55 benchmark.go:186: object spec: I, size: 0
2018/01/17 14:03:55 benchmark.go:187: random bin values false
2018/01/17 14:03:55 benchmark.go:188: workload: Read 100%, Write 0%
2018/01/17 14:03:55 benchmark.go:189: concurrency: 50
2018/01/17 14:03:55 benchmark.go:190: max throughput unlimited
2018/01/17 14:03:55 benchmark.go:191: timeout 0 ms
2018/01/17 14:03:55 benchmark.go:192: max retries 2
2018/01/17 14:03:55 benchmark.go:193: debug: false
2018/01/17 14:03:55 benchmark.go:194: latency: 0:0
2018/01/17 14:03:55 benchmark.go:137: Nodes Found: [BB90512440A0142]
2018/01/17 14:03:57 benchmark.go:717: write(tps=0 timeouts=0 errors=0) read(tps=446765 timeouts=0 errors=0) total(tps=446765 timeouts=0 errors=0, count=446765)
2018/01/17 14:03:58 benchmark.go:717: write(tps=0 timeouts=0 errors=0) read(tps=503307 timeouts=0 errors=0) total(tps=503307 timeouts=0 errors=0, count=950072)
2018/01/17 14:03:59 benchmark.go:717: write(tps=0 timeouts=0 errors=0) read(tps=487852 timeouts=0 errors=0) total(tps=487852 timeouts=0 errors=0, count=1437924)

从读写基准测试中,您可以看到Redis TPS(每秒事务数)与Aerospike TPS之间存在显着差距:

Aerospike每秒430,000写操作,而Redis每秒130,000写操作

VS.

Aerospike每秒读取480,000次,而Redis每秒读取133,000次

设置集群

我们决定继续使用Aerospike,并在GCE中设置3个节点的集群,其复制因子为2,我们已经将一些最常用的数据从Redis实时切换到了Aerospike。

每个名称空间均配置为具有未分区的SSD磁盘,并且data-in-memory(数据在内存中)参数设置为true。因此,基本上当节点启动时,它将立即将所有数据加载到内存中,然后开始处理请求。这种设置可以使节点更快地启动,并且仅从内存中提供数据(尽管Aerospike具有自己的在内存中缓存数据的机制,当第一次检索记录时,会从SSD抓取记录,然后将其驻留在内存中以供后续请求使用)。

我们使用Prometheus指标以及Aerospike管理控制台,从而我们可以更好地了解集群中的状况。

节点上的所有硬件/软件升级都可以轻松完成,一个接一个地切换节点。这意味着,当您进行升级或删除/添加节点时,只需等待数据迁移、平衡完成即可。Aerospike集群负责其他所有工作。

缺点

除了Aerospike带来的好处(与Redis相比),我还不得不提到缺点:

  • 行数限制:社区版每个节点每个命名空间的行数限制为40亿行,我们目前可以使用它。限制的完整列表可以在这里找到 https://www.aerospike.com/docs/guide/limitations.html
  • 不支持事务:在我们的具体情况下可能更是一个设计问题,我们必须处理来自Google PubSub消息队列的实体(该消息队列不保证消息顺序)。我们需要处理消息版本并将其存储在Aerospike中。因此,要保存每个实体,我们必须在Aerospike中进行多次写入。通常,缺少ACID事务的支持是为使用Aerospike这样高性能数据库付出的代价(没有交易数据库会那么快)。换句话说,Aerospike的generation是一个很好的折中方案。(注:Aerospike现在支持强一致性,甚至支持在不同地区分布的集群中的强一致性。这个用户的分享更加显示出使用Aerospike的益处)
  • 不稳定的Go客户端库:我们的Aerospike Go客户端会不时产生2到3倍的Go例程,并且这些例程保持停滞状态。这意味着它们会消耗更多的内存,并且永远不会释放它。(注:Aerospike的新版本中也解决了这个问题)