温馨提示:
1. 部分包含数学公式或PPT动画的文件,查看预览时可能会显示错乱或异常,文件下载后无此问题,请放心下载。
2. 本文档由用户上传,版权归属用户,汇文网负责整理代发布。如果您对本文档版权有争议请及时联系客服。
3. 下载前请仔细阅读文档内容,确认文档内容符合您的需求后进行下载,若出现内容与标题不符可向本站投诉处理。
4. 下载文档时可能由于网络波动等原因无法下载或下载错误,付费完成后未能成功下载的用户请联系客服处理。
网站客服:3074922707
美团-2020美团技术年货后台篇-2021.1-391页
2
2020
技术
年货
后台
2021.1
391
2020 美团技术年货CODE A BETTER LIFE【后台篇】后台1Java 线程池实现原理及其在美团业务中的实践1美团万亿级KV存储架构与实践32Java 中 9 种常见的 CMSGC 问题分析与解决53美团配送 A/B 评估体系建设实践120新一代垃圾回收器 ZGC 的探索与实践134设计模式在外卖营销业务中的实践154美团命名服务的挑战与演进176美团 MySQL 数据库巡检系统的设计与应用197Kubernetes 如何改变美团的云基础设施?206基本功|Java 即时编译器原理解析及实践224MyBatis 版本升级引发的线上告警回顾及原理分析257复杂环境下落地 ServiceMesh 的挑战与实践273C+服务编译耗时优化原理及实践287速度与压缩比如何兼得?压缩算法在构建部署中的优化310美团 OCTO 万亿级数据中心计算引擎技术解析328IntelPAUSE 指令变化影响到 MySQL 的性能,该如何解决?337美团内部讲座周烜:华东师范大学的数据库系统研究357目录Java 线程池实现原理及其在美团业务中的实践作者:致远陆晨随着计算机行业的飞速发展,摩尔定律逐渐失效,多核 CPU 成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C 提供的线程池:ThreadPoolExecutor 类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一个开发人员必修的基本功。本文开篇简述线程池概念和用途,接着结合线程池的源码,帮助读者领略线程池的设计思路,最后回归实践,通过案例讲述使用线程池遇到的问题,并给出了一种动态化线程池解决方案。一、写在前面1.1线程池是什么线程池(ThreadPool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如 MySQL。线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。而本文描述线程池是 JDK 中提供的 ThreadPoolExecutor 类。后台2美团 2020 技术年货当然,使用线程池可以带来一系列好处:降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。提高响应速度:任务到达时,无需等待线程创建即可立即执行。提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。1.2线程池解决的问题是什么线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:1.频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。2.对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。3.系统无法合理管理内部的资源分布,会降低系统的稳定性。为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。Poolingisthegroupingtogetherofresources(assets,equipment,personnel,effort,etc.)forthepurposesofmaximizingadvantageorminimizingrisktotheusers.Thetermisusedinfinance,computingandequipmentmanage-ment.wikipedia“池化”思想不仅仅能应用在计算机领域,在金融、设备、人员管理、工作管理等领域也有相关的应用。后台美团 2020 技术年货图 1ThreadPoolExecutor UML 类图ThreadPoolExecutor 实 现 的 顶 层 接 口 是 Executor,顶 层 接 口 Executor 提 供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供 Runnable 对象,将任务的运行逻辑提交到执行器(Executor)中,由 Executor 框架完成线程的调配和任务的执行部分。ExecutorService 接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成 Future 的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。AbstractExecutorService 则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor 将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。ThreadPoolExecutor 是如何运行,如何同时维护线程和执行任务的呢?其运行机制如下图所示:后台美团 2020 技术年货private final AtomicInteger ctl=new AtomicInteger(ctlOf(RUNNING,0);ctl 这个 AtomicInteger 类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段,它同时包含两部分的信息:线程池的运行状态(runState)和线程池内有效线程的数量(workerCount),高 3 位保存 runState,低 29 位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如以下代码所示:private static int runStateOf(int c)return c&CAPACITY;/计算当前运行状态private static int workerCountOf(int c)return c&CAPACITY;/计算当前线程数量private static int ctlOf(int rs,int wc)return rs|wc;/通过状态和线程数生成 ctlThreadPoolExecutor 的运行状态有 5 种,分别为:其生命周期转换如下入所示:后台7图 3线程池生命周期2.3任务执行机制2.3.1任务调度任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。首先,所有任务的调度都是由 execute 方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:1.首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务。2.如果 workerCount=corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。4.如 果 workerCount=corePoolSize&workerCount=maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务,默认的处理方式是直接抛异常。其执行流程如下图所示:8美团 2020 技术年货图 4任务调度流程2.3.2任务缓冲任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,后台美团 2020 技术年货现线程管理模块和任务管理模块之间的通信。这部分策略由 getTask 方法实现,其执行流程如下图所示:图 6获取任务流程图getTask 这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回 null 值。工作线程 Worker会不断接收新任务去执行,而当工作线程 Worker 接收不到任务的时候,就会开始被回收。2.3.4任务拒绝任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到 maximumPoolSize 时,就需要拒绝掉该后台美团 2020 技术年货就对应核心线程创建时的情况;如果这个值是 null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。Worker 执行任务的模型如下图所示:图 7Worker 执行任务线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张 Hash 表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。Worker 是通过继承 AQS,使用 AQS 来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用 AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。1.lock 方法一旦获取了独占锁,表示当前线程正在执行任务中。2.如果正在执行任务,则不应该中断线程。3.如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。4.线程池在执行 shutdown 方法或 tryTerminate 方法时会调用 interruptI-dleWorkers 方法来中断空闲的线程,interruptIdleWorkers 方法会使用tryLock 方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。后台美团 2020 技术年货图 9申请线程执行流程图2.4.3Worker 线程回收线程池中线程的销毁依赖 JVM 自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被 JVM 回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker 被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限后台美团 2020 技术年货执行流程如下图所示:图 11执行任务流程后台美团 2020 技术年货场景 2:快速处理批量任务描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的 corePoolSize 去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。图 13并行执行任务提升批量任务执行速度3.2实际问题及方案思考线程池使用面临的核心的问题在于:线程池的参数并不好配置。一方面线程池的运行后台美团 2020 技术年货图 15线程池队列长度设置过长、corePoolSize 设置过小导致任务执行速度低业务中要使用线程池,而使用不当又会导致故障,那么我们怎样才能更好地使用线程池呢?针对这个问题,我们下面延展几个方向:1.能否不用线程池?回到最初的问题,业务使用线程池是为了获取并发性,对于获取并发性,是否可以有什么其他的方案呢替代?我们尝试进行了一些其他方案的调研:后台美团 2020 技术年货将修改线程池参数的成本降下来,这样至少可以发生故障的时候可以快速调整从而缩短故障恢复的时间呢?基于这个思考,我们是否可以将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化前后的参数修改流程对比如下:图 16动态修改线程池参数新旧流程对比基于以上三个方向对比,我们可以看出参数动态化方向简单有效。3.3动态化线程池3.3.1整体设计动态化线程池的核心设计包括以下三个方面:1.简化线程池配置:线程池构造参数有 8 个,但是最核心的是 3 个:corePool-Size、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,LessisMore。2.参数可动态修改:为了解决参数不好配,修改参数成本高等问题。在 Java 线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,后台美团 2020 技术年货图 18动态化线程池功能架构参数动态化JDK 原生线程池 ThreadPoolExecutor 提供了如下几个 public 的 setter 方法,如下图所示:图 19JDK 线程池参数设置接口JDK 允许线程池使用方通过 ThreadPoolExecutor 的实例来动态设置线程池的核心策略,以 setCorePoolSize 为方法例,在运行期线程池使用方调用此方法设置corePoolSize 之后,线程池会直接覆盖原来的 corePoolSize 值,并且基于当前后台美团 2020 技术年货介绍。重点是基于这几个 public 方法,我们只需要维护 ThreadPoolExecutor 的实例,并且在需要修改的时候拿到实例修改其参数即可。基于以上的思路,我们实现了线程池参数的动态化、线程池参数在管理平台可配置可修改,其效果图如下图所示:图 21可动态修改线程池参数用户可以在管理平台上通过线程池的名字找到指定的线程池,然后对其参数进行修改,保存后会实时生效。目前支持的动态参数包括核心数、最大值、队列长度等。除此之外,在界面中,我们还能看到用户可以配置是否开启告警、队列等待任务告警阈后台美团 2020 技术年货图 22大象告警通知2.任务级精细化监控在传统的线程池应用场景中,线程池中的任务执行情况对于用户来说是透明的。比如在一个具体的业务场景中,业务开发申请了一个线程池同时用于执行两种任务,一个是发消息任务、一个是发短信任务,这两类任务实际执行的频率和时长对于用户来说后台美团 2020 技术年货图 25线程池实时运行情况3.4实践总结面对业务中使用线程池遇到的实际问题,我们曾回到支持并发性问题本身来思考有没有取代线程池的方案,也曾尝试着去追求线程池参数设置的合理性,但面对业界方案具体落地的复杂性、可维护性以及真实运行环境的不确定性,我们在前两个方向上可谓“举步维艰”。最终,我们回到线程池参数动态化方向上探索,得出一个且可以解决业务问题的方案,虽然本质上还是没有逃离使用线程池的范畴,但是在成本和收益后台美团 2020 技术年货美团万亿级KV存储架构与实践作者:泽斌KV存储作为美团一项重要的在线存储服务,承载了在线服务每天万亿级的请求量。在2019年QCon全球软件开发大会(上海站)上,美团高级技术专家齐泽斌分享了美团点评万亿级KV存储架构与实践,本文系演讲内容的整理,主要分为四个部分:第一部分讲述了美团KV存储的发展历程;第二部分阐述了内存KVSquirrel架构和实践;第三部分介绍了持久化KVCellar架构和实践;最后分享了未来的发展规划和业界新趋势。美团点评 KV 存储发展历程美团第一代的分布式KV存储如下图左侧的架构所示,相信很多公司都经历过这个阶段。在客户端内做一致性哈希,在后端部署很多的Memcached实例,这样就实现了最基本的KV存储分布式设计。但这样的设计存在很明显的问题:比如在宕机摘除节点时,会丢数据,缓存空间不够需要扩容,一致性哈希也会丢失一些数据等等,这样会给业务开发带来的很多困扰。随着Redis项目的成熟,美团也引入了Redis来解决我们上面提到的问题,进而演进出来如上图右侧这样一个架构。大家可以看到,客户端还是一样,采用了一致性哈后台美团 2020 技术年货求。最终,我们决定在已应用的开源系统之上进行自研。刚好在 2015年,Redis官方正式发布了集群版本RedisCluster。所以,我们紧跟社区步伐,并结合内部需求做了很多开发工作,演进出了全内存、高吞吐、低延迟的KV存储Squirrel。另外,基于Tair,我们还加入了很多自研的功能,演进出持久化、大容量、数据高可靠的KV存储Cellar。因为Tair的开源版本已经有四五年没有更新了,所以,Cellar的迭代完全靠美团自研,而Redis社区一直很活跃。总的来说,Squirrel的迭代是自研和社区并重,自研功能设计上也会尽量与官方架构进行兼容。后面大家可以看到,因为这些不同,Cellar和Squirrel在解决同样的问题时也选取了不同的设计方案。这两个存储其实都是KV存储领域不同的解决方案。在实际应用上,如果业务的数据量小,对延迟敏感,我们建议大家用Squirrel;如果数据量大,对延迟不是特别敏感,我们建议用成本更低的Cellar。目前这两套KV存储系统在美团内部每天的调用量均已突破万亿,它们的请求峰值也都突破了每秒亿级。内存 KV Squirrel 架构和实践在开始之前,本文先介绍两个存储系统共通的地方。比如分布式存储的经典问题:数据是如何分布的?这个问题在KV存储领域,就是Key是怎么分布到存储节点上的。这后台美团 2020 技术年货Squirrel 架构上图就是我们的Squirrel架构。中间部分跟Redis官方集群是一致的。它有主从的结构,Redis实例之间通过Gossip协议去通信。我们在右边添加了一个集群调度平台,包含调度服务、扩缩容服务和高可用服务等,它会去管理整个集群,把管理结果作为元数据更新到ZooKeeper。我们的客户端会订阅ZooKeeper上的元数据变更,实时获取到集群的拓扑状态,直接在Redis集群进行读写操作。Squirrel 节点容灾然后再看一下Squirrel容灾怎么做。对于Redis集群而言,节点宕机已经有完备的处理机制了。官方提供的方案,任何一个节点从宕机到被标记为FAIL摘除,一般需要经过30秒。主库的摘除可能会影响数据的完整性,所以,我们需要谨慎一些。但是对于从库呢?我们认为这个过程完全没必要。另一点,我们都知道内存的KV存储数据量一般都比较小。对于业务量很大的公司来说,它往往会有很多的集群。如果发生交换机故障,会影响到很多的集群,宕机之后去补副本就会变得非常麻烦。为了解决这两个问题,我们做了HA高可用服务。它的架构如下图所示,它会实时监控集群的所有节点。不管是网络抖动,还是发生了宕机(比如说Redis2),它可以实时更新ZooKeeper,告诉ZooKeeper去摘除Redis2,客户端收到消息后,读流量就直接路由到Redis3 上。如果Redis2只后台美团 2020 技术年货需要任何人工的介入。Squirrel 跨地域容灾我们解决了单节点宕机的问题,那么跨地域问题如何解决呢?我们首先来看下跨地域有什么不同。第一,相对于同地域机房间的网络而言,跨地域专线很不稳定;第二,跨地域专线的带宽是非常有限且昂贵的。而集群内的复制没有考虑极端的网络环境。假如我们把主库部署到北京,两个从库部署在上海,同样一份数据要在北上专线传输两次,这样会造成巨大的专线带宽浪费。另外,随着业务的发展和演进,我们也在做单元化部署和异地多活架构。用官方的主从同步,满足不了我们的这些需求。基于此,我们又做了集群间的复制方案。如上图所示,这里画出了北京的主集群以及上海的从集群,我们要做的是通过集群同步服务,把北京主集群的数据同步到上海从集群上。按照流程,首先要向我们的同步调度模块下发“在两个集群间建立同步链路”的任务,同步调度模块会根据主从集群的拓扑结构,把主从集群间的同步任务下发到同步集群,同步集群收到同步任务后会扮成Redis的Slave,通过Redis的复制协议,从主集群上的从库拉取数据,包括RDB 以及后续的增量变更。同步机收到数据后会把它转成客户端的写命令,写到上后台美团 2020 技术年货下面我们按照工作流,讲一下它是如何运行的。首先生成迁移任务,这步的核心是“就近原则”,比如说同机房的两个节点做迁移肯定比跨机房的两个节点快。迁移任务生成之后,会把任务下发到一批迁移机上。迁移机迁移的时候,有这样几个特点:第一,会在集群内迁出节点间做并发,比如同时给Redis1、Redis3下发迁移命令。第二,每个Migrate命令会迁移一批Key。第三,我们会用监控服务去实时采集客户端的成功率、耗时,服务端的负载、QPS等,之后把这个状态反馈到迁移机上。迁移数据的过程就类似TCP慢启动的过程,它会把速度一直往上加,若出现请求成功率下降等情况,它的速度就会降低,最终迁移速度会在动态平衡中稳定下来,这样就达到了最快速的迁移,同时又尽可能小地影响业务的正常请求。接下来,我们看一下大Value的迁移,我们实现了一个异步Migrate命令,该命令执行时,Redis的主线程会继续处理其他的正常请求。如果此时有对正在迁移Key的写请求过来,Redis会直接返回错误。这样最大限度保证了业务请求的正常处理,同时又不会阻塞主线程。后台美团 2020 技术年货少了很多的全量重传。另外,我们通过控制在低峰区生成RDB,减少了很多RDB造成的抖动。同时,我们也避免了写AOF造成的抖动。不过,这个方案因为写AOF是完全异步的,所以会比官方的数据可靠性差一些,但我们认为这个代价换来了可用性的提升,这是非常值得的。Squirrel 热点 Key下面看一下Squirrel的热点Key解决方案。如下图所示,普通主、从是一个正常集群中的节点,热点主、从是游离于正常集群之外的节点。我们看一下它们之间怎么发生联系。当有请求进来读写普通节点时,节点内会同时做请求Key的统计。如果某个Key达到了一定的访问量或者带宽的占用量,会自动触发流控以限制热点Key访问,防止节点被热点请求打满。同时,监控服务会周期性的去所有Redis实例上查询统计到的热点Key。如果有热点,监控服务会把热点Key所在Slot上报到我们的迁移服务。迁移服务这时会把热点主从节点加入到这个集群中,然后把热点Slot迁移到这个热点主从上。因为热点主从上只有热点Slot的请求,所以热点Key 的处理能力得到了大幅提升。通过这样的设计,我们可以做到实时的热点监控,并及时通过流控去后台美团 2020 技术年货Cellar 节点容灾介绍完整体的架构,我们看一下Cellar怎么做节点容灾。一个集群节点的宕机一般是临时的,一个节点的网络抖动也是临时的,它们会很快地恢复,并重新加入集群。因为节点的临时离开就把它彻底摘除,并做数据副本补全操作,会消耗大量资源,进而影响到业务请求。所以,我们实现了Handoff机制来解决这种节点短时故障带来的影响。如上图所示,如果A节点宕机了,会触发Handoff机制,这时候中心节点会通知客户端A 节点发生了故障,让客户端把分片1的请求也打到B上。B节点正常处理完客户端的读写请求之后,还会把本应该写入A节点的分片1&2数据写入到本地的Log中。后台美团 2020 技术年货过主动触发Handoff机制,我们就实现了静默升级的功能。Cellar 跨地域容灾下面我介绍一下Cellar跨地域容灾是怎么做的。Cellar跟Squirrel面对的跨地域容灾问题是一样的,解决方案同样也是集群间复制。以下图一个北京主集群、上海从集群的跨地域场景为例,比如说客户端的写操作到了北京的主集群A节点,A节点会像正常集群内复制一样,把它复制到B和D节点上。同时A节点还会把数据复制一份到从集群的H节点。H节点处理完集群间复制写入之后,它也会做从集群内的复制,把这个写操作复制到从集群的I、K节点上。通过在主从集群的节点间建立这样一个复制链路,我们完成了集群间的数据复制,并且这个复制保证了最低的跨地域带宽占用。同样的,集群间的两个节点通过配置两个双向复制的链路,就可以达到双向同步异地多活的效果。Cellar 强一致我们做好了节点容灾以及跨地域容灾后,业务又对我们提出了更高要求:强一致存储。我们之前的数据复制是异步的,在做故障摘除时,可能因为故障节点数据还没复制出来,导致数据丢失。但是对于金融支付等场景来说,它们是不容许数据丢失的。后台美团 2020 技术年货节点流量会很不均衡。所以我们的中心节点还会做Raft组的Leader调度。比如说Slot1存储在节点1、2、4,并且节点1是Leader。如果节点1挂了,Raft把节点2选成了Leader。然后节点1恢复了并重新加入集群,中心节点这时会让节点2把Leader还给节点1。这样,即便经过一系列宕机和恢复,我们存储节点之间的Leader数目仍然能保证是均衡的。接下来,我们看一下Cellar如何保证它的端到端高成功率。这里也讲三个影响成功率的问题。Cellar遇到的数据迁移和热点Key问题与Squirrel是一样的,但解决方案不一样。这是因为Cellar走的是自研路径,不用考虑与官方版本的兼容性,对架构改动更大些。另一个问题是慢请求阻塞服务队列导致大面积超时,这是Cellar网络、工作多线程模型设计下会遇到的不同问题。Cellar 智能迁移上图是Cellar智能迁移架构图。我们把桶的迁移分成了三个状态。第一个状态就是正常的状态,没有任何迁移。如果这时候要把Slot2从A节点迁移到B 节点,A会给Slot2打一个快照,然后把这个快照全量发到B节点上。在迁移数据的时候,B节点的回包会带回B节点的状态。B的状态包括什么?引擎的压力、网卡流量、队列长度等。A节点会根据B节点的状态调整自己的迁移速度。像Squirrel一样,它经后台美团 2020 技术年货会根据它的请求特点,是读还是写,快还是慢,分到四个队列里。读写请求比较好区分,但快慢怎么分开?我们会根据请求的Key个数、Value 大小、数据结构元素数等对请求进行快慢区分。然后用对应的四个工作线程池处理对应队列的请求,就实现了快慢读写请求的隔离。这样如果我有一个读的慢请求,不会影响另外三种请求的正常处理。不过这样也会带来一个问题,我们的线程池从一个变成四个,那线程数是不是变成原来的四倍?其实并不是的,我们某个线程池空闲的时候会去帮助其它的线程池处理请求。所以,我们线程池变成了四个,但是线程总数并没有变。我们线上验证中这样的设计能把服务TP999的延迟降低86%,可大幅降低超时率。Cellar 热点 Key上图是Cellar热点Key解决方案的架构图。我们可以看到中心节点加了一个职责,多了热点区域管理,它现在不只负责正常的数据副本分布,还要管理热点数据的分布,图示这个集群在节点C、D放了热点区域。我们通过读写流程看一下这个方案是怎么运转的。如果客户端有一个写操作到了A节点,A节点处理完成后,会根据实时的热点统计结果判断写入的Key是否为热点。如果这个Key是一个热点,那么它会在做集群内复制的同时,还会把这个数据复制有热点区域的节点,也就是图中的C、D节点。同时,存储节点在返回结果给客户端时,会告诉客户端,这个Key是热点,这时客户端内会缓存这个热点Key。当客户端有这个Key的读请求时,它就会直接后台美团 2020 技术年货内存了,以后闪存跟内存之间的界限也会变得越来越模糊;最后,看一下计算型硬件,比如通过在闪存上加FPGA卡,把原本应该CPU做的工作,像数据压缩、解压等,下沉到卡上执行,这种硬件能在解放CPU的同时,也可以降低服务的响应延迟。作者简介泽斌,美团点评高级技术专家,2014年加入美团。招聘信息美团基础技术部存储技术中心长期招聘C/C+、Go、Java高级/资深工程师和技术专家,欢迎加入美团基础技术部大家庭。欢迎感兴趣的同学发送简历至:(邮件标题注明:基础技术部-存储技术中心)后台美团 2020 技术年货GC问题处理能力能不能系统性掌握?一些影响因素都是互为因果的问题该怎么分析?比如一个服务RT突然上涨,有GC耗时增大、线程Block增多、慢查询增多、CPU负载高四个表象,到底哪个是诱因?如何判断GC有没有问题?使用CMS有哪些常见问题?如何判断根因是什么?如何解决或避免这些问题?阅读完本文,相信你将会对CMSGC的问题处理有一个系统性的认知,更能游刃有余地解决这些问题,下面就让我们开始吧!1.2概览想要系统性地掌握GC问题处理,笔者这里给出一个学习路径,整体文章的框架也是按照这个结构展开,主要分四大步。建立知识体系:从JVM的内存结构到垃圾收集的算法和收集器,学习GC的基础知识,掌握一些常用的GC问题分析工具。确定评价指标:了解基本GC的评价方法,摸清如何设定独立系统的指标,以及在业务场景中判断GC是否存在问题的手段。场景调优实践:运用掌握的知识和系统评价指标,分析与解决九种CMS中常见GC问题场景。总结优化经验:对整体过程做总结并提出笔者的几点建议,同时将总结到的经验完善到知识体系之中。2.GC 基础在正式开始前,先做些简要铺垫,介绍下JVM内存划分、收集算法、收集器等常用概念介绍,基础比较好的同学可以直接跳过这部分。后台美团 2020 技术年货clean间接管理。任何自动内存管理系统都会面临的步骤:为新对象分配空间,然后收集垃圾对象空间,下面我们就展开介绍一下这些基础知识。2.3分配对象Java中对象地址操作主要使用Unsafe调用了C的allocate和free两个方法,分配方法有两种:空闲链表(freelist):通过额外的存储记录空闲的地址,将随机IO变为顺序IO,但带来了额外的空间消耗。碰撞指针(bumppointer):通过一个指针作为分界点,需要分配内存时,仅需把指针往空闲的一端移动与对象大小相等的距离,分配效率较高,但使用场景有限。2.4收集对象2.4.1识别垃圾引用计数法(ReferenceCounting):对每个对象的引用进行计数,每当有一个地方引用它时计数器+1、引用失效则-1,引用的计数放到对象头中,大于0的对象被认为是存活对象。虽然循环引用的问题可通过Recycler算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。可达性分析,又称引用链法(TracingGC):从GCRoot开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。目前Java中主流的虚拟机均采用此算法。备注:引用计数法是可以处理循环引用问题的,下次面试时不要再这么说啦 后台美团 2020 技术年货把mark、sweep、compaction、copying这几种动作的耗时放在一起看,大致有这样的关系:虽然compaction与copying都涉及移动对象,但取决于具体算法,compaction可能要先计算一次对象的目标地址,然后修正指针,最后再移动对象。copying则可以把这几件事情合为一体来做,所以可以快一些。另外,还需要留意GC带来的开销不能只看Collector的耗时,还得看Allocator。如果能保证内存没碎片,分配就可以用pointerbumping方式,只需要挪一个指针就完成了分配,非常快。而如果内存有碎片就得用freelist之类的方式管理,分配速度通常会慢一些。2.5收集器目前在HotspotVM中主要有分代收集和分区收集两大类,具体可以看下面的这个图,不过未来会逐渐向分区收集发展。在美团内部,有部分业务尝试用了ZGC(感兴趣的同学可以学习下这篇文章新一代垃圾回收器 ZGC 的探索与实践),其余基本都停留在CMS和G1上。另外在JDK11后提供了一个不执行任何垃圾回收动作的回收器Epsilon(ANo-OpGarbageCollector)用作性能分析。另外一个就是Azul的ZingJVM,其C4(ConcurrentContinuouslyCompactingCollector)收集器也在业内有一定的影响力。后台美团 2020 技术年货2.5.3常用收集器目前使用最多的是CMS和G1收集器,二者都有分代的概念,主要内存结构如下:2.5.4其他收集器以上仅列出常见收集器,除此之外还有很多,如Metronome、Stopless、Stac-cato、Chicken、Clover等实时回收器,Sapphire、Compressor、Pauseless等并发复制/整理回收器,Doligez-Leroy-Conthier等标记整理回收器,由于篇幅原因,不在此一一介绍。后台美团 2020 技术年货系统总运行时间的百分比,例如系统运行了100min,GC耗时1min,则系统吞吐量为99%,吞吐量优先的收集器可以接受较长的停顿。目前各大互联网公司的系统基本都更追求低延时,避免一次GC停顿的时间过长对用户体验造成损失,衡量指标需要结合一下应用服务的SLA,主要如下两点来判断:简而言之,即为一次停顿的时间不超过应用服务的TP9999,GC的吞吐量不小于99.99%。举个例子,假设某个服务A的TP9999为80ms,平均GC停顿为30ms,那么该服务的最大停顿时间最好不要超过80ms,GC频次控制在5min以上一次。如果满足不了,那就需要调优或者通过更多资源来进行并联冗余。(大家可以先停下来,看看监控平台上面的gc.meantime分钟级别指标,如果超过了6ms那单机GC吞吐量就达不到4个9了。)备注:除了这两个指标之外还有Footprint(资源量大小测量)、反应速度等指标,互联网这种实时系统追求低延迟,而很多嵌入式系统则追求Footprint。3.1.2读懂GCCause拿到GC日志,我们就可以简单分析GC情况了,通过一些工具,我们可以比较直观地看到Cause的分布情况,如下图就是使用gceasy绘制的图表:后台美团 2020 技术年货 case _tenured_generation_full:return Tenured Generation Full;case _metadata_GC_threshold:return Metadata GC Threshold;case _metadata_GC_clear_soft_refs:return Metadata GC