LOADING

加载过慢请开启缓存 浏览器默认开启

Okabe's LAB

C++ 笔记(1)—— 万能引用/完美转发/引用折叠

Language 2024/3/27

std::move

std::move 的源码长这样:

template <class _Tp>
_LIBCPP_NODISCARD_EXT inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR typename remove_reference<_Tp>::type&&
move(_Tp&& __t) _NOEXCEPT {
  typedef _LIBCPP_NODEBUG typename remove_reference<_Tp>::type _Up;
  return static_cast<_Up&&>(__t);
}

它本质上是将一个引用强制转换成了右值引用

万能引用(Universal Reference)

当我们使用 T && x 或者 auto && x 这种需要类型推断的右值形式的时候,就会被编译器作为万能引用。所谓万能引用,就是既能绑定左值引用,又能绑定右值引用。(所以说,在 C++ 中,并不是说两个 & 就代表是右值引用)。

引用折叠(Reference Collapse)

  • 左值-左值 T& &:函数定义的形参类型是左值引用,传入的实参是左值引用
  • 左值-右值 T& &&:函数定义的形参类型是左值引用,传入的实参是右值引用
  • 右值-左值 T&& &:函数定义的形参类型是右值引用,传入的实参是左值引用
  • 右值-右值 T&& &&:函数定义的形参类型是右值引用,传入的实参是右值引用
    但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:

所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。

#include <iostream>
template <typename T>
void PrintInner(T &x)
{
    std::cout << "I got a lvalue: " << x << std::endl;
}

template <typename T>
void PrintInner(T &&x)
{
    std::cout << "I got a rvalue: " << x << std::endl;
}

template <typename T>
void Print(T &&x)
{
    PrintInner(x);
    PrintInner(std::move(x));
    PrintInner(std::forward<T>(x));
}

int main()
{
    Print(5);
    std::cout << "-------------------" << std::endl;

    int x = 20;
    Print(x);
    std::cout << "-------------------" << std::endl;

    auto &&x2 = 30;
    std::cout << std::boolalpha << (typeid(x2) == typeid(int &&)) << std::endl;
    std::cout << "-------------------" << std::endl;

    auto &&x3 = x;
    std::cout << std::boolalpha << (typeid(x3) == typeid(int &)) << std::endl;
}

上面这个例子最终会输出:

I got a lvalue: 5
I got a rvalue: 5
I got a rvalue: 5
-------------------
I got a lvalue: 20
I got a rvalue: 20
I got a lvalue: 20
-------------------
true
-------------------
true

观察:

  • 虽然 5 是作为右值被传入函数 Print,但是这个右值被绑定到了形参 x 上,所以 PrintInner 最终调用的是左值引用的版本。
  • std::move 毋庸置疑,本质上就是都强制转换成右值引用。
  • std::forward 为了将 5 继续当作右值转发,我们可以使用 std::forward,也就是 完美转发
  • 事实上,可以将 T && x 和 T &x 合并成 T &&,因为我们有万能引用。

常左值引用可以绑定一个右值。

const std::string && func()  {
    return std::move(std::string("ok"));
}
const std::string &ok = func(); // ok
const std::string &ok = "ok";   // ok

std::string &fail = "fail";
error: non-const lvalue reference to type 'std::string' (aka 'basic_string<char, char_traits<char>, allocator<char>>') cannot bind to a value of unrelated type 'const char[5]'
    std::string &fail = "fail";
阅读全文

CMU-DB 15-445 学习笔记(3)

2024/3/27

对应课程

Buffer Pool

Buffer Pool 需要有一个页表记录哪些页被缓存在内存中,同时需要跟踪页的使用情况(例如是否被修改?被访问过几次?)。当一个页面被使用的时候,我们应该更新它的访问记录,增加它的 pin count(类似 reference cnt,当 pin count = 0 的时候我们才可以将一个页标记为 evictable)。当一个页被驱逐的时候,如果它是脏页,我们还需要将其 Flush 到磁盘。

Latch & Lock

  • Lock: 高级层次、逻辑上的锁,比如事务锁。
  • Latch: 数据库内部数据结构的锁。

Page Directory & Page Table

  • Page Director:持久化在磁盘中,将 page id 映射到磁盘中对应页的位置。
  • Page Table:存储在内存中,将 page id 映射到 frame id。

Buffer Pool Optimization

Multiple Buffer Pools

  • 使用多个 Buffer Pool 以减少 latch 竞争。
  • 将 page id 映射到某个 Buffer Pool。

Pre-fetching

当执行某些扫描操作的时候,DBMS 可以进行预取操作,提前将未来可能读到的数据页加载到内存中。

数据库清楚自己的数据结构和语义,可以对 B+ 树上的范围查询进行预取。

Scan Sharing

当 DBMS 发现多个查询实际上是等价的时候,就可以将多个查询绑定到单个 cursor 上。查询的结果不一定要一样,还可以共享中间结果。

Buffer Pool Bypass

“Light Scan”。

顺序扫描之类的操作可能会污染 Buffer Pool,如果使用 Buffer Pool Bypass 将不会存储顺序扫描获取的页,而是保存到 worker 本地(不和其他 workers共享)。对于需要大量扫描的操作而言有效。

Bypass OS Page Cache

大多数的 DBMS 都使用 O_DIRECT 绕过 OS Page Cache,直接操作文件 I/O(这样 OS 就不会生成缓存,数据库自己来管理缓存)。

页面驱逐算法

LRU

Clock

LRU 的近似算法。

  • 访问一个页面:将该页面对应的位设置为 1;
  • 按照一定顺序(顺时针/逆时针)遍历,将位为 1 的设置为 0,将原先就是 0 的页面驱逐。

LRU-k

LRU 和 Clock 算法都存在的问题是它们只考虑了页面被访问的时间,但是没有考虑页面被访问的频率,很容易被 sequential flooding 污染。

  • 数据第一次被访问,加入到访问历史列表;
  • 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;
  • 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;
  • 缓存数据队列中被再次访问后,重新排序;
  • 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

K-distance 的概念(LRU-k 是按照 K-distance 排序的):

MYSQL 使用了一种类似的算法,第一次访问的时候进入 Old List 链表头,再次访问则从 Old List 转移到 Young List 头部。说它是一种近似是因为它没有真正记录时间戳,只是将其直接放到头部(注意 LRU-k 是会计算 k-distance 的)。

Dirty Pages

Use background writing.

阅读全文

CMU-DB 15-445 学习笔记(2)

2024/3/25

对应课程

三种数据处理模式

  • OLTP: On-Line Transaction Processing
  • OLAP: On-Line Analytical Processing
  • HTAP: Hybrid Transaction + Analytical Processing

三种存储模型

N-ary Storage Modal(NSM) 行存储

适合 OLTP,一般 Page 大小都是 Hardware Page 的整数倍。

优点:

  • 快速增删改查;
  • 适合 OLTP;
  • 可以基于 index-oriented 实现存储。

缺点:

  • 不适用于扫描表的大范围内容;
  • 如果是想获取其中一列的值,会有很多随机写,内存 locality 很差;
  • 不适合压缩,因为单个页面里面有多种 value domains,重复的值出现的概率低。

Decomposition Storage Model(DSM) 列存储

  • 适合 OLAP。
  • 一些系统可能会有一个 Buffer Pool,通常是日志结构,缓存行修改,并且定期转换为列存储。
  • Oracle 保存以行格式存储的主表,但是同步按列存储的副本。

两种存储方式:

  • 定长存储,这样做的好处就是不用额外的表示符确定某个数据(index = offset / value size)。但是对于不定长的数据,则需要通过字典压缩之类的方式,将变长内容映射到固定大小(32 bit)。
  • 用额外的列记录

我们在做 OLAP 的时候,往往并不是只看其中一列,我们可能要在多列上获取值并且计算。基于此,我们需要 PAX。

Hybrid Storage Mode(PAX)混合存储

  • PAX 即 Partition Attributes Across
  • ParuqetOrc 格式使用的就是 PAX
  • 既要获得列存储快速处理的优点,又要获得行存储的优良空间 locality。

数据库压缩

目标

  • 必须产生定长的值
  • 解压缩应尽可能被延迟,需要的时候再进行(late materialization)
  • 必须是无损压缩

压缩粒度

Block Level(Naive Compression)

  • 使用通用压缩算法,Snappy, Zstd 等
  • MySQL 使用的是通用的压缩,因为它是行存储,没法针对数据进行很好的压缩。
  • 使用通用算法的弊端在于,虽然他们都是基于字典压缩算法的变形,但是由于 MYSQL 无法知道字典数据结构,所以 MYSQL 不得不做整体解压(无法根据字典拿到某个局部值)。

我们真正想要获得的效果应该如下图所示。我们希望能够对某些局部值进行解压缩,而不用解压缩整个页。

我们可以在列式存储中实现这一点。

Column Level

RLE(Run-Length Encoding)

(value, index, number) 三元组,即(数据,索引,数量)。

Bit Packing

通常数据都不会撑满设定的数值上限,例如 32 字节,我可能最大的数值只有 4 字节不到,那么我们就可以用 4 字节存储一个页中的任一数据(而不是原先的 32 字节)。

Mostly Encoding

对于 9999… 这么大的数,可以维护一个查询表来存储,其他的数保持 Bit Packing。

Bitmap Encoding

适用于有限值域,这里 M,F 两种值占两个字节 2 * 8 = 16 bits。

Incremental Encoding

Delta Encoding

利用差值进行压,甚至可以基于 RLE 进一步压缩。

Dictionary Compression

  • 将原始值映射到一个新值,同时维护一个字典记录这种映射关系。

  • 这里查询的时候也需要将查询值进行压缩,不然的话每一行查询都需要将压缩值进行解压,这样就失去了压缩带来的好处。

  • 有时候我们希望对压缩值进行范围查询,所以我们也会希望映射值也能满足顺序。

  • 如果满足字典顺序没有发生变化,那么 DBMS 就可以把 LIKE ‘And%’ 重写成 BETWEEN min AND max 这样的语句(因为满足顺序)。

    SELECT name FROM users
    WHERE name LIKE 'And%';
    
  • 对于 DISTINCT 这样的语句,甚至可以直接在字典上查询(DISTINCT -> 无重复值 -> 字典无重复值)。

    SELECT DISTINCT name
        FROM users
    WHERE name LIKE 'And%';
    

阅读全文

CMU-DB 15-445 学习笔记(1)

2024/3/24

对应课程

聚合操作

  • AVG(col)
  • MIN(col)
  • MAX(col)
  • SUM(col)

以上都是 SQL 中的聚合函数。

  • 支持 DICTINCT

    SELECT COUNT(DISTINCT login) 
    FROM student WHERE login LIKE '%@cs'
    
  • 将聚合结果与非聚合的列混合在标准 SQL 下是未定义行为

    应该使用 GROUP BY

    SELECT AVG(s.gpa), e.cid, s.name
    FROM enrolled AS e, student AS s
    WHERE e.sid = s.sid
    GROUP BY e.cid
    
  • 使用 HAVING 语句来筛选聚合结果

    SELECT AVG(s.gpa) AS avg_gpa, e.cid
    FROM enrolled AS e, student AS s
    WHERE e.sid = s.sid
    AND avg_gpa > 3.9
    GROUP BY e.cid
    

    以上方式是错误的(AND avg_gpa > 3.9),因为平均值只有在聚合之后才知道。

    SELECT AVG(s.gpa) AS avg_gpa, e.cid
    FROM enrolled AS e, student AS s
    WHERE e.sid = s.sid
    GROUP BY e.cid
    HAVING AVG(s.gpa) > 3.9;
    

    SQL 是声明式的,AVG 函数不会执行两遍。

字符串

MySQL 默认情况下不区分字符串大小写,使用 CAST 将其转成二进制字符串:

SELECT * FROM student WHERE CAST(name AS BINARY) = 'TupaC';
  • 模糊查询

    • % 类似正则表达式的 *
    • _ 类似正则表达式的 .

窗口函数

窗口函数是一种类似聚合函数的函数,但它们不会将数据集分组为单个值,而是在给定窗口范围内对每一行进行计算。

ROW_NUMBER 是窗口函数,OVER 括号里面声明了如何切片,如果为空则代表不切分表。

例如,如果我希望计算每门课第二高成绩的学生:

SELECT * FROM (
 SELECT *, RANK() OVER (PARTITION BY cid
 ORDER BY grade ASC) AS rank
 FROM enrolled) AS ranking
WHERE ranking.rank = 2

嵌套查询

SELECT * FROM student AS s 
WHERE s.sid IN (
    SELECT sid FROM enrolled where cid = '15-445'
)

LATERAL JOIN

LATERAL JOIN 允许嵌套查询中的表能够引用另一张表的属性

以下的查询会报错,因为 t2t1 是并行执行的,t2 无法知道 t1 的存在。

SELECT * from (SELECT 1 AS x) AS t1, (SELECT t1.x + 1 AS y) as t2; 

使用 LATERAL 关键字可以实现这一点:

SELECT * from (SELECT 1 AS x) AS t1, LATERAL (SELECT t1.x + 1 AS y) as t2; 

CTE(Common Table Expression)

类似于临时表,或者说是 MYSQL Derived Table(嵌套查询的子查询生成的表叫做派生表) 的增强版。

