运维不迷茫,虎牙直播的SRE实践与思考

对运维的思考

1、传统运维窘境

我们运维一般是这样的,把软硬件资源按计划准备好,按需求安装起来,让业务快速上线,让服务器上进程和和业务正常,处理各种故障,响应各方的需求。我们经常陷在处理这些工作上,成为操作员、保姆、救火队员。

我们运维也都很努力,也不想每次被动救火,希望能主动控制服务状态,体现我们的技术价值,做了很多有效的工作。运维人员是非常勤奋、爱学习的,具有非常广泛的技术视野和技能池。但在技术生态中好像总是处于一种较为弱势的、从属的、被动的地位。

2、运维技术深度和价值

我个人也是在不断思考和学习, 几年前也发现自身传统运维的局限所在。尝试过深入业务,通过运维人员掌握更多业务知识,了解技术架构,更深度参与线上业务维护来提升价值。 比如,我们深入掌握了Nginx的运维知识和优化技术,掌握了MySQL的优化技术,掌握了PHP/Java的技术。这确实能一定程度提升业务质量,不过靠的是个人的主动性和某方面技术的深入,没有提升为SRE这么高的一种方法论体系。可以说我们一直在实践中进行摸索, 而SRE帮我们梳理了方法,树立了标杆,指引了方向。

3、DevOps和SRE的关系

DevOps 是一种运维研发协作,甚至是整个业务链路上的敏捷协作,是一种的文化和运动,而SRE是DevOps的一种实践、一种方法论。SRE对我们最大收益是提供了一种方法论体系,来指导我们运维工作,也提供了一些具体的实践来供我们参考。

今天想简单跟大家分享下我们在运维上跟SRE比较类似的经验。

我们的运维现状与挑战

1、直播平台的挑战

YY是秀场直播的开创者,而虎牙直播则是国内游戏直播的先行者,此外,虎牙直播是从YY里面分出来的一家公司,承袭了YY的部分技术基因。现在做直播,有很多CDN厂商可以选择,但我们在开始做直播时还没有这么多厂商,很多技术靠自己研究和实现,所以我们很早就有一套直播体系

直播平台

大家看到这个直播技术的流程,首先是主播开播,这里有N种方式可以开播,然后有多种推流的方式,将其推到某一条线路上,可能是我们自己的线路,也可能是CDN的线路,而后CDN要转推到多家去,还要在整个网络里分发到CDN边缘,通过转码,转码到各种地区的运营商,最后观众通过各种用户端连接到这些边缘节点的音视频流,观众可能是并发百万级的。

整个过程路径很长,需要在几秒之内完成,跟一般的Web类互联网业务是不同的,是我个人经历过的最复杂的互联网应用。

2、技术复杂性和运维着力点

对YY来说,在开始做直播的时候,还是有一定的技术开创性。但在这方面,我们运维的挑战比较大。看到下面主播到观众遍布的这张架构图:

架构

一方面,虎牙直播目前是异构多云的架构,从整个链路看,任何观众都可以看到任何线路上任何主播的情况,复杂度高。

另一方面,相对来说,研发同学以及各个团队会比较关注自己环节上的事情,所以在我们引入了多CDN以后,不仅技术和管理复杂性大幅提高,而且视频流路径在这么复杂的场景下,必须深入音视频运维工作,这对运维质量和运维人员技能提出了更高的要求。

因此,由于直播平台不同以往任何架构的特殊性,以及当时视频板块技术的有限性,促使我们必须尽快找到运维的着力点。后来,我们接轨了近年来一直倡导的DevOps和SRE解决了这一困局,接下来便分享虎牙直播在这方面上的一些实践经验。

关于SRE理念

1、SRE回顾

SRE

SRE由三个单词组成的:S、R、E,我先简单解释一下:

  • 第一个E。有两种理解:一则是Engineer工程师,一则是Engineering工程化。在国内的很多分享文章里,讲得更多是倾向于工程师这个概念。我个人更认同E是工程和工程化,比如我们不叫SRE岗位,但做的事情是提升业务可靠性的“工程”,甚至事情不是我们单独在做,而是和业务研发联合来做。
  • 第二个R。Reliability,意思是可靠性,表达在业务上、工程上就是质量,理解为对外部最终用户的质量和价值。
  • 第三个S。Site/Service,即运维对象、网站服务、业务服务和线上的各种服务。

2、SRE理念

从另外一个角度来看SRE岗位,Google是招聘软件工程师培养成为SRE的,而在国内,我们传统运维工程师如何转型,是一个值得思考的问题。目前我们在传统Ops模式上的主要问题是:过分关注如何解决那些常规问题、紧急问题,而不是找到根本原因和减少紧急事件的数量。大家都不喜欢风险,都不喜欢不期而遇、随时可能出现的故障,可故障经常不请自来,怎么办?SRE给出的答案是:

第一,拥抱风险。并且把风险识别出来,用SLI/SLO加以评估、度量、量化出来,最终达到消除风险的目的。

第二,质量目标。一般可能认为没有故障就是正常,万事大吉了。SRE要求明确定义SLI、SLO,定量分析某项服务的质量,而监控系统是 SRE 团队监控服务质量和可用性的一个主要手段。通过设立这样的指标,可量化质量,使得我们有权力PK业务研发,也能跟老板对话,取得更大的话语权。

第三,减少琐事。SRE理念里讲究要花50%左右的时间在工程研发上,剩余50%的时间用来做一些如资源准备、变更、部署的常规运维,以及查看和处理监控、应急事务处理、事后总结等应急处理工作。如果一个屏幕上十几个窗口,各种刷屏,但却不彻底解决问题,这时就需要用更好的方式——自动化、系统化、工具化的方式 ,甚至是自治的方式去处理这些“琐事”。

这里对传统运维的思维也有一些挑战,因为我们日常做得最多的工作在SRE中是被定义为价值不高的琐事,运维不是操作,“运维”是个工作内容,人工或是软件都可以做。在谷歌里,会要求SRE有能力进行自动化工具研发,对各种技术进行研究 ,用自动化软件完成运维工作 ,并通过软件来制定、管理合理SLI/SLO。

第四,工程研发。我个人理解的工程研发工作包括三个方面:

  1. 推进产品研发改进架构,经常和研发探讨架构、集群、性能问题;
  2. 引入新的运维技术,基础组件要hold住,比如TSDB、Bosun、Consul、Zipkin等;
  3. 自研工程技术、平台、工具、系统、基础组件、框架。

我们目前在这些方面都有一些开始在探索和转型,下面将展开详谈。

虎牙直播的SRE实践

1、质量指标SLI

我们来看看直播平台面对的风险和质量指标,以及我们是怎么样通过工程手段来提升质量的。

直播流媒体技术中有很多指标,内部大概有上百个指标,常用的也有十几个,下面是音视频方面的一些场景:

(1)直播质量指标

  • 主播端:开播、采集、处理、推流失败、崩溃
  • 观众端:进不了直播、拉视频失败、黑屏、花屏、卡顿、延迟

(2)卡顿分析

当我们把卡顿单独切出来进行分析,会发现它是由比如平台、主播、线路、地区、运营商、时间段、端、时长、用户数量、卡顿率等多方面因素制约的。虽然卡顿是平台中最常见也是最重要的质量指标,但什么是卡顿、什么是卡顿率?业界目前没有统一的定义。下面我们以虎牙的定义,来讲讲直播的SLI、SLO。

2、SLI卡顿定义

  • 第一种情况,由延时造成的卡顿。

如果没有丢帧,但持续超过一定时间没有画面就算是卡顿。(比如1,2是连续的丢帧,2比应该播放时刻晚数百ms算一个卡顿)

  • 第二种情况,由丢帧造成的卡顿。

如果连续丢帧大于1个,而且持续数百ms没有画面就是产生了卡顿。

  • 第三种情况,由跳帧造成的卡顿。

如果连续丢帧大于1个,有连续画面,但丢掉的帧播放时长大于一定时间的情况。

(即通过增加丢掉的帧前面帧的播放时长,可以有效减少卡顿,但后续画面接上去时会产生画面跳动感觉,超过一定时间用户就能察觉。)

  • 最后一种情况,是由视频源帧率低造成的卡顿。

如果可解码帧帧率低于10帧,以及丢帧率大于20%时会发生卡顿。(因为视频随机丢帧后,导致帧率下降,容易被人眼看出来)

3、卡顿率SLI定义

有了卡顿之后,怎么把卡顿计算成卡顿率呢?业界没有统一的定义,有人统计卡顿用户比例,卡顿时长方法,但这些太粗了,都不能满足我们的要求。而且很多的维度分析,都不能很好地衡量质量和做监控,卡顿率这事其实有点小复杂,这里说说我们的一些计算方法。

(1)卡顿数据

对于卡顿的数据,我们有5秒、20秒的粒度上报,而且上报的是有多维度信息。那卡顿率怎么来定义?

(2)卡顿率:卡次数/用户数

我们稍稍分析下,从纵向看,有全平台或某条流某个时刻的卡顿率,这个很好理解,单单统计这个时刻的卡顿上报次数/上报样本数即可。

从横向看,单条流在直播时段内的卡顿率,比如一个主播的卡顿率,卡顿样本次数累加/上报样本数累加;从全体来看,可以分全平台每天的卡顿率。此外,我们还有计算线路卡顿率以及其它多种维度的卡顿率。

但这里会有一个小小的问题:一个直播间有小部分用户一直卡和在一小段时间内一直卡顿,卡顿率可能都是一样的,这显然不公平,于是我们在这中间又多定义了中度卡顿率和重度卡顿率。

其中,当某个时刻卡顿率区间范围为10%-40%,属于中度卡顿率,超过40%的。直播平台带宽是非常猛的,每年可能有几个亿的带宽费用要付出去,而付给每一家都是一个很大的量。老板很重视这个情况,如果没有这个卡顿率,我们很难去跟老板上报质量如何,应该分配多少量给哪一家,得有数据可以作为决策的依据。

4、全链路监控

监控

有了卡顿率之后,接下来就是如何做监控。这是我们直播视频质量全链路监控围绕视频直播平台的场景,我们构建了从主播视频源到观众端观看直播所有环节的点,实时采集,展示、定位、告警系统。这个系统能够帮助运维人员快速准确定位到直播流卡顿的现象和原因,也能评估长期总体质量。各个环节研发往往对自己的节点感兴趣,由运维对整体负责,串起来。在这里面,整合多环节质量数据,体现了DevOps的理念;通过构建系统来做,体现了SRE的工程化理念;从上报到监控,告警、评估闭环,能力落地到系统,我们不是靠专家,而是解放了专家。

有了全链路系统后,我们还做了一个告警和事后问题分析总结的反馈闭环。

5、故障处理和质量闭环

这是我们做的一个质量故障处理和质量评估的闭环。首先是质量数据的采集,上报存储,然后由监控系统来监控,通过秒级监控,自动报障到运维和CDN厂商,由厂商人员分析定位后反馈,可以减少运维的人工参与。

从这个监控全平台的卡顿数据,我们还可以再挖掘一些数据出来,比如每天生成一些卡顿日报,然后自动发到我们内部和厂商两边,厂商会自己来做一些回复、调查和总结,最后反馈回给我们,这样通过定期Review CDN的质量,进行定期总结和评估对比,我们再以此为根据,看看质量调整和效果的情况。

同时我们会有一些评估的手段,也是从这些数据里面把它挖掘出来的,用来推动处理CDN直播平台的发展和完善。

还有就是建立更开放的技术交流氛围,把质量数据反馈给各CDN,推动分析问题。以往每家厂商过来都要踩很多坑,现在我们对各家CDN、各条线路、各个地区和各个运营商的质量线路都进行了切量、调度、线路的调整,实现了大部分主播的监控覆盖。当然,在这里面我们还会有一些运维能力在整合,后面会再展开讲。

6、质量指标SLI/SLO效果

这是我们把这整个质量指标串起来之后实现的效果:

案例1:直播音视频质量

  • 第一,建立了全链路的监控系统,实现了秒级发现流级别的卡顿情况,也提升了监控的覆盖率,同时是自动化、实时性、可观测的。
  • 第二,通过建立质量模型,运用卡顿率和稳定性可以随时评估主播、平台、线路的质量,可以Review质量。
  • 第三,和CDN厂商一起持续发现和优化质量。
  • 第四,把能力推到一线值班。把能力推到一线值班 ,以前运维是没有音视频Oncall能力的,只有资深的音视频研发工程师可以处理问题,现在一线值班,我们业务运维可以当做二线,只处理一些更重要的问题。

案例2:点播成功率

我们在点播项目上也用了质量指标的方式去做,也实现不错的效果。目前我们可以实现评估供应商,仅保留好用的;推动播放器改进统计,优化自动上报;推动服务端研发加强故障统计,整个质量有了大幅度的提升。同时我们也可以把全平台评估业务质量,生成相关数据报告给老板去看,让他了解到项目目前的质量状况和质量变化情况。

7、虎牙实践:带宽调度

接下来介绍虎牙带宽调度的一个实践,会从调度的原因、方法和评估三方面进行介绍。

(1)为什么要调度

  • 质量挑战。质量是我们最在乎的事情,但每家CDN线路和我们都经常有故障等各类情况出现,这里就需要有一个调度的能力,当某条线路或者某些情况下出现质量问题了,可以及时把它调走。
  • 容量挑战。直播平台对带宽消耗是非常大的,可每家CDN可承受的带宽是有一定上限的,必须要做一定调度,特别是大型活动上,要防止某家CDN厂商全部挂掉或者局部挂掉的情况。

(2)调度方法

调度方法有这样主播调度、观众调度、静态调度、动态调度、无缝切换能力几种。

主播调度,就是把这个主播调度到某条线路上去,我们或某家CDN的都可以。主播的调度又分为单CDN内的智能调度和多CDN的智能调度两种,我们会做一些默认的配置,并提供动态的能力,实现无缝的切流。观众端也是做了静态和动态的调度策略,优先使用平台里它的质量会应该是更有所保障的。此外,我们也提供了动态调度系统,让它在观众进直播间时可以调到某一条线路上去。

在这整个过程中,我们运维人员都参与其中,提出我们的需求和目标。并跟研发一起,或者自己开发其中的某些环节,形成一个个工程工作,促使业务质量大幅提升,并且自己的能力落地到了工程系统中,实现了运维价值的输出。

SRE理念的一些实践

除了上述的,我们还有一些其它比较接近SRE理念的实践,这里先和大家简单分享一下。

1、以SRE的姿势接维

如何接维一个新的业务,或是从其他人手里接维老项目;接维后如何处理和其他团队的关系,如何持续改进业务质量,这部分可以说是DevOps的实践,也是有我整理出来的一些实践,具体来说:

(1)了解产品,作为用户区了解,以开发者视角去了解产品,了解网站结构,以及背后的技术原理和流程;

(2)阅读文档,获取开发文档、运维文档、设计文档、阅读wiki等;

(3)掌握资源:作为内部人员去了解部署和服务器等资源、CMDB ,了解监控 、管理后台;

(4)熟悉架构:请研发leader 整体介绍产品、架构、部署;

(5)获取故障:请研发转发最近的bug邮件、故障邮件,了解最近的事故和事后总结;

(6)获取需求:最近重要的需求和开发计划;

(7)运研关系:参与研发周会,积极合作 ,改变运维与研发的关系;

(8)了解期望:和产品研发Leader沟通,了解核心问题和对运维的期望;

(9)梳理指标:核心业务指标,加上监控;

(10)输出进展:举行运维研发周会,请研发上级领导参加,了解最近接维后的故障;

(11)推进改进:大家都重视后,提出改进需求和工程计划;

(12)输出价值:把核心指标提升为公司级关注的指标。

2、运维研发会议

运维研发会议,我们试过了,效果很不错。时间上来说是每周一次,有两级的领导在场,包括研发团队的同学、具体业务研发的领导和上一级业务领导在场。内容有这么几点:

(1)定期Review性能指标、容量数据、可用性情况等质量、趋势;

(2)紧急的告警 、非紧急的告警;

(3)即将进行的生产环境变化;

(4)要进行技术决策的事宜;

(5)运维希望研发推进的事情、研发希望运维推进的事情;

(6)技术工作的同步;

(7)接下来待办事项及计划。

3、事后报告

事后总结和改进是SRE切入深入业务的最直接的方式。这是我们的模板:

SRE

其中,改进措施里面必须是要长效的措施,而不能是头痛医头,脚痛医脚这种方式。

转型SRE

1、SRE与运维的关系

那么,传统运维究竟如何转型SRE呢?正如我第一部分讲到的,在谷歌内部,是直接招聘软件工程师专职做SRE,跟传统的业务公司不一样,它是有第三种岗位的。但我个人的理解是,SRE是一个工程性的活动,不一定是一个岗位,研发可以转过来,运维也可以转过来,所以运维人员要有认识,这是可以互相抢饭碗的。

2、如何转型SRE

从SRE的工程性活动这个层面出发,在研发层面,运维做SRE,不一定是完全自己做,我们可以提出需求、设想和计划,然后找开发的资源实现,这是工程师的维度。此外,在反向工程的层面上,深入理解技术架构,学会站在更高视角去完善自己的架构思维和质量思维,留出时间来用于工程相关的思考与学习。要明确运维的本质:就是人与机器一起完成的工程性的工作!

作者:

张观石,虎牙直播业务运维负责人,《运维前线》联合作者。拥有多年互联网业务运维经验。

原文来自微信公众号:DBAplus社群

从苦逼到牛逼,详解Linux运维工程师的打怪升级之路

作者介绍

陈浩,北信源研发工程师,五年Linux运维工作经验,热衷运维技术研究、实践和团队分享。

运维工程师是从一个呆逼进化为苦逼再成长为牛逼的过程,前提在于你要能忍能干能拼,还要具有敏锐的嗅觉感知前方潮流变化。如:今年大数据,人工智能比较火……(相对表示就是 Python 比较火)

之前写过运维基础篇,发现对很多人收益挺大,接下来也写下关于这4年多的运维实践经验,从事了2年多游戏运维,1年多安全运维,1年大数据运维,相关行业信息不能算非常精通,但是熟悉和熟练还是相对可以的。

初级篇

linux运维人员常用工具拓扑详见:

linux

1.rsync工具

很多地方经常会用到rsync工具,实施几台服务器的同步效果。我们公司就是使用这个工具完成服务器的游戏的服务端和客户端同步,有几个文章例子:

  • rsync 强化技术(手动修改端口开启防火墙的情况下)并且通过脚本只同步需要的服务器http://chenhao6.blog.51cto.com/6228054/1322579
  • inotify+rsync+mutt+msmtp 实现linux文件或者目录自动更新并且实现发邮件给管理员http://chenhao6.blog.51cto.com/6228054/1298375

2.网络服务

服务有很多种,每间公司都会用到不同的,但基础的服务肯定要掌握,如FTP、DNS、SAMBA、邮件, 这几个大概学一下就行,LAMP和LNMP是必须要熟练,我所指的不是光会搭建,而是要很熟悉里面的相当配置才行,因为公司最关键的绝对是Web服务器,所以Nginx和Apache要熟悉,特别是Nginx一定要很熟悉才行,至少有些公司还会用Tomcat,这个也最好学一下。

其实网络服务方面不用太担心,一般公司的环境都已经搭建好,就算有新服务器或让你整改,公司会有相应的文档让你参照来弄,不会让你乱来的,但至少相关的配置一定要学熟,而且肯定是编译安装多,那些模块要熟悉一下他的作用,特别是PHP那些模块。

