首页 > 数据库 > mysql教程 > HBase深入分析之RegionServer

HBase深入分析之RegionServer

WBOY
发布: 2016-06-07 16:32:31
原创
1120 人浏览过

HBase深入分析之RegionServer 所有的用户数据以及元数据的请求,在经过Region的定位,最终会落在RegionServer上,并由RegionServer实现数据的读写操作。本小节将重点介绍RegionServer的代码结构和功能,从实现细节上深入理解RegionServer对于数据的操作流程

HBase深入分析之RegionServer

所有的用户数据以及元数据的请求,在经过Region的定位,最终会落在RegionServer上,并由RegionServer实现数据的读写操作。本小节将重点介绍RegionServer的代码结构和功能,从实现细节上深入理解RegionServer对于数据的操作流程。

1 RegionServer概述

RegionServer是HBase集群运行在每个工作节点上的服务。它是整个HBase系统的关键所在,一方面它维护了Region的状态,提供了对于Region的管理和服务;另一方面,它与Master交互,上传Region的负载信息上传,参与Master的分布式协调管理。具体如图(1)所示。

RegionServer_all

图(1) RegionServer的整体功能图

HRegionServer与HMaster以及Client之间采用RPC协议进行通信。HRegionServer向HMaster定期汇报节点的负载状况,包括RS内存使用状态、在线状态的Region等信息,在该过程中RS扮演了RPC客户端的角色,而HMaster扮演了RPC服务器端的角色。RS内置的RpcServer实现了数据更新、读取、删除的操作,以及Region涉及到Flush、Compaction、Open、Close、Load文件等功能性操作。此时,RS扮演了RPC服务的服务端的角色。RS与Client之间的RPC是HBase最为核心的操作,其服务状况的好坏,直接反映了RS内部、以及它所依赖的HDFS服务质量的好坏,因此,该过程的RPC经常成为分析读写性能异常的突破口。
从RegionServer实现的功能上而言,除了与HMaster和Client之间的RPC通信之外,还包括如下几个重要的模块:
(1)依托ZookeeperWatcher进行的分布式信息共享与任务协调的工作。
MasterAddressTracker:捕获Master服务节点的变化。HBase使用多Master来解决Master单点故障的问题,主Master服务故障时,它与ZooKeeper的心跳延迟超过阈值,ZooKeeeper路径下的数据被清理,备Master上的ActiveMaserManager服务会竞争该Master路径,成为主Master。MasterAddresTracker是RS内部监听Master节点变化的追踪器。
ClusterStatusTracker:HBase集群状态追踪器。该选项可以标识当前集群的状态,及它的启动时间。该设置选项有利于集群中的各个工作节点(RS)统一执行启动和退出操作。
CatalogTracker:跟踪-ROOT-、.META.表的Region的状态。在HBase支持的-ROOT-、.META.、以及User Region三层树级目录结构中,-ROOT-、.META.表用来定位Region的位置,追踪-ROOT-表和.META.表对应Region的变化,可以时刻保证整个层次目录树的完整性。
SplitLogWorker:基于Region的HLog文件切分器。在RS宕机之后,RS上的保存的HLog文件,需要按照Region进行切分。HMaster会把这些文件作为任务放置到Zookeeper的splitlog路径下,RS上SplitLogWorker会尝试获取任务,对获取到的HLog文件按照Region进行分组,处理的结果保存到相应Region的recovered.edits目录下。
(2)Region的管理。
Region是HBase数据存储和管理的基本单位。Client从.META.表的查找RowKey对应的Region的位置,每个Region只能被一个RS提供服务,RS可以同时服务多个Region,来自不同RS上的Region组合成表格的整体逻辑视图。

RS_Region_relation

图(2) Region与RS逻辑关系图

