设为首页 收藏本站
查看: 1502|回复: 0

[经验分享] HAProxy 研究笔记 -- TCP 连接处理流程

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2015-11-20 13:31:20 | 显示全部楼层 |阅读模式
本文基于
HAProxy 1.5-dev7 版本。



  • 目录

  • 1. 关键数据结构 session

  • 2. 相关初始化


    • 2.1. 初始化处理 TCP 连接的方法

    • 2.2. 初始化 listener

    • 2.3. 绑定所有已注册协议上的 listeners

    • 2.4. 启用所有已注册协议上的 listeners


  • 3. TCP 连接的处理流程


    • 3.1. 接受新建连接

    • 3.2. TCP 连接上的接收事件

    • 3.3. TCP 连接上的发送事件

    • 3.4. http 请求的处理


  1. 关键数据结构 session

haproxy 负责处理请求的核心数据结构是 struct session,本文不对该数据结构进行分析。
  从业务的处理的角度,简单介绍一下对 session 的理解:



  • haproxy 每接收到 client 的一个连接,便会创建一个 session 结构,该结构一直伴随着连接的处理,直至连接被关闭,session 才会被释放

  • haproxy 其他的数据结构,大多会通过引用的方式和 session 进行关联

  • 一个业务 session 上会存在两个 TCP 连接,一个是 client 到 haproxy,一个是 haproxy 到后端 server。
  此外,一个 session,通常还要对应一个 task,haproxy 最终用来做调度的是通过 task。

2. 相关初始化
  在 haproxy 正式处理请求之前,会有一系列初始化动作。这里介绍和请求处理相关的一些初始化。

2.1. 初始化处理 TCP 连接的方法
  初始化处理 TCP 协议的相关数据结构,主要是和 socket 相关的方法的声明。详细见下面 proto_tcpv4 (proto_tcp.c)的初始化:

static struct protocol proto_tcpv4 = {
.name = "tcpv4",
.sock_domain = AF_INET,
.sock_type = SOCK_STREAM,
.sock_prot = IPPROTO_TCP,
.sock_family = AF_INET,
.sock_addrlen = sizeof(struct sockaddr_in),
.l3_addrlen = 32/8,
.accept = &stream_sock_accept,
.read = &stream_sock_read,
.write = &stream_sock_write,
.bind = tcp_bind_listener,

.bind_all = tcp_bind_listeners,
.unbind_all = unbind_all_listeners,
.enable_all = enable_all_listeners,
.listeners = LIST_HEAD_INIT(proto_tcpv4.listeners),
.nb_listeners = 0,
};

2.2. 初始化 listener
  listener,顾名思义,就是用于负责处理监听相关的逻辑。
  在 haproxy 解析 bind 配置的时候赋值给 listener 的 proto 成员。函数调用流程如下:

cfgparse.c
-> cfg_parse_listen
-> str2listener
-> tcpv4_add_listener
-> listener->proto = &proto_tcpv4;

  由于这里初始化的是 listener 处理 socket 的一些方法。可以推断, haproxy 接收 client 新建连接的入口函数应该是 protocol 结构体中的 accpet 方法。对于tcpv4 来说,就是 stream_sock_accept() 函数。该函数到 1.5-dev19 中改名为 listener_accept()。这是后话,暂且不表。
  listener 的其他初始化

cfgparse.c
-> check_config_validity
-> listener->accept = session_accept;
listener->frontend = curproxy; (解析 frontend 时,会执行赋值: curproxy->accept = frontend_accept)
listener->handler = process_session;

  整个 haproxy 配置文件解析完毕,listener 也已初始化完毕。可以简单梳理一下几个 accept 方法的设计逻辑:



  • stream_sock_accept(): 负责接收新建 TCP 连接,并触发 listener 自己的 accept 方法 session_accept()

  • session_accept(): 负责创建 session,并作 session 成员的初步初始化,并调用 frontend 的 accept 方法 front_accetp()

  • frontend_accept(): 该函数主要负责 session 前端的 TCP 连接的初始化,包括 socket 设置,log 设置,以及 session 部分成员的初始化
  下文分析 TCP 新建连接处理过程,基本上就是这三个函数的分析。