这面2点只是基础,也是必要条件,不能说是工具,以下才是真正的要掌握的工具。

  • Samba文件共享服务(共享脚本 让你工作更轻松)http://chenhao6.blog.51cto.com/6228054/1218028
  • Linux web服务安装apache 思路 (源码编译,自己定义服务)http://chenhao6.blog.51cto.com/6228054/1223484
  • FTP(持虚拟用户,并且每个虚拟用户可以具有独立的属性配置)http://chenhao6.blog.51cto.com/6228054/1219713
  • linux 下构建DHCP服务器http://chenhao6.blog.51cto.com/6228054/1217232

3.脚本语言

Shell脚本和另一个脚本语言,Shell是运维人员必须具备的,不懂这个连入职都不行,至少也要写出一些系统管理脚本,最简单也得写个监控CPU,内存比率的脚本吧,这是最最最基本了,别以为会写那些猜数字和计算什么数的,这些没什么作用,只作学习意义,写系统脚本才是最有意义,而另一个脚本语言是可选的,一般是3P,即Python、Perl和PHP,PHP就不需要考虑了,除非你要做开发,我个人建议学Python会比较好,难实现自动化运维,Perl是文本处理很强大,反正这两个学一个就行了。

  • Shell(一) 入门到复杂 自己做的各种脚本实例与解释http://chenhao6.blog.51cto.com/6228054/1230337
  • Shell(二)入门到复杂 脚本实例(计算器)http://chenhao6.blog.51cto.com/6228054/1232070

4.sed和awk工具

这两个工具必须要掌握,同时还要掌握正则表达式,这个就痛苦了,正则是最难学的表达式,但结合到sed和awk中会很强大,在处理文本内容和过滤Web内容时十分有用,不过在学Shell的同时一般会经常结合用到的,所以学第3点就会顺便学第4点。

  • sed 简明教程https://coolshell.cn/articles/9104.html

5.文本处理命令

sort 、tr、cut、paste、uniq、tee等必学,也是结合第3点脚本语言时一并学习的。

6.数据库

首选MySQL,别问我为什么不学SQL Server和Oracle,因为Linux用得最多绝对是MySQL,增删改查必学,特别要学熟查,其它方面可能不太需要,因为运维人员使用最多还是查,哪些优化和开发语句不会让你弄的。

  • MySQL(手动编译详细思路,以及增删改查、授权、备份还原)http://chenhao6.blog.51cto.com/6228054/1225129

7.防火墙

防火墙也算是个难点,说难不难,说易不易,最重要弄懂规则,如果学过CCNA的朋友可能会比较好学,因为iptables也有NAT表,原理是一样的,而FILTER表用得最多,反正不学就肯定不合格。

  • 防火墙(一)主机型防火墙http://chenhao6.blog.51cto.com/6228054/1239306
  • 防火墙(二)SNAT和DNAThttp://chenhao6.blog.51cto.com/6228054/1240714

8.监控工具

我个人建议,最好学这3个:Cacti,Nagios,Zabbix,企业用得最多应该是Nagios和 Zabbix,反正都学吧,但Nagios会有点难,因为会涉及到用脚本写自动监控,那个地方很难。

  • CentOS 6.2+Nginx+Nagios,手机短信和QQ邮箱提醒http://chenhao6.blog.51cto.com/6228054/1323192
  • 服务器集中检测Cactihttp://chenhao6.blog.51cto.com/6228054/1249302

9.集群和热备

这个很重要,肯定要懂的,但到了公司就不会让你去弄,因为新手基本不让你碰,集群工具有很多,最好学是LVS,这是必学,最好也学Nginx集群、反向代理,还有热备,这个就更多工具能实现了,像我公司是自己开发热备工具的。MySQL热备也要学,就是主从复制,这个要学懂整个流程一点也不容易,只照着做根本没意思。

  • MySQL主从同步,双主同步,如果服务器意外挂机,不同步怎么办http://chenhao6.blog.51cto.com/6228054/1325247
  • MySQL高性能压力测试(总结了好久)http://chenhao6.blog.51cto.com/6228054/1314418
  • Nginx 缓存配置及报错解决http://chenhao6.blog.51cto.com/6228054/1329106

10.数据备份

工具有很多,但至少要把RAID的原理弄懂,特别是企业最常用的1+0或0+1,自己做实验也要弄出来,备份工具有很多,如tar、dump,最好多了解一下。

学会以上10点,应该可以入门了,有些技术会比较难学,例如Apache和Nginx中还有些很重要的技术,如系统调优、服务优化、程序优化,这些在没接触工作前很难学习到的,所以先把这10点学了吧,估计要学熟至少3个月不止,脚本部分会交很吃力了,我建议是先学熟shell,等工作后再学另一门脚本语言,这样会比较好。

以上就是踏入linux运维工程师需要掌握的工具,还有很多工具要掌握的,但在学习环境中是很难学到,最后我再提醒一下,这里所指的工具相当于技能,而不是像Windows或Ubuntu的图形化工具,还有学linux就别装图形界面,这样虚拟机就不用吃太多内存,而且绝对不建议在真机上装Linux,根本达不到学习效果。

中级篇

这部分来自我自己的面试经历和面试别人的经历总结。先附上运维思路拓扑图:

有些人认为,其实运维就是部署某个软件,设置些基础功能,就算会运维了。

举个例子:安装LAMP,LNMP,就感觉部署方法我都掌握了。其实网上大多数都有一键安装脚本啥的根本没有啥技术含量,在面试官眼里,这些都不是你的亮点。基本到了公司一般环境架构都是部署好的,很少需要你去变动环境架构。就算你安装好 LNMP 架构你熟悉里面的原理吗?熟悉 Nginx 优化吗?熟悉 MySQL 优化吗?

再举个例子:我面试遇到的问题,面试官问你既然熟悉 LNMP 架构,那么 Nginx 反向代理的作用。

你应该不是说出懂这个软件和配置,你尽可能的说怎么优化,怎么深入提高网站性能。

  • 使用反向代理可以理解为7层应用层的负载均衡,使用负载均衡之后可以非常便捷的横向扩展服务器集群,实现集群整体并发能力、抗压能力的提高。
  • 通常反向代理服务器会带有本地 Cache 功能,通过静态资源的 Cache,有效的减少后端服务器所承载的压力,从而提高性能。

下面说说运维在工作中需要掌握的核心技术。注意,这是在工作中掌握的,在学习中很难掌握。

1.第一条最主要的排错

  • 分析部分程序不能运行或没有按预想结果运行的原因,对程序运行跟踪,查看系统调用的过程。
  • 较深入的系统瓶颈点分析。

查看剩余内存:

ree -m

#-/+ buffers/cache:       6458       1649

#6458M为真实使用内存  1649M为真实剩余内存(剩余内存+缓存+缓冲器)

#linux会利用所有的剩余内存作为缓存,所以要保证linux运行速度,就需要保证内存的缓存大小

系统信息:

系统信息

硬件信息:

硬件信息

  • 使用分析系统分析web日志(如逆火软件)
  • 分析系统性能瓶颈点(IO/Memory/CPU,常用工具,top命令中shift组合键的特殊用Sar/vmstat/iostat/ipcs)

日志管理常用命令:

日志

2.优化

优化可以说是运维最吃香的技能,基本会优化的运维普遍工资很高,而且优化是要承担风险的,并不是网上搜个文章改一下配置文件或者参数就叫优化了,这样很容易造成宕机。

优化是根据实际的现场环境硬件各个参数进行部分优化,提高软件性能和网站性能。这个我只能讲半知半解,当时优化MySQL和Tomcat参数也是根据网上文章和官网文档查找参数在虚拟机上测试然后查看性能。

成本优化,性能优化。这里我给出 Tomcat 优化JVM参数(做过相应测试才放到现场环境的):(记住无监控不调优)

-标准参数,所有jvm都应该支持

-X 非标,每个jvm实现都不同

-XX 不稳定参数,下一版本可能会取消

serial collector 单线程 序列化

parallel collector 多线程

启动 jvisualvm.exe 监控 dump 内存溢出

-Xms:初始堆大小

-Xmx:最大堆大小

-Xss:线程栈大小

-XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代和年老代的比值,如3, 标示年轻代:年老代比值1:3,年轻代占整个年轻代年老代和的1/4

-XX:SurvivorRatio=n:年轻代中的eden区与2个Survivor区的比值。

-XX:MaxPermSize=n:设置持久代大小

收集器设置

-XX:+UseSerialGC:设置串行收集器

-XX:+UseParallelGC:设置并行收集器

-XX:+UseConcMarkSweepGC:设置并发收集器

回收统计信息

-XX:+PrintGC

-XX:+PrintGCDetails

-Xloggc:filename

Tocmat 优化,确认有几个JVM虚拟机

set JAVA_OPTS=

-Xms4g

-Xmx4g

-Xss512k

-XX:+AggressiveOpts 进攻型的优化选项,所有优化项都加上

-XX:+UseBiasedLocking 优化锁,基本都要选上,偏执锁

-XX:permSize=64m 原始区大小,最大300m 类多就设置大一点

-XX:MaxPermSize=300m

-XX:+DisableExplicitGC //System.gc() 不显示调用gc

-XX:+UseConcMarkSweepGC 使用cms缩短相应时间,并发收集,低停顿

-XX:+UseParNewGC   并行收集新生代的垃圾

-XX:+CMSParallelRemarkEnabled 在使用UseParNewGC的情况下,尽量减少mark的时间

-XX:+UseCMSCompactAtFullCollection 使用并发收集器时,开启对年老代的压缩,使碎片减少

-XX:LargePageSizelnBytes=128m 内存分页大小对性能的提升

-XX:+UseFastAccessorMethods get/set方法转成本地代码

-Djava awt headless=true  修复linux下tomcat处理图标时可能产生的bug

内存调优:

内存调优

Tomcat 前任何参数没参加大概每秒605,调优后大概每秒435,接近3倍的结果。

Tomcat

3.开发技能

优选 Shell 和 Python,现在 Shell 无法满足你的需求或者效率很低,那么选择自动化 Python 是最好的选择。现在普遍招聘需求要求,会写 Shell 或者 Python、Perl 脚本,个人选择还是选 Python。

Python 这门语言上手比较快,容易理解。在服务器管理工具上非常丰富,配置管理(Saltstack) 批量执行(Fabric、Saltstack) 监控(Zenoss、Nagios 插件) 虚拟化管理( python-libvirt) 进程管理 (Supervisor) 云计算(OpenStack) …… 还有大部分系统 C 库都有 Python 绑定。

对于流程确定的事情,最终一定是纳入系统管理的体系,写成程序,成为系统的一部分。而不是无法复用游离与整体的各种脚本。

随着云计算时代的来临,中小型公司不需要运维了,大型公司没有工程开发能力的运维,是没有竞争力的。

最重要的学好 Python 可以涨工资,可以涨工资,可以涨工资。(重要的事情说三遍~)目前本人也是在学 Python,正在把以前 Shell 脚本的实例转换成 Python 脚本。

推荐《Python笔记:Python实例手册》

下载链接:http://down.51cto.com/data/2329173

意识篇

1.安全意识

运维人员的权限很大,所以一定要保证帐号/私钥的安全。

  • 最好使用加密工具存储。比如Truecrypt、lpassword。
  • 基于本地存储。切勿用网盘,也不建议用lastpass等
  • SSH私钥添加密码

2.磨刀意识

关于任何操作配置,最好先搞明白操作或配置的原理,然后再去操作。应一句话叫做“磨刀不误砍柴功”,而且对于类似的操作可以举一反三。

3.计划意识

复杂的变更操作比如多台主机以及牵涉到san存储,最好先作 操作计划,写计划文档,详细致每条命令,然后请高手帮忙审核。 这样能最大程度使整个操作过程安全。如果是重要的客户业务系统,操作最好有回退方案,而一旦变更失败,客户可以在短时间内将业务回退。

4.记录分享意识

遇到自己认为较特殊的案例时,记得要写案例过程及分析的文档。也方便自己以后翻看,或者和其他兄弟分享,作知识的传播以便于大家以后都能少走弯路。

5.监控意识

运维来说,监控是非常重要的,监控是发现系统各种异常的眼睛,所以运维应该和监控紧密配合。

6.业务意识

尽量了解维护的各主机上业务类型,以及各主机业务之间的关联性。因为任何维护工作都是为主机能提供业务服务的,当某业务中断,能最快的知道与此业务相关的主机群,从而缩小故障排查范围,最快定位故障。并不是你技术很牛,学的技术很多很熟,就不代表你不需要运维意识,其实领导很看重运维意识的,例如有没有做好备份、权限分配问题、平台测试情况、故障响应时间等,这些都是意识,而不是你学了很多技术自认大牛了,平台发现故障你又没什么大不子,以为很简单的问题喜欢处理就处理,不需要向其它部门反馈等,领导不是看你的技术如何,而是看你的运维意识如何,你没运维意识,技术再牛也没用,只会让其它部门的人跟你不协调。要知道做IT这行是苦逼的,需要无尽的学习,不学习只会被淘汰,不想被年轻的淘汰,就只能不断增值自己,不然不是你工资无法提升,而是你无法再从事这行。这个世界,在悄悄惩罚不改变的人……原文来自微信公众号:DBAplus社群

MySQL大表优化方案

当MySQL单表记录数过大时,增删改查性能都会急剧下降,可以参考以下步骤来优化:

单表优化

除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑、部署、运维的各种复杂度,一般以整型值为主的表在千万级以下,字符串为主的表在五百万以下是没有太大问题的。而事实上很多时候MySQL单表的性能依然有不少优化空间,甚至能正常支撑千万级以上的数据量:

字段

  • 尽量使用TINYINTSMALLINTMEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED
  • VARCHAR的长度只分配真正需要的空间
  • 使用枚举或整数代替字符串类型
  • 尽量使用TIMESTAMP而非DATETIME
  • 单表不要有太多字段,建议在20以内
  • 避免使用NULL字段,很难查询优化且占用额外索引空间
  • 用整型来存IP

索引

  • 索引并不是越多越好,要根据查询有针对性的创建,考虑在WHEREORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描
  • 应尽量避免在WHERE子句中对字段进行NULL值判断,否则将导致引擎放弃使用索引而进行全表扫描
  • 值分布很稀少的字段不适合建索引,例如”性别”这种只有两三个值的字段
  • 字符字段只建前缀索引
  • 字符字段最好不要做主键
  • 不用外键,由程序保证约束
  • 尽量不用UNIQUE,由程序保证约束
  • 使用多列索引时主意顺序和查询条件保持一致,同时删除不必要的单列索引

查询SQL

  • 可通过开启慢查询日志来找出较慢的SQL
  • 不做列运算:SELECT id WHERE age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边
  • sql语句尽可能简单:一条sql只能在一个cpu运算;大语句拆小语句,减少锁时间;一条大sql可以堵死整个库
  • 不用SELECT *
  • OR改写成INOR的效率是n级别,IN的效率是log(n)级别,in的个数建议控制在200以内
  • 不用函数和触发器,在应用程序实现
  • 避免%xxx式查询
  • 少用JOIN
  • 使用同类型进行比较,比如用'123''123'比,123123
  • 尽量避免在WHERE子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
  • 对于连续数值,使用BETWEEN不用INSELECT id FROM t WHERE num BETWEEN 1 AND 5
  • 列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大

引擎

目前广泛使用的是MyISAM和InnoDB两种引擎:

MyISAM

MyISAM引擎是MySQL 5.1及之前版本的默认引擎,它的特点是:

  • 不支持行锁,读取时对需要读到的所有表加锁,写入时则对表加排它锁
  • 不支持事务
  • 不支持外键
  • 不支持崩溃后的安全恢复
  • 在表有读取查询的同时,支持往表中插入新纪录
  • 支持BLOBTEXT的前500个字符索引,支持全文索引
  • 支持延迟更新索引,极大提升写入性能
  • 对于不会进行修改的表,支持压缩表,极大减少磁盘空间占用
InnoDB

InnoDB在MySQL 5.5后成为默认索引,它的特点是:

  • 支持行锁,采用MVCC来支持高并发
  • 支持事务
  • 支持外键
  • 支持崩溃后的安全恢复
  • 不支持全文索引

总体来讲,MyISAM适合SELECT密集型的表,而InnoDB适合INSERTUPDATE密集型的表

系统调优参数

可以使用下面几个工具来做基准测试:

  • sysbench:一个模块化,跨平台以及多线程的性能测试工具
  • iibench-mysql:基于 Java 的 MySQL/Percona/MariaDB 索引进行插入性能测试工具
  • tpcc-mysql:Percona开发的TPC-C测试工具

