Impala3.4源码阅读笔记(七)解析ScanNode(上)

news2024/11/24 17:44:56

前言

本文为笔者个人阅读Apache Impala源码时的笔记,仅代表我个人对代码的理解,个人水平有限,文章可能存在理解错误、遗漏或者过时之处。如果有任何错误或者有更好的见解,欢迎指正。

正文

我们知道Impala执行一条SQL的主要流程包括:

  1. 指定单节点执行计划,将SQL转换为一颗包含若干不同计划结点PlanNode的计划树PlanTree
  2. 制定分布式执行计划,将单节点计划树拆分为若干片段Fragment,以便在分布式集群上分配调度;
  3. 分配与调度执行计划片段到执行节点;
  4. 各执行结点根据Fragment创建若干执行实例Instance,在一个InstanceFragment的每个PlanNode对应生成一个执行结点ExecNode
  5. 各执行结点完成各自的任务(如扫描数据、聚合、排序等等),数据以行批RowBatch的形式在结点间传递;
  6. 数据最终汇集到根结点作为查询结果返回给客户端,SQL执行完成;

在这一流程中,各个执行结点各司其职、逻辑解耦的设计使得Impala的开发与优化变得清晰与方便。在各类执行结点中,扫描结点ScanNode作为一个大类包括了各种数据源的扫描结点派生,负责了各种数据源的扫描,是Impala中最重要的结点之一。本文将以执行结点中的Kudu扫描结点为例子介绍ScanNode的主要结构和执行流程。

ScanNode的继承与派生关系

Impala支持多种数据源,每种数据都对应了一种扫描结点,这些扫描结点都派生自ScanNode类,具体派生关系如图所示:
在这里插入图片描述
图中ExecNode是所有执行结点的基类,主要定义了PrepareOpenGetNextClose四个接口,所有的执行结点都需要实现这些方法,完成准备、开启、获取下一批数据和关闭四种逻辑,整个执行树ExecTree的开关与执行也正是由根结点到叶结点地调用这些方法。

图中ScanNode作为所有扫描结点的基类,直接继承了ExecNode并在其基础之上增加了ScanRange、runtime filters和许多扫描性能相关的计时器、计数器,另外还有一个负责多线程扫描使用的内部类ScannerThreadState

如图所示,ScanNode又进一步派生出四个类,分别对应了自定义数据源(DataSource)、HBase、Kudu和Hdfs。其中Kudu和Hdfs都支持了MT_DOP功能(Impala中提升查询并发度的功能,可以手动指定运行多个实例来提升性能),所以还包括了MT和非MT两个版本的扫描结点。

各个扫描结点为了完成对应数据源的扫描工作,可能还会包含各自的扫描器类,如KuduScanner包括了连接Kudu、物化数据等逻辑。而HdfsScanner更加复杂,根据数据储存格式又分为了文本格式扫描器HdfsTextScanner、列存格式扫描器HdfsCoulumnarScanner等,HdfsCoulumnarScanner又进一步派生出了ORC格式、Parquet格式对应的扫描器。

由于ScanNode的派生类众多,尤其是Impala主力支持的Hdfs下还有众多Scanner类,想要一一介绍则篇幅过长,而KuduScanNode的相关代码量适中又包含了完整全面的相关逻辑,所以本文选择了Kudu的扫描结点作为例子来进行介绍。

Kudu扫描结点的基类KuduScanNodeBase

KuduScanNodeBase继承了ScanNode,是KuduScanNodeMTKuduScanNode的基类,包括了两者共通的一些逻辑和成员,是比较简单的一个类,我们直接看其定义:

class KuduScanNodeBase : public ScanNode {
 public:
  KuduScanNodeBase(ObjectPool* pool, const ScanPlanNode& pnode, const DescriptorTbl& descs);
  ~KuduScanNodeBase();

  virtual Status Prepare(RuntimeState* state) override;
  virtual Status Open(RuntimeState* state) override;
  // GetNext被定义为纯虚函数,需要KuduScanNode和KuduScanNodeMT具体实现,KuduScanNodeBase本身无法实例化。
  virtual Status GetNext(RuntimeState* state, RowBatch* row_batch, bool* eos)
      override = 0;

  // KuduScanNode支持利用统计信息对count(*)查询进行优化。
  bool optimize_count_star() const { return count_star_slot_offset_ != -1; }
  int count_star_slot_offset() const { return count_star_slot_offset_; }

 protected:
  virtual void DebugString(int indentation_level, std::stringstream* out) const override;

