怎样使像Lamoda这样的大公司拥有简化的流程和数十个互连的服务,才能显着改变方法? 动机可能完全不同:从立法到所有程序员尝试进行内在的渴望。
但这并不意味着完全不能依靠其他好处。 如果您在Kafka上实现事件驱动的API,Sergey Zaika(
littleald )将会告诉您,究竟能赢得什么。 当然,关于颠簸和有趣的发现,肯定会有-没有它们,实验是无法做的。
免责声明:本文基于Sergey于2018年11月在HighLoad ++上举行的mitap的材料。 Lamoda在Kafka上的现场体验吸引了不少于其他时间表报告的听众。 在我们看来,这是一个很好的例子,表明总是有可能并且有必要找到志趣相投的人,而HighLoad ++的组织者将继续尝试营造一种有利于此的氛围。关于过程
Lamoda是一个大型的电子商务平台,具有自己的联系中心,送货服务(以及许多关联公司),照相馆,庞大的仓库,所有这些都可以在其软件上运行。 B2B合作伙伴有数十种付款方式,它们可以使用部分或全部这些服务,并希望了解其产品的最新信息。 此外,拉莫达除俄罗斯联邦外还在三个国家开展业务,那里的一切都有些不同。 总共可能有一百多种方法来配置新订单,必须以自己的方式进行处理。 所有这一切都借助数十种有时以不明显的方式进行通信的服务的帮助。 还有一个中央系统,其主要职责是订单状态。 我们称她为BOB,我和她一起工作。
带有事件驱动API的退款工具
事件驱动这个词很老套,我们将更详细地定义这是什么意思。 我将从决定尝试Kafka事件驱动的API方法的上下文开始。

在任何商店中,除了客户付款的订单外,有时还需要商店退货,因为产品不适合客户。 这个过程相对较短:如果需要,我们会澄清信息,然后转移资金。
但是由于法律的变化,回报很复杂,我们不得不为其实施单独的微服务。

我们的动力:
- FZ-54法 -简而言之,该法要求您在几分钟内以相当短的SLA向税务局报告每笔货币交易(无论是退货还是收货)。 作为电子商务,我们开展了许多业务。 从技术上讲,这意味着新的责任(因此也要提供新的服务)并改善所有相关系统。
- BOB拆分 -公司的内部项目,旨在消除BOB的大量非核心职责,并降低其整体复杂性。

该图描述了主要的Lamoda系统。 现在,它们中的大多数更像是一个
由5-10个微服务组成的
星座,围绕着不断减少的整体 。 它们正在缓慢增长,但是我们正在尝试使其变小,因为部署在中间突出显示的片段很可怕-您不能让它掉下来。 我们被迫保留所有交易所(箭头),并基于它们可能不可用的事实。
BOB中也有很多交易:付款,交付,通知系统等。
从技术上讲,BOB是:
- 〜150k行代码+〜100k行测试;
- php7.2 + Zend 1和Symfony组件3;
- > 100个API和〜50个出站集成;
- 4个国家/地区拥有自己的业务逻辑。
部署BOB既昂贵又痛苦,它所解决的代码量和任务如此之多,以至于没人能将其付诸实践。 通常,有很多原因可以简化它。
退货流程
最初,该过程涉及两个系统:BOB和付款。 现在又出现两个:
- 财政服务,将解决财政问题以及与外部服务的沟通问题。
- 退款工具,可以简单地将新交易所带入该工具中,以免使BOB膨胀。
现在,过程如下所示:

- BOB收到退款请求。
- BOB讨论了此退款工具。
- 退款工具显示付款:“取回款项”。
- 付款会退还款项。
- 退款工具和BOB彼此同步状态,因为目前他们俩都需要它。 由于BOB具有用户界面,会计报表以及通常无法轻松转移的许多数据,因此我们还没有准备好完全切换到退款工具。 我们必须坐在两把椅子上。
- 税收要求离开。
结果,我们在Kafka上制作了一种事件总线-一种事件总线,一切开始于此。 哇,现在我们有一个单点故障(讽刺)。