具体的调优参数内容较多,具体可参考官方文档,这里介绍一些比较重要的参数:

  • back_log:back_log值指出在MySQL暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中。也就是说,如果MySql的连接数据达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源。可以从默认的50升至500
  • wait_timeout:数据库连接闲置时间,闲置连接会占用内存资源。可以从默认的8小时减到半小时
  • max_user_connection: 最大连接数,默认为0无上限,最好设一个合理上限
  • thread_concurrency:并发线程数,设为CPU核数的两倍
  • skip_name_resolve:禁止对外部连接进行DNS解析,消除DNS解析时间,但需要所有远程主机用IP访问
  • key_buffer_size:索引块的缓存大小,增加会提升索引处理速度,对MyISAM表性能影响最大。对于内存4G左右,可设为256M或384M,通过查询show status like 'key_read%',保证key_reads / key_read_requests在0.1%以下最好
  • innodb_buffer_pool_size:缓存数据块和索引块,对InnoDB表性能影响最大。通过查询show status like 'Innodb_buffer_pool_read%',保证 (Innodb_buffer_pool_read_requests – Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests越高越好
  • innodb_additional_mem_pool_size:InnoDB存储引擎用来存放数据字典信息以及一些内部数据结构的内存空间大小,当数据库对象非常多的时候,适当调整该参数的大小以确保所有数据都能存放在内存中提高访问效率,当过小的时候,MySQL会记录Warning信息到数据库的错误日志中,这时就需要该调整这个参数大小
  • innodb_log_buffer_size:InnoDB存储引擎的事务日志所使用的缓冲区,一般来说不建议超过32MB
  • query_cache_size:缓存MySQL中的ResultSet,也就是一条SQL语句执行的结果集,所以仅仅只能针对select语句。当某个表的数据有任何任何变化,都会导致所有引用了该表的select语句在Query Cache中的缓存数据失效。所以,当我们的数据变化非常频繁的情况下,使用Query Cache可能会得不偿失。根据命中率(Qcache_hits/(Qcache_hits+Qcache_inserts)*100))进行调整,一般不建议太大,256MB可能已经差不多了,大型的配置型静态数据可适当调大.
    可以通过命令show status like 'Qcache_%'查看目前系统Query catch使用大小
  • read_buffer_size:MySql读入缓冲区大小。对表进行顺序扫描的请求将分配一个读入缓冲区,MySql会为它分配一段内存缓冲区。如果对表的顺序扫描请求非常频繁,可以通过增加该变量值以及内存缓冲区大小提高其性能
  • sort_buffer_size:MySql执行排序使用的缓冲大小。如果想要增加ORDER BY的速度,首先看是否可以让MySQL使用索引而不是额外的排序阶段。如果不能,可以尝试增加sort_buffer_size变量的大小
  • read_rnd_buffer_size:MySql的随机读缓冲区大小。当按任意顺序读取行时(例如,按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySql会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。但MySql会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。
  • record_buffer:每个进行一个顺序扫描的线程为其扫描的每张表分配这个大小的一个缓冲区。如果你做很多顺序扫描,可能想要增加该值
  • thread_cache_size:保存当前没有与连接关联但是准备为后面新的连接服务的线程,可以快速响应连接的线程请求而无需创建新的
  • table_cache:类似于thread_cache_size,但用来缓存表文件,对InnoDB效果不大,主要用于MyISAM

升级硬件

Scale up,这个不多说了,根据MySQL是CPU密集型还是I/O密集型,通过提升CPU和内存、使用SSD,都能显著提升MySQL性能

读写分离

也是目前常用的优化,从库读主库写,一般不要采用双主或多主引入很多复杂性,尽量采用文中的其他方案来提高性能。同时目前很多拆分的解决方案同时也兼顾考虑了读写分离

缓存

缓存可以发生在这些层次:

  • MySQL内部:在系统调优参数介绍了相关设置
  • 数据访问层:比如MyBatis针对SQL语句做缓存,而Hibernate可以精确到单个记录,这里缓存的对象主要是持久化对象Persistence Object
  • 应用服务层:这里可以通过编程手段对缓存做到更精准的控制和更多的实现策略,这里缓存的对象是数据传输对象Data Transfer Object
  • Web层:针对web页面做缓存
  • 浏览器客户端:用户端的缓存

可以根据实际情况在一个层次或多个层次结合加入缓存。这里重点介绍下服务层的缓存实现,目前主要有两种方式:

  • 直写式(Write Through):在数据写入数据库后,同时更新缓存,维持数据库与缓存的一致性。这也是当前大多数应用缓存框架如Spring Cache的工作方式。这种实现非常简单,同步好,但效率一般。
  • 回写式(Write Back):当有数据要写入数据库时,只会更新缓存,然后异步批量的将缓存数据同步到数据库上。这种实现比较复杂,需要较多的应用逻辑,同时可能会产生数据库与缓存的不同步,但效率非常高。

表分区

MySQL在5.1版引入的分区是一种简单的水平拆分,用户需要在建表的时候加上分区参数,对应用是透明的无需修改代码

对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成,实现分区的代码实际上是通过对一组底层表的对象封装,但对SQL层来说是一个完全封装底层的黑盒子。MySQL实现分区的方式也意味着索引也是按照分区的子表定义,没有全局索引

用户的SQL语句是需要针对分区表做优化,SQL条件中要带上分区条件的列,从而使查询定位到少量的分区上,否则就会扫描全部分区,可以通过EXPLAIN PARTITIONS来查看某条SQL语句会落在那些分区上,从而进行SQL优化,如下图5条记录落在两个分区上:

mysql> explain partitions select count(1) from user_partition where id in (1,2,3,4,5);
+----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table          | partitions | type  | possible_keys | key     | key_len | ref  | rows | Extra                    |
+----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
|  1 | SIMPLE      | user_partition | p1,p4      | range | PRIMARY       | PRIMARY | 8       | NULL |    5 | Using where; Using index |
+----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
1 row in set (0.00 sec)

分区的好处是:

  • 可以让单表存储更多的数据
  • 分区表的数据更容易维护,可以通过清楚整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作
  • 部分查询能够从查询条件确定只落在少数分区上,速度会很快
  • 分区表的数据还可以分布在不同的物理设备上,从而搞笑利用多个硬件设备
  • 可以使用分区表赖避免某些特殊瓶颈,例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争
  • 可以备份和恢复单个分区

分区的限制和缺点:

  • 一个表最多只能有1024个分区
  • 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来
  • 分区表无法使用外键约束
  • NULL值会使分区过滤无效
  • 所有分区必须使用相同的存储引擎

分区的类型:

  • RANGE分区:基于属于一个给定连续区间的列值,把多行分配给分区
  • LIST分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择
  • HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式
  • KEY分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值

分区适合的场景有:

  • 最适合的场景数据的时间序列性比较强,则可以按时间来分区,如下所示:
CREATE TABLE members (
    firstname VARCHAR(25) NOT NULL,
    lastname VARCHAR(25) NOT NULL,
    username VARCHAR(16) NOT NULL,
    email VARCHAR(35),
    joined DATE NOT NULL
)
PARTITION BY RANGE( YEAR(joined) ) (
    PARTITION p0 VALUES LESS THAN (1960),
    PARTITION p1 VALUES LESS THAN (1970),
    PARTITION p2 VALUES LESS THAN (1980),
    PARTITION p3 VALUES LESS THAN (1990),
    PARTITION p4 VALUES LESS THAN MAXVALUE
);

查询时加上时间范围条件效率会非常高,同时对于不需要的历史数据能很容的批量删除。

  • 如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,查询时只访问一个很小的分区表,能够有效使用索引和缓存

另外MySQL有一种早期的简单的分区实现 – 合并表(merge table),限制较多且缺乏优化,不建议使用,应该用新的分区机制来替代

垂直拆分

垂直分库是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放到用户库、把订单数据放到订单库。垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联

比如原始的用户表是:

垂直拆分后是:

垂直拆分的优点是:

  • 可以使得行数据变小,一个数据块(Block)就能存放更多的数据,在查询时就会减少I/O次数(每次查询时读取的Block 就少)
  • 可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起
  • 数据维护简单

缺点是:

  • 主键出现冗余,需要管理冗余列
  • 会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join来减少数据库压力
  • 依然存在单表数据量过大的问题(需要水平拆分)
  • 事务处理复杂

水平拆分

概述

水平拆分是通过某种策略将数据分片来存储,分库内分表和分库两部分,每片数据会分散到不同的MySQL表或库,达到分布式的效果,能够支持非常大的数据量。前面的表分区本质上也是一种特殊的库内分表

库内分表,仅仅是单纯的解决了单一表数据过大的问题,由于没有把表的数据分布到不同的机器上,因此对于减轻MySQL服务器的压力来说,并没有太大的作用,大家还是竞争同一个物理机上的IO、CPU、网络,这个就要通过分库来解决

前面垂直拆分的用户表如果进行水平拆分,结果是:

实际情况中往往会是垂直拆分和水平拆分的结合,即将Users_A_MUsers_N_Z再拆成UsersUserExtras,这样一共四张表

水平拆分的优点是:

  • 不存在单库大数据和高并发的性能瓶颈
  • 应用端改造较少
  • 提高了系统的稳定性和负载能力

缺点是:

  • 分片事务一致性难以解决
  • 跨节点Join性能差,逻辑复杂
  • 数据多次扩展难度跟维护量极大

分片原则

  • 能不分就不分,参考单表优化
  • 分片数量尽量少,分片尽量均匀分布在多个数据结点上,因为一个查询SQL跨分片越多,则总体性能越差,虽然要好于所有数据在一个分片的结果,只在必要的时候进行扩容,增加分片数量
  • 分片规则需要慎重选择做好提前规划,分片规则的选择,需要考虑数据的增长模式,数据的访问模式,分片关联性问题,以及分片扩容问题,最近的分片策略为范围分片,枚举分片,一致性Hash分片,这几种分片都有利于扩容
  • 尽量不要在一个事务中的SQL跨越多个分片,分布式事务一直是个不好处理的问题
  • 查询条件尽量优化,尽量避免Select * 的方式,大量数据结果集下,会消耗大量带宽和CPU资源,查询尽量避免返回大量结果集,并且尽量为频繁使用的查询语句建立索引。
  • 通过数据冗余和表分区赖降低跨库Join的可能

这里特别强调一下分片规则的选择问题,如果某个表的数据有明显的时间特征,比如订单、交易记录等,则他们通常比较合适用时间范围分片,因为具有时效性的数据,我们往往关注其近期的数据,查询条件中往往带有时间字段进行过滤,比较好的方案是,当前活跃的数据,采用跨度比较短的时间段进行分片,而历史性的数据,则采用比较长的跨度存储。

总体上来说,分片的选择是取决于最频繁的查询SQL的条件,因为不带任何Where语句的查询SQL,会遍历所有的分片,性能相对最差,因此这种SQL越多,对系统的影响越大,所以我们要尽量避免这种SQL的产生。

解决方案

由于水平拆分牵涉的逻辑比较复杂,当前也有了不少比较成熟的解决方案。这些方案分为两大类:客户端架构和代理架构。

客户端架构

通过修改数据访问层,如JDBC、Data Source、MyBatis,通过配置来管理多个数据源,直连数据库,并在模块内完成数据的分片整合,一般以Jar包的方式呈现

这是一个客户端架构的例子:

可以看到分片的实现是和应用服务器在一起的,通过修改Spring JDBC层来实现

客户端架构的优点是:

  • 应用直连数据库,降低外围系统依赖所带来的宕机风险
  • 集成成本低,无需额外运维的组件

缺点是:

  • 限于只能在数据库访问层上做文章,扩展性一般,对于比较复杂的系统可能会力不从心
  • 将分片逻辑的压力放在应用服务器上,造成额外风险
代理架构

通过独立的中间件来统一管理所有数据源和数据分片整合,后端数据库集群对前端应用程序透明,需要独立部署和运维代理组件

这是一个代理架构的例子:

代理组件为了分流和防止单点,一般以集群形式存在,同时可能需要Zookeeper之类的服务组件来管理

代理架构的优点是:

  • 能够处理非常复杂的需求,不受数据库访问层原来实现的限制,扩展性强
  • 对于应用服务器透明且没有增加任何额外负载

缺点是:

  • 需部署和运维独立的代理中间件,成本高
  • 应用需经过代理来连接数据库,网络上多了一跳,性能有损失且有额外风险
各方案比较
出品方 架构模型 支持数据库 分库 分表 读写分离 外部依赖 是否开源 实现语言 支持语言 最后更新 Github星数
MySQL Fabric MySQL官方 代理架构 MySQL python 无限制 4个月前 35
Cobar 阿里巴巴 代理架构 MySQL Java 无限制 两年前 1287
Cobar Client 阿里巴巴 客户端架构 MySQL Java Java 三年前 344
TDDL 淘宝 客户端架构 无限制 Diamond 只开源部分 Java Java 未知 519
Atlas 奇虎360 代理架构 MySQL C 无限制 10个月前 1941
Heisenberg 百度熊照 代理架构 MySQL Java 无限制 2个月前 197
TribeDB 个人 代理架构 MySQL NodeJS 无限制 3个月前 126
ShardingJDBC 当当 客户端架构 MySQL Java Java 当天 1144
Shark 个人 客户端架构 MySQL Java Java 两天前 84
KingShard 个人 代理架构 MySQL Golang 无限制 两天前 1836
OneProxy 平民软件 代理架构 MySQL 未知 无限制 未知 未知
MyCat 社区 代理架构 MySQL Java 无限制 两天前 1270
Vitess Youtube 代理架构 MySQL Golang 无限制 当天 3636
Mixer 个人 代理架构 MySQL Golang 无限制 9个月前 472
JetPants Tumblr 客户端架构 MySQL Ruby Ruby 10个月前 957
HibernateShard Hibernate 客户端架构 无限制 Java Java 4年前 57
MybatisShard MakerSoft 客户端架构 无限制 Java Java 11个月前 119
Gizzard Twitter 代理架构 无限制 Java 无限制 3年前 2087

如此多的方案,如何进行选择?可以按以下思路来考虑:

  1. 确定是使用代理架构还是客户端架构。中小型规模或是比较简单的场景倾向于选择客户端架构,复杂场景或大规模系统倾向选择代理架构
  2. 具体功能是否满足,比如需要跨节点ORDER BY,那么支持该功能的优先考虑
  3. 不考虑一年内没有更新的产品,说明开发停滞,甚至无人维护和技术支持
  4. 最好按大公司->社区->小公司->个人这样的出品方顺序来选择
  5. 选择口碑较好的,比如github星数、使用者数量质量和使用者反馈
  6. 开源的优先,往往项目有特殊需求可能需要改动源代码

按照上述思路,推荐以下选择:

  • 客户端架构:ShardingJDBC
  • 代理架构:MyCat或者Atlas

兼容MySQL且可水平扩展的数据库

目前也有一些开源数据库兼容MySQL协议,如:

但其工业品质和MySQL尚有差距,且需要较大的运维投入,如果想将原始的MySQL迁移到可水平扩展的新数据库中,可以考虑一些云数据库:

NoSQL

在MySQL上做Sharding是一种戴着镣铐的跳舞,事实上很多大表本身对MySQL这种RDBMS的需求并不大,并不要求ACID,可以考虑将这些表迁移到NoSQL,彻底解决水平扩展问题,例如:

  • 日志类、监控类、统计类数据
  • 非结构化或弱结构化数据
  • 对事务要求不强,且无太多关联操作的数据

iOS开发60分钟入门

本文面向已有其它语言(如Java,C,PHP,Javascript)编程经验的iOS开发初学者,初衷在于让我的同事一小时内了解如何开始开发iOS App,学习目标包括:

  • 能使用Xcode IDE、模拟器
  • 能修改、调试已有iOS App
  • 能在已有应用内创建新模块
  • 能创建新应用
  • 能发布应用到App Store

本文不包含任何高级的iOS开发知识,已学会iOS开发的同学不要看,看完这篇文章学会了的同学也不用再看了。

不仅是学习一门新语言

有过脚本开发经验的人(如Javascript,PHP,Shell)在刚开始学习iOS开发的时候,会觉得iOS开发的学习曲线比脚本语言要高,是的,这种感觉是对的。因为学iOS开发,不仅是学习一门新语言,它包括:

  • 一门语言:Objective-C
  • 一个框架:Cocoa Touch
  • 一个IDE:Xcode

初学脚本语言通常不会来绘制图形界面、与人交互,iOS如果不做图形界面,像脚本语言一样处理文本操作数据库,就没啥意思了。

所以,过去我写别的新手入门教程,通常都是写《XXX入门15分钟教程》,而iOS就要花数倍的时间来写了。

环境准备

做iOS开发一定要有苹果的软件环境:Mac OS操作系统、Objective-C编译器、设备模拟器等,开发工具倒不一定要用Xcode,只要是个源代码编辑工具就行(vim都行,只是没Xcode那么多功能)。

Mac OS

拥有Mac OS环境最简单的方法是找一台苹果电脑,包括iMac, MacBook Pro, MacBook Air, Mac Mini,但不包括苹果的移动设备(iPod Touch, iPhone, iPad, iPad Mini,它们运行的是iOS系统,不是Mac OS),苹果电脑在出厂的时候就会预装Mac OS,目前最新版本是Mac OS X 10.8,主流的版本还有Mac OS X 10.6、Max OS X 10.7。

如果囊中羞涩,可以借一台,或者上淘宝买个二手的。

黑苹果

提到iOS开发入门,似乎没办法不说黑苹果。所谓黑苹果,就是把Mac OS改造后安装在非苹果的硬件上,这是违反DMCA法案的,黑苹果的更多资料,可以在维基上找到

苹果电脑价格高,国内软件开发者生存压力大,所以黑苹果在国内也有一些真实的存在,国外当然也有啦。

黑苹果基本可以胜任iOS开发,但有一些问题:

  • 安装黑苹果是非法的
  • 个人行为苹果公司一般不会追究,但会遭同行的鄙视
  • 黑苹果超级难装,挑硬件。即使完全相同的型号,相同的批次,也有可能A机器装上了,B机器装不上
  • 黑苹果系统多少都存在一些使用上的问题,像驱动Bug啦、待机恢复蓝屏啦、上网浏览有问题啦
  • 黑苹果不能随意升级,可能升级一次safari就导致整个系统崩溃了

上面这些虽然不会直接影响Xcode写代码、模拟器测试,但写着写着想上网查个东西的时候,safari不能翻页,确实挺影响心情的。所以,钱包允许的前提下,还是搞个苹果电脑省心一些。

Xcode 和 模拟器

Xcode可以在苹果官网免费下载:Xcode下载地址

安装Xcode时会自动安装iOS SDK和模拟器。

这么强大的IDE居然是免费的,还是挺让人开心的。

从改一个现成的应用开始吧

学一门新软件开发技能,能够第一时间做出一个可运行的产品非常重要,有助于给自己正面激励,我上大学的时候,有很多次想学一门新语言,往往花了半个月,还沉浸在数据类型和语法字典里,连第一个Hello World都没做出来。

这一次,就让我们从改一个现成的应用开始吧。

下载

首先,我们从苹果开发者中心下载一个示例代码回来。我选了ToolBarSearch

在本文档的末尾,还有一些其它的网址可以下载开源iOS产品或者代码段,但我试了一下,还是Apple Sample Code最容易成功。

下载回来的zip文件最好保存在”下载”或者”文稿”目录里,因为在Mac OS 10.8以前,有些目录(例如/var/private/tmp)在Finder中是看不到的,要通过Finder的“前往 > 前往文件夹”功能才能进入。

打开

有三种方式可以打开一个iOS Project

双击project文件

打开Finder,进入刚刚下载解压的ToolBarSearch目录,找到ToolBarSearch.Xcodeproj文件,双击之,Xcode会自动启动,并打开这个项目

在Xcode里选择Project打开

  • 在Xcode没启动的情况下(如果Xcode已经启动了,就先按Command Q退出),启动Xcode,会弹出“Welcome to Xcode”的欢迎页,点击左下角的“Open Other”按钮,找到ToolBarSearch目录,双击ToolBarSearch目录,或者双击ToolBarSearch.Xcodeproj文件都可以
  • 如果Xcode处于打开状态,可以点击其菜单栏的File -> Open,或者File -> Open Recent,然后再选择要打开的项目

通过命令行打开

在Mac OS 10.8以前,有些目录(例如/var/private/tmp),在Finder和Xcode的File > Open对话框中,点击鼠标是找不到的,这时候就要通过命令行终端来打开了。

打开终端,执行:

cd /ToolBarSearch的父目录/ToolBarSearch
open -a Xcode

open -a是mac os的系统命令,除了iOS项目,别的项目也可以这样打开。

运行刚下载的应用

点击Xcode左上角的Run按钮(或者同时按下Comman和R键),Xcode会编译源码并在模拟器中运行这个应用。

编译成功会在屏幕上淡淡地显示“Build Succeeded”。反之,失败就显示“Build Failed”且不启动模拟器。

修改

在模拟器上看到“Performed search using…”了吧,下面我们改掉它。

  • 在Xcode左上角的Run按钮下方,有一排小按钮,从左到右第三个是一个放大镜图标,鼠标移上去会显示“Show the Search Navigator”,点一下它,打开搜索界面,在它下方出现的Find输入框中输入“performed”
  • 搜索结果只有一条:ToolbarSearchViewController.m,点文件名下方被高亮的“Performed”字串,右侧代码编辑区会自动打开这个文件,并滚动屏幕,使包含“Performed”的这一行出现在编辑区的中间。
  • 修改双引号里的字串,随便改成啥,然后按“Command S”保存。

当然,这些操作,你也可以在终端下通过grep和vim完成。

运行修改后的应用

按Command R运行,看看,是不是看到效果啦?

是的,修改一个应用就这么简单。

Objective-C

Objective-C是苹果应用软件(包括苹果电脑上的Mac OS App和移动设备上的iOS App)的开发语言。它是一种面向对象的编程语言。

苹果公司还提供了一个软件,叫Interface Builder,简称IB,用于可视化的界面制作,就像用Dreamweaver做网页,或者像Visual Basic做桌面软件一样。后来IB就整合进了Xcode,成了Xcode的一部分。这篇文档不讲IB,只讲Objective-C,因为:

  • 基本上,每一本讲iOS开发的书(纸质书、电子书),都有大量的截图一步一步教如何用IB开发iOS应用,而讲Objective-C开发应用的书却没有那么多。
  • IB可以用来直观方便地画界面、设置控件属性、建立代码与控件的联系,但后台的业务逻辑和数据处理仍然要靠Objective-C,可见,不管用不用IB,Objective-C都是绕不过去的。

C的超集

Objective-C扩展了ANSI C,是C的超集,也就是说:

  • 任何C源程序,不经修改,即可通过Objective-C编译器成功编译
  • Objective-C源程序中可以直接使用任何C语言代码

除了面向对象有语法是SmallTalk风格的(下面会讲到),其它非面向对象的语法、数据类型,与C完全相同,所以本文就不再赘述。 来看一个经典的Hello World示例吧:

#import <Foundation/Foundation.h>
int main(int argc, char *argv[]){
	@autoreleasepool{
		NSLog(@"Hello World!");
	}
	return 0;
}

是不是仿佛穿越回了大一学习C语言的时代,看起来和C几乎没有区别,是吧?是的,因为还没用到它的面向对象特性,哈哈!

SmallTalk的消息传递语法风格

Objective-C的面向对象语法源自SmallTalk,消息传递(Message Passing)风格。在源码风格方面,这是它与C Family语言(包括C/C++、Java、PHP)差别最大的地方。

在Java、C++世界,我们调用一个对象的某方法,在Objective-C里,这称作给类型发送一个消息,这可不仅仅是文字游戏,他们的技术细节也是不同的。

在Java里,对象和方法关系非常严格,一个方法必须属于一个类/对象,否则编译是要报错的。而在Objective-C里,类型和消息的关系比较松散,消息处理到运行时(runtime)才会动态决定,给类型发送一个它无法处理的消息,也只会抛出一个异常,而不会挂掉。

[obj undefinedMethod];

在代码里调用没定义的方法(这是Java世界的习惯说法啊,专业的叫法是,给obj对象传递它无法处理的消息),Xcode会警告,但编译能成功,运行的时候会出错。它会输出这样一个错误:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSObject undefinedMethod]: unrecognized selector sent to instance 0x8871710'

