hugo-teek is loading...

自定义监控报警

最后更新于:

自定义监控报警

目录

[TOC]

自定义监控报警

上节课和大家讲解了 Prometheus Operator 的安装和基本使用方法,这节课给大家介绍如何在 Prometheus Operator 中添加一个自定义的监控项。

除了 Kubernetes 集群中的一些资源对象、节点以及组件需要监控,有的时候我们可能还需要根据实际的业务需求去添加自定义的监控项,添加一个自定义监控的步骤也是非常简单的。

  • 第一步建立一个 ServiceMonitor 对象,用于 Prometheus 添加监控项
  • 第二步为 ServiceMonitor 对象关联 metrics 数据接口的一个 Service 对象
  • 第三步确保 Service 对象可以正确获取到 metrics 数据

接下来我们就来为大家演示如何添加 etcd 集群的监控。无论是 Kubernetes 集群外的还是使用 kubeadm 安装在集群内部的 etcd 集群,我们这里都将其视作集群外的独立集群,因为对于二者的使用方法没什么特殊之处。

实验环境

实验软件

1、etcd 监控

1.查询etcd启动参数

由于我们这里演示环境使用的是 kubeadm 搭建的集群,我们可以使用 kubectl 工具去获取 etcd 启动的相关参数:

 1[root@master1 ~]#kubectl get pods -n kube-system -l component=etcd                                                                                                          
 2NAME           READY   STATUS    RESTARTS      AGE
 3etcd-master1   1/1     Running   6 (15d ago)   309d
 4
 5$ kubectl get pods etcd-master1 -n kube-system -o yaml
 6......
 7spec:
 8  containers:
 9  - command:
10    - etcd
11    - --advertise-client-urls=https://172.29.9.51:2379
12    - --cert-file=/etc/kubernetes/pki/etcd/server.crt
13    - --client-cert-auth=true
14    - --data-dir=/var/lib/etcd
15    - --initial-advertise-peer-urls=https://172.29.9.51:2380
16    - --initial-cluster=master1=https://172.29.9.51:2380
17    - --key-file=/etc/kubernetes/pki/etcd/server.key
18    - --listen-client-urls=https://127.0.0.1:2379,https://172.29.9.51:2379
19    - --listen-metrics-urls=http://127.0.0.1:2381 #注意这里的参数
20    - --listen-peer-urls=https://172.29.9.51:2380
21    - --name=master1
22    - --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
23    - --peer-client-cert-auth=true
24    - --peer-key-file=/etc/kubernetes/pki/etcd/peer.key
25    - --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
26    - --snapshot-count=10000
27    - --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
28......

我们可以看到启动参数里面有一个 --listen-metrics-urls=http://127.0.0.1:2381 的配置,该参数就是来指定 metrics 接口运行在 2381 端口下面的,而且是 http 的协议,所以也不需要什么证书配置,这就比以前的版本要简单许多了,以前的版本需要用 https 协议访问,所以要配置对应的证书。

2.创建ServiceMonitor 对象

  • 接下来我们直接创建对应的 ServiceMonitor 对象即可:

[root@master1 prometheus-operator]#vim kubernetesControlPlane-serviceMonitorEtcd.yaml

 1# kubernetesControlPlane-serviceMonitorEtcd.yaml
 2apiVersion: monitoring.coreos.com/v1
 3kind: ServiceMonitor
 4metadata:
 5  name: etcd-k8s
 6  namespace: monitoring
 7  labels:
 8    k8s-app: etcd-k8s
 9spec:
10  jobLabel: k8s-app
11  endpoints:
12    - port: port
13      interval: 15s
14  selector:
15    matchLabels:
16      k8s-app: etcd
17  namespaceSelector:
18    matchNames:
19      - kube-system

上面我们在 monitoring 命名空间下面创建了名为 etcd-k8s 的 ServiceMonitor 对象,基本属性和前面章节中的一致,匹配 kube-system 这个命名空间下面的具有 k8s-app=etcd 这个 label 标签的 Service,jobLabel 表示用于检索 job 任务名称的标签,由于 etcd 的 metrics 接口在 2381 端口下面,不需要 https 安全认证,所以用默认的配置即可。

