什么是sidecar?

sidecar,直譯為邊車。 如上圖所示,邊車就是加裝在摩托車旁來達到拓展功能的目的,比如行駛更加穩(wěn)定,可以拉更多的人和貨物,坐在邊車上的人可以給駕駛員指路等。邊車模式通過給應用服務加裝一個“邊車”來達到控制和邏輯的分離的目的。
對于微服務來講,我們可以用邊車模式來做諸如 日志收集、服務注冊、服務發(fā)現(xiàn)、限流、鑒權等不需要業(yè)務服務實現(xiàn)的控制面板能力。通常和邊車模式比較的就是像spring-cloud那樣的sdk模式,像上面提到的這些能力都通過sdk實現(xiàn)。

這兩種實現(xiàn)模式各有優(yōu)劣,sidecar模式會引入額外的性能損耗以及延時,但傳統(tǒng)的sdk模式會讓代碼變得臃腫并且升級復雜,控制面能力和業(yè)務面能力不能分開升級。
本文的代碼已經(jīng)上傳到gitee
sidecar 實現(xiàn)原理
介紹了sidecar的諸多功能,但是,sidecar是如何做到這些能力的呢?
原來,在kubernetes中,一個pod是部署的最小單元,但一個pod里面,允許運行多個container(容器),多個container(容器)之間共享存儲卷和網(wǎng)絡棧。這樣子,我們就可以多container來做sidecar,或者init-container(初始化容器)來調(diào)整掛載卷的權限

日志收集sidecar
日志收集sidecar的原理是利用多個container間可以共用掛載卷的原理實現(xiàn)的,通過將應用程序的日志路徑掛出,用另一個程序訪問路徑下的日志來實現(xiàn)日志收集,這里用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 創(chuàng)建pod,通過kubectl logs命令就可以看到sidecar-container打印的日志輸出
kubectl logs webserver sidecar-container
轉(zhuǎn)發(fā)請求sidecar
這一節(jié)我們來實現(xiàn),一個給應用程序轉(zhuǎn)發(fā)請求的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秒向應用程序發(fā)出請求
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發(fā)出請求
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=UTF-8,31 bytes total),HttpProtocol(HTTP/1.1))