  /// 返回扫描令牌的总数,扫描令牌是Kudu中类似ScanRange的对象,描述了Kudu表的一段连续物理位置。
  int NumScanTokens() { return scan_tokens_.size(); }

  /// 返回是否还有扫描令牌剩余,非线程安全的函数。
  bool HasScanToken() { return (next_scan_token_idx_ < scan_tokens_.size()); }

  /// 返回下一个扫描令牌,如果没有剩余的扫描令牌,则返回nullptr,非线程安全的函数。
  const std::string* GetNextScanToken() {
    if (!HasScanToken()) return nullptr;
    const string* token = &scan_tokens_[next_scan_token_idx_++];
    return token;
  }

  const TupleDescriptor* tuple_desc() const { return tuple_desc_; }

 private:
  friend class KuduScanner;

  /// 需要从Kudu表读取的数据的元组描述符在TupleDescriptorMap中的标识id.
  const TupleId tuple_id_;

  /// 需要从Kudu表读取的数据的元组描述符,主要包括若干槽位描述符SlotDescriptor。
  const TupleDescriptor* tuple_desc_ = nullptr;

  /// 指向KuduClient的指针,对象本身存储在QueryState中,并在KuduScanner和实例之间共享。
  /// KuduClient是Kudu提供的C++ API之一,用于连接Kudu读取数据。
  kudu::client::KuduClient* client_ = nullptr;

  /// Kudu API中的Kudu表对象, 在KuduScanNode的多个KuduScanner之间共享。
  kudu::client::sp::shared_ptr<kudu::client::KuduTable> table_;

  /// 本扫描结点需要处理的所有扫描令牌,以序列化后的形式储存,由KuduScanner反序列化并处理。
  std::vector<std::string> scan_tokens_;

  /// scan_tokens_中下一个待分配的令牌的索引。
  int next_scan_token_idx_ = 0;

  /// 如果启用了count(*)查询优化,该则值被设置为count(*)槽位在元组中的字节偏移量,否则为-1。
  /// 设置此参数后,该扫描节点可以通过使用num rows统计中的数据快速填充元组来优化count(*)查询。
  const int count_star_slot_offset_;

  /// Kudu相关的一些性能计数器。
  RuntimeProfile::Counter* kudu_round_trips_ = nullptr;
  RuntimeProfile::Counter* kudu_remote_tokens_ = nullptr;
  RuntimeProfile::Counter* kudu_client_time_ = nullptr;
  static const std::string KUDU_ROUND_TRIPS;
  static const std::string KUDU_REMOTE_TOKENS;
  static const std::string KUDU_CLIENT_TIME;

  kudu::client::KuduClient* kudu_client() { return client_; }
  RuntimeProfile::Counter* kudu_round_trips() const { return kudu_round_trips_; }
  RuntimeProfile::Counter* kudu_client_time() const { return kudu_client_time_; }
};

看完了KuduScanNodeBase的定义,我们再看看其中几个关键函数的实现,首先是Prepare函数,其负责了结点对象创建之后的准备工作,代码如下:

Status KuduScanNodeBase::Prepare(RuntimeState* state) {
  // 首先调用基类ScanNode的Prepare函数,完成ScanNode通用的准备工作。
  // 在ScanNode::Prepare()中又会调用ExecNode::Prepare()完成执行结点通用的准备工作。
  // ExecNode::Prepare()中的准备工作包括创建一些内存追踪器、内存池、计数器和评估谓词的表达式求值器,
  // 同时其还会调用其所有子结点的Prepare函数,不过本例ScanNode在ExecTree中都是作为叶子结点的,并无子结点。
  // ScanNode::Prepare()中的准备工作则包括创建一些Scan特有的计数器计时器和准备runtime filters上下文对象并为其创建表达式求值器。
  RETURN_IF_ERROR(ScanNode::Prepare(state));

  // 以下是一些计数器和计时器的初始化
  scan_ranges_complete_counter_ =
      PROFILE_ScanRangesComplete.Instantiate(runtime_profile());
  kudu_round_trips_ = ADD_COUNTER(runtime_profile(), KUDU_ROUND_TRIPS, TUnit::UNIT);
  kudu_remote_tokens_ = ADD_COUNTER(runtime_profile(), KUDU_REMOTE_TOKENS, TUnit::UNIT);
  kudu_client_time_ = ADD_TIMER(runtime_profile(), KUDU_CLIENT_TIME);

  // 从表描述符中获取到该扫描结点的元组描述符
  DCHECK(state->desc_tbl().GetTupleDescriptor(tuple_id_) != NULL);
  tuple_desc_ = state->desc_tbl().GetTupleDescriptor(tuple_id_);

  // 从TScanRangeParams初始化要处理的扫描令牌列表。
  DCHECK(scan_range_params_ != NULL);
  int num_remote_tokens = 0;
  for (const TScanRangeParams& params: *scan_range_params_) {
    if (params.__isset.is_remote && params.is_remote) ++num_remote_tokens;
    scan_tokens_.push_back(params.scan_range.kudu_scan_token);
  }
  COUNTER_SET(kudu_remote_tokens_, num_remote_tokens);

  return Status::OK();
}

