阅读更多
Loggly服务底层的很多核心功能都使用了ElasticSearch作为搜索引擎。就像Jon Gifford(译者注:Loggly博客作者之一)在他近期关于“ElasticSearch vs Solr”的文章中所述,日志管理在搜索技术方面产生一些粗暴的需求,坚持下来以后,它必须能够:
引用
在超大规模数据集上可靠地进行准实时索引 – 在我们的案例中,每秒有超过100,000个日志事件与此同时,在该索引上可靠高效地处理超大量的搜索请求

当时我们正在构建Gen2日志管理服务,想保证使用的所有ElasticSearch配置信息,可以获得最优的索引和搜索性能。悲剧的是,我们发现想在ElasticSearch文档里找到这样的信息非常困难,因为它们不只在一个地方。本文总结了我们的学习经验,可作为一个配置属性的参考检查单(checklist)用于优化你自己应用中的ES。

小贴士1:规划索引、分片 以及集群增长情况
ES使得创建大量索引和超大量分片非常地容易,但更重要的是理解每个索引和分片都是一笔开销。如果拥有太多的索引或分片,单单是管理负荷就会影响到ES集群的性能,潜在地也会影响到可用性方面。这里我们专注于管理负荷,但运行大量的索引/分片依然会非常显著地影响到索引和检索性能。

我们发现影响管理负荷的最大因素是集群状态数据的大小,因为它包含了集群中每个索引的所有mapping数据。我们曾经一度有单个集群拥有超过900MB的集群状态数据。该集群虽然在运行但并不可用。

让我们通过一些数据来了解到底发生了什么 。。。。。。

假如有一个索引包含50k的mapping数据(我们当时是有700个字段)。如果每小时生成一个索引,那么每天将增加24 x 50k的集群状态数据,或者1.2MB。如果需要在系统中保留一年的数据,那么集群状态数据将高达438MB(以及8670个索引,43800个分片)。如果与每天一个索引(18.25MB,365个索引,1825个分片)作比较,会看到每小时的索引策略将会是一个完全不同的境况。

幸运的是,一旦系统中有一些真实数据的话,实际上非常容易做这些预测。我们应当能够看到集群必须处理多少状态数据和多少索引/分片。在上到生产环境之前真的应该演练一下,以便防止凌晨3:00收到集群挂掉的电话告警。

在配置方面,我们完全可以控制系统中有多少索引(以及有多少分片),这将让我们远离危险地带。

小贴士2:在配置前了解集群的拓扑结构
Loggly通过独立的master节点和data节点来运行ES。这里不讨论太多的部署细节(请留意后续博文),但为了做出正确的配置选择,需要先确定部署的拓扑结构。

另外,我们为索引和搜索使用单独的ES client节点。这将减轻data节点的一些负载,更重要的是,这样我们的管道就可以和本地客户端通信,从而与集群的其他节点通信。

可通过设置以下两个属性的值为true或false来创建ES的data节点和master节点:

以上是相对容易的部分,现在来看一些值得关注的ES高级属性。对大多数部署场景来说默认设置已经足够了,但如果你的ES使用情况和我们在log管理中遇到的一样难搞,你将会从下文的建议中受益良多。

小贴士3: 内存设置
Linux把它的物理RAM分成多个内存块,称之为分页。内存交换(swapping)是这样一个过程,它把内存分页复制到预先设定的叫做交换区的硬盘空间上,以此释放内存分页。物理内存和交换区加起来的大小就是虚拟内存的可用额度。

内存交换有个缺点,跟内存比起来硬盘非常慢。内存的读写速度以纳秒来计算,而硬盘是以毫秒来计算,所以访问硬盘比访问内存要慢几万倍。交换次数越多,进程就越慢,所以应该不惜一切代价避免内存交换的发生。

ES的mlockall属性允许ES节点不交换内存。(注意只有Linux/Unix系统可设置。)这个属性可以在yaml文件中设置:

在5.x版本中,已经改成了Bootstrap.memory_lock: true.

mlockall默认设置成false,即ES节点允许内存交换。一旦把这个值加到属性文件中,需要重启ES节点才可生效。可通过以下方式来确定该值是否设置正确:

如果你正在设置这个属性,请使用-DXmx选项或ES_HEAP_SIZE属性来确保ES节点分配了足够的内存。

小贴士4:discovery.zen属性控制ElasticSearch的发现协议
Elasticsearch默认使用服务发现(Zen discovery)作为集群节点间发现和通信的机制。Azure、EC2和GCE也有使用其他的发现机制。服务发现由discovery.zen.*开头的一系列属性控制。

在0.x和1.x版本中同时支持单播和多播,且默认是多播。所以要在这些版本的ES中使用单播,需要设置属性discovery.zen.ping.multicast.enabled为false。

从2.0开始往后服务发现就仅支持单播了。

首先需要使用属性discovery.zen.ping.unicast.hosts指定一组通信主机。方便起见,在集群中的所有主机上为该属性设置相同的值,使用集群节点的名称来定义主机列表。

属性discovery.zen.minimum_master_nodes决定了有资格作为master的节点的最小数量,即一个应当“看见”集群范围内运作的节点。如果集群中有2个以上节点,建议设置该值为大于1。一种计算方法是,假设集群中的节点数量为N,那么该属性应该设置为N/2+1。

Data和master节点以两种不同方式互相探测:

通过master节点ping集群中的其他节点以验证他们处于运行状态通过集群中的其他节点ping master节点以验证他们处于运行状态或者是否需要初始化一个选举过程

节点探测过程通过discover.zen.fd.ping_timeout属性控制,默认值是30s,决定了节点将会等待响应多久后超时。当运行一个较慢的或者拥堵的网络时,应该调整这个属性;如果在一个慢速网络中,将该属性调大;其值越大,探测失败的几率就越小。

Loggly的discovery.zen相关属性配置如下:

以上属性配置表示节点探测将在30秒内发生,因为设置了discovery.zen.fd.ping_timeout属性。另外,其他节点应当探测到最少两个master节点(我们有3个master)。我们的单播主机是esmaster01、 esmaster02、esmaster03。

小贴士5:当心DELETE _all
必须要了解的一点是,ES的DELETE API允许用户仅仅通过一个请求来删除索引,支持使用通配符,甚至可以使用_all作为索引名来代表所有索引。例如:

这个特性非常有用,但也非常危险,特别是在生产环境中。在我们的所有集群中,已通过设置action.destructive_requires_name:true来禁用了它。

这项配置在1.0版本中开始引用,并取代了0.90版本中使用的配置属性disable_delete_all_indices。

小贴士6:使用Doc Values
2.0及以上版本默认开启Doc Values特性,但在更早的ES版本中必须显式地设置。当进行大规模的排序和聚合操作时,Doc Values相比普通属性有着明显的优势。本质上是将ES转换成一个列式存储,从而使ES的许多分析类特性在性能上远超预期。

为了一探究竟,我们可以在ES里比较一下Doc Values和普通属性。

当使用一个普通属性去排序或聚合时,该属性会被加载到属性数据缓存中。一个属性首次被缓存时,ES必须分配足够大的堆空间,以便能保存每一个值,然后使用每个文档的值逐步填充。这个过程可能会耗费一些时间,因为可能需要从磁盘读取他们的值。一旦这个过程完成,这些数据的任何相关操作都将使用这份缓存数据,并且会很快。如果尝试填充太多的属性到缓存,一些属性将被回收,随后再次使用到这些属性时将会强制它们重新被加载到缓存,且同样有启动开销。为了更加高效,人们会想到最小化或淘汰,这意味着我们的属性数量将受限于此种方式下的缓存大小。

相比之下,Doc Values属性使用基于硬盘的数据结构,且能被内存映射到进程空间,因此不影响堆使用,同时提供实质上与属性数据缓存一样的性能。当这些属性首次从硬盘读取数据时仍然会有较小的启动开销,但这会由操作系统缓存去处理,所以只有真正需要的数据会被实际读取。

Doc Values因此最小化了堆的使用(因为垃圾收集),并发挥了操作系统文件缓存的优势,从而可进一步最小化磁盘读操作的压力。

小贴士7:ElasticSearch配额类属性设置指南
分片分配就是分配分片到节点的过程,可能会发生在初始化恢复、副本分配、或者集群再平衡的阶段,甚至发生在处理节点加入或退出的阶段。

属性cluster.routing.allocation.cluster_concurrent_rebalance决定了允许并发再平衡的分片数量。这个属性需要根据硬件使用情况去适当地配置,比如CPU个数、IO负载等。如果该属性设置不当,将会影响ES的索引性能。

cluster.routing.allocation.cluster_concurrent_rebalance:2

默认值是2,表示任意时刻只允许同时移动2个分片。最好将该属性设置得较小,以便压制分片再平衡,使其不影响索引。

