Skip to content
New issue

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

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

Already on GitHub? # to your account

Use butil::ThreadLocal to store keytable #2645

Merged
merged 8 commits into from
Jul 7, 2024

Conversation

MJY-HUST
Copy link
Contributor

@MJY-HUST MJY-HUST commented May 19, 2024

What problem does this PR solve?

Issue Number:
#2635
Problem Summary:
当brpc server 下处理的应用执行时间较短(bthread生命周期短),且使用了bthread_local变量时,由于keytable 由bthread_keytable_pool_t中的一个全局链表维护,borrow_keytable、return_keytable时内部加互斥锁,导致锁成为瓶颈。
image

基于此,重新设计了bthread_keytable_pool_t:
新的bthread_keytable_pool_t结构体中,使用butil::ThreadLocalbthread::KeyTableList* list 保存TLS的KeyTable list。同时保留原有的free_keytables,不改变reserved_thread_local_data的语义,调用bthread_keytable_pool_reserve会在全局链表中预分配keytable.原有的互斥锁改为读写锁。

  • borrow_keytable时优先从本地TLS链表中查找是否有可用的KeyTable,如果没有,且free_keytables不为NULL,再去从全局链表中加写锁获取keytable;如果都没有可用table,则返回NULL。
  • return_keytable时只会将keytable插入TLS的KeyTable list中,由于是TLS变量,只有当bthread_keytable_pool_destroy才会产生竞争,所以只需要加写锁。
  • bthread_keytable_pool_destroy时加写锁,首先delete butil::ThreadLocalbthread::KeyTableList* list,pool->destroyed = 1.然后如果free_keytables不为NULL,再释放free_keytables的空间。
  • borrow_keytable、return_keytable内部只加读锁,bthread_keytable_pool_destroy、bthread_keytable_pool_reserve内部加写锁。sever正常运行时,一般只会调用borrow_keytable、return_keytable接口,不会产生锁的竞争。

What is changed and the side effects?

Changed:

Side effects:

  • Performance effects(性能影响):

  • Breaking backward compatibility(向后兼容性):


Check List:

  • Please make sure your changes are compilable(请确保你的更改可以通过编译).
  • When providing us with a new feature, it is best to add related tests(如果你向我们增加一个新的功能, 请添加相关测试).
  • Please follow Contributor Covenant Code of Conduct.(请遵循贡献者准则).

@chenBright
Copy link
Contributor

object_pool应该没法完全解决 #1449 的问题吧,从object_pool取出来的KeyTable有可能已经有data了,先调用bthread_setspecific函数,旧data还是会泄漏。

@chenBright
Copy link
Contributor

brpc/src/brpc/server.h

Lines 170 to 183 in b601c89

// The factory to create/destroy data attached to each searching thread
// in server.
// If this option is NULL, brpc::thread_local_data() is always NULL.
// NOT owned by Server and must be valid when Server is running.
// Default: NULL
const DataFactory* thread_local_data_factory;
// Prepare so many thread-local data before server starts, so that calls
// to brpc::thread_local_data() get data directly rather than calling
// thread_local_data_factory->Create() at first time. Useful when Create()
// is slow, otherwise the RPC session may be blocked by the creation
// of data and not served within timeout.
// Default: 0
size_t reserved_thread_local_data;

brpc/src/brpc/server.cpp

Lines 888 to 914 in b601c89

// Init _keytable_pool always. If the server was stopped before, the pool
// should be destroyed in Join().
_keytable_pool = new bthread_keytable_pool_t;
if (bthread_keytable_pool_init(_keytable_pool) != 0) {
LOG(ERROR) << "Fail to init _keytable_pool";
delete _keytable_pool;
_keytable_pool = NULL;
return -1;
}
if (_options.thread_local_data_factory) {
_tl_options.thread_local_data_factory = _options.thread_local_data_factory;
if (bthread_key_create2(&_tl_options.tls_key, DestroyServerTLS,
_options.thread_local_data_factory) != 0) {
LOG(ERROR) << "Fail to create thread-local key";
return -1;
}
if (_options.reserved_thread_local_data) {
bthread_keytable_pool_reserve(_keytable_pool,
_options.reserved_thread_local_data,
_tl_options.tls_key,
CreateServerTLS,
_options.thread_local_data_factory);
}
} else {
_tl_options = ThreadLocalOptions();
}

