微服务的概念

微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成。这些服务由各个小型独立团队负责。
微服务架构使应用程序更易于扩展和更快地开发,从而加速创新并缩短新功能的发布时间。

整体式架构 与 微服务架构 的比较

通过整体式架构

所有进程紧密耦合,并可作为单项服务运行。这意味着,如果应用程序的一个进程遇到需求峰值,则必须扩展整个架构。随着代码库的增长,添加或改进整体式应用程序的功能变得更加复杂。这种复杂性限制了试验的可行性,并使实施新概念变得困难。整体式架构增加了应用程序可用性的风险,因为许多依赖且紧密耦合的进程会扩大单个进程故障的影响。

使用微服务架构

将应用程序构建为独立的组件,并将每个应用程序进程作为一项服务运行。这些服务使用轻量级 API 通过明确定义的接口进行通信。这些服务是围绕业务功能构建的,每项服务执行一项功能。由于它们是独立运行的,因此可以针对各项服务进行更新、部署和扩展,以满足对应用程序特定功能的需求。

微服务的特性

自主性

可以对微服务架构中的每个组件服务进行开发、部署、运营和扩展,而不影响其他服务的功能。这些服务不需要与其他服务共享任何代码或实施。各个组件之间的任何通信都是通过明确定义的 API 进行的。

专用性

每项服务都是针对一组功能而设计的,并专注于解决特定的问题。如果开发人员逐渐将更多代码增加到一项服务中并且这项服务变得复杂,那么可以将其拆分成多项更小的服务。

单一职责

每个微服务都需要满足单一职责原则,微服务本身是内聚的,因此微服务通常比较小。每个微服务按业务逻辑划分,每个微服务仅负责自己归属于自己业务领域的功能。

微服务的优势

敏捷性

微服务促进若干小型独立团队形成一个组织,这些团队负责自己的服务。各团队在小型且易于理解的环境中行事,并且可以更独立、更快速地工作。这缩短了开发周期时间。您可以从组织的总吞吐量中显著获益。

灵活扩展

通过微服务,您可以独立扩展各项服务以满足其支持的应用程序功能的需求。这使团队能够适当调整基础设施需求,准确衡量功能成本,并在服务需求激增时保持可用性。

轻松部署

微服务支持持续集成和持续交付,可以轻松尝试新想法,并可以在无法正常运行时回滚。由于故障成本较低,因此可以大胆试验,更轻松地更新代码,并缩短新功能的上市时间。

技术自由

微服务架构不遵循“一刀切”的方法。团队可以自由选择最佳工具来解决他们的具体问题。因此,构建微服务的团队可以为每项作业选择最佳工具。

可重复使用的代码:将软件划分为小型且明确定义的模块,让团队可以将功能用于多种目的。专为某项功能编写的服务可以用作另一项功能的构建块。这样应用程序就可以自行引导,因为开发人员可以创建新功能,而无需从头开始编写代码。

弹性

服务独立性增加了应用程序应对故障的弹性。在整体式架构中,如果一个组件出现故障,可能导致整个应用程序无法运行。通过微服务,应用程序可以通过降低功能而不导致整个应用程序崩溃来处理总体服务故障。

微服务的缺点

当微服务过多时,服务间的通信变得错综复杂,比如:A服务 -> E服务 -> B服务 ... 甚至更多的分支串联,形成一张莫大的蜘蛛网,若要追踪一笔数据... 这对未来的工作变得更加复杂。

作者:[Sol·wang] - 博客园,原文出处:https://www.cnblogs.com/Sol-wang/p/17293829.html

认证授权

参考以往文章:

IdentityServer4 - v4.x 概念理解及运行过程

IdentityServer4 - v4.x .Net中的实践应用

服务限流

为什么要限流。。。削峰,减轻压力,为了确保服务器能够正常持续的平稳运行。
当访问量大于服务器的承载量,我们不希望有服务器的灾难发生;在接收请求的初期,适当的过滤一些请求,或延时处理或忽略掉。
有第三方工具如hystrix、有分布式网关限流如Nginx、未来的.NET7自带限流中间件AspNetCoreRateLimit等。
以下按限流算法的理解做一些分享。

限流方式

计数方式、固定窗口方式、滑动窗口方式、令牌桶方式、漏桶方式等。

滑动窗口方式

随着时间的流逝,窗口逐步向前移动;窗口有宽度,也就是时长;窗口内处理的量,也就是量有上限。

数组存放每个请求的时间点;数组首尾时间差不超过定义时长;定义时长可接收的量。

运行示例图

微服务 - 概念 · 应用 · 通讯 · 授权 · 跨域 · 限流-LMLPHP

