0%

Linux netplan

初创团队,各方面有限,我们是saas+硬件,但是我们只有单个公网IP、一个一级域名,所以为了短时间适配生产、研发、测试三个环境,同时支持SaaS+硬件通信,我们需要做前端入口的流量管理,团队的小伙伴选择了OPNsense。
这一篇是我为了了解该技术方案而简单整理的。

# 解决Ubuntu Server 24.04删除网卡后的Netplan问题

引言

在Ubuntu Server 24.04中,Netplan是默认的网络配置工具,使用YAML文件管理网络设置。最近,我在虚拟机中配置了双网卡(一张内网,一张外网),但删除一张网卡后,网络无法正常工作。经过调试,我通过手动更新Netplan配置文件解决了问题,以下是我的经验分享。

问题描述

我的虚拟机最初配置了两张网卡:enp0s3(外网,静态IP)用于访问外部网络,enp0s8(内网,DHCP)用于本地通信。删除enp0s8后,运行netplan apply没有生效,ip a显示enp0s3未正确分配IP。日志(journalctl -u systemd-networkd)提示Netplan仍尝试配置已删除的网卡。

1
2
4: ens38: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 00:30:26:8c:78:57 brd ff:ff:ff:ff:ff:ff

解决方案

以下是解决步骤:

  1. 启动网卡
1
ip link set ens38 up
  1. 检查现有Netplan配置
    查看/etc/netplan/目录中的配置文件(通常为00-installer-config.yaml)。原始配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    network:
    version: 2
    ethernets:
    enp0s3:
    dhcp4: no
    addresses: [172.162.1.100/24]
    gateway4: 172.162.1.1
    nameservers:
    addresses: [8.8.8.8, 8.8.4.4]
    enp0s8:
    dhcp4: yes
  2. 更新配置文件
    删除enp0s8相关配置,仅保留enp0s3。修改后的文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    network:
    version: 2
    ethernets:
    enp0s3:
    dhcp4: no
    addresses: [172.162.1.100/24]
    gateway4: 172.162.1.1
    nameservers:
    addresses: [8.8.8.8, 8.8.4.4]

    注意:确保YAML缩进为2个空格,避免格式错误。

  3. 验证并应用配置
    检查配置语法:

    1
    sudo netplan --debug apply

    确认无错误后,应用配置:

    1
    sudo netplan apply
  4. 测试网络
    验证接口状态和连通性:

    1
    2
    ip a
    ping 8.8.8.8
  5. 检查日志
    如果仍不生效,查看日志:

    1
    journalctl -u systemd-networkd

总结

删除虚拟机网卡后,Netplan不会自动更新配置,需手动移除无效网卡的配置条目。关键是检查YAML文件、确保格式正确,并使用netplan --debug apply定位问题。在多网卡场景下,建议定期验证网卡名称(ip link)和配置文件一致性。遇到类似问题?欢迎留言分享你的经验!

网络安全 软路由

初创团队,各方面有限,我们是saas+硬件,但是我们只有单个公网IP、一个一级域名,所以为了短时间适配生产、研发、测试三个环境,同时支持SaaS+硬件通信,我们需要做前端入口的流量管理,团队的小伙伴选择了OPNsense。
这一篇是我为了了解该技术方案而简单整理的。

OPNsense的防火墙模块基于FreeBSD的pf(Packet Filter),提供了强大的NAT功能,包括Port Forward(转发规则)、Outbound NAT(出站NAT)和NPTv6(IPv6前缀转换)。从我的截图可以看到,Port Forward页面列出了WAN到LAN的TCP流量规则(如Web服务和RDP),而Outbound NAT页面显示了自动生成的出站规则。这些功能让我在单一公网IP下实现了多环境隔离和外部访问。
NAT策略:Port Forward与Outbound NAT的协同
最初我以为Port Forward能解决所有需求,但实践证明,仅靠它处理入站流量是不够的。Outbound NAT才是出站流量的关键,尤其在多VLAN和硬件通信场景中,两者需协同工作。

Port Forward:

用途:处理入站流量,将公网端口映射到内部IP。例如,我配置了WAN:443到192.168.10.10:443,让外部通过prod.example.com访问生产环境。
局限:不管理出站流量,硬件向SaaS发送数据时需依赖Outbound NAT。
配置:Firewall → NAT → 转发,添加规则(Protocol: TCP;Destination: WAN address:443;Redirect to: 192.168.10.10:443)。

Outbound NAT:

位置:Firewall → NAT → Outbound,当前为自动模式,自动为WAN出站流量分配公网IP。
优化:切换到手动模式,为每个VLAN设置规则,确保出站流量隔离。
我的经验:自动模式曾因端口冲突导致硬件API请求失败,切换到手动后问题解决。

找到并配置Outbound NAT
Outbound NAT是管理出站流量的关键,位于Firewall → NAT → Outbound页面。从我的截图可以看到,默认使用“自动生成规则”,为LAN和Loopback网段分配WAN地址。但对于复杂场景,我切换到手动模式以满足需求。

手动配置:
点击“切换到手动规则”(Switch to Manual Outbound NAT rule generation),保存。
添加规则:Interface: WAN;Source: 192.168.10.0/24(生产),Translation: WAN地址,描述:“Production Outbound”。
依次为研发(192.168.20.0/24)和测试(192.168.30.0/24)设置规则。
硬件场景:为生产VLAN的硬件(如192.168.10.10)添加规则,确保其API请求(如curl https://prod.example.com/api/v1/data)顺利出站。

优化:启用NAT Reflection(Firewall → Advanced),让内部设备用公网IP访问暴露服务。
经验:备份配置(System → Config History)后切换模式,避免误操作。日志监控(Firewall → Log Files)帮助我定位流量问题。

下一步行动项

实战配置:综合NAT策略

入站:Port Forward规则处理prod.example.com的HTTPS请求,映射到生产环境的Web服务器。
出站:手动Outbound NAT为每个VLAN配置规则,保障出站流量隔离。我的硬件通过生产VLAN的规则上传数据,测试显示吞吐量稳定。
硬件场景:结合Port Forward和Outbound NAT,硬件既能接收SaaS命令(入站),又能上传数据(出站),单IP利用率显著提升。
NPTv6(未来扩展):当前用IPv4,但NPTv6为IPv6网络的前缀转换提供了可能,适合ISP支持IPv6时升级。

硬件通信:NAT与子域名的结合
我的SaaS硬件通过prod.example.com与服务通信,NAT策略确保其双向通信。

入站:Port Forward映射WAN:443到192.168.10.10:443,配合HAProxy根据子域名分发流量。
出站:Outbound NAT规则让硬件出站请求使用公网IP,日志显示连接正常。
安全:用别名(Firewall → Aliases)定义硬件IP范围,限制未授权访问。
实践:我用curl测试硬件请求,确认数据成功上传到SaaS。

VPN: 团队vpn后续我会研究是否能用OPNsense

架构师学习 第4篇

写在前面

说实话,刚开始接触SOLID原则的时候,我觉得这些都是些”虚头巴脑”的理论。直到临时接手一个离职同事的代码——一个类上千行,改一行bug到处冒,而且代码完全没有秩序,我才真正体会到这些原则的价值。

SOLID原则是我CLAUDE.md其中一个规范,是由Robert C. Martin(Uncle Bob)提出的五个面向对象设计原则。简单来说,它们能帮我写出更好维护、更好扩展、更好测试的代码。

因为在软件开发的早期,我们往往更关注功能的实现,而忽视了代码的设计质量。随着项目规模的增长,糟糕的代码设计会变得越来越难以维护,每一次修改都可能引发意想不到的问题。SOLID原则正是为了解决这些问题而诞生的。

为什么要学习SOLID原则?

SOLID原则能够帮助我们:

  1. 降低代码耦合度:让模块之间的依赖关系更加清晰
  2. 提高代码可维护性:单一职责让修改更加聚焦
  3. 增强代码可扩展性:开闭原则让功能扩展更安全
  4. 改善代码可测试性:依赖倒置让单元测试更容易编写

学习资料

单一职责原则(Single Responsibility Principle,SRP)

原则定义

一个类应该只有一个引起它变化的原因,换句话说,一个类应该只有一个职责。

直观理解

想象一个瑞士军刀,它有太多功能:刀、剪刀、开瓶器等。虽然功能强大,但当你只需要用刀的时候,带着整个军刀就显得笨重了。同样,一个类承担太多职责时,任何职责的变化都可能影响其他职责,导致系统变得脆弱。

反例代码

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
/**
* 违反单一职责原则的用户服务类
* 该类同时负责用户业务逻辑和日志记录
*/
public class UserService {

// 职责1:用户管理
public void register(String username, String password) {
// 注册逻辑
System.out.println("用户注册:" + username);
}

public void login(String username, String password) {
// 登录逻辑
System.out.println("用户登录:" + username);
}

// 职责2:日志记录 - 这不应该属于用户服务
public void logToFile(String message) {
try {
FileWriter writer = new FileWriter("app.log", true);
writer.write(message + "\n");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}

// 职责3:邮件发送 - 这也不应该属于用户服务
public void sendEmail(String to, String subject, String content) {
System.out.println("发送邮件给:" + to);
System.out.println("主题:" + subject);
System.out.println("内容:" + content);
}
}
问题分析: 1. UserService类承担了用户管理、日志记录、邮件发送三个职责 2. 修改日志格式需要修改UserService 3. 更换邮件服务提供商需要修改UserService 4. 这违反了单一职责原则,导致类的变化原因过多

正例代码

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
/**
* 符合单一职责原则的用户服务类
* 只负责用户相关的业务逻辑
*/
public class UserService {
private Logger logger;
private EmailService emailService;

public UserService(Logger logger, EmailService emailService) {
this.logger = logger;
this.emailService = emailService;
}

// 只关注用户管理职责
public void register(String username, String password) {
logger.log("用户注册:" + username);
// 注册逻辑
emailService.sendEmail(username, "注册成功", "欢迎注册");
}

public void login(String username, String password) {
logger.log("用户登录:" + username);
// 登录逻辑
}
}

/**
* 专门的日志服务类
*/
public class Logger {
public void log(String message) {
try {
FileWriter writer = new FileWriter("app.log", true);
writer.write(message + "\n");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 专门的邮件服务类
*/
public class EmailService {
public void sendEmail(String to, String subject, String content) {
System.out.println("发送邮件给:" + to);
System.out.println("主题:" + subject);
System.out.println("内容:" + content);
}
}
重构收益: 1. 每个类都有明确的单一职责 2. 修改日志实现只需修改Logger类 3. 更换邮件服务只需修改EmailService类 4. UserService类保持稳定,不受其他职责变化影响

开闭原则(Open-Closed Principle,OCP)

原则定义

软件实体(类、模块、函数等)应该对扩展开放,对修改封闭

这意味着当我们需要添加新功能时,应该通过扩展现有代码来实现,而不是修改已有的代码。

直观理解

想象一个插线板,它有多个插座。当你需要使用新电器时,你只需要插上新的插头,而不需要拆开插线板重新布线。开闭原则就是让我们的代码像插线板一样,能够轻松”插入”新功能而不需要修改核心代码。

反例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 违反开闭原则的折扣计算器
* 每次新增折扣类型都需要修改这个类
*/
public class DiscountCalculator {

public double calculateDiscount(String discountType, double price) {
switch (discountType) {
case "NONE":
return price;
case "TEN_PERCENT":
return price * 0.9;
case "TWENTY_PERCENT":
return price * 0.8;
// 每次新增折扣类型都需要在这里添加新的case
// 这违反了开闭原则
default:
return price;
}
}
}
违反开闭原则的后果: 1. 每次添加新的折扣类型都需要修改DiscountCalculator类 2. 修改已有代码可能引入新的bug 3. 需要重新测试整个折扣计算功能 4. 违反了对修改封闭的原则

正例代码

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* 折扣策略接口
*/
public interface DiscountStrategy {
double calculate(double price);
}

/**
* 无折扣策略
*/
public class NoDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price;
}
}

/**
* 10%折扣策略
*/
public class TenPercentDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.9;
}
}

/**
* 20%折扣策略
*/
public class TwentyPercentDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.8;
}
}

