在这篇文章中,我们将快速了解一下服务发现是什么,使用Consul在ASP.NET Core MVC框架中,并结合DnsClient.NET实现基于Dns的客户端服务发现

这篇文章的所有源代码都可以在GitHub上Demo项目获得.

Service Discovery

在现代微服务架构中,服务可以在容器中运行,并且可以动态启动,停止和扩展。 这导致了一个非常动态的托管环境,可能有数百个实际端点,无法手动配置或找到正确的端点。

话虽这么说,我相信服务发现不仅适用于生活在容器中的粒状微服务。它可以被任何必须访问其他资源的应用程序使用。资源可以是数据库,其他Web服务,也可以是托管在其他地方的网站的一部分。服务发现有助于摆脱特定于环境的配置文件!

服务发现可用于解决此问题,但通常,有许多不同的方法来实现它

  • 客户端服务发现

    一种解决方案是拥有一个中央服务注册表,其中所有服务实例都在这里注册。客户端必须实现逻辑以查询他们需要的服务,最终验证端点是否仍然存活并且可能将请求分发到多个端点。
  • 服务器端/负载平衡

    所有流量都通过负载均衡器,负载均衡器知道所有实际的,动态变化的端点,并相应地重定向所有请求

Consul是一个服务注册表,可用于实现客户端服务发现。

除了使用这种方法的许多强大功能和优点之外,它的缺点是每个客户端应用程序都需要实现一些逻辑来使用此中央注册表。这个逻辑可能非常具体,因为Consul和任何其他技术都有自定义API和逻辑工作方式。

负载平衡也可能无法自动完成。客户端可以查询服务的所有可用/已注册端点,然后决定选择哪个端点。

好的是Consul不仅带有REST API来查询服务注册表,它还提供DNS端点,返回标准SRV和TXT记录。

DNS端点确实关心服务运行状况,因为它不会返回不健康的服务实例。它还通过以交替顺序返回记录来进行负载平衡! 此外,它可能使服务具有更高的优先级,更接近客户端。

现在,让我们开始......

Consul 安装

Consul是由HashiCorp开发的软件,它不仅提供服务发现(如上所述),还提供“健康检查”,并提供分布式“密钥值存储”。

Consul旨在一个集群中运行,至少有三个实例处理集群环境中每个节点上的集群和“代理”的协调。应用程序始终只与本地代理通信,这使得通信速度非常快,并将网络延迟降至最低。

但是,对于本地开发,您可以在--dev模式下运行Consul,而不是设置完整集群。 但是请记住这一点,为了生产使用,需要做一些工作才能正确设置Consul。

下载和运行Consul

官方文档有很多例子,并且很好地解释了如何设置Consul。我不会详细介绍,我们只是将它作为本地开发代理运行。

要开始使用,请下载Consul

使用consul agent --dev命令和参数来运行启动Consul,这将在本地服务模式下启动Consul而无需配置文件,并且只能在localhost上访问。

访问http://localhost:8500 ,这应该可以打开Consul UI

注册第一个服务

Consul提供了添加或修改服务注册表的不同方法。一种选择是将JSON配置文件放入Consul的config目录中。下面的例子将注册一个Redis服务:

  1. {
  2. "service":{
  3. "name": "redis",
  4. "tags":[],
  5. "port": 6379
  6. }
  7. }

另一个更有趣的选择是通过REST API。幸运的是,已有许多语言的客户端库可用于此REST API,我们将使用https://github.com/PlayFab/consuldotnet,.Net Core也可以使用

要通过代码注册新服务,请创建一个新的ConsulClient实例并注册新的服务注册

  1. var client = new ConsulClient(); // uses default host:port which is localhost:8500
  2. var agentReg = new AgentServiceRegistration()
  3. {
  4. Address = "127.0.0.1",
  5. ID = "uniqueid",
  6. Name = "serviceName",
  7. Port = 5200
  8. };
  9. await client.Agent.ServiceRegister(agentReg);

重要的是要注意,即使服务不再运行,该注册理论上也将永远存在于Consul集群中。

  1. await client.Agent.ServiceDeregister("uniqueid");

如果服务崩溃,则可能无法始终手动取消注册服务。这就是Consul的另一个特色:健康检查。

健康检查 Health Checks

Consul中的监控检查可用于监视群集中的所有服务的状态,还可以从Consul注册表中自动删除不健康的服务端点注册。可以将Consul配置为根据需要定期为每个注册服务运行尽可能多的运行状况检查。

最基本的健康检查让Consul尝试通过TCP连接到服务:

  1. var tcpCheck = new AgentServiceCheck()
  2. {
  3. DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
  4. Interval = TimeSpan.FromSeconds(30),
  5. TCP = $"127.0.0.1:{port}"
  6. };

Consul还可以检查HTTP端点。在这种情况下,只要端点返回HTTP状态代码200,服务就是健康的。

一个非常简单的健康检查控制器可以像这样实现:

  1. [Route("[Controller]")]
  2. public class HealthCheckController : Controller
  3. {
  4. [HttpGet("")]
  5. [HttpHead("")]
  6. public IActionResult Ping()
  7. {
  8. return Ok();
  9. }
  10. }

在这次注册中,我们现在必须通过指定AgentServiceCheck的Http属性而不是Tcp属性来将Consul指向该节点:

  1. var httpCheck = new AgentServiceCheck()
  2. {
  3. DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
  4. Interval = TimeSpan.FromSeconds(30),
  5. HTTP = $"http://127.0.0.1:{port}/HealthCheck"
  6. };

更新之前注册代码,添加让Consul每30秒运行一次健康检查的部分。请注意,我还将检查配置为自动取消注册服务实例,以防它被标记为运行状况超过一分钟。

  1. var registration = new AgentServiceRegistration()
  2. {
  3. Checks = new[] { tcpCheck, httpCheck },
  4. Address = "127.0.0.1",
  5. ID = id,
  6. Name = name,
  7. Port = port
  8. };
  9. await client.Agent.ServiceRegister(registration);

这些基本示例应该足以开始。但是,运行健康检查可以执行更复杂的操作,Consul支持运行小脚本来验证响应。

Endpoint Name, ID and Port

您可能已经注意到,要注册服务,我们必须知道服务运行的实际端点(Endpoint),我们必须给它一个Name和一个ID

ID应该是足够唯一的字符串来标识服务实例,而Name应该是同一服务的所有实例的通用名称。

其他客户端将使用Name来查询服务注册表,该ID仅用于引用确切的实例,例如取消注册服务实例时。

但是我们如何定义名称和端口以及IP地址?

如果我们自己使用Kestrel托管ASP.NET Core应用程序很简单,因为我们还在哪个端口和地址上配置Kestrel。当使用IIS(或任何其他反向代理)托管服务时,这种方法会分崩离析,因为在反向代理模式下,Kestrel使用了动态配置,并且实际的托管信息无法在应用程序代码中使用。(译者注:IIS对外的端口和内部Kestrel的端口并不一致)

要了解如何使用Kestrel托管它,让我们创建一个空的ASP.NET Core web api项目。

运行dotnet new webapi或在Visual Studio中使用WebAPI模板。

这将创建一个Program.cs和Startup.cs。 修改Program.cs以创建主机。我们将使用host.Start而不是host.Run,它不会阻塞线程。之后,我们将注册该服务并在服务停止时取消注册:

  1. var host = new WebHostBuilder()
  2. .UseKestrel()
  3. .UseUrls("http://localhost:5200")
  4. .UseContentRoot(Directory.GetCurrentDirectory())
  5. .UseStartup<Startup>()
  6. .Build();
  7. host.Start();
  8. var client = new ConsulClient();
  9. var name = Assembly.GetEntryAssembly().GetName().Name;
  10. var port = 5200;
  11. var id = $"{name}:{port}";
  12. var tcpCheck = new AgentServiceCheck()
  13. {
  14. DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
  15. Interval = TimeSpan.FromSeconds(30),
  16. TCP = $"127.0.0.1:{port}"
  17. };
  18. var httpCheck = new AgentServiceCheck()
  19. {
  20. DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
  21. Interval = TimeSpan.FromSeconds(30),
  22. HTTP = $"http://127.0.0.1:{port}/HealthCheck"
  23. };
  24. var registration = new AgentServiceRegistration()
  25. {
  26. Checks = new[] { tcpCheck, httpCheck },
  27. Address = "127.0.0.1",
  28. ID = id,
  29. Name = name,
  30. Port = port
  31. };
  32. client.Agent.ServiceRegister(registration).GetAwaiter().GetResult();
  33. Console.WriteLine("DataService started...");
  34. Console.WriteLine("Press ESC to exit");
  35. while (Console.ReadKey().Key != ConsoleKey.Escape)
  36. {
  37. }
  38. client.Agent.ServiceDeregister(id).GetAwaiter().GetResult();

