LinkedIn的即时消息:在一台机器上支持几十万条长链接

最近咱们介绍了LinkedIn的即时通讯,最后提到了分型指标和读回复。为了实现这些功能,咱们须要有办法经过长链接来把数据从服务器端推送到手机或网页客户端,而不是许多当代应用所采起的标准的请求-响应模式。在这篇文章中会描述在咱们收到了消息、分型指标和读回复以后,如何马上把它们发往客户端。java

内容会包含咱们是如何使用Play框架和Akka Actor Model来管理长链接、由服务器主动发送事件的。咱们也会分享一些在生产环境中咱们是如何在服务器上作负载测试,来管理数十万条并发长链接的,还有一些心得。最后,咱们会分享在整个过程当中咱们用到的各类优化方法。浏览器

服务器发送事件服务器

服务器发送事件(Server-sent events,SSE)是一种客户端服务器之间的通讯技术,具体是在客户端向服务器创建起了一条普通的HTTP链接以后,服务器在有事件发生时就经过这条链接向客户端推送持续的数据流,而不须要客户端不断地发出后续的请求。客户端要用到EventSource接口来以文本或事件流的形式不断地接收服务器发送的事件或数据块,而没必要关闭链接。全部的现代网页浏览器都支持EventSource接口,iOS和安卓上也都有现成的库支持。网络

在咱们最先实现的版本中,咱们选择了基于Websockets的SSE技术,由于它能够基于传统的HTTP工做,并且咱们也但愿咱们采用的协议能够最大的兼容LinkedIn的广大会员们,他们会从各式各样的网络来访问咱们的网站。基于这样的理念,Websockets是一种能够实现双向的、全双工通讯的技术,能够把它做为协议的候选,咱们也会在合适的时候升级成它。多线程

Play框架和服务器发送的消息并发

咱们LinkedIn的服务器端程序使用了Play框架。Play是一个开源的、轻量级的、彻底异步的框架,可用于开发Java和Scala程序。它自己自带了对EventSource和Websockets的支持。为了能以可扩展的方式维护数十万条SSE长链接,咱们把Play和Akka结合起来用了。Akka可让咱们改进抽象模型,并用Actor Model来为每一个服务器创建起来的链接分配一个Actor。app

// Client A connects to the server and is assigned connectionIdA负载均衡

public Result listen() {框架

return ok(EventSource.whenConnected(eventSource -> {dom

   String connectionId = UUID.randomUUID().toString();

   // construct an Akka Actor with the new EventSource connection identified by a random connection identifier

   Akka.system().actorOf(

     ClientConnectionActor.props(connectionId, eventSource),

     connectionId);

   }));

}

上面的这段代码演示了如何使用Play的EventSource API来在程序控制器中接受并创建一条链接,再将它置于一个Akka Actor的管理之下。这样Actor就开始负责管理这个链接的整个生命周期,在有事件发生时把数据发送给客户端就被简化成了把消息发送给Akka Actor。

// User B sends a message to User A

// We identify the Actor which manages the connection on which User A is connected (connectionIdA)

ActorSelection actorSelection = Akka.system().actorSelection("akka://application/user/" + connectionIdA);

// Send B's message to A's Actor

actorSelection.tell(new ClientMessage(data), ActorRef.noSender());

请注意惟一与这条链接交互的地方就是向管理着这条链接的Akka Actor发送一条消息。这很重要,所以才能使Akka具备异步、非阻塞、高性能和为分布式系统而设计的特性。相应地,Akka Actor处理它收到的消息的方式就是转发给它管理的EventSource链接。

public class ClientConnectionActor extends UntypedActor {

public static Props props(String connectionId, EventSource eventSource) {

   return Props.create(ClientConnectionActor.class, () -> new ClientConnectionActor(connectionId, eventSource));

}

public void onReceive(Object msg) throws Exception {

   if (msg instanceof ClientMessage) {

     eventSource.send(event(Json.toJson(clientMessage)));

   }

}

}

就是这样了。用Play框架和Akka Actor Model来管理并发的EventSource链接就是这么简单。

可是在系统上规模以后这也能工做得很好吗?读读下面的内容就知道答案了。

使用真实生产环境流量作压力测试

全部的系统最终都是要用真实生产流量来考验一下的,可真实生产流量又不是那么容易复制的,由于你们能够用来模拟作压力测试的工具并很少。但咱们在部署到真实生产环境以前,又是如何用真实的生产流量来作测试的呢?在这一点上咱们用到了一种叫“暗地启动”的技术,在咱们下一篇文章中会详细讨论一下。

为了让这篇文章只关注本身的主题,让咱们假设咱们已经能够在咱们的服务器集群中产生真实的生产压力了。那么测试系统极限的一个有效方法就是把导向一个单一节点的压力不断加大,以此让整个生产集群在承受极大压力时所该暴露的问题极早暴露出来。

