zoukankan      html  css  js  c++  java
  • ASP.NET Core中Ocelot的使用:API网关的应用

    在向微服务体系架构转型的过程中,我们都会毫不意外地遇到越来越多的现实问题,而这些问题却并不是因为功能性需求而引入的。比如,服务的注册与发现,是应用程序在云中部署、提供可伸缩支持的主要实现方案,在特定的微服务架构中,实践这样的云设计模式是利远远大于弊的。今我们需要讨论的API网关也是这样的一种微服务实现方案,它解决了客户端与服务端之间繁琐的通信问题。 在进一步讨论API网关在微服务架构中的应用前,先一起了解一下目前我手上的两个微服务:A服务提供了数学计算的API,B服务则提供了天气数据查询的API。两者看上去风马牛不相及,然而,B服务中的一个需求使得两者之间产生了密不可分的联系:B服务需要提供某些城市5年来平均气温的标准差,以便用户能够了解各个城市的气温变化情况。业务需求其实挺简单,从一个外部数据源将指定城市的平均气温数据读入,然后计算标准差即可,不过从实现上看,由于A服务已经提供了计算标准差的API,因此,B服务没有必要自己再写一套,只需要调用A服务提供的API即可,也就是A和B之间存在互相调用的情况。另一方面,对于用户来说,也存在需要同时调用A和B两组API的场景,因此,A和B的API也需要向外界曝露,这样一来应用程序的结构大致如下: image 也就是上图中B服务的/weather/stddev API将会用到A服务中的/calc/stddev这个API。OK,目前我们的应用程序结构还是比较简单,无非也就是调用几个API,既然如此,我们先用ASP.NET Core Web API把这个应用程序实现出来。

    使用ASP.NET Core实现整个应用程序

    先看看A服务的实现,它比较简单,在ASP.NET Core的项目上,新建一个控制器(Controller),然后曝露RESTful API向外界提供计算服务即可。控制器代码如下:
    [Route("api/[controller]")]
    [ApiController]
    public class CalcController : ControllerBase
    {
        [HttpGet("add/{x}/{y}")]
        public int Add(int x, int y) => x + y;
    
        [HttpGet("sub/{x}/{y}")]
        public int Sub(int x, int y) => x - y;
    
        [HttpPost("stddev")]
        public double StdDev([FromBody]float[] numbers) 
            => Math.Sqrt(numbers.Select(x => Math.Pow(x - numbers.Average(), 2)).Sum() / (numbers.Length - 1));
    }
    
    A服务其它部分的代码就不多说明了,都是标准做法,没有什么特别。再看看B服务,其实除了通过外部数据源获取指定城市5年来每天的平均气温数据,以及调用A服务计算标准差之外,也没有什么特别之处,主要代码如下:
    [Route("api/[controller]")]
    [ApiController]
    public class WeatherController : ControllerBase
    {
        const string StdDevCalculationApiURI = "http://localhost:49814/api/calc/stddev";
    
        static readonly Dictionary<string, List<Tuple<DateTime, float>>> weatherData;
    
        static WeatherController()
        {
            weatherData = JsonConvert.DeserializeObject<Dictionary<string, List<Tuple<DateTime, float>>>>(System.IO.File.ReadAllText("weather_data.json"));
        }
    
        [HttpGet("stddev/{city}")]
        public async Task<IActionResult> StdDevForCity(string city)
        {
            if (!weatherData.ContainsKey(city.ToUpper()))
            {
                return BadRequest($"城市'{city}'的气象数据不存在。");
            }
    
            using (var client = new HttpClient())
            {
                var response = await client.PostAsJsonAsync(StdDevCalculationApiURI, weatherData[city.ToUpper()].Select(x => x.Item2).ToArray());
                response.EnsureSuccessStatusCode();
                return Ok(Convert.ToDouble(await response.Content.ReadAsStringAsync()));
            }
        }
    }
    
    为了便于演示,我已经预先将气温数据序列化成一个JSON文件,所以在上面的代码中,WeatherController在初次被引用时,会将气温数据从JSON文件中反序列化出来,并读入到weatherData字典中。在StdDevForCity的方法中可以看到,此API通过HttpClient向A服务发起了计算标准差的请求,并将平均气温数据以Post Body的形式代入到发出的请求中。 接下来,我们的前端页面需要使用A服务进行一些纯粹的数学计算,并且使用B服务为用户提供一些天气数据的统计信息。为了简化描述,这个前端应用我也采用ASP.NET Core来实现。使用Visual Studio创建了一个ASP.NET Core MVC的应用程序,然后,添加一个API的页面,在HomeController中,使用如下代码为API页面提供数据模型:
    public class HomeController : Controller
    {
        const string CalculationServiceUri = "http://localhost:49814";
        const string WeatherServiceUri = "http://localhost:50379";
    
        public async Task<IActionResult> API()
        {
            using (var client = new HttpClient())
            {
                // 调用计算服务,计算两个整数的和与差
                const int x = 124, y = 134;
                var sumResponse = await client.GetAsync($"{CalculationServiceUri}/api/calc/add/{x}/{y}");
                sumResponse.EnsureSuccessStatusCode();
                var sumResult = await sumResponse.Content.ReadAsStringAsync();
                ViewData["sum"] = $"x + y = {sumResult}";
    
                var subResponse = await client.GetAsync($"{CalculationServiceUri}/api/calc/sub/{x}/{y}");
                subResponse.EnsureSuccessStatusCode();
                var subResult = await subResponse.Content.ReadAsStringAsync();
                ViewData["sub"] = $"x - y = {subResult}";
    
                // 调用天气服务,计算大连和广州的平均气温标准差
                var stddevShenyangResponse = await client.GetAsync($"{WeatherServiceUri}/api/weather/stddev/shenyang");
                stddevShenyangResponse.EnsureSuccessStatusCode();
                var stddevShenyangResult = await stddevShenyangResponse.Content.ReadAsStringAsync();
                ViewData["stddev_sy"] = $"沈阳:{stddevShenyangResult}";
    
                var stddevGuangzhouResponse = await client.GetAsync($"{WeatherServiceUri}/api/weather/stddev/guangzhou");
                stddevGuangzhouResponse.EnsureSuccessStatusCode();
                var stddevGuangzhouResult = await stddevGuangzhouResponse.Content.ReadAsStringAsync();
                ViewData["stddev_gz"] = $"广州:{stddevGuangzhouResult}";
            }
            return View();
        }
    }
    
    然后,使用下面的视图(View)将数据模型显示给最终用户:
    @{
        ViewData["Title"] = "API";
    }
    <h2>API</h2>
    <h3>计算服务调用测试</h3>
    <p>
       @ViewData["sum"]
    </p>
    <p>
        @ViewData["sub"]
    </p>
    <h3>天气服务调用测试</h3>
    <h4>
        2013年1月1日至2018年10月26日各城市平均气温标准差为:
    </h4>
    <ul>
        <li>@ViewData["stddev_sy"]</li>
        <li>@ViewData["stddev_gz"]</li>
    </ul>
    
    代码都很简单,将A服务、B服务和这个前端应用程序都运行起来之后,就可以通过ASP.NET Core MVC主页的API菜单,打开API的测试页面。可以看到,2013年1月1日至2018年10月26日这段时间,沈阳的日平均气温变化量要远大于广州的日平均气温变化量。当然,这是合理的,因为南方一年四季的温差确实要比北方小。 image 在进行代码审核(Code Review)的过程中,你会发现,无论是B服务的实现还是前端页面的实现,我们都将所需调用的API地址通过const关键字写死在代码里,你肯定会指出这并不是一个好的做法,正确的做法应该是将这个API地址写在配置文件里,然后在代码中动态读取。OK,非常好的建议,不过,在云架构设计中,使用配置文件来指定所调服务的地址和端口,也不是一个很好的做法,这一点我们今后讨论。 OK,那么我们将API地址都写到配置文件中,然后在需要调用的时候将API的地址读入,我们可以根据《ASP.NET Core应用程序的参数配置及使用》一文中所描述的方法来实现这一步。不过,这还不是今天的重点。今天的重点是,你会发现,就前端页面而言,它需要为两个不同的服务指定API的URI,此时,你就会感觉到,这种做法是否真的合理:
    1. 在一个微服务架构的应用程序中,业务逻辑根据界定上下文分隔,并在不同的微服务中实现。所以,这样的应用程序通常会有几个、十几个甚至几十个微服务在运行、在调度、在伸缩。我们一个小小的演示案例就牵涉到了两个服务之间的互相调用,以及一个前端页面需要同时依赖两个服务的情况,更不用说在真实的应用中。比如,曾经有过报道,亚马逊商品信息页面后端就牵涉到几十个微服务,如果让前端页面对这几十个后端API进行逐个调用,就需要在前端应用中配置几十个API的地址,这样维护起来会非常麻烦而且容易出错
    2. 前端直接访问后端的API,会增加客户端与服务端之间的网络负载,比如:前端页面如果发出上百个API请求,那么就会有上百个来回于客户端和服务端的网络传输;但如果我们能够在服务端完成这些API的调用,并将结果组合起来一次发给客户端,那么网络传输就会变得更加高效
    3. 前端直接访问后端的API不利于微服务的治理和重构。比如:微服务容器重启之后,内部IP地址会发生变化,如果强依赖于IP,那么所有前端页面都需要更改。在今后维护后端代码时候,也会有可能对API的URI进行重构,也会导致URI维护出现困难和错误
    4. 前端直接访问后端的API,使得后端API不得不将访问接口曝露到公共域,而这样就加大了整个系统被攻击的可能性。比如,攻击A服务不成功,我可以尝试攻击B服务,一旦有一个服务受到攻击,就会对整个应用程序造成影响,带来安全隐患
    5. 与非功能性需求相关的技术实现,比如缓存、身份认证等等,都需要在每个微服务上进行实现,而绝大多数情况下,这些实现都是非常类似的,无需重复实现,如果在前端页面(客户端)与服务端之间有另外一层能够处理这部分逻辑,那么,后端微服务的实现就会得到简化
    接下来,我们就使用ASP.NET Core下的API网关:Ocelot来解决这些问题。

    在ASP.NET Core应用程序中使用Ocelot API网关

    API网关是这样一种机制,它能够向服务端外界提供访问内部服务的统一接口,并且提供一定的负载均衡、安全认证、缓存等应用特性。在微服务实践领域,API网关可以有很多种实现,比如可以使用Nginx,这也是我在《ASP.NET Core应用程序容器化、持续集成与Kubernetes集群部署(一)》一文中介绍的案例tasklist所使用的一种方式。在这里,我还是主要介绍ASP.NET Core下API网关的一种实现:Ocelot,其实,网上介绍Ocelot的文章已经有很多了,在这里,我只想由浅入深地将微服务实践中API网关的应用进行一个系统性的探讨,在接下来的文章中,我还会结合Ocelot对微服务的注册、发现以及容器部署进行更深入的实践。另外值得一提的是,国内.NET开源先锋,张善友队长也是Ocelot项目的贡献者之一。 在使用Ocelot之前,先了解一下,上述的应用程序架构会产生哪些变化。最直接的结果就是,在API用户与A、B两个服务之间,会多出API网关这一层,整体结构会是下面这个样子: image 现在开始改造我们的应用程序,来看看如何在ASP.NET Core的应用程序中使用Ocelot实现上述架构。 首先,新建一个空的ASP.NET Core应用程序,这里强调“空的”,意味着我们不需要实现任何控制器(Controller),只需要借用ASP.NET Core的运行机制即可。创建空的ASP.NET Core应用程序是指,在下面的对话框中,选择Empty模板: image 然后,添加Ocelot NuGet包的引用,可以通过Install-Package命令行,也可以在Visual Studio中右键点击刚刚新建的项目,选择Manage NuGet Packages选项,具体步骤就不说明了。添加完成后,就会在项目依赖项的NuGet节点下,看到Ocelot的引用: image 接下来,在项目中添加一个配置Ocelot的JSON文件,我们就用ocelot.configuration.json吧,内容如下:
    {
      "ReRoutes": [
        {
          "DownstreamPathTemplate": "/api/calc/add/{x}/{y}",
          "DownstreamScheme": "http",
          "DownstreamHostAndPorts": [
            {
              "Host": "localhost",
              "Port": 49814
            }
          ],
          "UpstreamPathTemplate": "/add/{x}/{y}",
          "UpstreamHttpMethod": [ "Get" ]
        },
        {
          "DownstreamPathTemplate": "/api/calc/sub/{x}/{y}",
          "DownstreamScheme": "http",
          "DownstreamHostAndPorts": [
            {
              "Host": "localhost",
              "Port": 49814
            }
          ],
          "UpstreamPathTemplate": "/sub/{x}/{y}",
          "UpstreamHttpMethod": [ "Get" ]
        },
        {
          "DownstreamPathTemplate": "/api/calc/stddev",
          "DownstreamScheme": "http",
          "DownstreamHostAndPorts": [
            {
              "Host": "localhost",
              "Port": 49814
            }
          ],
          "UpstreamPathTemplate": "/stddev",
          "UpstreamHttpMethod": [ "Post" ]
        },
        {
          "DownstreamPathTemplate": "/api/weather/stddev/{city}",
          "DownstreamScheme": "http",
          "DownstreamHostAndPorts": [
            {
              "Host": "localhost",
              "Port": 50379
            }
          ],
          "UpstreamPathTemplate": "/weather-stddev/{city}",
          "UpstreamHttpMethod": [ "Get" ]
        }
      ],
      "GlobalConfiguration": {
        "RequestIdKey": "OcRequestId",
        "AdministrationPath": "/administration"
      }
    }
    
    在ReRoutes部分定义了URL重定向的规则,比如,当Ocelot服务接收到/sub/{x}/{y}的Get请求时,它就会把请求转发到http://localhost:49814/api/calc/sub/{x}/{y}上。目前Ocelot的配置还是相对简单的,仅定义了一些重定向的规则,也没有使用Ocelot的一些高级功能。不过,这些设定对于我们实现上面的软件架构已经绰绰有余了。网上有很多文章介绍Ocelot的配置文件,官网也有详细说明,这里就不多解释了。 然后,修改Program.cs文件,修改WebHostBuilder的构建过程:
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }
    
        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((configBuilder) =>
            {
                configBuilder.AddJsonFile("ocelot.configuration.json");
            })
            .ConfigureServices((buildContext, services) =>
            {
                services.AddOcelot();
            })
            .UseStartup<Startup>()
            .Configure(app =>
            {
                app.UseOcelot().Wait();
            });
    }
    
    注意上面代码的高亮部分,基本上就是将配置文件载入,然后加入Ocelot的Middleware即可。OK,就这么简单,我们的API网关就完成了! 接下来的事情就显而易见了,将前面所介绍的前端MVC的代码,从分别向A、B两个服务发出请求,改为仅向API网关发出请求即可。例如,可以将MVC中HomeController的代码改造如下:
    public class HomeController : Controller
    {
        const string ServiceUri = "http://localhost:59495";
    
        public async Task<IActionResult> API()
        {
            using (var client = new HttpClient())
            {
                // 调用计算服务,计算两个整数的和与差
                const int x = 124, y = 134;
                var sumResponse = await client.GetAsync($"{ServiceUri}/add/{x}/{y}");
                sumResponse.EnsureSuccessStatusCode();
                var sumResult = await sumResponse.Content.ReadAsStringAsync();
                ViewData["sum"] = $"x + y = {sumResult}";
    
                var subResponse = await client.GetAsync($"{ServiceUri}/sub/{x}/{y}");
                subResponse.EnsureSuccessStatusCode();
                var subResult = await subResponse.Content.ReadAsStringAsync();
                ViewData["sub"] = $"x - y = {subResult}";
    
                // 调用天气服务,计算大连和广州的平均气温标准差
                var stddevShenyangResponse = await client.GetAsync($"{ServiceUri}/weather-stddev/shenyang");
                stddevShenyangResponse.EnsureSuccessStatusCode();
                var stddevShenyangResult = await stddevShenyangResponse.Content.ReadAsStringAsync();
                ViewData["stddev_sy"] = $"沈阳:{stddevShenyangResult}";
    
                var stddevGuangzhouResponse = await client.GetAsync($"{ServiceUri}/weather-stddev/guangzhou");
                stddevGuangzhouResponse.EnsureSuccessStatusCode();
                var stddevGuangzhouResult = await stddevGuangzhouResponse.Content.ReadAsStringAsync();
                ViewData["stddev_gz"] = $"广州:{stddevGuangzhouResult}";
            }
            return View();
        }
    }
    
    怎么,现在改为将API网关的URL地址hard code在代码里了?暂时还是先别纠结URL是hard code还是在配置文件中,后面的文章中我会逐渐改善这部分代码的。

    测试一下

    现在,我们把A服务、B服务、API网关全部运行起来,在浏览器中输入http://localhost:59495/add/2/3,可以看到,结果被正确输出: image 查看日志输出,发现我们的服务请求已经被成功重定向: image 再次刷新前端页面,结果没变: image

    总结

    本文首先介绍了一下微服务实践的挑战以及API网关的必要性,然后介绍了如何在ASP.NET Core应用程序中使用Ocelot实现API网关,并提供了一套完整的案例。虽然网上也有很多介绍Ocelot的文章,但本文随后的内容会从微服务架构的设计与实现的角度,将API网关的内容进一步深化,比如本文之前提到的URL hard code的问题,在后续的文章讨论中都会慢慢地得到解决。

    源码下载

    【请点击此处下载本文相关的源代码】 也可以访问我的Github页面下载本文案例代码,地址:https://github.com/daxnet/ocelot-sample/releases/tag/chapter_1
  • 相关阅读:
    JS播放视频代码
    kubernetes系列(小知识):kubectl命令自动补全
    docker(部署常见应用):docker部署mysql
    docker(部署常见应用):docker部署nginx
    docker(二):CentOS安装docker
    docker(一):docker是什么?
    kubernetes系列:(二)、kubernetes部署mysql(单节点)
    越早明白越好的人生道理(转载)
    JetBrains系列IDE快捷键大全(转载)
    spring-boot系列:(一)整合dubbo
  • 原文地址:https://www.cnblogs.com/daxnet/p/12941752.html
Copyright © 2011-2022 走看看