并且(如果您已添加运行状况检查控制器),它将成功运行两个运行状况检查:

我使用程序集名称作为服务名称,我正在硬编码端口和IP地址。显然,这需要是可配置的,阻止控制台线程的解决方案也不是很好。

更复杂的方式

了解基础知识以及注册过程的工作原理,让我们稍微改进一下实现。

目标

  • 可以通过appsettings.json配置服务名称
  • 主机和端口不应该是硬编码的
  • 使用Microsoft.Extensions.Configuration和Options来正确配置我们需要的所有内容
  • 将注册设置为Startup管道的一部分

Configuration

我定义了一个新的POCOs的配置文件在appsetting.json文件中,如下所示:

  1. {
  2. ...
  3. "ServiceDiscovery": {
  4. "ServiceName": "DataService",
  5. "Consul": {
  6. "HttpEndpoint": "http://127.0.0.1:8500",
  7. "DnsEndpoint": {
  8. "Address": "127.0.0.1",
  9. "Port": 8600
  10. }
  11. }
  12. }
  13. }

C#:

  1. public class ServiceDisvoveryOptions
  2. {
  3. public string ServiceName { get; set; }
  4. public ConsulOptions Consul { get; set; }
  5. }
  6. public class ConsulOptions
  7. {
  8. public string HttpEndpoint { get; set; }
  9. public DnsEndpoint DnsEndpoint { get; set; }
  10. }
  11. public class DnsEndpoint
  12. {
  13. public string Address { get; set; }
  14. public int Port { get; set; }
  15. public IPEndPoint ToIPEndPoint()
  16. {
  17. return new IPEndPoint(IPAddress.Parse(Address), Port);
  18. }
  19. }

然后在Startup.ConfigureServices方法中进行配置:

  1. services.AddOptions();
  2. services.Configure<ServiceDisvoveryOptions>(Configuration.GetSection("ServiceDiscovery"));

使用此配置来设置consul客户端:

  1. services.AddSingleton<IConsulClient>(p => new ConsulClient(cfg =>
  2. {
  3. var serviceConfiguration = p.GetRequiredService<IOptions<ServiceDisvoveryOptions>>().Value;
  4. if (!string.IsNullOrEmpty(serviceConfiguration.Consul.HttpEndpoint))
  5. {
  6. // if not configured, the client will use the default value "127.0.0.1:8500"
  7. cfg.Address = new Uri(serviceConfiguration.Consul.HttpEndpoint);
  8. }
  9. }));

ConsulClient不一定需要配置,如果没有指定,它将使用默认地址(localhost:8500)。

动态服务注册

只要使用Kestrel在某个端口上托管服务,就可以使用app.Properties["server.Features"]来确定托管服务的位置。如上所述,如果使用IIS集成或任何其他反向代理,此解决方案将不再起作用,并且必须使用服务可访问的实际端点来在Consul中注册服务,并且在启动期间无法获取该信息。

如果要将IIS集成与服务发现一起使用,请不要使用以下代码。而是通过配置配置端点,或手动注册服务。

