TPP有3600+个场景,每个场景是一些AB(算法方案代码+业务配置+流量分配策略)的集合,场景按业务团队划分物理集群,同一个物理集群内的容器是对等的,JVM内部署着算法容器,算法容器内混布相同的场景集合,算法容器是平台编码,场景方案代码则是算法编码并进行热部署。前端请求以场景为粒度请求RR,RR获取场景所在集群按集群进行路由。如下图所示。

 如前文所述,容器是平台开发编码,代码质量可控,而算法场景代码则是全集团各个算法owner编写,编码质量参差不齐。这种情况下JVM内场景混布就会出现相互影响的问题,如cpu分配不均,内存分配不均等问题,最讨厌的是出现死循环。针对这些问题TPP已经将重要的核心场景和非重要的小场景进行物理隔离,即调配到不同的物理集群,这样一定程度上减少了非重要场景代码问题导致核心场景大量异常的情况,如超时。但非核心集群死循环,甚至核心集群相互影响的情况还是时有发生。那为什么不直接每个场景单独一个容器部署呢,通过docker层面cgroup直接隔离场景是否可行?当然可行,但是机器成本将大幅上升。因为每个容器里要加载各种二方服务,如pandora,forest,igraph,sumamry,各种hsf服务等,而且每个场景要保证至少两台的可用度,这样机器内存规模至少要扩大好多倍,机器数自然答复上涨。很多场景qps非常低的,峰值也是错开的,混布能极大提高资源利用率。我们对隔离做了一些改进工作,包括线程池隔离,多租户隔离。

 首先系统进行了线程池隔离改造,算法方案代码从HSF业务线程直接执行改为HSF业务线程提交给场景线程池执行。每个场景都管理一个自己的线程池,平台根据流量需求可动态调配不同的线程池参数。如下图所示:


 这样做的好处是:

  • 保护了hsf入口工作线程,改造之前算法方案超时严重会造成容器hsf服务pool full。场景线程池隔离后根据场景超时上限(一般是200ms)做超时interrupt,保证不会大量并发堆积。同时场景线程池设置拒绝策略,在并发堆积超过wait_queue+max_pool的情况下立即拒绝服务。这样一定程度提升了hsf的可用性。
  • 减少无用的超时后计算,hsf业务线程并不会被中断,如果算法中途超时了,并没必要做后面的复杂计算工作,浪费的cpu资源也被节约下来。
  • 场景间公平性得到一定保障,代码有问题的场景不占满hsf线程的情况下,其他场景仍能有流量得到服务。

 线程池隔离在双11前也发挥了作用,如rtp第一次成功升级arpc后出现过死锁,发生调用的业务线程都会一直阻塞,如果发生在hsf线程,这台机器就game over了,而通过重置场景业务线程池就能免启动瞬间修复。

 线程池隔离带来的问题是增加一定的上下文切换开销,设置合理的core size和alive time,通过压测和实际运行发现并没有性能下降,也没有明显增加jvm的线程数。这里从同步改造成线程池方式,需要解决一些问题,典型的就是ThreadLocal问题,包括eagleeye和业务threadlocal。下面是支持eagleeye和业务threadlocal透传的线程池实现:

public class SolutionExecutorService extends ThreadPoolExecutor {public SolutionExecutorService(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory, RejectedExecutionHandler handler) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);}@Overrideprotected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {return new SolutionFutureTask<T>(runnable, value);}@Overrideprotected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {return new SolutionFutureTask<T>(callable);}class SolutionFutureTask<T> extends FutureTask<T> {// 当前context透传到工作线程final RpcContext_inner rpcContext = EagleEye.getRpcContext();final Map<String, Object> tppContext = ThreadLocalParams.save();public SolutionFutureTask(Callable<T> callable) {super(callable);}public SolutionFutureTask(Runnable runnable, T result) {super(runnable, result);}public void run() {Profiler.start("RunWithPool.");EagleEye.setRpcContext(rpcContext);ThreadLocalParams.restore(tppContext);try {super.run();} finally {EagleEye.clearRpcContext();ThreadLocalParams.clear();}}}}