类似Java的OOP概念

Objective-C中一些面向对象的概念,也可以在Java中找到类似的实现(只能说是类似,不是完全相同),我的读者基本都是Java和PHP程序员,我会在下文中尽量用Java的概念来类比。

GoogleCode上有人整理了Java和Objective-C的概念、数据类型对应表,参见这里

字符串

Objective-C里有字符串是由双引号包裹,并在引号前加一个@符号,例如:

title = @"Hello";
if(title == @"hello") {}

PHP程序员要注意,在这里不能用单引号,即使只有一个字符也不能用。Objective-C与Java、C一样,双引号表示字符串。

函数调用

前文述及,不涉及面向对象时,它和C是完全一样的。以下是几个函数调用的示例:

不带参数

startedBlock();

带参数

NSLog(@"decrypted string: %@", str);
CGRectMake(0,0,0,0);

传递消息给类/实例方法

不带参数

[obj method];

对应的Java版本

obj.method();

带一个参数:

[counter increase:1];

对应的Java版本

counter.increase(1);

带多个参数

对C Family程序员来说,这是最难接受的,最反人类的:

- (void) setColorToRed: (float)red Green: (float)green Blue:(float)blue {...} //定义方法
[myObj setColorToRed: 1.0 Green: 0.8 Blue: 0.2]; //调用方法

对应的Java版

public void setColorToRedGreenBlue(float red, float green, float blue) {...}
myObj.setColorToRedGreenBlue(1.0, 0.8, 0.2);

消息嵌套

UINavigationBar *bar = [[[UINavigationBar alloc] init] autorelease];

对应的Java版

UINavigationBar bar = UINavigationBar.alloc().init().autorelease();//Java没有指针,所以星号去掉了

接口和实现

Objective-C的类分为接口定义和实现两个部分。接口定义(Interface)放在头文件中,文件扩展名是.h,实现(implementation)放在实现文件中,文件扩展名是.m(也有.mm的扩展名,表示Objective-C和C++混编的代码)。

接口定义也可以写在.m文件中,但最好不要这么干

需要注意的是,与Objective-C的interface概念最接近的是C和C++里的头文件,它与implementation是成双成对出现的,作用是声明类的成员变量和方法。它与Java的interface概念完全不同:

  • Objective-C里,interface有且只有一个实现,Java的interface可以有0-N个实现
  • Objective-C里,interface可以定义成员属性,Java里不可以

在Objective-C里,和Java的Interface概念相似的是Protocol,下文会讲到。

请看示例:

Interface

@interface MyClass {
	int memberVar1;
	id  memberVar2;
}

-(return_type) instance_method1; 
-(return_type) instance_method2: (int) p1;
-(return_type) instance_method3: (int) p1 andPar: (int) p2;
@end

Implementation

@implementation MyClass {
	int memberVar3;
}

-(return_type) instance_method1 {
	....
}
-(return_type) instance_method2: (int) p1 {
	....
}
-(return_type) instance_method3: (int) p1 andPar: (int) p2 {
	....
}
@end

接口和实现以@interface、@implementation开头,都以@end结束。“@”符号在Objective-C中是个很神奇的符号。

冒号也是方法名的一部分,method和method:是两个不同的方法名,不是overload,第二个带参数。

上述代码对应的Java版:

public class MyClass {
	protected int memberVar1;
	protected pointer memberVar2;
	private int memberVar3;
	
	public (return_type) instance_method1() {
		....
	}
	
	public (return_type) instance_method2(int p1) {
		....
	}
	
	public (return_type) instance_method3andPar(int p1, int p2) {
		....
	}
}

私有方法和公开方法

写在.h头文件里的方法都是公开的,Objective-C里没有私有方法的概念(没有你说个蛋啊,哈哈哈哈)。

官方并没有提到Objective-C怎么实现私有方法,我查阅了stackoverflow,统一的答案是,要实现私有方法的效果只能借助Category,不过,根据我的测试,即使采用了Category,也不能阻止外部的代码调用这个“私有方法”,只是Xcode不支持“私有方法”的自动完成,并会有警告提示,运行的时候,还是会成功的。各位看官知道有这么回事就可以了,这里不深讲。

变量和属性

类方法和实例方法

类方法

类方法就是Java、PHP里的Static Method,不用实例化就能调。类方法有一个加号前缀。 示例:

类定义

@interface MyClass
	+(void) sayHello;
@end

@implementation MyClass

+(void) sayHello {
	NSLog(@"Hello, World");
}
@end

使用

[MyClass sayHello];
实例方法

实例方法就是Java、PHP里的普通方法,必须实例化才能调。实例方法有一个减号前缀。 示例:

类定义

@interface MyClass : NSObject
-(void) sayHello;
@end

@implementation MyClass

-(void) sayHello {
	NSLog(@"Hello, World");
}
@end

使用

mycls = [MyClass new];
[mycls sayHello];

Selector

selector就是一个方法指针,类似PHP里的动态方法名:

<?php
class Hello {
	public function sayHello() {}
	
	public function test() {
		$fun_name = "sayHello";
		$this->$fun_name();
	}
}

在Objective-C里,selector主要用来做两类事情:

绑定控件触发的动作
@implementation DemoViewController
- (void)downButtonPressed:(id)sender {//响应“按钮被按下事件”的方法
	UIButton *button = (UIButton*)sender;
	[button setSelected:YES];
}

- (void)drawAnButton {
	UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; 
	btn.frame = _frame; 
	btn.tag = 1;
	btn.backgroundColor = [UIColor clearColor];
	[btn addTarget: self
		 action: @selector(downButtonPressed:)
		 forControlEvents: UIControlEventTouchUpInside];//当这个按钮被按下时,触发downButtonPressed:方法
}
@end
延时异步执行
@implementation ETHotDealViewController
- (void)viewDidLoad {
	
	//获取数据源
	HotDealDataSource *ds = [[HotDealDataSource alloc]init];
	[ds reload];
	_items = ds.items;
	
	[self performSelector: @selector(refreshTable)
		  withObject: self
		  afterDelay: 0.5];//延迟0.5秒调用refreshTable方法
}

-(void)refreshTable
{
	[self.tableView reloadData];
}
@end

这个例子中,获取数据源是通过ASIHTTP组件异步调用服务端HTTP接口,refreshTable要用到数据源返回回来的数据,如果不延迟0.5秒,就会立刻执行,执行的时候数据还在路上呢,页面就要变空白了。

继承

继承是写在Interface定义里面的。语法为:子类名在左,父类名在右,中间用冒号分隔。 示例:

@interface MyClass : NSObject
@end

对应的Java版本是:

public class MyClass extends NSObject {
}

协议(Protocol)

就是Java、PHP里的Interface。

协议的定义

协议的定义用@protocol关键字:

@protocol Printable
	-(void)print:(NSString)str;
@end

对应的Java版本是:

publilc interface Printable {
	public void print(String str);
}
协议的继承

协议本身也可以继承别的协议:

@protocol Printable <NSObject>
	-(void)print:(NSString)str;
@end

对应的Java版本:

public interface Printable extends NSObject {
	public void print (String str);
}
可选方法

协议可以包含可选方法,顾名思义,可选方法可以不被类实现:

@protocol Printable
@optional
	-(void)print:(NSString)str;
@end

加了@optional关键字,一个类在implements这个协议时,便可以不实现print:方法。

Java里没有类似的实现,除了Collection里会有一些方法带有optional的注释,但Collection是个特例。

协议的实现

一个类实现某些协议是写在Interface定义里面的。语法为:协议名用尖括号包裹,多个协议名用逗号隔开,协议写在父类的右边(如果没有父类就直接写在子类右边)。

示例:

@interface MyClass : NSObject <Printable, Drawable>
@end

Printable, Drawablw就是两个协议。

对应的Java版本是:

public class MyClass extends NSObject implements Printable, Drawable {
}

分类(Category)

分类可以给一个已经存在的类增加方法,而不用去改它的源码。Java和PHP中都没有类似的特性。

比如说,NSObject是一个Objective-C内置的系统类,我们想给它增加toJson方法,就像这样:

头文件:NSObject+Json.h

@interface NSObject (Json)
	-(NSString)toJson;
@end

实现文件:NSObject+Json.m

@implementation NSObject (Json)
	-(NSString)toJson {
		//...
	}
@end

使用的时候,只要包含NSObject+Json.h,实例化NSObject类,就可以使用toJson方法了:

import "NSObject+Json.h"
@implementation XYZController
	-(void)test {
		NSObject *obj = [[NSObject alloc]init];
		NSString *str = [obj toJson];
	}
@end

当然了,NSObject本来的那些方法依然还是可以用的,什么都没变,只是多了个toJson方法。看起来是不是和继承没太多差别呢(除了使用的时候实例化的是NSObject,而不是JsonObject)?再看一个继承实现不了的例子:

头文件:NSObject+Json+XML.h

@interface NSObject (Json)
	-(NSString)toJson;
@end

@interface NSObject (XML)
	-(NSString)toXML;
@end

实现文件:NSObject+Json+XML.m

@implementation NSObject (Json)
	-(NSString)toJson {
		//...
	}
@end

@implementation NSObject (XML)
	-(NSString)toXML {
		//...
	}
@end

使用:

import "NSObject+Json+XML.h"
@implementation XYZController
	-(void)test {
		NSObject *obj = [[NSObject alloc]init];
		NSString *json = [obj toJson];
		NSString *xml = [obj toXML];
	}
@end

Cocoa Touch

Cocoa是Mac OS App的开发框架,Cocoa Touch是iOS开发用的框架,Cocoa Touch和Cocoa大部分是一样的,只是Cocoa Touch多了一些移动设备特有的东西,如:触摸屏、加速度传感器、GPS定位。Cocoa中多任务、多窗口的特性,在Cocoa Touch中也是没有的(或者跟Cocoa不完全一样的)。

就像学了Java语言还要再学一些Spring、Hibernate、Struts(或者其它类似的Java类库)才能开始做J2EE应用一样,学过Objective-C语言之后,也要再学习Cocoa Touch框架才能顺利地开发iOS应用。

最常用设计模式之Delegate

Cocoa Touch大量使用Delegate(委派)设计模式。

常用控件:按钮、文本块、图片、输入框

TableView

WebView

导航条

Xcode

运行

快捷键:Comman R

搜索

搜索文本

搜索文件

新建文件/目录

推荐在Finder中新建好的再添加进来

断点

模拟器和真机测试

模拟器测试

在Xcode中打开你的项目,在Xcode顶部工具栏的Stop按钮(Run按钮右边那个黑色正方形按钮)右边,有个下拉菜单,显示着 “ToolBarSearch > iPhone 5.0 Simulator” (即 你的应用英文名 > 当前选中的调试 ),点击这个下拉菜单,选中iPhone 5.0 Simulator(这里的5.0是指iOS版本,不是iPhone5的意思,如果你的项目是iPad应用,请选iPad 5.0 Simulator),再按“Run”按钮,Xcode就会自动把当前正在编辑开发的应用编译并安装到模拟器上。

在模拟器上操作时,如果执行过程中遇到了你在Xcode里设置的断点,模拟器会暂停运行,并将当前活动窗口切换回Xcode,供你调试。

在Xcode里增加或者取消了断点,不需要重新编译和安装应用即可生效。

切换被模拟的设备

模拟器的“硬件”菜单,可以选择想要模拟什么设备,有iPad、iPhone可选。

  • Retina:表示视网膜屏,iPhone(Retina)代表iPhone4,iPhone4S
  • 4-Inch:表示4英寸的iPhone,iPhone(Retina 4-Inch)就是iPhone 5

切换模拟的iOS版本

在模拟器的“版本”菜单,可以选择要模拟什么版本的iOS。设备和版本是彼此独立的,iPhone 4S可以有5.0,5.1,6.1几种iOS版本,当然了,iPhone 5不可能有4.3的iOS版本。

触摸屏

用鼠标点击(不区分左右键)模拟器上的iPhone、iPad屏幕,就是在模拟用手指触摸iPhone,iPad的屏幕,可以实现一些触摸效果比如:

  • 鼠标单击 等于 手指轻触
  • 鼠标长按 等于 手指长按(例如你可以在模拟器上长按应用icon调出删除应用的确认框)
  • 鼠标按住拖动 等于 手指拖动
  • 双击和单击模拟器的Home键也等于双击和单击真机的Home键
多指手势

多指手势比较复杂,在白苹果笔记本上可以模拟简单的双指手势,白苹果的触控板天然支持多指触摸,但要定位到模拟器的区域再响应多指手势就需要借助一些额外的键啦:

  • 按住Option键,再用两个手指去操作触摸板,可模拟双指拖动、旋转
  • 按住Option+Shift,可模拟双指合拢

输入法和键盘

输入中文

手机上特有的输入法(比如九宫格输入法)不能模拟。模拟器默认的iOS软键盘只有英文输入,在测试应用的时候,我们要用到中文,有两个办法:

  • 使用剪贴板,在Mac OS里复制,再到模拟器运行的应用中的输入框上长按鼠标(模拟手指长按)3秒以上,等弹出“粘贴”的时候选择之,即可。
  • 在模拟器里,按Home键,找到Setting那个App icon(不是Mac OS顶部的模拟器菜单啊,那里没有Setting的),打开被模拟iOS设备的设置,依次点击”General – Keyboard – International Keyboards – Add New Keyboard…”,加个中文键盘,以后就可以使用被模拟iOS设备软件盘输入中文了,跟在iPhone/iPad真机上一样。

使用Mac电脑的键盘

如果要输入大量文本,使用模拟器里的软键盘效率太低,这时候可以使用物理键盘,方法是:在Mac OS顶部的模拟器菜单栏,点击”硬件”菜单,勾选下拉菜单中的“模拟硬件键盘”。以后再用模拟器运行iOS应用时,点击iOS应用中的输入框,软键盘就不弹出来了,可直接使用Mac电脑的物理键盘输入。

注意

  • 模拟器中的iOS接管了物理键盘输入,所以,调用的是模拟器iOS的输入法,不是你的Mac电脑的输入法。打个比方,你的Mac OS装的是搜狗五笔,模拟器中iOS加了个拼音输入法(Add New Keyboard),那么,在iOS应用中输入中文会调用拼音输入法。
  • 要切换模拟器中iOS的中英文输入法,也只能按iOS设备软键盘上的小地球图标,按Mac电脑上的Command+空格键是不行的。

地理位置

但Mac电脑没有定位用的硬件(GPS)和软件基础,因此模拟器不能自动获得当前的地理位置,不能用模拟器测试定位功能。(注意,虽然WiFi也可以独立定位——iPad WiFi版没有GPS也可以定位,但Mac电脑的WiFi不具备定位相关的软件)

要在模拟器里测试依赖地理位置的功能(如”我附近的xx”),可以手工指定一个经纬度给模拟器,方法:在Mac电脑顶部的模拟器菜单,点击”调试 – 位置 – 自定位置”,会弹出一个对话框,在弹出的框内填入经纬度即可。

如何获得经纬度? 上谷歌地图(ditu.google.cn),在地图上找到你想要的位置(比如你想知道杭州大厦的位置,就在通过搜索框找到杭州大厦),点击右键,选择“这儿是什么”,搜索框中就会出现这个位置的经纬度了,前面是纬度,后面是经度。咱们天朝的版图,都是北纬和东经。

摄像头

Mac电脑有摄像头,但Mac OS没有设计API给iOS模拟器调用,所以,不能用模拟器测试对焦闪光灯等功能。

要在模拟器上测试依赖照片的功能,可以在代码里做一个workaround,即当代码检测到摄像头不可用时,弹出一个照片选择器,让测试人员从相册里选择一幅照片,来进行后续的操作(如照片美化、人脸识别、条码扫描)。

真机测试

模拟器能验证你开发的iOS应用的大部分功能,但有些Mac设备上不具备的硬件,模拟器是不能模拟的。前文提到了一个绕过这些限制的办法,但获取当前位置、拍照、加速度感应这些是模拟不了的,一款应用发布给消费者之前,必须要在真实设备上验证过。

将未提交App Store审核通过的应用安装到iOS设备上测试,有三种办法:

  • 加入苹果的Developer Program,成为付费会员,有了这个付费会员资格,就可以直接在Xcode中点击”Run”将刚刚改过的代码编译打包安装到开发测试用的iOS设备上。在iOS真机上操作被测试的程序能激活Xcode中设置的断点。
  • 越狱iOS设备。将iPhone和iPad越狱后,可以通过SSH直接上传Xcode编译好的ipa包(一个iOS App本质上就是一个ipa包)。
  • 越狱的iOS设备,配合破解过的Xcode,甚至可以实现和付费开发者计划一样的功能:在Xcode上点击”Run”,就自动编译安装到iOS设备上去运行了
  • 企业部署方案。就像阿里巴巴的轩辕剑一样,用iPhone/iPad访问这个网址,点击里面的轩辕剑链接就可以安装轩辕剑这个应用了。

破解Xcode是违法行为(越狱是合法的),而且挑版本挑得厉害,不是所有Xcode版本都能破解,也不是所有Xcode的破解版都能和越狱的iOS配合好。越狱+SSH上传跟企业部署一样效率低(部署效率低,无法激活Xcode中的断点),只能用于QA验收,不适合开发自测。综上所述,最适合开发实时测试的就是第一个正规途径了。下面重点讲这个:

拥有一个开发者账号

苹果的Developer Program分为个人开发者和公司开发者,分别是每年99美元和每年299美元,分别可以注册100台和500台苹果测试设备。这个台数限制在一个付费年度内不会清空,比如说,2013年4月1日付费成功的,付费会员资格在2014年3月31日之前有效,这期间,注册一台就少一个名额,哪怕这个设备注册进来用了之后一分钟马上又删掉了,减少的这个名额也不会回来。

在交钱之前,最好问一下,周围的同事,有没有已经交了钱的。如果有,你只需要注册一个免费的Apple ID(就是你在App Store安装软件用的Apple ID),请他发个邀请邮件给你,把你的Apple ID加入他的团队就可以了,苹果会认为你们两个人是一个团队的,你们分别用自己的账号,共享100台设备的限额,这是合法的。

安装证书和私钥

证书

不想看下面各种点击各种页面跳转的直接用浏览器访问证书管理,你要登录你就用Apple ID登录(前提是交过钱,或者找交了钱的人把你加入团队了)。

不嫌烦,或者想知道下次没我这个文档的时候怎么进证书管理吗?按这个步骤操作:

页面上有一个“Your Certificate”区域,下方有个Download圆角按钮,这是你的个人证书,下载下来。再下面一行,有一句“If you do not have the WWDR intermediate certificate installed, click here to download now”,这个是苹果的公共证书,也下下来。

双击下载回来的证书,装证书时,会提示你输入密码,这是【钥匙串访问工具】在问你要你的Mac OS账号开机密码(相当于linux里面的sudo),不是Apple ID的密码,不要搞错了。

安装私钥

如果你是和其它同事公用的账号,让他给你一个私钥即可,就是一个扩展名为p12的文件,双击之,钥匙串访问会自动出来,需要你输入一个密码,这个密码问给你p12文件的人要,不是你的Mac OS系统开机密码,也不是你的Apple ID密码。

将设备注册到Provisioning Portal

  • 打开Xcode,从Xcode的Window菜单中找到Organizer,打开之(Shift Command 2)。
  • 把iOS设备连上电脑,Organizer会自动识别出你的设备,并显示在左侧边栏。
  • 在Organizer左侧边栏找到你的设备,右键,点击“Add Device to Provisioning Portal”,然后等Organizer提示你操作成功即可。(选中设备后,右边设备详情区域会显示一个按钮“Use for Development”,点它也可以)。

到iOS真机上运行测试版程序

回到Xcode主界面,在Stop按钮(Run按钮右边那个黑色正方形按钮)右边,有个下拉菜单,显示着 “ToolBarSearch > iPhone 5.0 Simulator” (即 你的应用英文名 > 当前选中的调试 ),点击这个下拉菜单,选中你的真机设备名,再按“Run”按钮,Xcode就会自动把当前正在编辑开发的应用编译并安装到真机上测试啦!

发布到App Store

打IPA包

IPA包本质上是一个ZIP压缩包,只不过它有着特殊的目录结构,扩展名是ipa,制作方法如下:

  • 在Xcode中Build项目,快捷键Command B
  • 在左侧项目导航器中,展开Products文件夹,找到你要打包的应用,你的应用名.app,右键,选择show in finder
  • 到Finder中Copy这个.app目录(选中,按Command C),复制到一个你新建的名为Payload(区分大小写)的文件夹中
  • 找到你的应用Logo,即一个512 * 512像素的PNG文件,copy到与Payload一起(与Payload并列,不要放进Payload了),并重命名为iTunesArtwork(区分大小写,没有扩展名)
  • 将Payload目录、ItunesArtwork文件打成一个zip包,并更改扩展名为ipa
  • 双击这个ipa文件,会用iTunes打开,如果打开成功,且在iTunes里有应用Logo显示,就成功了

批量自动打包

除App Store外,还有许多其它的iOS应用市场(如91助手,同步推等等),如果一个应用需要发布到很多个应用市场,且他们的代码略有不同(比如说,统计代码不同),按上述方法手工修改源码再打包,再还原,比较容易出错。好消息是,Xcode是有命令行的,我们可以写一个shell脚本,先用se自动修改源码,再调用Xcode的命令行来编译以得到your——app.app目录,最后调用zip、mv等命令把上一个章节讲的ipa打包动作自动执行。

阅读应用代码

从头新建一个应用:Hello World

其它

代码里的控件尺寸

iOS App里的控件尺寸和字体大小都是指Point,Retina设备(iPhone 4,4S,5;the new Pad)和非Retina设备(iPhone 3GS,iPad,iPad 2)的Point数是一样的,尽管iPhone 4的分辨率是3GS的2倍。比如说,10point在Retina设备里是20 pixel,在非Retina设备(iPhone 3G)上则是10 pixel。

项目成员间交流时,应使用Point,不要使用pixel。

SVN操作含有@符号的文件

iOS应用中经常出现xxxx@2x.png这样的文件名,它们是给retina设备用的高分辨率大图,用svn命令行操作它们的时候会被@符号干扰,解决方案是在svn命令末尾加上一个@符号,如:

svn del icon@2x.png@
svn info Default@2x.png@

如果一次移动了几十个png文件再svn commit的,可以用shell批处理:

svn status | awk '($1=="!"){print $2}' | grep -v @ | xargs svn del

上面这个命令是将文件名不包含@符号的,且已经不在硬盘上的文件从svn version controll中删掉

for file in `svn status | awk '($1=="!"){print $2}' `; do svn del $file"@"; done     

上面这个命令是将文件名包含@符号的,且已经不在硬盘上的文件从svn version controll中删掉

svn add同上, 如法炮制即可.

Xcode中的代码结构与操作系统上的文件系统并不一致

推荐在Finder里建好目录再到Xcode的Project Navigator中点击“Add Files to”添加到项目中

iPhone 5适配

iPhone 5与之前的iPhone不一样,采用了4寸Retina屏,所以它的Point数变成了320 * 568 points

开源代码

Objective-C教程

C#使用es

Elasticsearch简介

Elasticsearch (ES)是一个基于Apache Lucene(TM)的开源搜索引擎,无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。

但是,Lucene只是一个库。想要发挥其强大的作用,你需使用C#将其集成到你的应用中。Lucene非常复杂,你需要深入的了解检索相关知识来理解它是如何工作的。
Elasticsearch是使用Java编写并使用Lucene来建立索引并实现搜索功能,但是它的目的是通过简单连贯的RESTful API让全文搜索变得简单并隐藏Lucene的复杂性。
不过,Elasticsearch不仅仅是Lucene和全文搜索引擎,它还提供:

  • 分布式的实时文件存储,每个字段都被索引并可被搜索
  • 实时分析的分布式搜索引擎
  • 可以扩展到上百台服务器,处理PB级结构化或非结构化数据

而且,所有的这些功能被集成到一台服务器,你的应用可以通过简单的RESTful API、各种语言的客户端甚至命令行与之交互。上手Elasticsearch非常简单,它提供了许多合理的缺省值,并对初学者隐藏了复杂的搜索引擎理论。它开箱即用(安装即可使用),只需很少的学习既可在生产环境中使用。Elasticsearch在Apache 2 license下许可使用,可以免费下载、使用和修改。
随着知识的积累,你可以根据不同的问题领域定制Elasticsearch的高级特性,这一切都是可配置的,并且配置非常灵活。

以上内容来自 [百度百科]

关于ES详细概念见:http://88250.b3log.org/full-text-search-elasticsearch#b3_solo_h3_0

 

使用C#操作ES

NEST是一个高层的客户端,可以映射所有请求和响应对象,拥有一个强类型查询DSL(领域特定语言),并且可以使用.net的特性比如协变、Auto Mapping Of POCOs,NEST内部使用的依然是Elasticsearch.Net客户端。elasticsearch.net(NEST)客户端提供了强类型查询DSL,方便用户使用,源码下载

一、如何安装NEST

打开VS的工具菜单,通过NuGet包管理器控制台,输入以下命令安装NEST

Install-Package NEST

安装后引用了以下三个DLL

Elasticsearch.Net.dll(2.4.4)
Nest.dll(2.4.4)
Newtonsoft.Json.dll(9.0版本)

二、链接elasticsearch

你可以通过单个节点或者指定多个节点使用连接池链接到Elasticsearch集群,使用连接池要比单个节点链接到Elasticsearch更有优势,比如支持负载均衡、故障转移等。

通过单点链接:

1 var node = new Uri("http://myserver:9200");
2 var settings = new ConnectionSettings(node);
3 var client = new ElasticClient(settings);

 

通过连接池链接:

复制代码
 1 var nodes = new Uri[]
 2 {
 3     new Uri("http://myserver1:9200"),
 4     new Uri("http://myserver2:9200"),
 5     new Uri("http://myserver3:9200")
 6 };
 7 
 8 var pool = new StaticConnectionPool(nodes);
 9 var settings = new ConnectionSettings(pool);
10 var client = new ElasticClient(settings);
复制代码

 

 NEST Index

为了知道请求需要操作哪个索引,Elasticsearch API期望收到一个或多个索引名称作为请求的一部分。

一、指定索引

1、可以通过ConnectionSettings使用.DefaultIndex(),来指定默认索引。当一个请求里没有指定具体索引时,NEST将请求默认索引。

1 var settings = new ConnectionSettings()
2     .DefaultIndex("defaultindex");

2、可以通过ConnectionSettings使用.MapDefaultTypeIndices(),来指定被映射为CLR类型的索引。

1 var settings = new ConnectionSettings()
2     .MapDefaultTypeIndices(m => m
3         .Add(typeof(Project), "projects")
4     );

 

注意:通过.MapDefaultTypeIndices()指定索引的优先级要高于通过.DefaultIndex()指定索引,并且更适合简单对象(POCO)

3、另外还可以显示的为请求指定索引名称,例如:

1 var response = client.Index(student, s=>s.Index("db_test"));
2 var result = client.Search<Student>(s => s.Index("db_test"));
3 var result = client.Delete<Student>(null, s => s.Index("db_test"));
4 ……

 

注意:当现实的为请求指定索引名称时,这个优先级是最高的,高于以上两种方式指定的索引。

4、一些Elasticsearch API(比如query)可以采用一个、多个索引名称或者使用_all特殊标志发送请求,请求NEST上的多个或者所有节点

复制代码
 1 //请求单一节点
 2 var singleString = Nest.Indices.Index("db_studnet");
 3 var singleTyped = Nest.Indices.Index<Student>();
 4 
 5 ISearchRequest singleStringRequest = new SearchDescriptor<Student>().Index(singleString);
 6 ISearchRequest singleTypedRequest = new SearchDescriptor<Student>().Index(singleTyped);
 7 
 8 //请求多个节点
 9 var manyStrings = Nest.Indices.Index("db_studnet", "db_other_student");
10 var manyTypes = Nest.Indices.Index<Student>().And<OtherStudent>();
11 
12 ISearchRequest manyStringRequest = new SearchDescriptor<Student>().Index(manyStrings);
13 ISearchRequest manyTypedRequest = new SearchDescriptor<Student>().Index(manyTypes);
14 
15 //请求所有节点
16 var indicesAll = Nest.Indices.All;
17 var allIndices = Nest.Indices.AllIndices;
18 
19 ISearchRequest indicesAllRequest = new SearchDescriptor<Student>().Index(indicesAll);
20 ISearchRequest allIndicesRequest = new SearchDescriptor<Student>().Index(allIndices);
复制代码

 

二、创建索引

Elasticsearch API允许你创建索引的同时对索引进行配置,例如:

1 var descriptor = new CreateIndexDescriptor("db_student")
2     .Settings(s => s.NumberOfShards(5).NumberOfReplicas(1));
3 
4 client.CreateIndex(descriptor);

 

这里指定了该索引的分片数为5、副本数为1。

三、删除索引

Elasticsearch API允许你删除索引,例如:

1 var descriptor = new DeleteIndexDescriptor("db_student").Index("db_student");
2 
3 client.DeleteIndex(descriptor)

 

这里制定了要删除的索引名称“db_student”,以下为更多删除用例:

1 //删除指定索引所在节点下的所有索引
2 var descriptor = new DeleteIndexDescriptor("db_student").AllIndices();

 

 NEST Mapping

NEST提供了多种映射方法,这里介绍下通过Attribute自定义映射。

一、简单实现

1、定义业务需要的POCO,并指定需要的Attribute

复制代码
 1 [ElasticsearchType(Name = "student")]
 2 public class Student
 3 {
 4     [Nest.String(Index = FieldIndexOption.NotAnalyzed)]
 5     public string Id { get; set; }
 6 
 7     [Nest.String(Analyzer = "standard")]
 8     public string Name { get; set; }
 9 
10     [Nest.String(Analyzer = "standard")]
11     public string Description { get; set; }
12 
13     public DateTime DateTime { get; set; }
14 }
复制代码

 

2、接着我们通过.AutoMap()来实现映射

复制代码
1 var descriptor = new CreateIndexDescriptor("db_student")
2     .Settings(s => s.NumberOfShards(5).NumberOfReplicas(1))
3     .Mappings(ms => ms
4         .Map<Student>(m => m.AutoMap())
5     );
6 
7 client.CreateIndex(descriptor);
复制代码

 

注意:通过.Properties()可以重写通过Attribute定义的映射

二、Attribute介绍

1、StringAttribute

属性名 值类型 描述
Analyzer string 分析器名称,值包含standard、simple、whitespace、stop、keyward、pattern、language、snowball、custom等,查看分析器更多信息请点击Elasticsearch Analyzers
Boost double 加权值,值越大得分越高
NullValue string 插入文档时,如果数据为NULL时的默认值
Index FieldIndexOption 是否使用分析器,默认使用FieldIndexOption.Analyzed,禁止使用分析器FieldIndexOption.NotAnalyzed

2、NumberAttribute

属性名 值类型 描述
type NumberType 构造函数参数,指定当前属性的类型,NumberType.Default、Float、Double、Integer、Long、Short、Byte
Boost double 加权值,值越大得分越高
NullValue double 插入文档时,如果数据为NULL时的默认值

3、BooleanAttribute

属性名 值类型 描述
Boost double 加权值,值越大得分越高
NullValue double 插入文档时,如果数据为NULL时的默认值

4、DateAttribute

属性名 值类型 描述
Boost double 加权值,值越大得分越高
NullValue string 插入文档时,如果数据为NULL时的默认值
Format string

5、ObjectAttribute

属性名 值类型 描述
type string/Type 构造函数参数,指定当前属性的类型T
Dynamic DynamicMapping

 NEST Search

NEST提供了支持Lambda链式query DLS(领域特定语言)方式,以下是简单实现及各个query的简述。

一、简单实现

1、定义SearchDescriptor,方便项目中复杂业务的实现

1 var query = new Nest.SearchDescriptor<Models.ESObject>();
2 
3 var result = client.Search<Student>(x => query)

2、检索title和content中包含key,并且作者不等于“wenli”的文档

复制代码
 1 query.Query(q =>
 2     q.Bool(b =>
 3         b.Must(m =>
 4             m.MultiMatch(t => t.Fields(f => f.Field(obj => obj.Title).Field(obj => obj.Content)).Query(key))
 5         )
 6         .MustNot(m =>
 7             m.QueryString(t => t.Fields(f => f.Field(obj => obj.Author)).Query("wenli"))
 8         )
 9     )
10 );
复制代码
 

注意:

如果Elasticsearch使用默认分词,Title和Content的attribute为[Nest.String(Analyzer = “standard”)]

如果Elasticsearch使用的是IK分词,Title和Content的attribute为[Nest.String(Analyzer = “ikmaxword”)]或者[Nest.String(Analyzer = “ik_smart”)]

Author的attribute为[Nest.String(Index = FieldIndexOption.NotAnalyzed)],禁止使用分析器

3、过滤作者等于“wenli”的文档

query.PostFilter(x => x.Term(t => t.Field(obj => obj.Author).Value("wenli")));

4、过滤作者等于“wenli”或者等于“yswenli”的文档,匹配多个作者中间用空格隔开

1 query.PostFilter(x => x.QueryString(t => t.Fields(f => f.Field(obj => obj.Author)).Query("wenli yswenli")));

5、过滤数量在1~100之间的文档

1 query.PostFilter(x => x.Range(t => t.Field(obj => obj.Number).GreaterThanOrEquals(1).LessThanOrEquals(100)));

 

6、排序,按照得分倒叙排列

1 query.Sort(x => x.Field("_score", Nest.SortOrder.Descending));

 

7、定义高亮样式及字段

复制代码
1 query.Highlight(h => h
2     .PreTags("<b>")
3     .PostTags("</b>")
4     .Fields(
5         f => f.Field(obj => obj.Title),
6         f => f.Field(obj => obj.Content),
7         f => f.Field("_all")
8     )
9 );
复制代码

 

8、拼装查询内容,整理数据,方便前段调用

复制代码
 1 var list = result.Hits.Select(c => new Models.ESObject()
 2 {
 3     Id = c.Source.Id,
 4     Title = c.Highlights == null ? c.Source.Title : c.Highlights.Keys.Contains("title") ? string.Join("", c.Highlights["title"].Highlights) : c.Source.Title, //高亮显示的内容,一条记录中出现了几次
 5     Content = c.Highlights == null ? c.Source.Content : c.Highlights.Keys.Contains("content") ? string.Join("", c.Highlights["content"].Highlights) : c.Source.Content, //高亮显示的内容,一条记录中出现了几次
 6     Author = c.Source.Author,
 7     Number = c.Source.Number,
 8     IsDisplay = c.Source.IsDisplay,
 9     Tags = c.Source.Tags,
10     Comments = c.Source.Comments,
11     DateTime = c.Source.DateTime,
12 })
复制代码

 

二、query DSL介绍

 elasticsearch.net Document

文档操作包含添加/更新文档、局部更新文档、删除文档及对应的批量操作文档方法。

一、添加/更新文档及批量操作

添加/更新单一文档

1 Client.Index(student);

 

批量添加/更新文档

1 var list = new List<Student>();
2 
3 client.IndexMany<Student>(list);

 

二、局部更新单一文档及批量操作

局部更新单一文档

1 client.Update<Student, object>("002", upt => upt.Doc(new { Name = "wenli" }));

 

局部更新批量文档

复制代码
var ids = new List<string>() { "002" };

var bulkQuest = new BulkRequest() { Operations = new List<IBulkOperation>() };

foreach (var v in ids)
{
    var operation = new BulkUpdateOperation<Student, object>(v);

    operation.Doc = new { Name = "wenli" };

    bulkQuest.Operations.Add(operation);
}

var result = client.Bulk(bulkQuest);
复制代码

 

三、删除文档及批量操作

删除单一文档

1 client.Delete<Student>("001");

 

批量删除文档

复制代码
 1 var ids = new List<string>() { "001", "002" };
 2 
 3 var bulkQuest = new BulkRequest() { Operations = new List<IBulkOperation>() };
 4 
 5 foreach (var v in ids)
 6 {
 7     bulkQuest.Operations.Add(new BulkDeleteOperation<Student>(v));
 8 }
 9 
10 var result = client.Bulk(bulkQuest);
复制代码

 

 

转载请标明本文来源:http://www.cnblogs.com/yswenli/
更多内容欢迎我的的github:https://github.com/yswenli
如果发现本文有什么问题和任何建议,也随时欢迎交流~

MySQL在并发场景下的问题及解决思路

1、背景

    对于数据库系统来说在多用户并发条件下提高并发性的同时又要保证数据的一致性一直是数据库系统追求的目标,既要满足大量并发访问的需求又必须保证在此条件下数据的安全,为了满足这一目标大多数数据库通过锁和事务机制来实现,MySQL数据库也不例外。尽管如此我们仍然会在业务开发过程中遇到各种各样的疑难问题,本文将以案例的方式演示常见的并发问题并分析解决思路。

2、表锁导致的慢查询的问题

首先我们看一个简单案例,根据ID查询一条用户信息:

mysql> select * from user where id=6;

这个表的记录总数为3条,但却执行了13秒。