应该没法废弃bthread_keytable_pool_t ,因为Server已经用了bthread_keytable_pool_t了,并对外提供了与之相关的设置项。

改造bthread_keytable_pool_t,将free_keytables由全局改成TLS,这样mutex就可以废弃了。同时修改reserved_thread_local_data语义:每个TLS上预留的data数量。这样可行吗?

@MJY-HUST
Copy link
Contributor Author

MJY-HUST commented May 20, 2024

brpc/src/brpc/server.h

Lines 170 to 183 in b601c89

// The factory to create/destroy data attached to each searching thread
// in server.
// If this option is NULL, brpc::thread_local_data() is always NULL.
// NOT owned by Server and must be valid when Server is running.
// Default: NULL
const DataFactory* thread_local_data_factory;
// Prepare so many thread-local data before server starts, so that calls
// to brpc::thread_local_data() get data directly rather than calling
// thread_local_data_factory->Create() at first time. Useful when Create()
// is slow, otherwise the RPC session may be blocked by the creation
// of data and not served within timeout.
// Default: 0
size_t reserved_thread_local_data;

brpc/src/brpc/server.cpp

Lines 888 to 914 in b601c89

// Init _keytable_pool always. If the server was stopped before, the pool
// should be destroyed in Join().
_keytable_pool = new bthread_keytable_pool_t;
if (bthread_keytable_pool_init(_keytable_pool) != 0) {
LOG(ERROR) << "Fail to init _keytable_pool";
delete _keytable_pool;
_keytable_pool = NULL;
return -1;
}
if (_options.thread_local_data_factory) {
_tl_options.thread_local_data_factory = _options.thread_local_data_factory;
if (bthread_key_create2(&_tl_options.tls_key, DestroyServerTLS,
_options.thread_local_data_factory) != 0) {
LOG(ERROR) << "Fail to create thread-local key";
return -1;
}
if (_options.reserved_thread_local_data) {
bthread_keytable_pool_reserve(_keytable_pool,
_options.reserved_thread_local_data,
_tl_options.tls_key,
CreateServerTLS,
_options.thread_local_data_factory);
}
} else {
_tl_options = ThreadLocalOptions();
}

应该没法废弃bthread_keytable_pool_t ,因为Server已经用了bthread_keytable_pool_t了,并对外提供了与之相关的设置项。

改造bthread_keytable_pool_t,将free_keytables由全局改成TLS,这样mutex就可以废弃了。同时修改reserved_thread_local_data语义:每个TLS上预留的data数量。这样可行吗?

如果使用TLS的话,当server结束调用Join时,通过bthread_keytable_pool_destroy方法需要删除所有线程内保存的TLS free_keytables才行,感觉也不是很方便?

我的理解,目前的实现是一个server会绑定一个全局的bthread_keytable_pool_t,在server的声明周期内,server和其生成的bthread使用同一个pool,在server结束时会销毁所有创建的KeyTable,通过bthread_keytable_pool_t内的链表;然后会从全局的KeyInfo中删除所有注册的Key。但是使用全局一个链表会存在锁的竞争问题,不知道理解的对不对。

@MJY-HUST
Copy link
Contributor Author

MJY-HUST commented May 20, 2024

改造bthread_keytable_pool_t ,其中引入一个类似Object_pool的成员,通过在成员内部使用tls变量来避免加互斥锁。然后对bthread_keytable_pool_t 中的锁的使用从互斥锁改为读写锁,来维护destroyed的值。在return_keytable内使用读锁,bthread_keytable_pool_init,bthread_keytable_pool_destroy时加写锁。这样就可以避免在sever未退出时多个线程return_keytable产生的锁的竞争。
不知道这样是否可行

