Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1509539
  • 博文数量: 228
  • 博客积分: 1698
  • 博客等级: 上尉
  • 技术积分: 3241
  • 用 户 组: 普通用户
  • 注册时间: 2008-12-24 21:49
个人简介

Linux

文章分类

全部博文(228)

文章存档

2017年(1)

2016年(43)

2015年(102)

2014年(44)

2013年(5)

2012年(30)

2011年(3)

分类: LINUX

2015-07-30 19:47:25

客户端访问业务,调度到HaProxy,HaProxy通过负载均衡算法选择后端的Server进行响应。HaProxy担当了应用层代理和负载均衡的角色,
前端与HaProxy建立TCP连接,HaProxy与后端Server建立TCP连接,两条连接互相独立,前端是广域网,而后端是局域网。

监控生产环境的传输质量时发现,HaProxy与Server的连接传输过程中,HaProxy经常出现接收缓冲区被占满的情况,其标志是HaProxy发出零窗口的ACK报文,
出现的比例占总体后端连接数量的1/3左右,且下载文件基本都是70K+的量级。此时初步怀疑是局域网和广域网之间的差异导致,但没有直接证据。

先登录生产环境的机房,查看各设备的系统状态,CPU占用率、磁盘IO都处于正常状态,但内存剩余不足百M。(设备上除了Ha还有其他很多业务)

首选需要确认当前的内存是否导致问题的原因,做了如下尝试,排除内存影响 <不是主因>
1. 同环境中,使用一些策略恢复部分设备的内存,观察恢复前后的设备出现的零窗口连接的占比
2. 用虚拟机精确模拟低内存,出现Page alloc failed,甚至OOM kill的场景,观察是否出现零窗口连接
3. 走读Kernel代码,确认内存与窗口调整之间的关联 <存在关联,但不是当前问题的主因>

观察中发现,前端全量连接中出现超时的连接比例约为20%+,后端连接仅为2.4%,后端链路明显优于前端,另外前端的RTT约为后端RTT的N倍,
以上都属于局域网与广域网之间的差异,基本符合我们的预期。

为什么窗口会满
前面整体的观测仅是想缩小问题的调查范围,内存的调整并没有带来明显改善。因此写了一个SystemTap脚本,用于跟踪HaProxy端的接收窗口,当接口将要变
化为0时,记录连接窗口的相关信息,先来梳理一下代码:

Tcp_output.c中的tcp_transmit_skb函数,通过tcp_select_window设置当前的窗口

tcp_select_window函数,确认具体窗口选择方式

T    
可以
看到选择的新窗口,由__tcp_select_window来设置,同时当new_win小于2^rcv_wscale时清为0。当前环境中窗口扩大因子为8(下图),因此
new_win小于256时被设置为0,Wireshark的截图不上传了。

__tcp_select_window函数




监控最后返回的window以及free_spacemss之间的关系,将和窗口计算相关的信息进行打印,删除连接IP信息,如下

点击(此处)折叠或打开

  1. [tcp_select_window: 1745], Wed Jul 22 15:02:46 2015 CST, rcv_buf=87380, rcv_wup=3169662632, rcv_nxt=3169699132, rcv_wnd=60672, adv_mss=1460, cop_seq=3169661164, rcv_wsc=8, rcv_ssthresh=61320, window_clamp=64075, mss=1460, free_space=-1857, full_space=64075, window

      从SystemTap脚本统计的结果来看,基本全部命中free_space < mss,看一下free_space的计算方式

点击(此处)折叠或打开

  1. static inline int tcp_win_from_space(int space)
  2. {
  3.     return sysctl_tcp_adv_win_scale<=0 ?
  4.         (space>>(-sysctl_tcp_adv_win_scale)) :
  5.         space - (space>>sysctl_tcp_adv_win_scale);
  6. }

  7. /* Note: caller must be prepared to deal with negative returns */
  8. static inline int tcp_space(const struct sock *sk)
  9. {
  10.     return tcp_win_from_space(sk->sk_rcvbuf -
  11.                  atomic_read(&sk->sk_rmem_alloc));
  12. }

      其中sysctl_tcp_adv_win_scal的值可以通过如下方式查看

点击(此处)折叠或打开

  1. [root@S-LAB hanwei]# cat /proc/sys/net/ipv4/tcp_adv_win_scale
  2.  2

从  目前的现象来看,是缓冲区上限sk_rcvbuf减去当前已申请的空间后,剩余空间不足。而当前观测到的sk_rcvbuf缓冲区上限基本都是在130K左右,系统默认配置如下

点击(此处)折叠或打开

  1. [root@S-LAB hanwei]# cat /proc/sys/net/ipv4/tcp_rmem
  2.  4096 87380 4194304

即  即sk_rcvbuf默认值为87K,上限为4M,期间系统会根据实际情况动态调整sk_rcvbuf

Tcp_ipv4.c中的tcp_v4_init_sock函数,sk_rcvbuf的初始化如下

点击(此处)折叠或打开

  1. sk->sk_rcvbuf = sysctl_tcp_rmem[1]
Tcp_input.c中的tcp_rcv_space_adjust函数,用于动态调整sk_rcvbuf

点击(此处)折叠或打开

  1. void tcp_rcv_space_adjust(struct sock *sk)
  2. {
  3.     struct tcp_sock *tp = tcp_sk(sk);
  4.     int time;
  5.     int space;

  6.     if (tp->rcvq_space.time == 0)
  7.         goto new_measure;

  8.     time = tcp_time_stamp - tp->rcvq_space.time;
  9.     if (time < (tp->rcv_rtt_est.rtt >> 3) || tp->rcv_rtt_est.rtt == 0)
  10.         return;

  11.     space = 2 * (tp->copied_seq - tp->rcvq_space.seq);

  12.     space = max(tp->rcvq_space.space, space);

  13.     if (tp->rcvq_space.space != space) {
  14.         int rcvmem;

  15.         tp->rcvq_space.space = space;

  16.         if (sysctl_tcp_moderate_rcvbuf &&
  17.          !(sk->sk_userlocks & SOCK_RCVBUF_LOCK)) {
  18.             int new_clamp = space;

  19.             /* Receive space grows, normalize in order to
  20.              * take into account packet headers and sk_buff
  21.              * structure overhead.
  22.              */
  23.             space /= tp->advmss;
  24.             if (!space)
  25.                 space = 1;
  26.             rcvmem = SKB_TRUESIZE(tp->advmss + MAX_TCP_HEADER);
  27.             while (tcp_win_from_space(rcvmem) < tp->advmss)
  28.                 rcvmem += 128;
  29.             space *= rcvmem;
  30.             space = min(space, sysctl_tcp_rmem[2]);
  31.             if (space > sk->sk_rcvbuf) {
  32.                 sk->sk_rcvbuf = space;

  33.                 /* Make the window clamp follow along. */
  34.                 tp->window_clamp = new_clamp;
  35.             }
  36.         }
  37.     }

  38. new_measure:
  39.     tp->rcvq_space.seq = tp->copied_seq;
  40.     tp->rcvq_space.time = tcp_time_stamp;
  41. }   

     代码比较清晰,每个RTT调整一次,计算RTT内复制到用户空间的数据量×2,当本次计算的space大于上次时,尝试对sk_rcvbuf进行调整。当可以调整时,会计算可以缓存的包个数,并增加报文的必要开销计算所需内存。当计算结果比当前的sk_rcvbuf要大时,则进行调整,同时调整window_clampnew_clamp<接收窗口上限>

     当前环境下,窗口占满的初步原因:
rcvbuf与User RTT内的读取数据量正相关,当sk_rcvbuf维持在低量时,说明User的消费能力较弱。当User的消费能力无法匹配Server的生产能时,sk_rcvbuf的可用缓存空间被占满,导致零窗口出现。

HaProxy读取速度的影响因素

监控一下HaProxy的读取信息,可以发现HaProxy读取数据时Buf经常不足1K,如下

点击(此处)折叠或打开

  1. [tcp_recvmsg: 1361] execname: haproxy, pid: 1554, len: 8192, nonblock: 64, flags: 0x0000000
  2. [tcp_recvmsg: 1361] execname: haproxy, pid: 1554, len: 497, nonblock: 64, flags: 0x00000000
  3. [tcp_recvmsg: 1361] execname: haproxy, pid: 1554, len: 381, nonblock: 64, flags: 0x00000000
据此,分析一下HaProxy的代码:
Stream_sock.c文件中的stream_sock_write_read函数

点击(此处)折叠或打开

  1. cur_read = 0;
  2.     while (1) {
  3.         max = buffer_max_len(b) - b->l;

  4.         if (max <= 0) {
  5.             b->flags |= BF_FULL;
  6.             si->flags |= SI_FL_WAIT_ROOM;
  7.             break;
  8.         }

  9.         /*
  10.          * 1. compute the maximum block size we can read at once.
  11.          */
  12.         if (b->l == 0) {
  13.             /* let's realign the buffer to optimize I/O */
  14.             b->r = b->w = b->lr = b->data;
  15.         }
  16.         else if (b->r > b->w) {
  17.             /* remaining space wraps at the end, with a moving limit */
  18.             if (max > b->data + b->size - b->r)
  19.                 max = b->data + b->size - b->r;
  20.         }
  21.         /* else max is already OK */

  22.         /*
  23.          * 2. read the largest possible block
  24.          */
  25.         ret = recv(fd, b->r, max, 0);

  26.         if (ret > 0) {
  27.             b->r += ret;
  28.             b->l += ret;
  29.             cur_read += ret;

  30.             /* if we're allowed to directly forward data, we must update send_max */
  31.             if (b->to_forward && !(b->flags & (BF_SHUTW|BF_SHUTW_NOW))) {
  32.                 unsigned long fwd = ret;
  33.                 if (b->to_forward != BUF_INFINITE_FORWARD) {
  34.                     if (fwd > b->to_forward)
  35.                         fwd = b->to_forward;
  36.                     b->to_forward -= fwd;
  37.                 }
  38.                 b->send_max += fwd;
  39.                 b->flags &= ~BF_OUT_EMPTY;
  40.             }

  41.             if (fdtab[fd].state == FD_STCONN) {
b->data为一块环形缓冲区,b->rb->w分为代表当前读写游标,读报文时指定值为缓冲区大小减去其中的数据长度。当上文中发现读数据时的缓冲区不足1K
说明HaProxy中可用缓冲区长度已不足1K,而HaProxy的缓冲区的默认大小为16384字节,其设置方式如下:

点击(此处)折叠或打开

  1. struct buffer {
  2.     unsigned int flags; /* BF_* */
  3.     int rex; /* expiration date for a read, in ticks */
  4.     int wex; /* expiration date for a write or connect, in ticks */
  5.     int rto; /* read timeout, in ticks */
  6.     int wto; /* write timeout, in ticks */
  7.     int cto; /* connect timeout, in ticks */
  8.     unsigned int l; /* data length */
  9.     char *r, *w, *lr; /* read ptr, write ptr, last read */
  10.     unsigned int size; /* buffer size in bytes */
  11.     unsigned int send_max; /* number of bytes the sender can consume om this buffer, <= l */
  12.     unsigned int to_forward; /* number of bytes to forward after send_max without a wake-up */
  13.     unsigned int analysers; /* bit field indicating what to do on the buffer */
  14.     int analyse_exp; /* expiration date for current analysers (if set) */
  15.     void (*hijacker)(struct session *, struct buffer *); /* alternative content producer */
  16.     unsigned char xfer_large; /* number of consecutive large xfers */
  17.     unsigned char xfer_small; /* number of consecutive small xfers */
  18.     unsigned long long total; /* total data read */
  19.     struct stream_interface *prod; /* producer attached to this buffer */
  20.     struct stream_interface *cons; /* consumer attached to this buffer */
  21.     struct pipe *pipe;        /* non-NULL only when data present */
  22.     char data[0]; /* <size> bytes */
  23. }

     其中data[0],可变数组data即为HaProxy用于读写的缓冲区,看一下其的赋值方式:

点击(此处)折叠或打开

  1. /* perform minimal intializations, report 0 in case of error, 1 if OK. */
  2. int init_buffer()
  3. {
  4.     pool2_buffer = create_pool("buffer", sizeof(struct buffer) + global.tune.bufsize, MEM_F_SHARED);
  5.     return pool2_buffer != NULL;
  6. }

     创建pool2_buffer的地址池,其中每个块发小为sizeof(buffer) + global.tune.bufsizeglobal.tune.bufsize即为HaProxy读写缓冲区的大小

点击(此处)折叠或打开

  1. /* global options */
  2. struct global global = {
  3.     logfac1 : -1,
  4.     logfac2 : -1,
  5.     loglev1 : 7, /* max syslog level : debug */
  6.     loglev2 : 7,
  7.     .stats_sock = {
  8.         .maxconn = 10, /* 10 concurrent stats connections */
  9.         .perm = {
  10.              .ux = {
  11.                  .uid = -1,
  12.                  .gid = -1,
  13.                  .mode = 0,
  14.              }
  15.          }
  16.     },
  17.     .tune = {
  18.         .bufsize = BUFSIZE,
  19.         .maxrewrite = MAXREWRITE,
  20.         .chksize = BUFSIZE,
  21.     },
  22.     /* others NULL OK */
  23. }

点击(此处)折叠或打开

  1. #ifndef BUFSIZE
  2. #define BUFSIZE     16384
  3. #endif

     可以看到data缓冲区的大小,默认被设置为了16384,这个值在配置文件可以设置,当前我们未配置。

     这个缓冲区的确不是很大,当文件较大并且HaProxy向广域网方向写的速度较慢时,HaProxy的缓冲区很容易占满,而且读取速度较慢,从而导致HaProxy连接Server端时,其内核接收缓冲区容易被占满。

     
由于广域网和局域网差异,HaProxyServer之间连接的传输质量及RTT要明显优于HaProxyClient之间的连接,比如由于广域网方向出现丢包,可能导致长达数秒没有新数据发送出去,而HaProxy 这段时间内又收到了大量的Server传输的数据,协议栈接收缓冲区/HaProxy缓冲区/协议栈发送缓冲区很快占满,零窗口再现。同样的,RTT的差异,导致单位时间内HaProxy接收的数据可能数倍于发送的数据,当文件足够大时,各缓冲区可以预期肯定会出现占满情况,导致零窗口问题

     
容易想到,当扩大HaProxy缓冲区上限时,可以有效改善该问题,降低零窗口比例。比如将该缓冲区扩大至M级别,即使存在广域网和局域网的差异M级别内的文件都不再会出现零窗口问题,简单方便。不过会对当前设备的内存分配产生影响。


通过后端的零窗口连接,找到HaProxy与客户端的连接

      至此,HaProxyServer传输时出现的零窗口问题,后端的情况已完成初步分析。对于当前的分析结论,需要找到零窗口连接对应的HA-客户端的连接,并对其进行分析来验证之前的结论。由于HaProxy本身代理的特性,已将前后连接完全剥离,因此无法直接进行关联分析。

      基于此,目前有两种方式:

1. 前后端连接同时抓包,根据后端零窗口连接产生的时间/URI,对前端连接进行过滤

2. 修改HaProxy,在请求头部增加客户端的源IP地址和端口,通过IP/端口进行过滤

      由于前端报文数量巨大,并发在4+的量级,pps在百万级别,方法1耗时耗力,尝试后放弃。
而方法2的缺点是需要替换并重启生产环境HaProxy,对HaProxy进行了修改,修改比较简单src/proto_http.c中3730行增加http_header_add_tail即可。

具体代码不贴了,经过上述步骤,关联前后端的连接,并基于大量连接的自动分析验证,基本验证了结论。

广域网与局域网的传输质量与传输时延的差异,是导致HaProxy出现零窗口的主要原因。由于问题的瓶颈在于广域网方向的传输,
因此HaProxy于后端出现的零窗口问题,对用户访问体验并没有影响。

阅读(1832) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~