架构

架构

站在岸上看DEVOPS

默认分类admin 发表了文章 • 0 个评论 • 144 次浏览 • 2017-01-17 17:41 • 来自相关话题

今年项目要试水(X)AAS,尝试运维自己的产品,便离不开这几年最热的单词—DEVOPS。趁还在岸上,理解下DEVOPS对开发人员来说意味着什么。

因为没有实战,也就纸上谈兵一下,真正有经验的人, @连城404:为了分摊黑锅,运维发明了devops和full stack。

What is DEVOPS ?

十年前,敏捷出现,以反瀑布流程的方式模糊了需求、架构、开发、测试之间的界限,但对整个软件价值链的最后一环,交付与运维并没有太多的强调,造成一个有趣的现象是,产品以三周为迭代进行开发并交付给客户,但只充当与客户演示并收集反馈的媒介,而客户仍然会有自己独立的上线周期。

十年后,越来越多的互联网站与手机应用,需要频繁而持续的发布,每次发布需要部署到大量的机器集群上,需要不中断和破坏当前服务,在发布前能提供本次发布质量的证明,在发布后对系统有实时的监控,支持错误转移、服务降级,支持快速定位和修复故障。

显然,只靠传统运维人员的知识技能集(操作系统、网络、Shell脚本编写) 以及开发人员编写的的产品管理手册已不足以满足上述的需求了。好在,有敏捷的先例,解决方法是现成的——把开发拖下水吧:

1. 人员上,运维人员要更懂产品的架构与内在,而不只是那份管理员手册。开发人员要懂实际的运维,从实际部署、上线流程,到故障的定位与解决。当然,最重要是打通两个部门间的隔阂。

2. 产品特性上,运维所需的功能甚至运维的基础设施,成为了产品非功能性需求的重要一部分。

3. 流程上,产品交付与运维,接入整个软件生产周期构成一个完整的环。
 

What should developer do?

现在大家最关心的问题来了,DEVOPS的大旗下,开发人员到底要参与多少运维?

这幅图,又是零到一百之间的选择,各个组织根据自己的人员、技术,文化,找到自己最合适的那个平衡点。

理论上,开发人员在DEVOPS中可以有两种的参与:

一,为运维而开发。

比如,在代码中加入Metrics收集的代码,向Graphite吐出服务的调用次数与延时,通常是与运维协作的第一步——运维可以在CPU,内存占用与应用日志之外,不那么黑盒的看待应用了。

又比如,要和运维商量着,在代码端需要做些什么,才能实现监控,无中断升级,动态节点扩展,故障转移,服务降级,跨机房容灾。这些功能以前可能也是要的,但现在需求的采集更注重实效。甚至,运维的一些基础设施,也可以由开发代劳了。

又比如,环境与流程的统一,原来只在简版的集成测试环境上跑完功能测试就完事的开发闭环,现在需要考虑至少延伸到一台从服务器配置到数据库部署模型等都贴近生产环境的Staging服务器,又比如测试环境的安装部署,也会用和运维一样的基于Ansible 或 Saltstack。

二,开发直接参与运维。

也是最存在争议的地方,比如系统的上线升级,日常的运维操作,故障的即时修复。

开发人员参与运维,可以吃自己的狗粮,可以更深切的了解运维功能的需求,开发人员的故障修复更快,但是——

程序员是不是应该做更有创造性的事情?程序员做运维会不会感到厌烦?社会化分工还是不是一个无需争辩的现代社会真理?

其实类似的争论已发生过一次,在开发与测试的分工上。起码在我司,专门的测试人员仍然有不可动摇的作用,而开发人员,则承担了从持续的单元测试和功能测试,最后到手工的性能测试与稳定性测试,总体80%以上的测试工作量,很自然的合作。

所以,还是没有绝对,还是各个组织自己去通过不断的尝试,找到自己最合适的那个平衡点。 查看全部
今年项目要试水(X)AAS,尝试运维自己的产品,便离不开这几年最热的单词—DEVOPS。趁还在岸上,理解下DEVOPS对开发人员来说意味着什么。

因为没有实战,也就纸上谈兵一下,真正有经验的人, @连城404:为了分摊黑锅,运维发明了devops和full stack。

What is DEVOPS ?

十年前,敏捷出现,以反瀑布流程的方式模糊了需求、架构、开发、测试之间的界限,但对整个软件价值链的最后一环,交付与运维并没有太多的强调,造成一个有趣的现象是,产品以三周为迭代进行开发并交付给客户,但只充当与客户演示并收集反馈的媒介,而客户仍然会有自己独立的上线周期。

十年后,越来越多的互联网站与手机应用,需要频繁而持续的发布,每次发布需要部署到大量的机器集群上,需要不中断和破坏当前服务,在发布前能提供本次发布质量的证明,在发布后对系统有实时的监控,支持错误转移、服务降级,支持快速定位和修复故障。

显然,只靠传统运维人员的知识技能集(操作系统、网络、Shell脚本编写) 以及开发人员编写的的产品管理手册已不足以满足上述的需求了。好在,有敏捷的先例,解决方法是现成的——把开发拖下水吧:

1. 人员上,运维人员要更懂产品的架构与内在,而不只是那份管理员手册。开发人员要懂实际的运维,从实际部署、上线流程,到故障的定位与解决。当然,最重要是打通两个部门间的隔阂。

2. 产品特性上,运维所需的功能甚至运维的基础设施,成为了产品非功能性需求的重要一部分。

3. 流程上,产品交付与运维,接入整个软件生产周期构成一个完整的环。
 

What should developer do?

现在大家最关心的问题来了,DEVOPS的大旗下,开发人员到底要参与多少运维?

这幅图,又是零到一百之间的选择,各个组织根据自己的人员、技术,文化,找到自己最合适的那个平衡点。

理论上,开发人员在DEVOPS中可以有两种的参与:

一,为运维而开发。

比如,在代码中加入Metrics收集的代码,向Graphite吐出服务的调用次数与延时,通常是与运维协作的第一步——运维可以在CPU,内存占用与应用日志之外,不那么黑盒的看待应用了。

又比如,要和运维商量着,在代码端需要做些什么,才能实现监控,无中断升级,动态节点扩展,故障转移,服务降级,跨机房容灾。这些功能以前可能也是要的,但现在需求的采集更注重实效。甚至,运维的一些基础设施,也可以由开发代劳了。

又比如,环境与流程的统一,原来只在简版的集成测试环境上跑完功能测试就完事的开发闭环,现在需要考虑至少延伸到一台从服务器配置到数据库部署模型等都贴近生产环境的Staging服务器,又比如测试环境的安装部署,也会用和运维一样的基于Ansible 或 Saltstack。

二,开发直接参与运维。

也是最存在争议的地方,比如系统的上线升级,日常的运维操作,故障的即时修复。

开发人员参与运维,可以吃自己的狗粮,可以更深切的了解运维功能的需求,开发人员的故障修复更快,但是——

程序员是不是应该做更有创造性的事情?程序员做运维会不会感到厌烦?社会化分工还是不是一个无需争辩的现代社会真理?

其实类似的争论已发生过一次,在开发与测试的分工上。起码在我司,专门的测试人员仍然有不可动摇的作用,而开发人员,则承担了从持续的单元测试和功能测试,最后到手工的性能测试与稳定性测试,总体80%以上的测试工作量,很自然的合作。

所以,还是没有绝对,还是各个组织自己去通过不断的尝试,找到自己最合适的那个平衡点。

架构师应该编码吗?

默认分类admin 发表了文章 • 0 个评论 • 155 次浏览 • 2017-01-17 17:19 • 来自相关话题

架构师应该编码吗?

有些公司认为架构师太宝贵了,不该承担日常编码工作。
有人认为优秀的架构师的重要特征是抽象思维能力,也可以理解为不把时间耗在细节里。
还有一些大型项目通常意味着照看更大的“大局”,有可能你根本没时间写代码。

以上都对。

 

你不必放弃编码,也不要把大部分时间用于编码

你不应该因为“我是架构师”,就把自己排除在编码之外。
但也必须有足够的时间扮演技术架构师的角色。

1. 参与编写代码

要避免成为PPT架构师, 最好是参与实现与交付的过程,确保架构的交付,了解设计在实现上的问题,演进架构而不是画完框图就交给实现团队从此不管。
同时,缩短与团队的距离,保持对团队的影响力,帮助团队对架构的正确理解,分享自己软件开发的经验。

另外,作为开发团队的一份子,你不需要是开发代码最好的。

2. 构建原型、框架和基础

如果不能参与日常编码,至少尝试在设计时快速构建原型去验证你的概念。
还有为团队编写框架和基础,这也是最磨练与体现编码与设计能力的时刻。

3. 进行代码评审

如果完全没有时间编码,至少参与代码评审,了解发生了什么。

4. 实验并与时俱进

如果完全没有时间在工作时间里编码,在工作之外你往往有更多空间来维持编码技能,从贡献开源项目,到不断尝试最新的语言、框架。

一般来说,一个写代码的软件架构师会更有成效也更快乐。

文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/a ... .html 查看全部
架构师应该编码吗?

有些公司认为架构师太宝贵了,不该承担日常编码工作。
有人认为优秀的架构师的重要特征是抽象思维能力,也可以理解为不把时间耗在细节里。
还有一些大型项目通常意味着照看更大的“大局”,有可能你根本没时间写代码。

以上都对。

 

你不必放弃编码,也不要把大部分时间用于编码

你不应该因为“我是架构师”,就把自己排除在编码之外。
但也必须有足够的时间扮演技术架构师的角色。

1. 参与编写代码

要避免成为PPT架构师, 最好是参与实现与交付的过程,确保架构的交付,了解设计在实现上的问题,演进架构而不是画完框图就交给实现团队从此不管。
同时,缩短与团队的距离,保持对团队的影响力,帮助团队对架构的正确理解,分享自己软件开发的经验。

另外,作为开发团队的一份子,你不需要是开发代码最好的。

2. 构建原型、框架和基础

如果不能参与日常编码,至少尝试在设计时快速构建原型去验证你的概念。
还有为团队编写框架和基础,这也是最磨练与体现编码与设计能力的时刻。

3. 进行代码评审

如果完全没有时间编码,至少参与代码评审,了解发生了什么。

4. 实验并与时俱进

如果完全没有时间在工作时间里编码,在工作之外你往往有更多空间来维持编码技能,从贡献开源项目,到不断尝试最新的语言、框架。

一般来说,一个写代码的软件架构师会更有成效也更快乐。

文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/a ... .html

软件设计杂谈

默认分类admin 发表了文章 • 0 个评论 • 172 次浏览 • 2017-01-17 17:06 • 来自相关话题

作者:陈天
链接:https://zhuanlan.zhihu.com/p/20005177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

disclaimer: 本文所讲的设计,非UI/UE的设计,单单指软件代码/功能本身在技术上的设计。UI/UE的主题请出门右转找特赞(Tezign)。:)
在如今这个Lean/Agile横扫一切的年代,设计似乎有了被边缘化的倾向,做事的周期如此之快,似乎已容不下人们更多的思考。MVP(Minimal Viable Produce)在很多团队里演化成一个形而上的图腾,于是工程师们找到了一个完美的借口:我先做个MVP,设计的事,以后再说。
如果纯属个人玩票,有个点子,hack out还说得过去;但要严肃做一个项目,还是要下工夫设计一番,否则,没完没了的返工会让你无语泪千行。
设计首先得搞懂要解决的问题
工程师大多都是很聪明的人,聪明人有个最大的问题就是自负。很多人拿到一个需求,还没太搞明白其外延和内涵,代码就已经在脑袋里流转。这样做出来的系统,纵使再精妙,也免不了承受因需求理解不明确而导致的返工之苦。
搞懂需求这事,说起来简单,做起来难。需求有正确的但表达错误的需求,有正确的但没表达出来的需求,还有过度表达的需求。所以,拿到需求后,先不忙寻找解决方案,多问问自己,工作伙伴,客户follow up questions来澄清需求模糊不清之处。
搞懂需求,还需要了解需求对应的产品,公司,以及(潜在)竞争对手的现状,需求的上下文,以及需求的约束条件。人有二知二不知:
I know that I know

I know that I don’t know

I don’t know that I know

I don’t know that I don’t know

澄清需求的过程,就是不断驱逐无知,掌握现状,上下文和约束条件的过程。
这个主题讲起来很大,且非常重要,但毕竟不是本文的重点,所以就此带过。
寻找(多个)解决方案
如果对问题已经有不错的把握,接下来就是解决方案的发现之旅。这是个考察big picture的活计。同样是满足孩子想要个汽车的愿望,你可以:
去玩具店里买一个现成的

买乐高积木,然后组装

用纸糊一个,或者找块木头,刻一个

这对应软件工程问题的几种解决之道:
购买现成软件(acuquire or licensing),二次开发之(如果需要)

寻找building blocks,组装之(glue)

自己开发(build from scratch, or DIY)

大部分时候,如果a或b的TCO [1] 合理,那就不要选择c。做一个产品的目的是为客户提供某种服务,而不是证明自己能一行行码出出来这个产品。
a是个很重要的点,可惜大部分工程师脑袋里没有钱的概念,或者出于job security的私心,而忽略了。工程师现在越来越贵,能用合理的价格搞定的功能,就不该雇人去打理(自己打脸)。一个产品,最核心的部分不超过整个系统的20%,把人力资源铺在核心的部分,才是软件设计之道。
b我们稍后再讲。
对工程师而言,DIY出一个功能是个极大的诱惑。一种DIY是源自工程师的不满。任何开源软件,在处理某种特定业务逻辑的时候总会有一些不足,眼里如果把这些不足放在,却忽略了人家的好处,是大大的不妥。前两天我听到有人说 "consul sucks, …​, I’ll build our own service discovery framework…​",我就苦笑。我相信他能做出来一个简单的service discovery tool,这不是件特别困难的事情。问题是值不值得去做。如果连处于consul这个层次的基础组件都要自己去做,那要么是心太大,要么是没有定义好自己的软件系统的核心价值(除非系统的核心价值就在于此)。代码一旦写出来,无论是5000行还是50行,都是需要有人去维护的,在系统的生命周期里,每一行自己写的代码都是一笔债务,需要定期不定期地偿还利息。
另外一种DIY是出于工程师的无知。「无知者无畏」在某些场合的效果是正向的,有利于打破陈规。但在软件开发上,还是知识和眼界越丰富越开阔越好。一个无知的工程师在面对某个问题时(比如说service discovery),如果不知道这问题也许有现成的解决方案(consul),自己铆足了劲写一个,大半会有失偏颇(比如说没做上游服务的health check,或者自己本身的high availability),结果bug不断,辛辛苦苦一个个都啃下来,才发现,自己走了很多弯路,费了大半天劲,做了某个开源软件的功能的子集。当然,对工程师而言,这个练手的价值还是很大的,但对公司来说,这是一笔沉重的无意义的支出。
眼界定义了一个人的高度,如果你每天见同类的人,看同质的书籍/视频,(读)写隶属同一domain的代码,那多半眼界不够开阔。互联网的发展一日千里,变化太快,如果把自己禁锢在一方小天地里,很容易成为陶渊明笔下的桃花源中人:乃不知有汉,无论魏晋。
构建灵活且有韧性的系统
如果说之前说的都是废话,那么接下来的和真正的软件设计能扯上些关系。
分解和组合
软件设计是一个把大的问题不断分解,直至原子级的小问题,然后再不断组合的过程。这一点可以类比生物学:原子(keyword/macro)组合成分子(function),分子组合成细胞(module/class),细胞组合成组织(micro service),组织组合成器官(service),进而组合成生物(system)。
一个如此组合而成系统,是满足关注点分离(Separation of Concerns)的。大到一个器官,小到一个细胞,都各司其职,把自己要做的事情做到极致。心脏不必关心肾脏会干什么,它只需要做好自己的事情:把新鲜血液通过动脉排出,再把各个器官用过的血液从静脉回收。
分解和组合在软件设计中的作用如此重要,以至于一个系统如果合理分解,那么日后维护的代价就要小得多。同样讲关注点分离,不同的工程师,分离的方式可能完全不同。但究其根本,还有有一些规律可循。
总线(System Bus)
首先我们要把系统的总线定义出来。人体的总线,大的有几条:血管(动脉,静脉),神经网络,气管,输尿管。它们有的完全负责与外界的交互(气管,输尿管),有的完全是内部的信息中枢(血管),有的内外兼修(神经网络)。
总线把生产者和消费者分离,让彼此互不依赖。心脏往外供血时,把血压入动脉血管就是了。它并不需要知道谁是接收者。
同样的,回到我们熟悉的计算机系统,CPU访问内存也是如此:它发送一条消息给总线,总线通知RAM读取数据,然后RAM把数据返回给总线,CPU再获取之。整个过程中CPU只知道一个内存地址,毋须知道访问的具体是哪个内存槽的哪块内存 —— 总线将二者屏蔽开。
学过计算机系统的同学应该都知道,经典的PC结构有几种总线:数据总线,地址总线,控制总线,扩展总线等;做过网络设备的同学也都知道,一个经典的网络设备,其软件系统的总线分为:control plane和data plane。
路由(routing)
有了总线的概念,接下来必然要有路由。我们看人体的血管:
每一处分叉,就涉及到一次路由。
路由分为外部路由和内部路由。外部路由处理输入,把不同的输入dispatch到系统里不同的组件。做web app的,可能没有意识到,但其实每个web framework,最关键的组件之一就是url dispatch。HTTP的伟大之处就是每个request,都能通过url被dispatch到不同的handler处理。而url是目录式的,可以层层演进 —— 就像分形几何,一个大的系统,通过不断重复的模式,组合起来 —— 非常利于系统的扩展。遗憾的是,我们自己做系统,对于输入既没有总线的考量,又无路由的概念,if-else下去,久而久之,代码便绕成了意大利面条。
再举一例:DOM中的event bubble,在javascript处理起来已然隐含着路由的概念。你只需定义当某个事件(如onclick)发生时的callback函数就好,至于这事件怎么通过eventloop抵达回调函数,无需关心。好的路由系统剥茧抽丝,把繁杂的信息流正确送到处理者手中。
外部路由总还有「底层」为我们完成,内部路由则需工程师考虑。service级别的路由(数据流由哪个service处理)可以用consul等service discovery组件,service内部的路由(数据流到达后怎么处理)则需要自己完成。路由的具体方式有很多种,pattern matching最为常见。
无论用何种方式路由,数据抵达总线前为其定义Identity(ID)非常重要,你可以管这个过程叫data normalization,data encapsulation等,总之,一个消息能被路由,需要有个用于路由的ID。这ID可以是url,可以是一个message header,也可以是一个label(想象MPLS的情况)。当我们为数据赋予一个个合理的ID后,如何路由便清晰可见。
队列(Queue)
对于那些并非需要立即处理的数据,可以使用队列。队列也有把生产者和消费者分离的功效。队列有:
single producer single consumer(SPSC)

single producer multiple consumers(SPMC)

multiple producers single consumer(MPSC)

multiple producers multiple consumers(MPMC)

仔细想想,队列其实就是总线+路由(可选)+存储的一个特殊版本。一般而言,system bus之上是系统的各个service,每个service再用service bus(或者queue)把micro service chain起来,然后每个micro service内部的组件间,再用queue连接起来。
有了队列,有利于提高流水线的效率。一般而言,流水线的处理速度取决于最慢的组件。队列的存在,让慢速组件有机会运行多份,来弥补生产者和消费者速度上的差距。
Pub/Sub
存储在队列中的数据,除路由外,还有一种处理方式:pub/sub。和路由相似,pub/sub将生产者和消费者分离;但二者不同之处在于,路由的目的地由路由表中的表项控制,而pub/sub一般由publisher控制 [2]:任何subscribe某个数据的consumer,都会到publisher处注册,publisher由此可以定向发送消息。
协议(protocol)
一旦我们把系统分解成一个个service,service再分解成micro service,彼此之间互不依赖,仅仅通过总线或者队列来通讯,那么,我们就需要协议来定义彼此的行为。协议听起来很高大上,其实不然。我们写下的每个function(或者每个class),其实就是在定义一个不成文的协议:function的arity是什么,接受什么参数,返回什么结果。调用者需严格按照协议调用方能得到正确的结果。
service级别的协议是一份SLA:服务的endpoint是什么,版本是什么,接收什么格式的消息,返回什么格式的消息,消息在何种网络协议上承载,需要什么样的authorization,可以正常服务的最大吞吐量(throughput)是什么,在什么情况下会触发throttling等等。
头脑中有了总线,路由,队列,协议等这些在computer science 101中介绍的基础概念,系统的分解便有迹可寻:面对一个系统的设计,你要做的不再是一道作文题,而是一道填空题:在若干条system bus里填上其名称和流进流出的数据,在system bus之上的一个个方框里填上服务的名称和服务的功能。然后,每个服务再以此类推,直到感觉毋须再细化为止。
组成系统的必要服务
有些管理性质的服务,尽管和业务逻辑直接关系不大,但无论是任何系统,都需要考虑构建,这里罗列一二。
代谢(sweeping)
一个活着的生物时时刻刻都进行着新陈代谢:每时每刻新的细胞取代老的细胞,同时身体中的「垃圾」通过排泄系统排出体外。一个运转有序的城市也有新陈代谢:下水道,垃圾场,污水处理等维持城市的正常功能。没有了代谢功能,生物会凋零,城市会荒芜。
软件系统也是如此。日志会把硬盘写满,软件会失常,硬件会失效,网络会拥塞等等。一个好的软件系统需要一个好的代谢系统:出现异常的服务会被关闭,同样的服务会被重新启动,恢复运行。
代谢系统可以参考erlang的supervisor/child process结构,以及supervision tree。很多软件,都运行在简单的supervision tree模式下,如nginx。
高可用性(HA)
每个人都有两个肾。为了apple watch卖掉一个肾,另一个还能保证人体的正常工作。当然,人的两个肾是Active-Active工作模式,内部的肾元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家这service的做的),少了一个,performance会一点点有折扣,但可以忽略不计。
大部分软件系统里的各种服务也需要高可用性:除非完全无状态的服务,且服务重启时间在ms级。服务的高可用性和路由是息息相关的:高可用性往往意味着同一服务的冗余,同时也意味着负载分担。好的路由系统(如consul)能够对路由至同一服务的数据在多个冗余服务间进行负载分担,同时在检测出某个失效服务后,将数据路只由至正常运作的服务。
高可用性还意味着非关键服务,即便不可恢复,也只会导致系统降级,而不会让整个系统无法访问。就像壁虎的尾巴断了不妨碍壁虎逃命,人摔伤了手臂还能吃饭一样,一个软件系统里统计模块的异常不该让用户无法访问他的个人页面。
安保(security)
安保服务分为主动安全和被动安全。authentication/authorization + TLS + 敏感信息加密 + 最小化输入输出接口可以算是主动安全,防火墙等安防系统则是被动安全。
继续拿你的肾来比拟 —— 肾脏起码有两大安全系统:
输入安全。肾器的厚厚的器官膜,保护器官的输入输出安全 —— 主要的输入输出只能是肾动脉,肾静脉和输尿管。

环境安全。肾器里有大量脂肪填充,避免在撞击时对核心功能的损伤。

除此之外,人体还提供了包括免疫系统,皮肤,骨骼,空腔等一系列安全系统,从各个维度最大程度保护一个器官的正常运作。如果我们仔细研究生物,就会发现,安保是个一揽子解决方案:小到细胞,大到整个人体,都有各自的安全措施。一个软件系统也需如此考虑系统中各个层次的安全。
透支保护(overdraft protection)
任何系统,任何服务都是有服务能力的 —— 当这能力被透支时,需要一定的应急计划。如果使用拥有auto scaling的云服务(如AWS),动态扩容是最好的解决之道,但受限于所用的解决方案,它并非万灵药,AWS的auto scaling依赖于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB对某些业务,如websocket,支持不佳;而第三方的load balancer,则需要考虑部署,与Amazon的auto scaling结合(需要写点代码),避免单点故障,保证自身的capacity等一堆头疼事。
在无法auto scaling的场景最通用的做法是back pressure,把压力反馈到源头。就好像你不断熬夜,最后大脑受不了,逼着你睡觉一样。还有一种做法是服务降级,停掉非核心的service/micro-service,如analytical service,ad service,保证核心功能正常。
把设计的成果讲给别人听
完成了分解和组合,也严肃对待了诸多与业务没有直接关系,但又不得不做的必要功能后,接下来就是要把设计在白板上画下来,讲给任何一个利益相关者听。听他们的反馈。设计不是一个闭门造车的过程,全程都需要和各种利益相关者交流。然而,很多人都忽视了设计定型后,继续和外界交流的必要性。很多人会认为:我的软件架构,设计结果和工程有关,为何要讲给工程师以外的人听?他们懂么?
其实pitch本身就是自我学习和自我修正的一部分。当着一个人或者几个人的面,在白板上画下脑海中的设计的那一刻,你就会有直觉哪个地方似乎有问题,这是很奇特的一种体验:你自己画给自己看并不会产生这种直觉。这大概是面对公众的焦灼产生的肾上腺素的效果。:)
此外,从听者的表情,或者他们提的听起来很傻很天真的问题,你会进一步知道哪些地方你以为你搞通了,其实自己是一知半解。太简单,太基础的问题,我们take it for granted,不屑去问自己,非要有人点出,自己才发现:啊,原来这里我也不懂哈。这就是破解 "you don’t know what you don’t know" 之法。
记得看过一个video,主讲人大谈企业文化,有个哥们傻乎乎发问:so what it culture literally? 主讲人愣了一下,拖拖拉拉讲了一堆自己都不能让自己信服的废话。估计回头他就去查韦氏词典了。
最后,总有人在某些领域的知识更丰富一些,他们会告诉你你一些你知道自己不懂的事情。填补了 "you know that you don’t know" 的空缺。
设计时的tradeoff
Rich hickey(clojure作者)在某个演讲中说:
everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.

所以,下回再腆着脸说:偶做了些tradeoff,先确保自己做足了功课再说。
设计的改变不可避免
设计不是一锤子买卖,改变不可避免。我之前的一个老板,喜欢把:change is your friend 挂在口头。软件开发的整个生命周期,变更是家常便饭,以至于变更管理都生出一门学问。软件的设计期更是如此。人总会犯错,设计总有缺陷,需求总会变化,老板总会指手画脚,PM总有一天会亮出獠牙,不再是贴心大哥,或者美萌小妹。。。所以,据理力争,然后接受必要的改变即可。连凯恩斯他老人家都说:

What do you do, sir?
(文章很长,感谢阅读至此,这文章3/30号开始写,拖拖拉拉算是凑合完成了)
如果您觉得这篇文章不错,请点赞。多谢!
欢迎订阅公众号『程序人生』(搜索微信号 programmer_life)。每篇文章都力求原汁原味,北京时间中午12点左右,美西时间下午8点左右与您相会。
1. Total Cost of Ownership
2. 要看具体的pub/sub系统的实现方式
原文链接:https://zhuanlan.zhihu.com/p/2 ... attle 查看全部

作者:陈天
链接:https://zhuanlan.zhihu.com/p/20005177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

disclaimer: 本文所讲的设计,非UI/UE的设计,单单指软件代码/功能本身在技术上的设计。UI/UE的主题请出门右转找特赞(Tezign)。:)
在如今这个Lean/Agile横扫一切的年代,设计似乎有了被边缘化的倾向,做事的周期如此之快,似乎已容不下人们更多的思考。MVP(Minimal Viable Produce)在很多团队里演化成一个形而上的图腾,于是工程师们找到了一个完美的借口:我先做个MVP,设计的事,以后再说。
如果纯属个人玩票,有个点子,hack out还说得过去;但要严肃做一个项目,还是要下工夫设计一番,否则,没完没了的返工会让你无语泪千行。
设计首先得搞懂要解决的问题
工程师大多都是很聪明的人,聪明人有个最大的问题就是自负。很多人拿到一个需求,还没太搞明白其外延和内涵,代码就已经在脑袋里流转。这样做出来的系统,纵使再精妙,也免不了承受因需求理解不明确而导致的返工之苦。
搞懂需求这事,说起来简单,做起来难。需求有正确的但表达错误的需求,有正确的但没表达出来的需求,还有过度表达的需求。所以,拿到需求后,先不忙寻找解决方案,多问问自己,工作伙伴,客户follow up questions来澄清需求模糊不清之处。
搞懂需求,还需要了解需求对应的产品,公司,以及(潜在)竞争对手的现状,需求的上下文,以及需求的约束条件。人有二知二不知:
I know that I know

I know that I don’t know

I don’t know that I know

I don’t know that I don’t know

澄清需求的过程,就是不断驱逐无知,掌握现状,上下文和约束条件的过程。
这个主题讲起来很大,且非常重要,但毕竟不是本文的重点,所以就此带过。
寻找(多个)解决方案
如果对问题已经有不错的把握,接下来就是解决方案的发现之旅。这是个考察big picture的活计。同样是满足孩子想要个汽车的愿望,你可以:
去玩具店里买一个现成的

买乐高积木,然后组装

用纸糊一个,或者找块木头,刻一个

这对应软件工程问题的几种解决之道:
购买现成软件(acuquire or licensing),二次开发之(如果需要)

寻找building blocks,组装之(glue)

自己开发(build from scratch, or DIY)

大部分时候,如果a或b的TCO [1] 合理,那就不要选择c。做一个产品的目的是为客户提供某种服务,而不是证明自己能一行行码出出来这个产品。
a是个很重要的点,可惜大部分工程师脑袋里没有钱的概念,或者出于job security的私心,而忽略了。工程师现在越来越贵,能用合理的价格搞定的功能,就不该雇人去打理(自己打脸)。一个产品,最核心的部分不超过整个系统的20%,把人力资源铺在核心的部分,才是软件设计之道。
b我们稍后再讲。
对工程师而言,DIY出一个功能是个极大的诱惑。一种DIY是源自工程师的不满。任何开源软件,在处理某种特定业务逻辑的时候总会有一些不足,眼里如果把这些不足放在,却忽略了人家的好处,是大大的不妥。前两天我听到有人说 "consul sucks, …​, I’ll build our own service discovery framework…​",我就苦笑。我相信他能做出来一个简单的service discovery tool,这不是件特别困难的事情。问题是值不值得去做。如果连处于consul这个层次的基础组件都要自己去做,那要么是心太大,要么是没有定义好自己的软件系统的核心价值(除非系统的核心价值就在于此)。代码一旦写出来,无论是5000行还是50行,都是需要有人去维护的,在系统的生命周期里,每一行自己写的代码都是一笔债务,需要定期不定期地偿还利息。
另外一种DIY是出于工程师的无知。「无知者无畏」在某些场合的效果是正向的,有利于打破陈规。但在软件开发上,还是知识和眼界越丰富越开阔越好。一个无知的工程师在面对某个问题时(比如说service discovery),如果不知道这问题也许有现成的解决方案(consul),自己铆足了劲写一个,大半会有失偏颇(比如说没做上游服务的health check,或者自己本身的high availability),结果bug不断,辛辛苦苦一个个都啃下来,才发现,自己走了很多弯路,费了大半天劲,做了某个开源软件的功能的子集。当然,对工程师而言,这个练手的价值还是很大的,但对公司来说,这是一笔沉重的无意义的支出。
眼界定义了一个人的高度,如果你每天见同类的人,看同质的书籍/视频,(读)写隶属同一domain的代码,那多半眼界不够开阔。互联网的发展一日千里,变化太快,如果把自己禁锢在一方小天地里,很容易成为陶渊明笔下的桃花源中人:乃不知有汉,无论魏晋。
构建灵活且有韧性的系统
如果说之前说的都是废话,那么接下来的和真正的软件设计能扯上些关系。
分解和组合
软件设计是一个把大的问题不断分解,直至原子级的小问题,然后再不断组合的过程。这一点可以类比生物学:原子(keyword/macro)组合成分子(function),分子组合成细胞(module/class),细胞组合成组织(micro service),组织组合成器官(service),进而组合成生物(system)。
一个如此组合而成系统,是满足关注点分离(Separation of Concerns)的。大到一个器官,小到一个细胞,都各司其职,把自己要做的事情做到极致。心脏不必关心肾脏会干什么,它只需要做好自己的事情:把新鲜血液通过动脉排出,再把各个器官用过的血液从静脉回收。
分解和组合在软件设计中的作用如此重要,以至于一个系统如果合理分解,那么日后维护的代价就要小得多。同样讲关注点分离,不同的工程师,分离的方式可能完全不同。但究其根本,还有有一些规律可循。
总线(System Bus)
首先我们要把系统的总线定义出来。人体的总线,大的有几条:血管(动脉,静脉),神经网络,气管,输尿管。它们有的完全负责与外界的交互(气管,输尿管),有的完全是内部的信息中枢(血管),有的内外兼修(神经网络)。
总线把生产者和消费者分离,让彼此互不依赖。心脏往外供血时,把血压入动脉血管就是了。它并不需要知道谁是接收者。
同样的,回到我们熟悉的计算机系统,CPU访问内存也是如此:它发送一条消息给总线,总线通知RAM读取数据,然后RAM把数据返回给总线,CPU再获取之。整个过程中CPU只知道一个内存地址,毋须知道访问的具体是哪个内存槽的哪块内存 —— 总线将二者屏蔽开。
学过计算机系统的同学应该都知道,经典的PC结构有几种总线:数据总线,地址总线,控制总线,扩展总线等;做过网络设备的同学也都知道,一个经典的网络设备,其软件系统的总线分为:control plane和data plane。
路由(routing)
有了总线的概念,接下来必然要有路由。我们看人体的血管:
每一处分叉,就涉及到一次路由。
路由分为外部路由和内部路由。外部路由处理输入,把不同的输入dispatch到系统里不同的组件。做web app的,可能没有意识到,但其实每个web framework,最关键的组件之一就是url dispatch。HTTP的伟大之处就是每个request,都能通过url被dispatch到不同的handler处理。而url是目录式的,可以层层演进 —— 就像分形几何,一个大的系统,通过不断重复的模式,组合起来 —— 非常利于系统的扩展。遗憾的是,我们自己做系统,对于输入既没有总线的考量,又无路由的概念,if-else下去,久而久之,代码便绕成了意大利面条。
再举一例:DOM中的event bubble,在javascript处理起来已然隐含着路由的概念。你只需定义当某个事件(如onclick)发生时的callback函数就好,至于这事件怎么通过eventloop抵达回调函数,无需关心。好的路由系统剥茧抽丝,把繁杂的信息流正确送到处理者手中。
外部路由总还有「底层」为我们完成,内部路由则需工程师考虑。service级别的路由(数据流由哪个service处理)可以用consul等service discovery组件,service内部的路由(数据流到达后怎么处理)则需要自己完成。路由的具体方式有很多种,pattern matching最为常见。
无论用何种方式路由,数据抵达总线前为其定义Identity(ID)非常重要,你可以管这个过程叫data normalization,data encapsulation等,总之,一个消息能被路由,需要有个用于路由的ID。这ID可以是url,可以是一个message header,也可以是一个label(想象MPLS的情况)。当我们为数据赋予一个个合理的ID后,如何路由便清晰可见。
队列(Queue)
对于那些并非需要立即处理的数据,可以使用队列。队列也有把生产者和消费者分离的功效。队列有:
single producer single consumer(SPSC)