经过这样的办法以及其它的辅助手段,咱们发现了系统的几处限制。下面几节就讲讲咱们是如何经过几处简单的优化,让单台服务器最终能够支撑数十万条链接的。

限制一:一个Socket上的处于待定状态的链接的最大数量

在一些最先的压力测试中咱们就常碰到一个奇怪的问题,咱们没办法同时创建不少个链接,大概128个就到上限了。请注意服务器是能够很轻松地处理几千个并发链接的,但咱们却作不到向链接池中同时加入多于128条链接。在真实的生产环境中,这大概至关于有128个会员同时在向同一个服务器初始化链接。

作了一番研究以后,咱们发现了下面这个内核参数:

net.core.somaxconn

这个内核参数的意思就是程序准备接受的处于等待创建链接状态的最大TCP链接数量。若是在队列满的时候来了一条链接创建请求,请求会直接被拒绝掉。在许多的主流操做系统上这个值都默认是128。

在“/etc/sysctl.conf”文件中把这个值改大以后,就解决了在咱们的Linux服务器上的“拒绝链接”问题了。

请注意Netty 4.x版本及以上在初始化Java ServerSocket时,会自动从操做系统中取到这个值并直接使用。不过,若是你也想在应用程序的级别配置它,你能够在Play程序的配置参数中这样设置:

play.server.netty.option.backlog=1024

限制二:JVM线程数量

在让比较大的生产流量第一次压向咱们的服务器以后,没过几个小时咱们就收到了告警,负载均衡器开始没办法连上一部分服务器了。作了进一步调查以后,咱们在服务器日志中发现了下面这些内容:

java.lang.OutOfMemoryError: unable to create new native thread

下面关于咱们服务器上JVM线程数量的图也证明了咱们当时出现了线程泄露,内存也快耗尽了。

咱们把JVM进程的线程状态打出来查看了一下,发现了许多处于以下状态的睡眠线程:

"Hashed wheel timer #11327" #27780 prio=5 os_prio=0 tid=0x00007f73a8bba000 nid=0x27f4 sleeping[0x00007f7329d23000]    java.lang.Thread.State: TIMED_WAITING (sleeping)

   at java.lang.Thread.sleep(Native Method)

   at org.jboss.netty.util.HashedWheelTimer$Worker.waitForNextTick(HashedWheelTimer.java:445)

       at org.jboss.netty.util.HashedWheelTimer$Worker.run(HashedWheelTimer.java:364)

   at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)

   at java.lang.Thread.run(Thread.java:745)

通过进一步调查,咱们发现缘由是LinkedIn对Play框架的实现中对于Netty的空闲超时机制的支持有个BUG,而原本的Play框架代码中对每条进来的链接都会相应地建立一个新的HashedWheelTimer实例。这个补丁很是清晰地说明了这个BUG的缘由。

若是你也碰上了JVM线程限制的问题,那颇有可能在你的代码中也会有一些须要解决的线程泄露问题。可是,若是你发现其实你的全部线程都在干活,并且干的也是你指望的活,那有没有办法改改系统,容许你建立更多线程,接受更多链接呢?

一如既往,答案仍是很是有趣的。要讨论有限的内存与在JVM中能够建立的线程数之间的关系,这是个有趣的话题。一个线程的栈大小决定了能够用来作静态内存分配的内存量。这样,理论上的最大线程数量就是一个进程的用户地址空间大小除以线程的栈大小。不过,实际上JVM也会把内存用于堆上的动态分配。在用一个小Java程序作了一些简单实验以后,咱们证明了若是堆分配的内存多,那栈能够用的内存就少。这样,线程数量的限制会随着堆大小的增长而减小。

结论就是,若是你想增长线程数量限制,你能够减小每一个线程使用的栈大小(-Xss),也能够减小分配给堆的内存(-Xms,-Xmx)。

限制三:临时端口耗尽

事实上咱们倒没有真的达到这个限制,但咱们仍是想把它写在这里,由于当你们想在一台服务器上支持几十万条链接时一般都会达到这个限制。每当负载均衡器连上一个服务器节点时,它都会占用一个临时端口。在这个链接的生命周期内,这个端口都会与它相关联,所以叫它“临时的”。当链接被终止以后,临时端口就会被释放,能够重复使用。但是长链接并不象普通的HTTP链接同样会终止,因此在负载均衡器上的可用临时端口池就会最终被耗尽。这时候的状态就是没有办法再创建新链接了,由于全部操做系统能够用来创建新链接的端口号都已经用掉了。在较新的负载均衡器上解决临时端口耗尽问题的方法有不少,但那些内容就不在本文范围以内了。