/**
* 符合开闭原则的折扣计算器
*/
public class DiscountCalculator {
private DiscountStrategy discountStrategy;

public DiscountCalculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}

public double calculate(double price) {
return discountStrategy.calculate(price);
}

// 可以动态切换折扣策略
public void setDiscountStrategy(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
}

// 使用示例
public class Main {
public static void main(String[] args) {
DiscountCalculator calculator = new DiscountCalculator(new NoDiscount());
System.out.println("无折扣价格:" + calculator.calculate(100));

// 切换到10%折扣
calculator.setDiscountStrategy(new TenPercentDiscount());
System.out.println("10%折扣后价格:" + calculator.calculate(100));

// 切换到20%折扣
calculator.setDiscountStrategy(new TwentyPercentDiscount());
System.out.println("20%折扣后价格:" + calculator.calculate(100));

// 添加新的折扣类型只需创建新的策略类,无需修改DiscountCalculator
calculator.setDiscountStrategy(new ThirtyPercentDiscount());
System.out.println("30%折扣后价格:" + calculator.calculate(100));
}
}

/**
* 新增30%折扣策略 - 无需修改任何现有代码
*/
public class ThirtyPercentDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.7;
}
}
重构收益: 1. 添加新的折扣类型只需创建新的策略类 2. DiscountCalculator类无需修改,符合开闭原则 3. 通过策略模式实现了对扩展开放、对修改封闭 4. 每个折扣策略都是独立的,易于测试和维护

里氏替换原则(Liskov Substitution Principle,LSP)

原则定义

所有引用基类的地方必须能够透明地使用其子类的对象,子类可以替换父类出现在父类能够出现的任何地方,而不破坏程序的正确性。

直观理解

如果你有一个正方形和一个长方形,从几何上讲,正方形是特殊的长方形。但在编程中,如果让正方形继承长方形类,可能会出现问题。因为正方形的长宽必须相等,这违反了长方形”长宽可以不同”的基本约定。

反例代码

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 长方形类
*/
public class Rectangle {
protected double width;
protected double height;

public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}

public void setWidth(double width) {
this.width = width;
}

public void setHeight(double height) {
this.height = height;
}

public double getWidth() {
return width;
}

public double getHeight() {
return height;
}

public double getArea() {
return width * height;
}
}

/**
* 正方形类继承长方形
* 这违反了里氏替换原则
*/
public class Square extends Rectangle {

public Square(double size) {
super(size, size);
}

@Override
public void setWidth(double width) {
this.width = width;
this.height = width; // 正方形长宽必须相等
}

@Override
public void setHeight(double height) {
this.width = height; // 正方形长宽必须相等
this.height = height;
}
}

/**
* 测试类
*/
public class LSPTest {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(5, 10);
System.out.println("长方形面积:" + rectangle.getArea()); // 50

rectangle.setWidth(10);
rectangle.setHeight(5);
System.out.println("长方形面积:" + rectangle.getArea()); // 50

// 使用正方形替换长方形
Rectangle square = new Square(5);
System.out.println("正方形面积:" + square.getArea()); // 25

square.setWidth(10);
square.setHeight(5);
System.out.println("正方形面积:" + square.getArea()); // 25, 而不是50!

// 这里违反了里氏替换原则
// 当使用Square替换Rectangle时,程序的行为发生了变化
}
}
违反里氏替换原则的问题: 1. Square继承Rectangle后,破坏了Rectangle的行为约定 2. 当使用Square替换Rectangle时,程序结果不一致 3. 客户端代码无法正确预测继承后的行为 4. 这种继承关系在设计上就是错误的

正例代码

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 形状接口
*/
public interface Shape {
double getArea();
}

/**
* 长方形类实现形状接口
*/
public class Rectangle implements Shape {
private double width;
private double height;

public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}

public void setWidth(double width) {
this.width = width;
}

public void setHeight(double height) {
this.height = height;
}

@Override
public double getArea() {
return width * height;
}
}

/**
* 正方形类实现形状接口
*/
public class Square implements Shape {
private double size;

public Square(double size) {
this.size = size;
}

public void setSize(double size) {
this.size = size;
}

@Override
public double getArea() {
return size * size;
}
}

/**
* 形状工具类 - 可以处理任何实现Shape接口的对象
*/
public class ShapeUtils {
public static void printArea(Shape shape) {
System.out.println("形状面积:" + shape.getArea());
}
}

/**
* 测试类
*/
public class LSPTestCorrect {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(5, 10);
Square square = new Square(5);

// Rectangle和Square可以透明替换
ShapeUtils.printArea(rectangle); // 形状面积:50.0
ShapeUtils.printArea(square); // 形状面积:25.0

// 使用多态
List<Shape> shapes = Arrays.asList(rectangle, square);
for (Shape shape : shapes) {
ShapeUtils.printArea(shape);
}
}
}
重构收益: 1. Rectangle和Square都实现Shape接口,各自保持独立性 2. 任何使用Shape的地方都可以透明地使用Rectangle或Square 3. 避免了不合理的继承关系 4. 符合里氏替换原则,保证程序行为的正确性

接口隔离原则(Interface Segregation Principle,ISP)

原则定义

客户端不应该被迫依赖于它不使用的接口,接口应该被拆分为更小和更具体的部分,这样客户端只需要知道它们所需的部分。

直观理解

想象一个万能遥控器,上面有电视、空调、音响等各种设备的按钮。当你只需要控制电视时,面对这么多无关的按钮会很困扰。接口隔离原则就是让每个接口都专注于特定的功能,避免”胖接口”。

反例代码

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
/**
* 臃肿的机器接口 - 包含所有可能的操作
*/
public interface Machine {
void print(String document);
void fax(String document);
void scan(String document);
void photocopy(String document);
}

/**
* 打印机类 - 被迫实现不需要的方法
*/
public class OldPrinter implements Machine {

@Override
public void print(String document) {
System.out.println("打印:" + document);
}

@Override
public void fax(String document) {
// 老式打印机不支持传真,但被迫实现这个方法
throw new UnsupportedOperationException("不支持传真功能");
}

@Override
public void scan(String document) {
// 老式打印机不支持扫描,但被迫实现这个方法
throw new UnsupportedOperationException("不支持扫描功能");
}

@Override
public void photocopy(String document) {
// 老式打印机不支持复印,但被迫实现这个方法
throw new UnsupportedOperationException("不支持复印功能");
}
}
违反接口隔离原则的问题: 1. Machine接口过于臃肿,包含了太多操作 2. OldPrinter只需要打印功能,但被迫实现其他方法 3. 客户端可能调用不支持的方法,导致运行时异常 4. 接口设计不够灵活,无法适应不同的设备组合

正例代码

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* 打印接口
*/
public interface Printer {
void print(String document);
}

/**
* 传真接口
*/
public interface Fax {
void fax(String document);
}

/**
* 扫描接口
*/
public interface Scanner {
void scan(String document);
}

/**
* 复印接口
*/
public interface Photocopier {
void photocopy(String document);
}

/**
* 简单打印机 - 只实现打印功能
*/
public class SimplePrinter implements Printer {
@Override
public void print(String document) {
System.out.println("打印:" + document);
}
}

/**
* 多功能打印机 - 实现多个接口
*/
public class MultiFunctionPrinter implements Printer, Fax, Scanner, Photocopier {

@Override
public void print(String document) {
System.out.println("打印:" + document);
}

@Override
public void fax(String document) {
System.out.println("传真:" + document);
}

@Override
public void scan(String document) {
System.out.println("扫描:" + document);
}

@Override
public void photocopy(String document) {
System.out.println("复印:" + document);
}
}

/**
* 打印扫描一体机 - 只实现需要的接口
*/
public class PrintScanCombo implements Printer, Scanner {

@Override
public void print(String document) {
System.out.println("打印:" + document);
}

@Override
public void scan(String document) {
System.out.println("扫描:" + document);
}
}

// 使用示例
public class ISPTest {
public static void main(String[] args) {
// 简单打印机只需要Printer接口
Printer simplePrinter = new SimplePrinter();
simplePrinter.print("简单文档");

// 多功能打印机可以实现所有功能
MultiFunctionPrinter mfp = new MultiFunctionPrinter();
usePrinter(mfp);
useFax(mfp);
useScanner(mfp);

// 打印扫描一体机只需要打印和扫描功能
PrintScanCombo combo = new PrintScanCombo();
usePrinter(combo);
useScanner(combo);
}

// 客户端只需要依赖它需要的接口
public static void usePrinter(Printer printer) {
printer.print("使用打印机");
}

public static void useFax(Fax fax) {
fax.fax("使用传真机");
}

public static void useScanner(Scanner scanner) {
scanner.scan("使用扫描仪");
}
}
重构收益: 1. 接口被拆分为多个小而专注的接口 2. 客户端只依赖它需要的接口,避免不必要的方法 3. 实现类可以选择性地实现需要的接口 4. 符合接口隔离原则,提高了系统的灵活性和可维护性

依赖倒置原则(Dependency Inversion Principle,DIP)

原则定义

高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。

直观理解

想象一个公司组织架构,总经理(高层)不应该直接管理员工(低层)的每一个具体工作。相反,总经理应该制定标准和接口,员工按照这些标准工作。这样,更换员工不会影响公司的整体运作。

反例代码

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
/**
* MySQL数据库连接类 - 低层模块
*/
public class MySQLConnection {
public void connect() {
System.out.println("连接到MySQL数据库");
}

public void executeQuery(String sql) {
System.out.println("在MySQL中执行查询:" + sql);
}
}