single producer multiple consumers(SPMC)

multiple producers single consumer(MPSC)

multiple producers multiple consumers(MPMC)

仔细想想,队列其实就是总线+路由(可选)+存储的一个特殊版本。一般而言,system bus之上是系统的各个service,每个service再用service bus(或者queue)把micro service chain起来,然后每个micro service内部的组件间,再用queue连接起来。
有了队列,有利于提高流水线的效率。一般而言,流水线的处理速度取决于最慢的组件。队列的存在,让慢速组件有机会运行多份,来弥补生产者和消费者速度上的差距。
Pub/Sub
存储在队列中的数据,除路由外,还有一种处理方式:pub/sub。和路由相似,pub/sub将生产者和消费者分离;但二者不同之处在于,路由的目的地由路由表中的表项控制,而pub/sub一般由publisher控制 [2]:任何subscribe某个数据的consumer,都会到publisher处注册,publisher由此可以定向发送消息。
协议(protocol)
一旦我们把系统分解成一个个service,service再分解成micro service,彼此之间互不依赖,仅仅通过总线或者队列来通讯,那么,我们就需要协议来定义彼此的行为。协议听起来很高大上,其实不然。我们写下的每个function(或者每个class),其实就是在定义一个不成文的协议:function的arity是什么,接受什么参数,返回什么结果。调用者需严格按照协议调用方能得到正确的结果。
service级别的协议是一份SLA:服务的endpoint是什么,版本是什么,接收什么格式的消息,返回什么格式的消息,消息在何种网络协议上承载,需要什么样的authorization,可以正常服务的最大吞吐量(throughput)是什么,在什么情况下会触发throttling等等。
头脑中有了总线,路由,队列,协议等这些在computer science 101中介绍的基础概念,系统的分解便有迹可寻:面对一个系统的设计,你要做的不再是一道作文题,而是一道填空题:在若干条system bus里填上其名称和流进流出的数据,在system bus之上的一个个方框里填上服务的名称和服务的功能。然后,每个服务再以此类推,直到感觉毋须再细化为止。
组成系统的必要服务
有些管理性质的服务,尽管和业务逻辑直接关系不大,但无论是任何系统,都需要考虑构建,这里罗列一二。
代谢(sweeping)
一个活着的生物时时刻刻都进行着新陈代谢:每时每刻新的细胞取代老的细胞,同时身体中的「垃圾」通过排泄系统排出体外。一个运转有序的城市也有新陈代谢:下水道,垃圾场,污水处理等维持城市的正常功能。没有了代谢功能,生物会凋零,城市会荒芜。
软件系统也是如此。日志会把硬盘写满,软件会失常,硬件会失效,网络会拥塞等等。一个好的软件系统需要一个好的代谢系统:出现异常的服务会被关闭,同样的服务会被重新启动,恢复运行。
代谢系统可以参考erlang的supervisor/child process结构,以及supervision tree。很多软件,都运行在简单的supervision tree模式下,如nginx。
高可用性(HA)
每个人都有两个肾。为了apple watch卖掉一个肾,另一个还能保证人体的正常工作。当然,人的两个肾是Active-Active工作模式,内部的肾元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家这service的做的),少了一个,performance会一点点有折扣,但可以忽略不计。
大部分软件系统里的各种服务也需要高可用性:除非完全无状态的服务,且服务重启时间在ms级。服务的高可用性和路由是息息相关的:高可用性往往意味着同一服务的冗余,同时也意味着负载分担。好的路由系统(如consul)能够对路由至同一服务的数据在多个冗余服务间进行负载分担,同时在检测出某个失效服务后,将数据路只由至正常运作的服务。
高可用性还意味着非关键服务,即便不可恢复,也只会导致系统降级,而不会让整个系统无法访问。就像壁虎的尾巴断了不妨碍壁虎逃命,人摔伤了手臂还能吃饭一样,一个软件系统里统计模块的异常不该让用户无法访问他的个人页面。
安保(security)
安保服务分为主动安全和被动安全。authentication/authorization + TLS + 敏感信息加密 + 最小化输入输出接口可以算是主动安全,防火墙等安防系统则是被动安全。
继续拿你的肾来比拟 —— 肾脏起码有两大安全系统:
输入安全。肾器的厚厚的器官膜,保护器官的输入输出安全 —— 主要的输入输出只能是肾动脉,肾静脉和输尿管。

环境安全。肾器里有大量脂肪填充,避免在撞击时对核心功能的损伤。

除此之外,人体还提供了包括免疫系统,皮肤,骨骼,空腔等一系列安全系统,从各个维度最大程度保护一个器官的正常运作。如果我们仔细研究生物,就会发现,安保是个一揽子解决方案:小到细胞,大到整个人体,都有各自的安全措施。一个软件系统也需如此考虑系统中各个层次的安全。
透支保护(overdraft protection)
任何系统,任何服务都是有服务能力的 —— 当这能力被透支时,需要一定的应急计划。如果使用拥有auto scaling的云服务(如AWS),动态扩容是最好的解决之道,但受限于所用的解决方案,它并非万灵药,AWS的auto scaling依赖于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB对某些业务,如websocket,支持不佳;而第三方的load balancer,则需要考虑部署,与Amazon的auto scaling结合(需要写点代码),避免单点故障,保证自身的capacity等一堆头疼事。
在无法auto scaling的场景最通用的做法是back pressure,把压力反馈到源头。就好像你不断熬夜,最后大脑受不了,逼着你睡觉一样。还有一种做法是服务降级,停掉非核心的service/micro-service,如analytical service,ad service,保证核心功能正常。
把设计的成果讲给别人听
完成了分解和组合,也严肃对待了诸多与业务没有直接关系,但又不得不做的必要功能后,接下来就是要把设计在白板上画下来,讲给任何一个利益相关者听。听他们的反馈。设计不是一个闭门造车的过程,全程都需要和各种利益相关者交流。然而,很多人都忽视了设计定型后,继续和外界交流的必要性。很多人会认为:我的软件架构,设计结果和工程有关,为何要讲给工程师以外的人听?他们懂么?
其实pitch本身就是自我学习和自我修正的一部分。当着一个人或者几个人的面,在白板上画下脑海中的设计的那一刻,你就会有直觉哪个地方似乎有问题,这是很奇特的一种体验:你自己画给自己看并不会产生这种直觉。这大概是面对公众的焦灼产生的肾上腺素的效果。:)
此外,从听者的表情,或者他们提的听起来很傻很天真的问题,你会进一步知道哪些地方你以为你搞通了,其实自己是一知半解。太简单,太基础的问题,我们take it for granted,不屑去问自己,非要有人点出,自己才发现:啊,原来这里我也不懂哈。这就是破解 "you don’t know what you don’t know" 之法。
记得看过一个video,主讲人大谈企业文化,有个哥们傻乎乎发问:so what it culture literally? 主讲人愣了一下,拖拖拉拉讲了一堆自己都不能让自己信服的废话。估计回头他就去查韦氏词典了。
最后,总有人在某些领域的知识更丰富一些,他们会告诉你你一些你知道自己不懂的事情。填补了 "you know that you don’t know" 的空缺。
设计时的tradeoff
Rich hickey(clojure作者)在某个演讲中说:
everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.

所以,下回再腆着脸说:偶做了些tradeoff,先确保自己做足了功课再说。
设计的改变不可避免
设计不是一锤子买卖,改变不可避免。我之前的一个老板,喜欢把:change is your friend 挂在口头。软件开发的整个生命周期,变更是家常便饭,以至于变更管理都生出一门学问。软件的设计期更是如此。人总会犯错,设计总有缺陷,需求总会变化,老板总会指手画脚,PM总有一天会亮出獠牙,不再是贴心大哥,或者美萌小妹。。。所以,据理力争,然后接受必要的改变即可。连凯恩斯他老人家都说:

What do you do, sir?
(文章很长,感谢阅读至此,这文章3/30号开始写,拖拖拉拉算是凑合完成了)
如果您觉得这篇文章不错,请点赞。多谢!
欢迎订阅公众号『程序人生』(搜索微信号 programmer_life)。每篇文章都力求原汁原味,北京时间中午12点左右,美西时间下午8点左右与您相会。
1. Total Cost of Ownership
2. 要看具体的pub/sub系统的实现方式
原文链接:https://zhuanlan.zhihu.com/p/2 ... attle

服务化体系之-限流

微信admin 发表了文章 • 0 个评论 • 184 次浏览 • 2017-01-17 10:53 • 来自相关话题

原文链接:http://calvin1978.blogcn.com/articles/ratelimiter.html
(上)设计篇
在实现算法之前,先临时客串一下产品经理,尝试用最少的字,把“限流”这简单二字所展开的种种需求给说清楚。
1.各种目的
1. 保护每个服务节点。
2. 保护服务集群背后的资源,比如数据库。
3. 避免单个调用者过度使用服务,影响其他调用者。

2. 各种设定维度
2.1. 节点级别 vs 集群级别
如果以保护每个服务节点为目的,可以简单的在本地做节点级别的限流。
但如果以保护服务集群背后的资源为目的,就需要做集群级别的限流。
集群级别的一个做法是使用Redis, Memcached之类做一个集群级别的计数器。但额外多一次访问Redis的消耗,代价有点大。
而另一个做法是把集群限流总数分摊到每个节点上,但一是流量不是100%均匀时会不够精准,二是如果使用Docker动态缩扩容,需要动态更新这个分摊数。

2.2 客户端 vs 服务端
当以保护服务端的节点为目的,应在服务端设定,因为有多少调用者是未知的。
当以避免单个调用者过度使用服务为目的,可以针对客户端节点或客户端集群设定限流。此时限流可以在客户端实现,节约了网络往返;也可以在服务端实现,让所有限流逻辑集中于一处发生。

2.3 服务级别 vs 方法级别
可以对消耗特别大的方法专门配置,比如复杂的查询,昂贵的写操作。
然后其他方法使用统一的值,或者配一个所有方法加起来的总和。

3. 各种触发条件
触发条件的设定,难点在于服务的容量,受着本服务节点的能力,背后的资源的能力,下游服务的响应的多重约束。

3.1 静态配置固定值
当然,这个固定值可以被动态更新。

3.2 根据预设规则触发
规则的条件可以是服务平均时延,可以是背后数据库的CPU情况等。
比如平时不限流,当服务时延大于100ms,则触发限流500 QPS。
还可以是多级条件,比如>100ms 限流500 QPS, >200ms 限流200 QPS。
条件反馈由监控系统配合服务治理中心来完成,但有些指标如服务延时也可以由服务节点自行计算,如服务延时。

3.3 全动态自动增减调控
这个诱人的想法,永远存在于老板的心里。

4. 各种处理
4.1 立刻返回拒绝错误
由客户端进行降级处理。

4.2 进行短暂的等待
短暂等待,期待有容量空余,直到超时,依然是客户端降级。

4.3 触发服务降级,调用服务端的降级方法
服务端的降级方法,走服务端的简单路径与预设值,则代表了服务端这边的态度和逻辑。
客户端降级与服务端降级各有适用的场景,等下一篇《服务降级》 再详述。

(下)实现篇
开涛的《聊聊高并发系统之限流特技》 已讲得非常好,这里再简单补充两句。
另一篇《接口限流算法总结》 也非常好非常详细,差别只在于对令牌桶的突发量的描述上,我有我自己的理解。

1. 并发控制
并发控制本身就是一种最简单的限流,包括:
框架本身的连接/线程限制
各种数据库连接池,Http连接池
服务/方法级别的信号量计数器限制

2. 窗口流量控制
窗口流量控制有几种做法。
一种最简单的计数器,维护一个单位时间内的Counter,如判断单位时间已经过去,则将Counter重置零。开涛文章里利用guava cache也是一种做法。
但此做法有时被认为粒度太粗,没有把QPS平摊到一秒的各个毫秒里,同时也没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,将目光移动一下,就看到在两毫秒内发生了两倍的TPS。
于是,另一个改进版的做法是把单位时间拆分,比如把1秒分成5个200毫秒宽的桶,然后以滑动窗口来计算限流,好像就能很大程度上解决上面的两个问题了。。。。滑动窗口的算法,统一到《熔断篇》再来讨论。
最后一组就是漏桶算法或令牌桶算法了,下面会详述。
另外,集群规模的计数器,基于Memcachd或Redis的实现,也见开涛的博客。

3. 令牌桶算法(Token Bucket )
随着时间流逝,系统会按速率 1/rate 的时间间隔(如果rate=100,则间隔是10ms)往桶里加入Token
如果桶满了(burst),则丢弃新加入的令牌
每个请求进来,都要消耗一个Token,如果桶空了,则丢弃请求或等待有新的令牌放入。





4. 漏桶算法(Leaky Bucket )
简单的想象有一个木桶,有新请求就是不断的倒水进来,然后桶底下有个洞,按照固定的速率把水漏走,如果水进来的速度比漏走的快,桶可能就会满了,然后就拒绝请求。
可见,两个算法的基本描述上,只是方向不一样,其他没什么不同的。所以WikiMedia里说, 两个算法实现可以一样,对于相同的参数得到的限流效果是一样的。

5. Guava版的令牌桶实现 -- RateLimiter
Guava已实现了一个性能非常好的RateLimiter,基本不需要我们再费心实现。但其中有一些实现的细节,需要我们留意:
1. 支持桶外预借的突发
突发,原本是指如果单位时间的前半段流量较少,桶里会积累一些令牌,然后支持来一波大的瞬时流量,将前面积累的令牌消耗掉。
但在RateLimiter的实现里,还多了个桶外预借(我自己给他的命名),就是即使桶里没有多少令牌,你也可以消耗一波大的,然后桶里面在时间段内都没有新令牌。比如桶的容量是5,桶里面现在只有1个令牌,如果你要拿5个令牌,也可以,清了桶里的一个令牌,再预借4个。然后再过800毫秒,桶里才会出现新令牌。
可见,Guava版的RateLimiter对突发的支持,比原版的两种算法都要大,你几乎随时都可以一次过消费burst个令牌,不管现在桶里有没有积累的令牌。
不过有个副作用,就是如果前面都没什么流量,桶里累积了5个令牌,则你其实可以一次过消费10个令牌。。。不过那么一下,超借完接下来还是固定速率的,直到还清了旧账,才可能再来那么一下。

2. 支持等待可用令牌与立刻返回两种接口

3. 单位时段是秒,这有点不太好用,不支持设定5分钟的单位。

4. 发令牌的计算粒度是MicroSeconds,也就是最多支持一百万的QPS。

6. 唯品会自家框架的实现

6.1 各种设定维度
1. 在每个服务节点上进行限流统计。
如果要保护后端的数据库资源,需要自己分摊一下集群总流量到每台服务器上(没有全docker化自动伸缩前,根据当前节点数量自动调整阀值的意义不大)

2. 支持不同的调用者集群(比如购物车服务与订单服务)不同的阀值,当然也支持剔除特定调用者后的总阀值。

3. 在方法级别上统计限流,可以对消耗特别大的特定方法设置另外的阀值,当然其他方法就用一个统一的值。
暂不支持按服务中所有方法的总流量来限流,因为除非所有方法的消耗很接近,否则意义不大。

6.2 各种触发条件
目前只支持静态配置的触发条件。
未来的服务治理中心,要从各种监控系统里采集数据,然后根据多级条件动态调整规则。又或者服务节点上先找些自己也能统计的指标如服务延时,先动态起来,就是有重复计算之嫌。

6.3 各种处理
目前只支持直接返回限流异常,由客户端来进行降级逻辑。
未来要支持调用在服务端的降级方法,由服务端提供降级逻辑的短路径。

6.4 计数器实现
限流只在服务端进行。不重新发明轮子,直接就是Guava的RateLimiter。
对于客户端的并发数限制,未来也可以做一下。 查看全部
原文链接:http://calvin1978.blogcn.com/articles/ratelimiter.html
(上)设计篇
在实现算法之前,先临时客串一下产品经理,尝试用最少的字,把“限流”这简单二字所展开的种种需求给说清楚。
1.各种目的
1. 保护每个服务节点。
2. 保护服务集群背后的资源,比如数据库。
3. 避免单个调用者过度使用服务,影响其他调用者。

2. 各种设定维度
2.1. 节点级别 vs 集群级别
如果以保护每个服务节点为目的,可以简单的在本地做节点级别的限流。
但如果以保护服务集群背后的资源为目的,就需要做集群级别的限流。
集群级别的一个做法是使用Redis, Memcached之类做一个集群级别的计数器。但额外多一次访问Redis的消耗,代价有点大。
而另一个做法是把集群限流总数分摊到每个节点上,但一是流量不是100%均匀时会不够精准,二是如果使用Docker动态缩扩容,需要动态更新这个分摊数。

2.2 客户端 vs 服务端
当以保护服务端的节点为目的,应在服务端设定,因为有多少调用者是未知的。
当以避免单个调用者过度使用服务为目的,可以针对客户端节点或客户端集群设定限流。此时限流可以在客户端实现,节约了网络往返;也可以在服务端实现,让所有限流逻辑集中于一处发生。

2.3 服务级别 vs 方法级别
可以对消耗特别大的方法专门配置,比如复杂的查询,昂贵的写操作。
然后其他方法使用统一的值,或者配一个所有方法加起来的总和。

3. 各种触发条件
触发条件的设定,难点在于服务的容量,受着本服务节点的能力,背后的资源的能力,下游服务的响应的多重约束。

3.1 静态配置固定值
当然,这个固定值可以被动态更新。

3.2 根据预设规则触发
规则的条件可以是服务平均时延,可以是背后数据库的CPU情况等。
比如平时不限流,当服务时延大于100ms,则触发限流500 QPS。
还可以是多级条件,比如>100ms 限流500 QPS, >200ms 限流200 QPS。
条件反馈由监控系统配合服务治理中心来完成,但有些指标如服务延时也可以由服务节点自行计算,如服务延时。

3.3 全动态自动增减调控
这个诱人的想法,永远存在于老板的心里。

4. 各种处理
4.1 立刻返回拒绝错误
由客户端进行降级处理。

4.2 进行短暂的等待
短暂等待,期待有容量空余,直到超时,依然是客户端降级。

4.3 触发服务降级,调用服务端的降级方法
服务端的降级方法,走服务端的简单路径与预设值,则代表了服务端这边的态度和逻辑。
客户端降级与服务端降级各有适用的场景,等下一篇《服务降级》 再详述。

(下)实现篇
开涛的《聊聊高并发系统之限流特技》 已讲得非常好,这里再简单补充两句。
另一篇《接口限流算法总结》 也非常好非常详细,差别只在于对令牌桶的突发量的描述上,我有我自己的理解。

1. 并发控制
并发控制本身就是一种最简单的限流,包括:
框架本身的连接/线程限制
各种数据库连接池,Http连接池
服务/方法级别的信号量计数器限制

2. 窗口流量控制
窗口流量控制有几种做法。
一种最简单的计数器,维护一个单位时间内的Counter,如判断单位时间已经过去,则将Counter重置零。开涛文章里利用guava cache也是一种做法。
但此做法有时被认为粒度太粗,没有把QPS平摊到一秒的各个毫秒里,同时也没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,将目光移动一下,就看到在两毫秒内发生了两倍的TPS。
于是,另一个改进版的做法是把单位时间拆分,比如把1秒分成5个200毫秒宽的桶,然后以滑动窗口来计算限流,好像就能很大程度上解决上面的两个问题了。。。。滑动窗口的算法,统一到《熔断篇》再来讨论。
最后一组就是漏桶算法或令牌桶算法了,下面会详述。
另外,集群规模的计数器,基于Memcachd或Redis的实现,也见开涛的博客。

3. 令牌桶算法(Token Bucket )
随着时间流逝,系统会按速率 1/rate 的时间间隔(如果rate=100,则间隔是10ms)往桶里加入Token
如果桶满了(burst),则丢弃新加入的令牌
每个请求进来,都要消耗一个Token,如果桶空了,则丢弃请求或等待有新的令牌放入。
CpAZrVfEQxkAAAAAAAEpWCD_84Y055.png


4. 漏桶算法(Leaky Bucket )
简单的想象有一个木桶,有新请求就是不断的倒水进来,然后桶底下有个洞,按照固定的速率把水漏走,如果水进来的速度比漏走的快,桶可能就会满了,然后就拒绝请求。
可见,两个算法的基本描述上,只是方向不一样,其他没什么不同的。所以WikiMedia里说, 两个算法实现可以一样,对于相同的参数得到的限流效果是一样的。

5. Guava版的令牌桶实现 -- RateLimiter
Guava已实现了一个性能非常好的RateLimiter,基本不需要我们再费心实现。但其中有一些实现的细节,需要我们留意:
1. 支持桶外预借的突发
突发,原本是指如果单位时间的前半段流量较少,桶里会积累一些令牌,然后支持来一波大的瞬时流量,将前面积累的令牌消耗掉。
但在RateLimiter的实现里,还多了个桶外预借(我自己给他的命名),就是即使桶里没有多少令牌,你也可以消耗一波大的,然后桶里面在时间段内都没有新令牌。比如桶的容量是5,桶里面现在只有1个令牌,如果你要拿5个令牌,也可以,清了桶里的一个令牌,再预借4个。然后再过800毫秒,桶里才会出现新令牌。
可见,Guava版的RateLimiter对突发的支持,比原版的两种算法都要大,你几乎随时都可以一次过消费burst个令牌,不管现在桶里有没有积累的令牌。
不过有个副作用,就是如果前面都没什么流量,桶里累积了5个令牌,则你其实可以一次过消费10个令牌。。。不过那么一下,超借完接下来还是固定速率的,直到还清了旧账,才可能再来那么一下。

2. 支持等待可用令牌与立刻返回两种接口

3. 单位时段是秒,这有点不太好用,不支持设定5分钟的单位。

4. 发令牌的计算粒度是MicroSeconds,也就是最多支持一百万的QPS。

6. 唯品会自家框架的实现

6.1 各种设定维度
1. 在每个服务节点上进行限流统计。
如果要保护后端的数据库资源,需要自己分摊一下集群总流量到每台服务器上(没有全docker化自动伸缩前,根据当前节点数量自动调整阀值的意义不大)

2. 支持不同的调用者集群(比如购物车服务与订单服务)不同的阀值,当然也支持剔除特定调用者后的总阀值。

3. 在方法级别上统计限流,可以对消耗特别大的特定方法设置另外的阀值,当然其他方法就用一个统一的值。
暂不支持按服务中所有方法的总流量来限流,因为除非所有方法的消耗很接近,否则意义不大。

6.2 各种触发条件
目前只支持静态配置的触发条件。
未来的服务治理中心,要从各种监控系统里采集数据,然后根据多级条件动态调整规则。又或者服务节点上先找些自己也能统计的指标如服务延时,先动态起来,就是有重复计算之嫌。

6.3 各种处理
目前只支持直接返回限流异常,由客户端来进行降级逻辑。
未来要支持调用在服务端的降级方法,由服务端提供降级逻辑的短路径。

6.4 计数器实现
限流只在服务端进行。不重新发明轮子,直接就是Guava的RateLimiter。
对于客户端的并发数限制,未来也可以做一下。

服务化体系-分布式Unique ID的生成方法一览

默认分类admin 发表了文章 • 0 个评论 • 136 次浏览 • 2017-01-17 10:01 • 来自相关话题

开一个新的主题,聊聊服务化框架的实现。我个人不是很喜欢“微服务”这个词,所以还是叫服务化。
分布式的Unique ID的用途如此广泛,从业务对象Id到服务化体系里分布式调用链的TraceId,本文总结了林林总总的各种生成算法。
对于UID的要求,一乐那篇《业务系统需要什么样的ID生成器》的提法很好: 唯一性,时间相关,粗略有序,可反解,可制造。
1. 发号器
我接触的最早的Unique ID,就是Oracle的Sequence。
特点是准连续的自增数字,为什么说是准连续?因为性能考虑,每个Client一次会领20个ID回去慢慢用,用完了再来拿。另一个Client过来,拿的就是另外20个ID了。
新浪微博里,Tim用Redis做相同的事情,Incr一下拿一批ID回去。如果有多个数据中心,那就拿高位的几个bit来区分。
只要舍得在总架构里增加额外Redis带来的复杂度,一个64bit的long就够表达了,而且不可能有重复ID。
批量是关键,否则每个ID都远程调用一次谁也吃不消。
2. UUID
2.1 概述
Universally Unique IDentifier(UUID),有着正儿八经的RFC4122规范 ,是一个128bit的数字,也可以表现为32个16进制的字符(每个字符0-F的字符代表4bit),中间用"-"分割。
时间戳+UUID版本号:分三段占16个字符(60bit+4bit),
Clock Sequence号与保留字段:占4个字符(13bit+3bit),
节点标识:占12个字符(48bit)
比如:f81d4fae-7dec-11d0-a765-00a0c91e6bf6
实际上,UUID一共有多种算法,能用于TraceId的是:
 version1: 基于时间的算法
version4: 基于随机数的算法
2.2 version 4 基于随机数的算法
先说Version4,这是最暴力的做法,也是JDK里的算法,不管原来各个位的含义了,除了少数几个位必须按规范来填,其余全部用随机数表达。
JDK里的实现,用 SecureRandom生成了16个随机的Byte,用2个long来存储。记得加-Djava.security.egd=file:/dev/./urandom,详见《SecureRandom的江湖偏方与真实效果》
2.3 version 1基于时间的算法
然后是Version1,严格守着原来各个位的规矩:
时间戳:因为时间戳有满满的64bit,所以可以尽情花,以100纳秒为1,从1582年10月15日算起(能撑3655年,真是位数多给烧的,1582年起头有意思么)
顺序号:这16bit则仅用于避免前面的节点标示改变(如网卡改了),时钟系统出问题(如重启后时钟快了慢了),让它随机一下避免重复。
节点标识:48bit,一般用MAC地址表达,如果有多块网卡就随便用一块。如果没网卡,就用随机数凑数,或者拿一堆尽量多的其他的信息,比如主机名什么的,拼在一起再hash一把。
但这里的顺序号并不是我们想象的自增顺序号,而是一个一次性的随机数,所以如果两个请求在同100个纳秒发生时。。。。。
还有这里的节点标识只在机器级别,没考虑过一台机器上起了两个进程。
所以严格的Version1没人实现,接着往下看各个变种吧。
2.4 version 1 vs version4
version4随机数的做法简单直接,但以唯一性,时间相关,粗略有序,可反解,可制造做要求的话,则不如version4,比如唯一性,因为只是两个随机long,并没有从理论上保证不会重复。
3. Version1变种 - Hibernate
Hibernate的CustomVersionOneStrategy.java,解决了之前version 1的两个问题
时间戳(6bytes, 48bit):毫秒级别的,从1970年算起,能撑8925年....
顺序号(2bytes, 16bit, 最大值65535): 没有时间戳过了一毫秒要归零的事,各搞各的,short溢出到了负数就归0。
机器标识(4bytes 32bit): 拿localHost的IP地址,IPV4呢正好4个byte,但如果是IPV6要16个bytes,就只拿前4个byte。
进程标识(4bytes 32bit): 用当前时间戳右移8位再取整数应付,不信两条线程会同时启动。
顺序号也不再是一次性的随机数而是自增序列了。
节点标识拆成机器和进程两个字段,增加了从时间戳那边改变精度节约下来的16bit。另外节点标识这64bit(1个Long)总是不变,节约了生成UID的时间。
4. Version1变种 - MongoDB
MongoDB java driver的ObjectId.java
时间戳(4 bytes 32bit): 是秒级别的,从1970年算起,能撑136年。
自增序列(3bytes 24bit, 最大值一千六百万): 是一个从随机数开始(机智)的Int不断加一,也没有时间戳过了一秒要归零的事,各搞各的。因为只有3bytes,所以一个4bytes的Int还要截一下后3bytes。
机器标识(3bytes 24bit): 将所有网卡的Mac地址拼在一起做个HashCode,同样一个int还要截一下后3bytes。搞不到网卡就用随机数混过去。
进程标识(2bytes 16bits):从JMX里搞回来到进程号,搞不到就用进程名的hash或者随机数混过去。
可见,MongoDB的每一个字段设计都比Hibernate的更合理一点,时间戳是秒级别的,自增序列变长了,进程标识变短了。总长度也降到了12 bytes 96bit。
但如果果用64bit长的Long来保存有点不上不下的,只能表达成byte数组或16进制字符串。
5. Twitter的snowflake派号器
snowflake也是一个派号器,基于Thrift的服务,不过不是用redis简单自增,而是类似UUID version1,
也是只有一个 64bit的long,所以IdWorker里紧巴巴的分配:
时间戳(42bit) 自从2012年以来(比那些从1970年算起的会过日子)的毫秒数,能撑139年。
自增序列(12bit,最大值4096), 毫秒之内的自增,过了一毫秒要重新置0
DataCenter ID (5 bit, 最大值32),配置值,派号器可能在多个机房。
Worker ID ( 5 bit, 最大值32),配置值,因为是派号器的id,所以一个数据中心里最多32个派号器就够了,还会在ZK里做下注册。
可见,因为是中央派号器,把至少40bit的节点标识都省出来了,换成10bit的派号器标识。所以整个UID能够只用一个Long表达。
另外,这种派号器,client每次只能一个ID,不能批量取,所以额外增加的延时是问题。
6.扩展阅读
《细聊分布式ID生成方法》
《生成全局唯一ID的3个思路,来自一个资深架构师的总结》
老规矩,链接请狠戳阅读原文。
7. 延伸问题,不用派号器,用一个Long如何搞定UUID??
这是我们服务化框架一开始的选择,TraceID设为了一个Long,而不是String,又不想用派号器的话,怎么办?
从UUID的128位压缩到Long的64位,又不用中央派号器而是本地生成,最难还是怎么来区分本地的机器+进程号。
思路一,压缩其他字段,留足够多的长度来做机器+进程号标识
时间戳是秒级别,1年要24位,两年要25位.....
自增序列,6万QPS要16位,10万要17位...
剩下20-24位,百万分之一到一千六百万分之一的重复率,然后把网卡Mac+进程号拼在一起再hash,取结果32个bit的后面20或24个bit。但假如这个标识字段重复了,后面时间戳和自增序列也很容易重复,不停的重复。
思路二,使用ZK 或 mysql 或 redis来自增管理机器+进程标识号
如果机器+进程标识字段只留了12位(4096),就要用ZK或etcd,线程启动时分配号码,当进程关闭了要回收这个号。
如果标示号的位数留得够多,比如有20位(一百万),那用redis或mysql来自增标识号最简单,每个进程启动时就去拿一个机器+服务进程的标示号。
思路三,继续Random
继续拼了,因为traceId的唯一性要求不高,偶然重复也没大问题,所以直接拿JDK UUID.randomUUID()的低位long(按UUID规范,高位的long被置了4个默认值的bit,低位只被设置3个bit),或者不用UUID.randomUUID()了,直接SecureRandom.nextLong(),不浪费了那3个bit。
你猜我们最后选了哪种? 查看全部
开一个新的主题,聊聊服务化框架的实现。我个人不是很喜欢“微服务”这个词,所以还是叫服务化。
分布式的Unique ID的用途如此广泛,从业务对象Id到服务化体系里分布式调用链的TraceId,本文总结了林林总总的各种生成算法。
对于UID的要求,一乐那篇《业务系统需要什么样的ID生成器》的提法很好: 唯一性,时间相关,粗略有序,可反解,可制造。
1. 发号器
我接触的最早的Unique ID,就是Oracle的Sequence。
特点是准连续的自增数字,为什么说是准连续?因为性能考虑,每个Client一次会领20个ID回去慢慢用,用完了再来拿。另一个Client过来,拿的就是另外20个ID了。
新浪微博里,Tim用Redis做相同的事情,Incr一下拿一批ID回去。如果有多个数据中心,那就拿高位的几个bit来区分。
只要舍得在总架构里增加额外Redis带来的复杂度,一个64bit的long就够表达了,而且不可能有重复ID。
批量是关键,否则每个ID都远程调用一次谁也吃不消。
2. UUID
2.1 概述
Universally Unique IDentifier(UUID),有着正儿八经的RFC4122规范 ,是一个128bit的数字,也可以表现为32个16进制的字符(每个字符0-F的字符代表4bit),中间用"-"分割。
时间戳+UUID版本号:分三段占16个字符(60bit+4bit),
Clock Sequence号与保留字段:占4个字符(13bit+3bit),
节点标识:占12个字符(48bit)
比如:f81d4fae-7dec-11d0-a765-00a0c91e6bf6
实际上,UUID一共有多种算法,能用于TraceId的是:
 version1: 基于时间的算法
version4: 基于随机数的算法
2.2 version 4 基于随机数的算法
先说Version4,这是最暴力的做法,也是JDK里的算法,不管原来各个位的含义了,除了少数几个位必须按规范来填,其余全部用随机数表达。
JDK里的实现,用 SecureRandom生成了16个随机的Byte,用2个long来存储。记得加-Djava.security.egd=file:/dev/./urandom,详见《SecureRandom的江湖偏方与真实效果》
2.3 version 1基于时间的算法
然后是Version1,严格守着原来各个位的规矩:
时间戳:因为时间戳有满满的64bit,所以可以尽情花,以100纳秒为1,从1582年10月15日算起(能撑3655年,真是位数多给烧的,1582年起头有意思么)
顺序号:这16bit则仅用于避免前面的节点标示改变(如网卡改了),时钟系统出问题(如重启后时钟快了慢了),让它随机一下避免重复。
节点标识:48bit,一般用MAC地址表达,如果有多块网卡就随便用一块。如果没网卡,就用随机数凑数,或者拿一堆尽量多的其他的信息,比如主机名什么的,拼在一起再hash一把。
但这里的顺序号并不是我们想象的自增顺序号,而是一个一次性的随机数,所以如果两个请求在同100个纳秒发生时。。。。。
还有这里的节点标识只在机器级别,没考虑过一台机器上起了两个进程。
所以严格的Version1没人实现,接着往下看各个变种吧。
2.4 version 1 vs version4
version4随机数的做法简单直接,但以唯一性,时间相关,粗略有序,可反解,可制造做要求的话,则不如version4,比如唯一性,因为只是两个随机long,并没有从理论上保证不会重复。
3. Version1变种 - Hibernate
Hibernate的CustomVersionOneStrategy.java,解决了之前version 1的两个问题
时间戳(6bytes, 48bit):毫秒级别的,从1970年算起,能撑8925年....
顺序号(2bytes, 16bit, 最大值65535): 没有时间戳过了一毫秒要归零的事,各搞各的,short溢出到了负数就归0。
机器标识(4bytes 32bit): 拿localHost的IP地址,IPV4呢正好4个byte,但如果是IPV6要16个bytes,就只拿前4个byte。
进程标识(4bytes 32bit): 用当前时间戳右移8位再取整数应付,不信两条线程会同时启动。
顺序号也不再是一次性的随机数而是自增序列了。
节点标识拆成机器和进程两个字段,增加了从时间戳那边改变精度节约下来的16bit。另外节点标识这64bit(1个Long)总是不变,节约了生成UID的时间。
4. Version1变种 - MongoDB
MongoDB java driver的ObjectId.java
时间戳(4 bytes 32bit): 是秒级别的,从1970年算起,能撑136年。
自增序列(3bytes 24bit, 最大值一千六百万): 是一个从随机数开始(机智)的Int不断加一,也没有时间戳过了一秒要归零的事,各搞各的。因为只有3bytes,所以一个4bytes的Int还要截一下后3bytes。
机器标识(3bytes 24bit): 将所有网卡的Mac地址拼在一起做个HashCode,同样一个int还要截一下后3bytes。搞不到网卡就用随机数混过去。
进程标识(2bytes 16bits):从JMX里搞回来到进程号,搞不到就用进程名的hash或者随机数混过去。
可见,MongoDB的每一个字段设计都比Hibernate的更合理一点,时间戳是秒级别的,自增序列变长了,进程标识变短了。总长度也降到了12 bytes 96bit。
但如果果用64bit长的Long来保存有点不上不下的,只能表达成byte数组或16进制字符串。
5. Twitter的snowflake派号器
snowflake也是一个派号器,基于Thrift的服务,不过不是用redis简单自增,而是类似UUID version1,
也是只有一个 64bit的long,所以IdWorker里紧巴巴的分配:
时间戳(42bit) 自从2012年以来(比那些从1970年算起的会过日子)的毫秒数,能撑139年。
自增序列(12bit,最大值4096), 毫秒之内的自增,过了一毫秒要重新置0
DataCenter ID (5 bit, 最大值32),配置值,派号器可能在多个机房。
Worker ID ( 5 bit, 最大值32),配置值,因为是派号器的id,所以一个数据中心里最多32个派号器就够了,还会在ZK里做下注册。
可见,因为是中央派号器,把至少40bit的节点标识都省出来了,换成10bit的派号器标识。所以整个UID能够只用一个Long表达。
另外,这种派号器,client每次只能一个ID,不能批量取,所以额外增加的延时是问题。
6.扩展阅读
《细聊分布式ID生成方法》
《生成全局唯一ID的3个思路,来自一个资深架构师的总结》
老规矩,链接请狠戳阅读原文。
7. 延伸问题,不用派号器,用一个Long如何搞定UUID??
这是我们服务化框架一开始的选择,TraceID设为了一个Long,而不是String,又不想用派号器的话,怎么办?
从UUID的128位压缩到Long的64位,又不用中央派号器而是本地生成,最难还是怎么来区分本地的机器+进程号。
思路一,压缩其他字段,留足够多的长度来做机器+进程号标识
时间戳是秒级别,1年要24位,两年要25位.....
自增序列,6万QPS要16位,10万要17位...
剩下20-24位,百万分之一到一千六百万分之一的重复率,然后把网卡Mac+进程号拼在一起再hash,取结果32个bit的后面20或24个bit。但假如这个标识字段重复了,后面时间戳和自增序列也很容易重复,不停的重复。
思路二,使用ZK 或 mysql 或 redis来自增管理机器+进程标识号
如果机器+进程标识字段只留了12位(4096),就要用ZK或etcd,线程启动时分配号码,当进程关闭了要回收这个号。
如果标示号的位数留得够多,比如有20位(一百万),那用redis或mysql来自增标识号最简单,每个进程启动时就去拿一个机器+服务进程的标示号。
思路三,继续Random
继续拼了,因为traceId的唯一性要求不高,偶然重复也没大问题,所以直接拿JDK UUID.randomUUID()的低位long(按UUID规范,高位的long被置了4个默认值的bit,低位只被设置3个bit),或者不用UUID.randomUUID()了,直接SecureRandom.nextLong(),不浪费了那3个bit。
你猜我们最后选了哪种?

服务化体系之-兼容性与版本号

默认分类admin 发表了文章 • 0 个评论 • 127 次浏览 • 2017-01-17 09:56 • 来自相关话题

家大业大之后,服务的版本和兼容性就是个让人不得不正视的问题。
最近,路上有个说法,既然都是微服务了,那不同的版本可以认为是两个完全不一样的微服务,没必要再保留版本号了。
这篇文章按着唯品会的实战经历来探讨一下。
我们面对的现状是:服务每周每天都在升级,有些升级是业务逻辑升级,接口不变;有些是接口变了,但是兼容的,偶然还有一些是不兼容的。
家大业大的体系永远不可能在服务端接口升级的时候,同时(比如同一个深夜,同一个小时甚至同一分钟)将所有调用者的客户端也升级的,相反所有客户端的升级可能是很长一段时间的事情,而且可能客户端还没全升完,服务端的接口又变了。
1. 兼容性原则
先搞些铺垫,我们一般认为下面的情况是兼容的:
增加新方法
增加可选的参数
修改参数为可选
删除参数
框架支持的话,参数的名称、顺序也可以改(如Thrift)
参数是对象的话,其属性的变更参见2-5条。
然后一般认为下面的情况是不兼容的:
业务上的不兼容
修改方法的名称
删除方法
增加必填的参数
修改参数为必填
修改参数和返回值的类型
参数是对象的话,属性的变更参照3-5条。
2. 不兼容时服务如何升级?
服务要进行不兼容的升级时,前面说了客户端不可能同时升级的,那怎么办?
一种是不修改原来的接口,直接在同一个服务里增加新的方法来解决。这种应该是最简单的做法,少量接口变动时优先使用,就是有点dirty。
一种是同时运行新旧两个服务。这种方式干净,但麻烦,要注意:
要保证旧版的客户端SDK,调用不能被路由到新服务上。
如果单独部署两个服务而不是在同一个应用里,随着客户端不断割接过来,控制好新旧两个的集群容量。
3. 版本号原则
服务的版本号,和软件的版本号一样,一般整成三位:
第一位:不兼容的大版本,   如1.0    vs  2.0
第二位:兼容的新功能版本,如1.1    vs 1.2
第三位:兼容的BugFix版本,如1.1.0 vs 1.1.1
如果拿着低版本的SDK(如1.0.0) 发起请求,会被服务化框架路由到所有的兼容版本上(如1.1.1,1.2.0),但不会到不兼容的版本上(如2.0.1)。
4. 最终问题,到底要不要版本号?
4.1 版本号用于标示SDK版本是有益的
即使是兼容的小版本。
版本号能让服务端与客户端两头的开发人员更好的对话。毕竟所谓兼容,有时候也有着某种代价与折衷,互相明确彼此的版本会更好。
如果服务治理中心,能让所有服务提供者一目了然各个调用者的版本会更好,起码方便催人升级呀。
同时,在中央服务文档中心,有了版本号后也能为服务接口每一个小版本保留一份文档。
4.2 不兼容的版本怎么办?
像我们这种命名渣,好不容易为服务搞了个贴切的名字,再想第二个其实很不容易,一般只能在函数名,服务名里直接带上数字,如GoodsService2,或者GoodsServiceNew, GoodsNewService.....
因此,如果加新方法的简单方式撑不住了,要独立新旧两个服务时,还是继续使用版本号来区分吧。当然,引入了版本号在框架里也引入了一定的复杂度,见后。
4.3 简化基于版本的配置
我们家框架之前的设计,将配置的粒度支持到每个小版本上,比如1.0版本的超时是10ms,1.1版本是20ms。然后根据客户端的版本来就近选择。回头来看,白白增加复杂度,还容易有bug....
其实兼容的小版本只有一份配置就好,明年得简化。
但不兼容的大版本还是要两份,比如自定义的路由。不兼容的版本本质上真的就是两个微服务了。
4.4 最终结论
建议还是有版本, 服务化框架里的相关逻辑和路由时,只取第一位的主版本号。第二、三位则用于SDK版本的管理沟通,以及中央服务文档中心。
当然,以上只是针对唯品会这种体量的公司的一家之言,并非四海皆准。 查看全部
家大业大之后,服务的版本和兼容性就是个让人不得不正视的问题。
最近,路上有个说法,既然都是微服务了,那不同的版本可以认为是两个完全不一样的微服务,没必要再保留版本号了。
这篇文章按着唯品会的实战经历来探讨一下。
我们面对的现状是:服务每周每天都在升级,有些升级是业务逻辑升级,接口不变;有些是接口变了,但是兼容的,偶然还有一些是不兼容的。
家大业大的体系永远不可能在服务端接口升级的时候,同时(比如同一个深夜,同一个小时甚至同一分钟)将所有调用者的客户端也升级的,相反所有客户端的升级可能是很长一段时间的事情,而且可能客户端还没全升完,服务端的接口又变了。
1. 兼容性原则
先搞些铺垫,我们一般认为下面的情况是兼容的:
增加新方法
增加可选的参数
修改参数为可选
删除参数
框架支持的话,参数的名称、顺序也可以改(如Thrift)
参数是对象的话,其属性的变更参见2-5条。
然后一般认为下面的情况是不兼容的:
业务上的不兼容
修改方法的名称
删除方法
增加必填的参数
修改参数为必填
修改参数和返回值的类型
参数是对象的话,属性的变更参照3-5条。
2. 不兼容时服务如何升级?
服务要进行不兼容的升级时,前面说了客户端不可能同时升级的,那怎么办?
一种是不修改原来的接口,直接在同一个服务里增加新的方法来解决。这种应该是最简单的做法,少量接口变动时优先使用,就是有点dirty。
一种是同时运行新旧两个服务。这种方式干净,但麻烦,要注意:
要保证旧版的客户端SDK,调用不能被路由到新服务上。
如果单独部署两个服务而不是在同一个应用里,随着客户端不断割接过来,控制好新旧两个的集群容量。
3. 版本号原则
服务的版本号,和软件的版本号一样,一般整成三位:
第一位:不兼容的大版本,   如1.0    vs  2.0
第二位:兼容的新功能版本,如1.1    vs 1.2
第三位:兼容的BugFix版本,如1.1.0 vs 1.1.1
如果拿着低版本的SDK(如1.0.0) 发起请求,会被服务化框架路由到所有的兼容版本上(如1.1.1,1.2.0),但不会到不兼容的版本上(如2.0.1)。
4. 最终问题,到底要不要版本号?
4.1 版本号用于标示SDK版本是有益的
即使是兼容的小版本。
版本号能让服务端与客户端两头的开发人员更好的对话。毕竟所谓兼容,有时候也有着某种代价与折衷,互相明确彼此的版本会更好。
如果服务治理中心,能让所有服务提供者一目了然各个调用者的版本会更好,起码方便催人升级呀。
同时,在中央服务文档中心,有了版本号后也能为服务接口每一个小版本保留一份文档。
4.2 不兼容的版本怎么办?
像我们这种命名渣,好不容易为服务搞了个贴切的名字,再想第二个其实很不容易,一般只能在函数名,服务名里直接带上数字,如GoodsService2,或者GoodsServiceNew, GoodsNewService.....
因此,如果加新方法的简单方式撑不住了,要独立新旧两个服务时,还是继续使用版本号来区分吧。当然,引入了版本号在框架里也引入了一定的复杂度,见后。
4.3 简化基于版本的配置
我们家框架之前的设计,将配置的粒度支持到每个小版本上,比如1.0版本的超时是10ms,1.1版本是20ms。然后根据客户端的版本来就近选择。回头来看,白白增加复杂度,还容易有bug....
其实兼容的小版本只有一份配置就好,明年得简化。
但不兼容的大版本还是要两份,比如自定义的路由。不兼容的版本本质上真的就是两个微服务了。
4.4 最终结论
建议还是有版本, 服务化框架里的相关逻辑和路由时,只取第一位的主版本号。第二、三位则用于SDK版本的管理沟通,以及中央服务文档中心。
当然,以上只是针对唯品会这种体量的公司的一家之言,并非四海皆准。

服务化体系之-配置中心,在ZK或etcd之外

默认分类admin 发表了文章 • 0 个评论 • 153 次浏览 • 2017-01-17 09:52 • 来自相关话题

如果一说到配置中心,你面前那个人立刻兴奋的和你讲ZooKeeper,etcd,很可能,你又遇到个空想型的玩家了,或者他家的系统其实很小。
1. 配置中心该有的清秀模样
家大业大的系统,肯定是要有个配置中心,统一管理所有应用的配置,并发布到对应的客户端上。
1. 推送: 将更改实时、准实时地推送到所有客户端。
2. 版本化与回滚:知道谁,什么时候,改了什么。最重要是,可以快速的回滚.....回滚、重启、灰度,可称运维三宝,在一时三刻便要报个P1故障的时候,一键回滚的便捷性如此重要。
3. 灰度发布:将更改推送到某些客户端上。同是运维三宝,先灰度一下能有效的降低风险。另外也是AB Test的一种实现方式-10%机器配置成开关打开调用A系统,90%机器开关关闭走B系统。
4. 预案:先改好一些配置保存起来,但不下发。发生问题时,一键批量执行,如降级整条选购线所有服务的非关键功能,好过在兵荒马乱中颤抖着去修改。
5. 权限,审批,流程: 咳,咳。权限控制和Pair Check其实还是好的。
6. 同时支持 Web界面 与 Restful API接口。
7. 支持多语言,其中最头痛是支持php这种无状态的。
2. 客户端这边要怎么配衬呢?
1. 配合配置中心的实时/灰度推送,在参数变化时调用客户端自行实现的回调接口,不需要重启应用。
2. 支持环境变量,JVM启动参数,配置文件,配置中心等多种来源按优先级互相覆盖,并有接口暴露最后的参数选择。
3. 配置文件中支持多套profile,如开发,单元测试,集成测试,生产。
3. 现在可以来谈实现了
3.1 Netflix Archaius
被谈得最多的Netflix,其Archaius说白了只是个客户端,服务端是一点没有的。
基于Apache Commons Configuration Library的扩展,多层,实时/准实时的数据源包括了环境变量,配置文件,数据库,ZK,etcd等。但没有Spring多profile的支持。
3.2 Spring Cloud
其Config Server的实现相当奇特,基于git,没错,就是基于git,暴露成Restful API,所以也算是支持了版本化,但也仅此而已了,其他界面功能什么的都需要自己另外做。
在客户端倒是集成了Config Server的读取,Spring本身也有profile机制。
而实时推送,还要自己拉上另一个Spring Cloud的Bus项目,基于RabbitMQ或Kafka来搭建一套推送体系,自己写代码实现。另外看文章,刷新时也不是通过回调接口,而是整个Bean重新构造,这对于有状态的Bean是不合适的。
3.3 国内开源
360开源了一个QCon,基于ZK,特色是基于Agent模式(见后)的多语言支持。但服务端也没有界面,灰度、预案什么的,直接通过API操作ZK而已。
淘宝的Diamond,有点历史了。
携程开源的Applo,支持Java,其他语言通过Http支持,看起来功能比较完整,界面模样周正,也还在更新维护。
还有个人开源的disconf,根脚在百度,只支持Java+Spring,看起来也可以。
3.4 基于ZK或etcd, DIY吧
综上所述,最好的配置中心还是在各个互联网公司的基础架构部里,虽然改改改的过程可能很长,虽然现在都还不完美。
一般都会支持界面和API,用数据库来保存版本历史,预案,走流程(正儿八经的数据库表更方便查询,比如“你的待审批变更”)。最后下发到ZK或etcd这种有推送能力的存储里(服务注册本身也是用ZK或etcd,选型就一块了)。客户端都直接和ZK或etcd打交道。
灰度发布麻烦些,其中一种实现是同时发布一个可接收的IP列表,客户端监听到配置节点变化时,对比一下自己是否属于该列表。
PHP和其他ZK/etcd不支持的语言,只好自己在客户端的机器上起一个Agent来监听变化,再写到配置文件或Share Memory。
我司走的就是这个路数。
最后,到底是ZK 还是etcd? 有些岁数的配置中心都是ZK的,但如果新写的话,的确可以考虑下etcd。虽然我没玩过,但看着它基于GRPC而支持多种语言,还可以动态修改集群,下发也不容易丢消息,挺不错。
3.5 基于运维自动化的配置文件推送
这是另一种玩法,一样有数据库与界面或API来管理配置,下发时生成配置文件,基于各种运维自动化工具如Puppet,Ansible推送到每个客户端。而应用则定时重新读取这个外部的配置文件。

4. 服务化框架的配置
除了业务上的值与开关的配置,服务化框架本身也需要配置。
服务化框架的配置又分两类:
框架底层的配置,如IO线程数,业务线程数,连接数,心跳间隔,日志配置等等。
每个服务/方法的元数据,如超时,重试,限流,熔断,路由,负载均衡等。
框架级的配置一般采用比较固定的配置方式,如配置文件,-D启动参数等,略过不谈。
4.1 服务元数据配置的演变
服务的元数据配置,则有以下逐步提升的过程:
Day1, 根据配置产生作用的地方,服务端或客户端,各配各的。比如超时配在客户端的配置文件,限流配在服务端。
Day2, 考虑到有些客户端可能漏配或不懂要配什么,服务端也可以配置超时,并把配置上传到服务注册中心,作为客户端的默认值。很多开源出来的RPC框架,为了减少依赖也就只做到这一步了。
Day3,业务系统可能把其中一些配置,抽取到配置中心里面实现动态修改。
Day4,各个业务系统各搞各的服务配置,一个字,乱。那不如在配置中心里,搞一个专门针对服务的标准界面吧。有时候,这也叫做服务治理中心。
4.2 服务治理中心的模样
1. 以服务为维度,统一管理所有服务相关的配置,包括服务端的,所有客户端的配置。
同一个服务,不同的调用者,可能有不同超时要求,比如订单与商品详情两个服务,对库存服务的延时敏感度不一样。
甚至同一个调用者,比如订单服务,在查看订单和新增订单两个场景里,对库存服务的延时的敏感度会不同。
这么麻烦!!要不要还是回到每个客户端自己在配置文件或代码里设置超时算了?
我的建议,为了达致比较好的服务治理,还是应该把服务提供者与所有调用者的配置放在一起,而不是散乱在代码里。作为一个服务提供者,如果完全不了解各个客户端在代码里设了什么超时,其实挺心虚的。
另外也可以做更多后续的治理工作,如整条调用链的超时设置展现。
同时,服务端获悉客户端那边的配置,也能够优化自己的处理,比如服务逻辑跑完就已经达到客户端那边配置的超时,那就不要序列化结果并发送回去了。 如果没有中央配置,客户端就要每次把超时时间也传输过去。
2. 多条件配置
除了前面提到的调用者/调用场景的不一样,可能还有服务在不同机房不同配置,灰度不同机器不同配置,服务默认值与某个方法特定值的差异等。
4.3 一种参考实现
需求很复杂,实现就要尽量简化.....无论界面怎么做,最后存储的JSON格式简化如下:
[
  //”订单服务“调用 “获取购物车方法” 为1000 
   {"method":"getCart","sourceApp":"orderService","value":1000},
  //默认值60
   {"value":60}
]
按顺序的多个规则,先中先走。规则里条件包括method, sourceApp, tag,targetIp等,多个条件是AND的关系。
而在读取配置时,不可能每次都做这样的解析,那就以空间换时间,把每一个遇到的method, sourceApp, targetIp,tag的组合的结果缓存起来。

好像所有开源的SOA框架里,只有我司的框架设计了这种多条件的配置。
我会的基础框架,细节对外说的不多。 这次抖漏了不少,怎么也值两块五了吧,赏么:) 查看全部
如果一说到配置中心,你面前那个人立刻兴奋的和你讲ZooKeeper,etcd,很可能,你又遇到个空想型的玩家了,或者他家的系统其实很小。
1. 配置中心该有的清秀模样
家大业大的系统,肯定是要有个配置中心,统一管理所有应用的配置,并发布到对应的客户端上。
1. 推送: 将更改实时、准实时地推送到所有客户端。
2. 版本化与回滚:知道谁,什么时候,改了什么。最重要是,可以快速的回滚.....回滚、重启、灰度,可称运维三宝,在一时三刻便要报个P1故障的时候,一键回滚的便捷性如此重要。
3. 灰度发布:将更改推送到某些客户端上。同是运维三宝,先灰度一下能有效的降低风险。另外也是AB Test的一种实现方式-10%机器配置成开关打开调用A系统,90%机器开关关闭走B系统。
4. 预案:先改好一些配置保存起来,但不下发。发生问题时,一键批量执行,如降级整条选购线所有服务的非关键功能,好过在兵荒马乱中颤抖着去修改。
5. 权限,审批,流程: 咳,咳。权限控制和Pair Check其实还是好的。
6. 同时支持 Web界面 与 Restful API接口。
7. 支持多语言,其中最头痛是支持php这种无状态的。
2. 客户端这边要怎么配衬呢?
1. 配合配置中心的实时/灰度推送,在参数变化时调用客户端自行实现的回调接口,不需要重启应用。
2. 支持环境变量,JVM启动参数,配置文件,配置中心等多种来源按优先级互相覆盖,并有接口暴露最后的参数选择。
3. 配置文件中支持多套profile,如开发,单元测试,集成测试,生产。
3. 现在可以来谈实现了
3.1 Netflix Archaius
被谈得最多的Netflix,其Archaius说白了只是个客户端,服务端是一点没有的。
基于Apache Commons Configuration Library的扩展,多层,实时/准实时的数据源包括了环境变量,配置文件,数据库,ZK,etcd等。但没有Spring多profile的支持。
3.2 Spring Cloud
其Config Server的实现相当奇特,基于git,没错,就是基于git,暴露成Restful API,所以也算是支持了版本化,但也仅此而已了,其他界面功能什么的都需要自己另外做。
在客户端倒是集成了Config Server的读取,Spring本身也有profile机制。
而实时推送,还要自己拉上另一个Spring Cloud的Bus项目,基于RabbitMQ或Kafka来搭建一套推送体系,自己写代码实现。另外看文章,刷新时也不是通过回调接口,而是整个Bean重新构造,这对于有状态的Bean是不合适的。
3.3 国内开源
360开源了一个QCon,基于ZK,特色是基于Agent模式(见后)的多语言支持。但服务端也没有界面,灰度、预案什么的,直接通过API操作ZK而已。
淘宝的Diamond,有点历史了。
携程开源的Applo,支持Java,其他语言通过Http支持,看起来功能比较完整,界面模样周正,也还在更新维护。
还有个人开源的disconf,根脚在百度,只支持Java+Spring,看起来也可以。
3.4 基于ZK或etcd, DIY吧
综上所述,最好的配置中心还是在各个互联网公司的基础架构部里,虽然改改改的过程可能很长,虽然现在都还不完美。
一般都会支持界面和API,用数据库来保存版本历史,预案,走流程(正儿八经的数据库表更方便查询,比如“你的待审批变更”)。最后下发到ZK或etcd这种有推送能力的存储里(服务注册本身也是用ZK或etcd,选型就一块了)。客户端都直接和ZK或etcd打交道。
灰度发布麻烦些,其中一种实现是同时发布一个可接收的IP列表,客户端监听到配置节点变化时,对比一下自己是否属于该列表。
PHP和其他ZK/etcd不支持的语言,只好自己在客户端的机器上起一个Agent来监听变化,再写到配置文件或Share Memory。
我司走的就是这个路数。
最后,到底是ZK 还是etcd? 有些岁数的配置中心都是ZK的,但如果新写的话,的确可以考虑下etcd。虽然我没玩过,但看着它基于GRPC而支持多种语言,还可以动态修改集群,下发也不容易丢消息,挺不错。
3.5 基于运维自动化的配置文件推送
这是另一种玩法,一样有数据库与界面或API来管理配置,下发时生成配置文件,基于各种运维自动化工具如Puppet,Ansible推送到每个客户端。而应用则定时重新读取这个外部的配置文件。

4. 服务化框架的配置
除了业务上的值与开关的配置,服务化框架本身也需要配置。
服务化框架的配置又分两类:
框架底层的配置,如IO线程数,业务线程数,连接数,心跳间隔,日志配置等等。
每个服务/方法的元数据,如超时,重试,限流,熔断,路由,负载均衡等。
框架级的配置一般采用比较固定的配置方式,如配置文件,-D启动参数等,略过不谈。
4.1 服务元数据配置的演变
服务的元数据配置,则有以下逐步提升的过程:
Day1, 根据配置产生作用的地方,服务端或客户端,各配各的。比如超时配在客户端的配置文件,限流配在服务端。
Day2, 考虑到有些客户端可能漏配或不懂要配什么,服务端也可以配置超时,并把配置上传到服务注册中心,作为客户端的默认值。很多开源出来的RPC框架,为了减少依赖也就只做到这一步了。
Day3,业务系统可能把其中一些配置,抽取到配置中心里面实现动态修改。
Day4,各个业务系统各搞各的服务配置,一个字,乱。那不如在配置中心里,搞一个专门针对服务的标准界面吧。有时候,这也叫做服务治理中心。
4.2 服务治理中心的模样
1. 以服务为维度,统一管理所有服务相关的配置,包括服务端的,所有客户端的配置。
同一个服务,不同的调用者,可能有不同超时要求,比如订单与商品详情两个服务,对库存服务的延时敏感度不一样。
甚至同一个调用者,比如订单服务,在查看订单和新增订单两个场景里,对库存服务的延时的敏感度会不同。
这么麻烦!!要不要还是回到每个客户端自己在配置文件或代码里设置超时算了?
我的建议,为了达致比较好的服务治理,还是应该把服务提供者与所有调用者的配置放在一起,而不是散乱在代码里。作为一个服务提供者,如果完全不了解各个客户端在代码里设了什么超时,其实挺心虚的。
另外也可以做更多后续的治理工作,如整条调用链的超时设置展现。
同时,服务端获悉客户端那边的配置,也能够优化自己的处理,比如服务逻辑跑完就已经达到客户端那边配置的超时,那就不要序列化结果并发送回去了。 如果没有中央配置,客户端就要每次把超时时间也传输过去。
2. 多条件配置
除了前面提到的调用者/调用场景的不一样,可能还有服务在不同机房不同配置,灰度不同机器不同配置,服务默认值与某个方法特定值的差异等。
4.3 一种参考实现
需求很复杂,实现就要尽量简化.....无论界面怎么做,最后存储的JSON格式简化如下:
[
  //”订单服务“调用 “获取购物车方法” 为1000 
   {"method":"getCart","sourceApp":"orderService","value":1000},
  //默认值60
   {"value":60}
]
按顺序的多个规则,先中先走。规则里条件包括method, sourceApp, tag,targetIp等,多个条件是AND的关系。
而在读取配置时,不可能每次都做这样的解析,那就以空间换时间,把每一个遇到的method, sourceApp, targetIp,tag的组合的结果缓存起来。

好像所有开源的SOA框架里,只有我司的框架设计了这种多条件的配置。
我会的基础框架,细节对外说的不多。 这次抖漏了不少,怎么也值两块五了吧,赏么:)

服务化之-负载均衡与路由的设计

默认分类admin 发表了文章 • 0 个评论 • 215 次浏览 • 2017-01-17 09:46 • 来自相关话题

