Istio Source Code Reading
WHY
istio のコードの全体像を掴みたい、その中でできれば catch-all virtual service という概念の挙動を正しく理解したい
controller が読みたくなるので istiod の deployment の中で特徴的な option 名で ag して下の file を見つけた
これがきっとそう?istiod の deployment で動かしている docker image の cmd と同名っぽいので多分正しい?
istio/istio / - master 4 jobs
:) % docker inspect docker.io/istio/pilot:latest | jq '.[0].ContainerConfig.Cmd' 21-01-14 19:17:23
[
"/bin/sh",
"-c",
"#(nop) ",
"ENTRYPOINT [\"/usr/local/bin/pilot-discovery\"]"
]
パット discoveryServer
をただ起動しているだけのように見えるのでこいつの挙動を見に行こう
https://github.com/istio/istio/blob/295f9a900dc6c9f0d5fd21dfaa8df713e0d83aee/pilot/pkg/bootstrap/server.go#L347 するとここにたどり着く
ただその中身を見ると server を起動している様子で resource の変更を検知しているような挙動がぱっと見えなかった。
もしかして違うコンポーネントだったりする?
よくわからないので istio を minikube で入れて何が起こるかを見よう
#!/bin/bash
set -eux
ISTIO_VERSION=1.8.1
ISTIO_PATH="./istio-${ISTIO_VERSION}"
if [[ -d $ISTIO_PATH ]]
then
echo "detected istio is installed"
else
curl -L https://istio.io/downloadIstio | sh -
fi
minikube start
ISTIOCTL=./${ISTIO_PATH}/bin/istioctl
${ISTIOCTL} install --set profile=demo -y
kubectl label namespace default istio-injection=enabled --overwrite
これで作った cluster を見ると deployment が下のものしかないしそれっぽいのは istiod
$ kubectl get deploy -A
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
istio-system istio-egressgateway 1/1 1 1 31s
istio-system istio-ingressgateway 1/1 1 1 31s
istio-system istiod 1/1 1 1 51s
kube-system coredns 2/2 2 2 55s
念の為にすべての deployment を確認する
$ kubectl get deploy -A -o json | jq '.items[].spec.template.spec.containers[] | {"image": .image, "cmd": .command}'
{
"image": "docker.io/istio/proxyv2:1.8.1",
"cmd": null
}
{
"image": "docker.io/istio/proxyv2:1.8.1",
"cmd": null
}
{
"image": "docker.io/istio/pilot:1.8.1",
"cmd": null
}
{
"image": "k8s.gcr.io/coredns:1.6.7",
"cmd": null
}
coredns は istio に関係ないので無視する
pilot は今見ているもの
残るは proxyv で中身は pilot-agent らしい
potsbo/istio-experiment / - ✚ … master 1 job
:) % docker inspect docker.io/istio/proxyv2:1.8.1 | jq '.[0].Config.Entrypoint' 21-01-14 20:45:50
[
"/usr/local/bin/pilot-agent"
]
envoy がやっていることの確証が持てないけど ingressgateway と egressgateway という名前なんだし proxy であって virtualservice の内容を直接見るような人ではないんじゃないかと思っている
ところでこの過程で operator というものを発見してしまった。名前的にはめっちゃ controller 持ってそうだけどどうだろうか、でもこれ cluster の中で動いてなさそうなんだけどな
operator は https://istio.io/latest/docs/setup/install/operator/ を見ると istio の install を簡単にしてくれる君であって、istio のロジックそれ自体には関係ないという理解でいいかな
envoy と istio がどうやって情報をやり取りしているのかを知らないとちょっとイメージが付きづらい
というのも envoy が定期的に pilot-discovery が起動している server に情報を取りに来てるのかな
そこで適当な pod の istio sidecar を見てみるとその image は docker.io/istio/proxyv2 だった
どうやらこいつは sidecar にも ingress, egress にも使える proxy という感じに理解すれば良さそう
今の所の勝手な予想だけど下のようになっている?
pilot-discovery
が virtualservice, destinationrule, gateway などの情報を集めてどの envoy がどんな config を持つべきかを知っている人
pilot-agent
sidecar と proxy の2つのモード(subcommand) がある
:) % kubectl get deploy -n istio-system istio-ingressgateway -o json | jq '.spec.template.spec.containers[0].args' 21-01-14 21:30:43
[
"proxy",
"router",
"--domain",
"$(POD_NAMESPACE).svc.cluster.local",
"--proxyLogLevel=warning",
"--proxyComponentLogLevel=misc:error",
"--log_output_level=default:info",
"--serviceCluster",
"istio-ingressgateway"
]
istio/istio / - master 7 jobs
:) % kubectl get deploy -n istio-system istio-egressgateway -o json | jq '.spec.template.spec.containers[0].args' 21-01-14 21:30:54
[
"proxy",
"router",
"--domain",
"$(POD_NAMESPACE).svc.cluster.local",
"--proxyLogLevel=warning",
"--proxyComponentLogLevel=misc:error",
"--log_output_level=default:info",
"--serviceCluster",
"istio-egressgateway"
]
当たり前かもしれないけど proxvy にのみ envoy が存在している
istio/istio / - master 8 jobs
:( % docker run --entrypoint bash -it docker.io/istio/proxyv2:1.8.1 21-01-14 21:37:40
root@1cbb70354ea2:/# which envoy
/usr/local/bin/envoy
root@1cbb70354ea2:/# exit
istio/istio / - master 8 jobs
:) % docker run --entrypoint bash -it docker.io/istio/pilot:latest 21-01-14 21:37:58
istio-proxy@1d42fcf860c1:/$ which envoy
istio-proxy@1d42fcf860c1:/$
ここで作った envoy がなのか見てみると ここで実際にenvoy の binary を呼び出しているのがわかる
envoy に渡される引数は多すぎてちょっとわけが分からなさそうなのでとりあえず Envoy API がどんなものなのかを知りたい
APIには、エンドポイントの設定を提供する Endpoint Discovery Service (EDS) や、クラスタの設定を提供する Cluster Discovery Service (CDS) などがあります。 これらをまとめてxDS APIと呼びます。
と書いてある
https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol を見ると他にも LDS, CDS, LDS などいろんな DS があるので xDS というおしゃれな名前にしたということだと理解した
istioctl dashboard envoy -l app=ratings
で istio dashboard が見れるので http://localhost:15000/config_dump
に行くと envoy の config が見れる
ざっくりとしたイメージでは多分これを pilot-discovery が serve していて pilot-agent が起動した envoy が request しに行ってるのではなかろうか
実際にはこれは dump 用に作った別の endpoint で実際の口は別にありそう、grpc も使えるんだし
ここで落ちてくる json をざっくり見るといろんな type がある
いま興味があるのは "type.googleapis.com/envoy.admin.v3.RoutesConfigDump" だと思う
jq < bar.json '.configs[]["@type"]'
"type.googleapis.com/envoy.admin.v3.BootstrapConfigDump"
"type.googleapis.com/envoy.admin.v3.ClustersConfigDump"
"type.googleapis.com/envoy.admin.v3.ListenersConfigDump"
"type.googleapis.com/envoy.admin.v3.ScopedRoutesConfigDump"
"type.googleapis.com/envoy.admin.v3.RoutesConfigDump"
"type.googleapis.com/envoy.admin.v3.SecretsConfigDump"
https://github.com/envoyproxy/envoy/blob/4cadf72e0b40d6dfcfaf755c243734e751927756/api/envoy/service/route/v3/rds.proto#L31 あたりを見ると Route の Config をとってくるために grpc で stream してそうな気配を感じる
Resources are requested via subscriptions, by specifying a filesystem path to watch, initiating gRPC streams, or polling a REST-JSON URL. The latter two methods involve sending requests with a DiscoveryRequest proto payload.
と書いてあるので REST の API と grpc のどっちも本質的には同じ情報が返ってくると理解して良さそう
というわけで自分がもともと知りたかった「VirtualService の評価順に関する挙動」というのは pilot-discovery のサーバー実装を見れば良さそうということになりそう
:) % kubectl get po istiod-67dbfcd4dd-z2p5x -n istio-system -o json | jq '.spec.containers[0].args' 21-01-14 22:31:08
[
"discovery",
"--monitoringAddr=:15014",
"--log_output_level=default:info",
"--domain",
"cluster.local",
"--keepaliveMaxServerConnectionAge",
"30m"
]
手元の istio を入れた minikube の istiod に設定されている arg を調べると上のようになっていた
https://github.com/istio/istio/blob/295f9a900dc6c9f0d5fd21dfaa8df713e0d83aee/pilot/pkg/bootstrap/server.go#L385 つまりこの部分の http server のみが立ち上がることになっていそう
つまり手元の minikube の istio は http の polling で config の更新をしているということになりそう
パット見て httpServer にまともな Handler が設定されてないなと思ったらどうやらこいつは debug の readiness 専用でロジックに関わることは何もやっていない様子
どうでもいいけど typo 見つけたmultplexing
じゃなくて multiplexing
だと思う
grpcAddr
が設定されてないから grpc で動いてないんだなと思ったらそんなことなくて default value がしっかり設定してあった https://github.com/istio/istio/blob/295f9a900dc6c9f0d5fd21dfaa8df713e0d83aee/pilot/cmd/pilot-discovery/main.go#L145
istio/istio / - master 1 job
:) % kubectl logs istiod-67dbfcd4dd-z2p5x -n istio-system | head -n 100 21-01-14 22:48:41
2021-01-14T10:57:35.663530Z info FLAG: --appNamespace=""
2021-01-14T10:57:35.663573Z info FLAG: --caCertFile=""
2021-01-14T10:57:35.663579Z info FLAG: --clusterID="Kubernetes"
2021-01-14T10:57:35.663581Z info FLAG: --clusterRegistriesNamespace="istio-system"
2021-01-14T10:57:35.663583Z info FLAG: --configDir=""
2021-01-14T10:57:35.663585Z info FLAG: --ctrlz_address="localhost"
2021-01-14T10:57:35.663589Z info FLAG: --ctrlz_port="9876"
2021-01-14T10:57:35.663591Z info FLAG: --domain="cluster.local"
2021-01-14T10:57:35.663595Z info FLAG: --grpcAddr=":15010"
2021-01-14T10:57:35.663599Z info FLAG: --help="false"
2021-01-14T10:57:35.663601Z info FLAG: --httpAddr=":8080"
2021-01-14T10:57:35.663603Z info FLAG: --httpsAddr=":15017"
2021-01-14T10:57:35.663606Z info FLAG: --keepaliveInterval="30s"
2021-01-14T10:57:35.663609Z info FLAG: --keepaliveMaxServerConnectionAge="30m0s"
2021-01-14T10:57:35.663611Z info FLAG: --keepaliveTimeout="10s"
2021-01-14T10:57:35.663613Z info FLAG: --kubeconfig=""
2021-01-14T10:57:35.663616Z info FLAG: --kubernetesApiBurst="160"
2021-01-14T10:57:35.663619Z info FLAG: --kubernetesApiQPS="80"
2021-01-14T10:57:35.663621Z info FLAG: --log_as_json="false"
2021-01-14T10:57:35.663623Z info FLAG: --log_caller=""
2021-01-14T10:57:35.663628Z info FLAG: --log_output_level="default:info"
2021-01-14T10:57:35.663630Z info FLAG: --log_rotate=""
2021-01-14T10:57:35.663632Z info FLAG: --log_rotate_max_age="30"
2021-01-14T10:57:35.663634Z info FLAG: --log_rotate_max_backups="1000"
2021-01-14T10:57:35.663637Z info FLAG: --log_rotate_max_size="104857600"
2021-01-14T10:57:35.663639Z info FLAG: --log_stacktrace_level="default:none"
2021-01-14T10:57:35.663645Z info FLAG: --log_target="[stdout]"
2021-01-14T10:57:35.663648Z info FLAG: --mcpInitialConnWindowSize="1048576"
2021-01-14T10:57:35.663650Z info FLAG: --mcpInitialWindowSize="1048576"
2021-01-14T10:57:35.663652Z info FLAG: --mcpMaxMsgSize="4194304"
2021-01-14T10:57:35.663654Z info FLAG: --meshConfig="./etc/istio/config/mesh"
2021-01-14T10:57:35.663656Z info FLAG: --monitoringAddr=":15014"
2021-01-14T10:57:35.663658Z info FLAG: --namespace="istio-system"
2021-01-14T10:57:35.663660Z info FLAG: --networksConfig="/etc/istio/config/meshNetworks"
2021-01-14T10:57:35.663665Z info FLAG: --plugins="[authn,authz,health]"
2021-01-14T10:57:35.663667Z info FLAG: --profile="true"
2021-01-14T10:57:35.663674Z info FLAG: --registries="[Kubernetes]"
2021-01-14T10:57:35.663677Z info FLAG: --resync="1m0s"
2021-01-14T10:57:35.663679Z info FLAG: --secureGRPCAddr=":15012"
2021-01-14T10:57:35.663680Z info FLAG: --tlsCertFile=""
2021-01-14T10:57:35.663682Z info FLAG: --tlsKeyFile=""
2021-01-14T10:57:35.663684Z info FLAG: --trust-domain=""
したがって次に見るべき entrypoint は
https://github.com/istio/istio/blob/295f9a900dc6c9f0d5fd21dfaa8df713e0d83aee/pilot/pkg/bootstrap/server.go#L376
になる
この server の実体はここ
s.XDSServer.Register(s.grpcServer)
つまりここであり
discovery.RegisterAggregatedDiscoveryServiceServer(rpcs, s)
これは proto による自動生成コード
実際に呼び出されるコードは proto を見に行けばわかる
proto を見に行くと下のように書いてある
service AggregatedDiscoveryService {
// This is a gRPC-only API.
rpc StreamAggregatedResources(stream DiscoveryRequest) returns (stream DiscoveryResponse) {
}
rpc DeltaAggregatedResources(stream DeltaDiscoveryRequest)
returns (stream DeltaDiscoveryResponse) {
}
}
というわけで StreamAggregatedResources
を見に行けばいいはず
// StreamAggregatedResources implements the ADS interface.
func (s *DiscoveryServer) StreamAggregatedResources(stream discovery.AggregatedDiscoveryService_StreamAggregatedResourcesServer) error {
return s.Stream(stream)
}
これはたらい回しにしてるだけで実体はそのすぐ下
ここが Stream
の本質的なところと考えて良さそう
for {
// Block until either a request is received or a push is triggered.
// We need 2 go routines because 'read' blocks in Recv().
//
// To avoid 2 routines, we tried to have Recv() in StreamAggregateResource - and the push
// on different short-lived go routines started when the push is happening. This would cut in 1/2
// the number of long-running go routines, since push is throttled. The main problem is with
// closing - the current gRPC library didn't allow closing the stream.
select {
case req, ok := <-reqChannel:
if !ok {
// Remote side closed connection or error processing the request.
return receiveError
}
// processRequest is calling pushXXX, accessing common structs with pushConnection.
// Adding sync is the second issue to be resolved if we want to save 1/2 of the threads.
err := s.processRequest(req, con)
if err != nil {
return err
}
case pushEv := <-con.pushChannel:
err := s.pushConnection(con, pushEv)
pushEv.done()
if err != nil {
return err
}
case <-con.stop:
return nil
}
}
下の2つの用途の chan を用意しておいてそれぞれに response を client に送るということをしている様子
- client からの(更新の?) request が来たとき
- server 側で変更を検知(多分) を検知したとき
いずれのケースでも下のように pushXds
を呼び出すことになる
response のデータそれ自体を作っているのはここっぽい
Generate
は誰の func かというとここで設定されているものの様子
// initGenerators initializes generators to be used by XdsServer.
func (s *DiscoveryServer) initGenerators() {
edsGen := &EdsGenerator{Server: s}
s.StatusGen = NewStatusGen(s)
s.Generators[v3.ClusterType] = &CdsGenerator{Server: s}
s.Generators[v3.ListenerType] = &LdsGenerator{Server: s}
s.Generators[v3.RouteType] = &RdsGenerator{Server: s}
s.Generators[v3.EndpointType] = edsGen
s.Generators[v3.NameTableType] = &NdsGenerator{Server: s}
s.Generators[v3.ExtensionConfigurationType] = &EcdsGenerator{Server: s}
s.Generators["grpc"] = &grpcgen.GrpcConfigGenerator{}
s.Generators["grpc/"+v3.EndpointType] = edsGen
s.Generators["grpc/"+v3.ListenerType] = s.Generators["grpc"]
s.Generators["grpc/"+v3.RouteType] = s.Generators["grpc"]
s.Generators["grpc/"+v3.ClusterType] = s.Generators["grpc"]
s.Generators["api"] = &apigen.APIGenerator{}
s.Generators["api/"+v3.EndpointType] = edsGen
s.Generators["api/"+TypeURLConnect] = s.StatusGen
s.Generators["event"] = s.StatusGen
}
もともとのモチベーションを再度思い出すと routing の挙動が知りたかった
つまりここを読めばよいということになる
generator はこれで生成している様子なので
// NewConfigGenerator creates a new instance of the dataplane configuration generator
func NewConfigGenerator(plugins []string, cache model.XdsCache) ConfigGenerator {
return v1alpha3.NewConfigGenerator(registry.NewPlugins(plugins), cache)
}
ここが本体
func (configgen *ConfigGeneratorImpl) BuildHTTPRoutes(node *model.Proxy, push *model.PushContext,
model.WatchedResource
の ResourceNames
という field が []string
でこれを routeNames という変数に入れて loop しているんだけど具体的に何が入っているのかわからない
route という抽象概念がある?
勝手に想像してた作りは下のような感じ
- virtualservice や destinationrule を常に watch して正しい route を持っている
- 必要に応じて envoy にくばる
しかし見ている感じありえる route を先に知っていて、それに応じて必要な resource を見に行っている?
それぞれの route は下のようなものらしい、すごいざっくりには kubernetes の service に対応するものだと思ってれば今回は問題なさそう
$ jq < bar.json '.configs[4].dynamic_route_configs[0]' 21-01-15 0:18:23
{
"version_info": "2021-01-14T13:08:44Z/8",
"route_config": {
"@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"name": "kube-dns.kube-system.svc.cluster.local:9153",
"virtual_hosts": [
{
"name": "kube-dns.kube-system.svc.cluster.local:9153",
"domains": [
"kube-dns.kube-system.svc.cluster.local",
"kube-dns.kube-system.svc.cluster.local:9153",
"kube-dns.kube-system",
"kube-dns.kube-system:9153",
"kube-dns.kube-system.svc.cluster",
"kube-dns.kube-system.svc.cluster:9153",
"kube-dns.kube-system.svc",
"kube-dns.kube-system.svc:9153",
"10.96.0.10",
"10.96.0.10:9153"
],
"routes": [
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "outbound|9153||kube-dns.kube-system.svc.cluster.local",
"timeout": "0s",
"retry_policy": {
"retry_on": "connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes",
"num_retries": 2,
"retry_host_predicate": [
{
"name": "envoy.retry_host_predicates.previous_hosts"
}
],
"host_selection_retry_max_attempts": "5",
"retriable_status_codes": [
503
]
},
"max_stream_duration": {
"max_stream_duration": "0s"
}
},
"decorator": {
"operation": "kube-dns.kube-system.svc.cluster.local:9153/*"
},
"name": "default"
}
],
"include_request_attempt_count": true
}
],
"validate_clusters": false
},
"last_updated": "2021-01-14T13:10:27.649Z"
}
routeName
と virtualHosts
の関係はわかりそうでイマイチわからないところがある
allow_any
があると port 番号ごとにまとめられる...?
jq < bar.json '[.configs[4].dynamic_route_configs[].route_config | {"virtual_hoshs": [.virtual_hosts[].name], "name": .name }] | sort_by(.name)' 21-01-15 0:42:19
[
{
"virtual_hoshs": [
"allow_any",
"istiod.istio-system.svc.cluster.local:15010"
],
"name": "15010"
},
{
"virtual_hoshs": [
"allow_any",
"istiod.istio-system.svc.cluster.local:15014"
],
"name": "15014"
},
{
"virtual_hoshs": [
"allow_any",
"istio-egressgateway.istio-system.svc.cluster.local:80",
"istio-ingressgateway.istio-system.svc.cluster.local:80"
],
"name": "80"
},
{
"virtual_hoshs": [
"allow_any",
"details.default.svc.cluster.local:9080",
"productpage.default.svc.cluster.local:9080",
"ratings.default.svc.cluster.local:9080",
"reviews.default.svc.cluster.local:9080"
],
"name": "9080"
},
{
"virtual_hoshs": [
"istio-ingressgateway.istio-system.svc.cluster.local:15021"
],
"name": "istio-ingressgateway.istio-system.svc.cluster.local:15021"
},
{
"virtual_hoshs": [
"kube-dns.kube-system.svc.cluster.local:9153"
],
"name": "kube-dns.kube-system.svc.cluster.local:9153"
}
]
ちょっとここはよくわからないけどとりあえず route の組み立てはここを見れば良さそう
今気になっているのはここの記述
Although the order of evaluation for rules in any given source VirtualService will be retained, the cross-resource order is UNDEFINED.
ここで UNDEFINED
といっているのは
virtualServices = egressListener.VirtualServices()
ここで帰ってくる slice の順序に依存するということかなと思う
次のような順で func が呼び出されていく
BuildHTTPRoutes
buildSidecarOutboundHTTPRouteConfig
buildSidecarOutboundVirtualHosts
BuildSidecarVirtualHostsFromConfigAndRegistry
buildSidecarVirtualHostsForVirtualService
BuildHTTPRoutesForVirtualService
がこの中でどこにも VirtualService を merge 指定そうな場所がない
gateway の方にはそれっぽいのがあるんだが...
と思ってもう一度 document を読みに行ったら
A VirtualService can only be fragmented this way if it is bound to a gateway. Host merging is not supported in sidecars.
と書いてある
つまり自分が知りたかった実装はそもそも存在していなかったということだった
悲しい
ではなぜ gateway だけこの機能が実装されているのか
CombineVHostRoutes
が実装された commit
この commit message に
restrict merging to gateways only
とある
PR は https://github.com/istio/istio/pull/8114 だがこれは https://github.com/istio/istio/pull/8113 の replica だと書いてある。確証はないが、release branch にも master にも入れることで何らかのプロジェクトの release flow に従っているのかなと思った。
そしてもともとの PR では https://github.com/istio/istio/issues/7793 を gateway のために直していると書いてある。ここになぜ sidecar に対応しなかったのかは書いていない。
そこで元 issue を読みに行くことにする。
ちょっと長くて全部は読んでないけど基本的に sidecar のについては何も考えてないように見えるけどそれがなぜなのかわからなかった
少なくともこの issue でリンクされている他の issue ではどれも gateway など外部からの request を受け取る virtual service について議論しているということだけわかった
Merging virtual services at the sidecar requires a lot of intrusive changes to the RDS code.
と書いているのでとりあえずなんか難しいということしかわからない
https://github.com/istio/istio/issues/22997 で sidecar にどうやって入れるのかについて議論している
mesh gateway というのが sidecar のことなんじゃないかと思っている。
とりあえず色んな人が同じように困っていていろんな workaround を考えているということがわかった
結論
- VirtualService を複数定義していいのは Gateway からに対してのみ
- sidecar に対しても適応するのは検討されてはいるという段階
わかった istio の全体像
- istio を構成するコマンドは全部で4つある
- pilot-discovery
- pilot-agent
- istioctl
- operator
- pilot-discovery
- xDS をしゃべる全 envoy に config を巻く
- VirtualService などの情報を集めて envoy の API 形式に変換する君
- pilot-agent
- 適切な引数で envoy を起動するしている
- sidecar でも動いているし、ingress, egress としても動いている
- istioctl
- utility な command line でこれ自体はなくても istio は使える
- operator
- istio の install や upgrade を簡単にしてくれる utility