diff --git a/MODULE.bazel b/MODULE.bazel index 8a59e9bb..71b81621 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -30,9 +30,14 @@ bazel_dep(name = 'googletest', version = '1.14.0', repo_name = 'com_google_googl bazel_dep(name = 'platforms', version = '0.0.10', dev_dependency = True) bazel_dep(name = 'rules_cc', version = '0.0.9', dev_dependency = True) -# --registry=file://%workspace%/registry # rules_cuda latest release 0.2.1 is too old and do not have auto detect feature -bazel_dep(name = 'rules_cuda', version = '0.2.2-dev', dev_dependency = True) +bazel_dep(name = 'rules_cuda', version = '0.2.1', dev_dependency = True) +archive_override( + module_name = 'rules_cuda', + urls = ['https://github.com/bazel-contrib/rules_cuda/archive/3482c70dc60d9ab1ad26b768c117fcd61ee12494.tar.gz'], + strip_prefix = 'rules_cuda-3482c70dc60d9ab1ad26b768c117fcd61ee12494', + integrity = 'sha256-x78dpBtaMUgKBHf0ztSe7QirHLOv93xwTjc8+cUmlPU=', +) cuda = use_extension('@rules_cuda//cuda:extensions.bzl', 'toolchain', dev_dependency = True) cuda.local_toolchain() diff --git a/README.md b/README.md index ab60f133..b0eee444 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Babylon也支持使用[CMake](https://cmake.org)进行构建,并支持通过[f - [:concurrent](docs/concurrent/index.md) - [:executor](docs/executor.md) - [:future](docs/future.md) -- [:logging](docs/logging.md) +- [:logging](docs/logging/index.md) - [:reusable](docs/reusable/index.md) - [:serialization](docs/serialization.md) - [:time](docs/time.md) diff --git a/docs/images/bfile.png b/docs/images/bfile.png deleted file mode 100644 index 6c0c9c8f..00000000 Binary files a/docs/images/bfile.png and /dev/null differ diff --git a/docs/logging.md b/docs/logging.md deleted file mode 100644 index 52f46ade..00000000 --- a/docs/logging.md +++ /dev/null @@ -1,81 +0,0 @@ -# logging - -## 原理 - -目前后端服务在glog的带动下,类streaming的<<接口模式被广泛采用,在此基础上结合sink扩展成为异步方案基本成为标准。百度后端之前内部使用的comlog系统,车载的apollo都使用了类似的模式。在常规场景下一般功能已经足够,不过如果追求最佳性能的话,这个常用组合还是存在一些问题 - -![](images/afile.png) - -1. 从用户<<接入开始,到writev系统调用间,一共存在3次日志内容的拷贝 - -- 原始信息>>streambuf -- streambuf->render_msgbuf -- render_msgbuf->current_buffer - -2. 对于全局结构采用锁保护,例如current_buffer为锁后拷贝,在并发增大情况下竞争问题会逐步显现 - -针对这两个问题,采用ConcurrentBoundedQueue实现babylon::AsyncFileAppender,消除了锁同步的动作,同时通过babylon::PageAllocator将中转使用的3次内存拷贝降低为1次 - -![](images/bfile.png) - -## 使用方法 - -### 用法示例 - -```c++ -#include "babylon/logging/logger.h" - -using ::babylon::LoggerManager; -using ::babylon::LogSeverity; - -// 取得默认logger -auto& root_logger = LoggerManager::instance().get_root_logger(); - -// 根据name取得某个层级的logger -// name根据层级查找配置,例如对于name = "a.b.c" -// 会依次尝试"a.b.c" -> "a.b" -> "a" -> root -auto& logger = LoggerManager::instance().get_logger("..."); - -// 开启一次log事务 -BLOG(INFO) << ... // 使用root logger -BLOG_STREAM(logger, INFO) << ... // 使用指定的logger - -// 一般在语句结束后,就会视为结束进行日志提交 -// 支持使用noflush来进行渐进组装 -BLOG(INFO) << "some " << ::babylon::noflush; -BLOG(INFO) << "thing"; // 输出[header] some thing - -// 支持类printf格式化功能,底层由abseil-cpp库提供支持 -BLOG(INFO).format("hello %s", world).format(" +%d", 10086); - -// 更说明见注释 -// 单测test/test_async_file_appender.cpp -``` - -## 性能对比 - -| payload=300B thread=1 | qps | cpu | latency | qps | cpu | latency | qps | cpu | latency | qps /dev/null | cpu | latency | -|-----------------------|-----|-------|---------|-----|-------|---------|-------|------|---------|---------------|------|---------| -| log4cxx | 1W | 0.08 | 2908 | 10W | 0.772 | 2858 | 20.5W | 1.56 | 3307 | 32.6W | 1.97 | 2945 | -| boost | 1W | 0.049 | 2232 | 10W | 0.438 | 2134 | 30.3W | 1.23 | 1975 | 37.7W | 1.92 | 2690 | -| spdlog | 1W | 0.021 | 674 | 10W | 0.173 | 553 | 65.0W | 1.25 | 824 | 89W | 1.67 | 883 | -| glog + comlog | 1W | 0.023 | 1479 | 10W | 0.170 | 1363 | 71.8W | 1.28 | 1390 | 70.8W | 1.00 | 1387 | -| BLOG | 1W | 0.018 | 1095 | 10W | 0.155 | 1064 | 100W | 1.51 | 995 | 102W | 1.02 | 970 | - -| payload=300B thread=12 | qps | cpu | latency | qps | cpu | latency | qps | cpu | latency | qps /dev/null | cpu | latency ns | -|------------------------|-----|-------|---------|-----|------|---------|-------|------|---------|---------------|------|------------| -| log4cxx | 1W | 0.105 | 4725 | 10W | 1.24 | 7427 | 18.2W | 3.08 | 20020 | 30.6W | 4.41 | 14086 | -| boost | 1W | 0.074 | 3441 | 10W | 0.53 | 2975 | 23.8W | 1.36 | 3442 | 39.1W | 2.43 | 4231 | -| spdlog | 1W | 0.043 | 2234 | 10W | 0.26 | 1782 | 35.0W | 3.24 | 7958 | 70.0W | 1.79 | 1538 | -| glog + comlog | 1W | 0.035 | 2227 | 10W | 0.22 | 1718 | 125W | 12.2 | 9426 | 139W | 11.7 | 8655 | -| BLOG | 1W | 0.025 | 1605 | 10W | 0.19 | 1338 | 192W | 4.27 | 6347 | 942W | 12.5 | 1279 | - -开源实现:异步日志性能spdlog >> boost > log4cxx - -- 共性问题:未进行批量写优化,导致cpu开销一般较大,相应的极限吞吐线也较低;异步队列简单锁+普通队列实现,大并发下竞争问题突出; -- spdlog:在github广受好评,实测效果确实不错,轻量实现额外开销很低,单线程模式延迟低于AFILE;但是由于采用简单锁同步,并发增高后延迟会显著退化,评测按照未引起退化的最大吞吐计算; -- boost:使用繁琐,性能表现一般,竞争增大后性能退化尤其严重; -- log4cxx:当前社区比较活跃,但目前实现还比较简陋,额外开销和竞争适应性显著是较差的; -内部实现: -- comlog:特有的全局buffer + 队列实现,一定程度上降低了临界区内直接操作队列的缓存竞争效应,以及动态内存申请,对比开源的锁 + 队列实现有显著竞争适应性提升; -- BLOG:利用无锁队列的性能优势,去掉了对全局buffer的依赖,一方面减少了拷贝减少了日志的额外开销;另一方面无锁队列进一步提升了竞争适应性; diff --git a/docs/logging/async_file_appender.md b/docs/logging/async_file_appender.md new file mode 100644 index 00000000..83bc2d68 --- /dev/null +++ b/docs/logging/async_file_appender.md @@ -0,0 +1,126 @@ +# async_file_appender + +## LogEntry&LogStreamBuffer + +LogStreamBuffer是一个std::stringbuf的实现,实际内存管理在分页定长内存上,LogEntry就是这个分页定长内存本身的维护结构。 + +### 用法示例 + +```c++ +#include "babylon/logging/log_entry.h" + +using babylon::LogStreamBuffer; +using babylon::LogEntry; + +// 使用LogStreamBuffer需要先设置一个PageAllocator +PageAllocator& page_allocator = ... +LogStreamBuffer buffer; +buffer.set_page_allocator(page_allocator); + +// 后续buffer可以反复使用 +loop: + buffer.begin(); // 每次使用需要用begin触发准备动作 + buffer.sputn(...); // 之后可以进行写入动作,一般不直接调用,而是成为LogStream的底层 + LogEntry& entry = buffer.end(); // 一轮写入完成,返回最终的组装结果 + ... // LogEntry本身只有一个cache line大小,可以轻量拷贝转移 + +consumer: + ::std::vector iov; + // 一般LogEntry经过异步队列转移到消费者执行 + LogEntry& entry = ... + // 追加倒出成iovec结构,主要方便对接writev + entry.append_to_iovec(page_allocator.page_size(), iov); +``` + +## FileObject + +FileObject是对于日志写入对向的抽象,功能为对外提供可用的fd,对于需要滚动的场景,内部完成滚动和老文件管理 + +```c++ +#include "babylon/logging/file_object.h" + +using babylon::FileObject; + +class CustomFileObject : public FileObject { + // 核心功能函数,上层在每次写出前需要调用此函数来获得文件操作符 + // 函数内部完成文件滚动检测等操作,返回最终准备好的描述符 + // 由于可能发生文件的滚动,返回值为新旧描述符(fd, old_fd)二元组 + // fd: + // >=0: 当前文件描述符,调用者后续写入通过此描述符发起 + // < 0: 发生异常无法打开文件 + // old_fd: + // >=0: 发生文件切换,返回之前的文件描述符 + // 一般由文件滚动引起,需要调用者执行关闭动作 + // 关闭前调用者可以进行最后的收尾写入等操作 + // < 0: 未发生文件切换 + virtual ::std::tuple check_and_get_file_descriptor() noexcept override { + ... + } +}; +``` + +## RollingFileObject + +实现滚动文件的FileObject,支持按照时间间隔滚动切换,并提供定量保留清理能力 + +```c++ +#include "babylon/logging/rolling_file_object.h" + +using babylon::RollingFileObject; + +RollingFileObject object; +object.set_directory("dir"); // 日志所在目录 +object.set_file_pattern("name.%Y-%m-%d"); // 日志文件名模板,支持strftime语法 + // 当时间驱动文件名发生变化时,执行文件滚动 +object.set_max_file_number(7); // 最多保留个数 + +// 实际会写入类似这样名称的文件当中 +// dir/name.2024-07-18 +// dir/name.2024-07-19 + +// 启动期间调用此接口可以扫描目录并记录其中符合pattern的已有文件 +// 并加入到跟踪列表,来支持重启场景下继续跟进正确的文件定量保留 +object.scan_and_tracking_existing_files(); + +loop: + // 检查目前跟踪列表中是否超出了保留数目,超出则进行清理 + object.delete_expire_files(); + // 一些场景进程会同时输出很多路日志文件 + // 主动调用便于在一个后台线程实现所有日志的过期删除 + ... + sleep(1); +``` + +## AsyncFileAppender&AsyncLogStream + +AsyncFileAppender实现了LogEntry的队列传输,以及最终异步向FileObject执行写入动作 +AsyncLogStream包装了AsyncFileAppender、FileObject和LogStreamBuffer,对接到Logger机制 + +```c++ +#include "babylon/logging/async_log_stream.h" + +using babylon::AsyncFileAppender; +using babylon::AsyncLogStream; +using babylon::FileObject; +using babylon::LoggerBuilder; +using babylon::PageAllocator; + +// 需要先准备一个PageAllocator和一个FileObject& +PageAllocator& page_allocator = ... +FileObject& file_object = ... + +AsyncFileAppender appender; +appender.set_page_allocator(page_allocator); +// 设置队列长度 +appender.set_queue_capacity(65536); +appender.initialize(); + +// 组合AsyncFileAppender和FileObject行程一个能够生成Logger的AsyncLogStream +LoggerBuilder builder; +builder.set_log_stream_creator(AsyncLogStream::creator(appender, object)); +LoggerManager::instance().set_root_builder(::std::move(builder)); +LoggerManager::instance().apply(); + +// 之后就会在日志宏背后开始生效 +BABYLON_LOG(INFO) << ... +``` diff --git a/docs/logging/images/logging-async.png b/docs/logging/images/logging-async.png new file mode 100644 index 00000000..59c9a25c Binary files /dev/null and b/docs/logging/images/logging-async.png differ diff --git a/docs/logging/images/logging-classic.png b/docs/logging/images/logging-classic.png new file mode 100644 index 00000000..3918a615 Binary files /dev/null and b/docs/logging/images/logging-classic.png differ diff --git a/docs/logging/images/logging-logger.png b/docs/logging/images/logging-logger.png new file mode 100644 index 00000000..649677fe Binary files /dev/null and b/docs/logging/images/logging-logger.png differ diff --git a/docs/logging/images/logging-nano.png b/docs/logging/images/logging-nano.png new file mode 100644 index 00000000..c8c4823b Binary files /dev/null and b/docs/logging/images/logging-nano.png differ diff --git a/docs/logging/index.md b/docs/logging/index.md new file mode 100644 index 00000000..39cae3d2 --- /dev/null +++ b/docs/logging/index.md @@ -0,0 +1,32 @@ +# logging + +## 背景和原理 + +由于写入page cache的动作涉及较多不确定的内核和设备因素,完成时间不可控,因此典型的服务端程序在执行日志记录时都会通过异步衔接解耦日志的组装生成和实际的写入动作。大多数独立的日志框架例如[spdlog](https://github.com/gabime/spdlog)、[boost.log](https://github.com/boostorg/log)一般都包含内置的异步方案。另一个使用广泛的日志框架[glog](https://github.com/google/glog)虽然自身不包含异步方案,不过保留了一定的扩展点,实际采用的应用框架例如[apollo](https://github.com/ApolloAuto/apollo/blob/master/cyber/logger/async_logger.h)、[brpc](https://github.com/apache/brpc/blob/master/src/butil/logging.cc)等一般都包含内置的异步化插件方案。 + +但是目前的流行实现一般容易存在几个典型的性能阻塞点 +- 用来解耦组装和写入的衔接机制往往采用了锁同步,对竞争激烈的场景会有明显的性能衰减 +- 对于承载日志的内存块,由于其变长的特性,往往设计中存在变长动态内存申请和释放,而且申请和释放往往存在跨线程转移穿透内存分配器的线程缓存 +- 有些实现对于localtime计算的[全局锁现象](../time.md)没有足够重视,同样会引起多线程竞争现象 + +![](images/logging-classic.png) + +值得一提的是一个独特的日志框架[NanoLog](https://github.com/PlatformLab/NanoLog),采用线程缓存汇聚的逻辑避免了上述内存问题,同时采用了独特的静态format spec单独记录的方式结合按需还原降低了需要写入文件的信息量。只是优化限制了使用场景在类printf场景,对于streaming类的序列化体系(operator<<)比较难兼容,使用范围有一定制约和限制。不过能够接受这样的制约和限制的场景下,这套框架选择的线程缓存采集汇聚逻辑很好的解决了典型的性能阻塞竞争点,提供了很优异的性能。 + +![](images/logging-nano.png) + +抛开动静分离的特殊优化手段,线程缓存汇聚作为一种优化手段,尽管有效解决的锁竞争的问题,但是在线程增多并结合生产环境偶发设备卡顿时间增长的情况下,需要设置显著增多的线程缓存空间来进行适应。所以这里提出了一个结合统一的无锁队列,以及定长无锁内存池的解决方案AsyncFileAppender。在前端部分,通过一个在定长分页内存上实现的streambuf,将结果承接到一个分页管理的LogEntry上。之后将LogEntry送入中央无锁队列进行异步解耦,并由统一的Appender后端消费完成写入动作,最终分页释放回定长内存池,进入后续的前端流转。 + +![](images/logging-async.png) + +进一步在AsyncFileAppender的之上,单独设计了Logger层的概念,主要出于两点考虑 +- 单独的Logger层采用了类似[log4j](https://github.com/apache/logging-log4j2)的树型层次化概念,在c++生态中类似的复杂管理能力相对较少,希望能够提供一个类似思想,但又更符合c++内存管理理念的对标产品 +- 将AsyncFileAppender整套机制和实际的日志接口解耦,提供尽可能干净的接口实现。便于实际在生产环境中,对接到业务服务实际使用日志框架。实际上即使在百度内部,AsyncFileAppender最广泛的使用方法依然是集成到内部更为广泛使用的已有日志接口框架下,作为底层异步能力来使用 +- babylon内部也会有记录日志的需求,一个轻量的Logger层可以让用户更方便对接到自己已有的日志系统中去,而不是必然绑定用户整体切换到AsyncFileAppender机制。对于向成熟系统的集成,这可能是一种更有好的提供方式,给用户更符合其意愿的选择权 + +![](images/logging-logger.png) + +## 功能文档 + +- [logger](logger.md) +- [async_file_appender](async_file_appender.md) diff --git a/docs/logging/logger.md b/docs/logging/logger.md new file mode 100644 index 00000000..e0fb8e38 --- /dev/null +++ b/docs/logging/logger.md @@ -0,0 +1,146 @@ +# logger + +## LogStream + +LogStream设计了一个实现streaming log宏的基准接口表达,并通过继承实现对用户提供了两个扩展点 +- 采用std::streambuf作为底层缓冲区提供者,便于对接到其他steaming log生态 +- 增加了begin/end插件点,对于非streaming log生态可以在thread local buffer格式化后完整导出对接,原生实现也可以利用begin/end插件点实现自定义layout + +### 用法示例 + +```c++ +#include "babylon/logging/log_stream.h" + +using babylon::LogStream; + +// 通过继承方式实现一个有实际意义的LogStream +class SomeLogStream : public LogStream { + // 基类构造必须传入一个可用的std::streambuf的子类 + // 所有写入动作最终都会生效到这个缓冲区中 + SomeLogStream() : LogStream(stringbuf) {} + + // 一次日志事务的开头结尾进行的额外动作 + virtual void do_begin() noexcept override { + *this << ... // 典型用法是实现日志头的前置输出 + } + virtual void do_end() noexcept override { + write('\n'); // 一般对于文本日志都需要追加结尾换行 + // 没有默认实现主要为了能够同样表达非文本日志 + ... // 一般最后都需要提交到日志系统的实际后端实现 + } + + ... + + // 这里以std::stringbuf为例,实际上可以是任意自定义的流缓冲区 + std::stringbuf stringbuf; +}; + +// 一般无需直接使用LogStream,而是通过Logger的包装宏使用 +// 包装宏会自动完成begin/end的调用管理 +LogStream& ls = ... +ls.begin(); +ls << ... +ls.end(); + +// 除了流操作符,也支持printf-like的格式化动作 +// 实际格式化功能由absl::Format提供 +ls.format("some spec %d", value); +``` + +## Logger&LoggerBuilder + +实际最终日志打印动作可见的对向是Logger,而非直接使用LogStream。Logger主要提供了两个能力 +- 日志流LogStream一般本身都是非线程安全的,Logger使用ThreadLocal进行了竞争保护 +- 提供了日志等级设置,以及为每个等级设置独立LogStream的能力,主要支持在noflush模式下可能同时需要两个不同等级独立写出流的场景(循环组装一行info的过程中,打出一条warning) + +Logger本身仅供使用,创建一个Logger通过LoggerBuilder完成 + +### 用法示例 + +```c++ +#include "babylon/logging/logger.h" + +using babylon::Logger; +using babylon::LoggerBuilder; +using babylon::LogSeverity; + +LoggerBuilder builder; +// 对所有日志等级设置统一的LogStream +// 由于最终会对每个线程创建一次 +// 所以传入的不是创建好的实例,而是创建实例的函数 +builder.set_log_stream_creator([] { + auto ls = ... + return std::unique_ptr(ls); +}); +// 也可以对某个级别设置单独的LogStream +builder.set_log_stream_creator(LogSeverity::INFO, [] { + auto ls = ... + return std::unique_ptr(ls); +}); +// 设置最低日志等级,低于最低等级无论设置与否都会按照空LogStream处理 +// 同时日志宏包装也会识别最低等级进行整段流操作符的省略 +// LogSeverity包含{DEBUG, INFO, WARNING, FATAL}四个等级 +builder.set_min_severity(LogSeverity min_severity); + +// 按照设置实际构造一个可以使用的Logger +Logger logger = builder.build(); + +// 使用logger,打印置顶级别的日志 +// ***部分可以追加其他业务通用头,也同样遵守noflush时只输出一次的规则 +// ...部分是正常的日志流输入 +BABYLON_LOG_STREAM(logger, INFO, ***) << ... +``` + +## LoggerManager + +LoggerManager维护了层级化的Logger树,初始化阶段用来配置,运行时用来获取获取各个层级节点的Logger,配置和运行阶段通过明确的初始化动作完成转换 +- 未配置的Logger树,获取到的Logger表现为默认行为,输出到标准错误 +- Logger初始化后,所有之前获取的表现为默认行为的Logger会切换到实际被初始化的真实Logger,过程中是线程安全的 +- 可以动态向Logger树增加节点Logger或者改变日志等级,过程中也是线程安全的 + +### 用法示例 + +```c++ +#include "babylon/logging/logger.h" + +using babylon::LoggerManager; + +// LoggerManager通过全局单例使用 +auto& manager = LoggerManager::instance(); + +// 根据name取得某个层级的logger +// name根据层级查找配置,例如对于name = "a.b.c" +// 会依次尝试"a.b.c" -> "a.b" -> "a" -> root +// 层次同时兼容"a::b::c" -> "a::b" -> "a" -> root +auto& logger = manager.get_logger("..."); +// 直接取得root logger +auto& root_logger = manager.get_root_logger(); + +// 在任何设置动作发生之前,取得的Logger都是默认状态,全部输出到标准错误 + +// 构造计划生效的builder +LoggerBuilder&& builder = ... +// 设置root logger +manager.set_root_builder(builder); +// 设置某个层级的logger +manager.set_builder("a.b.c", builder); +// 所有设置只有在apply之后才会统一生效 +manager.apply(); + +// 完成设置后,『之前』取得的Logger也会从默认状态,变更为正确的状态 +// 变更过程是线程安全的,正在使用中的Logger也会被正确处理 +// 『之后』取得的Logger直接就会是正确设置的状态 + +// 默认宏内部也会使用root logger +BABYLON_LOG(INFO) << ... +// 使用指定的logger +BABYLON_LOG_STREAM(logger, INFO) << ... + +// 一般在语句结束后,就会视为结束进行日志提交 +// 支持使用noflush来进行渐进组装 +BABYLON_LOG(INFO) << "some " << ::babylon::noflush; +BABYLON_LOG(INFO) << "thing"; // 输出[header] some thing + +// 支持类printf格式化功能,底层由abseil-cpp库提供支持 +BABYLON_LOG(INFO).format("hello %s", world).format(" +%d", 10086); +``` diff --git a/registry/modules/rules_cuda/0.2.2-dev/MODULE.bazel b/registry/modules/rules_cuda/0.2.2-dev/MODULE.bazel deleted file mode 100644 index 315989f7..00000000 --- a/registry/modules/rules_cuda/0.2.2-dev/MODULE.bazel +++ /dev/null @@ -1,17 +0,0 @@ -module( - name = "rules_cuda", - version = "0.2.2-dev", - compatibility_level = 1, -) - -bazel_dep(name = "bazel_skylib", version = "1.4.2") -bazel_dep(name = "platforms", version = "0.0.6") - -cuda = use_extension("@rules_cuda//cuda:extensions.bzl", "toolchain") -use_repo(cuda, "local_cuda") - -register_toolchains( - "@local_cuda//toolchain:nvcc-local-toolchain", - "@local_cuda//toolchain/clang:clang-local-toolchain", - "@local_cuda//toolchain/disabled:disabled-local-toolchain", -) diff --git a/registry/modules/rules_cuda/0.2.2-dev/source.json b/registry/modules/rules_cuda/0.2.2-dev/source.json deleted file mode 100644 index 334605a7..00000000 --- a/registry/modules/rules_cuda/0.2.2-dev/source.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "url": "https://github.com/bazel-contrib/rules_cuda/archive/3482c70dc60d9ab1ad26b768c117fcd61ee12494.tar.gz", - "strip_prefix": "rules_cuda-3482c70dc60d9ab1ad26b768c117fcd61ee12494", - "integrity": "sha256-x78dpBtaMUgKBHf0ztSe7QirHLOv93xwTjc8+cUmlPU=" -}