目录
前言
什么是微服务
微服务的优势
微服务的原则
创建项目
在Docker中运行服务
客户端调用
简单的集群服务
写这篇文章旨在用最简单的代码阐述一下微服务
微服务描述了从单独可部署的服务构建分布式应用程序的体系结构流程,同时这些服务会执行特定业务功能并通过 Web 接口进行通信,DevOps 团队通过将微服务(如构建块)组合在一起,从而将单个功能纳入微服务中以及构建更大的系统。
微服务采用了某一开放/封闭原则:
微服务为整体体系结构提供了众多优势:
使用微服务可提高团队速度。微服务通过允许软件开发团队利用事件驱动的编程和自动缩放等场景,很好地补充基于云的应用程序体系结构。 微服务组件通常会通过 REST 协议公开 API(应用程序编程接口),以便与其他服务通信。
顾名思义,微服务体系结构是一种将服务器应用程序生成为一组小型服务的方法。 这意味着微服务体系结构主要面向后端,虽然该方法也会用于前端。 每个服务都在自己的进程中运行,并使用 HTTP/HTTPS、WebSocket 或 AMQP 等协议与其他进程进行通信。 每个微服务在特定的上下文边界内实现特定的端到端域或业务功能,每个微服务都必须自主开发,并且可以独立部署。 最后,每个微服务都应拥有其自己的相关域数据模型和域逻辑(主权和分散式数据管理),并且可以基于不同的数据存储技术(SQL、NoSQL)和不同的编程语言。
微服务应该有多大? 在开发微服务时,大小不应成为重点。 相反,重点应该是创建松散耦合的服务以便自主地为每个服务进行开发、部署和缩放。 当然,在标识和设计微服务时,只要与其他微服务不存在过多的直接依赖项,就应尝试让它们尽可能地小。 比微服务的大小更重要的是,它必须具有内部内聚,并且独立于其他服务。
我们在项目中新建两个文件夹,Client跟Service。Client文件夹用于管理我们的客户端,Service文件夹用于管理我们的Api.
项目结构目录如下:
三个项目都是Asp.Net Core Web API。
我们为ForumProductApi以及ForumOrderApi添加一些基础代码,我们返回接口名称以及当前时间,服务的IP地址,端口等信息,以让我们更好的区分接口。
我们修改OrderApi的代码如下:
[ApiController]
[Route("order")]
public class OrderController : ControllerBase
{
private readonly ILogger _logger;
public OrderController(ILogger logger)
{
_logger = logger;
}
[HttpGet(Name = "GetOrder")]
public Task GetOrder()
{
return Task.FromResult(new OrderEntity()
{
date_time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
ip_address = Request.HttpContext.Connection.LocalIpAddress?.ToString(),
ip_port = Request.HttpContext.Connection.LocalPort.ToString(),
service_name = "订单服务"
});
}
}
public class OrderEntity
{
///
/// 当前时间
///
public string? date_time { get; set; }
///
/// Ip地址
///
public string? ip_address { get; set; }
///
/// Ip端口
///
public string? ip_port { get; set; }
///
/// 服务名称
///
public string? service_name { get; set; }
}
同理我们修改ProductApi的代码如下
[ApiController]
[Route("product")]
public class ProductController : ControllerBase
{
private readonly ILogger _logger;
public ProductController(ILogger logger)
{
_logger = logger;
}
[HttpGet(Name = "GetOrder")]
public Task GetOrder()
{
return Task.FromResult(new ProductEntity()
{
date_time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
ip_address = Request.HttpContext.Connection.LocalIpAddress?.ToString(),
ip_port = Request.HttpContext.Connection.LocalPort.ToString(),
service_name = "产品服务"
});
}
}
public class ProductEntity
{
///
/// 当前时间
///
public string? date_time { get; set; }
///
/// Ip地址
///
public string? ip_address { get; set; }
///
/// Ip端口
///
public string? ip_port { get; set; }
///
/// 服务名称
///
public string? service_name { get; set; }
}
发布Product服务。
修改Dockerfile如下
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed.
#For more information, please see https://aka.ms/containercompat
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
COPY ./ ./
ENTRYPOINT ["dotnet", "ForumProductApi.dll"]
打开prowershell,使用Docker编译项目并发布,
进入发布目录,build Api
docker build -t productcontainer:1.0 .
这就表示编译成功。
然后我们运行容器
docker run -d -p 8050:80 --name productapi productcontainer:1.0
然后我们在浏览器中访问该项目
http://localhost:8050/swagger/index.html
访问结果如下:
我们以同样的方式部署Order服务。
同样我们也需要修改Dockerfile如下:
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed.
#For more information, please see https://aka.ms/containercompat
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
COPY ./ ./
ENTRYPOINT ["dotnet", "ForumOrderApi.dll"]
打开prowershell,使用Docker编译项目并发布,
进入发布目录,build Api
docker build -t ordercontainer:1.0 .
运行项目
docker run -d -p 8060:80 --name ordertapi ordercontainer:1.0
然后我们在浏览器中访问该项目
http://localhost:8060/swagger/index.html
访问结果如下:
客户端我们需要http请求服务端接口,所以我们需要http请求,这里我使用了HttpClientFactory,代码很简单,可用于参考
Program代码
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
#region 注册IHttpClientFactory
builder.Services.AddHttpClient("local", config =>
{
config.BaseAddress = new Uri("http://localhost");
});
builder.Services.AddHttpClient();
#endregion
var app = builder.Build();
// Configure the HTTP request pipeline.
//if (app.Environment.IsDevelopment())
//{
app.UseSwagger();
app.UseSwaggerUI();
//}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
ClientController代码如下:
[ApiController]
[Route("client")]
public class ClientController:ControllerBase
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
public ClientController(IHttpClientFactory httpClientFactory, ILogger logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
[HttpGet(Name = "GetData")]
public async Task GetData()
{
var client = _httpClientFactory.CreateClient("local"); //
string order_url = "http://localhost:8060/order";
string product_url = "http://localhost:8050/product";
var order_result = await client.GetStringAsync(order_url);
var product_result = await client.GetStringAsync(product_url);
return $"订单服务:{order_result}========================产品服务:{product_result}";
}
}
直接运行看结果:
我们的接口都可以请求成功。
一切正常,进行到这里,各个服务都可以独立运行,客户端也可以正常调用,貌似我们已经完成一个简易的微服务了,但是微服务架构最重要的原则是,高可用,以上的做法并不能满足高可用性,因为我么的服务一旦挂掉,所有依赖这个服务的业务系统就会受到影响。
如,我们现在停止订单服务。
docker stop orderapi
我们再次使用客户端请求获取数据的接口:
出现如下结果:
要解决这个问题,我们很容易想到的解决方案就是,集群。
既然单个服务有挂掉的风险,那么部署多个服务实例就好了,只要大家不同时挂掉我们的请求就没有问题。
ok,我们使用docker运行多个服务实例
我们的Order服务运行三个实例,端口从60到62
docker run -d -p 8060:80 --name orderapi1 ordercontainer:1.0
docker run -d -p 8061:80 --name orderapi2 ordercontainer:1.0
docker run -d -p 8062:80 --name orderapi3 ordercontainer:1.0
同样的我们的product服务也运行三个实例,端口从50到52
docker run -d -p 8050:80 --name productapi1 productcontainer:1.0
docker run -d -p 8051:80 --name productapi2 productcontainer:1.0
docker run -d -p 8052:80 --name productapi3 productcontainer:1.0
那么也稍稍的改动一下我们的Client代码吧
[ApiController]
[Route("api/[controller]/[action]")]
public class ClientController:ControllerBase
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
public ClientController(IHttpClientFactory httpClientFactory, ILogger logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task GetProduct()
{
var client = _httpClientFactory.CreateClient("local"); //
string[] arr_product_url = { "http://localhost:8050/product", "http://localhost:8051/product", "http://localhost:8052/product" } ;
var product_result = await client.GetStringAsync(arr_product_url[new Random().Next(0, 3)]);
return $"产品服务:{product_result}";
}
public async Task GetOrder()
{
var client = _httpClientFactory.CreateClient("local"); //
string[] arr_order_url = { "http://localhost:8060/order", "http://localhost:8061/order", "http://localhost:8062/order" };
var order_result = await client.GetStringAsync(arr_order_url[new Random().Next(0, 3)]);
return $"订单服务:{order_result}";
}
}
当然拿到这些服务地址可以自己做复杂的负载均衡策略,比如轮询,随机,权重等等 都行,甚至在中间弄个nginx也可以。这些不是重点,所以就简单做一个随机吧,每次请求来了随便访问一个服务实例。
我们尝试多次调用该接口,发现我们已经实现了随机的效果。
但是,这种做法依然不安全,如果随机访问到的实例刚好挂掉,那么业务依然会出现问题,简单处理思路是什么呢?
1.如果某个地址请求失败了,那么换一个地址接着执行。
2.如果某个地址的请求连续多次失败了,那么就移除这个地址,下次就不会访问到它了。
业务系统实现以上逻辑,基本上风险就很低了,也算是大大增加了系统可用性了。
然而:
实际应用中,上层的业务系统可能非常多,为了保证可用性,每个业务系统都去考虑服务实例挂没挂掉吗?
而且实际应用中服务实例的数量或者地址大多是不固定的,例如双十一来了,流量大了,增加了一堆服务实例,这时候每个业务系统再去配置文件里配置一下这些地址吗?双十一过了又去把配置删掉吗?显然是不现实的,服务必须要做到可灵活伸缩。
这时候就需要引入一个问题,服务注册与发现。