自從docker的出現(xiàn),web架構(gòu)方式也出現(xiàn)了新的變化。這幾年一直在關(guān)注docker,但是從未實(shí)踐過。最近有點(diǎn)時(shí)間,決定實(shí)踐下,用以評(píng)估今后架構(gòu)轉(zhuǎn)型的可行性。
寫本文的目的是,分享自己的實(shí)踐,希望能讓初學(xué)者能少走點(diǎn)冤枉路。
這里就不再千篇一律的講什么docker的原理的,既然是初學(xué)者,那些說(shuō)實(shí)話沒什么用。初學(xué)者更需要的是通過實(shí)踐,窺到docker的外形。
為保持精煉,文章會(huì)隱藏一些細(xì)節(jié),如果不明白,請(qǐng)留言。
實(shí)踐過程中,發(fā)現(xiàn)docker這個(gè)大玩具,知識(shí)點(diǎn)還是非常多的。
所以在實(shí)踐前做了一些知識(shí)儲(chǔ)備:
Docker — 從入門到實(shí)踐:https://yeasy.gitbooks.io/docker_practice/content/introduction/what.html
這是一本好書,讓我縮短了知識(shí)儲(chǔ)備的時(shí)間,非常建議初學(xué)者還是先仔細(xì)研讀。
不過細(xì)節(jié)部分講的不是特別細(xì),很多知識(shí)還是得各種查資料。Docker文檔:https://docs.docker.com/
查資料用的consul文檔:https://www.consul.io/docs/guides/index.html
如果你選擇consul,這個(gè)是必讀的,這家伙比傳說(shuō)中的復(fù)雜多了。consul的docker鏡像:https://hub.docker.com/r/progrium/consul/
包裝consul的鏡像,簡(jiǎn)化了consul的部署,這也是docker的魅力。《Nodejs微服務(wù)架構(gòu)》
比較務(wù)實(shí)的一本書,從書里找到了一些靈感。比如Seneca可能是下一步會(huì)做的技術(shù)選型,去集中化的方式避免了服務(wù)發(fā)現(xiàn)的繁瑣。
架構(gòu)
下面廢話不多說(shuō),架構(gòu)大概是這樣的。很簡(jiǎn)單,沒有那種高來(lái)高去的東西,新手需要的是情境無(wú)關(guān)。

對(duì)于架構(gòu)的考慮:
- 首先架構(gòu)應(yīng)該是,簡(jiǎn)單的,可迭代演進(jìn)的。這樣可操作性和可維護(hù)性會(huì)更強(qiáng)。
- 談架構(gòu),無(wú)外乎就是高可用和可擴(kuò)展,脫離這兩個(gè)都是耍流氓。還有就是省錢,動(dòng)不動(dòng)20臺(tái)服務(wù)器,創(chuàng)業(yè)公司傷不起。所以,解決好了就是好架構(gòu)。
- 監(jiān)控方案是后續(xù)迭代演進(jìn)的事,你必須要保證你的系統(tǒng)正常運(yùn)轉(zhuǎn),才能縮短開發(fā)周期。留出更多的時(shí)間,你可以做這些重要的事。
- 關(guān)于負(fù)載均衡器,有很多備選方案,現(xiàn)在云服務(wù)這么發(fā)達(dá),可選的方案也很多,甚至有跨機(jī)房的負(fù)載均衡。比自己搭nginx+keepalived要方便的多。
- 選擇consul,用于服務(wù)發(fā)現(xiàn),解決的是服務(wù)互訪的問題。
- 沒有集群方案,這里沒有考慮使用集群方案,比如swarm之類的,在實(shí)踐過程中,發(fā)現(xiàn)配置過于繁瑣,更不用說(shuō)zookeeper了。
- 為什么沒有使用consul-template,雖然很巧妙,但是這兩個(gè)服務(wù)集成的耦合度過高。nginx模板配置繁瑣,這會(huì)極大增加運(yùn)維成本。不太像是一個(gè)適用與生產(chǎn)環(huán)境的成熟方案。
架構(gòu)原理
第一步,所有應(yīng)用啟動(dòng)之后會(huì)向consu集群注冊(cè)自己,注冊(cè)的信息包括
- 所屬數(shù)據(jù)中心 DC1
- 所屬數(shù)據(jù)中心的宿主機(jī)節(jié)點(diǎn)
- 所屬節(jié)點(diǎn)的服務(wù),服務(wù)訪問方式ip,端口
如何注冊(cè)?很多方式,比較簡(jiǎn)單的方式是,應(yīng)用在啟動(dòng)的時(shí)候往consul 注冊(cè)Api發(fā)送注冊(cè)服務(wù)信息。
可以使用shell或者應(yīng)用程序來(lái)發(fā)送。比如nodejs 可以引入node-consul庫(kù)來(lái)發(fā)送注冊(cè)信息。
后期consul會(huì)負(fù)責(zé)服務(wù)節(jié)點(diǎn)的健康檢查。
第二步,當(dāng)應(yīng)用間存在訪問時(shí),如Api網(wǎng)關(guān)訪問微服務(wù),web應(yīng)用訪問微服務(wù),微服務(wù)之間互訪。這里可以使用consul Api定期請(qǐng)求服務(wù)狀態(tài)的方式,來(lái)獲取可用的節(jié)點(diǎn),后面會(huì)詳細(xì)介紹。請(qǐng)求到節(jié)點(diǎn)后還可以在應(yīng)用程序級(jí)別做一些負(fù)載均衡策略。沒有使用dns的原因的,dns使用起來(lái)也不是很方便,配置起來(lái)也很繁瑣。
基礎(chǔ)工作
安裝docker
首先是安裝docker,mac下安裝很簡(jiǎn)單,其他環(huán)境除了安裝過程不一樣,后續(xù)基本一樣。
https://docs.docker.com/docker-for-mac/
安裝之后有GUI界面可以用,可以讓新手快速使用起來(lái)。

