本文將介紹如何使用Docker Compose搭建Istio。Istio號稱支持多種平臺(不只僅Kubernetes)。然而,官網上非基於Kubernetes的教程彷彿不是親兒子,寫得很是隨便,不只缺了一些內容,並且還有坑。本文但願能補實這些內容。我認爲在學習Istio的過程當中,相比於Kubernetes,使用Docker Compose部署更能深入地理解Istio各個組件的用處以及他們的交互關係。在理解了這些後,能夠在其餘環境,甚至直接在虛擬機上部署Istio。固然,生產環境建議使用Kubernetes等成熟的容器框架。html
本文使用官方的Bookinfo示例。經過搭建Istio控制平面,部署Bookinfo應用,最後配置路由規則,展現Istio基本的功能和架構原理。linux
本文涉及的名詞、用到的端口比較多。Don't panic.git
爲了防止不提供原網址的轉載,特在這裏加上原文連接:
http://www.javashuo.com/article/p-mdhnkery-dy.htmlgithub
kubectl
(Kubernetes的客戶端)。在微服務架構中,一般除了實現業務功能的微服務外,咱們還會部署一系列的基礎組件。這些基礎組件有些會入侵微服務的代碼。好比服務發現須要微服務啓動時註冊本身,鏈路跟蹤須要在HTTP請求的headers中插入數據,流量控制須要一整套控制流量的邏輯等。這些入侵的代碼須要在全部的微服務中保持一致。這致使了開發和管理上的一些難題。docker
爲了解決這個問題,咱們再次應用抽象和服務化的思想,將這些須要入侵的功能抽象出來,做爲一個獨立的服務。這個獨立的服務被稱爲sidecar,這種模式叫Sidecar模式。對每一個微服務節點,都須要額外部署一個sidecar來負責業務邏輯外的公共功能。全部的出站入站的網絡流量都會先通過sidecar進行各類處理或者轉發。這樣微服務的開發就不須要考慮業務邏輯外的問題。另外全部的sidecar都是同樣的,只須要部署的時候使用合適的編排工具便可方便地爲全部節點注入sidecar。json
Sidecar不會產生額外網絡成本。Sidecar會和微服務節點部署在同一臺主機上而且共用相同的虛擬網卡。因此sidecar和微服務節點的通訊實際上都只是經過內存拷貝實現的。bootstrap
圖片來自:Pattern: Service Meshapi
Sidecar只負責網絡通訊。還須要有個組件來統一管理全部sidecar的配置。在Service Mesh中,負責網絡通訊的部分叫數據平面(data plane),負責配置管理的部分叫控制平面(control plane)。數據平面和控制平面構成了Service Mesh的基本架構。網絡
圖片來自:Pattern: Service Mesh架構
Istio的數據平面主要由Envoy實現,控制平面則主要由Istio的Pilot組件實現。
若是你使用Linux操做系統,須要先配置DOCKER_GATEWAY
環境變量。非Linux系統不要配。
$ export DOCKER_GATEWAY=172.28.0.1:
到install/consul
目錄下,使用istio.yaml
文件啓動控制平面:
根據本身的網絡狀況(你懂得),能夠把
istio.yaml
中的鏡像gcr.io/google_containers/kube-apiserver-amd64:v1.7.3
換成mirrorgooglecontainers/kube-apiserver-amd64:v1.7.3
。
$ docker-compose -f istio.yaml up -d
用命令docker-compose -f istio.yaml ps
看一下是否是全部組件正常運行。你可能(大機率)會看到pilot的狀態是Exit 255
。使用命令docker-compose -f istio.yaml logs | grep pilot
查看日誌發現,pilot
啓動時訪問istio-apiserver
失敗。這是由於Docker Compose是同時啓動全部容器的,在pilot
啓動時,istio-apiserver
也是處於啓動狀態,因此訪問istio-apiserver
就失敗了。
等istio-apiserver
啓動完成後,從新運行啓動命令就能成功啓動pilot
了。你也能夠寫一個腳原本自動跑兩次命令:
docker-compose -f istio.yaml up -d # 有些依賴別人的第一次啓動會掛 sec=10 # 根據你的機器性能這個時間能夠修改 echo "Wait $sec seconds..." sleep $sec docker-compose -f istio.yaml up -d docker-compose -f istio.yaml ps
配置kubectl
,讓kubectl
使用咱們剛剛部署的istio-apiserver
做爲服務端。咱們後面會使用kubectl
來執行配置管理的操做。
$ kubectl config set-context istio --cluster=istio $ kubectl config set-cluster istio --server=http://localhost:8080 $ kubectl config use-context istio
部署完成後,使用地址localhost:8500
能夠訪問consul
,使用地址localhost:9411
能夠訪問zipkin
。
在下一步以前,咱們先來看一下控制平面都由哪些組件組成。下面是istio.yaml
文件的內容:
# GENERATED FILE. Use with Docker-Compose and consul # TO UPDATE, modify files in install/consul/templates and run install/updateVersion.sh version: '2' services: etcd: image: quay.io/coreos/etcd:latest networks: istiomesh: aliases: - etcd ports: - "4001:4001" - "2380:2380" - "2379:2379" environment: - SERVICE_IGNORE=1 command: ["/usr/local/bin/etcd", "-advertise-client-urls=http://0.0.0.0:2379", "-listen-client-urls=http://0.0.0.0:2379"] istio-apiserver: # 若是這個鏡像下載不了的話,能夠換成下面的地址: # image: mirrorgooglecontainers/kube-apiserver-amd64:v1.7.3 image: gcr.io/google_containers/kube-apiserver-amd64:v1.7.3 networks: istiomesh: ipv4_address: 172.28.0.13 aliases: - apiserver ports: - "8080:8080" privileged: true environment: - SERVICE_IGNORE=1 command: ["kube-apiserver", "--etcd-servers", "http://etcd:2379", "--service-cluster-ip-range", "10.99.0.0/16", "--insecure-port", "8080", "-v", "2", "--insecure-bind-address", "0.0.0.0"] consul: image: consul:1.3.0 networks: istiomesh: aliases: - consul ports: - "8500:8500" - "${DOCKER_GATEWAY}53:8600/udp" - "8400:8400" - "8502:8502" environment: - SERVICE_IGNORE=1 - DNS_RESOLVES=consul - DNS_PORT=8600 - CONSUL_DATA_DIR=/consul/data - CONSUL_CONFIG_DIR=/consul/config entrypoint: - "docker-entrypoint.sh" command: ["agent", "-bootstrap", "-server", "-ui", "-grpc-port", "8502" ] volumes: - ./consul_config:/consul/config registrator: image: gliderlabs/registrator:latest networks: istiomesh: volumes: - /var/run/docker.sock:/tmp/docker.sock command: ["-internal", "-retry-attempts=-1", "consul://consul:8500"] pilot: image: docker.io/istio/pilot:1.1.0 networks: istiomesh: aliases: - istio-pilot expose: - "15003" - "15005" - "15007" ports: - "8081:15007" command: ["discovery", "--httpAddr", ":15007", "--registries", "Consul", "--consulserverURL", "http://consul:8500", "--kubeconfig", "/etc/istio/config/kubeconfig", "--secureGrpcAddr", "", ] volumes: - ./kubeconfig:/etc/istio/config/kubeconfig zipkin: image: docker.io/openzipkin/zipkin:2.7 networks: istiomesh: aliases: - zipkin ports: - "9411:9411" networks: istiomesh: ipam: driver: default config: - subnet: 172.28.0.0/16 gateway: 172.28.0.1
控制平面部署了這幾個組件(使用istio.yaml
裏寫的名稱):
etcd
:分佈式key-value存儲。Istio的配置信息存在這裏。istio-apiserver
:其實是一個kube-apiserver
,提供了Kubernetes格式數據的讀寫接口。consul
:服務發現。registrator
:監聽Docker服務進程,自動將容器註冊到consul
。pilot
:從consul
和istio-apiserver
收集主機信息與配置數據,並下發到全部的sidecar。zipkin
:鏈路跟蹤組件。與其餘組件的關係相對獨立。這些組件間的關係以下圖:
控制平面主要實現瞭如下兩個功能:
etcd
和kube-apiserver
的組合能夠看做是一個對象存儲系統,它提供了讀寫接口和變動事件,而且能夠直接使用kubectl
做爲客戶端方便地進行操做。Istio直接使用這個組合做爲控制平面的持久化層,節省了重複開發的麻煩,另外也兼容了Kubernetes容器框架。pilot
容器中實際執行的是pilot-discovery
(發現服務)。它從consul
收集各個主機的域名和IP的對應關係,從istio-apiserver
獲取流量控制配置,而後按照Envoy的xDS API規範生成Envoy配置,下發到全部sidecar。接下來咱們開始部署微服務。這裏咱們使用Istio提供的例子,一個Bookinfo應用。
Bookinfo 應用分爲四個單獨的微服務:
productpage
:productpage
微服務會調用details
和reviews
兩個微服務,用來生成頁面。details
:這個微服務包含了書籍的信息。reviews
:這個微服務包含了書籍相關的評論。它還會調用ratings
微服務。ratings
:ratings
微服務中包含了由書籍評價組成的評級信息。reviews
微服務有3個版本:
ratings
服務。ratings
服務,並使用1到5個黑色星形圖標來顯示評分信息。ratings
服務,並使用1到5個紅色星形圖標來顯示評分信息。Bookinfo應用的架構以下圖所示:
圖片來自:Bookinfo應用
首先,咱們切換到這個示例的目錄samples/bookinfo/platform/consul
下。
使用bookinfo.yaml
文件啓動全部微服務:
$ docker-compose -f bookinfo.yaml up -d
這裏只啓動了微服務,還需使用bookinfo.sidecar.yaml
文件啓動全部sidecar:
$ docker-compose -f bookinfo.sidecars.yaml up -d
部署完畢。可是當咱們訪問時……
Bookinfo暴露到外面的端口是9081,使用地址localhost:9081/productpage
訪問productpage
頁面。
Emmm……出錯了:
原本應該顯示reviews
的部分報錯了,而details
仍是正常的。通過一番排查,咱們發現,在全部微服務的容器上,無論你訪問的是productpage
、details
、reviews
仍是ratings
,網絡請求都會跑到details
。
你的狀況不必定是
details
,也有可能全部流量都跑到另外的某個服務。這是隨機的。
# 到reviews查reviews,返回404 $ docker exec -it consul_ratings-v1_1 curl reviews.service.consul:9080/reviews/0 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"> <HTML> <HEAD><TITLE>Not Found</TITLE></HEAD> <BODY> <H1>Not Found</H1> `/reviews/0' not found. <HR> <ADDRESS> WEBrick/1.3.1 (Ruby/2.3.8/2018-10-18) at reviews.service.consul:9080 </ADDRESS> </BODY> </HTML> # 到reviews查details,反倒能查出數據,詭異的路由…… $ docker exec -it consul_ratings-v1_1 curl reviews.service.consul:9080/details/0 {"id":0,"author":"William Shakespeare","year":1595,"type":"paperback","pages":200,"publisher":"PublisherA","language":"English","ISBN-10":"1234567890","ISBN-13":"123-1234567890"}
不用懷疑部署的時候哪裏操做失誤了,就是官方的部署文件有坑……
要解決這個問題,咱們來看看sidecar的原理。
首先看看兩個部署用的yaml文件都作了什麼。因爲每一個微服務的部署都大同小異,這裏只貼出productpage
相關的內容。
bookinfo.yaml
:
version: '2' services: …… productpage-v1: image: istio/examples-bookinfo-productpage-v1:1.10.1 networks: istiomesh: ipv4_address: 172.28.0.14 dns: - 172.28.0.1 - 8.8.8.8 dns_search: - service.consul environment: - SERVICE_NAME=productpage - SERVICE_TAGS=version|v1 - SERVICE_PROTOCOL=http ports: - "9081:9080" expose: - "9080" ……
dns_search: - search.consul
。Docker Compose部署的這套樣例對短服務主機名的解析可能會有問題,因此這裏須要加個後綴。environment
環境變量的幾個設置。registrator
會以這些環境變量爲配置將服務註冊到consul
。SERVICE_NAME
是註冊的服務名,SERVICE_TAGS
是註冊服務的ServiceTags
,而SERVICE_PROTOCOL=http
則會將protocol: http
加入到ServiceMeta
。bookinfo.sidecar.yaml
:
version: '2' services: …… productpage-v1-init: image: docker.io/istio/proxy_init:0.7.1 cap_add: - NET_ADMIN network_mode: "container:consul_productpage-v1_1" command: - -p - "15001" - -u - "1337" productpage-v1-sidecar: image: docker.io/istio/proxy_debug:1.1.0 network_mode: "container:consul_productpage-v1_1" entrypoint: - su - istio-proxy - -c - "/usr/local/bin/pilot-agent proxy --serviceregistry Consul --serviceCluster productpage-v1 --zipkinAddress zipkin:9411 --configPath /var/lib/istio >/tmp/envoy.log" ……
proxy_init
,這個容器執行完就退出了;另外一個是實際的sidecar程序proxy_debug
。network_mode
,值爲container:consul_productpage-v1_1
。這是Docker的容器網絡模式,意思是這兩個容器和productpage-v1
共用同一個虛擬網卡,即它們在相同網絡棧上。proxy_init
sidecar的網絡代理通常是將一個端口轉發到另外一個端口。因此微服務使用的端口就必須和對外暴露的端口不同,這樣一來sidecar就不夠透明。
爲了使sidecar變得透明,以Istio使用proxy_init
設置了iptables的轉發規則(proxy_init
、proxy_debug
和productpage-v1
在相同的網絡棧上,因此這個配置對這三個容器都生效)。添加的規則爲:
好比productpage
服務使用的9080端口,當其餘服務經過9080端口訪問productpage
是,請求會先被iptables轉發到15001端口,Envoy再根據路由規則轉發到9080端口。這樣訪問9080的流量實際上都在15001繞了一圈,可是對外部來講,這個過程是透明的。
proxy_debug
proxy_debug
有兩個進程:pilot-agent
和envoy
。proxy_debug
啓動時,會先啓動pilot-agent
。pilot-agent
作的事很簡單,它生成了envoy
的初始配置文件/var/lib/istio/envoy-rev0.json
,而後啓動envoy
。後面的事就都交給envoy
了。
使用下面命令導出初始配置文件:
$ docker exec -it consul_productpage-v1-sidecar_1 cat /var/lib/istio/envoy-rev0.json > envoy-rev0.json
使用你心愛的編輯器打開初始配置文件,能夠看到有這麼一段:
…… "name": "xds-grpc", "type": "STRICT_DNS", "connect_timeout": "10s", "lb_policy": "ROUND_ROBIN", "hosts": [ { "socket_address": {"address": "istio-pilot", "port_value": 15010} } ], ……
這一段的意思是envoy
會鏈接到pilot
(控制平面的組件,忘記了請往上翻翻)的15010端口。這倆將按照xDS的API規範,使用GRPC協議實時同步配置數據。
xDS是Envoy約定的一系列發現服務(Discovery Service)的統稱。如CDS(Cluster Discovery Service),EDS(Endpoint Discovery Service),RDS(Route Discovery Service)等。Envoy動態配置須要從實現了xDS規範的接口(好比這裏的
pilot-discovery
)獲取配置數據。
總結一下,Envoy配置初始化流程爲:
圖片來自:Istio流量管理實現機制深度解析
那麼說envoy
實際使用的路由配置並不在初始配置文件中,而是pilot
生成並推送過來的。如何查看envoy
的當前配置呢?還好envoy
暴露了一個管理端口15000:
$ docker exec -it consul_productpage-v1-sidecar_1 curl localhost:15000/help admin commands are: /: Admin home page /certs: print certs on machine /clusters: upstream cluster status /config_dump: dump current Envoy configs (experimental) /contention: dump current Envoy mutex contention stats (if enabled) /cpuprofiler: enable/disable the CPU profiler /healthcheck/fail: cause the server to fail health checks /healthcheck/ok: cause the server to pass health checks /help: print out list of admin commands /hot_restart_version: print the hot restart compatibility version /listeners: print listener addresses /logging: query/change logging levels /memory: print current allocation/heap usage /quitquitquit: exit the server /reset_counters: reset all counters to zero /runtime: print runtime values /runtime_modify: modify runtime values /server_info: print server version/status information /stats: print server stats /stats/prometheus: print server stats in prometheus format
咱們能夠經過/config_dump
接口導出envoy
的當前配置:
$ docker exec -it consul_productpage-v1-sidecar_1 curl localhost:15000/config_dump > envoy.json
打開這個配置,看到這麼一段:
…… "listener": { "name": "0.0.0.0_9080", "address": { "socket_address": { "address": "0.0.0.0", "port_value": 9080 } }, "filter_chains": [ { "filters": [ { "name": "envoy.tcp_proxy", "typed_config": { "@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy", "stat_prefix": "outbound|9080||details.service.consul", "cluster": "outbound|9080||details.service.consul", ……
猜一下也能知道,這一段的意思是,訪問目標地址9080端口的出站流量,都會被路由到details
。太坑了!!!
從上面原理分析可知,這個問題的根源應該在於pilot
給Envoy生成的配置不正確。
查看pilot
源碼得知,pilot
在生成配置時,用一個map
保存Listener信息。這個map的key爲<ip>:<port>
。若是服務註冊的時候,沒有指明端口<port>
上的協議的話,默認認爲TCP協議。pilot
會將這個Listener和路由寫入到這個map
,並拒絕其餘相同地址端口再來監聽。因而只有第一個註冊的服務的路由會生效,全部流量都會走到那個服務。若是這個端口有指定使用HTTP協議的話,Pilot-discovery這裏生成的是一個RDS的監聽,這個RDS則根據域名路由到正確的地址。
簡單說就是全部微服務在註冊到consul
時應該在ServiceMeta
中說明本身9080端口的協議是http
。
等等,前面的bookinfo.yaml
配置裏,有指定9080端口的協議是了呀。咱們訪問一下consul
的接口看下ServiceMeta
是寫入了沒有:
果真沒有……看來Registrator註冊的時候出了岔子。網上搜了下,確實有Issue提到了這個問題:gliderlabs/registrator#633。istio.yaml
中使用的latest
版本的Registrator不支持寫入Consul的ServiceMeta。應該改成master
版本。
修改一下istio.yaml
配置。按照部署倒敘關閉sidecar、微服務,從新啓動控制平面,等registrator
啓動完畢後,從新部署微服務和sidecar。
# /samples/bookinfo/platform/consul $ docker-compose -f bookinfo.sidecars.yaml down $ docker-compose -f bookinfo.yaml down # /install/consul $ docker-compose -f istio.yaml up -d # /samples/bookinfo/platform/consul $ docker-compose -f bookinfo.yaml up -d $ docker-compose -f bookinfo.sidecars.yaml up -d
再訪問consul
的接口試試,有了(沒有的話多是registrator
沒啓動好致使沒註冊到consul
,再新部署下微服務和sidecar):
再訪問頁面,OK了。目前沒有配置路由規則,reviews
的版本是隨機的。多刷新幾回頁面,能夠看到打星在「沒有星星」、「黑色星星」和「紅色星星」三種效果間隨機切換。
使用地址http://localhost:9411
能訪問Zipkin鏈路跟蹤系統,查看微服務請求鏈路調用狀況。
咱們來看看正確的配置是什麼內容。再取出Envoy的配置,0.0.0.0_9080
的Listener內容變爲:
…… "listener": { "name": "0.0.0.0_9080", "address": { "socket_address": { "address": "0.0.0.0", "port_value": 9080 } }, "filter_chains": [ { "filters": [ { "name": "envoy.http_connection_manager", "typed_config": { "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager", "stat_prefix": "0.0.0.0_9080", "rds": { "config_source": { "ads": {} }, "route_config_name": "9080" }, ……
9080端口的出站路由規則由一個名稱爲"9080"
的route_config
定義。找一下這個route_config
:
…… "route_config": { "name": "9080", "virtual_hosts": [ { "name": "details.service.consul:9080", "domains": [ "details.service.consul", "details.service.consul:9080", "details", "details:9080", "details.service", "details.service:9080" ], "routes": [ { "match": { "prefix": "/" }, "route": { "cluster": "outbound|9080|v1|details.service.consul", …… }, …… } ] }, { "name": "productpage.service.consul:9080", "domains": [ "productpage.service.consul", "productpage.service.consul:9080", "productpage", "productpage:9080", "productpage.service", "productpage.service:9080" ], "routes": [ { "match": { "prefix": "/" }, "route": { "cluster": "outbound|9080|v1|productpage.service.consul", …… }, …… } ] }, ……
因爲內容太長,這裏只貼details
和productpage
的關鍵內容。能夠看到,9080端口的出站流量會根據目標地址的域名正確地轉發到對應的微服務。
注意:本節工做目錄爲/samples/bookinfo/platform/consul
。
最後咱們嘗試一下Istio的路由控制能力。在配置路由規則以前,咱們要先使用DestinationRule定義各個微服務的版本:
$ kubectl apply -f destination-rule-all.yaml
DestinationRule:DestinationRule定義了每一個服務下按照某種策略分割的子集。在本例子中按照版原本分子集,
reviews
分爲v一、v二、v3三個版本的子集,其餘微服務都只有v1一個子集。
使用命令kubectl get destinationrules -o yaml
能夠查看已配置的DestinationRule。
接下來咱們使用VirtualService來配置路由規則。virtual-service-all-v1.yaml
配置會讓全部微服務的流量都路由到v1版本。
$ kubectl apply -f virtual-service-all-v1.yaml
VirtualService:定義路由規則,按照這個規則決定每次請求服務應該將流量轉發到哪一個子集。
使用命令kubectl get virtualservices -o yaml
能夠查看已配置的VirtualService。
再刷新頁面,如今無論刷新多少次,reviews
都會使用v1版本,也就是頁面不會顯示星星。
下面咱們試一下基於用戶身份的路由規則。配置文件virtual-service-reviews-test-v2.yaml
配置了reviews
的路由,讓用戶jason
的流量路由到v2版本,其餘狀況路由到v1版本。
$ kubectl apply -f virtual-service-reviews-test-v2.yaml
執行命令後刷新頁面,能夠看到reviews
都使用的v1版本,頁面不會顯示星星。點擊右上角的Sign in
按鈕,以jason的身份登陸(密碼隨便),能夠看到reviews
切換到v2版本了,頁面顯示了黑色星星。
查看virtual-service-reviews-test-v2.yaml
文件內容能夠看到,基於身份的路由是按照匹配HTTP的headers實現的。當HTTP的headers有end-user: jason
的內容時路由到v2版本,不然路由到v1版本。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: reviews spec: hosts: - reviews.service.consul http: - match: - headers: end-user: exact: jason route: - destination: host: reviews.service.consul subset: v2 - route: - destination: host: reviews.service.consul subset: v1
istio.yaml
引用的Registrator的latest
版本不支持consul的ServiceMeta。要改成master
版本。istio.yaml
後,由於啓動時pilot
連不上istio-apiserver
,pilot
會失敗退出。等待istio-apiserver
啓動完畢後再跑一次istio.yaml
。kubectl
的context
,讓kubectl
使用istio-apiserver
提供的Kubernetes API接口。bookinfo.yaml
啓動各個微服務後,還要運行bookinfo.sidecar.yaml
以初始化和啓動sidecar。