暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

如何构建实用的RDS Proxy? (一)

数据库思维 2021-04-26
1713

RDS Proxy定义

RDS Proxy,定义为RDS的中间代理,致力于实现一些RDS当前不具备的功能或者解决RDS当前不易解决的问题。例如,在MySQL中,不易管控连接突增问题,不易按特定需求 (如:只审计DELETE操作) 定制审计功能。


RDS Proxy实用功能

我罗列了几个实用、不需要过多编码即可实现的功能:

  • 连接转发功能

    连接转发,做的事情很简单,将客户端的连接通过Proxy转发的DB   Server。该功能在做拆库,做设备裁撤很有用处。

  • 连接池功能

    连接池,可控制透传到DB Server的连接数,减轻DB连接压力。

  • IAM功能

    IAM, 全称Identify and Access Manangement, 身份认证管理。通过Proxy,     可实现一种常用场景: 

    DBA/研发通过企业个人账号,通过Proxy,Proxy与公司IAM互通,

    自动校验账户的身份,来决定是否转发请求到DB Server。

    这样,就不用在每个DB上去管理单独的个人账户,减少因人员更替带来的      账号管理不善风险。

  • 故障转移功能

     故障转移,也就是通过Proxy实现DB Server的HA能力,对应用透明。


连接转发功能实现

本文主要介绍连接转发功能,其他功能后续序列中介绍。

先说说为什么需要这个功能。

场景1:  在遇到机房裁撤或者DB Server机器跨机房搬迁时,如没有内部DNS服务,会涉及到DB地址的变更。DB地址变更后,需要业务配置做变更。为保证安全性,需要客户端业务侧一次性全部正确更改,如涉及多个业务,需要多个业务协同更改,难度极大。此时,Proxy可以闪亮登场了。



参考上图,使用RDS Proxy, 可以更安全地实现APP搬迁请求至新DB,步骤:

1. 使用RDS Proxy,Proxy请求转发至old DB。APP侧可以逐步更改DB配置文件,将DB请求转至Proxy。

2. 等待应用侧全部更改请求至RDS Proxy。(如何判断? 下图中,如果old DB还有非 10.11.1.2的连接,代表未改完。可以通过设置long_query_time=0, 开慢查询日志检测)。

3. 同步完成,校验完数据后,将RDS Proxy指向新DB。

4. APP侧可以逐步将DB配置文件,改回新DB。


场景2:  参考我之前的文章 "浅谈DB物理拆分"


接下来看看如何实现DB Proxy的转发功能,此处使用golang作为编程语言,mysql作为后端RDS。

关键流程为:

1. 在Proxy上监听1个端口,用来接收客户端请求。

    // listener为监听的对象,可读取客户端发送的数据,也可响应给客户端。
    // 假设Proxy监听端口在 8080上。客户端直接访问Proxy IP+该端口,即可连接到Proxy
    listener, err := net.Listen("tcp""0.0.0.0:8080")

    2. 通过goroutine不断地获取监听到的数据,然后持续连接到目标DB。

      // 伪代码示例
      for {
        // 通过Accept不断读取客户端数据。
        conn,err := listener.Accept()
        if err != nil {
         continue
        }
        go connDestDB(conn)
      }
      // connDestDB:与目标DB交换数据包
      func connDestDB(conn net.Conn) {
        dbConn,err := net.Dial("tcp", "10.11.1.1:3306")
        // 这里的readPackage,代表DB侧返回的初始认证包
        // 官方描述整个过程为 Initial Handshake: starts with server sending the Initial Handshake Packet.
        // and then client sends the Handshake Response Packet.
        
        // 这里面官方文档在Protocol::Handshake中没有体现出4字节包头,实际上存在
        header,handshake := readPackage(dbConn)
        // 发送给客户端,要求完成认证
        conn.Write(header)
        // handshake中为官方文档Protocol::HandshakeV10的内容。
        conn.Write(handshake)
      }


      // readPackage 读取DB交互过程中的数据包处理逻辑(伪代码)
      func readPackage(conn net.Conn) ([]byte,[]byte,error)  {
      header := []byte{0000}
        rb := bufio.NewReaderSize(conn, 8*1024)
        // 4字节为mysql包头,前3字节代表包长度,后1字节为sequence ID.
        if _, err := io.ReadFull(rb, header); err != nil {
          return nil,nil,err
        }
        // 网络字节序,小端存储。
      length := int(uint32(header[0]) | uint32(header[1])<<8 | uint32(header[2])<<16)
        data := make([]byte, length)
        handshake := io.ReadFull(rb, data)
        return header, handshake,nil
       }

      3. 客户端数据包与DB进行交互,完成Handshake认证后,即可完成连接建立。

        // 同样滴, 在我们将客户端的HandshakeResponse发送给服务端
        header,data := readPackage(conn)
        dbConn.Write(header)
        dbConn.Write(data)
        // 持续不断地获取MySQL Server返回的结果,Copy是指的从右到左复制
        go func() {
        _, err = io.Copy(conn, dbConn)
          if err != nil { //客户端exit,则退出
        return
        }
        }
        // 可以继续接收客端后续的SQL请求
        for {
        //正常会在连接成功后,客户端发送select @@version_comment limit 1
          _,_,err := readPackage(conn)
          //无法从客户端获取到数据,可能是账号认证失败了。
          if err != nil {
           return
          }
        }



        参考资料:

        MySQL官网连接过程介绍:

        https://dev.mysql.com/doc/internals/en/connection-phase.html

        Kingshard开源库:  

        https://github.com/flike/kingshard

        mysql握手协议抓包: 

        https://www.jianshu.com/p/af1c5406c737

        文章转载自数据库思维,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

        评论