导读: 本文以双 11 面临的挑战为背景, 从 Tair(阿里自研高速缓存系统)发展和应用开始谈起, 重点分享了性能优化方面的实践, 最后对缓存热点难题给出了解决方案, 希望能对大家的工作有所启发
本文作者为宗岱, 阿里巴巴资深技术专家, 2008 年加入淘宝, 阿里分布式缓存 NoSQL 数据库 Tair 和 Tengine 负责人
Tair 概览
Tair 发展历程
Tair 在阿里巴巴被广泛使用, 无论是淘宝天猫浏览下单, 还是打开优酷浏览播放时, 背后都有 Tair 的身影默默支撑巨大的流量 Tair 的发展历程如下:
2010.04 Tair v1.0 正式推出 @淘宝核心系统;
2012.06 Tair v2.0 推出 LDB 持久化产品, 满足持久化存储需求;
2012.10 推出 RDB 缓存产品, 引入类 Redis 接口, 满足复杂数据结构的存储需求;
2013.03 在 LDB 的基础上针对全量导入场景上线 Fastdump 产品, 大幅度降低导入时间和访问延时;
2014.07 Tair v3.0 正式上线, 性能数倍提升;
2016.11 泰斗智能运维平台上线, 助力 2016 双 11 迈入千亿时代;
2017.11 性能飞跃, 热点散列, 资源调度, 支持万亿流量
Tair 是一个高性能分布式可扩展高可靠的 key/value 结构存储系统! Tair 特性主要体现在以下几个方面:
高性能: 在高吞吐下保证低延迟, Tair 是阿里集团内调用量最大系统之一, 双 11 达到每秒 5 亿次峰值的调用量, 平均访问延迟在 1 毫秒以下;
高可用: 通过自动 failover, 限流, 审计和机房内容灾以及多单元多地域, 确保系统在任何情况下都能正常运行;
规模化: 分布全球各个数据中心, 阿里集团各个 BU 都在使用;
业务覆盖: 电商蚂蚁合一菜鸟高德阿里健康等
Tair 除了普通 Key/Value 系统提供的功能, 比如 getputdelete 以及批量接口外, 还有一些附加的实用功能, 使得其有更广的适用场景 Tair 应用场景包括以下四种:
MDB 典型应用场景: 用于缓存, 降低对后端数据库的访问压力, 比如淘宝中的商品都是缓存在 Tair 中; 用于临时数据存储, 部分数据丢失不会对业务产生较大影响, 例如登陆;
LDB 典型应用场景: 通用 kv 存储交易快照安全风控等; 存储黑白单数据, 读 qps 很高; 计数器功能, 更新非常频繁, 且数据不可丢失
RDB 典型应用场景: 复杂的数据结构的缓存与存储, 如播放列表, 直播间等
FastDump 典型应用场景: 周期性地将离线数据快速地导入到 Tair 集群中, 快速使用到新的数据, 对在线读取要求非常高; 读取低延迟, 不能有毛刺
双 11 挑战怎么办?
2012-2017 年数据如图, 可以看到, 2012 年 GMV 小于 200 亿, 2017 年 GMV 达到 1682 亿, 交易创建峰值从 1.4 万达到 32.5 万, 峰值 QPS 从 1300 万达到近 5 亿
从图中可以看出, tair 访问增速远大于交易创建峰值, 交易创建峰值也大于 GMV 的增长也就是 0 点的那刻, 对 Tair 来说, 在保证高并发访问的同时, 如何确保低延迟, 如何确保成本低于业务增速的技术挑战越来越大
对于分布式存储系统来说, 热点问题都是比较难解决的而缓存系统流量特别大, 热点问题更为突出 2017 年双 11, 我们通过了热点散列, 彻底解决掉了缓存热点问题
同时, 为了承载每秒 32.5 万笔交易阿里的技术架构也不断演进成为多地域多单元的架构, 不仅采用了阿里云上的单元, 而且也有和离线服务混部的单元, 这里对我们的挑战是如何快速弹性的部署和下线集群
多地域多单元
先看下我们大致整体的部署架构和 tair 在系统中的位置从这张简图上看到, 我们是一个多地域多机房多单元的部署架构整个系统上从流量的接入层, 到应用层然后应用层依赖了各种中间件, 例如消息队列, 配置中心等等最底层是基础的数据层, tair 和数据库在数据这一层, 我们需要为业务做需要的数据同步, 以保障上层业务是无状态的
多地域多单元除了防止黑天鹅之外, 另外一个重要的作用是能够通过快速上线一个单元来实现承载部分的流量 Tair 也做了一整套控制系统来实现快速的弹性建站
弹性建站
Tair 本身是一个很复杂分布式存储系统, 规模也非常庞大所以我们有一个叫泰斗的运营管理平台在这里面通过任务编排, 任务执行, 验证和交付等流程来确保快速的一键建站, 离在线混部集群的快上快下工作在部署工作完成后, 会经过一系列系统, 集群, 实例上的连通性验证来确保服务完整无误后, 再交付上线使用如果有一丝遗漏, 那么业务流量过来时, 可能会触发大规模故障这里面, 如果是带数据的持久化集群, 那么在部署完成后, 还需要等待存量数据迁移完成并且数据达到同步后才能进入验证阶段
Tair 的每一个业务集群水位其实是不一样的, 双 11 前的每一次全链路压测, 由于业务模型的变化, 所用 Tair 资源会发生变化, 造成水位出现变化在此情况下, 我们每次都需要压测多个集群间调度的 Tair 资源如果水位低, 就会把某些机器服务器资源往水位高挪, 达到所有集群水位值接近
数据同步
多地域多单元, 必须要求我们数据层能够做到数据的同步, 并且能够提供给业务各种不同的读写模式对于单元化业务, 我们提供了本单元访问本地 Tair 的能力, 对于有些非单元化业务, 我们也提供了更灵活的访问模型同步延迟是我们一直在做的事情, 2017 年双 11 每秒同步数据已经达到了千万级别, 那么, 如何更好地解决非单元化业务在多单元写入数据冲突问题? 这也是我们一直考虑的
性能优化降成本
服务器成本并不是随着访问量线性增长, 每年以百分之三四十成本在下降, 我们主要通过服务器性能优化客户端性能优化和不同的业务解决方案三方面达到此目标
先来看下我们如何从服务端角度提升性能和降低成本的这里的工作主要分为两大块: 一块是避免线程切换调度, 降低锁竞争和无锁化, 另外一块是采用用户态协议栈 + DPDK 来将 run-to-completion 进行到底
内存数据结构
MDB 内存数据结构示意图
我们在进程启动之后会申请一大块内存, 在内存中将格式组织起来主要有 slab 分配器 hashmap 和内存池, 内存写满后会经过 LRU 链进行数据淘汰随着服务器 CPU 核数不断增加, 如果不能很好处理锁竞争, 很难提升整体性能
通过参考各种文献, 并结合 tair 自身引擎需求, 我们使用了细粒度锁无锁数据结构 CPU 本地数据结构和 RCU 机制来提升引擎的并行性左图为未经过优化时各个功能模块的 CPU 消耗图, 可以看到网络部分和数据查找部分消耗最多, 优化后 (右图) 有 80% 的处理都是在网络和数据的查找, 这是符合我们期望的
用户态协议栈
锁优化后, 我们发现很多 CPU 消耗在内核态上, 这时我们采用 DPDK+Alisocket 来替换掉原有内核态协议栈, Alisocket 采用 DPDK 在用户态进行网卡收包, 并利用自身协议栈提供 socket API, 对其进行集成我们将 tair,memcached 以及业内以性能著称的 seastar 框架相比, tair 的性能优势在 seastar 10% 以上
内存合并
当性能提升后, 单位 qps 所占用的内存就变少了, 所以内存变得很紧缺另外一个现状, tair 是一个多租户的系统, 各个业务行为不太一样, 时常会造成 page 已经分配完毕, 但是很多 slab 里的 page 都是未写满的而有少量 slab 确实已经全占满了, 造成了看上去有容量, 但无法分配数据的情况
此时, 我们实施了一个将同一 slab 里未写满 page 内存合并的功能, 可以释放出大量空闲内存从图中可以看到, 在同一个 slab 里, 记录了每个 page 的使用率, 并挂载到不同规格的 bucket 上合并时, 将使用率低的 page 往使用率高的 page 上合并同时还需要将各个相关联的数据结构, 包括 LRU 链, 相当于整个内存结构的重整这个功能在线上的公用集群里效果特别好, 根据不同场景, 可以大幅提升内存使用效率
客户端优化
上面这些是服务端的变化, 接下来看看客户端的性能我们的客户端是运行在客户服务器上的, 所以占用了客户的资源如果能尽可能低的降低资源消耗, 对我们整个系统来说, 成本都是有利的客户端我们做了两方面优化: 网络框架替换, 适配协程, 从原有的 mina 替换成 netty, 吞吐量提升 40%; 序列化优化, 集成 kryo 和 hessian, 吞吐量提升 16%
内存网格
如何与业务结合来降低整体 Tair 与业务成本? Tair 提供了多级存储一体化解决业务问题, 比如安全风控场景, 读写量超大有大量本地计算, 我们可以在业务机器本地存下该业务机器所要访问的数据, 大量读会命中在本地, 而且写在一段时间内是可合并的, 在一定周期后, 合并写到远端 Tair 集群上作为最终存储我们提供读写穿透, 包括合并写和原有 Tair 本身具有多单元复制的能力, 双 11 时业务对 Tair 读取降至 27.68%, 对 Tair 写入降至 55.75%
热点难题已解决
缓存击穿
缓存从开始的单点发展到分布式系统, 通过数据分片方式组织, 但对每一个数据分片来说, 还是作为单点存在的当有大促活动或热点新闻时, 数据往往是在某一个分片上的, 这就会造成单点访问, 进而缓存中某个节点就会无法承受这么大压力, 致使大量请求没有办法响应对于缓存系统一个自保的方法是限流但是限流对于整个系统来说, 并无济于事限流后, 一部分流量会去访问数据库, 那依然和刚刚所说的无法承受是一样的结果, 整个系统出现异常
所以在这里, 唯一的解决办法是缓存系统能够作为流量的终结点不管是大促, 还是热点新闻, 还是业务自己的异常缓存都能够把这些流量吸收掉, 并且能让业务看到热点的情况
热点散列
经过多种方案的探索, 采用了热点散列方案我们评估过客户端本地 cache 方案和二级缓存方案, 它们可以在一定程度上解决热点问题, 但各有弊端例如二级缓存的服务器数目无法预估, 本地 cache 方案对的业务侧内存和性能的影响而热点散列直接在数据节点上加 hotzone 区域, 让 hotzone 承担热点数据存储对于整个方案来说, 最关键有以下几步:
智能识别热点数据总是在变化的, 或是频率热点, 或是流量热点内部实现采用多级 LRU 的数据结构, 设定不同权值放到不同层级的 LRU 上, 一旦 LRU 数据写满后, 会从低级 LRU 链开始淘汰, 确保权值高的得到保留
实时反馈和动态散列当访问到热点时, appserver 和服务端就会联动起来, 根据预先设定好的访问模型动态散列到其它数据节点 hotzone 上去访问, 集群中所有节点都会承担这个功能
通过这种方式, 我们将原来单点访问承担的流量通过集群中部分机器来承担
整个工程实现是很复杂的, 热点散列在双 11 中取得了非常显著的效果峰值每秒吸收了 800 多 w 的访问量从右图可以看到, 红色的线是如果未开启热点散列的水位, 绿色的线是开启热点散列的水位如果未开启, 很多集群都超过了死亡水位, 也就是我们集群水位的 130% 而开启之后, 通过将热点散列到整个集群, 水位降低到了安全线下换而言之, 如果不开启, 那么很多集群都可能出现问题
写热点
写热点与读热点有类似的地方, 这块主要是通过合并写操作来实施首先依然是识别出热点, 如果是热点写操作, 那么该请求会被分发到专门的热点合并线程处理, 该线程根据 key 对写请求进行一定时间内的合并, 随后由定时线程按照预设的合并周期将合并后的请求提交到引擎层通过这种方式来大幅降低引擎层的压力
经过双 11 考验对读写热点的处理, 我们可以放心的说, Tair 将缓存包括 kv 存储部分的读写热点彻底解决了
关注技术边城
把握前沿技术脉搏
来源: https://mp.weixin.qq.com/s?__biz=MzU2NzQxNzkxMw==&mid=2247483739&idx=1&sn=0291201f1c428fc118bbd3b8c6b6cf9f&chksm=fc9ccc0dcbeb451b4c56940480ba25afb3f955d77257a01ba7881f3e66df9f1410edee648869#rd