Go全棧面試題(3) -微服務面試題

title: Go全棧面試題(3) -微服務面試題
tags: go
author: Clown95


微服務面試題

Http get跟head

get:獲取由Request-URI標識的任何信息(以實體的形式),如果Request-URI引用某個數據處理過程,則應該以它產生的數據作爲在響應中的實體,而不是該過程的源代碼文本,除非該過程碰巧輸出該文本。

head: 除了服務器不能在響應中返回消息體,HEAD方法與GET相同。用來獲取暗示實體的元信息,而不需要傳輸實體本身。常用於測試超文本鏈接的有效性、可用性和最近的修改。

說一下中間件原理

中間件(middleware)是基礎軟件的一大類,屬於可複用軟件的範疇。中間件處於操作系統軟件與用戶的應用軟件的中間。中間件在操作系統、網絡和數據庫之上,應用軟件的下層,總的作用是爲處於自己上層的應用軟件提供運行與開發的環境,幫助用戶靈活、高效地開發和集成複雜的應用軟件
IDC的定義是:中間件是一種獨立的系統軟件或服務程序,分佈式應用軟件藉助這種軟件在不同的技術之間共享資源,中間件位於客戶機服務器的操作系統之上,管理計算資源和網絡通信。

中間件解決的問題是:

在中間件產生以前,應用軟件直接使用操作系統、網絡協議和數據庫等開發,這些都是計算機最底層的東西,越底層越複雜,開發者不得不面臨許多很棘手的問題,如操作系統的多樣性,繁雜的網絡程序設計、管理,複雜多變的網絡環境,數據分散處理帶來的不一致性問題、性能和效率、安全,等等。這些與用戶的業務沒有直接關係,但又必須解決,耗費了大量有限的時間和精力。於是,有人提出能不能將應用軟件所要面臨的共性問題進行提煉、抽象,在操作系統之上再形成一個可複用的部分,供成千上萬的應用軟件重複使用。這一技術思想最終構成了中間件這類的軟件。中間件屏蔽了底層操作系統的複雜性,使程序開發人員面對一個簡單而統一的開發環境,減少程序設計的複雜性,將注意力集中在自己的業務上,不必再爲程序在不同系統軟件上的移植而重複工作,從而大大減少了技術上的負擔。

用過原生的http包嗎?

Golang中http包中處理 HTTP 請求主要跟兩個東西相關:ServeMux 和 Handler。

ServrMux 本質上是一個 HTTP 請求路由器(或者叫多路複用器,Multiplexor)。它把收到的請求與一組預先定義的 URL 路徑列表做對比,然後在匹配到路徑的時候調用關聯的處理器(Handler)。

處理器(Handler)負責輸出HTTP響應的頭和正文。任何滿足了http.Handler接口的對象都可作爲一個處理器。通俗的說,對象只要有個如下簽名的ServeHTTP方法即可:

ServeHTTP(http.ResponseWriter, *http.Request)

Go 語言的 HTTP 包自帶了幾個函數用作常用處理器,比如FileServer,NotFoundHandler 和 RedirectHandler。

應用示例:

package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	rh := http.RedirectHandler("http://www.baidu.com", 307)
	mux.Handle("/foo", rh)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

在這個應用示例中,首先在 main 函數中我們只用了 http.NewServeMux 函數來創建一個空的 ServeMux。
然後我們使用 http.RedirectHandler 函數創建了一個新的處理器,這個處理器會對收到的所有請求,都執行307重定向操作到 http://www.baidu.com
接下來我們使用 ServeMux.Handle 函數將處理器註冊到新創建的 ServeMux,所以它在 URL 路徑/foo 上收到所有的請求都交給這個處理器。
最後我們創建了一個新的服務器,並通過 http.ListenAndServe 函數監聽所有進入的請求,通過傳遞剛纔創建的 ServeMux來爲請求去匹配對應處理器。
在瀏覽器中訪問 http://localhost:3000/foo,你應該能發現請求已經成功的重定向了。

此刻你應該能注意到一些有意思的事情:ListenAndServer 的函數簽名是 ListenAndServe(addr string, handler Handler) ,但是第二個參數我們傳遞的是個 ServeMux。

通過這個例子我們就可以知道,net/http包在編寫golang web應用中有很重要的作用,它主要提供了基於HTTP協議進行工作的client實現和server實現,可用於編寫HTTP服務端和客戶端。

grpc遵循什麼協議?

