-*- coding: utf-8 -*-
Changes with Apache 2.5.0
+ *) mod_http2: new directives 'H2TLSWarmUpSize' and 'H2TLSCoolDownSecs'
+ to control TLS record sizes during connection lifetime.
+
*) mod_cache: Accept HT (Horizontal Tab) when parsing cache related header
fields as described in RFC2616. [Christophe Jaillet]
</usage>
</directivesynopsis>
+ <directivesynopsis>
+ <name>H2TLSWarmUpSize</name>
+ <description></description>
+ <syntax>H2TLSWarmUpSize amount</syntax>
+ <default>H2TLSWarmUpSize 1048576</default>
+ <contextlist>
+ <context>server config</context>
+ <context>virtual host</context>
+ </contextlist>
+
+ <usage>
+ <p>
+ This directive sets the number of bytes to be sent in small
+ TLS records (~1300 bytes) until doing maximum sized writes (16k)
+ on https: HTTP/2 connections.
+ This can be used server wide or for specific
+ <directive module="core" type="section">VirtualHost</directive>s.
+ </p>
+ <p>
+ Measurements by <a href="https://www.igvita.com">google performance
+ labs</a> show that best performance on TLS connections is reached,
+ if initial record sizes stay below the MTU level, to allow a
+ complete record to fit into an IP packet.
+ </p>
+ <p>
+ While TCP adjust its flow-control and window sizes, longer TLS
+ records can get stuck in queues or get lost and need retransmission.
+ This is of course true for all packets. TLS however needs the
+ whole record in order to decrypt it. Any missing bytes at the end
+ will stall usage of the received ones.
+ </p>
+ <p>
+ After a sufficient number of bytes have been send successfully,
+ the TCP state of the connection is stable and maximum TLS record
+ sizes (16 KB) can be used for optimal performance.
+ </p>
+ <p>
+ In deployments where servers are reached locally or over reliable
+ connections only, the value might be decreased with 0 disabling
+ any warmup phase alltogether.
+ </p>
+ <p>
+ The following example sets the size to zero, effectively disabling
+ any warmup phase.
+ </p>
+ <example><title>Example</title>
+ <highlight language="config">
+ H2TLSWarmUpSize 0
+ </highlight>
+ </example>
+ </usage>
+ </directivesynopsis>
+
+ <directivesynopsis>
+ <name>H2TLSCoolDownSecs</name>
+ <description></description>
+ <syntax>H2TLSCoolDownSecs seconds</syntax>
+ <default>H2TLSCoolDownSecs 1</default>
+ <contextlist>
+ <context>server config</context>
+ <context>virtual host</context>
+ </contextlist>
+
+ <usage>
+ <p>
+ This directive sets the number of seconds of idle time on a TLS
+ connection before the TLS write size falls back to small (~1300 bytes)
+ length.
+ This can be used server wide or for specific
+ <directive module="core" type="section">VirtualHost</directive>s.
+ </p>
+ <p>
+ See <directive type="section">H2TLSWarmUpSize</directive> for a
+ description of TLS warmup. H2TLSCoolDownSecs reflects the fact
+ that connections may detoriate over time (and TCP flow adjusts)
+ for idle connections as well. It is beneficial to overall performance
+ to fall back to the pre-warmup phase after a number of seconds that
+ no data has been sent.
+ </p>
+ <p>
+ In deployments where connections can be considered reliable, this
+ timer can be disabled by setting it to 0.
+ </p>
+ <p>
+ The following example sets the seconds to zero, effectively disabling
+ any cooldown. Warmed up TLS connections stay on maximum record
+ size.
+ </p>
+ <example><title>Example</title>
+ <highlight language="config">
+ H2TLSCoolDownSecs 0
+ </highlight>
+ </example>
+ </usage>
+ </directivesynopsis>
+
</modulesynopsis>
-1, /* # session extra files */
1, /* modern TLS only */
-1, /* HTTP/1 Upgrade support */
+ 1024*1024, /* TLS warmup size */
+ 1, /* TLS cooldown secs */
};
static int files_per_session = 0;
conf->session_extra_files = DEF_VAL;
conf->modern_tls_only = DEF_VAL;
conf->h2_upgrade = DEF_VAL;
+ conf->tls_warmup_size = DEF_VAL;
+ conf->tls_cooldown_secs = DEF_VAL;
+
return conf;
}
n->session_extra_files = H2_CONFIG_GET(add, base, session_extra_files);
n->modern_tls_only = H2_CONFIG_GET(add, base, modern_tls_only);
n->h2_upgrade = H2_CONFIG_GET(add, base, h2_upgrade);
+ n->tls_warmup_size = H2_CONFIG_GET(add, base, tls_warmup_size);
+ n->tls_cooldown_secs = H2_CONFIG_GET(add, base, tls_cooldown_secs);
return n;
}
int h2_config_geti(h2_config *conf, h2_config_var_t var)
+{
+ return (int)h2_config_geti64(conf, var);
+}
+
+apr_int64_t h2_config_geti64(h2_config *conf, h2_config_var_t var)
{
int n;
switch(var) {
n = files_per_session;
}
return n;
+ case H2_CONF_TLS_WARMUP_SIZE:
+ return H2_CONFIG_GET(conf, &defconf, tls_warmup_size);
+ case H2_CONF_TLS_COOLDOWN_SECS:
+ return H2_CONFIG_GET(conf, &defconf, tls_cooldown_secs);
default:
return DEF_VAL;
}
return "value must be On or Off";
}
+static const char *h2_conf_set_tls_warmup_size(cmd_parms *parms,
+ void *arg, const char *value)
+{
+ h2_config *cfg = h2_config_sget(parms->server);
+ cfg->tls_warmup_size = apr_atoi64(value);
+ (void)arg;
+ return NULL;
+}
+
+static const char *h2_conf_set_tls_cooldown_secs(cmd_parms *parms,
+ void *arg, const char *value)
+{
+ h2_config *cfg = h2_config_sget(parms->server);
+ cfg->tls_cooldown_secs = (int)apr_atoi64(value);
+ (void)arg;
+ return NULL;
+}
+
#define AP_END_CMD AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL)
RSRC_CONF, "on to enable direct HTTP/2 mode"),
AP_INIT_TAKE1("H2SessionExtraFiles", h2_conf_set_session_extra_files, NULL,
RSRC_CONF, "number of extra file a session might keep open"),
+ AP_INIT_TAKE1("H2TLSWarmUpSize", h2_conf_set_tls_warmup_size, NULL,
+ RSRC_CONF, "number of bytes on TLS connection before doing max writes"),
+ AP_INIT_TAKE1("H2TLSCoolDownSecs", h2_conf_set_tls_cooldown_secs, NULL,
+ RSRC_CONF, "seconds of idle time on TLS before shrinking writes"),
AP_END_CMD
};
H2_CONF_SESSION_FILES,
H2_CONF_MODERN_TLS_ONLY,
H2_CONF_UPGRADE,
+ H2_CONF_TLS_WARMUP_SIZE,
+ H2_CONF_TLS_COOLDOWN_SECS,
} h2_config_var_t;
/* Apache httpd module configuration for h2. */
int session_extra_files; /* # of extra files a session may keep open */
int modern_tls_only; /* Accept only modern TLS in HTTP/2 connections */
int h2_upgrade; /* Allow HTTP/1 upgrade to h2/h2c */
+ apr_int64_t tls_warmup_size; /* Amount of TLS data to send before going full write size */
+ int tls_cooldown_secs; /* Seconds of idle time before going back to small TLS records */
} h2_config;
h2_config *h2_config_rget(request_rec *r);
int h2_config_geti(h2_config *conf, h2_config_var_t var);
+apr_int64_t h2_config_geti64(h2_config *conf, h2_config_var_t var);
void h2_config_init(apr_pool_t *pool);
#include "h2_h2.h"
#include "h2_util.h"
-#define WRITE_BUFFER_SIZE (64*1024)
+#define WRITE_BUFFER_SIZE (128*1024)
+/* Calculated like this: assuming MTU 1500 bytes
+ * 1500 - 40 (IP) - 20 (TCP) - 40 (TCP options)
+ * - TLS overhead (60-100)
+ * ~= 1300 bytes */
#define WRITE_SIZE_INITIAL 1300
-#define WRITE_SIZE_MAX (16*1024)
-#define WRITE_SIZE_IDLE_USEC (1*APR_USEC_PER_SEC)
-#define WRITE_SIZE_THRESHOLD (1*1024*1024)
+/* Calculated like this: max TLS record size
+ * 16*1024
+ * - TLS overhead (60-100) */
+#define WRITE_SIZE_MAX (16*1024 - 100)
apr_status_t h2_conn_io_init(h2_conn_io *io, conn_rec *c)
{
- io->connection = c;
- io->input = apr_brigade_create(c->pool, c->bucket_alloc);
- io->output = apr_brigade_create(c->pool, c->bucket_alloc);
- io->buflen = 0;
+ h2_config *cfg = h2_config_get(c);
+
+ io->connection = c;
+ io->input = apr_brigade_create(c->pool, c->bucket_alloc);
+ io->output = apr_brigade_create(c->pool, c->bucket_alloc);
+ io->buflen = 0;
/* That is where we start with,
* see https://issues.apache.org/jira/browse/TS-2503 */
- io->write_size = WRITE_SIZE_INITIAL;
- io->last_write = 0;
- io->buffer_output = h2_h2_is_tls(c);
+ io->tls_warmup_size = h2_config_geti64(cfg, H2_CONF_TLS_WARMUP_SIZE);
+ io->tls_cooldown_usecs = (h2_config_geti(cfg, H2_CONF_TLS_COOLDOWN_SECS)
+ * APR_USEC_PER_SEC);
+ io->write_size = WRITE_SIZE_INITIAL;
+ io->last_write = 0;
+ io->buffer_output = h2_h2_is_tls(c);
+ if (APLOGctrace1(c)) {
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, io->connection,
+ "h2_conn_io(%ld): init, buffering=%d, warmup_size=%ld, cd_secs=%f",
+ io->connection->id, io->buffer_output, (long)io->tls_warmup_size,
+ ((float)io->tls_cooldown_usecs/APR_USEC_PER_SEC));
+ }
/* Currently we buffer only for TLS output. The reason this gives
* improved performance is that buckets send to the mod_ssl network
* filter will be encrypted in chunks. There is a special filter
status = ap_get_brigade(io->connection->input_filters,
io->input, AP_MODE_READBYTES,
- block, 16 * 4096);
+ block, 64 * 4096);
switch (status) {
case APR_SUCCESS:
return h2_conn_io_bucket_read(io, block, on_read_cb, puser, &done);
ap_update_child_status(io->connection->sbh, SERVER_BUSY_WRITE, NULL);
status = apr_brigade_length(bb, 1, &bblen);
if (status == APR_SUCCESS) {
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, io->connection,
+ "h2_conn_io(%ld): flush, brigade %ld bytes",
+ io->connection->id, (long)bblen);
status = ap_pass_brigade(io->connection->output_filters, bb);
if (status == APR_SUCCESS) {
io->bytes_written += (apr_size_t)bblen;
apr_bucket *b;
int bcount, i;
- if (io->write_size > WRITE_SIZE_INITIAL
- && (apr_time_now() - io->last_write) >= WRITE_SIZE_IDLE_USEC) {
+ if (io->write_size > WRITE_SIZE_INITIAL
+ && (io->tls_cooldown_usecs > 0)
+ && (apr_time_now() - io->last_write) >= io->tls_cooldown_usecs) {
/* long time not written, reset write size */
io->write_size = WRITE_SIZE_INITIAL;
io->bytes_written = 0;
(long)io->connection->id, (long)io->write_size);
}
else if (io->write_size < WRITE_SIZE_MAX
- && io->bytes_written >= WRITE_SIZE_THRESHOLD) {
+ && io->bytes_written >= io->tls_warmup_size) {
/* connection is hot, use max size */
io->write_size = WRITE_SIZE_MAX;
ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection,
int buffer_output;
int write_size;
apr_time_t last_write;
- apr_size_t bytes_written;
+ apr_int64_t bytes_written;
+
+ apr_time_t tls_cooldown_usecs;
+ apr_int64_t tls_warmup_size;
+
char *buffer;
apr_size_t buflen;
if (session->aborted) {
return NGHTTP2_ERR_CALLBACK_FAILURE;
}
+ /* Set the need to flush output when we have added one of the
+ * following frame types */
+ switch (frame->hd.type) {
+ case NGHTTP2_RST_STREAM:
+ case NGHTTP2_WINDOW_UPDATE:
+ case NGHTTP2_PUSH_PROMISE:
+ case NGHTTP2_PING:
+ case NGHTTP2_GOAWAY:
+ session->flush = 1;
+ break;
+ default:
+ break;
+
+ }
if (APLOGctrace2(session->c)) {
char buffer[256];
frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0]));
void *userp)
{
h2_session *session = (h2_session *)userp;
- (void)ngh2; (void)frame;
- ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c,
- "h2_session(%ld): on_frame_send", session->id);
+ (void)ngh2;
+ if (APLOGctrace2(session->c)) {
+ char buffer[256];
+ frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0]));
+ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c,
+ "h2_session(%ld): on_frame_send %s",
+ session->id, buffer);
+ }
return 0;
}
session->id, (int)stream_id, error_code);
}
+ /* always flush on eos */
+ session->flush = 1;
+
return 0;
}
return h2_session_status_from_apr_status(status);
}
-
#define NGH2_SET_CALLBACK(callbacks, name, fn)\
nghttp2_session_callbacks_set_##name##_callback(callbacks, fn)
nghttp2_session_consume(session->ngh2, stream_id, bytes_read);
}
+static apr_status_t h2_session_flush(h2_session *session)
+{
+ session->flush = 0;
+ return h2_conn_io_flush(&session->io);
+}
+
static apr_status_t h2_session_update_windows(h2_session *session)
{
return h2_mplx_in_update_windows(session->mplx, update_window, session);
{
apr_status_t status = APR_EAGAIN;
h2_stream *stream = NULL;
- int flush_output = 0;
AP_DEBUG_ASSERT(session);
/* Check that any pending window updates are sent. */
status = h2_session_update_windows(session);
- if (status == APR_SUCCESS) {
- flush_output = 1;
- }
- else if (!APR_STATUS_IS_EAGAIN(status)) {
+ if (status != APR_SUCCESS && !APR_STATUS_IS_EAGAIN(status)) {
return status;
}
status = APR_ECONNABORTED;
}
}
- flush_output = 1;
}
/* If we have responses ready, submit them now. */
while (!session->aborted
&& (stream = h2_mplx_next_submit(session->mplx, session->streams)) != NULL) {
status = h2_session_handle_response(session, stream);
- flush_output = 1;
}
if (!session->aborted && h2_session_resume_streams_with_data(session) > 0) {
- flush_output = 1;
}
- if (!session->aborted && !flush_output
- && timeout > 0 && !h2_session_want_write(session)) {
+ if (!session->aborted && !session->flush && timeout > 0
+ && !h2_session_want_write(session)) {
+ h2_session_flush(session);
status = h2_mplx_out_trywait(session->mplx, timeout, session->iowait);
if (status != APR_TIMEUP
&& h2_session_resume_streams_with_data(session) > 0) {
- flush_output = 1;
}
else {
/* nothing happened to ongoing streams, do some house-keeping */
status = APR_ECONNABORTED;
}
}
- flush_output = 1;
}
- if (flush_output) {
- h2_conn_io_flush(&session->io);
+ if (session->flush) {
+ h2_session_flush(session);
}
return status;
apr_status_t h2_session_read(h2_session *session, apr_read_type_e block)
{
AP_DEBUG_ASSERT(session);
+ if (block == APR_BLOCK_READ) {
+ /* before we do a blocking read, make sure that all our output
+ * is send out. Otherwise we might deadlock. */
+ h2_session_flush(session);
+ }
return h2_conn_io_read(&session->io, block, session_receive, session);
}
request_rec *r; /* the request that started this in case
* of 'h2c', NULL otherwise */
int aborted; /* this session is being aborted */
+ int flush; /* if != 0, flush output on next occasion */
apr_size_t frames_received; /* number of http/2 frames received */
apr_size_t max_stream_count; /* max number of open streams */
apr_size_t max_stream_mem; /* max buffer memory for a single stream */
* a maximum of timeout micro-seconds and return to the caller. If timeout
* occurred, APR_TIMEUP will be returned.
*/
-apr_status_t h2_session_write(h2_session *session,
- apr_interval_time_t timeout);
+apr_status_t h2_session_write(h2_session *session, apr_interval_time_t timeout);
/* Start submitting the response to a stream request. This is possible
* once we have all the response headers. */