
12年Java研发经验,8年技术管理和架构经验,熟悉支付和电商领域,擅长微服务生态建设,对Dubbo、Spring Cloud和gRPC等微服务框架有深入研究,并应用于项目,帮助过多家公司进行过微服务建设和改造 合著作品《深入分布式缓存》,目前正在编写《微服务架构实战》
暂时未有相关通用技术能力~
阿里云技能认证
详细说明最近看了Dyno-queues分布式延迟队列的源码,发现了一些不错的技巧,而本文是对Dyno-queues架构精华的总结。本文是根据 https://medium.com/netflix-techblog/distributed-delay-queues-based-on-dynomite-6b31eca37fbc 翻译而来,如果有不准之处请大家多包含。 在Netflix的平台上运行着许多的业务流程,这些流程的任务是通过异步编排进行驱动,现在我们要实现一个分布式延迟队列,这个延迟队列具有如下特点: 分布式 不用外部的锁机制 高并发 至少一次语义交付 不遵循严格的FIFO 延迟队列(消息在将来某个时间之前不会从队列中取出) 优先级 一、使用Dynomite和Redis构建队列 Dynomite是一种通用的实现,可以与许多不同的key-value存储引擎一起使用。目前它提供了对Redis序列化协议(RESP)和Memcached写协议的支持。我们选择Dynomite,是因为其具有性能,多数据中心复制和高可用性的特点。此外,Dynomite提供分片和可插拔的数据存储引擎,允许我们在数据需求增加垂直和水平扩展。 1、为什么选择Redis? 我们选择Redis作为构建队列的存储引擎: Redis架构通过提供构建队列所需的数据结构很好地支持了队列设计,同时Redis的性能也非常优秀,具备低延迟的特性 Dynomite在Redis之上提供了高可用性、对等复制以及一致性等特性,用于构建分布式集群队列。 一个队列被存储为Redis的有序集合(ZADD和ZRANGE等操作),Redis使用分数对有序集合中的成员进行排序,当往队列中存储数据时,根据优先级和超时时间计算分数。 2、使用Redis实现数据的push和pop 对于每个队列,维护三组Redis数据结构: 包含队列元素和分数的有序集合 包含消息内容的Hash集合,其中key为消息ID。 包含客户端已经消费但尚未确认的消息有序集合,Un-ack集合。 PUSH 根据消息超时(延迟队列)和优先级计算得分 添加到队列的有序集合 将Message对象到Hash集合中,key是messageId。 POP 计算当前时间为最大分数。 获取分数在0和最大分数之间的消息。 将messageID添加到unack集合中,并从队列的有序集中删除这个messageID。 如果上一步成功,则根据messageID从Redis集合中检索消息。 ACK 从unack集合中删除messageID。 从Message有效集合中删除messageID。客户端未进行确认的消息,会被再度推回到队列中(这是一个定时任务负责检测)。 3、可用分区和机架意识 我们的队列是在Dynomite的JAVA客户端Dyno之上建立的,Dyno为持久连接提供连接池,并且可以配置为拓扑感知,此外,Dyno为应用程序提供特定的本地机架(在AWS中,机架是一个区域,例如 us-east-1a、us-east-1b等),us-east-1a的客户端将连接到相同区域的Dynomite/Redis节点,除非该节点不可用,在这种情况下该客户端将进行故障转移。这个属性被用于通过区域划分队列。 分片队列根据可用区域进行分片,将数据推送到队列时,通过轮训机制确定分片,这种机制可以确保所有分片的数据是平衡的,每个分片都代表Redis中的有序集合,有序集中的key是queueName和AVAILABILITY _ZONE的组合。 避免全局锁 每个节点(上图中的N1...Nn)与可用性区域具有关联性,并且与该区域中的redis服务器进行通信。 Dynomite / Redis节点一次只能提供一个请求,Dynomite可以允许数千个并发连接,但是请求是由Redis中的单个线程处理,这确保了当发出两个并发调用从队列轮询元素时,是由Redis服务器顺序执行,从而避免任何本地或分布式锁。 在发生故障转移的情况下,确保没有两个客户端连接从队列中获取相同的消息。 处理Un-ACK的消息后台进程监视UNACK集合中的消息,这些消息在给定时间内未被客户端确认(每个队列可配置)。这些消息将移回到队列中。 Dyno-queues分布式延迟队列的github地址是:https://github.com/Netflix/dyno-queues
我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
image.png 首先我要感谢我的写作团队和编辑牺牲大量的业余时间来共同完成本书,本书将于10月左右正式上市,期待大家多多关注。 书中的内容既包括大量非常有新意的微服务理论知识,也包括大量工作中实践的案例,多种案例可以直接上手借鉴使用。 实战案例的亮点主要包括: Dubbo源码以及大量实战案例 多机房多活的实践方案 Spring boot/Spring Cloud的实战案例 基于gRPC的微服务实战案例 Service mesh在某公司的案例使用 微服务在多家公司的改造案例
给公司的一些研发同事分享了Java Agent一些简单的原理和实现,最近比较懒就不整理成文字了,直接上ppt截图。 image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png
image.png CAP是什么? CAP理论,被戏称为[帽子理论]。CAP理论由Eric Brewer在ACM研讨会上提出,而后CAP被奉为分布式领域的重要理论[1] 。 分布式系统的 CAP 理论:首先把分布式系统中的三个特性进行了如下归纳: ● 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的 值。(等同于所有节点访问同一份最新的数据副本) ● 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端 的读写请求。(对数据更新具备高可用性) ● 分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统 如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前 操作在 C 和 A 之间做出选择。(分区状态可以理解为部分机器不连通了,比如 机器挂了,繁忙失去响应,单机房故障等) Partition 字面意思是网络分区,即因网络因素将系统分隔为多个单独的部 分,有人可能会说,网络分区的情况发生概率非常小啊,是不是不用考虑 P, 保证 CA 就好。要理解 P,我们看回 CAP 证明中 P 的定义: In order to model partition tolerance, the network will be allowed to lose arbitrarily many messages sent from one node to another。 网络分区的情况符合该定义,网络丢包的情况也符合以上定义,另外节点宕 机,其他节点发往宕机节点的包也将丢失,这种情况同样符合定义。现实情况 下我们面对的是一个不可靠的网络、有一定概率宕机的设备,这两个因素都会 导致 Partition,因而分布式系统实现中 P 是一个必须项,而不是可选项。 高可用、数据一致性是很多系统设计的目标,但是分区又是不可避免的事情。 我们来看一看分别拥有 CA、CP 和 AP 的情况。 CA without P:如果不要求 P(不允许分区),则 C(强一致性)和 A(可用 性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,CA 系 统基本上是单机系统,比如单机数据库。2PC 是实现强一致性的具体手段。 image.png CP without A:如果不要求 A(可用),相当于每个请求都需要在 Server 之间 强一致,而 P(分区)会导致同步时间无限延长,如此 CP 也是可以保证的。很 多传统的数据库分布式事务都属于这种模式。 image.png AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点 之间可能会失去联系,为了高可用,每个节点只能用本地数据 供服务,而这 样会导致全局数据的不一致性。现在众多的 NoSQL 都属于此类。 CAP 理论的证明 该理论由 brewer 出,2 年后就是 2002 年,Lynch 与其他人证明了 Brewer 猜 想,从而把 CAP 上升为一个定理。但是,它只是证明了 CAP 三者不可能同时满 足,并没有证明任意二者都可满足的问题,所以,该证明被认为是一个收窄的 结果。 Lynch 的证明相对比较简单:采用反证法,如果三者可同时满足,则因为允许 P 的存在,一定存在 Server 之间的丢包,如此则不能保证 C,证明简洁而严谨。 在该证明中,对 CAP 的定义进行了更明确的声明: C:一致性被称为原子对象,任何的读写都应该看起来是 “原子“的,或串行的。写后面的读一定能读到前面写的内容。所有的读写请 求都好像被全局排序一样。 A:对任何非失败节点都应该在有限时间内给出请求的回 应。(请求的可终止性) P:允许节点之间丢失任意多的消息,当网络分区发生时, 节点之间的消息可能会完全丢失。 对于 CAP 进一步的案例解释 2010 年的这篇文章 brewers-cap-theorem-on-distributed-systems/,用了三 个例子来阐述 CAP,分别是 example1:单点的 mysql;example2:两个 mysql, 但不同的 mysql 存储不同的数据子集,相当于 sharding;example3:两个 mysql,对 A 的一个 insert 操作,需要在 B 上执行成功才认为操作完成(类似 复制集)。作者认为在 example1 和 example2 上 都能保证强一致性,但不能保 证可用性;在 example3 这个例子,由于分区(partition)的存在,就需要在 一致性与可用性之间权衡。对于复制而言,在很多场景下不追求强一致性。比 如用户支付之后,交易记录落地了;但可能消费记录的消息同步存在延迟,比 如消息阻塞了。在金融业务中,采取类似两地三中心架构,往往可能采取本地 数据和异地机房数据同时写成功再返回的方式。这样付出了性能的损耗,响应 时间变长。但发生机房故障后,能确保数据是完全可以读写的,保障了一致性。 CAP 理论澄清 [CAP 理论十二年回顾:"规则"变了]一文首发于 Computer 杂志,后由 InfoQ 和 IEEE 联合呈现,非常精彩[2],文章表达了几个观点。 “三选二”是一个伪命题 不是为了 P(分区容忍性),要在 A 和 C 之间选择一个。分区很少出现,CAP 在 大多数时候允许完美的 C 和 A。但当分区存在或可感知其影响的情况下,就要 预备一种策略去探知分区并显式处理其影响。这样的策略应分为三个步骤:探 知分区发生,进入显式的分区模式以限制某些操作,启动恢复过程以恢复数据 一致性并补偿分区期间发生的错误。 “一致性的作用范围”其实反映了这样一种观念,即在一定的边界内状态是一 致的,但超出了边界就无从谈起。比如在一个主分区内可以保证完备的一致性 和可用性,而在分区外服务是不可用的。Paxos 算法和原子性多播(atomic multicast)系统一般符合这样的场景。像 Google 的一般做法是将主分区归属 在单个数据中心里面,然后交给 Paxos 算法去解决跨区域的问题,一方面保证 全局协商一致(global consensus)如 Chubby,一方面实现高可用的持久性存 储如 Megastore。 ACID、BASE、CAP ACID 和 BASE 这两个术语都好记有余而精确不足,出现较晚的 BASE 硬凑的感觉 更明显,它是“Basically Available, Soft state, Eventually consistent (基本可用、软状态、最终一致性)”的首字母缩写。其中的软状态和最终一 致性这两种技巧擅于对付存在分区的场合,并因此 高了可用性。 CAP 与 ACID 的关系更复杂一些,也因此引起更多误解。其中一个原因是 ACID 的 C 和 A 字母所代表的概念不同于 CAP 的 C 和 A。还有一个原因是选择可用性 只部分地影响 ACID 约束。 进一步看[分区]之后 image.png 用一下这张图[引用自 link 2],在状态 S 的时候是非分区状态,而分区模式则 演化出来了 S1,S2,那么问题来了,分区恢复之后,状态究竟是多少呢?有几 种解决方案。 分区恢复策略:可交换多副本数据类型 注意,能支持此类处理的场景是有限的。 riak_dt 就是这样一种保障最终一致性实现的数据结构,它分为Operation- based CRDTs、State-based CRDTs 2 种形态。 riak_dt link 参见 link [3]。 分区恢复策略:回放合并 在分区恢复过程中,设计师必须解决两个问题: 分区两侧的状态最终必须保持一致 并且必须补偿分区期间产生的错误。 如上图所示,对于分区恢复的状态 S*可以通过未分区时的状态 S 为起点,然后 按顺序[回放]相应的变化事件[以特定方式推进分区两侧的一系列操作,并在过 程中一直保持一致的状态。]。Bayou[link 4]就是这个实现机制,它会回滚数 据库到正确的时刻并按无歧义的、确定性的顺序重新执行所有的操作,最终使 所有的节点达到相同的状态。 对于有冲突的情况,比如版本管理软件 cvs,存在人工介入、消除冲突的处理策略。 image.png image.png
一、什么是蓝绿发布 蓝绿部署是不停老版本,部署新版本然后进行测试,确认OK,将流量切到新版本,然后老版本同时也升级到新版本。 1、特点 蓝绿部署无需停机,并且风险较小。 2、蓝绿发布的注意事项 当你切换到蓝色环境时,需要妥当处理未完成的业务和新的业务。如果你的数据库后端无法处理,会是一个比较麻烦的问题; 可能会出现需要同时处理“微服务架构应用”和“传统架构应用”的情况,如果在蓝绿部署中协调不好这两者,还是有可能会导致服务停止。 需要提前考虑数据库与应用部署同步迁移 /回滚的问题。 蓝绿部署需要有基础设施支持。 在非隔离基础架构( VM 、 Docker 等)上执行蓝绿部署,蓝色环境和绿色环境有被摧毁的风险。 二、为什么需要蓝绿发布系统 1、新项目和新需求非常多 2、新需求的上线过程是,先上线一台服务器然后观察会不会出问题,如果没有问题则全部上线。 3、分流是关键,但是动态分流是痛点。 三、老分流方案 image.png 方案存在的问题点: 1、nginx.conf配置文件里各种if、set和rewrite,并且容易配置出错。 2、修改完配置文件后,重启或者reload后才能生效。 3、不能实现太复杂的逻辑。 4、不能实现一些特殊分流方式。 四、新分流方案 image.png 功能说明: 采用Redis存放分流策略 分流策略包括按时间来分流,比如每分钟分流多少笔订单,还有按权重分流,比如新老系统之间的比例是1:9 采用OpenResty+lua,整体性能优秀。 单台压测结果: image.png
一、重客户端 写入缓存: image.png 应用同时更新数据库和缓存 如果数据库更新成功,则开始更新缓存,否则如果数据库更新失败,则整个更新过程失败。 判断更新缓存是否成功,如果成功则返回 如果缓存没有更新成功,则将数据发到MQ中 应用监控MQ通道,收到消息后继续更新Redis。 问题点:如果更新Redis失败,同时在将数据发到MQ之前的时间,应用重启了,这时候MQ就没有需要更新的数据,如果Redis对所有数据没有设置过期时间,同时在读多写少的场景下,只能通过人工介入来更新缓存。 读缓存: 如何来解决这个问题?那么在写入Redis数据的时候,在数据中增加一个时间戳插入到Redis中。在从Redis中读取数据的时候,首先要判断一下当前时间有没有过期,如果没有则从缓存中读取,如果过期了则从数据库中读取最新数据覆盖当前Redis数据并更新时间戳。具体过程如下图所示: image.png 二、客户端数据库与缓存解耦 上述方案对于应用的研发人员来讲比较重,需要研发人员同时考虑数据库和Redis是否成功来做不同方案,如何让研发人员只关注数据库层面,而不用关心缓存层呢?请看下图: image.png 应用直接写数据到数据库中。 数据库更新binlog日志。 利用Canal中间件读取binlog日志。 Canal借助于限流组件按频率将数据发到MQ中。 应用监控MQ通道,将MQ的数据更新到Redis缓存中。 可以看到这种方案对研发人员来说比较轻量,不用关心缓存层面,而且这个方案虽然比较重,但是却容易形成统一的解决方案。 推荐图书 最后推荐一本很不错的技术书籍《深入分布式缓存》 image.png 京东购书二维码: image.png
image.png 一、才智深藏 1、低调做人,高调做事。 2、清楚的事,精美的做,迷糊的事,谦善着做,模糊的事,慎重做。 3、大勇若怯,大七若拙,做个模糊的精明人。 4、示弱捕怜惜,并不丢人。 5、奇妙的躲藏自己的实力,是为了爆发时的一举成功。 6、装模糊,但别真模糊,揣着理解装模糊才敢问出路。 7、留一半清醒,留一半醉。 二、做人标准 1、万事都要留有余地,留三分给别人。 2、做人不要太傲骄。 3、得理也要饶人。 4、做什么事都要给自己留一条退路。 5、不要将赌注押在某一个人身上,押大押小都在赌。 6、不得罪小人。 7、人心叵测,凡事留一手,正如下棋一样,能杀的进来,杀的出去。 三、当人慎重 1、做人要多留一个心眼,社会处处是圈套。 2、逢人只说三分话,未可全抛一片心。 3、留意主动帮你忙的人,大多数是无事不登三宝殿。 4、看人要看心,结交要友谊。 5、向别人倾诉心事要慎重。 四、留意体面 1、给别人体面,就是给自己体面。 2、看穿别道破,面上都好过。 3、直话软说,真话慢说。 4、学会痛并快乐着,在批判中加点糖。 5、把功劳给领导,领导有肉吃,少不了你的汤。 6、死敌也要留面子,化敌为友才是最大的胜利。 五、学会闭嘴 1、能说会道会让人感觉轻浮。 2、看人脸色说话,说别人爱听的话。 3、说话要有保留,说出的话,泼出去的水。 4、避开无谓的争论,争到最后是同归于尽。 5、少说忠言,因为爱听的人不多。 6、开玩笑要留有分寸。 7、说话要讲究场合。 六、八面玲珑 1、人脉是成功的命脉。 2、结交比自己优秀的人。 3、紧记别人的姓名。 4、一定要树立一个自己兄弟的档案。 5、把精力放在关键人物身上。 6、真诚协助别人。 7、做人要诚信。 8、帮别人做事,要把人情做足。
image.png 这张图是作者多年经验积累的总结,涵盖面非常全,我看后深感震撼,转载与大家共享。 image.png 作者:陈明 高清原图:http://cmsblogs.com/wp-content/images/share/chenssy_juc_201712.png
暂存图片.....文章后补 image.png
2017首届领域驱动技术大会一直是我非常期望的,要非常感谢右军赠送的门票能够让我领略大会风采。 image.png 这届大会组织者非常用心,组织了非常多的话题可供探讨,确实大会的内容给我带来的感觉是震撼的,我之前对领域的了解也仅从《领域驱动设计》以及《实现领域驱动设计》这两本书中有过学习,以及在实现微服务生态体系的过程中有过一些接触。 在大会的整个进程中,听了很多老师不同主题的演讲,让我印象极为深刻的还是:张逸老师的《Bounded Context的实践意义》、腾云老师的《DDD-没那么难》。下面我将分别结合这二个议题谈谈我自己的一些想法。 Bounded Context(限界上下文)的实践意义 image.png (因为光线和距离的原因照片可能不清晰,望大家见谅) 首先我们先来解释一下,什么是限界上下文。 《实现领域驱动设计》这本书中解释到:限界上下文是一个显式边界,领域模型便存在于边界之内。在边界内,通用语言中的所有术语和词组都有特定的含义,而模型需要准确地反映通用语言。 用一段更形象的语言来描述:我们每天都去上班,上班的时候会换乘地铁,我从8号线下车,换乘2号线,然后再去换乘10号线,这样最终到达某一个地点,结束上班这个过程。在这个过程中,8号线、2号线和10号线都可以理解为不同的限界上下文,我们中间换乘的动作可以理解为领域事件,而我们最终的目标是为了上班,这个就是关键事件,我们上班就是在不同的上下文中切换。 我们还可以把上下文理解为一个模块,一个系统、一个应用或者一个服务。在我看来,限界上下文的存在对微服务的划分是有重大意义的,但是限界上下文不是新的概念,早在SOA时代就已经存在,只是当时在企业应用的时候并没有将SOA和DDD过多的联系在一起,不知道还有多少同学知道板桥里人(彭晨阳)的,早在2008年的时候在他的json网站中就已经对SOA和DDD的关系做过一些解释: SOA服务是在松耦合组件分离后的再次打包,而Evans DDD则是一把切断组件关系的利刃。从这个方面看,DDD应该是更基础平台,万丈高楼平地起啊,而DDD是对象方法论集大成,集合分析模式和设计模式 ----出自 SOA 与 DDD 通过当时的文章和解释我们不难看出实际上在SOA中更多的是使用DDD的OO来取代数据库分析设计,SOA是粗粒度的服务化打包,而DDD则是一把斩断粗粒度的利刃。正如这次大会张逸老师一句玩笑的话所说,正因为微服务拯救了DDD,通过这句话可以看出微服务的提出是真正的将DDD给与结合在了一起,而微服务的细粒度的服务与DDD本身的理念也是契合,从而达到了互相发展的境界。 张逸老师在这次演讲中深入探讨了几个关键词:康威定律,逻辑边界和物理边界,切断数据库的耦合、识别上下文的方法等等,能够明显的感觉出来,DDD也在发展也在和微服务和互联网领域不断的演进。 DDD-没那么难 image.png 腾云老师的分享更多是在实践过程中的总结,首先谈了数据驱动与领域驱动的不同点,表格如下: 数据驱动 领域驱动 数据库优先 领域模型优先 算法和数据机械结合 算法和数据有机结合 技术导向 业务导向 代码不能反映业务 代码即是设计 业务逻辑分散 业务逻辑内聚 扩展性差 扩展性佳 我记得在2010年以前,研发人员和产品聊完需求后第一步就是要使用PowerDesigner画数据库表结构图,根据数据库表结构图倒推项目架构,后面DDD开始推广以来,慢慢的由UML图开始逐渐占用主导地位,现在表结构图已经成为架构设计的补充。 在DDD中常见二种设计模型,分别是贫血模型和充血模型。 贫血模型 贫血模型是指领域对象里只有get和set方法,仅包含状态(属性),不包含行为(方法),采用这种设计时,需要分离出DB层,专门用于数据库操作。 image.png 从图中可以看出领域层的职责很弱,领域对象只是用来充当数据存储的对象。但是这种模型整体架构清晰,自上而下单向连接。 远程访问接口 -> Facade接口 -> Service服务 -> 领域层 -> DAO -> 数据库 充血模型 充血模型是贫血模型的相对定义,在这个模型中领域层的作用较大,不再是get和set方法的集合,而是将部分业务逻辑以及持久化的操作集成在内,如下图所示: image.png 这种模型的调用关系则变成了:应用层 -> Facade接口 -> 领域层 -> 基础设施层 其实这样的好处就是与领域对象相当的业务逻辑封装在对象内部,biz或者service层只需要调用对象进行简单的业务组装即可,不像贫血模型那样所有业务都集中在biz层或者service层中造成非常沉重难以拆分,但充血模型比较难以设计,需要有一定经验的设计师前期规划好,后期工作才能事半功倍,不然则会造成项目混乱。 在分享中腾云老师还做了实体和值对象的讲解,下面我将这二者的区别以表格的方式列出来供参考: 实体 值对象 具有生命周期 起描述作用 有唯一标识 无唯一标识 通过ID判断相等性 实现equals方法 增删改查/持久化 即时创建用完就扔 可变 不可变 比如Order/Car 比如Address/Color 看到这里让我突然想起一个故事来: 有一对双胞胎,他们出生的时候,长得一模一样,以至于爸妈都分不清,不得已他们在双胞胎的脖子上系个项链来标记:谁是老大?谁是老二?其实这个“标记”就可以看作是实体的标识,只不过是用项链来标识的。 有一天小镇要统计双胞胎的分布情况,然后调查人员来到他们家,问他们爸妈:“你们家里有没有双胞胎?几对双胞胎?龙凤胎?还是。。。”,然后他们爸妈就报上:“一对双胞胎-两个小子”,然后调查人员就做了笔记走了。在这个过程中,他们丝毫没有提及双胞胎脖子上的“项链”。 这也就是实体和值对象的根本区别:实体不仅需要知道它是什么?而且还需要知道它是哪个?而值对象只需要知道它是什么就可以了。 小结 夜已深,文章写到这里,我想也应该可以结束了,大会的内容非常丰富,在这里只是把我看到的、听到的,结合我自己的一些想法看法总结出来,文章难免有些地方比较偏面还望大家海涵。 参考文章http://raychase.iteye.com/blog/1328224https://www.cnblogs.com/xishuai/p/ddd-entity-value-object.html
代理命令示例 java -cp classes:lib/byte-buddy-1.4.16.jar -javaagent:test-1.0-SNAPSHOT.jar com.test.domain.AgentEntity 名词解释 JVMTI:全称JVM Tool Interface,是JVM暴露出来的一些供用户扩展的接口集合。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。 JPLISAgent(Java Programming Language Instrumentation Services Agent) JVM解析流程图 image.png
都在说微服务,那么微服务的反模式和陷阱是什么(一)http://www.jianshu.com/p/3986239138fe 都在说微服务,那么微服务的反模式和陷阱是什么(二)http://www.jianshu.com/p/c76f7f234a31 九、通信协议使用的陷阱 在微服务架构体系中要求每个服务都是独立布署,这就意味着服务之间会有通信,也就是说会有很多的远程访问。 当你不知道这些远程访问需要多长时间的时候,就会掉入到这个陷阱,当然我们可以假定远程访问一次50毫秒,但我们是否真正的进行过测试呢?那么服务的平均响应时间是多少呢?即使有看上去很好的平均响应时间,那么糟糕的“长尾延迟”也会将整体系统摧毁。 9.1 延迟测量 在生产环境中进行压力测试,是检测我们系统性能的重要手段之一,举个例子:我们有一个特定业务需要四个服务来协调处理,假如远程访问一次的时间是100毫秒,那么这个特定业务就需要消耗500毫秒(初始请求+四个服务的调用时间),这个只是远程访问的时间,还不算实际业务代码的执行时间,这是大多数应用系统都不能接受的时间。 9.2 通信协议比较 不同协议的延迟响应时间其实在不同的环境中表现的差异很大,因此我们也需要在不同的业务请求下建立一些测试基准。 图9-1 从图9-1中可以看出AMQP的性能要比REST的快近一倍,可以我们就可以做出一些选择了,在什么场景下应该用什么协议,另外在选择协议时性能并不是唯一的考虑因素,在第十章将会为大家介绍除了性能还需要考虑的点是什么。 十、REST陷阱 目前使用REST协议已然成了微服务协议的最佳选择了,现在最流行的DropWizard和Spring boot就是基于REST进行通信的,那问题来了,如果REST是一个最佳选择,那为什么又说它是一个陷阱呢?如果把REST作为唯一的通讯方式,就有可能掉入这个陷阱,比如如何处理异步通讯(http 1.1是blocking的)、如何在一个事务中管理多次服务调用?如何支持广播? 你应该考虑两种类型的消息标准作为微服务架构中的消息传递:特定平台的标准和平台无关的标准。特定平台的标准比如 JMS for java、MSMQ for .net。平台无关的比如 AMQP。 使用消息系统的好处可以异步请求,还可以实现广播的方式,还可以实现事务请求。 10.1 异步请求 使用微服务架构首先要考虑的是异步通信方式,因为异步通信的调用者不需要考虑等待服务的响应时间,如图10-1所示。 图10-1 使用异步方式的好处不仅提升了整体性能,还增加了一些可靠性的因素,另外也不需要担心超时问题和在程序中设置断路器模式。 10.2 广播能力 这个最典型的就是消息的“发布-订阅”,如图10-2所示。 图10-2 10.3 事务请求 消息系统需要支持事务消息的概念,这意味着如果消息被发送到多个队列或Topic中,在发送方对该事务进行提交之前, 这些消息实际上不会被接收方所接收。服务消费者发送一个消息到第一个服务,然后发送另一个消息的第二个服务,如图10-3所示。在服务使用者执行提交之前,这些消息都保存在队列中。一旦服务使用者执行提交,两个消息就会被释放。 图10-3 在图10-3中,服务消费者将消息发送到第一个队列中,然后服务消费者业务报错, 这时可以在消息事务中进行回滚,从消息系统的队列中删除掉刚才发的消息。 使用REST实现这种事务能力就非常困难,其实就是要求服务使用者使用TCC、或者补偿方式来达到最终一致性。 结束语 关于微服务的反模式和陷阱三部曲,到现在为止已经全部翻译完成,英文文档一共60多页,这里面有不少内容大家都是耳熟能详的,关于原版的英文文档我也提供给大家做一个参考,最后感谢大家的支持和帮助。 原版文档链接如下:http://pan.baidu.com/s/1qY3Etoo 密码:l26d
都在说微服务,那么微服务的反模式和陷井是什么(一)http://www.jianshu.com/p/3986239138fe 六、无因的开发者陷阱 名字来自詹姆斯·迪恩演的电影《无因的反叛》(Rebel Without a Cause),一个问题青年因为错误的原因做了错误的决定。 很多架构师和开发者在微服务的开发中权衡利弊, 比如服务粒度和运维工具,但是基于错误的原因,做了错误的决定。 6.1 做出错误的决定 图6-1说明了一种情况是通过测试发现服务划分的太细了,因此非常影响性能,主要是由于服务划分的太细导致增加了通信工作量也在一定程度上对稳定性造成一定影响。在这种情况下,开发人员或架构师决定将这些服务整合到一个更粗粒度的服务中,以解决性能和可靠性问题。 图6-1 这个方案看起来似乎合情合理,但是之后的布署、更改控制和测试都会受到影响。 再看图6-2,这种场景是左边的服务太粗了,影响了服务的测试和布署,于是进行了拆分,减少了每个服务的范围。 图6-2 通过以上二个场景我们可以看出,如果服务太细我们就需要考虑将服务合并,如果服务太粗,我们又会考虑将服务进行拆分。太细的话会增加通信成本和容易造成可靠性不稳定,太粗的话又容易导致不容易测试和上线布署,所以这就要看我们如何来权衡利弊。 6.2 了解业务驱动 了解业务驱动对于合理设计微服务至关重要,每一个架构师或者开发者都应该先回答以下三个问题: 我们为什么要设计微服务架构? 主要的业务驱动是什么? 最重要的架构特点是什么? 使用可布署、性能、健壮性和可扩展作为主要的架构特性,我们其实最先需要考虑的是如何利用业务来进行服务的整合和拆分。 场景一:迁移到微服务主要是想达到快速上线和布署 在这种场景下服务的可布署能力相对要大于性能、稳定性因素,所以要拆分服务的时候可以考虑稍微细粒度一些。 场景二:迁移到微服务主要是想提高系统的性能和健壮性 这种场景是从单一应用程序迁移到微服务架构的一个常见因素,很多公司都是业务驱动,那么就需要考虑服务的可靠性和健壮性,因此倾向于更粗粒度的服务,而不是细粒度的服务。 我经常使用的一种方式借助白板和团队成员一起讨论,如图6-3所示,每当有服务粒度的划分问题的时候,我们经常在白板上做草稿讨论清楚。 图6-3 七、赶潮流陷阱 你必须拥抱微服务,因为这是目前的趋势,而且其他人和公司目前都已经这么做了。 我们盲目的使用微服务,却还没有仔细分析业务需求、组织架构和技术环境,这就是随大流陷阱。 避免这个陷阱的方式充分理解微服务的好处和短处,俗话说,知己知彼,百战不殆。 7.1 微服务的优点和缺点 优点: 发布:易于发布 测试:易于测试 改变控制:更容易的改变一个服务的功能 模块 规模可扩展 缺点: Team组织改变 性能 可靠性降低 运维难度加大 7.2 其他架构模型 微服务的架构很好,但是不是唯一的架构模式,比如下面还有一些其它的架构模式: Service-Based Architecture Service-Oriented Architecture Layered Architecture Microkernel Architecture Space-Based Architecture Event-Driven Architecture Pipeline Architecture 当然你并不一定只使用唯一的一种架构模式,你可能在系统中混用这些架构模式。 下面有一些架构的参考资料:Software Architecture Fundamentals: Understanding the BasicsSoftware Architecture Fundamentals: Beyond the BasicsSoftware Architecture Fundamentals: Service-Based ArchitectureSoftware Architecture PatternsMicroservices vs. Service-Oriented Architecture 八、静态契约陷阱 在微服务的消费者和提供者之间提供了一种契约,契约主要包括输入和输出数据、以及操作的名称,契约通常是以XML、JSON或者JAVA对象等来表示。但是这些契约永远不会改变吗? 这里举个例子来说明因为服务契约版本控制而发生的问题: 假如你有一个服务是由三个不同的客户端访问(client1、client2和client3),这时client1想更改服务契约,你要检查client2和client3能否适应这个改变,并且client2和client3告诉我适应不了这个改变,需要数周时间才能调整完成。这时候我需要告诉client1,client2和client3需要数周时间才能适应调整完成,但是client1却不能等待这么长时间。 可以在你的服务契约中提供版本控制,实现向后兼容。现在我们可以根据client1的请求做一些灵活的控制,我们可以创建一个新版本的契约,比如v1.1,client2和client3依然使用v1版本,这样client1就可以立刻作为契约更改,而不必纠结于client2和client3需要适应调整的时间。 有两种实现方式:在header中加入版本号,或者在契约自身scheme中加入版本号。 8.1 header版本控制 契约版本的第一种办法是把版本号放在远程访问协议头,如图8-1所示,远程访问协议包括REST, SOAP, AMQP, JMS, MSMQ等等。 图8-1 例如在使用REST的时候,可以使用MIME类型来指定协议版本,如下代码: POST /trade/buy Accept: application/vnd.svc.trade.v2+json 通过在header的MIME类型中指定契约版本号,服务端就可以通过header的MINE类型解析得到相应的版本号。 8.2 Schema版本控制 第二种版本控制方式是在契约自身中进行,不需要在header中指定版本号,如图8-2所示。 图8-2 请先看如下json格式数据: { "$schema": "http://json-schema.org/draft-04/schema#", "properties": { "version": {"type": "integer"}, "acct": {"type": "number"}, "cusip": {"type": "string"}, "sedol": {"type": "string"}, "shares": {"type": "number", "minimum": 100} }, "required": ["version", "acct", "shares"] } 该模式直接将版本号定义在契约的输入数据中,这种模式最大的优点是与协议无关,只与数据格式有关。 POST /trade/buy Accept: application/json { "version": "2", "acct": "12345", "sedol": "2046251", "shares": "1000" } String msg = createJSON( "version","2", "acct","12345", "sedol","2046251", "shares","1000")}; jmsContext.createProducer().send(queue, msg); 这种方式也有一些不足就是每一次都需要对消息数据进行解析,提取出版本号进行校验,而且数据格式也有可能会改变也不太容易做到自动映射到JAVA对象中。
image.png 前言 网上看到一本关于微服务反模式的电子书,看后感觉内容非常棒,于是我决定分阶段翻译成中文书,翻译的目的也是想帮助想深入了解微服务的朋友,由于英文水平有限,如有翻译不对之处希望多留言指正。 书籍英文目录如下 image.png image.png 书籍中文目录如下: 1、数据驱动的迁移反模式 1.1、太多的数据迁移 1.2、功能分割优先,数据迁移最后 2、超时反模式 2.1、使用超时 2.2、使用断路器模式 3、共享反模式 3.1、过多依赖 3.2、共享代码的技术 4.到达报告反模式 4.1、微服务报告的问题 4.2、Asynchronous Event Pushing 5、沙粒陷阱 5.1、分析服务的范围和功能 5.2、分析数据库事务 5.3、分析服务编排 6、无因的开发者陷阱 7、随大流陷阱 8、其它架构模式 9、静态契约陷阱 10、通信协议使用的陷阱 11、REST陷阱 一、数据驱动的迁移反模式 微服务会创建大量小的、分布式的、单一用途的服务,每个服务拥有自己的数据。这种服务和数据耦合支持一个有界的上下文和一个无共享数据的架构,其中,每个服务及其对应的数据是独立一块,完全独立于所有其他服务。服务只暴露了一个明确的接口(服务契约)。有界的上下文可以允许开发者以最小的依赖快速轻松地开发,测试和部署。 采用数据驱动迁移反模式主要发生在当你从一个单体应用向微服务架构做迁移的时候。我们之所以称之为反模式主要原因是,刚开始我们觉得创建微服务是一个不错的主意,服务和相应的数据都独立成微服务,但这可能会将你带向一个错误的道路上,导致高风险、过剩成本和额外的迁移工作。 单体应用迁移到微服务架构有两个主要目标: 第一个目标是单体应用程序的功能分割成小的,单一用途的服务。 第二个目标是单体应用的数据迁移到每个服务自己独占的小数据库(或独立的服务)。 下图展示了一个典型的迁移,看起来像服务代码和相应的数据同时进行迁移。 图1-1 上图中有三个服务是从单体应用中划分而来,并且还划分独立的三个数据库,这是一个自然演变的过程,因为在每个服务和数据库之间都使用了最为关键的限界上下文,然而我们遇到的问题也正是基于这一过程将带领我们进入数据迁移的反模式。 1.1 太多的数据迁移 这种迁移路径的主要问题是,我们很难在一次就能够划分清楚每个服务的粒度,从一个更粗粒度的服务开始着手,一步步的进行细化工作,并且要多了解相关业务知识,不断的对服务的粒度进行调整,我们来看图1-1发现最左边的服务粒度太粗了,需要再拆分成二个小的服务,或者你发现左边的二个服务粒度划分的又太细了,需要进行合并。而数据迁移要比源代码迁移更复杂,更容易出错,我们最好只为数据进行一次迁移工作,因为数据迁移是一个高风险的工作。 我们的微服务划分也就是应用代码的迁移和数据的迁移。如图1-2所示。 图1-2 1.2 功能分割优先,数据迁移最后 此模式主要采用的是一种避免的手段,以迁移服务的功能为第一,同时也需要注意服务和数据之间的限界上下文。我们可以通过合并与拆分的手段对服务进行调整直到满意为止,这时候就可以迁移数据了。 如图1-3所示,左边所有三个服务都已经进行了迁移和拆分,但是所有服务仍然使用的是同一个数据库,如果这是一个临时中间方案还可以作为一个选择,这时候我们就需要更多的了解服务如何使用,以及接受什么类型的请求数据等。 图1-3 在图1-3中,我们要注意最左边的服务是如何发现粒度太粗而拆分成二个服务的。服务粒度最终确定完成之后,下一步就开始迁移数据了,采用这种方式可以避免重复的数据迁移。 二、超时反模式 微服务是一种分布式的架构,它所有的组件(也就是服务)会被部署为单独的应用程序,并通过某种远程访问协议进行通讯。分布式应用的挑战之一就是如何管理远程服务的可用性和它们的响应。虽然服务可用性和服务响应都涉及到服务的通信,但它们是两个完全不同的东西。服务可用性是服务消费者连接服务并能够发送请求的能力,服务响应则关注服务的响应时间。 图2-1 如图2-1的所示,如果此时服务消费者无法连接到服务提供者的时候,通过会在毫秒级的时间里得到通知和反馈,这时候服务消费者可以选择是直接返回错误信息还是进行重试,但是如果服务提供者接收了请求却不进行响应该怎么办,在这种情况下服务消费者可以选择无限期等待或者设置超时时间,使用超时时间看起来是个好办法,但是它会导致超时反模式。 2.1 使用超时 你可能感觉非常困惑,难道设置一个超时时间不是一件好事吗?在大部分的情况下超时时间的错误设置都会带来问题。比如当你上网购物的时候,你提交了订单,服务一直在处理没有返回,你在超时的时候再提交订单,显然服务器需要更复杂的逻辑来处理重复提交订单的问题。 那么超时时间设置多少合适呢? 第一种是基于数据库的超时来计算服务的超时时间。 第二种是计算负载下最长的处理时间,把它乘以2作为超时时间。 在图2-2中,通常的情况下平均响应时间是2秒,在高并发的情况下最长时间是5秒,因为可以使用加倍技术服务的超时时间设置为10秒。 图2-2 图2-2的解决方案似乎看起来很完美,它使每一个服务消费者必须等待10秒,其实只是为了判断服务没有响应。在大多数情况下,用户在等待提交按钮或放弃和关闭屏幕之前不会等待超过2到3秒。那就必须要有更好的办法来解决。 2.2 使用断路器模式 与上面超时的方法相比,使用断路器的方式更为稳妥,这种设计模式就像家里的电器的保险丝一样,当负载过大,或者电路发生故障或异常时,电流会不断升高,为防止升高的电流有可能损坏电路中的某些重要器件或贵重器件,烧毁电路甚至造成火灾。保险丝会在电流异常升高到一定的高度和热度的时候,自身熔断切断电流,从而起到保护电路安全运行的作用。 图2-3说明了断路器模式是如何工作的,当服务保持响应时,断路器将关闭,允许通过请求。如果远程服务突然变得不能响应,断路器就会打开,从而阻止请求通过,直到服务再次响应。当然这并不像你家中的保险丝,断路器本身可以持续监测服务。 图2-3 断路器模式相比设置超时的优点是,使用者可以立即知道服务已变得不响应,而不必等待超时,使用者将在毫秒内服务不响应,而不是等待10秒获得相同的信息。 另外断路器可以通过几种方式进行监控,最简单的方法是对远程服务进行简单的心跳检查,这种方式只是告诉断路器服务是活的,但是要想获取服务存活的详细信息,就需要定期(比如10秒)获取一次服务的详细信息,还有一种方式是实时用户监控,这种方式可以动态调整,一旦达到阈值,断路器可以进入半开放状态,可以设置一定数量的请求是通过(说1的10)。 三、共享反模式 微服务是一种无共享的架构,我更倾向于叫它为“尽量不共享”模式(share-as-little-as-possible), 因为总有一些代码会在微服务之间共享。比如不提供一个身份验证的微服务,而是将身份验证的代码打包成一个jar文件:security.jar,其它服务都能使用。如果安全检查是服务级别的功能,每个服务接收到请求都会检查安全性,这种方式可以很好的提高性能。 然后如果太过频繁的使用最终会出现依赖噩梦,如图3-1所示,其中每个服务都依赖于多个自定义共享库。 图3-1 这种共享级别不仅破坏了每个服务的限界上下文,而且还引入了几个问题,包括整体可靠性、更改控制、可测试性和部署能力。 3.1 过多依赖 在面向对象的软件开发过程中,经常会遇到共享的问题,特别是从单一分层结构迁移到微服务结构时,图3-2展示抽象类和共享,它们最终在多数单块分层体系结构中共享。 图3-2 创建抽象类和接口是面向对象编程的最重要做法,那我们如何来处理数百个服务共享的代码? 微服务架构的主要目标就是共享要尽可能的少,这有助于维护服务的限界上下文,使我们能够快速的测试和布署。服务之间依赖越强,服务隔离也就越困难,因此也就越难单独进行测试和布署。 3.2 共享代码的技术 要避免这个反模式的最好办法就是代码不共享,但是实际工作中总会有一些代码需要进行共享,那这些共享代码应该放到哪里呢? 图3-3给了四个最基本的技术: 共享项目 共享库 复制 服务合并 图3-3 四、到达报告反模式 有四种方式可以处理微服务架构中的报告。 database pull model HTTP pull model batch pull model event-based push model 前三种模式是从服务的数据库中拉取数据,所以这个反模式就叫"rearch-in reporting"。既然前三种会出现这中反模式,我们就先看看为什么它们会带来麻烦。 4.1 微服务报告的问题 主要是二个方面的问题: 如何及时获取最新数据 保持服务与数据之间的限界上下文 在微服务架构体系中第一种是使用数据库拉取模型,使用者直接从服务的数据库拉取数据,如图4-1所示: 图4-1 其实获取数据最快、最容易的方法是直接访问数据。虽然这在以前看似乎是个好主意,但它导致了服务之间的明显依赖关系。而上图会带来数据库的非独立性。 避免数据的耦合的另一种技术称为HTTP拉取模型。使用此模型不需要直接访问每个服务的数据库,使用者只需要对每个服务发出一个REST HTTP调用就可以访问其数据。如图4-2所示。 图4-2 这种方式的优点是根据限界上下文划分出了不同服务,但是这种方式又太慢,无法满足复杂的以及数据量较大数据获取需求。 第三种是批量拉取模式,这种方式是独立出一个报表数据库或者数据仓库,通过批处理作业将不同服务数据库的数据拉取这个新独立的数据库中,如图4-3所示。 图4-3 这种模型的问题在于依然是强依赖数据库,如果拉取服务的数据库进行了更新,那么这个批量数据拉取过程也必将修改。 最后一种是异步事件模型,也是推荐使用的模型,如图4-4所示 图4-4 五、沙粒陷阱 架构师和开发人员在采用微服务架构的时候最大的挑战之一就是服务粒度的问题。微服务的服务粒度多大合适?服务粒度至关重要,它会影响应用的性能、健壮性、可靠性、可测性、设置发布模型。 当服务的粒度太小的时候就会遇到沙粒陷阱。微服务的微并不意味着服务越小越好,但是多小是小? 这种陷阱的主要原因之一是开发人员常常将服务与类混淆,往往一个类就是一个服务,在这种情况下会很容易遇到沙粒陷阱。 服务应该被看成是一个服务组件,服务组件应该有一个清晰简明的角色和责任定义,并有一组明确的操作。由开发人员决定服务组件应该如何实现以及服务需要多少个实现类。 如图5-1所示,服务组件是通过一个或多个模块的实现(比如,java类)。模型和服务组件如果是一对一的关系会使服务的粒度过细而后期难以维护,而通过一个类实现的服务往往类太大,承担太多的责任,也使它们难以维护和测试。 图5-1 当然微服务的粒度并不是靠服务实现的类的数量所决定的,有些服务很简单,只需一个简单的类就可以实现,而有些确需要更多的类。既然类的数量不能用来决定微服务的粒度,那么用什么标准来衡量微服务的粒度是合适的呢? 主要有三种方式: 服务的范围(scope)和功能(functionality) 数据库事务的需求 服务编排的级别。 5.1 分析服务的范围和功能 确定服务粒度级别是否正确的第一种方法是分析服务的范围和功能。服务是做什么的?它的操作是什么? 比如一个顾客服务(customer service)有下面的操作: add_customer update_customer get_customer notify_customer record_customer_comments get_customer_comments 在这个例子中前三个操作是相关的,它们都是用来管理和维护顾客信息的,但是后面三个并不是和CRUD操作相关的。在分析这个服务的完整性的时候,我们就比较清晰了,这个服务可以被分成三个服务:顾客信息服务、顾客通知服务和顾客评论服务。 图5-1 正是一种由粗粒度服务向细粒度服务逐步演进的过程。 图5-1 Sam Newman提供了一个很好的可操作的方法,开始不妨将服务划分成粗粒度的服务,随着对服务了解更多,再进一步划分成更小粒度的服务。 5.2 分析数据库事务 数据库事务更正式的叫做 ACID 事务 (atomicity, consistency, isolation, and durability)。ACID事务封装多个数据库更新为一个工作单元,工作单元要不整体完成,要不就出现错误而回滚。 因为微服务架构中服务是分布式的独立的应用,再两个或者多个服务之间维护 ACID 事务就极度困难,所以微服务架构中经常会依赖 BASE (basic availability, soft state, and eventual consistency)。尽管如此,你还是再特定的服务中要使用 ACID 事务。当你需要在 ACID vs. BASE 事务中做艰难的决定的时候,可能你的服务划分的就太细了。 当发现不能使用最终一致性时,你通常就会把服务从细粒度调整为粗粒度的服务,如图5-2所示。 图5-2 5.3 分析服务编排 第三个衡量方式是分析服务编排。服务编排是指服务之间的通讯,通常也指内部服务通讯。 远程调用服务是需要花时间的,它会降低应用整体的性能。再者,它也会影响服务的健壮性和可靠性。 如果你发现完成一个逻辑请求需要调用太多的服务时,服务的划分可能粒度就太小了,对于单个业务请求,你调用的远程调用越多,其中一个远程调用失败或超时的可能性就越大。 如果你发现需要与太多的服务进行通信以完成单个业务请求,那么你的的服务可能粒度过细了。在分析服务编排水平,你通常会从细粒度的服务迁移到更粗,如图5-4所示。 图5-4 通过整合服务、合并到更粗粒度可以提升应用的整体性能,提高应用的健壮性和可靠性。你还可以移除服务之间的依赖,可以更好的控制、测试和发布。 当然你可能会说调用多个服务可以并行的执行,提高整体应用的的响应时间,比如 reactive 架构的异步编程方式, 其实关键还是要权衡利弊, 确保对用户的及时响应以及系统整体的可靠性。
image.png 利用早上一小段时间配置了一个demo,目的是帮助大家配置入门,随后我会非常详细的写一些使用和源码分析的文章。 链接:http://pan.baidu.com/s/1nuCrnAh 密码:aldn
image.png image.png image.png
image.png 注:本文转自好友吴晟的两篇译文 ,译文原文如下:https://github.com/opentracing-contrib/opentracing-specification-zh/blob/master/semantic_conventions.mdhttps://github.com/opentracing-contrib/opentracing-specification-zh/blob/master/specification.md 一、 前言 什么是OpenTracing? OpenTracing(http://opentracing.io/)是分布式跟踪系统,当我们把系统拆成服务化,分布式系统的时候,查询一个问题,很可能需要多个登录多台机器。 OpenTracing通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。OpenTracing正在为全球的分布式追踪,提供统一的概念和数据标准。 关于OpenTracing的一个Java版本的实现请参考如下代码: https://github.com/opentracing/opentracing-java 二、语义惯例 OpenTracing标准 描述的语言无关的数据模型,以及OpenTracing API的使用方法。在此数据模型中,包含了两个相关的概念 Span Tag 和 (结构化的) Log Field,尽管在标准中,已经明确了这些操作,但没有定义Span的tag和logging操作时,key的使用规范。 这些语义习惯通过这篇文档进行描述。这篇文档包括两个部分:一. 通过表格罗列出所有的tag和logging操作时,标准的key值。二.描述在特定的典型场景中,如何组合使用这些标准的key值,进行建模。 版本命名策略 修改此文件,将影响到OpenTracing标准的版本号。增加内容会增加小版本号,不向前兼容的改变(或新增大量内容)会增加大版本号。 标准的Span tag 和 log field Span tag 清单 Span的tag作用于 整个Span,也就是说,它会覆盖Span的整个事件周期,所以无需指定特别的时间戳。对于由时间点特性的事件,最好使用Span的log操作进行记录。(在本文档的下一章中进行描述)。 Span tag 名称 类型 描述与实例 component string 生成此Span所相关的软件包,框架,类库或模块。如 "grpc", "django", "JDBI". db.instance string 数据库实例名称。以Java为例,如果 jdbc.url="jdbc:mysql://127.0.0.1:3306/customers",实例名为 "customers". db.statement string 一个针对给定数据库类型的数据库访问语句。例如, 针对数据库类型 db.type="sql",语句可能是 "SELECT * FROM wuser_table"; 针对数据库类型为 db.type="redis",语句可能是 "SET mykey 'WuValue'". db.type string 数据库类型。对于任何支持SQL的数据库,取值为 "sql". 否则,使用小写的数据类型名称,如 "cassandra", "hbase", or "redis". db.user string 访问数据库的用户名。如 "readonly_user" 或 "reporting_user" error bool 设置为true,说明整个Span失败。译者注:Span内发生异常不等于error=true,这里由被监控的应用系统决定 http.method string Span相关的HTTP请求方法。例如 "GET", "POST" http.status_code integer Span相关的HTTP返回码。例如 200, 503, 404 http.url string 被处理的trace片段锁对应的请求URL。 例如 "https://domain.net/path/to?resource=here" message_bus.destination string 消息投递或交换的地址。例如,在Kafka中,在生产者或消费者两端,可以使用此tag来存储"topic name"。 peer.address string 远程地址。 适合在网络调用的客户端使用。存储的内容可能是"ip:port", "hostname",域名,甚至是一个JDBC的连接串,如 "mysql://prod-db:3306" peer.hostname string 远端主机名。例如 "opentracing.io", "internal.dns.name" peer.ipv4 string 远端 IPv4 地址,使用 . 分隔。例如 "127.0.0.1" peer.ipv6 string 远程 IPv6 地址,使用冒号分隔的元祖,每个元素为4位16进制数。例如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334" peer.port integer 远程端口。如 80 peer.service string 远程服务名(针对没有被标准化定义的"service")。例如 "elasticsearch", "a_custom_microservice", "memcache" sampling.priority integer 如果大于0,Tracer实现应该尽可能捕捉这个调用链。如果等于0,则表示不需要捕捉此调用链。如不存在,Tracer使用自己默认的采样机制。 span.kind string 基于RPC的调用角色,"client" 或 "server". 基于消息的调用角色,"producer" 或 "consumer" Log field 清单 每个Span的log操作,都具有一个特定的时间戳(这个时间戳必须在Span的开始时间和结束时间之间),并包含一个或多个 field。下面是标准的field。 Span log field 名称 类型 描述和实例 error.kind string 错误类型(仅在event="error"时使用)。如 "Exception", "OSError" error.object object 如果当前语言支持异常对象(如 Java, Python),则为实际的Throwable/Exception/Error对象实例本身。例如 一个 java.lang.UnsupportedOperationException 实例, 一个python的 exceptions.NameError 实例 event string Span生命周期中,特定时刻的标识。例如,一个互斥锁的获取与释放,或 在Performance.timing 规范中描述的,浏览器页面加载过程中的各个事件。 还例如,Zipkin中 "cs", "sr", "ss", 或 "cr". 或者其他更抽象的 "initialized" 或 "timed out"。出现错误时,设置为 "error" message string 简洁的,具有高可读性的一行事件描述。如 "Could not connect to backend", "Cache invalidation succeeded" stack string 针对特定平台的栈信息描述,不强制要求与错误相关。如 "File \"example.py\", line 7, in \<module\>\ncaller()\nFile \"example.py\", line 5, in caller\ncallee()\nFile \"example.py\", line 2, in callee\nraise Exception(\"Yikes\")\n" 典型场景建模 RPCs 使用下面tag为RPC调用建模: span.kind: "client" 或 "server"。在Span开始时,设置此tag是十分重要的,它可能影响内部ID的生成。 error: RPC调用是否发生错误 peer.address, peer.hostname, peer.ipv4, peer.ipv6, peer.port, peer.service: 可选tag。描述RPC的对端信息。(一般只有在无法获取到这些信息时,才不设置这些值) Message Bus 消息服务是一个异步调用,所以消费端的Span和生产端的Span使用 Follows From 关系。(查看 Span间关系) 使用下面tag为消息服务建模: message_bus.destination: 上表已描述 span.kind: "producer" 或 "consumer". 建议 在span开始时 设置此tag,它可能影响内部ID的生成。 peer.address, peer.hostname, peer.ipv4, peer.ipv6, peer.port, peer.service: 可选tag,描述消息服务中broker的地址。(可能在内部无法获取) Database (client) calls 使用下面tag为数据库客户端调用建模: db.type, db.instance, db.user, 和 db.statement: 上表已描述 peer.address, peer.hostname, peer.ipv4, peer.ipv6, peer.port, peer.service: 描述数据库信息的可选tag span.kind: "client" Captured errors,捕获错误 OpenTracing中,根据语言的不同,错误可以通过不同的方式来进行描述,有一些field是专门针对错误输出的,其他则不是(例如:event 或 message) 如果存在错误对象,它其中包含栈信息和错误信息,log时使用如下的field: event="error" error.object=<error object instance> 对于其他语言(译者注:不存在上述的错误对象),或上述操作不可行时: event="error" message="..." stack="..." (可选) error.kind="..." (可选) 通过此方案,Tracer实现可以在需要时,获取所需的错误信息。 三、OpenTracing语义标准 版本号: 1.1 综述 这是正式的OpenTracing语义标准。OpenTracing是一个跨编程语言的标准,此文档会避免具有语言特性的概念。比如,我们在文档中使用"interface",因为所有的语言都包含"interface"这种概念。 版本命名策略 OpenTracing标准使用Major.Minor版本命名策略(即:大版本.小版本),但不包含.Patch版本(即:补丁版本)。如果标准做出不向前兼容的改变,则使用“主版本”号提升。如果是向前兼容的改进,则进行小版本号提升,例如加入新的标准tag, log和SpanContext引用类型。(如果你想知道更多关于制定此版本政策的原因,可参考specification#2) OpenTracing数据模型 OpenTracing中的Trace(调用链)通过归属于此调用链的Span来隐性的定义。 特别说明,一条Trace(调用链)可以被认为是一个由多个Span组成的有向无环图(DAG图),Span与Span的关系被命名为References。 译者注: Span,可以被翻译为跨度,可以被理解为一次方法调用, 一个程序块的调用, 或者一次RPC/数据库访问.只要是一个具有完整时间周期的程序访问,都可以被认为是一个span.在此译本中,为了便于理解,Span和其他标准内声明的词汇,全部不做名词翻译。 例如:下面的示例Trace就是由8个Span组成: 单个Trace中,span间的因果关系 [Span A] ←←←(the root span) | +------+------+ | | [Span B] [Span C] ←←←(Span C 是 Span A 的孩子节点, ChildOf) | | [Span D] +---+-------+ | | [Span E] [Span F] >>> [Span G] >>> [Span H] ↑ ↑ ↑ (Span G 在 Span F 后被调用, FollowsFrom) 有些时候,使用下面这种,基于时间轴的时序图可以更好的展现Trace(调用链): 单个Trace中,span间的时间关系 ––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time [Span A···················································] [Span B··············································] [Span D··········································] [Span C········································] [Span E·······] [Span F··] [Span G··] [Span H··] 每个Span包含以下的状态:(译者注:由于这些状态会反映在OpenTracing API中,所以会保留部分英文说明) An operation name,操作名称 A start timestamp,起始时间 A finish timestamp,结束时间 Span Tag,一组键值对构成的Span标签集合。键值对中,键必须为string,值可以是字符串,布尔,或者数字类型。 Span Log,一组span的日志集合。 每次log操作包含一个键值对,以及一个时间戳。 键值对中,键必须为string,值可以是任意类型。 但是需要注意,不是所有的支持OpenTracing的Tracer,都需要支持所有的值类型。 SpanContext,Span上下文对象 (下面会详细说明) References(Span间关系),相关的零个或者多个Span(Span间通过SpanContext建立这种关系) 每一个SpanContext包含以下状态: 任何一个OpenTracing的实现,都需要将当前调用链的状态(例如:trace和span的id),依赖一个独特的Span去跨进程边界传输 Baggage Items,Trace的随行数据,是一个键值对集合,它存在于trace中,也需要跨进程边界传输 Span间关系 一个Span可以与一个或者多个SpanContexts存在因果关系。OpenTracing目前定义了两种关系:ChildOf(父子) 和 FollowsFrom(跟随)。这两种关系明确的给出了两个父子关系的Span的因果模型。 将来,OpenTracing可能提供非因果关系的span间关系。(例如:span被批量处理,span被阻塞在同一个队列中,等等)。 ChildOf 引用: 一个span可能是一个父级span的孩子,即"ChildOf"关系。在"ChildOf"引用关系下,父级span某种程度上取决于子span。下面这些情况会构成"ChildOf"关系: 一个RPC调用的服务端的span,和RPC服务客户端的span构成ChildOf关系 一个sql insert操作的span,和ORM的save方法的span构成ChildOf关系 很多span可以并行工作(或者分布式工作)都可能是一个父级的span的子项,他会合并所有子span的执行结果,并在指定期限内返回 下面都是合理的表述一个"ChildOf"关系的父子节点关系的时序图。 [-Parent Span---------] [-Child Span----] [-Parent Span--------------] [-Child Span A----] [-Child Span B----] [-Child Span C----] [-Child Span D---------------] [-Child Span E----] FollowsFrom 引用: 一些父级节点不以任何方式依赖他们子节点的执行结果,这种情况下,我们说这些子span和父span之间是"FollowsFrom"的因果关系。"FollowsFrom"关系可以被分为很多不同的子类型,未来版本的OpenTracing中将正式的区分这些类型 下面都是合理的表述一个"FollowFrom"关系的父子节点关系的时序图。 [-Parent Span-] [-Child Span-] [-Parent Span--] [-Child Span-] [-Parent Span-] [-Child Span-] OpenTracing API OpenTracing标准中有三个重要的相互关联的类型,分别是Tracer, Span 和 SpanContext。下面,我们分别描述每种类型的行为,一般来说,每个行为都会在各语言实现层面上,会演变成一个方法,而实际上由于方法重载,很可能演变成一系列相似的方法。 当我们讨论“可选”参数时,需要强调的是,不同的语言针对可选参数有不同理解,概念和实现方式 。例如,在Go中,我们习惯使用"functional Options",而在Java中,我们可能使用builder模式。 Tracer Tracer接口用来创建Span,以及处理如何处理Inject(serialize) 和 Extract (deserialize),用于跨进程边界传递。它具有如下官方能力: 创建一个新Span 必填参数 operation name, 操作名, 一个具有可读性的字符串,代表这个span所做的工作(例如:RPC方法名,方法名,或者一个大型计算中的某个阶段或子任务)。操作名应该是一个抽象、通用,明确、具有统计意义的名称。因此,"get_user" 作为操作名,比 "get_user/314159"更好。 例如,假设一个获取账户信息的span会有如下可能的名称: 操作名 指导意见 get 太抽象 get_account/792 太明确 get_account 正确的操作名,关于account_id=792的信息应该使用Tag操作 可选参数 零个或者多个关联(references)的SpanContext,如果可能,同时快速指定关系类型,ChildOf 还是 FollowsFrom。 一个可选的显性传递的开始时间;如果忽略,当前时间被用作开始时间。 零个或者多个tag。 返回值,返回一个已经启动Span实例(已启动,但未结束。译者注:英语上started和finished理解容易混淆) 将SpanContext上下文Inject(注入)到carrier 必填参数 SpanContext实例 format(格式化)描述,一般会是一个字符串常量,但不做强制要求。通过此描述,通知Tracer实现,如何对SpanContext进行编码放入到carrier中。 carrier,根据format确定。Tracer实现根据format声明的格式,将SpanContext序列化到carrier对象中。 将SpanContext上下文从carrier中Extract(提取) 必填参数 format(格式化)描述,一般会是一个字符串常量,但不做强制要求。通过此描述,通知Tracer实现,如何从carrier中解码SpanContext。 carrier,根据format确定。Tracer实现根据format声明的格式,从carrier中解码SpanContext。 返回值,返回一个SpanContext实例,可以使用这个SpanContext实例,通过Tracer创建新的Span。 注意,对于Inject(注入)和Extract(提取),format是必须的。 Inject(注入)和Extract(提取)依赖于可扩展的format参数。format参数规定了另一个参数"carrier"的类型,同时约束了"carrier"中SpanContext是如何编码的。所有的Tracer实现,都必须支持下面的format。 Text Map: 基于字符串:字符串的map,对于key和value不约束字符集。 HTTP Headers: 适合作为HTTP头信息的,基于字符串:字符串的map。(RFC 7230.在工程实践中,如何处理HTTP头具有多样性,强烈建议tracer的使用者谨慎使用HTTP头的键值空间和转义符) Binary: 一个简单的二进制大对象,记录SpanContext的信息。 Span 当Span结束后(span.finish()),除了通过Span获取SpanContext外,下列其他所有方法都不允许被调用。 除了通过Span获取SpanContext 不需要任何参数。 返回值,Span构建时传入的SpanContext。这个返回值在Span结束后(span.finish()),依然可以使用。 复写操作名(operation name) 必填参数 新的操作名operation name,覆盖构建Span时,传入的操作名。 结束Span 可选参数 一个明确的完成时间;如果省略此参数,使用当前时间作为完成时间。 为Span设置tag 必填参数 tag key,必须是string类型 tag value,类型为字符串,布尔或者数字 注意,OpenTracing标准包含"standard tags,标准Tag",此文档中定义了Tag的标准含义。 Log结构化数据 必填参数 一个或者多个键值对,其中键必须是字符串类型,值可以是任意类型。某些OpenTracing实现,可能支持更多的log值类型。 可选参数 一个明确的时间戳。如果指定时间戳,那么它必须在span的开始和结束时间之内。 注意,OpenTracing标准包含"standard log keys,标准log的键",此文档中定义了这些键的标准含义。 设置一个baggage(随行数据)元素 Baggage元素是一个键值对集合,将这些值设置给给定的Span,Span的SpanContext,以及所有和此Span有直接或者间接关系的本地Span。 也就是说,baggage元素随trace一起保持在带内传递。(译者注:带内传递,在这里指,随应用程序调用过程一起传递) Baggage元素为OpenTracing的实现全栈集成,提供了强大的功能 (例如:任意的应用程序数据,可以在移动端创建它,显然的,它会一直传递了系统最底层的存储系统。由于它如此强大的功能,他也会产生巨大的开销,请小心使用此特性。 再次强调,请谨慎使用此特性。每一个键值都会被拷贝到每一个本地和远程的下级相关的span中,因此,总体上,他会有明显的网络和CPU开销。 必填参数 baggage key, 字符串类型 baggage value, 字符串类型 获取一个baggage元素 必填参数 baggage key, 字符串类型 返回值,相应的baggage value,或者可以标识元素值不存在的返回值(译者注:如Null)。 SpanContext 相对于OpenTracing中其他的功能,SpanContext更多的是一个“概念”。也就是说,OpenTracing实现中,需要重点考虑,并提供一套自己的API。 OpenTracing的使用者仅仅需要,在创建span、向传输协议Inject(注入)和从传输协议中Extract(提取)时,使用SpanContext和references, OpenTracing要求,SpanContext是不可变的,目的是防止由于Span的结束和相互关系,造成的复杂生命周期问题。 遍历所有的baggage元素 遍历模型依赖于语言,实现方式可能不一致。在语义上,要求调用者可以通过给定的SpanContext实例,高效的遍历所有的baggage元素 NoopTracer 所有的OpenTracing API实现,必须提供某种方式的NoopTracer实现。NoopTracer可以被用作控制或者测试时,进行无害的inject注入(等等)。例如,在 OpenTracing-Java实现中,NoopTracer在他自己的模块中。 可选 API 元素 有些语言的OpenTracing实现,为了在串行处理中,传递活跃的Span或SpanContext,提供了一些工具类。例如,opentracing-go中,通过context.Context机制,可以设置和获取活跃的Span。
image.png 一、问题描述 你是山西的一个煤老板,你在矿区开采了有3000吨煤需要运送到市场上去卖,从你的矿区到市场有1000公里,你手里有一列烧煤的火车,这个火车最多只能装1000吨煤,且其能耗比较大――每一公里需要耗一吨煤。 请问,作为一个懂编程的煤老板的你,你会怎么运送才能运最多的煤到集市? 二、思考过程 这道题一开始看上去好像是无解的,因为你的火车每一公里就要消耗一吨煤,而到目的地有1000公里,而火车最多只能装1000吨媒。如果你的火车可以全部装下,到目的地也会被全部烧光,一丁点也不剩。所以,很多人的第一反应都是觉得这个不太可能。 三、结论: 装1000吨煤,走250公里,扔下500吨煤,回矿山。 装1000吨煤,走到250公里处,拿起250吨煤继续向前到500公里处,扔下500吨煤,回矿山。此时火车上还有250吨,再加上在250公里处还有250吨煤,所以,火车是可以回矿山的。 装上最后1000吨煤,走到500公里处,装上那里的500吨煤,然后一直走到目的。 同学们一定还有更好的方案,请集思广益!!!
image.png 一、Mysql数据库双活 1、Mysql binlog介绍 官网:https://dev.mysql.com/doc/refman/5.5/en/binary-log.html 中文: http://www.cnblogs.com/Richardzhu/p/3225254.html http://www.cnblogs.com/martinzhang/p/3454358.html 2、阿里数据库同步工具Canal 官网:https://github.com/alibabatech/canal/ 使用介绍: 阿里巴巴开源项目: canal 基于mysql数据库binlog的增量订阅&消费 利用Canal完成Mysql数据同步Redis 搭建: canal部署与实例运行 MySQL增量订阅&消费组件Canal POC 分布式集群项目中同步DB数据的解决方案之Canal Canal源码分析---模拟Slave同步binlog 用 Canal 监控mysql数据变化 使用 Binlog 和 Canal 从 MySQL 抽取数据 3、Mysql数据双活 sql高可用架构方案之中的一个(keepalived+主主双活) 大话数据中心双活 二、Oracle双活 1、Kettle实现数据同步 在Kettle里使用时间戳实现变化数据捕获(CDC) kettle参数、变量详细讲解 实时同步,增量ETL方案分享(源表含时间戳,做逻辑删除) Kettle Spoon 表输入组件报“无效的列索引”错误解决方案 关于Kettle的推荐书籍如下: image.png 2、使用Databus实现数据库同步 LinkedIn实时低延迟数据抓取系统Databus开源 Databus中文教程 Databus项目开源地址 3、Oracle日志数据同步 基于Oracle Logminer数据同步 OGG实现两台Oracle数据库的同步 基于OGG的Oracle与Hadoop集群准实时同步介绍 ORACLE 几种同步灾备手段(OGG,ADG,DSG,高级复制,流复制,logmnr) Oracle11gR2下搭建DataGuard主备同步详解
在项目迭代的过程中,不可避免需要”上线“。上线对应着部署,或者重新部署;部署对应着修改;修改则意味着风险。 目前有很多用于部署的技术,有的简单,有的复杂;有的得停机,有的不需要停机即可完成部署。本文的目的就是将目前常用的布署方案做一个总结。 一、蓝绿布署 Blue/Green Deployment(蓝绿部署) 1、定义 蓝绿部署是不停老版本,部署新版本然后进行测试,确认OK,将流量切到新版本,然后老版本同时也升级到新版本。 1、特点 蓝绿部署无需停机,并且风险较小。 2、布署过程 第一步、部署版本1的应用(一开始的状态) 所有外部请求的流量都打到这个版本上。 image.png 第二步、部署版本2的应用 版本2的代码与版本1不同(新功能、Bug修复等)。 第三步、将流量从版本1切换到版本2。 image.png 第四步、如版本2测试正常,就删除版本1正在使用的资源(例如实例),从此正式用版本2。 3、小结 从过程不难发现,在部署的过程中,我们的应用始终在线。并且,新版本上线的过程中,并没有修改老版本的任何内容,在部署期间,老版本的状态不受影响。这样风险很小,并且,只要老版本的资源不被删除,理论上,我们可以在任何时间回滚到老版本。 4、蓝绿发布的注意事项 当你切换到蓝色环境时,需要妥当处理未完成的业务和新的业务。如果你的数据库后端无法处理,会是一个比较麻烦的问题; 可能会出现需要同时处理“微服务架构应用”和“传统架构应用”的情况,如果在蓝绿部署中协调不好这两者,还是有可能会导致服务停止。 需要提前考虑数据库与应用部署同步迁移 /回滚的问题。 蓝绿部署需要有基础设施支持。 在非隔离基础架构( VM 、 Docker 等)上执行蓝绿部署,蓝色环境和绿色环境有被摧毁的风险。 二、Rolling update(滚动发布) 1、滚动发布定义 滚动发布:一般是取出一个或者多个服务器停止服务,执行更新,并重新将其投入使用。周而复始,直到集群中所有的实例都更新成新版本。 2、特点 这种部署方式相对于蓝绿部署,更加节约资源——它不需要运行两个集群、两倍的实例数。我们可以部分部署,例如每次只取出集群的20%进行升级。 这种方式也有很多缺点,例如: (1) 没有一个确定OK的环境。使用蓝绿部署,我们能够清晰地知道老版本是OK的,而使用滚动发布,我们无法确定。 (2) 修改了现有的环境。 (3) 如果需要回滚,很困难。举个例子,在某一次发布中,我们需要更新100个实例,每次更新10个实例,每次部署需要5分钟。当滚动发布到第80个实例时,发现了问题,需要回滚,这个回滚却是一个痛苦,并且漫长的过程。 (4) 有的时候,我们还可能对系统进行动态伸缩,如果部署期间,系统自动扩容/缩容了,我们还需判断到底哪个节点使用的是哪个代码。尽管有一些自动化的运维工具,但是依然令人心惊胆战。 (5) 因为是逐步更新,那么我们在上线代码的时候,就会短暂出现新老版本不一致的情况,如果对上线要求较高的场景,那么就需要考虑如何做好兼容的问题。 三、灰度发布/金丝雀部署 1、定义 灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度,而我们平常所说的金丝雀部署也就是灰度发布的一种方式。 注释:矿井中的金丝雀 17世纪,英国矿井工人发现,金丝雀对瓦斯这种气体十分敏感。空气中哪怕有极其微量的瓦斯,金丝雀也会停止歌唱;而当瓦斯含量超过一定限度时,虽然鲁钝的人类毫无察觉,金丝雀却早已毒发身亡。当时在采矿设备相对简陋的条件下,工人们每次下井都会带上一只金丝雀作为“瓦斯检测指标”,以便在危险状况下紧急撤离。 灰度发布结构图如下: image.png 2、灰度发布/金丝雀发布由以下几个步骤组成: 准备好部署各个阶段的工件,包括:构建工件,测试脚本,配置文件和部署清单文件。 从负载均衡列表中移除掉“金丝雀”服务器。 升级“金丝雀”应用(排掉原有流量并进行部署)。 对应用进行自动化测试。 将“金丝雀”服务器重新添加到负载均衡列表中(连通性和健康检查)。 如果“金丝雀”在线使用测试成功,升级剩余的其他服务器。(否则就回滚) 除此之外灰度发布还可以设置路由权重,动态调整不同的权重来进行新老版本的验证。 参考文章https://www.v2ex.com/t/344341
一、什么是ETL ETL(Extract-Transform-Load的缩写,即数据抽取、转换、装载的过程),对于企业或行业应用来说,我们经常会遇到各种数据的处理,转换,迁移,所以了解并掌握一种etl工具的使用,这里介绍一个ETL工具Kettle,这个工具很强大,支持图形化的GUI设计界面,然后可以以工作流的形式流转,在做一些简单或复杂的数据抽取、质量检测、数据清洗、数据转换、数据过滤等方面有着比较稳定的表现。 二、Kettle概念 Kettle是一款国外开源的ETL工具,纯java编写,可以在Window、Linux、Unix上运行,绿色无需安装,数据抽取高效稳定。 Kettle 中文名称叫水壶,该项目的主程序员MATT 希望把各种数据放到一个壶里,然后以一种指定的格式流出。 Kettle这个ETL工具集,它允许你管理来自不同数据库的数据,通过提供一个图形化的用户环境来描述你想做什么,而不是你想怎么做。 Kettle中有两种脚本文件,transformation和job,transformation完成针对数据的基础转换,job则完成整个工作流的控制。 Kettle可以在http://kettle.pentaho.org/网站下载。 三、Kettle的使用 要实现实时的增量更新共有两种方法: 1、通过触发器。 在要抽取的表上建立需要的触发器,一般要建立插入、修改、删除三个触发器,每当源表中的数据发生变化,就被相应的触发器将变化的数据写入一个临时 表,抽取线程从临时表中抽取数据,临时表中抽取过的数据被标记或删除。触发器方式的优点是数据抽取的性能较高,缺点是要求业务表建立触发器,对业务系统有 一定的影响。 2.通过时间戳。 可以在两边数据库的表里插入了一列(用来取数据变动时的时间),然后做个计划任务,设置每隔多少时间跑一次kettle,就行了。要实现“实时”,就只能把时间间隔设小一点。 通过时间戳方式 利用kettle工具,通过时间戳完成某表实时的增量更新。 利用模块完成即可,如下简图: image.png 准备一个实验环境: 准备基本完成如下效果:select t.*, t.rowid from EMP_ETL t image.png 编写时间戳ktr转换过程,把原表中最大的hiredate作为变量,如下图: image.png 下面在“表输入”环节中,使用传递过来的${MAXSJ}作为条件,完成向目标表插入数据,简要流程如下: image.png 执行转换,完成数据插入,简图如下: image.png 最后利用kettle形成的整体数据流如下图所示: image.png 下一篇文章,我将重点介绍基于Oracle的双活方案
关于Raft原理,许多朋友也许不是很明白原理,下面的地址是一个好玩的Raft动画,看完后能够很快的掌握Raft原理: http://thesecretlivesofdata.com/raft/ 动画中的一些概念和简要原理总结如下: image.png 一、Raft原理 在Raft中,每个结点会处于下面三种状态中的一种: 1、follower:所有结点都以follower的状态开始。如果没收到leader消息则会变成candidate状态。 2、candidate:会向其他结点“拉选票”,如果得到大部分的票则成为leader。这个过程就叫做Leader选举(Leader Election)。 3、leader:所有对系统的修改都会先经过leader。每个修改都会写一条日志(log entry)。leader收到修改请求后的过程如下,这个过程叫做日志复制(Log Replication): 复制日志到所有follower结点(replicate entry) 大部分结点响应时才提交日志 通知所有follower结点日志已提交 所有follower也提交日志 现在整个系统处于一致的状态 三种角色的状态转换关系如下: image.png 二、Leader Election 当follower在选举超时时间(election timeout)内未收到leader的心跳消息(append entries),则变成candidate状态。为了避免选举冲突,这个超时时间是一个150~300ms之间的随机数。 成为candidate的结点发起新的选举期(election term)去“拉选票”: 重置自己的计时器 投自己一票 发送 Request Vote消息 如果接收结点在新term内没有投过票那它就会投给此candidate,并重置它自己的选举超时时间。candidate拉到大部分选票就会成为leader,并定时发送心跳——Append Entries消息,去重置各个follower的计时器。当前Term会继续直到某个follower接收不到心跳并成为candidate。 如果不巧两个结点同时成为candidate都去“拉票”怎么办?这时会发生Splite Vote情况。两个结点可能都拉到了同样多的选票,难分胜负,选举失败,本term没有leader。之后又有计时器超时的follower会变成candidate,将term加一并开始新一轮的投票。 三、Log Replication 当发生改变时,leader会复制日志给follower结点,这也是通过Append Entries心跳消息完成的。前面已经列举了Log Replication的过程,这里就不重复了。 Raft能够正确地处理网络分区(“脑裂”)问题。假设A~E五个结点,B是leader。如果发生“脑裂”,A、B成为一个子分区,C、D、E成为一个子分区。此时C、D、E会发生选举,选出C作为新term的leader。这样我们在两个子分区内就有了不同term的两个leader。这时如果有客户端写A时,因为B无法复制日志到大部分follower所以日志处于uncommitted未提交状态。而同时另一个客户端对C的写操作却能够正确完成,因为C是新的leader,它只知道D和E。 当网络通信恢复,B能够发送心跳给C、D、E了,却发现“改朝换代”了,因为C的term值更大,所以B自动降格为follower。然后A和B都回滚未提交的日志,并从新leader那里复制最新的日志。
一、前言 我们在《微服务是在双刃剑 http://www.jianshu.com/p/82ec12651d2d 》中提到了当我们将应用服务化以后,很多在单块系统中能够开展的数据统计和分析业务将会受到很大程度的影响,本文将延续上一篇文章深入分析服务化后,作为后端的数据统计和分析如何做。 注:本文的数据库是基于Oracle数据库 二、服务化后的现状分析 拿一个简单的快捷支付系统为例,服务化后的系统调用图如下所示: image.png 通过上图我们可以看到,单块系统根据业务进行服务化后,每个系统功能单一、职责明确并且独立布署,这只是从系统的角度描述了服务化后的调用关系,那么从微服务的角度讲,还有一点是去中心化,也就是将数据库也按服务进行拆分,下图所示的正是每个服务与其对应的数据库间的关系。 image.png 上面我们可以看到,每个服务对应一个数据库,这样从上到下就已经全部拆分开了,再结合康威定律的理论,每个服务由一个团队负责管理,团队之间彼此协作和沟通。 三、数据抽取的技术选型 关于后台的数据统计需求,因为服务化后数据库已经拆分开,于是对后台数据统计造成了一定的困扰,针对这个问题我首先想到的是利用数据库同步来解决,将不同库或者表的数据统一汇总到一起。那么接下来,我将和大家一起逐步探讨和分析。 1、使用Oracle Golden Gate(简称OGG)工具 OGG的实现原理是抽取源端的redo log和archive log,然后通过TCP/IP协议投递到目标端,最后解析还原同步到目标端,使目标端实现源端的数据同步,如下图所示: image.png 1.1 使用OGG的优点: 1、对生产系统影响小:实时读取交易日志,以低资源占用实现大交易量数据实时复制。 2、以交易为单位复制,保证交易一致性:只同步已提交的数据。 3、灵活的拓扑结构:支持一对一、一对多、多对一、多对多和双向复制等。 4、可以自定义基于表和行的过滤规则,可以对实时数据执行灵活影射和变换。 1.2 使用OGG需要注意的问题点: 1、在二个库之间做数据同步的时候,如果我们要在表中新加字段,必须要将OGG停下来加字段,然后再启动,新字段同步才会生效。 2、使用OGG做数据同步的时候,工具不是很稳定,经常会出现假死或者退出的情况。 3、OGG偶尔出现在同步过程中丢数据的时候。 2、使用Oracle Logminer Logminer是oracle从8i开始提供的用于分析重做日志信息的工具,它包括DBMS_LOGMNR和DBMS_LOGMNR_D两个package,后边的D是字典的意思。它既能分析redo log file,也能分析归档后的archive log file。通过LogMiner可以跟踪Oracle数据库的所有DML、DDL和DCL操作。 2.1 使用LogMiner进行数据同步的框架图如下所示: image.png 2.2 数据同步流程图如下所示: image.png 同步流程说明:: 通过指定源端、目标端数据库信息、LogMiner 同步时间等配置信息,获取源端同步数据。 1、通过定时轮询的方式检测是否到达数据同步时间,如果是则进行数据同步,否则继续进行轮询。 2、定时加载数据库归档日志文件到动态表 v$logmnr_contents 中。 3、根据条件读取指定 sql 语句。 4、执行 sql 语句。 基于JAVA写的LogMiner的数据同步部分核心代码如下所示: try { ResultSet resultSet = null; // 获取源数据库连接 sourceConn = DataBase.getSourceDataBase(); Statement statement = sourceConn.createStatement(); // 添加所有日志文件,本代码仅分析联机日志 StringBuffer sbSQL = new StringBuffer(); sbSQL.append(" BEGIN"); sbSQL.append(" dbms_logmnr.add_logfile(logfilename=>'"+Constants.LOG_PATH+"\\REDO01.LOG', options=>dbms_logmnr.NEW);"); sbSQL.append(" dbms_logmnr.add_logfile(logfilename=>'"+Constants.LOG_PATH+"\\REDO02.LOG', options=>dbms_logmnr.ADDFILE);"); sbSQL.append(" dbms_logmnr.add_logfile(logfilename=>'"+Constants.LOG_PATH+"\\REDO03.LOG', options=>dbms_logmnr.ADDFILE);"); sbSQL.append(" END;"); CallableStatement callableStatement = sourceConn.prepareCall(sbSQL+""); callableStatement.execute(); // 打印获分析日志文件信息 resultSet = statement.executeQuery("SELECT db_name, thread_sqn, filename FROM v$logmnr_logs"); while(resultSet.next()) { System.out.println("已添加日志文件==>"+resultSet.getObject(3)); } System.out.println("开始分析日志文件,起始scn号:"+Constants.LAST_SCN); callableStatement = sourceConn.prepareCall("BEGINdbms_logmnr.start_logmnrstartScn=>'"+Constants.LAST_SCN+"',dictfilename=>'"+Constants.DATA_DICTIONARY+"\\dictionary.ora',OPTIONS =>DBMS_LOGMNR.COMMITTED_DATA_ONLY+dbms_logmnr.NO_ROWID_IN_STMT);END;"); callableStatement.execute(); System.out.println("完成分析日志文件"); // 查询获取分析结果 System.out.println("查询分析结果"); resultSet = statement.executeQuery("SELECT scn,operation,timestamp,status,sql_redo FROM v$logmnr_contents WHERE seg_owner='"+Constants.SOURCE_CLIENT_USERNAME+"' AND seg_type_name='TABLE' AND operation !='SELECT_FOR_UPDATE'"); // 连接到目标数据库,在目标数据库执行redo语句 targetConn = DataBase.getTargetDataBase(); Statement targetStatement = targetConn.createStatement(); String lastScn = Constants.LAST_SCN; String operation = null; String sql = null; boolean isCreateDictionary = false; while(resultSet.next()){ lastScn = resultSet.getObject(1)+""; if( lastScn.equals(Constants.LAST_SCN) ) { continue; } operation = resultSet.getObject(2)+""; if( "DDL".equalsIgnoreCase(operation) ) { isCreateDictionary = true; } sql = resultSet.getObject(5)+""; // 替换用户 sql = sql.replace("\""+Constants.SOURCE_CLIENT_USERNAME+"\".", ""); System.out.println("scn="+lastScn+",自动执行sql=="+sql+""); try { targetStatement.executeUpdate(sql.substring(0, sql.length()-1)); } catch (Exception e) { System.out.println("测试一下,已经执行过了"); } } // 更新scn Constants.LAST_SCN = (Integer.parseInt(lastScn))+""; // DDL发生变化,更新数据字典 if( isCreateDictionary ){ System.out.println("DDL发生变化,更新数据字典"); createDictionary(sourceConn); System.out.println("完成更新数据字典"); isCreateDictionary = false; } System.out.println("完成一个工作单元"); } finally { if( null != sourceConn ) { sourceConn.close(); } if( null != targetConn ) { targetConn.close(); } sourceConn = null; targetConn = null; } } 2.3 使用LogMiner做数据同步需要注意的点: 1、LogMiner是针对数据库级别的同步。 2、LogMiner工具的时效性较差,同步延时时间很长。 3、目标库必须与源库版本相同,或者比源库版本更高;目标库与源库字符集一致,或者是源库字符集的超集。 4、源数据库与目标库,必须运行在相同的硬件平台。 5、通过LogMiner方式获取日志的,通过oracle提供工具读取redo日志的信息,然后解析成SQL队列。有些特殊的数据类型,数据的变化是不记录到redo的,比如LOB字段的变化 3、总结 上面二种方案各有优缺点,但是实际工作中更需要同步延时小,同时稳定性极佳并且数据丢失率极低的方案,可以看到这二个方案并不适合做真正的数据抽取工具,来实现一个如下的方案: image.png 在下一文中,我将结合工作实战为大家介绍一款高效的数据库同步工具,最终解决微服务实施中所带来的数据统计的痛点。 注:本文参考了 http://www.cnblogs.com/shishanyuan/p/3142788.html
image.png 一、常用文档 Google Dapper 中文论文 http://bigbully.github.io/Dapper-translation/ 分布式追踪系统架构与设计 中文版:http://www.36dsj.com/archives/60241 英文版:https://tech.knewton.com/blog/2016/04/distributed-tracing-design-architecture/ OpenTracing官方标准-中文版 中文版:https://github.com/opentracing-contrib/opentracing-specification-zh 英文版:http://opentracing.io/documentation/pages/spec 二、常用开源工具介绍 Skywalking APM https://github.com/wu-sheng/sky-walking Twitter Zipkin 为一个分布式的调用链跟踪系统 http://zipkin.io/ 美团点评CAT https://github.com/dianping/cat 应用性能管理工具PinPoint https://github.com/naver/pinpoint Apache HTrace http://htrace.incubator.apache.org/
image.png 微服务是银弹吗?自2014年“微服务”一词真是越来越火,不谈Microservices彷佛就out了,那么我们先来看微服务具有哪些特点: 组件以服务的形式提供 围绕业务功能进行组织 强化终端与弱化管道 产品而不是项目 独立布署 单一职责 去中心化 DevOps与组织架构 我要讲的故事开始了 A公司的技术架构体系目前还是以集群扩展体系为主,我们可以看下图所示,在这种体系结构中,可以看到应用都是单块结构,但是单块结构的应用具有扩展性,通过布署在多个Tomcat上实现应用的集群,所有的应用都去访问同一个数据库(这个库可以假设为Oracle数据库),数据库间采用DataGuard来实现主从同步,读库只具有读取功能,为后台数据统计功能提供数据查询和统计服务。目前业务请求的并发量每分钟有几十笔交易,看起来这套架构还是能够支撑目前的业务发展的。 image.png 突然有一天客户在做活动的时候,监控中心各种告警,在每分钟500tps的时候很多请求超时,监控显示目前的服务器不能支撑这么大的并发量,于是快速增加服务器布署应用上线,发现根本没用,加了和没加一样,加几台都一样,运维和DBA发现此时的数据库压力非常大,好不容易熬过这段是时间后,团队成员痛定思痛一致认为,目前的架构体系已经不能支持业务的发展,微服务开始快速推进。 其中微服务的数据去中心化核心要点是: 每个微服务有自己私有的数据库持久化业务数据。 每个微服务只能访问自己的数据库,而不能访问其它服务的数据库。 某些业务场景下,需要在一个事务中更新多个数据库。这种情况也不能直接访问其它微服务的数据库,而是通过对于微服务进行操作。 数据的去中心化,进一步降低了微服务之间的耦合度。 最终经过服务化改进后,变成了如下图所示的样子: image.png 上图看起来是不是很棒,服务拆分是不是很清晰? 于是问题随后就来了: 1、以前团队一共就10个人只负责一二个项目,现在突然增加到平均每人维护二三个项目,上线还是采用由运维手工打war包上线,如果有修改的配置文件,则运维同学一台一台的进行修改,不仅容易上线出错,而且每次上线都会搞到半夜。 2、根据上面提到的数据去中心化原则,数据库拆分出来了,一个服务一个数据库实例,但是对后台统计系统来说就是恶梦,数据库拆分出来了统计工作、报表工作该怎么办呢?这部分工作还做不做呢?有人说可以分开统计啊,一个库一个库的来,可是这样的工作量将是巨大的。 3、机房的双活问题,对于金融公司来说双活还是很关键的一项技术指标,对于应用双活来说,其实还是比较容易实现,但是对于数据库来说确是一个技术问题了,对于oracle数据库来说,用oracle官方提供的OGG(Oracle GoldenGate)来进行数据同步的话,根据论坛上面查看的资料可以看出,OGG坑非常多,而且也容易丢数据,更重要的是贵。。。采用oracle的logminer来进行同步,同步的数据将不是实时的,会有一定延时而且在定时读取方面的工作上还需要自己进行开发,采用oracle的DataGuard也只能做主从同步,却不能做主主双活。于是通过调研过后,最终还是决定自己独立开发。 4、使用Dubbo或者Spring cloud就是微服务了吗?好吧,使用了Dubbo以后发现还有非常多的工作需要做,Dubbo只是一个服务治理框架而已,还需要开发分布式调用监控系统、统一配置管理中心,统一定时调度,还要在每个服务中做防重幂等,还要做并发限流,缓存也要根据不同的服务做隔离等等工作。。。 那我们用Spring cloud做一个大一统的整合可以吗?于是看到Spring cloud原来有这些坑啊: 注册IP问题 早期的Spring Cloud Eureka在注册获取网卡IP时,不能区分外网网卡和内网网卡,如果安装了虚拟机和docker也不能区分虚拟网卡,每次启动注册的IP都有可能不一样,如果要注册为外网网卡IP,那运行带宽就不够,这个bug应该说是比较严重的问题,因此重写了网卡IP获取的逻辑来解决,同时也反馈给了spring cloud团队,再后期的版本中添加了网卡接口排序和通过名称过滤的功能来得到解决。 HealthCheck的问题 在一些极小概率的情况下,会导致Eureka Server 下线微服务实例,出现“Remote status from Eureka server is down”的问题,即便是重启微服务也无济于事,不过已经有码友在spring cloud 官方github贴出了解决方法的issue。 Feign使用不当带来的性能问题 其他的小坑也就忍了,大坑却不能。。。。于是去各大社区讨论发现原来大家都对Cloud的不少组件进行了二次封装。。。 回顾一下 上面用了很大的篇幅各种吐槽,那么我们说微服务好吗?我一直坚持认为微服务很好,但是如果我们为了使用微服务而使用的话将会伤其自身,从单块系统到微服务的是需要逐步演进的过程,如果前期没有调研,没有一个整体规划,后期在做的时候会发现,需要做的事情只会越来越多,尤其是对于快速发展的创业型公司来说。另外针对项目上线的问题,根据微服务的发展来说使用devops进行CI和CD后就可以解决现有问题,还可以采用Docker解决容器化问题,但围绕微服务所做的周边工作确实巨大的工作量,换句话说,我们不懂Docker怎么办,这就需要大量的时间来做研究和试错,当然如果公司很有钱也可以购买这样的服务,总之成本是很高的。 就拿我上面举的例子来看,数据库自身压力大,经过分析看出其实是很多sql没有加索引,大量使用数据库悲观锁,大表的数据一直长期积累没有迁移出去所致。当单块系统遇到了性能问题后,如果认真分析了性能的根源,也许还会为我们做服务化演进争取了更多的时间。 最后想说一句,对于中小公司来说,如果业务发展非常快速,人员不足的情况下,我们更需要的是在业务发展和架构优化间做平衡,逐步演进,而不是快速使用。
image.png Java1.8 帮助文档 中文 – 谷歌版 在线版: https://blog.fondme.cn/apidoc/jdk-1.8-google/ 下载链接:http://download.csdn.net/detail/qw599186875/9802192 中文 – 必应版 下载链接:http://download.csdn.net/detail/qw599186875/9839280 中文 – 有道版 在线版: https://blog.fondme.cn/apidoc/jdk-1.8-youdao/ 下载链接:http://download.csdn.net/detail/qw599186875/9608721 中文 – 百度版 在线版: https://blog.fondme.cn/apidoc/jdk-1.8-baidu/ 下载链接:http://download.csdn.net/detail/qw599186875/9608724
一、前言 在之前的文章 http://www.jianshu.com/p/c128ed5c394e 中已经介绍了“自动化Mock系统0.9版本”,今天我将和大家一起探讨我们的“自动化Mock系统1.0版本”。 二、测试人员面临的测试问题 我公司目前用的是基于Dubbo的微服务改造,服务之间的调用链路冗长,每个服务又是单独的团队在维护,每个团队又在不断的演进和维护各个服务,那么对测试人员将是非常大的挑战。 测试人员每次进行功能测试的时候,测试用例每次都需要重新写一遍,无法将测试用例的数据沉淀,尤其是做自动化测试的时候,测试人员准备测试数据就需要很长时间,效率非常低。 目前接口自动化测试框架也多种多样,testng,junit,Fitnesse等,但都需要测试人员具备测试代码编写能力,如果要做好和手工接口测试一样效果的自动化测试更是需要大量的代码堆积,后期维护代码成本非常大。因此做成简单配置用例流,无需编写测试代码的系统是更贴合实际工作要求。 举个例子:拿互联网支付系统来说,某个团队新增了支付交易的需求,这时候要进行测试,测试人员除了要测试支付交易需求本身是否正确,同时也要结合上下游的服务整体进行回归测试,这时候开发人员往往在支付交易系统中采用“硬编码”的方式对上下游的系统进行“挡板”,如果测试人员对测试数据有所调整那么“挡板”也要跟着调整,同时在项目正式上线的时候,如果开发人员没有将“挡板”程序去除干净,将面临严重的线上问题。 三、Dubbo的Mock功能 1、Dubbo的Mock使用 Dubbo自带的Mock功能首先是为了做服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过Mock数据返回授权失败。 我们从官网上举一个例子来说明: <dubbo:reference interface="com.foo.BarService" mock="force" /> 我们可以在期望的reference标签上加一个mock="force",就可以将当前服务设置为mock。但是设置完mock属性后还没有结束,需要有一个Mock类对应我们的服务接口类。 规则如下: 接口名 + Mock后缀,服务接口调用失败Mock实现类,该Mock类必须有一个无参构造函数。 对应到com.foo.BarService的话,则创建BarServiceMock类。 public class BarServiceMock implements BarService { public String sayHello(String name) { // 你可以伪造容错数据,此方法只在出现RpcException时被执行 return "容错数据"; } } 经过以上设置后,当调用BarService进行远程调用的话,直接请求到BarServiceMock类上面进行模拟测试。 2、Dubbo Mock的原理解析 在dubbo的配置文件中classpath:/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.cluster.Cluster 可以看到如下配置列表: mock=com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterWrapper failover=com.alibaba.dubbo.rpc.cluster.support.FailoverCluster failfast=com.alibaba.dubbo.rpc.cluster.support.FailfastCluster failsafe=com.alibaba.dubbo.rpc.cluster.support.FailsafeCluster failback=com.alibaba.dubbo.rpc.cluster.support.FailbackCluster forking=com.alibaba.dubbo.rpc.cluster.support.ForkingCluster available=com.alibaba.dubbo.rpc.cluster.support.AvailableCluster switch=com.alibaba.dubbo.rpc.cluster.support.SwitchCluster mergeable=com.alibaba.dubbo.rpc.cluster.support.MergeableCluster broadcast=com.alibaba.dubbo.rpc.cluster.support.BroadcastCluster 我们可以看到配置文件中实际上有五大路由策略: AvailableCluster: 获取可用的调用。遍历所有Invokers判断Invoker.isAvalible,只要一个有为true直接调用返回,不管成不成功。 BroadcastCluster: 广播调用。遍历所有Invokers, 逐个调用每个调用catch住异常不影响其他invoker调用。 FailbackCluster: 失败自动恢复, 对于invoker调用失败, 后台记录失败请求,任务定时重发, 通常用于通知。 FailfastCluster: 快速失败,只发起一次调用,失败立即保错,通常用于非幂等性操作。 FailoverCluster: 失败转移,当出现失败,重试其它服务器,通常用于读操作,但重试会带来更长延迟。 Dubbo中默认使用的是FailoverCluster策略,而在实际执行的过程中是FailoverCluster会被先被注入到MockClusterWrapper中,过程就是: Cluster$Adaptive -> 定位到内部key为failover的对象 ->FailoverCluster->注入到MockClusterWrapper MockClusterWrapper内部会创建一个MockClusterInvoker对象。实际创建是封装了FailoverClusterInvoker的MockClusterInvoker,这样就成功地在Invoker之中植入了Mock机制。 我们来看MockClusterInvoker的内部实现: 如果在没有配置之中没有设置mock,那么直接把方法调用转发给实际的Invoker(也就是FailoverClusterInvoker)。 String mockValue = directory.getUrl().getMethodParameter( invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim(); if (mockValue.length() == 0 || mockValue.equalsIgnoreCase("false")) { //no mock result = this.invoker.invoke(invocation); } 如果配置了强制执行Mock,比如发生服务降级,那么直接按照配置执行mock之后返回。 else if (mockValue.startsWith("force")) { if (logger.isWarnEnabled()) { logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url: " + directory.getUrl()); } //force:direct mock result = doMockInvoke(invocation, null); } 如果是其它的情况,比如只是配置的是mock=fail:return null,那么就是在正常的调用出现异常的时候按照配置执行mock。 try { result = this.invoker.invoke(invocation); } catch (RpcException rpcException) { if (rpcException.isBiz()) { throw rpcException; } else { if (logger.isWarnEnabled()) { logger.info("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), rpcException); } result = doMockInvoke(invocation, rpcException); } } 3、Dubbo Mock的适用场景 Dubbo的Mock功能主要是为了做服务降级而使用的,服务提供方在客户端执行容错逻辑,在出现RpcException(比如网络失败,超时等)时进行容错,然后执行降级Mock逻辑。自身并不适合做Mock测试系统。 四、自动化Mock系统的实现 1、Mock系统的简单用例图 image.png 2、Mock系统的架构图 image.png 为了基于Dubbo实现Mock功能,需要对Dubbo源码进行一些必要的修改,通过上面的架构图我们可以看到,实际上我们正是利用了Dubbo的Filter chain过滤器链这一机制实现的,为了方便大家更好的理解,下面将简单介绍一下Dubbo的Filter机制。 2.1、Dubbo的Filter原理分析 Filter:是一种递归的链式调用,用来在远程调用真正执行的前后加入一些逻辑,跟aop的拦截器servlet中filter概念一样的。 Filter接口定义: @SPI public interface Filter { Result invoke(Invoker<?> invoker,Invocation invocation) throws RpcException; } Filter的实现类需要打上@Activate注解, @Activate的group属性是个string数组,我们可以通过这个属性来指定这个filter是在consumer, provider还是两者情况下激活,所谓激活就是能够被获取,组成filter链。 List<Filter> filters =ExtensionLoader.getExtensionLoader(Filter.class).getAct ivateExtension(invoker.getUrl(),key, group); Key就是SERVICE_FILTER_KEY还是REFERENCE_FILTER_KEY Group就是consumer或者provider 关于SPI的详细介绍请大家参考我之前写的另一篇文章http://www.jianshu.com/p/46aa69643c97 ProtocolFilterWrapper:在服务的暴露与引用的过程中根据KEY是PROVIDER还是CONSUMER来构建服务提供者与消费者的调用过滤器链,Filter最终都要被封装到Wrapper中的。 public <T> Exporter<T> export(Invoker<T>invoker)throws RpcException { return protocol.export(buildInvokerChain(invoker, Constants.SERVICE_FILTER_KEY, Constants.PROVIDER)); } public <T> Invoker<T> refer(Class<T> type,URL url)throws RpcException { return buildInvokerChain(protocol.refer(type, url),Constants.REFERENCE_FILTER_KEY, Constants.CONSUMER); } 构建filter链,当我们获取激活的filter集合后就通过ProtocolFilterWrapper类中的buildInvokerChain方法来构建。 for (int i = filters.size() - 1; i >= 0; i --) { final Filter filter = filters.get(i); final Invoker<T> next = last; last = new Invoker<T>() { public Result invoke(Invocation invocation)throws RpcException { return filter.invoke(next, invocation); } 。。。。。。。 //其他方法 }; } 2.2、Mock流程介绍 image.png 注:我们在<dubbo:application name>中新加了自定义的“env=test”这样的属性配置用来标明当前环境是测试的还是正式的,用户每次通过Dubbo请求的远程服务的时候,都会首先经过我们自定义的Filter,我们自定义的Filter会首先判断当前的环境是test还是正式,如果是test的环境则直接访问Mock配置中心获取提前配置好的Mock数据并封装成用户定义的Response对象返回。 3、Mock系统的配置中心 Mock配置中心就是用户将mock数据与应用环境建立关系的系统,整个系统就像一个工作流引擎: 环境设置->应用名称设置->挡板规则设置->Facade服务接口设置->方法规则设置 环境设置 image.png 注:如果尚未映射来源IP地址到环境,则点击环境列表导航链接,进入环境列表页面,点击添加,输入源IP及环境名,点击确定按钮,实现源IP到所设环境的映射。每个用户都可以建立属于自己的测试环境。 应用名称设置 image.png 注:创建所使用系统的应用名称,Mock配置中心默认使用<dubbo:application name>中的名称作为应用名称。 挡板规则 image.png 注:每一个挡板规则都是由一个环境名称和应用名称组成的唯一挡板,在挡板设置中选择环境名称和应用名称,并且设置挡板的有效状态。 Facade规则 image.png 注:每一个Facade就是一个Dubbo的服务接口类,在这里将自己的Facade名称与全路径与挡板名称对应,以标识哪些Facade服务接口类是属于哪个挡板的。 方法规则 image.png 注:方法规则是用来设置每个Facade中的需要mock的方法的,可以对不同的方法设置方法执行时间、方法抛出的异常等等。 4、Mock系统的其他功能 由于不少应用项目开发完后想对其进行单独压测,而很多时候应用系统和其他业务系统形成了依赖关系,如果不布署其他应用系统则无法完成压测,为了更好的支持性能测试组进行挡板压测,Mock系统支持压测功能,而Mock系统自身也可以达到单台服务器1000TPS以上(8C8G)。
Spring For All 玩最纯粹的技术!做最专业的 Spring 民间组织~ 欢迎加入:http://spring4all.com/ image.png
感谢微信群Spring Boot那些事”的水门兄弟的热心整理和总结,同时也感谢其他热心参与的朋友们 Paste_Image.png 如果大家看不清上述图片,可以点击如下URL查看大图:http://img.blog.csdn.net/20170524150154080?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMzk3MDk5MQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
一、说在前面 微服务是当下最火的词语,现在很多公司都在推广微服务,当服务越来越多的时候,我们是否会纠结以下几个问题: 面对一笔超时的订单,究竟是哪一步处理时间超长呢? 数据由于并发莫名篡改,到底都谁有重大嫌疑呢? 处理遗漏了一笔订单,曾经是哪个环节出错把它落下了? 系统莫名的报错,究竟是哪一个服务报的错误? 每个服务那么多实例服务器,如何快速定位到是哪一个实例服务器报错的呢? 现在很多系统都要求可用性达到99.9%以上,那么我们除了增加系统健壮性减少故障的同时,我们又如何在真正发生故障的时候,快速定位和解决问题,也将是我们的重中之重。 在做微服务框架选择的时候,Spring Cloud无疑是当下最火的,但是因为Spring Cloud是近二年的后起新秀,以及在使用方式上面的差别,目前在很多中小企业还是以dubbo为主,不过遗憾的是,dubbo从官方来讲已经不维护了,很多公司都是自己再去维护,那么今天我就来给大家介绍一下,我们是如何通过修改dubbo源码实现了分布式调用链的第一阶段:调用链日志的打印。 二、什么是分布式调用链 1、什么是调用链 基于Google Dapper论文,用户每次请求都会生成一个全局ID(traceId),通过它将不同系统的“孤立”的日志串在一起,重组成调用链。 2、调用链的调用过程 当用户发起一个请求时,首先到达前端A服务,然后分别对B服务和C服务进行RPC调用。 B服务处理完给A做出响应,但是C服务还需要和后端的D服务和E服务交互之后再返还给A服务,最后由A服务来响应用户的请求。 Paste_Image.png 3、对整个调用过程的追踪 请求到来生成一个全局TraceID,通过TraceID可以串联起整个调用链,一个TraceID代表一次请求。 除了TraceID外,还需要SpanID用于记录调用父子关系。每个服务会记录下Parent id和Span id,通过他们可以组织一次完整调用链的父子关系。 一个没有Parent id的span成为root span,可以看成调用链入口。 所有这些ID可用全局唯一的64位整数表示; 整个调用过程中每个请求都要透传TraceID和SpanID。 每个服务将该次请求附带的TraceID和附带的SpanID作为Parent id记录下,并且将自己生成的SpanID也记录下。 要查看某次完整的调用则只要根据TraceID查出所有调用记录,然后通过Parent id和Span id组织起整个调用父子关系。 最终的TraceId和SpanId的调用关系图如下所示: Paste_Image.png 三、基于Dubbo的实现 1、Dubbo的调用过程 在我们分析源码的时候,有一行代码是: Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); 这行代码实际上是利用SPI机制,动态加载指定的Protocol注入到ProtocolFilterWrapper中,再通过Wrapper访问到可执行的Invoker对象,Dubbo默认使用的是DubboProtocol最终通过netty的方式进行通信,具体调用过程请看下图: Paste_Image.png 可以看到基本的流程是: InvokerInvocationHandler ->ClusterInvoker ->LoadBalance -> ProtocolFilterWrapper -> Protocol -> DubboInvoker 而在调用链的实现过程中技术难点主要是有二个: 在哪里暂存调用链 调用链信息如何传递 2、Dubbo协议下的调用链传递过程 那么在默认的Dubbo协议下,实现调用链的过程很简单只需要在应用项目或者Dubbo源码中使用如下代码就可以实现调用链的传递。 RpcContext.getContext().setAttachment(CallChainContext.TRACEID, traceIdValue); RpcInvocation rpcInvocation = (RpcInvocation) inv; rpcInvocation.setAttachment(CallChainContext.TRACEID, traceIdValue); rpcInvocation.setAttachment(CallChainContext.SPANID, spanIdValue); 在DubboInvoker中最终通信的时候会将上述代码的RpcInvocation对象传递出去,那么我们只需要在接收端获取既可。 3、Hessian协议下的调用链传递过程 大家都知道,Dubbo在实现通信的协议上使用的有Netty、Hessian、Rest等方式,由于我们项目的特殊性,目前采用的是Dubbo的Hessian协议。 先看HessianProtocol的如下代码: protected <T> T doRefer(Class<T> serviceType, URL url) throws RpcException { HessianProxyFactory hessianProxyFactory = new HessianProxyFactory(); String client = url.getParameter(Constants.CLIENT_KEY, Constants.DEFAULT_HTTP_CLIENT); if ("httpclient".equals(client)) { hessianProxyFactory.setConnectionFactory(new HttpClientConnectionFactory()); } else if (client != null && client.length() > 0 && ! Constants.DEFAULT_HTTP_CLIENT.equals(client)) { throw new IllegalStateException("Unsupported http protocol client=\"" + client + "\"!"); } int timeout = url.getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); hessianProxyFactory.setConnectTimeout(timeout); hessianProxyFactory.setReadTimeout(timeout); return (T) hessianProxyFactory.create(serviceType, url.setProtocol("http").toJavaURL(), Thread.currentThread().getContextClassLoader()); } 通过代码可以看到,实际上在使用Hessian通信的时候并没有将RpcInvocation里面设定的TraceId和SpanId传递出去,调用在这一块中止了。 那我们如何自己来实现呢? 第一步、我们在Dubbo源码中自己实现了一个Filter(不是Dubbo的Filter),用来产生TraceId和SpanId,以及最后的清理工作,请看代码如下: public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 将请求转换成HttpServletRequest请求 HttpServletRequest httpServletRequest = (HttpServletRequest) request; try { archieveId(request); } catch (Throwable e) { log.log(Level.SEVERE, "traceId或spanId解析出错!", e); } try { chain.doFilter(request, response); } catch (IOException e) { //还原线程名称 throw e; } catch (ServletException e) { //还原线程名称 throw e; } finally { CallChainContext.getContext().clearContext(); } } 在Filter中产生TraceId和SpanId以后,会将二个值放到我们封装好的CallChainContext中进行暂存。 第二步、我们将HessianProxyFactory进行继承改造 public class HessianProxyWrapper extends HessianProxy { private static final long serialVersionUID = 353338409377437466L; private static final Logger log = Logger.getLogger(HessianProxyWrapper.class .getName()); public HessianProxyWrapper(URL url, HessianProxyFactory factory, Class<?> type) { super(url, factory, type); } protected void addRequestHeaders(HessianConnection conn) { super.addRequestHeaders(conn); conn.addHeader("traceId", CallChainContext.getContext().getTraceId()); conn.addHeader("spanId", CallChainContext.getContext().getSpanId()); } } 我们将CallChainContext中暂存的TraceId和SpanId放入到Hessian的header中。 继承Dubbo的HessianProxyFactory这个类,新类名是HessianProxyFactoryWrapper,在create方法中将HessianProxy替换为新封装的HessianProxyWrapper,代码如下: public Object create(Class<?> api, URL url, ClassLoader loader) { if (api == null) throw new NullPointerException( "api must not be null for HessianProxyFactory.create()"); InvocationHandler handler = null; //将HessianProxy修改为HessianProxyWrapper handler = new HessianProxyWrapper(url, this, api); return Proxy.newProxyInstance(loader, new Class[] { api, HessianRemoteObject.class }, handler); } 修改后的HessianProtocol的代码如下: protected <T> T doRefer(Class<T> serviceType, URL url) throws RpcException { //新继承的 HessianProxyFactoryWrapper hessianProxyFactory = new HessianProxyFactoryWrapper(); String client = url.getParameter(Constants.CLIENT_KEY, Constants.DEFAULT_HTTP_CLIENT); if ("httpclient".equals(client)) { hessianProxyFactory.setConnectionFactory(new HttpClientConnectionFactory()); } else if (client != null && client.length() > 0 && ! Constants.DEFAULT_HTTP_CLIENT.equals(client)) { throw new IllegalStateException("Unsupported http protocol client=\"" + client + "\"!"); } int timeout = url.getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); hessianProxyFactory.setConnectTimeout(timeout); hessianProxyFactory.setReadTimeout(timeout); return (T) hessianProxyFactory.create(serviceType, url.setProtocol("http").toJavaURL(), Thread.currentThread().getContextClassLoader()); } 通过以上方式可以将我们产生的TraceId和SpanId通过Hessian的方式传递出去,我们在接收请求的时候,只需要使用如下代码的方式就可以获取到二个值。 String traceIdValue = request.getHeader("traceId"); String spanIdValue = request.getHeader("spanId"); 第三步、如何打印调用链信息 我们在项目中使用的是Logback的方式打印日志,首先想到的是继承一个ClassicConverter对象,实现Logback的自定义格式转换器,参考代码如下: public class CallChainConverter extends ClassicConverter { @Override public String convert(ILoggingEvent event) { Map<String,String> globalMap = CallChainContext.getContext().get(); StringBuilder builder = new StringBuilder(); if(null == globalMap) { globalMap = new HashMap<String, String>(); CallChainContext.getContext().add(globalMap); } else { String traceId = globalMap.get("traceId"); String spainId = globalMap.get("spanId"); if(traceId == null) { traceId = String.valueOf(Thread.currentThread().getId()); } if(spainId == null) { spainId = "1"; } builder.append("GUID["); builder.append(traceId); builder.append("] - LEVEL["); builder.append(spainId); builder.append("] "); } return builder.toString(); } } 在Logback配置文件中进行如下修改: <conversionRule conversionWord="callContext" converterClass="com.ulpay.dubbox.core.util.CallChainConverter" /> <layout class="com.ulpay.dubbox.core.util.CallChainPatternLayout"> <pattern>%d %-5p %c [%t] %callContext - %m%n</pattern> </layout> 最终打印的日志格式如下样式: [RMI TCP Connection(127.0.0.1:2181)] GUID[760a1fedd7ab4ff8a309cebaa01cc61d] - LEVEL[15.27.1] - [执行时间] - [xxx项目] - [xxx服务.xxx方法] - 耗时[7]毫秒 4、采集日志信息实现分布式调用链界面展示 一个最简单的demo示意图如下: Paste_Image.png 通过logstash采集日志到kafka kafka负责提供数据给Hbase 通过Hbase进行数据分析 最终效果展示图如下: Paste_Image.png 四、总结 对于分布式调用链来说,目前市面上有很多开源的工具,比如:pinpoint,Cat以及sky-walking等等,将这些工具与我们扩展的调用链日志结合起来将起到更好的效果。 出于公司的考虑,以上的代码采用的是伪代码,但也具有一定参考价值,我写这篇文章的目的也是希望能够给大家提供一些思路,希望大家能够多提建议,我会持续改进。 参考资料 分布式系统的跟踪系统Dubbo RPC处理https://www.oschina.net/question/2254200_2137086
一、GIT发展史 同生活中的许多伟大事件一样,Git 诞生于一个极富纷争大举创新的年代。Linux 内核开源项目有着为数众广的参与者。绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间)。到 2002 年,整个项目组开始启用分布式版本控制系统 BitKeeper 来管理和维护代码。 到 2005 年的时候,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了免费使用 BitKeeper 的权力。这就迫使 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds )不得不吸取教训,只有开发一套属于自己的版本控制系统才不至于重蹈覆辙。他们对新的系统订了若干目标: 速度 简单的设计 对非线性开发模式的强力支持(允许上千个并行开发的分支) 完全分布式 有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量) 自诞生于 2005 年以来,Git 日臻成熟完善,在高度易用的同时,仍然保留着初期设定的目标。它的速度飞快,极其适合管理大项目,它还有着令人难以置信的非线性分支管理系统,可以应付各种复杂的项目开发需求。 二、如何安装GIT 1、Ubuntu上安装 $ sudo apt-get install git-core 2、Windows上安装 有个叫做 msysGit 的项目提供了安装包,参考: http://code.google.com/p/msysgit 3、从源码安装 Git的工作需要调用curl,zlib,openssl,expat,libiconv等库的代码,所有要先安装这些依赖工具。 $ sudo apt-get install curl-devel expat-devel gettext-devel openssl-devel zlib-devel 再从下面的 Git 官方站点下载最新版本源代码 : http://git-scm.com/download 然后编译并安装,例如: $ tar -zxf git-1.6.0.5.tar.gz $ cd git-1.6.0.5 $ make prefix=/usr/local all $ sudo make prefix=/usr/local install 现在已经可以用 git 命令了,例如把 Git 项目仓库克隆到本地,以便日后更新: $ git clone git://git.kernel.org/pub/scm/git/git.git 三、运行GIT前的配置 一般在新的系统上,我们都需要先配置下自己的 Git 工作环境。配置工作只需一次,以后升级时还会沿用现在的配置。当然,如果需要,你随时可以用相同的命令修改已有的配置。 Git 提供了一个叫做 git config 的工具,专门用来配置或读取相应的工作环境变量。而正是由这些环境变量,决定了 Git 在各个环节的具体工作方式和行为。这些变量可以存放在以下三个不同的地方: /etc/gitconfig文件: 系统中对所有用户都普遍适用的配置。若使用 git config 时用 --system 选项,读写的就是这个文件。 ~/.gitconfig文件:用户目录下的配置文件只适用于该用户。若使用 git config 时用 --global 选项,读写的就是这个文件。 当前项目的 git 目录中的配置文件(也就是工作目录中的 .git/config 文件):这里的配置仅仅针对当前项目有效。每一个级别的配置都会覆盖上层的相同配置,所以 .git/config 里的配置会覆盖/etc/gitconfig 中的同名变量。 第一个要配置的是你个人的用户名称和电子邮件地址。这两条配置很重要,每次 Git 提交时都会引用这两条信息,说明是谁提交了更新,所以会随更新内容一起被永久纳入历史记录: $ git config --global user.name "John Doe" $ git config --global user.email johndoe@example.com 如果用了 --global 选项,那么更改的配置文件就是位于你用户主目录下的那个,以后你所有的项目都会默认使用这里配置的用户信息。如果要在某个特定的项目中使用其他名字或者电邮,只要去掉 --global 选项重新配置即可,新的设定保存在当前项目的 .git/config 文件里。 接下来要设置的是默认使用的文本编辑器。Git 需要你输入一些额外消息的时候,会自动调用一个外部文本编辑器给你用。默认会使用操作系统指定的默认编辑器,一般可能会是 Vi 或者 Vim。如果你有其他偏好,比如 Emacs 的话,可以重新设置:$ git config --global core.editor emacs 还有一个比较常用的是,在解决合并冲突时使用哪种差异分析工具。比如要改用 vimdiff 的话:$ git config --global merge.tool vimdiff Git可以理解 kdiff3,tkdiff,meld,xxdiff,emerge,vimdiff,gvimdiff,ecmerge,和 opendiff 等合并工具的输出信息。当然,你也可以指定使用自己开发的工具。 查看配置信息 要检查已有的配置信息,可以使用 git config --list 命令: $ git config --list user.name=Scott Chacon user.email=schacon@gmail.com 也可以直接查阅某个环境变量的设定,只要把特定的名字跟在后面即可,例如: git config user.name Scott Chacon 四、获取GIT帮助 想了解 Git 的各种命令该怎么用,可以阅读它们的使用帮助,方法有三: $ git help <verb> $ git <verb> --help $ man git-<verb> 比如,要学习 config 命令可以怎么用,运行:$ git help config 我们随时都可以浏览这些帮助信息而无需连网。不过,要是你觉得还不够,可以到 Frenode IRC 服务器(irc.freenode.net)上的 #git 或 #github 频道寻求他人帮助。这两个频道上总有着上百号人,大多都有着丰富的 git 知识,并且乐于助人。 五、Git中文件的状态和基本工作流程 对于任何一个文件,在 Git 内都只有三种状态:已提交(committed),已修改(modified)和已暂存(staged)。已提交表示该文件已经被安全地保存在本地数据库中了;已修改表示修改了某个文件,但还没有提交保存;已暂存表示把已修改的文件放在下次提交时要保存的清单中。 由此我们看到 Git 管理项目时,文件流转的三个工作区域:Git 的本地数据目录,工作目录以及暂存区域。 Paste_Image.png 每个项目都有一个 git 目录,它是 Git 用来保存元数据和对象数据库的地方。该目录非常重要,每次克隆镜像仓库的时候,实际拷贝的就是这个目录里面的数据。 从项目中取出某个版本的所有文件和目录,用以开始后续工作的叫做工作目录。这些文件实际上都是从 git 目录中的压缩对象数据库中提取出来的,接下来就可以在工作目录中对这些文件进行编辑。 所谓的暂存区域只不过是个简单的文件,一般都放在 git 目录中。有时候人们会把这个文件叫做索引文件,不过标准说法还是叫暂存区域。 基本的 Git 工作流程如下所示: 在工作目录中修改某些文件。 对这些修改了的文件作快照,并保存到暂存区域。 提交更新,将保存在暂存区域的文件快照转储到 git 目录中。 所以,我们可以从文件所处的位置来判断状态:如果是 git 目录中保存着的特定版本文件,就属于已提交状态;如果作了修改并已放入暂存区域,就属于已暂存状态;如果自上次取出后,作了修改但还没有放到暂存区域,就是已修改状态。 六、Git常用命令 git init git clone git add <file> git commit git diff git diff --cached git rm <file> git rm <file> --cached git mv <file> <newfile> git status git log git commit --amend git reset HEAD <file> git reset --soft HEAD~n git reset --hard HEAD~n git checkout -- <file> git checkout <file> git remote git remote add [shortname] [url] git fetch [remotename] git push [remotename] [localbranch]:[remotebranch] git remote show [remotename] git remote rename [remotename] [new-remotename] git remote rm [remotename] git tag git tag -a [tagname] –m [comments] git push [remotename] [tagname] git push [remotename] –tags git branch [branchname] git checkout [branchname] git branch -d [branchname] git merge [branchname] git branch git checkout -b [branchname] [remotename]/[branchname] git push [remotename] :[remotebranch] git rebase [branchname] git pull 七、Git命令使用举例 从当前目录初始化 要对现有的某个项目开始用 Git 管理,只需到此项目所在的目录,执行:git init 初始化后,在当前目录下会出现一个名为 .git 的目录,所有 Git 需要的数据和资源都存放在这个目录中。 从现有仓库克隆,如克隆git的代码库git clone git://git.kernel.org/pub/scm/git/git.git 跟踪新文件和暂存已修改文件git add <file> 提交更新git commit 这种方式会启动文本编辑器以便输入本次提交的说明。也可以使用 -m 参数后跟提交说明的方式,在一行命令中提交更新:git commit -m “Initial commit of test repo” 查看已暂存和未暂存的修改git diff 此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容。若要看已经暂存起来的文件和上次提交时的快照之间的差异,可以用 git diff --cached 命令 移除文件 要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。git rm <file> 另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。 可以用--cached选项git rm --cached <file> 移动文件git mv <file> <newfile> 检查当前文件状态git status 查看提交历史git log 修改最后一次提交 有时候我们提交完了才发现漏掉了几个文件没有加,或者提交信息写错了。想要撤消刚才的提交操作,可以使用 --amend 选项重新提交:git commit --amend 如果刚才提交完没有作任何改动,直接运行此命令的话,相当于有机会重新编辑提交说明,而所提交的文件快照和之前的一样。如果刚才提交时忘了暂存某些修改,可以先补上暂存操作,然后再运行 --amend 提交。 取消已经暂存的文件git reset HEAD <file> 取消对文件的修改git checkout -- <file>git checkout <file> 一般与上面的命令效果相同,但如果有一个分支名与文件名相同,就不一样了。加--来消除歧义。 查看当前的远程库git remote 也可以加上 -v 选项,显示对应的克隆地址git remote -v 添加远程仓库git remote add [shortname] [url] 从远程仓库抓取数据git fetch [remotename] 此命令会到远程仓库中拉取所有你本地仓库中还没有的数据。运行完成后,你就可以在本地访问该远程仓库中的所有分支,将其中某个分支合并到本地,或者只是取出某个分支,一探究竟。 推送数据到远程仓库git push [remotename] [localbranch]:[remotebranch] 查看远程仓库信息git remote show [remotename] 远程仓库的删除和重命名 git remote rename [remotename] [new-remotename] git remote rm [remotename] 显示已有的标签git tag 新建标签git tag -a [tagname] –m [comments] 如果想为以前的某次提交打标签,只要在打标签的时候跟上对应提交对象的校验和(或前几位字符)即可 。 用某个标签新建分支git checkout –b [branchname] [tagname] 分享标签 默认情况下,git push 并不会把标签传送到远端服务器上,只有通过显式命令才能分享标签到远端仓库。git push [remotename] [tagname] 如果要一次推送所有(本地新增的)标签上去,可以使用 --tags 选项:git push [remotename] –tags 创建分支git branch [branchname] 切换分支git checkout [branchname] 新建并切换到该分支git checkout -b [branchname] 删除分支git branch -d [branchname] 合并分支git merge [branchname] 以上命令将[branchname]分支合并到当前分支 查看分支git branch 远程分支和创建跟踪分支 远程分支(remote branch)是对远程仓库状态的索引。它们是一些无法移动的本地分支;只有在进行 Git 的网络活动时才会更新。远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置。我们用 (远程仓库名)/(分支名) 这样的形式表示远程分支。从远程分支检出的本地分支成为跟踪分支。git checkout -b [branchname] [remotename]/[branchname] 或者 git checkout --track [remotename]/[branchname] 删除远程分支git branch -r -d origin/[branchname] 衍合git rebase [branchname] 从远程仓库抓取数据并mergegit pull 八、Git分支 在 Git 中提交时,会保存一个提交(commit)对象,它包含一个指向暂存内容快照的指针,作者和相关附属信息,以及一定数量(也可能没有)指向该提交对象直接祖先的指针:第一次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。 为直观起见,我们假设在工作目录中有三个文件,准备将它们暂存后提交。暂存操作会对每一个文件计算校验和(即SHA-1 哈希字串),然后把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 类型的对象存储这些快照),并将校验和加入暂存区域。现在,Git 仓库中有五个对象:三个表示文件快照内容的 blob 对象;一个记录着目录树内容及其中各个文件对应 blob 对象索引的 tree 对象;以及一个包含指向 tree 对象(根目录)的索引和其他提交信息元数据的 commit 对象 。 概念上来说,仓库中的各个对象保存的数据和相互关系看起来 如下所示: Paste_Image.png 多次提交后,仓库历史如下: Paste_Image.png Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。 Paste_Image.png Git 通过个git branch命令创建分支,比如新建一个testing分支:git branch testing 这会在当前commit对象上新建一个分支指针, 同时Git保存着一个名为HEAD的特别指针,来让Git知道当前在哪个分支上工作。如下: Paste_Image.png 要切换到其他分支,可以执行命令git checkout,如切换到新建的testing分支:git checkout testing 这样HEAD就指向testing分支,如下: Paste_Image.png 此时在testing分支上工作,如果有新的提交,提交后的结果如下: Paste_Image.png 此时切换回master分支再提交后的结果如下: Paste_Image.png 九、Git分支合并与衍合 如将experiment分支合并回master分支执行以下命令: git checkout master git merge experiment``` 合并前后如下所示:  有时候合并操作并不会如此顺利。如果你修改了两个待合并分支里同一个文件的同一部分,Git 就无法干净地把两者合到一起,这种问题只能由人来解决。如果你想用一个有图形界面的工具来解决这些问题,不妨运行 git mergetool,它会调用一个可视化的合并工具并引导你解决所有冲突。确认所有冲突都解决后,可以用 git commit 来完成这次合并提交。 其实,合并两个分支还有另外一个选择:你可以把在 C3 里产生的变化补丁重新在 C4 的基础上打一遍。在 Git 里,这种操作叫做衍合(rebase)。有了 rebase 命令,就可以把在一个分支里提交的改变在另一个分支里重放一遍。 git checkout experiment git rebase master 衍合前后如下所示:  再进行一次快进: git checkout master git merge experiment 结果如下所示:  现在,合并后的 C3(即现在的 C3’)所指的快照,同三方合并例子中的 C5 所指的快照内容一模一样了。最后整合得到的结果没有任何区别,但衍合能产生一个更为整洁的提交历史。如果视察一个衍合过的分支的历史记录,看起来更清楚:仿佛所有修改都是先后进行的,尽管实际上它们原来是同时发生的。 但衍合也并不是完美无缺的,一句话可以总结这点: 永远不要衍合那些已经推送到公共仓库的更新。
一、前言 在之前的一篇缓存穿透、缓存并发、缓存失效之思路变迁文章中介绍了关于缓存穿透、并发的一些常用思路,但是个人感觉文章中没有明确一些思路的使用场景,本文继续将继续深化与大家共同探讨,同时也非常感谢这段时间给我提宝贵建议的朋友们。 说明:本文中提到的缓存可以理解为Redis。 二、缓存穿透与并发方案 相信不少朋友之前看过很多类似的文章,但是归根结底就是二个问题: 如何解决穿透 如何解决并发 当并发较高的时候,其实我是不建议使用缓存过期这个策略的,我更希望缓存一直存在,通过后台系统来更新缓存系统中的数据达到数据的一致性目的,有的朋友可能会质疑,如果缓存系统挂了怎么办,这样数据库更新了但是缓存没有更新,没有达到一致性的状态。 解决问题的思路是: 如果缓存是因为网络问题没有更新成功数据,那么建议重试几次,如果依然没有更新成功则认为缓存系统出错不可用,这时候客户端会将数据的KEY插入到消息系统中,消息系统可以过滤相同的KEY,只需保证消息系统不存在相同的KEY,当缓存系统恢复可用的时候,依次从mq中取出KEY值然后从数据库中读取最新的数据更新缓存。注意:更新缓存之前,缓存中依然有旧数据,所以不会造成缓存穿透。 下图展示了整个思路的过程: Paste_Image.png 看完上面的方案以后,又会有不少朋友提出疑问,如果我是第一次使用缓存或者缓存中暂时没有我需要的数据,那又该如何处理呢? 解决问题的思路: 在这种场景下,客户端从缓存中根据KEY读取数据,如果读到了数据则流程结束,如果没有读到数据(可能会有多个并发都没有读到数据),这时候使用缓存系统中的setNX方法设置一个值(这种方法类似加个锁),没有设置成功的请求则sleep一段时间,设置成功的请求读取数据库获取值,如果获取到则更新缓存,流程结束,之前sleep的请求这时候唤醒后直接再从缓存中读取数据,此时流程结束。 在看完这个流程后,我想这里面会有一个漏洞,如果数据库中没有我们需要的数据该怎么处理,如果不处理则请求会造成死循环,不断的在缓存和数据库中查询,这时候我们会沿用我之前文章中的如果没有读到数据则往缓存中插入一个NULL字符串的思路,这样其他请求直接就可以根据“NULL”进行处理,直到后台系统在数据库成功插入数据后同步更新清理NULL数据和更新缓存。 流程图如下所示: Paste_Image.png 总结: 在实际工作中,我们往往将上面二个方案组合使用才能达到最佳效果,虽然第二种方案也会造成请求阻塞,但是只是在第一次使用或者缓存暂时没有数据的情况下才会产生,在生产中经过检验在TPS没有上万的情况下是不会造成问题的。 三、热点缓存解决方案 1、缓存使用背景: 我们拿用户中心的一个案例来说明: 每个用户都会首先获取自己的用户信息,然后再进行其他相关的操作,有可能会有如下一些场景情况: 会有大量相同用户重复访问该项目。 会有同一用户频繁访问同一模块。 2、思路解析 因为用户本身是不固定的而且用户数量也有几百万尤其上千万,我们不可能把所有的用户信息全部缓存起来,通过第一个场景情况可以看到一些规律,那就是有大量的相同用户重复访问,但是究竟是哪些用户重复访问我们也并不知道。 如果有一个用户频繁刷新读取项目,那么对数据库本身也会造成较大压力,当然我们也会有相关的保护机制来确实恶意攻击,可以从前端控制,也可以有采黑名单等机制,这里不在赘述。如果用缓存的话,我们又该如何控制同一用户繁重读取用户信息呢。 请看下图: Paste_Image.png 我们会通过缓存系统做一个排序队列,比如1000个用户,系统会根据用户的访问时间更新用户信息的时间,越是最近访问的用户排名越排前,系统会定期过滤掉排名最后的200个用户,然后再从数据库中随机取出200个用户加入队列,这样请求每次到达的时候,会先从队列中获取用户信息,如果命中则根据userId,再从另一个缓存数据结构中读取用户信息,如果没有命中则说明该用户请求频率不高。 JAVA伪代码如下所示: for (int i = 0; i < times; i++) { user = new ExternalUser(); user.setId(i+""); user.setUpdateTime(new Date(System.currentTimeMillis())); CacheUtil.zadd(sortKey, user.getUpdateTime().getTime(), user.getId()); CacheUtil.putAndThrowError(userKey+user.getId(), JSON.toJSONString(user)); } Set<String> userSet = CacheUtil.zrange(sortKey, 0, -1); System.out.println("[sortedSet] - " + JSON.toJSONString(userSet) ); if(userSet == null || userSet.size() == 0) return; Set<Tuple> userSetS = CacheUtil.zrangeWithScores(sortKey, 0, -1); StringBuffer sb = new StringBuffer(); for(Tuple t:userSetS){ sb.append("{member: ").append(t.getElement()).append(", score: ").append(t.getScore()).append("}, "); } System.out.println("[sortedcollect] - " + sb.toString().substring(0, sb.length() - 2)); Set<String> members = new HashSet<String>(); for(String uid:userSet){ String key = userKey + uid; members.add(uid); ExternalUser user2 = CacheUtil.getObject(key, ExternalUser.class); System.out.println("[user] - " + JSON.toJSONString(user2) ); } System.out.println("[user] - " + System.currentTimeMillis()); String[] keys = new String[members.size()]; members.toArray(keys); Long rem = CacheUtil.zrem(sortKey, keys); System.out.println("[rem] - " + rem); userSet = CacheUtil.zrange(sortKey, 0, -1); System.out.println("[remove - sortedSet] - " + JSON.toJSONString(userSet));
刘兵,花名玄靖,开源技术爱好者,高性能Redis中间件NRedis-Proxy作者,目前研究方向为java中间件,微服务等技术。 一、什么是分布式发号器 说起分布式发号器的前生今世,咱们应该感恩这个时代;随着互联网在中国越来越普及化,单机系统或者一个小系统已经无法满足需要,随着用户逐渐增多,数据量越来越大,单个应用或者单个数据库已经无法满足需求,在应用以至于微服务来临,在数据库存储方面分库分表来临,可以解决问题;但是新的问题产生,怎么样做到多个应用可以有唯一主键或者序号,防止数据重复呢?分布式发号器正好为解决这个问题,可以让大家无须为这个问题烦恼了,这是本人写这篇文章初衷。 二、分布式发号器优势 解决分库分表中唯一序号的问题 解决分布式应用或者微服务框架中唯一序号的问题 提供可定制化生成规则,根据业务需求可自定义扩展 性能高效且系统简单稳定 系统可任意扩展 三、分布式发号器架构图 Paste_Image.png 四、分布式发号器流程图 1、分布式发号器重要字段 Paste_Image.png 2、concurrentValue不存在的流程图 图片 1.png 3、concurrentValue存在的流程图 图片 2.png 五、目前存在分布式发号器解决方案 1、UUID Universally Unique IDentifier(UUID),有着正儿八经的RFC规范,是一个128bit的数字,也可以表现为32个16进制的字符(每个字符0-F的字符代表4bit),中间用"-"分割。 时间戳+UUID版本号: 分三段占16个字符(60bit+4bit) Clock Sequence号与保留字段:占4个字符(13bit+3bit) 节点标识:占12个字符(48bit) 2、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位再取整数应付,不信两条线程会同时启动。 3、MongoDB MongoDB的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。 4、Twitter的snowflake派号器 snowflake也是一个派号器,基于Thrift的服务,不过不是用redis简单自增,而是类似UUID version1,只有一个Long 64bit的长度,所以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,不能批量取,所以额外增加的延时是问题,而且只能1024台机器范围之内。 以上几种方案同一个问题,不可自定义,位数过长。
1、在Finder中找到StartUML图标右击: Paste_Image.png 2、找到目录:\www\license\node 3、找到LicenseManagerDomain.js 在 try 前面加上: return { name:"0xcb", product:"StartUML", licenseType:"vip", quantity:"hello word", licenseKey:"later equals never" }; 如图: Paste_Image.png 然后打开StarUML,Help->Enter License输入: Name: StarUML Licence Key: 8888-000-8888 点击确定破解完成。。。。
1、强一致性 这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。 2、弱一致性 这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不久承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。 3、最终一致性 最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型。 分布式环境的各种问题 分布式系统体系结构从其出现之初就伴随着诸多的难题和挑战: 1、通信异常 从集中式向分布式演变的过程中,必然引入网络因素,由于网络本身的不可靠性,因此 也引入了额外的问题。分布式系统需要在各个节点之间进行网络通信,因此每次网络通信都会伴随着网络不可用的风险,网络光纤、路由器或是DNS等硬件设备或 是系统不可用都会导致最终分布式系统无法顺利完成一次网络通信。另外,即使分布式系统各个节点之间的网络通信能够正常进行,其延时也会大于单机操作。通常 我们认为现代计算机体系结构中,单机内存访问的延时在纳秒数量级(通常是10ns),而正常的一次网络通信的延迟在0.1~1ms左右(相当于内存访问延 时的105倍),如此巨大的延时差别,也会影响到消息的收发过程,因此消息丢失和消息延迟变得非常普遍。 2、网络分区 当网络由于发生异常情况,导致分布式系统中部分节点之间的网络延时不断增大,最终导致组成分布式系统的所有节点中,只有部分节点之间能够正常通信,而另一些节点则不能----我们将这个现象称为网络分区。当网络分区出现时,分布式系统会出现局部小集群,在极端情况下,这些局部小集群会独立完成原本需要整个分布式系统才能完成的功能,包括对数据的事物处理,这就对分布式一致性提出了非常大的挑战。 3、三态 上面两点,我们已经了解到在分布式环境下,网络可能会出现各式各样的问题,因此分布式系统的每一次请求与响应,存在特有的三态概念,即成功、失败、超时。 在传统的单机系统中,应用程序在调用一个函数之后,能够得到一个非常明确的响应:成功或失败。而在分布式系统中,由于网络是不可靠的,虽然在绝大部分情况 下,网络通信也能够接受到成功或失败的响应,当时当网络出现异常的情况下,就可能会出现超时现象,通常有以下两种情况: (1)由于网络原因,该请求并没有被成功地发送到接收方,而是在发送过程中就发生了消息丢失现象。 (2)该请求成功地被接收方接收后,进行了处理,但是在将响应反馈给发送方的过程中,发生了消息丢失现象。 当出现这样的超时现象时,网络通信的发起方是无法确定当前请求是否被成功处理的。 4、节点故障 节点故障则是分布式环境下另一个比较常见的问题,指的是组成分布式系统的服务器节点出现的宕机或"僵死"现象,通常根据经验来说,每个节点都有可能出现故障,并且每天都在发生。 CAP理论 一个经典的分布式系统理论。CAP理论告诉我们:一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中两项。 BASE理论 BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
一、思路图展示 Paste_Image.png 二、思路解析 我们都知道Zookeeper的节点有两种类型,分别是持久节点和临时节点。临时节点有个特性,就是如果注册这个节点的机器失去连接(通常是宕机),那么这个节点会被zookeeper删除。选主过程就是利用这个特性,在服务器启动的时候,去zookeeper特定的一个目录下注册一个临时节点(这个节点作为master,谁注册了这个节点谁就是master),注册的时候,如果发现该节点已经存在,则说明已经有别的服务器注册了(也就是有别的服务器已经抢主成功),那么当前服务器只能放弃抢主,作为从机存在。同时,抢主失败的当前服务器需要订阅该临时节点的删除事件,以便该节点删除时(也就是注册该节点的服务器宕机了或者网络断了之类的)进行再次抢主操作。选主的过程,其实就是简单的争抢在Zookeeper注册临时节点的操作,谁注册了约定的临时节点,谁就是master。所有服务器同时会在servers节点下注册一个临时节点(保存自己的基本信息),以便于应用程序读取当前可用的服务器列表。 三、如何使用 <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.9.0</version> </dependency> 通过以上分析我们不难看出,其实想要实现这个简单的选举过程其实是很难的,那么我们下面将借助Curator来实现想要的功能。 public class MasterSelectTest { public static void main(String[] args) throws Exception { String masterPath = "/master_path"; CuratorFramework client = CuratorFrameworkFactory.builder().connectString("127.0.0.1:2181").sessionTimeoutMs(5000).connectionTimeoutMs(10000).retryPolicy(retryPolicy).namespace("text").build(); client.start(); /** * 该实例封装了所有Master选举相关的逻辑,包括所有和Zookeeper服务器交互的过程,其中Master_select代表一个Master选举的一个 * 根节点,表明本次Master选举都是在该节点下进行的。 * 在创建LeaderSelector实例的时候,还会传入一个监听器:LeaderSelectorListenerAdapter。这需要开发人员自行实现。Curator * 会在成功获取Master权利时候回调该监听器。 */ LeaderSelector leaderSelector = new LeaderSelector(client, master_path, new LeaderSelectorListener() { @Override public void stateChanged(CuratorFramework client, ConnectionState stat) { } /** * 成为Master角色 * 完成Master操作,释放Master权利 * 成为Master角色 */ @Override public void takeLeadership(CuratorFramework client) throws Exception { System.out.println("成为Master"); Thread.sleep(3000); List<String> path = client.getChildren().forPath(masterPath); System.out.println(path); System.out.println("完成Master操作,释放Master权利"); } }); leaderSelector.autoRequeue(); leaderSelector.start(); } }
Paste_Image.png 关于分布式任务调用不错的文章有: http://www.cnblogs.com/zuoxiaolong/p/niubi-job-3.html niubi-job 社区资料少,群(包含作者)活跃度极低,有问题靠自己 http://www.iyeele.com/354.html 目前市面上的分布式任务框架有: http://git.oschina.net/xuxueli0323/xxl-job //简单,但针对可靠性需要扩展的地方太多.建议,简单场景使用 https://github.com/dangdangdotcom/elastic-job //资料最多,技术群活跃度高..第一推荐 https://github.com/ltsopensource/light-task-scheduler //产品功能比较全,作者提供有较为详细的资料,可以自己上手. 技术群活跃度中偏低,但是有人会帮忙,第二推荐 https://github.com/vipshop/Saturn //目前已经到2.0版本,可靠性还需要检验 https://github.com/ChinaSilence/AnySchedule //Spring boot重构的调度框架 http://code.taobao.org/p/tbschedule/wiki/tbschedule-quick-start/
前言 我们在日常工作中经常会遇到要求缓存和数据库强一致性的问题,我们平常的做法是,确保数据库插入成功,然后再更新缓存,但有时候数据库插入成功后,缓存出现问题或者缓存系统挂了,这时候请求会直接访问数据库最新的数据,但是当缓存恢复的时候,我们的并发请求又会访问到以前旧的缓存数据,这时候就会出现不一致问题。如果我们的业务系统对一致性要求不高,那么可以这么做,但是如果必须是强一致性,那么这个方案是有明显漏洞的。 有的朋友可能会说,当缓存恢复的时候直接清空缓存就可以了,然后重新加载一遍,这样有二个问题,第一个是我们的缓存数据有一部分其实是不经常变动的,全部清空再加载效率就非常低了。 方案一讲解 数据写入端 Paste_Image.png 注:同样我们是同时写数据库和缓存,但是在写缓存的时候会判断写入是否成功,如果写入出错,我们将key和更新状态值插入到数据库状态表中,同时关闭前端访问缓存的开关。 数据读入端 Paste_Image.png 注:客户端在读数据的时候,要先判断一下当然开关是否打开,如果打开则读取缓存,如果关闭则直接访问数据库。关于判断缓存开关的问题,可以不用每次读库,而可以事先缓存到本地。 缓存恢复端 Paste_Image.png 注:后台有一个守护定时任务,每隔一定时间来检测缓存系统是否可用,如果可用则从状态表中读取key值和更新状态位,然后打开缓存读取开关,这样前端数据读取端则能够从缓存读取最新数据。 总结 这方案的前提是当前并发量并不是非常大的情况,试想如果当前并发非常大,同时缓存又出了问题,这时候整个请求就穿透到了数据库层造成严重问题。 那么我之前的初步想法是将这个流程封装为中间件,根据不同库配置不同的数据源,客户端只需要直接请求中间件即可。但此方案不适合分库分表的场景!在某种情况下是有局限性的!这个方案更多是为大家提供一种思路! 方案二讲解 当缓存不可用时,在第一时间不对数据库服务发起请求,在需要的时候异步填充缓存(优先热点缓存),然后我们将前端的请求直接返回失败,也就是快速返回失败,直到缓存恢复并且热点缓存填充完毕。 Paste_Image.png 注:在某些情况下,这种方法意义不大,但当系统的一部分发生故障时可以确保系统仍然可用的一种方式,让请求快速失败,确保不占用资源,也避免了级联下游服务的故障。
我们接第一篇来继续说明在代码review中,有哪些属于“层次结构”中的坏味道。 第一篇链接如下:http://www.jianshu.com/p/07dbf69c5957 Paste_Image.png 注:通过上图咱们看到了在层次结构中有九大问题点,咱们就从中找出四个典型的问题点给与分析和解释。 一、缺失的层次结构 问题点: public Insets getBorderInsets(Component c, Insets insets) { if(c instanceof AbstractButton) { margin = ((AbstractButton)c).getMargin(); } else if(c instanceof JToolBar) { margin = ((JToolBar)c).getMargin(); } else if(c instanceof JTextComponent) { margin = ((JTextComponent)c).getMargin(); } 注:串接的if else语句显示的检查类型AbstractButton,JToolBar和JTextCompont并在各种条件下调用方法getMargin(),这种造成的情况是将来可能在代码中的其他地方也会出现。 重构建议: 1、如果条件检查中的多个实现调用方法相同,可引入相关的接口来抽象共同的协议。 2、如果代码中包含可转换为类的条件语句,可采用重构手法“提取层次结构”来创建一个类层次结构,其中每个类都表示条件检查中的一种情形。 二、未归并层次结构 问题点: AbstractQueuedSynchronizer和AbstractQueuedLongSynchronizer类都是直接从AbstractOwnableSynchronizer派生而来的(这些类都包含在java.util.concurrent.locks包),这二个子类的很多代码都是重复的,每个类都包含2110行代码,但重复的代码多达1278行。 显然,这二个类的代码绝大部分是相同的,只是在AbstractQueuedLongSynchronizer中使用的是long而不是int,那么我们看这二个类的继承类图如下: Paste_Image.png 重构方案 对于AbstractOwnableSynchronizer,由于子类型中的方法定义相同,因此可采用重构手法上移,将相同的方法定义移到超类中。 Paste_Image.png 三、支离破碎的层次结构 这种层次结构主要体现在,虽然超类和子类之间不存在is-a的关系,但是超类的方法对于子类来说是适用或者相关的。 问题点: Paste_Image.png 注:java.util.Date这个类不仅提供了日期功能,如getDate(),getYeah()等方法,还提供了getTime(),getHours()等时间方法,但是它的二个子类java.sql.Date不支持与时间有关的功能,而java.sql.Time不支持与日期有关的功能,于是java.sql.Date拒绝了从超类继承的所有与时间有关的方法,java.sql.Time拒绝了继承的所有与日期有关的方法。 看一段简单的代码: java.util.Date date = new java.util.Date(); int dateValue = date.getDate(); //不报错,一切正常 date = new java.sql.Time(10,10,10); dateValue = date.getDate(); //将引发IllegalArgumentException异常 重构方案 超类和子类之间并不存在is-a的关系,它们在设计中使用继承只是为了能够利用抽象提供的功能,其实在相关类之间建立关联关系也可以达到这样的目的,采用重构手法”以委拖取代继承”,应用hash-a的关系取代is-a的关系。 总结 在第二篇中我们重点介绍了关于类层次结构方面的坏味道,那么我们将在第三篇中介绍关于封装类方面的故事。