grpc是由Google主導開發的RPC框架,使用HTTP/2協議並用ProtoBuf作爲序列化工具。gRPC是動態代理的模式實現的,客戶端應用可以像調用本地對象一樣直接調用另一臺不同的機器上服務端應用的方法。和傳統的REST不同的是gRPC使用了靜態路徑,從而提高性能,另外開發者不用瞭解各種底層網絡協議,不用去拼REST風格的動態URL,用一些格式化的錯誤碼代替了HTTP的狀態碼,不用管各種的HTTP狀態碼,開發者開發效率比較高。客戶端可以充分利用高級流和鏈接功能,從而有助於節省帶寬、降低的TCP鏈接次數、節省CPU使用.

grpc內部原理是什麼?

gRPC 是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計。gRPC 默認使用 protocol buffers,這是 Google 開源的一套成熟的結構數據序列化機制(當然也可以使用其他數據格式如 JSON).

client如何實現長連接?

  • 使用http keep-alvie
  • 使用HeartBeat心跳包

微服務架構是什麼樣子的?

通常傳統的項目體積龐大,需求、設計、開發、測試、部署流程固定。新功能需要在原項目上做修改。

但是微服務可以看做是對大項目的拆分,是在快速迭代更新上線的需求下產生的。新的功能模塊會發布成新的服務組件,與其他已發佈的服務組件一同協作。
服務內部有多個生產者和消費者,通常以http rest的方式調用,服務總體以一個(或幾個)服務的形式呈現給客戶使用。

微服務架構是一種思想對微服務架構我們沒有一個明確的定義,但簡單來說微服務架構是:

採用一組服務的方式來構建一個應用,服務獨立部署在不同的進程中,不同服務通過一些輕量級交互機制來通信,例如 RPC、HTTP 等,服務可獨立擴展伸縮,每個服務定義了明確的邊界,不同的服務甚至可以採用不同的編程語言來實現,由獨立的團隊來維護。

Golang的微服務框架kit中有詳細的微服務的例子,可以參考學習.

微服務架構設計包括:

  1. 服務熔斷降級限流機制 熔斷降級的概念(Rate Limiter 限流器,Circuit breaker 斷路器).
  2. 框架調用方式解耦方式 Kit 或 Istio 或 Micro 服務發現(consul zookeeper kubeneters etcd ) RPC調用框架.
  3. 鏈路監控,zipkin和prometheus.
  4. 多級緩存.
  5. 網關 (kong gateway).
  6. Docker部署管理 Kubenetters.
  7. 自動集成部署 CI/CD 實踐.
  8. 自動擴容機制規則.
  9. 壓測 優化.
  10. Trasport 數據傳輸(序列化和反序列化).
  11. Logging 日誌.
  12. Metrics 指針對每個請求信息的儀表盤化.

微服務有什麼優點?

  • 解耦——系統中的服務在很大程度上是解耦的。因此,整個應用程序可以很容易地構建、修改和伸縮
  • 組件化——微服務被視爲獨立的組件,可以很容易地替換和升級
  • 業務功能——微服務非常簡單,只關注一個功能
  • 自治——開發人員和團隊可以彼此獨立工作,從而提高速度
  • 持續交付——通過軟件創建、測試和批准的系統自動化,允許頻繁地發佈軟件
  • 責任——微服務不關注應用程序作爲項目。相反,他們將應用程序視爲自己負責的產品
  • 分散治理——重點是爲正確的工作使用正確的工具。這意味着沒有標準化的模式或任何技術模式。開發人員可以自由選擇最有用的工具來解決他們的問題
  • 敏捷——微服務支持敏捷開發。任何新特性都可以快速開發並再次丟棄

在使用微服務架構時,您面臨的挑戰是什麼?

開發一些較小的微服務聽起來很容易,但開發它們時經常遇到的挑戰如下。

  • 自動化組件:難自動化,因爲有許多較小的組件。因此,對於每個組件我們必須遵循Build,Deploy和Monitor的各個階段。
  • 易感性:將大量組件維護在一起變得難以部署,維護,監控和識別問題。它需要在所有組件周圍具有很好的感知能力。
  • 配置管理:在不同的環境中維護組件的配置有時會變得很困難。
  • 調試:很難找出每個服務的錯誤。維護集中的日誌記錄和儀表板來調試問題是非常重要的。

SOA和微服務架構之間的主要區別是什麼?

SOA和微服務之間的主要區別如下:

