ASP.NET Core 分布式架构的简易实现 (一)
ASP.NET Core 分布式架构的简易实现 (一)

ASP.NET Core 分布式架构的简易实现 (一)

分布式架构是一种可以轻松消解系统复杂度, 提升项目可维护性的架构方案. 在分布式系统中, 各模块独立运作, 互相不直接依赖. 但共同作为一个松散的整体支撑整个的业务. 当系统越来越大功能越来越多越来越复杂的时候, 就有必要考虑用分布式架构的思路来拆分功能了. 在这里我将分几篇来简单的讲解一下 ASP.NET Core 中如何实现分布式, 以及如何通过 CI (持续集成) 来解决由分布式架构设计所带来的维护难题. 本文基于 .NET 6.


建立服务


这里我们建立两个 WEB 服务, 一个 Service, 一个服务接口.
如果 WEB 服务绑定的不是 80 / 443 端口, 则需要在 Program.cs 中添加对 URL 跟端口的绑定.
app.Urls.Add("http://0.0.0.0:7002")

这里我们在 Test-WebApp1 跟 Test-WebApp2 各建立一个 Controller (建立时候应该默认会建立一个), 在里面各写一个测试方法:

//Test-WebApp1
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
  ......
  ......
  [HttpGet]
  public async Task<string> Get(string id) => $"Hello {id}";
}

//Test-WebApp2, 模板中自带的 Controller. 我们就拿这个做实验好了.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
  private static readonly string[] Summaries =
    new[] {"Freezing", "Bracing", "Chilly", "Cool",
    "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
  ......
  ......
  [HttpGet(Name = "GetWeatherForecast")]
  public IEnumerable<WeatherForecast> Get() =>
    Enumerable.Range(1, 5).Select(index => new WeatherForecast
      {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
      }).ToArray();
}

 

Ocelot 网关

其实这时候服务已经可以跑起来了. 但是这些服务各自为政, 每个服务都使用了不同的端口号或者并不公开外网端口. 前端必不可能因为后端分布式化就写 N 个 API 基址. 因此就需要 Ocelot 作为服务网关, 整合 API 接口, 并提供统一的权鉴服务.
这里我们新建一个 Test.Ocelot 的 WebAPI 项目. 并在 nuget 中搜索并添加 “Ocelot”.

Ocelot 包是 Ocelot 的本体, 添加之后, 删除 Ocelot 其余的 Controller.
然后打开 Ocelot 所在项目的 Program.cs, 开始注入 Ocelot 的配置:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOcelot(new ConfigurationBuilder()
.AddJsonFile("ocelot.json")
.Build());
......
......
var app = builder.Build();
app.Urls.Add("http://0.0.0.0:7000");
app.UseOcelot();

这几行代码就实现了对 Ocelot 的注入过程. 绑定 7000 端口. 但这不够. 我们还需要在 “ocelot.json” 中配置具体的路由指向.
{
  "Routes": [{
    "RouteIsCaseSensitive": false,
    "DownstreamPathTemplate": "/{url}",
    "DownstreamScheme": "http",
    "UpstreamHttpMethod": [ "Get", "POST", "PUT", "OPTIONS" ],
    "DownstreamHostAndPorts": [
      {
        "Host": "127.0.0.1",
        "Port": 7001
       }
     ],
     "UpstreamPathTemplate": "/api/test1/{url}",
    },{
    "RouteIsCaseSensitive": false,
    "DownstreamPathTemplate": "/{url}",
    "DownstreamScheme": "http",
    "UpstreamHttpMethod": [ "Get", "POST", "PUT", "OPTIONS" ],
    "DownstreamHostAndPorts": [
      {
        "Host": "127.0.0.1",
        "Port": 7002
       }
     ],
     "UpstreamPathTemplate": "/api/test2/{url}",
    }
  ]
}

其中, 这里的 “Upstream” (上游) 就是指前端访问时候的请求. 而 “Downstream” (下游) 指的实际接收并处理的各种 WEB 服务. 那么这里的两个配置就很好理解了, 就是将 请求 “/api/test1” 的所有请求分发给 test1 服务 (http://127.0.0.1:7001) 处理. 而访问 /api/test2 的请求则交给 test2 项目 (http://127.0.0.1:7002) 处理.
建好之后, Test.Ocelot 项目结构应该是这样的:

在完成以上步骤之后, 我们就可以分别将三个项目运行起来, 看一下效果了:

可以看到, 我们通过同一个基址 (http://127.0.0.1:7000/api) 加不同路径的方式就可以实现自动路由至不同服务的效果. 非常简单.

 

DOCKER 支持

这个相对比较简单, 虽然 Dockerfile 学起来麻烦, 但是无论是 Visual Studio 还是 Rider, 都支持根据项目直接生成 Dockerfile.

我们只需要将 EXPOSE 的端口号改为我们项目的端口号即可.
Test-WebApp1 的 Dockerfile 中, EXPOSE 指令只留下一个: EXPOSE 7001
Test-WebApp2 的 Dockerfile 中, EXPOSE 指令只留下一个: EXPOSE 7002
Dockerfile 内各种指令的含义可以参见: https://docs.docker.com/engine/reference/builder/#format
以上步骤完成之后, 我们就可以将我们的服务部署到 Docker 上去了.
打开 Terminal, 切换到解决方案所在目录, 执行命令以构建 Docker 镜像:
sudo docker build -t test-webapp1 -f ./Test-WebApp1/Dockerfile .

docker build 命令即为构建 docker 镜像的命令. -t 指定镜像名, -f 则是指定 Dockerfile 的路径. 而最后的 “.” 是指定当前目录. 由于解决方案几乎不可能只有一个 WebApi 项目单独运行, 构建时候应包含引用的其他项目. 因此这里指定的工作目录应该是解决方案文件 (*.sln) 的所在目录, 应以此目录为基准查找相关的文件. 执行之后, docker 将会根据 Dockerfile 自动开始复制相关文件, 并调用 dotnet build & dotnet publish 构建项目.

当出现

Successfully built ************
Successfully tagged test-webapp1:latest

的时候, 意味着构建已经成功.
然后我们可以用同样的命令去构建 Test-WebApp2 跟 Test-Ocelot
sudo docker build -t test-webapp2 -f ./Test-WebApp2/Dockerfile .
sudo docker build -t test-ocelot -f ./Test-Ocelot/Dockerfile .

我们可以用 docker images 命令来查看镜像是否都已经建立妥当.

此处可以看到镜像均已建立妥当, 这时候我们只需要挨个执行 docker run 命令即可.

sudo docker run -d –network=host –restart=always –name test-webapp1 test-webapp1
sudo docker run -d –network=host –restart=always –name test-testwebapp2 test-webapp2
sudo docker run -d –network=host –restart=always –name test-ocelot test-ocelot

其中, docker run 命令就是启动容器的命令. 这个命令会让 docker engine 去加载刚刚通过 docker build 生成的镜像. 

-d 表示此容器运行的程序属于驻留程序. 应长期执行, 因此不要附加在上面, 不要等待其执行结束, 加载成功即返回. 如果不加此参数, 则命令行将会一直卡住直至容器运行的程序结束退出. 

–network 指定 docker 容器的网路模式. docker 的网络模式分为 bridge (桥接)、none (无网络)、host (与主机共享网络)、container (容器间网络). 这里出于节省篇幅考虑暂时直接用 host 模式. 此模式下不需要手动公开接口, 镜像内运行的程序需要什么接口就会直接在宿主机上开放什么接口. 也就是说 Test-WebApp1 的 7001 跟 Test-WebApp2 的 7002 以及 Test-Ocelot 的 7000 端口都会默认开放出来供访问. 这种方法最简单但是由于会把容器里运行的所有端口都开放出来, 网络隔离性为 0, 且容易产生端口冲突. 后面不会用这种方式.
–restart 指定容器是否自启动, 以及在出错退出之后如何处理.

–name 指定容器名称.

最后一个参数是待加载的镜像的名称.

现在各容器均已启动, 可以试试是否能够运行了.

可以看到, 运行在 Docker 里的各程序均能正常运行.

 

Docker Compose

以上步骤其实还是蛮复杂的. 其实有一种更加一劳永逸的办法, 可以通过编写一个 yml 文件作为脚本, 将以上的 Docker 发布步骤自动执行的办法. 就是 Docker Compose.

Docker Compose 是用于定义和运行多容器 Docker 应用程序的工具, 此工具可以读取 yml 文件并根据文件内容自动构建, 运行.
一般来说, Docker Compose 是作为 Docker Engine 附带的一个组件自动安装的. 如果 “docker compose” 命令不存在或者是在 Windows 下运行, 可以到 https://github.com/docker/compose/releases 自行下载.

回到解决方案文件 (*.sln) 所在目录, 新建一个 “docker-compose.yml” 文件 (名字不要乱写, 固定叫这个名), 并对 TestApp1, TestApp2 以及 Ocelot 项目分别编写构建脚本.

services:
  test-webapp1:
    image: test-webapp1
    restart: always
    build:
      context: .
      dockerfile: ./Test-WebApp1/Dockerfile
    network_mode: host
    container_name: test-webapp1
    environment:
      - SET_CONTAINER_TIMEZONE=true
      - CONTAINER_TIMEZONE=Asia/Shanghai

  test-webapp2:
    image: test-webapp2
    restart: always
    build:
      context: .
      dockerfile: ./Test-WebApp2/Dockerfile
    network_mode: host
    container_name: test-webapp2
    environment:
      - SET_CONTAINER_TIMEZONE=true
      - CONTAINER_TIMEZONE=Asia/Shanghai

test-ocelot:
  image: test-ocelot
  restart: always
  build:
    context: .
    dockerfile: ./Test-Ocelot/Dockerfile
  network_mode: host
  container_name: test-ocelot
  environment:
    - SET_CONTAINER_TIMEZONE=true
    - CONTAINER_TIMEZONE=Asia/Shanghai

在解决方案目录中, 直接运行 sudo docker compose up --build --detach --force-recreate --remove-orphans

docker compose 就会自动完成以上的构建, 发布, 上传, 启动的过程. 其中 docker compose up 就是启动 docker compose 的命令. 相应的, docker compose down 就是停止 docker-compose.yml 中提到的各种容器. –build 指启动前应该重新编译, –detach 跟上文中提到的 docker run 中的 -d 是一个意思. 不要附加在上面, 不要等待其执行结束. –force-recreate 是指每次都删除容器重新创建. –remove-orphans 删除服务中没有在compose文件中定义的容器.

然后 docker ps 看一下, 几个服务都正常运行起来了. 以后每次都执行上面的命令, 整个过程就都可以自动完成了.

发表回复

您的电子邮箱地址不会被公开。