Yandex.Taxi坚持微服务架构。 随着微服务数量的增加,我们注意到开发人员将大量时间花在样板和典型问题上,而解决方案并不总是能达到最佳效果。
我们决定使用C ++ 17和协程创建自己的框架。 这是典型的微服务代码现在的外观:
Response View::Handle(Request&& request, const Dependencies& dependencies) { auto cluster = dependencies.pg->GetCluster(); auto trx = cluster->Begin(storages::postgres::ClusterHostType::kMaster); const char* statement = "SELECT ok, baz FROM some WHERE id = $1 LIMIT 1"; auto row = psql::Execute(trx, statement, request.id)[0]; if (!row["ok"].As<bool>()) { LOG_DEBUG() << request.id << " is not OK of " << GetSomeInfoFromDb(); return Response400(); } psql::Execute(trx, queries::kUpdateRules, request.foo, request.bar); trx.Commit(); return Response200{row["baz"].As<std::string>()}; }
这就是为什么它非常有效和快速的原因-我们将在削减中告诉您。
Userver-异步
我们的团队不仅由经验丰富的C ++开发人员组成:还有受训人员,初级开发人员,甚至是不习惯使用C ++编写的人员。 因此,用户设计基于易用性。 但是,凭借我们的数据量和负载,我们也无法承受无效率地浪费铁资源的负担。
微服务的特征在于对输入/输出的期望:微服务的响应通常由其他微服务和数据库的几种响应形成。 有效的I / O等待任务是通过异步方法和回调解决的:通过异步操作,不需要产生执行线程,因此,切换流程没有大的开销……仅代码很难编写和维护:
void View::Handle(Request&& request, const Dependencies& dependencies, Response response) { auto cluster = dependencies.pg->GetCluster(); cluster->Begin(storages::postgres::ClusterHostType::kMaster, [request = std::move(request), response](auto& trx) { const char* statement = "SELECT ok, baz FROM some WHERE id = $1 LIMIT 1"; psql::Execute(trx, statement, request.id, [request = std::move(request), response, trx = std::move(trx)](auto& res) { auto row = res[0]; if (!row["ok"].As<bool>()) { if (LogDebug()) { GetSomeInfoFromDb([id = request.id](auto info) { LOG_DEBUG() << id << " is not OK of " << info; }); } *response = Response400{}; } psql::Execute(trx, queries::kUpdateRules, request.foo, request.bar, [row = std::move(row), trx = std::move(trx), response]() { trx.Commit([row = std::move(row), response]() { *response = Response200{row["baz"].As<std::string>()}; }); }); }); }); }
在这里,stackfull-协程进行了救援。 框架的用户认为他编写了通常的同步代码:
auto row = psql::Execute(trx, queries::kGetRules, request.id)[0];
但是,大约在引擎盖下发生以下情况:
- 生成TCP数据包,并将其与请求一起发送到数据库;
- 当前正在运行View :: Handle函数的协程的执行被挂起;
- 我们对操作系统的内核说:“一旦数据库中有足够的TCP数据包,就将挂起的协程放入待执行的任务队列中”;
- 无需等待上一步,我们就可以启动另一个协程并准备从队列中执行。
换句话说,第一个示例中的函数是异步工作的,并且使用C ++ 20 Coroutines接近于这样的代码:
Response View::Handle(Request&& request, const Dependencies& dependencies) { auto cluster = dependencies.pg->GetCluster(); auto trx = co_await cluster->Begin(storages::postgres::ClusterHostType::kMaster); const char* statement = "SELECT ok, baz FROM some WHERE id = $1 LIMIT 1"; auto row = co_await psql::Execute(trx, statement, request.id)[0]; if (!row["ok"].As<bool>()) { LOG_DEBUG() << request.id << " is not OK of " << co_await GetSomeInfoFromDb(); co_return Response400{"NOT_OK", "Please provide different ID"}; } co_await psql::Execute(trx, queries::kUpdateRules, request.foo, request.bar); co_await trx.Commit(); co_return Response200{row["baz"].As<std::string>()}; }
只是用户不需要考虑co_await和co_return,一切都“独立”进行。
在我们的框架中,协程之间的切换比调用std :: this_thread :: yield()更快。 整个微服务花费很少的线程。
目前,userver包含异步驱动程序:
*用于OS插槽;
* http和https(客户端和服务器);
* PostgreSQL;
* MongoDB;
* Redis;
*处理文件;
*计时器;
*原语以同步并启动新的协程。
上面的异步方法解决了I / O绑定任务,这对Go开发人员应该很熟悉。 但是,与Go不同,我们不会从垃圾收集器中获得内存和CPU的开销。 开发人员可以使用具有各种容器和高性能库的丰富语言,而不会遭受缺乏一致性,RAII或模板的困扰。
Userver-组件
当然,一个完整的框架不仅是协程。 出租车中的开发人员的任务极为多样化,每个开发人员都需要一套自己的工具来解决。 因此,userver具有您需要的一切:
*用于记录;
*缓存;
*处理各种数据格式;
*使用配置并更新配置,而无需重新启动服务;
*分布式锁;
*测试;
*授权和认证;
*创建和发送指标;
*编写REST处理程序;
+代码生成和依赖关系支持(在框架的单独部分中完成)。
Userver-代码生成
让我们回到示例的第一行,看看Response和Request后面隐藏了什么:
Response Handle(Request&& request, const Dependencies& dependencies);
使用userver可以编写任何微服务,但是对于我们的微服务,要求必须记录其API(通过大摇大摆的方案进行描述)。
例如,对于示例中的Handle,摇摇图可能如下所示:
paths: /some/sample/{bar}: post: description: | Habr. summary: | , - . parameters: - in: query name: id type: string required: true - in: header name: foo type: string enum: - foo1 - foo2 required: true - in: path name: bar type: string required: true responses: '200': description: OK schema: type: object additionalProperties: false required: - baz properties: baz: type: string '400': $ref: '#/responses/ResponseCommonError'
好吧,既然开发人员已经有了一个包含请求和响应描述的方案,那么为什么不基于它生成这些请求和响应呢? 同时,还可以在方案中指示到protobuf / flatbuffer / ...文件的链接-从请求本身生成的代码将获得所有内容,根据方案验证输入数据,并将其分解为Response结构的字段。 用户只需要在Handle方法中编写功能,而不会因请求解析和响应序列化而分散注意力。
同时,代码生成适用于服务客户。 您可以指示您的服务需要客户端按照这种方案工作,并准备好一个类以用于创建异步请求:
Request req; req.id = id; req.foo = foo; req.bar = bar; dependencies.sample_client.SomeSampleBarPost(req);
这种方法还有另一个优点:始终是最新文档。 如果开发人员突然尝试使用文档中未包含的参数,则将收到编译错误。
Userver-记录
我们喜欢写日志。 如果仅记录最重要的信息,则每小时将运行几个TB的日志。 因此,我们的日志记录有自己的技巧并不奇怪:
*它是异步的(当然是:-));
*我们可以绕过慢的std :: locale和std :: ostream来记录日志;
*我们可以即时切换日志记录级别(无需重新启动服务);
*如果仅用于日志记录,则我们不执行用户代码。
例如,在微服务正常运行期间,日志记录级别将设置为INFO,整个表达式
LOG_DEBUG() << request.id << " is not OK of " << GetSomeInfoFromDb();
将不会被计算。 不会包含对资源密集型函数GetSomeInfoFromDb()的调用。
如果服务突然变得“傻瓜”,则开发人员总是可以告诉工作服务:“以DEBUG模式登录”。 在这种情况下,“不正确的”条目将开始出现在日志中,将执行GetSomeInfoFromDb()函数。
代替总数
在一篇文章中,不可能一次讲述所有功能和技巧。 因此,我们从简短的介绍开始。 在评论中写下有关您有兴趣学习和阅读的用户内容的信息。
现在,我们正在考虑是否将框架发布在开源中。 如果我们决定是的话,那么准备开放源代码的框架将需要大量的努力。