本文分享自華為雲社群《一步一步教你寫kubernetes sidecar》,作者: 張儉。
什麼是sidecar?
sidecar,直譯為邊車。 如上圖所示,邊車就是加裝在摩托車旁來達到擴充功能的目的,比如行駛更加穩定,可以拉更多的人和貨物,坐在邊車上的人可以給駕駛員指路等。邊車模式透過給應用服務加裝一個“邊車”來達到控制和邏輯的分離的目的。
對於微服務來講,我們可以用邊車模式來做諸如 日誌收集、服務註冊、服務發現、限流、鑑權等不需要業務服務實現的控制皮膚能力。通常和邊車模式比較的就是像spring-cloud那樣的sdk模式,像上面提到的這些能力都透過sdk實現。
這兩種實現模式各有優劣,sidecar模式會引入額外的效能損耗以及延時,但傳統的sdk模式會讓程式碼變得臃腫並且升級複雜,控制面能力和業務面能力不能分開升級。
本文的程式碼已經上傳到gitee
sidecar 實現原理
介紹了sidecar的諸多功能,但是,sidecar是如何做到這些能力的呢?
原來,在kubernetes中,一個pod是部署的最小單元,但一個pod裡面,允許執行多個container(容器),多個container(容器)之間共享儲存卷和網路棧。這樣子,我們就可以多container來做sidecar,或者init-container(初始化容器)來調整掛載卷的許可權
日誌收集sidecar
日誌收集sidecar的原理是利用多個container間可以共用掛載卷的原理實現的,透過將應用程式的日誌路徑掛出,用另一個程式訪問路徑下的日誌來實現日誌收集,這裡用cat來替代了日誌收集,部署yaml模板如下
apiVersion: v1 kind: Pod metadata: name: webserver spec: volumes: - name: shared-logs emptyDir: {} containers: - name: nginx image: ttbb/nginx:mate volumeMounts: - name: shared-logs mountPath: /opt/sh/openresty/nginx/logs - name: sidecar-container image: ttbb/base command: ["sh","-c","while true; do cat /opt/sh/openresty/nginx/logs/nginx.pid; sleep 30; done"] volumeMounts: - name: shared-logs mountPath: /opt/sh/openresty/nginx/logs
使用kubectl create -f 建立pod,透過kubectl logs命令就可以看到sidecar-container列印的日誌輸出
kubectl logs webserver sidecar-container
轉發請求sidecar
這一節我們來實現,一個給應用程式轉發請求的sidecar,應用程式程式碼如下
use std::io::prelude::*; use std::net::{TcpListener, TcpStream}; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } println!("Hello, world!"); } fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let contents = "Hello"; let response = format!( "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}", contents.len(), contents ); println!("receive a request!"); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); }
我們再來寫一個sidecar,它會每15秒嚮應用程式發出請求
use std::thread; use std::time::Duration; fn main() { loop { thread::sleep(Duration::from_secs(15)); let response = reqwest::blocking::get("http://localhost:7878").unwrap(); println!("{}", response.text().unwrap()) } }
透過倉庫下的intput/build.sh
指令碼構造映象,執行yaml如下
apiVersion: v1 kind: Pod metadata: name: webserver spec: containers: - name: input-server image: sidecar-examples:input-http-server - name: input-sidecar image: sidecar-examples:sidecar-input
透過檢視kubectl logs input input-http-server可以看到input-http-server收到了請求
receive a request!
receive a request!
攔截請求sidecar
應用程式程式碼,它會每15s向localhost
發出請求
package com.shoothzj.sidecar import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors import akka.http.scaladsl.Http import akka.http.scaladsl.model._ import scala.concurrent.{ExecutionContextExecutor, Future} import scala.util.{Failure, Success} object HttpClient { def main(args: Array[String]): Unit = { while (true) { Thread.sleep(15_000L) implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "SingleRequest") // needed for the future flatMap/onComplete in the end implicit val executionContext: ExecutionContextExecutor = system.executionContext val responseFuture: Future[HttpResponse] = Http().singleRequest(HttpRequest(uri = "http://localhost:7979/hello")) responseFuture .onComplete { case Success(res) => println(res) case Failure(_) => sys.error("something wrong") } } } }
我們再來寫一個sidecar,它會攔截http請求並列印日誌
package com.shoothzj.sidecar import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors import akka.http.scaladsl.Http import akka.http.scaladsl.model._ import akka.http.scaladsl.server.Directives._ import scala.concurrent.ExecutionContextExecutor import scala.io.StdIn object HttpServer { def main(args: Array[String]): Unit = { implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "my-system") // needed for the future flatMap/onComplete in the end implicit val executionContext: ExecutionContextExecutor = system.executionContext val route = path("hello") { get { println("receive a request") complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>")) } } val bindingFuture = Http().newServerAt("localhost", 7979).bind(route) while (true) { Thread.sleep(15_000L) } } }
透過倉庫下的output/build.sh
指令碼構造映象,執行yaml如下
apiVersion: v1 kind: Pod metadata: name: output spec: volumes: - name: shared-logs emptyDir: {} containers: - name: output-workload image: sidecar-examples:output-workload imagePullPolicy: Never - name: sidecar-output image: sidecar-examples:sidecar-output imagePullPolicy: Never
透過檢視kubectl logs output output-workload可以看到output-sidecar收到了請求
HttpResponse(200 OK,List(Server: akka-http/10.2.9, Date: Tue, 29 Mar 2022 00:15:47 GMT),HttpEntity.Strict(text/html; charset=UTF-8,31 bytes total),HttpProtocol(HTTP/1.1)) HttpResponse(200 OK,List(Server: akka-http/10.2.9, Date: Tue, 29 Mar 2022 00:16:02 GMT),HttpEntity.Strict(text/html; charset=UTF-8,31 bytes total),HttpProtocol(HTTP/1.1)) HttpResponse(200 OK,List(Server: akka-http/10.2.9, Date: Tue, 29 Mar 2022 00:16:17 GMT),HttpEntity.Strict(text/html; charset=UTF-8,31 bytes total),HttpProtocol(HTTP/1.1)) HttpResponse(200 OK,List(Server: akka-http/10.2.9, Date: Tue, 29 Mar 2022 00:16:32 GMT),HttpEntity.Strict(text/html; charset=UTF-8,31 bytes total),HttpProtocol(HTTP/1.1)) HttpResponse(200 OK,List(Server: akka-http/10.2.9, Date: Tue, 29 Mar 2022 00:16:47 GMT),HttpEntity.Strict(text/html; charset=UTF-8,31 bytes total),HttpProtocol(HTTP/1.1)) HttpResponse(200 OK,List(Server: akka-http/10.2.9, Date: Tue, 29 Mar 2022 00:17:02 GMT),HttpEntity.Strict(text/html; charset=UTF-8,31 bytes total),HttpProtocol(HTTP/1.1)) HttpResponse(200 OK,List(Server: akka-http/10.2.9, Date: Tue, 29 Mar 2022 00:17:17 GMT),HttpEntity.Strict(text/html; charset=UTF-8,31 bytes total),HttpProtocol(HTTP/1.1)) HttpResponse(200 OK,List(Server: akka-http/10.2.9, Date: Tue, 29 Mar 2022 00:17:32 GMT),HttpEntity.Strict(text/html; charset=