在CSDN学Golang云原生(监控解决方案Prometheus)
一,记录规则配置
在golang云原生中,通常使用日志库记录应用程序的日志。其中比较常见的有logrus、zap等日志库。这些库一般支持自定义的输出格式和级别,可以根据需要进行配置。
对于云原生应用程序,我们通常会采用容器化技术将其部署到容器集群中。为了方便管理和监控应用程序的运行状态,我们需要将其日志输出到标准输出流或者标准错误流,并且按照一定的规则格式化输出内容。例如Kubernetes就规定了Container Logging Best Practices,在此基础上提供了多种处理日志的方式,如Fluentd、Elasticsearch+Kibana等。
在golang云原生中,我们可以通过以下方式配置记录规则:
- 使用第三方日志库
在使用第三方日志库时,可以通过设置Formatter属性来指定输出格式和级别。例如,在使用logrus时,我们可以设置:
import ("github.com/sirupsen/logrus"
)func main() {log := logrus.New()// 设置Formatterformatter := &logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05",}log.Formatter = formatter// 设置Levellog.Level = logrus.DebugLevel// 输出日志log.Info("hello world")
}
- 直接使用标准包
如果不想使用第三方包,则可以直接使用golang标准库中的log包进行日志输出。例如:
import ("log"
)func main() {// 输出到标准错误流log.SetOutput(os.Stderr)// 设置前缀和标记log.SetPrefix("[myapp] ")log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)// 输出日志log.Println("hello world")
}
需要注意的是,使用log包输出到标准错误流时,需要将日志级别设置为log.PanicLevel
或者log.FatalLevel
,否则在Kubernetes容器中无法正确处理。
- 配置容器日志
对于Kubernetes集群来说,我们可以通过修改PodSpec中的配置项来指定应用程序的日志输出规则。例如,在Deployment或者StatefulSet对象的yaml文件中加入以下内容:
apiVersion: apps/v1beta2
kind: Deployment
metadata:name: myapp-deployment
spec:replicas: 1selector:matchLabels:app: myapptemplate:metadata:labels:app: myappspec:containers:- name: myapp-containerimage: myregistry/myapp:v1.0.0ports:- containerPort: 8080# 配置容器日志输出规则 volumeMounts:- name: logs-volume mountPath: /var/log/myapp# 定义volume以便将日志文件挂载到宿主机上 volumes:- name: logs-volume emptyDir: {}
在上述配置中,我们首先定义了一个名为logs-volume
的Volume,并将其挂载到容器中的/var/log/myapp
目录下。然后,在容器的定义中,通过设置volumeMounts属性来将该Volume挂载到指定目录。
最后,在应用程序中,可以通过直接向标准输出流或者标准错误流输出日志,即可将日志记录到位于宿主机上的日志文件中:
import ("log"
)func main() {// 输出到标准错误流log.SetOutput(os.Stderr)// 设置前缀和标记log.SetPrefix("[myapp] ")log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)// 输出日志log.Println("hello world")
}
需要注意的是,Kubernetes支持多种处理容器日志的方式,例如使用Fluentd、Elasticsearch+Kibana等工具对容器日志进行收集和分析。因此,在实际应用中还需要根据实际情况进行相应的配置。
二,报警规则配置
在golang云原生应用中,我们通常使用监控系统对应用程序进行实时监控,并且在出现异常情况时触发报警。常见的监控系统包括Prometheus、Zabbix等。
以下是一些常见的报警规则配置方法:
- Prometheus
Prometheus是一个开源的监控和报警工具,可以通过其提供的Alertmanager组件进行报警。Alertmanager可以根据Prometheus收集到的指标数据和用户定义的告警规则来判断是否需要发送告警信息。
首先,需要定义一条或多条告警规则,并将其保存在rules文件中。例如:
groups:
- name: myapp.rulesrules:- alert: HighErrorRateexpr: sum(rate(http_request_duration_seconds_count{job="myapp"}[5m])) by (handler) > 0.5for: 10mlabels:severity: warningannotations:summary: "High request latency"description: "{{ $labels.handler }} has high request latency"
在上述告警规则中,我们定义了一个名为HighErrorRate
的告警规则,该规则会检测http请求处理时间是否超过0.5秒,并且持续10分钟以上。如果符合条件,则会发送一条warning级别的告警消息。
然后,在Prometheus配置文件中,我们需要将该rules文件引入进来,并且设置相应的alerting_rules属性。例如:
rule_files:- /etc/prometheus/rules/*.rulesalerting:alertmanagers:- static_configs:- targets: ['localhost:9093']
最后,在Alertmanager配置文件中,我们需要定义告警规则与报警接收方式之间的映射关系。例如:
route:group_by: ['alertname']receiver: 'default'
receivers:
- name: 'default'email_configs:- to: 'admin@example.com'
在上述配置中,我们将所有告警规则都归类到同一个组中,并且指定了默认的报警接收方式为发送邮件给admin@example.com
。
- Zabbix
Zabbix是一款开源的企业级监控系统,也可以用于进行告警。通过在Zabbix Server中创建相应的监控项和触发器,当监控项的值满足触发器条件时就会触发告警。
首先,在Zabbix Web界面中创建一个新的触发器,并设置相应的条件和阈值。例如:
{myapp.server1:http_requests.avg(5m)}>10
在上述触发器中,我们设置了一个名为http_requests
的监控项,并计算其过去5分钟内请求平均值是否大于10。如果满足该条件,则会触发告警。
然后,在Zabbix Server端创建一个新的Action并指定相应的通知方式。例如:
- Name: Send message to Admins
- Conditions: Trigger value = PROBLEM
- Operations: Send message to user "Admin", with subject "MyApp alert" and body "{TRIGGER.NAME}: {TRIGGER.STATUS}"
- Recovery operations: Send message to user "Admin", with subject "MyApp recovery" and body "{TRIGGER.NAME} recovered"
在上述Action中,我们指定了当触发器值为PROBLEM时,发送消息给用户Admin
,并设置相应的主题和内容。
需要注意的是,Zabbix支持多种通知方式,如邮件、短信等。因此,在实际应用中还需要根据实际情况进行相应的配置。
三,PromQL查询语句
PromQL是Prometheus的查询语言,可以用于从Prometheus中查询和分析数据。以下是一些常见的Golang云原生PromQL查询语句:
- 查询CPU使用率
sum(rate(process_cpu_seconds_total{job="myapp"}[5m])) by (instance)
上述查询语句用于计算过去5分钟内各实例的CPU使用率,并按照实例进行汇总。其中,process_cpu_seconds_total
是一个指标,表示进程在CPU上花费的时间。
- 查询内存使用量
sum(container_memory_usage_bytes{image!="", container_name!=""}) by (container_name)
上述查询语句用于计算所有容器当前的内存使用量,并按照容器名称进行汇总。其中,container_memory_usage_bytes
是一个指标,表示容器当前使用的内存大小。
- 查询HTTP请求情况
rate(http_requests_total{job="myapp", handler="/api/v1/users"}[5m])
上述查询语句用于计算过去5分钟内处理/api/v1/users
接口请求的速率。其中,http_requests_total
是一个指标,表示HTTP请求数量。
- 查询响应时间分布情况
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="myapp"}[1m])) by (le))
上述查询语句用于计算过去1分钟内各档位HTTP响应时间(如小于100ms、200ms等)的速率,并计算95%分位点的响应时间。其中,http_request_duration_seconds_bucket
是一个指标,表示HTTP请求处理时间。
需要注意的是,PromQL语法较为复杂,以上查询语句仅供参考。在实际使用中,还需结合具体场景进行相应的调整和优化。
四,influxdb远程数据中心配置
在Golang云原生应用中使用InfluxDB进行数据存储,如果需要实现远程数据中心配置,则需要进行以下步骤:
- 在InfluxDB服务器上创建数据库。
首先,在远程数据中心的InfluxDB服务器上创建一个数据库。可以使用influx
命令行工具或者Web管理界面来创建。
- 安装InfluxDB Go客户端库。
在Golang项目中,使用InfluxDB Go客户端库可以方便地进行与InfluxDB的交互。可以通过以下命令来安装该库:
go get github.com/influxdata/influxdb-client-go
- 配置连接信息
在Golang应用程序中,通过如下方式设置连接信息:
package mainimport ("fmt""github.com/influxdata/influxdb-client-go"
)func main() {// 设置连接信息client := influxdb2.NewClientWithOptions("http://my-remote-influx-server:8086", "my-token",influxdb2.DefaultOptions().SetBatchSize(2000).SetFlushInterval(10000))defer client.Close()// 连接到指定数据库writeAPI := client.WriteAPIBlocking("", "my-database")// 写入数据p := influxdb2.NewPoint("my-measurement",map[string]string{"key": "value"},map[string]interface{}{"field1": 1.0, "field2": int64(5)},time.Now())writeAPI.WritePoint(context.Background(), p)}
其中,第一行代码指定了要连接到的远程数据中心InfluxDB服务器的地址和端口号;第二行代码使用访问令牌进行身份验证。
- 写入数据
通过WriteAPI
接口可以方便地向指定数据库写入数据,例如:
p := influxdb2.NewPoint("my-measurement",map[string]string{"key": "value"},map[string]interface{}{"field1": 1.0, "field2": int64(5)},time.Now())
writeAPI.WritePoint(context.Background(), p)
上述代码表示向名为my-measurement
的测量点中写入一个包含两个字段的记录。其中,第三个参数是一个键值对形式的对象,表示要写入的字段及其对应的值。
需要注意的是,在生产环境下,还需考虑诸如重试机制、错误处理等因素。
Golang云原生学习路线图、教学视频、文档资料、面试题资料(资料包括C/C++、K8s、golang项目实战、gRPC、Docker、DevOps等)免费分享 有需要的可以加qun:793221798领取
五,报警信息配置管理
在Golang云原生应用程序中,可以使用Prometheus作为监控和报警的工具,通过配置Prometheus告警规则文件以及Alertmanager配置文件来实现报警信息的管理。下面是一个简单的示例:
- 配置Prometheus告警规则
首先,在Prometheus服务器上创建一个告警规则文件prometheus.rules.yml
,并将其放置到指定目录中。例如:
groups:
- name: my-rulesrules:- alert: HighRequestRateexpr: sum(rate(http_requests_total{job="my-service"}[5m])) > 100for: 1mlabels:severity: warningannotations:summary: "High request rate detected"description: "The HTTP request rate is too high ({{ $value }} requests/minute)."
上述代码表示当5分钟内HTTP请求速率超过100个/分钟时触发告警,并在持续1分钟后自动解除。同时,该告警级别为warning。
- 配置Alertmanager
接下来,在Alertmanager服务器上创建一个配置文件alertmanager.yml
,并将其放置到指定目录中。例如:
global:resolve_timeout: 5mroute:group_by: ['alertname', 'severity']group_wait: 30sgroup_interval: 5mrepeat_interval: 12hreceivers:
- name: 'slack-webhook'slack_configs:- channel: '#alerts'send_resolved: truetitle_link: 'http://prometheus-server:9090/'username: 'Prometheus'api_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
上述代码表示使用Slack作为报警接收渠道,并配置了一个名为slack-webhook
的接收器,用于将告警信息发送到指定的Slack频道中。需要注意的是,其中的api_url
参数需要根据实际情况进行修改。
- 在Golang应用程序中引入Prometheus客户端库
在Golang应用程序中,通过如下方式引入Prometheus客户端库:
import "github.com/prometheus/client_golang/prometheus"
- 在Golang应用程序中添加监控指标
通过Prometheus客户端库,可以方便地向Prometheus服务器注册监控指标。例如:
httpRequestsTotal := prometheus.NewCounterVec(prometheus.CounterOpts{Name: "http_requests_total",Help: "The total number of HTTP requests.",
}, []string{"method", "status"})prometheus.MustRegister(httpRequestsTotal)http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 处理HTTP请求httpRequestsTotal.WithLabelValues(r.Method, strconv.Itoa(statusCode)).Inc()
})
上述代码表示定义了一个名为http_requests_total
的计数器类型监控指标,并在HTTP请求处理函数中对其进行更新。
- 启动Alertmanager和Prometheus服务器
最后,在启动Alertmanager和Prometheus服务器时,需要分别指定前面创建的配置文件路径。例如:
$ prometheus --config.file=prometheus.yml
$ alertmanager --config.file=alertmanager.yml
在启动后,Prometheus服务器将会按照规则文件中的设置进行监控,并在满足告警条件时向Alertmanager发送报警信息。Alertmanager收到报警信息后,将根据配置文件中的设置将其发送至指定接收器中。
六,自定义应用程序指标
在Golang云原生应用程序中,可以使用Prometheus客户端库来定义自定义的监控指标。下面是一个简单的示例:
- 引入Prometheus客户端库
在Golang应用程序中引入Prometheus客户端库:
import ("github.com/prometheus/client_golang/prometheus""github.com/prometheus/client_golang/prometheus/promauto"
)
- 定义自定义监控指标
使用promauto.NewCounterVec()
方法创建一个计数器类型的监控指标,并为其设置标签和描述信息。
var (myCustomMetric = promauto.NewCounterVec(prometheus.CounterOpts{Name: "my_custom_metric",Help: "This is my custom metric.",}, []string{"status_code"})
)
上述代码表示创建了一个名为my_custom_metric
的计数器类型监控指标,并为其设置了一个名为status_code
的标签。
- 在应用程序中更新监控指标
在应用程序处理请求或其他相关操作时,通过调用监控指标的相应方法来更新其值。例如:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 处理HTTP请求statusCode := 200 // 假设返回状态码为200myCustomMetric.WithLabelValues(strconv.Itoa(statusCode)).Inc()
})
上述代码表示在处理HTTP请求时,根据实际情况更新了自定义的监控指标值。
- 启动Prometheus服务器
最后,在启动Prometheus服务器时,需要注册之前定义的自定义监控指标。例如:
prometheus.MustRegister(myCustomMetric)
在启动后,Prometheus服务器将会按照配置文件中的设置进行监控,并收集和记录应用程序中定义的所有监控指标。可以通过访问http://localhost:9090/metrics
来查看当前收集到的所有监控指标信息。
相关文章:
在CSDN学Golang云原生(监控解决方案Prometheus)
一,记录规则配置 在golang云原生中,通常使用日志库记录应用程序的日志。其中比较常见的有logrus、zap等日志库。这些库一般支持自定义的输出格式和级别,可以根据需要进行配置。 对于云原生应用程序,我们通常会采用容器化技术将其…...

双重for循环优化
项目中有段代码逻辑是个双重for循环,发现数据量大的时候,直接导致数据接口响应超时,这里记录下不断优化的过程,算是抛砖引玉吧~ Talk is cheap,show me your code! 双重for循环优化 1、数据准备2、原始双重for循环3、…...

golang利用go mod巧妙替换使用本地项目的包
问题 拉了两个项目下来,其中一个项目依赖另一个项目,因为改动了被依赖的项目,想重新导入测试一下。 解决办法 go.mod文件的require中想要被代替的包名在replace中进行一个替换,注意:用来替换的需要用绝对路径…...
使用 docker 一键部署 MySQL
目录 1. 前期准备 2. 导入镜像 3. 创建部署脚本文件 4. MySQL 服务器配置文件模板 5. 执行脚本创建容器 6. 后续工作 7. 基本维护 1. 前期准备 新部署前可以从仓库(repository)下载 MySQL 镜像,或者从已有部署中的镜像生成文件&#x…...

MyBatis-Plus 查询PostgreSQL数据库jsonb类型保持原格式
文章目录 前言数据库问题背景后端返回实体对象前端 实现后端返回List<Map<String, Object>>前端 前言 在这篇文章,我们保存了数据库的jsonb类型:MyBatis-Plus 实现PostgreSQL数据库jsonb类型的保存与查询 这篇文章介绍了模糊查询json/json…...

Linux操作系统1-命令篇
不同领域的主流操作系统 桌面操作系统 Windos Mac os Linux服务器操作系统 Unix Linux(免费、稳定、占有率高) Windows Server移动设备操作系统 Android(基于Linux,开源) ios嵌入式操作系统 Linux(机顶盒、路由器、交换机) Linux 特点:免费、开源、多用户、多任务…...

opencv-24 图像几何变换03-仿射-cv2.warpAffine()
什么是仿射? 仿射变换是指图像可以通过一系列的几何变换来实现平移、旋转等多种操作。该变换能够 保持图像的平直性和平行性。平直性是指图像经过仿射变换后,直线仍然是直线;平行性是指 图像在完成仿射变换后,平行线仍然是平行线。…...
前端常用的条件限制方法小笔记
手机号的正则表达式(以1开头的11位数字) function checkPhone(){ var phone document.getElementById(phone).value;if(!(/^1[3456789]\d{9}$/.test(phone))){ alert("手机号码有误,请重填"); return false; } }限制输入大于0且最小值要小于最大值 c…...
【LeetCode 算法】Minimum Operations to Halve Array Sum 将数组和减半的最少操作次数-Greedy
文章目录 Minimum Operations to Halve Array Sum 将数组和减半的最少操作次数问题描述:分析代码TLE优先队列 Tag Minimum Operations to Halve Array Sum 将数组和减半的最少操作次数 问题描述: 给你一个正整数数组 nums 。每一次操作中,你…...

Doc as Code (3):业内人士的观点
作者 | Anne-Sophie Lardet 在技术传播国际会议十周年之际,Fluid Topics 的认证技术传播者和功能顾问 Gaspard上台探讨了“docOps 作为实现Doc as Code的中间结构”的概念。在他的演讲中,观众提出了几个问题,我们想分享Gaspard的见解&#x…...

【Kafka】消息队列Kafka基础
目录 消息队列简介消息队列的应用场景异步处理系统解耦流量削峰日志处理 消息队列的两种模式点对点模式发布订阅模式 Kafka简介及应用场景Kafka比较其他MQ的优势Kafka目录结构搭建Kafka集群编写Kafka一键启动/关闭脚本 Kafka基础操作创建topic生产消息到Kafka从Kafka消费消息使…...

Java的第十五篇文章——网络编程(后期再学一遍)
目录 学习目的 1. 对象的序列化 1.1 ObjectOutputStream 对象的序列化 1.2 ObjectInputStream 对象的反序列化 2. 软件结构 2.1 网络通信协议 2.1.1 TCP/IP协议参考模型 2.1.2 TCP与UDP协议 2.2 网络编程三要素 2.3 端口号 3. InetAddress类 4. Socket 5. TCP网络…...

【深度学习】High-Resolution Image Synthesis with Latent Diffusion Models,论文
13 Apr 2022 论文:https://arxiv.org/abs/2112.10752 代码:https://github.com/CompVis/latent-diffusion 文章目录 PS基本概念运作原理 AbstractIntroductionRelated WorkMethodPerceptual Image CompressionLatent Diffusion Models Conditioning Mec…...

前端学习——Vue (Day6)
路由进阶 路由的封装抽离 //main.jsimport Vue from vue import App from ./App.vue import router from ./router/index// 路由的使用步骤 5 2 // 5个基础步骤 // 1. 下载 v3.6.5 // 2. 引入 // 3. 安装注册 Vue.use(Vue插件) // 4. 创建路由对象 // 5. 注入到new Vue中&…...

STM32MP157驱动开发——按键驱动(tasklet)
文章目录 “tasklet”机制:内核函数定义 tasklet使能/ 禁止 tasklet调度 tasklet删除 tasklet tasklet软中断方式的按键驱动程序(stm32mp157)tasklet使用方法:button_test.cgpio_key_drv.cMakefile修改设备树文件编译测试 “tasklet”机制: …...

PostgreSQL构建时间
– PostgreSQL构建时间 select make_timestamp(2023,7,27,7,34,16);...

2023-将jar包上传至阿里云maven私有仓库(云效制品仓库)
一、背景介绍 如果要将平时积累的代码工具jar包,上传至云端,方便团队大家一起使用,一般的方式就是上传到Maven中心仓库(但是这种方式步骤多,麻烦,而且上传之后审核时间比较长,还不太容易通过&a…...

嵌入式linux之OLED显示屏SPI驱动实现(SH1106,ssd1306)
周日业余时间太无聊,又不喜欢玩游戏,大家的兴趣爱好都是啥?我觉得敲代码也是一种兴趣爱好。正巧手边有一块儿0.96寸的OLED显示屏,一直在吃灰,何不把玩一把?于是说干就干,最后在我的imax6ul的lin…...

关于element ui 安装失败的问题解决方法、查看是否安装成功及如何引入
Vue2引入 执行npm i element-ui -S报错 原因:npm版本太高 报错信息: 解决办法: 使用命令: npm install --legacy-peer-deps element-ui --save 引入: 在main.js文件中引入 //引入Vue import Vue from vue; //引入…...

Selenium多浏览器处理
Python 版本 #导入依赖 import os from selenium import webdriverdef test_browser():#使用os模块的getenv方法来获取声明环境变量browserbrowser os.getenv("browser").lower()#判断browser的值if browser "headless":driver webdriver.PhantomJS()e…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...

使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
应用升级/灾备测试时使用guarantee 闪回点迅速回退
1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间, 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点,不需要开启数据库闪回。…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...

MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...

遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...

【2025年】解决Burpsuite抓不到https包的问题
环境:windows11 burpsuite:2025.5 在抓取https网站时,burpsuite抓取不到https数据包,只显示: 解决该问题只需如下三个步骤: 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...