RS内涉及到提供的有关Region维护的服务组件有:
1) MemStoreFlusher,控制RS的内存使用,有选择性地将Region的MemStore数据写入文件。该组件可以有效地控制RS的内存使用,flush文件的速度在一定程度上可以反应HBase写服务的繁忙状况。
2) CompactSplitThread,合并文件清理不需要的数据,控制Region的规模。在Store内的文件个数超过阈值时,触发Compact合并文件操作,一是清理被删除的数据,二是多余版本的清理。在Region内的Store文件大小超过阈值,会触发Region的Split操作,一个Region被切分成两个Region。这两个操作都是在CompactSplitThread的各自的线程池中被触发。
3) CompactionChecker,周期性检查RS上的Region是否需要进行Compaction操作,确认需要进行Compaction操作的Region,提交给CompactSplitThread执行请求。
RS的内存的使用分为MemStore和BlockCache。其中MemStore提供写操作的缓存,而BlockCache是提供的读请求缓存。它们详细的内容会在后续章节中介绍。
(3)WAL的管理。
HBase对于数据的更新和删除操作默认先Append到HLog文件,然后再更新到RS对应的Region上,因此,由HLog文件在RS的处理方式,被称为Write-Ahead-Log。多个Region的更新删除操作会被相继写入同一个文件,出于以下的原因,HLog文件会被截断,然后创建新HLog文件继续当前的Append操作。
1) Append操作失败,避免因底层文件系统的文件异常,阻塞数据的操作。
2) 降低存储空间的开销。当HLog上记录的数据完全从MemStore写入HDFS,此时如果多个HLog文件,有利于筛选冗余的HLog文件,提高存储空间的效率。
3) 提高分布式HLog文件切分操作(Distributed Log Split)的效率。多个HLog文件就对应同样数目的LogSplit子任务,从而可以借助多个RS的SplitLogWorker组件快速完成HLog文件的切分,尽快恢复Region的服务。
在RS内,LogRoller定期刷新出一个新的HLog文件。
(4)Metrics
Metrics对外提供了衡量HBase内部服务状况的参数。RegionServer内Metrics包含了内存使用、Region服务状况、Compaction、blockCache等一系列标识服务状况的参数。HBase Metrics继承Hadoop Metrics的实现,目前支持文件、Ganglia、以及数据流等多种输出方式,可以针对输出的Metrics信息灵活构建监控系统。
(5)HttpServer
RS内置了一个Jetty Web Server,用来对外提供RS的访问页面。访问页面目前支持实时Metrics信息查询、日志查询、线程的Dump、修改日志级别等操作。

2 RegionServer的启动过程分析

RegionServer服务由org.apache.hadoop.hbase.regionserver.HRegionServer类提供。该类实现了四个接口,分别是HRegionInterface,RegionServerServices,HBaseRPCErrorHandler和Runnable。其中,HRegionInterface定义了RS对外提供的RPC访问接口,通过RPCServer内置的Handler来处理请求;RegionServerServices定义了基于RS内部的服务信息接口,例如onlineRegions增、删、查接口,以及获取HLog、文件系统等接口;HBaseRPCErrorHandler定义了RPCServer异常状态检测处理接口;Runnable是Java库中的线程接口,实现该接口意味着RegionServer生命周期会运行在run()的函数体内。
RegionServer是一个独立的服务,有一个main函数在启动时被调用,main函数内通过HRegionServerCommandLine的反射机制在JVM内动态加载RegionServer实现类,并按照args解析参数情况,决定启动或者关闭RS服务。

