我写这篇文章不是因为我已经找到了提高孩子专注力的方法,而是分享一些我的做法,想直接找到方法的读者可以路过了。 ——笔者
是什么时候开始发现我的孩子有些不专注的呢?是刚上幼儿园的时候。原本我们根本不会想到这个问题,平常就是陪她玩,讲讲故事什么的。直到上了幼儿园,老师跟我反应,孩子在上课的时候,只要有一点风吹草动,注意力立刻会被吸引过去。起先我们还不是很在意,单纯的以为只是孩子小而已,后来,在课外的绘画培训课上,我发现别的孩子多数可以集中精神做手上的事情,而不是像我的孩子很容易被小事吸引。
是什么原因会导致孩子不够专注呢?只是因为孩子小么?我还专门去查了一些文章,有的提到,人类的认知是随着年龄的增长在发生变化的。比如,一个抽屉打开,有很多东西,成年人会直接注意到自己想要的剪刀,然后拿走,至于抽屉里还有什么,可能不会在意;而孩子会注意到抽屉里的每个东西,至于自己想要的是什么可能会突然忘记,过一会再想起来,最后找到剪刀拿走。造成这个差异的就是专注力,成年人会专注在自己的事情上,但是孩子的专注力往往不足。如果是这样的话,随着孩子的长大,专注力就会逐渐加强,似乎不是问题。
但是这还是无法解释,为什么同龄的孩子会有比我的孩子更好的专注力。或许,是因为我的孩子比较小,孩子是8月份生日,9月份入学时刚刚满3周岁,大部分幼儿园同班的孩子都比我们的孩子要大,课外绘画培训课上的孩子也要比我的孩子大半年。只是差半年而已,会有如此大的差异么?可能是。
于是我又找了与孩子的专注力培养有关的文章,有的提到,孩子的专注力其实是有差异的,正常情况下,专注力主要是随自然成长而增强,一般不用刻意培养。但是有一些做法会降低孩子的专注力,比如,孩子正在专注做某件事情的时候,意外打断他,当然这可能不是故意的。孩子正在认真的看书,爷爷说,要不要吃苹果啊,孩子就被喊去吃苹果了,专注力就被打断了。时间长了,专注力就越来越不足。我估计,我的孩子就是这样的情况。我们家里特别讲究准时吃东西,几点喝牛奶,几点吃水果,几点出去玩都是定好时间的,到点就会提醒,不时的还会检查一下孩子是不是出汗了,要不要换衣服,这样反复的打断可能会造成孩子注意力不集中。
专注力不足对孩子有什么影响呢?最突出的影响就是,孩子对接受新事物和面对难一些的问题会有明显的抵触情绪,对一些需要重复工作的事情显得很不耐烦,这对孩子的成长是极为不利的,而这,正是发生在我孩子身上的问题。比如,搭积木,她只会选择一些比较简单形状的去搭,或者搭一些我教过她的,而不会去尝试新的形状;再比如,她可以为了找一本书,把书翻乱,但是找到书之后,却很反感把翻乱的书放回原位。类比到成人身上的话,就好比是重度网络快餐文化的依赖者,可以看一些短一些的微信文章,但是反感看完整本书;对一些新事物有抵触,不会去学习;碰到无法短时间解决的问题,往往选择放弃。如果不加以约束的话,我害怕我的孩子长大后会变成这样的大人。
网上一个测试和训练专注力的方法是舒尔特方格。在一个随机的5x5,总共25个格子的正方形里面随机填入1~25,测试者按顺序依次指出其位置,同时诵读出声,用时越短成绩越好。这个测试还有简单的版本,3x3、4x4个格子,标准测试是5x5的格子。成年人的成绩一般是1秒一格,也就是25秒左右。我自己测试了一下,先预热几盘,然后开始计时,第一次大约在35秒左右,再多训练几次就可以进入25秒。我也给孩子做了个测试,分几天,每天做2次,多数情况下,测试时间在10~15分钟,最快一次用了5分钟,可见,孩子的专注力还是非常不稳定的。如果孩子有意识的控制,可以很快完成任务,否则往往要花上几倍的时间,这可能是孩子注意力的特点。
当然,使用舒尔特方格并不是一个特别好的方法,因为非常枯燥,不要说小孩,成年人能坚持练习的都很少。网上有提到一个方法是倒背数,就是给孩子一组数,让他倒背出来,比如,你说396,他要说693。我们试过这样的方法,次数一多,孩子就厌烦了,毕竟数字是很枯燥的,这个方法似乎比舒尔特方格也没有好太多。于是我们就换了个思路,用卡片代替数字,比如,“猴子、苹果、鸡蛋”,她要回答“鸡蛋、苹果、猴子”,似乎比用数字的兴趣要大一些,我们刚开始用这个方法,不知道能坚持多久。
孩子的专注力是非常重要的,对他未来的人生、学业、事业都是基本中的基本,这也是我如此看中这一点的原因。如果有哪位有比较好的提高专注力的方法,也可以来与我交流。
]]>我写这篇文章不是因为我已经找到了提高孩子专注力的方法,而是分享一些我的做法,想直接找到方法的读者可以路过了。 ——笔者
是什么时候开始发现我的孩子有些不专注的呢?是刚上幼儿园的时候。原本我们根本不会想到这个]]>
本文写于2016年09月25日,当时对文章内容很不自信,所以延迟到很晚才发。 —— valleylord
2016年1月11日,广州,“微信之父”张小龙首次公开演讲,宣布微信公众号将推出“应用号”。
2016年9月21日晚间微信官方向部分公众号发出了应用号的内测邀请,2016年9月22日凌晨微信正式对外声明已经开始内测。
为什么在订阅号、服务号、企业号之后,仍然要开发“小程序”?
from 张小龙: 我们希望存在一种新的公众号形态,这种形态下面用户关注了一个公众号,就像安装了一个APP一样。
什么是小程序?
from 张小龙 的微信朋友圈: 小程序是一种不需要下载安装即可使用的应用,它实现了应用“触手可及”的梦想,用户扫一扫或者搜一下即可打开应用。也体现了“用完即走”的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载。
笔者动笔写下这篇文章的时间是2016年9月25日,宣布“小程序”内侧后1周之内,以上这些内容,搜索引擎都搜的到。
现在的 APP 都是通过应用市场进行分发,技术上使用原生应用模式开发。对比“小程序”的话,“小程序”相对封闭,通过微信分发,技术上使用 HTML5 模式开发(即不需要安装)。原生 APP 从 Android 系统桌面进入,“小程序”从微信的“发现->小程序”中进入,便捷程度上差很多。不可否认,原生 APP 的用户体验相比 HTML5 应用来说,还是好很多。原生 APP 需要从应用市场作为入口,先安装再使用,“小程序”可以做到即扫即用,不需要安装。
几个回合的比较下来,除了不需要安装这一点略胜一筹,“小程序”几乎完败。一群鼓吹“小程序”可以颠覆现有 APP 格局的所谓互联网分析师们可以醒醒了。
好吧,为了击碎“小程序改变 APP 格局”的幻想,我们来举个栗子。以京东商城 APP 来看,京东和腾讯达成合作,最终在2014年5月上线了微信购物,从“发现->购物”进入,可以进入京东购物,实际上就是相当于京东商城的 HTML5 版本。从技术实现和用户体验上来说,“京东购物”和“小程序”如出一辙,如果大家对“小程序”的用户体验什么的还有期待的话,可以多看看这个应用,最多也就是做到和“京东购物”差不多的水准。而且,京东购物的进入路径比小程序更加便捷。而结果呢??“我们还有非常大的空间,有90%流量资源可以开发。——刘强东”,换句话说,京东利用腾讯的流量最多只有10%1,这是京东购物上线1年半左右的一个报道。
目前,京东没有公开来自微信入口的下单量数据,我们也无从猜测。从公开的数据来看,2015年第四季度,移动端下单来量占比61.4%,2016年第一、二季度,移动端的下单量分别占比72.4%、79.3%,同时,腾讯入口带给京东的流量支持也在减缓2。同时,官方数据称,当入口的级别提升一层的时候,“交易量提升8倍”3。因此,如果京东购物的级别降低到“小程序”那个级别的时候,假定以上现象可以重复,交易量很可能会降低到1/8,这是一个粗略的估计。我们假定交易量的增长比例与用户增长比例相同,移动端用户量比例和移动端下单量比例等比增长,进行后续分析。
以京东财报来看,2015年第四季度,活跃用户有1.55亿,根据移动下单量61.4%(粗略等于移动端用户量比例),京东有“1.55亿x61.4%=9500万”移动用户量,而“手机QQ+微信”分别各拥有6亿活跃用户,按刘强东的说法,最多只有10%的用户流量,也就是1.2亿用户使用了京东购物,这个数字远大于9500万,看来刘强东还是吹了一些牛的。
不过,我们可以从一些文章中粗略估计出微信购物下单量的上线,以2014年“618”的战绩来看,移动端中,“手机QQ+微信”的贡献量最多在7%,移动端全部占比25%4,实际上,考虑到京东 APP 的增长,实际的“手机QQ+微信”的贡献量可能在5%(全平台),甚至不足,移动端下单量的比例大约是4:1(京东 APP : “手机QQ+微信”)。再看到2015年第四季度,京东新增用户中有1/3来自微信购物和手机QQ购物5,这里的用语有非常细微的差异,“新增用户”占比52%,并非“新增下单量”占比52%。可见“手机QQ+微信”作为入口,带来的很多的新用户,也许这才是京东和腾讯合作的目的。
下面的数字纯属笔者的猜测,京东 APP 的用户基数比较大,但是增长缓慢;“手机QQ+微信”京东入口的用户比较少,但是增长迅速,至今有多少用户,多少交易是使用“手机QQ+微信”作为入口的,也没有准确数字,假定“手机QQ+微信”已经超过京东 APP,比例是1:2(京东 APP :“手机QQ+微信”),这是一个最大化“手机QQ+微信”用户数的假设。在根据之前的9500万用户数,可以大致计算出,“手机QQ+微信”用户数大约有6000万,占所有用户的2/5。再假设,微信是“手机QQ+微信”流量的主导流量,其中7成流量来自微信,也就是4200万,这是上限假设,也是微信对京东目前能有的最大影响。
假设4200万的假设合理,京东有接近1/3的用户,或者说成交量来自微信,粗看这是一个非常客观的数字,但是别忘了,还有8倍的路径便捷效应。假设微信把京东放回到原先的位置,路径复杂度与“小程序”相当,4200万的用户会降低到1/8,也就是540万,总用户降低到1.18亿,微信用户占比为5%,而且这还是最好估计。可以说,京东购物是“小程序”的前期试水,也让微信在做“小程序”的时候找准方向。
因此,“小程序”对大互联网应用的影响是微乎其微的,比如滴滴、百度地图、墨迹天气、腾讯音乐等等,费劲心思去抢占所谓的微信入口,也就是能拿到5%不到的用户,不划算。所以,我认为,“小程序”对 APP 的格局影响是微乎其微的,有了“小程序”之后,你手机中的常用 APP 数量不会减少。
一句话,低频且重要,尤其一些定制化很强的场景需要“小程序”。“定制化很强”从字面上比较好理解,“低频且重要”作何解释呢?
一般的场景,重要会与高频联系在一起,之前举例的滴滴、百度地图、墨迹天气都是这类的例子;也有一些场景,高频且不重要,多数是一些体育娱乐产业 APP 相关的场景;低频且重要的场景也很多,比如猎聘、携程、京东众筹、代驾类;低频且不重要的场景主要是一些小众、专业的应用6。
上面的举例中,低频且重要的场景,都是通用场景,虽然使用频率很低,但是具有通用性,流程固定,不需要定制化。那么,“低频+重要+强定制化”是什么场景呢?比如,某XX广场的商圈,提供定制化服务“小程序”,客户可以根据店名找到这个店在商圈中的具体位置,比如在几楼,加入导航,怎么走过去;如果该店是一个餐馆,还可以进入该店的定制化“小程序”查看当前排队情况,顺便拿号排队,甚至可以提前查看该餐馆的定制化菜单,先点好菜。再比如,政府的在线服务窗口,出入境管理处办护照的流程,和民政局领结婚证的流程,肯定是不一样的,这都是定制化“小程序”的应用场景。
那么来说说,为什么这样的场景是低频的。首先,一般不会有人为了去买个东西、吃个饭特地去安装一个 APP,在用户看来,我去这个商场买东西、吃饭的概率是很低的,怕麻烦,不想装。而实际上,一般如果你觉得一个商圈运营的很好,你会多次的去那里消费,一年去个10次实际上就不能算是低频了(对一个消费场所而言),但在 APP 的使用来看,一年用10次的 APP 可能就是个超低频。
再来说说,为什么这样的场景是重要的。还是以商圈举例,假设有这样一个商圈,你1年会去5次,每次在那边花费100元,总共就是500元/年,这个数字对于一个稍有人气的商圈而言是明显偏低的。因此,如果有这样的“小程序”,绝对击败95%的应用,不信看看你手机里的应用,年交易流水超过500元的有多少?就算是餐馆的“小程序”,你一年去吃一次,一次200元,按交易额也依然能轻松打败你手机里面绝大部分应用。
最后来说一下定制化,不同商圈希望给客户带来不同的用户体验,不同的商圈地图、不同的商户介绍和图片等等;不同的餐馆会希望有不同的菜单呈现方式,是否允许在线排队、在线点菜,是否可以选特定位置的座位等等。这些都是定制化需求,能否统一这些需求?我觉得很难,几乎不可能。
这篇文章发布与2017年6月10日,这段话是发布这一天加的。从现在小程序的发展来看,“低频且重要”这个特性还是主流,增加了按地理位置搜索小程序功能,说明商圈式应用是有可能的,虽然还没有出现文中说的商圈定制化小程序(可能已经有了,我不知道而已)。再等等,看看吧。
]]>本文写于2016年09月25日,当时对文章内容很不自信,所以延迟到很晚才发。 —— valleylord
2016年1月11日,广州,“微信之父”张小龙首次公开演讲,宣布微信公众]]>
这节课触动了我很多,后来我又去看了他们的宣传博客,这是一家专门针对儿童的英语培训机构。博客中提到了他们的教学理念,教学目标,教学方式,教学内容,以及一些课堂短视频。他们的教学方法完全颠覆了我十几年的英语学习经验。
我们需要学的是什么样的英语呢?或者说,我们学了英语,想达到一个什么样的使用英语的状态呢?能用英语和英美人士自由交流,这是基本,就像我们用汉语交流一样,可以谈论电影、经济、体育,不会感觉到词汇的贫乏,词不达意。能用英语写一些日常的文章,比如工作邮件,可以准确表达我们的思想,不会让对方误解,甚至看不懂。如果能更进一步,用英语做一场流畅的演讲,中间穿插一些地道的笑话段子,用一些谐音和双关,那就更完美了。而实际上呢,我们的英语能用来说你好,能用来讨论一些专业领域的问题,但是,可能不能用来问路,不知道表达厕所到底是用toilet、bathroom、还是restroom,句子中有很多的a、the。至于谈到电影明星、经济政策、体育赛事,那就是噩梦了,几乎无法理解。我们接受了10多年的传统英语教学,为什么仍然如空中楼阁一般?
我们传统的英语教学是怎样的呢?首先,重视读写,听说其次。为什么这么说,传统教学是应试教育,考试主要考读写,听的比例较少,说的比例是零。因此在教学的时候,每一位老师都会把绝大部分的精力放在读写上,导致虽然我们会了一些单词,但是见到英美人士的时候,仍然不敢交流,不敢表达,我们对自己的英语发音潜意识里面是自卑的。其次,传统教学的过于强调语法,而忽略了英语自然表达的训练。就说汉语,我们说话的时候会注意语法的主谓宾分别是啥么?为什么我们看到一个英语句子,就会想到过去时、虚拟语态这些东西呢?过于强调以语法为基础的语言框架,那是考试英语,已经背离了语言的实质。再次,英语表重于英语文化,这一直是我对传统英语教学很疑惑的一个地方,为什么,我们学习汉语的时候,有童话故事,有小说,有笑话,有古诗词,但我们学英语的时候,为什么只剩下了日常对话、阅读理解、写作等等这些呢?看到的文章也是只有日常对话、论述文、说明文这类书面文章。这类死板的文字,不仅打击了孩子学英语的兴趣,也推波助澜形成了中式英语。最后,可能是根本原因,我们的英语教材都是中国人编写的,无论此人英语水平如何,总不可能高过英语为母语的人写出来的教材,我们的考试方式也是中国式的,考题也是中国人出的,如何能考察出一个人真正的英语水平?
在这样的英语教学体系下,我们的英语只能是中式英语,只有同样学过中式英语的中国人才能领会你在说什么。这是我们学习的英语,我们无从选择。如果我们的孩子可以选择,我们会想教给我们的孩子怎样的英语呢?如果要真正掌握英语,达到或者说无限接近母语水平的英语,就必须突破传统教学中的劣势,而这正是我在这家培训机构看到的可贵实践。
首先,侧重听说、读写其次。我不是说读写不重要,而是说,相比读写,听说才是根本的。因为,学汉语你也是先会听说,才有读的,写是最后才学会的,为什么我们不能这样来学英语呢?听说重于读写的一个原因是,听说更加强调锻炼语感,什么是语感,就是一种语言用多了之后,产生的一种自然的“肌肉反应”,有一种不经过大脑逻辑判断就可以说出正确语言的感觉,其实这跟学习很多技能是一样的,比如开车、游泳等。因为听说是需要即时反馈的,这迫使学习者将所谓的语感强行记在肌肉中,而不是根据语法通过“因为…所以…”的逻辑方式予以回答,这是非常慢的。而读写不需要这种迅速反馈,因此,强调读写就会导致出现“通过语法经过逻辑判断得出正确答案”的中式英语。
还有,听说的训练是不能间断的,这是“听说锻炼语感”方法的一个自然结论。因为语感是一种肌肉记忆,长时间不用,这种肌肉记忆就会衰退,正如长时间不开车就不会开了,长时间不游泳就不会游了,都是一个道理。一旦有英语语感,听说自然不是问题,读写也仅仅只是听说的文字化而已。
那么如何训练听说呢?听说学的好,必须要有英语母语的人来教发音。有人说,听MP3、听磁带也可以,这完全是自欺欺人的。首先,听音频资料,声音无法很好的还原,失真是无法避免的,我们根本无法确认真正的发音究竟是怎样;还有更重要的,听音频无法看到老师的口型,我们不但要学发出什么样的声音才是对的,也要学如何发出对的声音,你不告诉他如何发音,难道让他自我摸索么?因此,英语母语的老师几乎是所有教学的起点,一个基本的要求。
顺带一说,英语母语的老师和欧美相貌的老师是两回事。我们的母语不是英语,很多时候不太能区分什么样的英语是母语级别的,只能看外教老师的样子,一个白人相貌的老师很有优势。这是一个误区,欧美相貌不代表英语母语,因为他们可能是俄语、法语、德语或其他语种。相反,即使一个老师是亚洲人、非洲人的相貌,但是他/她确实出生长大在美国,从小说英语,那么,至少在英语母语的要求方面,这是一位合格的老师。
以上强调的是听说,其次是,实例重于语法。再次强调,语言不是逻辑,不需要很多“因为…所以…”的推断,反复的多情景的练习远胜于通过语法得到的逻辑正确。当然,语法也很重要,学语法的时候,应该强调,这个语法一般有哪些常用的句型,这些句型表达一个什么样的含义,然后在反复的句型练习中,让孩子理解这个句型,以及背后的语法的含义。就算最后语法没有学会,又有什么关系呢,我们说了这么久的汉语,大部分的汉语语法我们也是不知道的。语言是实用技能,不能按理论教学的那一套来。
实例练习需要掌握到什么程度呢?我觉得,一个实例,反复的练习各种场景,直到理解什么情况下使用这个实例可以表达自己的真实想法。这个跟之前说的“肌肉反应”是同样的,在这个过程中,听说是基础,也是必须要跨过的关。掌握了很多实例之后,在某些时刻会发现,原来这些例句背后的语法是一致的,这样的语法教学才能算是成功的。这样的语法不是逻辑推理,而是来自实践的总结,是鲜活实用的语法。
再次,文化重于表达。英语不是只用来说说Hello、Goodbye,写写邮件、新闻稿的官样语言,而是和汉语一样鲜活的文字,有历史,有文化。尝试理解使用英语的历史和文化,可以很好的激起孩子学英语的兴趣,毕竟孩子都会喜欢听故事,另外,增加阅读量本身就是对英语学习的巨大帮助。有了这些词汇,在日常交流中碰到,能加快反应速度,也是口语练习、语感联系所必须的。
在学习英语历史文化的过程中,一定会出现一些不常见的词汇,比如各种动植物名称、历史名词、童话人物等待。可能这些词汇的拼写比较难,但是记住这些词汇并不是很难,因为这些词汇多数只有一个含义,在这方面,我们往往低估了孩子的能力。当然,老师教的方法也很重要,比如,在旁听课程中碰到werewolf这个词,我不认识,但是,这个词和witch、skeleton、zombie同时出现,再看看词根,就能猜到应该是“狼人”的意思,这是一组万圣节词汇。
另外,正常的英语体系中,名词的占比是很高的,任何一种有较长历史的语言都是如此。而传统英语的教学是畸形的,我们学了很多“实用”的英语,认为历史文化中的词汇是不重要的,这种南辕北辙的做法,其实并没有让我们掌握真正实用的英语。
最后,一套侧重听说、实例、英美文化的教材。这套教材,必须是英语母语的专业人士编写,他们的思维更加侧重在用英语的思维进行教学,而不是每个词语都用中文进行对照。中英文对照的教学方式,会让孩子建立起每次见到单词都潜意识要翻译的习惯,降低了“肌肉反应”的速度。用英语的思维教学,是说要按英语的方式来理解单词用法,然后反复训练,直到建立起“肌肉反应”。说白了,这跟我们学习汉语是一样的。
只有教材仍然不足够,还需要有一套与之适应的考试体系,现在的各种英语级别考试太多,大人的小孩的都有,如果只是为了让孩子考个英语级别的考试,必然会出现“偏科”的情况,那不是英语学习的正路。如果要设置一个真正能测试英语级别的考试,我觉得只有一项要考,就是看无障碍与英语母语人士交流到什么程度。当然这个不太现实,我想说的是,英语级别考试仅仅是英语学习的一个方面,切莫为了考试而学。
说了这么多,只是说了什么样的英语教学是好的,那么,如果有这样的教学环境,我们该怎样教我们的孩子呢?
一是,充分利用外教资源。之前说过,外教对于听说练习是非常重要的,但是,我们往往没有合理的用好这个资源。首先,上课时要认真听,这几乎废话,但是对一个还没有完全自制力的学龄前儿童来说,这很重要。要让整个课堂的秩序予以维持,需要家长在课堂里维护秩序,使得孩子能集中精力,提高学习效率。还有,千万不要预习。一个普遍的经验是,第一次的教学是最重要的,如果有偏差,那么后续会花更多的时间来纠正。无论你个人的英语能达到何种程度,总不会好过英语母语的老师,预习可能会将孩子的英语发音有一个不好的开始,因此,让孩子第一次听到对的发音,对英语学习非常重要。
二是,反复练习,或者是复习。正如前面反复提到,英语学习是一种技能学习,其目的是建立起一种“肌肉反应”,为了建立起这种近乎直觉的反馈,必须有大量的训练,这就是复习的价值。家长要带着孩子,反复的练习课堂学到的内容,练习每一个单词和句型,直到学会,会用。整个过程中,辛苦的不仅是孩子,也有家长,切记,梅花香自苦寒来。Practice,这也是我在他们的一篇博客中,看到的最多的单词。
三是,严格要求孩子。所谓的严格要求,是相对于快乐学习、兴趣学习来说的。因为作为孩子,他根本不知道自己需要什么,他只会知道,他喜欢什么。孩子最喜欢什么,就是玩,而真正的学好一个本领,不可能一直都开开心心的。抱着玩的心态,是永远无法掌握一门技能的,作为家长,要做好孩子可能会哭会闹的心理准备,也让孩子知道获得一门技能是要付出艰辛努力的。
四是,坚持。如很多人所熟知的10000小时定律,成为一个技能的专家,需要10000小时的训练。这10000小时,必定充满了很多困难、挫折、反复,孩子会畏惧困难,但是作为家长,要不断的引导,陪孩子度过每一个难关,一直坚持,直到10000小时的临界点。因此,作为家长,要做好面对困难的打算,不能因为孩子的一时任性,就中断了学习。一旦中断,可能就捡不回来了。
对于那些总是带着孩子一边学一边玩的培训机构,无论是不是外教,还是不要去了。我就曾去过一个这样的培训机构去试听,全场就觉得孩子在期待老师下面会拿什么好玩的出来,整个课堂孩子蹦来蹦去,秩序几乎无法维持,虽然孩子偶尔学了几个单词,但是全程下来能学到什么也很难说。至少,我没有在这个课堂上看到一些英语比较好的学生,与这一次试听课堂上的学生相比,几乎是天差地别。学习的过程不是一个有趣的过程,至少不是一个一直有趣的过程,想要以一种一直有趣的方式来教学,最后可能什么也没有学到。
以上是我对我们需要一个什么样的英语教学环境,以及如何教我们的孩子学英语的一些想法,其中的很多内容整理自这家培训机构的宣传博客。所幸,他们有与我们的要求相当一致的教学理念和教学环境,我从他们的博客中摘抄一些原话,如下,
正如他们所说,“坚持我们的课程两年三年以上的孩子英语都非常优秀”,而我确实在他们的课堂上见到了这样的孩子,我庆幸我找到了这样一位教学实践者。我没有写他们机构的名称,知道的人肯定知道我在说谁。谢谢你们。
]]>如果仔细去搜索一些文章,就可以发现这些文章的模式,大多是这个路数:
从去中心化原理开始,分为两个流派,技术流和金融流。技术流的路数大概是这样的,
金融流的路数就玄幻很多了,比如这样的,
由于这类文章太多,我不一一引用了,至于一些东拼西凑来的粗略介绍的九流文章,更是数不胜数。在这个言必称颠覆的时代,我们似乎找到了一个万灵药。看到这些,第一反应是,牛!第二反应是,不太对劲;第三反应是,为啥都是优点?难道区块链没有缺点么?
理性的思考是,一个东西的引入,带来了某些方面的巨大进步,同时要知道其局限性。关于区块链的缺点,也可以搜索到相关的文章,但大多是在一味吹捧之余,将区块链劣势的部分一笔带过。区块链的局限性到底是什么呢?下面是我个人的分析,读之前最好先了解一些区块链相关的背景知识。
在区块链的分布式交易系统中,当一个区块获得交易权之后,需要广播给所有区块,直到大部分区块都接受了你的请求,并把你的交易请求记录在他的账本里面,你的交易才被认可,并不可修改。如果有两个区块想交易同一个东西,怎么办,那就看谁的交易请求扩散到多数的区块,最终多数派获胜。看到这里,我的一个直觉分析是,这得花多久才能确定交易完成啊?假设在交易系统中,有9个区块,那么至少要5个区块接受请求,才能判定为交易确认,如果每个账户一个区块的话,那么类似淘宝京东这种国民系统,就会有10亿级别的区块,需要至少5亿个区块接受请求。你tm在逗我么?就算我是买房子,需要那么多人确认么?系统的交易速度究竟要慢到什么程度?
还有,如果要篡改一笔交易,需要至少51%的区块认可,才可以修改。说白了,还是无法防止篡改(没什么系统可以防止篡改,只有人可以)。举个例子,如果某人要改一笔交易,他可以窜通区块链交易系统的公司在公司51%的区块服务器上修改,只是改的手段麻烦一点,这与现在的中心化交易系统又有和区别?如果说,区块链交易系统只提供服务程序(类似比特币的做法),不提供区块服务器,区块服务器由大家自己搭建,我就呵呵了,这是什么商业模式?怎么赚钱?往区块服务器终端上推送广告么?回到之前的那个问题,假设建立了1万个区块服务器,每次交易要至少5001个区块接受,交易速度怎么保证?别说淘宝京东这种交易量,就是全国房产交易中心的交易量都不一定能承受得了。
再退一步说,生活中有没有一些交易,交易量比较小,交易额比较大,这类交易可以借助区块链?这样的交易确实有,刚才提到的房产就是一个例子,还有私募股权、艺术品、古董等等,诸如此类,金融学里面对它们有统一的分类,叫另类投资品(Alternative Investments)。所以,如果没有看到这些领域里面有比较成功的区块链应用,估计其他地方能看到的可能性也极低了。
假设上面这些领域中,真的可以建立一个区块链交易系统,用户可以接受巨慢的交易速度,那么,这也注定该系统的用户不会很多,所谓的承载“人与人之间的信任”的重任,又从何谈起呢?只是这么一小搓人么?只是在这几个犄角旮旯的方面么?
促使我写下以上分析的是知乎上 Hold the door!!!!
知友的回答1,我觉得,他的回答也是切中要害。我全文引用如下,
Hold the door!!!!
市面上乱七八糟的书也够多了,我想说所有的书都是垃圾,没有一本例外。
要搞懂区块链,第一要看的当然是satoshi的论文,第二要看的是btc的源代码,wiki上有详细的协议分析,结合代码你就很清楚了。看完这些你再去看最早提出区块链的mastercoin,以及为什么这玩意就是扯淡。还有个扯淡的colorcoin,扯淡的原因和mastercoin一样。btc的proof of work机制为什么如此难以取代,为什么ripple被认为没有实现去中心化一致,为什么ether提出到现在快2年了还如此难产?它首创的“smart contract”基于btc里面的什么机制? 弄懂了这些你不妨去btctalk上面看看各种币的白皮书,以及pos机制。
区块链到底能用在哪里,现在90%的应用也是扯淡。什么提高交易速度咯,降低交易成本咯,全是扯淡。为什么?因为区块链本质上是通过牺牲速度和IT资源来换取公平性。除了这个,区块链的所有任何其他feature均被传统技术完爆。它的最大优势就是这个,它可以让你在匿名状态下完成公平交易,这是用来干嘛的?去翻翻刑法就懂,我不多说了。当然你要说他没有正经应用我也是不同意的。最后我总结下不懂技术的吹逼和不懂商业的装逼所吹嘘的区块链应用的致命问题在哪里:
1:开放性。对传统技术部署的交易或者支付应用来说,开放也很简单,问题是其组织的开放意愿。开放本身在技术上根本不构成门槛,写过代码的都懂。
2:低成本:区块链最大的泡沫,它的运营总成本远远高于传统系统,因为每个节点都要保存账簿链,而且chain的组织方式让账本规模变得无比巨大,还使得大量传统的吞吐技术无法支持。更不要说挖矿的存在了。
3:高速度:一个智商筛选器,相信这个的,您回去复读小学吧。10分钟一个快,一笔交易要至少50分钟确认这是btc的协议的规定,而proof of work机制要求必须给出挖矿(就是work)时间,所以它根本没有快起来的理论依据。0确认机制是要冒对手风险的。至于ripple所用的consensus,可以实现几秒钟完成交易,我就告诉你这个consensus就是google的levelDB内部实现的数据库多地一致性,别被人家概念忽悠瘸了,ripple就是个披着区块链外衣的传统系统。
4:智能:包括什么智能合约之类的。传统系统实现各类业务需求只需要更新一个版本,区块链呢?需要全网投票,你说哪个简单?而且传统系统是改代码,区块链可是要改协议,完全不是一个量级的难度。
blockchain真正的特别之处在于:
1:匿名:完全无法追踪的匿名,利用btc的MofN多签名机制可以实现从理论上无法追踪的资金转移。
2:财产安全性:非对称加密技术保证了每个人都不会被冻结财产。
中本聪是个反政府主义者,这是他发明btc的初衷。
该回答中提到了一些问题,按我自己的思路总结如下(部分与我之前的观点相近,但更加犀利、直戳要害),
现在,关于区块链的文章里面,充斥了各种虚幻,各种空中楼阁,各家公司也是各种扯概念,各种挂羊头卖狗肉,而对于连接原理和应用之间的逻辑纽带,却不去做细致分析,愚人愚己,又有何益?立言于此,区块链,看汝能火到几时。
]]>如果仔细去搜索一些文章,就可以发现这些文章的模式,大多是这个路数:
RocketMQ 由于借鉴了 Kafka 的设计,包括组件的命名也很多与 Kafka 相似,下面摘抄一段《RocketMQ 原理简介》中的介绍,可以与 Kafka 的命名比对一下,
《RocketMQ 原理简介》中还介绍了一些其他的概念,例如,广播消费和集群消费,广播消费是 Consumer Group 中对于同一条消息每个 Consumer 都消费,集群消费是 Consumer Group 中对于同一条消息只有一个 Consumer 消费。Kafka 采用的是集群消费,不支持广播消费(好吧,是我没有找到)。再例如,普通顺序消息和严格顺序消息,普通顺序消息在 Broker 重启情况下不会保证消息顺序性;严格顺序消息即使在异常情况下也会保证消息的顺序性。个人理解,所谓普通顺序消息,应该就是 Kafka 中的 Partition 级别有序,严格顺序消息,应该是 Topic 级别有序,但文中也提到,这样的有序级别是要付出代价的,Broker 集群中只要有一台机器不可用,则整个集群都不可用,降低服务可用性。使用这种模式,需要依赖同步双写,主备自动切换,但自动切换功能目前还未实现(我猜,自动切换仅仅是没开源吧)。说白了,严格顺序消息不具备生产可用性,自己玩玩还行,其应用场景主要是数据库 binlog 同步。
关于 RocketMQ 和 Kafka 的对比,可以参考 RocketMQ Wiki 中的文章 4,看看就行,不必较真。
顺序性的话题,刚才已经提到了一些,RocketMQ 的实现应该不弱于 Kafka。对于分区,RocketMQ 似乎有意弱化了这个概念,只有在 Producer 中有一个参数 defaultTopicQueueNums
,分区在 RocketMQ 中有时被称为队列。RocketMQ 的普通顺序消息模式,应该就是分区顺序性,这点与 Kafka 一致。
RocketMQ 实现高可用的方式有多种,《RocketMQ 用户指南》文档中提到的有:多主模式、多主多从异步复制模式、多主多从同步复制模式。多主模式下,性能较好,但是在 Broker 宕机的时候,该 Broker 上未消费的交易不可消费;多主多从异步复制模式,与 Kafka 的副本模式比较类似,主 Broker 宕机后,会自动切换到从 Broker,消息的消费不会出现间断;多主多从同步复制模式更进一步,采用同步刷盘的方式,避免了主 Broker 宕机带来的消息丢失,但是,目前不支持自动切换。
虽然 RocketMQ 提供了多种高可用方式,但是目前能生产使用的就只有多主多从异步复制模式,即使在这个模式上,其实现也比 Kafka 要差。因为 RocketMQ 的机制中,主从关系是人为指定的,主 Broker 上承担所有的消息派发,而 Kafka 的主从关系是通过选举的方式选出来的,每个分区的主节点都是不一样的,可以从不同的节点派发消息。Kafka 的模式是分散模式,有利于负载均衡,而且当一个 Broker 宕机的时候,只影响部分 Topic,而 RocketMQ 一旦主 Broker 宕机,会影响所有的 Topic。另外,Kafka 可以支持 Broker 间同步复制(通过设置 Broker 的 acks
参数),这样比的话,RocketMQ 就差太多了。
关于 RocketMQ 的介绍,网上的文章不算太多,也比较杂,《分布式开放消息系统(RocketMQ)的原理与实践》5 6 7这篇原理介绍的不错,推荐。
相比较 Kafka 而言,RocketMQ 提供的工具要少一些,如下,
除了进程启停之外,常用的运维命令都在 mqadmin
中,详见《RocketMQ 运维指令》文档。我实验中常用的一些命令如下,
RocketMQ 使用了自己的 name server 来做调度(Kafka 用了 Zookeeper),使用 sh mqnamesrv
来启动,默认监听端口9876,sh mqnamesrv -m
可以查看所有默认参数,使用 -c xxxx.properties
参数来指定自定义配置。sh mqbroker
是用于启动 Broker 的命令,参数比较多,详细可以通过 sh mqbroker -m
查看默认参数,配置项细节后文再说。sh mqadmin
是运维命令入口,topicList
是列出所有 Topic;topicRoute
是列出单个 Topic 的详细信息;clusterList
是列出集群的信息;deleteTopic
是删除 Topic。consumerProgress
是查看消费者消费进度,deleteSubGroup
是删除消费者的订阅,consumerConnection
是查询消费者订阅的情况。
Broker 的配置是最多的,实验中我修改到的部分如下,其他使用默认,
配置文件中的多数配置看例子就可以知道意思,挑几个说一下。brokerName
和 brokerId
, 同名的 Broker,ID 是0的是主节点,其他是从节点;deleteWhen
,删除文件时间点,默认凌晨4点;fileReservedTime
,文件保留时间,设置为120小时;brokerRole
,Broker 的角色,ASYNC_MASTER 是异步复制主节点,SYNC_MASTER 是同步双写主节点,SLAVE 是备节点。
其实,这些工具的写法也基本一致,都是先做一些检查,最后运行 Java 程序,JVM 系统上的应用应该差不多都这样。
RocketMQ 是用 Java 语言开发的,因此,其 Java API 相对是比较丰富的,当然也有部分原因是 RocketMQ 本身提供的功能就比较多。RocketMQ API 提供的功能包括,
单看功能的话,即使不算事务消息,也不算 Tag,RocketMQ 也远超 Kafka,Kafka 应该只实现了 Pull 模式消费 + 顺序消费这2个功能。RocketMQ 的代码示例在 rocketmq-example 中,注意,代码是不能直接运行的,因为所有的代码都少了设置 name server 的部分,需要自己手动加上,例如,producer.setNamesrvAddr("192.168.232.23:9876");
。
先来看一下生产者的 API,比较简单,只有一种,如下,
可以发现,相比 Kafka 的 API,只多了 Tag,但实际上行为有很大不同。Kafka 的生产者客户端,有同步和异步两种模式,但都是阻塞模式,send
方法返回发送状态的 Future
,可以通过 Future
的 get
方法阻塞获得发送状态。而 RocketMQ 采用的是同步非阻塞模式,发送之后立刻返回发送状态(而不是 Future
)。正常情况下,两者使用上差别不大,但是在高可用场景中发生主备切换的时候,Kafka 的同步可以等待切换完成并重连,最后返回;而 RocketMQ 只能立刻报错,由生产者选择是否重发。所以,在生产者的 API 上,其实 Kafka 是要强一些的。
另外,RocketMQ 可以通过指定 MessageQueueSelector
类的实现来指定将消息发送到哪个分区去,Kafka 是通过指定生产者的 partitioner.class
参数来实现的,灵活性上 RocketMQ 略胜一筹。
再来看消费者的API,由于 RocketMQ 的功能比较多,我们先看 Pull 模式消费的API,如下,
这部分的 API 其实是和 Kafka 很相似的,唯一不同的是,RocketMQ 需要手工管理 offset 和指定分区,而 Kafka 可以自动管理(当然也可以手动管理),并且不需要指定分区(分区是在 Kafka 订阅的时候指定的)。例子中,RocketMQ 使用 HashMap 自行管理,也可以用 OffsetStore
接口,提供了两种管理方式,本地文件和远程 Broker。这部分感觉两者差不多。
下面再看看 Push 模式顺序消费,代码如下,
虽然提供了 Push 模式,RocketMQ 内部实际上还是 Pull 模式的 MQ,Push 模式的实现应该采用的是长轮询,这点与 Kafka 一样。使用该方式有几个注意的地方,
MessageListenerOrderly
;ConsumeFromWhere
有几个参数,表示从头开始消费,从尾开始消费,还是从某个 TimeStamp 开始消费;context.setAutoCommit(false);
的作用;控制 offset 提交这个特性非常有用,某种程度上扩展一下,就可以当做事务来用了,看代码 ConsumeMessageOrderlyService
的实现,其实并没有那么复杂,在不启用 AutoCommit 的时候,只有返回 COMMIT
才 commit offset;启用 AutoCommit 的时候,返回 COMMIT
、ROLLBACK
(这个比较扯)、SUCCESS
的时候,都 commit offset。
后来发现,commit offset 功能在 Kafka 里面也有提供,使用新的 API,调用
consumer.commitSync
。
再看一个 Push 模式乱序消费 + 消息过滤的例子,消费者的代码如下,
这个例子与之前顺序消费不同的地方在于,
MessageListenerConcurrently
;MessageFilterImpl
;消息过滤类 MessageFilterImpl
的代码如下,
RocketMQ 执行过滤是在 Broker 端,Broker 所在的机器会启动多个 FilterServer 过滤进程;Consumer 启动后,会向 FilterServer 上传一个过滤的 Java 类;Consumer 从 FilterServer 拉消息,FilterServer 将请求转发给 Broker,FilterServer 从 Broker 收到消息后,按照 Consumer 上传的 Java 过滤程序做过滤,过滤完成后返回给 Consumer。这种过滤方法可以节省网络流量,但是增加了 Broker 的负担。可惜我没有实验出来使用过滤的效果,即使是用 github wiki 上的例子8也没成功,不纠结了。RocketMQ 的按 Tag 过滤的功能也是在 Broker 上做的过滤,能用,是个很方便的功能。
还有一种广播消费模式,比较简单,可以去看代码,不再列出。
总之,RocketMQ 提供的功能比较多,比 Kafka 多很多易用的 API。
按之前所说,只有 RocketMQ 的多主多从异步复制是可以生产使用的,因此只在这个场景下测试。另外,消息采用 Push 顺序模式消费。
假设集群采用2主2备的模式,需要启动4个 Broker,配置文件如下,
另外,每个机构共通的配置项如下,
其他设置均采用默认。启动 NameServer 和所有 Broker,并试运行一下 Producer,然后看一下 TestTopic1 当前的情况,
可见,TestTopic1 在2个 Broker 上,且每个 Broker 备机也在运行。下面开始主备切换的实验,分别启动 Consumer 和 Producer 进程,消息采用 Pull 顺序模式消费。在消息发送接收过程中,使用 kill -9
停掉 broker-a
的主进程,模拟突然宕机。此时,TestTopic1 的状态如下,
broker-a
的节点已经减少为只有1个从节点。然后启动broker-a
的主节点,模拟恢复,再看一下 TestTopic1 的状态,
此时,RocketMQ 已经恢复。
再来看看 Producer 和 Consumer 的日志,先看 Producer 的,如下,
日志中显示,在发送完00583条消息之后,开始发生异常 connect to <192.168.232.23:10911> failed
,原因应该是 broker-a
的主节点被 kill 掉。之后,从00596条消息开始,RocketMQ 又恢复正常,原因是 broker-b
已经开始提供服务,承担了所有的工作。然后,又重新启动了 broker-a
主节点,由于该节点的加入,从01392条消息开始,broker-a
又开始恢复工作。实验中可以验证,RocketMQ 所谓的多主多备模式,实际上,备机被弱化到无以复加,在主节点宕机的时候,备机无法接替主机的工作,而只是将尚未发送的数据发送出去,由剩下的主节点接替工作。也就是说,N 主 N 备的 RocketMQ 集群中,总共有 2N 台机器,实际工作的只有 N 台,如果有一台挂了,就只有 N-1 台工作了,机器的利用率太低了。
再来看一下 Consumer 的日志,如下,
可以看到,Consumer 在 broker-a
宕机时间的附近,也出现了异常,connect to <192.168.232.23:10911> failed
。虽然还能保持分区上的顺序性,但是已经某种程度上出现了一些紊乱,例如,将我在实验之前的数据给取了出来(Hello MetaQ
的消息)。可是,我在实验前,明明做过删除这个 Topic 的动作,看来 RocketMQ 所谓的删除,并未删除 Topic 的数据。之后,broker-a
主机重启之后,又恢复正常。
RocketMQ Pull模式消费需要手动管理 offset 和指定分区,这个在调用的时候不觉得,实际运行的时候才会发现每次总是消费一个分区,消费完之后,才开始消费下一个分区,而下一个分区可能已经堆积了很多消息了,手动做消息分配又比较费事。或许,Push 顺序模式消费才是更好的选择。
另外还有几个比较异常的情况,实验中有几次出现了 CODE: 17 DESC: topic[TopicTest1] not exist, apply first please!
这样的错误,实际上,这时候我只是关掉了 Producer;还有,sh mqadmin updateTopic –n 192.168.232.23:9876 –c DefaultCluster –t TopicTest1
明明文档中说可以用来新增 Topic,而实际上不行。
补充一下:之后,我又使用 Push 顺序模式消费重做了上述实验,结论差不多。只是因为有多线程的原因,日志看起来偶尔有错位,这个问题不大,可以解决。而且,在关闭重启 Broker 的附近,往往伴随着多次的消息重发,不过,RocketMQ 也不保证消息只收到一次就是了。消息重复的问题,Kafka 要比 RocketMQ 显得不那么严重一些。Push 顺序模式消费不需要指定 offset,不需要指定分区,第二次启动可以自动从前一次的 offset 后开始消费。功能上这个与 Kafka 的 Consumer 更类似,虽然 RocketMQ 采用的是异步模式。
实际上,RocketMQ 自己就有一份《RocketMQ 最佳实践》的文档,里面提到了一些系统设计的问题,例如消费者要幂等,一个应用对应一个 Topic,如此等等。这些经验不仅仅是对 RocketMQ 有用,对 Kafka 也颇有借鉴意义。
这里谈谈我对选择 RocketMQ 还是 Kafka 的个人建议。以上已经做了多处 RocketMQ 和 Kafka 的对比,我个人觉得,Kafka 是一个不断发展中的系统,开源社区比 RocketMQ 要大,也要更活跃一些;另外,Kafka 最新版本已经有了同步复制,消息可靠性更有保障;还有,Kafka 的分区机制,几乎实现了自动负载均衡,这绝对是个杀手级特性;RocketMQ 虽然提供了很多易用的功能,远超出 Kafka,但这些功能并不一定都能用得上,而且多数可以绕过。相比之下,Kafka 的基本功能更加吸引我,再处理故障恢复的时候,细节上要胜过 RocketMQ。当然,如果是 A 公司内部,或者所在公司使用了 A 公司的云产品,那么 RocketMQ 的企业级特性更多一些,或许我会选择 RocketMQ。
]]>特别说明,Broker 是指单个消息服务进程,一般情况下,Kafka 是集群运行的,Broker 只是集群中的一个服务进程,而非代指整个 Kafka 服务,可以简单将 Broker 理解成服务器(Server)。Kafka 引入的术语都比较常见,从字面上理解相对直观。Kafka 的大致结构图是这样,
Kafka 是 Pull 模式的消息队列,即 Consumer 连到消息队列服务上,主动请求新消息,如果要做到实时性,需要采用长轮询,Kafka 在0.8的时候已经支持长轮询模式。上图中 Consumer 的连接箭头方向可能会让读者误以为是 Push 模式,特此注明。更多关于 Kafka 设计的文章可以参考官方文档,或者一些比较好的博客文章 3。
Kafka 是一个力求保持消息顺序性的消息队列,但不是完全保证,其保证的是 Partition 级别的顺序性,如下图,
此图是 Topic 的分区 log 的示意图,可见,每个分区上的 log 都是一个有序的队列,所以,Kafka 是分区级别有序的。如果,某个 Topic 只有一个分区,那么这个 Topic 下的消息就都是有序的。
分区是为了提升消息处理的吞吐率而产生的,将一个 Topic 中的消息分成几份,分别给不同的 Broker 处理。如下图,
此图中有2个 Broker,Server 1 和 Server 2,每个 Broker 上有2个分区,总共4个分区,P0 ~ P3;有2个 Consumer Group,Consumer Group A 有2个 Consumer,Consumer Group B 有4个 Consumer。Kafka 的实现是,在稳定的情况下,维持固定的连接,每个 Consumer 稳定的消费其中某几个分区的消息,以上图举例,Consumer Group A 中的 C1 稳定消费 P0、P3,C2 稳定消费 P1、P2。这样的连接分配可能会导致消息消费的不均匀分布,但好处是比较容易保证顺序性。
维持完全的顺序性在分布式系统看来几乎是无意义的。因为,如果需要维持顺序性,那么就只能有一条线程阻塞的处理顺序消息,即,Producer -> MQ -> Consumer 必须线程上一一对应。这与分布式系统的初衷是相违背的。但是局部的有序性,是可以维持的。比如,有30000条消息,每3条之间有关联,1->2->3,4->5->6,……,但是全局范围来看,并不需要保证 1->4->7,可以 7->4->1 的顺序来执行,这样可以达到最大并行度10000,而这通常是现实中我们面对的情况。通常应用中,将有先后关系的消息发送到相同的分区上,即可解决大部分问题。
副本是高可用 Kafka 集群的实现方式。假设集群中有3个 Broker,那么可以指定3个副本,这3个副本是对等的,对于某个 Topic 的分区来说,其中一个是 Leader,即主节点,另外2个副本是 Follower,即从节点,每个副本在一个 Broker 上。当 Leader 收到消息的时候,会将消息写一份到副本中,通常情况,只有 Leader 处于工作状态。在 Leader 发生故障宕机的时候,Follwer 会取代 Leader 继续传送消息,而不会发生消息丢失。Kafka 的副本是以分区为单位的,也就是说,即使是同一个 Topic,其不同分区的 Leader 节点也不同。甚至,Kafka 倾向于用不同的 Broker 来做分区的 Leader,因为这样能做到更好的负载均衡。
在副本间的消息同步,实际上是复制消息的 log,复制可以是同步复制,也可以是异步复制。同步复制是说,当 Leader 收到消息后,将消息写入从副本,只有在收到从副本写入成功的确认后才返回成功给 Producer;异步复制是说,Leader 将消息写入从副本,但是不等待从副本的成功确认,直接返回成功给 Producer。同步复制效率较低,但是消息不会丢;异步复制效率高,但是在 Broker 宕机的时候,可能会出现消息丢失。
任何一个 MQ 都需要处理丢消息和重复收到消息的,正常情况下,Kafka 可以保证:1. 不丢消息;2. 不重复发消息;3. 消息读且只读一次。当然这都是正常情况,极端情况,如 Broker 宕机,断电,这类情况下,Kafka 只能保证 1 或者 2,无法保证 3。
在有副本的情况下,Kafka 是可以保证消息不丢的,其前提是设置了同步复制,这也是 Kafka 的默认设置,但是可能出现重复发送消息,这个交给上层应用解决;在生产者中使用异步提交,可以保证不重复发送消息,但是有丢消息的可能,如果应用可以容忍,也可以接受。如果需要实现读且只读一次,就比较麻烦,需要更底层的 API 4。
Kafka 提供的工具还是比较全的,bin/
目录下的工具有以下一些,
我常用的命令有以下几个,
kafka-server-start.sh
是用于 Kafka 的 Broker 启动的,主要就一个参数 config/server.properties
,该文件中的配置项待会再说.还有一个 -daemon
参数,这个是将 Kafka 放在后台用守护进程的方式运行,如果不加这个参数,Kafka 会在运行一段时间后自动退出,据说这个是 0.10.0.0 版本才有的问题 5。kafka-topics.sh
是用于管理 Topic 的工具,我主要用的 --describe
、--list
、--delete
、--create
这4个功能,上述的例子基本是不言自明的,--replication-factor 3
、--partitions 2
这两个参数分别表示3个副本(含 Leader),和2个分区。kafka-console-consumer.sh
和 kafka-console-producer.sh
是生产者和消费者的简易终端工具,在调试的时候比较有用,我常用的是 kafka-console-consumer.sh
。我没有用 Kafka 自带的 zookeeper,而是用的 zookeeper 官方的发布版本 3.4.8,端口是默认2181,与 Broker 在同一台机器上。
下面说一下 Broker 启动的配置文件 config/server.properties
,我在默认配置的基础上,修改了以下一些,
broker.id
是 Kafka 集群中的 Broker ID,不可重复,我在多副本的实验中,将他们分别设置为0、1、2;listeners
是 Broker 监听的地址,默认是监听 localhost:9092
,因为我不是单机实验,所以修改为本机局域网地址,当然,如果要监听所有地址的话,也可以设置为 0.0.0.0:9092
,多副本实验中,将监听端口分别设置为 9092、9093、9094;log.dirs
是 Broker 的 log 的目录,多副本实验中,不同的 Broker 需要有不同的 log 目录;delete.topic.enable
设为 true 后,可以删除 Topic,并且连带 Topic 中的消息也一并删掉,否则,即使调用 kafka-topics.sh --delete
也无法删除 Topic,这是一个便利性的设置,对于开发环境可以,生产环境一定要设为 false(默认)。实验中发现, 如果有消费者在消费这个 Topic,那么也无法删除,还是比较安全的。
剩下的工具多数在文档中也有提到。如果看一下这些脚本的话,会发现多数脚本的写法都是一致的,先做一些参数的校验,最后运行 exec $base_dir/kafka-run-class.sh XXXXXXXXX "$@"
,可见,这些工具都是使用运行 Java Class 的方式调用的。
在编程接口方面,官方提供了 Scala 和 Java 的接口,社区提供了更多的其他语言的接口,基本上,无论用什么语言开发,都能找到相应的 API。下面说一下 Java 的 API 接口。
生产者的 API 只有一种,相对比较简单,代码如下,
上例中使用了同步和异步发送两种方式。在多副本的情况下,如果要指定同步复制还是异步复制,可以使用 acks
参数,详细参考官方文档 Producer Configs 部分的内容;在多分区的情况下,如果要指定发送到哪个分区,可以使用 partitioner.class
参数,其值是一个实现了 org.apache.kafka.clients.producer.Partitioner
接口的类,用于根据不同的消息指定分区6。消费者的 API 有几种,比较新的 API 如下,
消费者还有旧的 API,比如 Consumer
和 SimpleConsumer
API,这些都可以从 Kafka 代码的 kafka-example 中找到,上述的两个例子也是改写自 kafka-example。使用新旧 API 在功能上都能满足消息收发的需要,但新 API 只依赖 kafka-clients
,打包出来的 jar 包会小很多,以我的测试,新 API 的消费者 jar 包大约有 2M 左右,而旧 API 的消费者 jar 包接近 16M。
其实,Kafka 也提供了按分区订阅,可以一次订阅多个分区 TopicPartition[]
;也支持手动提交 offset,需要调用 consumer.commitSync
。
Kafka 似乎没有公开 Topic 创建以及修改的 API(至少我没有找到),如果生产者向 Broker 写入的 Topic 是一个新 Topic,那么 Broker 会创建这个 Topic。创建的过程中会使用默认参数,例如,分区个数,会使用 Broker 配置中的 num.partitions
参数(默认1);副本个数,会使用 default.replication.factor
参数。但是通常情况下,我们会需要创建自定义的 Topic,那官方的途径是使用 Kafka 的工具。也有一些非官方的途径 7,例如可以这样写,
但是这样写有一个问题,在执行完 TopicCommand.main(options);
之后,系统会自动退出,原因是执行完指令之后,会调用 System.exit(exitCode);
系统直接退出。这样当然不行,我的办法是,把相关的执行代码挖出来,写一个 TopicUtils 类,如下,
以上的 oper
方法改写自 kafka.admin.TopicCommand$.main
方法。可以发现这部分代码非常怪异,原因是 TopicCommand$
是 Scala 写的,再编译成 Java class 字节码,然后我根据这些字节码反编译得到 Java 代码,并以此为基础进行修改,等于是我在用 Java 的方式改写 Scala 的代码,难免会觉得诡异。当然,这种写法用在生产环境的话是不太合适的,因为调用的 topicCommand$.createTopic
等方法都没有抛出异常,例如参数不合法的情况,而且也没有使用 log4j 之类的 log 库,只是用 System.out.println
这样的方法屏显,在出现错误的时候,比较难以定位。
在生产环境中,Kafka 总是以“集群+分区”方式运行的,以保证可靠性和性能。下面是一个3副本的 Kafka 集群实例。
首先,需要启动3个 Kafka Broker,Broker 的配置文件分别如下,
虽然每个 Broker 只配置了一个端口,实际上,Kafka 会多占用一个,可能是用来 Broker 之间的复制的。另外,3个 Broker 都配置了,
在同一个 Zookeeper 上的 Broker 会被归类到一个集群中。注意,这些配置中并没有指定哪一个 Broker 是主节点,哪些 Broker 是从节点,Kafka 采用的办法是从可选的 Broker 中,选出每个分区的 Leader。也就是说,对某个 Topic 来说,可能0节点是 Leader,另外一些 Topic,可能1节点是 Leader;甚至,如果 topic1 有2个分区的话,分区1的 Leader 是0节点,分区2的 Leader 是1节点。
这种对等的设计,对于故障恢复是十分有用的,在节点崩溃的时候,Kafka 会自动选举出可用的从节点,将其升级为主节点。在崩溃的节点恢复,加入集群之后,Kafka 又会将这个节点加入到可用节点,并自动选举出新的主节点。
实验如下,先新建一个3副本,2分区的 Topic,
初始状况下,topic1 的状态如下,
对于上面的输出,即使没有文档,也可以看懂大概:topic1 有2个分区,Partition 0 和 Partition 1,Leader 分别在 Broker 0 和 1。Replicas 表示副本在哪些 Broker 上,Isr(In-Sync Replicas)表示处于同步状态中的 Broker,如果有 Broker 宕机了,那么 Replicas 不会变,但是 Isr 会仅显示没有宕机的 Broker,详见下面的实验。
然后分2个线程,运行之前写的 Producer 和 Consumer 的示例代码,Producer 采用异步发送,消息采用同步复制。在有消息传送的情况下,kill -9
停掉其中2个 Broker(Broker 0 和 Broker 1),模拟突然宕机。此时,topic1 状态如下,
可见,Kafka 已经选出了新的 Leader,消息传送没有中断。接着再启动被停掉的那两个 Broker,并查看 topic1 的状态,如下,
可以发现, 有一个短暂的时间,topic1 的两个分区的 Leader 都是 Broker 2,但是在 Kafka 重新选举之后,分区1的 Leader 变为 Broker 1。说明 Kafka 倾向于用不同的 Broker 做分区的 Leader,这样更能达到负载均衡的效果。
再来看看 Producer 和 Consumer 的日志,下面这个片段是2个 Broker 宕机前后的日志,
出现错误的时候,Producer 抛出了 NetworkException
异常。其中有3589条 Received 日志,3583条 Send 日志,7条 NetworkException
异常日志,发送消息的最大序号是3590,接收消息的最大序号是3589,有以下几个值得注意的地方,
从这个实验中,可以看到,虽然 Kafka 不保证消息重复发送,但是却在尽量保证没有消息被重复发送,可能我的实验场景还不够极端,没有做出消息重复的情况。
如之前所说,如果要保持完全顺序性,需要使用单分区;如果要避免抛出 NetworkException
异常,就使用 Producer 同步发送。下面,我们重做上面的例子,不同之处是使用单分区和 Producer 同步发送,截取一段 Broker 宕机时的日志如下,
可见,由于采用同步发送,Broker 宕机并没有造成抛出异常,另外,由于使用单分区,顺序性也得到了保证,全局没有出现乱序的情况。
综上,是否使用多分区更多的是对顺序性的要求,而使用 Producer 同步发送还是异步发送,更多是出于重复消息的考虑,如果异步发送抛出异常,在保证不丢消息的前提下,势必要重发消息,这就会导致收到重复消息。多分区和 Producer 异步发送,会带来性能的提升,但是也会引入非顺序性,重复消息等问题,如何取舍要看应用的需求。
Kafka 在一些应用场景中,有一些前人总结的最佳实践 8 9。对最佳实践,我的看法是,对于自己比较熟悉,有把握的部分,可以按自己的步骤进行;对一些自己不清楚的领域,可以借鉴其中的一些内容,至少不会错的特别厉害。有文章10说,Kafka 在分区比较多的时候,相应时间会变长,这个现象值得在实践中注意。
在 Kafka 与 RocketMQ 的对比中,RocketMQ 的一个核心功能就是可以支持同步刷盘,此时,即使突然断电,也可以保证消息不丢;而 Kafka 采用的是异步刷盘,即使返回写入成功,也只是写入缓冲区成功,并非已经持久化。因此,如果出现断电或 kill -9
的情况,Kafka 内存中的消息可能丢失。另外,同步刷盘的效率是比较低下的,一般生产中估计也不会使用,可以用优雅关闭的方式来关闭进程。如果不考虑这些极端情况的话,Kafka 基本是一个很可靠的消息中间件。
nexus 是目前用的比较多的 maven 仓库的私服,本文记录在 nexus 私服上发布 dubbox 的方法,参考 1。
下载 dubbox 2.8.4 的源码,在源码的根目录下运行 maven install
,
构建的过程中跳过测试,构建完成之后,会安装到本地 maven 仓库中。
pom.xml
和 setting.xml
修改源码根目录下的 pom.xml
,在文件尾部增加 distributionManagement
节点,如下,
上述的 maven-private-server
需要换成实际的私服地址。还有 maven 的配置文件 setting.xml
,也需要确认一下,需要有可以 deploy 的用户,如下,
注意,maven server 的 id 需要和上面配置的 repository 的 id 保持一致。
确认 releases 这个私服的权限是允许重复发布的,如下,
这个主要是如果 dubbox 需要重复发布的话,可以覆盖之前发布的版本。
用 maven deploy
发布到 nexus 私服,如下,
deploy 完成后,发现只发布了几个模块,并不是发布了所有的组件,
原因是,部分组件的 pom.xml
中设置了 <skip_maven_deploy>true</skip_maven_deploy>
在 deploy 的时候会跳过这些组件。如果要全部发布,需要全部修改为 <skip_maven_deploy>false</skip_maven_deploy>
,可以使用如下命令来全部替换,
修改完之后,重新运行 mvn deploy -DskipTests
即可全部 deploy。
至此,dubbox 已经发布到私服完成.
]]>内存模型(Memory Model)是编程中比较深入的一个问题,它与编程语言有关、与编译器有关、与并发有关、与处理器也有关。但是一旦发生与内存模型相关的问题,总是出现在并发的场景下,多数情况下,我们搞不清楚内存模型和并发有什么关系,似乎紧密相关,又似乎找不到必然的联系。本文试图尽量浅显明白的说清楚内存模型这个问题,行文中参考了一些文章 1 2 3 4 5 6 。
先说说什么是重排序。重排序有很多种,最常见的是编译优化重排序,就是说,你写的代码,可能你自己已经觉得很优化了,但是计算机不见得觉得足够优化,因此它会调整你的代码中某些行的执行顺序(甚至以等价的方式重写代码),以达到效率上的提升。这个在写代码的时候已经见过很多了,比如,在 Windows 上写代码,开发的时候会用 Debug 模式构建,但是发布的程序会使用 Release 模式构建;Linux 上也类似,开发的时候用 gcc -O
,发布的时候用 gcc -O3
。这其中的差异,部分就在代码的重排序优化上,例如,循环的展开:如果编译器认为这个循环比较小,那么展开循环可以省去跳转指令(JUMP),对性能会有提升。当然,这个例子可能不是特别恰当,总之想说明白的就是,你写的代码和计算机执行的代码几乎必然是两回事,虽然结果一样。
还有一种重排序,指令并行重排序。现代的 CPU 都是流水线的,同时在执行的指令有多条(在执行的不同阶段,这与 CPU 架构有关),如果 CPU 认为,你代码中的这些指令不存在相关性,那么它就会选择并行执行。并行执行后,代码的顺序进一步被打乱。
其实,我个人认为,重排序只有这两种,但是也有认为7还有第三种重排序,内存系统重排序,我认为这应该属于 CPU 缓存。
这两种重排序,可以称为“线程级”(这是我造的名词),因为这两种重排序都是发生在单个线程内部,以不改变该线程的计算结果为依据进行的改进,也就是说,如果碰到多线程相互作用的情况,可能有问题。
再说说 CPU 缓存,学计算机的都知道,CPU 和内存之间还隔着 N 层缓存,以现代的 CPU 来看,一般有 3 层缓存,L1、L2、L3、再下面就是内存了,每一层比下一层速度更快,容量更小。如下图(这是只有一层缓存的示意图),
为了提升程序运行效率,CPU 在从内存取数据之后,会把数据存在缓存中,下次取数据的时候,直接从缓存中拿。当然,缓存数据最终会写入内存,但是在程序运行的过程中,可能内存中的数据不是最新的,最新数据在缓存中。在现代 CPU 中,一般有多个核,每个 CPU 核都会有自己的缓存,但是内存只有一块,是共享的。何时从缓存中读取数据,CPU 的优化有个标准,就是“线程级”结果正确,也就是说,当 CPU 认为,对于当前线程(不考虑其他线程的修改),如果内存和缓存中的数据一致,那么会用缓存中的数据。当然,如果有其他线程对共享的内存做了修改,就会导致 CPU 没有意识到内存已经变化,而仍然取缓存中的数据,导致执行错误。
上文中大致介绍了内存模型与编译器有关(编译优化重排序)、与处理器有关(指令并行重排序、CPU 缓存)、与并发有关(CPU 缓存),那么内存模型究竟解决了什么问题呢?
先看一个例子,假设有两个线程,线程的代码和初始值如下,
线程1 | 线程2 |
---|---|
r2=A; //1 |
r1=B; //3 |
B=1; //2 |
A=2; //4 |
初始值:A=B=0; |
问题是,程序运行完后,r1
和 r2
的值是多少?如果你的答案是不确定,那么答对了,那么有几种可能的结果呢?我们来分析一下,
r1=1
r2=0
;r1=0
r2=0
;r1=0
r2=2
;因此,总共有3个不同的结果。可是,这个答案对么?不对,因为这个解答没有考虑到刚才说到的重排序和 CPU 缓存。
我们先分析,如果有 CPU 缓存会发生什么。有缓存之后,A
B
的值都可能缓存在内存中,因此,缓存中可能存有 A=0
A=2
B=0
B=1
这样的中间结果,而另一个线程会读到什么值是不确定的。假设执行顺序是 1->2->3->4,但是,1、2完成的时候,只修改了线程1所在 CPU 的缓存,并未更新内存中的值,这样,线程2就无法读取到 B=1
,结果还是 r1=0
r2=0
。同样的逻辑也适用于执行顺序是 3->4->1->2 的情况,看起来程序多了很多 r1=0
r2=0
的情况。那么,虽然引发的原因不同,但是,程序的结果应该是只有这 3 种了,对么?不对,别忘了还有重排序。
不管是编译器优化重排序,还是指令并行重排序,单看本线程内,如果语句的顺序调整不会引发结果错误,那么这种重排序就是可能发生的。也就是说,虽然线程1中的代码是 1->2 这样的执行顺序,实际上 2->1 这样的执行顺序也是可能发生的,同理,4->3 也可能发生。情况似乎复杂了,我们需要考虑另外的3种情况:1. 线程1有重排序,线程2没有;2. 线程1没有重排序,线程2有;3. 线程1和线程2都有重排序。而且每一种情况,都需要考虑缓存的影响。
到这里,我们已经感到无所适从了,当然,你可以仔细分析所有的情况,最终得出所有的结果,但这并不是我们想要的,在此只举一个例子。例如,结果是 r1=1
r2=2
是否可能?尽管这个结果看上去很不可思议,但确实是可能的,比如,执行顺序是 2->4->1->3,即线程1和线程2都发生了重排序,但没有缓存的影响。
以上例子描述了内存模型需要解决的问题。问题的背景是多线程并发,问题的内容,一是重排序优化问题,即如何限制编译器优化和指令并行优化;二是可见性问题,即如何将一个线程修改的结果传递给其他线程(或者叫发布),最终的目标是,程序计算结果正确。一句话,如何在多线程并发的情况下,限制编译器优化、指令并行优化,控制线程间的变量传递,使得程序计算结果正确。当然,这是我自己的总结,不严谨,领会其大意即可。注意,内存模型面对的问题中,似乎并没有出现锁,其实并非如此,在“控制线程间变量传递”的过程中,锁是基本的底层工具之一,但内存模型所面对的问题远比锁要来的复杂。
以上已经分析了内存模型问题的复杂性,有没有解决办法呢?有,我的解法非常简单直接:1. 废除编译器优化和流水线的指令并行优化;2. 废除 CPU 缓存,直接从内存中读取。做到这两点,基本不再会有多线程并发的问题了,另一个连带效应是,你的多线程程序,可能比优化过的单线程程序还慢。
当然,这是玩笑话,我们不可能因为要多线程并发,而放弃这么多年来积累的编译器优化技术、CPU 优化技术,这是因噎废食的。现代编程语言(如C++、Java)对这个问题的解法主要包括两个,限制编译器优化,以及内存屏障。
这个很好理解,对于一些需要在线程间同步的变量,我们可以限制编译器对含这些变量语句的重排序优化,禁止某些情况下的语句重排序,至于那些不需要同步的变量,那么自然是允许编译器尽可能的优化,以提升性能。这些限制重排序优化的规则中,比较有名的是 happens-before
规则,这个规则在不同的语言中包含的内容也不太一样,但基本上是符合常识的。这里先不介入 happens-before
的细节内容,但是其中的一些内容可能你在写代码的时候已经用到了,例如,Java 的 synchronized
原语,C++11 中锁的 memory order。
其实,内存模型面对的问题,并不是分开逐个解决的,而是在一些语言特性中统一的解决,因此,上文提到的 happens-before
规则中也有很多关于内存屏障的内容。所谓内存屏障,就是用来限制流水线的指令并行优化,并将缓存中的结果同步到内存。这是我对内存屏障的认识,并不严谨。最著名的内存屏障的应用就是锁了,还有其他的表现,例如,刚才提到的 Java 的 synchronized
原语,C++11 中锁的 memory order,也包含了对内存屏障的描述。内存屏障是与特定硬件体系有关的,例如,在 x86 体系的 CPU 中,有 mfence
、sfence
、lfence
这样的指令,显示的指明内存屏障。
本文描述了内存模型面对的问题,并简介了解决问题的方法,这些方法在不同的编程语言中有不同的实现,需要进一步细致讨论。多线程编程有多种模式,主要有两大类,共享内存和消息传递,使用消息传递模式的语言主要有 Scala、Erlang、Go;使用共享内存模式的语言有 Java、C++等。粗略分一下的话,函数式编程语言多数会使用消息传递模式,命令式编程语言大多使用共享内存模式,例外也有,比如 Go。本文提到的内存模型实际上是共享内存模式的一种实现,当然,多线程编程模式是一个更加宏大的问题,在此无法详述。
]]>内存模型(Memory Model)是编程中比较深入的一个问题,它与编程语言有关、与编译器有关、与并发有关、与处理器也有关。但是一旦发生与内存模型相关的问题,总是出现在并发的场景下,多数情况下,我们搞不清楚内存模型和并发有什么关系,似乎紧密相关]]>
已测试 Keytool 转 OpenSSL。 -- by valleylord
项目中的一个系统,即将第一版上线,时间压的比较紧,半个月前刚刚发布到 UAT 环境。除去一些业务上的 bug 不提,发现一个很诡异的现象:系统的一个模块在工作2天之后连不上了。
经过检查,发现是服务器后台 TCP 连接有很多处于 CLOSE_WAIT 状态。问题短时间内无法查明,于是决定重启应用,不耽误测试进度。第二天,问题重现,无解,重启,第三天,仍然重现,周而复始。
完全不知问题所在,一筹莫展。
开始怀疑是 Tomcat 的keepAliveTimeout
或connectionLinger
相关参数设置有问题,导致 HTTP 连接长时间没有释放,但实际上并不是这个原因。
系统的大致架构是这样,共有2个应用,A 应用对外提供 REST 访问;B 应用对内提供 REST 访问,同时,B 应用上连接了很多 ActiveMQ,用于请求其他系统的数据。A、B 应用和 MQ 均部署在同一服务器上。
真正的原因是,实际上的多个 CLOSE_WAIT 状态是因为连了很多 tcp://xxxxxxx
的MQ造成的。细节是这样,A 应用的一个访问请求,通过 REST 访问 B 应用,再通过 B 上的 MQ 去获取外部的数据,B 在请求外部数据的时候,使用了 responseQueue.poll()
这样的写法来异步等待 MQ 的返回(别问我为什么不用同步的请求,外部的接口就只能提供这些)。因此,在外部系统处理发生异常的时候,该消息就永远不会返回,而可怜的 B 应用只能傻傻在那里等,并耗费一条 Tomcat 的线程,A 应用则因为 REST 请求超时,早早收回了资源,进入了 TCP 所谓的半关闭状态。
如果仅仅如此的话,请求不会返回,我们应该也能很快发现这个问题。实际上,奇葩的是,来自 A 应用的这个请求是一个定时请求,每隔一段时间请求一次,如果取不到数据,就用前一次的数据返回。更奇葩的是,MQ 另一端的数据提供方并不是每次都会产生错误,前几次数据请求是返回正常的,后几次会发生问题。更更奇葩的是,数据提供方的这个数据并没有经常变,在生产环境中该数据变动频率会比较高,但是在测试环境,可能没去造数据,每天的数据都一样。种种特例情况导致了我们开始查问题的时候根本没去往这个方向想。
改为使用 responseQueue.poll(timeout, timeUnit)
这样的写法来异步等待 MQ 的返回,最多等待 30 秒,如无返回直接超时。修改代码总量仅3行。
Connection Reset
问题还是这个项目,在上一个新版之后,发现原先能用的 HTTPClient 请求,无法请求到了,客户端提示的异常中有 Connection Reset
。
有了之前那个 CLOSE_WAIT 问题的经验,在查这个问题的时候,对 TCP、HTTP 什么的已经有一些经验了。造成这个问题的原因基本上是,TCP 的一端(通常是服务器端)在向另一端写数据的时候,在数据尚未写完的时候,就强行关闭了连接,导致发的数据不全。类似的原因还可能提示 BROKEN PIPE
这样的错误。
多数情况下,该连接的服务器端应该在打开 socket 连接的时候启用了 SO_LINGER
这样的参数,并设置 linger=0
,对 Tomcat 来说,就是设置了 connectionLinger=0
。但是仅仅这样,仍然不足以产生这样的问题,问题是在 HTTPClient 连接的时候,设置了 HTTP Header 参数 Connection: close
,默认情况下,该参数是 Connection: keep-alive
,表示请求完之后,不立即关闭 TCP 连接,如果马上还有 HTTP 连接的话,可以继续使用这条连接,如果该参数值是 close,那就表示立刻关闭。正因为有 Connection: close
和 connectionLinger=0
的配合,才导致了一请求完成,立刻关闭连接,并且不等待数据发送完毕,因此造成 TCP 数据包不完整。
删掉使用 HTTPClient 的时候设置的 HTTP Header Connection: close
,即这一行 webClient.replaceHeader("Connection", "close");
。修改代码总量仅1行。
net::ERR_SSL_PROTOCOL_ERROR
还是这个项目,使用 Chrome 浏览器测试,在 POST、PUT 少量数据的时候,系统正常;当 HTTP 数据包大于 8k(近似值)的时候,提示 net::ERR_SSL_PROTOCOL_ERROR
。系统采用 HTTPS 协议。
这是一个很诡异的问题,在开发测试环境没有发生,在 UAT 环境发生了。首先怀疑是操作系统和依赖库的原因,查了一遍,无果。使用旧版的浏览器没这个问题,但是新版的有;使用其他浏览器提示的信息不一致,FireFox 没有提示,仅仅是空返回,某国产浏览器提示 Connection Reset
。总之,找不到该问题必然发生的条件。
其实,HTTPS 背后的 SSL/TLS 协议是有分版本的,做 Java 的可以参考这个。对 Tomcat 来说,有以下一些版本可以设置(sslEnabledProtocols
参数),SSLv2、SSLv3、TLSv1、TLSv1.1、TLSv1.2、SSLv2Hello。我们设置的是 sslEnabledProtocols="TLSv1,TLSv1.1,TLSv1.2,SSLv2Hello"
在服务器端启用了这么多 SSL/TLS 协议版本的时候,浏览器在访问的时候会先握手,以确定要用哪个版本的协议(我猜这部分应该是明文),最后会选择一个服务器和浏览器都支持的协议版本,如果符合要求的协议版本有多个,应该会选择版本最高的那个(不确定是不是最高版本)。
我们的问题发生在库文件上,服务器和浏览器在握手阶段,判定使用 TLSv1.2 协议是没问题的,于是接下来使用该协议。但是,实际上该协议在服务器端的实现有 Bug,不能处理超过 8k 的包,因此就会报错 net::ERR_SSL_PROTOCOL_ERROR
,Chrome 的报错还是比较准确的。为什么不怀疑是浏览器的问题,因为 Chrome、FireFox、IE 都有问题,所以应该是服务器的问题。但是,具体这个问题是发生在 OpenSSL、JDK、还是 Tomcat,就不得而知了,OpenSSL 1.0.1p、JDK 1.8.0_66、Tomcat 8.0.32。
该问题在以下一些场景必然不会发生,
问题的发现是采用 FireFox 中的隐藏参数 security.tls.version.max
,限制浏览器可选的 TLS 协议版本,可以参考这里。
将 Tomcat 中的参数改为 sslEnabledProtocols="TLSv1,TLSv1.1,SSLv2Hello"
。修改代码总量仅8个字符。
虽然找到的解决方法和可能的原因,但是仍然没有最终解决这个问题。因为同样的操作系统、库、JDK、Tomcat,在开发测试环境就没有碰到这样的问题,这个还没有找到解释。
问题的修改可能很简单,但是问题本身可能很复杂(当然,对大牛来说,这都是简单问题),需要坚实的基本功才能找到问题,对症下药。
]]>项目中的一个系统,即将第一版上线,时间压的]]>
Catlet 的入口类是在 HintCatletHandler
,该类实现了 HintHandler
接口,可以在 RouteService.route()
中有 Hint 的时候使用,可见,Catlet 的调用需要使用注释。HintCatletHandler.route()
方法是该类的主要方法,其重要的代码有以下几行,
先初始化一个 Catlet
类,再依次调用其 route()
和 processSQL()
方法,这两个方法也是 Catlet 的入口方法。实际上,Catlet
是一个接口,有多个实现,route()
和 processSQL()
是其中两个重要的抽象方法。对于跨数据的多表连接,目前只有一个开发中的类 ShareJoin
,该类文件中定义了3个类,ShareJoin
、ShareDBJoinHandler
和 ShareRowOutPutDataHandler
。
Catlet.route()
的代码并不是特别复杂,重要代码是以下几行,
即调用 JoinParser.parser()
方法来解析 SQL。重要的代码基本都在 Catlet.processSQL()
方法中,这个方法相对复杂一些,调用的层次也比较多,会调用到 ShareDBJoinHandler
和 ShareRowOutPutDataHandler
,重要代码有以下几行,
以上代码中,先使用之前 route()
方法中 JoinParser
的结果,获取所有用于表连接列(getJoinLkey()
),并以此为参数初始化 ShareDBJoinHandler
类;然后把 ShareDBJoinHandler
作为执行 SQL 的回调类,执行 SQL;最后,设置所有工作都完成之后的监听类 AllJobFinishedListener
。因此,ShareDBJoinHandler
是处理 JOIN 的关键类,并且,处理的算法与表连接的列有关。
在 ShareDBJoinHandler
类中,onRowData
方法用于处理收到查询结果,是主要的回调方法,这个方法主要调用了同一个类中的 putDBRow
方法。putDBRow
方法也比较短,主要代码有以下几行,
先设置了一个批处理的大小,999(实际应该是1000,因为使用的是 >
而不是 >=
),然后调用 createQryJob
。createQryJob
相对复杂一点,先是使用 StringBuilder sb
做了比较长的一段字符串拼接,然后有几行比较重要的代码,如下,
先根据之前字符串拼接的结果,生成了一个 SQL,然后就是正常的调用路由并执行,执行的回调类是 ShareRowOutPutDataHandler
。如果不求甚解的话,感觉是 ShareJoin 把原有的 SQL 按每 1000 个表连接列的值为大小,生成一些子 SQL,并执行这些子 SQL,但并不清楚是如何拆分的。ShareRowOutPutDataHandler
类的 onRowData
方法,用于处理子 SQL 的返回。这个方法里面的代码没有什么特别,基本就是将处理的数据写回。
因此,拆分子 SQL 的方法实际上与字符串拼接有关,字符串拼接部分的代码如下,
实际上也没有特别复杂,就是根据连接列的类型,如果是 String 的话,就拼接为 ('a','b','c')
这样;如果是 int/long 类型,就拼接为 (1,2,3)
这样。然后在生成 SQL 的时候,调用 String sql = String.format(joinParser.getChildSQL(), sb);
,joinParser.getChildSQL()
的代码是,
代码并未深究,从注释来看,是在连接的列上加了 in
,也就是说,最后拼成的 SQL 会是类似这样,
其中的 YYYYYYYY
是之前字符拼接的结果。因此,从此可以得知 Catlet 在处理多表连接算法的大致步骤,假设原先的 SQL 是 select a.name, b.dept from tableA a, tableB b where a.id = b.id
,流程是,
select a.name, a.id from tableA a
,然后在相应节点上执行;a.id
拆分,每 1000 个值生成一个子 SQL,生成的 SQL 大致是 select b.dept, b.id from tableB b where b.id in (YYYYYYYY)
,其中,YYYYYYYY 是 a.id
的值每 1000 个拼出来的逗号分割的字符串;以上是 MyCAT Catlet 的大致处理流程。
]]>Catlet 的入口类是在 HintCatletHandler
,该类实现了 HintHandl]]>
系统的入口方法在 io.mycat.MycatStartup
中的 main
方法,主要代码如下两行,
MycatServer
是一个单例类,所以,等于直接调用 MycatServer
中的 startup()
方法。startup()
方法中,除去一些打印 log 的代码,主要初始化了一些系统参数(如网络、datasource)和连接池,重要的代码是以下几行,
首先初始化了一个 NIOReactor 的线程池 NIOReactorPool
和一个 MySQL 连接的工厂类 MySQLFrontConnectionFactory
,然后以这两个为参数,构造了 NIOAcceptor
类,并在主线程中启动 start()
。其中,NIOReactorPool
主要包含一个 NIOReactor
的数组,每个数组都是一个线程对象,处理每一个客户端网络连接,该类在初始化完成的时候,已经调用了 reactor.startup()
,启动了所有 NIOReactorPool
中的所有线程。NIOReactorPool
、NIOReactorPool
和 NIOReactor
这3个类组成了 MyCAT 处理客户端连接的几乎全部代码。MyCAT 主要使用 NIO (java.nio)网络模型,对高并发请求有更好的处理,但是其程序结构中有很多回调函数的写法,不是很容易理解和掌握。另外,MySQLFrontConnectionFactory
是工厂类,主要用于生成处理连接的 MySQLFrontConnection
类(该类继承自 GenalMySQLConnection
,GenalMySQLConnection
继承自 Connection
)。这些类都是之后代码分析中非常重要的类。
至此,主线程已经完成初始化,并启动了 NIOAcceptor
,NIOAcceptor
继承了 Thread
,下面的代码入口在 NIOAcceptor.run()
。
处理网络连接的入口在 NIOAcceptor.run()
,该方法中启动了一个无限循环,主要调用了该类中的 accept()
方法,方法的主要代码有以下几行,
先是调用 serverChannel.accept()
,这是 NIO 的调用,用于接受一个新的连接 channel
,然后设置连接为 nonblocking 模式。然后以新连接 channel
为参数创建 Connection
,然后,从线程池中获取一个 NIOReactor
线程,并调用 postRegister(c)
,将 Connection
注册在该线程中,所有连接的请求都调用该 Connection
中的方法来处理。由于 NIOReactor
线程已经启动,所以会直接调用其中的 run()
方法。而实际上,NIOReactor
中有一个内部类 RW
,线程调用的是 RW.run()
。
注意,这里通过工厂类创建的 Connection
实际上是 MySQLFrontConnection
类,因为工厂类是传入的 MySQLFrontConnectionFactory
类。严格来说,该工厂类在设计模式上应该属于抽象工厂,其父类 ConnectionFactory
会通过 Connection make(SocketChannel channel)
方法创建 Connection
类,该方法代码如下,
该方法中,依次调用 makeConnection()
和 setHandler()
这两个抽象方法用于创建连接,并将请求处理类设置为 NIOHandler
,而子工厂类中,需要实现这两个方法用于创建连接实例。在 MySQLFrontConnectionFactory
类中,makeConnection()
方法创建了 MySQLFrontConnection
,NIOHandler
设置为初始化该工厂类时的 MySQLFrontConnectionHandler
。这两个类是处理 sql 请求的主要类,关于这两个类,后文再详细描述。
在 RW.run()
方法中,主要有一些 NIO 相关的调用,最重要的调用是 con.asynRead();
,该方法比较简单,重要代码如下,
先是将网络数据读入到 readBuffer
中,然后调用 onReadData()
。在 onReadData()
方法中,有很多读入字节的代码,重要的代码是调用 handle(readBuffer, offset, length);
方法,实际上是调用 NIOHandler
的 handle()
方法,也就是 MySQLFrontConnectionHandler
类的 handle()
方法。
至此,MyCAT 处理网络连接部分的代码已经完成,对于网络请求的数据已经完成读入到 Buffer,之后的代码入口在 MySQLFrontConnectionHandler.handle()
。
处理 SQL 请求的入口在 MySQLFrontConnectionHandler.handle()
,其中,根据 Connection 的状态,分别调用了 doConnecting
和 doHandleBusinessMsg
,doHandleBusinessMsg
方法主要用来处理 SQL 请求。该方法中,处理一般 SQL 的代码是,
其中的 source
是之前的 MySQLFrontConnection
类,也就是执行其中的 query(byte[])
方法,该方法中做了一些字符处理的操作,最主要的应该是 sql = mm.readString(charset);
这一行,用于处理字符集。最后调用了 query(String)
,该方法先执行了 SQL 的检查,最后开始真正执行 SQL,如下,
其中,用于执行一般 SQL 查询的是 SelectHandler.handle(sql, this, rs >>> 8);
,该方法中,对 SQL 的类型做了一些判断,一般的 SQL 会执行最后一行的 c.execute(stmt, ServerParse.SELECT);
(该方法属于 MySQLFrontConnection
类)。该方法中会检查数据库 Schema 的配置,最后调用 routeEndExecuteSQL(sql, type, schema);
,该方法对 SQL 进行路由(即寻找执行 SQL 的数据库),然后执行 SQL(PS:我猜测方法名应该是取错了,应该是 routeAndExecuteSQL
,而不是 routeEndExecuteSQL
)。其中重要的代码如下,
前一个调用是将 SQL 路由到数据库,后一个调用是执行解析之后的 SQL,这两部分的代码都比较独立,可以分别解析。其中,session
是 NonBlockingSession
类,该类有两个比较重要的 field,
source
是前端连接,表示 MyCAT 面向客户端的连接,target
是后端连接,表示若干个连接到后端 MySQL 上的连接。
SQL 路由的入口在 RouteService.route()
,该方法先判断 SQL 路由是否有之前解析的结果,如果有直接使用;否则,开始解析 SQL。解析 SQL 的时候,会先判断该 SQL 有没有 Hint,如果有,按Hint中指定路径进行解析;否则,调用 RouteStrategyFactory.getRouteStrategy().route()
方法寻找合适的路由。RouteStrategyFactory
是一个路由策略的工厂类,目前,MyCAT 中只有一个基于 druidParser
的路由策略,对应 DruidMycatRouteStrategy
类。
MyCAT 中的 SQL 路由相关的类有:RouteResultset
用于保存路由结果;RouteStrategyFactory
是路由工厂,生成 RouteStrategy
;RouteStrategy
是路由类最顶层的接口,其中只有一个 route()
抽象方法;AbstractRouteStrategy
是路由类的抽象类,实现了 RouteStrategy
,定义了路由的基本步骤,返回 RouteResultset
,其中,最重要的抽象方法是 routeNormalSqlWithAST
,基于 AST 树来寻找路由;DruidMycatRouteStrategy
继承了 AbstractRouteStrategy
,实现了 AbstractRouteStrategy
中的所有抽象方法。
DruidMycatRouteStrategy
的 routeNormalSqlWithAST
方法中,有以下一些比较重要的调用,
由于 MyCAT 使用的是第三方的 Druid SQL 解析工具,因此要在 Druid 解析器中加入自己的处理,这里,Druid 解析器使用了 visitor 模式,MycatSchemaStatVisitor
类继承 MySqlSchemaStatVisitor
并实现了其中的多个重载的 visit
方法,在调用 druidParser.parser()
的时候进行计算。routeNormalSqlWithAST
方法的最后,解析之后的 SQL 被路由到若干个分片节点上,并保存在 RouteResultset
中,然后返回。
SQL 执行的入口在 NonBlockingSession.execute()
,该方法主要分两个分支,单节点 SQL 执行和多节点 SQL 执行,分别是 SingleNodeHandler
和 MultiNodeQueryHandler
两个类,在每个分支中,依次调用了 setPrepared()
方法和 execute()
方法。
对于单节点情况,SingleNodeHandler.execute()
先获取该单节点的 MySQL 后端连接,然后调用 _execute(conn)
,在 _execute(conn)
中,主要代码如下,
先将 SQL 返回的回调类设为 SingleNodeHandler
,也就是自己,然后调用后端连接 BackendConnection
类的 execute
方法,真正的执行 SQL。对于后端是 MySQL 数据库的时候,实际上使用的的是 MySQLBackendConnection
类,该类的 execute
方法调用了 synAndDoExecute
,并在 synAndDoExecute
中调用了 sendQueryCmd
,向 MySQL 发送 SQL 请求。对于 SQL 的返回,是实现 ResponseHandler
接口来实现的,该接口定义了不同的 SQL 返回处理方法。
对于多节点的情况,MultiNodeQueryHandler
类的基本流程和 SingleNodeHandler
一样,不同之处有几个地方。一个是,在 execute()
方法中,对每一个节点分别调用 _execute(conn)
执行 SQL;还有一个是,回调的接口实现要更加复杂一些,例如,rowEofResponse
接口的实现中,调用了 DataMergeService
,用于合并多个数据库上查询返回的结果。
总体大致的结构图如下,比较粗略,
]]>系统的入口方法在 io.mycat.MycatStartup
中的 main
方法,主要代码]]>
如果说Mycat有一些不足的话,那就是,在整个系统中,Mycat会成为一个单点。因为所有的sql都会通过Mycat来路由,在数据库比较多的情况下,Mycat本身的cpu性能压力就会随之增大。因此,在生产系统中,Mycat不可避免的会需要一些多活的高可用手段。同样,由于Mycat本身需要解析sql,也需要合并各个数据库返回的结果,本身的CPU消耗就会比较高,在数据库较多的情况下,CPU可能不堪重负。
因此,在数据库比较多的情况下,生产环境的部署可能是这样的,
按之前的讨论,Mycat 会成为系统的单点,性能压力比较大。如果,Mycat 可以开发一个嵌入式 Mycat 系统,将 Mycat 代码嵌入在每个客户端中,这样,原先在 Mycat 上的集中压力就分散到了每个客户端上。当然,这样的架构需要对 Mycat 做一些改造,比如需要引入配置中心概念,将原来的分库分表配置集中管理,这样,每个客户端上的配置就一致了。同时,如果配置发生变化,还需要同步给每个客户端。引入嵌入式 Mycat(Mycat Embeded)之后的系统架构可能如下(图中略去了配置中心部分),
当然,目前而言,Mycat 的优势还是非常明显的,相比与嵌入式 Mycat,现有的架构对原有系统的侵入非常小,现有架构模拟了 Mysql 数据库的接口,应用系统完全不用引入新的依赖,只需要改写部分 sql 即可。
]]>Mycat-eye 运行过程中需要依赖 zookeeper,因此需要先安装 zookeeper,我安装的是 zookeeper-3.4.8。
先下载 zookeeper-3.4.8.tar.gz,然后解压,在 conf/
目录下找到 zoo-sample.cfg,将其复制为 zoo.cfg。其内容主要如下,
要记得的是端口号2181,启动 Mycat-eye 的时候会用到。然后启动 zookeeper,启动的入口在 bin/
目录下,
可见,在启动的过程中,读取了刚才配置的 zoo.cfg。由于仅仅是实验,我们这里只使用了 zookeeper 的单机(standalone)模式。
然后安装 Mycat-eye,我安装的是 Mycat-web-1.0-SNAPSHOT-20160331220346-linux.tar.gz,同样需要先解压,解压后得到 mycat-web 目录。Mycat-eye 的配置文件在 mycat-web/WEB-INF/classes/mycat.properties
,确认其中配置的 zookeeper 地址正确,如下,
然后可以启动 Mycat-eye,如下,
首先要登陆 Mycat-eye,浏览器打开页面 http://localhost:8082/mycat/
,即可看到初始界面,
登陆之后,可以配置 Mycat 连接,在“mycat服务管理”中点击“新增”,
也可以配置 mysql 连接,在“mysql管理”中点击“新增”,
还可以查看 Mycat 的系统参数和日志,在“mycat系统参数”和“mycat日志管理”中,
使用之前做压力测试的脚本运行多个类似 select * from travelrecord where id = ?
这样的查询,查看 Mycat-eye 的监控数据。首先是“mycat性能监控”和“mysql性能监控”这两个菜单,
这两个菜单列出了 Mycat 的线程、TPS、内存等信息的时间变化图,以及 mysql 的缓存命中率、数据发送接收速度、线程、关键事件、临时表、恶性表联接等的统计数据。
还有专门针对 sql 的监控数据,在“SQL统计”、“SQL表分析”、“SQL监控”、“高频SQL”、“慢SQL统计”、“SQL解析”等这几个菜单中。
其中,可能比较有用的有:“SQL表分析”可以列出 sql 的读写比例;“高频SQL”可以列出 sql 的使用频率;“慢SQL统计”可以列出执行时间比较长的 sql。另外,“SQL解析”可以在线分析一个 sql 的执行计划,省去了使用 mysql 客户端的麻烦。
此外,在“高频SQL”中,点击“分析”,还可以查看某个 sql 的请求数变化情况,如下,
总而言之,Mycat-eye 还是一款比较不错的监控工具,上手也比较简单。
]]>Jconsole 是 Java 自带的性能监控工具,可以监控 Java 程序在运行过程中的 CPU、内存等的使用情况。
如果要使用 Jconsole 来监控 MyCAT 的运行状况,需要添加 MyCAT 的运行参数,在 conf/wrapper.conf
中,需要修改以下参数,
其中,前11个参数是 MyCAT 默认的参数,最后一个参数 -Djava.rmi.server.hostname=192.168.2.201
是我本地的 rmi 监听 IP,即使用 Jconsole 远程连接的 IP。以上参数中,与 Jconsole 监控有关的参数还有以下几个,
其中,-Dcom.sun.management.jmxremote
表示启用远程 jmx 监听;-Dcom.sun.management.jmxremote.port=1984
表示监听端口是1984;-Dcom.sun.management.jmxremote.authenticate=false
表示不启用登陆认证;-Dcom.sun.management.jmxremote.ssl=false
表示不启用 ssl 加密连接。因此,Jconsole 的远程连接地址就是 192.168.2.201:1984
。
然后,运行 Jconsole,输入连接地址即可登陆开始监控,如下,
登陆之后,可以看到 MyCAT 进程使用CPU、内存的情况,如下,
运行 MyCAT 自带的性能测试工具 testtool 来进行此次测试,测试用的表是 travelrecord,共有10个分片,采用主键 mod 10 的算法来执行分片,初始状态是空表,mysql 采用5.7版本,只有1个 mysql 实例,上面有10个数据库,本次测试固定100个连接。
先运行一个10000数据量的插入,得到在我的测试机上,单表插入 tps 大约是1600,如下,
再做一个单表查询的测试,基于之前已经插入的表,qps 大约是5000左右,如下,
其中,参数1000表示,每个线程执行1000次查询,而非总共执行1000次查询。travelrecord_select.sql 的内容如下,
设计一个实验场景,先执行大约10分钟的插入,再 sleep 30秒,最后执行大约10分钟的查询。根据之前的测试数据,大约需要插入90万(15006010)条数据,执行300万(50006010)次查询(每个连接3万次查询),这个估算是根据数据量的增大仍然不影响 tps 和 qps 的基础上来进行的,实际上,数据量大的时候,插入和查询的效率都会受到影响,实验中,执行50万次插入,300万次查询。因此,编写测试脚本 perf-test.sh 如下,
但是,MyCAT 的工具似乎有问题,在100个并发连接下,不能执行16000条数据以上的插入,否则会报错如下,
怀疑是并发连接数量到达上线的原因,按官方文档的说法,在 schema.xml 中,增大了 minCon 参数,仍然不起效果,因此,实际运行的是如下脚本,
travelrecord_select.sql 脚本也需要修改查询范围,如下,
正式执行之前,可以先执行上述脚本一次,然后清空 travelrecord 表,再重启 MyCAT,这主要是用来预热 mysql,并将 MyCAT 重置,最后连接 Jconsole,开始监控。
在运行测试的过程中,使用管理端登陆可以看到进程的信息,如下,
Jconsole 的监控截图如下,这里只做了30分钟的监控,整个测试运行了约26分30秒(包括其中 sleep 的时间),
从上述图表可知,内存的使用虽然波动比较剧烈,但是总体比较稳定,线程数量基本没有变化,CPU 的使用上,插入的时候比查询的时候用的要少,这应该是因为查询的 CPU 消耗比较密集,而插入的瓶颈仍然在 IO 部分。
]]>Jconsole 是 Java 自带的性能监控工具,可以监控 Java 程序在运行过程中的 CPU、内存等的使用情况。
如果要使用 Jconsole 来监控 MyCAT 的运行状况,需要添加 M]]>
ku8eye 是使用 docker 镜像来安装运行的。先下载 ku8eye,官方发布在百度网盘,我下载的是 ku8eye-web-0.6.tar.gz。
用gunzip解压缩后,得到文件ku8eye-web-0.6.tar(2.1G)。导入docker镜像,并给该镜像打上tag ku8eye-web
,
实际上,默认情况下,已经是 ku8eye-web 命名了,不需要 tag。
运行开发环境,docker run -tid --name ku8eye-web -p 3306:3306 -p 8080:8080 -p 9001:9001 ku8eye-web
,其中 3306 为mysql服务端口,8080 为tomcat服务端口,9001 为supervisor服务端口,均映射到宿主机上。
由于我的机器上8080端口已经被占用,这里用8081端口代替。
用网页的方式登陆宿主机的8081端口,例如,我的是 http://192.168.2.202:8081,会看到登陆界面,如下,
还挺好看的。然后用账号密码 guest/123456
登陆即可进行管理,点击左侧“资源管理”菜单,选择“集群安”装进行安装。如下,
我选择的是”All In One Cluster“,因为我没有那么多虚拟机...... 这样就可以在本机安装 Kubernetes Master 等,如下
点击”开始安装“即可。不知道为什么总是提示安装失败,可能与我已经安装过 kubernetes 有关。
ku8eye 的应用管理功能,感觉上和直接写 kubernetes 的 yaml 文件比较类似,如下,
将需要的要素填入,即可工作。
]]>数据迁移的第一步就是使用 MyCAT 作为中间件隔离 Oracle,这其中可能涉及到部分 sql 和应用的改写。配置如下,
完成 MyCAT 的配置之后,应用程序将看不到 Oracle,后续的数据迁移对应用是透明的,此时,架构图如下,
假设有 Oracle 中有3张表,用户表(c_user)、交易表(c_order)、转账表(c_transfer),需要迁移用户表和交易表到 mysql 中。这3张表在当前的 MyCAT 中可以做如下配置,
假设 Oracle 中,上述3张表的建表语句如下,
为了便于演示,测试数据库中的数据量比较小,每个表只有1000条,不过这不影响操作结果。另外,为了测试迁移中中文编码可能出现的问题,部分字段使用了中文,数据库字符集是 GBK,如下,
在生产环境中,应该小步前进,对于数据比较多的表,逐个迁移(如果 join 的 sql 不多的话)。实验中,我们采用一次迁移两张表的做法。
将数据从 Oracle 中导出,采用 spool 方法,这主要是方便 MyCAT 后续导入,sqlplus 相关语句如下,
用这样的方法,导出的数据会比较整齐,而且连字符集也统一处理了(只要 sqlplus 的客户端字符集配置的对),如下,
同样的方法可以导出另一个表的数据,在此不再赘述。
首先,先要在 MyCAT 中配置将要导入的数据库和表,假设导入的数据要分片到10个 mysql 数据库中,可以如下配置,
迁移的2张表采用 id 取余数算法分片,另外的表不做变动,如下配置,
注:此处采用简便的分片方法,实际上,这里应该使用ER分片会更好。参考另一篇文章《MyCAT 分片》。
然后,重启 MyCAT,或者使用管理端的 reload @@config_all
,使用如下语句建表,
MyCAT 支持类似 mysql 的 load data,而且支持在导入的过程中完成数据路由,操作如下,
其中,有一些地方需要特别注意。
--local-infile=1
,否则不能导入,而 jdbc 不需要加这个参数(我估计没人用 jdbc 做这样的操作);(id, user_name, passwd, nick_name)
;SET NAMES 'utf8';
,将客户端字符集统一设置成 utf-8,如下,与上面相同的方法,可以导入另一张表,如下,
每个分片上有100条数据,如下,
至此,MyCAT 已经可以将数据路由到新的 mysql 服务器上,迁移完成。
如果不常用 mysql 的人,做 load data 应该会碰到挺多问题,在加上 MyCAT,估计一下很难发现问题,比如下面这个问题,
从字面上感觉是数据格式出错了,实际上是 mysql 客户端登陆的时候没有加 --local-infile=1
参数,而且这个问题会导致 mysql 客户端卡死,并断掉连接。还有,如果导入的时候没有加列名,也会提示很诡异的错误,
这个错误也会导致断掉客户端连接,甚至还会打印很多类似这样的日志,
如果从日志看的话,可以发现是 sql 里面缺少列名。因此,只有按照 MyCAT 官方文档的步骤,一步一步来,不可逾越。
]]>当service有了port和nodePort之后,就可以对内/外提供服务。那么其具体是通过什么原理来实现的呢?奥妙就在kube-proxy在本地node上创建的iptables规则。
Kubernetes为每个service分配一个clusterIP(虚拟ip)。不同的service用不同的ip,所以端口也不会冲突。Kubernetes的虚拟ip是通过iptables机制实现的。每个service定义的端口,kube-proxy都会监听一个随机端口对应,然后通过iptables nat规则做转发。比如Kubernetes上有个dns服务,clusterIP:10.254.0.10,端口:53。应用对10.254.0.10:53的请求会被转发到该node的kube-proxy监听的随机端口上,然后再转发给对应的pod。如果该服务的pod不在当前node上,会先在kube-proxy之间进行转发。该转发完全通过iptables实现。
Kube-Proxy 通过配置 DNAT 规则(从容器出来的访问,从本地主机出来的访问两方面),将到这个服务地址的访问映射到本地的kube-proxy端口(随机端口)。然后 Kube-Proxy 会监听在本地的对应端口,将到这个端口的访问给代理到远端真实的 pod 地址上去。
如果 kubernetes 的启动参数中有 --logtostderr=true
表示使用 systemd 接管 kubernetes 的输出,可以用 journalctl 查看,如下,
从 log 中可以发现,刚刚创建了一个 redis-master-xuv93 的 Pod,这个 Pod 中运行了一个 redis。通过 docker ps
可以查看到对应的容器,
可以发现,当前有2个容器在运行,56f58c6fb142 就是 redis-master-xuv93 这个 Pod 对应的容器;另一个是 kubernetes 的控制节点,是 Pod 网络访问代理。可以用 docker inspect
查看这两个节点的 IP ,
从容器的信息上来看,redis 容器 56f58c6fb142 本身并没有 IP 地址,但是,通过容器内部互联,监听了 10.254.225.16:6379
这个 虚拟IP+端口,因此,直接访问 10.254.225.16:6379 即可,如下,
另外,由于 d377dcefdbc5 是 redis 的访问代理,因此,访问这个地址的6379端口,也是可以通的,如下,
可以验证,这个 redis 服务的确是在监听 10.254.225.16:6379
,只不过这个是 kubernetes service 的虚拟 IP,
通常情况下,kubernetes 的负载均衡方式是如下进行,
由 kube-proxy 进程负责将请求转发到每个工作 Pod 上,好处是对客户端透明,坏处是多了一次转发,性能上有所损耗。另一种办法是采用客户端负载均衡方式,如下,
客户端先去向 apiserver 询问可以服务的工作 Pod 的地址,然后直接访问该地址,这样的好处是少了一次转发,坏处是客户端需要做一些逻辑判断。
如果使用客户端负载均衡的方式,那么就可以用类似上述的方法来获取服务真正的监听端口。如果需要直接访问该 IP 的话,需要在访问端增加路由规则,如下,
也可以使用 redis client 来访问,
这样,外部系统就可以直接访问 Pod。
]]>当service有了port和nodePort之后,就可以对内/外提供服务。那么其具体是通过什么原理来实现的呢?奥妙就在kube-proxy在本地node上创建的iptables规则。
<]]>