查看原文
其他

首发丨360开源的类Redis存储系统:Pika

2016-05-12 陈宗志 高可用架构

编者按:本文由陈宗志在高可用架构群分享,转载请注明来自高可用架构「 ArchNotes 」。


陈宗志,360公司基础架构组高级存储研发工程师, 负责设计实现了 bada, pika, floyd 等一系列存储相关的产品, 开发设计团队内部的基础库 pink, mario 等等。


Pika 是 360 DBA 和基础架构组联合开发的类 Redis 存储系统,完全支持 Redis 协议,用户不需要修改任何代码,就可以将服务迁移至 Pika。有维护 Redis 经验的 DBA 维护 Pika 不需要学习成本。


Pika 主要解决的是用户使用 Redis 的内存大小超过 50G、80G 等等这样的情况,会遇到启动恢复时间长,一主多从代价大,硬件成本贵,缓冲区容易写满等问题。Pika 就是针对这些场景的一个解决方案。


Pika 目前已经开源,github 地址:

https://github.com/Qihoo360/pika


重点


  • Pika 的单线程的性能肯定不如 Redis,Pika 是多线程的结构,因此在线程数比较多的情况下,某些数据结构的性能可以优于 Redis。

  • Pika 肯定不是完全优于 Redis 的方案,只是在某些场景下面更适合。所以目前公司内部 Redis,Pika 是共同存在的方案。DBA 会根据业务的场景挑选合适的方案。


背景


Redis 提供了丰富的多数据结构的接口,在 Redis 之前,比如 memcache 都认为后端只需要存储 kv 的结构就可以,不需要感知这个 value 里面的内容。用户需要使用的话通过 json_encode,json_decode 等形式进行数据的读取就行。但是 Redis 做了一个微创新,提供了多数据结构的支持,让前端写代码起来更加的方便。


因此 Redis 在很多公司的使用率越来越广泛,用户不知不觉把越来越多的数据存储在 Redis 中。随着用户的使用,DBA 发现有些 Redis 实例的大小也是越来越大。在 Redis 实例内存使用比较大的情况下,遇到的问题也会越来越多,因此 DBA 和我们一起实现了大容量 Redis 的解决方案。


最近半年公司每天 Redis 的访问情况 

Redis 架构方案




大容量 Redis 遇到的问题


1. 恢复时间长


我们线上的 Redis 一般同时开启 rdb 和 aof。


我们知道 aof 的作用是实时的记录用户的写入操作,rdb 是 Redis 某一时刻数据的完整快照。那么恢复的时候一般是通过 rdb + aof 的方式进行恢复,根据我们线上的情况 50G Redis 恢复时间需要差不多 70 分钟。


2. 一主多从,主从切换代价大


Redis 在主库挂掉以后,从库升级为新的主库。那么切换主库以后,所有的从库都需要跟新主做一次全同步,全量同步一次大容量的 Redis 代价非常大。


3. 缓冲区写满问题


为了防止同步缓冲区被复写,dba 给 Redis 设置了 2G 的巨大同步缓冲区,这对于内存资源来讲代价很大。当由于机房之间网络有故障,主从同步出现延迟了大于 2G 以后,就会触发全同步的过程。如果多个从库同时触发全同步的过程, 那么很容易就将主库给拖死。


4. 内存太贵


我们一般线上使用的 Redis 机器是 64G、96G,我们只会使用 80% 的空间。


如果一个 Redis 的实例是 50G,那么基本一台机器只能运行一个 Redis 实例,特别浪费资源。


总结


可以看到在 Redis 比较小的情况下,这些问题都不是问题,但是当 Redis 容量上去以后,很多操作需要的时间也就越来越长了。


Pika 整体架构




主要组成:


  • 网络模块 pink

  • 线程模块

  • 存储引擎 nemo

  • 日志模块 binlog


1. pink 网络模块


基础架构团队开发网络编程库,支持 pb、Redis 等协议。对网络编程的封装,用户实现一个高性能的 server 只需要实现对应的 DealMessage() 函数即可

支持单线程模型、多线程 worker 模型。


github 地址:

https://github.com/baotiao/pink


2. 线程模块



Pika 基于 pink 对线程进行封装,使用多个工作线程来进行读写操作,由底层 nemo 引擎来保证线程安全,线程分为 11 种:


1) PikaServer:主线程


