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)。

除此之外,想要解决负载均衡器的单点故障问题,可以将第二个负载均衡器连接到第一个上,从而形成一个集群。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