/**
* 用户服务类 - 高层模块
* 直接依赖具体的MySQLConnection
*/
public class UserService {
private MySQLConnection dbConnection;

public UserService() {
this.dbConnection = new MySQLConnection(); // 高层依赖低层具体实现
}

public void getUser(String userId) {
dbConnection.connect();
dbConnection.executeQuery("SELECT * FROM users WHERE id = " + userId);
}
}
违反依赖倒置原则的问题: 1. UserService直接依赖MySQLConnection具体类 2. 如果要更换数据库(如PostgreSQL),需要修改UserService 3. 高层模块被低层模块的具体实现所束缚 4. 难以进行单元测试(无法mock数据库连接)

正例代码

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* 数据库连接接口 - 抽象层
*/
public interface DatabaseConnection {
void connect();
void executeQuery(String sql);
}

/**
* MySQL数据库连接实现
*/
public class MySQLConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("连接到MySQL数据库");
}

@Override
public void executeQuery(String sql) {
System.out.println("在MySQL中执行查询:" + sql);
}
}

/**
* PostgreSQL数据库连接实现
*/
public class PostgreSQLConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("连接到PostgreSQL数据库");
}

@Override
public void executeQuery(String sql) {
System.out.println("在PostgreSQL中执行查询:" + sql);
}
}

/**
* MongoDB数据库连接实现
*/
public class MongoDBConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("连接到MongoDB数据库");
}

@Override
public void executeQuery(String sql) {
System.out.println("在MongoDB中执行查询:" + sql);
}
}

/**
* 符合依赖倒置原则的用户服务类
* 依赖抽象接口,而不是具体实现
*/
public class UserService {
private DatabaseConnection dbConnection;

// 通过构造器注入依赖
public UserService(DatabaseConnection dbConnection) {
this.dbConnection = dbConnection; // 依赖抽象而非具体实现
}

public void getUser(String userId) {
dbConnection.connect();
dbConnection.executeQuery("SELECT * FROM users WHERE id = " + userId);
}

// 也可以通过setter注入
public void setDbConnection(DatabaseConnection dbConnection) {
this.dbConnection = dbConnection;
}
}

// 使用示例
public class DIPTest {
public static void main(String[] args) {
// 可以轻松切换不同的数据库实现
DatabaseConnection mysqlConnection = new MySQLConnection();
DatabaseConnection pgConnection = new PostgreSQLConnection();
DatabaseConnection mongoConnection = new MongoDBConnection();

UserService userService1 = new UserService(mysqlConnection);
userService1.getUser("1");

UserService userService2 = new UserService(pgConnection);
userService2.getUser("2");

UserService userService3 = new UserService(mongoConnection);
userService3.getUser("3");

// 运行时切换数据库实现
userService1.setDbConnection(mongoConnection);
userService1.getUser("4");
}
}
重构收益: 1. UserService依赖DatabaseConnection抽象接口,而非具体实现 2. 可以轻松切换不同的数据库实现,无需修改UserService 3. 高层模块和低层模块都依赖抽象,降低了耦合度 4. 便于单元测试,可以轻松mock DatabaseConnection

面试中被问到SOLID原则怎么办?

有次在Reddit上看到一个吐槽帖,说某公司招聘人员让候选人”凭空解释5个SOLID原则”,结果博主直接拒绝回答。其实这问题挺常见的,尤其是面试初级到中级岗位的时候。

答题思路

别死记硬背。先说总体理解,然后逐个简述:

原则 一句话解释
S 一个类只做一件事
O 加功能别改老代码,用扩展加
L 子类能完美替代父类
I 接口别太臃肿,该拆就拆
D 依赖接口,别依赖具体实现

然后补一句:”这些原则我实际项目里也在用,比如最近做的XX项目,用策略模式实现XX功能,就体现了开闭原则…”

这样回答既展示了理论知识,又证明了实践经验。

记忆技巧

  • SRP:Single Responsibility → 单一职责
  • OCP:Open/Closed → 对扩展开放,对修改封闭
  • LSP:Liskov Substitution → 里氏替换
  • ISP:Interface Segregation → 接口隔离
  • DIP:Dependency Inversion → 依赖倒置(依赖抽象)

总结

SOLID原则是面向对象设计的基石,它们相互关联、相互补充。掌握这些原则能够帮助我们设计出更加优雅、灵活、可维护的软件系统。

SOLID原则的关系图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────────┐
│ SOLID 原则 │
├─────────────────────────────────────────────────┤
│ │
│ SRP ──────── 单一职责 ───────── 一个类只有一个职责 │
│ ↓ │
│ OCP ──────── 开闭原则 ────── 对扩展开放,对修改封闭 │
│ ↓ │
│ LSP ──────── 里氏替换 ──── 子类可以透明替换父类 │
│ ↓ │
│ ISP ──────── 接口隔离 ──── 接口应该小而专注 │
│ ↓ │
│ DIP ──────── 依赖倒置 ── 依赖抽象而非具体实现 │
│ │
└─────────────────────────────────────────────────┘

实践建议

如何在项目中应用SOLID原则:

  1. 逐步重构:不要试图一次性重构所有代码,逐步应用这些原则
  2. 识别坏味道:学会识别违反SOLID原则的代码坏味道
  3. 设计优先:在编写新代码时,优先考虑SOLID原则
  4. 团队共识:确保团队成员都理解并认同这些原则
  5. 适度应用:不要过度设计,根据实际情况灵活应用
重要提示: SOLID原则是指导原则,不是绝对的规则。在实际项目中,需要根据具体情况灵活应用。过度遵循这些原则可能导致过度设计,增加系统复杂度。

架构师学习 第3篇

设计模式 (Design Patterns)

  • 核心概念与原则

    • 定义:一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
    • 目的:提高代码的可重用性,使代码更容易被他人理解,保证代码可靠性。
    • 核心原则
      • 针对接口编程:客户无须知道对象的特定类型,只需知道对象有客户所期望的接口。
      • 优先使用对象组合:优先使用对象组合(黑箱复用),而不是类继承(白箱复用)。
    • MVC模式案例:Smalltalk中的MVC(模型/视图/控制器)体现了观察者、组合和策略模式的综合应用。
  • 一、创建型模式 (Creational Patterns)

    • 关注点:对象的创建过程,将对象的创建与使用分离。
    • 1. 工厂模式 (Factory)
      • **简单工厂 (Simple Factory)**:(非GoF标准,但常用) 定义一个用于创建对象的接口,由工厂类决定创建哪一种产品实例(如“司机开车”的例子)。
      • **工厂方法 (Factory Method)**:定义创建对象的接口,让子类决定实例化哪一个类。使实例化延迟到子类。
      • **抽象工厂 (Abstract Factory)**:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
    • 2. 单例模式 (Singleton)
      • 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
      • 实现方式
        • 饿汉式:类加载时初始化。
        • 懒汉式:第一次使用时初始化(需注意线程同步)。
        • 注册表方式:通过HashMap维护实例。
    • 3. 建造者模式 (Builder)
      • 定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
      • 角色:指导者 (Director)、抽象建造者 (Builder)、具体建造者 (ConcreteBuilder)、产品 (Product)。
    • 4. 原型模式 (Prototype)
      • 定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
      • 特点:利用Java的clone()方法,分为深克隆和浅克隆。
  • 二、结构型模式 (Structural Patterns)

    • 关注点:类或对象的组合,形成更大的结构。
    • 1. 适配器模式 (Adapter)
      • 定义:将一个类的接口转换成客户希望的另外一个接口,解决接口不兼容问题。
      • 分类:类适配器(继承)、对象适配器(组合)。
    • 2. 桥接模式 (Bridge)
      • 定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
      • 应用:如Java AWT框架,将组件与其在不同操作系统下的实现分离。
    • 3. 组合模式 (Composite)
      • 定义:将对象组合成树形结构以表示“部分-整体”的层次结构,使用户对单个对象和组合对象的使用具有一致性。
      • 应用:文件系统、JUnit中的TestCase与TestSuite。
    • 4. 装饰模式 (Decorator)
      • 定义:动态地给一个对象添加一些额外的职责。比生成子类更为灵活。
      • 特点:透明围栏,客户分不出组件和装饰后的组件的区别。
    • 5. 外观/门面模式 (Facade)
      • 定义:为子系统中的一组接口提供一个一致的界面,定义高层接口使子系统更易使用。
      • 目的:降低客户与子系统之间的耦合。
    • 6. 享元模式 (Flyweight)
      • 定义:运用共享技术有效地支持大量细粒度的对象。
      • 关键:区分内蕴状态(共享)和外蕴状态(不共享)。
    • 7. 代理模式 (Proxy)
      • 定义:为其他对象提供一种代理以控制对这个对象的访问。
      • 类型:远程代理、虚拟代理、保护代理、智能引用等。
  • 三、行为型模式 (Behavioral Patterns)

    • 关注点:对象间的交互和职责分配。
    • 1. 责任链模式 (Chain of Responsibility)
      • 定义:使多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链传递请求,直到有对象处理它。
    • 2. 命令模式 (Command)
      • 定义:将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;支持排队、日志和撤销操作。
    • 3. 解释器模式 (Interpreter)
      • 定义:给定一个语言,定义它的文法表示,并定义一个解释器来解释语言中的句子。
    • 4. 迭代器模式 (Iterator)
      • 定义:提供一种方法顺序访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。
    • 5. 中介者/调停者模式 (Mediator)
      • 定义:用一个中介对象来封装一系列的对象交互,使各对象不需要显式地相互引用。
    • 6. 备忘录模式 (Memento)
      • 定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
    • 7. 观察者模式 (Observer)
      • 定义:定义对象间的一种一对多的依赖关系,当一个对象状态改变时,所有依赖者都得到通知并自动更新。
      • 模型:推模型(广播详情) vs 拉模型(观察者主动获取)。
    • 8. 状态模式 (State)
      • 定义:允许一个对象在其内部状态改变时改变它的行为。
      • 对比:与策略模式结构相似,但意图不同(状态是内在变化,策略是外部选择)。
    • 9. 策略模式 (Strategy)
      • 定义:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。
    • 10. 模板方法模式 (Template Method)
      • 定义:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
    • 11. 访问者模式 (Visitor)
      • 定义:表示一个作用于某对象结构中的各元素的操作,使你可以在不改变各元素的类的前提下定义新操作。
      • 机制:依赖于“双重分派”技术。

常用的设计模式(10个)

一、 创建型模式 (Creational Patterns)

这类模式主要关注对象的创建过程,旨在将对象的创建与使用分离。

1. 单例模式 (Singleton)

  • 概念:保证一个类仅有一个实例,并提供一个访问它的全局访问点。通常用于代表系统中本质上唯一的组件。
  • 示例
    • 系统资源管理:如文件系统、打印机假脱机程序或窗口管理器,在系统中通常只应有一个实例存在。
    • 代码实现:可以通过私有化构造函数,并提供一个静态方法(如 getInstance)来返回唯一的实例(可以是饿汉式或懒汉式实现)。

2. 工厂方法模式 (Factory Method)

  • 概念:定义一个用于创建对象的接口,让子类决定实例化哪一个类。这使得一个类的实例化延迟到其子类。
  • 示例
    • 文档应用框架:一个抽象的 Application 类负责管理文档,但它不知道具体的文档类(如 DrawingDocumentTextDocument)。它定义一个 CreateDocument 的工厂方法,由子类来实现具体的文档创建逻辑。
    • 暴发户坐车:在这个例子中,工厂方法模式用来创建不同品牌的汽车(如奔驰、宝马),不同的司机子类(工厂子类)负责创建对应的汽车实例。

3. 抽象工厂模式 (Abstract Factory)

  • 概念:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。主要用于处理产品族的问题。
  • 示例
    • 多视感标准 UI:支持多种界面风格(如 Motif 和 Presentation Manager)的工具包。定义一个 WidgetFactory 接口,包含创建滚动条、窗口、按钮的操作。具体的子类 MotifWidgetFactory 创建 Motif 风格的组件,而 PMWidgetFactory 创建 PM 风格的组件,客户仅需通过抽象接口与工厂交互。

二、 结构型模式 (Structural Patterns)

这类模式关注类和对象的组合。

4. 适配器模式 (Adapter)

  • 概念:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
  • 示例
    • 绘图编辑器:想要复用一个已有的 TextView 类来显示文本,但它的接口与编辑器期望的 Shape 接口不匹配。可以定义一个 TextShape 类(适配器),它继承 Shape 的接口并持有一个 TextView 的实例,将 Shape 的请求(如 BoundingBox)转换为 TextView 的对应操作(如 GetExtent)。
    • USB 转接口:给只提供 USB 充电口的 MP3 播放器配一个充电器转接头,使其能通过普通电源充电。

5. 装饰模式 (Decorator)

  • 概念:动态地给一个对象添加一些额外的职责。相比生成子类,这种方式更为灵活。
  • 示例
    • 图形界面组件:一个文本显示视图 TextView 缺省没有滚动条。如果需要添加滚动条或边框,无需创建子类,而是将 TextView 放入 ScrollDecoratorBorderDecorator 中。对客户而言,装饰后的对象仍是可视组件,但拥有了新功能。
    • JUnit 测试TestDecorator 可以给测试用例添加额外行为,例如 RepeatedTest 装饰器可以让一个测试用例重复运行多次。

6. 代理模式 (Proxy)

  • 概念:为其他对象提供一种代理以控制对这个对象的访问。代理可以在访问实体前进行预处理或控制。
  • 示例
    • **图片懒加载 (虚代理)**:文档编辑器打开包含大型图片的文档时,为了速度不立即加载图片,而是先创建一个 ImageProxy 替代。只有当用户滚动到该图片需要显示时,代理才真正创建并加载图像对象。
    • **权限控制 (保护代理)**:在论坛系统中,通过代理对象判断用户权限(如注册用户与游客),控制是否允许执行“发帖”等操作。

7. 组合模式 (Composite)

  • 概念:将对象组合成树形结构以表示“部分-整体”的层次结构,使用户对单个对象和组合对象的使用具有一致性。
  • 示例
    • 图形系统Picture(组合对象)可以包含 LineRectangle(基本对象)或其他 Picture。用户可以对整个 Picture 调用 Draw 操作,它会自动递归调用所有子部件的 Draw
    • JUnitTestSuite 可以包含多个 TestCase 或其他 TestSuite,运行 TestSuite 时会自动运行其包含的所有测试。

三、 行为型模式 (Behavioral Patterns)

这类模式关注对象间的通信、职责分配和算法封装。

8. 策略模式 (Strategy)

  • 概念:定义一系列算法,把它们封装起来,并且使它们可相互替换。该模式让算法独立于使用它的客户而变化。
  • 示例
    • 文本换行算法:一个文本排版系统可能支持多种换行策略(如简单换行、TeX 优化换行、数组式换行)。将这些算法封装在不同的 Compositor 子类中,排版对象 Composition 可以根据需要动态切换使用的策略。
    • 布局管理器:Java AWT 中的 LayoutManager 接口有多种实现(FlowLayout, GridLayout),容器将布局行为委托给具体的策略对象。

9. 观察者模式 (Observer)

  • 概念:定义对象间的一种一对多的依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
  • 示例
    • 数据与图表:一个电子表格数据对象(目标)可能有多个展示图表(观察者,如柱状图、饼图)。当数据改变时,数据对象通知所有图表,图表自动重绘以反映最新数据。
    • JUnitTestResult 维护一个 TestListener 列表。当测试失败或结束时,它会通知所有注册的监听器(如打印结果的界面)。

10. 模板方法模式 (Template Method)

  • 概念:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
  • 示例
    • 打开文档:抽象类 Application 定义了 OpenDocument 的流程(检查文档、创建对象、读取数据),其中具体的步骤如 DoCreateDocumentDoRead 由子类实现,但整体流程由父类控制。
    • JUnit 测试运行TestCase 类定义了 runBare 方法,依次执行 setUp(初始化)、runTest(运行测试)、tearDown(清理)。用户只需要重写这三个步骤的具体实现,而无需改变执行顺序。

资料

https://refactoringguru.cn/design-patterns
https://design-patterns.readthedocs.io/zh-cn/latest/
https://jueee.github.io/design-patterns/

架构师学习 第2篇
## **写在前面**
DDD 我在上一家公司的时候就专门学习过,也做了一些实践,可能因为我当时没有太深入的了解,所以总觉得好像有些重,有一种概念大于实操的感觉,不过有些概念定义对我还是很有用的,刚好当下做架构回过头再好好学习下

示例

构建一个在线电子商务系统(E-Shop)的设计示例。这个示例将从宏观的战略设计(如何划分系统边界)到微观的战术设计(代码层面的核心元素),全面展示 DDD 的核心思想。

1. 战略设计:界定的上下文 (Bounded Contexts)

DDD 的首要原则是软件必须植根于领域,并且模型需要有清晰的边界。

场景: 假设我们要构建一个大型在线商店,不仅涉及用户下单,还需要处理库存发货和销售报表。

传统问题: 许多团队会试图创建一个包含所有属性(如用户、订单、商品、库存、报表)的单一庞大模型。这会导致模型臃肿,不同职能的团队互相干扰。

DDD 解决方案:
我们将系统划分为两个独立的界定的上下文(Bounded Contexts),:

  1. 在线交易上下文(E-Shop Context): 关注客户下单、购物车、结账。这里的“商品”关注价格和描述。
  2. 报表上下文(Reporting Context): 关注销售趋势、库存周转。这里的“商品”可能只关注销售数量和成本,不需要描述信息。

核心思想:

  • 消除歧义: 同一个词(如“商品”)在不同上下文中可能有不同的含义和属性。通过划分上下文,我们可以保证模型在各自边界内的纯洁性和一致性。
  • 上下文映射(Context Map): 我们定义这两个上下文的关系。例如,报表系统需要从交易系统获取数据,它们可能通过客户-供应商(Customer-Supplier)模式交互,或者通过防崩溃层(Anticorruption Layer)来转换数据,确保报表系统的模型不受交易系统模型变更的直接破坏,。

2. 战术设计:领域模型的核心要素

在“在线交易上下文”内部,我们使用战术模式来构建领域模型。

A. 通用语言 (Ubiquitous Language)

开发人员与业务专家(如销售经理)共同制定一套语言。

  • 示例: 大家不再说“插入一条记录到订单表”,而是统一说“提交订单(Submit Order)”。
  • 价值: 这消除了沟通障碍,代码中的类名和方法名将直接反映业务意图,。

B. 分层架构 (Layered Architecture)

为了隔离关注点,我们将系统分为四层,:

  1. 用户界面层: 展示商品页面,接收用户点击。
  2. 应用层: 协调任务(如“协调结账流程”),但不包含业务逻辑。
  3. 领域层(核心): 包含 OrderCustomer 等业务对象和规则。这是软件的心脏
  4. 基础设施层: 处理数据库持久化、发送邮件等技术实现。

C. 实体 (Entities) 与 值对象 (Value Objects)

这是领域模型的基本构建块。

  • 实体(Entity):

    • 示例: Order(订单)。
    • 设计理由: 订单有生命周期(从创建到支付到发货),并且需要被追踪。即使两个订单的内容完全一样,只要 ID 不同,它们就是不同的对象。因此,Order 是一个实体,必须有唯一的标识符(Identity),。
  • 值对象(Value Object):

    • 示例: Address(送货地址)。
    • 设计理由: 我们只关心地址的属性(街道、城市),而不关心它的唯一标识。如果两个客户住在同一地址,这在业务上是等价的。Address 应该是不可变的(Immutable),如果客户搬家了,我们是用一个新的 Address 对象替换旧的,而不是修改旧对象,。

D. 聚合 (Aggregates)

为了保证数据一致性,我们需要划定修改数据的边界。

  • 示例: 一个 Order(订单)可能包含多个 OrderItem(订单项)。
  • 设计: Order 是这个聚合的根(Aggregate Root)
  • 规则: 外部对象只能引用根(Order),不能直接引用内部的 OrderItem。如果想修改某个订单项的数量,必须通过根的方法(如 order.updateItemQuantity())来进行。这确保了订单总价等不变量(Invariants)在修改过程中始终保持一致,。

E. 服务 (Services)

有些动作不属于特定的对象。

  • 示例: CheckoutService(结账服务)或 FundTransferService(转账服务)。
  • 设计理由: 结账可能涉及订单状态更新、库存扣减、支付网关调用等。这些行为放入 OrderCustomer 都不合适,因此我们创建一个无状态的领域服务来封装这些操作,。

F. 资源库 (Repositories)

为了解耦领域模型与数据库。

  • 示例: OrderRepository
  • 设计: 领域层只定义接口 findOrder(id),不关心底层是 SQL Server 还是 Oracle。资源库负责从数据库中检索数据并将其重建为领域对象(如 Order 聚合)。这让开发人员可以像从内存集合中获取对象一样获取领域对象,而无需在业务逻辑中编写 SQL,。

3. 系统运作流程示例

结合上述概念,一个“用户修改收货地址”的业务场景在代码设计中如下流转:

  1. 应用层接收请求,调用资源库CustomerRepository)。
  2. 资源库利用基础设施层从数据库检索数据,重建 Customer 聚合(实体)。
  3. 应用层调用 Customer 实体的业务方法(如 customer.moveTo(newAddress))。
  4. 在方法内部,Customer 实体将旧的 Address 值对象替换为新的 Address 值对象
  5. 资源库将更新后的 Customer 聚合保存回数据库。

总结与比喻

这个设计示例体现了 DDD 的核心:通过将软件实现与业务领域模型紧密绑定,来应对复杂性。