安裝后的配置
如果你還沒有注冊(cè) docker hub.,按提示注冊(cè)即可。
登陸完后,進(jìn)入Preferences...
添加阿里云docker hub鏡像:https://45599kaw.mirror.aliyuncs.com
也可以自己注冊(cè)一個(gè)阿里云,開通容器云服務(wù),鏡像是免費(fèi)送的。

打開終端,實(shí)驗(yàn)下是否成功
$ docker search nginx
出現(xiàn)登陸提示:
登陸請(qǐng)注意,登錄名使用注冊(cè)時(shí)的用戶名,千萬(wàn)別用郵箱。
安裝虛擬機(jī)
安裝VirtualBox
作為實(shí)驗(yàn)性項(xiàng)目,使用VirutalBox可以快速構(gòu)建你想要的物理環(huán)境,而且docker和virtualbox搭配的很好,使用docker-machine可以非常簡(jiǎn)單的管理所有虛擬機(jī)。
開始
好了,萬(wàn)事具備。現(xiàn)在我們開始創(chuàng)建虛擬機(jī)。
使用docker工具包自帶的 docker-machine工具,可以幫你快速創(chuàng)建一個(gè)docker宿主機(jī)。
在這個(gè)架構(gòu)中,我們一共只需要?jiǎng)?chuàng)建3臺(tái)宿主機(jī)
docker-machine命令后面會(huì)用的比較頻繁,所以我們改個(gè)短點(diǎn)的名字。
這里我用zsh,bash類似。
$ vi ~/.zshrc
#增加
alias dm="docker-machine”
依次創(chuàng)建3臺(tái)虛擬機(jī)
$ dm create -d "virtualbox” node1
$ dm create -d "virtualbox” node2
$ dm create -d "virtualbox" node3
ip是自動(dòng)分配的,不出意外的話,會(huì)得到下面對(duì)應(yīng)的ip(如果真出意外了,就改改ip吧)。
宿主機(jī) node1: 192.169.99.100
宿主機(jī) node2: 192.169.99.101
宿主機(jī) node3: 192.169.99.102
$ dm ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
node1 - virtualbox Running tcp://192.168.99.100:2376 v17.06.0-ce
node2 - virtualbox Running tcp://192.168.99.101:2376 v17.06.0-ce
node3 - virtualbox Running tcp://192.168.99.102:2376 v17.06.0-ce
第一臺(tái)宿主機(jī)配置
宿主機(jī)node1
我們新開一個(gè)終端
$ dm ssh node1
這個(gè)命令可以快速登入node1宿主機(jī)
$ sudo vi /etc/docker/daemon.json
{
"experimental" : true,
"registry-mirrors" : [
"https://45599kaw.mirror.aliyuncs.com"
]
}
雖然可以架設(shè)regsiter私服,但是使用起來(lái)的麻煩程度,遠(yuǎn)遠(yuǎn)超過重復(fù)下載帶來(lái)的代價(jià)。所以不用糾結(jié)了,就這么整,非常簡(jiǎn)單。
改完之后,重啟docker
$ sudo /etc/init.d/docker restart
執(zhí)行命令后有提示錯(cuò)誤,不用理會(huì)。
配置consul-server
啟動(dòng)第一臺(tái) consul-server,非常簡(jiǎn)單,一條命令搞定,這就是docker的魅力。
$ docker run -h node1 --name consul -d -v /data:/data --restart=always\
-p 8300:8300 \
-p 8301:8301 \
-p 8301:8301/udp \
-p 8302:8302 \
-p 8302:8302/udp \
-p 8400:8400 \
-p 8500:8500 \
progrium/consul -server \
-bootstrap-expect 3 \
-advertise 192.168.99.100
下面來(lái)解釋下各個(gè)參數(shù)
-h 節(jié)點(diǎn)名字
--name 容器(container)名稱,后期用來(lái)方便啟動(dòng)關(guān)閉,看日志等,這個(gè)一定要寫
-d 后臺(tái)運(yùn)行
-v /data:/data 使用宿主機(jī)的/data目錄映射到容器內(nèi)部的/data,用于保存consul的注冊(cè)信息,要不docker 一重啟,數(shù)據(jù)是不保留的。
--restart=always 這個(gè)可以活得長(zhǎng)一點(diǎn)
下面幾個(gè)參數(shù)都是consul集群用的,非集群模式可以不使用。
-p 8300:8300
-p 8301:8301
-p 8301:8301/udp
-p 8302:8302
-p 8302:8302/udp \
progrium/consul 鏡像名稱,本地沒有就自動(dòng)從公共docker庫(kù)下載
后面的都是consul的參數(shù):
-server \ 以服務(wù)節(jié)點(diǎn)啟動(dòng)
-bootstrap-expect 3 \ 預(yù)期的啟動(dòng)節(jié)點(diǎn)數(shù)3,最少是3,要不達(dá)不到cluster的效果
-advertise 192.168.99.100 告訴集群,我的ip是什么,就是注冊(cè)集群用的
執(zhí)行完畢后 ,使用docker ps看下,是否運(yùn)行正常。docker logs就不用看了,里面各種警告和錯(cuò)誤,其實(shí)那都是假象。
但是consul cluster你必須明白,只有3個(gè)consul-server節(jié)點(diǎn)都啟動(dòng)正常了,整個(gè)集群才能正常啟動(dòng)。
打開 http://192.168.99.100:8500/
但是你看不到下面的consul標(biāo)簽,因?yàn)榧哼€沒有都起來(lái)。

