knative queue-proxy

knative revision最終創建的deploy,會在pod中啓用一個sidecar容器,即queue-proxy。

下圖展示了queue-proxy的用途,業務流量首先進入 Istio Gateway,然後會轉發到 Queue-Proxy 的 8012 端口,Queue-Proxy 8012 再把請求轉發到業務容器的監聽端口。

queue-proxy

queue-proxy用途

  1. 應用請求數等指標統計。
  2. Pod健康檢查(k8s探針)
  3. 代理轉發流量
  4. 判斷Ingress是否ready需要通過訪問queue-proxy實現

queue-proxy預留的端口

knative的組件預留了一些端口作爲服務端口,例如8012端口默認是給queue使用的,因此應用的端口應該避開這些預留的端口。

  • 8012,queue-proxy代理的http端口,一般訪問應用流量的入口。
  • 8013,http2端口,用於grpc流量的轉發。
  • 8022,queue-proxy管理端口。
  • 9090,queue-proxy容器的監控端口,圖中的端口已過時。數據由Autoscaler採集,用於kpa。
  • 9091,queue-proxy採集到的應用監控指標(req請求數,響應市場等),圖中的端口已過時。由prometheus採集,用於計費。
    serving/pkg/apis/networking/ports.go
// The ports we setup on our services.
const (
	// ServiceHTTPPort is the port that we setup our Serving and Activator K8s services for
	// HTTP/1 endpoints.
	ServiceHTTPPort = 80

	// ServiceHTTP2Port is the port that we setup our Serving and Activator K8s services for
	// HTTP/2 endpoints.
	ServiceHTTP2Port = 81

	// BackendHTTPPort is the backend, i.e. `targetPort` that we setup for HTTP services.
	BackendHTTPPort = 8012

	// BackendHTTP2Port is the backend, i.e. `targetPort` that we setup for HTTP services.
	BackendHTTP2Port = 8013

	// QueueAdminPort specifies the port number for
	// health check and lifecycle hooks for queue-proxy.
	QueueAdminPort = 8022

	// AutoscalingQueueMetricsPort specifies the port number for metrics emitted
	// by queue-proxy for autoscaler.
	AutoscalingQueueMetricsPort = 9090

	// UserQueueMetricsPort specifies the port number for metrics emitted
	// by queue-proxy for end user.
	UserQueueMetricsPort = 9091

	// ServicePortNameHTTP1 is the name of the external port of the service for HTTP/1.1
	ServicePortNameHTTP1 = "http"

	// ServicePortNameH2C is the name of the external port of the service for HTTP/2
	ServicePortNameH2C = "http2"
)

默認情況通過queue-proxy的流量會轉發到應用的8080端口,實際上queue-proxy是從USER_PORT讀取的應用端口值。

func getUserPort(rev *v1alpha1.Revision) int32 {
	ports := rev.Spec.GetContainer().Ports

	if len(ports) > 0 && ports[0].ContainerPort != 0 {
		return ports[0].ContainerPort
	}

	return v1alpha1.DefaultUserPort
}

這裏可以看出userport等於第一個端口值,因此knative只支持一個端口,使用kantive serving時建議顯式指出服務端口。

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
  name: wordpress-serving
  namespace: default
spec:
  template:
    metadata:
      labels:
        app: wordpress
      annotations:
        autoscaling.knative.dev/target: "100"
    spec:
      containers:
        - image: registry.cn-hangzhou.aliyuncs.com/knative-sample/wordpress:5.2-20190524100810
          ports:
            - name: http1
              containerPort: 80
          env:
          - name: WORDPRESS_DB_HOST
            value: rm-2xx.mysql.rds.aliyuncs.com:3306
          - name: WORDPRESS_DB_USER
            value: wordpress
          - name: WORDPRESS_DB_PASSWORD
            value: xxx
          imagePullPolicy: Always

這裏我們主要注意的是port的name和端口值。
name 字段表示使用的協議,有效值只有http1 和 h2c 兩個,其中:http1 表示使用 http1 協議,比如 web 服務和 websock 都可以使用 http1。grpc 需要設置成 h2c。
containerPort 字段就是容器提供服務的唯一端口。

應用請求指標統計

knative kpa根據應用的請求次數進行擴縮容,因此queue-proxy的一個主要功能就是爲kpa提供相應指標。

serving/pkg/queue/request_metric.go

