热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

2019你该掌握的开源日志管理平台ELKSTACK

转载于https:www.vtlab.io?p217在企业级开源日志管理平台ELKVSGRAYLOG一文中,我简单阐述了日志管理平台对技术人员的重要性,并把ELKS

 

转载于https://www.vtlab.io/?p=217

在企业级开源日志管理平台ELK VS GRAYLOG一文中,我简单阐述了日志管理平台对技术人员的重要性,并把ELK Stack和Graylog进行了标记。本篇作为“企业级开源日志管理平台”的延伸,基于我在生产环境中的使用经验,向读者介绍ELK Stack的安装与配置。不足之处,还望指正。

架构

Beats工具收集各节的日志,以list数据结构存储在Redis中,Logstash从Redis消费这些数据并在条件匹配及规则过滤后,输出到Elasticsearch,最终通过kibana展示给用户。
architecture

环境介绍

Elastic Stack的产品被设计成需要一起使用,并且版本同步发布,以简化安装和升级过程。本次安装,采用最新的6.5通用版。完整堆栈包括:
1. Beats 6.5
2. Elasticsearch 6.5
3. Elasticsearch Hadoop 6.5(不在本次介绍范围)
4. Kibana 6.5
5. Logstash 6.5
操作系统CentOS7.5,JDK需要8及以上版本。
官方介绍的安装途径包括:tar包安装、rpm包安装、docker安装、yum仓库安装,我使用RPM包安装。

系统设置

