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

一、微服务的概念

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

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

通过整体式架构

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

使用微服务架构

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

微服务的特性

自主性

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

专用性

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

单一职责

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

微服务总体架构示意图

微服务的优势

敏捷性

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

灵活扩展

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

轻松部署

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

技术自由

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

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

弹性

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

微服务的缺点

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

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

二、RESTful API

Representational State Transfer,意为表述性状态转移。RESTful架构应该遵循统一接口原则,统一接口包含了一组受限的预定义的操作,不论什么样的资源,都是通过使用相同的接口进行资源的访问。接口应该使用标准的HTTP方法,并遵循这些方法的语义。

下面列出了常用的HTTP方法及说明:

GET 查询提取
POST 新增创建
PUT 编辑修改
PATCH 局部变更
DELETE 移除资源
HEAD 无Body从Head中窥视。如 Content-Length有数据,200有/404无; 或自定义Head
OPTIONS 预先检测请求。常用于跨域时浏览器的预检请求,以询问目标域是否支持此跨域请求
TRACE 服务器请求的链路过程,C1>S1>S3>S5>S6。安全因素,更多语言不支持
CONNECT 客户端以目标服务器为代理,使客户端连接到其它服务器。安全因素,更多语言不支持

POST 后端样例

d.Save(m);
return Created();    # Response Status Code 201

PATCH 后端样例

entity.email = "sol@xxx.como";
return NoContent();    # Response Status Code 204

DELETE 后端样例

prod.Delete(123);
return NoContent();    # Response Status Code 204

HEAD 后端样例

Response.Headers.Add("Content-Length", resource.ContentLength.ToString());
Response.Headers.Add("Last-Modified", resource.LastModified.ToString("R"));
return true ? Ok() : NotFound();    # Response Status Code 200/404

实际应用更多时候,成功都用 200,省事儿。

三、认证授权

组建认证授权中心,提供各服务的认证授权。

请参考以往文章:

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

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

四、服务限流

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

以下也对单个服务的各种限流方式的算法理解做一些分享。

限流方式

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

滑动窗口方式

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

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

运行示例图

限流 - 滑动窗口图解

实现过程

  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接口,测试效果如下图:

漏桶方式

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

实现逻辑

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

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

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

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

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

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

令牌方式

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

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

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

实现逻辑

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

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

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

对于单个服务的限流,ASP.NET Core in .NET 8.0,微软已有了自带的限流中间件于Microsoft.AspNetCore.RateLimiting中,拿来直接用,请参考官网说明

五、跨域 Cors

为什么有跨域

N多个服务集群构成一个平台,不同的二级域名区分,不同的域划分等等,站点间就需要互相访问,势必有跨域(名)的通信。跨域 - 源自于浏览器;出于安全的考虑,浏览器默认限制不同站点域名间的通讯,所以 JS/Cookie 只能访问本站点下的内容;叫 同源策略。以下阐述非同源的跨域方式。

跨域的原理及策略

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

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

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

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

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

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

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

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

跨域请求的过程

浏览器 OPTIONS 询问 -> 目标答复允许 -> 浏览器再继续请求

1、预检请求:浏览器先向目标域用OPTIONS方式发起请求,询问是否允许以下这样的请求。

# 浏览器 预检请求,主要 HEADER 项
# Method: OPTIONS
Origin: http://foo.example # 请求者
Access-Control-Request-Method: POST # 将会使用的请求方式
Access-Control-Request-Headers: X-Custom-Header, Content-Type # 附带的头信息

2、目标域的答复 Header 告知是否允许请求

# 目标域返回的答复,主要 HEADER 项
Access-Control-Allow-Origin:http://*.example # 允许的源
Access-Control-Allow-Headers:* # 允许的头,或 * 不限制
Access-Control-Allow-Credentials:true # 允许后返回请求的内容
Access-Control-Allow-Methods:POST,PUT,DELETE,OPTIONS # 列出允许的请求方式

3、实际请求:浏览器再发起实际的(POST)请求,后并会得到最终的响应结果

# Method: POST
Origin: http://foo.example
X-Custom-Header: Custom-Content
Content-Type: application/json; charset=UTF-8

OPTIONS 请求方式:本意是列出服务资源允许的请求方式,这里用在跨域时的预检请求非常恰当。

何时需要触发跨域

Simple Requests 时,才会触发跨域请求模式,那么什么是简单请求呢?

GET/POST/HEADER的请求方式,原生的Header项,不使用ReadableStream(不读资源),XMLHttpRequest.upload无附加侦听事件,Content-Typetext/plainmultipart/form-dataapplication/x-www-form-urlencoded其一。

...所以呢,可能很多请求都非简单请求,就需要浏览器先用OPTIONS方式完成预检询问请求。

...所以呢,以下跨域的例子中,都是诸如 PUT/DELETE 等的案例。

.NET跨域的实现

🔖 Header 设定方式:

# 源头时 Request-Header
Request.Method = "OPTIONS";
Request.Headers["Access-Control-Request-Method"] = "POST"; # 后续实际的请求方式
Request.Headers["Access-Control-Request-Headers"] = "X-Custom,Y-Custom"; # 将会携带的HEADER
# 目标时 Response-Header
Response.Headers["Access-Control-Allow-Origin"] = "{域名地址}"; # 允许的访问来源,或*不限制
Response.Headers["Access-Control-Allow-Headers"] = "*"; # 请求时,允许携带的头名称,或*不限制
Response.Headers["Access-Control-Allow-Credentials"] = "true"; # (等同认证通过)是否返回内容
Response.Headers["Access-Control-Allow-Methods"] = "PUT,DELETE,OPTIONS";# 请求时,允许的请求方式

以上仅为请求或应答时的设置,目标是否允许请求,还要在目标中做相应的处理,允许|拒绝。
...那么接下来就是,目标中的具体处理方式/策略。

🔖 中间件定义策略方式:

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

🔖 全局策略案例:

# 设定跨域策略
builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "策略名称A", policy =>
    {
        // 允许的域名
        policy.WithOrigins("http://contoso.com", "http://*.sol.com")
        // 允许的请求动作
        .WithMethods("PUT", "DELETE", "OPTIONS")
        // 允许的 Header
        .AllowAnyHeader();
    });
});
# ...
# 最后...启用跨域中间件
app.UseCors("{策略名称}");

🔖 Action单独设定跨域:

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

六、服务间的通信

前端发起一个请求,此请求要汇总各方面流程步骤数据等,由于拆分后的服务不能涵盖所有业务,势必要调用其它服务协调完成任务,所以通过后端完成服务间的通信,直接用API也是一种选择,某些业务会有大量数据的传输。基于此场景,以下阐述后端服务间的通信方案。

Remote Procedure Call

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

整体运行过程:

.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、最终目录效果图

.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)
posted @ 2023-04-06 19:01  Sol·wang  阅读(1160)  评论(3编辑  收藏  举报