 线程池隔离并没有根本解决死循环和cpu分配不均问题,因为cpu密集型计算是无法interrupt的,同时TPP的一个平台价值之一是算法可以随时变更算法方案热部署(包括双11当天),如果方案内存回收不彻底也会造成内存泄漏。因此我们利用多租户进一步解决cpu隔离和内存回收两个问题,改造后的隔离模式如下图所示, 将算法方案之间以及算法与系统之间进行隔离:

 首先结合AJDK的多租户,利用cgroup进行彻底的cpu隔离。但这不是容易的事,对于TPP这样复杂的容器更不容易,下文将介绍TPP多租户改造的艰辛之路。
 cgroup的cpu隔离主要有这么几种方式:cpuset,cpushares,cpu quota.

  • cpuset是cpu核为粒度的物理隔离,我们搜索hippo调度docker容器的时候就是cpuset隔离,保证每个容器不相互影响。
  • cpushares设置使用者的cpu使用权重,权重越大则分配的cpu资源越多,它是个相对值。如3个进程的cpu shares分别为512,1024,1024,则他们满负载时候分配到的cpu资源是1:2:2即20%:40%:40%。如果后两个线程没有满负载,第一个share为512的可以使用超过20%。如果后两个空闲,则第一个可以用到100%,一旦share为1024的进程要使用cpu,则512的进程会让出cpu。
  • 最后cpu quota设置了进程能使用的cpu最大比例绝对值,如cfs_period=100000,cfs_quota=50000,则进程能用到一个cpu core的50%,cfs=50000n则可以用到n个core的50%,总cpu可以使用到50%n/cores。
     再来分析下AJDK的多租户实现原理,首先看一个线程怎么被cgroup限制cpu:
TenantConfiguration tenantConfiguration = new TenantConfiguration(cpuShares, memLimit).limitCpuCfs(cfsPeriod, cfsQuota);
TenantContainer container = TenantContainer.create(name, tenantConfiguration);

用户创建了个租户容器,这里指定了租户的cpu shares,内存上限,cpu利用率上限。然后用户调用租户容器去执行运算。

container.run(new Runnable() {@Overridepublic void run() {doRun();}
});


AJDK底层对多租户的改造有这样一个非常重要的原则:
线程1由租户容器1创建,则线程1创建的其他线程都属于容器1,这些线程整体cpu利用率受容器1的cgroup限制

这个原则会带来什么麻烦事呢,先看看租户执行的代码:

public void run(final Runnable runnable) throws TenantException {if (state == TenantState.DEAD || state == TenantState.STOPPING) {throw new TenantException("Tenant is dead");}// The current thread is already attached to tenantif (this == TenantContainer.current()) {runnable.run();} else {if (TenantContainer.current() != null) {throw new TenantException("must be in root tenant before running into non-root tenant.");}// attach to new tenantattach();try {runnable.run();} finally {// detach from the tenantdetach();}}}

 这里首先检查当前线程所属租户容器(下文以容器1代替)和当前执行租户容器(下文以容器2代替)是否同一个,如果同一个执行执行runnable,这里没有性能开销。如果不是麻烦来了,调用attach通过jni调用绑定当前线程到容器2的cgroup组,然后执行runnable,这时候线程的cpu就得到了租户容器2的cgroup限制,runnable执行结束后再通过jni恢复线程和容器1的绑定。这里有严重的性能开销,即jni调用cgroup非常慢(实测50ms以上)。因此每个场景都要有一个线程池和一个租户容器,线程池必须有一定的coresize和alive,防止频繁new线程调用cgroup产生大耗时,场景线程必须由租户容器创建。这样线程池submit一个task就打到和普通线程池一样的性能,我们为场景线程池定制了ThreadFactory,在线程池隔离的基础上能轻松实现:

static class TenantThreadFactory extends NamedThreadFactory {private TenantContainer container;public TenantThreadFactory(TenantContainer container, String prefix) {super(prefix);this.container = container;}@Overridepublic Thread newThread(Runnable r) {final ObjectWrapper<Thread> wrapper = new ObjectWrapper<>();// 用租户容器去创建线程try {container.run(() -> {return super.newThread(r);wrapper.setObject(t);});} catch (TenantException e) {throw new RuntimeException("create tenant thread exception", e);}return wrapper.getObject();}
}

 对于简单应用到此为止就完成了多租户改造,而对TPP来说则只是完成了一小步。因为TPP接入了大量的二方服务,如IGraph, RTP, SUMMARY,很多HSF服务,Forest等,前文已经介绍过混布场景是为了复用二方服务,为每个场景克隆二方服务client会产生很大的内存开销。这些复用的二方服务也管理了自己的线程池,结合前文所述租户线程创建的其他线程也属于这个租户,一旦二方服务的线程由某个租户创建然后被其他租户复用则产生了cgroup切换的开销,同时cpu分配也会错乱。因此TPP还要对场景租户线程和二方服务线进行隔离,这就涉及对一些核心高并发二方服务(双11 IGraph峰值530w qps,SUMMARY 69w qps, RTP 75w qps)client的改造。原理很简单,为二方服务的线程池增加定制的ThreadFactory:

public class RootTenantThreadFactory extends NamedThreadFactory {public RootTenantThreadFactory(String prefix, boolean daemon) {super(prefix, daemon);}@Overridepublic Thread newThread(Runnable r) {if (JvmUtil.isTenantEnabled()) {final ObjectWrapper<Thread> wrapper = new ObjectWrapper<>();// 用root容器去创建线程try {TenantContainer.primitiveRunInRoot(() -> {Thread t = super.newThread(r);wrapper.setObject(t);});}return wrapper.getObject();} else {return super.newThread(r);}}
}

 对于大部分异步httpclient类的扩展client只需要在构造时候增加设置threadFactory即可:

public class RootTenantThreadFactory extends NamedThreadFactory {public RootTenantThreadFactory(String prefix, boolean daemon) {super(prefix, daemon);}@Overridepublic Thread newThread(Runnable r) {if (Profiler.getEntry() == null) {Profiler.start("Create Pool Thread ");} else {Profiler.enter("Create Pool Thread ");}if (JvmUtil.isTenantEnabled()) {final ObjectWrapper<Thread> wrapper = new ObjectWrapper<>();// 用root容器去创建线程try {TenantContainer.primitiveRunInRoot(() -> {Thread t = super.newThread(r);wrapper.setObject(t);});} finally {Profiler.release();}return wrapper.getObject();} else {return super.newThread(r);}}
}

 友情提示:多租户的隔离方式不当使用会导致宿主机cgroup下目录太多而负载过高,这个之前在sigma上有反馈,容器销毁时需要删除ajdk的进程cgroup目录,需要应用自己操作,幸运的是hippo调度自动完成了这个工作。

最后看一下多租户隔离的效果:
8核虚拟机下进行测试,非租户隔离的情况下集群内其他场景发生死循环,且源源不断的有死循环请求进来,当前场景会因为并发数过大全部被限流

cpu基本被打满(这里对root租户作了10%cpu保护,并不会800%,这里实际略高于720%)

使用多租且限定租户最大cpu使用50%,仍然构建一个场景死循环,可以看到当前场景只是少量超时,因为cgroup的调度也会造成场景rt上升,符合业务95%以上正确率的要求。

观察容器cpu,正常场景800qps时容器cpu仍有余量

接下去我们还会做多租户的动态调控,对于问题场景自动降权,避免cpu的浪费。

最后感谢ajdk团队对我们需求的支持和技术帮助, @传胜 @三红 @右席 @卓仁

TPP稳定性之场景隔离和多租户相关推荐

  1. SaaS 系统架构,租户数据隔离模式与租户信息解析方案!

    这段时候在准备从零开始做一套SaaS系统,之前的经验都是开发单数据库系统并没有接触过SaaS系统,所以接到这个任务的时候也有也些头疼,不过办法部比困难多,难得的机会. 在网上找了很多关于SaaS的资料 ...

  2. TPP多租户隔离之资源清理

      双11的时候TPP引入了ajdk多租户,对场景的cpu进行隔离,参考文章 <TPP稳定性之场景隔离和多租户>.文章中对tpp提供给算法方案的二方服务客户端进行改造,这些共享的二方服务注 ...

  3. 京东OLAP亿级查询高可用实践

    OLAP(On-Line Analytical Processing)是联机分析处理,它主要用于支持企业决策和经营管理,是许多报表.商业智能和分析系统的底层支撑组件,支持从海量数据中快速获取数据指标. ...

  4. K8s 实践 | 如何解决多租户集群的安全隔离问题?

    作者 | 匡大虎  阿里巴巴技术专家 导读:如何解决多租户集群的安全隔离问题是企业上云的一个关键问题,本文主要介绍 Kubernetes 多租户集群的基本概念和常见应用形态,以及在企业内部共享集群的业 ...

  5. 从单租户IaaS到多租户PaaS——金融级别大数据平台MaxCompute的多租户隔离实践

    摘要:在2017年云栖大会•北京峰会的大数据专场中,来自阿里云的高级技术专家李雪峰带来了主题为<金融级别大数据平台的多租户隔离实践>的演讲.在分享中,李雪峰首先介绍了基于传统IaaS单租户 ...

  6. 基于Mybatis-Plus的多租户架构下的数据隔离解决方案

    目录 一.多租户架构 方案1:数据分区隔离(Partitioned (discriminator) data) 方案2:数据库实例隔离(Separate database) 方案3:Schema隔离( ...

  7. 多租户数据隔离的三种方案

    一.多租户在数据存储上存在三种主要的方案,分别是: 1. 独立数据库 这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高. 优点: 为不同的租户提供独立的数据 ...

  8. 微服务的接入层设计与动静资源隔离

    作者:刘超,毕业于上海交通大学,15年云计算领域研发及架构经验,先后在EMC,CCTV证券资讯频道,HP,华为,网易从事云计算和大数据架构工作.在工作中积累了大量运营商系统,互联网金融系统,电商系统等 ...

  9. 漫谈企业级SaaS的多租户设计

    企业级SaaS市场近几年在每个细分领域都涌现出了一批玩家.从技术角度看,不同的领域.不同的SaaS产品,必定有着同样的架构内核,其中最关键的便是对于多租户(Multi-Tenancy)的支持.对广大企 ...

最新文章

  1. 迁移学习与图神经网络“合力”模型:用DoT-GNN克服组重识别难题
  2. scater分析单细胞转录组数据代码
  3. python语言介绍-Python语言的简介
  4. 入门Web前端要注意什么?要学哪些软件?
  5. 在Linux下用源码编译安装apache2
  6. [HNOI 2001]求正整数
  7. python3学习之元组
  8. rr与hr_rr指标:HR和RR的区别
  9. Kali安装foremost
  10. 手机社交游戏与触动用户的环节
  11. 阿里Android开发手册正式版一览
  12. js对联广告,顶部浮动广告,固定位置广告插件
  13. python输入名字配对情侣网名_输入名字配置情侣网名-网名搜索
  14. ih5连接mysql数据库_iH5高级教程:H5数据应用,多种数据的判断
  15. 爬虫实战-链家北京房租数据
  16. 深入解析Word页码设置:你不得不学的Office技巧(一)
  17. FastBoot BootLoader Recovery 模式学习
  18. Springboot毕设项目高校教材征订系统设计与实现ig8t1(java+VUE+Mybatis+Maven+Mysql)
  19. (灵感)集设网关于设计灵感的网站
  20. pandas删除重复数据

热门文章

  1. 共同抵制恶意APP CNCERT公布首批黑名单
  2. python 拆分pdf指定页,Python按页拆分pdf
  3. 30 行Python代码实现蚂蚁森林自动收能量(附送源码)
  4. kafka 修改分区_kafka的分区数设置
  5. vivo联手京东,打通线上线下营销生态
  6. 双系统 Win10下安装Linux(单/双硬盘)
  7. MIUI打开相册怎么默认显示全部照片_小米手机让相册默认展示所有图片怎么设置?
  8. 【Unity 框架】QFramework v1.0 使用指南 工具篇:06. UIKit 界面管理快速开发解决方案 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏
  9. 使用karma + mocha + sinon 测试 Ajax 请求
  10. Bluetooth技术学习笔记 ——蓝牙核心系统架构