当前位置: 首页 > news >正文

envoy 源码分析

整体架构

Envoy 的架构如图所示:

Envoy 中也可能有多个 Listener,每个 Listener 中可能会有多个 filter 组成了 chain。

Envoy 接收到请求后,会先走 FilterChain,通过各种 L3/L4/L7 Filter 对请求进行微处理,然后再路由到指定的集群,并通过负载均衡获取一个目标地址,最后再转发出去。

在Envoy中,具有最为核心的四种资源:Listener,Router,Cluster,以及Filter。

Listener:Envoy工作的基础

简单理解,Listener是Envoy打开的一个监听端口,用于接收来自Downstream(客户端)连接。Envoy可以支持复数个Listener。多个Listener之间几乎所有的配置都是隔离的。Listener配置中核心包括监听地址、Filter链等。

Filter:强大源于可扩展

Filter,通俗的讲,就是插件。通过Filter机制,Envoy提供了极为强大的可扩展能力。

利用Filter机制,Envoy理论上可以实现任意协议的支持以及协议之间的转换,可以对请求流量进行全方位的修改和定制。强大的Filter机制带来的不仅仅是强大的可扩展性,同时还有优秀的可维护性。Filter机制让Envoy的使用者可以在不侵入社区源码的基础上对Envoy做各个方面的增强。

  • 网络过滤器(Network Filters): 工作在 L3/L4,是 Envoy 网络连接处理的核心,处理的是原始字节,分为 Read、Write 和 Read/Write 三类。

  • HTTP 过滤器(HTTP Filters): 工作在 L7,由特殊的网络过滤器 HTTP connection manager 管理,专门处理 HTTP1/HTTP2/gRPC 请求。它将原始字节转换成 HTTP 格式,从而可以对 HTTP 协议进行精确控制。

  • 网络过滤器(Network Filters): 工作在 L3/L4,是 Envoy 网络连接处理的核心,处理的是原始字节,分为 Read、Write 和 Read/Write 三类。

  • HTTP 过滤器(HTTP Filters): 工作在 L7,由特殊的网络过滤器 HTTP connection manager 管理,专门处理 HTTP1/HTTP2/gRPC 请求。它将原始字节转换成 HTTP 格式,从而可以对 HTTP 协议进行精确控制。

Cluster:对上游服务的抽象

在Envoy中,每个Upstream上游服务都被抽象成一个Cluster。Cluster包含该服务的连接池、超时时间、endpoints地址、端口、类型(类型决定了Envoy获取该Cluster具体可以访问的endpoint方法)等等。

Cluster对应的配置/资源发现服务称之为CDS。一般情况下,CDS服务会将其发现的所有可访问服务全量推送给Envoy。与CDS紧密相关的另一种服务称之为EDS。CDS服务负责Cluster资源的推送。而当该Cluster类型为EDS时,说明该Cluster的所有endpoints需要由xDS服务下发,而不使用DNS等去解析。下发endpoints的服务就称之为EDS。

Listener对应的配置/资源发现服务称之为LDS。LDS是Envoy正常工作的基础,没有LDS,Envoy就不能实现端口监听(如果启动配置也没有提供静态Listener的话),其他所有xDS服务也失去了作用。

Router:上下游之间的桥梁

Listener可以接收来自下游的连接,Cluster可以将流量发送给具体的上游服务,而Router则决定Listener在接收到下游连接和数据之后,应该将数据交给哪一个Cluster处理。它定义了数据分发的规则。虽然说到Router大部分时候都可以默认理解为HTTP路由,但是Envoy支持多种协议,如Dubbo、Redis等,所以此处Router泛指所有用于桥接Listener和后端服务(不限定HTTP)的规则与资源集合。

Route对应的配置/资源发现服务称之为RDS。Router中最核心配置包含匹配规则和目标Cluster,此外,也可能包含重试、分流、限流等等。

服务管理

静态配置

下面的配置将所有流量代理到 baidu.com,配置完成后我们应该能够通过请求 Envoy 的端点就可以直接看到百度的主页了,而无需更改 URL 地址。

static_resources:# 1. 监听器listeners:- name: listener_0address:socket_address: { address: 0.0.0.0, port_value: 10000 }# 2. 过滤器filter_chains:- filters:- name: envoy.http_connection_managerconfig:stat_prefix: ingress_httproute_config:name: local_routevirtual_hosts:- name: local_servicedomains: ["*"]routes:- match: { prefix: "/" }route: { host_rewrite: www.baidu.com, cluster: service_baidu }http_filters:- name: envoy.router# 3. 集群clusters:- name: service_baiduconnect_timeout: 0.25stype: LOGICAL_DNSdns_lookup_family: V4_ONLYlb_policy: ROUND_ROBINhosts: [{ socket_address: { address: www.baidu.com, port_value: 443 }}]tls_context: { sni: baidu.com }# 4. 管理    
admin:access_log_path: /tmp/admin_access.logaddress:socket_address: { address: 0.0.0.0, port_value: 9901 }

动态配置xDS协议(动态服务发现)

Envoy 通过查询文件或管理服务器来动态发现资源。这些发现服务及其相应的 API 被统称为 xDS。

Envoy 通过订阅(subscription)方式来获取资源,如监控指定路径下的文件、启动 gRPC 流(streaming)或轮询 REST-JSON URL。后两种方式会发送 DiscoveryRequest 请求消息,发现的对应资源则包含在响应消息 DiscoveryResponse 中。

xDS 协议是 “X Discovery Service” 的简写,这里的 “X” 表示它不是指具体的某个协议,是一组基于不同数据源的服务发现协议的总称:

CDS:Cluster Discovery Service
EDS:Endpoint Discovery Service
SDS:Secret Discovery Service
RDS:Route Discovery Service
LDS:Listener Discovery Service

xDS 协议是由 Envoy 提出的,目前已成为服务网格的协议标准之一。

Envoy是 Istio 中默认的 sidecar 代理,但只要实现了 xDS 协议,理论上也可以作为 Istio 中的 sidecar 代理 —— 比如蚂蚁集团开源的 MOSN。

比如每个 Envoy 流以发送一个 DiscoveryRequest 开始,包括了列表订阅的资源、订阅资源对应的类型 URL、节点标识符和空的 version_info。EDS 请求示例如下:

version_info:
node: { id: envoy }
resource_names:
- foo
- bar
type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
response_nonce

管理服务器可立刻或等待资源就绪时发送 DiscoveryResponse 作为响应,示例如下

version_info: X
resources:
- foo ClusterLoadAssignment proto encoding
- bar ClusterLoadAssignment proto encoding
type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
nonce: A

源码分析

源码版本

release/v1.31

启动

Bootstrap文件详解

Envoy 内部对请求的处理流程其实跟我们上面脑补的流程大致相同,即对请求的处理流程基本是不变的,而对于变化的部分,即对请求数据的微处理,全部抽象为 Filter,例如对请求的读写是 ReadFilter、WriteFilter,对 HTTP 请求数据的编解码是 StreamEncoderFilter、StreamDecoderFilter,对 TCP 的处理是 TcpProxyFilter,其继承自 ReadFilter,对 HTTP 的处理是 ConnectionManager,其也是继承自 ReadFilter 等等,各个 Filter 最终会组织成一个 FilterChain,在收到请求后首先走 FilterChain,其次路由到指定集群并做负载均衡获取一个目标地址,然后转发出去。

Bootstrap启动过程

  1. 载入Bootstrap启动yaml文件

  2. 设置header prefix

  3. 初始化stats、设置TagProducer、StatsMatcher、HistogramSettings等

  4. 创建Server stats

  5. 注册assert action、bug action

  6. 设置healthy check为false

  7. cluster manager包含了多阶段初始化,第一阶段要初始化的是static/DNS clsuter, 然后是预先定义的静态的EDS cluster, 如果包含了CDS需要等待CDS收到一个response,或者是失败的响应,最后初始化CDS,接着开始初始化CDS提供的Cluster。

  8. 如果集群提供了健康检查,Envoy还会进行一轮健康检查

  9. 等到cluster manager初始化完毕后,RDS和LDS开始初始化,直到收到响应或者失败,在此之前Envoy是不会接受连接来处理流量的

  10. 如果LDS的响应中嵌入了RDS的配置,那么还需要等待RDS收到响应,这个过程被称为listener warming

  11. 上述所有流程完毕后,listener开始接受流量。

