本文将介绍如何使用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。