2) DispatchThread:监听 1 个端口,接收用户连接请求


3) ClientWorker:存在多个(用户配置),每个线程里有若干个用户客户端的连接,负责接收处理用户命令并返回结果,每个线程执行写命令后,追加到 binlog 中


4) Trysync:尝试与 master 建立首次连接,并在以后出现故障后发起重连


5) ReplicaSender:存在多个(动态创建销毁,本 master 节点挂多少个 slave 节点就有多少个),每个线程根据 slave 节点发来的同步偏移量,从 binlog 指定的偏移开始实时同步命令给 slave 节点


6) ReplicaReceiver:存在 1 个(动态创建销毁,一个 slave 节点同时只能有一个 master),将用户指定或当前的偏移量发送给 master 节点并开始接收执行 master 实时发来的同步命令,在本地使用和 master 完全一致的偏移量来追加 binlog


7) SlavePing:slave 用来向 master 发送心跳进行存活检测


8) HeartBeat:master 用来接收所有 slave 发送来的心跳并恢复进行存活检测


9) bgsave:后台 dump 线程


10) scan:后台扫描 keyspace 线程


11) purge:后台删除 binlog 线程


3. 存储引擎 nemo


Pika 的存储引擎,基于 Rocksdb 修改。封装 Hash, List, Set, Zset 等数据结构


我们知道 Redis 是需要支持多数据结构,而 rocksdb 只是一个 kv 的接口,那么我们如何实现的呢?


比如对于 Hash 数据结构:


对于每一个 Hash 存储,它包括 hash 键(key),hash 键下的域名(field)和存储的值(value)。


nemo 的存储方式是将 key 和 field 组合成为一个新的 key,将这个新生成的 key 与所要存储的 value 组成最终落盘的 kv 键值对。同时,对于每一个 hash 键,nemo 还为它添加了一个存储元信息的落盘 kv,它保存的是对应 hash 键下的所有域值对的个数。


每个 hash 键、field、value到落盘 kv 的映射转换 


每个 hash 键的元信息的落盘 kv 的存储格式



比如对于 List 数据结构:


顾名思义,每个 List 结构的底层存储也是采用链表结构来完成的。对于每个 List 键,它的每个元素都落盘为一个 kv 键值对,作为一个链表的一个节点,称为元素节点。和 hash 一样,每个 List 键也拥有自己的元信息。


每个元素节点对应的落盘 kv 存储格式 


每个元信息的落盘 kv 的存储格式



其他的数据结构实现的方式也类似,通过将 hash_filed 拼成一个 key,存储到支持 kv 的 RocksDB 里面去,从而实现多数据结构的结构。


4. 日志模块 binlog


Pika 的主从同步是使用 Binlog 来完成的;

binlog 本质是顺序写文件,通过 Index + offset 进行同步点检查。


解决了同步缓冲区太小的问题;


支持全同步 + 增量同步;


master 执行完一条写命令就将命令追加到 Binlog 中,ReplicaSender 将这条命令从 Binlog 中读出来发送给 slave,slave 的 ReplicaReceiver 收到该命令,执行,并追加到自己的 Binlog 中。


当发生主从切换以后,slave 仅需要将自己当前的 Binlog Index + offset 发送给 master,master 找到后从该偏移量开始同步后续命令。


为了防止读文件中写错一个字节则导致整个文件不可用,所以Pika采用了类似 leveldb log 的格式来进行存储,具体如下:




主要功能


Pika 线上架构




主从架构


为了减少用户的学习成本,目前 Pika 的主从同步功能是和 Redis 完全一样,只需要 slaveof 就可以实现主从关系的建立,使用起来非常方便。


背景

1. Pika Replicate


Pika 支持 master/slave 的复制方式,通过 slave 端的 slaveof 命令激发

salve 端处理 slaveof 命令,将当前状态变为 slave,改变连接状态

slave的trysync线程向 master 发起 trysync,同时将要同步点传给 master

master处理trysync命令,发起对slave的同步过程,从同步点开始顺序发送 binlog 或进行全同步


Binlog

Pika 同步依赖 binlog

binlog 文件会自动或手动删除

当同步点对应的 binlog 文件不存在时,需要通过全同步进行数据同步


全同步


简介

需要进行全同步时,master 会将 db 文件 dump 后发送给 slave

通过 rsync 的 deamon 模式实现 db 文件的传输


