在Go上开发高度可靠的服务器的技术

Web程序员有时会面临甚至会吓到专业人员的任务。 我们谈论的是开发无权犯错的服务器应用程序,涉及失败成本极高的项目。 该材料的作者(我们今天出版的翻译)将讨论如何处理此类任务。



您的项目需要什么级别的可靠性?


在深入研究开发高度可靠的服务器应用程序的细节之前,您应该问自己,您的项目是否真的需要可实现的最高可靠性。 对于大多数错误不特别可怕的项目,开发针对错误场景类似于普遍灾难的工作场景设计的系统的过程可能会变得不合理地复杂。

如果错误的代价没有变得很高,则可以接受一种方法,开发人员会在执行该方法时尽最大的努力来确保项目的可操作性,如果出现问题,他只是理解它们即可。 现代化的监控工具和持续的软件部署过程使您能够快速发现生产问题并几乎立即修复它们。 在许多情况下,这就足够了。

在我今天从事的项目中,事实并非如此。 我们正在谈论区块链的实现-分布式服务器基础架构,用于在信任度较低的环境中安全执行代码,同时达成共识。 该技术的一种应用是数字货币。 这是错误成本极高的系统的经典示例。 在这种情况下,项目开发人员确实需要使其非常非常可靠。

但是,在其他一些项目中,即使它们与财务无关,追求最高的代码可靠性也是有意义的。 维修经常中断的代码库的成本可以很快达到天文数字。 在解决问题的成本仍然很低的情况下,能够在开发过程的早期阶段发现问题的能力,对于及时投入时间和精力投入高度可靠的系统的开发方法,似乎是一种真正的回报。

也许解决方案是TDD?


通过测试进行开发( 测试驱动开发 ,TDD)通常被认为是解决不良代码的最佳方法。 TDD是一种纯粹的开发方法,在该应用程序中首先编写测试,然后才编写测试-仅当检查项目的测试停止产生错误时,才将代码添加到项目中。 这个过程保证了100%的代码覆盖了测试,并且常常给人一种错觉,认为代码在其使用的所有可能变体中都经过测试。

但是,事实并非如此。 TDD是一种很棒的方法,在某些领域效果很好,但是仅仅开发真正可靠的代码还不够。 甚至更糟的是,TDD激发开发人员错误的信心,并且这种方法的应用可能导致这样的事实,即他出于懒惰而不会编写测试来检查系统的故障,这种情况从常识的角度来看几乎是不可能的。 我们稍后再讨论。

测试是可靠性的关键


实际上,在编写代码之前还是之后创建测试都无关紧要,是否使用TDD之类的开发方法。 最主要的是进行测试。 测试是最好的防御性防御,可以保护您的代码免受生产问题的影响。

由于我们将非常频繁地运行测试,因此理想情况下,在将每行添加到代码中之后,都必须使测试自动化。 我们对代码质量的信心绝不能以手动检查为基础。 问题是人们容易犯错误。 一个人连续多次完成相同的艰巨任务后,对细节的关注就会减弱。

测试应该很快。 非常快

如果完成测试套件需要花费几秒钟以上的时间,那么开发人员很可能会懒惰并在不进行测试的情况下将代码添加到项目中。 速度是Go的最大优势之一。 这种语言的开发工具包是现有工具中最快的工具包之一。 几秒钟内即可完成编译,重建和测试项目。

此外,测试是开源项目的重要驱动力之一。 例如,这适用于与区块链技术相关的所有事物。 这里的开源几乎是一种宗教。 为了使使用它的人有信心,该代码库必须是开放的。 例如,这允许进行审核,创建分散的氛围,其中没有某些实体可以控制项目。

如果该项目不包括质量测试,则等待外部开发人员对该开源项目做出重大贡献是没有道理的。 外部项目参与者需要一种机制来快速检查他们编写的内容与项目中已添加内容的兼容性。 实际上,整套测试应在收到将新代码添加到项目的每个请求后自动执行。 如果应该通过这种请求添加到项目中的内容破坏了某些内容,则测试应立即报告此情况。

用测试完全覆盖代码库是一个欺骗性但重要的指标。 用测试实现100%代码覆盖率的目标似乎过高,但是考虑一下,事实证明,如果测试未完全覆盖代码,则部分代码未经验证即被发送到生产环境,这是以前从未执行过的。

用测试完全覆盖代码不一定意味着项目中有足够的测试,也不意味着这些测试绝对提供使用代码的所有选项。 可以放心地说,如果项目没有100%包含在测试中,则开发人员将无法确定代码的绝对可靠性,因为从未对代码的某些部分进行过测试。

尽管如此,在某些情况下测试太多。 理想情况下,每种可能的错误都应导致一项测试失败。 如果测试数量过多,也就是说,不同的测试会检查相同的代码片段,则修改现有代码并更改现有系统行为将导致以下事实:为了使现有测试与新代码相对应,将花费太多时间来处理它们。 。

为什么Go是高度可靠项目的绝佳选择?


Go是一种静态类型的语言。 类型是一起执行的各种代码之间的约定。 在项目组装过程中没有自动类型检查的情况下,如果您需要遵守严格的规则来覆盖测试代码,则我们将不得不实施测试以自行验证这些“合同”。 例如,这发生在基于JavaScript的服务器和客户端项目中。 编写仅针对类型检查的复杂测试意味着大量的额外工作,在Go的情况下,可以避免这些工作。

Go是一种简单而教条的语言。 如您所知,Go包含许多编程语言的传统思想,例如经典的OOP继承。 复杂性是可靠代码的最大敌人。 问题往往隐藏在复杂结构的关节处。 这体现在以下事实中:尽管使用某种设计的典型选项很容易测试,但测试开发人员甚至可能不会想到奇怪的边界情况。 最后,该项目将减少其中一种情况。 从这个意义上说,教条主义也是有用的。 在Go中,通常只有一种执行动作的方法。 这似乎是阻碍程序员自由进取的一个因素,但是当只能以一种方式完成某件事时,很难做错什么。

Go简洁但富有表现力。 可读的代码更易于分析和审核。 如果代码过于冗长,其主要目的可能会淹没在辅助结构的“噪音”中。 如果代码过于简洁,则其中的程序可能难以阅读和理解。 Go在简洁和表现力之间保持平衡。 例如,像Java或C ++这样的语言中,辅助结构并不多。 同时,Go结构(例如与错误处理等领域相关的结构)非常清晰且非常详细,从而简化了程序员的工作,并帮助他确保例如已经检查了所有可能的事情。

Go崩溃后具有清晰的错误处理和恢复机制。 完善的运行时错误处理机制是高度可靠的代码的基石。 Go对于返回和分发错误具有严格的规则。 在诸如Node.js的环境中,混合使用来控制程序流的方法(例如回调,promise和异步函数)通常会导致未处理的错误,例如未处理的对promise的拒绝 。 在类似事件发生后恢复程序几乎是不可能的

Go具有广泛的标准库。 依赖项是一种风险,尤其是当它们的来源是对代码的可靠性没有给予足够重视的项目时。 投入生产的服务器应用程序包含所有依赖项。 而且,如果出现问题,完成的应用程序的开发人员将对此负责,而不是由创建该应用程序的人之一负责。 结果,在为小型项目而编写的项目不堪重负的环境中,创建可靠的应用程序更加困难。

依赖项也是一种安全风险,因为项目的漏洞级别与其最不安全的依赖项的漏洞级别相对应。 广泛的标准库Go由其开发人员维护,状态良好,它的存在减少了对外部依赖项的需求。

开发速度高。 像Node.js这样的环境的关键特征是其极短的开发周期。 编写代码花费的时间更少,因此,程序员的工作效率更高。

Go也有很高的发展速度。 一组用于构建项目的工具足够快,可以立即查看实际的代码。 编译时间非常短;结果,在Go上运行的代码被视为未经编译但已被解释。 而且,该语言具有足够的抽象,例如垃圾回收系统,它使开发人员可以直接努力实现其项目的功能,而不用解决辅助任务。

