【One by one系列】微服务:一步步开发与调试容器化的 .NET 应用程序
最近一直在研究微服务体系架构。微服务概念一直很火,但是作为一个初学者往往迷失在高深理论与纷繁多样的技术,而失去了方向,慢慢的,^_^,还没开始就已经放弃了。所以还是不得不夸一夸微软一切以开发者为中心的价值观:好文档,好工具。
1.微服务学习线路
1.1 开卷有益
首先我们从微软的微服务架构的白皮书(中文版,英文版)入手,开卷有益。这是一本无关平台,学习微服务,理解微服务的好书,虽然技术是.NET,但是书中更多的内容是介绍的微服务思想,理论,最佳实践,其他平台同样适用,适用于我们整体把控微服务架构体系中的核心问题:
- 微服务之间的通信
- 网关
- 身份认证与授权
- 数据库服务
- DDD
- CQRS
虽然不一定能够全部理解书中的所有理论概念,但是总能给到一些启发,开拓思维。
1.2 实际项目
然后就是微软架构师利用.net core技术,基于docker容器技术,实现的适用于容器化 .NET 应用程序的体系结构微服务架构demo项目-eShopOnContainer,这个项目在上面的白皮书中也有介绍。
下面大概介绍一下这个项目的架构,虽然是一个demo,其中部分内容具有一定的局限性,并不适合生产环境,但是这并不妨碍我们去理解微服务整体的体系架构。
eShopOnContainer
是一个在客户端、服务端同时可以跨平台的项目。这都得益于 .NET Core能够跑在不同系统的容器上,windows或者linux。项目还有Xamarin
移动APP,ASP.NET Core Web MVC
和一个SPA
。
eShopOnContainer
的架构,是一种面向微服务体系架构的实现。这些微服务都是可以自我治理的:
- 每个微服务有属于自己的数据库。
- 每个微服务都有简单的CRUD方法、和精细的DDD/CQRS模式方法。
- 客户端和微服务通过HTTP协议进行数据交换
- 微服务之间通过异步消息进行通信
- 消息队列可以通过RabbitMQ或者AzureAzure Service Bus去传递集成事件。
事件总线
项目中有一个简化的事件抽象总线,来处理集成事件。这个抽象事件总线在项目中有两个实现:
- RabbitMQ
- Azure Service Bus
这里对于生产级别的解决方案,微软建议使用更加健壮的组件。
API 网关
整体架构中还包括了API网关和BFF模式的实现:
- 发布简化的API
- 在外部消费者和内部微服务之间增加安全措施,以此对外隐藏并保护内部微服务
这些API网关是通过Envoy实现的,我顺带翻阅了下官网,使用Envoy的公司还比较多,基本都是耳熟能详,Uber
,ebay
,airbnb
,amazon
,Google
,IBM
,Microsoft
,还有腾讯等等。在架构中,Envoy实现的网关,只执行向内部微服务和自定义聚合器的请求转发,从而为客户端提供单一基本的URL.其实还可以通过Envoy实现:
- 在gRPC于HTTP/REST之间的自动转换
- 身份验证
- 授权管理
- 缓存支持
项目中,除了API网关之外,还提供了一组“自定义聚合器”。这些聚合器为某些操作的客户端提供了一个简单的API。
- 移动购物:购物操作的聚合,供XamarinAPP调用
- PC购物:购物操作的聚合,供Web客户端调用,(mvc与spa)
之前eShop使用的是Ocelot实现网关的。对于Ocelot,官方给的说法是欲抑先扬:Ocelot很好,很优秀,也是.net core 优秀的开源项目,也支持许多特性,它可以作为.net core项目网关实现候选组件。但是,Ocelot缺乏对gRPC的支持,所以在最新的项目(这个eShopOnContainer项目一直在迭代更新与维护,从众多分支就可以看出)中就换为Envoy提供网关服务。
自定义聚合器
这个主要用于公开一个具有涉及内部各个微服务之间的复杂方法的HTTP/JSON API,每个自定义聚合器的方法都能调用1个或者多个内部微服务,根据逻辑聚合多个结果并提供给客户端。从聚合器到微服务的调用的都是使用gRPC
gRPC
在众多微服务之间,大多数微服务都是通过事件总线和发布者/观察者模式进行异步通信。但是,自定义的聚合器和内部微服务之间的同步通信是用gRPC实现的。gRPC是一种基于RPC的协议,具有良好的性能,带宽占比也低,是内部微服务通信协议中的最佳候选协议。项目中使用了4个网关实现BFF,目前它们是通过Envoy
来实现的。每个BFF为其客户端提供一个唯一的端点,然后将调用转发到特定的微服务或自定义聚合器。
- 1.客户端通过Envoy代理暴露的URL调用BFF.
- 2.通过请求数据,Envoy转发请求至内部的微服务(简单的增删改查),或者复杂的聚合器(复杂逻辑),这对客户端都是透明的。
当调用直接从Envoy转发到内部微服务时,它是使用HTTP/JSON执行的。也就是说,现在内部微服务公开了一组混合的方法:
- 一些走gRPC(由聚集器调用)
- 一些走HTTP/JSON中(由Envoy调用)。
这里微软官方进行了展望"这可能会在未来发生变化”,即所有的微服务方法都可以使用gRPC,如果需要,Envoy可以在gRPC和HTTP/JSON之间自动转换。
微服务内部架构模式
不同类型的微服务可能采用不同的内部架构模式和方法,这取决于微服务的用途。
数据库服务
- 4个
SQL Server
,部署在同一个容器内
主要是降低内存的需求,生产部署不建议这样做,应该使用High-availability的解决方案。
- 1个
Redis
实例,单独一个容器 - 1个
MongoDb
实例,单独一个容器
Redis
和MongoDb
都是单独的容器,作为两个广泛使用的NO-SQL数据库的示例。
其他
项目中除了,上面的架构内容,还有DDD领域驱动开发(Domain Drive Design),CQRS命令与查询职责分离(Command and Query Responsibility Segregation)的实践,日志,健康检查等内容。所以涵盖的范围蛮广,个人觉得非常值得研习。
2.容器化 .NET 应用程序的开发调试
铺垫了这么多,终于要进入本篇文章的主题,对于我们的微服务化的应用,我们可以说,我们的应用都是跑在容器上的,或者说我们所有的微服务都跑在容器上(当然容器指的就是docker,docker容器几乎成为了行业标准)。我们如何进行开发呢,这里再夸一下微软,在白皮书中有:
- 编码:创建应用
- 为应用创建Dockerfile
- 创建自定义docker镜像
- 定义docker-compose.yaml
- 构建并运行docker应用
- 测试docker应用(微服务)
- 推送代码提交或者继续开发
下面就将开始把我的一个应用以容器的方式跑起来,根据上面的工作流进行实践与书写
项目概述
这本身是一个公司推送集中平台,接受公司多个产品线的推送请求,然后通过阿里进行移动推送,然后每一次推送都有后台记录,进行存储。由于我接下来实践的Docker应用开发的工作流,所以实际只有一个webapi项目,也并不打算去拆分,这对我们实践意义也不太大。
我们的目标
开发环境拆分为多个docker容器,且调试时能够正常运行。
2.1 安装docker-desktop
docker引擎需要运行在linux上,那么win10就需要装装虚拟机:hyper-v
,实际上docker是跑在这个虚拟机上,windows上的docker适用于测试和开发。生产环境还是linux哈。
即便是win10也请注意下版本:Docker Desktop requires Windows 10 Pro/Enterprise (15063+) or Windows 10 Home (19018+).
- 下载:https://www.docker.com/get-started
- 安装:傻瓜式点下一步
- 设置
Resources ADVANCED
选择虚拟机cpu颗数,内存大小
Resources-FILE SHARING
docker容器能够通过volume挂载宿主机操作系统(linux)的文件目录或目录,宿主操作系统在Windows的Docker Desktop中,就是指是 Hyper-v 里的 Linux 系统。但是,如果只能从hyper-v中的linux系统中进行挂载,显然不足以达到我们的需求,最方便的方式肯定是直接从Hyper-v的宿主windows里挂载文件咯。(有点绕,多理解下,windows>hyper-v>docker) 最终效果:**Docker 容器直接挂载主机系统的目录,我们可以先将目录挂载到虚拟 Linux 系统上,,再利用 Docker 挂载到容器之中。**这个过程被集成在了 Docker Desktop 系列软件中,我们不需要人工进行任何操作,整个过程已经实现了自动化。这就是FILE SHARING选项的意义。如果还不好理解,往下看。
Docker Engine
配置阿里云镜像加速器,使用加速器可以提升获取Docker官方镜像的速度,亲测还是有用,但是,2018 年五月之后,微软将后续发布的所有 docker image
都推送到了 自家的MCR (Miscrosoft Container Registry)
,但在中国大陆,由于众所周知的原因,它的速度实在是令人发指。后续有解决方案,文章会讲到。
2.2 编码-创建我们的应用
由于项目是现成的,那么这一步我们可以省略,这个跟您开发一个webapi项目没有任何区别,原来怎么做的,现在还是怎么做。我只说一个关键点,那就是数据初始化,我们的推送数据需要存入数据库中,你也可以等mysql容器启动后,再去初始化容器中的mysql数据库,但是我们能用代码一步到位:
在program.cs
:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace AliMobilePush.Webapi
{
public class Program
{
public static void Main(string[] args)
{
//CreateHostBuilder(args).Build().Run();
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
//...CreateHostBuilder
}
}
SeedData.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace AliMobilePush.Infrastructure
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new PushContext(
serviceProvider.GetRequiredService<
DbContextOptions<PushContext>>()))
{
context.Database.EnsureCreated();
context.SaveChanges();
}
}
}
}
2.3 为应用创建Dockerfile
无论是通过Visual Studio
自动部署,还是通过Docker CLI
。都需要为应用创建Dockerfile
。一般情况,Dockerfile是放到应用或者服务的根文件夹下。这里有三种方式创建dockerfile。
- 创建项目时,勾选Enable Docker Support项
- 已经建立好的webapi项目,右键 Solution Explorer 然后选择 Add > Docker Support
- 手写dockerfile,不是我们本文的重点,请参考另外一篇文章【One by one系列】一步步学习docker(三)——实战部署dotnetcore
不管哪种方式,一定会或者要在项目根目录下增加Dockerfile
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["Webapi/AliMobilePush.Webapi.csproj", "Webapi/"]
COPY ["Infrastructure/AliMobilePush.Infrastructure/AliMobilePush.Infrastructure.csproj", "Infrastructure/AliMobilePush.Infrastructure/"]
COPY ["Domain/AliMobilePush.Domain/AliMobilePush.Domain.csproj", "Domain/AliMobilePush.Domain/"]
COPY ["Application/AliMobilePush.Application/AliMobilePush.Application.csproj", "Application/AliMobilePush.Application/"]
RUN dotnet restore "Webapi/AliMobilePush.Webapi.csproj"
COPY . .
WORKDIR "/src/Webapi"
RUN dotnet build "AliMobilePush.Webapi.csproj" -o /app/build
FROM build AS publish
RUN dotnet publish "AliMobilePush.Webapi.csproj" -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "AliMobilePush.Webapi.dll"]
上面dockerfile分为了base
,build
,publish
三个阶段的多阶段构建.
1.base
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
Debian10的asp.net core 运行时image开头,并创建公开端口80,443的中间image base
2.build
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["Webapi/AliMobilePush.Webapi.csproj", "Webapi/"]
COPY ["Infrastructure/AliMobilePush.Infrastructure/AliMobilePush.Infrastructure.csproj", "Infrastructure/AliMobilePush.Infrastructure/"]
COPY ["Domain/AliMobilePush.Domain/AliMobilePush.Domain.csproj", "Domain/AliMobilePush.Domain/"]
COPY ["Application/AliMobilePush.Application/AliMobilePush.Application.csproj", "Application/AliMobilePush.Application/"]
RUN dotnet restore "Webapi/AliMobilePush.Webapi.csproj"
COPY . .
WORKDIR "/src/Webapi"
RUN dotnet build "AliMobilePush.Webapi.csproj" -o /app/build
build阶段是从编译工具—sdk镜像开始,而不是aspnet
,那是因为只有sdk
镜像用后构建编译工具,所以sdk镜像也比aspnet镜像大。先还原restore
,再publish
3.publish
FROM build AS publish
RUN dotnet publish "AliMobilePush.Webapi.csproj" -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "AliMobilePush.Webapi.dll"]
最后阶段再次从base
开始,包括COPY --from=publish /app/publish .
将发布的输出复制到最终镜像中。由于无需包含sdk
镜像中的构建编译工具,因此此过程可以使最终镜像小得多。
官方最佳实践,多阶段构建镜像,这样生成过程更高效,并使容器更小。官方文档,整个多阶段构建 可以让后一个阶段构建可以使用前一个阶段构建的产物,形成一条构建阶段的chain;最终结果仅产生一个image,避免产生冗余的多个临时images或临时容器对象,这正是我们所需要的:我们只需要个结果。
2.4 创建自定义docker镜像
一个服务对应一个镜像,需要知道,在Visual Studio
的强大功能下,docker镜像是自动创建的。
作为开发者,只要功能没完成,或者代码不提交到版本控制。都是需要在本地部署和测试的。那么这就意味你需要在本地的docker主机上创建docker镜像,部署docker容器,并在这些容器上去运行,测试,调试。使用 Visual Studio
创建具有 Docker 支持的项目时,不会显示的创建映像。 而是在按下 F5(或 Ctrl-F5)运行docker 化的应用程序或服务时创建映像 。 Visual Studio
会自动执行这个操作,开发人员不会看到该过程,但务必要了解其原理。
2.5 定义docker-compose.yaml
定义服务,创建多容器应用,主要是可以在docker-compose.yml
中定义一系列的服务。通过部署命令将其部署为组合应用程序。 它还配置其依赖项关系和运行时配置。在主解决方案文件夹或根解决方案文件夹中创建该docker-compose.yml
文件,docker-compose.yml
是可以拆分成多个docker-compose
文件。然后根据不同的环境去覆盖值。添加docker-compose.yml文件也有两种方式
- 已经建立好的webapi项目,右键 Solution Explorer 然后选择 Add>Container Orchestrator Support
- 手写
docker-compose.yml
,这个后续博文会详细介绍,亦不是本篇的重点。所以下面重点介绍第一种方式:
第一次作Solution Explorer > Add>Container Orchestrator Support操作
- 会在api项目下增加
Dockerfile
,如果原本没有的话 - 会在解决方案目录增加
docker-compose.dcproj
docker-compose.override.yml
docker-compose.yml
.dockerignore
docker-compose.yml
version: '3.4'
services:
webapi:
build:
context: .
dockerfile: Webapi/Dockerfile
networks:
- asp-net
depends_on:
- "cachedata"
- "sqldata"
cachedata:
image: redis
networks:
- asp-net
sqldata:
image: mysql
networks:
- asp-net
networks:
asp-net:
driver: bridge
docker-compose.override.yml
version: '3.4'
services:
webapi:
environment:
- ASPNETCORE_ENVIRONMENT=Development
ports:
- "5000:5000"
- "5001:5001"
volumes:
- ./docker/log/alipush.log:/app/alipush.log
cachedata:
ports:
- "6379:6379"
volumes:
- ./docker/data/redis:/data
sqldata:
ports:
- "3307:3306"
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: 123456
volumes:
- ./docker/data/mysql:/var/lib/mysql
注意:修改数据库连接配置
这里需要注意,所有有用到镜像间的通信的地方,我们都需要使用镜像名进行指代,例如我们需要修改程序的数据库访问字符串的服务器地址
Mysql:
"ConnectionStrings": {
"PushContext": "Persist Security Info=False;database=pushcenter;server=sqldata;Connect Timeout=30;user id=pushcenter; pwd=123456"
},
Redis:
"Redis": {
"ConnectionString": "cachedata,defaultDatabase=1",
"Instance": "push_request_",
"Timeout": 1
},
2.6 构建并运行docker应用
如果是单容器应用,直接跑。
如果多服务容器应用,就有两个选择
docker-compose up
Visual Studio
2.6.1 单容器应用
使用docker命令,docker run
即可
docker run -t -d -p 80:5000 cesardl/netcore-webapi-microservice-docker:first
2.6.2 运行多容器应用
使用docker-compose
命令,docker-compose up
使用 docker-compose up
和 docker run
命令(或在 Visual Studio
中运行和调试容器)足以在开发环境中测试容器。 但不应该将这种方法用于生产部署,在生产部署中应该以业务流程协调程序为目标,,比如K8S,或者docker swarm
。
在Visual Studio 中运行和调试容器
- 1.选择解决方案中选择
docker-compose
项目,Solution Explorer > Set as a Startup Project
F5
开始运行调试吧
可以在output-build窗口下观察:
实际上,是visual studio帮我们直接执行了docker-compose -f docker-compose.yml
的命令
然后紧接着,docker-compose就会
- 创建桥接网络
- 创建并启动redis,mysql容器:按照docker-compose.yml的依赖 depends_on项
- 创建并启动webapi容器
构建的过程中,win10会一直提示,文件是否共享,会一直不停的点share it.这时我们去观察下:
docker-desktop>Resources>FILE SHARING
没错,我们把这些主机(win10)文件夹挂载到hyper-v(虚拟机,docker宿主机),hyper-v又挂载到容器,实现主机文件夹与容器文件夹的映射。
再看下结果:镜像与容器
然后就可以打断点调试容器应用了。
如果你发现构建的镜像与容器有问题,想重新来过,vs大法提供了如下方法:
Solution>Clean Solution
再在output-build窗口下观察:
- 先kill服务
- 然后在删掉容器
- 最后删掉应用的镜像-不过实际没有删掉
应用容器倒是停了并且删除了,但是mysql,redis这些容器数据服务,仅仅只是停了。
**注意:**dockerfile里面的mcr.microsoft.com/dotnet/core/sdk:3.1-buster
镜像,下载巨慢,构建一次,一碗番茄煎蛋面都要做好了。
国内下载微软镜像慢的解决方案
https://github.com/newbe36524/Newbe.McrMirror
使用docker-mcr下载镜像
dotnet tool install newbe.mcrmirror -g
docker-mcr -i mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim
docker-mcr -i mcr.microsoft.com/dotnet/core/sdk:3.1-buster
把构建过程需要下载的镜像,先提前下下来吧。
2.7 测试
测试用例1 webapi-swagger
测试用例2 mysql能否访问,且通过ef生成了数据库
测试用例3 redis能否访问
测试用例4 文件挂载是否正常(举例一个即可)
2.8 推送代码提交或者继续开发
推送下班,避免996
或者继续开发
3.Visual Studio大法好
实际上,使用 Visual Studio
进行开发的工作流比使用编辑器或CLI 方法的工作流简单得多。 Visual Studio
隐藏或简化了 Docker 需要执行的与 Dockerfile 和 docker-compose.yml 文件相关的大部分步骤
- 自动生成Dockerfile,可编辑
- 自动生成docker-compose.yml,可编辑
- 自动执行docker-compose up,且可调试
- 可自动停止且并移除容器
微软以开发者为中心的价值观,为开发者省了不少事,Visual Studio不愧为宇宙第一的IDE。
参考链接
https://www.cnblogs.com/xianwang/p/12039922.html
https://zhuanlan.zhihu.com/p/147369525
http://www.imooc.com/article/259789
https://my.oschina.net/u/4285813/blog/3661653/print
https://github.com/dotnet-architecture/eShopOnContainers
https://docs.microsoft.com/zh-cn/dotnet/architecture/microservices/
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2020-06-21-microservice-dotnetcore/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。