Guohao Lu's Blog

个人技术博客

磁盘数据发送到网络的拷贝过程

  1. 传统方式的4次拷贝

    1
    2
    3
    4
    ┌─────────┐    ┌──────────┐    ┌─────────┐    ┌──────────┐    ┌─────────┐
    │ 磁盘 │ -> │ 内核缓冲区│ -> │ 用户缓冲区│ -> │socket缓冲区│ -> │ 网卡 │
    └─────────┘ └──────────┘ └─────────┘ └──────────┘ └─────────┘
    拷贝1 拷贝2 拷贝3 拷贝4
  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
    // 示例代码
    File.read(fileDesc, buf, len); // 读取文件
    Socket.send(buf, len); // 发送数据

    过程解析:
    1次拷贝:DMA拷贝
    - 由DMA控制器完成
    - 将磁盘数据拷贝到内核缓冲区
    - 不需要CPU参与

    2次拷贝:CPU拷贝
    - 由CPU完成
    - 将数据从内核缓冲区拷贝到用户缓冲区(如JVM堆)
    - read()系统调用的结果

    3次拷贝:CPU拷贝
    - 由CPU完成
    - 将数据从用户缓冲区拷贝到socket缓冲区
    - write()系统调用的过程

    4次拷贝:DMA拷贝
    - 由DMA控制器完成
    - 将socket缓冲区的数据拷贝到网卡
    - 准备通过网络发送
  3. 上下文切换

    1
    2
    3
    4
    5
    6
    7
    涉及的上下文切换:
    1. read()调用:用户态 -> 内核态
    2. read()返回:内核态 -> 用户态
    3. write()调用:用户态 -> 内核态
    4. write()返回:内核态 -> 用户态

    总计:4次上下文切换
  4. 性能影响

    1
    2
    3
    4
    5
    6
    7
    8
    9
    资源消耗:
    - CPU时间:用于数据拷贝和上下文切换
    - 内存带宽:多次数据拷贝
    - 延迟增加:每次上下文切换大约需要1-10微秒

    示例计算(发送1MB数据):
    - 数据拷贝:~10ms
    - 上下文切换:~4-40微秒
    - 总耗时:>10ms
  5. 对比零拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    零拷贝方式:
    ┌─────────┐ ┌──────────┐ ┌─────────┐
    │ 磁盘 │ -> │ 内核缓冲区│ -> │ 网卡 │
    └─────────┘ └──────────┘ └─────────┘
    DMA拷贝 DMA拷贝

    优势:
    - 只有2次DMA拷贝
    - 不需要CPU拷贝
    - 减少上下文切换
    - 降低延迟和CPU使用率

