taoCMS-基于php+sqlite最小巧的CMS http://www.taocms.org/ taoCMS是基于php+sqlite/mysql的国内最小(100Kb左右)的功能完善的CMS管理系统 2018-04-20 taoCMS-基于php+sqlite最小巧的CMS 1144 有的公司看起来人多热闹,实际上成了老板一个人的战斗|

人人都希望做好人,但这种好发展到极致在团队中就意味着“无原则的妥协”。以至于有的公司看起来人多热闹,实际上成了老板一个人的战斗。

 

显然,这种管理上的过于宽容,非常不利于团队的长远发展,如果再遇到业务发展这样的根本性问题,有的公司甚至因此倒闭。这也是为什么互联网公司常常强调狼性文化,希望员工拥有不惜加班也要使命必达、坚决完成任务的状态。

 

但从员工的角度来说,人性里天然期待着更宽松、更自由的管理。极端者则容易对上司、对老板有天然的敌意,管理严一点,加班多一点,就认为是苛刻和压榨。

 

对于管理者来说,挑战也来自于这里——公司会存在这种天然的对立;但从另一个角度来说,个人福祉和公司发展又一定是密切相关的,让个人充分发挥特长,凝聚共同的愿景和一致的努力方向,建立团队合作,协同个人目标和共同福祉。

 

管理往往是“菩萨心肠”与“霹雳手段”的一体两面;而对员工来说,每一段时光也终究是职业生涯的一番经历,那就做喜欢的并全力以赴,以及保持独立的判断和进取的心气。以下,Enjoy:




来源 / 跨越式企业(ID:dfy82853236)

作者/ 跨越式发展理论创始人·东方赢

01

搞死企业的三个问题

宽容、假装高逼格、除了老板没人加班


我的学生是一家公司创始人,曾给我讲过他的一个创业失败故事。


前几年,他公司在国內推出了一项互联网创新业务,一开始比较受欢迎,还得到了几笔投资。但好景不长,没多久就被另一家资金实力不如他们的小公司赶超了,大多数用户被吸引过去了。因为这家公司的产品更贴近用户需求,迭代速度快。


他后来反省失败的原因,认为有三点:


一是研发项目负责人有老好人习气,搞一团和气,明知几位主要技术开发人员知识老化并与目标用户有代沟,但一直没有换人。还有对团队存在的问题也做不到及时彻底纠正。


二是公司用高薪和高档办公环境招聘一批人,搞什么去KPI,却缺乏合适的管理,导致效率不高,烧钱飞快。


三是团队缺乏创业激情,该抢时间赶速度该加班的时候却几乎没人加班,使产品迭代与市场推广的速度总是落后于竞争对手。


所以我曾总结,致使企业失败的原因有三个:宽容、假装高逼格、除了老板没人加班。


实质上这三个问题都是管理上宽容的问题。


但有些人不赞同我的观点,他们认为成功的公司是宽容的。


真的是这样吗?




02

成功的公司

对员工都不够宽容


不赞同我观点的人,喜欢例举这几个他们认为是宽容的成功公司,如谷歌。


实际上,这些人眼里的所谓宽容的公司,都是最不宽容的公司。


他们只从网络文章上看到了谷歌上下班不打卡,员工还能在办公室带孩遛狗,没有KPI等零碎现象。但他们不知道谷歌还有以下一系列不宽容到严苛的管理做法。


谷歌最聪明与最牛x的一招,是把这种不宽容安排在管理的源头上。每个参加Google面试的人至少要先后与六位面试官交谈过,后者均是公司管理层面或潜在同事组成的。每个人的观点都算数,从而使招聘程序更加公平,标准更高、更严格。所有面试官的目的就一个,将那些工作积极性与学识差的和不如他们的人剔除出去,不让其混入谷歌。


那些羡慕谷歌“宽容”而抱怨自己公司严苛的员工,其中绝大多数人都可能因为不够格,在招聘环节就会被谷歌极不宽容的严苛面试而干掉,根本不可能有享受那份“宽容”的福分。而那些进入谷歌的员工,都是经受了学校与其他公司严酷的竞争与管理环境并从中打拼出来的佼佼者。


他们主动、努力、有责任心、能力强,谷歌的那些宽容是留给这些人的。


既使拥有这样的优秀人才,谷歌仍然没有放松管理。谷歌创始人拉里·佩奇和谢尔盖·布林曾想过,我们招聘来的员工都是最聪明和最努力的人,我们干嘛还要派人去管理他们?于是两位创始人曾一度取消工程师们头上的管理者,但后来发现行不通,又恢复了管理层。


谷歌虽然不搞KPI,但推行的是比KPI目标要求更高、过程管理更严的OKR(目标与关键结果)管理体系。

 

我的一个咨询客户,公司是行业老大,是外人眼里的“宽容”公司,上下班不打卡,不搞KPI。但我知道这公司实际上更严苛,你绩效好,一切好说,如果绩效不好又不努力,根本不给你改正的机会,立即辞退。



03

只有老板一个人在战斗

为什么成功的公司都不宽容?


每一个老板心中都有个挥之不去的痛:老板都面临用人的双重压力与困境。


一方面,用人相关成本越来越贵,利润越来越薄。


另一方面,由于老板没退路但员工有退路;由于企业是老板的目的与命,但对员工而言未必是这样的,一份工作是可以牺牲与替换的;由于人皆有自私、惰怠的一面。所以难免发生这样的情况:老板在拼命,员工只求工作交差;管理者做老好人而老板做恶人;员工不奋斗只有老板在加班;员工出工不出活、不用心或偷懒耍滑到处埋雷,最终由老板担责买单。


有的公司看起来人多热闹,实际上成了老板一个人的战斗。


所以讲,用人及其相关投资,既是企业以小博大的创造性机会和最重要的驱动力,又是企业最大的不确定性风险!买房租房买机器设备,都会得到相应价值,但购买人工劳动,如果管理不当、不严,可能血本无归甚至带来破坏作用。


因此用人成了影响当今公司成败的主要因素。


而管理上是严格,还是宽容?决定着用人效率的高低与企业成败。只有严格而不宽容的管理,才能控制这种不确定性风险。




04

成功的公司

没有不是“奋斗文化”的


《对员工宽容的公司都死掉了》一文还引发了一个争论:员工该不该加班?


正如马云所言:我们不是在乎你加班这点工作,我们在乎的是你是否在乎你的工作,如果你在乎、热爱你的工作,你就会早起晚归。


在商界几落几起并见多识广的老板史玉柱,曾感慨道,“只要是伟大的公司,没有不是狼文化的。兔子窝文化肯定要失败。尤其搞互联网,竞争这么激烈,如果不是狼文化,肯定是死路一条。


什么是“兔子窝文化”?


管理者做老好人;

员工只图安逸,不热爱工作,不认真负责;

只求完成不求完美,不出业绩;

在学习和工作上不求进取,不想奋斗;

该加班时不愿加班;

该拼搏的时候不敢拼搏。


那些成功的公司其“不宽容”的本质是什么?


是对兔子窝文化零容忍,是对“奋斗文化”的信仰与追求。


奋斗文化,也是“狼性文化”。拥有奋斗文化的团队具备以下几个特征:


居安思危,追求持续竞争优势;

有责任心,并热爱工作,追求成就感;

能够延迟满足,控制欲望;

自觉,勤奋,上进。这种作风落实下来,便是养成“自己找目标与突破口”与“今日事今日毕、不惜加班也要使命必达、坚决完成任务”的习惯;

敢拼,能够屡败屡战、不达目的不罢休;

求真务实;

善于学习、创新,对机会反应灵敏。


真正的严格与不宽容,不是表面形式上的一时的斤斤计较,而是要求团队骨子里的改变与成长,是一边实施末尾淘汰、一边广泛寻找"德勇智综合人格"的优秀人才,形成以优秀的核心团队为中坚的奋斗文化。


没有这种奋斗文化,企业就会变成老板孤身奋战与烧钱的游戏,社会就会缺乏引领力量,人生就没有真正的幸福。


当然,这种奋斗文化的形成,还需要企业建立老板与员工共苦同甘的命运共同体与相应的激励机制。




05

对你不够宽容的上司

不是压榨,而是你还不够优秀


以前,我公司有一位员工到我办公室来告他部门经理的状,说这个上司对他管理苛刻、压榨。


我问他,怎么个苛刻?怎么个压榨?


他说,经理有好几次下班后,叫他留下加班;有时还批评他,口气很凶;一年活没少干,年终奖却给的是部门最低的。


我后来找到他的经理了解情况,经理解释说,让他加班是他白天上班活没干好,留下他返工。批评他,是因为他几次犯同样的错误,语气重了点。而年终评奖给得少,是因为他的绩效考核排名在部门是最低的。


为避免偏听偏信,我又找了人力资源部负责人核实,情况确实如此。


看来,有的员工对上司、对老板有一种天然的敌意,管理严一点,就认为是苛刻,是压榨。


从理性上看,劳动力也是一种商品,要求劳动创造的价值与价格(即薪酬)相等。公司管理的严格与不宽容,代表着市场与契约的严肃,当你的劳动价值少于你拿到的价格(即薪酬),市场就会要求你补齐,或斥责你处罚你,当你觉得干得多但拿得少或不如别人多时,说明你只有苦劳没有功劳,或者不如别人优秀。


不要玻璃心,动不动情绪化,抱怨别人,更不要幻想哪里有什么无缘无故给你待遇又好又宽容的公司。市场是公平的,也是不相信眼泪的。你如果不努力,不能提高自己的能力与劳动价值,不能使自己变优秀并成为不可替代的人,你跳槽到任何公司都会被不宽容对待。


优秀的人,都不害怕也不抱怨那些不宽容与严格的管理。


因为他们都是经历了一次又一次不宽容与严格甚至严苛的管理锻炼、考验的人,他们养成了自己对自己不宽容的习惯,其奋斗态度与能力及绩效达到或超过了上司的要求,而最终赢得上司的尊重与信任,成为自我管理者。



06

善意的领导,每一次批评都是挽救

都是防微杜渐、不舍放弃的爱护


我察觉到所谓“人性化管理”存在被误读的趋势,不少人把宽容与人性化管理等同起来。


有一次董明珠在一个部门巡视时,发现一个办公桌是没人的,然后就问这个部门经理今天这个员工是否没来,部门经理回答是的,他今天请假了。