从这章开始,各个服务化框架之间要贴身肉搏的比较了。
以Java体系的,有被真实地大量使用的,能接触到源码的这三条作标准,选择了这些学习对象: Dubbo与Dubbox兄弟,新浪微博的Motan,美团点评的Pigeon,还有SpringCloud/Netflix家的一应物件。嗯,当然还有唯品会自家出品的OSP。
推家的Finagle,谷家的GRPC,还有Netty作者写的Armeria更偏重于RPC框架,这章先不出场。
1. 服务注册与发现
时代在进步,服务注册与发现简单讲讲就行。
1.1 几桩好处
与Apache,Nginx,HAProxy,LVS这些传统的Load Balancer对比,服务的自注册自发现+智能客户端有几桩好处:
一、绕过了中央的代理节点,少了一层网络节点。
二、避免了中央的代理节点自身的处理速度和网卡带宽的瓶颈,以及LB自身的高可用。
传统的LB节点,需要KeepAlived之类的方案来做一个主备。然后前面再放一个DNS轮询之类的方案来做扩容。考虑到DNS的TTL设置,一旦主从LB一起失效,还要等待比如10分钟的过期时间,或紧急绑定客户机的/etc/host。
三、往日的LB,更改所代理的集群实例还要再重启一把。不过现在好像不需要了,比如Docker里的Bamboo方案,就可以动态注册Docker容器到HAProxy中。这桩好处便大大削弱了,只剩这注册可以不假于外物。
四、智能客户端上可以做更多的服务治理逻辑,比往Nginx上挂Lua脚本强。
1.2 大致功能
框架启动时,应用等完成初始预热了,才注册迎客。
框架退出时,先把自己从注册中心下线,稍等所有客户端都收到这个通知,再完成手头剩下的请求了,才优雅的转身退出。
还应提供API,一来是运维摘流量的一种方式,二来应用程序自己,可以随时将某个实例临时下线,比如应用做一些缓存刷新的工作时,便不希望接客。
另外一件要紧事情,注册中心虽然也会与服务端进行定时心跳。但这毕竟走的是注册中心与服务端之间的特殊通道,而不像传统LB的Health Check URL那样直接与服务端口进行交互,所以两者效用不可等同,还得有别的健康检查措施。
注册中心的DashBoard?觉得可以融合在服务治理中心里,单独的DashBoard意义不大。
1.3 种种实现
服务注册中心可以有zk,etcd,consul,我自己只用过ZK,其他没什么发言权,但和配置中心一样,etcd看着不错,有些框架可以同时支持其中的二三者。
Netflix家也有个Eureka,目前的版本基于RESTFul的API, 所以推送能力比前几家弱,靠着默认定时30秒的刷新,Server间要数据同步,Client与Server要数据同步,文档里说最多两分钟才保证通知到所有客户端,与ZK们秒级的推送速度相差甚远。对于脑裂的情况,Eureka的态度是宁愿保留坏数据,不要丢失好数据,见仁见智。
2. 负载均衡
2.1 衡量的标准
2.1.1  算法一览
1.Round Robbin(轮询)
最古老的算法最可信的算法,缺点是有状态,必须在并发之下记住上一次到谁了。
2.Random(随机)
最简单的,无状态的算法。
3. Least Load(最小负载)
可以实现某种程度上的智能调节。具体的实现包括最少在途请求数(发出去了还没收到应答的请求),最少并发连接数,最小平均响应时间等等。
4. Hash (参数哈希)
根据参数进行哈希,一来有分区的效果(参见单元化架构,可改善数据库连接,缓存热度等),二来可以Stick Session(可本机进行防刷运算等)。
2.1.2  权重支持
传统LB都有权重设置, 机器(Docker)硬件配置的不一致需要权重,压测引流需要权重。
灰度发布也需要权重。假如这个应用只有3台服务器,灰度发布1台,万一有错,那就是33%的失败率了。如果信心不足,可以增加一个灰度批次,先把第一台的权重降到原来的1%。
还有,机器刚启动时,需要一点预热的时间(如Class Loading),如果一开始就把一堆请求压给它,可能会有批量的超时,所以最好刚启动时只放一些流量过去,等运行一段时间才递增到正常流量(Dubbo是10分钟,Pigeon大约30分钟)
另外调权重为0,也是一种摘流量的方式。
唯独Netflix家的Ribbon,完全不支持权重。
2.1.3  性能,静态与动态实例列表
每家的实现算法,对性能都有影响,尤其是如何支持权重。 像Ribbon这种完全不支持权重的就首先退场了。
传统的LB,实例列表是静态的,所以一致性哈希环,按权重分配的Random环,RoundRobbin环之类的都可以预先生成。
而像我们家,需要经过动态的路由规则,最后才知道可选的实例列表,那什么什么环做起来就不容易了。
后来看了Motan的实现,一言惊醒梦中人,原来只要使劲,也可以根据当前机器的IP,预先进行机房路由的计算,预先得到每个可选目标分组的环,然后再来侦听实例的变化(注册变化,路由变化,熔断变化)。
2.1.4 可预测性
随便设计的LB算法可能会引起莫名的意外,比如RoundRobbin 和 最少在途请求数,如何与权重结合一定要谨慎。我们之前就设计过看起来可以,但跑起来压力都到了一台机上的算法。
2.2 实现的比较
2.2.1  轮询
Dubbo首先检查权重是否相等,如果相等就退化为无权重设置。其实日常99%的时段可能都是无权重的,区分快慢路径,为99%的情况作优化,是设计算法时的重要思路。
如果有权重,则每次生成个环,比对上一次的状态,在每个节点上调用完自己的权重数后,再往下一个节点走。
这里就有个问题,如果客户把三台服务器的权重分别设为100,10000,100, 本来只想表达个1:100:1的权重关系,结果。。。。连续一万个请求都压在第二台机上。
Pigeon的算法做了改进,会把权重求一个公约数(不过每个请求算一次也是累),真正达到1:100:1的效果。
我们家OSP嘛,与Dubbo有着同样的问题,这打击还没恢复过来,所以暂时主打Random,不支持RoundRobbin了。
要重新支持的话,会融合Motan的静态列表与Pigeon的公约数,直接生成一个静态环,每个实例按公约后的权重数,在环上插入若干个点,然后不断遍历这个环就行。
2.2.2  随机
这里有块测试项目新鲜程度的试金石:有没有用无锁的JDK8 ThrealLocalRandom 或者其移植类。
结果好像只有我们家OSP用了。
Dubbo同样先判断一下权重是否一致。如果不一致,则要遍历所有节点得到总权重,然后以总权重为上限生成一个随机数,然后再遍历每个节点,击鼓传花的减去其权重,直到数字小于0,那就是它了。
Pigeon和OSP的算法,和Dubbo一模一样。
Motan则像前面设计的完美版RoundRobbin一样,预先生成一个环,每个实例按公约后的权重数来插入若干个点,然后随机选一个点即可。OSP以后也要见贤思齐。
2.2.3  最小负载
最小负载的表达,最直接还是最少在途请求数(如果是同步请求,退化为最小并发数)。最小平均响应时间什么的,受不同方法不同的参数什么的影响太多。
最小负载最好就不要扯上权重了,因为说不清。Dubbo的做法,如果有在途请求数相等的节点,则再来按权重随机那套,但好像复杂了。
Motan做得有意思,考虑到服务实例的列表可能有一百几十台,全部比较一次谁的在途请求数最少太浪费时间。所以它会在列表里随机取其中一段,10个实例之间比较一下即可。
2.2.4  参数哈希
一致性哈希的基本算法:
1. 每个服务实例在一个以整数最大值为范围的环上,各占据错落有致不连续的若干个点。每个实例的点的数量可以固定如160,也可以按着权重来。
2.  然后随机一个整数,看它落在环上靠近的点是属于哪个实例的,就选择哪个实例。
与哈希取模相比,如果一台服务器挂了,只会让本来属于这台服务器的数据去到其他服务器,而原本属于其他服务器的数据则不会变。而哈希取模,模7 和 模8,几乎所有数据的落位都变了。
如果服务实例有一百个,每次生成一万六千个点的环,成本还是蛮大的。所以Dubbo采用了预生成的方式,只要实例列表的SystemIdentityHashCode不变,就继续用下去。
刀刀说Dubbo有个Bug,用可变的FullUrl 而不是固定的Host来生成点,会起不到一致性哈希的效果,另外用md5而不是新潮的murmurhash,也是历史感的体现。
Motan的类名虽然叫一致性哈希,但实现上好像不是......
OSP因为成本关系,暂时是直接取模的,日后也要学习Motan静态化服务列表,按权重构造一致性哈希环。
最后,拿什么来哈希呢?Dubbo和Motan哈希了全部请求参数(Dubbo也可以配置只哈希第1-N个参数),考虑到这样哈希成本非常不可控,及Proxy(支持多语言所用,以后再谈)要避免做反序列化,OSP采用了让客户端自己往Header中放分区键的方式。
Ribbon和Pigeon不支持Hash算法。
3. 路由规则定制
路由规则的定制,可以演化出无数的用法,如分流与隔离(读写分离,将非重要调用者分流到小集群,为重要调用者保留机器等等),AB测试,黑白名单等等。
经常听到的各种热词,都能靠路由规则支持。能用来做什么,只受限于配置员的想象力。
3.1 Dubbo的DSL
Dubbo的DSL语言:
比如:为重要调用者保留机器
application != kylin => host != 172.22.3.95,172.22.3.96
比如读写分离:
method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96
条件包括applicaiton,method, host, port
判断包括 ==,与 !=, 
值可以是 ","分隔的列表,"*"代表的后缀匹配。
还可以用Java支持的脚本语言如JRuby,Groovy来自定义规则。
3.2 Motan的DSL
Motan的DSL简单很多,基本只支持两端的服务名作条件,再配上两端的IP地址,也能玩出不少花样了。

服务名的定义:
#匹配com.weibo下以User开头的不包括UserMapping的所有服务,或以Status开头的所有服务 (com.weibo.User* & !com.weibo.UserMapping) | com.weibo.Status*以 to 分隔的两端IP:* to 10.75.1.* 10.75.2.* to 10.73.1.* * to !10.75.1.1


3.3 唯品会OSP的JSON化界面定义

我们的框架,原本也专门用Scala来解析的DSL,得益于Scala的强大(在祥威武),只用很少语句就很简洁实现了。
但后来我们老板觉得,还是需要一种更规范化,结构化,容易扩展的表达方式,所以选择了JSON(其实就是把前面DSL语法解析后的Java对象JSON化表达出来),并且独立出“目标集群”的概念。

3.3.1  整体语法 

条件集是多个条件的“与”的组合。

按顺序匹配每个条件集,中了就去到它的目标服务集群。

如果都没符合的,则走到“默认集群”去。

规则集 与 目标集群 是多对多的关系。 一个规则集可以对应多个有顺序的目标集群,用于满足HA的需求,如果集群A中的所有服务实例均不可用,则按顺序访问集群B中的实例。

目标服务集群,可以一个个IP定义,可以子网掩码或范围定义。注意Docker化之后,IP都是动态的,要么用范围定义,要么用Label。权重什么的也在里面顺便标明了。

3.3.2  条件语法 

参数列表:

客户端的应用名称

客户端的IP地址

服务的版本号,方法名

由客户端主动放入到Header中的上下文属性(绕过了复杂的参数提取,改为客户端自己放入),从这里玩出万千花样。
条件判断则包括了!=,  ==,  正则匹配,逗号分隔的列表等,还有针对某些特定类型的,如针对IP的子网掩码匹配。

3.3.3  运维友好的配置界面 




4. 机房路由
4.1 距离优先路由

虽然有爱因斯坦相对论的加持,从广州跑到北京的机房,几十毫秒的延时还是有的,所以,最好完全按距离远近来决定路由。

优先调用同机房的服务。但如果本机房的该服务的所有实例都挂了,或者服务只部署在其他机房,就调用离得最近的那个机房。

做起来其实也简单,一般按服务器IP的前两位,就能区分出机房来。然后做个配置,把每个机房之间的距离粗略量化出来。至于根据网络状态的动态选路,暂时不需要这么高大上的东西。

好像只有美团点评的Pigeon和我们自己家的OSP实现了。

4.2 其他方式
另外几家,虽然没有优先路由,但机房仍然是个跨不过的概念。
Motan的实现,通过前面提到的DSL语法,还能手工做流量容灾切换。
Dubbo的实现里,注册中心是按机房部署的,每个服务可以注册到一个或多个注册中心。
Netflix的Robbin,则有一种SameAvaiableZoneRule的负载均衡方式。不过它把机房路由也当成一种负载均衡,因此选了它的话,就硬编码了使用Random做均衡。
5. 小结

最后,还应该有一种静态配置文件的方式(类似Motan的DSL),在开发联调时,绕过上面所有所有的路由和负载均衡,直达目标测试服务器。
一通比较下来,感觉自家的OSP虽然还有种种待改进的地方,但整体完成度还可以。

Netflix家的Ribbon,基本上没什么亮点,处处敬陪末席。

还有,开源真好,太多让人眼前一亮的代码。

版权声明:本文版权归  江南白衣@唯品会 所有。任何形式的转载,必须获得作者授权。
本文可能还会持续修改,请戳 “阅读原文” 获得最新版本。 查看全部
从这章开始,各个服务化框架之间要贴身肉搏的比较了。
以Java体系的,有被真实地大量使用的,能接触到源码的这三条作标准,选择了这些学习对象: Dubbo与Dubbox兄弟,新浪微博的Motan,美团点评的Pigeon,还有SpringCloud/Netflix家的一应物件。嗯,当然还有唯品会自家出品的OSP。
推家的Finagle,谷家的GRPC,还有Netty作者写的Armeria更偏重于RPC框架,这章先不出场。
1. 服务注册与发现
时代在进步,服务注册与发现简单讲讲就行。
1.1 几桩好处
与Apache,Nginx,HAProxy,LVS这些传统的Load Balancer对比,服务的自注册自发现+智能客户端有几桩好处:
一、绕过了中央的代理节点,少了一层网络节点。
二、避免了中央的代理节点自身的处理速度和网卡带宽的瓶颈,以及LB自身的高可用。
传统的LB节点,需要KeepAlived之类的方案来做一个主备。然后前面再放一个DNS轮询之类的方案来做扩容。考虑到DNS的TTL设置,一旦主从LB一起失效,还要等待比如10分钟的过期时间,或紧急绑定客户机的/etc/host。
三、往日的LB,更改所代理的集群实例还要再重启一把。不过现在好像不需要了,比如Docker里的Bamboo方案,就可以动态注册Docker容器到HAProxy中。这桩好处便大大削弱了,只剩这注册可以不假于外物。
四、智能客户端上可以做更多的服务治理逻辑,比往Nginx上挂Lua脚本强。
1.2 大致功能
框架启动时,应用等完成初始预热了,才注册迎客。
框架退出时,先把自己从注册中心下线,稍等所有客户端都收到这个通知,再完成手头剩下的请求了,才优雅的转身退出。
还应提供API,一来是运维摘流量的一种方式,二来应用程序自己,可以随时将某个实例临时下线,比如应用做一些缓存刷新的工作时,便不希望接客。
另外一件要紧事情,注册中心虽然也会与服务端进行定时心跳。但这毕竟走的是注册中心与服务端之间的特殊通道,而不像传统LB的Health Check URL那样直接与服务端口进行交互,所以两者效用不可等同,还得有别的健康检查措施。
注册中心的DashBoard?觉得可以融合在服务治理中心里,单独的DashBoard意义不大。
1.3 种种实现
服务注册中心可以有zk,etcd,consul,我自己只用过ZK,其他没什么发言权,但和配置中心一样,etcd看着不错,有些框架可以同时支持其中的二三者。
Netflix家也有个Eureka,目前的版本基于RESTFul的API, 所以推送能力比前几家弱,靠着默认定时30秒的刷新,Server间要数据同步,Client与Server要数据同步,文档里说最多两分钟才保证通知到所有客户端,与ZK们秒级的推送速度相差甚远。对于脑裂的情况,Eureka的态度是宁愿保留坏数据,不要丢失好数据,见仁见智。
2. 负载均衡
2.1 衡量的标准
2.1.1  算法一览
1.Round Robbin(轮询)
最古老的算法最可信的算法,缺点是有状态,必须在并发之下记住上一次到谁了。
2.Random(随机)
最简单的,无状态的算法。
3. Least Load(最小负载)
可以实现某种程度上的智能调节。具体的实现包括最少在途请求数(发出去了还没收到应答的请求),最少并发连接数,最小平均响应时间等等。
4. Hash (参数哈希)
根据参数进行哈希,一来有分区的效果(参见单元化架构,可改善数据库连接,缓存热度等),二来可以Stick Session(可本机进行防刷运算等)。
2.1.2  权重支持
传统LB都有权重设置, 机器(Docker)硬件配置的不一致需要权重,压测引流需要权重。
灰度发布也需要权重。假如这个应用只有3台服务器,灰度发布1台,万一有错,那就是33%的失败率了。如果信心不足,可以增加一个灰度批次,先把第一台的权重降到原来的1%。
还有,机器刚启动时,需要一点预热的时间(如Class Loading),如果一开始就把一堆请求压给它,可能会有批量的超时,所以最好刚启动时只放一些流量过去,等运行一段时间才递增到正常流量(Dubbo是10分钟,Pigeon大约30分钟)
另外调权重为0,也是一种摘流量的方式。
唯独Netflix家的Ribbon,完全不支持权重。
2.1.3  性能,静态与动态实例列表
每家的实现算法,对性能都有影响,尤其是如何支持权重。 像Ribbon这种完全不支持权重的就首先退场了。
传统的LB,实例列表是静态的,所以一致性哈希环,按权重分配的Random环,RoundRobbin环之类的都可以预先生成。
而像我们家,需要经过动态的路由规则,最后才知道可选的实例列表,那什么什么环做起来就不容易了。
后来看了Motan的实现,一言惊醒梦中人,原来只要使劲,也可以根据当前机器的IP,预先进行机房路由的计算,预先得到每个可选目标分组的环,然后再来侦听实例的变化(注册变化,路由变化,熔断变化)。
2.1.4 可预测性
随便设计的LB算法可能会引起莫名的意外,比如RoundRobbin 和 最少在途请求数,如何与权重结合一定要谨慎。我们之前就设计过看起来可以,但跑起来压力都到了一台机上的算法。
2.2 实现的比较
2.2.1  轮询
Dubbo首先检查权重是否相等,如果相等就退化为无权重设置。其实日常99%的时段可能都是无权重的,区分快慢路径,为99%的情况作优化,是设计算法时的重要思路。
如果有权重,则每次生成个环,比对上一次的状态,在每个节点上调用完自己的权重数后,再往下一个节点走。
这里就有个问题,如果客户把三台服务器的权重分别设为100,10000,100, 本来只想表达个1:100:1的权重关系,结果。。。。连续一万个请求都压在第二台机上。
Pigeon的算法做了改进,会把权重求一个公约数(不过每个请求算一次也是累),真正达到1:100:1的效果。
我们家OSP嘛,与Dubbo有着同样的问题,这打击还没恢复过来,所以暂时主打Random,不支持RoundRobbin了。
要重新支持的话,会融合Motan的静态列表与Pigeon的公约数,直接生成一个静态环,每个实例按公约后的权重数,在环上插入若干个点,然后不断遍历这个环就行。
2.2.2  随机
这里有块测试项目新鲜程度的试金石:有没有用无锁的JDK8 ThrealLocalRandom 或者其移植类。
结果好像只有我们家OSP用了。
Dubbo同样先判断一下权重是否一致。如果不一致,则要遍历所有节点得到总权重,然后以总权重为上限生成一个随机数,然后再遍历每个节点,击鼓传花的减去其权重,直到数字小于0,那就是它了。
Pigeon和OSP的算法,和Dubbo一模一样。
Motan则像前面设计的完美版RoundRobbin一样,预先生成一个环,每个实例按公约后的权重数来插入若干个点,然后随机选一个点即可。OSP以后也要见贤思齐。
2.2.3  最小负载
最小负载的表达,最直接还是最少在途请求数(如果是同步请求,退化为最小并发数)。最小平均响应时间什么的,受不同方法不同的参数什么的影响太多。
最小负载最好就不要扯上权重了,因为说不清。Dubbo的做法,如果有在途请求数相等的节点,则再来按权重随机那套,但好像复杂了。
Motan做得有意思,考虑到服务实例的列表可能有一百几十台,全部比较一次谁的在途请求数最少太浪费时间。所以它会在列表里随机取其中一段,10个实例之间比较一下即可。
2.2.4  参数哈希
一致性哈希的基本算法:
1. 每个服务实例在一个以整数最大值为范围的环上,各占据错落有致不连续的若干个点。每个实例的点的数量可以固定如160,也可以按着权重来。
2.  然后随机一个整数,看它落在环上靠近的点是属于哪个实例的,就选择哪个实例。
与哈希取模相比,如果一台服务器挂了,只会让本来属于这台服务器的数据去到其他服务器,而原本属于其他服务器的数据则不会变。而哈希取模,模7 和 模8,几乎所有数据的落位都变了。
如果服务实例有一百个,每次生成一万六千个点的环,成本还是蛮大的。所以Dubbo采用了预生成的方式,只要实例列表的SystemIdentityHashCode不变,就继续用下去。
刀刀说Dubbo有个Bug,用可变的FullUrl 而不是固定的Host来生成点,会起不到一致性哈希的效果,另外用md5而不是新潮的murmurhash,也是历史感的体现。
Motan的类名虽然叫一致性哈希,但实现上好像不是......
OSP因为成本关系,暂时是直接取模的,日后也要学习Motan静态化服务列表,按权重构造一致性哈希环。
最后,拿什么来哈希呢?Dubbo和Motan哈希了全部请求参数(Dubbo也可以配置只哈希第1-N个参数),考虑到这样哈希成本非常不可控,及Proxy(支持多语言所用,以后再谈)要避免做反序列化,OSP采用了让客户端自己往Header中放分区键的方式。
Ribbon和Pigeon不支持Hash算法。
3. 路由规则定制
路由规则的定制,可以演化出无数的用法,如分流与隔离(读写分离,将非重要调用者分流到小集群,为重要调用者保留机器等等),AB测试,黑白名单等等。
经常听到的各种热词,都能靠路由规则支持。能用来做什么,只受限于配置员的想象力。
3.1 Dubbo的DSL
Dubbo的DSL语言:
比如:为重要调用者保留机器
application != kylin => host != 172.22.3.95,172.22.3.96
比如读写分离:
method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96
条件包括applicaiton,method, host, port
判断包括 ==,与 !=, 
值可以是 ","分隔的列表,"*"代表的后缀匹配。
还可以用Java支持的脚本语言如JRuby,Groovy来自定义规则。
3.2 Motan的DSL
Motan的DSL简单很多,基本只支持两端的服务名作条件,再配上两端的IP地址,也能玩出不少花样了。

服务名的定义:
#匹配com.weibo下以User开头的不包括UserMapping的所有服务,或以Status开头的所有服务 (com.weibo.User* & !com.weibo.UserMapping) | com.weibo.Status*以 to 分隔的两端IP:* to 10.75.1.* 10.75.2.* to 10.73.1.* * to !10.75.1.1


3.3 唯品会OSP的JSON化界面定义

我们的框架,原本也专门用Scala来解析的DSL,得益于Scala的强大(在祥威武),只用很少语句就很简洁实现了。
但后来我们老板觉得,还是需要一种更规范化,结构化,容易扩展的表达方式,所以选择了JSON(其实就是把前面DSL语法解析后的Java对象JSON化表达出来),并且独立出“目标集群”的概念。

3.3.1  整体语法 

条件集是多个条件的“与”的组合。

按顺序匹配每个条件集,中了就去到它的目标服务集群。

如果都没符合的,则走到“默认集群”去。

规则集 与 目标集群 是多对多的关系。 一个规则集可以对应多个有顺序的目标集群,用于满足HA的需求,如果集群A中的所有服务实例均不可用,则按顺序访问集群B中的实例。

目标服务集群,可以一个个IP定义,可以子网掩码或范围定义。注意Docker化之后,IP都是动态的,要么用范围定义,要么用Label。权重什么的也在里面顺便标明了。

3.3.2  条件语法 

参数列表:

客户端的应用名称

客户端的IP地址

服务的版本号,方法名

由客户端主动放入到Header中的上下文属性(绕过了复杂的参数提取,改为客户端自己放入),从这里玩出万千花样。
条件判断则包括了!=,  ==,  正则匹配,逗号分隔的列表等,还有针对某些特定类型的,如针对IP的子网掩码匹配。

3.3.3  运维友好的配置界面 
640.jpg

4. 机房路由
4.1 距离优先路由

虽然有爱因斯坦相对论的加持,从广州跑到北京的机房,几十毫秒的延时还是有的,所以,最好完全按距离远近来决定路由。

优先调用同机房的服务。但如果本机房的该服务的所有实例都挂了,或者服务只部署在其他机房,就调用离得最近的那个机房。

做起来其实也简单,一般按服务器IP的前两位,就能区分出机房来。然后做个配置,把每个机房之间的距离粗略量化出来。至于根据网络状态的动态选路,暂时不需要这么高大上的东西。

好像只有美团点评的Pigeon和我们自己家的OSP实现了。

4.2 其他方式
另外几家,虽然没有优先路由,但机房仍然是个跨不过的概念。
Motan的实现,通过前面提到的DSL语法,还能手工做流量容灾切换。
Dubbo的实现里,注册中心是按机房部署的,每个服务可以注册到一个或多个注册中心。
Netflix的Robbin,则有一种SameAvaiableZoneRule的负载均衡方式。不过它把机房路由也当成一种负载均衡,因此选了它的话,就硬编码了使用Random做均衡。
5. 小结

最后,还应该有一种静态配置文件的方式(类似Motan的DSL),在开发联调时,绕过上面所有所有的路由和负载均衡,直达目标测试服务器。
一通比较下来,感觉自家的OSP虽然还有种种待改进的地方,但整体完成度还可以。

Netflix家的Ribbon,基本上没什么亮点,处处敬陪末席。

还有,开源真好,太多让人眼前一亮的代码。

版权声明:本文版权归  江南白衣@唯品会 所有。任何形式的转载,必须获得作者授权。
本文可能还会持续修改,请戳 “阅读原文” 获得最新版本。

漫谈面向对象基石之开闭原则(OCP)

默认分类admin 发表了文章 • 0 个评论 • 135 次浏览 • 2016-12-30 10:37 • 来自相关话题

开闭原则的意思是软件实体应该对扩展开发,对修改关闭(Software entities should be open for extension,but closed for modification)。实现开闭原则的途径是抽象,将需要扩展的部分抽象出来,并留出扩展接口。打个比方,比如电脑机箱上有usb的插口,这些插口就是可扩展的部分,我们可以在这些usb插口上插上鼠标,键盘,U盘,还可以插上网银的U盾等等。电脑硬件上对于usb接口的这个设计就是一个符合开闭原则的设计。

为什么要遵循开闭原则呢?因为开闭原则可以使软件系统更容易复用,更容易维护,当某个软件实体,不适合了,我可以重新做另外一种实现,并将现有的实现替换掉。比如说统计个税的算法发生了一些变化,我可以在不改变原有代码的情况下,重新实现一个算法将原有的算法替换下来。比如说杀毒软件,在出现一种新的病毒时,开发出一个查杀这种病毒的新模块,可以只开发更新这个查杀模块,而不需要改变原有系统的内容。

开闭原则这么好,如何实现符合开闭原则的软件系统呢?答案是抽象,将可能发生变化的功能点进行抽象,并留出变化的接口。设计模式中很多模式都可以帮我们实现开闭原则,个人的理解设计模式是对抽象用法的一种总结。其实我们在项目已经为开闭原则做了一些工作了,比如说我们进行三层开发,将数据层抽象出来,并定义个数据处理的接口,我们可以通过新开发一个数据层把刚开始将数据存放到sql server中的实现,修改为将数据存放到my sql中的实现;我们将业务逻辑中的代码从UI代码中分离出来,这就为我们复用业务逻辑的代码提供了可能,我们可以开发一个专门为手机使用的UI层出来,当用户用手机访问我们的系统时,智能的切换到手机UI层的代码上去执行。
开闭原则实现的关键点在于抽象,也许我们刚开始不知道该把那部分抽象出来,但是这并不是问题,我们可以遵循简单设计的原则,当变化来了的时候,再重构代码,做到一种满足开闭原则的设计。

切忌到处都抽象,如果到处都抽象就会导致系统过度设计,过度复杂。这反而是不利于系统的维护。完全的开闭原则是不可能实现的,所以请保持简单设计,在需要的时候做符合开闭原则的设计。 查看全部
开闭原则的意思是软件实体应该对扩展开发,对修改关闭(Software entities should be open for extension,but closed for modification)。实现开闭原则的途径是抽象,将需要扩展的部分抽象出来,并留出扩展接口。打个比方,比如电脑机箱上有usb的插口,这些插口就是可扩展的部分,我们可以在这些usb插口上插上鼠标,键盘,U盘,还可以插上网银的U盾等等。电脑硬件上对于usb接口的这个设计就是一个符合开闭原则的设计。

为什么要遵循开闭原则呢?因为开闭原则可以使软件系统更容易复用,更容易维护,当某个软件实体,不适合了,我可以重新做另外一种实现,并将现有的实现替换掉。比如说统计个税的算法发生了一些变化,我可以在不改变原有代码的情况下,重新实现一个算法将原有的算法替换下来。比如说杀毒软件,在出现一种新的病毒时,开发出一个查杀这种病毒的新模块,可以只开发更新这个查杀模块,而不需要改变原有系统的内容。

开闭原则这么好,如何实现符合开闭原则的软件系统呢?答案是抽象,将可能发生变化的功能点进行抽象,并留出变化的接口。设计模式中很多模式都可以帮我们实现开闭原则,个人的理解设计模式是对抽象用法的一种总结。其实我们在项目已经为开闭原则做了一些工作了,比如说我们进行三层开发,将数据层抽象出来,并定义个数据处理的接口,我们可以通过新开发一个数据层把刚开始将数据存放到sql server中的实现,修改为将数据存放到my sql中的实现;我们将业务逻辑中的代码从UI代码中分离出来,这就为我们复用业务逻辑的代码提供了可能,我们可以开发一个专门为手机使用的UI层出来,当用户用手机访问我们的系统时,智能的切换到手机UI层的代码上去执行。
开闭原则实现的关键点在于抽象,也许我们刚开始不知道该把那部分抽象出来,但是这并不是问题,我们可以遵循简单设计的原则,当变化来了的时候,再重构代码,做到一种满足开闭原则的设计。

切忌到处都抽象,如果到处都抽象就会导致系统过度设计,过度复杂。这反而是不利于系统的维护。完全的开闭原则是不可能实现的,所以请保持简单设计,在需要的时候做符合开闭原则的设计。

开发一个业务逻辑复杂的系统,应该怎么样设计才能使项目的扩展性更好?

默认分类admin 发表了文章 • 0 个评论 • 145 次浏览 • 2016-12-29 21:01 • 来自相关话题

既然业务逻辑复杂,那意味着项目前期的业务建模、需求分析、分析设计极为重要,直接抛开这几个阶段进入技术实施开发阶段,不管套用什么设计模式、架构模式,系统的扩展性肯定难以保证。

项目的扩展性虽然最终体现为系统架构、技术实现的扩展性,但系统扩展性的根源在于系统业务架构及业务模型的扩展性。大家经常骂xx系统烂、扩展性差,大都将原因归结为技术实现烂,但总结那些成功的大型项目或产品的最佳实践,原因都会有:某某是业务专家,对xx业务很熟悉,能够衔接业务与技术。因此一个好的项目角色中,应该有行业专家/领域专家、业务过程分析师、系统分析师、软件架构师等角色,从业务架构、信息架构、技术架构保证系统的扩展性。

具体怎样进行业务建模,搭建良好的业务架构和业务模型,从而为技术架构、信息架构、技术实现奠定良好基础,有一些较为成熟的软件开发过程可供参考。例如 RUP(Rational Unified Process,统一软件开发过程)。一个标准的RUP工作流程包括:业务建模,需求分析,分析设计,实施开发,测试,部署,配置和变更管理,项目管理,环境。当然RUP只是一个方法论,且过于庞大,大部分项目很难完整执行其过程,需要根据实际情况进行裁剪,但其方法论对于复杂业务逻辑系统的建设具有指导意义。像互联网产品设计中常用的用例分析技术就源于RUP。

因此对于题主描述的一个复杂系统,标准的过程应当在业务建模,需求分析,分析设计,实施开发,测试,部署完整过程的分析设计(与开发语言无关)或实施开发(分析设计的成果映射为具体语言,例如Java、.NET等)阶段才考虑设计模式、架构模式的引入。设计模式的使用会经历僵化->固化->优化的阶段,类似禅修中“看山是山、看水是水”的三个阶段,才能体会模式的运用之妙。

值得强调的是:如果是偏交易(例如支付、金融)的系统,在考虑扩展性时候,一定要将信息架构、信息模型的扩展性纳入到考虑范围,此类系统数据模型至关重要,也不可能频繁变动。

上面描述方法的特别适用与传统软件、系统集成等需求偏稳定的项目,对于互联网偏创新性的项目就不一定完全适用了,此类项目的现实情况如下:业务模式不确定,会不停试错,验证模式;需求不停变化,要求能够快速响应;全新的行业,没有行业专家,没有行业标杆可借鉴(至多有跨界标杆可参考);此时候,类似精益创业、Scrum之类的敏捷开发模式更适合,但对于复杂的业务而言,业务建模->需求分析->分析设计的理念仍然值得参考借鉴。

最后,最最重要的是:完美系统的架构和扩展性是管理出来的、持续重构出来的。正如各大城市马路不停翻了再修、修了再翻的命运一样,中国大部分公司后任会不停否定掉前任的架构、系统,推倒再来一遍,然后等新系统刚开发出来不久,尚未上线或上线运营一段时间后,再换一帮人继续折腾,然后。。。

总结这么多年的经历,深刻体会到:再烂的系统和架构,如果能够强化管理、持续积累、持续重构、持续完善,都能够有机会成为完美的系统,完美的系统不在于其架构的牛逼和完美,而在于:符合公司的业务模式,能够完美支撑公司业务的高速发展和市场需求的快速响应。

 

http://www.zhihu.com/question/ ... 06943 查看全部
既然业务逻辑复杂,那意味着项目前期的业务建模、需求分析、分析设计极为重要,直接抛开这几个阶段进入技术实施开发阶段,不管套用什么设计模式、架构模式,系统的扩展性肯定难以保证。

项目的扩展性虽然最终体现为系统架构、技术实现的扩展性,但系统扩展性的根源在于系统业务架构及业务模型的扩展性。大家经常骂xx系统烂、扩展性差,大都将原因归结为技术实现烂,但总结那些成功的大型项目或产品的最佳实践,原因都会有:某某是业务专家,对xx业务很熟悉,能够衔接业务与技术。因此一个好的项目角色中,应该有行业专家/领域专家、业务过程分析师、系统分析师、软件架构师等角色,从业务架构、信息架构、技术架构保证系统的扩展性。

具体怎样进行业务建模,搭建良好的业务架构和业务模型,从而为技术架构、信息架构、技术实现奠定良好基础,有一些较为成熟的软件开发过程可供参考。例如 RUP(Rational Unified Process,统一软件开发过程)。一个标准的RUP工作流程包括:业务建模,需求分析,分析设计,实施开发,测试,部署,配置和变更管理,项目管理,环境。当然RUP只是一个方法论,且过于庞大,大部分项目很难完整执行其过程,需要根据实际情况进行裁剪,但其方法论对于复杂业务逻辑系统的建设具有指导意义。像互联网产品设计中常用的用例分析技术就源于RUP。

因此对于题主描述的一个复杂系统,标准的过程应当在业务建模,需求分析,分析设计,实施开发,测试,部署完整过程的分析设计(与开发语言无关)或实施开发(分析设计的成果映射为具体语言,例如Java、.NET等)阶段才考虑设计模式、架构模式的引入。设计模式的使用会经历僵化->固化->优化的阶段,类似禅修中“看山是山、看水是水”的三个阶段,才能体会模式的运用之妙。

值得强调的是:如果是偏交易(例如支付、金融)的系统,在考虑扩展性时候,一定要将信息架构、信息模型的扩展性纳入到考虑范围,此类系统数据模型至关重要,也不可能频繁变动。

上面描述方法的特别适用与传统软件、系统集成等需求偏稳定的项目,对于互联网偏创新性的项目就不一定完全适用了,此类项目的现实情况如下:业务模式不确定,会不停试错,验证模式;需求不停变化,要求能够快速响应;全新的行业,没有行业专家,没有行业标杆可借鉴(至多有跨界标杆可参考);此时候,类似精益创业、Scrum之类的敏捷开发模式更适合,但对于复杂的业务而言,业务建模->需求分析->分析设计的理念仍然值得参考借鉴。

最后,最最重要的是:完美系统的架构和扩展性是管理出来的、持续重构出来的。正如各大城市马路不停翻了再修、修了再翻的命运一样,中国大部分公司后任会不停否定掉前任的架构、系统,推倒再来一遍,然后等新系统刚开发出来不久,尚未上线或上线运营一段时间后,再换一帮人继续折腾,然后。。。

总结这么多年的经历,深刻体会到:再烂的系统和架构,如果能够强化管理、持续积累、持续重构、持续完善,都能够有机会成为完美的系统,完美的系统不在于其架构的牛逼和完美,而在于:符合公司的业务模式,能够完美支撑公司业务的高速发展和市场需求的快速响应。

 

http://www.zhihu.com/question/ ... 06943

现在有这样一个需求,在一秒中有3万的支付订单请求,有什么比较好的解决方案吗?

回复

默认分类admin 回复了问题 • 1 人关注 • 5 个回复 • 195 次浏览 • 2016-12-29 20:50 • 来自相关话题

站在岸上看DEVOPS

默认分类admin 发表了文章 • 0 个评论 • 144 次浏览 • 2017-01-17 17:41 • 来自相关话题

今年项目要试水(X)AAS,尝试运维自己的产品,便离不开这几年最热的单词—DEVOPS。趁还在岸上,理解下DEVOPS对开发人员来说意味着什么。

因为没有实战,也就纸上谈兵一下,真正有经验的人, @连城404:为了分摊黑锅,运维发明了devops和full stack。

What is DEVOPS ?

十年前,敏捷出现,以反瀑布流程的方式模糊了需求、架构、开发、测试之间的界限,但对整个软件价值链的最后一环,交付与运维并没有太多的强调,造成一个有趣的现象是,产品以三周为迭代进行开发并交付给客户,但只充当与客户演示并收集反馈的媒介,而客户仍然会有自己独立的上线周期。

十年后,越来越多的互联网站与手机应用,需要频繁而持续的发布,每次发布需要部署到大量的机器集群上,需要不中断和破坏当前服务,在发布前能提供本次发布质量的证明,在发布后对系统有实时的监控,支持错误转移、服务降级,支持快速定位和修复故障。

显然,只靠传统运维人员的知识技能集(操作系统、网络、Shell脚本编写) 以及开发人员编写的的产品管理手册已不足以满足上述的需求了。好在,有敏捷的先例,解决方法是现成的——把开发拖下水吧:

1. 人员上,运维人员要更懂产品的架构与内在,而不只是那份管理员手册。开发人员要懂实际的运维,从实际部署、上线流程,到故障的定位与解决。当然,最重要是打通两个部门间的隔阂。

2. 产品特性上,运维所需的功能甚至运维的基础设施,成为了产品非功能性需求的重要一部分。

3. 流程上,产品交付与运维,接入整个软件生产周期构成一个完整的环。
 

What should developer do?

现在大家最关心的问题来了,DEVOPS的大旗下,开发人员到底要参与多少运维?

这幅图,又是零到一百之间的选择,各个组织根据自己的人员、技术,文化,找到自己最合适的那个平衡点。

理论上,开发人员在DEVOPS中可以有两种的参与:

一,为运维而开发。

比如,在代码中加入Metrics收集的代码,向Graphite吐出服务的调用次数与延时,通常是与运维协作的第一步——运维可以在CPU,内存占用与应用日志之外,不那么黑盒的看待应用了。

又比如,要和运维商量着,在代码端需要做些什么,才能实现监控,无中断升级,动态节点扩展,故障转移,服务降级,跨机房容灾。这些功能以前可能也是要的,但现在需求的采集更注重实效。甚至,运维的一些基础设施,也可以由开发代劳了。

又比如,环境与流程的统一,原来只在简版的集成测试环境上跑完功能测试就完事的开发闭环,现在需要考虑至少延伸到一台从服务器配置到数据库部署模型等都贴近生产环境的Staging服务器,又比如测试环境的安装部署,也会用和运维一样的基于Ansible 或 Saltstack。

二,开发直接参与运维。

也是最存在争议的地方,比如系统的上线升级,日常的运维操作,故障的即时修复。

开发人员参与运维,可以吃自己的狗粮,可以更深切的了解运维功能的需求,开发人员的故障修复更快,但是——

程序员是不是应该做更有创造性的事情?程序员做运维会不会感到厌烦?社会化分工还是不是一个无需争辩的现代社会真理?

其实类似的争论已发生过一次,在开发与测试的分工上。起码在我司,专门的测试人员仍然有不可动摇的作用,而开发人员,则承担了从持续的单元测试和功能测试,最后到手工的性能测试与稳定性测试,总体80%以上的测试工作量,很自然的合作。

所以,还是没有绝对,还是各个组织自己去通过不断的尝试,找到自己最合适的那个平衡点。 查看全部
今年项目要试水(X)AAS,尝试运维自己的产品,便离不开这几年最热的单词—DEVOPS。趁还在岸上,理解下DEVOPS对开发人员来说意味着什么。

因为没有实战,也就纸上谈兵一下,真正有经验的人, @连城404:为了分摊黑锅,运维发明了devops和full stack。

What is DEVOPS ?

十年前,敏捷出现,以反瀑布流程的方式模糊了需求、架构、开发、测试之间的界限,但对整个软件价值链的最后一环,交付与运维并没有太多的强调,造成一个有趣的现象是,产品以三周为迭代进行开发并交付给客户,但只充当与客户演示并收集反馈的媒介,而客户仍然会有自己独立的上线周期。

十年后,越来越多的互联网站与手机应用,需要频繁而持续的发布,每次发布需要部署到大量的机器集群上,需要不中断和破坏当前服务,在发布前能提供本次发布质量的证明,在发布后对系统有实时的监控,支持错误转移、服务降级,支持快速定位和修复故障。

显然,只靠传统运维人员的知识技能集(操作系统、网络、Shell脚本编写) 以及开发人员编写的的产品管理手册已不足以满足上述的需求了。好在,有敏捷的先例,解决方法是现成的——把开发拖下水吧:

1. 人员上,运维人员要更懂产品的架构与内在,而不只是那份管理员手册。开发人员要懂实际的运维,从实际部署、上线流程,到故障的定位与解决。当然,最重要是打通两个部门间的隔阂。

2. 产品特性上,运维所需的功能甚至运维的基础设施,成为了产品非功能性需求的重要一部分。

3. 流程上,产品交付与运维,接入整个软件生产周期构成一个完整的环。
 

What should developer do?

现在大家最关心的问题来了,DEVOPS的大旗下,开发人员到底要参与多少运维?

这幅图,又是零到一百之间的选择,各个组织根据自己的人员、技术,文化,找到自己最合适的那个平衡点。

理论上,开发人员在DEVOPS中可以有两种的参与:

一,为运维而开发。

比如,在代码中加入Metrics收集的代码,向Graphite吐出服务的调用次数与延时,通常是与运维协作的第一步——运维可以在CPU,内存占用与应用日志之外,不那么黑盒的看待应用了。

又比如,要和运维商量着,在代码端需要做些什么,才能实现监控,无中断升级,动态节点扩展,故障转移,服务降级,跨机房容灾。这些功能以前可能也是要的,但现在需求的采集更注重实效。甚至,运维的一些基础设施,也可以由开发代劳了。

又比如,环境与流程的统一,原来只在简版的集成测试环境上跑完功能测试就完事的开发闭环,现在需要考虑至少延伸到一台从服务器配置到数据库部署模型等都贴近生产环境的Staging服务器,又比如测试环境的安装部署,也会用和运维一样的基于Ansible 或 Saltstack。

二,开发直接参与运维。

也是最存在争议的地方,比如系统的上线升级,日常的运维操作,故障的即时修复。

开发人员参与运维,可以吃自己的狗粮,可以更深切的了解运维功能的需求,开发人员的故障修复更快,但是——

程序员是不是应该做更有创造性的事情?程序员做运维会不会感到厌烦?社会化分工还是不是一个无需争辩的现代社会真理?

其实类似的争论已发生过一次,在开发与测试的分工上。起码在我司,专门的测试人员仍然有不可动摇的作用,而开发人员,则承担了从持续的单元测试和功能测试,最后到手工的性能测试与稳定性测试,总体80%以上的测试工作量,很自然的合作。

所以,还是没有绝对,还是各个组织自己去通过不断的尝试,找到自己最合适的那个平衡点。

架构师应该编码吗?

默认分类admin 发表了文章 • 0 个评论 • 155 次浏览 • 2017-01-17 17:19 • 来自相关话题

架构师应该编码吗?

有些公司认为架构师太宝贵了,不该承担日常编码工作。
有人认为优秀的架构师的重要特征是抽象思维能力,也可以理解为不把时间耗在细节里。
还有一些大型项目通常意味着照看更大的“大局”,有可能你根本没时间写代码。

以上都对。

 

你不必放弃编码,也不要把大部分时间用于编码

你不应该因为“我是架构师”,就把自己排除在编码之外。
但也必须有足够的时间扮演技术架构师的角色。

1. 参与编写代码

要避免成为PPT架构师, 最好是参与实现与交付的过程,确保架构的交付,了解设计在实现上的问题,演进架构而不是画完框图就交给实现团队从此不管。
同时,缩短与团队的距离,保持对团队的影响力,帮助团队对架构的正确理解,分享自己软件开发的经验。

另外,作为开发团队的一份子,你不需要是开发代码最好的。

2. 构建原型、框架和基础

如果不能参与日常编码,至少尝试在设计时快速构建原型去验证你的概念。
还有为团队编写框架和基础,这也是最磨练与体现编码与设计能力的时刻。

3. 进行代码评审

如果完全没有时间编码,至少参与代码评审,了解发生了什么。

4. 实验并与时俱进

如果完全没有时间在工作时间里编码,在工作之外你往往有更多空间来维持编码技能,从贡献开源项目,到不断尝试最新的语言、框架。

一般来说,一个写代码的软件架构师会更有成效也更快乐。

文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/a ... .html 查看全部
架构师应该编码吗?

有些公司认为架构师太宝贵了,不该承担日常编码工作。
有人认为优秀的架构师的重要特征是抽象思维能力,也可以理解为不把时间耗在细节里。
还有一些大型项目通常意味着照看更大的“大局”,有可能你根本没时间写代码。

以上都对。

 

你不必放弃编码,也不要把大部分时间用于编码

你不应该因为“我是架构师”,就把自己排除在编码之外。
但也必须有足够的时间扮演技术架构师的角色。

1. 参与编写代码

要避免成为PPT架构师, 最好是参与实现与交付的过程,确保架构的交付,了解设计在实现上的问题,演进架构而不是画完框图就交给实现团队从此不管。
同时,缩短与团队的距离,保持对团队的影响力,帮助团队对架构的正确理解,分享自己软件开发的经验。

另外,作为开发团队的一份子,你不需要是开发代码最好的。

2. 构建原型、框架和基础

如果不能参与日常编码,至少尝试在设计时快速构建原型去验证你的概念。
还有为团队编写框架和基础,这也是最磨练与体现编码与设计能力的时刻。

3. 进行代码评审

如果完全没有时间编码,至少参与代码评审,了解发生了什么。

4. 实验并与时俱进

如果完全没有时间在工作时间里编码,在工作之外你往往有更多空间来维持编码技能,从贡献开源项目,到不断尝试最新的语言、框架。

一般来说,一个写代码的软件架构师会更有成效也更快乐。

文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/a ... .html

软件设计杂谈

默认分类admin 发表了文章 • 0 个评论 • 172 次浏览 • 2017-01-17 17:06 • 来自相关话题

作者:陈天
链接:https://zhuanlan.zhihu.com/p/20005177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

disclaimer: 本文所讲的设计,非UI/UE的设计,单单指软件代码/功能本身在技术上的设计。UI/UE的主题请出门右转找特赞(Tezign)。:)
在如今这个Lean/Agile横扫一切的年代,设计似乎有了被边缘化的倾向,做事的周期如此之快,似乎已容不下人们更多的思考。MVP(Minimal Viable Produce)在很多团队里演化成一个形而上的图腾,于是工程师们找到了一个完美的借口:我先做个MVP,设计的事,以后再说。
如果纯属个人玩票,有个点子,hack out还说得过去;但要严肃做一个项目,还是要下工夫设计一番,否则,没完没了的返工会让你无语泪千行。
设计首先得搞懂要解决的问题
工程师大多都是很聪明的人,聪明人有个最大的问题就是自负。很多人拿到一个需求,还没太搞明白其外延和内涵,代码就已经在脑袋里流转。这样做出来的系统,纵使再精妙,也免不了承受因需求理解不明确而导致的返工之苦。
搞懂需求这事,说起来简单,做起来难。需求有正确的但表达错误的需求,有正确的但没表达出来的需求,还有过度表达的需求。所以,拿到需求后,先不忙寻找解决方案,多问问自己,工作伙伴,客户follow up questions来澄清需求模糊不清之处。
搞懂需求,还需要了解需求对应的产品,公司,以及(潜在)竞争对手的现状,需求的上下文,以及需求的约束条件。人有二知二不知:
I know that I know

I know that I don’t know

I don’t know that I know

I don’t know that I don’t know

澄清需求的过程,就是不断驱逐无知,掌握现状,上下文和约束条件的过程。
这个主题讲起来很大,且非常重要,但毕竟不是本文的重点,所以就此带过。
寻找(多个)解决方案
如果对问题已经有不错的把握,接下来就是解决方案的发现之旅。这是个考察big picture的活计。同样是满足孩子想要个汽车的愿望,你可以:
去玩具店里买一个现成的

买乐高积木,然后组装

用纸糊一个,或者找块木头,刻一个

这对应软件工程问题的几种解决之道:
购买现成软件(acuquire or licensing),二次开发之(如果需要)

寻找building blocks,组装之(glue)

自己开发(build from scratch, or DIY)

大部分时候,如果a或b的TCO [1] 合理,那就不要选择c。做一个产品的目的是为客户提供某种服务,而不是证明自己能一行行码出出来这个产品。
a是个很重要的点,可惜大部分工程师脑袋里没有钱的概念,或者出于job security的私心,而忽略了。工程师现在越来越贵,能用合理的价格搞定的功能,就不该雇人去打理(自己打脸)。一个产品,最核心的部分不超过整个系统的20%,把人力资源铺在核心的部分,才是软件设计之道。
b我们稍后再讲。
对工程师而言,DIY出一个功能是个极大的诱惑。一种DIY是源自工程师的不满。任何开源软件,在处理某种特定业务逻辑的时候总会有一些不足,眼里如果把这些不足放在,却忽略了人家的好处,是大大的不妥。前两天我听到有人说 "consul sucks, …​, I’ll build our own service discovery framework…​",我就苦笑。我相信他能做出来一个简单的service discovery tool,这不是件特别困难的事情。问题是值不值得去做。如果连处于consul这个层次的基础组件都要自己去做,那要么是心太大,要么是没有定义好自己的软件系统的核心价值(除非系统的核心价值就在于此)。代码一旦写出来,无论是5000行还是50行,都是需要有人去维护的,在系统的生命周期里,每一行自己写的代码都是一笔债务,需要定期不定期地偿还利息。
另外一种DIY是出于工程师的无知。「无知者无畏」在某些场合的效果是正向的,有利于打破陈规。但在软件开发上,还是知识和眼界越丰富越开阔越好。一个无知的工程师在面对某个问题时(比如说service discovery),如果不知道这问题也许有现成的解决方案(consul),自己铆足了劲写一个,大半会有失偏颇(比如说没做上游服务的health check,或者自己本身的high availability),结果bug不断,辛辛苦苦一个个都啃下来,才发现,自己走了很多弯路,费了大半天劲,做了某个开源软件的功能的子集。当然,对工程师而言,这个练手的价值还是很大的,但对公司来说,这是一笔沉重的无意义的支出。
眼界定义了一个人的高度,如果你每天见同类的人,看同质的书籍/视频,(读)写隶属同一domain的代码,那多半眼界不够开阔。互联网的发展一日千里,变化太快,如果把自己禁锢在一方小天地里,很容易成为陶渊明笔下的桃花源中人:乃不知有汉,无论魏晋。
构建灵活且有韧性的系统
如果说之前说的都是废话,那么接下来的和真正的软件设计能扯上些关系。
分解和组合
软件设计是一个把大的问题不断分解,直至原子级的小问题,然后再不断组合的过程。这一点可以类比生物学:原子(keyword/macro)组合成分子(function),分子组合成细胞(module/class),细胞组合成组织(micro service),组织组合成器官(service),进而组合成生物(system)。
一个如此组合而成系统,是满足关注点分离(Separation of Concerns)的。大到一个器官,小到一个细胞,都各司其职,把自己要做的事情做到极致。心脏不必关心肾脏会干什么,它只需要做好自己的事情:把新鲜血液通过动脉排出,再把各个器官用过的血液从静脉回收。
分解和组合在软件设计中的作用如此重要,以至于一个系统如果合理分解,那么日后维护的代价就要小得多。同样讲关注点分离,不同的工程师,分离的方式可能完全不同。但究其根本,还有有一些规律可循。
总线(System Bus)
首先我们要把系统的总线定义出来。人体的总线,大的有几条:血管(动脉,静脉),神经网络,气管,输尿管。它们有的完全负责与外界的交互(气管,输尿管),有的完全是内部的信息中枢(血管),有的内外兼修(神经网络)。
总线把生产者和消费者分离,让彼此互不依赖。心脏往外供血时,把血压入动脉血管就是了。它并不需要知道谁是接收者。
同样的,回到我们熟悉的计算机系统,CPU访问内存也是如此:它发送一条消息给总线,总线通知RAM读取数据,然后RAM把数据返回给总线,CPU再获取之。整个过程中CPU只知道一个内存地址,毋须知道访问的具体是哪个内存槽的哪块内存 —— 总线将二者屏蔽开。
学过计算机系统的同学应该都知道,经典的PC结构有几种总线:数据总线,地址总线,控制总线,扩展总线等;做过网络设备的同学也都知道,一个经典的网络设备,其软件系统的总线分为:control plane和data plane。
路由(routing)
有了总线的概念,接下来必然要有路由。我们看人体的血管:
每一处分叉,就涉及到一次路由。
路由分为外部路由和内部路由。外部路由处理输入,把不同的输入dispatch到系统里不同的组件。做web app的,可能没有意识到,但其实每个web framework,最关键的组件之一就是url dispatch。HTTP的伟大之处就是每个request,都能通过url被dispatch到不同的handler处理。而url是目录式的,可以层层演进 —— 就像分形几何,一个大的系统,通过不断重复的模式,组合起来 —— 非常利于系统的扩展。遗憾的是,我们自己做系统,对于输入既没有总线的考量,又无路由的概念,if-else下去,久而久之,代码便绕成了意大利面条。
再举一例:DOM中的event bubble,在javascript处理起来已然隐含着路由的概念。你只需定义当某个事件(如onclick)发生时的callback函数就好,至于这事件怎么通过eventloop抵达回调函数,无需关心。好的路由系统剥茧抽丝,把繁杂的信息流正确送到处理者手中。
外部路由总还有「底层」为我们完成,内部路由则需工程师考虑。service级别的路由(数据流由哪个service处理)可以用consul等service discovery组件,service内部的路由(数据流到达后怎么处理)则需要自己完成。路由的具体方式有很多种,pattern matching最为常见。
无论用何种方式路由,数据抵达总线前为其定义Identity(ID)非常重要,你可以管这个过程叫data normalization,data encapsulation等,总之,一个消息能被路由,需要有个用于路由的ID。这ID可以是url,可以是一个message header,也可以是一个label(想象MPLS的情况)。当我们为数据赋予一个个合理的ID后,如何路由便清晰可见。
队列(Queue)
对于那些并非需要立即处理的数据,可以使用队列。队列也有把生产者和消费者分离的功效。队列有:
single producer single consumer(SPSC)

single producer multiple consumers(SPMC)

multiple producers single consumer(MPSC)

multiple producers multiple consumers(MPMC)

仔细想想,队列其实就是总线+路由(可选)+存储的一个特殊版本。一般而言,system bus之上是系统的各个service,每个service再用service bus(或者queue)把micro service chain起来,然后每个micro service内部的组件间,再用queue连接起来。
有了队列,有利于提高流水线的效率。一般而言,流水线的处理速度取决于最慢的组件。队列的存在,让慢速组件有机会运行多份,来弥补生产者和消费者速度上的差距。
Pub/Sub
存储在队列中的数据,除路由外,还有一种处理方式:pub/sub。和路由相似,pub/sub将生产者和消费者分离;但二者不同之处在于,路由的目的地由路由表中的表项控制,而pub/sub一般由publisher控制 [2]:任何subscribe某个数据的consumer,都会到publisher处注册,publisher由此可以定向发送消息。
协议(protocol)
一旦我们把系统分解成一个个service,service再分解成micro service,彼此之间互不依赖,仅仅通过总线或者队列来通讯,那么,我们就需要协议来定义彼此的行为。协议听起来很高大上,其实不然。我们写下的每个function(或者每个class),其实就是在定义一个不成文的协议:function的arity是什么,接受什么参数,返回什么结果。调用者需严格按照协议调用方能得到正确的结果。
service级别的协议是一份SLA:服务的endpoint是什么,版本是什么,接收什么格式的消息,返回什么格式的消息,消息在何种网络协议上承载,需要什么样的authorization,可以正常服务的最大吞吐量(throughput)是什么,在什么情况下会触发throttling等等。
头脑中有了总线,路由,队列,协议等这些在computer science 101中介绍的基础概念,系统的分解便有迹可寻:面对一个系统的设计,你要做的不再是一道作文题,而是一道填空题:在若干条system bus里填上其名称和流进流出的数据,在system bus之上的一个个方框里填上服务的名称和服务的功能。然后,每个服务再以此类推,直到感觉毋须再细化为止。
组成系统的必要服务
有些管理性质的服务,尽管和业务逻辑直接关系不大,但无论是任何系统,都需要考虑构建,这里罗列一二。
代谢(sweeping)
一个活着的生物时时刻刻都进行着新陈代谢:每时每刻新的细胞取代老的细胞,同时身体中的「垃圾」通过排泄系统排出体外。一个运转有序的城市也有新陈代谢:下水道,垃圾场,污水处理等维持城市的正常功能。没有了代谢功能,生物会凋零,城市会荒芜。
软件系统也是如此。日志会把硬盘写满,软件会失常,硬件会失效,网络会拥塞等等。一个好的软件系统需要一个好的代谢系统:出现异常的服务会被关闭,同样的服务会被重新启动,恢复运行。
代谢系统可以参考erlang的supervisor/child process结构,以及supervision tree。很多软件,都运行在简单的supervision tree模式下,如nginx。
高可用性(HA)
每个人都有两个肾。为了apple watch卖掉一个肾,另一个还能保证人体的正常工作。当然,人的两个肾是Active-Active工作模式,内部的肾元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家这service的做的),少了一个,performance会一点点有折扣,但可以忽略不计。
大部分软件系统里的各种服务也需要高可用性:除非完全无状态的服务,且服务重启时间在ms级。服务的高可用性和路由是息息相关的:高可用性往往意味着同一服务的冗余,同时也意味着负载分担。好的路由系统(如consul)能够对路由至同一服务的数据在多个冗余服务间进行负载分担,同时在检测出某个失效服务后,将数据路只由至正常运作的服务。
高可用性还意味着非关键服务,即便不可恢复,也只会导致系统降级,而不会让整个系统无法访问。就像壁虎的尾巴断了不妨碍壁虎逃命,人摔伤了手臂还能吃饭一样,一个软件系统里统计模块的异常不该让用户无法访问他的个人页面。
安保(security)
安保服务分为主动安全和被动安全。authentication/authorization + TLS + 敏感信息加密 + 最小化输入输出接口可以算是主动安全,防火墙等安防系统则是被动安全。
继续拿你的肾来比拟 —— 肾脏起码有两大安全系统:
输入安全。肾器的厚厚的器官膜,保护器官的输入输出安全 —— 主要的输入输出只能是肾动脉,肾静脉和输尿管。

环境安全。肾器里有大量脂肪填充,避免在撞击时对核心功能的损伤。

除此之外,人体还提供了包括免疫系统,皮肤,骨骼,空腔等一系列安全系统,从各个维度最大程度保护一个器官的正常运作。如果我们仔细研究生物,就会发现,安保是个一揽子解决方案:小到细胞,大到整个人体,都有各自的安全措施。一个软件系统也需如此考虑系统中各个层次的安全。
透支保护(overdraft protection)
任何系统,任何服务都是有服务能力的 —— 当这能力被透支时,需要一定的应急计划。如果使用拥有auto scaling的云服务(如AWS),动态扩容是最好的解决之道,但受限于所用的解决方案,它并非万灵药,AWS的auto scaling依赖于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB对某些业务,如websocket,支持不佳;而第三方的load balancer,则需要考虑部署,与Amazon的auto scaling结合(需要写点代码),避免单点故障,保证自身的capacity等一堆头疼事。
在无法auto scaling的场景最通用的做法是back pressure,把压力反馈到源头。就好像你不断熬夜,最后大脑受不了,逼着你睡觉一样。还有一种做法是服务降级,停掉非核心的service/micro-service,如analytical service,ad service,保证核心功能正常。
把设计的成果讲给别人听
完成了分解和组合,也严肃对待了诸多与业务没有直接关系,但又不得不做的必要功能后,接下来就是要把设计在白板上画下来,讲给任何一个利益相关者听。听他们的反馈。设计不是一个闭门造车的过程,全程都需要和各种利益相关者交流。然而,很多人都忽视了设计定型后,继续和外界交流的必要性。很多人会认为:我的软件架构,设计结果和工程有关,为何要讲给工程师以外的人听?他们懂么?
其实pitch本身就是自我学习和自我修正的一部分。当着一个人或者几个人的面,在白板上画下脑海中的设计的那一刻,你就会有直觉哪个地方似乎有问题,这是很奇特的一种体验:你自己画给自己看并不会产生这种直觉。这大概是面对公众的焦灼产生的肾上腺素的效果。:)
此外,从听者的表情,或者他们提的听起来很傻很天真的问题,你会进一步知道哪些地方你以为你搞通了,其实自己是一知半解。太简单,太基础的问题,我们take it for granted,不屑去问自己,非要有人点出,自己才发现:啊,原来这里我也不懂哈。这就是破解 "you don’t know what you don’t know" 之法。
记得看过一个video,主讲人大谈企业文化,有个哥们傻乎乎发问:so what it culture literally? 主讲人愣了一下,拖拖拉拉讲了一堆自己都不能让自己信服的废话。估计回头他就去查韦氏词典了。
最后,总有人在某些领域的知识更丰富一些,他们会告诉你你一些你知道自己不懂的事情。填补了 "you know that you don’t know" 的空缺。
设计时的tradeoff
Rich hickey(clojure作者)在某个演讲中说:
everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.

所以,下回再腆着脸说:偶做了些tradeoff,先确保自己做足了功课再说。
设计的改变不可避免
设计不是一锤子买卖,改变不可避免。我之前的一个老板,喜欢把:change is your friend 挂在口头。软件开发的整个生命周期,变更是家常便饭,以至于变更管理都生出一门学问。软件的设计期更是如此。人总会犯错,设计总有缺陷,需求总会变化,老板总会指手画脚,PM总有一天会亮出獠牙,不再是贴心大哥,或者美萌小妹。。。所以,据理力争,然后接受必要的改变即可。连凯恩斯他老人家都说:

What do you do, sir?
(文章很长,感谢阅读至此,这文章3/30号开始写,拖拖拉拉算是凑合完成了)
如果您觉得这篇文章不错,请点赞。多谢!
欢迎订阅公众号『程序人生』(搜索微信号 programmer_life)。每篇文章都力求原汁原味,北京时间中午12点左右,美西时间下午8点左右与您相会。
1. Total Cost of Ownership
2. 要看具体的pub/sub系统的实现方式
原文链接:https://zhuanlan.zhihu.com/p/2 ... attle 查看全部

作者:陈天
链接:https://zhuanlan.zhihu.com/p/20005177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

disclaimer: 本文所讲的设计,非UI/UE的设计,单单指软件代码/功能本身在技术上的设计。UI/UE的主题请出门右转找特赞(Tezign)。:)
在如今这个Lean/Agile横扫一切的年代,设计似乎有了被边缘化的倾向,做事的周期如此之快,似乎已容不下人们更多的思考。MVP(Minimal Viable Produce)在很多团队里演化成一个形而上的图腾,于是工程师们找到了一个完美的借口:我先做个MVP,设计的事,以后再说。
如果纯属个人玩票,有个点子,hack out还说得过去;但要严肃做一个项目,还是要下工夫设计一番,否则,没完没了的返工会让你无语泪千行。
设计首先得搞懂要解决的问题
工程师大多都是很聪明的人,聪明人有个最大的问题就是自负。很多人拿到一个需求,还没太搞明白其外延和内涵,代码就已经在脑袋里流转。这样做出来的系统,纵使再精妙,也免不了承受因需求理解不明确而导致的返工之苦。
搞懂需求这事,说起来简单,做起来难。需求有正确的但表达错误的需求,有正确的但没表达出来的需求,还有过度表达的需求。所以,拿到需求后,先不忙寻找解决方案,多问问自己,工作伙伴,客户follow up questions来澄清需求模糊不清之处。
搞懂需求,还需要了解需求对应的产品,公司,以及(潜在)竞争对手的现状,需求的上下文,以及需求的约束条件。人有二知二不知:
I know that I know

I know that I don’t know

I don’t know that I know

I don’t know that I don’t know

澄清需求的过程,就是不断驱逐无知,掌握现状,上下文和约束条件的过程。
这个主题讲起来很大,且非常重要,但毕竟不是本文的重点,所以就此带过。
寻找(多个)解决方案
如果对问题已经有不错的把握,接下来就是解决方案的发现之旅。这是个考察big picture的活计。同样是满足孩子想要个汽车的愿望,你可以:
去玩具店里买一个现成的

买乐高积木,然后组装

用纸糊一个,或者找块木头,刻一个

这对应软件工程问题的几种解决之道:
购买现成软件(acuquire or licensing),二次开发之(如果需要)

寻找building blocks,组装之(glue)

自己开发(build from scratch, or DIY)

大部分时候,如果a或b的TCO [1] 合理,那就不要选择c。做一个产品的目的是为客户提供某种服务,而不是证明自己能一行行码出出来这个产品。
a是个很重要的点,可惜大部分工程师脑袋里没有钱的概念,或者出于job security的私心,而忽略了。工程师现在越来越贵,能用合理的价格搞定的功能,就不该雇人去打理(自己打脸)。一个产品,最核心的部分不超过整个系统的20%,把人力资源铺在核心的部分,才是软件设计之道。
b我们稍后再讲。
对工程师而言,DIY出一个功能是个极大的诱惑。一种DIY是源自工程师的不满。任何开源软件,在处理某种特定业务逻辑的时候总会有一些不足,眼里如果把这些不足放在,却忽略了人家的好处,是大大的不妥。前两天我听到有人说 "consul sucks, …​, I’ll build our own service discovery framework…​",我就苦笑。我相信他能做出来一个简单的service discovery tool,这不是件特别困难的事情。问题是值不值得去做。如果连处于consul这个层次的基础组件都要自己去做,那要么是心太大,要么是没有定义好自己的软件系统的核心价值(除非系统的核心价值就在于此)。代码一旦写出来,无论是5000行还是50行,都是需要有人去维护的,在系统的生命周期里,每一行自己写的代码都是一笔债务,需要定期不定期地偿还利息。
另外一种DIY是出于工程师的无知。「无知者无畏」在某些场合的效果是正向的,有利于打破陈规。但在软件开发上,还是知识和眼界越丰富越开阔越好。一个无知的工程师在面对某个问题时(比如说service discovery),如果不知道这问题也许有现成的解决方案(consul),自己铆足了劲写一个,大半会有失偏颇(比如说没做上游服务的health check,或者自己本身的high availability),结果bug不断,辛辛苦苦一个个都啃下来,才发现,自己走了很多弯路,费了大半天劲,做了某个开源软件的功能的子集。当然,对工程师而言,这个练手的价值还是很大的,但对公司来说,这是一笔沉重的无意义的支出。
眼界定义了一个人的高度,如果你每天见同类的人,看同质的书籍/视频,(读)写隶属同一domain的代码,那多半眼界不够开阔。互联网的发展一日千里,变化太快,如果把自己禁锢在一方小天地里,很容易成为陶渊明笔下的桃花源中人:乃不知有汉,无论魏晋。
构建灵活且有韧性的系统
如果说之前说的都是废话,那么接下来的和真正的软件设计能扯上些关系。
分解和组合
软件设计是一个把大的问题不断分解,直至原子级的小问题,然后再不断组合的过程。这一点可以类比生物学:原子(keyword/macro)组合成分子(function),分子组合成细胞(module/class),细胞组合成组织(micro service),组织组合成器官(service),进而组合成生物(system)。
一个如此组合而成系统,是满足关注点分离(Separation of Concerns)的。大到一个器官,小到一个细胞,都各司其职,把自己要做的事情做到极致。心脏不必关心肾脏会干什么,它只需要做好自己的事情:把新鲜血液通过动脉排出,再把各个器官用过的血液从静脉回收。
分解和组合在软件设计中的作用如此重要,以至于一个系统如果合理分解,那么日后维护的代价就要小得多。同样讲关注点分离,不同的工程师,分离的方式可能完全不同。但究其根本,还有有一些规律可循。
总线(System Bus)
首先我们要把系统的总线定义出来。人体的总线,大的有几条:血管(动脉,静脉),神经网络,气管,输尿管。它们有的完全负责与外界的交互(气管,输尿管),有的完全是内部的信息中枢(血管),有的内外兼修(神经网络)。
总线把生产者和消费者分离,让彼此互不依赖。心脏往外供血时,把血压入动脉血管就是了。它并不需要知道谁是接收者。
同样的,回到我们熟悉的计算机系统,CPU访问内存也是如此:它发送一条消息给总线,总线通知RAM读取数据,然后RAM把数据返回给总线,CPU再获取之。整个过程中CPU只知道一个内存地址,毋须知道访问的具体是哪个内存槽的哪块内存 —— 总线将二者屏蔽开。
学过计算机系统的同学应该都知道,经典的PC结构有几种总线:数据总线,地址总线,控制总线,扩展总线等;做过网络设备的同学也都知道,一个经典的网络设备,其软件系统的总线分为:control plane和data plane。
路由(routing)
有了总线的概念,接下来必然要有路由。我们看人体的血管:
每一处分叉,就涉及到一次路由。
路由分为外部路由和内部路由。外部路由处理输入,把不同的输入dispatch到系统里不同的组件。做web app的,可能没有意识到,但其实每个web framework,最关键的组件之一就是url dispatch。HTTP的伟大之处就是每个request,都能通过url被dispatch到不同的handler处理。而url是目录式的,可以层层演进 —— 就像分形几何,一个大的系统,通过不断重复的模式,组合起来 —— 非常利于系统的扩展。遗憾的是,我们自己做系统,对于输入既没有总线的考量,又无路由的概念,if-else下去,久而久之,代码便绕成了意大利面条。
再举一例:DOM中的event bubble,在javascript处理起来已然隐含着路由的概念。你只需定义当某个事件(如onclick)发生时的callback函数就好,至于这事件怎么通过eventloop抵达回调函数,无需关心。好的路由系统剥茧抽丝,把繁杂的信息流正确送到处理者手中。
外部路由总还有「底层」为我们完成,内部路由则需工程师考虑。service级别的路由(数据流由哪个service处理)可以用consul等service discovery组件,service内部的路由(数据流到达后怎么处理)则需要自己完成。路由的具体方式有很多种,pattern matching最为常见。
无论用何种方式路由,数据抵达总线前为其定义Identity(ID)非常重要,你可以管这个过程叫data normalization,data encapsulation等,总之,一个消息能被路由,需要有个用于路由的ID。这ID可以是url,可以是一个message header,也可以是一个label(想象MPLS的情况)。当我们为数据赋予一个个合理的ID后,如何路由便清晰可见。
队列(Queue)
对于那些并非需要立即处理的数据,可以使用队列。队列也有把生产者和消费者分离的功效。队列有:
single producer single consumer(SPSC)

single producer multiple consumers(SPMC)

multiple producers single consumer(MPSC)

multiple producers multiple consumers(MPMC)

仔细想想,队列其实就是总线+路由(可选)+存储的一个特殊版本。一般而言,system bus之上是系统的各个service,每个service再用service bus(或者queue)把micro service chain起来,然后每个micro service内部的组件间,再用queue连接起来。
有了队列,有利于提高流水线的效率。一般而言,流水线的处理速度取决于最慢的组件。队列的存在,让慢速组件有机会运行多份,来弥补生产者和消费者速度上的差距。
Pub/Sub
存储在队列中的数据,除路由外,还有一种处理方式:pub/sub。和路由相似,pub/sub将生产者和消费者分离;但二者不同之处在于,路由的目的地由路由表中的表项控制,而pub/sub一般由publisher控制 [2]:任何subscribe某个数据的consumer,都会到publisher处注册,publisher由此可以定向发送消息。
协议(protocol)
一旦我们把系统分解成一个个service,service再分解成micro service,彼此之间互不依赖,仅仅通过总线或者队列来通讯,那么,我们就需要协议来定义彼此的行为。协议听起来很高大上,其实不然。我们写下的每个function(或者每个class),其实就是在定义一个不成文的协议:function的arity是什么,接受什么参数,返回什么结果。调用者需严格按照协议调用方能得到正确的结果。
service级别的协议是一份SLA:服务的endpoint是什么,版本是什么,接收什么格式的消息,返回什么格式的消息,消息在何种网络协议上承载,需要什么样的authorization,可以正常服务的最大吞吐量(throughput)是什么,在什么情况下会触发throttling等等。
头脑中有了总线,路由,队列,协议等这些在computer science 101中介绍的基础概念,系统的分解便有迹可寻:面对一个系统的设计,你要做的不再是一道作文题,而是一道填空题:在若干条system bus里填上其名称和流进流出的数据,在system bus之上的一个个方框里填上服务的名称和服务的功能。然后,每个服务再以此类推,直到感觉毋须再细化为止。
组成系统的必要服务
有些管理性质的服务,尽管和业务逻辑直接关系不大,但无论是任何系统,都需要考虑构建,这里罗列一二。
代谢(sweeping)
一个活着的生物时时刻刻都进行着新陈代谢:每时每刻新的细胞取代老的细胞,同时身体中的「垃圾」通过排泄系统排出体外。一个运转有序的城市也有新陈代谢:下水道,垃圾场,污水处理等维持城市的正常功能。没有了代谢功能,生物会凋零,城市会荒芜。
软件系统也是如此。日志会把硬盘写满,软件会失常,硬件会失效,网络会拥塞等等。一个好的软件系统需要一个好的代谢系统:出现异常的服务会被关闭,同样的服务会被重新启动,恢复运行。
代谢系统可以参考erlang的supervisor/child process结构,以及supervision tree。很多软件,都运行在简单的supervision tree模式下,如nginx。
高可用性(HA)
每个人都有两个肾。为了apple watch卖掉一个肾,另一个还能保证人体的正常工作。当然,人的两个肾是Active-Active工作模式,内部的肾元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家这service的做的),少了一个,performance会一点点有折扣,但可以忽略不计。
大部分软件系统里的各种服务也需要高可用性:除非完全无状态的服务,且服务重启时间在ms级。服务的高可用性和路由是息息相关的:高可用性往往意味着同一服务的冗余,同时也意味着负载分担。好的路由系统(如consul)能够对路由至同一服务的数据在多个冗余服务间进行负载分担,同时在检测出某个失效服务后,将数据路只由至正常运作的服务。
高可用性还意味着非关键服务,即便不可恢复,也只会导致系统降级,而不会让整个系统无法访问。就像壁虎的尾巴断了不妨碍壁虎逃命,人摔伤了手臂还能吃饭一样,一个软件系统里统计模块的异常不该让用户无法访问他的个人页面。
安保(security)
安保服务分为主动安全和被动安全。authentication/authorization + TLS + 敏感信息加密 + 最小化输入输出接口可以算是主动安全,防火墙等安防系统则是被动安全。
继续拿你的肾来比拟 —— 肾脏起码有两大安全系统:
输入安全。肾器的厚厚的器官膜,保护器官的输入输出安全 —— 主要的输入输出只能是肾动脉,肾静脉和输尿管。

环境安全。肾器里有大量脂肪填充,避免在撞击时对核心功能的损伤。

除此之外,人体还提供了包括免疫系统,皮肤,骨骼,空腔等一系列安全系统,从各个维度最大程度保护一个器官的正常运作。如果我们仔细研究生物,就会发现,安保是个一揽子解决方案:小到细胞,大到整个人体,都有各自的安全措施。一个软件系统也需如此考虑系统中各个层次的安全。
透支保护(overdraft protection)
任何系统,任何服务都是有服务能力的 —— 当这能力被透支时,需要一定的应急计划。如果使用拥有auto scaling的云服务(如AWS),动态扩容是最好的解决之道,但受限于所用的解决方案,它并非万灵药,AWS的auto scaling依赖于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB对某些业务,如websocket,支持不佳;而第三方的load balancer,则需要考虑部署,与Amazon的auto scaling结合(需要写点代码),避免单点故障,保证自身的capacity等一堆头疼事。
在无法auto scaling的场景最通用的做法是back pressure,把压力反馈到源头。就好像你不断熬夜,最后大脑受不了,逼着你睡觉一样。还有一种做法是服务降级,停掉非核心的service/micro-service,如analytical service,ad service,保证核心功能正常。
把设计的成果讲给别人听
完成了分解和组合,也严肃对待了诸多与业务没有直接关系,但又不得不做的必要功能后,接下来就是要把设计在白板上画下来,讲给任何一个利益相关者听。听他们的反馈。设计不是一个闭门造车的过程,全程都需要和各种利益相关者交流。然而,很多人都忽视了设计定型后,继续和外界交流的必要性。很多人会认为:我的软件架构,设计结果和工程有关,为何要讲给工程师以外的人听?他们懂么?
其实pitch本身就是自我学习和自我修正的一部分。当着一个人或者几个人的面,在白板上画下脑海中的设计的那一刻,你就会有直觉哪个地方似乎有问题,这是很奇特的一种体验:你自己画给自己看并不会产生这种直觉。这大概是面对公众的焦灼产生的肾上腺素的效果。:)
此外,从听者的表情,或者他们提的听起来很傻很天真的问题,你会进一步知道哪些地方你以为你搞通了,其实自己是一知半解。太简单,太基础的问题,我们take it for granted,不屑去问自己,非要有人点出,自己才发现:啊,原来这里我也不懂哈。这就是破解 "you don’t know what you don’t know" 之法。
记得看过一个video,主讲人大谈企业文化,有个哥们傻乎乎发问:so what it culture literally? 主讲人愣了一下,拖拖拉拉讲了一堆自己都不能让自己信服的废话。估计回头他就去查韦氏词典了。
最后,总有人在某些领域的知识更丰富一些,他们会告诉你你一些你知道自己不懂的事情。填补了 "you know that you don’t know" 的空缺。
设计时的tradeoff
Rich hickey(clojure作者)在某个演讲中说:
everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.

所以,下回再腆着脸说:偶做了些tradeoff,先确保自己做足了功课再说。
设计的改变不可避免
设计不是一锤子买卖,改变不可避免。我之前的一个老板,喜欢把:change is your friend 挂在口头。软件开发的整个生命周期,变更是家常便饭,以至于变更管理都生出一门学问。软件的设计期更是如此。人总会犯错,设计总有缺陷,需求总会变化,老板总会指手画脚,PM总有一天会亮出獠牙,不再是贴心大哥,或者美萌小妹。。。所以,据理力争,然后接受必要的改变即可。连凯恩斯他老人家都说:

What do you do, sir?
(文章很长,感谢阅读至此,这文章3/30号开始写,拖拖拉拉算是凑合完成了)
如果您觉得这篇文章不错,请点赞。多谢!
欢迎订阅公众号『程序人生』(搜索微信号 programmer_life)。每篇文章都力求原汁原味,北京时间中午12点左右,美西时间下午8点左右与您相会。
1. Total Cost of Ownership
2. 要看具体的pub/sub系统的实现方式
原文链接:https://zhuanlan.zhihu.com/p/2 ... attle

服务化体系之-限流

微信admin 发表了文章 • 0 个评论 • 184 次浏览 • 2017-01-17 10:53 • 来自相关话题

原文链接:http://calvin1978.blogcn.com/articles/ratelimiter.html
(上)设计篇
在实现算法之前,先临时客串一下产品经理,尝试用最少的字,把“限流”这简单二字所展开的种种需求给说清楚。
1.各种目的
1. 保护每个服务节点。
2. 保护服务集群背后的资源,比如数据库。
3. 避免单个调用者过度使用服务,影响其他调用者。

2. 各种设定维度
2.1. 节点级别 vs 集群级别
如果以保护每个服务节点为目的,可以简单的在本地做节点级别的限流。
但如果以保护服务集群背后的资源为目的,就需要做集群级别的限流。
集群级别的一个做法是使用Redis, Memcached之类做一个集群级别的计数器。但额外多一次访问Redis的消耗,代价有点大。
而另一个做法是把集群限流总数分摊到每个节点上,但一是流量不是100%均匀时会不够精准,二是如果使用Docker动态缩扩容,需要动态更新这个分摊数。

2.2 客户端 vs 服务端
当以保护服务端的节点为目的,应在服务端设定,因为有多少调用者是未知的。
当以避免单个调用者过度使用服务为目的,可以针对客户端节点或客户端集群设定限流。此时限流可以在客户端实现,节约了网络往返;也可以在服务端实现,让所有限流逻辑集中于一处发生。

2.3 服务级别 vs 方法级别
可以对消耗特别大的方法专门配置,比如复杂的查询,昂贵的写操作。
然后其他方法使用统一的值,或者配一个所有方法加起来的总和。

3. 各种触发条件
触发条件的设定,难点在于服务的容量,受着本服务节点的能力,背后的资源的能力,下游服务的响应的多重约束。

3.1 静态配置固定值
当然,这个固定值可以被动态更新。

3.2 根据预设规则触发
规则的条件可以是服务平均时延,可以是背后数据库的CPU情况等。
比如平时不限流,当服务时延大于100ms,则触发限流500 QPS。
还可以是多级条件,比如>100ms 限流500 QPS, >200ms 限流200 QPS。
条件反馈由监控系统配合服务治理中心来完成,但有些指标如服务延时也可以由服务节点自行计算,如服务延时。

3.3 全动态自动增减调控
这个诱人的想法,永远存在于老板的心里。

4. 各种处理
4.1 立刻返回拒绝错误
由客户端进行降级处理。

4.2 进行短暂的等待
短暂等待,期待有容量空余,直到超时,依然是客户端降级。

4.3 触发服务降级,调用服务端的降级方法
服务端的降级方法,走服务端的简单路径与预设值,则代表了服务端这边的态度和逻辑。
客户端降级与服务端降级各有适用的场景,等下一篇《服务降级》 再详述。

(下)实现篇
开涛的《聊聊高并发系统之限流特技》 已讲得非常好,这里再简单补充两句。
另一篇《接口限流算法总结》 也非常好非常详细,差别只在于对令牌桶的突发量的描述上,我有我自己的理解。

1. 并发控制
并发控制本身就是一种最简单的限流,包括:
框架本身的连接/线程限制
各种数据库连接池,Http连接池
服务/方法级别的信号量计数器限制

2. 窗口流量控制
窗口流量控制有几种做法。
一种最简单的计数器,维护一个单位时间内的Counter,如判断单位时间已经过去,则将Counter重置零。开涛文章里利用guava cache也是一种做法。
但此做法有时被认为粒度太粗,没有把QPS平摊到一秒的各个毫秒里,同时也没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,将目光移动一下,就看到在两毫秒内发生了两倍的TPS。
于是,另一个改进版的做法是把单位时间拆分,比如把1秒分成5个200毫秒宽的桶,然后以滑动窗口来计算限流,好像就能很大程度上解决上面的两个问题了。。。。滑动窗口的算法,统一到《熔断篇》再来讨论。
最后一组就是漏桶算法或令牌桶算法了,下面会详述。
另外,集群规模的计数器,基于Memcachd或Redis的实现,也见开涛的博客。

3. 令牌桶算法(Token Bucket )
随着时间流逝,系统会按速率 1/rate 的时间间隔(如果rate=100,则间隔是10ms)往桶里加入Token
如果桶满了(burst),则丢弃新加入的令牌
每个请求进来,都要消耗一个Token,如果桶空了,则丢弃请求或等待有新的令牌放入。





4. 漏桶算法(Leaky Bucket )
简单的想象有一个木桶,有新请求就是不断的倒水进来,然后桶底下有个洞,按照固定的速率把水漏走,如果水进来的速度比漏走的快,桶可能就会满了,然后就拒绝请求。
可见,两个算法的基本描述上,只是方向不一样,其他没什么不同的。所以WikiMedia里说, 两个算法实现可以一样,对于相同的参数得到的限流效果是一样的。

5. Guava版的令牌桶实现 -- RateLimiter
Guava已实现了一个性能非常好的RateLimiter,基本不需要我们再费心实现。但其中有一些实现的细节,需要我们留意:
1. 支持桶外预借的突发
突发,原本是指如果单位时间的前半段流量较少,桶里会积累一些令牌,然后支持来一波大的瞬时流量,将前面积累的令牌消耗掉。
但在RateLimiter的实现里,还多了个桶外预借(我自己给他的命名),就是即使桶里没有多少令牌,你也可以消耗一波大的,然后桶里面在时间段内都没有新令牌。比如桶的容量是5,桶里面现在只有1个令牌,如果你要拿5个令牌,也可以,清了桶里的一个令牌,再预借4个。然后再过800毫秒,桶里才会出现新令牌。
可见,Guava版的RateLimiter对突发的支持,比原版的两种算法都要大,你几乎随时都可以一次过消费burst个令牌,不管现在桶里有没有积累的令牌。
不过有个副作用,就是如果前面都没什么流量,桶里累积了5个令牌,则你其实可以一次过消费10个令牌。。。不过那么一下,超借完接下来还是固定速率的,直到还清了旧账,才可能再来那么一下。

2. 支持等待可用令牌与立刻返回两种接口

3. 单位时段是秒,这有点不太好用,不支持设定5分钟的单位。

4. 发令牌的计算粒度是MicroSeconds,也就是最多支持一百万的QPS。

6. 唯品会自家框架的实现

6.1 各种设定维度
1. 在每个服务节点上进行限流统计。
如果要保护后端的数据库资源,需要自己分摊一下集群总流量到每台服务器上(没有全docker化自动伸缩前,根据当前节点数量自动调整阀值的意义不大)

2. 支持不同的调用者集群(比如购物车服务与订单服务)不同的阀值,当然也支持剔除特定调用者后的总阀值。

3. 在方法级别上统计限流,可以对消耗特别大的特定方法设置另外的阀值,当然其他方法就用一个统一的值。
暂不支持按服务中所有方法的总流量来限流,因为除非所有方法的消耗很接近,否则意义不大。

6.2 各种触发条件
目前只支持静态配置的触发条件。
未来的服务治理中心,要从各种监控系统里采集数据,然后根据多级条件动态调整规则。又或者服务节点上先找些自己也能统计的指标如服务延时,先动态起来,就是有重复计算之嫌。

6.3 各种处理
目前只支持直接返回限流异常,由客户端来进行降级逻辑。
未来要支持调用在服务端的降级方法,由服务端提供降级逻辑的短路径。

6.4 计数器实现
限流只在服务端进行。不重新发明轮子,直接就是Guava的RateLimiter。
对于客户端的并发数限制,未来也可以做一下。 查看全部
原文链接:http://calvin1978.blogcn.com/articles/ratelimiter.html
(上)设计篇
在实现算法之前,先临时客串一下产品经理,尝试用最少的字,把“限流”这简单二字所展开的种种需求给说清楚。
1.各种目的
1. 保护每个服务节点。
2. 保护服务集群背后的资源,比如数据库。
3. 避免单个调用者过度使用服务,影响其他调用者。

2. 各种设定维度
2.1. 节点级别 vs 集群级别
如果以保护每个服务节点为目的,可以简单的在本地做节点级别的限流。
但如果以保护服务集群背后的资源为目的,就需要做集群级别的限流。
集群级别的一个做法是使用Redis, Memcached之类做一个集群级别的计数器。但额外多一次访问Redis的消耗,代价有点大。
而另一个做法是把集群限流总数分摊到每个节点上,但一是流量不是100%均匀时会不够精准,二是如果使用Docker动态缩扩容,需要动态更新这个分摊数。

2.2 客户端 vs 服务端
当以保护服务端的节点为目的,应在服务端设定,因为有多少调用者是未知的。
当以避免单个调用者过度使用服务为目的,可以针对客户端节点或客户端集群设定限流。此时限流可以在客户端实现,节约了网络往返;也可以在服务端实现,让所有限流逻辑集中于一处发生。

2.3 服务级别 vs 方法级别
可以对消耗特别大的方法专门配置,比如复杂的查询,昂贵的写操作。
然后其他方法使用统一的值,或者配一个所有方法加起来的总和。

3. 各种触发条件
触发条件的设定,难点在于服务的容量,受着本服务节点的能力,背后的资源的能力,下游服务的响应的多重约束。

3.1 静态配置固定值
当然,这个固定值可以被动态更新。

3.2 根据预设规则触发
规则的条件可以是服务平均时延,可以是背后数据库的CPU情况等。
比如平时不限流,当服务时延大于100ms,则触发限流500 QPS。
还可以是多级条件,比如>100ms 限流500 QPS, >200ms 限流200 QPS。
条件反馈由监控系统配合服务治理中心来完成,但有些指标如服务延时也可以由服务节点自行计算,如服务延时。

3.3 全动态自动增减调控
这个诱人的想法,永远存在于老板的心里。

4. 各种处理
4.1 立刻返回拒绝错误
由客户端进行降级处理。

4.2 进行短暂的等待
短暂等待,期待有容量空余,直到超时,依然是客户端降级。

4.3 触发服务降级,调用服务端的降级方法
服务端的降级方法,走服务端的简单路径与预设值,则代表了服务端这边的态度和逻辑。
客户端降级与服务端降级各有适用的场景,等下一篇《服务降级》 再详述。

(下)实现篇
开涛的《聊聊高并发系统之限流特技》 已讲得非常好,这里再简单补充两句。
另一篇《接口限流算法总结》 也非常好非常详细,差别只在于对令牌桶的突发量的描述上,我有我自己的理解。

1. 并发控制
并发控制本身就是一种最简单的限流,包括:
框架本身的连接/线程限制
各种数据库连接池,Http连接池
服务/方法级别的信号量计数器限制

2. 窗口流量控制
窗口流量控制有几种做法。
一种最简单的计数器,维护一个单位时间内的Counter,如判断单位时间已经过去,则将Counter重置零。开涛文章里利用guava cache也是一种做法。
但此做法有时被认为粒度太粗,没有把QPS平摊到一秒的各个毫秒里,同时也没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,将目光移动一下,就看到在两毫秒内发生了两倍的TPS。
于是,另一个改进版的做法是把单位时间拆分,比如把1秒分成5个200毫秒宽的桶,然后以滑动窗口来计算限流,好像就能很大程度上解决上面的两个问题了。。。。滑动窗口的算法,统一到《熔断篇》再来讨论。
最后一组就是漏桶算法或令牌桶算法了,下面会详述。
另外,集群规模的计数器,基于Memcachd或Redis的实现,也见开涛的博客。

3. 令牌桶算法(Token Bucket )
随着时间流逝,系统会按速率 1/rate 的时间间隔(如果rate=100,则间隔是10ms)往桶里加入Token
如果桶满了(burst),则丢弃新加入的令牌
每个请求进来,都要消耗一个Token,如果桶空了,则丢弃请求或等待有新的令牌放入。
CpAZrVfEQxkAAAAAAAEpWCD_84Y055.png


4. 漏桶算法(Leaky Bucket )
简单的想象有一个木桶,有新请求就是不断的倒水进来,然后桶底下有个洞,按照固定的速率把水漏走,如果水进来的速度比漏走的快,桶可能就会满了,然后就拒绝请求。
可见,两个算法的基本描述上,只是方向不一样,其他没什么不同的。所以WikiMedia里说, 两个算法实现可以一样,对于相同的参数得到的限流效果是一样的。

5. Guava版的令牌桶实现 -- RateLimiter
Guava已实现了一个性能非常好的RateLimiter,基本不需要我们再费心实现。但其中有一些实现的细节,需要我们留意:
1. 支持桶外预借的突发
突发,原本是指如果单位时间的前半段流量较少,桶里会积累一些令牌,然后支持来一波大的瞬时流量,将前面积累的令牌消耗掉。
但在RateLimiter的实现里,还多了个桶外预借(我自己给他的命名),就是即使桶里没有多少令牌,你也可以消耗一波大的,然后桶里面在时间段内都没有新令牌。比如桶的容量是5,桶里面现在只有1个令牌,如果你要拿5个令牌,也可以,清了桶里的一个令牌,再预借4个。然后再过800毫秒,桶里才会出现新令牌。
可见,Guava版的RateLimiter对突发的支持,比原版的两种算法都要大,你几乎随时都可以一次过消费burst个令牌,不管现在桶里有没有积累的令牌。
不过有个副作用,就是如果前面都没什么流量,桶里累积了5个令牌,则你其实可以一次过消费10个令牌。。。不过那么一下,超借完接下来还是固定速率的,直到还清了旧账,才可能再来那么一下。

2. 支持等待可用令牌与立刻返回两种接口

3. 单位时段是秒,这有点不太好用,不支持设定5分钟的单位。

4. 发令牌的计算粒度是MicroSeconds,也就是最多支持一百万的QPS。

6. 唯品会自家框架的实现

6.1 各种设定维度
1. 在每个服务节点上进行限流统计。
如果要保护后端的数据库资源,需要自己分摊一下集群总流量到每台服务器上(没有全docker化自动伸缩前,根据当前节点数量自动调整阀值的意义不大)

2. 支持不同的调用者集群(比如购物车服务与订单服务)不同的阀值,当然也支持剔除特定调用者后的总阀值。

3. 在方法级别上统计限流,可以对消耗特别大的特定方法设置另外的阀值,当然其他方法就用一个统一的值。
暂不支持按服务中所有方法的总流量来限流,因为除非所有方法的消耗很接近,否则意义不大。

6.2 各种触发条件
目前只支持静态配置的触发条件。
未来的服务治理中心,要从各种监控系统里采集数据,然后根据多级条件动态调整规则。又或者服务节点上先找些自己也能统计的指标如服务延时,先动态起来,就是有重复计算之嫌。

6.3 各种处理
目前只支持直接返回限流异常,由客户端来进行降级逻辑。
未来要支持调用在服务端的降级方法,由服务端提供降级逻辑的短路径。

6.4 计数器实现
限流只在服务端进行。不重新发明轮子,直接就是Guava的RateLimiter。
对于客户端的并发数限制,未来也可以做一下。

服务化体系-分布式Unique ID的生成方法一览

默认分类admin 发表了文章 • 0 个评论 • 136 次浏览 • 2017-01-17 10:01 • 来自相关话题

开一个新的主题,聊聊服务化框架的实现。我个人不是很喜欢“微服务”这个词,所以还是叫服务化。
分布式的Unique ID的用途如此广泛,从业务对象Id到服务化体系里分布式调用链的TraceId,本文总结了林林总总的各种生成算法。
对于UID的要求,一乐那篇《业务系统需要什么样的ID生成器》的提法很好: 唯一性,时间相关,粗略有序,可反解,可制造。
1. 发号器
我接触的最早的Unique ID,就是Oracle的Sequence。
特点是准连续的自增数字,为什么说是准连续?因为性能考虑,每个Client一次会领20个ID回去慢慢用,用完了再来拿。另一个Client过来,拿的就是另外20个ID了。
新浪微博里,Tim用Redis做相同的事情,Incr一下拿一批ID回去。如果有多个数据中心,那就拿高位的几个bit来区分。
只要舍得在总架构里增加额外Redis带来的复杂度,一个64bit的long就够表达了,而且不可能有重复ID。
批量是关键,否则每个ID都远程调用一次谁也吃不消。
2. UUID
2.1 概述
Universally Unique IDentifier(UUID),有着正儿八经的RFC4122规范 ,是一个128bit的数字,也可以表现为32个16进制的字符(每个字符0-F的字符代表4bit),中间用"-"分割。
时间戳+UUID版本号:分三段占16个字符(60bit+4bit),
Clock Sequence号与保留字段:占4个字符(13bit+3bit),
节点标识:占12个字符(48bit)
比如:f81d4fae-7dec-11d0-a765-00a0c91e6bf6
实际上,UUID一共有多种算法,能用于TraceId的是:
 version1: 基于时间的算法
version4: 基于随机数的算法
2.2 version 4 基于随机数的算法
先说Version4,这是最暴力的做法,也是JDK里的算法,不管原来各个位的含义了,除了少数几个位必须按规范来填,其余全部用随机数表达。
JDK里的实现,用 SecureRandom生成了16个随机的Byte,用2个long来存储。记得加-Djava.security.egd=file:/dev/./urandom,详见《SecureRandom的江湖偏方与真实效果》
2.3 version 1基于时间的算法
然后是Version1,严格守着原来各个位的规矩:
时间戳:因为时间戳有满满的64bit,所以可以尽情花,以100纳秒为1,从1582年10月15日算起(能撑3655年,真是位数多给烧的,1582年起头有意思么)
顺序号:这16bit则仅用于避免前面的节点标示改变(如网卡改了),时钟系统出问题(如重启后时钟快了慢了),让它随机一下避免重复。
节点标识:48bit,一般用MAC地址表达,如果有多块网卡就随便用一块。如果没网卡,就用随机数凑数,或者拿一堆尽量多的其他的信息,比如主机名什么的,拼在一起再hash一把。
但这里的顺序号并不是我们想象的自增顺序号,而是一个一次性的随机数,所以如果两个请求在同100个纳秒发生时。。。。。
还有这里的节点标识只在机器级别,没考虑过一台机器上起了两个进程。
所以严格的Version1没人实现,接着往下看各个变种吧。
2.4 version 1 vs version4
version4随机数的做法简单直接,但以唯一性,时间相关,粗略有序,可反解,可制造做要求的话,则不如version4,比如唯一性,因为只是两个随机long,并没有从理论上保证不会重复。
3. Version1变种 - Hibernate
Hibernate的CustomVersionOneStrategy.java,解决了之前version 1的两个问题
时间戳(6bytes, 48bit):毫秒级别的,从1970年算起,能撑8925年....
顺序号(2bytes, 16bit, 最大值65535): 没有时间戳过了一毫秒要归零的事,各搞各的,short溢出到了负数就归0。
机器标识(4bytes 32bit): 拿localHost的IP地址,IPV4呢正好4个byte,但如果是IPV6要16个bytes,就只拿前4个byte。
进程标识(4bytes 32bit): 用当前时间戳右移8位再取整数应付,不信两条线程会同时启动。
顺序号也不再是一次性的随机数而是自增序列了。
节点标识拆成机器和进程两个字段,增加了从时间戳那边改变精度节约下来的16bit。另外节点标识这64bit(1个Long)总是不变,节约了生成UID的时间。
4. Version1变种 - MongoDB
MongoDB java driver的ObjectId.java
时间戳(4 bytes 32bit): 是秒级别的,从1970年算起,能撑136年。
自增序列(3bytes 24bit, 最大值一千六百万): 是一个从随机数开始(机智)的Int不断加一,也没有时间戳过了一秒要归零的事,各搞各的。因为只有3bytes,所以一个4bytes的Int还要截一下后3bytes。
机器标识(3bytes 24bit): 将所有网卡的Mac地址拼在一起做个HashCode,同样一个int还要截一下后3bytes。搞不到网卡就用随机数混过去。
进程标识(2bytes 16bits):从JMX里搞回来到进程号,搞不到就用进程名的hash或者随机数混过去。
可见,MongoDB的每一个字段设计都比Hibernate的更合理一点,时间戳是秒级别的,自增序列变长了,进程标识变短了。总长度也降到了12 bytes 96bit。
但如果果用64bit长的Long来保存有点不上不下的,只能表达成byte数组或16进制字符串。
5. Twitter的snowflake派号器
snowflake也是一个派号器,基于Thrift的服务,不过不是用redis简单自增,而是类似UUID version1,
也是只有一个 64bit的long,所以IdWorker里紧巴巴的分配:
时间戳(42bit) 自从2012年以来(比那些从1970年算起的会过日子)的毫秒数,能撑139年。
自增序列(12bit,最大值4096), 毫秒之内的自增,过了一毫秒要重新置0
DataCenter ID (5 bit, 最大值32),配置值,派号器可能在多个机房。
Worker ID ( 5 bit, 最大值32),配置值,因为是派号器的id,所以一个数据中心里最多32个派号器就够了,还会在ZK里做下注册。
可见,因为是中央派号器,把至少40bit的节点标识都省出来了,换成10bit的派号器标识。所以整个UID能够只用一个Long表达。
另外,这种派号器,client每次只能一个ID,不能批量取,所以额外增加的延时是问题。
6.扩展阅读
《细聊分布式ID生成方法》
《生成全局唯一ID的3个思路,来自一个资深架构师的总结》
老规矩,链接请狠戳阅读原文。
7. 延伸问题,不用派号器,用一个Long如何搞定UUID??
这是我们服务化框架一开始的选择,TraceID设为了一个Long,而不是String,又不想用派号器的话,怎么办?
从UUID的128位压缩到Long的64位,又不用中央派号器而是本地生成,最难还是怎么来区分本地的机器+进程号。
思路一,压缩其他字段,留足够多的长度来做机器+进程号标识
时间戳是秒级别,1年要24位,两年要25位.....
自增序列,6万QPS要16位,10万要17位...
剩下20-24位,百万分之一到一千六百万分之一的重复率,然后把网卡Mac+进程号拼在一起再hash,取结果32个bit的后面20或24个bit。但假如这个标识字段重复了,后面时间戳和自增序列也很容易重复,不停的重复。
思路二,使用ZK 或 mysql 或 redis来自增管理机器+进程标识号
如果机器+进程标识字段只留了12位(4096),就要用ZK或etcd,线程启动时分配号码,当进程关闭了要回收这个号。
如果标示号的位数留得够多,比如有20位(一百万),那用redis或mysql来自增标识号最简单,每个进程启动时就去拿一个机器+服务进程的标示号。
思路三,继续Random
继续拼了,因为traceId的唯一性要求不高,偶然重复也没大问题,所以直接拿JDK UUID.randomUUID()的低位long(按UUID规范,高位的long被置了4个默认值的bit,低位只被设置3个bit),或者不用UUID.randomUUID()了,直接SecureRandom.nextLong(),不浪费了那3个bit。
你猜我们最后选了哪种? 查看全部
开一个新的主题,聊聊服务化框架的实现。我个人不是很喜欢“微服务”这个词,所以还是叫服务化。
分布式的Unique ID的用途如此广泛,从业务对象Id到服务化体系里分布式调用链的TraceId,本文总结了林林总总的各种生成算法。
对于UID的要求,一乐那篇《业务系统需要什么样的ID生成器》的提法很好: 唯一性,时间相关,粗略有序,可反解,可制造。
1. 发号器
我接触的最早的Unique ID,就是Oracle的Sequence。
特点是准连续的自增数字,为什么说是准连续?因为性能考虑,每个Client一次会领20个ID回去慢慢用,用完了再来拿。另一个Client过来,拿的就是另外20个ID了。
新浪微博里,Tim用Redis做相同的事情,Incr一下拿一批ID回去。如果有多个数据中心,那就拿高位的几个bit来区分。
只要舍得在总架构里增加额外Redis带来的复杂度,一个64bit的long就够表达了,而且不可能有重复ID。
批量是关键,否则每个ID都远程调用一次谁也吃不消。
2. UUID
2.1 概述
Universally Unique IDentifier(UUID),有着正儿八经的RFC4122规范 ,是一个128bit的数字,也可以表现为32个16进制的字符(每个字符0-F的字符代表4bit),中间用"-"分割。
时间戳+UUID版本号:分三段占16个字符(60bit+4bit),
Clock Sequence号与保留字段:占4个字符(13bit+3bit),
节点标识:占12个字符(48bit)
比如:f81d4fae-7dec-11d0-a765-00a0c91e6bf6
实际上,UUID一共有多种算法,能用于TraceId的是:
 version1: 基于时间的算法