func (h *requestMetricHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	rr := pkghttp.NewResponseRecorder(w, http.StatusOK)
	startTime := time.Now()
	if h.breaker != nil {
		h.statsReporter.ReportQueueDepth(h.breaker.InFlight())
	}

	defer func() {
		// Filter probe requests for revision metrics.
		if network.IsProbe(r) {
			return
		}

		// If ServeHTTP panics, recover, record the failure and panic again.
		err := recover()
		latency := time.Since(startTime)
		if err != nil {
			h.sendRequestMetrics(http.StatusInternalServerError, latency)
			panic(err)
		}
		h.sendRequestMetrics(rr.ResponseCode, latency)
	}()

	h.handler.ServeHTTP(rr, r)
}

指標統計對應用的訪問,對k8s probe和knative probe不做統計。

knative probe

queue-proxy爲knative服務提供探針能力。

  • ingress的狀態依賴於底層pod的狀態,所以會使用probe探測pod是否ready。
    請求中header包含K-Network-Probe=probe

serving/pkg/network/probe_handler.go

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if ph := r.Header.Get(ProbeHeaderName); ph != ProbeHeaderValue {
		r.Header.Del(HashHeaderName)
		h.next.ServeHTTP(w, r)
		return
	}

	hh := r.Header.Get(HashHeaderName)
	if hh == "" {
		http.Error(w, fmt.Sprintf("a probe request must contain a non-empty %q header", HashHeaderName), http.StatusBadRequest)
		return
	}

	w.Header().Set(HashHeaderName, hh)
	w.WriteHeader(200)
}
  • ativitor要等到底層pod爲ready纔會轉發流量,所以也利用了queue-proxy的probe功能。請求中header包含K-Network-Probe=queue

serving/cmd/queue/main.go

func handleKnativeProbe(w http.ResponseWriter, r *http.Request, ph string, healthState *health.State, prober func() bool, isAggressive bool) {
	_, probeSpan := trace.StartSpan(r.Context(), "probe")
	defer probeSpan.End()

	if ph != queue.Name {
		http.Error(w, fmt.Sprintf(badProbeTemplate, ph), http.StatusBadRequest)
		probeSpan.Annotate([]trace.Attribute{
			trace.StringAttribute("queueproxy.probe.error", fmt.Sprintf(badProbeTemplate, ph))}, "error")
		return
	}

	if prober == nil {
		http.Error(w, "no probe", http.StatusInternalServerError)
		probeSpan.Annotate([]trace.Attribute{
			trace.StringAttribute("queueproxy.probe.error", "no probe")}, "error")
		return
	}

	healthState.HandleHealthProbe(func() bool {
		if !prober() {
			probeSpan.Annotate([]trace.Attribute{
				trace.StringAttribute("queueproxy.probe.error", "container not ready")}, "error")
			return false
		}
		return true
	}, isAggressive, w)
}

k8s probe

k8s的probe功能,從應用容器上移到queue-proxy,爲pod配置的probe實際配置給了queue-proxy,然後由queue-proxy轉發給應用容器。

serving/cmd/queue/main.go

func handler(reqChan chan queue.ReqEvent, breaker *queue.Breaker, handler http.Handler,
	healthState *health.State, prober func() bool, isAggressive bool) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		if network.IsKubeletProbe(r) {
			handler.ServeHTTP(w, r)
			return
		}

		if ph := network.KnativeProbeHeader(r); ph != "" {
			handleKnativeProbe(w, r, ph, healthState, prober, isAggressive)
			return
		}

		proxyCtx, proxySpan := trace.StartSpan(r.Context(), "proxy")
		defer proxySpan.End()

		// Metrics for autoscaling.
		in, out := queue.ReqIn, queue.ReqOut
		if activator.Name == network.KnativeProxyHeader(r) {
			in, out = queue.ProxiedIn, queue.ProxiedOut
		}
		reqChan <- queue.ReqEvent{Time: time.Now(), EventType: in}
		defer func() {
			reqChan <- queue.ReqEvent{Time: time.Now(), EventType: out}
		}()
		network.RewriteHostOut(r)

		// Enforce queuing and concurrency limits.
		if breaker != nil {
			if err := breaker.Maybe(r.Context(), func() {
				handler.ServeHTTP(w, r.WithContext(proxyCtx))
			}); err != nil {
				switch err {
				case context.DeadlineExceeded, queue.ErrRequestQueueFull:
					http.Error(w, err.Error(), http.StatusServiceUnavailable)
				default:
					w.WriteHeader(http.StatusInternalServerError)
				}
			}
		} else {
			handler.ServeHTTP(w, r.WithContext(proxyCtx))
		}
	}
}

參考:https://yq.aliyun.com/articles/722412?spm=a2c4e.11155435.0.0.539af84bEVQCgz

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章