• HDFS源码分析心跳汇报之数据结构初始化


     在《HDFS源码分析心跳汇报之整体结构》一文中,我们详细了解了HDFS中关于心跳的整体结构,知道了BlockPoolManager、BPOfferService和BPServiceActor三者之间的关系。那么,HDFS心跳相关的这些数据结构,都是如何被初始化的呢?本文,我们就开始研究HDFS心跳汇报之数据结构初始化。

            首先,在DataNode节点启动时所必须执行的startDataNode()方法中,有如下代码:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. // DataNode启动时执行的startDataNode()方法  
    2. // 构造一个BlockPoolManager实例  
    3. // 调用其refreshNamenodes()方法  
    4. blockPoolManager = new BlockPoolManager(this);  
    5. blockPoolManager.refreshNamenodes(conf);  

            它构造了一个BlockPoolManager实例,并调用其refreshNamenodes()方法,完成NameNodes的刷新。我们来看下这个方法:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. void refreshNamenodes(Configuration conf)  
    2.     throws IOException {  
    3.   LOG.info("Refresh request received for nameservices: " + conf.get  
    4.           (DFSConfigKeys.DFS_NAMESERVICES));  
    5.   
    6.   // 从配置信息conf中获取nameserviceid->{namenode名称->InetSocketAddress}的映射集合newAddressMap  
    7.   Map<String, Map<String, InetSocketAddress>> newAddressMap = DFSUtil  
    8.           .getNNServiceRpcAddressesForCluster(conf);  
    9.   
    10.   // 需要通过使用synchronized关键字在refreshNamenodesLock上加互斥锁  
    11.   synchronized (refreshNamenodesLock) {  
    12.     // 调用doRefreshNamenodes()方法执行集合newAddressMap中的刷新  
    13.     doRefreshNamenodes(newAddressMap);  
    14.   }  
    15. }  

            很简单,两大步骤:第一步,从配置信息conf中获取nameserviceid->{namenode名称->InetSocketAddress}的映射集合newAddressMap,第二步调用doRefreshNamenodes()方法执行集合newAddressMap中NameNodes的刷新。

            首先,我们看下如何从配置信息conf中获取nameserviceid->{namenode名称->InetSocketAddress}的映射集合newAddressMap,相关代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. /** 
    2.  * Returns list of InetSocketAddresses corresponding to the namenode 
    3.  * that manages this cluster. Note this is to be used by datanodes to get 
    4.  * the list of namenode addresses to talk to. 
    5.  * 
    6.  * Returns namenode address specifically configured for datanodes (using 
    7.  * service ports), if found. If not, regular RPC address configured for other 
    8.  * clients is returned. 
    9.  * 
    10.  * @param conf configuration 
    11.  * @return list of InetSocketAddress 
    12.  * @throws IOException on error 
    13.  */  
    14. public static Map<String, Map<String, InetSocketAddress>>  
    15.   getNNServiceRpcAddressesForCluster(Configuration conf) throws IOException {  
    16.     
    17. / Use default address as fall back  
    18.   String defaultAddress;  
    19.   try {  
    20.     // 获取默认地址defaultAddress  
    21.     defaultAddress = NetUtils.getHostPortString(NameNode.getAddress(conf));  
    22.   } catch (IllegalArgumentException e) {  
    23.     defaultAddress = null;  
    24.   }  
    25.   
    26.   // 获取hdfs的内部命名服务:dfs.internal.nameservices,得到集合parentNameServices  
    27.   Collection<String> parentNameServices = conf.getTrimmedStringCollection  
    28.           (DFSConfigKeys.DFS_INTERNAL_NAMESERVICES_KEY);  
    29.   
    30.   if (parentNameServices.isEmpty()) {// 如果没有配置dfs.internal.nameservices  
    31.     // 获取dfs.nameservices,赋值给集合parentNameServices  
    32.     parentNameServices = conf.getTrimmedStringCollection  
    33.             (DFSConfigKeys.DFS_NAMESERVICES);  
    34.   } else {  
    35.     // Ensure that the internal service is ineed in the list of all available  
    36.     // nameservices.  
    37.       
    38.     // 获取dfs.nameservices  
    39.     Set<String> availableNameServices = Sets.newHashSet(conf  
    40.             .getTrimmedStringCollection(DFSConfigKeys.DFS_NAMESERVICES));  
    41.       
    42.     // 验证parentNameServices中的每个nsId在dfs.nameservices中是否都存在  
    43.     // 即参数dfs.internal.nameservices包含在参数dfs.nameservices范围内  
    44.     for (String nsId : parentNameServices) {  
    45.       if (!availableNameServices.contains(nsId)) {  
    46.         throw new IOException("Unknown nameservice: " + nsId);  
    47.       }  
    48.     }  
    49.   }  
    50.   
    51.   // 调用getAddressesForNsIds()方法,获取nameserviceId->{nameNodeId->InetSocketAddress}对应关系的集合  
    52.   // dfs.namenode.servicerpc-address  
    53.   // dfs.namenode.rpc-address  
    54.   Map<String, Map<String, InetSocketAddress>> addressList =  
    55.           getAddressesForNsIds(conf, parentNameServices, defaultAddress,  
    56.                   DFS_NAMENODE_SERVICE_RPC_ADDRESS_KEY, DFS_NAMENODE_RPC_ADDRESS_KEY);  
    57.   if (addressList.isEmpty()) {  
    58.     throw new IOException("Incorrect configuration: namenode address "  
    59.             + DFS_NAMENODE_SERVICE_RPC_ADDRESS_KEY + " or "  
    60.             + DFS_NAMENODE_RPC_ADDRESS_KEY  
    61.             + " is not configured.");  
    62.   }  
    63.   return addressList;  
    64. }  

            这个方法的处理逻辑如下:

            1、首先,根据NameNode类的静态方法getAddress()从配置信息中获取默认地址defaultAddress;

            2、然后,获取hdfs的内部命名服务:dfs.internal.nameservices,得到集合parentNameServices:

                  2.1、如果没有配置dfs.internal.nameservices,获取dfs.nameservices,赋值给集合parentNameServices;

                  2.2、如果配置了dfs.internal.nameservices,再获取获取dfs.nameservices,得到availableNameServices,验证parentNameServices中的每个nsId在availableNameServices中是否都存在,即参数dfs.internal.nameservices包含在参数dfs.nameservices范围内;

            3、调用getAddressesForNsIds()方法,利用conf、parentNameServices、defaultAddress等获取nameserviceId->{nameNodeId->InetSocketAddress}对应关系的集合addressList,并返回。

            下面,我们再看下getAddressesForNsIds()方法,代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1.  /** 
    2.   * Returns the configured address for all NameNodes in the cluster. 
    3.   * @param conf configuration 
    4.   * @param nsIds 
    5.   *@param defaultAddress default address to return in case key is not found. 
    6.   * @param keys Set of keys to look for in the order of preference   @return a map(nameserviceId to map(namenodeId to InetSocketAddress)) 
    7.   */  
    8.  private static Map<String, Map<String, InetSocketAddress>>  
    9.    getAddressesForNsIds(Configuration conf, Collection<String> nsIds,  
    10.                         String defaultAddress, String... keys) {  
    11.    // Look for configurations of the form <key>[.<nameserviceId>][.<namenodeId>]  
    12.    // across all of the configured nameservices and namenodes.  
    13.   
    14. // dfs.namenode.servicerpc-address  
    15. // dfs.namenode.rpc-address  
    16.    Map<String, Map<String, InetSocketAddress>> ret = Maps.newLinkedHashMap();  
    17.      
    18.    // 遍历每个nameserviceId,做以下处理:  
    19.    for (String nsId : emptyAsSingletonNull(nsIds)) {  
    20.       
    21.      // 通过getAddressesForNameserviceId()方法获取nameNodeId->InetSocketAddress的对应关系,nameNodeId来自参数dfs.ha.namenodes.nsId  
    22.      Map<String, InetSocketAddress> isas =  
    23.        getAddressesForNameserviceId(conf, nsId, defaultAddress, keys);  
    24.      if (!isas.isEmpty()) {  
    25.         
    26.     // 将nameserviceId->{nameNodeId->InetSocketAddress}的对应关系放入集合ret  
    27.        ret.put(nsId, isas);  
    28.      }  
    29.    }  
    30.      
    31.    // 返回nameserviceId->{nameNodeId->InetSocketAddress}对应关系的集合ret  
    32.    return ret;  
    33.  }  

            非常简单,遍历每个nameserviceId,做以下处理:

            1、通过getAddressesForNameserviceId()方法获取nameNodeId->InetSocketAddress的对应关系,nameNodeId来自参数dfs.ha.namenodes.nsId;

            2、将nameserviceId->{nameNodeId->InetSocketAddress}的对应关系放入集合ret;

            3、最后返回nameserviceId->{nameNodeId->InetSocketAddress}对应关系的集合ret。

            继续看getAddressesForNameserviceId()方法,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1.  private static Map<String, InetSocketAddress> getAddressesForNameserviceId(  
    2.      Configuration conf, String nsId, String defaultValue,  
    3.      String... keys) {  
    4. // keys  
    5. // dfs.namenode.servicerpc-address  
    6. // dfs.namenode.rpc-address  
    7.     
    8. // 获取dfs.ha.namenodes.nsId  
    9.    Collection<String> nnIds = getNameNodeIds(conf, nsId);  
    10.    Map<String, InetSocketAddress> ret = Maps.newHashMap();  
    11.    for (String nnId : emptyAsSingletonNull(nnIds)) {  
    12.      String suffix = concatSuffixes(nsId, nnId);  
    13.        
    14.      // 根据keys获取address  
    15.      String address = getConfValue(defaultValue, suffix, conf, keys);  
    16.      if (address != null) {  
    17.         
    18.     // 将address封装成InetSocketAddress,得到isa  
    19.        InetSocketAddress isa = NetUtils.createSocketAddr(address);  
    20.        if (isa.isUnresolved()) {  
    21.          LOG.warn("Namenode for " + nsId +  
    22.                   " remains unresolved for ID " + nnId +  
    23.                   ".  Check your hdfs-site.xml file to " +  
    24.                   "ensure namenodes are configured properly.");  
    25.        }  
    26.          
    27.        // 将nnId->InetSocketAddress的对应关系放入到Map中  
    28.        ret.put(nnId, isa);  
    29.      }  
    30.    }  
    31.    return ret;  
    32.  }  

            它通过参数获取dfs.ha.namenodes.nsId获取到NameNodeId的集合nnIds,然后针对每个NameNode,根据keys获取address,这keys传递进来的就是dfs.namenode.servicerpc-address、dfs.namenode.rpc-address,也就是优先取前一个参数,前一个取不到的话,再取第二个参数,然后将address封装成InetSocketAddress,得到isa,将nnId->InetSocketAddress的对应关系放入到Map中,最终返回给上层应用。

            至此,从配置信息conf中获取nameserviceid->{namenode名称->InetSocketAddress}的映射集合newAddressMap就分析完了。下面,我们再看下初始化的重点:调用doRefreshNamenodes()方法执行集合newAddressMap中的刷新。代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1.  private void doRefreshNamenodes(  
    2.      Map<String, Map<String, InetSocketAddress>> addrMap) throws IOException {  
    3.      
    4. // 确保当前线程在refreshNamenodesLock上拥有互斥锁  
    5. assert Thread.holdsLock(refreshNamenodesLock);  
    6.   
    7. // 定义三个集合,分别为待刷新的toRefresh、待添加的toAdd和待移除的toRemove  
    8.    Set<String> toRefresh = Sets.newLinkedHashSet();  
    9.    Set<String> toAdd = Sets.newLinkedHashSet();  
    10.    Set<String> toRemove;  
    11.      
    12.    // 使用synchronized关键字在当前对象上获得互斥锁  
    13.    synchronized (this) {  
    14.      // Step 1. For each of the new nameservices, figure out whether  
    15.      // it's an update of the set of NNs for an existing NS,  
    16.      // or an entirely new nameservice.  
    17.      // 第一步,针对所有新的nameservices中的每个nameservice,  
    18.      // 确认它是一个已存在nameservice中的被更新了的NN集合,还是完全的一个新的nameservice  
    19.      // 判断的依据就是对应nameserviceId是否在bpByNameserviceId结合中存在  
    20.       
    21.      // 循环addrMap,放入待添加或者待刷新集合  
    22.      for (String nameserviceId : addrMap.keySet()) {  
    23.          
    24.     // 如果bpByNameserviceId结合中存在nameserviceId,加入待刷新集合toRefresh,否则加入到待添加集合toAdd  
    25.        if (bpByNameserviceId.containsKey(nameserviceId)) {  
    26.          toRefresh.add(nameserviceId);  
    27.        } else {  
    28.          toAdd.add(nameserviceId);  
    29.        }  
    30.      }  
    31.        
    32.      // Step 2. Any nameservices we currently have but are no longer present  
    33.      // need to be removed.  
    34.      // 第二步,删除所有我们目前拥有但是现在不再需要的,也就是bpByNameserviceId中存在,而配置信息addrMap中没有的  
    35.        
    36.      // 加入到待删除集合toRemove  
    37.      toRemove = Sets.newHashSet(Sets.difference(  
    38.          bpByNameserviceId.keySet(), addrMap.keySet()));  
    39.        
    40.      // 验证,待刷新集合toRefresh的大小与待添加集合toAdd的大小必须等于配置信息addrMap中的大小  
    41.      assert toRefresh.size() + toAdd.size() ==  
    42.        addrMap.size() :  
    43.          "toAdd: " + Joiner.on(",").useForNull("<default>").join(toAdd) +  
    44.          "  toRemove: " + Joiner.on(",").useForNull("<default>").join(toRemove) +  
    45.          "  toRefresh: " + Joiner.on(",").useForNull("<default>").join(toRefresh);  
    46.   
    47.        
    48.      // Step 3. Start new nameservices  
    49.      // 第三步,启动所有新的nameservices  
    50.      if (!toAdd.isEmpty()) {// 待添加集合toAdd不为空  
    51.       
    52.        LOG.info("Starting BPOfferServices for nameservices: " +  
    53.            Joiner.on(",").useForNull("<default>").join(toAdd));  
    54.        
    55.        // 针对待添加集合toAdd中的每个nameserviceId,做以下处理:  
    56.        for (String nsToAdd : toAdd) {  
    57.            
    58.          // 从addrMap中根据nameserviceId获取对应Socket地址InetSocketAddress,创建集合addrs  
    59.          ArrayList<InetSocketAddress> addrs =  
    60.            Lists.newArrayList(addrMap.get(nsToAdd).values());  
    61.            
    62.          // 根据addrs创建BPOfferService  
    63.          BPOfferService bpos = createBPOS(addrs);  
    64.            
    65.          // 将nameserviceId->BPOfferService的对应关系添加到集合bpByNameserviceId中  
    66.          bpByNameserviceId.put(nsToAdd, bpos);  
    67.            
    68.          // 将BPOfferService添加到集合offerServices中  
    69.          offerServices.add(bpos);  
    70.        }  
    71.      }  
    72.        
    73.      // 启动所有BPOfferService,实际上是通过调用它的start()方法启动  
    74.      startAll();  
    75.    }  
    76.   
    77.    // Step 4. Shut down old nameservices. This happens outside  
    78.    // of the synchronized(this) lock since they need to call  
    79.    // back to .remove() from another thread  
    80.    // 第4步,停止所有旧的nameservices。这个是发生在synchronized代码块外面的,是因为它们需要回调另外一个线程的remove()方法  
    81.      
    82.    if (!toRemove.isEmpty()) {  
    83.      LOG.info("Stopping BPOfferServices for nameservices: " +  
    84.          Joiner.on(",").useForNull("<default>").join(toRemove));  
    85.        
    86.      // 遍历待删除集合toRemove中的每个nameserviceId  
    87.      for (String nsToRemove : toRemove) {  
    88.          
    89.     // 根据nameserviceId从集合bpByNameserviceId中获取BPOfferService  
    90.     BPOfferService bpos = bpByNameserviceId.get(nsToRemove);  
    91.       
    92.     // 调用BPOfferService的stop()和join()方法停止服务  
    93.        bpos.stop();  
    94.        bpos.join();  
    95.        // they will call remove on their own  
    96.        // 它们会调用本身的remove()方法  
    97.      }  
    98.    }  
    99.      
    100.    // Step 5. Update nameservices whose NN list has changed  
    101.    // 第5步,更新NN列表已变化的nameservices  
    102.    if (!toRefresh.isEmpty()) {// 待更新集合toRefresh不为空时  
    103.      LOG.info("Refreshing list of NNs for nameservices: " +  
    104.          Joiner.on(",").useForNull("<default>").join(toRefresh));  
    105.        
    106.      // 遍历待更新集合toRefresh中的每个nameserviceId  
    107.      for (String nsToRefresh : toRefresh) {  
    108.         
    109.     // 根据nameserviceId从集合bpByNameserviceId中取出对应的BPOfferService  
    110.        BPOfferService bpos = bpByNameserviceId.get(nsToRefresh);  
    111.          
    112.        // 根据BPOfferService从配置信息addrMap中取出NN的Socket地址InetSocketAddress,形成列表addrs  
    113.        ArrayList<InetSocketAddress> addrs =  
    114.          Lists.newArrayList(addrMap.get(nsToRefresh).values());  
    115.          
    116.        // 调用BPOfferService的refreshNNList()方法根据addrs刷新NN列表  
    117.        bpos.refreshNNList(addrs);  
    118.      }  
    119.    }  
    120.  }  

            整个doRefreshNamenodes()方法比较长,但是主体逻辑很清晰,主要分五大步骤,分别如下:

            1、第一步,针对nameserviceid->{namenode名称->InetSocketAddress}的映射集合newAddressMap中每个nameserviceid,确认它是一个完全新加的nameservice,还是一个其NameNode列表被更新的nameservice,分别加入待添加toAdd和待刷新toRefresh集合;

            2、第二步,针对newAddressMap中没有,而目前DataNode内存bpByNameserviceId中存在的nameservice,需要删除,添加到待删除toRemove集合;

            3、第三步,处理待添加toAdd集合,启动所有新的nameservices:根据addrs创建BPOfferService,维护BPOfferService相关映射集合,然后启动所有的BPOfferService;

            4、第四步,处理待删除toRemove集合,停止所有旧的nameservices;

            5、第五步,处理待刷新toRefresh集合,更新NN列表已变化的nameservices。

            对,就是这么简单,将需要处理的nameservice分别加入到不同的集合,然后按照添加、删除、更新的顺序针对处理类型相同的nameservice一并处理即可。

            接下来,我们分别研究下每一步的细节:

            1、第一步,针对nameserviceid->{namenode名称->InetSocketAddress}的映射集合newAddressMap中每个nameserviceid,确认它是一个完全新加的nameservice,还是一个其NameNode列表被更新的nameservice,分别加入待添加toAdd和待刷新toRefresh集合;

            它的处理思路是,循环addrMap中每个nameserviceid,放入待添加toAdd或者待刷新toRefresh集合;如果bpByNameserviceId结合中存在nameserviceId,加入待刷新集合toRefresh,否则加入到待添加集合toAdd。

            2、第二步,针对newAddressMap中没有,而目前DataNode内存bpByNameserviceId中存在的nameservice,需要删除,添加到待删除toRemove集合;

            它的处理思路是:利用Sets的difference()方法,比较bpByNameserviceId和addrMap两个集合的keySet,找出bpByNameserviceId中存在,但是addrMap中不存在的nameserviceid,生成待删除集合toRemove。

            3、第三步,处理待添加toAdd集合,启动所有新的nameservices:根据addrs创建BPOfferService,维护BPOfferService相关映射集合,然后启动所有的BPOfferService;

            这一步针对待添加集合toAdd中的每个nameserviceId,做以下处理:

                  3.1、从addrMap中根据nameserviceId获取对应Socket地址InetSocketAddress,创建集合addrs;

                  3.2、根据addrs创建BPOfferService实例bpos;

                  3.3、将nameserviceId->BPOfferService的对应关系添加到集合bpByNameserviceId中

                  3.4、将BPOfferService添加到集合offerServices中;

            最后,调用startAll()方法启动所有BPOfferService,实际上是通过调用它的start()方法启动。

            其中,创建BPOfferService实例bpos时,BPOfferService的构造方法如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. // 构造方法  
    2. BPOfferService(List<InetSocketAddress> nnAddrs, DataNode dn) {  
    3.   Preconditions.checkArgument(!nnAddrs.isEmpty(),  
    4.       "Must pass at least one NN.");  
    5.   this.dn = dn;  
    6.   
    7.   // 遍历nnAddrs,为每个namenode添加一个构造的BPServiceActor线城实例,加入到bpServices列表  
    8.   for (InetSocketAddress addr : nnAddrs) {  
    9.     this.bpServices.add(new BPServiceActor(addr, this));  
    10.   }  
    11. }  

            它实际上是遍历nnAddrs,为每个namenode添加一个构造的BPServiceActor线城实例,加入到bpServices列表。
            而调用startAll()方法启动所有BPOfferService时,执行的代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. synchronized void startAll() throws IOException {  
    2.   try {  
    3.     UserGroupInformation.getLoginUser().doAs(  
    4.         new PrivilegedExceptionAction<Object>() {  
    5.           @Override  
    6.           public Object run() throws Exception {  
    7.               
    8.             // 遍历offerServices,启动所有的BPOfferService  
    9.             for (BPOfferService bpos : offerServices) {  
    10.               bpos.start();  
    11.             }  
    12.             return null;  
    13.           }  
    14.         });  
    15.   } catch (InterruptedException ex) {  
    16.     IOException ioe = new IOException();  
    17.     ioe.initCause(ex.getCause());  
    18.     throw ioe;  
    19.   }  
    20. }  

            它会遍历offerServices,启动所有的BPOfferService,而BPOfferService的启动,实际上就是将其所持有的每个NameNode对应的BPServiceActor线程启动,代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. //This must be called only by blockPoolManager  
    2. void start() {  
    3.   for (BPServiceActor actor : bpServices) {  
    4.     actor.start();  
    5.   }  
    6. }  

            4、第四步,处理待删除toRemove集合,停止所有旧的nameservices;

            在这一步中,遍历待删除集合toRemove中的每个nameserviceId:

                   4.1、根据nameserviceId从集合bpByNameserviceId中获取BPOfferService;

                   4.2、调用BPOfferService的stop()和join()方法停止服务,它们会调用本身的remove()方法;

            而BPOfferService的stop()和join()方法,则是依次调用BPOfferService所包含的所有BPServiceActor线程的stop()和join()方法,代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. //This must be called only by blockPoolManager.  
    2. void stop() {  
    3.   for (BPServiceActor actor : bpServices) {  
    4.     actor.stop();  
    5.   }  
    6. }  
    7.   
    8. //This must be called only by blockPoolManager  
    9. void join() {  
    10.   for (BPServiceActor actor : bpServices) {  
    11.     actor.join();  
    12.   }  
    13. }  

            5、第五步,处理待刷新toRefresh集合,更新NN列表已变化的nameservices;

            在最后一步中,遍历待更新集合toRefresh中的每个nameserviceId:

                   5.1、根据nameserviceId从集合bpByNameserviceId中取出对应的BPOfferService;

                   5.2、根据BPOfferService从配置信息addrMap中取出NN的Socket地址InetSocketAddress,形成列表addrs;

                   5.3、调用BPOfferService的refreshNNList()方法根据addrs刷新NN列表。

            好了,HDFS心跳相关数据结构的初始化已分析完毕,至此,涉及到每个命名空间服务中每个NameNode相关的BPServiceActor线程均已启动,它是真正干活的苦力,真正的底层劳动人民啊!至于它是怎么运行来完成HDFS心跳的,我们下一节再分析吧!

  • 相关阅读:
    使用 Selenium
    Senium 简介
    第8章 动态渲染页面爬取
    Ajax 结果提取
    Ajax 分析方法
    WINDOW 2008多人访问设置
    Windows 2012设置允许单个用户连接多个会话的方法
    Windows Server 2012开启多人远程
    BOM 表
    修复材料工单分配材料订单重复占料问题的开发
  • 原文地址:https://www.cnblogs.com/jirimutu01/p/5556202.html
Copyright © 2020-2023  润新知