第五名
6983分

因为工做缘由实在太忙,比赛只有周末的时间能够突击,下一篇我会抽空整理下复赛《单机百万消息队列的存储设计》的思路方案分享给你们,我的感受实现方案上也是决赛队伍中比较特别的。linux

What's Service Mesh?

Service Mesh另辟蹊径,实现服务治理的过程不须要改变服务自己。经过以proxy或sidecar形式部署的 Agent,全部进出服务的流量都会被Agent拦截并加以处理,这样一来微服务场景下的各类服务治理能力均可以经过Agent来完成,这大大下降了服务化改造的难度和成本。并且Agent做为两个服务之间的媒介,还能够起到协议转换的做用,这可以使得基于不一样技术框架和通信协议建设的服务也能够实现互联互通,这一点在传统微服务框架下是很难实现的。nginx

下图是一个官方提供的一个评测框架,整个场景由5个Docker 实例组成(蓝色的方框),分别运行了 etcd、Consumer、Provider服务和Agent代理。Provider是服务提供者,Consumer是服务消费者,Consumer消费Provider提供的服务。Agent是Consumer和Provider服务的代理,每一个Consumer或 Provider都会伴随一个Agent。etcd是注册表服务,用来记录服务注册信息。从图中能够看出,Consumer 与Provider 之间的通信并非直接进行的,而是通过了Agent代理。这看似多余的一环,却在微服务的架构演进中带来了重要的变革。git

有关Service Mesh的更多内容,请参考下列文章:github

赛题要求

  • 服务注册和发现
  • 协议转换(这也是实现不一样语言、不一样框架互联互通的关键)
  • 负载均衡
  • 限流、降级、熔断、安全认证(不做要求)
最后Agent Proxy的资源占用率必定要小,由于Agent与服务是共生的,服务一旦失去响应,Agent即便拥有再好的性能也是没有意义的。

Why Golang?

我的认为关于Service Mesh的选型必定会在Cpp和Golang之间,这个要参考公司的技术栈。若是追求极致的性能仍是首选Cpp,这样能够避免Gc问题。由于Service Mesh链路相比传统Rpc要长,Agent Proxy须要保证轻量、稳定、性能出色。docker

关于技术选型为何是Golang?这里不只仅是为了当作一次锻炼本身Golang的机会,固然还出于如下一些缘由:json

  • 一些大厂的经验沉淀,好比蚂蚁Sofa Mesh,新浪Motan Mesh等。
  • K8s、docker在微服务领域很火,并且之后Agent的部署必定依托于k8s,因此Go是个不错的选择,亲和度高。
  • Go有协程,有高质量的网络库,高性能方面应该占优点。

优化点剖析

官方提供了一个基于Netty实现的Java Demo,因为是阻塞版本,因此性能并不高,固然这也是对Java选手的一个福音了,能够快速上手。其余语言相对起步较慢,所有都要本身从新实现。安全

无论什么语言,你们的优化思路大部分都是同样的。这里分享一下Kirito徐靖峰很是细致的思路总结(Java版本):天池中间件大赛dubboMesh优化总结(qps从1000到6850),你们能够做为参考。

下面这张图基本涵盖了在整个agent全部优化的工做,图中绿色的箭头都是用户能够本身实现的。

复制代码

ForBlock: for { httpReqList[reqCount] = req agentReqList[reqCount] = &AgentRequest{ Interf: req.interf, Method: req.callMethod, ParamType: ParamType_String, Param: []byte(req.parameter), } reqCount++ if reqCount == *config.HttpMergeCountMax { break } select { case req = <-workerQueue: default: break ForBlock } } ```

if err = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_NODELAY, *config.Nodelay); err != nil {
	logger.Error("cannot disable Nagle's algorithm", err)
}

if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_SNDBUF, *config.TCPSendBuffer); err != nil {
	logger.Error("set sendbuf fail", err)
}
if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, *config.TCPRecvBuffer); err != nil {
	logger.Error("set recvbuf fail", err)
}
复制代码

网络辛酸史 —— (预热赛256并发压测4400~4500)

Go由于有协程以及高质量的网络库,协程切换代价较小,因此大部分场景下Go推荐的网络玩法是每一个链接都使用对应的协程来进行读写。

这个版本的网络模型也取得了比较客观的成绩,QPS最高大约在4400~4500。对这个网络选型简单作下总结:

  • Go由于有goroutine,能够采用多协程来解决并发问题。
  • 在linux上Go的网络库也是采用的epoll做为最底层的数据收发驱动。
  • Go网络底层实现中一样存在“上下文切换”的工做,只是切换工做由runtime调度器完成。

网络辛酸史 —— (正式赛512并发压测)

cpu的资源占用率也是比较高的,高达约100%

得到高分的秘诀分析:

  • Consumer Agent压力繁重,给Consumer Agent减压。
  • 因为Consumer的性能不好,Consumer以及Consumer Agent共生于一个Docker实例(4C 8G)中,只有避免资源争抢,才能达到极致性能。
  • Consumer在压测过程当中Cpu占用高达约350%。
  • 为了不与Consumer争抢资源,须要把Consumer Agent的资源利用率降到极致。
尽量下降Consumer Agent的资源开销

a. 优化方案1:协程池 + 任务队列(废弃)

这是一个比较简单、经常使用的优化思路,相似线程池。虽然有所突破,可是并无达到理想的效果,cpu仍是高达约70~80%。Goroutine虽然开销很小,毕竟高并发状况下仍是有必定上下文切换的代价,只能想办法再去寻找一些性能的突破。

通过慎重思考,我最终仍是决定尝试采用相似netty的reactor网络模型

b. 优化方案2:Reactor网络模型

选型以前咨询了几位好朋友,都是遭到一顿吐槽。固然他们无法理解我只有不到50%的Cpu资源能够利用的困境,最终仍是毅然决然地走向这条另类的路。

通过一番简单的调研,我找到了一个看上去还挺靠谱(Github Star2000, 没有一个PR)的开源第三方库evio,可是真正实践下来遇到太多坑,并且功能很是简易。不由感慨Java拥有Netty真的是太幸福了!Java取得成功的缘由在于它的生态如此成熟,Go语言这方面还须要时间的磨炼,高质量的资源太少了。

固然不能全盘否认evio,它能够做为一个学习网络方面很好的资源。先看Github上一个简单的功能介绍:

evio is an event loop networking framework that is fast and small. It makes direct epoll and kqueue syscalls rather than using the standard Go net package, and works in a similar manner as libuv and libevent.
复制代码
说明:关于kqueue是FreeBSD上的一种的多路复用机制,推荐学习。

为了可以达到极致的性能,我对evio进行了大量改造:

  • 支持主动链接(默认只支持被动链接)
  • 支持多种协议
  • 减小无效的唤醒次数
  • 支持异步写,提升吞吐率
  • 修复Linux下诸多bug形成的性能问题
6700+

c. 复用EventLoop

对优化以后的网络模式再进行一次梳理(见下图):

若是入站的io协程和出站的io协程使用相同的协程,能够进一步下降Cpu切换的开销复用EventLoop
func CreateAgentEvent(loops int, workerQueues []chan *AgentRequest, processorsNum uint64) *Events {
	events := &Events{}
	events.NumLoops = loops

	events.Serving = func(srv Server) (action Action) {
		logger.Info("agent server started (loops: %d)", srv.NumLoops)
		return
	}

	events.Opened = func(c Conn) (out []byte, opts Options, action Action) {
		if c.GetConnType() != config.ConnTypeAgent {
			return GlobalLocalDubboAgent.events.Opened(c)
		}
		lastCtx := c.Context()
		if lastCtx == nil {
			c.SetContext(&AgentContext{})
		}

		opts.ReuseInputBuffer = true

		logger.Info("agent opened: laddr: %v: raddr: %v", c.LocalAddr(), c.RemoteAddr())
		return
	}

	events.Closed = func(c Conn, err error) (action Action) {
		if c.GetConnType() != config.ConnTypeAgent {
			return GlobalLocalDubboAgent.events.Closed(c, err)
		}
		logger.Info("agent closed: %s: %s", c.LocalAddr(), c.RemoteAddr())
		return
	}

	events.Data = func(c Conn, in []byte) (out []byte, action Action) {
		if c.GetConnType() != config.ConnTypeAgent {
			return GlobalLocalDubboAgent.events.Data(c, in)
		}

		if in == nil {
			return
		}
		agentContext := c.Context().(*AgentContext)

		data := agentContext.is.Begin(in)

		for {
			if len(data) > 0 {
				if agentContext.req == nil {
					agentContext.req = &AgentRequest{}
					agentContext.req.conn = c
				}
			} else {
				break
			}

			leftover, err, ready := parseAgentReq(data, agentContext.req)

			if err != nil {
				action = Close
				break
			} else if !ready {
				data = leftover
				break
			}

			index := agentContext.req.RequestID % processorsNum
			workerQueues[index] <- agentContext.req
			agentContext.req = nil
			data = leftover
		}
		agentContext.is.End(data)
		return
	}
	return events
}
复制代码

复用eventloop获得了一个比较稳健的成绩提高,每一个阶段的eventloop的资源数都设置为1个,最终512并发压测下cpu资源占用率约50%。

Go语言层面的一些优化尝试

最后阶段只能丧心病狂地寻找一些细节点,因此也对语言层面作了一些尝试:

  • Ringbuffer来替代Go channel实现任务分发

RingBuffer在高并发任务分发的场景中比Channel性能有小幅度提高,可是站在工程的角度,我的仍是推荐Go channel这种更加优雅的作法。

  • Go自带的encoding/json包是基于反射实现的,性能是个诟病

使用字符串本身拼装Json数据,这样压测的数据越多,节省的时间越多。

runtime.LockOSThread()
defer runtime.UnlockOSThread()
复制代码

总结

性能优化离不开的一些套路:异步、去锁、复用、零拷贝、批量等

最后抛出几个想继续探讨的Go网络问题,和你们一块儿讨论,有经验的朋友还但愿能指点一二:

  1. 在资源稀少的状况下,处理高并发请求的网络模型你会怎么选型?(假设并发为1w长链接或者短链接)
  2. 百万链接的规模下又将如何选型?

亚普的技术轮子