实际实验


既然我们已经表达了足够的一般要点,现在该看一下代码了。 我们需要一个足够简单的示例,以便在研究它时,我们可以专注于开发方法,但是与此同时,它应该足够先进,以便我们在探讨它时可以谈一谈。 我认为从日常工作中获取一些东西是最容易的。 因此,我建议分析服务器的创建,该服务器处理类似于金融交易的事物。 该服务器的用户将能够检查与其帐户关联的帐户余额。 此外,他们将能够将资金从一个帐户转移到另一个帐户。

我们将尽量不使该示例复杂化。 我们的系统将有一台服务器。 我们不会与认证和密码系统联系。 这些是工作项目的组成部分。 但是,我们需要关注此类项目的核心,以展示如何使其尽可能可靠。

a将复杂的项目分为便于管理的部分


复杂性是可靠性的最大敌人。 处理复杂系统时,最好的方法之一就是应用众所周知的“分而治之”原则。 该任务需要分为几个小子任务,并分别解决每个子任务。 哪一方来处理我们的任务? 我们将遵循共同责任的原则。 我们项目的每个部分都应该有自己的责任范围。

这个想法非常适合流行的微服务架构。 我们的服务器将包含单独的服务。 每个服务将有一个明确定义的职责范围以及一个与其他服务进行交互的清晰描述的界面。

以这种方式构造服务器后,我们将能够决定每个服务的工作方式。 所有服务都可以在同一过程中一起执行,您可以从每个服务中创建单独的服务器并使用RPC建立它们之间的交互,还可以将服务分开并在单独的计算机上运行它们。

我们不会使任务复杂化,我们将选择最简单的选项。 即,所有服务将在同一过程中执行,它们将直接交换信息,例如库。 如有必要,将来可以轻松地查看和更改此体系结构解决方案。

那么我们需要什么服务? 我们的服务器可能过于简单,无法将其划分为多个部分,但是出于教育目的,我们仍将其划分。 我们需要响应旨在检查余额和执行交易的客户端HTTP请求。 其中一项服务可以与客户端的HTTP接口配合使用。 PublicApi称之为PublicApi 。 另一个服务将拥有有关系统状态的信息-资产负债表。 StateStorage称之为StateStorage 。 第三项服务将结合上述两者,并实现旨在改变余额的“合同”逻辑。 第三项服务的任务将是合同的执行。 VirtualMachine称之为VirtualMachine


应用服务器架构

将这些服务的代码放在项目文件夹/services/publicapi/services/virtualmachine/services/statestorage

▍明确服务职责


在实施服务的过程中,我们希望能够单独使用它们。 甚至有可能在不同的程序员之间分配这些服务的开发。 由于服务是相互依赖的,并且我们希望并行化它们的开发,因此我们需要开始对它们用于相互交互的接口进行清晰的定义。 使用这些接口,我们可以为每个接口之外的所有内容准备存根,从而自动测试服务。

如何描述界面? 一种选择是记录所有内容,但是文档具有过时的特性,在处理项目的过程中,文档和代码之间的差异开始累积。 另外,我们可以使用Go接口声明。 这是一个有趣的选项,但是最好描述该接口,以便此描述不依赖于特定的编程语言。 在一个非常实际的情况下,这对我们很有用,如果在进行项目的过程中决定使用其他语言来实现其某些服务,则其功能将更适合解决他们的问题。

描述接口的一种方法是使用protobuf 。 这是用于描述消息和服务端点的简单语言和独立于语言的协议。

让我们从StateStorage服务的接口开始。 我们将以键值视图结构的形式呈现应用程序的状态。 这是statestorage.proto文件的代码:

 syntax = "proto3"; package statestorage; service StateStorage { rpc WriteKey (WriteKeyInput) returns (WriteKeyOutput); rpc ReadKey (ReadKeyInput) returns (ReadKeyOutput); } message WriteKeyInput { string key = 1; int32 value = 2; } message WriteKeyOutput { } message ReadKeyInput { string key = 1; } message ReadKeyOutput { int32 value = 1; } 

尽管客户端通过PublicApi服务使用HTTP,但它也不会干扰通过与上述相同的方式( publicapi.proto文件)描述的明文接口:

 syntax = "proto3"; package publicapi; import "protocol/transactions.proto"; service PublicApi { rpc Transfer (TransferInput) returns (TransferOutput); rpc GetBalance (GetBalanceInput) returns (GetBalanceOutput); } message TransferInput { protocol.Transaction transaction = 1; } message TransferOutput { string success = 1; int32 result = 2; } message GetBalanceInput { protocol.Address from = 1; } message GetBalanceOutput { string success = 1; int32 result = 2; } 

现在我们需要描述TransactionAddress数据结构( transactions.proto文件):

 syntax = "proto3"; package protocol; message Address { string username = 1; } message Transaction { Address from = 1; Address to = 2; int32 amount = 3; } 

在该项目中,服务的原始描述位于/types/services文件夹中,而通用数据结构的描述位于/types/protocol文件夹中。

接口说明准备好后,就可以将其编译为Go代码。

这种方法的优点是,与接口描述不匹配的代码根本不会出现在编译结果中。 使用替代方法将需要我们编写特殊的测试来验证代码是否与接口描述匹配。

完整定义,生成的Go文件和编译说明可在此处找到。 这得益于Square Engineering及其goprotowrap的开发。

请注意,在我们的项目中,未实现传输层RPC,并且服务之间的数据交换看起来像普通的库调用。 当我们准备在不同服务器上分发服务时,可以向系统添加诸如gRPC的传输层。

project项目中使用的测试类型


由于测试是高度可靠的代码的关键,因此我建议我们首先讨论将为项目编写的测试。

单元测试


单元测试是测试金字塔的核心。 我们将单独测试每个模块。 什么是模块? 在Go中,我们可以将模块视为包中的独立文件。 例如,如果我们有文件/services/publicapi/handlers.go ,则将其单元测试放在/services/publicapi/handlers_test.go的同一包中。

最好将单元测试与测试代码放在同一程序包中,以使测试可以访问未导出的变量和函数。

服务测试


以下测试类型有不同的名称。 这些就是所谓的服务,集成或组件测试。 他们的本质是采取几个模块并测试他们的联合工作。 这些测试比测试金字塔中的单元测试高一个级别。 在我们的案例中,我们将使用集成测试来测试整个服务。 这些测试确定服务的规格。 例如,用于StateStorage服务的测试将放置在/services/statestorage/spec文件夹中。

最好将这些测试放在与所测试的代码所处的程序包不同的程序包中,以便仅通过导出的接口来访问此代码的功能。

端到端测试


这些测试位于测试金字塔的顶部,它们可以帮助检查整个系统及其所有服务。 这样的测试描述了系统的端到端e2e规范,因此我们将它们放在/e2e/spec文件夹中。

端对端测试以及服务测试必须放在与测试代码所在的软件包不同的软件包中,以便只能通过导出的接口来操作系统。

首先应该编写哪些测试? 从“金字塔”的基础开始,然后向上移动? 还是从顶部开始然后向下走? 这些方法中的任何一种都有生命权。 自上而下方法的好处是首先为整个系统创建规范。 通常在工作之初就讨论整个系统的功能是最容易的。 即使我们错误地将系统划分为单独的服务,系统规格也将保持不变。 此外,这将帮助我们理解较低级别的操作是错误的。

自上而下的方法的缺点是,端到端测试是在创建整个要开发的系统时在所有其他测试之后使用的测试。 这意味着它们将长时间产生错误。 在为我们的项目编写测试时,我们将使用这种方法。

▍测试开发


端到端测试开发