为了巩固理解,我们可以用书中提到的汽车制造来类比这个系统设计:

  • 领域模型就像是汽车的设计蓝图。工人在造车前必须先有精准的图纸,同样,开发软件前必须先理解并建模业务领域。
  • 实体与聚合就像是汽车的发动机和底盘。它们是核心部件,有独立的标识和生命周期,必须作为一个整体来组装和维护。
  • 分层架构就像是汽车的不同系统(传动系统、电子系统、内饰)。内饰(UI)的变化不应直接影响发动机(领域逻辑)的运作。
  • 通用语言就像是工程师团队之间的技术术语。如果有人把“方向盘”叫成“转弯器”,制造过程就会混乱;同样,代码必须精确使用业务术语。

学习资料

https://github.com/Sairyss/domain-driven-hexagon?tab=readme-ov-file

AI提效 Claude Sonnet 4
最近产品上线的前期准备,小团队+AI编程,各项工作几乎手撮。因为产品投入市场后很可能会有一个迅速从100到10000的过程,所以我要前置考虑一些事情,慢慢 查漏补缺。此篇是我对于数据灾备的参考播客之一。基于ByteByteGo博客+一些实操+AI辅助而输出该博客。

数据库复制指南:核心概念与策略

Database Replication Guide: Key Concepts and Strategies

引言 | Introduction

每个现代应用程序都依赖于数据,用户期望数据快速、实时且始终可访问。然而,数据库并不是魔法,它们可能会失败或在负载下变慢。它们也会遇到物理和地理限制,这就是复制变得必要的地方。

Every modern application relies on data, and users expect that data to be fast, current, and always accessible. However, databases are not magic. They can fail or slow down under load. They can also encounter physical and geographic limits, which is where replication becomes necessary.

数据库复制意味着在多台机器上保持相同数据的副本。这些机器可以位于同一个数据中心,也可以分布在全球各地。目标很简单:

  • 提高容错性
  • 扩展读取能力
  • 通过将数据移近需要的地方来减少延迟

Database Replication means keeping copies of the same data across multiple machines. These machines can sit in the same data center or be spread across the globe. The goal is straightforward:

  • Increase fault tolerance
  • Scale reads
  • Reduce latency by bringing data closer to where it’s needed

复制的重要性 | The Importance of Replication

复制是任何旨在在不丢失数据或令用户失望的情况下从故障中恢复的系统的核心。无论是毫秒级更新的社交动态、处理限时抢购的电商网站,还是处理全球交易的金融系统,复制确保系统即使在部分组件故障时也能继续运行。

Replication sits at the heart of any system that aims to survive failures without losing data or disappointing users. Whether it’s a social feed updating in milliseconds, an e-commerce site handling flash sales, or a financial system processing global transactions, replication ensures the system continues to operate, even when parts of it break.

然而,复制也带来了复杂性。它迫使我们在一致性、可用性和性能之间做出艰难的决定。数据库可能正常运行,但滞后的副本仍可能提供过时的数据。网络分区可能使两个主节点认为它们在负责,导致脑裂写入。围绕这些问题进行设计并非易事。

However, replication also introduces complexity. It forces difficult decisions around consistency, availability, and performance. The database might be up, but a lagging replica can still serve stale data. A network partition might make two leader nodes think they’re in charge, leading to split-brain writes. Designing around these issues is non-trivial.

复制策略概述 | Overview of Replication Strategies

在分布式数据库中,有三种主要的复制策略:

In distributed databases, there are three main replication strategies:

1. 单主复制 (Single-Leader Replication)

工作原理 | How It Works:

  • 一个主节点接收所有写入操作
  • 主节点将更改复制到多个从节点
  • 从节点提供读取服务

优势 | Advantages:

  • 简单且易于理解
  • 强一致性保证
  • 避免写入冲突

劣势 | Disadvantages:

  • 主节点成为单点故障

  • 写入性能受限于单个节点

  • 主节点故障时需要故障转移

  • One primary node accepts all writes

  • Primary replicates changes to multiple secondary nodes

  • Secondary nodes serve read requests

  • Simple and easy to understand

  • Strong consistency guarantees

  • Avoids write conflicts

  • Primary node becomes a single point of failure

  • Write performance limited to single node

  • Requires failover when primary fails

2. 多主复制 (Multi-Leader Replication)

工作原理 | How It Works:

  • 多个主节点可以接受写入
  • 主节点之间相互复制更改
  • 需要冲突检测和解决机制

优势 | Advantages:

  • 高写入可用性
  • 更好的性能和容错性
  • 适合多数据中心部署

劣势 | Disadvantages:

  • 写入冲突需要解决

  • 复杂的一致性模型

  • 需要冲突解决策略

  • Multiple primary nodes can accept writes

  • Primaries replicate changes to each other

  • Requires conflict detection and resolution

  • High write availability

  • Better performance and fault tolerance

  • Suitable for multi-datacenter deployments

  • Write conflicts need resolution

  • Complex consistency model

  • Requires conflict resolution strategies

3. 无主复制 (Leaderless Replication)

工作原理 | How It Works:

  • 所有副本都是对等的
  • 客户端可以向任何副本写入
  • 使用仲裁机制确保一致性

优势 | Advantages:

  • 高可用性
  • 简单的故障处理
  • 良好的可扩展性

劣势 | Disadvantages:

  • 最终一致性

  • 复杂的读取修复

  • 需要仲裁机制

  • All replicas are peers

  • Clients can write to any replica

  • Uses quorum mechanisms for consistency

  • High availability

  • Simple failure handling

  • Good scalability

  • Eventual consistency

  • Complex read repair

  • Requires quorum mechanisms

复制延迟的挑战 | Challenges of Replication Lag

复制延迟是分布式数据库面临的一个关键挑战。当主节点接收写入并将更改传播到副本时,存在时间延迟。这种延迟可能导致:

Replication lag is a key challenge faced by distributed databases. When the primary node receives a write and propagates changes to replicas, there’s a time delay. This lag can lead to:

读取后写入不一致 | Read-After-Write Inconsistency

用户写入数据后立即读取可能看到旧数据。

Users might see stale data when reading immediately after writing.

单调读取问题 | Monotonic Read Issues

用户可能看到数据”倒退”,即先看到新数据后看到旧数据。

Users might see data “go backwards” - seeing newer data then older data.

因果关系违反 | Causality Violations

相关事件可能以错误的顺序出现。

Related events might appear in the wrong order.

选择合适的复制策略 | Choosing the Right Replication Strategy

何时选择单主复制 | When to Choose Single-Leader Replication

  • 需要强一致性的应用

  • 写入量相对较低

  • 简单的故障转移需求

  • Applications requiring strong consistency

  • Relatively low write volume

  • Simple failover requirements

何时选择多主复制 | When to Choose Multi-Leader Replication

  • 多数据中心部署

  • 高写入可用性需求

  • 可以容忍冲突解决的复杂性

  • Multi-datacenter deployments

  • High write availability requirements

  • Can tolerate conflict resolution complexity

何时选择无主复制 | When to Choose Leaderless Replication

  • 最终一致性可接受

  • 需要高可用性

  • 简单的扩展需求

  • Eventual consistency is acceptable

  • High availability is needed

  • Simple scaling requirements

实现考虑因素 | Implementation Considerations

一致性模型 | Consistency Models

  • 强一致性: 所有副本始终同步

  • 最终一致性: 副本最终会收敛

  • 因果一致性: 保持事件的因果关系

  • Strong Consistency: All replicas always in sync

  • Eventual Consistency: Replicas eventually converge

  • Causal Consistency: Maintains causality between events

冲突解决策略 | Conflict Resolution Strategies

  • 最后写入获胜 (LWW): 基于时间戳的简单策略

  • 应用层解决: 让应用程序处理冲突

  • 合并策略: 自动合并冲突的更改

  • Last Write Wins (LWW): Simple timestamp-based strategy

  • Application-level resolution: Let application handle conflicts

  • Merge strategies: Automatically merge conflicting changes

网络分区处理 | Network Partition Handling

  • CAP定理: 在一致性、可用性和分区容忍性之间选择

  • 脑裂预防: 使用仲裁和租约机制

  • 分区检测: 监控网络连接状态

  • CAP Theorem: Choose between consistency, availability, and partition tolerance

  • Split-brain prevention: Use quorum and lease mechanisms

  • Partition detection: Monitor network connectivity

现实世界的例子 | Real-World Examples

单主复制系统 | Single-Leader Systems

  • MySQL主从复制: 传统的主从架构

  • PostgreSQL流复制: 支持同步和异步复制

  • MongoDB副本集: 自动故障转移

  • MySQL Master-Slave: Traditional master-slave architecture

  • PostgreSQL Streaming: Supports sync and async replication

  • MongoDB Replica Sets: Automatic failover

多主复制系统 | Multi-Leader Systems

  • MySQL集群: 多主动主配置

  • CouchDB: 文档数据库的多主复制

  • Cassandra: 分布式NoSQL数据库

  • MySQL Cluster: Multi-active master configuration

  • CouchDB: Multi-master replication for document databases

  • Cassandra: Distributed NoSQL database

无主复制系统 | Leaderless Systems

  • Amazon DynamoDB: 无主键值存储

  • Apache Cassandra: 对等复制

  • Riak: 分布式键值存储

  • Amazon DynamoDB: Leaderless key-value store

  • Apache Cassandra: Peer-to-peer replication

  • Riak: Distributed key-value store

监控和维护 | Monitoring and Maintenance

关键指标 | Key Metrics

  • 复制延迟: 主副本之间的时间差

  • 吞吐量: 每秒处理的操作数

  • 可用性: 系统正常运行时间百分比

  • Replication Lag: Time difference between primary and replicas

  • Throughput: Operations processed per second

  • Availability: System uptime percentage

维护最佳实践 | Maintenance Best Practices

  • 定期备份和恢复测试

  • 监控复制状态

  • 计划故障转移演练

  • Regular backup and recovery testing

  • Monitor replication status

  • Plan failover drills

PostgreSQL复制实战经验 | PostgreSQL Replication Practical Experience

为什么选择PostgreSQL | Why Choose PostgreSQL

在实际项目中,PostgreSQL作为企业级开源数据库,在复制、扩展功能方面有着独特的优势。我的上一家公司的几个项目选用的就是PostgreSQL,有以下深刻体会:

In real projects, PostgreSQL as an enterprise-grade open-source database has unique advantages in replication. Through my experience with PostgreSQL replication in multiple projects, I have the following insights:

PostgreSQL的复制优势 | PostgreSQL Replication Advantages:

  • 流复制稳定可靠: 相比MySQL的binlog复制,PostgreSQL的流复制更加稳定,延迟更低

  • 逻辑复制灵活: 支持表级复制,可以选择性复制部分数据

  • 强一致性保证: 同步复制模式下可以确保零数据丢失

  • 丰富的监控工具: pg_stat_replication视图提供详细的复制状态信息

  • Stable streaming replication: Compared to MySQL’s binlog replication, PostgreSQL’s streaming replication is more stable with lower latency

  • Flexible logical replication: Supports table-level replication, allowing selective data replication

  • Strong consistency guarantees: Synchronous replication mode ensures zero data loss

  • Rich monitoring tools: pg_stat_replication view provides detailed replication status information