然后是Open函数,其负责了结点对象准备完毕之后的启动工作,代码如下:

Status KuduScanNodeBase::Open(RuntimeState* state) {
  // 与Prepare函数一样,Open函数也会先调用基类的Open函数。
  // ExecNode::Open()中会开启所有表达式求值器,
  // ScanNode::Open()中会开启所有runtime filter的表达式求值器。
  RETURN_IF_ERROR(ScanNode::Open(state));
  // 检查查询是否被取消了,若是则直接返回。
  RETURN_IF_CANCELLED(state);
  // ExecNode::QueryMaintenance()会清理内存池,并检查查询状态是否正常,该函数应当定期调用。
  RETURN_IF_ERROR(QueryMaintenance(state));
  // 为总计时器开始计时,SCOPED_TIMER是范围计时器,创建时开始计时,退出作用域时停止计时并将计时累加到传入参数中。
  SCOPED_TIMER(runtime_profile_->total_time_counter());

  // 从元组描述符中拿到表描述符并静态转换为Kudu表描述符。
  const KuduTableDescriptor* table_desc =
      static_cast<const KuduTableDescriptor*>(tuple_desc_->table_desc());

  // 从Kudu表描述符中拿到该表所在的Kudu地址,并以此创建Kudu客户端。
  RETURN_IF_ERROR(ExecEnv::GetInstance()->GetKuduClient(
      table_desc->kudu_master_addresses(), &client_));

  // 设置最新观测到的Kudu时间戳。
  uint64_t latest_ts = static_cast<uint64_t>(
      max<int64_t>(0, state->query_ctx().session.kudu_latest_observed_ts));
  VLOG_RPC << "Latest observed Kudu timestamp: " << latest_ts;
  if (latest_ts > 0) client_->SetLatestObservedTimestamp(latest_ts);

  // 调用Kudu API开启kudu表。
  KUDU_RETURN_IF_ERROR(client_->OpenTable(table_desc->table_name(), &table_),
      "Unable to open Kudu table");

  runtime_profile_->AddInfoString("Table Name", table_desc->fully_qualified_name());
  // 如果有runtime filters可用,则先等待所有runtime filters到达。
  if (filter_ctxs_.size() > 0) WaitForRuntimeFilters();
  return Status::OK();
}

如上文所述,KuduScanNodeBase只是一个抽象了KuduScanNodeKuduScanNodeMT共通逻辑的抽象类,本身无法实例化,主要的作用就是减少重复代码,更为核心的执行逻辑GetNext需要KuduScanNodeKuduScanNodeMT自行实现,我们接下来看KuduScanNodeMT

多实例版本的Kudu扫描结点KuduScanNodeMT

KuduScanNodeBase有两个派生类,分别为KuduScanNodeKuduScanNodeMT,其中KuduScanNode是更为常用的版本,其内部实现了多线程的扫描逻辑,而KuduScanNodeMT则是为Impala的MT_DOP准备的版本。Impala为了更加充分地利用CPU和内存资源提升查询并发度而提供了MT_DOP这个Query Option,其允许用户手动指定一个并发度,Impala会在每个执行节点为每个Fragment创建指定个数的实例(实例数量还受别的因素制约,如ScanRange数量、Fragment类型),Impala3.4对MT_DOP的支持还比较有限,只有某些ScanNode进行了支持。

通过MT_DOP指定并发度后,Kudu扫描将使用KuduScanNodeMT,每个相关实例包含一个KuduScanNodeMT来实现指定并发度,所以KuduScanNodeMT本身是单线程的工作模型,代码也比较简单,其定义如下:

class KuduScanNodeMt : public KuduScanNodeBase {
 public:
  KuduScanNodeMt(ObjectPool* pool, const ScanPlanNode& pnode, const DescriptorTbl& descs);
  ~KuduScanNodeMt();

