aws lambda使用_使用AWS Lambda,无服务器框架和Go的按需WebSockets

aws lambda使用

Lambda functions and WebSockets can be seen as concepts difficult to reconcile. Lambdas are ephemeral by nature. They appear when invoked and then disappear sometime after they have finished working. WebSockets, on the contrary, maintain stable, long-living connections between one server and many clients.

Lambda函数WebSockets可以视为难以协调的概念。 Lambdas是短暂的。 它们在被调用时出现,然后在完成工作后的某个时间消失。 相反,WebSocket可以在一台服务器和许多客户端之间维持稳定的长期连接。

The AWS APIGateway offers the possibility to bring these two things together, combining the advantages of the Lambda on-demand model with the power of WebSockets’ real-time bidirectional communication.

AWS APIGateway提供了将这两件事结合在一起的可能性,将Lambda按需模型的优势与WebSockets实时双向通信的强大功能相结合。

In this article, we describe how to implement a WebSocket server using AWS API Gateway and Lambda functions. We will use the Serveless framework to set up the infrastructure and deploy the solution and Go as the programming language.

在本文中,我们描述了如何使用AWS API Gateway和Lambda函数实现WebSocket服务器。 我们将使用Serveless框架来设置基础结构并部署解决方案,并将Go用作编程语言。

Serverless has been chosen because it is an easy and well-documented way to define infrastructure as code. Go has been chosen because it guarantees it has the potential to optimise Lambda costs and provide low cold-start latency, an important feature for the implementation of such a model.

选择无服务器是因为它是将基础结构定义为代码的简便且有据可查的方法。 选择Go是因为它保证了它有潜力优化Lambda成本并提供低的冷启动等待时间,这是实现这种模型的重要功能。

一个简单的WebSocket服务器以及客户端可以做什么 (A Simple WebSocket Server and What Clients Can Do)

To make things concrete, we are going to implement a simple WebSocket server. The goal of this server is to broadcast, as an echo, each message received from one client to all connected clients.

为了使事情具体,我们将实现一个简单的WebSocket服务器。 该服务器的目标是将从一个客户端接收到的每个消息作为回音广播到所有连接的客户端。

Image for post
An echo-broadcasting server
回声广播服务器

As we can see from the diagram, each client connects to the server, sends messages to be broadcast by the server to all connected clients, and, in the end, disconnects from the server.

从图中可以看出,每个客户端都连接到服务器,将要由服务器广播的消息发送到所有连接的客户端,最后断开与服务器的连接。

In other words, a client can trigger three types of events:

换句话说,客户端可以触发三种类型的事件:

  • connect to the server

    连接到服务器

  • disconnect from the server

    服务器断开连接

  • send a message to the server

    服务器发送消息

The server, on the other side, has to react appropriately to these events. The way it reacts is codified using Lambda function(s).

另一方面,服务器必须对这些事件做出适当的React。 它的React方式使用Lambda函数进行编码。

在云中设置组件 (Setup of the Components in the Cloud)

Now that we understand what the clients can do and what the server is called to do, we can start building the infrastructure in the cloud. As we said, we are going to use the Serverless framework to do this.

现在我们了解了客户端可以做什么以及服务器被要求做什么,我们可以开始在云中构建基础架构。 如前所述,我们将使用无服务器框架来完成此任务。

The configuration of the API Gateway and the Lambda function(s) is pretty simple. We start linking the Lambda function(s) to the types of event they are asked to manage. This is the Serveless yaml configuration that defines such links.

API网关和Lambda函数的配置非常简单。 我们开始将Lambda函数链接到要求它们管理的事件类型。 这是Serveless yaml配置,它定义了此类链接。

Serverless configuration yaml
无服务器配置Yaml

In the above snippet, we define a service, a provider, and a Lambda function to be executed when the WebSocket events reach the server. The route property links an event to its function, meaning that when the event occurs, the function is triggered. The handler property points to the code that the Lambda function will execute, which in this case is bin/handleRequest, a Go compiled executable.

