目录
- ASP.NET Core Ocelot+Consul+Nginx+JWT Linux服务器部署文件
- 整体文件结构介绍
- 鉴权中心 + Nginx 文件夹:Nginx_AuthenticationCenter
- 网关+Nginx文件夹:Nginx_GateWay
- Consul+Nginx文件夹:Nginx_Consul
- 用户微服务文件夹:User
- 课程微服务文件夹:Lesson
- 各类Shell脚本文件
- mysql文件夹
ASP.NET Core Ocelot+Consul+Nginx+JWT Linux服务器部署文件
整体文件结构介绍
- 项目文件夹:TestProject
- 鉴权中心 + Nginx 文件夹:Nginx_AuthenticationCenter
- config文件夹
- publish文件夹
- docker-compose
- Dockerfile
- 网关+Nginx文件夹:Nginx_GateWay
- config文件夹
- publish文件夹
- docker-compose
- Dockerfile
- Consul+Nginx文件夹:Nginx_Consul
- config文件夹
- docker-compose
- 用户微服务文件夹:User
- config文件夹
- publish文件夹
- docker-compose
- Dockerfile
- 课程微服务文件夹:Lesson
- config文件夹
- publish文件夹
- docker-compose
- Dockerfile
- 各类SH脚本文件
- authenticationcenter_svc.sh
- gateway_svc.sh
- user_svc.sh
- lesson_svc.sh
- update_svc.sh
- start_svc.sh
- 鉴权中心 + Nginx 文件夹:Nginx_AuthenticationCenter
鉴权中心 + Nginx 文件夹:Nginx_AuthenticationCenter
config文件夹
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConfigInformation": {
"RootUrl": "http://111.44.222.111:6300", //�������
"UserUrl": "/api/userapi/User/validate",
"JWTTokenOptions": {
"Audience": "http://localhost:8761",
"Issuer": "http://localhost:8761",
"SecurityKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB"
}
}
}
key.json
{"D":"ayJEcDAFTdAjKtn/wUvAe0z0RtXcOFENJm55PaTbDV8QAKfKKENY5K+nvU36uSi3qh2xP6NVoo3H3rDlk6X8AEuAOQs+arGfHQ/aL4Ob3skuEioHWszXScJ2KTzrrsolOik9SybNLRSMIgQKyZY5URk3BhLqSvMDwBQ2Nht/mlx+eQi1GpPgiJFH77BlRt3O/LKafAtgS292RxeKUJn3Q4dsn1PtJX+PMPT+bn+9PZXpQtSI8r8yUtrFja61WcGN8aJrG47EfT5wa3J/mcfhEK+4hU2uI3ycW+TaNjuxDZ+nAD4k3pcNT6a1ldSi3CnZKR2p/MUh07oazmx2QEg54Q==","DP":"t58aASvJT2+mQCi9EN5RksOXrgzGNB2U6PeS8NJ9ht6HiA78+fZKrfbxXxz8i/069Tyg7dkzYeKFd93q9FhFKqsOGE67gqjelKIXFTN2s2DFiJ7neFHkIhPisdS/a+SzHziFsxYHJbWobuHlrDw2QcoYGDsgS1Crbatn7t90Hfs=","DQ":"dzpHSw7DD1vwy+mOX5nRJLVniSmcIX8MMWtCXlmzj6CdUddyiGSGFhTB+hjVHLPxsJAzoV4zBFRt1s+CHGlgjhfD6ct58i7bDVG/6OVUI4v95iYiA7kPB44DlOzVjuhlGmTm5Tw5eTwjA3s/5FUuif0DShzt4jam7f+jlTvkXaM=","Exponent":"AQAB","InverseQ":"MTykln8IgIQ2DwhC4d0d/RXNk5/PvKXSY8goldKfxCiAwTmArivvuxfHC01oKFlZkZbPRVvh0rM9QkM4pX9ITfKd4+VoxmDtMMx5oEkbxKMbJQkUvJeADmtcy/zfXq8ZNSNcIkAI4setydA6tOvRZKuudJ5tEpXOxwTel8U5ltM=","Modulus":"o0jSDb5OYfSTPFPjZS67yovVQLEA5OIrey/1mBCH8Xxvo1zLwKPYzWwkRjzSLURZ19V9AeKAiP+JxDtGRzmUflqXY3e7vKeEosk5MoUj4MlBvxVxDL3bdghJaqhARaqsuXQ1dvOGABsDIogBmvCJyJOBHXISLl+hDGIOQSpHqtMFz4UHAF5v62x82oMYT8O4lTfoTSF1+jMH31rCCERXFEz2DUngdsT8gwQncTMrVTS2dIdacvkWmN0yvzLmMqZetv12p10O7jjuN61hlhhccAibGeU3X1veOHS4L9TzQ0rLPK/yTm3QlShWZD8oiLBnNXGhS0m/RTk3Uc7IrvvaqQ==","P":"0daJpBirbZIUYZyqeXW6csoy2eKDO81G4DAe0gzyZUk7ZQ97H3sIRdKU05lmeR0KuEtp71LOaljx2MJ1vawF4zoJ3MQEjzQYQS0Gq5zLPrX/Q+Sy/7Brb9oYlfwDzlyszlZqjSyJjupNOAlpkTkytt6a5g6LtD44mo2A9XCteTM=","Q":"xzSFkOTiJGyTNatXO8pAxZyGg4qjAweJOL5wv5dGqFF7fWx92uJrMcGy6kda5A3aCE0KG0441fWjGPjzb6GvoTzwADRx4mNhOcVV0gx/lbKydc15KaBNEX29TkmYbG4dRQ5wOs+FBm0PAHcQgK64AFYhobG4w8VZBLxCXdwndLM="}
nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream authentication{
server 111.44.222.111:6597;
server 111.44.222.111:6598;
server 111.44.222.111:6599;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://authentication;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
pubish文件夹主要存放项目的发布文件
docker-compose
version: '3.3'
services:
serviceocelot1:
build:
context: /home/TestProject/AuthenticationCenter/
dockerfile: /home/TestProject/AuthenticationCenter/Dockerfile
image: authenticationcenter.v0127
ports:
- 6597:80/tcp
container_name: "AuthenticationCenter1"
command: "dotnet /app/Common.AuthenticationCenter.dll"
volumes:
- /home/TestProject/AuthenticationCenter/config/appsettings.json:/app/appsettings.json
serviceocelot2:
image: authenticationcenter.v0127
ports:
- 6598:80/tcp
container_name: "AuthenticationCenter2"
command: "dotnet /app/Common.AuthenticationCenter.dll"
volumes:
- /home/TestProject/AuthenticationCenter/config/appsettings.json:/app/appsettings.json
nginxocelot:
depends_on:
- serviceocelot2
image: nginx:latest
ports:
- 6600:80/tcp
container_name: "nginxAuthenticationCenter"
volumes:
- /home/TestProject/AuthenticationCenter/config/nginx.conf:/etc/nginx/nginx.conf
Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
EXPOSE 80
COPY ./publish app/
WORKDIR /app
ENTRYPOINT ["dotnet", "Common.AuthenticationCenter.dll"]
网关+Nginx文件夹:Nginx_GateWay
config文件夹
appsetings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"JWTTokenOptions": {
"Audience": "http://localhost:8761",
"Issuer": "http://localhost:8761",
"SecurityKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB"
}
}
OcelotConfiguration.json
{
"Routes": [
{
"UpstreamPathTemplate": "/api/lessonapi/{url}",
"UpstreamHttpMethod": [
"Get",
"Post",
"Put",
"Patch",
"Delete",
"Options"
],
"UserServiceDIscovery": true,
"ServiceName": "LessonCenter",
"LoadBalancerOptions": {
"Type": "RoundRobin"
},
"DownstreamPathTemplate": "/api/lessonapi/{url}",
"DownstreamScheme": "http",
"DownstreamHeaderTransform": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*"
},
"AuthenticationOptions": {
"AuthenticationProviderKey": "UserGatewayKey",
"AllowedScopes": []
}
},
{
"UpstreamPathTemplate": "/lesson/swagger/v1/swagger.json",
"UpstreamHttpMethod": [
"Get"
],
"UseServiceDiscovery": true,
"ServiceName": "LessonCenter",
"LoadBalancerOptions": {
"Type": "RoundRobin"
},
"DownstreamPathTemplate": "/swagger/v1/swagger.json",
"DownstreamScheme": "http",
"RateLimitOptions": {
"ClientWhiteList": [
"ajun816",
"superhero"
],
"EnableRateLimiting": true,
"Period": "5m",
"PeriodTimespan": 30,
"Limit": 5
}
},
{
"UpstreamPathTemplate": "/api/userapi/{url}",
"UpstreamHttpMethod": [
"Get",
"Post",
"Put",
"Patch",
"Delete",
"Options"
],
"UserServiceDIscovery": true,
"ServiceName": "UserCenter",
"LoadBalancerOptions": {
"Type": "RoundRobin"
},
"DownstreamPathTemplate": "/api/userapi/{url}",
"DownstreamScheme": "http",
"DownstreamHeaderTransform": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*"
}
},
{
"UpstreamPathTemplate": "/user/swagger/v1/swagger.json",
"UpstreamHttpMethod": [
"Get"
],
"UseServiceDiscovery": true,
"ServiceName": "UserCenter",
"LoadBalancerOptions": {
"Type": "RoundRobin"
},
"DownstreamPathTemplate": "/swagger/v1/swagger.json",
"DownstreamScheme": "http",
"RateLimitOptions": {
"ClientWhiteList": [
"ajun816",
"superhero"
],
"EnableRateLimiting": true,
"Period": "5m",
"PeriodTimespan": 30,
"Limit": 5
}
},
"GlobalConfiguration": {
"BaseUrl": "http://127.0.0.1:8070", //网关对外地址
"ServiceDiscoveryProvider": {
"Host": "127.0.0.1",
"Port": 8500,
"Type": "Consul" //由Consul提供服务发现
},
"RateLimitOptions": {
"QuotaExceededMessage": "Too many requests, maybe later? 11", // 当请求过载被截断时返回的消息
"HttpStatusCode": 666, // 当请求过载被截断时返回的http status
"ClientIdHeader": "client_id" // 用来识别客户端的请求头,默认是 ClientId
}
}
}
nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream mygateway{
server 111.44.222.111:6297;
server 111.44.222.111:6298;
server 111.44.222.111:6299;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://mygateway;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
publish文件夹存放项目的发布内容
docker-compose
version: '3.3'
services:
gateway1:
build:
context: ${currpwd}
dockerfile: ${currpwd}/Dockerfile
image: gateway.v1
ports:
- 6297:80/tcp
container_name: "gatewayservice1"
command: "dotnet /app/Common.OcelotGateway.dll"
volumes:
- ${currpwd}/config/OcelotConfiguration.json:/app/OcelotConfiguration.json
gateway2:
image: gateway.v1
ports:
- 6298:80/tcp
container_name: "gatewayservice2"
command: "dotnet /app/Common.OcelotGateway.dll"
volumes:
- ${currpwd}/config/OcelotConfiguration.json:/app/OcelotConfiguration.json
gateway3:
image: gateway.v1
ports:
- 6299:80/tcp
container_name: "gatewayservice3"
command: "dotnet /app/Common.OcelotGateway.dll"
volumes:
- ${currpwd}/config/OcelotConfiguration.json:/app/OcelotConfiguration.json
nginxocelot:
depends_on:
- gateway3
image: nginx:latest
ports:
- 6300:80/tcp
container_name: "nginxocelot"
volumes:
- ${currpwd}/config/nginx.conf:/etc/nginx/nginx.conf
Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
COPY ./publish app/
WORKDIR /app
ENTRYPOINT ["dotnet", "Zhzt.Common.OcelotGateway.dll"]
Consul+Nginx文件夹:Nginx_Consul
Config文件夹
nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream consul {
server 111.44.222.111:8500;
server 111.44.222.111:9500;
server 111.44.222.111:10500;
server 111.44.222.111:11500;
server 111.44.222.111:12500;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://consul;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
docker-compose
version: '3.3'
services:
service1:
image: consul
ports:
- 8300:8300/tcp
- 8301:8301/tcp
- 8302:8302/tcp
- 8400:8400/tcp
- 8500:8500/tcp
- 8600:8600/tcp
- 8301:8301/udp
- 8302:8302/udp
container_name: "node1"
command: "agent -server -bind=0.0.0.0 -bootstrap-expect=3 -node=node1 -data-dir=/tmp/data-dir -client=0.0.0.0 -ui"
service2:
depends_on:
- service1
image: consul
ports:
- 9300:8300/tcp
- 9301:8301/tcp
- 9302:8302/tcp
- 9400:8400/tcp
- 9500:8500/tcp
- 9600:8600/tcp
- 9301:8301/udp
- 9302:8302/udp
container_name: "node2"
command: "agent -server -bind=0.0.0.0 -join=101.43.223.131 -node=node2 -data-dir=/tmp/data-dir -client=0.0.0.0 -ui"
service3:
depends_on:
- service2
image: consul
ports:
- 10300:8300/tcp
- 10301:8301/tcp
- 10302:8302/tcp
- 10400:8400/tcp
- 10500:8500/tcp
- 10600:8600/tcp
- 10301:8301/udp
- 10302:8302/udp
container_name: "node3"
command: "agent -server -bind=0.0.0.0 -join=101.43.223.131 -node=node3 -data-dir=/tmp/data-dir -client=0.0.0.0 -ui"
service4:
depends_on:
- service3
image: consul
ports:
- 11300:8300/tcp
- 11301:8301/tcp
- 11302:8302/tcp
- 11400:8400/tcp
- 11500:8500/tcp
- 11600:8600/tcp
- 11301:8301/udp
- 11302:8302/udp
container_name: "node4"
command: "agent -bind=0.0.0.0 -retry-join=101.43.223.131 -node=node4 -client=0.0.0.0 -ui"
service5:
depends_on:
- service4
image: consul
ports:
- 12300:8300/tcp
- 12301:8301/tcp
- 12302:8302/tcp
- 12400:8400/tcp
- 12500:8500/tcp
- 12600:8600/tcp
- 12301:8301/udp
- 12302:8302/udp
container_name: "node5"
command: "agent -bind=0.0.0.0 -retry-join=101.43.223.131 -node=node5 -client=0.0.0.0 -ui"
service6:
depends_on:
- service5
image: nginx
ports:
- 18500:80/tcp
container_name: "consulnginx"
volumes:
- ./conf/nginx.conf:/etc/nginx/nginx.conf
用户微服务文件夹:User
config文件夹
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MySqlConn": "Server=111.44.222.111;Database=db_user;Uid=root;Pwd=111111;SslMode=none;"
},
"CodeFirstSettings": {
"Migrate": "true", //是否开启同步(是否进行codefirst创建表)
"Backup": "true", //是否进行备份
"ModelPath": "UserModel" //要进行同步的Model程序集路径
},
"SqlSugarSnowFlakeSettings": {
"WorkerId": "1"
},
"ConsulClientOption": {
"IP": "111.44.222.111",
"Port": "18500",
"Datacenter": "dc1"
},
"ConsulRegisterOption": {
"IP": "111.44.222.111",
"Port": "8761",
"GroupName": "UserCenter",
"HealthCheckUrl": "http://111.44.222.111:8761/Health",
"Interval": 10,
"Timeout": 5,
"DergisterCriticalServiceAfter": 20,
"Tag": "13"
}
}
publish存放项目发布文件
docker-compose
version: '3.3'
services:
user1:
build:
context: ${currpwd}
dockerfile: ${currpwd}/Dockerfile
image: user.v1
ports:
- 10091:80/tcp
container_name: "userservice1"
command: "dotnet /app/UserMicroservice.dll ConsulRegisterOption:IP=111.44.222.111 ConsulRegisterOption:Port=10091 ConsulRegisterOption:HealthCheckUrl=http://111.44.222.111:10091/Health"
volumes:
- ${currpwd}/config/appsettings.json:/app/appsettings.json
user2:
depends_on:
- user1
image: user.v1
ports:
- 10092:80/tcp
container_name: "userservice2"
command: "dotnet /app/UserMicroservice.dll ConsulRegisterOption:IP=111.44.222.111 ConsulRegisterOption:Port=10092 ConsulRegisterOption:HealthCheckUrl=http://111.44.222.111:10092/Health"
volumes:
- ${currpwd}/config/appsettings.json:/app/appsettings.json
Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
EXPOSE 80
COPY ./publish app/
WORKDIR /app
ENTRYPOINT ["dotnet", "UserMicroservice.dll"]
课程微服务文件夹:Lesson
config文件夹
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MySqlConn": "Server=111.44.222.111;Database=db_Lesson;Uid=root;Pwd=111111;SslMode=none;"
},
"CodeFirstSettings": {
"Migrate": "true", //是否开启同步(是否进行codefirst创建表)
"Backup": "true", //是否进行备份
"ModelPath": "LessonModel" //要进行同步的Model程序集路径
},
"SqlSugarSnowFlakeSettings": {
"WorkerId": "1"
},
"JWTTokenOptions": {
"Audience": "http://localhost:8761",
"Issuer": "http://localhost:8761",
"SecurityKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB"
},
//"ConsulClientOption": {
// "IP": "111.44.222.111",
// "Port": "18500",
// "Datacenter": "dc1"
//},
//"ConsulRegisterOption": {
// "IP": "111.44.222.111",
// "Port": "8761",
// "GroupName": "LessonCenter",
// "HealthCheckUrl": "http://111.44.222.111:8761/Health",
// "Interval": 10,
// "Timeout": 5,
// "DergisterCriticalServiceAfter": 20,
// "Tag": "13"
//},
//本地测试ConsulClientOption
"ConsulClientOption": {
"IP": "localhost",
"Port": "8500",
"Datacenter": "dc1"
},
"ConsulRegisterOption": {
"IP": "localhost",
"Port": "8761",
"GroupName": "LessonCenter",
"HealthCheckUrl": "http://localhost:8761/Health",
"Interval": 10,
"Timeout": 5,
"DergisterCriticalServiceAfter": 20,
"Tag": "13"
}
}
publish存放项目的发布文件
docker-compose
version: '3.3'
services:
lesson1:
build:
context: ${currpwd}
dockerfile: ${currpwd}/Dockerfile
image: lesson.v1
ports:
- 10071:80/tcp
container_name: "lessonservice1"
command: "dotnet /app/LessonMicroservice.dll ConsulRegisterOption:IP=111.44.222.111 ConsulRegisterOption:Port=10071 ConsulRegisterOption:HealthCheckUrl=http://111.44.222.111:10071/Health"
volumes:
- ${currpwd}/config/appsettings.json:/app/appsettings.json
lesson2:
image: lesson.v1
ports:
- 10072:80/tcp
container_name: "lessonservice2"
command: "dotnet /app/LessonMicroservice.dll ConsulRegisterOption:IP=111.44.222.111 ConsulRegisterOption:Port=10072 ConsulRegisterOption:HealthCheckUrl=http://111.44.222.111:10072/Health"
volumes:
- ${currpwd}/config/appsettings.json:/app/appsettings.json
Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
EXPOSE 80
COPY ./publish app/
WORKDIR /app
ENTRYPOINT ["dotnet", "LessonMicroservice.dll"]
各类Shell脚本文件
authenticationcenter_svc.sh
cd AuthenticationCenter
export currpwd=$(pwd)
docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml up --build -d
gateway.sh
cd GateWay
export currpwd=$(pwd)
docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml up --build -d
lesson_svc.sh
cd Lesson
export currpwd=$(pwd)
docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml up --build -d
update.sh
- shell声明一个currpwd变量
- pwd 小写是命令 大写是变量 效果一样, Linux pwd(英文全拼:print work directory) 命令用于显示工作目录。执行 pwd 指令可立刻得知您目前所在的工作目录的绝对路径名称。
- 执行docker-compose时,传入这个变量(当前文件路径)
#!/bin/bash
cd consul
export currpwd=$(pwd)
docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml up --build -d
cd ..
cd GateWay
export currpwd=$(pwd)
docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml up --build -d
cd ..
cd User
export currpwd=$(pwd)
docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml up --build -d
cd ..
cd Lesson
export currpwd=$(pwd)
docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml up --build -d
mysql文件夹
docker-compose
version: '3.3'
services:
service1:
image: mysql:8.0
ports:
- 3306:3306/tcp
container_name: "db_mysql"
volumes:
- /home/data/mysql_data:/var/lib/mysql
environment:
- "MYSQL_ROOT_PASSWORD=111111"
- "TZ=Asia/Shanghai"
原创文章,作者:3628473679,如若转载,请注明出处:https://blog.ytso.com/tech/aiops/245143.html