利弊非常明显。 我们做了公共汽车,所以现在所有的服务都依靠它。 这简化了设计,但在系统中引入了单点故障。 卡夫卡将倒下,进程将上升。
什么是事件驱动的API
Martin Fowler(GOTO,2017年)的报告
“事件驱动架构的多种含义”中提供了一个很好的答案。
简要地说,我们做了什么:
- 通过事件存储包装了所有异步交换。 我们没有通过网络通知每个有兴趣的消费者有关状态更改的信息,而是将状态更改事件写入集中式存储库,并且对该主题感兴趣的消费者可以读取那里出现的所有内容。
- 在这种情况下,一个事件是某处发生了更改的通知 。 例如,订单状态已更改。 对状态更改随附的某种数据感兴趣但对通知不感兴趣的消费者可以自己找到其状态。
- 最大的选择是成熟的事件源, 状态转移 ,其中事件包含处理所需的所有信息:从何处切换到什么状态,数据究竟发生了怎样的变化,等等。唯一的问题是您是否可以负担多少信息?
作为启动退款工具的一部分,我们使用了第三个选项。 由于不必获取详细信息,因此简化了事件的处理,此外,它还排除了每个新事件都会激起来自消费者的获取请求的情况。
退款工具服务
未加载 ,因此Kafka不仅仅是笔测试,而且不是必需的。 我认为,如果退款服务成为一项高负荷的项目,那么生意会很高兴。
异步交换原样
对于异步交换,PHP部门通常使用RabbitMQ。 我们收集了请求的数据,将其放入队列中,并且同一服务的使用者读取并发送了(或未发送)。 对于API本身,Lamoda积极使用Swagger。 我们设计API,在Swagger中对其进行描述,生成客户端和服务器代码。 我们还使用了稍微高级的JSON RPC 2.0。
在此使用esb总线,有人居住在activeMQ上,但是通常,
RabbitMQ是标准的 。
异步交换有待
通过事件总线设计交换时,会进行类比。 我们同样通过事件结构描述来描述未来的数据交换。 yaml格式,代码生成必须由我们自己完成,生成器根据规范创建DTO,并教客户和服务器如何使用它们。 生成分为两种语言
-golang和php 。 这样可以使库保持一致。 生成器使用golang编写,因此其名称为gogi。
卡夫卡的事件外包是很典型的事情。 有一个主要企业版本的Kafka Confluent解决方案,有一个
nakadi ,这是我们在Zalando领域的“兄弟”的解决方案。 我们
从香草Kafka入手的动机是让解决方案免费,直到我们最终决定是否在所有地方都使用它,同时还留有回旋余地和改进空间:我们想要支持我们的
JSON RPC 2.0 ,两种语言的生成器,以及其他用途。
具有讽刺意味的是,即使在这样令人满意的情况下,当与做出类似决定的Zalando有类似业务时,我们也无法有效地使用它。
在架构上,在启动时,模式如下:直接从Kafka读取,但仅通过events-bus写入。 卡夫卡(Kafka)有很多可供阅读的内容:经纪人,平衡者,并且或多或少已经准备好进行横向扩展,我想保留它。 该记录是我们想要遍历一个Gateway aka Events-bus的原因,这就是原因。
活动巴士
或事件总线。 这只是一个无状态的HTTP网关,它承担着多个重要角色:
- 生产验证 -我们验证事件是否符合规范。
- 事件主系统 ,即它是公司中主要且唯一的系统,用于回答哪些事件具有有效结构的问题。 验证仅包括数据类型和用于严格规定内容的枚举。
- 用于分片的哈希函数 -Kafka的消息结构是键-值,在这里它是通过将密钥放在其中的哈希值计算出来的。
为何
我们在一家大型公司中以简化的流程工作。 为什么要改变一些东西?
这是一个实验 ,我们希望能获得一些好处。
1:n + 1交流(一对多)
使用Kafka,可以很容易地将新使用者连接到API。
假设您有一个目录,该目录需要一次在多个系统中(以及在一些新系统中)保持最新。 以前,我们发明了一个实现set-API的捆绑软件,并将使用者地址报告给主系统。 现在,主系统向该主题以及所有有兴趣阅读的人发送更新。 一个新的系统出现了-他们在主题上签名了。 是的,也捆绑在一起,但更简单。
如果是BOB的退款工具,我们可以方便地通过Kafka使它们保持同步。 付款后,付款人说他们退了款:RTB银行发现了这笔钱,更改了状态,Fiscalization Service发现了这笔钱并敲了一张支票。