关于 ServiceMonitor 更多的配置属性,可以参考官方的 API 文档的描述。

  • 然后我们直接创建这个 ServiceMonitor 对象即可:
1[root@master1 prometheus-operator]#kubectl apply -f kubernetesControlPlane-serviceMonitorEtcd.yaml
2servicemonitor.monitoring.coreos.com/etcd-k8s created

但实际上现在并不能监控到 etcd 集群,因为并没有一个满足 ServiceMonitor 条件的 Service 对象与之关联:

1[root@master1 prometheus-operator]#kubectl get svc -n kube-system -l k8s-app=etcd
2No resources found in kube-system namespace.
  • 此时,可以看下prometheus-k8s监控对象

3.自定义 Endpoints 对象来创建 Service 对象

  • 所以接下来我们需要创建一个满足上面条件的 Service 对象,由于我们把 etcd 当成是集群外部的服务,所以要引入到集群中来我们就需要自定义 Endpoints 对象来创建 Service 对象了:

[root@master1 prometheus-operator]#vim svc-etcd.yaml

 1# svc-etcd.yaml
 2apiVersion: v1
 3kind: Service
 4metadata:
 5  name: etcd-k8s
 6  namespace: kube-system
 7  labels:
 8    k8s-app: etcd
 9spec:
10  type: ClusterIP
11  clusterIP: None # 一定要设置 clusterIP:None
12  ports:
13    - name: port
14      port: 2381
15---
16apiVersion: v1
17kind: Endpoints #注意这里
18metadata:
19  name: etcd-k8s
20  namespace: kube-system
21  labels:
22    k8s-app: etcd
23subsets:
24  - addresses:
25      - ip: 172.29.9.51 # 指定etcd节点地址,如果是集群则继续向下添加
26        nodeName: etc-master
27    ports:
28      - name: port
29        port: 2381

我们这里创建的 Service 没有采用前面通过 label 标签的形式去匹配 Pod 的做法,因为前面我们说过很多时候我们创建的 etcd 集群是独立于集群之外的,这种情况下面我们就需要自定义一个 Endpoints,要注意 metadata 区域的内容要和 Service 保持一致,Service 的 clusterIP 设置为 None,新版本的 etcd 将 metrics 接口数据放置到了 2381 端口。

  • 直接创建该资源对象即可:
1[root@master1 prometheus-operator]#kubectl apply -f svc-etcd.yaml 
2service/etcd-k8s created
3endpoints/etcd-k8s created
4[root@master1 prometheus-operator]#kubectl get svc -n kube-system -l k8s-app=etcd
5NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
6etcd-k8s   ClusterIP   None         <none>        2381/TCP   8s

4.修改

  • 创建完成后,隔一会儿去 Prometheus 的 Dashboard 中查看 targets,便会有 etcd 的监控项了:

可以看到有一个明显的错误,2381 端口链接被拒绝,这是因为我们这里的 etcd 的 metrics 接口是监听在 127.0.0.1 这个 IP 上面的,所以访问会拒绝:

1--listen-metrics-urls=http://127.0.0.1:2381

我们只需要在 /etc/kubernetes/manifest/ 目录下面(静态 Pod 默认的目录)的 etcd.yaml 文件中将上面的listen-metrics-urls 更改成节点 IP 即可:

1--listen-metrics-urls=http://0.0.0.0:2381

注意:这里修改完etcd后要稍等一会儿才行。

  • 当 etcd 重启生效后,查看 etcd 这个监控任务就正常了:

5.验证

  • 数据采集到后,可以在 grafana 中导入编号为 3070 的 dashboard,就可以获取到 etcd 的监控图表:

测试完成。😘

2、配置 PrometheusRule

现在我们知道怎么自定义一个 ServiceMonitor 对象了,但是如果需要自定义一个报警规则的话呢?

我们去查看 Prometheus Dashboard 的 Alert 页面下面就已经有很多报警规则了,这一系列的规则其实都来自于项目 https://github.com/kubernetes-monitoring/kubernetes-mixin,我们都通过 Prometheus Operator 安装配置上了。