另一个分片分配相关的属性是cluster.routing.allocation.disk.threshold_enabled。如果该属性设备为true(默认值),在分配分片到一个节点时将会把可用的磁盘空间算入配额内。关闭该属性会导致ES可能分配分片到一个磁盘可用空间不足的节点,从而影响分片的增长。

当打开时,分片分配会将两个阀值属性加入配额:低水位和高水位。
低水位定义ES将不再分配新分片到该节点的磁盘使用百分比。(默认是85%)高水位定义分配将开始从该节点迁移走分片的磁盘使用百分比。(默认是90%)

这两个属性都可以被定义为磁盘使用的百分比(比如“80%”表示80%的磁盘空间已使用,或者说还有20%未使用),或者最小可用空间大小(比如“20GB”表示该节点还有20GB的可用空间)。

如果有很多的小分片,那么默认值就非常保守了。举个例子,如果有一个1TB的硬盘,分片是典型的10GB大小,那么理论上可以在该节点上分配100个分片。在默认设置的情况下,只能分配80个分片到该节点上,之后ES就认为这个节点已经满了。

为得到适合的配置参数,应该看看分片到底在变多大之后会结束他们的生命周期,然后从这里反推,确认包括一个安全系数。在上面的例子中,只有5个分片写入,所以需要一直确保有50GB的可用空间。对于一个1TB的硬盘,这个情形会变成95%的低水位线,并且没有安全系数。额外的,比如一个50%的安全系数,意味着应该确保有75GB的可以空间,或者一个92.5%的低水位线。

小贴士8:Recovery属性允许快速重启
ES有很多恢复相关的属性,可以提升集群恢复和重启的速度。最佳属性设置依赖于当前使用的硬件(硬盘和网络是最常见的瓶颈),我们能给出的最好建议是测试、测试、还是测试。

想控制多少个分片可以在单个节点上同时恢复,使用:

恢复分片是一个IO非常密集的操作,所以应当谨慎调整该值。在5.x版本中,该属性分为了两个:

想控制单个节点上的并发初始化主分片数量,使用:

想控制恢复一个分片时打开的并行流数量,使用:

与流数量密切相关的,是用于恢复的总可用网络带宽:

除了所有这些属性,最佳配置将依赖于所使用的硬件。如果有SSD硬盘以及万兆光纤网络,那么最佳配置将完全不同于使用普通磁盘和千兆网卡。

以上所有属性都将在集群重启后生效。

小贴士9:线程池属性防止数据丢失
ElasticSearch节点有很多的线程池,用于提升一个节点中的线程管理效率。

在Loggly,索引时使用了批量操作模式,并且我们发现通过threadpool.bulk.queue_size属性为批量操作的线程池设置正确的大小,对于防止因批量重试而可能引起的数据丢失是极其关键的。

这会告诉ES,当没有可用线程来执行一个批量请求时,可排队在该节点执行的分片请求的数量。该值应当根据批量请求的负载来设置。如果批量请求数量大于队列大小,就会得到一个下文展示的RemoteTransportException异常。

正如上文所述,一个分片包含一个批量操作队列,所以这个数字需要大于想发送的并发批量请求的数量与这些请求的分片数的乘积。例如,一个单一的批量请求可能包含10个分片的数据,所以即使只发送一个批量请求,队列大小也必须至少为10。这个值设置太高,将会吃掉很多JVM堆空间(并且表明正在推送更多集群无法轻松索引的数据),但确实能转移一些排队情况到ES,简化了客户端。

既要保持属性值高于可接受的负载,又要平滑地处理客户端代码的RemoteTransportException异常。如果不处理该异常,将会丢失数据。我们模拟使用一个大小为10的队列来发送大于10个的批处理请求,获得了以下所示异常。

为2.0版本以前的用户再赠送一个小贴士:最小化Mapping刷新时间

如果你仍在使用2.0版本以前的ES,且经常会更新属性mapping,那么可能会发现集群的任务等待队列有一个较大的refresh_mappings请求数。对它自身来说,这并不坏,但可能会有滚雪球效应严重影响集群性能。

如果确实遇到这种情况,ES提供了一个可配置参数来帮助应对。可按下述方式使用该参数:

那么,这是怎么个意思,为什么可以凑效?