我们计划制作一个通知服务,该服务将通知客户有关其订单/退货的新闻。 现在,这种责任分散在系统之间。 对于我们来说,通知通知服务捕获和响应来自Kafka的相关信息(并在其他系统中禁用这些通知)就足够了。 不需要新的直接交易。
数据驱动
系统之间的信息变得透明-无论您的企业流血如何,积压的资源如何。 Lamoda拥有一个数据分析部门,该部门负责收集系统上的数据,并将其以可重用的形式放入业务和智能系统中。 Kafka使您可以快速为他们提供大量数据,并使这些信息保持最新状态。
复制日志
阅读后,消息不会消失,就像RabbitMQ一样。 当事件包含足够的信息以进行处理时,我们将拥有对象最近更改的历史记录,并且如果需要,还可以应用这些更改。
复制日志的存储时间取决于编写此主题的强度,Kafka允许您灵活设置存储时间和数据量的限制。 对于密集的主题,重要的是,即使在短期无法操作的情况下,所有消费者都必须有时间在信息消失之前阅读信息。 通常,事实证明
是以天为
单位存储数据,这足以支持。

然后对不熟悉Kafka的人重新介绍一下文档(图片也来自文档)
AMQP中有队列:我们将消息写入使用者的队列。 通常,一个队列由具有相同业务逻辑的一个系统处理。 如果需要通知多个系统,则可以教应用程序编写多个队列,或者配置与扇出机制的交换,扇出机制本身会克隆它们。
Kafka具有类似的
主题抽象,您可以在其中编写消息,但消息在阅读后不会消失。 默认情况下,当您连接到Kafka时,会收到所有消息,同时有机会保存您上次离开的地方。 也就是说,您按顺序阅读,不能将邮件标记为已读,而是保存ID,然后从中继续阅读。 您将在其上停止的id称为offset,该机制为commit offset。
因此,可以实现不同的逻辑。 例如,我们在4个实例中有针对不同国家/地区的BOB-Lamoda位于俄罗斯,哈萨克斯坦,乌克兰和白俄罗斯。 由于它们是分开部署的,因此它们有一些自己的配置和他们自己的业务逻辑。 在消息中,我们指出了它所指的国家。 每个国家/地区的每个BOB消费者使用不同的groupId进行阅读,如果该消息不适用于他,请跳过该消息,即 立即提交偏移量+1。 如果我们的付款服务读取了相同的主题,那么它将使用一个单独的组进行此操作,因此抵消不会重叠。
活动要求:- 数据的完整性。 我希望事件中有足够的数据以便可以对其进行处理。
- 廉正 我们委托事件总线来验证事件是一致的并且可以处理。
- 顺序很重要。 在返回的情况下,我们被迫与历史合作。 对于通知,顺序并不重要,如果它们是同类通知,则无论先到达哪个电子邮件,电子邮件都是一样的。 在退货的情况下,有一个明确的流程,如果您更改订单,那么将会有例外,将不会创建或处理退款-我们最终将处于其他状态。
- 连贯性。 我们有一个存储库,现在我们创建事件而不是API。 我们需要一种快速廉价地将有关新事件和更改的信息传输到我们的服务的方法。 这是通过在单独的git存储库和代码生成器中使用通用规范来实现的。 因此,不同服务中的客户端和服务器将与我们进行协调。
拉莫达的卡夫卡
我们有三个Kafka安装:
- 日志
- 研发;
- 事件总线。
今天,我们只谈论最后一点。 在events-bus中,我们没有非常庞大的安装-3个代理(服务器)和总共27个主题。 通常,一个主题就是一个过程。 但这是一个微妙的时刻,现在我们将探讨它。

