# 前言
此系列旨在复习微服务的相关知识,示例代码中不会出现聚合、聚合根、服务拆分等相关概念(不会涉及到领域驱动相关知识),只使用最简单的.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);
}
}
}
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"]
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 restore
和 dotnet 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"]
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]#
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:
2
至此订单服务就部署完毕。下面使用 Web.Client
客户端测试,这里的客户端是泛指,实际可能是各种业务系统、手机端、小程序等等。
# 客户端调用
这里使用 RestSharp
作为Http请求客户端,Nuget
搜索 【RestSharp】 (opens new window) 安装即可。
核心代码如下:
IServiceHelper.cs:
public interface IServiceHelper
{
Task<string> GetOrder();
}
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;
}
}
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>();
}
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 });
}
}
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>
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
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;
}
2
3
4
5
6
7
8
9
10
这里拿到服务地址可以自己做复杂的负载均衡策略,比如轮询,随机,权重等或者使用nginx都可以。这不是重点,所以这里只是简单随机访问一个服务实例
这里已经做到了将请求随机分配到一个服务实例,但这种做法依旧存在问题:
- 如果随机访问到的实例刚好挂掉,依然无法正常访问
- 如果到某个地址的请求连续多次失败,应该移除这个地址保证其他请求不会再访问到
- 实际应用中,上层的业务系统可能非常多,为了保证可用性,每个业务系统都需要考虑服务实例运行状态吗?而且实际应用中服务实例的数量或者地址大多数时候是不固定的,比如:流量高峰期,增加服务实例,这时候每个业务系统再去配置文件里配置地址?高峰期过了又去把配置删掉?显然是不现实的。服务必须要做到可灵活伸缩
要做到可灵活伸缩就引入了另一个名词:服务注册与发现。
← DockerFile 微服务项目之注册发现 →