@chenBright
Copy link
Contributor

chenBright commented May 21, 2024

我的理解,目前的实现是一个server会绑定一个全局的bthread_keytable_pool_t,在server的声明周期内,server和其生成的bthread使用同一个pool,在server结束时会销毁所有创建的KeyTable,通过bthread_keytable_pool_t内的链表;然后会从全局的KeyInfo中删除所有注册的Key。但是使用全局一个链表会存在锁的竞争问题,不知道理解的对不对。

嗯,我也是这样理解的。不过一般接口实现的瓶颈都是在业务逻辑,所以这个锁的影响应该不大。

@chenBright
Copy link
Contributor

chenBright commented May 21, 2024

改造bthread_keytable_pool_t ,其中引入一个类似Object_pool的成员,通过在成员内部使用tls变量来避免加互斥锁。然后对bthread_keytable_pool_t 中的锁的使用从互斥锁改为读写锁,来维护destroyed的值。在return_keytable内使用读锁,bthread_keytable_pool_init,bthread_keytable_pool_destroy时加写锁。这样就可以避免在sever未退出时多个线程return_keytable产生的锁的竞争。 不知道这样是否可行

之前考虑过在bthread_keytable_pool_t中使用ObjectPool,但是ObjectPool好像不能实现reserved_thread_local_data吧?

destroyed应该可以用原子变量,就不用锁了吧。

@chenBright
Copy link
Contributor

如果使用TLS的话,当server结束调用Join时,通过bthread_keytable_pool_destroy方法需要删除所有线程内保存的TLS free_keytables才行,感觉也不是很方便?

用butil::ThreadLocal应该挺方便的吧。
bthread_keytable_pool_init的时候,new一个butil::ThreadLocal。
bthread_keytable_pool_destroy的时候,delete。

@MJY-HUST
Copy link
Contributor Author

如果使用TLS的话,当server结束调用Join时,通过bthread_keytable_pool_destroy方法需要删除所有线程内保存的TLS free_keytables才行,感觉也不是很方便?

用butil::ThreadLocal应该挺方便的吧。 bthread_keytable_pool_init的时候,new一个butil::ThreadLocal。 bthread_keytable_pool_destroy的时候,delete。

使用butil::ThreadLocal的话,new的object只有当线程结束才会释放,而目前基于bthread_keytable_pool_t的实现是在server Join后即清理释放所有的table,从KeyInfo中移除注册的key信息。如果使用butil::ThreadLocal,可以在delete后,析构所有的keytable吗?

@MJY-HUST
Copy link
Contributor Author

改造bthread_keytable_pool_t ,其中引入一个类似Object_pool的成员,通过在成员内部使用tls变量来避免加互斥锁。然后对bthread_keytable_pool_t 中的锁的使用从互斥锁改为读写锁,来维护destroyed的值。在return_keytable内使用读锁,bthread_keytable_pool_init,bthread_keytable_pool_destroy时加写锁。这样就可以避免在sever未退出时多个线程return_keytable产生的锁的竞争。 不知道这样是否可行

之前考虑过在bthread_keytable_pool_t中使用ObjectPool,但是ObjectPool好像不能实现reserved_thread_local_data吧?

destroyed应该可以用原子变量,就不用锁了吧。

  1. 应该也不能直接使用Object_pool,因为它是一个单例,不能做到sever间的隔离。
  2. 我看ResourcePool获取对象的模式是如下图,那么reserve时初始化多个全局的block是否可行。
    image
  3. return_keytable时,加读锁的临界区是判断destroyed + 将对象返回给pool,感觉不能直接使用原子变量

@MJY-HUST
Copy link
Contributor Author