上面是RPS图表。 退款过程标有青绿色线(是的,位于X轴上),粉红色是内容更新过程。
Lamoda的目录包含数百万种产品,数据一直在更新。 有些收藏已经过时,新的收藏代替了它们,新的款式不断出现在目录中。 我们试图预测明天对我们的客户来说有趣的事情,因此我们不断购买新事物,拍摄新事物并更新窗口。
粉红峰是产品更新,即产品变更。 可以看出,这些家伙先拍照,然后拍照! -下载了事件包。
Lamoda Events用例
我们将构建的体系结构用于此类操作:
- 跟踪退货状态 :号召性用语和所有相关系统的状态跟踪。 付款,状态,税收,通知。 在这里,我们尝试了这种方法,制作了工具,收集了所有错误,编写了文档,并告诉同事如何使用它。
- 更新产品卡:配置,元数据,特征。 一个系统读取(显示),几个系统写入。
- 电子邮件,推送和短信 :订单已收集,订单已到达,退货已被接受等,其中很多。
- 库存,仓库更新 -对项目进行定量更新,仅更新编号:仓库收货,退货。 与商品预订相关的所有系统都必须使用最相关的数据来运行。 现在排水系统升级非常复杂,Kafka将对其进行简化。
- 数据分析 (研发部门),机器学习工具,分析,统计数据。 我们希望信息透明-这款Kafka非常适合。
现在,更有趣的部分是有关填充锥体和六个月来发生的有趣发现的信息。
设计问题
假设我们要制作新东西-例如,将整个交付过程转移到Kafka。 现在,部分流程正在BOB的订单处理中实施。 在将订单转移到交货服务,转移到中间仓库等背后,有一个状态模型。 一整块,甚至两块,再加上一堆交付API。 他们对交付了解更多。
这些似乎是相似的区域,但是对于BOB中的订单处理和交货系统而言,状态是不同的。 例如,某些快递服务不发送中间状态,而仅发送最终状态:“已交付”或“丢失”。 相反,其他人则详细报告了货物的流动情况。 每个人都有自己的验证规则:对于某人来说,电子邮件是有效的,因此将被处理; 对于其他人来说,这是无效的,但是订单仍然会被处理,因为有电话可以通讯,并且有人会说根本不会处理这样的订单。
数据流
在Kafka的情况下,出现了组织数据流的问题。 这项任务与策略的选择有几分关连,我们将逐一讨论。
在一个主题中还是在另一个主题中?
我们有一个事件规范。 在BOB中,我们写道必须交付这样的订单,并指出:订单号,其组成,一些SKU和条形码等。 当货物到达仓库时,交货将能够接收状态,时间戳和所需的一切。 但是,我们还希望在BOB中接收有关此数据的更新。 我们面临着从交付中获取数据的逆向过程。 这是同一事件吗? 还是值得单独讨论的单独交流?
它们很可能非常相似,并且使一个主题成为主题的诱惑也不是没有道理的,因为一个单独的主题是单独的使用者,单独的配置以及所有这些的单独生成。 但事实并非如此。
新领域还是新事件?
但是,如果使用相同的事件,则会出现另一个问题。 例如,并非所有的交付系统都可以生成可以生成BOB的DTO。 我们发送给他们一个id,但是他们不保存它们,因为它们不需要它们,并且从启动事件总线过程的角度来看,此字段是必填字段。
如果我们为事件总线引入了一个必填字段的规则,那么我们将被迫在BOB或启动事件处理程序中设置其他验证规则。 验证开始蔓延到服务上-这不是很方便。
— . , - , , , , . — . — , . JSON .
refunds . -, refund update, type, , update . «» , , type.
Kafka
Avro , Confluent. . replication log, «». , , : , . , , , .
partitions
Kafka partitions. , , , .
Kafka . partition, . . , , , . , , , Kafka partition, Kafka — , .
Kafka ? ( JSON) key. -, , partition .
refunds , partition, , . - , partition.
Events vs commands
, . Event — : , - - (something_happened), , item refund. - , «item » refund , « refund» - .
, , — , - . something_happened (item_canceled, refund_refunded), something_should_be_done. , item .
, , . , . , do_something. , - ; , ; , -, - . , do_something, , .

RabbitMQ, , http, response — , . Kafka, , Kafka, , , .
, - , - . , , - . , «item_ready_to_refund», , refund , , «money_refunded». , .
细微差别
: , - , , .
, offset , .
, , . , events-bus, , PostgreSQL, MySQL UNSIGNED INT, PostgreSQL INT. , Id . Symfony . , , , , offset, , . , Symfony , offset.
- — , Kafka , . . .
Kafka tooling offset. , — , , redeployments. Kafka tooling offset, .
—
replication log vs rdkafka.so — . PHP, PHP, , , Kafka rdkafka.so, - . , , , - . , .
partitions,
consumers >= topic partitions . , . , partitions. , partition, 20 , , . , , partitions.
监控方式
, , , , .
, , , , , , . Kafka , . , .

, , , events-bus , . , Refund Tool , BOB - ( ).

consumer-group lag. , . , 0, . Kafka , .
Burrow , Kafka. API consumer-group , . Failed warning, , — , . , .

API. bob-live-fifa, partition refund.update.v1, , lag 0 — offset -.
updated_at SLA (stuck) . , , . Cron, , 5 refund ( ), - , . Cron, , 0, .
, , :
, — API Kafka, .
-, HighLoad++ , , .
-, KnowledgeConf . , 26 , .
PHP Russia ++ ( DevOpsConf ) — , .