原文:Fast growing architectures with serverless and .NET Core

作者:Samuele Resca

Serverless 技术为开发人员提供了一种快速而独立的方式将实现投入生产。这种技术在企业的技术栈中日益流行,自 2017 年以来,它一直是 ThoughtWorks 技术雷达的实验级别的技术[译注:技术雷达是 ThoughtWorks 每半年发布的前沿技术解析]。

本篇文章的第一部分介绍了有关 Serverless 计算的基本概念。第二部分展示了如何构建 .NET Core 的 Lambda 函数,其中使用了 AWS 的 Serverless 框架。

Serverless 计算的好处

Serverless 技术是 FaaS(功能即服务)技术体系的一部分。随着云计算的采用,这些技术变得越来越受欢迎。如今,serverless 实现被提升为云计算提供商的首选技术,无论是私有云还是公有云。

此外,典型的软件服务和系统会通过在内存中保留大量数据并在复杂数据源中写入成批数据来完成操作。

然而一般而言,像 Serverless 一样的 FaaS 技术旨在通过尽可能快地处理许多小请求和事件,来使我们的系统保持快速响应。Serverless 组件通常与运行它们的云服务商所提供的事件紧密耦合:一个通知、一个队列调度的事件或者一个来自 API 网关的请求,都被视为此组件中包含的一小部分计算的触发器。这也就是云服务商的定价系统基于请求数而不是基于计算时间的主要原因。

再者,serverless 组件通常在执行时间上有一些限制。与每种技术一样,serverless 并不适合每一个解决方案和系统。但是事实上,它确实简化了软件工程师的一些工作,lambda 部署周期通常很快,开发人员只需要做少量工作就可以快速将新功能投入生产。此外,使用 serverless 技术构建组件意味着开发人员无需担心扩展问题或故障,让云提供商去关心这些问题吧。

最后,我们还应该知道 serverless 函数是无状态的。因此,基于此技术构建的每个系统都更加模块化和松耦合。

Serverless 的痛点

但是这种能力和敏捷性却不是没有代价的。首先,serverless 函数是在云上执行的,它们通常由与云提供商紧密耦合的事件触发,因此调试它们并不容易。这就是为什么要使它的作用域保持尽可能小,并且始终将函数的核心逻辑与外部组件和事件分隔开的原因。此外,用单元测试和集成测试覆盖 serverless 代码非常重要。

其次,就像微服务架构一样,它具有大量的服务,但是关注的范围很小,因此很难对 serverless 的组件进行监控,某些问题也很难检测。总之,很难对不同的 serverless 组件之间的体系结构和依赖性有一个全面的认识。因此,云提服务商和第三方公司都在提供监控和系统分析功能的一体式工具上投入了大量资金。

体验一下 serverless 计算

现如今,根据业务需求快速进化的架构以往任何时候都更为重要。数据驱动的体验是这个过程的一部分。此外,在发布新功能之前,我们应该实现MVP(译注:最小可行化产品)并在部分客户群上测试它。如果实验结果是肯定的,则值得在MVP上进行投资,以将其转化为我们产品的功能。

是的,serverless 计算提供了这样一种方法,可以在不考虑基础设施的情况下快速进化我们的架构。Serverless 轻量级开销提供了一种实现一次性 MVP 的方法,用于试验新功能和新特性。此外,它们还可以很容易地启动和关闭。

使用 .NET Core 来实现 AWS Lambda 函数

这一节将介绍使用 .NET Core 的一些 AWS Lambdas 的简单实现。该例子涉及三个关键技术:

  • AWS 是承载我们 serverless 功能的云服务商;
  • serverless 框架,它是将 Lambdas 放入 AWS 的非常有用的工具。作为一个通用的框架,它兼容所有主要的云服务商;
  • .NET Core 是微软提供的开源的、跨平台的框架;

我们将要讨论的示例也放在了 GitHub 上,URL 如下: serverless/examples/aws-dotnet-rest-api-with-dynamodb。该示例是 serverless 框架提供的一些模板项目的一部分。

AWS Lambda 项目遵循以下功能架构:

总结一下,该功能实现了对数据的一些读取/写入操作。客户端通过API网关发出HTTP请求,lambda 项目定义了三个函数:GetItem、InsertItem 和 UpdateItem。它们都对 DynamoDB 表进行操作。

项目结构

我们将要实现的解决方案具有以下项目结构:

  • src/DotNetServerless.Application 该项目包含了由 Serverless 执行的核心逻辑;
  • src/DotNetServerless.Lambda 该项目包含了 Serverless 函数的入口点以及所有与 AWS 紧密耦合的组件;
  • tests/DotNetServerless.Tests 该项目包含了 Serverless 功能的单元测试和集成测试;

