RabbitMQ 进程内流控(Flow Control) 源码解析

news2024/11/17 23:47:05

1. 概述

1.1 为什么要流控?

流控主要是为了防止生产者生产消息速度过快,超过 Broker 可以处理的速度。这时需要暂时限制生产者的生产速度,让 Broker 的处理能够跟上生产速度。

Erlang进程之间不共享内存,每个进程都有自己的进程邮箱,进程间只通过消息来通信。Erlang没有对进程邮箱的大小进行限制,所以当有大量消息持续发往某个进程时,会导致该进程邮箱过大,最终内存溢出并崩溃。如果没有流控,可能会导致内部进程邮箱的大小很快达到内存阈值。

1.2 RabbitMQ 的多种流控机制

1.2.1 全局流控(内存高水位、磁盘低水位)

RabbitMQ 可以对内存磁盘使用量设置阈值,当达到阈值后,生产者将被完全阻塞(处于block状态) ,直到对应项恢复正常。

内存和磁盘的流控相当于全局流控,流控时发送消息被完全阻塞,通常会阻塞较长时间(几分钟以上)才恢复。

全局流控时,从Web UI可以观察到 Connection 处于blocked状态。

在 rabbitmq-java-client 中,可以用给 Connection 添加 blockedListener 的方式监听阻塞和阻塞解除的事件,用以在客户端应对可能的阻塞情况。