2.3. 绑定所有已注册协议上的 listeners

haproxy.c
-> protocol_bind_all
-> all registered protocol bind_all
-> tcp_bind_listeners (TCP)
-> tcp_bind_listener
-> [ fdtab[fd].cb[DIR_RD].f = listener->proto->accept ]

  该函数指针指向 proto_tcpv4 结构体的 accept 成员,即函数 stream_sock_accept

2.4. 启用所有已注册协议上的 listeners
  把所有 listeners 的 fd 加到 polling lists 中 haproxy.c -> protocol_enable_all -> all registered protocol enable_all -> enable_all_listeners (TCP) -> enable_listener 函数会将处于 LI_LISTEN 的 listener 的状态修改为 LI_READY,并调用 cur poller
的 set 方法, 比如使用 sepoll,就会调用 __fd_set


3. TCP 连接的处理流程

3.1. 接受新建连接
  前面几个方面的分析,主要是为了搞清楚当请求到来时,处理过程中实际的函数调用关系。以下分析 TCP 建连过程。

haproxy.c
-> run_poll_loop
-> cur_poller.poll
-> __do_poll (如果配置使用的是 sepoll,则调用 ev_sepoll.c 中的 poll 方法)
-> fdtab[fd].cb[DIR_RD].f(fd) (TCP 协议的该函数指针指向 stream_sock_accept )
-> stream_sock_accept
-> 按照 global.tune.maxaccept 的设置尽量可能多执行系统调用 accept,然后再调用 l->accept(),即 listener 的 accept 方法 session_accept
-> session_accept

  session_accept 主要完成以下功能



  • 调用 pool_alloc2 分配一个 session 结构

  • 调用 task_new 分配一个新任务

  • 将新分配的 session 加入全局 sessions 链表中

  • session 和 task 的初始化,若干重要成员的初始化如下


    • t->process = l->handler: 即 t->process 指向 process_session

    • t->context = s: 任务的上下文指向 session

    • s->listener = l: session 的 listener 成员指向当前的 listener

    • s->si[] 的初始化,记录 accept 系统调用返回的 cfd 等

    • 初始化 s->txn

    • 为 s->req 和 s->rep 分别分配内存,并作对应的初始化


      • s->req = pool_alloc2(pool2_buffer)

      • s->rep = pool_alloc2(pool2_buffer)

      • 从代码上来看,应该是各自独立分配 tune.bufsize + sizeof struct buffer 大小的内存


    • 新建连接 cfd 的一些初始化


      • cfd 设置为非阻塞

      • 将 cfd 加入 fdtab[] 中,并注册新建连接 cfg 的 read 和 write 的方法

      • fdtab[cfd].cb[DIR_RD].f = l->proto->read,设置 cfd 的 read 的函数 l->proto->read,对应 TCP 为 stream_sock_read,读缓存指向 s->req,

      • fdtab[cfd].cb[DIR_WR].f = l->proto->write,设置 cfd 的 write 函数 l->proto->write,对应 TCP 为 stream_sock_write,写缓冲指向 s->rep



  • p->accept 执行 proxy 的 accept 方法即 frontend_accept


    • 设置 session 结构体的 log 成员

    • 根据配置的情况,分别设置新建连接套接字的选项,包括 TCP_NODELAY/KEEPALIVE/LINGER/SNDBUF/RCVBUF 等等

    • 如果 mode 是 http 的话,将 session 的 txn 成员做相关的设置和初始化


3.2. TCP 连接上的接收事件