领域项目

让我们从 application 层开始分析。项目的核心实体是 Item 类,它表示 DynamoDB(译注:AWS的一种数据库) 表中存储的实体:

using Amazon.DynamoDBv2.DataModel;

namespace DotNetServerless.Application.Entity
{
public class Item
{
[DynamoDBHashKey]
public string Id { get; set; }
[DynamoDBRangeKey]
public string Code { get; set; }
[DynamoDBProperty]
public string Description { get; set; }
[DynamoDBProperty]
public bool IsChecked { get; set; }
}
}

实体的字段使用了一些特性进行修饰,以便使用 DynamoDb 存储模型映射它们。Item 实体被 IItemsRepository 接口引用,该接口定义用于存储数据的操作:

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.DocumentModel;
using DotNetServerless.Application.Entities;
using DotNetServerless.Application.Infrastructure.Configs; namespace DotNetServerless.Application.Infrastructure.Repositories
{
public interface IItemRepository
{
Task<IEnumerable<T>> GetById<T>(string id, CancellationToken cancellationToken); Task Save(Item item, CancellationToken cancellationToken);
} public class ItemDynamoRepository : IItemRepository
{
private readonly AmazonDynamoDBClient _client;
private readonly DynamoDBOperationConfig _configuration; public ItemDynamoRepository(DynamoDbConfiguration configuration,
IAwsClientFactory<AmazonDynamoDBClient> clientFactory)
{
_client = clientFactory.GetAwsClient();
_configuration = new DynamoDBOperationConfig
{
OverrideTableName = configuration.TableName,
SkipVersionCheck = true
};
} public async Task Save(Item item, CancellationToken cancellationToken)
{
using (var context = new DynamoDBContext(_client))
{
await context.SaveAsync(item, _configuration, cancellationToken);
}
} public async Task<IEnumerable<T>> GetById<T>(string id, CancellationToken cancellationToken)
{
var resultList = new List<T>();
using (var context = new DynamoDBContext(_client))
{
var scanCondition = new ScanCondition(nameof(Item.Id), ScanOperator.Equal, id);
var search = context.ScanAsync<T>(new[] {scanCondition}, _configuration); while (!search.IsDone)
{
var entities = await search.GetNextSetAsync(cancellationToken);
resultList.AddRange(entities);
}
} return resultList;
}
}
}

IItemRepository 的实现定义了两个基本操作:

  • Save,允许调用者插入和更新实体;
  • GetById,根据 ID 返回对象;

最后,DotNetServerless.Application 的顶层是 Handler 部分。并且

,整个 application 项目都基于中介模式,以保证 AWS 函数和核心逻辑之间的松散耦合。让我们以创建项目处理程序的定义为例:

using System;
using System.Threading;
using System.Threading.Tasks;
using DotNetServerless.Application.Entities;
using DotNetServerless.Application.Infrastructure.Repositories;
using DotNetServerless.Application.Requests;
using MediatR; namespace DotNetServerless.Application.Handlers
{
public class CreateItemHandler : IRequestHandler<CreateItemRequest, Item>
{
private readonly IItemRepository _itemRepository; public CreateItemHandler(IItemRepository itemRepository)
{
_itemRepository = itemRepository;
} public async Task<Item> Handle(CreateItemRequest request, CancellationToken cancellationToken)
{
var item = request.Map();
item.Id = Guid.NewGuid().ToString(); await _itemRepository.Save(item, cancellationToken); return item;
}
}
}

如您所见,代码非常简单。CreateItemHandler 实现了 IRequestHandler,它使用内置的依赖注入来解析 IItemRepository 接口。处理程序的 Handler 方法仅将传入的请求与Item实体映射,并调用IItemRepository接口提供的Save方法。

函数项目

函数项目包含 lambda 功能的入口点。它定义了三个函数类,它们表示 AWS 的 lambda:CreateItemFunction, GetItemFunction 和 UpdateItemFunction; 稍后我们将看到,每个函数都将使用 API 网关的特定路由进行映射。

让我们以 CreateItem 函数为例,对函数定义进行一些深入探讨:

