学如逆水行舟,不进则退!最近发现微服务真的是大势所趋,停留在公司所用框架里已经严重满足不了未来的项目需要了,所以抽空了解了一下微服务,并进行了代码落地。

虽然项目简单,但过程中确实也学到了不少东西。

写在前面:先看下项目总体目录以及拓扑图,其中包括2个服务(几乎一样),一个网关,一个mvc项目。我的服务器是用虚拟机搭建的,环境是CentOS 7。本片文章看似繁杂冗长,其实只是记录的步骤比较详细,实操完成你会发现并不难理解,只是微服务的简单入门而已。还请大神多多指教!

一、准备Consul注册中心

在Docker中安装一个Consul

1. 拉取镜像

docker pull consul

2. 启动Server

启动前, 先建立 /consul/data文件夹, 保存 consul 的数据

mkdir -p /data/consul

3. 使用 docker run 启动 server

docker run -d -p 8500:8500 -v /consul/data:/consul/data -e --name=consul1 consul agent -server -bootstrap -ui -client='0.0.0.0'
  • agent: 表示启动 agent 进程
  • server: 表示 consul 为 server 模式
  • client: 表示 consul 为 client 模式
  • bootstrap: 表示这个节点是 Server-Leader
  • ui: 启动 Web UI, 默认端口 8500
  • node: 指定节点名称, 集群中节点名称唯一
  • client: 绑定客户端接口地址, 0.0.0.0 表示所有地址都可以访问

4. 启动后,就可以访问您的服务器Ip+8500端口看到Consul控制台了,如图:

 

二、准备服务

1. 创建订单服务项目

2. 创建OrderController和HealthCheckController,用于显示订单信息和进行健康检查。

(1)OrderController代码如下:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Order.API.Controllers
{
[ApiController,Route("[controller]")]
public class OrderController:ControllerBase
{
private readonly ILogger<OrderController> _logger;
private readonly IConfiguration _configuration; public OrderController(ILogger<OrderController> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
} [HttpGet]
public IActionResult Get()
{
string result = $"【订单服务】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
$"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
return Ok(result);
}
}
}

  (2)HealthCheckController控制器代码如下:

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Order.API.Controllers
{
[Route("[controller]")]
[ApiController]
public class HealthCheckController : ControllerBase
{
/// <summary>
/// 健康检查接口
/// </summary>
/// <returns></returns>
[HttpGet]
public IActionResult Get()
{
return Ok();
}
}
}

  (3)创建ConsulHelper帮助类,用来向Consul(服务注册中心)进行服务注册。注意,先在Nuget中添加Consul类库。

using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Order.API.Helper
{
public static class ConsulHelper
{
/// <summary>
/// 服务注册到consul
/// </summary>
/// <param name="app"></param>
/// <param name="lifetime"></param>
public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime)
{
var consulClient = new ConsulClient(c =>
{
//consul地址
c.Address = new Uri(configuration["ConsulSetting:ConsulAddress"]);
}); var registration = new AgentServiceRegistration()
{
ID = Guid.NewGuid().ToString(),//服务实例唯一标识
Name = configuration["ConsulSetting:ServiceName"],//服务名称
Address = configuration["ConsulSetting:ServiceIP"], //服务所在宿主机IP
Port = int.Parse(configuration["ConsulSetting:ServicePort"] ?? "5000"),//服务端口 因为要运行多个实例,所以要在在docker容器启动时时动态传入 --ConsulSetting:ServicePort="port"
Check = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后注册
Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔
HTTP = $"http://{configuration["ConsulSetting:ServiceIP"]}:{configuration["ConsulSetting:ServicePort"]}{configuration["ConsulSetting:ServiceHealthCheck"]}",//健康检查地址
Timeout = TimeSpan.FromSeconds(5)//超时时间
}
}; //服务注册
consulClient.Agent.ServiceRegister(registration).Wait(); //应用程序终止时,取消注册
lifetime.ApplicationStopping.Register(() =>
{
consulClient.Agent.ServiceDeregister(registration.ID).Wait();
}); return app;
}
}
}

  (4)appsetting.json配置文件代码

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConsulSetting": {
"ServiceName": "order", //服务名称
"ServiceIP": "192.168.183.129", //服务所在宿主机的IP地址
"ServiceHealthCheck": "/healthcheck",//健康检查地址
"ConsulAddress": "http://192.168.183.129:8500/" //Consul注册中心所在宿主机的IP地址和端口
}
}

  (5)Program类代码如下

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Order.API
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
} public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseUrls("http://0.0.0.0:5000");//用来在服务器中监听外部请求的,0.0.0.0代表任何地址都可访问该服务,5000端口
webBuilder.UseStartup<Startup>();
});
}
}

