GitHub user Alanxtl reopened a discussion: [GATEWAY] 优化pixiu的字典树并发读写能力


由 yuque 文档 
[Pixiu路由原理简介.pdf](https://github.com/user-attachments/files/21966865/Pixiu.pdf) 
总结而来,原作者 @yqxu 


### **Pixiu路由字典树(Trie)并发优化方案**

#### **1. 背景:当前实现的瓶颈**

在当前的Pixiu路由实现中,为了保证数据一致性,对字典树的所有读、写操作都由一把全局锁(LOCK)来控制。无论是读取路由规则进行匹配,还是通过RDS协议动态更新路由规则,操作前都必须先获取这把锁。

这种设计的直接问题是,当读、写操作并发进行时,会产生激烈的锁竞争。特别是在高并发场景下,大量的读请求(URL匹配)会被少数的写请求(规则更新)所阻塞,从而影响网关的整体性能和延迟。该实现之所以要求读操作也加锁,是因为底层大量使用了Go语言的`map`结构,该结构在并发读写时会直接导致程序崩溃。

**当前实现示意图:**

```mermaid
graph LR
    subgraph "读/写线程"
        A(读请求)
        B(写请求)
    end

    subgraph "全局锁控制"
        C{LOCK}
    end

    D[Trie]

    A -- "竞争锁" --> C
    B -- "竞争锁" --> C
    C -- "获取成功" --> D
```

#### **2. 优化目标**

为了解决全局锁带来的性能瓶颈,优化目标是实现对字典树的**无锁读取**,从而让路由匹配(读操作)的性能不受规则更新(写操作)的影响。

#### **3. 优化方案:读写分离与双树切换**

本次优化引入了**读写分离**的设计思想,核心是维护两棵功能上完全相同的字典树(Trie1和Trie2),一棵用于读取(“读树”),另一棵用于写入(“写树”)。

**方案核心组件:**

  * 
**Command队列**:所有对字典树的写操作(如添加、删除路由)不再直接作用于树,而是先被封装成一个`command`对象,放入一个先进先出(FIFO)的队列中。
  * **双字典树(Trie1 & 
Trie2)**:系统中同时存在两个Trie实例。在任意时刻,一个作为“读树”对外提供无锁的路由匹配服务,另一个作为“写树”在后台消费Command队列中的指令。
  * 
**追Log线程**:一个独立的后台线程,负责从Command队列中读取指令,并将其应用到当前的“写树”上。由于只有这一个线程对“写树”进行操作,因此“写树”本身也无需加锁。
  * **双游标(Index1 & Index2)**:队列维护两个游标,分别对应两棵树追赶指令的进度。

**整体架构示意图:**

```mermaid
graph LR
    subgraph "用户写操作"
        A[Operator: url add /a/b/c] --> Q;
        B[Operator: url remove /a/b] --> Q;
    end

    subgraph "Command队列 (FIFO)"
        Q(Command Queue)
    end

    subgraph "后台追Log线程"
        T[追Log线程]
    end

    subgraph "双Trie实例"
        T1(Trie1 - 读树);
        T2(Trie2 - 写树);
    end

    R[读请求]

    Q -- "读取Command" --> T;
    T -- "写入" --> T2;

    R -- "无锁读取" --> T1;

    %% 切换逻辑
    style T1 fill:#cde,stroke:#333
    style T2 fill:#fce,stroke:#333
```

#### **4. 角色切换逻辑**

为了让“写树”上更新的配置能够生效,系统会周期性地(例如,每3秒)切换“读树”和“写树”的角色 [cite: 246, 
247]。切换过程经过精心设计,以保证服务的平滑过渡:

1.  
**暂停写入**:首先让“追Log线程”空转,即暂停向“写树”应用新的Command。这样做是为了避免在切换过程中树的状态发生变化,同时通过空转而非挂起线程来避免上下文切换的开销。
2.  **确保无写入**:保证当前没有任何写入线程在操作两棵树。
3.  **切换读引用**:将全局的“读引用”切换到另一棵树(即原来的“写树”)。从这一刻起,所有新的读请求都将从这棵更新过的树中读取数据。
4.  **切换写引用**:将“追Log线程”的“写引用”切换到原来的“读树”上。
5.  **恢复写入**:恢复“追Log线程”的工作,使其开始向新的“写树”同步Command队列中的指令。

通过这个机制,可以实现配置的“延迟生效”(例如延迟3秒),同时保证了最终一致性,因为两棵树追赶的是同一份操作日志(Command队列)。

#### **5. 优化带来的收益**

  * **高性能读取**:路由匹配(读操作)不再需要等待锁,实现了完全的无锁读取,性能得到极大提升。
  * **消除竞争**:写操作被异步化,读和写在不同的数据结构上进行,彻底消除了读写冲突。
  * **高可用性**:即使在频繁更新路由规则的场景下,网关对外的服务能力(路由匹配)也不会受到影响。

-----

GitHub link: https://github.com/apache/dubbo-go-pixiu/discussions/743

----
This is an automatically sent email for [email protected].
To unsubscribe, please send an email to: 
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to