<span style="color: #000000; font-weight: bold;">public</span> <span style="color: #000000; font-weight: bold;">class</span> HRegionServer <span style="color: #000000; font-weight: bold;">implements</span> HRegionInterface,
HBaseRPCErrorHandler,<span style="color: #003399;">Runnable</span>, RegionServerServices <span style="color: #009900;">&#123;</span>
... <span style="color: #666666; font-style: italic;">//成员变量定义和成员函数实现</span>
<span style="color: #000000; font-weight: bold;">public</span> <span style="color: #000000; font-weight: bold;">static</span> <span style="color: #000066; font-weight: bold;">void</span> main<span style="color: #009900;">&#40;</span><span style="color: #003399;">String</span><span style="color: #009900;">&#91;</span><span style="color: #009900;">&#93;</span> args<span style="color: #009900;">&#41;</span> <span style="color: #000000; font-weight: bold;">throws</span> <span style="color: #003399;">Exception</span> <span style="color: #009900;">&#123;</span>
...
<span style="color: #006633;">Configuration</span> conf <span style="color: #339933;">=</span> HBaseConfiguration.<span style="color: #006633;">create</span><span style="color: #009900;">&#40;</span><span style="color: #009900;">&#41;</span><span style="color: #339933;">;</span>
@SuppressWarnings<span style="color: #009900;">&#40;</span><span style="color: #0000ff;">"unchecked"</span><span style="color: #009900;">&#41;</span>
Class<span style="color: #339933;"><?</span> <span style="color: #000000; font-weight: bold;">extends</span> HRegionServer<span style="color: #339933;">></span> regionServerClass <span style="color: #339933;">=</span> <span style="color: #009900;">&#40;</span>Class<span style="color: #339933;"><?</span> <span style="color: #000000; font-weight: bold;">extends</span> HRegionServer<span style="color: #339933;">></span><span style="color: #009900;">&#41;</span> conf
.<span style="color: #006633;">getClass</span><span style="color: #009900;">&#40;</span>HConstants.<span style="color: #006633;">REGION_SERVER_IMPL</span>, HRegionServer.<span style="color: #000000; font-weight: bold;">class</span><span style="color: #009900;">&#41;</span><span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//获取RegionServer对应的类</span>
 
<span style="color: #000000; font-weight: bold;">new</span> HRegionServerCommandLine<span style="color: #009900;">&#40;</span>regionServerClass<span style="color: #009900;">&#41;</span>.<span style="color: #006633;">doMain</span><span style="color: #009900;">&#40;</span>args<span style="color: #009900;">&#41;</span><span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//创建RegionServer实例并启动</span>
<span style="color: #009900;">&#125;</span>
...
<span style="color: #009900;">&#125;</span>
登录后复制

初始化与执行过程包括:
(1)构造HRegionServer实例,初始化变量和对象。这涉及到以下重要变量初始化:

protected volatile boolean stopped = false;//关闭Server的标识,关闭过程中会置成ture
private boolean stopping = false;//关闭Region过程的标识,是进入stopped之前的状态
protected volatile boolean fsOk;//文件系统状态标识,false表示文件系统不可用
private final ConcurrentSkipListMap regionsInTransitionInRS =
new ConcurrentSkipListMap(Bytes.BYTES_COMPARATOR);//RS内处于迁移过程中的Region,其中true表示在open,false表示在close
protected final Map onlineRegions =
new ConcurrentHashMap();//RS内正在服务的Region
protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();//修改onlineRegions对象的读写锁
protected final int threadWakeFrequency;//工作线程服务周期间隔
private final int msgInterval;//向Master汇报心跳,收集Metrics间隔
private final long maxScannerResultSize;//Scanner执行next返回的数据量阈值,默认设置是Long.MAX_VALUE
private int webuiport = -1;//webServer的端口号
private final long startcode;//HRegiongServer初始化的时间,取自系统时间
private ServerName serverNameFromMasterPOV;//标识Server的名字
private final int rpcTimeout;//定义到HMaster之间的rpc超时时间

在RS上重要的对象列表,如表1所示。
表1RegionServer重要对象的解释

对象名 对应类名 功能描述
hbaseMaster HMasterRegionInterface RS向HMaster汇报信息,提供的RPC客户端
rpcServer RpcServer RS内的Rpc服务器
leases Leases 维护客户访问的租约
compactSplitThread CompactSplitThread RS内执行Compact和Split功能组件
cacheFlusher MemStoreFlusher 负责将Region的MemStore写入文件
compactionChecker Chore 定期检查Region的Compaction过程
hlog HLog 接收Write-Ahead-Log
hlogRoller LogRoller 定期开启新的HLog文件

(2)监听服务组件的初始化与执行。
这个过程初始化以ZooKeeperWatcher为基础的服务,例如监听Master服务节点的MasterAddressManager,标识HBase集群状态的ClusterStatusTracker,以及元数据(-ROOT-, .META.)变化的监听器。启动这些服务可以保证整个集群信息协调一致。
(3)RS服务组件的初始化与执行。
这个过程是初始化compactSplitThread,cacheFlusher,compactionChecker,以及Leases。
(4)尝试连接HMaster,注册RS到HMaster。
(5)周期性收集Metrics和向Master发送心跳。