我的理解,目前的实现是一个server会绑定一个全局的bthread_keytable_pool_t,在server的声明周期内,server和其生成的bthread使用同一个pool,在server结束时会销毁所有创建的KeyTable,通过bthread_keytable_pool_t内的链表;然后会从全局的KeyInfo中删除所有注册的Key。但是使用全局一个链表会存在锁的竞争问题,不知道理解的对不对。

嗯,我也是这样理解的。不过一般接口实现的瓶颈都是在业务逻辑,所以这个锁的影响应该不大。

我们这边的场景是使用brpc server下面接了个rocksdb,当数据全被cache住时,service端的处理时间可能很短,导致瓶颈体现在了锁的竞争上
image

@chenBright
Copy link
Contributor

如果使用TLS的话,当server结束调用Join时,通过bthread_keytable_pool_destroy方法需要删除所有线程内保存的TLS free_keytables才行,感觉也不是很方便?

用butil::ThreadLocal应该挺方便的吧。 bthread_keytable_pool_init的时候,new一个butil::ThreadLocal。 bthread_keytable_pool_destroy的时候,delete。

使用butil::ThreadLocal的话,new的object只有当线程结束才会释放,而目前基于bthread_keytable_pool_t的实现是在server Join后即清理释放所有的table,从KeyInfo中移除注册的key信息。如果使用butil::ThreadLocal,可以在delete后,析构所有的keytable吗?

butil::ThreadLocal析构时,会delete使用过butil::ThreadLocal的每个线程上的local data。应该可以满足需求吧。

@chenBright
Copy link
Contributor

我看ResourcePool获取对象的模式是如下图,那么reserve时初始化多个全局的block是否可行。

ResourcePool也是单例哦。

return_keytable时,加读锁的临界区是判断destroyed + 将对象返回给pool,感觉不能直接使用原子变量

嗯嗯,return_keytable和bthread_keytable_pool_destroy需要互斥,不能用原子变量。

无论是ObjectPool还是ResourcePool的实现方式,即使destroyed=1,也是要将KeyTable返回给ObjectPool或者ResourcePool的吧。

@MJY-HUST
Copy link
Contributor Author

如果使用TLS的话,当server结束调用Join时,通过bthread_keytable_pool_destroy方法需要删除所有线程内保存的TLS free_keytables才行,感觉也不是很方便?

用butil::ThreadLocal应该挺方便的吧。 bthread_keytable_pool_init的时候,new一个butil::ThreadLocal。 bthread_keytable_pool_destroy的时候,delete。

使用butil::ThreadLocal的话,new的object只有当线程结束才会释放,而目前基于bthread_keytable_pool_t的实现是在server Join后即清理释放所有的table,从KeyInfo中移除注册的key信息。如果使用butil::ThreadLocal,可以在delete后,析构所有的keytable吗?

butil::ThreadLocal析构时,会delete使用过butil::ThreadLocal的每个线程上的local data。应该可以满足需求吧。

请问一下这块逻辑具体在哪里呢?我没有看到butil::ThreadLocal这个类的定义。是ThreadLocalPointer吗?
当在一个线程上调用delete时,它是如何做到会delete使用过butil::ThreadLocal的每个线程上的local data,我没有看到类似链表或者list来存放所有线程的tls指针

@chenBright
Copy link
Contributor

brpc/src/butil/thread_key.h

Lines 144 to 154 in b601c89

template <typename T>
ThreadLocal<T>::~ThreadLocal() {
thread_key_delete(_key);
if (!_delete_on_thread_exit) {
BAIDU_SCOPED_LOCK(_mutex);
for (auto ptr : ptrs) {
DefaultDtor(ptr);
}
}
pthread_mutex_destroy(&_mutex);
}

@MJY-HUST MJY-HUST force-pushed the keytable_optimize branch from d147ced to b339a7b Compare May 22, 2024 18:02
@MJY-HUST
Copy link
Contributor Author

@chenBright hello,我根据讨论修改了实现,可以帮忙看一下么