出现这种问题我们首先想到的是看看当前MySQL进程状态:

从进程上可以看出select语句是在等待一个表锁,那么这个表锁又是什么查询产生的呢?这个结果中并没有显示直接的关联关系,但我们可以推测多半是那条update语句产生的(因为进程中没有其他可疑的SQL),为了印证我们的猜测,先检查一下user表结构:

果然user表使用了MyISAM存储引擎,MyISAM在执行操作前会产生表锁,操作完成再自动解锁。如果操作是写操作,则表锁类型为写锁,如果操作是读操作则表锁类型为读锁。正如和你理解的一样写锁将阻塞其他操作(包括读和写),这使得所有操作变为串行;而读锁情况下读-读操作可以并行,但读-写操作仍然是串行。以下示例演示了显式指定了表锁(读锁),读-读并行,读-写串行的情况。

显式开启/关闭表锁,使用lock table user read/write; unlock tables;

session1:

session2:

可以看到会话1启用表锁(读锁)执行读操作,这时会话2可以并行执行读操作,但写操作被阻塞。接着看:

session1:

session2:

当session1执行解锁后,seesion2则立刻开始执行写操作,即读-写串行。

总结:

到此我们把问题的原因基本分析清楚,总结一下——MyISAM存储引擎执行操作时会产生表锁,将影响其他用户对该表的操作,如果表锁是写锁,则会导致其他用户操作串行,如果是读锁则其他用户的读操作可以并行。所以有时我们遇到某个简单的查询花了很长时间,看看是不是这种情况。

解决办法:

1)、尽量不用MyISAM存储引擎,在MySQL8.0版本中已经去掉了所有的MyISAM存储引擎的表,推荐使用InnoDB存储引擎。

2)、如果一定要用MyISAM存储引擎,减少写操作的时间;

3、线上修改表结构有哪些风险?

如果有一天业务系统需要增大一个字段长度,能否在线上直接修改呢?在回答这个问题前,我们先来看一个案例:

以上语句尝试修改user表的name字段长度,语句被阻塞。按照惯例,我们检查一下当前进程:

从进程可以看出alter语句在等待一个元数据锁,而这个元数据锁很可能是上面这条select语句引起的,事实正是如此。在执行DML(select、update、delete、insert)操作时,会对表增加一个元数据锁,这个元数据锁是为了保证在查询期间表结构不会被修改,因此上面的alter语句会被阻塞。那么如果执行顺序相反,先执行alter语句,再执行DML语句呢?DML语句会被阻塞吗?例如我正在线上环境修改表结构,线上的DML语句会被阻塞吗?答案是:不确定。

在MySQL5.6开始提供了online ddl功能,允许一些DDL语句和DML语句并发,在当前5.7版本对online ddl又有了增强,这使得大部分DDL操作可以在线进行。详见:https://dev.mysql.com/doc/refman/5.7/en/innodb-create-index-overview.html

所以对于特定场景执行DDL过程中,DML是否会被阻塞需要视场景而定。

总结:通过这个例子我们对元数据锁和online ddl有了一个基本的认识,如果我们在业务开发过程中有在线修改表结构的需求,可以参考以下方案:

1、尽量在业务量小的时间段进行;

2、查看官方文档,确认要做的表修改可以和DML并发,不会阻塞线上业务;

3、推荐使用percona公司的pt-online-schema-change工具,该工具被官方的online ddl更为强大,它的基本原理是:通过insert… select…语句进行一次全量拷贝,通过触发器记录表结构变更过程中产生的增量,从而达到表结构变更的目的。

例如要对A表进行变更,主要步骤为:

创建目的表结构的空表,A_new;
在A表上创建触发器,包括增、删、改触发器;
通过insert…select…limit N 语句分片拷贝数据到目的表
Copy完成后,将A_new表rename到A表。

4、一个死锁问题的分析

在线上环境下死锁的问题偶有发生,死锁是因为两个或多个事务相互等待对方释放锁,导致事务永远无法终止的情况。为了分析问题,我们下面将模拟一个简单死锁的情况,然后从中总结出一些分析思路。

演示环境:MySQL5.7.20 事务隔离级别:RR

表user:

CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(300) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8

下面演示事务1、事务2工作的情况:

事务1
事务2
事务监控
T1 begin;

Query OK, 0 rows affected (0.00 sec)

begin;

Query OK, 0 rows affected (0.00 sec)

T2 select * from user where id=3 for update;

+—-+——+——+
| id | name | age |
+—-+——+——+
| 3 | sun | 20 |
+—-+——+——+
1 row in set (0.00 sec)

select * from user where id=4 for update;

+—-+——+——+
| id | name | age |
+—-+——+——+
| 4 | zhou | 21 |
+—-+——+——+
1 row in set (0.00 sec)

select * from information_schema.INNODB_TRX;

通过查询元数据库innodb事务表,监控到当前运行事务数为2,即事务1、事务2。

T3 update user set name=’haha’ where id=4;

因为id=4的记录已被事务2加上行锁,该语句将阻塞

监控到当前运行事务数为2。
T4 阻塞状态 update user set name=’hehe’ where id=3;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

id=3的记录已被事务1加上行锁,而本事务持有id=4的记录行锁,此时InnoDB存储引擎检查出死锁,本事务被回滚。

事务2被回滚,事务1仍在运行中,监控当前运行事务数为1。
T5 Query OK, 1 row affected (20.91 sec)
Rows matched: 1 Changed: 1 Warnings: 0

由于事务2被回滚,原来阻塞的update语句被继续执行。

监控当前运行事务数为1。
T6 commit;

Query OK, 0 rows affected (0.00 sec)

事务1已提交、事务2已回滚,监控当前运行事务数为0。

这是一个简单的死锁场景,事务1、事务2彼此等待对方释放锁,InnoDB存储引擎检测到死锁发生,让事务2回滚,这使得事务1不再等待事务B的锁,从而能够继续执行。那么InnoDB存储引擎是如何检测到死锁的呢?为了弄明白这个问题,我们先检查此时InnoDB的状态:

show engine innodb status\G

————————
LATEST DETECTED DEADLOCK
————————
2018-01-14 12:17:13 0x70000f1cc000
*** (1) TRANSACTION:
TRANSACTION 5120, ACTIVE 17 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 10, OS thread handle 123145556967424, query id 2764 localhost root updating
update user set name=’haha’ where id=4
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 94 page no 3 n bits 80 index PRIMARY of table `test`.`user` trx id 5120 lock_mode X locks rec but not gap waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 0000000013fa; asc ;;
2: len 7; hex 520000060129a6; asc R ) ;;
3: len 4; hex 68616861; asc haha;;
4: len 4; hex 80000015; asc ;;

*** (2) TRANSACTION:
TRANSACTION 5121, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 11, OS thread handle 123145555853312, query id 2765 localhost root updating
update user set name=’hehe’ where id=3
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 94 page no 3 n bits 80 index PRIMARY of table `test`.`user` trx id 5121 lock_mode X locks rec but not gap
Record lock, heap no 5 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 0000000013fa; asc ;;
2: len 7; hex 520000060129a6; asc R ) ;;
3: len 4; hex 68616861; asc haha;;
4: len 4; hex 80000015; asc ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 94 page no 3 n bits 80 index PRIMARY of table `test`.`user` trx id 5121 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 0000000013fe; asc ;;
2: len 7; hex 5500000156012f; asc U V /;;
3: len 4; hex 68656865; asc hehe;;
4: len 4; hex 80000014; asc ;;

*** WE ROLL BACK TRANSACTION (2)

InnoDB状态有很多指标,这里我们截取死锁相关的信息,可以看出InnoDB可以输出最近出现的死锁信息,其实很多死锁监控工具也是基于此功能开发的。

在死锁信息中,显示了两个事务等待锁的相关信息(蓝色代表事务1、绿色代表事务2),重点关注:WAITING FOR THIS LOCK TO BE GRANTED和HOLDS THE LOCK(S)。

WAITING FOR THIS LOCK TO BE GRANTED表示当前事务正在等待的锁信息,从输出结果看出事务1正在等待heap no为5的行锁,事务2正在等待 heap no为7的行锁;

HOLDS THE LOCK(S):表示当前事务持有的锁信息,从输出结果看出事务2持有heap no为5行锁。

从输出结果看出,最后InnoDB回滚了事务2。

那么InnoDB是如何检查出死锁的呢?

我们想到最简单方法是假如一个事务正在等待一个锁,如果等待时间超过了设定的阈值,那么该事务操作失败,这就避免了多个事务彼此长等待的情况。参数innodb_lock_wait_timeout正是用来设置这个锁等待时间的。

如果按照这个方法,解决死锁是需要时间的(即等待超过innodb_lock_wait_timeout设定的阈值),这种方法稍显被动而且影响系统性能,InnoDB存储引擎提供一个更好的算法来解决死锁问题,wait-for graph算法。简单的说,当出现多个事务开始彼此等待时,启用wait-for graph算法,该算法判定为死锁后立即回滚其中一个事务,死锁被解除。该方法的好处是:检查更为主动,等待时间短。

下面是wait-for graph算法的基本原理:

为了便于理解,我们把死锁看做4辆车彼此阻塞的场景:

                

4辆车看做4个事务,彼此等待对方的锁,造成死锁。wait-for graph算法原理是把事务作为节点,事务之间的锁等待关系,用有向边表示,例如事务A等待事务B的锁,就从节点A画一条有向边到节点B,这样如果A、B、C、D构成的有向图,形成了环,则判断为死锁。这就是wait-for graph算法的基本原理。

总结:

1、如果我们业务开发中出现死锁如何检查出?刚才已经介绍了通过监控InnoDB状态可以得出,你可以做一个小工具把死锁的记录收集起来,便于事后查看。

2、如果出现死锁,业务系统应该如何应对?从上文我们可以看到当InnoDB检查出死锁后,对客户端报出一个Deadlock found when trying to get lock; try restarting transaction信息,并且回滚该事务,应用端需要针对该信息,做事务重启的工作,并保存现场日志事后做进一步分析,避免下次死锁的产生。

5、锁等待问题的分析

在业务开发中死锁的出现概率较小,但锁等待出现的概率较大,锁等待是因为一个事务长时间占用锁资源,而其他事务一直等待前个事务释放锁。

事务1
事务2
事务监控
T1 begin;

Query OK, 0 rows affected (0.00 sec)

begin;

Query OK, 0 rows affected (0.00 sec)

T2 select * from user where id=3 for update;

+—-+——+——+
| id | name | age |
+—-+——+——+
| 3 | sun | 20 |
+—-+——+——+
1 row in set (0.00 sec)

其他查询操作 select * from information_schema.INNODB_TRX;

通过查询元数据库innodb事务表,监控到当前运行事务数为2,即事务1、事务2。

T3  其他查询操作  update user set name=’hehe’ where id=3;

因为id=3的记录被事务1加上行锁,所以该语句将阻塞(即锁等待)

 监控到当前运行事务数为2。
T4 其他查询操作 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

锁等待时间超过阈值,操作失败。注意:此时事务2并没有回滚。

监控到当前运行事务数为2。
T5 commit; 事务1已提交,事务2未提交,监控到当前运行事务数为1。

从上述可知事务1长时间持有id=3的行锁,事务2产生锁等待,等待时间超过innodb_lock_wait_timeout后操作中断,但事务并没有回滚。如果我们业务开发中遇到锁等待,不仅会影响性能,还会给你的业务流程提出挑战,因为你的业务端需要对锁等待的情况做适应的逻辑处理,是重试操作还是回滚事务。

在MySQL元数据表中有对事务、锁等待的信息进行收集,例如information_schema数据库下的INNODB_LOCKS、INNODB_TRX、INNODB_LOCK_WAITS,你可以通过这些表观察你的业务系统锁等待的情况。你也可以用一下语句方便的查询事务和锁等待的关联关系:

SELECT     r.trx_id waiting_trx_id,     r.trx_mysql_thread_id waiting_thread,     r.trx_query wating_query,     b.trx_id blocking_trx_id,     b.trx_mysql_thread_id blocking_thread,     b.trx_query blocking_query FROM     information_schema.innodb_lock_waits w         INNER JOIN     information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id         INNER JOIN     information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

结果:

waiting_trx_id: 5132
waiting_thread: 11
wating_query: update user set name=’hehe’ where id=3
blocking_trx_id: 5133
blocking_thread: 10
blocking_query: NULL

总结:

1、请对你的业务系统做锁等待的监控,这有助于你了解当前数据库锁情况,以及为你优化业务程序提供帮助;

2、业务系统中应该对锁等待超时的情况做合适的逻辑判断。

6、小结

    本文通过几个简单的示例介绍了我们常用的几种MySQL并发问题,并尝试得出针对这些问题我们排查的思路。文中涉及事务、表锁、元数据锁、行锁,但引起并发问题的远远不止这些,例如还有事务隔离级别、GAP锁等。真实的并发问题可能多而复杂,但排查思路和方法却是可以复用,在本文中我们使用了show processlist;show engine innodb status;以及查询元数据表的方法来排查发现问题,如果问题涉及到了复制,还需要借助master/slave监控来协助。

 

参考资料:

姜承尧《InnoDB存储引擎》

李宏哲 杨挺 《MySQL排查指南》

何登成 http://hedengcheng.com

 

========================我是广告=======================

美团点评在成都、北京、上海招聘初中高级java后台、前端工程师,月薪20-50k,需要内推的可以发邮件给我:360841519@qq.com

有赞API网关实践

一、API网关简介

随着移动互联网的兴起、开放合作思维的盛行,不同终端和第三方开发者都需要大量的接入企业核心业务能力,此时各业务系统将会面临同一系列的问题,例如:如何让调用方快速接入、如何让业务方安全地对外开放能力,如何应对和控制业务洪峰调用等等。于是就诞生了一个隔离企业内部业务系统和外部系统调用的屏障 – API网关,它负责在上层抽象出各业务系统需要的通用功能,例如:鉴权、限流、ACL、降级等。另外随着近年来微服务的流行,API网关已经成为一个微服务架构中的标配组件。

二、有赞API网关简介

有赞API网关目前承载着微商城、零售、微小店、餐饮、美业、AppSDK、部分PC、三方开发者等多个业务的调用,每天有着亿级别的流量。

有赞后端服务最开始是由PHP搭建,随着整个技术体系的升级,后面逐步从PHP迁移到Java体系。在API网关设计之初主要支持Dubbo、Http两种协议。迁移过程中,我们发现部分服务需要通过RPC方式调用PHP服务,于是我们(公司)基于Dubbo开发了一个新的框架Nova,兼容Dubbo调用,同时支持调用PHP服务。于是网关也支持了新的Nova协议,这样就有Dubbo、Http、Nova三种协议。

随着业务的不断发展,业务服务化速度加快,网关面临各类新的需求。例如回调类型的API接入,这种API不需要鉴权,只需要一个限流服务,路由到后端服务即可;另外还有参数、返回值的转换需求也不断到来,这期间我们快速迭代满足新的需求。而在这个过程中我们也走了很多弯路,例如API的规范,在最开始规范意识比较笼统,导致返回值在对外暴露时出现了不统一的情况,后续做SDK自动化的时候比较棘手,经过不断的约束开发者,最终做到了统一。

三、架构与设计

1. 网关架构

部署架构图

网关的调用方主要包括微商城、微小店、零售等App应用,以及三方开发者和部分PC业务。通过LVS做负载均衡,后端Tengine实现反向代理,网关应用调用到实际的业务集群

应用架构图

网关核心由Pipe链构成,每个Pipe负责一块功能,同时使用缓存、异步等特性提升并发及性能

线程模型图

网关采用Jetty部署,调用采用Http协议,请求由容器线程池处理(容器开启了Servlet3.0异步,提升了较大的吞吐量),之后分发到应用线程池异步处理。应用线程池在设计之初考虑不同的任务执行可能会出现耗时不一的情况,所以将任务分别拆分到不同的线程池,以提高不同类型任务的并发度,如图分为CommonGroup, ExecutionGroup, ResultGroup

CommonGroup执行通用任务,ExecutionGroup执行多协议路由及调用任务,ResultGroup执行结果处理任务(包含异常)

网关业务生态图

网关生态主要包含控制台、网关核心、网关统计与监控
控制台主要对API生命周期进行管理,以及ACL、流量管控等功能;
网关核心主要处理API调用,包含鉴权、限流、路由、协议转换等功能;
统计与监控模块主要完成API调用的统计以及对店铺、三方的一些报表统计,同时提供监控功能和报警功能

2. 网关核心设计

2.1 异步

我们使用Jetty容器来部署应用,并开启Servlet3.0的异步特性,由于网关业务本身就是调用大量业务接口,因此IO操作会比较频繁,使用该特性能较大提升网关整体并发能力及吞吐量。另外我们在内部处理开启多组线程池进行异步处理,以异步回调的方式通知任务完成,进一步提升并发量

image

2.2 二级缓存

为了进一步提升网关的性能,我们增加了一层分布式缓存(借用Codis实现),将一些不经常变更的API元数据缓存下来,这样不仅减少了应用和DB的交互次数,还加快了读取效率。我们同时考虑到Codis在极端情况下存在不稳定因素,因此我们在本地再次做了本地缓存,这样的读取可以从ms级别降低到ns级别。为了实现多台机器的本地缓存一致性,我们使用了ZK监听节点变化来更新各机器本地缓存

image

2.3 链式处理

在设计网关的时候,我们采用责任链模式来实现网关的核心处理流程,将每个处理逻辑看成一个Pipe,每个Pipe按照预先设定的顺序先后执行,与开源的Zuul 1.x类似,我们也采用了PRPE模式(Pre、Routing、Post、Error),在我们这里Pre分为PrePipe、RateLimitPipe、AuthPipe、AclPipe、FlowSepPipe,这些Pipe对数据进行预处理、限流、鉴权、访问控制、分流,并将过滤后的Context向下传递;Routing分为DubboPipe、HttpPipe,这些Pipe分别处理Dubbo协议、Http协议路由及调用;Post为ResultPipe,处理正常返回值以及统计打点,Error为ErrorPipe,处理异常场景

image

2.4 线程池隔离

Jetty容器线程池(QTP)负责接收Http请求,之后交由应用线程池CommonGroup,ExecutionGroup, ResultGroup,通用的操作将会被放到CommonGroup线程池执行,执行真实调用的被放到ExecutionGroup,结果处理放到ResultGroup。这样部分Pipe之间线程隔离,通常前置Pipe处理都比较快,所以共享线程池即可,真实调用通常比较耗时,因此我们放到独立的线程池,同时结果处理也存在一些运算,因此也放到独立线程池

image

2.5 平滑限流

最早我们采用了简单的分布式缓存(Codis)计数实现限流,以IP、API维度构建Key进行累加,这种限流方式实现简单,但是不能做到连续时间段内平滑限流。例如针对某个API每分钟限流100次,第1秒发起20次,第二秒发起30次,第3秒发起40次,这样的限流波动比较大,因此我们决定将其改进。经过调研我们最终选择了令牌桶限流,令牌桶限流相比于漏桶限流能适应闲置较长时段后的尖峰调用,同时消除了简单计数器限流带来的短时间内流量不均的问题。目前网关支持IP、店铺、API、应用ID和三方ID等多个维度的限流,也支持各维度的自由组合限流,可以很容易扩展出新的维度

image

2.6 熔断降级

由于我们经常遇到调用后端接口超时,或者异常的情况,后端服务无法立即恢复,这种情况下再将请求发到后端已没有意义。于是我们使用Hystrix进行熔断降级处理。Hystrix支持线程池和信号量2种模式的隔离方案,网关的业务场景是多API和API分组,每个API都可能路由到不同后端服务,如果我们对API或者API分组做线程池隔离,就会产生大量的线程,所以我们选择了信号量做隔离。我们为每个API提供一个降级配置,用户可以选择自己配置的API在达到多少错误率时进行熔断降级。
引入Hystrix后,Hystrix会对每个API做统计,包括总量、正确率、QPS等指标,同时会产生大量事件,当API很多的时候,这些指标和事件会占用大量内存,导致更加频繁的YoungGC,这对应用性能产生了一定的影响,不过整体的收益还是不错的

另外有赞内部也开发了一个基于Hystrix的服务熔断平台(Tesla),平台在可视化、易用性、扩展性上面均有较大程度的提升;后续网关会考虑熔断模块的实现基于服务熔断平台,以提供更好的服务

image

2.7 分流

有赞内部存在多种协议类型的后端服务,最原始的服务是PHP开发,后面逐渐迁移到Java,很早一部分API是由PHP暴露的,后续为了能做灰度迁移到Java,我们做了分流,将老的PHP接口的流量按照一定的比例分发到新的Java接口上

3. 控制台

除了核心功能的调用外,网关还需要支持内部用户(下称业务方)快速配置接口暴露给开发者。 控制台主要职责包括:快速配置API、一站式测试API、一键发布API,自动化文档生成,自动化SDK生成

  • 快速配置API:这块我们主要是按照对外、对内来进行配置,业务方将自己要对外公开的名称、参数编辑好,再通过对内映射将对外参数映射到内部服务的接口里面

image

  • 一站式测试API:API配置完成后,为了能让业务方快速测试,我们做了一站式获取鉴权值,参数值自动保存,做到一站式测试

image

  • 一键发布API:在完成配置和测试后,API就可以直接发布,这个时候选择对应环境的注册中心或者服务域名即可

image

  • 自动化文档生成:我们针对文档这块做了文档中心,对内部用户,他们只需要到平台来搜索即可,对外部用户,可以在有赞云官网查看或者在控制台直接导出pdf文件给用户

image

  • 自动化SDK生成:对于开发者来说,接入一个平台必然少不了SDK,我们针对多语言做了自动化SDK生成,当用户的接口发布成功后,我们会监听到有新的接口,这时会触发自动编译(Java)SDK的模块,将新接口打包成新版本的压缩包,供开发者使用;如果编译失败(Java)则不会替换老的压缩包,我们会发送报警给相应的开发者,让其调整不规范的地方

image

4. 数据统计

为了让业务方能在上线后了解自己的接口的运行状况,我们做了API相关的统计。我们通过在核心模块里面打日志,利用rsyslog采集数据到Kafka,然后从Kafka消费进行统计,之后回流到数据库供在线查询

除此之外,我们为每个商家做了他们授权的服务商调用接口的统计。这块功能的实现,我们通过Storm从Kafka实时消费,并实时统计落HBase,每天凌晨将前一天的数据同步到Hive进行统计并回流到数据库

image

5. 报警监控

业务方API上线后,除了查看统计外,当API出问题时,还需要及时发现。我们针对这块做了API报警功能。用户在平台配置自己的API的报警,这里我们主要支持基于错误数或RT维度的报警。
我们实时地从Kafka消费API调用日志,如果发现某个API的RT或者错误次数超过配置的报警阈值,则会立即触发报警

image

四、实践总结

1. 规范

在网关上暴露的API很多,如何让这些API按照统一的标准对外暴露,让开发者能够低门槛快速接入是网关需要思考的问题

网关规范主要是对API的命名、入参(公用入参、业务入参)、内部服务返回值、错误码(公用错误码、业务错误码)、出参(公用出参、业务出参),进行规范

在我们的实践过程中,总结了以下规范:

  • 命名规范:youzan.[业务线(可选)].[应用名].[动作].[版本],例如:youzan.item.create.3.0.0
  • 入参规范:要求全部小写,组合单词以下划线分隔,例如:title, item_id;入参如果是一个结构体,要求以json字符串传入,并且json中的key必须小写并且以下划线分隔
  • 出参规范:要求全部小写,组合单词以下划线分隔,例如:page_num, total_count;如果参数为结构体,结构体里面的key必须小写且以下划线分隔
  • 错误码规范:我们做了统一的错误码,例如系统级错误码51xxx,业务错误码50000,详情信息由msg显示;业务级错误码由业务方自行定义,同时约束每个业务方的错误码范围
  • 服务返回值规范:针对不同的业务方,每个API可能会有不同的业务错误,我们需要将这部分业务级错误展示给开发者,因此我们约定返回值需要按照一个POJO类型(包含code, msg, data)来返回,对于code为200,我们认为正常返回,否则认为是业务错误,将返回值包装为错误结果

2. 发布

  • 我们将API划分到3个环境,分别为测试环境、预发环境、生产环境。API的创建、编辑必须在测试环境进行,测试完成后,可以将API发布到预发环境,之后再从预发环境发布到生产环境,这样可以保持三个环境的API数据一致。好处是:一方面可以让测试开发能在测试环境进行自动化验证,另一方面可以防止用户直接编辑线上接口引发故障

3. 工具化

  • 对于内部用户经常可能需要排查问题,例如OAuth Token里面带的参数,需要经常查询,我们提供工具化的控制台,能让用户方便查询,从而减少答疑量
  • 我们上线后也曾经出现过缓存不一致的情况,为了能快速排查问题,我们做了缓存管理工具,能在图形化界面上查看本地缓存以及Codis的缓存,可以进行对比找出差异
  • 为了更好的排查线上问题,我们接入了有赞对比引擎(Replay)平台,该平台能将线上的流量引到预发,帮助开发者更快定位问题

五、踩过的坑

  • Meta区Full GC导致服务无法响应

    现象:应用hung死,调用接口返回503,无法服务

    排查过程:现场dump了内存,GC记录,以及线程运行快照。首先看了GC发现是Full GC,但是不清楚是哪里发生的,看线程运行快照也没发现什么问题。于是在本地用HeapAnalysis分析,堆区没看出什么问题,大对象都是应该占用的;于是查看方法区,通过ClassLoader Analysis发现Fastjson相关的类较多,因此怀疑是class泄露,进一步通过MAT的OQL语法分析,发现是Fastjson在序列化Jetty容器的HttpServletRequest时,为了加快速度于是创建新的类时抛了异常,导致动态创建的类在方法区堆积从而引发Full GC,后续我们也向Fastjson提了相关bug

    解决方案:将序列化HttpServletRequest的代码移除

  • 伪死循环导致CPU 100%

    现象:在有赞双11全链路压测期间,某个业务调用API,导致我们的应用CPU几乎接近100%

    排查过程:经过日志分析,发现该接口存在大量超时,但是从代码没看出特别有问题的地方。于是我们将接口在QA环境模拟调用,用VisualVM连上去,通过抽样器抽样CPU,发现某个方法消耗CPU较高,因此我们迅速定位到源码,发现这段代码主要是执行轮询任务是否完成,如果完成则调用完成回调,如果未完成继续放到队列。再结合之前的环境观察发现大量超时的任务被放到队列,导致任务被取出后,任务仍然是未完成状态,这样会将任务放回队列,这样其实构成了一个死循环

    解决方案:将主动轮询改为异步通知,我们这里是Dubbo调用,Dubbo调用返回的Future实际是一个FutureAdapter,可以获取到里面的ResponseFuture(DefaultFuture),这个类型的Future支持设置Callback,任务完成时会通知到设置的回调

六、未来展望

  1. 业务级资源组隔离。随着业务的不断发展,当业务线较多时,可以将重要的业务分配到更优质的资源组(例如:机器性能、线程池的大小),将一般业务放到普通资源组,这样可以更好的服务不同的业务场景
  2. 更高并发的线程池/IO的优化。随着业务的发展,未来可能会出现更高的并发,需要更精良的线程及IO模型
  3. 更多的协议支持。以后技术的发展,Http2可能会蓬勃发展,这时需要接入Http2的协议

七、结语

有赞网关目前归属有赞共享技术-基础服务中心团队开发和维护;
该团队目前主要分为商品中心、库存中心、物流中心、消息沟通平台、云生态5个小组;
商品/库存/物流中心:通过不断抽象上层业务,完成通用的模型建设;为上层业务方提供高可用的服务,并快速响应多变的业务需求;针对秒杀、洪峰调用、及上层业务多变等需求,三个小组还齐力开发和持续完善着 对比引擎、服务熔断、热点探测等三个通用系统;
消息沟通平台:提供几乎一切消息沟通相关的能力及一套帮助商家与用户联系的多客服系统,每天承载着上亿次调用(短信、apppush、语音、微信、微博、多客服、邮件等通道);
云生态:承担着核心网关的建设和发展(上面的网关应用系统)、三方推送系统、有赞云后台、商业化订购以及App Engine的预研和开发;

目前该团队HC开放,期待有机会与各位共事;(内推邮箱:huangtao@youzan.com)

注,本文作者:有赞网关(黄涛、尹铁夫、叮咚)

知识共享许可协议
如无特殊说明,本文版权归 本文作者及有赞技术团队 所有,采用 署名-非商业性使用 4.0 国际许可协议 进行许可。
转载请注明:来自有赞技术团队博客 http://tech.youzan.com/api-gateway-in-practice/

别慌,不就是跨域么

什么是跨域

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。

同源策略限制了一下行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获取
  • Ajax请求发送不出去

常见的跨域场景

所谓的同源是指,域名、协议、端口均为相同。

http://www.nealyang.cn/index.html 调用 http://www.nealyang.cn/server.php 非跨

http://www.nealyang.cn/index.html 调用 http://www.neal.cn/server.php 跨域,主域不同

http://abc.nealyang.cn/index.html 调用 http://def.neal.cn/server.php 跨域,子域名不同

http://www.nealyang.cn:8080/index.html 调用 http://www.nealyang.cn/server.php 跨域,端口不同

https://www.nealyang.cn/index.html 调用 http://www.nealyang.cn/server.php 跨域,协议不同

localhost 调用 127.0.0.1 跨域

跨域的解决办法

jsonp跨域

jsonp跨域其实也是JavaScript设计模式中的一种代理模式。在html页面中通过相应的标签从不同域名下加载静态资源文件是被浏览器允许的,所以我们可以通过这个“犯罪漏洞”来进行跨域。一般,我们可以动态的创建script标签,再去请求一个带参网址来实现跨域通信

//原生的实现方式

let script = document.createElement(‘script’);

script.src = ‘http://www.nealyang.cn/login?username=Nealyang&callback=callback’;

document.body.appendChild(script);

function callback(res) {

console.log(res);

}

当然,jquery也支持jsonp的实现方式

$.ajax({

url:’http://www.nealyang.cn/login’,

type:’GET’,

dataType:’jsonp’,//请求方式为jsonp

jsonpCallback:’callback’,

data:{

“username”:”Nealyang”

}

})

虽然这种方式非常好用,但是一个最大的缺陷是,只能够实现get请求

document.domain + iframe 跨域

这种跨域的方式最主要的是要求主域名相同。什么是主域名相同呢? www.nealyang.cn aaa.nealyang.cn ba.ad.nealyang.cn 这三个主域名都是nealyang.cn,而主域名不同的就不能用此方法。

假设目前a.nealyang.cn 和 b.nealyang.cn 分别对应指向不同ip的服务器。

a.nealyang.cn 下有一个test.html文件

<!DOCTYPE html>

<html lang=”en”>

<head>

<meta charset=”UTF-8″>

<title>html</title>

<script type=”text/javascript” src = “jquery-1.12.1.js”></script>

</head>

<body>

<div>A页面</div>

<iframe

style = “display : none”

name = “iframe1”

id = “iframe”

src=”http://b.nealyang.cn/1.html” frameborder=”0″></iframe>

<script type=”text/javascript”>

$(function(){

try{

document.domain = “nealyang.cn”

}catch(e){}

$(“#iframe”).load(function(){

var jq = document.getElementById(‘iframe’).contentWindow.$

jq.get(“http://nealyang.cn/test.json”,function(data){

console.log(data);

});

})

})

</script>

</body>

</html>

利用 iframe 加载 其他域下的文件(nealyang.cn/1.html), 同时 document.domain 设置成 nealyang.cn ,当 iframe 加载完毕后就可以获取 nealyang.cn 域下的全局对象, 此时尝试着去请求 nealyang.cn 域名下的 test.json (此时可以请求接口),就会发现数据请求失败了~~ 惊不惊喜,意不意外!!!!!!!

数据请求失败,目的没有达到,自然是还少一步:

<!DOCTYPE html>

<html lang=”en”>

<head>

<meta charset=”UTF-8″>

<title>html</title>

<script type=”text/javascript” src = “jquery-1.12.1.js”></script>

<script type=”text/javascript”>

$(function(){

try{

document.domain = “nealyang.com”

}catch(e){}

})

</script>

</head>

<body>

<div id = “div1”>B页面</div>

</body>

</html>

此时在进行刷新浏览器,就会发现数据这次真的是成功了

window.name + iframe 跨域

window.name属性可设置或者返回存放窗口名称的一个字符串。他的神器之处在于name值在不同页面或者不同域下加载后依旧存在,没有修改就不会发生变化,并且可以存储非常长的name(2MB)

假设index页面请求远端服务器上的数据,我们在该页面下创建iframe标签,该iframe的src指向服务器文件的地址(iframe标签src可以跨域),服务器文件里设置好window.name的值,然后再在index.html里面读取改iframe中的window.name的值。完美~

<body>

<script type=”text/javascript”>

iframe = document.createElement(‘iframe’),

iframe.src = ‘http://localhost:8080/data.php’;

document.body.appendChild(iframe);

iframe.onload = function() {

console.log(iframe.contentWindow.name)

};

</script>

</body>

当然,这样还是不够的。

因为规定如果index.html页面和和该页面里的iframe框架的src如果不同源,则也无法操作框架里的任何东西,所以就取不到iframe框架的name值了,告诉你我们不是一家的,你也休想得到我这里的数据。 既然要同源,那就换个src去指,前面说了无论怎样加载window.name值都不会变化,于是我们在index.html相同目录下,新建了个proxy.html的空页面,修改代码如下:

<body>

<script type=”text/javascript”>

iframe = document.createElement(‘iframe’),

iframe.src = ‘http://localhost:8080/data.php’;

document.body.appendChild(iframe);

iframe.onload = function() {

iframe.src = ‘http://localhost:81/cross-domain/proxy.html’;

console.log(iframe.contentWindow.name)

};

</script>

</body>

理想似乎很美好,在iframe载入过程中,迅速重置iframe.src的指向,使之与index.html同源,那么index页面就能去获取它的name值了!但是现实是残酷的,iframe在现实中的表现是一直不停地刷新, 也很好理解,每次触发onload时间后,重置src,相当于重新载入页面,又触发onload事件,于是就不停地刷新了(但是需要的数据还是能输出的)。修改后代码如下:

<body>

<script type=”text/javascript”>

iframe = document.createElement(‘iframe’);

iframe.style.display = ‘none’;

var state = 0;

iframe.onload = function() {

if(state === 1) {

var data = JSON.parse(iframe.contentWindow.name);

console.log(data);

iframe.contentWindow.document.write(”);

iframe.contentWindow.close();

document.body.removeChild(iframe);

} else if(state === 0) {

state = 1;

iframe.contentWindow.location = ‘http://localhost:81/cross-domain/proxy.html’;

}

};

iframe.src = ‘http://localhost:8080/data.php’;

document.body.appendChild(iframe);

</script>

</body>

所以如上,我们就拿到了服务器返回的数据,但是有几个条件是必不可少的:

  • iframe标签的跨域能力
  • window.names属性值在文档刷新后依然存在的能力

location.hash + iframe 跨域

此跨域方法和上面介绍的比较类似,一样是动态插入一个iframe然后设置其src为服务端地址,而服务端同样输出一端js代码,也同时通过与子窗口之间的通信来完成数据的传输。

关于锚点相信大家都已经知道了,其实就是设置锚点,让文档指定的相应的位置。锚点的设置用a标签,然后href指向要跳转到的id,当然,前提是你得有个滚动条,不然也不好滚动嘛是吧。

而location.hash其实就是url的锚点。比如http://www.nealyang.cn#Nealyang的网址打开后,在控制台输入location.hash就会返回#Nealyang的字段。

基础知识补充完毕,下面我们来说下如何实现跨域

如果index页面要获取远端服务器的数据,动态的插入一个iframe,将iframe的src执行服务器的地址,这时候的top window 和包裹这个iframe的子窗口是不能通信的,因为同源策略,所以改变子窗口的路径就可以了,将数据当做改变后的路径的hash值加载路径上,然后就可以通信了。将数据加在index页面地址的hash上, index页面监听hash的变化,h5的hashchange方法

<body>

<script type=”text/javascript”>

function getData(url, fn) {

var iframe = document.createElement(‘iframe’);

iframe.style.display = ‘none’;

iframe.src = url;

iframe.onload = function() {

fn(iframe.contentWindow.location.hash.substring(1));

window.location.hash = ”;

document.body.removeChild(iframe);

};

document.body.appendChild(iframe);

}

// get data from server

var url = ‘http://localhost:8080/data.php’;

getData(url, function(data) {

var jsondata = JSON.parse(data);

console.log(jsondata.name + ‘ ‘ + jsondata.age);

});

</script>

</body>

补充说明:其实location.hash和window.name都是差不多的,都是利用全局对象属性的方法,然后这两种方法和jsonp也是一样的,就是只能够实现get请求

postMessage跨域

这是由H5提出来的一个炫酷的API,IE8+,chrome,ff都已经支持实现了这个功能。这个功能也是非常的简单,其中包括接受信息的Message时间,和发送信息的postMessage方法。

发送信息的postMessage方法是向外界窗口发送信息

otherWindow.postMessage(message,targetOrigin);

otherWindow指的是目标窗口,也就是要给哪一个window发送消息,是window.frames属性的成员或者是window.open方法创建的窗口。 Message是要发送的消息,类型为String,Object(IE8、9不支持Obj),targetOrigin是限定消息接受范围,不限制就用星号 *

接受信息的message事件

var onmessage = function(event) {

var data = event.data;

var origin = event.origin;

}

if(typeof window.addEventListener != ‘undefined’){

window.addEventListener(‘message’,onmessage,false);

}else if(typeof window.attachEvent != ‘undefined’){

window.attachEvent(‘onmessage’, onmessage);

}

举个栗子

