MichaelFu

好记性不如烂笔头

本文介绍23设计模式使用场景及其示例。

单例模式

单例模式适用于整个系统只有一个实例的场景,比如数据库连接池、线程池、系统配置信息管理等。确保在任何时候只有一个对象被创建来提供统一的访问点,避免资源浪费和不一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
use std::cell::RefCell;
use std::sync::{Mutex, MutexGuard};

use once_cell::sync::Lazy;

#[derive(Debug, Clone, Default)]
pub struct Connection {
name: String,
}

pub struct DatabaseConnectionPool {
connections: Vec<RefCell<Connection>>,
}

impl DatabaseConnectionPool {
fn new(pool_size: usize) -> DatabaseConnectionPool {
DatabaseConnectionPool {
connections: Vec::with_capacity(pool_size),
}
}

pub fn get_connection(&mut self) -> Option<RefCell<Connection>> {
let length = self.connections.len();
if length > 0 {
Some(self.connections.remove(length - 1))
} else {
None
}
}

pub fn release_connection(&mut self, connection: Connection) {
self.connections.push(RefCell::new(connection))
}

pub fn count(&self) -> usize {
self.connections.len()
}
}

static GLOBAL_POOL: Lazy<Mutex<DatabaseConnectionPool>> =
Lazy::new(|| Mutex::new(DatabaseConnectionPool::new(10)));

pub fn get_pool_instance() -> MutexGuard<'static, DatabaseConnectionPool> {
GLOBAL_POOL.lock().unwrap()
}

fn main() {
{
let mut pool1 = get_pool_instance();
pool1.release_connection(Connection::default());
println!("Address of x: {:p}", std::ptr::addr_of!(pool1));
}
{
let mut pool2 = get_pool_instance();
pool2.release_connection(Connection::default());
println!("Address of x: {:p}", std::ptr::addr_of!(pool2));
}
println!("connections length: {}", get_pool_instance().count());
}

这将输出:

Address of x: 0x41330ff5f8
Address of x: 0x41330ff678
connections length: 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import "fmt"

// DatabaseConnectionPool结构体模拟数据库连接池
type DatabaseConnectionPool struct {
connections []*Connection
}

// Connection结构体模拟数据库连接
type Connection struct{}

// 包级别的私有变量,用于存储单例的数据库连接池实例
var instance *DatabaseConnectionPool

// 获取数据库连接池单例实例的函数
func GetDatabaseConnectionPoolInstance() *DatabaseConnectionPool {
if instance == nil {
instance = &DatabaseConnectionPool{
connections: make([]*Connection, 10), // 假设初始化10个连接
}
}
return instance
}

