上篇中,我們主要介紹了使用docker-compose對Windows Docker單服務器進行遠程管理,編譯和部署鏡像,並且設置容器的自動啟動。但是,還有一些重要的問題沒有解決,這些問題不解決,就完全談不上運維:
問題一:如此部署的應用,在宿主機外部,只能通過宿主機的ip加一個個特定的端口來訪問每個容器內的應用,這顯然是不滿足實際需求的。
問題二:相比於將應用直接部署在有UI界面的Windows Server,因為每個應用部署於自己的Windows Docker容器,當應用運行時發生各種問題時,比如,cpu高,內存高,訪問變慢等等,如何才能方便地排查問題呢?即使你願意一個個容器attach上去,也因為它沒有UI,遠沒有傳統有UI界面的Windows Server上容易。所以,我們必須有必要的工具來更方便的監控容器的運行。
下篇
- 負載均衡和反向代理
- 日志解析和監控
負載均衡和反向代理
要解決問題一:
- 首先,我們需要一個機制,當用戶訪問我們的ip或域名時,服務器能根據不同的域名,或者不同的子路徑,在相同的端口比如80或者443,從每個應用容器返回內容——這就是反向代理;
- 接着,我們不希望我們的應用在更新、系統維護、或者局部故障時無法提供服務,所以,每個部署的應用不能是單點,那么,如果同一個應用部署了多個容器實例,如何才能讓他們Serve相同的域名和URL請求呢?——這就是負載均衡;
通常,支持反向代理的組件,往往也同時提供負載均衡功能。例如:F5、nginx、Apache2、HAProxy甚至IIS的ARR等。不同的方案可能側重點略有不同,我們可以根據實際情況選擇不同的方案。另外,既然我們的應用部署在Windows Docker服務器,那么最好我們所用的代理組件同樣能部署在Windows Docker容器,這樣我們就能用一致的流程和工具來管理。下面的示例中,我們選擇Apache2,實現一個部署於Windows Docker部署的反向代理加負載均衡器。
為了演示負載均衡,我們新建一個two-instances-demo目錄,其中docker-compose.yml里為iis-demo添加兩個不同內部ip的容器實例,再添加一個apache容器,它的Dockerfile定義在apache目錄中。
version: "2.1"
services:
apache:
build: .
image: "apache-proxy:1.0"
ports:
- "80:80"
networks:
nat:
iis-demo-1:
build: ../
image: "iis-demo:1.0"
ports:
- "80"
networks:
nat:
ipv4_address: 172.24.128.101
volumes:
- "c:/temp:c:/inetpub/logs/LogFiles"
environment:
- "env1=LIVE1"
- "env2=LIVE2"
- "HOSTS=1.2.3.4:TEST.COM"
iis-demo-2:
build: ../
image: "iis-demo:1.0"
ports:
- "80"
networks:
nat:
ipv4_address: 172.24.128.102
volumes:
- "c:/temp:c:/inetpub/logs/LogFiles"
environment:
- "env1=LIVE1"
- "env2=LIVE2"
- "HOSTS=1.2.3.4:TEST.COM"
networks:
nat:
external: true
Apache的Dockerfile,很簡單,只是安裝和覆蓋默認conf,然后,運行https.exe服務。
FROM microsoft/windowsservercore:latest
ADD vc_redist.x64.exe /vc_redist.x64.exe
RUN /vc_redist.x64.exe /q
ADD httpd-2.4.25-x64-vc14-r1.zip /
RUN powershell -executionpolicy bypass -Command "expand-archive -Path 'c:\httpd-2.4.25-x64-vc14-r1.zip' -DestinationPath 'c:\'"
COPY conf /conf
RUN del c:\Apache24\conf\httpd.conf
RUN del c:\Apache24\conf\extra\httpd-vhosts.conf
RUN copy "c:\conf\httpd.conf" "c:\Apache24\conf\"
RUN copy "c:\conf\extra\httpd-vhosts.conf" "c:\Apache24\conf\extra\"
ENTRYPOINT ["C:\\Apache24\\bin\\httpd.exe"]
然后,我們打開一個命令窗口,在two-instances-demo目錄下執行docker-compose up,稍作等待,等容器運行起來,然后,在宿主機外部,注意一定是從宿主機外部(原因上一篇文章有解釋),訪問宿主機的ip地址下的/iis-demo/路徑,可以看到,iis-demo的默認頁面內容能夠被正常顯示,說明反向代理和負載均衡已經正常運行了。
其他備注:
- 這里的Apache配置是出於演示目的,拋磚引玉,極度簡化的版本,如果用於真實環境,請根據Apache官方文檔以和相應的性能測試,做必要調整;
- 除了Apache之外,nginx可以運行於Windows,但是性能不佳,官方不推薦在Windows下使用;
- HAProxy只能運行於Linux;
- IIS的ARR,反向代理功能尚可,但是負載均衡依賴於IIS的集群,限制頗多,如果同一個應用的容器只需要部署單個實例的話可以考慮;
- 這里在docker-compose.yml中定義同一個應用的兩個服務的做法,只是在不使用docker的集群化部署時的簡化做法,如果應用了Swarm集群,或者使用了其他支持集群和自動擴展的容器編排方案,是可以直接通過docker-compose的scale參數,讓一個docker-compose服務運行多個實例的,這個我們以后會聊到;
日志解析和監控
要解決問題二:
- 首先,我們需要一個方便的機制查看宿主機上每個容器運行時的CPU,內存,IO等資源開銷,還要能保留這些運行情況的歷史紀錄,便於回溯和問題的排查;
- 其次,我們需要一個方便的機制查看宿主機上每個容器內的系統和應用產生的日志,例如:操作系統日志、IIS訪問日志、應用的異常日志等;
對於容器的運行時的性能指標,docker的命令行工具,提供了docker stats命令,可以查看每個容器實時的CPU、內存、IO能指標,我們可以考慮定時將它們收集保存起來,用於集中化的監控。
另外,玩過Linux下docker的小伙伴們肯定知道,docker會將每個容器內運行時打印到console的內容,都記錄在宿主機的docker日志目錄中,而大多數Linux容器部署應用,大多會將應用自己的日志也打印到console,這樣,所有的日志都可以包含在docker宿主機的容器日志中。這有什么好處呢?好處就是,我們可以在宿主機上,配置日志解析工具,比如Logstash或fluentd,解析和forward所有日志。
在Windows Docker下,由於Windows的基因問題,一方面,大多數應用都是基於IIS的應用,沒辦法將日志直接打印到console,另一方面,IIS本身的日志和Windows的EventLog也無法方便地配置把它們打印到console,所以,一般的做法是,需要把這些日志所在的目錄,mount到宿主機,然后,再在宿主機上統一解析。特別對於Windows EventLog,它在Windows文件系統的格式無法被簡單讀取和解析,因此,我們一般需要用到一些地第三方工具,如nxlog和Elastic公司Beats(這兩個工具都是免費開源的)將解析后的Windows EventLog保存為易於解析的格式,比如JSON格式。
對於經過解析的日志,現在比較流行的做法是把它導入Elasticsearch,這樣就可以方便通過kibana,grafana這樣的工具,可視化查看,遠程實時監控了。
本想將相關組件都做成Windows Docker鏡像,方便大家能直接下載運行的,無奈這些組件都比較大,動輒幾十上百兆,國內的網絡下,不翻牆的情況下,我本機下載都很費勁,想必,做成Docker鏡像,大家運行的體驗也不會很好,所以,就先不做了。下面簡單介紹一下這幾個工具的使用,並分享一些核心的配置腳本,給大家做個參考。
首先是對IIS Log和Windows EventLog的解析,以nxlog為例:
nxlog的Windows版安裝完之后,是一個Windows Service。它的配置文件在C:\Program Files (x86)\nxlog\conf目錄下,每次更改nxlog.conf文件,都需要重啟nxlog service使配置生效。最經常的用法,一般是將原始的IIS的W3C格式的日志,還有Windows EventLog解析為JSON格式,然后經過Logstash中轉之后保存到Elasticsearch。
下面是一個典型的解析IIS W3C格式Log的nxlog.conf文件:
define ROOT C:\Program Files (x86)\nxlog
Moduledir %ROOT%\modules
CacheDir %ROOT%\data
Pidfile %ROOT%\data\nxlog.pid
SpoolDir %ROOT%\data
LogFile %ROOT%\data\nxlog.log
<Extension json>
Module xm_json
</Extension>
<Extension w3c>
Module xm_csv
Fields $log_date, $log_time, $log_site_id, $log_server_name, $log_server_ip, $log_http_method, $log_path, $log_query, $log_port, $log_user_name, $log_client_ip, $log_http_version, $log_user_agent, $log_referer, $log_domain_name, $log_http_status, $log_http_substatus, $log_win32_status, $log_response_size, $log_request_size, $log_time_taken
FieldTypes string, string, string, string, string, string, string, string, integer, string, string, string, string, string, string, integer, integer, integer, integer, integer, integer
Delimiter ' '
EscapeControl FALSE
UndefValue -
</Extension>
<Input in-iis>
Module im_file
File "C:\\temp\\iislogs\\\\u_ex*.log"
SavePos True
ReadFromLast False
ActiveFiles 10
Exec if $raw_event =~ /^#/ drop(); else { w3c->parse_csv(); if ($log_date) $log_request_timestamp = $log_date + " " + $log_time; else drop(); if ($log_referer) $log_referer = lc($log_referer); if ($log_path) { $log_path = lc($log_path); if $log_path =~ /(\.[^.]+)/ $log_request_type = $1; else $log_request_type = "unknown"; } if ($log_user_agent) $log_user_agent = replace($log_user_agent, "+", " "); if ($log_domain_name) $log_domain_name = replace($log_domain_name, ":80", ""); };
</Input>
<Output iis>
Module om_tcp
Exec $raw_event = to_json();
Host localhost
Port 5151
</Output>
<Route out_iis>
Path in-iis => iis
</Route>
它的第一部分Extension W3C定義了哪些W3C字段需要解析;第二部分iis input調用w3c擴展組件,解析指定目錄的日志文件,做必要的規整;第三部分定義了如何保存解析結果,將解析后的消息,保存為JSON格式的鍵值對,然后寫入一個Logstash的TCP輸入端口。
下面是一個典型的nxlog解析Windows EventLog的例子:
define ROOT C:\Program Files (x86)\nxlog
Moduledir %ROOT%\modules
CacheDir %ROOT%\data
Pidfile %ROOT%\data\nxlog.pid
SpoolDir %ROOT%\data
LogFile %ROOT%\data\nxlog.log
<Extension _syslog>
Module xm_syslog
</Extension>
<Extension _json>
Module xm_json
</Extension>
<Input eventlog>
Module im_msvistalog
ReadFromLast TRUE
SavePos FALSE
Query <QueryList>\
<Query Id="0">\
<Select Path="System">*</Select>\
<Select Path="Application">*</Select>\
</Query>\
</QueryList>
</Input>
<Output out>
Module om_file
File "C:\temp\EventLog\" + $Hostname + ".json"
<Exec>
if out->file_size() > 20M
{
$newfile = "C:\temp\EventLog\" + $Hostname + "_" + strftime(now(), "%Y%m%d%H%M%S") + ".json";
out->rotate_to($newfile);
}
</Exec>
Exec to_json();
</Output>
<Route 1>
Path eventlog => out
</Route>
這里,第一部分我們定義了如何從Windows EventLog中篩選消息;第二部分,定義了如何以每20M為大小,分割保存最新的EventLog為JSON。
將JSON格式的數據通過Logstash保存到Elasticsearch非常簡單,網上示例比比皆是,這理解不舉例了。
最后分享一個Logstash的配置文件,用於每隔30秒,收集宿主機上所有docker容器的性能指標,並且以JSON格式,保存到Elasticsearch:
input {
exec {
command => "C:\temp\get-docker-stats.cmd"
interval => 30
codec => line {}
}
}
filter {
grok {
match => { "message" => "%{WORD:container_id} %{WORD:container_name} %{NUMBER:cpu_percent}% %{NUMBER:mem} %{WORD:mem_unit} %{NUMBER:net_in} %{WORD:net_in_unit} / %{NUMBER:net_out} %{WORD:net_out_unit} %{NUMBER:block_in} %{WORD:block_in_unit} / %{NUMBER:block_out} %{WORD:block_out_unit}" }
}
mutate {
convert => {
"cpu_percent" => "float"
"mem" => "float"
"net_in" => "float"
"net_out" => "float"
"block_in" => "float"
"block_out" => "float"
}
}
#calc memory bytes
if [mem_unit] == "KiB" { ruby { code => "event.set('mem_bytes',event.get('mem').to_f*1024)" } }
if [mem_unit] == "MiB" { ruby { code => "event.set('mem_bytes',event.get('mem').to_f*1024*1024)" } }
if [mem_unit] == "GiB" { ruby { code => "event.set('mem_bytes',event.get('mem').to_f*1024*1024*1024)" } }
#calc net_in bytes
if [net_in_unit] == "kB" { ruby { code => "event.set('net_in_bytes',event.get('net_in').to_f*1000)" } }
if [net_in_unit] == "MB" { ruby { code => "event.set('net_in_bytes',event.get('net_in').to_f*1000*1000)" } }
#calc net_out bytes
if [net_out_unit] == "kB" { ruby { code => "event.set('net_out_bytes',event.get('net_out').to_f*1000)" } }
if [net_out_unit] == "MB" { ruby { code => "event.set('net_out_bytes',event.get('net_out').to_f*1000*1000)" } }
#calc block_in bytes
if [block_in_unit] == "kB" { ruby { code => "event.set('block_in_bytes',event.get('block_in').to_f*1000)" } }
if [block_in_unit] == "MB" { ruby { code => "event.set('block_in_bytes',event.get('block_in').to_f*1000*1000)" } }
#calc block_out bytes
if [block_out_unit] == "kB" { ruby { code => "event.set('block_out_bytes',event.get('block_out').to_f*1000)" } }
if [block_out_unit] == "MB" { ruby { code => "event.set('block_out_bytes',event.get('block_out').to_f*1000*1000)" } }
mutate {
remove_field => ["mem", "mem_unit", "net_in", "net_in_unit", "net_out", "net_out_unit", "block_in", "block_in_unit", "block_out", "block_out_unit", "message", "command"]
}
}
output {
elasticsearch {
hosts => ["localhost"]
index => "logstash-docker-stats-log-%{+YYYY.MM.dd}"
timeout => 30
workers => 1
}
}
其中,get-docker-stats.cmd文件真正執行docker stats命令,獲取所有正在運行的容器的性能指標,其中具體的命令如下:
@echo off
docker stats --no-stream --format "{{.Container}} {{.Name}} {{.CPUPerc}} {{.MemUsage}} {{.NetIO}} {{.BlockIO}}"
所有JSON格式的監控數據保存到Elasticsearch以后,使用kibana或者grafana進行數據的展示、設置監控警報等等,就相對比較簡單了,目前也非常流行,網上應該是能找到非常多的示例的,使用上也不存在Linux和Windows的區別,這里就不詳述了。對這方面有疑問的同學,我們可以私下交流。
單節點Windows Docker服務器簡單運維下篇完。
我們簡單回顧一下,在最近的上下兩篇中,我們介紹了運維一個單節點Windows Docker服務器的主要思路和常用工具。這些思想和工具,也是更復雜的docker集群模式下的運維的基礎。運維的水很深,Windows Docker的運維,對大多數公司來說,也都還只是在摸索的過程中。文中的示例,更多的還在於拋磚引用,大家不要受其局限,要習慣於發揮想象力,創造性的解決問題,提出新的思路。
前面的示例中,雖然盡可能不依賴於Linux下的容器,但畢竟Linux下的容器和各種支持工具,現在已經非常成熟了,在實際的部署中,還是應該根據實際情況和Windows Docker結合使用,只要能解決問題的工具和方法就都是極好的!