SOA 微服務
遵循“ 儘可能多的共享 ”架構方法 遵循“ 儘可能少分享 ”的架構方法
重要性在於 業務功能 重用 重要性在於“ 有界背景 ” 的概念
他們有 共同的 治理 和標準 他們專注於 人們的 合作 和其他選擇的自由
使用 企業服務總線(ESB) 進行通信 簡單的消息系統
它們支持 多種消息協議 他們使用 輕量級協議 ,如 HTTP / REST 等。
多線程, 有更多的開銷來處理I / O 單線程 通常使用Event Loop功能進行非鎖定I / O處理
最大化應用程序服務可重用性 專注於 解耦
傳統的關係數據庫 更常用 現代 關係數據庫 更常用
系統的變化需要修改整體 系統的變化是創造一種新的服務
DevOps / Continuous Delivery正在變得流行,但還不是主流 專注於DevOps /持續交付

微服務之間是如何通信的呢?

  • REST over HTTP(S)
  • 通過Message Broker進行消息傳遞
  • RPC (跨語言或單語言)

REST over HTTP(S)
自Roy Fielding提出RESTful架構自提出以來,一直都是備受歡迎的方案,特別是在Web應用的開發中。Fielding提出的約束雖然不是標準,但在聲明我們的API爲RESTful之前,應該始終遵循這些約束。
HTTP上有各種各樣的REST,因爲沒有強制執行的標準。開發人員可以自由選擇以JSON、XML或某種自定義格式形成請求有效負載。
REST over HTTP(S)僅意味着使用REST架構風格並通過HTTP(S)發送請求。

通過Message Broker進行消息傳遞
該選項基本上通過將微服務連接到集中消息總線來工作,並且服務之間的所有通信都通過backbone發送消息來完成。

RPC (跨語言或單語言)
遠程過程調用在分佈式系統中並不新鮮,它通過在網絡上的另一個設備上執行函數/方法/過程來工作。

什麼是微服務熔斷?什麼是服務降級?

微服務的熔斷與降級,當然熔斷與降級並不是一個概念,只是很多時候會一起實現了:

  • 服務熔斷
    一般是某個服務故障或異常引起,類似“保險絲”,當某個異常被觸發,直接熔斷整個服務,而不是等到此服務超時。

  • 服務降級
    降級是在客戶端,與服務端無關。
    降級,從整體負荷考慮,某個服務熔斷後,服務器將不再被調用,此時客戶端可以自己準備一個本地的fallback回調,這樣,雖然服務水平下降,但可用,比直接掛掉要好。

你能否給出關於REST和微服務的要點?

  • REST
    雖然您可以通過多種方式實現微服務,但REST over HTTP是實現微服務的一種方式。REST還可用於其他應用程序,如Web應用程序,API設計和MVC應用程序,以提供業務數據。

  • 微服務
    微服務是一種體系結構,其中系統的所有組件都被放入單獨的組件中,這些組件可以單獨構建,部署和擴展。微服務的某些原則和最佳實踐有助於構建彈性應用程序。

簡而言之,您可以說REST是構建微服務的媒介。

Etcd怎麼實現分佈式鎖?

首先思考下Etcd是什麼?可能很多人第一反應可能是一個鍵值存儲倉庫,卻沒有重視官方定義的後半句,用於配置共享和服務發現。

A highly-available key value store for shared configuration and service discovery.

實際上,etcd 作爲一個受到 ZooKeeper 與 doozer 啓發而催生的項目,除了擁有與之類似的功能外,更專注於以下四點。

  • 簡單:基於 HTTP+JSON 的 API 讓你用 curl 就可以輕鬆使用。
  • 安全:可選 SSL 客戶認證機制。
  • 快速:每個實例每秒支持一千次寫操作。
  • 可信:使用 Raft 算法充分實現了分佈式。

但是這裏我們主要講述Etcd如何實現分佈式鎖?

因爲 Etcd 使用 Raft 算法保持了數據的強一致性,某次操作存儲到集羣中的值必然是全局一致的,所以很容易實現分佈式鎖。鎖服務有兩種使用方式,一是保持獨佔,二是控制時序。

  • 保持獨佔即所有獲取鎖的用戶最終只有一個可以得到。etcd 爲此提供了一套實現分佈式鎖原子操作 CAS(CompareAndSwap)的 API。通過設置prevExist值,可以保證在多個節點同時去創建某個目錄時,只有一個成功。而創建成功的用戶就可以認爲是獲得了鎖。

  • 控制時序,即所有想要獲得鎖的用戶都會被安排執行,但是獲得鎖的順序也是全局唯一的,同時決定了執行順序。etcd 爲此也提供了一套 API(自動創建有序鍵),對一個目錄建值時指定爲POST動作,這樣 etcd 會自動在目錄下生成一個當前最大的值爲鍵,存儲這個新的值(客戶端編號)。同時還可以使用 API 按順序列出所有當前目錄下的鍵值。此時這些鍵的值就是客戶端的時序,而這些鍵中存儲的值可以是代表客戶端的編號。

