Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

疑似keytable内存泄漏 #2756

Closed
lsdh-fei opened this issue Sep 4, 2024 · 12 comments
Closed

疑似keytable内存泄漏 #2756

lsdh-fei opened this issue Sep 4, 2024 · 12 comments

Comments

@lsdh-fei
Copy link

lsdh-fei commented Sep 4, 2024

Describe the bug (描述bug)
升级1.10之后,内存会不断增长,在vars界面查看,发现bthread_keytable_count在不断增长,我理解keytable是thread local的,thread销毁后也会跟着销毁,不应该无限增长

To Reproduce (复现方法)
使用1.10之后就会出现,但是用1.8就不存在泄漏

Expected behavior (期望行为)
keytable对象数量维持动态平衡

Versions (各种版本)
OS: Ubuntu 20.04.3 LTS
Compiler: clang8
brpc: 1.10
protobuf: 3.15.8

Additional context/screenshots (更多上下文/截图)

image

@lsdh-fei
Copy link
Author

lsdh-fei commented Sep 4, 2024

@wwbmmm @MJY-HUST 看了下提交记录,不知道是不是 #2645 引入的,辛苦看下哈

@chenBright
Copy link
Contributor

thread销毁后也会跟着销毁,不应该无限增长

自己起的线程中使用bthread_keytable吗?

@lsdh-fei
Copy link
Author

lsdh-fei commented Sep 4, 2024

@chenBright 不是,就是rpc处理的bthread中使用的

@MJY-HUST
Copy link
Contributor

MJY-HUST commented Sep 4, 2024

使用bthread-local的场景是什么样的呢@lsdh-fei
在使用bthread-local变量时,如果create的bthread的addr中没有初始化bthread_keytable_pool_t的话,那么每次return_keytable都会直接析构keytable;另外如果初始化了bthread_keytable_pool_t的话,使用bthread-local变量时需要先调用bthread_getspecific尝试borrow_keytable进行复用,如果每次都是直接bthread_setspecific赋值的话,如果当前bthread不存在keytable的话,就会new一个新的keytable,内存就会不断增长,这个是之前就有的问题

@lsdh-fei
Copy link
Author

lsdh-fei commented Sep 4, 2024

@MJY-HUST 没有单独使用bthread-local,就是task_runner中的,我门最近把brpc从1.8升级到了1.10,然后就发现新版有内存泄漏,LogStream占用了大量内存,看了下brpc源代码,发现log stream的buf是存在keytable中,然后就怀疑是keytable的问题,然后看vars监控,发现keytable的数量一直在不断增长,所以才怀疑是新版的改动导致的

@lsdh-fei
Copy link
Author

lsdh-fei commented Sep 4, 2024

现在观察到的现象是经常会出现 LogStream** get_or_new_tls_stream_array() => bthread_getspecific 无法获取LogStream,然后new了LogStream之后通过 bthread_setspecific 设置到tls_bls.keytable,但是同时在这里new一个KeyTable。所以新增的LogStream和KeyTable的数量是相同的。
image
image
应该就是new log stream时创建的keytable泄漏了

@fausturs
Copy link

fausturs commented Sep 5, 2024

我提供一个个人的角度,可以讨论一下。

因为新的keytable的策略,是return到一个pthread 的thread local的list中。但没办法保证的是,同一个keytable被borrow后,会被return到同一个pthread里。

本来其实也没什么问题,只要等待足够时间,每个pthread中,就会create足够的keytable。但是可能,目前的一些task的调度策略,会使得pthread中的keytable数量像波浪一样震荡,使得keytable稳定的总数大大增加。

可以看这么一个例子

void* print_log(void *) {
    std::random_device rd;
    std::mt19937 mt{rd()};
    std::uniform_int_distribution<uint64_t> uid(0, 100000);

    bthread_usleep(50000 + uid(mt));
    LOG(INFO) << "some log here xxx";
    return NULL;
}

class GreeterImpl final : public Greeter {
    void SayHello(google::protobuf::RpcController* controller, const HelloRequest* request, HelloReply* response, google::protobuf::Closure* done) override {
        brpc::ClosureGuard done_guard(done);
        brpc::Controller* cntl = static_cast<brpc::Controller*>(controller);

        (void)print_log(NULL);

        std::string message = "Hello " + request->name() + "!\n";
        response->set_message(message);
    }
};

image

先做sleep,再进行LOG(相当于使用keytable),可以看到keytable的数量非常稳定。因为这里,每个pthread里,有一个keytable就可以了。

但我们换一下位置

void* print_log(void *) {
    std::random_device rd;
    std::mt19937 mt{rd()};
    std::uniform_int_distribution<uint64_t> uid(0, 100000);

    LOG(INFO) << "some log here xxx";
    bthread_usleep(50000 + uid(mt));
    return NULL;
}

class GreeterImpl final : public Greeter {
    void SayHello(google::protobuf::RpcController* controller, const HelloRequest* request, HelloReply* response, google::protobuf::Closure* done) override {
        brpc::ClosureGuard done_guard(done);
        brpc::Controller* cntl = static_cast<brpc::Controller*>(controller);

        (void)print_log(NULL);

        std::string message = "Hello " + request->name() + "!\n";
        response->set_message(message);
    }
};

image
可以看到keytable有一个持续的增长。

这里从统计意义上来说,增长可能会一直存在,因为当前worker的task sleep了(或者是等待某个锁),之后它好像会尝试从其他队列steal一个pendding的任务。随着这些任务都sleep(或者都block在某个锁上),这个work所在的pthread的存量keyTable会被持续消耗。而且这些keytable大概率不会被return到这个pthread了。

所以,pthread中的keytables,不是维持在一个较为稳定的状态,而是有着潮水一样的涨落。随着borrow的失败,只能不停的new新的keytable

@MJY-HUST
Copy link
Contributor

MJY-HUST commented Sep 5, 2024

是的,假设每个bthread都持有keytable的前提下,如果pendding的bthread数量过多(持续增长),那么内存消耗是无法避免的。
如果是因为task调度的不均衡(极端情况下一个task group只执行需要创建/获取keytable的任务,然后yield,这些任务最终在其他的task group中执行完成),那么也会出现内存持续增长的情况。这种情况下的一种解决方法是为每个task group的 tls keytable list的长度设置一个阈值,一旦超过之后加锁批量返回给一个全局链表;而当borrow_keytable失败需要new keytable时,先查看全局链表中是否有keytable,再加锁批量取一批,不直接new keytable。

@fausturs
Copy link

fausturs commented Sep 5, 2024

这个解决方案看上去很棒。
因为现有的borrow_keytable中,已经包含了对thread local 的keytable为空时,落到另一个全局的list(pool->free_keytables)中的逻辑。

看上去,只需要对return_keytable逻辑进行一些调整。

期待大佬们给出修复。

@chenBright
Copy link
Contributor

@MJY-HUST 10月节后会发新版本,能赶上新版本修复这个问题吗?

@MJY-HUST
Copy link
Contributor

@MJY-HUST 10月节后会发新版本,能赶上新版本修复这个问题吗?
周末修复一下,提个pr

@chenBright
Copy link
Contributor

Closed this as completed in #2768.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants