1.7 其他知识点
new和make的区别
make 仅用来分配及初始化类型为 slice、map、chan 的数据。
new 可分配任意类型的数据,根据传入的类型申请一块内存,返回指向这块内存的指针,即类型 *Type。
make 返回引用,即 Type,new 分配的空间被清零, make 分配空间后,会进行初始。
go的内存分配是怎么样的
Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法,其核心思想是内存池 + 多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;多级对象有:mheap、mspan、arenas、mcentral、mcache。它们以 mspan 作为基本分配单位。具体的分配逻辑如下:
当要分配大于 32K 的对象时,从 mheap 分配。
当要分配的对象小于等于 32K 大于 16B 时,从 P 上的 mcache 分配,如果 mcache 没有内存,则从 mcentral 获取,如果 mcentral 也没有,则向 mheap 申请,如果 mheap 也没有,则从操作系统申请内存。
当要分配的对象小于等于 16B 时,从 mcache 上的微型分配器上分配。
竞态、内存逃逸
竞态
资源竞争,就是在程序中,同一块内存同时被多个 goroutine 访问。我们使用 go build、go run、go test 命令时,添加 -race 标识可以检查代码中是否存在资源竞争。
解决这个问题,我们可以给资源进行加锁,让其在同一时刻只能被一个协程来操作。
sync.Mutex
sync.RWMutex
逃逸分析
「逃逸分析」就是程序运行时内存的分配位置(栈或堆),是由编译器来确定的。堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
在 Go 里变量的内存分配方式则是由编译器来决定的。如果变量在作用域(比如函数范围)之外,还会被引用的话,那么称之为发生了逃逸行为,此时将会把对象放到堆上,即使声明为值类型;如果没有发生逃逸行为的话,则会被分配到栈上,即使 new 了一个对象。
逃逸场景:
指针逃逸
栈空间不足逃逸
动态类型逃逸
闭包引用对象逃逸
什么是 rune 类型
rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。
go语言触发异常的场景有哪些
空指针解析
下标越界
除数为0
调用panic函数
go的接口
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
接口可以让我们将不同的类型绑定到一组公共的方法上,从而实现多态和灵活的设计。
Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态。
相比较于其他语言, Go 有什么优势或者特点
Go代码的设计是务实的。每个功能和语法决策都旨在让程序员的生活更轻松。
Golang 针对并发进行了优化,并且在规模上运行良好。
由于单一的标准代码格式,Golang 通常被认为比其他语言更具可读性。
自动垃圾收集明显比 Java 或 Python 更有效,因为它与程序同时执行。
defer、panic、recover 三者的用法
defer 函数调用的顺序是后进先出,当产生 panic 的时候,会先执行 panic 前面的 defer 函数后才真的抛出异常。一般的,recover 会在 defer 函数里执行并捕获异常,防止程序崩溃。
Go反射
介绍
Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
反射是指在程序运行期对程序本身进行访问和修改的能力,程序在编译时变量被转换为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时程序无法获取自身的信息。
Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 Type 和 Value 任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。
反射三定律
反射第一定律:反射可以将interface类型变量转换成反射对象
反射第二定律:反射可以将反射对象还原成interface对象
反射第三定律:反射对象可修改,value值必须是可设置的
Go语言函数传参是值类型还是引用类型
在Go语言中只存在值传递,要么是值的副本,要么是指针的副本。无论是值类型的变量还是引用类型的变量亦或是指针类型的变量作为参数传递都会发生值拷贝,开辟新的内存空间。
另外值传递、引用传递和值类型、引用类型是两个不同的概念,不要混淆了。引用类型作为变量传递可以影响到函数外部是因为发生值拷贝后新旧变量指向了相同的内存地址。
Go语言中的内存对齐
CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问。比如 32 位的 CPU ,字长为 4 字节,那么 CPU 访问内存的单位也是 4 字节。
CPU 始终以字长访问内存,如果不进行内存对齐,很可能增加 CPU 访问内存的次数,例如:
变量 a、b 各占据 3 字节的空间,内存对齐后,a、b 占据 4 字节空间,CPU 读取 b 变量的值只需要进行一次内存访问。如果不进行内存对齐,CPU 读取 b 变量的值需要进行 2 次内存访问。第一次访问得到 b 变量的第 1 个字节,第二次访问得到 b 变量的后两个字节。
内存对齐对实现变量的原子性操作也是有好处的,每次内存访问是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的访问就是原子的,这个特性在并发场景下至关重要。
简言之:合理的内存对齐可以提高内存读写的性能,并且便于实现变量操作的原子性。
空 struct{} 的用途
因为空结构体不占据内存空间,因此被广泛作为各种场景下的占位符使用。
将 map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。
不发送数据的信道(channel)
使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。
结构体只包含方法,不包含任何的字段
值传递和地址传递(引用传递)
Go 语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct 等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等 这些),这样就可以修改原内容数据。
Golang 的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分 配内存和初始化成员结构,返回对象而非指针。
原子操作
一个或者多个操作在 CPU 执行过程中不被中断的特性,称为原子性 (atomicity)。
这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。而在现实世界中,CPU不可能不中断的执行一系列操作,但如果我们在执行多个操作时,能让他们的中间状态对外不可见,那我们就可以宣城他们拥有了“不可分割”的原子性。
在 Go 中,一条普通的赋值语句其实不是一个原子操作。列如,在 32 位机器上写 int64 类型的变量就会有中间状态,因为他会被拆成两次写操作(MOV)——写低 32 位和写高 32 位。
2. Web框架Gin和微服务框架Micro
2.1 Gin框架
什么是Gin框架
Gin是一个用Go语言编写的web框架,,优点是封装比较好,API友好,源码注释比较明确,具有快速灵活,容错方便等特点。它具有运行速度快,分组的路由器,良好的崩溃捕获和错误处理,非常好的支持中间件和 json。
Gin路由的实现
gin框架使用的是定制版本的httprouter,其路由的原理是大量使用公共前缀的树结构,它基本上是一个紧凑的Trie tree(或者只是Radix Tree)。具有公共前缀的节点也共享一个公共父节点。
gin框架使用的是定制版本的httprouter,其路由的原理是大量使用公共前缀的树结构,它基本上是一个紧凑的Trie tree(或者只是Radix Tree)。具有公共前缀的节点也共享一个公共父节点。
路由树是由一个个节点构成的,gin框架路由树的节点由node结构体表示
在gin的路由中,每一个HTTP Method(GET、POST、PUT、DELETE…)都对应了一棵 radix tree,我们注册路由的时候会调用addRoute`函数
注册路由的逻辑主要有addRoute函数和insertChild方法。
路由树构造的详细过程:
第一次注册路由,例如注册search
继续注册一条没有公共前缀的路由,例如blog
注册一条与先前注册的路由有公共前缀的路由,例如support
路由树构造的详细过程:
第一次注册路由,例如注册search
继续注册一条没有公共前缀的路由,例如blog
注册一条与先前注册的路由有公共前缀的路由,例如support
路由匹配是由节点的 getValue方法实现的。getValue根据给定的路径(键)返回nodeValue值,保存注册的处理函数和匹配到的路径参数数据。
gin框架路由使用前缀树,路由注册的过程是构造前缀树的过程,路由匹配的过程就是查找前缀树的过程。
Gin的中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
gin框架涉及中间件相关有4个常用的方法,它们分别是c.Next()、c.Abort()、c.Set()、c.Get()。
gin框架的中间件函数和处理函数是以切片形式的调用链条存在的,我们可以顺序调用也可以借助c.Next()方法实现嵌套调用。
借助c.Set()和c.Get()方法我们能够在不同的中间件函数中传递数据。
gin默认中间件
gin.Default()默认使用了Logger和Recovery中间件,其中:
Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。
2.2 Micro框架
对微服务的了解
使用一套小服务来开发单个应用的方式,每个服务运行在独立的进程里,一般采用轻量级的通讯机制互联,并且它们可以通过自动化的方式部署
微服务特点
单一职责,此时项目专注于登录和注册
轻量级的通信,通信与平台和语言无关,http是轻量的
隔离性,数据隔离
有自己的数据
技术多样性
微服务架构的优势和缺点
优点
1、易于开发和维护
2、启动较快
3、局部修改容易部署
4、技术栈不受限
5、按需伸缩
缺点
1、运维要求较高
2、分布式的复杂性
3、接口调整成本高
4、重复劳动
RPC协议
远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议
该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程
如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用
RPC调用流程
微服务架构下数据交互一般是对内 RPC,对外 REST
将业务按功能模块拆分到各个微服务,具有提高项目协作效率、降低模块耦合度、提高系统可用性等优点,但是开发门槛比较高,比如 RPC 框架的使用、后期的服务监控等工作
一般情况下,我们会将功能代码在本地直接调用,微服务架构下,我们需要将这个函数作为单独的服务运行,客户端通过网络调用
流行RPC框架:Dubbo、Motan、Thrift、gRPC
gRPC介绍
gRPC由google开发,是一款语言中立、平台中立、开源的远程过程调用系统
gRPC 是一个高性能、开源、通用的RPC框架,基于HTTP2协议标准设计开发,默认采用Protocol Buffers数据序列化协议,支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务,并且为客户端和服务端自动生成可靠的功能库。
在gRPC客户端可以直接调用不同服务器上的远程程序,使用起来就像调用本地程序一样,很容易去构建分布式应用和服务。和很多RPC系统一样,服务端负责实现定义好的接口并处理客户端的请求,客户端根据接口描述直接调用需要的服务。客户端和服务端可以分别使用gRPC支持的不同语言实现。
gRPC主要特性
强大的IDL
gRPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议(类似于XML、JSON、hessian)。ProtoBuf能够将数据进行序列化,并广泛应用在数据存储、通信协议等方面。
多语言支持
gRPC支持多种语言,并能够基于语言自动生成客户端和服务端功能库。目前已提供了C版本grpc、Java版本grpc-java 和 Go版本grpc-go,其它语言的版本正在积极开发中,其中,grpc支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#等语言,grpc-java已经支持Android开发。
HTTP2
gRPC基于HTTP2标准设计,所以相对于其他RPC框架,gRPC带来了更多强大功能,如双向流、头部压缩、多复用请求等。这些功能给移动设备带来重大益处,如节省带宽、降低TCP链接次数、节省CPU使用和延长电池寿命等。同时,gRPC还能够提高了云端服务和Web应用的性能。gRPC既能够在客户端应用,也能够在服务器端应用,从而以透明的方式实现客户端和服务器端的通信和简化通信系统的构建。
Protobuf介绍
Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化、或者说序列化。它很适合做数据存储或RPC数据交换格式。可以用于即时通讯、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式
protobuf的核心内容包括:
定义消息:消息的结构体,以message标识。
定义接口:接口路径和参数,以service标识。
通过protobuf提供的机制,服务端与服务端之间只需要关注接口方法名(service)和参数(message)即可通信,不需关注繁琐的链路协议和字段解析,极大降低服务端的设计开发成本。
Micro介绍和主要功能
go-micro简介
Go Micro是一个插件化的基础框架,基于此可以构建微服务,Micro的设计哲学是可插拔的插件化架构
在架构之外,它默认实现了consul作为服务发现,通过http进行通信,通过protobuf和json进行编解码
是用来构建和管理分布式程序的系统
Runtime (运行时) : 用来管理配置,认证,网络等
Framework (程序开发框架) : 用来方便编写微服务
Clients (多语言客户端) : 支持多语言访问服务端
go-micro的主要功能
服务发现:自动服务注册和名称解析。
负载均衡:基于服务发现构建的客户端负载均衡。
消息编码:基于内容类型的动态消息编码。
请求/响应:基于RPC的请求/响应,支持双向流。
Async Messaging:PubSub是异步通信和事件驱动架构的一流公民。
可插拔接口:Go Micro为每个分布式系统抽象使用Go接口,因此,这些接口是可插拔的,并允许Go Micro与运行时无关,可以插入任何基础技术
go-micro特性
api: api 网关。使用服务发现具有动态请求路由的单个入口点. API 网关允许您在后端构建可扩展的微服务体系结构,并在前端合并公共 api. micro api 通过发现和可插拔处理程序提供强大的路由,为 http, grpc, Websocket, 发布事件等提供服务.
broker: 允许异步消息的消息代理。微服务是事件驱动的体系结构,应该作为一等公民提供消息传递。通知其他服务的事件,而无需担心响应.
network: 通过微网络服务构建多云网络。只需跨任何环境连接网络服务,创建单个平面网络即可全局路由. Micro 的网络根据每个数据中心中的本地注册表动态构建路由,确保根据本地设置路由查询.
new: 服务模板生成器。创建新的服务模板以快速入门. Micro 提供用于编写微服务的预定义模板。始终以相同的方式启动,构建相同的服务以提高工作效率.
proxy: 建立在 Go Micro 上的透明服务代理。将服务发现,负载平衡,容错,消息编码,中间件,监视等卸载到单个位置。独立运行它或与服务一起运行.
registry: 注册表提供服务发现以查找其他服务,存储功能丰富的元数据和终结点信息。它是一个服务资源管理器,允许您在运行时集中和动态地存储此信息.
store: 有状态是任何系统的必然需求。我们提供密钥值存储,提供简单的状态存储,可在服务之间共享或长期卸载 m 以保持微服务无状态和水平可扩展.
web: Web 仪表板允许您浏览服务,描述其终结点,请求和响应格式,甚至直接查询它们。仪表板还包括内置 CLI 的体验,适用于希望动态进入终端的开发人员.
Micro通信流程
Server监听客户端的调用,和Brocker推送过来的信息进行处理。并且Server端需要向Register注册自己的存在或消亡,这样Client才能知道自己的状态
Register服务的注册的发现,Client端从Register中得到Server的信息,然后每次调用都根据算法选择一个的Server进行通信,当然通信是要经过编码/解码,选择传输协议等一系列过程的
如果有需要通知所有的Server端可以使用Brocker进行信息的推送,Brocker 信息队列进行信息的接收和发布
consul
Consul是用于实现分布式系统的服务发现与配置,Consul是分布式的、高可用的、可横向扩展的。
注册中心Consul关键功能
服务发现:
客户端可以注册服务,程序可以轻松找到它们所依赖的服务
运行状况检查:
Consul客户端可以提供任意数量的运行状况检查
KV 存储:
应用程序可以将Consul的层级键/值存储用于任何目的,包括动态配置,功能标记,协调,领导者选举等
安全服务通信:
Consul 可以为服务生成和分发TLS证书,建立相互的TLS连接
多数据中心:
Consul 支持多个数据中心
注册中心Consul两个重要协议
Gossip Protocol (八卦协议)
Raft Protocol ( 选举协议)
Jaeger
什么是链路追踪:
分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等
链路追踪主要功能:
故障快速定位:可以通过调用链结合业务日志快速定位错误信息
链路性能可视化:各个阶段链路耗时、服务依赖关系可以通过可视化界面展现出来
链路分析:通过分析链路耗时、服务依赖关系可以得到用户的行为路径,汇总分析应用在很多业务场景
jaeger链路追踪作用
它是用来监视和诊断基于微服务的分布式系统
用于服务依赖性分析,辅助性能优化
Jaeger组成
Jaeger Client - 为不同语言实现了符合 OpenTracing 标准的 SDK。应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序指定的采样策略传递给 jaeger-agent。
Agent - 它是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector。它被设计成一个基础组件,部署到所有的宿主机上。Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节。
Collector - 接收 jaeger-agent 发送来的数据,然后将数据写入后端存储。Collector 被设计成无状态的组件,因此您可以同时运行任意数量的 jaeger-collector。
Data Store - 后端存储被设计成一个可插拔的组件,支持将数据写入 cassandra、elastic search。
Query - 接收查询请求,然后从后端存储系统中检索 trace 并通过 UI 进行展示。Query 是无状态的,您可以启动多个实例,把它们部署在 nginx 这样的负载均衡器后面。
分布式追踪系统发展很快,种类繁多,但核心步骤一般有三个:代码埋点,数据存储、查询展示
Prometheus
promethues介绍
是一套开源的监控&报警&时间序列数据库的组合
基本原理是通过HTTP协议周期性抓取被监控组件的状态
适合Docker、 Kubernetes环境的监控系统
promethues工作流程
Prometheus server定期从配置好的jobs/exporters/Pushgateway中拉数据
Prometheus server记录数据并且根据报警规则推送alert数据
Alertmanager 根据配置文件,对接收到的警报进行处理,发出告警。
在图形界面中,可视化采集数据
promethues重要组件
Prometheus Server:用于收集和存储时间序列数据。
Client Library:客户端库成相应的metrics并暴露给Prometheus server
Push Gateway:主要用于短期的jobs
Exporters: 用于暴露已有的第三方服务的metrics给Prometheus
Alertmanager: 从Prometheus server端接收到alerts后,会进行
grafana看板
拥有 丰富dashboard和图表编辑的指标分析平台
拥有自己的权限管理和用户管理系统
Grafana 更适合用于数据可视化展示
熔断降级、限流、负载均衡
熔断降级
服务熔断也称服务隔离或过载保护。在微服务应用中,服务存在一定的依赖关系,形成一定的依赖链,如果某个目标服务调用慢或者有大量超时,造成服务不可用,间接导致其他的依赖服务不可用,最严重的可能会阻塞整条依赖链,最终导致业务系统崩溃(又称雪崩效应)。此时,对该服务的调用执行熔断,对于后续请求,不再继续调用该目标服务,而是直接返回,从而可以快速释放资源。等到目标服务情况好转后,则可恢复其调用。
关闭 (Closed):在这种状态下,我们需要一个计数器来记录调用失败的次数和总的请求次数,如果在某个时间窗口内,失败的失败率达到预设的阈值,则切换到断开状态,此时开启一个超时时间,当到达该时间则切换到半关闭状态,该超时时间是给了系统一次机会来修正导致调用失败的错误,以回到正常的工作状态。在关闭状态下,调用错误是基于时间的,在特定的时间间隔内会重置,这能够防止偶然错误导致熔断器进去断开状态
打开 (Open):在该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复
半打开 (Half-Open):在该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,那么可以认为被调用服务已经恢复正常,此时熔断器切换到关闭状态,同时需要重置计数。如果这部分仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到关闭状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮。
常见的有三种熔断降级策略
错误比例:在所设定的时间窗口内,调用的访问错误比例大于所设置的阈值,则对接下来访问的请求进行自动熔断。
错误计数:在所设定的时间窗口内,调用的访问错误次数大于所设置的阈值,则对接下来访问的请求进行自动熔断。
慢调用比例:在所设定的时间窗口内,慢调用的比例大于所设置的阈值,则对接下来访问的请求进行自动熔断。
服务降级
当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,增加响应速度。
关于降级,这里有两种场景:
当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,增加响应速度!
当下游的服务因为某种原因不可用,上游主动调用本地的一些降级逻辑,避免卡顿,迅速返回给用户!
限流
在微服务架构下,若大量请求超过微服务的处理能力时,可能会将服务打跨,甚至产生雪崩效应、影响系统的整体稳定性。比如说你的用户服务处理能力是1w/s,现在因为异常流量或其他原因,有10w的并发请求访问你的服务,那你的服务肯定扛不住啊。这种情况下,我们可以在流量超出承受阈值时,直接进行”限流”、拒绝部分请求,从而保证系统的整体稳定性。
限流算法
固定时间窗口
基于固定时间窗口的限流算法是非常简单的。首先需要选定一个时间起点,之后每次接口请求到来都累加计数器,如果在当前时间窗口内,根据限流规则(比如每秒钟最大允许 100 次接口请求),累加访问次数超过限流值,则限流熔断拒绝接口请求。当进入下一个时间窗口之后,计数器清零重新计数。
滑动时间窗口算法
滑动时间窗口算法是对固定时间窗口算法的一种改进,流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题。对比固定时间窗口限流算法,滑动时间窗口限流算法的时间窗口是持续滑动的,并且除了需要一个计数器来记录时间窗口内接口请求次数之外,还需要记录在时间窗口内每个接口请求到达的时间点,对内存的占用会比较多。
漏桶和令牌桶算法
漏桶算法(Leaky Bucket):主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。
请求先进入到漏桶里,漏桶以一定的速度出水,当水请求过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
令牌桶算法(Token Bucket):是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。
漏桶和令牌桶算法的区别
令牌桶算法,主要放在服务端,用来保护服务端(自己),主要用来对调用者频率进行限流,为的是不让自己被压垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制=桶大小),那么实际处理速率可以超过配置的限制(桶大小)。
而漏桶算法,主要放在调用方,这是用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。
自适应限流
一般的限流常常需要指定一个固定值(qps)作为限流开关的阈值,这个值一是靠经验判断,二是靠通过大量的测试数据得出。但这个阈值,在流量激增、系统自动伸缩或者某某commit了一段有毒代码后就有可能变得不那么合适了。并且一般业务方也不太能够正确评估自己的容量,去设置一个合适的限流阈值。那么我们就可以考虑用自适应限流来解决这个问题。
对于自适应限流来说, 一般都是结合系统的 Load、CPU 使用率以及应用的入口 QPS、平均响应时间和并发量等几个维度的监控指标,通过自适应的流控策略, 让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定。
分布式限流
上面使用的限流算法,都是基本单节点限流的。但线上业务出于各种原因考虑,多是分布式系统,单节点的限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。比如说我希望某个接口的QPS的1000次/秒,服务部署在5台机器上,虽然我们可以通过配置每台节点200次/秒来限流。但如果节点收缩或者扩容,那么久不能满足需求了。而且不同服务的物理配置不一定相同,可能有些节点处理得比较快,那么配置均值来限流,就不是一个好方法了。
常见的分布式限流策略
网关层限流:将限流规则应用在所有流量的入口处,比如nigix+lua
中间件限流:将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量。
负载均衡
Load balancing,即负载均衡,是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。
负载均衡(Load Balance),意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。
负载均衡算法
1、轮询法
将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
2、随机法
通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
3、源地址哈希法
源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。
4、加权轮询法
不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
5、加权随机法
与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。
6、最小连接数法
最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前。
积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。
2.3 Viper
什么是Viper
Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。
特性:
设置默认值
从JSON、TOML、YAML、HCL、envfile和Java properties格式的配置文件读取配置信息
实时监控和重新读取配置文件(可选)
从环境变量中读取
从远程配置系统(etcd或Consul)读取并监控配置变化
从命令行参数读取配置
从buffer读取配置
显式配置值
Viper支持什么功能
Viper能够为你执行下列操作:
查找、加载和反序列化JSON、TOML、YAML、HCL、INI、envfile和Java properties格式的配置文件。
提供一种机制为你的不同配置选项设置默认值。
提供一种机制来通过命令行参数覆盖指定选项的值。
提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
当用户提供了与默认值相同的命令行或配置文件时,可以很容易地分辨出它们之间的区别。
建立默认值
读取配置文件
写入配置文件
监控并重新读取配置文件
从io.Reader读取配置
覆盖设置
注册和使用别名
使用环境变量
使用Flags
远程Key/Value存储支持
监控etcd中的更改-未加密
2.4 Swagger
什么是Swagger
Swagger本质上是一种用于描述使用JSON表示的RESTful API的接口描述语言。Swagger与一组开源软件工具一起使用,以设计、构建、记录和使用RESTful Web服务。Swagger包括自动文档,代码生成和测试用例生成。
想要使用gin-swagger为你的代码自动生成接口文档,一般需要下面三个步骤:
按照swagger要求给接口代码添加声明式注释,具体参照声明式注释格式
使用swag工具扫描代码自动生成API接口文档数据
使用gin-swagger渲染在线接口文档页面
Swagger的优势
在前后端分离的项目开发过程中,如果后端同学能够提供一份清晰明了的接口文档,那么就能极大地提高大家的沟通效率和开发效率。可是编写接口文档历来都是令人头痛的,而且后续接口文档的维护也十分耗费精力。
最好是有一种方案能够既满足我们输出文档的需要又能随代码的变更自动更新,而Swagger正是那种能帮我们解决接口文档问题的工具。
2.5 Zap
什么是Zap
Zap是在 Go 中实现超快、结构化、分级的日志记录。
Zap日志能够提供下面这些功能:
1、能够将事件记录到文件中,也可以在应用控制台输出
2、日志切割-可以根据文件大小,时间或间隔来切割日志文件
3、支持不同的日志级别。例如 INFO、DEBUG、ERROR等
4、能够打印基本信息,如调用文件/函数名和行号,日志时间等。
zap的基本配置
Zap提供了两种类型的日志记录器—Sugared Logger 和 Logger 。
在性能很好但不是很关键的上下文中,使用 SugaredLogger 。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。
在每一微秒和每一次内存分配都很重要的上下文中,使用 Logger 。它甚至比 SugaredLogger 更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
这个日志程序中唯一缺少的就是日志切割归档功能。添加日志切割归档功能,我们将使用第三方库Lumberjack来实现。
2.6 JWT
什么是JWT
JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。
JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。
一个JWT Token就像这样:
eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyODAx0DcyNzQ40DMyMzU4NSwiZ
XhwIjoxNTk0NTQwMjkxLCJpc3MiOiJibHV1YmVsbCJ9.1k_ZrAtYGCeZhK3iupHxP1kgjBJzQTVTtX0iZYFx9wU
JWT的实现
JWT由.分割的三部分组成,这三部分依次是:
头部(Header)
作用:记录令牌类型、签名算法等 例如:{“alg":“HS256”,“type”,"JWT}
负载(Payload)
作用:携带一些用户信息 例如{“userId”:“1”,“username”:“mayikt”}
签名(Signature)
作用:防止Token被篡改、确保安全性 例如 计算出来的签名,一个字符串
头部和负载以json形式存在,这就是JWT中的JSON,三部分的内容都分别单独经过了Base64编码,以.拼接成一个JWT Token。
JWT的优势
JWT就是一种基于Token的轻量级认证模式,服务端认证通过后,会生成一个JSON对象,经过签名后得到一个Token(令牌)再发回给用户,用户后续请求只需要带上这个Token,服务端解密之后就能获取该用户的相关信息了。
JWT拥有基于Token的会话管理方式所拥有的一切优势,不依赖Cookie,使得其可以防止CSRF攻击,也能在禁用Cookie的浏览器环境中正常运行。
而JWT的最大优势是服务端不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储Session所需要引入的Redis等组件,降低了系统架构复杂度。但这也是JWT最大的劣势,由于有效期存储在Token中,JWT Token-旦签发,就会在有效期内-直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的JWT Token,如果需要禁用用户,单纯使用JWT就无法做到了。