AIOps 一场颠覆传统运维的盛筵
1446
2022-10-14
Elasticsearch 最佳运维实践 - 总结(二)
一、ElasticSearch使用场景存储ElasticSearch天然支持分布式,具备存储海量数据的能力,其搜索和数据分析的功能都建立在ElasticSearch存储的海量的数据之上;ElasticSearch很方便的作为海量数据的存储工具,特别是在数据量急剧增长的当下,ElasticSearch结合爬虫等数据收集工具可以发挥很大用处
搜索ElasticSearch使用倒排索引,每个字段都被索引且可用于搜索,更是提供了丰富的搜索api,在海量数据下近实时实现近秒级的响应,基于Lucene的开源搜索引擎,为搜索引擎(全文检索,高亮,搜索推荐等)提供了检索的能力。 具体场景:1. Stack Overflow(国外的程序异常讨论论坛),IT问题,程序的报错,提交上去,有人会跟你讨论和回答,全文检索,搜索相关问题和答案,程序报错了,就会将报错信息粘贴到里面去,搜索有没有对应的答案;2. GitHub(开源代码管理),搜索上千亿行代码;3. 电商网站,检索商品;4. 日志数据分析,logstash采集日志,ElasticSearch进行复杂的数据分析(ELK技术,elasticsearch+logstash+kibana);
数据分析ElasticSearch也提供了大量数据分析的api和丰富的聚合能力,支持在海量数据的基础上进行数据的分析和处理。具体场景: 爬虫爬取不同电商平台的某个商品的数据,通过ElasticSearch进行数据分析(各个平台的历史价格、购买力等等);
二、ElasticSearch架构
[[ 小提示:目前市场上开放源代码的最好全文检索引擎工具包就属于 Apache 的 Lucene了。但是 Lucene 只是一个工具包,它不是一个完整的全文检索引擎。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。目前以 Lucene 为基础建立的开源可用全文搜索引擎主要是 Solr 和 Elasticsearch。Solr 和 Elasticsearch 都是比较成熟的全文搜索引擎,能完成的功能和性能也基本一样。但是 ES 本身就具有分布式的特性和易安装使用的特点,而 Solr 的分布式需要借助第三方来实现,例如通过使用 ZooKeeper 来达到分布式协调管理。不管是 Solr 还是 Elasticsearch 底层都是依赖于 Lucene,而 Lucene 能实现全文搜索主要是因为它实现了倒排索引的查询结构。]]
三、ElasticSearch核心概念
对比RDBMS (关系型数据库管理系统)
四、ElasticSearch配置
1. 数据目录和日志目录,生产环境下应与软件分离
#注意:数据目录可以有多个,可以通过逗号分隔指定多个目录。一个索引数据只会放入一个目录中!!path.data: /path/to/data1,/path/to/data2 # Path to log files:path.logs: /path/to/logs # Path to where plugins are installed:path.plugins: /path/to/plugins
2. 所属的集群名,默认为 elasticsearch ,可自定义(最好给生产环境的ES集群改个名字,改名字的目的其实就是防止某台服务器加入了集群这种意外)
cluster.name: kevin_elasticsearch
3. 节点名,默认为 UUID前7个字符,可自定义
node.name: kevin_elasticsearch_node01
4. network.host IP绑定,默认绑定的是["127.0.0.1", "[::1]"]回环地址,集群下要服务间通信,需绑定一个ipv4或ipv6地址或0.0.0.0
network.host: 172.16.60.11
6. transport.tcp.port: 9300-9400 节点间交互的端口, 默认 9300-9400 。可以为它指定一个值或一个区间,当为区间时会取用区间第一个可用的端口。
7. Discovery Config 节点发现配置ES中默认采用的节点发现方式是 zen(基于组播(多播)、单播)。在应用于生产前有两个重要参数需配置
8. discovery.zen.ping.unicast.hosts: ["host1","host2:port","host3[portX-portY]"]单播模式下,设置具有master资格的节点列表,新加入的节点向这个列表中的节点发送请求来加入集群。
9. discovery.zen.minimum_master_nodes: 1这个参数控制的是,一个节点需要看到具有master资格的节点的最小数量,然后才能在集群中做操作。官方的推荐值是(N/2)+1,其中N是具有master资格的节点的数量。
10. transport.tcp.compress: false是否压缩tcp传输的数据,默认false
13. node.master: true 指定该节点是否可以作为master节点,默认是true。ES集群默认是以第一个节点为master,如果该节点出故障就会重新选举master。
14. node.data: true该节点是否存索引数据,默认true。
15. discover.zen.ping.timeout: 3s设置集群中自动发现其他节点时ping连接超时时长,默认为3秒。在网络环境较差的情况下,增加这个值,会增加节点等待响应的时间,从一定程度上会减少误判。
16. discovery.zen.ping.multicast.enabled: false是否启用多播来发现节点。
17. Jvm heap 大小设置生产环境中一定要在jvm.options中调大它的jvm内存。
18. JVM heap dump path 设置生产环境中指定当发生OOM异常时,heap的dump path,好分析问题。在jvm.options中配置:-XX:HeapDumpPath=/var/lib/elasticsearch
五、ElasticSearch安装配置手册
1)环境要求Elasticsearch 6.0版本至少需要JDK版本1.8。可以使用java -version 命令查看JDK版本。
2. 将下载好的安装包上传至需要安装的服务器目录下并解压。# cd /opt/gov# tar -zxvf elasticsearch-6.1.1.tar.gz
3. 解压完毕文件夹elasticsearch-6.1.1 为elasticsearch-6.1.1所在目录。
3)配置Elasticsearch1. 进入Elasticsearch-6.1.1所在目录结构,其中config文件夹为es配置文件所在位置,进入config文件夹。
2. 修改elasticsearch.yml文件中的集群名称和节点名称
cluster.name(集群名称) 和node.name(节点名称) 可以自己配置简单易懂的值。node.name为节点名称,如果多个es实例,可加上数字1.2.3进行区分,方便查看日志和区分节点,简单易懂。如果该es集群用于落地skywalking的apm-collector数据,建议将cluster.name配置为cluster.name: CollectorDBCluster,即该名称需要和skywalking的collector配置文件一致。同一集群下多个es节点的cluster.name应该保持一致。
node.master指定该节点是否有资格被选举成为master,默认是true,elasticsearch默认集群中的第一台启动的机器为master,如果这台机挂了就会重新选举master。node.data指定该节点是否存储索引数据,默认为true。如果节点配置node.master:false并且node.data: false,则该节点将起到负载均衡的作用。
3. 修改elasticsearch.yml文件中的数据和日志保存目录
此处作为实例将data目录和logs目录建立在了elasticsearch-6.1.1下,这样是危险的。当es被卸载,数据和日志将完全丢失。可以根据具体环境将数据目录和日志目录保存到其他路径下以确保数据和日志完整、安全。
4. 修改elasticsearch.yml文件中的network信息
5. 修改elasticsearch.yml文件中的discovery信息
discovery.zen.ping.unicast.hosts: ["192.168.0.8:9300", "192.168.0.9:9300","192.168.0.10:9300"]如果多个es实例组成集群,各节点ip+port信息用逗号分隔。其中port为各es实例中配置的transport.tcp.port。
discovery.zen.minimum_master_nodes: 1设置这个参数来保证集群中的节点可以知道其它N个有master资格的节点。默认为1,对于大的集群来说,可以设置大一点的值(2-4)
6. 在elasticsearch.yml文件末尾添加配置
bootstrap.memory_lock: false是否锁住内存。因为当jvm开始swapping时es的效率会降低,配置为true时要允许elasticsearch的进程可以锁住内存,同时一定要保证机器有足够的内存分配给es。如果不熟悉机器所处环境,建议配置为false。
bootstrap.system_call_filter: falseCentos6不支持SecComp,而ES5.2.0版本默认bootstrap.system_call_filter为true禁用:在elasticsearch.yml中配置bootstrap.system_call_filter为false,注意要在Memory的后面配置该选项。
六、启动Elasticsearch
1. 进入elasticsearch-6.1.1下的bin目录。2. 运行 ./elasticsearch -d 命令。3. 如果出现./elasticsearch: Permission denied ,这时执行没有权限。需要授权执行命令:chmod +x bin/elasticsearch 。4. 再次执行./elasticsearch -d即可启动 5. 在我们配置的es日志目录中,查看日志文件elasticsearch.log,确保es启动成功。
6. 查看elasticssearch进程, 运行"ps -aux|grep elasticsearch" 命令即可
七、Elasticsearch 配置文件详解
elasticsearch的配置文件是在elasticsearch目录下的config文件下的elasticsearch.yml,同时它的日志文件在elasticsearch目录下的logs,由于elasticsearch的日志也是使用log4j来写日志的,所以其配置模式与log4j基本相同。
Cluster部分cluster.name: kevin-elk (默认值:elasticsearch)cluster.name可以确定你的集群名称,当你的elasticsearch集群在同一个网段中elasticsearch会自动的找到具有相同cluster.name 的elasticsearch服务。所以当同一个网段具有多个elasticsearch集群时cluster.name就成为同一个集群的标识。
Node部分node.name: "elk-node01" 节点名,可自动生成也可手动配置。node.master: true (默认值:true) 允许一个节点是否可以成为一个master节点,es是默认集群中的第一台机器为master,如果这台机器停止就会重新选举master。node.client 当该值设置为true时,node.master值自动设置为false,不参加master选举。node.data: true (默认值:true) 允许该节点存储数据。node.rack 无默认值,为节点添加自定义属性。node.max_local_storage_nodes: 1 (默认值:1) 设置能运行的节点数目,一般采用默认的1即可,因为我们一般也只在一台机子上部署一个节点。
配置文件中给出了三种配置高性能集群拓扑结构的模式,如下:workhorse:如果想让节点从不选举为主节点,只用来存储数据,可作为负载器node.master: falsenode.data: truecoordinator:如果想让节点成为主节点,且不存储任何数据,并保有空闲资源,可作为协调器 node.master: truenode.data: falsesearch load balancer:(fetching data from nodes, aggregating results, etc.理解为搜索的负载均衡节点,从其他的节点收集数据或聚集后的结果等),客户端节点可以直接将请求发到数据存在的节点,而不用查询所有的数据节点,另外可以在它的上面可以进行数据的汇总工作,可以减轻数据节点的压力。node.master: falsenode.data: false
Indices部分
index.number_of_shards: 5 (默认值为5) 设置默认索引分片个数。index.number_of_replicas: 1(默认值为1) 设置索引的副本个数
服务器够多,可以将分片提高,尽量将数据平均分布到集群中,增加副本数量可以有效的提高搜索性能。需要注意: "number_of_shards" 是索引创建后一次生成的,后续不可更改设置 "number_of_replicas" 是可以通过update-index-settings API实时修改设置。
Indices Circuit Breakerelasticsearch包含多个circuit breaker来避免操作的内存溢出。每个breaker都指定可以使用内存的限制。另外有一个父级breaker指定所有的breaker可以使用的总内存
indices.breaker.total.limit 所有breaker使用的内存值,默认值为 JVM 堆内存的70%,当内存达到最高值时会触发内存回收。
Field data circuit breaker 允许elasticsearch预算待加载field的内存,防止field数据加载引发异常
indices.breaker.fielddata.limit field数据使用内存限制,默认为JVM 堆的60%。indices.breaker.fielddata.overhead elasticsearch使用这个常数乘以所有fielddata的实际值作field的估算值。默认为 1.03。
请求断路器(Request circuit breaker) 允许elasticsearch防止每个请求的数据结构超过了一定量的内存
indices.breaker.request.limit request数量使用内存限制,默认为JVM堆的40%。indices.breaker.request.overhead elasticsearch使用这个常数乘以所有request占用内存的实际值作为最后的估算值。默认为 1。
Indices Fielddata cache字段数据缓存主要用于排序字段和计算聚合。将所有的字段值加载到内存中,以便提供基于文档快速访问这些值
indices.fielddata.cache.size:unbounded 设置字段数据缓存的最大值,值可以设置为节点堆空间的百分比,例:30%,可以值绝对值,例:12g。默认为无限。该设置是静态设置,必须配置到集群的每个节点。
Indices Node query cachequery cache负责缓存查询结果,每个节点都有一个查询缓存共享给所有的分片。缓存实现一个LRU驱逐策略:当缓存使用已满,最近最少使用的数据将被删除,来缓存新的数据。query cache只缓存过滤过的上下文
indices.queries.cache.size查询请求缓存大小,默认为10%。也可以写为绝对值,例:512m。该设置是静态设置,必须配置到集群的每个数据节点。
Indexing Buffer索引缓冲区用于存储新索引的文档。缓冲区写满,缓冲区的文件才会写到硬盘。缓冲区划分给节点上的所有分片。Indexing Buffer的配置是静态配置,必须配置都集群中的所有数据节点
indices.memory.index_buffer_size允许配置百分比和字节大小的值。默认10%,节点总内存堆的10%用作索引缓冲区大小。indices.memory.min_index_buffer_size如果index_buffer_size被设置为一个百分比,这个设置可以指定一个最小值。默认为 48mb。indices.memory.max_index_buffer_size如果index_buffer_size被设置为一个百分比,这个设置可以指定一个最小值。默认为无限。indices.memory.min_shard_index_buffer_size设置每个分片的最小索引缓冲区大小。默认为4mb。
Indices Shard request cache当一个搜索请求是对一个索引或者多个索引的时候,每一个分片都是进行它自己内容的搜索然后把结果返回到协调节点,然后把这些结果合并到一起统一对外提供。分片缓存模块缓存了这个分片的搜索结果。这使得搜索频率高的请求会立即返回。
注意:请求缓存只缓存查询条件 size=0的搜索,缓存的内容有hits.total, aggregations, suggestions,不缓存原始的hits。通过now查询的结果将不缓存。缓存失效:只有在分片的数据实际上发生了变化的时候刷新分片缓存才会失效。刷新的时间间隔越长,缓存的数据越多,当缓存不够的时候,最少使用的数据将被删除。
缓存过期可以手工设置,例如:
curl -XPOST 'localhost:9200/kimchy,elasticsearch/_cache/clear?request_cache=true'
默认情况下缓存未启用,但在创建新的索引时可启用,例如:
curl -XPUT localhost:9200/my_index -d'{ "settings": { "index.requests.cache.enable": true }} '
当然也可以通过动态参数配置来进行设置:
curl -XPUT localhost:9200/my_index/_settings -d'{ "index.requests.cache.enable": true }'
每请求启用缓存,查询字符串参数request_cache可用于启用或禁用每个请求的缓存。例如:
curl 'localhost:9200/my_index/_search?request_cache=true' -d'{ "size": 0, "aggs": { "popular_colors": { "terms": { "field": "colors" } } }}'
注意:如果你的查询使用了一个脚本,其结果是不确定的(例如,它使用一个随机函数或引用当前时间)应该设置 request_cache=false 禁用请求缓存。
缓存key,数据的缓存是整个JSON,这意味着如果JSON发生了变化 ,例如如果输出的顺序顺序不同,缓存的内容江将会不同。不过大多数JSON库对JSON键的顺序是固定的。
分片请求缓存是在节点级别进行管理的,并有一个默认的值是JVM堆内存大小的1%,可以通过配置文件进行修改。 例如: indices.requests.cache.size: 2%
分片缓存大小的查看方式:
curl 'localhost:9200/_stats/request_cache?pretty&human'
或者
curl 'localhost:9200/_nodes/stats/indices/request_cache?pretty&human'
Indices Recovery
indices.recovery.concurrent_streams 限制从其它分片恢复数据时最大同时打开并发流的个数。默认为 3。indices.recovery.concurrent_small_file_streams 从其他的分片恢复时打开每个节点的小文件(小于5M)流的数目。默认为 2。indices.recovery.file_chunk_size 默认为 512kb。indices.recovery.translog_ops 默认为 1000。indices.recovery.translog_size 默认为 512kb。indices.recovery.compress 恢复分片时,是否启用压缩。默认为 true。indices.recovery.max_bytes_per_sec 限制从其它分片恢复数据时每秒的最大传输速度。默认为 40mb。
Indices TTL interval
indices.ttl.interval 允许设置多久过期的文件会被自动删除。默认值是60s。indices.ttl.bulk_size 设置批量删除请求的数量。默认值为1000。
Paths部分
path.conf: /path/to/conf 配置文件存储位置。path.data: /path/to/data 数据存储位置,索引数据可以有多个路径,使用逗号隔开。path.work: /path/to/work 临时文件的路径 。path.logs: /path/to/logs 日志文件的路径 。path.plugins: /path/to/plugins 插件安装路径 。
Memory部分bootstrap.mlockall: true(默认为false) 锁住内存,当JVM进行内存转换的时候,es的性能会降低,所以可以使用这个属性锁住内存。同时也要允许elasticsearch的进程可以锁住内存,linux下可以通过`ulimit -l unlimited`命令,或者在/etc/sysconfig/elasticsearch文件中取消 MAX_LOCKED_MEMORY=unlimited 的注释即可。如果使用该配置则ES_HEAP_SIZE必须设置,设置为当前可用内存的50%,最大不能超过31G,默认配置最小为256M,最大为1G。
可以通过请求查看mlockall的值是否设定:
如果mlockall的值是false,则设置失败。可能是由于elasticsearch的临时目录(/tmp)挂载的时候没有可执行权限。可以使用下面的命令来更改临时目录:
./bin/elasticsearch -Djna.tmpdir=/path/to/new/dir
Network 、Transport and HTTP 部分
network.bind_host设置绑定的ip地址,可以是ipv4或ipv6的。
network.publish_host设置其它节点和该节点交互的ip地址,如果不设置它会自动设置,值必须是个真实的ip地址。
network.host同时设置bind_host和publish_host两个参数,值可以为网卡接口、127.0.0.1、私有地址以及公有地址。
transport.tcp.port节点通信的绑定端口。可以为一个值或端口范围,如果是一个端口范围,节点将绑定到第一个可用端口。默认为:9300-9400。
transport.tcp.connect_timeout套接字连接超时设置,默认为 30s。
transport.tcp.compress设置为true启用节点之间传输的压缩(LZF),默认为false。
transport.ping_schedule定时发送ping消息保持连接,默认transport客户端为5s,其他为-1(禁用)。
Discovery部分
discovery.zen.minimum_master_nodes: 3预防脑裂(split brain)通过配置大多数节点(总节点数/2+1)。默认为3。
discovery.zen.ping.multicast.enabled: false设置是否打开组播发现节点。默认false。
discovery.zen.ping.unicast.host单播发现所使用的主机列表,可以设置一个属组,或者以逗号分隔。每个值格式为 host:port 或 host(端口默认为:9300)。默认为 127.0.0.1,[::1]。
discovery.zen.ping.timeout: 3s设置集群中自动发现其它节点时ping连接超时时间,默认为3秒,对于比较差的网络环境可以高点的值来防止自动发现时出错。
discovery.zen.join_timeout节点加入到集群中后,发送请求到master的超时时间,默认值为ping.timeout的20倍。
discovery.zen.master_election.filter_client:true当值为true时,所有客户端节点(node.client:true或node.date,node.master值都为false)将不参加master选举。默认值为:true。
discovery.zen.master_election.filter_data:false当值为true时,不合格的master节点(node.data:true和node.master:false)将不参加选举。默认值为:false。
discovery.zen.fd.ping_interval发送ping监测的时间间隔。默认为:1s。
discovery.zen.fd.ping_timeoutping的响应超时时间。默认为30s。
discovery.zen.fd.ping_retriesping监测失败、超时的次数后,节点连接失败。默认为3。
discovery.zen.publish_timeout通过集群api动态更新设置的超时时间,默认为30s。
discovery.zen.no_master_block设置无master时,哪些操作将被拒绝。all 所有节点的读、写操作都将被拒绝。write 写操作将被拒绝,可以读取最后已知的集群配置。默认为:write。
Gateway部分
gateway.expected_nodes: 0设置这个集群中节点的数量,默认为0,一旦这N个节点启动,就会立即进行数据恢复。
gateway.expected_master_nodes设置这个集群中主节点的数量,默认为0,一旦这N个节点启动,就会立即进行数据恢复。
gateway.expected_data_nodes设置这个集群中数据节点的数量,默认为0,一旦这N个节点启动,就会立即进行数据恢复。
gateway.recover_after_time: 5m设置初始化数据恢复进程的超时时间,默认是5分钟。
gateway.recover_after_nodes设置集群中N个节点启动时进行数据恢复。
gateway.recover_after_master_nodes设置集群中N个主节点启动时进行数据恢复。
gateway.recover_after_data_nodes设置集群中N个数据节点启动时进行数据恢复。
八. Elasticsearch常用插件
插件安装方法1
插件安装方法2
插件安装方法1
插件安装方法2
插件安装方法
九、Elasticsearch 的fielddata内存控制、预加载以及circuit breaker断路器
fielddata核心原理fielddata加载到内存的过程是lazy加载的,对一个analzyed field执行聚合时,才会加载,而且是field-level加载的一个index的一个field,所有doc都会被加载,而不是少数doc不是index-time创建,是query-time创建
fielddata内存限制elasticsearch.yml: indices.fielddata.cache.size: 20%,超出限制,清除内存已有fielddata数据fielddata占用的内存超出了这个比例的限制,那么就清除掉内存中已有的fielddata数据默认无限制,限制内存使用,但是会导致频繁evict和reload,大量IO性能损耗,以及内存碎片和gc
监控fielddata内存使用
circuit breaker断路由如果一次query load的feilddata超过总内存,就会oom --> 内存溢出;circuit breaker会估算query要加载的fielddata大小,如果超出总内存,就短路,query直接失败;在elasticsearch.yml文件中配置如下内容:indices.breaker.fielddata.limit: fielddata的内存限制,默认60%indices.breaker.request.limit: 执行聚合的内存限制,默认40%indices.breaker.total.limit: 综合上面两个,限制在70%以内
限制内存使用 (Elasticsearch聚合限制内存使用)
通常为了让聚合(或者任何需要访问字段值的请求)能够快点,访问fielddata一定会快点, 这就是为什么加载到内存的原因。但是加载太多的数据到内存会导致垃圾回收(gc)缓慢, 因为JVM试着发现堆里面的额外空间,甚至导致OutOfMemory (即OOM)异常。
然而让人吃惊的发现, Elaticsearch不是只把符合你的查询的值加载到fielddata. 而是把index里的所document都加载到内存,甚至是不同的 _type 的document。逻辑是这样的,如果你在这个查询需要访问documents X,Y和Z, 你可能在下一次查询就需要访问别documents。而一次把所有的值都加载并保存在内存 , 比每次查询都去扫描倒排索引要更方便。
JVM堆是一个有限制的资源需要聪明的使用。有许多现成的机制去限制fielddata对堆内存使用的影响。这些限制非常重要,因为滥用堆将会导致节点的不稳定(多亏缓慢的垃圾回收)或者甚至节点死亡(因为OutOfMemory异常);但是垃圾回收时间过长,在垃圾回收期间,ES节点的性能就会大打折扣,查询就会非常缓慢,直到最后超时。
如何设置堆大小对于环境变量 $ES_HEAP_SIZE 在设置Elasticsearch堆大小的时候有2个法则可以运用:
1) 不超过RAM的50%Lucene很好的利用了文件系统cache,文件系统cache是由内核管理的。如果没有足够的文件系统cache空间,性能就会变差;
2) 不超过32G如果堆小于32GB,JVM能够使用压缩的指针,这会节省许多内存:每个指针就会使用4字节而不是8字节。把对内存从32GB增加到34GB将意味着你将有更少的内存可用,因为所有的指针占用了双倍的空间。同样,更大的堆,垃圾回收变得代价更大并且可能导致节点不稳定;这个限制主要是大内存对fielddata影响比较大。
Fielddata大小参数 indices.fielddata.cache.size 控制有多少堆内存是分配给fielddata。当你执行一个查询需要访问新的字段值的时候,将会把值加载到内存,然后试着把它们加入到fielddata。如果结果的fielddata大小超过指定的大小 ,为了腾出空间,别的值就会被驱逐出去。默认情况下,这个参数设置的是无限制 — Elasticsearch将永远不会把数据从fielddata里替换出去。
这个默认值是故意选择的:fielddata不是临时的cache。它是一个在内存里为了快速执行必须能被访问的数据结构,而且构建它代价非常昂贵。如果你每个请求都要重新加载数据,性能就会很差。
一个有限的大小强迫数据结构去替换数据。下面来看看什么时候去设置下面的值,首先看一个警告: 这个设置是一个保护措施,而不是一个内存不足的解决方案!
如果你没有足够的内存区保存你的fielddata到内存里,Elasticsearch将会经常性的从磁盘重新加载数据,并且驱逐别的数据区腾出空间。这种数据的驱逐会导致严重的磁盘I/O,并且在内存里产生大量的垃圾,这个会在后面被垃圾回收。
发表评论
暂时没有评论,来抢沙发吧~