实现逻辑

  • slave 在 trysnc 前启动 rsync 进程启动 rsync 服务;

  • master 发现需要全同步时,判断是否有备份文件可用,如果没有先 dump 一份

  • master 通过 rsync 向 slave 发送 dump 出的文件

  • slave 用收到的文件替换自己的 db

  • slave 用最新的偏移量再次发起 trysnc

  • 完成同步



Slave 连接状态


  • No Connect:不尝试成为任何其他节点的 slave;

  • Connect:Slaveof 后尝试成为某个节点的 slave,发送 trysnc 命令和同步点;

  • Connecting:收到 master 回复可以 slaveof,尝试跟 master 建立心跳;

  • Connected:心跳建立成功

  • WaitSync:不断检测是否 DBSync 完成,完成后更新 DB 并发起新的 slaveof。




主从命令同步




上图1是一个主从同步的一个过程(即根据主节点数据库的操作日志,将主节点数据库的改变过程顺序的映射到从节点的数据库上),从图中可以看出,每一个从节点在主节点下都有一个唯一对应的 BinlogSenderThread。


为了说明方便,我们定一个“同步命令”的概念,即会改变数据库的命令,如 set,hset,lpush 等,而 get,hget,lindex 则不是.


主要模块的功能:


1) WorkerThread:接受和处理用户的命令;


2) BinlogSenderThread:负责顺序地向对应的从节点发送在需要同步的命令;


3) BinlogReceiverModule:负责接受主节点发送过来的同步命令


4) Binglog:用于顺序的记录需要同步的命令


主要的工作过程:


1.当WorkerThread 接收到客户端的命令,按照执行顺序,添加到 Binlog 里;


2.BinglogSenderThread 判断它所负责的从节点在主节点的 Binlog 里是否有需要同步的命令,若有则发送给从节点;


3.BinglogReceiverModule 模块则做以下三件事情:

a. 接收主节点的 BinlogSenderThread 发送过来的同步命令;

b. 把接收到的命令应用到本地的数据上;

c. 把接收到的命令添加到本地 Binlog 里


至此,一条命令从主节点到从节点的同步过程完成


BinLogReceiverModule 的工作过程:



上图是 BinLogReceiverModule(在源代码中没有这个对象,这里是为了说明方便,抽象出来的)的组成,从图中可以看出 BinlogReceiverModule 由一个 BinlogReceiverThread 和多个 BinlogBGWorker 组成。


BinlogReceiverThread:负责接受由主节点传送过来的命令,并分发给各个 BinlogBGWorker,若当前的节点是只读状态(不能接受客户端的同步命令),则在这个阶段写 Binlog


BinlogBGWorker:负责执行同步命令;若该节点不是只读状态(还能接受客户端的同步命令),则在这个阶段写 Binlog(在命令执行之前写)


BinlogReceiverThread 接收到一个同步命令后,它会给这个命令赋予一个唯一的序列号(这个序列号是递增的),并把它分发给一个 BinlogBGWorker;而各个 BinlogBGWorker 则会根据各个命令的所对应的序列号的顺序来执行各个命令,这样也就保证了命令执行的顺序和主节点执行的顺序一致了


之所以这么设计主要原因是


  1. 配备多个 BinlogBGWorker 是可以提高主从同步的效率,减少主从同步的滞后延迟;

  2. 让 BinlogBGWorker 在执行执行之前写 Binlog 可以提高命令执行的并行度;

  3. 在当前节点是非只读状态,让 BinglogReceiverThread 来写 Binlog,是为了让 Binglog 里保存的命令顺序和命令的执行顺序保持一致;


数据备份


不同于 Redis,Pika 的数据主要存储在磁盘中,这就使得其在做数据备份时有天然的优势,可以直接通过文件拷贝实现。


流程:

  • 打快照:阻写,并在这个过程中或的快照内容;

  • 异步线程拷贝文件:通过修改 RocksDB 提供的 BackupEngine 拷贝快照中文件,这个过程中会阻止文件的删除。


快照内容


  • 当前 db 的所有文件名

  • manifest 文件大小

  • sequence_number

  • 同步点:binlog index + offset




秒删大量的 key


在我们大量的使用场景中,对于 Hash、zset、set、list 这几种多数据机构,当 member 或者 field 很多的时候,用户有批量删除某一个 key 的需求, 那么这个时候实际删除的就是 RocksDB 底下大量的 kv 结构,如果只是单纯暴力的进行删 key 操作,那时间肯定非常的慢,难以接受。那我们如何快速删除 key?


刚才的 nemo 的实现里面我们可以看到,我们在 value 里面增加了 version、ttl 字段,这两个字段就是做这个事情。


Solution 0:暴力删除每一个 member,时间复杂度O(m) ,m 是 member 的个数;


优点:易实现;

缺点:同步处理,会阻碍请求;


Solution 1:启动后台线程,维护删除队列,执行删除,时间复杂度 O(m)


优点:不会明显阻住 server;

缺点:仍然要 O(m) 去删除 members,可以优化删除的速度;


Redis 是怎么做的?


旧版本的Del接口,在实际 free 大量内存的时候仍然会阻塞 server;

新版增加了 lazy free,根据当前 server 的负载,多次动态 free;


Solution 2:不删除,只做标记,时间复杂度 O(1)


优点:效率就够了;

缺点:需要改动下层 RocksDB,一定程度破坏了 RocksDB 的封装,各个模块之间耦合起来;


方案:


Key的元信息增加版本,表示当前 key 的有效版本;


操作:


Put:查询 key 的最新版本,后缀到 val;

Get:查询 key 的最新版本,过滤最新的数据;

Iterator: 迭代时,查询 key 的版本,过滤旧版本数据;


Compact:数据的实际删除是在 Compact 过程中,根据版本信息过滤;

目前 nemo 采用的就是第二种,通过对 RocksDB 的修改,可以实现秒删的功能,后续通过修改 RocksDB compact 的实现,在 compact 的过程中,将历史的数据淘汰掉。


数据 compact 策略


RoRocksDBcksdb 的 compact 策略是在写放大,读放大,空间放大的权衡。


那么我们 DBA 经常会存在需求尽可能减少空间的使用,因此 DBA 希望能够随时触发手动 compact,而又尽可能的不影响线上的使用。而 RocksDB 默认的手动 compact 策略是最高优先级的,会阻塞线上的正常流程的合并。


RocksDB 默认的 manual compact 的限制


  1. 当 manual compact 执行时,会等待所有的自动 compact 任务结束,然后才会执行本次 manual compact;

  2. manual 执行期间,自动 compact 无法执行


  • 当 manual 执行很长时间,无法执行自动 compact,导致线上新的写请求只能在 memtable 中;

  • 当 memtable 个数超过设置的 level0_slowdown_writes_trigger(默认20),写请求会出被 sleep;

  • 再严重一些,当超过 level0_stop_writes_trigger(默认 24),完全停写;


为了避免这种情况,我们对 compact 的策略进行调整,使得自动 compact 一直优先执行,避免停写;


总结


恢复时间长

Pika 的存储引擎是 nemo, nemo 使用的是 RocksDB,我们知道 Rocksdb 启动不需要加载全部数据,只需要加载几M的 log 文件就可以启动,因此恢复时间非常快;


一主多从,主从切换代价大

在主从切换的时候,新主确定以后,从库会用当前的偏移量尝试与新主做一次部分同步,如果部分同步不成功才做全同步,这样尽可能的减少全同步次数;


缓冲区写满问题

Pika 不适用内存buffer 进行数据同步,Pika 的主从同步的操作记录在本地的 binlog 上,binlog 会随着操作的增长进行 rotate 操作,因此不会出现把缓冲区写满的问题;


内存昂贵问题

Pika 的存储引擎 nemo 使用的是 RocksDBRocksDB 和同时使用内存和磁盘减少对内存的依赖。同时我们尽可能使用 SSD 盘来存放数据,尽可能跟上 Redis 的性能。


Pika vs Redis


Pika 相对于 Redis,最大的不同就是 Pika 是持久化存储,数据存在磁盘上,而Redis 是内存存储,由此不同也给 Pika 带来了相对于 Redis 的优势和劣势。


优势


  • 容量大:Pika 没有 Redis 的内存限制,最大使用空间等于磁盘空间的大小;

  • 加载 db 速度快:Pika 在写入的时候, 数据是落盘的, 所以即使节点挂了, 不需要 rbd 或者 aof,Pika 重启不用重新加载数据到内存而是直接使用已经持久化在磁盘上的数据, 不需要任何数据回放操作,这大大降低了重启成本;

  • 备份速度快:Pika备份的速度大致等同于 cp 的速度(拷贝数据文件后还有一个快照的恢复过程,会花费一些时间),这样在对于百G大库的备份是快捷的,更快的备份速度更好的解决了主从的全同步问题。


