代码人生

在单体服务和微型服务之间:第三条道路

代码人生 http://www.she9.com 2018-08-22 14:08 出处:网络 编辑:@技术狂热粉
对于程序员、工程师和架构师来说,整体服务相对于微服务是一个非常热门的话题。我们对这两种方法研究得越多,就越意识到从头构建应用程序时,需要从一开始就选择其中一种。我认为或多或少每个人都对微服务感兴趣,每

对于程序员、工程师和架构师来说,整体服务相对于微服务是一个非常热门的话题。我们对这两种方法研究得越多,就越意识到从头构建应用程序时,需要从一开始就选择其中一种。我认为或多或少每个人都对微服务感兴趣,每个人都想使用它。它适用于基于云的可伸缩系统,它允许我们在不太关心我们选择的基础设施的情况下扩展系统。但是,从复杂性、设计和维护等方面对所要付出的代价进行理性的调查,并没有给我们真正的选择:整体利益才是赢家。但是,几个月后…


这就是今天软件工程的“生存还是毁灭”难题。


每个人对此都有自己合理的看法。但是,如果我们发现这样的问题不存在会发生什么呢?如果我们找到了一种将开发作为一个整体开始的方法,它允许我们以后根据我们的需要分发它的组件,会发生什么呢?您只需编写几行代码就可以重新配置整个系统,并从一个整体系统更改为一个微服务系统,反之亦然?是可能的吗?


在每一个令人沮丧的困境背后,总是会有一场思维的革命,它将揭示这种幻觉,并将这种困境化解。这里的错觉是什么?这种错觉是,不可能将一个整体构建为可分发消息传递组件的组合。这种错觉来自于这样一个事实,即我们通常从关注计算而不是通信的范式着手编程。我们将重点放在逻辑上,然后将组件分发和发布作为结果。作为一个示例,考虑将Java代码转换为REST服务的情况。我们将从面向对象的组件转到基于HTTP/ json的消息组件。在组件中,我们需要使用面向对象的方法进行推理,而在外部,我们需要以面向服务的方式进行推理。但这种转变并不是免费的。我们需要引入特定的软件层。

在单体服务和微型服务之间:第三条道路

改变你的想法

如果我们改变主意,首先考虑通信,然后再考虑计算,会发生什么?

在单体服务和微型服务之间:第三条道路

在这种情况下,我们所处理的所有实体都通过使用消息传递范式进行本地通信。组件的大小无关紧要,因为它总是一个可分发的消息传递组件。它总是一种服务。如果我们接受这个观点,我们就应该接受这样的观点:我们创建的每个系统都是天生的分布式的。因此,构建一个整体或基于微服务的系统的最初难题消失了,因为它变成了一个简单的问题,即将服务单独或一起部署到单个应用程序中。

在单体服务和微型服务之间:第三条道路


一种语言的方法


我相信现在你已经有了直觉,你正在试图通过使用某种框架来解决这个问题——但是,我们不想使用框架,因为我们不想支付引入框架的费用。在这里,主要的思想是跳到新一代的编程语言,它将面向服务的计算的基本概念具体化为语法结构,这一代编程语言专注于通信,并允许我们按照直观的方法编写分布式应用程序。

Jolie

Jolie是我们从2006年就开始研究的编程语言。它最初被认为是WS-BPEL和WSDL等SOA标准的形式化,但它立即向我们展示了它是一种处理基于服务的分布式编程的创新技术。微服务是我们第一次发布后的关键词,当我们发现它的时候,我们看到了很多强大的概念。唯一的区别是我们用一种语言而不是多种技术。Jolie有自己的语法,实际的引擎是用Java开发的开源项目。不幸的是,这里没有足够的空间来讨论朱莉的所有特征。让我们向你展示一下为什么我们没有和朱莉一起在整体服务和微服务之间陷入两难的境地。

An Example in Java

让我们考虑一个非常简单的例子:一个计算器,它提供了执行算术运算的单一方法,并根据选择选择相关的实现。让我们在Java中这样做,其中有一个名为Calculator的类,它提供了一个静态方法calculate,然后根据参数操作选择执行求和或减法的可能性。

        public class Calculator        
        {
        public enum CalcType {
        SUM,
        SUBT
        }
        
        public static int calculate( int x, int y, CalcType operation ) throws Exception {
        
                switch( operation ) {
                    case SUM:
                        return new Sum( x, y ).execute();
                    case SUBT:
                        return new Subt( x, y ).execute();
                    default:
                        throw new Exception("OperationNotSupported");
                }
            }
        }

