特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
單機吞吐量 | 萬級,吞吐量比RocketMQ和Kafka要低了一個數量級 | 萬級,吞吐量比RocketMQ和Kafka要低了一個數量級 | 10萬級,RocketMQ也是可以支撐高吞吐的一種MQ | 10萬級別,這是kafka最大的優點,就是吞吐量高。一般配合大數據類的系統來進行實時數據計算、日誌採集等場景 |
時效性 | ms級 | 微秒級 | ms級 | 延遲在ms級以內 |
可用性 | 高,基於主從架構實現高可用性 | 高,基於主從架構實現高可用性 | 非常高,分佈式架構 | 非常高,kafka是分佈式的,一個數據多個副本,少數機器宕機,不會丟失數據,不會導致不可用 |
消息可靠性 | 有較低的概率丟失數據 | 經過參數優化配置,可以做到0丟失 | 經過參數優化配置,消息可以做到0丟失 | |
功能支持 | MQ領域的功能極其完備 | 基於erlang開發,所以併發能力很強,性能極其好,延時很低 | MQ功能較爲完善,還是分佈式的,擴展性好 | 功能較爲簡單,主要支持簡單的MQ功能,在大數據領域的實時計算以及日誌採集被大規模使用,是事實上的標準 |
實戰 | 雲原生時代的微服務架構
{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:Damon","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"博客:","attrs":{}},{"type":"link","attrs":{"href":"http://www.damon8.cn","title":"","type":null},"content":[{"type":"text","text":"http://www.damon8.cn","attrs":{}}]}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"程序猿Damon | 微服務 | 容器化 | 自動化","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"寫作本書的目的","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 微服務架構已經火了很多年了,如:Dubbo、Spring Cloud,再到後來的 Spring Cloud Alibaba,但都是僅限於 Java 語言的瓶頸,如何讓各種語言之間的微服務更加有效、快速的通訊,這是當前很多企業需要面臨的問題,因爲一個企業中,不只是基於單純的某一種語言開發,這就涉及到多語言服務之間的訪問。本書的創作重點,則是在於講述在巨多語言的情況下,該如何設計微服務架構,以及雲原生時代的微服務的高可用、自動化等等。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"如何閱讀本書","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 由於本書介紹的一些知識都是比較流行的,最近幾年很火的,而且本身其涉及的面還是很廣的:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"K8s","attrs":{}},{"type":"text","text":",大家可以通過其","attrs":{}},{"type":"link","attrs":{"href":"https://kubernetes.io/","title":null,"type":null},"content":[{"type":"text","text":"官網","attrs":{}}],"marks":[{"type":"strong"}]},{"type":"text","text":"系統的學習其相關技術與實踐,以便可以更好的將其發揮於微服務架構設計中。另外,本書中介紹的知識,大家都可以一邊實踐、一邊閱讀,以便更深刻的理解其原理。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"掃碼關注作者","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者畢業於三流院校,筆名:Damon,擁有七年工作經驗,技術愛好者,長期從事 Java 開發、Spring Cloud、Golang 的微服務架構設計,以及結合 Docker、K8s 做微服務容器化,自動化部署等一站式項目落地。目前主要從事基於 K8s 雲原生架構研發的工作。Golang 語言開發,長期研究邊緣計算框架 KubeEdge、調度框架 Volcano 等。公衆號:","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#9254DE","name":"purple"}},{"type":"strong","attrs":{}}],"text":"程序猿Damon","attrs":{}},{"type":"text","text":",個站:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"www.damon8.cn","attrs":{}},{"type":"text","text":"。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"歡迎關注","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"公號:程序猿Damon","attrs":{}},{"type":"text","text":",回覆「入羣」加入技術交流羣","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"目錄","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"第一部分 基礎知識","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第一章 什麼是微服務架構","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1.1 微服務到底是啥","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1.2 微服務的發展史","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1.3 微服務的革命性與重要性","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第二章 微服務的拆分","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2.1 微服務的設計原則","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2.2 微服務劃分的粒度","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2.3 不同場景的微服務","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第三章 容器化技術","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3.1 什麼是容器","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3.2 容器的發展進程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3.3 Docker 與 Kubernetes","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3.4 K8s 容器化應用","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第四章 爲何藉助容器助力微服務","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"4.1 微服務的多語言性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"4.2 微服務的高可用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"4.3 微服務的複雜性","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"第二部分 原理與應用","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第五章 Kubernetes 介紹","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"5.1 Kubernetes 的基本概念與特性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"5.2 部署 Kubernetes 集羣","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"5.3 Kubernetes 的組件及及負載均衡","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第六章 爲什麼選擇 Kubernetes","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"6.1 Kubernetes 與微服務的天生絕配","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"6.2 基於 Kubernetes 集羣的服務治理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"6.3 基於 Kubernetes 的服務無縫遷移","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第七章 第一個基於 K8s 的多語言微服務架構","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"7.1 基於 K8s 的 Java 微服務","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"7.2 第一個Golang微服務","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"7.3 部署微服務應用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"正文","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"第一部分 基礎知識","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第一章 什麼是微服務架構","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1.1 微服務的發展史","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在微服務到來之前,單體應用程序所暴露的缺點主要有:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"複雜性高","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"團隊協作開發成本高","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"擴展性差","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"部署效率低下","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"系統很差的高可用性","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 複雜性,體現在:隨着業務的不斷迭代,項目的代碼量急劇的增多,項目模塊也會隨着而增加,整個項目就會變成的非常複雜。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 開發成本高,體現在:團隊開發幾十個人在修改代碼,然後一起合併到同一地址分支,打包部署,測試階段只要有一小塊功能有問題,就得重新編譯打包部署,重新測試,所有相關的開發人員都得參與其中,效率低下,開發成本極高。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 擴展性差,體現在:在新增功能業務的時候,代碼層面會考慮在不影響現有的業務基礎上編寫代碼,提高了代碼的複雜性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 部署效率低,體現在:當單體應用的代碼越來越多,依賴的資源越來越多時,應用編譯打包、部署測試一次,需要花費的時間越來越多,導致部署效率低下。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 高可用差,體現在:由於所有的業務功能最後都部署到同一個文件,一旦某一功能涉及的代碼或者資源有問題,那就會影響到整個文件包部署的功能。舉個特別鮮明的示例:上世紀八、九十年代,很多的黃頁以及延伸到後來的網站中,很多的展示頁面與獲取數據的後端都是在一個服務模塊中。這就造成一個很不好的影響:如果只是修改極小部分的頁面展示或圖片展示,則需要把整個服務模塊進行打包部署,這樣會導致時間的嚴重浪費以及成本的增加。更加糟糕的是,給用戶帶來非常不好的體驗,用戶無法理解的是:只是換個網站的某塊微小的展示區,導致了整個網站在那一時刻無法正常的訪問。當然,也許,對於那個時候互聯網的不發達,人們對於這樣的體驗,已經算是一種幸福的享受了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 由於單體應用具有以上的種種缺點,導致了一個新名詞、新概念的誕生:微服務。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1.2 微服務到底是啥","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 其實,從早年間的單體應用,到 2014 年起,得益於以 Docker 爲代表的容器化技術的成熟以及 DevOps 文化的興起,服務化的思想進一步演化,演變爲今天我們所熟知的微服務。那麼,微服務到底是啥?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 微服務,英文名:microservice,百度百科上將其定義爲:SOA 架構的一種變體。微服務(或微服務架構)是一種將應用程序構造爲一組低耦合的服務。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微服務有着一些鮮明的特點:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"功能單一","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務粒度小","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務間獨立性強","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務間依賴性弱","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務獨立維護","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務獨立部署","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 對於每一個微服務來說,其提供的功能應該是單一的;其粒度很小的;它只會提供某一業務功能涉及到的相關接口。如:電商系統中的訂單系統、支付系統、產品系統等,每一個系統服務都只是做該系統獨立的功能,不會涉及到不屬於它的功能邏輯。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 微服務之間的依賴性應該是儘量弱的,這樣帶來的好處是:不會因爲單一系統服務的宕機,而導致其它系統無法正常運行,從而影響用戶的體驗。同樣以電商系統爲例:用戶將商品加入購物車後,提交訂單,這時候去支付,發現無法支付,此時,可以將訂單進入待支付狀態,從而防止訂單的丟失和用戶體驗的不友好。如果訂單系統與支付系統的強依賴性,會導致訂單系統一直在等待支付系統的迴應,這樣會導致用戶的界面始終處於加載狀態,從而導致用戶無法進行任何操作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 當出現某個微服務的功能需要升級,或某個功能需要修復 bug 時,只需要把當前的服務進行編譯、部署即可,不需要一個個打包整個產品業務功能的巨多服務,獨立維護、獨立部署。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上面描述的微服務,其實突出其鮮明特性:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"高內聚、低耦合","attrs":{}},{"type":"text","text":",問題來了。什麼是高內聚,什麼是低耦合呢?所謂高內聚:就是說每個服務處於同一個網絡或網域下,而且相對於外部,整個的是一個封閉的、安全的盒子。盒子對外的接口是不變的,盒子內部各模塊之間的接口也是不變的,但是各模塊內部的內容可以更改。模塊只對外暴露最小限度的接口,避免強依賴關係。增刪一個模塊,應該只會影響有依賴關係的相關模塊,無關的不應該受影響。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 所謂低耦合:從小的角度來看,就是要每個 Java 類之間的耦合性降低,多用接口,利用 Java 面向對象編程思想的封裝、繼承、多態,隱藏實現細節。從模塊之間來講,就是要每個模塊之間的關係降低,減少冗餘、重複、交叉的複雜度,模塊功能劃分儘可能單一。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1.3 微服務的革命性與重要性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上一小節講述了什麼是微服務,微服務的鮮明特性。其實,從單體應用看微服務,就能看出微服務的重要性,它是徹底改革了應用程序的慣性,它的設計理念的出現:讓開發人員減少大量的開發成本以及修復成本;讓產品的使用者擁有一種舒適的體驗感。它解決了單體應用程序的很多難以解決的問題,更具有創新性。它讓我們的系統儘可能快地響應變化。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 微服務將原來耦合在一起的複雜業務拆分爲單個服務,規避了原本複雜度無止境的積累,每一個微服務專注於單一功能,並通過定義良好的接口清晰表述服務邊界。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 由於微服務具備獨立的運行進程,所以每個微服務可以獨立部署。當業務迭代時只需要發佈相關服務的迭代即可,降低了測試的工作量同時也降低了服務發佈的風險。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在微服務架構下,當某一組件發生故障時,故障會被隔離在單個服務中。如通過限流、熔斷等方式降低錯誤導致的危害,保障核心業務的正常運行。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第二章 微服務的拆分","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2.1 微服務的設計原則","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"高內聚、低耦合","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 緊密關聯的事應該放在一起,每個服務是針對一個單一職責的業務能力的封裝,需要專注於做好一件事情。這樣避免內容耦合,降低代碼的冗餘,提高代碼的可複用性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 避免服務之間的數據庫的共享。這樣既可以減少數據庫的併發操作,又可以避免死鎖的出現。通常微服務不會直接共用一個數據庫,可以通過主、從表的方式來進行,結合讀、寫分離,實現數據的共享。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 微服務之間應該是輕量級的通信方式,主要是爲了解耦,降低業務的複雜性,以及服務的負載。一個服務調用另一個服務應該是不受到後者的牽制,如果後者發生宕機,前者應該照樣繼續運行下去,這纔是微服務設計時的合理安排。爲了降低前者的不受牽制,這就需要輕量級的通信,比如:異步調用、重試策略等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以業務爲中心","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 每個服務代表了特定的業務邏輯,不應該摻着其他業務的邏輯,或微不足道的、公共的邏輯。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 圍繞業務開展,主要就當前業務進行擴展。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 能快速的響應業務的變化,需要做到接口的兼容性,兼容業務場景的變化,這樣減少變動太大帶來的風險。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 隔離實現細節,讓業務領域可以被重用,需要以封裝接口形式來實現業務邏輯,這就涉及到代碼的複用性。引用 Java 的原理:封裝、繼承、多態。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"彈性容錯設計","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 設計可容錯的系統:擁抱失敗,爲已知的錯誤而設計,這主要是爲了增強交互。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 可防禦的系統:服務降級、服務隔離、請求限制、防止級聯錯誤等,這也是爲了增強交互的友好。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自治和高可用","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 獨立開發和業務擴展,這就涉及到服務隨着業務的發展而進行兼容、合理的擴展後的高度自治。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 獨立部署、運行和高可用,避免單點的孤注一擲。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"日誌與監控","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 聚合系統日誌、數據,從而當遇到問題時,可以深入分析原因。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 當需要重現問題時,可以根據日誌以及監控來複盤。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 監控主要包括服務狀態、請求流量、調用鏈、API 錯誤計數,結構化的日誌、服務依賴關係可視化等內容,以便發現問題及時修復,實時調整系統負載,必要時進行服務降級,過載保護等等,從而讓系統和環境提供高效高質量的服務。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自動化","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 降低部署和發佈的難度,如:在持續集成和持續交付中,自動化編譯,測試,安全掃描,打包,集成測試,部署。隨着服務越來越多,在發佈過程中,需要進一步自動化金絲雀部署。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 利用 K8s 等進行服務自動彈性伸縮等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2.2 微服務劃分的粒度","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 服務的劃分,可以從水平的功能劃分,也可從垂直的業務劃分,粒度的大小,可以根據當前的產品需求來定位,最關鍵的是要做到:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"高內聚、低耦合","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如電商系統爲例,如下圖:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cc/cc086c43f4f5b547f2e3405d26ff7ae8.jpeg","alt":"","title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 電商中涉及到業務很可能是最多的,商品、庫存、訂單、促銷、支付、會員、購物車、發票、店鋪等等,這個是根據業務的不同來進行模塊的劃分。微服務劃分的粒度一定是要有明確性的,不能因爲含糊而新增一個服務模塊,這樣會導致功能接口的可複用性差。一個好的架構設計,肯定是可複用性很強的結構模式。我喜歡這樣的一句話:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"微服務的邊界 (粒度) 是 \"決策\", 而不是個 \"標準答案\"","attrs":{}},{"type":"text","text":"。即應該將各微服務劃分的方式,深度思考,周全的考量各方面的因素下,所作出的一個”最適合”的架構決策,而不是一個人芸亦芸的”標準答案“。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2.3 不同場景的微服務","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 微服務的應用場景也是很多的,電商場景是比較常見的,比如阿里的體系:淘寶、支付寶、釘釘、餓了麼、鹹魚、口碑等。電商場景的微服務實相對比較複雜的,所以需要更好的做好微服務的拆分以及擴展。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 金融系統中也存在微服務的場景,比如:銀行系統、證券系統、金融公司、機構。其需要考慮的重點是微服務的安全性、可靠性。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第三章 容器化技術","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3.1 什麼是容器","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 什麼是容器呢?自然界的解釋:容器是指用以容納物料並以殼體爲主的基本裝置。但今天講的容器也是一個容納物質的載體。那計算機所指的容器(Container)到底是什麼呢?容器是鏡像(Image)的運行時實例。正如從虛擬機模板上啓動 VM 一樣,用戶也同樣可以從單個鏡像上啓動一個或多個容器。虛擬機和容器最大的區別是容器更快並且更輕量級,與虛擬機運行在完整的操作系統之上相比,容器會共享其所在主機的操作系統/內核。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 爲什麼要用容器呢?假設你在使用一臺電腦開發一個應用,而且開發環境具有特定的配置。其他開發人員身處的環境配置可能稍有不同。你正在開發的應用不止依賴於您當前的配置,還需要某些特定的庫、依賴項和文件。與此同時,你的企業還擁有標準化的開發和生產環境,有着自己的配置和一系列支持文件。你希望儘可能多在本地模擬這些環境,而不產生重新創建服務器環境的開銷。這時候,就會需要容器來模擬這些環境。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 我們常見的容器啓動方式是 Docker,Docker 是一個開源的應用容器引擎,基於 Go 語言 並遵從 Apache2.0 協議開源。Docker 可以讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中,然後發佈到任何 Linux 機器上,也可以實現虛擬化。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/04/04a9b140ed18d59ceb7ccbeb2155e737.jpeg","alt":"","title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3.2 容器的發展進程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 2010 年,幾個年輕小夥在舊金山成立了一家做 PaaS 平臺的公司,起名爲\"dotCloud\",該公司主要是基於 PaaS 平臺爲開發者或開發商提供技術服務。他們提供了對多種運行環境支持。但隨着市場接受度、規模、加上科技巨頭等影響,有一天 dotCloud 的創始人 Solomon Hykes 就召集了公司核心開發人員,商量準備開源 Docker 技術。因此,在 2013 年 3 月,Docker 正式以開源軟件形式在 pycon 網站(見下圖)首次發佈了。正式由於這次開源,讓容器領域煥發了第二春。後來在美國,幾乎所有的雲計算廠商都在擁抱 Docker 這個生態圈。很快 Docker 技術風靡全球,於是,dotCloud 決定改名爲 Docker Inc(下面簡稱\"Docker\"),全身心投入到 Docker 的開發中。更名後的 Docker 並於 2014 年 8 月,Docker 宣佈把平臺即服務的業務 dotCloud 出售給位於德國柏林的平臺即服務提供商 cloudControl,自此 dotCloud 和 Docker 分道揚鑣。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d1/d14c4c065f26b524a66c33b93c7e6529.jpeg","alt":"","title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3.3 Docker 與 Kubernetes","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Google 多年來一直使用容器作爲交付應用程序的一種重要方式,且運行有一款名爲 Borg 的編排工具。Google、RedHat 等公司爲了對抗以 Docker 公司爲核心的容器商業生態,他們一起成立了 CNCF(Cloud Native Computing Foundation)。當谷歌於 2014 年 3 月開始開發 Kubernetes 時,很明智的選擇當時最流行的容器,沒錯,就是 Docker。Kubernetes 對 Docker 容器運行時的支持,迎來了大量的使用用戶。Kubernetes 於 2014 年 6 月 6 日首次發佈。這便有了容器編排工具 Kubernetes 的誕生。另外,CNCF 的目的是以開源的 K8S 爲基礎,使得 K8S 能夠在容器編排方面能夠覆蓋更多的場景,提供更強的能力。K8S 必須面臨 Swarm 和 Mesos 的挑戰。Swarm 的強項是和 Docker 生態的天然無縫集成,Mesos 的強項是大規模集羣的管理和調度。K8S 是 Google 基於公司已經使用了十多年的 Borg 項目進行了沉澱和昇華才提出的一套框架。它的優點就是有一套完整的全新的設計理念,同時有 Google 的背書,而且在設計上有很強的擴展性,所以,最終 K8S 贏得了勝利,成爲了容器生態的行業標準。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3.4 K8s 容器化應用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 前面說了,K8s 是一種編排容器管理容器工具,那麼如何通過 K8s 來將服務容器化呢?首先,我們來看看 K8s 如何使用?第一條就是編寫配置文件,因爲配置文件可以是 YAML 或者 JSON 格式的。爲方便閱讀與理解,在後面的講解中,我會統一使用 YAML 文件來指代它們。 Kubernetes 跟 Docker 等很多項目最大的不同,就在於它不推薦你使用命令行的方式直接運行容器(雖然 Kubernetes 項目也支持這種方式,比如:kubectl run),而是希望你用 YAML 文件的方式,即:把容器的定義、參數、配置,統統記錄在一個 YAML 文件中,然後用這樣一句指令把它運行起來:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubectl create -f xxx.yaml\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 這樣做最直接的一個好處是:你會有一個文件能記錄下 K8s 到底 run 了哪些東西。比如下面這個例子:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: tomcat-deployment\nspec:\n selector:\n matchLabels:\n app: tomcat\n replicas: 2\n template:\n metadata:\n labels:\n app: tomcat\n spec:\n containers:\n - name: tomcat\n image: tomcat:10.0.5\n ports:\n - containerPort: 80\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 像這樣的一個 YAML 文件,對應到 kubernetes 中,就是一個 API Object(API 對象)。當你爲這個對象的各個字段填好值並提交給 Kubernetes 之後,Kubernetes 就會負責創建出這些對象所定義的容器或者其他類型的 API 資源。可以看到,這個 YAML 文件中的 Kind 字段,指定了這個 API 對象的類型(Type),是一個 Deployment。Deployment 是一個定義多副本應用(即多個副本 Pod)的對象。此外,Deployment 還負責在 Pod 定義發生變化時,對每個副本進行滾動更新(Rolling+Update)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在上面這個 Yaml 文件中,我給它定義的 Pod 副本個數 (spec.replicas)是:2。但,這些 Pod 副本長啥樣子呢?爲此,我們定義了一個 Pod 模版(spec.template),這個模版描述了我想要創建的 Pod 的細節。在上面的例子裏,這個 Pod 裏只有一個容器,這個容器的鏡像(spec.containers.image)是 tomcat=10.0.5,這個容器監聽端口(containerPort)是 80。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 需要注意的是,像這種,使用一種 API 對象(Deployment)管理另一種 API 對象(Pod)的方法,在 Kubernetes 中,叫作“控制器”模式(controller pattern)。在我們的這個 demo 中,Deployment 扮演的正是 Pod 的控制器的角色。而 Pod 是 Kubernetes 世界裏的應用;而一個應用,可以由多個容器(container)組成。爲了讓我們這個 tomcat 服務容器化運行起來,我們只需要執行:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"tom@PK001:~/damon$ kubectl create -f tomcat-deployment.yaml\ndeployment.apps/tomcat-deployment created\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 執行完上面的命令後,你就可以看容器運行情況,此時,只需要執行:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"tom@PK001:~/damon$ kubectl get pod -l app=tomcat\nNAME READY STATUS RESTARTS AGE\ntomcat-deployment-799f46f546-7nxrj 1/1 Running 0 77s\ntomcat-deployment-799f46f546-hp874 0/1 Running 0 77s\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 'kubectl get' 指令的作用,就是從 Kubernetes 裏面獲取(GET)指定的 API 對象。可以看到,在這裏我還加上了一個 -l 參數,即獲取所有匹配 app=nginx 標籤的 Pod。需要注意的是,在命令行中,所有 key-value 格式的參數,都使用“=”而非“:”表示。 從這條指令返回的結果中,我們可以看到現在有兩個 Pod 處於 Running 狀態,也就意味着我們這個 Deployment 所管理的 Pod 都處於預期的狀態。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 此外, 你還可以使用 kubectl describe 命令,查看一個 API 對象的細節,比如:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"tom@PK001:~/damon$ kubectl describe pod tomcat-deployment-799f46f546-7nxrj\nName: tomcat-deployment-799f46f546-7nxrj\nNamespace: default\nPriority: 0\nNode: ca005/10.10.2.5\nStart Time: Thu, 08 Apr 2021 10:41:08 +0800\nLabels: app=tomcat\n pod-template-hash=799f46f546\nAnnotations: cni.projectcalico.org/podIP: 20.162.35.234/32\nStatus: Running\nIP: 20.162.35.234\nControlled By: ReplicaSet/tomcat-deployment-799f46f546\nContainers:\n tomcat:\n Container ID: docker://5a734248525617e950b7ce03ad7a19acd4ffbd71c67aacd9e3ec829d051b46d3\n Image: tomcat:10.0.5\n Image ID: docker-pullable://tomcat@sha256:2637c2c75e488fb3480492ff9b3d1948415151ea9c503a496c243ceb1800cbe4\n Port: 80/TCP\n Host Port: 0/TCP\n State: Running\n Started: Thu, 08 Apr 2021 10:41:58 +0800\n Ready: True\n Restart Count: 0\n Environment: \n Mounts:\n /var/run/secrets/kubernetes.io/serviceaccount from default-token-2ww52 (ro)\nConditions:\n Type Status\n Initialized True\n Ready True\n ContainersReady True\n PodScheduled True\nVolumes:\n default-token-2ww52:\n Type: Secret (a volume populated by a Secret)\n SecretName: default-token-2ww52\n Optional: false\nQoS Class: BestEffort\nNode-Selectors: \nTolerations: node.kubernetes.io/not-ready:NoExecute for 300s\n node.kubernetes.io/unreachable:NoExecute for 300s\nEvents:\n Type Reason Age From Message\n ---- ------ ---- ---- -------\n Normal Scheduled 4m17s default-scheduler Successfully assigned default/tomcat-deployment-799f46f546-7nxrj to ca005\n Normal Pulling 4m16s kubelet, ca005 Pulling image \"tomcat:10.0.5\"\n Normal Pulled 3m27s kubelet, ca005 Successfully pulled image \"tomcat:10.0.5\"\n Normal Created 3m27s kubelet, ca005 Created container tomcat\n Normal Started 3m27s kubelet, ca005 Started container tomcat\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在 kubectl describe 命令返回的結果中,可以的清楚地看到這個 Pod 的詳細信息,比如它的 IP 地址等等。其中,有一個部分值得你特別關注,它就是 Events(事件)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在 Kubernetes 執行的過程中,對 API 對象的所有重要操作,都會被記錄在這個對象的 Events 裏,並且顯示在 kubectl describe 指令返回的結果中。這些 Events 中的信息很重要,可以排查容器是否運行、正常運行的原因。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你希望升級 tomcat 的版本,那可以直接修改 Yaml 文件:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" spec:\n containers:\n - name: tomcat\n image: tomcat:latest\n ports:\n - containerPort: 80\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"修改完 Yaml 文件後,執行:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubectl apply -f tomcat-deployment.yaml\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 這樣的操作方法,是 Kubernetes“聲明式 API”所推薦的使用方法。也就是說,作爲用戶,你不必關心當前的操作是創建,還是更新,你執行的命令始終是 kubectl apply,而 Kubernetes 則會根據 YAML 文件的內容變化,自動進行具體的處理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時,可以查看容器內的服務的日誌情況:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"tom@PK001:~/damon$ kubectl logs -f tomcat-deployment-799f46f546-7nxrj\nNOTE: Picked up JDK_JAVA_OPTIONS: --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED\n08-Apr-2021 02:41:59.037 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.0.5\n08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Mar 30 2021 08:19:50 UTC\n08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.0.5.0\n08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux\n08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 4.4.0-116-generic\n08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64\n08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /usr/local/openjdk-11\n08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 11.0.10+9\n08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Oracle Corporation\n08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat\n08-Apr-2021 02:41:59.041 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat\n08-Apr-2021 02:41:59.051 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED\n08-Apr-2021 02:41:59.051 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED\n08-Apr-2021 02:41:59.051 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED\n08-Apr-2021 02:41:59.051 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dignore.endorsed.dirs=\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat\n08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp\n08-Apr-2021 02:41:59.056 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [1.2.27] using APR version [1.6.5].\n08-Apr-2021 02:41:59.056 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent APR capabilities: IPv6 [true], sendfile [true], accept filters [false], random [true], UDS [true].\n08-Apr-2021 02:41:59.059 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 1.1.1d 10 Sep 2019]\n08-Apr-2021 02:41:59.312 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler [\"http-nio-8080\"]\n08-Apr-2021 02:41:59.331 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [441] milliseconds\n08-Apr-2021 02:41:59.369 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]\n08-Apr-2021 02:41:59.370 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.0.5]\n08-Apr-2021 02:41:59.377 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler [\"http-nio-8080\"]\n08-Apr-2021 02:41:59.392 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [61] milliseconds\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第四章 爲何藉助容器助力微服務","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"4.1 微服務的多語言性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 微服務可以說是越來越火了:那麼對於語言場景,從 Java 中的 Spring MVC,到後面的 SOA、Dubbo、Spring Cloud、Spring cloud Alibaba 等。這是單純 Java 語言的微服務的發展史,那麼對於 Python、Go 呢?其實也是有其微服務設計理念的,比如 Golang 的 beego、gin 等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 這麼多的微服務框架,都是獨立、限制於自己的體系,撇開其它技術不說,Java 可以自成生態體系,Golang 亦是。其實微服務不應該受某種語言的限制,各種語言的服務之間應該可以互通:這在雲原生時代設計中,它們都被稱爲 Service。後面會介紹對於不同語言的服務(Service)如何在雲原生中互通。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"4.2 微服務的高可用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 微服務出現後,同樣面臨着一個重要的話題:高可用。所謂","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"高可用","attrs":{}},{"type":"text","text":":英文縮寫 HA(High Availability),是指當某個服務或服務所在節點出現故障時,其對外的功能可以轉移到該服務其他的副本或該服務在其他節點的副本,從而在減少停工時間的前提下,滿足業務的持續性,這兩個或多個服務構成了服務高可用。同時,這種高可用需要考慮到服務的性能壓力,即服務的負載均衡。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們知道對於服務的高可用,或者說服務的負載來說,有很多方式來解決這些問題。比如:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主從方式,其工作原理是:主機處於工作狀態,備機處於監控、準備狀態,當主機出現宕機的情況下,備機可以接管主機的一切工作,等到主機恢復正常後,將會手動或自動的方式將服務切換到主機上運行,數據的一致性通過共享存儲來實現。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"集羣方式,其工作原理是:多臺機器同時運行一個或幾個服務,當其中的某個節點出現宕機時,這時該節點的服務將無法提供業務功能,可以選擇根據一定的機制,將服務請求轉移到該服務所在的其他節點上,這樣可以讓邏輯持續的執行下去,即消除軟件單點故障。這其實就涉及到負載均衡策略。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 對於微服務的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"高可用","attrs":{}},{"type":"text","text":",涉及到的其中一個就是其服務的負載均衡。在微服務中,負載均衡的前提是,同一個服務需要被發現多個,或者說多個副本,這樣才能實現負載均衡以及服務的高可用。那麼,怎麼讓服務被發現呢?我們來看一張圖:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/26/2670441bb586695aa5eea48a910f1447.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上圖中,我們可以看到:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.各微服務先往註冊中心 Eureka 註冊服務。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.各微服務保持與註冊中心心跳。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.註冊中心發現各微服務。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.註冊中心根據配置規則定期獲取心跳,超時即認爲節點無效。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5.根據規則來定期清理無效節點。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 這是基於註冊中心 Eureka 的服務註冊與發現,同樣,基於其他的註冊中心實現原理基本類似。如:Eureka、Zookeeper、Consul、etcd、nacos 等。其實,在雲原生 K8s 中,也存在着服務的註冊與發現,這在後面章節中會講解。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 服務發現後,其實面臨的是一個主要的問題就是應該訪問哪一個?因爲發現了某個服務的多個實例,最終只會訪問其中某一個,這就涉及到服務的負載均衡了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 負載均衡在微服務中是一個很常見的話題,實現負載均衡的插件也越來越多。netflix 開源的 Zuul、Gateway 等等,其實 K8s 中也存在着負載均衡器:kube-dns、kube-proxy。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實現服務註冊與發現、負載均衡後,其實高可用還涉及很多:高併發、緩存等。先講講","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"高併發","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"冪等性","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接口代碼的規範性","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"操作 DB 的性能","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讀寫分離操作","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務的橫向擴展","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務的健壯性(緩存、限流、熔災)","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 其中,冪等性:就是說一次和多次請求某一個資源時對於資源本身應該具有同樣的結果(網絡超時等問題除外)。也就是說,其任意次執行所產生的效果和返回的結果都是一樣的。這種場景是一個很有效的實現高併發的情景,設想,用戶充值某個會員,在併發情況下,用戶由於誤操作,或者由於網絡、時間等問題導致重試機制的發生時,可能會觸發觸發多次交易的扣費,這樣給用戶一個很不好的體驗。此時,就需要接口冪等性來解決這類問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"冪等性解決方案有以下幾種:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"token 機制","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接口邏輯實現冪等性","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據庫層處理實現冪等性","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" token 機制:數據提交時攜帶 token,token 放到 redis,token 有效時間,提交後臺後校驗 token,同時刪除 token,生成新的 token 並返回。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 接口的冪等性:常見的接口冪等性,是定義接口時,加上參數序列號、來源等,序列號與請求來源聯合唯一索引,這樣可以有效判斷本次請求方與請求的序列號,防止重複的請求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 數據庫處理:DB 層處理有多種方式:1. 悲觀鎖,2. 樂觀鎖,3. 唯一索引、組合唯一索引,4. 分佈式鎖。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 悲觀鎖:所謂悲觀鎖,是指存在危機意識,事先(查詢時)加鎖處理,防止事情發生。如:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"select * from xxx where id= 1 for update\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 樂觀鎖:是指存在樂觀心理,只在更新時加鎖,樂觀鎖通常用 version 版本號來控制如:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"update xxx set name=#name#,version=version+1 where version=#version#\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 也可以通過條件限制,這裏就使用了組合唯一索引來處理,如:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"update xxx set name=#name#,version=version+1 where id=#id# and version=#version#\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 分佈式鎖:通過 redis、zookeeper 來設置分佈式鎖,當插入或更新數據時,獲取分佈式鎖,然後做操作,之後釋放鎖。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 接口的規範性:接口的性能如何,最終還是跟接口的實現邏輯有關,比如代碼規範,邏輯實現等,尤其是業務邏輯複雜的情況下,這點需要注意的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 操作 DB:對於業務的持久層,用的比較多的就是 mybatis、hibernate,還有可能是 JPA,無論是哪個,最終都是通過工廠類注入 bean,最後執行 SQL 來操作 DB。所以這裏尤爲重要的是 SQL 的寫法,SQL 的優化決定着操作 DB 的時間以及效果,如果寫得不好的話,則會導致死循環,或死鎖,或內存溢出。另外測試時,使用真實、規範的數據進行測試,並在測試時不要侷限於相同的數據,最後就是併發壓測了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 讀寫分離:當服務足夠多,數據足夠多時,有可能讀與寫的佔比爲:10:1,此時讀寫應該分離,這樣可以有效減少因爲讀的頻繁操作導致的寫的性能下降。常見的讀寫分離的方法有:採用 mycat 中間件方式、amoeba 直接實現讀寫分離、手動修改 mysql 操作類直接實現讀寫分離和隨機實現的負載均衡,權限獨立分配、mysql-proxy(還是測試版本,時間消耗有點高)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 服務的橫向擴展:對於服務的請求越來越多時,此時需要對服務進行多節點部署,這樣減少單機帶來的服務負載壓力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 服務的健壯性:服務的健壯性包括緩存、限流、熔災。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 對於緩存,大家都知道,有常見的許多中間件如:redis、kafka、RabbitMq、zookeeper。對於一些 session 等常用 redis 來緩存、共享。對於一些大一點的數據如果嫌棄加載慢,也可以採用緩存機制來解決。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 什麼叫限流呢?很好理解,就是限制節點的流量,限制服務的請求數。那麼如何做到限流呢?常用的限流算法比如有計數器算法、令牌桶算法、漏桶算法。有幾種方式:利用 springcloud 組件 zuul 來對請求進行限流,主要是通過谷歌提供的 RateLimiter 結合一些限流算法來限流比較常用。利用 redis 同樣可以做限流算法的,甚至可以利用 nginx 直接作計數限流,可以對請求速率進行限制、對每個 ip 連接數量進行限制、對每個服務的連接數量進行限制。如:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"#對請求速率進行限制\nlimit_req_zone $binary_remote_addr zone=req_one:20m rate=12r/s;\nlimit_conn_zone $binary_remote_addr zone=addr:10m;\n#對每個ip連接數量進行限制\nlimit_conn_zone $server_name zone=perserver:20m;\n#對每個服務的連接數量進行限制\nserver{\n listen 80;\n location / {\n proxy_pass http://ip:port;\n limit_req zone=req_one burst= 80 nodelay;\n limit_conn addr 20;\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 其實在 Springboot2.x 中,推出自己的 Spring-Cloud-Gateway 來作網關,同時 Spring-Cloud-Gateway 中提供了基於 Redis 的實現來達到限流的目的。對於其它框架,都有加入限流的插件功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 對於熔災,或者說熔斷,這個在實際的業務當中是很有必要的。比如:用戶在某一商城秒殺某一件物品,或在某米商城上搶購某一部手機,在準點搶購時,發現人很多,請求很多,這時,主要是需要有限流機制,同時也需要有熔災(熔斷),給用戶留下一個很好的體驗的感覺。當用戶在點擊搶購按鈕後,如果當前的請求數很多,需要用戶等待,這是需要給一個友好的界面讓用戶去等待,而不是直接給用戶提示請求失敗,或者報異常,這樣的紅色拋出是一個非常不好的事情,用戶可能會罵街的,下次也不會逛了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Spring-Cloud-Gateway 作網關時,過濾器時使用 HystrixGatewayFilterFactory 來創建一個 Filter 實現基於 Route 級別的熔斷功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 對於緩存問題,隨着系統用戶的越來愈多,所有的服務壓力也會指數型遞增,這時候緩存是一個很好的減輕服務壓力的方式。這樣可以有效緩衝請求對服務的負載壓力。常見的緩存可能是 Redis、MQ(RabbitMQ、RocketMQ)、Kafka、ZooKeeper 等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Redis 一般主要做 session 或用戶信息的緩存,實現多機中 session 的共享。也會用來作分佈式鎖,在分佈式高併發下實現鎖的功能,例如實現秒殺、搶單等功能。還會被用作一些訂單信息的緩存,防止大量的訂單信息被積壓而導致服務器的負載很高。總之,Redis 常被用來作爲一種緩衝劑使用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" MQ 常見的有 RabbitMQ、RocketMQ、ActiveMQ、Kafka 等,以下是各種之間的對比:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ZooKeeper 也是經常會存儲海量數據,例如 Hadoop 中,在使用 YARN 作資源調度時,採用 ZooKeeper 來存儲海量的狀態機狀態以及任務的信息(包括歷史信息)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"4.3 微服務的複雜性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 說起微服務,使用 DDD 劃分微服務的好處的時候,經常會說 DDD 能夠讓相關的業務邏輯更加內聚,並且降低服務之間的耦合性,從而最終實現達到降解系統的複雜性。但是在這裏,不論是高內聚,低耦合,甚至我們經常說的系統複雜性,我們有沒有一個客觀的可以量化的指標來衡量這些概念。由於無法量化,所以就沒法度量,這樣當團隊在討論一個系統是否複雜,有多複雜這些問題的時候,就很容易陷入各種主觀直覺的爭論中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 互聯網時代,這些巨多的系統在細節上不一樣,但是如果從抽象層面來看有很多共性,一個微服務系統由很多個微服務組成,這些微服務的行爲產生出複雜的行爲模式。整個微服務系統會通過 API 利用內部或外部的信號,同時在系統內部也是通過服務之間的接口進行信息傳遞。一個微服務系統並不是靜態的,而是會不斷適應業務變化,改變系統的 API 或者系統內部的組織方式來增加生存的機會。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 微服務的複雜,不僅僅在於其系統本身,還需要考慮的是:高可用、服務自治、服務併發、服務限流、熔災等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 服務的高可用在上一節中已經說明了,服務自治,目的其實是在對其修改時對其他部分造成儘可能小的影響;自治服務運營時也不會對其他服務的功能造成影響。服務幾乎總是要依賴其他服務提供的數據。例如,網上商城都有一個購物車微服務,一些其他服務必須能向購物車添加商品,還必須能訪問購物車內的商品並下單和配送。現在問題是,如何在保持服務儘可能自治的前提下實現對接。那需要遵循一定的模式:交互、信息傳遞。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 交互模式:Request-Reply 還是 Publish-Subscribe","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Request-Reply(請求-應答)意味着一個服務處理信息的特定請求或者執行一些動作並返回一個應答。發起調用的服務需要知道去哪兒請求以及請求些什麼?這種模式仍然可以被實現爲異步執行,並且你還可以做一些抽象使服務調用方不需要知道被調用服務的物理地址,不能逃避的一點是服務必須明確的要求一個特定的信息和功能(或者執行動作)並等待應答。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Publish-Subscribe(發佈-訂閱) 這種模式下的服務將自己註冊爲對特定的信息感興趣,或者能夠處理特定的請求,相關的信息和請求將被交付給它,並且它可以決定怎麼處理這些信息和請求。本文假定有一些中間件能夠處理交付或者發佈消息給訂閱這些消息服務。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 信息傳遞:Events 還是 Queries/Commands","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Events(事件)是沒有爭議的事實,比如訂單號 123 的訂單已經創建,事件只陳述發生了什麼事,不描述這樣一個事件會導致什麼事情發生。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Queries/Commands(查詢/命令)兩者都傳達了什麼事情會發生,查詢是對信息的特定請求,命令是要求一個服務執行一些動作的特定請求。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上面的四種即可作爲微服務間對接的四種方式:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"REQUEST-REPLY WITH EVENTS","attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"REQUEST-REPLY WITH COMMANDS/QUERIES","attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"PUBLISH-SUBSCRIBE WITH EVENTS","attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"PUBLISH-SUBSCRIBE WITH COMMANDS/QUERIES","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REQUEST-REPLY WITH EVENTS,在這種模式下,一個服務請求另一個導致事件發生的特定服務,這意味着這兩種服務之間有很強的依賴。配送服務必須知道要連接那個服務來獲得訂單相關的事件,這也導致了運行時依賴,因爲配送服務只有在訂單服務可用的時候才能配送新訂單。","attrs":{}}]}]}],"attrs":{}},{"type":"blockquote","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然配送服務只接收事件,它基於事件裏的信息自己決定何時一個訂單可以被配送,訂單服務不需要知道配送服務的任何信息,它只是簡單的提供事件表明當其他服務請求時訂單進行怎樣的處理,把響應事件的職責完全交給請求事件的服務。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REQUEST-REPLY WITH COMMANDS/QUERIES,如:訂單服務將請求配送服務來配送一個訂單,這意味着強烈的依賴,因爲訂單服務明確的請求一個特定的服務來處理配送,現在訂單服務必須決定何時一個訂單準備好配送,它意識到配送服務的存在,甚至知道怎樣與配送服務交互,在訂單配送前需要考慮是否有其他因素關聯到訂單(比如客戶信用卡狀態),訂單服務在請求配送服務來配送訂單前也需要考慮這一點。現在業務處理被混到了架構裏,因此架構不能被簡單的修改。這也是運行時依賴,因爲訂單服務必須確保配送請求成功交付給了配送服務。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PUBLISH-SUBSCRIBE WITH EVENTS,配送服務註冊自己對訂單相關的事件感興趣,註冊後,配送服務會收到訂單的所有事件而不需要關心訂單事件的來源,這是對訂單事件來源的鬆散耦合,配送服務需要保留接收到事件的副本,這樣就可以決定何時訂單準備好配送。訂單服務需要對配送無關,如果多個服務提供包含配送服務需要的相關數據的訂單相關事件,配送服務應該不可識別,如果一個提供訂單事件的服務宕機,配送服務也應該不知道,只是收到的事件變少了,配送服務不會因此阻塞。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PUBLISH-SUBSCRIBE WITH COMMANDS/QUERIES,配送服務自己註冊爲能夠配送貨物的服務,接受所有想要配送貨物的命令,配送服務不需要意識到配送命令的來源,同樣訂單服務業不知道那些服務將處理配送,在這個意義上說,他們是鬆散耦合的,不過,訂單服務知道既然發送了配送命令,訂單必須被配送的事實,這確實讓耦合更強了。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 兩種 Request-Reply 模式都意味着兩個服務的運行時耦合和強耦合,兩種 Command/Queries 模式意味着一個服務知道另一個服務應該做的事,這也意味着強耦合,但是這一次在功能級別。留下了一個選項:PUBLISH-SUBSCRIBE WITH EVENTS,這種情況下,兩種服務從運行時和功能的角度都沒有意識到彼此的存在。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"但是,我們需要考慮更多的因素,一直使用這種方式交互是有代價的,例如,數據被複制、事件丟失、事件驅動的架構增加更多基礎設施的需求、額外的延遲。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"第二部分 原理與應用","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第五章 Kubernetes 介紹","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"5.1 Kubernetes 的基本概念與特性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在前面的章節中介紹了 Kubernetes 的由來,那麼 Kubernetes 到底是幹嘛的呢?這就涉及到 Kubernetes 的定義以及其特性,本節來描述下 Kubernetes 的特性以及應用場景。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Kubernetes,簡稱 K8s,是把 8 代替了 8 個字符“ubernete”而成的縮寫。K8s 是一個一個開源的,用於管理雲平臺中多個主機上的容器化的應用編排工具。它的目標是讓部署容器化的應用簡單且高效,Kubernetes 提供了應用部署,規劃,更新,維護的一種機制。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Kubernetes 是 Google 開源的一個容器編排引擎,支持自動化部署、大規模可伸縮、應用容器化管理。在生產環境中部署一個應用程序時,通常要部署該應用的多個實例以便對應用請求進行負載均衡。在 Kubernetes 中,我們可以創建多個容器,每個容器裏面運行一個應用實例,然後通過內置的負載均衡策略,實現對這一組應用實例的管理、發現、訪問,而這些細節都不需要運維人員去進行復雜的手工配置和處理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"特性:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可移植: 支持公有云,私有云,混合雲","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可擴展: 模塊化,插件化,可掛載,可組合","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自動化: 自動部署,自動重啓,自動複製,自動彈性伸縮","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 可移植,意味着可以穿梭任何系統,不受系統的限制,也不受任何語言的限制,支持任何的其他的服務形式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 可擴展,是指 K8s 的各個模塊之間是解耦合的,可以增加插件來豐富其功能,也可以替換其組件來達到想要的效果。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 自動部署和回滾,K8s 採用滾動更新策略更新應用,一次更新一個 Pod,而不是同時刪除所有 Pod,如果更新過程中出現問題,將回滾更改,確保升級不受影響業務。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 彈性伸縮,使用命令、UI 或者基於 CPU 使用情況自動快速擴容和縮容應用程序實例,保證應用業務高峯併發時的高可用性;業務低峯時回收資源,以最小成本運行服務。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 自動重啓,是說在集羣節點宕機、機器重啓後,其 K8s 集羣具有自動重啓的功能,同時,會拉起集羣中所有的應用服務,這就是其編排能力的一個體現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 自動複製,是指所有相關的數據,可以被備份到 etcd 或其它插件中,以便 K8s 可以通過 controllers、scheduler 來很好的編排。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 可掛載,是指存儲編排,掛載外部存儲系統,無論是來自本地存儲,公有云(如 AWS),還是網絡存儲(如 NFS、GlusterFS、Ceph)都作爲集羣資源的一部分使用,極大提高存儲使用靈活性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 自我修復,在節點故障時重新啓動失敗的容器,替換和重新部署,保證預期的副本數量;殺死健康檢查失敗的容器,並且在未準備好之前不會處理客戶端請求,確保線上服務不中斷。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"5.2 部署 Kubernetes 集羣","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 如果想要了解 K8s 的一些特性,並且將其應運的很好,那就需要動手部署一個 K8s 集羣。下面講解下 K8s 集羣部署流程。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"單機版 K8s","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"環境:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Ubuntu 16.04","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GPU 驅動 418.56","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Docker 18.06","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"K8s 1.13.5","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上的環境,針對的是高版本的 K8s,而且 Docker 的版本必須要注意。另外 GPU 驅動的話,如果大家是非 GPU 機器的話,可以考慮不用。如果含有 GPU 機器的話,需要安裝驅動,並且驅動版本不能過低喔。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"設置環境","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在配置環境前,首先備份一下源配置:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"cp /etc/apt/sources.list /etc/apt/sources.list.cp\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後我們重新寫一份配置,編輯內容,加上阿里源:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"vim /etc/apt/sources.list\n\ndeb-src http://archive.ubuntu.com/ubuntu xenial main restricted\ndeb http://mirrors.aliyun.com/ubuntu/ xenial main restricted\ndeb-src http://mirrors.aliyun.com/ubuntu/ xenial main restricted multiverse universe\ndeb http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted\ndeb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted multiverse universe\ndeb http://mirrors.aliyun.com/ubuntu/ xenial universe\ndeb http://mirrors.aliyun.com/ubuntu/ xenial-updates universe\ndeb http://mirrors.aliyun.com/ubuntu/ xenial multiverse\ndeb http://mirrors.aliyun.com/ubuntu/ xenial-updates multiverse\ndeb http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse\ndeb-src http://mirrors.aliyun.com/ubuntu/xenial-backports main restricted universe multiverse\ndeb http://archive.canonical.com/ubuntu xenial partner\ndeb-src http://archive.canonical.com/ubuntu xenial partner\ndeb http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted\ndeb-src http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted multiverse universe\ndeb http://mirrors.aliyun.com/ubuntu/ xenial-security universe\ndeb http://mirrors.aliyun.com/ubuntu/ xenial-security multiverse\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"添加好後,可以執行如下命令,更新一下源:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"apt-get update\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果出現問題,可以執行如下命令來自動修復安裝出現 broken 的 package:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"apt --fix-broken install\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行升級命令時,注意:對於 GPU 機器可不執行,否則可能升級 GPU 驅動導致問題。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"apt-get upgrade\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於 K8s 安裝要求,需要關閉防火牆:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"ufw disable\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"安裝 SELinux:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"apt install selinux-utils\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SELinux 防火牆配置:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"setenforce 0\n\nvim/etc/selinux/conifg\n\nSELINUX=disabled\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設置網絡,將橋接的 IPV4 流量傳遞到 iptables 的鏈:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"tee /etc/sysctl.d/k8s.conf < --delete-local-data --force --ignore-daemonsets\n\nkubectl delete node \n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"清空 init 配置在需要刪除的節點上執行(注意,當執行 init 或者 join 後出現任何錯誤,都可以使用此命令返回):","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubeadm reset\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"查問題","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"初始化後出現問題,可以通過以下命令先查看其容器狀態以及網絡情況:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"sudo docker ps -a | grep kube | grep -v pause\n\nsudo docker logs CONTAINERID\n\nsudo docker images && systemctl status -l kubelet\n\nnetstat -nlpt\n\nkubectl describe ep kubernetes\n\nkubectl describe svc kubernetes\n\nkubectl get svc kubernetes\n\nkubectl get ep\n\nnetstat -nlpt | grep apiser\n\nvi /var/log/syslog\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"給當前用戶配置 K8s apiserver 訪問公鑰","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"sudo mkdir -p $HOME/.kube\n\nsudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\n\nsudo chown $(id -u):$(id -g) $HOME/.kube/config\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"網絡插件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面的步驟後,如果查看節點情況:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubectl get nodes\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"查看 nodes 狀態信息,看到 node 節點的狀態爲 NotReady,這是因爲缺少容器網絡的配置。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來需要部署網絡插件:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubectl apply -f https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/rbac-kdd.yaml\n\nwget https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/kubernetes-datastore/calico-networking/1.7/calico.yaml\n\nvi calico.yaml\n\n- name: CALICO_IPV4POOL_IPIP\n value:\"off\"\n- name: CALICO_IPV4POOL_CIDR\n value: \"10.244.0.0/16\n\nkubectl apply -f calico.yaml\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單機下允許 master 節點部署 pod 命令如下:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubectl taint nodes --all node-role.kubernetes.io/master-\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"禁止 master 部署 pod:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubectl taint nodes k8s node-role.kubernetes.io/master=true:NoSchedule\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上單機版部署結束,如果你的項目中,交付的是軟硬件結合的一體機,那麼到此就結束了。記得單機下要允許 master 節點部署喲!","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"K8s 集羣版實戰","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上面部署的機器爲例,作爲 master 節點,我們備份一些配置到節點機器,執行:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"scp /etc/kubernetes/admin.conf $nodeUser@$nodeIp:/home/$nodeUser\n\nscp /etc/kubernetes/pki/etcd/* $nodeUser@$nodeIp:/home/$nodeUser/etcd\n\nkubeadm token generate\n\nkubeadm token create $token_name --print-join-command --ttl=0\n\nkubeadm join $masterIP:6443 --token $token_name --discovery-token-ca-cert-hash $hash\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意,這個 token 24 小時後會失效,如果後面有其他節點要加入的話,處理方法:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubeadm token generate\n\nkubeadm token list\n\nopenssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後拿到 token 和一個 sha256 密鑰後執行下面即可加入集羣:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubeadm join $masterIP:6443 --token $token_name --discovery-token-ca-cert-hash $hash\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Node 機器執行時,如果需要 Cuda,可以參考以下資料:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#ubuntu-installation","title":null,"type":null},"content":[{"type":"text","text":"https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#ubuntu-installation","attrs":{}}],"marks":[{"type":"strong"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://blog.csdn.net/u012235003/article/details/54575758","title":null,"type":null},"content":[{"type":"text","text":"https://blog.csdn.net/u012235003/article/details/54575758","attrs":{}}],"marks":[{"type":"strong"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://blog.csdn.net/qq_39670011/article/details/90404111","title":null,"type":null},"content":[{"type":"text","text":"https://blog.csdn.net/qq_39670011/article/details/90404111","attrs":{}}],"marks":[{"type":"strong"}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正式執行:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"vim /etc/modprobe.d/blacklist-nouveau.conf\n\nblacklist nouveau\noptions nouveau modeset=0\nupdate-initramfs -u\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重啓 Ubuntu 查看是否禁用成功:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"lsmod | grep nouveau\n\napt-get remove --purge nvidia*\n\nhttps://developer.nvidia.com/cuda-downloads\n\nsudo apt-get install freeglut3-dev build-essential libx11-dev libxmu-dev libxi-dev libgl1-mesa-glx libglu1-mesa libglu1-mesa-dev\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"安裝 Cuda:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"accept\n\nselect \"Install\" / Enter\n\nselect \"Yes\"\n\nsh cuda_10.1.168_418.67_linux.run\n\necho 'export PATH=/usr/local/cuda-10.1/bin:$PATH' >> ~/.bashrc\n\necho 'export PATH=/usr/local/cuda-10.1/NsightCompute-2019.3:$PATH' >> ~/.bashrc\n\necho 'export LD_LIBRARY_PATH=/usr/local/cuda-10.1/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc\n\nsource ~/.bashrc\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重啓機器,檢查 Cuda 是否安裝成功。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"查看是否有 nvidia* 的設備:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"cd /dev && ls -al\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果沒有,創建一個 nv.sh:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"vi nv.sh\n#!/bin/bash /sbin/modprobe nvidia\nif [ \"$?\" -eq 0 ];\nthen\nNVDEVS=`lspci |\n grep -i NVIDIA\n`\nN3D=`\necho\n\"$NVDEVS\"\n| grep \"3D controller\" |\n wc -l\n`\nNVGA=`\necho\n\"$NVDEVS\"\n| grep \"VGA compatible controller\" |\n wc -l\n`\nN=`\nexpr $N3D + $NVGA -\n1\n`\nfor i in `\nseq\n0\n $N\n`; do\n mknod -m 666 /dev/nvidia$i c 195 $i\ndone\n mknod -m 666 /dev/nvidiactl c 195 255\nelse\n exit 1\nfi\n\nchmod +x nv.sh && bash nv.sh\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再次重啓機器查看 Cuda 版本:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"nvcc -V\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編譯:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"cd /usr/local/cuda-10.1/samples && make\n\ncd /usr/local/cuda-10.1/samples/bin/x86_64/linux/release ./deviceQuery\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上如果輸出“Result = PASS”,代表 Cuda 安裝成功。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"安裝 nvdocker:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"vim /etc/docker/daemon.json\n{\n\"runtimes\":{\n \"nvidia\":{\n \"path\":\"nvidia-container-runtime\",\n \"runtimeArgs\":[]\n }\n},\n\"registry-mirrors\":[\"https://registry.docker-cn.com\"],\n\"storage-driver\":\"overlay2\",\n\"default-runtime\":\"nvidia\",\n\"log-driver\":\"json-file\",\n\"log-opts\":{\n \"max-size\":\"100m\"\n},\n\"exec-opts\": [\"native.cgroupdriver=systemd\"],\n\"insecure-registries\": [$harborRgistry],\n\"live-restore\": true\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重啓 Docker:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"sudo systemctl daemon-reload && sudo systemctl restart docker && docker info\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"檢查 nvidia-docker 安裝是否成功:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"docker run --runtime=nvidia --rm nvidia/cuda:9.0-base nvidia-smi\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在節點機器進入 su 模式:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"su $nodeUser\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"給當前節點用戶配置 K8s apiserver 訪問公鑰:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"mkdir -p $HOME/.kube\n\ncp -i admin.conf $HOME/.kube/config\n\nchown $(id -u):$(id -g) $HOME/.kube/config\n\nmkdir -p $HOME/etcd\n\nsudo rm -rf /etc/kubernetes\n\nsudo mkdir -p /etc/kubernetes/pki/etcd\n\nsudo cp /home/$nodeUser/etcd/* /etc/kubernetes/pki/etcd\n\nsudo kubeadm join $masterIP:6443 --token $token_name --discovery-token-ca-cert-hash $hash\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"sudo kubeadm join 192.168.8.116:6443 --token vyi4ga.foyxqr2iz9i391q3 --discovery-token-ca-cert-hash sha256:929143bcdaa3e23c6faf20bc51ef6a57df02edf9df86cedf200320a9b4d3220a\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"檢查 node 是否加入 master:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubectl get node\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 到此,K8s 單機、集羣版部署流程就結束了,後面我們會將 K8s 與微服務結合一起來分析 K8s 的組件的特性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"5.3 Kubernetes 的組件及負載均衡","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上面介紹了 K8s 的由來、概念以及特性,接下來,我們看看 K8s 的組件。K8s 的組件分爲 Master 組件、Node 組件。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"Master 組件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、kube-apiserver","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Kubernetes API 服務器的主要實現是 kube-apiserver。 kube-apiserver 設計上考慮了水平伸縮,也就是說,它可通過部署多個實例進行伸縮。 你可以運行 kube-apiserver 的多個實例,並在這些實例之間平衡流量。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、etcd","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" etcd 是兼具一致性和高可用性的鍵值數據庫,可以作爲保存 Kubernetes 所有集羣數據的後臺數據庫。通常,集羣的 etcd 數據庫通常需要有個備份計劃。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、kube-controller-manager","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在主節點上運行控制器的組件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、cloud-controller-manager","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" cloud-controller-manager 僅運行特定於雲平臺的控制迴路。 如果你在自己的環境中運行 Kubernetes,或者在本地計算機中運行學習環境, 所部署的環境中不需要雲控制器管理器。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5、kube-scheduler","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 控制平面組件,負責監視新創建的、未指定運行節點(node)的 Pods,選擇節點讓 Pod 在上面運行。調度決策考慮的因素包括單個 Pod 和 Pod 集合的資源需求、硬件/軟件/策略約束、親和性和反親和性規範、數據位置、工作負載間的干擾和最後時限。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"Node 組件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 節點組件在每個節點上運行,維護運行的 Pod 並提供 Kubernetes 運行環境。如果 Master 也被設置允許爲工作節點,則節點組件同樣運行在 Master 上。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、kubelet","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 一個在集羣中每個節點(node)上運行的代理。 它保證容器(containers)都 運行在 Pod 中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、kube-proxy","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" kube-proxy 是集羣中每個節點上運行的網絡代理, 實現 Kubernetes 服務(Service) 概念的一部分。kube-proxy 維護節點上的網絡規則。這些網絡規則允許從集羣內部或外部的網絡會話與 Pod 進行網絡通信。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.kube-proxy 主要是處理集羣外部通過 nodePort 訪問集羣內服務,通過 iptables 規則,解析 cluterIP 到 PodIp 的過程,並提供服務的負載均衡能力。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.kube-proxy 還可以提供集羣內部服務間通過 clusterIP 訪問,也會經過 kube-proxy 負責轉發。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.kube-dns 主要在 Pod 內通過 serviceName 訪問其他服務,找到服務對應的 clusterIP 的關係,和一些基本的域名解析功能。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.kube-dns 是和 kube-proxy 協同工作的,前者通過 servicename 找到指定 clusterIP,後者完成通過 clusterIP 到 PodIP 的過程。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏,K8s 通過虛擬出一個集羣 IP,利用 kube-proxy 爲 service 提供 cluster 內的服務發現和負載均衡。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、Container Runtime","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 容器運行環境是負責運行容器的軟件。Kubernetes 支持多個容器運行環境: Docker、 containerd、CRI-O 以及任何實現 Kubernetes CRI (容器運行環境接口)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、插件 Addons","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 插件使用 Kubernetes 資源(DaemonSet、 Deployment 等)實現集羣功能。 因爲這些插件提供集羣級別的功能,插件中命名空間域的資源屬於 kube-system 命名空間。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5、DNS","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 儘管其他插件都並非嚴格意義上的必需組件,但幾乎所有 Kubernetes 集羣都應該 有集羣 DNS, 因爲很多示例都需要 DNS 服務。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"6、Dashboard","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Dashboard 是 Kubernetes 集羣的通用的、基於 Web 的用戶界面。 它使用戶可以管理集羣中運行的應用程序以及集羣本身並進行故障排除。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b5/b5662d4d56083e5a500dd73eb9d1244f.png","alt":"","title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"K8s如何實現服務註冊與發現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上面介紹了K8s的各種組件,接下來,我們看看K8s是如何實現服務的註冊與發現,然後如何做到服務的轉發、實現負載均衡的能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 服務在K8s中,也定義了一種資源:Service,Service,顧名思義是一個服務,什麼樣的服務呢?它是定義了一個服務的多種 pod 的邏輯合集以及一種訪問 pod 的策略。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"service 的類型有四種:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ExternalName:創建一個 DNS 別名指向 service name,這樣可以防止 service name 發生變化,但需要配合 DNS 插件使用。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ClusterIP:默認的類型,用於爲集羣內 Pod 訪問時,提供的固定訪問地址,默認是自動分配地址,可使用 ClusterIP 關鍵字指定固定 IP。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"NodePort:基於 ClusterIp,用於爲集羣外部訪問 Service 後面 Pod 提供訪問接入端口。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LoadBalancer:它是基於 NodePort。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面講的 Service,我們可以看到一種場景:所有的微服務在一個局域網內,或者說在一個 K8s 集羣下,那麼可以通過 Service 用於集羣內 Pod 的訪問,這就是 Service 默認的一種類型 ClusterIP,ClusterIP 這種的默認會自動分配地址。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 那麼問題來了,既然可以通過上面的 ClusterIp 來實現集羣內部的服務訪問,那麼如何註冊服務呢?其實 K8s 並沒有引入任何的註冊中心,使用的就是 K8s 的 kube-dns 組件。然後 K8s 將 Service 的名稱當做域名註冊到 kube-dns 中,每一個Service在kube-dns中都有一條DNS記錄,同時,如果有服務的ip更換,kube-dns自動會同步,對服務來說是不需要改動的。通過 Service 的名稱就可以訪問其提供的服務。那麼問題又來了,如果一個服務的 pod 對應有多個,那麼如何實現 LB?其實,最終通過 kube-proxy,實現負載均衡。也就是說kube-dns通過 servicename 找到指定 clusterIP,kube-proxy完成通過 clusterIP 到 PodIP 的過程。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說到這,我們來看下 Service 的服務發現與負載均衡的策略,Service 負載分發策略有兩種:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RoundRobin:輪詢模式,即輪詢將請求轉發到後端的各個 pod 上,其爲默認模式。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SessionAffinity:基於客戶端 IP 地址進行會話保持的模式,類似 IP Hash 的方式,來實現服務的負載均衡。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面寫一個很簡單的例子:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"apiVersion: v1\nkind: Service\nmetadata:\n name: cas-server-service\n namespace: default\nspec:\n ports:\n - name: cas-server01\n port: 2000\n targetPort: cas-server01\n selector:\n app: cas-server\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到執行 kubectl apply -f service.yaml 後:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"root@ubuntu:~$ kubectl get svc\nNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\nadmin-web-service ClusterIP 10.16.129.24 2001/TCP 84d\ncas-server-service ClusterIP 10.16.230.167 2000/TCP 67d\ncloud-admin-service-service ClusterIP 10.16.25.178 1001/TCP 190d\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣,我們可以看到默認的類型是 ClusterIP,用於爲集羣內 Pod 訪問時,可以先通過域名來解析到多個服務地址信息,然後再通過 LB 策略來選擇其中一個作爲請求的對象。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"K8s 如何處理微服務中常用的配置","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 接下來我們看看微服務中場景的居多配置該如何來利用K8s實現統一管理。其實,在K8s中,定義了一種資源:ConfigMap,我們來看看這種資源。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ConfigMap,看到這個名字可以理解:它是用於保存配置信息的鍵值對,可以用來保存單個屬性,也可以保存配置文件。對於一些非敏感的信息,比如應用的配置信息,則可以使用 ConfigMap。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"創建一個 ConfigMap 有多種方式如下。","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"key-value 字符串創建","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kubectl create configmap test-config --from-literal=baseDir=/usr\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上面的命令創建了一個名爲 test-config,擁有一條 key 爲 baseDir,value 爲 \"/usr\" 的鍵值對數據。","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"2","normalizeStart":"2"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"根據 yml 描述文件創建","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test-config\ndata:\n baseDir: /usr\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也可以這樣,創建一個 yml 文件,選擇不同的環境配置不同的信息:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kind: ConfigMap\napiVersion: v1\nmetadata:\n name: cas-server\ndata:\n application.yaml: |-\n greeting:\n message: Say Hello to the World\n ---\n spring:\n profiles: dev\n greeting:\n message: Say Hello to the Dev\n spring:\n profiles: test\n greeting:\n message: Say Hello to the Test\n spring:\n profiles: prod\n greeting:\n message: Say Hello to the Prod\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意點:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ConfigMap 必須在 Pod 使用其之前創建。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Pod 只能使用同一個命名空間的 ConfigMap。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 當然,還有其他更多用途,具體可以參考官網(https://kubernetes.io/zh/docs/concepts/configuration/configmap/)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面講述了幾種創建ConfigMap的方式,其中有一種在 Java 中常常用到:通過創建 yml 文件來實現配置管理。比如:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"kind: ConfigMap\napiVersion: v1\nmetadata:\n name: cas-server\ndata:\n application.yaml: |-\n greeting:\n message: Say Hello to the World\n ---\n spring:\n profiles: dev\n greeting:\n message: Say Hello to the Dev\n spring:\n profiles: test\n greeting:\n message: Say Hello to the Test\n spring:\n profiles: prod\n greeting:\n message: Say Hello to the Prod\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 這樣,當我們啓動容器時,通過 --spring.profiles.active=dev 來指定當前容器的活躍環境,即可獲取 ConfigMap 中對應的配置。是不是感覺跟 Java 中的 Config 配置多個環境的配置有點類似呢?但是,我們不用那麼複雜,這些統統可以交給 K8s 來處理。只需要你啓動這一命令即可,是不是很簡單?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第六章 爲什麼選擇 Kubernetes","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"6.1 Kubernetes 與微服務的天生絕配","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 其實,爲什麼我們需要 K8s,它到底能做什麼呢?容器是打包和運行應用程序的好方式。在生產環境中,你需要管理運行應用程序的容器,並確保不會停機。 例如,如果一個容器發生故障,則需要啓動另一個容器。如果系統處理此行爲,會不會更容易?這就是 Kubernetes 來解決這些問題的方法! Kubernetes 爲你提供了一個可彈性運行分佈式系統的框架。 Kubernetes 會滿足你的擴展要求、故障轉移、部署模式等。 例如,Kubernetes 可以輕鬆管理系統的 Canary 部署。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Kubernetes 會提供:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務發現和負載均衡,Kubernetes 可以使用 DNS 名稱或自己的 IP 地址公開容器,如果進入容器的流量很大, Kubernetes 可以負載均衡並分配網絡流量,從而使部署穩定。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自動部署和回滾,你可以使用 Kubernetes 描述已部署容器的所需狀態,它可以以受控的速率將實際狀態更改爲期望狀態。例如,你可以自動化 Kubernetes 來爲你的部署創建新容器, 刪除現有容器並將它們的所有資源用於新容器。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自我修復,Kubernetes 重新啓動失敗的容器、替換容器、殺死不響應用戶定義的運行狀況檢查的容器,並且在準備好服務之前不將其通告給客戶端。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"存儲編排,Kubernetes 允許你自動掛載你選擇的存儲系統,例如本地存儲、公共雲提供商等。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自動完成裝箱計算,Kubernetes 允許你指定每個容器所需 CPU 和內存(RAM)。 當容器指定了資源請求時,Kubernetes 可以做出更好的決策來管理容器的資源。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"密鑰與配置管理,Kubernetes 允許你存儲和管理敏感信息,例如密碼、OAuth 令牌和 ssh 密鑰。 你可以在不重建容器鏡像的情況下部署和更新密鑰和應用程序配置,也無需在堆棧配置中暴露密鑰。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 那麼對於 K8s 提供的這些功能,其實對於微服務來講,都是很好的一個平臺提供。換句話說,對於微服務來說,如果使用 K8s 的話,可以不用考慮語言上的限制,更不用考慮各種開發語言的框架的限制;對於各種語言來說,在前面也介紹過,都有很多不同的框架,那如果運用這些框架時,就需要考慮不同服務之間如果屬於不同的語言,那麼該如何來實現微服務的架構呢?從這一角度來分析,微服務與 K8s 屬於天作之合,它們的結合可以說是天衣無縫、完美至極。在後面章節中,將會介紹它們的天衣無縫:自治與無縫遷移。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"6.2 基於 Kubernetes 集羣的服務治理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 其實,做微服務架構設計,我們希望得到什麼呢?看下圖: ","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5c/5c17301931632913b47422c2262931c4.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 從上面這張圖中可以看到,微服務的解耦、封裝,從而簡化開發人員的開發。調用方便,主要體現在sdk或者說client的提供者很容易被調用,這就體現了K8s的服務註冊與發現。安全性考慮,基於K8s集羣的保障,可以讓微服務們處於一個堡壘中,這樣避免外部的干擾。同時,服務之間直接走內部網絡,可以大大提升性能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 說到這些,其實服務的自動化纔是一個重點,自治能力體現了系統的健壯性。在前面章節中說到了K8s具有自動修復的能力,可以將失敗或出現問題的容器進行重新編排、啓動。容器本身的健康檢查會被監視,當不響應用戶定義的運行檢查的容器就會被殺死。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 同時,K8s提供自動部署能力,可以通過簡單的命令來執行即可發揮K8s的作用。同時,K8s會根據用戶的節點選擇,將pod分配到對應的節點,這樣對於運維人員來說,即使出現節點宕機,pod可以迅速的在其他節點被啓動。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"6.3 基於 Kubernetes 的服務無縫遷移","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在K8s集羣中的服務,如果想要被遷移到其他的機器或其他集羣。在傳統的實現中,可能需要考慮到很多點:服務包的轉移、共享,配置的轉移,數據庫的轉移等等。但對於容器化來說,這些都被打包成image,而這些image可以被上傳到一個倉庫Habor,當需要遷移環境的時候,這些服務的鏡像其實都可以不動,運維人員將要做的是:將開發人員編寫的基於K8s的yaml文件在對應的集羣中進行部署即可。運維人員無需關心任何其他事情,只需要在部署前,將需要的相關配置處理好即可。這將大大減少開發人員、運維的成本,讓他們專注於部署,而不用關心其他的瑣碎的事情。因爲K8s是可以跨平臺、跨系統的。主要存在K8s集羣,服務都可以無縫的進行遷移過去。這也是微服務基於K8s的一個優勢。下圖爲K8s基於Habor的架構圖。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e8/e847a9866cc308fc964fad04cd808c16.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過kubectl命令工具發起資源創建kubectl create -f xxx.yaml","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"k8s處理相關請求後kube-scheduler服務爲pod尋找一個合適的“家”node2並創建pod。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"node2上的kubelet處理相關資源,使用docker拉取相關鏡像並運行。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 從上面的流程操作來看,將微服務從一個K8s集羣,遷移到另一個集羣,其操作是可以無縫對接的,可以同配置、同環境參數的無縫的遷移,這就是雲原生下K8s帶來的優勢。也是需要企業現在一直推薦屬性docker、K8s等技術的一個關鍵性要素。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"第七章 第一個基於 K8s 的多語言微服務架構","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"7.1 基於 K8s 的 Java 微服務","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 前面很多的都是在說K8s爲什麼可以實現配置化,爲什麼可以提供負載均衡能力,接下來,我們舉個電商系統來實現體驗下K8s帶來的效果,手寫Java代碼。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 如下圖,我們簡單的畫了一個系統圖,從客戶端到網關,再到訂單服務、後臺管理系統以及鑑權中心的流程。 ","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/36/36548234c9acc7889fc9fd12fb0866e8.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"認證中心","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 基於Java現在許多比較流行的框架,作者選擇了SpringCloud,因爲很多基礎特性SpringCloud都具備了,接下來我們看如何實現鑑權。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"環境:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Ubuntu 16.04","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Docker 18.06","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"K8s 1.13.5","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"springboot 2.3.8.RELEASE","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"springcloud Hoxton.SR9","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先新建一個Java項目:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/35/35b9ebb2d04125500128008d6f41a50f.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"引入依賴配置:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\n org.springframework.cloud \n spring-cloud-starter-oauth2 \n \n\n\n org.springframework.boot \n spring-boot-actuator \n \n\n\n org.springframework.boot \n spring-boot-actuator-autoconfigure \n \n\n\n org.springframework.cloud \n spring-cloud-starter-kubernetes-config \n \n\n\n org.springframework.boot \n spring-boot-starter-test \n test \n \n\n\n io.jsonwebtoken \n jjwt \n 0.9.0 \n \n\n cn.hutool \n hutool-all \n 4.6.3 \n \n\n\n com.google.guava \n guava \n 19.0 \n \n\n\n org.apache.commons \n commons-lang3 \n \n\n\n commons-collections \n commons-collections \n 3.2.2 \n \n\n\n\n org.mybatis.spring.boot \n mybatis-spring-boot-starter \n 1.1.1 \n \n \n mysql \n mysql-connector-java \n ${mysql.version} \n \n\n\n\n com.alibaba \n druid \n 1.1.3 \n \n\n org.springframework.boot \n spring-boot-starter-data-redis \n \n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"新建服務啓動類:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ef/ef481f8fb27ab610a568214f61f0cb2e.png","alt":"","title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常見的配置文件bootstrap.yml、application.yml,主要是配置服務啓動時,需要加載的參數、配置:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"management:\n endpoint:\n restart:\n enabled: true\n health:\n enabled: true\n info:\n enabled: true\n\nspring:\n application:\n name: cas-server\n cloud:\n kubernetes:\n config:\n sources:\n - name: ${spring.application.name}\n namespace: system-server\n discovery:\n all-namespaces: true\n reload:\n #自動更新配置的開關設置爲打開\n enabled: true\n #更新配置信息的模式:polling是主動拉取,event是事件通知\n mode: polling\n #主動拉取的間隔時間是500毫秒\n period: 500\n \n redis: #redis相關配置\n database: 8\n host: 10.10.3.15 #localhost\n port: 6379\n password: xxxxx #有密碼時設置\n jedis:\n pool:\n max-active: 8\n max-idle: 8\n min-idle: 0\n timeout: 10000ms\n \n http:\n encoding:\n charset: UTF-8\n enabled: true\n force: true\n mvc:\n throw-exception-if-no-handler-found: true\n main:\n allow-bean-definition-overriding: true\n \nlogging:\n path: /data/${spring.application.name}/logs\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 第一個配置中,介紹了該服務的信息,以及springboot2結合K8s的一個特性:自動刷新、加載配置,該模式有兩種:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主動拉取:polling,每隔一定時間拉取,自定義設置。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事件通知,event","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 這裏主要用主動拉取模式。後面就是配置一些插件redis、日誌配置。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 第二個配置文件可以設置一些環境、mybatis配置以及設置http協議的超時:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"spring:\n profiles:\n active: dev\n\nserver:\n port: 2000\n undertow:\n accesslog:\n enabled: false\n pattern: combined\n servlet:\n session:\n timeout: PT120M\n\nclient:\n http:\n request:\n connectTimeout: 8000\n readTimeout: 30000\n \nmybatis:\n mapperLocations: classpath:mapper/*.xml\n typeAliasesPackage: com.damon.*.model\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 由於我們可以設置","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"spring.profiles.active=dev","attrs":{}},{"type":"text","text":",所以可以設置幾個不同環境的文件來設置日誌的級別。當然,你也可以直接在啓動服務時,設置參數:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"-- spring.profiles.active=dev --logging.level.org.springframework.web=INFO --logging.level.com.damon=INFO\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到目前爲止,一些基礎配置都已經寫完了,接下來,我們看看鑑權的核心邏輯了。首先我們來寫一個認證服務器配置:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"package com.damon.config;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.sql.DataSource;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.env.Environment;\nimport org.springframework.security.authentication.AuthenticationManager;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.security.oauth2.config.annotation.builders.ClientDetailsServiceBuilder;\nimport org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder;\nimport org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;\nimport org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;\nimport org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;\nimport org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;\nimport org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;\nimport org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;\nimport org.springframework.security.oauth2.provider.token.TokenEnhancer;\nimport org.springframework.security.oauth2.provider.token.TokenEnhancerChain;\nimport org.springframework.security.oauth2.provider.token.TokenStore;\nimport org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;\n\nimport com.damon.component.JwtTokenEnhancer;\nimport com.damon.login.service.LoginService;\n\n/**\n * \n * 認證服務器配置\n * @author Damon \n * @date 2020年1月13日 下午3:03:30\n *\n */\n@Configuration\n@EnableAuthorizationServer\npublic class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {\n\n @Autowired\n private PasswordEncoder passwordEncoder;\n\n @Autowired\n private AuthenticationManager authenticationManager;\n\n @Autowired\n private LoginService loginService;\n\n @Autowired\n //@Qualifier(\"jwtTokenStore\")\n @Qualifier(\"redisTokenStore\")\n private TokenStore tokenStore;\n /*@Autowired\n private JwtAccessTokenConverter jwtAccessTokenConverter;\n @Autowired\n private JwtTokenEnhancer jwtTokenEnhancer;*/\n \n @Autowired\n private Environment env;\n \n \n @Autowired\n private DataSource dataSource;\n \n @Autowired\n private WebResponseExceptionTranslator userOAuth2WebResponseExceptionTranslator;\n\n /* @Override\n public void configure(AuthorizationServerEndpointsConfigurer endpoints) {\n TokenEnhancerChain enhancerChain = new TokenEnhancerChain();\n List delegates = new ArrayList<>();\n delegates.add(jwtTokenEnhancer); //配置JWT的內容增強器\n delegates.add(jwtAccessTokenConverter);\n enhancerChain.setTokenEnhancers(delegates);\n endpoints.authenticationManager(authenticationManager)//支持 password 模式\n .userDetailsService(loginService)\n .tokenStore(tokenStore) //配置令牌存儲策略\n .accessTokenConverter(jwtAccessTokenConverter)\n .tokenEnhancer(enhancerChain);\n }*/\n \n /**\n * redis token 方式\n */\n @Override\n public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {\n //驗證時發生的情況處理\n endpoints.authenticationManager(authenticationManager) //支持 password 模式\n .exceptionTranslator(userOAuth2WebResponseExceptionTranslator)//自定義異常處理類添加到認證服務器配置\n .userDetailsService(loginService)\n .tokenStore(tokenStore);\n\n }\n\n /**\n * 客戶端配置(給誰發令牌)\n * 不同客戶端配置不同\n * \n * authorizedGrantTypes 可以包括如下幾種設置中的一種或多種:\n authorization_code:授權碼類型。需要redirect_uri\n implicit:隱式授權類型。需要redirect_uri\n password:資源所有者(即用戶)密碼類型。\n client_credentials:客戶端憑據(客戶端ID以及Key)類型。\n refresh_token:通過以上授權獲得的刷新令牌來獲取新的令牌。\n \n accessTokenValiditySeconds:token 的有效期\n scopes:用來限制客戶端訪問的權限,在換取的 token 的時候會帶上 scope 參數,只有在 scopes 定義內的,纔可以正常換取 token。\n * @param clients\n * @throws Exception\n * @author Damon \n * @date 2020年1月13日\n *\n */\n @Override\n public void configure(ClientDetailsServiceConfigurer clients) throws Exception {\n clients.inMemory()\n .withClient(\"admin-web\")\n .secret(passwordEncoder.encode(\"admin-web-123\"))\n .accessTokenValiditySeconds(3600)\n .refreshTokenValiditySeconds(864000)//配置刷新token的有效期\n .autoApprove(true) //自動授權配置\n .scopes(\"all\")//配置申請的權限範圍\n .authorizedGrantTypes(\"password\", \"authorization_code\", \"client_credentials\", \"refresh_token\")//配置授權模式\n .redirectUris(\"http://localhost:2001/login\")//授權碼模式開啓後必須指定\n \n .and()\n .withClient(\"order-service\")\n .secret(passwordEncoder.encode(\"order-service-123\"))\n .accessTokenValiditySeconds(3600)\n .refreshTokenValiditySeconds(864000)//配置刷新token的有效期\n .autoApprove(true) //自動授權配置\n .scopes(\"all\")\n .authorizedGrantTypes(\"password\", \"authorization_code\", \"client_credentials\", \"refresh_token\")//配置授權模式\n .redirectUris(\"http://localhost:2003/login\")//授權碼模式開啓後必須指定\n \n .and()\n .withClient(\"customer-service\")\n .secret(passwordEncoder.encode(\"customer-service-123\"))\n .accessTokenValiditySeconds(3600)\n .refreshTokenValiditySeconds(864000)//配置刷新token的有效期\n .autoApprove(true) //自動授權配置\n .scopes(\"all\")\n .authorizedGrantTypes(\"password\", \"authorization_code\", \"client_credentials\", \"refresh_token\")//配置授權模式\n .redirectUris(\"http://localhost:6000/login\")//授權碼模式開啓後必須指定\n ;\n }\n \n\n @Override\n public void configure(AuthorizationServerSecurityConfigurer security) {\n security.allowFormAuthenticationForClients();//是允許客戶端訪問 OAuth2 授權接口,否則請求 token 會返回 401\n security.checkTokenAccess(\"isAuthenticated()\");//是允許已授權用戶訪問 checkToken 接口\n security.tokenKeyAccess(\"isAuthenticated()\"); // security.tokenKeyAccess(\"permitAll()\");獲取密鑰需要身份認證,使用單點登錄時必須配置,是允許已授權用戶獲取 token 接口\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在這個配置中,我們redis來記錄token,自定義了登錄認證的邏輯LoginService,自定義異常處理類添加到認證服務器配置。同時函數","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"configure","attrs":{}},{"type":"text","text":"加載了居多需要認證的客戶端服務,配置了授權模式:\"password\"、\"authorization_code\"、\"client_credentials\"、\"refresh_token\",配置刷新token的有效期。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 我們先來看看自定義的認證邏輯,先來看看接口類:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"package com.damon.login.service;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.core.userdetails.UserDetailsService;\nimport org.springframework.security.core.userdetails.UsernameNotFoundException;\n\nimport com.damon.commons.Response;\n\n/**\n * @author Damon \n * @date 2018年11月15日 上午11:59:24\n *\n */\n\npublic interface LoginService extends UserDetailsService {\n \n /**\n * \n * Spring Security默認函數\n * @param username\n * @return\n * @throws UsernameNotFoundException\n * @author Damon \n * @date 2020年1月13日\n *\n */\n UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;\n \n //以下自定義\n \n Response
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.