配置下一臺(tái)consul-server
開啟一個(gè)新的終端
$ dm ssh node2 #進(jìn)入node2宿主機(jī)
增加daemon.json,重啟docker,不再贅述。
$ docker run -h node2 --name consul -d -v /data:/data --restart=always\
-p 8300:8300 \
-p 8301:8301/udp \
-p 8302:8302 \
-p 8302:8302/udp \
-p 8400:8400 \
-p 8500:8500 \
progrium/consul -server \
-advertise 192.168.99.101 \
-join 192.168.99.100
這里多了一個(gè)參數(shù)
-join 192.168.99.100 代表的是加入node1建立好的consul-server
好,已經(jīng)加入了,但是集群還是沒有完備。
用同樣的方法配置 最后一臺(tái)
新開終端進(jìn)入node3
$ docker run -h node3 --name consul -d -v /data:/data --restart=always\
-p 8300:8300 \
-p 8301:8301/udp \
-p 8302:8302 \
-p 8302:8302/udp \
-p 8400:8400 \
-p 8500:8500 \
progrium/consul -server \
-advertise 192.168.99.102 \
-join 192.168.99.100
consul配置完畢
檢查是否成功
沒出意外的話,就看到下面的界面
http://192.168.99.100:8500/

但是我有預(yù)感,意外的可能性比較大。如果不成功,可以留言,這里面細(xì)節(jié)比較多。。。
開始啟動(dòng)應(yīng)用
這里拿最簡(jiǎn)單的nginx服務(wù)作為演示,情境無(wú)關(guān)。
啟動(dòng)nginx
進(jìn)入3個(gè)節(jié)點(diǎn)
執(zhí)行
$ docker run -d -p 80:80 --name nginx nginx
在node1,node2,node3中, 分別執(zhí)行以下命令
$ curl -X PUT -d '{"id": "nginx","name": "nginx","address": "192.168.99.100","port": 80,"checks": [{"http": "http://192.168.99.100/","interval": "5s"}]}' http://127.0.0.1:8500/v1/agent/service/register
$ curl -X PUT -d '{"id": "nginx","name": "nginx","address": "192.168.99.101","port": 80,"checks": [{"http": "http://192.168.99.101/","interval": "5s"}]}' http://127.0.0.1:8500/v1/agent/service/register
$ curl -X PUT -d '{"id": "nginx","name": "nginx","address": "192.168.99.102","port": 80,"checks": [{"http": "http://192.168.99.102/","interval": "5s"}]}' http://127.0.0.1:8500/v1/agent/service/register
好了,我們啟動(dòng)了3個(gè)nginx,并將它們都注冊(cè)到了consul.