  virtual Status Open(RuntimeState* state) override;
  // KuduScanNodeMt的核心函数,实现了通过KuduScanner处理token并返回行批RowBatch的逻辑。
  virtual Status GetNext(RuntimeState* state, RowBatch* row_batch, bool* eos) override;
  virtual void Close(RuntimeState* state) override;
  
  // getExecutionModel函数覆写了ExecNode::getExecutionModel(),返回本结点的执行模型,DEBUG适用。
  // 执行模型ExecutionModel是个枚举类型,反应了本结点的多线程模型,
  // TASK_BASED指的是基于任务的多线程,另外还有HdfsScanNodeMT属于此类型。
  virtual ExecutionModel getExecutionModel() const override { return TASK_BASED; }

 private:
  // 指向当前正在扫描的scan token的指针。
  const std::string* scan_token_;
  // KuduScanner的独占指针,KuduScanner实现了Kudu扫描的核心逻辑。
  std::unique_ptr<KuduScanner> scanner_;
};

然后是KuduScanNodeMT的几个关键方法:

Status KuduScanNodeMt::Open(RuntimeState* state) {
  SCOPED_TIMER(runtime_profile_->total_time_counter());
  // 首先调用基类的Open函数,完成Kudu扫描的通用开启逻辑。
  RETURN_IF_ERROR(KuduScanNodeBase::Open(state));
  // 实例化一个KuduScanner并开启,KuduScanner实现了Kudu扫描的核心逻辑。
  scanner_.reset(new KuduScanner(this, runtime_state_));
  RETURN_IF_ERROR(scanner_->Open());
  return Status::OK();
}

Status KuduScanNodeMt::GetNext(RuntimeState* state, RowBatch* row_batch, bool* eos) {
  SCOPED_TIMER(runtime_profile_->total_time_counter());
  DCHECK(row_batch != NULL);
  // ExecDebugAction用来执行调试行为,可以在查询中引入人为设定的问题条件,用于内部调试和故障排除。
  // 具体参见QueryOptions DEBUG_ACTION的说明。
  RETURN_IF_ERROR(ExecDebugAction(TExecNodePhase::GETNEXT, state));
  // 若查询被取消则直接返回。
  RETURN_IF_CANCELLED(state);
  RETURN_IF_ERROR(QueryMaintenance(state));
  // eos标识了当前扫描结点是否扫描完成,当数据扫描完成或达到limit限制时,该值会被置为true。
  *eos = false;

  // 首先判断当前是否有正在处理的scan token,scan_token_为nullptr则进入循环。
  bool scan_token_eos = scan_token_ == nullptr;
  while (scan_token_eos) {
    // 尝试获取下一个scan token。
    scan_token_ = GetNextScanToken();
    // 若获取到nullptr,说明本结点的scan token已经全部处理完毕。
    if (scan_token_ == nullptr) {
      // 扫描结束,停止所有定期更新的计数器、关闭scanner并设置eos为true然后返回。
      runtime_profile_->StopPeriodicCounters();
      scanner_->Close();
      scanner_.reset();
      *eos = true;
      return Status::OK();
    }
    // 获取到下个scan token,调用KuduScanner::OpenNextScanToken进行开启,
    // 如果该scan token没有需要扫描的行,则scan_token_eos会被置为true。
    RETURN_IF_ERROR(scanner_->OpenNextScanToken(*scan_token_, &scan_token_eos));
  }

  // scanner_eos标识了当前KuduScanner正在处理的scan token是否完成。
  bool scanner_eos = false;
  // 调用KuduScanner::GetNext(),获取下一个RowBatch。
  RETURN_IF_ERROR(scanner_->GetNext(row_batch, &scanner_eos));
  // 如果scanner_eos为true,说明当前scan token扫描完成,可以为相关计数器+1并设置scan_token_为空指针。
  if (scanner_eos) {
    scan_ranges_complete_counter_->Add(1);
    scan_token_ = nullptr;
  }
  // 让KuduScanner向Kudu服务发送Ping以保持活动状态。
  scanner_->KeepKuduScannerAlive();

  // ExecNode::CheckLimitAndTruncateRowBatchIfNeeded函数会检查该结点是达到了limit限制,
  // 若达到限制则设置eos为true并截断RowBatch中超出limit的多余行,此外每次调用都还会更新相关计数器。
  if (CheckLimitAndTruncateRowBatchIfNeeded(row_batch, eos)) {
    // CheckLimitAndTruncateRowBatchIfNeeded返回true说明扫描达到了limit限制,
    // 扫描结束,停止所有定期更新的计数器、关闭scanner。
    scan_token_ = nullptr;
    runtime_profile_->StopPeriodicCounters();
    scanner_->Close();
    scanner_.reset();
  }
  // rows_returned计数器记录了当前节点目前返回的行数,将其更新到profile的计数器中。
  COUNTER_SET(rows_returned_counter_, rows_returned());

  return Status::OK();
}

