Posts for: #Learning

Kubernetes CSI 简介

K8s CSI 是 Kubernetes 中非常重要的一个组件,它解决了存储与计算分离的复杂性,并为容器化应用提供了持久化存储的能力。

1. 基本概念

CSI (Container Storage Interface) 译为 容器存储接口。它是由 Kubernetes 社区与存储厂商共同制定的一套标准接口规范。

在 CSI 出现之前,Kubernetes 存储插件的开发和管理存在以下痛点:

  • 紧耦合问题: Kubernetes 内部集成了大量的存储驱动(In-tree 存储插件),例如 AWS EBS、GCE PD、Azure Disk、Ceph RBD 等。这意味着每当存储厂商需要支持 Kubernetes 时,他们都必须将其存储驱动代码提交到 Kubernetes 的核心代码库中。这种方式导致了:
    • Kubernetes 代码库臃肿: 集成了大量存储逻辑,增加了核心代码的复杂性和维护难度。
    • 发布周期长: 存储驱动的更新需要跟随 Kubernetes 的发布周期,新功能和 bug 修复不能及时推送到用户。
    • 存储厂商开发受限: 每次更新都需要与 Kubernetes 社区协调,开发和测试流程繁琐。
  • 兼容性问题: 不同存储厂商的存储系统差异巨大,缺乏统一的接口规范,导致存储系统与 Kubernetes 之间的集成困难。

CSI 的目标就是解决这些问题,实现存储系统与 Kubernetes 的解耦。 它定义了一套通用的接口,允许任何存储厂商开发自己的 CSI 驱动,然后通过这些驱动来与 Kubernetes 进行交互,从而为容器提供存储服务。

1.1. 资源定义

1.1.1. Volumes

Volumes 是 Kubernetes 中 Pod 通过文件系统访问和共享数据的抽象。它主要提供了如下功能:

  • 通过 ConfigMap 或者 Secret 共享配置;
  • 跨容器、跨 Pod 甚至跨 Node 共享数据;
  • 数据持久化。在 Pod 销毁之后仍能继续访问数据。 对于 Pod 来说,Volumes 通过 .spec.volumes 提供给 Pod,容器通过 .spec.containers[*].volumeMounts 来将指定 Volumes 挂载到指定目录。

1.1.2. Persistent Volumes 和 Persistent Volumes Claim

PV 和 PVC 提供了两套 API 将存储的提供和消费分离。

VRF: An Overview

什么是 VRF(virtual routing forwarding)

VRF是一种实现三层网络隔离的关键技术。它通过创建多个路由表,为不同的网络流量提供独立的转发路径。 这意味着,任何三层网络结构,如接口的IP地址、静态路由的配置,甚至BGP(边界网关协议)会话,都可以被映射到特定的VRF中。这种映射机制就像是为每个VRF构建了一个独立的网络空间,彼此之间相互隔离,极大地增强了网络的安全性和管理的便利性。在MPLS VPN(多协议标签交换虚拟专用网络)等应用场景中,VRF为实现大规模的网络隔离和灵活的路由策略提供了基础框架。就像 VLAN 隔离了二层网络一样,VRF 隔离了三层网络。

5eb20c3e919fe3724b92c2ae7a66a7da_MD5

为什么需要 VRF

在 VRF 出现之前,Linux 用户主要采用两种方式来尝试实现类似的功能:策略路由(policy routing)和网络命名空间(net namespace)。然而,这两种方法都存在明显的局限性。

策略路由虽然能够通过多个路由表和策略规则来模拟 VRF 的部分功能(事实上,在 Linux 中,也是基于策略路由来去对 VRF 做的实现),但它的缺点十分突出。这种方式在配置和管理上非常复杂,难以确保网络隔离的有效性,在面对严格的网络审计时,往往无法通过。其复杂性不仅增加了运维的难度,还可能导致网络故障的风险上升,因此不被推荐使用。