在這裏Ectd實現分佈式鎖基本實現原理爲:

  1. 在ectd系統裏創建一個key
  2. 如果創建失敗,key存在,則監聽該key的變化事件,直到該key被刪除,回到1
  3. 如果創建成功,則認爲我獲得了鎖

應用示例:

package etcdsync

import (
	"fmt"
	"io"
	"os"
	"sync"
	"time"

	"github.com/coreos/etcd/client"
	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
)

const (
	defaultTTL = 60
	defaultTry = 3
	deleteAction = "delete"
	expireAction = "expire"
)

// A Mutex is a mutual exclusion lock which is distributed across a cluster.
type Mutex struct {
	key    string
	id     string // The identity of the caller
	client client.Client
	kapi   client.KeysAPI
	ctx    context.Context
	ttl    time.Duration
	mutex  *sync.Mutex
	logger io.Writer
}

// New creates a Mutex with the given key which must be the same
// across the cluster nodes.
// machines are the ectd cluster addresses
func New(key string, ttl int, machines []string) *Mutex {
	cfg := client.Config{
		Endpoints:               machines,
		Transport:               client.DefaultTransport,
		HeaderTimeoutPerRequest: time.Second,
	}

	c, err := client.New(cfg)
	if err != nil {
		return nil
	}

	hostname, err := os.Hostname()
	if err != nil {
		return nil
	}

	if len(key) == 0 || len(machines) == 0 {
		return nil
	}

	if key[0] != '/' {
		key = "/" + key
	}

	if ttl < 1 {
		ttl = defaultTTL
	}

	return &Mutex{
		key:    key,
		id:     fmt.Sprintf("%v-%v-%v", hostname, os.Getpid(), time.Now().Format("20060102-15:04:05.999999999")),
		client: c,
		kapi:   client.NewKeysAPI(c),
		ctx: context.TODO(),
		ttl: time.Second * time.Duration(ttl),
		mutex:  new(sync.Mutex),
	}
}

// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() (err error) {
	m.mutex.Lock()
	for try := 1; try <= defaultTry; try++ {
		if m.lock() == nil {
			return nil
		}
		
		m.debug("Lock node %v ERROR %v", m.key, err)
		if try < defaultTry {
			m.debug("Try to lock node %v again", m.key, err)
		}
	}
	return err
}

func (m *Mutex) lock() (err error) {
	m.debug("Trying to create a node : key=%v", m.key)
	setOptions := &client.SetOptions{
		PrevExist:client.PrevNoExist,
		TTL:      m.ttl,
	}
	resp, err := m.kapi.Set(m.ctx, m.key, m.id, setOptions)
	if err == nil {
		m.debug("Create node %v OK [%q]", m.key, resp)
		return nil
	}
	m.debug("Create node %v failed [%v]", m.key, err)
	e, ok := err.(client.Error)
	if !ok {
		return err
	}

	if e.Code != client.ErrorCodeNodeExist {
		return err
	}

	// Get the already node's value.
	resp, err = m.kapi.Get(m.ctx, m.key, nil)
	if err != nil {
		return err
	}
	m.debug("Get node %v OK", m.key)
	watcherOptions := &client.WatcherOptions{
		AfterIndex : resp.Index,
		Recursive:false,
	}
	watcher := m.kapi.Watcher(m.key, watcherOptions)
	for {
		m.debug("Watching %v ...", m.key)
		resp, err = watcher.Next(m.ctx)
		if err != nil {
			return err
		}

		m.debug("Received an event : %q", resp)
		if resp.Action == deleteAction || resp.Action == expireAction {
			return nil
		}
	}

}

// Unlock unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() (err error) {
	defer m.mutex.Unlock()
	for i := 1; i <= defaultTry; i++ {
		var resp *client.Response
		resp, err = m.kapi.Delete(m.ctx, m.key, nil)
		if err == nil {
			m.debug("Delete %v OK", m.key)
			return nil
		}
		m.debug("Delete %v falied: %q", m.key, resp)
		e, ok := err.(client.Error)
		if ok && e.Code == client.ErrorCodeKeyNotFound {
			return nil
		}
	}
	return err
}

func (m *Mutex) debug(format string, v ...interface{}) {
	if m.logger != nil {
		m.logger.Write([]byte(m.id))
		m.logger.Write([]byte(" "))
		m.logger.Write([]byte(fmt.Sprintf(format, v...)))
		m.logger.Write([]byte("\n"))
	}
}

func (m *Mutex) SetDebugLogger(w io.Writer) {
	m.logger = w
}

