在一条h2连接上,有多个并行处理的流,流其实是逻辑单位,每个流由构建成他的帧组成,实际上在发送http2响应时,是在交错发送这些归属于不同流的各个帧。
组帧方式
last_out链表
h2将待发送的帧放入一条单链表上 h2c->last_out.
帧的发送
加入帧方式
每次有新的帧生成时,h2有三种方式加入该帧到链表里面
ngx_http_v2_queue_blocked_frame
ngx_http_v2_queue_ordered_frame
ngx_http_v2_queue_frame
其中 ngx_http_v2_queue_ordered_frame时最为简单头插
static ngx_inline void
ngx_http_v2_queue_ordered_frame(ngx_http_v2_connection_t *h2c,
ngx_http_v2_out_frame_t *frame)
{
frame->next = h2c->last_out;
h2c->last_out = frame;
}
ngx_http_v2_queue_blocked_frame :将新加入的frame插入到第一个被block的帧的前面(关于block后面详述)。
ngx_http_v2_queue_frame:考虑到了每个stream的优先级与frame的blocked情况,较为复杂。
注意:从last_out的含义可知,放在链头的,会是最后一个被发送出去的。
实际发送
ngx_http_v2_send_output_queue
发送并没有看上去的直观,直接遍历。 注意需要发送的并不是frame,而是挂在上面的chain。
以下面的的帧为例观察h2的发送场景
注意:这些帧可能属于不同的stream。
逆转
首先 将 frame逆转了,再次,将这些frame上的chain都组织为了一条链表:
rame的标头变为了out,chain的标头变为cl:
此时进行实际的发送:注意这里的c->send_chain就是ngx_ssl_send_chain,返回的是发送到哪个cl返回截止。
cl = c->send_chain(c, cl, 0);
从这里可以看出几点
1 在last_out表头的最后发送。(所以如果一个响应frame优先级较高的话通常位于表尾)
2 每个frame的chain都是按照顺序发送(即每个frame的数据都是有序发送)
恢复
for ( /* void */ ; out; out = fn) {
fn = out->next;
if (out->handler(h2c, out) != NGX_OK) {
out->blocked = 1;
break;
}
}
这里out指向首个发送出去的frame,此时需要对每个“尝试发送”的frame做判断:是否有发送完全,发送完全了后应该回收相关资源,没发送完,则返回AGAIN,然后将block置位,说明该frame没有发送完成。
每个frame上都会有一个在创建时就指定的handler: 通过 out->handler。 比如如果是HEADERS帧,其handler为:ngx_http_v2_headers_frame_handler
ngx_http_v2_headers_frame_handler
{
ngx_chain_t *cl, *ln;
ngx_http_v2_stream_t *stream;
stream = frame->stream;
cl = frame->first;
#if (NGX_HTTP_RESPONSE_FBT) // 只要发送出去数据,就记录fbt
if (cl->buf->pos > cl->buf->start) {
if (stream->response_fbt_msec == 0) {
stream->response_fbt_msec = ngx_current_msec;
}
}
#endif
for ( ;; ) { // 检测每个chain是否都发送完了
if (cl->buf->pos != cl->buf->last) {
frame->first = cl;
return NGX_AGAIN; // 没有返回AGAIN
}
ln = cl->next;
if (cl->buf->tag == (ngx_buf_tag_t) &ngx_http_v2_module) {
cl->next = stream->free_frame_headers;
stream->free_frame_headers = cl; //回收该chain
} else {
cl->next = stream->free_bufs; //回收该chain
stream->free_bufs = cl;
}
if (cl == frame->last) { //整个chain都发送完了,
break;
}
cl = ln;
}
ngx_http_v2_handle_frame(stream, frame); //回收该frame(放入stream的池子)
return NGX_OK;
}
从这里再看看frame->blocked的作用:标记该frame在上一次的发送中没有将数据发送完成,如果通过ngx_http_v2_queue_blocked_frame加入新的frame时,就会放入他的前面,至少让这个blocked的帧有机会先于新加入的帧进行发送. 这里讨论
- 为何直接按照先来后到的顺序发送,直接放在表头即可。
这里可见nginx考虑到了一种情况就是插队:比如某个帧优先级较高,如果其他帧都还没发送而此帧需要发送但又不能先于其他已经发送了一半的帧(blocked) .
这里以DATA帧为例:nginx在插入DATA帧时都是通过 ngx_http_v2_queue_frame ,即既考虑了blocked的情况,又考虑了proirity的情况。
【可以想一下:如果某个发送的DATA帧被block住了,后续所有加入的DATA帧都会被block住】
假设在发送到第二个帧时由于客户端满了此时没发送完返回:out指向
继续恢复
frame = NULL;
for ( /* void */ ; out; out = fn) {
fn = out->next;
out->next = frame;
frame = out;
}
h2c->last_out = frame;
再次恢复到之前的布局。
发送完成
至此一轮h2上的发送完成。
这里观察几个现象:
1 如何衡量每个stream的wwt
- 在代码中中将每次写入frame发送的时间点记录,此处就是在output
- 线上已经统一wwt:http2/http1 计算正常。
2 如何衡量 response-fbt
- 在代码中每次发送了响应headers帧时记录
- 线上已经统一fbt ,另外针对http2还增加了rfbt
流量控制
http2中增加了对各个stream的流量控制机制,和tcp的流量控制机制类似:也分为接受窗口和发送窗口。 其中发送窗口用于控制服务端向client发送流量的情况。此种重点讨论发送窗口的机制:
1 window帧
服务端会向客户端发送windonw_update帧,用于更新服务端的发送窗口 ngx_http_v2_state_window_update
ngx_http_v2_state_window_update(ngx_http_v2_connection_t *h2c, u_char *pos,
u_char *end)
{
if (h2c->state.sid) { //针对某个特定流
stream->send_window += window;
return ;
}
h2c->send_window += window; // 整条连接
....;
2 服务端如何使用
目前发送窗口只针对DATA帧,对其他帧不进行流控。在发送DATA帧之前(更具体的说是 在 将 frame挂在last_out链之前),会将该stream的windo配额和h2c的配额都减去:
ngx_http_v2_send_chain
{
ngx_http_v2_queue_frame(h2c, frame);
h2c->send_window -= frame_size;
stream->send_window -= frame_size;
stream->queued++;
}
如果某个stream因为自己的send_window限制了,则stream->exhuased标记被置位
如果某个stream因为h2c的send_window限制了,则该stream会被挂在h2c->waiting链表上【即使stream自己有窗口,也不能发送】
static ngx_inline ngx_int_t
ngx_http_v2_flow_control(ngx_http_v2_connection_t *h2c,
ngx_http_v2_stream_t *stream)
{
ngx_log_debug3(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
"http2:%ui windows: conn:%uz stream:%z",
stream->node->id, h2c->send_window, stream->send_window);
if (stream->send_window <= 0) {
stream->exhausted = 1;
return NGX_DECLINED;
}
if (h2c->send_window == 0) {
ngx_http_v2_waiting_queue(h2c, stream);
return NGX_DECLINED;
}
return NGX_OK;
}
恢复配额
h2c->send_window什么时候增加?
- 收到针对h2c的window帧: adjust_window_update
- stream有被回收,其占用的值被增加: ngx_http_v2_filter_cleanup
拥塞判断
- 记录由于h2c连接级别的拥塞时间http2_flow_ctrl
- 记录由于stream自身的拥塞时间:http2_stream_flow_ctrl
窗口记录
日志
1 增加h2c窗口值
2 增加stream的窗口值
统计可视化
统计平均域名的窗口值