劣势


由于 Pika 是基于内存和文件来存放数据,所以性能肯定比 Redis 低一些,但是我们一般使用 SSD 盘来存放数据,尽可能跟上 Redis 的性能。


总结


从以上的对比可以看出,如果你的业务场景的数据比较大,Redis 很难支撑, 比如大于 50G,或者你的数据很重要,不允许断电丢失,那么使用 Pika 就可以解决你的问题。


而在实际使用中,大多数场景下 Pika 的性能大约是 Redis 的 50%~80%,在某些特定场景下,例如 range 500,Pika 的性能只有 Redis 的 20%,针对这些场景我们仍然在改进。


在 360 内部使用情况


粗略的统计如下:

当前每天承载的总请求量超过 100 亿,实例数超过 100 个

当前承载的数据总量约 3 TB


性能对比


Server Info:

CPU: 24 Cores, Intel® Xeon® CPU E5-2630 v2 @ 2.60GHz

MEM: 165157944 kB

OS: CentOS release 6.2 (Final)

NETWORK CARD: Intel Corporation I350 Gigabit Network Connection


  • 测试过程,在 Pika 中先写入 150G 大小的数据,写入 Hash key 50 个,field 1千万级别。

  • Redis 写入 5G 大小的数据。


  • Pika:18 个线程

  • Redis:单线程




结论


Pika 的单线程的性能肯定不如 Redis,Pika 是多线程的结构,因此在线程数比较多的情况下,某些数据结构的性能可以优于 Redis。


WiKi


github 地址:

https://github.com/Qihoo360/Pika


github wiki:

https://github.com/Qihoo360/Pika/wiki/Pika介绍


FAQ


1. 如果我们想使用新 DB,那核心问题是如何进行数据迁移,从 Redis 迁移到 Pika 需要经过几个步骤?

开发需要做的:

开发不需要做任何事,不用改代码、不用替换 driver(Pika 使用原生 Redis 的 driver),什么都不用动,看 DBA 干活就好。


DBA 需要做的

  1. 迁移 Redis 数据到 Pika;

  2. 将 Redis 的数据实时同步到 Pika,确保 Redis 与 Pika 的数据始终一致;

  3. 切换 LVS 后端 IP,由 Pika 替换 Redis


2. 迁移过程中需要停业务/业务会受到影响吗?

然而并不会


3. 迁移是无缝且温和的吗?

那当然


4. 和你们公司内部的 bada 有什么区别?

我们之前在 bada 上面支持过 Redis 的多数据结构,并且兼容 Redis 协议,但是遇到了问题。


在分布式系统里面,对 key 的 hash 场景的通常是两种方案:

  • 以 BigTable 为代表的,支持 range key 的 hash 方案。这个方案的好处是可以实现动态的扩展。

  • 以 Dynamo 为代表取模的 hash 方案,这个方案的好处是时间简单。


我们 bada 目前支持的是取模的 hash 方案,在实现 Redis 的多数据结构的时候,比如 hash 我们采用 key 取模找到对应的分片。那么这样带来的问题是由于多数据结构里面 key 不多,field 比较多的场景还是大部分的情况,因此极容易照成分片的不均匀,性能退化很明显。


5. 为什么 Pika 使用多线程而不是像 Redis 单线程的结构

因为 Redis 所有的操作都是对于内存的操作,因此理论上 Redis 的每次操作很短的。


Q&A


1、数据分片是在代理层做的?集合操作落在不同的槽,比如 mget,是在代理层聚合的?

陈宗志:目前没有对数据进行分片,你可以理解成和单机 Redis 类似,支持 master-slave 的架构,因此单个 pika 实例存储的大小的限制是磁盘大小的限制。


2、pika 支持的客户端有哪些,是否支持 pipelining?

陈宗志:pika 支持所有的 Redis 客户端,因为 pika 设计之初就考虑到了用户的迁移成本,因此各种语言的客户端都支持。pipelining 是客户端来做的,因此我们是支持 pipelining 的。


3、为什么不考虑 Redis cluster shard 呢?