haproxy.c
-> run_poll_loop
-> cur_poller.poll
-> __do_poll (如果配置使用的是 sepoll,则调用 ev_sepoll.c 中的 poll 方法)
-> fdtab[fd].cb[DIR_RD].f(fd) (该函数在建连阶段被初始化为四层协议的 read 方法,对于 TCP 协议,为 stream_sock_read )
-> stream_sock_read

  stream_sock_read 主要完成以下功能



  • 找到当前连接的读缓冲,即当前 session 的 req buffer:
        struct buffer *b = si->ib

  • 根据配置,调用 splice 或者 recv 读取套接字上的数据,并填充到读缓冲中,即填充到从 b->r(初始位置应该就是 b->data)开始的内存中

  • 如果读取到 0 字节,则意味着接收到对端的关闭请求,调用 stream_sock_shutr 进行处理


    • 读缓冲标记 si->ib->flags 的 BF_SHUTR 置位,清除当前 fd 的 epoll 读事件,不再从该 fd 读取

    • 如果写缓冲 si->ob->flags 的 BF_SHUTW 已经置位,说明应该是由本地首先发起的关闭连接动作


      • 将 fd 从 fdset[] 中清除,从 epoll 中移除 fd,执行系统调用 close(fd), fd.state 置位 FD_STCLOSE

      • stream interface 的状态修改 si->state = SI_ST_DIS



  • 唤醒任务 task_wakeup,把当前任务加入到 run queue 中。随后检测 runnable tasks 时,就会处理该任务

3.3. TCP 连接上的发送事件

haproxy.c
-> run_poll_loop
-> cur_poller.poll
-> __do_poll (如果配置使用的是 sepoll,则调用 ev_sepoll.c 中的 poll 方法)
-> fdtab[fd].cb[DIR_WR].f(fd) (该函数在建连阶段被初始化为四层协议的 write 方法,对于 TCP 协议,为 stream_sock_write )
-> stream_sock_write

  stream_sock_write主要完成以下功能



  • 找到当前连接的写缓冲,即当前 session 的 rep buffer:
        struct buffer *b = si->ob

  • 将待发送的数据调用 send 系统调用发送出去

  • 或者数据已经发送完毕,需要发送关闭连接的动作 stream_sock_shutw-> 系统调用 shutdown

  • 唤醒任务 task_wakeup,把当前任务加入到 run queue 中。随后检测 runnable tasks 时,就会处理该任务

3.4. http 请求的处理

haproxy.c
-> run_poll_loop
-> process_runnable_tasks,查找当前待处理的任务所有 tasks, 然后调用 task->process(大多时候就是 process_session) 进行处理

-> process_session

  process_session 主要完成以下功能



  • 处理连接需要关闭的情形,分支 resync_stream_interface

  • 处理请求,分支 resync_request (read event)


    • 根据 s->req->analysers 的标记位,调用不同的 analyser 进行处理请求

    • ana_list & AN_REQ_WAIT_HTTP: http_wait_for_request

    • ana_list & AN_REQ_HTTP_PROCESS_FE: http_process_req_common

    • ana_list & AN_REQ_SWITCHING_RULES:process_switching_rules


  • 处理应答,分支 resync_response (write event)


    • 根据 s->rep->analysers 的标记位,调用不同的 analyser 进行处理请求

    • ana_list & AN_RES_WAIT_HTTP: http_wait_for_response

    • ana_list & AN_RES_HTTP_PROCESS_BE:http_process_res_common


  • 处理 forward buffer 的相关动作

  • 关闭 req 和 rep 的 buffer,调用 pool2_free 释放 session 及其申请的相关内存,包括读写缓冲 (read 0 bytes)


    • pool_free2(pool2_buffer, s->req);

    • pool_free2(pool2_buffer, s->rep);

    • pool_free2(pool2_session, s);


  • task 从运行任务队列中清除,调用 pool2_free 释放 task 申请的内存: task_delete(); task_free();

本文简单分析了 TCP 连接的处理过程,不侧重细节分析,而且缺少后端 server 的选择以及建连等,重在希望展示出一个
haproxy 处理 TCP 连接的框架。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-141524-1-1.html 上篇帖子: HAproxy研究与应用(千万级并发、负载均衡) 下篇帖子: Haproxy源码分析一、概述
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表