1、微服務簡介
一種架構模式,提倡將單一應用程序划分成一組小的服務,服務之間互相協調、互相配合,為用戶提供最終價值。每個服務運行在其獨立的進程中,服務與服務間采用輕量級的通信機制互相溝通(RESTful API)。每個服務都圍繞着具體的業務進行構建,並且能夠被獨立地部署到生產環境、類生產環境等。應盡量避免統一的、集中式的服管理機制,對具體的一個服務而言,應根據業務上下文,選擇合適的語言、工具對其進行構建。 ——馬丁•福勒
1.1、.net core下的微服務構件
- 服務治理:Consul
- API網關:Ocelot
- 作業調度:Quartz.NET,Hangfire
- 分布式日志:Exceptionless
- ESB:Masstransit(RabbitMQ)
- APM:Metrac.App,Buttfly
1.2、微架構
2、Consul
http api官方文檔地址:https://www.consul.io/api/index.html
Api本地url: http://localhost:8500/v1/agent/services
2.1、Consul是什么
是一個服務管理軟件。支持多數據中心下,分布式高可用的,服務發現和配置共享。consul支持健康檢查,允許存儲鍵值對。一致性協議采用 Raft 算法,用來保證服務的高可用。成員管理和消息廣播 采用GOSSIP協議,支持ACL訪問控制。
服務注冊:一個服務將其位置信息在“中心注冊節點”注冊的過程。該服務一般會將它的主機IP地址以及端口號進行注冊,有時也會有服務訪問的認證信息,使用協議,版本號,以及關於環境的一些細節信息。
服務發現:服務發現可以讓一個應用或者組件發現其運行環境以及其它應用或組件的信息。用戶配置一個服務發現工具就可以將實際容器跟運行配置分離開。常見配置信息包括:ip、端口號、名稱等。
2.2、述語
- Agent
Agent是長期運行在每個consul集群成員節點上守護進程。通過命令consul agent啟動。Agent有client和server兩種模式。由於每個節點都必須運行agent,所有節點要么是client要么是server。所有的Agent都可以調用DNS或HTTP API,並負責檢查和維護服務同步。
- client
運行client模式的Agent,將所有的RPCs轉發到Server。Client是相對無狀態的。Client唯一所做的是在后台參與LAN gossip pool。只消耗少量的資源,少量的網絡帶寬。 - Server
運行Server模式的Agent,參與Raft quorum,維護集群的狀態,響應RPC查詢,與其他數據中心交互WAN gossip,轉發查詢到Leader或遠程數據中心。 - Datacenter
數據中心的定義似乎是顯而易見的,有一些細節是必須考慮的。例如,在EC2,多個可用性區域是否被認為組成了單一的數據中心?我們定義數據中心是在同一個網絡環境中——私有的,低延遲,高帶寬。這不包括基於公共互聯網環境,但是對於我們而言,在同一個EC2的多個可用性區域會被認為是一個的數據中心。 - Consensus
當本系列文檔中,consensus,意味着leader election協議,以及事務的順序。由於這些事務是基於一個有限狀態機,consensus的定義意味着復制狀態機的一致性。 - Gossip
consul是建立在Serf之上,提供了完成的Gossip協議,用於成員維護故障檢測、事件廣播。詳細細節參見gossip documentation。這足以知道gossip是基於UDP協議實現隨機的節點到節點的通信,主要是在UDP。 - LAN Gossip
指的是LAN gossip pool,包含位於同一個局域網或者數據中心的節點。 - WAN Gossip
指的是WAN gossip pool,只包含server節點,這些server主要分布在不同的數據中心或者通信是基於互聯網或廣域網的。 - RPC
遠程過程調用。是允許client請求服務器的請求/響應機制。
2.3、部署結構圖
2.4、命令
- -advertise
通知展現地址用來改變我們給集群中的其他節點展現的地址,一般情況下-bind地址就是展現地址 - -bootstrap
用來控制一個server是否在bootstrap模式,在一個datacenter中只能有一個server處於bootstrap模式,當一個server處於bootstrap模式時,可以自己選舉為raft leader。 - -bootstrap-expect
在一個datacenter中期望提供的server節點數目,當該值提供的時候,consul一直等到達到指定sever數目的時候才會引導整個集群,該標記不能和bootstrap公用 - -bind
該地址用來在集群內部的通訊,集群內的所有節點到地址都必須是可達的,默認是0.0.0.0 - -client
consul綁定在哪個client地址上,這個地址提供HTTP、DNS、RPC等服務,默認是127.0.0.1 - -config-file
明確的指定要加載哪個配置文件 - -config-dir
配置文件目錄,里面所有以.json結尾的文件都會被加載 - -data-dir
提供一個目錄用來存放agent的狀態,所有的agent允許都需要該目錄,該目錄必須是穩定的,系統重啟后都繼續存在 - -datacenter
該標記控制agent允許的datacenter的名稱,默認是dc1 - -encrypt
指定secret key,使consul在通訊時進行加密,key可以通過consul keygen生成,同一個集群中的節點必須使用相同的key - -join
加入一個已經啟動的agent的ip地址,可以多次指定多個agent的地址。如果consul不能加入任何指定的地址中,則agent會啟動失敗,默認agent啟動時不會加入任何節點。 - -retry-join
和join類似,但是允許你在第一次失敗后進行嘗試。 - -retry-interval
兩次join之間的時間間隔,默認是30s - -retry-max
嘗試重復join的次數,默認是0,也就是無限次嘗試 - -log-level
consul agent啟動后顯示的日志信息級別。默認是info,可選:trace、debug、info、warn、err。 - -node
節點在集群中的名稱,在一個集群中必須是唯一的,默認是該節點的主機名 - -protocol
consul使用的協議版本 - -rejoin
使consul忽略先前的離開,在再次啟動后仍舊嘗試加入集群中。 - -server
定義agent運行在server模式,每個集群至少有一個server,建議每個集群的server不要超過5個 - -syslog
開啟系統日志功能,只在linux/osx上生效 - -ui-dir
提供存放web ui資源的路徑,該目錄必須是可讀的 - -pid-file
提供一個路徑來存放pid文件,可以使用該文件進行SIGINT/SIGHUP(關閉/更新)agent
2.5、常用API
consul的主要接口是RESTful HTTP API,該API可以用來增刪查改nodes、services、checks、configguration。所有的endpoints主要分為以下類別:
- kv - Key/Value存儲
- agent - Agent控制
- catalog - 管理nodes和services
- health - 管理健康監測
- session - Session操作
- acl - ACL創建和管理
- event - 用戶Events
- status - Consul系統狀態
- agent endpoints:agent endpoints用來和本地agent進行交互,一般用來服務注冊和檢查注冊,支持以下接口
/v1/agent/checks : 返回本地agent注冊的所有檢查(包括配置文件和HTTP接口) /v1/agent/services : 返回本地agent注冊的所有 服務 /v1/agent/members : 返回agent在集群的gossip pool中看到的成員 /v1/agent/self : 返回本地agent的配置和成員信息 /v1/agent/join/<address> : 觸發本地agent加入node /v1/agent/force-leave/<node>>: 強制刪除node /v1/agent/check/register : 在本地agent增加一個檢查項,使用PUT方法傳輸一個json格式的數據 /v1/agent/check/deregister/<checkID> : 注銷一個本地agent的檢查項 /v1/agent/check/pass/<checkID> : 設置一個本地檢查項的狀態為passing /v1/agent/check/warn/<checkID> : 設置一個本地檢查項的狀態為warning /v1/agent/check/fail/<checkID> : 設置一個本地檢查項的狀態為critical /v1/agent/service/register : 在本地agent增加一個新的服務項,使用PUT方法傳輸一個json格式的數據 /v1/agent/service/deregister/<serviceID> : 注銷一個本地agent的服務項
- catalog endpoints:catalog endpoints用來注冊/注銷nodes、services、checks
/v1/catalog/register : Registers a new node, service, or check /v1/catalog/deregister : Deregisters a node, service, or check /v1/catalog/datacenters : Lists known datacenters /v1/catalog/nodes : Lists nodes in a given DC /v1/catalog/services : Lists services in a given DC /v1/catalog/service/<service> : Lists the nodes in a given service /v1/catalog/node/<node> : Lists the services provided by a node
- health endpoints:health endpoints用來查詢健康狀況相關信息,該功能從catalog中單獨分離出來
/v1/healt/node/<node>: 返回node所定義的檢查,可用參數?dc= /v1/health/checks/<service>: 返回和服務相關聯的檢查,可用參數?dc= /v1/health/service/<service>: 返回給定datacenter中給定node中service /v1/health/state/<state>: 返回給定datacenter中指定狀態的服務,state可以是"any", "unknown", "passing", "warning", or "critical",可用參數?dc=
- session endpoints:session endpoints用來create、update、destory、query sessions
/v1/session/create: Creates a new session /v1/session/destroy/<session>: Destroys a given session /v1/session/info/<session>: Queries a given session /v1/session/node/<node>: Lists sessions belonging to a node /v1/session/list: Lists all the active sessions
- acl endpoints:acl endpoints用來create、update、destory、query acl
/v1/acl/create: Creates a new token with policy /v1/acl/update: Update the policy of a token /v1/acl/destroy/<id>: Destroys a given token /v1/acl/info/<id>: Queries the policy of a given token /v1/acl/clone/<id>: Creates a new token by cloning an existing token /v1/acl/list: Lists all the active tokens
- event endpoints:event endpoints用來fire新的events、查詢已有的events
/v1/event/fire/<name>: 觸發一個新的event,用戶event需要name和其他可選的參數,使用PUT方法 /v1/event/list: 返回agent知道的events
- status endpoints:status endpoints用來或者consul 集群的信息
/v1/status/leader : 返回當前集群的Raft leader /v1/status/peers : 返回當前集群中同事
2.6、使用consul
- 啟動
語法:
consul agent -server -datacenter=數據中心名稱 -bootstrap -data-dir 數據存放路徑 -config-file 配置文件路徑 -ui-dir UI存放路徑 -node=n1 -bind 本機IP
注冊成Windows服務
sc.exe create "Consul" binPath= "E:\Consul\consule.exe agent -server -datacenter=數據中心名稱 -bootstrap -data-dir 數據存放路徑 -config-file 配置文件路徑 -ui-dir UI存放路徑 -node=n1 -bind 本機IP"
示例:
consul agent -server -datacenter=dc1 -bootstrap -data-dir /tmp/consul -config-file ./conf -ui-dir ./dist -node=n1 -bind 127.0.0.1
- 查看集群成員
consul members
- 把192.168.1.126加入集群
consul join 192.168.1.126
- 查看節點raft信息
consul operator raft list-peers
2.7、項目實例
- 項目准備
項目地址:https://github.com/786744873/HisMicroserviceSample
項目部署說明:分別部署 192.168.103.203 、 192.168.103.207 兩台服務器 - 配置consul配置文件
文件結構:
│ consul.exe │ ├─conf │ service.json │ watchs.json │ xacl.json │ ├─data ├─dist
service.json(服務發現配置):
{ "encrypt": "7TnJPB4lKtjEcCWWjN6jSA==", //加密秘鑰 "services": [{ "id": "BasicsService", //服務id "name": "BasicsService", //服務名稱 "tags": ["BasicsService"], //服務標簽 "address": "192.168.103.203", //服務地址 "port": 6801, //端口 "checks": [{ "id": "BasicsServiceCheck", //檢查id "name": "BasicsServiceCheck", //檢查名稱 "http": "http://192.168.103.203:6801/health", //檢車接口地址 "interval": "10s", //檢查周期 "tls_skip_verify": false, //跳過驗證 "method": "GET", //檢查請求方法 "timeout": "1s" //請求超時時間 }] }, { "id": "InvoicingService", //服務id "name": "InvoicingService", //服務名稱 "tags": ["InvoicingService"], //服務標簽 "address": "192.168.103.203", //服務地址 "port": 6802, //端口 "checks": [{ "id": "InvoicingServiceCheck", //檢查id "name": "InvoicingServiceCheck", //檢查名稱 "http": "http://192.168.103.203:6802/health", //檢車接口地址 "interval": "10s", //檢查周期 "tls_skip_verify": false, //跳過驗證 "method": "GET", //檢查請求方法 "timeout": "1s" //請求超時時間 }] } ] }
watchs.json(服務監控配置):
{ "watches": [{ "type": "checks", //監控觸發類型 "handler_type": "http", //異常通知類型 "state": "critical", //監控觸發狀態 "http_handler_config": { "path": "http://localhost:6801/notice", //通知地址 "method": "POST", //通知請求方式 "timeout": "10s", //通知超時時間 "header": { "Authorization": ["Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZ3N3IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL2V4cGlyYXRpb24iOiIyMDIyLzEyLzMxIDEyOjM2OjEyIiwibmJmIjoxNTE0Njk0OTcyLCJleHAiOjE1MTQ3MzA5NzIsImlzcyI6ImdzdyIsImF1ZCI6ImdzdyJ9.jPu1yZ8jORN5QgCuPV50sYOKvX88GLSDiRX_0fpEzU4"] } //請求頭 } }] }
- 分別啟動 192.168.103.203 、 192.168.103.207 上的應用基礎和進銷存服務,然后再啟動Consul,我們讓 192.168.103.203 作為主Consul
第一台service(192.168.103.203):
consul agent -server -datacenter=dc1 -bootstrap -data-dir ./data -config-file ./conf -ui-dir ./dist -node=n1 -bind 192.168.103.203
第二台service(192.168.103.207):
consul agent -server -datacenter=dc1 -data-dir ./data -config-file ./conf -ui-dir ./dist -node=n2 -bind 192.168.103.207
然后可以通過訪問192.168.103.203:8500進入UI頁面查看信息
- client
consul agent -datacenter=dc1 -data-dir /tmp/consul -node cn1
Mac OX系統,進入consul所在目錄執行:
Sudo scp consul /usr/local/bin/
2.8、Consul DNS
DnsAgent.exe作為DNS工具
[ { "Pattern": "^.*\\.consul$", "NameServer": "127.0.0.1:8600", "QueryTimeout": 1000, "CompressionMutation": false } ]
訪問地址:http://服務名稱.service.consul
3、Ocelot
github地址:https://github.com/TomPallister/Ocelot
Ocelot的目標是使用.NET運行微服務/面向服務架構,我們需要一個統一的入口進入我們的服務,提供監控、鑒權、負載均衡等機制,也可以通過編寫中間件的形式,來擴展Ocelot的功能。 Ocelot是一堆特定順序的中間件。
3.1、Ocelot使用
- 安裝Ocelot
Install-Package Ocelot
- 引入在Program.cs中加載配置文件
public static IWebHost BuildWebHost(string[] args) { IWebHostBuilder builder = new WebHostBuilder(); //注入WebHostBuilder return builder.ConfigureServices(service => { service.AddSingleton(builder); }) //加載configuration配置文人年 .ConfigureAppConfiguration(conbuilder => { conbuilder.AddJsonFile("appsettings.json"); conbuilder.AddJsonFile("configuration.json"); }) .UseContentRoot(Directory.GetCurrentDirectory()) .UseKestrel() .UseUrls("http://*:6800") .UseStartup<Startup>() .Build(); }
- 修改Startup.cs
public void ConfigureServices(IServiceCollection services) { //注入配置文件 services.AddOcelot(Configuration); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //添加中間件 app.UseOcelot().Wait(); }
- 創建配置文件(configuration.json)
{ "ReRoutes": [ //路由配置 { "DownstreamPathTemplate": "/{url}", //下游請求路由 "DownstreamScheme": "http", //下游請求方式,有http或https "DownstreamHostAndPorts": [ //下游請求的host和端口,為了配合負載均衡,可以配置多項 { "Host": "localhost", "Port": 6801 } ], "UpstreamPathTemplate": "/basics/{url}", //上游請求路由 "UpstreamHttpMethod": [ "Get", "Post", "Delete", "Put" ], //上游請求謂詞 //"ServiceName": "BasicsService", //Consul中注冊服務的名稱 //"LoadBalancer": "RoundRobin", //負載均衡(可選)LeastConnection –請求空閑的Url RoundRobin – 輪詢請求 NoLoadBalance – 無負載均衡 //"UseServiceDiscovery": true, //是否啟用負載均衡 "ReRouteIsCaseSensitive": false, // "QoSOptions": { //熔斷設置(可選) "ExceptionsAllowedBeforeBreaking": 3, //允許異常請求數 "DurationOfBreak": 10, //熔斷時間,以秒為單位 "TimeoutValue": 5000 //請求超時數,以毫秒為單位 }, "HttpHandlerOptions": { // "AllowAutoRedirect": false, // "UseCookieContainer": false, // "UseTracing": false // }, "AuthenticationOptions": { // "AuthenticationProviderKey": "", // "AllowedScopes": [] // }, "RateLimitOptions": { //限流設置(可選) "ClientWhitelist": [ "admin" ], //白名單,不受限流控制的 "EnableRateLimiting": true, //是否啟用限流 "Period": "1m", //統計時間段:1s, 2m, 3h, 4d "PeriodTimespan": 15, //間隔多少秒后可以重試 "Limit": 100 //設定時間段內允許的最大請求數 } }, { "DownstreamPathTemplate": "/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 6802 } ], "UpstreamPathTemplate": "/invoicing/{url}", "UpstreamHttpMethod": [ "Get", "Post", "Delete", "Put" ], //"ServiceName": "InvoicingService", //"LoadBalancer": "RoundRobin", //"UseServiceDiscovery": true, "ReRouteIsCaseSensitive": false, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 }, "HttpHandlerOptions": { "AllowAutoRedirect": false, "UseCookieContainer": false, "UseTracing": false }, "AuthenticationOptions": { "AuthenticationProviderKey": "", "AllowedScopes": [] }, "RateLimitOptions": { "ClientWhitelist": [ "admin" ], "EnableRateLimiting": true, "Period": "1m", "PeriodTimespan": 15, "Limit": 100 } }, { "DownstreamPathTemplate": "/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 6806 } ], "UpstreamPathTemplate": "/authentication/{url}", "UpstreamHttpMethod": [ "Get", "Post", "Delete", "Put" ], //"ServiceName": "AuthenticationService", //"LoadBalancer": "RoundRobin", //"UseServiceDiscovery": true, "ReRouteIsCaseSensitive": false, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 }, "HttpHandlerOptions": { "AllowAutoRedirect": false, "UseCookieContainer": false, "UseTracing": false }, "AuthenticationOptions": { "AuthenticationProviderKey": "", "AllowedScopes": [] }, "RateLimitOptions": { "ClientWhitelist": [ "admin" ], "EnableRateLimiting": true, "Period": "1m", "PeriodTimespan": 15, "Limit": 100 } } ], "GlobalConfiguration": { //全局設置 "ServiceDiscoveryProvider": {//Consul服務地址,用於上方的服務發現 "Host": "localhost", "Port": 8500 }, "RateLimitOptions": { //全局限流設置(可選) "ClientIdHeader": "clientid", //識別請求頭,默認是 ClientId "QuotaExceededMessage": "access is denied", //被限流后,當請求過載時返回的提示消息 "HttpStatusCode": //600,當請求過載時返回的http狀態碼 "DisableRateLimitHeaders": false //此值指定是否禁用X-Rate-Limit和Rety-After標頭 } } }
4、Docker
容器是一個打包了應用服務的環境。它是一個輕量級的虛擬機,每一個容器由一組特定的應用和必要的依賴庫組成。
4.1、Docker-鏡像常用命令
docker images:查看本地鏡像,docker images ubu*,通配符查看 docker inspect ubuntu:查看鏡像詳細信息 docker search aspnetcore:搜索docker hub上符合要求的鏡像 docker pull microsoft/aspnetcore:拉取鏡像,在run時不用從docker hub拉取 docker rmi 鏡像ID1 鏡像ID2:刪除鏡像ID1和ID2,如果強制刪除加-f
4.2、Docker-容器常用命令
docker create ubuntu:14.04:創建容器,處於停止狀態 docker ps:查看運行的容器,加-a查看所有容器。加-l查詢出最后創建的容器,加-n=3查看最后創建的3個容器 docker start 容器名:運行已存在的容器 docker stop 容器名:停止容器 docker rm 容器名:刪除容器,docker rm $(docker ps -a -q)刪除所有容器 docker run -i -t --name ubuntu14 ubuntu:14.04 /bin/bash:運行一個ubuntu14.04的,帶終端的容器,名字叫ubuntu14 ,-i用於打開容器的標准輸入,-t讓容器建立一個命令行終端 docker run --name back_ubuntu14 -d ubuntu:14.04 /bin/sh -c "while true;do echo hello world;sleep 1;done":-d是后台開容器 docker attach 容器名:依附容器 docker logs -f --tail=5 back_ubuntu14:查看最近的5條日志 docker top 容器名:查看容器進程 docker inspect 容器名:查看容器信息,查看具體子項docker inspect --format='{{.NetworkSettings.IPAddress}}' back_ubuntu14 docker export 容器名 >容器存儲名稱.tar:導出容器 win powershell下 docker export 容器ID -o 名字.tar docker import 容器存儲名稱.tar:導入鏡像 docker commit -m="abc" --author="gsw" 容器ID 鏡像名稱:提交容器到本地鏡像
4.4、Docker-Dockerfile
FROM:指定待擴展的父級鏡像。除了注釋外,在文件開頭必須是一個FROM指令,接下來的指令便在這個父級鏡像的環境中運行,直到遇到下一個FROM指令。通過添加多個FROM指令,可以在同一個Dockerfile文件中創建多個鏡像。 MAINTAINER:用來聲明創建的鏡像的作都信息。非必需 RUN:用來修改鏡像命令,常用來安裝庫、程序 以及配置程序。一條RUN指令執行完畢后,會在當前鏡像上創建一個新的鏡像層,接下來的指令會在新的鏡像上繼續執行。 EXPOSE:用來指明容器內進程對外開放的端口,多個端口之間使用空格隔開。運行容器時,通過參數-P(大寫)即可將EXPOSE里所指定的端口映射到主機上國外的墜機端口,其隊容器或主機就可以通過映射后的端口與此容器通信。同時,我們也可以通過-p(小寫)參數將Dockerfile中EXPOSE中沒有的端口設置成公開的。 ADD:向新鏡像中添加文件,這個文件可以是一個主機文件,也可以是一個網絡文件,也可以是一個文件夾。 VOLUME:在鏡像里創建一個指定路徑的掛載點,這個路徑可以來自主機或都其他容器。多個容器可以通過同一個掛載點共享數據,即便其中一個容器已經停止,掛載點也仍然可以訪問,只有當掛載點的容器引用全部消失時,掛載點才會自動刪除。 WORKDIR:為接下來執行的指令指定一個新的工作目錄,這個目錄可以是絕對目錄,也可以是相對目錄。 ENV:設置容器運行的環境變量。在運行容器的時候,通過-e參數可以修改這個環境變量值 ,也可以添加新的環境變量 CMD:用來設置啟動容器時默認運行的命令。 ENTRYPOINT:與CMD類似,它也是用來指定容器啟動時默認運行的命令。 USER:為容器的運行及接下來RUN、CMD、ENTRYPOINT等指令的運行指定用戶或UID ONBUILD:觸發指令。構建鏡像的時候,Docker的鏡像構建器會將所有的ONBUILD指令指定的命令保存到鏡像的元數據中,這些命令在當前鏡像的構建的構建過程中並不會執行。只有新的鏡像用用FRMO指令指定父鏡像為這個鏡像時,便會觸發。
4.5、Docker生成asp.net core鏡像和運行
發布asp.net core項目,並在發布文件夾下創建Dockerfile文件,復制下面內容
#父鏡像 FROM microsoft/aspnetcore #設置工作目錄 WORKDIR /app #復制發布文件到/app下 COPY . /app #設置端口 EXPOSE 80 #使用dotnet XXXXXXXXX.dll來運行asp.net core項目,注意大小寫 ENTRYPOINT ["dotnet", “XXXXXXXXX.dll"]
4.6、Docker生成asp.net core鏡像和運行
docker build -t xxxxxxxxxxx:latest . docker run -it -p 6801:6801 xxxxxxxxxxx:latest
注意:docker內部web的端口, 上述命令中,第二個端口為docker內web的端口。
5、App.Metrics+InfluxDB+Grafana
建議:建議在網關上進行監控,因為網關上監控可以監控所有
App.Metrics:https://www.app-metrics.io
InfluxDB1.5.1-1:https://portal.influxdata.com
Grafana-5.0.4:https://grafana.com/get
5.1、安裝使用
- 下載 influxdb
https://portal.influxdata.com - 下載 Grafana
https://grafana.com/get - 運行influxdb-版本號下的influxd.exe
- 運行grafana-版本號下,bin目錄下grafana-server.exe
- 運行influxdb-版本號下的influx.exe,輸入 create database influxdbtest 創建數據庫,同時 create user "user1" with password '123456' 創建用戶
- 配置Grafana,然后啟動網關程序,登錄localhost:3000查看監控信息,用戶名密碼是:admin
5.2、配置Grafana
5.2.1、配置數據源
5.2.2、配置Dashboard
我們采用模板導入模式,將項目引用 App.Metrics 並訪問 App.Metrics 源地址:https://www.app-metrics.io/
獲取到InfluxDB對應的儀表盤編號2125,然后輸入使用該模板
導入成功后
5.3、App.Metrics監控數據采集
- 項目nuget引用
Install-Package App.Metrics Install-Package App.Metrics.AspNetCore.Endpoints Install-Package App.Metrics.AspNetCore.Reporting Install-Package App.Metrics.AspNetCore.Tracking Install-Package App.Metrics.Reporting.InfluxDB
- 修改配置文件 appsettings.json
"InfluxDB": { "IsOpen": true,//是否開啟監控 "DataBaseName": "influxdbtest",//數據庫名稱 "ConnectionString": "http://localhost:8086",//數據庫地址 "username": "user1",//用戶名 "password": "123456",//密碼 "app": "HIS",//自定義名字 "env": "Ocelot"//自定義環境 }
- 修改 Startup.cs
public void ConfigureServices(IServiceCollection services) { ... #region Metrics監控配置 string IsOpen = Configuration.GetSection("InfluxDB:IsOpen").Value.ToLower();//是否打開 if (IsOpen == "true") { string database = Configuration.GetSection("InfluxDB")["DataBaseName"];//數據庫名稱 string InfluxDBConStr = Configuration.GetSection("InfluxDB")["ConnectionString"];//數據庫地址 string app = Configuration.GetSection("InfluxDB")["app"];//自定義名字 string env = Configuration.GetSection("InfluxDB")["env"];//自定義環境 string username = Configuration.GetSection("InfluxDB")["username"];//用戶名 string password = Configuration.GetSection("InfluxDB")["password"];//密碼 var uri = new Uri(InfluxDBConStr); //配置metrics var metrics = AppMetrics.CreateDefaultBuilder() .Configuration.Configure( options => { options.AddAppTag(app); options.AddEnvTag(env); }) .Report.ToInfluxDb( options => { options.InfluxDb.BaseUri = uri; options.InfluxDb.Database = database; options.InfluxDb.UserName = username; options.InfluxDb.Password = password; options.HttpPolicy.BackoffPeriod = TimeSpan.FromSeconds(30); options.HttpPolicy.FailuresBeforeBackoff = 5; options.HttpPolicy.Timeout = TimeSpan.FromSeconds(10); options.FlushInterval = TimeSpan.FromSeconds(5); }) .Build(); services.AddMetrics(metrics);//注入metrics services.AddMetricsReportScheduler();//報表 services.AddMetricsTrackingMiddleware();//追蹤 services.AddMetricsEndpoints(); //終點 } #endregion ... }
public async void Configure(IApplicationBuilder app, IHostingEnvironment env) { #region 使用中間件Metrics string IsOpen = Configuration.GetSection("InfluxDB")["IsOpen"].ToLower(); if (IsOpen == "true") { app.UseMetricsAllMiddleware(); app.UseMetricsAllEndpoints(); } #endregion //使用中間件 //await app.UseOcelot(); }
接下來就可以進行追蹤了
5.4、APM-Grafana告警
- 打開grafana-版本號下,conf目錄下的 defaults.ini ,填寫[smtp]節點信息
[smtp] enabled = true host = smtp.163.com:25 user =ego_it # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" password =****** cert_file = key_file = skip_verify = false from_address = ego_it@163.com from_name = Grafana ehlo_identity =
- 配置
- 添加報警觸發條件
添加新的查詢條件
創建alert
6、Exceptionless
- 在線方式
https://exceptionless.com/注冊用戶,新建Organizations和Project,並選項目類型。 - 離線方式
下載地址:https://github.com/exceptionless/Exceptionless/releases
解壓壓縮包,運行Start.bat
系統會自動下載elasticsearch和kibanaElasticSearch是一個基於Lucene的搜索服務器。它提供了一個分布式多用戶能力的全文搜索引擎,基於RESTful web接口。Elasticsearch是用Java開發的,並作為Apache許可條款下的開放源碼發布,是當前流行的企業級搜索引擎。設計用於雲計算中,能夠達到實時搜索,穩定,可靠,快速,安裝使用方便。
Kibana是一個開源的分析與可視化平台,設計出來用於和Elasticsearch一起使用的。你可以用kibana搜索、查看、交互存放在Elasticsearch索引里的數據,使用各種不同的圖表、表格、地圖等kibana能夠很輕易地展示高級數據分析與可視化。
6.1、創建組織
6.2、創建項目
6.3、集成Exceptionless 客戶端
Install-Package Exceptionless.AspNetCore
通過 API 密鑰執行 app.UseExceptionless("Qa3OzvEJC9FXo9SdwwFBv6bAkVbjWQKbV6hhtYEM") 方法
6.4、示例代碼
#region Exceptionless測試 try { ExceptionlessClient.Default.SubmitLog("調試Exceptionless.Logging.LogLevel.Debu", Exceptionless.Logging.LogLevel.Debug); ExceptionlessClient.Default.SubmitLog("錯誤Exceptionless.Logging.LogLevel.Error", Exceptionless.Logging.LogLevel.Error); ExceptionlessClient.Default.SubmitLog("大錯Exceptionless.Logging.LogLevel.fatal", Exceptionless.Logging.LogLevel.Fatal); ExceptionlessClient.Default.SubmitLog(" Exceptionless.Logging.LogLevel.Info", Exceptionless.Logging.LogLevel.Info); ExceptionlessClient.Default.SubmitLog(" Exceptionless.Logging.LogLevel.Off", Exceptionless.Logging.LogLevel.Off); ExceptionlessClient.Default.SubmitLog(" Exceptionless.Logging.LogLevel.Other", Exceptionless.Logging.LogLevel.Other); ExceptionlessClient.Default.SubmitLog(" Exceptionless.Logging.LogLevel.Trace", Exceptionless.Logging.LogLevel.Trace); ExceptionlessClient.Default.SubmitLog("Exceptionless.Logging.LogLevel.Warn", Exceptionless.Logging.LogLevel.Warn); var data = new Exceptionless.Models.DataDictionary(); data.Add("data1key", "data1value"); ExceptionlessClient.Default.SubmitEvent(new Exceptionless.Models.Event { Count = 1, Date = DateTime.Now, Data = data, Geo = "geo", Message = "message", ReferenceId = "referencelId", Source = "source", Tags = new Exceptionless.Models.TagSet() { "tags" }, Type = "type", Value = 1.2m }); ExceptionlessClient.Default.SubmitFeatureUsage("feature"); ExceptionlessClient.Default.SubmitNotFound("404 not found"); ExceptionlessClient.Default.SubmitException(new Exception("自定義異常")); throw new DivideByZeroException("throw DivideByZeroException的異常:" + DateTime.Now); } catch (Exception exc) { exc.ToExceptionless().Submit(); } #endregion
6.5、本地部署
下載Windows版本安裝包,並進行解壓,然后雙擊運行Start.bat即可
需要環境:
- .NET 4.6
- Java 1.8+ (The JAVA_HOME environment variable must also be configured when using Windows.)
- IIS Express 8+
- PowerShell 3+
6.6、項目集成
注意:本地化不能再使用 app.UseExceptionless(apiKey: "tJxBWkCbgDLCMoKKqWII3Eyw4aJOsyOCgX26Yurm"); 形式來上傳日志數據,應采用另外的方式:配置文件方式
"Exceptionless": { "ApiKey": "tJxBWkCbgDLCMoKKqWII3Eyw4aJOsyOCgX26Yurm", "ServerUrl": "http://localhost:50000", "DefaultData": { }, "DefaultTags": [ "xplat" ], "Settings": { "FeatureXYZEnabled": false } }
然后修改 Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... //app.UseExceptionless(apiKey: "tJxBWkCbgDLCMoKKqWII3Eyw4aJOsyOCgX26Yurm"); //上方的方法本地化不適用 app.UseExceptionless(Configuration); ... }
搞定
6.7、查詢語法
示例
6.8、常見問題
Invoke-WebRequest : 請求被中止: 未能創建 SSL/TLS 安全通道。
elasticsearch-XXX”,因為該路徑不存在。
解決方案:編輯Start-ElasticSearch.ps1,將所需的文件全部下載下來,然后解壓進行拷貝,如下圖,然后在雙擊運行Start.bat即可
幫助類:

/// <summary> /// /// </summary> public static class ExceptionLessLog { static bool IsInit = false; static ExceptionLessLog() { if (!IsInit) { #region Exceptionless配置 ExceptionlessClient.Default.Configuration.ApiKey = "KwqNUJ5njrnOehQTSYY6yXXXXXXXXXXXXXXX"; ExceptionlessClient.Default.Configuration.ServerUrl = "http://XXX.XXX.XXX.XXX:50000"; ExceptionlessClient.Default.Startup(); #endregion } } #region 日志功能 /// <summary> /// 跟蹤 /// </summary> public static void Trace(string message, params string[] tags) { ExceptionlessClient.Default.CreateLog(message, LogLevel.Trace).AddTags(tags).Submit(); } /// <summary> /// 調試 /// </summary> public static void Debug(string message, params string[] tags) { ExceptionlessClient.Default.CreateLog(message, LogLevel.Debug).AddTags(tags).Submit(); } /// <summary> /// 信息 /// </summary> public static void Info(string message, params string[] tags) { ExceptionlessClient.Default.CreateLog(message, LogLevel.Info).AddTags(tags).Submit(); } /// <summary> /// 警告 /// </summary> public static void Warn(string message, params string[] tags) { ExceptionlessClient.Default.CreateLog(message, LogLevel.Warn).AddTags(tags).Submit(); } /// <summary> /// 錯誤 /// </summary> public static void Error(string message, params string[] tags) { ExceptionlessClient.Default.CreateLog(message, LogLevel.Error).AddTags(tags).Submit(); } #endregion /// <summary> /// 異常捕獲提交 /// </summary> /// <param name="exception"></param> /// <param name="pluginContextData"></param> /// <param name="client"></param> public static void Submit(this Exception exception, ContextData pluginContextData = null, ExceptionlessClient client = null) { exception.ToExceptionless().Submit(); } }
7、數據一致性
- C:數據一致性(consistency):如果系統對一個寫操作返回成功,那么之后的讀請求都必須讀到這個新數據;如果返回失敗,那么所有讀操作都不能讀到這個數據,對調用者而言數據具有強一致性(strong consistency) (又叫原子性 atomic、線性一致性 linearizable consistency)
- A:服務可用性(availability):所有讀寫請求在一定時間內得到響應,可終止、不會一直等待
- P:分區容錯性(partition-tolerance):在網絡分區的情況下,被分隔的節點仍能正常對外服務
7.1、最終一致性
- 可用性,可靠性,
- 最終一致性:在微服務之間使用事件驅動通信和發布訂閱系統實現最終一致性
- 強一致性:當更新操作完成之后,任何多個后續進程或者線程的訪問都會返回最新的更新過的值。這種是對用戶最友好的,就是用戶上一次寫什么,下一次就保證能讀到什么。根據 CAP 理論,這種實現需要犧牲可用性。=> 在傳統單體式應用中,大部分都是強一致性的應用,想想我們寫過多少工作單元模式的Code?
- 弱一致性:系統並不保證續進程或者線程的訪問都會返回最新的更新過的值。系統在數據寫入成功之后,不承諾立即可以讀到最新寫入的值,也不會具體的承諾多久之后可以讀到。
- 最終一致性:弱一致性的特定形式。系統保證在沒有后續更新的前提下,系統最終返回上一次更新操作的值。在沒有故障發生的前提下,不一致窗口的時間主要受通信延遲,系統負載和復制副本的個數影響。
- 為保證可用性,互聯網分布式架構中經常將強一致性需求轉換成最終一致性的需求,並通過系統執行冪等性的保證,保證數據的最終一致性。
在微服務架構中,各個微服務之間通常會使用事件驅動通信和發布訂閱系統實現最終一致性。
7.2、最終一致性-補償機制
- Polly:實現重試,熔斷機制
- 或提供后台任務調度實現補償
7.3、冪等和防重
- 其任意多次執行對資源本身所產生的影響均與一次執行的影響相同。
- 對重復刪除或返回成功結果;防重可以在數據庫級別處理也以以在MQ級別
7.4、MassTransit
MassTransit 是一個自由、開源、輕量級的消息總線, 用於使用. NET 框架創建分布式應用程序。MassTransit 在現有消息傳輸上提供了一組廣泛的功能, 從而使開發人員能夠友好地使用基於消息的會話模式異步連接服務。基於消息的通信是實現面向服務的體系結構的可靠和可擴展的方式。
官網地址:http://masstransit-project.com/,GitHub地址:https://github.com/MassTransit/MassTransit (目前:1590Star,719Fork)
類似的國外開源組件還有NServiceBus,沒有用過,據說MassTransit比NServiceBus更加輕量級,並且在開發之初就選用了RabbitMQ作為消息傳輸組件,當然MassTransit還支持Azure Service Bus。類似的國內開源組件則有園友savorboard(楊曉東)的CAP
7.5、最簡單的發送/接收實例
這里以MassTransit + RabbitMQ為例子,首先請確保安裝了RabbitMQ,如果沒有安裝,可以閱讀我的RabbitMQ在Windows環境下的安裝與使用去把RabbitMQ先安裝到你的電腦上。另外,RabbitMQ的背景知識也有一堆,有機會也還是要了解下Exchange,Channel、Queue等內容。
- 准備兩個控制台程序,一個為Sender(發送者),一個為Receiver(接收者),並分別通過NuGet安裝MassTransit以及MassTransit.RabbitMQ
Install-Package MassTransit Install-Package MassTransit.RabbitMQ
- 編寫Sender
class Program { static void Main(string[] args) { Console.Title = "客戶端"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost/"), hst => { hst.Username("guest"); hst.Password("guest"); }); }); var uri = new Uri("rabbitmq://localhost/wyt"); var mes = Console.ReadLine(); while (null != mes) { Task.Run(() => SendCommand(bus, uri, mes)).Wait(); mes = Console.ReadLine(); } Console.ReadLine(); } private static async void SendCommand(IBusControl bus, Uri sendToUri, string mes) { var endPoint = await bus.GetSendEndpoint(sendToUri); var command=new Receiver.ABC() { Name = "張三", Birthday = DateTime.Now, Message = mes }; await endPoint.Send(command); Console.WriteLine($"發送的實體 Name={command.Name},Birthday={command.Birthday},Message={command.Message}"); } }
- 編寫Receiver
class Program { static void Main(string[] args) { Console.Title = "服務端"; //創建基於RabbitMq的bus var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost/"), hst => { hst.Username("guest"); hst.Password("guest"); }); cfg.ReceiveEndpoint(host, "wyt", e => { e.Consumer<ConsumerABC>(); e.Consumer<ConsumerABC1>(); }); }); bus.Start(); Console.WriteLine("按任意鍵退出!"); Console.ReadLine(); bus.Stop(); } } public class ConsumerABC : IConsumer<ABC> { public async Task Consume(ConsumeContext<ABC> context) { await Console.Out.WriteLineAsync($"收到信息: {context.Message.Name},{context.Message.Birthday},{context.Message.Message}"); } } public class ConsumerABC1 : IConsumer<ABC> { public async Task Consume(ConsumeContext<ABC> context) { await Console.Out.WriteLineAsync($"收到信息1: {context.Message.Name},{context.Message.Birthday},{context.Message.Message}"); } } public class ABC { public DateTime Birthday { get; set; } public string Name { get; set; } public string Message { get; set; } }
對於Receiver,要做的事就只有兩件:一是連接到RabbitMQ,二是告訴RabbitMQ我要接收哪個消息隊列的什么類型的消息。
- 測試一下:
7.6、一對一的發布/訂閱實例(類似於RabbitMQ的工作模式)
除了簡單的發送/接收模式外,我們用的更多的是發布/訂閱這種模式。
注意:發布方如果發布時沒有訂閱方,發布的數據將會丟失
- 准備下圖所示的類庫和控制台項目,並對除Messages類庫之外的其他項目安裝MassTransit以及MassTransit.RabbitMQ。
- MEDemo_Entity 類庫:准備需要傳輸的實體類信息
public interface IEntity { int ID { get; set; } } public class Entity:IEntity { public int ID { get; set; } public string Name { get; set; } public DateTime Time { get; set; } } public class MyEntity:Entity { public int Age { get; set; } } public class YouEntity { public int ID { get; set; } public string Name { get; set; } public DateTime Time { get; set; } public int Age { get; set; } }
- MEDemo_Producer :接收我的消息吧騷年們
static void Main(string[] args) { Console.Title = "發布方"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost"), hst => { hst.Username("guest"); hst.Password("guest"); }); }); bus.Start(); do { Console.WriteLine("請出請按q,否則請按其他鍵!"); string value = Console.ReadLine(); if (value.ToLower() == "q") { break; } bus.Publish(new Entity() { ID = 1, Name = "張三", Time = DateTime.Now }); } while (true); bus.Stop(); }
這里向RabbitMQ發布了兩個不同類型的消息(Entity和IEntity)
- MEDemo_ConsumerA :我只接收Entity和IEntity類型的消息,其他的我不要
class Program { static void Main(string[] args) { Console.Title="訂閱方"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost/"), hst => { hst.Username("guest"); hst.Password("guest"); }); cfg.ReceiveEndpoint(host, "mewyt", e => { e.Consumer<IEntityConsumer>(); e.Consumer<EntityConsumer>(); e.Consumer<MyEntityConsumer>(); }); cfg.ReceiveEndpoint(host, "mewyt1", e => { e.Consumer<YouEntityConsumer>(); }); }); bus.Start(); Console.ReadLine(); bus.Stop(); } } public class IEntityConsumer : IConsumer<IEntity> { public async Task Consume(ConsumeContext<IEntity> context) { await Console.Out.WriteLineAsync($"IEntityConsumer 類型 {context.Message.GetType()} {context.Message.ID}"); } } public class EntityConsumer : IConsumer<Entity> { public async Task Consume(ConsumeContext<Entity> context) { await Console.Out.WriteLineAsync($"EntityConsumer 類型 {context.Message.GetType()} {context.Message.ID} {context.Message.Name} {context.Message.Time}"); } } public class MyEntityConsumer : IConsumer<MyEntity> { public async Task Consume(ConsumeContext<MyEntity> context) { await Console.Out.WriteLineAsync($"MyEntityConsumer 類型 {context.Message.GetType()} {context.Message.ID} {context.Message.Name} {context.Message.Time} {context.Message.Age}"); } } public class YouEntityConsumer : IConsumer<YouEntity> { public async Task Consume(ConsumeContext<YouEntity> context) { await Console.Out.WriteLineAsync($"YouEntityConsumer 類型 {context.Message.GetType()} {context.Message.ID} {context.Message.Name} {context.Message.Time} {context.Message.Age}"); } }
- 測試一下:啟動兩個MEDemo_ConsumerA,一個MEDemo_Producer
7.6、一對多的發布/訂閱實例(隊列名不同即可)
- PSDemo_Entity 類庫:准備需要傳輸的實體類信息
public class EntityA { public string Name { get; set; } public DateTime Time { get; set; } } public class EntityB { public string Name { get; set; } public DateTime Time { get; set; } public int Age { get; set; } }
- PSDemo_Publisher :發布消息
class Program { static void Main(string[] args) { var bus= Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost/"), hst => { hst.Username("guest"); hst.Password("guest"); }); }); do { Console.WriteLine("請出請按q,否則請按其他鍵!"); string value = Console.ReadLine(); if (value.ToLower() == "q") { break; } bus.Publish(new PSDemo_Entity.EntityA() { Name="張三", Time = DateTime.Now }); bus.Publish(new PSDemo_Entity.EntityB() { Name = "李四", Time = DateTime.Now,Age=22 }); } while (true); bus.Stop(); } }
- PSDemo_SubscriberA :訂閱EntityA和EntityB
class Program { static void Main(string[] args) { Console.Title="訂閱者A"; var bus= Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost/"), hst => { hst.Username("guest"); hst.Password("guest"); }); cfg.ReceiveEndpoint(host, "wytPSA", e => { e.Consumer<ConsumerA>(); e.Consumer<ConsumerB>(); }); }); bus.Start(); Console.ReadLine(); bus.Stop(); } } public class ConsumerA : IConsumer<PSDemo_Entity.EntityA> { public async Task Consume(ConsumeContext<PSDemo_Entity.EntityA> context) { await Console.Out.WriteLineAsync($"訂閱者A ConsumerA收到信息: {context.Message.Name} {context.Message.Time} 類型:{context.Message.GetType()}"); } } public class ConsumerB : IConsumer<PSDemo_Entity.EntityB> { public async Task Consume(ConsumeContext<PSDemo_Entity.EntityB> context) { await Console.Out.WriteLineAsync($"訂閱者A ConsumerB收到信息: {context.Message.Name} {context.Message.Time} 類型:{context.Message.GetType()}"); } }
- PSDemo_SubscriberB :訂閱EntityA
class Program { static void Main(string[] args) { Console.Title="訂閱者B"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost/"), hst => { hst.Username("guest"); hst.Password("guest"); }); cfg.ReceiveEndpoint(host, "wytPSB", e => { e.Consumer<ConsumerA>(); }); }); bus.Start(); Console.ReadLine(); bus.Stop(); } } public class ConsumerA : IConsumer<PSDemo_Entity.EntityA> { public async Task Consume(ConsumeContext<PSDemo_Entity.EntityA> context) { await Console.Out.WriteLineAsync($"訂閱者B ConsumerA收到信息: {context.Message.Name} {context.Message.Time} 類型:{context.Message.GetType()}"); } }
- 測試一下:啟動PSDemo_SubscriberA和PSDemo_SubscriberB,一個PSDemo_Publisher
7.7、帶返回狀態消息的示例
之前的例子都是發布之后,不管訂閱者有沒有收到以及收到后有沒有處理成功(即有沒有返回消息,類似於HTTP請求和響應),在MassTransit中提供了這樣的一種模式,並且還可以結合GreenPipes的一些擴展方法實現重試、限流以及熔斷機制。這一部分詳見官方文檔:http://masstransit-project.com/MassTransit/usage/request-response.html
- 准備下圖所示的三個項目:通過NuGet安裝MassTransit以及MassTransit.RabbitMQ
- RRDemo_Entity.Entity :准備請求和響應的消息傳輸類型
public interface IRequestEntity { int ID { get; set; } string Name { get; set; } } public class RequestEntity : IRequestEntity { public int ID { get; set; } public string Name { get; set; } } public interface IResponseEntity { int ID { get; set; } string Name { get; set; } int RequestID { get; set; } } public class ResponseEntity : IResponseEntity { public int ID { get; set; } public string Name { get; set; } public int RequestID { get; set; } }
- RRDemo_Server.Program 請求接收端
class Program { static void Main(string[] args) { Console.Title = "應答方"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost/"), hst => { hst.Username("guest"); hst.Password("guest"); }); cfg.ReceiveEndpoint(host, "request_response_wyt", e => { e.Consumer<RequestConsumer>(); }); }); bus.Start(); Console.ReadLine(); bus.Stop(); } } public class RequestConsumer : IConsumer<IRequestEntity> { public async Task Consume(ConsumeContext<IRequestEntity> context) { Console.ForegroundColor = ConsoleColor.Red; await Console.Out.WriteLineAsync($"收到請求id={context.Message.ID} name={context.Message.Name}"); Console.ResetColor(); var response = new ResponseEntity { ID = 22, Name = $"李四", RequestID = context.Message.ID }; Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"應答ID={response.ID},Name={response.Name},RequestID={response.RequestID}"); Console.ResetColor(); context.Respond(response); } }
- RRDemo_Client.Program 請求發送端
static void Main(string[] args) { Console.Title = "請求方"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost/"), hst => { hst.Username("guest"); hst.Password("guest"); }); //重試 cfg.UseRetry(ret => { ret.Interval(3, 10);// 消費失敗后重試3次,每次間隔10s }); //限流 cfg.UseRateLimit(1000, TimeSpan.FromSeconds(100));// 1分鍾以內最多1000次調用訪問 //熔斷 cfg.UseCircuitBreaker(cb => { cb.TrackingPeriod = TimeSpan.FromSeconds(60);//1分鍾 cb.TripThreshold = 15;// 當失敗的比例至少達到15%才會啟動熔斷 cb.ActiveThreshold = 10;// 當失敗次數至少達到10次才會啟動熔斷 cb.ResetInterval = TimeSpan.FromMinutes(5);// 當在1分鍾內消費失敗率達到15%或調用了10次還是失敗時,暫停5分鍾的服務訪問 }); }); bus.Start(); var serviceAddress = new Uri($"rabbitmq://localhost/request_response_wyt"); var client = bus.CreateRequestClient<IRequestEntity, IResponseEntity>(serviceAddress, TimeSpan.FromHours(10)); // 創建請求客戶端,10H之內木有回饋則認為是超時(Timeout) while (true) { Console.WriteLine("請出請按q,否則請按其他鍵!"); string value = Console.ReadLine(); if (value.ToLower() == "q") { break; } Task.Run(async () => { var request = new RequestEntity() { ID = 1, Name = "張三" }; var response = await client.Request(request); Console.WriteLine($"請求ID={request.ID},Name={request.Name}"); Console.WriteLine($"應簽ID={response.ID},Name={response.Name},RequestID={response.RequestID}"); }).Wait(); } }
- 效果展示
注意:這里的請求方關閉后應答方則無法將應答再回復給請求方,會丟失
7.8、帶Observer模式的發布/訂閱示例
在某些場景中,我們需要針對一個消息進行類似於AoP(面向切面編程)或者監控的操作,比如在發送消息之前和結束后記日志等操作,我們可以借助MassTransit中的Observer模式來實現。(在MassTransit的消息接收中,可以通過兩種模式來實現:一種是基於實現IConsumer接口,另一種就是基於實現IObserver接口)關於這一部分,詳見官方文檔:http://masstransit-project.com/MassTransit/usage/observers.html
- 准備以下圖所示的項目:
- ObserverSubscriber
class Program { static void Main(string[] args) { Console.Title = "訂閱方"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost/"), hst => { hst.Username("guest"); hst.Password("guest"); }); cfg.ReceiveEndpoint(host, "ObserverTest", e => { e.Consumer<EntityConsumer>(); }); }); var observer = new ReceiveObserver(); var handle = bus.ConnectReceiveObserver(observer); bus.Start(); Console.ReadLine(); bus.Stop(); } } public class ReceiveObserver : IReceiveObserver { public Task PreReceive(ReceiveContext context) { Console.WriteLine("------------------PreReceive--------------------"); Console.WriteLine(Encoding.Default.GetString(context.GetBody())); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } public Task PostReceive(ReceiveContext context) { Console.WriteLine("-----------------PostReceive---------------------"); Console.WriteLine(Encoding.Default.GetString(context.GetBody())); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } public Task PostConsume<T>(ConsumeContext<T> context, TimeSpan duration, string consumerType) where T : class { Console.WriteLine("------------------PostConsume--------------------"); Console.WriteLine($"ID={ (context.Message as Entity).ID},Name={(context.Message as Entity).Name},Time={(context.Message as Entity).Time}"); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } public Task ConsumeFault<T>(ConsumeContext<T> context, TimeSpan elapsed, string consumerType, Exception exception) where T : class { Console.WriteLine("-----------------ConsumeFault---------------------"); Console.WriteLine($"ID={ (context.Message as Entity).ID},Name={(context.Message as Entity).Name},Time={(context.Message as Entity).Time}"); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } public Task ReceiveFault(ReceiveContext context, Exception exception) { Console.WriteLine("----------------ReceiveFault----------------------"); Console.WriteLine(Encoding.Default.GetString(context.GetBody())); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } } public class EntityConsumer : IConsumer<Entity> { public async Task Consume(ConsumeContext<Entity> context) { await Console.Out.WriteLineAsync($"IEntityConsumer 類型 {context.Message.GetType()} {context.Message.ID} {context.Message.Age} {context.Message.Name} {context.Message.Time}"); } } public class Entity { public int ID { get; set; } public int Age { get; set; } public string Name { get; set; } public DateTime Time { get; set; } }
- ObserverPublish
class Program { static void Main(string[] args) { Console.Title = "發布方"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://localhost"), hst => { hst.Username("guest"); hst.Password("guest"); }); }); var observer = new SendObserver(); var handle = bus.ConnectSendObserver(observer); var observer1 = new PublishObserver(); var handle1 = bus.ConnectPublishObserver(observer1); bus.Start(); do { Console.WriteLine("請出請按q,否則請按其他鍵!"); string value = Console.ReadLine(); if (value.ToLower() == "q") { break; } bus.Publish(new Entity() { ID = 1, Age = 10, Name = "張三", Time = DateTime.Now }); } while (true); bus.Stop(); } } public class PublishObserver : IPublishObserver { public Task PrePublish<T>(PublishContext<T> context) where T : class { Console.WriteLine("------------------PrePublish--------------------"); Console.WriteLine($"ID={ (context.Message as Entity).ID},Name={(context.Message as Entity).Name},Time={(context.Message as Entity).Time}"); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } public Task PostPublish<T>(PublishContext<T> context) where T : class { Console.WriteLine("------------------PostPublish--------------------"); Console.WriteLine($"ID={ (context.Message as Entity).ID},Name={(context.Message as Entity).Name},Time={(context.Message as Entity).Time}"); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } public Task PublishFault<T>(PublishContext<T> context, Exception exception) where T : class { Console.WriteLine("------------------PublishFault--------------------"); Console.WriteLine($"ID={ (context.Message as Entity).ID},Name={(context.Message as Entity).Name},Time={(context.Message as Entity).Time}"); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } } public class SendObserver : ISendObserver { public Task PreSend<T>(SendContext<T> context) where T : class { Console.WriteLine("------------------PreSend--------------------"); Console.WriteLine($"ID={ (context.Message as Entity).ID},Name={(context.Message as Entity).Name},Time={(context.Message as Entity).Time}"); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } public Task PostSend<T>(SendContext<T> context) where T : class { Console.WriteLine("------------------PostSend--------------------"); Console.WriteLine($"ID={ (context.Message as Entity).ID},Name={(context.Message as Entity).Name},Time={(context.Message as Entity).Time}"); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } public Task SendFault<T>(SendContext<T> context, Exception exception) where T : class { Console.WriteLine("------------------SendFault--------------------"); Console.WriteLine($"ID={ (context.Message as Entity).ID},Name={(context.Message as Entity).Name},Time={(context.Message as Entity).Time}"); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } }
- 效果展示
Publish:
請出請按q,否則請按其他鍵! 111111 請出請按q,否則請按其他鍵! ------------------PrePublish-------------------- ID=1,Name=張三,Time=2019/5/2 12:23:23 -------------------------------------- ------------------PreSend-------------------- ID=1,Name=張三,Time=2019/5/2 12:23:23 -------------------------------------- ------------------PostSend-------------------- ID=1,Name=張三,Time=2019/5/2 12:23:23 -------------------------------------- ------------------PostPublish-------------------- ID=1,Name=張三,Time=2019/5/2 12:23:23 --------------------------------------
------------------PreReceive-------------------- { "messageId": "36500000-a632-9828-3029-08d6ceb5ea32", "conversationId": "36500000-a632-9828-ef7f-08d6ceb5ea37", "sourceAddress": "rabbitmq://localhost/bus-DESKTOP-PUOA6D7-dotnet-g3eyyyfggkcnt4wdbdmc7pxgn4?durable=false&autodelete=true", "destinationAddress": "rabbitmq://localhost/ObserverSubscriber:Entity", "messageType": [ "urn:message:ObserverSubscriber:Entity" ], "message": { "id": 1, "age": 10, "name": "張三", "time": "2019-05-02T12:23:23.2223222+08:00" }, "sentTime": "2019-05-02T04:23:23.3522586Z", "headers": {}, "host": { "machineName": "DESKTOP-PUOA6D7", "processName": "dotnet", "processId": 25668, "assembly": "ObserverPublish", "assemblyVersion": "1.0.0.0", "frameworkVersion": "4.0.30319.42000", "massTransitVersion": "4.1.0.1470", "operatingSystemVersion": "Microsoft Windows NT 6.2.9200.0" } } -------------------------------------- IEntityConsumer 類型 ObserverSubscriber.Entity 1 10 張三 2019/5/2 12:23:23 ------------------PostConsume-------------------- ID=1,Name=張三,Time=2019/5/2 12:23:23 -------------------------------------- -----------------PostReceive--------------------- { "messageId": "36500000-a632-9828-3029-08d6ceb5ea32", "conversationId": "36500000-a632-9828-ef7f-08d6ceb5ea37", "sourceAddress": "rabbitmq://localhost/bus-DESKTOP-PUOA6D7-dotnet-g3eyyyfggkcnt4wdbdmc7pxgn4?durable=false&autodelete=true", "destinationAddress": "rabbitmq://localhost/ObserverSubscriber:Entity", "messageType": [ "urn:message:ObserverSubscriber:Entity" ], "message": { "id": 1, "age": 10, "name": "張三", "time": "2019-05-02T12:23:23.2223222+08:00" }, "sentTime": "2019-05-02T04:23:23.3522586Z", "headers": {}, "host": { "machineName": "DESKTOP-PUOA6D7", "processName": "dotnet", "processId": 25668, "assembly": "ObserverPublish", "assemblyVersion": "1.0.0.0", "frameworkVersion": "4.0.30319.42000", "massTransitVersion": "4.1.0.1470", "operatingSystemVersion": "Microsoft Windows NT 6.2.9200.0" } } --------------------------------------
7.9、數據一致性示例
詳見:https://github.com/786744873/DataConsistentSample
8、Jenkins
官方地址:https://jenkins.io/
Jenkins 是一款流行的開源持續集成(CI)與持續部署(CD)工具,廣泛用於項目開發,具有自動化構建、測試和部署等功能。
使用Jenkins的目的在於:
(1)持續、自動地構建/測試軟件項目。
(2)監控軟件開放流程,快速問題定位及處理,提升開發效率。
8.1、Jenkins下載與安裝
這里我們下載Windows版本的
安裝完成后會提示我們解鎖 Jenkins
這里選擇安裝推薦的插件
創建管理賬戶 => 也可以直接使用admin賬戶繼續
配置Jenkins端口,默認8080,最好不要使用8080端口
修改Jenkins服務端口,改為8080-->8081
修改安裝目錄下 jenkins.xml 文件
然后重啟Jenkins服務
8.2、持續集成Asp.Net Core項目
- 我們以Github上面的項目為例,github項目地址:https://github.com/786744873/first.git
- 配置源代碼
- 構建觸發器(這里每半小時輪詢一次)
- 構建
cd CITest cd CITest dotnet restore dotnet build dotnet publish -o "bin\Debug\netcoreapp2.0\publish" cd bin\Debug\netcoreapp2.0\publish docker rm clitest -f docker rmi clitest -f docker build -t clitest:latest . docker run -p 4555:4555 -d --name clitest clitest:latest
- 保存,然后去配置構建郵件發送
Jenkins->系統管理->系統設置
設置系統管理員收件地址(實際上這邊配置的是發件人的郵箱地址): -
繼續進行項目配置
- 構建項目