@MJY-HUST MJY-HUST changed the title Use object_pool to store keytable by gflag Use butil::ThreadLocal to store keytable May 23, 2024
Copy link
Contributor

@chenBright chenBright left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@chenBright
Copy link
Contributor

@wwbmmm 也看看这个PR

g->current_task()->local_storage.keytable = old_kt;
}
}
KeyTable* keytable;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class 类型不建议直接把成员暴露成public,要么改成struct,要么封装成getter/setter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

改成了struct

delete kt;
return;
}
kt->next = (KeyTable*)pool->free_keytables;
pool->free_keytables = kt;
auto list = (butil::ThreadLocal<bthread::KeyTableList>*)pool->list;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

全局free_keytables里的kt一旦被借走就永远还不回去free_keytables了吗?这样keytables的复用率会降低吧,如果线程数很多,可能会占用更多的内存

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是不会返回给free_keytables了。free_keytables的大小是根据server.option的reserved_thread_local_data变量设置的,默认为0。
不过这里的线程指的应该只是一个bthread_keytable_pool_t pool所对应的server的brpc_worker threads,而不是普通的pthread。
所以相比使用单链表管理,如果brpc_worker thread是公平调度的话,内部的butil::ThreadLocal维护的链表长度应该是比使用一个单链表来说要短,但是性能会更好,因为同样存在回收机制,内存不会多太多。

@wwbmmm
Copy link
Contributor

wwbmmm commented Jun 10, 2024

对这个场景有点疑问,如果bthread生命周期很短,为什么还要使用bthread_local变量呢,用pthread local、局部变量或者通过函数参数传递不行吗

@MJY-HUST
Copy link
Contributor Author

MJY-HUST commented Jun 11, 2024

对这个场景有点疑问,如果bthread生命周期很短,为什么还要使用bthread_local变量呢,用pthread local、局部变量或者通过函数参数传递不行吗

场景是:使用brpc框架通信,server下面接着一个数据库。使用bthread pool用于处理数据库的查询请求,数据库内部的实现使用了bthread级别的锁和tls来避免阻塞线程。数据库内部需要使用一些thread_local变量(bthread_local)来保证其一致性并减少重复创建。
并不是所有的条件下bthread的生命周期都会很短,但是在一些场景下,如果数据都在缓存中,那么一个bthread的生命周期就会很短,此时单链表结构的keytable pool管理模式就会出现瓶颈。

@wwbmmm
Copy link
Contributor

wwbmmm commented Jun 13, 2024

LGTM

@Huixxi
Copy link
Contributor

Huixxi commented Jul 7, 2024

请问下按照这样做了设计之后,有具体的性能测试报告么?能够直观的显示这种改造在特定场景下带来的性能提升

@MJY-HUST
Copy link
Contributor Author

MJY-HUST commented Jul 7, 2024

请问下按照这样做了设计之后,有具体的性能测试报告么?能够直观的显示这种改造在特定场景下带来的性能提升

修改前:
image
image
image

修改后:
image
image
image

@MJY-HUST
Copy link
Contributor Author

MJY-HUST commented Jul 7, 2024

请问下按照这样做了设计之后,有具体的性能测试报告么?能够直观的显示这种改造在特定场景下带来的性能提升

相同的负载下,修改前单链表维护的keytable,锁成为瓶颈。cpu被打满,sy时间占比高;
修改后,解除了锁的瓶颈,cpu利用率降低,QPS提升了一倍。火焰图上和加锁/解锁操作对应的futex_wait\futex_wake耗时部分消失

@Huixxi Huixxi merged commit 5f991fe into apache:master Jul 7, 2024
20 checks passed
@MJY-HUST MJY-HUST deleted the keytable_optimize branch August 8, 2024 16:36
@lsdh-fei
Copy link

lsdh-fei commented Sep 4, 2024

这里有个疑问,为什么不直接把bthread_keytable_pool_t改成thread-local的呢?@MJY-HUST

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

Successfully merging this pull request may close these issues.

5 participants