
上周,
我们讨论了我们的小型演示项目Shrimp ,该
项目清楚地显示了如何在相似的条件下使用C ++库
RESTinio和
SObjectizer 。 Shrimp是一个小型C ++ 17应用程序,它通过RESTinio接受HTTP图像缩放请求,并通过SObjectizer和ImageMagick ++以多线程模式处理这些请求。
事实证明,该项目对我们来说是无用的。 用于扩展RESTinio和SObjectizer功能的Wishlist储钱罐已得到大量补充。
RESTinio-0.4.7的最新版本甚至体现了这
一点 。 因此,我们决定不停留在Shrimp的第一个也是最琐碎的版本上,而是围绕这个项目进行一两次以上的迭代。 如果有人对这段时间和我们的工作方式感兴趣,欢迎您的光临。
作为一个破坏者:我们将讨论如何摆脱对相同请求的并行处理,如何使用出色的spdlog库向Shrimp添加日志记录,以及如何强制重置转换图像的缓存。
v0.3:控制相同请求的并行处理
上一篇文章中介绍的Shrimp的第一个版本包含一个严重的简化:无法控制当前是否正在处理同一请求。
想象一下,虾第一次收到格式为“ /demo.jpg?op=resize&max=1024”的请求。 转换后的图像缓存中尚无此类图像,因此正在处理请求。 处理可能要花费相当多的时间,例如几百毫秒。
请求处理尚未完成,Shrimp再次从另一个客户端接收到相同的请求“ /demo.jpg?op=resize&max=1024”。 缓存中尚无转换结果,因此此请求也将得到处理。
第一个请求和第二个请求都尚未完成,Shrimp可以再次收到相同的请求“ /demo.jpg?op=resize&max=1024”。 并且此请求也将被处理。 事实证明,同一张图片并行地缩放到相同的大小几次。
这不好。 因此,我们决定在虾店做的第一件事就是摆脱如此严重的障碍。 我们这样做是由于transform_manager代理中有两个棘手的容器。 第一个容器是等待空闲转换器请求的队列。 这是一个名为m_pending_requests的容器。 第二个容器存储已处理的请求(即已将特定的转换器分配给这些请求)。 这是一个名为m_inprogress_requests的容器。
当transform_manager收到下一个请求时,它将检查已转换图像的高速缓存中是否存在完成的图像。 如果没有转换后的图片,则检查m_inprogress_requests和m_pending_requests容器。 并且,如果在这些容器中的任何一个中都没有带有此类参数的请求,则仅尝试将请求放入m_pending_requests队列。 看起来
像这样 :
void a_transform_manager_t::handle_not_transformed_image( transform::resize_request_key_t request_key, sobj_shptr_t<resize_request_t> cmd ) { const auto store_to = [&](auto & queue) { queue.insert( std::move(request_key), std::move(cmd) ); }; if( m_inprogress_requests.has_key( request_key ) ) {
上面说过,m_inprogress_requests和m_pending_requests是棘手的容器。 但是诀窍是什么?
诀窍是这些容器结合了常规FIFO队列(保留添加元素的时间顺序)和多重映射的属性。 可以将多个值映射到单个键的关联容器。
保持时间顺序很重要,因为m_pending_requests中最旧的元素需要定期检查,并从m_pending_requests中删除那些超出最大超时的请求。 并且需要通过密钥对元素的有效访问,既要检查队列中是否存在相同的请求,又需要一次将所有重复的请求从队列中删除。
在虾,我们出于这些目的
对小型容器进行了循环。 虽然,如果在虾中使用Boost,则可以使用Boost.MultiIndex。 而且,可能随着时间的流逝,将需要通过其他一些条件来组织对m_pending_requests的有效搜索,然后必须激活Shrimp中的Boost.MultiIndex。
v0.4:使用spdlog记录
我们试图让Shrimp的第一个版本尽可能简单和紧凑。 因此,在第一个Shrimp版本中,我们没有使用日志记录。 一般而言。
一方面,这使第一个版本的代码保持简洁,除了必要的虾业务逻辑之外,什么都没有。 但是,另一方面,缺乏日志记录使虾的开发及其操作都变得复杂。 因此,一旦掌握了它,我们就立即将一个出色的现代C ++库(用于记录
spdlog)拖入Shrimp中 。 尽管某些方法的代码量越来越大,但是呼吸很快变得容易了。
例如,上面带有日志记录的handle_not_transformed_image()方法的代码开始看起来像
这样 :
void a_transform_manager_t::handle_not_transformed_image( transform::resize_request_key_t request_key, sobj_shptr_t<resize_request_t> cmd ) { const auto store_to = [&](auto & queue) { queue.insert( std::move(request_key), std::move(cmd) ); }; if( m_inprogress_requests.has_key( request_key ) ) {
配置spdlog记录器
虾的登录是在控制台上完成的(即在标准输出流中)。 原则上,可以沿着一条非常简单的路径在Shrimp中创建spd-logger的唯一实例。 即 可以调用
stdout_color_mt (或
stdout_logger_mt ),然后将此记录器传递给Shrimp中的所有实体。 但是我们采用了更为复杂的方式:我们手动创建了所谓的 接收器(即spdlog将在其中输出生成的消息的通道),并为虾实体创建了附加到此接收器的单独的记录器。
在spdlog中配置记录器有一个微妙的地方:默认情况下,记录器会忽略具有跟踪和调试严重性级别的消息。 即,它们在调试时被证明是最有用的。 因此,在make_logger中,默认情况下,我们启用所有级别的日志记录,包括跟踪/调试。
由于Shrimp中的每个实体都有其自己的记录器(具有自己的名称),因此我们可以在日志中看到谁在做什么:

使用spdlog跟踪SObjectizer
作为SObjectizer应用程序的主要业务逻辑的一部分执行的日志记录时间不足以调试应用程序。 目前尚不清楚为什么某个动作是在一个代理中启动的,而实际上并未在另一代理中执行。 在这种情况下,内置在SObjectizer中的msg_tracing机制会有所帮助(我们
在另一篇文章中已经讨论过)。 但是在SObjectizer的标准msg_tracing实现中,没有一个使用spdlog。 我们将自己为虾做此实现:
class spdlog_sobj_tracer_t : public so_5::msg_tracing::tracer_t { std::shared_ptr<spdlog::logger> m_logger; public: spdlog_sobj_tracer_t( std::shared_ptr<spdlog::logger> logger ) : m_logger{ std::move(logger) } {} virtual void trace( const std::string & what ) noexcept override { m_logger->trace( what ); } [[nodiscard]] static so_5::msg_tracing::tracer_unique_ptr_t make( spdlog::sink_ptr sink ) { return std::make_unique<spdlog_sobj_tracer_t>( make_logger( "sobjectizer", std::move(sink) ) ); } };
在这里,我们看到了特殊的SObjectizer接口tracer_t的实现,其中主要是虚拟trace()方法。 使用spdlog工具执行SObjectizer内部跟踪的人是他。
接下来,在启动SObjectizer时,将此实现安装为跟踪器:
so_5::wrapped_env_t sobj{ [&]( so_5::environment_t & env ) {...}, [&]( so_5::environment_params_t & params ) { if( sobj_tracing_t::on == sobj_tracing ) params.message_delivery_tracer( spdlog_sobj_tracer_t::make( logger_sink ) ); } };
RESTinio通过spdlog跟踪
除了跟踪SObjectizer内部发生的事情之外,有时跟踪RESTinio内部发生的事情可能非常有用。 在虾的更新版本中,还添加了这样的跟踪。
此跟踪是通过可以在RESTinio中执行日志记录的特殊类的定义实现的:
class http_server_logger_t { public: http_server_logger_t( std::shared_ptr<spdlog::logger> logger ) : m_logger{ std::move( logger ) } {} template< typename Builder > void trace( Builder && msg_builder ) { log_if_enabled( spdlog::level::trace, std::forward<Builder>(msg_builder) ); } template< typename Builder > void info( Builder && msg_builder ) { log_if_enabled( spdlog::level::info, std::forward<Builder>(msg_builder) ); } template< typename Builder > void warn( Builder && msg_builder ) { log_if_enabled( spdlog::level::warn, std::forward<Builder>(msg_builder) ); } template< typename Builder > void error( Builder && msg_builder ) { log_if_enabled( spdlog::level::err, std::forward<Builder>(msg_builder) ); } private: template< typename Builder > void log_if_enabled( spdlog::level::level_enum lv, Builder && msg_builder ) { if( m_logger->should_log(lv) ) { m_logger->log( lv, msg_builder() ); } } std::shared_ptr<spdlog::logger> m_logger; };
由于RESTinio中的日志记录机制基于通用编程,而不是基于传统的面向对象的方法,因此该类不会从任何继承。 这样,在根本不需要日志记录的情况下,您可以完全摆脱任何开销(当我们
谈论在RESTinio中使用模板时,我们将更详细地
介绍该主题)。
接下来,我们需要指出HTTP服务器将使用上面显示的http_server_logger_t类作为其记录器。 这是通过阐明HTTP服务器的属性来完成的:
struct http_server_traits_t : public restinio::default_traits_t { using logger_t = http_server_logger_t; using request_handler_t = http_req_router_t; };
好了,那么就没事了-创建spd-logger的特定实例,然后将此logger发送到创建的HTTP服务器:
auto restinio_logger = make_logger( "restinio", logger_sink, restinio_tracing_t::off == restinio_tracing ? spdlog::level::off : log_level ); restinio::run( asio_io_ctx, shrimp::make_http_server_settings( thread_count.m_io_threads, params, std::move(restinio_logger), manager_mbox_promise.get_future().get() ) );
v0.5:强制重置转换后的图像缓存
在调试Shrimp的过程中,发现了一件令人讨厌的小事情:为了刷新转换后的图像缓存的内容,您必须重新启动整个Shrimp。 看起来有点琐事,但不愉快。
如果不愉快,那么您应该摆脱这种缺点。 幸运的是,这一点都不困难。
首先,我们将在Shrimp中定义另一个URL,您可以向其发送HTTP DELETE请求:“ / cache”。 因此,我们将处理程序挂在以下URL上:
std::unique_ptr< http_req_router_t > make_router( const app_params_t & params, so_5::mbox_t req_handler_mbox ) { auto router = std::make_unique< http_req_router_t >(); add_transform_op_handler( params, *router, req_handler_mbox ); add_delete_cache_handler( *router, req_handler_mbox ); return router; }
其中add_delete_cache_handler()函数如下所示:
void add_delete_cache_handler( http_req_router_t & router, so_5::mbox_t req_handler_mbox ) { router.http_delete( "/cache", [req_handler_mbox]( auto req, auto ) { const auto qp = restinio::parse_query( req->header().query() ); auto token = qp.get_param( "token"sv ); if( !token ) { return do_403_response( req, "No token provided\r\n" ); }
有点冗长,但没有什么复杂的。 查询的查询字符串必须具有令牌参数。 此参数必须包含一个具有特殊值的字符串,用于管理令牌。 仅当token参数中的token值与启动Shrimp时设置的值匹配时,才能重置缓存。 如果没有令牌参数,则不接受该处理请求。 如果存在令牌,那么将向拥有高速缓存的transform_manager代理发送一条特殊的命令消息,执行该命令后,transform_manager代理本身将响应HTTP请求。
其次,我们在transform_manager_t代理中实现新的消息处理程序delete_cache_request_t:
void a_transform_manager_t::on_delete_cache_request( mutable_mhood_t<delete_cache_request_t> cmd ) { m_logger->warn( "delete cache request received; " "connection_id={}, token={}", cmd->m_http_req->connection_id(), cmd->m_token ); const auto delay_response = [&]( std::string response_text ) { so_5::send_delayed< so_5::mutable_msg<negative_delete_cache_response_t> >( *this, std::chrono::seconds{7}, std::move(cmd->m_http_req), std::move(response_text) ); }; if( const char * env_token = std::getenv( "SHRIMP_ADMIN_TOKEN" );
这里有两点需要澄清。
on_delete_cache_request()实现的第一步是对令牌值本身的验证。 管理令牌是通过SHRIMP_ADMIN_TOKEN环境变量设置的。 如果设置了此变量,并且其值与HTTP DELETE请求的令牌参数中的值匹配,那么将清除缓存并立即生成对该请求的肯定响应。
on_delete_cache_request()实现的第二点是对HTTP DELETE的否定响应的强制延迟。 如果出现了错误的管理令牌值,则应延迟对HTTP DELETE的响应,这样就不必再通过蛮力选择令牌的值了。 但是如何使这种延迟呢? 毕竟,调用std :: thread :: sleep_for()是不可行的。
这是SObjectizer的挂起消息用于救援的地方。 无需立即在on_delete_cache_request()内部生成否定响应,transform_manager代理仅向自身发送未决的negative_delete_cache_response_t消息。 SObjectizer计时器将计算设置的时间,并在经过指定的延迟后将此消息传递给代理。 现在,在negative_delete_cache_response_t处理程序中,您已经可以立即生成对HTTP DELETE请求的响应:
void a_transform_manager_t::on_negative_delete_cache_response( mutable_mhood_t<negative_delete_cache_response_t> cmd ) { m_logger->debug( "send negative response to delete cache request; " "connection_id={}", cmd->m_http_req->connection_id() ); do_403_response( std::move(cmd->m_http_req), std::move(cmd->m_response_text) ); }
即 原来是以下情形:
- HTTP服务器接收HTTP DELETE请求,将该请求转换为发给transform_manager代理的delete_cache_request_t消息;
- transform_manager代理接收delete_cache_request_t消息,并立即生成对该请求的肯定响应,或者向自身发送未决的negative_delete_cache_response_t消息;
- transform_manager收到negative_delete_cache_response_t消息,并立即生成对相应HTTP DELETE请求的否定响应。
第二部分结束
在第二部分的结尾,很自然地问一个问题:“下一步呢?”
此外,我们的演示项目可能还会有另一个迭代和另一个更新。 我想将图像从一种格式转换为另一种格式。 假设在服务器上,图像以jpg格式保存,转换后将其发送到webp中的客户端。
附加一个单独的“页面”并显示虾工作的当前统计信息也很有趣。 首先,这只是好奇。 但是,原则上,这样的页面也可以适合于监视虾的生存能力的需求。
如果有人对我希望在Shrimp或Shrimp周围的文章中看到的东西有建议,那么我们将很高兴听到任何建设性的想法。
另外,我想指出Shrimp实施的一个方面,这使我们有些惊讶。 当彼此和HTTP服务器进行通信时,这是对可变消息的一种主动使用。 通常,在我们的实践中,情况恰恰相反-经常通过免疫消息交换数据。 这里不是。 这表明我们在适当的时候有意听取了用户的意愿,并向SObjectizer添加了易变的消息。 因此,如果您想在RESTinio或SObjectizer中看到某些内容,请随时分享您的想法。 我们一定会听取好声音。
好了,总而言之,我要感谢所有抽出宝贵时间在Habré上和通过其他资源发表有关虾的第一版的人。 谢谢你
待续...