<div dir="auto"><div style="font-family:sans-serif;font-size:12.8px" dir="auto"><div style="margin:16px 0px"><div><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div>See <a href="http://lists.ceph.com/pipermail/ceph-users-ceph.com/2018-September/029780.html" style="text-decoration-line:none;color:rgb(66,133,244)" rel="noreferrer noreferrer noreferrer noreferrer noreferrer" target="_blank">http://lists.ceph.com/pipermail/ceph-users-ceph.com/2018-September/029780.html</a> for the original thread.<br></div><div><br></div><div>Here is a trivial reproducer not using any aio or dynamically allocated memory to store the objects read.</div><div>It simply reads 20,000 1MB large objects sequentially: when run, instead of using a roughly fixed amount of memory, the RSS increases up to 400 MB.</div><div><br></div><div><code></div><div>#include <assert.h><br></div><div><div>#include <stdio.h></div><div>#include <stdlib.h></div><div>#include <string.h></div><div>#include <rados/librados.h></div><div><br></div><div>#define SIZE 1000000</div><div><br></div><div>void readobj(rados_ioctx_t* io, char objname[], unsigned long size) {</div><div>    static char data[SIZE];</div><div>    unsigned long bytes_read;</div><div>    int retval;</div><div><br></div><div>    rados_read_op_t read_op = rados_create_read_op();</div><div>    rados_read_op_read(read_op, 0, size, data, &bytes_read, &retval);</div><div>    retval = rados_read_op_operate(read_op, *io, objname, 0);</div><div>    assert(retval == 0);</div><div>    rados_release_read_op(read_op);</div><div>}</div><div><br></div><div>int main() {</div><div>    rados_t cluster;</div><div>    rados_create(&cluster, "test");</div><div>    rados_conf_read_file(cluster, "/etc/ceph/ceph.conf");</div><div>    rados_connect(cluster);</div><div><br></div><div>    rados_ioctx_t io;</div><div>    rados_ioctx_create(cluster, "test", &io);</div><div><br></div><div>    char smallobj_name[16];</div><div>    int i; </div><div>    long long total_bytes_read = 0;</div><div><br></div><div>    for (i = 0; i < 20000; i++) {</div><div>        sprintf(smallobj_name, "1mobj_%d", i);</div><div>        readobj(&io, smallobj_name, SIZE);</div><div>        readobj(&io, smallobj_name, 1);</div><div><br></div><div>        total_bytes_read += SIZE;</div><div>        printf("Read %s for total %lld\n", smallobj_name, total_bytes_read);</div><div>    }</div><div><br></div><div>    return 0;</div><div>}</div></div><div></code></div><div><br></div><div dir="ltr"><div><br></div><div>Running it under valgrind with --show-reachable=yes shows where the problem is:</div><div><br></div><div><div>==10253== 423,755,776 bytes in 424 blocks are still reachable in loss record 882 of 882</div><div>==10253==    at 0x4C2FEA6: memalign (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)</div><div>==10253==    by 0x4C2FFB1: posix_memalign (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)</div><div>==10253==    by 0x57A3C06: ceph::buffer::create_aligned_in_mempool(unsigned int, unsigned int, int) (in /usr/lib/ceph/libceph-common.so.0)</div><div>==10253==    by 0x5984B49: AsyncConnection::process() (in /usr/lib/ceph/libceph-common.so.0)</div><div>==10253==    by 0x5994C97: EventCenter::process_events(int, std::chrono::duration<unsigned long, std::ratio<1l, 1000000000l> >*) (in /usr/lib/ceph/libceph-common.so.0)</div><div>==10253==    by 0x5999817: ??? (in /usr/lib/ceph/libceph-common.so.0)</div><div>==10253==    by 0xE960C7F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)</div><div>==10253==    by 0xE6926B9: start_thread (pthread_create.c:333)</div><div>==10253==    by 0x52A041C: clone (clone.S:109)</div></div><div><br></div><div>We can see that the memory is allocated in AsyncConnection::process, precisely here: <a href="https://github.com/ceph/ceph/blob/master/src/msg/async/AsyncConnection.cc#L614" style="text-decoration-line:none;color:rgb(66,133,244)" rel="noreferrer noreferrer noreferrer noreferrer noreferrer" target="_blank">https://github.com/ceph/ceph/blob/master/src/msg/async/AsyncConnection.cc#L614</a><br></div><div><br></div><div>From my understanding - I have never read ceph code before, so my understanding might be flawed - this code is allocating a buffer to store the data contained in the message received from a ceph node and adding it to a buffers list `data_buf`.<br></div><div><div>Note that this list of buffers is per AsyncConnection, that's important.</div><div><br></div><div>Unfortunately, looking through the file shows that `data_buf` only gets cleared here: <a href="https://github.com/ceph/ceph/blob/master/src/msg/async/AsyncConnection.cc#L477" style="text-decoration-line:none;color:rgb(66,133,244)" rel="noreferrer noreferrer noreferrer noreferrer noreferrer" target="_blank">https://github.com/ceph/ceph/blob/master/src/msg/async/AsyncConnection.cc#L477</a></div><div>when it transitions to the STATE_OPEN_MESSAGE_HEADER state, IIUC only occurs when it starts receiving a new <span style="color:rgb(36,41,46);font-family:'sfmono-regular','consolas','liberation mono','menlo','courier',monospace;font-size:12px;white-space:pre-wrap">CEPH_MSGR_TAG_MSG </span>message.</div><div><br></div><div>Which means that basically the following happens - the below is for a single AsyncConnection:</div><div><br></div><div>repeat:</div><div>   0. wait for message</div><div>   1. parse message header</div><div>   2. realize it's an <span style="color:rgb(36,41,46);font-family:'sfmono-regular','consolas','liberation mono','menlo','courier',monospace;font-size:12px;white-space:pre-wrap">CEPH_MSGR_TAG_MSG</span> message (not a heartbeat, etc): transition to STATE_OPEN_MESSAGE_HEADER</div><div>   3. clear all buffers (<a href="https://github.com/ceph/ceph/blob/master/src/msg/async/AsyncConnection.cc#L477" style="text-decoration-line:none;color:rgb(66,133,244)" rel="noreferrer noreferrer noreferrer noreferrer noreferrer" target="_blank">https://github.com/ceph/ceph/blob/master/src/msg/async/AsyncConnection.cc#L477</a>)</div><div>   4. start parsing message, allocate memory buffer (<a href="https://github.com/ceph/ceph/blob/master/src/msg/async/AsyncConnection.cc#L614" style="text-decoration-line:none;color:rgb(66,133,244)" rel="noreferrer noreferrer noreferrer noreferrer noreferrer" target="_blank">https://github.com/ceph/ceph/blob/master/src/msg/async/AsyncConnection.cc#L614</a>)</div><div>   ...</div><div>   n. done processing message</div><div><br></div><div>Which means that the memory allocated when processing a message - at step 4 - is only released when a subsequent <span style="color:rgb(36,41,46);font-family:'sfmono-regular','consolas','liberation mono','menlo','courier',monospace;font-size:12px;white-space:pre-wrap">CEPH_MSGR_TAG_MSG</span> message is received.</div><div>For a single connection that's not too bad - memory is just freed later than it could, but with a large OSD pool, it adds up.</div><div>Our cluster is made of over 40 ceph OSDs, which results in around 400 connections. Which means that if reading for example 1MB objects, this delayed freeing can use up to 400MB (1MB per connection), which is exactly what we're seeing.</div><div><br></div><div>It would also explains:</div><div>- why reading a small subset of objects over and over again doesn't result in a memory increase: because they are distributed over few ceph OSDs, hence use few connections</div><div>- the shape of the memory consumption over time: if it were a leak (even with reachable objects), it would grow linearly, whereas we expect it to grow slower and slower as we've connected all ceph OSDs in the pool</div><div>- why interspersing small reads helps: if one does a large read follows by a small read over the same connection, the large read buffer will be cleared before the small read buffer is allocated</div><div><br></div><div>The followingg is enough to keep the memory usage under control:</div></div><div><diff></div><div><div>--- a.c     2018-09-14 12:56:13.770350595 +0100</div><div>+++ b.c        2018-09-14 12:55:32.921856548 +0100</div><div>@@ -34,7 +34,6 @@</div><div>     for (i = 0; i < 20000; i++) {</div><div>         sprintf(smallobj_name, "1mobj_%d", i);</div><div>         readobj(&io, smallobj_name, SIZE);</div><div>+        readobj(&io, smallobj_name, 1);</div><div> </div><div>         total_bytes_read += SIZE;</div><div>         printf("Read %s for total %lld\n", smallobj_name, total_bytes_read);</div></div><div></diff></div><div><br></div><div>Reading just 1 byte of the same object after having read the object will make the AsyncConnection clear the buffer allocated for processing the large read result, and allocate a tiny buffer for the 1 byte read result.</div><div><br></div><div><br></div><div>TLDR: It seems that the buffers allocated while processing a message in AsyncConnection are not cleared when the message has been processed, but only when a subsequent message is received, which yields to a significant memory usage if there are many connections in use (objects stored on many different OSDs).</div></div></div></div></div></div></div></div></div></div></div><div style="height:0px"></div></div><div style="font-family:sans-serif;font-size:12.8px;height:96px" dir="auto"></div><br></div>