陈宗志:我们开始做 pika 的时候,Redis cluster shard 还不完善,而且 Redis cluster 定位的场景和 pika 还是有区别。目前我们内部还没大范围使用 Redis cluster。


4、不理解前面为什么加 LVS?Redis 类服务都是带状态,负载反而用吧?

陈宗志:我们暴露给用户的 ip 是我们 LVS 的 ip。在 Redis 前面 LVS 是为了方便主从切换,这样可以做到用户完全不感知。这里 LVS 下面挂的多个 Redis 实例,都是 master-slave 结构的。


5、有没有对比过 ssdb,LevelDB?优势是什么?

陈宗志:我们公司内部有业务部门用 ssdb,目前除了游戏大部分的 ssdb 已经迁移到 pika上来。我觉得 pika 的优势在于我们代码实现的比较细,性能会比较好。


6、存储引擎为什么没有选择 LevelDB 呢,另外市面上有类似的方案如 ssdb,有什么不同之处吗?

陈宗志:存储引擎上我们在 LevelDB,RocksDB 上面做过对比。LevelDB,RocksDB 在数据量比较小的时候性能差异不大,但是在数据量比较大的情况下,比如 200G 的时候,RocksDB 的性能会比 LevelDB 要来得好。


但是 RocksDB 也有他的缺点,就是代码没有 LevelDB 来的那么优雅,我一直觉得一个好的 c++ 程序员看 LevelDB 代码和 effective c++  就好了


7、若类似于单机 Redis,那么单机性能是个瓶颈吧?大量的客户端连接,命令处理,以及网卡流量等

陈宗志:是的。所以目前内部的 pika 的架构是支持一主多从、多机房自洽的方案。目前线上最多一个主 14 个从这样的结构。DBA 可以很容易的slaveof 给一个主挂上slave,然后进行数据的全同步过程。


8、pika 的多线程比 Redis 的全内存,在 get上竟然快两倍?set 也快,不存在多线程的锁消耗吗?

陈宗志:这里大家可以看到,这个测试结果是 pika work thread 开了 18 个。


在多数据结构的接口里面 kv 的结构的性能是最好的,而多数据结构的接口比如 hash、zset 等等就算开了 18 个线程,性能依然不如 Redis 要来得好。因为 hash、zset 等数据结构需要有抢占多数据结构元数据锁的开销,因此性能很容易下来。但是 kv 接口基本没有锁的开销。唯一的锁开销就是 RocksDB 为了实现线程安全增加的锁,因此这个结果也是可以理解了。


9、完全是因为分布式切片不均的缘故,而放弃分布式集群吗?m-s架构每个节点不都是全量数据,占用更多资源吗?

陈宗志:其实我们在 bada 里面增加了多数据结构的接口,并且兼容了 Redis 的协议,但是后来用户的使用中,发现其实使用多数据结构接口的用户数据量其实不是特别大。单机 1T 的盘基本都能够承受下来。但是还是因为 Hash 分布式切片不均衡,导致我们的维护成本增加,因此我们去实现了 m-s 架构方案。


目前 bada 的方案也是和 pika 并存的方案,我们会根据用户具体的使用场景推荐使用的存储方案。我一直觉得肯定不是一套存储方案解决公司内部的所有需求,一定是某一个方案更适用于某一种存储方案。


10、除了类比为单机 Redis 外,有没有考虑分布式支持?比如 Redis 的 sentinel 或者支持 Codis 这样可能其它 Redis 集群可以无缝迁移。

陈宗志:Pika 目前并没有使用类似 Redis 的 sentinel,pika 前面是挂 LVS 来负责主从切换。目前也没有使用 Codis 这样的 proxy 方案。


11、一主 14 个从?主从同步岂不是很慢?另外,从是只读的吧,读从的话,从的数据可能是过期的,数据一致性怎么解决?

陈宗志:一主 14 从的场景是用户的写入都是晚上定期的灌数据,读取的话从各个从库进行读取。因此这个数据一致性是用户可以接受的场景。


推荐阅读



欢迎关注 360 基础架构组公众号



本文策划陈刚,了解更多 Redis 与架构内容请关注公众号。转载请注明来自高可用架构「ArchNotes」微信公众号及包含以下二维码。


高可用架构

改变互联网的构建方式


长按二维码 关注「高可用架构」公众号


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存