点击查看目录
本文为翻译文章,点击查看原文。
Sidecar 代理模式是一个重要的概念,它允许Istio为服务网格中运行的服务提供路由、度量、安全和其他功能。在这篇文章中,我将解释为 Istio 提供支持的关键技术,同时还将向您展示一种构建简单的 HTTP 流量嗅探 sidecar 代理的方法。
引言
服务网格的实现通常依赖于 sidecar 代理,这些代理使得服务网格能够控制、观察和加密保护应用程序。sidecar 代理是反向代理,所有流量在到达目标服务之前流过它。代理将分析流经自己的流量并生成有用的统计信息,而且还能提供灵活的路由功能。此外,代理还可以使用mTLS来加密保护应用程序流量。
在这篇文章中,我们将构建一个简单的 sidecar 代理,它可以嗅探 HTTP 流量并生成统计信息,例如请求大小,响应状态等。然后,我们将在Kubernetes Pod 中部署 HTTP 服务,配置 sidecar 代理,并检查生成的统计信息。
构建 HTTP 流量嗅探代理
Istio 依靠Envoy来代理网络流量。Envoy 代理被打包为一个容器,并部署在一个 Pod 中的服务容器旁边。在这篇文章中,我们将使用 Golang 来构建一个可以嗅探 HTTP 流量的微型代理。
我们的代理需要在 TCP 端口上侦听传入的 HTTP 请求,然后将它们转发到目标地址。因为在我们的例子中,代理和服务都驻留在同一个 Pod 中,所以目标主机可以通过环回 IP 地址(即,127.0.0.1)进行寻址。但是,我们仍然需要一个端口号来标识目标服务。
const (
proxyPort = 8000
servicePort = 80
)
现在,我们可以开始编写代理的骨架代码了。代理将侦听proxyPort
上的请求并将请求转发给servicePort
。代理将在服务每个请求后最终打印统计信息。
// Create a structure to define the proxy functionality.
type Proxy struct{}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Forward the HTTP request to the destination service.
res, duration, err := p.forwardRequest(req)
// Notify the client if there was an error while forwarding the request.
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
// If the request was forwarded successfully, write the response back to
// the client.
p.writeResponse(w, res)
// Print request and response statistics.
p.printStats(req, res, duration)
}
func main() {
// Listen on the predefined proxy port.
http.ListenAndServe(fmt.Sprintf(":%d", proxyPort), &Proxy{})
}
代理最重要的部分是它转发请求的能力。我们首先在代理实现中定义此功能。
func (p *Proxy) forwardRequest(req *http.Request) (*http.Response, time.Duration, error) {
// Prepare the destination endpoint to forward the request to.
proxyUrl := fmt.Sprintf("http://127.0.0.1:%d%s", servicePort, req.RequestURI)
// Print the original URL and the proxied request URL.
fmt.Printf("Original URL: http://%s:%d%s\n", req.Host, servicePort, req.RequestURI)
fmt.Printf("Proxy URL: %s\n", proxyUrl)
// Create an HTTP client and a proxy request based on the original request.
httpClient := http.Client{}
proxyReq, err := http.NewRequest(req.Method, proxyUrl, req.Body)
// Capture the duration while making a request to the destination service.
start := time.Now()
res, err := httpClient.Do(proxyReq)
duration := time.Since(start)
// Return the response, the request duration, and the error.
return res, duration, err
}
现在我们得到了代理请求的响应,让我们定义将其写回客户端的逻辑。
func (p *Proxy) writeResponse(w http.ResponseWriter, res *http.Response) {
// Copy all the header values from the response.
for name, values := range res.Header {
w.Header()[name] = values
}
// Set a special header to notify that the proxy actually serviced the request.
w.Header().Set("Server", "amazing-proxy")
// Set the status code returned by the destination service.
w.WriteHeader(res.StatusCode)
// Copy the contents from the response body.
io.Copy(w, res.Body)
// Finish the request.
res.Body.Close()
}
代理的最后一部分是打印统计信息。让我们继续将其实现。
func (p *Proxy) printStats(req *http.Request, res *http.Response, duration time.Duration) {
fmt.Printf("Request Duration: %v\n", duration)
fmt.Printf("Request Size: %d\n", req.ContentLength)
fmt.Printf("Response Size: %d\n", res.ContentLength)
fmt.Printf("Response Status: %d\n\n", res.StatusCode)
}
至此,我们已经构建了一个功能齐全的 HTTP 流量嗅探代理。
为代理构建容器镜像
Istio 打包了 Envoy 并将其作为 sidecar 容器运行在服务容器旁边。让我们构建一个代理容器镜像,运行上面的 Go 代码来模仿 Istio 的运行模式。
# Use the Go v1.12 image for the base.
FROM golang:1.12
# Copy the proxy code to the container.
COPY main.go .
# Run the proxy on container startup.
ENTRYPOINT [ "go" ]
CMD [ "run", "main.go" ]
# Expose the proxy port.
EXPOSE 8000
要构建代理容器镜像,我们可以简单地执行以下 Docker 命令:
$ docker build -t venilnoronha/amazing-proxy:latest -f Dockerfile .
设置 Pod 网络
我们需要设置 Pod 网络以确保 sidecar 代理能够接收所有应用程序的流量,以便它可以对其进行分析并转发到所需的目标。实现此目的的一种方法是要求用户将所有客户端请求地址指向代理端口,同时将代理配置为指向目标服务端口。这使用户体验变得复杂。更好更透明的方法是使用 Linux 内核中的Netfilter/iptables组件。
Kubernetes 网络
为了更好地理解,让我们列出 Kubernetes 向 Pod 公开的网络接口。
$ kubectl run -i --rm --restart=Never busybox --image=busybox -- sh -c "ip addr"
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
174: eth0@if175: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
如您所见,Pod 可以访问至少 2 个网络接口,即lo
和eth0
。lo
接口表示环回地址,eth0
表示以太网。这里要注意的是这些是虚拟的而不是真正的接口。
使用 iptables 进行端口映射
iptables
最简单的用途是将一个端口映射到另一个端口。我们可以利用它来透明地将流量路由到我们的代理。Istio 正是基于这个确切的概念来建立它的 Pod 网络。
这里的想法是将eth0
接口上的服务端口(80
)映射到代理端口(8000
)。这将确保每当容器尝试通过端口80
访问服务时,来自容器外部的流量就会路由到代理。如上图所示,我们让lo
接口将 Pod 内部流量直接路由到目标服务,即没有跳转到代理服务。
Init 容器
Kubernetes 允许在 Pod 运行普通容器之前运行init容器
。Istio 使用 init 容器来设置 Pod 网络,以便设置必要的 iptables 规则。这里,让我们做同样的事情来将 Pod 外部流量路由到代理。
#!/bin/bash
# Forward TCP traffic on port 80 to port 8000 on the eth0 interface.
iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 80 -j REDIRECT --to-port 8000
# List all iptables rules.
iptables -t nat --list
我们现在可以使用此初始化脚本创建 Docker 容器镜像。
# Use the latest Ubuntu image for the base.
FROM ubuntu:latest
# Install the iptables command.
RUN apt-get update && \
apt-get install -y iptables
# Copy the initialization script into the container.
COPY init.sh /usr/local/bin/
# Mark the initialization script as executable.
RUN chmod +x /usr/local/bin/init.sh
# Start the initialization script on container startup.
ENTRYPOINT ["init.sh"]
要构建 Docker 镜像,只需执行以下命令:
$ docker build -t venilnoronha/init-networking:latest -f Dockerfile .
演示
我们已经构建了一个代理和一个 init 容器来建立 Pod 网络。现在是时候进行测试了。为此,我们将使用httpbin容器作为服务。
部署 Deployment
Istio 自动注入 init 容器和代理。但是,对于我们的实验,可以手动制作 Pod yaml。
apiVersion: v1
kind: Pod
metadata:
name: httpbin-pod
labels:
app: httpbin
spec:
initContainers:
- name: init-networking
image: venilnoronha/init-networking
securityContext:
capabilities:
add:
- NET_ADMIN
privileged: true
containers:
- name: service
image: kennethreitz/httpbin
ports:
- containerPort: 80
- name: proxy
image: venilnoronha/amazing-proxy
ports:
- containerPort: 8000
我们已经设置了具有root
权限的 init 容器,并将proxy
和service
配置为普通容器。要在 Kubernetes 集群上部署它,我们可以执行以下命令:
$ kubectl apply -f httpbin.yaml
测试
为了测试部署,我们首先确定 Pod 的 ClusterIP。为此,我们可以执行以下命令:
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
httpbin-pod 2/2 Running 0 21h 172.17.0.4 minikube
我们现在需要从 Pod 外部生成流量。为此,我将使用busybox
容器通过curl
发出 HTTP 请求。
首先,我们向 httpbin 服务发送一个 GET 请求。
$ kubectl run -i --rm --restart=Never busybox --image=odise/busybox-curl \
-- sh -c "curl -i 172.17.0.4:80/get?query=param"
HTTP/1.1 200 OK
Content-Length: 237
Content-Type: application/json
Server: amazing-proxy
然后,再发送一个 POST 请求。
$ kubectl run -i --rm --restart=Never busybox --image=odise/busybox-curl \
-- sh -c "curl -i -X POST -d 'body=parameters' 172.17.0.4:80/post"
HTTP/1.1 200 OK
Content-Length: 317
Content-Type: application/json
Server: amazing-proxy
最后,向/status
端点发送一个 GET 请求。
$ kubectl run -i --rm --restart=Never busybox --image=odise/busybox-curl \
-- sh -c "curl -i http://172.17.0.4:80/status/429"
HTTP/1.1 429 Too Many Requests
Content-Length: 0
Content-Type: text/html; charset=utf-8
Server: amazing-proxy
请注意,我们将请求发送到端口80
,即服务端口而不是代理端口。iptables
规则确保首先将其路由到代理,然后将请求转发给服务。此外,我们还看到了额外的请求头Server: amazing-proxy
,这个请求头是我们手动实现的的代理自动加上的。
代理统计
现在我们来看看代理生成的统计数据。为此,我们可以运行以下命令:
$ kubectl logs httpbin-pod --container="proxy"
Original URL: http://172.17.0.4:80/get?query=param
Proxy URL: http://127.0.0.1:80/get?query=param
Request Duration: 1.979348ms
Request Size: 0
Response Size: 237
Response Status: 200
Original URL: http://172.17.0.4:80/post
Proxy URL: http://127.0.0.1:80/post
Request Duration: 2.026861ms
Request Size: 15
Response Size: 317
Response Status: 200
Original URL: http://172.17.0.4:80/status/429
Proxy URL: http://127.0.0.1:80/status/429
Request Duration: 1.191793ms
Request Size: 0
Response Size: 0
Response Status: 429
如您所见,我们确实看到代理的结果与我们生成的请求相匹配。
结论
本文中,我们实现了一个简单的 HTTP 流量嗅探代理,使用 init 容器将其嵌入 Kubernetes Pod 与原有服务无缝连接。而且,我们也了解了iptables
是如何提供灵活的网络,以便在处理代理时提供优良的用户体验的。最重要的是,我们已经学会了关于 Istio 实现的一些关键概念。