GaussDB高智能--自治运维技术(中)

目录

[2.4 日志分析](#2.4 日志分析)

(1)日志解析阶段

(2)日志分析模型的训练

(3)在线检测模块

[2.5 慢SQL发现](#2.5 慢SQL发现)

(1)训练阶段

(2)预测流程

[2.6 慢SQL诊断](#2.6 慢SQL诊断)

(1)获取慢SQL相关信息

(2)获取数据库关键指标信息

(3)结合数据库关键指标信息和执行计划,分析慢SQL根因;

[2.7 集群故障根因诊断](#2.7 集群故障根因诊断)


2.4 日志分析

日志是产品最重要的基础运维数据之一,它是一种文本数据,一般由时间戳和文本消息(等级、常量、其余变量)组成,实时记录了业务的运行状态。因此凡是能够打印日志的设备,理论上均可以通过分析日志,进行故障预测、亚健康检测、故障定界定位等维护活动。对故障诊断任务而言,日志相较指标有着诊断代码段级别故障,支持对程序执行逻辑的跟踪,捕捉故障细节的优势,并且很多时候是唯一可用的故障诊断数据源。

GaussDB的日志分析功能主要是将非结构化的日志流,转换为时序数据,而后天然地与异常检测等机制进行对接,便于整合完整的异常诊断能力。日志分析功能包括两个关键部分,一个是日志采集模块,另一个是日志分析模块,这两个部分都在日志采集端实现,其整体模块结构图如下:

日志采集和分析的步骤如下:

(1)启动本功能的exporter, 利用Linux的inotify系统调用,监听日志写入事件;

(2)GaussDB数据库实例向被监听的日志文件目录写入日志;

(3)exporter 通过inotify系统调用,获知日志写入事件,exporter的LogCollector子模块从日志文件目录中读取被写入的日志文件内容;

(4)exporter的LogAnalyst模块获取写入的日志文件内容,并对其进行量化整理;

(5)时序数据库访问exporter,LogAnalyst将已经量化好的日志指标项返回时序数据库。

日志分析功能在具体实现上,主要包括离线学习模块和在线检测模块两部分,其设计流程图如下图所示:

其中离线学习模块具体有以下步骤:

(1)日志解析阶段

数据预处理:根据预设正则表达式去除噪声变量和拆分日志记录文本字符串得到单词列表,如日志记录"2020 04 23 17:01:11 INFO getGraceTime, customerID: lisi4, bpID: d1, graceTime:0"通过正则表达式替换customerID信息为通配符'*',并拆分字符和驼峰词命名词得到三部分,分别是时间:"2020 04 23 17:01:11",等级:"INFO","单词列表":['get', 'grace', 'time', ',', 'customer', 'id', ':', '*', ',', 'bp', 'id', ':', 'd1hg123', 'grace', 'time', ':', '0'];

日志初始模板提取:这里关键词指日志常量部分,一般来讲常用英文,通过关键词词典,这里使用了维基百科语料出现频率前10000且含有字母数目超过2的纯英文单词加上业务专有术语作为关键词词典,将上述单词列表里非关键词替换为通配符,得到日志初始模板['get', 'grace', 'time', ',', 'customer', 'id', ':', '*', ',', 'bp', 'id', ':', '*', 'grace', 'time', ':', '*'];

日志记录聚类:相同日志初始模板的日志记录会被聚类为一个日志集合,比如日志记录"getGraceTime, customerID: lisi4, bpID: d1, graceTime:0"与另一个日志记录"getGraceTime, customerID: zhangsan123, bpID: ss2, graceTime:0"的日志初始模板都是['get', 'grace', 'time', ',', 'customer', 'id', ':', '*', ',', 'bp', 'id', ':', '*', 'grace', 'time', ':', '*'],因此这两条日志记录会被聚类在同一个日志集合里;

日志聚类的不变量提取:对于同一日志集合里多条日志记录的单词列表相同位置的不变量提取日志模板,如对上述日志集合对应的单词列表['get', 'grace', 'time', ',', 'customer', 'id', ':', '*', ',', 'bp', 'id', ':', '*', 'grace', 'time', ':', '*']和['get', 'grace', 'time', ',', 'customer', 'id', ':', '*', ',', 'bp', 'id', ':', '*', 'grace', 'time', ':', '*'],通过相同位置的两两比较,保留其中不变量,变化的单词位置则替换为通配符'*',最后得到该日志集合里日志记录的日志模板"getGraceTime, customerID: *, bpID: *, graceTime:0";

可变深度模板前缀树构建:以日志模板诸的每个单词(包括常量和通配符)为节点构建可变深度模板前缀树,逐个插入上述过程中得到的日志模板,建立单词列表到日志模板的映射,可以在线对日志单词列表推理其日志模板。其中可变深度指单个通配符节点允许匹配更多或更少的单词,从而能实现在线提取变长变量的日志记录的模板。

(2)日志分析模型的训练

上述离线学习模块是与数据库日志结构和内容相关的,功能在发布时,会自带一个小的预训练模型。此模型的训练和使用过程对用户是透明的,用户无需任何额外操作。

(3)在线检测模块

日志解析阶段的数据预处理:与离线学习模块一致,根据预设正则表达式去除噪声变量和拆分日志记录文本字符串得到单词列表;

日志解析阶段的可变深度模板前缀树推理:通过模板前缀树匹配单词列表,将每个日志记录的单词列表映射到日志模板。如果出现变量数量有变化的日志,如"2020 04 23 17:01:11 INFO getGraceTime, customerID: lisi4,zhangsan3, bpID: d1, graceTime:0",可变深度模板前缀树会允许单个通配符节点"*"匹配更多的单词(这个例子里则是"lisi4"和"zhangsan3")提取出日志模板['get', 'grace', 'time', ',', 'customer', 'id', ':', '*', ',', 'bp', 'id', ':', '*', 'grace', 'time', ':', '0']。另外如果前缀树无法搜索到相应日志模板,则代表该日志模板为离线学习数据里没有出现过,会被标记为新增异常日志。

日志分析时,对已经在内存中模板化后的日志进行分析处理,提取我们希望使用的日志模板,进行指标量化。

2.5 慢SQL发现

慢SQL发现基于数据库的历史SQL语句,通过对历史SQL语句的执行表现进行总结归纳,将之再用于推断新的未知业务上。由于短时间内数据库SQL语句执行时长不会有太大的差距,SQLdiag可以从历史数据中检测出与已执行SQL语句相似的语句结果集,并基于SQL向量化技术和模板化方法预测SQL语句执行时长。慢SQL发现不需要SQL语句的执行计划,对数据库性能不会有任何的影响。

慢SQL发现的时序执行图如下:

在整个慢sql发现过程中,存在两个主要步骤:

  • 训练阶段:通过用户输入的日志地址导入历史sql数据,训练自编码模型,聚类模型及执行时间序列模型。

  • 预测阶段:用户输入待预测负载,系统根据训练阶段生成的自编码模型对待预测负载进行编码,之后根据训练阶段生成的聚类模型进行分类,进而根据每类的历史信息预测执行时间。

慢SQL发现中的训练阶段和预测阶段的详细设计如下:

(1)训练阶段

前提条件:用户已导入数据,准备好自己的典型业务SQL历史记录,且输入格式为:(SQL执行时长,SQL语句,SQL锁等待时长),其中锁等待时长和SQL执行时长二者必须保证至少有一个不为0. 每一行通过跳格(TAB)分隔。

用户在Console页面输入历史日志文件的路径与希望模型文件保存的路径。

前处理模块首先对日志进行前处理,抽取初级特征并对语句进行模板化,然后将模板化后的语句进行向量化处理,保存向量化过程所使用的字典。将数据集向量传入自编码模块。

自编码模块对数据集数据进行自编码训练,保存自编码模型并将编码后的数据集传给聚类模块。

聚类模块对编码后的数据集进行聚类,保存聚类模型。数据集在分类后,每类分别抽取所有类语句的历史执行时间记录,建立时间序列预测模型。

(2)预测流程

前提条件:训练阶段已完成,并且用户输入的待预测负载格式正确。

用户在页面输入待预测负载文件路径与结果文件保存路径。

与训练阶段同样,前处理模块会对待预测负载模板化,随后读取保存的字典将模板化后的语句向量化。在此步骤中,如果前处理模块捕捉到新元素的数量大于用户设定的重训练阈值,会提示用户该情况并询问用户是否重新训练。如果选是,则会重新开始训练阶段,否则继续执行预测阶段;如果前处理模块捕捉到新元素的数量不大于用户设定的重训练阈值,执行预测阶段。

读取训练阶段生成的自编码模型及聚类模型,对待预测负载进行编码后分类。

使用各类的时间序列预测模型,预测待预测负载的执行时间,保存至用户指定路径。

自编码器在设计时,可以采用2种不同的模型,分别为:

Doc2vec:着重于前后文特征关系的文本向量化编码器。

LSTM:着重于训练样本本身特征关系的向量化自编码器。

模型对比如下表所示:

|---------|---------------------------------|-----------------------------------------------|
| 模型 | 优点 | 缺点 |
| Doc2Vec | 训练和预测速度快,模型简单可控 | 容易产生过度解读短期词语,遗失距离较远的单词间关系的问题。 |
| LSTM | 计算量大、训练速度较慢,模型需要调优,对设计和实现的要求较高。 | 避免了Doc2Vect的主要缺点,使用逐层降维再还原的隐藏层训练语句中所有词语的内部关系。 |

聚类的设计流程图如下:

对任意一个新SQL模板,计算它的访问频次历史与现有聚类中心的距离,判断是否超过阈值。

检测已聚类的模板与聚类中心点的距离,如果相似度低于阈值,则移除当前聚类。

计算聚类中心之间的距离,如果超过阈值,则合并两个聚类。

通过以上三个步骤可以确保该online聚类算法能够对数据做到自适应,并且确保聚类中的模板与聚类中心始终是相似的。

预测模型采用3种不同出发点的模型,分别为:

  • 线性:线性意味着假定输入与输出之间存在一定线性关系。

  • 记忆:模型是否能够综合输入与它从历史数据中保存下的信息来预测未来。

  • 核函数:模型支持核函数就可以对非线性关系进行分析。

通常,线性模型能够有效的避免过拟合,并且对计算资源和训练数据要求较低,在预测较近的未来时间时表现较好。具有记忆性的模型可以挖掘数据的动态行为信息,但是增加了训练的复杂性与模型对数据的依赖。较好的一个方案是采用ENSEMBE方法,将多个模型进行合并做平均预测。

2.6 慢SQL诊断

慢SQL诊断提供诊断慢SQL所需要的必要信息,帮助开发者回溯执行时间超过阈值的SQL,诊断SQL性能瓶颈,用户无需通过复现就能离线诊断特定慢SQL的性能问题。慢SQL提供表和函数两种维度的查询接口,用户从接口中能查询到作业的执行计划,开始、结束执行时间,执行查询的语句,行活动,内核时间,CPU时间,执行时间,解析时间,编译时间,查询重写时间,计划生成时间,网络时间,IO时间,网络开销,锁开销等。所有信息都是脱敏的。

慢SQL诊断的执行流程如下:

在慢SQL根因诊断过程中,主要包括以下步骤:

(1)获取慢SQL相关信息

慢SQL相关信息包括SQL文本信息、SQL执行计划、SQL执行开始时间、SQL执行结束时间、SQL执行计划;

(2)获取数据库关键指标信息

数据库关键指标信息包括数据库部分GUC参数、状态参数、资源指标、负载指标、节点上其余进程信息,典型指标信息如下表所示:

|----------|--------------------|---------------------------|
| 参数类型 | 参数名称 | 参数解释 |
| Guc参数 | Work_mem | 写入临时文件之前内部排序操作和散列表使用的内存量 |
| Guc参数 | Shared_buffers | 数据库共享内存 |
| Guc参数 | max_connections | 最大客户端连接数 |
| 状态参数 | QPS | 数据库每秒请求数 |
| 状态参数 | Current_connection | 数据库当前连接数 |
| 资源指标 | Cpu usage | 数据库cpu使用率 |
| 资源指标 | Memory_usage | 数据库内存使用率 |
| 资源指标 | IO | 数据库磁盘IO(IO read和IO write) |
| 资源指标 | Disk_usgae | 数据库磁盘使用率 |

(3)结合数据库关键指标信息和执行计划,分析慢SQL根因;

在慢SQL文本、执行计划和其他信息的基础上,基于特定规则对慢SQL的根因进行分析,涉及到的规则如下:

  • 情况一:执行计划中出现SeqScan算子,且该算子执行代价较大或扫描行数较大,该场景下扫描算子IO占用高,引起性能慢;

模糊查询like,建议避免模糊查询或全模糊查询;

查询中包含is not null,建议不要使用is not null,否则会导致索引失效;

查询条件中使用了不等于操作符(!=),SQL中的不等操作符限制索引,引起全表扫描,建议把不等操作符改成or;

or语句连接条件中包含列没有全部创建索引,建议相关列全部创建索引;

update语句update了全部字段,建议如果只更改了1、2个字段,不要update全部字段、否则频繁调用会引起明显的性能消耗;

select count(*) from table,建议如果不是必要的话请杜绝此操作;

where子句中对字段进行表达式操作,这将导致SQL引擎放弃使用索引而使用全表扫描,建议修改where子句,对字段不要进行表达式操作;

where子句中使用函数操作,导致SQL引擎放弃使用索引而进行全表扫描,建议避免在where子句中使用函数操作;

not in 导致全表扫描,建议对于连续性的数值,可以使用between代替;

范围查询语句中的range条件范围过大;

  • 情况二:查询语句执行计划sort算子代价较大,同时在该SQL执行期间,数据目录下的base/pgsql_tmp路径下产生了临时文件;该场景下盘触发读写IO,引起的性能慢;

此时可能原因是查询语句排序成本过高,导致出现慢SQL,建议调整work_mem的大小;

  • 情况三:两表join操作,选择了NestLoop方式,且执行代价较大,该场景计算慢,引起的性能慢;

此时可能原因是大表join操作,NestLoop算子执行较慢,导致慢SQL,建议通过设置GUC参数enable_nestloop=off关掉NestLoop,让优化器选择其他join方式;

  • 情况四:Agg操作使用Sort Aggregate算子,且执行代价较大;

此时可能原因是对于较大结果集的Aggregate操作,Sort Aggregate算子的性能较差,建议通过设置GUC参数enable_sort=off,让优化器选择HashAgg算子;

  • 情况五:执行计划正常,不存在代价较大算子;

相关列中存在大量重复索引,导致插入性能速度变慢,建议先删除重复索引后再进行插入操作;

数据库负载请求集中,引起IO、CPU、MEMORY等资源指标上升,导致数据库资源紧张从而产生慢SQL;

外部进程占用大量系统资源,导致数据库资源紧张,进而出现慢SQL,建议停止没有必要的大进程;

慢SQL根因分析有两种使用方式,第一种方式是定期从sqlite数据库中拉取慢SQL信息,并基于上述规则去分析慢SQL根因,最后将结果输出到日志;第二种方式是通过用户输入慢SQL信息,然后基于特定规则去分析慢SQL根因,最后将结果输出到命令行;

使用时将Anomaly-detection中的main.py作为统一程序入口,与慢SQL诊断相关参数如下表所示,命令如下:

python main.py [-m MODE] [-q QUERY] [-s START_TIME] [-e END_TIME]

|---------------|----------------------------------------------------------------------------------------------------------|
| 参数名称 | 释义 |
| -m MODE | 指定运行模式 [collect, server],其中collect是数据采集服务,server是数据收集服务、资源异常检测和慢SQL诊断服务,后续也讲和web server对接,将收集数据显示在网页中。 |
| diagnosis | 用户自行调用慢SQL根因分析功能 |
| -q QUERY | SQL文本信息 |
| -s START_TIME | SQL执行开始的时间(参考作用,可选) |
| -e END_TIME | SQL执行结束的时间(参考作用,可选) |

Collect模式用于在数据库节点本地收集信息方式;Server模式用于数据存储、异常检测和慢SQL诊断,后续还会扩展其他功能。

基于自动采集后,Server自主发现问题,示例结果如下所示:

{START-TIME: 2021-03-22 16:58:19, END-TIME: 2021-03-25 16:58:19,SQL: select i_id from bmsql_item where i_id not in(1,100),RCA: The plan contains SeqScan operator, possibly caused by 'NOT IN',Suggestion: For the continuous values, 'BETWEEN' is recommended}

用户自行调用慢SQL根因分析功能:

python main.py diagnosis -q 'select i_id from bmsql_item where i_id not in(1,100)' -s '2021-03-22 16:58:19' -e '2021-03-25 16:58:19'

输出结果如下:

|-----------------------------------------------------------------------------------------------------------------------------------------|
| RCA:The plan contains SeqScan operator,and is possibly caused by NOT IN, Suggestion: For the continuous values, BETWEEN is recommended. |

上述示例中,用户输入了SQL执行的起止时间,工具会在数据库WDR报告中在该时间范围内进行搜寻,以发现SQL的精确执行时间和结束时间,之后基于上述规则进行分析;如果用户在命令行中只输入了一种时间,则工具会以该时间为基准,在正负2小时之内进行搜寻,以匹配该SQL的精确时间信息;如果用户没有提供任何的时间信息,则工具会以现在时间为基准,往前推24小时进行搜寻,以确定该SQL的精确执行时间信息;如果在搜寻时间范围内都没有发现该条SQL,则会在命令行输出如下信息:

|--------------------------------------------------------------|
| Error: Sorry, can not find the information of this slow SQL. |

同时该功能只支持单条SQL的分析,如果同时输入多条SQL,则只会对第一条进行分析。

2.7 集群故障根因诊断

在现网业务中需要对发生的故障原因进行快速定位定界,集群故障根因诊断功能可以通过收集数据库集群中各个组件(如CMS、DN)等的信息和即时状态(如网络连通性),来判断集群环境是否存在故障,以及故障根因。可用于实现集群级别的故障根因诊断。

自治运维服务支持CN、DN、CMS等节点日志采集,以及支持基于节点间网络连通(如ping)状态采集。经过故障场景分析和梳理,并对数据集进行枚举扩充,最终实现DN、CN故障快速定位。

集群故障根因诊断的设计和交互图如下:

自治运维服务通过采集各个组件(如DN、CN、CMS等)的关键日志信息,以及即时状态信息(如网络连通状态),构建集群状态特征,然后,根据DBMind预置的根因模型库,对问题根因进行判断。对于日志信息的采集对日志格式有一定要求,当日志格式发生变化时,需要代码同步更新以实现采集。下表中的"包含关键词"列即为采集日志所使用的正则匹配格式。

其中,DN组件故障场景特征与根因分别下表所示:

|---------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 名称 | 日志种类 | 包含关键词(DN组件故障场景特征) |
| 非日志 | 非日志 | DN节点状态: Normal、Unknown、Need repair、Wait promoting、Promoting or Demoting、Disk damaged、Port conflicting、Building、Build failed、CoreDump、ReadOnly、Manually stopped 注意:Normal外的状态都视为异常 |
| CMA日志 | CMA日志 | datanodeId=(.*?), dn_manual_stop=([0,1]), g_dnDiskDamage=([0,1]), g_nicDown=([0,1]), port_conflict=([0,1]) |
| CMA日志 | CMA日志 | datanodeId=(.*?), dn_manual_stop=([0,1]), g_dnDiskDamage=([0,1]), g_nicDown=([0,1]), port_conflict=([0,1]) |
| CMA日志 | CMA日志 | datanodeId=(.*?), dn_manual_stop=([0,1]), g_dnDiskDamage=([0,1]), g_nicDown=([0,1]), port_conflict=([0,1]) |
| system_call日志 | system_call日志 | could not bind (.*?) socket: Is another instance already running on port (.*?) |
| CMA日志 | CMA日志 | datanodeId=(.*?), dn_manual_stop=([0,1]), g_dnDiskDamage=([0,1]), g_nicDown=([0,1]), port_conflict=([0,1]) |
| CMS日志 | CMS日志 | restart (.*?), there is not report msg for |
| CMS日志 | CMS日志 | restart to pending |
| CMS日志 | CMS日志 | (instance|instanceId:) (.*?) (is transaction |set |was set |is )read only |
| 非日志 | 非日志 | DN节点IP是否可以Ping通 |
| ffic日志 | ffic日志 | 任意内容 |
| 内核日志 | 内核日志 | walreceiver could not connect and shutting down |
| CMA日志 | CMA日志 | data path disc writable test failed |

DN根因和特征的关系流程图如下:

DN故障诊断部分特征和根因对应关系如下表,对于日志类特征,0代表对应日志不包含关键字,1代表对应日志包含关键字,对于其他非日志类特征,0代表否,1代表是,最后不同标签数值代表不同的根因。如下表为DN组件特征与根因对应关系示例:

|---------------|---------------------------------------------------------------------------------------------------------------------|---|----|----|----|----|---|---|---|
| 非日志 | DN节点状态: | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| CMA日志 | datanodeId=(.*?), dn_manual_stop=([0,1]), g_dnDiskDamage=([0,1]), g_nicDown=([0,1]), port_conflict=([0,1]) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| CMA日志 | datanodeId=(.*?), dn_manual_stop=([0,1]), g_dnDiskDamage=([0,1]), g_nicDown=([0,1]), port_conflict=([0,1]) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| CMA日志 | datanodeId=(.*?), dn_manual_stop=([0,1]), g_dnDiskDamage=([0,1]), g_nicDown=([0,1]), port_conflict=([0,1]) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| system_call日志 | could not bind (.*?) socket: Is another instance already running on port (.*?) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| CMA日志 | datanodeId=(.*?), dn_manual_stop=([0,1]), g_dnDiskDamage=([0,1]), g_nicDown=([0,1]), port_conflict=([0,1]) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| CMS日志 | restart (.*?), there is not report msg for | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| CMS日志 | phony dead times\(.*?:.*?\) already exceeded, will restart\((.*?)\) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| CMS日志 | restart to pending | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| CMS日志 | (instance|instanceId:) (.*?) (is transaction |set |was set |is )read only | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 非日志 | DN节点IP是否可以Ping通 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
| ffic日志 | 任意内容 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
| 内核日志 | walreceiver could not connect and shutting down | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 1 |
| CMA日志 | data path disc writable test failed | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
| 标签 | 对应故障结果 | 2 | 10 | 10 | 10 | 10 | 9 | 9 | 9 |

通过上述根因和特性之间关联,利用决策树模型,根据故障日志信息,快速获取故障根因,方便运维人员及时进行故障排除。

以上内容从日志分析、慢SQL发现、慢SQL诊断、集群故障根因诊断等方面介绍了GaussDB的自治运维技术,下篇我们将从索引推荐、分布键推荐、参数调优等三方面继续解读GaussDB的自治运维技术。

相关推荐
海害嗨5 分钟前
阿里巴巴官方「SpringCloudAlibaba全彩学习手册」限时开源!
学习·开源
Elastic 中国社区官方博客20 分钟前
Elasticsearch 中的热点以及如何使用 AutoOps 解决它们
大数据·运维·elasticsearch·搜索引擎·全文检索
如意机反光镜裸28 分钟前
如何快速将Excel数据导入到SQL Server数据库
数据库
DC_BLOG30 分钟前
Linux-Nginx虚拟主机
linux·运维·nginx
坐公交也用券34 分钟前
使用Python3实现Gitee码云自动化发布
运维·gitee·自动化
不爱学习的啊Biao44 分钟前
初识mysql数据库
数据库·mysql·oracle
执键行天涯1 小时前
【日常经验】修改大数据量的表字段类型,怎么修改更快
sql
小A1591 小时前
STM32完全学习——使用SysTick精确延时(阻塞式)
stm32·嵌入式硬件·学习
1900431 小时前
linux复习5:C prog
linux·运维·服务器
小A1591 小时前
STM32完全学习——使用标准库点亮LED
stm32·嵌入式硬件·学习