一般咱们去面试确定会有些不错的Golang的面试题目的,因此总结下,让其余Golang开发者也能够查看到,同时也用来检测本身的能力和提醒本身的不足之处,欢迎你们补充和提交新的面试题目.php
Golang面试问题汇总:html
Golang中Goroutine 能够经过 Channel 进行安全读写共享变量。java
ch := make(chan int) 无缓冲的channel因为没有缓冲发送和接收须要同步. ch := make(chan int, 2) 有缓冲channel不要求发送和接收操做同步.
CSP模型是上个世纪七十年代提出的,不一样于传统的多线程经过共享内存来通讯,CSP讲究的是“以通讯的方式来共享内存”。用于描述两个独立的并发实体经过共享的通信 channel(管道)进行通讯的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。node
Golang中channel 是被单首创建而且能够在进程之间传递,它的通讯模式相似于 boss-worker 模式的,一个实体经过将消息发送到channel 中,而后又监听这个 channel 的实体处理,两个实体之间是匿名的,这个就实现实体中间的解耦,其中 channel 是同步的一个消息被发送到 channel 中,最终是必定要被另外的实体消费掉的,在实现原理上其实相似一个阻塞的消息队列。python
Goroutine 是Golang实际并发执行的实体,它底层是使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程,相似于 greenthread,go底层选择使用coroutine的出发点是由于,它具备如下特色:mysql
Golang中的Goroutine的特性:linux
Golang内部有三个对象: P对象(processor) 表明上下文(或者能够认为是cpu),M(work thread)表明工做线程,G对象(goroutine).ios
正常状况下一个cpu对象启一个工做线程对象,线程去检查并执行goroutine对象。碰到goroutine对象阻塞的时候,会启动一个新的工做线程,以充分利用cpu资源。 全部有时候线程对象会比处理器对象多不少.c++
咱们用以下图分别表示P、M、G:git
G(Goroutine) :咱们所说的协程,为用户级的轻量级线程,每一个Goroutine对象中的sched保存着其上下文信息.
M(Machine) :对内核级线程的封装,数量对应真实的CPU数(真正干活的对象).
P(Processor) :即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可经过GOMAXPROCS()来设置,默认为核心数.
在单核状况下,全部Goroutine运行在同一个线程(M0)中,每个线程维护一个上下文(P),任什么时候刻,一个上下文中只有一个Goroutine,其余Goroutine在runqueue中等待。
一个Goroutine运行完本身的时间片后,让出上下文,本身回到runqueue中(以下图所示)。
当正在运行的G0阻塞的时候(能够须要IO),会再建立一个线程(M1),P转到新的线程中去运行。
当M0返回时,它会尝试从其余线程中“偷”一个上下文过来,若是没有偷到,会把Goroutine放到Global runqueue中去,而后把本身放入线程缓存中。 上下文会定时检查Global runqueue。
Golang是为并发而生的语言,Go语言是为数很少的在语言层面实现并发的语言;也正是Go语言的并发特性,吸引了全球无数的开发者。
Golang的CSP并发模型,是经过Goroutine和Channel来实现的。
Goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“相似,能够理解为”线程“。 Channel是Go语言中各个并发结构体(Goroutine)以前的通讯机制。一般Channel,是各个Goroutine之间通讯的”管道“,有点相似于Linux中的管道。
通讯机制channel也很方便,传数据用channel <- data,取数据用<-channel。
在通讯过程当中,传数据channel <- data和取数据<-channel必然会成对出现,由于这边传,那边取,两个goroutine之间才会实现通讯。
并且无论传仍是取,必阻塞,直到另外的goroutine传或者取为止。
Golang 中经常使用的并发模型有三种:
无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 goroutine 和接收 goroutine 同时准备好,才能够完成发送和接收操做。
从上面无缓冲的通道定义来看,发送 goroutine 和接收 gouroutine 必须是同步的,同时准备后,若是没有同时准备好的话,先执行的操做就会阻塞等待,直到另外一个相对应的操做准备好为止。这种无缓冲的通道咱们也称之为同步通道。
func main() { ch := make(chan struct{}) go func() { fmt.Println("start working") time.Sleep(time.Second * 1) ch <- struct{}{} }() <-ch fmt.Println("finished") }
当主 goroutine 运行到 <-ch 接受 channel 的值的时候,若是该 channel 中没有数据,就会一直阻塞等待,直到有值。 这样就能够简单实现并发控制
Goroutine是异步执行的,有的时候为了防止在结束mian函数的时候结束掉Goroutine,因此须要同步等待,这个时候就须要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它会等待它收集的全部 goroutine 任务所有完成。在WaitGroup里主要有三个方法:
在主 goroutine 中 Add(delta int) 索要等待goroutine 的数量。 在每个 goroutine 完成后 Done() 表示这一个goroutine 已经完成,当全部的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回返回。
func main(){ var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", } for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() http.Get(url) }(url) } wg.Wait() }
在Golang官网中对于WaitGroup介绍是A WaitGroup must not be copied after first use
,在 WaitGroup 第一次使用后,不能被拷贝
应用示例:
func main(){ wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func(wg sync.WaitGroup, i int) { fmt.Printf("i:%d", i) wg.Done() }(wg, i) } wg.Wait() fmt.Println("exit") }
运行:
i:1i:3i:2i:0i:4fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]: sync.runtime_Semacquire(0xc000094018) /home/keke/soft/go/src/runtime/sema.go:56 +0x39 sync.(*WaitGroup).Wait(0xc000094010) /home/keke/soft/go/src/sync/waitgroup.go:130 +0x64 main.main() /home/keke/go/Test/wait.go:17 +0xab exit status 2
它提示全部的 goroutine 都已经睡眠了,出现了死锁。这是由于 wg 给拷贝传递到了 goroutine 中,致使只有 Add 操做,其实 Done操做是在 wg 的副本执行的。
所以 Wait 就死锁了。
这个第一个修改方式:将匿名函数中 wg 的传入类型改成 *sync.WaitGrou,这样就能引用到正确的WaitGroup了。 这个第二个修改方式:将匿名函数中的 wg 的传入参数去掉,由于Go支持闭包类型,在匿名函数中能够直接使用外面的 wg 变量
一般,在一些简单场景下使用 channel 和 WaitGroup 已经足够了,可是当面临一些复杂多变的网络并发场景下 channel 和 WaitGroup 显得有些力不从心了。 好比一个网络请求 Request,每一个 Request 都须要开启一个 goroutine 作一些事情,这些 goroutine 又可能会开启其余的 goroutine,好比数据库和RPC服务。 因此咱们须要一种能够跟踪 goroutine 的方案,才能够达到控制他们的目的,这就是Go语言为咱们提供的 Context,称之为上下文很是贴切,它就是goroutine 的上下文。 它是包括一个程序的运行环境、现场和快照等。每一个程序要运行时,都须要知道当前程序的运行状态,一般Go 将这些封装在一个 Context 里,再将它传给要执行的 goroutine 。
context 包主要是用来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理。
context 包的核心是 struct Context,接口声明以下:
// A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Done returns a channel that is closed when this `Context` is canceled // or times out. Done() <-chan struct{} // Err indicates why this Context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} }
Done() 返回一个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有一个取消信号
Err() 在Done() 以后,返回context 取消的缘由。
Deadline() 设置该context cancel的时间点
Value() 方法容许 Context 对象携带request做用域的数据,该数据必须是线程安全的。
Context 对象是线程安全的,你能够把一个 Context 对象传递给任意个数的 gorotuine,对它执行 取消 操做时,全部 goroutine 都会接收到取消信号。
一个 Context 不能拥有 Cancel 方法,同时咱们也只能 Done channel 接收数据。 其中的缘由是一致的:接收取消信号的函数和发送信号的函数一般不是一个。 典型的场景是:父操做为子操做操做启动 goroutine,子操做也就不能取消父操做。
首先JSON 标准库对 nil slice 和 空 slice 的处理是不一致.
一般错误的用法,会报数组越界的错误,由于只是声明了slice,却没有给实例化的对象。
var slice []int slice[1] = 0
此时slice的值是nil,这种状况能够用于须要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。
empty slice 是指slice不为nil,可是slice没有值,slice的底层的空间是空的,此时的定义以下:
slice := make([]int,0) slice := []int{}
当咱们查询或者处理一个空的列表的时候,这很是有用,它会告诉咱们返回的是一个列表,可是列表内没有任何值。
总之,nil slice 和 empty slice是不一样的东西,须要咱们加以区分的.
进程是具备必定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每一个进程都有本身的独立内存空间,不一样进程经过进程间通讯来通讯。因为进程比较重量,占据独立的内存,因此上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程本身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),可是它可与同属一个进程的其余的线程共享进程所拥有的所有资源。线程间通讯主要经过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程是一种用户态的轻量级线程,协程的调度彻底由用户控制。协程拥有本身的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其余地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操做栈则基本没有内核切换的开销,能够不加锁的访问全局变量,因此上下文的切换很是快。
互斥锁就是互斥变量mutex,用来锁住临界区的.
条件锁就是条件变量,当进程的某些资源要求不知足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行;读写锁,也相似,用于缓冲区等临界资源能互斥访问的。
一般有些公共数据修改的机会不多,但其读的机会不少。而且在读的过程当中会伴随着查找,给这种代码加锁会下降咱们的程序效率。读写锁能够解决这个问题。
注意:写独占,读共享,写锁优先级高
通常状况下,若是同一个线程前后两次调用lock,在第二次调用时,因为锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被本身占用着的,该线程又被挂起而没有机会释放锁,所以就永远处于挂起等待状态了,这叫作死锁(Deadlock)。 另一种状况是:若线程A得到了锁1,线程B得到了锁2,这时线程A调用lock试图得到锁2,结果是须要挂起等待线程B释放锁2,而这时线程B也调用lock试图得到锁1,结果是须要挂起等待线程A释放锁1,因而线程A和B都永远处于挂起状态了。
死锁产生的四个必要条件:
a. 预防死锁
能够把资源一次性分配:(破坏请求和保持条件)
而后剥夺资源:即当某进程新的资源未知足时,释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
b. 避免死锁
预防死锁的几种策略,会严重地损害系统性能。所以在避免死锁时,要施加较弱的限制,从而得到 较满意的系统性能。因为在避免死锁的策略中,容许进程动态地申请资源。于是,系统在进行资源分配以前预先计算资源分配的安全性。若这次分配不会致使系统进入不安全状态,则将资源分配给进程;不然,进程等待。其中最具备表明性的避免死锁算法是银行家算法。
c. 检测死锁
首先为每一个进程和每一个资源指定一个惟一的号码,而后创建资源分配表和进程等待表.
d. 解除死锁
当发现有进程死锁后,便应当即把它从死锁状态中解脱出来,常采用的方法有.
e. 剥夺资源
从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态.
f. 撤消进程
能够直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止.所谓代价是指优先级、运行代价、进程的重要性和价值等。
一般小对象过多会致使GC三色法消耗过多的GPU。优化思路是,减小对象分配.
同步访问共享数据是处理数据竞争的一种有效的方法.golang在1.1以后引入了竞争检测机制,能够使用 go run -race 或者 go build -race来进行静态检测。 其在内部的实现是,开启多个协程执行同一个命令, 而且记录下每一个变量的状态.
竞争检测器基于C/C++的ThreadSanitizer 运行时库,该库在Google内部代码基地和Chromium找到许多错误。这个技术在2012年九月集成到Go中,从那时开始,它已经在标准库中检测到42个竞争条件。如今,它已是咱们持续构建过程的一部分,当竞争条件出现时,它会继续捕捉到这些错误。
竞争检测器已经彻底集成到Go工具链中,仅仅添加-race标志到命令行就使用了检测器。
$ go test -race mypkg // 测试包 $ go run -race mysrc.go // 编译和运行程序 $ go build -race mycmd // 构建程序 $ go install -race mypkg // 安装程序
要想解决数据竞争的问题能够使用互斥锁sync.Mutex,解决数据竞争(Data race),也能够使用管道解决,使用管道的效率要比互斥锁高.
Channel是Go中的一个核心类型,能够把它当作一个管道,经过它并发核心单元就能够发送或者接收数据进行通信(communication),Channel也能够理解是一个先进先出的队列,经过管道进行通讯。
Golang的Channel,发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。并且Go的设计思想就是:不要经过共享内存来通讯,而是经过通讯来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,这固然是安全的。
开发高性能网络程序时,windows开发者们言必称Iocp,linux开发者们则言必称Epoll。你们都明白Epoll是一种IO多路复用技术,能够很是高效的处理数以百万计的Socket句柄,比起之前的Select和Poll效率提升了不少。
先简单了解下如何使用C库封装的3个epoll系统调用。
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
使用起来很清晰,首先要调用epoll_create
创建一个epoll对象。参数size是内核保证可以正确处理的最大句柄数,多于这个最大数时内核可不保证效果。 epoll_ctl能够操做上面创建的epoll,例如,将刚创建的socket
加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,再也不监控它等等。
epoll_wait
在调用时,在给定的timeout时间内,当在监控的全部句柄中有事件发生时,就返回用户态的进程。
从调用方式就能够看到epoll相比select/poll的优越之处是,由于后者每次调用时都要传递你所要监控的全部socket给select/poll系统调用,这意味着须要将用户态的socket列表copy到内核态,若是以万计的句柄会致使每次都要copy几十几百KB的内存到内核态,很是低效。而咱们调用epoll_wait
时就至关于以往调用select/poll,可是这时却不用传递socket句柄给内核,由于内核已经在epoll_ctl中拿到了要监控的句柄列表。
因此,实际上在你调用epoll_create
后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl
只是在往内核的数据结构里塞入新的socket句柄。
在内核里,一切皆文件。因此,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里建立一个file结点。固然这个file不是普通文件,它只服务于epoll。
epoll在被内核初始化时(操做系统启动),同时会开辟出epoll本身的内核高速cache区,用于安置每个咱们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是创建连续的物理内存页,而后在之上创建slab层,一般来说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
static int __init eventpoll_init(void) { ... ... /* Allocates slab cache used to allocate "struct epitem" items */ epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem), 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); /* Allocates slab cache used to allocate "struct eppoll_entry" */ pwq_cache = kmem_cache_create("eventpoll_pwq", sizeof(struct eppoll_entry), 0, EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); ... ... }
epoll的高效就在于,当咱们调用epoll_ctl
往里塞入百万个句柄时,epoll_wait
仍然能够飞快的返回,并有效的将发生事件的句柄给咱们用户。这是因为咱们在调用epoll_create
时,内核除了帮咱们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储之后epoll_ctl传来的socket外,还会再创建一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据便可。有数据就返回,没有数据就sleep,等到timeout时间到后即便链表没数据也返回。因此,epoll_wait很是高效。
并且,一般状况下即便咱们要监控百万计的句柄,大多一次也只返回不多量的准备就绪句柄而已,因此,epoll_wait仅须要从内核态copy少许的句柄到用户态而已,所以就会很是的高效!
然而,这个准备就绪list链表是怎么维护的呢?当咱们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上以外,还会给内核中断处理程序注册一个回调函数,告诉内核,若是这个句柄的中断到了,就把它放到准备就绪list链表里。因此,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此,一个红黑树,一张准备就绪句柄链表,少许的内核cache,就帮咱们解决了大并发下的socket处理问题。执行epoll_create
时,建立了红黑树和就绪链表,执行epoll_ctl时,若是增长socket句柄,则检查在红黑树中是否存在,存在当即返回,不存在则添加到树干上,而后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时马上返回准备就绪链表里的数据便可。
最后看看epoll独有的两种模式LT和ET。不管是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在之后调用epoll_wait时每次返回这个句柄,而ET模式仅在第一次返回。
当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时咱们调用epoll_wait
,会把准备就绪的socket拷贝到用户态内存,而后清空准备就绪list链表,最后,epoll_wait
须要作的事情,就是检查这些socket,若是不是ET模式(就是LT模式的句柄了),而且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。因此,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即便socket上的事件没有处理完,也是不会每次从epoll_wait返回的。
所以epoll比select的提升其实是一个用空间换时间思想的具体应用.对比阻塞IO的处理模型, 能够看到采用了多路复用IO以后, 程序能够自由的进行本身除了IO操做以外的工做, 只有到IO状态发生变化的时候由多路复用IO进行通知, 而后再采起相应的操做, 而不用一直阻塞等待IO状态发生变化,提升效率.
首先咱们先来了解下垃圾回收.什么是垃圾回收?
内存管理是程序员开发应用的一大难题。传统的系统级编程语言(主要指C/C++)中,程序开发者必须对内存当心的进行管理操做,控制内存的申请及释放。由于稍有不慎,就可能产生内存泄露问题,这种问题不易发现而且难以定位,一直成为困扰程序开发者的噩梦。如何解决这个头疼的问题呢?
过去通常采用两种办法:
内存泄露检测工具。这种工具的原理通常是静态代码扫描,经过扫描程序检测可能出现内存泄露的代码段。然而检测工具不免有疏漏和不足,只能起到辅助做用。
智能指针。这是 c++ 中引入的自动内存管理方法,经过拥有自动内存管理功能的指针对象来引用对象,是程序员不用太关注内存的释放,而达到内存自动释放的目的。这种方法是采用最普遍的作法,可是对程序开发者有必定的学习成本(并不是语言层面的原生支持),并且一旦有忘记使用的场景依然没法避免内存泄露。
为了解决这个问题,后来开发出来的几乎全部新语言(java,python,php等等)都引入了语言层面的自动内存管理 – 也就是语言的使用者只用关注内存的申请而没必要关心内存的释放,内存释放由虚拟机(virtual machine)或运行时(runtime)来自动进行管理。而这种对再也不使用的内存资源进行自动回收的行为就被称为垃圾回收。
经常使用的垃圾回收的方法:
这是最简单的一种垃圾回收算法,和以前提到的智能指针殊途同归。对每一个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被建立或被赋值给其余对象时引用计数自动加一。当引用计数为0时则当即回收对象。
这种方法的优势是实现简单,而且内存的回收很及时。这种算法在内存比较紧张和实时性比较高的系统中使用的比较普遍,如ios cocoa框架,php,python等。
可是简单引用计数算法也有明显的缺点:
一种简单的解决方法就是编译器将相邻的引用计数更新操做合并到一次更新;还有一种方法是针对频繁发生的临时变量引用不进行计数,而是在引用达到0时经过扫描堆栈确认是否还有临时对象引用而决定是否释放。等等还有不少其余方法,具体能够参考这里。
当对象间发生循环引用时引用链中的对象都没法获得释放。最明显的解决办法是避免产生循环引用,如cocoa引入了strong指针和weak指针两种指针类型。或者系统检测循环引用并主动打破循环链。固然这也增长了垃圾回收的复杂度。
标记-清除(mark and sweep)分为两步,标记从根变量开始迭代得遍历全部被引用的对象,对可以经过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操做,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操做)。这种方法解决了引用计数的不足,可是也有比较明显的问题:每次启动垃圾回收都会暂停当前全部的正常代码执行,回收是系统响应能力大大下降!固然后续也出现了不少mark&sweep算法的变种(如三色标记法)优化了这个问题。
java的jvm 就使用的分代回收的思路。在面向对象编程语言中,绝大多数对象的生命周期都很是短。分代收集的基本思想是,将堆划分为两个或多个称为代(generation)的空间。新建立的对象存放在称为新生代(young generation)中(通常来讲,新生代的大小会比 老年代小不少),随着垃圾回收的重复执行,生命周期较长的对象会被提高(promotion)到老年代中(这里用到了一个分类的思路,这个是也是科学思考的一个基本思路)。
所以,新生代垃圾回收和老年代垃圾回收两种不一样的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度很是快,比老年代快几个数量级,即便新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是由于大多数对象的生命周期都很短,根本无需提高到老年代。
Golang GC 时会发生什么?
Golang 1.5后,采起的是“非分代的、非移动的、并发的、三色的”标记清除垃圾回收算法。
golang 中的 gc 基本上是标记清除的过程:
gc的过程一共分为四个阶段:
整个进程空间里申请每一个对象占据的内存能够视为一个图,初始状态下每一个内存对象都是白色标记。
Golang gc 优化的核心就是尽可能使得 STW(Stop The World) 的时间愈来愈短。
详细的Golang的GC介绍能够参看Golang垃圾回收.
goroutine是Golang语言中最经典的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心。 goroutine使用方式很是的简单,只需使用go关键字便可启动一个协程,而且它是处于异步方式运行,你不须要等它运行完成之后在执行之后的代码。
go func()//经过go关键字启动一个协程来运行函数
协程:
协程拥有本身的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其余地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。 所以,协程能保留上一次调用时的状态(即全部局部状态的一个特定组合),每次过程重入时,就至关于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。 线程和进程的操做是由程序触发系统接口,最后的执行者是系统;协程的操做执行者则是用户自身程序,goroutine也是协程。
groutine能拥有强大的并发实现是经过GPM调度模型实现.
Go的调度器内部有四个重要的结构:M,P,S,Sched,如上图所示(Sched未给出).
调度实现:
从上图中能够看到,有2个物理线程M,每个M都拥有一个处理器P,每个也都有一个正在运行的goroutine。P的数量能够经过GOMAXPROCS()来设置,它其实也就表明了真正的并发度,即有多少个goroutine能够同时运行。
图中灰色的那些goroutine并无运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),Go语言里,启动一个goroutine很容易:go function 就行,因此每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪一个goroutine?)一个goroutine执行。
当一个OS线程M0陷入阻塞时,P转而在运行M1,图中的M1多是正被建立,或者从线程缓存中取出。
当MO返回时,它必须尝试取得一个P来运行goroutine,通常状况下,它会从其余的OS线程那里拿一个P过来, 若是没有拿到的话,它就把goroutine放在一个global runqueue里,而后本身睡眠(放入线程缓存里)。全部的P也会周期性的检查global runqueue并运行其中的goroutine,不然global runqueue上的goroutine永远没法执行。
另外一种状况是P所分配的任务G很快就执行完了(分配不均),这就致使了这个处理器P很忙,可是其余的P还有任务,此时若是global runqueue没有任务G了,那么P不得不从其余的P里拿一些G来执行。
一般来讲,若是P从其余的P那里要拿任务的话,通常就拿run queue的一半,这就确保了每一个OS线程都能充分的使用。
并行是指两个或者多个事件在同一时刻发生;并发是指两个或多个事件在同一时间间隔发生。
并行是在不一样实体上的多个事件,并发是在同一实体上的多个事件。在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群
并发偏重于多个任务交替执行,而多个任务之间有可能仍是串行的。而并行是真正意义上的“同时执行”。
并发编程是指在一台处理器上“同时”处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。并发编程的目标是充分的利用处理器的每个核,以达到最高的处理性能。
负载均衡Load Balance)是高可用网络基础架构的关键组件,一般用于将工做负载分布到多个服务器来提升网站、应用、数据库或其余服务的性能和可靠性。负载均衡,其核心就是网络流量分发,分不少维度。
负载均衡(Load Balance)一般是分摊到多个操做单元上进行执行,例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工做任务。
负载均衡是创建在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增长吞吐量、增强网络数据处理能力、提升网络的灵活性和可用性。
经过一个例子详细介绍:
在这里用户是直连到 web 服务器,若是这个服务器宕机了,那么用户天然也就没办法访问了。 另外,若是同时有不少用户试图访问服务器,超过了其能处理的极限,就会出现加载速度缓慢或根本没法链接的状况。
而经过在后端引入一个负载均衡器和至少一个额外的 web 服务器,能够缓解这个故障。 一般状况下,全部的后端服务器会保证提供相同的内容,以便用户不管哪一个服务器响应,都能收到一致的内容。
用户访问负载均衡器,再由负载均衡器将请求转发给后端服务器。在这种状况下,单点故障如今转移到负载均衡器上了。 这里又能够经过引入第二个负载均衡器来缓解。
那么负载均衡器的工做方式是什么样的呢,负载均衡器又能够处理什么样的请求?
负载均衡器的管理员能主要为下面四种主要类型的请求设置转发规则:
负载均衡器如何选择要转发的后端服务器?
负载均衡器通常根据两个因素来决定要将请求转发到哪一个服务器。首先,确保所选择的服务器可以对请求作出响应,而后根据预先配置的规则从健康服务器池(healthy pool)中进行选择。
由于,负载均衡器应当只选择能正常作出响应的后端服务器,所以就须要有一种判断后端服务器是否健康的方法。为了监视后台服务器的运行情况,运行状态检查服务会按期尝试使用转发规则定义的协议和端口去链接后端服务器。 若是,服务器没法经过健康检查,就会从池中剔除,保证流量不会被转发到该服务器,直到其再次经过健康检查为止。
负载均衡算法
负载均衡算法决定了后端的哪些健康服务器会被选中。 其中经常使用的算法包括:
若是你的应用须要处理状态而要求用户能链接到和以前相同的服务器。能够经过 Source 算法基于客户端的 IP 信息建立关联,或者使用粘性会话(sticky sessions)。
除此以外,想要解决负载均衡器的单点故障问题,能够将第二个负载均衡器链接到第一个上,从而造成一个集群。
LVS是 Linux Virtual Server 的简称,也就是Linux虚拟服务器。这是一个由章文嵩博士发起的一个开源项目,它的官方网站是LinuxVirtualServer如今 LVS 已是 Linux 内核标准的一部分。使用 LVS 能够达到的技术目标是:经过 LVS 达到的负载均衡技术和 Linux 操做系统实现一个高性能高可用的 Linux 服务器集群,它具备良好的可靠性、可扩展性和可操做性。 从而以低廉的成本实现最优的性能。LVS 是一个实现负载均衡集群的开源软件项目,LVS架构从逻辑上可分为调度层、Server集群层和共享存储。
LVS的基本工做原理:
LVS的组成:
LVS 由2部分程序组成,包括 ipvs
和 ipvsadm
。
详细的LVS的介绍能够参考LVS详解.
一般传统的项目体积庞大,需求、设计、开发、测试、部署流程固定。新功能须要在原项目上作修改。
可是微服务能够看作是对大项目的拆分,是在快速迭代更新上线的需求下产生的。新的功能模块会发布成新的服务组件,与其余已发布的服务组件一同协做。 服务内部有多个生产者和消费者,一般以http rest的方式调用,服务整体以一个(或几个)服务的形式呈现给客户使用。
微服务架构是一种思想对微服务架构咱们没有一个明确的定义,但简单来讲微服务架构是:
采用一组服务的方式来构建一个应用,服务独立部署在不一样的进程中,不一样服务经过一些轻量级交互机制来通讯,例如 RPC、HTTP 等,服务可独立扩展伸缩,每一个服务定义了明确的边界,不一样的服务甚至能够采用不一样的编程语言来实现,由独立的团队来维护。
Golang的微服务框架kit中有详细的微服务的例子,能够参考学习.
微服务架构设计包括:
微服务架构介绍详细的能够参考:
在分析分布式锁的三种实现方式以前,先了解一下分布式锁应该具有哪些条件:
分布式的CAP理论告诉咱们“任何一个分布式系统都没法同时知足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时知足两项。”因此,不少系统在设计之初就要对这三者作出取舍。在互联网领域的绝大多数的场景中,都须要牺牲强一致性来换取系统的高可用性,系统每每只须要保证“最终一致性”,只要这个最终时间是在用户能够接受的范围内便可。
一般分布式锁以单独的服务方式实现,目前比较经常使用的分布式锁实现有三种:
尽管有这三种方案,可是不一样的业务也要根据本身的状况进行选型,他们之间没有最好只有更适合!
基于数据库的实现方式的核心思想是:在数据库中建立一个表,表中包含方法名等字段,并在方法名字段上建立惟一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
建立一个表:
DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名', `desc` varchar(255) NOT NULL COMMENT '备注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
想要执行某个方法,就使用这个方法名向表中插入数据:
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');
由于咱们对method_name作了惟一性约束,这里若是有多个请求同时提交到数据库的话,数据库会保证只有一个操做能够成功,那么咱们就能够认为操做成功的那个线程得到了该方法的锁,能够执行方法体内容。
成功插入则获取锁,执行完成后删除对应的行数据释放锁:
delete from method_lock where method_name ='methodName';
注意:这里只是使用基于数据库的一种方法,使用数据库实现分布式锁还有不少其余的用法能够实现!
使用基于数据库的这种实现方式很简单,可是对于分布式锁应该具有的条件来讲,它有一些问题须要解决及优化:
一、由于是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,因此,数据库须要双机部署、数据同步、主备切换;
二、不具有可重入的特性,由于同一个线程在释放锁以前,行数据一直存在,没法再次成功插入数据,因此,须要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
三、没有锁失效机制,由于有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,因此,须要在表中新增一列,用于记录失效时间,而且须要有定时任务清除这些失效的数据;
四、不具有阻塞锁特性,获取不到锁直接返回失败,因此须要优化获取逻辑,循环屡次去获取。
五、在实施的过程当中会遇到各类不一样的问题,为了解决这些问题,实现方式将会愈来愈复杂;依赖数据库须要必定的资源开销,性能问题须要考虑。
选用Redis实现分布式锁缘由:
主要实现方式:
使用redis单机来作分布式锁服务,可能会出现单点问题,致使服务可用性差,所以在服务稳定性要求高的场合,官方建议使用redis集群(例如5台,成功请求锁超过3台就认为获取锁),来实现redis分布式锁。详见RedLock。
优势:性能高,redis可持久化,也能保证数据不易丢失,redis集群方式提升稳定性。
缺点:使用redis主从切换时可能丢失部分数据。
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个惟一文件名。基于ZooKeeper实现分布式锁的步骤以下:
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
优势:具有高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:由于须要频繁的建立和删除节点,性能上不如Redis方式。
上面的三种实现方式,没有在全部场合都是完美的,因此,应根据不一样的应用场景选择最适合的实现方式。
在分布式环境中,对资源进行上锁有时候是很重要的,好比抢购某一资源,这时候使用分布式锁就能够很好地控制资源。
首先思考下Etcd是什么?可能不少人第一反应多是一个键值存储仓库,却没有重视官方定义的后半句,用于配置共享和服务发现。
A highly-available key value store for shared configuration and service discovery.
实际上,etcd 做为一个受到 ZooKeeper 与 doozer 启发而催生的项目,除了拥有与之相似的功能外,更专一于如下四点。
可是这里咱们主要讲述Etcd如何实现分布式锁?
由于 Etcd 使用 Raft 算法保持了数据的强一致性,某次操做存储到集群中的值必然是全局一致的,因此很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。
保持独占即全部获取锁的用户最终只有一个能够获得。etcd 为此提供了一套实现分布式锁原子操做 CAS(CompareAndSwap)的 API。经过设置prevExist值,能够保证在多个节点同时去建立某个目录时,只有一个成功。而建立成功的用户就能够认为是得到了锁。
控制时序,即全部想要得到锁的用户都会被安排执行,可是得到锁的顺序也是全局惟一的,同时决定了执行顺序。etcd 为此也提供了一套 API(自动建立有序键),对一个目录建值时指定为POST动做,这样 etcd 会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还能够使用 API 按顺序列出全部当前目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值能够是表明客户端的编号。
在这里Ectd实现分布式锁基本实现原理为:
应用示例:
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)