PostgreSQL复制最佳实践 | PostgreSQL Replication Best Practices

基于实际运维经验,我总结了以下PostgreSQL复制的最佳实践:

Based on practical operational experience, I’ve summarized the following PostgreSQL replication best practices:

1. 流复制配置建议 | Streaming Replication Configuration Recommendations

主库配置要点 | Primary Configuration Key Points:

1
2
3
4
5
6
-- postgresql.conf
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
synchronous_commit = on # 根据业务需求调整
synchronous_standby_names = '*' # 同步复制

从库配置要点 | Standby Configuration Key Points:

1
2
3
4
-- postgresql.conf
hot_standby = on
max_standby_streaming_delay = 30s
wal_receiver_status_interval = 1s

2. 监控和告警策略 | Monitoring and Alert Strategies

关键监控指标 | Key Monitoring Metrics:

  • 复制延迟: 通过pg_stat_replication.replay_lag监控
  • WAL发送状态: 监控pg_stat_replication.state
  • 磁盘空间: WAL日志积累可能导致磁盘满
  • 网络连接: 复制连接的稳定性

告警阈值建议 | Recommended Alert Thresholds:

  • 复制延迟超过10秒告警

  • WAL发送异常立即告警

  • 主从连接断开超过1分钟告警

  • Replication lag: Monitor via pg_stat_replication.replay_lag

  • WAL sender status: Monitor pg_stat_replication.state

  • Disk space: WAL log accumulation may cause disk full

  • Network connection: Stability of replication connections

  • Replication lag exceeding 10 seconds

  • WAL sender exceptions immediate alert

  • Primary-standby connection lost for more than 1 minute

3. 故障切换实践 | Failover Practices

自动故障切换工具推荐 | Recommended Automatic Failover Tools:

  • Patroni: 基于etcd/consul的高可用解决方案
  • repmgr: 轻量级的复制管理工具
  • Stolon: 云原生的PostgreSQL高可用方案

手动故障切换步骤 | Manual Failover Steps:

  1. 确认主库真正故障
  2. 提升从库为主库:pg_promote()
  3. 重新配置应用连接
  4. 修复原主库并重建复制
  • Patroni: High availability solution based on etcd/consul
  • repmgr: Lightweight replication management tool
  • Stolon: Cloud-native PostgreSQL high availability solution
  1. Confirm primary database is truly failed
  2. Promote standby to primary: pg_promote()
  3. Reconfigure application connections
  4. Repair original primary and rebuild replication

我的技术观点 | My Technical Perspectives

关于复制策略选择 | On Replication Strategy Selection

单主复制依然是主流 | Single-Leader Replication Remains Mainstream

虽然多主复制和无主复制在理论上很吸引人,但在实际生产环境中,我发现单主复制仍然是最可靠的选择,特别是对于需要强一致性的业务场景。原因如下:

While multi-leader and leaderless replication are theoretically attractive, in actual production environments, I find single-leader replication is still the most reliable choice, especially for business scenarios requiring strong consistency. Here’s why:

  1. 复杂性可控: 单主复制的逻辑简单,故障排查容易

  2. 一致性保证: 避免了复杂的冲突解决机制

  3. 工具成熟: PostgreSQL的单主复制工具链非常成熟

  4. 性能可预测: 读写分离的性能模式清晰

  5. Manageable complexity: Single-leader replication logic is simple, easy to troubleshoot

  6. Consistency guarantee: Avoids complex conflict resolution mechanisms

  7. Mature tooling: PostgreSQL’s single-leader replication toolchain is very mature

  8. Predictable performance: Clear read-write separation performance pattern

关于同步vs异步复制 | On Synchronous vs Asynchronous Replication

混合模式是最佳选择 | Hybrid Mode is the Best Choice

在实际项目中,我通常采用”同步+异步”的混合复制模式:

In actual projects, I usually adopt a “synchronous + asynchronous” hybrid replication mode:

  • 关键业务: 使用同步复制,确保数据安全
  • 读取扩展: 使用异步复制,提供更多读取能力
  • 跨地域备份: 使用异步复制,降低网络延迟影响

配置示例 | Configuration Example:

1
synchronous_standby_names = 'FIRST 1 (standby1), standby2, standby3'
  • Critical business: Use synchronous replication to ensure data safety
  • Read scaling: Use asynchronous replication for more read capacity
  • Cross-region backup: Use asynchronous replication to reduce network latency impact

关于PostgreSQL版本选择 | On PostgreSQL Version Selection

推荐PostgreSQL 14+版本 | Recommend PostgreSQL 14+ Versions

基于我的使用经验,PostgreSQL 14及以上版本在复制功能上有显著改进:

Based on my experience, PostgreSQL 14 and above versions have significant improvements in replication features:

  1. 逻辑复制增强: 支持二进制格式,性能提升30%以上

  2. 复制监控改进: 更丰富的统计信息和监控视图

  3. 故障恢复优化: 崩溃恢复时间大幅缩短

  4. 安全性增强: 支持更细粒度的复制权限控制

  5. Logical replication enhancements: Support for binary format, 30%+ performance improvement

  6. Replication monitoring improvements: Richer statistics and monitoring views

  7. Failover optimization: Significantly reduced crash recovery time

  8. Security enhancements: Support for more granular replication permission control

网络和安全配置 | Network and Security Configuration

网络优化 | Network Optimization:

  • 使用专用网络进行复制
  • 配置合适的TCP参数优化
  • 监控网络带宽使用情况

安全配置 | Security Configuration:

  • 使用SSL加密复制连接

  • 配置防火墙规则

  • 定期更新密码和证书

  • Use dedicated network for replication

  • Configure appropriate TCP parameter optimization

  • Monitor network bandwidth usage

  • Use SSL encryption for replication connections

  • Configure firewall rules

  • Regularly update passwords and certificates

结论 | Conclusion

数据库复制是构建可靠、可扩展系统的基础技术。选择正确的复制策略取决于应用程序的具体需求,包括一致性要求、可用性目标和性能期望。理解每种策略的权衡是设计成功分布式系统的关键。

Database replication is a fundamental technology for building reliable, scalable systems. Choosing the right replication strategy depends on your application’s specific requirements, including consistency needs, availability goals, and performance expectations. Understanding the trade-offs of each approach is crucial for designing successful distributed systems.

基于我在PostgreSQL复制方面的实战经验,我强烈建议:从简单开始,逐步优化。先建立稳定的单主复制架构,然后根据业务增长和性能需求,逐步引入更复杂的复制策略。PostgreSQL作为企业级数据库,其复制功能完全能够满足大多数业务场景的需求。

Based on my practical experience with PostgreSQL replication, I strongly recommend: Start simple, optimize gradually. First establish a stable single-leader replication architecture, then gradually introduce more complex replication strategies based on business growth and performance requirements. PostgreSQL as an enterprise-grade database, its replication features can fully meet the needs of most business scenarios.

无论选择哪种策略,都需要仔细考虑实现细节、监控系统状态,并为故障情况做好准备。随着应用程序的发展,复制策略也可能需要演进以满足新的需求。

Regardless of which strategy you choose, careful consideration of implementation details, monitoring system health, and preparing for failure scenarios is essential. As applications evolve, replication strategies may need to evolve as well to meet new requirements.


本文基于ByteByteGo的数据库复制指南编写,旨在为开发者提供全面的复制策略参考。

参考:This article is based on ByteByteGo’s database replication guide, aimed at providing developers with comprehensive reference for replication strategies.

架构师学习 第1篇

写在前面

以前,我相信代码即真理。作为一名专注于实现的技术专家,我聚焦于代码写得足够好,我就牛逼。直到我加入这家初创公司,成为研发负责人,现实给了我更复杂的挑战。

我们没有预算去组建豪华的架构团队,我不得不从微观的代码世界抬起头,开始被迫去思考那些我不曾涉足的宏大命题:技术选型、系统边界、网络安全、以及如何在资源捉襟见肘时支撑复杂的业务增长。

这不是一个“资深架构师下凡”的故事,而是一个“写代码的手艺人被迫去画图纸”的记录。

正因为我没有科班架构师的思维定式,我是带着“代码的触觉”去搭建系统的。我更加警惕过度设计,更加关注落地的成本。在本书/本文中,你看到的不仅是技术的选型,更是一个工程师在理想与现实、代码洁癖与商业速度之间,无数次权衡后的真实思考。

这一系列风格大概率就会以记录或碎碎念的方式呈现,估计没什么章法。

一、 硬核内功:分布式与数据密集型系统

  1. 必读神书:
    • 《数据密集型应用系统设计》 (Designing Data-Intensive Applications - DDIA)
      • 评价: 架构领域的“圣经”。Martin Kleppmann 把分布式系统、数据库原理、一致性哈希、CAP 理论讲得极为透彻。
      • 你的关注点: 不要只看结论,要看它对不同存储引擎(B-Tree vs LSM-Tree)、事务隔离级别、流处理的深度剖析。这对于你理解 Minio、SeaweedFS 这类分布式存储的底层逻辑至关重要。

[Image of Designing Data-Intensive Applications book cover]

  1. 现代架构模式:
    • 《软件架构:架构模式、特征及实践》 (Fundamentals of Software Architecture)
      • 评价: O’Reilly 出品的红皮书。它系统性地定义了架构风格(微内核、微服务、事件驱动、基于空间等)及其适用场景。
      • 核心价值: 帮你建立系统的“评估维度”(如可扩展性、弹性、性能、成本),学会用雷达图来做技术选型。

二、 方法论:如何驾驭业务复杂度

架构师不仅要懂技术,更要懂业务。

  1. 领域驱动设计 (DDD):

    • 推荐阅读: 也就是 Eric Evans 的蓝皮书(太晦涩,建议当字典查)或者 Vaughn Vernon 的《实现领域驱动设计》(红皮书,更实战)。
    • 关键点: 限界上下文(Bounded Context)、聚合根(Aggregate Root)、防腐层(ACL)。
    • 实战意义: 当你在做 SaaS 或 CRM 系统时,DDD 能帮你厘清微服务的边界,避免微服务变成“分布式单体”。
  2. 可视化与沟通:C4 模型

    • 架构师的一大工作是沟通。UML 太重,白板太乱。
    • C4 Model (Context, Containers, Components, Code): 由 Simon Brown 提出。它像谷歌地图一样,从宏观(系统全貌)到微观(类图)分层展示。
    • 建议: 以后做设计评审(Design Review),尝试用 C4 画图,你的专业度会瞬间提升。

三、 破局:AI Native 架构与 LLM 集成

作为现在的架构师,如果不考虑 AI,设计就是过时的。你需要思考如何把 LLM 融入现有架构。

  1. RAG (检索增强生成) 与 Agent 架构:

    • 不要只盯着模型微调,重点关注 向量数据库 (Vector DB) 的选型与 Context Window 的管理。
    • 学习 LangChainLangGraph 的设计理念(尽管你可能不直接用 Python 写生产代码,但思想通用)。
    • 思考题: 如何设计一个架构,既能处理传统的 CRUD 业务,又能低延迟地响应 AI 推理请求?如何处理 AI 的非确定性输出?
  2. 架构演进:

    • DevOps 转向 **Platform Engineering (平台工程)**。
    • 关注 IDP (Internal Developer Portal) 的构建,让开发人员自助服务,架构师负责制定标准和“铺路”。