Elasticsearch默认监听127.0.0.1,这显然无法跨主机交互。当我们对网络相关配置进行修改后,Elasticsearch由开发模式切换为生产模式,会在启动时进行一系列安全检查,以防出现配置不当导致的问题。
这些检查主要包括:
1. max_map_count:Elasticsearch默认使用混合的NioFs( 注:非阻塞文件系统)和MMapFs( 注:内存映射文件系统)存储索引。请确保你配置的最大映射数量,以便有足够的虚拟内存可用于mmapped文件。此值设置不当,启动Elasticsearch时日志会输出以下错误:
[1] bootstrap checks failed
[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

解决方法:

# vim /etc/sysctl.conf
vm.max_map_count=262144
# sysctl -p
Shell
Copy
  1. 修改最大文件描述符
# vim /etc/security/limits.conf
* soft nofile 65535
* hard nofile 65535
Shell
Copy
  1. 修改最大线程数
# vim /etc/security/limits.conf
* soft nproc 4096
* hard nproc 4096
Shell
Copy

注意:通过RPM包或YUM形式安装,Elasticsearch会自动优化这些参数,如果采用tar包的形式安装,除了手动修改这些配置,还需要创建启动Elasticsearch程序的系统用户,Elasticsearch不允许以root身份运行。

安装

安装顺序:
1. Elasticsearch
2. Kibana
3. Logstash
4. Beats

安装Elasticsearch

  1. 导入Elasticsearch PGP key,使用Elasticsearch签名密钥(PGP key D88E42B4,可从https://pgp.mit.edu获得)和fingerprint对所有包进行签名:
# rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
Shell
Copy
  1. 下载安装RPM包
# wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.5.4.rpm
# wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.5.4.rpm.sha512
# sha512sum -c elasticsearch-6.5.4.rpm.sha512 #比较RPM包的SHA和应输出的已发布校验和,输出 elasticsearch-6.5.4.rpm: OK
# rpm --install elasticsearch-6.5.4.rpm
Shell
Copy
  1. 配置文件
    Elasticsearch包含三个配置文件:
    elasticsearch.yml:配置Elasticsearch
    jvm.options:设置Elasticsearch堆栈大小,对JVM进行优化
    log4j2.properties:定义Elasticsearch日志
    这些文件的默认位置取决于安装方式。tar包形式的配置文件目录$ES_HOME/config,RPM包默认位置/etc/elasticsearch/。
    elasticsearch.yml文件采用yaml格式,以修改路径为例:
path:
    data: /var/lib/elasticsearch
    logs: /var/log/elasticsearch
or
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
YAML
Copy

elasticsearch.yml文件也支持环境变量,引用环境变量的方法${…},例如:

node.name: ${HOSTNAME}
network.host: ${ES_NETWORK_HOST}
YAML
Copy

jvm.options文件包含以下格式:
行分割;
空行被忽略;
以#开头表示注释;
以-开头的行被视为独立于JVM版本,此项配置会影响所有版本的JVM;
数字开头,后面跟一个:和一个-号的行,只会影响匹配此数字的JVM版本,例如:8:-Xmx2g,只会影响JDK8;
数字开头跟一个-号再跟一个数字再跟一个:,定义两个版本之间,且包含这两个版本,例如8-9:-Xmx2g,影响JDK8,JDK9。
注意:在配置中,应保证JVM堆栈min和max的值相同,Xms表示总堆栈空间的初始值,XmX表示总堆栈空间的最大值。

# cat /etc/elasticsearch/elasticsearch.yml

cluster.name: vtlab  #集群名称,只有cluster.name相同时,节点才能加入集群。请设置为具有描述性的名字。不建议在不同环境中使用相同的集群名。
node.name: vtlab01  #节点描述名称,默认情况下,Elasticsearch将使用随机生成的UUID的前7个字符作为节点id。设为服务器的主机名 node.name: ${HOSTNAME}
node.attr.rack: r1  #指定节点的部落属性,机架位置,比集群范围更大。
path.data: /var/lib/elasticsearch  #Elasticsearch的数据文件存放目录 如果是默认位置,在将Elasticsearch升级到新版本时,很可能会把数据删除。
path.logs: /var/log/elasticsearch  #日志目录
bootstrap.memory_lock: true  #启动后锁定内存,禁用swap交换,提高ES性能。伴随这个参数还需要调整其他配置,后面讨论。
network.host: 0.0.0.0  #指定监听的地址
http.port: 9200  #监听的WEB端口
discovery.zen.ping.unicast.hosts:  #默认网络配置中,Elasticsearch将绑定到回环地址,并扫描9300-9305端口,试图连接同一台服务器上的其他节点,可以自动发现并加入集群。
    - 10.0.0.46:9300  #此端口为TCP传输端口,用于集群内节点发现、节点间信息传输、ES Java API也是通过此端口传输数据,transport.tcp.port定义。9200为HTTP端口。
    - 10.0.0.28:9300
    - 10.0.0.29:9300
    - host1.vtlab.io
discovery.zen.minimum_master_nodes: 2  #为防止数据丢失,discovery.zen.minimum_master_nodes设置至关重要,主节点的最小选举数。避免脑裂,应将此值设为(master_eligible_nodes / 2) + 1,换言之,如果有3个节点,(3/2)+1 or 2
#以下选项仅在完全重启集群时生效
#本地网关模块在整个群集重新启动时存储群集状态和分片数据。
#以下静态设置必须在每个主节点上设置,控制新选择的主节点在试图恢复集群状态和集群数据之前应该等待多长时间:
gateway.expected_nodes: 0  #集群中预期的(数据或主)节点数量。一旦加入集群的节点数量达到预期,本地碎片的恢复就会开始。默认值为0
gateway.expected_master_nodes: 0  #集群中预期的主节点数量。一旦加入集群的主节点数量达到预期,本地碎片的恢复就会开始。默认值为0
gateway.expected_data_nodes: 0  #集群中预期的数据节点数量。一旦预期的数据节点数量加入集群,本地碎片的恢复就会开始。默认值为0
gateway.recover_after_time: 5  #如果没有达到预期的节点数量,则恢复过程将等待配置的时间量,然后再尝试恢复。如果配置了一个expected_nodes设置,则默认值为5m。
gateway.recover_after_nodes: 1  #只要有这么多数据或主节点加入集群,就可以恢复。
gateway.recover_after_master_nodes: 1  #只要有这么多主节点加入集群,就可以恢复。
gateway.recover_after_data_nodes: 1  #只要有这么多数据节点加入集群,就可以恢复。
action.destructive_requires_name: true  #禁用通过api以通配符删除所有索引。删除索引时需要指定被删除的索引名称。
YAML
Copy

配置文件中,我将bootstrap.memory_lock的值设为了true,启动时遇到了以下错误:
Elasticsearch process memory locking failed
解决此问题,还需要修改三个地方:
1. /etc/sysconfig/elasticsearch

ES_JAVA_OPTS="-Xms4g -Xmx4g" 
MAX_LOCKED_MEMORY=unlimited
Shell
Copy

替换4g为总内存的一半(Elasticsearch官方建议是主机总内存的一半)
2. /etc/security/limits.conf

elasticsearch soft memlock unlimited
elasticsearch hard memlock unlimited
Shell
Copy

需要将elasticsearch替换为运行Elasticsearch程序的用户
3. /usr/lib/systemd/system/elasticsearch.service
取消服务脚本文件/usr/lib/systemd/system/elasticsearch.service中对LimitMEMLOCK=infinity的注释

LimitMEMLOCK=infinity
Shell
Copy

然后运行systemctl daemon-reload命令

# systemctl daemon-reload
Shell
Copy
  1. 启动节点
# systemctl enable elasticsearch
# systemctl start elasticsearch
Shell
Copy

Elasticsearch安装完毕,接下来安装Kibana。

安装Kibana

同样使用RPM的形式安装
1. 安装公钥

# rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
Shell
Copy
  1. 下载rpm包
# wget https://artifacts.elastic.co/downloads/kibana/kibana-6.5.4-x86_64.rpm
# sha512sum kibana-6.5.4-x86_64.rpm 
# rpm --install kibana-6.5.4-x86_64.rpm
Shell
Copy
  1. 配置文件
    Kibana启动时从/etc/kibana/kibana.yml文件里读取配置信息,配置文件内容如下:
# Default: 5601。定义Kibana后端服务启动时监听的端口。
server.port: 5601
# Default: "localhost"。后端服务监听的地址,需要修改为网卡地址
server.host: "localhost"
# 如果前面有代理,可以指定安装Kibana的路径。
# 使用server.rewriteBasePath设置告诉Kibana是否应从其收到的请求中删除basePath,并防止在启动时发生弃用警告。
# 此设置不能以斜杠(/)结尾。
server.basePath: ""
# Default: false 
# 指定Kibana是否应重写以server.basePath为前缀的请求,或者要求它们由反向代理重写。
# 在Kibana 6.3之前,此设置实际上始终为false,在Kibana 7.0中默认为true
server.rewriteBasePath: false
# Default: 1048576
# 请求服务的最大有效负载,单位字节
server.maxPayloadBytes: 1048576
# Default: "your-hostname"。Kibana服务器的名称,用于显示目的。
server.name: "your-hostname"
# 用于查询的Elasticsearch实例的URL
elasticsearch.url: "http://localhost:9200"
# Default: true
# 值为true时,Kibana使用server.host设置中指定的主机名。值为false时,Kibana使用连接到此Kibana实例的主机的主机名
elasticsearch.preserveHost: true
# Default: ".kibana" 
# Kibana使用此索引名存储Elasticsearch中已保存的搜索、可视化和仪表板。
# 如果索引尚不存在,Kibana会创建一个新索引。
kibana.index: ".kibana"
# 要加载的默认应用程序。
kibana.defaultAppId: "home"
# 如果Elasticsearch设置了认证,Kibana通过这两个选项进行Elasticsearch的验证。
elasticsearch.username: "user"
elasticsearch.password: "pass"
# Default: "false"
# 从Kibana服务器到浏览器的传出请求启用SSL,当设置为True时,server.ssl.certificate and server.ssl.key必须设置
server.ssl.enabled: false
server.ssl.certificate: /path/to/your/server.crt
server.ssl.key: /path/to/your/server.key
# 可选设置,提供PEM格式SSL证书和密钥文件的路径。
# 这些文件用于验证Kibana到Elasticsearch的身份,并需要在Elasticsearch中的xpack.ssl.verification_mode设置为certificate或full
elasticsearch.ssl.certificate: /path/to/your/client.crt
elasticsearch.ssl.key: /path/to/your/client.key
# 可选设置,使您可以为Elasticsearch实例的证书颁发机构指定PEM文件的路径列表。
elasticsearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ]
# 控制Elasticsearch提供的证书验证。有效值为none,certificate和full。完整执行主机名验证,证书不执行
elasticsearch.ssl.verificationMode: full
# Defau:elasticsearch.requestTimeout 设置的值,等待Elasticsearch响应ping的时间,单位ms。
elasticsearch.pingTimeout: 1500
# Default:30000ms
# 等待来自后端或Elasticsearch的响应时间,必须是正整数。单位ms。
elasticsearch.requestTimeout: 30000
# Default: [ \'authorization\' ]
# 要发送到Elasticsearch的Kibana客户端标头列表。要不发送客户端标头,请将此值设置为[](空列表)。
elasticsearch.requestHeadersWhitelist: [ authorization ]
# Default: {}
# 要发送到Elasticsearch的标头名称和值。无论elasticsearch.requestHeadersWhitelist配置如何,客户端标头都不能覆盖任何自定义标头。
elasticsearch.customHeaders: {}
# Default: 30000
# 对Elasticsearch等待碎片响应的时间(以毫秒为单位)。设置为0禁用
elasticsearch.shardTimeout: 30000
# Default: 5000
# 在重试之前在Kibana启动时等待Elasticsearch的时间(以毫秒为单位)
elasticsearch.startupTimeout: 5000
# Default false记录发送到Elasticsearch的查询。需要将logging
# 记录发送到Elasticsearch的查询。需要将logging.verbose设置为true。
# 这对于查看当前没有检查器的应用程序生成的查询DSL非常有用,例如Timelion和Monitoring。
elasticsearch.logQueries: false
# PID文件路径
pid.file: /var/run/kibana.pid
# Default: stdout
# 日志输出文件路径
logging.dest: stdout
# Default: false
# 此值设为true时,禁止记录日志
logging.silent: false
# Default: false
# 当设置为true时,仅记录error日志
logging.quiet: false
# Default: false
# 将此值设置为true以记录所有事件,包括系统使用信息和所有请求。
logging.verbose: false
# Default: 5000
# 设置以毫秒为单位的间隔来采样系统和处理性能指标。最小值是100。
ops.interval: 5000
YAML
Copy
  1. 启动
# systemctl enable kibana
# systemctl start kibana
Shell
Copy

安装Logstash

  1. 下载
# wget https://artifacts.elastic.co/downloads/logstash/logstash-6.5.4.rpm
# rpm --install logstash-6.5.4.rpm
Shell
Copy

安装Filebeat

Filebeat客户端是一个轻量级,资源消耗较低的工具,它从服务器的文件中收集日志,并将这些日志转发到Logstash进行处理。
1. 下载filebeat

# wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-6.5.4-x86_64.rpm
# rpm -vi filebeat-6.5.4-x86_64.rpm
Shell
Copy
  1. 配置文件
vim /etc/filebeat/filebeat.yml
# 定义获取日志的路径,可以是目录或文件:
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/messages
  tags: [syslog]
- type: log
  enabled: true
  paths:
    - /data/logs/nginx/vtlab/*.log
  tags: [nginx_access]
# 配置输出
output.redis:
  enable: true                  #默认true,是否开启传输
  hosts: ["10.0.0.26:6379"]     #redis服务的IP、端口
  db: 1                         #选择redis哪个库
  timeout: 5                    #连接redis超时时间,默认5s
  key: filter_index             #数据被传输到redis list的名称
  #password: password           #连接redis的密码,如果redis配置文件中没有设置,则此处不需要填写
YAML
Copy
  1. 启动Filebeat
# systemctl enable filebeat.service
# systemctl start filebeat.service
Shell
Copy

ELK各组件安装完成,开始日志收集相关的配置。

Logstash pipeline

Logstash管道包含两个必要元素Inputs和Outputs,以及一个可选元素Filter。Inputs接收、消费元数据,Filter根据设定过滤数据,Outputs将数据输出到指定程序,我这里定义的是输出到Elasticsearch。
我配置了Filebeat输出message日志和www.vtlab.io 的访问日志,现在创建Logstash管道接收这些数据。

# vim first-pipeline.conf
# 定义Input
input {
    redis {
        host => "10.0.0.26"
        type => "redis-input"
        data_type => "list"
        db => 1
        key => "filter_index"
        port => 6379
        #password => "password"
    }
}

# 定义Filter
filter{
    if "syslog" in [tags] {
         grok {
            match => {"message" => "%{SYSLOGBASE2}"}
            remove_field => "beat.version"
            remove_field => "beat.name"
        }
    }
    if "nginx_access" in [tags] {
        grok {
            match => { "message" => "%{COMBINEDAPACHELOG}" }
            remove_field => "beat.version"
            remove_field => "beat.name"
        }
        geoip {
           source => "clientip"
          # fields => ["city_name", "country_code2", "country_name", "latitude", "longitude", "region_name"]
          # remove_field => ["[geoip][latitude]", "[geoip][longitude]"]
        }
    }
}

# 定义output
output{
    if "syslog" in [tags] {
        elasticsearch {
            hosts => [ "10.0.0.28:9200","10.0.0.29:9200","10.0.0.46:9200" ]
            index => "message-%{+YYYY.MM.dd}"
        }
    }
    if "nginx_access" in [tags] {
        elasticsearch {
            hosts => [ "10.0.0.28:9200","10.0.0.29:9200","10.0.0.46:9200" ]
            index => "logstash-nginx-%{+YYYY.MM.dd}"
        }
    }
}
YAML
Copy

配置文件中,我定义了两个patterns:”message” => “%{SYSLOGBASE2}”与”message” => “%{COMBINEDAPACHELOG}”,一个匹配系统messages日志,一个匹配Nginx访问日志,其他日志类型,就需要不同的Patterns了。ELK提供了很多默认的Patterns,也可以自定义。

定义Patterns

一个简单的方法,通过以下两个步骤实现:
1. 打开Grokdebug discover页面,输入日志内容到文本框,点击discover按钮,如下图:
patterns
2. 打开Grokdebug页面,按图中步骤操作,输出内容就是Patterns。
patterns

启动Logstash

  1. 启动前,检验first-pipeline.conf配置文件的语法是否正确:
# /usr/share/logstash/bin/logstash -f /etc/logstash/conf.d/first-pipeline.conf --config.test_and_exit
Configuration OK
[INFO ] 2019-01-07 17:23:32.527 [LogStash::Runner] runner - Using config.test_and_exit mode. Config Validation Result: OK. Exiting Logstash
Shell
Copy

输出中包含以上内容,说明配置文件正确。
2. 启动指定pipeline:
–config.reload.automatic:开启自动加载配置,这样在每次修改配置文件时会自动加载配置文件

# /usr/share/logstash/bin/logstash -f /etc/logstash/conf.d/first-pipeline.conf --config.reload.automatic
Shell
Copy

Kibana UI

日志流已经通过Filebeat、Redis、Logstash进入了Elasticsearch,我们通过Kibana对Elasticsearch做一些可视化的管理,方便查看日志。

创建索引

登录Kibana页面,在浏览器中输入 http://10.0.0.21:5601 (10.0.0.21为部署Kibana主机的IP地址,5601是Kibana监听的端口),如图:
Kibana
点击“Management”–>“Index Patterns”–>“Create index pattern”,在Index pattern框中填写Index name,也就是我们通过Logstash输出到Elasticsearch时的索引名”message-*”和”logstash-nginx-*”,点击Next step,在“Time Filter field name”中选择“@timestamp”,最后点击“Create index pattern”完成索引创建。
经过以上的步骤,就可以在Kibana的discover中依照索引查看日志了。

通过GeoIP展示用户分布

我在Logstash的filter插件部分进行了GeoIP相关的配置,现在我演示下将用户的分布按照地图展示出来。Kibana的图形化可谓丰富多姿,其他的就交给读者自己探索了。
1. 点击左侧导航栏“Dashboard”–>“Create new dashboard”
2. 点击页面中间的“add”标签,如果之前在Visualize中创建过地图,可以通过搜索名字添加,没有的话,需要先在Visualize中创建
3. 点击“Add new Visualization”,选择Maps下的“Coordinate Map”
4. 选择我们需要创建地图的索引“logstash-nginx”
5. 点击“Geo Coordinates”,选择“Geohash”
6. 点击“Options”调整颜色及图例说明位置
7. 点击页面右上角的“Save”按钮,创建完成
user_map

安全

细心的读者朋友可能已经发现了,登录kibana的时候,页面并没有验证功能,任何能访问Kibana地址的人,都能查询日志,这对我们来说是不可接受的。
改进方法:
1. X-Pack:Kibana本身不提供认证机制,需要通过X-Pack插件来实现。X-Pack是付费插件,可以申请License获取一年期的免费试用。
2. Nginx:通过htpasswd创建用户及密码,进行Kibana认证。

结尾

ELK Stack包含的功能太多了,这里只介绍了些常用功能,足以应付日常所需,更多功能,还需深入探索。


推荐阅读
  • Ansible:自动化运维工具详解
    Ansible 是一款新兴的自动化运维工具,基于 Python 开发,集成了多种运维工具(如 Puppet、CFEngine、Chef、Func 和 Fabric)的优点,实现了批量系统配置、程序部署和命令执行等功能。本文将详细介绍 Ansible 的架构、特性和优势。 ... [详细]
  • 从0到1搭建大数据平台
    从0到1搭建大数据平台 ... [详细]
  • 服务器部署中的安全策略实践与优化
    服务器部署中的安全策略实践与优化 ... [详细]
  • 持续集成持续部署持续交付今天,我将谈论开发人员的一个误解:持续集成是关于运行自动化集成管道的…什么是持续集成(CI) ... [详细]
  • Nacos 0.3 数据持久化详解与实践
    本文详细介绍了如何将 Nacos 0.3 的数据持久化到 MySQL 数据库,并提供了具体的步骤和注意事项。 ... [详细]
  • 本文介绍 DB2 中的基本概念,重点解释事务单元(UOW)和事务的概念。事务单元是指作为单个原子操作执行的一个或多个 SQL 查询。 ... [详细]
  • 微服务优雅上下线的最佳实践
    本文介绍了微服务上下线的正确姿势,避免使用 kill -9 等粗暴手段,确保服务的稳定性和可靠性。 ... [详细]
  • 本文节选自《NLTK基础教程——用NLTK和Python库构建机器学习应用》一书的第1章第1.2节,作者Nitin Hardeniya。本文将带领读者快速了解Python的基础知识,为后续的机器学习应用打下坚实的基础。 ... [详细]
  • 本文将带你快速了解 SpringMVC 框架的基本使用方法,通过实现一个简单的 Controller 并在浏览器中访问,展示 SpringMVC 的强大与简便。 ... [详细]
  • 双指针法在链表问题中应用广泛,能够高效解决多种经典问题,如合并两个有序链表、合并多个有序链表、查找倒数第k个节点等。本文将详细介绍这些应用场景及其解决方案。 ... [详细]
  • DAO(Data Access Object)模式是一种用于抽象和封装所有对数据库或其他持久化机制访问的方法,它通过提供一个统一的接口来隐藏底层数据访问的复杂性。 ... [详细]
  • IOS Run loop详解
    为什么80%的码农都做不了架构师?转自http:blog.csdn.netztp800201articledetails9240913感谢作者分享Objecti ... [详细]
  • 本文详细介绍了 InfluxDB、collectd 和 Grafana 的安装与配置流程。首先,按照启动顺序依次安装并配置 InfluxDB、collectd 和 Grafana。InfluxDB 作为时序数据库,用于存储时间序列数据;collectd 负责数据的采集与传输;Grafana 则用于数据的可视化展示。文中提供了 collectd 的官方文档链接,便于用户参考和进一步了解其配置选项。通过本指南,读者可以轻松搭建一个高效的数据监控系统。 ... [详细]
  • 如何在Windows内置的Ubuntu系统中更改SSH服务的端口号设置
    如何在Windows内置的Ubuntu系统中更改SSH服务的端口号设置 ... [详细]
  • 为了确保iOS应用能够安全地访问网站数据,本文介绍了如何在Nginx服务器上轻松配置CertBot以实现SSL证书的自动化管理。通过这一过程,可以确保应用始终使用HTTPS协议,从而提升数据传输的安全性和可靠性。文章详细阐述了配置步骤和常见问题的解决方法,帮助读者快速上手并成功部署SSL证书。 ... [详细]
author-avatar
菜蕻的薇笑2602929033
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有