func main() {
pool1 := GetDatabaseConnectionPoolInstance()
pool2 := GetDatabaseConnectionPoolInstance()

// 验证两个实例是否相同
if pool1 == pool2 {
fmt.Println("数据库连接池实例是相同的,单例模式生效")
} else {
fmt.Println("数据库连接池实例不同,单例模式未生效")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class DatabaseConnectionPool {
// 私有静态实例变量,初始化为null
private static DatabaseConnectionPool instance = null;

// 模拟数据库连接的列表
private Connection[] connections;

// 私有化构造函数,防止外部通过构造函数创建实例
private DatabaseConnectionPool() {
connections = new Connection[10]; // 假设初始化10个连接
}

// 静态方法用于获取单例实例
public static DatabaseConnectionPool getInstance() {
if (instance == null) {
instance = new DatabaseConnectionPool();
}
return instance;
}

public static void main(String[] args) {
DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance();

// 验证两个实例是否相同
if (pool1 == pool2) {
System.out.println("数据库连接池实例是相同的,单例模式生效");
} else {
System.out.println("数据库连接池实例不同,单例模式未生效");
}
}
}

class Connection {}
阅读全文 »

eBPF,全称“扩展的伯克利数据包过滤器 (Extended Berkeley Packet Filter)”,它是一种数据包过滤技术,是从 BPF (Berkeley Packet Filter) 技术扩展而来的。BPF 提供了一种在内核事件和用户程序事件发生时安全注入代码的机制,这就让非内核开发人员也可以对内核进行控制。随着内核的发展,BPF 逐步从最初的数据包过滤扩展到了网络、内核、安全、跟踪等,而且它的功能特性还在快速发展中,这种扩展后的 BPF 被简称为 eBPF(相应的,早期的 BPF 被称为经典 BPF,简称 cBPF)。实际上,现代内核所运行的都是 eBPF,如果没有特殊说明,内核和开源社区中提到的 BPF 等同于 eBPF

eBPF 之前,内核模块是注入内核的最主要机制。由于缺乏对内核模块的安全控制,内核的基本功能很容易被一个有缺陷的内核模块破坏。而 eBPF 则借助即时编译器(JIT),在内核中运行了一个虚拟机,保证只有被验证安全的 eBPF 指令才会被内核执行。同时,因为 eBPF 指令依然运行在内核中,无需向用户态复制数据,这就大大提高了事件处理的效率。

由于这些突出的特性,eBPF 现如今已经在故障诊断、网络优化、安全控制、性能监控等领域获得大量应用。比如,Facebook 开源的高性能网络负载均衡器 KatranIsovalent 开源的容器网络方案 Cilium ,以及著名的内核跟踪排错工具 BCCbpftrace 等,都是基于 eBPF 技术实现的。

本文主要简单介绍一些常用的 eBPF 相关工具的安装。

阅读全文 »

MacvlanipvlanLinux 网络驱动程序,可将底层或主机接口直接暴露给主机中运行的虚拟机或容器。在运行裸机服务器时,主机联网可以很简单,只需几个以太网接口和一个默认网关即可提供外部连接。但是当在一台主机上运行多个虚拟机时,就需要在主机内和主机间提供虚拟机之间的连接。单个主机中的虚拟机数量平均不超过 15-20 个。在主机中运行容器时,单个主机中的容器数量很容易超过 100 个,这就需要有复杂的机制来实现容器之间的互联。容器或虚拟机之间的通信大致有两种方式,在底层网络中,通常使用网桥、macvlan、ipvlan将虚拟机或容器直接暴露于主机网络。但是在用于跨主机通信的overlay网络中,会使用 VXLAN 这样的技术进行额外的封装。

在安装 docekr 之后,会默认创建 docker0 这样的网桥,这也是docker默认的容器网络实现方式,连接同一个网桥上的容器,处于相同的网络之内,可以直接在二层实现网络互通,对于外部访问则通过网桥实现。

1
2
3
4
5
6
7
8
9
$ ifconfig docker0
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
inet6 fe80::42:92ff:feb9:e637 prefixlen 64 scopeid 0x20<link>
ether 02:42:92:b9:e6:37 txqueuelen 0 (Ethernet)
RX packets 15861 bytes 835715 (835.7 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 31502 bytes 48605473 (48.6 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

macvlanipvlan允许单个物理接口创建出多个子接口,其中每个macvlan子都有唯一的MACIP地址,并直接暴露在底层网络中。macvlan 接口通常用于虚拟化应用,每个 macvlan 接口都连接到一个容器或虚拟机。macvlan4种类型(PrivateVEPABridgePassthru),常用的类型是 Bridge,它允许单个主机中的实体在数据包不离开主机的情况下相互通信。对于外部连接,则使用底层网络,下图显示两个容器使用 macvlan 网桥相互通信并与外界通信,两个容器都将使用 macvlan 子接口直接接入底层网络。

ipvlanmacvlan 类似,区别在于每个子接口具有相同的 mac 地址,ipvlan 支持 L2L3 模式,父接口只能选择其中一种工作模式,在 ipvlan l2 模式下,父接口作为交换机来转发子接口的数据,同一个网络的子接口可以通过父接口来转发数据,而如果想发送到其他网络,报文则会通过父接口的路由转发出去。L3 模式下,ipvlan 有点像路由器的功能,它在各个虚拟网络和主机网络之间进行不同网络报文的路由转发工作。只要父接口相同,即使虚拟机/容器不在同一个网络,也可以互相 ping 通对方,因为 ipvlan 会在中间做报文的转发工作。

阅读全文 »

Istio 是服务网格,即ServiceMesh的一种实现,服务网格通常用于描述构成应用程序的网络以及它们之间的交互。在从单体应用向分布式微服务架构转型的过程中,虽然从中获益良多,但是随着规模和复杂性的增长,服务网格越来越难以理解,给开发人员和运维人员带来的挑战快速增加。这些挑战包括:服务发现,负载均衡,故障恢复,指标收集,监控以及一些更加复杂的运维需求,例如:A/B测试、金丝雀发布、限流、访问控制,端到端认证等。而 Istio 提供了一个完整的解决方案,通过为整个服务网格提供行为洞察和操作控制来满足微服务应用程序的多样化需求。

Istio 以非常简单的方式来为已部署的服务建立网络,对应用程序代码只需要进行一点或者不需要做任何改动,要想让服务支持Istio,只需要在应用旁边部署一个 sidecar 代理,使用 Istio 的控制面进行功能配置和管理代理,拦截服务之间的所有网络通信,已达到:

  • HTTPgRPCWebSocketTCP 流量的自动负载均衡;
  • 通过丰富的路由规则、重试、故障转移和故障注入,可以对流量行为进行细粒度控制;
  • 可插入的策略层和配置 API,支持访问控制、速率限制和配额;
  • 对出入集群入口和出口中所有流量的自动度量指标、日志记录和追踪;
  • 通过强大的基于身份的验证和授权,在集群中实现安全的服务间通信;

综上,对 Istio 的核心功能可以总结为以下几点:

  1. 流量管理,通过简单的规则配置和流量路由,可以控制服务之间的流量和API调用。Istio 简化了断路器、超时和重试等服务级别属性的配置,并且可以轻松设置A/B测试、金丝雀部署和基于百分比的流量分割的分阶段部署等重要任务;
  2. 安全,Istio 的安全功能使开发人员可以专注于应用程序级别的安全性。Istio 提供底层安全通信信道,并大规模管理服务通信的认证、授权和加密。使用Istio,服务通信在默认情况下是安全的,它允许您跨多种协议和运行时一致地实施策略——所有这些都很少或根本不需要应用程序更改;
  3. 可观察性,Istio 强大的追踪、监控和日志记录可让开发或者运维人员深入了解服务网格部署。通过 Istio 的监控功能,可以真正了解服务性能如何影响上游和下游的功能,而其自定义仪表板可以提供对所有服务性能的可视性;

在架构上,Istio 服务网格逻辑上分为数据平面和控制平面,其中:

  • 数据平面由一组以 sidecar 方式部署的智能代理(Envoy)组成。这些代理负责协调和控制微服务之间的所有网络通信。 它们还收集和报告所有网格流量的遥测数据;
  • 控制平面 管理并配置代理来进行流量路由;

架构图如下图所示:

Envoy:是用 C++ 开发的高性能代理,用于协调服务网格中所有服务的入站和出站流量。Envoy 代理是唯一与数据平面流量交互的 Istio 组件。Envoy 被部署为服务的 Sidecar,在逻辑上为服务增加了 Envoy 的许多内置特性,例如:动态服务发现,负载均衡,TLS 终端,HTTP/2gRPC 代理,熔断器,健康检查,基于百分比流量分割的分阶段发布,故障注入,丰富的指标;

Sidecar 代理模型还允许您向现有的部署添加 Istio 功能,而不需要重新设计架构或重写代码。由 Envoy 代理启用的一些 Istio 的功能和任务包括:

  • 流量控制功能:通过丰富的 HTTPgRPCWebSocketTCP 流量路由规则来执行细粒度的流量控制;
  • 网络弹性特性:重试设置、故障转移、熔断器和故障注入;
  • 安全性和身份认证特性:执行安全性策略,并强制实行通过配置 API 定义的访问控制和速率限制;
  • 基于 WebAssembly 的可插拔扩展模型,允许通过自定义策略执行和生成网格流量的遥测;

Istiod 将控制流量行为的高级路由规则转换为 Envoy 特定的配置, 并在运行时将其传播给 SidecarPilot 提取了特定平台的服务发现机制,并将其综合为一种标准格式,任何符合 Envoy APISidecar 都可以使用。以及通过内置的身份和凭证管理,实现了强大的服务对服务和终端用户认证,可以使用 Istio 来升级服务网格中未加密的流量,这样运营商可以基于服务身份而不是相对不稳定的第3层或第4层网络标识符来执行策略。Istiod 还可以充当证书授权(CA),生成证书以允许在数据平面中进行安全的 mTLS 通信。

阅读全文 »

为了从外部能够访问到集群中的服务,k8S 提供了多种方式,从 NodePort 类型的 ServiceLoadBalancer 类型的 Service,到 Ingress,一直在改进,NodePort类型的服务流量从单一节点进来,没法在节点之间负载均衡,进而衍生出LoadBalancer类型的服务,该类型的服务虽然解决了前面存在的问题,但是需要云厂商的支持,况且针对每个Service粒度提供一个公网IP地址,未免有点浪费,进而衍生出 Ingress,支持7层代理,能够通过单一的入口,以及域名和Path匹配等机制将流量转发到不同的后端服务中去。Ingress 虽然解决了LoadBalancer存在的问题,但它在实际的使用场景中又遇到了新的问题:

  1. Ingress 仅支持7层,没法对四层的流量进行转发;
  2. Ingress 在设计的时候只考虑一种用户角色,既整个系统的运维人员和管理员,这种模型在许多拥有多个团队的企业中都不适用,包括应用开发人员、平台运维人员、安全管理员等,他们在协作开发和交付应用的过程中需要控制Ingress配置的不同方面;
  3. Ingress 中使用了很多annotion实现自定义功能,对于不同的 ingress controller 没法做到一致性,例如,这里的 nginx annotions,这些在原本的 Ingress 对象中都是不支持的;

在这些问题的促使下,社区又提出了新的概念:Gateway,明确定义并划分不同角色的职责范围有助于简化管理,对三个主要的 Gateway API 资源(GatewayClassGatewayRoute)进行了标准化。具体来说,基础架构提供商负责为 Kubernetes 集群定义 GatewayClasses,集群运维人员则负责在集群中部署和配置 Gateway(包括策略),而应用开发人员可以自由地将 Route 附加到 Gateway,以对外暴露应用。

阅读全文 »

本篇文章主要简单讲解TLS证书的基本知识以及如何在K8S中使用证书提供安全服务。

证书

TLS证书用于证明访问目标的有效性,当访问某个网站时,浏览器会自动验证证书是否有效,并且会通过证书中提供的Server公钥和Server协商出用于接下来安全数据传输的对称加密秘钥。浏览器(或者客户端,例如:curl)验证服务端证书的流程如下:

  1. 浏览器或者客户端在和服务端建立加密通信的流程中,会下载服务端的证书到本地,这个证书中包含了服务端证书的公钥,并且这个证书会使用可信任的CA机构的私钥进行签名;
  2. 浏览器或者操作系统中安装了大多数著名机构的根证书,浏览器或者客户端会使用这些机构根证书的公钥验证收到的证书是否是可信任机构颁发的;
  3. 如果浏览器或者客户端收到的证书是有效的,紧接着会验证证书中包含的服务器或者IP地址是不是和当前打开的地址匹配;
  4. 紧接着浏览器会和服务端协商出用于本次数据加密的对称秘钥,使用对称秘钥一是这个秘钥是在每次数据通信时动态协商出来的,会话结束就是小了,防止公钥泄漏带来的安全问题,而是对称加解密密相比非对称加解密有更好的性能表现;

X.509 公钥证书中有一些常用的扩展名,如下所示是它们的含义:

  • .csr:证书请求文件,是由 RFC 2986定义的PKCS10格式,包含部分/全部的请求证书的信息,比如,主题, 机构,国家等,并且包含了请求证书的公玥,这些被CA中心签名后返回一张证书,返回的证书是公钥证书(只包含公玥不含私钥);
  • .pem:是一种容器格式,可能仅包含公钥证书,也可以包含完整的证书链(包括公玥,私钥,和根证书)。也可能用来编码 CSR文件;
  • .key:就是一个pem格式只包含私玥的文件,.key 作为文件名只是作为一个明显的别名;
  • .pkcs12 .pfx .p12pkcs即 RSA定义的公玥密码学(Public-Key Cryptography Standards)标准,有多个标准pkcs12只是其一,是描述个人信息交换语法标准,有的文件直接使用其作为文件后缀名。这种文件包含公钥和私玥证书对,跟pem文件不同的是,它的内容是完全加密的。用openssl可以把其转换成包含公玥和私玥的.pem文件。命令:openssl pkcs12 -in file-to-convert.p12 -out converted-file.pem -nodes
  • .derderASN.1众多编码方案中的一个,使用der编码方案编码的pem文件。der编码是使用二进制编码,一般pem文件使用的是base64进行编码,所以完全可以把der编码的文件转换成pem文件,命令:openssl x509 -inform der -in to-convert.der -out converted.pem 使用der编码的pem文件,后缀名可以为.der,也可以为 .cert .cer .crt
  • .cert .cer .crtpem或者der编码格式的证书文件,这些文件后缀名都会被windows资源管理器认为是证书文件。有趣的是, .pem 反而不会被认为是证书文件;
阅读全文 »

tun 是一种虚拟的3层虚拟网络设备,同时它也是一个字符设备,字符设备意味着可以把它当做一个文件,可以使用文件API操作这个设备,例如 open/read/write,由于它同时也是一个网络设备,所以它也可以像一个网卡一样,从内核网络协议栈中收发报文。所以从它架构上来看,tun 设备的一端连接着应用程序,一端连接着网络协议栈,如下图所示:

从在系统中的表象来看,字符设备的文件类型是c,没有大小,但是有主次版本号:

1
2
$ ls -alh /dev/net/tun
crw-rw-rw- 1 root root 10, 200 Jan 30 17:00 /dev/net/tun

tun 设备的创建是通过打开/dev/net/tun这个文件,然后使用ioctl系统调用对其进行clone。也可以使用 ip 命令来实现tun设备的创建:

ip tuntap add dev tun1 mod tun

新创建的 tun1 设备位于 /sys/class/net/ 目录中:

1
2
$ ll /sys/class/net/tun1
lrwxrwxrwx 1 root root 0 Feb 4 15:44 /sys/class/net/tun1 -> ../../devices/virtual/net/tun1/

删除使用如下命令:

ip tuntap del dev tun1 mod tun

阅读全文 »

Pod中的容器它里面的文件在磁盘上是临时存放的,当容器崩溃或停止时,kubelet 会以干净的状态重新启动容器,在容器生命周期内创建或修改的所有文件都将丢失。另外Pod在运行期间,需要为它注入一些必要的配置信息,以满足其运行。在这些场景的要求下, 就应需而生,为了持久化存放容器里的数据,为容器运行提供配置。所有这些卷可以分为3类:

  • 持久卷(Persistent Volume):和节点一样,属于集群资源,可以由管理员事先制备,或者使用存储类来动态制备,使用时通过PVC申请,就像其名字表述的一样,为了持久化数据;
  • 投射卷(Projected Volumes):为Pod运行注入必要的配置信息;
  • 临时卷(Ephemeral Volume):应用程序需要额外的存储,但并不关心数据在重启后是否仍然可用,随Pod而生,随Pod而灭。例如,Redis缓存服务经常受限于内存大小,而且可以将不常用的数据转移到比内存慢的存储中,对总体性能的影响并不大;

这些卷以及它们的分类、涉及的概念如下:


mindmap
卷
    投射卷
        Secret
        DownwardAPI
        Configmap
        ServiceAccountToken
    持久卷
        PV
        PVC
        StorageClass
    临时卷
        emptyDir
        CSI 临时卷
        通用临时卷

阅读全文 »

PodK8S中的调度单位,它是一个逻辑概念,用于将一些关系密切的容器部署在一起提供对外服务,这些容器互相之间会发生直接的文件交换、使用localhost或者Socket文件进行本地通信、会发生非常频繁的远程调用、需要共享某些Linux Namespace等等,Pod 中的所有容器都共享同一个Network NamespaceK8S 中为了实现不同的目的,在Pod基础之上衍生出了不同的部署模型,例如,常见的 DeploymentReplicaset、以及StatefulSet等等,本文就来举例并且说明它们之间的区别。

这些对象之间的关系如下图所示:

阅读全文 »

ServiceKubernetes中的资源类型,用来将一组Pod的应用作为网络服务公开。虽然每个Pod都有自己的IP,但是这个IP的生命周期与Pod生命周期一致,也就是说Pod销毁后这个IP也就无效了,而ServiceIP(ClusterIP) 则是在创建之后便不会改变,ServicePod 之前通过iptablesipvs代理等手段关联。k8s一共提供了四种不同目的类型的Service,分别是ClusterIPNodePortLoadBalancer以及ExternalName,本来我们就来探索这四种服务的使用场景以及背后的使用原理。

k8s 集群中的每个节点都是运行一个kube-proxy,它用于实现流量从ServicePod之间的转发。默认在 Linux 平台下,它使用 iptables 作为后端实现,通过监听Kubernetes控制平面,获知对 ServiceEndpointSlice对象的添加和删除操作,对于每个Servicekube-proxy 会添加 iptables 规则,在这些这些规则的加持下,流向Service的流量会被重新路由到Service后端集合中的其中之一。

四种模式的基本工作原理如下图所示:

阅读全文 »
0%