四、 软技能:决策与文档

架构师是技术团队的政委。

  1. ADR (Architecture Decision Records):

    • 强烈推荐: 开始在你的项目中使用 ADR。
    • 是什么: 记录每一个架构决策的背景、选项、决策结果、后果(好的和坏的)。
    • 为什么: 解决“为什么当初那个傻X选了这个方案”的问题。它是架构师的“免责声明”和团队的知识资产。
  2. 技术影响力:

    • 阅读 **《技术管理模式:像在谷歌一样进行软件工程》 (Software Engineering at Google)**。了解大规模团队如何做代码评审、发布管理和知识共享。

五、 极简资源清单 (High Signal/Noise Ratio)

为了节省你的时间,我只推荐最高质量的信息源:

  • InfoQ (架构师特刊): 依然是国内质量较高的架构案例来源,关注大厂的复盘。
  • High Scalability (Blog): 虽然更新慢了,但以前的 Case Study(如 WhatsApp, Netflix 架构)是经典。
  • ThoughtWorks 技术雷达: 每半年看一次,了解什么是 Hold(别碰),什么是 Adopt(该用了)。
  • Hacker News: 保持对全球前沿技术的敏感度。

学习资料

https://refactoring.guru/design-patterns/catalog
https://github.com/Sairyss/domain-driven-hexagon
https://archguard.org/book-list
https://www.infoq.cn/article/crafting-architectural-diagrams/
https://www.infoq.cn/article/C4-architecture-model/
https://c4model.com/introduction

前言

在创业公司干TeamLeader,很多事情都需要自己去完成,Jira作为一款优秀的项目管理工具,可以帮助我们更高效地完成这些任务。本文将介绍 Jira 的一些基本功能和使用方法,以及如何在软件质量保证(SQA)中发挥其作用。

在现代软件开发中,软件质量保证(SQA)是确保产品可靠性和用户满意度的核心环节。Jira 作为主流的项目与测试管理工具,通过插件和与CI/CD工具的集成,极大提升了测试管理的效率和可追溯性。

Jira在软件质量保证中的作用

  • 集中管理测试流程:Jira 支持需求、缺陷、测试用例、测试计划等全流程管理,便于追踪每个阶段的质量状态。
  • 可扩展的测试管理插件:如 Zephyr、synapseRT、Test Management 等插件,扩展了 Jira 的测试管理能力,实现测试用例、测试套件、测试执行和缺陷的统一管理。
  • 需求与缺陷追踪:通过需求追踪矩阵和缺陷管理,确保每个需求都被充分测试,每个缺陷都能被及时发现和修复。

主要插件及CI/CD集成

  • ZephyrsynapseRT 等插件可与 Jenkins 等 CI/CD 工具集成,实现自动化测试结果的回传和可视化。
  • 插件配置流程一般包括:
    1. 在 Jira 安装对应插件。
    2. 在 Jenkins 安装插件并配置 Jira 连接。
    3. 在测试用例中关联自动化脚本,测试执行后自动同步结果到 Jira。
  • 通过集成,测试执行、结果反馈、缺陷跟踪实现自动化闭环,提升了测试效率和质量可控性。

最佳实践与总结

  • 明确测试流程,合理使用插件进行测试用例和缺陷管理。
  • 利用 CI/CD 工具与 Jira 集成,实现自动化测试与持续反馈。
  • 定期生成报告,监控项目质量,及时调整测试策略。

Jira 结合测试管理插件和自动化工具,为软件质量保证提供了强大支撑,是现代敏捷团队不可或缺的工具之一。

专业观点

从专业角度来看,Jira 不仅仅是一个项目管理工具,更是现代软件质量保证体系的中枢。它通过流程化、可追溯和自动化的管理方式,将需求、开发、测试、缺陷等环节有机串联,极大提升了团队协作效率和产品交付质量。

但在实际落地过程中,也存在一些挑战:如插件生态复杂、集成配置门槛较高、团队成员对流程规范的认知和执行力参差不齐等。因此,企业在引入 Jira 及其测试管理方案时,需结合自身实际,制定清晰的流程规范,并持续进行培训和优化。

展望未来,随着 DevOps、AI 测试等理念的发展,Jira 及其生态将更加智能化和自动化。如何更好地与云原生、微服务架构、智能分析等新技术融合,将是提升软件质量管理能力的关键方向。


使用心得

在实际使用 Jira 结合测试管理插件的过程中,我体会最深的是其对测试流程标准化和可追溯性的提升。通过需求、测试用例、缺陷的全链路管理,极大方便了团队协作和问题定位。尤其在多项目、多团队协作时,Jira 的权限和视图配置可以灵活满足不同角色的需求。

但也遇到过一些实际问题,比如:

  • 插件兼容性和升级带来的配置丢失或功能异常,需要定期备份和测试环境验证。
  • 自动化测试结果与 Jira 的集成,初期配置较为繁琐,建议团队制定详细的集成文档和标准流程。
  • 测试用例和缺陷数据量大时,Jira 的性能和检索效率会受到影响,建议定期归档历史数据。

总体来说,Jira+测试管理插件是提升测试管理效率和质量的有力工具,但要发挥最大价值,离不开团队的流程规范、持续优化和技术积累。

参考:JIRA-测试管理实用手册-全

网络 HTTP

从HTTP/1到HTTP/3:现代Web通信协议的演进

深入解析互联网基础设施的技术变革

HTTP是现代互联网通信的基石,从浏览器请求到Kubernetes集群内的微服务调用,几乎所有的网络交互都基于HTTP协议。每当浏览器加载页面、APP获取API响应、或后端服务相互查询时,几乎都是通过HTTP进行的。即使底层传输协议发生变化,这一点仍然成立——例如,gRPC将自己包装在HTTP/2之上,而主导后端设计的RESTful API只是建立在HTTP动词和状态码之上的约定。

基础认知:理解HTTP的生态角色

在深入探讨HTTP演进之前,我们必须理解HTTP运行在一个复杂的生态系统中。现代应用不仅仅是孤立地处理HTTP——它们还必须考虑数据库选择(SQL vs NoSQL)、CAP定理的权衡(一致性、可用性、分区容错性),以及HTTP所依赖的底层网络协议。

经典的客户端-服务器模型

alt text

这个简单的交换隐藏了巨大的复杂性。当你的浏览器请求/index.html时,它不仅仅是在获取一个文件——它参与了一个精心编排的复杂过程:

  • DNS解析
  • TCP连接建立
  • HTTP请求/响应循环
  • 连接管理
  • 缓存策略

HTTP/1.0:先驱者(1996年)

HTTP/1.0在当时是革命性的,但以今天的标准来看极其简单:

主要特征:

  • 无状态:每个请求都是独立的
  • 基于文本:人类可读的协议
  • 每请求一连接:每个资源都需要新的TCP连接
  • 无持久性:每次响应后连接都会关闭

问题所在:
想象加载一个包含50个资源(图片、CSS、JavaScript)的网页。HTTP/1.0需要建立50个独立的TCP连接。考虑到TCP的三次握手开销,这种效率是灾难性的。

1
2
3
请求1: [SYN] → [SYN-ACK] → [ACK] → [HTTP请求] → [响应] → [FIN]
请求2: [SYN] → [SYN-ACK] → [ACK] → [HTTP请求] → [响应] → [FIN]
... (再重复48次)

HTTP/1.1:主力军(1997年)

HTTP/1.1解决了HTTP/1.0的低效问题,成为互联网20多年来的主力协议。

主要改进:

1. 持久连接

1
Connection: keep-alive

HTTP/1.1允许在单个TCP连接上进行多个请求,而不是每次请求后都关闭连接。

2. 请求管道化
可以发送多个请求而无需等待响应,但响应仍必须按顺序处理。

3. Host头部

1
Host: example.com

启用虚拟主机——单个IP地址上的多个网站。

4. 分块传输编码

1
Transfer-Encoding: chunked

允许在不知道总内容长度的情况下流式传输响应。

队头阻塞(HOL Blocking)问题

尽管有这些改进,HTTP/1.1仍存在一个根本限制:队头阻塞。在管道化连接中,如果第一个响应延迟,所有后续响应都必须等待,即使它们已经准备好了。

1
2
3
请求A ────────────────────> [处理中...]
请求B ──> [就绪] [等待A]
请求C ──> [就绪] [等待A]

这与数据库理论相关。正如数据库在CAP定理中面临权衡(无法同时完美地拥有一致性、可用性和分区容错性),网络协议也面临自己的约束。HTTP/1.1选择了简单性和有序交付,而非并行性。

HTTP/2:游戏改变者(2015年)

HTTP/2代表了Web性能思维的根本转变。

革命性特性:

1. 二进制协议
与HTTP/1.1的基于文本格式不同,HTTP/2使用二进制帧,使其解析更高效、更不容易出错。

2. 多路复用
多个请求和响应可以在单个TCP连接上交错进行,不会相互阻塞。

1
2
3
4
连接1:
├── 流1: [请求A] ──> [响应A]
├── 流2: [请求B] ──> [响应B]
└── 流3: [请求C] ──> [响应C]

3. 服务器推送
服务器可以主动向客户端发送资源:

1
2
客户端请求: index.html
服务器推送: style.css, script.js, logo.png

4. 头部压缩(HPACK)
通过压缩HTTP头部(通常包含重复信息)来减少开销。

5. 流优先级
客户端可以指示哪些资源更重要:

1
Priority: weight=200, depends_on=stream_1

TCP瓶颈

然而,HTTP/2仍然依赖TCP,TCP在传输层有自己的队头阻塞。如果单个TCP数据包丢失,整个连接就会停滞,直到重传完成,影响所有HTTP/2流。

这反映了分布式数据库中的一致性挑战。正如最终一致性系统(如NoSQL文档存储)可以通过放松严格的一致性要求来提供更好的可用性,HTTP/3后来也会放松TCP的严格顺序保证。

HTTP/3:突破束缚(2020年)

HTTP/3通过完全放弃TCP而采用基于UDP的QUIC,代表了HTTP演进中最激进的变化。

QUIC:革命基础

QUIC(Quick UDP Internet Connections)解决了TCP的根本限制:

1. 减少连接建立
QUIC结合了传输和加密握手:

1
2
TCP + TLS: 3次往返
QUIC: 1次往返(后续连接为0-RTT)

2. 每流控制流量
与TCP的连接级流量控制不同,QUIC提供流级控制,消除了传输层队头阻塞。

3. 连接迁移
移动设备在WiFi和蜂窝网络之间切换时可以保持连接。

4. 内置加密
与基于TCP的HTTP/2不同,QUIC在设计上就包含加密——没有未加密的QUIC。