很幸运咱们每台负载均衡器均可以支持高达25万条链接。不过,但你达到这个限制的时候,要和管理你的负载均衡器的团队一块儿合做,来提升负载均衡器与你的服务器节点之间的开放链接的数量限制。

限制四:文件描述符

当咱们在数据中心中搭建起来了16台服务器,而且能够处理很可观的生产流量以后,咱们决定测试一下每台服务器所能承受的长链接数量的限制。具体的测试方法是一次关掉几台服务器,这样负载均衡器就会把愈来愈多的流量导到剩下的服务器上了。这样的测试产生了下面这张美妙的图,表示了每台服务器上咱们的服务器进程所使用的文件描述符数量,咱们内部给它起了个花名:“毛毛虫图”。

文件描述符在Unix一类操做系统中都是一种抽象的句柄,与其它不一样的是它是用来访问网络Socket的。不出意外,每台服务器上支撑的持久链接越多,那所须要分配的文件描述符也越多。你能够看到,当16台服务器只剩2台时,它们每一台都用到了2万个文件描述符。当咱们把它们之中再关掉一台时,咱们在剩下的那台上看到了下面的日志:

java.net.SocketException: Too many files open

在把全部的链接都导向惟一的一台服务器时,咱们就会达到单进程的文件描述符限制。要查看一个进程可用的文件描述符限制数,能够查看下面这个文件的“Max open files”的值。

$ cat /proc/<pid>/limits

Max open files            30000

以下面的例子,这个能够加大到20万,只须要在文件/etc/security/limits.conf中添加下面的行:

<process username>  soft nofile 200000

<process username>  hard nofile 200000

注意还有一个系统级的文件描述符限制,能够调节文件/etc/sysctl.conf中的内核参数:

fs.file-max

这样咱们就把全部服务器上面的单进程文件描述符限制都调大了,因此你看,咱们如今每台服务器才能轻松地处理3万条以上的链接。

限制五:JVM

下一步,咱们重复了上面的过程,只是把大约6万条链接导向剩下的两台服务器中幸存的那台时,状况又开始变糟了。已分配的文件描述符数,还有相应的活跃长链接的数量,都一会儿大大下降,而延迟也上升到了不可接受的地步。

通过进一步的调查,咱们发现缘由是咱们耗尽了4GB的JVM堆空间。这也造就了下面这张罕见的图,显示每次内存回收器所能回收的堆空间都愈来愈少,直到最后全都用光了。

咱们在数据中心的即时消息服务里用了TLS处理全部的内部通讯。实践中,每条TLS链接都会消耗JVM的约20KB的内存,并且还会随着活跃的长链接数量的增长而增涨,最终致使如上图所示的内存耗尽状态。

咱们把JVM堆空间的大小调成了8GB(-Xms8g, -Xmx8g)并重跑了测试,不断地向一台服务器导过去愈来愈多的链接,最终在一台服务器处理约9万条链接时内存再次耗尽,链接数开始降低。

事实上,咱们又把堆空间耗尽了,这一次是8G。

处理能力却是历来都没用达到过极限,由于CPU利用率一直低于80%。

咱们接下来是怎么测的?由于咱们每台服务器都是很是奢侈地有着64GB内存的配置,咱们直接把JVM堆大小调成了16GB。从那之后,咱们就再也没在性能测试中达到这个内存极限了,也在生产环境中成功地处理了10万条以上的并发长链接。但是,在上面的内容中你已经看到,当压力继续增大时咱们还会碰上某些限制的。你以为会是什么呢?内存?CPU?请经过个人Twitter帐号@agupta03告诉我你的想法。

结论

在这篇文章中,咱们简单介绍了LinkedIn为了向即时通讯客户端推送服务器主动发送的消息而要保持长链接的状况。事实也证实,Akka的Actor Model在Play框架中管理这些链接是很是好用的。

不断地挑战咱们的生产系统的极限,并尝试提升它,这样的事情是咱们在LinkedIn最喜欢作的。咱们分享了在咱们在咱们通过重重挑战,最终让咱们的单台即时通讯服务器能够处理几十万条长链接的过程当中,咱们碰到的一些有趣的限制和解决方法。咱们把这些细节分享出来,这样你就能够理解每一个限制每种技术背后的缘由所在,以即可以压榨出你的系统的最佳性能。但愿你能从咱们的文章中借鉴到一些东西,而且应用到你本身的系统上。

 

from:http://mp.weixin.qq.com/s?__biz=MzA5NzkxMzg1Nw==&mid=2653161464&idx=1&sn=8a31ae325c547a2eeb9dc20c1ab25bdc&chksm=8b493a96bc3eb3800ec78dc94f925f67e14128a1db0a8a2fb4c6186cbf381e311000721d640b#rd

相关文章
相关标签/搜索