在创建测试之前,我们需要确定是否要编写测试而不使用任何辅助工具或使用某种框架。 依赖于框架,将其用作开发依赖项,比依赖于投入生产的代码中的框架的危险要小。 在我们的案例中,由于标准的Go库不具有不错的BDD支持,并且这种格式非常适合描述规范,因此我们将选择一个工作选项,其中包括使用框架。

有许多伟大的框架可以满足我们的需求。 其中包括GoConveyGinkgo

就个人而言,我喜欢结合使用GinkgoGomega (糟糕的名称,但要做什么)的组合,这些组合使用语法结构如Describe()It()

我们的测试是什么样的? 例如,这是对用户余额检查机制( sanity.go文件)的测试:

 package spec import ... var _ = Describe("Sanity", func() { var ( node services.Node ) BeforeEach(func() { node = services.NewNode() node.Start() }) AfterEach(func() { node.Stop() }) It("should show balances with GET /api/balance", func() { resp, err := http.Get("http://localhost:8080/api/balance?from=user1") Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) Expect(ResponseBodyAsString(resp)).To(Equal("0")) }) }) 

由于可以通过HTTP从外部访问服务器,因此我们将使用http.Get使用其Web API。 事务测试呢? 这是相应测试的代码:

 It("should transfer funds with POST /api/transfer", func() { resp, err := http.Get("http://localhost:8080/api/transfer?from=user1&to=user2&amount=17") Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) Expect(ResponseBodyAsString(resp)).To(Equal("-17")) resp, err = http.Post("http://localhost:8080/api/balance?from=user2", "text/plain", nil) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) Expect(ResponseBodyAsString(resp)).To(Equal("17")) }) 

测试代码完美地描述了它们的本质,甚至可以代替文档。 如您所见,我们承认存在负的用户帐户余额。 这是我们项目的功能。 如果禁止,则此决定将反映在测试中。

这是完整的测试代码

服务测试开发


现在,在开发了端到端测试之后,我们沿测试金字塔走了下来,然后继续创建服务测试。 这些测试是针对每种单独的服务开发的。 我们选择一个依赖于另一个服务的服务,因为这种情况比为独立服务开发测试更有趣。

让我们从VirtualMachine服务开始。 在这里,您可以找到带有此服务原型说明的界面。 由于VirtualMachine服务依赖于StateStorage服务并对其进行调用,因此我们需要为StateStorage服务创建一个模拟对象 ,以便StateStorage测试VirtualMachine服务。 存根对象允许我们在测试期间控制StateStorage响应。

如何在Go中实现存根对象? 这可以完全通过语言来完成,而无需辅助工具,或者您可以诉诸于适当的库,此外,这将使在测试过程中使用语句成为可能。 为此,我更喜欢使用go-mock库。

我们将存根代码放在文件/services/statestorage/mock.go 。 最好将存根对象与它们模仿的实体放在同一位置,以使它们可以访问未导出的变量和函数。 此阶段的存根是服务的示意性实现,但是,随着服务的发展,我们可能需要开发存根的实现。 这是存根对象( mock.go文件)的代码:

 package statestorage import ... type MockService struct { mock.Mock } func (s *MockService) Start() { s.Called() } func (s *MockService) Stop() { s.Called() } func (s *MockService) IsStarted() bool { return s.Called().Bool(0) } func (s *MockService) WriteKey(input *statestorage.WriteKeyInput) (*statestorage.WriteKeyOutput, error) { ret := s.Called(input) return ret.Get(0).(*statestorage.WriteKeyOutput), ret.Error(1) } func (s *MockService) ReadKey(input *statestorage.ReadKeyInput) (*statestorage.ReadKeyOutput, error) { ret := s.Called(input) return ret.Get(0).(*statestorage.ReadKeyOutput), ret.Error(1) } 

如果将各个服务的开发交给不同的程序员,则首先创建存根并将其传递给团队是有意义的。