a.html(http://www.nealyang.cn/a.html)

<iframe id=”iframe” src=”http://www.neal.cn/b.html” style=”display:none;”></iframe>

<script>

var iframe = document.getElementById(‘iframe’);

iframe.onload = function() {

var data = {

name: ‘aym’

};

// 向neal传送跨域数据

iframe.contentWindow.postMessage(JSON.stringify(data), ‘http://www.neal.cn’);

};

// 接受domain2返回数据

window.addEventListener(‘message’, function(e) {

alert(‘data from neal —> ‘ + e.data);

}, false);

</script>

b.html(http://www.neal.cn/b.html)

<script>

// 接收domain1的数据

window.addEventListener(‘message’, function(e) {

alert(‘data from nealyang —> ‘ + e.data);

var data = JSON.parse(e.data);

if (data) {

data.number = 16;

// 处理后再发回nealyang

window.parent.postMessage(JSON.stringify(data), ‘http://www.nealyang.cn’);

}

}, false);

</script>

跨域资源共享 CORS

因为是目前主流的跨域解决方案。所以这里多介绍点。

简介

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。IE8+:IE8/9需要使用XDomainRequest对象来支持CORS。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

两种请求

说起来很搞笑,分为两种请求,一种是简单请求,另一种是非简单请求。只要满足下面条件就是简单请求

  • 请求方式为HEAD、POST 或者 GET
  • http头信息不超出一下字段:Accept、Accept-Language 、 Content-Language、 Last-Event-ID、 Content-Type(限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)

为什么要分为简单请求和非简单请求,因为浏览器对这两种请求方式的处理方式是不同的。

简单请求

基本流程

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。 下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1

Origin: http://api.bob.com

Host: api.alice.com

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0

Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。 浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。

注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com

Access-Control-Allow-Credentials: true

Access-Control-Expose-Headers: FooBar

Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头

  • Access-Control-Allow-Origin :该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
  • Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
  • Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

另一方面,开发者必须在AJAX请求中打开withCredentials属性。

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie

xhr.withCredentials = true;

xhr.open(‘post’, ‘http://www.domain2.com:8080/login’, true);

xhr.setRequestHeader(‘Content-Type’, ‘application/x-www-form-urlencoded’);

xhr.send(‘user=admin’);

xhr.onreadystatechange = function() {

if (xhr.readyState == 4 && xhr.status == 200) {

alert(xhr.responseText);

}

};

// jquery

$.ajax({

xhrFields: {

withCredentials: true // 前端设置是否带cookie

},

crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie

});

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。 但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

var url = ‘http://api.alice.com/cors’;

var xhr = new XMLHttpRequest();

xhr.open(‘PUT’, url, true);

xhr.setRequestHeader(‘X-Custom-Header’, ‘value’);

xhr.send();

浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求,要求服务器确认可以这样请求。下面是这个”预检”请求的HTTP头信息。

OPTIONS /cors HTTP/1.1

Origin: http://api.bob.com

Access-Control-Request-Method: PUT

Access-Control-Request-Headers: X-Custom-Header

Host: api.alice.com

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0…

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,”预检”请求的头信息包括两个特殊字段。

  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
预检请求的回应

服务器收到”预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应

HTTP/1.1 200 OK

Date: Mon, 01 Dec 2008 01:15:39 GMT

Server: Apache/2.0.61 (Unix)

Access-Control-Allow-Origin: http://api.bob.com

Access-Control-Allow-Methods: GET, POST, PUT

Access-Control-Allow-Headers: X-Custom-Header

Content-Type: text/html; charset=utf-8

Content-Encoding: gzip

Content-Length: 0

Keep-Alive: timeout=2, max=100

Connection: Keep-Alive

Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

如果浏览器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

服务器回应的其他CORS相关字段如下:

Access-Control-Allow-Methods: GET, POST, PUT

Access-Control-Allow-Headers: X-Custom-Header

Access-Control-Allow-Credentials: true

Access-Control-Max-Age: 1728000

  • Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
  • Access-Control-Allow-Headers:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。
  • Access-Control-Allow-Credentials: 该字段与简单请求时的含义相同。
  • Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
浏览器正常请求回应

一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

PUT /cors HTTP/1.1

Origin: http://api.bob.com

Host: api.alice.com

X-Custom-Header: value

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0…

浏览器的正常CORS请求。上面头信息的Origin字段是浏览器自动添加的。下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.bob.com

Content-Type: text/html; charset=utf-8

Access-Control-Allow-Origin字段是每次回应都必定包含的

结束语

CORS与JSONP的使用目的相同,但是比JSONP更强大。JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

前端代码:

<div>user input:<input type=”text”></div>

<script src=”./socket.io.js”></script>

<script>

var socket = io(‘http://www.domain2.com:8080’);

// 连接成功处理

socket.on(‘connect’, function() {

// 监听服务端消息

socket.on(‘message’, function(msg) {

console.log(‘data from server: —> ‘ + msg);

});

// 监听服务端关闭

socket.on(‘disconnect’, function() {

console.log(‘Server socket has closed.’);

});

});

document.getElementsByTagName(‘input’)[0].onblur = function() {

socket.send(this.value);

};

</script>

node Server

var http = require(‘http’);

var socket = require(‘socket.io’);

// 启http服务

var server = http.createServer(function(req, res) {

res.writeHead(200, {

‘Content-type’: ‘text/html’

});

res.end();

});

server.listen(‘8080’);

console.log(‘Server is running at port 8080…’);

// 监听socket连接

socket.listen(server).on(‘connection’, function(client) {

// 接收信息

client.on(‘message’, function(msg) {

client.send(‘hello:’ + msg);

console.log(‘data from client: —> ‘ + msg);

});

// 断开处理

client.on(‘disconnect’, function() {

console.log(‘Client socket has closed.’);

});

});

node代理跨域

node中间件实现跨域代理,是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

利用node + express + http-proxy-middleware搭建一个proxy服务器

前端代码

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie

xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器

xhr.open(‘get’, ‘http://www.domain1.com:3000/login?user=admin’, true);

xhr.send();

后端代码

var express = require(‘express’);

var proxy = require(‘http-proxy-middleware’);

var app = express();

app.use(‘/’, proxy({

// 代理跨域目标接口

target: ‘http://www.domain2.com:8080’,

changeOrigin: true,

// 修改响应头信息,实现跨域并允许带cookie

onProxyRes: function(proxyRes, req, res) {

res.header(‘Access-Control-Allow-Origin’, ‘http://www.domain1.com’);

res.header(‘Access-Control-Allow-Credentials’, ‘true’);

},

// 修改响应信息中的cookie域名

cookieDomainRewrite: ‘www.domain1.com’ // 可以为false,表示不修改

}));

app.listen(3000);

console.log(‘Proxy server is listen at port 3000…’);

nginx代理跨域

NGINX其实个人没有怎么玩过,所以暂且也就不能误人子弟了,原谅笔者才疏尚浅~ 有机会学习研究再回来补充~~

参考文档

http://www.ruanyifeng.com/blog/2016/04/cors.html

https://segmentfault.com/a/1190000011145364

via  github.com/Nealyang/YOU-SHOULD-KNOW-JS/blob/master/doc/basic_js/JavaScript中的跨域总结.md

Net Core中数据库事务隔离详解——以Dapper和Mysql为例

事务隔离级别

.NET Core中的IDbConnection接口提供了BeginTransaction方法作为执行事务,BeginTransaction方法提供了两个重载,一个不需要参数BeginTransaction()默认事务隔离级别为RepeatableRead;另一个BeginTransaction(IsolationLevel il)可以根据业务需求来修改事务隔离级别。由于Dapper是对IDbConnection的扩展,所以Dapper在执行增删除改查时所有用到的事务需要由外部来定义。事务执行时与数据库之间的交互如下:

2017-12-19-22-43-23

从WireShark抓取的数据包来看程序和数据交互步骤依次是:建立连接-->设置数据库隔离级别-->告诉数据库一个事务开始-->执行数据增删查改-->提交事务-->断开连接

准备工作

准备数据库:Mysql (笔者这里是:MySql 5.7.20 社区版)

创建数据库并创建数据表,创建数据表的脚本如下:


CREATE TABLE `posts` (
  `Id` varchar(255) NOT NULL ,
  `Text` longtext NOT NULL,
  `CreationDate` datetime NOT NULL,
  `LastChangeDate` datetime NOT NULL,
  `Counter1` int(11) DEFAULT NULL,
  `Counter2` int(11) DEFAULT NULL,
  `Counter3` int(11) DEFAULT NULL,
  `Counter4` int(11) DEFAULT NULL,
  `Counter5` int(11) DEFAULT NULL,
  `Counter6` int(11) DEFAULT NULL,
  `Counter7` int(11) DEFAULT NULL,
  `Counter8` int(11) DEFAULT NULL,
  `Counter9` int(11) DEFAULT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

创建.NET Core Domain类:


[Table("Posts")]
public class Post
{
    [Key]
    public string Id { get; set; }
    public string Text { get; set; }
    public DateTime CreationDate { get; set; }
    public DateTime LastChangeDate { get; set; }
    public int? Counter1 { get; set; }
    public int? Counter2 { get; set; }
    public int? Counter3 { get; set; }
    public int? Counter4 { get; set; }
    public int? Counter5 { get; set; }
    public int? Counter6 { get; set; }
    public int? Counter7 { get; set; }
    public int? Counter8 { get; set; }
    public int? Counter9 { get; set; }

}

具体怎样使用Dapper,请看上篇

Read uncommitted 读未提交

允许脏读,即不发布共享锁,也不接受独占锁。意思是:事务A可以读取事务B未提交的数据。

优点:查询速度快

缺点:容易造成脏读,如果事务A在中途回滚

以下为执行脏读的测试代码片断:


public static void RunDirtyRead(IsolationLevel transaction1Level,IsolationLevel transaction2Level)
{
    var id = Guid.NewGuid().ToString();
    using (var connection1 = new MySqlConnection(connStr))
    {
        connection1.Open();
        Console.WriteLine("transaction1 {0} Start",transaction1Level);
        var transaction1 = connection1.BeginTransaction(transaction1Level);
        Console.WriteLine("transaction1 插入数据 Start");
        var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)";
        var detail1 = connection1.Execute(sql,
        new Post
        {
            Id = id,
            Text = Guid.NewGuid().ToString(),
            CreationDate = DateTime.Now,
            LastChangeDate = DateTime.Now
        },
            transaction1);
        Console.WriteLine("transaction1 插入End 返回受影响的行:{0}", detail1);
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2 {0} Start",transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            Console.WriteLine("transaction2 查询数据 Start");
            var result = connection2.QueryFirstOrDefault<Post>("select * from posts where id=@Id", new { id = id }, transaction2);
            //如果result为Null 则程序会报异常
            Console.WriteLine("transaction2 查询结事 返回结果:Id={0},Text={1}", result.Id, result.Text);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End",transaction2Level);
        }
        transaction1.Rollback();
        Console.WriteLine("transaction1 {0} Rollback ",transaction1Level);
    }

}

1、当执行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadUncommitted),即事务1和事务2都设置为ReadUncommitted时结果如下:

2017-12-22-22-06-49

当事务1回滚以后,数据库并没有事务1添加的数据,所以事务2获取的数据是脏数据。

2、当执行RunDirtyRead(IsolationLevel.Serializable,IsolationLevel.ReadUncommitted),即事务1隔离级别为Serializble,事务2的隔离级别设置为ReadUncommitted,结果如下:

2017-12-22-22-07-28

3、当执行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadCommitted);,即事务1隔离级别为ReadUncommitted,事务2的隔离级别为Readcommitted,结果如下:

2017-12-22-22-08-13

结论:当事务2(即取数据事务)隔离级别设置为ReadUncommitted,那么不管事务1隔离级别为哪一种,事务2都能将事务1未提交的数据得到;但是测试结果可以看出当事务2为ReadCommitted则获取不到事务1未提交的数据从而导致程序异常。

Read committed 读取提交内容

这是大多数数据库默认的隔离级别,但是,不是MySQL的默认隔离级别。读取数据时保持共享锁,以避免脏读,但是在事务结束前可以更改数据。

优点:解决了脏读的问题

缺点:一个事务未结束被另一个事务把数据修改后导致两次请求的数据不一致

测试重复读代码片断:


public static void RunRepeatableRead(IsolationLevel transaction1Level, IsolationLevel transaction2Level)
{
    using (var connection1 = new MySqlConnection(connStr))
    {
        connection1.Open();
        var id = "c8de065a-3c71-4273-9a12-98c8955a558d";
        Console.WriteLine("transaction1 {0} Start", transaction1Level);
        var transaction1 = connection1.BeginTransaction(transaction1Level);
        Console.WriteLine("transaction1 第一次查询开始");
        var sql = "select * from posts where id=@Id";
        var detail1 = connection1.QueryFirstOrDefault<Post>(sql, new { Id = id }, transaction1);
        Console.WriteLine("transaction1 第一次查询结束,结果:Id={0},Counter1={1}", detail1.Id, detail1.Counter1);
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2  {0} Start", transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            var updateCounter1=(detail1.Counter1 ?? 0) + 1;
            Console.WriteLine("transaction2  开始修改Id={0}中Counter1的值修改为:{1}", id,updateCounter1);
            var result = connection2.Execute(
                "update posts set Counter1=@Counter1 where id=@Id",
                new { Id = id, Counter1 = updateCounter1 },
                transaction2);
            Console.WriteLine("transaction2 修改完成 返回受影响行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
        Console.WriteLine("transaction1 第二次查询 Start");
        var detail2 = connection1.QueryFirstOrDefault<Post>(sql, new { Id = id }, transaction1);
        Console.WriteLine("transaction1 第二次查询 End 结果:Id={0},Counter1={1}", detail2.Id, detail2.Counter1);
        transaction1.Commit();
        Console.WriteLine("transaction1 {0} End", transaction1Level);
    }
}

在事务1中detail1中得到的Counter1为1,事务2中将Counter1的值修改为2,事务1中detail2得到的Counter1的值也会变为2

下面分几种情况来测试:

1、当事务1和事务2都为ReadCommitted时,结果如下:

2017-12-21-21-29-41

2017-12-22-22-08-38

2、当事务1和事务2隔离级别都为RepeatableRead时,执行结果如下:

2017-12-22-22-08-53

3、当事务1隔离级别为RepeatableRead,事务2隔离级别为ReadCommitted时执行结果如下:

2017-12-22-22-09-09

4、当事务1隔离级别为ReadCommitted,事务2隔离级别为RepeatableRead时执行结果如下:

2017-12-22-22-09-30

结论:当事务1隔离级别为ReadCommitted时数据可重复读,当事务1隔离级别为RepeatableRead时可以不可重复读,不管事务2隔离级别为哪一种不受影响。

注:在RepeatableRead隔离级别下虽然事务1两次获取的数据一致,但是事务2已经是将数据库中的数据进行了修改,如果事务1对该条数据进行修改则会对事务2的数据进行覆盖。

Repeatable read (可重读)

这是MySQL默认的隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行(目标数据行不会被修改)。

优点:解决了不可重复读和脏读问题

缺点:幻读

测试幻读代码

 
public static void RunPhantomRead(IsolationLevel transaction1Level, IsolationLevel transaction2Level)
{
    using (var connection1 = new MySqlConnection(connStr))
    {
        connection1.Open();
        Console.WriteLine("transaction1 {0} Start", transaction1Level);
        var transaction1 = connection1.BeginTransaction(transaction1Level);
        Console.WriteLine("transaction1 第一次查询数据库 Start");
        var detail1 = connection1.Query<Post>("select * from posts").ToList();
        Console.WriteLine("transaction1 第一次查询数据库 End 查询条数:{0}", detail1.Count);
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2 {0} Start", transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            Console.WriteLine("transaction2 执行插入数据 Start");
            var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)";
            var entity = new Post
            {
                Id = Guid.NewGuid().ToString(),
                Text = Guid.NewGuid().ToString(),
                CreationDate = DateTime.Now,
                LastChangeDate = DateTime.Now
            };
            var result = connection2.Execute(sql, entity, transaction2);
            Console.WriteLine("transaction2 执行插入数据 End 返回受影响行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
        Console.WriteLine("transaction1 第二次查询数据库 Start");
        var detail2 = connection1.Query<Post>("select * from posts").ToList();
        Console.WriteLine("transaction1 第二次查询数据库 End 查询条数:{0}", detail2.Count);
        transaction1.Commit();
        Console.WriteLine("transaction1 {0} End", transaction1Level);
    }
}

分别对几种情况进行测试:

1、事务1和事务2隔离级别都为RepeatableRead,结果如下:

2017-12-22-22-09-46

2、事务1和事务2隔离级别都为Serializable,结果如下:

2017-12-22-22-10-02

3、当事务1的隔离级别为Serializable,事务2的隔离级别为RepeatableRead时,执行结果如下:

2017-12-22-22-10-18

4、当事务1的隔离级别为RepeatableRead,事务2的隔离级别为Serializable时,执行结果如下:

2017-12-22-22-10-32

结论:当事务隔离级别为RepeatableRead时虽然两次获取数据条数相同,但是事务2是正常将数据插入到数据库当中的。当事务1隔离级别为Serializable程序异常,原因接下来将会讲到。

Serializable 序列化

这是最高的事务隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。

优点:解决幻读

缺点:在每个读的数据行上都加了共享锁,可能导致大量的超时和锁竞争

当执行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.Serializable)或执行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.RepeatableRead)时代码都会报异常,是因为Serializable隔离级别下强制事务以串行方式执行,由于这里是一个主线程上第一个事务未完时执行了第二个事务,但是第二个事务必须等到第一个事务执行完成后才参执行,所以就会导致程序报超时异常。这里将代码作如下修改:


using (var connection1 = new MySqlConnection(connStr))
{
    connection1.Open();
    Console.WriteLine("transaction1 {0} Start", transaction1Level);
    var transaction1 = connection1.BeginTransaction(transaction1Level);
    Console.WriteLine("transaction1 第一次查询数据库 Start");
    var detail1 = connection1.Query<Post>("select * from posts").ToList();
    Console.WriteLine("transaction1 第一次查询数据库 End 查询条数:{0}", detail1.Count);
    Thread thread = new Thread(new ThreadStart(() =>
    {
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2 {0} Start", transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            Console.WriteLine("transaction2 执行插入数据 Start");
            var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)";
            var entity = new Post
            {
                Id = Guid.NewGuid().ToString(),
                Text = Guid.NewGuid().ToString(),
                CreationDate = DateTime.Now,
                LastChangeDate = DateTime.Now
            };
            var result = connection2.Execute(sql, entity, transaction2);
            Console.WriteLine("transaction2 执行插入数据 End 返回受影响行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
    }));
    thread.Start();
    //为了证明两个事务是串行执行的,这里让主线程睡5秒
    Thread.Sleep(5000);
    Console.WriteLine("transaction1 第二次查询数据库 Start");
    var detail2 = connection1.Query<Post>("select * from posts").ToList();
    Console.WriteLine("transaction1 第二次查询数据库 End 查询条数:{0}", detail2.Count);
    transaction1.Commit();
    Console.WriteLine("transaction1 {0} End", transaction1Level);
}

执行结果如下:

2017-12-22-22-11-02

2017-12-22-22-11-13

结论:当事务1隔离级别为Serializable时对后面的事务的增删改改操作进行强制排序。避免数据出错造成不必要的麻烦。

注:在.NET Core中IsolationLevel枚举值中还提供了另外三种隔离级别:ChaosSnapshotUnspecified由于这种事务隔离级别MySql不支持设置时会报异常:

2017-12-20-21-31-13

总结

本节通过Dapper对MySql中事务的四种隔离级别下进行测试,并且指出事务之间的相互关系和问题以供大家参考。

1、事务1隔离级别为ReadUncommitted时,可以读取其它任何事务隔离级别下未提交的数据

2、事务1隔离级别为ReadCommitted时,不可以读取其它事务未提交的数据,但是允许其它事务对数据表进行查询、添加、修改和删除;并且可以将其它事务增删改重新获取出来。

3、事务1隔离级别为RepeatableRead时,不可以读取其它事务未提交的数据,但是允许其它事务对数据表进行查询、添加、修改和删除;但是其它事务的增删改不影响事务1的查询结果

4、事务1隔离级别为Serializable时,对其它事务对数据库的修改(增删改)强制串行处理。

脏读 重复读 幻读
Read uncommitted
Read committed 不会
Repeatable read 不会 不会
Serializable 不会 不会 不会

作者:xdpie 出处:http://www.cnblogs.com/vipyoumay/p/8134434.html

以上内容有任何错误或不准确的地方请大家指正,不喜勿喷! 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧。