Ulaanbuh
I will always cherish my visit in Ulaanbuh desert.
My favourite photo.
The galaxy.
加载过慢请开启缓存 浏览器默认开启
I will always cherish my visit in Ulaanbuh desert.
My favourite photo.
The galaxy.
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);
}
它本质上是将一个引用强制转换成了右值引用
。
当我们使用 T && x
或者 auto && x
这种需要类型推断的右值形式的时候,就会被编译器作为万能引用。所谓万能引用,就是既能绑定左值引用,又能绑定右值引用。(所以说,在 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
观察:
Print
,但是这个右值被绑定到了形参 x
上,所以 PrintInner
最终调用的是左值引用的版本。std::move
毋庸置疑,本质上就是都强制转换成右值引用。std::forward
为了将 5 继续当作右值转发,我们可以使用 std::forward
,也就是 完美转发。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";
Buffer Pool 需要有一个页表记录哪些页被缓存在内存中,同时需要跟踪页的使用情况(例如是否被修改?被访问过几次?)。当一个页面被使用的时候,我们应该更新它的访问记录,增加它的 pin count
(类似 reference cnt,当 pin count = 0 的时候我们才可以将一个页标记为 evictable)。当一个页被驱逐的时候,如果它是脏页,我们还需要将其 Flush
到磁盘。
当执行某些扫描操作的时候,DBMS 可以进行预取操作,提前将未来可能读到的数据页加载到内存中。
数据库清楚自己的数据结构和语义,可以对 B+ 树上的范围查询进行预取。
当 DBMS 发现多个查询实际上是等价的时候,就可以将多个查询绑定到单个 cursor 上。查询的结果不一定要一样,还可以共享中间结果。
“Light Scan”。
顺序扫描之类的操作可能会污染 Buffer Pool,如果使用 Buffer Pool Bypass 将不会存储顺序扫描获取的页,而是保存到 worker 本地(不和其他 workers共享)。对于需要大量扫描的操作而言有效。
大多数的 DBMS 都使用 O_DIRECT
绕过 OS Page Cache,直接操作文件 I/O(这样 OS 就不会生成缓存,数据库自己来管理缓存)。
LRU 的近似算法。
LRU 和 Clock 算法都存在的问题是它们只考虑了页面被访问的时间,但是没有考虑页面被访问的频率,很容易被 sequential flooding 污染。
K-distance
的概念(LRU-k 是按照 K-distance 排序的):
MYSQL 使用了一种类似的算法,第一次访问的时候进入 Old List 链表头,再次访问则从 Old List 转移到 Young List 头部。说它是一种近似是因为它没有真正记录时间戳,只是将其直接放到头部(注意 LRU-k 是会计算 k-distance
的)。
Use background writing.
适合 OLTP,一般 Page 大小都是 Hardware Page 的整数倍。
优点:
缺点:
两种存储方式:
我们在做 OLAP 的时候,往往并不是只看其中一列,我们可能要在多列上获取值并且计算。基于此,我们需要 PAX。
Paruqet
和 Orc
格式使用的就是 PAX我们真正想要获得的效果应该如下图所示。我们希望能够对某些局部值进行解压缩,而不用解压缩整个页。
我们可以在列式存储中实现这一点。
(value, index, number) 三元组,即(数据,索引,数量)。
通常数据都不会撑满设定的数值上限,例如 32 字节,我可能最大的数值只有 4 字节不到,那么我们就可以用 4 字节存储一个页中的任一数据(而不是原先的 32 字节)。
对于 9999… 这么大的数,可以维护一个查询表来存储,其他的数保持 Bit Packing。
适用于有限值域,这里 M,F 两种值占两个字节 2 * 8 = 16 bits。
利用差值进行压,甚至可以基于 RLE 进一步压缩。
将原始值映射到一个新值,同时维护一个字典记录这种映射关系。
这里查询的时候也需要将查询值进行压缩,不然的话每一行查询都需要将压缩值进行解压,这样就失去了压缩带来的好处。
有时候我们希望对压缩值进行范围查询,所以我们也会希望映射值也能满足顺序。
如果满足字典顺序没有发生变化,那么 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%';
以上都是 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 允许嵌套查询中的表能够引用另一张表的属性
以下的查询会报错,因为 t2
和 t1
是并行执行的,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;
类似于临时表,或者说是 MYSQL Derived Table(嵌套查询的子查询生成的表叫做派生表
) 的增强版。
例子(找到 sid
最大的学生):
WITH cteSource (maxId) AS (
SELECT MAX(sid) FROM enrolled
)
SELECT name FROM student, cteSource
WHERE student.sid = cteSource.maxId
虽然我们可以使用 mmap
调用将文件映射到内存(操作系统会帮我们进行页面换入和换出),但是操作系统执行页面驱逐的逻辑可能和数据库的驱逐逻辑冲突(换出数据库不想被换出的页面)。
SIGBUS
中断,DBMS 必须处理它,即使正在执行一些 critical section。固定大小的数据块
数据 + 元数据、日志
Oracle 中 Page 是 Self-contained(包含了用于理解这个页的元数据,比如所属 Table 的信息,这种冗余可以避免数据库文件损坏导致无法读取数据)。
每个页都有一个唯一标识符(由此映射到物理内存位置)。
Hardware Page(4KB)是能够保证原子性写入的数据块最大大小。数据库页默认大小通常大于 4KB(非原子性)。
页的无序集合,每个数据元祖随机存储(关系型数据库并不要求按照顺序排列)。对于单个文件而言,找到对应的页很容易,但是对于多个文件而言则比较棘手,需要存储额外的元数据记录哪一个页存在哪一个文件里面,也就是 Page Directory。Page Directory 需要和数据页保持同步,同时需要维护和记录空闲页的数量和位置。
存储诸如页大小,校验和之类的元数据。
File -> Page -> Tuple
ctid
)。存在的问题:
HDFS
。存在的问题:
Kind 即 “Kubernetes in Docker”,在 docker 内部使用 kubeadm 来安装集群。
参考:Kubernetes教程(十五)—使用 kind 在本地快速部署一个 k8s集群
注意一下 KUBECONFIG
环境变量是不是正确,我之前用 kubeadm 在本地安装过 k8s,所以 KUBECONFIG
环境变量不对。kind 的配置文件应该在 ~/.kube/config(我这里是 root 用户)。
该笔记将记录在 Ubuntu-20.04
上安装并使用 Spark
的历程。
安装参考:在 Ubuntu 20.04 上安装 Apache Spark 教程。
启动 master
:
start-master.sh
这个指令会在当前主机启动一个 master 节点,可以在 localhost:8080
访问到控制面板。
启动 worker
:
复制命令行中生成的 master
节点的 URL,启动 worker
:
start-worker.sh spark://xxx
启动成功后长这样:
Spring Boot + Spark:
遇到的问题:
这一套 pom.xml 是可以用的:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.reins</groupId>
<artifactId>spark</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spark</name>
<description>spark</description>
<properties>
<java.version>17</java.version>
<jakarta-servlet.version>4.0.3</jakarta-servlet.version>
<jersey.version>2.36</jersey.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.shyiko</groupId>
<artifactId>mysql-binlog-connector-java</artifactId>
<version>0.21.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.13</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.13</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>3.0.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
注意:在我的环境中,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");
参考:
生成 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 RDD
RDD
是一种弹性分布式数据集,是一种只读分区数据。它是 Spark
的基础数据结构,具有内存计算能力、数据容错性以及数据不可修改特性。
Spark Dataframe
Dataframe
也是一种不可修改的分布式数据集合,它可以按列查询数据,类似于关系数据库里面的表结构。可以对数据指定数据模式(schema)。
Spark Dataset
Dataset
是 DataFrame
的扩展,它提供了类型安全,面向对象的编程接口。也就是说 DataFrame
是 Dataset
的一种特殊形式。
参考:
假如说有这样一个异步函数 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 论坛中看到了相关问题。解决方案是使用 Arc
或 Arc/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
而不是捕获它的引用(捕获引用是默认行为)。
该 lab 基于 Minikube,用于练习 K8s 最基本的 Api Object,如 ConfigMap,Secret,Pod,Service 等等。
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
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 和 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"
我们可以通过以下方式对 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 可以支持滚动升级,当我们的 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 变为最新版本(关闭一部分旧的,开启一部分新的)。
项目网址: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
What I can’t create, I don’t understand.