version4: 基于随机数的算法
2.2 version 4 基于随机数的算法
先说Version4,这是最暴力的做法,也是JDK里的算法,不管原来各个位的含义了,除了少数几个位必须按规范来填,其余全部用随机数表达。
JDK里的实现,用 SecureRandom生成了16个随机的Byte,用2个long来存储。记得加-Djava.security.egd=file:/dev/./urandom,详见《SecureRandom的江湖偏方与真实效果》
2.3 version 1基于时间的算法
然后是Version1,严格守着原来各个位的规矩:
时间戳:因为时间戳有满满的64bit,所以可以尽情花,以100纳秒为1,从1582年10月15日算起(能撑3655年,真是位数多给烧的,1582年起头有意思么)
顺序号:这16bit则仅用于避免前面的节点标示改变(如网卡改了),时钟系统出问题(如重启后时钟快了慢了),让它随机一下避免重复。
节点标识:48bit,一般用MAC地址表达,如果有多块网卡就随便用一块。如果没网卡,就用随机数凑数,或者拿一堆尽量多的其他的信息,比如主机名什么的,拼在一起再hash一把。
但这里的顺序号并不是我们想象的自增顺序号,而是一个一次性的随机数,所以如果两个请求在同100个纳秒发生时。。。。。
还有这里的节点标识只在机器级别,没考虑过一台机器上起了两个进程。
所以严格的Version1没人实现,接着往下看各个变种吧。
2.4 version 1 vs version4
version4随机数的做法简单直接,但以唯一性,时间相关,粗略有序,可反解,可制造做要求的话,则不如version4,比如唯一性,因为只是两个随机long,并没有从理论上保证不会重复。
3. Version1变种 - Hibernate
Hibernate的CustomVersionOneStrategy.java,解决了之前version 1的两个问题
时间戳(6bytes, 48bit):毫秒级别的,从1970年算起,能撑8925年....
顺序号(2bytes, 16bit, 最大值65535): 没有时间戳过了一毫秒要归零的事,各搞各的,short溢出到了负数就归0。
机器标识(4bytes 32bit): 拿localHost的IP地址,IPV4呢正好4个byte,但如果是IPV6要16个bytes,就只拿前4个byte。
进程标识(4bytes 32bit): 用当前时间戳右移8位再取整数应付,不信两条线程会同时启动。
顺序号也不再是一次性的随机数而是自增序列了。
节点标识拆成机器和进程两个字段,增加了从时间戳那边改变精度节约下来的16bit。另外节点标识这64bit(1个Long)总是不变,节约了生成UID的时间。
4. Version1变种 - MongoDB
MongoDB java driver的ObjectId.java
时间戳(4 bytes 32bit): 是秒级别的,从1970年算起,能撑136年。
自增序列(3bytes 24bit, 最大值一千六百万): 是一个从随机数开始(机智)的Int不断加一,也没有时间戳过了一秒要归零的事,各搞各的。因为只有3bytes,所以一个4bytes的Int还要截一下后3bytes。
机器标识(3bytes 24bit): 将所有网卡的Mac地址拼在一起做个HashCode,同样一个int还要截一下后3bytes。搞不到网卡就用随机数混过去。
进程标识(2bytes 16bits):从JMX里搞回来到进程号,搞不到就用进程名的hash或者随机数混过去。
可见,MongoDB的每一个字段设计都比Hibernate的更合理一点,时间戳是秒级别的,自增序列变长了,进程标识变短了。总长度也降到了12 bytes 96bit。
但如果果用64bit长的Long来保存有点不上不下的,只能表达成byte数组或16进制字符串。
5. Twitter的snowflake派号器
snowflake也是一个派号器,基于Thrift的服务,不过不是用redis简单自增,而是类似UUID version1,
也是只有一个 64bit的long,所以IdWorker里紧巴巴的分配:
时间戳(42bit) 自从2012年以来(比那些从1970年算起的会过日子)的毫秒数,能撑139年。
自增序列(12bit,最大值4096), 毫秒之内的自增,过了一毫秒要重新置0
DataCenter ID (5 bit, 最大值32),配置值,派号器可能在多个机房。
Worker ID ( 5 bit, 最大值32),配置值,因为是派号器的id,所以一个数据中心里最多32个派号器就够了,还会在ZK里做下注册。
可见,因为是中央派号器,把至少40bit的节点标识都省出来了,换成10bit的派号器标识。所以整个UID能够只用一个Long表达。
另外,这种派号器,client每次只能一个ID,不能批量取,所以额外增加的延时是问题。
6.扩展阅读
《细聊分布式ID生成方法》
《生成全局唯一ID的3个思路,来自一个资深架构师的总结》
老规矩,链接请狠戳阅读原文。
7. 延伸问题,不用派号器,用一个Long如何搞定UUID??
这是我们服务化框架一开始的选择,TraceID设为了一个Long,而不是String,又不想用派号器的话,怎么办?
从UUID的128位压缩到Long的64位,又不用中央派号器而是本地生成,最难还是怎么来区分本地的机器+进程号。
思路一,压缩其他字段,留足够多的长度来做机器+进程号标识
时间戳是秒级别,1年要24位,两年要25位.....
自增序列,6万QPS要16位,10万要17位...
剩下20-24位,百万分之一到一千六百万分之一的重复率,然后把网卡Mac+进程号拼在一起再hash,取结果32个bit的后面20或24个bit。但假如这个标识字段重复了,后面时间戳和自增序列也很容易重复,不停的重复。
思路二,使用ZK 或 mysql 或 redis来自增管理机器+进程标识号
如果机器+进程标识字段只留了12位(4096),就要用ZK或etcd,线程启动时分配号码,当进程关闭了要回收这个号。
如果标示号的位数留得够多,比如有20位(一百万),那用redis或mysql来自增标识号最简单,每个进程启动时就去拿一个机器+服务进程的标示号。
思路三,继续Random
继续拼了,因为traceId的唯一性要求不高,偶然重复也没大问题,所以直接拿JDK UUID.randomUUID()的低位long(按UUID规范,高位的long被置了4个默认值的bit,低位只被设置3个bit),或者不用UUID.randomUUID()了,直接SecureRandom.nextLong(),不浪费了那3个bit。
你猜我们最后选了哪种?

服务化体系之-兼容性与版本号

默认分类admin 发表了文章 • 0 个评论 • 127 次浏览 • 2017-01-17 09:56 • 来自相关话题

家大业大之后,服务的版本和兼容性就是个让人不得不正视的问题。
最近,路上有个说法,既然都是微服务了,那不同的版本可以认为是两个完全不一样的微服务,没必要再保留版本号了。
这篇文章按着唯品会的实战经历来探讨一下。
我们面对的现状是:服务每周每天都在升级,有些升级是业务逻辑升级,接口不变;有些是接口变了,但是兼容的,偶然还有一些是不兼容的。
家大业大的体系永远不可能在服务端接口升级的时候,同时(比如同一个深夜,同一个小时甚至同一分钟)将所有调用者的客户端也升级的,相反所有客户端的升级可能是很长一段时间的事情,而且可能客户端还没全升完,服务端的接口又变了。
1. 兼容性原则
先搞些铺垫,我们一般认为下面的情况是兼容的:
增加新方法
增加可选的参数
修改参数为可选
删除参数
框架支持的话,参数的名称、顺序也可以改(如Thrift)
参数是对象的话,其属性的变更参见2-5条。
然后一般认为下面的情况是不兼容的:
业务上的不兼容
修改方法的名称
删除方法
增加必填的参数
修改参数为必填
修改参数和返回值的类型
参数是对象的话,属性的变更参照3-5条。
2. 不兼容时服务如何升级?
服务要进行不兼容的升级时,前面说了客户端不可能同时升级的,那怎么办?
一种是不修改原来的接口,直接在同一个服务里增加新的方法来解决。这种应该是最简单的做法,少量接口变动时优先使用,就是有点dirty。
一种是同时运行新旧两个服务。这种方式干净,但麻烦,要注意:
要保证旧版的客户端SDK,调用不能被路由到新服务上。
如果单独部署两个服务而不是在同一个应用里,随着客户端不断割接过来,控制好新旧两个的集群容量。
3. 版本号原则
服务的版本号,和软件的版本号一样,一般整成三位:
第一位:不兼容的大版本,   如1.0    vs  2.0
第二位:兼容的新功能版本,如1.1    vs 1.2
第三位:兼容的BugFix版本,如1.1.0 vs 1.1.1
如果拿着低版本的SDK(如1.0.0) 发起请求,会被服务化框架路由到所有的兼容版本上(如1.1.1,1.2.0),但不会到不兼容的版本上(如2.0.1)。
4. 最终问题,到底要不要版本号?
4.1 版本号用于标示SDK版本是有益的
即使是兼容的小版本。
版本号能让服务端与客户端两头的开发人员更好的对话。毕竟所谓兼容,有时候也有着某种代价与折衷,互相明确彼此的版本会更好。
如果服务治理中心,能让所有服务提供者一目了然各个调用者的版本会更好,起码方便催人升级呀。
同时,在中央服务文档中心,有了版本号后也能为服务接口每一个小版本保留一份文档。
4.2 不兼容的版本怎么办?
像我们这种命名渣,好不容易为服务搞了个贴切的名字,再想第二个其实很不容易,一般只能在函数名,服务名里直接带上数字,如GoodsService2,或者GoodsServiceNew, GoodsNewService.....
因此,如果加新方法的简单方式撑不住了,要独立新旧两个服务时,还是继续使用版本号来区分吧。当然,引入了版本号在框架里也引入了一定的复杂度,见后。
4.3 简化基于版本的配置
我们家框架之前的设计,将配置的粒度支持到每个小版本上,比如1.0版本的超时是10ms,1.1版本是20ms。然后根据客户端的版本来就近选择。回头来看,白白增加复杂度,还容易有bug....
其实兼容的小版本只有一份配置就好,明年得简化。
但不兼容的大版本还是要两份,比如自定义的路由。不兼容的版本本质上真的就是两个微服务了。
4.4 最终结论
建议还是有版本, 服务化框架里的相关逻辑和路由时,只取第一位的主版本号。第二、三位则用于SDK版本的管理沟通,以及中央服务文档中心。
当然,以上只是针对唯品会这种体量的公司的一家之言,并非四海皆准。 查看全部
家大业大之后,服务的版本和兼容性就是个让人不得不正视的问题。
最近,路上有个说法,既然都是微服务了,那不同的版本可以认为是两个完全不一样的微服务,没必要再保留版本号了。
这篇文章按着唯品会的实战经历来探讨一下。
我们面对的现状是:服务每周每天都在升级,有些升级是业务逻辑升级,接口不变;有些是接口变了,但是兼容的,偶然还有一些是不兼容的。
家大业大的体系永远不可能在服务端接口升级的时候,同时(比如同一个深夜,同一个小时甚至同一分钟)将所有调用者的客户端也升级的,相反所有客户端的升级可能是很长一段时间的事情,而且可能客户端还没全升完,服务端的接口又变了。
1. 兼容性原则
先搞些铺垫,我们一般认为下面的情况是兼容的:
增加新方法
增加可选的参数
修改参数为可选
删除参数
框架支持的话,参数的名称、顺序也可以改(如Thrift)
参数是对象的话,其属性的变更参见2-5条。
然后一般认为下面的情况是不兼容的:
业务上的不兼容
修改方法的名称
删除方法
增加必填的参数
修改参数为必填
修改参数和返回值的类型
参数是对象的话,属性的变更参照3-5条。
2. 不兼容时服务如何升级?
服务要进行不兼容的升级时,前面说了客户端不可能同时升级的,那怎么办?
一种是不修改原来的接口,直接在同一个服务里增加新的方法来解决。这种应该是最简单的做法,少量接口变动时优先使用,就是有点dirty。
一种是同时运行新旧两个服务。这种方式干净,但麻烦,要注意:
要保证旧版的客户端SDK,调用不能被路由到新服务上。
如果单独部署两个服务而不是在同一个应用里,随着客户端不断割接过来,控制好新旧两个的集群容量。
3. 版本号原则
服务的版本号,和软件的版本号一样,一般整成三位:
第一位:不兼容的大版本,   如1.0    vs  2.0
第二位:兼容的新功能版本,如1.1    vs 1.2
第三位:兼容的BugFix版本,如1.1.0 vs 1.1.1
如果拿着低版本的SDK(如1.0.0) 发起请求,会被服务化框架路由到所有的兼容版本上(如1.1.1,1.2.0),但不会到不兼容的版本上(如2.0.1)。
4. 最终问题,到底要不要版本号?
4.1 版本号用于标示SDK版本是有益的
即使是兼容的小版本。
版本号能让服务端与客户端两头的开发人员更好的对话。毕竟所谓兼容,有时候也有着某种代价与折衷,互相明确彼此的版本会更好。
如果服务治理中心,能让所有服务提供者一目了然各个调用者的版本会更好,起码方便催人升级呀。
同时,在中央服务文档中心,有了版本号后也能为服务接口每一个小版本保留一份文档。
4.2 不兼容的版本怎么办?
像我们这种命名渣,好不容易为服务搞了个贴切的名字,再想第二个其实很不容易,一般只能在函数名,服务名里直接带上数字,如GoodsService2,或者GoodsServiceNew, GoodsNewService.....
因此,如果加新方法的简单方式撑不住了,要独立新旧两个服务时,还是继续使用版本号来区分吧。当然,引入了版本号在框架里也引入了一定的复杂度,见后。
4.3 简化基于版本的配置
我们家框架之前的设计,将配置的粒度支持到每个小版本上,比如1.0版本的超时是10ms,1.1版本是20ms。然后根据客户端的版本来就近选择。回头来看,白白增加复杂度,还容易有bug....
其实兼容的小版本只有一份配置就好,明年得简化。
但不兼容的大版本还是要两份,比如自定义的路由。不兼容的版本本质上真的就是两个微服务了。
4.4 最终结论
建议还是有版本, 服务化框架里的相关逻辑和路由时,只取第一位的主版本号。第二、三位则用于SDK版本的管理沟通,以及中央服务文档中心。
当然,以上只是针对唯品会这种体量的公司的一家之言,并非四海皆准。

服务化体系之-配置中心,在ZK或etcd之外

默认分类admin 发表了文章 • 0 个评论 • 153 次浏览 • 2017-01-17 09:52 • 来自相关话题

如果一说到配置中心,你面前那个人立刻兴奋的和你讲ZooKeeper,etcd,很可能,你又遇到个空想型的玩家了,或者他家的系统其实很小。
1. 配置中心该有的清秀模样
家大业大的系统,肯定是要有个配置中心,统一管理所有应用的配置,并发布到对应的客户端上。
1. 推送: 将更改实时、准实时地推送到所有客户端。
2. 版本化与回滚:知道谁,什么时候,改了什么。最重要是,可以快速的回滚.....回滚、重启、灰度,可称运维三宝,在一时三刻便要报个P1故障的时候,一键回滚的便捷性如此重要。
3. 灰度发布:将更改推送到某些客户端上。同是运维三宝,先灰度一下能有效的降低风险。另外也是AB Test的一种实现方式-10%机器配置成开关打开调用A系统,90%机器开关关闭走B系统。
4. 预案:先改好一些配置保存起来,但不下发。发生问题时,一键批量执行,如降级整条选购线所有服务的非关键功能,好过在兵荒马乱中颤抖着去修改。
5. 权限,审批,流程: 咳,咳。权限控制和Pair Check其实还是好的。
6. 同时支持 Web界面 与 Restful API接口。
7. 支持多语言,其中最头痛是支持php这种无状态的。
2. 客户端这边要怎么配衬呢?
1. 配合配置中心的实时/灰度推送,在参数变化时调用客户端自行实现的回调接口,不需要重启应用。
2. 支持环境变量,JVM启动参数,配置文件,配置中心等多种来源按优先级互相覆盖,并有接口暴露最后的参数选择。
3. 配置文件中支持多套profile,如开发,单元测试,集成测试,生产。
3. 现在可以来谈实现了
3.1 Netflix Archaius
被谈得最多的Netflix,其Archaius说白了只是个客户端,服务端是一点没有的。
基于Apache Commons Configuration Library的扩展,多层,实时/准实时的数据源包括了环境变量,配置文件,数据库,ZK,etcd等。但没有Spring多profile的支持。
3.2 Spring Cloud
其Config Server的实现相当奇特,基于git,没错,就是基于git,暴露成Restful API,所以也算是支持了版本化,但也仅此而已了,其他界面功能什么的都需要自己另外做。
在客户端倒是集成了Config Server的读取,Spring本身也有profile机制。
而实时推送,还要自己拉上另一个Spring Cloud的Bus项目,基于RabbitMQ或Kafka来搭建一套推送体系,自己写代码实现。另外看文章,刷新时也不是通过回调接口,而是整个Bean重新构造,这对于有状态的Bean是不合适的。
3.3 国内开源
360开源了一个QCon,基于ZK,特色是基于Agent模式(见后)的多语言支持。但服务端也没有界面,灰度、预案什么的,直接通过API操作ZK而已。
淘宝的Diamond,有点历史了。
携程开源的Applo,支持Java,其他语言通过Http支持,看起来功能比较完整,界面模样周正,也还在更新维护。
还有个人开源的disconf,根脚在百度,只支持Java+Spring,看起来也可以。
3.4 基于ZK或etcd, DIY吧
综上所述,最好的配置中心还是在各个互联网公司的基础架构部里,虽然改改改的过程可能很长,虽然现在都还不完美。
一般都会支持界面和API,用数据库来保存版本历史,预案,走流程(正儿八经的数据库表更方便查询,比如“你的待审批变更”)。最后下发到ZK或etcd这种有推送能力的存储里(服务注册本身也是用ZK或etcd,选型就一块了)。客户端都直接和ZK或etcd打交道。
灰度发布麻烦些,其中一种实现是同时发布一个可接收的IP列表,客户端监听到配置节点变化时,对比一下自己是否属于该列表。
PHP和其他ZK/etcd不支持的语言,只好自己在客户端的机器上起一个Agent来监听变化,再写到配置文件或Share Memory。
我司走的就是这个路数。
最后,到底是ZK 还是etcd? 有些岁数的配置中心都是ZK的,但如果新写的话,的确可以考虑下etcd。虽然我没玩过,但看着它基于GRPC而支持多种语言,还可以动态修改集群,下发也不容易丢消息,挺不错。
3.5 基于运维自动化的配置文件推送
这是另一种玩法,一样有数据库与界面或API来管理配置,下发时生成配置文件,基于各种运维自动化工具如Puppet,Ansible推送到每个客户端。而应用则定时重新读取这个外部的配置文件。

4. 服务化框架的配置
除了业务上的值与开关的配置,服务化框架本身也需要配置。
服务化框架的配置又分两类:
框架底层的配置,如IO线程数,业务线程数,连接数,心跳间隔,日志配置等等。
每个服务/方法的元数据,如超时,重试,限流,熔断,路由,负载均衡等。
框架级的配置一般采用比较固定的配置方式,如配置文件,-D启动参数等,略过不谈。
4.1 服务元数据配置的演变
服务的元数据配置,则有以下逐步提升的过程:
Day1, 根据配置产生作用的地方,服务端或客户端,各配各的。比如超时配在客户端的配置文件,限流配在服务端。
Day2, 考虑到有些客户端可能漏配或不懂要配什么,服务端也可以配置超时,并把配置上传到服务注册中心,作为客户端的默认值。很多开源出来的RPC框架,为了减少依赖也就只做到这一步了。
Day3,业务系统可能把其中一些配置,抽取到配置中心里面实现动态修改。
Day4,各个业务系统各搞各的服务配置,一个字,乱。那不如在配置中心里,搞一个专门针对服务的标准界面吧。有时候,这也叫做服务治理中心。
4.2 服务治理中心的模样
1. 以服务为维度,统一管理所有服务相关的配置,包括服务端的,所有客户端的配置。
同一个服务,不同的调用者,可能有不同超时要求,比如订单与商品详情两个服务,对库存服务的延时敏感度不一样。
甚至同一个调用者,比如订单服务,在查看订单和新增订单两个场景里,对库存服务的延时的敏感度会不同。
这么麻烦!!要不要还是回到每个客户端自己在配置文件或代码里设置超时算了?
我的建议,为了达致比较好的服务治理,还是应该把服务提供者与所有调用者的配置放在一起,而不是散乱在代码里。作为一个服务提供者,如果完全不了解各个客户端在代码里设了什么超时,其实挺心虚的。
另外也可以做更多后续的治理工作,如整条调用链的超时设置展现。
同时,服务端获悉客户端那边的配置,也能够优化自己的处理,比如服务逻辑跑完就已经达到客户端那边配置的超时,那就不要序列化结果并发送回去了。 如果没有中央配置,客户端就要每次把超时时间也传输过去。
2. 多条件配置
除了前面提到的调用者/调用场景的不一样,可能还有服务在不同机房不同配置,灰度不同机器不同配置,服务默认值与某个方法特定值的差异等。
4.3 一种参考实现
需求很复杂,实现就要尽量简化.....无论界面怎么做,最后存储的JSON格式简化如下:
[
  //”订单服务“调用 “获取购物车方法” 为1000 
   {"method":"getCart","sourceApp":"orderService","value":1000},
  //默认值60
   {"value":60}
]
按顺序的多个规则,先中先走。规则里条件包括method, sourceApp, tag,targetIp等,多个条件是AND的关系。
而在读取配置时,不可能每次都做这样的解析,那就以空间换时间,把每一个遇到的method, sourceApp, targetIp,tag的组合的结果缓存起来。

好像所有开源的SOA框架里,只有我司的框架设计了这种多条件的配置。
我会的基础框架,细节对外说的不多。 这次抖漏了不少,怎么也值两块五了吧,赏么:) 查看全部
如果一说到配置中心,你面前那个人立刻兴奋的和你讲ZooKeeper,etcd,很可能,你又遇到个空想型的玩家了,或者他家的系统其实很小。
1. 配置中心该有的清秀模样
家大业大的系统,肯定是要有个配置中心,统一管理所有应用的配置,并发布到对应的客户端上。
1. 推送: 将更改实时、准实时地推送到所有客户端。
2. 版本化与回滚:知道谁,什么时候,改了什么。最重要是,可以快速的回滚.....回滚、重启、灰度,可称运维三宝,在一时三刻便要报个P1故障的时候,一键回滚的便捷性如此重要。
3. 灰度发布:将更改推送到某些客户端上。同是运维三宝,先灰度一下能有效的降低风险。另外也是AB Test的一种实现方式-10%机器配置成开关打开调用A系统,90%机器开关关闭走B系统。
4. 预案:先改好一些配置保存起来,但不下发。发生问题时,一键批量执行,如降级整条选购线所有服务的非关键功能,好过在兵荒马乱中颤抖着去修改。
5. 权限,审批,流程: 咳,咳。权限控制和Pair Check其实还是好的。
6. 同时支持 Web界面 与 Restful API接口。
7. 支持多语言,其中最头痛是支持php这种无状态的。
2. 客户端这边要怎么配衬呢?
1. 配合配置中心的实时/灰度推送,在参数变化时调用客户端自行实现的回调接口,不需要重启应用。
2. 支持环境变量,JVM启动参数,配置文件,配置中心等多种来源按优先级互相覆盖,并有接口暴露最后的参数选择。
3. 配置文件中支持多套profile,如开发,单元测试,集成测试,生产。
3. 现在可以来谈实现了
3.1 Netflix Archaius
被谈得最多的Netflix,其Archaius说白了只是个客户端,服务端是一点没有的。
基于Apache Commons Configuration Library的扩展,多层,实时/准实时的数据源包括了环境变量,配置文件,数据库,ZK,etcd等。但没有Spring多profile的支持。
3.2 Spring Cloud
其Config Server的实现相当奇特,基于git,没错,就是基于git,暴露成Restful API,所以也算是支持了版本化,但也仅此而已了,其他界面功能什么的都需要自己另外做。
在客户端倒是集成了Config Server的读取,Spring本身也有profile机制。
而实时推送,还要自己拉上另一个Spring Cloud的Bus项目,基于RabbitMQ或Kafka来搭建一套推送体系,自己写代码实现。另外看文章,刷新时也不是通过回调接口,而是整个Bean重新构造,这对于有状态的Bean是不合适的。
3.3 国内开源
360开源了一个QCon,基于ZK,特色是基于Agent模式(见后)的多语言支持。但服务端也没有界面,灰度、预案什么的,直接通过API操作ZK而已。
淘宝的Diamond,有点历史了。
携程开源的Applo,支持Java,其他语言通过Http支持,看起来功能比较完整,界面模样周正,也还在更新维护。
还有个人开源的disconf,根脚在百度,只支持Java+Spring,看起来也可以。
3.4 基于ZK或etcd, DIY吧
综上所述,最好的配置中心还是在各个互联网公司的基础架构部里,虽然改改改的过程可能很长,虽然现在都还不完美。
一般都会支持界面和API,用数据库来保存版本历史,预案,走流程(正儿八经的数据库表更方便查询,比如“你的待审批变更”)。最后下发到ZK或etcd这种有推送能力的存储里(服务注册本身也是用ZK或etcd,选型就一块了)。客户端都直接和ZK或etcd打交道。
灰度发布麻烦些,其中一种实现是同时发布一个可接收的IP列表,客户端监听到配置节点变化时,对比一下自己是否属于该列表。
PHP和其他ZK/etcd不支持的语言,只好自己在客户端的机器上起一个Agent来监听变化,再写到配置文件或Share Memory。
我司走的就是这个路数。
最后,到底是ZK 还是etcd? 有些岁数的配置中心都是ZK的,但如果新写的话,的确可以考虑下etcd。虽然我没玩过,但看着它基于GRPC而支持多种语言,还可以动态修改集群,下发也不容易丢消息,挺不错。
3.5 基于运维自动化的配置文件推送
这是另一种玩法,一样有数据库与界面或API来管理配置,下发时生成配置文件,基于各种运维自动化工具如Puppet,Ansible推送到每个客户端。而应用则定时重新读取这个外部的配置文件。

4. 服务化框架的配置
除了业务上的值与开关的配置,服务化框架本身也需要配置。
服务化框架的配置又分两类:
框架底层的配置,如IO线程数,业务线程数,连接数,心跳间隔,日志配置等等。
每个服务/方法的元数据,如超时,重试,限流,熔断,路由,负载均衡等。
框架级的配置一般采用比较固定的配置方式,如配置文件,-D启动参数等,略过不谈。
4.1 服务元数据配置的演变
服务的元数据配置,则有以下逐步提升的过程:
Day1, 根据配置产生作用的地方,服务端或客户端,各配各的。比如超时配在客户端的配置文件,限流配在服务端。
Day2, 考虑到有些客户端可能漏配或不懂要配什么,服务端也可以配置超时,并把配置上传到服务注册中心,作为客户端的默认值。很多开源出来的RPC框架,为了减少依赖也就只做到这一步了。
Day3,业务系统可能把其中一些配置,抽取到配置中心里面实现动态修改。
Day4,各个业务系统各搞各的服务配置,一个字,乱。那不如在配置中心里,搞一个专门针对服务的标准界面吧。有时候,这也叫做服务治理中心。
4.2 服务治理中心的模样
1. 以服务为维度,统一管理所有服务相关的配置,包括服务端的,所有客户端的配置。
同一个服务,不同的调用者,可能有不同超时要求,比如订单与商品详情两个服务,对库存服务的延时敏感度不一样。
甚至同一个调用者,比如订单服务,在查看订单和新增订单两个场景里,对库存服务的延时的敏感度会不同。
这么麻烦!!要不要还是回到每个客户端自己在配置文件或代码里设置超时算了?
我的建议,为了达致比较好的服务治理,还是应该把服务提供者与所有调用者的配置放在一起,而不是散乱在代码里。作为一个服务提供者,如果完全不了解各个客户端在代码里设了什么超时,其实挺心虚的。
另外也可以做更多后续的治理工作,如整条调用链的超时设置展现。
同时,服务端获悉客户端那边的配置,也能够优化自己的处理,比如服务逻辑跑完就已经达到客户端那边配置的超时,那就不要序列化结果并发送回去了。 如果没有中央配置,客户端就要每次把超时时间也传输过去。
2. 多条件配置
除了前面提到的调用者/调用场景的不一样,可能还有服务在不同机房不同配置,灰度不同机器不同配置,服务默认值与某个方法特定值的差异等。
4.3 一种参考实现
需求很复杂,实现就要尽量简化.....无论界面怎么做,最后存储的JSON格式简化如下:
[
  //”订单服务“调用 “获取购物车方法” 为1000 
   {"method":"getCart","sourceApp":"orderService","value":1000},
  //默认值60
   {"value":60}
]
按顺序的多个规则,先中先走。规则里条件包括method, sourceApp, tag,targetIp等,多个条件是AND的关系。
而在读取配置时,不可能每次都做这样的解析,那就以空间换时间,把每一个遇到的method, sourceApp, targetIp,tag的组合的结果缓存起来。

好像所有开源的SOA框架里,只有我司的框架设计了这种多条件的配置。
我会的基础框架,细节对外说的不多。 这次抖漏了不少,怎么也值两块五了吧,赏么:)

服务化之-负载均衡与路由的设计

默认分类admin 发表了文章 • 0 个评论 • 215 次浏览 • 2017-01-17 09:46 • 来自相关话题

