05.13 基於容器應用設計的原則,模式和反模式

容器和容器編排(Kubernetes)的廣泛使用,讓我們可以輕鬆的構建基於微服務的“雲原生”(Cloud Native)的應用。容器成為了雲時代的新的編程單元,類似面向對象概念下的

對象,J2EE中的組件或者函數式編程中的函數

在面向對象時代,有許多著名的設計原則,模式和反模式等,例如:

  • SOLID (單一功能、開閉原則、里氏替換、接口隔離以及依賴反轉

  • Design Patterns: Elements of Reusable Object-Oriented Software

  • Anti-Pattern

在新的容器背景下,相應的原則和模式有助於幫助我們更好的構建“雲原生”的應用。我們可以看到,這些原則和模式並非對之前模式的顛覆和推翻,更像是適應新環境的演進版本。

原則

單一職責原則 SINGLE CONCERN PRINCIPLE (SCP)

與OO的單一功能相對應,每一個容器應該提供單一的職責,只關注於做好一件事。單一職責使得容器更容易重用。通常容器對應於一個進程,而該進程專注於做好一件事。

基於容器應用設計的原則,模式和反模式

高可觀測性原則 HIGH OBSERVABILITY PRINCIPLE (HOP)

容器像對象一樣,應該是一個封裝良好的黑盒子。但是在雲的環境下,這個黑盒子應該提供良好的觀測接口,使得其在雲的環境下得到相應的監控和管理。這樣,整個應用才能提供一致的生命週期的管理。

基於容器應用設計的原則,模式和反模式

可觀測性包含:

  • 提供健康檢查 Health Check,或者心跳

  • 提供狀態

  • 把日誌輸出到標準輸出(STDOUT)和標準出錯(STDERR)

  • 等等

生命週期確認原則 LIFE-CYCLE CONFORMANCE PRINCIPLE (LCP)

生命週期確認原則指的是容器應該提供和平臺交互來處理相應的生命週期的變化。

基於容器應用設計的原則,模式和反模式

  • 捕獲並響應Terminate (SIGTERM)信號,來儘快優雅的終止服務進程,以避免kill (SIGKILL)信號強行終止進程。例如一下的NodeJS代碼。

    process.on('SIGTERM', function () { console.log("Received SIGTERM. Exiting.")
    server.close(function () {
    process.exit(0);
    });
    });
  • 返回退出碼

    process.exit(0);

鏡像不可變原則 IMAGE IMMUTABILITY PRINCIPLE (IIP)

在運行時,配置可以不同,但是鏡像應該是不可變的。

基於容器應用設計的原則,模式和反模式

我們可以理解為鏡像是個類,是容器是對象實例,類是不變的,而容器是擁有不同配置參數的鏡像實例。

進程用完既丟原則 PROCESS DISPOSABILITY PRINCIPLE (PDP)

在雲環境下,我們應該假定所有的容器都是臨時的,它隨時有可能被其它的容器實例所替代。

基於容器應用設計的原則,模式和反模式

這也就意味著需要把容器的狀態保存在容器之外。並且儘可能快速的啟動和終止容器。通常越小的容器就越容易實現這一點。

自包含原則 SELF-CONTAINMENT PRINCIPLE (S-CP)

容器在構建的時候應該包含所有的依賴,也就是所說容器在運行時不應該有任何的外部依賴。

基於容器應用設計的原則,模式和反模式

限制運行資源原則 RUNTIME CONFINEMENT PRINCIPLE (RCP)

容器的最佳實踐應該是在運行時指定容器對資源配置的需求。例如需要多少的內存,CPU等等。這樣做可以使得容器編排能都更有效的調度和管理資源。

基於容器應用設計的原則,模式和反模式

模式

許多容器應用的模式和Pod的概念相關,Pod是Kubernetes為了有效的管理容器而提出的概念,它是容器的集合,我們可以理解為“超容器”(我隨便發明的)。Pod包含的容器之間就好像運行在同一臺機器上,這些容器共享Localhost主機地址,可以本機通信,共享卷等等。

基於容器應用設計的原則,模式和反模式

Kubernetes 類似雲上OS,提供了用容器構建雲原生應用的最佳實踐。我們看看這些常見的模式都有什麼。

邊車(側鬥)(Sidecar)

基於容器應用設計的原則,模式和反模式

Sidecar是最常見的模式,在同一個Pod中,我們需要把不同的責任分在不同的容器中,對外部提供一個完整的功能。

基於容器應用設計的原則,模式和反模式

這樣的例子有很多,例如:

  • 上圖中的Node後端和提供緩存的Redis

  • Web服務器和收集日誌的服務

  • Web服務器和負責監控服務器性能數據的服務

這樣做有點類似面向對象的組合模式,好處有很多:

基於容器應用設計的原則,模式和反模式

  • 應用單一職責原則,每一個容器只負責專注做好一件事。

  • 隔離,容器之間不會出現互相競爭資源,當一個次要功能(例如日誌收集或者緩存)失效或者崩潰的時候,對主要功能的影響降至最小。

  • 可以對每一個容器進行獨立的生命週期管理

  • 可以對每一個容器進行獨立的彈性擴張

  • 可以方便的替換其中一個容器

代理(大使)容器

基於容器應用設計的原則,模式和反模式

類似於面向對象的Proxy模式,利用Pod中一個容器提供對外的訪問連接。如下圖中Node後端總是通過Service Discovery容器來和外部進行通信。

基於容器應用設計的原則,模式和反模式

這樣做,負責Node模塊開發的只需要假定所有的通信都是來自於本機,而把通信的複雜性交給代理容器,去處理諸如負載均衡,安全,過濾請求,必要時中斷通信等功能。

適配器容器

基於容器應用設計的原則,模式和反模式

大家常常會把面向對象的Proxy模式,Bridge模式和Adapter模式搞混,因為單單從UML關係圖上來看,它們都大同小異。似乎只是取了不同的名字。事實也確實如此,就像幾乎所有的OO模式都是組合模式的衍生,所有容器模式都是邊車模式的衍生。

在下圖的例子中,如果Logging Adapter的名字不提及Adapter,我們不會認為這是個適配器模式。

基於容器應用設計的原則,模式和反模式

其實適配器模式關注的是如果把Pod內部的不同容器的功能通過適配器統一的暴漏出來。在上圖中,如果我們再多加一個容器,它同時會向卷中寫入日誌的化,這樣就更清楚了。Logging Adapter適配不同容器用不同的接口提供的日誌,並提供統一的訪問接口。

容器鏈

基於容器應用設計的原則,模式和反模式

類似於OO的責任鏈模式,把負責不同功能的容器按照依賴順序鏈在一起,也是一種常見的模式。

基於容器應用設計的原則,模式和反模式

準備就緒的Pod

基於容器應用設計的原則,模式和反模式

通常作為服務的容器有一個啟動的過程,在啟動過程中,服務是不可用的。Kubernetes提供了Readiness探測功能。

readinessProbe: httpGet: path: / port: 5000 timeoutSeconds: 1 periodSeconds: 5

和其它模式相比,這個更像是一個使用Kubernetes的最佳實踐。

反模式

構建環境和運行環境混雜在一起

應該使得用於生產的運行環境的鏡像儘可能的小,避免在運行環境的鏡像中包含構建時的殘留。

例如下面的Dockerfile例子:

FROM ubuntu:14.04
RUN apt-get updateRUN apt-get install gcc
RUN gcc hello.c -o /hello

在這個構建的鏡像中,有很多不需要也不應該出現在生產環境中的東西,例如gcc,源代碼hello.c。這樣的結果既不安全(直接暴漏源代碼),也會有性能開銷(過大的鏡像體積導致加載變慢)。

Docker17.05 以後提供的multi-stage builds也可以解決這個問題。

直接使用Pod

避免直接使用Pod,用Deployment來管理Pod。利用Deployment可以很方便的對Pod進行擴展和管理。

使用latest標籤

Latest標籤用於標記最近的穩定版本,然而在創建容器時,儘可能避免在生產環境使用Latest標籤。即使使用imagePullPolicy選項為alway。

快速失敗的任務

Job是Kubernetes提供的只運行一次的容器,和service正好相反。要避免快速失敗

apiVersion: batch/v1kind: Jobmetadata: name: badspec: template: metadata: name: bad spec: restartPolicy: Never containers:
- name: box image: busybox command: ["/bin/sh", "-c", "exit 1"]

如果你嘗試在你的cluster裡面創建以上的Job,你可能會碰到如下的狀態。

$ kubectl describe jobs
Name:\t\tbad
Namespace:\tdefaultImage(s):\tbusybox
Selector:\tcontroller-uid=18a6678e-11d1-11e7-8169-525400c83acf
Parallelism:\t1Completions:\t1Start Time:\tSat, 25 Mar 2017 20:05:41 -0700Labels:\t\tcontroller-uid=18a6678e-11d1-11e7-8169-525400c83acf
job-name=bad
Pods Statuses:\t1 Running / 0 Succeeded / 24 FailedNo volumes.Events:
FirstSeen\tLastSeen\tCount\tFrom\t\t\tSubObjectPath\tType\t\tReason\t\t\tMessage ---------\t--------\t-----\t----\t\t\t-------------\t--------\t------\t\t\t-------
1m\t\t1m\t\t1\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\tCreated pod: bad-fws8g 1m\t\t1m\t\t1\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\tCreated pod: bad-321pk 1m\t\t1m\t\t1\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\tCreated pod: bad-2pxq1 1m\t\t1m\t\t1\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\tCreated pod: bad-kl2tj 1m\t\t1m\t\t1\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\tCreated pod: bad-wfw8q 1m\t\t1m\t\t1\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\tCreated pod: bad-lz0hq 1m\t\t1m\t\t1\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\tCreated pod: bad-0dck0 1m\t\t1m\t\t1\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\tCreated pod: bad-0lm8k 1m\t\t1m\t\t1\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\tCreated pod: bad-q6ctf 1m\t\t1s\t\t16\t{job-controller }\t\t\tNormal\t\tSuccessfulCreate\t(events with common reason combined)

因為任務快速失敗。Kubernetes認為任務沒能成功啟動,嘗試創建新的容器以恢復這個失敗,導致的Cluster會在短時間創建大量的容器,這樣的結果可能會消耗大量的計算資源。

在Spec中使用.spec.activeDeadlineSeconds來避免這個問題。這個參數定了等待多長時間重試失敗的Job。


分享到:


相關文章: