# 前言

此系列旨在复习微服务的相关知识,示例代码中不会出现聚合、聚合根、服务拆分等相关概念(不会涉及到领域驱动相关知识),只使用最简单的.Net Core Web程序,主要关注点在于:

  • 如何使用 Docker 部署 .NetCore 应用
  • 应用程序的 DockerFile 编写
  • 服务注册和服务发现是什么?解决了什么问题?
  • 网关用来做什么?服务治理相关(熔断/限流/降级/链路追踪/缓存…)

# 微服务概念

关于微服务的概念解释网上有很多,每个人的理解都不同。至于为什么要使用微服务?微服务的优缺点等相关问题每个人理解不同。个人理解:微服务是一种系统架构模式,和语言无关,框架无关,工具无关,服务器环境无关,微服务目的是:将传统单体系统按照业务拆分成多个职责单一、且可独立运行的服务。至于服务如何拆分,没有明确的定义。采用微服务优点是:每个服务的职责单一且可独立部署、不同服务间采用轻量级的通信协议作为通信原则,松耦合。这样不同服务就可以使用不同的技术栈(优势语言),缺点的话是:微服务架构避免不了会引入更多技术栈、中间件等等增加系统复杂度。(微服务不是银弹,要根据实际业务体量考虑是否使用,否则只会徒增不必要的麻烦)

# 项目结构搭建

  • Order.Api:订单服务
  • Web.Client:测试使用的客户端

创建项目时启用Docker支持,或者之后添加也可以。添加基础代码,简单的返回服务名称、当前时间、服务IP、端口:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

using System;

namespace Order.Api.Controller
{
    [Route("[Controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        [HttpGet]
        public IActionResult Index()
        {
            string result = $"订单服务:{DateTime.Now:yyyy-MM-dd HH:mm:ss},-{Request.HttpContext.Connection.LocalIpAddress}:{Request.HttpContext.Connection.LocalPort}";
            return Ok(result);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 容器化部署

代码就写这么简单,下面使用Docker来部署订单服务。这里先了解一下如果启用了Docker支持,VS默认生成的 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/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["Order.Api/Order.Api.csproj", "Order.Api/"]
RUN dotnet restore "Order.Api/Order.Api.csproj"
COPY . .
WORKDIR "/src/Order.Api"
RUN dotnet build "Order.Api.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Order.Api.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Order.Api.dll"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

关于Dockerfile 各个命令的作用这里不再解释 这里的 Dockerfile 文件不能直接使用,因为我采用的方式是:将发布后的应用部署到 Centos => docker build镜像=>运行容器。跳过了这里的 dotnet restoredotnet publish。修改后的 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/aspnet:5.0 AS base
# 设置工作目录,如不存在会被创建
WORKDIR /app
# Copy release文件夹内容到工作目录app
COPY . /app
# 运行.dll
ENTRYPOINT ["dotnet", "Order.Api.dll"]
1
2
3
4
5
6
7
8
9

将发布后的包扔到虚机指定目录中:

# 进入目录
[root@centos-01 ~]# cd /usr/dotnetcore_src/order.api.release/

# 查看本地镜像列表
[root@centos-01 order.api.release]# docker image ls
REPOSITORY                        TAG          IMAGE ID       CREATED        SIZE
<none>                            <none>       16ff5dcb1c6d   2 hours ago    206MB
<none>                            <none>       6d3756023f75   25 hours ago   210MB
<none>                            <none>       3f41b63e8f79   25 hours ago   210MB
mcr.microsoft.com/dotnet/sdk      5.0          da19c23a5531   2 days ago     631MB
mcr.microsoft.com/dotnet/aspnet   5.0          a2be3e478ffa   2 days ago     205MB
consul                            latest       b74a0a01afc4   2 weeks ago    116MB
rabbitmq                          management   0bfe221339ae   7 weeks ago    253MB
mongo                             latest       aad77ae58e0c   7 weeks ago    682MB
redis                             latest       08502081bff6   2 months ago   105MB
portainer/portainer               latest       580c0e4e98b0   6 months ago   79.1MB
elasticsearch                     7.1.1        b0e9f9f047e6   2 years ago    894MB

# build镜像
[root@centos-01 order.api.release]# docker build -t order.api .
Sending build context to Docker daemon  1.184MB
Step 1/4 : FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
 ---> a2be3e478ffa
Step 2/4 : WORKDIR /app
 ---> Using cache
 ---> 9f551bd1698a
Step 3/4 : COPY . /app
 ---> 04334af56137
Step 4/4 : ENTRYPOINT ["dotnet", "Order.Api.dll"]
 ---> Running in 44daedf04664
Removing intermediate container 44daedf04664
 ---> 58968d65acff
Successfully built 58968d65acff
Successfully tagged order.api:latest

# 查看最新本地镜像列表发现 order.api 镜像
[root@centos-01 order.api.release]# docker image ls
REPOSITORY                        TAG          IMAGE ID       CREATED              SIZE
order.api                         latest       58968d65acff   About a minute ago   206MB
<none>                            <none>       16ff5dcb1c6d   2 hours ago          206MB
<none>                            <none>       6d3756023f75   25 hours ago         210MB
<none>                            <none>       3f41b63e8f79   25 hours ago         210MB
mcr.microsoft.com/dotnet/sdk      5.0          da19c23a5531   2 days ago           631MB
mcr.microsoft.com/dotnet/aspnet   5.0          a2be3e478ffa   2 days ago           205MB
consul                            latest       b74a0a01afc4   2 weeks ago          116MB
rabbitmq                          management   0bfe221339ae   7 weeks ago          253MB
mongo                             latest       aad77ae58e0c   7 weeks ago          682MB
redis                             latest       08502081bff6   2 months ago         105MB
portainer/portainer               latest       580c0e4e98b0   6 months ago         79.1MB
elasticsearch                     7.1.1        b0e9f9f047e6   2 years ago          894MB
[root@centos-01 order.api.release]# 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

有了镜像之后就可以基于镜像创建容器:

[root@centos-01 order.api.release]# docker run -d --name order.api -p 80:80 order.api
eaa1d05afe39ccdc6a07347df78c994f57c654267db1e40b64d21e030b565903:
1
2

至此订单服务就部署完毕。下面使用 Web.Client 客户端测试,这里的客户端是泛指,实际可能是各种业务系统、手机端、小程序等等。

# 客户端调用

这里使用 RestSharp作为Http请求客户端,Nuget 搜索 【RestSharp】 (opens new window) 安装即可。

核心代码如下:

IServiceHelper.cs:

public interface IServiceHelper
{
    Task<string> GetOrder();
}
1
2
3
4

ServiceHelper.cs:

 public class ServiceHelper : IServiceHelper
 {
     public async Task<string> GetOrder()
     {
         // 订单服务地址
         string serviceUrl = "http://192.168.31.191:80";
         var Client = new RestClient(serviceUrl);
         var request = new RestRequest("/orders", Method.GET);
         var response = await Client.ExecuteAsync(request);
         return response.Content;
     }      
}
1
2
3
4
5
6
7
8
9
10
11
12

Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    // 注入IServiceHelper
    services.AddSingleton<IServiceHelper, ServiceHelper>();
}
1
2
3
4
5
6

HomeController.cs:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> logger;
    private readonly IServiceHelper serviceHelper;

    public HomeController(ILogger<HomeController> logger, IServiceHelper serviceHelper)
    {
        this.logger = logger;
        this.serviceHelper = serviceHelper;
    }

    public async Task<IActionResult> IndexAsync()
    {
        ViewBag.OrderData = await serviceHelper.GetOrder();
        return View();
    }

    public IActionResult Privacy()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Index.cshtml:

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>
        @ViewBag.OrderData
    </p>
</div>
1
2
3
4
5
6
7
8
9
10

到这里服务已经独立部署运行,客户端也可以正常调用了。但是思考一个问题:如果这个服务挂掉了怎么办?微服务中非常重要的原则就是"高可用",以上的做法明显不能满足。要解决这个问题一般都会采用集群方式。

# 简单服务集群

既然单个服务实例有挂掉的风险,那么部署多个服务实例试试,只要不同时挂掉就可以保证正常访问。下面使用Docker运行多个服务实例:

[root@centos-01 ~]# docker run -d --name order.api -p 80:80 order.api
c4a974a607b54377115a32a4227fa0f9d2ca4332405875b3763cca2696932c1c
[root@centos-01 ~]# docker run -d --name order.api1 -p 81:80 order.api
992f0b2975f60320ba92c2e79b33ae066c17b3b26f54e74b96ad7677d54042d7
[root@centos-01 ~]# docker run -d --name order.api2 -p 82:80 order.api
dca6a0cd36a4bca3111b5694f34c8f8ffbcc81d6dbbadb45a2d3209afa7b0595
1
2
3
4
5
6

现在订单服务增加到三个服务实例,分别映射到80/81/82端口。需要修改一下客户端代码:

public async Task<string> GetOrder()
{
    // 服务实例集合
    string[] serviceUrls = { "http://192.168.31.191:80", "http://192.168.31.191:81", "http://192.168.31.191:82" };
    // 每次随机访问一个服务实例
    var client = new RestClient(serviceUrls[new Random().Next(0, 3)]);
    var request = new RestRequest("/orders", Method.GET);
    var response = await client.ExecuteAsync(request);
    return response.Content;
}
1
2
3
4
5
6
7
8
9
10

这里拿到服务地址可以自己做复杂的负载均衡策略,比如轮询,随机,权重等或者使用nginx都可以。这不是重点,所以这里只是简单随机访问一个服务实例

这里已经做到了将请求随机分配到一个服务实例,但这种做法依旧存在问题:

  1. 如果随机访问到的实例刚好挂掉,依然无法正常访问
  2. 如果到某个地址的请求连续多次失败,应该移除这个地址保证其他请求不会再访问到
  3. 实际应用中,上层的业务系统可能非常多,为了保证可用性,每个业务系统都需要考虑服务实例运行状态吗?而且实际应用中服务实例的数量或者地址大多数时候是不固定的,比如:流量高峰期,增加服务实例,这时候每个业务系统再去配置文件里配置地址?高峰期过了又去把配置删掉?显然是不现实的。服务必须要做到可灵活伸缩

要做到可灵活伸缩就引入了另一个名词:服务注册与发现。