例子(找到 sid 最大的学生):

WITH cteSource (maxId) AS (
 SELECT MAX(sid) FROM enrolled
)
SELECT name FROM student, cteSource
WHERE student.sid = cteSource.maxId

面向磁盘的 DBMS

  • Buffer Pool 作为中转,缓存数据库页面,页面目录标识数据页在磁盘的位置。
  • 获取页面 2 的时候,先加载目录到内存,然后通过操作系统调用加载数据页到内存,返回给执行引擎一个内存地址。
  • Buffer Pool 还需要负责写回脏页,保持一致性。

MMAP 的问题

虽然我们可以使用 mmap 调用将文件映射到内存(操作系统会帮我们进行页面换入和换出),但是操作系统执行页面驱逐的逻辑可能和数据库的驱逐逻辑冲突(换出数据库不想被换出的页面)。

  • 无法满足事务安全,操作系统可能随时 flash 脏页。假如事务要求将多个页按照一定顺序写入,无法保证 page A 在 page B 写入之后再被写入。
  • DBMS 无法知道某个页面在不在内存中,操作系统会触发 page fault 阻塞线程。
  • 难以处理错误,任何访问都可能触发一个 SIGBUS 中断,DBMS 必须处理它,即使正在执行一些 critical section。
  • 与操作系统产生数据竞争。

Storage Manager

  • 负责维护数据库文件
  • 调度读写以获取更好的时空 locality。
  • 以页的形式组织数据库文件
  • 记录元数据(可用页面)

Page

  • 固定大小的数据块

  • 数据 + 元数据、日志

  • Oracle 中 Page 是 Self-contained(包含了用于理解这个页的元数据,比如所属 Table 的信息,这种冗余可以避免数据库文件损坏导致无法读取数据)。

  • 每个页都有一个唯一标识符(由此映射到物理内存位置)。

  • Hardware Page(4KB)是能够保证原子性写入的数据块最大大小。数据库页默认大小通常大于 4KB(非原子性)。

Heap File

页的无序集合,每个数据元祖随机存储(关系型数据库并不要求按照顺序排列)。对于单个文件而言,找到对应的页很容易,但是对于多个文件而言则比较棘手,需要存储额外的元数据记录哪一个页存在哪一个文件里面,也就是 Page Directory。Page Directory 需要和数据页保持同步,同时需要维护和记录空闲页的数量和位置。

  • 对 Page Directory 的更新需要立即写入磁盘。
  • 像 BLOB 这种字段可能是分另一个文件存储。

存储诸如页大小,校验和之类的元数据。

Tuple-Oriented Storage

File -> Page -> Tuple

  • 使用 Slotted Pages,Slot Array 从头部往尾部增长,Tuple 从尾部往头部增长。
  • 使用 Record ID 唯一标识一个 Page(Postgresql 中的 ctid)。

存在的问题:

  • 一个 page 中可能有大量未被使用的空间(内存碎片);
  • 更新一个 tuple 需要读写整个 page;
  • 随机磁盘读写,极端情况是修改 N 个 tuple,每个都属于不同的 page;
  • 由于需要写回更新,在一些云存储系统可能不支持这么做,例如 HDFS

Log-Structured Storage

  • 追加对 tuple 的 PUT/DELETE 操作
  • 定期合并 page(compaction)

存在的问题:

  • 写放大:Compaction 非常昂贵,从磁盘读回内存再写回磁盘。
  • 读放大

NULL

  • 通过一个 bitmap 标识属性是否为空,这通常不仅仅占一个 bit,因为还要解决内存问题(填充多个 0)。
  • 标记为非 NULL 就不需要存 bitmap。

Large Values & External Value Storage

阅读全文

Kubeflow 笔记(1)—— 安装与 Hello World

Cloud Native 2024/3/23

Installation

步骤1,使用 kind 安装本地 k8s 集群

Kind 即 “Kubernetes in Docker”,在 docker 内部使用 kubeadm 来安装集群。

参考:Kubernetes教程(十五)—使用 kind 在本地快速部署一个 k8s集群

注意一下 KUBECONFIG 环境变量是不是正确,我之前用 kubeadm 在本地安装过 k8s,所以 KUBECONFIG 环境变量不对。kind 的配置文件应该在 ~/.kube/config(我这里是 root 用户)。

步骤2,安装 kubeflow

参考官方:https://github.com/kubeflow/manifests

阅读全文

Spark 笔记(1)——安装与基础使用

Big Data 2024/3/12

正文

该笔记将记录在 Ubuntu-20.04 上安装并使用 Spark 的历程。

环境

  • Ubuntu 20.04
  • Spring Boot 3.2.3
  • Spark 3.2.0
  • JDK 17

安装参考:在 Ubuntu 20.04 上安装 Apache Spark 教程

启动流程

  • 启动 master

    start-master.sh
    

    这个指令会在当前主机启动一个 master 节点,可以在 localhost:8080 访问到控制面板。

  • 启动 worker

    复制命令行中生成的 master 节点的 URL,启动 worker

    start-worker.sh spark://xxx
    

    启动成功后长这样:

配置 Java Spark 环境

遇到的问题:

注意:在我的环境中,worker 占用了 8081 端口,而我的 Spring 服务启动在 8081 端口,所以产生问题。按照上面的链接能够解决剩余的版本冲突问题。

简单的例子

  • 经典 word count:

    JavaRDD<String> file = session.read().textFile(filePath).cache().toJavaRDD();
    JavaRDD<String> words = file.flatMap((FlatMapFunction<String, String>) s -> Arrays.asList(s.split(" ")).iterator());
    JavaPairRDD<String, Integer> wordToCountMap = words.mapToPair((PairFunction<String, String, Integer>) s -> new Tuple2<>(s, 1));
    JavaPairRDD<String, Integer> wordCounts = wordToCountMap.reduceByKey((Function2<Integer, Integer, Integer>) Integer::sum);
    wordCounts.saveAsTextFile("./word_count");
    

参考:

  • spark中map()和flatmap()的区别

  • 生成 parquet:

    SparkSession session = sparkService.getSparkSession();
    Properties properties = new Properties();
    properties.setProperty("user", "root");
    properties.setProperty("password", "123456");
    Dataset<Row> dataset = session.read().jdbc("jdbc:mysql://localhost:3306/spark", "person", properties);
    // 输出表格
    dataset.show();
    dataset.coalesce(1).write().mode(SaveMode.Overwrite).option("header", true).parquet("./test.parquet");
    

Pandas 也支持生成 parquet:pandas.DataFrame.to_parquet

Spark 中的三种数据结构

  • Spark RDD

    RDD 是一种弹性分布式数据集,是一种只读分区数据。它是 Spark 的基础数据结构,具有内存计算能力、数据容错性以及数据不可修改特性。

  • Spark Dataframe

    Dataframe 也是一种不可修改的分布式数据集合,它可以按列查询数据,类似于关系数据库里面的表结构。可以对数据指定数据模式(schema)。

  • Spark Dataset

    DatasetDataFrame 的扩展,它提供了类型安全,面向对象的编程接口。也就是说 DataFrameDataset 的一种特殊形式。

参考:

阅读全文

纪念一次成功的开源贡献

Life 2024/2/8

https://github.com/qdrant/qdrant/pull/3549

没想到还能赚美刀~

阅读全文

Rust 笔记(1)—— 关于 tokio spawn

Language 2024/2/5

正文

假如说有这样一个异步函数 async_func,如果在同步函数中不用 await 调用它,它是不会被执行的,除非使用 tokio::task::spawn 函数开启一个异步任务。该函数接受一个 Future 参数,会返回一个 tokio::task::JoinHandle<T>,其中范型 T 是异步任务的返回值。

JoinHandle类型可以通过await来等待异步任务的完成,也可以通过abort()来中断异步任务,异步任务被中断后返回JoinError类型。

举个例子:

async fn async_func() -> Result<(), Box<dyn std::error::Error>> {
    ...
    Ok(())
}

fn sync_func() {
    // handle 的类型为 JoinHandle<Result<(), Box<dyn std::error::Error>>>
    let handle = spawn(async_func());
    // 中断
    handle.abort();
    // 等待完成(需要在异步函数中)
    // handle.await
}

可以使用 tokio::join! 宏来等待多个 JoinHandle 执行完成。

这里的 spawn 如果不等待的话,就会自己独立执行,所以会要求引用数据的生命周期是 'static,也就是活到程序结束(因为异步任务也可能执行到程序结束)。这就会带来一些编码上的困难,例如下面的例子:

struct Manager;
impl Manager {
    async fn do_something_async(&self) {}

    fn start(&self) {
        spawn(self.do_something_async());
    }
}

上述的代码会报错:

error[E0521]: borrowed data escapes outside of method
  --> src/test.rs:22:15
   |
21 |     fn start(&self) {
   |              -----
   |              |
   |              `self` is a reference that is only valid in the method body
   |              let's call the lifetime of this reference `'1`
22 |         spawn(self.do_something_async());
   |               ^^^^^^^^^^^^^^^^^^^^^^^^^
   |               |
   |               `self` escapes the method body here
   |               argument requires that `'1` must outlive `'static`

spawn 要求 &self 的生命周期必须是 'static,我查阅了一些资料,从 Rust 论坛中看到了相关问题。解决方案是使用 ArcArc/Mutex(如果有可变引用)。

struct Manager {
    inner: Arc<ManagerInner>,
}
struct ManagerInner;
impl ManagerInner {
    async fn do_something_async(&self) {}
}

impl Manager {
    fn start(&self) {
        let inner_cloned = self.inner.clone();
        spawn(async move {
            inner_cloned.do_something_async().await;
        });
    }
}

通过这一层包裹,就不报错了。Arc 保证了引用可以活到程序结束(只要还有引用,就不会被回收),即使 Manager 被回收,只要异步任务还在进行,ManagerInner 还是存在一份。这边必须使用 move 告诉编译器移动 inner_cloned 而不是捕获它的引用(捕获引用是默认行为)。

参考

阅读全文

Kubernetes 笔记(2)—— 记一次 Kubernetes 小练习

2023/4/6

Lab 1

该 lab 基于 Minikube,用于练习 K8s 最基本的 Api Object,如 ConfigMap,Secret,Pod,Service 等等。

使用 Secret 指定 Mysql 密码

kubectl apply -f lab1/mysql_secret.yml

该 YAML 文件中制定了数据库的密码。

stringData:
  db_password: '123456'

在创建 Mysql 对应的 Pod 的时候,可以使用这个 Secret 来指定 Mysql 的密码。
如下所示,使用环境变量指定 Mysql root 用户的密码,这个密码源自 Secret 中定义的 db_password

env:
    - name: MYSQL_ROOT_PASSWORD
    valueFrom:
      secretKeyRef:
        name: mysql-secret
        key: db_password

创建 Mysql 对应的 Pod:

kubectl apply -f lab1/mysql_pod.yml

验证 Mysql 正确指定了密码:

kubectl exec -it mysql -- mysql -uroot -p123456

有关 Secret 的更多声明方式:https://github.com/omerbsezer/Fast-Kubernetes/blob/main/K8s-Secret.md

创建 Mysql 对应的 Service

kubectl apply -f lab1/mysql_service.yml

该 YAML 文件中指定了选中的 Pod 以及端口。使用 NodePort 的方式会在每个 Node 的 ip 上暴露一个端口,
来访问对应的 Service 服务。
同时,可以随机暴露出一个端口外部访问的端口(默认值:30000-32767)。

一般而言,对于数据库这种服务,应该使用 ClusterIp,只在集群内部使用,这边为了测试服务连接,故暴露给外部。

spec:
  type: NodePort
  selector:
    app: db
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306

这里的 selector 对应了 lab1/mysql_pod.yml 中指定的 labels:

labels:
  app: db

创建该 Service:

kubectl apply -f lab1/mysql_service.yml

查看当前的 Service:

$ kubectl get svc
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
kubernetes      ClusterIP   10.96.0.1        <none>        443/TCP          25m
mysql-service   NodePort    10.108.143.219   <none>        3306:30160/TCP   14s

其中 3306 是集群内的端口,pod 可以通过这个端口进行访问:mysql-service:3306(通过 service 名直接访问该服务)。30160 是对外可见的端口,在集群外部可以通过 nodeIp:30160 访问。

可以看到集群暴露了 30160 的端口给外部使用,不过我们使用的是 Minikube,所有的 k8s 组件都跑在 Docker 容器里,
所以我们必须要用 tunnel 才能真正访问该 Service:

minikube service --url mysql-service

该指令会开启一个 tunnel,并且提供一个可访问的 url,相当于让我们能够去访问 30160 端口,并最终访问到内部的 Service。

$ minikube service --url mysql-service
http://127.0.0.1:51634
❗  Because you are using a Docker driver on windows, the terminal needs to be open to run it.

可以用一些数据库工具来验证是否能够连接:

使用 ConfigMap 配置数据库信息

ConfigMap 和 Secret 基本一致,只不过后者用于存储密文信息。但是注意,单纯使用 Secret 仍然存在风险,
因为其使用的 base64 并不能保证安全性,应该配合 k8s 提供的 RBAC 机制使用。

这边指定了使用的 mysql-server。

data:
  db_server: "mysql-service"

添加该 ConfigMap:

kubectl apply -f lab1/mysql_configmap.yml

我们启动一个测试 Pod 查看效果:

kubectl apply -f lab1/test_pod.yml

在该 Pod 内部查看环境变量是否正确:

kubectl exec -it test -- bin/sh
echo $MYSQL_SERVER
echo $MYSQL_ROOT_PASSWORD

应该得到如下的输出:

$ kubectl exec -it test -- bin/sh
/ # echo $MYSQL_SERVER
mysql-service
/ # echo $MYSQL_ROOT_PASSWORD
123456

注意,k8s 里的 Service 是 ping 不通的,以下摘自:https://kuboard.cn/learning/faq/ping-service.html

因为 Kubernetes 只是为 Service 生成了一个虚拟 IP 地址,实现的方式有:

  • User space 代理模式
  • Iptables 代理模式
  • IPVS 代理模式

不管是哪种代理模式,Kubernetes Service 的 IP 背后都没有任何实体可以响应「ICMP」,全称为 Internet 控制报文协议(Internet Control Message Protocol)。

我们可以在 test pod 里面安装 telnet 指令,查看连接情况:

apk update
apk add busybox-extras

注意,可以直接用 Service 的名字,依靠 DNS 访问服务,但是这里只是 hostname,还需要指定端口号

telnet "$MYSQL_SERVER:3306"

修改 ConfigMap 之后更新 Pod

我们可以通过以下方式对 Api Object 的配置进行修改,以之前的 ConfigMap 为例:

kubectl edit configmap mysql-config

这会启动一个文本编辑器让你进行修改。这里我们把 db_server 修改成了 dummy
我们再次进入 test Pod,看看环境变量是否改变:

kubectl exec -it test -- bin/sh
echo $MYSQL_SERVER

输出依旧是之前的 mysql-service,也就是说修改 ConfigMap 不会导致引用它的 Pod 的自动更新。我们需要一些其他手段让 Pod 在 ConfigMap 更新的时候也进行更新。

使用 Deployment

Deployment 可以支持滚动升级,当我们的 ConfigMap 修改的时候,可以认为是一次版本变动,我们可以通过 Deployment 更新对应的 Pod。

我们先删除之前的 test Pod,

kubectl delete -f lab1/test_pod.yml

接下来我们要用 Deployment 来管理这个 Pod。Deployment 的 Template 对应了 Pod 的 Spec。

创建对应的 Deployment:

kubectl apply -f lab1/test_deployment.yml

查看生成的 Pod(s):

$ kubectl get pods
NAME                   READY   STATUS    RESTARTS   AGE
mysql                  1/1     Running   0          102m
test-7775f744b-c7sgs   1/1     Running   0          25s

进入该 Pod 查看环境变量:

kubectl exec -it test-7775f744b-c7sgs -- bin/sh
echo $MYSQL_SERVER

输出为 dummy,现在我们把 configmap 修改为之前的版本。

然后,我们使用如下方法( https://www.qttc.net/504-how-update-latest-configmap-in-pods.html )更新 Pod:

kubectl rollout restart deploy/test

我们可以在另外两个终端,通过:

kubectl get rs -w

以及

kubectl get pods -w

查看发生的变化:

$ kubectl get rs -w
NAME             DESIRED   CURRENT   READY   AGE
test-7775f744b   1         1         1       9m32s
test-595fb97b87   1         0         0       0s
test-595fb97b87   1         0         0       0s
test-595fb97b87   1         1         0       0s
test-595fb97b87   1         1         1       5s
test-7775f744b    0         1         1       9m58s
test-7775f744b    0         1         1       9m58s
test-7775f744b    0         0         0       9m58s
$ kubectl get pods -w
NAME                   READY   STATUS    RESTARTS   AGE  
mysql                  1/1     Running   0          112m 
test-7775f744b-c7sgs   1/1     Running   0          9m46s
test-595fb97b87-mqb2m   0/1     Pending   0          0s
test-595fb97b87-mqb2m   0/1     Pending   0          0s
test-595fb97b87-mqb2m   0/1     ContainerCreating   0          0s
test-595fb97b87-mqb2m   1/1     Running             0          5s
test-7775f744b-c7sgs    1/1     Terminating         0          9m58s
test-7775f744b-c7sgs    0/1     Terminating         0          10m
test-7775f744b-c7sgs    0/1     Terminating         0          10m
test-7775f744b-c7sgs    0/1     Terminating         0          10m

可以通过如下指令查看 Deployment 的历史版本:

kubectl rollout history deploy/test

我们再次进入 test Pod,查看环境变量:

kubectl exec -it test-595fb97b87-mqb2m -- bin/sh
echo $MYSQL_SERVER

更新成功:mysql-service。具体的,还可以根据 https://github.com/omerbsezer/Fast-Kubernetes/blob/main/K8s-Rollout-Rollback.md 中提到的两种策略,指定更新策略。
Recreate 是全部删除,然后新建(显然服务会有一段时间 Down),而 RollingUpdate 也就是滚动升级,两个版本的 Pod 将同时存在,慢慢将所有 Pod 变为最新版本(关闭一部分旧的,开启一部分新的)。

使用 Kustomize

项目网址:https://github.com/kubernetes-sigs/kustomize

kustomize lets you customize raw, template-free YAML files for multiple purposes, leaving the original YAML untouched and usable as is.

推荐阅读:

我们先删除之前测试的残留:

kubectl delete -f lab1/mysql_configmap.yml
kubectl delete -f lab1/test_deployment.yml

创建如下的 Kustomization.yml:

resources:
  - test_deployment.yml
configMapGenerator:
  - name: mysql-config
    literals:
      - db_server=mysql-service

查看对应的生成结果(只是打印,没有创建):

kubectl kustomize lab1/base
kubectl kustomize lab1/stagging

应用到 k8s:

kubectl apply -k lab1/base

查看生成的 ConfigMap:

$ kubectl get configmap
NAME                      DATA   AGE
kube-root-ca.crt          1      164m
mysql-config-5bhm7k67gb   1      38s

进入 Pod,查看环境变量:

kubectl exec -it test-6fc4d8f9cc-f6zwr -- bin/sh
echo $MYSQL_SERVER

结果为 mysql-service,和预期一致。

接下来修改为 dummy

kubectl apply -k lab1/stagging

可以发现自动创建了新的 Pod:

$ kubectl get pods
NAME                    READY   STATUS        RESTARTS   AGE
mysql                   1/1     Running       0          166m
test-6fc4d8f9cc-f6zwr   1/1     Terminating   0          2m13s
test-85d74d7669-d9ft8   1/1     Running       0          26s

进入 Pod,查看环境变量:

kubectl exec -it test-85d74d7669-d9ft8 -- bin/sh
echo $MYSQL_SERVER

输出为 dummy,成功!

注意,原来的 configmap 还是存在的(这种方式比较好,删除总是一个比较危险的行为)。

$ kubectl get configmap
NAME                      DATA   AGE
kube-root-ca.crt          1      174m
mysql-config-2h6ddfhh59   1      4m48s
mysql-config-5bhm7k67gb   1      6m35s
阅读全文

Kubernetes 笔记(1)—— Informer 与 Controller

2023/4/6

Informer

组成部分

  • Controller:Informer 的实施载体,可以创建 reflector 及控制 processLoop。processLoop 将DeltaFIFO 队列中的数据 pop 出,首先调用Indexer进行缓存并建立索引,然后分发给 processor 进行处理。

  • Reflector:Informer 并没有直接访问 api-server,而是通过一个叫 Reflector 的对象进行 api-server 的访问。Reflector 通过 ListAndWatch 监控指定的 kubernetes 资源,当资源发生变化的时候,例如发生了 Added 资源添加等事件,会将其资源对象存放在本地缓存 DeltaFIFO 中。

  • DeltaFIFO:是一个先进先出的缓存队列,用来存储 Watch API 返回的各种事件,如Added、Updated、Deleted。

  • Indexer:Indexer 使用一个线程安全的数据存储来存储对象和它们的键值。需要注意的是,Indexer 中的数据与 etcd中 的数据是完全一致的,这样 client-go 需要数据时,无须每次都从 api-server 获取,从而减少了请求过多造成对 api-server 的压力。一句话总结:Indexer 是用于存储+快速查找资源。

  • Processor:记录了所有的回调函数(即 ResourceEventHandler)的实例,并负责触发回调函数

使用 client-go 自定义 controller

controller 可以使用 informer 来绑定事件的回调函数,以实现对对象的控制。比如对于 replicaset 而言,监听三种事件,判断 replicaset 实际的 replicas 数量是否符合预期,若不符合预期,则进行相应的扩缩容。

最佳实践

源自:https://raw.githubusercontent.com/HanFa/learn-k8s/master/lesson3/lesson3_slides.pdf

阅读全文
1 2
avatar
Zihong Lin

What I can’t create, I don’t understand.