Sentinel与Redis限流方案对比

  1. 架构对比

    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
    Sentinel单机限流:
    ┌──────────────┐
    │ 请求 │
    └──────┬───────┘


    ┌──────────────┐
    │ 应用服务器 │ ◄── Sentinel限流
    └──────────────┘

    Redis分布式限流:
    ┌──────────────┐
    │ 请求 │
    └──────┬───────┘


    ┌──────────────┐ ┌──────────────┐
    │ 应用服务器1 │ │ 应用服务器2 │
    └──────┬───────┘ └──────┬───────┘
    │ │
    └─────────┬────────┘

    ┌──────────────┐
    │ Redis │ ◄── 限流统计
    └──────────────┘
  2. 特性对比

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ┌────────────┬───────────────────┬───────────────────┐
    │ 特性 │ Sentinel │ Redis限流 │
    ├────────────┼───────────────────┼───────────────────┤
    │部署方式 │应用内限流 │中心化限流 │
    │实现复杂度 │简单 │相对复杂 │
    │限流精度 │较高 │受网络延迟影响 │
    │扩展性 │单机 │分布式 │
    │限流规则 │丰富 │需自行实现 │
    │实时性 │很高 │有少量延迟 │
    │资源占用 │内存 │网络IO │
    └────────────┴───────────────────┴───────────────────┘
  3. 应用场景

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Sentinel适用场景:
    1. 单机应用的限流保护
    2. 微服务内部限流
    3. 需要精确控制的场景
    4. 对实时性要求高的场景

    Redis限流适用场景:
    1. 分布式系统的全局限流
    2. 跨服务的限流需求
    3. 需要共享限流数据的场景
    4. 动态调整限流规则
  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
    @Service
    public class OrderService {

    private final RedisLimiter redisLimiter;

    @SentinelResource(
    value = "createOrder",
    blockHandler = "handleFlowControl"
    )
    public Order createOrder(OrderRequest request) {
    // 1. 分布式限流 - 全局QPS控制
    if (!redisLimiter.isAllowed("order:global", 1000)) {
    throw new LimitException("全局限流");
    }

    // 2. Sentinel限流 - 本地线程数控制
    return processOrder(request);
    }

    // Sentinel限流配置
    private void initSentinelRules() {
    FlowRule rule = new FlowRule();
    rule.setResource("createOrder");
    rule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
    rule.setCount(10);
    FlowRuleManager.loadRules(Arrays.asList(rule));
    }
    }
  5. 为什么需要组合使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    技术原因:
    1. 分层防护
    - Redis限流:全局流量控制
    - Sentinel:应用级保护

    2. 不同维度
    - Redis限流:QPS维度
    - Sentinel:线程数维度

    3. 补充优势
    - Redis:分布式一致性
    - Sentinel:实时精确控制

    业务原因:
    1. 多级限流需求
    - 用户级别限流(Redis)
    - 接口级别限流(Sentinel)

    2. 差异化限流
    - 全局业务限流(Redis)
    - 资源保护限流(Sentinel)
  6. 最佳实践

    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
    public class RateLimiterConfig {

    @Bean
    public GlobalFilter globalRateLimiter(RedisLimiter redisLimiter) {
    return (exchange, chain) -> {
    String path = exchange.getRequest().getPath().value();

    // 1. 全局限流 - Redis
    if (!redisLimiter.isAllowed("global:" + path)) {
    return ResponseEntity.status(429).build();
    }

    // 2. 本地限流 - Sentinel
    Entry entry = null;
    try {
    entry = SphU.entry(path);
    return chain.filter(exchange);
    } catch (BlockException e) {
    return ResponseEntity.status(429).build();
    } finally {
    if (entry != null) {
    entry.exit();
    }
    }
    };
    }
    }

本文介绍零拷贝(Zero-Copy)的原理和应用场景。

什么是零拷贝

零拷贝是一种 I/O 操作优化技术…

零拷贝(Zero-Copy)实现原理

  1. 传统方式 vs 零拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    传统方式:read() + write()
    应用程序需要:
    1. read()读取数据到用户空间
    2. write()写入数据到socket缓冲区

    零拷贝:sendfile()
    应用程序:
    1. 直接调用sendfile()系统调用
    2. 内核直接在内核空间完成传输
  2. sendfile()系统调用

    1
    2
    3
    4
    5
    // Linux系统调用
    ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

    // Java NIO中的使用
    FileChannel.transferTo(position, count, socketChannel);
  3. 实现机制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    两种实现方式:

    1. 带DMA收集功能的网卡:
    ┌─────────┐ ┌──────────┐ ┌─────────┐
    │ 磁盘 │ -> │ 内核缓冲区│ => │ 网卡 │
    └─────────┘ └──────────┘ └─────────┘
    DMA拷贝 只传递描述符 DMA拷贝
    (不复制数据)

    2. 普通网卡:
    ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐
    │ 磁盘 │ -> │ 内核缓冲区│ -> │socket缓冲区│ -> │ 网卡 │
    └─────────┘ └──────────┘ └──────────┘ └─────────┘
    DMA拷贝 CPU拷贝 DMA拷贝
  4. DMA收集功能原理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    核心机制:
    1. 内核缓冲区不复制数据
    2. 只向网卡传递内存描述符(sg_list)
    3. 网卡根据描述符直接从内核缓冲区读取数据

    描述符内容:
    - 内存地址
    - 数据长度
    - 偏移量
  5. 性能对比

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    发送1GB文件:

    传统方式:
    - CPU拷贝:2次
    - DMA拷贝:2次
    - 上下文切换:4次
    - 总耗时:~10秒

    零拷贝(带DMA收集):
    - CPU拷贝:0次
    - DMA拷贝:2次
    - 上下文切换:2次
    - 总耗时:~3秒
  6. Kafka中的应用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Kafka源码中的使用
    public class FileChannel {
    public long transferTo(long position, long count, WritableByteChannel target) {
    // 底层调用sendfile()
    return transferTo0(position, count, target);
    }
    }

    优势:
    1. 减少CPU使用
    2. 提高吞吐量
    3. 降低内存使用
    4. 减少上下文切换