其實類似的實現有很多,但目前都已經過時,使用的都是被官方標記爲deprecated的項目。且大部分接口都不如上述代碼簡單。 使用上,跟Golang官方sync包的Mutex接口非常類似,先New(),然後調用Lock(),使用完後調用Unlock(),就三個接口,就是這麼簡單。示例代碼如下:

package main

import (
	"github.com/zieckey/etcdsync"
	"log"
)

func main() {
	//etcdsync.SetDebug(true)
	log.SetFlags(log.Ldate|log.Ltime|log.Lshortfile)
	m := etcdsync.New("/etcdsync", "123", []string{"http://127.0.0.1:2379"})
	if m == nil {
		log.Printf("etcdsync.NewMutex failed")
	}
	err := m.Lock()
	if err != nil {
		log.Printf("etcdsync.Lock failed")
	} else {
		log.Printf("etcdsync.Lock OK")
	}

	log.Printf("Get the lock. Do something here.")

	err = m.Unlock()
	if err != nil {
		log.Printf("etcdsync.Unlock failed")
	} else {
		log.Printf("etcdsync.Unlock OK")
	}
}

負載均衡原理是什麼?

負載均衡Load Balance)是高可用網絡基礎架構的關鍵組件,通常用於將工作負載分佈到多個服務器來提高網站、應用、數據庫或其他服務的性能和可靠性。負載均衡,其核心就是網絡流量分發,分很多維度。

負載均衡(Load Balance)通常是分攤到多個操作單元上進行執行,例如Web服務器、FTP服務器、企業關鍵應用服務器和其它關鍵任務服務器等,從而共同完成工作任務。

負載均衡是建立在現有網絡結構之上,它提供了一種廉價有效透明的方法擴展網絡設備和服務器的帶寬、增加吞吐量、加強網絡數據處理能力、提高網絡的靈活性和可用性。

通過一個例子詳細介紹:

  • 沒有負載均衡 web 架構

在這裏用戶是直連到 web 服務器,如果這個服務器宕機了,那麼用戶自然也就沒辦法訪問了。
另外,如果同時有很多用戶試圖訪問服務器,超過了其能處理的極限,就會出現加載速度緩慢或根本無法連接的情況。

而通過在後端引入一個負載均衡器和至少一個額外的 web 服務器,可以緩解這個故障。
通常情況下,所有的後端服務器會保證提供相同的內容,以便用戶無論哪個服務器響應,都能收到一致的內容。

  • 有負載均衡 web 架構

用戶訪問負載均衡器,再由負載均衡器將請求轉發給後端服務器。在這種情況下,單點故障現在轉移到負載均衡器上了。
這裏又可以通過引入第二個負載均衡器來緩解。

那麼負載均衡器的工作方式是什麼樣的呢,負載均衡器又可以處理什麼樣的請求?

負載均衡器的管理員能主要爲下面四種主要類型的請求設置轉發規則:

  • HTTP (七層)
  • HTTPS (七層)
  • TCP (四層)
  • UDP (四層)

負載均衡器如何選擇要轉發的後端服務器?

負載均衡器一般根據兩個因素來決定要將請求轉發到哪個服務器。首先,確保所選擇的服務器能夠對請求做出響應,然後根據預先配置的規則從健康服務器池(healthy pool)中進行選擇。

因爲,負載均衡器應當只選擇能正常做出響應的後端服務器,因此就需要有一種判斷後端服務器是否健康的方法。爲了監視後臺服務器的運行狀況,運行狀態檢查服務會定期嘗試使用轉發規則定義的協議和端口去連接後端服務器。
如果,服務器無法通過健康檢查,就會從池中剔除,保證流量不會被轉發到該服務器,直到其再次通過健康檢查爲止。

負載均衡算法

負載均衡算法決定了後端的哪些健康服務器會被選中。 其中常用的算法包括:

  • Round Robin(輪詢):爲第一個請求選擇列表中的第一個服務器,然後按順序向下移動列表直到結尾,然後循環。
  • Least Connections(最小連接):優先選擇連接數最少的服務器,在普遍會話較長的情況下推薦使用。
  • Source:根據請求源的 IP 的散列(hash)來選擇要轉發的服務器。這種方式可以一定程度上保證特定用戶能連接到相同的服務器。

如果你的應用需要處理狀態而要求用戶能連接到和之前相同的服務器。可以通過 Source 算法基於客戶端的 IP 信息創建關聯,或者使用粘性會話(sticky sessions)。

除此之外,想要解決負載均衡器的單點故障問題,可以將第二個負載均衡器連接到第一個上,從而形成一個集羣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章