-
目录前言... 1从一次页面请求说起... 3浏览器运行机制... 3浏览器是多进程的... 3浏览器内核是多线程的... 4HTTP缓存... 6强制缓存... 7对比缓存... 7运行流程... 8DNS域名解析... 10TCP连接... 12HTTP请求与响应... 12页面渲染... 15关闭TCP连接... 16结束语... 17 前言 经常听到有人说,前端很简单,就是画画界面。好像也不无道理,只是还没有真正的去了解它,就像跑步一样,也确实很简单,但是你百米跑进十秒试试。只有对每一个动作都充分了解,才能把速度提上去,而且每进步一秒,都要付出更大的努力。简单的事情做到极致,就不简单了。其实前端并非想象中的简单,也有自己的一套庞大生态体系。下面是一张最新的前端知识体系图,长路漫漫,还需要潜心修炼才行。本文旨在带领大家进入前端的世界,不会讲解具体的HTML/CSS/JS的语法,也不涉及前端框架,而是帮助大家了解前端的运行机制,准确的说是浏览器页面加载的机制。只有充分的了解了前端,才能更好的优化我们的Web系统。对前端不感兴趣也没关系,就当是了解一下,后续还有《Web之旅-后端篇》带给大家。本文中部分内容参考《WebKit技术内幕》一书,感兴趣的童鞋可以研究一下。从一次页面请求说起先考大家一个问题:从打开一个URL到看到页面,中间都经历了什么?输入URL,加载资源,显示页面?没毛病,不过从前端角度来说,一般会经历下面几个过程:1) 在浏览器中输入URL并回车(当然,也可以直接点击URL超链接打开)2) 浏览器查找当前URL是否存在缓存,并比较缓存是否过期3) DNS解析URL获取对应IP4) 根据IP建立TCP连接(三次握手)5) 发送HTTP请求6) 服务器处理请求,浏览器接收HTTP响应7) 渲染页面8) 关闭TCP连接(四次挥手)那么这里的每一步又是怎么实现的呢?作为一名IT工作者,可能有些过程大家已经耳濡目染了,那就可以选择性的跳过啦。如果还不太了解,那也没关系,下面就开始我们的Web之旅-前端篇。首先第一步,输入URL?不不不,是打开浏览器。是的,你没看错,作为此次Web之旅的主办单位及赞助商,在发车之前,浏览器的运行机制,确定不先了解一下么。浏览器运行机制首先申明,下面要讲述的是Chrominum浏览器的架构模型,不排除个别浏览器特立独行哦。浏览器是多进程的 当我们启动浏览器的时候,默认就启动了一个进程(进程和线程的概念就不再科普了),作为主进程,并从系统中获取资源(CPU、内存)。每当我们打开一个tab页签,就会另起一个新的进程(某些情况下多个tab会合并进程)。那么浏览器到底包含哪些进程呢?下面列举了一些主要进程:1) Browser进程:浏览器的主进程(负责协调、主控),只有一个。a. 负责浏览器界面显示,与用户交互。如前进,后退等b. 负责各个页面的管理,创建和销毁其他进程c. 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上d. 网络资源的管理,下载等2) 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建3) GPU进程:最多一个,用于3D绘制等4) 浏览器内核(渲染进程)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要用来进行页面渲染、脚本执行以及事件的处理那么Browser进程和浏览器内核(Renderer进程)的通信过程是怎样的呢?引用Chromium浏览器的代码层次结构图:从WebKit接口层到用户界面的路径1) Browser进程收到用户请求,首先需要获取页面内容(比如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程。2) Renderer进程的Renderer接口收到消息,简单解释后,调用相应的WebKit接口层,进行渲染,并将Webkit处理后的结果发送回去3) Browser进程接收到结果并将结果绘制出来作为前端开发,我们需要重点关注一下浏览器内核进程,也是渲染页面所需要的。浏览器内核是多线程的 每一个tab页面可以看作是一个浏览器内核进程,内核进程是多线程的,主要包含下面几大类线程:1) GUI渲染线程a. 负责渲染浏览器界面,解析HTML(SVG/XHTML)和CSS,构建DOM树和Render树,并进行布局和绘制等操作。b. 当界面需要重绘(Repaint)或由于某种操作引发重排(Reflow)时,该线程就会执行c. 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。2) JS引擎线程a. 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)b. JS引擎线程负责解析Javascript脚本,执行代码。c. JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(Renderer进程)中无论什么时候都只有一个JS线程在运行JS程序d. 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染阻塞。3) 事件触发线程a. 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)b. 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中c. 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理d. 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)4) 定时触发器线程a. 传说中的setInterval与setTimeout所在线程b. 浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确性。因此通过单独线程来计时并触发定时任务(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)c. 注意,W3C在HTML标准中规定,要求setTimeout中低于4ms的时间间隔算为4ms。5) 异步http请求线程a. XMLHttpRequest在连接后是通过浏览器新开一个线程进行异步请求b. 当检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理。c. 在发起了一个异步请求时,http请求线程则负责去请求服务器,有了响应以后,事件触发线程再把回调函数放到事件队列当中。d. 大部分浏览器支持6个左右的并发请求(并发数是针对同一域名的),超过限制数目的请求会被阻塞。浏览器这块先介绍到这里,至于浏览器内核(渲染进程)具体是怎么工作的,各线程之间是怎么配合的,我们到页面渲染那一章节再详细介绍。好了,参观完浏览器,现在是不是可以出发了呢?还不行,下面我们要验票了,已经参加过web之旅的童鞋可能就不需要再次参加了。HTTP缓存 HTTP缓存有多种规则,根据是否需要向服务器发起请求,可以分为两大类:强制缓存和对比缓存。强制缓存如果生效,不再需要和服务器发生交互,而对比缓存不管是否生效,都需要与服务端发生交互。两类缓存规则可以同时存在,而强制缓存优先级高于对比缓存,也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存规则。和缓存规则相关的配置信息均存在于Http报文的请求头里。强制缓存强制缓存规则主要通过Cache-Control字段进行描述,而早先的HTTP1.0中使用的是Expires字段,以及古老的IE中支持的Pragma字段(这里就不细说了),为了保证兼容性,这些旧的字段还是会用到的。1) Expires:值为服务端返回的到期时间,是一个绝对时间。浏览器检查当前时间,如果还没到失效时间就直接使用缓存文件。估计大家已经想到了一个问题:当服务器时间与客户端时间不一致时,这种方式就会产生误差。所以从HTTP1.1版开始,使用Cache-Control来代替Expires。2) Cache-Control:用于定义所有的缓存机制都必须遵循的缓存指示,这些指示是一些特定的指令,包括public、private、no-cache、no-store、max-age、s-maxage以及must-revalidate等。其中的max-age保存一个相对时间。例如Cache-Control: max-age = 484200,表示浏览器收到文件后,缓存在484200s内均有效。当同时设置了上述三种规则时(包括Pragma),浏览器按照Cache-Control、Expires、Pragma的顺序进行覆盖处理。对比缓存强制缓存决定客户端是否向服务器发送请求,缓存判断生效,则直接从本地缓存读取数据,缓存失效,则需要向服务器发送请求。那么这时的资源是否真的已经发生了变化呢?如果没有变化,再把数据重传一遍岂不是会浪费带宽和时间。为了让客户端与服务器之间能实现缓存文件是否更新的验证,提升缓存的复用率,HTTP1.1新增了两种规则:Last-Modified以及ETag。一个是对比最后更新时间,一个是对比内容变化(如MD5值)。1) Last-Modifie:服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在响应头上一起返回给客户端。客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间一致,则说明该资源没有被修改过,返回304状态码知会客户端直接使用本地缓存即可。否则以常规GET 200回包形式将新的资源(包括新的Last-Modified时间)返回给客户端。相关报文头字段:If-Modified-Since 和 If-Unmodified-Since。可能细心的小伙伴又要问了,如果只是更新了时间,内容没有发生变化呢?为了解决这个困扰,HTTP1.1还增加了ETag规则。2) Etag:服务器会通过某种算法,给资源计算出一个唯一标志符(比如md5值),在把资源返回给客户端的时候,会在响应头加上“ETag: 唯一标识符”一起返回给客户端。客户端保留该 ETag 字段,并在下一次请求时将其一并带给服务器。服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器判断ETag是一致的,直接返回304状态码知会客户端直接使用本地缓存即可。否则以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端相关报文头字段:If-None-Match 和 If-Match3) 如果同时指定了Last-Modifie和ETag字段,那么需要同时校验这两个规则。运行流程讲了这么多概念,是不是有点大脑缺氧呢,这到底是怎样一个运行流程呢?下面就通过一张图来回顾一下HTTP的缓存机制: 到这里,大家对HTTP的缓存机制应该有了清晰的认识了吧,下面我们放松一下,探讨一个比较有趣的话题:你知道浏览器里的回车(转到)和刷新(F5)有什么区别么?对,按键不同,还有呢?简单的说:1) 回车保证了原有的缓存规则不变,即在 强制缓存有效的时候,是不会去请求服务器的,打开调试看到的请求也只是伪造的,比如 谷歌浏览器可能显示 200(from cached),其实并没有发起实际的请求,而是直接读取了本地缓存。2) F5则会跳过 强制缓存,只有Last-Modified/ETag起作用,如果服务器比较后返回 304,再去本地缓存中读取,返回200则使用服务器返回的资源。但是浏览器依然会保留之前的一些变量的值,所以可能会造成页面刷新后,网页出现错误,甚至打不开的情况。3) 那么如果想完全抛弃本地缓存怎么办?Ctrl+F5!此时请求头会发送 Cache-Control: no-cache,告诉浏览器,我不要缓存中的文件了,直接从服务器获取文件,此时缓存完全失效。是不是很有意思呢,一般人我都不告诉他的。好了,那些已经参加过Web前端之旅的乘客,你已经被浏览器的强制缓存检测到了,请前往浏览器工作室进行页面的渲染。剩下的乘客请前往候车室,并耐心在座位等待。什么?为什么还不出发?因为导游要确定一下行程,你说去www.webzhilv.com ,除非是老司机,要不怎么知道具体在哪呢?没关系,我们正在查询线路网系统:DNS域名解析。DNS域名解析 关于域名解析,想必大部分童鞋都有所了解的,我们在地址栏输入的域名并不是最后资源所在的真实位置,域名只是与IP地址的一个映射。网络服务器的IP地址那么多,我们不可能去记一串串的数字,因此域名就产生了,域名解析的过程实际是将域名还原为IP地址的过程。 域名解析的过程,大体会经历下面几个步骤:1) 浏览器检查自身的缓存中有没有这个域名对应的解析过的IP地址,如果有,就使用这个IP。浏览器域名缓存在数量和时间(TTL)上都是有限制的,合理的设置才能保证域名的正常解析。小窍门:在浏览器中输入chrome://net-internals/#dns可以查看当前缓存的域名信息哦。2) 查找操作系统缓存即本地hosts文件,如果hosts文件中有相应的域名映射记录,则使用对应的IP地址。温馨提示:要当心恶意程序修改你的hosts文件来把特定的域名解析到它指定的IP地址上,导致这些域名被劫持。3) 当前面步骤都无法完成域名解析时,就要请求本地域名解析器(LDNS)来解析了。那么怎么知道域名服务器在哪呢?这个就是操作系统网络配置中的DNS服务器地址了。4) 如果LDNS仍然没有解析到,就直接到Root Service域名解析器请求解析。5) 根域名服务器返回给本地域名服务器一个所查询余的主域名服务器(gTLDServer)地址。gTLD是国际顶级域名服务器,如:.com/.cn/.org等,全球只有13台左右。6) 本地域名服务器再向gTLD服务器发送请求。7) 接收请求的gTLD服务器查找并返回此域名对应的Name Server域名服务器的地址,这个Name Server通常就是你注册的域名服务器(如你的域名供应商)。8) Name Server域名服务器会查询存储的域名和IP的映射关系表,正常情况下都根据域名得到目标IP记录,连同一个TTL值返回给DNS Server域名服务器。9) 返回该域名对应的IP和TTL值,本地域名服务器会缓存这个域名和IP的对应关系,缓存的时间有TTL值控制。10) 把解析的结果返回给用户,用户根据TTL值缓存在浏览器缓存中,域名解析过程结束。经过这么一系列查询,总算知道目的地在哪了,可以出发了吧。什么?还不行?奔溃中...为了大家安全着想,我们需要先向目的地发信息确认一下,路线是否畅通,是否有工作人员接站,要不然就有去无回了。先来个三次握手(TCP连接)吧。TCP连接在获取到服务器的IP地址后,便会开始建立一次连接,这是由TCP协议完成的,主要通过三次握手的方式建立连接:1) 第一次握手: 建立连接时,客户端发送SYN包(syn=i)到服务器,并进入SYN_SENT状态,等待服务器确认。2) 第二次握手: 服务器收到SYN包,必须确认客户的SYN(ack=i+1),同时自己也发送一个SYN包(syn=j),即SYN+ACK包,此时服务器进入SYN_RECV状态。3) 第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=j+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。经过三次握手之后,我们就可以放心的出发了。NO NO NO,还要稍等片刻(估计大家已经崩溃了吧),先检查下火车头及车厢是否准备就绪(也就是封装HTTP消息头和消息体)。HTTP请求与响应 先来科普一下:HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW)服务器传输超文本到本地浏览器的传送协议。HTTP基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件,查询结果等),是一个属于应用层的面向对象的协议。 HTTP请求报文由三部分组成:请求行、请求报文头、请求报文体。下面是一个实际的请求报文: HTTP请求行包括请求方法、请求URL和HTTP协议及版本。1) GET和POST是最常见的HTTP请求方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。2) 请求URL和报文头里的HOST属性拼接组成完整的请求URL。3) 当前使用的协议是HTTP/1.1版本,在此之前还有HTTP/0.9和HTTP/1.0。那么HTTP/1.1和HTTP/1.0有哪些区别呢?a. HTTP/1.1则默认支持长连接,HTTP/1.0中每对请求/ 响应都使用一个新的连接。b. HTTP/1.1在请求消息头多一个Host域,HTTP/1.0则没有这个域。c. HTTP/1.1增加了带宽优化的一些字段:比如range头域、100响应码、Content-Encoding等,具体细节在后续的优化篇中再详细介绍。d. HTTP1.1增加了OPTIONS、PUT、 DELETE、TRACE、CONNECT请求方法。e. HTTP/1.1中新增了24个状态响应码。HTTP请求报文头,以明文的字符串格式传送,是以冒号分隔的键/值对,如:Cache-Control: no-cache,每一个消息头最后以回车符(CR)和换行符(LF)结尾。HTTP消息头结束后,会用一个空白的字段来标识,这样就会出现两个连续的CR-LF。结构大体如下图所示:我们前面在介绍HTTP缓存时提到的Cache-Control字段,就位于请求报文头里。此外常见的HTTP请求报文头属性还有Accept、Cookie、Referer、Connection、Host、Content-Type等等。详细的报文头属性这里就不再介绍了,感兴趣的童鞋可自行查阅。Content-Type用来表示请求体数据的媒体类型信息,常见类型有application/json、text/xml以及application/x-www-form-urlencoded。浏览器负责发送HTTP请求,同时也接收返回的HTTP响应信息。HTTP响应报文同样由三部分组成:响应行、响应头和响应体。下面是一个实际的HTTP响应报文:响应行包含协议版本信息以及状态码和状态描述信息。HTTP响应状态码由5段组成:1) 1xx 消息,一般是告诉客户端,请求已经收到了,正在处理,别急...2) 2xx 处理成功,一般表示:请求收到、我明白你要的、请求已受理、已经处理完成等信息.3) 3xx 重定向到其它地方,它让客户端再发起一个请求以完成整个处理。4) 4xx 处理发生错误,责任在客户端,如客户端的请求一个不存在的资源,客户端未被授权,禁止访问等。5) 5xx 处理发生错误,责任在服务端,如服务端抛出异常,路由出错,HTTP版本不支持等。响应头结构和请求头一样,常见报文头属性有:Cache-Control、ETag、Location、Set-Cookie、Content-Type等。响应体则是服务器返回的数据了,可能是一个HTML页面,一个图片或者一段JSON数据。HTTP请求报文生成后,总算是可以出发了,祝大家一路顺风!HTTP请求发出之后,就交由指定的后台服务来进行处理(Web服务器或者反向代理服务器等),具体的处理过程,我们会在后续的《Web之旅-后端篇》中再详细介绍。终于来到了大家最感兴趣的环节,估计看了前面那么多繁琐的步骤,已经脑阔疼了吧。建议休息五分钟,然后再来看下浏览器获取到HTML页面后,是怎么一步步把界面渲染出来的。页面渲染在前面浏览器运行机制章节,我们有提到,浏览器是多进程的,每打开一个tab页就会创建一个新的进程,也就是渲染进程。渲染进程又是多线程的,各个线程之间相互协作,共同完成页面的渲染工作。那么各个线程是怎样的一个分工呢?简单来讲,GUI渲染线程按照从上到下的顺序解析HTML文件,当遇到任何样式(link、style)与脚本(script)时,就会调用异步请求线程去加载资源。加载到的CSS交给GUI渲染线程继续处理,JS文件则交由JS引擎线程去解析执行。当JS引擎线程解析遇到事件(如Ajax异步请求)时,提交任务给事件触发线程,由事件触发线程判断触发条件是否满足,满足时再交由JS引擎线程处理。对于setInterval和setTimeout方法,定时计数交给定时触发器线程处理,计时完毕后再交由JS引擎处理。从页面的渲染角度来讲,大概可以划分成以下几个过程:1) 解析HTML/SVG/XHTML,构造DOM树。Webkit中有三个C++的类来处理这三类文档,解析后会产生一个DOM树。2) 解析CSS构造CSS规则树。3) 解析JS,通过DOM API和CSSOM API 来操作DOM树和CSS规则树4) 通过DOM树和CSS规则树构造Render树,计算各元素尺寸和位置,即Layout/Reflow的过程。DOM树与HTML一一对应,而Render树会忽略诸如head、display:none的元素,并且附加CSS规则到Render树的每个节点(Element)上。5) 绘制Render树(Paint),绘制页面像素信息6) 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。整个解析过程,默认采用同步的方式进行。当解析到外部资源(CSS、JS)时,浏览器会启用异步请求线程去加载资源。如果是CSS资源,资源加载不会阻塞DOM树的解析,但是要在CSS加载完成后,才能构造Render树。如果CSS加载不阻塞render树渲染的话,那么当CSS加载完之后,Render树可能又得Repaint或者Reflow了,这就造成了一些没有必要的损耗。什么是Repaint和Reflow呢?Repaint(重绘):部分节点需要更新,但不改变其他集合形状。如改变某个元素的颜色是,就会发生重绘。Reflow(重排):Render树的部分更新,如尺寸变化,就会发生重排。为提高页面的加载速度,我们要尽量的避免页面的Repaint和Reflow操作。如果解析遇到外部JS资源,资源加载将阻塞DOM树的解析,直到JS解析完成后,再继续往下解析。这是因为JS是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS引擎线程和GUI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎线程为互斥的关系,当JS引擎执行时GUI线程会被挂起,GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。所以如果JS执行时间过长,就会阻塞整个页面的渲染,我们要尽量避免这种情况,比如把JS脚本应用放到HTML的最后(</body>之前)。那如果耗时操作必不可少呢,别担心,JS里有一个叫Worker的类,可以帮助我们实现类似多线程的操作,感兴趣的童鞋可以去了解下。当然,JS也可以采用异步加载的方式,如HTML5在<script>增加的async和defer属性,可以让浏览器异步去下载并执行JS,而不阻塞页面的渲染。异步加载原理基本上都是向DOM中写入script或者通过eval函数执行JS代码,可以把它放在匿名函数中执行,也可以在onload中执行,也可以通过XHR注入实现,也可以创建一个iframe元素,然后在iframe中执行**JS代码。但是异步执行需要自行判断指定JS文件是否会对页面的渲染造成影响,否则会出现渲染异常的情况。这次旅程差不多到这里就要结束了,是时候挥手告别了。关闭TCP连接 建立TCP连接需要三次握手,关闭TCP连接则需要四次挥手。 第一次挥手是浏览器发完数据后,发送FIN请求断开连接。第二次挥手是服务器发送ACK表示同意,如果在这一次服务器也发送FIN请求断开连接似乎也没有不妥,但考虑到服务器可能还有数据要发送,所以服务器发送FIN放在了第三次挥手中。最后浏览器返回ACK表示同意,也就是第四次挥手。结束语 首先恭喜大家完成了此次的web前端之旅,通过这次旅程,大家应该对前端的整个加载机制有了初步的了解,如果还意犹未尽,可以参考《WebKit技术内幕》一书进行深入学习和研究。 同时我们也看到了,在整个页面的加载机制中,有很多复杂繁琐的步骤。而浏览器也已经做了很多的优化工作,那么对于前端开发来说,如何才能快速的获取和展示出我们想要的数据呢?下一篇文章《Web之旅-前端优化篇》,将针对每一个步骤,阐述如何进行优化,以达到极致体验。敬请期待。
-
背景说明:在项目和培训中多次被问题FusionSphere物理CPU和vCPU的对应或分配关系,一个物理CPU能虚拟出多少个vCPU,一个vCPU的主频是多少等问题。设置了CPU预留、份额与限制之后又是什么情况。看过之前的一些讨论,也没有定论,本着实践是检验整理的唯一标准,本文通过实验,并对照相关文档来梳理这些问题,希望能让大家有更清楚的理解。1. 系统可用的VCPU总数计算服务器CPU信息:1台R2288H V3,2个CPU, 10 核,超线程为2。总共2x10x2= 40个thread,每个Thread 2.3GHz。Haswell EP CPU 02311CDJ BC1M12CPU X86 series,2300**z,1.8V,64bit,105000mW,Haswell EP Xeon E5-2650 v3,10Core,with heatsink 2 2服务器BMC管理界面上查看 CPU信息 Intel官网看到E5-2650的信息http://ark.intel.com/products/81705/Intel-Xeon-Processor-E5-2650-v3-25M-Cache-2_30-GHz在主机上部署FusionCompute R5C00, 登录CNA主机运行xentop命令查看CPU信息CPUs:40 @ 2294 **z,主频总容量为40 x 2.294 GHz = 91.76 GHz。Domain 0默认配置2个VCPU,占用2 x 2.294 = 4.588 GHz用户可用的主频总容量 = 91.76 - 4.588 = 87.172 GHzFC portal上查看CPU信息,总容量87.17GHz正好等于系统总容量减去Domain 0占用的容量。结论1: 系统可用的vCPU总数(逻辑处理器) = Socket数(CPU个数)x Core数(内核)x Thread数(超线程)1个VCPU = 1个超线程Thread。如下图: CPU QoS如图所示,CPU预留容量为4.59GHz,可用容量为82.58GHz,说明除了VRM01的2个VCPU预留容量4588**z之外的VCPU主频均是可用的,尽管该环境已创建了7台4 VCPU的VM,还可以创建更多VM,这些VM的VCPU总数可以远远超过当前系统显示可用的38个VCPU。在不对VRM01的VCPU进行限制的情况下,将VCPU份额自定义为128000,显示可使用的CPU数为38,说明如果需要的话VRM01可以占用该主机上的除了Domain 0之外的所有VCPU(Domain 0占用了2个VCPU)。2. 虚拟机VCPU的分配与调度对虚拟机来说,不直接感知物理CPU,虚拟机的计算单元通过vCPU对象来呈现。虚拟机只看到VMM呈现给它的vCPU。在VMM中,每个vCPU对应一个VMCS(Virtual-Machine Control Structure)结构,当VCPU被从物理CPU上切换下来的时候,其运行上下文会被保存在其对应的VMCS结构中;当VCPU被切换到PCPU上运行时,其运行上下文会从对应的VMCS结构中导入到物理CPU上。通过这种方式,实现各vCPU之间的独立运行。从虚拟机系统的结构与功能划分可以看出,客户操作系统与虚拟机监视器共同构成了虚拟机系统的两级调度框架,如图所示是一个多核环境下虚拟机系统的两级调度框架。客户操作系统负责第2 级调度,即线程或进程在vCPU 上的调度(将核心线程映射到相应的VCPU上)。虚拟机监视器负责第1 级调度, 即vCPU在物理处理单元上的调度。两级调度的调度策略和机制不存在依赖关系。vCPU调度器负责物理处理器资源在各个虚拟机之间的分配与调度,本质上即把各个虚拟机中的vCPU按照一定的策略和机制调度在物理处理单元上可以采用任意的策略来分配物理资源, 满足虚拟机的不同需求。vCPU可以调度在一个或多个物理处理单元执行(分时复用或空间复用物理处理单元), 也可以与物理处理单元建立一对一固定的映射关系(限制访问指定的物理处理单元)。3. CPU QoS说明Hypervisor层根据分时复用的原理实现对VCPU的调度,CPU QoS的原理是定期给各VCPU分配运行时间片,并对各VCPU运行的时间进行记账,对于消耗完时间片的虚拟CPU将被限制运行,直到获得时间片。以此控制虚拟机获得物理计算资源的比例。以上分配时间片和记账的时间周期很短,对虚拟机用户来说会感觉一直在运行。CPU预留定义了分配给该VM的最少CPU资源。CPU限制定义了分配虚拟机占用CPU资源的上限。CPU份额定义多个虚拟机在竞争CPU资源的时候按比例分配。CPU份额只在各虚拟机竞争计算资源时发挥作用,如果没有竞争,有需求的虚拟机可以独占主机的物理CPU资源。如果虚拟机根据份额值计算出来的计算能力小于虚拟机预留值,调度算法会优先按照虚拟机预留值分配给虚拟机,对于预留值超出按份额分配的计算资源的部分,调度算法会从主机上其他虚拟机的CPU上按各自的份额比例扣除。如果虚拟机根据份额值计算出来的计算能力大于虚拟机预留值,那么虚拟机的计算能力会以份额值计算为准。以一台主频为2800**z的单核物理机为例,如果满负载运行3台单VCPU的虚拟机A、B、C,分配情况如下。结论2:由于采用分时复用的方式,在不做VCPU预留的条件下,系统可分配给VM的VCPU总数远远大于实际可提供的VCPU数目(具体能创建多少额外的VCPU依赖于物理CPU的性能和VCPU的使用情率),在出现资源争用的时根据CPU QoS中的预留和份额来分配资源。
-
[中国,上海,2016年8月31日] 华为历史上规模最大的面向ICT行业的全球生态大会——HUAWEI CONNECT 2016全联接大会31日在上海开幕,为期三天。来自120多个国家和地区的20000名业界精英,围绕“塑造云时代”主题,共同探讨云时代趋势与洞察,以及各行各业如何通过打造云技术、构筑云生态,积极实现数字化转型。华为轮值CEO胡厚崑在31日开幕的华为全联接大会上发表题为《站在云端看世界》演讲在此次大会上,华为首次全面阐述了华为云战略定位。华为轮值CEO胡厚崑在大会首场大会演讲中表示,华为的定位,就是成为智能社会的使能者与推动者。华为将坚持以客户为中心,聚焦ICT基础设施,做创新的云技术提供者;做企业云化、数字化战略的使能者和优选合作伙伴;秉承开放、合作、共赢的原则,做云生态的积极贡献者。面向智能社会,信息通信技术是最重要的基石。终端是万物感知的触角,网络连接万物,云是万物智能的源泉。与此对应的是全面协同的“端、管、云”架构,这也是华为重点投入的战略方向。胡厚崑表示:“5到10年内,将出现各式各样的多场景、自适应的智能终端,人和物都能感知环境,成为智能世界的入口;光缆和无线网络可以提供无处不在的超宽带连接;分布全球而又相互连接的计算机汇聚了海量信息,在云端生成了‘数字大脑’,实时进化,永不衰老,人和机器可以通过连接和终端随时调用其智慧。”胡厚崑表示,企业是云化的主角,华为就是坚持以客户为中心,深入理解客户需求,用创新的技术去匹配。华为通过统一开放的云架构为客户提供可交付、可运营的混合云解决方案,特别强调了开放性、安全性、企业级的性能以及一站式的特点,恰恰来自于对客户需求深入而准确的把握。另外,华为主张,云生态的构建要基于为客户创造价值的目的,每个参与生态建设者,都应该有自己独特的价值。他也强调,华为不会独自做几朵云,华为要做的是帮助千千万万客户建好千万朵云,积极参与云生态的建设。“生于云的一代”企业主导了云的1.0时代,颠覆了很多行业。胡厚崑认为,下个十年,将是云的2.0时代,行业云将兴起。到2025年,所有企业信息技术解决方案都会被云化,85%以上企业应用会被部署到云上。每个企业需要结合自身核心业务,探索最适合自己的云化解决方案。他结合华为经验,提出“化云为雨,让云为业务创造价值”的理念和三个实现途径。首先要重塑观念,重新认识ICT的作用,企业要把信息技术从辅助性技术上升为生产技术,大胆利用技术重新设计生产流程;其次,重构人才,掌握以云为基础的信息技术应成为基本技能;最后是小步快跑,用循序渐进的成功建立持久的信心。“云正在塑造一切,有变革才有重生。对于任何企业来说,云的2.0时代,有变化,才有希望,有行动,才有未来”,他说。大会期间,华为轮值CEO胡厚崑、徐直军、郭平,英特尔公司首席执行官Brian Krzanich,Infosys首席执行官Vishal Sikka, SAP公司高级副总裁Thomas Saueressig,麻省理工大学斯隆商学院首席研究科学家Andrew McAfee等发表主题演讲。华为全联接大会是一场生态大会,将在上海世博中心搭建18000平方米互动展厅,其中一半用于产业伙伴能力展示,约有80家华为及产业生态系统中的赞助商以及产业组织参展,包括Intel、HGST、SAP、Accenture、Infosys、OpenStack、GSMA等。华为将基于云计算、SDN、大数据等四大关键技术,发布云存储、云服务、SDN统一控制器等八大重量级解决方案,使能政府及公共事业、金融、电信、能源、媒资等九大行业客户的数字化转型之路。欲了解HUAWEI CONNECT 2016更多详情,请参阅大会官网: http://www.huawei.com/minisite/huaweiconnect2016。
-
本文转载自:https://zhuanlan.zhihu.com/p/37487213前言Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或缺的一部分。我们使用Redis时,会接触Redis的5种对象类型(字符串、哈希、列表、集合、有序集合),丰富的类型是Redis相对于Memcached等的一大优势。在了解Redis的5种对象类型的用法和特点的基础上,进一步了解Redis的内存模型,对Redis的使用有很大帮助,例如:1、估算Redis内存使用量。目前为止,内存的使用成本仍然相对较高,使用内存不能无所顾忌;根据需求合理的评估Redis的内存使用量,选择合适的机器配置,可以在满足需求的情况下节约成本。2、优化内存占用。了解Redis内存模型可以选择更合适的数据类型和编码,更好的利用Redis内存。3、分析解决问题。当Redis出现阻塞、内存占用等问题时,尽快发现导致问题的原因,便于分析解决问题。这篇文章主要介绍Redis的内存模型(以3.0为例),包括Redis占用内存的情况及如何查询、不同的对象类型在内存中的编码方式、内存分配器(jemalloc)、简单动态字符串(SDS)、RedisObject等;然后在此基础上介绍几个Redis内存模型的应用。在后面的文章中,会陆续介绍关于Redis高可用的内容,包括主从复制、哨兵、集群等等,欢迎关注。目录一、Redis内存统计二、Redis内存划分1、数据2、进程本身运行需要的内存3、缓冲内存4、内存碎片三、Redis数据存储的细节1、概述2、jemalloc3、redisObject4、SDS四、Redis的对象类型与内部编码1、字符串2、列表3、哈希4、集合5、有序集合五、应用举例1、估算Redis内存使用量2、优化内存占用3、关注内存碎片率一、Redis内存统计工欲善其事必先利其器,在说明Redis内存之前首先说明如何统计Redis使用内存的情况。在客户端通过redis-cli连接服务器后(后面如无特殊说明,客户端一律使用redis-cli),通过info命令可以查看内存使用情况:1info memory其中,info命令可以显示redis服务器的许多信息,包括服务器基本信息、CPU、内存、持久化、客户端连接信息等等;memory是参数,表示只显示内存相关的信息。返回结果中比较重要的几个说明如下:(1)used_memory:Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即swap);Redis分配器后面会介绍。used_memory_human只是显示更友好。(2)used_memory_rss:Redis进程占据操作系统的内存(单位是字节),与top及ps命令看到的值是一致的;除了分配器分配的内存之外,used_memory_rss还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存。因此,used_memory和used_memory_rss,前者是从Redis角度得到的量,后者是从操作系统角度得到的量。二者之所以有所不同,一方面是因为内存碎片和Redis进程运行需要占用内存,使得前者可能比后者小,另一方面虚拟内存的存在,使得前者可能比后者大。由于在实际应用中,Redis的数据量会比较大,此时进程运行占用的内存与Redis数据量和内存碎片相比,都会小得多;因此used_memory_rss和used_memory的比例,便成了衡量Redis内存碎片率的参数;这个参数就是mem_fragmentation_ratio。(3)mem_fragmentation_ratio:内存碎片比率,该值是used_memory_rss / used_memory的比值。mem_fragmentation_ratio一般大于1,且该值越大,内存碎片比例越大。mem_fragmentation_ratio<1,说明Redis使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多,当这种情况出现时,应该及时排查,如果内存不足应该及时处理,如增加Redis节点、增加Redis服务器的内存、优化应用等。一般来说,mem_fragmentation_ratio在1.03左右是比较健康的状态(对于jemalloc来说);上面截图中的mem_fragmentation_ratio值很大,是因为还没有向Redis中存入数据,Redis进程本身运行的内存使得used_memory_rss 比used_memory大得多。(4)mem_allocator:Redis使用的内存分配器,在编译时指定;可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc;截图中使用的便是默认的jemalloc。二、Redis内存划分Redis作为内存数据库,在内存中存储的内容主要是数据(键值对);通过前面的叙述可以知道,除了数据以外,Redis的其他部分也会占用内存。Redis的内存占用主要可以划分为以下几个部分:1、数据作为数据库,数据是最主要的部分;这部分占用的内存会统计在used_memory中。Redis使用键值对存储数据,其中的值(对象)包括5种类型,即字符串、哈希、列表、集合、有序集合。这5种类型是Redis对外提供的,实际上,在Redis内部,每种类型可能有2种或更多的内部编码实现;此外,Redis在存储对象时,并不是直接将数据扔进内存,而是会对对象进行各种包装:如redisObject、SDS等;这篇文章后面将重点介绍Redis中数据存储的细节。2、进程本身运行需要的内存Redis主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。补充说明:除了主进程外,Redis创建的子进程运行也会占用内存,如Redis执行AOF、RDB重写时创建的子进程。当然,这部分内存不属于Redis进程,也不会统计在used_memory和used_memory_rss中。3、缓冲内存缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等;其中,客户端缓冲存储客户端连接的输入输出缓冲;复制积压缓冲用于部分复制功能;AOF缓冲区用于在进行AOF重写时,保存最近的写入命令。在了解相应功能之前,不需要知道这些缓冲的细节;这部分内存由jemalloc分配,因此会统计在used_memory中。4、内存碎片内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致redis释放的空间在物理内存中并没有释放,但redis又无法有效利用,这就形成了内存碎片。内存碎片不会统计在used_memory中。内存碎片的产生与对数据进行的操作、数据的特点等都有关;此外,与使用的内存分配器也有关系:如果内存分配器设计合理,可以尽可能的减少内存碎片的产生。后面将要说到的jemalloc便在控制内存碎片方面做的很好。如果Redis服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片:因为重启之后,Redis重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。三、Redis数据存储的细节1、概述关于Redis数据存储的细节,涉及到内存分配器(如jemalloc)、简单动态字符串(SDS)、5种对象类型及内部编码、redisObject。在讲述具体内容之前,先说明一下这几个概念之间的关系。下图是执行set hello world时,所涉及到的数据模型。图片来源:https://searchdatabase.techtarget.com.cn/7-20218/(1)dictEntry:Redis是Key-Value数据库,因此对每个键值对都会有一个dictEntry,里面存储了指向Key和Value的指针;next指向下一个dictEntry,与本Key-Value无关。(2)Key:图中右上角可见,Key(”hello”)并不是直接以字符串存储,而是存储在SDS结构中。(3)redisObject:Value(“world”)既不是直接以字符串存储,也不是像Key一样直接存储在SDS中,而是存储在redisObject中。实际上,不论Value是5种类型的哪一种,都是通过redisObject来存储的;而redisObject中的type字段指明了Value对象的类型,ptr字段则指向对象所在的地址。不过可以看出,字符串对象虽然经过了redisObject的包装,但仍然需要通过SDS存储。实际上,redisObject除了type和ptr字段以外,还有其他字段图中没有给出,如用于指定对象内部编码的字段;后面会详细介绍。(4)jemalloc:无论是DictEntry对象,还是redisObject、SDS对象,都需要内存分配器(如jemalloc)分配内存进行存储。以DictEntry对象为例,有3个指针组成,在64位机器下占24个字节,jemalloc会为它分配32字节大小的内存单元。下面来分别介绍jemalloc、redisObject、SDS、对象类型及内部编码。2、jemallocRedis在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc。jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。jemalloc划分的内存单元如下图所示:图片来源:http://blog.csdn.net/zhengpeitao/article/details/76573053例如,如果需要存储大小为130字节的对象,jemalloc会将其放入160字节的内存单元中。3、redisObject前面说到,Redis对象有5种类型;无论是哪种类型,Redis都不会直接存储,而是通过redisObject对象进行存储。redisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要redisObject支持,下面将通过redisObject的结构来说明它是如何起作用的。redisObject的定义如下(不同版本的Redis可能稍稍有所不同):1234567typedef struct redisObject { unsigned type:4; unsigned encoding:4;unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ int refcount; void *ptr;} robj;redisObject的每个字段的含义和作用如下:(1)typetype字段表示对象的类型,占4个比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。当我们执行type命令时,便是通过读取RedisObject的type字段获得对象的类型;如下图所示:(2)encodingencoding表示对象的内部编码,占4个比特。对于Redis支持的每种类型,都有至少两种内部编码,例如对于字符串,有int、embstr、raw三种编码。通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率。以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入;当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。通过object encoding命令,可以查看对象采用的编码方式,如下图所示:5种对象类型对应的编码方式以及使用条件,将在后面介绍。(3)lrulru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(如4.0版本占24比特,2.6版本占22比特)。通过对比lru时间与当前时间,可以计算某个对象的空转时间;object idletime命令可以显示该空转时间(单位是秒)。object idletime命令的一个特殊之处在于它不改变对象的lru值。lru值除了通过object idletime命令打印之外,还与Redis的内存回收有关系:如果Redis打开了maxmemory选项,且内存回收算法选择的是volatile-lru或allkeys—lru,那么当Redis内存占用超过maxmemory指定的值时,Redis会优先选择空转时间最长的对象进行释放。(4)refcountrefcount与共享对象refcount记录的是该对象被引用的次数,类型为整型。refcount的作用,主要在于对象的引用计数和内存回收。当创建新对象时,refcount初始化为1;当有新程序使用该对象时,refcount加1;当对象不再被一个新程序使用时,refcount减1;当refcount变为0时,对象占用的内存会被释放。Redis中被多次使用的对象(refcount>1),称为共享对象。Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。共享对象的具体实现Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是0~9999的整数值;当Redis需要使用值为0~9999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以通过调整参数REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值进行改变。共享对象的引用次数可以通过object refcount命令查看,如下图所示。命令执行的结果页佐证了只有0~9999之间的整数会作为共享对象。(5)ptrptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。(6)总结综上所述,redisObject的结构与对象类型、编码、内存回收、共享对象都有关系;一个redisObject对象的大小为16字节:4bit+4bit+24bit+4Byte+8Byte=16Byte。4、SDSRedis没有直接使用C字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。SDS是简单动态字符串(Simple Dynamic String)的缩写。(1)SDS结构sds的结构如下:12345struct sdshdr {int len;int free;char buf[];};其中,buf表示字节数组,用来存储字符串;len表示buf已使用的长度,free表示buf未使用的长度。下面是两个例子。图片来源:《Redis设计与实现》通过SDS的结构可以看出,buf数组的长度=free+len+1(其中1表示字符串结尾的空字符);所以,一个SDS结构占据的空间为:free所占长度+len所占长度+ buf数组的长度=4+4+free+len+1=free+len+9。(2)SDS与C字符串的比较SDS在C字符串的基础上加入了free和len字段,带来了很多好处:获取字符串长度:SDS是O(1),C字符串是O(n)缓冲区溢出:使用C字符串的API时,如果字符串长度增加(如strcat操作)而忘记重新分配内存,很容易造成缓冲区的溢出;而SDS由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。修改字符串时内存的重分配:对于C字符串,如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。而对于SDS,由于可以记录len和free,因此解除了字符串长度和空间数组长度之间的关联,可以在此基础上进行优化:空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。存取二进制数据:SDS可以,C字符串不可以。因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而SDS以字符串长度len来作为字符串结束标识,因此没有这个问题。此外,由于SDS中的buf仍然使用了C字符串(即以’\0’结尾),因此SDS可以使用C字符串库中的部分函数;但是需要注意的是,只有当SDS用来存储文本数据时才可以这样使用,在存储二进制数据时则不行(’\0’不一定是结尾)。(3)SDS与C字符串的应用Redis在存储对象时,一律使用SDS代替C字符串。例如set hello world命令,hello和world都是以SDS的形式存储的。而sadd myset member1 member2 member3命令,不论是键(”myset”),还是集合中的元素(”member1”、 ”member2”和”member3”),都是以SDS的形式存储。除了存储对象,SDS还用于存储各种缓冲区。只有在字符串不会改变的情况下,如打印日志时,才会使用C字符串。四、Redis的对象类型与内部编码前面已经说过,Redis支持5种对象类型,而每种结构都有至少两种编码;这样做的好处在于:一方面接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。Redis各种对象类型支持的内部编码如下图所示(图中版本是Redis3.0,Redis后面版本中又增加了内部编码,略过不提;本章所介绍的内部编码都是基于3.0的):图片来源:《Redis设计与实现》关于Redis内部编码的转换,都符合以下规律:编码转换在Redis写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换。1、字符串(1)概况字符串是最基础的类型,因为所有的键都是字符串类型,且字符串之外的其他几种复杂类型的元素也是字符串。字符串长度不能超过512MB。(2)内部编码字符串类型的内部编码有3种,它们的应用场景如下:int:8个字节的长整型。字符串值是整型时,这个值使用long整型表示。embstr:<=39字节的字符串。embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。raw:大于39个字节的字符串示例如下图所示:embstr和raw进行区分的长度,是39;是因为redisObject的长度是16字节,sds的长度是9+字符串长度;因此当字符串长度是39时,embstr的长度正好是16+9+39=64,jemalloc正好可以分配64字节的内存单元。(3)编码转换当int数据不再是整数,或大小超过了long的范围时,自动转化为raw。而对于embstr,由于其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了39个字节。示例如下图所示:2、列表(1)概况列表(list)用来存储多个有序的字符串,每个字符串称为元素;一个列表可以存储2^32-1个元素。Redis中的列表支持两端**和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等。(2)内部编码列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)。双端链表:由一个list结构和多个l**ode结构组成;典型结构如下图所示:图片来源:《Redis设计与实现》通过图中可以看出,双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;链表中保存了列表的长度;dup、free和match为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。而链表中每个节点指向的是type为字符串的redisObject。压缩列表:压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构;具体结构相对比较复杂,略。与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。压缩列表不仅用于实现列表,也用于实现哈希、有序列表;使用非常广泛。(3)编码转换只有同时满足下面两个条件时,才会使用压缩列表:列表中元素数量小于512个;列表中所有字符串对象都不足64字节。如果有一个条件不满足,则使用双端列表;且编码只可能由压缩列表转化为双端链表,反方向则不可能。下图展示了列表编码转换的特点:其中,单个字符串不能超过64字节,是为了便于统一分配每个节点的长度;这里的64字节是指字符串的长度,不包括SDS结构,因为压缩列表使用连续、定长内存块存储字符串,不需要SDS结构指明长度。后面提到压缩列表,也会强调长度不超过64字节,原理与这里类似。3、哈希(1)概况哈希(作为一种数据结构),不仅是redis对外提供的5种对象类型的一种(与字符串、列表、集合、有序结合并列),也是Redis作为Key-Value数据库所使用的数据结构。为了说明的方便,在本文后面当使用“内层的哈希”时,代表的是redis对外提供的5种对象类型的一种;使用“外层的哈希”代指Redis作为Key-Value数据库所使用的数据结构。(2)内部编码内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)两种;Redis的外层的哈希则只使用了hashtable。压缩列表前面已介绍。与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(n)变为了O(1),但由于哈希中元素数量较少,因此操作的时间并没有明显劣势。hashtable:一个hashtable由1个dict结构、2个dictht结构、1个dictEntry指针数组(称为bucket)和多个dictEntry结构组成。正常情况下(即hashtable没有进行rehash时)各部分关系如下图所示:图片改编自:《Redis设计与实现》下面从底层向上依次介绍各个部分:dictEntrydictEntry结构用于保存键值对,结构定义如下:123456789typedef struct dictEntry{void *key;union{void *val;uint64_tu64;int64_ts64;}v;struct dictEntry *next;}dictEntry;其中,各个属性的功能如下:key:键值对中的键;val:键值对中的值,使用union(即共用体)实现,存储的内容既可能是一个指向值的指针,也可能是64位整型,或无符号64位整型;next:指向下一个dictEntry,用于解决哈希冲突问题在64位系统中,一个dictEntry对象占24字节(key/val/next各占8字节)。bucketbucket是一个数组,数组的每个元素都是指向dictEntry结构的指针。redis中bucket数组的大小计算规则如下:大于dictEntry的、最小的2^n;例如,如果有1000个dictEntry,那么bucket大小为1024;如果有1500个dictEntry,则bucket大小为2048。dicthtdictht结构如下:123456typedef struct dictht{dictEntry **table;unsigned long size;unsigned long sizemask;unsigned long used;}dictht;其中,各个属性的功能说明如下:table属性是一个指针,指向bucket;size属性记录了哈希表的大小,即bucket的大小;used记录了已使用的dictEntry的数量;sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置。dict一般来说,通过使用dictht和dictEntry结构,便可以实现普通哈希表的功能;但是Redis的实现中,在dictht结构的上层,还有一个dict结构。下面说明dict结构的定义及作用。dict结构如下:123456typedef struct dict{dictType *type;void *privdata;dictht ht[2];int trehashidx;} dict;其中,type属性和privdata属性是为了适应不同类型的键值对,用于创建多态字典。ht属性和trehashidx属性则用于rehash,即当哈希表需要扩展或收缩时使用。ht是一个包含两个项的数组,每项都指向一个dictht结构,这也是Redis的哈希会有1个dict、2个dictht结构的原因。通常情况下,所有的数据都是存在放dict的ht[0]中,ht[1]只在rehash的时候使用。dict进行rehash操作的时候,将ht[0]中的所有数据rehash到ht[1]中。然后将ht[1]赋值给ht[0],并清空ht[1]。因此,Redis中的哈希之所以在dictht和dictEntry结构之外还有一个dict结构,一方面是为了适应不同类型的键值对,另一方面是为了rehash。(3)编码转换如前所述,Redis中内层的哈希既可能使用哈希表,也可能使用压缩列表。只有同时满足下面两个条件时,才会使用压缩列表:哈希中元素数量小于512个;哈希中所有键值对的键和值字符串长度都小于64字节。如果有一个条件不满足,则使用哈希表;且编码只可能由压缩列表转化为哈希表,反方向则不可能。下图展示了Redis内层的哈希编码转换的特点:4、集合(1)概况集合(set)与列表类似,都是用来保存多个字符串,但集合与列表有两点不同:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。一个集合中最多可以存储2^32-1个元素;除了支持常规的增删改查,Redis还支持多个集合取交集、并集、差集。(2)内部编码集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。哈希表前面已经讲过,这里略过不提;需要注意的是,集合在使用哈希表时,值全部被置为null。整数集合的结构定义如下:12345typedef struct intset{uint32_t encoding;uint32_t length;int8_t contents[];} intset;其中,encoding代表contents中存储内容的类型,虽然contents(存储集合中的元素)是int8_t类型,但实际上其存储的值是int16_t、int32_t或int64_t,具体的类型便是由encoding决定的;length表示元素个数。整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(n)变为了O(1),但由于集合数量较少,因此操作的时间并没有明显劣势。(3)编码转换只有同时满足下面两个条件时,集合才会使用整数集合:集合中元素数量小于512个;集合中所有元素都是整数值。如果有一个条件不满足,则使用哈希表;且编码只可能由整数集合转化为哈希表,反方向则不可能。下图展示了集合编码转换的特点:5、有序集合(1)概况有序集合与集合一样,元素都不能重复;但与集合不同的是,有序集合中的元素是有顺序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。(2)内部编码有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。ziplist在列表和哈希中都有使用,前面已经讲过,这里略过不提。跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。Redis的跳跃表实现由zskiplist和zskipl**ode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。具体结构相对比较复杂,略。(3)编码转换只有同时满足下面两个条件时,才会使用压缩列表:有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节。如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。下图展示了有序集合编码转换的特点:五、应用举例了解Redis的内存模型之后,下面通过几个例子说明其应用。1、估算Redis内存使用量要估算redis中的数据占据的内存大小,需要对redis的内存模型有比较全面的了解,包括前面介绍的hashtable、sds、redisobject、各种对象类型的编码方式等。下面以最简单的字符串类型来进行说明。假设有90000个键值对,每个key的长度是7个字节,每个value的长度也是7个字节(且key和value都不是整数);下面来估算这90000个键值对所占用的空间。在估算占据空间之前,首先可以判定字符串类型使用的编码方式:embstr。90000个键值对占据的内存空间主要可以分为两部分:一部分是90000个dictEntry占据的空间;一部分是键值对所需要的bucket空间。每个dictEntry占据的空间包括:1) 一个dictEntry,24字节,jemalloc会分配32字节的内存块2) 一个key,7字节,所以SDS(key)需要7+9=16个字节,jemalloc会分配16字节的内存块3) 一个redisObject,16字节,jemalloc会分配16字节的内存块4) 一个value,7字节,所以SDS(value)需要7+9=16个字节,jemalloc会分配16字节的内存块5) 综上,一个dictEntry需要32+16+16+16=80个字节。bucket空间:bucket数组的大小为大于90000的最小的2^n,是131072;每个bucket元素为8字节(因为64位系统中指针大小为8字节)。因此,可以估算出这90000个键值对占据的内存大小为:90000*80 + 131072*8 = 8248576。下面写个程序在redis中验证一下:123456789101112131415161718192021222324public class RedisTest { public static Jedis jedis = new Jedis("localhost", 6379); public static void main(String[] args) throws Exception{ Long m1 = Long.valueOf(getMemory()); insertData(); Long m2 = Long.valueOf(getMemory()); System.out.println(m2 - m1); } public static void insertData(){ for(int i = 10000; i < 100000; i++){ jedis.set("aa" + i, "aa" + i); //key和value长度都是7字节,且不是整数 } } public static String getMemory(){ String memoryAllLine = http://jedis.info("memory"); String usedMemoryLine = memoryAllLine.split("\r\n")[1]; String memory = usedMemoryLine.substring(usedMemoryLine.indexOf(':') + 1); return memory; }}运行结果:8247552理论值与结果值误差在万分之1.2,对于计算需要多少内存来说,这个精度已经足够了。之所以会存在误差,是因为在我们**90000条数据之前redis已分配了一定的bucket空间,而这些bucket空间尚未使用。作为对比将key和value的长度由7字节增加到8字节,则对应的SDS变为17个字节,jemalloc会分配32个字节,因此每个dictEntry占用的字节数也由80字节变为112字节。此时估算这90000个键值对占据内存大小为:90000*112 + 131072*8 = 11128576。在redis中验证代码如下(只修改**数据的代码):12345public static void insertData(){ for(int i = 10000; i < 100000; i++){ jedis.set("aaa" + i, "aaa" + i); //key和value长度都是8字节,且不是整数 }}运行结果:11128576;估算准确。对于字符串类型之外的其他类型,对内存占用的估算方法是类似的,需要结合具体类型的编码方式来确定。2、优化内存占用了解redis的内存模型,对优化redis内存占用有很大帮助。下面介绍几种优化场景。(1)利用jemalloc特性进行优化上一小节所讲述的90000个键值便是一个例子。由于jemalloc分配内存时数值是不连续的,因此key/value字符串变化一个字节,可能会引起占用内存很大的变动;在设计时可以利用这一点。例如,如果key的长度如果是8个字节,则SDS为17字节,jemalloc分配32字节;此时将key长度缩减为7个字节,则SDS为16字节,jemalloc分配16字节;则每个key所占用的空间都可以缩小一半。(2)使用整型/长整型如果是整型/长整型,Redis会使用int类型(8字节)存储来代替字符串,可以节省更多空间。因此在可以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型。(3)共享对象利用共享对象,可以减少对象的创建(同时减少了redisObject的创建),节省内存空间。目前redis中的共享对象只包括10000个整数(0-9999);可以通过调整REDIS_SHARED_INTEGERS参数提高共享对象的个数;例如将REDIS_SHARED_INTEGERS调整到20000,则0-19999之间的对象都可以共享。考虑这样一种场景:论坛网站在redis中存储了每个帖子的浏览数,而这些浏览数绝大多数分布在0-20000之间,这时候通过适当增大REDIS_SHARED_INTEGERS参数,便可以利用共享对象节省内存空间。(4)避免过度设计然而需要注意的是,不论是哪种优化场景,都要考虑内存空间与设计复杂度的权衡;而设计复杂度会影响到代码的复杂度、可维护性。如果数据量较小,那么为了节省内存而使得代码的开发、维护变得更加困难并不划算;还是以前面讲到的90000个键值对为例,实际上节省的内存空间只有几MB。但是如果数据量有几千万甚至上亿,考虑内存的优化就比较必要了。3、关注内存碎片率内存碎片率是一个重要的参数,对redis 内存的优化有重要意义。如果内存碎片率过高(jemalloc在1.03左右比较正常),说明内存碎片多,内存浪费严重;这时便可以考虑重启redis服务,在内存中对数据进行重排,减少内存碎片。如果内存碎片率小于1,说明redis内存不足,部分数据使用了虚拟内存(即swap);由于虚拟内存的存取速度比物理内存差很多(2-3个数量级),此时redis的访问速度可能会变得很慢。因此必须设法增大物理内存(可以增加服务器节点数量,或提高单机内存),或减少redis中的数据。要减少redis中的数据,除了选用合适的数据类型、利用共享对象等,还有一点是要设置合理的数据回收策略(maxmemory-policy),当内存达到一定量后,根据不同的优先级对内存进行回收。
-
跳槽不算频繁,但参加过不少面试(电话面试、face to face 面试),面过大 / 小公司、互联网 / 传统软件公司,面糊过(眼高手低,缺乏实战经验,挂掉),也面过人,所幸未因失败而气馁,在此过程中不断查缺补漏,养成了踏实、追本溯源、持续改进的习惯,特此将自己经历过、构思过的一些面试题记录下来,如果答案有问题,欢迎拍砖讨论,希望能对找工作或者感兴趣的同学有所帮助,陆续整理中。1. synchronized 和 reentrantlock 异同相同点都实现了多线程同步和内存可见性语义都是可重入锁不同点实现机制不同 synchronized 通过 java 对象头锁标记和 Monitor 对象实现 reentrantlock 通过CAS、ASQ(AbstractQueuedSynchronizer)和 locksupport(用于阻塞和解除阻塞)实现synchronized 依赖 jvm 内存模型保证包含共享变量的多线程内存可见性 reentrantlock 通过 ASQ 的volatile state 保证包含共享变量的多线程内存可见性使用方式不同 synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)reentrantlock 显示调用 trylock()/lock() 方法,需要在 finally 块中释放锁功能丰富程度不同 reentrantlock提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、signal等方法)等丰富语义 reentrantlock 提供公平锁和非公平锁实现 synchronized不可设置等待时间、不可被中断(interrupted)2. concurrenthashmap 为何读不用加锁jdk1.71)HashEntry 中的 key、hash、next 均为 final 型,只能表头**、删除结点2)HashEntry 类的 value 域被声明为 volatile 型3)不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null时,便知道产生了冲突——发生了重排序现象(put 设置新 value 对象的字节码指令重排序),需要加锁后重新读入这个 value 值4)volatile 变量 count 协调读写线程之间的内存可见性,写操作后修改 count,读操作先读 count,根据happen-before 传递性原则写操作的修改读操作能够看到jdk1.81)Node 的 val 和 next 均为 volatile 型2)tabAt 和 casTabAt 对应的 unsafe 操作实现了 volatile 语义3. ContextClassLoader(线程上下文类加载器)的作用越过类加载器的双亲委派机制去加载类,如 serviceloader 实现使用线程上下文类加载器加载类,要注意保证多个需要通信的线程间的类加载器应该是同一个,防止因为不同的类加载器导致类型转换异常(ClassCastException)4. tomcat 类加载机制不同应用使用不同的 webapp 类加载器,实现应用隔离的效果,webapp 类加载器下面是 jsp 类加载器不同应用共享的 jar 包可以放到 Shared 类加载器 /shared 目录下5. osgi 类加载机制osgi 类加载模型是网状的,可以在模块(Bundle)间互相委托osgi 实现模块化热部署的关键是自定义类加载器机制的实现,每个 Bundle 都有一个自己的类加载器,当需要更换一个 Bundle时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换当收到类加载请求时,osgi 将按照下面的顺序进行类搜索:1)将以 java.* 开头的类委派给父类加载器加载2)否则,将委派列表名单(配置文件 org.osgi.framework.bootdelegation 中定义)内的类委派给父类加载器加载3)否则,检查是否在 Import-Package 中声明,如果是,则委派给 Export 这个类的 Bundle 的类加载器加载4)否则,检查是否在 Require-Bundle 中声明,如果是,则将类加载请求委托给 required bundle 的类加载器5)否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载6)否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载7)否则,查找 Dynamic Import-Package(Dynamic Import 只有在真正用到此 Package的时候才进行加载)的 Bundle,委派给对应 Bundle 的类加载器加载8)否则,类查找失败6. 如何结束一个一直运行的线程使用退出标志,这个 flag 变量要多线程可见使用 interrupt,结合 isInterrupted() 使用7. threadlocal 使用场景及问题threadlocal 并不能解决多线程共享变量的问题,同一个 threadlocal 所包含的对象,在不同的 thread中有不同的副本,互不干扰用于存放线程上下文变量,方便同一线程对变量的前后多次读取,如事务、数据库 connection 连接,在 web 编程中使用的更多问题: 注意线程池场景使用 threadlocal,因为实际变量值存放在了 thread 的 threadlocalmap类型变量中,如果该值没有 remove,也没有先 set 的话,可能会得到以前的旧值问题: 注意线程池场景下的内存泄露,虽然 threadlocal 的 get/set 会清除 key(key 为 threadlocal的弱引用,value 是强引用,导致 value 不释放)为 null 的 entry,但是最好 remove8. 线程池从启动到工作的流程刚创建时,里面没有线程调用 execute() 添加任务时:1)如果正在运行的线程数量小于核心参数 corePoolSize,继续创建线程运行这个任务2)否则,如果正在运行的线程数量大于或等于 corePoolSize,将任务加入到阻塞队列中3)否则,如果队列已满,同时正在运行的线程数量小于核心参数 maximumPoolSize,继续创建线程运行这个任务4)否则,如果队列已满,同时正在运行的线程数量大于或等于 maximumPoolSize,根据设置的拒绝策略处理5)完成一个任务,继续取下一个任务处理6)没有任务继续处理,线程被中断或者线程池被关闭时,线程退出执行,如果线程池被关闭,线程结束7)否则,判断线程池正在运行的线程数量是否大于核心线程数,如果是,线程结束,否则线程阻塞。因此线程池任务全部执行完成后,继续留存的线程池大小为corePoolSize8)本文所列出的 14 个 Java面试题只是我所遭遇的面试中的一部分,其他的面试题我也会陆续整理出来,说到这里另外顺便给大家推荐一个架构交流学习群:650385180,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty 源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,相信对于已经工作和遇到技术瓶颈的码友,在这个群里会有你需要的内容。9. 阻塞队列 BlockingQueue take 和 poll 区别poll(time):取走 BlockingQueue 里排在首位的对象, 若不能立即取出,则可以等 time参数规定的时间,取不到时返回 nulltake():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻塞直到BlockingQueue 有新的对象被加入10. 如何从 FutureTask 不阻塞获取结果get(long timeout,TimeUnit unit),超时则返回轮询,先通过 isDone()判断是否结束,然后调用 get()11. blockingqueue 如果存放了比较关键的数据,系统宕机该如何处理开放性问题,欢迎讨论将队列持久化,比较麻烦,需要将生产数据持久化到磁盘,持久化成功才返回,消费者线程从磁盘加载数据到内存阻塞队列中,维护消费offset,启动时,根据消费 offset 从磁盘加载数据加入消息队列,保证消息不丢失,生成序列号,消费幂等,根据消费进程决定系统重启后的生产状态12. NIO 与传统 I/O 的区别节约线程,NIO 由原来的每个线程都需要阻塞读写变成了由单线程(即 Selector)负责处理多个 channel注册(register)的兴趣事件(SelectionKey)集合(底层借助操作系统提供的 epoll()),netty bossgroup 处理 accept 连接(没看明白为什么 bossgroup 设置多个 thread的必要性),workergroup 处理具体业务流程和数据读写NIO 提供非阻塞操作传统 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据,NIO 提供 bytebuffer,分为堆内和堆外缓冲区,读写时均先放到该缓冲区中,然后由内核通过 channel传输到对端,堆外缓冲区不走内核,提升了性能13. list 中存放可重复字符串,如何删除某个字符串调用 iterator 相关方法删除倒删,防止正序删除导致的数组重排,index 跳过数组元素问题14. 有哪些 GC ROOTS(跟日常开发比较相关的是和此相关的内存泄露)所有 Java 线程当前活跃的栈帧里指向 GC 堆里的对象的引用,因此用不到的对象及时置 null,提升内存回收效率静态变量引用的对象,因此减少静态变量特别是静态集合变量的大小,集合存放的对象覆写 euqls()和 hashcode(),防止持续增长本地方法 JNI 引用的对象方法区中的常量引用的对象,因此减少在长字符串上调用 String.intern()classloader 加载的 class 对象,因此自定义 classloader 无效时及时置 null并且注意类加载器加载对象之间的隔离jvm 里的一些静态数据结构里指向 GC 堆里的对象的引用…本文转自茶轴的青春博客51CTO博客,如需转载,请自行联系原作者。原文链接
-
文章来源:CSDN博客,https://blog.csdn.net/mysqldba23/article/details/68066322# Server(服务器信息)redis_version:3.0.0 #redis服务器版本redis_git_sha1:00000000 #Git SHA1redis_git_dirty:0 #Git dirty flagredis_build_id:6c2c390b97607ff0 #redis build idredis_mode:cluster #运行模式,单机或者集群os:Linux 2.6.32-358.2.1.el6.x86_64 x86_64 #redis服务器的宿主操作系统arch_bits:64 #架构(32或64位)multiplexing_api:epoll #redis所使用的事件处理机制gcc_version:4.4.7 #编译redis时所使用的gcc版本process_id:12099 #redis服务器进程的pidrun_id:63bcd0e57adb695ff0bf873cf42d403ddbac1565 #redis服务器的随机标识符(用于sentinel和集群)tcp_port:9021 #redis服务器监听端口uptime_in_seconds:26157730 #redis服务器启动总时间,单位是秒uptime_in_days:302 #redis服务器启动总时间,单位是天hz:10 #redis内部调度(进行关闭timeout的客户端,删除过期key等等)频率,程序规定serverCron每秒运行10次。lru_clock:14359959 #自增的时钟,用于LRU管理,该时钟100ms(hz=10,因此每1000ms/10=100ms执行一次定时任务)更新一次。config_file:/redis_cluster/etc/9021.conf #配置文件路径# Clients(已连接客户端信息)connected_clients:1081 #已连接客户端的数量(不包括通过slave连接的客户端)client_longest_output_list:0 #当前连接的客户端当中,最长的输出列表,用client list命令观察omem字段最大值client_biggest_input_buf:0 #当前连接的客户端当中,最大输入缓存,用client list命令观察qbuf和qbuf-free两个字段最大值blocked_clients:0 #正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量# Memory(内存信息)used_memory:327494024 #由redis分配器分配的内存总量,以字节为单位used_memory_human:312.32M #以人类可读的格式返回redis分配的内存总量used_memory_rss:587247616 #从操作系统的角度,返回redis已分配的内存总量(俗称常驻集大小)。这个值和top命令的输出一致used_memory_peak:1866541112 #redis的内存消耗峰值(以字节为单位) used_memory_peak_human:1.74G #以人类可读的格式返回redis的内存消耗峰值used_memory_lua:35840 #lua引擎所使用的内存大小(以字节为单位)mem_fragmentation_ratio:1.79 #used_memory_rss和used_memory之间的比率,小于1表示使用了swap,大于1表示碎片比较多mem_allocator:jemalloc-3.6.0 #在编译时指定的redis所使用的内存分配器。可以是libc、jemalloc或者tcmalloc# Persistence(rdb和aof的持久化相关信息)loading:0 #服务器是否正在载入持久化文件rdb_changes_since_last_save:28900855 #离最近一次成功生成rdb文件,写入命令的个数,即有多少个写入命令没有持久化rdb_bgsave_in_progress:0 #服务器是否正在创建rdb文件rdb_last_save_time:1482358115 #离最近一次成功创建rdb文件的时间戳。当前时间戳 - rdb_last_save_time=多少秒未成功生成rdb文件rdb_last_bgsave_status:ok #最近一次rdb持久化是否成功rdb_last_bgsave_time_sec:2 #最近一次成功生成rdb文件耗时秒数rdb_current_bgsave_time_sec:-1 #如果服务器正在创建rdb文件,那么这个域记录的就是当前的创建操作已经耗费的秒数aof_enabled:1 #是否开启了aofaof_rewrite_in_progress:0 #标识aof的rewrite操作是否在进行中aof_rewrite_scheduled:0 #rewrite任务计划,当客户端发送bgrewriteaof指令,如果当前rewrite子进程正在执行,那么将客户端请求的bgrewriteaof变为计划任务,待aof子进程结束后执行rewrite aof_last_rewrite_time_sec:-1 #最近一次aof rewrite耗费的时长aof_current_rewrite_time_sec:-1 #如果rewrite操作正在进行,则记录所使用的时间,单位秒aof_last_bgrewrite_status:ok #上次bgrewriteaof操作的状态aof_last_write_status:ok #上次aof写入状态aof_current_size:4201740 #aof当前尺寸aof_base_size:4201687 #服务器启动时或者aof重写最近一次执行之后aof文件的大小aof_pending_rewrite:0 #是否有aof重写操作在等待rdb文件创建完毕之后执行?aof_buffer_length:0 #aof buffer的大小aof_rewrite_buffer_length:0 #aof rewrite buffer的大小aof_pending_bio_fsync:0 #后台I/O队列里面,等待执行的fsync调用数量aof_delayed_fsync:0 #被延迟的fsync调用数量# Stats(一般统计信息)total_connections_received:209561105 #新创建连接个数,如果新创建连接过多,过度地创建和销毁连接对性能有影响,说明短连接严重或连接池使用有问题,需调研代码的连接设置total_commands_processed:2220123478 #redis处理的命令数instantaneous_ops_per_sec:279 #redis当前的qps,redis内部较实时的每秒执行的命令数total_net_input_bytes:118515678789 #redis网络入口流量字节数total_net_output_bytes:236361651271 #redis网络出口流量字节数instantaneous_input_kbps:13.56 #redis网络入口kpsinstantaneous_output_kbps:31.33 #redis网络出口kpsrejected_connections:0 #拒绝的连接个数,redis连接个数达到maxclients限制,拒绝新连接的个数sync_full:1 #主从完全同步成功次数sync_partial_ok:0 #主从部分同步成功次数sync_partial_err:0 #主从部分同步失败次数expired_keys:15598177 #运行以来过期的key的数量evicted_keys:0 #运行以来剔除(超过了maxmemory后)的key的数量keyspace_hits:1122202228 #命中次数keyspace_misses:577781396 #没命中次数pubsub_channels:0 #当前使用中的频道数量pubsub_patterns:0 #当前使用的模式的数量latest_fork_usec:15679 #最近一次fork操作阻塞redis进程的耗时数,单位微秒migrate_cached_sockets:0 ## Replication(主从信息,master上显示的信息)role:master #实例的角色,是master or slaveconnected_slaves:1 #连接的slave实例个数slave0:ip=192.168.64.104,port=9021,state=online,offset=6713173004,lag=0 #lag从库多少秒未向主库发送REPLCONF命令master_repl_offset:6713173145 #主从同步偏移量,此值如果和上面的offset相同说明主从一致没延迟repl_backlog_active:1 #复制积压缓冲区是否开启repl_backlog_size:134217728 #复制积压缓冲大小repl_backlog_first_byte_offset:6578955418 #复制缓冲区里偏移量的大小repl_backlog_histlen:134217728 #此值等于 master_repl_offset - repl_backlog_first_byte_offset,该值不会超过repl_backlog_size的大小# Replication(主从信息,slave上显示的信息)role:slave #实例的角色,是master or slavemaster_host:192.168.64.102 #此节点对应的master的ipmaster_port:9021 #此节点对应的master的portmaster_link_status:up #slave端可查看它与master之间同步状态,当复制断开后表示downmaster_last_io_seconds_ago:0 #主库多少秒未发送数据到从库?master_sync_in_progress:0 #从服务器是否在与主服务器进行同步slave_repl_offset:6713173818 #slave复制偏移量slave_priority:100 #slave优先级slave_read_only:1 #从库是否设置只读connected_slaves:0 #连接的slave实例个数master_repl_offset:0 repl_backlog_active:0 #复制积压缓冲区是否开启repl_backlog_size:134217728 #复制积压缓冲大小repl_backlog_first_byte_offset:0 #复制缓冲区里偏移量的大小repl_backlog_histlen:0 #此值等于 master_repl_offset - repl_backlog_first_byte_offset,该值不会超过repl_backlog_size的大小# CPU(CPU计算量统计信息)used_cpu_sys:96894.66 #将所有redis主进程在核心态所占用的CPU时求和累计起来used_cpu_user:87397.39 #将所有redis主进程在用户态所占用的CPU时求和累计起来used_cpu_sys_children:6.37 #将后台进程在核心态所占用的CPU时求和累计起来used_cpu_user_children:52.83 #将后台进程在用户态所占用的CPU时求和累计起来# Commandstats(各种不同类型的命令的执行统计信息)cmdstat_get:calls=1664657469,usec=8266063320,usec_per_call=4.97 #call每个命令执行次数,usec总共消耗的CPU时长(单位微秒),平均每次消耗的CPU时长(单位微秒)# Cluster(集群相关信息)cluster_enabled:1 #实例是否启用集群模式# Keyspace(数据库相关的统计信息)db0:keys=194690,expires=191702,avg_ttl=3607772262 #db0的key的数量,以及带有生存期的key的数,平均存活时间
-
现象描述集群安装过程或执行DBService服务重启操作时,DBService服务启动失败,打印的错误日志中出现20051端口被占用等信息。 可能原因 [*]DBService使用的默认端口20051被其他进程占用。 [*]DBService进程没有停止成功,使用的端口未释放。 定位思路 [*]检查Manager管理界面显示的错误日志中是否存在如下内容:“already exists for port:20051”。 [*]恢复该故障大约需要10分钟。 处理步骤 [*]使用PuTTY工具,以root用户登录安装DBService报错的节点主机,执行netstat -nap | grep 20051命令查看使用端口的进程。 [*]使用kill命令强制终止使用20051端口的进程。 [*]约2分钟后,再次执行netstat -nap | grep 20051命令,查看是否还有进程占用该端口。 [*]是,执行步骤4 [*]否,执行步骤5 [*]确认占用该端口进程所属的服务,并修改为其他端口。 说明:20051端口为DBservice专用端口,其他进程不可以使用。 [*]分别在“/tmp”和“/var/run/FusionInsight-DBService”目录下执行find . -name '*20051*'命令,将搜索到的文件全部删除。 [*]登录FusionInsight Manager,单击“服务管理”,重启DBService服务。
-
现象描述在Yarn上运行MapReduce任务,在任务执行过程中出现一些Map或者Reduce被中止的情况,出现“Task Failed”时在jobhistoryserver的原生WebUI界面显示如下日志信息:Container [pid=143419,containerID=container_1432208368012_0016_01_000066] is running beyond virtual memory limits. Current usage: 769.8 MB of 1 GB physical memory used; 3.8 GB of 2.5 GB virtual memory used. Killing container. 可能原因从日志显示这个“container killed”直接原因是虚拟内存使用超过了限定值,YARN的NodeManager监控到内存使用超过阈值,强制终止该container进程。 定位思路 [*]检查任务执行失败的直接原因是物理内存溢出还是虚拟内存溢出。 [*]检查失败的Map或Reduce的物理内存配置及虚拟内存的比率配置,需要检查的配置项包括“mapreduce.map.memory.mb”、“mapreduce.reduce.memory.mb” 及“yarn.nodemanager.vmem-pmem-ratio”。 处理步骤 [*]如果是物理内存溢出,则根据实际报错日志中显示需要的物理内存值,调整配置项“mapreduce.map.memory.mb”、“mapreduce.reduce.memory.mb”到合适的值 [*]如果是虚拟内存溢出,则根据实际报错日志中显示需要的虚拟内存值,按照以下公式:“yarn.nodemanager.vmem-pmem-ratio > 实际使用的虚拟内存值/map或者reduce的物理内存”进行调整比率
-
本帖最后由 小柴不加胡 于 2018-5-14 17:58 编辑消息队列(MQ)是目前系统架构中主流方式,在大型系统及大数据中广泛采用。对任何架构或应用来说, MQ都是一个至关重要的组件。今天我们就来细数MQ那些不得不说的好处。好处一:解耦在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间**了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。比如我们的货款抵扣业务场景,用户生成订单发送MQ后立即返回,结算系统去消费该MQ进行用户账户金额的扣款。这样订单系统只需要关注把订单创建成功,最大可能的提高订单量,并且生成订单后立即返回用户。而结算系统重点关心的是账户金额的扣减,保证账户金额最终一致。好处二:冗余有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。MQ把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多MQ所采用的"**-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。好处三:扩展性因为MQ解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。就比如DMS分布式消息服务,不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。好处四:灵活性和峰值处理能力在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用MQ能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。还是以订单系统和结算系统场景为例,如果订单系统通过RPC框架来调用结算系统,在有高峰促销的情况下生成订单的量会非常大,而且由于生成订单的速度也非常快,这样势必会给结算系统造成系统压力,服务器利用率则会偏高,但在不是高峰的时间点订单量比较小,结算系统的服务器利用率则会偏低。对于结算系统来说就会出现下面这样的高峰波谷现象图。那么如果通过MQ的方式,将订单存储到MQ队列中,消费端通过拉取的方式,并且拉去速度有消费端来控制,则就可以控制流量趋于平稳。这样对于结算系统来讲,就达到了削峰填谷的目的。或者说起到了流控的目标好处五:可恢复性系统的一部分组件失效时,不会影响到整个系统。MQ降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。好处六:顺序保证在大多使用场景下,数据处理的顺序都很重要。大部分MQ本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka保证一个Partition内的消息的有序性。好处七:缓冲在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行———写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。好处八:异步通信很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。对系统而言,MQ消息队列机制能承受更大访问压力;对架构而言,松耦合,系统维护性方便;而对用户而言,想要系统访问更快、系统体验更好,自然首选DMS啦!
-
本帖最后由 小柴不加胡 于 2018-5-11 11:28 编辑背景国内某大型税务系统,业务应用分布式上云改造。 业务难题15178如上图所示是模拟客户的业务网页构建的一个并发访问模型。用户在页面点击从而产生一个HTTP请求,这个请求发送到业务生产进程,就会启动一个投递线程(Deliver Thread)调用Kafka的SDK接口,并发送3条消息到DMS(分布式消息服务),每条消息大小3k,需要等待3条消息都被处理完成后才会返回请求响应⑧。当消息达到DMS后,业务消费进程调用Kafka的消费接口把消息取出来,然后将每条消息放到一个响应线程(Response Thread)中进行处理,响应线程处理完后,通过HTTP请求通知投递线程,投递线程收到响应后返回回复响应。 100并发访问时延500ms,未达成用户业务要求客户提出了明确的要求:每1个两核的ECS要能够支撑并发访问量100,每条消息端到端的时延范围是几十毫秒,即从生产者发送开始到接收到消费者响应的时间。客户实测在使用了DMS的Kafka 队列后,并发访问量为100时时延高达到500ms左右,甚至出现达到秒级的时延,远未达到客户提出的业务诉求。相比较而言,客户在Pod区使用的是自己搭建的原生Kafka,在并发访问量为100时测试到的时延大约只有10~20ms左右。那么问题来了,在并发访问量相同的条件下,DMS的Kafka 队列与Pod区自建的原生Kafka相比为什么时延会有这么大的差异呢?我们DMS的架构师 Mr. Peng对这个时延难题进行了一系列分析后完美解决了这个客户难题,下面就让我们来看看他的心路历程。 难题剖析根据模拟的客户业务模型,Mr. Peng在华为云类生产环境上也构造了一个测试程序,同样模拟构造了100的并发访问量,通过测试发现,类生产环境上压测得到的时延平均时间在60ms左右。类生产上的时延数值跟客户在真实生产环境上测到的时延差距这么大,这是怎么回事呢?问题变得扑朔迷离起来。Mr. Peng当机立断,决定就在华为云现网上运行构造的测试程序,来看看到底是什么原因。同时,在客户的ECS服务器上,也部署了相同的测试程序,模拟构建了100的并发量,得到如下的时延结果对比表:15181 表1 华为云现网与类生产环境时延对比表 从时延对比表的结果看来,Mr. Peng发现,即使在相同的并发压力下,华为云现网的时延比类生产差很多。Mr.Peng意识到,现在有2个问题需要分析:为什么华为云现网的时延会比类生产差?DMS的Kafka队列时延比原生自建的Kafka队列时延表现差的问题怎么解决?Mr. Peng分析如下: 时延分析回归问题的本质,DMS Kafka队列的时延到底是怎么产生的?可控的端到端时延具体分为哪些?Mr.Peng给出了如下的计算公式:总时延 = 入队时延 + 发送时延 + 写入时延 + 复制时延+ 拉取时延让我们来依次了解一下,公式中的每一项都是指什么。入队时延: 消息进入Kafka sdk后,先进入到要发送分区的队列,完成消息打包后再发送,这一过程所用的时间。发送时延:消息从生产者发送到服务端的时间。写入时延:消息写入到Kafka Leader的时间。复制时延:消费者只可以消费到高水位以下的消息(即被多个副本都保存的消息),所以消息从写入到Kafka Leader,到所有副本都写入该消息直到上涨至高水位这段时间就是消息复制的时延。拉取时延:消费者采用pull模式拉取数据,拉取过程所用的时间。 (1) 入队时延现网是哪一部分的时延最大呢?通过我们的程序可以看到,入队列等待发送时延非常大,如下图:15179 即消息都等待在生产端的队列中,来不及发送! 我们再看其他时延分析,因为无法在现网测试,我们分别在类生产测试了相同压力的,测试其他各种时延如下:(2) 复制时延以下是类生产环境测试的1并发下的15182 从日志上看,复制时延包括在remoteTime里面,当然这个时间也会包括生产者写入时延比较慢导致的,但是也从一定的程度反映复制时延也是提升性能时延的一个因素。 (3) 写入时延因为用户使用的是高吞吐队列,写入都是异步落盘,我们从日志看到写入时延非常低(localTime),可以判断不是瓶颈。发送时延与拉取时延都是跟网络传输有关系,这个优化主要是通过调TCP的参数来决定的。轻轻松松把Kafka消息时延秒降10倍,就用华为云DMS
-
本帖最后由 大脸猫爱吃鱼 于 2018-4-17 10:06 编辑 【作者】文/华为云微服务引擎团队 【摘要】近年来越来越多的企业开始实践微服务,本文分为上下两篇介绍微服务框架ServiceComb如何帮助企业应用进行微服务化,实现快速交付,并可靠地运行在云端。下篇介绍ServiceComb的通信处理设计。 【参考】打造一个企业级应用的微服务开发框架(上)---从服务注册中心到服务管理中心 近年来越来越多的企业开始实践微服务,而微服务在企业应用落地的过程,面临着微服务开发框架的选型,无论是自研还是选择第三方框架都不得不考虑的问题包括:微服务框架是否具备高可靠性,任何时间不能中断业务;微服务框架是否能够实现高速通信性能,保证业务从单体架构向微服务架构切换时,性能下降不会太多。本文从服务管理中心、通信处理两个模块来介绍华为开源微服务框架SeviceComb如何帮助企业应用快速具备高性能的通信能力以及高可靠的服务管理能力。下篇将详细介绍ServiceComb的通信处理。 ServiceComb通信处理详解1 整体介绍ServiceComb的底层通信框架依赖Vert.x.vertx标准工作模式为高性能的reactive模式,其工作方式如下图所示:13913图9 reactive模式工作方式业务逻辑直接在eventloop中执行,整个业务流程中没有线程切换,所有的等待逻辑都是异步的,只要有任务,则不会让线程停下来,充分、有效地利用系统资源。vertx生态中包含了业界常用各种组件的reactive封装,包括jdbc、zookeeper、各种mq等等。但是reactive模式对业务的要求相当高,业务主流程中不允许有任何的阻塞行为。因此,为了简化上层业务逻辑,方便开发人员的使用,在Vertx之上提供同步模式的开发接口还是必不可少的,例如: [*]各种安全加固的组件,只提供了同步工作模式,比如redis、zookeeper等等 [*]一些存量代码工作于同步模式,需要低成本迁移 [*]开发人员技能不足以控制reactive逻辑 所以ServiceComb底层基于vertx,但在vertx之上进行了进一步封装,同时支持reactive及同步模式。工作于Reactive模式时,利用Vertx原生的能力,不必做什么额外的优化,仅需要注意不要在业务代码中阻塞整个进程。而同步模式则会遭遇各种并发性能问题。,本文描述同步模式下的各种问题以及解决方案。RESTful流程中,连接由vertx管理,当前没有特别的优化,所以本文中,连接都是指highway流程中的tcp连接。 2 同步模式下的整体线程模型13914图10 同步模式下的整体线程模型 [*]一个微服务进程中,为transport创建了一个独立的vertx实例 [*]Eventloop是vertx中的网络、任务线程 [*]一个vertx实例默认的Eventloop数为:2 * Runtime.getRuntime().availableProcessors() 3 服务消费者端在服务消费者端,主要需要处理的问题是如何更加高效地把请求推送到服务提供者上去,然后拿到服务提供者的返回信息。所以在这一端我们主要关注“如何更高效的发送数据”这个话题。 3.1 单连接模型最简单的单连接模型13916图11 最简单的单连接模型 从模型图中,我们可以看到,所有的consumer线程,如果向同一个目标发送数据,必然产生资源竞争,此时实际的处理如下: [*]Connection.send内部直接调用Vertx的socket.write(buf),是必然加锁互斥的。 这必然导致大量并发时,大多数consumer线程都无法及时地发送自己的数据。 [*]Socket.write内部会调用netty的channel.write,此时会判断出执行线程不是eventloop线程,所以会创建出一个任务并加入到eventloop任务队列中,如果eventloop线程当前在睡眠态,则立即唤醒eventloop线程,异步执行任务。 这导致频繁的任务下发及线程唤醒,无谓地增加cpu占用,降低性能。优化的单连接模型13917图12 优化的单连接模型 在优化模型中: [*]每个TcpClientConnection额外配备一个CAS消息队列 [*]Connection.send不再直接调用vertx的write方法,而是: 所有消息保存到CAS队列中,减少入队竞争 通过原子变量判定,只有入队前CAS队列为空,才向eventloop下发write任务,唤醒eventloop线程 在eventloop中处理write任务时,将多个请求数据包装为composite buffer,批量发送,减少进入os内核的次数,提高tcp发送效率。代码参见: https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/foundations/foundation-vertx/src/main/java/io/servicecomb/foundation/vertx/client/tcp/TcpClientConnection.java[code]io.servicecomb.foundation.vertx.client.tcp.TcpClientConnection.packageQueue io.servicecomb.foundation.vertx.client.tcp.TcpClientConnection.send(AbstractTcpClientPackage,long, TcpResponseCallback)[/code]https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/foundations/foundation-vertx/src/main/java/io/servicecomb/foundation/vertx/tcp/TcpConnection.java[code]io.servicecomb.foundation.vertx.tcp.TcpConnection.write(ByteBuf) io.servicecomb.foundation.vertx.tcp.TcpConnection.writeInContext()[/code]进行此项优化后,在同一环境下测试2组数据,可以看到性能有明显提升(不同硬件的测试环境,数据可能差异巨大,不具备比较意义):TPSLatency(ms)CPUTPS提升比例时延提升比例ConsumerProducer(新-旧)/旧(旧-新)/新优化前819861.22290%290%77.31%43.61%优化后1453690.688270%270%表6 单连接模型优化前后性能对比3.2 多连接模型在单连接场景下进行相应的优化后,我们发现其实还有更多的优化空间。因为在大多数场景中,实际机器配置足够高,比如多核、万兆网络连接、网卡支持RSS特性等。此时,需要允许一对consumer与producer之间建立多条连接来充分发挥硬件的性能。13918图13 多连接模型 [*]允许配置多个eventloop线程 在microservice.yaml中进行以下配置:[code]cse: highway: client: thread-count: 线程数 server: thread-count: 线程数[/code] [*]Consumer线程与eventloop线程建立均衡的绑定关系,进一步降低consumer线程的竞争概率。 代码参见:https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/foundations/foundation-vertx/src/main/java/io/servicecomb/foundation/vertx/client/ClientPoolManager.java[code]io.servicecomb.foundation.vertx.client.ClientPoolManager.findThreadBindClientPool()[/code]优化后的性能对比:TPSLatency (ms)CPUTPS提升比例时延提升比例ConsumerProducer(新-旧)/旧(旧-新)/新简单单连接*105434420.9192305%1766%72.81%42.11%CAS单连接*109391170.5321960%1758%表7 多连接下线程模型优化前后性能对比 每请求大小为1KB,可以看到万兆网的带宽接近吃满了,可以充分利用硬件性能。(该测试环境,网卡支持RSS特性。)4 服务提供者端不同于服务消费者,服务提供者主要的工作模式就是等待消费者的请求,然后处理后返回应答的信息。所以在这一端,我们更加关注“如何高效的接收和处理数据”这件事情。同步模式下,业务逻辑和IO逻辑分开,且根据“隔离仓”原则,为了保证整个系统更加稳定和高效地运行,业务逻辑本身也需要在不同隔离的区域内运行。而这些区域,就是线程池。所以构建服务提供者,就需要对线程池进行精细的管理。下面是针对线程池的各种管理方式。4.1 单线程池(ThreadPoolExecutor)下图表示的是将业务逻辑用单独的线程池实现的方式。在这种方式下,IO仍然采用异步模式,所有接到的请求放入队列中等待处理。在同一个线程池内的线程消费这个队列并进行业务处理。13919图14 单线程池实现方式在这种方式下,有以下瓶颈点: [*]所有的eventloop向同一个BlockingQueue中提交任务 [*]线程池中所有线程从同一个BlockingQueue中抢任务执行 ServiceComb默认不使用这种线程池。4.2 多线程池(ThreadPoolExecutor)为规避线程池中Queue带来的瓶颈点,我们可以使用一个Executor将多个真正的Executor包起来。13920图15 多线程池实现方式 [*]Eventloop线程与线程池建立均衡的绑定关系,降低锁冲突概率 [*]相当于将线程分组,不同线程从不同Queue中抢任务,降低冲突概率 ServiceComb默认所有请求使用同一个线程池实例:io.servicecomb.core.executor.FixedThreadExecutorFixedThreadExecutor内部默认创建2个真正的线程池,每个池中有CPU数目的线程,可以通过配置修改默认值:[code]servicecomb: executor: default: group: 内部真正线程池的数目 thread-per-group: 每个线程池中的线程数[/code]代码参见:https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/core/src/main/java/io/servicecomb/core/executor/FixedThreadExecutor.java4.3 隔离仓业务接口的处理速度有快有慢,如果所有的请求统一在同一个Executor中进行处理,则可能每个线程都在处理慢速请求,导致其他请求在Queue中排队。此时,可以根据业务特征,事先做好规划,将不同的业务处理按照一定的方式进行分组,每个组用不同的线程池,以达到隔离的目的。13921图16 隔离仓 隔离仓的实现依托到ServiceComb灵活的线程池策略,具体在下一节进行描述。4.4 灵活的线程池策略ServiceComb微服务的概念模型如下:13922图17 ServiceComb微服务概念模型可以针对这3个层次进行线程池的配置,operation与线程池之间的对应关系,在启动阶段既完成绑定。operation与线程池之间的绑定按以下逻辑进行: [*]查看配置项cse.executors.Provider.[schemaId].[operationId]是否有值 如果有值,则将值作为beanId从spring中获取bean实例,该实例即是一个Executor 如果没有值,则继续尝试下一步 [*]使用相同的方式,查看配置项cse.executors.Provider.[schemaId]是否有值 [*]使用相同的方式,查看配置项cse.executors.default是否有值 [*]以”cse.executor.groupThreadPool”作为beanId,获取线程池(系统内置的FixedThreadExecutor) 代码参见:https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/core/src/main/java/io/servicecomb/core/executor/ExecutorManager.java 按以上策略,用户如果需要创建自定义的线程池,需要按以下步骤执行: [*]实现java.util.concurrent.Executor接口 [*]将实现类定义为一个bean [*]在microservice.yaml中将线程池与对应的业务进行绑定 4.5 线程池模型总结如上一节所述,在默认多线程池的基础上,CSE提供了更为灵活的线程池配置。“隔离仓”模式的核心价值是实现不同业务之间的相互隔离,从而让一个业务的故障不要影响其他业务。这一点在CSE中可以通过对线程池的配置实现。例如,可以为不同的operation配置各自独立的线程池。 另外,灵活性也带来了一定的危险性。要避免将线程池配置为前面提到的“单业务线程池”模式,从而为整个系统引入瓶颈点。 ServiceComb除了在华为云微服务引擎商用之外,也于2017年12月全票通过进入Apache孵化器。欢迎感兴趣的读者前往开源社区和我们讨论切磋,希望此文可以给正在进行微服务方案实施的读者们一些启发。
-
本帖最后由 danglf 于 2018-3-23 15:18 编辑副本集方式是MongoDB最常用的部署方式,副本集使用之前必须先初始化, 副本集初始化是通过执行replSetInitiate完成的,本文主要分析副本集收到该命令后背后的逻辑(本文基于MongoDB3.2.18分析)。1.1 入口 当用户执行replSetInitiate 命令后, 最终会调用到CmdReplSetInitiate类的 run 方法,CmdReplSetInitiate 在Replset_commands.cpp里面定义。对于Mongod 启动后, 用户执行各种命令和查询操作, 是如何运行起来的,及command内部原理和体系结构 ,后续会写一篇专门的文章去分析。代码如下: 13173 13174 总体流程如下1 校验mongod的配置文件是不是配置配置副本集参数。2 对于副本集配置参数进行补齐3 核心语句, 调用getGlobalReplicationCoordinator()->processReplSetInitiat, 进入复制集初始化流程。1.2 主体流程: 1 ) 判断当前状态是不是可以进行初始化,必须是副本集之前没有初始化过才可以,否则报错 2) 构建内部初始化需要的配置数据, 并判断初始化副本集中的副本集名称和配置文件中的是不是一致,初次以外还有其他的逻辑校验 3 ) 判断当前实例状态, 发起选举,选出主 备节点 4)启动所有后台线程: a)后台拉取主节点oplog线程 b) 向主节点同步自己复制状态的线程 c) 定时快照线程。 相关流程对应代码如下 1)状态判断 replication_coordinator_impl.cpp –>processReplSetInitiate 13175 2) 部分参数校验 13176 3) 发起选举 processReplSetInitiate->_finishReplSetInitiate->_performPostMemberStateUpdateAction->_startElectSelfV1 4) 启动后台线程: 13177 以上就是replSetInitiate 命令后,mongod 内部的大致流程, 分析比较粗, 实际上每一个流程,如选举的内部机制, 初始化同步过程, 增量同步过程, 备节点如何应用oplog,每一个都可以作为专项来分析,后面会持续进行, 文中如有不正确的地方,也欢迎指出并讨论。
-
客户简介 中软独家中标税务核心征管系统,全国34个省国/地税。电子税务局15省格局。 大数据国家税务总局局点,中国软件电子税务局技术路径:核心征管 + 纳税服务 业务应用分布式上云改造。业务难题12226 如上图所示是模拟客户的业务网页构建的一个并发访问模型。用户在页面点击从而产生一个HTTP请求,这个请求发送到业务生产进程,就会启动一个投递线程(DeliverThread)调用Kafka的SDK接口,并发送3条消息到DMS(分布式消息服务),每条消息大小3k,需要等待3条消息都被处理完成后才会返回请求响应⑧。当消息达到DMS后,业务消费进程调用Kafka的消费接口把消息取出来,然后将每条消息放到一个响应线程(Response Thread)中进行处理,响应线程处理完后,通过HTTP请求通知投递线程,投递线程收到响应后返回回复响应。 100并发访问时延500ms,未达成用户业务要求客户提出了明确的要求:每1个两核的ECS要能够支撑并发访问量100,每条消息端到端的时延范围是几十毫秒,即从生产者发送开始到接收到消费者响应的时间。客户实测在使用了DMS的Kafka 队列后,并发访问量为100时时延高达到500ms左右,甚至出现达到秒级的时延,远未达到客户提出的业务诉求。相比较而言,客户在Pod区使用的是自己搭建的原生Kafka,在并发访问量为100时测试到的时延大约只有10~20ms左右。那么问题来了,在并发访问量相同的条件下,DMS的Kafka 队列与Pod区自建的原生Kafka相比为什么时延会有这么大的差异呢?我们DMS的架构师 Mr. Peng对这个时延难题进行了一系列分析后完美解决了这个客户难题,下面就让我们来看看他的心路历程。难题剖析根据模拟的客户业务模型,Mr. Peng在华为云类生产环境上也构造了一个测试程序,同样模拟构造了100的并发访问量,通过测试发现,类生产环境上压测得到的时延平均时间在60ms左右。类生产上的时延数值跟客户在真实生产环境上测到的时延差距这么大,这是怎么回事呢?问题变得扑朔迷离起来。Mr. Peng当机立断,决定就在华为云现网上运行构造的测试程序,来看看到底是什么原因。同时,在客户的ECS服务器上,也部署了相同的测试程序,模拟构建了100的并发量,得到如下的时延结果对比表:调优前时延现网时延(ms)类生产时延(ms)100并发500ms ~ 4000ms40ms ~ 80 ms1并发31ms6msPing测试0.9ms ~ 1.2ms0.3ms ~ 0.4ms表1 华为云现网与类生产环境时延对比表 从时延对比表的结果看来,Mr. Peng发现,即使在相同的并发压力下,华为云现网的时延比类生产差很多。Mr.Peng意识到,现在有2个问题需要分析:为什么华为云现网的时延会比类生产差?DMS的Kafka队列时延比原生自建的Kafka队列时延表现差的问题怎么解决?Mr. Peng陷入了沉思之中。 欲知后事如何,请看下【云中间件锦囊妙计】巧用参数调优为客户解决时延高的难题(中篇)。云中间件锦囊妙计系列专题旨在为开发者、用户和对云中间件技术有兴趣的同学提供一系列的客户案例、技术干货、疑难杂症解决思路的分享,欢迎关注我们的小伙伴在下方留言,共同学习和分享,请大家多多支持哦~
-
本帖最后由 云彩飞扬 于 2018-2-7 11:50 编辑1.启动一个Slave进程后,它会向Master发送一个SYNC Command,请求同步连接。2.无论是第一次连接还是重新连接,Master都会启动一个后台进程,将数据快照保存到数据文件中,同时Master会记录所有修改数据的命令并缓存在数据文件中。3.后台进程完成缓存操作后,Master就发送数据文件给Slave,Slave端将数据文件保存到硬盘上,然后将其在加载到内存中,接着Master就会所有修改数据的操作,将其发送给Slave端。4.若Slave出现故障导致宕机,恢复正常后会自动重新连接,Master收到Slave的连接后,将其完整的数据文件发送给Slave。5.如果Mater同时收到多个Slave发来的同步请求,Master只会在后台启动一个进程保存数据文件,然后将其发送给所有的Slave,确保Slave正常。其优点:1.同一个Master可以拥有多个Slaves。2.Master下的Slave还可以接受同一架构中其它slave的链接与同步请求,实现数据的级联复制,即Master->Slave->Slave模式;3.Master以非阻塞的方式同步数据至slave,这将意味着Master会继续处理一个或多个slave的读写请求;4.Slave端同步数据也可以修改为非阻塞是的方式,当slave在执行新的同步时,它仍可以用旧的数据信息来提供查询;否则,当slave与master失去联系时,slave会返回一个错误给客户端;5.主从复制具有可扩展性,即多个slave专门提供只读查询与数据的冗余,Master端专门提供写操作;通过配置禁用Master数据持久化机制,将其数据持久化操作交给Slaves完成,避免在Master中要有独立的进程来完成此操作。
上滑加载中
推荐直播
-
华为AI技术发展与挑战:集成需求分析的实战指南
2024/11/26 周二 18:20-20:20
Alex 华为云学堂技术讲师
本期直播将综合讨论华为AI技术的发展现状,技术挑战,并深入探讨华为AI应用开发过程中的需求分析过程,从理论到实践帮助开发者快速掌握华为AI应用集成需求的框架和方法。
去报名 -
华为云DataArts+DWS助力企业数据治理一站式解决方案及应用实践
2024/11/27 周三 16:30-18:00
Walter.chi 华为云数据治理DTSE技术布道师
想知道数据治理项目中,数据主题域如何合理划分?数据标准及主数据标准如何制定?数仓分层模型如何合理规划?华为云DataArts+DWS助力企业数据治理项目一站式解决方案和应用实践告诉您答案!本期将从数据趋势、数据治理方案、数据治理规划及落地,案例分享四个方面来助力企业数据治理项目合理咨询规划及顺利实施。
去报名
热门标签