微服务之负载均衡示例

Last updated on 9 months ago

Nginx

下载 nginx: download

image-20220101205520096

配置Conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

#user nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

# 配置多个地址
upstream ServiceInstance{
server localhost:5726;
server localhost:5727;
server localhost:5728;
}

server {
# 监听端口 8080
listen 8080;

# 监听本地 可替换成IP
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
# root html;
# index index.html index.htm;

# 指向配置
proxy_pass http://ServiceInstance;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#}

}

创建应用

image-20220101205640901

image-20220101205703680

image-20220101205718852

image-20220101205813853

执行多个应用

image-20220101210214117

执行:

1
2
3
dotnet run urls="http://*:5726"
dotnet run urls="http://*:5727"
dotnet run urls="http://*:5728"

启动Nginx

1
D:\nginx-1.18.0\nginx-1.18.0>start nginx

停止Nginx

1
D:\nginx-1.18.0\nginx-1.18.0>nginx.exe -s stop

重启Nginx

1
nginx -s reload

访问流程

访问流程

访问结果

image-20220101211704858

最后

Nginx 不能伸缩

也就是再加一个端口,要把端口加进来让Nginx发现,需要修改配置并且重启Nginx,做不到热处理

看下面的Consul吧

Consul

Consul

Consul 服务注册

参考:.NET 5.0实现Consul服务注册 - 半山上的人 - 博客园 (cnblogs.com)

Consul 1.7.2

http://localhost:8500 1.7.2 长这样

image-20220102201451530

consul 1.11.1

http://localhost:8500 1.11.1 长这样

image-20220102211045421

API

appsetting.json 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConsulConfig": {
"ServiceId": "d72e7de8b01a43acac640b1a00b26c81",//可以换成Guid
"ServiceName": "HarrisService",
"ServiceIP": "127.0.0.1", //当前应用部署的服务器IP地址
"ServicePort": 5726, //当前应用部署的服务器端口 可以去参数传来得Port
"ConsulIP": "127.0.0.1", //Consul部署的服务器IP地址
"ConsulPort": 8500 //Consul端口
}
}

Model 实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace ConsulTest.Models
{
public class ServiceConfig
{
/// <summary>
/// 服务唯一ID
/// </summary>
public string ServiceId { get; set; }
/// <summary>
/// 服务部署的IP
/// </summary>
public string ServiceIP { get; set; }
/// <summary>
/// 服务部署的端口
/// </summary>
public int ServicePort { get; set; }
/// <summary>
/// 服务名称
/// </summary>
public string ServiceName { get; set; }
/// <summary>
/// consul部署的IP
/// </summary>
public string ConsulIP { get; set; }
/// <summary>
/// consul端口
/// </summary>
public int ConsulPort { get; set; }
/// <summary>
/// 权重
/// </summary>
public int weight { get; set; }
}
}

AppBuilderExtensions 帮助类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using Consul;
using ConsulTest.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using System;

namespace ConsulTest.Untiy
{
public static class AppBuilderExtensions
{
public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IHostApplicationLifetime lifetime, ServiceConfig serviceConfig)
{
var consulClient = new ConsulClient(x => x.Address = new Uri($"http://{serviceConfig.ConsulIP}:{serviceConfig.ConsulPort}"));
var httpCheck = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务器启动5秒后注册
Interval = TimeSpan.FromMinutes(1),//每分钟检测一次(健康检查间隔时间)
HTTP = $"http://{serviceConfig.ServiceIP}:{serviceConfig.ServicePort}/api/health",//本服务健康检查地址
Timeout = TimeSpan.FromSeconds(20),
};
var registerAgent = new AgentServiceRegistration()
{
Check = httpCheck,
Checks = new[] { httpCheck },
ID = serviceConfig.ServiceId,//一定要指定服务ID,否则每次都会创建一个新的服务节点
Name = serviceConfig.ServiceName,
Address = serviceConfig.ServiceIP,
Port = serviceConfig.ServicePort,
Tags = new[] { $"urlprefix-/{serviceConfig.ServiceName}" }//添加 urlprefix-/servicename 格式的tag标签,以便Fabio识别
};
consulClient.Agent.ServiceRegister(registerAgent).Wait();//服务启动时注册,使用Consul API进行注册(HttpClient发起)
lifetime.ApplicationStopped.Register(() =>
{
consulClient.Agent.ServiceDeregister(registerAgent.ID).Wait();//服务器停止时取消注册
});
return app;
}
}
}

Startup 注册Consul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
using ConsulTest.Models;
using ConsulTest.Untiy;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ConsulTest
{
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.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ConsulTest", Version = "v1" });
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime,IConfiguration configuration)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ConsulTest v1"));
}

app.UseRouting();

app.UseAuthorization();

#region 注册Consul服务

var serviceConfig = Configuration.GetSection("ConsulConfig").Get<ServiceConfig>();

//获取weight
int weight = string.IsNullOrWhiteSpace(configuration["weight"]) ? 1 : int.Parse(configuration["weight"]);

serviceConfig.weight = weight;

serviceConfig.ServiceId = "Service" + Guid.NewGuid().ToString();

//获取端口
int port = int.Parse(configuration["port"]);

serviceConfig.ServicePort = port;

Console.WriteLine(port);

app.RegisterConsul(lifetime, serviceConfig);

#endregion

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

启动Consul

consul agent -dev(如果需要其他机器访问,命令为consul agent -dev -client 0.0.0.0 -ui)

image-20220102215459287

启动API应用

1
2
3
4
dotnet run --urls="http://*:5726" --ip=127.0.0.1 --port=5726 --weight=2
dotnet run --urls="http://*:5727" --ip=127.0.0.1 --port=5727 --weight=3
dotnet run --urls="http://*:5728" --ip=127.0.0.1 --port=5728 --weight=4
dotnet run --urls="http://*:5729" --ip=127.0.0.1 --port=5729 --weight=5

image-20220102215640307

Consul 应用界面

image-20220102211045421

image-20220102215804743

Consul 服务发现

image-20220102220005859

image-20220102220034369

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
using Consul;
using CustomerDemo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace CustomerDemo.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

// (随机标识)没考虑溢出问题,到达一定长度应该重置
private static int iSeed = 0;


public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}

public IActionResult Index()
{
#region Consul 服务发现

ConsulClient client = new ConsulClient(c =>
{
c.Address = new Uri("http://localhost:8500");
c.Datacenter = "dc1";
});

var response = client.Agent.Services().Result.Response;

string url = "http://HarrisService/WeatherForecast";

Uri uri = new Uri(url);

string groupName = uri.Host;

AgentService service = null;

var serviceDictionary = response.Where(s => s.Value.Service.Equals(groupName, StringComparison.OrdinalIgnoreCase)).ToList();

////方便测试这里取第一个
//{
// service = serviceDictionary.FirstOrDefault().Value;
//}

//均衡策略实现
{
//随机策略---平均策略
//Random rd = new Random();
//int index = rd.Next(serviceDictionary.Count);
//service = serviceDictionary[index].Value;

// 随机策略/平均策略
//var array = serviceDictionary.ToArray();
//service = array[new Random(iSeed++).Next(0, array.Length)].Value;
}

// 轮巡策略 / 平均策略
{
//var array = serviceDictionary.ToArray();
//service = array[iSeed++ % array.Length].Value;// 取余数:就能按照0 1 2 的顺序调用
}

// 权重模式
{
List<KeyValuePair<string, AgentService>> serviceList = new List<KeyValuePair<string, AgentService>>();

foreach (KeyValuePair<string, AgentService> agentService in serviceDictionary)
{
int count = int.Parse(agentService.Value.Tags[0]);

for (int i = 0; i < count; i++)
{
serviceList.Add(agentService);
}
}

service = serviceList[new Random(iSeed++).Next(0, serviceList.Count())].Value;
}

url = $"{uri.Scheme}://{service.Address}:{service.Port}{uri.PathAndQuery}";

string content = Untiy.WebApiHelperExtend.InvokeApi(url);

var weather = Newtonsoft.Json.JsonConvert.DeserializeObject<List<WeatherForecast>>(content);

this.ViewData["weather"] = weather;

this.ViewData["Url"] = url;

#endregion

return View();
}