3 Store相关

Region是RS上的基本数据服务单位,用户表格由1个或者多个Region组成,根据Table的Schema定义,在Region内每个ColumnFamily的数据组成一个Store。每个Store内包括一个MemStore和若干个StoreFile(HFile)组成。如图(3)所示。本小节将介绍Store内的MemStore、StoreFile(HFile)的内部结构与实现。

Region_ms_hfile

图(3) Region-Store结构图

3.1 MemStore原理与实现分析
MemStore是一个内存区域,用以缓存Store内最近一批数据的更新操作。对于Region指定的ColumnFamily下的更新操作(Put、Delete),首先根据是否写WriteAheadLog,决定是否append到HLog文件,然后更新到Store的MemStore中。显然,MemStore的容量不会一直增长下去,因此,在每次执行更新操作时,都会判断RS上所有的MemStore的内存容量是否超过阈值,如果超过阈值,通过一定的算法,选择Region上的MemStore上的数据Flush到文件系统。更详细的处理流程图如图(4)。

Region_flush

图(4) 更新操作的流程图

MemStore类内的重要的成员变量:

<span style="color: #000000; font-weight: bold;">volatile</span> KeyValueSkipListSet kvset<span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//内存中存放更新的KV的数据结构</span>
<span style="color: #000000; font-weight: bold;">volatile</span> KeyValueSkipListSet snapshot<span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//Flush操作时的KV暂存区域</span>
<span style="color: #000000; font-weight: bold;">final</span> ReentrantReadWriteLock lock <span style="color: #339933;">=</span> <span style="color: #000000; font-weight: bold;">new</span> ReentrantReadWriteLock<span style="color: #009900;">&#40;</span><span style="color: #009900;">&#41;</span><span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//Flush操作与kvset之间的可重入读写锁</span>
<span style="color: #000000; font-weight: bold;">final</span> AtomicLong size<span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//跟踪记录MemStore的占用的Heap内存大小</span>
TimeRangeTracker timeRangeTracker<span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//跟踪记录kvset的最小和最大时间戳</span>
TimeRangeTracker snapshotTimeRangeTracker<span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//跟踪记录snapshot的最小和最大时间戳</span>
MemStoreLAB allocator<span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//实际内存分配器</span>
登录后复制

注意 KeyValueSkipListSet是对于jdk提供的ConcurrentSkipListMap的封装,Map结构是的形式。Concurrent表示线程安全。SkipList是一种可以代替平衡树的数据结构,默认是按照Key值升序的。对于ConcurrentSkipListMap的操作的时间复杂度平均在O(logn),设置KeyValue. KVComparator比较KeyValue中Key的顺序。

写入MemStore中的KV,被记录在kvset中。根据JVM内存的垃圾回收策略,在如下条件会触发Full GC。
? 内存满或者触发阈值。
? 内存碎片过多,造成新的分配找不到合适的内存空间。
RS上服务多个Region,如果不对KV的分配空间进行控制的话,由于访问的无序性以及KV长度的不同,每个Region上的KV会无规律地分散在内存上。Region执行了MemStore的Flush操作,再经过JVM GC之后就会出现零散的内存碎片现象,而进一步数据大量写入,就会触发Full-GC。图(5)显示这种假设场景的内存分配过程。
memstore_jvm

图(5) 无处理状态下MemStore内存分配图