当索引中出现一个新的属性时,添加该属性的数据节点会更新它自己的mapping,然后把新的mapping发送给主节点。如果这个新的mapping还在主节点的等待任务队列中,同时主节点发布了自己的下一个集群状态,那么数据节点将接收到一个过时的旧版本mapping。通常这会让它发送一个更新mapping的请求到主节点,因为直到跟该数据节点有关,主节点一直都拥有错误的mapping信息。这是一个糟糕的默认行为————该节点应该有所行动来保证主节点上拥有正确的mapping信息,而重发新的mapping信息是一个不错的选择。

但是,当有很多的mapping更新发生,并且主节点无法持续坚持时,会有一个乱序聚集(stampeding horde)效应,数据节点发给主节点的刷新消息就可能泛滥。

参数indices.cluster.send_refresh_mapping可以禁用掉默认行为,因此消除这些从数据节点发送到主节点的refresh_mapping请求,可以让主节点保持最新。即时没有刷新请求,主节点也最终会看到最初的mapping变更,并会发布一个包含该变更的集群状态更新。

总结:ElasticSearch的可配置属性是其弹性的关键
对Loggly来讲ElasticSearch可深度配置的属性是一个巨大的优势,因为在我们的使用案例中已经最大限度发挥了ElasticSearch的参数威力(有时更甚)。如果在你自己应用进化的当前阶段ES默认配置工作得足够好了,请放心,随着应用的发展你还会有很大的优化空间。
  • 大小: 41.8 KB
  • 大小: 4 KB
  • 大小: 1.5 KB
  • 大小: 2.3 KB
  • 大小: 5.1 KB
  • 大小: 2.3 KB
  • 大小: 1.6 KB
  • 大小: 3.2 KB
  • 大小: 1.8 KB
  • 大小: 1.4 KB
  • 大小: 1.6 KB
  • 大小: 1.6 KB
  • 大小: 8.7 KB
  • 大小: 2.2 KB