启动项目测试,结果如下

2. 创建产品服务,内容跟Order服务一样,这里就不赘述了。注意为了区分两个服务,把ProductController中的输出信息做一下改变,如:把【订单服务】修改为【产品服务】

  (1)ProductController控制器代码

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Product.API.Controllers
{
[ApiController,Route("[controller]")]
public class ProductController:ControllerBase
{
private readonly ILogger<ProductController> _logger;
private readonly IConfiguration _configuration; public ProductController(ILogger<ProductController> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
} [HttpGet]
public IActionResult Get()
{
string result = $"【产品服务】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
$"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
return Ok(result);
}
}
}

  (2)appsetting.json代码

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConsulSetting": {
"ServiceName": "product",//注意这里的服务名称跟Order不一样
"ServiceIP": "192.168.183.129",
"ServiceHealthCheck": "/healthcheck",
"ConsulAddress": "http://192.168.183.129:8500/"
}
}

  (3)运行测试,输出内容跟Order服务大体一致。

二、部署到Docker容器(以Order服务为例)

  1. 创建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:3.1 AS base
WORKDIR /app
EXPOSE 5000
COPY . .
ENTRYPOINT ["dotnet", "Order.API.dll"]

  2. 发布项目

3. 发布完成之后将发布以后的文件目录放到服务器上,并将DockerFile文件放入根目录,如图

4. 构建Order服务的镜像

在Order服务的根目录下,执行docker命令,

docker build -t orderapi .

构建完成后,启动一个docker容器同时启动我们已经拷贝过来的Order服务,启动命令如下(以下启动了三个容器,分别为9050、9051、9052端口,都映射到了容器的5000端口)

docker run -d -p 9050:5000 --name order1 orderapi --ConsulSetting:ServicePort="9050"
docker run -d -p 9051:5000 --name order2 orderapi --ConsulSetting:ServicePort="9051"
docker run -d -p 9052:5000 --name order3 orderapi --ConsulSetting:ServicePort="9052"

以上,9050:5000,表示将服务器的9050端口映射到容器的5000端口,--ConsulSetting:ServicePort="9050" 表示向服务动态传入端口,服务通过此端口来向Consul注册端口信息

5. 观察Consul控制台,如果5秒后,控制台出现了新的服务,并从红色变味了绿色,那么恭喜您,您的第一个服务就向注册中心注册成功了!

    点击Order服务,您会发现有3个服务实例,这就是您刚刚用容器启动的三个服务实例了。

                                                                                       

同理,再次部署Product服务,注意,Product的服务端口要改为别的端口,比如9060,不能跟Order重复了!

docker命令如下:

docker build -t productapi .
docker run -d -p 9060:5000 --name product1 productapi --ConsulSetting:ServicePort="9060"
docker run -d -p 9061:5000 --name product2 productapi --ConsulSetting:ServicePort="9061"
docker run -d -p 9062:5000 --name product3 productapi --ConsulSetting:ServicePort="9062"

6. 至此,我们所需要的2个服务,6个实例已经都部署好了,那么我们在请求接口时,就会有6个请求地址:

http://ip:9050/order

http://ip:9051/order

http://ip:9052/order

http://ip:9060/product

http://ip:9061/product

http://ip:9062/product

那么我们mvc项目在请求接口的时候不可能挨个请求啊,这也不太现实,这时候,我么则需要一个网关Ocelot。有了Ocelot,我们只需要请求网关地址就可以了,网关就会根据我们请求的地址,自动匹配下游的服务。

比如,网关地址为http://ip:5000,那么当我们请求http://ip:5000/oder的时候,网关就会匹配路由,自动请求http://ip:9050/order、http://ip:9051/order、http://ip:9052/order的任意一个地址。

当然,这几个地址是通过Ocelot与Consul的集成实现的,Ocelot会自动获取Consul中已经存在的服务的地址,供路由进行自动匹配。

下面,我们来搭建网关。

三、搭建网关

1. 新建空的Web项目,如图:

2. 添加Ocelot和Consul的类库,如图

3. 添加代码

  (1)startup类的代码

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Consul;
using Ocelot.Provider.Polly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Ocelot.APIGateway
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddOcelot()
.AddConsul()//集成Consul服务发现
.AddPolly();//添加超时/熔断服务 } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseOcelot().Wait();
}
}
}

  (2)添加ocelot.json配置文件,代码如下

{
"ReRoutes": [
{
"DownstreamPathTemplate": "/order",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/order",
"UpstreamHttpMethod": [ "GET", "POST" ],
"ServiceName": "order", //Consul服务名称
"RateLimitOptions": { //限流
"ClientWhitelist": [ "SuperClient" ], //白名单,不受限
"EnableRateLimiting": true,
"Period": "5s", //1s,5m,1h,1d等
"PeriodTimespan": 2,
"Limit": 1
},
"QoSOptions": { //超时/熔断配置
"ExceptionsAllowedBeforeBreaking": 3, //代表发生错误的次数
"DurationOfBreak": 10000, //代表熔断时间
"TimeoutValue": 5000 //代表超时时间
}
},
{
"DownstreamPathTemplate": "/product",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/product",
"UpstreamHttpMethod": [ "GET", "POST" ],
"ServiceName": "product", //Consul服务名称
"RateLimitOptions": { //限流
"ClientWhitelist": [ "SuperClient" ], //白名单,不受限
"EnableRateLimiting": true,
"Period": "5s", //1s,5m,1h,1d等
"PeriodTimespan": 2,
"Limit": 1 //最重要的就是Period,PeriodTimespan,Limit这几个配置。
},
"QoSOptions": { //超时/熔断配置
"ExceptionsAllowedBeforeBreaking": 3, //代表发生错误的次数
"DurationOfBreak": 10000, //代表熔断时间
"TimeoutValue": 5000 //代表超时时间
}
}
],
"GlobalConfiguration": {
"BaseUrl": "http://192.168.183.129:5000", //用来访问服务的地址
"ServiceDiscoveryProvider": {//Consul服务发现,用来获取Consul的服务地址的
"Scheme": "http",
"Host": "192.168.183.129",//Consul的IP地址
"Port": 8500,//端口
"Type": "Consul"
},
"RateLimitOptions": { //限流
"DisableRateLimitHeaders": false, //代表是否禁用X-Rate-Limit和Retry-After标头(请求达到上限时response header中的限制数和多少秒后能重试)
"QuotaExceededMessage": "too many requests...", //代表请求达到上限时返回给客户端的消息
"HttpStatusCode": 999, //代表请求达到上限时返回给客户端的HTTP状态代码
"ClientIdHeader": "Test" //可以允许自定义用于标识客户端的标头。默认情况下为“ ClientId”
}
}
}

  (3)Startup.cs类代码

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Consul;
using Ocelot.Provider.Polly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Ocelot.APIGateway
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddOcelot()
.AddConsul()//集成Consul服务发现
.AddPolly();//添加超时/熔断服务 } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseOcelot().Wait();
}
}
}

  (4)Program.cs类代码如下

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Ocelot.APIGateway
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
} public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) => {
config.AddJsonFile("ocelot.json");
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

  (5)启动网关项目,进行测试,结果如下

至此,网关搭建完成,之后,我们的MVC项目只需要访问网关的地址进行接口调用就可以了。

四、新建MVC项目,进行调用模拟

1. 新建空MVC项目并添加代码,如图

2. 添加代码

  (1)HomeController代码

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Web.MVC.Helper; namespace Web.MVC.Controllers
{
public class HomeController:Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IServiceHelper _serviceHelper; public HomeController(ILogger<HomeController> logger, IServiceHelper serviceHelper)
{
_logger = logger;
_serviceHelper = serviceHelper;
} public async Task<IActionResult> Index()
{
ViewBag.OrderData = await _serviceHelper.GetOrder();
ViewBag.ProductData = await _serviceHelper.GetProduct();
return View();
}
}
}

  (2)IServiceHelper、ServiceHelper代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Web.MVC.Helper
{
public interface IServiceHelper
{
/// <summary>
/// 获取产品数据
/// </summary>
/// <returns></returns>
Task<string> GetProduct(); /// <summary>
/// 获取订单数据
/// </summary>
/// <returns></returns>
Task<string> GetOrder();
}
}
using Consul;
using Microsoft.Extensions.Configuration;
using RestSharp;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Web.MVC.Helper
{
public class ServiceHelper : IServiceHelper
{
public async Task<string> GetOrder()
{
var Client = new RestClient("http://ip:5000");//此处指的是网关项目的地址
var request = new RestRequest("/order", Method.GET);
var response = await Client.ExecuteAsync(request);
return response.Content;
} public async Task<string> GetProduct()
{
var Client = new RestClient("http://ip:5000");//此处指的是网关的地址
var request = new RestRequest("/product", Method.GET);
var response = await Client.ExecuteAsync(request);
return response.Content;
}
}
}

  (3)Home/Index.cshtml的代码