董明珠继续追问,他请的是什么假,部门经理说他请的是病假。董明珠还继续追问他生的什么病,病得怎么样呢?是不是住院了?经理说我不知道,董明珠立即批评这位部门经理。


“你连这个都不知道,你凭什么去管员工,员工生的是什么病都不知道,你的心里有没有员工,你有没有关爱你的同事、关爱你的团队、关爱你的下属。”


董明珠这个严厉批评,看起来是小题大作的苛刻,实质上是防微杜渐的爱护,是对员工和工作滑向不利局势的挽救。


我在想,任正非、董明珠及我认识的不少企业家为什么都常在管理中使用严厉批评的手段呢?


“玉不琢,不成器”。从管理角度看下属犯错误的时候,一方面说明其方法或行为习惯及思维方式有问题,应该纠正;另一方面这错误或多或少造成了公司损失,二者都需要给下属加以一定处罚以达到管理的效果。


以我的管理经验而言,管理者对釆取什么处罚往往心里是纠结的,罚款?不忍!辞退?不舍!几者权衡最终择其轻,严厉批评罢了,让下属长长记性。我揣测这些老板们大抵也是这么个心理吧。


对下属严厉的上级,说明领导还关心他,还想培养他,还愿意给他进步与成长的机会。如果下属犯了错领导不吭声,说明领导不在乎他,或者说在心里己经放弃了他。




07

做人可以宽容

做事不可以宽容


“做人可以宽容,做事不可以宽容”,其意是这样的:


自己做人,别人犯错侵犯了你,你可以选择容忍与原谅。而为别人做事,无论你是老板还是管理者,在为股东、用户、社区、员工承担社会责任时,你对下属的工作问题是不能宽容的。


一是对“人性恶”如自私恶念与惰怠导致的差距与错误,不能宽容;


二是对“人性善”所驱使的行为,如努力工作、学习创新中难免发生的差距与错误,可以原谅,也允许犯错,给予改正与成长的机会,但不能容忍,须加以指出、督促或纠正,须反复抓、抓反复。


管理者对下属工作问题的宽容,其实是对用户、对股东、对员工集体的不负责任或利益侵犯。



08

管理的本质就是不宽容


我还在想,为什么那些成功的公司都不宽容?


这是因为"成功法则"在起决定性作用。


"成功法则"告诉我们,只有持续达到市场竞争优势与竞争要求的企业才能成功,否则被淘汰。


选择宽容还是不宽容?实际上由不得老板们,而是市场决定的,由企业外部市场的四周竞争方如用户、供应方、同行、潜在竞争者、跨界竞争者来决定。


所以管理者与管理必须向每个员工传递市场竞争要求与竞争压力,采取不宽容的严格姿态,使员工放弃一定程度的安逸与自由,调动起最大努力与最大热情,服从集体行动统一的竞争求胜的绩效目标、标准、期限、劳动投入需要、纪律,并解决达不到竞争要求的差距与错误,


这样,才能最终取得公司与员工的共同成功,才能最终使每个人从不自由达到自由。


看来,管理的本质可能就是不宽容。


而宽容的管理,是没有及时察觉与体现市场的意志,是对“人性恶”与员工负面表现及公司市场竞争问题的妥协与放纵,这是一条死路!


作者简介: 东方赢,跨越式发展理论之父,真业心学倡导者,曾担任大型实业公司总裁,出版著作《企业超速成长》、《跨越式战略》,2007年中国经济十大新闻人物,微信公众号"跨越式企业”出品人。

]]>
taoCMS-基于php+sqlite最小巧的CMS 2018-04-11 22:04:34
1143 如何跟朋友保持或加深关系?
  • 当朋友向你寻求帮助时,请小心提供建议。试着不要使用以下的句子:“你应该”“你需要”和“你必须”。相反地,问一些让他们可以自己找出答案的问题。你的朋友、家人以及同事会很感激你没有指指点点而是聆听他们的想法。

  • 当别人告诉你好消息的时候注意一下,练习完全投入的状态。把手机拿走,合上电脑,停下手中的事,用微笑和真诚的祝贺甚至是一个拥抱来享受别人的喜悦。如果不够专注地分享别人的喜悦,那么下一次,他们就不会再把好消息告诉你。

  • 为你的人生创建一块意见板。寻找好的聆听者以及尊重你的价值观并不尝试改变你的人。选择和你对这个世界看法不同的人。向他们寻求意见,多和他们相处。

  • 制作一张重要朋友的清单。大概10个人或者更少,问问他们对你的需求是什么,并给予。

  •  找一个和你意见不一致的人。下一次你们就某事讨论的时候,试着改变你的目的,不为赢,不为正确与否,也不强迫别人做事(是的,哪怕是你的孩子们,特别是你的孩子们!)。相反地,试着只是简单地对他们的立场表示好奇,试着多学习少说话。

  • 最近看过一部好电影吗?或者看过一本好书还是有趣的表演?和别人分享这些经历。调查表明我们通过与人分享,可以保持积极情绪,甚至强化我们的情绪。

  • 找到一个组并加入。和别人一起赢得胜利更加有趣,和别人一起分享失败也更具意义。不要忘记享受过程。

  • 找一个你并不是特别信赖的人,向他寻求帮助。这是展示脆弱的简单方式,也是修缮关系的第一步。

  • 找一个曾经帮助过你,但是你还没来得及感谢的人。给他写一封感谢信,提到他们给予的帮助,对你给他们带来的不便表示抱歉,以及这份帮助对你所产生的重大的、积极的影响。当面把这封信给他们并读出来,即使你见不到他们,也要写这封信。

  • 当与别人在一起时,要高度注意自己手机的使用。试着把手机放到别处,要享受和别人相处的时光。

  • 下一次如果想给朋友发短信,改成打电话吧。下一次想给朋友打电话问候的时候,试着改成拜访他吧。

  • 去帮助别人。给自己一个放松的机会,比方说请一天假,放弃一场演出或者少锻炼几个小时。

  • 在你的社交网络里找一个可能会面临困难的人,手写一些鼓励的话送给他。

  • 选择一个可以和别人一起去玩的爱好。比如去去学柔术、健身、羽毛球之类的,去加一些社团,你能参加时结交不少新朋友。

  • ]]>
    taoCMS-基于php+sqlite最小巧的CMS 2018-04-07 00:04:59
    1141 为什么在职场上不要锋芒毕露?

    有时候,我常常会想,这个世界究竟是不是公平的?

     

    春节我在老家,来拜年的五岁小朋友支支吾吾地从嘴里挤出一句“锄禾日当午”,能赢得满堂喝彩,而我就算把金融政策倒背如流也没人夸我聪明。

     

    有一天,办公室的保洁阿姨对同事说了一句“Good morning”,大家都夸赞阿姨真潮,我用法语来一句“Bonjour”,也没人夸我是个潮男,反得吃一个“装X”的标签。

     

    我们可以在生活中,轻松取得相比他人更高更好的成绩,但是却没有得到更好的赞赏,所以这世界不公平,对吗?

     

    我们都忘记了一点:任何成绩,都会因为实现的人的不同而有不一样的效果

    同样的道理,这种规则也适用于职场。

     

    想想看,你每天在公司干着三、四个人的活儿,给客户陪笑、跟老板吹牛,还得给手下画饼。是不是觉得自己励志到爆炸?

     

    可为什么偏偏就没人来称赞自己呢?

     

    其实,我的员工也常常这么怼我:“老板,我一个人当3个人用,你为什么经常看不到呢?”

     

    于是我开始反思。


    今天的这篇文章,其实是我梳理自己的老板思维后,想跟你们说的职场套路之——为什么在职场上不要锋芒毕露?

     

    其实,每个老板心中都会对员工有一个理所当然的标准,你现在的工作在老板眼中,就是“理所当然”。

     

    只有你完成了别人觉得你做不到的事情时,才会产生对你的反认知,从而称赞。

     

    但是,这里有一个问题,我们每天都努力做到最棒最好,完成上级交待的所有任务,怎么样才能发掘出一个“不可能完成的任务”呢?

     

    答案是:你得先学会隐藏自己。

     

    之前我曾在文章中提过:用户预期管理。


    在职场中,你不能不知道:老板的预期管理。

     

    在公司对一个岗位产生需求时,人事和老板会一起设计岗位的职责,他们的心中早已经对适应这个职位的人有一个大概的画像。

     

    现在很多的实习生,跑来我这面试,简历上满满当当的,恨不得把这辈子所有的经历都写上去,要是A4纸够大,他或许连幼儿园得了几朵小红花都贴上去。

     

    有用么?的确有,哪个老板不希望自己招的员工,一个顶两个,德智体美全面发展。

     

    可对你而言,进公司时你已经那么优秀了,所以别人的业绩叫业绩,你的业绩就叫本分。

     

    所以,面试之前的你,要好好地熟读岗位描述,了解公司,心里一定要先装上一瓶水:他们公司要的人是什么样的?

     

    然后,你再稍微多表现,显出几个简单的加分项,就够了。

     

    最好的面试,不是表现成天下第一 ,而是“恰到好处”,刚刚好并且有小亮点。

     

    职场里,一开始就表现得太优秀,会造成一种自我伤害。

     

    就好比:要是18年俄罗斯世界杯,中国队能拿冠军,估计这一届队员人人都得名留青史,被称为伟大一代。就算是出个线,估计都能成个热点,被吹个半天。


    可要是兵乓球国家队,不管包揽了几个奖杯,打哭了多少个福原爱,国人们都会觉得,咱们兵乓球就该这么强,司空见惯了。


    当你老板很肯定,一个活儿你能干好,干好了他会觉得“我真牛,看员工真准”。干不好,他就会感觉到你是徒有其表,大失所望,随后你也就泯然众人了。

     

    而如果你的老板给你工作时,内心抱着一种“他行不行”的疑问,多半是机会来了。因为当你完成了这件事,老板心里想的就是:我靠,他还真行。

     

    这就是刮目相看了。

     

    就算真的能力有限,活儿没干好,老板的失落感也会大大减少,对你的责备也会转嫁一部分在自己身上,他会认为你还需要成长,自己太操之过急。

     

    这个时候你偏要抓住机会,主动承担责任,拿出职业担当,这在危机公关里叫作“Take Control”,也就是夺取事态主导权,操作得当或许还能弯道超车。

     

    说不定,他还能主动揽锅,未来会花更多的心思锻炼你。

     

    贯穿你整个职场人生的老板预期管理,就好比是海盗之间的寻宝游戏,你埋下宝藏,悄悄地流出线索,老板则循着线索自觉地发现你的亮点,每找到一个优点、每看到一件成功的活儿,他都会欣喜若狂。

     

    在职业贡献度标准的制定上,你得伸出一只无形的手暗中调控,永远让别人用你的价值观来评价你,你的每一次增长都值得他们赞叹。


    这就是示弱的力量。


    但是能力总要增长,职业贡献度也是有一个透明天花板的,当你达到一个相对高端的位置时,要想更上一层楼,除了自己出门创业,就只能静待时代的变动了。

     

    所以,你必须塑造自己的稀缺性。

     

    古代,任何一个人若脱离了基础生产资料——土地,都必须掌握并保密一门手艺,铁匠要是教会了所有人锻造的秘诀,大家都能锻铁,那么他的职业就不复存在了。所以厨师们才会拼命保密自己的菜谱,每个人都要有自己的稀缺性。

     

    职场中,你的稀缺性一般来自两个方面。 

    01

    技术垄断

    技术权威带来的铁饭碗是谁也无法撼动的,就好比房产公司一年要花十几万请一级建造师来挂牌。

     

    技术专业垄断,就是他们的第一稀缺性。

    02

      资源垄断及运用  

    职场中激烈的竞争本质,无非比两样,一比客户,二比渠道。谁客户多谁就牛,谁渠道多谁就能解决问题,两样合起来,都叫资源。

     

    成长的过程中,每一个职场人都要有意识地积累资源,从基础到高级,缺一不可。

     

    能把现有资源合理高效地整合运用起来,那么你身上就已经出现了自己层面的稀缺性又没有锋芒毕露。

     

    我可以合理地推测,当这样的人成长为公司高层,老板应该会很依赖他。

     

    最好的员工,都能解决问题;最会解决问题的员工,则能让老板没有问题。

     

    这个世界究竟公不公平呢?

     

    我想大抵上还是公平的,当一个人取得的成就超出了预期,他就是值得夸奖的。


    当一个员工能把老板的预期降低,他的本分都能变成业绩

    ]]>
    taoCMS-基于php+sqlite最小巧的CMS 2018-03-21 00:03:53
    1140 Apollo配置中心介绍 1、What is Apollo

    1.1 背景

    随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、参数的配置、服务器的地址……

    对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制……

    在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。

    Apollo配置中心应运而生!

    1.2 Apollo简介

    Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。

    Apollo支持4个维度管理Key-Value格式的配置:

    1. application (应用)
    2. environment (环境)
    3. cluster (集群)
    4. namespace (命名空间)

    同时,Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo

    1.3 配置基本概念

    既然Apollo定位于配置中心,那么在这里有必要先简单介绍一下什么是配置。

    按照我们的理解,配置有以下几个属性:

    • 配置是独立于程序的只读变量
      • 配置首先是独立于程序的,同一份程序在不同的配置下会有不同的行为。
      • 其次,配置对于程序是只读的,程序通过读取配置来改变自己的行为,但是程序不应该去改变配置。
      • 常见的配置有:DB Connection Str、Thread Pool Size、Buffer Size、Request Timeout、Feature Switch、Server Urls等。
    • 配置伴随应用的整个生命周期
      • 配置贯穿于应用的整个生命周期,应用在启动时通过读取配置来初始化,在运行时根据配置调整行为。
    • 配置可以有多种加载方式
      • 配置也有很多种加载方式,常见的有程序内部hard code,配置文件,环境变量,启动参数,基于数据库等
    • 配置需要治理
      • 权限控制
        • 由于配置能改变程序的行为,不正确的配置甚至能引起灾难,所以对配置的修改必须有比较完善的权限控制
      • 不同环境、集群配置管理
        • 同一份程序在不同的环境(开发,测试,生产)、不同的集群(如不同的数据中心)经常需要有不同的配置,所以需要有完善的环境、集群配置管理
      • 框架类组件配置管理
        • 还有一类比较特殊的配置 - 框架类组件配置,比如CAT客户端的配置。
        • 虽然这类框架类组件是由其他团队开发、维护,但是运行时是在业务实际应用内的,所以本质上可以认为框架类组件也是应用的一部分。
        • 这类组件对应的配置也需要有比较完善的管理方式。

    2、Why Apollo

    正是基于配置的特殊性,所以Apollo从设计之初就立志于成为一个有治理能力的配置管理平台,目前提供了以下的特性:

    • 统一管理不同环境、不同集群的配置
      • Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。
      • 同一份代码部署在不同的集群,可以有不同的配置,比如zk的地址等
      • 通过命名空间(namespace)可以很方便的支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖
    • 配置修改实时生效(热发布)
      • 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序
    • 版本发布管理
      • 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚
    • 灰度发布
      • 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例
    • 权限管理、发布审核、操作审计
      • 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
      • 所有的操作都有审计日志,可以方便的追踪问题
    • 客户端配置信息监控
      • 可以在界面上方便地看到配置在被哪些实例使用
    • 提供Java和.Net原生客户端
      • 提供了Java和.Net的原生客户端,方便应用集成
      • 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)
      • 同时提供了Http接口,非Java和.Net应用也可以方便的使用
    • 提供开放平台API
      • Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。
      • 不过Apollo出于通用性考虑,对配置的修改不会做过多限制,只要符合基本的格式就能够保存。
      • 在我们的调研中发现,对于有些使用方,它们的配置可能会有比较复杂的格式,而且对输入的值也需要进行校验后方可保存,如检查数据库、用户名和密码是否匹配。
      • 对于这类应用,Apollo支持应用方通过开放接口在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制
    • 部署简单
      • 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少
      • 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来
      • Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数

    3、Apollo at a glance

    3.1 基础模型

    如下即是Apollo的基础模型:

    1. 用户在配置中心对配置进行修改并发布
    2. 配置中心通知Apollo客户端有配置更新
    3. Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用

    basic-architecture

    3.2 界面概览

    portal-overview

    上图是Apollo配置中心中一个项目的配置首页

    • 在页面左上方的环境列表模块展示了所有的环境和集群,用户可以随时切换。
    • 页面中央展示了两个namespace(application和FX.apollo)的配置信息,默认按照表格模式展示、编辑。用户也可以切换到文本模式,以文件形式查看、编辑。
    • 页面上可以方便地进行发布、回滚、灰度、授权、查看更改历史和发布历史等操作

    3.3 添加/修改配置项

    用户可以通过配置中心界面方便的添加/修改配置项:

    edit-item-1

    输入配置信息:

    edit-item

    3.4 发布配置

    通过配置中心发布配置:

    publish-items-1

    填写发布信息:

    publish-items

    3.5 客户端获取配置(Java API样例)

    配置发布后,就能在客户端获取到了,以Java API方式为例,获取配置的示例代码如下。更多客户端使用说明请参见Java客户端使用指南

    Config config = ConfigService.getAppConfig();
    Integer defaultRequestTimeout = 200;
    Integer requestTimeout = 
             config.getIntProperty("request.timeout",defaultRequestTimeout);
    

    3.6 客户端监听配置变化(Java API样例)

    通过上述获取配置代码,应用就能实时获取到最新的配置了。

    不过在某些场景下,应用还需要在配置变化时获得通知,比如数据库连接的切换等,所以Apollo还提供了监听配置变化的功能,Java示例如下:

    Config config = ConfigService.getAppConfig();
    config.addChangeListener(new ConfigChangeListener() {
        @Override
        public void onChange(ConfigChangeEvent changeEvent) {
            for (String key : changeEvent.changedKeys()) {
                ConfigChange change = changeEvent.getChange(key);
                System.out.println(String.format(
                    "Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", 
                    change.getPropertyName(), change.getOldValue(),
                    change.getNewValue(), change.getChangeType()));
            }
        }
    });
    

    3.7 Spring集成样例

    Apollo和Spring也可以很方便地集成,只需要标注@EnableApolloConfig后就可以通过@Value获取配置信息:

    @Configuration
    @EnableApolloConfig
    public class AppConfig {}
    
    
    @Component
    public class SomeBean {
        @Value("${request.timeout:200}")
        private int timeout;
    
        @ApolloConfigChangeListener
        private void someChangeHandler(ConfigChangeEvent changeEvent) {
            if (changeEvent.isChanged("request.timeout")) {
                refreshTimeout();
            }
        }
    }
    
    

    4、Apollo in depth

    通过上面的介绍,相信大家已经对Apollo有了一个初步的了解,并且相信已经覆盖到了大部分的使用场景。

    接下来会主要介绍Apollo的cluster管理(集群)、namespace管理(命名空间)和对应的配置获取规则。

    4.1 Core Concepts

    在介绍高级特性前,我们有必要先来了解一下Apollo中的几个核心概念:

    1. application (应用)
      • 这个很好理解,就是实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置
      • 每个应用都需要有唯一的身份标识 - appId,我们认为应用身份是跟着代码走的,所以需要在代码中配置,具体信息请参见Java客户端使用指南
    2. environment (环境)
      • 配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置
      • 我们认为环境和代码无关,同一份代码部署在不同的环境就应该能够获取到不同环境的配置
      • 所以环境默认是通过读取机器上的配置(server.properties中的env属性)指定的,不过为了开发方便,我们也支持运行时通过System Property等指定,具体信息请参见Java客户端使用指南
    3. cluster (集群)
      • 一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。
      • 对不同的cluster,同一个配置可以有不一样的值,如zookeeper地址。
      • 集群默认是通过读取机器上的配置(server.properties中的idc属性)指定的,不过也支持运行时通过System Property指定,具体信息请参见Java客户端使用指南
    4. namespace (命名空间)
      • 一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,rpc配置文件,应用自身的配置文件等
      • 应用可以直接读取到公共组件的配置namespace,如DAL,RPC等
      • 应用也可以通过继承公共组件的配置namespace来对公共组件的配置做调整,如DAL的初始数据库连接数

    4.2 自定义Cluster

    【本节内容仅对应用需要对不同集群应用不同配置才需要,如没有相关需求,可以跳过本节】

    比如我们有应用在A数据中心和B数据中心都有部署,那么如果希望两个数据中心的配置不一样的话,我们可以通过新建cluster来解决。

    4.2.1 新建Cluster

    新建Cluster只有项目的管理员才有权限,管理员可以在页面左侧看到“添加集群”按钮。

    create-cluster

    点击后就进入到集群添加页面,一般情况下可以按照数据中心来划分集群,如SHAJQ、SHAOY等。

    不过也支持自定义集群,比如可以为A机房的某一台机器和B机房的某一台机创建一个集群,使用一套配置。

    create-cluster-detail

    4.2.2 在Cluster中添加配置并发布

    集群添加成功后,就可以为该集群添加配置了,首选需要按照下图所示切换到SHAJQ集群,之后配置添加流程和3.2添加/修改配置项一样,这里就不再赘述了。

    cluster-created

    4.2.3 指定应用实例所属的Cluster

    Apollo会默认使用应用实例所在的数据中心作为cluster,所以如果两者一致的话,不需要额外配置。

    如果cluster和数据中心不一致的话,那么就需要通过System Property方式来指定运行时cluster:

    • -Dapollo.cluster=SomeCluster
    • 这里注意apollo.cluster为全小写

    4.3 自定义Namespace

    【本节仅对公共组件配置或需要多个应用共享配置才需要,如没有相关需求,可以跳过本节】

    如果应用有公共组件(如hermes-producer,cat-client等)供其它应用使用,就需要通过自定义namespace来实现公共组件的配置。

    4.3.1 新建Namespace

    以hermes-producer为例,需要先新建一个namespace,新建namespace只有项目的管理员才有权限,管理员可以在页面左侧看到“添加Namespace”按钮。

    create-namespace

    点击后就进入namespace添加页面,Apollo会把应用所属的部门作为namespace的前缀,如FX。

    create-namespace-detail

    4.3.2 关联到环境和集群

    Namespace创建完,需要选择在哪些环境和集群下使用

    link-namespace-detail

    4.3.3 在Namespace中添加配置项

    接下来在这个新建的namespace下添加配置项

    add-item-in-new-namespace

    添加完成后就能在FX.Hermes.Producer的namespace中看到配置。

    item-created-in-new-namespace

    4.3.4 发布namespace的配置

    publish-items-in-new-namespace

    4.3.5 客户端获取Namespace配置

    对自定义namespace的配置获取,稍有不同,需要程序传入namespace的名字。更多客户端使用说明请参见Java客户端使用指南

    Config config = ConfigService.getConfig("FX.Hermes.Producer");
    Integer defaultSenderBatchSize = 200;
    Integer senderBatchSize = config.getIntProperty("sender.batchsize", defaultSenderBatchSize);
    

    4.3.6 客户端监听Namespace配置变化

    Config config = ConfigService.getConfig("FX.Hermes.Producer");
    config.addChangeListener(new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        System.out.println("Changes for namespace " + changeEvent.getNamespace());
        for (String key : changeEvent.changedKeys()) {
          ConfigChange change = changeEvent.getChange(key);
          System.out.println(String.format(
            "Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s",
            change.getPropertyName(), change.getOldValue(),
            change.getNewValue(), change.getChangeType()));
         }
      }
    });
    

    4.3.7 Spring集成样例

    @Configuration
    @EnableApolloConfig("FX.Hermes.Producer")
    public class AppConfig {}
    
    @Component
    public class SomeBean {
        @Value("${request.timeout:200}")
        private int timeout;
    
        @ApolloConfigChangeListener("FX.Hermes.Producer")
        private void someChangeHandler(ConfigChangeEvent changeEvent) {
            if (changeEvent.isChanged("request.timeout")) {
                refreshTimeout();
            }
        }
    }
    

    4.4 配置获取规则

    【本节仅当应用自定义了集群或namespace才需要,如无相关需求,可以跳过本节】

    在有了cluster概念后,配置的规则就显得重要了。

    比如应用部署在A机房,但是并没有在Apollo新建cluster,这个时候Apollo的行为是怎样的?

    或者在运行时指定了cluster=SomeCluster,但是并没有在Apollo新建cluster,这个时候Apollo的行为是怎样的?

    接下来就来介绍一下配置获取的规则。

    4.4.1 应用自身配置的获取规则

    当应用使用下面的语句获取配置时,我们称之为获取应用自身的配置,也就是应用自身的application namespace的配置。

    Config config = ConfigService.getAppConfig();
    

    对这种情况的配置获取规则,简而言之如下:

    1. 首先查找运行时cluster的配置(通过apollo.cluster指定)
    2. 如果没有找到,则查找数据中心cluster的配置
    3. 如果还是没有找到,则返回默认cluster的配置

    图示如下:

    application-config-precedence

    所以如果应用部署在A数据中心,但是用户没有在Apollo创建cluster,那么获取的配置就是默认cluster(default)的。

    如果应用部署在A数据中心,同时在运行时指定了SomeCluster,但是没有在Apollo创建cluster,那么获取的配置就是A数据中心cluster的配置,如果A数据中心cluster没有配置的话,那么获取的配置就是默认cluster(default)的。

    4.4.2 公共组件配置的获取规则

    FX.Hermes.Producer为例,hermes producer是hermes发布的公共组件。当使用下面的语句获取配置时,我们称之为获取公共组件的配置。

    Config config = ConfigService.getConfig("FX.Hermes.Producer");
    

    对这种情况的配置获取规则,简而言之如下:

    1. 首先获取当前应用下的FX.Hermes.Producer namespace的配置
    2. 然后获取hermes应用下FX.Hermes.Producer namespace的配置
    3. 上面两部分配置的并集就是最终使用的配置,如有key一样的部分,以当前应用优先

    图示如下:

    public-namespace-config-precedence

    通过这种方式,就实现了对框架类组件的配置管理,框架组件提供方提供配置的默认值,应用如果有特殊需求,可以自行覆盖。

    4.5 总体设计

    overall-architecture

    上图简要描述了Apollo的总体设计,我们可以从下往上看:

    • Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端
    • Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)
    • Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳
    • 在Eureka之上我们架了一层Meta Server用于封装Eureka的服务发现接口
    • Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试
    • Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试
    • 为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中

    4.5.1 Why Eureka

    为什么我们采用Eureka作为服务注册中心,而不是使用传统的zk、etcd呢?我大致总结了一下,有以下几方面的原因:

    • 它提供了完整的Service Registry和Service Discovery实现
      • 首先是提供了完整的实现,并且也经受住了Netflix自己的生产环境考验,相对使用起来会比较省心。
    • 和Spring Cloud无缝集成
      • 我们的项目本身就使用了Spring Cloud和Spring Boot,同时Spring Cloud还有一套非常完善的开源代码来整合Eureka,所以使用起来非常方便。
      • 另外,Eureka还支持在我们应用自身的容器中启动,也就是说我们的应用启动完之后,既充当了Eureka的角色,同时也是服务的提供者。这样就极大的提高了服务的可用性。
      • 这一点是我们选择Eureka而不是zk、etcd等的主要原因,为了提高配置中心的可用性和降低部署复杂度,我们需要尽可能地减少外部依赖。
    • Open Source
      • 最后一点是开源,由于代码是开源的,所以非常便于我们了解它的实现原理和排查问题。

    4.6 客户端设计

    client-architecture

    上图简要描述了Apollo客户端的实现原理:

    1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
    2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
      • 这是一个fallback机制,为了防止推送机制失效导致配置不更新
      • 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
      • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。
    3. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
    4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份
      • 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
    5. 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知

    4.6.1 配置更新推送实现

    前面提到了Apollo客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。

    长连接实际上我们是通过Http Long Polling实现的,具体而言:

    • 客户端发起一个Http请求到服务端
    • 服务端会保持住这个连接30秒
    • 如果在30秒内有客户端关心的配置变化,被保持住的客户端请求会立即返回,并告知客户端有配置变化的namespace信息,客户端会据此拉取对应namespace的最新配置
    • 如果在30秒内没有客户端关心的配置变化,那么会返回Http状态码304给客户端
    • 客户端在服务端请求返回后会自动重连

    考虑到会有数万客户端向服务端发起长连,在服务端我们使用了async servlet(Spring DeferredResult)来服务Http Long Polling请求。

    4.7 可用性考虑

    配置中心作为基础服务,可用性要求非常高,下面的表格描述了不同场景下Apollo的可用性:

    场景影响降级原因
    某台config service下线无影响 Config service无状态,客户端重连其它config service
    所有config service下线客户端无法读取最新配置,Portal无影响客户端重启时,可以读取本地缓存配置文件 
    某台admin service下线无影响 Admin service无状态,Portal重连其它admin service
    所有admin service下线客户端无影响,portal无法更新配置  
    某台portal下线无影响 Portal域名通过slb绑定多台服务器,重试后指向可用的服务器
    全部portal下线客户端无影响,portal无法更新配置  
    某个数据中心下线无影响 多数据中心部署,数据完全同步,Meta Server/Portal域名通过slb自动切换到其它存活的数据中心

    5、Contribute to Apollo

    Apollo从开发之初就是以开源模式开发的,所以也非常欢迎有兴趣、有余力的朋友一起加入进来。

    服务端开发使用的是Java,基于Spring Cloud和Spring Boot框架。客户端目前提供了Java和.Net两种实现。

    Github地址:https://github.com/ctripcorp/apollo

    ]]>
    taoCMS-基于php+sqlite最小巧的CMS 2018-02-24 13:02:22
    1139 使用Zuul构建API Gateway 一  微服务网关背景及简介

    不同的微服务一般有不同的网络地址,而外部的客户端可能需要调用多个服务的接口才能完成一个业务需求。比如一个电影购票的收集APP,可能回调用电影分类微服务,用户微服务,支付微服务等。如果客户端直接和微服务进行通信,会存在一下问题:

    # 客户端会多次请求不同微服务,增加客户端的复杂性

    # 存在跨域请求,在一定场景下处理相对复杂

    # 认证复杂,每一个服务都需要独立认证

    # 难以重构,随着项目的迭代,可能需要重新划分微服务,如果客户端直接和微服务通信,那么重构会难以实施

    # 某些微服务可能使用了其他协议,直接访问有一定困难

    上述问题,都可以借助微服务网管解决。微服务网管是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过微服务网关,架构演变成:


    这样客户端只需要和网关交互,而无需直接调用特定微服务的接口,而且方便监控,易于认证,减少客户端和各个微服务之间的交互次数

     

    二 Zuul 简介

    Zuul是Netflix开源的微服务网关,他可以和Eureka,Ribbon,Hystrix等组件配合使用。Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:

    # 身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求

    # 审查与监控:

    # 动态路由:动态将请求路由到不同后端集群

    # 压力测试:逐渐增加指向集群的流量,以了解性能

    # 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求

    # 静态响应处理:边缘位置进行响应,避免转发到内部集群

    # 多区域弹性:跨域AWS Region进行请求路由,旨在实现ELB(ElasticLoad Balancing)使用多样化

    Spring Cloud对Zuul进行了整合和增强。目前,Zuul使用的默认是Apache的HTTP Client,也可以使用Rest Client,可以设置ribbon.restclient.enabled=true.

     

     

    三 编写Zuul微服务网关

    # 添加依赖

    Zuul的依赖肯定是要加的,如何和Eureka配合使用, Zuul需要注册到Eureka上,但是Zuul的依赖不包含Eureka Discovery客户端,所以还需要添加Eureka的客户端依赖

    <dependency>

          <groupId>org.springframework.cloud</groupId>

          <artifactId>spring-cloud-starter-eureka</artifactId>

    </dependency>

    <dependency>

          <groupId>org.springframework.cloud</groupId>

          <artifactId>spring-cloud-starter-zuul</artifactId>

    </dependency>

     

    # 启动类加上注解@EnableZuulProxy

    它默认加上了@EnableCircuitBreaker和@EnableDiscoveryClient

    @SpringBootApplication

    @EnableZuulProxy

    public class ZuulApplication {

          public static void main(String[] argsthrows Exception {

               SpringApplication.run(ZuulApplication.classargs);

          }

    }

    # 配置application.yml

    server:

      port: 8040

    spring:

      application:

        name: microservice-gateway-zuul

    eureka:

      client:

        register-with-eureka: true

        fetch-registry: true

        service-url:

          defaultZone:http://nicky:123abcABC@localhost:8761/eureka

      instance:

        ip-address: true

    zuul:

      ignoredServices: '*'

      routes:

        microservice-provider-user: /ecom/**

    zuul:

     ignoredServices: '*' // 忽略所有请求

      routes:

        服务名: /ecom/** //允许将服务名映射到ecom

     

    # 启动Eureka,Zuul和 其他应用

    访问http://localhost:8040/ecom/user/1

    或者

    http://localhost:8040/microservice-provider-user/user/1

    都可以

     

    四 微服务网关相关的配置

    4.1 路由路径的配置

    zuul:

     ignoredServices: '*' // 忽略所有请求

      routes:

    服务名: /ecom/** //允许将服务名映射到ecom

     

     

    为了更加细粒度控制路由路径:

    // 表示只要HTTP请求是 /ecom开始的,就会forward到服务id为users_service的服务上面

    zuul:

      routes:

        users:

          path:/ecom/**  // 路由路径

         serviceId: users_service // 服务id

     

    zuul:

      routes:

       users:  // 路由名称,随意,唯一即可

          path: /ecom/**  // 路由路径

          url:http://localhost:9000

     

    4.2 不破坏Zuul的Hystrix和Ribbon特性

    上述简单的url路由配置,不会作为Hystrix Command执行,也不会进行ribbon的负载均衡。为了同时指定path 和 url,但是不破坏Zuul的Hystrix和Ribbon特性:

    zuul:

      routes:

        users:

          path:/ecom/**

         serviceId: microservice-provider-user

     

    ribbon:

      eureka:

        enabled:false // ribbon禁掉Eureka

     

    microservice-provider-user:

      ribbon:

    listOfServers: localhost:9000,localhost:9001

     

    4.3 使用正则表达式指定Zuul的路由匹配规则

    借助PatternServiceRoute    Mapper实现从微服务到映射路由的正则配置,例如:

          @Bean

          publicPatternServiceRouteMapper serviceRouteMapper(){

               // servicePattern: 指的是微服务的pattern

               // routePattern: 指的是路由的pattern

               // 当你访问/microservice-provider-user/v1 其实就是

               // localhost:8040/v1/microservice-provider-user/user/1

               return newPatternServiceRouteMapper(

                     "(?<name>^.+)-(?<version>v.+$)","${version}/${name}"

               );

          }

    4.4 路由的前缀

    zuul.prefix: 我们可以指定一个全局的前缀

    strip-prefix: 是否将这个代理前缀去掉

    zuul:

      prefix: /ecom

      strip-prefix: false

      routes:

        microservice-provider-user: /provider/**

     

    比如你访问http://localhost:8040/ecom/microservice-provider-user/user/1,其实真实访问路径是/ecom/user/1

     

    zuul:

      prefix: /ecom

      strip-prefix: true

      routes:

        microservice-provider-user: /provider/**

    比如你访问http://localhost:8040/ecom/microservice-provider-user/user/1,其实真实访问路径是/user/1,因为我们可以将前缀去掉

     

    如果strip-prefix只是放在路由下面,那么就是局部的,不会影响全局

    zuul:

      prefix: /ecom

      routes:

        abc:

          path: /provider/**

          service-id: microservice-provider-user

          strip-prefix: true

     

    比如你访问http://localhost:8040/ecom/microservice-provider-user/user/1

    其实真实访问路径是/user/1,因为我们可以将前缀去掉

     

    zuul:

      prefix: /ecom

      routes:

        abc:

          path: /provider/**

          service-id: microservice-provider-user

          strip-prefix: false

    比如你访问http://localhost:8040/ecom/provider/user/1

    其实真实访问路径是/provider/user/1,因为我们可以将前缀去掉

     

    4.5 忽略某些路径

    zuul:

      ignoredPatterns: /**/admin/**

      routes:

    users: /myusers/**

    过滤掉path包含admin的请求

     

    五 Zuul的安全和Header

    5.1 敏感的Header设置

    在同一个系统中微服务之间共享Header,但是某些时候尽量防止让一些敏感的Header外泄。因此很多场景下,需要通过为路由指定一系列敏感的Header列表。例如:

    zuul:

      routes:

        abc:

          path: /provider/**

          service-id: microservice-provider-user

          sensitiveHeaders:Cookie,Set-Cookie,Authorization

          url: https://downstream

    5.2 忽略Header

    被忽略的Header不会被传播到其他的微服务去。其实敏感的Heade最终也是走的这儿

    zuul:

      ignored-headers: Header1,Header2

    默认情况下,ignored-headers是空的

     

     

    六strangle模式

    在迁移现有应用程序或API时,一种常见的模式是“扼杀”旧的端点,用不同的实现慢慢替换它们。Zuul代理是一个有用的工具,因为您可以使用它来处理来自旧端点客户端的所有流量,但是将一些请求重定向到新端点。

    zuul:

      routes:

        first:

          path: /first/**

          url: http://first.example.com

        second:

          path: /second/**

          url: forward:/second

        third:

          path: /third/**

          url: forward:/3rd

        legacy:

          path: /**

          url: http://legacy.example.com  // 遗留的或者剩余的都走这个路径

     

     

    七 使用Zuul上传文件

    7.1 如果我们不使用Zuul上传文件,即通过web框架,也可以实现文件上传,即原始的文件上传功能:

    # 添加对应的依赖

    <dependency>

     <groupId>org.springframework.cloud</groupId>

      <artifactId>spring-cloud-starter-eureka</artifactId>

    </dependency>

    <dependency>

     <groupId>org.springframework.boot</groupId>

     <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

    <dependency>

     <groupId>org.springframework.boot</groupId>

      <artifactId>spring-boot-starter-actuator</artifactId>

    </dependency>

     

    # 创建上传文件的controller

    @Controller

    public classFileUploadController {

     

          @RequestMapping(value="/upload",method=RequestMethod.POST)

          public @ResponseBody StringhandleFileUpload(

                     @RequestParam(value="file",required=true)MultipartFile file){

               try {

                     byte[] in = file.getBytes();

                     File out = new File(file.getOriginalFilename());

                     FileCopyUtils.copy(inout);

                     return out.getAbsolutePath();

               } catch (IOException e) {

                     // TODO Auto-generatedcatch block

                     e.printStackTrace();

               }

               return null;

          }

    }

     

    # 编写配置文件application.yml


    # 使用工具CURL测试

    curl -F"file=@微服务简介.docx"localhost:8086/upload

     

    7.2 通过Zuul上传文件

    # 不添加/zuul,上传小文件没有问题

    curl -F"file=@Hystrix.docx" http://localhost:8050/microservice-consumer/upload

     

    # 不添加/zuul前缀上传大文件

    curl -F"file=@ATGPUB.DMP" http://localhost:8050/microservice-consumer/upload

    如果不加/zuul前缀就会报错

    {"timestamp":1507123311527,"status":500,"error":"InternalServerError","exception":"org.springframework.web.multipart.MultipartException","message":"Couldnot parse multipart servlet request; nested exception isjava.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException:the request was rejected because its size (132780234) exceeds the configuredmaximum(10485760)","path":"/microservice-consumer/upload"}

     

    # 添加/zuul前缀,上传大文件

    curl -F"file=@ATGPUB.DMP"http://localhost:8050/zuul/microservice-consumer/upload

     

    {"timestamp":1507123418018,"status":500,"error":"InternalServerError","exception":"com.netflix.zuul.exception.ZuulException","message":"TIMEOUT"}

    此时已经不是文件大小的错误了,我们可以将上传大文件的超时时间设置长一些

    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:60000

    ribbon:

      ConnectTimeout: 3000

      ReadTimeout: 60000

     

    八 Zuul的容错与回退

    在Spring Cloud, Zuul默认已经整合了Hystrix, 而且如果启动了Dashborad,也可以知道Zuul对Hystrix监控的粒度是微服务,而不是某一个API; 同时也说明所有经过Zuul的请求都会被Hystrix保护起来。

    8.1 为Zuul添加回退

    想要为Zuul添加回退,需要实现ZuulFallbackProvider接口。在实现类中,指定为哪一个微服务提供回退,并且提供一个ClientHttpResponse作为回退响应。

     

    编写Zuul回退类:

    @Component

    public classUserFallbackProvider implementsZuulFallbackProvider{

     

          @Override

          public String getRoute(){

               // 指定为哪一个微服务提供回退

               return "microservice-provider-user";

          }

     

          @Override

          publicClientHttpResponse fallbackResponse() {

               return newClientHttpResponse() {

                     @Override

                     public HttpHeadersgetHeaders() {

                          // 设置header

                          HttpHeaders headers = new HttpHeaders();

                          MediaType mediaType = new MediaType("application",

                                      "json",Charset.forName("UTF-8"));

                          headers.setContentType(mediaType);

                          return headers;

                     }

     

                     @Override

                     public InputStreamgetBody() throws IOException {

                          // 响应体

                          return newByteArrayInputStream(

                                "Usermicro-service is unavailable, please try it again later!".getBytes());

                     }

     

                     @Override

                     public StringgetStatusText() throws IOException {

                          // 返回状态文本

                          return this.getStatusCode().getReasonPhrase();

                     }

     

                     @Override

                     public HttpStatusgetStatusCode() throws IOException {

     

                          return HttpStatus.OK;

                     }

     

                     @Override

                     public intgetRawStatusCode() throws IOException {

                          // 返回数字类型的状态码

                          return this.getStatusCode().value();

                     }

     

                     @Override

                     public void close() {

     

                     }

               };

          }

    }

     

    重新启动Eureka Server,microservice-gateway-zuul-fallback以及microservice-provider-user

    我们正常通过zuul访问microservice-provider-user微服务

    http://localhost:8040/microservice-provider-user/user/1 没有问题

    然后我们关掉microservice-provider-user微服务

    在访问http://localhost:8040/microservice-provider-user/user/1,则会出现User micro-service is unavailable, please try it again later!

    而不是以前不友好的那个页面了

     

    九 Zuul的高可用
    Zuul的高可用非常关键,因为外部请求到后端的微服务的流量都会经过Zuul。故而在生产环境中一般都需要部署高可用的Zuul以避免单点故障

    9.1 Zuul客户端注册到了Eureka Server上

    此种情况,Zuul的高可用实现比较简单,只需将多个Zuul节点注册到Eureka Server,就可以实现Zuul高可用。此时,Zuul与其他的微服务高可用没啥区别。Zuul客户端会自动从Eureka Server中查询Zuul Server的列表,并使用Ribbon负载均衡的请求Zuul集群


    9.2 Zuul客户端未注册到Eureka Server上

    如果Zuul客户端未注册到Eureka上,因为微服务可能被其他微服务调用,也可能直接被终端调用,比如手机App。此种情况下,我们需要借助额外的负载均衡器来实现Zuul的高可用,比如Nginx,比如HAProxy或者F5等


    九 Sidecar

    我们可以使用Sidecar整合非JVM微服务,比如C++、Python、PHP等语言写的。其他非JVM微服务可操作Eureka的REST端点,从而实现注册与发现。事实上,也可以使用sidecar更加方便整合非JVM微服务


    # 首先添加依赖

    <dependency>

     <groupId>org.springframework.cloud</groupId>

      <artifactId>spring-cloud-starter-eureka</artifactId>

    </dependency>

    <dependency>

     <groupId>org.springframework.cloud</groupId>

     <artifactId>spring-cloud-starter-zuul</artifactId>

    </dependency>

    <dependency>

     <groupId>org.springframework.cloud</groupId>

      <artifactId>spring-cloud-netflix-sidecar</artifactId>

    </dependency>

     

    # 添加启动类,在启动类上加上@EnableSidecar注解,声明这是一个Sidecar

    这个注解整合了三个注解即:

    @EnableCircuitBreaker

    @EnableDiscoveryClient

    @EnableZuulProxy

    @SpringBootApplication

    @EnableSidecar

    public classZuulSidecarApplication {

          public static void main(String[] argsthrows Exception {

               SpringApplication.run(ZuulSidecarApplication.classargs);

          }

    }

    # 编写application.yml

    Sidecar.port指的就是其他语言微服务的端口


    Sidecar与其他语言的微服务分离部署

    上面我们是将其他语言微服务和sidecar放在同一个机器上,现实中,常常会将Sidecar与IVM微服务分离部署,部署在不同的主机上面或者容器中,这时候应该如何配置呢?

    方法一:

    eureka:

      instance:

    hostname: # 非JVM微服务所在的hostname

    方法二:

    sidecar:

      hostname: # 非JVM微服务所在的hostname

      ip-address: # 非JVM微服务所在的IP 地址

    注意:如果这种微服务太多,而且还涉及到集群的话使用sidecar我们应该权衡一下。因为一个sidecar只能对应一个其他语言写的微服务,如果很多,那表示就多个sidecar了。

     

    十 Zuul的过滤器

    过滤器是Zuul的核心组件,Zuul大部分功能都是通过过滤器来实现的。

    10.1 过滤器类型和请求生命周期

    Zuul中定义了四种标准的过滤器类型,这些过滤器类型对应于典型的生命周期。

    PRE: 这种过滤器在请求被路由之前调用。可利用其实现身份验证等

    ROUTING: 这种过滤器将请求路由到微服务,用于构建发送给微服务的请求,并使用Apache Http Client或者Netflix Ribbon请求微服务

    POST: 这种过滤器在路由到微服务以后执行,比如为响应添加标准的HTTP Header,收集统计信息和指标,将响应从微服务发送到客户端等

    ERROR: 在其他阶段发生错误时执行该过滤器

    除了默认的过滤器类型,Zuul还允许创建自定义的过滤器类型。


    10.2 编写Zuul过滤器

    我们只需要继承抽象类ZuulFilter过滤器即可,让该过滤器打印请求日志

    public classPreRequestLogFilter extends ZuulFilter {

     

          private static final Logger logger = LoggerFactory.getLogger(

                     PreRequestLogFilter.class);

          @Override

          public Object run() {

               RequestContext context = RequestContext.getCurrentContext();

               HttpServletRequest reqeust = context.getRequest();

               PreRequestLogFilter.logger.info(

                          String.format("send %srequest to %s",

                          reqeust.getMethod(),

                          reqeust.getRequestURL().toString()));

               return null;

          }

     

          @Override

          public boolean shouldFilter() {

               // 判断是否需要过滤

               return true;

          }

     

          @Override

          public int filterOrder() {

               // 过滤器的优先级,越大越靠后执行

               return 1;

          }

     

          @Override

          public StringfilterType() {

               // 过滤器类型

               return "pre";

          }

    }

     

    修改启动类,为启动类添加:

    @SpringBootApplication

    @EnableZuulProxy

    public classZuulFilterApplication {

          @Bean

          publicPreRequestLogFilter preRequestLogFilter(){

               return newPreRequestLogFilter();

          }

     

          public static void main(String[] argsthrows Exception {

               SpringApplication.run(ZuulFilterApplication.classargs);

          }

    }

    10.3 禁用Zuul过滤器

    Spring Cloud默认为Zuul编写并启用了一些过滤器,例如DebugFilter,FromBodyWrapperFilter,PreDecorationFilter等,这些过滤器都存放在spring-cloud-netflix-core这个jar里的

    在某些场景下,希望禁掉一些过滤器,该怎办呢?

    只需设置zuul.<SimpleClassName>.<filterType>.disable=true即可,比如

    zuul.PreRequestLogFilter.pre.disable=true

     

    十一 Zuul聚合微服务

    许多场景下,一个外部请求,可能需要查询Zuul后端多个微服务。比如说一个电影售票系统,在购票订单页上,需要查询电影微服务,还需要查询用户微服务获得当前用户信息。如果让系统直接请求各个微服务,就算Zuul转发,网络开销,流量耗费,时长都不是很好的。这时候我们就可以使用Zuul聚合微服务请求,即应用系统只发送一个请求给Zuul,由Zuul请求用户微服务和电影微服务,并把数据返给应用系统。

    ]]>
    taoCMS-基于php+sqlite最小巧的CMS 2018-02-24 13:02:58
    1138 使用腾讯云GPU学习深度学习系列之五:文字的识别与定位 文档:https://keras-cn.readthedocs.io/en/latest/

    这是《使用腾讯云GPU学习深度学习》系列文章的第五篇,以车牌识别和简单OCR为例,谈了谈如何进行字母、数字的识别以及定位。本系列文章主要介绍如何使用腾讯云GPU服务器进行深度学习运算,前面主要介绍原理部分,后期则以实践为主。

    往期内容:

    1. 使用腾讯云 GPU 学习深度学习系列之一:传统机器学习的回顾
    2. 使用腾讯云 GPU 学习深度学习系列之二:Tensorflow 简明原理
    3. 使用腾讯云 GPU 学习深度学习系列之三:搭建深度神经网络
    4. 使用腾讯云 GPU 学习深度学习系列之四:深度学习的特征工程

    上一节,我们简要介绍了一些与深度学习相关的数据预处理方法。其中我们特别提到,使用 基于深度学习的 Spatial Transform 方法,可以让“草书” 字体的手写数字同样也可以被高效识别。

    但无论是工整书写的 Tensorflow 官网上的 MNIST 教程,还是上节提到“草书”数字,都是 单一的数字识别问题。 但是,在实际生活中,遇到数字、字母识别问题时,往往需要识别一组数字。这时候一个简单的深度神经网络可能就做不到了。本节内容,就是在讨论遇到这种情况时,应该如何调整深度学习模型。

    1. 固定长度

    固定长度的字符、数字识别,比较常见的应用场景包括:

    • 识别验证码
    • 识别机动车车牌

    识别验证码的方法,这篇文章 有详细介绍。不过该文章使用的是版本较早的 Keras1,实际使用时会有一些问题。如果想尝试,根据Jupyter 的提示更改就好,最终效果也是相当不错:

    我们这里要识别的内容,是中华人民共和国机动车车牌。相比上面例子的 4 位验证码,车牌长度更长,达到了 7 位,并且内容也更加丰富,第一位是各省的汉字简称,第二位是 A-Z 的大写字母,3-7位则是数字、字母混合。

    由于车牌涉及个人隐私,我们使用了用户 szad670401 在 Github 上开源的一个车牌生成器,随机的生成一些车牌的图片,用于模型训练。当然这个项目同样提供了完整的 MXNet 深度学习框架编写的代码,我们接下来会用 Keras 再写一个。

    首先做些准备工作,从 szad670401 的开源项目中获取必要的文件:

    ### 从 szad670401 github 项目下载车牌生成器以及字体文件
    !git clone https://github.com/szad670401/end-to-end-for-chinese-plate-recognition
    !cp -r end-to-end-for-chinese-plate-recognition/* ./
    !sed 's/for i in range(batchSize):/l_plateStr = []n        l_plateImg = []n        for i in range(batchSize):/g' ./genplate.py  | sed 's/cv2.imwrite(outputPath/l_plateStr.append(plateStr)n                l_plateImg.append(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))n                #cv2.imwrite(outputPath/g' | sed 's/img);/img);n        return l_plateStr,l_plateImg/g'  >genplateRev.py

    来看看生成器的效果:

    from keras.models import Model
    from keras.callbacks import ModelCheckpoint
    from keras.layers import Conv2D, MaxPool2D, Flatten, Dropout, Dense, Input
    from keras.optimizers import Adam
    from keras.backend.tensorflow_backend import set_session
    from keras.utils.vis_utils import model_to_dot
    import tensorflow as tf
    import matplotlib
    import matplotlib.pyplot as plt
    from matplotlib.font_manager import FontProperties
    
    from IPython.display import SVG
    
    from genplate import *
    
    %matplotlib inline
    
    np.random.seed(5)
    config = tf.ConfigProto()
    config.gpu_options.allow_growth=True
    set_session(tf.Session(config=config))
    
    chars = ["京", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂",
                 "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A",
                 "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X",
                 "Y", "Z"
                 ];
    
    M_strIdx = dict(zip(chars, range(len(chars))))
    
    n_generate = 100
    rows = 20
    cols = int(n_generate/rows)
    
    G = GenPlate("./font/platech.ttf",'./font/platechar.ttf',"./NoPlates")
    l_plateStr,l_plateImg = G.genBatch(100,2,range(31,65),"./plate",(272,72))
    
    l_out = []
    for i in range(rows):
        l_tmp = []
        for j in range(cols):
            l_tmp.append(l_plateImg[i*cols+j])
    
        l_out.append(np.hstack(l_tmp))
    
        fig = plt.figure(figsize=(10, 10))
        ax  = fig.add_subplot(111)
        ax.imshow( np.vstack(l_out), aspect="auto" )

    看来 szad670401 开源的车牌生成器,随机生成的车牌确实达到了以假乱真的效果。于是我们基于这个生成器,再自己写一个生成器,用于深度神经网络的数据输入:

    def gen(batch_size=32):
        while True:
            l_plateStr,l_plateImg = G.genBatch(batch_size, 2, range(31,65),"./plate",(272,72))
            X = np.array(l_plateImg, dtype=np.uint8)
            ytmp = np.array(list(map(lambda x: [M_strIdx[a] for a in list(x)], l_plateStr)), dtype=np.uint8)
            y = np.zeros([ytmp.shape[1],batch_size,len(chars)])
            for batch in range(batch_size):
                for idx,row_i in enumerate(ytmp[batch]):
                    y[idx,batch,row_i] = 1
    
            yield X, [yy for yy in y]

    因为是固定长度,所以我们有个想法,就是既然我们知道识别七次,那就可以用七个模型按照顺序识别。这个思路没有问题,但实际上根据之前卷积神经网络的原理,实际上卷积神经网络在扫描整张图片的过程中,已经对整个图像的内容以及相对位置关系有所了解,所以,七个模型的卷积层实际上是可以共享的。我们实际上可以用一个 一组卷积层+7个全链接层 的架构,来对应输入的车牌图片:

    adam = Adam(lr=0.001)
    
    input_tensor = Input((72, 272, 3))
    x = input_tensor
    for i in range(3):
        x = Conv2D(32*2**i, (3, 3), activation='relu')(x)
        x = Conv2D(32*2**i, (3, 3), activation='relu')(x)
        x = MaxPool2D(pool_size=(2, 2))(x)
    x = Flatten()(x)
    x = Dropout(0.25)(x)
    
    n_class = len(chars)
    x = [Dense(n_class, activation='softmax', name='c%d'%(i+1))(x) for i in range(7)]
    model = Model(inputs=input_tensor, outputs=x)
    model.compile(loss='categorical_crossentropy',
                  optimizer=adam,
                  metrics=['accuracy'])
    
    SVG(model_to_dot(model=model, show_layer_names=True, show_shapes=True).create(prog='dot', format='svg'))

    训练模型:

    best_model = ModelCheckpoint("chepai_best.h5", monitor='val_loss', verbose=0, save_best_only=True)
    
    model.fit_generator(gen(32), steps_per_epoch=2000, epochs=5,
                        validation_data=gen(32), validation_steps=1280,
                        callbacks=[best_model]
    )

    Epoch 1/5
    2000/2000 [==============================] - 547s - loss: 11.1077 - c1_loss: 1.3878 - c2_loss: 0.7512 - c3_loss: 1.1270 - c4_loss: 1.3997 - c5_loss: 1.7955 - c6_loss: 2.3060 - c7_loss: 2.3405 - c1_acc: 0.6157 - c2_acc: 0.7905 - c3_acc: 0.6831 - c4_acc: 0.6041 - c5_acc: 0.5025 - c6_acc: 0.3790 - c7_acc: 0.3678 - val_loss: 3.1323 - val_c1_loss: 0.1970 - val_c2_loss: 0.0246 - val_c3_loss: 0.0747 - val_c4_loss: 0.2076 - val_c5_loss: 0.5099 - val_c6_loss: 1.0774 - val_c7_loss: 1.0411 - val_c1_acc: 0.9436 - val_c2_acc: 0.9951 - val_c3_acc: 0.9807 - val_c4_acc: 0.9395 - val_c5_acc: 0.8535 - val_c6_acc: 0.7065 - val_c7_acc: 0.7190
    Epoch 2/5
    2000/2000 [==============================] - 546s - loss: 2.7473 - c1_loss: 0.2008 - c2_loss: 0.0301 - c3_loss: 0.0751 - c4_loss: 0.1799 - c5_loss: 0.4407 - c6_loss: 0.9450 - c7_loss: 0.8757 - c1_acc: 0.9416 - c2_acc: 0.9927 - c3_acc: 0.9790 - c4_acc: 0.9467 - c5_acc: 0.8740 - c6_acc: 0.7435 - c7_acc: 0.7577 - val_loss: 1.4777 - val_c1_loss: 0.1039 - val_c2_loss: 0.0118 - val_c3_loss: 0.0300 - val_c4_loss: 0.0665 - val_c5_loss: 0.2145 - val_c6_loss: 0.5421 - val_c7_loss: 0.5090 - val_c1_acc: 0.9725 - val_c2_acc: 0.9978 - val_c3_acc: 0.9937 - val_c4_acc: 0.9824 - val_c5_acc: 0.9393 - val_c6_acc: 0.8524 - val_c7_acc: 0.8609
    Epoch 3/5
    2000/2000 [==============================] - 544s - loss: 1.7686 - c1_loss: 0.1310 - c2_loss: 0.0156 - c3_loss: 0.0390 - c4_loss: 0.0971 - c5_loss: 0.2689 - c6_loss: 0.6416 - c7_loss: 0.5754 - c1_acc: 0.9598 - c2_acc: 0.9961 - c3_acc: 0.9891 - c4_acc: 0.9715 - c5_acc: 0.9213 - c6_acc: 0.8223 - c7_acc: 0.8411 - val_loss: 1.0954 - val_c1_loss: 0.0577 - val_c2_loss: 0.0088 - val_c3_loss: 0.0229 - val_c4_loss: 0.0530 - val_c5_loss: 0.1557 - val_c6_loss: 0.4247 - val_c7_loss: 0.3726 - val_c1_acc: 0.9849 - val_c2_acc: 0.9987 - val_c3_acc: 0.9948 - val_c4_acc: 0.9861 - val_c5_acc: 0.9569 - val_c6_acc: 0.8829 - val_c7_acc: 0.8994
    Epoch 4/5
    2000/2000 [==============================] - 544s - loss: 1.4012 - c1_loss: 0.1063 - c2_loss: 0.0120 - c3_loss: 0.0301 - c4_loss: 0.0754 - c5_loss: 0.2031 - c6_loss: 0.5146 - c7_loss: 0.4597 - c1_acc: 0.9677 - c2_acc: 0.9968 - c3_acc: 0.9915 - c4_acc: 0.9773 - c5_acc: 0.9406 - c6_acc: 0.8568 - c7_acc: 0.8731 - val_loss: 0.8221 - val_c1_loss: 0.0466 - val_c2_loss: 0.0061 - val_c3_loss: 0.0122 - val_c4_loss: 0.0317 - val_c5_loss: 0.1085 - val_c6_loss: 0.3181 - val_c7_loss: 0.2989 - val_c1_acc: 0.9870 - val_c2_acc: 0.9986 - val_c3_acc: 0.9969 - val_c4_acc: 0.9910 - val_c5_acc: 0.9696 - val_c6_acc: 0.9117 - val_c7_acc: 0.9182
    Epoch 5/5
    2000/2000 [==============================] - 553s - loss: 1.1712 - c1_loss: 0.0903 - c2_loss: 0.0116 - c3_loss: 0.0275 - c4_loss: 0.0592 - c5_loss: 0.1726 - c6_loss: 0.4305 - c7_loss: 0.3796 - c1_acc: 0.9726 - c2_acc: 0.9971 - c3_acc: 0.9925 - c4_acc: 0.9825 - c5_acc: 0.9503 - c6_acc: 0.8821 - c7_acc: 0.8962 - val_loss: 0.7210 - val_c1_loss: 0.0498 - val_c2_loss: 0.0079 - val_c3_loss: 0.0132 - val_c4_loss: 0.0303 - val_c5_loss: 0.0930 - val_c6_loss: 0.2810 - val_c7_loss: 0.2458 - val_c1_acc: 0.9862 - val_c2_acc: 0.9987 - val_c3_acc: 0.9971 - val_c4_acc: 0.9915 - val_c5_acc: 0.9723 - val_c6_acc: 0.9212 - val_c7_acc: 0.9336

    可见五轮训练后,即便是位置靠后的几位车牌,也实现了 93% 的识别准确率。

    展示下模型预测结果:

    myfont = FontProperties(fname='./font/Lantinghei.ttc')  
    matplotlib.rcParams['axes.unicode_minus']=False  
    
    fig = plt.figure(figsize=(12,12))
    l_titles = list(map(lambda x: "".join([M_idxStr[xx] for xx in x]), np.argmax(np.array(model.predict( np.array(l_plateImg) )), 2).T))
    for idx,img in enumerate(l_plateImg[0:40]):
        ax = fig.add_subplot(10,4,idx+1)
        ax.imshow(img)
        ax.set_title(l_titles[idx],fontproperties=myfont)
        ax.set_axis_off()

    可见预测的其实相当不错,很多字体已经非常模糊,模型仍然可以看出来。图中一个错误是 皖TQZ680 被预测成了 皖TQZG8D,当然这也和图片裁剪不当有一定的关系。

    2. 不固定长度

    车牌的应用场景中,我们固定了长度为7位,并且基于这个预设设计了卷积神经网络。但是在实际运用中,可能长度并不固定。此时如果长度过长,用这个架构也将会导致参数过多,占用过多显存。

    针对这种情况,Keras 的案例中,提供了一种基于循环神经网络的方法,在 Keras Example 中有写到。具体而言,就是数据首先通过卷积神经网络部分扫描特征,然后通过循环神经网络部分,同时从左到右、从右到左扫描特征,最后基于扫描的结果,通过计算 Conectionist Temporal Classification(CTC) 损失函数,完成模型训练。

    2.1. 循环神经网络

    使用循环神经网络,是因为循环神经网络有一个很重要的特点,就是相邻的节点之间,可以相互影响。这里相邻节点,既可以是时间上的(前一秒数据和后一秒数据),也可以是位置关系上的,比如我们这里从左向右扫描,左边一列的扫描结果会影响右边一列的扫描结果。

    图片来源:知乎:CNN(卷积神经网络)、RNN(循环神经网络)、DNN(深度神经网络)的内部网络结构有什么区别

    2.2. CTC 损失函数

    同时,对于循环神经网络的结果,由于长度不固定,可能会有空间上的“错配”:

    图片来源:Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks

    但由于这种错配实际上并没有什么严重的影响,如上图所示, __TH____E_T__H__EE 其实都是 THE 这个单词,因此这里这种错配在损失函数的优化环节中,是需要被忽略掉的。于是这里就使用了CTC 优化函数。CTC 可以在计算过程中,通过综合所有可能情况的排列组合,进而忽略相对的位置关系。

    Keras 的 CTC loss 函数位于 https://github.com/fchollet/keras/blob/master/keras/backend/tensorflow_backend.py 这个文件中,内容如下:

    import tensorflow as tf
    from tensorflow.python.ops import ctc_ops as ctc
    
    #...
    
    def ctc_batch_cost(y_true, y_pred, input_length, label_length):
        """Runs CTC loss algorithm on each batch element.
        # Arguments
            y_true: tensor `(samples, max_string_length)`
                containing the truth labels.
            y_pred: tensor `(samples, time_steps, num_categories)`
                containing the prediction, or output of the softmax.
            input_length: tensor `(samples, 1)` containing the sequence length for
                each batch item in `y_pred`.
            label_length: tensor `(samples, 1)` containing the sequence length for
                each batch item in `y_true`.
        # Returns
            Tensor with shape (samples,1) containing the
                CTC loss of each element.
        """
        label_length = tf.to_int32(tf.squeeze(label_length))
        input_length = tf.to_int32(tf.squeeze(input_length))
        sparse_labels = tf.to_int32(ctc_label_dense_to_sparse(y_true, label_length))
    
        y_pred = tf.log(tf.transpose(y_pred, perm=[1, 0, 2]) + 1e-8)
    
        return tf.expand_dims(ctc.ctc_loss(inputs=y_pred,
                                           labels=sparse_labels,
                                           sequence_length=input_length), 1)

    3.3. 完整代码

    首先是一些必要的函数:

    import os
    import itertools
    import re
    import datetime
    import cairocffi as cairo
    import editdistance
    import numpy as np
    from scipy import ndimage
    import pylab
    
    from keras import backend as K
    from keras.layers.convolutional import Conv2D, MaxPooling2D
    from keras.layers import Input, Dense, Activation, Reshape, Lambda
    from keras.layers.merge import add, concatenate
    from keras.layers.recurrent import GRU
    from keras.models import Model
    from keras.optimizers import SGD
    from keras.utils.data_utils import get_file
    from keras.preprocessing import image
    from keras.callbacks import EarlyStopping,Callback
    
    from keras.backend.tensorflow_backend import set_session
    import tensorflow as tf
    import matplotlib.pyplot as plt
    
    %matplotlib inline
    
    config = tf.ConfigProto()
    config.gpu_options.allow_growth=True
    set_session(tf.Session(config=config))
    
    
    OUTPUT_DIR = 'image_ocr'
    
    np.random.seed(55)
    
    # 从 Keras 官方文件中 import 相关的函数
    !wget https://raw.githubusercontent.com/fchollet/keras/master/examples/image_ocr.py
    from image_ocr import *

    必要的参数:

    run_name = datetime.datetime.now().strftime('%Y:%m:%d:%H:%M:%S')
    start_epoch = 0
    stop_epoch  = 200
    img_w = 128
    img_h = 64
    words_per_epoch = 16000
    val_split = 0.2
    val_words = int(words_per_epoch * (val_split))
    
    # Network parameters
    conv_filters = 16
    kernel_size = (3, 3)
    pool_size = 2
    time_dense_size = 32
    rnn_size = 512
    input_shape = (img_w, img_h, 1)

    使用这些函数以及对应参数构建生成器,生成不固定长度的验证码:

    fdir = os.path.dirname(get_file('wordlists.tgz',
                                        origin='http://www.mythic-ai.com/datasets/wordlists.tgz', untar=True))
    
    img_gen = TextImageGenerator(monogram_file=os.path.join(fdir, 'wordlist_mono_clean.txt'),
                                     bigram_file=os.path.join(fdir, 'wordlist_bi_clean.txt'),
                                     minibatch_size=32,
                                     img_w=img_w,
                                     img_h=img_h,
                                     downsample_factor=(pool_size ** 2),
                                     val_split=words_per_epoch - val_words
                                     )
    act = 'relu'

    构建网络:

    input_data = Input(name='the_input', shape=input_shape, dtype='float32')
    inner = Conv2D(conv_filters, kernel_size, padding='same',
                       activation=act, kernel_initializer='he_normal',
                       name='conv1')(input_data)
    inner = MaxPooling2D(pool_size=(pool_size, pool_size), name='max1')(inner)
    inner = Conv2D(conv_filters, kernel_size, padding='same',
                       activation=act, kernel_initializer='he_normal',
                       name='conv2')(inner)
    inner = MaxPooling2D(pool_size=(pool_size, pool_size), name='max2')(inner)
    
    conv_to_rnn_dims = (img_w // (pool_size ** 2), (img_h // (pool_size ** 2)) * conv_filters)
    inner = Reshape(target_shape=conv_to_rnn_dims, name='reshape')(inner)
    
    # cuts down input size going into RNN:
    inner = Dense(time_dense_size, activation=act, name='dense1')(inner)
    
    # Two layers of bidirecitonal GRUs
    # GRU seems to work as well, if not better than LSTM:
    gru_1 = GRU(rnn_size, return_sequences=True, kernel_initializer='he_normal', name='gru1')(inner)
    gru_1b = GRU(rnn_size, return_sequences=True, go_backwards=True, kernel_initializer='he_normal', name='gru1_b')(inner)
    gru1_merged = add([gru_1, gru_1b])
    gru_2 = GRU(rnn_size, return_sequences=True, kernel_initializer='he_normal', name='gru2')(gru1_merged)
    gru_2b = GRU(rnn_size, return_sequences=True, go_backwards=True, kernel_initializer='he_normal', name='gru2_b')(gru1_merged)
    
    # transforms RNN output to character activations:
    inner = Dense(img_gen.get_output_size(), kernel_initializer='he_normal',
                      name='dense2')(concatenate([gru_2, gru_2b]))
    y_pred = Activation('softmax', name='softmax')(inner)
    
    Model(inputs=input_data, outputs=y_pred).summary()
    labels = Input(name='the_labels', shape=[img_gen.absolute_max_string_len], dtype='float32')
    input_length = Input(name='input_length', shape=[1], dtype='int64')
    label_length = Input(name='label_length', shape=[1], dtype='int64')
    # Keras doesn't currently support loss funcs with extra parameters
    # so CTC loss is implemented in a lambda layer
    
    loss_out = Lambda(ctc_lambda_func, output_shape=(1,), name='ctc')([y_pred, labels, input_length, label_length])
    
    # clipnorm seems to speeds up convergence
    sgd = SGD(lr=0.02, decay=1e-6, momentum=0.9, nesterov=True, clipnorm=5)
    
    model = Model(inputs=[input_data, labels, input_length, label_length], outputs=loss_out)
    
    # the loss calc occurs elsewhere, so use a dummy lambda func for the loss
    model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer=sgd)
    if start_epoch > 0:
        weight_file = os.path.join(OUTPUT_DIR, os.path.join(run_name, 'weights%02d.h5' % (start_epoch - 1)))
        model.load_weights(weight_file)
    
    # captures output of softmax so we can decode the output during visualization
    test_func = K.function([input_data], [y_pred])
    
    # 反馈函数,即运行固定次数后,执行反馈函数可保存模型,并且可视化当前训练的效果
    viz_cb = VizCallback(run_name, test_func, img_gen.next_val())

    模型完整架构如下图所示:

    执行训练:

    model.fit_generator(generator=img_gen.next_train(), steps_per_epoch=(words_per_epoch - val_words),
                            epochs=stop_epoch, validation_data=img_gen.next_val(), validation_steps=val_words,
                            callbacks=[EarlyStopping(patience=10), viz_cb, img_gen], initial_epoch=start_epoch)

    Epoch 1/200
    12799/12800 [============================>.] - ETA: 0s - loss: 0.4932
    Out of 256 samples:  Mean edit distance: 0.000 Mean normalized edit distance: 0.000
    12800/12800 [==============================] - 2025s - loss: 0.4931 - val_loss: 3.7432e-04

    完成一个 Epoch 后,输出文件夹 image_ocr 里,可以看到,一轮训练后,我们模型训练效果如下:

    ]]>
    taoCMS-基于php+sqlite最小巧的CMS 2018-01-27 14:01:11