using System;
using System.Threading.Tasks;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using DotNetServerless.Application.Requests;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json; namespace DotNetServerless.Lambda.Functions
{
public class CreateItemFunction
{
private readonly IServiceProvider _serviceProvider; public CreateItemFunction() : this(Startup
.BuildContainer()
.BuildServiceProvider())
{
} public CreateItemFunction(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
} [LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
public async Task<APIGatewayProxyResponse> Run(APIGatewayProxyRequest request)
{
var requestModel = JsonConvert.DeserializeObject<CreateItemRequest>(request.Body);
var mediator = _serviceProvider.GetService<IMediator>(); var result = await mediator.Send(requestModel); return new APIGatewayProxyResponse { StatusCode = 201, Body = JsonConvert.SerializeObject(result)};
}
}
}

上面提到的代码定义了函数的入口点。首先,它声明一个构造函数,并使用Startup类公开的BuildContainer和BuildServiceProvider方法。稍后我们将看到,这些方法是为初始化依赖项注入容器而提供的。CreateItem 函数的 Run 方法使用 Lambda 序列器属性进行修饰,这意味着它是函数的入口点。此外,运行函数使用 APIGatewayProxyRequest 请求和 APIGatewayProxyReposne 作为 lambda 计算的输入和输出。

依赖注入

该项目使用了 .NET Core 内置的依赖注入。Startup 类定义了 BuildContainer 静态方法,该方法返回一个新的 ServiceCollection,其中包含实体之间的依赖关系映射:

using System.IO;
using DotNetServerless.Application.Infrastructure;
using DotNetServerless.Application.Infrastructure.Configs;
using DotNetServerless.Application.Infrastructure.Repositories;
using DotNetServerless.Lambda.Extensions;
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; namespace DotNetServerless.Lambda
{
public class Startup
{
public static IServiceCollection BuildContainer()
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddEnvironmentVariables()
.Build(); return ConfigureServices(configuration);
} private static IServiceCollection ConfigureServices(IConfigurationRoot configurationRoot)
{
var services = new ServiceCollection(); services
.AddMediatR()
.AddTransient(typeof(IAwsClientFactory<>), typeof(AwsClientFactory<>))
.AddTransient<IItemRepository, ItemDynamoRepository>()
.BindAndConfigure(configurationRoot.GetSection("DynamoDbConfiguration"), new DynamoDbConfiguration())
.BindAndConfigure(configurationRoot.GetSection("AwsBasicConfiguration"), new AwsBasicConfiguration()); return services;
}
}
}

Startup使用ConfigureServices初始化新的ServiceCollection并与其一起解决依赖关系。此外,它还使用 BindAndConfigure 方法创建一些配置对象。BuildContainer方法将由函数调用,以解决依赖项。

测试我们的代码

如前所述,测试一下我们的代码,对于持续集成和交付是非常重要的,尤其是在lambda项目中。在这种情况下,测试将覆盖 IMediator 接口和处理程序之间的集成。此外,它们还覆盖了依赖项注入部分。让我们看看 CreateItemFunctionTests 的实现:

using System.Threading;
using System.Threading.Tasks;
using Amazon.Lambda.APIGatewayEvents;
using DotNetServerless.Application.Entities;
using DotNetServerless.Application.Infrastructure.Repositories;
using DotNetServerless.Application.Requests;
using DotNetServerless.Lambda;
using DotNetServerless.Lambda.Functions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Moq;
using Newtonsoft.Json;
using Xunit; namespace DotNetServerless.Tests.Functions
{
public class CreateItemFunctionTests
{
public CreateItemFunctionTests()
{
_mockRepository = new Mock<IItemRepository>();
_mockRepository.Setup(_ => _.Save(It.IsAny<Item>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask); var serviceCollection = Startup.BuildContainer(); serviceCollection.Replace(new ServiceDescriptor(typeof(IItemRepository), _ => _mockRepository.Object,
ServiceLifetime.Transient)); _sut = new CreateItemFunction(serviceCollection.BuildServiceProvider());
} private readonly CreateItemFunction _sut;
private readonly Mock<IItemRepository> _mockRepository; [Fact]
public async Task run_should_trigger_mediator_handler_and_repository()
{
await _sut.Run(new APIGatewayProxyRequest {Body = JsonConvert.SerializeObject(new CreateItemRequest())});
_mockRepository.Verify(_ => _.Save(It.IsAny<Item>(), It.IsAny<CancellationToken>()), Times.Once);
} [Theory]
[InlineData(201)]
public async Task run_should_return_201_created(int statusCode)
{
var result = await _sut.Run(new APIGatewayProxyRequest {Body = JsonConvert.SerializeObject(new CreateItemRequest())});
Assert.Equal(result.StatusCode, statusCode);
}
}
}

如您所见,上述代码执行了我们的函数,并且对已注入的依赖项执行一些验证,并验证 IItemRepository 公开的 Save 方法是否被调用。因为一些原因,测试类并没有覆盖 DynamoDb 的特性。此外,当我们将复杂的实体和操作结合在一起时,可以使用 Docker 容器通过一些集成测试来覆盖数据库部分。对了,提到 .NET Core 和 AWS 的话题,.NET AWS 团队有一个很好的工具来改进 lambda 的测试:LambdaTestTool

部署项目

让我们来看看如何将项目导入AWS。为此,我们将使用 serverless 框架。该框架的定义是:

serverless 框架是一个 CLI 工具,允许用户构建和部署自动缩放、按执行付费、事件驱动的函数。

为了把 serverless 添加我们的项目,我们应该执行以下命令:

npm install serverless --save-dev

定义基础架构

默认情况下,基础架构的定义将放在 serverless.yml 文件中。该文件看起来像这样:

service: ${file(env.configs.yml):feature}

frameworkVersion: ">=1.6.0 <2.1.0"

provider:
name: aws
stackName: ${file(env.configs.yml):feature}-${file(env.configs.yml):environment}
runtime: dotnetcore2.1
region: ${file(env.configs.yml):region}
accountId: ${file(env.configs.yml):accountId}
environment:
DynamoDbConfiguration__TableName: ${file(env.configs.yml):dynamoTable} iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:*
Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DynamoDbConfiguration__TableName}" package:
artifact: bin/release/netcoreapp2.1/deploy-package.zip functions:
create:
handler: DotNetServerless.Lambda::DotNetServerless.Lambda.Functions.CreateItemFunction::Run
events:
- http:
path: items
method: post
cors: true get:
handler: DotNetServerless.Lambda::DotNetServerless.Lambda.Functions.GetItemFunction::Run
events:
- http:
path: items/{id}
method: get
cors: true update:
handler: DotNetServerless.Lambda::DotNetServerless.Lambda.Functions.UpdateItemFunction::Run
events:
- http:
path: items
method: put
cors: true resources:
Resources:
ItemsDynamoDbTable:
Type: 'AWS::DynamoDB::Table'
DeletionPolicy: Retain
Properties:
AttributeDefinitions:
- AttributeName: Id
AttributeType: S
- AttributeName: Code
AttributeType: S
KeySchema:
- AttributeName: Id
KeyType: HASH
- AttributeName: Code
KeyType: RANGE
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: ${self:provider.environment.DynamoDbConfiguration__TableName}

以上代码使用 AWS 的 cloud formation 对基础架构进行一些操作。provider 节点定义了有关 lambda 的某些信息,例如堆栈名称、运行时以及有关AWS账户的一些信息。此外,它还描述了 lambda 的角色和授权,例如,应该允许 lambda 对 DynamoDb 表执行操作。 functions 节点定义了不同的 lambda 函数,并将其与特定的 HTTP 路径进行映射。最后,resources 节点用于设置 DynamoDB 表模式。

配置文件

serverless.yml 定义通常与另一个 YAML 文件结合使用,该文件仅定义与环境相关的配置。例如,DynamoDbConfiguration__TableName 节点就是这种情况,该节点使用以下语法从另一个 YAML 文件获取信息:${file(env.configs.yml):dynamoTable}。以下代码段显示了 env.config.yml 文件的一个示例:

feature: <feature_name>
version: 1.0.0.0
region: <aws_region>
environment: <environment>
accountId: <aws_account_id>
dynamoTable: <dynamo_table_name>

最后的想法

这篇文章涵盖了一些关于 serverless 计算的理论主题,以及 .Net Core 实现 lambda 函数的例子。着重讲解了如何使用 serverless 计算来快速推进我们的架构。此外,勇于尝试是一个不断发展的产品很关键的一方面,它对于快速适应业务的变化是很重要的。

最后,您可以在以下存储库中找到一些 serverless 的 lambda 示例。

serverless/examples/aws-dotnet-rest-api-with-dynamodb

[翻译] 使用 Serverless 和 .NET Core 构建飞速发展的架构的更多相关文章

  1. 棒!使用.NET Core构建3D游戏引擎

    原文地址:https://mellinoe.wordpress.com/2017/01/18/net-core-game-engine/ 作者:ERIC MELLINO 翻译:杨晓东(Savorboa ...

  2. [翻译] 初看 ASP.NET Core 3.0 即将到来的变化

    [翻译] 初看 ASP.NET Core 3.0 即将到来的变化 原文: A first look at changes coming in ASP.NET Core 3.0 在我们努力完成下一个 m ...

  3. 使用ASP.NET Core构建RESTful API的技术指南

    译者荐语:利用周末的时间,本人拜读了长沙.NET技术社区翻译的技术标准<微软RESTFul API指南>,打算按照步骤写一个完整的教程,后来无意中看到了这篇文章,与我要写的主题有不少相似之 ...

  4. 在Visual Studio 2017中使用Asp.Net Core构建Angular4应用程序

    前言 Visual Studio 2017已经发布了很久了.做为集成了Asp.Net Core 1.1的地表最强IDE工具,越来越受.NET系的开发人员追捧. 随着Google Angular4的发布 ...

  5. ASP.NET Core 入门教程 1、使用ASP.NET Core 构建第一个Web应用

    一.前言 1.本文主要内容 Visual Studio Code 开发环境配置 使用 ASP.NET Core 构建Web应用 ASP.NET Core Web 应用启动类说明 ASP.NET Cor ...

  6. [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置

    [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置 原文: USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET COR ...

  7. 【Asp.Net Core】在Visual Studio 2017中使用Asp.Net Core构建Angular4应用程序

    前言 Visual Studio 2017已经发布了很久了.做为集成了Asp.Net Core 1.1的地表最强IDE工具,越来越受.NET系的开发人员追捧. 随着Google Angular4的发布 ...

  8. Net Core构建Angular4应用程序

    在Visual Studio 2017中使用Asp.Net Core构建Angular4应用程序   前言 Visual Studio 2017已经发布了很久了.做为集成了Asp.Net Core 1 ...

  9. 用ASP.NET Core构建可检测的高可用服务--学习笔记

    摘要 随着现代化微服务架构的发展,系统故障的定位与快速恢复面临着诸多挑战,构建可检测的服务,帮助线上保障团队时刻掌控应用的运行状况越来越重要.本次分享会讲解如何让 ASP .NET Core 应用与现 ...

随机推荐

  1. 在虚拟机上的关于Apache(阿帕奇)(5)基于端口访问网站

    这篇随笔是基于端口访问网站,和前面两篇文章基于ip和基于域名一起练习效果更好 接下来分别创建三个网站数据目录 输入命令: mkdir  -p  /home/wwwroot/{8001,8002,800 ...

  2. Web for pentester_writeup之Commands injection篇

    Web for pentester_writeup之Commands injection篇 Commands injection(命令行注入) 代码注入和命令行注入有什么区别呢,代码注入涉及比较广泛, ...

  3. [考试反思]1027csp-s模拟测试90:自我

    其实这套题很好. 但是这次,在T1爆炸的同时,T2和T3并没有出现能弥补的表现. 在AK仍然存在的同时,我居然连一个AC都没有. 所以最后就是一无是处的一场. 考试结束前估分:100+100+30=2 ...

  4. [考试反思]1011csp-s模拟测试69:无常

    承蒙大脸skyh的毒奶,加之以被kx和Parisb以及板儿逼剥夺了一中午的睡眠(其实还有半个晚上)RP守恒终于失效了,连续两场没考好 RP也是不够了,竟然考原题,而且还不换题,连样例都一模一样只不过加 ...

  5. NOIP模拟测试12

    T1 斐波那契 一道找规律题,被我做成了贼难的题. 观察图片可知x=f[i-1]+j.(j为x的父亲)且j<=f[i-1],然后就二分找父亲没了. #include<bits/stdc++ ...

  6. 【IOS开发—视图控制器】

    一.UIViewController 视图控制器是UIViewController类或者其子类对象,每个视图控制器都负责管理一个视图层次结构.在UIViewController中有一个重要的UIVie ...

  7. python——字符串操作函数

    字符串 join() map() split() rsplit() splitlines() partiton() rpartition() upper() lower() swapcase() ca ...

  8. nginx跨域解决方案

    nginx跨域解决方案Access to Font at 'http://47.104.86.187/yinjiatoupiao2/iconfont/iconfont.woff' from origi ...

  9. Redis必备面试题《基础篇》

    Date:2019-11-12 读前思考: 面试官会问什么样的问题? 所问的问题背后真实的套路是什么? 喜欢问Redis哪些问题? 如何顺畅回答面试问的问题?吊打面试官. 1.什么是Redis? Re ...

  10. IDEA Debug 无法进入断点的解决方法

    文章来源: https://studyidea.cn/idea_breakpoint_not_use 前言 某个多模块项目中使用多个版本的 Spring,如 Spring 4,Spring 5,在使用 ...