@{
ViewData["Title"] = "Home Page";
} <div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>
@ViewBag.OrderData
</p>
<p>
@ViewBag.ProductData
</p>
</div>

  (4)Startup.cs中在ConfigureServices中,添加代码

services.AddSingleton<IServiceHelper, ServiceHelper>();

  (5)设置应用的访问地址

  

3. 启动项目,查看结果

 访问成功,至此,我们就把一个简单版的微服务搭建起来了!

看起来步骤繁琐,其实只是本人记录的比较详细而已,实操完成后并没有那么多东西!下图为本项目的基本架构:

好了,今天就到这里,下班!

.Net Core 3.1简单搭建微服务的更多相关文章

  1. 手把手教你使用spring cloud+dotnet core搭建微服务架构:服务治理(-)

    背景 公司去年开始使用dotnet core开发项目.公司的总体架构采用的是微服务,那时候由于对微服务的理解并不是太深,加上各种组件的不成熟,只是把项目的各个功能通过业务层面拆分,然后通过nginx代 ...

  2. spring cloud+dotnet core搭建微服务架构:Api授权认证(六)

    前言 这篇文章拖太久了,因为最近实在太忙了,加上这篇文章也非常长,所以花了不少时间,给大家说句抱歉.好,进入正题.目前的项目基本都是前后端分离了,前端分Web,Ios,Android...,后端也基本 ...

  3. spring cloud+.net core搭建微服务架构:服务注册(一)

    背景 公司去年开始使用dotnet core开发项目.公司的总体架构采用的是微服务,那时候由于对微服务的理解并不是太深,加上各种组件的不成熟,只是把项目的各个功能通过业务层面拆分,然后通过nginx代 ...

  4. spring cloud+.net core搭建微服务架构:Api授权认证(六)

    前言 这篇文章拖太久了,因为最近实在太忙了,加上这篇文章也非常长,所以花了不少时间,给大家说句抱歉.好,进入正题.目前的项目基本都是前后端分离了,前端分Web,Ios,Android...,后端也基本 ...

  5. spring cloud+dotnet core搭建微服务架构:服务发现(二)

    前言 上篇文章实际上只讲了服务治理中的服务注册,服务与服务之间如何调用呢?传统的方式,服务A调用服务B,那么服务A访问的是服务B的负载均衡地址,通过负载均衡来指向到服务B的真实地址,上篇文章已经说了这 ...

  6. spring cloud+dotnet core搭建微服务架构:Api网关(三)

    前言 国庆假期,一直没有时间更新. 根据群里面的同学的提问,强烈推荐大家先熟悉下spring cloud.文章下面有纯洁大神的spring cloud系列. 上一章最后说了,因为服务是不对外暴露的,所 ...

  7. spring cloud+dotnet core搭建微服务架构:配置中心(四)

    前言 我们项目中有很多需要配置的地方,最常见的就是各种服务URL地址,这些地址针对不同的运行环境还不一样,不管和打包还是部署都麻烦,需要非常的小心.一般配置都是存储到配置文件里面,不管多小的配置变动, ...

  8. spring cloud+dotnet core搭建微服务架构:配置中心续(五)

    前言 上一章最后讲了,更新配置以后需要重启客户端才能生效,这在实际的场景中是不可取的.由于目前Steeltoe配置的重载只能由客户端发起,没有实现处理程序侦听服务器更改事件,所以还没办法实现彻底实现这 ...

  9. spring cloud+.net core搭建微服务架构:配置中心续(五)

    前言 上一章最后讲了,更新配置以后需要重启客户端才能生效,这在实际的场景中是不可取的.由于目前Steeltoe配置的重载只能由客户端发起,没有实现处理程序侦听服务器更改事件,所以还没办法实现彻底实现这 ...