void KuduScanNodeMt::Close(RuntimeState* state) {
  if (is_closed()) return;
  SCOPED_TIMER(runtime_profile_->total_time_counter());
  // 首先关闭KuduScanner,然后调用基类的Close函数,
  // KuduScanNodeBase并没有覆写Close函数,实际调用的是ScanNode::Close(),
  if (scanner_.get() != nullptr) scanner_->Close();
  scanner_.reset();
  KuduScanNodeBase::Close(state);
}

至此,KuduScanNodeMT我们就分析完了,由于其是单线程工作模型,可以发现其逻辑还是比较简单的,而关键的扫描逻辑都被KuduScanner实现了,后续文章我们继续分析多线程工作的KuduScanNodeKuduScanner

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

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

相关文章

在vscode中配置git bash终端

将以下配置添加到vscode中的settings.json中 "terminal.integrated.profiles.windows": {"PowerShell": {"source": "PowerShell","icon": "terminal-powershell"},"Command Prompt": {"path"…

【C++】类和对象(中篇)----->六大默认成员函数

目录 一、类的6个默认成员函数 二、构造函数 1、概念 2、特性 三、析构函数 1、概念 2、特性 四、拷贝构造函数 1、概念 2、特征 五、赋值运算符重载 1、运算符重载 2、值运算符重载 2.1 赋值运算符重载格式 2.2 赋值运算符只能重载成类的成员函数不能重载成全局函数 2.3…

使用postman发请求报错Error: connect ECONNREFUSED 127.0.0.1:33210

原因&#xff1a;代理服务器问题 解决&#xff1a; 两种方案任选其一 1.电脑网络设置&#xff0c;关闭代理服务器 2.postman 的设置proxy 取消勾选

Mac中VSCode配置vue项目环境

一、下载VSCode 进入VSCode官网&#xff0c;下载Mac版安装包 设置中文: vscode导航栏view -> Command Palette -> 输入Configure Display Language -> 选择简体中文 -> 重启 二、下载node.js 下载地址&#xff1a;node.js官网&#xff0c;建议下载长期维护版本…

交叉编译paho带SSL

1.新建文件夹 /home/yiweijiao/woke_lab/paho 2.解压paho.mqtt.c到/home/yiweijiao/woke_lab/paho/paho.mqtt.c 3.新建文件夹/home/yiweijiao/woke_lab/paho/openssl_lib 将已经交叉编译好的openssl复制到这里 4.cd /home/yiweijiao/woke_lab/paho/paho.mqtt.c 新建文件夹…

hive和datax數據採集數量對不上

hive和datax數據採集數量對不上 對數據的時候發現有些對不上&#xff0c;在hive中 staff_id DF67B3FC-02DD-4142-807A-DF4A75A4A22E’的數據只有1033 而在mysql中發現staff_id DF67B3FC-02DD-4142-807A-DF4A75A4A22E’的數據有4783條記錄&#xff08;昨天的記錄是4781&#…

控制哈威比例多路阀放大板

控制各种不带电气位移反馈的HAWE哈威多路比例阀PSV系列、PSVF系列等比例电磁铁。 比例多路阀用于控制液压执行元件的运动方向的运动方向和运动速度(无级地,并且不取决于负载).为此,可使多个执行元件同时并相互独立地以不同的速度和压力工作,直到所有部分流量的总和达到泵的流量…

leetcode 404. 左叶子之和

2023.7.6 这道题关键就是要判断某个节点是否为左叶子节点&#xff0c;但是必须要靠他的父节点来判断&#xff0c;逻辑就是其父节点的左孩子不为空 并且 父节点的左孩子的左孩子和右孩子都为空&#xff0c;此时该节点就是左叶子了。 下面用两种迭代法求解&#xff1a; 队列&…

聚观早报|比亚迪在巴西建工厂;国产Model系列贡献约半数交付量

今日要闻&#xff1a;比亚迪在巴西建三座工厂&#xff1b;小米对华为锁屏专利发起无效宣告请求&#xff1b;国产Model系列贡献约半数交付量&#xff1b;杨澜公司回应数百万财产被冻结&#xff1b;雅虎公司计划重新上市 比亚迪在巴西建三座工厂 7 月 5 日消息&#xff0c;据比亚…

C语言学习(三十三)---动态内存(二)

在上一节的内容中&#xff0c;我们初步学习了有关动态内存的有关内容&#xff0c;但是在使用上实际上还有很多的细节问题&#xff0c;今天我们将继续对该部分的内容进行学习&#xff0c;好了&#xff0c;话不多说&#xff0c;开整&#xff01;&#xff01;&#xff01; 动态内…

第一章 Android 基础--开发环境搭建

文章目录 1.Android 发展历程2.Android 开发机器配置要求3.Android Studio与SDK下载安装4.创建工程与创建模拟器5.观察App运行日志6.环境安装可能会遇到的问题7.练习题 本专栏主要在B站学习视频&#xff1a; B站Android视频链接 本视频范围&#xff1a;P1—P8 1.Android 发展历…

【机器学习核心总结】什么是KNN( K近邻算法)

什么是KNN( K近邻算法) 虽然名字中有NN&#xff0c;KNN并不是哪种神经网络&#xff0c;它全名K-Nearest-Neighbors&#xff1a;K近邻算法&#xff0c;是机器学习中常用的分类算法。 物以类聚&#xff0c;人以群分。KNN的基础思想很简单&#xff0c;要判断一个新数据的类别&…

AI最新开源:LMSYS Org开源LongChat、法律大语言模型ChatLaw、中文医疗对话模型扁鹊

一周SOTA&#xff1a;LMSYS Org开源LongChat、法律大语言模型ChatLaw、中文医疗对话模型扁鹊 文章目录 1. LMSYS Org发布LongChat&#xff0c;上下文碾压64K开源模型2. 北大团队发布法律大模型 ChatLaw3. 扁鹊&#xff1a;指令与多轮问询对话联合微调的医疗对话大模型 1. LMSY…

Linux 内核源代码情景分析(四)

系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核 Linux 设备驱动程序 Linux设备驱动开发详解 深入理解Linux虚拟内存管理 Linux 内核源代码情景分析&#xff08;一&#xff09; Linux 内核源代码情景分析&#xff08;二&#xff09; Linux 内核源代码情景分析&#xff…

开源项目推荐 【SkyEyeSystem】

大家好&#xff0c;今天向大家推荐一个开源项目——SkyEyeSystem。 这是一个基于Spring Boot的全网热点爬虫项目&#xff0c;旨在提供全面而准确的全网热搜数据。 关于项目 SkyEyeSystem通过定时任务间隔10min爬取全网热搜数据。目前包括的平台有&#xff1a; 微博热搜B站热…

Huawei Cloud EulerOS 安装 MySQL8.0

EulerOS 安装 MySQL8.0 安装MySQL配置文件 安装MySQL 当创建一个基于EulerOS的服务器时&#xff0c;MySQL是一个常见且强大的数据库管理系统选择。在此博客中&#xff0c;我将向您展示如何在EulerOS上安装MySQL 8.0。 步骤1&#xff1a;更新系统 在开始之前&#xff0c;让我…

【FATE】联邦学习 optimizer在FATE的自定义trainer中被改变

起因 使用torch的optimizer添加了2组parameter&#xff0c;传参进入FATE的trainer后&#xff0c;optimizer被改变&#xff0c;且FATE框架无提示。 代码差不多是下面这样&#xff1a; # optimizer中加入2组优化参数&#xff08;param&#xff09; optimizer torch.optim.SGD…

滑动窗口 /【模板】单调队列

day1 滑动窗口 /【模板】单调队列 题目描述 有一个长为 n n n 的序列 a a a&#xff0c;以及一个大小为 k k k 的窗口。现在这个从左边开始向右滑动&#xff0c;每次滑动一个单位&#xff0c;求出每次滑动后窗口中的最大值和最小值。 例如&#xff1a; The array is [ …

mysql redis区别

一、.redis和mysql的区别总结 &#xff08;1&#xff09;类型上 从类型上来说&#xff0c;mysql是关系型数据库&#xff0c;redis是缓存数据库 &#xff08;2&#xff09;作用上 mysql用于持久化的存储数据到硬盘&#xff0c;功能强大&#xff0c;但是速度较慢 redis用于存储使…

NSS [NSSCTF 2022 Spring Recruit]ezgame

NSS [NSSCTF 2022 Spring Recruit]ezgame 前端小游戏&#xff0c;乐。