但是这些报警信息是哪里来的呢?他们应该用怎样的方式通知我们呢?我们知道之前我们使用自定义的方式可以在 Prometheus 的配置文件之中指定 AlertManager 实例和 报警的 rules 文件,现在我们通过 Operator 部署的呢?

  • 我们可以在 Prometheus Dashboard 的 Config 页面下面查看关于 AlertManager 的配置:
 1alerting:
 2  alert_relabel_configs:
 3  - separator: ;
 4    regex: prometheus_replica
 5    replacement: $1
 6    action: labeldrop
 7  alertmanagers:
 8  - follow_redirects: true
 9    enable_http2: true
10    scheme: http
11    path_prefix: /
12    timeout: 10s
13    api_version: v2
14    relabel_configs:
15    - source_labels: [__meta_kubernetes_service_name]
16      separator: ;
17      regex: alertmanager-main
18      replacement: $1
19      action: keep
20    - source_labels: [__meta_kubernetes_endpoint_port_name]
21      separator: ;
22      regex: web
23      replacement: $1
24      action: keep
25    kubernetes_sd_configs:
26    - role: endpoints
27      kubeconfig_file: ""
28      follow_redirects: true
29      enable_http2: true
30      namespaces:
31        own_namespace: false
32        names:
33        - monitoring
34rule_files:
35- /etc/prometheus/rules/prometheus-k8s-rulefiles-0/*.yaml

上面 alertmanagers 的配置我们可以看到是通过 role 为 endpoints 的 kubernetes 的自动发现机制获取的,匹配的是服务名为 alertmanager-main,端口名为 web 的 Service 服务。

  • 我们可以查看下 alertmanager-main 这个 Service:
 1$ kubectl describe svc alertmanager-main -n monitoring
 2Name:                     alertmanager-main
 3Namespace:                monitoring
 4Labels:                   app.kubernetes.io/component=alert-router
 5                          app.kubernetes.io/instance=main
 6                          app.kubernetes.io/name=alertmanager
 7                          app.kubernetes.io/part-of=kube-prometheus
 8                          app.kubernetes.io/version=0.24.0
 9Annotations:              <none>
10Selector:                 app.kubernetes.io/component=alert-router,app.kubernetes.io/instance=main,app.kubernetes.io/name=alertmanager,app.kubernetes.io/part-of=kube-prometheus
11Type:                     NodePort
12IP Family Policy:         SingleStack
13IP Families:              IPv4
14IP:                       10.109.67.21
15IPs:                      10.109.67.21
16Port:                     web  9093/TCP
17TargetPort:               web/TCP
18NodePort:                 web  32033/TCP
19Endpoints:                10.244.1.193:9093,10.244.2.208:9093,10.244.2.210:9093
20Port:                     reloader-web  8080/TCP
21TargetPort:               reloader-web/TCP
22NodePort:                 reloader-web  30181/TCP
23Endpoints:                10.244.1.193:8080,10.244.2.208:8080,10.244.2.210:8080
24Session Affinity:         ClientIP
25External Traffic Policy:  Cluster
26Events:                   <none>

可以看到服务名正是 alertmanager-main,Port 定义的名称也是 web,符合上面的规则,所以 Prometheus 和 AlertManager 组件就正确关联上了。

而对应的报警规则文件位于:/etc/prometheus/rules/prometheus-k8s-rulefiles-0/ 目录下面所有的 YAML 文件。

我们可以进入 Prometheus 的 Pod 中验证下该目录下面是否有 YAML 文件:

 1$ kubectl exec -it prometheus-k8s-0 /bin/sh -n monitoring
 2kubectl exec -it prometheus-k8s-0 -n monitoring -- /bin/sh /prometheus 
 3
 4$ ls /etc/prometheus/rules/prometheus-k8s-rulefiles-0/
 5monitoring-alertmanager-main-rules-79543974-2f8e-4c5f-9d23-2c349c38ff1d.yaml
 6monitoring-grafana-rules-8fc5e057-099e-4546-b6bd-d8fb1107c24d.yaml
 7monitoring-kube-prometheus-rules-79b18777-2df4-4e43-84a8-193053400842.yaml
 8monitoring-kube-state-metrics-rules-8341740e-f2b7-48e9-82c2-bd6b979f1da2.yaml
 9monitoring-kubernetes-monitoring-rules-4b169784-b211-4449-922f-52fb2efd839c.yaml
10monitoring-node-exporter-rules-b5f0f4d3-aa18-4e7d-836f-ef0a8fda7569.yaml
11monitoring-prometheus-k8s-prometheus-rules-9560ae4f-764c-4ba4-9a37-2fedb56773c7.yaml
12monitoring-prometheus-operator-rules-7d3a1645-efe3-4214-b825-c77c39ceb0d4.yaml
13/prometheus 
14
15$ cat /etc/prometheus/rules/prometheus-k8s-rulefiles-0/monitoring-kube-prometheus-rules-79b18777-2df4-4e43-84a8-193053400842.yaml
16groups:
17- name: general.rules
18  rules:
19  - alert: TargetDown
20    annotations:
21      description: '{{ printf "%.4g" $value }}% of the {{ $labels.job }}/{{ $labels.service
22        }} targets in {{ $labels.namespace }} namespace are down.'
23      runbook_url: https://runbooks.prometheus-operator.dev/runbooks/general/targetdown
24      summary: One or more targets are unreachable.
25    expr: 100 * (count(up == 0) BY (job, namespace, service) / count(up) BY (job,
26      namespace, service)) > 10
27    for: 10m
28    labels:
29      severity: warning
30......

这个 YAML 文件实际上就是我们之前创建的一个 PrometheusRule 文件包含的内容:

 1$ cat kubePrometheus-prometheusRule.yaml
 2apiVersion: monitoring.coreos.com/v1
 3kind: PrometheusRule
 4metadata:
 5  labels:
 6    app.kubernetes.io/component: exporter
 7    app.kubernetes.io/name: kube-prometheus
 8    app.kubernetes.io/part-of: kube-prometheus
 9    prometheus: k8s
10    role: alert-rules
11  name: kube-prometheus-rules
12  namespace: monitoring
13spec:
14  groups:
15  - name: general.rules
16    rules:
17    - alert: TargetDown
18      annotations:
19        description: '{{ printf "%.4g" $value }}% of the {{ $labels.job }}/{{ $labels.service
20          }} targets in {{ $labels.namespace }} namespace are down.'
21        runbook_url: https://runbooks.prometheus-operator.dev/runbooks/general/targetdown
22        summary: One or more targets are unreachable.
23      expr: 100 * (count(up == 0) BY (job, namespace, service) / count(up) BY (job,
24        namespace, service)) > 10
25      for: 10m
26      labels:
27        severity: warning
28......

我们这里的 PrometheusRule 的 name 为 kube-prometheus-rules,namespace 为 monitoring,我们可以猜想到我们创建一个 PrometheusRule 资源对象后,会自动在上面的 prometheus-k8s-rulefiles-0 目录下面生成一个对应的 <namespace>-<name>-<xxx-id>.yaml 文件。

所以如果以后我们需要自定义一个报警选项的话,只需要定义一个 PrometheusRule 资源对象即可。至于为什么 Prometheus 能够识别这个 PrometheusRule 资源对象呢?这就需要查看我们创建的 prometheus 这个资源对象了,里面有非常重要的一个属性 ruleSelector,用来匹配 rule 规则的过滤器,我们这里没有过滤,所以可以匹配所有的。

假设要求匹配具有 prometheus=k8srole=alert-rules 标签的 PrometheusRule 资源对象,则可以添加下面的配置:

1ruleSelector:
2  matchLabels:
3    prometheus: k8s
4    role: alert-rules

1.创建prometheus-etcdRules资源对象

所以我们要想自定义一个报警规则,只需要创建一个能够被 prometheus 对象匹配的 PrometheusRule 对象即可,比如现在我们添加一个 etcd 是否可用的报警,我们知道 etcd 整个集群有一半以上的节点可用的话集群就是可用的,所以我们判断如果不可用的 etcd 数量超过了一半那么就触发报警,创建文件 prometheus-etcdRules.yaml

vim prometheus-etcdRules.yaml

 1apiVersion: monitoring.coreos.com/v1
 2kind: PrometheusRule
 3metadata:
 4  labels:
 5    prometheus: k8s
 6    role: alert-rules
 7  name: etcd-rules
 8  namespace: monitoring
 9spec:
10  groups:
11    - name: etcd
12      rules:
13        - alert: EtcdClusterUnavailable
14          annotations:
15            summary: etcd cluster small
16            description: If one more etcd peer goes down the cluster will be unavailable
17          expr: |
18            count(up{job="etcd"} == 0) > (count(up{job="etcd"}) / 2 - 1)
19          for: 3m
20          labels:
21            severity: critical

2.验证

创建完成后,隔一会儿再去容器中查看下 rules 文件夹:

 1[root@master1 prometheus-operator]#kubectl apply -f prometheus-etcdRules.yaml
 2prometheusrule.monitoring.coreos.com/etcd-rules created
 3
 4[root@master1 prometheus-operator]#kubectl exec -it prometheus-k8s-0 /bin/sh -n monitoring
 5kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
 6/prometheus $ ls -l /etc/prometheus/rules/prometheus-k8s-rulefiles-0/|grep etcd
 7lrwxrwxrwx    1 root     2000            70 Sep 11 00:09 monitoring-etcd-rules-1a4df4bb-cab3-4d3c-b0a3-21461b0daac0.yaml -> ..data/monitoring-etcd-rules-1a4df4bb-cab3-4d3c-b0a3-21461b0daac0.yaml
 8/prometheus $
 9/prometheus $ cat  /etc/prometheus/rules/prometheus-k8s-rulefiles-0/monitoring-etcd-rules-1a4df4bb-cab3-4d3c-b0a3-21461b0daac0.yaml
10groups:
11- name: etcd
12  rules:
13  - alert: EtcdClusterUnavailable
14    annotations:
15      description: If one more etcd peer goes down the cluster will be unavailable
16      summary: etcd cluster small
17    expr: |
18      count(up{job="etcd"} == 0) > (count(up{job="etcd"}) / 2 - 1)
19    for: 3m
20    labels:
21      severity: critical
22/prometheus $

可以看到我们创建的 rule 文件已经被注入到了对应的 rulefiles 文件夹下面了,证明我们上面的设想是正确的。

然后再去 Prometheus Dashboard 的 Alert 页面下面就可以查看到上面我们新建的报警规则了:

3、配置报警

我们知道了如何去添加一个报警规则配置项,但是这些报警信息用怎样的方式去发送呢?前面的课程中我们知道我们可以通过 AlertManager 的配置文件去配置各种报警接收器,现在我们是通过 Operator 提供的 alertmanager 资源对象创建的组件,应该怎样去修改配置呢?

  • 首先我们去 Alertmanager 的页面上 status 路径下面查看 AlertManager 的配置信息:

  • 这些配置信息实际上是来自于 Prometheus-Operator 自动创建的名为 alertmanager-main-generated 的 Secret 对象:
 1[root@master1 ~]#kubectl get sts alertmanager-main -oyaml -nmonitoring
 2……
 3        volumeMounts:
 4        - mountPath: /etc/alertmanager/config
 5          name: config-volume
 6          
 7……
 8      volumes:
 9      - name: config-volume
10        secret:
11          defaultMode: 420
12          secretName: alertmanager-main-generated
 1$ kubectl get secret alertmanager-main-generated -n monitoring -o json | jq -r '.data."alertmanager.yaml"' | base64 --decode
 2"global":
 3  "resolve_timeout": "5m"
 4"inhibit_rules":
 5- "equal":
 6  - "namespace"
 7  - "alertname"
 8  "source_matchers":
 9  - "severity = critical"
10  "target_matchers":
11  - "severity =~ warning|info"
12- "equal":
13  - "namespace"
14  - "alertname"
15  "source_matchers":
16  - "severity = warning"
17  "target_matchers":
18  - "severity = info"
19- "equal":
20  - "namespace"
21  "source_matchers":
22  - "alertname = InfoInhibitor"
23  "target_matchers":
24  - "severity = info"
25"receivers":
26- "name": "Default"
27- "name": "Watchdog"
28- "name": "Critical"
29- "name": "null"
30"route":
31  "group_by":
32  - "namespace"
33  "group_interval": "5m"
34  "group_wait": "30s"
35  "receiver": "Default"
36  "repeat_interval": "12h"
37  "routes":
38  - "matchers":
39    - "alertname = Watchdog"
40    "receiver": "Watchdog"
41  - "matchers":
42    - "alertname = InfoInhibitor"
43    "receiver": "null"
44  - "matchers":
45    - "severity = critical"
46    "receiver": "Critical"

我们可以看到内容和上面查看的配置信息是一致的,所以如果我们想要添加自己的接收器,我们就可以直接更改这个文件,但是这里的内容是 base64 编码过后的,如果手动添加内容就非常不方便,为此 Prometheus-Operator 新增了一个 AlertmanagerConfig 的 CRD,比如我们将 Critical 这个接收器的报警信息都发送到钉钉进行报警。

1.部署钉钉 webhook 处理器

首先在 monitoring 命名空间下面部署一个简单的钉钉 webhook 处理器,前面 Alertmanager 章节已经学习过,这里就不赘述了。

2.新建AlertmanagerConfig 资源对象

然后新建一个 AlertmanagerConfig 类型的资源对象,可以通过 kubectl explain alertmanagerconfig 或者在线 API 文档来查看字段的含义

 1# alertmanager-config.yaml
 2apiVersion: monitoring.coreos.com/v1alpha1
 3kind: AlertmanagerConfig
 4metadata:
 5  name: dinghook
 6  namespace: monitoring
 7  labels:
 8    alertmanagerConfig: example
 9spec:
10  receivers:
11    - name: Critical
12      webhookConfigs:
13        - url: http://<webhook-url>
14          sendResolved: true
15  route:
16    groupBy: ["namespace"]
17    groupWait: 30s
18    groupInterval: 5m
19    repeatInterval: 12h
20    receiver: Critical
21    routes:
22      - receiver: Critical
23        match:
24          severity: critical

不过如果直接创建上面的配置是不会生效的,我们需要添加一个 Label 标签,并在 Alertmanager 的资源对象中通过标签来关联上面的这个对象,比如我们这里新增了一个 Label 标签:alertmanagerConfig: example,然后需要重新更新 Alertmanager 对象,添加 alertmanagerConfigSelector 属性去匹配 AlertmanagerConfig 资源对象:

 1# alertmanager-alertmanager.yaml
 2apiVersion: monitoring.coreos.com/v1
 3kind: Alertmanager
 4metadata:
 5  labels:
 6    alertmanager: main
 7  name: main
 8  namespace: monitoring
 9spec:
10  image: quay.io/prometheus/alertmanager:v0.21.0
11  nodeSelector:
12    kubernetes.io/os: linux
13  replicas: 3
14  securityContext:
15    fsGroup: 2000
16    runAsNonRoot: true
17    runAsUser: 1000
18  serviceAccountName: alertmanager-main
19  version: v0.21.0
20  configSecret:
21  alertmanagerConfigSelector: # 匹配 AlertmanagerConfig 的标签
22    matchLabels:
23      alertmanagerConfig: example
  • 现在我们重新更新上面的资源对象:
1kubectl apply -f alertmanager-config.yaml
2kubectl apply -f alertmanager-alertmanager.yaml

3.验证

更新完成后默认的配置会和我们创建的配置进行合并,我们可以重新查看生成的 Secret 资源对象内容,也可以直接查看 Alertmanager 的 WEB UI 界面的配置内容:

可以看到我们在 AlertmanagerConfig 里面定义的名为 Critical 的 Receiver,在最终生成的配置中名称了 monitoring-dinghook-Critical,格式为 <namespace>-<name>-<receiver name>

到这里我们就完成了 Prometheus Operator 的自定义监控和报警。

测试结束。😘

FAQ

注意:这里会出现2个node-exporter监控失败问题,重新创建下pod就好:

关于我

我的博客主旨:

  • 排版美观,语言精炼;
  • 文档即手册,步骤明细,拒绝埋坑,提供源码;
  • 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!

🍀 微信二维码

x2675263825 (舍得), qq:2675263825。

🍀 微信公众号

《云原生架构师实战》

🍀 csdn

https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421

🍀 博客

www.onlyyou520.com

🍀 知乎

https://www.zhihu.com/people/foryouone

🍀 语雀

https://www.yuque.com/books/share/34a34d43-b80d-47f7-972e-24a888a8fc5e?# 《云笔记最佳实践》

最后

好了,关于本次就到这里了,感谢大家阅读,最后祝大家生活快乐,每天都过的有意义哦,我们下期见!

推荐使用微信支付
微信支付二维码
推荐使用支付宝
支付宝二维码
最新文章

文档导航