Skip to content

Commit f1fe550

Browse files
committed
Resolve comments: gzip only, simplify _bulk_docs body handling
1 parent 7581daa commit f1fe550

6 files changed

Lines changed: 75 additions & 177 deletions

File tree

rel/overlay/etc/default.ini

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -724,11 +724,12 @@ partitioned||* = true
724724
; *.example.com:443:[2001:db8::1]:443
725725
;connect_to =
726726

727-
; Compression settings for replication
728-
;compress_requests = true
727+
; Compress outbound replication request bodies (_bulk_docs, _revs_diff) with gzip.
728+
; Disabled by default. Only gzip is supported. Enable only when talking to CouchDB
729+
; servers that support gzip Content-Encoding on inbound requests.
730+
;compress_requests = false
729731
;compress_min_size = 1024
730732
;compression_algorithm = gzip
731-
;accept_encodings = gzip, deflate
732733

733734

734735
; Some socket options that might boost performance in some scenarios:
Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,16 @@
1-
# CouchDB Replicator Compression
1+
# CouchDB Replicator Request Compression
22

3-
## Overview
3+
The replicator can optionally gzip-compress outbound request bodies for
4+
`_bulk_docs` and `_revs_diff`. This reduces bandwidth during replication.
5+
CouchDB already supports `Content-Encoding: gzip` on inbound requests, so no
6+
server-side changes are needed.
47

5-
The replicator now supports configurable HTTP compression to reduce bandwidth during replication.
6-
7-
## Configuration
8+
Compression is disabled by default. Relevant `[replicator]` config keys:
89

910
```ini
1011
[replicator]
11-
; Enable compression (default: true)
12-
compress_requests = true
13-
14-
; Minimum body size to compress in bytes (default: 1024)
15-
compress_min_size = 1024
16-
17-
; Algorithm: gzip (default), deflate
18-
compression_algorithm = gzip
19-
20-
; Accept these encodings in responses
21-
accept_encodings = gzip, deflate
12+
compress_requests = false
13+
compress_min_size = 1024 ; minimum body size in bytes before compressing
2214
```
2315

24-
## Algorithms
25-
26-
- **gzip** (default): Best compatibility, built-in to Erlang
27-
- **deflate**: Built-in to Erlang, slightly faster than gzip
28-
29-
## Statistics
30-
31-
- `couch_replicator.requests.compressed` - Total compressed requests
32-
- `couch_replicator.requests.compressed.{algorithm}` - Per-algorithm stats
16+
Metric: `couch_replicator.requests_compressed.gzip` — number of gzip-compressed requests sent.

src/couch_replicator/priv/stats_descriptions.cfg

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,7 @@
147147
{desc, <<"number of times DNS overrides were applied to replication requests">>}
148148
]}.
149149

150-
{[couch_replicator, requests_compressed], [
151-
{type, counter},
152-
{desc, <<"number of HTTP requests compressed by the replicator">>}
153-
]}.
154150
{[couch_replicator, requests_compressed, gzip], [
155151
{type, counter},
156152
{desc, <<"number of HTTP requests compressed with gzip by the replicator">>}
157153
]}.
158-
{[couch_replicator, requests_compressed, deflate], [
159-
{type, counter},
160-
{desc, <<"number of HTTP requests compressed with deflate by the replicator">>}
161-
]}.