在上面的代码片段中,我们定义了当WebSocket事件到达服务器时要执行的服务,提供程序和Lambda函数。 route属性将事件链接到其功能,这意味着事件发生时,将触发该功能。 handler属性指向Lambda函数将执行的代码,在这种情况下为bin/handleRequest ,它是Go编译的可执行文件。

In this example, the same function (implemented by bin/handleRequest) manages the connect, disconnect, and default events. We could have defined different functions to manage different events, but we opted for a single function for the sake of simplicity and to allow some forms of optimisation which we will discuss later.

在此示例中,相同的功能(由bin/handleRequest )管理connectdisconnectdefault事件。 我们可以定义不同的功能来管理不同的事件,但是为了简单起见,我们选择了一个功能,并允许某些形式的优化,我们将在后面讨论。

Let’s go back to the events. We know what connect and disconnect are. But what is default?

让我们回到事件。 我们知道什么是connectdisconnect 。 但是default是什么?

When a client sends a message to the server, the meaning (i.e., the semantic) of the message is embedded in the content of its payload. For instance, if the message carries a JSON payload, then the JSON could have a property action to identify the logic the message is supposed to trigger. We could then configure AWS API Gateway to react with different Lambda functions to messages with different semantics. In other words, we could attach different Lambda functions to the different values of the action field (if we follow the example above). If no match is found, then the system reverts back to the default event and the Lambda function linked to it.

当客户端向服务器发送消息时,消息的含义(即语义)将嵌入其有效负载的内容中。 例如,如果消息携带JSON有效负载,则JSON可以具有属性action来标识消息应该触发的逻辑。 然后,我们可以配置AWS API Gateway以与不同的Lambda函数对具有不同语义的消息作出React。 换句话说,我们可以将不同的Lambda函数附加到action字段的不同值(如果我们遵循上面的示例)。 如果未找到匹配项,则系统将还原为default事件,并将Lambda函数链接到该default事件。

In our case we use the same Lambda function for all types of events, so no custom event is defined and only the $default event is configured.

在我们的案例中,我们对所有类型的事件使用相同的Lambda函数,因此未定义custom事件,仅配置了$default事件。

执行结构 (Go Implementation Structure)

The structure of the Go implementation of a Lambda function used by an API Gateway WebSocket service is also pretty simple.

API网关WebSocket服务使用的Lambda函数Go实现的结构也非常简单。

We need to define a main function in the main package. The main function simply calls lambda.Start(handleRequest).

我们需要在main包中定义一个main函数。 main功能只是调用lambda.Start(handleRequest)

Since in our case one single Lambda function manages all types of events, we need to find the way to recognise the type of event and implement some switch logic based on it. handleRequest knows which type of event it is dealing with by querying the RouteKey field of the RequestContext of the event struct passed in as a parameter, which is of type APIGatewayWebsocketProxyRequest. Based on the type of event, the appropriate logic is executed.

由于在我们的案例中,一个Lambda函数可以管理所有类型的事件,因此我们需要找到一种方法来识别事件的类型并基于此实现一些开关逻辑。 handleRequest知道它是通过查询处理事件的类型RouteKey所述的字段RequestContext在作为参数传递,它的类型的事件结构的APIGatewayWebsocketProxyRequest 。 根据事件的类型,执行适当的逻辑。

$ connect,$ disconnect和connectionID ($connect, $disconnect and connectionIDs)

A WebSocket server is able to send targeted messages to specific clients. This means that each connected client needs to have a unique connectionID and that the server needs to keep track of such connectionIDs.

WebSocket服务器能够将目标消息发送到特定客户端。 这意味着每个连接的客户端都需要具有唯一的connectionID ,并且服务器需要跟踪此类connectionID

Image for post

The API Gateway is responsible to assign a unique connectionID to each client when they connect to the server. That connectionID is passed as a parameter in each invocation of the handleRequest function.

当每个客户端连接到服务器时,API网关负责为其分配唯一的connectionID 。 在每个handleRequest函数调用中,该connectionID作为参数传递。

The WebSocket server needs to have some form of store for the active connectionIDs. When a $connect event is received, the connectionID is added to the store. When a $disconnect event occurs, the connectionID is removed from the store.