领域事件驱动在微服务中的应用

核心概念关系

架构关系图:

1
2
3
4
5
6
7
8
9
10
┌─────────────────────────────────────┐
│ 微服务架构 │
│ ┌────────────┐ ┌────────────┐ │
│ │ 订单服务 │ │ 库存服务 │ │
│ └────────────┘ └────────────┘ │
│ │ │ │
│ └───────┬───────┘ │
│ ▼ │
│ 领域事件驱动通信 │
└─────────────────────────────────────┘

优势互补:

  1. 微服务:服务边界清晰、独立部署
  2. 事件驱动:解耦、异步、可扩展

领域事件示例

领域事件定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
public class OrderCreatedEvent {
private String orderId;
private String userId;
private BigDecimal amount;
private Date createTime;

public static OrderCreatedEvent from(Order order) {
OrderCreatedEvent event = new OrderCreatedEvent();
event.setOrderId(order.getId());
event.setUserId(order.getUserId());
event.setAmount(order.getAmount());
event.setCreateTime(new Date());
return event;
}
}

事件发布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class OrderService {
@Autowired
private EventBus eventBus;

@Transactional
public void createOrder(OrderDTO dto) {
// 1. 创建订单
Order order = orderRepository.save(
Order.create(dto)
);

// 2. 发布领域事件
eventBus.publish(
OrderCreatedEvent.from(order)
);
}
}

事件订阅:

1
2
3
4
5
6
7
8
9
10
@Service
public class InventoryService {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
// 扣减库存
inventoryRepository.deduct(
event.getOrderId()
);
}
}

实现方式

技术选型图:

1
2
3
4
┌────────────┐    ┌────────────┐    ┌────────────┐
│ Kafka │ │ RabbitMQ │ │ EventBus │
└────────────┘ └────────────┘ └────────────┘
异步 异步+即时 进程内事件

消息格式示例:

1
2
3
4
5
6
7
8
9
{
"eventId": "xxx",
"eventType": "OrderCreated",
"timestamp": "xxx",
"data": {
"orderId": "xxx",
"userId": "xxx"
}
}

典型应用场景

场景一:订单支付流程

1
2
3
4
┌──────────┐   事件   ┌──────────┐   事件   ┌──────────┐
│订单服务 │───────►│支付服务 │───────►│库存服务 │
└──────────┘ └──────────┘ └──────────┘
OrderCreated PaymentSuccess StockDeducted

场景二:用户注册流程

1
2
3
4
┌──────────┐   事件   ┌──────────┐   事件   ┌──────────┐
│用户服务 │───────►│积分服务 │───────►│消息服务 │
└──────────┘ └──────────┘ └──────────┘
UserRegistered PointsAwarded WelcomeSent

最佳实践

事件发布可靠性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class ReliableEventBus {
@Transactional
public void publish(DomainEvent event) {
// 1. 保存事件到本地消息表
eventRepository.save(event);

// 2. 异步发送消息
kafkaTemplate.send(
"topic",
JSON.toJSON(event)
);
}
}

幂等性处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class IdempotentEventHandler {
@EventListener
public void handle(DomainEvent event) {
String key = event.getEventId();
if (processed(key)) {
return;
}

try {
// 处理业务逻辑
processEvent(event);
// 记录已处理
markAsProcessed(key);
} catch (Exception e) {
// 异常处理
}
}
}

注意事项

注意事项列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 事件设计原则
- 事件应该是过去时
- 包含必要上下文
- 版本化管理

2. 可靠性保证
- 本地消息表
- 消息幂等性
- 死信队列

3. 性能考虑
- 异步处理
- 批量处理
- 合适的分区

4. 监控告警
- 事件处理延迟
- 失败重试次数
- 积压队列大小