数据流处理

总的来说,Envoy中的filter处理过程可以简单概括为:请求到达Listener -> 经过一系列filter处理 -> 转发给上游服务 -> 接收上游服务的响应 -> 经过相同filter链处理 -> 发送响应给客户端。

其中,将请求转发给上游服务的过程是由Proxy模块完成的。Proxy模块负责管理与上游服务之间的通信,包括建立连接、发送请求、接收响应等操作。一旦请求在经过所有的filter处理后,Proxy模块将负责将请求转发给相应的上游服务,并将上游服务返回的响应转发回客户端。Proxy模块在Envoy中扮演着重要的角色,确保请求能够顺利地到达目标服务并返回响应。

Listener 分析

listener流程

  1. 监听器配置:在Envoy的配置文件中,可以配置一个或多个listener来指定Envoy应该监听的地址和端口,以及应用于该listener的filter链。

  2. 传入连接接受:当Envoy启动时,每个listener会开始在其指定的地址和端口上监听传入的连接。一旦有新的连接建立,listener将会接受这个连接。

  3. Filter链处理:一旦listener接受了传入连接,连接将会按照listener配置的顺序通过一系列的filter。每个filter可以对连接进行操作,比如解密、身份验证、流量控制等。

在监听处理时,为了截获关注的流量到listener端口,经常通过iptables等方式配置,将关注的流量转发至对应的listener统一处理。

Filter分析

Filter 作用在listen socket上,当有连接到来的时候,通过libevent会触发可读事件,调用listen socket的accept获取到连接socket封装为ConnectionSocket, 最后调用ActiveListener::onAccept,将获取到的连接socket作为其参数。

  1. 创建filter chain

  2. continueFilterChain 调用filter chain

  3. 如果有filter返回了StopIteration,那么就开启timer,在这个时间内还没有继续continue就直接关闭当前socket

  4. filter返回StopIteration后,要继续运行剩下的filter可以回调continueFilterChain

比如proxy_protocol这个listener filter当接收到一个filter后会注册读事件,从socket读取proxy协议头,所以会返回StopIteration 等到有数据可读的时候,并且读到了协议头才会回调continueFilterChain继续执行下面的filter

void ConnectionHandlerImpl::ActiveListener::onAccept(Network::ConnectionSocketPtr&& socket, bool hand_off_restored_destination_connections) {auto active_socket = std::make_unique<ActiveSocket>(*this, std::move(socket),hand_off_restored_destination_connections);// Create and run the filtersconfig_.filterChainFactory().createListenerFilterChain(*active_socket);active_socket->continueFilterChain(true);// Move active_socket to the sockets_ list if filter iteration needs to continue later.// Otherwise we let active_socket be destructed when it goes out of scope.if (active_socket->iter_ != active_socket->accept_filters_.end()) {// 启动了一个timer,避免filter长时间不调用active_socket->startTimer();active_socket->moveIntoListBack(std::move(active_socket), sockets_);}
}// 如果超时就从socket list中移除当前socket
void ConnectionHandlerImpl::ActiveSocket::onTimeout() {listener_.stats_.downstream_pre_cx_timeout_.inc();ASSERT(inserted());unlink();
}void ConnectionHandlerImpl::ActiveSocket::startTimer() {if (listener_.listener_filters_timeout_.count() > 0) {timer_ = listener_.parent_.dispatcher_.createTimer([this]() -> void { onTimeout(); });timer_->enableTimer(listener_.listener_filters_timeout_);}
}

四层filter执行链

  1. Downstream连接建立后,开始创建filter,然后初始化filter

  2. 回调onNewConnection

  3. 回调onData

bool FilterManagerImpl::initializeReadFilters() {if (upstream_filters_.empty()) {return false;}// 初始化完成后,开始从头开始执行filteronContinueReading(nullptr);return true;
}// 传入的是nullptr的时候,从头开始执行filter的
// 设置initialized_标志为true
// 回调onNewConnection,如果是返回stop就停止运行了
// 等待filter返回通过ReadFilterCallbacks回调onContinueReading来继续执行
void FilterManagerImpl::onContinueReading(ActiveReadFilter* filter) {std::list<ActiveReadFilterPtr>::iterator entry;if (!filter) {entry = upstream_filters_.begin();} else {entry = std::next(filter->entry());}for (; entry != upstream_filters_.end(); entry++) {if (!(*entry)->initialized_) {(*entry)->initialized_ = true;FilterStatus status = (*entry)->filter_->onNewConnection();if (status == FilterStatus::StopIteration) {return;}}BufferSource::StreamBuffer read_buffer = buffer_source_.getReadBuffer();if (read_buffer.buffer.length() > 0 || read_buffer.end_stream) {FilterStatus status = (*entry)->filter_->onData(read_buffer.buffer, read_buffer.end_stream);if (status == FilterStatus::StopIteration) {return;}}}
}

Example: 有三个filter、其中任何一个filter其中的一个callback返回StopIteration那么整个流程就停止了,需要等待调用onContinueReading才能继续 执行下一个callback方法。

FilterA::onNewConnection FilterA::onData

FilterB::onNewConnection FilterB::onData

FilterC::onNewConnection FilterC::onData

执行顺序为: FilterA::onNewConnection->FilterA::onData->FilterB::onNewConnection->FilterB::onData->FilterC::onNewConnection->FilterC::onData 任何一个callback返回StopIteration整个流程就不会继续往下走了,需要等待对应的filter回调onContinueReading,这样就会带来一个问题,一旦停止filter chain 继续往下走,那么网络层依然会收数据存在内部buffer里面,这会导致内存上涨,因此TCP PROXY中会在调用onNewConnection的时候先关闭读,然后和upstream建立连接 连接建立后才会开启读,防止内存被打爆。

目前实现的listener filter主要有original_dstoriginal_srcproxy_protocoltls_inspector等,同时支持用户扩展自定义的filter。

original_dst filter

一般应用于通过iptables或者tproxy的方式将流量发送给envoy,导致原来要访问的地址信息丢失,但是可以通过从socket中获取到这些信息,交给envoy做listen转发。

  1. 主要就是从socket中获取到原来的目的地址信息 (getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, &orig_addr, &addr_len))

  2. 然后设置socket的restore_local_address为原来的目的地址

Network::FilterStatus OriginalDstFilter::onAccept(Network::ListenerFilterCallbacks& cb) {ENVOY_LOG(debug, "original_dst: New connection accepted");Network::ConnectionSocket& socket = cb.socket();const Network::Address::Instance& local_address = *socket.localAddress();if (local_address.type() == Network::Address::Type::Ip) {Network::Address::InstanceConstSharedPtr original_local_address =getOriginalDst(socket.ioHandle().fd());// A listener that has the use_original_dst flag set to true can still receive// connections that are NOT redirected using iptables. If a connection was not redirected,// the address returned by getOriginalDst() matches the local address of the new socket.// In this case the listener handles the connection directly and does not hand it off.if (original_local_address) {// Restore the local address to the original one.socket.restoreLocalAddress(original_local_address);}}return Network::FilterStatus::Continue;

四层filter执行链

  1. Downstream连接建立后,开始创建filter,然后初始化filter

  2. 回调onNewConnection

  3. 回调onData

bool FilterManagerImpl::initializeReadFilters() {if (upstream_filters_.empty()) {return false;}// 初始化完成后,开始从头开始执行filteronContinueReading(nullptr);return true;
}// 传入的是nullptr的时候,从头开始执行filter的
// 设置initialized_标志为true
// 回调onNewConnection,如果是返回stop就停止运行了
// 等待filter返回通过ReadFilterCallbacks回调onContinueReading来继续执行
void FilterManagerImpl::onContinueReading(ActiveReadFilter* filter) {std::list<ActiveReadFilterPtr>::iterator entry;if (!filter) {entry = upstream_filters_.begin();} else {entry = std::next(filter->entry());}for (; entry != upstream_filters_.end(); entry++) {if (!(*entry)->initialized_) {(*entry)->initialized_ = true;FilterStatus status = (*entry)->filter_->onNewConnection();if (status == FilterStatus::StopIteration) {return;}}BufferSource::StreamBuffer read_buffer = buffer_source_.getReadBuffer();if (read_buffer.buffer.length() > 0 || read_buffer.end_stream) {FilterStatus status = (*entry)->filter_->onData(read_buffer.buffer, read_buffer.end_stream);if (status == FilterStatus::StopIteration) {return;}}}
}

Example: 有三个filter、其中任何一个filter其中的一个callback返回StopIteration那么整个流程就停止了,需要等待调用onContinueReading才能继续 执行下一个callback方法。

FilterA::onNewConnection FilterA::onData

FilterB::onNewConnection FilterB::onData

FilterC::onNewConnection FilterC::onData

执行顺序为: FilterA::onNewConnection->FilterA::onData->FilterB::onNewConnection->FilterB::onData->FilterC::onNewConnection->FilterC::onData 任何一个callback返回StopIteration整个流程就不会继续往下走了,需要等待对应的filter回调onContinueReading,这样就会带来一个问题,一旦停止filter chain 继续往下走,那么网络层依然会收数据存在内部buffer里面,这会导致内存上涨,因此TCP PROXY中会在调用onNewConnection的时候先关闭读,然后和upstream建立连接 连接建立后才会开启读,防止内存被打爆。

目前实现的listener filter主要有original_dstoriginal_srcproxy_protocoltls_inspector等,同时支持用户扩展自定义的filter。

original_dst filter

一般应用于通过iptables或者tproxy的方式将流量发送给envoy,导致原来要访问的地址信息丢失,但是可以通过从socket中获取到这些信息,交给envoy做listen转发。

  1. 主要就是从socket中获取到原来的目的地址信息 (getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, &orig_addr, &addr_len))

  2. 然后设置socket的restore_local_address为原来的目的地址

Network::FilterStatus OriginalDstFilter::onAccept(Network::ListenerFilterCallbacks& cb) {ENVOY_LOG(debug, "original_dst: New connection accepted");Network::ConnectionSocket& socket = cb.socket();const Network::Address::Instance& local_address = *socket.localAddress();if (local_address.type() == Network::Address::Type::Ip) {Network::Address::InstanceConstSharedPtr original_local_address =getOriginalDst(socket.ioHandle().fd());// A listener that has the use_original_dst flag set to true can still receive// connections that are NOT redirected using iptables. If a connection was not redirected,// the address returned by getOriginalDst() matches the local address of the new socket.// In this case the listener handles the connection directly and does not hand it off.if (original_local_address) {// Restore the local address to the original one.socket.restoreLocalAddress(original_local_address);}}return Network::FilterStatus::Continue;

original_src filter

L3/L4 transparency的含义: L3要求源IP可见、L4要求端口可见,这个filter的目的是将原地址信息透传到upstream,让upstream可以 获取到真实的源IP和端口信息。

proxy_protocol filter

建立连接后发送一段数据来传递源地址和端口信息。

// 连接建立后,开始注册读事件,读取传递过来的数据。
Network::FilterStatus Filter::onAccept(Network::ListenerFilterCallbacks& cb) {ENVOY_LOG(debug, "proxy_protocol: New connection accepted");Network::ConnectionSocket& socket = cb.socket();ASSERT(file_event_.get() == nullptr);file_event_ =cb.dispatcher().createFileEvent(socket.ioHandle().fd(),[this](uint32_t events) {ASSERT(events == Event::FileReadyType::Read);onRead();},Event::FileTriggerType::Edge, Event::FileReadyType::Read);cb_ = &cb;return Network::FilterStatus::StopIteration;
}void Filter::onRead() {try {onReadWorker();} catch (const EnvoyException& ee) {config_->stats_.downstream_cx_proxy_proto_error_.inc();cb_->continueFilterChain(false);}
}// 读取proxy头,这里的读取是通过ioctl(fd, FIONREAD, &bytes_avail) 来获取缓存中的数据大小 ,
// 然后通过MSG_PEEK的方式查看数据。并不是直接read,因为一旦不是proxy ptorocol协议头会导致数据不完整(被读走了)。void Filter::onReadWorker() {Network::ConnectionSocket& socket = cb_->socket();if ((!proxy_protocol_header_.has_value() && !readProxyHeader(socket.ioHandle().fd())) ||(proxy_protocol_header_.has_value() && !parseExtensions(socket.ioHandle().fd()))) {// We return if a) we do not yet have the header, or b) we have the header but not yet all// the extension data. In both cases we'll be called again when the socket is ready to read// and pick up where we left off.return;}....// 读取完成后,拿到获取的源地址信息进行操作。// Only set the local address if it really changed, and mark it as address being restored.if (*proxy_protocol_header_.value().local_address_ != *socket.localAddress()) {socket.restoreLocalAddress(proxy_protocol_header_.value().local_address_);}socket.setRemoteAddress(proxy_protocol_header_.value().remote_address_);....
}

TLS Inspector filter

TLS Inspector listener filter allows detecting whether the transport appears to be TLS or plaintext, and if it is TLS, it detects the Server Name Indication and/or Application-Layer Protocol Negotiation from the client. This can be used to select a FilterChain via the server_names and/or application_protocols of a FilterChainMatch.

  1. 注册读数据,等待数据到来

  2. 解析Client hello报文

  3. 找到TLS信息,设置TransportSocket为Tls

Cluster分析

clusuter的定义

在Envoy中Cluster表示的是一个集群,一个集群主要包含两个部分的信息,一个部分是这个集群相关的配置,比如集群的名字、集群下机器建链的超时时间、负载均衡策略、建立链接用什么协议等等。 另外一个部分则是这个集群下所包含的机器列表。

在isto中 一组服务通过标签筛选中的上游pod及对应这里的集群下的集群列表, 服务则对应envoy 中的 cluster。

例如下面这个例子。

 clusters:- name: statsdtype: STATICconnect_timeout: 0.25slb_policy: ROUND_ROBINload_assignment:cluster_name: statsdendpoints:- lb_endpoints:- endpoint:address:socket_address:address: 127.0.0.1port_value: 8125protocol: TCP

 上面这段yaml定义了一个名为statsd的集群,负载均衡策略是ROUND_ROBIN、连接超时时间是0.25s、这个集群下面有一个机器,这个集群的类型是STATIC。根据这段yamlEnvoy就会创建一个Cluster对象。 这个Cluster对象并非是一个通用的对象,而且根据yaml中的type字段,找到对象类型的Cluster的构造工厂函数来进行构造。

而STRICT_DNS类型的Cluster则是通过DNS查询指定域名来获取机器列表的。EDS类型的Cluster则是通过发送EDS请求给控制面来获取机器列表的。无论是何种方式获取, 最终机器列表都是存在envoy::config::endpoint::v3::ClusterLoadAssignment这样的protobuf message中的。

message ClusterLoadAssignment {// Load balancing policy settings.// [#next-free-field: 6]message Policy {// [#not-implemented-hide:]message DropOverload {// Identifier for the policy specifying the drop.string category = 1 [(validate.rules).string = {min_bytes: 1}];// Percentage of traffic that should be dropped for the category.type.FractionalPercent drop_percentage = 2;}reserved 1;// Action to trim the overall incoming traffic to protect the upstream// hosts. This action allows protection in case the hosts are unable to// recover from an outage, or unable to autoscale or unable to handle// incoming traffic volume for any reason.//// At the client each category is applied one after the other to generate// the 'actual' drop percentage on all outgoing traffic. For example://// .. code-block:: json////  { "drop_overloads": [//      { "category": "throttle", "drop_percentage": 60 }//      { "category": "lb", "drop_percentage": 50 }//  ]}//// The actual drop percentages applied to the traffic at the clients will be//    "throttle"_drop = 60%//    "lb"_drop = 20%  // 50% of the remaining 'actual' load, which is 40%.//    actual_outgoing_load = 20% // remaining after applying all categories.// [#not-implemented-hide:]repeated DropOverload drop_overloads = 2;// Priority levels and localities are considered overprovisioned with this// factor (in percentage). This means that we don't consider a priority// level or locality unhealthy until the percentage of healthy hosts// multiplied by the overprovisioning factor drops below 100.// With the default value 140(1.4), Envoy doesn't consider a priority level// or a locality unhealthy until their percentage of healthy hosts drops// below 72%. For example://// .. code-block:: json////  { "overprovisioning_factor": 100 }//// Read more at :ref:`priority levels <arch_overview_load_balancing_priority_levels>` and// :ref:`localities <arch_overview_load_balancing_locality_weighted_lb>`.google.protobuf.UInt32Value overprovisioning_factor = 3 [(validate.rules).uint32 = {gt: 0}];// The max time until which the endpoints from this assignment can be used.// If no new assignments are received before this time expires the endpoints// are considered stale and should be marked unhealthy.// Defaults to 0 which means endpoints never go stale.google.protobuf.Duration endpoint_stale_after = 4 [(validate.rules).duration = {gt {}}];// The flag to disable overprovisioning. If it is set to true,// :ref:`overprovisioning factor// <arch_overview_load_balancing_overprovisioning_factor>` will be ignored// and Envoy will not perform graceful failover between priority levels or// localities as endpoints become unhealthy. Otherwise Envoy will perform// graceful failover as :ref:`overprovisioning factor// <arch_overview_load_balancing_overprovisioning_factor>` suggests.// [#not-implemented-hide:]bool disable_overprovisioning = 5 [deprecated = true];}// Name of the cluster. This will be the :ref:`service_name// <envoy_api_field_Cluster.EdsClusterConfig.service_name>` value if specified// in the cluster :ref:`EdsClusterConfig// <envoy_api_msg_Cluster.EdsClusterConfig>`.string cluster_name = 1 [(validate.rules).string = {min_bytes: 1}];// List of endpoints to load balance to.repeated endpoint.LocalityLbEndpoints endpoints = 2;// Map of named endpoints that can be referenced in LocalityLbEndpoints.// [#not-implemented-hide:]map<string, endpoint.Endpoint> named_endpoints = 5;// Load balancing policy settings.Policy policy = 4;
}

一个集群下面是多个LocalityLbEndpoints,一个LocalityLbEndpoints包含一个Locality、一个优先级、一个区域的权重、以及一批LbEndpoint 一个LbEndpoint包含了一个机器和对应的元数据和权重。

message LocalityLbEndpoints {// Identifies location of where the upstream hosts run.core.Locality locality = 1;// The group of endpoints belonging to the locality specified.repeated LbEndpoint lb_endpoints = 2;// Optional: Per priority/region/zone/sub_zone weight; at least 1. The load// balancing weight for a locality is divided by the sum of the weights of all// localities  at the same priority level to produce the effective percentage// of traffic for the locality. The sum of the weights of all localities at// the same priority level must not exceed uint32_t maximal value (4294967295).//// Locality weights are only considered when :ref:`locality weighted load// balancing <arch_overview_load_balancing_locality_weighted_lb>` is// configured. These weights are ignored otherwise. If no weights are// specified when locality weighted load balancing is enabled, the locality is// assigned no load.google.protobuf.UInt32Value load_balancing_weight = 3 [(validate.rules).uint32 = {gte: 1}];// Optional: the priority for this LocalityLbEndpoints. If unspecified this will// default to the highest priority (0).//// Under usual circumstances, Envoy will only select endpoints for the highest// priority (0). In the event all endpoints for a particular priority are// unavailable/unhealthy, Envoy will fail over to selecting endpoints for the// next highest priority group.//// Priorities should range from 0 (highest) to N (lowest) without skipping.uint32 priority = 5 [(validate.rules).uint32 = {lte: 128}];// Optional: Per locality proximity value which indicates how close this// locality is from the source locality. This value only provides ordering// information (lower the value, closer it is to the source locality).// This will be consumed by load balancing schemes that need proximity order// to determine where to route the requests.// [#not-implemented-hide:]google.protobuf.UInt32Value proximity = 6;
}

Primary Cluster 和 Secondary Cluster

Envoy 中,将构建两种集群 Primary 集群(Primary Cluster)Secondary 集群(Secondary Cluster) 主要用于 动态负载均衡服务发现(Service Discovery) 机制,二者的区别主要体现在 管理方式负载均衡的动态性 上。

Primary 集群(Primary Cluster)

概念

  • Primary 集群Envoy 启动时 解析并加载的集群,通常是静态定义的(但也可以是动态发现的)。

  • 这些集群通常 不会自动更新,除非 Envoy 重新加载配置。

特点

  • 可以是静态的(static)或者 动态的(EDS, DNS)。

  • 优先级较高,通常是 Envoy 最先解析并初始化 的集群。

  • 变更需要重新加载,或者依赖 动态服务发现(EDS) 来更新。

  • 适用于 长期稳定的后端服务,如数据库、监控系统等。

示例

clusters:- name: primary_clustertype: STATICload_assignment:cluster_name: primary_clusterendpoints:- lb_endpoints:- endpoint:address:socket_address:address: 10.0.0.1port_value: 80

Secondary 集群

概念

  • Secondary 集群 是在 Envoy 运行时 通过 xDS(如 CDS、EDS)动态添加和管理的。

  • 这些集群可以 在运行时动态添加、删除或修改,而无需重新加载 Envoy。

特点

  • 完全依赖 xDS 发现(如 CDS、EDS),不会在启动时静态加载

  • 动态性更强,适合需要 频繁变更 的服务(如微服务)。

  • 适用于大规模可变的服务发现场景,如 Kubernetes 中的 Pod 变更。

示例(CDS 动态发现)

dynamic_resources:cds_config:ads: {}

Cluster构建

无论是Primary cluster、还是Secondary Cluster,最终都是通过loadCluster把Cluster Protobuf变成Cluster对象。这两者的区别就是added_via_api,前者为false、后者为true。这个参数表明是否是通过API获取的。很明显Primary都不是通过API来获取的。

absl::StatusOr<ClusterManagerImpl::ClusterDataPtr>
ClusterManagerImpl::loadCluster(const envoy::config::cluster::v3::Cluster& cluster,const uint64_t cluster_hash, const std::string& version_info,bool added_via_api, const bool required_for_ads,ClusterMap& cluster_map)

这个方法主要做了以下几件事:

1. 通过ClusterManagerFactory以及Cluster的Protobuf来创建ClusterThreadAwareLoadBalancer

source/common/upstream/cluster_manager_impl.cc

absl::StatusOr<std::pair<ClusterSharedPtr, ThreadAwareLoadBalancerPtr>>new_cluster_pair_or_error =factory_.clusterFromProto(cluster, *this, outlier_event_logger_, added_via_api);

Cluster是对集群的抽象,而ThreadAwareLoadBalancer则是对这个集群Load Balancer的抽象,这个load balancer是感知线程的。 在实现自定义集群的时候需要自己来实现,目前Envoy中只有Dynamic forward proxyAggregateredis等三种类似的集群是实现了ThreadAwareLoadBalancer接口, 他们有自己专用的LoadBalancer,其他的集群用的都是Envoy内置的的几个标准Load Balancer实现。比如Aggregate集群的构造函数如下,他创建了AggregateThreadAwareLoadBalancer, 属于这个集群特有的LoadBalancer

source/extensions/clusters/aggregate/cluster.cc

absl::StatusOr<std::pair<Upstream::ClusterImplBaseSharedPtr, Upstream::ThreadAwareLoadBalancerPtr>>
ClusterFactory::createClusterWithConfig(const envoy::config::cluster::v3::Cluster& cluster,const envoy::extensions::clusters::aggregate::v3::ClusterConfig& proto_config,Upstream::ClusterFactoryContext& context) {absl::Status creation_status = absl::OkStatus();auto new_cluster =std::shared_ptr<Cluster>(new Cluster(cluster, proto_config, context, creation_status));RETURN_IF_NOT_OK(creation_status);auto lb = std::make_unique<AggregateThreadAwareLoadBalancer>(*new_cluster);return std::make_pair(new_cluster, std::move(lb));
}

2. 设置healthChecker、outlierDetector等callback

 ASSERT(state_ == State::WaitingToStartSecondaryInitialization ||state_ == State::CdsInitialized ||state_ == State::WaitingForPrimaryInitializationToComplete);ENVOY_LOG(debug, "maybe finish initialize primary init clusters empty: {}",primary_init_clusters_.empty());// If we are still waiting for primary clusters to initialize, do nothing.if (!primary_init_clusters_.empty()) {return;} else if (state_ == State::WaitingForPrimaryInitializationToComplete) {state_ = State::WaitingToStartSecondaryInitialization;if (primary_clusters_initialized_callback_) {primary_clusters_initialized_callback_();}return;}

首先判断是否完成了Primary Cluster的初始化,Primary Cluster初始化完成的标志就是primary_init_clusters_为空,因为载入的时候会把所有的Primary CLuster存进去, 然后遍历这个列表进行初始化,初始化完成的话则从这个列表中移除,因此这个列表为空就表明初始化完成了。

void ClusterManagerInitHelper::addCluster(ClusterManagerCluster& cm_cluster) {// See comments in ClusterManagerImpl::addOrUpdateCluster() for why this is only called during// server initialization.ASSERT(state_ != State::AllClustersInitialized);const auto initialize_cb = [&cm_cluster, this] {onClusterInit(cm_cluster);cm_cluster.cluster().info()->configUpdateStats().warming_state_.set(0);};Cluster& cluster = cm_cluster.cluster();cluster.info()->configUpdateStats().warming_state_.set(1);if (cluster.initializePhase() == Cluster::InitializePhase::Primary) {// Remove the previous cluster before the cluster object is destroyed.primary_init_clusters_.insert_or_assign(cm_cluster.cluster().info()->name(), &cm_cluster);cluster.initialize(initialize_cb);} else {ASSERT(cluster.initializePhase() == Cluster::InitializePhase::Secondary);// Remove the previous cluster before the cluster object is destroyed.secondary_init_clusters_.insert_or_assign(cm_cluster.cluster().info()->name(), &cm_cluster);if (started_secondary_initialize_) {// This can happen if we get a second CDS update that adds new clusters after we have// already started secondary init. In this case, just immediately initialize.cluster.initialize(initialize_cb);}}ENVOY_LOG(debug, "cm init: adding: cluster={} primary={} secondary={}", cluster.info()->name(),primary_init_clusters_.size(), secondary_init_clusters_.size());
}void ClusterManagerInitHelper::onClusterInit(ClusterManagerCluster& cluster) {ASSERT(state_ != State::AllClustersInitialized);per_cluster_init_callback_(cluster);removeCluster(cluster);
}void ClusterManagerInitHelper::removeCluster(ClusterManagerCluster& cluster) {if (state_ == State::AllClustersInitialized) {return;}// There is a remote edge case where we can remove a cluster via CDS that has not yet been// initialized. When called via the remove cluster API this code catches that case.absl::flat_hash_map<std::string, ClusterManagerCluster*>* cluster_map;if (cluster.cluster().initializePhase() == Cluster::InitializePhase::Primary) {cluster_map = &primary_init_clusters_;} else {ASSERT(cluster.cluster().initializePhase() == Cluster::InitializePhase::Secondary);cluster_map = &secondary_init_clusters_;}// It is possible that the cluster we are removing has already been initialized, and is not// present in the initializer map. If so, this is fine as a CDS update may happen for a// cluster with the same name. See the case "UpdateAlreadyInitialized" of the// target //test/common/upstream:cluster_manager_impl_test.auto iter = cluster_map->find(cluster.cluster().info()->name());if (iter != cluster_map->end() && iter->second == &cluster) {cluster_map->erase(iter);}ENVOY_LOG(debug, "cm init: init complete: cluster={} primary={} secondary={}",cluster.cluster().info()->name(), primary_init_clusters_.size(),secondary_init_clusters_.size());maybeFinishInitialize();
}

3. 如果Primary集群都初始化完成了,那接下来就看是否是在做Secondary cluster的初始化

secondary_init_clusters_不为空表明Secondary cluster还没有开始初始化或者没初始化完成,这个时候如果started_secondary_initialize_为false,表明 没有开始初始化。此时通过调用initializeSecondaryClusters开始正在的进行Secondary的初始化。

// If we are still waiting for secondary clusters to initialize, see if we need to first call// initialize on them. This is only done once.ENVOY_LOG(debug, "maybe finish initialize secondary init clusters empty: {}",secondary_init_clusters_.empty());if (!secondary_init_clusters_.empty()) {if (!started_secondary_initialize_) {ENVOY_LOG(info, "cm init: initializing secondary clusters");// If the first CDS response doesn't have any primary cluster, ClusterLoadAssignment// should be already paused by CdsApiImpl::onConfigUpdate(). Need to check that to// avoid double pause ClusterLoadAssignment.Config::ScopedResume maybe_resume_eds_leds_sds;if (cm_.adsMux()) {const std::vector<std::string> paused_xds_types{Config::getTypeUrl<envoy::config::endpoint::v3::ClusterLoadAssignment>(),Config::getTypeUrl<envoy::config::endpoint::v3::LbEndpoint>(),Config::getTypeUrl<envoy::extensions::transport_sockets::tls::v3::Secret>()};maybe_resume_eds_leds_sds = cm_.adsMux()->pause(paused_xds_types);}initializeSecondaryClusters();}return;

4. 初始化CDS,否则没有拿到所有的cluster没办法进行Seondary cluster的初始化

  // At this point, if we are doing static init, and we have CDS, start CDS init. Otherwise, move// directly to initialized.started_secondary_initialize_ = false;ENVOY_LOG(debug, "maybe finish initialize cds api ready: {}", cds_ != nullptr);if (state_ == State::WaitingToStartSecondaryInitialization && cds_) {ENVOY_LOG(info, "cm init: initializing cds");state_ = State::WaitingToStartCdsInitialization;cds_->initialize();} else {ENVOY_LOG(info, "cm init: all clusters initialized");state_ = State::AllClustersInitialized;if (initialized_callback_) {initialized_callback_();}}

cds初始化完成后会发送xds请求给控制面获取所有的cluster,当收到所有的cluster的时候,就触发cds设置的callback,在callback里面会再次触发maybeFinishInitialize 这个时候就走到了步骤3中的逻辑了。

void ClusterManagerInitHelper::setCds(CdsApi* cds) {ASSERT(state_ == State::Loading);cds_ = cds;if (cds_) {cds_->setInitializedCb([this]() -> void {ASSERT(state_ == State::WaitingToStartCdsInitialization);state_ = State::CdsInitialized;maybeFinishInitialize();});}
}

cluster在线程中的同步

每一个Cluster初始化完成后都会在其callback中调用这个方法进行Cluster额外的初始化。在这个初始化中会添加一些callback 最后触发thread local cluster的更新,以确保每一个thread都包含了最新的cluster内容了。

void ClusterManagerImpl::onClusterInit(ClusterManagerCluster& cm_cluster)

在运行中一个Cluster动态初始化完成或者更新后,需要更新所有的Thread Local中,让所有的Thread可以拿到最新的Cluster

void ClusterManagerImpl::postThreadLocalClusterUpdate(ClusterManagerCluster& cm_cluster,ThreadLocalClusterUpdateParams&& params);

设置集群的AddedOrUpdated位,表明已经更新了

bool add_or_update_cluster = false;if (!cm_cluster.addedOrUpdated()) {add_or_update_cluster = true;cm_cluster.setAddedOrUpdated();}

开始生成update hosts params、locality weight、overprovision_factor等需要参数

各个thread中的Cluster Priority Set会根据这些参数来进行更新。

for (auto& per_priority : params.per_priority_update_params_) {const auto& host_set =cm_cluster.cluster().prioritySet().hostSetsPerPriority()[per_priority.priority_];per_priority.update_hosts_params_ = HostSetImpl::updateHostsParams(*host_set);per_priority.locality_weights_ = host_set->localityWeights();per_priority.weighted_priority_health_ = host_set->weightedPriorityHealth();per_priority.overprovisioning_factor_ = host_set->overprovisioningFactor();}

在各个线程中获取到ThreadLocalClusterManagerImpl同步更新

  tls_.runOnAllThreads([info = cm_cluster.cluster().info(), params = std::move(params),add_or_update_cluster, load_balancer_factory, map = std::move(host_map),cluster_initialization_object = std::move(cluster_initialization_object),drop_overload](OptRef<ThreadLocalClusterManagerImpl> cluster_manager) {ASSERT(cluster_manager.has_value(),"Expected the ThreadLocalClusterManager to be set during ClusterManagerImpl creation.");// Cluster Manager here provided by the particular thread, it will provide// this allowing to make the relevant change.if (const bool defer_unused_clusters =cluster_initialization_object != nullptr &&!cluster_manager->thread_local_clusters_.contains(info->name()) &&!Envoy::Thread::MainThread::isMainThread();defer_unused_clusters) {// Save the cluster initialization object.ENVOY_LOG(debug, "Deferring add or update for TLS cluster {}", info->name());cluster_manager->thread_local_deferred_clusters_[info->name()] =cluster_initialization_object;// Invoke similar logic of onClusterAddOrUpdate.ThreadLocalClusterCommand command = [&cluster_manager,cluster_name = info->name()]() -> ThreadLocalCluster& {// If we have multiple callbacks only the first one needs to use the// command to initialize the cluster.auto existing_cluster_entry = cluster_manager->thread_local_clusters_.find(cluster_name);if (existing_cluster_entry != cluster_manager->thread_local_clusters_.end()) {return *existing_cluster_entry->second;}auto* cluster_entry = cluster_manager->initializeClusterInlineIfExists(cluster_name);ASSERT(cluster_entry != nullptr, "Deferred clusters initiailization should not fail.");return *cluster_entry;};for (auto cb_it = cluster_manager->update_callbacks_.begin();cb_it != cluster_manager->update_callbacks_.end();) {// The current callback may remove itself from the list, so a handle for// the next item is fetched before calling the callback.auto curr_cb_it = cb_it;++cb_it;(*curr_cb_it)->onClusterAddOrUpdate(info->name(), command);}} else {// BroadcastThreadLocalClusterManagerImpl::ClusterEntry* new_cluster = nullptr;if (add_or_update_cluster) {if (cluster_manager->thread_local_clusters_.contains(info->name())) {ENVOY_LOG(debug, "updating TLS cluster {}", info->name());} else {ENVOY_LOG(debug, "adding TLS cluster {}", info->name());}new_cluster = new ThreadLocalClusterManagerImpl::ClusterEntry(*cluster_manager, info,load_balancer_factory);cluster_manager->thread_local_clusters_[info->name()].reset(new_cluster);cluster_manager->local_stats_.clusters_inflated_.set(cluster_manager->thread_local_clusters_.size());}if (cluster_manager->thread_local_clusters_[info->name()]) {cluster_manager->thread_local_clusters_[info->name()]->setDropOverload(drop_overload);}for (const auto& per_priority : params.per_priority_update_params_) {cluster_manager->updateClusterMembership(info->name(), per_priority.priority_, per_priority.update_hosts_params_,per_priority.locality_weights_, per_priority.hosts_added_, per_priority.hosts_removed_,per_priority.weighted_priority_health_, per_priority.overprovisioning_factor_, map);}if (new_cluster != nullptr) {ThreadLocalClusterCommand command = [&new_cluster]() -> ThreadLocalCluster& {return *new_cluster;};for (auto cb_it = cluster_manager->update_callbacks_.begin();cb_it != cluster_manager->update_callbacks_.end();) {// The current callback may remove itself from the list, so a handle for// the next item is fetched before calling the callback.auto curr_cb_it = cb_it;++cb_it;(*curr_cb_it)->onClusterAddOrUpdate(info->name(), command);}}}});

router分析

Envoy 通过 route_config 规则匹配请求路径:

  • 根据 HostPathMethod 进行匹配。
  • 将请求路由到 指定集群(Cluster)
routes:- match:prefix: "/api"route:cluster: backend_service

此外流量定向到对应的cluster后,需要获取custer下的主机列表做负载均衡

  • Envoy 支持 多种负载均衡策略
    • Round Robin(轮询)
    • Random(随机)
    • Least Request(最少请求)
    • Ring Hash(哈希一致性)
  • 负载均衡策略在 Cluster 配置 中定义:
clusters:- name: backend_servicetype: EDSlb_policy: ROUND_ROBINload_assignment:cluster_name: backend_serviceendpoints:- lb_endpoints:- endpoint:address:socket_address:address: 10.0.0.1port_value: 8080

相关文章:

envoy 源码分析

整体架构 Envoy 的架构如图所示: Envoy 中也可能有多个 Listener&#xff0c;每个 Listener 中可能会有多个 filter 组成了 chain。 Envoy 接收到请求后&#xff0c;会先走 FilterChain&#xff0c;通过各种 L3/L4/L7 Filter 对请求进行微处理&#xff0c;然后再路由到指定的集…...

c++基础知识--返回值优化

在 C 中&#xff0c;Named Return Value Optimization&#xff08;NRVO&#xff0c;具名返回值优化&#xff09; 是一种编译器优化技术&#xff0c;用于消除函数返回一个局部对象时的拷贝或移动操作。它是 返回值优化&#xff08;RVO&#xff09; 的一种更复杂的变体&#xff0…...

【第14节】windows sdk编程:进程与线程介绍

目录 一、进程与线程概述 1.1 进程查看 1.2 何为进程 1.3 进程的创建 1.4 进程创建实例 1.5 线程查看 1.6 何为线程 1.7 线程的创建 1.8 线程函数 1.9 线程实例 二、内核对象 2.1 何为内核对象 2.2 内核对象的公共特点 2.3 内核对象句柄 2.4 内核对象的跨进程访…...

实测 Gemini 2.0 Flash 图像生成:多模态 AI 的创作力边界

近日&#xff0c;Google 发布了 Gemini 2.0 Flash 的实验性图像生成功能&#xff08;Gemini 2.0 Flash (Image Generation) Experimental&#xff09;。我也第一时间体验了这一功能&#xff0c;再次感受到 AI 技术对传统图像处理工具的颠覆性冲击。 引言 Gemini 2.0 Flash 的…...

每日一题——买卖股票的最佳时机

买卖股票的最佳时机 问题描述示例示例 1示例 2 提示 问题分析难点分析 算法设计思路 代码实现复杂度分析测试用例测试用例 1测试用例 2测试用例 3 总结 问题描述 给定一个数组 prices&#xff0c;其中第 i 个元素 prices[i] 表示一支给定股票在第 i 天的价格。你可以选择某一天…...

以太坊节点间通信机制 DEVp2p 协议

文章目录 概要1. 协议概述2. 协议栈与关键技术3. RLPx 协议核心机制3.1 数据包结构3.2 加密握手流程 4. 核心子协议与消息类型4.1 基础控制消息4.2 以太坊子协议示例4.3 网络 ID 列表 5. 安全与防攻击机制6. 节点标识与声誉管理7. 对比其他区块链通信协议8. 总结 概要 1. 协议…...

YOLO+OpenCV强强联手:高精度跌倒检测技术实战解析

目录 关于摔倒检测 摔倒检测核心逻辑 摔倒检测:联合多种逻辑判断 原理详细解释 1. 导入必要的库 2. 定义函数和关键点连接关系 3. 筛选有效关键点并计算边界框 4. 计算人体上下半身中心点和角度 5. 绘制关键点和连接线 6. 绘制角度标注和检测跌倒 7. 返回处理后的图…...

HyperAD:学习弱监督音视频暴力检测在双曲空间中的方法

文章目录 速览摘要1. 引言2. 相关工作弱监督暴力检测双曲空间中的神经网络 3. 预备知识双曲几何切空间&#xff08;Tangent Space&#xff09;指数映射与对数映射&#xff08;Exponential and Logarithmic Maps&#xff09;3.1 双曲图卷积网络&#xff08;Hyperbolic Graph Con…...

网络协议抓取与分析(SSL Pinning突破)

1. 网络协议逆向基础 1.1 网络协议分析流程 graph TD A[抓包环境配置] --> B[流量捕获] B --> C{协议类型} C -->|HTTP| D[明文解析] C -->|HTTPS| E[SSL Pinning突破] D --> F[参数逆向] E --> F F --> G[协议重放与模拟] 1.1.1 关键分析目标…...

基于C#的以太网通讯实现:TcpClient异步通讯详解

基于C#的以太网通讯实现&#xff1a;TcpClient异步通讯详解 在现代工业控制和物联网应用中&#xff0c;以太网通讯是一种常见的数据传输方式。本文将介绍如何使用C#实现基于TCP协议的以太网通讯&#xff0c;并通过异步编程提高通讯效率。我们将使用TcpClient类来实现客户端与服…...

通过C#脚本更改材质球的参数

// 设置贴图Texture mTexture Resources.Load("myTexture", typeof(Texture )) as Texture;material.SetTexture("_MainTex", mTexture );// 设置整数material.SetInt("_Int", 1);// 设置浮点material.SetFloat("_Float", 0.1f);// 设…...

SpringBoot常用注解

SpringBoot常用注解 SpringBoot框架提供了丰富的注解&#xff0c;极大地简化了应用开发。本文将SpringBoot常用注解按功能分组&#xff0c;并提供详细说明和使用示例。 一、核心注解 1. SpringBootApplication 这是SpringBoot应用的核心注解&#xff0c;标记在主类上&#…...

Vim 编辑器复制文件所有内容

Vim 编辑器复制文件所有内容 在 Vim 的可视化模式下复制所有内容&#xff0c;可以通过以下步骤完成&#xff1a; 方法 1&#xff1a;可视化模式全选复制 进入可视化模式 按下 V&#xff08;大写 V&#xff09;进入 行可视化模式。 全选内容 依次按下 gg&#xff08;跳转到文件…...

MySQL 安全传输

Doris 开启 SSL 功能需要配置 CA 密钥证书和 Server 端密钥证书&#xff0c;如需开启双向认证&#xff0c;还需生成 Client 端密钥证书&#xff1a; 默认的 CA 密钥证书文件位于Doris/fe/mysql_ssl_default_certificate/ca_certificate.p12&#xff0c;默认密码为doris&#xf…...

【速览】数据库

一、课程性质和特点 数据库系统原理是高等教育自学考试计算机信息管理专业(独立本科段)、计算机网络专业(独立本科段)、计算机及应用专业(独立本科段)、计算机通信工程专业(独立本科段)考试计划的一门专业基础课。本课程的设置目的是为了使应考者掌握数据库系统的基本原理、方法…...

MySQL 中利用 mysql.help_topic 实现行转列的深入剖析

MySQL 中利用 mysql.help_topic 实现行转列的深入剖析 在数据库操作中&#xff0c;我们常常会遇到数据格式转换的需求。其中&#xff0c;行转列是一种常见的数据处理任务&#xff0c;它能将数据从一种便于存储的行结构&#xff0c;转换为更便于分析和展示的列结构。在 MySQL 数…...

学习使用smartengine

1、开源地址 smartengine的地址 GitCode - 全球开发者的开源社区,开源代码托管平台 2、如何基于这个开源的框架实现自己的业务定制 参考一些文章&#xff1a; 探索BPMN—工作流技术的理论与实践&#xff5c;得物技术...

鸿蒙保姆级教学

鸿蒙&#xff08;HarmonyOS&#xff09;是华为推出的一款面向全场景的分布式操作系统&#xff0c;支持手机、平板、智能穿戴、智能家居、车载设备等多种设备。鸿蒙系统的核心特点是分布式架构、一次开发多端部署和高性能。以下是从入门到大神级别的鸿蒙开发深度分析&#xff0c…...

HW华为流程管理体系精髓提炼华为流程运营体系(124页PPT)(文末有下载方式)

资料解读&#xff1a;HW华为流程管理体系精髓提炼华为流程运营体系&#xff08;124页PPT&#xff09; 详细资料请看本解读文章的最后内容。 华为作为全球领先的科技公司&#xff0c;其流程管理体系的构建与运营是其成功的关键之一。本文将从华为流程管理体系的核心理念、构建…...

What a code!

要在前后两个图表之间连接对应的坐标轴刻度点&#xff0c;可以通过在父部件中绘制线条来实现。以下是具体步骤和代码实现&#xff1a; 步骤说明 重写paintEvent函数&#xff1a;在Bigraph的paintEvent中绘制连接线。获取刻度值列表&#xff1a;根据每个坐标轴的最小值、最大值…...

Qt开发中的常见问题与解决方案

目录 1.Qt中大资源文件的处理 2.中文URL编码问题 3.编译器类型、版本与操作系统的判断 4.Qt版本与构建套件位数的判断 5.QWidget样式表不起作用的解决方案 6.动态改变弹簧的拉伸策略 7.文件操作的性能优化 8.自定义心跳包与TCP保活机制 9.Qt平台插件加载失败问题 10.…...

蓝桥杯嵌入式赛道复习笔记3(lcd与led引脚冲突问题)

直接上干货 1.在初始化lcd之前要关闭锁存器 切记一定要开启PD2的引脚&#xff0c;否则白搭 2.在用到的lcd函数要加 uint16_t temp GPIOC->ODR;GPIOC->ODR temp;例如...

【cf】交换

交换数组中元素&#xff0c;逆序对数1&#xff0c;所以逆序对奇偶性发生改变 D. Swap Dilemma https://www.cnblogs.com/pure4knowledge/p/18292578这个写的太好了 任意交换两个数&#xff0c;会使序列的逆序对数加减一个奇数。 所以如果两个序列&#xff0c;初始逆序对数的奇…...

anythingLLM之stream-chat传参

1、 接口地址 /v1/workspace/{slug}/stream-chat POST请求 {"message": "根据以下事件信息找出今天发生的事件有哪几个[{\"事件所在桩号\":\"K1045900\",\"事件发生位置&#xff08;经纬度值&#xff09;\":\"114.149…...

友思特应用 | 行业首创:基于深度学习视觉平台的AI驱动轮胎检测自动化

导读 全球领先的轮胎制造商 NEXEN TIRE 在其轮胎生产检测过程中使用了基于友思特伙伴Neurocle开发的AI深度学习视觉平台&#xff0c;实现缺陷检测率高达99.96%&#xff0c;是该行业首个使用AI平台技术推动缺陷检测自动化流程的企业。 将AI应用从轮胎开发扩展到制造过程 2024年…...

Python 变量的定义与使用:从基础到高级

Python 变量的定义与使用:从基础到高级 在 Python 中,变量是程序中最基本的概念之一。变量用于存储数据,并在程序运行过程中随时访问和修改这些数据。理解变量的定义和使用是学习 Python 编程的第一步。 1. 变量的定义 1.1 什么是变量? 变量是程序中用于存储数据的容器。…...

Linux 系统性能调优

概述 在日常运维和架构优化中&#xff0c;Linux 性能调优是提高系统稳定性和运行效率的重要手段。本文结合工作经验&#xff0c;总结了 Linux 服务器常见的优化技巧&#xff0c;涵盖 CPU、内存、磁盘 I/O、网络等多个方面&#xff0c;帮助大家在不同场景下快速定位和优化系统性…...

蓝桥杯备考:奶牛晒衣服

这道题第一眼想用贪心做&#xff0c;1 2 3 我们可以让最多的3用烘干机1秒就能完成&#xff0c;那么是不是我们每次都给湿度最大的衣服用烘干机呢&#xff1f;我们试试哈&#xff0c;比如[5,8]&#xff0c;每秒晒干1我们给8衣服一直用烘干机是需要4秒的&#xff0c;4秒后8这个…...

英伟达“AI 超级碗”开幕

Nvidia的AI和机器人技术进展 2025年03月19日 | AI日报 ![](https://i-blog.csdnimg.cn/direct/e7838b88f17f40c9a435f6dc48d26c59.jpeg#pic_center) 欢迎各位人工智能爱好者。 Nvidia的CEO Jensen Huang刚刚拉开了他的“AI超级碗”&#xff0c;并发表了关于该公司最新芯片、…...

Java使用FFmpegFrameGrabber进行视频拆帧,结合Thumbnails压缩图片保存到文件夹

引入依赖 <dependency><groupId>net.coobird</groupId><artifactId>thumbnailator</artifactId><version>0.4.17</version></dependency><dependency><groupId>org.bytedeco</groupId><artifactId>ja…...

KVM安全模块生产环境配置与优化指南

KVM安全模块生产环境配置与优化指南 一、引言 在当今复杂多变的网络安全环境下&#xff0c;生产环境中KVM&#xff08;Kernel-based Virtual Machine&#xff09;的安全配置显得尤为重要。本指南旨在详细阐述KVM安全模块的配置方法&#xff0c;结合强制访问控制&#xff08;M…...

如何设计一个 RPC 框架?需要考虑哪些点?

设计一个完整的 RPC 框架需要覆盖以下核心模块及关键技术点&#xff1a; 一、核心架构模块 模块功能与实现要点服务注册与发现使用 Zookeeper/Nacos 等实现服务地址动态注册与订阅&#xff0c;支持心跳检测和节点变更通知网络通信层基于 Netty 或 gRPC 的 HTTP/2 实现异步非阻…...

dify+deepseek联网搜索:免费开源搜索引擎Searxng使用(让你的大模型也拥有联网的功能)

docker安装SearXng 项目地址:https://github.com/searxng/searxng-docker 第一步 git clone下来 git clone https://github.com/searxng/searxng-docker.git第二步 进入 searxng-docker目录中修改docker-compose.yaml(直接复制粘贴) cd searxng-dockerdocker-compose.yaml …...

主流的Java生态下权限管理框架

在当今国内互联网行业中&#xff0c;主流的Java生态下权限管理框架主要分为三类&#xff1a; 通用权限框架&#xff08;含认证和权限&#xff09;权限细粒度控制框架&#xff08;专注资源访问&#xff09;企业级安全认证和权限框架&#xff08;更完善的安全功能&#xff09; &…...

dijkstra算法——47. 参加科学大会

卡码网:47. 参加科学大会https://kamacoder.com/problempage.php?pid=1047 题目描述 小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。 小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以…...

LAC拨号的L2TP VPN实验

目录 一.拓扑信息​ 二.需求分析 三.详细配置信息 1.基础信息配置 服务器&#xff1a; 2.建立PPPOE 3.建立L2TP隧道 4.安全策略 四.测试 一.拓扑信息​ 二.需求分析 一.基础信息配置&#xff08;IP和安全区域&#xff09; 二.建立PPPOE连接 是FW1和FW2之间的配置&#…...

天梯赛 PTAL2-009 抢红包

很简单的一道模拟题&#xff0c;使用map统计每个用户的钱数和红包数&#xff0c;最后在使用结构体存储&#xff0c;重载小于号&#xff0c;sort排序即可。 #include <bits/stdc.h> using namespace std; #define endl \n #define int long long typedef long long ll; c…...

信息学奥赛一本通 1831:【03NOIP提高组】神经网络 | 洛谷 P1038 [NOIP 2003 提高组] 神经网络

【题目链接】 ybt 1831&#xff1a;【03NOIP提高组】神经网络 洛谷 P1038 [NOIP 2003 提高组] 神经网络 【题目考点】 1. 图论&#xff1a;拓扑排序&#xff0c;有向无环图动规 【解题思路】 神经网络是一个有向无环图&#xff0c;输入层神经元是入度为0的顶点&#xff0c…...

如何切换node版本

在Linux或MacOS系统中&#xff0c;切换Node.js版本通常可以通过nvm&#xff08;Node Version Manager&#xff09;工具来实现。nvm允许你在不同的Node.js版本之间轻松切换&#xff0c;而无需重新安装或配置。 安装nvm 使用curl命令安装nvm&#xff08;适用于大多数Linux发行版…...

前端样式库推广——TailwindCss

官方网址&#xff1a; https://tailwindcss.com/docs/installation/using-vite 中文官方文档&#xff1a;https://www.tailwindcss.cn/ github地址&#xff1a;tailwindcss 正在使用tailwindcss的网站&#xff1a;https://tailwindcss.com/showcase 一看github&#xff0c;竟然…...

【前端 vue 或者麦克风,智能语音识别和播放功能】

前端 vue 或者麦克风&#xff0c;智能语音识别和播放功能 1. 终端安装 npm install recordrtc2.引入 import RecordRTC from recordrtc3.html&#xff08;根据自己业务更改&#xff09; <div class"Page"><el-form ref"mainFormRef" class&qu…...

Java基础编程练习第34题-正则表达式

在Java里&#xff0c;正则表达式是一种强大的文本处理工具&#xff0c;它可以用于字符串的搜索、替换、分割和校验等操作。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。Java通过java.util.regex包提供了对正则表达式的支持。 以下是正则表达式在Jav…...

Java+Html实现前后端客服聊天

文章目录 核心组件网络通信层事件调度层服务编排层 Spring实现客服聊天技术方案对比WebScoket建立连接用户上线实现指定用户私聊群聊离线 SpringBootWebSocketHtmljQuery实现客服聊天1. 目录结构2. 配置类3. 实体类、service、controller4. ChatWebSocketHandler消息处理5.前端…...

基于Spring Boot的冷链物流系统的设计与实现的设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…...

《线程池:Linux平台编译线程池动态库发生的死锁问题》

关于如何编译动态库可以移步《Linux&#xff1a;动态库动态链接与静态库静态链接》-CSDN博客 我们写的线程池代码是闭源的&#xff0c;未来想提供给别人使用&#xff0c;只需要提供so库和头文件即可。 系统默认库文件路径为&#xff1a; usr/lib usr/loacl/lib 系统默认头文件…...

鸿蒙NEXT项目实战-百得知识库03

代码仓地址&#xff0c;大家记得点个star IbestKnowTeach: 百得知识库基于鸿蒙NEXT稳定版实现的一款企业级开发项目案例。 本案例涉及到多个鸿蒙相关技术知识点&#xff1a; 1、布局 2、配置文件 3、组件的封装和使用 4、路由的使用 5、请求响应拦截器的封装 6、位置服务 7、三…...

sql server数据迁移,springboot搭建开发环境遇到的问题及解决方案

最近搭建springboot项目开发环境&#xff0c;数据库连的是sql server&#xff0c;遇到许多问题在此记录一下。 1、sql server安装教程 参考&#xff1a;https://www.bilibili.com/opus/944736210624970769 2、sql server导出、导入数据库 参考&#xff1a;https://blog.csd…...

Sensodrive机器人力控关节模组SensoJoint在海洋垃圾清理机器人中的拓展应用

海洋污染已成为全球性的环境挑战&#xff0c;其中海底垃圾的清理尤为困难。据研究&#xff0c;海洋中约有2600万至6600万吨垃圾&#xff0c;超过90%沉积在海底。传统上&#xff0c;潜水员收集海底垃圾不仅成本高昂&#xff0c;而且充满风险。为解决这一问题&#xff0c;欧盟资助…...

matrix-breakout-2-morpheus 靶机----练习攻略 【仅获取shell】

【此练习仅做到反弹shell】 1.靶机下载地址 https://download.vulnhub.com/matrix-breakout/matrix-breakout-2-morpheus.ova 2. 打开靶机&#xff0c;kali使用nmap扫描同C段的主机 找到靶机ip 确保靶机和kali网卡均为NAT模式 先查看kali的ip nmap 192.168.182.1/24 …...

吴恩达机器学习笔记复盘(八)多元线性回归的梯度下降

简介 梯度下降是多元线性回归的主流优化方法&#xff0c;具有普适性和可扩展性&#xff0c;而标准方程法适用于特定场景。实际应用中需结合特征工程和参数调优提升模型性能。本篇不复盘参数调优。 1.多元线性回归模型 多元线性回归模型假设因变量 与多个自变量 之间存在线性…...