从这章开始,各个服务化框架之间要贴身肉搏的比较了。
以Java体系的,有被真实地大量使用的,能接触到源码的这三条作标准,选择了这些学习对象: Dubbo与Dubbox兄弟,新浪微博的Motan,美团点评的Pigeon,还有SpringCloud/Netflix家的一应物件。嗯,当然还有唯品会自家出品的OSP。
推家的Finagle,谷家的GRPC,还有Netty作者写的Armeria更偏重于RPC框架,这章先不出场。
1. 服务注册与发现
时代在进步,服务注册与发现简单讲讲就行。
1.1 几桩好处
与Apache,Nginx,HAProxy,LVS这些传统的Load Balancer对比,服务的自注册自发现+智能客户端有几桩好处:
一、绕过了中央的代理节点,少了一层网络节点。
二、避免了中央的代理节点自身的处理速度和网卡带宽的瓶颈,以及LB自身的高可用。
传统的LB节点,需要KeepAlived之类的方案来做一个主备。然后前面再放一个DNS轮询之类的方案来做扩容。考虑到DNS的TTL设置,一旦主从LB一起失效,还要等待比如10分钟的过期时间,或紧急绑定客户机的/etc/host。
三、往日的LB,更改所代理的集群实例还要再重启一把。不过现在好像不需要了,比如Docker里的Bamboo方案,就可以动态注册Docker容器到HAProxy中。这桩好处便大大削弱了,只剩这注册可以不假于外物。
四、智能客户端上可以做更多的服务治理逻辑,比往Nginx上挂Lua脚本强。
1.2 大致功能
框架启动时,应用等完成初始预热了,才注册迎客。
框架退出时,先把自己从注册中心下线,稍等所有客户端都收到这个通知,再完成手头剩下的请求了,才优雅的转身退出。
还应提供API,一来是运维摘流量的一种方式,二来应用程序自己,可以随时将某个实例临时下线,比如应用做一些缓存刷新的工作时,便不希望接客。
另外一件要紧事情,注册中心虽然也会与服务端进行定时心跳。但这毕竟走的是注册中心与服务端之间的特殊通道,而不像传统LB的Health Check URL那样直接与服务端口进行交互,所以两者效用不可等同,还得有别的健康检查措施。
注册中心的DashBoard?觉得可以融合在服务治理中心里,单独的DashBoard意义不大。
1.3 种种实现
服务注册中心可以有zk,etcd,consul,我自己只用过ZK,其他没什么发言权,但和配置中心一样,etcd看着不错,有些框架可以同时支持其中的二三者。
Netflix家也有个Eureka,目前的版本基于RESTFul的API, 所以推送能力比前几家弱,靠着默认定时30秒的刷新,Server间要数据同步,Client与Server要数据同步,文档里说最多两分钟才保证通知到所有客户端,与ZK们秒级的推送速度相差甚远。对于脑裂的情况,Eureka的态度是宁愿保留坏数据,不要丢失好数据,见仁见智。
2. 负载均衡
2.1 衡量的标准
2.1.1  算法一览
1.Round Robbin(轮询)
最古老的算法最可信的算法,缺点是有状态,必须在并发之下记住上一次到谁了。
2.Random(随机)
最简单的,无状态的算法。
3. Least Load(最小负载)
可以实现某种程度上的智能调节。具体的实现包括最少在途请求数(发出去了还没收到应答的请求),最少并发连接数,最小平均响应时间等等。
4. Hash (参数哈希)
根据参数进行哈希,一来有分区的效果(参见单元化架构,可改善数据库连接,缓存热度等),二来可以Stick Session(可本机进行防刷运算等)。
2.1.2  权重支持
传统LB都有权重设置, 机器(Docker)硬件配置的不一致需要权重,压测引流需要权重。
灰度发布也需要权重。假如这个应用只有3台服务器,灰度发布1台,万一有错,那就是33%的失败率了。如果信心不足,可以增加一个灰度批次,先把第一台的权重降到原来的1%。
还有,机器刚启动时,需要一点预热的时间(如Class Loading),如果一开始就把一堆请求压给它,可能会有批量的超时,所以最好刚启动时只放一些流量过去,等运行一段时间才递增到正常流量(Dubbo是10分钟,Pigeon大约30分钟)
另外调权重为0,也是一种摘流量的方式。
唯独Netflix家的Ribbon,完全不支持权重。
2.1.3  性能,静态与动态实例列表
每家的实现算法,对性能都有影响,尤其是如何支持权重。 像Ribbon这种完全不支持权重的就首先退场了。
传统的LB,实例列表是静态的,所以一致性哈希环,按权重分配的Random环,RoundRobbin环之类的都可以预先生成。
而像我们家,需要经过动态的路由规则,最后才知道可选的实例列表,那什么什么环做起来就不容易了。
后来看了Motan的实现,一言惊醒梦中人,原来只要使劲,也可以根据当前机器的IP,预先进行机房路由的计算,预先得到每个可选目标分组的环,然后再来侦听实例的变化(注册变化,路由变化,熔断变化)。
2.1.4 可预测性
随便设计的LB算法可能会引起莫名的意外,比如RoundRobbin 和 最少在途请求数,如何与权重结合一定要谨慎。我们之前就设计过看起来可以,但跑起来压力都到了一台机上的算法。
2.2 实现的比较
2.2.1  轮询
Dubbo首先检查权重是否相等,如果相等就退化为无权重设置。其实日常99%的时段可能都是无权重的,区分快慢路径,为99%的情况作优化,是设计算法时的重要思路。
如果有权重,则每次生成个环,比对上一次的状态,在每个节点上调用完自己的权重数后,再往下一个节点走。
这里就有个问题,如果客户把三台服务器的权重分别设为100,10000,100, 本来只想表达个1:100:1的权重关系,结果。。。。连续一万个请求都压在第二台机上。
Pigeon的算法做了改进,会把权重求一个公约数(不过每个请求算一次也是累),真正达到1:100:1的效果。
我们家OSP嘛,与Dubbo有着同样的问题,这打击还没恢复过来,所以暂时主打Random,不支持RoundRobbin了。
要重新支持的话,会融合Motan的静态列表与Pigeon的公约数,直接生成一个静态环,每个实例按公约后的权重数,在环上插入若干个点,然后不断遍历这个环就行。
2.2.2  随机
这里有块测试项目新鲜程度的试金石:有没有用无锁的JDK8 ThrealLocalRandom 或者其移植类。
结果好像只有我们家OSP用了。
Dubbo同样先判断一下权重是否一致。如果不一致,则要遍历所有节点得到总权重,然后以总权重为上限生成一个随机数,然后再遍历每个节点,击鼓传花的减去其权重,直到数字小于0,那就是它了。
Pigeon和OSP的算法,和Dubbo一模一样。
Motan则像前面设计的完美版RoundRobbin一样,预先生成一个环,每个实例按公约后的权重数来插入若干个点,然后随机选一个点即可。OSP以后也要见贤思齐。
2.2.3  最小负载
最小负载的表达,最直接还是最少在途请求数(如果是同步请求,退化为最小并发数)。最小平均响应时间什么的,受不同方法不同的参数什么的影响太多。
最小负载最好就不要扯上权重了,因为说不清。Dubbo的做法,如果有在途请求数相等的节点,则再来按权重随机那套,但好像复杂了。
Motan做得有意思,考虑到服务实例的列表可能有一百几十台,全部比较一次谁的在途请求数最少太浪费时间。所以它会在列表里随机取其中一段,10个实例之间比较一下即可。
2.2.4  参数哈希
一致性哈希的基本算法:
1. 每个服务实例在一个以整数最大值为范围的环上,各占据错落有致不连续的若干个点。每个实例的点的数量可以固定如160,也可以按着权重来。
2.  然后随机一个整数,看它落在环上靠近的点是属于哪个实例的,就选择哪个实例。
与哈希取模相比,如果一台服务器挂了,只会让本来属于这台服务器的数据去到其他服务器,而原本属于其他服务器的数据则不会变。而哈希取模,模7 和 模8,几乎所有数据的落位都变了。
如果服务实例有一百个,每次生成一万六千个点的环,成本还是蛮大的。所以Dubbo采用了预生成的方式,只要实例列表的SystemIdentityHashCode不变,就继续用下去。
刀刀说Dubbo有个Bug,用可变的FullUrl 而不是固定的Host来生成点,会起不到一致性哈希的效果,另外用md5而不是新潮的murmurhash,也是历史感的体现。
Motan的类名虽然叫一致性哈希,但实现上好像不是......
OSP因为成本关系,暂时是直接取模的,日后也要学习Motan静态化服务列表,按权重构造一致性哈希环。
最后,拿什么来哈希呢?Dubbo和Motan哈希了全部请求参数(Dubbo也可以配置只哈希第1-N个参数),考虑到这样哈希成本非常不可控,及Proxy(支持多语言所用,以后再谈)要避免做反序列化,OSP采用了让客户端自己往Header中放分区键的方式。
Ribbon和Pigeon不支持Hash算法。
3. 路由规则定制
路由规则的定制,可以演化出无数的用法,如分流与隔离(读写分离,将非重要调用者分流到小集群,为重要调用者保留机器等等),AB测试,黑白名单等等。
经常听到的各种热词,都能靠路由规则支持。能用来做什么,只受限于配置员的想象力。
3.1 Dubbo的DSL
Dubbo的DSL语言:
比如:为重要调用者保留机器
application != kylin => host != 172.22.3.95,172.22.3.96
比如读写分离:
method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96
条件包括applicaiton,method, host, port
判断包括 ==,与 !=, 
值可以是 ","分隔的列表,"*"代表的后缀匹配。
还可以用Java支持的脚本语言如JRuby,Groovy来自定义规则。
3.2 Motan的DSL
Motan的DSL简单很多,基本只支持两端的服务名作条件,再配上两端的IP地址,也能玩出不少花样了。

服务名的定义:
#匹配com.weibo下以User开头的不包括UserMapping的所有服务,或以Status开头的所有服务 (com.weibo.User* & !com.weibo.UserMapping) | com.weibo.Status*以 to 分隔的两端IP:* to 10.75.1.* 10.75.2.* to 10.73.1.* * to !10.75.1.1


3.3 唯品会OSP的JSON化界面定义

我们的框架,原本也专门用Scala来解析的DSL,得益于Scala的强大(在祥威武),只用很少语句就很简洁实现了。
但后来我们老板觉得,还是需要一种更规范化,结构化,容易扩展的表达方式,所以选择了JSON(其实就是把前面DSL语法解析后的Java对象JSON化表达出来),并且独立出“目标集群”的概念。

3.3.1  整体语法 

条件集是多个条件的“与”的组合。

按顺序匹配每个条件集,中了就去到它的目标服务集群。

如果都没符合的,则走到“默认集群”去。

规则集 与 目标集群 是多对多的关系。 一个规则集可以对应多个有顺序的目标集群,用于满足HA的需求,如果集群A中的所有服务实例均不可用,则按顺序访问集群B中的实例。

目标服务集群,可以一个个IP定义,可以子网掩码或范围定义。注意Docker化之后,IP都是动态的,要么用范围定义,要么用Label。权重什么的也在里面顺便标明了。

3.3.2  条件语法 

参数列表:

客户端的应用名称

客户端的IP地址

服务的版本号,方法名

由客户端主动放入到Header中的上下文属性(绕过了复杂的参数提取,改为客户端自己放入),从这里玩出万千花样。
条件判断则包括了!=,  ==,  正则匹配,逗号分隔的列表等,还有针对某些特定类型的,如针对IP的子网掩码匹配。

3.3.3  运维友好的配置界面 




4. 机房路由
4.1 距离优先路由

虽然有爱因斯坦相对论的加持,从广州跑到北京的机房,几十毫秒的延时还是有的,所以,最好完全按距离远近来决定路由。

优先调用同机房的服务。但如果本机房的该服务的所有实例都挂了,或者服务只部署在其他机房,就调用离得最近的那个机房。

做起来其实也简单,一般按服务器IP的前两位,就能区分出机房来。然后做个配置,把每个机房之间的距离粗略量化出来。至于根据网络状态的动态选路,暂时不需要这么高大上的东西。

好像只有美团点评的Pigeon和我们自己家的OSP实现了。

4.2 其他方式
另外几家,虽然没有优先路由,但机房仍然是个跨不过的概念。
Motan的实现,通过前面提到的DSL语法,还能手工做流量容灾切换。
Dubbo的实现里,注册中心是按机房部署的,每个服务可以注册到一个或多个注册中心。
Netflix的Robbin,则有一种SameAvaiableZoneRule的负载均衡方式。不过它把机房路由也当成一种负载均衡,因此选了它的话,就硬编码了使用Random做均衡。
5. 小结

最后,还应该有一种静态配置文件的方式(类似Motan的DSL),在开发联调时,绕过上面所有所有的路由和负载均衡,直达目标测试服务器。
一通比较下来,感觉自家的OSP虽然还有种种待改进的地方,但整体完成度还可以。

Netflix家的Ribbon,基本上没什么亮点,处处敬陪末席。

还有,开源真好,太多让人眼前一亮的代码。

版权声明:本文版权归  江南白衣@唯品会 所有。任何形式的转载,必须获得作者授权。
本文可能还会持续修改,请戳 “阅读原文” 获得最新版本。 查看全部
从这章开始,各个服务化框架之间要贴身肉搏的比较了。
以Java体系的,有被真实地大量使用的,能接触到源码的这三条作标准,选择了这些学习对象: Dubbo与Dubbox兄弟,新浪微博的Motan,美团点评的Pigeon,还有SpringCloud/Netflix家的一应物件。嗯,当然还有唯品会自家出品的OSP。
推家的Finagle,谷家的GRPC,还有Netty作者写的Armeria更偏重于RPC框架,这章先不出场。
1. 服务注册与发现
时代在进步,服务注册与发现简单讲讲就行。
1.1 几桩好处
与Apache,Nginx,HAProxy,LVS这些传统的Load Balancer对比,服务的自注册自发现+智能客户端有几桩好处:
一、绕过了中央的代理节点,少了一层网络节点。
二、避免了中央的代理节点自身的处理速度和网卡带宽的瓶颈,以及LB自身的高可用。
传统的LB节点,需要KeepAlived之类的方案来做一个主备。然后前面再放一个DNS轮询之类的方案来做扩容。考虑到DNS的TTL设置,一旦主从LB一起失效,还要等待比如10分钟的过期时间,或紧急绑定客户机的/etc/host。
三、往日的LB,更改所代理的集群实例还要再重启一把。不过现在好像不需要了,比如Docker里的Bamboo方案,就可以动态注册Docker容器到HAProxy中。这桩好处便大大削弱了,只剩这注册可以不假于外物。
四、智能客户端上可以做更多的服务治理逻辑,比往Nginx上挂Lua脚本强。
1.2 大致功能
框架启动时,应用等完成初始预热了,才注册迎客。
框架退出时,先把自己从注册中心下线,稍等所有客户端都收到这个通知,再完成手头剩下的请求了,才优雅的转身退出。
还应提供API,一来是运维摘流量的一种方式,二来应用程序自己,可以随时将某个实例临时下线,比如应用做一些缓存刷新的工作时,便不希望接客。
另外一件要紧事情,注册中心虽然也会与服务端进行定时心跳。但这毕竟走的是注册中心与服务端之间的特殊通道,而不像传统LB的Health Check URL那样直接与服务端口进行交互,所以两者效用不可等同,还得有别的健康检查措施。
注册中心的DashBoard?觉得可以融合在服务治理中心里,单独的DashBoard意义不大。
1.3 种种实现
服务注册中心可以有zk,etcd,consul,我自己只用过ZK,其他没什么发言权,但和配置中心一样,etcd看着不错,有些框架可以同时支持其中的二三者。
Netflix家也有个Eureka,目前的版本基于RESTFul的API, 所以推送能力比前几家弱,靠着默认定时30秒的刷新,Server间要数据同步,Client与Server要数据同步,文档里说最多两分钟才保证通知到所有客户端,与ZK们秒级的推送速度相差甚远。对于脑裂的情况,Eureka的态度是宁愿保留坏数据,不要丢失好数据,见仁见智。
2. 负载均衡
2.1 衡量的标准
2.1.1  算法一览
1.Round Robbin(轮询)
最古老的算法最可信的算法,缺点是有状态,必须在并发之下记住上一次到谁了。
2.Random(随机)
最简单的,无状态的算法。
3. Least Load(最小负载)
可以实现某种程度上的智能调节。具体的实现包括最少在途请求数(发出去了还没收到应答的请求),最少并发连接数,最小平均响应时间等等。
4. Hash (参数哈希)
根据参数进行哈希,一来有分区的效果(参见单元化架构,可改善数据库连接,缓存热度等),二来可以Stick Session(可本机进行防刷运算等)。
2.1.2  权重支持
传统LB都有权重设置, 机器(Docker)硬件配置的不一致需要权重,压测引流需要权重。
灰度发布也需要权重。假如这个应用只有3台服务器,灰度发布1台,万一有错,那就是33%的失败率了。如果信心不足,可以增加一个灰度批次,先把第一台的权重降到原来的1%。
还有,机器刚启动时,需要一点预热的时间(如Class Loading),如果一开始就把一堆请求压给它,可能会有批量的超时,所以最好刚启动时只放一些流量过去,等运行一段时间才递增到正常流量(Dubbo是10分钟,Pigeon大约30分钟)
另外调权重为0,也是一种摘流量的方式。
唯独Netflix家的Ribbon,完全不支持权重。
2.1.3  性能,静态与动态实例列表
每家的实现算法,对性能都有影响,尤其是如何支持权重。 像Ribbon这种完全不支持权重的就首先退场了。
传统的LB,实例列表是静态的,所以一致性哈希环,按权重分配的Random环,RoundRobbin环之类的都可以预先生成。
而像我们家,需要经过动态的路由规则,最后才知道可选的实例列表,那什么什么环做起来就不容易了。
后来看了Motan的实现,一言惊醒梦中人,原来只要使劲,也可以根据当前机器的IP,预先进行机房路由的计算,预先得到每个可选目标分组的环,然后再来侦听实例的变化(注册变化,路由变化,熔断变化)。
2.1.4 可预测性
随便设计的LB算法可能会引起莫名的意外,比如RoundRobbin 和 最少在途请求数,如何与权重结合一定要谨慎。我们之前就设计过看起来可以,但跑起来压力都到了一台机上的算法。
2.2 实现的比较
2.2.1  轮询
Dubbo首先检查权重是否相等,如果相等就退化为无权重设置。其实日常99%的时段可能都是无权重的,区分快慢路径,为99%的情况作优化,是设计算法时的重要思路。
如果有权重,则每次生成个环,比对上一次的状态,在每个节点上调用完自己的权重数后,再往下一个节点走。
这里就有个问题,如果客户把三台服务器的权重分别设为100,10000,100, 本来只想表达个1:100:1的权重关系,结果。。。。连续一万个请求都压在第二台机上。
Pigeon的算法做了改进,会把权重求一个公约数(不过每个请求算一次也是累),真正达到1:100:1的效果。
我们家OSP嘛,与Dubbo有着同样的问题,这打击还没恢复过来,所以暂时主打Random,不支持RoundRobbin了。
要重新支持的话,会融合Motan的静态列表与Pigeon的公约数,直接生成一个静态环,每个实例按公约后的权重数,在环上插入若干个点,然后不断遍历这个环就行。
2.2.2  随机
这里有块测试项目新鲜程度的试金石:有没有用无锁的JDK8 ThrealLocalRandom 或者其移植类。
结果好像只有我们家OSP用了。
Dubbo同样先判断一下权重是否一致。如果不一致,则要遍历所有节点得到总权重,然后以总权重为上限生成一个随机数,然后再遍历每个节点,击鼓传花的减去其权重,直到数字小于0,那就是它了。
Pigeon和OSP的算法,和Dubbo一模一样。
Motan则像前面设计的完美版RoundRobbin一样,预先生成一个环,每个实例按公约后的权重数来插入若干个点,然后随机选一个点即可。OSP以后也要见贤思齐。
2.2.3  最小负载
最小负载的表达,最直接还是最少在途请求数(如果是同步请求,退化为最小并发数)。最小平均响应时间什么的,受不同方法不同的参数什么的影响太多。
最小负载最好就不要扯上权重了,因为说不清。Dubbo的做法,如果有在途请求数相等的节点,则再来按权重随机那套,但好像复杂了。
Motan做得有意思,考虑到服务实例的列表可能有一百几十台,全部比较一次谁的在途请求数最少太浪费时间。所以它会在列表里随机取其中一段,10个实例之间比较一下即可。
2.2.4  参数哈希
一致性哈希的基本算法:
1. 每个服务实例在一个以整数最大值为范围的环上,各占据错落有致不连续的若干个点。每个实例的点的数量可以固定如160,也可以按着权重来。
2.  然后随机一个整数,看它落在环上靠近的点是属于哪个实例的,就选择哪个实例。
与哈希取模相比,如果一台服务器挂了,只会让本来属于这台服务器的数据去到其他服务器,而原本属于其他服务器的数据则不会变。而哈希取模,模7 和 模8,几乎所有数据的落位都变了。
如果服务实例有一百个,每次生成一万六千个点的环,成本还是蛮大的。所以Dubbo采用了预生成的方式,只要实例列表的SystemIdentityHashCode不变,就继续用下去。
刀刀说Dubbo有个Bug,用可变的FullUrl 而不是固定的Host来生成点,会起不到一致性哈希的效果,另外用md5而不是新潮的murmurhash,也是历史感的体现。
Motan的类名虽然叫一致性哈希,但实现上好像不是......
OSP因为成本关系,暂时是直接取模的,日后也要学习Motan静态化服务列表,按权重构造一致性哈希环。
最后,拿什么来哈希呢?Dubbo和Motan哈希了全部请求参数(Dubbo也可以配置只哈希第1-N个参数),考虑到这样哈希成本非常不可控,及Proxy(支持多语言所用,以后再谈)要避免做反序列化,OSP采用了让客户端自己往Header中放分区键的方式。
Ribbon和Pigeon不支持Hash算法。
3. 路由规则定制
路由规则的定制,可以演化出无数的用法,如分流与隔离(读写分离,将非重要调用者分流到小集群,为重要调用者保留机器等等),AB测试,黑白名单等等。
经常听到的各种热词,都能靠路由规则支持。能用来做什么,只受限于配置员的想象力。
3.1 Dubbo的DSL
Dubbo的DSL语言:
比如:为重要调用者保留机器
application != kylin => host != 172.22.3.95,172.22.3.96
比如读写分离:
method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96
条件包括applicaiton,method, host, port
判断包括 ==,与 !=, 
值可以是 ","分隔的列表,"*"代表的后缀匹配。
还可以用Java支持的脚本语言如JRuby,Groovy来自定义规则。
3.2 Motan的DSL
Motan的DSL简单很多,基本只支持两端的服务名作条件,再配上两端的IP地址,也能玩出不少花样了。

服务名的定义:
#匹配com.weibo下以User开头的不包括UserMapping的所有服务,或以Status开头的所有服务 (com.weibo.User* & !com.weibo.UserMapping) | com.weibo.Status*以 to 分隔的两端IP:* to 10.75.1.* 10.75.2.* to 10.73.1.* * to !10.75.1.1


3.3 唯品会OSP的JSON化界面定义

我们的框架,原本也专门用Scala来解析的DSL,得益于Scala的强大(在祥威武),只用很少语句就很简洁实现了。
但后来我们老板觉得,还是需要一种更规范化,结构化,容易扩展的表达方式,所以选择了JSON(其实就是把前面DSL语法解析后的Java对象JSON化表达出来),并且独立出“目标集群”的概念。

3.3.1  整体语法 

条件集是多个条件的“与”的组合。

按顺序匹配每个条件集,中了就去到它的目标服务集群。

如果都没符合的,则走到“默认集群”去。

规则集 与 目标集群 是多对多的关系。 一个规则集可以对应多个有顺序的目标集群,用于满足HA的需求,如果集群A中的所有服务实例均不可用,则按顺序访问集群B中的实例。

目标服务集群,可以一个个IP定义,可以子网掩码或范围定义。注意Docker化之后,IP都是动态的,要么用范围定义,要么用Label。权重什么的也在里面顺便标明了。

3.3.2  条件语法 

参数列表:

客户端的应用名称

客户端的IP地址

服务的版本号,方法名

由客户端主动放入到Header中的上下文属性(绕过了复杂的参数提取,改为客户端自己放入),从这里玩出万千花样。
条件判断则包括了!=,  ==,  正则匹配,逗号分隔的列表等,还有针对某些特定类型的,如针对IP的子网掩码匹配。

3.3.3  运维友好的配置界面 
640.jpg

4. 机房路由
4.1 距离优先路由

虽然有爱因斯坦相对论的加持,从广州跑到北京的机房,几十毫秒的延时还是有的,所以,最好完全按距离远近来决定路由。

优先调用同机房的服务。但如果本机房的该服务的所有实例都挂了,或者服务只部署在其他机房,就调用离得最近的那个机房。

做起来其实也简单,一般按服务器IP的前两位,就能区分出机房来。然后做个配置,把每个机房之间的距离粗略量化出来。至于根据网络状态的动态选路,暂时不需要这么高大上的东西。

好像只有美团点评的Pigeon和我们自己家的OSP实现了。

4.2 其他方式
另外几家,虽然没有优先路由,但机房仍然是个跨不过的概念。
Motan的实现,通过前面提到的DSL语法,还能手工做流量容灾切换。
Dubbo的实现里,注册中心是按机房部署的,每个服务可以注册到一个或多个注册中心。
Netflix的Robbin,则有一种SameAvaiableZoneRule的负载均衡方式。不过它把机房路由也当成一种负载均衡,因此选了它的话,就硬编码了使用Random做均衡。
5. 小结

最后,还应该有一种静态配置文件的方式(类似Motan的DSL),在开发联调时,绕过上面所有所有的路由和负载均衡,直达目标测试服务器。
一通比较下来,感觉自家的OSP虽然还有种种待改进的地方,但整体完成度还可以。

Netflix家的Ribbon,基本上没什么亮点,处处敬陪末席。

还有,开源真好,太多让人眼前一亮的代码。

版权声明:本文版权归  江南白衣@唯品会 所有。任何形式的转载,必须获得作者授权。
本文可能还会持续修改,请戳 “阅读原文” 获得最新版本。

漫谈面向对象基石之开闭原则(OCP)

默认分类admin 发表了文章 • 0 个评论 • 135 次浏览 • 2016-12-30 10:37 • 来自相关话题

开闭原则的意思是软件实体应该对扩展开发,对修改关闭(Software entities should be open for extension,but closed for modification)。实现开闭原则的途径是抽象,将需要扩展的部分抽象出来,并留出扩展接口。打个比方,比如电脑机箱上有usb的插口,这些插口就是可扩展的部分,我们可以在这些usb插口上插上鼠标,键盘,U盘,还可以插上网银的U盾等等。电脑硬件上对于usb接口的这个设计就是一个符合开闭原则的设计。

为什么要遵循开闭原则呢?因为开闭原则可以使软件系统更容易复用,更容易维护,当某个软件实体,不适合了,我可以重新做另外一种实现,并将现有的实现替换掉。比如说统计个税的算法发生了一些变化,我可以在不改变原有代码的情况下,重新实现一个算法将原有的算法替换下来。比如说杀毒软件,在出现一种新的病毒时,开发出一个查杀这种病毒的新模块,可以只开发更新这个查杀模块,而不需要改变原有系统的内容。

开闭原则这么好,如何实现符合开闭原则的软件系统呢?答案是抽象,将可能发生变化的功能点进行抽象,并留出变化的接口。设计模式中很多模式都可以帮我们实现开闭原则,个人的理解设计模式是对抽象用法的一种总结。其实我们在项目已经为开闭原则做了一些工作了,比如说我们进行三层开发,将数据层抽象出来,并定义个数据处理的接口,我们可以通过新开发一个数据层把刚开始将数据存放到sql server中的实现,修改为将数据存放到my sql中的实现;我们将业务逻辑中的代码从UI代码中分离出来,这就为我们复用业务逻辑的代码提供了可能,我们可以开发一个专门为手机使用的UI层出来,当用户用手机访问我们的系统时,智能的切换到手机UI层的代码上去执行。
开闭原则实现的关键点在于抽象,也许我们刚开始不知道该把那部分抽象出来,但是这并不是问题,我们可以遵循简单设计的原则,当变化来了的时候,再重构代码,做到一种满足开闭原则的设计。

切忌到处都抽象,如果到处都抽象就会导致系统过度设计,过度复杂。这反而是不利于系统的维护。完全的开闭原则是不可能实现的,所以请保持简单设计,在需要的时候做符合开闭原则的设计。 查看全部
开闭原则的意思是软件实体应该对扩展开发,对修改关闭(Software entities should be open for extension,but closed for modification)。实现开闭原则的途径是抽象,将需要扩展的部分抽象出来,并留出扩展接口。打个比方,比如电脑机箱上有usb的插口,这些插口就是可扩展的部分,我们可以在这些usb插口上插上鼠标,键盘,U盘,还可以插上网银的U盾等等。电脑硬件上对于usb接口的这个设计就是一个符合开闭原则的设计。

为什么要遵循开闭原则呢?因为开闭原则可以使软件系统更容易复用,更容易维护,当某个软件实体,不适合了,我可以重新做另外一种实现,并将现有的实现替换掉。比如说统计个税的算法发生了一些变化,我可以在不改变原有代码的情况下,重新实现一个算法将原有的算法替换下来。比如说杀毒软件,在出现一种新的病毒时,开发出一个查杀这种病毒的新模块,可以只开发更新这个查杀模块,而不需要改变原有系统的内容。

开闭原则这么好,如何实现符合开闭原则的软件系统呢?答案是抽象,将可能发生变化的功能点进行抽象,并留出变化的接口。设计模式中很多模式都可以帮我们实现开闭原则,个人的理解设计模式是对抽象用法的一种总结。其实我们在项目已经为开闭原则做了一些工作了,比如说我们进行三层开发,将数据层抽象出来,并定义个数据处理的接口,我们可以通过新开发一个数据层把刚开始将数据存放到sql server中的实现,修改为将数据存放到my sql中的实现;我们将业务逻辑中的代码从UI代码中分离出来,这就为我们复用业务逻辑的代码提供了可能,我们可以开发一个专门为手机使用的UI层出来,当用户用手机访问我们的系统时,智能的切换到手机UI层的代码上去执行。
开闭原则实现的关键点在于抽象,也许我们刚开始不知道该把那部分抽象出来,但是这并不是问题,我们可以遵循简单设计的原则,当变化来了的时候,再重构代码,做到一种满足开闭原则的设计。

切忌到处都抽象,如果到处都抽象就会导致系统过度设计,过度复杂。这反而是不利于系统的维护。完全的开闭原则是不可能实现的,所以请保持简单设计,在需要的时候做符合开闭原则的设计。

开发一个业务逻辑复杂的系统,应该怎么样设计才能使项目的扩展性更好?

默认分类admin 发表了文章 • 0 个评论 • 145 次浏览 • 2016-12-29 21:01 • 来自相关话题

既然业务逻辑复杂,那意味着项目前期的业务建模、需求分析、分析设计极为重要,直接抛开这几个阶段进入技术实施开发阶段,不管套用什么设计模式、架构模式,系统的扩展性肯定难以保证。

项目的扩展性虽然最终体现为系统架构、技术实现的扩展性,但系统扩展性的根源在于系统业务架构及业务模型的扩展性。大家经常骂xx系统烂、扩展性差,大都将原因归结为技术实现烂,但总结那些成功的大型项目或产品的最佳实践,原因都会有:某某是业务专家,对xx业务很熟悉,能够衔接业务与技术。因此一个好的项目角色中,应该有行业专家/领域专家、业务过程分析师、系统分析师、软件架构师等角色,从业务架构、信息架构、技术架构保证系统的扩展性。

具体怎样进行业务建模,搭建良好的业务架构和业务模型,从而为技术架构、信息架构、技术实现奠定良好基础,有一些较为成熟的软件开发过程可供参考。例如 RUP(Rational Unified Process,统一软件开发过程)。一个标准的RUP工作流程包括:业务建模,需求分析,分析设计,实施开发,测试,部署,配置和变更管理,项目管理,环境。当然RUP只是一个方法论,且过于庞大,大部分项目很难完整执行其过程,需要根据实际情况进行裁剪,但其方法论对于复杂业务逻辑系统的建设具有指导意义。像互联网产品设计中常用的用例分析技术就源于RUP。

因此对于题主描述的一个复杂系统,标准的过程应当在业务建模,需求分析,分析设计,实施开发,测试,部署完整过程的分析设计(与开发语言无关)或实施开发(分析设计的成果映射为具体语言,例如Java、.NET等)阶段才考虑设计模式、架构模式的引入。设计模式的使用会经历僵化->固化->优化的阶段,类似禅修中“看山是山、看水是水”的三个阶段,才能体会模式的运用之妙。

值得强调的是:如果是偏交易(例如支付、金融)的系统,在考虑扩展性时候,一定要将信息架构、信息模型的扩展性纳入到考虑范围,此类系统数据模型至关重要,也不可能频繁变动。

上面描述方法的特别适用与传统软件、系统集成等需求偏稳定的项目,对于互联网偏创新性的项目就不一定完全适用了,此类项目的现实情况如下:业务模式不确定,会不停试错,验证模式;需求不停变化,要求能够快速响应;全新的行业,没有行业专家,没有行业标杆可借鉴(至多有跨界标杆可参考);此时候,类似精益创业、Scrum之类的敏捷开发模式更适合,但对于复杂的业务而言,业务建模->需求分析->分析设计的理念仍然值得参考借鉴。

最后,最最重要的是:完美系统的架构和扩展性是管理出来的、持续重构出来的。正如各大城市马路不停翻了再修、修了再翻的命运一样,中国大部分公司后任会不停否定掉前任的架构、系统,推倒再来一遍,然后等新系统刚开发出来不久,尚未上线或上线运营一段时间后,再换一帮人继续折腾,然后。。。

总结这么多年的经历,深刻体会到:再烂的系统和架构,如果能够强化管理、持续积累、持续重构、持续完善,都能够有机会成为完美的系统,完美的系统不在于其架构的牛逼和完美,而在于:符合公司的业务模式,能够完美支撑公司业务的高速发展和市场需求的快速响应。

 

http://www.zhihu.com/question/ ... 06943 查看全部
既然业务逻辑复杂,那意味着项目前期的业务建模、需求分析、分析设计极为重要,直接抛开这几个阶段进入技术实施开发阶段,不管套用什么设计模式、架构模式,系统的扩展性肯定难以保证。

项目的扩展性虽然最终体现为系统架构、技术实现的扩展性,但系统扩展性的根源在于系统业务架构及业务模型的扩展性。大家经常骂xx系统烂、扩展性差,大都将原因归结为技术实现烂,但总结那些成功的大型项目或产品的最佳实践,原因都会有:某某是业务专家,对xx业务很熟悉,能够衔接业务与技术。因此一个好的项目角色中,应该有行业专家/领域专家、业务过程分析师、系统分析师、软件架构师等角色,从业务架构、信息架构、技术架构保证系统的扩展性。

具体怎样进行业务建模,搭建良好的业务架构和业务模型,从而为技术架构、信息架构、技术实现奠定良好基础,有一些较为成熟的软件开发过程可供参考。例如 RUP(Rational Unified Process,统一软件开发过程)。一个标准的RUP工作流程包括:业务建模,需求分析,分析设计,实施开发,测试,部署,配置和变更管理,项目管理,环境。当然RUP只是一个方法论,且过于庞大,大部分项目很难完整执行其过程,需要根据实际情况进行裁剪,但其方法论对于复杂业务逻辑系统的建设具有指导意义。像互联网产品设计中常用的用例分析技术就源于RUP。

因此对于题主描述的一个复杂系统,标准的过程应当在业务建模,需求分析,分析设计,实施开发,测试,部署完整过程的分析设计(与开发语言无关)或实施开发(分析设计的成果映射为具体语言,例如Java、.NET等)阶段才考虑设计模式、架构模式的引入。设计模式的使用会经历僵化->固化->优化的阶段,类似禅修中“看山是山、看水是水”的三个阶段,才能体会模式的运用之妙。

值得强调的是:如果是偏交易(例如支付、金融)的系统,在考虑扩展性时候,一定要将信息架构、信息模型的扩展性纳入到考虑范围,此类系统数据模型至关重要,也不可能频繁变动。

上面描述方法的特别适用与传统软件、系统集成等需求偏稳定的项目,对于互联网偏创新性的项目就不一定完全适用了,此类项目的现实情况如下:业务模式不确定,会不停试错,验证模式;需求不停变化,要求能够快速响应;全新的行业,没有行业专家,没有行业标杆可借鉴(至多有跨界标杆可参考);此时候,类似精益创业、Scrum之类的敏捷开发模式更适合,但对于复杂的业务而言,业务建模->需求分析->分析设计的理念仍然值得参考借鉴。

最后,最最重要的是:完美系统的架构和扩展性是管理出来的、持续重构出来的。正如各大城市马路不停翻了再修、修了再翻的命运一样,中国大部分公司后任会不停否定掉前任的架构、系统,推倒再来一遍,然后等新系统刚开发出来不久,尚未上线或上线运营一段时间后,再换一帮人继续折腾,然后。。。

总结这么多年的经历,深刻体会到:再烂的系统和架构,如果能够强化管理、持续积累、持续重构、持续完善,都能够有机会成为完美的系统,完美的系统不在于其架构的牛逼和完美,而在于:符合公司的业务模式,能够完美支撑公司业务的高速发展和市场需求的快速响应。

 

http://www.zhihu.com/question/ ... 06943