以前dotnet web應用程序開發完成后,我們都是使用IIS部署在Windows Server上,如今netcore技術發展迅速,因為其跨平台的特性,將dotnet web應用程序部署在更方便部署和更廉價的Linux服務器上日益流行。這里簡單介紹如何使用Nginx/Systemd/Kestrel將netcore web應用程序部署在Centos系統上。將會涉及兩個概念:反向代理和負載均衡。
1.Nginx/Systemd/Kestrel托管netcore應用
1.1 准備netcore web應用
首先創建一個netcore mvc項目,修改 Startup.cs,添加了加粗部分代碼,如下:
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.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } //轉接頭中間件 app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); app.UseStaticFiles(); app.UseCookiePolicy(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } }
修改Program.cs,如下:
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) { //添加額外的配置 var cfg = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .AddJsonFile("hosting.json") .Build(); return WebHost.CreateDefaultBuilder(args) .UseConfiguration(cfg) .UseStartup<Startup>(); } }
hosting.json主要是為了設置部署的端口,內容如下
{ "server.urls": "http://0.0.0.0:1111"}
這里只是演示部署,HomeController和Index的邏輯很簡單,僅僅展示X-Forwarded-For和Host主機:
//HomeController public class HomeController : Controller { public IActionResult Index() {
//X-Forwarded-For可用於獲取真實的客戶端IP,格式為IP1,IP2...,其中第一個IP就是客戶端IP,中間的為代理服務器IP ViewBag.xfor = Request.Headers["X-Forwarded-For"]; ViewBag.host = Request.Headers["Host"]; return View(); } } //Index View <div class="text-center"> <h1 style="background-color:aquamarine" class="display-4">歡迎來到 Site1 </h1> <h3> X-Forwarded-For: @ViewBag.xfor</h3><br /> <h3> Host: @ViewBag.host</h3> <br /> </div>
發布項目,將發布文件傳輸到Centos的/var/www/MySite1目錄下(也可以放在其他地方),到/var/www/MySit1目錄下,執行cli命令 dotnet MySite.dll ,如果可以成功啟動表示編碼沒問題,到這里准備工作就結束了。
1.2 Systemd托管dotnet web應用
在Windows上部署時,如果不使用IIS進行部署的話,我們一般把netcore應用部署成window服務,因為部署成服務可以實現開機自啟,也比較穩定。在Linux部署時也最好部署成服務(守護進程),這里采用Systemd將dotnet web應用部署成服務,本篇的底部有Systemd簡單介紹,如果不了解的園友可以看一下。首先 cd /usr/lib/systemd/system/ 到Unit配置文件目錄下,創建一個配置文件,名字為 kestrel-mysite1.service,內容如下:
[Unit] #簡單描述 Description=run MySite on Centos [Service] #工作目錄 WorkingDirectory=/var/www/MySite1 #開啟時執行的命令 ExecStart=/usr/bin/dotnet /var/www/MySite1/MySite.dll #只有出錯時重啟,Restart=always表示無論什么原因造成服務停止都會重啟 Restart=on-failure#服務崩潰時,十秒鍾重啟一次 RestartSec=10 #用戶 User=wyy [Install] #該服務所在的target #這里符號鏈接放在/usr/lib/systemd/system/multi-user.target.wants目錄下 WantedBy=multi-user.target
然后通過命令 systemctl daemon-reload 重新加載配置文件,使用 systemctl start kestrel-mysite1 啟動服務。通過命令啟動服務不一定成功,使用 systemctl status kestrel-mysite1 查看服務運行情況,效果如下:
到這里我們已經將netcore項目部署成一個服務了,dotnet web應用運行在Kestrel服務器上(我們安裝netcore環境時會自動安裝kestrel服務器),接下來使用Nginx實現反向代理和負載均衡。
1.3 Nginx實現反向代理
首先介紹一下反向代理的概念。有反向代理就有前向代理,前向代理作為客戶端的代理,將從互聯網上獲取的資源返回給一個或多個的客戶端,服務端(如Web服務器)只知道代理的IP地址而不知道客戶端的IP地址;而反向代理是作為服務器端(如Web服務器)的代理使用,而不是客戶端,客戶端知道代理服務器IP地址而不知道具體后台服務器的IP地址。簡單的講,我們在公司通過代理服務器訪問外網,這里代理服務器屬於前向代理服務器,而客戶通過代理服務器從外網訪問我們公司的服務器,這里的代理服務器就是反向代理服務器。Nginx實現反向代理很簡單,使用一個proxy_pass指令將請求轉發給指定的后台服務器即可。
通過 vim /usr/local/nginx/conf/nginx.conf 編輯nginx配置,修改nginx配置如下:
worker_processes 2; events { worker_connections 1024; } http { include mime.types; sendfile on; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; server{ listen 80 ; server_name default_server; location /{ root html; index index.html; } } #加載vhosts目錄下所有配置 include vhosts/* ; }
通過 vim /usr/local/nginx/conf/vhosts/www.mysite.com 編輯www.mysite.com配置文件如下, proxy_pass http://192.168.70.99:1111 表示將請求交給192.168.70.99:1111處理,就是將請求交給上邊部署的kestrel-mysite1服務處理:
server { listen 80; server_name mysite.com *.mysite.com; access_log logs/mysite_access.log main; location / { proxy_pass http://192.168.70.99:1111; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
然后使用命令 nginx -s reload 重新加載nginx配置 。為了演示方便我們在自己的電腦上添加一個本機DNS,到C:\Windows\System32\drivers\etc目錄下,在host文件下添加DNS: 192.168.70.99 www.mysite.com 。到這里反向代理就配置完成了,在瀏覽器上輸入www.mysite.com顯示如下:
反向代理的基本流程:我們在瀏覽器輸入www.mysite.com,首先通過DNS解析找到訪問的主機IP為192.168.70.99,請求發送后被IP為192.168.70.99虛擬機的80端口監聽到,location匹配后通過proxy指令將請求交給192.168.70.99:1111服務去處理,處理完成后將結果返回Nginx,再由Nginx返回給客戶端。192.168.70.99:1111部署的就是運行在kestrel上的netcore web應用。
1.4 Nginx實現負載均衡
考慮一個問題:如果我們的web應用並發量比較大,一台服務器不滿足需求,我們多部署了幾台服務器,那么怎么讓Nginx將請求轉發給多台服務器呢?簡單的說,將請求轉發給多台服務器可以叫做負載均衡,同一個location下不能寫多個proxy指令,upstream指令可以實現將請求轉發給多個不同的服務器。實現負載均衡的步驟也十分簡單。
我們修改示例項目的hosting.json配置文件,內容為 { "server.urls": "http://0.0.0.0:2222"} ,即部署端口設置為2222,將發布文件放在/var/www/MySite2目錄下(為了便於區分,部署在2222端口的界面顯示【歡迎來到Site2】);然后通過Systemd添加一個kestrel-mysite2服務,添加過程和kestrel-mysite1服務一樣,配置文件如下:
[Unit] #簡單描述 Description=run MySite on Centos [Service] #工作目錄 WorkingDirectory=/var/www/MySite2 #開啟時執行的命令 ExecStart=/usr/bin/dotnet /var/www/MySite2/MySite.dll #出錯造成服務停止時重啟,Restart=always表示無論什么原因造成服務未運行都重啟 Restart=on-failure #服務崩潰時,十秒鍾重啟一次 RestartSec=10 #用戶 User=wyy [Install] #該服務所在的target #這里符號鏈接放在/usr/lib/systemd/system/multi-user.target.wants目錄下 WantedBy=multi-user.target
接下來修改/usr/local/nginx/vhosts/www.mysite.com配置文件,修改如下:
upstream mysite_hosts{ server 192.168.70.99:1111 weight=1 max_fails=2 fail_timeout=30s; server 192.168.70.99:2222 weight=3 max_fails=2 fail_timeout=30s; } server { listen 80; server_name mysite.com *.mysite.com; access_log logs/mysite_access.log main; location / { proxy_pass http://mysite_hosts; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
upstrem節點下 server 192.168.70.99:1111 weight=1 max_fails=2 fail_timeout=30s 中weight表示權重,權重越大轉發的幾率就越高;max_fails=2,最多失敗兩次,失敗兩次后就不再向這個服務器轉發請求;fail_timeout=30s表示超時時間為30s。這里為了方便,部署在同一虛擬機的兩個端口上,也可以部署在不同的設備上,
部署在不同的設備上時,修改一下IP即可。
最后我們打開瀏覽器,輸入www.mysite.com,不停的刷新,會出現兩種界面如下,且出現第二種界面的概率更大,如果我們故意關掉其中的一個服務器,另一台服務器並不會受到影響,網站也可以正常運行:
到這里我們已經通過Nginx實現了負載均衡,負載均衡還有其他的形式,在下一篇會介紹。
補充2 Systemd
2.1 Systemd簡單認識
Systemd即為system daemon,是linux的一種init軟件,用來啟動和管理守護進程(類似於windows下的NSSM),它替代initd成為系統的第一個進程(PID=1)。管理后台服務的軟件還有python開發的supervisor等。
Systemd管理系統資源時,所有的資源統稱為Unit(單元)。每一個Unit都有一個配置文件,用於告訴Systemd怎么啟動這個Unit。開機時Systemd 從目錄/etc/systemd/system/
讀取配置文件,該目錄里面存放的大部分文件都是符號鏈接,指向目錄/usr/lib/systemd/system/
,真正的配置文件存放在/usr/lib/systemd/system/目錄下。
看一下Systemd管理防火牆的命令: systemctl enable firewalld 這條命令的作用是讓防火牆開機自啟,執行這條命令時會在/etc/systemd/system/添加一個符號鏈接,指向/usr/lib/systemd/system下的firewalld.service。開機時只會執行/etc/systemd/system/目錄下的配置文件,通過防火牆配置文件的符號鏈接,執行/usr/lib/systemd/system下的firewalld.service。相對的 systemctl disable firewalld 用於讓防火牆開機不自啟,實質上就是撤銷/etc/systemd/system/目錄下的符號鏈接。
我們可以通過 systemctl list-unit-files 查看配置文件,如下所示:
綠色的enabled表示開機啟動,紅色的disabled表示開機不啟動,static表示不能執行,只能作為其他配置文件的依賴,masked表示禁止執行。
2.2 Unit配置文件
我們知道Unit配置文件存放在 /usr/lib/systemd/system/目錄中,所以當添加后台進程來托管netcore應用時,首先要在該目錄下添加一個配置文件。
看一下上邊的Unit配置文件:
[Unit] #簡單描述 Description=run MySite on Centos [Service] #工作目錄 WorkingDirectory=/var/www/MySite #開啟時執行的命令 ExecStart=/usr/bin/dotnet /var/www/MySite1/MySite1.dll #出錯造成服務停止時重啟 Restart=on-failure#服務崩潰時,十秒鍾重啟一次 RestartSec=10 #用戶 User=wyy [Install] #該服務所在的target #這里符號鏈接放在/usr/lib/systemd/system/multi-user.target.wants目錄下 WantedBy=multi-user.target
[Unit]
區塊通常是配置文件的第一個區塊,用來定義 Unit 的元數據,以及配置與其他 Unit 的關系。它的主要字段如下:
Description:簡短描述
Documentation:文檔地址
Requires:當前 Unit 依賴的其他 Unit,如果它們沒有運行,當前 Unit 會啟動失敗
Wants:與當前 Unit 配合的其他 Unit,如果它們沒有運行,當前 Unit 不會啟動失敗
BindsTo:與Requires類似,它指定的 Unit 如果退出,會導致當前 Unit 停止運行
Before:如果該字段指定的 Unit 也要啟動,那么必須在當前 Unit 之后啟動
After:如果該字段指定的 Unit 也要啟動,那么必須在當前 Unit 之前啟動
Conflicts:這里指定的 Unit 不能與當前 Unit 同時運行
Condition...:當前 Unit 運行必須滿足的條件,否則不會運行
Assert...:當前 Unit 運行必須滿足的條件,否則會報啟動失敗
[Install]
通常是配置文件的最后一個區塊,用來定義如何啟動,以及是否開機啟動。它的主要字段如下:
WantedBy:它的值是一個或多個 Target,當前 Unit 激活時(enable)符號鏈接會放入/etc/systemd/system目錄下面以 Target 名 + .wants后綴構成的子目錄中 RequiredBy:它的值是一個或多個 Target,當前 Unit 激活時,符號鏈接會放入/etc/systemd/system目錄下面以 Target 名 + .required后綴構成的子目錄中 Alias:當前 Unit 可用於啟動的別名 Also:當前 Unit 激活(enable)時,會被同時激活的其他 Unit
補充:Target 就是一個 Unit 組,包含許多相關的 Unit 。啟動某個 Target 的時候,Systemd 就會啟動里面所有的 Unit。從這個意義上說,Target 這個概念類似於"狀態點",啟動某個 Target 就好比啟動到某種狀態。
[Service]
區塊用來 Service 的配置,只有 Service 類型的 Unit 才有這個區塊。它的主要字段如下。
Type:定義啟動時的進程行為。它有以下幾種值。 Type=simple:默認值,執行ExecStart指定的命令,啟動主進程 Type=forking:以 fork 方式從父進程創建子進程,創建后父進程會立即退出 Type=oneshot:一次性進程,Systemd 會等當前服務退出,再繼續往下執行 Type=dbus:當前服務通過D-Bus啟動 Type=notify:當前服務啟動完畢,會通知Systemd,再繼續往下執行 Type=idle:若有其他任務執行完畢,當前服務才會運行 ExecStart:啟動當前服務的命令 ExecStartPre:啟動當前服務之前執行的命令 ExecStartPost:啟動當前服務之后執行的命令 ExecReload:重啟當前服務時執行的命令 ExecStop:停止當前服務時執行的命令 ExecStopPost:停止當其服務之后執行的命令 RestartSec:自動重啟當前服務間隔的秒數 Restart:定義何種情況 Systemd 會自動重啟當前服務,可能的值包括always(總是重啟)、on-success、on-failure、on-abnormal、on-abort、on-watchdog TimeoutSec:定義 Systemd 停止當前服務之前等待的秒數 Environment:指定環境變量
注意:一旦修改配置文件,就要讓 Systemd重新加載配置文件,然后重新啟動,否則修改不會生效,命令如下:
sudo systemctl daemon-reload
sudo systemctl restart xxx.service
Systemctl是Systemd的主命令,我們經常用到的命令並不多,簡單總結如下:
# 立即啟動一個服務 $ sudo systemctl start firewalld.service # 立即停止一個服務 $ sudo systemctl stop firewalld.service # 重啟一個服務 $ sudo systemctl restart firewalld.service # 殺死一個服務的所有子進程 $ sudo systemctl kill firewalld.service # 重新加載一個服務的配置文件 $ sudo systemctl reload firewalld.service # 重載所有修改過的配置文件 $ sudo systemctl daemon-reload # 顯示某個 Unit 的所有底層參數 $ systemctl show firewalld.service
參考文章:
【1】http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html
【2】https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-2.2