让我们回到为VirtualMachine开发服务测试的过程。 我应该在这里检查什么情况? 最好专注于服务接口和每个端点的设计测试。 我们使用代表"GetBalance"方法的参数为CallContract()端点实现测试。 以下是相应的代码( contracts.go文件):

 package spec import ... var _ = Describe("Contracts", func() { var ( service uut.Service stateStorage *_statestorage.MockService ) BeforeEach(func() { service = uut.NewService() stateStorage = &_statestorage.MockService{} service.Start(stateStorage) }) AfterEach(func() { service.Stop() }) It("should support 'GetBalance' contract method", func() { stateStorage.When("ReadKey", &statestorage.ReadKeyInput{Key: "user1"}).Return(&statestorage.ReadKeyOutput{Value: 100}, nil).Times(1) addr := protocol.Address{Username: "user1"} out, err := service.CallContract(&virtualmachine.CallContractInput{Method: "GetBalance", Arg: &addr}) Expect(err).ToNot(HaveOccurred()) Expect(out.Result).To(BeEquivalentTo(100)) Expect(stateStorage).To(ExecuteAsPlanned()) }) }) 

请注意,我们正在测试的服务VirtualMachine通过简单的依赖项注入机制在Start()方法中获得了指向其依赖项StateStorage的指针。 这是我们传递存根对象的实例的地方。 另外,请注意stateStorage.When("ReadKey", &statestorage.ReadKeyInput{Key… ,在这里告诉存根对象访问它时的行为。当ReadKey方法时,它应该返回一个值100.然后,在Expect(stateStorage).To(ExecuteAsPlanned()) ,我们检查此命令是否仅被调用一次。

类似的测试成为该服务的规范。 可以在此处找到VirtualMachine服务的全套测试。 我们项目的其他服务的测试套件可在此处此处找到。

单元测试开发


也许"GetBalance"方法的合同的实现太简单了,所以"GetBalance"谈谈实现稍微复杂一些的"Transfer"方法。 用这种方法表示的将资金从一个帐户转移到另一个帐户的合同需要读取资金的发送者和接收者的余额数据,以计算新的余额并记录应用程序状态下发生的情况。 所有这些的服务测试与我们刚刚实现的服务测试( transactions.go文件)非常相似:

 It("should support 'Transfer' transaction method", func() { stateStorage.When("ReadKey", &statestorage.ReadKeyInput{Key: "user1"}).Return(&statestorage.ReadKeyOutput{Value: 100}, nil).Times(1) stateStorage.When("ReadKey", &statestorage.ReadKeyInput{Key: "user2"}).Return(&statestorage.ReadKeyOutput{Value: 50}, nil).Times(1) stateStorage.When("WriteKey", &statestorage.WriteKeyInput{Key: "user1", Value: 90}).Return(&statestorage.WriteKeyOutput{}, nil).Times(1) stateStorage.When("WriteKey", &statestorage.WriteKeyInput{Key: "user2", Value: 60}).Return(&statestorage.WriteKeyOutput{}, nil).Times(1) t := protocol.Transaction{From: &protocol.Address{Username: "user1"}, To: &protocol.Address{Username: "user2"}, Amount: 10} out, err := service.ProcessTransaction(&virtualmachine.ProcessTransactionInput{Method: "Transfer", Arg: &t}) Expect(err).ToNot(HaveOccurred()) Expect(out.Result).To(BeEquivalentTo(90)) Expect(stateStorage).To(ExecuteAsPlanned()) }) 

在处理项目的过程中,我们最终开始创建其内部机制,并在文件processor.go创建一个模块,其中包含合同的实现。 这是原始版本( processor.go文件):

 package virtualmachine import ... func (s *service) processTransfer(fromUsername string, toUsername string, amount int32) (int32, error) { fromBalance, err := s.stateStorage.ReadKey(&statestorage.ReadKeyInput{Key: fromUsername}) if err != nil { return 0, err } toBalance, err := s.stateStorage.ReadKey(&statestorage.ReadKeyInput{Key: toUsername}) if err != nil { return 0, err } _, err = s.stateStorage.WriteKey(&statestorage.WriteKeyInput{Key: fromUsername, Value: fromBalance.Value - amount}) if err != nil { return 0, err } _, err = s.stateStorage.WriteKey(&statestorage.WriteKeyInput{Key: toUsername, Value: toBalance.Value + amount}) if err != nil { return 0, err } return fromBalance.Value - amount, nil } 

此设计满足了服务测试,但是在我们的案例中,集成测试仅包含对基本方案的测试。 那么临界情况和潜在的失败呢? 如您所见,我们对StateStorage任何调用StateStorage可能失败。 如果需要通过测试覆盖100%的代码,我们需要检查所有这些情况。 单元测试非常适合实施此类测试。

由于我们将使用不同的输入数据多次调用该函数,并模拟参数以到达代码的所有分支,因此,为了使此过程更高效,我们可以采用基于表的测试。 Go倾向于避免异国情调的单元测试框架。 我们可以拒绝银杏 ,但可能我们应该离开Gomega 。 因此,此处执行的检查将类似于我们先前测试中执行的检查。 这是测试代码(文件processor_test.go ):

 package virtualmachine import ... var transferTable = []struct{ to string //  ,    read1Err error //       read2Err error //       write1Err error //       write2Err error //       output int32 //   errs bool //        }{ {"user2", errors.New("a"), nil, nil, nil, 0, true}, {"user2", nil, errors.New("a"), nil, nil, 0, true}, {"user2", nil, nil, errors.New("a"), nil, 0, true}, {"user2", nil, nil, nil, errors.New("a"), 0, true}, {"user2", nil, nil, nil, nil, 90, false}, } func TestTransfer(t *testing.T) { Ω := NewGomegaWithT(t) for _, tt := range transferTable { s := NewService() ss := &_statestorage.MockService{} s.Start(ss) ss.When("ReadKey", &statestorage.ReadKeyInput{Key: "user1"}).Return(&statestorage.ReadKeyOutput{Value: 100}, tt.read1Err) ss.When("ReadKey", &statestorage.ReadKeyInput{Key: "user2"}).Return(&statestorage.ReadKeyOutput{Value: 50}, tt.read2Err) ss.When("WriteKey", &statestorage.WriteKeyInput{Key: "user1", Value: 90}).Return(&statestorage.WriteKeyOutput{}, tt.write1Err) ss.When("WriteKey", &statestorage.WriteKeyInput{Key: "user2", Value: 60}).Return(&statestorage.WriteKeyOutput{}, tt.write2Err) output, err := s.(*service).processTransfer("user1", tt.to, 10) if tt.errs { Ω.Expect(err).To(HaveOccurred()) } else { Ω.Expect(err).ToNot(HaveOccurred()) Ω.Expect(output).To(BeEquivalentTo(tt.output)) } } } 

«Ω» — , — ( Gomega ). .

, TDD, , , . processTransfer() .

VirtualMachine . .

100% . , . .

, ? . , , , .

▍ -


. ? HTTP- Go (goroutine). , — , . , , , .

- . , , , , . - /e2e/stress . - ( stress.go ):

 package stress import ... const NUM_TRANSACTIONS = 20000 const NUM_USERS = 100 const TRANSACTIONS_PER_BATCH = 200 const BATCHES_PER_SEC = 40 var _ = Describe("Transaction Stress Test", func() { var ( node services.Node ) BeforeEach(func() { node = services.NewNode() node.Start() }) AfterEach(func() { node.Stop() }) It("should handle lots and lots of transactions", func() { //  HTTP-     transport := http.Transport{ IdleConnTimeout: time.Second*20, MaxIdleConns: TRANSACTIONS_PER_BATCH*10, MaxIdleConnsPerHost: TRANSACTIONS_PER_BATCH*10, } client := &http.Client{Transport: &transport} //      ledger := map[string]int32{} for i := 0; i < NUM_USERS; i++ { ledger[fmt.Sprintf("user%d", i+1)] = 0 } //     HTTP   rand.Seed(42) done := make(chan error, TRANSACTIONS_PER_BATCH) for i := 0; i < NUM_TRANSACTIONS / TRANSACTIONS_PER_BATCH; i++ { log.Printf("Sending %d transactions... (batch %d out of %d)", TRANSACTIONS_PER_BATCH, i+1, NUM_TRANSACTIONS / TRANSACTIONS_PER_BATCH) time.Sleep(time.Second / BATCHES_PER_SEC) for j := 0; j < TRANSACTIONS_PER_BATCH; j++ { from := randomizeUser() to := randomizeUser() amount := randomizeAmount() ledger[from] -= amount ledger[to] += amount go sendTransaction(client, from, to, amount, &done) } for j := 0; j < TRANSACTIONS_PER_BATCH; j++ { err := <- done Expect(err).ToNot(HaveOccurred()) } } //   for i := 0; i < NUM_USERS; i++ { user := fmt.Sprintf("user%d", i+1) resp, err := client.Get(fmt.Sprintf("http://localhost:8080/api/balance?from=%s", user)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) Expect(ResponseBodyAsString(resp)).To(Equal(fmt.Sprintf("%d", ledger[user]))) } }) }) func randomizeUser() string { return fmt.Sprintf("user%d", rand.Intn(NUM_USERS)+1) } func randomizeAmount() int32 { return rand.Int31n(1000)+1 } func sendTransaction(client *http.Client, from string, to string, amount int32, done *chan error) { url := fmt.Sprintf("http://localhost:8080/api/transfer?from=%s&to=%s&amount=%d", from, to, amount) resp, err := client.Post(url, "text/plain", nil) if err == nil { ioutil.ReadAll(resp.Body) resp.Body.Close() } *done <- err } 

, - . ( rand.Seed(42) ) , . . , , — , .

- HTTP , TCP- ( , , ). , , 200 IdleConnection TCP- . , 100.

… :

 fatal error: concurrent map writes goroutine 539 [running]: runtime.throw(0x147bf60, 0x15) /usr/local/go/src/runtime/panic.go:616 +0x81 fp=0xc4207159d8 sp=0xc4207159b8 pc=0x102ca01 runtime.mapassign_faststr(0x13f5140, 0xc4201ca0c0, 0xc4203a8097, 0x6, 0x1012001) /usr/local/go/src/runtime/hashmap_fast.go:703 +0x3e9 fp=0xc420715a48 sp=0xc4207159d8 pc=0x100d879 services/statestorage.(*service).WriteKey(0xc42000c060, 0xc4209e6800, 0xc4206491a0, 0x0, 0x0) services/statestorage/methods.go:15 +0x10c fp=0xc420715a88 sp=0xc420715a48 pc=0x138339c services/virtualmachine.(*service).processTransfer(0xc4201ca090, 0xc4203a8097, 0x6, 0xc4203a80a1, 0x6, 0x2a4, 0xc420715b30, 0x1012928, 0x40) services/virtualmachine/processor.go:19 +0x16e fp=0xc420715ad0 sp=0xc420715a88 pc=0x13840ee services/virtualmachine.(*service).ProcessTransaction(0xc4201ca090, 0xc4209e67c0, 0x30, 0x1433660, 0x12a1d01) Ginkgo ran 1 suite in 1.288879763s Test Suite Failed 

? StateStorage ( map ), . , , . , map sync.map . .

processTransfer() . , — . , , , , . , processTransfer() . .

, . , , .

 e2e/stress/transactions.go:44 Expected <string>: -7498 to equal <string>: -7551 e2e/stress/transactions.go:82 ------------------------------ Ginkgo ran 1 suite in 5.251593179s Test Suite Failed 

, . , , ( , ). , , .

— . TDD . ? , 100%?! , — . processTransfer() , , .

. , , . .

总结


, , , -, , , ? ? — .

, -. , «» processTransfer() . , , . , — . , - . , , .

. , . , StateStorage WriteKey , , , , WriteKeys , , .

, : . « ». -, , , , , . — . , , — .

, — GitHub. . , , , , , , .

亲爱的读者们! ?

Source: https://habr.com/ru/post/zh-CN413681/


All Articles