WebSocket服务器需要具有某种形式的活动connectionID存储。 收到$connect事件时, $connect connectionID添加到存储中。 发生$disconnect事件时, connectionID从存储中删除。

Given the ephemeral nature of Lambda functions, we can not use its internal memory to store the connectionIDs, but we need to rely on some durable external system, for instance a database.

鉴于Lambda函数的短暂特性,我们不能使用其内部存储器来存储connectionID ,但是我们需要依赖一些持久的外部系统,例如数据库。

Image for post
ConnectionIDs store
ConnectionIDs存储

Typically we would consider to use AWS Dynamo DB, but any permanent storing mechanism would work (we actually use MongoDB on Atlas in the code provided, just to add an unusual bit to the example). So, to keep the implementation open, we define an interface, connectionStorer, to describe the behaviour we expect and delegate to some dependency injection mechanism the decision on the specific implementation to use for it.

通常,我们会考虑使用AWS Dynamo DB,但是任何永久存储机制都可以使用(我们在提供的代码中实际上在Atlas上使用了MongoDB ,只是在示例中添加了一个不寻常的地方)。 因此,为了保持实现的开放性,我们定义了一个接口connectionStorer ,以描述我们期望的行为,并将对特定实现的决策委派给某种依赖注入机制。

connectionStorer interface
connectionStorer接口
manage $connect and $disconnect events
管理$ connect和$ disconnect事件

$ default,动作发生的地方 ($default, Where the Action Happens)

Well, actually in our example not much happens. The logic is pretty simple. Every message received is echoed back to all connected clients. This simple logic is triggered by the $default case and is delegated to an echo function.

好吧,实际上在我们的例子中并没有发生太多。 逻辑很简单。 收到的每个消息都会回显给所有连接的客户端。 这个简单的逻辑由$default情况触发,并委托给echo函数。

manage $default event
管理$ default事件

The Lambda function acts like a client of the API Gateway, creating an instance of the gateway and posting to it the echoed message, one post per active connection.

Lambda函数的行为就像API网关的客户端一样,创建网关的实例并将其回显的消息发布到该实例,每个活动连接一个消息。

There is also a small trick in the implementation. If the message received from the client represents an integer, then this value is used to simulate a long running process using time.Sleep. We will use this trick later to test how Lambda reacts to concurrent requests coming from different clients.

在实现中还有一个小技巧。 如果从客户端收到的消息表示一个整数,则此值用于使用time.Sleep模拟长时间运行的过程。 我们稍后将使用此技巧来测试Lambda如何响应来自不同客户端的并发请求。

优化策略 (Optimisation Strategies)

It is worth noticing that we use the global variable apigateway to store one single instance of the gateway for the entire lifespan of the Lambda function. The reason behind this choice is that an instance of a Lambda function can serve more than one subsequent request. So, while it is true that a Lambda function can be instantiated on demand, when a request arrives, it is also true that this instance remains active and ready to serve other requests for a certain period of time (simple empiric measurements suggest that this period is about ten minutes). We can define global variables to hold resources which we want to reuse for subsequent request processing, like, for instance, the API Gateway instance.

值得注意的是,我们使用全局变量apigateway在Lambda函数的整个生命周期中存储一个网关实例。 该选择背后的原因是Lambda函数的实例可以处理多个后续请求。 因此,虽然可以按需实例化Lambda函数,但当请求到达时,也确实是该实例保持活动状态并准备在一定时间内为其他请求提供服务(简单的经验测量表明,这一时期大约十分钟)。 我们可以定义全局变量来保存我们要重用于后续请求处理的资源,例如API Gateway实例。

Similarly, we hold in a global variable the MongoDB connection since creating it is an expensive operation which we want to perform only one time per Lambda function instantiation. This is the reason why having a single handleRequest function to manage all events — $connect, $disconnect, and $default — brings an advantage in terms of efficiency. All these events need to connect to the MongoDB, a single handler for all these events means a single Lambda function and therefore the need to open a single database connection for all events.

同样,我们将MongoDB连接保存在全局变量中,因为创建它是一项昂贵的操作,我们希望每个Lambda函数实例化仅执行一次。 这就是为什么只有一个handleRequest函数来管理所有事件( $connect$disconnect$default在效率方面具有优势的原因。 所有这些事件都需要连接到MongoDB,所有这些事件的单个处理程序意味着单个Lambda函数,因此需要为所有事件打开单个数据库连接。

相同的Lambda函数可以满足多少个请求 (How Many Requests Can Be Served by the Same Lambda Function)

A Lambda function is ephemeral but not too ephemeral. The same instantiation of a Lambda function can serve more than one request. Let’s examine few different cases.

Lambda函数是短暂的,但不是短暂的。 Lambda函数的相同实例可以服务多个请求。 让我们研究几种不同的情况。

相同Lambda函数处理的顺序请求 (Sequential requests served by the same Lambda function)

Image for post
One Lambda function instance serves more requests
一个Lambda函数实例可处理更多请求

Some requests are sent sequentially to the same Lambda endpoint. The Lambda function is fast and completes the execution of a request before the next one comes in. In this case, we can expect only one instance of Lambda function serving all the requests. (This can be checked simply logging when the global variables are set and verifying that this happens only once.)

某些请求将顺序发送到同一Lambda端点。 Lambda函数速度很快,可以在下一个请求进入之前完成请求的执行。在这种情况下,我们可以预期只有Lambda函数的一个实例可以满足所有请求。 (这可以通过简单地记录设置全局变量的时间并验证是否仅发生一次来检查。)

两个后续请求之间的空闲时间长 (Long idle time between two subsequent requests)

Image for post
Lambda function remains idle too long and is terminated
Lambda函数保持空闲时间过长并终止

On the other hand, if we leave the Lambda function idle for a too long period, then the Lambda function is eventually terminated. When the next request arrives, a new Lambda function is created to execute the new request. Again, this can be checked by logging the creation of the global variables.

另一方面,如果我们将Lambda函数保持空闲时间太长,则Lambda函数最终会终止。 下一个请求到达时,将创建一个新的Lambda函数以执行新请求。 同样,可以通过记录全局变量的创建来检查。

并发请求 (Concurrent requests)

Image for post
Two concurrent requests are processed by two different Lambda functions
两个并发请求由两个不同的Lambda函数处理

The last case is that of concurrent requests. A request is still being processed by a Lambda function instance when the next one comes in. In this case, another instance of Lambda function is spun off since, as per AWS Lambda documentation, “a single instance of your Lambda function will never handle multiple events simultaneously”. This can be tested using the Sleep capability of the echo function.

最后一种情况是并发请求。 当下一个请求传入时,Lambda函数实例仍在处理请求。在这种情况下,Lambda函数的另一个实例会被剥离,因为根据AWS Lambda文档 ,“ Lambda函数的单个实例永远不会处理多个活动同时进行”。 可以使用echo功能的“睡眠”功能进行测试。

It is important to be aware of such behaviours since this gives us the possibility to implement some optimisations, like the caching of database connections. On the other hand, we need to be cautious when using such techniques since many details are not documented, e.g., the time a Lambda function can remain idle before being terminated, and we cannot blindly count on them in our logic.

重要的是要意识到这种行为,因为这使我们能够实现一些优化,例如缓存数据库连接。 另一方面,由于没有记录许多细节,例如,Lambda函数在被终止之前可以保持空闲的时间,并且我们不能在逻辑上盲目指望它们,因此在使用此类技术时我们需要谨慎。

为什么去? (Why Go?)

We have seen that one Lambda function can serve only one request at a time. So we cannot use the power of goroutines to process more requests concurrently. Managing the concurrency among different requests is the job of API Gateway and Lambda.

我们已经看到,一个Lambda函数一次只能服务一个请求。 因此,我们无法使用goroutine的功能来同时处理更多请求。 管理不同请求之间的并发是API Gateway和Lambda的工作。

Nonetheless, we can still leverage Go concurrency within the process of a single request, if this makes sense. And in many cases it can make sense.

尽管如此,如果有必要,我们仍然可以在单个请求的过程中利用Go并发。 在许多情况下,这是有道理的。

Image for post
A Lambda function leveraging Go concurrency
利用Go并发的Lambda函数

Let’s consider the above example, where the process of a single request requires different I/O operations, e.g., call a REST API, access a DB, do something with storage. In this case, we can make good use of Go by executing concurrently all I/O operations, reducing thus the total time of Lambda processing, minimising the response time, and potentially optimising costs, considering that Lambda are priced also by processing time.

让我们考虑上面的示例,其中单个请求的过程需要不同的I / O操作,例如,调用REST API,访问数据库,对存储进行某些操作。 在这种情况下,考虑到Lambda也按处理时间定价,我们可以通过同时执行所有I / O操作来充分利用Go,从而减少Lambda处理的总时间,最小化响应时间并潜在地优化成本。

Go, being a compiled language, is also a good choice if we want to have efficient use of memory. And memory is the other factor that influences the cost of executing Lambda functions.

如果我们想有效利用内存,Go是一种编译语言,也是一个不错的选择。 内存是影响执行Lambda函数成本的另一个因素。

Finally, Go seems also a good choice if we want to have a low cold-start latency, i.e., the time required to start a Lambda function which does not yet have an execution context, even if benchmarks for this aspect vary.

最后,如果我们希望具有低的冷启动延迟 (即,启动尚不具有执行上下文的Lambda函数所需的时间),即使该方面的基准有所不同 ,Go也是一个不错的选择。

构建,部署和测试 (Build, Deploy, and Test)

Finally, we need to build, deploy, and test our WebSocket server. The Serverless framework template guides us in these steps, which anyways are pretty straightforward.

最后,我们需要构建,部署和测试WebSocket服务器。 无服务器框架模板指导我们执行这些步骤,无论如何这些步骤都非常简单。

For build we need to run the following command:

对于构建,我们需要运行以下命令:

env GOOS=linux go build -ldflags="-s-w" -o ./bin/handleRequest ./handleRequest

env GOOS=linux go build -ldflags="-sw" -o ./bin/handleRequest ./handleRequest

Once the build is complete, we can deploy with the following command:

构建完成后,我们可以使用以下命令进行部署

sls deploy

sls deploy

In the deploy command, we can specify environment variable values in case we use them in the implementation, e.g., to pass db connection strings.

在deploy命令中,我们可以指定环境变量值,以防我们在实现中使用它们,例如,传递数据库连接字符串。

To manually test the server functionalities, we use wscat to launch an interactive command line base session via the following command:

为了手动测试服务器功能,我们使用wscat通过以下命令启动交互式命令行基础会话:

wscat -c wss://urlOfTheServer

wscat -c wss://urlOfTheServer

From the prompt, we can type any message and press enter to send a message to the server.

在提示符下,我们可以键入任何消息,然后按enter将消息发送到服务器。

结论 (Conclusions)

We have gone through the steps required to build a WebSocket server based on AWS API Gateway and AWS Lambda functions. We have used Serverless for the setup of the cloud configuration and the deployment, and Go to program the logic.

我们已经完成了基于AWS API Gateway和AWS Lambda函数构建WebSocket服务器所需的步骤。 我们已使用Serverless进行云配置和部署的设置,并使用Go语言对逻辑进行编程。

The good news is that this is a relatively easy way to build a WebSocket server available on demand, as per Serverless philosophy.

好消息是,按照无服务器理念,这是一种构建按需可用的WebSocket服务器的相对简单的方法。

We have now a powerful additional tool to address use cases where we want to provide rich, interactive, many-to-many platforms without incurring the complexity and costs involved in setting up and manage dedicated servers or containerised solutions.

现在,我们有了功能强大的附加工具,可以解决需要提供丰富,交互式,多对多平台的用例,而又不会引起设置和管理专用服务器或容器化解决方案所涉及的复杂性和成本。

You can find all the code of the example on GitHub.

您可以在GitHub上找到该示例的所有代码

翻译自: https://medium.com/better-programming/websockets-on-demand-with-aws-lambda-serverless-framework-and-go-616bd7ff11c9

aws lambda使用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值