非常简单。类Sum和Subt实现了一个名为OperationAbstract的抽象类,如下:

        public abstract class OperationAbstract {        
         protected int X;
         protected int Y;
         public OperationAbstract(int x, int y) {
          X = x;
          Y = y;
         }
        
         abstract int execute();
        }

如果出于任何原因,我们需要提取类和并将其作为REST服务部署到外部,会发生什么?我们将选择哪个框架来部署它?我们会提供一个Swagger接口吗?计算器类中的调用发生了什么?

An Example in Jolie

首先,请注意,在Jolie中,所有东西都是服务,所以负责计算和减去的组件必须是服务。因此,让我们介绍一下他们需要实现的接口。它可以与Java中的操作抽象相媲美,即使它是一个服务接口。

        type ExecuteRequest: void {        
           .x: int
           .y: int
        }
        
        interface OperationServiceInterface {
        RequestResponse:
        execute( ExecuteRequest )( int )
        }

The interface is called OperationServiceInterface and it provides a RequestResponse operation called  execute. A RequestResponse operation is an operation which receives a request message and replies with a response message. In this case, the request message type is defined by ExecuteRequest, which contains two subnodes: x and y. Both of them are integers. On the other hand, the response is just an integer. Now, let us see how a service which implements this interface looks:

include "OperationServiceInterface.iol"

execution{ concurrent }

inputPort Sum {
    Location: "socket://localhost:9000"
    Protocol: sodep
    Interfaces: OperationServiceInterface
}

main {
    execute( request )( response ) {
        response = request.x + request.y
    }
}

Note that the keyword highlight is pretty ugly because the Jolie syntax does not exist on DZone.

Row 1 means that we are including the file where OperationServiceInterface is defined. Inclusion is just a way for better organizing code into separated files. In row 3, the directive execution{ concurrent } states that all the initiated sessions must be executed concurrently, thus the service is able to serve several calls simultaneously. At rows 5-9, we declare the listening endpoint (in Jolie it is called inputPort) where we receive messages for the service Sum. Note that an inputPort requires a Location, that is where the message must be sent, a Protocol, that is the way a message is sent (sodep is a binary protocol you can use between Jolie services) and the Interfaces available at that endpoint. Finally, the scope main at rows 11-15 defines the code to be executed when a message is received on operation execute. The incoming message parameters are stored into the variable request, whereas the variable response holds the reply which will be automatically sent when the scope ends. The service Subt is identical with the exception of the body of the operation execute and the Location which will be different because the two services are independently deployed.

Now we need a service which plays the same role of class Calculator and that finalizes the architecture, as follows:

在单体服务和微型服务之间:第三条道路

Let us see the code of the service Calculator:

include "OperationServiceInterface.iol"

type CalculateRequest: void {
   .x: int
.y:int
.op: string
}

interface CalculatorInterface {
RequestResponse:
calculate( CalculateRequest )( int )
throws OperationNotSupported
}

execution{ concurrent }

outputPort Operation {
  Protocol: sodep
Interfaces: OperationServiceInterface
}

inputPort Calculator {
    Location: "socket://localhost:8999"
    Protocol: sodep
    Interfaces: CalculatorInterface
}

main {
    calculate( request )( response ) {
        if ( request.op == "SUM" ) {
Operation.location = "socket://localhost:9000"
        } else if ( request.op == "SUBT" ) {
            Operation.location = "socket://localhost:9001"
        } else {
            throw( OperationNotSupported )
        }
        ;
        undef( request.op );
        execute@Operation( request )( response )
    }
}

Note that in rows 22-26, there is the definition of the inputPort of the service Calculator which is listening on port 8999 where the CalculatorInterface is defined in rows 3-13. Differently from the ExecuteRequest of the Sum and Subt services, here the request also contains the subnode .op:string, which permits to specify the operation type. In Jolie, it is also possible to specify a fault sent as a response, like we did in line 12, where we define that operation calculate can also reply with the fault OperationNotSupported.

In rows 17-20, we define a target endpoint for the service Calculator by means of a primitive outputPort. Usually, an outputPort requires the same parameters of an inputPort (Location, Protocol, Interfaces) but here the Location is omitted because it is dynamically bound at runtime depending on the value of request node op. Indeed, in rows 31 and 33, we bind the port Operation to a different location depending on if we call the service Sum or the service Subt. In line 39, we actually call the operation service which is now correctly bound to the selected service operation. In line 38, we erase the node .op from the request in order to reuse it as a request message for the operation service.

As you can see, such a system is distributed. We could place the three services in different machines on the same network and it would work. But what about our initial dilemma? What about deployment as a monolith? The solution is very simple — we just need to run the services Calculator, Sum, and Subt together within the same virtual machine. In Jolie, we can achieve this by embedding the services Sum and Subt inside Calculator and everything will work in the same way. In the following you can see how the service Calculator must be modified in order to obtain a monolith:

/* interface definition does not change */

execution{ concurrent }

outputPort Operation {
Protocol: sodep
Interfaces: OperationServiceInterface
}

embedded {
  Jolie:
  "sum.ol",
    "subt.ol"
}

inputPort Calculator {
    Location: "socket://localhost:8999"
    Protocol: sodep
    Interfaces: CalculatorInterface
}

main {
    calculate( request )( response ) {
        if ( request.op == "SUM" ) {
            Operation.location = "local://Sum"
        } else if ( request.op == "SUBT" ) {
            Operation.location = "local://Subt"
        } else {
            throw( OperationNotSupported )
        }
        ;
        undef( request.op );
        execute@Operation( request )( response )
    }
}

The only differences are in lines 10-14, 25, and 27. In rows 10-14, we use a primitive of Jolie called embedding which permits executing external services inside the parent one. In this case, the service Calulator embeds the target services Sum and Subt defined in files sum.ol and subt.ol respectively. In lines 25 and 27, we just specify the new local locations for service Sum and Subt. It is worth noting that, clearly, aslo their inputPorts must be modified coherently. For example, the inputPort of the service Sum now must be:

inputPort Sum {
    Location: "local://Sum"
    Protocol: sodep
    Interfaces: OperationServiceInterface
}

软件的结构不会在整体系统和分布式系统之间发生变化。


这里最重要的事实是,软件的结构不会因部署而改变。通过将服务编程的核心概念具体化为一组连贯的原语,我们得到了这样的结果。软件天生就被认为是分布式的,它的所有组件都是作为服务诞生的。此外,正如示例所示,按照代码行来付出的努力与Java的情况具有相同的数量级。


在Jolie中,可编程软件的基本单位是服务,程序员只能创建服务而不关心它们的实际部署。基于所有这些原因,在朱莉看来,决定建造一个庞然大物不是生与死之间的选择,而是一种简单的部署选择,很容易被推迟。

记住,单体系统和分布式系统是不同的


虽然在Jolie看来,单体系统和分布式系统的区别很轻,但我们不能忘记,分布式系统从本质上与单体系统是不同的。很明显,当我们分发一个组件时,我们需要考虑网络的谬误,这些谬误并不存在于一个整体中,所有的交互都在内存中运行。在某些情况下,可靠性非常重要,我们需要通过编程恢复活动引入额外的代码来处理通信异常。在Jolie中,由于语言提供了强大的故障处理机制,这样的努力被减轻了。此外,还有用于处理终止和补偿的特定原语,在复杂的分布式场景中非常珍贵。

结论


通过本文,我希望已经展示了直接将软件视为服务的分布式组合存在的可能性,即使是在一个整体的情况下也是如此。如果我们考虑利用一种专门的编程语言来实现它,我们的观点可能会发生很大的变化,以至于选择单一方法的两难选择可能会消失。这种情况的发生仅仅是因为软件已经被分发了,而整体只是部署它的一种方式。


像Jolie这样创建一种新的编程语言的项目非常令人兴奋,当在生产环境中使用时,它也给了我们很多满足。我们试验了本地编程分布式应用程序的所有好处,并且非常热衷于这种方法每天带来的新可能性。


请关注公众号:程序你好
0

精彩评论

暂无评论...
验证码 换一张
取 消