文章目录
前言
本篇文章基于 k8s 集群部署 gitlab、sonarQube、Jenkins 等工具,并把上述工具集成到 Jenkins 中,以 Django 项目和 SpringBoot 项目为例,通过多分支流水线及 Jenkinsfile 实现项目代码提交到不同的仓库分支,实现自动代码扫描、单元测试、docker 容器构建、k8s 服务的自动部署。
1. DevOps介绍
Continuous Integration (CI) 和 Continuous Delivery (CD)
传统的软件交付流程如下:
一个软件从零开始到最终交付,大概包括以下几个阶段:规划、编码、构建、测试、发布、部署和维护,基于这些阶段,我们的软件交付模型大致经历了以下几个阶段。
🍑 瀑布式流程
前期需求确立之后,软件开发人员花费数周和数月编写代码,把所有需求一次性开发完,然后将代码交给 QA(质量保障)团队进行测试,然后将最终的发布版交给运维团队去部署。
瀑布模型,简单来说,就是等一个阶段所有工作完成之后,再进入下一个阶段。这种模式的问题也很明显,产品迭代周期长,灵活性差。一个周期动辄几周几个月,适应不了当下产品需要快速迭代的场景。
🍑 敏捷开发
任务由大拆小,开发、测试协同工作,注重开发敏捷,不重视交付敏捷。
🍑 DevOps
开发、测试、运维协同工作,持续开发+持续交付。
我们是否可以认为 DevOps = 提倡开发、测试、运维协同工作来实现持续开发、持续交付的一种软件交付模式?
大家想一下为什么最初的开发模式没有直接进入 DevOps 的时代?
原因是:沟通成本。
各角色人员去沟通协作的时候都是手动去做,交流靠嘴,靠人去指挥,很显然会出大问题。所以说不能认为 DevOps 就是一种交付模式,因为解决不了沟通协作成本,这种模式就不具备可落地性。
那 DevOps 时代如何解决角色之间的成本问题?DevOps 的核心就是自动化。自动化的能力靠什么来支撑,工具和技术。
DevOps工具链
靠这些工具和技术,才实现了自动化流程,进而解决了协作成本,使得 devops 具备了可落地性。因此我们可以大致给 devops 一个定义:
devops = 提倡开发、测试、运维协同工作来实现持续开发、持续交付的一种软件交付模式 + 基于工具和技术支撑的自动化流程的落地实践。
因此 devops 不是某一个具体的技术,而是一种思想+自动化能力,来使得构建、测试、发布软件能够更加地便捷、频繁和可靠的落地实践。
本篇文章的核心内容就是要教会大家如何利用工具和技术来实现完整的 DevOps 平台的建设。我们主要使用的工具有:
- gitlab,代码仓库,企业内部使用最多的代码版本管理工具。
- Jenkins, 一个可扩展的持续集成引擎,用于自动化各种任务,包括构建、测试和部署软件。
- robotFramework, 基于Python的自动化测试框架
- sonarqube,代码质量管理平台
- maven,java包构建管理工具
- Kubernetes
- Docker
2. Jenkins初体验
关于 Jenkins 的介绍这里不过多叙述,除了下面我自己的部署方案,大家还可以参考这个网站:Jenkins部署方案
🍑 K8s环境中部署jenkins
安装的注意点:
- 第一次启动很慢
- 因为后面 Jenkins 会与 kubernetes 集群进行集成,会需要调用 kubernetes 集群的 api,因此安装的时候创建了 ServiceAccount 并赋予了 cluster-admin 的权限
- 初始化容器来设置权限
- ingress 来外部访问
- 数据存储通过 pvc 挂载到宿主机中
(1)jenkins/jenkins-all.yaml
文件
apiVersion: v1
kind: Namespace
metadata:
name: jenkins
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: jenkins
namespace: jenkins
spec:
accessModes:
- ReadWriteOnce
storageClassName: nfs
resources:
requests:
storage: 200Gi
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
namespace: jenkins
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: jenkins-crb
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: jenkins
namespace: jenkins
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jenkins-master
namespace: jenkins
spec:
replicas: 1
selector:
matchLabels:
devops: jenkins-master
template:
metadata:
labels:
devops: jenkins-master
spec:
serviceAccount: jenkins #Pod 需要使用的服务账号
initContainers:
- name: fix-permissions
image: busybox
command: ["sh", "-c", "chown -R 1000:1000 /var/jenkins_home"]
securityContext:
privileged: true
volumeMounts:
- name: jenkinshome
mountPath: /var/jenkins_home
containers:
- name: jenkins
image: jenkinsci/blueocean:1.24.7
imagePullPolicy: IfNotPresent
ports:
- name: http #Jenkins Master Web 服务端口
containerPort: 8080
- name: slavelistener #Jenkins Master 供未来 Slave 连接的端口
containerPort: 50000
volumeMounts:
- name: jenkinshome
mountPath: /var/jenkins_home
env:
- name: JAVA_OPTS
value: "-Xms4096m -Xmx5120m -Duser.timezone=Asia/Shanghai -Dhudson.model.DirectoryBrowserSupport.CSP="
volumes:
- name: jenkinshome
persistentVolumeClaim:
claimName: jenkins
---
apiVersion: v1
kind: Service
metadata:
name: jenkins
namespace: jenkins
spec:
ports:
- name: http
port: 8080
targetPort: 8080
- name: slavelistener
port: 50000
targetPort: 50000
type: ClusterIP
selector:
devops: jenkins-master
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jenkins-web
namespace: jenkins
spec:
rules:
- host: jenkins.luffy.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jenkins
port:
number: 8080
(2)创建服务:
## 部署服务
$ kubectl create -f jenkins-all.yaml
## 查看服务
$ kubectl -n jenkins get po
NAME READY STATUS RESTARTS AGE
jenkins-master-767df9b574-lgdr5 1/1 Running 0 20s
# 查看日志,第一次启动提示需要完成初始化设置
$ kubectl -n jenkins logs -f jenkins-master-767df9b574-lgdr5
......
*************************************************************
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
5396b4e1c395450f8360efd8ee641b18
This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
*************************************************************
(3)访问服务:
配置 hosts
解析,172.21.51.143 jenkins.luffy.com
,然后使用浏览器域名访问服务。第一次访问需要大概几分钟的初始化时间。
使用 jenkins 启动日志中的密码,或者执行下面的命令获取解锁的管理员密码:
$ kubectl -n jenkins exec jenkins-master-767df9b574-lgdr5 bash
/ # cat /var/jenkins_home/secrets/initialAdminPassword
35b083de1d25409eaef57255e0da481a
点击叉号,跳过选择安装推荐的插件环节,直接进入 Jenkins。由于默认的插件地址安装非常慢,我们可以替换成国内清华的源,进入 jenkins 工作目录,目录下面有一个 updates 的目录,下面有一个 default.json 文件,我们执行下面的命令替换插件地址:
$ cd /var/jenkins_home/updates
$ sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json
$ sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json
注意:暂时先不用重新启动 pod,汉化后一起重启。
选择右上角 admin->configure->password
重新设置管理员密码,设置完后,会退出要求重新登录,使用 admin/xxxxxx
(新密码),登录即可。
🍑 安装汉化插件
Jenkins -> manage Jenkins -> Plugin Manager -> Avaliable
,搜索 chinese
关键字
选中后,选择 [Install without restart]
,等待下载完成,然后点击 [ Restart Jenkins when installation is complete and no jobs are running ]
,让 Jenkins 自动重启
启动后,界面默认变成中文。
3. Jenkins基本使用演示
🍑 演示目标
- 代码提交 gitlab,自动触发 Jenkins 任务
- Jenkins 任务完成后发送钉钉消息通知
🍑 演示准备
gitlab 代码仓库搭建
## 全量部署的组件
$ gitlab-ctl status
run: alertmanager: (pid 1987) 27s; run: log: (pid 1986) 27s
run: gitaly: (pid 1950) 28s; run: log: (pid 1949) 28s
run: gitlab-exporter: (pid 1985) 27s; run: log: (pid 1984) 27s
run: gitlab-workhorse: (pid 1956) 28s; run: log: (pid 1955) 28s
run: logrotate: (pid 1960) 28s; run: log: (pid 1959) 28s
run: nginx: (pid 2439) 1s; run: log: (pid 1990) 27s
run: node-exporter: (pid 1963) 28s; run: log: (pid 1962) 28s
run: postgres-exporter: (pid 1989) 27s; run: log: (pid 1988) 27s
run: postgresql: (pid 1945) 28s; run: log: (pid 1944) 28s
run: prometheus: (pid 1973) 28s; run: log: (pid 1972) 28s
run: puma: (pid 1968) 28s; run: log: (pid 1966) 28s
run: redis: (pid 1952) 28s; run: log: (pid 1951) 28s
run: redis-exporter: (pid 1971) 28s; run: log: (pid 1964) 28s
run: sidekiq: (pid 1969) 28s; run: log: (pid 1967) 28s
部署分析:
- 依赖 postgres
- 依赖 redis
使用 k8s 部署:
(1)准备 secret 文件
$ cat gitlab-secret.txt
postgres.user.root=root
postgres.pwd.root=1qaz2wsx
$ kubectl -n jenkins create secret generic gitlab-secret --from-env-file=gitlab-secret.txt
(2)部署postgres
注意点:
- 使用 secret 来引用账户密码
- 使用 postgres=true 来指定节点
$ cat postgres.yaml
apiVersion: v1
kind: Service
metadata:
name: postgres
labels:
app: postgres
namespace: jenkins
spec:
ports:
- name: server
port: 5432
targetPort: 5432
protocol: TCP
selector:
app: postgres
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: postgredb
namespace: jenkins
spec:
accessModes:
- ReadWriteOnce
storageClassName: nfs
resources:
requests:
storage: 200Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: jenkins
name: postgres
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
tolerations:
- operator: "Exists"
containers:
- name: postgres
image: postgres:11.4
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER #PostgreSQL 用户名
valueFrom:
secretKeyRef:
name: gitlab-secret
key: postgres.user.root
- name: POSTGRES_PASSWORD #PostgreSQL 密码
valueFrom:
secretKeyRef:
name: gitlab-secret
key: postgres.pwd.root
resources:
limits:
cpu: 1000m
memory: 2048Mi
requests:
cpu: 50m
memory: 100Mi
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgredb
volumes:
- name: postgredb
persistentVolumeClaim:
claimName: postgredb
#创建postgres
$ kubectl create -f postgres.yaml
# 创建数据库gitlab,为后面部署gitlab组件使用
$ kubectl -n jenkins exec -ti postgres-7ff9b49f4c-nt8zh bash
root@postgres-7ff9b49f4c-nt8zh:/# psql
root=# create database gitlab;
CREATE DATABASE
(3)部署 redis
$ cat redis.yaml
apiVersion: v1
kind: Service
metadata:
name: redis
labels:
app: redis
namespace: jenkins
spec:
ports:
- name: server
port: 6379
targetPort: 6379
protocol: TCP
selector:
app: redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: jenkins
name: redis
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
tolerations:
- operator: "Exists"
containers:
- name: redis
image: sameersbn/redis:4.0.9-2
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 6379
resources:
limits:
cpu: 1000m
memory: 2048Mi
requests:
cpu: 50m
memory: 100Mi
# 创建
$ kubectl create -f redis.yaml
(4)部署gitlab
注意点:
- 使用 ingress 暴漏服务
- 添加 annotation,指定 nginx 端上传大小限制,否则推送代码时会默认被限制 1m 大小,相当于给 nginx 设置
client_max_body_size
的限制大小 - 使用服务发现地址来访问 postgres 和 redis
- 在 secret 中引用数据库账户和密码
- 数据库名称为 gitlab
$ cat gitlab.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitlab
namespace: jenkins
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
rules:
- host: gitlab.luffy.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitlab
port:
number: 80
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: gitlab
namespace: jenkins
spec:
accessModes:
- ReadWriteOnce
storageClassName: nfs
resources:
requests:
storage: 200Gi
---
apiVersion: v1
kind: Service
metadata:
name: gitlab
labels:
app: gitlab
namespace: jenkins
spec:
ports:
- name: server
port: 80
targetPort: 80
protocol: TCP
selector:
app: gitlab
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: jenkins
name: gitlab
labels:
app: gitlab
spec:
replicas: 1
selector:
matchLabels:
app: gitlab
template:
metadata:
labels:
app: gitlab
spec:
tolerations:
- operator: "Exists"
containers:
- name: gitlab
image: sameersbn/gitlab:13.2.2
imagePullPolicy: "IfNotPresent"
env:
- name: GITLAB_HOST
value: "gitlab.luffy.com"
- name: GITLAB_PORT
value: "80"
- name: GITLAB_SECRETS_DB_KEY_BASE
value: "long-and-random-alpha-numeric-string"
- name: GITLAB_SECRETS_DB_KEY_BASE
value: "long-and-random-alpha-numeric-string"
- name: GITLAB_SECRETS_SECRET_KEY_BASE
value: "long-and-random-alpha-numeric-string"
- name: GITLAB_SECRETS_OTP_KEY_BASE
value: "long-and-random-alpha-numeric-string"
- name: DB_HOST
value: "postgres"
- name: DB_NAME
value: "gitlab"
- name: DB_USER
valueFrom:
secretKeyRef:
name: gitlab-secret
key: postgres.user.root
- name: DB_PASS
valueFrom:
secretKeyRef:
name: gitlab-secret
key: postgres.pwd.root
- name: REDIS_HOST
value: "redis"
- name: REDIS_PORT
value: "6379"
ports:
- containerPort: 80
resources:
limits:
cpu: 2000m
memory: 5048Mi
requests:
cpu: 100m
memory: 500Mi
volumeMounts:
- mountPath: /home/git/data
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: gitlab
# 创建
$ kubectl create -f gitlab.yaml
配置 hosts 解析:
172.21.51.143 gitlab.luffy.com
设置root密码
访问 http://gitlab.luffy.com
,设置管理员密码
配置 k8s-master 节点的 hosts
$ echo "172.21.51.143 gitlab.luffy.com" >>/etc/hosts
myblog 项目推送到 gitlab
mkdir demo
cp -r python-demo demo/
cd demo/myblog
git remote rename origin old-origin
git remote add origin http://gitlab.luffy.com/root/myblog.git
git push -u origin --all
git push -u origin --tags
钉钉推送
-
配置机器人
-
试验发送消息
$ curl 'https://oapi.dingtalk.com/robot/send?access_token=4778abd23dbdbaf66fc6f413e6ab9c0103a039b0054201344a22a5692cdcc54e' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"text": {
"content": "基于K8s的DevOps平台实践"
}
}'
🍑 演示过程
流程示意图:
(1)安装 gitlab plugin
插件中心搜索并安装gitlab,直接安装即可
(2)配置 Gitlab
系统管理->系统配置->Gitlab,其中的 API Token
,需要从下个步骤中获取
(3)获取 AccessToken
登录 gitlab,选择 user->Settings->access tokens
新建一个访问 token
(4)配置 host 解析
由于我们的 Jenkins 和 gitlab 域名是本地解析,因此需要让 itlab 和 Jenkins 服务可以解析到对方的域名。两种方式:
-
在容器内配置hosts
-
配置coredns的静态解析
hosts {
172.21.51.143 jenkins.luffy.com gitlab.luffy.com
fallthrough
}
(5)创建自由风格项目
- gitlab connection 选择为刚创建的 gitlab
- 源码管理选择 Git,填项项目地址
- 新建一个 Credentials 认证,使用用户名密码方式,配置 gitlab 的用户和密码
- 构建触发器选择
Build when a change is pushed to GitLab
- 生成一个
Secret token
- 保存
(6)到 gitlab 配置 webhook
- 进入项目下
settings->Integrations
- URL:
http://jenkins.luffy.com/project/free
- Secret Token 填入在 Jenkins 端生成的token
- Add webhook
- test push events,报错:
Requests to the local network are not allowed
(7)设置 gitlab 允许向本地网络发送 webhook 请求
访问 Admin Aera -> Settings -> Network
,展开Outbound requests
Collapse,勾选第一项即可。再次 test push events
,成功。
(8)配置 free 项目,增加构建步骤,执行 shell,将发送钉钉消息的 shell 保存
(9)提交代码到 gitlab 仓库,查看构建是否自动执行
4. Master-Slaves(agent)模式
上面演示的任务,默认都是在 master 节点执行的,多个任务都在 master 节点执行,对 master 节点的性能会造成一定影响,如何将任务分散到不同的节点,做成多 slave 的方式?
(1)添加 slave 节点
- 系统管理 -> 节点管理 -> 新建节点
- 比如添加
172.21.51.68
,选择固定节点,保存 - 远程工作目录
/opt/jenkins_jobs
- 标签为任务选择节点的依据,如
172.21.51.68
- 启动方式选择通过
java web
启动代理,代理是运行jar
包,通过JNLP
(是一种允许客户端启动托管在远程 Web 服务器上的应用程序的协议 )启动连接到 maste r节点服务中
(2)执行 java 命令启动 agent 服务
## 登录172.21.51.68,下载agent.jar
$ wget http://jenkins.luffy.com/jnlpJars/agent.jar
## 会提示找不到agent错误,因为没有配置地址解析,由于连接jenkins master会通过50000端口,直接使用cluster-ip
$ kubectl -n jenkins get svc #在master节点执行查询cluster-ip地址
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
jenkins ClusterIP 10.99.204.208 <none> 8080/TCP,50000/TCP 4h8m
## 再次回到68节点
$ wget 10.99.204.208:8080/jnlpJars/agent.jar
$ java -jar agent.jar -jnlpUrl http://10.99.204.208:8080/computer/172.21.51.68/slave-agent.jnlp -secret 4be4d164f861d2830835653567867a1e695b30c320d35eca2be9f5624f8712c8 -workDir "/opt/jenkins_jobs"
...
INFO: Remoting server accepts the following protocols: [JNLP4-connect, Ping]
Apr 01, 2020 7:03:51 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Agent discovery successful
Agent address: 10.99.204.208
Agent port: 50000
Identity: e4:46:3a:de:86:24:8e:15:09:13:3d:a7:4e:07:04:37
Apr 01, 2020 7:03:51 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Handshaking
Apr 01, 2020 7:03:51 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Connecting to 10.99.204.208:50000
Apr 01, 2020 7:03:51 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Trying protocol: JNLP4-connect
Apr 01, 2020 7:04:02 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Remote identity confirmed: e4:46:3a:de:86:24:8e:15:09:13:3d:a7:4e:07:04:37
Apr 01, 2020 7:04:03 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Connected
若出现如下错误:
SEVERE: http://jenkins.luffy.com/ provided port:50000 is not reachable
java.io.IOException: http://jenkins.luffy.com/ provided port:50000 is not reachable
at org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver.resolve(JnlpAgentEndpointResolver.java:311)
at hudson.remoting.Engine.innerRun(Engine.java:689)
at hudson.remoting.Engine.run(Engine.java:514)
可以选择: 配置从节点 -> 高级 -> Tunnel连接位置
,参考下图进行设置:
(3)查看 Jenkins 节点列表,新节点已经处于可用状态
(4)测试使用新节点执行任务
-
配置
free
项目 -
限制项目的运行节点 ,标签表达式选择
172.21.51.68
-
立即构建
-
查看构建日志
Started by user admin
Running as SYSTEM
Building remotely on 172.21.51.68 in workspace /opt/jenkins_jobs/workspace/free-demo
using credential gitlab-user
Cloning the remote Git repository
Cloning repository http://gitlab.luffy.com/root/myblog.git
> git init /opt/jenkins_jobs/workspace/free-demo # timeout=10
...
5. Jenkins定制化容器
由于每次新部署 Jenkins 环境,均需要安装很多必要的插件,因此考虑把插件提前做到镜像中。
Dockerfile
FROM jenkinsci/blueocean:1.23.2
LABEL maintainer="inspur_lyx@hotmail.com"
ENV JENKINS_UC https://updates.jenkins-zh.cn
ENV JENKINS_UC_DOWNLOAD https://mirrors.tuna.tsinghua.edu.cn/jenkins
ENV JENKINS_OPTS="-Dhudson.model.UpdateCenter.updateCenterUrl=https://updates.jenkins-zh.cn/update-center.json"
ENV JENKINS_OPTS="-Djenkins.install.runSetupWizard=false"
## 用最新的插件列表文件替换默认插件文件
COPY plugins.txt /usr/share/jenkins/ref/
## 执行插件安装
RUN /usr/local/bin/install-plugins.sh < /usr/share/jenkins/ref/plugins.txt
plugins.txt
ace-editor:1.1
allure-jenkins-plugin:2.28.1
ant:1.10
antisamy-markup-formatter:1.6
apache-httpcomponents-client-4-api:4.5.10-1.0
authentication-tokens:1.3
...
get_plugin.sh
admin:
123456@localhost
需要替换成 Jenkins 的用户名、密码及访问地址
#!/usr/bin/env bash
curl -sSL "http://admin:123456@localhost:8080/pluginManager/api/xml?depth=1&xpath=/*/*/shortName|/*/*/version&wrapper=plugins" | perl -pe 's/.*?<shortName>([\w-]+).*?<version>([^<]+)()(<\/\w+>)+/\1:\2\n/g'|sed 's/ /:/' > plugins.txt
执行构建,定制 jenkins 容器
$ docker build . -t 172.21.51.143:5000/jenkins:v20200414 -f Dockerfile
$ docker push 172.21.51.143:5000/jenkins:v20200414
至此,我们可以使用定制化的镜像启动 jenkins 服务
## 删掉当前服务
$ kubectl delete -f jenkins-all.yaml
## 删掉已挂载的数据
$ rm -rf /var/jenkins_home
## 替换使用定制化镜像
$ sed -i 's#jenkinsci/blueocean#172.21.51.143:5000/jenkins:v20200404#g' jenkins-all.yaml
## 重新创建服务
$ kubectl create -f jenkins-all.yaml
6. 总结
自由风格项目弊端:
- 任务的完成需要在Jenkins端维护大量的配置
- 没法做版本控制
- 可读性、可移植性很差,不够优雅
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/80797.html