为了解决因为内存碎片造成的Full-GC的现象,RegionServer引入了MSLAB(HBASE-3455)。MSLAB全称是MemStore-Local Allocation Buffers。它通过预先分配连续的内存块,把零散的内存申请合并,有效改善了过多内存碎片导致的Full GC问题。
MSLAB的工作原理如下:
? 在MemStore初始化时,创建MemStoreLAB对象allocator。
? 创建一个2M大小的Chunk数组,偏移量起始设置为0。Chunk的大小可以通过参数hbase.hregion.memstore.mslab.chunksize调整。
? 当MemStore有KeyValue加入时,maybeCloneWithAllocator(KeyValue)函数调用allocator为其查找KeyValue.getBuffer()大小的空间,若KeyValue的大小低于默认的256K,会尝试在当前Chunk下查找空间,如果空间不够,MemStoreLAB重新申请新的Chunk。选中Chunk之后,会修改offset=原偏移量+KeyValue.getBuffer().length。chunk内控制每个KeyValue大小由hbase.hregion.memstore.mslab.max.allocation配置。
? 空间检查通过的KeyValue,会拷贝到Chunk的数据块中。此时,原KeyValue由于不再被MemStore引用,会在接下来的JVM的Minor GC被清理。

注意 设置chunk的默认大小以及对于KeyValue大小控制的原因在于,MSLAB虽然会降低内存碎片造成的Full-GC的风险,但是它的使用会降低内存的利用率。如果超过一定大小的KeyValue,此时该KeyValue空间被回收之后,碎片现象不明显。因此,MSLAB只解决小KV的聚合。

MSLAB解决了因为碎片造成Full GC的问题,然而在MemStore被Flush到文件系统时,没有reference的chunk,需要GC来进行回收,因此,在更新操作频繁发生时,会造成较多的Young GC。
针对该问题,HBASE-8163提出了MemStoreChunkPool的解决方案,方案已经被HBase-0.95版本接收。它的实现思路:
? 创建chunk池来管理没有被引用的chunk,不再依靠JVM的GC回收。
? 当一个chunk没有引用时,会被放入chunk池。
? chunk池设置阈值,如果超过了,则会放弃放入新的chunk到chunk池。
? 如果当需要新的chunk时,首先从chunk池中获取。
根据patch的测试显示,配置MemStoreChunkPool之后,YGC降低了40%,写性能有5%的提升。如果是0.95以下版本的用户,可以参考HBASE-8163给出patch。

思考 通过MemStore提供的MSLAB和MemStoreChunkPool给出的解决方案,可以看出在涉及到大规模内存的Java应用中,如何有效地管理内存空间,降低JVM GC对于系统性能造成的影响,成为了一个研究热点。整体上来说,一是设置与应用相适应的JVM启动参数,打印GC相关的信息,实时监控GC对于服务的影响;二是从应用程序设计层面,尽可能地友好地利用内存,来降低GC的影响。

在ChunkPool就是帮助JVM维护了chunk信息,并把那些已经不再MemStore中的数据的chunk重新投入使用。这样就可以避免大量的YGC。

3.2 MemStore参数控制原理与调优
对于任何一个HBase集群而言,都需要根据应用特点对其系统参数进行配置,以达到更好的使用效果。MemStore作为更新数据的缓存,它的大小及处理方式的调整,会极大地影响到写数据的性能、以及随之而来的Flush、Compaction等功能。这种影响的原因在于以下两个方面。
? RS全局的MemStore的大小与Region规模以及Region写数据频度之间的关系。
? 过于频繁的Flush操作对于读数据的影响。
这其中涉及到的可调整的参数如下表。
表MemStore相关的配置参数

参数名称 参数含义 默认值
hbase.regionserver.global.memstore.upperLimit RS内所有MemStore的总和的上限/Heap Size的比例,超过该值,阻塞update,强制执行Flush操作。 0.4
hbase.regionserver.global.memstore.lowerLimit 执行Flush操作释放内存空间,需要达到的比例。 0.35
hbase.hregion.memstore.flush.size 每个MemStore占用空间的最大值,超过该值会执行Flush操作。 128MB
hbase.hregion.memstore.block.multiplier HRegion的更新被阻塞的MemStore容量的倍数。 2
hbase.hregion.preclose.flush.size 关闭Region之前需要执行Flush操作的MemStore容量阈值。 5MB

对于上述参数理解:
(1)RS控制内存使用量的稳定。
例如,假设我们的RS的内存设置为10GB,按照以上参数的默认值,RS用以MemStore的上限为4GB,超出之后,会阻塞整个RS的所有Reigon的请求,直到全局的MemStore总量回落到正常范围之内。
以上涉及到cacheFlusher在MemStore总量使用超过上限时,选择Region进行Flush的算法,由MemStoreFlusher.flushOneForGlobalPressure()算法实现。算法的处理流程如下。
关键的数据结构:

SortedMap<span style="color: #339933;"><</span>Long,HRegion<span style="color: #339933;">></span> regionsBySize <span style="color: #339933;">=</span>
server.<span style="color: #006633;">getCopyOfOnlineRegionsSortedBySize</span><span style="color: #009900;">&#40;</span><span style="color: #009900;">&#41;</span><span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//从RS上获取在线的Region,以及它们在MemStore上使用量,并按照MemStore使用量作为Key,降序。</span>
<span style="color: #003399;">Set</span> excludedRegions <span style="color: #339933;">=</span> <span style="color: #000000; font-weight: bold;">new</span> <span style="color: #003399;">HashSet</span><span style="color: #009900;">&#40;</span><span style="color: #009900;">&#41;</span><span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//记录尝试执行Flush操作失败的Region</span>
…
HRegion bestFlushableRegion <span style="color: #339933;">=</span> getBiggestMemstoreRegion<span style="color: #009900;">&#40;</span>
regionsBySize, excludedRegions, <span style="color: #000066; font-weight: bold;">true</span><span style="color: #009900;">&#41;</span><span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//选出storefile个数不超标、当前MemStore使用量最大的Region</span>
HRegion bestAnyRegion <span style="color: #339933;">=</span> getBiggestMemstoreRegion<span style="color: #009900;">&#40;</span>
regionsBySize, excludedRegions, <span style="color: #000066; font-weight: bold;">false</span><span style="color: #009900;">&#41;</span><span style="color: #339933;">;</span><span style="color: #666666; font-style: italic;">//选出当前MemStore使用量最大的Region</span>
登录后复制

步骤1:RS上在线的Region,按照当前MemStore的使用量进行排序,并存储在regionsBySize中。
步骤2:选出Region下的Store中的StoreFile的个数未达到hbase.hstore.blockingStoreFiles,并且MemStore使用量最大的Region,存储到bestFlushableRegion。
步骤3:选出Region下的MemStore使用量最大的Region,存储到bestAnyRegion对象。
步骤4:如果bestAnyRegion的memstore使用量超出了bestFlushableRegion的两倍,这从另外一个角度说明,虽然当前bestAnyRegion有超过blockingStoreFiles个数的文件,但是考虑到RS内存的压力,冒着被执行Compaction的风险,也选择这个Region作为regionToFlush,因为收益大。否则,直接选择bestFlushableRegion作为regionToFlush。
步骤5:对regionToFlush执行flush操作。如果操作失败,regionToFlush放入excludedRegions,避免该Region下次再次被选中,然后返回步骤2执行,否则程序退出。

(2)设置两个limit,尽可能减少因为控制内存造成数据更新流程的阻塞。
当RS的MemStore使用总量超过(Heap*hbase.regionserver.global.memstore.lowerLimit)的大小时,同样会向cacheFlusher提交一个Flush请求,并以(1)中Region选择算法,对其进行Flush操作。与(1)不同,这个过程中RS不会阻塞RS的写请求。
因此,在生产环境中,我们肯定不希望更新操作被block,一般会配置(upperLimit –lowerlimit)的值在[0.5,0.75]之间,如果是应用写负载较重,可以设置区间内较大的值。

3.3 StoreFile—HFile

该节请参考:HFile文件格式与HBase读写

3.4 Compaction对于服务的影响

该小节请参考:深入分析HBase Compaction机制

本系列文章属于Binos_ICT在Binospace个人技术博客原创,原文链接为http://www.binospace.com/index.php/hbase-in-depth-analysis-of-the-regionserver/,未经允许,不得转载。

 

From Binospace, post HBase深入分析之RegionServer

文章的脚注信息由WordPress的wp-posturl插件自动生成


Copyright © 2008
This feed is for personal, non-commercial use only.
The use of this feed on other websites breaches copyright. If this content is not in your news reader, it makes the page you are viewing an infringement of the copyright. (Digital Fingerprint:
)
相关标签:
来源:php.cn
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板