src/couch_replicator/src/couch_replicator_api_wrap.erl

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,14 @@ ensure_full_commit(#httpdb{} = Db) ->
171171

172172
get_missing_revs(#httpdb{} = Db, IdRevs) ->
173173
JsonBody = {[{Id, couch_doc:revs_to_strs(Revs)} || {Id, Revs} <- IdRevs]},
174+
{Body, ExtraHeaders} = maybe_compress(?JSON_ENCODE(JsonBody)),
174175
send_req(
175176
Db,
176177
[
177178
{method, post},
178179
{path, "_revs_diff"},
179-
{body, ?JSON_ENCODE(JsonBody)},
180-
{headers, [{"Content-Type", "application/json"}]}
180+
{body, Body},
181+
{headers, [{"Content-Type", "application/json"} | ExtraHeaders]}
181182
],
182183
fun
183184
(200, _, {Props}) ->
@@ -477,40 +478,35 @@ update_docs(#httpdb{} = HttpDb, DocList, Options, UpdateType) ->
477478
% Note: nginx and other servers don't like PUT/POST requests without
478479
% a Content-Length header, so we can't do a chunked transfer encoding
479480
% and JSON encode each doc only before sending it through the socket.
480-
{Docs, Len} = lists:mapfoldl(
481+
{Docs, _} = lists:mapfoldl(
481482
fun
482483
(#doc{} = Doc, Acc) ->
483484
Json = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, [revs, attachments])),
484485
{Json, Acc + iolist_size(Json)};
485486
(Doc, Acc) ->
486487
{Doc, Acc + iolist_size(Doc)}
487488
end,
488-
byte_size(Prefix) + byte_size(Suffix) + length(DocList) - 1,
489+
0,
489490
DocList
490491
),
491-
BodyFun = fun
492-
(eof) ->
493-
eof;
494-
([]) ->
495-
{ok, Suffix, eof};
496-
([prefix | Rest]) ->
497-
{ok, Prefix, Rest};
498-
([Doc]) ->
499-
{ok, Doc, []};
500-
([Doc | RestDocs]) ->
501-
{ok, [Doc, ","], RestDocs}
502-
end,
492+
% Collect the full body into a binary so we can optionally gzip it.
493+
% Content-Length is required (nginx etc. reject chunked PUT/POST).
494+
RawBody = iolist_to_binary(
495+
[Prefix, lists:join(",", Docs), Suffix]
496+
),
497+
{Body, ExtraHeaders} = maybe_compress(RawBody),
503498
Headers = [
504-
{"Content-Length", Len},
499+
{"Content-Length", byte_size(Body)},
505500
{"Content-Type", "application/json"},
506501
{"X-Couch-Full-Commit", FullCommit}
502+
| ExtraHeaders
507503
],
508504
send_req(
509505
HttpDb,
510506
[
511507
{method, post},
512508
{path, "_bulk_docs"},
513-
{body, {BodyFun, [prefix | Docs]}},
509+
{body, Body},
514510
{headers, Headers}
515511
],
516512
fun
@@ -1052,6 +1048,33 @@ header_value(Key, Headers, Default) ->
10521048
_ ->
10531049
Default
10541050
end.
1051+
1052+
%% Compress Body with gzip if enabled and body is large enough.
1053+
%% Returns {Body, ExtraHeaders} where ExtraHeaders may contain Content-Encoding.
1054+
maybe_compress(Body) when is_binary(Body) ->
1055+
case config:get_boolean("replicator", "compress_requests", false) of
1056+
true ->
1057+
Algorithm = config:get("replicator", "compression_algorithm", "gzip"),
1058+
MinSize = config:get_integer("replicator", "compress_min_size", 1024),
1059+
case byte_size(Body) >= MinSize of
1060+
true -> compress_with(Algorithm, Body);
1061+
false -> {Body, []}
1062+
end;
1063+
false ->
1064+
{Body, []}
1065+
end.
1066+
1067+
compress_with("gzip", Body) ->
1068+
Compressed = zlib:gzip(Body),
1069+
couch_stats:increment_counter([couch_replicator, requests_compressed]),
1070+
couch_stats:increment_counter([couch_replicator, requests_compressed, gzip]),
1071+
{Compressed, [{"Content-Encoding", "gzip"}]};
1072+
compress_with(Other, Body) ->
1073+
couch_log:warning(
1074+
"~p: unsupported compression_algorithm ~p, skipping compression",
1075+
[?MODULE, Other]
1076+
),
1077+
{Body, []}.
10551078

10561079
% Normalize an #httpdb{} or #db{} record such that it can be used for
10571080
% comparisons. This means remove things like pids and also sort options / props.

src/couch_replicator/src/couch_replicator_httpc.erl

Lines changed: 4 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -41,80 +41,6 @@
4141
% where we may end up processing an unbounded number of messages.
4242
-define(MAX_DISCARDED_MESSAGES, 100).
4343

44-
should_compress_request(Body) when is_binary(Body) ->
45-
MinSize = config:get_integer("replicator", "compress_min_size", 1024),
46-
byte_size(Body) >= MinSize;
47-
should_compress_request(Body) when is_list(Body) ->
48-
should_compress_request(iolist_to_binary(Body));
49-
should_compress_request(_) ->
50-
false.
51-
52-
get_compression_algorithm() ->
53-
% Supported: gzip (default), deflate
54-
Algorithm = config:get("replicator", "compression_algorithm", "gzip"),
55-
case Algorithm of
56-
"gzip" -> gzip;
57-
"deflate" -> deflate;
58-
_ ->
59-
couch_log:warning(
60-
"couch_replicator_httpc: Unknown compression algorithm ~p, using gzip",
61-
[Algorithm]
62-
),
63-
gzip
64-
end.
65-
66-
compress_body(Body) when is_binary(Body) ->
67-
compress_body_with_algorithm(Body, get_compression_algorithm());
68-
compress_body(Body) when is_list(Body) ->
69-
compress_body(iolist_to_binary(Body));
70-
compress_body(Body) ->
71-
Body.
72-
73-
compress_body_with_algorithm(Body, gzip) ->
74-
zlib:gzip(Body);
75-
compress_body_with_algorithm(Body, deflate) ->
76-
zlib:compress(Body).
77-
78-
get_content_encoding(gzip) -> "gzip";
79-
get_content_encoding(deflate) -> "deflate".
80-
81-
decompress_body(Headers, Body) ->
82-
case lists:keyfind("Content-Encoding", 1, Headers) of
83-
{"Content-Encoding", Encoding} ->
84-
decompress_body_with_encoding(Encoding, Body);
85-
_ ->
86-
Body
87-
end.
88-
89-
decompress_body_with_encoding("gzip", Body) ->
90-
try
91-
zlib:gunzip(Body)
92-
catch
93-
error:data_error ->
94-
couch_log:warning(
95-
"couch_replicator_httpc: Failed to decompress gzip response, using original",
96-
[]
97-
),
98-
Body
99-
end;
100-
decompress_body_with_encoding("deflate", Body) ->
101-
try
102-
zlib:uncompress(Body)
103-
catch
104-
error:data_error ->
105-
couch_log:warning(
106-
"couch_replicator_httpc: Failed to decompress deflate response, using original",
107-
[]
108-
),
109-
Body
110-
end;
111-
decompress_body_with_encoding(Other, Body) ->
112-
couch_log:warning(
113-
"couch_replicator_httpc: Unknown content encoding ~p, using original body",
114-
[Other]
115-
),
116-
Body.
117-
11844
setup(Db) ->
11945
#httpdb{
12046
httpc_pool = nil,
@@ -183,36 +109,11 @@ stop_http_worker() ->
183109

184110
send_ibrowse_req(#httpdb{headers = BaseHeaders} = HttpDb0, Params) ->
185111
Method = get_value(method, Params, get),
186-
UserHeaders0 = get_value(headers, Params, []),
187-
% Accept multiple compression algorithms
188-
AcceptEncodings = config:get("replicator", "accept_encodings", "gzip, deflate, zstd"),
189-
UserHeaders1 = case lists:keyfind("Accept-Encoding", 1, UserHeaders0) of
190-
false -> [{"Accept-Encoding", AcceptEncodings} | UserHeaders0];
191-
_ -> UserHeaders0
192-
end,
193-
Body0 = get_value(body, Params, []),
194-
CompressEnabled = config:get_boolean("replicator", "compress_requests", true),
195-
ShouldCompress = should_compress_request(Body0),
196-
{Body, UserHeaders2} = case CompressEnabled andalso ShouldCompress of
197-
true ->
198-
Algorithm = get_compression_algorithm(),
199-
CompressedBody = compress_body(Body0),
200-
ContentEncoding = get_content_encoding(Algorithm),
201-
UpdatedHeaders = case lists:keyfind("Content-Encoding", 1, UserHeaders1) of
202-
false -> [{"Content-Encoding", ContentEncoding} | UserHeaders1];
203-
_ -> UserHeaders1
204-
end,
205-
% Track compression algorithm usage
206-
couch_stats:increment_counter([couch_replicator, requests_compressed]),
207-
couch_stats:increment_counter([couch_replicator, requests_compressed, Algorithm]),
208-
{CompressedBody, UpdatedHeaders};
209-
false ->
210-
{Body0, UserHeaders1}
211-
end,
212-
213-
Headers1 = merge_headers(BaseHeaders, UserHeaders2),
112+
UserHeaders = get_value(headers, Params, []),
113+
Headers1 = merge_headers(BaseHeaders, UserHeaders),
214114
{Headers2, HttpDb} = couch_replicator_auth:update_headers(HttpDb0, Headers1),
215115
Url0 = full_url(HttpDb, Params),
116+
Body = get_value(body, Params, []),
216117
case get_value(path, Params) == "_changes" of
217118
true ->
218119
Timeout = infinity;
@@ -282,9 +183,6 @@ process_response({error, connection_closing}, Worker, HttpDb, Params, _Cb) ->
282183
process_response({ibrowse_req_id, ReqId}, Worker, HttpDb, Params, Callback) ->
283184
process_stream_response(ReqId, Worker, HttpDb, Params, Callback);
284185
process_response({ok, Code, Headers, Body}, Worker, HttpDb, Params, Callback) ->
285-
% Decompress body if it's gzip compressed
286-
DecompressedBody = decompress_body(Headers, Body),
287-
288186
case list_to_integer(Code) of
289187
R when R =:= 301; R =:= 302; R =:= 303 ->
290188
backoff_success(HttpDb, Params),
@@ -298,7 +196,7 @@ process_response({ok, Code, Headers, Body}, Worker, HttpDb, Params, Callback) ->
298196
backoff_success(HttpDb, Params),
299197
couch_stats:increment_counter([couch_replicator, responses, success]),
300198
EJson =
301-
case DecompressedBody of
199+
case Body of
302200
<<>> ->
303201
null;
304202
Json ->

src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,32 +26,32 @@ compression_test_() ->
2626
fun couch_replicator_test_helper:test_setup/0,
2727
fun couch_replicator_test_helper:test_teardown/1,
2828
[
29-
?TDEF_FE(should_compress_http_requests, ?TIMEOUT_EUNIT)
29+
?TDEF_FE(should_not_compress_by_default, ?TIMEOUT_EUNIT),
30+
?TDEF_FE(should_compress_when_enabled, ?TIMEOUT_EUNIT)
3031
]
3132
}
3233
}.
3334

34-
should_compress_http_requests({_Ctx, {Source, Target}}) ->
35-
config:set("replicator", "compress_min_size", "10", false),
35+
should_not_compress_by_default({_Ctx, {Source, Target}}) ->
36+
Before = couch_stats:sample([couch_replicator, requests_compressed, gzip]),
37+
populate_db(Source, ?DOCS_COUNT),
38+
replicate(Source, Target),
39+
compare_dbs(Source, Target),
40+
After = couch_stats:sample([couch_replicator, requests_compressed, gzip]),
41+
?assertEqual(Before, After).
42+
43+
should_compress_when_enabled({_Ctx, {Source, Target}}) ->
3644
config:set("replicator", "compress_requests", "true", false),
45+
config:set("replicator", "compress_min_size", "10", false),
3746
config:set("replicator", "compression_algorithm", "gzip", false),
38-
InitialCompressed = couch_stats:sample([couch_replicator, requests_compressed]),
39-
InitialGzip = couch_stats:sample([couch_replicator, requests_compressed, gzip]),
40-
?assertEqual(0, InitialCompressed),
41-
?assertEqual(0, InitialGzip),
47+
Before = couch_stats:sample([couch_replicator, requests_compressed, gzip]),
4248
populate_db(Source, ?DOCS_COUNT),
4349
replicate(Source, Target),
44-
compare_dbs(Source, Target),
45-
FinalCompressed = couch_stats:sample([couch_replicator, requests_compressed]),
46-
FinalGzip = couch_stats:sample([couch_replicator, requests_compressed, gzip]),
47-
?assertEqual(?DOCS_COUNT, FinalCompressed,
48-
io_lib:format("Expected ~p compressed requests, got: ~p", [?DOCS_COUNT, FinalCompressed])),
49-
?assertEqual(?DOCS_COUNT, FinalGzip,
50-
io_lib:format("Expected ~p gzip requests, got: ~p", [?DOCS_COUNT, FinalGzip])),
51-
?assertEqual(FinalCompressed, FinalGzip,
52-
"All compressed requests should use gzip algorithm"),
53-
config:delete("replicator", "compress_min_size", false),
50+
compare_dbs(Source, Target),
51+
After = couch_stats:sample([couch_replicator, requests_compressed, gzip]),
52+
?assert(After > Before),
5453
config:delete("replicator", "compress_requests", false),
54+
config:delete("replicator", "compress_min_size", false),
5555
config:delete("replicator", "compression_algorithm", false).
5656

5757
populate_db(DbName, Count) ->

0 commit comments

Comments
 (0)