【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的公司还比较多,基本都是耳熟能详,Uberebay,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实例,单独一个容器

RedisMongoDb都是单独的容器,作为两个广泛使用的NO-SQL数据库的示例。

其他

项目中除了,上面的架构内容,还有DDD领域驱动开发(Domain Drive Design),CQRS命令与查询职责分离(Command and Query Responsibility Segregation)的实践,日志,健康检查等内容。所以涵盖的范围蛮广,个人觉得非常值得研习。

2.容器化 .NET 应用程序的开发调试

铺垫了这么多,终于要进入本篇文章的主题,对于我们的微服务化的应用,我们可以说,我们的应用都是跑在容器上的,或者说我们所有的微服务都跑再容器上(当然容器指的就是docker,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+).

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

#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 updocker 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

把构建过程需要下载的镜像,先提前下下来吧。

测试

测试用例1 webapi-swagger

测试用例2 mysql能否访问,且通过ef生成了数据库

测试用例3 redis能否访问

测试用例4 文件挂载是否正常(举例一个即可)

2.7 推送代码提交或者继续开发

推送下班,避免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/

本文版权归作者所有,未经许可禁止转载,否则保留追究法律责任的权利,若有需要请联系287572291@qq.com

posted @ 2020-06-21 03:33  Garfield-加菲  阅读(508)  评论(2编辑  收藏  举报