一、 写作背景
近来工作需要,预将公司现有应用服务 Docker
化,并使用 Kubernetes
进行统一资源编排,现将部署过程记录一下。
在 K8S 上部署有状态应用 ELK,收集日常测试数据的上报(应用拨测的 Heartbeat、调用链追踪的 APM、性能指标 metabeat 等)。本文通过rook提供底层存储,用于安装elk的statefulset,然后部署MetalLB实现本地负载均衡,最后通过ingress-control实现访问kibana。
二、 系列文章
- 快速搭建Kubernetes高可用集群一 基础环境初始化
- 快速搭建Kubernetes高可用集群二 Kubeadm 初始化集群
- 快速搭建Kubernetes高可用集群三 Ingress、Dashboard、Metrics-server
- 快速搭建Kubernetes高可用集群四 Rook-Ceph
- 快速搭建Kubernetes高可用集群五 Harbor
- 快速搭建Kubernetes高可用集群六 Prometheus
- 快速搭建Kubernetes高可用集群七 ELK-stack
三、 项目部署
3.1 项目地址
使用 helm 部署,这里先添加一下 repo
helm repo add elastic https://helm.elastic.co
helm repo update
helm search elastic
下载项目文件备用
helm pull elastic/elasticsearch
helm pull elastic/filebeat
helm pull elastic/kibana
helm pull elastic/logstash
helm pull elastic/metricbeat
3.2 ELK 集群规划
本集群在搭建好的 3M+6W
k8s 集群基础上搭建,ELK 集群架构为 ES(3Master+6Data+3Client)+LS(6Logstash)+K(1Kibana)+FB(6Filebeat),具体如下:
服务 | 节点数量 | 部署名称 | 部署方式 | 功能 |
---|---|---|---|---|
elasticsearch | 3 | gxes-m | helm+stateful | Master节点,负责集群管理,有状态服务 |
elasticsearch | 6 | gxes-d | helm+stateful | Data,数据存储节点,有状态服务 |
elasticsearch | 3 | gxes-c | helm+stateful | Client,服务提供节点,有状态服务 |
logstash | 6 | gxls-logstash | helm+stateful | logstash,数据分析、转发,有状态服务 |
kibana | 1 | gxki-kibana | helm+deployments | kibana,数据展示,无状态服务,整个 |
filebeat | 6 | gxfb-filebeat | helm+daemon | filebeat数据收集,无状态服务,每个k8s节点只需要一个服务,需采集的数据,从 Docker 中挂载到主机目录中 |
3.3 创建 secret
3.3.1 证书说明
证书使用 cfssl
方式创建,详见快速搭建Kubernetes高可用集群三 Ingress、Dashboard、Metrics-server的** 4.3.2 创建证书** 小节,我在之前创建证书时使用了 *.kube.uat,所以所有以 kube.uat 为后缀的域名均可使用该证书。
3.3.2 创建ES集群加密证书
# 运行容器生成证书
docker run --name elastic-charts-certs -i -w /app elasticsearch:7.7.1 /bin/sh -c \
"elasticsearch-certutil ca --out /app/elastic-stack-ca.p12 --pass '' && \
elasticsearch-certutil cert --name security-master --dns \
security-master --ca /app/elastic-stack-ca.p12 --pass '' --ca-pass '' --out /app/elastic-certificates.p12"
# 复制生成的证书到本地
docker cp elastic-charts-certs:/app/elastic-certificates.p12 ./
# 删除容器
docker rm -f elastic-charts-certs
# 将 pcks12 中的信息分离出来,写入文件
openssl pkcs12 -nodes -passin pass:'' -in elastic-certificates.p12 -out elastic-certificate.pem
3.3.3 使用证书创建 secret
# 创建 namespace
kubectl create namespace elkstuck
# 创建域名chart.kube.uat 的 SSL 证书
kubectl -n elkstuck create secret tls chart-kube-uat-tls --key /etc/kubernetes/pki/kube-uat-key.pem --cert /etc/kubernetes/pki/kube-uat.pem
# 创建域名 kibana.kube.uat 的 SSL 证书
kubectl -n elkstuck create secret tls kibana-kube-uat-tls --key /etc/kubernetes/pki/kube-uat-key.pem --cert /etc/kubernetes/pki/kube-uat.pem
# 创建 ELK 集群 SSL(p12) 证书的证书
kubectl -n elkstuck create secret generic elastic-certificates --from-file=/etc/kubernetes/gitlab/elk/certs/elastic-certificates.p12
kubectl -n elkstuck create secret generic elastic-certificate-pem --from-file=/etc/kubernetes/gitlab/elk/certs/elastic-certificate.pem
# 创建 ELK 集群的管理员账号密码,用户名使用 `elastic`
kubectl -n elkstuck create secret generic elastic-credentials --from-literal=username=elastic --from-literal=password=your-password
# 创建 namespace 拉取 harbor 中镜像使用的账号密码证书
kubectl -n elkstuck create secret docker-registry registry-secret --docker-server harbor.kube.uat --docker-username=your-harbor-username --docker-password= your-harbor-password
3.3.4 创建 StorageClass
ES在使用过程中需要存储大量日志数据,所以我们需要在 rook-ceph 中创建 StorageClass(SC),并在后续使用中配置 PersistentVolumeClaim(PVC)。
StorageClass 配置
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
# 这里更改为你需要创建的存储池名称,便于区别与其它存储池
name: elkstuck-replicapool
namespace: rook-ceph
spec:
failureDomain: host
replicated:
size: 3
requireSafeReplicaSize: true
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
# 这里设置要创建的StorageClass名称。elkstuck的相关数据均存储在此StorageClass。
name: elkstuck-rook-ceph-block
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
clusterID: rook-ceph
# 这里是 StorageClass 所在的存储池,是上面一步创建的。
pool: elkstuck-replicapool
imageFormat: "2"
imageFeatures: layering
csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner
csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
# 分区模式选择为 xfs (CentOS7 以上都支持该分区模式)
csi.storage.k8s.io/fstype: xfs
allowVolumeExpansion: true
reclaimPolicy: Delete
部署
kubectl apply -f ./elastuck-storageclass.yaml
3.4 部署 Elasticsearch 节点
3.4.1 Master 节点
helm 部署文件 es-master.yaml,仅展示修改项。
---
# 集群名称
clusterName: "gxsk"
# 节点所属群组
nodeGroup: "master"
# Master 节点的服务地址,这里是Master,不需要
masterService: ""
# 节点类型:
roles:
master: "true"
ingest: "false"
data: "false"
# 节点数量,做为 Master 节点,数量必须是 node >=3 and node mod 2 == 1
replicas: 3
minimumMasterNodes: 2
esMajorVersion: ""
esConfig:
# elasticsearch.yml 的配置,主要是数据传输和监控的开关及证书配置
elasticsearch.yml: |
xpack:
security:
enabled: true
transport:
ssl:
enabled: true
verification_mode: certificate
keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
monitoring:
collection:
enabled: true
# 设置 ES 集群的 elastic 账号密码为变量
extraEnvs:
- name: ELASTIC_USERNAME
valueFrom:
secretKeyRef:
name: elastic-credentials
key: username
- name: ELASTIC_PASSWORD
valueFrom:
secretKeyRef:
name: elastic-credentials
key: password
envFrom: []
# 挂载证书位置
secretMounts:
- name: elastic-certificates
secretName: elastic-certificates
path: /usr/share/elasticsearch/config/certs
# 镜像拉取来源,我对镜像做了一些简单的修改,故放置于自建的 harbor 里。
image: "harbor.kube.uat/base/elasticsearch"
imageTag: "7.9.2-x"
imagePullPolicy: "IfNotPresent"
imagePullSecrets:
- name: registry-secret
podAnnotations: {}
labels: {}
# ES 的 JVM 内存
esJavaOpts: "-Xmx1g -Xms1g"
# ES 运行所需的资源
resources:
requests:
cpu: "2000m"
memory: "2Gi"
limits:
cpu: "2000m"
memory: "2Gi"
initResources: {}
sidecarResources: {}
# ES 的服务 IP,如果没有设置这个,服务有可能无法启动。
networkHost: "0.0.0.0"
# ES 的存储配置
volumeClaimTemplate:
storageClassName: "elkstuck-rook-ceph-block"
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 5Gi
# PVC 开关
persistence:
enabled: true
labels:
enabled: false
annotations: {}
# rbac 暂未详细研究
rbac:
create: false
serviceAccountAnnotations: {}
serviceAccountName: ""
# 镜像部署选择节点
nodeSelector:
elk-rolse: master
# 容忍污点,如果 K8S 集群节点较少,需要在 Master 节点部署,需要使用此项
tolerations:
- operator: "Exists"
3.4.2 Data节点
helm 部署文件 es-data.yaml,仅展示修改项。
---
# 集群名称,必须和 Master 节点的集群名称保持一致
clusterName: "gxsk"
# 节点类型
nodeGroup: "data"
# Master 节点服务名称
masterService: "gxsk-master"
# 节点权限,为 True 的是提供相关服务,Data 节点不需要 Master 权限
roles:
master: "false"
ingest: "true"
data: "true"
# 节点数量
replicas: 6
esMajorVersion: ""
esConfig:
# elasticsearch.yml 配置,同 Master 节点配置
extraEnvs:
# 同 Master 节点配置
envFrom: []
secretMounts:
# 证书挂载,同 Master 节点配置
# ES节点的 JVM 内存分配,根据实际情况进行增加
esJavaOpts: "-Xmx1g -Xms1g"
# ES 数据存储
volumeClaimTemplate:
storageClassName: "elkstuck-rook-ceph-block"
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 50Gi
# PVC 开关
persistence:
enabled: true
labels:
enabled: false
annotations: {}
3.4.3 Client节点
整个集群通过 Client节点进行输入输出,所以不需要存储,不需要 Master,需要调用 ingress 提供服务,配置helm 部署文件 es-client.yaml,仅展示修改项。
---
# 集群名称,需要同 Master 节点配置保持一致。
clusterName: "gxsk"
# 节点所属群组
nodeGroup: "client"
# Master 节点服务名称
masterService: "gxsk-master"
# 节点权限功能,所列权限均为 flase 则该节点仅做服节点。
roles:
master: "false"
ingest: "false"
data: "false"
# 节点数量
replicas: 3
# ES 的 JVM 内存
esJavaOpts: "-Xmx1g -Xms1g"
networkHost: "0.0.0.0"
# PVC 配置
persistence:
enabled: false
labels:
enabled: false
annotations: {}
# ingress 配置
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
path: /
hosts:
- chart.kube.uat
tls:
- secretName: chart-kube-uat-tls
hosts:
- chart.kube.uat
3.4.4 许可证书
服务开启了 X-pack 服务,需要导入许可证书。以下是证书内容,issued_to字段和signature字段有修改,请自行申请证书,并保存为 license.json 文件(可以任意命名):
{
"license": {
"uid": "f51b6155-4219-46f8-95ee-f7506ba19278",
"type": "platinum",
"issue_date_in_millis": 1551225600000,
"expiry_date_in_millis": 3490301288000,
"max_nodes": 100,
"issued_to": "xukezhengshu (beijing)",
"issuer": "Web Form",
"signature": "AAAAAwAAAA2LctDYGqDuwwP80KOZAAABmC9ZN0hjZDBGYnVyRXpCOW5Bb3FjZDAxOWpSbTVoMVZwUzRxVk1PSmkxaktJRVl5MUYvUWh3bHZVUTllbXNPbzBUemtnbWpBbmlWRmRZb25KNFlBR2x0TXc2K2p1Y1VtMG1UQU9TRGZVSGRwaEJGUjE3bXd3LzRqZ05iLzRteWFNekdxRGpIYlFwYkJiNUs0U1hTVlJKNVlXekMrSlVUdFIvV0FNeWdOYnlESDc3MWhlY3hSQmdKSjJ2ZTcvYlBFOHhPQlV3ZHdDQ0tHcG5uOElCaDJ4K1hob29xSG85N0kvTWV3THhlQk9NL01VMFRjNDZpZEVXeUtUMXIyMlIveFpJUkk2WUdveEZaME9XWitGUi9WNTZVQW1FMG1DenhZU0ZmeXlZakVEMjZFT2NvOWxpZGlqVmlHNC8rWVVUYzMwRGVySHpIdURzKzFiRDl4TmM1TUp2VTBOUlJZUlAyV0ZVL2kvVk10L0NsbXNFYVZwT3NSU082dFNNa2prQ0ZsclZ4NTltbU1CVE5lR09Bck93V2J1Y3c9PQAAAQCg6gKXMrdvhm6ZD9er8HIk5+CZiCNGIdh7cDjJ39Z1in5/8mVy+L3ibsdC/26KtyuhDVJoELOhfjtXHC7TEgjuMJKWesetPG6cSfFqD2l5g/Fmly9z7WPDt9TNOw3GfPFWHp4IOlTF8JhrIFgp1FeoxWbS7UWHP1m1G1nEEJ0a9Zrw1OaPhKw3n1zYG3cJ37eXOwEaTAyGq3fmZMn92bYYYcA5SSKq7NGjNr4t3EO97CJVyS5YoNcQRIiNo9b/",
"start_date_in_millis": 1551225600000
}
}
3.4.5 服务部署
首先部署 Master 节点,然后导入许可证书。
# 部署 Master 节点
helm -n elkstuck install gxes-m elastic/elasticsearch -f /etc/kubernetes/gitlab/elk/elasticsearch/values_master.yaml
# 部署完成后,可以看到 Master 的 Services 集群 IP 地址为:"10.106.77.51",
# 这个地址会有变化的,请自行查找一下,如果启用 Ingress 则直接使用域名也可以,
# 内部 Endpoints 地址为"gxsk-master.elkstuck:9200"
# 通过此地址导入许可文件(.elkstuck这一段域名,在 K8S 内部可以不写)
curl -XPUT -u elastic 'http://10.106.77.51:9200/_xpack/license' -H "Content-Type: application/json" -d @license.json
# 查看许可状态
curl -u elastic http://10.106.77.51:9200/_xpack?pretty
Master启动并导入许可成功后,即可部署 Data 节点和 Client 节点
helm -n elkstuck install gxes-d elastic/elasticsearch -f /etc/kubernetes/gitlab/elk/elasticsearch/values_data.yaml
helm -n elkstuck install gxes-c elastic/elasticsearch -f /etc/kubernetes/gitlab/elk/elasticsearch/values_client.yaml
成功部署后,查看部署状态
curl -u elastic http://10.106.77.51:9200/_cat/nodes?v
部署 kibana
helm -n elkstuck install gxki elastic/kibana -f /etc/kubernetes/gitlab/elk/kibana/values.yaml
3.5 部署 Logstash
Logstash 在 K8S 的部署暂时还处于 Bate 版,可以把日志从 Filebeat 等收集器的数据直接传输到 ES 集群中,而不使用 Logstash。
3.5.1 Logstash 部署文件
该配置文件仅展示修改部分,其它部分均采用官方默认
---
# Logstash 节点数量
replicas: 6
# configmap: logstash.yml 配置
logstashConfig:
logstash.yml: |
# 节点IP配置,这个http.host字段必须添加并设置成 0.0.0.0,否则不能部署成功。
http:
host: 0.0.0.0
# 日志输出级别
log:
level: info
# pipeline ID 及配置
pipeline:
id: main
workers: 2
batch:
size: 250
delay: 100
# 配置即时生效或重启生效,true 为即时生效
config:
reload:
automatic: true
interval: 5s
# xpack 配置
xpack:
# 监控开启
monitoring:
enabled: true
elasticsearch:
# ES集群服务地址、用户名、密码
hosts: '${ELASTICSEARCH_HOSTS:gxsk-client:9200}'
username: '${ELASTIC_USERNAME}'
password: '${ELASTIC_PASSWORD}'
logstashPipeline:
# logstash.conf 配置
logstash.conf: |
# 输入,来自 Filebeat,接收端口 5044.
input {
beats {
port => 5044
}
}
# 过滤、解析规则,提取日志内时间字段,替换系统的 timestamp
filter {
grok {
match => ["message","(?<logdate>%{YEAR}-%{MONTHNUM}-%{MONTHDAY}\s+%{TIME})"]
}
date {
match => ["logdate", "yyyy-MM-dd'T'HH:mm:ss"]
target => "@timestamp"
remove_field => "logdate"
}
date {
match => ["logdate", "yyyy-MM-dd HH:mm:ss:SSS"]
target => "@timestamp"
remove_field => "logdate"
}
date {
match => ["logdate", "yyyy-MM-dd HH:mm:ss.SSS"]
target => "@timestamp"
remove_field => "logdate"
}
}
# 多源输入日志,根据一定的规则输出,这里是根据 Filebeat 在传输时添加的 Tag来判断。
output {
if 'spider_info' in [tags] or 'spider_error' in [tags] {
# 输出到 ES
elasticsearch {
# es集群服务的地址、用户名、密码,存储到 ES 中的 Index 名称
hosts => '${ELASTICSEARCH_HOSTS:gxsk-client:9200}'
user => '${ELASTIC_USERNAME}'
password => '${ELASTIC_PASSWORD}'
manage_template => true
index => "spider-%{+yyyy.MM.dd}"
}
}
}
extraEnvs:
# 设置 ES 集群的 elastic 账号密码为变量
- name: ELASTIC_USERNAME
valueFrom:
secretKeyRef:
name: elastic-credentials
key: username
- name: ELASTIC_PASSWORD
valueFrom:
secretKeyRef:
name: elastic-credentials
key: password
envFrom: []
secrets: []
# 挂载证书位置
secretMounts:
- name: elastic-certificates
secretName: elastic-certificates
path: /usr/share/logstash/config/certs
# 镜像拉取
image: "docker.elastic.co/logstash/logstash"
imageTag: "7.9.2"
imagePullPolicy: "IfNotPresent"
imagePullSecrets: []
podAnnotations: {}
labels: {}
# logstash 的 JVM 内存配置
logstashJavaOpts: "-Xmx1g -Xms1g"
resources:
requests:
cpu: "100m"
memory: "1536Mi"
limits:
cpu: "1000m"
memory: "1536Mi"
# Logstash 为有状态服务,设置 PVC 存储
volumeClaimTemplate:
storageClassName: "elkstuck-rook-ceph-block"
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 3Gi
# rbac 未深入研究,暂关闭。
rbac:
create: false
serviceAccountAnnotations: {}
serviceAccountName: ""
# pvc 开关
persistence:
enabled: true
annotations: {}
# 服务端口
httpPort: 9600
# Services 服务
service:
type: ClusterIP
ports:
- name: beats
port: 5044
protocol: TCP
targetPort: 5044
3.5.2 部署 logstash
helm install -n elkstuck gxls elastic/logstash -f /etc/kubernetes/gitlab/elk/logstash/values.yaml
3.6 部署 Kibana
3.6.1 Kibana 部署配置文件
---
# es 集群服务地址
elasticsearchHosts: "http://gxsk-client:9200"
# 部署副本数
replicas: 1
# 设置 ES 集群用户名、密码为变量
extraEnvs:
- name: "NODE_OPTIONS"
value: "--max-old-space-size=1800"
- name: 'ELASTICSEARCH_USERNAME'
valueFrom:
secretKeyRef:
name: elastic-credentials
key: username
- name: 'ELASTICSEARCH_PASSWORD'
valueFrom:
secretKeyRef:
name: elastic-credentials
key: password
envFrom: []
# 挂载 ES 集群证书
secretMounts:
- name: elastic-certificates
secretName: elastic-certificates
path: /usr/share/kibana/config/certs
- name: elasticsearch-ca-pem
secretName: elasticsearch-ca-pem
path: /usr/share/kibana/config/certs
# 拉取镜像
image: "docker.elastic.co/kibana/kibana"
imageTag: "7.9.2"
imagePullPolicy: "IfNotPresent"
# 服务协议及地址
protocol: http
serverHost: "0.0.0.0"
healthCheckPath: "/app/kibana"
kibanaConfig:
# kibana.yml 配置
kibana.yml: |
# 本地化:中文
i18n.locale: "zh-CN"
# es 集群的账号、密码、SSL 证书
elasticsearch:
username: '${ELASTICSEARCH_USERNAME}'
password: '${ELASTICSEARCH_PASSWORD}'
ssl:
certificate: /usr/share/kibana/config/certs/elastic-certificates.p12
key: /usr/share/kibana/config/certs/elastic-certificates.p12
verificationMode: certificate
# xpack 开启
xpack:
security:
enabled: true
# 服务端口
service:
type: ClusterIP
loadBalancerIP: ""
port: 5601
nodePort: ""
labels: {}
annotations: {}
loadBalancerSourceRanges: []
# ingress 配置,同时配置 SSL 访问
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
path: /
hosts:
- kibana.kube.uat
tls:
- secretName: kibana-kube-uat-tls
hosts:
- kibana.kube.uat
3.6.2 部署
执行 helm 部署,并验证
helm -n elkstuck install gxki elastic/kibana -f /etc/kubernetes/gitlab/elk/kibana/values.yaml
部署成功后,使用 https://kibana.kube.uat 访问即可。
3.7 Filebeat 部署
3.7.1 Filebeat 部署配置文件
---
filebeatConfig:
# filebeat.yml 配置
filebeat.yml: |
# 数据输入,读取日志的位置,这里是直接读取宿主机的相应目录,
# 运行环境中的Docker中业务日志,如果需要使用 ELK 查看需要挂载到宿主机的相关目录下。
filebeat.inputs:
# 读取类型
- type: log
# 开关
enabled: true
# 日志路径,宿主机上的文件路径
paths:
- /var/log/multi-registered/multi-registered/server.log
# 区分业务日志,添加的 tag, 在 logstash 处理时会根据这个来进行解析并分发到 ES 存储。
tags: ['spider_info']
# 多行日志的换行规则(开头到结尾的位置定位)
multiline.pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}:\d{3}'
multiline.negate: true
multiline.match: after
# 输出到 Logstash
output.logstash:
# logstash 的 Service 服务和端口
hosts: '${ELASTICSEARCH_HOSTS:gxls-logstash:5044}'
# 启用 Logstash 负载均衡
loadbalance: true
index: filebeat
# Filebeat 自身日志配置
logging:
level: info
to_files: true
files:
path: /var/log/filebeat
name: filebeat.log
keepfiles: 7
permissions: 0644
# Filebeat 监控配置(数据传输到 ES)
monitoring:
enabled: true
cluster_uuid: 2gJSBHYRT4iUc3Gfs7A-mw
elasticsearch:
hosts: ['${ELASTICSEARCH_HOSTS:gxsk-client:9200}']
username: '${ELASTIC_USERNAME}'
password: '${ELASTIC_PASSWORD}'
# 挂载 ES 集群的账号、密码
extraEnvs:
- name: ELASTIC_USERNAME
valueFrom:
secretKeyRef:
name: elastic-credentials
key: username
- name: ELASTIC_PASSWORD
valueFrom:
secretKeyRef:
name: elastic-credentials
key: password
# 镜像拉取
image: "docker.elastic.co/beats/filebeat"
imageTag: "7.9.2"
imagePullPolicy: "IfNotPresent"
imagePullSecrets: []
# 挂载 ES 集群的证书文件
secretMounts:
- name: elastic-certificates
secretName: elastic-certificates
path: /usr/share/filebeat/certs
3.7.2 部署
helm install -n elkstuck gx-fb elastic/filebeat -f /etc/kubernetes/gitlab/elk/filebeat/values.yaml
五、 文章参考
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 long@longger.xin