HTTP/3的革命性架构

HTTP/3从根本上改变了我们对Web通信的思考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HTTP/3协议栈:
┌─────────────────┐
│ 应用层 │ ← HTTP/3语义
├─────────────────┤
│ QUIC │ ← 传输+安全
├─────────────────┤
│ UDP │ ← 网络层
└─────────────────┘

传统协议栈:
┌─────────────────┐
│ 应用层 │ ← HTTP/1.1 或 HTTP/2
├─────────────────┤
│ TLS │ ← 安全层
├─────────────────┤
│ TCP │ ← 传输层
├─────────────────┤
│ IP │ ← 网络层
└─────────────────┘

演进时间线:可视化之旅

HTTP/1.1(1997年):顺序处理

1
2
3
4
5
6
7
8
9
客户端 ────────────> 服务器
│ TCP连接 │
│ ┌─────────────┐ │
│ │ 请求1 │──>│
│ │<── 响应 │ │
│ │ 请求2 │──>│
│ │<── 响应 │ │
│ └─────────────┘ │
│ 关闭连接 │

主要特性:

  • 持久连接和管道化
  • 使用TCP进行可靠传输
  • 应用层队头阻塞
  • 基于文本的协议

HTTP/2(2015年):多路复用革命

1
2
3
4
5
6
7
8
9
客户端 ────────────> 服务器
│ 单个TCP连接 │
│ ┌─────────────────────┐ │
│ │ 流1: 请求A │──>│
│ │ 流2: 请求B │──>│
│ │ 流3: 请求C │──>│
│ │<────── 响应 ────│ │
│ │ (多路复用) │ │
│ └─────────────────────┘ │

革命性变化:

  • 二进制帧层:高效解析和减少错误
  • 多路复用:同一TCP连接上的多个并发请求
  • 流优先级:关键资源获得优先级
  • 服务器推送:主动资源传递
  • 头部压缩(HPACK):减少开销

二进制帧的魔力:

1
2
3
4
5
6
HTTP消息(逻辑)
┌─────────────────┐
│ 请求头部 │ ──> 帧头部 <类型 = 头部>
├─────────────────┤ 帧主体 <压缩头部>
│ 请求主体 │ ──> 帧头部 <类型 = 数据>
└─────────────────┘ 帧主体 <实际数据>

HTTP/3(2019年):QUIC优势

1
2
3
4
5
6
7
8
9
10
客户端 ────────────> 服务器
│ 基于UDP的QUIC │
│ ┌─────────────────────┐ │
│ │ ╔═══════════════╗ │ │
│ │ ║ HTTP请求 ║ │ │
│ │ ╚═══════════════╝ │ │
│ │ ╔═══════════════╗ │ │
│ │ ║ HTTP响应 ║ │ │
│ │ ╚═══════════════╝ │ │
│ └─────────────────────┘ │

QUIC的颠覆性特性:

  • 无需正式连接建立
  • 基于UDP的最大灵活性
  • 每流流量控制消除传输层HOL阻塞
  • 0-RTT连接恢复
  • 连接迁移支持

性能对比:现实世界的影响

连接建立

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 + TLS:
[DNS] → [TCP SYN] → [TCP ACK] → [TLS握手] → [HTTP请求]
总计: ~3-4次往返

HTTP/2 + TLS:
[DNS] → [TCP SYN] → [TCP ACK] → [TLS握手] → [HTTP请求]
总计: ~3-4次往返(与HTTP/1.1相同)

HTTP/3 + QUIC:
[DNS] → [QUIC握手 + TLS + HTTP请求]
总计: ~1-2次往返(回访客户端为0-RTT)

队头阻塞分析

HTTP/1.1:应用层和传输层都有阻塞

1
2
3
请求A [████████████████] (慢)
请求B [██] (快,但在等待)
请求C [███] (快,但在等待)

HTTP/2:解决应用层,但TCP仍然阻塞

1
2
3
流A [████████████████] (数据包丢失影响所有)
流B [██] (被TCP重传阻塞)
流C [███] (被TCP重传阻塞)

HTTP/3:任何层都没有阻塞

1
2
3
流A [████████████████] (独立)
流B [██] (立即完成)
流C [███] (立即完成)

数据库类比:理解权衡

正如数据库面临CAP定理约束,HTTP协议也在做权衡:

HTTP/1.1:像ACID数据库

  • 强一致性:请求按顺序处理
  • 可靠性:TCP保证传输
  • 简单性:易于实现和调试
  • 性能代价:顺序处理限制吞吐量

HTTP/2:像带事务的NoSQL

  • 更好性能:多路复用增加吞吐量
  • 维持一致性:仍依赖TCP顺序
  • 增加复杂性:二进制协议、流管理
  • 部分解决方案:仍受TCP队头阻塞影响

HTTP/3:像最终一致性系统

  • 最大性能:独立流处理
  • 放松顺序:QUIC在适当时允许乱序传输
  • 高可用性:连接迁移、更快恢复
  • 复杂性权衡:更复杂的流控制和拥塞管理

现实世界的实现挑战

对开发者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// HTTP/1.1: 简单但低效
fetch('/api/data1').then(() =>
fetch('/api/data2').then(() =>
fetch('/api/data3')
)
);

// HTTP/2: 高效多路复用
Promise.all([
fetch('/api/data1'),
fetch('/api/data2'),
fetch('/api/data3')
]); // 所有请求在单连接上多路复用

// HTTP/3: 相同API,更好性能
// 无需代码更改 - 浏览器自动处理QUIC

对基础设施团队

  • 负载均衡器:必须支持QUIC转发
  • CDN:需要HTTP/3边缘服务器
  • 监控:QUIC连接的新指标
  • 调试:二进制协议需要专门工具

未来:下一步是什么?

当前采用情况(2024年)

  • HTTP/3:约30%的网站支持
  • 主要参与者:Google、Cloudflare、Facebook引领采用
  • 浏览器支持:所有现代浏览器都支持HTTP/3
  • 移动影响:在移动网络上获益最大

新兴模式

  1. 混合协议:同时使用HTTP/2和HTTP/3
  2. 边缘计算:HTTP/3的低延迟完美适合边缘部署
  3. 物联网集成:QUIC的效率有利于资源受限设备
  4. 实时应用:基于HTTP/3的WebRTC实现更好的流媒体

结论:理解性能权衡

从HTTP/1.1到HTTP/3的演进反映了分布式系统思维的广泛演进。正如我们从单体数据库转向具有最终一致性的微服务,HTTP也从简单的请求-响应模式演进为复杂的多路复用、加密、移动优化协议。

关键要点:

  1. HTTP/1.1仍然是基础——简单、可靠、通用支持
  2. HTTP/2解决了应用层多路复用但受TCP约束
  3. HTTP/3代表了对传输协议的根本重新思考

对系统架构师的建议:

  • 考虑为移动重点应用使用HTTP/3
  • 监控目标市场的采用率
  • 规划渐进式迁移策略
  • 理解HTTP/2在未来几年仍将相关

大局观:
理解HTTP不在于记忆状态码,而在于内化协议演进中固化的性能权衡。HTTP/1.0打开了大门。HTTP/1.1使其在规模上可用。HTTP/2通过在单个TCP连接上多路复用流来推动效率。而基于UDP上QUIC构建的HTTP/3,终于突破了数十年的旧约束。

在我们这个微秒级至关重要、移动连接变化无常的互联世界中,这些协议改进不仅仅是技术好奇心——它们是使现代Web体验成为可能的基础。


作为网络工程师和系统架构师,我们的工作不仅是实现这些协议,更要理解它们的基本权衡,并为每个特定挑战选择正确的工具。Web性能的未来不在于任何单一协议,而在于理解何时以及如何有效地利用每一个协议。

AI提效 Claude Sonnet 4+ ChatGPT
此系列,多多少少都会用到AI。

背景

刚购置了一台服务器,准备付尾款了,但是因为服务器是全新的,只装了几台虚拟机,显卡啥的都还没有用,所以在付钱之前需要先测试一下,看看表现。

跑了一整晚,第二天,看着没问题就删掉了虚拟机,忘了截图。

服务器配置

  1. CPU: 2*96
  2. 内存:384G
  3. 硬盘:5*3.4T
  4. 显卡:2*RTX5880

准备工作

  1. 开启显卡直通
  2. 停掉所有虚拟机
  3. 新建一台临时虚拟机
    1. 分配所有CPU+350GB内存+500G硬盘+2*RTX5880
    2. 安装ubuntu 24.04

安装显卡对应的驱动

1
2
3
sudo apt install ubuntu-drivers-common
ubuntu-drivers devices
sudo ubuntu-drivers autoinstall

安装烤鸡需要的工具

  1. gpu-burn
  2. stress-ng

系统测试脚本( GPU + CPU + 内存 + IO综合烤机测试)

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
#!/bin/bash

set -e

# 设置工具路径(修改为你实际的 gpu_burn 目录)
GPU_BURN_PATH="$HOME/gpu-burn/gpu_burn"

# 检查是否存在命令
command -v stress-ng >/dev/null 2>&1 || { echo >&2 "需要安装 stress-ng:sudo apt install stress-ng -y"; exit 1; }
command -v fio >/dev/null 2>&1 || { echo >&2 "需要安装 fio:sudo apt install fio -y"; exit 1; }

# 检查 GPU_BURN 是否存在
if [ ! -f "$GPU_BURN_PATH" ]; then
echo "找不到 gpu_burn,可在 https://github.com/wilicc/gpu-burn 下载并编译"
exit 1
fi

echo "开始 GPU + CPU + 内存 + IO 综合烤机测试"

# GPU 测试
"$GPU_BURN_PATH" 86400 &
GPU_PID=$!

# CPU 测试
stress-ng --cpu 0 --timeout 24h --metrics-brief &
CPU_PID=$!

# 内存测试
stress-ng --vm 4 --vm-bytes 80% --timeout 24h --metrics-brief &
MEM_PID=$!

# 磁盘I/O测试
fio --name=randwrite --ioengine=libaio --iodepth=16 --rw=randwrite --bs=4k --direct=1 --size=4G --numjobs=4 --runtime=86400 --group_reporting &
IO_PID=$!

echo "测试开始,PID: GPU=$GPU_PID, CPU=$CPU_PID, MEM=$MEM_PID, IO=$IO_PID"
wait


监控几种方案

  1. watch命令
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 每5秒刷新GPU状态
    watch -n 5 nvidia-smi

    # 每10秒刷新(减少刷新频率)
    watch -n 10 nvidia-smi

    # 只看关键信息
    watch -n 5 "nvidia-smi --query-gpu=name,temperature.gpu,power.draw,utilization.gpu,memory.used --format=csv"

  2. nvtop
    1
    2
    3
    4
    5
    6
    7
    bash# 安装nvtop - GPU实时监控工具
    sudo apt update
    sudo apt install nvtop -y

    # 运行nvtop
    nvtop

3.安装netdata + GPU集成插件