public IActionResult Privacy()
{
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

Index.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@{
ViewData["Title"] = "Home Page";
}

<div class="text-center">
<h1 class="display-4">Welcome</h1>

<h2>Weather</h2>

<ul>

@foreach (WeatherForecast item in ViewData["weather"] as List<WeatherForecast>)
{
<li>@item.summary +@item.date.ToString()+ @item.summary+@item.temperatureC+@item.temperatureF </li>
}

</ul>

<h2>@this.ViewData["Url"]</h2>

<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

WebApiHelperExtend.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Net.Http;

namespace CustomerDemo.Untiy
{
/// <summary>
/// 接口调用帮助类
/// </summary>
public static class WebApiHelperExtend
{
/// <summary>
/// 模拟调用接口
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string InvokeApi(string url)
{
// 提供基类,用于从由URI标识的资源发送HTTP请求和接收HTTP响应。
using (HttpClient httpClient = new HttpClient())
{
// Http请求消息
HttpRequestMessage message = new HttpRequestMessage();
// 请求方式
message.Method = HttpMethod.Get;
// 请求地址
message.RequestUri = new Uri(url);
// 以异步操作的形式发送HTTP请求。并接收响应信息
var result = httpClient.SendAsync(message).Result;
// 将HTTP内容序列化为字符串
string content = result.Content.ReadAsStringAsync().Result;
return content;
}
}
}
}

实体

1
2
3
4
5
6
7
public class WeatherForecast
{
public DateTime date { get; set; }
public int temperatureC { get; set; }
public int temperatureF { get; set; }
public string summary { get; set; }
}

结果

image-20220103171428670

Docker For Consul

安装

1
2
$ docker pull consul # 默认拉取latest
$ docker pull consul:1.6.1 # 拉取指定版本

运行第一个节点 leader

1
docker run --publish 8600:8600 --publish 8500:8500 --publish 8300:8300 --publish 8301:8301 --publish 8302:8302 --name consul-01 --restart always --volume D:\docker\Consul\Consul-01\data:/consul/data --volume D:\docker\Consul\Consul-01\config:/consul/config consul:latest agent --server --bootstrap-expect=1 --ui --bind=0.0.0.0 --client=0.0.0.0 
1
docker run --publish 8600:8600 --publish 8500:8500 --publish 8300:8300 --publish 8301:8301 --publish 8302:8302 --name consul-01 --volume /docker/consul/data:/consul/data  --volume /docker/consul/config:/consul/config consul:latest agent  --server --bootstrap-expect=1 --ui --bind=0.0.0.0 --client=0.0.0.0 
  • 其中,端口示意:
    • 8500端口,用于Consul的HTTP接口及Web UI界面。
    • 8600端口,用于Consul已注册服务的DNS服务。
    • 8300端口,用于同一数据中心内Consul Server的RPC通信。
    • 8301端口,用于同一数据中心内Consul Server的gossip serf lan通信。
    • 8302端口,用于不同数据中心内Consul Server的gossip serf wan通信。
  • 其中,参数示意:

    • –net=host docker参数, 使得docker容器越过了net namespace的隔离,免去手动指定端口映射的步骤

    • -server consul支持以server或client的模式运行, server是服务发现模块的核心, client主要用于转发请求

    • -advertise 将本机私有IP传递到consul

    • -retry-join 指定要加入的consul节点地址,失败后会重试, 可多次指定不同的地址

    • -client 指定consul绑定在哪个client地址上,这个地址可提供HTTP、DNS、RPC等服务,默认是>127.0.0.1

    • -bind 绑定服务器的ip地址;该地址用来在集群内部的通讯,集群内的所有节点到地址必须是可达的,>默认是0.0.0.0

    • allow_stale 设置为true则表明可从consul集群的任一server节点获取dns信息, false则表明每次请求都会>经过consul的server leader

    • -bootstrap-expect 数据中心中预期的服务器数。指定后,Consul将等待指定数量的服务器可用,然后>启动群集。允许自动选举leader,但不能与传统-bootstrap标志一起使用, 需要在server模式下运行。

    • -data-dir 数据存放的位置,用于持久化保存集群状态

    • -node 群集中此节点的名称,这在群集中必须是唯一的,默认情况下是节点的主机名。

    • -config-dir 指定配置文件,当这个目录下有 .json 结尾的文件就会被加载,详细可参考https://www.consul.io/docs/agent/options.html#configuration_files

    • -enable-script-checks 检查服务是否处于活动状态,类似开启心跳

    • -datacenter 数据中心名称

    • -ui 开启ui界面

    • -join 指定ip, 加入到已有的集群中

查看已存在节点的IP

1
docker inspect --format='{{.NetworkSettings.IPAddress}}' consul-01

开启第二个节点

1
docker run --publish 8501:8500 --name consul-02 --restart always --volume D:\docker\Consul\Consul-02\data:/consul/data --volume D:\docker\Consul\Consul-02\config:/consul/config consul:latest agent --server --ui --bind=0.0.0.0 --client=0.0.0.0 --join 172.17.0.2

开启第三个节点

1
docker run --publish 8502:8500 --name consul-03 --restart always --volume D:\docker\Consul\Consul-03\data:/consul/data --volume D:\docker\Consul\Consul-03\config:/consul/config consul:latest agent --server --ui --bind=0.0.0.0 --client=0.0.0.0 --join 172.17.0.2

查看Consul集群信息

1
docker exec -it consul-01 consul members

查看容器IP

1
2
3
4
5
6
docker inspect NAMES
# 查看容器所有状态信息;
docker inspect --format='{{.NetworkSettings.IPAddress}}'
# 查看 容器ip 地址
docker inspect --format '{{.Name}} {{.State.Running}}'
# 容器运行状态

net core 发布

img

新建Dockerfile

1
2
3
4
5
6
7
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80

WORKDIR /app
COPY . /app
ENTRYPOINT ["dotnet", "ConsulTest.dll"]

build

1
docker build -t consultest02:v2.0 .

run

1
docker run --name consultest02 -p 80:80 -d consultest02:v2.0

宿主访问容器

1
2
3
4
添加路由
route -p add 172.17.0.0 MASK 255.255.255.0 192.168.65.2
删除路由
route delete 172.17.0.0

微服务之负载均衡示例
http://example.com/2022/01/01/微服务之负载均衡/
Author
Harris
Posted on
January 1, 2022
Licensed under