健康檢查
獲取狀態(tài)
http 命令工具 httpie,可以自行安裝,替代curl,可以高亮格式化返回的json
$ http http://192.168.99.100:8500/v1/health/checks/nginx
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 30 Jul 2017 11:41:59 GMT
Transfer-Encoding: chunked
X-Consul-Index: 20
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
[
{
"CheckID": "service:nginx",
"Name": "Service 'nginx' check",
"Node": "node1",
"Notes": "",
"Output": "HTTP GET http://192.168.99.100/: 200 OK Output: <!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
"ServiceID": "nginx",
"ServiceName": "nginx",
"Status": "passing"
},
{
"CheckID": "service:nginx",
"Name": "Service 'nginx' check",
"Node": "node2",
"Notes": "",
"Output": "HTTP GET http://192.168.99.101/: 200 OK Output: <!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
"ServiceID": "nginx",
"ServiceName": "nginx",
"Status": "passing"
},
{
"CheckID": "service:nginx",
"Name": "Service 'nginx' check",
"Node": "node3",
"Notes": "",
"Output": "HTTP GET http://192.168.99.102/: 200 OK Output: <!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
"ServiceID": "nginx",
"ServiceName": "nginx",
"Status": "passing"
}
]
可以看到3個(gè)nginx的服務(wù)節(jié)點(diǎn)都是passing狀態(tài),這時(shí)候你可以選擇一個(gè)使用了。
制造一些事故
進(jìn)入node3
$ docker kill nginx

進(jìn)入node2
$ docker kill consul

再次檢查
$ http http://192.168.99.100:8500/v1/health/checks/nginx
[
{
"CheckID": "service:nginx",
"Name": "Service 'nginx' check",
"Node": "node1",
"Notes": "",
"Output": "HTTP GET http://192.168.99.100/: 200 OK Output: <!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
"ServiceID": "nginx",
"ServiceName": "nginx",
"Status": "passing"
},
{
"CheckID": "service:nginx",
"Name": "Service 'nginx' check",
"Node": "node2",
"Notes": "",
"Output": "HTTP GET http://192.168.99.101/: 200 OK Output: <!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
"ServiceID": "nginx",
"ServiceName": "nginx",
"Status": "passing"
},
{
"CheckID": "service:nginx",
"Name": "Service 'nginx' check",
"Node": "node3",
"Notes": "",
"Output": "Get http://192.168.99.102/: dial tcp 192.168.99.102:80: connection refused",
"ServiceID": "nginx",
"ServiceName": "nginx",
"Status": "critical"
}
]
可以看到,當(dāng)consul服務(wù)掛掉一個(gè)的時(shí)候,并不影響nginx服務(wù)的健康狀況。其中有一個(gè)nginx已經(jīng)處于critical狀態(tài)。這樣我們就有足夠的信息不選擇不健康的節(jié)點(diǎn)。
總結(jié)
好了,終于寫完了??偟膩?lái)說(shuō),這個(gè)已經(jīng)算是極簡(jiǎn)的架構(gòu)了。當(dāng)然,docker的生命周期遠(yuǎn)不止這些,比如ci,發(fā)布上線,灰度發(fā)布等。docker遠(yuǎn)沒有傳說(shuō)中那么簡(jiǎn)單,美妙。docker有很多的好處,但是需要DevOPS做很多的工作。
在實(shí)踐過程中,我發(fā)現(xiàn)有一個(gè)或許是更優(yōu)的架構(gòu)。那就是seneca的方案,使用事件相應(yīng)作為微服務(wù)的提供方式,這樣就避免了服務(wù)發(fā)現(xiàn)這件事,完全不需要注冊(cè)服務(wù),選擇服務(wù)這么麻煩。也不用為服務(wù)發(fā)現(xiàn)服務(wù)搭建一個(gè)集群確保其高可用。