无论如何,对于Kestrel,我们可以执行以下操作:获取URIs kestrel托管服务(这不适用于像UseUrls("*:5000")这样的通配符,然后循环地址以在Consul中注册所有地址:

  1. ublic void Configure(
  2. IApplicationBuilder app,
  3. IApplicationLifetime appLife,
  4. ILoggerFactory loggerFactory,
  5. IOptions<ServiceDisvoveryOptions> serviceOptions,
  6. IConsulClient consul)
  7. {
  8. ...
  9. var features = app.Properties["server.Features"] as FeatureCollection;
  10. var addresses = features.Get<IServerAddressesFeature>()
  11. .Addresses
  12. .Select(p => new Uri(p));
  13. foreach (var address in addresses)
  14. {
  15. var serviceId = $"{serviceOptions.Value.ServiceName}_{address.Host}:{address.Port}";
  16. var httpCheck = new AgentServiceCheck()
  17. {
  18. DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
  19. Interval = TimeSpan.FromSeconds(30),
  20. HTTP = new Uri(address, "HealthCheck").OriginalString
  21. };
  22. var registration = new AgentServiceRegistration()
  23. {
  24. Checks = new[] { httpCheck },
  25. Address = address.Host,
  26. ID = serviceId,
  27. Name = serviceOptions.Value.ServiceName,
  28. Port = address.Port
  29. };
  30. consul.Agent.ServiceRegister(registration).GetAwaiter().GetResult();
  31. appLife.ApplicationStopping.Register(() =>
  32. {
  33. consul.Agent.ServiceDeregister(serviceId).GetAwaiter().GetResult();
  34. });
  35. }
  36. ...

serviceId必须足够独特,以便稍后再次找到该服务的特定实例,以取消注册它。我正在使用主机和端口以及实际的服务名称的连接方式,这应该足够好了。

这样我们就达到了所有的目标,虽然在启动的时候写了很多的代码,不过我们可以重构一下使用扩展方法来改善。

查询服务注册信息

新服务正在运行并在Consul中注册,现在应该很容易通过Consul API或DNS找到它。

使用Consul客户端查询

使用Consul客户端,我们可以使用两种Consul服务

  • 使用Catalog端点,它提供有关服务的原始信息,这个将返回未过滤的结果
  1. var consulResult = await _consul.Catalog.Service(_options.Value.ServiceName);
  • 使用Health端点,它将返回已经过滤过的结果
  1. var healthResult = await _consul.Health.Service(_options.Value.ServiceName, tag: null, passingOnly: true);

这里需要注意的重要一点是,这些端点返回的服务列表(如果多个实例正在运行)将始终采用相同的顺序。您必须实现逻辑,以便不会一直调用相同的服务端点,并在所有端点之间传播流量。

同样,这就是我们可以使用DNS的方式。除了建立负载平衡之外,优点还在于,我们不必再进行另一次昂贵的http调用,并且并且把最终结果缓存一小段时间。使用DNS,我们只需几行代码就可以实现这一切。

使用DNS查询

让我们用dig命令检查DNS端点,以了解响应的样子:

要求SRV记录的域名语法是<servicename>.consul.service,这意味着我们可以使用dig @127.0.0.1 -p 8600 dataservice.service.consul SRV查询我们的dataService

  1. ; <<>> DiG 9.11.0-P2 <<>> @127.0.0.1 -p 8600 dataservice.service.consul SRV
  2. ; (1 server found)
  3. ;; global options: +cmd
  4. ;; Got answer:
  5. ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25053
  6. ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
  7. ;; WARNING: recursion requested but not available
  8. ;; QUESTION SECTION:
  9. ;dataservice.service.consul. IN SRV
  10. ;; ANSWER SECTION:
  11. dataservice.service.consul. 0 IN SRV 1 1 5200 machinename.node.eu-west.consul.
  12. ;; ADDITIONAL SECTION:
  13. machinename.node.eu-west.consul. 0 IN CNAME localhost.
  14. ;; Query time: 0 msec
  15. ;; SERVER: 127.0.0.1#8600(127.0.0.1)
  16. ;; WHEN: Tue Apr 25 21:08:19 DST 2017
  17. ;; MSG SIZE rcvd: 109

我们获取SRV记录中的端口,相应的CNAME记录包含我们用于注册服务的主机名或地址.

Consul DNS端点还允许我们查询标签并限制查询仅查看一个特定的数据中心。 要查询标记,我们必须在标记和服务名称前加上_: _<tag>._<serviceName>.service.consul,要指定数据中心查询,将根域更改为<servicename>.service.<datacenter>.consul.

DNS负载均衡

DNS端点通过以交替顺序返回结果来执行负载均衡。如果我在另一个端口上启动另一个服务实例,我们得到:

  1. ;; QUESTION SECTION:
  2. ;dataservice.service.consul. IN SRV
  3. ;; ANSWER SECTION:
  4. dataservice.service.consul. 0 IN SRV 1 1 5200 machinename.node.eu-west.consul.
  5. dataservice.service.consul. 0 IN SRV 1 1 5300 machinename.node.eu-west.consul.
  6. ;; ADDITIONAL SECTION:
  7. machinename.node.eu-west.consul. 0 IN CNAME localhost.
  8. machinename.node.eu-west.consul. 0 IN CNAME localhost.

如果您运行查询几次,您将看到答案以不同的顺序返回。

使用DnsClient

要通过C#代码查询DNS,我将使用我的DnsClient库。我将ResolveService扩展方法添加到库中,以使这些SRV查找非常简单。

安装DnsClient NuGet包后,我只需在DI中注册一个DnsLookup客户端:

  1. services.AddSingleton<IDnsQuery>(p =>
  2. {
  3. return new LookupClient(IPAddress.Parse("127.0.0.1"), 8600);
  4. });
  1. private readonly IDnsQuery _dns;
  2. private readonly IOptions<ServiceDisvoveryOptions> _options;
  3. public SomeController(IDnsQuery dns, IOptions<ServiceDisvoveryOptions> options)
  4. {
  5. _dns = dns ?? throw new ArgumentNullException(nameof(dns));
  6. _options = options ?? throw new ArgumentNullException(nameof(options));
  7. }
  8. [HttpGet("")]
  9. [HttpHead("")]
  10. public async Task<IActionResult> DoSomething()
  11. {
  12. var result = await _dns.ResolveServiceAsync("service.consul", _options.Value.ServiceName);
  13. ...
  14. }

DnsClient.NETResolveServiceAsync执行DNS SRV查找,匹配CNAME记录并为包含主机名和端口(以及使用的地址)的每个条目返回一个对象。

现在,我们可以使用简单的HttpClient调用(或生成的客户端)来调用服务:

  1. var address = result.First().AddressList.FirstOrDefault();
  2. var port = result.First().Port;
  3. using (var client = new HttpClient())
  4. {
  5. var serviceResult = await client.GetStringAsync($"http://{address}:{port}/Values");
  6. }

结论

Consul是一个伟大,灵活和稳定的工具。我喜欢它的API和使用模式并不是固定的,你可以有很多选择来使用服务注册和其他功能。与此同时,它的性能表现也是非常优异。

在今天来说,因为有了众多的工具,在.NET中使用Consul也是非常简单方便。如果你的程序有不同部分需要通讯,那我确定它可以帮助你。

我在GitHub上整理了一个包含完整演示项目,把你的想法在评论中告诉我

原文地址:http://michaco.net/blog/ServiceDiscoveryAndHealthChecksInAspNetCoreWithConsul

Service Discovery And Health Checks In ASP.NET Core With Consul的更多相关文章

  1. Asp.net core 向Consul 注册服务

    Consul服务发现的使用方法:1. 在每台电脑上都以Client Mode的方式运行一个Consul代理, 这个代理只负责与Consul Cluster高效地交换最新注册信息(不参与Leader的选 ...

  2. 实战中的asp.net core结合Consul集群&Docker实现服务治理

    0.目录 整体架构目录:ASP.NET Core分布式项目实战-目录 一.前言 在写这篇文章之前,我看了很多关于consul的服务治理,但发现基本上都是直接在powershell或者以命令工具的方式在 ...

  3. ASP.NET CORE 使用Consul实现服务治理与健康检查(2)——源码篇

    题外话 笔者有个习惯,就是在接触新的东西时,一定要先搞清楚新事物的基本概念和背景,对之有个相对全面的了解之后再开始进入实际的编码,这样做最主要的原因是尽量避免由于对新事物的认知误区导致更大的缺陷,Bu ...

  4. asp.net core 和consul

    consul集群搭建 Consul是HashiCorp公司推出的使用go语言开发的开源工具,用于实现分布式系统的服务发现与配置,内置了服务注册与发现框架.分布一致性协议实现.健康检查.Key/Valu ...

  5. 【架构篇】ASP.NET Core 基于 Consul 动态配置热更新

    背景 通常,.Net 应用程序中的配置存储在配置文件中,例如 App.config.Web.config 或 appsettings.json.从 ASP.Net Core 开始,出现了一个新的可扩展 ...

  6. ASP.NET CORE 使用Consul实现服务治理与健康检查(1)——概念篇

    背景 笔者所在的公司正在进行微服务改造,这其中服务治理组件是必不可少的组件之一,在一番讨论之后,最终决定放弃 Zookeeper 而采用 Consul 作为服务治理框架基础组件.主要原因是 Consu ...

  7. Ubuntu & Docker & Consul & Fabio & ASP.NET Core 2.0 微服务跨平台实践

    相关博文: Ubuntu 简单安装 Docker Mac OS.Ubuntu 安装及使用 Consul Consul 服务注册与服务发现 Fabio 安装和简单使用 阅读目录: Docker 运行 C ...

  8. Docker & Consul & Fabio & ASP.NET Core 2.0 微服务跨平台实践

    相关博文: Ubuntu 简单安装 Docker Mac OS.Ubuntu 安装及使用 Consul Consul 服务注册与服务发现 Fabio 安装和简单使用 阅读目录: Docker 运行 C ...

  9. asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

    一.项目分析 在上篇中介绍了什么是"干净架构",DDD符合了这种干净架构的特点,重点描述了DDD架构遵循的依赖倒置原则,使软件达到了低藕合.eShopOnWeb项目是学习DDD领域 ...

随机推荐

  1. 【JDK】JDK模块化(1)-为什么要模块化

    Java9发布已经有一年了,跟Java8相比,从目录对比就看得出来差别相当大. 实际上Java9最大的变化就是JDK模块化(Modular). 那么,模块化的目的是什么呢? 官方的说法是: 之前版本的 ...

  2. java Swing组件和事件处理

    1.常见的容器 JComponent是 Container 的子类,中间容器必须添加到底层容器中才能够发挥作用, JPanel 面板 :使用jPanel 创建一个面板,再通过添加组件到该面板上面,JP ...

  3. Win#password;;processon #clone;;disassemble;;source find

    1.密码学思维导图 源地址:https://www.processon.com/view/5a61d825e4b0c090524f5b8b 在这之前给大家分享 如何在 processon上搜索公开克隆 ...

  4. python网络爬虫笔记(六)

    1.获取属性如果不存在就返回404,通过内置一系列函数,我们可以对任意python对象进行剖析,拿到其内部数据,但是要注意的是,只是在不知道对象信息的时候,我们可以获得对象的信息. 2.实例属性和类属 ...

  5. 饮冰三年-人工智能-linux-01通过VM虚拟机安装contes系统

    先决条件:VM虚拟机的安装.contes系统的镜像文件 1:创建新的虚拟机 2:下一步,稍后安装操作系统 3:选择对应的系统 4:选择对应的路径 至此虚拟机已经创建完成(相当于买了一台新电脑) 5:编 ...

  6. 正则 ?<= 和 ?= 用法,范例

    (exp) 匹配exp,并捕获文本到自动命名的组里(?<name>exp) 匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp)(?:exp) 匹配exp,不捕 ...

  7. Centos7+ASP.Net Core 运行

    一:ASP.Net Core跨平台运行,需要在Linux安装运行环境.本机器使用的Centos,下载安装地址为:https://www.microsoft.com/net/core#centos su ...

  8. Git reset与checkout的区别

    reset: 将暂存区的文件回撤到工作区,文件内容不会有任何变化 checkout: 将工作区文件恢复到上一次commit时的内容,将会丢失修改了但未加入暂存区的内容

  9. CSS3 伸缩布局盒模型

    CSS3引入的布局模式Flexbox布局,主要思想是让容器有能力让其子项目能够改变其宽度,高度,以最佳方式填充可用空间.Flex容器使用Flex项目可以自动放大与收缩,用来填补可用的空闲空间.更重要的 ...

  10. la 3938(未完成)

    题意:给出一个长度为n的整数序列D,你的任务是对m个询问作出回答.对于询问(a,b), 需要找到两个下标x和y,使得a≤x≤y≤b,并且Dx+Dx+1+...+Dy尽量大. 如果有多组满足条件的x和y ...