精华内容
下载资源
问答
  • 2022-05-28 09:28:40

    前言

    在很多公司,nginx不仅作为反向代理服务器使用,而且承载着一部分静态资源存储的功能,比如将图片等静态资源放在nginx目录下,比较熟悉的是,在一些前后端分离的网站中,某些情况下,为了能充分提升用户访问页面时的加载体验,常常需要将一部分的html等静态资源缓存在nginx中,这就涉及到nginx 静态资源优化相关的配置

    静态资源优化常用配置策略

    下面提供几个常用的静态资源优化的配置指令,以供实战参考

    • sendfile on;
    • tcp_nopush on;
    • tcp_nodeplay on;
    • gzip on;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    

    一、sendfile 指令

    用来开启高效的文件传输模式

    在这里插入图片描述

    一个正常的请求静态资源的过程大概是这样的:

    1. 客户端通过网络接口向服务端发送请求;
    2. 操作系统将客户端请求传递给服务器端应用程序;
    3. 服务器端应用程序处理这些请求,请求处理完成后,操作
    更多相关内容
  • SpringBoot 实战:加载读取资源文件

    千次阅读 多人点赞 2021-10-24 10:46:15
    Resource接口抽象出一种更底层的方式管理资源,可以实现通过统一的方式处理各类文件资源。下面是几种获取资源实例的方法。 手动加载 访问类路径中的文件,我们可以直接使用ClassPathResource加载内容,比如: new ...

    SpringBoot 实战:加载和读取资源文件内容

    该图片由Marna BuysPixabay上发布

    你好,我是看山。

    本文聊一聊在 SpringBoot 应用中,访问加载类路径(classpath)中的文件内容的多种方法。

    通过Resource接口

    Resource接口抽象出一种更底层的方式管理资源,可以实现通过统一的方式处理各类文件资源。下面是几种获取资源实例的方法。

    手动加载

    访问类路径中的文件,我们可以直接使用ClassPathResource加载内容,比如:

    new ClassPathResource("data/resource-data.txt");
    

    默认情况下,ClassPathResource会在线程的上下文类加载器和默认系统类加载器之间进行选择,以删除样板文件。我们也可以直接指定类加载器,比如:

    new ClassPathResource("data/resource-data.txt", this.getClass().getClassLoader());
    

    或者是通过指定类的类加载器:

    new ClassPathResource("data/resource-data.txt", Employee.class.getClassLoader());
    

    Resource对象,我们可以很容易的将其转换为InputStreamFile对象。

    上面说的方式都是相对于类路径的地址。如果想要指定某个类的相对路径,我们和可以通过指定具体类来定义,比如:

    new ClassPathResource("../../../data/resource-data.txt", Example.class).getFile();
    

    这样就是相对于Example的相对路径了。在实际使用中,不太建议通过类获取其相对路径的文件。这样会将类与文件的相对坐标绑定,如果修改了类的包路径,但是忘记修改文件位置,就会出现错误。而且,大家现在一般是使用 Maven 之类的包管理器管理,可以直接在resources目录中定义配置文件,使用类路径的相对地址加载文件即可。

    通过@Value自动转换

    我们还可以使用@Value直接注入Resource对象,比如:

    @Value("classpath:data/resource-data.txt")
    Resource resourceFile;
    

    @Value还能支持其他的加载协议,比如file:url:

    通过ResourceLoader加载

    我们还能使用注入ResourceLoader来实现资源的懒加载,比如,先注入ResourceLoader实例:

    @Autowired
    ResourceLoader resourceLoader;
    

    然后在我们需要的地方,通过ResourceLoader实例加载资源:

    resourceLoader.getResource("classpath:data/resource-data.txt");
    

    在 Spring 中,ApplicationContext实现了ResourceLoader,所以,我们也可以直接通过ApplicationContext实例加载资源,比如:

    ApplicationContext context;
    
    public Resource loadEmployeesWithApplicationContext() {
        return context.getResource("classpath:data/resource-data.txt");
    }
    

    使用ResourceUtils加载资源

    在 Spring 内部,还提供了一个工具类ResourceUtils,可以很方便的获取类路径中额资源。但是通过这个类的 Javadoc 可以看到,这个类主要是在 Spring 内部使用,也就是说,不太推荐这种用法。不过我们可以了解一下:

    public File loadEmployeesWithSpringInternalClass() 
      throws FileNotFoundException {
        return ResourceUtils.getFile(
          "classpath:data/resource-data.txt");
    }
    

    我们可以了解其内部实现,但是还是建议使用其他更加标准的做法。

    读取资源中的内容

    上面都是通过各种方式获取了Resource资源,接下来我们就聊聊如果获取资源对象的数据。

    比如,我们的资源文件内容如下:

    站点:https://www.howardliu.cn
    作者:看山
    公号:看山的小屋 (kanshanshuo)
    关注公号得福利:不知道多少 G 的资料(电子书、视频等等)
    

    通过File对象读取

    我们可以使用getFile方法得到File实例,就可以用读取File对象的方式读取文件了,比如:

    @Test
    File resource = new ClassPathResource("data/resource-data.txt").getFile();
    String content = new String(Files.readAllBytes(resource.toPath()));
    

    但是这种方式不适用与读取 jar 包中的文件,鉴于现在很多应用都是通过 FatJar 方式部署,我们还需要找找其他方式。

    通过InputStream对象读取

    File对象不适合 jar 包中资源读取的原因在于文件路径格式不一样,所以我们可以直接将资源内容读取为流的形式,这样就没有文件路径的问题了。比如:

    InputStream resource = new ClassPathResource("data/resource-data.txt").getInputStream();
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource))) {
        String content = reader.lines().collect(Collectors.joining("\n"));
    }
    

    读取完毕。

    文末总结

    本文从加载资源、读取内容两个方法讲解,给出了多种读取方式。

    《SpringBoot 实战》是一个系列,我会通过实战+原理的方式完整整个专栏,可以关注公众号「看山的小屋」回复 spring 获取源码。

    推荐阅读


    你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

    个人主页:https://www.howardliu.cn
    个人博文:SpringBoot 实战:加载和读取资源文件
    CSDN 主页:https://kanshan.blog.csdn.net/
    CSDN 博文:SpringBoot 实战:加载和读取资源文件内容

    👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻
    展开全文
  • Yarn资源请求处理和资源分配原理解析

    万次阅读 多人点赞 2017-12-14 09:57:31
    FairScheduler的资源调度原理代码 FairScheduler的调度概览 两种调度时机-心跳调度持续调度 开始进行资源调度 判断这个application是否适合在这个节点上分配资源运行 YARN请求资源时的localityrelaxility限定 ...

    目录

    概述

    在我的上一篇《Yarn FairScheduler 的资源预留机制导致集群停止服务事故分析》中介绍了我们由12台服务器、每台资源容量(100G,32 vCores)组成的yarn集群由于资源预留导致宕机的一次事故。资源预留是Yarn进行资源分配过程中为了让大的应用不至于被小的饿死而进行资源预定的分配方式。本文就从原理和代码层面详细介绍Yarn的资源分配流程。

    下图是我们向yarn提交任务到最后任务以一个一个container的形式运行起来的大致过程。我们通过客户端向Yarn提交计算任务,Yarn会首先为我们的任务生成一个ApplicationMaster,AM负责对整个应用的资源进行管理,包括资源的申请、释放、任务中container的调度和运行。 关于ApplicationMaster与ResourceManager之间进行通信的详细机制,大家可以参考我的博客《YARN ApplicationMaster与ResourceManager之间基于applicationmaster_protocol.proto协议的allocate()接口源码解析》 ,本文不再重复讲解。本文将讲解的资源调度,就是ApplicationMaster向ResourceManager请求资源以后,Yarn的ResourceManager委托我们所配置的调度器(FairScheduler/CapacityScheduler)决定是否分配资源、分配多少资源的过程。当然,ApplicationMaster本身也是运行在一个container中的,也是进行调度的。下文将进行讲解。

    RM分配Container的流程

    FairScheduler的资源调度原理和代码

    FairScheduler的调度概览

    我们所配置的调度器FairScheduler是运行在ResourceManager内的调度算法。Hadoop的官方文档详细介绍了FairScheduler的使用方法。

    FairScheduler的资源队列之间存在层级关系,树的根节点的代表了整个集群的资源,根节点的名字叫做root。我们定义Yarn的资源队列,就是在root下面定义其子节点,比如,root.queue1、root.queue2等等,即,实际上,资源队列之间形成了资源队列树。FSParentQueue对象代表了树中的非叶子节点,FSLeafQueue代表了树的叶子节点。由于我们提交的任何一个应用都需要运行在某个队列中,因此,叶子节点下面还挂载了正在该队列上运行的应用,Yarn使用FSAppAttempt来作为一个运行时的应用在ResourceManager端的抽象,因此,这些FSAppAttempt对象都挂载在对应的叶子节点下面。

    使用树恰当地表达对集群资源进行划分时所需要的隔离关系和层级关系。比如,我们一个集群资源给公司的所有部门提供计算服务,部门与部门之间有可能是有平级部门的兄弟关系,平级部门之间的资源需要相互隔离,也有可能是上下级部门的关系,上级部门有权利使用下级任何部门的资源。因此,使用树来抽象资源,既可以实现平级部门之间的资源隔离,也能够表达上下级部门之间资源的包含关系。

    资源队列树

    上图就是这个资源树的示意图。FairScheduler通过这样一棵资源树,维护了整个集群的资源使用情况。资源树中的每一个叶子节点都挂载了此时正在这个队列上运行、或者正在申请运行的应用。FairScheduler只需要对这个树进行适当遍历,就可以知道任何一个资源队列当前有哪些应用在运行、队列当前还剩下多少资源、已经使用多少资源等。

    通过这样一个资源树,资源调度的过程就变成了这样一个不断进行的过程:从树中取出一个资源请求,如果这个资源请求不违背队列的最大资源量等限制,也能够在某个服务器上运行(这个服务器剩余资源可供运行这个请求的资源),那么,就让这个请求运行在对应的服务器上,即创建对应的container。实际上,分配了这个container以后,会在ApplicationMaster某一次心跳响应中返回这个分配结果,ApplicationMaster知道资源分配成功,就与对应的NodeManager通信,请求该NodeManager将对应的Container(比如运行Mapper或者Reducer的Container或者运行Spark executor 的Container)在其节点上启动,NodeManager收到请求,如果验证通过,则启动对应的Container。

    那么,一次调度的发生是如何被触发的呢?这就涉及到两种调度时机,即心跳调度和持续调度。

    两种调度时机-心跳调度和持续调度

    心跳调度是最早的yarn的调度方式,在2.3版本以前的yarn只支持心跳调度。Yarn的NodeManager会通过心跳的方式定期向ResourceManager汇报自身状态,当NodeManager向ResourceManager汇报了自身资源情况(比如,当前可用资源,正在使用的资源,已经释放的资源),这个RPC会触发ResourceManager调用nodeUpdate()方法,这个方法为这个节点进行一次资源调度,即,从维护的Queue中取出合适的应用的资源请求(合适 ,指的是这个资源请求既不违背队列的最大资源使用限制,也不违背这个NodeManager的剩余资源量限制)放到这个NodeManager上运行。这种调度方式一个主要缺点就是调度缓慢,当一个NodeManager即使已经有了剩余资源,调度也只能在心跳发送以后才会进行,不够及时

    YARN-1010中引入了连续资源调度机制,不用等待NodeManager向ResourceManager发送心跳才进行任务调度,而是由一个独立的线程进行实时的资源分配等调度,与NodeManager的心跳出发的调度相互异步并行进行。当心跳到来,只需要把调度结果通过心跳响应告诉对应的NodeManager即可。

    我们通过yarn.scheduler.fair.continuous-scheduling-enabled来配置是否打开连续调度功能。默认情况下该功能关闭。

      /**
        ContinuousSchedulingThread负责进行持续的资源调度,与NodeManager的心跳产生的调度同时进行
       */
      private class ContinuousSchedulingThread extends Thread {
        @Override
        public void run() {
          while (!Thread.currentThread().isInterrupted()) {
            try {
              continuousSchedulingAttempt();
              Thread.sleep(getContinuousSchedulingSleepMs());//睡眠很短的一段时间进行下一轮调度
            } catch (InterruptedException e) {
              //略
            }
          }
        }
      }

    FairScheduler.initScheduler()方法中构造了ContinuousSchedulingThread线程对象:

          if (continuousSchedulingEnabled) { 
            // start continuous scheduling thread
            schedulingThread = new ContinuousSchedulingThread();
            schedulingThread.setName("FairSchedulerContinuousScheduling");
            schedulingThread.setDaemon(true);
          }

    FairScheduler.serviceStart()方法中启动了ContinuousSchedulingThread线程。该线程每一轮执行完毕,会通过我们配置的yarn.scheduler.fair.continuous-scheduling-sleep-ms睡眠一段时间,然后进行下一轮调度,默认情况下,这个时间是5ms,这个时间间隔接近实时。而对于心跳调度方式,心跳时间间隔通过yarn.nodemanager.heartbeat.interval-ms配置,默认值 1000ms。我把连续调度的每一次执行叫做一轮,在下文中我们可以看到,每一轮,会遍历当前集群的所有节点,挨个对这个节点进行一次调度,即,取出合适的请求,分配到这个节点上运行。

    这就是持续调度线程的初始化和启动机制 ,其初始化和启动时伴随着我们的FairScheduler调度器的初始化和启动同时进行的,是FairScheduler的一种调度优化机制。

    无论是NodeManager心跳时触发调度,还是通过ContinuousSchedulingThread进行实时、持续触发,他们对某个节点进行一次调度的算法和原理是公用的,都是通过synchronized void attemptScheduling(FSSchedulerNode node)来在某个节点上进行一次调度,方法的参数代表了准备进行资源分配的节点。两种触发机制不同的地方只有两个:

    1. 调度时机:心跳调度仅仅发生在收到了某个NodeManager的心跳信息的情况下,持续调度则不依赖与NodeManager的心跳通信,是连续发生的,当心跳到来,会将调度结果直接返回给NodeManager;
    2. 调度范围:心跳调度机制下,当收到某个节点的心跳,就对这个节点且仅仅对这个节点进行一次调度,即谁的心跳到来就触发对谁的调度,而持续调度的每一轮,是会遍历当前集群的所有节点,每个节点依次进行一次调度,保证一轮下来每一个节点都被公平的调度一次

    开始进行资源调度

    这是连续调度方式的一轮调度开始的入口:

    
    void continuousSchedulingAttempt() throws InterruptedException {
      long start = getClock().getTime();
      List<NodeId> nodeIdList = new ArrayList<NodeId>(nodes.keySet());
      //进行调度以前,先对节点根据剩余资源的多少进行排序,从而让资源更充裕的节点先得到调度
      //这样我们更容易让所有节点的资源能够被均匀分配,而不会因为某些节点总是先被调度所以总是比
      //后调度的节点的资源使用率更高
      synchronized (this) {
        Collections.sort(nodeIdList, nodeAvailableResourceComparator);
      }
      // 遍历所有节点,依次对每一个节点进行一次调度
      for (NodeId nodeId : nodeIdList) {
        //FSSchedulerNode是FairScheduler视角下的一个节点
        FSSchedulerNode node = getFSSchedulerNode(nodeId);
        try {
          //判断
          if (node != null && Resources.fitsIn(minimumAllocation,
              node.getAvailableResource())) {
            attemptScheduling(node);
          }
        } catch (Throwable ex) { //这每次调度过程中如果发生异常,这个异常将被捕获,因此不会影响在其它节点上进行调度
          //异常处理,略
          }
        }
      }
    
      long duration = getClock().getTime() - start;
      fsOpDurations.addContinuousSchedulingRunDuration(duration);
    }

    continuousSchedulingAttempt()会遍历所有节点,依次进行资源调度,这里的调度,就是试图找出一个container请求放到这个服务器上运行。为了让整个集群的资源分配在服务器节点之间能够更均匀,调度以前通过资源比较器对节点按照资源余量从多到少排序,从而让资源更充裕的先被调度,这样做更有利于让所有节点的资源使用量达到均衡,而不至于由于某些节点的序号排在前面而总是被先调度,造成资源调度倾斜

    关于比较器,使用的是NodeAvailableResourceComparator比较器,我们跟踪比较器的代码看到,实际上比较资源的时候只考虑了内存,没有考虑vCores等其它资源。

    对于每一个节点,如果节点的资源剩余量大于yarn.scheduler.minimum-allocation-mb所配置的最小调度量才会对这个节点进行调度。如果满足要求,attemptScheduling(node)就开始针对这个节服务器进行调度了,即,把选出合适的资源请求,分配到这个节点上。上面说过,如果是心跳调度模式,也是通过这个方法对发送心跳的节点进行资源调度的。

    synchronized void attemptScheduling(FSSchedulerNode node) {
      //略
      // Assign new containers...
      // 1. Check for reserved applications
      // 2. Schedule if there are no reservations
      FSAppAttempt reservedAppSchedulable = node.getReservedAppSchedulable();
      if (reservedAppSchedulable != null) { //如果这个节点上已经有reservation
        Priority reservedPriority = node.getReservedContainer().getReservedPriority();
        FSQueue queue = reservedAppSchedulable.getQueue();
        //如果这个节点被这个应用预定,这里就去判断这个应用是不是有能够分配到这个node上到请求,如果有这样到请求,并且,没有超过队列到剩余资源,那么,就可以把这个预定的资源尝试进行分配(有可能分配失败)
          //而如果发现这个应用没有任何一个请求适合在这个节点运行,或者,当前队列的剩余资源已经不够运行这个预留的、还没来得及执行的container,那么这个container就没有再预留的必要了
        if (!reservedAppSchedulable.hasContainerForNode(reservedPriority, node)
            || !fitsInMaxShare(queue,
            node.getReservedContainer().getReservedResource())) {
          //如果这个被预留的container已经不符合运行条件,就没有必要保持预留了,直接取消预留,让出资源
          reservedAppSchedulable.unreserve(reservedPriority, node);
          reservedAppSchedulable = null;
        } else {
          //对这个已经进行了reservation对节点进行节点分配,当然,有可能资源还是不足,因此还将处于预定状态
          node.getReservedAppSchedulable().assignReservedContainer(node);
        }
      }
      if (reservedAppSchedulable == null) {这个节点还没有进行reservation,则尝试进行assignment
        // No reservation, schedule at queue which is farthest below fair share
        int assignedContainers = 0;
        while (node.getReservedContainer() == null) { //如果这个节点没有进行reservation,那么,就尝试
          boolean assignedContainer = false;
          if (!queueMgr.getRootQueue().assignContainer(node).equals(
              Resources.none())) { //尝试进行container的分配,并判断是否完全没有分配到并且也没有reserve成功
            assignedContainers++; //如果分配到了资源,或者预留到了资源,总之不是none
            assignedContainer = true;
          }
          if (!assignedContainer) { break; }
          if (!assignMultiple) { break; }
          if ((assignedContainers >= maxAssign) && (maxAssign > 0)) { break; }
        }
      }
      updateRootQueueMetrics();
    }

    在对这个节点进行调度的时候,会先判断这个节点是不是一个被预留的节点,预留和非预留的节点的处理方式是不同的,具体流程是:

    • 如果这个服务器是一个被某个container预留的服务器,那么,需要对这种状态进行处理,以选择(1)剥夺预留、(2)保持预留状态或者(3)将预留状态转换成分配状态:
      • 剥夺预留: 如果发现这个被预留的应用没有任何一个请求适合在这个节点上运行,另外,由于每一个应用都有所在的资源队列,因此如果我们发现资源队列的剩余资源已经小于这个应用的预留资源,那么,这个预留已经没有存在的必要了,需要取消预留权;从这里我们可以看到,如果一个container在这个节点上是预留状态,说明队列的剩余资源肯定满足container的资源需求,只是服务器剩余资源无法运行这个container;
      • 尝试分配资源:如果我们发现这个在这个节点预留资源的应用的确可以在这个节点运行,并且,预留的资源也的确在队列剩余资源的允许范围内,即,还有预留的必要性,那么,就可以尝试进行一次资源分配了,当然,这个资源分配有可能还是失败,如果失败就已然保持预留状态,这个为预留的container进行 资源分配尝试 的过程是在FSAppAttempt.assignReservedContainer()中进行的;
    • 如果这个节点是一个正常未预留的节点,那么就可以进行正常的资源分配了。

    判断这个application是否适合在这个节点上分配资源运行

    在判断是进行预留权剥夺还是资源分配尝试的时候,需要判断这个预留资源的应用是否有任何一个请求适合在这个节点上运行,以及队列剩余资源是否允许这个预留的container的存在。在这里有必要详细解释什么叫做适合在这个节点上运行,这是通过

    这是通过hasContainerForNode()方法进行判断的:

      /**
       * Whether this app has containers requests that could be satisfied on the
       * given node, if the node had full space.
       * 关于这个方法的判断条件,为什么如果anyRequest==null就直接返回false,这是因为applicationMaster在
       * 为应用申请资源的时候,如果是NODE_LOCAL,顺便也会创建这个节点对应的RACK的RACK_LOCAL的请求和offswitch的请求
       * 这个可以看MRAppMaster发送请求的时候所使用的RMContainerRequestor.addContainerReq()和ApplicationMaster通过
       * 调用AMRMClientImpl.addContainerRequest()申请资源的过程
       * 或者查看董西成的博客http://dongxicheng.org/mapreduce-nextgen/yarnmrv2-mrappmaster-containerallocator/
       */
      public boolean hasContainerForNode(Priority prio, FSSchedulerNode node) {
        //查找这个优先级下面目前三种请求,一种是没有任何本地化限制的请求,一种是限制为本地机架的请求,一种是限制为本节点内的请求
        ResourceRequest anyRequest = getResourceRequest(prio, ResourceRequest.ANY); //所有请求,对机架和节点没有要求
        ResourceRequest rackRequest = getResourceRequest(prio, node.getRackName());//在这个节点所在机架上的请求
        ResourceRequest nodeRequest = getResourceRequest(prio, node.getNodeName());  //在这个节点上的请求
    
        return
            // There must be outstanding requests at the given priority:
            anyRequest != null && anyRequest.getNumContainers() > 0 &&
                // If locality relaxation is turned off at *-level, there must be a
                // non-zero request for the node's rack:
                (anyRequest.getRelaxLocality() ||
                    (rackRequest != null && rackRequest.getNumContainers() > 0)) &&
                // If locality relaxation is turned off at rack-level, there must be a
                // non-zero request at the node:
                (rackRequest == null || rackRequest.getRelaxLocality() ||
                    (nodeRequest != null && nodeRequest.getNumContainers() > 0)) &&
                // The requested container must be able to fit on the node:
                Resources.lessThanOrEqual(RESOURCE_CALCULATOR, null,
                    anyRequest.getCapability(), node.getRMNode().getTotalCapability());
      }

    YARN请求资源时的locality和relaxility限定

    ApplicationMaster客户端向ResourceManager资源申请,会把自己的资源需求构造为一个ResourceRequest对象发送给RM。一个ResourceRequest包含了若干个相同Capability的container的请求的集合,包含以下元素:

    • Capability: 单个Container的资源量,由<mem,vCore>组合构成,是一个Resource类的实现;

    • Containers Number : Container的数量,整个请求的资源量为 container number * capability

    • Resource Name:资源名称(某个节点的ip、某个机架的ip或者是*代表任意),这里叫做名称有些让人难以理解,其实是限制这个ResourceRequest对象资源运行的locality,这里有必要非常具体的讲解YARN的locality。ApplicationMaster客户端在提交应用的时候,有时候会对container运行的位置提出限制,比如,由于某些数据在服务器node1上,因此ApplicationMaster客户端希望用来处理这些数据的container就运行在这个服务器上,这个要求运行在某个特定节点的本地化叫做NODE_LOCAL,也可以,要求运行在某个固定的机架rack1上,这种机架的本地化叫做RACK_LOCAL,或者,也许没有要求(OFF_SWITCH)。因此,Resource Name可以是某个节点的ip(NODE_LOCAL),或者某个机架的ip(RACK_LOCAL),或者是通配符*(OFF_SWITCH);

      本地化分为三种,定义在NodeType中:

      public enum NodeType {
      NODE_LOCAL(0), //请求规定了必须运行在某个的服务器节点
      RACK_LOCAL(1), //请求规定了必须运行在某个机架上
      OFF_SWITCH(2); //这个请求对本地化没有要求
      public int index;
      
      private NodeType(int index) {
        this.index = index;
      }
      }

    • relaxLocality:是否允许某种本地化限制松弛到更低的要求,比如,当某个ResourceRequest要求其container运行在node1,但是node1的资源剩余量始终无法满足要求,那么需要进行本地化松弛,可以放弃必须运行在服务器node1的要求,改为只要求运行在这个服务器node1所在的机架rack1上就行。

      我们用一个例子来讲解请求一个NODE_LOCAL或者RACK_LOCAL是怎样进行的。

      如果某一个Container的运行有本地化需求,比如,这个Container要求只在node1上运行,那么,它其实会为这个Container创建多个不同的locality的请求:

      资源名称         内存         cpu     松弛度
      <“node1”,      “memory:1G”, 1,      true>  //发送resourcename为node1的请求,relaxLocality=true
      <“rack1”,      “memory:1G”, 1,      false/true> //同时发送node1所在的rack的请求,是否松弛relaxLocality的值
      <“*”,          “memory:1G”, 1,      false/true>  //同时发送off-switch请求,是否松弛relaxLocality的值

      如果这个请求值允许在node1上运行,那么relaxLocality==false,即rack1和off-switch的请求中的松弛变量都是false,那么,当node1请求无法满足,RM试图将请求降级到rack1或者off-switch的时候,检查他们的relaxLocality,发现是false,降级不被允许,只能继续等待直到node1满足条件,或者node1始终无法分配资源,资源分配失败。

    总之,ApplicationMaster 在请求container的时候,如果本地化需求是NODE_LOCAL或者RACK_LOCAL,都会为一个container同时发送多个不同的resource name的请求,RM最终只会选择一个请求进行执行,并且,尽量满足NODE_LOCAL,如果不满足就只能RACK_LOCAL,最后只能运行OFF_SWITCH,总之根据可用资源的情况和请求中设置的允许的松弛程度决定是否分配资源。

    在这里,我们可以深刻体会到ApplicationMaster的api是多么复杂

    有了对资源本地化(locality)和本地化松弛(relaxility)的理解,我们就可以看懂hasContainerForNode() 方法用来判断这个应用的资源请求当前是否可以运行在这台服务器上了:

    1. 如果连OFF_SWITCH的请求都没有,那么这个应用肯定不需要运行了。我们在上面说过,无论是NODE_LOCAL、RACK_LOCAL抑或本来就是OFF_SWITCH的请求,都会发送OFF_SWITCH的请求,因此,如果发现一个应用连OFF_SWITCH的请求都没有,就没有必要再考虑其他的了。

    2. 如果OFF_SWITCH的relaxLocality是打开的(允许松弛到OFF_SWITCH的级别),或者虽然是关闭的(不允许松弛到OFF_SWITCH)但是有RACK_LOCAL的请求存在

    3. 如果RACK_LOCAL级别的relaxLocality是打开的(允许松弛到RACK_LOCAL级别),或者,虽然RACK_LOCAL级别的relaxLocality是关闭的(不允许松弛到RACK_LOCAL),但是却有NODE_LOCAL级别的请求

    4. 这个资源请求能够在这个节点上运行,即节点剩余资源足够运行这个请求

    如果以上条件都满足,hasContainerForNode()返回true,则说明这个在这个节点上进行预订的app或许可以从预定状态变成分配状态了,因此,尝试对这个预定进行分配。而如果hasContainerForNode()返回false,说明这个预定的container实际上不可以在这个服务器上运行,因此没有必要继续空占资源,防止资源被长期无效占用。尝试将app在节点上预定的资源进行allocate的过程,是调用的FSAppAttempt.assignReservedContainer()方法,其实最终也是调用private Resource assignContainer(FSSchedulerNode node, boolean reserved)方法,尝试将这个container在这个节点上进行分配,后面一个参数标记了这个节点是否被预定了。

    可见,资源分配的关键方法,就是assignContainer()方法。在进行container分配的过程中,会发生分配成功、或者将资源请求转变为预留状态的过程。我们将具体讲解assignContainer()方法。

    资源分配:assignContainer()

    我在资源调度实现机制概览介绍了Yarn 对队列配置的树形结构。树的每一个节点(注意这里的节点指的是树的节点,不是服务器节点)都是一个资源的抽象,比如,这个节点代表的最大资源、已使用资源、空闲未使用资源。

    树的非叶子节点(FSParentQueue)和叶子节点(FSLeafQueue)都是资源集合,代表了一定的资源,不同的是,非叶子节点(FSParentQueue)只是用来代表他所管理的叶子节点的集合,叶子节点是应用具体运行的队列,因此叶子节点上挂载了正在它上面运行的应用的集合。FSParentQueue和FSLeafQueue都实现了assignContainer()方法,用来在这个队列上进行资源分配。每一次调度,都是从root节点开始,通过递归方式,调用这个节点的assignContainer()方法,尝试为挂载的请求进行一次分配,一旦分配成功则退出递归。我们分别来看FSParentQueue和FSLeafQueue对assignContainer()方法的实现。

    下图是进行一轮资源分配(只有连续调度的一轮分配会对所有服务器挨个进行一次分配,心跳调度只对心跳节点进行一次分配,但是原理相同)的概图。从图中可以看到,为某一个节点进行资源分配,就是对资源树进行递归搜索,依次从资源树最上层的root节点、到普通的非叶子节点、再到叶子节点、然后是应用、最后是应用的某个资源请求,然后将选出的请求分配到对应的服务器上的过程。

    Yarn资源分配概图

    下面,我们就依次从代码角度讲解这个资源分配过程。

    Parent节点调用FSParentQueue.assignContainer()方法进行资源分配

    我在介绍连续调度和心跳调度 的时候,讲到Yarn是遍历所有的服务器,然后对于每一个服务器,对资源树进行一次递归搜索,选出请求在这个服务器上执行。

    这是FSParentQueue.assignContainer()方法:

      public Resource assignContainer(FSSchedulerNode node) {
        Resource assigned = Resources.none();
    
        // If this queue is over its limit, reject
        if (!assignContainerPreCheck(node)) {
          return assigned;
        }
    
        Collections.sort(childQueues, policy.getComparator());
        for (FSQueue child : childQueues) { //从这个for循环可以看出来这是广度优先遍历
          assigned = child.assignContainer(node); //childQueus有可能是FSParentQueue或者FSLeafQueue
          if (!Resources.equals(assigned, Resources.none())) { //如果成功分配到了资源,那么,没有必要再去兄弟节点上进行资源分配了
            break;
          }
        }
        return assigned;
      }

    我们从代码中可以看出,FSParentQueue其实并没有进行实际的资源分配,只是不断遍历子节点直到遇到叶子节点才会尝试进行分配,因此资源分配代码实际上是在FSChildQueue.assignContainer()中执行的。

    同时,我们必须看到FSParentQueue.assignContainer()中对FSParentQueue的子节点的遍历并不是随机排序的,而是通过对应的policy定义了排序规则:

     Collections.sort(childQueues, policy.getComparator());

    这里的Policy就是FairScheduler的官方文档上介绍的不同的Policy,对于FSParentQueue.assignContainer()中使用Policy进行子队列的排序,决定了对某个父节点的多个子节点进行资源分配的顺序。我在我的另外一篇文章将会介绍不同的Policy,在这里我们忽略策略问题。

    同时,我们从for循环的退出规则可以看出,递归遍历过程中,一旦分配成功,for循环即退出,这轮分配结束,代码退出到continuousSchedulingAttempt()的for循环处,开始对下一个节点的剩余资源进行尝试分配。即,一轮分配中,对某个服务器,只会进行一个请求的分配。这样也是为了达到平均分配的结果,避免多个请求都堆积分配到某一个服务器而其它服务器却空闲的情况。

    Leaf节点通过调用FSLeafQueue.assignContainer()方法进行资源分配

    通过从root(FSParentQueue)节点递归调用assignContainer(),最终将到达最终叶子节点的assignContainer()方法,才真正开始进行分配:

    /**
     * 尽量将自己的app分配到某个节点上,FSLeafQueue会遍历自己目前所有的runnableApps,然后逐个尝试进行分配,只要有一个分配成功就退出
     * @param node 等待进行container分配的节点
     * @return
     */
    @Override
    public Resource assignContainer(FSSchedulerNode node) {
      Resource assigned = Resources.none();
      if (!assignContainerPreCheck(node)) {
        return assigned;
      }
      Comparator<Schedulable> comparator = policy.getComparator();//根据对应的policy提供的排序器对apps进行排序
      writeLock.lock();
      try {
        Collections.sort(runnableApps, comparator);
      } finally {
        writeLock.unlock();
      }
      // Release write lock here for better performance and avoiding deadlocks.
      // runnableApps can be in unsorted state because of this section,
      // but we can accept it in practice since the probability is low.
      readLock.lock();
      try {
        for (FSAppAttempt sched : runnableApps) { //对排序完成的资源一次进行调度,即对他们进行资源分配尝试
          if (SchedulerAppUtils.isBlacklisted(sched, node, LOG)) {
            continue;
          }
    
          assigned = sched.assignContainer(node); //这里的sched应该是FSAppAttempt
          if (!assigned.equals(Resources.none())) { //如果发现进行了资源分配,即,不管是进行了预留,还是进行了实际的分配,都跳出循环
            break;
          }
        }
      } finally {
        readLock.unlock();
      }
      return assigned;
    }

    在进行分配以前,通过调用assignContainerPreCheck(node)判断能否在当前这个leaf queue上往这个节点进行资源分配,即,如果这个队列的已使用资源小于这个队列的最大可使用资源(还有剩余资源)并且这个节点没有被预定,那么才可以继续往下走进行分配:

      /**
       * 判断当前的这个Queue是否能够进行一次资源分配,即,
       * 如果这个队列已经使用的资源小于最大资源并且这不是一个被预定的节点才能进行分配,否则,不可以再分配container了
       */
      protected boolean assignContainerPreCheck(FSSchedulerNode node) {
        if (!Resources.fitsIn(getResourceUsage(),
            scheduler.getAllocationConfiguration().getMaxResources(getName()))
            || node.getReservedContainer() != null) {
          return false;
        }
        return true;
      }
    
     public static boolean fitsIn(Resource smaller, Resource bigger) {
        return smaller.getMemory() <= bigger.getMemory() &&
            smaller.getVirtualCores() <= bigger.getVirtualCores();
      }

    同样,我们看到,和FSParentQueue.assignContainer()中对挂载的子节点进行排序一样,使用Policy对挂载的FSAppAttempt进行了排序。显然,根据不同的Policy定义的比较器,决定了对Application进行资源分配的先后顺序。

    在完成了队列资源检查并对这个叶子节点下面挂载的应用进行了排序,就开始遍历这些应用并尝试将某个应用的请求分配到当前的这个yarn服务器节点。同样,我们从for循环可以看到,一旦有成功分配,循环立刻退出。这种限制一轮分配最多只进行一次成功分配,是为了请求被均匀分配到服务器节点,以及每个队列都得到及时的请求分配,而不至于在某一轮分配中将请求全部分配到某个节点,或者,某个资源队列进行了多次调度,其它队列却一次都没有被调度。

    这样,FSLeafQueue.assignContainer()最终通过调用具体的Application的assignContainer()方法实现调度。在RM端,每个Application使用FSAppAttemt对象来表示。

    应用通过调用FSAppAttemt.assignContainer()方法进行资源分配

    这是FSAppAttemt.assignContainer()方法:

    private Resource assignContainer(FSSchedulerNode node, boolean reserved) {
    
     //返回一个基于Priority进行排序的Priority集合
      Collection<Priority> prioritiesToTry = (reserved) ?
          Arrays.asList(node.getReservedContainer().getReservedPriority()) :
          getPriorities();
    
      // For each priority, see if we can schedule a node local, rack local
      // or off-switch request. Rack of off-switch requests may be delayed
      // (not scheduled) in order to promote better locality.
      synchronized (this) {
        for (Priority priority : prioritiesToTry) {
          if (getTotalRequiredResources(priority) <= 0 ||
              !hasContainerForNode(priority, node)) {
            continue;
          }
    
          addSchedulingOpportunity(priority);
    
          // Check the AM resource usage for the leaf queue
          if (getLiveContainers().size() == 0 && !getUnmanagedAM()) {
            if (!getQueue().canRunAppAM(getAMResource())) {
              return Resources.none();
            }
          }
    
          ResourceRequest rackLocalRequest = getResourceRequest(priority,
              node.getRackName());
          ResourceRequest localRequest = getResourceRequest(priority,
              node.getNodeName());
    
          if (localRequest != null && !localRequest.getRelaxLocality()) {
            LOG.warn("Relax locality off is not supported on local request: "
                + localRequest);
          }
    
          NodeType allowedLocality;
          if (scheduler.isContinuousSchedulingEnabled()) { //如果使用的是持续调度,那么需要根据当前的时间确认当前对这个priority可以采取的本地化水平
            allowedLocality = getAllowedLocalityLevelByTime(priority,
                scheduler.getNodeLocalityDelayMs(),
                scheduler.getRackLocalityDelayMs(),
                scheduler.getClock().getTime()); //根据时间去决定允许的本地策略是NODE_LOCAL/RACK_LOCAL/OFF_SWITCH
          } else {
            allowedLocality = getAllowedLocalityLevel(priority,
                scheduler.getNumClusterNodes(),
                scheduler.getNodeLocalityThreshold(), //yarn.scheduler.fair.locality.threshold.node ,这是一个从0到1之间的小数,代表,我必须经过多少次失败的调度,才能允许将本地化策略降级到RACK_LOCAL
                scheduler.getRackLocalityThreshold());//yarn.scheduler.fair.locality.threshold.node,这是一个从0到1之间的小数,代表,我必须经过多少次失败的调度,才能允许将本地化策略降级到OFF_SWITCH
          }
    
          if (rackLocalRequest != null && rackLocalRequest.getNumContainers() != 0
              && localRequest != null && localRequest.getNumContainers() != 0) { //如果NODE_LOCAL/RACK_LOCAL都不是空的,那么进行NODE_LOCAL级别的调度
            return assignContainer(node, localRequest,
                NodeType.NODE_LOCAL, reserved);
          }
    
          if (rackLocalRequest != null && !rackLocalRequest.getRelaxLocality()) {
            continue;
          }
    
          if (rackLocalRequest != null && rackLocalRequest.getNumContainers() != 0
              && (allowedLocality.equals(NodeType.RACK_LOCAL) ||
              allowedLocality.equals(NodeType.OFF_SWITCH))) { //如果RACK_LOCAL的请求不是空的并且允许的本地化策略是RACK_LOCAL/OFF_SWITCH,则进行RACK_LOCAL调度
            return assignContainer(node, rackLocalRequest,
                NodeType.RACK_LOCAL, reserved);
          }
    
          ResourceRequest offSwitchRequest =
              getResourceRequest(priority, ResourceRequest.ANY); //否则,进行OFF_SWITCH调度
          if (offSwitchRequest != null && !offSwitchRequest.getRelaxLocality()) {
            continue;
          }
    
          if (offSwitchRequest != null &&
              offSwitchRequest.getNumContainers() != 0) {
            if (!hasNodeOrRackLocalRequests(priority) ||
                allowedLocality.equals(NodeType.OFF_SWITCH)) {
              return assignContainer(
                  node, offSwitchRequest, NodeType.OFF_SWITCH, reserved); //进行OFF_SWITCH调度
            }
          }
        }
      }
      return Resources.none();
    }

    根据Priority优先级排序以及资源请求的Priority简介

    每一个FSAppAttempt保存了自己当前所有container请求的Priority的list,通过调用getPriorities()获取了按照Priority(优先级)排序的container请求的list,我们来看这个对Priority进行排序的比较器:

      //一个排序的HashSet,用来保存当前这个applicaiton的所有请求的优先级
      final Set<Priority> priorities = new TreeSet<Priority>(
          new org.apache.hadoop.yarn.server.resourcemanager.resource.Priority.Comparator());
    
    public static class Comparator 
      implements java.util.Comparator<org.apache.hadoop.yarn.api.records.Priority> {
        @Override
        public int compare(org.apache.hadoop.yarn.api.records.Priority o1, org.apache.hadoop.yarn.api.records.Priority o2) {
          return o1.getPriority() - o2.getPriority(); //从比较器来看,值越小优先级越高
        }
      }

    这些container请求的Priority是不同类型的应用的ApplicationMaster自己决定的。以MapReduce为例,MapReduce的任务的ApplicationMaster实现类是MRAppMaster,它委托RMContainerAllocator向远程的ApplicationMasterService请求资源,MR的任务类型包括Map任务、Reduce任务和Fail Map任务(失败需要重试的map),我们看在RMContainerAllocator中对这三种类型的任务的优先级定义:

    static final Priority PRIORITY_FAST_FAIL_MAP;
    static final Priority PRIORITY_REDUCE;
    static final Priority PRIORITY_MAP;
    //略
    static {
      PRIORITY_FAST_FAIL_MAP = RecordFactoryProvider.getRecordFactory(null).newRecordInstance(Priority.class);
      PRIORITY_FAST_FAIL_MAP.setPriority(5);
      PRIORITY_REDUCE = RecordFactoryProvider.getRecordFactory(null).newRecordInstance(Priority.class);
      PRIORITY_REDUCE.setPriority(10);
      PRIORITY_MAP = RecordFactoryProvider.getRecordFactory(null).newRecordInstance(Priority.class);
      PRIORITY_MAP.setPriority(20);
    }

    可以看到,FAST_FAIL_MAP任务、REDUCE任务和MAP任务的Priority分别是5,10,15,数字越小,优先级越高,因此在FSAppAttempt.assignContainer()中这个请求就会优先被考虑,即,如果有失败的Map任务,这个失败的Map任务优先执行,其次是Reduce任务,最后是正常的Map任务。实际运行情况下,MR任务总是先产生map任务,因此先只提交map任务运行,然后如果有reduce任务,reduce任务将优先于现有的map任务运行,而失败的map任务由于需要重试,其它reduce任务可能在等待这个失败的map任务执行完才能进入下一个阶段,因此它的优先级最高。

    获得排序后的Priority,就可以按照优先级,遍历这个Priority的list,取出对应Priority的container请求,尝试进行资源分配。

    分配前的校验

    对于某一个priority的请求,在进行资源分配之前,会进行以下检查:

    • 确认这个priority的确有请求存在,因为FSAppAttemtp除了保存所有请求的Priority的list,还保存了每一个Priority到请求的对应关系,即Priority到ResourceRequest的对应关系。我在 YARN请求资源时的locality和reality限定 这一节讲过RequestRequest对象的结构;

    • 再次确认这个priority对应的请求的确适合在这个节点上运行,同样是调用hasContainerForNode()方法进行判断了,我在判断这个application是否适合在这个节点上分配资源运行 这一节专门讲过请求的本地化特性以及hasContainerForNode()的判断机制;

    • 如果当前准备分配的container是ApplicationMaster的container,那么这个container的分配需要判断是否满足maxAMShare的配置限制;maxAMShare是FairScheduler用来限制一个队列的资源最多可用来运行ApplicationMaster而不是具体job的比例。我们具体来看看这一部分代码:

      //如果liveContainer==0,并且这个Application的AM是一个managedAM,那么在分配container的时候必须考虑
      //是否超过了当前队列的maAMShare配置的最大AM比例值
      if (getLiveContainers().size() == 0 && !getUnmanagedAM()) {
      if (!getQueue().canRunAppAM(getAMResource())) {
        return Resources.none();
      }
      }
      
       /*
       * 判断当前队列是否能够运行一个ApplicationMaster应用
       * @param amResource 需要运行的am的资源量
       * @return true if this queue can run
       */
      public boolean canRunAppAM(Resource amResource) {
        float maxAMShare = //获取队列的maxAMShare参数,即队列允许用来运行AM的资源总量
            scheduler.getAllocationConfiguration().getQueueMaxAMShare(getName());
        if (Math.abs(maxAMShare - -1.0f) < 0.0001) { //如果配置的值为-1.0f,则说明没有限制
          return true;
        }
        //计算队列中可以用来运行AM的最大资源量,即,用队列的FairShare * maxAMShare,这里的fair share指的是instaneous fair share值
        Resource maxAMResource = Resources.multiply(getFairShare(), maxAMShare);
        Resource ifRunAMResource = Resources.add(amResourceUsage, amResource); //计算如果运行了这个am以后这个队列所有的am的资源使用量
        return !policy
            .checkIfAMResourceUsageOverLimit(ifRunAMResource, maxAMResource);  //对于默认的FairSharePolicy,判断如果运行了这个am,是否超过了maxAMShare的限制
      }

      了解一下什么叫做 unmanaged AM:yarn中ApplicationMaster分为两种,一种是unmanaged am,这种ApplicationMaster独立运行在yarn之外,虽然需要与Yarn RM进行通信和进行资源申请,但是其本身运行所需要的资源不是yarn分配的;另外一种叫做managed AM,即客户端在向yarn提交应用,会先申请ApplicationMaster的启动请求,yarn会分配一个container来运行ApplicationMaster,然后ApplicationMaster会向yarn进行Map/Reduce job的资源申请以及job的管理;

      对于managed am,yarn通过maxAMShare对资源队列中用来运行ApplicationMaster的资源进行了限制,这是为了防止发生资源死锁:如果一个队列中太多的资源都用来运行ApplicationMaster,即队列被很多小的应用充斥,应用都在运行,此时队列资源耗尽,所有ApplicationMaster都申请不到新的container,因此不断等待,新的应用又无法申请新的资源。这段代码就是判断当前的这个container是不是AppliationMaster的container,如果是,则必须满足maxAMShare的资源限制。

    • 在完成了以上校验,即确认可以进行container的分配,就开始进行资源分配。

    判断资源分配的locality并进行资源分配

    yarn需要确认当前这一个分配是进行的NODE_LOCAL/RACK_LOCAL/OFF_SWITCH中的哪种分配。显然,从优先级角度,NODE_LOCAL>RACK_LOCAL>OFF_SWITCH,因为Yarn是最希望能够满足container的NODE_LOCAL请求的,当然,如果不能满足,则只能进行本地化降级,这种降级不是第一次发现无法满足NODE_LOCAL就立刻进行的,而是稍作延迟,如果还是无法按照原本地化标准执行,则对本地化进行降级,因为,NODE_LOCAL现在满足不了,但是也许过一会儿就可以满足了,我们来看:

            ResourceRequest rackLocalRequest = getResourceRequest(priority,
                node.getRackName()); //获取这个应用请求这个节点所在机架的RACK_LOCAL的请求
            ResourceRequest localRequest = getResourceRequest(priority,
                node.getNodeName()); //获取这个应用请求这个节点的NODE_LOCAL的请求
            NodeType allowedLocality;
            if (scheduler.isContinuousSchedulingEnabled()) { //如果使用的是持续调度,那么需要根据当前的时间确认当前对这个priority可以采取的本地化水平
              allowedLocality = getAllowedLocalityLevelByTime(priority,
                  scheduler.getNodeLocalityDelayMs(),
                  scheduler.getRackLocalityDelayMs(),
                  scheduler.getClock().getTime()); //根据时间去决定允许的本地策略是NODE_LOCAL/RACK_LOCAL/OFF_SWITCH
            } else {
              allowedLocality = getAllowedLocalityLevel(priority,
                  scheduler.getNumClusterNodes(),
                  scheduler.getNodeLocalityThreshold(), //yarn.scheduler.fair.locality.threshold.node ,这是一个从01之间的小数,代表,我必须经过多少次失败的调度,才能允许将本地化策略降级到RACK_LOCAL
                  scheduler.getRackLocalityThreshold());//yarn.scheduler.fair.locality.threshold.node,这是一个从0到1之间的小数,代表,我必须经过多少次失败的调度,才能允许将本地化策略降级到OFF_SWITCH
            }

    Yarn会通过调用getAllowedLocalityLevelByTime()或者getAllowedLocalityLevel()判断当前允许的本地化策略,他们分别从时间层面和重试次数层面决定是不是已经经历了足够长时间的等待或者足够多次的等待,如果等待时间够长,或者失败次数哦够多,就只能尝试进行本地化降级了。

    getAllowedLocalityLevelByTime()是用在连续性调度方式的,是从时间层面确定当前运行的本地化,思想是,如果距离上一次Container的分配时间已经超过了阈值,说明这个NODE_LOCAL的调度已经失败了一段时间,因此需要立刻降级:

      /**
       * 返回允许的进行container调度的本地化水平,参数nodeLocalityDelayMs、rackLocalityDelayMs分别代表对NODE_LOCAL和RACK_LOCAL的
       * 进行降级前需要延迟的时间阈值
       */
      public synchronized NodeType getAllowedLocalityLevelByTime(Priority priority,
              long nodeLocalityDelayMs, long rackLocalityDelayMs,
              long currentTimeMs) {
    
        //默认NODE_LOCAL和RACK_LOCAL的延迟调度时间是-1,代表我们的FairSchedulerd调度器
        // if not being used, can schedule anywhere
        if (nodeLocalityDelayMs < 0 || rackLocalityDelayMs < 0) { //如果NODE_LOCAL或者RACK_LOCAL允许off-swtich本地级别的调度,则直接返回OFF_SWITCH的本地级别
          return NodeType.OFF_SWITCH;
        }
    
        //默认的本地级别是NODE_LOCAL
        if (! allowedLocalityLevel.containsKey(priority)) { //
          allowedLocalityLevel.put(priority, NodeType.NODE_LOCAL);
          return NodeType.NODE_LOCAL;
        }
    
        NodeType allowed = allowedLocalityLevel.get(priority); //获取这个优先级运行的LOCAL级别
    
        // if level is already most liberal, we're done
        if (allowed.equals(NodeType.OFF_SWITCH)) { //如果这个LOCAL级别直接允许OFF_SWITCH,那就直接OFF_SWITCH
          return NodeType.OFF_SWITCH;
        }
    
        //如果这个LOCAL级别不允许OFF_SWITCH调度,就需要根据RACK_LOCAL或者NODE_LOCAL以及超时时间进行判断最终是进行NODE_LOCAL、RACK_LOCAL还是OFF_SWITCH的本地级别
        // check waiting time
        long waitTime = currentTimeMs;
        if (lastScheduledContainer.containsKey(priority)) { //如果上一次调度的container还有这个优先级的 , 则用当前时间减去上一个container的调度时间从而获得等待时间
          waitTime -= lastScheduledContainer.get(priority);
        } else {
          waitTime -= getStartTime();//否则,等待时间就是从这个application启动的时间到现在的时间
        }
    
        long thresholdTime = allowed.equals(NodeType.NODE_LOCAL) ?
                nodeLocalityDelayMs : rackLocalityDelayMs; //RACK_LOCAL或者NODE_LOCAL等待的阈值
    
        //如果目前的等待时间已经超过了thresholdTime
        if (waitTime > thresholdTime) { //如果等待时间超过了阈值
          if (allowed.equals(NodeType.NODE_LOCAL)) { //
            allowedLocalityLevel.put(priority, NodeType.RACK_LOCAL);//将NODE_LOCAL降级为RACK_LOCAL
            resetSchedulingOpportunities(priority, currentTimeMs);
          } else if (allowed.equals(NodeType.RACK_LOCAL)) {//将RACK_LOCAL降级为OFF_SWITCH
            allowedLocalityLevel.put(priority, NodeType.OFF_SWITCH);
            resetSchedulingOpportunities(priority, currentTimeMs);
          }
        }
        //如果等待时间还没有超时,那就不会对locality进行降级,原来是什么,现在还是什么
        return allowedLocalityLevel.get(priority);
      }

    getAllowedLocalityLevel()是从重试次数角度去考虑的,用在心跳调度方式,即,如果发现针对这个请求的重试次数已经超过了我们的FairScheduler规定的次数,则没有必要再等待了,直接降级处理:

    /**
     * 给出当前集群的节点数量以及在进行本地化降级前失败的调度的次数阈值(这个阈值是一个比例值,即失败的次数占集群节点规模的比例),
     * 返回允许调度对当前这个Priority的container请求进行调度的locality,
     */
    public synchronized NodeType getAllowedLocalityLevel(Priority priority,
        int numNodes, double nodeLocalityThreshold, double rackLocalityThreshold) {
      // upper limit on threshold
      if (nodeLocalityThreshold > 1.0) { nodeLocalityThreshold = 1.0; }
      if (rackLocalityThreshold > 1.0) { rackLocalityThreshold = 1.0; }
    
      // If delay scheduling is not being used, can schedule anywhere
      if (nodeLocalityThreshold < 0.0 || rackLocalityThreshold < 0.0) { //如果我们没有打开延迟调度策略,那么,直接就用OFF_SWITCH
        return NodeType.OFF_SWITCH;
      }
      //如果已经配置了延迟调度,则根据
    
      // 默认的本地化策略是NODE_LOCAL
      if (!allowedLocalityLevel.containsKey(priority)) { //如果这个优先级目前还没有保存在allowedLocalityLevel,则使用默认的NODE_LOCAL的本地化策略,因为我们总是希望进行NodeLocal的调度
        allowedLocalityLevel.put(priority, NodeType.NODE_LOCAL);
        return NodeType.NODE_LOCAL;
      }
    
      NodeType allowed = allowedLocalityLevel.get(priority); //如果当前优先级已经有了对应的允许的本地策略略,则根据允许的本地策略
    
      // If level is already most liberal, we're done
      if (allowed.equals(NodeType.OFF_SWITCH)) return NodeType.OFF_SWITCH; //乳沟直接就允许OFF_SWITCH,就OFF_SWITCH
    
      double threshold = allowed.equals(NodeType.NODE_LOCAL) ? nodeLocalityThreshold :
        rackLocalityThreshold; //获取允许的本地级别对应的阈值
    
      // Relax locality constraints once we've surpassed threshold.
      if (getSchedulingOpportunities(priority) > (numNodes * threshold)) { //如果超过了阈值,则需要进行本地化降级
        if (allowed.equals(NodeType.NODE_LOCAL)) {
          allowedLocalityLevel.put(priority, NodeType.RACK_LOCAL); //将本地化策略从NODE_LOCAL降级为RACK_LOCAL
          resetSchedulingOpportunities(priority);
        }
        else if (allowed.equals(NodeType.RACK_LOCAL)) {
          allowedLocalityLevel.put(priority, NodeType.OFF_SWITCH); //将本地化策略从RACK_LOCAL降级为OFF_SWITCH
          resetSchedulingOpportunities(priority);
        }
      }
      //如果还没有达到阈值,则该是什么本地化就还是什么本地化
      return allowedLocalityLevel.get(priority);
    }

    由此可以看到,连续调度和心跳调度对于本地化降级的处理思想其实是相同的,都是在降级之前进行适当的延迟。不同之处只是降级时机的判断标准不同

    连续调度的降级时机是在时间层面判断距离上一次成功调度的时间是否已经超过了FairScheduler所规定的阈值,如果是,则可以立刻进行降级了,我们通过这两个配置项控制时间阈值:

    而心跳调度的降级时机是在失败调度的次数层面,即,连续失败的次数是否已经超过了指定的阈值(这个阈值其实是一个比例值,乘以集群节点数,就是允许的失败次数),如果超过了,则需要降级,我们通过这两个配置项控制这个比例阈值:

    在完成了本地化判断以后,就可以开始为container分配资源了,即调用private Resource assignContainer( FSSchedulerNode node, ResourceRequest request, NodeType type, boolean reserved),用来在确认了本地化策略后进行container的资源分配。assignContainer(...)的响应代码虽然比较繁杂,但是难度都不大,因此不做列出。

    下图描述了 assignContainer()方法分配这个container的流程:

    assignContainer()的具体流程

    最后一个参数reserved标记当前是否是在给一个处于reserved状态的container分配资源,如果是一个reserved container,则不需要新创建container,而是直接把这个预留态的container从RMContainerState.RESERVED状态变成RMContainerState.ALLOCATED,而如果不是,则需要新创建container,并直接从RMContainerState.NEW状态变为RMContainerState.ALLOCATED状态,这个我们可以从RMContainerImpl中定义的container的状态机看到:

    
      private static final StateMachineFactory<RMContainerImpl, RMContainerState, 
                                               RMContainerEventType, RMContainerEvent> 
       stateMachineFactory = new StateMachineFactory<RMContainerImpl, 
           RMContainerState, RMContainerEventType, RMContainerEvent>(
          RMContainerState.NEW)
    
        // Transitions from NEW state
        .addTransition(RMContainerState.NEW, RMContainerState.ALLOCATED,
            RMContainerEventType.START, new ContainerStartedTransition())
        //略
        .addTransition(RMContainerState.RESERVED, RMContainerState.ALLOCATED,
            RMContainerEventType.START, new ContainerStartedTransition())
    

    可以看到,如果发生了RMContainerEventType.START事件,处于RMContainerState.NEW或者RMContainerState.RESERVED状态的container都会变成RMContainerState.ALLOCATED状态,同时,这个状态机的hook类是ContainerStartedTransition,会被调用。关于RM端container的状态定义和状态转换关系,大家可以参考这篇博客:《RMContainer状态机分析》

    ContainerStartedTransition判断当前的container是否是AM container,如果是,则需要开始ApplicationMaster的启动过程,并更新整个Appliation Attempt的状态(Appliation Attempt是整个Application的运行时的实例),因为,如果这个container是整个ApplicationMaster的container,那么整个Appliation Attempt的状态就发生变化,比如,如果这个continer是AM的container并且分配成功,那么整个Appliation Attempt的状态将从SCHEDULED变为 ALLOCATED_SAVING状态。关于Application Attempt的状态转换,大家可以参考《RMAppAttempt状态机分析》

    因此,ApplicationMaster Container和普通的执行具体Map/Reduce任务的Container分配以后的处理流程是不同的:

    • 如果是ApplicationMaster的Container被成功分配,这个分配事件最终会注册给ResourceManager的ApplicationMasterLaucher,ApplicationMasterLaucher维护着一个独立的线程,不断检测是否有新的ApplicationMaster的启动事件,如果有,就会通过ContainerManagementProtocol协议与container所在的服务器通信,直接在对应的节点上把这个ApplicationMaster进程运行起来,AppAttempt的状态因此从ALLOCATED变为LAUCHED;ApplicationMaster运行起来以后,开始进行普通的container的请求;

    • 如果是普通的container的请求,是ApplicationMaster向ResourceManager请求资源,ResourceManager会分配对应的Container并将分配结果告知ApplicationMaster,ApplicationMaster会和对应的NodeManager通信,启动这个container,这个通信也是基于ContainerManagementProtocol协议进行的。

    RM分配Container的流程

    如果这个启动的container是普通的container,那么只是会更新RM端每个应用的container信息等等。下一次收到了ApplicationMaster的心跳,就会把这些新分配的container信息返回给ApplicationMaster,ApplicationMaster接着会在对应的NodeManager上启动这些container。

    从上图中还可以看到,如果这个container请求满足队列剩余资源,即队列剩余可用资源大于container需要的资源,但是,这个服务器节点的剩余资源却不足以运行这个container,就需要进行资源预留。关于资源预留,大家可以参考我专门讲解资源预留机制的博客:《Yarn FairScheduler 的资源预留机制导致集群停止服务事故分析》

    总结

    Yarn通过资源树的形式来对整个集群的资源进行横向划分和纵向层级划分,很好地表达了企业应用中需要表达的部门之间的资源隔离以及上下级部门之间的资源分层逻辑。

    Yarn的资源调度不是阻塞式的,即,不是ApplicationMaster发起资源请求、Yarn处理请求然后进行资源分配然后将分配结果通过本次响应返回给ApplicationMaster。每次ApplicationMaster发起请求,Yarn会把这些请求挂载到上文提到的资源队列树中。然后,通过连续调度方式,以一个独立的线程,每隔一段时间,对集群中所有的服务器进行一轮调度,或者,某个NodeManager的心跳信息到来以后触发对这个心跳服务器的资源分配。然后,当收到某个AppliationMaster的资源请求以后,就将当前已经进行了成功分配的分配结果通过这次请求的Response返回给AM,显然,这次返回的分配成功的container完全可能是上一次所请求的资源的container。

    由此可见,AM和RM之前的资源请求通信,是一种心跳式的通信,AM的心跳定时发出,如果有新的请求,心跳就携带这些请求,否则不携带任何请求,如果有新的分配结果,心跳的响应就会携带回这些结果,否则,不返回任何新的container的分配结果。

    Yarn将资源请求的本地化层级分为节点内、机架内和集群内三个本地化级别,满足某些应用希望数据计算和数据本身在同一节点,或者在同一机架的需求。对于节点内或者机架内的请求无法满足,Yarn采用延迟调度的方式,即过一段时间再尝试满足这种本地化需求。如果失败时间或者失败次数超过限制,就进行本地化降级,比如,如果重试N次或者N分钟发现仍然无法将请求分配到应用所指定的node,则尝试分配到这个node相同的机架,或者,如果重试N次或者N分钟发现仍然无法将请求分配到应用所指定的机架,则只好将应用分配到集群内任何可用的节点上。

    展开全文
  • CocosCreator之获取加载资源(转)

    千次阅读 2019-03-05 17:05:48
    转自官方文档 ...加载远程资源和设备资源 资源的依赖释放 资源属性的声明 在 Creator 中,所有继承自 cc.Asset 的类型都统称资源,如 cc.Texture2D, cc.SpriteFrame, cc.AnimationClip, cc.Pre...

    转自官方文档

    Cocos Creator 有一套统一的资源管理机制,在本篇教程,我们将介绍

    • 资源属性的声明
    • 如何在 属性检查器 里设置资源
    • 动态加载资源
    • 加载远程资源和设备资源
    • 资源的依赖和释放

    资源属性的声明

    在 Creator 中,所有继承自 cc.Asset 的类型都统称资源,如 cc.Texture2D, cc.SpriteFrame, cc.AnimationClip, cc.Prefab 等。它们的加载是统一并且自动化的,相互依赖的资源能够被自动预加载。

    例如,当引擎在加载场景时,会先自动加载场景关联到的资源,这些资源如果再关联其它资源,其它也会被先被加载,等加载全部完成后,场景加载才会结束。

    脚本中可以这样定义一个 Asset 属性:

    // NewScript.js
    
    cc.Class({
    	extends: cc.Component,
    	properties: {
    		spriteFrame: {
            	default: null,
            	type: cc.SpriteFrame
        	},
    	}
    });
    

    如何在属性检查器里设置资源

    只要在脚本中定义好类型,就能直接在 属性检查器 很方便地设置资源。假设我们创建了这样一个脚本:

    // NewScript.js
    
    cc.Class({
    	extends: cc.Component,
    	properties: {
    
        	texture: {
            	default: null,
            	type: cc.Texture2D
        	},
        	spriteFrame: {
           		default: null,
            	type: cc.SpriteFrame
        	},
    	}
    });
    

    将它添加到节点后,在 属性检查器 中是这样的:
    在这里插入图片描述
    接下来我们从 资源管理器 里面分别将一张 Texture 和一个 SpriteFrame 拖到 属性检查器 的对应属性中:
    asset-in-properties-dnd
    结果如下:
    asset-in-properties-dnd
    这样就能在脚本里直接拿到设置好的资源:

    onLoad: function () {
        var spriteFrame = this.spriteFrame;
        var texture = this.texture;
    
        spriteFrame.setTexture(texture);
    }
    

    在 属性检查器 里设置资源虽然很直观,但资源只能在场景里预先设好,没办法动态切换。如果需要动态切换,你需要看看下面的内容。

    动态加载

    动态加载资源要注意两点,一是所有需要通过脚本动态加载的资源,都必须放置在 resources 文件夹或它的子文件夹下。resources 需要在 assets 文件夹中手工创建,并且必须位于 assets 的根目录,就像这样:
    在这里插入图片描述
    resources 文件夹中的资源,可以引用文件夹外部的其它资源,同样也可以被外部场景或资源引用到。项目构建时,除了已在 构建发布 面板勾选的场景外,resources 文件夹中的所有资源,连同它们关联依赖的 resources 文件夹外部的资源,都会被导出。

    如果一份资源仅仅是被 resources 中的其它资源所依赖,而不需要直接被 cc.loader.loadRes 调用,那么 请不要 放在 resources 文件夹里。否则会增大包体和 settings.js 的大小,并且项目中无用的资源,将无法在构建的过程中自动剔除。同时在构建过程中,JSON 的自动合并策略也将受到影响,无法尽可能将零碎的 JSON 合并起来。

    第二个要注意的是 Creator 相比之前的 Cocos2d-JS,资源动态加载的时候都是 异步 的,需要在回调函数中获得载入的资源。这么做是因为 Creator 除了场景关联的资源,没有另外的资源预加载列表,动态加载的资源是真正的动态加载。

    动态加载 Asset
    Creator 提供了 cc.loader.loadRes 这个 API 来专门加载那些位于 resources 目录下的 Asset。和 cc.loader.load 不同的是,loadRes 一次只能加载单个 Asset。调用时,你只要传入相对 resources 的路径即可,并且路径的结尾处 不能 包含文件扩展名。

    // 加载 Prefab
    cc.loader.loadRes("test assets/prefab", function (err, prefab) {
    	var newNode = cc.instantiate(prefab);
    	cc.director.getScene().addChild(newNode);
    });
    
    // 加载 AnimationClip
    	var self = this;
    	cc.loader.loadRes("test assets/anim", function (err, clip) {
    		self.node.getComponent(cc.Animation).addClip(clip, "anim");
    	});
    

    加载 SpriteFrame
    图片设置为 Sprite 后,将会在 资源管理器 中生成一个对应的 SpriteFrame。但如果直接加载 test assets/image,得到的类型将会是 cc.Texture2D。你必须指定第二个参数为资源的类型,才能加载到图片生成的 cc.SpriteFrame:

    // 加载 SpriteFrame
    var self = this;
    cc.loader.loadRes("test assets/image", cc.SpriteFrame, function (err, spriteFrame) {
        self.node.getComponent(cc.Sprite).spriteFrame = spriteFrame;
    });
    

    如果指定了类型参数,就会在路径下查找指定类型的资源。当你在同一个路径下同时包含了多个重名资源(例如同时包含 player.clip 和 player.psd),或者需要获取 “子资源”(例如获取 Texture2D 生成的 SpriteFrame),就需要声明类型。

    加载图集中的 SpriteFrame
    对从 TexturePacker 等第三方工具导入的图集而言,如果要加载其中的 SpriteFrame,则只能先加载图集,再获取其中的 SpriteFrame。这是一种特殊情况。

    // 加载 SpriteAtlas(图集),并且获取其中的一个 SpriteFrame
    // 注意 atlas 资源文件(plist)通常会和一个同名的图片文件(png)放在一个目录下, 所以需要在第二个参数指定资源类型
    cc.loader.loadRes("test assets/sheep", cc.SpriteAtlas, function (err, atlas) {
        var frame = atlas.getSpriteFrame('sheep_down_0');
        sprite.spriteFrame = frame;
    });
    

    资源释放
    loadRes 加载进来的单个资源如果需要释放,可以调用 cc.loader.releaseRes,releaseRes 可以传入和 loadRes 相同的路径和类型参数。

    cc.loader.releaseRes("test assets/image", cc.SpriteFrame);
    cc.loader.releaseRes("test assets/anim");
    

    此外,你也可以使用 cc.loader.releaseAsset 来释放特定的 Asset 实例。

    cc.loader.releaseAsset(spriteFrame);
    

    资源批量加载
    cc.loader.loadResDir 可以加载相同路径下的多个资源:

    // 加载 test assets 目录下所有资源
    cc.loader.loadResDir("test assets", function (err, assets) {
        // ...
    });
    
    // 加载 test assets 目录下所有 SpriteFrame,并且获取它们的路径
    cc.loader.loadResDir("test assets", cc.SpriteFrame, function (err, assets, urls) {
        // ...
    });
    

    加载远程资源和设备资源

    在目前的 Cocos Creator 中,我们支持加载远程贴图资源,这对于加载用户头像等需要向服务器请求的贴图很友好,需要注意的是,这需要开发者直接调用 cc.loader.load。同时,如果用户用其他方式下载了资源到本地设备存储中,也需要用同样的 API 来加载,上文中的 loadRes 等 API 只适用于应用包内的资源和热更新的本地资源。下面是这个 API 的用法:

    // 远程 url 带图片后缀名
    var remoteUrl = "http://unknown.org/someres.png";
    cc.loader.load(remoteUrl, function (err, texture) {
        // Use texture to create sprite frame
    });
    
    // 远程 url 不带图片后缀名,此时必须指定远程图片文件的类型
    remoteUrl = "http://unknown.org/emoji?id=124982374";
    cc.loader.load({url: remoteUrl, type: 'png'}, function () {
        // Use texture to create sprite frame
    });
    
    // 用绝对路径加载设备存储内的资源,比如相册
    var absolutePath = "/dara/data/some/path/to/image.png"
    cc.loader.load(absolutePath, function () {
        // Use texture to create sprite frame
    });
    

    目前的此类手动资源加载还有一些限制,对用户影响比较大的是:

    1. 原生平台远程加载不支持图片文件以外类型的资源
    2. 这种加载方式只支持图片、声音、文本等原生资源类型,不支持这种加载方式只支持图片、声音、文本等原生资源类型,不支持SpriteFrame、SpriteAtlas、Tilemap 等资源的直接加载和解析(需要后续版本中的 AssetBundle 支持)
    3. Web 端的远程加载受到浏览器的 CORS 跨域策略限制,如果对方服务器禁止跨域访问,那么会加载失败,而且由于 WebGL安全策略的限制,即便对方服务器允许 http 请求成功之后也无法渲染。

    资源的依赖和释放

    在加载完资源之后,所有的资源都会临时被缓存到 cc.loader 中,以避免重复加载资源时发送无意义的 http 请求,当然,缓存的内容都会占用内存,有些资源可能用户不再需要了,想要释放它们,这里介绍一下在做资源释放时需要注意的事项。

    首先最为重要的一点就是:资源之间是互相依赖的。

    比如下图,Prefab 资源中的 Node 包含 Sprite 组件,Sprite 组件依赖于 SpriteFrame,SpriteFrame 资源依赖于 Texture 资源,而 Prefab,SpriteFrame 和 Texture 资源都被 cc.loader 缓存起来了。这样做的好处是,有可能有另一个 SpriteAtlas 资源依赖于同样的一个 SpriteFrame 和 Texture,那么当你手动加载这个 SpriteAtlas 的时候,就不需要再重新请求贴图资源了,cc.loader 会自动使用缓存中的资源。
    在这里插入图片描述
    在搞明白资源的相互引用之后,资源释放的问题也就呼之欲出了,当你选择释放一个 Prefab 时,我们是不会自动释放它依赖的其他资源的,因为有可能这些依赖资源还有其他的用处。所以用户在释放资源时经常会问我们,为什么我都把资源释放了,内存占用还是居高不下?原因就是真正占用内存的贴图等基础资源并不会随着你释放 Prefab 或者 SpriteAtlas 而被释放。

    接下来要介绍问题的另一个核心:JavaScript 中无法跟踪对象引用

    在 JavaScript 这种脚本语言中,由于其弱类型特性,以及为了代码的便利,往往是不包含内存管理功能的,所有对象的内存都由垃圾回收机制来管理。这就导致 JS 层逻辑永远不知道一个对象会在什么时候被释放,这意味着引擎无法通过类似引用计数的机制来管理外部对象对资源的引用,也无法严谨得统计资源是否不再被需要了。基于以上的原因,目前 cc.loader 的设计实际上是依赖于用户根据游戏逻辑管理资源,用户可以决定在某一时刻不再需要某些资源以及它依赖的资源,立即将它们在 cc.loader 中的缓存释放。也可以选择在释放依赖资源的时候,防止部分共享资源被释放。下面是一个简单的示例:

    // 直接释放某个贴图
    cc.loader.release(texture);
    // 释放一个 prefab 以及所有它依赖的资源
    var deps = cc.loader.getDependsRecursively('prefabs/sample');
    cc.loader.release(deps);
    // 如果在这个 prefab 中有一些和场景其他部分共享的资源,你不希望它们被释放,可以将这个资源从依赖列表中删除
    var deps = cc.loader.getDependsRecursively('prefabs/sample');
    var index = deps.indexOf(texture2d._uuid);
    if (index !== -1)
        deps.splice(index, 1);
    cc.loader.release(deps);
    

    最后一个值得关注的要点:JavaScript 的垃圾回收是延迟的

    想象一种情况,当你释放了 cc.loader 对某个资源的引用之后,由于考虑不周的原因,游戏逻辑再次请求了这个资源。此时垃圾回收还没有开始(垃圾回收的时机不可控),或者你的游戏逻辑某处,仍然持有一个对于这个旧资源的引用,那么意味着这个资源还存在内存中,但是 cc.loader 已经访问不到了,所以会重新加载它。这造成这个资源在内存中有两份同样的拷贝,浪费了内存。如果只是一个资源还好,但是如果类似的资源很多,甚至不止一次被重复加载,这对于内存的压力是有可能很高的。如果观察到游戏使用的内存曲线有这样的异常,请仔细检查游戏逻辑,是否存在泄漏,如果没有的话,垃圾回收机制是会正常回收这些内存的。

    以上就是管理资源依赖和释放时需要注意的细节,这部分的功能和 API 设计还没有完全定案,我们还是希望尽力给大家带来尽可能方便的引擎 API,所以后续也会尝试一些其他的办法提升友好度,届时会更新这篇文档。

    展开全文
  • Yarn资源队列配置使用

    万次阅读 2019-06-14 17:08:37
    Yarn资源队列配置使用 前言 试想一下,你现在所在的公司有一个hadoop的集群。但是A项目组经常做一些定时的BI报表,B项目组则经常使用一些软件做一些临时需求。那么他们肯定会遇到同时提交任务的场景,这个时候...
  • 静态资源存储 vs 对象资源存储

    千次阅读 2022-01-17 20:12:54
    权限设置:支持基础权限设置高级安全规则权限控制 上传管理:在这里可以查看文件上传历史、进度及状态 文件搜索:支持文件前缀名称及子目录文件的搜索 组件支持支持在 image、audio 等组件中传入云文件 ID 缓存...
  • Unity资源机制 1、概述  本文意在阐述Unity资源机制相关的信息,以及一些关于个人的理解与试验结果。... Unity必须通过导入将所支持资源序列化,生成AssetComponents后,才能被Unity使用。以下是Unity对As
  • RestFul资源架构设计详解

    万次阅读 2021-12-11 16:17:25
    要理解RESTful架构,需要理解...下面我们结合REST原则,围绕资源展开讨论,从资源的定义、获取、表述、关联、状态变迁等角度,列举一些关键概念并加以解释。 资源与URI 统一资源接口 资源的表述 资源的链接 状态的转移
  • 第1章 基带无线资源概述 1.1 无线资源的作用 所谓无线资源是指能够承载用户二进制数据的无线信号。 有点类似飞机以及飞机上的座位。 不同的频率类似不同航班的飞机。 不同子载波波类似与同一个飞机上的不同...
  • 公众号Tab栏功能介绍↓↓↓重点指数:★★★★Tab1:资源推荐(位置见上图)此栏目主要功能有:全部资源、C4D教程、设计必备、插画资源 1、【全部资源】:可翻看/搜索本公众号内的全部资源,进入本栏目后,点击上方...
  • 游戏资源提取工具集合

    热门讨论 2011-01-20 12:52:55
    PAK,PBO,PFF,PKR,POD,RES,U,VDF,VPP,WAD,WDIR,WTN,XCR,ZWP等等数十种文件包的解包/封包程序,程序可以在扫描过程中自动按选择的分类识别你需要的文件,有了它,几乎各种游戏的文件包你都可以解开并得到你想要的资源 ...
  • 什么是决策支持系统? 决策支持系统(Decision-making Support System,DSS)是管理信息系统应用概念深化,在管理信息系统基础上发展起来的系统。 DSS是解决非结构化问题,服务于高层决策的管理信息系统,按功能可...
  • 使用SpringBoot搭建web项目时静态资源的访问必然是一个需要考虑的问题,不过SpringBoot在对于静态资源访问的问题上已经做了很好的支持,下面就静态资源访问的正确姿势进行说明。 一、静态资源配置 SpringBoot默认从...
  • 资源共享网盘app是一款方便好用的网盘管理软件,软件的界面十分简洁,并且操作起来很方便,能够支持多人共享云盘资料,只要有一个人上传资料,就可以实现多人共享资料,不在需要再重复上传下载,为用户带来许多...
  • 目录 前言 摘要 1.介绍 2.动机 3.分配特性 4.主导资源公平(DRF) 4.1举个栗子 4.2DRF调度算法 4.3加权DRF 5.可选的公平分配策略 5.1资产公平 ...6.1.3资源单调性VS共享激励帕累托最...
  • 5G NR 上下行资源分配

    万次阅读 多人点赞 2019-09-05 14:16:25
    为了接收PDSCH或PUSCH,UE一般要先接收PDCCH,其中包含...下面先介绍下行资源分配,上行下行有很多共通的地方,然后只介绍上行下行不同的地方。 一 下行资源分配 1.时域资源分配 DCI中的Time domain resourc...
  • 收费下载资源专业版 WordPress插件,经过完美测试运行于wordpress 3.1.x-3.8.x版本,很多做素材和资源分享的网站也许都希望增加付费下载,而国外的插件要么就是商城购物,要么下载不支持支付功能,这款收费下载资源...
  • 5G NR PDSCH、PUSCH资源分配

    千次阅读 2020-06-17 20:07:00
    通俗易懂讲解5G NR PDSCH、PUSCH资源分配。
  • kubernetes资源对象--podjob

    千次阅读 2017-08-01 18:03:39
    pod Pod是K8S的最小操作单元,一个Pod可以由一个或多个容器组成;整个K8S系统都是围绕着Pod展开的,比如如何部署运行Pod、如何保证Pod的数量、...Pod是能够被创建、调度管理的最小单元; 每个Pod都有一个独立的IP;
  • Qt中资源文件的使用及意义

    千次阅读 2019-09-29 16:25:07
    前言 一个应用程序中可能需要加载各种资源(如图标、文本翻译等),对于这些资源的管理,Qt有专门的资源管理系统,它是一个跨平台的资源机制...资源系统基于qmake、rccQFile之间的紧密合作。它废弃了 Qt 3的嵌入工...
  • OpenStack云端的资源调度优化剖析

    万次阅读 2016-07-26 16:37:37
    它已经在当前的基础设施即服务(IaaS)资源管理领域占据领导地位,成为公有云、私有云及混合云管理的“云操作系统”事实上的标准,在政府、电信、金融、制造、能源、零售、医疗、交通等行业成为企业创新的利器。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,095,715
精华内容 438,286
关键字:

如何获得资源和支持