来自: 杨振涛
1
0
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • Go语言开发区块链只需180行代码(推荐)

    主要介绍了Go语言开发区块链只需180行代码,文章中将不会涉及工作量证明算法(PoW)以及权益证明算法(PoS)这类的共识算法。需要的朋友可以参考下

  • Go语言入门经典 源码

    Go语言入门经典 源码

  • Go语言学习笔记(二十一)

    本章介绍如何使用Go语言来处理文件。

  • 推荐几本学习Go语言的书

    最近有一本《Go语言高级编程》的新书上架了,小编觉着有必要把之前的优秀Go语言书也带大家来回顾一下。 最新上架: Go语言高级编程 作者:柴树杉 曹春晖 一本能满足Gopher好奇心的Go语言进阶读物 汇集了作者多年来学习和使用Go语言的经验 更倾向于描述实现细节,极大地满足开发者的探索欲望 本书作者是国内第一批Go语言实践者和Go语言代码贡献者,创建了Go语言中国讨论组,并组织了早期Go语...

  • Go语言学习笔记(六)

    Go语言学习笔记(六)一、数组、切片(slice)和映射1 使用数组2 使用切片2.1 在切片中添加元素2.2 从切片中删除元素2.3 复制切片中的元素3 使用映射(map)3.1 删除映射的元素4 注意事项题外话: 一、数组、切片(slice)和映射 前言,在本章中,将学习关于数组的相关知识 1 使用数组 数组的定义格式如下: var <数组名> [<数组长度>]<数组类型> //e.g. var cheeses [2] string 数组的使用实例,如下: p

  • Go语言学习笔记(三)

    Go语言学习笔记(三)一、理解变量1.1 变量是什么1.1.1 为变量赋值的方法1.2 快捷变量声明1.3 理解变量和零值1.4 编写简短变量声明1.5 变量的声明方式1.6 理解变量作用域1.7 使用指针1.8 声明常量题外话: 一、理解变量 1.1 变量是什么 变量的定义(自己想) Go语言是一种静态类型语言,声明变量必须显式或者隐式地指定其类型。 1.1.1 为变量赋值的方法 声明变量的方法和别的语言类似,可以在声明的时候初始化,也可以先声明在赋值,但是要注意的是,Go语言不支持不同类型变量之间的

  • go语言快速开发入门示例

    https://studygolang.com/ hello world 代码解读 编译代码 运行exe

  • Go语言学习笔记(一)

    Go语言学习笔记(一)1.初识Go语言1.1Go简介1.2 Go是编译型语言1.3 编译型语言和解释型语言的区别1.4 Go是一种静态类型语言2 正式学习2.1 第一个Go程序hello world2.2 Go程序的运行题外话: 1.初识Go语言 1.1Go简介 Go(或Golang)是Google在2007年开发的一种开源编程语言,出自Robert Griesemer、Rob Pike和Ken Thompson之手。2009年11月10日,Google Open Sourse Blog向全球发布了这款语

  • Go语言学习笔记(十六)

    这篇笔记关于调试部分内容

  • 软件工程—软件计划

    当我们想开发一个软件之前,必须有一个详细的计划,而不是脑袋一发热想开发就开发的。就像我们盖房子,当我们有这个想法的时候,必须考虑我们这个想法是否现实,盖房子需要那些材料,需要准备那些东西,有那些问题是我们必须面临和解决的。开发一个软件也是这样,前期的计划是必须做的,也是至关重要的。 点击查看大图 因为我们整个软件工程都是以文档为驱动的,所以我们的软件计划也需要形成两个文档:可行性分...

  • 10分钟入门go语言

    Go是一种新的语言,一种并发的、带垃圾回收的、快速编译的语言,go支持的平台:LinuxFreeBSDMac OS X(也称为 Darwin)Window我们选择在Windows下载安装go环境https://golang.org/dl/  选择windows-amd64.msi完成后双击安装,完成后path中会自动配置go环境以上不用自己配置。完成后进入dos窗口,运行go可以看到支持的命令,并...

  • 盖房子

    <br />【Vijos 1057】盖房子Vijos 2009-11-02 20:50:54 阅读26 评论0 字号:大中小 订阅 .<br />描述 Description<br />永恒の灵魂最近得到了面积为n*m的一大块土地(高兴ING^_^),他想在这块土地上建造一所房子,这个房子必须是正方形的。 <br />但是,这块土地并非十全十美,上面有很多不平坦的地方(也可以叫瑕疵)。这些瑕疵十分恶心,以至于根本不能在上面盖一砖一瓦。 <br />他希望找到一块最大的正方形无瑕疵土地来盖房子。 <br

  • 计算机书籍-Go语言入门经典SAMS Teach Yourself

    书名:Go语言入门经典 作者:[英]乔治 奥尔波(George Ornbo) 出版社:人民邮电出版社 出版时间:2018年08月 去当当网了解

  • 盖房子和软件开发

    今天给家里打电话,老妈说老家的房子快盖好了。结合这段时间学习的过程改进相关的知识,我恍然大悟,原来简单的“盖房子”和软件开发一个道理啊。 老妈说最近老家盖房子的人家很多,但是我家的房子算盖的快的。因为我家准备工作做的充分,材料准备的齐,找好工人以后,实际上只花了几天的时间,就基本完工了。工人们也乐意优先做我家的。 想想“盖房子”实际上和软...

  • Go语言入门

    在学习Go语言编程之前,我们需要安装和配置好Go语言的开发环境。可以选择线上的编译器:http://tour.golang.org/welcome/1 来直接执行代码。也可以在您自己的计算机上安装开发编译环境。 Go本地环境设置 如果您愿意在本地环境安装和配置Go编程语言,则需要在计算机上提供以下两个软件: 文本编辑器Go编译器 文本编辑器 这是用于编写您的程序代码。常见的几个编辑器

  • Go语言学习笔记(二)

    Go语言学习笔记(二)1 命令行参数1.1 os.Args2.Go数据类型2.1类型简介2.2常见类型声明2.3 常见类型2.3.1 布尔类型2.3.2 数值类型2.3.2.1 字符串类型2.3.2.2 数组2.4 检查变量的类型2.5类型转换 1 命令行参数 大部分的程序处理输入,然后产生输出,这就是关于计算的定义,数据的输入可以是程序自己产生,但大部分时候程序的输入来自于外部源:文件、网络连接、其他程序的输出、键盘、命令行参数等 1.1 os.Args Go语言中的os包提供一些函数和变量,以与平台无

  • 《Go语言入门经典》23章读书笔记

    第23章 Go语言时间编程 23.1 时间元素编程 要使用Go语言打印计算机中的当前时间,可使用函数Now。 import( "time" ) time.Now() 23.2 让程序休眠 time.Sleep(3 * time.Second) 23.3 设置超时时间 要在特定的时间过后执行某项操作,可使用函数After。 package main import ( "fmt" "time" ) func main() { fmt.Println("You have 2 seconds

  • Java实战项目——基于ssh实现的博客系统(毕业设计)(前后端源码+论文+数据库+说明文档)25.zip

    ava实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),可运行高分资源 Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现

Global site tag (gtag.js) - Google Analytics