BFE负载均衡源码--http请求的转发

321 阅读5分钟

前言

通过之前的分析,我们可以了解到一条请求在进入bfe服务后通过转发机制可以确定承载这条请求的子集群, 然后通过不同的负载均衡算法选举出承载这条请求的具体后端实例, 本期我们就仔细看一下http请求是如何进行转发和响应的。

image.png

前期回顾:

BFE负载均衡源码--转发模型

BFE负载均衡源码--加权轮询算法及实现

源码结构

http协议的实现在bfe_http目录下, 相关文件说明如下:

类别文件名或目录说明
基础类型common.goHTTP基础数据类型定义
state.goHTTP协议内部状态指标
eof_reader.goEofReader类型定义,实现了io.ReadCloser接口,并永远返回EOF
协议消息request.goHTTP请求类型的定义、读取及发送
response.goHTTP响应类型的定义、读取及发送
header.goHTTP头部类型定义及相关操作
cookie.goHTTP Cookie字段的处理
status.goHTTP响应状态码定义
lex.goHTTP合法字符表
消息收发client.goRoundTrpper接口定义,支持并发的发送请求并获取响应
transport.goHTTP连接池管理,实现了RoundTrpper接口,在反向代理场景用于管理与后端的HTTP通信
transfer.gotransferWriter/transfterReader类型定义,在反向代理场景用于向后端流式发送请求及读取响应
response_writer.goResponseWriter类型定义,在反向代理场景用于构造响应并发送
辅助工具httputilHTTP相关辅助函数
chunked.goHTTP Chunked编码处理
sniff.goHTTP MIME检测算法实现(mimesniff.spec.whatwg.org)

转发流程

数据传输接口

RoundTripper是一个数据传输的接口定义, 可以见名知意的理解为往返传输数据, 接口与定义如下:

type RoundTripper interface {
   RoundTrip(*Request) (*Response, error)
}

RoundTrip方法接受一个*Request对象, 返回转发后的*Response, 目前开源版本v1.5.0实现了http, http2, fcgi三种协议的转发。

创建传输对象

通过转发流程确认集群后, 会创建关于这个集群的Transport。在bfe_http/transport.go中, 定义了http协议的转发实现.

// bfe_server/reverseproxy.go
func (p *ReverseProxy) clusterInvoke(srv *BfeServer, cluster *bfe_cluster.BfeCluster,
request *bfe_basic.Request, rw bfe_http.ResponseWriter) (
res *bfe_http.Response, action int, err error) {

//......
// 创建传输对象
clusterTransport := p.getTransport(cluster)
//......
// 获取集群的负载均衡破算法配置
bal, err = srv.balTable.Lookup(cluster.Name)
// ......
// 选择后端实例
clusterBackend, err = bal.Balance(request)
// ......
// 向后端转发请求, res为获取的响应。
res, err = transport.RoundTrip(outreq)
}

向后端转发请求

bfe_http/transport.goRoundTrip()方法实现了对于http请求的转发, BFE和下游的连接交互支持两种方式:

  1. 短连接方式, 即每次发起和后端实例的转发都建立新的TCP连接。
  2. 连接池方式, 即为下游的每个实例维护一个连接池, 每次需要向下游转发请求是优化从连接池中获取空闲连接, 如果没有空闲连接则创建新的TCP连接。转发完成后, 根据BFE与当前实例保持的连接数确定是否关闭连接。

转发核心方法如下:

// bfe_http/transport.go
func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
    // ......
    // 一. 创建连接信息
    cm, err := t.connectMethodForRequest(treq)
    // ......
    // 二. 获取空闲连接
    pconn, err := t.getConn(cm)
    // ......
    // 三. 执行转发操作
    resp, err = pconn.roundTrip(treq)
}

一. 创建连接信息

type connectMethod struct {
   proxyURL     *url.URL // nil for no proxy, else full proxy URL
   targetScheme string   // "http" or "https"
   targetAddr   string   // Not used if proxy + http targetScheme (4th example in table)
}

// 从请求信息中创建连接信息
func (t *Transport) connectMethodForRequest(treq *transportRequest) (*connectMethod, error) {
   cm := &connectMethod{
      targetScheme: treq.URL.Scheme,
      targetAddr:   canonicalAddr(treq.URL),
   }
   if t.Proxy != nil {
      var err error
      cm.proxyURL, err = t.Proxy(treq.Request)
      if err != nil {
         return nil, err
      }
   }
   return cm, nil
}

connectMethodForRequest从连接信息中提取schema, host, proyURL信息, 用于连接池缓存连接的key, 连接池的结构为: map[string][]*persistConn, 由缓存的key信息对应TCP连接切片组成, 其中缓存的key有connectMethod结构的key()方法生成, 不存在代理是缓存key如: ||http|127.0.0.1, ||https|127.0.0.1, 存在代理转发的情况时, 缓存key如: http://proxy.com|https|foo.com

二. 获取连接

// 获取空闲连接
func (t *Transport) getIdleConn(cm *connectMethod) (pconn *persistConn) {
   key := cm.key() // key的表示见上一步描述, 如: ||http|127.0.0.1
   // ...... 
   for {
      pconns, ok := t.idleConn[key]
      if !ok {
         return nil
      }
      if len(pconns) == 1 {
         // 只有一个连接情况, 使用此链接, 并删除连接池缓存
         pconn = pconns[0]
         delete(t.idleConn, key)
      } else {
         // 返回最后一个连接, 并更新连接切片
         pconn = pconns[len(pconns)-1]
         t.idleConn[key] = pconns[0 : len(pconns)-1]
      }
      if !pconn.isBroken() {
         return
      }
   }
}

如果不存在空闲连接, 则调用dialConn(cm *connectMethod)方法生成一个新的连接。

三. 执行转发操作

调用dialConn(cm *connectMethod)创建一个新的TCP连接时, 会同时启动pconn.readLoop(), pconn.writeLoop()两个协程执行对此连接的读写操作。

func (pc *persistConn) writeLoop() {
   //......
   for {
      select {
      case wr := <-pc.writech:
         //......
         // 调用wr.req.Request.write发送一个写请求
         err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra)
         if err == nil {
            err = pc.bw.Flush()
         }
         //......
      }
   }
}

func (pc *persistConn) readLoop() {
   //......
   for alive {
      //......
      var resp *Response
      if err == nil {
         //......
         // 读取响应
         resp, err = ReadResponse(pc.br, rc.req)
         //......
      }
   }
}

pconn.writeLoop()协程会将收到的请求发送到连接缓冲区中, pconn.readLoop()会从连接缓冲区中读取响应数据。roundTrip会构造读和写请求结构并发送到对应channel中, 待channel读取并处理响应的业务。

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
    //......
    writeErrCh := make(chan error, 1)
    pc.writech <- writeRequest{req, writeErrCh}

    resc := make(chan responseAndError, 1)
    pc.reqch <- requestAndChan{req.Request, resc, requestedGzip}
    //......
}

响应数据

获取到相应数据后, 调用sendResponse()方法发送响应数据, 在bfe_server/response.go文件中实现了ResponseWriter接口, 用于发送Http/Https响应。

总结与思考

在日常的工作开发中也会涉及到Http请求的转发需求, 学习工业级的协议转发实现可以帮忙我们思考如何构建一个可靠高效的转发组件。整体转发流程如下:

image.png