实现过程

  1. 准备一个数组,存储每次请求的时间点;定义时长1s;定义单位时长内可接收请求数量的上限
  2. 本次请求的当前时间点,与数组中最早的请求时间点 比对(数组首尾比对)
  3. 比对差值(秒)在定义的时间内 & 在上限数量的范围内,当前时间点记录到数组,被视为可接收的请求
  4. 比对差值(秒)超过定义时长(1s)或超出上限的请求,被限制/忽略;不加入数组,设置Response后返回
  5. 每次记得移除超出时长的记录,以确保持续接收合规的新请求

限流中间件案例

非完整版 看懂就行

public class RequestLimitingMiddleware
{
    // 单位时间内,可接收的请求数量
    private int _qps = 6;
    // 定义单位时长(秒)
    private readonly int _unit_seconds = 1;
    // 集合存放已接收的请求
    private ConcurrentQueue<DateTime> _backlog_request = new ConcurrentQueue<DateTime>();
        
        
    /// <summary>
    /// 限流方法 - 时间滑动窗口算法,是否限流
    /// </summary>
    /// <returns></returns>
    private bool Limiting()
    {
        // 比对的结果差值
        double _diff_sec = 0;
        // 本次请求时间
        DateTime _curr_req_now = DateTime.Now;


        #region 1、每次先消除已过期的请求(超出时间范围的请求,被定义为系统已处理)
        // 遍历整个集合
        DateTime _disused_req = new DateTime();
        while (_backlog_request.TryPeek(out _disused_req))
        {
            // 超出定义时长的
            if (_curr_req_now.Subtract(_disused_req).TotalSeconds > _unit_seconds)
            {
                // 移除
                _backlog_request.TryDequeue(out _disused_req);
            }
            else
                break;
        }
        #endregion


        #region 2、有积压的请求,取最早的那个请求时间,与本次时间比对,并计算出差值
        DateTime _first_req_now = new DateTime();
        if (_backlog_request.TryPeek(out _first_req_now))
        {
            // 当前请求的时间 与 最早的请求时间 跨度
            _diff_sec = _curr_req_now.Subtract(_first_req_now).TotalSeconds;
        }
        #endregion


        #region 3、是否限制的请求
        // 集合的首尾不能超过单位时长,及数量上限
        if (_diff_sec < _unit_seconds && _backlog_request.Count < _qps)
        {
            // 可接收的新请求 记录到集合
            _backlog_request.Enqueue(_curr_req_now);
            return true;
        }
        // 被视为限制的请求
        return false;
        #endregion
    }


    public Task Invoke(HttpContext context)
    {
        #region 限流方法的应用
        if (!this.Limiting())
        {
            _logger.LogWarning($" ! 被限制的请求,忽略");
            context.Response.StatusCode = (Int16)HttpStatusCode.TooManyRequests;
            context.Response.ContentType = "text/json;charset=utf-8;";
            return context.Response.WriteAsync("抱歉,限流了,请稍后再试。");
        }
        _logger.LogInformation($" + 新增的请求,当前积压 {_backlog_request.Count} req.");
        #endregion


        // 模拟运行消耗时间
        Thread.Sleep(300);
        _next(context);
        return Task.CompletedTask;
    }
}

滑动窗口限流测试

由于设置的1s/6次请求,所以手动可以测试;浏览器快速的敲击F5请求API接口,测试效果如下图:

微服务 - 概念 &#183; 应用 &#183; 通讯 &#183; 授权 &#183; 跨域 &#183; 限流-LMLPHP

漏桶方式

看桶内容量,溢出就拒绝;(累加的请求数是否小于上限)

实现逻辑

有上限数量的桶,接收任意请求

随着时间的流逝,上次请求时间到现在,通过速率,计算出桶内应有的量

此量超过上限,拒绝新的请求

直到消耗出空余数量后,再接收新的请求

以上仅通过计算出的剩余的数字,决定是否接收新请求

比如:每秒10个请求上线,还没到下一秒,进来的第11个请求被拒绝

令牌方式

看令牌数量,用完就拒绝;(累减的令牌是否大于0)

假如以秒为单位发放令牌,每秒发10个令牌,当这一秒还没过完,收到了第11个请求,此时令牌干枯了,那就拒绝此请求;

所以每次请求看有没有令牌可用。

实现逻辑

按速率,两次请求的时间差,计算出可生成的令牌数;每个请求减一个令牌

相同时间进来的请求,时间差值为0,所以每次没能生成新的令牌,此请求也消耗一个令牌

直到令牌数等于0,拒绝新请求

跨域

为什么有跨域

源自于浏览器;出于安全的考虑,浏览器默认限制不同站点域名间的通讯,所以 JS/Cookie 只能访问本站点下的内容;叫 同源策略

跨域的原理及策略

浏览器默认是限制跨域的,当然也可以告诉浏览器,怎样的站点间通讯可以取消限制。

Request 或 Response 中追加 Header 的设定:允许的请求源头允许的请求动作允许的Header方式等。