网络命名空间在容器技术兴起后得到了广泛应用,它能够为容器提供全面的网络隔离。但在模拟 VRF 功能时,却显得有些“大材小用”。网络命名空间会对所有网络相关的资源进行完全隔离,包括设备、接口、ARP 表和路由表等。这意味着,即使是一些不需要隔离的服务,也会被隔离在不同的命名空间中。以 LLDP(链路层发现协议)为例,在使用网络命名空间的情况下,若要在不同的网络隔离环境中使用 LLDP,就需要在每个命名空间中单独运行实例,并且由于默认套接字相同,还需要为每个实例创建独特的套接字。这不仅增加了系统的开销,还使得管理变得更加复杂。相比之下,VRF 在隔离三层网络结构的同时,允许全局配置的共享和非三层感知服务的统一运行,大大提高了资源的利用效率。

Policy Routing VRF Net Namespace
隔离路由表 隔离三层网络 整个协议栈从二层到 socket 隔离

a9f7c55f8f39572d339b138fb1e12429_MD5c9b6614c864c7d35a8ef0a4f12ecdbfa_MD59edc8b051f504bf72140d1238513d687_MD5

VRF 配置

在 Linux 系统中配置 VRF,主要借助iproute2包来完成一系列操作。

  • 创建 VRF,并关联到 table 1
test1@test1:~$ ip link add vrf-1 type vrf table 1
test1@test1:~$ ip link set vrf-1 up
  • 添加接口到 VRF,可以看到 wg0 的 master 是 vrf-1,所有 wg0 的流量会使用关联的 vrf-1 路由表进行路由
test1@test1:~$ ip link set wg0 master vrf-1
test1@test1:~$ ip -d link show wg0 
9: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1400 qdisc noqueue master vrf-1 state UNKNOWN mode DEFAULT group default qlen 1000
    link/none  promiscuity 0 minmtu 0 maxmtu 2147483552 
    wireguard 
    vrf_slave table 1 addrgenmode none numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
  • 添加和查看 VRF 静态路由
root@test1:~# ip route add default via 10.1.0.10 vrf vrf-1
root@test1:~# ip route show table 1 
default via 10.1.0.10 dev wg0 
local 10.1.0.10 dev wg0 proto kernel scope host src 10.1.0.10 
root@test1:~# ip route show vrf vrf-1 
default via 10.1.0.10 dev wg0 

VRF 之间路由

有两种方法可以执行跨 VRF 路由。第一种方法涉及一个 VRF 的表中配置的路由,指向绑定到不同 VRF 的设备。

eBPF 指令集规范

1 BPF Instruction Set Specification, v1.0 — The Linux Kernel documentation

寄存器和调用约定

eBPF有10个通用寄存器和一个只读的帧指针寄存器,所有的寄存器都是64bits位长。

eBPF的调用约定如下:

  • R0: 函数调用的返回值和eBPF程序的退出值
  • R1 - R5: 函数调用的参数
  • R6 - R9: callee saved registers,由函数调用负责保存到栈上
  • R10: 只读的帧指针,用于访问栈 R0 - R5是临时寄存器,eBPF程序在函数调用时如需使用需要将它们保存/填充。

指令编码

eBPF使用64bit指令,编码如下:

32位(最高有效位) 16位 4位 4位 8位(最低有效位)
immediate offset source register dest register opcode
  • imm: 有符号整型立即数
  • offset: 有符号整型数,用于指针算术
  • src_reg: 除开特殊指令的编码外(如64位立即数指令)指的是源寄存器(R0 - R10)
  • dst_reg: 目标寄存器(R0 - R10)
  • opcode: 操作码

绝大多数指令不会使用所有的字段。没有使用的字段应当被置0。多字节的字段(如imm和offset)在大端BPF中以大端字节序存储,小端BPF中以小端字节序存储。

opcode                  offset imm          assembly
       src_reg dst_reg
07     0       1        00 00  44 33 22 11  r1 += 0x11223344 // little
       dst_reg src_reg
07     1       0        00 00  11 22 33 44  r1 += 0x11223344 // big

除了这些基本指令编码外,eBPF还有一种宽指令编码,在基本指令之后使用第2个64位立即数做扩展。第2个立即数包括一个伪指令,和基本指令的格式一样,不过所有的字段(除了imm)都置为0。它的imm作为整个宽指令中的imm64值的高32位:

eBPF 全局变量的实现

Linux内核在v5.2支持了eBPF全局变量(见v5.2)。

全局变量会被Clang编译器放入.bss, .data, .rodata sections。那么支持全局变量即如何把这些sections中的数据加载进内核,并且在relocation时,给访问这些变量的指令正确的地址。

早期 workaround

关于 eBPF 静态变量支持的讨论在Linux Plumbers Conference 2018 由Cilium提出。早期的 workaround 如下:

#include <linux/bpf.h>

typedef unsigned int __u32;
typedef long long unsigned int __64;

#ifndef __section
# define __section(NAME)				\
	__attribute__((section(NAME), used))
#endif
#ifndef __fetch
# define __fetch(x) (__u32)(__u64)(&(x))
#endif

__u32 foo = 42; 			// .data section
// __u32 foo;   			// .bss section
// const __u32 foo = 42; 	// .rodata section

int __main(struct __sk_buff *skb)
{
    skb->mark = __fetch(foo);
    return 0;
}

char __license[] __section("license") = "";

编译后,foo变量存储在.data section内。

# llvm-readelf -S test.o
There are 8 section headers, starting at offset 0x148:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .strtab           STRTAB          0000000000000000 0000fa 00004b 00      0   0  1
  [ 2] .text             PROGBITS        0000000000000000 000040 000028 00  AX  0   0  8
  [ 3] .rel.text         REL             0000000000000000 0000e8 000010 10      7   2  8
  [ 4] .data             PROGBITS        0000000000000000 000068 000004 00  WA  0   0  4
  [ 5] license           PROGBITS        0000000000000000 00006c 000001 00  WA  0   0  1
  [ 6] .llvm_addrsig     LLVM_ADDRSIG    0000000000000000 0000f8 000002 00   E  7   0  1
  [ 7] .symtab           SYMTAB          0000000000000000 000070 000078 18      1   2  8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

.rel.text section中也存在foo变量的relocation信息:

make命令和编译

Makefile

标准格式

target:dependencies1 dependencies2 ...
   recipe

.PHONY: clean
clean:
   -rm ...

注意事项

  • 运行make时如果没有指定target,那么make会构建Makefile中的第一个target
  • 通常情况下,目标名即生成的文件名。如果一条规则的目标文件存在并且该文件比它所有的依赖都要新,那么make会跳过recipe;如果目标文件不存在,那么目标文件的 timestamp 为开始的时间;否则timestamp为相应文件的修改时间。
  • 每次运行 make clean,”clean“中的recipe都会被执行,因为clean文件永远都不会被创建。(可以使用 .PHONY 创建伪目标使Makefile可读性更高)
  • recipes必须用tab缩进
  • 可以通过并行的方式运行recipes:make -j 4(指定并行的任务数)
  • 如果没有指定规则,Make会自动化创建规则。例如,本地有一个C文件”program.c“,当运行 make program时,Make会自动编译生成 program

变量

  • $@ 目标文件
  • $^ 所有的依赖文件
  • $< 第一个依赖

头文件

环境变量

  • C_INCLUDE_PATH C语言头文件路径
  • CPLUS_INCLUDE_PATH C++ 头文件路径

搜索路径

#include<>

  1. 先搜索 -I 指定的目录
  2. 然后搜索gcc的环境变量 CPLUS_INCLUDE_PATH
  3. 最后搜索gcc的内定目录
    1. /usr/include
    2. /usr/local/include
    3. /usr/lib/gcc/x86_64-redhat-linux/4.1.1/include

#include ""

  • 搜索当前目录,#include<>方式不会搜索当前目录

动态库

环境变量

  • LD_LIBRARY_PATH 动态链接库搜索路径
  • PKG_CONFIG_PATH .pc文件(package config)文件搜索路径

搜索路径

  1. 首先在环境变量 LD_LIBRARY_PATH 所记录的路径中查找
  2. 在程序链接时指定的 rpath 中查找,可以 readelf binfile | grep RPATH
  3. 然后从缓存文件/etc/ld.so.cache中查找。这个缓存文件由/sbin/ldconfig命令读取配置文件/etc/ld.so.conf 之后生成(也可以在 ld.so.conf.d 目录下增加 .conf 文件,里面写入库路径,在 ld.so.conf 中 include ld.so.conf.d/.conf )
  4. 如果上述步骤都找不到,则到默认的系统路径中查找,先是/usr/lib然后是/lib

编译参数

-shared

指定生成动态连接库