connection.addBlockedListener(
    reason -> {
        try {
            unblock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
},
    () -> latch.countDown()
);
1.2.2 进程内流控

进程内流控是针对 Erlang 进程的流控,与全局流控不是一个概念。又可称作 Per-Connection Flow Control。

在 RabbitMQ Broker 中使用多种进程来处理消息,进程的处理顺序如下。

*A simplified depiction of message flows*
*A simplified depiction of message flows*

A simplified depiction of message flows

reader -> channel -> queue process -> message store

进程内流控指的是这4种进程之间的流控。

进程内流控不会影响到消费端。

某进程处于流控状态时,从 Web UI 可以观察到该进程的状态为黄色flow,此时该进程会暂时阻塞消息的生产。

A queue in flow state
A queue in flow state

A queue in flow state

进程内流控的阻塞时间通常很短,在1秒之内。但是也有长至几分钟的。

进程内流控是阻塞在 Broker 端的 socket 接收方法中,client 端无法监听和做出处理。

从 RabbitMQ 3.5.5 版本开始,引入了一套基于信用证的流控实现。

本文主要讨论基于信用证的进程内流控实现。

1.2.3 发送方确认

这其实并不属于流控机制,但是通过生产者确认的方式可以让发送消息不丢失,并且控制发送消息的速度。

未开启发送方确认时,消息可能未达到服务器就发送完毕。

发送方确认开启后,消息在投递到匹配的队列后会给发送方返回一个确认请求,至此发送消息的动作才执行完毕。

1.2.4 消费者预取

通过Channel#basicQos(int prefetchCount)方法设置消费者允许存在的的最大未Ack消息数量,可以达到预取一批消息到消费者进行消费的目的。

2. 概要流程

从 RabbitMQ 3.5.5 版本开始,引入了一套基于信用证的流控实现。

2.1 信用证配置

信用证流控的两个参数可以通过查询环境变量的方式找到

rabbitmqctl eval 'application:get_all_env(rabbit).'
# ...
{credit_flow_default_credit,{400,200}}  # {InitialCredit, MoreCreditAfter}
# ...

其中400表示每个进程初始的信用值,200表示下游进程处理200个消息后会一次性给上游进程加200信用值。

这两个参数在老一点的版本中为{200, 50}

2.2 基于信用证的流控

Erlang 进程与操作系统的进程不同,是一种轻量级进程。

简单来说,RabbitMQ中有四种进程。

reader -> channel -> queue process -> message store
400       400        400

在初始化时,会为前三种进程分配信用值,分配的值为InitialCredit,默认400。

当进程处理一条消息并且发给下游进程时,它自己的信用值会减一。

下游进程处理完一条消息时,会给上有进程发一个Ack消息。但是此时并不会直接让上游进程的信用值加一,而是等到处理完MoreCreditAfter条消息(默认200)时,才将上游进程的信用值加200。

当进程的信用值将为1时,就会阻塞上游进程向它发送消息。

3. 详细流程

3.1 流控详细流程

下图每个橙色组件都是一个 Erlang 进程。

每个RabbitMQ broker在内部都是通过actor模式实现的,不同组件之间通过消息传递(有时是本地的)进行通信。

***A simplified depiction of message flows***
***A simplified depiction of message flows***

A simplified depiction of message flows

下面我们把这个模型简化,然后分析基于信用证的流控机制。

  • rabbit_reader:Connection 的处理进程。负责接收、解析 AMQP 协议数据包,将消息发送到 Channel
  • rabbit_channel:Channel 的处理进程,负责处理 AMQP 协议的各种方法、进行路由解析;进行安全和协调的处理等
  • rabbit_amqqueue_process:Queue 的处理进程,负责将消息存入内存、将队列索引持久化
  • rabbit_msg_store:Store 的处理进程,负责消息的持久化
*Credit based flow control with classic queues、*
*Credit based flow control with classic queues、*

Credit based flow control with classic queues、

  1. 信用证初始化时,下游进程分别为前三个进程 reader、channel、queue 分配初始信用值 InitialCredit(400)(图中1)
  2. 当 reader 进程开始处理一条消息,它会先将自己的信用值-1,然后将消息处理完后发给 channel 进程(图中2)
  3. channel 进程接收 reader 发过来的消息时,会在信用证系统种进行 ack 操作。channel 进程会持续追踪它从 reader 进程 ack 了多少条消息。当累计接收并 ack 的消息数达到 MoreCreditAfter(200) 后,会给 reader 分配新的 MoreCreditAfter(200)信用值。(图中3)
  4. 当进程字典中的信用值降为0时,该进程会被阻塞。它不会接收消息也不会发送消息,直到获得新的信用值。
  5. 最终,TCP 读取进程被阻塞,从 socket 读取的操作被停止。

3.2 如何识别性能瓶颈

在管理 UI 中,你可能看到 Connection、Channel、Queue 处于flow状态,说明它们最近处于流控状态。这意味着它们暂时耗尽了信用值,等待下游进程授予更多信用。进程内流控可能在1秒钟内触发多次。

如何通过flow状态识别进程的性能瓶颈?

简单来说,一个进程的flow状态会导致它的上游进程进入flow状态。而该进程进入flow状态的原因是因为它的下游进程成为了性能瓶颈。

例如,在下图中,Queue 进程成为性能瓶颈:

*Credit exhaustion.*
*Credit exhaustion.*

Credit exhaustion.

上图中,Queue 处理缓慢,这就意味着 Queue 可能在较长时间内都没有授予 Channel 新的信用值。Channel 处理比 Queue 快,这样 Channel 的信用值就会先一步耗尽。

Channel 信用值耗尽后,Channel 被阻塞,不会接受消息也不会处理消息,这样 Reader 的信用值也将会耗尽。

也就是说,Queue 如果是性能瓶颈,最终会导致它的上游,即 Channel 和 Reader 处于flow状态。


下面可以总结出判断性能瓶颈在何处的结论:

  • 当某个 Connection 处于 flow状态,但这个 Connection 中没有一个 Channel 处于 flow状态时,这就意味这个 Connection 中有一个或者多个 Channel 出现了性能瓶颈。某些 Channel 进程的运作(比如处理路由逻辑)会使得服务器 CPU 的负载过高从而导致了此种情形。尤其是在发送 大量较小的非持久化消息时,此种情形最易显现。
  • 当某个 Connection 处于 flow状态 ,并且这个 Connection 中也有若干个 Channel 处于 flow状态,但没有任何一个对应的队列处于 flow状态时,这就意味着有一个或者多个队列出现了性能瓶颈。这可能是由于将消息存入队列的过程中引起服务器 CPU 负载过高,或者是将队列中的消息存入磁盘的过程中引起服务器 I/O 负载过高而引起的此种情形。尤其是在发送 大量较小的持久化消息时,此种情形最易显现。
  • 当某个 Connection 处于 flow状态,同时这个 Connection 中也有若干个 Channel 处于 flow状态,井且也有若干个对应的队列处于 flow状态时,这就意味着在消息持久化时出现了性能瓶颈。在将队列中的消息存入磁盘的过程中引起服务器 I/O 负载过高而引起的此种情形。尤其是在 发送大量较大的持久化消息时,此种情形最易显现。

4. 源码解析

在 Erlang 中,每个进程都保存为一个.erl文件。这里的进程与操作系统的进程不同,是一个由 Erlang 系统管理的轻量级进程。而信用证流控的逻辑都位于credit_flow.erl文件中。

下面我们以rabbit_reader(Connection 进程)和rabbit_channel进程为例,看一下源码中如何处理信用的流动和消息的阻塞。

4.1 处理消息,减少信用

rabbit_reader处理一个有内容的命令(比如basic.publish),会执行如下加粗逻辑

% rabbit_reader.erl
process_frame(Frame, Channel, State) ->
    ChKey = {channel, Channel},
    case (case get(ChKey) of
              undefined -> create_channel(Channel, State);
              Other     -> {ok, Other, State}
          endof
        {error, Error} ->
            handle_exception(State, Channel, Error);
        {ok, {ChPid, AState}, State1} ->
            case rabbit_command_assembler:process(Frame, AState) of
                {ok, NewAState} ->
                    put(ChKey, {ChPid, NewAState}),
                    post_process_frame(Frame, ChPid, State1);
                {ok, Method, NewAState} ->
                    rabbit_channel:do(ChPid, Method),
                    put(ChKey, {ChPid, NewAState}),
                    post_process_frame(Frame, ChPid, State1);
                **{ok, Method, Content, NewAState} ->
                    rabbit_channel:do_flow(ChPid, Method, Content),
                    put(ChKey, {ChPid, NewAState}),
                    post_process_frame(Frame, ChPid, control_throttle(State1));**
                {error, Reason} ->
                    handle_exception(State1, Channel, Reason)
            end
    end.

可以看到会先执行rabbit_channel:doflow/3,再看一下这个方法

% rabbit_channel_common.erl
do_flow(Pid, Method, Content) ->
    %% Here we are tracking messages sent by the rabbit_reader
    %% process. We are accessing the rabbit_reader process dictionary.
    credit_flow:send(Pid),
    gen_server2:cast(Pid, {method, Method, Content, flow}).

可以看到在rabbit_channel中会调用credit_flow:send/1方法。这里的Pid是 Channel 的进程号。

这里的逻辑是:rabbit_reader通过credit_flow模块来追踪它已经向rabbit_channel进程发送的消息数,每发一条消息就会将自己的信用值减一。被追踪的信息保存在rabbit_reader的进程字典中。

注意,尽管这里是在rabbit_channel模块中调用credit_flow:send/1方法,但是此处仍处于rabbit_reader进程中,只有在执行完gen_server2:cast/2方法后才会进入到rabbit_channel进程的内存空间。因此,当credit_flow:send/1方法被调用时,信用值减一的操作仍然在rabbit_reader进程中被追踪。

见下面credit_flow:send/2credit_flow:UPDATE的定义,通过调用get/1put/2方法获取并更新进程字典的值。

% credit_flow.erl
send(From, {InitialCredit, _MoreCreditAfter}) ->
    ?UPDATE({credit_from, From}, InitialCredit, C,
            if C == 1 -> block(From),
                         0;
               true   -> C - 1
            end).
% credit_flow.erl
%% process dict update macro - eliminates the performance-hurting
%% closure creation a HOF would introduce
-define(UPDATE(Key, Default, Var, Expr),
        begin
            %% We deliberately allow Var to escape from the case here
            %% to be used in Expr. Any temporary var we introduced
            %% would also escape, and might conflict.
            Var = case get(Key) of
                undefined -> Default;
                V         -> V
            end,
            put(Key, Expr)
        end).

来看一下进程字典中关于信用证的信息

Untitled
Untitled

用来保存信用值信息的 key 是{credit_from, From}From表示消息接受者的进程号(这里是rabbit_channel)。当这个 key 对应的值达到 0,拥有该进程字典的进程会被阻塞(调用credit_flow:block/1)方法

4.2 进程阻塞,停止接收信息

上面说到,当进程字典中的信用值达到 0 时,会调用credit_flow:block/1方法,我们来看看这个方法中做了什么。

% credit_flow.erl
block(From) ->
    ?TRACE_BLOCKED(self(), From),
    case blocked() of
        false -> put(credit_blocked_at, erlang:monotonic_time());
        true  -> ok
    end,
    ?UPDATE(credit_blocked, [], Blocks, [From | Blocks]).

这里更新了进程字典中credit_blocked的值,将阻塞这个进程的下游进程ID(这里是rabbit_channel)加入到credit_blocked中。

注意,因为rabbit_reader可能会将消息发送给多个进程,所以它也可能被多个进程阻塞。因此credit_blocked的值是一个进程ID列表。

credit_blocked -> [pid()]

那么进程阻塞之后,如何停止信息接收?我们来分析一下rabbit_reader接收消息的入口,recvloop方法。

% rabbit_reader.erl
recvloop(Deb, Buf, BufLen, State = #v1{pending_recv = true}) ->
    mainloop(Deb, Buf, BufLen, State);
recvloop(Deb, Buf, BufLen, State = #v1{connection_state = blocked}) ->
    mainloop(Deb, Buf, BufLen, State);
recvloop(Deb, Buf, BufLen, State = #v1{connection_state = {become, F}}) ->
    throw({become, F(Deb, Buf, BufLen, State)});
recvloop(Deb, Buf, BufLen, State = #v1{sock = Sock, recv_len = RecvLen})
  when BufLen < RecvLen ->

    case rabbit_net:setopts(Sock, [{active, once}]) of
        ok              -> mainloop(Deb, Buf, BufLen,
                                    State#v1{pending_recv = true});
        {error, Reason} -> stop(Reason, State)
    end;

其中mainloop会调用recvloop函数,达成无限循环的效果。

rabbit_reader每接收一个包,就设置套接字属性为{active, once},若当前连接处于blocked状态,则不设置{active, once},这个接收进程就阻塞在receive方法上。

4.3 增加信用值

rabbit_channel每处理一条消息,都会向rabbit_reader进行一次确认(credit_flow:ack)。

rabbit_channel累计处理的消息数达到MoreCreditAfter值时,会授予rabbit_reader新的MoreCreditAfter点信用值。


我们先来看一下ack函数的实现

% credit_flow.erl
ack(To, {_InitialCredit, MoreCreditAfter}) ->
    ?UPDATE({credit_to, To}, MoreCreditAfter, C,
            if C == 1 -> grant(To, MoreCreditAfter),
                         MoreCreditAfter;
               true   -> C - 1
            end).

rabbit_channel进程会记录它向特定的发送者(rabbit_reader)ack了多少条消息。在进程字典中用来保存ack消息数的 key 是{credit_to, To},这里To是发送者(rabbit_reader)的进程号。

MoreCreditAfter条消息被ack,会调用grant方法授予rabbit_reader更多的信用值。


% credit_flow.erl
grant(To, Quantity) ->
    Msg = {bump_credit, {self(), Quantity}},
    case blocked() of
        false -> To ! Msg;
        true  -> ?UPDATE(credit_deferred, [], Deferred, [{To, Msg} | Deferred])
    end.

在这里,rabbit_channel将会发送一条{bump_credit, {self(), Quantity}}的消息给rabbit_reader来授予信用。其中self()指向rabbit_channel


当rabbit_reader进程收到bump_credit消息后,它需要将消息传入并调用credit_flow:handle_bump_msg/1方法来处理新增信用值。

% credit_flow.erl
handle_bump_msg({From, MoreCredit}) ->
    ?UPDATE({credit_from, From}, 0, C,
            if C =< 0 andalso C + MoreCredit > 0 -> unblock(From),
                                                    C + MoreCredit;
               true                              -> C + MoreCredit
            end).

我们访问rabbit_reader的进程字典,更新{credit_from, From}这个 key。如果信用值大于0,那么进程会解除阻塞。

4.4 进程解除阻塞

% credit_flow.erl
unblock(From) ->
    ?TRACE_UNBLOCKED(self(), From),
    ?UPDATE(credit_blocked, [], Blocks, Blocks -- [From]),
    case blocked() of
        false -> case erase(credit_deferred) of
                     undefined -> ok;
                     Credits   -> _ = [To ! Msg || {To, Msg} <- Credits],
                                  ok
                 end;
        true  -> ok
    end.

调用credit_flow:unblock/1会更新credit_blocked列表,将其清空。随后进程可以继续发送消息。

同时,credit_flow:unblock/1将负责发送在credit_deferred列表中保存的所有消息。


当unblock/1被调用时,rabbit_channel进程的ID将从credit_blocked的列表中删除。

%% We are operating on process A dictionary.
get(credit_blocked) => [B, C].
unblock(B).
get(credit_blocked) => [C].

在这种情况下,A 仍然被阻塞,直到 C 授予它更多信用。当 A 的阻塞解除,它将处理它的 credit_deferred列表,发送bump_credit消息给列表中的进程。

5. 参考资料

  • Flow Control
  • Finding bottlenecks with RabbitMQ 3.3
  • New Credit Flow Settings on RabbitMQ 3.5.5
  • RABBITMQ INTERNALS - CREDIT FLOW FOR ERLANG PROCESSES
  • Quorum Queues and Flow Control - The Concepts
  • RabbitMQ实战指南
  • RabbitMQ流量控制机制分析

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1891142.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么是眼球凹渲染?如何在Varjo Base中设置眼球追踪与凹渲染功能

当谈到Varjo耳机时&#xff0c;它们总是与超高分辨率显示器和有凹渲染联系在一起&#xff0c;从而能够高效地渲染到高像素的显示器上。 本篇博文的主题是分享一些可用于OpenXR应用程序的新设置&#xff0c;但在此之前&#xff0c;让我们先了解关于有凹渲染的一些背景。有凹渲染…

【计算机毕业设计】基于Springboot的B2B平台医疗病历交互系统【源码+lw+部署文档】

包含论文源码的压缩包较大&#xff0c;请私信或者加我的绿色小软件获取 免责声明&#xff1a;资料部分来源于合法的互联网渠道收集和整理&#xff0c;部分自己学习积累成果&#xff0c;供大家学习参考与交流。收取的费用仅用于收集和整理资料耗费时间的酬劳。 本人尊重原创作者…

Python爬取国家医保平台公开数据

国家医保服务平台数据爬取python爬虫数据爬取医疗公开数据 定点医疗机构查询定点零售药店查询医保机构查询药品分类与代码查询 等等&#xff0c;数据都能爬 接口地址&#xff1a;/ebus/fuwu/api/nthl/api/CommQuery/queryFixedHospital 签名参数&#xff1a;signData {dat…

H5打包失败:JS堆内存不足

傻子没看懂报错&#xff0c;像个无头苍蝇 解决方式&#xff1a;清空缓存&#xff0c;重启电脑&#xff0c;打包成功。

二叉树中序遍历-递归法详解-数据结构与算法

首先看下中序遍历的代码&#xff1a;&#xff08;左 跟 右&#xff09; 其首先要接受一个根结点root作为参数 判断根节点是否为NULL 不为NULL则递归遍历左子树 ①我们把树根结点A传递给它 其左结点为B&#xff0c;右结点为C ②首先我们要检查root是否为NULL 其不为NULL …

使用pyinstaller 如何打包python项目

参考&#xff1a;【python项目正确打包方法-哔哩哔哩】 https://b23.tv/EDB6zbG Pyinstaller 详解多种打包过程(去坑,填坑)。_pyinstaller -f -w-CSDN博客 1.打开命令提示符&#xff1a; 找到python项目所在位置&#xff0c;输入cmd即可 2. 安装pipenv: 在命令提示符&#…

1000T的文件怎么能快速从南京传到北京?最佳方案你肯定想不到

今天刷面试题看到一个有意思的面试题&#xff0c; 1000T的文件怎么能以最快速度从南京传到北京&#xff1f; 网络传输 首先我们考虑通过网络传输&#xff0c;需要多长时间。 我特地咨询了在运营商工作的同学&#xff0c;目前带宽&#xff1a; 家庭宽带下行最大1Gbps&#…

x264 编码器汇编模块介绍

aarch64汇编架构 解释:AArch64 是 ARM 架构的 64 位版本,也称为 ARMv8-A特点: 64位寻址能力,支持更大的地址空间,理论上可达16EB(Exabyte)使用64位宽的寄存器,有31个通用寄存器(X0-X30),外加一个链接寄存器(X31)支持扩展的 NEON SIMD 指令集,提供更多的执行单元和…

电脑显示由于找不到MSVCP140.dll,无法继续执行代码

电脑已经成为我们生活和工作中不可或缺的工具&#xff0c;然而&#xff0c;在使用电脑的过程中&#xff0c;我们常常会遇到一些错误提示&#xff0c;其中之一就是“电脑显示由于找不到MSVCP140.dll是怎么回事&#xff1f;”这个问题。小编将详细介绍该问题的原因、解决方法以及…

新版本发布丨昂辉科技EasySAR-Configurator V1.2.0再启航

昂辉科技新一代跨平台高性能AUTOSAR配置工具EasySAR-Configurator V1.2.0全新版本重磅发布&#xff01;产品基于Web架构前后端分离的方式开发&#xff0c;可提供SaaS部署&#xff0c;能够实现精准配置和最大限度的代码裁剪&#xff0c;且配备标准的约束限制、配置验证、代码生成…

技术成神之路:设计模式(二)建造者模式

1.定义 建造者模式&#xff08;Builder Pattern&#xff09;是一种创建型设计模式&#xff0c;它允许你分步骤创建复杂对象&#xff0c;而不必直接调用构造函数。建造者模式特别适合那些包含多个组成部分并且构造过程复杂的对象。 2. 结构 建造者模式的主要组成部分包括&#…

基于字符和词特征融合的恶意域名检测

传统的恶意域名检测方法在检测由域名生成算法&#xff08;DGA&#xff09;随机生成的恶意域名方面性能不佳&#xff0c;尤其是对于那些由随机单词组成的域名。文章提出了一种新的检测算法&#xff0c;通过融合字符和词特征来提高对恶意域名的检测能力&#xff0c;特别是对于更具…

【RT摩拳擦掌】如何构建RT AVB switchendpoint平台

【RT摩拳擦掌】如何构建RT AVB switch&endpoint平台 一&#xff0c;文档简介二&#xff0c;平台构建2.1 软硬件情况2.2 配置RT1170 AVB端点2.2.1 1块MIMXRT1170开发板做talker配置2.2.2 2块MIMXRT1170开发板做listener配置 2.3 AVB Switch 配置2.3.1 MOTU AVB Switch2.3.2 …

【鸿蒙学习笔记】@Prop装饰器:父子单向同步

官方文档&#xff1a;Prop装饰器&#xff1a;父子单向同步 [Q&A] Prop装饰器作用 Prop装饰的变量可以和父组件建立单向的同步关系。Prop装饰的变量是可变的&#xff0c;但是变化不会同步回其父组件。 [Q&A] Prop装饰器特点 &#xff11;・Prop装饰器不能在Entry装饰的…

关于ant design vue 使用Modal无法关闭弹窗的解决思路

文章目录 1: 出现问题的版本2.出现问题&#xff08;1&#xff09;ant design 的问题&#xff08;2&#xff09;poina的提示报错 3.正确版本总结 1: 出现问题的版本 "ant-design-vue": "^3.2.20", "pinia": "^2.1.7", "vue"…

Mybatis Plus 自动填充注解 @TableField(fill = FieldFill.INSERT_UPDATE)

第一步&#xff1a;在需要自动填充的位置加上注解 通过在创建时间和修改时间上添加 fill 填充字段 进行自动填充 第二步&#xff1a;要想实现自动填充还需要实现MetaObjectHandler接口&#xff0c;在这里实现自动填充的逻辑 Component public class MyMetaObjectHandler …

python sklearn机械学习-数据预处理

&#x1f308;所属专栏&#xff1a;【机械学习】✨作者主页&#xff1a; Mr.Zwq✔️个人简介&#xff1a;一个正在努力学技术的Python领域创作者&#xff0c;擅长爬虫&#xff0c;逆向&#xff0c;全栈方向&#xff0c;专注基础和实战分享&#xff0c;欢迎咨询&#xff01; 您…

filex文件系统功能预研

filex资源 filex的源码路径有两个&#xff1a; 一个是azure-rtos下的filex&#xff1a;azure-rtos/filex (github.com)一个是eclipse-threadx下的filex&#xff1a;eclipse-threadx/filex filex的文档地址&#xff1a;rtos-docs/rtos-docs/filex 第三方文档&#xff1a;Thre…

搭建知识付费系统的技术框架与实现路径

知识付费系统已经成为内容创作者和企业变现的重要工具。要成功搭建一个高效、稳定、用户体验良好的知识付费系统&#xff0c;明确技术框架和实现路径至关重要。本文将详细解析搭建知识付费系统的技术框架&#xff0c;并提供具体的实现路径和相关技术代码示例。 一、知识付费系…

大陆ARS548使用记录

一、Windows连接上位机 雷达是在深圳路达买的&#xff0c;商家给的资料中首先让配置网口&#xff0c;但我在使用过程中一直出现无法连接上位机的情况。接下来说说我的见解和理解。 1.1遇到的问题 按要求配置好端口后上位机无连接不到雷达&#xff0c;但wireshark可以正常抓到数…