如:Access-Control-Allow-Origin:{目标域名Url}

可以用不受限的*,允许所有的跨域请求,这样的安全性低;

也可以指定一个二级域名,域名下所有的Url不受限;

也可以仅指定一个固定的Url;

也可以指定请求动作 GET/PUT;

以上设定都称为跨域的策略,按实际情况自定义策略。

.NET跨域的实现

Request / Response 的 Header 设定方式:

Response.Headers["Access-Control-Allow-Origin"] = "{域名地址}";
Response.Headers["Access-Control-Allow-Credentials"] = "true";
Response.Headers["Access-Control-Allow-Headers"] = "x-requested-with,content-type";

中间件定义策略方式:

.NET默认提供了跨域的中间件UseCors,同样可以在中间件中设定 源头/动作/Header 等。

全局策略案例:

// 设定跨域策略
builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "策略名称1", policy =>
    {
        // 允许的域名
        policy.WithOrigins("http://contoso.com", "http://*.sol.com")
        // 允许的请求动作
        .WithMethods("GET", "POST", "PUT", "DELETE")
        // 允许的 Header
        .AllowAnyHeader();

    });
});
// ... 最后启用跨域中间件
app.UseCors("{策略名称}");

Action单独设定跨域:

启用:[EnableCors]
指定:[EnableCors("策略名称")]
详细:[EnableCors(origins: "http://Sol.com:8013/", headers: "*", methods: "GET,PUT")]
排除:[DisableCors]

服务间的通信

Remote Procedure Call - RPC

Remote Procedure Call,远程过程调用。通常,RPC要求在调用方中放置被调用的方法的接口。调用方只要调用了这些接口,就相当于调用了被调用方的实际方法,十分易用。于是,调用方可以像调用内部接口一样调用远程的方法,而不用封装参数名和参数值等操作。传输速度快,效率高的特点,常用于服务间的通信。

整体运行过程:

微服务 - 概念 &#183; 应用 &#183; 通讯 &#183; 授权 &#183; 跨域 &#183; 限流-LMLPHP

.NET服务被调方集成 gRPC

1、NuGet 安装 Grpc.AspNetCore

2、编写 Proto 文件(为生成C#代码)

syntax = "proto3";
// 生成代码后的命名空间
option csharp_namespace = "GrpcService";
// 包名(不是必须)
package product;
// 定义一个服务
service Producter{
    // 定义一个方法(请求参数类,返回参数类)
    rpc Add(CreateProductRequest) returns (CreateProductResponse);
    rpc Query(QueryProductRequest) returns (QueryProductResponse);
}

// 为上述服务 定义 请求参数类
message QueryProductRequest{
    // 类型、名称、唯一标识
    string name = 1;
    string code = 2;
}
// 为上述服务 定义 返回参数类
message QueryProductResponse{
    // 定义为集合类型
    repeated Product products = 1;
}

message CreateProductRequest{
    string name = 1;
    string code = 2;
    string color = 3;
    string size = 4;
    string manufacturing = 5;
}

message CreateProductResponse{
    ResultType result = 1;
}
// 定义(以上用到的)枚举
enum ResultType{
    success=0;
    fail=1;
}

message Product{
    int32 id = 1;
    string name = 2;
    string code = 3;
    string color = 4;
    string size = 5;
}

3、项目属性文件配置编译包含项

4、Build 项目;通过 proto 文件自动生成C#代码(于obj目录中)

5、编写对应的Service 继承于自动生成的抽象类,并实现其中抽象方法

public class ProductService : Producter.ProducterBase

6、注册到容器

// 注册
builder.Services.AddGrpc();
// 到容器
app.MapGrpcService<ProductService>();

7、appsettings.json 配置启用RPC所需的HTTP2协议

"Kestrel": {
  "EndpointDefaults": {
    "Protocols": "Http2"
  }
}

8、最终目录效果图

微服务 - 概念 &#183; 应用 &#183; 通讯 &#183; 授权 &#183; 跨域 &#183; 限流-LMLPHP

.NET服务调用方集成 gRPC

1、NuGet 安装 Grpc.AspNetCore、Grpc.Net.Client

2、Cope 服务端 Proto 文件于目录

3、项目属性文件配置编译包含项

<ItemGroup>
    <Protobuf Include="Protos\product.proto" GrpcServices="Client" />
</ItemGroup>

4、Build 项目;通过 proto 文件自动生成C#代码(于obj目录中)

5、使用生成的客户端代码请求服务端

// 建立连接
var channel = GrpcChannel.ForAddress("https://localhost:7068");
// 创建客户端对象
var client = new Producter.ProducterClient(channel);
// 调用服务端方法(及参数)
QueryProductResponse resp = client.Query(new QueryProductRequest { Code = "1", Name = "1" });
// 返回的数据集合
foreach (var item in resp.Products)
04-07 03:29