随机推荐

  1. 并发王者课 - 青铜4:synchronized用法初体验

    在前面的文章<双刃剑-理解多线程带来的安全问题>中,我们提到了多线程情况下存在的线程安全问题.本文将以这个问题为背景,介绍如何通过使用synchronized关键字解这一问题.当然,在青铜 ...

  2. mysql登录框注入绕过单引号匹配

    0x00 原理   网站使用正则匹配对用户名一栏传到服务器的参数进行了匹配,如果匹配到了单引号则报错 0x01 简单例子   当我们输入admin'时,网站直接报错,很有可能就是用了正则,这样我们也不 ...

  3. SSH自动断开连接的原因-20200323

    SSH自动断开连接的原因   方法一: 用putty/SecureCRT连续3分钟左右没有输入, 就自动断开, 然后必须重新登陆, 很麻烦. 在网上查了很多资料, 发现原因有多种, 环境变量TMOUT ...

  4. 033.Python的__del__析构方法he__call__方法

    一 __del__ 魔术方法(析构方法) 1.1 介绍 触发时机:当对象被内存回收的时候自动触发[1.页面执行完毕回收所有变量 2.所有对象被del的时候] 功能:对象使用完毕后资源回收 参数:一个s ...

  5. UCOS 多任务系统中需要注意的地方 一个任务至少要有一些执行内容

    图片说明: 在一个TASK的最外层FOR();中  如果有调用BREAK会怎样??

  6. android格式化日期

    import android.text.format.DateFormat import java.util.* dateTextView.text = DateFormat.format(" ...

  7. ubuntu 20.04 编译安装 p 详解

    事情的起因 实验需要安装 p4 环境 我考虑到我自己的电脑性能不足,因此打算在本机安装 github上官方仓库的安装教程老旧,都是在 ubuntu14.04或者ubuntu16.04 我长时间用的li ...

  8. Python+Selenium自动化-设置浏览器大小、刷新页面、前进和后退

    Python+Selenium自动化-设置浏览器大小.刷新页面.前进和后退   1.设置浏览器大小 maximize_window():设置浏览器大小为全屏 set_window_size(500,5 ...

  9. K8S集群etcd备份与恢复

    参考链接: K8S集群多master:Etcd v3备份与恢复 K8S集群单master:Kubernetes Etcd 数据备份与恢复 ETCD系列之一:简介:https://developer.a ...

  10. 视觉SLAM技术应用

    视觉SLAM技术应用 SLAM技术背景 SLAM技术全称Simultaneous localization and mapping,中文为"同时定位与地图构建".SLAM可以在未知 ...