关于源码:Koa源码解析手写

本文基于koa 3.0.0-alpha.1版本源码进行剖析因为koa的源码量非常少,然而体现的思维十分经典和难以记忆,如果忽然要手写koa代码,可能还不肯定能很快写进去,因而本文将集中于如何了解以及记忆koa的代码 本文一些代码块为了演示不便,可能有一些语法排列谬误,因而本文所有代码均能够视为伪代码 文章内容从0到1推导koa 3.0.0-alpha.1版本源码的实现,一步一步欠缺简化版koa的手写逻辑剖析罕用中间件koa-router的源码以及进行对应的手写剖析罕用中间件koa-bodyparser的源码以及进行对应的手写外围代码剖析&手写2.1 koa-composeconst Koa = require('koa');const app = new Koa();app.use(async (ctx, next) => { console.log("中间件1 start"); await next(); console.log("中间件1 end");});app.use(async (ctx, next) => { console.log("中间件2 start"); await next(); console.log("中间件2 end");});app.use(async (ctx, next) => { console.log("中间件3 start"); await next(); console.log("中间件3 end");});app.listen(3000);下面代码块中间件运行流程如下所示下面的运行流程看起来就跟咱们平时开发不太一样,咱们能够看一个类似的场景,比方上面 咱们在fn1()中执行一系列的业务逻辑然而咱们在fn1()遇到了await fn2(),因而咱们得期待fn2()执行结束后能力持续前面的业务逻辑function fn1() { console.log("fn1执行业务逻辑1"); await fn2(); console.log("fn1执行业务逻辑2")}async function fn2() { console.log("fn2执行业务逻辑1");}咱们将fn2作为参数传入 async function fn2() { console.log("fn2执行业务逻辑1");}function fn1(fn2) { console.log("fn1执行业务逻辑1"); await fn2(); console.log("fn1执行业务逻辑2")}如果咱们有fn3、fn4呢? async function fn1(fn2) { console.log("fn1执行业务逻辑1"); await fn2(); console.log("fn1执行业务逻辑2")}async function fn2(fn3) { console.log("fn2执行业务逻辑1"); await fn3(); console.log("fn2执行业务逻辑2")}async function fn3(fn4) { console.log("fn3执行业务逻辑1"); await fn4(); console.log("fn3执行业务逻辑2")}async function fn4() { console.log("fn4执行业务逻辑1"); console.log("fn4执行业务逻辑2")}那如果咱们还有fn5、fn6....呢? ...

July 2, 2023 · 11 min · jiezi

关于源码:开源彩虹易支付源码带搭建开发下载教程免授权

 随着网络的一直倒退,越来越多的人开始守业,其中集体发卡网站就是一个不错的抉择。发卡网站不仅可能为用户提供方便的在线充值服务,还可能为网站管理员带来可观的收益。然而,对于刚刚入门的网站管理员来说,如何搭建一个稳固、平安、易用的发卡网站是一个大问题。本文将介绍一个适宜集体应用的发卡网站源码,帮忙网站管理员轻松搭建本人的发卡网站。 为了可能实现企业发卡网业务,企业须要搭建一套稳固的、灵便的、易于操作的发卡网零碎。因而,企业发卡网源码成为了十分重要的计划之一。 源码:paywks.top/ka 随着市场竞争的加剧和区块链技术的倒退,企业发卡网成为了一种越来越风行的商业模式。企业发卡网是一种新型的电子商务模式,即企业将本人的产品或服务作为银行信用卡销售给客户,客户通过应用信用卡购买商品或服务,从而生产掉信用额度,企业再依据生产状况向客户收取利息、服务费等费用。这种商业模式的长处是可能补救企业资金不足的问题,而且还可能进步客户忠诚度、减少销售额等。 一、开发环境和技术选型 1.开发环境 本我的项目的开发环境为Windows零碎,应用的开发工具是Visual Studio 2017,数据库采纳的是MSSQL Server 2016。 2.技术选型 本我的项目采纳ASP.NET MVC5作为开发框架,应用Entity Framework作为ORM框架,前端采纳Bootstrap框架,数据加密应用AES算法,短信验证码应用云片网API,领取接口应用支付宝API。 二、零碎设计 1.零碎架构 本零碎采纳MVC架构,其中Model层包含了所有与数据库相干的操作,View层包含了所有用户界面,Controller层则负责调用Model层的接口,管制View层的显示逻辑。在Model层中,咱们采纳Entity Framework来实现对象关系映射,大大简化了对数据库的操作。 2.数据库设计 本零碎的数据库设计如下: 用户表(User):存储所有用户的信息,包含用户名、明码、手机号码、余额等。 订单表(Order):存储用户的订单信息,包含订单号、订单状态、商品名称、商品价格等。 商品表(Product):存储所有商品的信息,包含商品名称、商品价格、库存量等。 日志表(Log):存储系统操作日志,包含操作类型、操作工夫、操作人等。 3.用户注册和登录 用户注册和登录是发卡网站的基本功能之一。在本零碎中,用户注册须要输出用户名、明码和手机号码,并通过短信验证码验证身份。用户登录则须要输出用户名和明码,零碎会对用户的身份进行验证,如果验证通过则进入用户的集体核心页面。 4.商品展现和购买 在本零碎中,所有的商品都会在首页进行展现,用户能够抉择须要购买的商品并进行结算。零碎反对支付宝领取和余额领取两种领取形式,用户能够依据本人的需要进行抉择。如果用户应用余额领取,零碎会对用户余额进行扣款,并将商品的激活码发送到用户的手机上。 5.系统管理 为了保证系统的安全性和稳定性,本零碎还提供了系统管理性能。管理员能够通过后盾治理页面对用户信息、商品信息和订单信息进行治理,也能够查看零碎的操作日志。系统管理性能还反对用户的解冻和冻结操作,以及商品的上架和下架操作,不便管理员对系统进行保护。 三、系统优化 为了进步零碎的性能和安全性,本零碎采纳了以下几种优化措施: 1.数据加密 为了保障用户的数据安全,本零碎采纳AES算法对用户明码和激活码进行加密解决,避免数据泄露。 2.短信验证码 为了避免歹意注册和登录,本零碎采纳云片网API发送短信验证码,无效保障了用户身份的安全性。 3.领取接口 为了保障用户领取的安全性,本零碎采纳支付宝API作为领取接口,保障了用户的资金平安。 4.数据缓存 为了进步零碎的性能,本零碎采纳了Redis作为数据缓存,缩小了对数据库的拜访,进步了零碎的响应速度。 四、企业发卡网的特点 1. 灵活性:企业发卡网的次要特点是灵活性,它容许企业依据本身需要来定制信用卡的应用形式,如限度生产类型、金额、地点等。 2. 危险管制:企业发卡网的另一个特点是危险管制。企业须要通过信用评估系统对客户进行危险评估,以防止客户产生欺诈行为或守约行为。 3. 数据分析:企业发卡网还须要领有一套欠缺的数据分析系统,以帮忙企业对客户的生产习惯进行剖析,进步服务质量和销售额。 五、企业发卡网的架构 企业发卡网的架构包含前端、后端和数据库三个局部。 1. 前端:企业发卡网的前端负责向用户展现产品、提供服务并接管用户申请等。 2. 后端:企业发卡网的后端负责解决用户申请的业务逻辑,包含信用评估、贷款发放、还款治理等。 3. 数据库:企业发卡网的数据库负责数据的存储和治理,包含用户信息、账户余额、流水记录等。 企业发卡网的架构须要具备高可用性、高性能和高扩展性的特点,以应答业务增长和客户量的变动。 六、企业发卡网源码的重要性 企业发卡网源码是企业搭建发卡网零碎的根底,它能够缩短企业搭建零碎的工夫和老本,进步零碎的可靠性。企业发卡网源码提供了多种性能和模块,包含用户治理、卡片治理、还款治理、危险管制等,企业能够依据本身需要选取相应的性能和模块进行定制化开发。企业发卡网源码还提供了多种技术计划和架构设计,帮忙企业进步零碎的性能、可扩展性和稳定性。 七、企业发卡网源码的抉择 企业须要选取适宜本身业务需要的企业发卡网源码。在抉择源码时,企业须要思考以下因素: 1. 可靠性:源码的稳定性和可靠性是企业抉择的首要因素。 2. 可扩展性:企业发卡网须要具备良好的可扩展性,以应答将来业务增长和客户量的变动。 3. 技术计划:源码提供的技术计划须要合乎企业的技术栈和业务需要,以便疾速实现零碎的定制化开发和保护。 4. 性能:源码须要具备高性能和低提早的特点,以进步用户体验和零碎效率。 5. 用户体验:企业发卡网须要具备良好的用户体验,包含页面交互、响应速度、界面设计等。 八、企业发卡网的利用 企业发卡网次要利用于在线生产、领取和借贷场景,包含以下利用: 1. 电商平台:通过发卡网技术,企业能够将本人的产品和服务作为信用卡提供给客户,在线销售商品或服务,并收取肯定的利息、服务费等。 2. 金融机构:金融机构能够将发卡网技术利用于信用卡发行、生产贷款、集体借贷等场景。 3. P2P平台:P2P平台能够通过发卡网技术实现集体借贷和生产贷款等业务。 九、总结 本文介绍了一个适宜集体应用的发卡网站源码,包含零碎架构、数据库设计、用户注册和登录、商品展现和购买以及系统管理等方面。本零碎采纳ASP.NET MVC5作为开发框架,应用Entity Framework作为ORM框架,前端采纳Bootstrap框架,数据加密应用AES算法,短信验证码应用云片网API,领取接口应用支付宝API。本零碎还采纳了数据缓存等优化措施,进步了零碎的性能和安全性。对于刚刚入门的网站管理员来说,这个发卡网站源码是一个不错的抉择。 企业发卡网作为一种新型商业模式,曾经在多个畛域失去了利用。企业须要依据本身需要选取适宜的企业发卡网源码,并进行二次开发和优化,以构建稳固、高性能、灵便的发卡网零碎。企业发卡网将进一步促成金融翻新和生产降级,成为新一轮产业改革的重要驱动力。 ...

June 8, 2023 · 1 min · jiezi

关于源码:故障分析-从-Insert-并发死锁分析-Insert-加锁源码逻辑

作者:李锡超 一个爱笑的江苏苏宁银行 数据库工程师,次要负责数据库日常运维、自动化建设、DMP平台运维。善于MySQL、Python、Oracle,喜好骑行、钻研技术。 本文起源:原创投稿 *爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。 一、前言死锁,作为数据库一个常见的并发问题。此类问题: 1.触发起因往往与利用的逻辑相干,参加的事务可能是两个、三个、甚至更多; 2.因为不同数据库的锁实现机制简直齐全不同、实现逻辑简单,还存在多种锁类型; 3.数据库产生死锁后,会立刻终止局部事务,预先无奈看到死锁前的期待状态。 即,死锁问题具备业务关联、机制简单、类型多样等特点,导致当数据库产生死锁问题时,不是那么容易剖析。 基于解决死锁问题存在的难点,本文以MySQL数据库一则并发Insert导致的死锁为例,从发现问题、重现问题、根因剖析、解决问题4个步骤,冀望能提供一套对于死锁的迷信无效计划,供读者敌人参考。 二、问题景象某零碎在进行上线前压测时,发现利用日志存在如下日志提醒触发死锁问题: Deadlock found when trying to get lock; try restarting transaction好在压测时,就发现了问题,防止上线后影响生产。 随后,执行 show engine innodb status,有如下内容(脱敏后): ------------------------LATEST DETECTED DEADLOCK------------------------2023-03-24 19:07:50 140736694093568*** (1) TRANSACTION:TRANSACTION 56118, ACTIVE 6 sec insertingmysql tables in use 1, locked 1LOCK WAIT 2 lock struct(s), heap size 1192, 1 row lock(s), undo log entries 1MySQL thread id 9, OS thread handle 140736685700864, query id 57 localhost root updateinsert into dl_tab(id,name) values(30,10)*** (1) HOLDS THE LOCK(S):RECORD LOCKS space id 11 page no 5 n bits 72 index ua of table `testdb`.`dl_tab` trx id 56118 lock mode S waitingRecord lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; # 十进制: 10 1: len 4; hex 8000001a; asc ;; # 十进制: 26*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 11 page no 5 n bits 72 index ua of table `testdb`.`dl_tab` trx id 56118 lock mode S waitingRecord lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; # 十进制: 10 1: len 4; hex 8000001a; asc ;; # 十进制: 26*** (2) TRANSACTION:TRANSACTION 56113, ACTIVE 12 sec insertingmysql tables in use 1, locked 1LOCK WAIT 3 lock struct(s), heap size 1192, 2 row lock(s), undo log entries 2MySQL thread id 8, OS thread handle 140736952903424, query id 58 localhost root updateinsert into dl_tab(id,name) values(40,8)*** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 11 page no 5 n bits 72 index ua of table `testdb`.`dl_tab` trx id 56113 lock_mode X locks rec but not gapRecord lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; # 十进制: 10 1: len 4; hex 8000001a; asc ;; # 十进制: 26*** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 11 page no 5 n bits 72 index ua of table `testdb`.`dl_tab` trx id 56113 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; # 十进制: 10 1: len 4; hex 8000001a; asc ;; # 十进制: 26*** WE ROLL BACK TRANSACTION (1)------------1、死锁信息梳理依据以上信息,发现是 dl_tab 执行insert操作导致死锁。初步梳理如下。 ...

April 23, 2023 · 5 min · jiezi

关于源码:一文吃透低代码平台源代码交付的重要性避坑指南

一、前言作为这两年IT界的风口,低代码在众人眼里曾经不是什么生疏的概念。对标于传统的纯代码开发,低代码是一种疾速开发软件(应用程序)的办法,平台通过对大量性能与场景做提前封装,使得用户能够在可视化的根底上,通过利落拽就能实现开发,手动编码非常少。 这种可视化的开发大大不便了开发者,但也会导致开发者对本人开发我的项目的底层逻辑并不齐全理解,一旦呈现非凡状况就会难以解决,置信这也是泛滥程序员放心的问题。但如果领有平台的底层代码,就能迎刃而解。 二、论述源码的重要性事实上,目前国内大多数低代码产品都不会提供源码给客户,许多平台更违心做SaaS服务,按应用时长与服务数量进行免费,交付源码岂不是“自砸饭碗”?也正是因为这个环境,许多客户在对低代码平台进行选型的时候,往往疏忽了其源代码的重要性。  有了源码,低代码平台的实用性会大幅回升:1、学习晋升:能够通过剖析源代码,来学习、理解开发者的思路,学习开发者如何通过奇妙的形式、算法解决业务问题。总的来说,浏览源代码是最快晋升开发程度的一种形式。 2、二开自在:占据二次开发的劣势位置。后续能够在源代码的根底上自在组织二次开发,欠缺和丰盛现有零碎性能。 3、软著的主动权:源代码意味着主动权。手握源代码,能够自主申请软件著作权,晋升本身的企业形象,减少无形资产。 三、源码全交付的黑马厂商JNPF疾速开发平台是市面上为数不多向客户实现全源码交付机制的低代码平台,采纳业内当先的SpringBoot微服务架构、反对SpringCloud模式,欠缺了平台的扩增根底,满足了零碎疾速开发、灵便拓展、无缝集成和高性能利用等综合能力;采纳前后端拆散模式,前端和后端的开发人员可分工合作负责不同板块,省事又便捷。 此外,该平台采纳的是业内先进的引擎式软件疾速开发模式,精心配置了流程引擎、表单引擎、报表引擎、图表引擎、接口引擎、门户引擎、组织用户引擎等可视化性能引擎,内置超过数百种性能控件以及大量实用模板,使得在利落拽的简略操作下,也能大限度满足用户个性化需要,轻松实现开发。 开源体验:JNPF 同时,它反对私有化部署的形式,间接把零碎部署在用户本地服务器上,无效实现内外网隔离,数据安全把握在本人手里,安全性、可控性与稳定性有所保障,大幅升高数据外泄的危险。 四、小结说到这里,置信许多人对低代码平台抱有心动又犹豫的态度:心动于它的高性价比,又犹豫平台到底好不好用。倡议搭档们能够去到JNPF官网撸一把,低代码不仅降本增效,业务灵活性也很值得你把玩一番。

April 19, 2023 · 1 min · jiezi

关于源码:美食小吃加盟网站源码餐饮奶茶招商加盟类网站

demo软件园每日更新资源,请看到最初就能获取你想要的: 1.机器学习与数据迷信基于R的统计学习办法   机器学习与数据迷信:基于R的统计学习办法电子书封面 读者评估 机器学习是近年来渐趋热门的一个畛域,同时R语言通过一段时间的倒退也已逐步成为支流的编程语言之一。本书联合了机器学习和R语言两个热门的畛域,通过利用两种外围的机器学习算法来将R语言在数据分析方面的劣势施展到极致。 还不错吧!本人也跟着学习下,就是好多看不懂,专业性强! 从业者应用的工具是决定他的工作是否胜利的重要因素之一。本书为数据科学家提供了一些在统计学习畛域会用到的工具和技巧,为他们在数据迷信畛域的长期职业生涯提供了所需的一套根本工具。针对解决重要的数据迷信问题的高级技能,本书也给出了学习的倡议。 本书包含以下内容: 机器学习概述 监督机器学习 数据连贯 非监督机器学习 数据处理 模型评估 探索性数据分析 本书选用R统计环境。R在全世界范畴内利用越来越宽泛,很多数据科学家只应用R就能进行我的项目工作。本书的所有代码示例都是用R语言写的。除此之外,书中还应用了很多风行的R包和数据集。 内容介绍 以后,机器学习和数据迷信都是很重要和热门的相干学科,须要深刻地钻研学习能力精通。 本书试图领导读者把握如何实现波及机器学习的数据迷信我的项目。本书将为数据科学家提供一些在统计学习畛域会用到的工具和技巧,波及数据连贯、数据处理、探索性数据分析、监督机器学习、非监督机器学习和模型评估。本书选用的是R统计环境,书中所有代码示例都是用R语言编写的,波及泛滥风行的R包和数据集。 本书适宜数据科学家、数据分析师、软件开发者以及须要理解数据迷信和机器学习办法的科研人员浏览参考。 目录 第1章机器学习综述1 第2章连贯数据25 第3章数据处理54 第4章探索性数据分析83 第5章回归107 第6章分类136 第7章评估模型性能176 第8章非监督学习208 术语表234 2.美食小吃加盟网站源码餐饮奶茶招商加盟类网站pbootcms模板(PC+WAP) PbootCMS内核开发的网站模板,该模板实用于招商加盟网站、美食小吃加盟网站等企业,当然其余行业也能够做,只须要把文字图片换成其余行业的即可; PC+WAP,同一个后盾,数据即时同步,简略实用!附带测试数据! 敌对的seo,所有页面均都能齐全自定义题目/关键词/形容,PHP程序,平安、稳固、疾速;用低成本获取源源不断订单! 后盾:域名/admin.php 账号:admin 明码:admin 模板特点 1:手工书写DIV+CSS、代码精简无冗余。 2:自适应构造,寰球先进技术,高端视觉体验。 3:SEO框架布局,栏目及文章页均可独立设置题目/关键词/形容。 4:附带测试数据、装置教程、入门教程、平安及备份教程。 5:后盾间接批改联系方式、传真、邮箱、地址等,批改更加不便。 页面成果: 理解更多请点我头像或到我的主页去取得,谢谢

April 1, 2023 · 1 min · jiezi

关于源码:zookeeper的Leader选举源码解析

作者:京东物流 梁吉超 zookeeper是一个分布式服务框架,次要解决分布式应用中常见的多种数据问题,例如集群治理,状态同步等。为解决这些问题zookeeper须要Leader选举进行保障数据的强一致性机制和稳定性。本文通过集群的配置,对leader选举源进行解析,让读者们理解如何利用BIO通信机制,多线程多层队列实现高性能架构。 01Leader选举机制Leader选举机制采纳半数选举算法。 每一个zookeeper服务端称之为一个节点,每个节点都有投票权,把其选票投向每一个有选举权的节点,当其中一个节点选举出票数过半,这个节点就会成为Leader,其它节点成为Follower。 02Leader选举集群配置重命名zoo_sample.cfg文件为zoo1.cfg ,zoo2.cfg,zoo3.cfg,zoo4.cfg批改zoo.cfg文件,批改值如下:【plain】zoo1.cfg文件内容:dataDir=/export/data/zookeeper-1clientPort=2181server.1=127.0.0.1:2001:3001server.2=127.0.0.1:2002:3002:participantserver.3=127.0.0.1:2003:3003:participantserver.4=127.0.0.1:2004:3004:observerzoo2.cfg文件内容:dataDir=/export/data/zookeeper-2clientPort=2182server.1=127.0.0.1:2001:3001server.2=127.0.0.1:2002:3002:participantserver.3=127.0.0.1:2003:3003:participantserver.4=127.0.0.1:2004:3004:observerzoo3.cfg文件内容:dataDir=/export/data/zookeeper-3clientPort=2183server.1=127.0.0.1:2001:3001server.2=127.0.0.1:2002:3002:participantserver.3=127.0.0.1:2003:3003:participantserver.4=127.0.0.1:2004:3004:observerzoo4.cfg文件内容:dataDir=/export/data/zookeeper-4clientPort=2184server.1=127.0.0.1:2001:3001server.2=127.0.0.1:2002:3002:participantserver.3=127.0.0.1:2003:3003:participantserver.4=127.0.0.1:2004:3004:observerserver.第几号服务器(对应myid文件内容)=ip:数据同步端口:选举端口:选举标识participant默认参加选举标识,可不写. observer不参加选举4.在/export/data/zookeeper-1,/export/data/zookeeper-2,/export/data/zookeeper-3,/export/data/zookeeper-4目录下创立myid文件,文件内容别离写1 ,2,3,4,用于标识sid(全称:Server ID)赋值。 启动三个zookeeper实例:bin/zkServer.sh start conf/zoo1.cfgbin/zkServer.sh start conf/zoo2.cfgbin/zkServer.sh start conf/zoo3.cfg每启动一个实例,都会读取启动参数配置zoo.cfg文件,这样实例就能够晓得其作为服务端身份信息sid以及集群中有多少个实例参加选举。03Leader选举流程 图1 第一轮到第二轮投票流程 前提: 设定票据数据格式vote(sid,zxid,epoch) sid是Server ID每台服务的惟一标识,是myid文件内容;zxid是数据事务id号;epoch为选举周期,为不便了解上面解说内容暂定为1首次选举,不写入上面内容里。依照程序启动sid=1,sid=2节点 第一轮投票: sid=1节点:初始选票为本人,将选票vote(1,0)发送给sid=2节点;sid=2节点:初始选票为本人,将选票vote(2,0)发送给sid=1节点;sid=1节点:收到sid=2节点选票vote(2,0)和以后本人的选票vote(1,0),首先比对zxid值,zxid越大代表数据最新,优先选择zxid最大的选票,如果zxid雷同,选举最大sid。以后投票选举后果为vote(2,0),sid=1节点的选票变为vote(2,0);sid=2节点:收到sid=1节点选票vote(1,0)和以后本人的选票vote(2,0),参照上述选举形式,选举后果为vote(2,0),sid=2节点的选票不变;第一轮投票选举完结。第二轮投票: sid=1节点:以后本人的选票为vote(2,0),将选票vote(2,0)发送给sid=2节点;sid=2节点:以后本人的选票为vote(2,0),将选票vote(2,0)发送给sid=1节点;sid=1节点:收到sid=2节点选票vote(2,0)和本人的选票vote(2,0), 依照半数选举算法,总共3个节点参加选举,已有2个节点选举出雷同选票,推举sid=2节点为Leader,本人角色变为Follower;sid=2节点:收到sid=1节点选票vote(2,0)和本人的选票vote(2,0),依照半数选举算法推举sid=2节点为Leader,本人角色变为Leader。这时启动sid=3节点后,集群里曾经选举出leader,sid=1和sid=2节点会将本人的leader选票发回给sid=3节点,通过半数选举后果还是sid=2节点为leader。 3.1 Leader选举采纳多层队列架构zookeeper选举底层次要分为选举应用层和音讯传输队列层,第一层应用层队列对立接管和发送选票,而第二层传输层队列,是依照服务端sid分成了多个队列,是为了防止给每台服务端发送音讯相互影响。比方对某台机器发送不胜利不会影响失常服务端的发送。 图2 多层队列高低关系交互流程图 04解析代码入口类通过查看zkServer.sh文件内容找到服务启动类: org.apache.zookeeper.server.quorum.QuorumPeerMain 05选举流程代码解析 图3 选举代码实现流程图 加载配置文件QuorumPeerConfig.parse(path);针对 Leader选举要害配置信息如下: 读取dataDir目录找到myid文件内容,设置以后利用sid标识,做为投票人身份信息。上面遇到myid变量为以后节点本人sid标识。设置peerType以后利用是否参加选举new QuorumMaj()解析server.前缀加载集群成员信息,加载allMembers所有成员,votingMembers参加选举成员,observingMembers观察者成员,设置half值votingMembers.size()/2.【Java】public QuorumMaj(Properties props) throws ConfigException { for (Entry<Object, Object> entry : props.entrySet()) { String key = entry.getKey().toString(); String value = entry.getValue().toString(); //读取集群配置文件中的server.结尾的利用实例配置信息 if (key.startsWith("server.")) { int dot = key.indexOf('.'); long sid = Long.parseLong(key.substring(dot + 1)); QuorumServer qs = new QuorumServer(sid, value); allMembers.put(Long.valueOf(sid), qs); if (qs.type == LearnerType.PARTICIPANT)//利用实例绑定的角色为PARTICIPANT意为参加选举 votingMembers.put(Long.valueOf(sid), qs); else { //观察者成员 observingMembers.put(Long.valueOf(sid), qs); } } else if (key.equals("version")) { version = Long.parseLong(value, 16); } } //过半基数 half = votingMembers.size() / 2; }QuorumPeerMain.runFromConfig(config) 启动服务;QuorumPeer.startLeaderElection() 开启选举服务;设置以后选票new Vote(sid,zxid,epoch)【plain】synchronized public void startLeaderElection(){try { if (getPeerState() == ServerState.LOOKING) { //首轮:以后节点默认投票对象为本人 currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch()); } } catch(IOException e) { RuntimeException re = new RuntimeException(e.getMessage()); re.setStackTrace(e.getStackTrace()); throw re; }//........}创立选举治理类:QuorumCnxnManager;初始化recvQueue<Message(sid,ByteBuffer)>接管投票队列(第二层传输队列);初始化queueSendMap<sid,queue>按sid发送投票队列(第二层传输队列);初始化senderWorkerMap<sid,SendWorker>发送投票工作线程容器,示意着与sid投票节点已连贯;初始化选举监听线程类QuorumCnxnManager.Listener。【Java】//QuorumPeer.createCnxnManager()public QuorumCnxManager(QuorumPeer self, final long mySid, Map<Long,QuorumPeer.QuorumServer> view, QuorumAuthServer authServer, QuorumAuthLearner authLearner, int socketTimeout, boolean listenOnAllIPs, int quorumCnxnThreadsSize, boolean quorumSaslAuthEnabled) { //接管投票队列(第二层传输队列) this.recvQueue = new ArrayBlockingQueue<Message>(RECV_CAPACITY); //按sid发送投票队列(第二层传输队列) this.queueSendMap = new ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>>(); //发送投票工作线程容器,示意着与sid投票节点已连贯 this.senderWorkerMap = new ConcurrentHashMap<Long, SendWorker>(); this.lastMessageSent = new ConcurrentHashMap<Long, ByteBuffer>(); String cnxToValue = System.getProperty("zookeeper.cnxTimeout"); if(cnxToValue != null){ this.cnxTO = Integer.parseInt(cnxToValue); } this.self = self; this.mySid = mySid; this.socketTimeout = socketTimeout; this.view = view; this.listenOnAllIPs = listenOnAllIPs; initializeAuth(mySid, authServer, authLearner, quorumCnxnThreadsSize, quorumSaslAuthEnabled); // Starts listener thread that waits for connection requests //创立选举监听线程 接管选举投票申请 listener = new Listener(); listener.setName("QuorumPeerListener");}//QuorumPeer.createElectionAlgorithmprotected Election createElectionAlgorithm(int electionAlgorithm){ Election le=null; //TODO: use a factory rather than a switch switch (electionAlgorithm) { case 0: le = new LeaderElection(this); break; case 1: le = new AuthFastLeaderElection(this); break; case 2: le = new AuthFastLeaderElection(this, true); break; case 3: qcm = createCnxnManager();// new QuorumCnxManager(... new Listener()) QuorumCnxManager.Listener listener = qcm.listener; if(listener != null){ listener.start();//启动选举监听线程 FastLeaderElection fle = new FastLeaderElection(this, qcm); fle.start(); le = fle; } else { LOG.error("Null listener when initializing cnx manager"); } break; default: assert false; }return le;}开启选举监听线程QuorumCnxnManager.Listener;创立ServerSockket期待大于本人sid节点连贯,连贯信息存储到senderWorkerMap<sid,SendWorker>;sid>self.sid才能够连贯过去。【Java】//下面的listener.start()执行后,抉择此办法public void run() { int numRetries = 0; InetSocketAddress addr; Socket client = null; while((!shutdown) && (numRetries < 3)){ try { ss = new ServerSocket(); ss.setReuseAddress(true); if (self.getQuorumListenOnAllIPs()) { int port = self.getElectionAddress().getPort(); addr = new InetSocketAddress(port); } else { // Resolve hostname for this server in case the // underlying ip address has changed. self.recreateSocketAddresses(self.getId()); addr = self.getElectionAddress(); } LOG.info("My election bind port: " + addr.toString()); setName(addr.toString()); ss.bind(addr); while (!shutdown) { client = ss.accept(); setSockOpts(client); LOG.info("Received connection request " + client.getRemoteSocketAddress()); // Receive and handle the connection request // asynchronously if the quorum sasl authentication is // enabled. This is required because sasl server // authentication process may take few seconds to finish, // this may delay next peer connection requests. if (quorumSaslAuthEnabled) { receiveConnectionAsync(client); } else {//接管连贯信息 receiveConnection(client); } numRetries = 0; } } catch (IOException e) { if (shutdown) { break; } LOG.error("Exception while listening", e); numRetries++; try { ss.close(); Thread.sleep(1000); } catch (IOException ie) { LOG.error("Error closing server socket", ie); } catch (InterruptedException ie) { LOG.error("Interrupted while sleeping. " + "Ignoring exception", ie); } closeSocket(client); } } LOG.info("Leaving listener"); if (!shutdown) { LOG.error("As I'm leaving the listener thread, " + "I won't be able to participate in leader " + "election any longer: " + self.getElectionAddress()); } else if (ss != null) { // Clean up for shutdown. try { ss.close(); } catch (IOException ie) { // Don't log an error for shutdown. LOG.debug("Error closing server socket", ie); } }}//代码执行门路:receiveConnection()->handleConnection(...)private void handleConnection(Socket sock, DataInputStream din) throws IOException {//...省略 if (sid < self.getId()) { /* * This replica might still believe that the connection to sid is * up, so we have to shut down the workers before trying to open a * new connection. */ SendWorker sw = senderWorkerMap.get(sid); if (sw != null) { sw.finish(); } /* * Now we start a new connection */ LOG.debug("Create new connection to server: {}", sid); closeSocket(sock); if (electionAddr != null) { connectOne(sid, electionAddr); } else { connectOne(sid); } } else { // Otherwise start worker threads to receive data. SendWorker sw = new SendWorker(sock, sid); RecvWorker rw = new RecvWorker(sock, din, sid, sw); sw.setRecv(rw); SendWorker vsw = senderWorkerMap.get(sid); if (vsw != null) { vsw.finish(); } //存储连贯信息<sid,SendWorker> senderWorkerMap.put(sid, sw); queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY)); sw.start(); rw.start(); }}创立FastLeaderElection疾速选举服务;初始选票发送队列sendqueue(第一层队列)初始选票接管队列recvqueue(第一层队列)创立线程WorkerSender创立线程WorkerReceiver【Java】//FastLeaderElection.starterprivate void starter(QuorumPeer self, QuorumCnxManager manager) { this.self = self; proposedLeader = -1; proposedZxid = -1; //发送队列sendqueue(第一层队列) sendqueue = new LinkedBlockingQueue<ToSend>(); //接管队列recvqueue(第一层队列) recvqueue = new LinkedBlockingQueue<Notification>(); this.messenger = new Messenger(manager);}//new Messenger(manager)Messenger(QuorumCnxManager manager) { //创立线程WorkerSender this.ws = new WorkerSender(manager); this.wsThread = new Thread(this.ws, "WorkerSender[myid=" + self.getId() + "]"); this.wsThread.setDaemon(true); //创立线程WorkerReceiver this.wr = new WorkerReceiver(manager); this.wrThread = new Thread(this.wr, "WorkerReceiver[myid=" + self.getId() + "]"); this.wrThread.setDaemon(true);}开启WorkerSender和WorkerReceiver线程。WorkerSender线程自旋获取sendqueue第一层队列元素 ...

March 29, 2023 · 9 min · jiezi

关于源码:源码阅读gozero的coreconf包

这个代码库次要用于加载和解析配置文件,反对 JSON、TOML 和 YAML 格局。次要性能包含从文件或字节数据中加载配置、填充默认值以及解决配置数据的键大小写。代码的次要构造和函数如下: fieldInfo 构造体:用于示意字段信息,包含子字段和映射字段。从文件或字节数据加载配置的函数:Load, LoadConfig, LoadFromJsonBytes, LoadConfigFromJsonBytes, LoadFromTomlBytes, LoadFromYamlBytes, LoadConfigFromYamlBytes 和 MustLoad。构建和解决字段信息的函数:buildFieldsInfo, buildNamedFieldInfo, buildAnonymousFieldInfo, buildStructFieldsInfo, addOrMergeFields 和 mergeFields。解决字符串、映射和数组数据的辅助函数:toLowerCase, toLowerCaseInterface, toLowerCaseKeyMap,以及示意键反复谬误的自定义类型 dupKeyError 和相干函数。整个库的性能是通过反射和递归地解决构造体字段信息来实现的。在加载配置时,首先将 TOML 和 YAML 格局的数据转换为 JSON 格局,而后对立解决 JSON 数据。配置数据加载后,库会确保数据的键与构造体字段的名称匹配,以便将数据正确地填充到构造体中。 开始起因是在浏览疾速开始go-zero服务时,主函数调用了这两个包,为了不便了解主函数和go-zero框架,同时也为了学习优质源码,进步代码能力,疾速浏览了这两个包的内容。go-zero-demohttps://go-zero.dev/cn/docs/quick-start/monolithic-service其中主函数如下 package mainimport ( "flag" "fmt" "go-zero-demo/greet/internal/config" "go-zero-demo/greet/internal/handler" "go-zero-demo/greet/internal/svc" "github.com/zeromicro/go-zero/core/conf" "github.com/zeromicro/go-zero/rest")var configFile = flag.String("f", "etc/greet-api.yaml", "the config file")func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) ctx := svc.NewServiceContext(c) server := rest.MustNewServer(c.RestConf) defer server.Stop() handler.RegisterHandlers(server, ctx) fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) server.Start()}其中 ...

March 29, 2023 · 4 min · jiezi

关于源码:React-Hooks源码深度解析

作者:京东批发 郑炳懿前言React Hooks是React16.8 引入的一个新个性,它容许函数组件中应用state和其余 React 个性,而不用应用类组件。Hooks是一个十分重要的概念,因为它们提供了更简略、更易于了解的React开发体验。 React Hooks的外围源码次要包含两个局部:React外部的Hook管理器和一系列预置的Hook函数。 首先,让咱们看一下React外部的Hook管理器。这个管理器是React外部的一个重要机制,它负责管理组件中的所有Hook,并确保它们在组件渲染期间以正确的顺序调用。 外部Hook管理器示例: const Hook = { queue: [], current: null,};function useState(initialState) { const state = Hook.current[Hook.queue.length]; if (!state) { Hook.queue.push({ state: typeof initialState === 'function' ? initialState() : initialState, setState(value) { this.state = value; render(); }, }); } return [state.state, state.setState.bind(state)];}function useHook(callback) { Hook.current = { __proto__: Hook.current, }; try { callback(); } finally { Hook.current = Hook.current.__proto__; }}function render() { useHook(() => { const [count, setCount] = useState(0); console.log('count:', count); setTimeout(() => { setCount(count + 1); }, 1000); });}render();在这个示例中,Hook对象有两个重要属性:queue和current。queue存储组件中所有Hook的状态和更新函数,current存储以后正在渲染的组件的Hook链表。useState和useHook函数则别离负责创立新的Hook状态和在组件中应用Hook。 ...

March 9, 2023 · 2 min · jiezi

关于源码:TiKV-源码阅读三部曲二读流程

TiKV 是一个反对事务的分布式 Key-Value 数据库,目前曾经是 CNCF 基金会 的顶级我的项目。 作为一个新同学,须要肯定的后期筹备才可能有能力参加 TiKV 社区的代码开发,包含但不限于学习 Rust 语言,了解 TiKV 的原理和在前两者的根底上理解相熟 TiKV 的源码。 TiKV 官网源码解析文档 具体地介绍了 TiKV 3.x 版本重要模块的设计要点,次要流程和相应代码片段,是学习 TiKV 源码必读的学习材料。以后 TiKV 曾经迭代到了 6.x 版本,不仅引入了很多新的性能和优化,而且对源码也进行了屡次重构,因此一些官网源码解析文档中的代码片段曾经不复存在,这使得读者在浏览源码解析文档时无奈对照最新源码加深了解;此外只管 TiKV 官网源码解析文档系统地介绍了若干重要模块的工作,但并没有将读写流程全链路串起来去介绍通过的模块和对应的代码片段,实际上尽快地相熟读写流程全链路会更利于新同学从全局角度了解代码。 基于以上存在的问题,笔者将基于 6.1 版本的源码撰写三篇博客,别离介绍以下三个方面: TiKV 源码浏览三部曲(一)重要模块:TiKV 的基本概念,TiKV 读写门路上的三个重要模块(KVService,Storage,RaftStore)和断点调试 TiKV 学习源码的计划TiKV 源码浏览三部曲(二)读流程:TiKV 中一条读申请的全链路流程TiKV 源码浏览三部曲(三)写流程:TiKV 中一条写申请的全链路流程心愿此三篇博客可能帮忙对 TiKV 开发感兴趣的新同学尽快理解 TiKV 的 codebase。本文为第二篇博客,将次要介绍 TiKV 中一条读申请的全链路流程。 读流程TiKV 源码解析系列文章(十九)read index 和 local read 情景剖析 介绍了 TiKV 3.x 版本的 ReadIndex/LeaseRead 实现计划。 本大节将在 TiKV 6.1 版本的源码根底上,以一条读申请为例,介绍以后版本读申请的全链路执行流程。 前文曾经提到,能够从 kvproto 对应的 service Tikv 中理解以后 TiKV 反对的 RPC 接口。 ...

October 27, 2022 · 8 min · jiezi

关于源码:算命源码八字算命风水测字占卜起名周易程序源码占卜类源码PHP

 明天,简直每个受过教育的人都会对占卜不屑一顾,并宣称没有方法预测将来。这可能是粗率的,因为对于咱们宇宙的很多事件都是能够预测的——即便是其中最弱小的:天体的静止。 源码及演示:m.appwin.top 大多数占卜技术都波及机会——纯正的随机性。易经也是如此,你能够通过掷硬币或扔棍子来取得你的卦。机会是其中最大的谜。 在一个受自然规律束缚的宇宙中,机会是不可能存在的。这只不过是咱们对世界如何运作的常识的限度。如果咱们有公式,咱们应该可能计算将来。矛盾的是,诸如《易经》之类的必然性办法仿佛渗透到了咱们的公式没有渗透到的将来畛域,就如同宇宙的根本法令是建设在必然性之上的。好吧,量子物理学仿佛正朝着对宇宙的这种了解迈进。 无论如何,在占卜技巧中,我发现《易经》是最有价值的。我想这是因为《易经》用文字谈话,就像咱们整个物种喜爱做的那样。这使得它的预测对咱们来说很容易了解并且出其不意地显著。 这是我用来构建一个小而乏味的网页的过程。我心愿这能够帮忙揭开建设网站的过程的神秘面纱,即便只是一点点。 我的第一步是弄清楚易经是如何工作的,这意味着去维基百科浏览它。这让我有点胆怯,因为过程相当简单。你晓得,这就像OG算法。密码学中应用了一种算法(并在 macOS 的 /dev/random 中应用),它以易经占卜办法之一命名,即 Yarrow 算法。好吧,这曾经足够“钻研”了,因为当初我晓得我须要晓得什么——我要么须要构建其中一种占卜算法,要么找到一种,要么伪装去做。 而后我去了现有的易经网站,看看它们是如何构建的——它们仿佛都是用 PHP 构建的,因而以一种混同的形式运行(所以我不能把它们撕掉)。 我找到了易经易变应用程序和构建易经卦结构“蓍草法”的开源代码片段。如许救命啊!领有 MIT 许可证的救生员!当初我能够确信我的网站将应用最好的预言机征询。 从可能波及数学的局部中劳动一下,我通过从维基百科做一些复制粘贴技巧和在 rad 文本编辑器中进行多光标粘贴来设置一些 JSON(我目前正在应用 Atom)。计算机很棒,只需点击几下,而不是苦楚地复制粘贴 64 个卦的每个局部。那是64x3!我增加了一个定义、代表每个十六进制的符号(尽管不牢靠但很酷)和数字(这样我就能够链接到基于六角数字后果的其余网站)。 难题是算法,这曾经解决了。简略的问题是如何利用该算法,并基于六次点击(通过掷六次蓍草并数数,或应用三个硬币等)生成一个投射卦和转换卦。看了开源的 yarrow-sorting 代码,想出了一个计划,有1示意一直不变的线,0示意断不变的线,x示意一直的线变断(强变弱),o示意一条虚线变为不间断(从弱到强)。 每次单击按钮都会进行这种排序并将后果(四个选项之一)利用于字符串,从而生成一个字符串,该字符串能够通过切换x和o理解他们是什么以及他们将成为什么。这最后让我难以了解,但一旦我了解了它,它就很容易在 Javascript 中实现,因而能够通过从 hexagrams.json 文件中提取代码来显示每个六角星。 (我也不想建设一个服务器来测试这个,而且 json 文件不是很大,所以我实际上伪造了它并在变量外部制作了一个虚伪的哈希。不管怎样都行。) 然而,例如,如果 yarrow 算法抛出“11xxo1”作为后果,那将变成 110011(即 61 卦,Center Conforming)并变为 111101(即 14 卦,Great Possessing) 当初应用程序的主体曾经实现,我能够设置 DOM。我思考过应用React,直到我意识到应用具备一百万个依赖项的框架来渲染一点内容是没有意义的,我应用了久经考验的老朋友jQuery。我年纪大了,没那么时尚,所以我的代码也是。 设置好 DOM 后,我当初能够打扮它了。这已经是我最不喜爱的局部,因为我对 CSS 感到十分丧气以至于我想尖叫,但当初我不再有这种感觉了。 我想要情绪环的共鸣。为了做到这一点,我去了 Codepen,搜寻了变色背景,而后始终在巡游,直到找到适宜我想要的货色。而后我复制粘贴那个吸盘并进行调整,直到它看起来正确并具备正确的色调。如果从互联网上复制粘贴代码是谬误的,我永远不想正确(我也从未正确过)。这使页面看起来豪华、漂亮和成熟,但只须要很少的工作。 在更理论的问题上,Skeleton始终是我抉择的非 Bootstrap 框架。谷歌字体给我带来了一些额定的空想。 就是这样。这就是单页简略网页的工作原理!这只花了我几个小时,但两年前可能花了我一整天!有时,当你始终在学习时,很难说你在学习什么,所以这是一个很好的练习,能够回去尝试一些小事,以意识到你曾经学会了。 ...

September 20, 2022 · 1 min · jiezi

关于源码:什么是SassPass和Iass

Iass,Pass和Saas都是什么意思?想必大家都听过也查阅过材料。但当初网上很多文章都会把一些比较简单的概念包装得十分牛气,逼格很高,各种高大上就是不说大白话,本文正好通过搭建网校平台为例和小伙伴简略分享一下它们之间的区别。 通过MeEdu开源零碎搭建网校平台,须要将源码署到服务器上能力让大家拜访,那服务器从哪来,咱们能够独自买一台实体服务器放家里放公司里,然而这样老本会比拟高,而且保护会比拟麻烦,所以更不便的形式就是去云服务平台,租一台服务器,租的这个服务器包含什么?包含服务器、存储设备和网络设备这些基础设施这些基础设施,这种租硬件设施的服务就是Iass,Infrastructure-as-a-Service(基础设施即服务)根底服务,就是把服务器存储设备和网络这些当做服务卖给客户,所以小伙伴们平时本人做个网站说我租个服务器,其实就是购买的Iass服务,很好了解吧! 了解了Iass之后咱们再回到网校零碎设计上,就以视频线上点播性能的实现为例。除了根本的零碎框架外,须要思考课程视频上传哪里?视频如何转码?视频如何加密?视频是否倍速播放?这么多问题和需要都须要本人解决吗?不必!大厂的云平台都提供了媒体服务,我只须要调用媒体服务的接口,就能实现视频的存储、播放加密、转码、清晰度调节等等性能,所以视频播放这个性能基本不必本人开发,用第三方的服务就能够了,而这个服务就是Pass,全称Platform as a service平台级服务。简略了解Pass就是提供了一套工具或者框架的接口来满足开发人员间接实现某些性能,再比方线上售卖课程,领取性能少不了。这样的话就能够调用第三方的领取零碎来实现领取性能,第三方的领取零碎也是一个Pass服务。 不同于MeEdu私有化部署模式,市场上有很多通过云部署多租户的零碎服务,每年通过收取租金,让客户通过拜访租用第三方云零碎,那这中服务模式就是Saas。全称Software as a service(软件即服务)。像大家用的一些云笔记云记账,还有企业用的一些云财务软软件,云办公软件或者是理发店饭店用的一些云会员管理系统,这些都是Saas服务或者叫Saas零碎。 通过MeEdu零碎简略和大家分享了一下Iass,Pass和Saas。大家只有晓得把软件当作服务,就是Saas。把工具或者框架当做服务,让开发人员调接口就能应用的话,这个就是Pass。把服务器和网络还有存储设备这种基础设施当作服务,是Iass就行了。

August 19, 2022 · 1 min · jiezi

关于源码:OneFlow源码一览GDB编译调试

作者|王益、严浩翻译|程浩源、董文文 1 GDB Python3PyTorch官网公布了如何应用GDB对Python触发的C++代码进行调试的指南,详情参考:https://github.com/pytorch/py... 其外围思路是运行gdb python3。在GDB会话中,能够为给定的C++函数名设置断点,如at::Tensor::neg。GDB以后无奈找到这个函数,prompt中会提醒是否在共享库加载时将断点挂起,答复yes。而后输出run,GDB会启动Python解释器。Python解释器会提醒输出Python源码。输出import torch,而后回车。 当Python解释器执行import语句时,会加载相干的共享库。GDB会监督加载并设置断点。执行Python源码,触发断点,而后关上GDB prompt进行C++调试,例如应用 bt 查看回溯,应用 l 显示Python调用的C++代码。 2 在调试模式下编译OneFlowLinux零碎 OneFlow 反对 Linux,暂不反对macOS和Windows。本文次要介绍在AWS GPU主机上运行Amazon Linux 2(相似于CentOS)。 (base) [wkyi ~]$ cat /etc/os-releaseNAME="Amazon Linux"VERSION="2"ID="amzn"ID_LIKE="centos rhel fedora"VERSION_ID="2"PRETTY_NAME="Amazon Linux 2"ANSI_COLOR="0;33"CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"HOME_URL="https://amazonlinux.com/"Conda或Docker环境 OneFlow官网文档倡议应用Conda或Docker镜像:https://github.com/Oneflow-In...。本次运行应用Anaconda。应用Conda或Docker是为了修复C++编译器和其余构建工具链的版本。应用新版本的g++须要对源代码进行更新,如https://github.com/Oneflow-In...。 编译调试版本 这里要留神,必须先编译OneFlow的调试版本,因为GDB须要调试符号能力使 bt 和 l 的输入有意义。 cd ~/w/oneflow/buildCMAKE_BUILD_TYPE=Debug cmake .. -C ../cmake/caches/international/cpu.cmake我装置的是CPU版本的OneFlow,创立了cpu.cmake文件。因为我的 AWS 主机不在中国,所以是在international目录下创立文件。 报错 在装置报错时,我在GitHub上提交了相干issue(https://github.com/Oneflow-In...),OneFlow的研发人员疾速给出了回应,向他们致敬! 编译步骤 本大节将展现编译OneFlow的具体步骤: 下载安装Anaconda。默认装置门路是 ~/anaconda3。装置时将环境变量增加到~/.bashrc。而后,获取环境变量或从新连贯主机使更改失效。创立并激活Conda环境,具体步骤参考:https://github.com/Oneflow-In...Git clone源码mkdir ~/w cd ~/w git clone https://github.com/Oneflow-In... 编译OneFlowcd oneflow mkdir build cd build `CMAKE_BUILD_TYPE=Debug cmake .. -C ../cmake/caches/international/cpu.cmake make -k -j $(nproc)` ...

July 14, 2022 · 2 min · jiezi

关于源码:如此好用的读Android源码利器还有人不知道

作者:字节小站起源:字节小站 举荐一个能够在线搜寻Android源代码的网站cs.android.com。它是由Google开发的一款可帮忙开发者查看理论应用的 Android 源代码的工具。它性能特地弱小。 无需下载Android源代码到本地,无需搭建Android开发环境反对文件查问反对class文件查问反对函数名查问反对穿插援用查问,函数调用跳转反对查看git提交记录留神 须要迷信上网能力拜访cs.android.com网站主页如下: 网站我的项目构造如下,反对文件导航 应用教程⒈查找文件搜寻框输出 file:文件名 ⒉查找类搜寻框输出 class:类名 ⒊查找办法名搜寻框输出 function:办法名 ⒋查看调用单击办法名。会弹出References界面。在Android Studio 查找 postSyncBarrier办法调用居然找不到。然而应用该网站却能找到。Android Studio对有的办法调用反对并不好 咱们能够看到在ViewRootImpl.java 的scheduleTraversals()办法中调用了postSyncBarrier()办法 ⒌查看git历史记录。通过历史记录咱们能够查看每笔提交减少了哪些性能,对于钻研源码太有用了 例如Handler的同步屏障机制。咱们通过历史记录能够很理解到为什么Google引入这个机制,以及它能干什么。通过学习google大神的批改记录,咱们也能失去很大的晋升 更多功能请移步官网查看。最初,如果你之前不理解这个网站,或者之前理解过一些其余相似的网站。我强烈建议你试试cs.android.com。理由很简略,因为这个google官网出品的。如果你感觉好用,欢送把它分享给你身边的小伙伴。最初帮忙点个“赞“吧

March 29, 2022 · 1 min · jiezi

关于源码:HAVE-FUN-源码解析活动进展

源码解析在第一篇源码解析流动文中,咱们介绍了 SOFARegistry 源码解析的具体介绍与具体参加办法,错过的小伙伴能够点击回顾哦,流动还在进行中... 流动公布后的一周工夫,咱们收到了很多来自社区小伙伴们的倡议和反馈,明天在这里和大家分享一下。 流动停顿先来看看这一周的流动停顿吧。 本次 SOFARegistry 源码解析工作共计公布 9 个。 截至 3 月 16 日,源码解析工作仅剩 2 个工作未被认领,残余工作均在进行中,感激大家的奉献! 各难度的源码解析工作完成度如下,咱们通过这几个 issue 来追踪工作的实现停顿,欢送大家去认领还未被领走的源码解析工作哦。 待认领工作通信数据压缩: https://github.com/sofastack/sofaregistry/issues/200 无损运维:https://github.com/sofastack/sofa-registry/issues/198 「我的项目介绍♂️」SOFARegistry 是蚂蚁团体开源的一个生产级、高时效、高可用的服务注册核心。SOFARegistry 最早源自于淘宝的 ConfigServer。十年来,随着蚂蚁团体的业务倒退,注册核心架构曾经演进至第五代。 目前 SOFARegistry 不仅全面服务于蚂蚁团体的自有业务,还随着蚂蚁金融科技服务泛滥合作伙伴,同时也兼容开源生态。SOFARegistry 采纳 AP 架构,反对秒级时效性推送,同时采纳分层架构反对有限程度扩大。 「将来打算」继 SOFARegistry 源码解析工作公布以来,大家反应热烈,都在问本人关怀的 SOFAStack 系列的其余我的项目组件什么时候公布源码解析打算。 「下期流动预报」Layotto 和 SOFAArk 源码解析工作正在筹备中,预计不久后会和大家见面,小伙伴们敬请期待吧。 在工作公布前先给大家简略介绍一下两个我的项目及 Contributor 养成工作,大家能够先理解一下,不便后续能够更快的参加到源码解析工作中。 LayottoLayotto(/let/) 是一款应用 Golang 开发的利用运行时, 旨在帮忙开发人员疾速构建云原生利用,帮忙利用和基础设施解耦。它为利用提供了各种分布式能力,比方状态治理,配置管理,事件公布订阅等能力,以简化利用的开发。 Layotto 以开源的 MOSN 为底座,在提供分布式能力以外,提供了 Service Mesh 对于流量的管控能力。 我的项目主页: https://mosn.io/layotto GitHub 地址: https://github.com/mosn/layotto ### Contributor 养成工作: LayottoEasy为actuator模块增加单元测试为java sdk新增分布式锁 API开发in-memory configuration 组件Medium让 Layotto 兼容 Dapr API降级由 rust 开发的 wasm demo用 mysql、consul或leaf等零碎实现分布式自增id APIHard让 Layotto 反对通过接口调用的形式动静加载 wasm,以反对 FaaS 场景动静调度「具体参考」: ...

March 16, 2022 · 1 min · jiezi

关于源码:数睿数据深度-关于软件自主可控源代码向左无代码向右

都快2022年了,为什么软件我的项目还要求厂商交付源代码?千行代码万行愁,一行正文思千秋。若让我知谁人写,定然让他断双手。——佚名 这是笔者最近5G冲浪时看到的一首打油诗,用语文老师的套路来解读就是:这首诗通过夸大的比较手法,粗浅地体现了诗人对于代码保护的疾恶如仇之情。如题所述,为什么甲方验收我的项目保持要交付源代码?要到源码就能居安思危了吗?如何感性对待代码的商业价值? 为什么甲方要求交付源代码?软件的交付就像是交付一栋建好的房子,那么修建图纸、布线图什么都须要一并交付,以便房子的前期保护。在软件我的项目中,源代码就好比这些修建图纸,我的项目验收时交付源代码以便于甲方后续对软件进行保护。对于软件交付这种交钥匙工程,客户认为把握源代码就把握了软件主动权的钥匙,将来有新的需要变更能够本人批改代码来适应,不须要再付昂扬的维护费用。 另外甲方会认为源代码是软件的外围价值,是原创标识,属于拥有者的知识产权。源代码上交后,有肯定能力的甲方还能将代码二次批改后成为本人的货色,申请软著排列在公司的荣誉柜里,或者本人接单持续做第二三个我的项目。 小编就听过一个电信软件供应商的A公司的敌人提到一个故事,过后国内开始推广虚构运营商,某电商巨头J拿了工信部牌照,洽购了A司的大量license的电信计费零碎后并要求上缴所有的源代码。A司认为虚构运营商在国内蓝海一片,欢快地签了合同。后果J司利用上缴的源代码重构了计费零碎,第二年A上门收受权费时将A司一脚踢出门。 除了以上两种状况,在中国还有一种非凡状况,就是一些涉密行业的政策要求,对安全性要求很高的企业会扫描源代码来保障软件系统的整体合规性。 总结来说,甲方要源代码无非是为了自主可控、继续二开、平安合规。这么看来,只有合同中有相干条款,交付源代码荒诞不经,一本万利。事实真是如此吗? 想实现“软件开发自在”,不能高估源代码的作用来看一个源于生存的段子,说国内大厂的代码不违心凋谢的重要起因是写得太烂了,一旦开源,就没人敢用他们的产品。这通知咱们,互联网上曾经有许多十分优良的像Linux的开源代码,千万不要高估本人或他人写的代码真的有微小的“商业价值”。政策说变就变,我的项目交付的时候还是二胎政策,刚交付完三胎政策凋谢了,须要加个流程。领导把这个需要传递给开发经理,你想想方法把乙方代码改改用,下个月上线。但如果单纯指望领有源代码就能实现“软件开发自在”、能够随心所欲,恐怕要悲观了。后面也说了,软件开发就像建房子,代码就好比盖房子用的砖,当砖的品质不好,建造进去的零碎的稳定性和可靠性都不能保障。咱们要面对一个事实,有些公司为了赶我的项目进度其实交付的代码品质个别,程序员在写代码的时候也不会太多思考复用的问题。并不是所有公司都能提交出齐全标准化的产品,甲方最终验收的也只是功能测试、性能测试,代码品质这一项无从考据。所以即便不愿意,也必须抵赖,乙方交付的代码能失常运行,且不出错,那就是牛x,不要指望品质有多高。另外,交付源代码对乙方来说也有“砸本人饭碗”的危险,如果客户齐全有能力本人保护、开发软件了,还找你干嘛。在不情不愿又不得不交付源码的这件事上,国内某论坛上祭出了“给一部分,他们只有一部分代码是没有太大用处的”、“给一些版本有误的”、“源代码文档给个简略点的”这样的倡议。千行代码万行愁,一行正文思千秋。这样交付的代码有多难保护?这里援用看过的另一个帖子:程序员被公司解雇都12天了,原团队没人能接手他写的代码,前领导要求他回公司讲清楚代码,员工回复:一次一万。本人团队产出的代码都没法接手,更别提是他人写进去的代码了。旧代码不易测试、无奈保障新代码的正确性、或者改一个新需要引入旧性能报错...这些也会给零碎带来极大的不稳定性。批改乙方代码费时费力,理论能给甲方带来多少自主可控的空间?这个问题很难答上来,烂代码自身就不是一个能够简略的可掂量的货色,没有可评估性。 最初,放大一点格局,交付源代码不利于软件行业的标准化倒退。把眼光脱离源代码自身,来看看整个软件行业。2020年,SaaS在中国私有云中的占比仅为25.5%,远低于SaaS在美国私有云中的占比67.1%。咱们晓得,软件的标准化将大大降低应用软件的总领有老本(TCO),进步整个行业的效率。而国内因为市场竞争强烈、甲方客户对产品性能需要含糊、多变等起因,我的项目上定制化代码的占比越来越高,软件行业的标准化之路堪称说是任重道远。软件厂商面临着大客户简单的定制需要与昂扬的人工成本,基本无暇顾及晋升代码品质,打造标准化产品。 不须要源代码,仍然能够实现自主可控后面说了那么多,如同交付源代码是甲方原罪,都重大到影响中国软件业的标准化倒退了。甲方爸爸何其无辜,他们只是想要自主可控而已啊!他们有什么错!如果短期内无奈解决代码品质的问题,拿到源代码进行二次开发不过是戴着脚链跳舞,想实现自主可控也不是只有源代码这一条路,咱们回避写代码不就能够了吗?回避尽管可耻但有用。 试想一下,如果有这样的一个平台,平台将企业级软件中的各类元素,包含表单、导航、视图、菜单等高度形象成一个个可拖拽的组件,用户无需写代码即可构建出企业级的利用,用来交付产品和我的项目,大大降低了开发的复杂度。更重要的是,构建进去的利用和写代码生成的利用一样能够通过甲方的功能测试和性能测试。这样的平台,居然真有厂商给做进去了,还是纯国产的——企业级无代码软件平台Smartdata。应用Smartdata开发的软件我的项目在验收后,乙方无需交接代码,构建进去的利用作为标准化产品积淀为企业资产,实现同类我的项目的规模化的复制交付;甲方无需接管和重构代码,透过平台“所见即所得”的利用构建界面,即可实现二次性能调整,十分不便,工作效率晋升数倍, 甲乙方关系迅速升温。 实现了自主可控、继续二开之后,问题又来了:交付的利用能满足涉密企业的平安需要吗?能申请软著吗? 企业级无代码三把斧 能够申请软著通过企业级无代码平台Smartdata构建的利用蕴含了设计者和搭建者的常识与智慧,毫无疑问创作者(自然人和法人)都享有著作权,是能够申请软件著作权的。著作权爱护的是指用户在平台根底上构筑的利用局部,而不蕴含平台自身。这就像通过Office创作小说的作家,只享有小说的知识产权,而不享有Office软件的著作权。为配合无代码平台用户申请软件著作权的工作,Smartdata方面示意能够为签约用户在申请软件著作权时,提供相干的申请材料。以上为平台用户申请软著中的软件应用(应用无代码平台构建)满足平安合规要求在平安合规这方面,事实上可能与想得不太一样,无代码平台深受涉密企业的青眼。正是因为行业保密性和安全性要求极高,参加开发的内部人员越少平安透露危险越小。而无代码平台交付的产品,相干用户可自行调整外部需要,进行疾速迭代,防止过多内部人员长期染指,大大提高零碎的安全性。造福甲乙双方应用无代码平台对于软件厂商(乙方)的收益不言而喻,规模化的我的项目复制,能够比传统开发方式更加省时省力降老本,帮忙企业疾速扩张、占领市场。同时骨干的开发人员能够安顿去做更高价值的事件,聚焦行业畛域模型,投入新产品的翻新,实现业务增长。至于无代码平台对于甲方的价值,还是拿三胎政策的例子,须要增加三胎申请页面、审批流程、校验逻辑来算个账:没有一劳永逸的胜利。笔者不认为交付源代码是原罪,只是如果换个思路能给乙方更多利润空间,给甲方更多自主权力,能为当下的IT行业提供更弱小的生产力,何不放弃思维焕新、付诸实践试一下呢。

November 25, 2021 · 1 min · jiezi

关于源码:不破不立分享源码优质的消防安全知识竞答活动小程序

不破不立,分享源码。立过的flag,以立冬之名,分享优质的消防安全常识竞答流动小程序。 介绍消防安全常识答题流动, 答题、 交通安全答题、 消防安全常识宣传、 答题流动、有奖答题 利用场景11月是全国“119”消防宣传月,主题是“落实消防责任,防备平安危险”。不少单位会举办消防安全常识比赛,因而我搭建了最新版的消防安全常识竞答小程序。帮忙大家能够定期举办消防安全常识竞答流动,让大家理解火灾隐患,学习消防常识,预防火灾,认真贯彻消防法规。 软件架构微信原生小程序+云开发 装置教程下载开发者工具导入小程序我的项目创立云开发环境 以后版本首页 转发分享答题 答题页 后果页 转发分享问题 界面截图 此套优质UI能够用于二开,拿走应用请先赞叹反对,谢谢。须要继续更新欠缺版本的,请点赞、点Star反馈~ 源码地址

November 8, 2021 · 1 min · jiezi

关于源码:几十套大屏数据展示模板速度收藏源码好又多

为什么大屏数据展现模板越来越受欢迎? 大屏在企业中越来越受欢迎,次要有两个方面的起因 第一:全方位的数据展现。 目前企业都有面临“信息孤岛”问题,各个系统平台之间的数据无奈实现交融共享,难以实现一体化的数据分析。 相比于传统图表与数据仪表盘,可视化监控大屏的呈现,能够突破数据隔离,通过数据采集、荡涤、剖析到直观实时的数据可视化,可能多方位、多角度、全景展示各项指标,实时监控,动静高深莫测。 第二:难看。 视化技术的倒退,可视化大屏通常搭载精美的可视化图表和特效,能更加活泼地展示数据,而且领有丰盛的交互性能和实时性,给人惊艳的视觉体验。 一张科技感满满的大屏,对企业形象晋升非常有帮忙,这也是诸多企业领导偏爱大屏的起因。源码获取办法:微信公众号:源码好又多 回复“大屏数据模板”

October 12, 2021 · 1 min · jiezi

关于源码:SpringBoot源码-bean的加载上

我又来讲源码恶心大家了,嘿嘿~ 上一节中讲的 run() 办法启动流程中,有那么一行代码: refreshContext(context);这一行代码就是明天的男主角了 - 它实现了bean的加载。它的实现在 AbstractApplicationContext 类的refresh()办法中,上码: @Overridepublic void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh"); // 容器状态设置、初始化属性设置、验证必备属性 prepareRefresh(); // 获取beanFactory ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 设置beanFactory的一些属性、增加后置处理器、注册默认的环境beans、 prepareBeanFactory(beanFactory); try { // 空办法,给子类留个钩子注入 postProcessors postProcessBeanFactory(beanFactory); StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process"); // 执行 BeanFactoryPostProcessor invokeBeanFactoryPostProcessors(beanFactory); // BeanPostProcessor注册进容器 registerBeanPostProcessors(beanFactory); beanPostProcess.end(); // 为容器初始化音讯源 initMessageSource(); // 为容器注册事件播送器 initApplicationEventMulticaster(); // 空办法,留给子类注册其余bean onRefresh(); // 注册事件监听器,派发之前未解决的事件 registerListeners(); // 初始化剩下的单实例bean finishBeanFactoryInitialization(beanFactory); // 初始化什么周期处理器,公布容器启动事件 finishRefresh(); } ………… finally { // 清缓存 resetCommonCaches(); contextRefresh.end(); } }}以上是spring容器启动的外围流程,下一节,重点来了~~ ...

October 6, 2021 · 1 min · jiezi

关于源码:StringString-BuilderString-Buffer源码

[TOC] StringString是一个很一般的类 源码剖析//该值用于字符存储private final char value[];//缓存字符串的哈希码private int hash;// Default to 0//这个是一个构造函数//把传递进来的字符串对象value这个数组的值,//赋值给结构的以后对象,hash的解决形式也一样。public String(String original) { this.value = original.value; this.hash = original.hash;}//String的初始化有很多种//空参数初始化//String初始化//字符数组初始化//字节数组初始化//通过StringBuffer,StringBuilder结构问题: 我现正在筹备结构一个String的对象,那original这个对象又是从何而来?是什么时候结构的呢? 测试一下: public static void main(String[] args) { String str = new String("zwt"); String str1 = new String("zwt");}在Java中,当值被双引号引起来(如本示例中的"abc"),JVM会去先查看看一看常量池里有没有abc这个对象, 如果没有,把abc初始化为对象放入常量池,如果有,间接返回常量池内容。 Java字符串两种申明形式在堆内存中不同的体现, 为了防止反复的创建对象,尽量应用String s1 ="123" 而不是String s1 = new String("123"),因为JVM对前者给做了优化。 罕用的APISystem.out.println(str.isEmpty());//判断是不是空字符串System.out.println(str.length());//获取字符串长度System.out.println(str.charAt(1));//获取指定地位的字符System.out.println(str.substring(2, 3));//截取指定区间字符串System.out.println(str.equals(str1));//比拟字符串isEmpty() public boolean isEmpty() { return value.length == 0; }length() public int length() { return value.length; }charAt() public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; }substring() public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } //如果截取的开始范畴刚好是0并且完结范畴等于数组的长度,间接返回以后对象, //否则用该数组和传入的开始范畴和完结范畴从新构建String对象并返回。 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }equals() public boolean equals(Object anObject) { //如果是同一个援用,间接返回true if (this == anObject) { return true; } //判断是否是String if (anObject instanceof String) { //判断长度是否统一 String anotherString = (String)anObject; int n = value.length; //判断char[]外面的每一个值是否相等 if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }equals()与“==”这两者之间没有必然的分割, ...

August 3, 2021 · 3 min · jiezi

关于源码:Integer源码

Integer 是java5 引进的新个性先上一个小试验: public static void main(String[] args) { Integer a1 = 100; Integer a2 = 100; System.out.println(a1 == a2); Integer b1 = 1000; Integer b2 = 1000; System.out.println(b1 == b2); }后果: truefalseProcess finished with exit code 0先说论断,[-128,127] 这个区间 true ,其余的范畴为 new 一个新的对象。 剖析: 查看字节码 public static main([Ljava/lang/String;)V L0 LINENUMBER 7 L0 BIPUSH 100 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; ASTORE 1 L1 LINENUMBER 8 L1 BIPUSH 100 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; ASTORE 2 …………代码实际上是Integer.valueOf ...

August 3, 2021 · 2 min · jiezi

关于源码:HashMap的转化时机

HashMap的转化机会 /** * 应用红黑树(而不是链表)来寄存元素。当向至多具备这么多节点的链表再增加元素时,链表就将转换为红黑树。 * 该值必须大于2,并且应该至多为8,以便于删除红黑树时转回链表。 */ static final int TREEIFY_THRESHOLD = 8; /** * 当桶数组容量小于该值时,优先进行扩容,而不是树化: */ static final int MIN_TREEIFY_CAPACITY = 64;putval片段 …………else { //上面的代码是探索“链表转红黑树”的重点: for (int binCount = 0;; ++binCount) { if ((e = p.next) == null) { //沿着p节点,找到该桶上的最初一个节点: p.next = newNode(hash, key, value, null); //间接生成新节点,链在最初一个节点的前面; //“binCount >= 7”:p从链表.index(0)开始, //当binCount == 7时,p.index == 7,newNode.index == 8; //也就是说,当链表曾经有8个节点了 //此时再新链上第9个节点,在胜利增加了这个新节点之后,立马做链表转红黑树。 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); //链表转红黑树 break; } ………… 如果你的table总容量小于64就不给你树化了,哪怕你一个单链的元素个数超过了8个,不树化,而是进行扩容。 ...

July 20, 2021 · 1 min · jiezi

关于源码:探秘RocketMQ源码Series1Producer视角看事务消息

简介: 探秘RocketMQ源码——Series1:Producer视角看事务音讯 1. 前言Apache RocketMQ作为广为人知的开源消息中间件,诞生于阿里巴巴,于2016年捐献给了Apache。从RocketMQ 4.0到现在最新的v4.7.1,不论是在阿里巴巴外部还是内部社区,都博得了宽泛的关注和好评。出于趣味和工作的须要,近期自己对RocketMQ 4.7.1的局部代码进行了研读,其间产生了很多困惑,也播种了更多的启发。 本文将站在发送方视角,通过浏览RocketMQ Producer源码,来剖析在事务音讯发送中RocketMQ是如何工作的。须要阐明的是,本文所贴代码,均来自4.7.1版本的RocketMQ源码。本文中所探讨的发送,仅指从Producer发送到Broker的过程,并不蕴含Broker将音讯投递到Consumer的过程。 2. 宏观概览RocketMQ事务音讯发送流程: 图1 联合源码来看,RocketMQ的事务音讯TransactionMQProducer的sendMessageInTransaction办法,理论调用了DefaultMQProducerImpl的sendMessageInTransaction办法。咱们进入sendMessageInTransaction办法,整个事务音讯的发送流程清晰可见: 首先,做发送前查看,并填入必要参数,包含设prepare事务音讯。 源码清单-1 public TransactionSendResult sendMessageInTransaction(final Message msg, final LocalTransactionExecuter localTransactionExecuter, final Object arg) throws MQClientException { TransactionListener transactionListener = getCheckListener(); if (null == localTransactionExecuter && null == transactionListener) { throw new MQClientException("tranExecutor is null", null); } // ignore DelayTimeLevel parameter if (msg.getDelayTimeLevel() != 0) { MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL); } Validators.checkMessage(msg, this.defaultMQProducer); SendResult sendResult = null; MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());进入发送解决流程: ...

May 7, 2021 · 6 min · jiezi

关于源码:ISOIEC-5055软件代码质量的标尺

摘要:ISO 5055是首个间接从软件内部结构方面掂量软件品质(如安全性和可靠性)的ISO规范。该规范基于统计安全性、可靠性、可维护性和性能效率方面的软件缺陷来掂量软件的构造品质。本文分享自华为云社区《主动源代码品质度量(ISO/IEC 5055)》,原文作者:Uncle_Tom 。 ISO 5055是首个间接从软件内部结构方面掂量软件品质(如安全性和可靠性)的ISO规范。该规范基于统计安全性、可靠性、可维护性和性能效率方面的软件缺陷来掂量软件的构造品质。基于ISO 5055,开发人员可能在要害缺点导致操作问题之前发现并打消这些缺点; 工具查看的供应商可能明确软件品质的查看方向;为管理层提供了明确指标,以明确软件应用程序给业务带来的危险。 1. 零碎和软件品质要求和评估(ISO/IEC 25000)ISO/IEC 25000系列规范,也称为SQuaRE(零碎和软件品质要求和评估),蕴含评估软件产品品质的框架。 ISO/IEC 25000系列次要蕴含以下几个局部: ISO/IEC 2500n — 品质治理(Quality Management Division): 定义了由SQuaRE系列规范中的所有其余规范援用的全副公共模型、术语和定义。在针对特定利用状况应用适当规范方面的援用门路和高级的实用倡议有助于所有类型的用户。这一分部还提供了用于负责管理软件产品需要和评估的反对性能的要求和指南。ISO/IEC 2501n — 品质模式(Quality Model Division): 给出一个包含软件外部品质、 软件内部品质和软件应用品质的个性的具体品质模型。此外, 外部和内部的软件品质个性被合成细化成一些子个性,并且还提供了应用该品质模型的实用指南。ISO/IEC 2502n — 品质测量(Quality Measurement Division): 包含软件产品品质测量参考模型、品质测量的数学定义及其利用的实用指南。给出了利用于软件外部品质、软件内部品质和应用品质的测量。定义并给出了形成后续测量根底的品质测量元素。ISO/IEC 2503n — 品质需要(Quality Requirements Division): 帮忙用户规定品质要求。这些品质要求可用在要开发的软件产品的品质需要抽取过程中或用作评估过程的输出。需要定义过程可映射到ISO/IEC 15288 中定义的技术过程。ISO/IEC 2504n — 品质评估(Quality Evaluation Division): 给出了无论由评估方、需方还是由开发方执行的软件产品评估的要求、倡议和指南。还给出了作为评估模块的测量文档编制反对。ISO/IEC 25050 到 ISO/IEC 25099 保留用于 SQuaRE 扩大的国际标准和/或技术报告。ISO/IEC 25000规范系列之间的关系1.1. 零碎和软件品质模型(ISO/IEC 25010)在软件品质模型的ISO/IEC 25010中定义了:应用品质模型和产品质量模型。这两个模型定义的特色与所有软件产品和计算机系统无关。这些个性和子个性为指定,测量和评估零碎和软件产品品质提供了统一的术语。它们还提供了一组品质个性,能够将其与规定的品质要求进行比拟,以确保完整性。模型的范畴不包含纯正的功能属性,但的确包含性能适用性。只管产品质量模型的范畴旨在于软件和计算机系统,然而许多特色也与更宽泛的零碎和服务无关。 1.1.1. 应用品质模型应用品质模型由五个特色(其中一些特色进一步细分为子特色)组成,这些特色与在特定用处下应用产品时的交互后果无关。该零碎模型实用于残缺的人机系统,包含正在应用的计算机系统和正在应用的软件产品。 1.1.2. 产品质量模型产品质量模型由八个个性(进一步细分为子个性)组成,这些个性与软件的动态属性和计算机系统的动静属性无关。该模型实用于计算机系统和软件产品。 性能适应性(functional suitability):软件所实现的性能达到其设计规范和满足用户需要的水平,强调正确性、齐备性、适宜性等。效率(efficiency):在指定条件下,软件对操作所体现出的工夫个性(如响应速度)以及实现某种性能无效利用计算机资源(包含内存大小、CPU占用工夫等)的水平,部分资源占用高通常是性能瓶颈存在;零碎可接受的并发用户数、连贯数量等,须要思考零碎的可伸缩性。兼容性(compatibility),波及共存和互操作性,共存要求软件能给与零碎平台、子系统、第三方软件等兼容,同时针对国际化和本地化进行了适合的解决。 互操作性要求零碎性能之间的无效对接,波及API和文件格式等。易用性(usability):对于一个软件,用户学习、操作、筹备输出和了解输入所作致力的水平,如安装简单不便、容易应用、界面敌对,并能实用于不同特点的用户,包含对残疾人、有缺点的人能提供产品应用的有效途径或伎俩(即可达性)。可靠性(reliability):在规定的工夫和条件下,软件所能维持其失常的性能操作、性能程度的水平/概率,如成熟性越高,可靠性就越高;用MTTF (mean time to failure,均匀生效前工夫) 或MTBF(mean time Between failures,均匀故障间隔时间)来掂量可靠性。安全性(security),要求其数据传输和存储等方面能确保其平安,包含对用户身份的认证、对数据进行加密和完整性校验,所有关键性的操作都有记录(log),可能审查不同用户角色所做的操作。它波及保密性、完整性、抗抵赖性、可核查性、真实性。可维护性(maintainability):当一个软件投入运行利用后,需要发生变化、环境扭转或软件产生谬误时,进行相应批改所做致力的水平。它波及模块化、复用性、易剖析性、易批改性、易测试性等可移植性(portability)软件从一个计算机系统或环境移植到另一个零碎或环境的容易水平,或者是一个零碎和内部条件独特工作的容易水平。它波及适应性、易装置性、易替换性。1.1.3. 品质模型的利用范畴品质模型的利用范畴包含与软件和软件密集型计算机系统的购买,需要,开发,应用,评估,反对,保护,质量保证和管制以及审核相干的各个方面,从而反对对软件和软件密集型计算机系统的标准和评估。例如,开发人员,获取者,质量保证和管制人员以及独立评估人员(尤其是负责指定和评估软件产品品质的人员)能够应用这些模型。应用品质模型可从产品开发过程中受害的流动包含: ...

April 25, 2021 · 1 min · jiezi

关于后端:RocketMQ基础概念剖析源码解析

TopicTopic是一类音讯的汇合,是一种逻辑上的分区。为什么说是逻辑分区呢?因为最终数据是存储到Broker上的,而且为了满足高可用,采纳了分布式的存储。 这和Kafka中的实现一模一样,Kafka的Topic也是一种逻辑概念,每个Topic的数据会分成很多份,而后存储在不同的Broker上,这个「份」叫Partition。而在RocketMQ中,Topic的数据也会分布式的存储,这个「份」叫MessageQueue。 其散布能够用下图来示意。 这样一来,如果某个Broker所在的机器意外宕机,而且刚好MessageQueue中的数据还没有长久化到磁盘,那么该Topic下的这部分音讯就会齐全失落。此时如果有备份的话,MQ就能够持续对外提供服务。 为什么还会呈现没有长久化到磁盘的状况呢?当初的OS当中,程序写入数据到文件之后,并不会立马写入到磁盘,因为磁盘I/O是十分耗时的操作,在计算机来看是十分慢的一种操作。所以写入文件的数据会先写入到OS本人的缓存中去,而后择机异步的将Buffer中的数据刷入磁盘。 通过多正本冗余的机制,使得RocketMQ具备了高可用的个性。除此之外,分布式存储可能应答前期业务大量的数据存储。如果不应用分布式进行存储,那么随着前期业务倒退,音讯量越来越大,单机是无论如何也满足不了RocketMQ音讯的存储需要的。如果不做解决,那么一台机器的磁盘总有被塞满的时候,此时的零碎就不具备可伸缩的个性,也无奈满足业务的应用要求了。 然而这里的可伸缩,和微服务中的服务可伸缩还不太一样。因为在微服务中,各个服务是无状态的。而Broker是有状态的,每个Broker上存储的数据都不太一样,因为Producer在发送音讯的时候会通过指定的算法,从Message Queue列表中选出一个MessageQueue发送音讯。 如果不是很了解这个横向扩大,那么能够把它当成Redis的Cluster,通过一致性哈希,抉择到Redis Cluster中的具体某个节点,而后将数据写入Redis Master中去。如果此时想要扩容很不便,只须要往Redis Cluster中新增Master节点就好了。 所以,数据分布式的存储实质上是一种数据分片的机制。在此基础上,通过冗余多正本,达成了高可用。 BrokerBroker能够了解为咱们微服务中的一个服务的某个实例,因为微服务中咱们的服务一般来说都会多实例部署,而RocketMQ也同理,多实例部署能够帮忙零碎扛住更多的流量,也从某种方面进步了零碎的健壮性。 在RocketMQ4.5之前,它应用主从架构,每一个Master Broker都有一个本人的Slave Broker。 那RocketMQ的主从Broker是如何进行数据同步的呢?Broker启动的时候,会启动一个定时工作,定期的从Master Broker同步全量的数据。 这块能够先不必纠结,前面咱们会通过源码来验证这个主从同步逻辑。 下面提到了Broker会部署很多个实例,那么既然多实例部署,那必然会存在一个问题,客户端是如何得悉本人是连贯的哪个服务器?如何得悉对应的Broker的IP地址和端口?如果某个Broker忽然挂了怎么办? NameServer这就须要NameServer了,NameServer是什么? 这里先拿Spring Cloud举例子——Spring Cloud中服务启动的时候会将本人注册到Eureka注册核心上。当服务实例启动的时候,会从Eureka拉取全量的注册表,并且之后定期的从Eureka增量同步,并且每隔30秒发送心跳到Eureka去续约。如果Eureka检测到某个服务超过了90秒没有发送心跳,那么就会该服务宕机,就会将其从注册表中移除。 RocketMQ中,NameServer充当的也是相似的角色。两者从性能上也有肯定的区别。 Broker在启动的时候会向NameServer注册本人,并且每隔30秒向NameServerv发送心跳。如果某个Broker超过了120秒没有发送心跳,那么就会认为该Broker宕机,就会将其从保护的信息中移除。这块前面也会从源码层面验证。 当然NameServer不仅仅是存储了各个Broker的IP地址和端口,还存储了对应的Topic的路由数据。什么是路由数据呢?那就是某个Topic下的哪个Message Queue在哪台Broker上。 Producer总体流程接下来,咱们来看看Producer发送一条音讯到Broker的时候会做什么事件,整体的流程如下。 查看音讯合法性整体来看,其实是个很简略的操作,跟咱们平时写代码是一样的,来申请了先校验申请是否非法。Producer启动这里会去校验以后Topic数据的合法性。 Topic名称中是否蕴含了非法字符Topic名称长度是否超过了最大的长度限度,由常量TOPIC_MAX_LENGTH来决定,其默认值为127以后音讯体是否是NULL或者是空音讯以后音讯体是否超过了最大限度,由常量maxMessageSize决定,值为1024 1024 4,也就是4M。都是些很惯例的操作,和咱们平时写的checker都差不多。 获取Topic的详情当通过了音讯的合法性校验之后,就须要持续往下走。此时的关注点就应该从音讯是否非法转移到我要发消息给谁。 此时就须要通过以后音讯所属的Topic拿到Topic的具体数据。 获取Topic的办法源码在下面曾经给进去了,首先会从内存中保护的一份Map中获取数据。顺带一提,这里的Map是ConcurrentHashMap,是线程平安的,和Golang中的Sync.Map相似。 当然,首次发送的话,这个Map必定是空的,此时会调用NameServer的接口,通过Topic去获取详情的Topic数据,此时会在下面的办法中将其退出到Map中去,这样一来下次再往该Topic发送音讯就可能间接从内存中获取。这里就是简略的实现的缓存机制 。 从办法名称来看,是通过Topic获取路由数据。实际上该办法,通过调用NameServer提供的API,更新了两局部数据,别离是: Topic路由信息Topic下的Broker相干信息而这两局部数据都来源于同一个构造体TopicRouteData。其构造如下。 通过源码能够看到,就蕴含了该Topic下所有Broker下的Message Queue相干的数据、所有Broker的地址信息。 发送的具体Queue此时咱们获取到了须要发送到的Broker详情,包含地址和MessageQueue,那么此时问题的关注点又该从「音讯发送给谁」转移到「音讯具体发送到哪儿」。 什么叫发送到哪儿?开篇提到过一个Topic下会被分为很多个MessageQueue,「发送到哪儿」指的就是具体发送到哪一个Message Queue中去。 Message Queue抉择机制外围的抉择逻辑还是先给出流程图 外围逻辑,用大白话讲就是将一个随机数和Message Queue的容量取模。这个随机数存储在Thread Local中,首次计算的时候,会间接随机一个数。 尔后,都间接从ThreadLocal中取出该值,并且+1返回,拿到了MessageQueue的数量和随机数两个要害的参数之后,就会执行最终的计算逻辑。 接下来,咱们来看看抉择Message Queue的办法SelectOneMessageQueue都做了什么操作吧。 能够看到,主逻辑被变量sendLatencyFaultEnable分为了两局部。 容错机制下的抉择逻辑该变量表意为发送提早故障。实质上是一种容错的策略,在原有的MessageQueue抉择根底上,再过滤掉不可用的Broker,对之前失败的Broker,按肯定的工夫做退却。 能够看到,如果调用Broker信息产生了异样,那么就会调用updateFault这个办法,来更新Broker的Aviable状况。留神这个参数isolation的值为true。接下来咱们从源码级别来验证下面说的退却3000ms的事实。 ...

March 23, 2021 · 1 min · jiezi

关于vue.js:刨根问底揭开-Vue-中-Scope-CSS-实现的幕后原理

前言我想大家都对 Vue 的 Scope CSS 耳熟能详了,然而说起 Vue 的 Scope CSS 实现的原理,很多人应该会说不就是给 HTML、CSS 增加属性吗 ️? 的确是这样的,不过这只是最终 Scope CSS 出现的后果。而这个过程又是如何实现的?我想能答复上一二的同学应该不多。 那么,回到明天本文,我将会围绕以下 3 点,和大家一起从 Vue 的 Scope CSS 的最终出现后果登程,深入浅出一番其实现的底层原理: 什么是 Scope CSSvue-loader 解决组件(.vue 文件)Patch 阶段利用 ScopeId 生成 HTML 的属性1 什么是 Scope CSSScope CSS 即作用域 CSS,组件化所密不可分的一部分。Scope CSS 使得咱们能够在各组件中定义的 CSS 不产生净化。例如,咱们在 Vue 中定义一个组件: <!-- App.vue --><template> <div class="box">scoped css</div></template><script> export default {};</script><style scoped> .box { width: 200px; height: 200px; background: #aff; }</style>通常状况下,在开发环境咱们的组件会先通过 vue-loader 的解决,而后联合运行时的框架代码渲染到页面上。相应地,它们对应的 HTML 和 CSS 别离会是这样: ...

March 20, 2021 · 4 min · jiezi

关于开源框架:掌握了开源框架还不够你更需要掌握源代码

摘要:本篇文章将以解决 Element Plus 问题的经验开始,循序渐进探讨开源我的项目或开源框架的问题,进一步探讨驾驭开源我的项目源代码的办法和技巧,分享本人浏览、了解和更改源代码的思路。本文分享自华为云社区《优良开源框架就肯定靠谱么?五招助你驾驭源代码》,原文作者:Marvin Zhang 。 前言The most incomprehensible thing about the world is that it is comprehensible. 世界上最不可了解的中央就是它居然是能够了解的。-- 阿尔伯特·爱因斯坦 开源(Open-Source)造就了现在凋敝沉闷的软件行业。开源让全世界的开发者都可能协力编写出优良的工具类我的项目,也就是所谓的 “轮子”,在造福大大小小的公司集体的同时,也能够展示创作者或贡献者的技术实力。现在很多开发者都在大量应用开源我的项目作为本人我的项目的第三方库或依赖,更快更高效的实现开发工作。 笔者也不例外。我最近在用 Vue 3 重构 Crawlab 前端的时候,用到了 Element 团队开发的升级版的 ElementUI,也就是 Vue 3 重构的新 UI 框架 Element Plus。Element 团队在 Element Plus 中将该我的项目用 Vue 3 齐全重构,全面拥抱了TypeScript;而且相比于之前的 Vue 2 版本丰盛了局部组件;而整体格调和应用形式跟之前的版本统一;一些 API 在应用上还变得更精简了。因而,笔者在重构 Crawlab 前端初期过程中没有遇到太大的阻碍,再加上之前的编写教训,开发过程中显得轻车熟路。然而,好景不长,随着我的项目的一直开发,笔者遭逢到一些技术上的艰难。更精确的说,在实现一些简单性能时遇到了来自于 Element Plus 框架自身的限度。尽管最终千方百计将问题解决了,然而我也粗浅领会到了硬啃(Hacking)开源我的项目源代码的艰难。因而,也心愿借此机会将本人驾驭开源代码的教训分享给读者。 本篇文章将以解决 Element Plus 问题的经验开始,循序渐进探讨开源我的项目或开源框架的问题,进一步探讨驾驭开源我的项目源代码的办法和技巧,分享本人浏览、了解和更改源代码的思路。本篇文章次要是方法论的探讨,不波及太多技术细节,任何业余背景的读者都可浏览。 硬啃 Element Plus 框架简介如果你是应用过 Vue 的前端工程师,你必定据说过 ElementUI。这是一个 UI 框架,也就是说它是帮忙你构建 Web 我的项目的工具类框架,其中蕴含很多罕用的组件(Component)、布局(Layout)以及主题(Theme)等。晚期的 ElementUI 是用 Vue 2 写的,是 Vue 中最受欢迎的 UI 框架,在 Github 上有 49k 标星,是第二名 iView(24k)的两倍多。随着 Vue 作者尤雨溪公布 Vue 3 版本,宣告全面拥抱 TS 之后,原 Element 团队在 Vue 3 的根底上开发了新版本 Element Plus,也就是这个故事的配角。如果对 Vue 3 甚至 Vue 不理解,能够浏览本博客之前的技术文章《TS 加持的 Vue 3,如何帮你轻松构建企业级前端利用》。 ...

March 16, 2021 · 3 min · jiezi

关于源码:初学者如何阅读源码

原文:How to read code – a primer原文作者:technikhil译者:newbiewang我喜爱编程,它也是我的工作,而且我很快乐可能将大部分的工夫都花在开发软件上。像许多程序员一样,我既着迷但又困惑的是,我写的代码到底怎么样,以及如何写得更好。 多年来,我曾经浏览了许多无关软件开发的文章和书籍。其中不乏有许多墨宝(书上的或者网上的)通知你如何进步编程,并成为一个像忍者一样的受过专业训练的编程高手!这些倡议大多有一些共性,其中之一就是浏览源码。然而相比于其它倡议,浏览源码通常也就是简略的一句话来概括:找一些很棒的开源软件,或是任何你喜爱的软件,关上它们(或打印进去)而后浏览它们。尽管总的来说,这的确是个很好的倡议,但纸上得来终觉浅,理论去实际的时候才发现问题多多。在这篇文章中,我会尝试给出一些浏览源码的实用倡议,但在这之前,首先让咱们列举一下都有哪些问题。 对浏览源码的误会他人一说浏览源码,给你的个别印象仿佛他们就像编程巨匠一样,能够单纯地坐在椅子上,而后像看小说一样读着手上的代码。好吧,我敢肯定,的确有一些精湛的程序员,他们能够很享受地一边喝着咖啡、一边看着一堆相似英语句子的神秘符号,并且还可能在脑海里构建整个类的档次和体系结构。显然这篇文章并不是给他们看的,它的受众是像我一样的,感觉盯着一堆源码看就好比看一些无聊没有意义的练习题的人。当然,有人会辩论说,能够从一个残缺我的项目里一点一点地看单个类或者单个函数来学习,但在我看来,除非是最简略的问题,大多数软件外部都是相互依赖的。在不理解零碎其余部分的状况下,通常不可能了解一个特定函数或者类背地的设计思维和原理。 下一个问题是从哪里取得能够读的源码(当然,在此之前,你得可能甄别哪些源码值得一读)。优良的软件很多,既有开源软件能够收费取得,也有闭源软件须要受权。开源仓库有譬如 Sourceforge 和 GitHub 。如果你在软件开发公司工作,那么能够拜访源代码库中的专有代码。第三种常见路径是软件开发书籍附带的程序,或者作为教育资源而提供的程序( Minix 是典型的例子)。的确,泛滥的选项使咱们难以抉择,因而从茫茫代码世界中找出适宜咱们浏览的是一项艰巨而必不可少的工作。 另一个问题是程序所用的编程语言,读别人的代码曾经足够艰难了,如果同时还须要去相熟一门夹杂着奇葩语法的新语言,它所带来的累赘,在我看来几乎就是个会带来极大挫败感的劫难。所以你须要找到用你相熟的语言所编写的代码。但如果你要看的代码是来自书本上或作为教育资源所提供的,那懂不懂这门新语言并无关紧要,因为有导师能够解释上下文。假使你明知山有虎偏差虎山行,在没有书或者导师指引下,去浏览一门并不相熟的编程语言,那我倡议你至多须要学习,并达到能够写出本人的程序的水平(Hello World 就不算了哈)。 前文无关上下文的问题使我想到了下一个问题,如果你不相熟软件自身,弄清楚代码在做什么就艰难得多。例如,如果你不是每天都在应用 Linux 并通晓 Linux 启动程序,那么就很难在看一边 Linux 代码后弄清楚运行级别是什么。应用某个软件取得的教训、常识可能帮忙咱们更好地浏览它的源码,这包含罕用的术语、软件的性能和个性,甚至包含你遇到的各种谬误自身。 了解源码对我而言,我意识到 “浏览源码” 并不能精确形容我所从事的流动,用 “了解源码” 来表述会更适合。对我来说,坐在笔记本屏幕前(或打印成纸),只是单纯地读满屏的代码是十分艰难的。我须要代码之外其它的货色,比方我喜爱翻一翻文档,玩一玩这个软件,单步运行代码甚至写测试代码去跑一跑,而后能力真正观赏它。因为我会为此投入十分大的工夫和精力,所以我必须要精挑细选,寻找我要 “浏览”(了解)的软件。 我的第一层过滤是通过编程语言进行筛选,对我来说,我只浏览由 C#、VB.NET、Python 和 Javascript 编写而成的程序的代码(只管我也相熟 C++、Ruby 和 F#,但我并不认为本人有程度来了解其他人的代码)。接下来是寻找我应用过的软件,这会让我有种曾经上车的感觉,因为我晓得代码的用意,以及它不能做的事件还有它的局限性(如果我足够相熟的话)。每天都在应用的开源软件正是优良的候选项(比方,我应用用 C# 编写的开源工具 Cruise Control.NET、NANT 和 NUnit) 碰巧我在一家软件产品公司(一家微软的公司)工作,所以我浏览的源码选择项之一是咱们公司在源代码库中的代码。如果碰巧你也在一家软件公司工作,你能够查看其余的我的项目,甚至你着手我的项目的较晚期版本。这样,除了能够取得更深层次的代码了解之外,你还能够很好地理解之前和之后都曾尝试过哪些货色。不过有一些正告须要留神: 首先,如果你没有权限拜访其余我的项目,则须要征得许可,因为一些公司对其 “知识产权” 十分看重。其次,这些软件的品质可能没有你想像的那么高,因为通常状况下,专有代码没有通过像开源代码那样严格的代码走查。须要留神的是,如果不足惯例的代码审查,那么代码的品质可能不佳。第三(这一点是从我的敌人提供的反馈中失去启发的),如果你的公司开发的是商业软件(HR、财务、ERP 等),则须要首先了解很多业务关系。而且,因为大多数代码受业务性能因素的影响,因而通常模块化水平不如应用程序或 API 高。寻找文档齐全的我的项目(这实用于开源以及专有代码)。我的意思是说,这样的文档应该突出总体设计,并阐明代码背地的原理。如果只是简略地主动生成的 Java Doc 类型文档,则不能视之为我所形容的文档 ????。其中一种寻找路径是利用为教育而发明的软件(例如 Minix)。因为它们的目标是通过软件进行教学,因而通常会有十分清晰的文档记录下来,并且有大量材料解释代码背地的设计原理。 总结那么,当初你曾经确定了要浏览源码的软件并下载了它的源代码和文档,让咱们一步步浏览并了解它: 浏览设计文档,并尝试理解代码的构建形式。好的软件我的项目遵循某些架构模式,这些决定了代码的组织。一旦把握了这一点,了解代码就变得容易了很多。如果你还能画出类图,就能更好地理解整体布局。接下来要做的是编译并运行它。依据我的项目及其文档循序渐进,这可能很简略也可能很艰难。当初是时候关上你喜爱的 IDE 并开始摸索了。一个好的摸索终点是,尝试一步步浏览你相熟的性能的代码。这样一来,你能够遍历各个层和子系统,并理解它们之间的关联。例如,当我摸索 NUnit 时,我首先编写了一个测试用例,而后查看波及到的类。尝试确定代码中应用的设计模式。如果你还不晓得什么是设计模式,那么立即马上进行看本文,转去浏览设计模式的经典书籍。相熟设计模式,它们是辨认和了解优良代码中所蕴含的设计的好办法。相熟之后就能够更轻松地在浏览代码时将其牢记在心。它还能够帮忙你更轻松地辨认代码作者在原有设计模式上所做的轻微调整和魔改。尝试为代码编写测试用例以齐全了解它,这是了解代码不同局部之间的依赖关系的一种十分有用的办法。写测试用例之前,首先须要满足所有的依赖。接下来,理解代码的可能的入口点和返回值。这能够增进你对代码的了解,助你更上一层楼。最初,尝试重构代码。在这一步,你曾经从单纯地了解代码迈向足够相熟以可能对其进行批改。随着重构复杂程度的进步,你的了解也将随之减少。此时,如果须要,你能够为我的项目奉献本人的代码。“源码浏览”在我看来,不仅仅是浏览,它是一组独特的流动,独特帮忙人们了解代码。这仿佛比简略的 “浏览代码” 更令人生畏,但它值得付出致力。 ...

January 7, 2021 · 1 min · jiezi

关于源码:如何高效阅读源码

“我能纯熟应用这个框架/软件/技术就行了, 为什么要看源码?” “平时不必看源码, 看源码太费时间,还容易遗记,工作中呈现问题再针对性地浏览,效率更高。” “为了面试才须要看源码!” 。。。。。。 如果你也有相似的疑难,无妨接着往下看 1、为什么要浏览源码?1.1 在通用型根底技术中进步技术能力在 JAVA 畛域中蕴含 JAVA 汇合、Java并发(JUC)等, 它们是我的项目中应用的高频技术,在各种简单的场景中选用适合的数据结构、线程并发模型,正当管制锁粒度等都能显著进步应用程序的可用性、健壮性,非常容易凸显出本人的技术实力,更容易受到领导的认可,助力职场。 当然通过浏览源码并不是通晓原理的惟一办法,但作为一个名程序员、直面代码,亲自感触代码的魅力或者会显得的更加间接。 1.2 在重点畛域打造本人的亮点我所在公司应用了 Dubbo、RocketMQ,我也有幸参加到这些技术栈的使用与运维,积攒了丰盛的应用教训,为了突出在这两个畛域的劣势,我具体浏览了它们的源码,在CSDN和公众号等常识分享平台公布了大量的技术文章,成体系的分析其实现原理、架构设计理念,实践与实战相结合,让我成为在Dubbo、RocketMQ畛域当仁不让的技术专家,团队中的外围骨干。 同时因为文章是成体系的,被出版社相中,邀请出书,《RocketMQ技术底细》一书应运而生,从而成为我职业技能列表中十分亮眼的名片,造成公认的技术影响力,具备肯定的“品牌溢价”能力。 当然, 也能够等到出了问题再看源码,“投入产出比”更高,但这是个被动过程,如果生产环境因为并发不高,那可能一年、两年你都遇不到真正的问题,教训积攒十分慢, 工作了4、5年,有可能还比不过工作2、3年的人。 所以如果是你想疾速打造亮点的话,还是须要被动浏览源码,成体系把握其设计理念、实现原理。 1.3 从优良的源码中学习设计和编码学习编程的过程其实就是一个模拟的过程, 优良的源码都是大师级作品,极具养分,能够看到巨匠们是如何形象接口的,如何利用SOLID准则的,还有很多十分有用的编程技巧。 例如JUnit是从模式开始构建零碎, 其中你能够看到大量的设计模式的利用,这些都是活生生的案例,比水灵灵地看那些设计模式实践,或者简略的例子不晓得好到哪里去了。 2、如何浏览源码 我依据多年的浏览教训,整顿了这么一套办法: 理解这款软件的应用场景、以及架构设计中将承当的责任。寻找官网文档,从整体上把握这款软件的设计理念。搭建本人的开发调试环境,运行官网提供Demo示例,为后续深入研究打下基础。先骨干流程再分支流程,留神切割,一一击破。接下来分享一下我在浏览 RocketMQ 源码时的一些经验,尽量让上述实践具备画面感。 2.1 理解 RocketMQ的利用场景MQ的应用场景是比拟清晰的,它的两大根本职责是解耦与削峰填谷。 举一个最简略的场景:新用户注册送积分、送优惠券场景,其原始架构设计通常如下: 能够看出用户注册和发优惠券,送积分是紧耦合的, 随着业务一直倒退,流动部门提出在春节期间用户注册不送积分,发优惠券,而是赠送一个新春礼包,如果基于上述架构的话,须要去改变用户注册的主流程,违反了设计模式中的对批改敞开、对扩大凋谢的设计理念。 MQ的呈现,能够很好地解决下面的问题: 通过引入MQ,用户注册主流程只须要实现注册逻辑,并向MQ发送一条音讯,而后流动模块(送积分、送优惠券、送礼包)只须要订阅MQ中的音讯,进行对应的解决。 这样音讯注册主流程会十分的简略,不论流动品种如何变动,注册流程无需更改,这样就实现理解耦。 2.2  通读官网文档,从全局把握其设计理念理解应用场景当前,接下咱们能够去查阅官网文档,次要包含用户设计文档(架构设计),用户使用手册等,从全局理解其设计理念。 通过通读官网文档,不仅能够得出MQ的整体脉络(例如NameServer路由发现、音讯发送、音讯存储、音讯生产、音讯过滤),也能对程序生产,零拷贝、同步刷盘、异步刷盘等“高端大气上档次”的高级个性产生趣味与好奇,驱动咱们去浏览其源码,探索其实现细节,使得咱们在浏览源码中进行肯定的自我思考成为了可能。 2.3 搭建开发调试环境不同的零碎搭建形式也不同,我这里有一篇手把手教你装置RocketMQ与IDEA Debug环境搭建。 2.4 先骨干,再分支在搭建好本地开发环境后,切忌间接用Debug去跟踪音讯发送的整体流程,因为这个流程切实是太长了,从比拟粗粒度来看其流程如下图所示: 如果大家想一次性将上述流程的源码全副看一遍,简直是不可能的。 因为音讯发送高可用设计、音讯存储、刷盘、同步等机制,每个点具体开展的工作都是海量的,咱们没有这么多间断的工夫,所以适当的拆分十分有必要。 通过这样一合成,就能专一了解其某一块的设计原理,所须要的间断工夫也能大大减少,一口一口“吃”,最终实现整个体系的了解。 这还带来了一个额定的益处:分批次输入多篇文章,进步了文章产出,进步了成就感。 3、浏览源码很容易放弃,怎么办?浏览源码是干燥的,一个人孤军奋战很容易放弃,尤其是遇到问题的时候,  如何能力坚持下去,把它读完呢? 我的答案是保持,但容许短暂的休整、停留,而后重复发动冲刺,拼抢,直到攻克这个“山头”。 因为一旦放弃就将半途而废,一旦冲破,本身能力能失去一个质的飞跃。 我在浏览Netty源码时就遇到了难题,过后刚刚写完Netty的内存泄露检测,筹备开始钻研内存分配机制, 这一块儿十分形象,波及的数据结构简单,须要把握二叉树与数组之间如何映射、牵扯到大量的位运算等等,让我在探索其原理时举步维艰,怎么办呢?放弃? 当一周、两周都无奈获得冲破时,人很容易想到:这样继续投入工夫,又没有回报, 是不是在浪费时间? ...

January 4, 2021 · 1 min · jiezi

关于源码:海南七星玩法的五分十分彩网站系统源码项目解析

这个是之前帮敌人做的一个海南七星玩法的五分非常彩网站零碎源码我的项目,当初分享进去给大家一起学习交换.我的扣2607788043 登陆控制器 namespace Portal\Controller;class UserController { protected $user_model; public function __construct() { parent::__construct(); $this->check_login();// $this->uid = session('uid');// $this->user_login = session('user_login');// $this->user_model = D("Portal/User");// $this->user = $this->user_model->find($this->uid);// $this->assign('user', $this->user); } public function index(){ $this->display(); }}namespace Portal\Controller;use Common\Controller\HomebaseController;class DrawListController extends HomebaseController { protected $user_model; public function __construct() { parent::__construct(); $this->check_login(); $this->user_model = M('user');//打款凭证 } public function Index(){ $this->display(); } public function GetLotteryResult(){ header('Content-type: application/json'); $start=$_GET['StartDate']; $end=$_GET['EndDate']; $where['time']=array('between',strtotime($start."00:00:00").','.strtotime($end."23:59:59")); $count=M('data')->where($where)->count(); $list=M('data')->where($where)->limit((($_GET['pageIndex']-1)*20).',20')->order('time desc')->select(); $PageCount=ceil($count/20); $list2=array(); foreach ($list as $key => $value) { $list2[$key][CreateDt]=date('Y-m-d H:i:s',$value['time']); $list2[$key][DrawDt]=date('Y-m-d H:i:s',$value['time']); $list2[$key][OpenDt]=date('Y-m-d H:i:s',$value['time']); $list2[$key][ID]=$value['number']; $list2[$key][PeriodsNumber]=$value['number']; $list2[$key][ResultNumber]=$value['data']; } $data['list']=$list2; $data['PageCount']=$PageCount; echo json_encode($data); }}

August 3, 2020 · 1 min · jiezi

对于魔术方法callcallStatic-新的认识

误解的一般解释__call方法在对象方法不存在的时候被调用 __callStatic方法在调用对象静态方法不存在的时候被调用 例如 class Car{ public function __call($method,$params=[]){ echo "car call\n"; }}(new Car())->color();class Bus{ public static function __callStatic($method,$params=[]){ echo "Bus callStatic\n"; }}Bus::isSale();特殊情况其实上面的解释在某些情况下是正确的。但是在一些特殊情形,如果按照这个解释来理解,就会觉得结果不可思议了。 以下面几个例子进行说明。 __call的调用关注的是方法能不能被访问class Car{ public function __call($method,$params=[]){ echo "car call\n"; } public function color(){ echo "color red\n"; } protected function isRed(){ echo "yes is Red\n"; } public function checkColor(){ $this->color(); $this->isRed(); }}$car = new Car();$car->color();$car->isRed();$car->checkColor();输出的结果是 color redcar call isRedcolor redyes is Red从上面可以看出,其实是否调用__call,依赖的是当前调用方能否访问到要调用的函数,如果可以访问到,则直接调用函数,如果不能访问到,则调用魔术方法__call。所以,调用与否关注的是可访问性。 __callStatic关注的是方法能否被静态的方式访问接下来看另外一个静态调用的例子 class Car{ public static function __callStatic($method,$params=[]){ echo "car callStatic\n"; } public function color(){ echo "color red\n"; } protected function isRed(){ echo "yes is Red\n"; } public function checkColor(){ Car::color(); Car::isRed(); }}Car::color();Car::isRed();(new Car())->checkColor();输出内容是 ...

October 14, 2019 · 1 min · jiezi

Go中使用Seed得到重复随机数的问题

重复的随机数废话不多说,首先我们来看使用seed的一个很神奇的现象。 func main() { for i := 0; i < 5; i++ { rand.Seed(time.Now().Unix()) fmt.Println(rand.Intn(100)) }}// 结果如下// 90// 90// 90// 90// 90可能不熟悉seed用法的看到这里会很疑惑,我不是都用了seed吗?为何我随机出来的数字都是一样的?不应该每次都不一样吗? 可能会有人说是你数据的样本空间太小了,OK,我们加大样本空间到10w再试试。 func main() { for i := 0; i < 5; i++ { rand.Seed(time.Now().Unix()) fmt.Println(rand.Intn(100000)) }}// 结果如下// 84077// 84077// 84077// 84077// 84077你会发现结果仍然是一样的。简单的推理一下我们就能知道,在上面那种情况,每次都取到相同的随机数跟我们所取的样本空间大小是无关的。那么唯一有关的就是seed。我们首先得明确seed的用途。 seed的用途在这里就不卖关子了,先给出结论。 上面每次得到相同随机数是因为在上面的循环中,每次操作的间隔都在毫秒级下,所以每次通过time.Now().Unix()取出来的时间戳都是同一个值,换句话说就是使用了同一个seed。 这个其实很好验证。只需要在每次循环的时候将生成的时间戳打印出来,你就会发现每次打印出来的时间戳都是一样的。 每次rand都会使用相同的seed来生成随机队列,这样一来在循环中使用相同seed得到的随机队列都是相同的,而生成随机数时每次都会去取同一个位置的数,所以每次取到的随机数都是相同的。 seed 只用于决定一个确定的随机序列。不管seed多大多小,只要随机序列一确定,本身就不会再重复。除非是样本空间太小。解决方案有两种: 在全局初始化调用一次seed即可每次使用纳秒级别的种子(强烈不推荐这种)不用每次调用上面的解决方案建议各位不要使用第二种,给出是因为在某种情况下的确可以解决问题。比如在你的服务中使用这个seed的地方是串行的,那么每次得到的随机序列的确会不一样。 但是如果在高并发下呢?你能够保证每次取到的还是不一样的吗?事实证明,在高并发下,即使使用UnixNano作为解决方案,同样会得到相同的时间戳,Go官方也不建议在服务中同时调用。 Seed should not be called concurrently with any other Rand method.接下来会带大家了解一下代码的细节。想了解源码的可以继续读下去。 源码解析-seedseed首先来看一下seed做了什么。 func (rng *rngSource) Seed(seed int64) { rng.tap = 0 rng.feed = rngLen - rngTap seed = seed % int32max if seed < 0 { // 如果是负数,则强行转换为一个int32的整数 seed += int32max } if seed == 0 { // 如果seed没有被赋值,则默认给一个值 seed = 89482311 } x := int32(seed) for i := -20; i < rngLen; i++ { x = seedrand(x) if i >= 0 { var u int64 u = int64(x) << 40 x = seedrand(x) u ^= int64(x) << 20 x = seedrand(x) u ^= int64(x) u ^= rngCooked[i] rng.vec[i] = u } }}首先,seed赋值了两个定义好的变量,rng.tap和rng.feed。rngLen和rngTap是两个常量。我们来看一下相关的常量定义。 ...

October 9, 2019 · 2 min · jiezi

学习-lodash-源码整体架构打造属于自己的函数式编程类库

前言这是学习源码整体架构系列第三篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。 上上篇文章写了jQuery源码整体架构,学习 underscore 源码整体架构,打造属于自己的函数式编程类库 上一篇文章写了underscore源码整体架构,学习 jQuery 源码整体架构,打造属于自己的 js 类库 感兴趣的读者可以点击阅读。 underscore源码分析的文章比较多,而lodash源码分析的文章比较少。原因之一可能是由于lodash源码行数太多。注释加起来一万多行。 分析lodash整体代码结构的文章比较少,笔者利用谷歌、必应、github等搜索都没有找到,可能是找的方式不对。于是打算自己写一篇。平常开发大多数人都会使用lodash,而且都或多或少知道,lodash比underscore性能好,性能好的主要原因是使用了惰性求值这一特性。 本文章学习的lodash的版本是:v4.17.15。unpkg.com地址 https://unpkg.com/lodash@4.17... 文章篇幅可能比较长,可以先收藏再看,所以笔者使用了展开收缩的形式。 导读: 文章主要学习了runInContext() 导出_ lodash函数使用baseCreate方法原型继承LodashWrapper和LazyWrapper,mixin挂载方法到lodash.prototype、后文用结合例子解释lodash.prototype.value(wrapperValue)和Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。匿名函数执行;(function() {}.call(this));暴露 lodash var _ = runInContext();runInContext 函数这里的简版源码,只关注函数入口和返回值。 var runInContext = (function runInContext(context) { // 浏览器中处理context为window // ... function lodash(value) {}{ // ... return new LodashWrapper(value); } // ... return lodash;});可以看到申明了一个runInContext函数。里面有一个lodash函数,最后处理返回这个lodash函数。 再看lodash函数中的返回值 new LodashWrapper(value)。 LodashWrapper 函数function LodashWrapper(value, chainAll) { this.__wrapped__ = value; this.__actions__ = []; this.__chain__ = !!chainAll; this.__index__ = 0; this.__values__ = undefined;}设置了这些属性: ...

September 10, 2019 · 10 min · jiezi

我还是不够了解Vue-Vue的实例化

Vue的定义直入主题,Vue定义在/core/instance/index.js中。 import { initMixin } from './init'import { stateMixin } from './state'import { renderMixin } from './render'import { eventsMixin } from './events'import { lifecycleMixin } from './lifecycle'import { warn } from '../util/index'function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options)}initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)export default Vue这就是Vue的定义,它其实就是一个构造函数,这里值得一提的是,Vue并没有使用ES6 Class的语法,而是通过扩展Vue构造函数的prototype,充分利用javascript原型的设计实现了模块化,可以看到下面很多mixin都是去扩展Vue的功能, 这样的代码设计非常利于阅读和维护。 下面许多__Mixin(Vue)就不多提了,其实就是给Vue.prototype增加方法,以便于其实例化的对象可以使用这些功能。 这篇文章主要是讲讲Vue的实例化过程。 一切的一切都是从new Vue(options)开始,实际呢就是调用了上面看到的this._init(options)。看看_init的定义: ...

September 10, 2019 · 3 min · jiezi

Centos7下PHP源码编译和通过yum安装的区别和以后的选择

最近在Centos7下配置PHP+Nginx+MySQL,对源码编译和yum编译的两种方法产生好奇。究竟这两种哪一种好?其实这两种方法各有千秋: 从yum安装来说吧,yum相当于是自动化帮你安装,你不用管软件的依赖关系,在yum安装过程是帮你把软件的全部依赖关系帮你傻瓜式的解决了。而且现在Centos7的服务启动已经换成systemctl命令来控制了。通过yum安装会帮你自动注册服务,你可以通过systemctl start xxx.service启动服务,方便快捷。但是缺点是yum安装你没办法干预,安装的目录也是分散的。你可能要执行whereis或者find命令去找yum安装的路径。有时候yum安装的软件版本比较低,你不得不去找其他的yum源,或者rpm包。 源码编译在安装过程中可能要解决很多的依赖问题,才能装好一个软件。装好的软件你还不能通过systemctl来启动服务,因为在/usr/lib/systemd/system/路径下并没有你的服务的配置文件,你要自己手写一个。但是好处在于你能选择软件的版本,自定义安装目录,安装的模块。更加灵活方便。 以上两种都是有各自的优点,建议是初学者一定要掌握源码编译的过程,手动解决安装过成中遇到的问题,熟悉如何编译一个软件,对于以后的发展是很有利的,而且有些软件没办法通过yum安装,这时候源码编译就显得很重要了。而像PHP这类软件来说,如果是编译安装的,如果缺少一个扩展,你就得做phpize, ./configure, make && make install等方式编译PHP扩展,这是很繁琐的。通过yum安装的话,当你要增加一个扩展,例如pdo,你就能够yum search php | grep pdo来寻找合适的pdo包,然后下载安装,系统会自动帮你添加到PHP扩展列表。省去我们很多工作。 个人愚见,不喜勿喷。

August 18, 2019 · 1 min · jiezi

解密Redux-从源码开始

Redux是当今比较流行的状态管理库,它不依赖于任何的框架,并且配合着react-redux的使用,Redux在很多公司的React项目中起到了举足轻重的作用。接下来笔者就从源码中探寻Redux是如何实现的。 注意:本文不去过多的讲解Redux的使用方法,更多的使用方法和最佳实践请移步Redux官网。源码之前基础概念随着我们项目的复杂,项目中的状态就变得难以维护起来,这些状态在什么时候,处于什么原因,怎样变化的我们就很难去控制。因此我们考虑在项目中引入诸如Redux、Mobx这样的状态管理工具。 Redux其实很简单,可以简单理解为一个约束了特定规则并且包括了一些特殊概念的的发布订阅器。 在Redux中,我们用一个store来管理一个一个的state。当我们想要去修改一个state的时候,我们需要去发起一个action,这个action告诉Redux发生了哪个动作,但是action不能够去直接修改store里头的state,他需要借助reducer来描述这个行为,reducer接受state和action,来返回新的state。 三大原则在Redux中有三大原则: 单一数据源:所有的state都存储在一个对象中,并且这个对象只存在于唯一的store中;state只读性:唯一改变state的方法就是去触发一个action,action用来描述发生了哪个行为;使用纯函数来执行修改:reducer描述了action如何去修改state,reducer必须是一个纯函数,同样的输入必须有同样的输出;剖析源码项目结构 抛去一些项目的配置文件和其他,Redux的源码其实很少很简单: index.js:入口文件,导出另外几个核心函数;createStore.js:store相关的核心代码逻辑,本质是一个发布订阅器;combineReducers.js:用来合并多个reducer到一个root reducer的相关逻辑;bindActionCreators.js:用来自动dispatch的一个方法;applyMiddleware.js:用来处理使用的中间件;compose.js:导出一个通过从右到左组合参数函数获得的函数;utils:两个个工具函数和一个系统注册的actionType;从createStore来讲一个store的创建首先我们先通过createStore函数的入参和返回值来简要理解它的功能: export default function createStore(reducer, preloadedState, enhancer) { // ... return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }}createStore接受三个参数: reducer:用来描述action如何改变state的方法,它给定当前state和要处理的action,返回下一个state;preloadedState:顾名思义就是初始化的state;enhancer:可以直译为增强器,用它来增强store的第三方功能,Redux附带的唯一store增强器是applyMiddleware;createStore返回一个对象,对象中包含使用store的基本函数: dispatch:用于action的分发;subscribe:订阅器,他将会在每次action被dispatch的时候调用;getState:获取store中的state值;replaceReducer:替换reducer的相关逻辑;接下来我们来看看createStore的核心逻辑,这里我省略了一些简单的警告和判断逻辑: export default function createStore(reducer, preloadedState, enhancer) { // 判断是不是传入了过多的enhancer // ... // 如果不传入preloadedState只传入enhancer可以写成,const store = createStore(reducers, enhancer) // ... // 通过在增强器传入createStore来增强store的基本功能,其他传入的参数作为返回的高阶函数参数传入; if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } // 闭包内的变量; // state作为内部变量不对外暴露,保持“只读”性,仅通过reducer去修改 let currentReducer = reducer let currentState = preloadedState // 确保我们所操作的listener列表不是原始的listener列表,仅是他的一个副本; let currentListeners = [] let nextListeners = currentListeners let isDispatching = false // 确保我们所操作的listener列表不是原始的listener列表,仅是他的一个副本; // 只有在dispatch的时候,才会去将currentListeners和nextListeners更新成一个; function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } // 通过闭包返回了state,state仅可以通过此方法访问; function getState() { // 判断当前是否在dispatch过程中 // ... return currentState } // Redux内部的发布订阅器 function subscribe(listener) { // 判断listener的合法性 // ... // 判断当前是否在dispatch过程中 // ... let isSubscribed = true // 复制一份当前的listener副本 // 操作的都是副本而不是源数据 ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { if (!isSubscribed) { return } // 判断当前是否在dispatch过程中 // ... isSubscribed = false ensureCanMutateNextListeners() // 根据当前listener的索引从listener数组中删除来实现取掉订阅; const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } } function dispatch(action) { // 判断action是不是一个普通对象; // ... // 判断action的type是否合法 // ... // 判断当前是否在dispatch过程中 // ... try { isDispatching = true // 根据要触发的action, 通过reducer来更新当前的state; currentState = currentReducer(currentState, action) } finally { isDispatching = false } // 通知listener执行对应的操作; const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action } // 替换reducer,修改state变化的逻辑 function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } currentReducer = nextReducer // 此操作对ActionTypes.INIT具有类似的效果。 // 新旧rootReducer中存在的任何reducer都将收到先前的状态。 // 这有效地使用来自旧状态树的任何相关数据填充新状态树。 dispatch({ type: ActionTypes.REPLACE }) } function observable() { const outerSubscribe = subscribe return { // 任何对象都可以被用作observer,observer对象应该有一个next方法 subscribe(observer) { if (typeof observer !== 'object' || observer === null) { throw new TypeError('Expected the observer to be an object.') } function observeState() { if (observer.next) { observer.next(getState()) } } observeState() const unsubscribe = outerSubscribe(observeState) // 返回一个带有unsubscribe方法的对象可以被用来在store中取消订阅 return { unsubscribe } }, [$$observable]() { return this } } } // 创建store时,将调度“INIT”操作,以便每个reducer返回其初始状态,以便state的初始化。 dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }}从combineReducers谈store的唯一性仅靠上面的createStore其实已经可以完成一个简单的状态管理了,但是随着业务体量的增大,state、action、reducer也会随之增大,我们不可能把所有的东西都塞到一个reducer里,最好是划分成不同的reducer来处理不同模块的业务。 ...

July 15, 2019 · 4 min · jiezi

TiKV-源码解析系列文章十Snapshot-的发送和接收

作者:黄梦龙 背景知识TiKV 使用 Raft 算法来提供高可用且具有强一致性的存储服务。在 Raft 中,Snapshot 指的是整个 State Machine 数据的一份快照,大体上有以下这几种情况需要用到 Snapshot: 正常情况下 leader 与 follower/learner 之间是通过 append log 的方式进行同步的,出于空间和效率的考虑,leader 会定期清理过老的 log。假如 follower/learner 出现宕机或者网络隔离,恢复以后可能所缺的 log 已经在 leader 节点被清理掉了,此时只能通过 Snapshot 的方式进行同步。Raft 加入新的节点的,由于新节点没同步过任何日志,只能通过接收 Snapshot 的方式来同步。实际上这也可以认为是 1 的一种特殊情形。出于备份/恢复等需求,应用层需要 dump 一份 State Machine 的完整数据。TiKV 涉及到的是 1 和 2 这两种情况。在我们的实现中,Snapshot 总是由 Region leader 所在的 TiKV 生成,通过网络发送给 Region follower/learner 所在的 TiKV。 理论上讲,我们完全可以把 Snapshot 当作普通的 RaftMessage 来发送,但这样做实践上会产生一些问题,主要是因为 Snapshot 消息的尺寸远大于其他 RaftMessage: Snapshot 消息需要花费更长的时间来发送,如果共用网络连接容易导致网络拥塞,进而引起其他 Region 出现 Raft 选举超时等问题。构建待发送 Snapshot 消息需要消耗更多的内存。过大的消息可能导致超出 gRPC 的 Message Size 限制等问题。基于上面的原因,TiKV 对 Snapshot 的发送和接收进行了特殊处理,为每个 Snapshot 创建单独的网络连接,并将 Snapshot 拆分成 1M 大小的多个 Chunk 进行传输。 ...

July 10, 2019 · 3 min · jiezi

SOFAJRaft-选举机制剖析-SOFAJRaft-实现原理

SOFAStackScalable Open Financial Architecture Stack是蚂蚁金服自主研发的金融级分布式架构,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。 本文为《剖析 | SOFAJRaft 实现原理》第四篇,本篇作者力鲲,来自蚂蚁金服《剖析 | SOFAJRaft 实现原理》系列由 SOFA 团队和源码爱好者们出品,项目代号:<SOFA:JRaftLab/>,目前领取已经完成,感谢大家的参与。 SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 SOFAJRaft :https://github.com/sofastack/sofa-jraft 前言在 Raft 算法中,选举是很重要的一部分,所谓选举也就是在多个节点中选出一个 Leader 节点,由他来对外提供写服务 (以及默认情况下的读服务)。 在剖析源码时,对选举机制的理解经常会遇到两极分化的情况,对于了解 Raft 算法基本原理的同学,阅读源码就是品味实现之巧妙的过程,而对初体验的同学,却会陷入丈二和尚的窘境,仿佛坠入云里雾里。 为了提升文章的可读性,我还是希望花一部分篇幅讲清楚选举机制的基本原理,以便后面集中注意力于代码实现。下面是一段图文比喻,帮助理解的同时也让整篇文章不至于过早陷入细节的讨论。 问题1:选举要解决什么一个分布式集群可以看成是由多条战船组成的一支舰队,各船之间通过旗语来保持信息交流。这样的一支舰队中,各船既不会互相完全隔离,但也没法像陆地上那样保持非常密切的联系,天气、海况、船距、船只战损情况导致船舰之间的联系存在但不可靠。 舰队作为一个统一的作战集群,需要有统一的共识、步调一致的命令,这些都要依赖于旗舰指挥。各舰船要服从于旗舰发出的指令,当旗舰不能继续工作后,需要有别的战舰接替旗舰的角色。 图1 - 所有船有责任随时准备接替旗舰 如何在舰队中,选出一艘得到大家认可的旗舰,这就是 SOFAJRaft 中选举要解决的问题。 问题2:何时可以发起选举何时可以发起选举?换句话说,触发选举的标准是什么?这个标准必须对所有战舰一致,这样就能够在标准得到满足时,所有舰船公平的参与竞选。在 SOFAJRaft 中,触发标准就是通信超时,当旗舰在规定的一段时间内没有与 Follower 舰船进行通信时,Follower 就可以认为旗舰已经不能正常担任旗舰的职责,则 Follower 可以去尝试接替旗舰的角色。这段通信超时被称为 Election Timeout (简称 ET), Follower 接替旗舰的尝试也就是发起选举请求。 图2 - ET 触发其他船竞选旗舰 问题3:何时真正发起选举在选举中,只有当舰队中超过一半的船都同意,发起选举的船才能够成为旗舰,否则就只能开始一轮新的选举。所以如果 Follower 采取尽快发起选举的策略,试图尽早为舰队选出可用的旗舰,就可能引发一个潜在的风险:可能多艘船几乎同时发起选举,结果其中任何一支船都没能获得超过半数选票,导致这一轮选举无果,然后失败的 Follower 们再一次几乎同时发起选举,又一次失败,再选举 again,再失败 again ··· 图3 - 同时发起选举,选票被瓜分 为避免这种情况,我们采用随机的选举触发时间,当 Follower 发现旗舰失联之后,会选择等待一段随机的时间 Random(0, ET) ,如果等待期间没有选出旗舰,则 Follower 再发起选举。 ...

July 10, 2019 · 2 min · jiezi

TiKV-源码解析系列文章九Service-层处理流程解析

作者:周振靖 之前的 TiKV 源码解析系列文章介绍了 TiKV 依赖的周边库,从本篇文章开始,我们将开始介绍 TiKV 自身的代码。本文重点介绍 TiKV 最外面的一层——Service 层。 TiKV 的 Service 层的代码位于 src/server 文件夹下,其职责包括提供 RPC 服务、将 store id 解析成地址、TiKV 之间的相互通信等。这一部分的代码并不是特别复杂。本篇将会简要地介绍 Service 层的整体结构和组成 Service 层的各个组件。 整体结构位于 src/server/server.rs 文件中的 Server 是我们本次介绍的 Service 层的主体。它封装了 TiKV 在网络上提供服务和 Raft group 成员之间相互通信的逻辑。Server 本身的代码比较简短,大部分代码都被分离到 RaftClient,Transport,SnapRunner 和几个 gRPC service 中。上述组件的层次关系如下图所示: 接下来,我们将详细介绍这些组件。 Resolver在一个集群中,每个 TiKV 实例都由一个唯一的 store id 进行标识。Resolver 的功能是将 store id 解析成 TiKV 的地址和端口,用于建立网络通信。 Resolver 是一个很简单的组件,其接口仅包含一个函数: pub trait StoreAddrResolver: Send + Clone { fn resolve(&self, store_id: u64, cb: Callback) -> Result<()>;}其中 Callback 用于异步地返回结果。PdStoreAddrResolver 实现了该 trait,它的 resolve 方法的实现则是简单地将查询任务通过其 sched 成员发送给 Runner。而 Runner 则实现了 Runnable<Task>,其意义是 Runner 可以在自己的一个线程里运行,外界将会向 Runner 发送 Task 类型的消息,Runner 将对收到的 Task 进行处理。 这里使用了由 TiKV 的 util 提供的一个单线程 worker 框架,在 TiKV 的很多处代码中都有应用。Runner 的 store_addrs 字段是个 cache,它在执行任务时首先尝试在这个 cache 中找,找不到则向 PD 发送 RPC 请求来进行查询,并将查询结果添加进 cache 里。 ...

July 8, 2019 · 3 min · jiezi

TiDB-Binlog-源码阅读系列文章二初识-TiDB-Binlog-源码

作者:satoru TiDB Binlog 架构简介TiDB Binlog 主要由 Pump 和 Drainer 两部分组成,其中 Pump 负责存储 TiDB 产生的 binlog 并向 Drainer 提供按时间戳查询和读取 binlog 的服务,Drainer 负责将获取后的 binlog 合并排序再以合适的格式保存到对接的下游组件。 在《TiDB Binlog 架构演进与实现原理》一文中,我们对 TiDB Binlog 整体架构有更详细的说明,建议先行阅读该文。 相关源码仓库TiDB Binlog 的实现主要分布在 tidb-tools 和 tidb-binlog 两个源码仓库中,我们先介绍一下这两个源码仓库中的关键目录。 1. tidb-toolsRepo: https://github.com/pingcap/tidb-tools/ 这个仓库除了 TiDB Binlog 还有其他工具的组件,在这里与 TiDB Binlog 关系最密切的是 tidb-binlog/pump_client 这个 package。pump_client 实现了 Pump 的客户端接口,当 binlog 功能开启时,TiDB 使用它来给 pump 发送 binlog 。 2. tidb-binlogRepo: https://github.com/pingcap/tidb-binlog TiDB-Binlog 的核心组件都在这个仓库,下面是各个关键目录: cmd:包含 pump,drainer,binlogctl,reparo,arbiter 等 5 个子目录,分别对应 5 个同名命令行工具。这些子目录下面的 main.go 是对应命令行工具的入口,而主要功能的实现则依赖下面将介绍到的各个同名 packages。pump:Pump 源码,主要入口是 pump.NewServer 和 Server.Start;服务启动后,主要的功能是 WriteBinlog(面向 TiDB/pump_client) 和 PullBinlogs(面向 Drainer)。drainer:Drainer 源码,主要入口是 drainer.NewServer 和 Server.Start;服务启动后,Drainer 会先找到所有 Pump 节点,然后调用 Pump 节点的 PullBinlogs 接口同步 binlog 到下游。目前支持的下游有:mysql/tidb,file(文件增量备份),kafka 。binlogctl:Binlogctl 源码,实现一些常用的 Binlog 运维操作,例如用 -cmd pumps 参数可以查看当前注册的各个 Pump 节点信息,相应的实现就是 QueryNodesByKind。reparo:Reparo 源码,实现从备份文件(Drainer 选择 file 下游时保存的文件)恢复数据到指定数据库的功能。arbiter:Arbiter 源码,实现从 Kafka 消息队列中读取 binlog 同步到指定数据库的功能,binlog 在消息中以 Protobuf 格式编码。pkg:各个工具公用的一些辅助类的 packages,例如 pkg/util 下面有用于重试函数执行的 RetryOnError,pkg/version 下面有用于打印版本信息的 PrintVersionInfo。tests:集成测试。启动测试集群上个小节提到的 tests 目录里有一个名为 run.sh 脚本,我们一般会使用 make integration_test 命令,通过该脚本执行一次完整的集成测试,不过现在我们先介绍如何用它来启动一个测试集群。 ...

July 5, 2019 · 2 min · jiezi

TiDB-30-GA稳定性和性能大幅提升

作者:段兵 TiDB 是 PingCAP 自主研发的开源分布式关系型数据库,具备商业级数据库的数据可靠性,可用性,安全性等特性,支持在线弹性水平扩展,兼容 MySQL 协议及生态,创新性实现 OLTP 及 OLAP 融合。 TiDB 3.0 版本显著提升了大规模集群的稳定性,集群支持 150+ 存储节点,300+TB 存储容量长期稳定运行。易用性方面引入大量降低用户运维成本的优化,包括引入 Information_Schema 中的多个实用系统视图、EXPLAIN ANALYZE、SQL Trace 等。在性能方面,特别是 OLTP 性能方面,3.0 比 2.1 也有大幅提升,其中 TPC-C 性能提升约 4.5 倍,Sysbench 性能提升约 1.5 倍,OLAP 方面,TPC-H 50G Q15 因实现 View 可以执行,至此 TPC-H 22 个 Query 均可正常运行。新功能方面增加了窗口函数、视图(实验特性)、分区表、插件系统、悲观锁(实验特性)。 截止本文发稿时 TiDB 已在 500+ 用户的生产环境中长期稳定运行,涵盖金融、保险、制造,互联网,游戏等领域,涉及交易、数据中台、历史库等多个业务场景。不同业务场景对关系型数据库的诉求可用 “百花齐放”来形容,但对关系数据库最根本的诉求未发生任何变化,如数据可靠性,系统稳定性,可扩展性,安全性,易用性等。请跟随我们的脚步梳理 TiDB 3.0 有什么样的惊喜。 一、提升大规模集群稳定性3.0 与 2.1 版本相比,显著提升了大规模集群的稳定性,支持单集群 150+ 存储节点,300+TB 存储容量长期稳定运行,主要的优化点如下: 1. 优化 Raft 副本之间的心跳机制,按照 Region 的活跃程度调整心跳频率,减小冷数据对集群的负担。 ...

June 29, 2019 · 2 min · jiezi

深入koa源码二核心库原理

最近读了 koa2 的源码,理清楚了架构设计与用到的第三方库。本系列将分为 3 篇,分别介绍 koa 的架构设计和 3 个核心库,最终会手动实现一个简易的 koa。这是系列第 2 篇,关于 3 个核心库的原理。 本文来自《心谭博客·深入koa源码:核心库原理》所有系列文章都放在了Github。欢迎交流和Star ✿✿ ヽ(°▽°)ノ ✿is-generator-function:判断 generatorkoa2 种推荐使用 async 函数,koa1 推荐的是 generator。koa2 为了兼容,在调用use添加中间件的时候,会判断是否是 generator。如果是,则用covert库转化为 async 函数。 判断是不是 generator 的逻辑写在了 is-generator-function 库中,逻辑非常简单,通过判断Object.prototype.toString.call 的返回结果即可: function* say() {}Object.prototype.toString.call(say); // 输出: [object GeneratorFunction]delegates:属性代理delegates和 koa 一样,这个库都是出自大佬 TJ 之手。它的作用就是属性代理。这个代理库常用的方法有getter,setter,method 和 access。 用法假设准备了一个对象target,为了方便访问其上request属性的内容,对request进行代理: const delegates = require("delegates");const target = { request: { name: "xintan", say: function() { console.log("Hello"); } }};delegates(target, "request") .getter("name") .setter("name") .method("say");代理后,访问request将会更加方便: ...

June 24, 2019 · 2 min · jiezi

DM-源码阅读系列文章八Online-Schema-Change-同步支持

作者:lan 本文为 DM 源码阅读系列文章的第八篇,上篇文章 对 DM 中的定制化数据同步功能进行详细的讲解,包括库表路由(Table routing)、黑白名单(Black & white table lists)、列值转化(Column mapping)、binlog 过滤(Binlog event filter)四个主要功能的实现。 本篇文章将会以 gh-ost 为例,详细地介绍 DM 是如何支持一些 MySQL 上的第三方 online schema change 方案同步,内容包括 online schema change 方案的简单介绍,online schema change 同步方案,以及同步实现细节。 MySQL 的 Online Schema Change 方案目前有一些第三方工具支持在 MySQL 上面进行 Online Schema Change,比较主流的包括 pt-online-schema-change 和 gh-ost。 这些工具的实现原理比较类似,本文会以 gh-ost 为例来进行分析讲解。 从上图可以大致了解到 gh-ost 的逻辑处理流程: 在操作目标数据库上使用 create table ghost table like origin table 来创建 ghost 表;按照需求变更表结构,比如 add column/index;gh-ost 自身变为 MySQL replica slave,将原表的全量数据和 binlog 增量变更数据同步到 ghost 表;数据同步完成之后执行 rename origin table to table_del, table_gho to origin table 完成 ghost 表和原始表的切换pt-online-schema-change 通过 trigger 的方式来实现数据同步,剩余流程类似。 ...

June 20, 2019 · 2 min · jiezi

TiDB-Binlog-源码阅读系列文章一序

作者:黄佳豪 TiDB Binlog 组件用于收集 TiDB 的 binlog,并准实时同步给下游,如 TiDB、MySQL 等。该组件在功能上类似于 MySQL 的主从复制,会收集各个 TiDB 实例产生的 binlog,并按事务提交的时间排序,全局有序的将数据同步至下游。利用 TiDB Binlog 可以实现数据准实时同步到其他数据库,以及 TiDB 数据准实时的备份与恢复。随着大家使用的广泛和深入,我们遇到了不少由于对 TiDB Binlog 原理不理解而错误使用的情况,也发现了一些 TiDB Binlog 支持并不完善的场景和可以改进的设计。 在这样的背景下,我们开展 TiDB Binlog 源码阅读分享活动,通过对 TiDB Binlog 代码的分析和设计原理的解读,帮助大家理解 TiDB Binlog 的实现原理,和大家进行更深入的交流,同时也有助于社区参与 TiDB Binlog 的设计、开发和测试。 背景知识本系列文章会聚焦 TiDB Binlog 本身,读者需要有一些基本的知识,包括但不限于: Go 语言,TiDB Binlog 由 Go 语言实现,有一定的 Go 语言基础有助于快速理解代码。数据库基础知识,包括 MySQL、TiDB 的功能、配置和使用等;了解基本的 DDL、DML 语句和事务的基本常识。了解 Kafka 的基本原理。基本的后端服务知识,比如后台服务进程管理、RPC 工作原理等。总体而言,读者需要有一定 MySQL/TiDB/Kafka 的使用经验,以及可以读懂 Go 语言程序。在阅读 TiDB Binlog 源码之前,可以先从阅读 《TiDB Binlog 架构演进与实现原理》 入手。 ...

June 18, 2019 · 2 min · jiezi

go源码解析Println的故事

本文主要通过平常常用的go的一个函数,深入源码,了解其底层到底是如何实现的。 PrintlnPrintln函数接受参数a,其类型为…interface{}。用过Java的对这个应该比较熟悉,Java中也有…的用法。其作用是传入可变的参数,而interface{}类似于Java中的Object,代表任何类型。 所以,…interface{}转换成Java的概念,就是Object args ...。 Println函数中没有什么实现,只是return了Fprintln函数。 func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...)} 而在此处的…放在了参数的后面。我们知道...interface{}是代表可变参数,即函数可接收任意数量的参数,而且参数参数分开写的。 当我们再调用这个函数的时候,我们就没有必要再将参数一个一个传给被调用函数了,直接使用a…就可以达到相同的效果。 Fprintln该函数接收参数os.Stdout.write,和需要打印的数据作为参数。 func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintln(a) n, err = w.Write(p.buf) p.free() return}sync.Pool从广义上看,newPrinter申请了一个临时对象池。我们逐行来看newPrinter函数做了什么。 var ppFree = sync.Pool{ New: func() interface{} { return new(pp) },}// newPrinter allocates a new pp struct or grabs a cached one.func newPrinter() *pp { p := ppFree.Get().(*pp) p.panicking = false p.erroring = false p.wrapErrs = false p.fmt.init(&p.buf) return p}sync.Pool是go的临时对象池,用于存储被分配了但是没有被使用,但是未来可能会使用的值。以此来减少 GC的压力。 ...

June 14, 2019 · 3 min · jiezi

TiKV-源码解析系列文章八grpcrs-的封装与实现

作者: 李建俊 上一篇《gRPC Server 的初始化和启动流程》为大家介绍了 gRPC Server 的初始化和启动流程,本篇将带大家深入到 grpc-rs 这个库里,查看 RPC 请求是如何被封装和派发的,以及它是怎么和 Rust Future 进行结合的。 gRPC C CoregRPC 包括了一系列复杂的协议和流控机制,如果要为每个语言都实现一遍这些机制和协议,将会是一个很繁重的工作。因此 gRPC 提供了一个统一的库来提供基本的实现,其他语言再基于这个实现进行封装和适配,提供更符合相应语言习惯或生态的接口。这个库就是 gRPC C Core,grpc-rs 就是基于 gRPC C Core 进行封装的。 要说明 grpc-rs 的实现,需要先介绍 gRPC C Core 的运行方式。gRPC C Core 有三个很关键的概念 grpc_channel、grpc_completion_queue、grpc_call。grpc_channel 在 RPC 里就是底层的连接,grpc_completion_queue 就是一个处理完成事件的队列。grpc_call 代表的是一个 RPC。要进行一次 RPC,首先从 grpc_channel 创建一个 grpc_call,然后再给这个 grpc_call 发送请求,收取响应。而这个过程都是异步,所以需要调用 grpc_completion_queue 的接口去驱动消息处理。整个过程可以通过以下代码来解释(为了让代码更可读一些,以下代码和实际可编译运行的代码有一些出入)。 grpc_completion_queue* queue = grpc_completion_queue_create_for_next(NULL);grpc_channel* ch = grpc_insecure_channel_create("example.com", NULL);grpc_call* call = grpc_channel_create_call(ch, NULL, 0, queue, "say_hello");grpc_op ops[6];memset(ops, 0, sizeof(ops));char* buffer = (char*) malloc(100);ops[0].op = GRPC_OP_SEND_INITIAL_METADATA;ops[1].op = GRPC_OP_SEND_MESSAGE;ops[1].data.send_message.send_message = "gRPC";ops[2].op = GRPC_OP_SEND_CLOSE_FROM_CLIENT;ops[3].op = GRPC_OP_RECV_INITIAL_METADATA;ops[4].op = GRPC_OP_RECV_MESSAGE;ops[4].data.recv_message.recv_message = buffer;ops[5].op = GRPC_OP_RECV_STATUS_ON_CLIENT;void* tag = malloc(1);grpc_call_start_batch(call, ops, 6, tag);grpc_event ev = grpc_completion_queue_next(queue);ASSERT_EQ(ev.tag, tag);ASSERT(strcmp(buffer, "Hello gRPC"));可以看到,对 grpc_call 的操作是通过一次 grpc_call_start_batch 来指定的。这个 start batch 会将指定的操作放在内存 buffer 当中,然后通过 grpc_completion_queue_next 来实际执行相关操作,如收发消息。这里需要注意的是 tag 这个变量。当这些操作都完成以后,grpc_completion_queue_next 会返回一个包含 tag 的消息来通知这个操作完成了。所以在代码的末尾就可以在先前指定的 buffer 读出预期的字符串。 ...

June 13, 2019 · 4 min · jiezi

CICD联动阿里云容器服务Kubernetes实践之Bamboo篇

本文档以构建一个 Java 软件项目并部署到 阿里云容器服务的Kubernetes集群 为例说明如何使用 Bamboo在阿里云Kubernetes服务上运行Remote Agents并在agents上运行Build Plans。 1. 源码项目本示例中创建的GitHub源码项目地址为: https://github.com/AliyunContainerService/jenkins-demo.git 分支为: bamboo2. 在Kubernetes中部署Remote Agent2.1 创建kaniko-docker-cfg secret kaniko-docker-cfg secret用于Remote Agent上构建任务使用kaniko推送容器镜像时的权限配置 kubectl -n bamboo create secret generic kaniko-docker-cfg --from-file=/root/.docker/config.json上面命令中的/root/.docker/config.json,是在linux服务器上使用root用户通过以下命令生成的: docker login registry.cn-hangzhou.aliyuncs.com2.2 创建serviceaccount bamboo以及clusterrolebinding用于kubectl部署应用到kubernetes集群的权限设置,创建bamboo-agent deployment 注意: 本示例中的clusterrolebinding为admin权限, 具体使用中可以根据自己的需要创建最小权限的serviceaccount bamboo-agent.yaml: ---apiVersion: v1kind: ServiceAccountmetadata: namespace: bamboo name: bamboo---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: bamboo-cluster-adminsubjects: - kind: ServiceAccount name: bamboo namespace: bambooroleRef: kind: ClusterRole name: cluster-admin apiGroup: rbac.authorization.k8s.io---apiVersion: apps/v1beta2kind: Deploymentmetadata: name: bamboo-agentspec: replicas: 1 selector: matchLabels: app: bamboo-agent template: metadata: labels: app: bamboo-agent spec: serviceAccountName: bamboo containers: - name: bamboo-agent env: - name: BAMBOO_SERVER_URL value: http://xx.xx.xx.xx:8085 image: registry.cn-hangzhou.aliyuncs.com/haoshuwei/docker-bamboo-agent:v1 imagePullPolicy: Always volumeMounts: - mountPath: /root/.docker/ name: kaniko-docker-cfg volumes: - name: kaniko-docker-cfg secret: secretName: kaniko-docker-cfgkubectl -n bamboo apply -f bamboo-agent.yaml上述kubernetes资源创建完毕后等待remote agent完成初始化配置, 可以使用如下命令查看日志: ...

June 10, 2019 · 1 min · jiezi

DM-源码阅读系列文章六relay-log-的实现

作者:张学程 本文为 DM 源码阅读系列文章的第六篇,在 上篇文章 中我们介绍了 binlog replication 处理单元的实现,对在增量复制过程中 binlog event 的读取、过滤、路由、转换以及执行等逻辑进行了分析。 本篇文章我们将会对 relay 数据处理单元的实现进行详细的讲解。这个单元的作用是从上游 MySQL/MariaDB 读取 binlog event 并写入到本地的 relay log file 中;当执行增量复制任务时,binlog replication 处理单元将读取 relay log file 中的 event 并在进行解析后复制到下游的 TiDB 中。本篇文章的内容包括 relay log 目录结构定义、relay log 数据的处理流程、主从切换支持、relay log 的读取等逻辑。 值得注意的是,由于我们近期正在对 relay 处理单元进行重构,因此源码中会同时包含重构前后的相关代码实现。 relay log 目录结构一个已经进行过一次主从切换的 relay log 目录结构大致如下: <deploy_dir>/relay_log/|-- 7e427cc0-091c-11e9-9e45-72b7c59d52d7.000001| |-- mysql-bin.000001| |-- mysql-bin.000002| |-- mysql-bin.000003| |-- mysql-bin.000004| `-- relay.meta|-- 842965eb-091c-11e9-9e45-9a3bff03fa39.000002| |-- mysql-bin.000001| `-- relay.meta`-- server-uuid.index在 relay log 目录下,主要包含以下几类文件或文件夹数据: ...

June 3, 2019 · 5 min · jiezi

为什么建议你常阅读源码

作者:谢伟授权 LeanCloud 转载 我叫谢伟,是一名侧重在后端的程序员,进一步定位现阶段是 Web 后台开发。 由于自身智力一般,技术迭代又非常快,为不至于总处于入门水平,经常会尝鲜新技术。 为保持好奇心,日常除技术以外,还会涉猎摄影、演示设计、拍视频、自媒体写作等。 如果此刻我是一个成功人士,看到上面的领域,有人会羡慕说:「斜杠」,遗憾的是,在下没有成功,所以,上面的领域都一定程度上会被人认为:「不务正业」,不过不重要,我本职还是一名后端程序员。 记忆记忆有遗忘曲线,这是大家都懂的道理,所以为了防止忘记,最重要的方法是经常使用、反复使用。这也是为什么,有些人说:在工作中学最容易进步。因为工作的流程、项目不会频繁变动,你会经常性的关注一个或者多个项目进行开发,假以时日,你会越来越熟悉,理所当然,你会越做越快。这个时候,就达到了所谓的:舒适圈。要再想进步,你得跳到「学习区」。再反复这个动作。 问题是,除了工作之外,你很少有其他机会再进行技能锻炼了。 创造机会主动承接更为复杂的任务这个比较容易理解,因为更为复杂的任务,你才可能尝试使用新的技术栈,有机会进行其他技能的锻炼,这样就能进入「学习区」。 如果公司项目就这么点,没有太复杂的,或者说新项目和你接触的相差不多,只不过应用场景不同而已。这个时候,任务如果一定需要你的参与,你最好尝试新的架构,尝试新的技术点,尽管大体相同,可以将你认为原系统不合理的地方改进,这样也能创造机会进入「学习区」。 但就我认为,一般项目开发时间都非常紧,开发人员有可能没有充足的时间进行考虑,会依然使用原有技术点,这样进入学习区的机会就被浪费了,你只是使用一份经验,做了两个类型的项目而已。 旧知识补全刚进入职场,核心位置就那么几个人占着,论经验、论资历,你都不如别人,你接触到的资源有限,没有新项目让你独立开发,只有旧项目的 Bug 让你修复,那该怎么办? 换坑吗?怕不怕另外一个也是坑? 补知识体系即使是你能完成的任务,你有没有尝试过自己独立写一个,你有没有尝试过自己弥补下不懂的知识点,你有没有尝试过总结下自己的开发流程是否是最优的,你有没有尝试过总结下项目的技术要点,你有没有尝试过提炼可以复用的技术点... 如果你都没有,恭喜你,你又找到了一个进入「学习区」的点,即:补充原有技术栈。 也许你工作中已经有一门常用编程语言,但都是靠 Google、StackOverFlow,你是不是要尝试梳理下整个编程语言的知识体系,当然梳理的切入点依然是和工作相关为先,因为这最迫切,最能反复,使用频率最高。 也许你对数据库相关知识略懂,对优化数据知识点却不是很懂,你是不是要尝试下找相关资料弥补下。 也许... 也许你还可以翻阅源码,比如内置库的实现,之前我还不太会关注这些,写起代码来不是很有底气,后来经常性的查看源码,借助 IDE 的跳转功能实现对源码的阅读,再结合 IDE 的 structure ,可以对文件的函数、结构体、方法等进行组织。这样从整体观看一目了然,看得多了,你甚至可以总结出一些共性: 比如包的错误处理一般定义在包的顶部几行,而且格式都统一比如 Interface 是方法的集合,内置的常用的 Interface 其实不多,很多内置包都相互实现比如包的结构体,可以实例化一个默认的,这样可以直接调用函数,比如 http.DefaultClient...阅读库的源码,我一般是怎么做的呢?(不要太关注具体的实现,除非你完全能看懂) 官方文档:了解常用使用方法思维导图:输出可导出的结构体、函数、方法等,依然选择最为常用的IDE 的 structure 功能,查看文件的具体组织形式,看可导出的结构体、函数、方法等持续总结举个例子net/http 包几乎奠定了 Go 领域所有 Web 框架、网络请求库的基础。由此来看下我是如何梳理的。 了解 HTTP 相关知识随意找本相关的书,发现是个大块知识啊。结合一般的历史经验,你可能作出这么张思维导图。 整个过程像是:你从一本书总摘出的目录,前提是看过书的内容而得出来的。 net/http 客户端网络请求分为两个层面: 客户端发起网络请求服务端提供网络请求访问资源func getHandle(rawString string) { response, err := http.Get(rawString) if err != nil { return } defer response.Body.Close() content, _ := ioutil.ReadAll(response.Body) fmt.Println(string(content))}看上去发起网络请求很简单,只需要使用 http.Get 即可。 ...

May 22, 2019 · 2 min · jiezi

Whats-New-in-TiDB-300rc1

作者:段兵 2019 年 5 月 10 日,TiDB 3.0.0-rc.1 版本正式推出,该版本对系统稳定性,性能,安全性,易用性等做了较多的改进,接下来逐一介绍。 提升系统稳定性众所周知,数据库的查询计划的稳定性至关重要,此版本采用多种优化手段促进查询计划的稳定性得到进一步提升,如下: 新增 Fast Analyze 功能,使 TiDB 收集统计信息的速度有了数量级的提升,对集群资源的消耗和生产业务的影响比普通 Analyze 方式更小。新增 Incremental Analyze 功能,对于值单调增的索引能够更加方便和快速地更新其统计信息。在 CM-Sketch 中新增 TopN 的统计信息,缓解因为 CM-Sketch 哈希冲突导致估算偏大的问题,使代价估算更加准确。优化 Cost Model,利用和 RowID 列之间的相关性更加精准的估算谓词的选择率,使得索引选择更加稳定和准确。提升系统性能TableScan,IndexScan,Limit 算子,进一步提升 SQL 执行性能。TiKV 采用Iterator Key Bound Option存储结构减少内存分配及拷贝,RocksDB 的 Column Families 共享 block cache 提升 cache命中率等手段大幅提升性能。TiDB Lightning encode SQL 性能提升 50%,将数据源内容解析成 TiDB 的 types.Datum,减少 encode 过程中多余的解析工作,使得性能得到较大的提升。增强系统安全性RBAC(Role-Based Access Control)基于角色的权限访问控制是商业系统中最常见的权限管理技术之一,通过 RBAC 思想可以构建最简单”用户-角色-权限“的访问权限控制模型。RBAC 中用户与角色关联,权限与角色关联,角色与权限之间一般是多对多的关系统,用户通过成为什么样的角色获取该角色所拥有的权限,达到简化权限管理的目的,通过此版本的迭代 RBAC 功能开发完成,欢迎试用。 提升产品易用性新增 SQL 方式查询慢查询,丰富 TiDB 慢查询日志内容,如:Coprocessor 任务数,平均/最长/90% 执行/等待时间,执行/等待时间最长的 TiKV 地址,简化慢查询定位工作,提升产品易用性。新增系统配置项合法性检查,优化系统监控项等,提升产品易用性。支持对 TableReader、IndexReader 和 IndexLookupReader 算子进行内存追踪控制,对 Query 内存使用统计更加精确,可以更好地检测、处理对内存消耗较大的语句。社区贡献V3.0.0-rc.1 版本的开发过程中,开源社区贡献者给予了我们极大的支持,例如美团的同学负责开发的 SQL Plan Management 特性对于提升产品的易用性有很大的帮助,一点资讯的陈付同学与其他同学一起对 TiKV 线程池进行了重构,提高了性能并降低了延迟,掌门科技的聂殿辉同学实现 TiKV 大量 UDF 函数帮忙 TiKV 完善 Coprocessor 功能,就不再一一列举。在此对各位贡献者表示由衷的感谢。接下来我们会开展更多的专项开发活动以及一系列面向社区的培训课程,希望能对大家了解如何做分布式数据库有帮助。 ...

May 13, 2019 · 1 min · jiezi

TiDB-300rc1-Release-Notes

2019 年 5 月 10 日,TiDB 发布 3.0.0-rc.1 版,对应的 TiDB-Ansible 版本为 3.0.0-rc.1。相比 3.0.0-beta.1 版本,该版本对系统稳定性、易用性、功能、优化器、统计信息以及执行引擎做了很多改进。 TiDBSQL 优化器 利用列之间的顺序相关性提升代价估算准确度,并提供启发式参数 tidb_opt_correlation_exp_factor 用于控制在相关性无法被直接用于估算的场景下对索引扫描的偏好程度。当过滤条件中包含相关列时,在抽取复合索引的访问条件时尽可能多地匹配索引的前缀列。用动态规划决定连接的执行顺序,当参与连接的表数量不多于 tidb_opt_join_reorder_threshold 时启用。在构造 Index Join 的的内表中,以复合索引作为访问条件时,尽可能多地匹配索引的前缀列。提升对单列索引上值为 NULL 的行数估算准确度。在逻辑优化阶段消除聚合函数时特殊处理 GROUP_CONCAT ,防止产生错误的执行结果。当过滤条件为常量时,正确地将它下推到连接算子的子节点上。在逻辑优化阶段列剪裁时特殊处理一些函数,例如 RAND() ,防止产生和 MySQL 不兼容的执行结果。支持 FAST ANALYZE,通过tidb_enable_fast_analyze 变量控制。该特性通过用对 Region 进行采样取代扫描整个 region 的方式加速统计信息收集。支持 SQL PLAN MANAGEMENT。该特性通过对 SQL 进行执行计划绑定,以确保执行稳定性。该特性目前处于测试阶段,仅支持对 SELECT 语句使用绑定的执行计划,不建议在生产场景中直接使用。执行引擎 支持对 TableReader、IndexReader 和 IndexLookupReader 算子进行内存追踪控制。在慢日志中展示更多 COPROCESSOR 端执行任务相关细节。如 COPROCESSOR 任务数,平均/最长/90% 执行/等待时间,执行/等待时间最长的 TiKV 地址等。支持 PREPARE 不含占位符的 DDL 语句。Server TiDB 启动时,只允许 DDL owner 执行 bootstrap新增 tidb_skip_isolation_level_check 变量控制检查隔离级别设置为 SERIALIZABLE 时不报错在慢日志中,将隐式提交的时间与 SQL 执行时间融合在一起RBAC 权限管理 ...

May 13, 2019 · 2 min · jiezi

Redis-radix-tree源码解析

Redis实现了不定长压缩前缀的radix tree,用在集群模式下存储slot对应的的所有key信息。本文将详述在Redis中如何实现radix tree。 核心数据结构raxNode是radix tree的核心数据结构,其结构体如下代码所示: typedef struct raxNode { uint32_t iskey:1; uint32_t isnull:1; uint32_t iscompr:1; uint32_t size:29; unsigned char data[];} raxNode;iskey:表示这个节点是否包含key 0:没有key1:表示从头部到其父节点的路径完整的存储了key,查找的时候按子节点iskey=1来判断key是否存在isnull:是否有存储value值,比如存储元数据就只有key,没有value值。value值也是存储在data中iscompr:是否有前缀压缩,决定了data存储的数据结构size:该节点存储的字符个数data:存储子节点的信息iscompr=0:非压缩模式下,数据格式是:[header strlen=0][abc][a-ptr][b-ptr][c-ptr](value-ptr?),有size个字符,紧跟着是size个指针,指向每个字符对应的下一个节点。size个字符之间互相没有路径联系。iscompr=1:压缩模式下,数据格式是:[header strlen=3][xyz][z-ptr](value-ptr?),只有一个指针,指向下一个节点。size个字符是压缩字符片段Rax Insert以下用几个示例来详解rax tree插入的流程。假设j是遍历已有节点的游标,i是遍历新增节点的游标。 场景一:只插入abcd z-ptr指向的叶子节点iskey=1,使用了压缩前缀。 场景二:在abcd之后插入abcdef 从abcd父节点的每个压缩前缀字符比较,遍历完所有abcd节点后指向了其空子节点,j = 0, i < len(abcded)。查找到abcd的空子节点,直接将ef赋值到子节点上,成为abcd的子节点。ef节点被标记为iskey=1,用来标识abcd这个key。ef节点下再创建一个空子节点,iskey=1来表示abcdef这个key。 场景三:在abcd之后插入ab ab在abcd能找到前两位的前缀,也就是i=len(ab),j < len(abcd)。将abcd分割成ab和cd两个子节点,cd也是一个压缩前缀节点,cd同时被标记为iskey=1,来表示ab这个key。cd下挂着一个空子节点,来标记abcd这个key。 场景四:在abcd之后插入abABC abcABC在abcd中只找到了ab这个前缀,即i < len(abcABC),j < len(abcd)。这个步骤有点复杂,分解一下: step 1:将abcd从ab之后拆分,拆分成ab、c、d 三个节点。step 2:c节点是一个非压缩的节点,c挂在ab子节点上。step 3:d节点只有一个字符,所以也是一个非压缩节点,挂在c子节点上。step 4:将ABC 拆分成了A和BC, A挂在ab子节点上,和c节点属于同一个节点,这样A就和c同属于父节点ab。step 5:将BC作为一个压缩前缀的节点,挂在A子节点下。step 6:d节点和BC节点都挂一个空子节点分别标识abcd和abcABC这两个key。 场景五:在abcd之后插入Aabc abcd和Aabc没有前缀匹配,i = 0,j = 0。将abcd拆分成a、bcd两个节点,a节点是一个非压缩前缀节点。将Aabc拆分成A、abc两个节点,A节点也是一个非压缩前缀节点。将A节点挂在和a相同的父节点上。同上,在bcd和abc这两个节点下挂空子节点来分别表示两个key。 Rax Remove删除 ...

April 30, 2019 · 1 min · jiezi

深入了解Vue响应式系统

前言前面几篇文章一直都以源码分析为主,其实枯燥无味,对于新手玩家来说很不友好。这篇文章主要讲讲 Vue 的响应式系统,形式与前边的稍显不同吧,分析为主,源码为辅,如果能达到深入浅出的效果那就更好了。什么是响应式系统「响应式系统」一直以来都是我认为 Vue 里最核心的几个概念之一。想深入理解 Vue ,首先要掌握「响应式系统」的原理。从一个官方的例子开始由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:var vm = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '<div>{{ message }}</div>'})// 之后设置 `message`vm.message = 'Hello!'如果你未在 data 选项中声明 message,Vue 将警告你渲染函数正在试图访问不存在的属性。当然,仅仅从上面这个例子我们也只能知道,Vue不允许动态添加根级响应式属性。这意味我们需要将使用到的变量先在data函数中声明。 抛砖????引玉新建一个空白工程,加入以下代码 export default { name: 'JustForTest', data () { return {} }, created () { this.b = 555 console.log(this.observeB) this.b = 666 console.log(this.observeB) }, computed: { observeB () { return this.b } }}运行上述代码,结果如下: ...

April 26, 2019 · 5 min · jiezi

TiKV-源码解析六raftrs-日志复制过程分析

作者:屈鹏 在 《TiKV 源码解析(二)raft-rs proposal 示例情景分析》 中,我们主要介绍了 raft-rs 的基本 API 使用,其中,与应用程序进行交互的主要 API 是: RawNode::propose 发起一次新的提交,尝试在 Raft 日志中追加一个新项;RawNode::ready_since 从 Raft 节点中获取最近的更新,包括新近追加的日志、新近确认的日志,以及需要给其他节点发送的消息等;在将一个 Ready 中的所有更新处理完毕之后,使用 RawNode::advance 在这个 Raft 节点中将这个 Ready 标记为完成状态。熟悉了以上 3 个 API,用户就可以写出基本的基于 Raft 的分布式应用的框架了,而 Raft 协议中将写入同步到多个副本中的任务,则由 raft-rs 库本身的内部实现来完成,无须应用程序进行额外干预。本文将对数据冗余复制的过程进行详细展开,特别是关于 snapshot 及流量控制的机制,帮助读者更深刻地理解 Raft 的原理。 一般 MsgAppend 及 MsgAppendResponse 的处理在 Raft leader 上,应用程序通过 RawNode::propose 发起的写入会被处理成一条 MsgPropose 类型的消息,然后调用 Raft::append_entry 和 Raft::bcast_append 将消息中的数据追加到 Raft 日志中并广播到其他副本上。整体流程如伪代码所示: fn Raft::step_leader(&mut self, mut m: Message) -> Result<()> { if m.get_msg_type() == MessageType::MsgPropose { // Propose with an empty entry list is not allowed. assert!(!m.get_entries().is_empty()); self.append_entry(&mut m.mut_entries()); self.bcast_append(); }}这段代码中 append_entry 的参数是一个可变引用,这是因为在 append_entry 函数中会为每一个 Entry 赋予正确的 term 和 index。term 由选举产生,在一个 Raft 系统中,每选举出一个新的 Leader,便会产生一个更高的 term。而 index 则是 Entry 在 Raft 日志中的下标。Entry 需要带上 term 和 index 的原因是,在其他副本上的 Raft 日志是可能跟 Leader 不同的,例如一个旧 Leader 在相同的位置(即 Raft 日志中具有相同 index 的地方)广播了一条过期的 Entry,那么当其他副本收到了重叠的、但是具有更高 term 的消息时,便可以用它们替换旧的消息,以便达成与最新的 Leader 一致的状态。 ...

April 25, 2019 · 3 min · jiezi

GitOpsKubernetes多集群环境下的高效CICD实践

为了解决传统应用升级缓慢、架构臃肿、不能快速迭代、故障不能快速定位、问题无法快速解决等问题,云原生这一概念横空出世。云原生可以改进应用开发的效率,改变企业的组织结构,甚至会在文化层面上直接影响一个公司的决策,可以说,云时代的云原生应用大势已来。在容器领域内,Kubernetes已经成为了容器编排和管理的社区标准。它通过把应用服务抽象成多种资源类型,比如Deployment、Service等,提供了一个云原生应用通用的可移植模型。在这样的背景下,我们如何在云原生的环境下实践更高效的DevOps来达到更有生产力的表现就成为了一个新的课题和诉求。 与GitOps这个概念相比,大家可能对DevOps的概念已经耳熟能详了。起初DevOps是为了打破开发测试、运营这些部门之间的壁垒,通过自动化的构建、程式化的脚本,最低限度减少人工误差,一定程度上提高应用版本的迭代效率;容器技术出现以后,轻量、标准化的能力使得DevOps技术才有了突飞猛进的发展。不管技术怎样更新迭代,DevOps最主要的核心诉求是不变的,那就是提高应用迭代的频率和降低成本。GitOps就是DevOps的逻辑扩展,它的核心目标是为了更加高效和安全的应用发布。 首先我们提取出一些用户在做devops的过程中遇到的痛点进行分析。第一个问题是如何自动化推进应用在环境栈中的无差别发布.这里我列举了三种环境,测试环境、生产环境和预发环境,对于一个应用来说,我们通常的设定都是把不同分支部署到对应环境,比如master分支的源码对应的是线上环境,latest分支对应的是预发环境,其他开发分支对应地部署到测试环境;目前大多数的做法是创建不同的job,拉取不同的源码分支、部署到不同的环境,或者同一个job,通过添加不同的构建参数来决定进行怎样的构建和发布动作。 非常容易产生混乱和不便于管理。 第二个问题就是,生产环境的发布权限一般都是需要严格控制的,通常只有应用管理员或者运维管理员才有生产发布权限。我们在跟一些客户的交流中发现,一种方式是在同一套cicd环境中创建不同的job,然后通过基于角色访问控制策略来做job的隔离,只有管理员权限的人员才能看到用于发布生产的job; 更直接的一种做法就是再建一套cicd环境专门做生产环境的发布, 但这样既浪费资源又降低了应用迭代的频率。 第三个问题是说我们想要提高应用迭代的频率进而降低人力成本、时间成本、把精力放在新业务或创新业务的拓展上,但目前我们的开发测试人员在应用运行状态或测试结果的同步与反馈上有一定的隔阂,另外一个是线上业务出现问题的时候,如何快速定位、复现和回滚,这是一个我们可以重点思考的地方。以上三点只是我列举出来的我们用户在实际使用cicd的过程中的一些痛点的子集,那接下来我们就带着这些问题来看一下gitops模型的设计思路是怎样的 我们在设计gitosp发布模型的时候是有以下这些核心诉求的:第一个是版本管理,我们希望每一个发布的应用的版本号都能跟git commit id关联,这样的好处就是每一个变更都有历史记录查询、可以更快进行故障定位和修复,第二个是基线管理,这里我们一会儿会讲到分两种类型的基线,第三个是怎么做安全发布,包括发布权限管理以及安全审批的内容;最后一个是如何让开发测试人员快速获取反馈 首先gitops的核心思想就是将应用系统的声明性基础架构和应用程序存放在Git版本库中,所有对应用的操作变更都来源于Git仓库的更新,这也是gitops这个名称的由来。另外一个问题是,按照以往通用的做法,我们可能会把应用如何构建如何部署的脚本以及配置文件跟应用源码本身存放在同一个仓库里,这样带来的问题有两个,一是开发人员可能还需要维护这个部署脚本或配置文件,不能把精力集中到产品开发上,另外一个问题是部署脚本有时候会涉及环境敏感信息,安全性不够,所以我们这里一定要把应用源码仓库与构建仓库分开管理。 接下来就是基线管理,基线管理分两种,一种是环境栈基线,如图所示,我们的设定是,生产环境只能部署master分支的代码,预发环境只能部署latest分支的代码,预览环境用来部署其他开发分支,这里有个名词叫预览环境,其实也就是测试环境,但我们会在开发分支通过测试、通过验证成功合并到latest分支以后动态销毁这个测试环境,当然这在kubernetes容器集群下是非常容器做到的,在其他具体的场景下可以用不同的策略。这个基线我们可以把它称为小基线,它是用来明确管理应用在预览、预发、生产环境中的推进的。大基线是针对线上发布版本的管理的,这能保证我们在线上出现故障的时候能快速回滚到上一个稳定的版本。这在生产发布管理中是必不可少的,在gitops中我们还能快速定位故障精确到某个git commit。 然后是应用发布的权限管理和安全审批,gitops中的权限管理是通过代码合并的控制来做的,在这个模型中,普通的开发人员没有cicd环境比如jenkins的访问权限,更精确地说的话是只有日志查看的权限,在git这一端,普通开发者只有向开发测试分支推送代码的权限,并可以申请向latest分支合并代码,即提交MR/PR的权限,当普通开发者新建MR/PR后,就会触发构建把应用部署到预览环境,管理员通过查看这个新分支的构建部署是否通过一系列测试和验证来决定是否接受这个MR/PR, 只有管理员接受MR/PR的合并后,latest分支代码才会重新构建和部署到预发环境,这样就通过MR/PR的接受和拒绝来达到应用发布安全审批的目的。 最后是如何进行快速反馈和团队成员间的互动,这包括两部分内容:一个是普通开发测试人员在推送源码后,能通过邮件、钉钉、slack等工具实时地获取构建结果,对自己的应用进行高效开发测试,;另一方面是能在MR/PR的页面上查看自动化测试的反馈结果、应用预览链接、其他团队成员的comment等。 下面是使用GitOps管理应用发布到不同kubernetes集群的架构图和时序图。首先是应用源码与构建源码分离。最上面有一条虚线,虚线上面是普通开发者能看到的,或者说是有权限进行操作的部分,剩下其他的部分都是管理员才有权限做的,绿色区域是Jenkins的流水线任务。普通开发者没有Jenkins环境的创建Job和构建Job的权限,他有的只是构建Job的日志查看权限。这个普通应用是在Git仓库里,它有不同的分支,有一定设定的关系,每次有构建的时候会从另外一个Git仓库里做,比如preview-plpeline、prod-plpeline,在这里面可以存放一些信息,只有应用管理员才能看到,普通开发者没有权限看到信息。 然后我们需要设置应用发布环境栈,这在个示例中我们有预览环境、预发环境、生产环境的设置,应用在预发环境和生产环境中的发布是需要经过管理员安全审批的。 最后是一个时序图,开发人员提交新的feature,创建指向latest分支的MR,创建MR的动作会触发preview-plpeline的构建,构建会拉取preview-plpeline的构建仓库,构建仓库存放的是构建脚本以及要部署的环境信息。然后就是自动化的构建流程,首先会从应用源码仓库把应用源码拉取下来做构建,静态代码测试、单元测试,测试结果会反馈到MR上,然后打包容器镜像并把镜像推送到镜像仓库,最后会把应用通过文件部署到Kubernetes的集群里并进行功能测试,测试结果反馈到MR上,部署之后会收集应用相关信息,通过钉钉通知发送到开发群里。开发人员收到钉钉通知,可以直接点击链接查看应用状态,如果有问题,可以返回来自己重新开发,再重新进行提交,把前面的流程再走一遍,没问题就可以请求管理员进行审批,把代码合并到latest分支。latest分支和master分支有更新时,就会触发与前面的构建类似的流程把应用推进到预发环境和生产环境。 本文作者:流生阅读原文 本文为云栖社区原创内容,未经允许不得转载。

April 24, 2019 · 1 min · jiezi

源码|详解分布式事务之 Seata-Client 原理及流程

摘要: 本文主要基于 spring cloud + spring jpa + spring cloud alibaba fescar + mysql + seata 的结构,搭建一个分布式系统的 demo,通过 seata 的 debug 日志和源代码,从 client 端(RM、TM)的角度分析其工作流程及原理。前言在分布式系统中,分布式事务是一个必须要解决的问题,目前使用较多的是最终一致性方案。自年初阿里开源了Fescar(四月初更名为Seata)后,该项目受到了极大的关注,目前已接近 8000 Star。Seata以高性能和零侵入的特性为目标解决微服务领域的分布式事务难题,目前正处于快速迭代中,近期小目标是生产可用的 Mysql 版本。 本文主要基于 spring cloud + spring jpa + spring cloud alibaba fescar + mysql + seata 的结构,搭建一个分布式系统的 demo,通过 seata 的 debug 日志和源代码,从 client 端(RM、TM)的角度分析其工作流程及原理。(示例项目:https://github.com/fescar-group/fescar-samples/tree/master/springcloud-jpa-seata) 为了更好地理解全文,我们来熟悉一下相关概念: XID:全局事务的唯一标识,由 ip:port:sequence 组成;Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;Transaction Manager (TM ):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚;提示:文中代码是基于 fescar-0.4.1 版本,由于项目刚更名为 seata 不久,其中一些包名、类名、jar包等名称还没统一更换过来,故下文中仍使用 fescar 进行表述。分布式框架支持Fescar 使用 XID 表示一个分布式事务,XID 需要在一次分布式事务请求所涉的系统中进行传递,从而向 feacar-server 发送分支事务的处理情况,以及接收 feacar-server 的 commit、rollback 指令。 Fescar 官方已支持全版本的 dubbo 协议,而对于 spring cloud(spring-boot)的分布式项目社区也提供了相应的实现 ...

April 23, 2019 · 11 min · jiezi

【React深入】深入分析虚拟DOM的渲染原理和特性

导读React的虚拟DOM和Diff算法是React的非常重要的核心特性,这部分源码也非常复杂,理解这部分知识的原理对更深入的掌握React是非常必要的。本来想将虚拟DOM和Diff算法放到一篇文章,写完虚拟DOM发现文章已经很长了,所以本篇只分析虚拟DOM。本篇文章从源码出发,分析虚拟DOM的核心渲染原理(首次渲染),以及React对它做的性能优化点。说实话React源码真的很难读????,如果本篇文章帮助到了你,那么请给个赞????支持一下吧。开发中的常见问题为何必须引用React自定义的React组件为何必须大写React如何防止XSSReact的Diff算法和其他的Diff算法有何区别key在React中的作用如何写出高性能的React组件如果你对上面几个问题还存在疑问,说明你对React的虚拟DOM以及Diff算法实现原理还有所欠缺,那么请好好阅读本篇文章吧。首先我们来看看到底什么是虚拟DOM:虚拟DOM在原生的JavaScript程序中,我们直接对DOM进行创建和更改,而DOM元素通过我们监听的事件和我们的应用程序进行通讯。而React会先将你的代码转换成一个JavaScript对象,然后这个JavaScript对象再转换成真实DOM。这个JavaScript对象就是所谓的虚拟DOM。比如下面一段html代码:<div class=“title”> <span>Hello ConardLi</span> <ul> <li>苹果</li> <li>橘子</li> </ul></div>在React可能存储为这样的JS代码:const VitrualDom = { type: ‘div’, props: { class: ’title’ }, children: [ { type: ‘span’, children: ‘Hello ConardLi’ }, { type: ‘ul’, children: [ { type: ‘ul’, children: ‘苹果’ }, { type: ‘ul’, children: ‘橘子’ } ] } ]}当我们需要创建或更新元素时,React首先会让这个VitrualDom对象进行创建和更改,然后再将VitrualDom对象渲染成真实DOM;当我们需要对DOM进行事件监听时,首先对VitrualDom进行事件监听,VitrualDom会代理原生的DOM事件从而做出响应。为何使用虚拟DOMReact为何采用VitrualDom这种方案呢?提高开发效率使用JavaScript,我们在编写应用程序时的关注点在于如何更新DOM。使用React,你只需要告诉React你想让视图处于什么状态,React则通过VitrualDom确保DOM与该状态相匹配。你不必自己去完成属性操作、事件处理、DOM更新,React会替你完成这一切。这让我们更关注我们的业务逻辑而非DOM操作,这一点即可大大提升我们的开发效率。关于提升性能很多文章说VitrualDom可以提升性能,这一说法实际上是很片面的。直接操作DOM是非常耗费性能的,这一点毋庸置疑。但是React使用VitrualDom也是无法避免操作DOM的。如果是首次渲染,VitrualDom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存。VitrualDom的优势在于React的Diff算法和批处理策略,React在页面更新之前,提前计算好了如何进行更新和渲染DOM。实际上,这个计算过程我们在直接操作DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如React好的。所以,在这个过程中React帮助我们"提升了性能"。所以,我更倾向于说,VitrualDom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快。如果您对本部分的分析有什么不同见解,欢迎在评论区拍砖。跨浏览器兼容React基于VitrualDom自己实现了一套自己的事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,抹平了各个浏览器的事件兼容性问题。跨平台兼容VitrualDom为React带来了跨平台渲染的能力。以React Native为例子。React根据VitrualDom画出相应平台的ui层,只不过不同平台画的姿势不同而已。虚拟DOM实现原理如果你不想看繁杂的源码,或者现在没有足够时间,可以跳过这一章,直接????虚拟DOM原理总结在上面的图上我们继续进行扩展,按照图中的流程,我们依次来分析虚拟DOM的实现原理。JSX和createElement我们在实现一个React组件时可以选择两种编码方式,第一种是使用JSX编写:class Hello extends Component { render() { return <div>Hello ConardLi</div>; }}第二种是直接使用React.createElement编写:class Hello extends Component { render() { return React.createElement(‘div’, null, Hello ConardLi); }}实际上,上面两种写法是等价的,JSX只是为 React.createElement(component, props, …children) 方法提供的语法糖。也就是说所有的JSX 代码最后都会转换成React.createElement(…) ,Babel帮助我们完成了这个转换的过程。如下面的JSX<div> <img src=“avatar.png” className=“profile” /> <Hello /></div>;将会被Babel转换为React.createElement(“div”, null, React.createElement(“img”, { src: “avatar.png”, className: “profile”}), React.createElement(Hello, null));注意,babel在编译时会判断JSX中组件的首字母,当首字母为小写时,其被认定为原生DOM标签,createElement的第一个变量被编译为字符串;当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象;另外,由于JSX提前要被Babel编译,所以JSX是不能在运行时动态选择类型的,比如下面的代码:function Story(props) { // Wrong! JSX type can’t be an expression. return <components[props.storyType] story={props.story} />;}需要变成下面的写法:function Story(props) { // Correct! JSX type can be a capitalized variable. const SpecificStory = components[props.storyType]; return <SpecificStory story={props.story} />;}所以,使用JSX你需要安装Babel插件babel-plugin-transform-react-jsx{ “plugins”: [ [“transform-react-jsx”, { “pragma”: “React.createElement” }] ]}创建虚拟DOM下面我们来看看虚拟DOM的真实模样,将下面的JSX代码在控制台打印出来:<div className=“title”> <span>Hello ConardLi</span> <ul> <li>苹果</li> <li>橘子</li> </ul></div>这个结构和我们上面自己描绘的结构很像,那么React是如何将我们的代码转换成这个结构的呢,下面我们来看看createElement函数的具体实现(文中的源码经过精简)。createElement函数内部做的操作很简单,将props和子元素进行处理后返回一个ReactElement对象,下面我们来逐一分析:(1).处理props:1.将特殊属性ref、key从config中取出并赋值2.将特殊属性self、source从config中取出并赋值3.将除特殊属性的其他属性取出并赋值给props后面的文章会详细介绍这些特殊属性的作用。(2).获取子元素1.获取子元素的个数 —— 第二个参数后面的所有参数2.若只有一个子元素,赋值给props.children3.若有多个子元素,将子元素填充为一个数组赋值给props.children(3).处理默认props将组件的静态属性defaultProps定义的默认props进行赋值ReactElementReactElement将传入的几个属性进行组合,并返回。type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class)key:组件的唯一标识,用于Diff算法,下面会详细介绍ref:用于访问原生dom节点props:传入组件的propsowner:当前正在构建的Component所属的Component$$typeof:一个我们不常见到的属性,它被赋值为REACT_ELEMENT_TYPE:var REACT_ELEMENT_TYPE = (typeof Symbol === ‘function’ && Symbol.for && Symbol.for(‘react.element’)) || 0xeac7;可见,$$typeof是一个Symbol类型的变量,这个变量可以防止XSS。如果你的服务器有一个漏洞,允许用户存储任意JSON对象, 而客户端代码需要一个字符串,这可能会成为一个问题:// JSONlet expectedTextButGotJSON = { type: ‘div’, props: { dangerouslySetInnerHTML: { __html: ‘/* put your exploit here */’ }, },};let message = { text: expectedTextButGotJSON };<p> {message.text}</p>JSON中不能存储Symbol类型的变量。ReactElement.isValidElement函数用来判断一个React组件是否是有效的,下面是它的具体实现。ReactElement.isValidElement = function (object) { return typeof object === ‘object’ && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;};可见React渲染时会把没有$$typeof标识,以及规则校验不通过的组件过滤掉。当你的环境不支持Symbol时,$$typeof被赋值为0xeac7,至于为什么,React开发者给出了答案:0xeac7看起来有点像React。self、source只有在非生产环境才会被加入对象中。self指定当前位于哪个组件实例。_source指定调试代码来自的文件(fileName)和代码行数(lineNumber)。虚拟DOM转换为真实DOM上面我们分析了代码转换成了虚拟DOM的过程,下面来看一下React如何将虚拟DOM转换成真实DOM。本部分逻辑较复杂,我们先用流程图梳理一下整个过程,整个过程大概可分为四步:过程1:初始参数处理在编写好我们的React组件后,我们需要调用ReactDOM.render(element, container[, callback])将组件进行渲染。render函数内部实际调用了_renderSubtreeIntoContainer,我们来看看它的具体实现: render: function (nextElement, container, callback) { return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback); },1.将当前组件使用TopLevelWrapper进行包裹TopLevelWrapper只一个空壳,它为你需要挂载的组件提供了一个rootID属性,并在render函数中返回该组件。TopLevelWrapper.prototype.render = function () { return this.props.child;};ReactDOM.render函数的第一个参数可以是原生DOM也可以是React组件,包裹一层TopLevelWrapper可以在后面的渲染中将它们进行统一处理,而不用关心是否原生。2.判断根结点下是否已经渲染过元素,如果已经渲染过,判断执行更新或者卸载操作3.处理shouldReuseMarkup变量,该变量表示是否需要重新标记元素4.调用将上面处理好的参数传入_renderNewRootComponent,渲染完成后调用callback。在_renderNewRootComponent中调用instantiateReactComponent对我们传入的组件进行分类包装:根据组件的类型,React根据原组件创建了下面四大类组件,对组件进行分类渲染:ReactDOMEmptyComponent:空组件ReactDOMTextComponent:文本ReactDOMComponent:原生DOMReactCompositeComponent:自定义React组件他们都具备以下三个方法:construct:用来接收ReactElement进行初始化。mountComponent:用来生成ReactElement对应的真实DOM或DOMLazyTree。unmountComponent:卸载DOM节点,解绑事件。具体是如何渲染我们在过程3中进行分析。过程2:批处理、事务调用在_renderNewRootComponent中使用ReactUpdates.batchedUpdates调用batchedMountComponentIntoNode进行批处理。ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);在batchedMountComponentIntoNode中,使用transaction.perform调用mountComponentIntoNode让其基于事务机制进行调用。 transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context);关于批处理事务,在我前面的分析setState执行机制中有更多介绍。过程3:生成html在mountComponentIntoNode函数中调用ReactReconciler.mountComponent生成原生DOM节点。mountComponent内部实际上是调用了过程1生成的四种对象的mountComponent方法。首先来看一下ReactDOMComponent:1.对特殊DOM标签、props进行处理。2.根据标签类型创建DOM节点。3.调用_updateDOMProperties将props插入到DOM节点,_updateDOMProperties也可用于props Diff,第一个参数为上次渲染的props,第二个参数为当前props,若第一个参数为空,则为首次创建。4.生成一个DOMLazyTree对象并调用_createInitialChildren将孩子节点渲染到上面。那么为什么不直接生成一个DOM节点而是要创建一个DOMLazyTree呢?我们先来看看_createInitialChildren做了什么:判断当前节点的dangerouslySetInnerHTML属性、孩子节点是否为文本和其他节点分别调用DOMLazyTree的queueHTML、queueText、queueChild。可以发现:DOMLazyTree实际上是一个包裹对象,node属性中存储了真实的DOM节点,children、html、text分别存储孩子、html节点和文本节点。它提供了几个方法用于插入孩子、html以及文本节点,这些插入都是有条件限制的,当enableLazy属性为true时,这些孩子、html以及文本节点会被插入到DOMLazyTree对象中,当其为false时会插入到真实DOM节点中。var enableLazy = typeof document !== ‘undefined’ && typeof document.documentMode === ’number’ || typeof navigator !== ‘undefined’ && typeof navigator.userAgent === ‘string’ && /\bEdge/\d/.test(navigator.userAgent);可见:enableLazy是一个变量,当前浏览器是IE或Edge时为true。在IE(8-11)和Edge浏览器中,一个一个插入无子孙的节点,效率要远高于插入一整个序列化完整的节点树。所以lazyTree主要解决的是在IE(8-11)和Edge浏览器中插入节点的效率问题,在后面的过程4我们会分析到:若当前是IE或Edge,则需要递归插入DOMLazyTree中缓存的子节点,其他浏览器只需要插入一次当前节点,因为他们的孩子已经被渲染好了,而不用担心效率问题。下面来看一下ReactCompositeComponent,由于代码非常多这里就不再贴这个模块的代码,其内部主要做了以下几步:处理props、contex等变量,调用构造函数创建组件实例判断是否为无状态组件,处理state调用performInitialMount生命周期,处理子节点,获取markup。调用componentDidMount生命周期在performInitialMount函数中,首先调用了componentWillMount生命周期,由于自定义的React组件并不是一个真实的DOM,所以在函数中又调用了孩子节点的mountComponent。这也是一个递归的过程,当所有孩子节点渲染完成后,返回markup并调用componentDidMount。过程4:渲染html在mountComponentIntoNode函数中调用将上一步生成的markup插入container容器。在首次渲染时,_mountImageIntoNode会清空container的子节点后调用DOMLazyTree.insertTreeBefore:判断是否为fragment节点或者<object>插件:如果是以上两种,首先调用insertTreeChildren将此节点的孩子节点渲染到当前节点上,再将渲染完的节点插入到html如果是其他节点,先将节点插入到插入到html,再调用insertTreeChildren将孩子节点插入到html。若当前不是IE或Edge,则不需要再递归插入子节点,只需要插入一次当前节点。判断不是IE或bEdge时return若children不为空,递归insertTreeBefore进行插入渲染html节点渲染文本节点原生DOM事件代理有关虚拟DOM的事件机制,我曾专门写过一篇文章,有兴趣可以????【React深入】React事件机制虚拟DOM原理、特性总结React组件的渲染流程使用React.createElement或JSX编写React组件,实际上所有的JSX 代码最后都会转换成React.createElement(…) ,Babel帮助我们完成了这个转换的过程。createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个ReactElement对象(所谓的虚拟DOM)。ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM。虚拟DOM的组成即ReactElementelement对象,我们的组件最终会被渲染成下面的结构:type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class)key:组件的唯一标识,用于Diff算法,下面会详细介绍ref:用于访问原生dom节点props:传入组件的props,chidren是props中的一个属性,它存储了当前组件的孩子节点,可以是数组(多个孩子节点)或对象(只有一个孩子节点)owner:当前正在构建的Component所属的Componentself:(非生产环境)指定当前位于哪个组件实例_source:(非生产环境)指定调试代码来自的文件(fileName)和代码行数(lineNumber)防止XSSReactElement对象还有一个$$typeof属性,它是一个Symbol类型的变量Symbol.for(‘react.element’),当环境不支持Symbol时,$$typeof被赋值为0xeac7。这个变量可以防止XSS。如果你的服务器有一个漏洞,允许用户存储任意JSON对象, 而客户端代码需要一个字符串,这可能为你的应用程序带来风险。JSON中不能存储Symbol类型的变量,而React渲染时会把没有$$typeof标识的组件过滤掉。批处理和事务React在渲染虚拟DOM时应用了批处理以及事务机制,以提高渲染性能。关于批处理以及事务机制,在我之前的文章【React深入】setState的执行机制中有详细介绍。针对性的性能优化在IE(8-11)和Edge浏览器中,一个一个插入无子孙的节点,效率要远高于插入一整个序列化完整的节点树。React通过lazyTree,在IE(8-11)和Edge中进行单个节点依次渲染节点,而在其他浏览器中则首先将整个大的DOM结构构建好,然后再整体插入容器。并且,在单独渲染节点时,React还考虑了fragment等特殊节点,这些节点则不会一个一个插入渲染。虚拟DOM事件机制React自己实现了一套事件机制,其将所有绑定在虚拟DOM上的事件映射到真正的DOM事件,并将所有的事件都代理到document上,自己模拟了事件冒泡和捕获的过程,并且进行统一的事件分发。React自己构造了合成事件对象SyntheticEvent,这是一个跨浏览器原生事件包装器。 它具有与浏览器原生事件相同的接口,包括stopPropagation() 和 preventDefault() 等等,在所有浏览器中他们工作方式都相同。这抹平了各个浏览器的事件兼容性问题。上面只分析虚拟DOM首次渲染的原理和过程,当然这并不包括虚拟 DOM进行 Diff的过程,下一篇文章我们再来详细探讨。关于开篇提的几个问题,我们在下篇文章中进行统一回答。推荐阅读【React深入】setState的执行机制【React深入】React事件机制【React深入】从Mixin到HOC再到Hook末尾文中如有错误,欢迎在评论区指正,或者您对文章的排版,阅读体验有什么好的建议,欢迎在评论区指出,谢谢阅读。想阅读更多优质文章、下载文章中思维导图源文件、阅读文中demo源码、可关注我的github博客,你的star✨、点赞和关注是我持续创作的动力!推荐关注我的微信公众号【code秘密花园】,每天推送高质量文章,我们一起交流成长。 ...

April 17, 2019 · 2 min · jiezi

Go调度器系列(4)源码阅读与探索

各位朋友,这次想跟大家分享一下Go调度器源码阅读相关的知识和经验,网络上已经有很多剖析源码的好文章,所以这篇文章不是又一篇源码剖析文章,注重的不是源码分析分享,而是带给大家一些学习经验,希望大家能更好的阅读和掌握Go调度器的实现。本文主要分2个部分:解决如何阅读源码的问题。阅读源码本质是把脑海里已经有的调度设计,看看到底是不是这么实现的,是怎么实现的。带给你一个探索Go调度器实现的办法。源码都到手了,你可以修改、窥探,通过这种方式解决阅读源码过程中的疑问,验证一些想法。比如:负责调度的是g0,怎么才能schedule()在执行时,当前是g0呢?如何阅读源码阅读前提阅读Go源码前,最好已经掌握Go调度器的设计和原理,如果你还无法回答以下问题:为什么需要Go调度器?Go调度器与系统调度器有什么区别和关系/联系?G、P、M是什么,三者的关系是什么?P有默认几个?M同时能绑定几个P?M怎么获得G?M没有G怎么办?为什么需要全局G队列?Go调度器中的负载均衡的2种方式是什么?work stealing是什么?什么原理?系统调用对G、P、M有什么影响?Go调度器抢占是什么样的?一定能抢占成功吗?建议阅读Go调度器系列文章,以及文章中的参考资料:Go调度器系列(1)起源Go调度器系列(2)宏观看调度器Go调度器系列(3)图解调度原理优秀源码资料推荐既然你已经能回答以上问题,说明你对Go调度器的设计已经有了一定的掌握,关于Go调度器源码的优秀资料已经有很多,我这里推荐2个:雨痕的Go源码剖析六章并发调度,不止是源码,是以源码为基础进行了详细的Go调度器介绍:ttps://github.com/qyuhen/bookGo夜读第12期,golang中goroutine的调度,M、P、G各自的一生状态,以及转换关系:https://reading.developerlear…Go调度器的源码还涉及GC等,阅读源码时,可以暂时先跳过,主抓调度的逻辑。另外,Go调度器涉及汇编,也许你不懂汇编,不用担心,雨痕的文章对汇编部分有进行解释。最后,送大家一幅流程图,画出了主要的调度流程,大家也可边阅读边画,增加理解,高清版可到博客下载(原图原文跳转)。如何探索调度器这部分教你探索Go调度器的源码,验证想法,主要思想就是,下载Go的源码,添加调试打印,编译修改的源文件,生成修改的go,然后使用修改go运行测试代码,观察结果。下载和编译GoGithub下载,并且换到go1.11.2分支,本文所有代码修改都基于go1.11.2版本。$ GODIR=$GOPATH/src/github.com/golang/go$ mkdir -p $GODIR$ cd $GODIR/..$ git clone https://github.com/golang/go.git$ cd go$ git fetch origin go1.11.2$ git checkout origin/go1.11.2$ git checkout -b go1.11.2$ git checkout go1.11.2初次编译,会跑测试,耗时长一点$ cd $GODIR/src$ ./all.bash以后每次修改go源码后可以这样,4分钟左右可以编译完成$ cd $GODIR/src$ time ./make.bashBuilding Go cmd/dist using /usr/local/go.Building Go toolchain1 using /usr/local/go.Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.Building Go toolchain2 using go_bootstrap and Go toolchain1.Building Go toolchain3 using go_bootstrap and Go toolchain2.Building packages and commands for linux/amd64.—Installed Go for linux/amd64 in /home/xxx/go/src/github.com/golang/goInstalled commands in /home/xxx/go/src/github.com/golang/go/binreal 1m11.675suser 4m4.464ssys 0m18.312s编译好的go和gofmt在$GODIR/bin目录。$ ll $GODIR/bintotal 16044-rwxrwxr-x 1 vnt vnt 13049123 Apr 14 10:53 go-rwxrwxr-x 1 vnt vnt 3377614 Apr 14 10:53 gofmt为了防止我们修改的go和过去安装的go冲突,创建igo软连接,指向修改的go。$ mkdir -p ~/testgo/bin$ cd /testgo/bin$ ln -sf $GODIR/bin/go igo最后,把/testgo/bin加入到PATH,就能使用igo来编译代码了,运行下igo,应当获得go1.11.2的版本:$ igo versiongo version go1.11.2 linux/amd64当前,已经掌握编译和使用修改的go的办法,接下来就以1个简单的例子,教大家如何验证想法。验证schedule()由g0执行阅读源码的文章,你已经知道了g0是负责调度的,并且g0是全局变量,可在runtime包的任何地方直接使用,看到schedule()代码如下(所在文件:$GODIR/src/runtime/proc.go):// One round of scheduler: find a runnable goroutine and execute it.// Never returns.func schedule() { // 获取当前g,调度时这个g应当是g0 g := getg() if g.m.locks != 0 { throw(“schedule: holding locks”) } // m已经被某个g锁定,先停止当前m,等待g可运行时,再执行g,并且还得到了g所在的p if g.m.lockedg != 0 { stoplockedm() execute(g.m.lockedg.ptr(), false) // Never returns. } // 省略…}问题:既然g0是负责调度的,为何schedule()每次还都执行_g_ := getg(),直接使用g0不行吗?schedule()真的是g0执行的吗?在《Go调度器系列(2)宏观看调度器》这篇文章中我曾介绍了trace的用法,阅读代码时发现使用debug.schedtrace和print()函数可以用作打印调试信息,那我们是不是可以使用这种方法打印我们想获取的信息呢?当然可以。另外,注意print()并不是fmt.Print(),也不是C语言的printf,所以不是格式化输出,它是汇编实现的,我们不深入去了解它的实现了,现在要掌握它的用法:// The print built-in function formats its arguments in an// implementation-specific way and writes the result to standard error.// Print is useful for bootstrapping and debugging; it is not guaranteed// to stay in the language.func print(args …Type)从上面可以看到,它接受可变长参数,我们使用的时候只需要传进去即可,但要手动控制格式。我们修改schedule()函数,使用debug.schedtrace > 0控制打印,加入3行代码,把goid给打印出来,如果始终打印goid为0,则代表调度确实是由g0执行的:if debug.schedtrace > 0 { print(“schedule(): goid = “, g.goid, “\n”) // 会是0吗?是的}schedule()如下:// One round of scheduler: find a runnable goroutine and execute it.// Never returns.func schedule() { // 获取当前g,调度时这个g应当是g0 g := getg() if debug.schedtrace > 0 { print(“schedule(): goid = “, g.goid, “\n”) // 会是0吗?是的 } if g.m.locks != 0 { throw(“schedule: holding locks”) } // …}编译igo:$ cd $GODIR/src$ ./make.bash编写一个简单的demo(不能更简单):package mainfunc main() {}结果如下,你会发现所有的schedule()函数调用都打印goid = 0,足以证明Go调度器的调度由g0完成(如果你认为还是缺乏说服力,可以写复杂一些的demo):$ GODEBUG=schedtrace=1000 igo run demo1.goschedule(): goid = 0schedule(): goid = 0SCHED 0ms: gomaxprocs=8 idleprocs=6 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0schedule(): goid = 0// 省略几百行启发比结论更重要,希望各位朋友在学习Go调度器的时候,能多一些自己的探索和研究,而不仅仅停留在看看别人文章之上。参考资料Installing Go from source如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019… ...

April 15, 2019 · 2 min · jiezi

DM 源码阅读系列文章(三)数据同步处理单元介绍

作者:lan本文为 DM 源码阅读系列文章的第三篇,上篇文章 介绍了 DM 的整体架构,DM 组件 DM-master 和 DM-worker 的入口代码,以及两者之间的数据交互模型。本篇文章详细地介绍 DM 数据同步处理单元(DM-worker 内部用来同步数据的逻辑单元),包括数据同步处理单元实现了什么功能,数据同步流程、运行逻辑,以及数据同步处理单元的 interface 设计。数据同步处理单元从上图可以了解到目前 DM 包含 relay log、dump、load、binlog replication(sync) 4 个数据同步处理单元,涵盖了以下数据同步处理的功能:处理单元功能relay log持久化 MySQL/MariaDB Binlog 到磁盘dump从 MySQL/MariaDB dump 全量数据load加载全量数据到 TiDB clusterbinlog replication(sync)复制 relay log 存储的 Binlog 到 TiDB cluster数据同步流程Task 数据同步流程初始化操作步骤:DM-master 接收到 task,将 task 拆分成 subtask 后 分发给对应的各个 DM-worker;DM-worker 接收到 subtask 后 创建一个 subtask 对象,然后 初始化数据同步流程。从 初始化数据同步流程 的代码中我们可以看到,根据 task 配置项 task-mode 的不同,DM-worker 会初始化不同的数据同步流程:task-mode同步流程需要的数据同步处理单元all全量同步 -> 增量数据同步relay log、dump、load、binlog replication(sync)full全量同步dump、loadincremental增量同步relay log,binlog replication(sync)运行逻辑DM 数据同步处理单元 interface 定义在 dm/unit,relay log、dump、load、binlog replication(sync)都实现了该 interface(golang interface 介绍)。实际上 DM-worker 中的数据同步处理单元分为两类:全局共享单例。dm-worker 启动的时候只初始化一次这类数据同步处理单元,所有的 subtask 都可以使用这类数据同步处理单元的服务;relay log 属于这种类型。subtask 独享。dm-worker 会为每个 subtask 初始化一系列的数据同步处理单元;dump、load、binlog replication(sync)属于这种类型。两类数据同步处理单元的使用逻辑不同,这篇文档会着重讲一下 subtask 独享的数据同步处理单元的使用逻辑,不会囊括更多的 relay log 相关的内容,后面会有单独一篇文章详细介绍它。relay log 相关使用代码在 dm/worker/relay.go 、具体功能实现代码在 relay/relay.go,有兴趣的同学也可以先行阅读一下相关代码,relay log 的代码注释也是比较丰富,并且简单易懂。subtask 独享数据同步处理单元使用逻辑相关代码在 dm/worker/subtask.go。subtask 对象包含的主要属性有:units:初始化后要运行的数据同步处理单元。currUnit:当前正在运行的数据同步处理单元。prevUnit:上一个运行的数据同步处理单元。stage:subtask 的运行阶段状态, 包含 New、Running、Paused,Stopped,Finished,具体定义的代码在 dm/proto/dmworker.proto。result:subtask 当前数据同步处理单元的运行结果,对应着 stage = Paused/Stopped/Finished 的详细信息。主要的逻辑有:初始化 subtask 对象实例的时候会 编排数据同步处理单元的运行先后顺序。所有的数据同步处理单元都实现了 dm/unit interface,所以接下来的运行中就不需要关心具体的数据同步处理单元的类型,可以按照统一的 interface 方法来运行数据同步处理单元,以及对其进行状态监控。初始化各个数据同步处理单元。subtask 在运行前集中地初始化所有的数据同步处理单元,我们计划之后优化成在各个数据同步处理单元运行前再进行初始化,这样子减少资源的提前或者无效的占用。数据同步处理单元运行状态监控。通过监控当前运行的数据同步处理单元的结果,将 subtask 的 stage 设置为 Paused/Stopped/Finished。如果 当前的数据同步处理单元工作已经完成,则会根据 units 来 选取下一个需要运行的数据同步处理单元,如果没有需要的数据同步处理单元,那么会将 subtask 的 stage 设置为 Finished。这里有个注意点,因为 binlog replication 单元永远不会结束,所以不会进入 Finished 的状态。如果 返回的 result 里面包含有错误信息,则会将 subtask 的 stage 设置为 Paused,并且打印具体的错误信息。如果是用户手动暂停或者停止,则会将 subtask 的 stage 设置为 Paused/Stopped。这里有个注意点,这个时候 stage=Paused 是没有错误信息的。数据同步处理单元之间的运行交接处理逻辑。部分数据同步处理单元在开始工作的时候需要满足一些前置条件,例如 binlog replication(sync)的运行需要等待 relay log 处理单元已经储存下来其开始同步需要的 binlog 文件,否则 subtask 将处于 stage=Paused 的暂停等待状态。小结本篇文章主要介绍了数据同步处理单元实现了什么功能,数据同步流程、运行逻辑,以及数据同步处理单元的 interface 设计。后续会分三篇文章详细地介绍数据同步处理单元的实现,包括:dump/load 全量同步实现binlog replication 增量同步实现relay log 实现 ...

April 11, 2019 · 1 min · jiezi

React源码系列一之createElement

前言:使用react也有二年多了,一直停留在使用层次。虽然很多时候这样是够了。但是总觉得不深入理解其背后是的实现逻辑,很难体会框架的精髓。最近会写一些相关的一些文章,来记录学习的过程。备注:react和react-dom源码版本为16.8.6 本文适合使用过REact进行开发,并有一定经验的人阅读。好了闲话少说,我们一起来看源码吧写过react知道,我们使用react编写代码都离不开webpack和babel,因为React要求我们使用的是class定义组件,并且使用了JSX语法编写HTML。浏览器是不支持JSX并且对于class的支持也不好,所以我们都是需要使用webpack的jsx-loader对jsx的语法做一个转换,并且对于ES6的语法和react的语法通过babel的babel/preset-react、babel/env和@babel/plugin-proposal-class-properties等进行转义。不熟悉怎么从头搭建react的可以看一下这篇文章好了,我们从一个最简单实例demo来看react到底做了什么1、createElement下面是我们的代码import React from “react”;import ReactDOM from “react-dom”;ReactDOM.render( <h1 style={{color:‘red’}} >11111</h1>, document.getElementById(“root”));这是页面上的效果我们现在看看在浏览器中的代码是如何实现的:react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“h1”, { style: { color: ‘red’ }}, “11111”), document.getElementById(“root”));最终经过编译后的代码是这样的,发现原本的<h1>11111</h1>变成了一个react.createElement的函数,其中原生标签的类型,内容都变成了参数传入这个函数中.这个时候我们大胆的猜测react.createElement接受三个参数,分别是元素的类型、元素的属性、子元素。好了带着我们的猜想来看一下源码。我们不难找到,源码位置在位置 ./node_modules/react/umd/react.development.js:1941function createElement(type, config, children) { var propName = void 0; // Reserved names are extracted var props = {}; var key = null; var ref = null; var self = null; var source = null; if (config != null) { if (hasValidRef(config)) { ref = config.ref; } if (hasValidKey(config)) { key = ’’ + config.key; } self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object for (propName in config) { if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) { props[propName] = config[propName]; } } } // Children can be more than one argument, and those are transferred onto // the newly allocated props object. var childrenLength = arguments.length - 2; if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { var childArray = Array(childrenLength); for (var i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } // Resolve default props if (type && type.defaultProps) { var defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } { if (key || ref) { var displayName = typeof type === ‘function’ ? type.displayName || type.name || ‘Unknown’ : type; if (key) { defineKeyPropWarningGetter(props, displayName); } if (ref) { defineRefPropWarningGetter(props, displayName); } } } return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);}首先我们来看一下它的三个参数第一个type:我们想一下这个type的可能取值有哪些?第一种就是我们上面写的原生的标签类型(例如h1、div,span等);第二种就是我们React组件了,就是这面这种Appclass App extends React.Component { static defaultProps = { text: ‘DEMO’ } render() { return (<h1>222{this.props.text}</h1>) }}第二个config:这个就是我们传递的一些属性第三个children:这个就是子元素,最开始我们猜想就三个参数,其实后面看了源码就知道这里其实不止三个。接下来我们来看看react.createElement这个函数里面会帮我们做什么事情。1、首先会初始化一些列的变量,之后会判断我们传入的元素中是否带有有效的key和ref的属性,这两个属性对于react是有特殊意义的(key是可以优化React的渲染速度的,ref是可以获取到React渲染后的真实DOM节点的),如果检测到有传入key,ref,__self和__source这4个属性值,会将其保存起来。2、接着对传入的config做处理,遍历config对象,并且剔除掉4个内置的保留属性(key,ref,__self,__source),之后重新组装新的config为props。这个RESERVED_PROPS是定义保留属性的地方。 var RESERVED_PROPS = { key: true, ref: true, __self: true, __source: true };3、之后会检测传入的参数的长度,如果childrenLength等于1的情况下,那么就代表着当前createElement的元素只有一个子元素,那么将内容赋值到props.children。那什么时候childrenLength会大于1呢?那就是当你的元素里面涉及到多个子元素的时候,那么children将会有多个传入到createElement函数中。例如: ReactDOM.render( <h1 style={{color:‘red’}} key=‘22’> <div>111</div> <div>222</div> </h1>, document.getElementById(“root”) );编译后是什么样呢? react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render( react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“h1”, { style: { color: ‘red’ }, key: “22” }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“div”, null, “111”), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“div”, null, “222”)), document.getElementById(“root”) );这个时候react.createElement拿到的arguments.length就大于3了。也就是childrenLength大于1。这个时候我们就遍历把这些子元素添加到props.children中。4、接着函数将会检测是否存在defaultProps这个参数,因为现在的是一个最简单的demo,而且传入的只是原生元素,所以没有defaultProps这个参数。那么我们来看下面的例子: import React, { Component } from “react”; import ReactDOM from “react-dom”; class App extends Component { static defaultProps = { text: ‘33333’ } render() { return (<h1>222{this.props.text}</h1>) } } ReactDOM.render( <App/>, document.getElementById(“root”) );编译后的 var App = /#PURE/ function (_Component) { _inherits(App, _Component); function App() { _classCallCheck(this, App); return _possibleConstructorReturn(this, getPrototypeOf(App).apply(this, arguments)); } createClass(App, [{ key: “render”, value: function render() { return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“h1”, null, “222”, this.props.text); } }]); return App; }(react__WEBPACK_IMPORTED_MODULE_0[“Component”]); _defineProperty(App, “defaultProps”, { text: ‘33333’ }); react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render( react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(App, null), document.getElementById(“root”) );发现传入react.createElement的是一个App的函数,class经过babel转换后会变成一个构造函数。有兴趣可以自己去看babel对于class的转换,这里就不解析转换过程,总得来说就是返回一个App的构造函数传入到react.createElement中.如果type传的东西是个对象,且type有defaultProps这个东西并且props中对应的值是undefined,那就defaultProps的值也塞props里面。这就是我们组价默认属性的由来。5、 检测key和ref是否有赋值,如果有将会执行defineKeyPropWarningGetter和defineRefPropWarningGetter两个函数。function defineKeyPropWarningGetter(props, displayName) { var warnAboutAccessingKey = function () { if (!specialPropKeyWarningShown) { specialPropKeyWarningShown = true; warningWithoutStack$1(false, ‘%s: key is not a prop. Trying to access it will result ’ + ‘in undefined being returned. If you need to access the same ’ + ‘value within the child component, you should pass it as a different ’ + ‘prop. (https://fb.me/react-special-props)', displayName); } }; warnAboutAccessingKey.isReactWarning = true; Object.defineProperty(props, ‘key’, { get: warnAboutAccessingKey, configurable: true });}function defineRefPropWarningGetter(props, displayName) { var warnAboutAccessingRef = function () { if (!specialPropRefWarningShown) { specialPropRefWarningShown = true; warningWithoutStack$1(false, ‘%s: ref is not a prop. Trying to access it will result ’ + ‘in undefined being returned. If you need to access the same ’ + ‘value within the child component, you should pass it as a different ’ + ‘prop. (https://fb.me/react-special-props)', displayName); } }; warnAboutAccessingRef.isReactWarning = true; Object.defineProperty(props, ‘ref’, { get: warnAboutAccessingRef, configurable: true });}我么可以看出这个二个方法就是给key和ref添加了警告。这个应该只是在开发环境才有其中isReactWarning就是上面判断key与ref是否有效的一个标记。6、最后将一系列组装好的数据传入ReactElement函数中。2、ReactElementvar ReactElement = function (type, key, ref, self, source, owner, props) { var element = { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: ref, props: props, _owner: owner }; { element._store = {}; Object.defineProperty(element._store, ‘validated’, { configurable: false, enumerable: false, writable: true, value: false }); Object.defineProperty(element, ‘_self’, { configurable: false, enumerable: false, writable: false, value: self }); Object.defineProperty(element, ‘_source’, { configurable: false, enumerable: false, writable: false, value: source }); if (Object.freeze) { Object.freeze(element.props); Object.freeze(element); } } return element;};其实里面非常简单,就是将传进来的值都包装在一个element对象中$$typeof:其中REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElementvar hasSymbol = typeof Symbol === ‘function’ && Symbol.for;var REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for(‘react.element’) : 0xeac7;从代码上看如果支持Symbol就会用Symbol.for方法创建一个key为react.element的symbol,否则就会返回一个0xeac7type -> tagName或者是一个函数key -> 渲染元素的keyref -> 渲染元素的refprops -> 渲染元素的props_owner -> Record the component responsible for creating this element.(记录负责创建此元素的组件,默认为null)_store -> 新的对象_store中添加了一个新的对象validated(可写入),element对象中添加了_self和_source属性(只读),最后冻结了element.props和element。这样就解释了为什么我们在子组件内修改props是没有效果的,只有在父级修改了props后子组件才会生效最后就将组装好的element对象返回了出来,提供给ReactDOM.render使用。到这有关的主要内容我们看完了。下面我们来补充一下知识点Object.freezeObject.freeze方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。const obj = { a: 1, b: 2};Object.freeze(obj);obj.a = 3; // 修改无效需要注意的是冻结中能冻结当前对象的属性,如果obj中有一个另外的对象,那么该对象还是可以修改的。所以React才会需要冻结element和element.props。if (Object.freeze) { Object.freeze(element.props); Object.freeze(element);} ...

April 6, 2019 · 4 min · jiezi

一个从基础到实战的学习机会:Go & Rust、分布式数据库系统 | PingCAP Talent Plan

TiDB 每一次微小进步都离不开广大社区小伙伴们的支持,但也有很多同学反映 TiDB 是一个非常复杂的分布式数据库系统,如果没有相关知识和经验积累,在参与之初难免会遇到各种问题。因此我们决定全面升级 PingCAP Talent Plan 计划,为社区小伙伴开放一系列关于编程语言、数据库及分布式系统的线上课程,线上考核成绩优异的小伙伴还有机会参加为期 4 周的线下课程(免费的大神辅导班哦)!什么是 PingCAP Talent PlanPingCAP Talent Plan 是 PingCAP 为 TiDB 开源社区小伙伴提供的进阶式学习计划,以循序渐进的方式,让大家深入了解并掌握 TiDB/TiKV 相关知识及实操技能。去年 11 月我们成功举办了 PingCAP Talent Plan 第一期 线下培训,如今 PingCAP Talent Plan 内容和形式全面升级,整个课程将分为线上&线下两个阶段,从语言层面开始,到数据库、分布式系统基础知识,再到 TiDB/TiKV 架构原理和源码,层层递进,最后让小伙伴们在操作实战中加深理解,掌握实操技能。课程设计整个课程分为两个方向,包括面向 SQL 引擎的 TiDB 方向,面向大规模、一致性的分布式存储的 TiKV 方向。每个方向的课程都包含线上和线下两部分,且有相应的课程作业。大家可以根据兴趣选择一个或多个方向的线上课程学习,而线下课程由于时间冲突,每人每期限选一个方向。线上课程线上课程对社区所有小伙伴们开放,时间上比较灵活。小伙伴们可以在任何一个合适的时间点开始线上学习。我们希望通过线上课程,大家能够对编程语言、数据库及分布式系统的基础知识有一定程度的了解,为学习和掌握 TiDB/TiKV 架构原理和源码打下基础。线上课程学习链接:https://docs.google.com/document/d/1UG0OHuL6l_hHWs3oyT9gA2n7LuYUfV23nmz0tRvXq2k/edit#heading=h.ywlair765ic9注意:因为本期课程设置中参考了一些其他课程(譬如 MIT 6.824),这些课程要求大家不能将自己的作业答案公布到网上,所以不推荐大家公开自己的答案。线上课程中会有对应的作业,你可以尝试解决,加深一下对课程的理解。完成线上课程后,可以将所有作业答案以附件形式发送给我们(记得打包哟~),我们评估之后会尽快给予反馈意见,并为通过考核的小伙伴授予 PingCAP Talent Plan 线上课程结业证书。邮件地址: ts-team@pingcap.com邮件主题:【PingCAP Talent Plan】申请线上课程作业评估+申请人+联系方式。正文:请简单介绍自己(包括姓名、GitHub ID、常用联系方式等)。在校学生需注明所在高校、年级和专业等信息;非在校学生需注明当前就职公司、是否能 full-time 参与 4 周线下课程等。最后附上打包好的课程作业答案,如果你刚好有意向加入我们,附上一份简历就更完美啦~:)线下课程如果你已经完成了线上课程,并且以优异的成绩通过了全部线上考核,恭喜你将有机会参与半年内我们组织的任意一期 PingCAP Talent Plan 线下课程。PingCAP Talent Plan 每年设有三期线下课程,分别在 4-5 月份,7-8 月份以及 11-12 月份,所有线下课程将在 PingCAP 北京总部进行。大家不仅可以与 PingCAP 工程师小伙伴进行面对面的深入沟通,还可以近距离地体验 PingCAP 内部的整个工作流程。PingCAP 会负责大家活动期间的食宿,大家只需要安心集中地学习就可以了:) 第二期线下课程将于 2019 年 4 月 15 日正式开始,目前线下课程学员已集结 80%,他们将聚集在 PingCAP 北京总部,开始为期 4 周的线下课程学习。在 4 月 15 日之前完成线上课程学习的小伙伴依然有机会参与第二期的线下课程哦!温馨提示:由于线下课程需要抽出 4 周左右的时间在 PingCAP 北京总部进行集中学习,所以目前主要面向社区中的学生群体。非学生群体如果能够保证 full-time 参与,也是可以报名的。当大家完成了线下课程和全部课程考核,我们会举办一个充满仪式感的结业答辩,并为顺利结业的小伙伴授予专属的结业证书。结业答辩不仅是对大家学习线下课程活动的一个检查,也是一个让大家进行自我总结和梳理的机会。对于成绩优异的同学,我们还会提供额外的 Bonus 奖励,包括但不限于:PingCAP/TiDB 全球 Meetup 的邀请函(一起看看外面更大的世界)校招/实习 Special Offer(大家一致认可你的能力,可以免面试加入 PingCAP)校招/实习绿色通道(免除笔试小作业和 1-2 轮次的技术面试)PingCAP Talent Plan 线下实战训练营的邀请函(TiDB 也可以有不一样的 Google Summer of Code 哦)年度 TiDB DevCon 邀请函(与 TiDB 社区全球开发者及用户一起享受属于大家的技术盛宴)你将获得什么?由浅入深地逐步了解分布式系统和数据库的基础知识深入了解 TiDB/TiKV 的架构设计原理和源码近距离体验和实践 PingCAP 内部的新人培养体系获得深入参与开发世界级开源项目 TiDB 的实践机会如果你来不及参与第二期 PingCAP Talent Plan 线下课程也不要着急,可以先从第二期线上课程开始学习,完成线上考核后依然有机会参与第三期的线下课程哦~未来第三期的线上课程也会在第二期的基础上进行优化,主要会结合第二期线下实战情况做细微的调整。我们在 PingCAP 等你来! ...

April 4, 2019 · 1 min · jiezi

TiKV 源码解析(五)fail-rs 介绍

作者:张博康本文为 TiKV 源码解析系列的第五篇,为大家介绍 TiKV 在测试中使用的周边库 fail-rs。fail-rs 的设计启发于 FreeBSD 的 failpoints,由 Rust 实现。通过代码或者环境变量,其允许程序在特定的地方动态地注入错误或者其他行为。在 TiKV 中通常在测试中使用 fail point 来构建异常的情况,是一个非常方便的测试工具。Fail point 需求在我们的集成测试中,都是简单的构建一个 KV 实例,然后发送请求,检查返回值和状态的改变。这样的测试可以较为完整地测试功能,但是对于一些需要精细化控制的测试就鞭长莫及了。我们当然可以通过 mock 网络层提供网络的精细模拟控制,但是对于诸如磁盘 IO、系统调度等方面的控制就没办法做到了。同时,在分布式系统中时序的关系是非常关键的,可能两个操作的执行顺行相反,就导致了迥然不同的结果。尤其对于数据库来说,保证数据的一致性是至关重要的,因此需要去做一些相关的测试。基于以上原因,我们就需要使用 fail point 来复现一些 corner case,比如模拟数据落盘特别慢、raftstore 繁忙、特殊的操作处理顺序、错误 panic 等等。基本用法示例在详细介绍之前,先举一个简单的例子给大家一个直观的认识。还是那个老生常谈的 Hello World:#[macro_use]extern crate fail;fn say_hello() { fail_point!(“before_print”); println!(“Hello World~”);}fn main() { say_hello(); fail::cfg(“before_print”, “panic”); say_hello();}运行结果如下:Hello World~thread ‘main’ panicked at ‘failpoint before_print panic’ …可以看到最终只打印出一个 Hello World~,而在打印第二个之前就 panic 了。这是因为我们在第一次打印完后才指定了这个 fail point 行为是 panic,因此第一次在 fail point 不做任何事情之后正常输出,而第二次在执行到 fail point 时就会根据配置的行为 panic 掉!Fail point 行为当然 fail point 不仅仅能注入 panic,还可以是其他的操作,并且可以按照一定的概率出现。描述行为的格式如下:[<pct>%][<cnt>*]<type>[(args…)][-><more terms>]pct:行为被执行时有百分之 pct 的机率触发cnt:行为总共能被触发的次数type:行为类型off:不做任何事return(arg):提前返回,需要 fail point 定义时指定 expr,arg 会作为字符串传给 expr 计算返回值sleep(arg):使当前线程睡眠 arg 毫秒panic(arg):使当前线程崩溃,崩溃消息为 argprint(arg):打印出 argpause:暂停当前线程,直到该 fail point 设置为其他行为为止yield:使当前线程放弃剩余时间片delay(arg):和 sleep 类似,但是让 CPU 空转 arg 毫秒args:行为的参数比如我们想在 before_print 处先 sleep 1s 然后有 1% 的机率 panic,那么就可以这么写:“sleep(1000)->1%panic"定义 fail point只需要使用宏 fail_point! 就可以在相应代码中提前定义好 fail point,而具体的行为在之后动态注入。fail_point!(“failpoint_name”);fail_point!(“failpoint_name”, || { // 指定生成自定义返回值的闭包,只有当 fail point 的行为为 return 时,才会调用该闭包并返回结果 return Error});fail_point!(“failpoint_name”, a == b, || { // 当满足条件时,fail point 才被触发 return Error})动态注入环境变量通过设置环境变量指定相应 fail point 的行为:FAILPOINTS="<failpoint_name1>=<action>;<failpoint_name2>=<action>;…“注意,在实际运行的代码需要先使用 fail::setup() 以环境变量去设置相应 fail point,否则 FAILPOINTS 并不会起作用。#[macro_use]extern crate fail;fn main() { fail::setup(); // 初始化 fail point 设置 do_fallible_work(); fail::teardown(); // 清除所有 fail point 设置,并且恢复所有被 fail point 暂停的线程}代码控制不同于环境变量方式,代码控制更加灵活,可以在程序中根据情况动态调整 fail point 的行为。这种方式主要应用于集成测试,以此可以很轻松地构建出各种异常情况。fail::cfg(“failpoint_name”, “actions”); // 设置相应的 fail point 的行为fail::remove(“failpoint_name”); // 解除相应的 fail point 的行为内部实现以下我们将以 fail-rs v0.2.1 版本代码为基础,从 API 出发来看看其背后的具体实现。fail-rs 的实现非常简单,总的来说,就是内部维护了一个全局 map,其保存着相应 fail point 所对应的行为。当程序执行到某个 fail point 时,获取并执行该全局 map 中所保存的相应的行为。全局 map 其具体定义在 FailPointRegistry。struct FailPointRegistry { registry: RwLock<HashMap<String, Arc<FailPoint>>>,}其中 FailPoint 的定义如下:struct FailPoint { pause: Mutex<bool>, pause_notifier: Condvar, actions: RwLock<Vec<Action>>, actions_str: RwLock<String>,}pause 和 pause_notifier 是用于实现线程的暂停和恢复,感兴趣的同学可以去看看代码,太过细节在此不展开了;actions_str 保存着描述行为的字符串,用于输出;而 actions 就是保存着 failpoint 的行为,包括概率、次数、以及具体行为。Action 实现了 FromStr 的 trait,可以将满足格式要求的字符串转换成 Action。这样各个 API 的操作也就显而易见了,实际上就是对于这个全局 map 的增删查改:fail::setup() 读取环境变量 FAILPOINTS 的值,以 ; 分割,解析出多个 failpoint name 和相应的 actions 并保存在 registry 中。fail::teardown() 设置 registry 中所有 fail point 对应的 actions 为空。fail::cfg(name, actions) 将 name 和对应解析出的 actions 保存在 registry 中。fail::remove(name) 设置 registry 中 name 对应的 actions 为空。而代码到执行到 fail point 的时候到底发生了什么呢,我们可以展开 fail_point! 宏定义看一下:macro_rules! fail_point { ($name:expr) => {{ $crate::eval($name, |_| { panic!(“Return is not supported for the fail point "{}"”, $name); }); }}; ($name:expr, $e:expr) => {{ if let Some(res) = $crate::eval($name, $e) { return res; } }}; ($name:expr, $cond:expr, $e:expr) => {{ if $cond { fail_point!($name, $e); } }};}现在一切都变得豁然开朗了,实际上就是对于 eval 函数的调用,当函数返回值为 Some 时则提前返回。而 eval 就是从全局 map 中获取相应的行为,在 p.eval(name) 中执行相应的动作,比如输出、等待亦或者 panic。而对于 return 行为的情况会特殊一些,在 p.eval(name) 中并不做实际的动作,而是返回 Some(arg) 并通过 .map(f) 传参给闭包产生自定义的返回值。pub fn eval<R, F: FnOnce(Option<String>) -> R>(name: &str, f: F) -> Option<R> { let p = { let registry = REGISTRY.registry.read().unwrap(); match registry.get(name) { None => return None, Some(p) => p.clone(), } }; p.eval(name).map(f)}小结至此,关于 fail-rs 背后的秘密也就清清楚楚了。关于在 TiKV 中使用 fail point 的测试详见 github.com/tikv/tikv/tree/master/tests/failpoints,大家感兴趣可以看看在 TiKV 中是如何来构建异常情况的。同时,fail-rs 计划支持 HTTP API,欢迎感兴趣的小伙伴提交 PR。 ...

April 1, 2019 · 2 min · jiezi

[译] 监测与调试 Vue.js 的响应式系统:计算属性树(Computed Tree)

原文地址:Tracing or Debugging Vue.js Reactivity: The computed tree原文作者:Michael Gallagher译文出自:掘金翻译计划本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/tracing-or-debugging-vue-js-reactivity-the-computed-tree.md译者:SHERlocked93校对者:Reaper622, hanxiansen关于 Vue 的下一个主版本,公布的很多新特性引发了激烈的讨论,但其中有一个特性引起了我的注意:更良好的可调试能力:我们可以精确地追踪到一个组件发生重渲染的触发时机和完成时机,及其原因在本文中,我们将讨论在 Vue2.x 中如何监测响应式机制,并且将演示一些和性能调优相关的代码段。为什么响应式系统相关代码需要调优如果你的项目比较大,那么你很有可能在用 Vuex。你会将 store 分割为模块,并且为了关联数据的访问一致性你甚至需要将你的状态范式化。你可能使用 Vuex 的 getter 来派生状态,事实上,你还会使用复合的派生数据,即一个 getter 会引用另一个 getter 派生的数据。在 Vue 组件中,你会使用各种分层的模式,当然也包括经常用的 slots。在这样的组件树中,肯定会有计算属性(派生出来的数据)。当这些发生的时候,从 store 中的状态到渲染的组件之间的响应式依赖关系将很难理清楚。这就是计算属性树了,如果不把它弄清楚的话,那么翻转一个看似不起眼的布尔值可能会触发一百个组件的更新。基础知识我们将学习一些响应式机制的内部工作原理。如果你还没有(比较深地)理解 Dependency 类(译者注:Dep — 为与源码一致,后文都采用 Dep)与 Watcher 类之间的关系,可以考虑学习一下内容丰富、条例清晰的高级 Vue 课程:建立一个响应式系统。在浏览器开发工具中调试过程中见过 ob 么?承认吧,当时是不是有点好奇,ob 看起来是不是像这样?这些在 subs 中的 Watcher 将会在这个响应式数据发生改变的时候更新。有时候你会在开发者工具中浏览一下这些对象,并且找到一些有用的信息,有时候找不到。有时候你会发现 Watcher 远不止 5 个。举个例子我们用一些简单的代码说明一下:JSFiddle这个例子的 store 中的状态有散列数组 users 和 currentUserId 两个属性。还有一个 getter 用来返回当前用户的信息。另外还有一个 getter 只返回状态为活跃的用户数组。然后这里有两个组件,其中有三个计算属性:validCurrentUser — 若当前用户是有效用户则为 truetotal — 引用反映当前所有活跃用户的 getter,将返回活跃用户数upperCaseName — 将用户的姓名映射为大写形式希望举的这个特别的例子,对理解我们讨论的内容有所帮助。计算属性的响应式机制是如何运转的?通常,当从一个 Dep 类实例获取到更新的通知时,响应机制将会触发对应的 Watcher 函数。当我变更一个被组件渲染所依赖的响应式数据时,将触发重渲染。但我们看看派生的数据,它的情况有点复杂。首先,计算属性的值是被缓存起来的,以便在它计算出来之后就一直可用计算后的值,只有当它的缓存失效才会被重新计算,换句话说,只在其依赖的数据发生改变时它们才会重新求值。我们再来看看之前的例子。currentUserId 状态被 currentUser 这个 getter 引用了,然后在 validCurrentUser 计算属性引用了 currentUser,validCurrentUser 又是根组件 render 函数的 v-if 表达式的一部分。这条引用链看起来不错。实际上,响应数据的存储是通过一个 Watcher 的配置选项来处理的。当我们使用组件中的 Watcher 时,API 文档中介绍了两个可选选项(deep,immediate),但其实还有一些没被文档记录的选项,我并不推介你使用这些没被记录的选项,但理解他们却很有益处。其中一个选项是 lazy,配置它之后 Watcher 将会维护一个 dirty 标志,如果依赖的响应数据已经更改但这个 Watcher 还未运行时它将为 true,也就是说,此时缓存已过时。在我们的例子中,如果 currentUserId 被改成 3。任何依赖于它且被设置了 lazy 的 Watcher 都会被标记为 dirty,但 Watcher 并没有运行。currentUser 和 validCurrentUser 都是这个状态的 lazy Watcher。根渲染函数同样会依赖于这个状态,渲染将在下一个 tick 时被触发。当渲染函数执行时,将会访问已经被标记为 dirty 的 validCurrentUser,它将重新运行它的 getter 函数,进而访问同样需要更新的 currentUser。至此,这个组件将会被正确重渲染,并且相关缓存将被更新。等等,我似乎听见你在问,为什么所有 3 个 Watcher 都是依赖于这个状态的呢?难道他们不是相互依赖的么?计算属性 watcher 有一个特性就是不仅它自身的值是响应式的,而且当计算属性的 getter 被调用时,如果当前有 Wathcer 在读取这个计算属性的话(即 Dep.target 中有值–译者),所有这个计算属性的依赖也将会被这个 Wathcer 收集起来。这种依赖收集关系链的扁平化对性能表现更优,而且也是个比较简单的解决方案。这意味着一个组件将发生更新,即使它所依赖的计算属性在重新计算后的值并没有发生变化,这种更新显然没有什么意义。其中一些逻辑可以阅读一下 watcher 类源码的优雅实现,代码量 240 行左右。那么从 ob 中我们可以得到哪些关于计算属性响应式机制的信息呢我们可以看到有哪些 Watcher 订阅(subs)了响应式数据的更新。记住,响应式机制在下面这些情景下起作用:对象数组对象的属性最后一个情景很有可能被忽略,因为在开发者工具中是无法浏览它的 Dep 类实例(译者注:ob)。因为 Dep 类是在最初响应式化的时候就被实例化的,但是并没有在这个对象中的什么地方把它记录下来。稍后我们将回头讨论这个问题,因为我将用一个小技巧来间接拿到它。然而通过观察对象和数组的 Watcher 也可以让我们收获良多,下面是一个简单的 Watcher:将示例跑起来之后打开开发者工具,它应该在页面全部渲染完成之后暂停运行。你可以输入下面的表达式,就能看到跟上面这个图一样的情况了:this.$store.state.users[2].ob.dep.subs[5]这是一个组件的渲染 Watcher,也是一个对象引用。能看到 dirty 和 lazy 这两个我之前提到过的标志位。同时,我们还可以知道它不是一个用户创建的 Watcher(译者注:user 为 false)。 有时,试图找出这个 Watcher 是哪个组件的渲染 Watcher 是困难的,因为如果这个组件没有全局注册,或者这个组件没有设置 name 属性,那么基本可以说它是匿名的。然而如果你从另一个组件引用了这个匿名组件的时候,它的 $vnode.tag 属性通常包含它被引用时所用的名称。上面的这个 Watcher 来自于被其父组件定义为 Comp 的子组件。它与 upperCaseName 计算属性相关。计算属性通常有一个在 getter 函数上指明的有意义的名称,这是因为计算属性通常被定义为对象属性。Vuex 的 getter通常计算属性会给出他们的名称及其所属的组件,但是 Vuex 的 getter 却并不如此。currentUser 这个 Watcher 看起来长这样:唯一能证明它是 Vuex 中的 getter 的线索是:它的函数体定义在 vuex.min.js 中(译者注:[[FunctionLocation]])。所以我们应该怎样获取 getter 的名称呢?在开发者工具中你通常可以访问 [[Scopes]],你可以在 [[Scopes]] 中找到它的名称,然而这并不是通过编程的方式来获取的。下面是我的一个解决方法,在创建 Vuex 的 store 之后运行:const watchers = store._vm._computedWatchers;Object.keys(watchers).forEach(key => { watchers[key].watcherName = key;});第一行可能看起来有点奇怪,但其实 Vuex 的 store 中会维护一个 Vue 的实例,来帮助实现 getter 的功能,实际上,getter 就是一个伪装起来的计算属性!现在,当我们查看 subs 数组中的 Watcher 时,我们可以通过获取 watcherName 来获取 Vuex 的 getter 的名称。对象属性的 Dep 类实例上面我提到调试响应式数据时你是看不到对象属性的 Dep 类实例。在示例中,每个 user 对象都有一个 name 属性,每个属性都包含各自的 Watcher,这些 Watcher 将会在属性发生变更时收到更新通知。尽管 Dep 实例并不能直接访问到,但是可以被监听他们的 Watcher 访问到。Watcher 保留有一份它所依赖的所有依赖项的数组。我的小技巧是给属性增加一个 Watcher,然后拿到这个 Watcher 的依赖项但是这并不简单,我可以通过 Vue 的 $watch 接口来添加一个 Watcher,但是返回的并不是 Watcher 实例。因此我需要从 Vue 实例的内部属性中获取到 Watcher 实例。const tempVm = new Vue();tempVm.$watch(() => store.state.users[2].name, () => {});const tempWatch = tempVm._watchers[0];// now pull the subs from the depstempWatch.deps.forEach(dep => dep.subs .filter(s => s !== tempWatch) .forEach(s => subs.add(s)));想把这个功能包装成一个工具函数吗?我已经把这些小的代码片段封装到了一个任何人都可以获取到的工具库中:vue-pursue。可以看看使用示例。例子中的 () => this.$store.state.users[2].name 经过 vue-pursue 处理后返回:{ “computed”: [ “currentUser”, “validCurrentUser”, “Comp.upperCaseName” ], “components”: [ “Comp” ], “unrecognised”: 1}需要注意的是,根组件将会在操作后更新,但因为根组件没有名称,所以其显示为 unrecognised。currentUser 这个 Vuex 的 getter 将会更新,且这个更新并不来源于 name 的更新。通过传递一个箭头函数给 vue-pursue,这个箭头函数所具有的所有依赖将会被将会被订阅者考虑在内,这意味着 users 和 users[2] 对象也包括在内。或者,如果我们传递 (this.$store.state.users[2], ‘name’),输出将会是:{ “computed”: [ “validCurrentUser”, “Comp.upperCaseName” ], “components”: [ “Comp” ], “unrecognised”: 1}最后一点…我需要着重强调的是,要谨慎使用任何以下划线作为开头的属性,因为这不是公共 API 的一部分,它们可能会在没有任何警告的情况下被移除。上面介绍的这个功能,一开始就没打算使用于生产环境,也没打算使用在运行时环境,这只是一个方便调试的开发者工具。最终随着 Vue3.0 的出现,这将会被更全面、更简单易用、更可靠的替代。如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。 ...

March 28, 2019 · 2 min · jiezi

从源码聊聊mybatis一次查询都经历了些什么

mybatis是一种非常流行的ORM框架,可以通过一些灵活简单的配置,大大提升我们操作数据库的效率,当然,我觉得它如此受欢迎的原因更主要的是,它的源码设计的非常简单。接下来我们就来聊聊使用mybatis做一次数据库查询操作背后都经历了什么。首先我们先上一段非常简单的代码,这是原始的JDBC方式的数据库操作。// 1. 创建数据源DataSource dataSource = getDataSource();// 2. 创建数据库连接try (Connection conn = dataSource.getConnection()) {try { conn.setAutoCommit(false); // 3. 创建Statement PreparedStatement stat = conn.prepareStatement(“select * from std_addr where id=?”); stat.setLong(1, 123456L); // 4. 执行Statement,获取结果集 ResultSet resultSet = stat.executeQuery(); // 5. 处理结果集,这一步往往是非常复杂的 processResultSet(resultSet); // 6.1 成功提交,对于查询操作,步骤6是不需要的 conn.commit();} catch (Throwable throwable) {// 6.2 失败回滚 conn.rollback();}}下面这段是mybatis连接数据库以及做同样的查询操作的代码。DataSource dataSource = getDataSource();TransactionFactory txFactory = new JdbcTransactionFactory();Environment env = new Environment(“test”, txFactory, dataSource);Configuration conf = new Configuration(env);conf.setMapUnderscoreToCamelCase(true);conf.addMapper(AddressMapper.class);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(conf);try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {AddressMapper mapper = sqlSession.getMapper(AddressMapper.class);Address addr = mapper.getById(123456L);}这是mybatis的Mapper,也非常简单@Mapperpublic interface AddressMapper {String TABLE = “std_addr”;@Select(“select * from " + TABLE + " where id=#{id}")Address getById(long id);}从上面的代码可以看出,通过mybatis查询数据库需要以下几个步骤:准备运行环境Environment,即创建数据源和事务工厂创建核心配置对象Configuration,此对象包含mybatis的配置信息(xml或者注解方式配置)创建SqlSessionFactory,用于创建数据库会话SqlSession创建SqlSession进行数据库操作下面我们从源码逐步分析mybatis在一次select查询中这几个步骤的详细情况。准备运行环境EnvironmentEnvironment有两个核心属性,dataSource和transactionFactory,下面是源码public final class Environment {private final String id;private final TransactionFactory transactionFactory;private final DataSource dataSource;}其中,dataSource用来获取数据库连接,transactionFactory用来创建事务。我们详细看一下mybatis的JdbcTransactionFactory的源码,这里可以通过数据源或者数据库连接来创建JdbcTransaction。public class JdbcTransactionFactory implements TransactionFactory {public Transaction newTransaction(Connection conn) {return new JdbcTransaction(conn);}public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {return new JdbcTransaction(ds, level, autoCommit);}}我把JdbcTransaction的源码精简了一下,大概是这个样子的。这里实际上就是把JDBC的DataSource或者一个Connection托管给了mybatis的Transaction对象,由Transaction来管理事务的提交与回滚。public class JdbcTransaction implements Transaction {protected Connection connection;protected DataSource dataSource;protected TransactionIsolationLevel level;protected boolean autoCommmit;public Connection getConnection() throws SQLException {if (connection == null) { connection = dataSource.getConnection(); if (level != null) { connection.setTransactionIsolation(level.getLevel()); } if (connection.getAutoCommit() != autoCommmit) { connection.setAutoCommit(autoCommmit); }}return connection;}public void commit() throws SQLException {if (connection != null && !connection.getAutoCommit()) { connection.commit();}}public void rollback() throws SQLException {if (connection != null && !connection.getAutoCommit()) { connection.rollback();}}}到这里,运行环境Environment已经准备完毕,我们可以从Environment中获取DataSource或者创建一个新的Transaction,从而创建一个数据库连接。创建核心配置对象ConfigurationConfiguration类非常复杂,包含很多配置信息,我们优先关注以下核心属性public class Configuration {protected Environment environment;protected boolean cacheEnabled = true;protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;// 保存着所有Mapper的动态代理对象protected final MapperRegistry mapperRegistry;// 保存着所有类型处理器,处理Java类型和JDBC类型的转换protected final TypeHandlerRegistry typeHandlerRegistry;// 保存配置的Statement信息,可以是XML或注解protected final Map<String, MappedStatement> mappedStatements;// 保存二级缓存信息protected final Map<String, Cache> caches;// 保存配置的ResultMap信息protected final Map<String, ResultMap> resultMaps;}从SqlSessionFactory的build方法可以看出,mybatis提供了两种解析配置信息的方式,分别是XMLConfigBuilder和MapperAnnotationBuilder。解析配置的过程,其实就是填充上述Configuration核心属性的过程。// 根据XML构建InputStream xmlInputStream = Resources.getResourceAsStream(“xxx.xml”);SqlSessionFactory xmlSqlSessionFactory = new SqlSessionFactoryBuilder().build(xmlInputStream);// 根据注解构建Configuration configuration = new Configuration(environment);configuration.addMapper(AddressMapper.class);SqlSessionFactory annoSqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);TypeHandlerRegistry处理Java类型和JDBC类型的映射关系,从TypeHandler的接口定义可以看出,主要是用来为PreparedStatement设置参数和从结果集中获取结果的public interface TypeHandlerRegistry<T> {void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;T getResult(ResultSet rs, String columnName) throws SQLException;T getResult(ResultSet rs, int columnIndex) throws SQLException;T getResult(CallableStatement cs, int columnIndex) throws SQLException;}总而言之,Configuration对象包含了mybatis的Statement、ResultMap、Cache等核心配置,这些配置信息是后续执行SQL操作的关键。创建SqlSessionFactory我们提供new SqlSessionFactoryBuilder().build(conf)构建了一个DefaultSqlSessionFactory,这是默认的SqlSessionFactorypublic SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);}DefaultSqlSessionFactory的核心方法有两个,代码精简过后是下面这个样子的。其实都是一个套路,通过数据源或者连接创建一个事务(上面提到的TransactionFactory创建事务的两种方式),然后创建执行器Executor,最终组合成一个DefaultSqlSession,代表着一次数据库会话,相当于一个JDBC的连接周期。private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);final Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);}private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {boolean autoCommit;try {autoCommit = connection.getAutoCommit();} catch (SQLException e) {autoCommit = true;}final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);final Transaction tx = transactionFactory.newTransaction(connection);final Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);}下面这段代码是Configuration对象创建执行器Executor的过程,默认的情况下会创建SimpleExecutor,然后在包装一层用于二级缓存的CachingExecutor,很明显Executor的设计是一个典型的装饰者模式。public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}if (cacheEnabled) {executor = new CachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);return executor;}创建SqlSession进行数据库操作进行一次数据库查询操作的步骤如下:通过DefaultSqlSessionFactory创建一个DefaultSqlSession对象SqlSession sqlSession = sqlSessionFactory.openSession(true);创建获取一个Mapper的代理对象AddressMapper mapper = sqlSession.getMapper(AddressMapper.class);DefaultSqlSession的getMapper方法参数是我们定义的Mapper接口的Class对象,最终是从Configuration对象的mapperRegistry注册表中获取这个Mapper的代理对象。下面是MapperRegistry的getMapper方法的核心代码,可见这里是通过MapperProxyFactory创建代理public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);return mapperProxyFactory.newInstance(sqlSession);}然后是MapperProxyFactory的newInstance方法,看上去是不是相当熟悉。很明显,这是一段JDK动态代理的代码,这里会返回Mapper接口的一个代理类实例。public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}调用代理对象的查询方法Address byId = mapper.getById(110114);这里实际上是调用到Mapper对应的MapperProxy,下面是MapperProxy的invoke方法的一部分。可见,这里针对我们调用的Mapper的抽象方法,创建了一个对应的代理方法MapperMethod。public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {final MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);}我精简了MapperMethod的execute方法的代码,如下所示。其实最终动态代理为我们调用了SqlSession的select方法。public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case SELECT:Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);break;}return result;}接下来的关注点在SqlSessionSqlSession的selectOne方法最终是调用的selectList,这个方法也非常简单,入参statement其实就是我们定义的Mapper中被调用的方法的全名,本例中就是x.x.AddressMapper.getById,通过statement获取对应的MappedStatement,然后交由executor执行query操作。public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {MappedStatement ms = configuration.getMappedStatement(statement);return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);}前面我们提到过默认的执行器是SimpleExecutor再装饰一层CachingExecutor,下面看看CachingExecutor的query代码,在这个方法之前会先根据SQL和参数等信息创建一个缓存的CacheKey。下面这段代码也非常明了,如果配置了Mapper级别的二级缓存(默认是没有配置的),则优先从缓存中获取,否则将调用被装饰者也就是SimpleExecutor(其实是BaseExecutor)的query方法。public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {Cache cache = ms.getCache();// cache不为空,表示当前Mapper配置了二级缓存if (cache != null) {flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) { List<E> list = (List<E>) tcm.getObject(cache, key); // 缓存未命中,查库 if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); } return list;}}return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}BaseExecutor的query方法的核心代码如下所示,这里有个一级缓存,是开启的,默认的作用域是SqlSession级别的。如果一级缓存未命中,则调用queryFromDatabase方法从数据库中查询。public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}return list;}然后将调用子类SimpleExecutor的doQuery方法,核心代码如下。public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);stmt = prepareStatement(handler, ms.getStatementLog());return handler.<E>query(stmt, resultHandler);}通过源码发现Configuration创建的是一个RoutingStatementHandler,然后根据MappedStatement的statementType属性创建一个具体的StatementHandler(三种STATEMENT、PREPARED或者CALLABLE)。终于出现了一些熟悉的东西了,这不就是JDBC的三种Statement吗。我们选择其中的PreparedStatementHandler来看一看源码,这里就很清晰了,就是调用了JDBC的PreparedStatement的execute方法,然后将结果交由ResultHandler处理。public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute();return resultSetHandler.<E> handleResultSets(ps);}从上面doQuery的代码可以看出,执行的Statement是由prepareStatement方法创建的,可以看出这里是调用了StatementHandler的prepare方法创建Statement,实际上是通过MappedStatement的SQL、参数等信息,创建了一个预编译的PrepareStatement。private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog);stmt = handler.prepare(connection, transaction.getTimeout());handler.parameterize(stmt);return stmt;}最终,这个PrepareStatement的执行结果ResultSet,会交由DefaultResultSetHandler来处理,然后根据配置中的类型、Results、返回值等信息,生成对应的实体对象。到这里我们就分析完了mybatis做一次查询操作所经历的全部流程。当然,这里面还有一些细节没有提到,比如说二级缓存、参数和结果集的解析等,这些具体的内容可能会在后续的mybatis源码解析文章中详细描述。说到最后给大家免费分享一波福利吧!我自己收集了一些Java资料,里面就包涵了一些BAT面试资料,以及一些 Java 高并发、分布式、微服务、高性能、源码分析、JVM等技术资料感兴趣的可以自己来我的Java架构进阶群,可以免费来群里下载Java资料,群号:171662117对Java技术,架构技术感兴趣的同学,欢迎加群,一起学习,相互讨论。 ...

March 28, 2019 · 4 min · jiezi

DM 源码阅读系列文章(二)整体架构介绍

作者:张学程本文为 DM 源码阅读系列文章的第二篇,第一篇文章 简单介绍了 DM 源码阅读的目的和规划,以及 DM 的源码结构以及工具链。从本篇文章开始,我们会正式开始阅读 DM 的源码。本篇文章主要介绍 DM 的整体架构,包括 DM 有哪些组件、各组件分别实现什么功能、组件之间交互的数据模型和 RPC 实现。整体架构通过上面的 DM 架构图,我们可以看出,除上下游数据库及 Prometheus 监控组件外,DM 自身有 DM-master、DM-worker 及 dmctl 这 3 个组件。其中,DM-master 负责管理和调度数据同步任务的各项操作,DM-worker 负责执行具体的数据同步任务,dmctl 提供用于管理 DM 集群与数据同步任务的各项命令。DM-masterDM-master 的入口代码在 cmd/dm-master/main.go,其中主要操作包括:调用 cfg.Parse 解析命令行参数与参数配置文件调用 log.SetLevelByString 设置进程的 log 输出级别调用 signal.Notify 注册系统 signal 通知,用于接受到指定信号时退出进程等调用 server.Start 启动 RPC server,用于响应来自 dmctl 与 DM-worker 的请求在上面的操作中,可以看出其中最关键的是步骤 4,其对应的实现代码在 dm/master/server.go 中,其核心为 Server 这个 struct,其中的主要 fields 包括:rootLis, svr:监听网络连接,分发 RPC 请求给对应的 handler。workerClients:维护集群各 DM-worker ID 到对应的 RPC client 的映射关系。taskWorkers:维护用于执行各同步(子)任务的 DM-worker ID 列表。lockKeeper:管理在协调处理 sharding DDL 时的 lock 信息。sqlOperatorHolder:管理手动 skip/replace 指定 sharding DDL 时的 SQL operator 信息。在本篇文章中,我们暂时不会关注 lockKeeper 与 sqlOperatorHolder,其具体的功能与代码实现会在后续相关文章中进行介绍。在 DM-master Server 的入口方法 Start 中:通过 net.Listen 初始化 rootLis 并用于监听 TCP 连接(借助 soheilhy/cmux,我们在同一个 port 同时提供 gRPC 与 HTTP 服务)。根据读取的配置信息(DeployMap),初始化用于连接到各 DM-worker 的 RPC client 并保存在 workerClients 中。通过 pb.RegisterMasterServer 注册 gRPC server(svr),并将该 Server 作为各 services 的 implementation。调用 m.Serve 开始提供服务。DM-master 提供的 RPC 服务包括 DM 集群管理、同步任务管理等,对应的 service 以 Protocol Buffers 格式定义在 dm/proto/dmmaster.proto 中,对应的 generated 代码在 dm/pb/dmmaster.pb.go 中。各 service 的具体实现在 dm/master/server.go 中(Server)。DM-workerDM-worker 的结构与 DM-master 类似,其入口代码在 cmd/dm-worker/main.go 中。各 RPC services 的 Protocol Buffers 格式定义在 dm/proto/dmworker.proto 中,对应的 generated 代码在 dm/pb/dmworker.pb.go 中,对应的实现代码在 dm/worker/server.go 中(Server)。DM-worker 的启动流程与 DM-master 类似,在此不再额外说明。Server 这个 struct 的主要 fields 除用于处理 RPC 的 rootLis 与 svr 外,另一个是用于管理同步任务与 relay log 的 worker(相关代码在 dm/worker/worker.go 中)。在 Worker 这个 struct 中,主要 fields 包括:subTasks:维护该 DM-worker 上的所有同步子任务信息。relayHolder:对 relay 处理单元相关操作进行简单封装,转发相关操作请求给 relay 处理单元,获取 relay 处理单元的状态信息。relayPurger:根据用户配置及相关策略,尝试定期对 relay log 进行 purge 操作。数据同步子任务管理的代码实现主要在 dm/worker/subtask.go 中, relay 处理单元管理的代码实现主要在 dm/worker/relay.go 中,对 relay log 进行 purge 操作的代码实现主要在 relay/purger pkg 中。在本篇文章中,我们暂时只关注 DM 架构相关的实现,上述各功能的具体实现将在后续的相关文章中展开介绍。Worker 的入口方法为 Start,其中的主要操作包括:通过 w.relayHolder.Start 启动 relay 处理单元,开始从上游拉取 binlog。通过 w.relayPurger.Start 启动后台 purge 线程,尝试对 relay log 进行定期 purge。其他的操作主要还包括处理 Server 转发而来的同步任务管理、relay 处理单元管理、状态信息查询等。dmctldmctl 的入口代码在 cmd/dm-ctl/main.go,其操作除参数解析与 signal 处理外,主要为调用 loop 进入命令处理循环、等待用户输入操作命令。在 loop 中,我们借助 chzyer/readline 提供命令行交互环境,读取用户输入的命令并输出命令执行结果。一个命令的处理流程为:调用 l.Readline 读取用户输入的命令判断是否需要退出命令行交互环境(exit 命令)或需要进行处理调用 ctl.Start 进行命令分发与处理dmctl 的具体命令处理实现在 dm/ctl pkg 中,入口为 dm/ctl/ctl.go 中的 Start 方法,命令的分发与参数解析借助于 spf13/cobra。命令的具体功能实现在相应的子 pkg 中:master:dmctl 与 DM-master 交互的命令,是当前 DM 推荐的命令交互方式。worker:dmctl 与 DM-worker 交互的命令,主要用于开发过程中进行 debug,当前并没有实现所有 DM-worker 支持的命令,未来可能废弃。common:多个命令依赖的通用操作及 dmctl 依赖的配置信息等。每个 dmctl 命令,其主要对应的实现包括 3 个部分:在各命令对应的实现源文件中,通过 New*Cmd 形式的方法创建 cobra.Command 对象。在 dm/ctl/ctl.go 中通过调用 rootCmd.AddCommand 添加该命令。在各命令对应的实现源文件中,通过 ***Func 形式的方法实现参数验证、RPC 调用等具体功能。任务管理调用链示例让我们用一个启动数据同步任务的操作示例来说明 DM 中的组件交互与 RPC 调用流程。用户在 dmctl 命令行交互环境中输入 start-task 命令及相应参数。dmctl 在 dm/ctl/ctl.go 的 Start 方法中进行命令分发,请求 dm/ctl/master/start_task.go 中的 startTaskFunc 处理命令。startTaskFunc 通过 cli.StartTask 调用 DM-master 上的 RPC 方法。DM-master 中的 Server.StartTask 方法(dm/master/server.go)响应来自 dmctl 的 RPC 请求。Server.Start 从 workerClients 中获取任务对应 DM-worker 的 RPC client,并通过 cli.StartSubTask 调用 DM-worker 上的 RPC 方法。DM-worker 中的 Server.StartSubTask 方法(dm/worker/server.go)响应来自 DM-master 的 RPC 请求。Server.StartSubTask 中将任务管理请求转发给 Worker.StartSubTask(dm/worker/worker.go),并将处理结果通过 RPC 返回给 DM-master。DM-master 将 DM-worker 返回的 RPC 响应重新封装后通过 RPC 返回给 dmctl。dmctl 通过 common.PrettyPrintResponse 输出命令操作的 RPC 响应。小结在本篇文章中,我们主要介绍了 DM 的各个组件的入口函数,最后以 dmctl 的 start-task 为例介绍了交互的调用流程细节。下一篇文章我们会开始介绍 DM-worker 组件内各数据同步处理单元(relay-unit, dump-unit, load-unit, sync-unit)的设计原理与具体实现。 ...

March 26, 2019 · 2 min · jiezi

深入Parcel--架构与流程篇

本篇文章是对 Parce 的源码解析,代码基本架构与执行流程,在这之前你如果对 parcel 不熟悉可以先到 Parcel官网 了解介绍下面是偷懒从官网抄下来的介绍:极速零配置Web应用打包工具极速打包Parcel 使用 worker 进程去启用多核编译。同时有文件系统缓存,即使在重启构建后也能快速再编译。将你所有的资源打包Parcel 具备开箱即用的对 JS, CSS, HTML, 文件 及更多的支持,而且不需要插件。自动转换如若有需要,Babel, PostCSS, 和 PostHTML 甚至 node_modules 包会被用于自动转换代码.零配置代码分拆使用动态 import() 语法, Parcel 将你的输出文件束(bundles)分拆,因此你只需要在初次加载时加载你所需要的代码。热模块替换Parcel 无需配置,在开发环境的时候会自动在浏览器内随着你的代码更改而去更新模块。友好的错误日志当遇到错误时,Parcel 会输出 语法高亮的代码片段,帮助你定位问题。打包工具时间browserify22.98swebpack20.71sparcel9.98sparcel - with cache2.64s打包工具我们常用的打包工具大致功能:模块化(代码的拆分, 合并, Tree-Shaking 等)编译(es6,7,8 sass typescript 等)压缩 (js, css, html包括图片的压缩)HMR (热替换)versionparcel-bundler 版本:“version”: “1.11.0"文件架构|– assets 资源目录 继承自 Asset.js|– builtins 用于最终构建|– packagers 打包|– scope-hoisting 作用域提升 Tree-Shake|– transforms 转换代码为 AST|– utils 工具|– visitors 遍历 js AST树 收集依赖等|– Asset.js 资源|– Bundle.js 用于构建 bundle 树|– Bundler.js 主目录 |– FSCache.js 缓存|– HMRServer.js HMR服务器提供 WebSocket|– Parser.js 根据文件扩展名获取对应 Asset|– Pipeline.js 多线程执行方法|– Resolver.js 解析模块路径|– Server.js 静态资源服务器|– SourceMap.js SourceMap|– cli.js cli入口 解析命令行参数|– worker.js 多线程入口流程说明Parcel是面向资源的,JavaScript,CSS,HTML 这些都是资源,并不是 webpack 中 js 是一等公民,Parcel 会自动的从入口文件开始分析这些文件 和 模块中的依赖,然后构建一个 bundle 树,并对其进行打包输出到指定目录一个简单的例子我们从一个简单的例子开始了解 parcel 内部源码与流程index.html |– index.js |– module1.js |– module2.js上面是我们例子的结构,入口为 index.html, 在 index.html 中我们用 script 标签引用了 src/index.js,在 index.js 中我们引入了2个子模块执行npx parcel index.html 或者 ./node_modules/.bin/parcel index.html,或者使用 npm scriptcli"bin”: { “parcel”: “bin/cli.js”}查看 parcel-bundler的 package.json 找到 bin/cli.js,在cli.js里又指向 ../src/cliconst program = require(‘commander’);program .command(‘serve [input…]’) // watch build … .action(bundle);program.parse(process.argv);async function bundle(main, command) { const Bundler = require(’./Bundler’); const bundler = new Bundler(main, command); if (command.name() === ‘serve’ && command.target === ‘browser’) { const server = await bundler.serve(); if (server && command.open) {…启动自动打开浏览器} } else { bundler.bundle(); }}在 cli.js 中利用 commander 解析命令行并调用 bundle 方法有 serve, watch, build 3个命令来调用 bundle 函数,执行 pracel index.html 默认为 serve,所以调用的是 bundler.serve 方法进入 Bundler.jsbundler.serveasync serve(port = 1234, https = false, host) { this.server = await Server.serve(this, port, host, https); try { await this.bundle(); } catch (e) {} return this.server; }bundler.serve 方法 调用 serveStatic 起了一个静态服务指向 最终打包的文件夹下面就是重要的 bundle 方法bundler.bundleasync bundle() { // 加载插件 设置env 启动多线程 watcher hmr await this.start(); if (isInitialBundle) { // 创建 输出目录 await fs.mkdirp(this.options.outDir); this.entryAssets = new Set(); for (let entry of this.entryFiles) { let asset = await this.resolveAsset(entry); this.buildQueue.add(asset); this.entryAssets.add(asset); } } // 打包队列中的资源 let loadedAssets = await this.buildQueue.run(); // findOrphanAssets 获取所有资源中独立的没有父Bundle的资源 let changedAssets = […this.findOrphanAssets(), …loadedAssets]; // 因为接下来要构建 Bundle 树,先对上一次的 Bundle树 进行 clear 操作 for (let asset of this.loadedAssets.values()) { asset.invalidateBundle(); } // 构建 Bundle 树 this.mainBundle = new Bundle(); for (let asset of this.entryAssets) { this.createBundleTree(asset, this.mainBundle); } // 获取新的最终打包文件的url this.bundleNameMap = this.mainBundle.getBundleNameMap( this.options.contentHash ); // 将代码中的旧文件url替换为新的 for (let asset of changedAssets) { asset.replaceBundleNames(this.bundleNameMap); } // 将改变的资源通过websocket发送到浏览器 if (this.hmr && !isInitialBundle) { this.hmr.emitUpdate(changedAssets); } // 对资源打包 this.bundleHashes = await this.mainBundle.package( this, this.bundleHashes ); // 将独立的资源删除 this.unloadOrphanedAssets(); return this.mainBundle; }我们一步步先从 this.start 看startif (this.farm) { return;}await this.loadPlugins();if (!this.options.env) { await loadEnv(Path.join(this.options.rootDir, ‘index’)); this.options.env = process.env;}if (this.options.watch) { this.watcher = new Watcher(); this.watcher.on(‘change’, this.onChange.bind(this));}if (this.options.hmr) { this.hmr = new HMRServer(); this.options.hmrPort = await this.hmr.start(this.options);}this.farm = await WorkerFarm.getShared(this.options, { workerPath: require.resolve(’./worker.js’) });start:开头的判断 防止多次执行,也就是说 this.start 只会执行一次loadPlugins 加载插件,找到 package.json 文件 dependencies, devDependencies 中 parcel-plugin-开头的插件进行调用loadEnv 加载环境变量,利用 dotenv, dotenv-expand 包将 env.development.local, .env.development, .env.local, .env 扩展至 process.envwatch 初始化监听文件并绑定 change 回调函数,内部 child_process.fork 起一个子进程,使用 chokidar 包来监听文件改变hmr 起一个服务,WebSocket 向浏览器发送更改的资源farm 初始化多进程并指定 werker 工作文件,开启多个 child_process 去解析编译资源接下来回到 bundle,isInitialBundle 是一个判断是否是第一次构建fs.mkdirp 创建输出文件夹遍历入口文件,通过 resolveAsset,内部调用 resolver 解析路径,并 getAsset 获取到对应的 asset(这里我们入口是 index.html,根据扩展名获取到的是 HTMLAsset)将 asset 添加进队列然后启动 this.buildQueue.run() 对资源从入口递归开始打包PromiseQueue这里 buildQueue 是一个 PromiseQueue 异步队列PromiseQueue 在初始化的时候传入一个回调函数 callback,内部维护一个参数队列 queue,add 往队列里 push 一个参数,run 的时候while遍历队列 callback(…queue.shift()),队列全部执行完毕 Promise 置为完成(resolved)(可以将其理解为 Promise.all)这里定义的回调函数是 processAsset,参数就是入口文件 index.html 的 HTMLAssetasync processAsset(asset, isRebuild) { if (isRebuild) { asset.invalidate(); if (this.cache) { this.cache.invalidate(asset.name); } } await this.loadAsset(asset);}processAsset 函数内先判断是否是 Rebuild ,是第一次构建,还是 watch 监听文件改变进行的重建,如果是重建则对资源的属性重置,并使其缓存失效之后调用 loadAsset 加载资源编译资源loadAssetasync loadAsset(asset) { if (asset.processed) { return; } // Mark the asset processed so we don’t load it twice asset.processed = true; // 先尝试读缓存,缓存没有在后台加载和编译 asset.startTime = Date.now(); let processed = this.cache && (await this.cache.read(asset.name)); let cacheMiss = false; if (!processed || asset.shouldInvalidate(processed.cacheData)) { processed = await this.farm.run(asset.name); cacheMiss = true; } asset.endTime = Date.now(); asset.buildTime = asset.endTime - asset.startTime; asset.id = processed.id; asset.generated = processed.generated; asset.hash = processed.hash; asset.cacheData = processed.cacheData; // 解析和加载当前资源的依赖项 let assetDeps = await Promise.all( dependencies.map(async dep => { dep.parent = asset.name; let assetDep = await this.resolveDep(asset, dep); if (assetDep) { await this.loadAsset(assetDep); } return assetDep; }) ); if (this.cache && cacheMiss) { this.cache.write(asset.name, processed); } }loadAsset 在开始有个判断防止重复编译之后去读缓存,读取失败就调用 this.farm.run 在多进程里编译资源编译完就去加载并编译依赖的文件最后如果是新的资源没有用到缓存,就重新设置一下缓存下面说一下这里吗涉及的两个东西:缓存 FSCache 和 多进程 WorkerFarmFSCacheread 读取缓存,并判断最后修改时间和缓存的修改时间write 写入缓存缓存目录为了加速读取,避免将所有的缓存文件放在一个文件夹里,parcel 将 16进制 两位数的 256 种可能创建为文件夹,这样存取缓存文件的时候,将目标文件路径 md5 加密转换为 16进制,然后截取前两位是目录,后面几位是文件名WorkerFarm在上面 start 里初始化 farm 的时候,workerPath 指向了 worker.js 文件,worker.js 里有两个函数,init 和 runWorkerFarm.getShared 初始化的时候会创建一个 new WorkerFarm ,调用 worker.js 的 init 方法,根据 cpu 获取最大的 Worker 数,并启动一半的子进程farm.run 会通知子进程执行 worker.js 的 run 方法,如果进程数没有达到最大会再次开启一个新的子进程,子进程执行完毕后将 Promise状态更改为完成worker.run -> pipeline.process -> pipeline.processAsset -> asset.processAsset.process 处理资源:async process() { if (!this.generated) { await this.loadIfNeeded(); await this.pretransform(); await this.getDependencies(); await this.transform(); this.generated = await this.generate(); } return this.generated; }将上面的代码内部扩展一下:async process() { // 已经有就不需要编译 if (!this.generated) { // 加载代码 if (this.contents == null) { this.contents = await this.load(); } // 可选。在收集依赖之前转换。 await this.pretransform(); // 将代码解析为 AST 树 if (!this.ast) { this.ast = await this.parse(this.contents); } // 收集依赖 await this.collectDependencies(); // 可选。在收集依赖之后转换。 await this.transform(); // 生成代码 this.generated = await this.generate(); } return this.generated;}// 最后处理代码async postProcess(generated) { return generated}processAsset 中调用 asset.process 生成 generated 这个generated 不一定是最终代码 ,像 html里内联的 script ,vue 的 html, js, css,都会进行二次或多次递归处理,最终调用 asset.postProcess 生成代码Asset下面说几个实现HTMLAsset:pretransform 调用 posthtml 将 html 解析为 PostHTMLTree(如果没有设置posthtmlrc之类的不会走)parse 调用 posthtml-parser 将 html 解析为 PostHTMLTreecollectDependencies 用 walk 遍历 ast,找到 script, img 的 src,link 的 href 等的地址,将其加入到依赖transform htmlnano 压缩代码generate 处理内联的 script 和 csspostProcess posthtml-render 生成 html 代码JSAsset:pretransform 调用 @babel/core 将 js 解析为 AST,处理 process.envparse 调用 @babel/parser 将 js 解析为 ASTcollectDependencies 用 babylon-walk 遍历 ast, 如 ImportDeclaration,import xx from ‘xx’ 语法,CallExpression 找到 require调用,import 被标记为 dynamic 动态导入,将这些模块加入到依赖transform 处理 readFileSync,__dirname, __filename, global等,如果没有设置scopeHoist 并存在 es6 module 就将代码转换为 commonjs,terser 压缩代码generate @babel/generator 获取 js 与 sourceMap 代码VueAsset:parse @vue/component-compiler-utils 与 vue-template-compiler 对 .vue 文件进行解析generate 对 html, js, css 处理,就像上面说到会对其分别调用 processAsset 进行二次解析postProcess component-compiler-utils 的 compileTemplate, compileStyle处理 html,css,vue-hot-reload-api HMR处理,压缩代码回到 bundle 方法:let loadedAssets = await this.buildQueue.run() 就是上面说到的PromiseQueue 和 WorkerFarm 结合起来:buildQueue.run —> processAsset -> loadAsset -> farm.run -> worker.run -> pipeline.process -> pipeline.processAsset -> asset.process,执行之后所有资源编译完毕,并返回入口资源loadedAssets就是 index.html 对应的 HTMLAsset 资源之后是 let changedAssets = […this.findOrphanAssets(), …loadedAssets] 获取到改变的资源findOrphanAssets 是从所有资源中查找没有 parentBundle 的资源,也就是独立的资源,这个 parentBundle 会在等会的构建 Bundle 树中被赋值,第一次构建都没有 parentBundle,所以这里会重复入口文件,这里的 findOrphanAssets 的作用是在第一次构建之后,文件change的时候,在这个文件 import了新的一个文件,因为新文件没有被构建过 Bundle 树,所以没有 parentBundle,这个新文件也被标记物 changeinvalidateBundle 因为接下来要构建新的树所以调用重置所有资源上一次树的属性createBundleTree 构建 Bundle 树:首先一个入口资源会被创建成一个 bundle,然后动态的 import() 会被创建成子 bundle ,这引发了代码的拆分。当不同类型的文件资源被引入,兄弟 bundle 就会被创建。例如你在 JavaScript 中引入了 CSS 文件,那它会被放置在一个与 JavaScript 文件对应的兄弟 bundle 中。如果资源被多于一个 bundle 引用,它会被提升到 bundle 树中最近的公共祖先中,这样该资源就不会被多次打包。Bundle:type:它包含的资源类型 (例如:js, css, map, …)name:bundle 的名称 (使用 entryAsset 的 Asset.generateBundleName() 生成)parentBundle:父 bundle ,入口 bundle 的父 bundle 是 nullentryAsset:bundle 的入口,用于生成名称(name)和聚拢资源(assets)assets:bundle 中所有资源的集合(Set)childBundles:所有子 bundle 的集合(Set)siblingBundles:所有兄弟 bundle 的集合(Set)siblingBundlesMap:所有兄弟 bundle 的映射 Map<String(Type: js, css, map, …), Bundle>offsets:所有 bundle 中资源位置的映射 Map<Asset, number(line number inside the bundle)> ,用于生成准确的 sourcemap 。我们的例子会被构建成:html ( index.html ) |– js ( index.js, module1.js, module2.js ) |– map ( index.js, module1.js, module2.js )module1.js 和 module2.js 被提到了与 index.js 同级,map 因为类型不同被放到了 子bundle一个复杂点的树:// 资源树index.html |– index.css |– bg.png |– index.js |– module.js// mainBundlehtml ( index.html ) |– js ( index.js, module.js ) |– map ( index.map, module.map ) |– css ( index.css ) |– js ( index.css, css-loader.js bundle-url.js ) |– map ( css-loader.js, bundle-url.js ) |– png ( bg.png )因为要对 css 热更新,所以新增了 css-loader.js, bundle-url.js 两个 jsreplaceBundleNames替换引用:生成树之后将代码中的文件引用替换为最终打包的文件名,如果是生产环境会替换为 contentHash 根据内容生成 hashhmr更新: 判断启用 hmr 并且不是第一次构建的情况,调用 hmr.emitUpdate 将改变的资源发送给浏览器Bundle.package 打包unloadOrphanedAssets 将独立的资源删除packagepackage 将generated 写入到文件有6种打包:CSSPackager,HTMLPackager,SourceMapPackager,JSPackager,JSConcatPackager,RawPackager当开启 scopeHoist 时用 JSConcatPackager 否则 JSPackager图片等资源用 RawPackager最终我们的例子被打包成 index.html, src.[hash].js, src.[hash].map 3个文件index.html 里的 js 路径被替换成立最终打包的地址我们看一下打包的 js:parcelRequire = (function (modules, cache, entry, globalName) { // Save the require from previous bundle to this closure if any var previousRequire = typeof parcelRequire === ‘function’ && parcelRequire; var nodeRequire = typeof require === ‘function’ && require; function newRequire(name, jumped) { if (!cache[name]) { localRequire.resolve = resolve; localRequire.cache = {}; var module = cache[name] = new newRequire.Module(name); modules[name][0].call(module.exports, localRequire, module, module.exports, this); } return cache[name].exports; function localRequire(x){ return newRequire(localRequire.resolve(x)); } function resolve(x){ return modules[name][4][x] || x; } } for (var i = 0; i < entry.length; i++) { newRequire(entry[i]); } // Override the current require with this new one return newRequire;})({“src/module1.js”:[function(require,module,exports) {“use strict”;},{}],“src/module2.js”:[function(require,module,exports) {“use strict”;},{}],“src/index.js”:[function(require,module,exports) {“use strict”;var _module = require("./module");var _module2 = require("./module1");var _module3 = require("./module2");console.log(_module.m);},{"./module":“src/module.js”,"./module1":“src/module1.js”,"./module2":“src/module2.js”,“fs”:“node_modules/parcel-bundler/src/builtins/_empty.js”}],{}]},{},[“node_modules/parcel-bundler/src/builtins/hmr-runtime.js”,“src/index.js”], null)//# sourceMappingURL=/src.a2b27638.map可以看到代码被拼接成了对象的形式,接收参数 module, require 用来模块导入导出,实现了 commonjs 的模块加载机制,一个更加简化版:parcelRequire = (function (modules, cache, entry, globalName) { function newRequire(id){ if(!cache[id]){ let module = cache[id] = { exports: {} } modules[id][0].call(module.exports, newRequire, module, module.exports, this); } return cache[id] } for (var i = 0; i < entry.length; i++) { newRequire(entry[i]); } return newRequire;})()代码被拼接起来:(function(modules){ //...newRequire})({ + asset.id + ‘:[function(require,module,exports) {\n’ + asset.generated.js + ‘\n},’ +’})’(function(modules){ //…newRequire})({ “src/index.js”:[function(require,module,exports){ // code }]})hmr-runtime上面打包的 js 中还有个 hmr-runtime.js 太长被我省略了hmr-runtime.js 创建一个 WebSocket 监听服务端消息修改文件触发 onChange 方法,onChange 将改变的资源 buildQueue.add 加入构建队列,重新调用 bundle 方法,打包资源,并调用 emitUpdate 通知浏览器更新当浏览器接收到服务端有新资源更新消息时新的资源就会设置或覆盖之前的模块modules[asset.id] = new Function(‘require’, ‘module’, ’exports’, asset.generated.js)对模块进行更新:function hmrAccept(id){ // dispose 回调 cached.hot._disposeCallbacks.forEach(function (cb) { cb(bundle.hotData); }); delete bundle.cache[id]; // 删除之前缓存 newRequire(id); // 重新此加载 // accept 回调 cached.hot._acceptCallbacks.forEach(function (cb) { cb(); }); // 递归父模块 进行更新 getParents(global.parcelRequire, id).some(function (id) { return hmrAccept(global.parcelRequire, id); });}至此整个打包流程结束总结parcle index.html进入 cli,启动Server调用 bundle,初始化配置(Plugins, env, HMRServer, Watcher, WorkerFarm),从入口资源开始,递归编译(babel, posthtml, postcss, vue-template-compiler等),编译完设置缓存,构建 Bundle 树,进行打包如果没有 watch 监听,结束关闭 Watcher, Worker, HMR有 watch 监听:文件修改,触发 onChange,将修改的资源加入构建队列,递归编译,查找缓存(这一步缓存的作用就提醒出来了),编译完设置新缓存,构建 Bundle 树,进行打包,将 change 的资源发送给浏览器,浏览器接收 hmr 更新资源最后通过此文章希望你对 parcel 的大致流程,打包工具原理有更深的了解了解更多请关注专栏,后续 深入Parcel 同系列文章,对 Asset,Packager,Worker,HMR,scopeHoist,FSCache,SourceMap,import 更加 详细讲解与代码实现 ...

March 18, 2019 · 7 min · jiezi

根据调试工具看Vue源码之computed(二)

回顾上回提到,computed————计算属性的缓存与Watcher这个类的dirty属性有关,那么这次我们接着来看下,dirty属性到底取决于什么情况来变化,从而对computed进行缓存。依赖收集切入正题之前,我们先来看一个问题:如果一个computed的结果是受data属性下的值影响的,那么如何去捕获因某个值变化而引起的computed的变化?答案是:依赖收集根据上面的断点,在update函数执行之前,我们注意到,有个reactiveSetter函数在它之前。我们点击右侧调用栈中的reactiveSetter,此时有一个函数特别醒目:defineReactive$$1,经过又一次的断点,我们发现它在几处都有调用:在initRender函数中调用在walk函数中调用在实际断点调试的时候,我们很容易可以知道存在这样的,同时也是与本文有关的调用顺序(从下往上):defineReactive$$1walkObserverobserveinitDatainitState…Observer类根据上边提供的调用顺序,我们重点看一下几个关键的函数:observe/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. /function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; if (hasOwn(value, ‘ob’) && value.ob instanceof Observer) { ob = value.ob; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob}光看注释我们都能知道,observe函数的作用是:为某个值创建一个observer实例,随后将这个observer实例返回,在这里起到一个对值进行筛选的作用Observer/* * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object’s property keys into getter/setters that * collect dependencies and dispatch updates. /var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, ‘ob’, this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); }};注释大意:每个被观察的对象都附属于Observer类。每次对对象的观察都会将它的 getter和setter属性覆盖,用以收集依赖以及触发更新walk && defineReactive$$1Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); }};/* * Define a reactive property on an Object. /function defineReactive$$1 ( obj, key, val, customSetter, shallow) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; / eslint-disable no-self-compare / if (newVal === value || (newVal !== newVal && value !== value)) { return } / eslint-enable no-self-compare / if (process.env.NODE_ENV !== ‘production’ && customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } });}其中,这端代码是关键:get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value },如果阅读了整段defineReactive$$1函数,那么很容易就发现,dep不过是Dep类new出来的实例,那么即使不看Dep.prototype.depend的实现,你也知道dep.depend()其实也就是在收集依赖。 另外,这段代码意味着单单在data属性下声明一个变量是不会进行依赖收集的,需要变量在程序中被调用,那么才会被收集到依赖中(其实这也是一种优化)附Dep类下的相关实现/* * A dep is an observable that can have multiple * directives subscribing to it. */var Dep = function Dep () { this.id = uid++; this.subs = [];};Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub);};Dep.prototype.removeSub = function removeSub (sub) { remove(this.subs, sub);};Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); }};Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); if (process.env.NODE_ENV !== ‘production’ && !config.async) { // subs aren’t sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); }};总结上面说了这么多未免有点乱,最后重新梳理下computed实现缓存的思路:Vue在初始化data属性时,会将data属性下相关的变量进行观察(observe),同时重新设置它的getter和setter属性,以便在其被调用时收集到它的依赖。初始化computed调用computed时判断this.dirty属性,为true时调用evaluate重新计算它的值并将this.dirty置为false,将值存在this.value 上,再调用computed则直接返回this.value当computed中依赖的值发生变化时会自动触发该值的setter属性,紧接着调用notify函数,遍历一个subs数组,触发update函数将this.dirty重置为true当computed再次被调用时,由于this.dirty已经是true,则会重新计算 ...

March 16, 2019 · 3 min · jiezi

根据调试工具看Vue源码之computed(一)

官方定义类型:{ [key: string]: Function | { get: Function, set: Function } }详细:计算属性将被混入到 Vue 实例中。所有 getter 和 setter 的 this 上下文自动地绑定为 Vue 实例…计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。注意,如果某个依赖 (比如非响应式属性) 在该实例范畴之外,则计算属性是不会被更新的。上面这几段话其实可以归纳为以下几点:computed是计算属性,会被混入到Vue实例中computed的结果会被缓存,除非依赖的响应式属性变化才会重新计算如何初始化computed?同以往一样,先新建一个Vue项目,同时加入以下代码:export default { name: ’test’, data () { return { app: 666 } }, created () { console.log(‘app proxy –>’, this.appProxy) }, computed () { appProxy () { debugger return this.app } }}F12打开调试界面,刷新后断点停在了debugger的位置,同时可以看到右边的调用栈:appProxygetevaluatecomputedGettercreated…瞥到computedGetter之后,点进去,可以看到:function createComputedGetter (key) { return function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } }}看到这里不禁一脸懵逼???? 当然,根据前面我们看源码的经验,没有思路时,直接搜索相关函数的调用位置,这里我们可以直接搜索createComputedGetter,看看它是在哪里调用的。此处忽略搜索的过程,直接给出我的结论:Vue中存在两种初始化computed的方法:在option中初始化在Vue.prototype.extend函数中初始化这两种初始化其实大同小异,我们选择在组件中写computed,自然断点就会跑到Vue.prototype.extend函数里:…if (Sub.options.computed) { initComputed$1(Sub);}…initComputed$1函数:function initComputed$1 (Comp) { // 拿到组件的computed var computed = Comp.options.computed; for (var key in computed) { // 循环遍历 defineComputed(Comp.prototype, key, computed[key]); }}显然,这句代码:defineComputed(Comp.prototype, key, computed[key])将computed挂载在了组件的原型上,下面来看下它的实现方式:defineComputed:function defineComputed ( target, key, userDef) { // 判断是否要将结果缓存下来 var shouldCache = !isServerRendering(); // 下面进行分类判断 // 对应的computed是函数的情况 if (typeof userDef === ‘function’) { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef); sharedPropertyDefinition.set = noop; } else { // 非函数的情况 sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop; sharedPropertyDefinition.set = userDef.set || noop; } if (process.env.NODE_ENV !== ‘production’ && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( (“Computed property "” + key + “" was assigned to but it has no setter.”), this ); }; } // 将sharedPropertyDefinition绑定到组件对象上 Object.defineProperty(target, key, sharedPropertyDefinition);}????感觉有点乱,最后再梳理下上边的逻辑:initComputed:执行initComputed,从Vue中拿到computed对象里所有的key值循环拿到的key值,调用defineComputed函数,把computed绑定到组件对象上defineComputed:判断是否在服务端渲染,是则computed的结果会被缓存,不是则不会缓存计算结果由于computed存在两种写法,这里也对函数跟对象的写法做了区分computed的结果缓存是如何实现的?上面我们大致梳理了下computed的初始化逻辑,现在我们回过头来再看一下官方定义,发现其中提到了计算属性会将计算结果缓存下来,那么这个计算结果到底是怎么被缓存下来的呢?回到defineComputeddefineComputed里最后将sharedPropertyDefinition绑定到组件对象上,在代码里面可以看到对sharedPropertyDefinition.get做了特殊处理,两种情况分别封装了:createComputedGettercreateGetterInvokercreateComputedGetter的实现:function createComputedGetter (key) { return function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } }}createGetterInvoker的实现:function createGetterInvoker(fn) { return function computedGetter () { return fn.call(this, this) }}可以看到,服务端渲染确实是对计算属性的结果不做缓存的,但是我们对结果是如何缓存,依旧是一脸懵逼????回到最初的断点刷新页面回到一开始我们在appProxy中打下的断点,在调用栈中有两个显眼的函数:evaluateget分别点进去,我们可以看到:evaluate实现源码:Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); this.dirty = false;};get实现源码:Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; try { value = this.getter.call(vm, vm); } catch (e) { if (this.user) { handleError(e, vm, (“getter for watcher "” + (this.expression) + “"”)); } else { throw e } } finally { // “touch” every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value); } popTarget(); this.cleanupDeps(); } return value};结合上面给出的createComputedGetter源码我们可以知道,computed的计算结果是通过Watcher.prototype.get来得到的,拿到value以后,在Wathcer.prototype.evaluate中执行了这样一行代码:…this.dirty = false;聪明的读者肯定猜到了,计算属性是否重新计算结果,肯定跟这个属性有关。接下来我们只要跟踪这个属性的变化,就可以轻松的知道计算属性的缓存原理了。 ...

March 10, 2019 · 2 min · jiezi

【React深入】React事件机制

关于React事件的疑问1.为什么要手动绑定this2.React事件和原生事件有什么区别3.React事件和原生事件的执行顺序,可以混用吗4.React事件如何解决跨浏览器兼容5.什么是合成事件下面是我阅读过源码后,将所有的执行流程总结出来的流程图,不会贴代码,如果你想阅读代码看看具体是如何实现的,可以根据流程图去源码里寻找。事件注册组件装载 / 更新。通过lastProps、nextProps判断是否新增、删除事件分别调用事件注册、卸载方法。调用EventPluginHub的enqueuePutListener进行事件存储获取document对象。根据事件名称(如onClick、onCaptureClick)判断是进行冒泡还是捕获。判断是否存在addEventListener方法,否则使用attachEvent(兼容IE)。给document注册原生事件回调为dispatchEvent(统一的事件分发机制)。事件存储EventPluginHub负责管理React合成事件的callback,它将callback存储在listenerBank中,另外还存储了负责合成事件的Plugin。EventPluginHub的putListener方法是向存储容器中增加一个listener。获取绑定事件的元素的唯一标识key。将callback根据事件类型,元素的唯一标识key存储在listenerBank中。listenerBank的结构是:listenerBank[registrationName][key]。例如:{ onClick:{ nodeid1:()=>{…} nodeid2:()=>{…} }, onChange:{ nodeid3:()=>{…} nodeid4:()=>{…} }}事件触发 / 执行这里的事件执行利用了React的批处理机制,在前一篇的【React深入】setState执行机制中已经分析过,这里不再多加分析。触发document注册原生事件的回调dispatchEvent获取到触发这个事件最深一级的元素例如下面的代码:首先会获取到this.child <div onClick={this.parentClick} ref={ref => this.parent = ref}> <div onClick={this.childClick} ref={ref => this.child = ref}> test </div> </div>遍历这个元素的所有父元素,依次对每一级元素进行处理。构造合成事件。将每一级的合成事件存储在eventQueue事件队列中。遍历eventQueue。通过isPropagationStopped判断当前事件是否执行了阻止冒泡方法。如果阻止了冒泡,停止遍历,否则通过executeDispatch执行合成事件。释放处理完成的事件。react在自己的合成事件中重写了stopPropagation方法,将isPropagationStopped设置为true,然后在遍历每一级事件的过程中根据此遍历判断是否继续执行。这就是react自己实现的冒泡机制。合成事件调用EventPluginHub的extractEvents方法。循环所有类型的EventPlugin(用来处理不同事件的工具方法)。在每个EventPlugin中根据不同的事件类型,返回不同的事件池。在事件池中取出合成事件,如果事件池是空的,那么创建一个新的。根据元素nodeid(唯一标识key)和事件类型从listenerBink中取出回调函数返回带有合成事件参数的回调函数总流程将上面的四个流程串联起来。为什么要手动绑定this通过事件触发过程的分析,dispatchEvent调用了invokeGuardedCallback方法。function invokeGuardedCallback(name, func, a) { try { func(a); } catch (x) { if (caughtError === null) { caughtError = x; } }}可见,回调函数是直接调用调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的this是undefined。这里可以使用实验性的属性初始化语法 ,也就是直接在组件声明箭头函数。箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。因此这样我们在React事件中获取到的就是组件本身了。和原生事件有什么区别React 事件使用驼峰命名,而不是全部小写。通过 JSX , 你传递一个函数作为事件处理程序,而不是一个字符串。例如,HTML:<button onclick=“activateLasers()"> Activate Lasers</button>在 React 中略有不同:<button onClick={activateLasers}> Activate Lasers</button>另一个区别是,在 React 中你不能通过返回 false 来阻止默认行为。必须明确调用 preventDefault 。由上面执行机制我们可以得出:React自己实现了一套事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,并且抹平了各个浏览器的兼容性问题。React事件和原生事件的执行顺序 componentDidMount() { this.parent.addEventListener(‘click’, (e) => { console.log(‘dom parent’); }) this.child.addEventListener(‘click’, (e) => { console.log(‘dom child’); }) document.addEventListener(‘click’, (e) => { console.log(‘document’); }) } childClick = (e) => { console.log(‘react child’); } parentClick = (e) => { console.log(‘react parent’); } render() { return ( <div onClick={this.parentClick} ref={ref => this.parent = ref}> <div onClick={this.childClick} ref={ref => this.child = ref}> test </div> </div>) }执行结果:由上面的流程我们可以理解:react的所有事件都挂载在document中当真实dom触发后冒泡到document后才会对react事件进行处理所以原生的事件会先执行然后执行react合成事件最后执行真正在document上挂载的事件react事件和原生事件可以混用吗?react事件和原生事件最好不要混用。原生事件中如果执行了stopPropagation方法,则会导致其他react事件失效。因为所有元素的事件将无法冒泡到document上。由上面的执行机制不难得出,所有的react事件都将无法被注册。合成事件、浏览器兼容 function handleClick(e) { e.preventDefault(); console.log(‘The link was clicked.’); }这里, e 是一个合成的事件。 React 根据 W3C 规范 定义了这个合成事件,所以你不需要担心跨浏览器的兼容性问题。事件处理程序将传递 SyntheticEvent 的实例,这是一个跨浏览器原生事件包装器。 它具有与浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault() ,在所有浏览器中他们工作方式都相同。每个 SyntheticEvent 对象都具有以下属性:boolean bubblesboolean cancelableDOMEventTarget currentTargetboolean defaultPreventednumber eventPhaseboolean isTrustedDOMEvent nativeEventvoid preventDefault()boolean isDefaultPrevented()void stopPropagation()boolean isPropagationStopped()DOMEventTarget targetnumber timeStampstring typeReact合成的SyntheticEvent采用了事件池,这样做可以大大节省内存,而不会频繁的创建和销毁事件对象。另外,不管在什么浏览器环境下,浏览器会将该事件类型统一创建为合成事件,从而达到了浏览器兼容的目的。推荐阅读【React深入】setState的执行机制 ...

March 5, 2019 · 1 min · jiezi

Android平台架构的介绍和源码下载

本篇文章为Android源码学习的第一章,主要讲述Android平台架构的分层,以及如何下载Android源码。Android平台架构介绍Android 是一种基于 Linux 的开放源代码软件栈,为广泛的设备和机型而创建。下图所示为 Android 平台的主要组件。从上图可以看出,Android系统大体可以分为6个层次,从下往上依次是:Linux内核层:Android 平台的基础是 Linux 内核。例如,Android Runtime (ART) 依靠 Linux 内核来执行底层功能,例如线程和低层内存管理。使用 Linux 内核可让 Android 利用主要安全功能,并且允许设备制造商为著名的内核开发硬件驱动程序。硬件抽象层 (HAL):硬件抽象层 (HAL) 提供标准界面,向更高级别的 Java API 框架显示设备硬件功能。HAL 包含多个库模块,其中每个模块都为特定类型的硬件组件实现一个界面,例如相机或蓝牙模块。当框架 API 要求访问设备硬件时,Android 系统将为该硬件组件加载库模块。Android Runtime:对于运行 Android 5.0(API 级别 21)或更高版本的设备,每个应用都在其自己的进程中运行,并且有其自己的 Android Runtime (ART) 实例。ART 编写为通过执行 DEX 文件在低内存设备上运行多个虚拟机,DEX 文件是一种专为 Android 设计的字节码格式,经过优化,使用的内存很少。编译工具链(例如 Jack)将 Java 源代码编译为 DEX 字节码,使其可在 Android 平台上运行。ART 的部分主要功能包括:预先 (AOT) 和即时 (JIT) 编译优化的垃圾回收 (GC)更好的调试支持,包括专用采样分析器、详细的诊断异常和崩溃报告,并且能够设置监视点以监控特定字段在 Android 版本 5.0(API 级别 21)之前,Dalvik 是 Android Runtime。如果您的应用在 ART 上运行效果很好,那么它应该也可在 Dalvik 上运行,但反过来不一定。Android 还包含一套核心运行时库,可提供 Java API 框架使用的 Java 编程语言大部分功能,包括一些 Java 8 语言功能。原生 C/C++ 库:许多核心 Android 系统组件和服务(例如 ART 和 HAL)构建自原生代码,需要以 C 和 C++ 编写的原生库。Android 平台提供 Java 框架 API 以向应用显示其中部分原生库的功能。例如,您可以通过 Android 框架的 Java OpenGL API 访问 OpenGL ES,以支持在应用中绘制和操作 2D 和 3D 图形。如果开发的是需要 C 或 C++ 代码的应用,可以使用 Android NDK 直接从原生代码访问某些原生平台库。Java API 框架:您可通过以 Java 语言编写的 API 使用 Android OS 的整个功能集。这些 API 形成创建 Android 应用所需的构建块,它们可简化核心模块化系统组件和服务的重复使用,包括以下组件和服务:丰富、可扩展的视图系统,可用以构建应用的 UI,包括列表、网格、文本框、按钮甚至可嵌入的网络浏览器资源管理器,用于访问非代码资源,例如本地化的字符串、图形和布局文件通知管理器,可让所有应用在状态栏中显示自定义提醒Activity 管理器,用于管理应用的生命周期,提供常见的导航返回栈内容提供程序,可让应用访问其他应用(例如“联系人”应用)中的数据或者共享其自己的数据开发者可以完全访问 Android 系统应用使用的框架 API。系统应用:Android 随附一套用于电子邮件、短信、日历、互联网浏览和联系人等的核心应用。平台随附的应用与用户可以选择安装的应用一样,没有特殊状态。因此第三方应用可成为用户的默认网络浏览器、短信 Messenger 甚至默认键盘(有一些例外,例如系统的“设置”应用)。系统应用可用作用户的应用,以及提供开发者可从其自己的应用访问的主要功能。例如,如果您的应用要发短信,您无需自己构建该功能,可以改为调用已安装的短信应用向您指定的接收者发送消息。从上图可以将Android平台划分为两层,一层是由C/C++编写的,可以称为Native层。另一层是由Java编写的,可以称为Framework层。这两层之间的联系是通过JNI进行连接。Android源码下载了解了Android平台架构之后,作为开发者学习源码,需要下载其源代码。在下载源代码之前,需要了解AOSP这个概念,AOSP是Android Open Source Project(Androi开源项目)的缩写,如果可以翻墙的话,可以按照AOSP官网 https://source.android.com/se… 这个地址上的步骤进行源码下载,如果不具备翻墙的条件,可以在清华大学开源软件镜像站 https://mirrors.tuna.tsinghua… 进行下载。步骤如下:安装 Repomkdir /binPATH=/bin:$PATHcurl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo## 如果上述 URL 不可访问,可以用下面的:## curl -sSL ‘https://gerrit-googlesource.proxy.ustclug.org/git-repo/+/master/repo?format=TEXT' |base64 -d > ~/bin/repochmod a+x ~/bin/repo建立工作目录mkdir WORKING_DIRECTORYcd WORKING_DIRECTORY初始化仓库repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest## 如果提示无法连接到 gerrit.googlesource.com,可以编辑 ~/bin/repo,把 REPO_URL 一行替换成下面的:## REPO_URL = ‘https://gerrit-googlesource.proxy.ustclug.org/git-repo'## 如果需要某个特定的Android版本,则使用repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest -b android-7.1.0_r1同步源码树(以后只需执行这条命令来同步)repo sync如果中间有网络断开的情况,则只需要执行repo sync继续同步即可。直到下载完Android源码。相关链接Android官方文档AOSP官网清华大学AOSP镜像地址中国科学技术大学开源软件镜像服务 ...

February 28, 2019 · 1 min · jiezi

分布式事务中间件 Fescar - 全局写排它锁解读

前言一般,数据库事务的隔离级别会被设置成 读已提交,已满足业务需求,这样对应在Fescar中的分支(本地)事务的隔离级别就是 读已提交,那么Fescar中对于全局事务的隔离级别又是什么呢?如果认真阅读了 分布式事务中间件Txc/Fescar-RM模块源码解读 的同学应该能推断出来:Fescar将全局事务的默认隔离定义成读未提交。对于读未提交隔离级别对业务的影响,想必大家都比较清楚,会读到脏数据,经典的就是银行转账例子,出现数据不一致的问题。而对于Fescar,如果没有采取任何其它技术手段,那会出现很严重的问题,比如:如上图所示,问最终全局事务A对资源R1应该回滚到哪种状态?很明显,如果再根据UndoLog去做回滚,就会发生严重问题:覆盖了全局事务B对资源R1的变更。那Fescar是如何解决这个问题呢?答案就是 Fescar的全局写排它锁解决方案,在全局事务A执行过程中全局事务B会因为获取不到全局锁而处于等待状态。对于Fescar的隔离级别,引用官方的一段话来作说明:全局事务的隔离性是建立在分支事务的本地隔离级别基础之上的。在数据库本地隔离级别 读已提交 或以上的前提下,Fescar 设计了由事务协调器维护的 全局写排他锁,来保证事务间的 写隔离,将全局事务默认定义在 读未提交 的隔离级别上。我们对隔离级别的共识是:绝大部分应用在 读已提交 的隔离级别下工作是没有问题的。而实际上,这当中又有绝大多数的应用场景,实际上工作在 读未提交 的隔离级别下同样没有问题。在极端场景下,应用如果需要达到全局的 读已提交,Fescar 也提供了相应的机制来达到目的。默认,Fescar 是工作在 读未提交 的隔离级别下,保证绝大多数场景的高效性。下面,本文将深入到源码层面对Fescar全局写排它锁实现方案进行解读。Fescar全局写排它锁实现方案在TC(Transaction Coordinator)模块维护,RM(Resource Manager)模块会在需要锁获取全局锁的地方请求TC模块以保证事务间的写隔离,下面就分成两个部分介绍:TC-全局写排它锁实现方案、RM-全局写排它锁使用一、TC—全局写排它锁实现方案首先看一下TC模块与外部交互的入口,下图是TC模块的main函数:上图中看出RpcServer处理通信协议相关逻辑,而对于TC模块真实处理器是DefaultCoordiantor,里面包含了所有TC对外暴露的功能,比如doGlobalBegin(全局事务创建)、doGlobalCommit(全局事务提交)、doGlobalRollback(全局事务回滚)、doBranchReport(分支事务状态上报)、doBranchRegister(分支事务注册)、doLockCheck(全局写排它锁校验)等,其中doBranchRegister、doLockCheck、doGlobalCommit就是全局写排它锁实现方案的入口。/*** 分支事务注册,在注册过程中会获取分支事务的全局锁资源*/@Overrideprotected void doBranchRegister(BranchRegisterRequest request, BranchRegisterResponse response, RpcContext rpcContext) throws TransactionException { response.setTransactionId(request.getTransactionId()); response.setBranchId(core.branchRegister(request.getBranchType(), request.getResourceId(), rpcContext.getClientId(), XID.generateXID(request.getTransactionId()), request.getLockKey()));}/*** 校验全局锁能否被获取到*/@Overrideprotected void doLockCheck(GlobalLockQueryRequest request, GlobalLockQueryResponse response, RpcContext rpcContext) throws TransactionException { response.setLockable(core.lockQuery(request.getBranchType(), request.getResourceId(), XID.generateXID(request.getTransactionId()), request.getLockKey()));}/*** 全局事务提交,会将全局事务下的所有分支事务的锁占用记录释放*/@Overrideprotected void doGlobalCommit(GlobalCommitRequest request, GlobalCommitResponse response, RpcContext rpcContext)throws TransactionException { response.setGlobalStatus(core.commit(XID.generateXID(request.getTransactionId())));}上述代码逻辑最后会被代理到DefualtCore去做执行如上图,不管是获取锁还是校验锁状态逻辑,最终都会被LockManger所接管,而LockManager的逻辑由DefaultLockManagerImpl实现,所有与全局写排它锁的设计都在DefaultLockManagerImpl中维护。首先,就先来看一下全局写排它锁的结构:private static final ConcurrentHashMap<String, ConcurrentHashMap<String, ConcurrentHashMap<Integer, Map<String, Long>>>> LOCK_MAP = new ConcurrentHashMap<~>();整体上,锁结构采用Map进行设计,前半段采用ConcurrentHashMap,后半段采用HashMap,最终其实就是做一个锁占用标记:在某个ResourceId(数据库源ID)上某个Tabel中的某个主键对应的行记录的全局写排它锁被哪个全局事务占用。下面,我们来看一下具体获取锁的源码:如上图注释,整个acquireLock逻辑还是很清晰的,对于分支事务需要的锁资源,要么是一次性全部成功获取,要么全部失败,不存在部分成功部分失败的情况。通过上面的解释,可能会有两个疑问:1. 为什么锁结构前半部分采用ConcurrentHashMap,后半部分采用HashMap?前半部分采用ConcurrentHashMap好理解:为了支持更好的并发处理;疑问的是后半部分为什么不直接采用ConcurrentHashMap,而采用HashMap呢?可能原因是因为后半部分需要去判断当前全局事务有没有占用PK对应的锁资源,是一个复合操作,即使采用ConcurrentHashMap还是避免不了要使用Synchronized加锁进行判断,还不如直接使用更轻量级的HashMap。2. 为什么BranchSession要存储持有的锁资源这个比较简单,在整个锁的结构中未体现分支事务占用了哪些锁记录,这样如果全局事务提交时,分支事务怎么去释放所占用的锁资源呢?所以在BranchSession保存了分支事务占用的锁资源。下图展示校验全局锁资源能否被获取逻辑:下图展示分支事务释放全局锁资源逻辑以上就是TC模块中全局写排它锁的实现原理:在分支事务注册时,RM会将当前分支事务所需要的锁资源一并传递过来,TC获取负责全局锁资源的获取(要么一次性全部成功,要么全部失败,不存在部分成功部分失败);在全局事务提交时,TC模块自动将全局事务下的所有分支事务持有的锁资源进行释放;同时,为减少全局写排它锁获取失败概率,TC模块对外暴露了校验锁资源能否被获取接口,RM模块可以在在适当位置加以校验,以减少分支事务注册时失败概率。二、RM-全局写排它锁使用在RM模块中,主要使用了TC模块全局锁的两个功能,一个是校验全局锁能否被获取,一个是分支事务注册去占用全局锁,全局锁释放跟RM无关,由TC模块在全局事务提交时自动释放。分支事务注册前,都会去做全局锁状态校验逻辑,以保证分支注册不会发生锁冲突。在执行Update、Insert、Delete语句时,都会在sql执行前后生成数据快照以组织成UndoLog,而生成快照的方式基本上都是采用Select…For Update形式,RM尝试校验全局锁能否被获取的逻辑就在执行该语句的执行器中:SelectForUpdateExecutor,具体如下图:基本逻辑如下:执行Select … For update语句,这样本地事务就占用了数据库对应行锁,其它本地事务由于无法抢占本地数据库行锁,进而也不会去抢占全局锁。循环掌握校验全局锁能否被获取,由于全局锁可能会被先于当前的全局事务获取,因此需要等之前的全局事务释放全局锁资源;如果这里校验能获取到全局锁,那么由于步骤1的原因,在当前本地事务结束前,其它本地事务是不会去获取全局锁的,进而保证了在当前本地事务提交前的分支事务注册不会因为全局锁冲突而失败。注:细心的同学可能会发现,对于Update、Delete语句对应的UpdateExecutor、DeleteExecutor中会因获取beforeImage而执行Select..For Update语句,进而会去校验全局锁资源状态,而对于Insert语句对应的InsertExecutor却没有相关全局锁校验逻辑,原因可能是:因为是Insert,那么对应插入行PK是新增的,全局锁资源必定未被占用,进而在本地事务提交前的分支事务注册时对应的全局锁资源肯定是能够获取得到的。接下来我们再来看看分支事务如何提交,对于分支事务中需要占用的全局锁资源如何生成和保存的。首先,在执行SQL完业务SQL后,会根据beforeImage和afterImage生成UndoLog,与此同时,当前本地事务所需要占用的全局锁资源标识也会一同生成,保存在ContentoionProxy的ConnectionContext中,如下图所示。在ContentoionProxy.commit中,分支事务注册时会将ConnectionProxy中的context内保存的需要占用的全局锁标识一同传递给TC进行全局锁的获取。以上,就是RM模块中对全局写排它锁的使用逻辑,因在真正执行获取全局锁资源前会去循环校验全局锁资源状态,保证在实际获取锁资源时不会因为锁冲突而失败,但这样其实坏处也很明显:在锁冲突比较严重时,会增加本地事务数据库锁占用时长,进而给业务接口带来一定的性能损耗。三、总结本文详细介绍了Fescar为在 读未提交 隔离级别下做到 写隔离 而实现的全局写排它锁,包括TC模块内的全局写排它锁的实现原理以及RM模块内如何对全局写排它锁的使用逻辑。在了解源码过程中,笔者也遗留了两个问题:1. 全局写排它锁数据结构保存在内存中,如果服务器重启/宕机了怎么办,即TC模块的高可用方案是什么呢?2. 一个Fescar管理的全局事务和一个非Fescar管理的本地事务之间发生锁冲突怎么办?具体问题如下图,问题是:全局事务A如何回滚?对于问题1有待继续研究;对于问题2目前已有答案,但Fescar目前暂未实现,具体就是全局事务A回滚时会报错,全局事务A内的分支事务A1回滚时会校验afterImage与当前表中对应行数据是否一致,如果一致才允许回滚,不一致则回滚失败并报警通知对应业务方,由业务方自行处理。参考Fescar官方介绍fescar锁设计和隔离级别的理解姊妹篇:分布式事务中间件TXC/Fescar—RM模块源码解读本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 27, 2019 · 1 min · jiezi

vue-router 源码阅读 - 文件结构与注册机制

前端路由是我们前端开发日常开发中经常碰到的概念,在下在日常使用中知其然也好奇着所以然,因此对 vue-router 的源码进行了一些阅读,也汲取了社区的一些文章优秀的思想,于本文记录总结作为自己思考的输出,本人水平有限,欢迎留言讨论~目标 vue-rouer 版本:3.0.2vue-router源码注释:vue-router-analysis声明:文章中源码的语法都使用 Flow,并且源码根据需要都有删节(为了不被迷糊 @_@),如果要看完整版的请进入上面的 github地址 ~ 本文是系列文章,链接见底部 0. 前备知识FlowES6语法设计模式 - 外观模式HTML5 History Api如果你对这些还没有了解的话,可以看一下本文末尾的推介阅读。1. 文件结构首先我们来看看文件结构:.├── build // 打包相关配置├── scripts // 构建相关├── dist // 构建后文件目录├── docs // 项目文档├── docs-gitbook // gitbook配置├── examples // 示例代码,调试的时候使用├── flow // Flow 声明├── src // 源码目录│ ├── components // 公共组件│ ├── history // 路由类实现│ ├── util // 相关工具库│ ├── create-matcher.js // 根据传入的配置对象创建路由映射表│ ├── create-route-map.js // 根据routes配置对象创建路由映射表 │ ├── index.js // 主入口│ └── install.js // VueRouter装载入口├── test // 测试文件└── types // TypeScript 声明我们主要关注的就是 src 中的内容。2. 入口文件2.1 rollup 出口与入口按照惯例,首先从 package.json 看起,这里有两个命令值得注意一下:{ “scripts”: { “dev:dist”: “rollup -wm -c build/rollup.dev.config.js”, “build”: “node build/build.js” }}dev:dist 用配置文件 rollup.dev.config.js 生成 dist 目录下方便开发调试相关生成文件,对应于下面的配置环境 development;build 是用 node 运行 build/build.js 生成正式的文件,包括 es6、commonjs、IIFE 方式的导出文件和压缩之后的导出文件;这两种方式都是使用 build/configs.js 这个配置文件来生成的,其中有一段语义化比较不错的代码挺有意思,跟 Vue 的配置生成文件比较类似:// vue-router/build/configs.jsmodule.exports = [{ // 打包出口 file: resolve(‘dist/vue-router.js’), format: ‘umd’, env: ‘development’ },{ file: resolve(‘dist/vue-router.min.js’), format: ‘umd’, env: ‘production’ },{ file: resolve(‘dist/vue-router.common.js’), format: ‘cjs’ },{ file: resolve(‘dist/vue-router.esm.js’), format: ’es’ }].map(genConfig)function genConfig (opts) { const config = { input: { input: resolve(‘src/index.js’), // 打包入口 plugins: […] }, output: { file: opts.file, format: opts.format, banner, name: ‘VueRouter’ } } return config}可以清晰的看到 rollup 打包的出口和入口,入口是 src/index.js 文件,而出口就是上面那部分的配置,env 是开发/生产环境标记,format 为编译输出的方式:es: ES Modules,使用ES6的模板语法输出cjs: CommonJs Module,遵循CommonJs Module规范的文件输出umd: 支持外链规范的文件输出,此文件可以直接使用script标签,其实也就是 IIFE 的方式那么正式输出是使用 build 方式,我们可以从 src/index.js 看起// src/index.jsimport { install } from ‘./install’export default class VueRouter { … }VueRouter.install = install首先这个文件导出了一个类 VueRouter,这个就是我们在 Vue 项目中引入 vue-router 的时候 Vue.use(VueRouter) 所用到的,而 Vue.use 的主要作用就是找注册插件上的 install 方法并执行,往下看最后一行,从一个 install.js 文件中导出的 install 被赋给了 VueRouter.install,这就是 Vue.use 中执行所用到的 install 方法。2.2 Vue.use可以简单看一下 Vue 中 Vue.use 这个方法是如何实现的:// vue/src/core/global-api/use.jsexport function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { // … 省略一些判重操作 const args = toArray(arguments, 1) args.unshift(this) // 注意这个this,是vue对象 if (typeof plugin.install === ‘function’) { plugin.install.apply(plugin, args) } return this }}上面可以看到 Vue.use 这个方法就是执行待注册插件上的 install 方法,并将这个插件实例保存起来。值得注意的是 install 方法执行时的第一个参数是通过 unshift 推入的 this,因此 install 执行时可以拿到 Vue 对象。对应上一小节,这里的 plugin.install 就是 VueRouter.install。3. 路由注册3.1 install接之前,看一下 install.js 里面是如何进行路由插件的注册:// vue-router/src/install.js/* vue-router 的注册过程 Vue.use(VueRouter) /export function install(Vue) { _Vue = Vue // 这样拿到 Vue 不会因为 import 带来的打包体积增加 const isDef = v => v !== undefined const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode // 至少存在一个 VueComponent 时, _parentVnode 属性才存在 // registerRouteInstance 在 src/components/view.js if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal) } } // new Vue 时或者创建新组件时,在 beforeCreate 钩子中调用 Vue.mixin({ beforeCreate() { if (isDef(this.$options.router)) { // 组件是否存在$options.router,该对象只在根组件上有 this._routerRoot = this // 这里的this是根vue实例 this._router = this.$options.router // VueRouter实例 this._router.init(this) Vue.util.defineReactive(this, ‘_route’, this._router.history.current) } else { // 组件实例才会进入,通过$parent一级级获取_routerRoot this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, destroyed() { registerInstance(this) } }) // 所有实例中 this.$router 等同于访问 this._routerRoot._router Object.defineProperty(Vue.prototype, ‘$router’, { get() { return this._routerRoot._router } }) // 所有实例中 this.$route 等同于访问 this._routerRoot._route Object.defineProperty(Vue.prototype, ‘$route’, { get() { return this._routerRoot._route } }) Vue.component(‘RouterView’, View) // 注册公共组件 router-view Vue.component(‘RouterLink’, Link) // 注册公共组件 router-link const strats = Vue.config.optionMergeStrategies strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created}install 方法主要分为几个部分:通过 Vue.mixin 在 beforeCreate、 destroyed 的时候将一些路由方法挂载到每个 vue 实例中给每个 vue 实例中挂载路由对象以保证在 methods 等地方可以通过 this.$router、this.$route 访问到相关信息注册公共组件 router-view、router-link注册路由的生命周期函数Vue.mixin 将定义的两个钩子在组件 extend 的时候合并到该组件的 options 中,从而注册到每个组件实例。看看 beforeCreate,一开始访问了一个 this.$options.router 这个是 Vue 项目里面 app.js 中的 new Vue({ router }) 这里传入的这个 router,当然也只有在 new Vue 这时才会传入 router,也就是说 this.$options.router 只有根实例上才有。这个传入 router 到底是什么呢,我们看看它的使用方式就知道了:const router = new VueRouter({ mode: ‘hash’, routes: [{ path: ‘/’, component: Home }, { path: ‘/foo’, component: Foo }, { path: ‘/bar’, component: Bar }]})new Vue({ router, template: &lt;div id="app"&gt;&lt;/div&gt;}).$mount(’#app’)可以看到这个 this.$options.router 也就是 Vue 实例中的 this._route 其实就是 VueRouter 的实例。剩下的一顿眼花缭乱的操作,是为了在每个 Vue 组件实例中都可以通过 _routerRoot 访问根 Vue 实例,其上的 _route、_router 被赋到 Vue 的原型上,这样每个 Vue 的实例中都可以通过 this.$route、this.$router 访问到挂载在根实例 _routerRoot 上的 _route、_router,后面用 Vue 上的响应式化方法 defineReactive 来将 _route 响应式化,另外在根组件上用 this._router.init() 进行了初始化操作。随便找个 Vue 组件,打印一下其上的 _routerRoot:可以看到这是 Vue 的根组件。3.2 VueRouter在之前我们已经看过 src/index.js 了,这里来详细看一下 VueRouter 这个类// vue-router/src/index.jsexport default class VueRouter { constructor(options: RouterOptions = {}) { } / install 方法会调用 init 来初始化 / init(app: any / Vue组件实例 /) { } / createMatcher 方法返回的 match 方法 / match(raw: RawLocation, current?: Route, redirectedFrom?: Location) { } / 当前路由对象 / get currentRoute() { } / 注册 beforeHooks 事件 / beforeEach(fn: Function): Function { } / 注册 resolveHooks 事件 / beforeResolve(fn: Function): Function { } / 注册 afterHooks 事件 / afterEach(fn: Function): Function { } / onReady 事件 / onReady(cb: Function, errorCb?: Function) { } / onError 事件 / onError(errorCb: Function) { } / 调用 transitionTo 跳转路由 / push(location: RawLocation, onComplete?: Function, onAbort?: Function) { } / 调用 transitionTo 跳转路由 / replace(location: RawLocation, onComplete?: Function, onAbort?: Function) { } / 跳转到指定历史记录 / go(n: number) { } / 后退 / back() { } / 前进 / forward() { } / 获取路由匹配的组件 / getMatchedComponents(to?: RawLocation | Route) { } / 根据路由对象返回浏览器路径等信息 / resolve(to: RawLocation, current?: Route, append?: boolean) { } / 动态添加路由 / addRoutes(routes: Array<RouteConfig>) { }}VueRouter 类中除了一坨实例方法之外,主要关注的是它的构造函数和初始化方法 init。首先看看构造函数,其中的 mode 代表路由创建的模式,由用户配置与应用场景决定,主要有三种 History、Hash、Abstract,前两种我们已经很熟悉了,Abstract 代表非浏览器环境,比如 Node、weex 等;this.history 主要是路由的具体实例。实现如下:// vue-router/src/index.jsexport default class VueRouter { constructor(options: RouterOptions = {}) { let mode = options.mode || ‘hash’ // 路由匹配方式,默认为hash this.fallback = mode === ‘history’ && !supportsPushState && options.fallback !== false if (this.fallback) { mode = ‘hash’ } // 如果不支持history则退化为hash if (!inBrowser) { mode = ‘abstract’ } // 非浏览器环境强制abstract,比如node中 this.mode = mode switch (mode) { // 外观模式 case ‘history’: // history 方式 this.history = new HTML5History(this, options.base) break case ‘hash’: // hash 方式 this.history = new HashHistory(this, options.base, this.fallback) break case ‘abstract’: // abstract 方式 this.history = new AbstractHistory(this, options.base) break default: … } }}init 初始化方法是在 install 时的 Vue.mixin 所注册的 beforeCreate 钩子中调用的,可以翻上去看看;调用方式是 this._router.init(this),因为是在 Vue.mixin 里调用,所以这个 this 是当前的 Vue 实例。另外初始化方法需要负责从任一个路径跳转到项目中时的路由初始化,以 Hash 模式为例,此时还没有对相关事件进行绑定,因此在第一次执行的时候就要进行事件绑定与 popstate、hashchange 事件触发,然后手动触发一次路由跳转。实现如下:// vue-router/src/index.jsexport default class VueRouter { / install 方法会调用 init 来初始化 / init(app: any / Vue组件实例 */) { const history = this.history if (history instanceof HTML5History) { // 调用 history 实例的 transitionTo 方法 history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() // 设置 popstate/hashchange 事件监听 } history.transitionTo( // 调用 history 实例的 transitionTo 方法 history.getCurrentLocation(), // 浏览器 window 地址的 hash 值 setupHashListener, // 成功回调 setupHashListener // 失败回调 ) } }}除此之外,VueRouter 还有很多实例方法,用来实现各种功能的,剩下的将在系列文章分享 本文是系列文章,随后会更新后面的部分,共同进步vue-router 源码阅读 - 文件结构与注册机制网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出推介阅读:H5 History Api - MDNECMAScript 6 入门 - 阮一峰JS 静态类型检查工具 Flow - SegmentFault 思否JS 外观模式 - SegmentFault 思否前端路由跳转基本原理 - 掘金参考:Vue.js 技术揭秘 ...

February 24, 2019 · 5 min · jiezi

由实际问题探究setState的执行机制

一.几个开发中经常会遇到的问题以下几个问题是我们在实际开发中经常会遇到的场景,下面用几个简单的示例代码来还原一下。1.setState是同步还是异步的,为什么有的时候不能立即拿到更新结果而有的时候可以?1.1 钩子函数和React合成事件中的setState现在有两个组件 componentDidMount() { console.log(‘parent componentDidMount’); } render() { return ( <div> <SetState2></SetState2> <SetState></SetState> </div> ); }组件内部放入同样的代码,并在Setstate1中的componentDidMount中放入一段同步延时代码,打印延时时间: componentWillUpdate() { console.log(‘componentWillUpdate’); } componentDidUpdate() { console.log(‘componentDidUpdate’); } componentDidMount() { console.log(‘SetState调用setState’); this.setState({ index: this.state.index + 1 }) console.log(‘state’, this.state.index); console.log(‘SetState调用setState’); this.setState({ index: this.state.index + 1 }) console.log(‘state’, this.state.index); }下面是执行结果:说明:1.调用setState不会立即更新2.所有组件使用的是同一套更新机制,当所有组件didmount后,父组件didmount,然后执行更新3.更新时会把每个组件的更新合并,每个组件只会触发一次更新的生命周期。1.2 异步函数和原生事件中的setstate?在setTimeout中调用setState(例子和在浏览器原生事件以及接口回调中执行效果相同) componentDidMount() { setTimeout(() => { console.log(‘调用setState’); this.setState({ index: this.state.index + 1 }) console.log(‘state’, this.state.index); console.log(‘调用setState’); this.setState({ index: this.state.index + 1 }) console.log(‘state’, this.state.index); }, 0); }执行结果:说明:1.在父组件didmount后执行2.调用setState同步更新2.为什么有时连续两次setState只有一次生效?分别执行以下代码: componentDidMount() { this.setState({ index: this.state.index + 1 }, () => { console.log(this.state.index); }) this.setState({ index: this.state.index + 1 }, () => { console.log(this.state.index); }) } componentDidMount() { this.setState((preState) => ({ index: preState.index + 1 }), () => { console.log(this.state.index); }) this.setState(preState => ({ index: preState.index + 1 }), () => { console.log(this.state.index); }) }执行结果:1122说明:1.直接传递对象的setstate会被合并成一次2.使用函数传递state不会被合并二.setState执行过程由于源码比较复杂,就不贴在这里了,有兴趣的可以去github上clone一份然后按照下面的流程图去走一遍。1.流程图partialState:setState传入的第一个参数,对象或函数_pendingStateQueue:当前组件等待执行更新的state队列isBatchingUpdates:react用于标识当前是否处于批量更新状态,所有组件公用dirtyComponent:当前所有处于待更新状态的组件队列transcation:react的事务机制,在被事务调用的方法外包装n个waper对象,并一次执行:waper.init、被调用方法、waper.closeFLUSH_BATCHED_UPDATES:用于执行更新的waper,只有一个close方法2.执行过程对照上面流程图的文字说明,大概可分为以下几步:1.将setState传入的partialState参数存储在当前组件实例的state暂存队列中。2.判断当前React是否处于批量更新状态,如果是,将当前组件加入待更新的组件队列中。3.如果未处于批量更新状态,将批量更新状态标识设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。4.调用事务的waper方法,遍历待更新组件队列依次执行更新。5.执行生命周期componentWillReceiveProps。6.将组件的state暂存队列中的state进行合并,获得最终要更新的state对象,并将队列置为空。7.执行生命周期componentShouldUpdate,根据返回值判断是否要继续更新。8.执行生命周期componentWillUpdate。9.执行真正的更新,render。10.执行生命周期componentDidUpdate。三.总结1.钩子函数和合成事件中:在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true。按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState。2.异步函数和原生事件中由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBranchUpdate被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。3.partialState合并机制我们看下流程中_processPendingState的代码,这个函数是用来合并state暂存队列的,最后返回一个合并后的state。 _processPendingState: function (props, context) { var inst = this._instance; var queue = this._pendingStateQueue; var replace = this._pendingReplaceState; this._pendingReplaceState = false; this._pendingStateQueue = null; if (!queue) { return inst.state; } if (replace && queue.length === 1) { return queue[0]; } var nextState = _assign({}, replace ? queue[0] : inst.state); for (var i = replace ? 1 : 0; i < queue.length; i++) { var partial = queue[i]; _assign(nextState, typeof partial === ‘function’ ? partial.call(inst, nextState, props, context) : partial); } return nextState; },我们只需要关注下面这段代码: _assign(nextState, typeof partial === ‘function’ ? partial.call(inst, nextState, props, context) : partial);如果传入的是对象,很明显会被合并成一次:Object.assign( nextState, {index: state.index+ 1}, {index: state.index+ 1})如果传入的是函数,函数的参数preState是前一次合并后的结果,所以计算结果是准确的。4.componentDidMount调用setstate在componentDidMount()中,你 可以立即调用setState()。它将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了在此情况下即使render()将会调用两次,用户也不会看到中间状态。谨慎使用这一模式,因为它常导致性能问题。在大多数情况下,你可以 在constructor()中使用赋值初始状态来代替。然而,有些情况下必须这样,比如像模态框和工具提示框。这时,你需要先测量这些DOM节点,才能渲染依赖尺寸或者位置的某些东西。以上是官方文档的说明,不推荐直接在componentDidMount直接调用setState,由上面的分析:componentDidMount本身处于一次更新中,我们又调用了一次setState,就会在未来再进行一次render,造成不必要的性能浪费,大多数情况可以设置初始值来搞定。当然在componentDidMount我们可以调用接口,再回调中去修改state,这是正确的做法。当state初始值依赖dom属性时,在componentDidMount中setState是无法避免的。5.componentWillUpdate componentDidUpdate这两个生命周期中不能调用setState。由上面的流程图很容易发现,在它们里面调用setState会造成死循环,导致程序崩溃。6.推荐使用方式在调用setState时使用函数传递state值,在回调函数中获取最新更新后的state。 ...

February 23, 2019 · 2 min · jiezi

使用Android studio阅读Android源码

1,下载源码:http://pan.baidu.com/s/1o6N86a22,合并:mac下合并,命令行执行:cat Android6_r1_*>M.tgz3.解压缩,直接双击M.tgz解压缩。4.将idegen.jar拷贝到源码的out/host/linux-x86/framework/的目录下,没有的话自己新建该目录。idegen.jar下载地址:http://download.csdn.net/deta…5.在Android源码根目录,命令行执行: . development/tools/idegen/idegen.sh等一段时间运行完成后,查看目录,多了2个文件:android.ipr和android.iml。6.导入到android studio打开Android studio,点击File > Open,选择刚刚生成的android.ipr就好了。

February 18, 2019 · 1 min · jiezi

vue源码分析系列之响应式数据(四)

前言上一节着重讲述了initComputed中的代码,以及数据是如何从computed中到视图层的,以及data修改后如何作用于computed。这一节主要记录initWatcher中的内容。正文demo修改之前的new Vue(options)的options中,我们可以观察到computed,data,但是对于watch是没法演示的,所以我们在代码中加入一段可以观察到watch初始化以及效果的代码。{ watch: { a(newV,oldV) { console.log(${oldV} -&gt; ${newV}); } }}依旧是观察a这个变量,当点击+1按钮时候,即可让a变化。入口if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch);}initWatchfunction initWatch (vm: Component, watch: Object) { for (const key in watch) { // 拿到watch中相关的处理逻辑 const handler = watch[key] // 如果是个数组,就挨个创建watcher if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { // 否则直接初始化,我们此次例子中会直接走这里 // createWatcher(vm, a, fn) createWatcher(vm, key, handler) } }}createWatcherfunction createWatcher ( vm: Component, keyOrFn: string | Function, handler: any, options?: Object) { // 这里只是对不同创建形式的标准化。 if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === ‘string’) { handler = vm[handler] } // 最终这里是真正监听值变化的地方。 // $watch(a, handle, undefined); return vm.$watch(keyOrFn, handler, options)}vm.$watchVue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true // 主要是这里创建一个观察者 // new Watcher(vm, ‘a’, handle, {user: true}) const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }new Watcherwatcher,即观察者,是我们多次提到的一个东西。这里主要强调的是watcher.get()中的内容。class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { // 一堆初始化信息 if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } else { this.deep = this.user = this.lazy = this.sync = false } this.value = this.lazy ? undefined : this.get() } get () { // 将当前watcher设为Dep.target方便后面访问a的getter时候, // 设定为a的依赖 pushTarget(this) let value const vm = this.vm try { // 求a的值。并把当前watcher加入a的依赖中。 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, getter for watcher "${this.expression}") } else { throw e } } finally { // “touch” every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }}run watcher到上一步,监听a的watcher已经初始化完毕了,当a因为点击鼠标变化时候,会触发这个watcher的变化。执行watcher的run方法,我们继续来看下run方法里的内容。class Watcher { run () { if (this.active) { // 求a的最新值。 const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // 当a变的时候,将旧值赋给oldValue。 const oldValue = this.value // this.value赋予最新的值。 this.value = value // 用户定义的watch调用时加入try,catch。 if (this.user) { try { // 执行当时传入的回调,并将新值与旧值一并传入。 this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, callback for watcher "${this.expression}") } } else { this.cb.call(this.vm, value, oldValue) } } } }}总结至此,watch,监听属性的这一部分已经完结,本质上就是对于每个监听的属性,创建一个watcher。当watcher改变时候,会触发开发者定义的回调。通过前两篇文章的学习,这篇应该算是很理解的内容。 ...

February 18, 2019 · 2 min · jiezi

vue源码分析系列之响应式数据(四)

前言上一节着重讲述了initComputed中的代码,以及数据是如何从computed中到视图层的,以及data修改后如何作用于computed。这一节主要记录initWatcher中的内容。正文demo修改之前的new Vue(options)的options中,我们可以观察到computed,data,但是对于watch是没法演示的,所以我们在代码中加入一段可以观察到watch初始化以及效果的代码。{ watch: { a(newV,oldV) { console.log(${oldV} -&gt; ${newV}); } }}依旧是观察a这个变量,当点击+1按钮时候,即可让a变化。入口if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch);}initWatchfunction initWatch (vm: Component, watch: Object) { for (const key in watch) { // 拿到watch中相关的处理逻辑 const handler = watch[key] // 如果是个数组,就挨个创建watcher if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { // 否则直接初始化,我们此次例子中会直接走这里 // createWatcher(vm, a, fn) createWatcher(vm, key, handler) } }}createWatcherfunction createWatcher ( vm: Component, keyOrFn: string | Function, handler: any, options?: Object) { // 这里只是对不同创建形式的标准化。 if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === ‘string’) { handler = vm[handler] } // 最终这里是真正监听值变化的地方。 // $watch(a, handle, undefined); return vm.$watch(keyOrFn, handler, options)}vm.$watchVue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true // 主要是这里创建一个观察者 // new Watcher(vm, ‘a’, handle, {user: true}) const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }new Watcherwatcher,即观察者,是我们多次提到的一个东西。这里主要强调的是watcher.get()中的内容。class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { // 一堆初始化信息 if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } else { this.deep = this.user = this.lazy = this.sync = false } this.value = this.lazy ? undefined : this.get() } get () { // 将当前watcher设为Dep.target方便后面访问a的getter时候, // 设定为a的依赖 pushTarget(this) let value const vm = this.vm try { // 求a的值。并把当前watcher加入a的依赖中。 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, getter for watcher "${this.expression}") } else { throw e } } finally { // “touch” every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }}run watcher到上一步,监听a的watcher已经初始化完毕了,当a因为点击鼠标变化时候,会触发这个watcher的变化。执行watcher的run方法,我们继续来看下run方法里的内容。class Watcher { run () { if (this.active) { // 求a的最新值。 const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // 当a变的时候,将旧值赋给oldValue。 const oldValue = this.value // this.value赋予最新的值。 this.value = value // 用户定义的watch调用时加入try,catch。 if (this.user) { try { // 执行当时传入的回调,并将新值与旧值一并传入。 this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, callback for watcher "${this.expression}") } } else { this.cb.call(this.vm, value, oldValue) } } } }}总结至此,watch,监听属性的这一部分已经完结,本质上就是对于每个监听的属性,创建一个watcher。当watcher改变时候,会触发开发者定义的回调。通过前两篇文章的学习,这篇应该算是很理解的内容。文章链接vue源码分析系列vue源码分析系列之debug环境搭建vue源码分析系列之入口文件分析vue源码分析系列之响应式数据(一)vue源码分析系列之响应式数据(二)vue源码分析系列之响应式数据(三) ...

February 17, 2019 · 2 min · jiezi

vue源码分析系列之响应式数据(三)

前言上一节着重讲述了initData中的代码,以及数据是如何从data中到视图层的,以及data修改后如何作用于视图。这一节主要记录initComputed中的内容。正文前情回顾在demo示例中,我们定义了一个计算属性。computed:{ total(){ return this.a + this.b }}本章节我们继续探究这个计算属性的相关流程。initComputed// initComputed(vm, opts.computed)function initComputed (vm: Component, computed: Object) { // 定义计算属性相关的watchers. const watchers = vm._computedWatchers = Object.create(null) // 是否是服务端渲染,这里赞不考虑。 const isSSR = isServerRendering() for (const key in computed) { // 获得用户定义的计算属性中的item,通常是一个方法 // 在示例程序中,仅有一个key为total的计算a+b的方法。 const userDef = computed[key] const getter = typeof userDef === ‘function’ ? userDef : userDef.get if (process.env.NODE_ENV !== ‘production’ && getter == null) { warn( Getter is missing for computed property "${key}"., vm ) } if (!isSSR) { // create internal watcher for the computed property. // 为计算属性创建一个内部的watcher。 // 其中computedWatcherOptions的值为lazy,意味着这个wacther内部的value,先不用计算。 // 只有在需要的情况下才计算,这里主要是在后期页面渲染中,生成虚拟dom的时候才会计算。 // 这时候new Watcher只是走一遍watcher的构造函数,其内部value由于 // lazy为true,先设置为了undefined.同时内部的dirty = lazy; watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions // 上文定义过,值为{lazy: true} ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. // 组件定义的属性只是定义在了组件上,这里只是把它翻译到实例中。即当前的vm对象。 if (!(key in vm)) { // 将计算属性定义到实例中。 defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== ‘production’) { if (key in vm.$data) { warn(The computed property "${key}" is already defined in data., vm) } else if (vm.$options.props && key in vm.$options.props) { warn(The computed property "${key}" is already defined as a prop., vm) } } }}defineComputedconst sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop}// defineComputed(vm, key, userDef)export function defineComputed ( target: any, key: string, userDef: Object | Function) { // 是否需要缓存。即非服务端渲染需要缓存。 // 由于本案例用的demo非服务端渲染,这里结果是true const shouldCache = !isServerRendering() if (typeof userDef === ‘function’) { // userDef = total() {…} sharedPropertyDefinition.get = shouldCache // 根据key创建计算属性的getter ? createComputedGetter(key) : userDef // 计算属性是只读的,所以设置setter为noop. sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.set = userDef.set ? userDef.set : noop } // 计算属性是只读的,所以设置值得时候需要报错提示 if (process.env.NODE_ENV !== ‘production’ && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( Computed property "${key}" was assigned to but it has no setter., this ) } } // 将组件属性-》实例属性,关键的一句,设置属性描述符 Object.defineProperty(target, key, sharedPropertyDefinition)}createComputedGetter// 根据key创建计算属性的getter// createComputedGetter(key)function createComputedGetter (key) { return function computedGetter () { // 非服务端渲染的时候,在上述的initComputed中定义了vm._computedWatchers = {},并根据组件中的设定watchers[key] = new Watcher(..),这里只是根据key取出了当时new的watcher const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // watcher.dirty表示这个值是脏值,过期了。所以需要重新计算。 // new Watcher的时候,这个total的watcher中,内部的dirty已经被置为 // dirty = lazy = true; // 那么这个值什么时候会过期,会脏呢。就是内部的依赖更新时候, // 比如我们的total依赖于this.a,this.b,当着两个值任意一个变化时候 // 我们的total就已经脏了。需要根据最新的a,b计算。 if (watcher.dirty) { // 计算watcher中的值,即value属性. watcher.evaluate() } // 将依赖添加到watcher中。 if (Dep.target) { watcher.depend() } // getter的结果就是返回getter中的值。 return watcher.value } }}initComputed小结继initComputed之后,所有组件中的computed都被赋值到了vm实例的属性上,并设置好了getter和setter。在非服务端渲染的情况下,getter会缓存计算结果。并在需要的时候,才计算。setter则是一个什么都不做的函数,预示着计算属性只能被get,不能被set。即只读的。接下来的问题就是:这个计算属性什么时候会计算,前文{lazy:true}预示着当时new Watcher得到的值是undefined。还没开始计算。计算属性是怎么知道它本身依赖于哪些属性的。以便知道其什么时候更新。vue官方文档的缓存计算结果怎么理解。接下来我们继续剖析后面的代码。用来生成vnode的render函数下次再见到这个计算属性total的时候,已是在根据el选项或者template模板中,生成的render函数,render函数上一小节也提到过。长这个样子。(function anonymous() { with (this) { return _c(‘div’, { attrs: { “id”: “demo” } }, [_c(‘div’, [_c(‘p’, [_v(“a:” + _s(a))]), _v(" “), _c(‘p’, [_v(“b: " + _s(b))]), _v(” “), _c(‘p’, [_v(“a+b: " + _s(total))]), _v(” “), _c(‘button’, { on: { “click”: addA } }, [_v(“a+1”)])])]) }})这里可以结合一下我们的html,看出一些特点。<div id=“demo”> <div> <p>a:{{a}}</p> <p>b: {{b}}</p> <p>a+b: {{total}}</p> <button @click=“addA”>a+1</button> </div></div>这里使用到计算属性的主要是这一句_v(“a+b: " + _s(total))那么对于我们来说的关键就是_s(total)。由于这个函数的with(this)中,this被设置为vm实例,所以这里就可以理解为_s(vm.total)。那么这里就会触发之前定义的sharedPropertyDefinition.get-> initComputed()-> defineComputed()-> Object.defineProperty(target, key, sharedPropertyDefinition)也就是如下的内容:coding… ...

February 17, 2019 · 3 min · jiezi

vue源码分析系列之响应式数据(一)

概述在使用vue的时候,data,computed,watch是一些经常用到的概念,那么他们是怎么实现的呢,让我们从一个小demo开始分析一下它的流程。demo演示代码片段html代码<!DOCTYPE html><html> <head> <title>demo</title> <script src="../../dist/vue.js"></script> </head> <body> <div id=“demo”> <div> <p>a:{{a}}</p> <p>b: {{b}}</p> <p>a+b: {{total}}</p> <button @click=“addA”>a+1</button> </div> </div> <script src=“app.js”></script> </body></html>js代码var demo = new Vue({ el: ‘#demo’, data: { a: 1, b: 2, }, computed:{ total() { return this.a + this.b; } }, methods: { addA() { this.a += 1; } }})简单说明这是一段简单的代码。页面中引用了data中的a,b属性,计算属性total则是求a与b的和。页面中提供一个button按钮,每点击一次会对属性a+1。total属性则会根据依赖变化,判断total值是否需要更新,并在合适的时机更新。代码初始化部分new一个Vue的时候做了什么当我们new一个vue时,实际上执行了vue的构造函数,这个构造函数内部挂载了很多方法,可以在我的上一篇文章中看到。构造函数内部调用了_init方法,那我们看看init里做了什么即可。function Vue (options) { this._init(options)}init函数Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ // 通过_isVue标识该对象不需要被做响应式处理。 vm._isVue = true // 合并构造函数上挂载的options与当前传入的options. if (options && options._isComponent) { initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // 非生产环境,包装实例本身,在后期渲染时候,做一些校验提示输出。 if (process.env.NODE_ENV !== ‘production’) { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 初始化生命周期相关 initLifecycle(vm) // 初始化事件相关 initEvents(vm) // 初始化渲染相关 initRender(vm) // 这里调用beforeCreate钩子 callHook(vm, ‘beforeCreate’) // inject/provide相关处理 initInjections(vm) // resolve injections before data/props // 初始化data、props以及computed,watch等。 initState(vm) initProvide(vm) // resolve provide after data/props // 调用created钩子 callHook(vm, ‘created’) if (vm.$options.el) { // 挂载组件到页面上的 vm.$mount(vm.$options.el) }}这篇文章讲述的内容,需要我们着重关注一下initState函数与vm.$mount中渲染部分的内容。initState函数export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options //初始化props if (opts.props) initProps(vm, opts.props) // 初始化methods if (opts.methods) initMethods(vm, opts.methods) // 初始化data if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } // 初始化计算属性 if (opts.computed) initComputed(vm, opts.computed) // 初始化watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }}接下里的几篇我们将围绕着initData,initComputed,initWatch函数,分别展开,探究其内部做了什么。相关章节vue源码分析系列之响应式数据(二) ...

February 14, 2019 · 2 min · jiezi

vue源码分析系列之响应式数据(二)

前言接着上一篇的初始化部分,我们细看initData中做了什么。正文coding….相关章节vue源码分析系列之响应式数据(一)

February 14, 2019 · 1 min · jiezi

根据调试工具看Vue源码之生命周期(一)

由于工作中经常使用chrome调试工具来定位问题,觉着这东西真的挺好用。突然有一天受到启发,想着:“我学习源码是否也可以通过调试工具呢?” 因此,诞生了这篇文章来记录我的一些学习成果,后续应该会写成一个系列。阅读源码的一些常见方式这里列举一些阅读源码的一些常见方式:直接从github上查看某一个版本的源码,针对某些功能的实现进行剖析从第一个commit开始看上面是我所知的一些阅读源码的常见方式,但是以上两种方式,无论是哪一种,都需要对flow稍微熟悉一些,不然看着多别扭(当然啦,如果你直接下载源码到本地转码以后慢慢看,那只能当我没说);同时,从第一个commit开始看的话未免太消磨时间,相信在座的各位都不是很愿意。 那使用chrome调试工具看源码都有啥优点呢?chrome调试工具里的代码都是经过转码的,阅读成本相对较低打下断点之后可以清晰的看到某个功能的实现步骤,跟直接阅读源码相比,不用来回切换文件夹,从而能更加集中自己的注意力进入正题说起Vue,首先必不可少的就是讲Vue的生命周期了,不仅是面试的时候经常会被问到这个问题,开发的时候也经常会在生命周期这里遇到一些坑执行顺序Vue 中常见的生命周期及对应顺序: beforeCreate —> created —> beforeMount —> mounted —> beforeDestroy —> destroyed,官网有张则很清晰的描绘了这个过程: 接下来让我们在上面对应的钩子函数里打下一个断点 我们可以发现,beforeCreate —> created —> beforeMount —> mounted 这几个钩子函数都是挨个执行的,文档诚不我欺! 但是细心的同学可以发现,beforeCreate这个钩子函数居然执行了两次!为什么?是Vue的bug吗?显然不是! 通过两次执行,我们可以看到两次vm对象是由不同的构造函数new出来的,一个是Vue,另外一个则是VueComponent通过观察右边的调用堆栈可以发现的确是存在VueComponent这个构造函数的,具体是用来干嘛的我们先不深究。怎么去定位到这个问题呢?首先先在VueComponent这里打下一个断点,重新刷新浏览器并查看右边的调用堆栈 原来,两次beforeCreate钩子函数分别是Vue本身和VueRouter执行的(终于破案了…)除了这几个钩子函数以外,还有beforeDestroy跟destroyed这两个钩子,顾名思义,应该是页面销毁的时候才会执行,所以我们在上面打了断点进去也没有看到这两个钩子触发了。 另外还有beforeUpdate跟updated两个钩子,字面意思就是“更新前”与“更新后”嘛。同样,上面的断点也没有在这里停下来。为了验证它们之间的执行顺序,我在这个项目里面加了几句代码:data () { return { lists: [ 1, 2, 3, 4 ] }},methods: { handleClick () { let len = this.lists.length this.lists.push(this.lists[len - 1] + 1) }}然后刷新页面,点击这个按钮可以看到执行了beforeUpdate钩子,放开这个断点以后,页面数据刷新,断点停在了updated这个钩子函数中。 最后,我们回过头来再看这张图片,是不是对整个生命周期的流程清晰多了呢?未完待续…

February 4, 2019 · 1 min · jiezi

vue-router源码解析(四)路由匹配规则

前面我们讲过,在使用 vue-router 的时候,主要有以下几个步骤:// 1. 安装 插件Vue.use(VueRouter);// 2. 创建router对象const router = new VueRouter({ routes // 路由列表 eg: [{ path: ‘/foo’, component: Foo }]});// 3. 挂载routerconst app = new Vue({ router}).$mount(’#app’);然后再进行路由跳转的时候,我们会有以下几种使用方式 。 详细使用请查看官方文档// 字符串router.push(‘home’);// 对象router.push({ path: ‘home’ });// 命名的路由router.push({ name: ‘user’, params: { userId: ‘123’ } });// 带查询参数,变成 /register?plan=privaterouter.push({ path: ‘register’, query: { plan: ‘private’ } });那么,你有没有想过, push 进去的对象是如何与我们之前定义的 routes 相对应的 ??接下来,我们一步步来进行探个究竟吧!匹配路由入口之前我们说过 push 方法的具体实现, 里面主要是通过 transitionTo 来实现路由匹配并切换 // src/history/hash.js // 跳转到 push(location: RawLocation, onComplete ? : Function, onAbort ? : Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) }所以我们来看看 transitionTo// src/history/base.js// 切换路由 transitionTo(location: RawLocation, onComplete ? : Function, onAbort ? : Function) { // 匹配路由 // 根据路径获取到匹配的路径 const route = this.router.match(location, this.current) // 跳转路由 this.confirmTransition(route, () => { // …more }, err => { // …more }) }这里看到, transitionTo 主要处理两件事匹配路由将匹配到的路由作为参数,调用 confirmTransition 进行跳转我们来看看具体如何匹配路由的 , 这里直接调用了匹配器的 match 方法// 获取匹配的路由对象 match( raw: RawLocation, current ? : Route, redirectedFrom ? : Location ): Route { // 直接调用match方法 return this.matcher.match(raw, current, redirectedFrom) }匹配器export default class VueRouter { constructor() { // …more // 创建匹配器 this.matcher = createMatcher(options.routes || [], this); // …more }}创建匹配器在 VueRouter 实例化的时候, 会通过我们之前设置的 routers , 以及 createMatcher 创建一个匹配器, 匹配器包含一个 match 方法,用于匹配路由// 文件位置: src/create-matcher.js// 创建匹配export function createMatcher( routes: Array<RouteConfig>, router: VueRouter): Matcher { // 创建 路由映射的关系 ,返回对应的关系 const { pathList, pathMap, nameMap } = createRouteMap(routes); // 添加 路由 function addRoutes(routes) { createRouteMap(routes, pathList, pathMap, nameMap); } // 匹配规则 function match( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { // 路径 const location = normalizeLocation(raw, currentRoute, false, router); const { name } = location; // 如果存在 name if (name) { // 找出匹配的 const record = nameMap[name]; if (!record) return _createRoute(null, location); // …more if (record) { location.path = fillParams( record.path, location.params, named route "${name}" ); return _createRoute(record, location, redirectedFrom); } } else if (location.path) { // 根据路径寻找匹配的路由 location.params = {}; for (let i = 0; i < pathList.length; i++) { const path = pathList[i]; const record = pathMap[path]; // 查找匹配的路由 if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom); } } } // no match return _createRoute(null, location); } // 创建路由 function _createRoute( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { // …more return createRoute(record, location, redirectedFrom, router); } return { match, addRoutes };}获取路由映射关系 createRouteMapexport function createRouteMap( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord>): { pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>} { // the path list is used to control path matching priority // 数组,包括所有的 path const pathList: Array<string> = oldPathList || []; // $flow-disable-line // 对象 , key 为 path , 值为 路由对象 const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null); // $flow-disable-line // 对象 , key 为 name , 值为 路由对象 const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null); // 循环遍历 routes ,添加路由记录 routes.forEach(route => { addRouteRecord(pathList, pathMap, nameMap, route); }); // ensure wildcard routes are always at the end // 确保 * 匹配符放到最后面 for (let i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === ‘*’) { pathList.push(pathList.splice(i, 1)[0]); l–; i–; } } return { pathList, pathMap, nameMap };}addRouteRecord 主要完成了几项工作生成 normalizedPath 复制给 record.path通过 compileRouteRegex 生成 record.regex , 用于后期的路由匹配将 record 分别加入到 pathMap 、 pathList、nameMap 里面// 添加路由记录对象function addRouteRecord( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string) { const { path, name } = route; // … const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}; const normalizedPath = normalizePath( path, parent, pathToRegexpOptions.strict ); if (typeof route.caseSensitive === ‘boolean’) { pathToRegexpOptions.sensitive = route.caseSensitive; } // 路由记录对象 const record: RouteRecord = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } }; // … if (!pathMap[record.path]) { pathList.push(record.path); pathMap[record.path] = record; } if (name) { if (!nameMap[name]) { nameMap[name] = record; } // … }}创建路由对象// 文件位置: src/util/route.js// 创建路由对象export function createRoute( record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter): Route { const stringifyQuery = router && router.options.stringifyQuery; // 请求参数 let query: any = location.query || {}; try { query = clone(query); } catch (e) {} // 生成路由对象 const route: Route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || ‘/’, hash: location.hash || ‘’, query, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery), matched: record ? formatMatch(record) : [] }; if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery); } // 冻结路由对象,防止篡改 return Object.freeze(route);}createRoute 生成的对象,便是是我们经常用到的路由对象。 当前激活的路由信息对象则是this.$route路由匹配规则路由是否匹配 , 主要是通过 path-to-regexp , 来创建一个正则表达式 , 然后 , 通过这个正则来检查是否匹配import Regexp from ‘path-to-regexp’;// …more// 编译路径,返回一个正则function compileRouteRegex( path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp { const regex = Regexp(path, [], pathToRegexpOptions); // …more return regex;}关于 path-to-regexp ,这里主要讲几个例子。import Regexp from ‘path-to-regexp’;// 假如我们页面 path 为 /aboutlet reg = Regexp(’/about’, [], {}); // reg ==> /^/about(?:/(?=$))?$/i’/about’.match(reg); // ["/about", index: 0, input: “/about”, groups: undefined]’/home’.match(reg); // null// 假如我们页面 path 为 /about/:idlet reg = Regexp(’/about/:id’, [], {}); // reg ==> /^/about/((?:[^/]+?))(?:/(?=$))?$/i’/about’.match(reg); // null’/about/123’.match(reg); //["/about/123", “123”, index: 0, input: “/about/123”, groups: undefined]具体文档可参照这里 : path-to-regexp最后通过正则检查路由是否匹配, 匹配结果非 null 则表示路由符合预先设定的规则// 匹配路由规则function matchRoute(regex: RouteRegExp, path: string, params: Object): boolean { const m = path.match(regex); if (!m) { return false; } else if (!params) { // 没参数直接返回true return true; } // …more, 这里对参数做了一些处理 return true;}总结最后,对路由匹配做一个总结 。 路由匹配具体的步骤有:实例化的时候,创建匹配器 ,并生成路由的映射关系 。匹配器中包含 match 方法push 的时候,调用到 match 方法match 方法里面,从路由的映射关系里面,通过编译好的正则来判定是否匹配,返回最终匹配的路由对象transitionTo 中,拿到匹配的路由对象,进行路由跳转其他系列文章列表个人博客 ...

January 29, 2019 · 5 min · jiezi

TiKV 源码解析系列文章(一)序

作者:唐刘TiKV 是一个支持事务的分布式 Key-Value 数据库,有很多社区开发者基于 TiKV 来开发自己的应用,譬如 titan、tidis。尤其是在 TiKV 成为 CNCF 的 Sandbox 项目之后,吸引了越来越多开发者的目光,很多同学都想参与到 TiKV 的研发中来。这时候,就会遇到两个比较大的拦路虎:Rust 语言:众所周知,TiKV 是使用 Rust 语言来进行开发的,而 Rust 语言的学习难度相对较高,有些人认为其学习曲线大于 C++,所以很多同学在这一步就直接放弃了。文档:最开始 TiKV 是作为 HTAP 数据库 TiDB 的一个底层存储引擎设计并开发出来的,属于内部系统,缺乏详细的文档,以至于同学们不知道 TiKV 是怎么设计的,以及代码为什么要这么写。对于第一个问题,我们内部正在制作一系列的 Rust 培训课程,由 Rust 作者以及 Rust 社区知名的开发者亲自操刀,预计会在今年第一季度对外发布。希望通过该课程的学习,大家能快速入门 Rust,使用 Rust 开发自己的应用。而对于第二个问题,我们会启动 《TiKV 源码解析系列文章》以及 《Deep Dive TiKV 系列文章》计划,在《Deep Dive TiKV 系列文章》中,我们会详细介绍与解释 TiKV 所使用技术的基本原理,譬如 Raft 协议的说明,以及我们是如何对 Raft 做扩展和优化的。而 《TiKV 源码解析系列文章》则是会从源码层面给大家抽丝剥茧,让大家知道我们内部到底是如何实现的。我们希望,通过这两个系列,能让大家对 TiKV 有更深刻的理解,再加上 Rust 培训,能让大家很好的参与到 TiKV 的开发中来。结构本篇文章是《TiKV 源码解析系列文章》的序篇,会简单的给大家讲一下 TiKV 的基本模块,让大家对这个系统有一个整体的了解。要理解 TiKV,只是了解 https://github.com/tikv/tikv 这一个项目是远远不够的,通常,我们也需要了解很多其他的项目,包括但不限于:https://github.com/pingcap/raft-rshttps://github.com/pingcap/rust-prometheushttps://github.com/pingcap/rust-rocksdbhttps://github.com/pingcap/fail-rshttps://github.com/pingcap/rocksdbhttps://github.com/pingcap/grpc-rshttps://github.com/pingcap/pd在这个系列里面,我们首先会从 TiKV 使用的周边库开始介绍,然后介绍 TiKV,最后会介绍 PD。下面简单来说下我们的一些介绍计划。Storage EngineTiKV 现在使用 RocksDB 作为底层数据存储方案。在 pingcap/rust-rocksdb 这个库里面,我们会简单说明 Rust 是如何通过 Foreign Function Interface (FFI) 来跟 C library 进行交互,以及我们是如何将 RocksDB 的 C API 封装好给 Rust 使用的。另外,在 pingcap/rocksdb 这个库里面,我们会详细的介绍我们自己研发的 Key-Value 分离引擎 - Titan,同时也会让大家知道如何使用 RocksDB 对外提供的接口来构建自己的 engine。RaftTiKV 使用的是 Raft 一致性协议。为了保证算法的正确性,我们直接将 etcd 的 Go 实现 port 成了 Rust。在 pingcap/raft-rs,我们会详细介绍 Raft 的选举,Log 复制,snapshot 这些基本的功能是如何实现的。另外,我们还会介绍对 Raft 的一些优化,譬如 pre-vote,check quorum 机制,batch 以及 pipeline。最后,我们会说明如何去使用这个 Raft 库,这样大家就能在自己的应用里面集成 Raft 了。gRPCTiKV 使用的是 gRPC 作为通讯框架,我们直接把 Google C gRPC 库封装在 grpc-rs 这个库里面。我们会详细告诉大家如何去封装和操作 C gRPC 库,启动一个 gRPC 服务。另外,我们还会介绍如何使用 Rust 的 futures-rs 来将异步逻辑变成类似同步的方式来处理,以及如何通过解析 protobuf 文件来生成对应的 API 代码。最后,我们会介绍如何基于该库构建一个简单的 gRPC 服务。PrometheusTiKV 使用 Prometheus 作为其监控系统, rust-prometheus 这个库是 Prometheus 的 Rust client。在这个库里面,我们会介绍如果支持不同的 Prometheus 的数据类型(Coutner,Gauge,Historgram)。另外,我们会重点介绍我们是如何通过使用 Rust 的 Macro 来支持 Prometheus 的 Vector metrics 的。最后,我们会介绍如何在自己的项目里面集成 Prometheus client,将自己的 metrics 存到 Prometheus 里面,方便后续分析。FailFail 是一个错误注入的库。通过这个库,我们能很方便的在代码的某些地方加上 hook,注入错误,然后在系统运行的时候触发相关的错误,看系统是否稳定。我们会详细的介绍 Fail 是如何通过 macro 来注入错误,会告诉大家如何添加自己的 hook,以及在外面进行触发TiKVTiKV 是一个非常复杂的系统,这块我们会重点介绍,主要包括:Raftstore,该模块里面我们会介绍 TiKV 如何使用 Raft,如何支持 Multi-Raft。Storage,该模块里面我们会介绍 Multiversion concurrency control (MVCC),基于 Percolator 的分布式事务的实现,数据在 engine 里面的存储方式,engine 操作相关的 API 等。Server,该模块我们会介绍 TiKV 的 gRPC API,以及不同函数执行流程。Coprocessor,该模块我们会详细介绍 TiKV 是如何处理 TiDB 的下推请求的,如何通过不同的表达式进行数据读取以及计算的。PD,该模块我们会介绍 TiKV 是如何跟 PD 进行交互的。Import,该模块我们会介绍 TiKV 如何处理大量数据的导入,以及如何跟 TiDB 数据导入工具 lightning 交互的。Util,该模块我们会介绍一些 TiKV 使用的基本功能库。PDPD 用来负责整个 TiKV 的调度,我们会详细的介绍 PD 内部是如何使用 etcd 来进行元数据存取和高可用支持,也会介绍 PD 如何跟 TiKV 交互,如何生成全局的 ID 以及 timestamp。最后,我们会详细的介绍 PD 提供的 scheduler,以及不同的 scheudler 所负责的事情,让大家能通过配置 scheduler 来让系统更加的稳定。小结上面简单的介绍了源码解析涉及的模块,还有一些模块譬如 https://github.com/tikv/client-rust 仍在开发中,等完成之后我们也会进行源码解析。我们希望通过该源码解析系列,能让大家对 TiKV 有一个更深刻的理解。当然,TiKV 的源码也是一直在不停的演化,我们也会尽量保证文档的及时更新。最后,欢迎大家参与 TiKV 的开发。 ...

January 28, 2019 · 2 min · jiezi

终于等到你!阿里正式向 Apache Flink 贡献 Blink 源码

阿里妹导读:如同我们去年12月在 Flink Forward China 峰会所约,阿里巴巴内部 Flink 版本 Blink 将于 2019 年 1 月底正式开源。今天,我们终于等到了这一刻。阿里资深技术专家大沙,将为大家详细介绍本次开源的Blink主要功能和优化点,希望与业界同仁共同携手,推动Flink社区进一步发展。Blink简介Apache Flink是德国柏林工业大学的几个博士生和研究生从学校开始做起来的项目,早期叫做Stratosphere。2014年,StratoSphere项目中的核心成员从学校出来开发了Flink,同时将Flink计算的主流方向定位为流计算,并在同年将Flink捐赠Apache,后来快速孵化成为Apache的顶级项目。现在Flink是业界公认的最好的大数据流计算引擎。阿里巴巴在2015年开始尝试使用Flink。但是阿里的业务体量非常庞大,挑战也很多。彼时的Flink不管是规模还是稳定性尚未经历实践,成熟度有待商榷。为了把这么大的业务体量支持好,我们不得不在Flink之上做了一系列的改进,所以阿里巴巴维护了一个内部版本的Flink,它的名字叫做Blink。基于Blink的计算平台于2016年正式上线。截至目前,阿里绝大多数的技术部门都在使用Blink。Blink一直在阿里内部错综复杂的业务场景中锻炼成长着。对于内部用户反馈的各种性能、资源使用率、易用性等诸多方面的问题,Blink都做了针对性的改进。虽然现在Blink在阿里内部用的最多的场景主要还是在流计算,但是在批计算场景也有不少业务上线使用了。例如,在搜索和推荐的算法业务平台中,它使用Blink同时进行流计算和批处理。Blink被用来实现了流批一体化的样本生成和特征抽取这些流程,能够处理的特征数达到了数千亿,而且每秒钟处理数亿条消息。在这个场景的批处理中,我们单个作业处理的数据量已经超过400T,并且为了节省资源,我们的批处理作业是和流计算作业以及搜索的在线引擎运行在同样的机器上。所以大家可以看到流批一体化已经在阿里巴巴取得了极大的成功,我们希望这种成功和阿里巴巴内部的经验都能够带回给社区。Blink开源的背景其实从我们选择Flink的第一天开始我们就一直和社区紧密合作。过去的这几年我们也一直在把阿里对Flink 的改进推回社区。从2016年开始我们已经将流计算SQL的大部分功能,针对runtime的稳定性和性能优化做的若干重要设计都推回了社区。但是Blink本身发展迭代的速度非常快,而社区有自己的步伐,很多时候可能无法把我们的变更及时推回去。对于社区来说,一些大的功能和重构,需要达成共识后,才能被接受,这样才能更好地保证开源项目的质量,但是同时就会导致推入的速度变得相对较慢。经过这几年的开发迭代,我们这边和社区之间的差距已经变得比较大了。Blink 有一些很好的新功能,比如性能优越的批处理功能,在社区的版本是没有的。在过去这段时间里,我们不断听到有人在询问Blink的各种新功能。期望Blink尽快开源的呼声越来越大。我们一直在思考如何开源的问题,一种方案就是和以前一样,继续把各种功能和优化分解,逐个和社区讨论,慢慢地推回Flink。但这显然不是大家所期待的。另一个方案,就是先完整的尽可能的多的把代码开源,让社区的开发者能够尽快试用起来。第二个方案很快收到社区广大用户的支持。因此,从2018年年中开始我们就开始做开源的相关准备。经过半年的努力,我们终于把大部分Blink的功能梳理好,开源了出来。Blink开源的方式我们把代码贡献出来,是为了让大家能先尝试一些他们感兴趣的功能。Blink永远不会单独成为一个独立的开源项目来运作,他一定是Flink的一部分。开源后我们期望能找到办法以最快的方式将Blink merge到Flink中去。Blink开源只有一个目的,就是希望 Flink 做得更好。Apache Flink 是一个社区项目,Blink以什么样的形式进入 Flink 是最合适的,怎么贡献是社区最希望的方式,我们都要和社区一起讨论。在过去的一段时间内,我们在Flink社区征求了广泛的意见,大家一致认为将本次开源的Blink代码作为Flink的一个branch直接推回到Apache Flink项目中是最合适的方式。并且我们和社区也一起讨论规划出一套能够快速merge Blink到Flink master中的方案(具体细节可以查看Flink社区正在讨论的FLIP32)。我们期望这个merge能够在很短的时间内完成。这样我们之后的Machine Learning等其他新功能就可以直接推回到Flink master。相信用不了多久,Flink 和 Blink 就完全合二为一了。在那之后,阿里巴巴将直接使用Flink用于生产,并同时协助社区一起来维护Flink。本次开源的Blink的主要功能和优化点本次开源的Blink代码在Flink 1.5.1版本之上,加入了大量的新功能,以及在性能和稳定性上的各种优化。主要贡献包括,阿里巴巴在流计算上积累的一些新功能和性能的优化,一套完整的(能够跑通全部TPC-H/TPC-DS,能够读取Hive meta和data)高性能Batch SQL,以及一些以提升易用性为主的功能(包括支持更高效的interactive programming, 与zeppelin更紧密的结合, 以及体验和性能更佳的Flink web)。未来我们还将继续给Flink贡献在AI,IoT以及其他新领域的功能和优化。更多的关于这一版本Blink release的细节,请参考Blink代码根目录下的README.md文档。下面,我来分模块介绍下Blink主要的新的功能和优化点。Runtime为了更好的支持batch processing,以及解决阿里巴巴大规模生产场景中遇到的各种挑战,Blink对Runtime架构、效率、稳定性方面都做了大量改进。在架构方面,首先Blink引入了Pluggable ShuffleArchitecture,开发者可以根据不同的计算模型或者新硬件的需要实现不同的shuffle策略进行适配。此外Blink还引入新的调度架构,容许开发者根据计算模型自身的特点定制不同调度器。为了优化性能,Blink可以让算子更加灵活的chain在一起,避免了不必要的数据传输开销。在Pipeline Shuffle模式中,使用了ZeroCopy减少了网络层内存消耗。在BroadCast Shuffle模式中,Blink优化掉了大量的不必要的序列化和反序列化开销。此外,Blink提供了全新的JM FailOver机制,JM发生错误之后,新的JM会重新接管整个JOB而不是重启JOB,从而大大减少了JM FailOver对JOB的影响。最后,Blink也开发了对Kubernetes的支持。不同于Standalone模式在Kubernetes上的拉起方式,在基于Flink FLIP6的架构上基础之上,Blink根据job的资源需求动态的申请/释放Pod来运行TaskExecutor,实现了资源弹性,提升了资源的利用率。SQL/TableAPISQL/TableAPI架构上的重构和性能的优化是Blink本次开源版本的一个重大贡献。首先,我们对SQL engine的架构做了较大的调整。提出了全新的Query Processor(QP), 它包括了一个优化层(Query Optimizer)和一个算子层(Query Executor)。这样一来,流计算和批计算的在这两层大部分的设计工作就能做到尽可能的复用。另外,SQL和TableAPI的程序最终执行的时候将不会翻译到DataStream和DataSet这两个API上,而是直接构建到可运行的DAG上来,这样就使得物理执行算子的设计不完全依赖底层的API,有了更大的灵活度,同时执行代码也能够被灵活的codegen出来。唯一的一个影响就是这个版本的SQL和TableAPI不能和DataSet这个API进行互相转换,但仍然保留了和DataStream API互相转换的能力(将DataStream注册成表,或将Table转成DataStream后继续操作)。未来,我们计划把dataset的功能慢慢都在DataStream和TableAPI上面实现。到那时DataStream和SQL以及tableAPI一样,是一个可以同时描述bounded以及unbounded processing的API。除了架构上的重构,Blink还在具体实现上做了较多比较大的重构。首先,Blink引入了二进制的数据结构BinaryRow,极大的减少了数据存储上的开销以及数据在序列化和反序列化上计算的开销。其次,在算子的实现层面,Blink在更广范围内引入了CodeGen技术。由于预先知道算子需要处理的数据的类型,在QP层内部就可以直接生成更有针对性更高效的执行代码。Blink的算子会动态的申请和使用资源,能够更好的利用资源,提升效率,更加重要的是这些算子对资源有着比较好的控制,不会发生OutOfMemory 的问题。此外,针对流计算场景,Blink加入了miniBatch的执行模式,在aggregate、join等需要和state频繁交互且往往又能先做部分reduce的场景中,使用miniBatch能够极大的减少IO,从而成数量级的提升性能。除了上面提到的这些重要的重构和功能点,Blink还实现了完整的SQL DDL,带emit策略的流计算DML,若干重要的SQL功能,以及大量的性能优化策略。有了上面提到的诸多架构和实现上的重构。Blink的SQL/tableAPI在功能和性能方面都取得了脱胎换骨的变化。在批计算方面,首先Blink batch SQL能够完整的跑通TPC-H和TPC-DS,且性能上有着极大的提升。如上图所示,是这次开源的Blink版本和spark 2.3.1的TPC-DS的benchmark性能对比。柱状图的高度代表了运行的总时间,高度越低说明性能越好。可以看出,Blink在TPC-DS上和Spark相比有着非常明显的性能优势。而且这种性能优势随着数据量的增加而变得越来越大。在实际的场景这种优势已经超过 Spark的三倍。在流计算性能上我们也取得了类似的提升。我们线上的很多典型作业,它的性能是原来的3到5倍。在有数据倾斜的场景,以及若干比较有挑战的TPC-H query,流计算性能甚至得到了数十倍的提升。除了标准的Relational SQL API。TableAPI在功能上是SQL的超集,因此在SQL上所有新加的功能,我们在tableAPI也添加了相对应的API。除此之外,我们还在TableAPI上引入了一些新的功能。其中一个比较重要是cache功能。在批计算场景下,用户可以根据需要来cache计算的中间结果,从而避免不必要的重复计算。它极大的增强了interactive programming体验。我们后续会在tableAPI上添加更多有用的功能。其实很多新功能已经在社区展开讨论并被社区接受,例如我们在tableAPI增加了对一整行操作的算子map/flatMap/aggregate/flatAggregate(Flink FLIP29)等等。Hive的兼容性我们这次开源的版本实现了在元数据(meta data)和数据层将Flink和Hive对接和打通。国内外很多公司都还在用 Hive 在做自己的批处理。对于这些用户,现在使用这次Blink开源的版本,就可以直接用Flink SQL去查询Hive的数据,真正能够做到在Hive引擎和Flink引擎之间的自由切换。为了打通元数据,我们重构了Flink catalog的实现,并且增加了两种catalog,一个是基于内存存储的FlinkInMemoryCatalog,另外一个是能够桥接Hive metaStore的HiveCatalog。有了这个HiveCatalog,Flink作业就能读取Hive的metaData。为了打通数据,我们实现了HiveTableSource,使得Flink job可以直接读取Hive中普通表和分区表的数据。因此,通过这个版本,用户可以使用Flink SQL读取已有的Hive meta和data,做数据处理。未来我们将在Flink上继续加大对Hive兼容性的支持,包括支持Hive特有的query,data type,和Hive UDF等等。Zeppelin for Flink为了提供更好的可视化和交互式体验,我们做了大量的工作让Zeppelin能够更好的支持Flink。这些改动有些是在Flink上的,有些是在Zeppelin上的。在这些改动全部推回Flink和Zeppelin社区之前,大家可以使用这个Zeppelin image(具体细节请参考Blink代码里的docs/quickstart/zeppelin_quickstart.md)来测试和使用这些功能。这个用于测试的Zeppelin版本,首先很好的融合和集成了Flink的多种运行模式以及运维界面。使用文本SQL和tableAPI可以自如的查询Flink的static table和dynamic table。此外,针对Flink的流计算的特点,这一版Zeppelin也很好的支持了savepoint,用户可以在界面上暂停作业,然后再从savepoint恢复继续运行作业。在数据展示方面,除了传统的数据分析界面,我们也添加了流计算的翻牌器和时间序列展示等等功能。为了方便用户试用,我们在这一版zeppelin中提供3个built-in的Flink tutorial的例子: 一个是做StreamingETL的例子, 另外两个分别是做Flink Batch,Flink Stream的基础样例。Flink Web我们对Flink Web的易用性与性能等多个方面做了大量的改进,从资源使用、作业调优、日志查询等维度新增了大量功能,使得用户可以更方便的对Flink作业进行运维。在资源使用方面,新增了Cluster、TaskManager与Job三个级别的资源信息,使得资源的申请与使用情况一目了然。作业的拓扑关系及数据流向可以追溯至 Operator 级别,Vertex 增加了InQueue,OutQueue等多项指标,可以方便的追踪数据的反压、过滤及倾斜情况。TaskManager 和 JobManager 的日志功能得到大幅度加强,从Job、Vertex、SubTask 等多个维度都可以关联至对应日志,提供多日志文件访问入口,以及分页展示查询和日志高亮功能。另外,我们使用了较新的Angular 7.0 对Flink web进行了全面重构,页面运行性能有了一倍以上的提升。在大数据量情况下也不会发生页面卡死或者卡顿情况。同时对页面的交互逻辑进行了整体优化,绝大部分关联信息在单个页面就可以完成查询和比对工作,减少了大量不必要的跳转。未来的规划Blink迈出了全面开源的第一步,接下来我们会和社区合作,尽可能以最快的方式将Blink的功能和性能上的优化merge回Flink。本次的开源版本一方面贡献了Blink多年在流计算的积累,另一方面又重磅推出了在批处理上的成果。接下来,我们会持续给Flink社区贡献其他方面的功能。我们期望每过几个月就能看到技术上有一个比较大的亮点贡献到社区。下一个亮点应该是对机器学习的支持。要把机器学习支持好,有一系列的工作要做,包括引擎的功能,性能,和易用性。这里面大部分的工作我们已经开发完成,并且很多功能都已经在阿里巴巴内部服务上线了。除了技术上创新以及新功能之外,Flink的易用性和外围生态也非常重要。我们已经启动了若干这方面的项目,包括Python以及Go等多语言支持,Flink集群管理,Notebook,以及机器学习平台等等。这些项目有些会成为Flink自身的一部分贡献回社区,有些不是。但它们都基于Flink,是Flink生态的一个很好的补充。独立于Flink之外的那些项目,我们都也在认真的考虑开源出来。总之,Blink在开源的第一天起,就已经完全all-in的融入了Flink社区,我们希望所有的开发者看到我们的诚意和决心。未来,无论是功能还是生态,我们都会在Flink社区加大投入,我们也将投入力量做 Flink 社区的运营,让 Flink 真正在中国、乃至全世界大规模地使用起来。我们衷心的希望更多的人加入,一起把Apache Flink开源社区做得更好!本文作者:大沙阅读原文本文来自云栖社区合作伙伴“阿里技术”,如需转载请联系原作者。 ...

January 28, 2019 · 1 min · jiezi

Kube Controller Manager 源码分析

Kube Controller Manager 源码分析Controller Manager 在k8s 集群中扮演着中心管理的角色,它负责Deployment, StatefulSet, ReplicaSet 等资源的创建与管理,可以说是k8s的核心模块,下面我们以概略的形式走读一下k8s Controller Manager 代码。func NewControllerManagerCommand() *cobra.Command { s, err := options.NewKubeControllerManagerOptions() if err != nil { klog.Fatalf(“unable to initialize command options: %v”, err) } cmd := &cobra.Command{ Use: “kube-controller-manager”, Long: The Kubernetes controller manager is a daemon that embedsthe core control loops shipped with Kubernetes. In applications of robotics andautomation, a control loop is a non-terminating loop that regulates the state ofthe system. In Kubernetes, a controller is a control loop that watches the sharedstate of the cluster through the apiserver and makes changes attempting to move thecurrent state towards the desired state. Examples of controllers that ship withKubernetes today are the replication controller, endpoints controller, namespacecontroller, and serviceaccounts controller., Run: func(cmd *cobra.Command, args []string) { verflag.PrintAndExitIfRequested() utilflag.PrintFlags(cmd.Flags()) c, err := s.Config(KnownControllers(), ControllersDisabledByDefault.List()) if err != nil { fmt.Fprintf(os.Stderr, “%v\n”, err) os.Exit(1) } if err := Run(c.Complete(), wait.NeverStop); err != nil { fmt.Fprintf(os.Stderr, “%v\n”, err) os.Exit(1) } }, }Controller Manager 也是一个命令行,通过一系列flag启动,具体的各个flag 我们就不多看,有兴趣的可以去文档或者flags_opinion.go 文件里面去过滤一下,我们直接从Run 函数入手。Run Function 启动流程Kube Controller Manager 既可以单实例启动,也可以多实例启动。 如果为了保证 HA 而启动多个Controller Manager,它就需要选主来保证同一时间只有一个Master 实例。我们来看一眼Run 函数的启动流程,这里会把一些不重要的细节函数略过,只看重点func Run(c *config.CompletedConfig, stopCh <-chan struct{}) error { run := func(ctx context.Context) { rootClientBuilder := controller.SimpleControllerClientBuilder{ ClientConfig: c.Kubeconfig, } controllerContext, err := CreateControllerContext(c, rootClientBuilder, clientBuilder, ctx.Done()) if err != nil { klog.Fatalf(“error building controller context: %v”, err) } if err := StartControllers(controllerContext, saTokenControllerInitFunc, NewControllerInitializers(controllerContext.LoopMode), unsecuredMux); err != nil { klog.Fatalf(“error starting controllers: %v”, err) } controllerContext.InformerFactory.Start(controllerContext.Stop) close(controllerContext.InformersStarted) select {} } id, err := os.Hostname() if err != nil { return err } // add a uniquifier so that two processes on the same host don’t accidentally both become active id = id + “_” + string(uuid.NewUUID()) rl, err := resourcelock.New(c.ComponentConfig.Generic.LeaderElection.ResourceLock, “kube-system”, “kube-controller-manager”, c.LeaderElectionClient.CoreV1(), resourcelock.ResourceLockConfig{ Identity: id, EventRecorder: c.EventRecorder, }) if err != nil { klog.Fatalf(“error creating lock: %v”, err) } leaderelection.RunOrDie(context.TODO(), leaderelection.LeaderElectionConfig{ Lock: rl, LeaseDuration: c.ComponentConfig.Generic.LeaderElection.LeaseDuration.Duration, RenewDeadline: c.ComponentConfig.Generic.LeaderElection.RenewDeadline.Duration, RetryPeriod: c.ComponentConfig.Generic.LeaderElection.RetryPeriod.Duration, Callbacks: leaderelection.LeaderCallbacks{ OnStartedLeading: run, OnStoppedLeading: func() { klog.Fatalf(“leaderelection lost”) }, }, WatchDog: electionChecker, Name: “kube-controller-manager”, }) panic(“unreachable”)}这里的基本流程如下:首先定义了run 函数,run 函数负责具体的controller 构建以及最终的controller 操作的执行使用Client-go 提供的选主函数来进行选主如果获得主权限,那么就调用OnStartedLeading 注册函数,也就是上面的run 函数来执行操作,如果没选中,就hang住等待选主流程解析Client-go 选主工具类主要是通过kubeClient 在Configmap或者Endpoint选择一个资源创建,然后哪一个goroutine 创建成功了资源,哪一个goroutine 获得锁,当然所有的锁信息都会存在Configmap 或者Endpoint里面。之所以选择这两个资源类型,主要是考虑他们被Watch的少,但是现在kube Controller Manager 还是适用的Endpoint,后面会逐渐迁移到ConfigMap,因为Endpoint会被kube-proxy Ingress Controller等频繁Watch,我们来看一眼集群内Endpoint内容[root@iZ8vb5qgxqbxakfo1cuvpaZ ~]# kubectl get ep -n kube-system kube-controller-manager -o yamlapiVersion: v1kind: Endpointsmetadata: annotations: control-plane.alpha.kubernetes.io/leader: ‘{“holderIdentity”:“iZ8vbccmhgkyfdi8aii1hnZ_d880fea6-1322-11e9-913f-00163e033b49”,“leaseDurationSeconds”:15,“acquireTime”:“2019-01-08T08:53:49Z”,“renewTime”:“2019-01-22T11:16:59Z”,“leaderTransitions”:1}’ creationTimestamp: 2019-01-08T08:52:56Z name: kube-controller-manager namespace: kube-system resourceVersion: “2978183” selfLink: /api/v1/namespaces/kube-system/endpoints/kube-controller-manager uid: cade1b65-1322-11e9-9931-00163e033b49可以看到,这里面涵盖了当前Master ID,获取Master的时间,更新频率以及下一次更新时间。这一切最终还是靠ETCD 完成的选主。主要的选主代码如下func New(lockType string, ns string, name string, client corev1.CoreV1Interface, rlc ResourceLockConfig) (Interface, error) { switch lockType { case EndpointsResourceLock: return &EndpointsLock{ EndpointsMeta: metav1.ObjectMeta{ Namespace: ns, Name: name, }, Client: client, LockConfig: rlc, }, nil case ConfigMapsResourceLock: return &ConfigMapLock{ ConfigMapMeta: metav1.ObjectMeta{ Namespace: ns, Name: name, }, Client: client, LockConfig: rlc, }, nil default: return nil, fmt.Errorf(“Invalid lock-type %s”, lockType) }}StartController选主完毕后,就需要真正启动controller了,我们来看一下启动controller 的代码func StartControllers(ctx ControllerContext, startSATokenController InitFunc, controllers map[string]InitFunc, unsecuredMux *mux.PathRecorderMux) error { // Always start the SA token controller first using a full-power client, since it needs to mint tokens for the rest // If this fails, just return here and fail since other controllers won’t be able to get credentials. if _, _, err := startSATokenController(ctx); err != nil { return err } // Initialize the cloud provider with a reference to the clientBuilder only after token controller // has started in case the cloud provider uses the client builder. if ctx.Cloud != nil { ctx.Cloud.Initialize(ctx.ClientBuilder, ctx.Stop) } for controllerName, initFn := range controllers { if !ctx.IsControllerEnabled(controllerName) { klog.Warningf("%q is disabled", controllerName) continue } time.Sleep(wait.Jitter(ctx.ComponentConfig.Generic.ControllerStartInterval.Duration, ControllerStartJitter)) klog.V(1).Infof(“Starting %q”, controllerName) debugHandler, started, err := initFn(ctx) if err != nil { klog.Errorf(“Error starting %q”, controllerName) return err } if !started { klog.Warningf(“Skipping %q”, controllerName) continue } if debugHandler != nil && unsecuredMux != nil { basePath := “/debug/controllers/” + controllerName unsecuredMux.UnlistedHandle(basePath, http.StripPrefix(basePath, debugHandler)) unsecuredMux.UnlistedHandlePrefix(basePath+"/", http.StripPrefix(basePath, debugHandler)) } klog.Infof(“Started %q”, controllerName) } return nil}遍历所有的controller list执行每个controller 的Init Function那么一共有多少Controller 呢func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc { controllers := map[string]InitFunc{} controllers[“endpoint”] = startEndpointController controllers[“replicationcontroller”] = startReplicationController controllers[“podgc”] = startPodGCController controllers[“resourcequota”] = startResourceQuotaController controllers[“namespace”] = startNamespaceController controllers[“serviceaccount”] = startServiceAccountController controllers[“garbagecollector”] = startGarbageCollectorController controllers[“daemonset”] = startDaemonSetController controllers[“job”] = startJobController controllers[“deployment”] = startDeploymentController controllers[“replicaset”] = startReplicaSetController controllers[“horizontalpodautoscaling”] = startHPAController controllers[“disruption”] = startDisruptionController controllers[“statefulset”] = startStatefulSetController controllers[“cronjob”] = startCronJobController controllers[“csrsigning”] = startCSRSigningController controllers[“csrapproving”] = startCSRApprovingController controllers[“csrcleaner”] = startCSRCleanerController controllers[“ttl”] = startTTLController controllers[“bootstrapsigner”] = startBootstrapSignerController controllers[“tokencleaner”] = startTokenCleanerController controllers[“nodeipam”] = startNodeIpamController controllers[“nodelifecycle”] = startNodeLifecycleController if loopMode == IncludeCloudLoops { controllers[“service”] = startServiceController controllers[“route”] = startRouteController controllers[“cloud-node-lifecycle”] = startCloudNodeLifecycleController // TODO: volume controller into the IncludeCloudLoops only set. } controllers[“persistentvolume-binder”] = startPersistentVolumeBinderController controllers[“attachdetach”] = startAttachDetachController controllers[“persistentvolume-expander”] = startVolumeExpandController controllers[“clusterrole-aggregation”] = startClusterRoleAggregrationController controllers[“pvc-protection”] = startPVCProtectionController controllers[“pv-protection”] = startPVProtectionController controllers[“ttl-after-finished”] = startTTLAfterFinishedController controllers[“root-ca-cert-publisher”] = startRootCACertPublisher return controllers}答案就在这里,上面的代码列出来了当前kube controller manager 所有的controller,既有大家熟悉的Deployment StatefulSet 也有一些不熟悉的身影。下面我们以Deployment 为例看看它到底干了什么Deployment Controller先来看一眼Deployemnt Controller 启动函数func startDeploymentController(ctx ControllerContext) (http.Handler, bool, error) { if !ctx.AvailableResources[schema.GroupVersionResource{Group: “apps”, Version: “v1”, Resource: “deployments”}] { return nil, false, nil } dc, err := deployment.NewDeploymentController( ctx.InformerFactory.Apps().V1().Deployments(), ctx.InformerFactory.Apps().V1().ReplicaSets(), ctx.InformerFactory.Core().V1().Pods(), ctx.ClientBuilder.ClientOrDie(“deployment-controller”), ) if err != nil { return nil, true, fmt.Errorf(“error creating Deployment controller: %v”, err) } go dc.Run(int(ctx.ComponentConfig.DeploymentController.ConcurrentDeploymentSyncs), ctx.Stop) return nil, true, nil}看到这里,如果看过上一篇针对Client-go Informer 文章的肯定不陌生,这里又使用了InformerFactory,而且是好几个。其实kube Controller Manager 里面大量使用了Informer,Controller 就是使用 Informer 来通知和观察所有的资源。可以看到,这里Deployment Controller 主要关注Deployment ReplicaSet Pod 这三个资源。Deployment Controller 资源初始化下面来看一下Deployemnt Controller 初始化需要的资源// NewDeploymentController creates a new DeploymentController.func NewDeploymentController(dInformer appsinformers.DeploymentInformer, rsInformer appsinformers.ReplicaSetInformer, podInformer coreinformers.PodInformer, client clientset.Interface) (*DeploymentController, error) { eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartLogging(klog.Infof) eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: client.CoreV1().Events("")}) if client != nil && client.CoreV1().RESTClient().GetRateLimiter() != nil { if err := metrics.RegisterMetricAndTrackRateLimiterUsage(“deployment_controller”, client.CoreV1().RESTClient().GetRateLimiter()); err != nil { return nil, err } } dc := &DeploymentController{ client: client, eventRecorder: eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: “deployment-controller”}), queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), “deployment”), } dc.rsControl = controller.RealRSControl{ KubeClient: client, Recorder: dc.eventRecorder, } dInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: dc.addDeployment, UpdateFunc: dc.updateDeployment, // This will enter the sync loop and no-op, because the deployment has been deleted from the store. DeleteFunc: dc.deleteDeployment, }) rsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: dc.addReplicaSet, UpdateFunc: dc.updateReplicaSet, DeleteFunc: dc.deleteReplicaSet, }) podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ DeleteFunc: dc.deletePod, }) dc.syncHandler = dc.syncDeployment dc.enqueueDeployment = dc.enqueue dc.dLister = dInformer.Lister() dc.rsLister = rsInformer.Lister() dc.podLister = podInformer.Lister() dc.dListerSynced = dInformer.Informer().HasSynced dc.rsListerSynced = rsInformer.Informer().HasSynced dc.podListerSynced = podInformer.Informer().HasSynced return dc, nil}是不是这里的代码似曾相识,如果接触过Client-go Informer 的代码,可以看到这里如出一辙,基本上就是对创建的资源分别触发对应的Add Update Delete 函数,同时所有的资源通过Lister获得,不需要真正的Query APIServer。先来看一下针对Deployment 的Handlerfunc (dc *DeploymentController) addDeployment(obj interface{}) { d := obj.(*apps.Deployment) klog.V(4).Infof(“Adding deployment %s”, d.Name) dc.enqueueDeployment(d)}func (dc *DeploymentController) updateDeployment(old, cur interface{}) { oldD := old.(*apps.Deployment) curD := cur.(*apps.Deployment) klog.V(4).Infof(“Updating deployment %s”, oldD.Name) dc.enqueueDeployment(curD)}func (dc *DeploymentController) deleteDeployment(obj interface{}) { d, ok := obj.(*apps.Deployment) if !ok { tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { utilruntime.HandleError(fmt.Errorf(“Couldn’t get object from tombstone %#v”, obj)) return } d, ok = tombstone.Obj.(*apps.Deployment) if !ok { utilruntime.HandleError(fmt.Errorf(“Tombstone contained object that is not a Deployment %#v”, obj)) return } } klog.V(4).Infof(“Deleting deployment %s”, d.Name) dc.enqueueDeployment(d)}不论是Add Update Delete,处理方法如出一辙,都是一股脑的塞到Client-go 提供的worker Queue里面。 再来看看ReplicaSetfunc (dc *DeploymentController) addReplicaSet(obj interface{}) { rs := obj.(*apps.ReplicaSet) if rs.DeletionTimestamp != nil { // On a restart of the controller manager, it’s possible for an object to // show up in a state that is already pending deletion. dc.deleteReplicaSet(rs) return } // If it has a ControllerRef, that’s all that matters. if controllerRef := metav1.GetControllerOf(rs); controllerRef != nil { d := dc.resolveControllerRef(rs.Namespace, controllerRef) if d == nil { return } klog.V(4).Infof(“ReplicaSet %s added.”, rs.Name) dc.enqueueDeployment(d) return } // Otherwise, it’s an orphan. Get a list of all matching Deployments and sync // them to see if anyone wants to adopt it. ds := dc.getDeploymentsForReplicaSet(rs) if len(ds) == 0 { return } klog.V(4).Infof(“Orphan ReplicaSet %s added.”, rs.Name) for _, d := range ds { dc.enqueueDeployment(d) }}func (dc *DeploymentController) updateReplicaSet(old, cur interface{}) { curRS := cur.(*apps.ReplicaSet) oldRS := old.(*apps.ReplicaSet) if curRS.ResourceVersion == oldRS.ResourceVersion { // Periodic resync will send update events for all known replica sets. // Two different versions of the same replica set will always have different RVs. return } curControllerRef := metav1.GetControllerOf(curRS) oldControllerRef := metav1.GetControllerOf(oldRS) controllerRefChanged := !reflect.DeepEqual(curControllerRef, oldControllerRef) if controllerRefChanged && oldControllerRef != nil { // The ControllerRef was changed. Sync the old controller, if any. if d := dc.resolveControllerRef(oldRS.Namespace, oldControllerRef); d != nil { dc.enqueueDeployment(d) } } // If it has a ControllerRef, that’s all that matters. if curControllerRef != nil { d := dc.resolveControllerRef(curRS.Namespace, curControllerRef) if d == nil { return } klog.V(4).Infof(“ReplicaSet %s updated.”, curRS.Name) dc.enqueueDeployment(d) return } // Otherwise, it’s an orphan. If anything changed, sync matching controllers // to see if anyone wants to adopt it now. labelChanged := !reflect.DeepEqual(curRS.Labels, oldRS.Labels) if labelChanged || controllerRefChanged { ds := dc.getDeploymentsForReplicaSet(curRS) if len(ds) == 0 { return } klog.V(4).Infof(“Orphan ReplicaSet %s updated.”, curRS.Name) for _, d := range ds { dc.enqueueDeployment(d) } }}总结一下Add 和 Update根据ReplicaSet ownerReferences 寻找到对应的Deployment Name判断是否Rs 发生了变化如果变化就把Deployment 塞到Wokrer Queue里面去最后看一下针对Pod 的处理func (dc *DeploymentController) deletePod(obj interface{}) { pod, ok := obj.(*v1.Pod) // When a delete is dropped, the relist will notice a pod in the store not // in the list, leading to the insertion of a tombstone object which contains // the deleted key/value. Note that this value might be stale. If the Pod // changed labels the new deployment will not be woken up till the periodic resync. if !ok { tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { utilruntime.HandleError(fmt.Errorf(“Couldn’t get object from tombstone %#v”, obj)) return } pod, ok = tombstone.Obj.(*v1.Pod) if !ok { utilruntime.HandleError(fmt.Errorf(“Tombstone contained object that is not a pod %#v”, obj)) return } } klog.V(4).Infof(“Pod %s deleted.”, pod.Name) if d := dc.getDeploymentForPod(pod); d != nil && d.Spec.Strategy.Type == apps.RecreateDeploymentStrategyType { // Sync if this Deployment now has no more Pods. rsList, err := util.ListReplicaSets(d, util.RsListFromClient(dc.client.AppsV1())) if err != nil { return } podMap, err := dc.getPodMapForDeployment(d, rsList) if err != nil { return } numPods := 0 for _, podList := range podMap { numPods += len(podList.Items) } if numPods == 0 { dc.enqueueDeployment(d) } }}可以看到,基本思路差不多,当检查到Deployment 所有的Pod 都被删除后,将Deployment name 塞到Worker Queue 里面去。Deployment Controller Run 函数资源初始化完毕后,就开始真正的Run 来看一下Run 函数func (dc *DeploymentController) Run(workers int, stopCh <-chan struct{}) { defer utilruntime.HandleCrash() defer dc.queue.ShutDown() klog.Infof(“Starting deployment controller”) defer klog.Infof(“Shutting down deployment controller”) if !controller.WaitForCacheSync(“deployment”, stopCh, dc.dListerSynced, dc.rsListerSynced, dc.podListerSynced) { return } for i := 0; i < workers; i++ { go wait.Until(dc.worker, time.Second, stopCh) } <-stopCh}func (dc *DeploymentController) worker() { for dc.processNextWorkItem() { }}func (dc *DeploymentController) processNextWorkItem() bool { key, quit := dc.queue.Get() if quit { return false } defer dc.queue.Done(key) err := dc.syncHandler(key.(string)) dc.handleErr(err, key) return true}可以看到 这个代码就是Client-go 里面标准版的Worker 消费者,不断的从Queue 里面拿Obj 然后调用syncHandler 处理,一起来看看最终的Handler如何处理dc.syncHandlerfunc (dc *DeploymentController) syncDeployment(key string) error { startTime := time.Now() klog.V(4).Infof(“Started syncing deployment %q (%v)”, key, startTime) defer func() { klog.V(4).Infof(“Finished syncing deployment %q (%v)”, key, time.Since(startTime)) }() namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { return err } deployment, err := dc.dLister.Deployments(namespace).Get(name) if errors.IsNotFound(err) { klog.V(2).Infof(“Deployment %v has been deleted”, key) return nil } if err != nil { return err } // Deep-copy otherwise we are mutating our cache. // TODO: Deep-copy only when needed. d := deployment.DeepCopy() everything := metav1.LabelSelector{} if reflect.DeepEqual(d.Spec.Selector, &everything) { dc.eventRecorder.Eventf(d, v1.EventTypeWarning, “SelectingAll”, “This deployment is selecting all pods. A non-empty selector is required.”) if d.Status.ObservedGeneration < d.Generation { d.Status.ObservedGeneration = d.Generation dc.client.AppsV1().Deployments(d.Namespace).UpdateStatus(d) } return nil } // List ReplicaSets owned by this Deployment, while reconciling ControllerRef // through adoption/orphaning. rsList, err := dc.getReplicaSetsForDeployment(d) if err != nil { return err } // List all Pods owned by this Deployment, grouped by their ReplicaSet. // Current uses of the podMap are: // // * check if a Pod is labeled correctly with the pod-template-hash label. // * check that no old Pods are running in the middle of Recreate Deployments. podMap, err := dc.getPodMapForDeployment(d, rsList) if err != nil { return err } if d.DeletionTimestamp != nil { return dc.syncStatusOnly(d, rsList) } // Update deployment conditions with an Unknown condition when pausing/resuming // a deployment. In this way, we can be sure that we won’t timeout when a user // resumes a Deployment with a set progressDeadlineSeconds. if err = dc.checkPausedConditions(d); err != nil { return err } if d.Spec.Paused { return dc.sync(d, rsList) } // rollback is not re-entrant in case the underlying replica sets are updated with a new // revision so we should ensure that we won’t proceed to update replica sets until we // make sure that the deployment has cleaned up its rollback spec in subsequent enqueues. if getRollbackTo(d) != nil { return dc.rollback(d, rsList) } scalingEvent, err := dc.isScalingEvent(d, rsList) if err != nil { return err } if scalingEvent { return dc.sync(d, rsList) } switch d.Spec.Strategy.Type { case apps.RecreateDeploymentStrategyType: return dc.rolloutRecreate(d, rsList, podMap) case apps.RollingUpdateDeploymentStrategyType: return dc.rolloutRolling(d, rsList) } return fmt.Errorf(“unexpected deployment strategy type: %s”, d.Spec.Strategy.Type)}根据Worker Queue 取出来的Namespace & Name 从Lister 内Query到真正的Deployment 对象根据Deployment label 查询对应的ReplicaSet 列表根据ReplicaSet label 查询对应的 Pod 列表,并生成一个key 为ReplicaSet ID Value 为PodList的Map 数据结构判断当前Deployment 是否处于暂停状态判断当前Deployment 是否处于回滚状态根据更新策略Recreate 还是 RollingUpdate 决定对应的动作这里我们以Recreate为例来看一下策略动作func (dc *DeploymentController) rolloutRecreate(d *apps.Deployment, rsList []*apps.ReplicaSet, podMap map[types.UID]*v1.PodList) error { // Don’t create a new RS if not already existed, so that we avoid scaling up before scaling down. newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false) if err != nil { return err } allRSs := append(oldRSs, newRS) activeOldRSs := controller.FilterActiveReplicaSets(oldRSs) // scale down old replica sets. scaledDown, err := dc.scaleDownOldReplicaSetsForRecreate(activeOldRSs, d) if err != nil { return err } if scaledDown { // Update DeploymentStatus. return dc.syncRolloutStatus(allRSs, newRS, d) } // Do not process a deployment when it has old pods running. if oldPodsRunning(newRS, oldRSs, podMap) { return dc.syncRolloutStatus(allRSs, newRS, d) } // If we need to create a new RS, create it now. if newRS == nil { newRS, oldRSs, err = dc.getAllReplicaSetsAndSyncRevision(d, rsList, true) if err != nil { return err } allRSs = append(oldRSs, newRS) } // scale up new replica set. if _, err := dc.scaleUpNewReplicaSetForRecreate(newRS, d); err != nil { return err } if util.DeploymentComplete(d, &d.Status) { if err := dc.cleanupDeployment(oldRSs, d); err != nil { return err } } // Sync deployment status. return dc.syncRolloutStatus(allRSs, newRS, d)}根据ReplicaSet 获取当前所有的新老ReplicaSet如果有老的ReplicaSet 那么先把老的ReplicaSet replicas 缩容设置为0,当然第一次创建的时候是没有老ReplicaSet的如果第一次创建,那么需要去创建对应的ReplicaSet创建完毕对应的ReplicaSet后 扩容ReplicaSet 到对应的值等待新建的创建完毕,清理老的ReplcaiSet更新Deployment Status下面我们看看第一次创建Deployment 的代码func (dc *DeploymentController) getNewReplicaSet(d *apps.Deployment, rsList, oldRSs []*apps.ReplicaSet, createIfNotExisted bool) (*apps.ReplicaSet, error) { existingNewRS := deploymentutil.FindNewReplicaSet(d, rsList) // Calculate the max revision number among all old RSes maxOldRevision := deploymentutil.MaxRevision(oldRSs) // Calculate revision number for this new replica set newRevision := strconv.FormatInt(maxOldRevision+1, 10) // Latest replica set exists. We need to sync its annotations (includes copying all but // annotationsToSkip from the parent deployment, and update revision, desiredReplicas, // and maxReplicas) and also update the revision annotation in the deployment with the // latest revision. if existingNewRS != nil { rsCopy := existingNewRS.DeepCopy() // Set existing new replica set’s annotation annotationsUpdated := deploymentutil.SetNewReplicaSetAnnotations(d, rsCopy, newRevision, true) minReadySecondsNeedsUpdate := rsCopy.Spec.MinReadySeconds != d.Spec.MinReadySeconds if annotationsUpdated || minReadySecondsNeedsUpdate { rsCopy.Spec.MinReadySeconds = d.Spec.MinReadySeconds return dc.client.AppsV1().ReplicaSets(rsCopy.ObjectMeta.Namespace).Update(rsCopy) } // Should use the revision in existingNewRS’s annotation, since it set by before needsUpdate := deploymentutil.SetDeploymentRevision(d, rsCopy.Annotations[deploymentutil.RevisionAnnotation]) // If no other Progressing condition has been recorded and we need to estimate the progress // of this deployment then it is likely that old users started caring about progress. In that // case we need to take into account the first time we noticed their new replica set. cond := deploymentutil.GetDeploymentCondition(d.Status, apps.DeploymentProgressing) if deploymentutil.HasProgressDeadline(d) && cond == nil { msg := fmt.Sprintf(“Found new replica set %q”, rsCopy.Name) condition := deploymentutil.NewDeploymentCondition(apps.DeploymentProgressing, v1.ConditionTrue, deploymentutil.FoundNewRSReason, msg) deploymentutil.SetDeploymentCondition(&d.Status, *condition) needsUpdate = true } if needsUpdate { var err error if d, err = dc.client.AppsV1().Deployments(d.Namespace).UpdateStatus(d); err != nil { return nil, err } } return rsCopy, nil } if !createIfNotExisted { return nil, nil } // new ReplicaSet does not exist, create one. newRSTemplate := *d.Spec.Template.DeepCopy() podTemplateSpecHash := controller.ComputeHash(&newRSTemplate, d.Status.CollisionCount) newRSTemplate.Labels = labelsutil.CloneAndAddLabel(d.Spec.Template.Labels, apps.DefaultDeploymentUniqueLabelKey, podTemplateSpecHash) // Add podTemplateHash label to selector. newRSSelector := labelsutil.CloneSelectorAndAddLabel(d.Spec.Selector, apps.DefaultDeploymentUniqueLabelKey, podTemplateSpecHash) // Create new ReplicaSet newRS := apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ // Make the name deterministic, to ensure idempotence Name: d.Name + “-” + podTemplateSpecHash, Namespace: d.Namespace, OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(d, controllerKind)}, Labels: newRSTemplate.Labels, }, Spec: apps.ReplicaSetSpec{ Replicas: new(int32), MinReadySeconds: d.Spec.MinReadySeconds, Selector: newRSSelector, Template: newRSTemplate, }, } allRSs := append(oldRSs, &newRS) newReplicasCount, err := deploymentutil.NewRSNewReplicas(d, allRSs, &newRS) if err != nil { return nil, err } *(newRS.Spec.Replicas) = newReplicasCount // Set new replica set’s annotation deploymentutil.SetNewReplicaSetAnnotations(d, &newRS, newRevision, false) // Create the new ReplicaSet. If it already exists, then we need to check for possible // hash collisions. If there is any other error, we need to report it in the status of // the Deployment. alreadyExists := false createdRS, err := dc.client.AppsV1().ReplicaSets(d.Namespace).Create(&newRS)这里截取了部分重要代码首先查询一下当前是否有对应的新的ReplicaSet如果有那么仅仅需要更新Deployment Status 即可如果没有 那么创建对应的ReplicaSet 结构体最后调用Client-go 创建对应的ReplicaSet 实例后面还有一些代码 这里就不贴了,核心思想就是,根据ReplicaSet的情况创建对应的新的ReplicaSet,其实看到使用Client-go 创建ReplicaSet Deployment 这里基本完成了使命,剩下的就是根据watch 改变一下Deployment 的状态了,至于真正的Pod 的创建,那么就得ReplicaSet Controller 来完成了。ReplicaSet ControllerReplicaSet Controller 和Deployment Controller 长得差不多,重复的部分我们就不多说,先看一下初始化的时候,ReplicaSet 主要关注哪些资源func NewBaseController(rsInformer appsinformers.ReplicaSetInformer, podInformer coreinformers.PodInformer, kubeClient clientset.Interface, burstReplicas int, gvk schema.GroupVersionKind, metricOwnerName, queueName string, podControl controller.PodControlInterface) *ReplicaSetController { if kubeClient != nil && kubeClient.CoreV1().RESTClient().GetRateLimiter() != nil { metrics.RegisterMetricAndTrackRateLimiterUsage(metricOwnerName, kubeClient.CoreV1().RESTClient().GetRateLimiter()) } rsc := &ReplicaSetController{ GroupVersionKind: gvk, kubeClient: kubeClient, podControl: podControl, burstReplicas: burstReplicas, expectations: controller.NewUIDTrackingControllerExpectations(controller.NewControllerExpectations()), queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), queueName), } rsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: rsc.enqueueReplicaSet, UpdateFunc: rsc.updateRS, // This will enter the sync loop and no-op, because the replica set has been deleted from the store. // Note that deleting a replica set immediately after scaling it to 0 will not work. The recommended // way of achieving this is by performing a stop operation on the replica set. DeleteFunc: rsc.enqueueReplicaSet, }) rsc.rsLister = rsInformer.Lister() rsc.rsListerSynced = rsInformer.Informer().HasSynced podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: rsc.addPod, // This invokes the ReplicaSet for every pod change, eg: host assignment. Though this might seem like // overkill the most frequent pod update is status, and the associated ReplicaSet will only list from // local storage, so it should be ok. UpdateFunc: rsc.updatePod, DeleteFunc: rsc.deletePod, }) rsc.podLister = podInformer.Lister() rsc.podListerSynced = podInformer.Informer().HasSynced rsc.syncHandler = rsc.syncReplicaSet return rsc}可以看到ReplicaSet Controller 主要关注所有的ReplicaSet Pod的创建,他们的处理逻辑是一样的,都是根据触发函数,找到对应的ReplicaSet实例后,将对应的ReplicaSet 实例放到Worker Queue里面去。syncReplicaSet这里我们直接来看ReplicaSet Controller 的真正处理函数func (rsc *ReplicaSetController) syncReplicaSet(key string) error { startTime := time.Now() defer func() { klog.V(4).Infof(“Finished syncing %v %q (%v)”, rsc.Kind, key, time.Since(startTime)) }() namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { return err } rs, err := rsc.rsLister.ReplicaSets(namespace).Get(name) if errors.IsNotFound(err) { klog.V(4).Infof("%v %v has been deleted", rsc.Kind, key) rsc.expectations.DeleteExpectations(key) return nil } if err != nil { return err } rsNeedsSync := rsc.expectations.SatisfiedExpectations(key) selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) if err != nil { utilruntime.HandleError(fmt.Errorf(“Error converting pod selector to selector: %v”, err)) return nil } // list all pods to include the pods that don’t match the rs`s selector // anymore but has the stale controller ref. // TODO: Do the List and Filter in a single pass, or use an index. allPods, err := rsc.podLister.Pods(rs.Namespace).List(labels.Everything()) if err != nil { return err } // Ignore inactive pods. var filteredPods []*v1.Pod for _, pod := range allPods { if controller.IsPodActive(pod) { filteredPods = append(filteredPods, pod) } } // NOTE: filteredPods are pointing to objects from cache - if you need to // modify them, you need to copy it first. filteredPods, err = rsc.claimPods(rs, selector, filteredPods) if err != nil { return err } var manageReplicasErr error if rsNeedsSync && rs.DeletionTimestamp == nil { manageReplicasErr = rsc.manageReplicas(filteredPods, rs) } rs = rs.DeepCopy() newStatus := calculateStatus(rs, filteredPods, manageReplicasErr)根据从Worker Queue 得到的Name 获取真正的ReplicaSet 实例根据ReplicaSet Label 获取对应的所有的Pod List将所有的Running Pod 遍历出来根据Pod 情况判断是否需要创建 Pod将新的状态更新到ReplicaSet Status 字段中manageReplicas我们主要来看一眼创建Pod 的函数func (rsc *ReplicaSetController) manageReplicas(filteredPods []*v1.Pod, rs apps.ReplicaSet) error { diff := len(filteredPods) - int((rs.Spec.Replicas)) rsKey, err := controller.KeyFunc(rs) if err != nil { utilruntime.HandleError(fmt.Errorf(“Couldn’t get key for %v %#v: %v”, rsc.Kind, rs, err)) return nil } if diff < 0 { diff *= -1 if diff > rsc.burstReplicas { diff = rsc.burstReplicas } // TODO: Track UIDs of creates just like deletes. The problem currently // is we’d need to wait on the result of a create to record the pod’s // UID, which would require locking across the create, which will turn // into a performance bottleneck. We should generate a UID for the pod // beforehand and store it via ExpectCreations. rsc.expectations.ExpectCreations(rsKey, diff) klog.V(2).Infof(“Too few replicas for %v %s/%s, need %d, creating %d”, rsc.Kind, rs.Namespace, rs.Name, *(rs.Spec.Replicas), diff) // Batch the pod creates. Batch sizes start at SlowStartInitialBatchSize // and double with each successful iteration in a kind of “slow start”. // This handles attempts to start large numbers of pods that would // likely all fail with the same error. For example a project with a // low quota that attempts to create a large number of pods will be // prevented from spamming the API service with the pod create requests // after one of its pods fails. Conveniently, this also prevents the // event spam that those failures would generate. successfulCreations, err := slowStartBatch(diff, controller.SlowStartInitialBatchSize, func() error { boolPtr := func(b bool) *bool { return &b } controllerRef := &metav1.OwnerReference{ APIVersion: rsc.GroupVersion().String(), Kind: rsc.Kind, Name: rs.Name, UID: rs.UID, BlockOwnerDeletion: boolPtr(true), Controller: boolPtr(true), } err := rsc.podControl.CreatePodsWithControllerRef(rs.Namespace, &rs.Spec.Template, rs, controllerRef) if err != nil && errors.IsTimeout(err) { // Pod is created but its initialization has timed out. // If the initialization is successful eventually, the // controller will observe the creation via the informer. // If the initialization fails, or if the pod keeps // uninitialized for a long time, the informer will not // receive any update, and the controller will create a new // pod when the expectation expires. return nil } return err }) // Any skipped pods that we never attempted to start shouldn’t be expected. // The skipped pods will be retried later. The next controller resync will // retry the slow start process. if skippedPods := diff - successfulCreations; skippedPods > 0 { klog.V(2).Infof(“Slow-start failure. Skipping creation of %d pods, decrementing expectations for %v %v/%v”, skippedPods, rsc.Kind, rs.Namespace, rs.Name) for i := 0; i < skippedPods; i++ { // Decrement the expected number of creates because the informer won’t observe this pod rsc.expectations.CreationObserved(rsKey) } } return err } else if diff > 0 { if diff > rsc.burstReplicas { diff = rsc.burstReplicas } klog.V(2).Infof(“Too many replicas for %v %s/%s, need %d, deleting %d”, rsc.Kind, rs.Namespace, rs.Name, *(rs.Spec.Replicas), diff) // Choose which Pods to delete, preferring those in earlier phases of startup. podsToDelete := getPodsToDelete(filteredPods, diff) // Snapshot the UIDs (ns/name) of the pods we’re expecting to see // deleted, so we know to record their expectations exactly once either // when we see it as an update of the deletion timestamp, or as a delete. // Note that if the labels on a pod/rs change in a way that the pod gets // orphaned, the rs will only wake up after the expectations have // expired even if other pods are deleted. rsc.expectations.ExpectDeletions(rsKey, getPodKeys(podsToDelete)) errCh := make(chan error, diff) var wg sync.WaitGroup wg.Add(diff) for _, pod := range podsToDelete { go func(targetPod *v1.Pod) { defer wg.Done() if err := rsc.podControl.DeletePod(rs.Namespace, targetPod.Name, rs); err != nil { // Decrement the expected number of deletes because the informer won’t observe this deletion podKey := controller.PodKey(targetPod) klog.V(2).Infof(“Failed to delete %v, decrementing expectations for %v %s/%s”, podKey, rsc.Kind, rs.Namespace, rs.Name) rsc.expectations.DeletionObserved(rsKey, podKey) errCh <- err } }(pod) } wg.Wait()这里的逻辑就非常简单的,基本上就是根据当前Running Pod 数量和真正的replicas 声明比对,如果少了那么就调用Client-go 创建Pod ,如果多了就调用CLient-go 去删除 Pod。总结至此,一个Deployment -> ReplicaSet -> Pod 就真正的创建完毕。当Pod 被删除时候,ReplicaSet Controller 就会把 Pod 拉起来。如果更新Deployment 就会创建新的ReplicaSet 一层层嵌套多个Controller 结合完成最终的 Pod 创建。 当然,这里其实仅仅完成了Pod 数据写入到ETCD,其实真正的 Pod 实例并没有创建,还需要scheduler & kubelet 配合完成,我们会在后面的章节继续介绍。本文作者:xianlubird阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 23, 2019 · 18 min · jiezi

Kubernetes Client-go Informer 源码分析

几乎所有的Controller manager 和CRD Controller 都会使用Client-go 的Informer 函数,这样通过Watch 或者Get List 可以获取对应的Object,下面我们从源码分析角度来看一下Client go Informer 的机制。kubeClient, err := kubernetes.NewForConfig(cfg)if err != nil { klog.Fatalf(“Error building kubernetes clientset: %s”, err.Error())}kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)controller := NewController(kubeClient, exampleClient, kubeInformerFactory.Apps().V1().Deployments(), exampleInformerFactory.Samplecontroller().V1alpha1().Foos())// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh)// Start method is non-blocking and runs all registered informers in a dedicated goroutine.kubeInformerFactory.Start(stopCh)这里的例子是以https://github.com/kubernetes/sample-controller/blob/master/main.go节选,主要以 k8s 默认的Deployment Informer 为例子。可以看到直接使用Client-go Informer 还是非常简单的,先不管NewCOntroller函数里面执行了什么,顺着代码来看一下kubeInformerFactory.Start 都干了啥。// Start initializes all requested informers.func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() for informerType, informer := range f.informers { if !f.startedInformers[informerType] { go informer.Run(stopCh) f.startedInformers[informerType] = true } }}可以看到这里遍历了f.informers,而informers 的定义我们来看一眼数据结构type sharedInformerFactory struct { client kubernetes.Interface namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc lock sync.Mutex defaultResync time.Duration customResync map[reflect.Type]time.Duration informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. // This allows Start() to be called multiple times safely. startedInformers map[reflect.Type]bool}我们这里的例子,在运行的时候,f.informers里面含有的内容如下type *v1.Deployment informer &{0xc000379fa0 <nil> 0xc00038ccb0 {} 0xc000379f80 0xc00033bb00 30000000000 30000000000 0x28e5ec8 false false {0 0} {0 0}}也就是说,每一种k8s 类型都会有自己的Informer函数。下面我们来看一下这个函数是在哪里注册的,这里以Deployment Informer 为例。首先回到刚开始初始化kubeClient 的代码,controller := NewController(kubeClient, exampleClient, kubeInformerFactory.Apps().V1().Deployments(), exampleInformerFactory.Samplecontroller().V1alpha1().Foos()) deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: controller.handleObject, UpdateFunc: func(old, new interface{}) { newDepl := new.(*appsv1.Deployment) oldDepl := old.(*appsv1.Deployment) if newDepl.ResourceVersion == oldDepl.ResourceVersion { // Periodic resync will send update events for all known Deployments. // Two different versions of the same Deployment will always have different RVs. return } controller.handleObject(new) }, DeleteFunc: controller.handleObject, })注意这里的传参, kubeInformerFactory.Apps().V1().Deployments(), 这句话的意思就是指创建一个只关注Deployment 的Informer.controller := &Controller{ kubeclientset: kubeclientset, sampleclientset: sampleclientset, deploymentsLister: deploymentInformer.Lister(), deploymentsSynced: deploymentInformer.Informer().HasSynced, foosLister: fooInformer.Lister(), foosSynced: fooInformer.Informer().HasSynced, workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), “Foos”), recorder: recorder, }deploymentInformer.Lister() 这里就是初始化了一个Deployment Lister,下面来看一下Lister函数里面做了什么。// NewFilteredDeploymentInformer constructs a new informer for Deployment type.// Always prefer using an informer factory to get a shared informer instead of getting an independent// one. This reduces memory footprint and number of connections to the server.func NewFilteredDeploymentInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.AppsV1().Deployments(namespace).List(options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.AppsV1().Deployments(namespace).Watch(options) }, }, &appsv1.Deployment{}, resyncPeriod, indexers, )}func (f *deploymentInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredDeploymentInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)}func (f *deploymentInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&appsv1.Deployment{}, f.defaultInformer)}func (f *deploymentInformer) Lister() v1.DeploymentLister { return v1.NewDeploymentLister(f.Informer().GetIndexer())}注意这里的Lister 函数,它调用了Informer ,然后触发了f.factory.InformerFor ,这就最终调用了sharedInformerFactory InformerFor函数,// InternalInformerFor returns the SharedIndexInformer for obj using an internal// client.func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informerType := reflect.TypeOf(obj) informer, exists := f.informers[informerType] if exists { return informer } resyncPeriod, exists := f.customResync[informerType] if !exists { resyncPeriod = f.defaultResync } informer = newFunc(f.client, resyncPeriod) f.informers[informerType] = informer return informer}这里可以看到,informer = newFunc(f.client, resyncPeriod)这句话最终完成了对于informer的创建,并且注册到了Struct object中,完成了前面我们的问题。下面我们再回到informer start // Start initializes all requested informers.func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() for informerType, informer := range f.informers { if !f.startedInformers[informerType] { go informer.Run(stopCh) f.startedInformers[informerType] = true } }}这里可以看到,它会遍历所有的informer,然后选择异步调用Informer 的RUN方法。我们来全局看一下Run方法func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) { defer utilruntime.HandleCrash() fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, s.indexer) cfg := &Config{ Queue: fifo, ListerWatcher: s.listerWatcher, ObjectType: s.objectType, FullResyncPeriod: s.resyncCheckPeriod, RetryOnError: false, ShouldResync: s.processor.shouldResync, Process: s.HandleDeltas, } func() { s.startedLock.Lock() defer s.startedLock.Unlock() s.controller = New(cfg) s.controller.(*controller).clock = s.clock s.started = true }() // Separate stop channel because Processor should be stopped strictly after controller processorStopCh := make(chan struct{}) var wg wait.Group defer wg.Wait() // Wait for Processor to stop defer close(processorStopCh) // Tell Processor to stop wg.StartWithChannel(processorStopCh, s.cacheMutationDetector.Run) wg.StartWithChannel(processorStopCh, s.processor.run) defer func() { s.startedLock.Lock() defer s.startedLock.Unlock() s.stopped = true // Don’t want any new listeners }() s.controller.Run(stopCh)}首先它根据得到的 key 拆分函数和Store index 创建一个FIFO队列,这个队列是一个先进先出的队列,主要用来保存对象的各种事件。func NewDeltaFIFO(keyFunc KeyFunc, knownObjects KeyListerGetter) *DeltaFIFO { f := &DeltaFIFO{ items: map[string]Deltas{}, queue: []string{}, keyFunc: keyFunc, knownObjects: knownObjects, } f.cond.L = &f.lock return f}可以看到这个队列创建的比较简单,就是使用 Map 来存放数据,String 数组来存放队列的 Key。后面根据client 创建的List 和Watch 函数,还有队列创建了一个 config,下面将根据这个config 来初始化controller. 这个controller是client-go 的Cache controller ,主要用来控制从 APIServer 获得的对象的 cache 以及更新对象。下面主要关注这个函数调用wg.StartWithChannel(processorStopCh, s.processor.run)这里进行了真正的Listering 调用。func (p *sharedProcessor) run(stopCh <-chan struct{}) { func() { p.listenersLock.RLock() defer p.listenersLock.RUnlock() for _, listener := range p.listeners { p.wg.Start(listener.run) p.wg.Start(listener.pop) } p.listenersStarted = true }() <-stopCh p.listenersLock.RLock() defer p.listenersLock.RUnlock() for _, listener := range p.listeners { close(listener.addCh) // Tell .pop() to stop. .pop() will tell .run() to stop } p.wg.Wait() // Wait for all .pop() and .run() to stop}主要看 run 方法,还记得前面已经把ADD UPDATE DELETE 注册了自定义的处理函数了吗。这里就实现了前面函数的触发func (p processorListener) run() { // this call blocks until the channel is closed. When a panic happens during the notification // we will catch it, the offending item will be skipped!, and after a short delay (one second) // the next notification will be attempted. This is usually better than the alternative of never // delivering again. stopCh := make(chan struct{}) wait.Until(func() { // this gives us a few quick retries before a long pause and then a few more quick retries err := wait.ExponentialBackoff(retry.DefaultRetry, func() (bool, error) { for next := range p.nextCh { switch notification := next.(type) { case updateNotification: p.handler.OnUpdate(notification.oldObj, notification.newObj) case addNotification: p.handler.OnAdd(notification.newObj) case deleteNotification: p.handler.OnDelete(notification.oldObj) default: utilruntime.HandleError(fmt.Errorf(“unrecognized notification: %#v”, next)) } } // the only way to get here is if the p.nextCh is empty and closed return true, nil }) // the only way to get here is if the p.nextCh is empty and closed if err == nil { close(stopCh) } }, 1time.Minute, stopCh)}可以看到当p.nexhCh channel 接收到一个对象进入的时候,就会根据通知类型的不同,选择对应的用户注册函数去调用。那么这个channel 谁来向其中传入参数呢func (p *processorListener) pop() { defer utilruntime.HandleCrash() defer close(p.nextCh) // Tell .run() to stop var nextCh chan<- interface{} var notification interface{} for { select { case nextCh <- notification: // Notification dispatched var ok bool notification, ok = p.pendingNotifications.ReadOne() if !ok { // Nothing to pop nextCh = nil // Disable this select case } case notificationToAdd, ok := <-p.addCh: if !ok { return } if notification == nil { // No notification to pop (and pendingNotifications is empty) // Optimize the case - skip adding to pendingNotifications notification = notificationToAdd nextCh = p.nextCh } else { // There is already a notification waiting to be dispatched p.pendingNotifications.WriteOne(notificationToAdd) } } }}答案就是这个pop 函数,这里会从p.addCh中读取增加的通知,然后转给p.nexhCh 并且保证每个通知只会读取一次。下面就是最终的Controller run 函数,我们来看看到底干了什么// Run begins processing items, and will continue until a value is sent down stopCh.// It’s an error to call Run more than once.// Run blocks; call via go.func (c *controller) Run(stopCh <-chan struct{}) { defer utilruntime.HandleCrash() go func() { <-stopCh c.config.Queue.Close() }() r := NewReflector( c.config.ListerWatcher, c.config.ObjectType, c.config.Queue, c.config.FullResyncPeriod, ) r.ShouldResync = c.config.ShouldResync r.clock = c.clock c.reflectorMutex.Lock() c.reflector = r c.reflectorMutex.Unlock() var wg wait.Group defer wg.Wait() wg.StartWithChannel(stopCh, r.Run) wait.Until(c.processLoop, time.Second, stopCh)}这里主要的就是wg.StartWithChannel(stopCh, r.Run),// Run starts a watch and handles watch events. Will restart the watch if it is closed.// Run will exit when stopCh is closed.func (r *Reflector) Run(stopCh <-chan struct{}) { klog.V(3).Infof(“Starting reflector %v (%s) from %s”, r.expectedType, r.resyncPeriod, r.name) wait.Until(func() { if err := r.ListAndWatch(stopCh); err != nil { utilruntime.HandleError(err) } }, r.period, stopCh)}这里就调用了r.ListAndWatch 方法,这个方法比较复杂,我们慢慢来看。// watchHandler watches w and keeps *resourceVersion up to date.func (r *Reflector) watchHandler(w watch.Interface, resourceVersion *string, errc chan error, stopCh <-chan struct{}) error { start := r.clock.Now() eventCount := 0 // Stopping the watcher should be idempotent and if we return from this function there’s no way // we’re coming back in with the same watch interface. defer w.Stop() // update metrics defer func() { r.metrics.numberOfItemsInWatch.Observe(float64(eventCount)) r.metrics.watchDuration.Observe(time.Since(start).Seconds()) }()loop: for { select { case <-stopCh: return errorStopRequested case err := <-errc: return err case event, ok := <-w.ResultChan(): if !ok { break loop } if event.Type == watch.Error { return apierrs.FromObject(event.Object) } if e, a := r.expectedType, reflect.TypeOf(event.Object); e != nil && e != a { utilruntime.HandleError(fmt.Errorf("%s: expected type %v, but watch event object had type %v", r.name, e, a)) continue } meta, err := meta.Accessor(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event)) continue } newResourceVersion := meta.GetResourceVersion() switch event.Type { case watch.Added: err := r.store.Add(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", r.name, event.Object, err)) } case watch.Modified: err := r.store.Update(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", r.name, event.Object, err)) } case watch.Deleted: // TODO: Will any consumers need access to the “last known // state”, which is passed in event.Object? If so, may need // to change this. err := r.store.Delete(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", r.name, event.Object, err)) } default: utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event)) } resourceVersion = newResourceVersion r.setLastSyncResourceVersion(newResourceVersion) eventCount++ } } watchDuration := r.clock.Now().Sub(start) if watchDuration < 1time.Second && eventCount == 0 { r.metrics.numberOfShortWatches.Inc() return fmt.Errorf(“very short watch: %s: Unexpected watch close - watch lasted less than a second and no items received”, r.name) } klog.V(4).Infof("%s: Watch close - %v total %v items received", r.name, r.expectedType, eventCount) return nil}这里就是真正调用watch 方法,根据返回的watch 事件,将其放入到前面创建的 FIFO 队列中。最终调用了controller 的POP 方法// processLoop drains the work queue.// TODO: Consider doing the processing in parallel. This will require a little thought// to make sure that we don’t end up processing the same object multiple times// concurrently.//// TODO: Plumb through the stopCh here (and down to the queue) so that this can// actually exit when the controller is stopped. Or just give up on this stuff// ever being stoppable. Converting this whole package to use Context would// also be helpful.func (c *controller) processLoop() { for { obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process)) if err != nil { if err == FIFOClosedError { return } if c.config.RetryOnError { // This is the safe way to re-enqueue. c.config.Queue.AddIfNotPresent(obj) } } }}前面是将 watch 到的对象加入到队列中,这里的goroutine 就是用来消费的。具体的消费函数就是前面创建的Process 函数func (s *sharedIndexInformer) HandleDeltas(obj interface{}) error { s.blockDeltas.Lock() defer s.blockDeltas.Unlock() // from oldest to newest for _, d := range obj.(Deltas) { switch d.Type { case Sync, Added, Updated: isSync := d.Type == Sync s.cacheMutationDetector.AddObject(d.Object) if old, exists, err := s.indexer.Get(d.Object); err == nil && exists { if err := s.indexer.Update(d.Object); err != nil { return err } s.processor.distribute(updateNotification{oldObj: old, newObj: d.Object}, isSync) } else { if err := s.indexer.Add(d.Object); err != nil { return err } s.processor.distribute(addNotification{newObj: d.Object}, isSync) } case Deleted: if err := s.indexer.Delete(d.Object); err != nil { return err } s.processor.distribute(deleteNotification{oldObj: d.Object}, false) } } return nil}这个函数就是根据传进来的obj,先从自己的cache 中取一下,看是否存在,如果存在就代表是Update ,那么更新自己的队列后,调用用户注册的Update 函数,如果不存在,就调用用户的 Add 函数。到此Client-go 的Informer 流程源码分析基本完毕。本文作者:xianlubird 阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 22, 2019 · 8 min · jiezi

关于属性描述符PropertyDescriptor

本文首发于本博客 猫叔的博客,转载请申明出处前言感谢GY丶L粉丝的提问:属性描述器PropertyDescriptor是干嘛用的?本来我也没有仔细了解过描述符这一块的知识,不过粉丝问了,我就抽周末的时间看看,顺便学习一下,粉丝问的刚好是PropertyDescriptor这个属性描述符,我看了下源码。/** * A PropertyDescriptor describes one property that a Java Bean * exports via a pair of accessor methods. /public class PropertyDescriptor extends FeatureDescriptor { //…}emmmm,假装自己英语能厉害的说,属性描述符描述了一个属性,即Java Bean 通过一对访问器方法来导出。(没错,他确实是存在于java.beans包下的)通过类关系图,可以知道,我们应该提前了解一下FeatureDescriptor才行了。很好,起码目前还没有设计抽象类或者接口。FeatureDescriptor/* * The FeatureDescriptor class is the common baseclass for PropertyDescriptor, * EventSetDescriptor, and MethodDescriptor, etc. * <p> * It supports some common information that can be set and retrieved for * any of the introspection descriptors. * <p> * In addition it provides an extension mechanism so that arbitrary * attribute/value pairs can be associated with a design feature. /public class FeatureDescriptor { //…}okay,这是很合理的设计方式,FeatureDescriptor为类似PropertyDescriptor、EvebtSetDescriptor、MethodDescriptor的描述符提供了一些共用的常量信息。同时它也提供一个扩展功能,方便任意属性或键值对可以于设计功能相关联。这里简单的说下,在我大致看了一下源码后(可能不够详细,最近有点忙,时间较赶),FeatureDescriptor主要是针对一下属性的一些get/set,同时这些属性都是基本通用于PropertyDescriptor、EvebtSetDescriptor、MethodDescriptor。 private boolean expert; // 专有 private boolean hidden; // 隐藏 private boolean preferred; // 首选 private String shortDescription; //简单说明 private String name; // 编程名称 private String displayName; //本地名称 private Hashtable<String, Object> table; // 属性表其实该类还有另外几个方法,比如深奥的构造函数等等,这里就不深入探讨了。PropertyDescriptor那么我们大致知道了FeatureDescriptor,接下来就可以来深入了解看看这个属性描述符PropertyDescriptor。说到属性,大家一定会想到的就是get/set这个些基础的东西,当我打开PropertyDescriptor源码的时候,我也看到了一开始猜想的点。 private final MethodRef readMethodRef = new MethodRef(); private final MethodRef writeMethodRef = new MethodRef(); private String writeMethodName; private String readMethodName;这里的代码是我从源码中抽离的一部分,起码我们这样看可以大致理解,是分为写和读的步骤,那么就和我们初学java的get/set是一致的。同时我还看到了,这个,及其注释。 // The base name of the method name which will be prefixed with the // read and write method. If name == “foo” then the baseName is “Foo” private String baseName;这好像可以解释,为什么我们的属性在生成get/set的时候,第一个字母变成大写?!注释好像确实是这样写的。由于可能需要一个Bean对象,所以我以前在案例中先创建了一个Cat类。public class Cat { private String name; private String describe; private int age; private int weight; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescribe() { return describe; } public void setDescribe(String describe) { this.describe = describe; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; }}构造函数起码目前,我还不知道我应该怎么使用它,那么我们就一步一步来吧,我看到它有好几个构造函数,这是一个有趣而且有难度的事情,我们先试着创建一个PropertyDescriptor吧。第一种构造函数 /* * Constructs a PropertyDescriptor for a property that follows * the standard Java convention by having getFoo and setFoo * accessor methods. Thus if the argument name is “fred”, it will * assume that the writer method is “setFred” and the reader method * is “getFred” (or “isFred” for a boolean property). Note that the * property name should start with a lower case character, which will * be capitalized in the method names. * * @param propertyName The programmatic name of the property. * @param beanClass The Class object for the target bean. For * example sun.beans.OurButton.class. * @exception IntrospectionException if an exception occurs during * introspection. / public PropertyDescriptor(String propertyName, Class<?> beanClass) throws IntrospectionException { this(propertyName, beanClass, Introspector.IS_PREFIX + NameGenerator.capitalize(propertyName), Introspector.SET_PREFIX + NameGenerator.capitalize(propertyName)); }这个好像是参数最少的,它只需要我们传入一个属性字符串,还有对应的类就好了,其实它也是调用了另一个构造函数,只是它会帮我们默认生成读方法和写方法。方法中的Introspector.IS_PREFIX + NameGenerator.capitalize(propertyName)其实就是自己拼出一个默认的get/set方法,大家有兴趣可以去看看源码。那么对应的实现内容,我想大家应该都想到了。 public static void main(String[] args) throws Exception { PropertyDescriptor CatPropertyOfName = new PropertyDescriptor(“name”, Cat.class); System.out.println(CatPropertyOfName.getPropertyType()); System.out.println(CatPropertyOfName.getPropertyEditorClass()); System.out.println(CatPropertyOfName.getReadMethod()); System.out.println(CatPropertyOfName.getWriteMethod()); }第二种构造函数/* * This constructor takes the name of a simple property, and method * names for reading and writing the property. * * @param propertyName The programmatic name of the property. * @param beanClass The Class object for the target bean. For * example sun.beans.OurButton.class. * @param readMethodName The name of the method used for reading the property * value. May be null if the property is write-only. * @param writeMethodName The name of the method used for writing the property * value. May be null if the property is read-only. * @exception IntrospectionException if an exception occurs during * introspection. / public PropertyDescriptor(String propertyName, Class<?> beanClass, String readMethodName, String writeMethodName) throws IntrospectionException { if (beanClass == null) { throw new IntrospectionException(“Target Bean class is null”); } if (propertyName == null || propertyName.length() == 0) { throw new IntrospectionException(“bad property name”); } if ("".equals(readMethodName) || “".equals(writeMethodName)) { throw new IntrospectionException(“read or write method name should not be the empty string”); } setName(propertyName); setClass0(beanClass); this.readMethodName = readMethodName; if (readMethodName != null && getReadMethod() == null) { throw new IntrospectionException(“Method not found: " + readMethodName); } this.writeMethodName = writeMethodName; if (writeMethodName != null && getWriteMethod() == null) { throw new IntrospectionException(“Method not found: " + writeMethodName); } // If this class or one of its base classes allow PropertyChangeListener, // then we assume that any properties we discover are “bound”. // See Introspector.getTargetPropertyInfo() method. Class[] args = { PropertyChangeListener.class }; this.bound = null != Introspector.findMethod(beanClass, “addPropertyChangeListener”, args.length, args); }没错,这个构造函数就是第一种构造函数内部二次调用的,所需要的参数很简单,同时我也希望大家可以借鉴这个方法中的一些检测方式。这次的实现方式也是同样的形式。 public static void main(String[] args) throws Exception { PropertyDescriptor CatPropertyOfName = new PropertyDescriptor(“name”, Cat.class,“getName”,“setName”); System.out.println(CatPropertyOfName.getPropertyType()); System.out.println(CatPropertyOfName.getPropertyEditorClass()); System.out.println(CatPropertyOfName.getReadMethod()); System.out.println(CatPropertyOfName.getWriteMethod()); }第三种构造函数 /* * This constructor takes the name of a simple property, and Method * objects for reading and writing the property. * * @param propertyName The programmatic name of the property. * @param readMethod The method used for reading the property value. * May be null if the property is write-only. * @param writeMethod The method used for writing the property value. * May be null if the property is read-only. * @exception IntrospectionException if an exception occurs during * introspection. */ public PropertyDescriptor(String propertyName, Method readMethod, Method writeMethod) throws IntrospectionException { if (propertyName == null || propertyName.length() == 0) { throw new IntrospectionException(“bad property name”); } setName(propertyName); setReadMethod(readMethod); setWriteMethod(writeMethod); }这个不用传类,因为你需要传递两个实际的方法进来,所以主要三个对应属性的参数既可。看看大致的实现内容 public static void main(String[] args) throws Exception { Class<?> classType = Cat.class; Method CatNameOfRead = classType.getMethod(“getName”); Method CatNameOfWrite = classType.getMethod(“setName”, String.class); PropertyDescriptor CatPropertyOfName = new PropertyDescriptor(“name”, CatNameOfRead,CatNameOfWrite); System.out.println(CatPropertyOfName.getPropertyType()); System.out.println(CatPropertyOfName.getPropertyEditorClass()); System.out.println(CatPropertyOfName.getReadMethod()); System.out.println(CatPropertyOfName.getWriteMethod()); }好了,大致介绍了几种构造函数与实现方式,起码我们现在知道它需要什么。一些使用方式其实在我上面写一些构造函数的时候,我想大家应该已经感受到与反射相关了,起码我感觉上是这样的,所以我一开始想到这样的案例形式,通过反射与这个属性描述类去赋予我的类。 public static void main(String[] args) throws Exception { //获取类 Class classType = Class.forName(“com.example.demo.beans.Cat”); Object catObj = classType.newInstance(); //获取Name属性 PropertyDescriptor catPropertyOfName = new PropertyDescriptor(“name”,classType); //得到对应的写方法 Method writeOfName = catPropertyOfName.getWriteMethod(); //将值赋进这个类中 writeOfName.invoke(catObj,“river”); Cat cat = (Cat)catObj; System.out.println(cat.toString()); }运行结果还是顺利的。Cat{name=‘river’, describe=‘null’, age=0, weight=0}可以看到,我们确实得到了一个理想中的对象。那么我是不是可以改变一个已经创建的对象呢? public static void main(String[] args) throws Exception { //一开始的默认对象 Cat cat = new Cat(“river”,“黑猫”,2,4); //获取name属性 PropertyDescriptor catPropertyOfName = new PropertyDescriptor(“name”,Cat.class); //得到读方法 Method readMethod = catPropertyOfName.getReadMethod(); //获取属性值 String name = (String) readMethod.invoke(cat); System.out.println(“默认:” + name); //得到写方法 Method writeMethod = catPropertyOfName.getWriteMethod(); //修改值 writeMethod.invoke(cat,“copy”); System.out.println(“修改后:” + cat); }上面的demo是,我先创建了一个对象,然后通过属性描述符读取name值,再进行修改值,最后输出的对象的值也确实改变了。默认:river修改后:Cat{name=‘copy’, describe=‘黑猫’, age=2, weight=4}收尾这是一个有趣的API,我想另外两个(EvebtSetDescriptor、MethodDescriptor)应该也差不多,大家可以再通过此方法去探究,只有自己尝试一次才能学到这里面的一些东西,还有一些项目场景的使用方式,不过一般的业务场景应该很少使用到这个API。那么这个东西究竟可以干什么呢?我想你试着敲一次也许有一些答案了。公众号:Java猫说现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。 ...

January 19, 2019 · 5 min · jiezi

TiDB 源码阅读系列文章(二十四)TiDB Binlog 源码解析

作者:姚维TiDB Binlog Overview这篇文章不是讲 TiDB Binlog 组件的源码,而是讲 TiDB 在执行 DML/DDL 语句过程中,如何将 Binlog 数据 发送给 TiDB Binlog 集群的 Pump 组件。目前 TiDB 在 DML 上的 Binlog 用的类似 Row-based 的格式。具体 Binlog 具体的架构细节可以参考这篇 文章。这里只描述 TiDB 中的代码实现。DML BinlogTiDB 采用 protobuf 来编码 binlog,具体的格式可以见 binlog.proto。这里讨论 TiDB 写 Binlog 的机制,以及 Binlog 对 TiDB 写入的影响。TiDB 会在 DML 语句提交,以及 DDL 语句完成的时候,向 pump 输出 Binlog。Statement 执行阶段DML 语句包括 Insert/Replace、Update、Delete,这里挑 Insert 语句来阐述,其他的语句行为都类似。首先在 Insert 语句执行完插入(未提交)之前,会把自己新增的数据记录在 binlog.TableMutation 结构体中。// TableMutation 存储表中数据的变化message TableMutation { // 表的 id,唯一标识一个表 optional int64 table_id = 1 [(gogoproto.nullable) = false]; // 保存插入的每行数据 repeated bytes inserted_rows = 2; // 保存修改前和修改后的每行的数据 repeated bytes updated_rows = 3; // 已废弃 repeated int64 deleted_ids = 4; // 已废弃 repeated bytes deleted_pks = 5; // 删除行的数据 repeated bytes deleted_rows = 6; // 记录数据变更的顺序 repeated MutationType sequence = 7;}这个结构体保存于跟每个 Session 链接相关的事务上下文结构体中 TxnState.mutations。 一张表对应一个 TableMutation 对象,TableMutation 里面保存了这个事务对这张表的所有变更数据。Insert 会把当前语句插入的行,根据 RowID + Row-value 的格式编码之后,追加到 TableMutation.InsertedRows 中:func (t *Table) addInsertBinlog(ctx context.Context, h int64, row []types.Datum, colIDs []int64) error { mutation := t.getMutation(ctx) pk, err := codec.EncodeValue(ctx.GetSessionVars().StmtCtx, nil, types.NewIntDatum(h)) if err != nil { return errors.Trace(err) } value, err := tablecodec.EncodeRow(ctx.GetSessionVars().StmtCtx, row, colIDs, nil, nil) if err != nil { return errors.Trace(err) } bin := append(pk, value…) mutation.InsertedRows = append(mutation.InsertedRows, bin) mutation.Sequence = append(mutation.Sequence, binlog.MutationType_Insert) return nil}等到所有的语句都执行完之后,在 TxnState.mutations 中就保存了当前事务对所有表的变更数据。Commit 阶段对于 DML 而言,TiDB 的事务采用 2-phase-commit 算法,一次事务提交会分为 Prewrite 阶段,以及 Commit 阶段。这里分两个阶段来看看 TiDB 具体的行为。Prewrite Binlog在 session.doCommit 函数中,TiDB 会构造 binlog.PrewriteValue:message PrewriteValue { optional int64 schema_version = 1 [(gogoproto.nullable) = false]; repeated TableMutation mutations = 2 [(gogoproto.nullable) = false];}这个 PrewriteValue 中包含了跟这次变动相关的所有行数据,TiDB 会填充一个类型为 binlog.BinlogType_Prewrite 的 Binlog:info := &binloginfo.BinlogInfo{ Data: &binlog.Binlog{ Tp: binlog.BinlogType_Prewrite, PrewriteValue: prewriteData, }, Client: s.sessionVars.BinlogClient.(binlog.PumpClient),}TiDB 这里用一个事务的 Option kv.BinlogInfo 来把 BinlogInfo 绑定到当前要提交的 transaction 对象中:s.txn.SetOption(kv.BinlogInfo, info)在 twoPhaseCommitter.execute 中,在把数据 prewrite 到 TiKV 的同时,会调用 twoPhaseCommitter.prewriteBinlog,这里会把关联的 binloginfo.BinlogInfo 取出来,把 Binlog 的 binlog.PrewriteValue 输出到 Pump。binlogChan := c.prewriteBinlog()err := c.prewriteKeys(NewBackoffer(prewriteMaxBackoff, ctx), c.keys)if binlogChan != nil { binlogErr := <-binlogChan // 等待 write prewrite binlog 完成 if binlogErr != nil { return errors.Trace(binlogErr) }}这里值得注意的是,在 prewrite 阶段,是需要等待 write prewrite binlog 完成之后,才能继续做接下去的提交的,这里是为了保证 TiDB 成功提交的事务,Pump 至少一定能收到 Prewrite Binlog。Commit Binlog在 twoPhaseCommitter.execute 事务提交结束之后,事务可能提交成功,也可能提交失败。TiDB 需要把这个状态告知 Pump:err = committer.execute(ctx)if err != nil { committer.writeFinishBinlog(binlog.BinlogType_Rollback, 0) return errors.Trace(err)}committer.writeFinishBinlog(binlog.BinlogType_Commit, int64(committer.commitTS))如果发生了 error,那么输出的 Binlog 类型就为 binlog.BinlogType_Rollback,如果成功提交,那么输出的 Binlog 类型就为 binlog.BinlogType_Commit。func (c *twoPhaseCommitter) writeFinishBinlog(tp binlog.BinlogType, commitTS int64) { if !c.shouldWriteBinlog() { return } binInfo := c.txn.us.GetOption(kv.BinlogInfo).(*binloginfo.BinlogInfo) binInfo.Data.Tp = tp binInfo.Data.CommitTs = commitTS go func() { err := binInfo.WriteBinlog(c.store.clusterID) if err != nil { log.Errorf(“failed to write binlog: %v”, err) } }()}值得注意的是,这里 WriteBinlog 是单独启动 goroutine 异步完成的,也就是 Commit 阶段,是不再需要等待写 binlog 完成的。这里可以节省一点 commit 的等待时间,这里不需要等待是因为 Pump 即使接收不到这个 Commit Binlog,在超过 timeout 时间后,Pump 会自行根据 Prewrite Binlog 到 TiKV 中确认当条事务的提交状态。DDL Binlog一个 DDL 有如下几个状态:const ( JobStateNone JobState = 0 JobStateRunning JobState = 1 JobStateRollingback JobState = 2 JobStateRollbackDone JobState = 3 JobStateDone JobState = 4 JobStateSynced JobState = 6 JobStateCancelling JobState = 7)这些状态代表了一个 DDL 任务所处的状态:JobStateNone,代表 DDL 任务还在处理队列,TiDB 还没有开始做这个 DDL。JobStateRunning,当 DDL Owner 开始处理这个任务的时候,会把状态设置为 JobStateRunning,之后 DDL 会开始变更,TiDB 的 Schema 可能会涉及多个状态的变更,这中间不会改变 DDL job 的状态,只会变更 Schema 的状态。JobStateDone, 当 TiDB 完成自己所有的 Schema 状态变更之后,会把 Job 的状态改为 Done。JobStateSynced,当 TiDB 每做一次 schema 状态变更,就会需要跟集群中的其他 TiDB 做一次同步,但是当 Job 状态为 JobStateDone 之后,在 TiDB 等到所有的 TiDB 节点同步之后,会将状态修改为 JobStateSynced。JobStateCancelling,TiDB 提供语法 ADMIN CANCEL DDL JOBS job_ids 用于取消某个正在执行或者还未执行的 DDL 任务,当成功执行这个命令之后,DDL 任务的状态会变为 JobStateCancelling。JobStateRollingback,当 DDL Owner 发现 Job 的状态变为 JobStateCancelling 之后,它会将 job 的状态改变为 JobStateRollingback,以示已经开始处理 cancel 请求。JobStateRollbackDone,在做 cancel 的过程,也会涉及 Schema 状态的变更,也需要经历 Schema 的同步,等到状态回滚已经做完了,TiDB 会将 Job 的状态设置为 JobStateRollbackDone。对于 Binlog 而言,DDL 的 Binlog 输出机制,跟 DML 语句也是类似的,只有开始处理事务提交阶段,才会开始写 Binlog 出去。那么对于 DDL 来说,跟 DML 不一样,DML 有事务的概念,对于 DDL 来说,SQL 的事务是不影响 DDL 语句的。但是 DDL 里面,上面提到的 Job 的状态变更,是作为一个事务来提交的(保证状态一致性)。所以在每个状态变更,都会有一个事务与之对应,但是上面提到的中间状态,DDL 并不会往外写 Binlog,只有 JobStateRollbackDone 以及 JobStateDone 这两种状态,TiDB 会认为 DDL 语句已经完成,会对外发送 Binlog,发送之前,会把 Job 的状态从 JobStateDone 修改为 JobStateSynced,这次修改,也涉及一次事务提交。这块逻辑的代码如下:worker.handleDDLJobQueue():if job.IsDone() || job.IsRollbackDone() { binloginfo.SetDDLBinlog(d.binlogCli, txn, job.ID, job.Query) if !job.IsRollbackDone() { job.State = model.JobStateSynced } err = w.finishDDLJob(t, job) return errors.Trace(err)}type Binlog struct { DdlQuery []byte DdlJobId int64}DdlQuery 会设置为原始的 DDL 语句,DdlJobId 会设置为 DDL 的任务 ID。对于最后一次 Job 状态的提交,会有两条 Binlog 与之对应,这里有几种情况:如果事务提交成功,类型分别为 binlog.BinlogType_Prewrite 和 binlog.BinlogType_Commit。如果事务提交失败,类型分别为 binlog.BinlogType_Prewrite 和 binlog.BinlogType_Rollback。所以,Pumps 收到的 DDL Binlog,如果类型为 binlog.BinlogType_Rollback 应该只认为如下状态是合法的:JobStateDone (因为修改为 JobStateSynced 还未成功)JobStateRollbackDone如果类型为 binlog.BinlogType_Commit,应该只认为如下状态是合法的:JobStateSyncedJobStateRollbackDone当 TiDB 在提交最后一个 Job 状态的时候,如果事务提交失败了,那么 TiDB Owner 会尝试继续修改这个 Job,直到成功。也就是对于同一个 DdlJobId,后续还可能会有多次 Binlog,直到出现 binlog.BinlogType_Commit。 ...

January 16, 2019 · 3 min · jiezi

修改golang源代码实现无竞争版ThreadLocal

开篇书接上文 修改golang源代码获取goroutine id实现ThreadLocal。上文实现的版本由于map是多个goroutine共享的,存在竞争,影响了性能,实现思路类似java初期的ThreadLocal,今天我们借鉴现代版java的ThreadLocal来实现。思路先看看java里面怎么实现的可以看到每个线程实例都引用了一个map,map的key是ThreadLocal对象,value是实际存储的数据。下面我们也按照这个思路来实现,golang中g实例相当于java的Thread实例,我们可以修改g的结构来达到目的。实现修改g结构修改 $GOROOT/src/runtime/runtime2.go 文件,为g结构体添加 localMap *goroutineLocalMap 字段type g struct { // Stack parameters. // stack describes the actual stack memory: [stack.lo, stack.hi). // stackguard0 is the stack pointer compared in the Go stack growth prologue. // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption. // stackguard1 is the stack pointer compared in the C stack growth prologue. // It is stack.lo+StackGuard on g0 and gsignal stacks. // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash). stack stack // offset known to runtime/cgo stackguard0 uintptr // offset known to liblink stackguard1 uintptr // offset known to liblink _panic *_panic // innermost panic - offset known to liblink _defer *_defer // innermost defer m *m // current m; offset known to arm liblink sched gobuf syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc stktopsp uintptr // expected sp at top of stack, to check in traceback param unsafe.Pointer // passed parameter on wakeup atomicstatus uint32 stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus goid int64 schedlink guintptr waitsince int64 // approx time when the g become blocked waitreason waitReason // if status==Gwaiting preempt bool // preemption signal, duplicates stackguard0 = stackpreempt paniconfault bool // panic (instead of crash) on unexpected fault address preemptscan bool // preempted g does scan for gc gcscandone bool // g has scanned stack; protected by _Gscan bit in status gcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; TODO: remove? throwsplit bool // must not split stack raceignore int8 // ignore race detection events sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine sysexitticks int64 // cputicks when syscall has returned (for tracing) traceseq uint64 // trace event sequencer tracelastp puintptr // last P emitted an event for this goroutine lockedm muintptr sig uint32 writebuf []byte sigcode0 uintptr sigcode1 uintptr sigpc uintptr gopc uintptr // pc of go statement that created this goroutine ancestors *[]ancestorInfo // ancestor information goroutine(s) that created this goroutine (only used if debug.tracebackancestors) startpc uintptr // pc of goroutine function racectx uintptr waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order cgoCtxt []uintptr // cgo traceback context labels unsafe.Pointer // profiler labels timer *timer // cached timer for time.Sleep selectDone uint32 // are we participating in a select and did someone win the race? // Per-G GC state // gcAssistBytes is this G’s GC assist credit in terms of // bytes allocated. If this is positive, then the G has credit // to allocate gcAssistBytes bytes without assisting. If this // is negative, then the G must correct this by performing // scan work. We track this in bytes to make it fast to update // and check for debt in the malloc hot path. The assist ratio // determines how this corresponds to scan work debt. gcAssistBytes int64 localMap goroutineLocalMap //这是我们添加的}注意不要放在第一个字段,否则编译会出现 fatal: morestack on g0实现goroutineLocal在 $GOROOT/src/runtime/ 目录下创建go原文件 goroutine_local.gopackage runtimetype goroutineLocalMap struct { m map[goroutineLocal]interface{}}type goroutineLocal struct { initfun func() interface{}}func NewGoroutineLocal(initfun func() interface{}) goroutineLocal { return &goroutineLocal{initfun}}func (gl goroutineLocal)Get() interface{} { if getg().localMap == nil { getg().localMap = &goroutineLocalMap{make(map[goroutineLocal]interface{})} } v, ok := getg().localMap.m[gl] if !ok && gl.initfun != nil{ v = gl.initfun() } return v}func (gl goroutineLocal)Set(v interface{}) { if getg().localMap == nil { getg().localMap = &goroutineLocalMap{make(map[goroutineLocal]interface{})} } getg().localMap.m[gl] = v}func (gl goroutineLocal)Remove() { if getg().localMap != nil { delete(getg().localMap.m, gl) }}重新编译cd ~/go/srcGOROOT_BOOTSTRAP=’/Users/qiuxudong/go1.9’ ./all.bash写个mian函数测试一下package mainimport ( “fmt” “time” “runtime”)var gl = runtime.NewGoroutineLocal(func() interface{} { return “default”})func main() { gl.Set(“test0”) fmt.Println(runtime.GetGoroutineId(), gl.Get()) go func() { gl.Set(“test1”) fmt.Println(runtime.GetGoroutineId(), gl.Get()) gl.Remove() fmt.Println(runtime.GetGoroutineId(), gl.Get()) }() time.Sleep(2 * time.Second)}可以看到1 test018 test118 default同样的,这个版本也可能会内存泄露,建议主动调用Remove清除数据。但是如果goroutine销毁了,对应的数据不再被引用,是可以被GC清理的,泄露的概率降低很多。两种实现方式GC情况对比修改golang源代码获取goroutine id实现ThreadLocal 中的实现可以测试下泄露情况:package goroutine_localimport ( “testing” “fmt” “time” “runtime”)var gl = NewGoroutineLocal(func() interface{} { return make([]byte, 10241024)})func TestGoroutineLocal(t testing.T) { var stats runtime.MemStats runtime.ReadMemStats(&stats) go func() { for { runtime.GC() runtime.ReadMemStats(&stats) fmt.Printf(“HeapAlloc = %d\n”, stats.HeapAlloc) fmt.Printf(“NumGoroutine = %d\n”, runtime.NumGoroutine()) time.Sleep(1time.Second) } }() startAlloc() time.Sleep(10000 * time.Second)}func startAlloc() { for i := 0; i < 1000; i++ { runtime.GC() go func() { gl.Set(make([]byte, 1010241024)) fmt.Println(runtime.GetGoroutineId()) //gl.Remove() //故意不删除数据,观察是否泄露 time.Sleep(1 * time.Second) //模拟其它操作 }() time.Sleep(1 * time.Second) } fmt.Println(“done”)}结果:HeapAlloc = 98336NumGoroutine = 2HeapAlloc = 92280NumGoroutine = 41949HeapAlloc = 21070408NumGoroutine = 45HeapAlloc = 31556568NumGoroutine = 438HeapAlloc = 42043600NumGoroutine = 421HeapAlloc = 52529512NumGoroutine = 56HeapAlloc = 63015760NumGoroutine = 47HeapAlloc = 73500784NumGoroutine = 440HeapAlloc = 83986616NumGoroutine = 4…可以看到是持续上升的。本文实现的泄露情况测试代码:package mainimport ( “fmt” “time” “runtime”)var gl = runtime.NewGoroutineLocal(func() interface{} { return make([]byte, 1010241024)})func main() { var stats runtime.MemStats go func() { for { runtime.GC() runtime.ReadMemStats(&stats) fmt.Printf(“HeapAlloc = %d\n”, stats.HeapAlloc) fmt.Printf(“NumGoroutine = %d\n”, runtime.NumGoroutine()) time.Sleep(1time.Second) } }() startAlloc() time.Sleep(10000 * time.Second)}func startAlloc() { for i := 0; i < 1000; i++ { runtime.GC() go func() { gl.Set(make([]byte, 1010241024)) fmt.Println(runtime.GetGoroutineId()) //gl.Remove() //故意不删除数据,观察是否泄露 time.Sleep(1 * time.Second) //模拟其它操作 }() time.Sleep(1 * time.Second) } fmt.Println(“done”)}结果:…HeapAlloc = 178351296NumGoroutine = 3114HeapAlloc = 178351296NumGoroutine = 3112HeapAlloc = 188837800NumGoroutine = 3131HeapAlloc = 188837800NumGoroutine = 3145HeapAlloc = 178351296NumGoroutine = 3146HeapAlloc = 178351296NumGoroutine = 3HeapAlloc = 188837736NumGoroutine = 313258HeapAlloc = 178351296NumGoroutine = 3…可以看到 HeapAlloc 不是一直上升的,中间会有GC使其下降 ...

January 10, 2019 · 4 min · jiezi

玩转Elasticsearch源码-使用Intellij IDEA和remote debug调试源代码

开篇学习源码第一步就是搭建调试环境,但是看了网上大部分Elasticsearch调试方式都是配置各种环境变量然后直接启动Main方法,而且还各种报错。今天提供新的方式--remote debug来避免这些麻烦。步骤环境首先要安装jdk8,gradle和Intellij IDEA源码下载拉取代码,checkout到想要调试的版本(这里切到v6.1.0,需要注意的是不同ES分支对gradle版本要求不一样,可以到README文件中查看对应到gradle版本要求)git clone git@github.com/elastic/elasticsearchcd elasticsearchgit checkout v6.1.0导入到IDEA执行gradle idea,成功后会提示BUILD SUCCESSFUL,然后导入到IDEA:test:fixtures:hdfs-fixture:idea:test:fixtures:krb5kdc-fixture:ideaModule:test:fixtures:krb5kdc-fixture:idea:test:fixtures:old-elasticsearch:ideaModule:test:fixtures:old-elasticsearch:ideaBUILD SUCCESSFULTotal time: 2 mins 2.159 secs使用gradle启动Elasticsearchgradle run –debug-jvm执行成功后是这样的,其中8000就是远程debug端口配置remote debug点击IDEA的Edit Configurations,再点击➕填写主机和端口,Name是配置名称,可以自定义(我这里就填es),点OK保存配置搜一下源码里面Elasticsearch类,,看到Main方法,先打个断点等会看效果最后再点下绿色小虫子启动debug是不是在断点停下来了跳过断点再看下控制台,是不是启动日志都出来了再验证下是否启动成功原理一切源于被称作 Agent 的东西。JVM有一种特性,可以允许外部的库(Java或C++写的libraries)在运行时注入到 JVM 中。这些外部的库就称作 Agents, 他们有能力修改运行中 .class 文件的内容。这些 Agents 拥有的这些 JVM 的功能权限, 是在 JVM 内运行的 Java Code 所无法获取的, 他们能用来做一些有趣的事情,比如修改运行中的源码, 性能分析等。 像 JRebel 工具就是用了这些功能达到魔术般的效果。传递一个 Agent Lib 给 JVM, 通过添加 agentlib:libname[=options] 格式的启动参数即可办到。像上面的远程调试我们用的就是 -agentlib:jdwp=… 来引入 jdwp 这个 Agent 的。jdwp 是一个 JVM 特定的 JDWP(Java Debug Wire Protocol) 可选实现,用来定义调试者与运行JVM之间的通讯,它的是通过 JVM 本地库的 jdwp.so 或者 jdwp.dll 支持实现的。简单来说, jdwp agent 会建立运行应用的 JVM 和调试者(本地或者远程)之间的桥梁。既然他是一个Agent Library, 它就有能力拦截运行的代码。在 JVM 架构里, debugging 功能在 JVM 本身的内部是找不到的,它是一种抽象到外部工具的方式(也称作调试者 debugger)。这些调试工具或者运行在 JVM 的本地 或者在远程。这是一种解耦,模块化的架构。关于Agent还有很多值得研究的细节,甚至基于JVMTI自己实现。参考https://www.ibm.com/developerworks/cn/java/j-lo-jpda2/index.html ...

January 9, 2019 · 1 min · jiezi

Vue源码探究-组件的持久活跃

Vue源码探究-组件的持久活跃本篇代码位于vue/src/core/components/keep-alive.js较新版本的Vue增加了一个内置组件 keep-alive,用于存储组件状态,即便失活也能保持现有状态不变,切换回来的时候不会恢复到初始状态。由此可知,路由切换的钩子所触发的事件处理是无法适用于 keep-alive 组件的,那如果需要根据失活与否来给予组件事件通知,该怎么办呢?如前篇所述,keep-alive 组件有两个特有的生命周期钩子 activated 和 deactivated,用来响应失活状态的事件处理。来看看 keep-alive 组件的实现,代码文件位于 components 里,目前入口文件里也只有 keep-alive 这一个内置组件,但这个模块的分离,会不会预示着官方将在未来开发更多具有特殊功能的内置组件呢?// 导入辅助函数import { isRegExp, remove } from ‘shared/util’import { getFirstComponentChild } from ‘core/vdom/helpers/index’// 定义VNodeCache静态类型// 它是一个包含key名和VNode键值对的对象,可想而知它是用来存储组件的type VNodeCache = { [key: string]: ?VNode };// 定义getComponentName函数,用于获取组件名称,传入组件配置对象function getComponentName (opts: ?VNodeComponentOptions): ?string { // 先尝试获取配置对象中定义的name属性,或无则获取标签名称 return opts && (opts.Ctor.options.name || opts.tag)}// 定义matches函数,进行模式匹配,传入匹配的模式类型数据和name属性function matches (pattern: string | RegExp | Array<string>, name: string): boolean { // 匹配数组模式 if (Array.isArray(pattern)) { // 使用数组方法查找name,返回结果 return pattern.indexOf(name) > -1 } else if (typeof pattern === ‘string’) { // 匹配字符串模式 // 将字符串转换成数组查找name,返回结果 return pattern.split(’,’).indexOf(name) > -1 } else if (isRegExp(pattern)) { // 匹配正则表达式 // 使用正则匹配name,返回结果 return pattern.test(name) } / istanbul ignore next */ // 未匹配正确模式则返回false return false}// 定义pruneCache函数,修剪keep-alive组件缓存对象// 接受keep-alive组件实例和过滤函数function pruneCache (keepAliveInstance: any, filter: Function) { // 获取组件的cache,keys,_vnode属性 const { cache, keys, _vnode } = keepAliveInstance // 遍历cache对象 for (const key in cache) { // 获取缓存资源 const cachedNode: ?VNode = cache[key] // 如果缓存资源存在 if (cachedNode) { // 获取该资源的名称 const name: ?string = getComponentName(cachedNode.componentOptions) // 当名称存在 且不匹配缓存过滤时 if (name && !filter(name)) { // 执行修剪缓存资源操作 pruneCacheEntry(cache, key, keys, _vnode) } } }}// 定义pruneCacheEntry函数,修剪缓存条目// 接受keep-alive实例的缓存对象和键名缓存对象,资源键名和当前资源function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode) { // 检查缓存对象里是否已经有以key值存储的资源 const cached = cache[key] // 如果有旧资源并且没有传入新资源参数或新旧资源标签不同 if (cached && (!current || cached.tag !== current.tag)) { // 销毁该资源 cached.componentInstance.$destroy() } // 置空key键名存储资源 cache[key] = null // 移除key值的存储 remove(keys, key)}// 定义模式匹配接收的数据类型const patternTypes: Array<Function> = [String, RegExp, Array]// 导出keep-alive组件实例的配置对象export default { // 定义组件名称 name: ‘keep-alive’, // 设置abstract属性 abstract: true, // 设置组件接收的属性 props: { // include用于包含模式匹配的资源,启用缓存 include: patternTypes, // exclude用于排除模式匹配的资源,不启用缓存 exclude: patternTypes, // 最大缓存数 max: [String, Number] }, created () { // 实例创建时定义cache属性为空对象,用于存储资源 this.cache = Object.create(null) // 设置keys数组,用于存储资源的key名 this.keys = [] }, destroyed () { // 实例销毁时一并销毁存储的资源并清空缓存对象 for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { // DOM加载完成后,观察include和exclude属性的变动 // 回调执行修改缓存对象的操作 this.$watch(‘include’, val => { pruneCache(this, name => matches(val, name)) }) this.$watch(’exclude’, val => { pruneCache(this, name => !matches(val, name)) }) }, render () { // 实例渲染函数 // 获取keep-alive包含的子组件结构 // keep-alive组件并不渲染任何真实DOM节点,只渲染嵌套在其中的组件资源 const slot = this.$slots.default // 将嵌套组件dom结构转化成虚拟节点 const vnode: VNode = getFirstComponentChild(slot) // 获取嵌套组件的配置对象 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 如果配置对象存在 if (componentOptions) { // 检查是否缓存的模式匹配 // check pattern // 获取嵌套组件名称 const name: ?string = getComponentName(componentOptions) // 获取传入keep-alive组件的include和exclude属性 const { include, exclude } = this // 如果有included,且该组件不匹配included中资源 // 或者有exclude。且该组件匹配exclude中的资源 // 则返回虚拟节点,不继续执行缓存 if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } // 获取keep-alive组件的cache和keys对象 const { cache, keys } = this // 获取嵌套组件虚拟节点的key const key: ?string = vnode.key == null // 同样的构造函数可能被注册为不同的本地组件,所以cid不是判断的充分条件 // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? ::${componentOptions.tag} : ‘’) : vnode.key // 如果缓存对象里有以key值存储的组件资源 if (cache[key]) { // 设置当前嵌套组件虚拟节点的componentInstance属性 vnode.componentInstance = cache[key].componentInstance // make current key freshest // 从keys中移除旧key,添加新key remove(keys, key) keys.push(key) } else { // 缓存中没有该资源,则直接存储资源,并存储key值 cache[key] = vnode keys.push(key) // 如果设置了最大缓存资源数,从最开始的序号开始删除存储资源 // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } // 设置该资源虚拟节点的keepAlive标识 vnode.data.keepAlive = true } // 返回虚拟节点或dom节点 return vnode || (slot && slot[0]) }}keep-alive 组件的实现也就这百来行代码,分为两部分:第一部分是定义一些处理具体实现的函数,比如修剪缓存对象存储资源的函数,匹配组件包含和过滤存储的函数;第二部分是导出一份 keep-alive 组件的应用配置对象,仔细一下这跟我们在实际中使用的方式是一样的,但这个组件具有已经定义好的特殊功能,就是缓存嵌套在它之中的组件资源,实现持久活跃。那么实现原理是什么,在代码里可以清楚得看到,这里是利用转换组件真实DOM节点为虚拟节点将其存储到 keep-alive 实例的 cache 对象中,另外也一并存储了资源的 key 值方便查找,然后在渲染时检测其是否符合缓存条件再进行渲染。keep-alive 的实现就是以上这样简单。最初一瞥此段代码时,不知所云。然而当开始逐步分析代码之后,才发现原来只是没有仔细去看,误以为很深奥,由此可见,任何不用心的行为都不能直抵事物的本质,这是借由探索这一小部分代码而得到的教训。因为在实际中有使用过这个功能,所以体会更深,有时候难免会踩到一些坑,看了源码的实现之后,发现原来是自己使用方式不对,所以了解所用轮子的实现还是很有必要的。 ...

January 9, 2019 · 3 min · jiezi

【Dubbo源码阅读系列】之 Dubbo XML 配置加载

今天我们来谈谈 Dubbo XML 配置相关内容。关于这部分内容我打算分为以下几个部分进行介绍:Dubbo XMLSpring 自定义 XML 标签解析Dubbo 自定义 XML 标签解析DubboBeanDefinitionParser.parse()EndDubbo XML在本小节开始前我们先来看下 Dubbo XML 配置文件示例:dubbo-demo-provider.xml<?xml version=“1.0” encoding=“UTF-8”?><!–<beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo=“http://dubbo.apache.org/schema/dubbo" xmlns=“http://www.springframework.org/schema/beans" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"> <!– provider’s application name, used for tracing dependency relationship –> <dubbo:application name=“demo-provider”/> <!– use multicast registry center to export service –> <!–<dubbo:registry address=“multicast://224.5.6.7:1234”/>–> <dubbo:registry address=“zookeeper://10.14.22.68:2181”/> <!– use dubbo protocol to export service on port 20880 –> <dubbo:protocol name=“dubbo” port=“20880”/> <!– service implementation, as same as regular local bean –> <bean id=“demoService” class=“org.apache.dubbo.demo.provider.DemoServiceImpl”/> <!– declare the service interface to be exported –> <dubbo:service interface=“org.apache.dubbo.demo.DemoService” ref=“demoService”/></beans>在这段配置文件中有一些以 dubbo 开头的 xml 标签,直觉告诉我们这种标签和 dubbo 密切相关。那么这些标签的用途是什么?又是如何被识别的呢?我们结合 Spring 自定义 xml 标签实现相关内容来聊聊 Dubbo 是如何定义并加载这些自定义标签的。Spring 自定义 XML 标签解析Dubbo 中的自定义 XML 标签实际上是依赖于 Spring 解析自定义标签的功能实现的。网上关于 Spring 解析自定义 XML 标签的文章也比较多,这里我们仅介绍下实现相关功能需要的文件,给大家一个直观的印象,不去深入的对 Spring 自定义标签实现作详细分析。定义 xsd 文件XSD(XML Schemas Definition) 即 XML 结构定义。我们通过 XSD 文件不仅可以定义新的元素和属性,同时也使用它对我们的 XML 文件规范进行约束。在 Dubbo 项目中可以找类似实现:dubbo.xsdspring.schemas该配置文件约定了自定义命名空间和 xsd 文件之间的映射关系,用于 spring 容器感知我们自定义的 xsd 文件位置。http://dubbo.apache.org/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsdhttp://code.alibabatech.com/schema/dubbo/dubbo.xsd=META-INF/compat/dubbo.xsdspring.handlers该配置文件约定了自定义命名空间和 NamespaceHandler 类之间的映射关系。 NamespaceHandler 类用于注册自定义标签解析器。http://dubbo.apache.org/schema/dubbo=org.apache.dubbo.config.spring.schema.DubboNamespaceHandlerhttp://code.alibabatech.com/schema/dubbo=org.apache.dubbo.config.spring.schema.DubboNamespaceHandler命名空间处理器命名空间处理器主要用来注册 BeanDefinitionParser 解析器。对应上面 spring.handlers 文件中的 DubboNamespaceHandlerpublic class DubboNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { registerBeanDefinitionParser(“application”, new DubboBeanDefinitionParser(ApplicationConfig.class, true)); // 省略… registerBeanDefinitionParser(“annotation”, new AnnotationBeanDefinitionParser()); }}BeanDefinitionParser 解析器实现 BeanDefinitionParser 接口中的 parse 方法,用于自定义标签的解析。Dubbo 中对应 DubboBeanDefinitionParser 类。Dubbo 解析自定义 XML 标签终于进入到本文的重头戏环节了。在介绍 Dubbo 自定义 XML 标签解析前,先放一张图帮助大家理解以下 Spring 是如何从 XML 文件中解析并加载 Bean 的。上图言尽于 handler.parse() 方法,如果你仔细看了上文,对 parse() 应该是有印象的。 没错,在前一小结的第五点我们介绍了 DubboBeanDefinitionParser 类。该类有个方法就叫 parse()。那么这个 parse() 方法有什么用? Spring 是如何感知到我就要调用 DubboBeanDefinitionParser 类中的 parse() 方法的呢?我们带着这两个问题接着往下看。BeanDefinitionParserDelegate上面图的流程比较长,我们先着重看下 BeanDefinitionParserDelegate 类中的几个关键方法。BeanDefinitionParserDelegate.javapublic BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) { // 获取当前 element 的 namespaceURI // 比如 dubbo.xsd 中的为 http://dubbo.apache.org/schema/dubbo String namespaceUri = this.getNamespaceURI(ele); // 根据 URI 获取对应的 NamespaceHandler NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); if (handler == null) { this.error(“Unable to locate Spring NamespaceHandler for XML schema namespace [” + namespaceUri + “]”, ele); return null; } else { return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); }}这个方法干了三件事获取 element 元素的 namespaceURI,并据此获取对应的 NamespaceHandler 对象。Dubbo 自定义标签(比如 Dubbo:provider) namespaceUri 的值为 http://dubbo.apache.org/schema/dubbo;根据 step1 获取到的 namespaceUri ,获取对应的 NamespaceHandler 对象。这里会调用 DefaultNamespaceHandlerResolver 类的 resolve() 方法,我们下面会分析;调用 handler 的 parse 方法,我们自定以的 handler 会继承 NamespaceHandlerSupport 类,所以这里调用的其实是 NamespaceHandlerSupport 类的 parse() 方法,后文分析;一图胜千言 在详细分析 step2 和 step3 中涉及的 resolver() 和 parse() 方法前,先放一张时序图让大家有个基本概念:DefaultNamespaceHandlerResolver.javapublic NamespaceHandler resolve(String namespaceUri) { Map<String, Object> handlerMappings = this.getHandlerMappings(); // 以 namespaceUri 为 Key 获取对应的 handlerOrClassName Object handlerOrClassName = handlerMappings.get(namespaceUri); if (handlerOrClassName == null) { return null; } else if (handlerOrClassName instanceof NamespaceHandler) { return (NamespaceHandler)handlerOrClassName; } else { // 如果不为空且不为 NamespaceHandler 的实例,转换为 String 类型 // DubboNamespaceHandler 执行的便是这段逻辑 String className = (String)handlerOrClassName; try { Class<?> handlerClass = ClassUtils.forName(className, this.classLoader); // handlerClass 是否为 NamespaceHandler 的实现类,若不是则抛出异常 if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) { throw new FatalBeanException(“Class [” + className + “] for namespace [” + namespaceUri + “] does not implement the [” + NamespaceHandler.class.getName() + “] interface”); } else { // 初始化 handlerClass NamespaceHandler namespaceHandler = (NamespaceHandler)BeanUtils.instantiateClass(handlerClass); // 执行 handlerClass类的 init() 方法 namespaceHandler.init(); handlerMappings.put(namespaceUri, namespaceHandler); return namespaceHandler; } } catch (ClassNotFoundException var7) { throw new FatalBeanException(“NamespaceHandler class [” + className + “] for namespace [” + namespaceUri + “] not found”, var7); } catch (LinkageError var8) { throw new FatalBeanException(“Invalid NamespaceHandler class [” + className + “] for namespace [” + namespaceUri + “]: problem with handler class file or dependent class”, var8); } }}resolve() 方法用途是根据方法参数中的 namespaceUri 获取对应的 NamespaceHandler 对象。这里会先尝试以 namespaceUri 为 key 去 handlerMappings 集合中取对象。如果 handlerOrClassName 不为 null 且不为 NamespaceHandler 的实例。那么尝试将 handlerOrClassName 作为 className 并调用 BeanUtils.instantiateClass() 方法初始化一个NamespaceHandler 实例。初始化后,调用其 init() 方法。这个 init() 方法比较重要,我们接着往下看。DubboNamespaceHandlerpublic void init() { registerBeanDefinitionParser(“application”, new DubboBeanDefinitionParser(ApplicationConfig.class, true)); registerBeanDefinitionParser(“module”, new DubboBeanDefinitionParser(ModuleConfig.class, true)); registerBeanDefinitionParser(“registry”, new DubboBeanDefinitionParser(RegistryConfig.class, true)); registerBeanDefinitionParser(“monitor”, new DubboBeanDefinitionParser(MonitorConfig.class, true)); registerBeanDefinitionParser(“provider”, new DubboBeanDefinitionParser(ProviderConfig.class, true)); registerBeanDefinitionParser(“consumer”, new DubboBeanDefinitionParser(ConsumerConfig.class, true)); registerBeanDefinitionParser(“protocol”, new DubboBeanDefinitionParser(ProtocolConfig.class, true)); registerBeanDefinitionParser(“service”, new DubboBeanDefinitionParser(ServiceBean.class, true)); registerBeanDefinitionParser(“reference”, new DubboBeanDefinitionParser(ReferenceBean.class, false)); registerBeanDefinitionParser(“annotation”, new AnnotationBeanDefinitionParser());}NamespaceHandlerSupportprivate final Map<String, BeanDefinitionParser> parsers = new HashMap();protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { this.parsers.put(elementName, parser);}DubboNamespaceHandler 类中的 init() 方法干的事情特别简单,就是新建 DubboBeanDefinitionParser 对象并将其放入 NamespaceHandlerSupport 类的 parsers 集合中。我们再回顾一下 parseCustomElement() 方法。BeanDefinitionParserDelegate.javapublic BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) { // 省略… return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); // 省略…}这里会调用 NamespaceHandlerSupport 类的 parse() 方法。我们继续跟踪一下。public BeanDefinition parse(Element element, ParserContext parserContext) { return this.findParserForElement(element, parserContext).parse(element, parserContext);}private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { String localName = parserContext.getDelegate().getLocalName(element); BeanDefinitionParser parser = (BeanDefinitionParser)this.parsers.get(localName); if (parser == null) { parserContext.getReaderContext().fatal(“Cannot locate BeanDefinitionParser for element [” + localName + “]”, element); } return parser;}看到这里大家有没有一丝豁然开朗的感觉?之前的 resolve() 方法实际上就是根据当前 element 的 namespaceURI 获取对应的 NamespaceHandler 对象(对于 Dubbo 来说是 DubboNamespaceHandler),然后调用 DubboNamespaceHandler 中的 init() 方法新建 DubboBeanDefinitionParser 对象并注册到 NamespaceHandlerSupport 类的 parsers 集合中。然后 parser 方法会根据当前 element 对象从 parsers 集合中获取合适的 BeanDefinitionParser 对象。对于 Dubbo 元素来说,实际上最后执行的是 DubboBeanDefinitionParser 的 parse() 方法。DubboBeanDefinitionParser.parse()最后我们再来看看 Dubbo 解析 XML 文件的详细实现吧。如果对具体实现没有兴趣可直接直接跳过。private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) { RootBeanDefinition beanDefinition = new RootBeanDefinition(); beanDefinition.setBeanClass(beanClass); beanDefinition.setLazyInit(false); String id = element.getAttribute(“id”); // DubboBeanDefinitionParser 构造方法中有对 required 值进行初始化; // DubboNamespaceHandler 类中的 init 方法会创建并注册 DubboBeanDefinitionParser 类 if ((id == null || id.length() == 0) && required) { String generatedBeanName = element.getAttribute(“name”); if (generatedBeanName == null || generatedBeanName.length() == 0) { if (ProtocolConfig.class.equals(beanClass)) { generatedBeanName = “dubbo”; } else { // name 属性为空且不为 ProtocolConfig 类型,取 interface 值 generatedBeanName = element.getAttribute(“interface”); } } if (generatedBeanName == null || generatedBeanName.length() == 0) { // 获取 beanClass 的全限定类名 generatedBeanName = beanClass.getName(); } id = generatedBeanName; int counter = 2; while (parserContext.getRegistry().containsBeanDefinition(id)) { id = generatedBeanName + (counter++); } } if (id != null && id.length() > 0) { if (parserContext.getRegistry().containsBeanDefinition(id)) { throw new IllegalStateException(“Duplicate spring bean id " + id); } // 注册 beanDefinition parserContext.getRegistry().registerBeanDefinition(id, beanDefinition); // 为 beanDefinition 添加 id 属性 beanDefinition.getPropertyValues().addPropertyValue(“id”, id); } // 如果当前 beanClass 类型为 ProtocolConfig // 遍历已经注册过的 bean 对象,如果 bean 对象含有 protocol 属性 // protocol 属性值为 ProtocolConfig 实例且 name 和当前 id 值一致,为当前 beanClass 对象添加 protocl 属性 if (ProtocolConfig.class.equals(beanClass)) { for (String name : parserContext.getRegistry().getBeanDefinitionNames()) { BeanDefinition definition = parserContext.getRegistry().getBeanDefinition(name); PropertyValue property = definition.getPropertyValues().getPropertyValue(“protocol”); if (property != null) { Object value = property.getValue(); if (value instanceof ProtocolConfig && id.equals(((ProtocolConfig) value).getName())) { definition.getPropertyValues().addPropertyValue(“protocol”, new RuntimeBeanReference(id)); } } } } else if (ServiceBean.class.equals(beanClass)) { // 如果当前元素包含 class 属性,调用 ReflectUtils.forName() 方法加载类对象 // 调用 parseProperties 解析其他属性设置到 classDefinition 对象中 // 最后设置 beanDefinition 的 ref 属性为 BeanDefinitionHolder 包装类 String className = element.getAttribute(“class”); if (className != null && className.length() > 0) { RootBeanDefinition classDefinition = new RootBeanDefinition(); classDefinition.setBeanClass(ReflectUtils.forName(className)); classDefinition.setLazyInit(false); parseProperties(element.getChildNodes(), classDefinition); beanDefinition.getPropertyValues().addPropertyValue(“ref”, new BeanDefinitionHolder(classDefinition, id + “Impl”)); } } else if (ProviderConfig.class.equals(beanClass)) { parseNested(element, parserContext, ServiceBean.class, true, “service”, “provider”, id, beanDefinition); } else if (ConsumerConfig.class.equals(beanClass)) { parseNested(element, parserContext, ReferenceBean.class, false, “reference”, “consumer”, id, beanDefinition); } Set<String> props = new HashSet<String>(); ManagedMap parameters = null; for (Method setter : beanClass.getMethods()) { String name = setter.getName(); if (name.length() > 3 && name.startsWith(“set”) && Modifier.isPublic(setter.getModifiers()) && setter.getParameterTypes().length == 1) { Class<?> type = setter.getParameterTypes()[0]; String propertyName = name.substring(3, 4).toLowerCase() + name.substring(4); String property = StringUtils.camelToSplitName(propertyName, “-”); props.add(property); Method getter = null; try { getter = beanClass.getMethod(“get” + name.substring(3), new Class<?>[0]); } catch (NoSuchMethodException e) { try { getter = beanClass.getMethod(“is” + name.substring(3), new Class<?>[0]); } catch (NoSuchMethodException e2) { } } if (getter == null || !Modifier.isPublic(getter.getModifiers()) || !type.equals(getter.getReturnType())) { continue; } if (“parameters”.equals(property)) { parameters = parseParameters(element.getChildNodes(), beanDefinition); } else if (“methods”.equals(property)) { parseMethods(id, element.getChildNodes(), beanDefinition, parserContext); } else if (“arguments”.equals(property)) { parseArguments(id, element.getChildNodes(), beanDefinition, parserContext); } else { String value = element.getAttribute(property); if (value != null) { value = value.trim(); if (value.length() > 0) { // 如果属性为 registry,且 registry 属性的值为"N/A”,标识不会注册到任何注册中心 // 新建 RegistryConfig 并将其设置为 beanDefinition 的 registry 属性 if (“registry”.equals(property) && RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(value)) { RegistryConfig registryConfig = new RegistryConfig(); registryConfig.setAddress(RegistryConfig.NO_AVAILABLE); beanDefinition.getPropertyValues().addPropertyValue(property, registryConfig); } else if (“registry”.equals(property) && value.indexOf(’,’) != -1) { // 多注册中心解析 parseMultiRef(“registries”, value, beanDefinition, parserContext); } else if (“provider”.equals(property) && value.indexOf(’,’) != -1) { parseMultiRef(“providers”, value, beanDefinition, parserContext); } else if (“protocol”.equals(property) && value.indexOf(’,’) != -1) { // 多协议 parseMultiRef(“protocols”, value, beanDefinition, parserContext); } else { Object reference; if (isPrimitive(type)) { // type 为方法参数,type 类型是否为基本类型 if (“async”.equals(property) && “false”.equals(value) || “timeout”.equals(property) && “0”.equals(value) || “delay”.equals(property) && “0”.equals(value) || “version”.equals(property) && “0.0.0”.equals(value) || “stat”.equals(property) && “-1”.equals(value) || “reliable”.equals(property) && “false”.equals(value)) { // 新老版本 xsd 兼容性处理 // backward compatibility for the default value in old version’s xsd value = null; } reference = value; } else if (“protocol”.equals(property) && ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(value) && (!parserContext.getRegistry().containsBeanDefinition(value) || !ProtocolConfig.class.getName().equals(parserContext.getRegistry().getBeanDefinition(value).getBeanClassName()))) { // 如果 protocol 属性值有对应的扩展实现,而且没有被注册到 spring 注册表中 // 或者 spring 注册表中对应的 bean 的类型不为 ProtocolConfig.class if (“dubbo:provider”.equals(element.getTagName())) { logger.warn(“Recommended replace <dubbo:provider protocol="” + value + “" … /> to <dubbo:protocol name="” + value + “" … />”); } // backward compatibility ProtocolConfig protocol = new ProtocolConfig(); protocol.setName(value); reference = protocol; } else if (“onreturn”.equals(property)) { int index = value.lastIndexOf(”.”); String returnRef = value.substring(0, index); String returnMethod = value.substring(index + 1); reference = new RuntimeBeanReference(returnRef); beanDefinition.getPropertyValues().addPropertyValue(“onreturnMethod”, returnMethod); } else if (“onthrow”.equals(property)) { int index = value.lastIndexOf(”.”); String throwRef = value.substring(0, index); String throwMethod = value.substring(index + 1); reference = new RuntimeBeanReference(throwRef); beanDefinition.getPropertyValues().addPropertyValue(“onthrowMethod”, throwMethod); } else if (“oninvoke”.equals(property)) { int index = value.lastIndexOf("."); String invokeRef = value.substring(0, index); String invokeRefMethod = value.substring(index + 1); reference = new RuntimeBeanReference(invokeRef); beanDefinition.getPropertyValues().addPropertyValue(“oninvokeMethod”, invokeRefMethod); } else { // 如果 ref 属性值已经被注册到 spring 注册表中 if (“ref”.equals(property) && parserContext.getRegistry().containsBeanDefinition(value)) { BeanDefinition refBean = parserContext.getRegistry().getBeanDefinition(value); // 非单例抛出异常 if (!refBean.isSingleton()) { throw new IllegalStateException(“The exported service ref " + value + " must be singleton! Please set the " + value + " bean scope to singleton, eg: <bean id="” + value + “" scope="singleton" …>”); } } reference = new RuntimeBeanReference(value); } beanDefinition.getPropertyValues().addPropertyValue(propertyName, reference); } } } } } } NamedNodeMap attributes = element.getAttributes(); int len = attributes.getLength(); for (int i = 0; i < len; i++) { Node node = attributes.item(i); String name = node.getLocalName(); if (!props.contains(name)) { if (parameters == null) { parameters = new ManagedMap(); } String value = node.getNodeValue(); parameters.put(name, new TypedStringValue(value, String.class)); } } if (parameters != null) { beanDefinition.getPropertyValues().addPropertyValue(“parameters”, parameters); } return beanDefinition; }上面这一大段关于配置的解析的代码需要大家自己结合实际的代码进行调试才能更好的理解。我在理解 Dubbo XML 解析的时候,也是耐着性子一遍一遍的来。 关于 ProtocolConfig 和 protocol 加载先后顺序的问题最后再集合一个小例子总结下吧: dubbo-demo-provider.xml <dubbo:protocol name=“dubbo” port=“20880”/>当我们先解析了 ProtocolConfig 元素时,我们会遍历所有已经注册 spring 注册表中 bean。如果 bean 对象存在 protocol 属性且与 name 和当前 ProtolConfig id 匹配,则会新建 RuntimeBeanReference 对象覆盖 protocol 属性。对于上面这行配置,最后会新建一个拥有 name 和 port 的 beanDefinition 对象。先解析了 protocol 元素,ProtocolConfig 未被解析。此时我们在 spring 注册表中找不到对应的 ProtocolConfig bean。此时我们将需要新建一个 ProtocolConfig 并将其 name 属性设置为当前属性值。最后将其设置为 beanDefinition 对象的 protocol 属性。后面加载到了 ProtocolConfig 元素时,会替换 protocol 的值。EndDubbo 对于自定义 XML 标签的定义和解析实际上借助了 Spring 框架对自定义 XML 标签的支持。本篇水文虽然又臭又长,但是对于理解 Dubbo 的初始化过程还是很重要的。后面我们会介绍关于 Dubbo 服务暴露相关内容。本BLOG上原创文章未经本人许可,不得用于商业用途及传统媒体。网络媒体转载请注明出处,否则属于侵权行为。https://juejin.im/post/5c1753… ...

January 2, 2019 · 8 min · jiezi

【Dubbo源码阅读系列】之 Dubbo SPI 机制

最近抽空开始了 Dubbo 源码的阅读之旅,希望可以通过写文章的方式记录和分享自己对 Dubbo 的理解。如果在本文出现一些纰漏或者错误之处,也希望大家不吝指出。Dubbo SPI 介绍Java SPI在阅读本文之前可能需要你对 Java SPI(Service Provider Interface) 机制有过简单的了解。这里简单介绍下:在面向对象的设计中,我们提倡模块之间基于接口编程。不同模块可能会有不同的具体实现,但是为了避免模块的之间的耦合过大,我们需要一种有效的服务(服务实现)发现机制来选择具体模块。SPI 就是这样一种基于接口编程+策略模式+配置文件,同时可供使用者根据自己的实际需要启用/替换模块具体实现的方案。Dubbo SPI 的改进点以下内容摘录自 https://dubbo.gitbooks.io/dub… Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点在 Dubbo 中,如果某个 interface 接口标记了 @SPI 注解,那么我们认为它是 Dubbo 中的一个扩展点。扩展点是 Dubbo SPI 的核心,下面我们就扩展点加载、扩展点自动包装、扩展点自动装配几方面来聊聊具体实现。Dubbo SPI 机制详解Dubbo 扩展点的加载在阅读本文前,如果你阅读过Java SPI 相关内容,大概能回忆起来有 /META-INF/services 这样一个目录。在这个目录下有一个以接口命名的文件,文件的内容为接口具体实现类的全限定名。在 Dubbo 中我们也能找到类似的设计。META-INF/services/(兼容JAVA SPI)META-INF/dubbo/(自定义扩展点实现)META-INF/dubbo/internal/(Dubbo内部扩展点实现)非常好~我们现在已经知道了从哪里加载扩展点了,再回忆一下,JAVA SPI是如何加载的。ServiceLoader<DubboService> spiLoader = ServiceLoader.load(XXX.class);类似的,在 Dubbo 中也有这样一个用于加载扩展点的类 ExtensionLoader。这一章节,我们会着重了解一下这个类到底是如何帮助我们加载扩展点的。我们先来看一段简短的代码。Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();在 Dubbo 的实现里面用到了大量类似的代码片段,我们只需要提供一个 type ,即可获取该 type 的自适应(关于自适应的理解在后文会提到)扩展类。在获取对应自适应扩展类时,我们首先获取该类型的 ExtensionLoader。看到这里我们应该下意识的感觉到对于每个 type 来说,都应该有一个对应的 ExtensionLoader 对象。我们先来看看 ExtensionLoader 是如何获取的。getExtensionLoader()public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { if (type == null) { throw new IllegalArgumentException(“Extension type == null”); } if (!type.isInterface()) { throw new IllegalArgumentException(“Extension type(” + type + “) is not interface!”); } // 是否被 SPI 注解标识 if (!withExtensionAnnotation(type)) { throw new IllegalArgumentException(“Extension type(” + type + “) is not extension, because WITHOUT @” + SPI.class.getSimpleName() + " Annotation!"); } //EXTENSION_LOADERS 为一个 ConcurrentMap集合,key 为 Class 对象,value 为ExtenLoader 对象 ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); if (loader == null) { EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); } return loader;}private ExtensionLoader(Class<?> type) { this.type = type; objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());}上面这一段的代码比较简单,根据 type 从 EXTENSION_LOADERS 集合中获取 loader ,如果返回的值为 null 则新建一个 ExtensionLoader 对象。这里的 objectFactory 获取也用到了类似的方法,获取到了 ExtensionFactory 的扩展自适应类。getAdaptiveExtension()public T getAdaptiveExtension() { //cachedAdaptiveInstance用于缓存自适应扩展类实例 Object instance = cachedAdaptiveInstance.get(); if (instance == null) { if (createAdaptiveInstanceError == null) { synchronized (cachedAdaptiveInstance) { instance = cachedAdaptiveInstance.get(); if (instance == null) { try { instance = createAdaptiveExtension(); cachedAdaptiveInstance.set(instance); } catch (Throwable t) { // … } } } } return (T) instance;}getAdaptiveExtension()方法用于获取当前自适应扩展类实例,首先会从 cachedAdaptiveInstance 对象中获取,如果值为 null 同时 createAdaptiveInstanceError 为空,则调用 createAdaptiveExtension 方法创建扩展类实例。创建完后更新 cachedAdaptiveInstance 。createAdaptiveExtension()private T createAdaptiveExtension() { try { return injectExtension((T) getAdaptiveExtensionClass().newInstance()); } catch (Exception e) { // 省略异常 }}这里有两个方法值得我们关注,injectExtension() 和 getAdaptiveExtensionClass()。injectExtension() 看名字像是一个实现了注入功能的方法,而 getAdaptiveExtensionClass() 则用于获取具体的自适应扩展类。我们依次看下这两个方法。injectExtension()private T injectExtension(T instance) { try { if (objectFactory != null) { for (Method method : instance.getClass().getMethods()) { if (method.getName().startsWith(“set”) && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) { //如果存在 DisableInject 注解则跳过 if (method.getAnnotation(DisableInject.class) != null) { continue; } //获取 method 第一个参数的类型 Class<?> pt = method.getParameterTypes()[0]; try { String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : “”; Object object = objectFactory.getExtension(pt, property); if (object != null) { method.invoke(instance, object); } } catch (Exception e) { logger.error(“fail to inject via method " + method.getName() + " of interface " + type.getName() + “: " + e.getMessage(), e); } } } } } catch (Exception e) { logger.error(e.getMessage(), e); } return instance;}简单的总结下这个方法做了什么:遍历当前实例的 set 方法,以 set 方法第四位开始至末尾的字符串为关键字,尝试通过 objectFactory 来获取对应的 扩展类实现。如果存在对应扩展类,通过反射注入到当前实例中。这个方法相当于完成了一个简单的依赖注入功能,我们常说 Dubbo 中的 IOC 实际上也是在这里体现的。getAdaptiveExtensionClass()private Class<?> getAdaptiveExtensionClass() { getExtensionClasses(); if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } return cachedAdaptiveClass = createAdaptiveExtensionClass();}接着看 getAdaptiveExtensionClass() 方法。首先调用 getExtensionClasses() 方法,如果 cachedAdaptiveClass() 不为 null 则返回,如果为 null 则调用 createAdaptiveExtensionClass() 方法。依次看下这两个方法。getExtensionClasses()private Map<String, Class<?>> getExtensionClasses() { Map<String, Class<?>> classes = cachedClasses.get(); if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes;}内容比较简单,我们直接看 loadExtensionClasses() 方法。private Map<String, Class<?>> loadExtensionClasses() { final SPI defaultAnnotation = type.getAnnotation(SPI.class); if (defaultAnnotation != null) { String value = defaultAnnotation.value(); if ((value = value.trim()).length() > 0) { String[] names = NAME_SEPARATOR.split(value); // 只能有一个默认扩展实例 if (names.length > 1) { throw new IllegalStateException(“more than 1 default extension name on extension " + type.getName() + “: " + Arrays.toString(names)); } if (names.length == 1) { cachedDefaultName = names[0]; } } } Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>(); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace(“org.apache”, “com.alibaba”)); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace(“org.apache”, “com.alibaba”)); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName()); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace(“org.apache”, “com.alibaba”)); return extensionClasses;}绕来绕去这么久,终于要进入主题了。为什么说进入主题了呢?看看这几个变量的值DUBBO_INTERNAL_DIRECTORY:META-INF/dubbo/internal/DUBBO_DIRECTORY:META-INF/dubbo/SERVICES_DIRECTORY:META-INF/services/熟悉的配方熟悉的料。。没错了,我们马上就要开始读取这三个目录下的文件,然后开始加载我们的扩展点了。loadDirectory()private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) { String fileName = dir + type; try { Enumeration<java.net.URL> urls; ClassLoader classLoader = findClassLoader(); if (classLoader != null) { urls = classLoader.getResources(fileName); } else { urls = ClassLoader.getSystemResources(fileName); } if (urls != null) { while (urls.hasMoreElements()) { java.net.URL resourceURL = urls.nextElement(); loadResource(extensionClasses, classLoader, resourceURL); } } } catch (Throwable t) { // … }}private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { try { BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), “utf-8”)); try { String line; while ((line = reader.readLine()) != null) { //#对应注释位置,剔除存在的注释 final int ci = line.indexOf(’#’); if (ci >= 0) { line = line.substring(0, ci); } line = line.trim(); if (line.length() > 0) { try { String name = null; //文件中的内容以 key=value 的形式保存,拆分 key 和 vlaue int i = line.indexOf(’=’); if (i > 0) { name = line.substring(0, i).trim(); line = line.substring(i + 1).trim(); } if (line.length() > 0) { loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); } } catch (Throwable t) { // … } } } } finally { reader.close(); } } catch (Throwable t) { // … }}private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { // 用于判断 class 是不是 type 接口的实现类 if (!type.isAssignableFrom(clazz)) { throw new IllegalStateException(“Error when load extension class(interface: " + type + “, class line: " + clazz.getName() + “), class " + clazz.getName() + “is not subtype of interface.”); } // 如果当前 class 被 @Adaptive 注解标记,更新 cachedAdaptiveClass 缓存对象 if (clazz.isAnnotationPresent(Adaptive.class)) { if (cachedAdaptiveClass == null) { cachedAdaptiveClass = clazz; } else if (!cachedAdaptiveClass.equals(clazz)) { // 省略异常 } } else if (isWrapperClass(clazz)) { // 这里涉及到了 Dubbo 扩展点的另一个机制:包装,在后文介绍 Set<Class<?>> wrappers = cachedWrapperClasses; if (wrappers == null) { cachedWrapperClasses = new ConcurrentHashSet<Class<?>>(); wrappers = cachedWrapperClasses; } wrappers.add(clazz); } else { clazz.getConstructor(); // 如果 name 为空,调用 findAnnotationName() 方法。如果当前类有 @Extension 注解,直接返回 @Extension 注解value; // 若没有 @Extension 注解,但是类名类似 xxxType(Type 代表 type 的类名),返回值为小写的 xxx if (name == null || name.length() == 0) { name = findAnnotationName(clazz); if (name.length() == 0) { throw new IllegalStateException(“No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); } } String[] names = NAME_SEPARATOR.split(name); if (names != null && names.length > 0) { // @Activate 注解用于配置扩展被自动激活条件 // 如果当前 class 包含 @Activate ,加入到缓存中 Activate activate = clazz.getAnnotation(Activate.class); if (activate != null) { cachedActivates.put(names[0], activate); } else { // support com.alibaba.dubbo.common.extension.Activate com.alibaba.dubbo.common.extension.Activate oldActivate = clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class); if (oldActivate != null) { cachedActivates.put(names[0], oldActivate); } } for (String n : names) { if (!cachedNames.containsKey(clazz)) { cachedNames.put(clazz, n); } Class<?> c = extensionClasses.get(n); if (c == null) { // 还记得文件内容长啥样吗?(name = calssvalue),我们最后将其保存到了 extensionClasses 集合中 extensionClasses.put(n, clazz); } else if (c != clazz) { // … } } } }}这一段代码真的相当长啊。。梳理下之后发现其实他做的事情也很简单:拼接生成文件名:dir + type,读取该文件读取文件内容,将文件内容拆分为 name 和 class 字符串如果 clazz 类中包含 @Adaptive 注解,将其加入到 cachedAdaptiveClass 缓存中 如果 clazz 类中为包装类,添加到 wrappers 中 如果文件不为 key=class 形式,会尝试通过 @Extension 注解获取 name 如果 clazz 包含 @Activate 注解(兼容 com.alibaba.dubbo.common.extension.Activate 注解),将其添加到 cachedActivates 缓存中最后以 name 为 key ,clazz 为 vlaue,将其添加到 extensionClasses 集合中并返回获取自适应扩展类getAdaptiveExtensionClass()private Class<?> getAdaptiveExtensionClass() { getExtensionClasses(); if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } return cachedAdaptiveClass = createAdaptiveExtensionClass();}Ok,我们已经分析了 getExtensionClasses 方法,并且已经将扩展点实现加载到了缓存中。这个方法是由 getAdaptiveExtensionClass() 方法引出来的,它看起来是像是创建自适应扩展类的。这里会先判断缓存对象 cachedAdaptiveClass 是否会空,cachedAdaptiveClass 是什么时候被初始化的呢?回顾一下之前的代码:private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { // 省略… if (clazz.isAnnotationPresent(Adaptive.class)) { if (cachedAdaptiveClass == null) { cachedAdaptiveClass = clazz; } else if (!cachedAdaptiveClass.equals(clazz)) { // 省略… } }}在 loadClass() 方法中如果发现当前 clazz 包含 @Adaptive 注解,则将当前 clazz 作为缓存自适应类保存。例如在 AdaptiveExtensionFactory 类中就有这么用,我们会将 AdaptiveExtensionFactory 类作为 ExtensionFactory 类型的自适应类缓存起来。@Adaptivepublic class AdaptiveExtensionFactory implements ExtensionFactory我们继续分析该方法的后部分。如果 cachedAdaptiveClass 为 null,则会调用 createAdaptiveExtensionClass() 方法动态生成一个自适应扩展类。private Class<?> createAdaptiveExtensionClass() { String code = createAdaptiveExtensionClassCode(); System.out.println(code); ClassLoader classLoader = findClassLoader(); org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); return compiler.compile(code, classLoader);}这一段代码在本次分享中不打算重点叙述,可以简单的理解为 dubbo 帮我生成了一个自适应类。我摘取了生成的一段代码,如下所示:package org.apache.dubbo.rpc;import org.apache.dubbo.common.extension.ExtensionLoader;public class ProxyFactory$Adaptive implements org.apache.dubbo.rpc.ProxyFactory { private static final org.apache.dubbo.common.logger.Logger logger = org.apache.dubbo.common.logger.LoggerFactory.getLogger(ExtensionLoader.class); private java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(0); public org.apache.dubbo.rpc.Invoker getInvoker(java.lang.Object arg0, java.lang.Class arg1, org.apache.dubbo.common.URL arg2) throws org.apache.dubbo.rpc.RpcException { if (arg2 == null) throw new IllegalArgumentException(“url == null”); org.apache.dubbo.common.URL url = arg2; String extName = url.getParameter(“proxy”, “javassist”); if(extName == null) throw new IllegalStateException(“Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(” + url.toString() + “) use keys([proxy])”); org.apache.dubbo.rpc.ProxyFactory extension = null; try { extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName); }catch(Exception e){ if (count.incrementAndGet() == 1) { logger.warn(“Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.ProxyFactory, will use default extension javassist instead.”, e); } extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(“javassist”); } return extension.getInvoker(arg0, arg1, arg2); } public java.lang.Object getProxy(org.apache.dubbo.rpc.Invoker arg0, boolean arg1) throws org.apache.dubbo.rpc.RpcException { if (arg0 == null) throw new IllegalArgumentException(“org.apache.dubbo.rpc.Invoker argument == null”); if (arg0.getUrl() == null) throw new IllegalArgumentException(“org.apache.dubbo.rpc.Invoker argument getUrl() == null”);org.apache.dubbo.common.URL url = arg0.getUrl(); String extName = url.getParameter(“proxy”, “javassist”); if(extName == null) throw new IllegalStateException(“Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(” + url.toString() + “) use keys([proxy])”); org.apache.dubbo.rpc.ProxyFactory extension = null; try { extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName); }catch(Exception e){ if (count.incrementAndGet() == 1) { logger.warn(“Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.ProxyFactory, will use default extension javassist instead.”, e); } extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(“javassist”); } return extension.getProxy(arg0, arg1); } public java.lang.Object getProxy(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException { if (arg0 == null) throw new IllegalArgumentException(“org.apache.dubbo.rpc.Invoker argument == null”); if (arg0.getUrl() == null) throw new IllegalArgumentException(“org.apache.dubbo.rpc.Invoker argument getUrl() == null”);org.apache.dubbo.common.URL url = arg0.getUrl(); String extName = url.getParameter(“proxy”, “javassist”); if(extName == null) throw new IllegalStateException(“Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(” + url.toString() + “) use keys([proxy])”); org.apache.dubbo.rpc.ProxyFactory extension = null; try { extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName); }catch(Exception e){ if (count.incrementAndGet() == 1) { logger.warn(“Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.ProxyFactory, will use default extension javassist instead.”, e); } extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(“javassist”); } return extension.getProxy(arg0); }}extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);这一段代码实际上才是自适应适配类的精髓,看看 extName 是怎么来的?String extName = url.getParameter(“proxy”, “javassist”);extName 又是从 url 中取得的,实际上 url 对于 Dubbo 来说是一种非常重要的上下文传输载体,在后续系列文章中大家会逐步感受到。public T getExtension(String name) { if (name == null || name.length() == 0) { throw new IllegalArgumentException(“Extension name == null”); } if (“true”.equals(name)) { return getDefaultExtension(); } // 从缓存中读取扩展实现类 Holder<Object> holder = cachedInstances.get(name); if (holder == null) { cachedInstances.putIfAbsent(name, new Holder<Object>()); holder = cachedInstances.get(name); } Object instance = holder.get(); if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { instance = createExtension(name); holder.set(instance); } } } return (T) instance;}上面的逻辑比较简单,这里也不赘述了,直接看 createExtension() 方法。private T createExtension(String name) { Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (wrapperClasses != null && !wrapperClasses.isEmpty()) { for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException(“Extension instance(name: " + name + “, class: " + type + “) could not be instantiated: " + t.getMessage(), t); }}getExtensionClasses() 方法在前文已经分析过了,但是需要注意的是:getExtensionClasses 返回给我们的不过是使用 Class.forName() 加载过的类而已,充其量执行了里面的静态代码段,而并非得到了真正的实例。真正的实例对象仍需要调用 class.newInstance() 方法才能获取。 了解了这些之后我们继续看,我们通过 getExtensionClasses() 尝试获取系统已经加载的 class 对象,通过 class 对象再去扩展实例缓存中取。如果扩展实例为 null,调用 newInstance() 方法初始化实例,并放到 EXTENSION_INSTANCES 缓存中。之后再调用 injectExtension() 方法进行依赖注入。最后一段涉及到包装类的用法,下一个章节进行介绍。扩展类的包装在 createExtension() 方法中有如下一段代码:private T createExtension(String name) { // ···省略··· Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (wrapperClasses != null && !wrapperClasses.isEmpty()) { for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; // ···省略···}还记得 wrapperClasses 在什么地方被初始化的吗?在前文中的 loadClass() 方法中我们已经有介绍过。再回顾一下:private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { // ···省略··· if (isWrapperClass(clazz)) { Set<Class<?>> wrappers = cachedWrapperClasses; if (wrappers == null) { cachedWrapperClasses = new ConcurrentHashSet<Class<?>>(); wrappers = cachedWrapperClasses; } wrappers.add(clazz); } // ···省略···}private boolean isWrapperClass(Class<?> clazz) { try { clazz.getConstructor(type); return true; } catch (NoSuchMethodException e) { return false; }}在看这个方法前我们先了解下 Dubbo 中 wrapper 类的定义。举个例子:class A { private A a; public A(A a){ this.a = a; }}我们可以看到 A 类有一个以 A 为参数的构造方法,我们称它为复制构造方法。有这样构造方法的类在 Dubbo 中我们称它为 Wrapper 类。继续看 isWrapperClass() 方法,这个方法比较简单,尝试获取 clazz 中以 type 为参数的构造方法,如果可以获取到,则认为 clazz 则是当前 type 类的包装类。再结合上面的代码,我们会发现在加载扩展点时,我们将对应 type 的包装类缓存起来。private T createExtension(String name) { // ···省略··· T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (wrapperClasses != null && !wrapperClasses.isEmpty()) { for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; // ···省略···}为了更好的理解这段代码,我们假设当前 type 值为 Protocol.class ,我们可以在 org.apache.dubbo.rpc.Protocol 文件中找到 Protocol 接口的包装类 ProtocolFilterWrapper 和 ProtocolListenerWrapper,他们会依次被添加到 cachedWrapperClasses 集合中。依次遍历 cachedWrapperClasses 集合,比如第一次取到的是 ProtocolFilterWrapper 类,则会以调用 ProtocolFilterWrapper 的复制构造方法将 instance 包装起来。创建完 ProtocolFilterWrapper 对象实例后,调用 injectExtension() 进行依赖注入。此时 instance 已经为 ProtocolFilterWrapper 的实例,继续循环,会将 ProtocolFilterWrapper 类包装在 ProtocolListenerWrapper 类中。因此我们最后返回的是一个 ProtocolListenerWrapper 实例。最后调用时,仍会通过一层一层的调用,最后调用原始 instance 的方法。这里的包装类有点类似 AOP 思想,我们可以通过一层一层的包装,在调用扩展实现之前添加一些日志打印、监控等自定义的操作。Dubbo 中的 IOC 机制上文中我们已经讨论过 Dubbo 中利用反射机制实现一个类 IOC 功能。在这一章节中,我们再回顾一下 injectExtension() 方法,仔细的来看看 Dubbo 中 IOC 功能的实现。createExtension()instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));private T injectExtension(T instance) { // ··· Class<?> pt = method.getParameterTypes()[0]; try { String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : “”; Object object = objectFactory.getExtension(pt, property); if (object != null) { method.invoke(instance, object); } } // ···}public class StubProxyFactoryWrapper implements ProxyFactory { // … private Protocol protocol; public void setProtocol(Protocol protocol) { this.protocol = protocol; } //…}在上一章节中我们已经讲过 wrapper 类,在这里我们举个例子说明一下。比如我们当前的 wrapperClass 类为 StubProxyFactoryWrapper,那么代码执行逻辑大致如下所示:创建 StubProxyFactoryWrapper 实例;获取流程1创建的实例作为 injectExtension() 的参数,执行;injectExtension() 方法循环遍历到 StubProxyFactoryWrapper 的 setProtocol()方法(此时 pt=Protocol.class,property=protocol),执行 objectFactory.getExtension(pt,property) 方法。objectFactory 在 ExtensionLoader 的构造方法中被初始化,在这里获取到自适应扩展类为 AdaptiveExtensionFactory。执行 AdaptiveExtensionFactory.getExtension()。AdaptiveExtensionFactory 类中有一个集合变量 factories。factories 在 AdaptiveExtensionFactory 的构造方法中被初始化,包含了两个工厂类:SpiExtensionFactory、SpringExtensionFactory。执行 AdaptiveExtensionFactory 类的 getExtension() 方法会依次调用 SpiExtensionFactory 和 SpringExtensionFactory 类的 getExtension() 方法。执行 SpiExtensionFactory 的 getExtension() 方法。上面有说到此时的 type=Procotol.class,property=protocol,从下面的代码我们可以发现 Protocol 是一个接口类,同时标注了 @SPI 注解,此时会获取 Protocol 类型的 ExtensionLoader 对象,最后又去调用 loader 的 getAdaptiveExtension() 方法。最终获取到的自适应类为 Protocol$Adaptive 动态类。objectFactory.getExtension(pt, property); 最后得到的类为 Protocol$Adaptive 类,最后利用反射机制将其注入到 StubProxyFactoryWrapper 实例中。@SPI(“dubbo”)public interface Protocol {}public class SpiExtensionFactory implements ExtensionFactory { @Override public <T> T getExtension(Class<T> type, String name) { if (type.isInterface() && type.isAnnotationPresent(SPI.class)) { ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type); if (!loader.getSupportedExtensions().isEmpty()) { return loader.getAdaptiveExtension(); } } return null; }}END在最后,我们再回顾下开头关于 Dubbo SPI 基于 JAVA SPI 改进的那段话:Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。总结如下:Dubbo SPI 在加载扩展点,会以 key-value 的形式将扩展类保存在缓存中,但此时的扩展类只是调用 Class.forName() 加载的类,并没有实例化。扩展类会在调用 getExtension() 方法时被实例化。Dubbo 通过工厂模式和反射机制实现了依赖注入功能。Dubbo 中通过包装类实现了 AOP 机制,方便我们添加监控和打印日志。本BLOG上原创文章未经本人许可,不得用于商业用途及传统媒体。网络媒体转载请注明出处,否则属于侵权行为。https://juejin.im/post/5c0cd7… ...

December 25, 2018 · 10 min · jiezi

云栖专辑 | 阿里开发者们的第3个感悟:从身边开源开始学习,用过才能更好理解代码

2015年12月20日,云栖社区上线。2018年12月20日,云栖社区3岁。阿里巴巴常说“晴天修屋顶”。在我们看来,寒冬中,最值得投资的是学习,是增厚的知识储备。所以社区特别制作了这个专辑——分享给开发者们20个弥足珍贵的成长感悟,50本书单。多年以后,再回首2018-19年,留给我们自己的,除了寒冷,还有不断上升的技术能力与拼搏后的成就感。*12月24日,从身边熟悉的开源系统开始,用过才能更好理解代码。这是我们送给开发者的第3个感悟。正研,社区HBase社群大V。在他的博文中,可以清晰看到经验的积累。正研:开源改变世界杨文龙(正研)阿里巴巴存储技术事业部技术专家Ali-HBase内核研发负责人ApacheHBase社区Committer&PMC成员对分布式存储系统的设计、实践具备丰富的大规模生产的经验有些人一直想去学习热门开源软件的代码,但其实不如从身边熟悉的开源系统开始;因为只有用过,才能更好地理解代码,只有带着实际生产的问题去看,才能明白为什么要这样去设计架构。反过来,只有从源码级理解这个系统,才能在使用过程中避免采坑。推荐的书单《深入理解计算机系统》《HeadFirst设计模式》*12月21日,使命感与开放心态,是我们送给开发者的第2个感悟。德歌:公益是一辈子的事,I’m digoal, just do it德歌,江湖人称德哥。PG大神,在社区拥有6500+位粉丝。三年来,他沉淀在社区的博文超过2000+篇。还记得社区刚成立时,有位开发者在博文后留言“我一直认为PG是小众数据库,没想到社区有这么多干货。” 三年过去,PG的地位一直在上升,云栖社区PG钉群也已经超过1000位开发者在一起交流讨论。*周正中(德歌)PostgreSQL 中国社区发起人之一,PostgreSQL 象牙塔发起人之一,DBA+社群联合发起人之一,10余项数据库相关专利,现就职于阿里云数据库内核技术组。学习第一要有使命感,第二要有开放的心态。使命感是技术为业务服务,结合业务一起创造社会价值,有使命感才能让你坚持下去,遇到困难时不容易被打倒。开放是在扎实的技术功底之上,跳出纯粹的技术从生态进行思考,要埋头苦干也要抬头看路。比如行业生态中重叠部分,盟友与竞争关系,问题及补齐办法等,同时也要密切关注国家和国际形势,分析背后原因,在未来技术方向决策上避免逆流行舟。推荐的书单:《PostgreSQL实战》*12月20日,场景中学习,这是我们送给开发者的第1个感悟。阿里毕玄:程序员的成长路线在这篇《程序员的成长路线》里,阿里基础设施负责人毕玄结合自己的经历跟大家讲述了他在各个角色上成长的感受。在他的职业经历中,在成长方面经历了技术能力的成长、架构能力的成长,以及现在作为一个在修炼中的技术 Leader 的成长。其中技术能力和架构能力的成长是所有程序员都很需要的,值得所有正为职业发展而迷茫的技术同学细细品味。*林昊(毕玄)阿里基础设施负责人阿里巴巴HSF、T4创始人,HBase负责人主导阿里电商分布式应用架构、异地多活架构、资源弹性架构升级程序员,要寻找甚至创造场景来学习相应的技术能力。正如学Java通讯框架,尝试基于BIO/NIO写一个,然后对比Mina/Netty,看看为什么不一样;学Java的内存管理,尝试写程序去控制GC的行为。书籍外,更建议翻看源码,结合场景才能真正理解和学会。我的职业经历是技术能力成长、架构能力成长和正在修炼中的技术Leader的成长,三条路线都可发展,没有孰优孰劣,兴趣、个人优势仍是最重要的。出版的图书:《OSGi原理与最佳实践》《分布式Java应用:基础与实践》推荐的书单:《硅谷之谜》《智能时代:大数据与智能革命重新定义未来》预计更新到1月20日,欢迎收藏。本文作者:云篆阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 24, 2018 · 1 min · jiezi

深度学习目标检测系列:一文弄懂YOLO算法|附Python源码

摘要: 本文是目标检测系列文章——YOLO算法,介绍其基本原理及实现细节,并用python实现,方便读者上手体验目标检测的乐趣。在之前的文章中,介绍了计算机视觉领域中目标检测的相关方法——RCNN系列算法原理,以及Faster RCNN的实现。这些算法面临的一个问题,不是端到端的模型,几个构件拼凑在一起组成整个检测系统,操作起来比较复杂,本文将介绍另外一个端到端的方法——YOLO算法,该方法操作简便且仿真速度快,效果也不差。YOLO算法是什么?YOLO框架(You Only Look Once)与RCNN系列算法不一样,是以不同的方式处理对象检测。它将整个图像放在一个实例中,并预测这些框的边界框坐标和及所属类别概率。使用YOLO算法最大优的点是速度极快,每秒可处理45帧,也能够理解一般的对象表示。YOLO框架如何运作?在本节中,将介绍YOLO用于检测给定图像中的对象的处理步骤。首先,输入图像:然后,YOLO将输入图像划分为网格形式(例如3 X 3):最后,对每个网格应用图像分类和定位处理,获得预测对象的边界框及其对应的类概率。整个过程是不是很清晰,下面逐一详细介绍。首先需要将标记数据传递给模型以进行训练。假设已将图像划分为大小为3 X 3的网格,且总共只有3个类别,分别是行人(c1)、汽车(c2)和摩托车(c3)。因此,对于每个单元格,标签y将是一个八维向量:其中:pc定义对象是否存在于网格中(存在的概率);bx、by、bh、bw指定边界框;c1、c2、c3代表类别。如果检测对象是汽车,则c2位置处的值将为1,c1和c3处的值将为0;假设从上面的例子中选择第一个网格:由于此网格中没有对象,因此pc将为零,此网格的y标签将为:?意味着其它值是什么并不重要,因为网格中没有对象。下面举例另一个有车的网格(c2=1):在为此网格编写y标签之前,首先要了解YOLO如何确定网格中是否存在实际对象。大图中有两个物体(两辆车),因此YOLO将取这两个物体的中心点,物体将被分配到包含这些物体中心的网格中。中心点左侧网格的y标签会是这样的:由于此网格中存在对象,因此pc将等于1,bx、by、bh、bw将相对于正在处理的特定网格单元计算。由于检测出的对象是汽车,所以c2=1,c1和c3均为0。对于9个网格中的每一个单元格,都具有八维输出向量。最终的输出形状为3X3X8。使用上面的例子(输入图像:100X100X3,输出:3X3X8),模型将按如下方式进行训练:使用经典的CNN网络构建模型,并进行模型训练。在测试阶段,将图像传递给模型,经过一次前向传播就得到输出y。为了简单起见,使用3X3网格解释这一点,但通常在实际场景中会采用更大的网格(比如19X19)。即使一个对象跨越多个网格,它也只会被分配到其中点所在的单个网格。可以通过增加更多网格来减少多个对象出现在同一网格单元中的几率。如何编码边界框?如前所述,bx、by、bh和bw是相对于正在处理的网格单元计算而言的。下面通过一个例子来说明这一点。以包含汽车的右边网格为例:由于bx、by、bh和bw将仅相对于该网格计算。此网格的y标签将为:由于这个网格中有一个对象汽车,所以pc=1、c2=1。现在,看看如何决定bx、by、bh和bw的取值。在YOLO中,分配给所有网格的坐标都如下图所示:bx、by是对象相对于该网格的中心点的x和y坐标。在例子中,近似bx=0.4和by=0.3:bh是边界框的高度与相应单元网格的高度之比,在例子中约为0.9:bh=0.9,bw是边界框的宽度与网格单元的宽度之比,bw=0.5。此网格的y标签将为:请注意,bx和by将始终介于0和1之间,因为中心点始终位于网格内,而在边界框的尺寸大于网格尺寸的情况下,bh和bw可以大于1。非极大值抑制|Non-Max Suppression这里有一些思考的问题——如何判断预测的边界框是否是一个好结果(或一个坏结果)?单元格之间的交叉点,计算实际边界框和预测的边界框的并集交集。假设汽车的实际和预测边界框如下所示:其中,红色框是实际的边界框,蓝色框是预测的边界框。如何判断它是否是一个好的预测呢?IoU将计算这两个框的并集交叉区域:IoU =交叉面积/联合的面积;在本例中:IoU =黄色面积/绿色面积;如果IoU大于0.5,就可以说预测足够好。0.5是在这里采取的任意阈值,也可以根据具体问题进行更改。阈值越大,预测就越准确。还有一种技术可以显着提高YOLO的效果——非极大值抑制。对象检测算法最常见的问题之一是,它不是一次仅检测出一次对象,而可能获得多次检测结果。假设:上图中,汽车不止一次被识别,那么如何判定边界框呢。非极大值抑可以解决这个问题,使得每个对象只能进行一次检测。下面了解该方法的工作原理。1.它首先查看与每次检测相关的概率并取最大的概率。在上图中,0.9是最高概率,因此首先选择概率为0.9的方框:2.现在,它会查看图像中的所有其他框。与当前边界框较高的IoU的边界框将被抑制。因此,在示例中,0.6和0.7概率的边界框将被抑制:3.在部分边界框被抑制后,它会从概率最高的所有边界框中选择下一个,在例子中为0.8的边界框:4.再次计算与该边界框相连边界框的IoU,去掉较高IoU值的边界框:5.重复这些步骤,得到最后的边界框:以上就是非极大值抑制的全部内容,总结一下关于非极大值抑制算法的要点:丢弃概率小于或等于预定阈值(例如0.5)的所有方框;对于剩余的边界框:选择具有最高概率的边界框并将其作为输出预测;计算相关联的边界框的IoU值,舍去IoU大于阈值的边界框;重复步骤2,直到所有边界框都被视为输出预测或被舍弃;Anchor Boxes在上述内容中,每个网格只能识别一个对象。但是如果单个网格中有多个对象呢?这就行需要了解 Anchor Boxes的概念。假设将下图按照3X3网格划分:获取对象的中心点,并根据其位置将对象分配给相应的网格。在上面的示例中,两个对象的中心点位于同一网格中:上述方法只会获得两个边界框其中的一个,但是如果使用Anchor Boxes,可能会输出两个边界框!我们该怎么做呢?首先,预先定义两种不同的形状,称为Anchor Boxes。对于每个网格将有两个输出。这里为了易于理解,这里选取两个Anchor Boxes,也可以根据实际情况增加Anchor Boxes的数量:没有Anchor Boxes的YOLO输出标签如下所示:有Anchor Boxes的YOLO输出标签如下所示:前8行属于Anchor Boxes1,其余8行属于Anchor Boxes2。基于边界框和框形状的相似性将对象分配给Anchor Boxes。由于Anchor Boxes1的形状类似于人的边界框,后者将被分配给Anchor Boxes1,并且车将被分配给Anchor Boxes2.在这种情况下的输出,将是3X3X16大小。 因此,对于每个网格,可以根据Anchor Boxes的数量检测两个或更多个对象。结合思想在本节中,首先介绍如何训练YOLO模型,然后是新的图像进行预测。训练训练模型时,输入数据是由图像及其相应的y标签构成。样例如下:假设每个网格有两个Anchor Boxes,并划分为3X3网格,并且有3个不同的类别。因此,相应的y标签具有3X3X16的形状。训练过程的完成方式就是将特定形状的图像映射到对应3X3X16大小的目标。测试对于每个网格,模型将预测·3X3X16·大小的输出。该预测中的16个值将与训练标签的格式相同。前8个值将对应于Anchor Boxes1,其中第一个值将是该网络中对象的概率,2-5的值将是该对象的边界框坐标,最后三个值表明对象属于哪个类。以此类推。最后,非极大值抑制方法将应用于预测框以获得每个对象的单个预测结果。以下是YOLO算法遵循的确切维度和步骤:准备对应的图像(608,608,3);将图像传递给卷积神经网络(CNN),该网络返回(19,19,5,85)维输出;输出的最后两个维度被展平以获得(19,19,425)的输出量:19×19网格的每个单元返回425个数字;425=5 * 85,其中5是每个网格的Anchor Boxes数量;85= 5+80,其中5表示(pc、bx、by、bh、bw),80是检测的类别数;最后,使用IoU和非极大值抑制去除重叠框;YOLO算法实现本节中用于实现YOLO的代码来自Andrew NG的GitHub存储库,需要下载此zip文件,其中包含运行此代码所需的预训练权重。首先定义一些函数,这些函数将用来选择高于某个阈值的边界框,并对其应用非极大值抑制。首先,导入所需的库:import osimport matplotlib.pyplot as pltfrom matplotlib.pyplot import imshowimport scipy.ioimport scipy.miscimport numpy as npimport pandas as pdimport PILimport tensorflow as tffrom skimage.transform import resizefrom keras import backend as Kfrom keras.layers import Input, Lambda, Conv2Dfrom keras.models import load_model, Modelfrom yolo_utils import read_classes, read_anchors, generate_colors, preprocess_image, draw_boxes, scale_boxesfrom yad2k.models.keras_yolo import yolo_head, yolo_boxes_to_corners, preprocess_true_boxes, yolo_loss, yolo_body%matplotlib inline然后,实现基于概率和阈值过滤边界框的函数:def yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold = .6): box_scores = box_confidencebox_class_probs box_classes = K.argmax(box_scores,-1) box_class_scores = K.max(box_scores,-1) filtering_mask = box_class_scores>threshold scores = tf.boolean_mask(box_class_scores,filtering_mask) boxes = tf.boolean_mask(boxes,filtering_mask) classes = tf.boolean_mask(box_classes,filtering_mask) return scores, boxes, classes之后,实现计算IoU的函数:def iou(box1, box2): xi1 = max(box1[0],box2[0]) yi1 = max(box1[1],box2[1]) xi2 = min(box1[2],box2[2]) yi2 = min(box1[3],box2[3]) inter_area = (yi2-yi1)(xi2-xi1) box1_area = (box1[3]-box1[1])(box1[2]-box1[0]) box2_area = (box2[3]-box2[1])(box2[2]-box2[0]) union_area = box1_area+box2_area-inter_area iou = inter_area/union_area return iou然后,实现非极大值抑制的函数:def yolo_non_max_suppression(scores, boxes, classes, max_boxes = 10, iou_threshold = 0.5): max_boxes_tensor = K.variable(max_boxes, dtype=‘int32’) K.get_session().run(tf.variables_initializer([max_boxes_tensor])) nms_indices = tf.image.non_max_suppression(boxes,scores,max_boxes,iou_threshold) scores = K.gather(scores,nms_indices) boxes = K.gather(boxes,nms_indices) classes = K.gather(classes,nms_indices) return scores, boxes, classes随机初始化下大小为(19,19,5,85)的输出向量:yolo_outputs = (tf.random_normal([19, 19, 5, 1], mean=1, stddev=4, seed = 1), tf.random_normal([19, 19, 5, 2], mean=1, stddev=4, seed = 1), tf.random_normal([19, 19, 5, 2], mean=1, stddev=4, seed = 1), tf.random_normal([19, 19, 5, 80], mean=1, stddev=4, seed = 1))最后,实现一个将CNN的输出作为输入并返回被抑制的边界框的函数:def yolo_eval(yolo_outputs, image_shape = (720., 1280.), max_boxes=10, score_threshold=.6, iou_threshold=.5): box_confidence, box_xy, box_wh, box_class_probs = yolo_outputs boxes = yolo_boxes_to_corners(box_xy, box_wh) scores, boxes, classes = yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold = score_threshold) boxes = scale_boxes(boxes, image_shape) scores, boxes, classes = yolo_non_max_suppression(scores, boxes, classes, max_boxes, iou_threshold) return scores, boxes, classes使用yolo_eval函数对之前创建的随机输出向量进行预测:scores, boxes, classes = yolo_eval(yolo_outputs)with tf.Session() as test_b: print(“scores[2] = " + str(scores[2].eval())) print(“boxes[2] = " + str(boxes[2].eval())) print(“classes[2] = " + str(classes[2].eval()))score表示对象在图像中的可能性,boxes返回检测到的对象的(x1,y1,x2,y2)坐标,classes表示识别对象所属的类。现在,在新的图像上使用预训练的YOLO算法,看看其工作效果:sess = K.get_session()class_names = read_classes(“model_data/coco_classes.txt”)anchors = read_anchors(“model_data/yolo_anchors.txt”)yolo_model = load_model(“model_data/yolo.h5”)在加载类别信息和预训练模型之后,使用上面定义的函数来获取·yolo_outputs·。yolo_outputs = yolo_head(yolo_model.output, anchors, len(class_names))之后,定义一个函数来预测边界框并在图像上标记边界框:def predict(sess, image_file): image, image_data = preprocess_image(“images/” + image_file, model_image_size = (608, 608)) out_scores, out_boxes, out_classes = sess.run([scores, boxes, classes], feed_dict={yolo_model.input: image_data, K.learning_phase(): 0}) print(‘Found {} boxes for {}’.format(len(out_boxes), image_file)) # Generate colors for drawing bounding boxes. colors = generate_colors(class_names) # Draw bounding boxes on the image file draw_boxes(image, out_scores, out_boxes, out_classes, class_names, colors) # Save the predicted bounding box on the image image.save(os.path.join(“out”, image_file), quality=90) # Display the results in the notebook output_image = scipy.misc.imread(os.path.join(“out”, image_file)) plt.figure(figsize=(12,12)) imshow(output_image) return out_scores, out_boxes, out_classes接下来,将使用预测函数读取图像并进行预测:img = plt.imread(‘images/img.jpg’)image_shape = float(img.shape[0]), float(img.shape[1])scores, boxes, classes = yolo_eval(yolo_outputs, image_shape)最后,输出预测结果:out_scores, out_boxes, out_classes = predict(sess, “img.jpg”)以上就是YOLO算法的全部内容,更多详细内容可以关注darknet的官网。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 18, 2018 · 2 min · jiezi

深度学习目标检测系列:faster RCNN实现|附python源码

摘要: 本文在讲述RCNN系列算法基本原理基础上,使用keras实现faster RCNN算法,在细胞检测任务上表现优异,可动手操作一下。目标检测一直是计算机视觉中比较热门的研究领域,有一些常用且成熟的算法得到业内公认水平,比如RCNN系列算法、SSD以及YOLO等。如果你是从事这一行业的话,你会使用哪种算法进行目标检测任务呢?在我寻求在最短的时间内构建最精确的模型时,我尝试了其中的R-CNN系列算法,如果读者们对这方面的算法还不太了解的话,建议阅读《目标检测算法图解:一文看懂RCNN系列算法》。在掌握基本原理后,下面进入实战部分。本文将使用一个非常酷且有用的数据集来实现faster R-CNN,这些数据集具有潜在的真实应用场景。问题陈述数据来源于医疗相关数据集,目的是解决血细胞检测问题。任务是通过显微图像读数来检测每张图像中的所有红细胞(RBC)、白细胞(WBC)以及血小板。最终预测效果应如下所示:选择该数据集的原因是我们血液中RBC、WBC和血小板的密度提供了大量关于免疫系统和血红蛋白的信息,这些信息可以帮助我们初步地识别一个人是否健康,如果在其血液中发现了任何差异,我们就可以迅速采取行动来进行下一步的诊断。通过显微镜手动查看样品是一个繁琐的过程,这也是深度学习模式能够发挥重要作用的地方,一些算法可以从显微图像中分类和检测血细胞,并且达到很高的精确度。本文采用的血细胞检测数据集可以从这里下载,本文稍微修改了一些数据:边界框已从给定的.xml格式转换为.csv格式;随机划分数据集,得到训练集和测试集;这里使用流行的Keras框架构建本文模型。系统设置在真正进入模型构建阶段之前,需要确保系统已安装正确的库和相应的框架。运行此项目需要以下库:pandasmatplotlibtensorflowkeras – 2.0.3numpyopencv-pythonsklearnh5py对于已经安装了Anaconda和Jupyter的电脑而言,上述这些库大多数已经安装好了。建议从此链接下载requirements.txt文件,并使用它来安装剩余的库。在终端中键入以下命令来执行此操作:pip install -r requirement.txt系统设置好后,下一步是进行数据处理。数据探索首先探索所拥有的数据总是一个好开始(坦率地说,这是一个强制性的步骤)。对数据熟悉有助于挖掘隐藏的模式,还可以获得对整体的洞察力。本文从整个数据集中创建了三个文件,分别是:train_images:用于训练模型的图像,包含每个图像的类别和实际边界框;test_images:用于模型预测的图像,该集合缺少对应的标签;train.csv:包含每个图像的名称、类别和边界框坐标。一张图像可以有多行数据,因为单张图像可能包含多个对象;读取.csv文件并打印出前几行:# importing required librariesimport pandas as pdimport matplotlib.pyplot as plt%matplotlib inlinefrom matplotlib import patches# read the csv file using read_csv function of pandastrain = pd.read_csv(‘train.csv’)train.head()训练文件中总共有6列,其中每列代表的内容如下:image_names:图像的名称;cell_type:表示单元的类型;xmin:图像左下角的x坐标;xmax:图像右上角的x坐标;ymin:图像左下角的y坐标;ymax:图像右上角的y坐标;下面打印出一张图片来展示正在处理的图像:# reading single image using imread function of matplotlibimage = plt.imread(‘images/1.jpg’)plt.imshow(image)上图就是血细胞图像的样子,其中,蓝色部分代表WBC,略带红色的部分代表RBC。下面看看整个训练集中总共有多少张图像和不同类型的数量。# Number of classestrain[‘cell_type’].value_counts()结果显示训练集有254张图像。# Number of classestrain[‘cell_type’].value_counts()结果显示有三种不同类型的细胞,即RBC,WBC和血小板。最后,看一下检测到的对象的图像是怎样的:fig = plt.figure()#add axes to the imageax = fig.add_axes([0,0,1,1])# read and plot the imageimage = plt.imread(‘images/1.jpg’)plt.imshow(image)# iterating over the image for different objectsfor _,row in train[train.image_names == “1.jpg”].iterrows(): xmin = row.xmin xmax = row.xmax ymin = row.ymin ymax = row.ymax width = xmax - xmin height = ymax - ymin # assign different color to different classes of objects if row.cell_type == ‘RBC’: edgecolor = ‘r’ ax.annotate(‘RBC’, xy=(xmax-40,ymin+20)) elif row.cell_type == ‘WBC’: edgecolor = ‘b’ ax.annotate(‘WBC’, xy=(xmax-40,ymin+20)) elif row.cell_type == ‘Platelets’: edgecolor = ‘g’ ax.annotate(‘Platelets’, xy=(xmax-40,ymin+20)) # add bounding boxes to the image rect = patches.Rectangle((xmin,ymin), width, height, edgecolor = edgecolor, facecolor = ’none’) ax.add_patch(rect)上图就是训练样本示例,从中可以看到,细胞有不同的类及其相应的边界框。下面进行模型训练,本文使用keras_frcnn库来训练搭建的模型以及对测试图像进行预测。faster R-CNN实现为了实现 faster R-CNN算法,本文遵循此Github存储库中提到的步骤。因此,首先请确保克隆好此存储库。打开一个新的终端窗口并键入以下内容以执行此操作:git clone https://github.com/kbardool/keras-frcnn.git并将train_images和test_images文件夹以及train.csv文件移动到该存储库目录下。为了在新数据集上训练模型,输入的格式应为:filepath,x1,y1,x2,y2,class_name其中:filepath是训练图像的路径;x1是边界框的xmin坐标;y1是边界框的ymin坐标;x2是边界框的xmax坐标;y2是边界框的ymax坐标;class_name是该边界框中类的名称;这里需要将.csv格式转换为.txt文件,该文件具有与上述相同的格式。创建一个新的数据帧,按照格式将所有值填入该数据帧,然后将其另存为.txt文件。data = pd.DataFrame()data[‘format’] = train[‘image_names’]# as the images are in train_images folder, add train_images before the image namefor i in range(data.shape[0]): data[‘format’][i] = ’train_images/’ + data[‘format’][i]# add xmin, ymin, xmax, ymax and class as per the format requiredfor i in range(data.shape[0]): data[‘format’][i] = data[‘format’][i] + ‘,’ + str(train[‘xmin’][i]) + ‘,’ + str(train[‘ymin’][i]) + ‘,’ + str(train[‘xmax’][i]) + ‘,’ + str(train[‘ymax’][i]) + ‘,’ + train[‘cell_type’][i]data.to_csv(‘annotate.txt’, header=None, index=None, sep=’ ‘)下一步进行模型训练,使用train_frcnn.py文件来训练模型。cd keras-frcnnpython train_frcnn.py -o simple -p annotate.txt由于数据集较大,需要一段时间来训练模型。如果条件满足的话,可以使用GPU来加快训练过程。同样也可以尝试减少num_epochs参数来加快训练过程。模型每训练好一次(有改进时),该特定时刻的权重将保存在与“model_frcnn.hdf5”相同的目录中。当对测试集进行预测时,将使用到这些权重。根据机器的配置,可能需要花费大量时间来训练模型并获得权重。建议使用本文训练大约500个时期的权重作为初始化。可以从这里下载这些权重,并设置好相应的路径。因此,当模型训练好并保存好权重后,下面进行预测。Keras_frcnn对新图像进行预测并将其保存在新文件夹中,这里只需在test_frcnn.py文件中进行两处更改即可保存图像:从该文件的最后一行删除注释:cv2.imwrite(’./ results_imgs / {}。png’.format(idx),img);在此文件的倒数第二行和第三行添加注释:#cv2.imshow(‘img’,img) ;#cv2.waitKey(0); 使用下面的代码进行图像预测:python test_frcnn.py -p test_images最后,检测到对象的图像将保存在“results_imgs”文件夹中。以下是本文实现faster R-CNN后预测几个样本获得的结果:总结R-CNN算法确实是用于对象检测任务的变革者,改变了传统的做法,并开创了深度学习算法。近年来,计算机视觉应用的数量突然出现飙升,而R-CNN系列算法仍然是其中大多数应用的核心。Keras_frcnn也被证明是一个很好的对象检测工具库,在本系列的下一篇文章中,将专注于更先进的技术,如YOLO,SSD等。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 17, 2018 · 2 min · jiezi

vuex源码解析

vuex简介能看到此文章的人,应该大部分都已经使用过vuex了,想更深一步了解vuex的内部实现原理。所以简介就少介绍一点。官网介绍说Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。数据流的状态非常清晰,按照 组件dispatch Action -> action内部commit Mutation -> Mutation再 mutate state 的数据,在触发render函数引起视图的更新。附上一张官网的流程图及vuex的官网地址:https://vuex.vuejs.org/zh/Questions在使用vuex的时候,大家有没有如下几个疑问,带着这几个疑问,再去看源码,从中找到解答,这样对vuex的理解可以加深一些。官网在严格模式下有说明:在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。vuex是如何检测状态改变是由mutation函数引起的?通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中。为什么所有子组件都可以取到store?为什么用到的属性在state中也必须要提前定义好,vue视图才可以响应?在调用dispatch和commit时,只需传入(type, payload),为什么action函数和mutation函数能够在第一个参数中解构出来state、commit等?带着这些问题,我们来看看vuex的源码,从中寻找到答案。源码目录结构vuex的源码结构非常简洁清晰,代码量也不是很大,大家不要感到恐慌。vuex挂载vue使用插件的方法很简单,只需Vue.use(Plugins),对于vuex,只需要Vue.use(Vuex)即可。在use 的内部是如何实现插件的注册呢?读过vue源码的都知道,如果传入的参数有 install 方法,则调用插件的 install 方法,如果传入的参数本身是一个function,则直接执行。那么我们接下来就需要去 vuex 暴露出来的 install 方法去看看具体干了什么。store.jsexport function install(_Vue) { // vue.use原理:调用插件的install方法进行插件注册,并向install方法传递Vue对象作为第一个参数 if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== “production”) { console.error( “[vuex] already installed. Vue.use(Vuex) should be called only once.” ); } return; } Vue = _Vue; // 为了引用vue的watch方法 applyMixin(Vue);}在 install 中,将 vue 对象赋给了全局变量 Vue,并作为参数传给了 applyMixin 方法。那么在 applyMixin 方法中干了什么呢?mixin.jsfunction vuexInit() { const options = this.$options; // store injection if (options.store) { this.$store = typeof options.store === “function” ? options.store() : options.store; } else if (options.parent && options.parent.$store) { this.$store = options.parent.$store; } }在这里首先检查了一下 vue 的版本,2以上的版本把 vuexInit 函数混入 vuex 的 beforeCreate 钩子函数中。在 vuexInit 中,将 new Vue() 时传入的 store 设置到 this 对象的 $store 属性上,子组件则从其父组件上引用其 $store 属性进行层层嵌套设置,保证每一个组件中都可以通过 this.$store 取到 store 对象。这也就解答了我们问题 2 中的问题。通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,注入方法是子从父拿,root从options拿。接下来让我们看看new Vuex.Store()都干了什么。store构造函数store对象构建的主要代码都在store.js中,是vuex的核心代码。首先,在 constructor 中进行了 Vue 的判断,如果没有通过 Vue.use(Vuex) 进行 Vuex 的注册,则调用 install 函数注册。( 通过 script 标签引入时不需要手动调用 Vue.use(Vuex) )并在非生产环境进行判断: 必须调用 Vue.use(Vuex) 进行注册,必须支持 Promise,必须用 new 创建 store。if (!Vue && typeof window !== “undefined” && window.Vue) { install(window.Vue);}if (process.env.NODE_ENV !== “production”) { assert(Vue, must call Vue.use(Vuex) before creating a store instance.); assert( typeof Promise !== “undefined”, vuex requires a Promise polyfill in this browser. ); assert( this instanceof Store, store must be called with the new operator. );}然后进行一系列的属性初始化。其中的重点是 new ModuleCollection(options),这个我们放在后面再讲。先把 constructor 中的代码过完。const { plugins = [], strict = false } = options;// store internal statethis._committing = false; // 是否在进行提交mutation状态标识this._actions = Object.create(null); // 保存action,_actions里的函数已经是经过包装后的this._actionSubscribers = []; // action订阅函数集合this._mutations = Object.create(null); // 保存mutations,_mutations里的函数已经是经过包装后的this._wrappedGetters = Object.create(null); // 封装后的getters集合对象// Vuex支持store分模块传入,在内部用Module构造函数将传入的options构造成一个Module对象,// 如果没有命名模块,默认绑定在this._modules.root上// ModuleCollection 内部调用 new Module构造函数this._modules = new ModuleCollection(options);this._modulesNamespaceMap = Object.create(null); // 模块命名空间mapthis._subscribers = []; // mutation订阅函数集合this._watcherVM = new Vue(); // Vue组件用于watch监视变化属性初始化完毕后,首先从 this 中解构出原型上的 dispatch 和 commit 方法,并进行二次包装,将 this 指向当前 store。const store = this;const { dispatch, commit } = this;/** 把 Store 类的 dispatch 和 commit 的方法的 this 指针指向当前 store 的实例上. 这样做的目的可以保证当我们在组件中通过 this.$store 直接调用 dispatch/commit 方法时, 能够使 dispatch/commit 方法中的 this 指向当前的 store 对象而不是当前组件的 this.*/this.dispatch = function boundDispatch(type, payload) { return dispatch.call(store, type, payload);};this.commit = function boundCommit(type, payload, options) { return commit.call(store, type, payload, options);};接着往下走,包括严格模式的设置、根state的赋值、模块的注册、state的响应式、插件的注册等等,其中的重点在 installModule 函数中,在这里实现了所有modules的注册。//options中传入的是否启用严格模式this.strict = strict;// new ModuleCollection 构造出来的_mudulesconst state = this._modules.root.state;// 初始化组件树根组件、注册所有子组件,并将其中所有的getters存储到this._wrappedGetters属性中installModule(this, state, [], this._modules.root);//通过使用vue实例,初始化 store._vm,使state变成可响应的,并且将getters变成计算属性resetStoreVM(this, state);// 注册插件plugins.forEach(plugin => plugin(this));// 调试工具注册const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;if (useDevtools) { devtoolPlugin(this);}到此为止,constructor 中所有的代码已经分析完毕。其中的重点在 new ModuleCollection(options) 和 installModule ,那么接下来我们到它们的内部去看看,究竟都干了些什么。ModuleCollection由于 Vuex 使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。Vuex 允许我们将 store 分割成模块(module),每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。例如下面这样:const childModule = { state: { … }, mutations: { … }, actions: { … }}const store = new Vuex.Store({ state, getters, actions, mutations, modules: { childModule: childModule, }})有了模块的概念,可以更好的规划我们的代码。对于各个模块公用的数据,我们可以定义一个common store,别的模块用到的话直接通过 modules 的方法引入即可,无需重复的在每一个模块都写一遍相同的代码。这样我们就可以通过 store.state.childModule 拿到childModule中的 state 状态, 对于Module的内部是如何实现的呢?export default class ModuleCollection { constructor(rawRootModule) { // 注册根module,参数是new Vuex.Store时传入的options this.register([], rawRootModule, false); } register(path, rawModule, runtime = true) { if (process.env.NODE_ENV !== “production”) { assertRawModule(path, rawModule); } const newModule = new Module(rawModule, runtime); if (path.length === 0) { // 注册根module this.root = newModule; } else { // 注册子module,将子module添加到父module的_children属性上 const parent = this.get(path.slice(0, -1)); parent.addChild(path[path.length - 1], newModule); } // 如果当前模块有子modules,循环注册 if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime); }); } }}在ModuleCollection中又调用了Module构造函数,构造一个Module。Module构造函数constructor (rawModule, runtime) { // 初始化时为false this.runtime = runtime // 存储子模块 this._children = Object.create(null) // 将原来的module存储,以备后续使用 this._rawModule = rawModule const rawState = rawModule.state // 存储原来module的state this.state = (typeof rawState === ‘function’ ? rawState() : rawState) || {} }通过以上代码可以看出,ModuleCollection 主要将传入的 options 对象整个构造为一个 Module 对象,并循环调用 this.register([key], rawModule, false) 为其中的 modules 属性进行模块注册,使其都成为 Module 对象,最后 options 对象被构造成一个完整的 Module 树。经过 ModuleCollection 构造后的树结构如下:(以上面的例子生成的树结构)模块已经创建好之后,接下来要做的就是 installModule。installModule首先我们来看一看执行完 constructor 中的 installModule 函数后,这棵树的结构如何?从上图中可以看出,在执行完installModule函数后,每一个 module 中的 state 属性都增加了 其子 module 中的 state 属性,但此时的 state 还不是响应式的,并且新增加了 context 这个对象。里面包含 dispatch 、 commit 等函数以及 state 、 getters 等属性。它就是 vuex 官方文档中所说的Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象 这个 context 对象。我们平时在 store 中调用的 dispatch 和 commit 就是从这里解构出来的。接下来让我们看看 installModule 里面执行了什么。function installModule(store, rootState, path, module, hot) { // 判断是否是根节点,跟节点的path = [] const isRoot = !path.length; // 取命名空间,形式类似’childModule/’ const namespace = store._modules.getNamespace(path); // 如果namespaced为true,存入_modulesNamespaceMap中 if (module.namespaced) { store._modulesNamespaceMap[namespace] = module; } // 不是根节点,把子组件的每一个state设置到其父级的state属性上 if (!isRoot && !hot) { // 获取当前组件的父组件state const parentState = getNestedState(rootState, path.slice(0, -1)); // 获取当前Module的名字 const moduleName = path[path.length - 1]; store._withCommit(() => { Vue.set(parentState, moduleName, module.state); }); } // 给context对象赋值 const local = (module.context = makeLocalContext(store, namespace, path)); // 循环注册每一个module的Mutation module.forEachMutation((mutation, key) => { const namespacedType = namespace + key; registerMutation(store, namespacedType, mutation, local); }); // 循环注册每一个module的Action module.forEachAction((action, key) => { const type = action.root ? key : namespace + key; const handler = action.handler || action; registerAction(store, type, handler, local); }); // 循环注册每一个module的Getter module.forEachGetter((getter, key) => { const namespacedType = namespace + key; registerGetter(store, namespacedType, getter, local); }); // 循环_childern属性 module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot); });}在installModule函数里,首先判断是否是根节点、是否设置了命名空间。在设置了命名空间的前提下,把 module 存入 store._modulesNamespaceMap 中。在不是跟节点并且不是 hot 的情况下,通过 getNestedState 获取到父级的 state,并获取当前 module 的名字, 用 Vue.set() 方法将当前 module 的 state 挂载到父 state 上。然后调用 makeLocalContext 函数给 module.context 赋值,设置局部的 dispatch、commit方法以及getters和state。那么来看一看这个函数。function makeLocalContext(store, namespace, path) { // 是否有命名空间 const noNamespace = namespace === “”; const local = { // 如果没有命名空间,直接返回store.dispatch;否则给type加上命名空间,类似’childModule/‘这种 dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); const { payload, options } = args; let { type } = args; if (!options || !options.root) { type = namespace + type; if ( process.env.NODE_ENV !== “production” && !store._actions[type] ) { console.error( [vuex] unknown local action type: ${ args.type }, global type: ${type} ); return; } } return store.dispatch(type, payload); }, // 如果没有命名空间,直接返回store.commit;否则给type加上命名空间 commit: noNamespace ? store.commit : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); const { payload, options } = args; let { type } = args; if (!options || !options.root) { type = namespace + type; if ( process.env.NODE_ENV !== “production” && !store._mutations[type] ) { console.error( [vuex] unknown local mutation type: ${ args.type }, global type: ${type} ); return; } } store.commit(type, payload, options); } }; // getters and state object must be gotten lazily // because they will be changed by vm update Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) }, state: { get: () => getNestedState(store.state, path) } }); return local;}经过 makeLocalContext 处理的返回值会赋值给 local 变量,这个变量会传递给 registerMutation、forEachAction、registerGetter 函数去进行相应的注册。mutation可以重复注册,registerMutation 函数将我们传入的 mutation 进行了一次包装,将 state 作为第一个参数传入,因此我们在调用 mutation 的时候可以从第一个参数中取到当前的 state 值。function registerMutation(store, type, handler, local) { const entry = store._mutations[type] || (store._mutations[type] = []); entry.push(function wrappedMutationHandler(payload) { // 将this指向store,将makeLocalContext返回值中的state作为第一个参数,调用值执行的payload作为第二个参数 // 因此我们调用commit去提交mutation的时候,可以从mutation的第一个参数中取到当前的state值。 handler.call(store, local.state, payload); });}action也可以重复注册。注册 action 的方法与 mutation 相似,registerAction 函数也将我们传入的 action 进行了一次包装。但是 action 中参数会变多,里面包含 dispatch 、commit、local.getters、local.state、rootGetters、rootState,因此可以在一个 action 中 dispatch 另一个 action 或者去 commit 一个 mutation。这里也就解答了问题4中提出的疑问。function registerAction(store, type, handler, local) { const entry = store._actions[type] || (store._actions[type] = []); entry.push(function wrappedActionHandler(payload, cb) { //与mutation不同,action的第一个参数是一个对象,里面包含dispatch、commit、getters、state、rootGetters、rootState let res = handler.call( store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb ); if (!isPromise(res)) { res = Promise.resolve(res); } if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit(“vuex:error”, err); throw err; }); } else { return res; } });}注册 getters,从getters的第一个参数中可以取到local state、local getters、root state、root getters。getters不允许重复注册。function registerGetter(store, type, rawGetter, local) { // getters不允许重复 if (store._wrappedGetters[type]) { if (process.env.NODE_ENV !== “production”) { console.error([vuex] duplicate getter key: ${type}); } return; } store._wrappedGetters[type] = function wrappedGetter(store) { // getters的第一个参数包含local state、local getters、root state、root getters return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ); };}现在 store 的 _mutation、_action 中已经有了我们自行定义的的 mutation 和 action函数,并且经过了一层内部报装。当我们在组件中执行 this.$store.dispatch() 和 this.$store.commit() 的时候,是如何调用到相应的函数的呢?接下来让我们来看一看 store 上的 dispatch 和 commit 函数。commitcommit 函数先进行参数的适配处理,然后判断当前 action type 是否存在,如果存在则调用 _withCommit 函数执行相应的 mutation 。 // 提交mutation函数 commit(_type, _payload, _options) { // check object-style commit //commit支持两种调用方式,一种是直接commit(‘getName’,‘vuex’),另一种是commit({type:‘getName’,name:‘vuex’}), //unifyObjectStyle适配两种方式 const { type, payload, options } = unifyObjectStyle( _type, _payload, _options ); const mutation = { type, payload }; // 这里的entry取值就是我们在registerMutation函数中push到_mutations中的函数,已经经过处理 const entry = this._mutations[type]; if (!entry) { if (process.env.NODE_ENV !== “production”) { console.error([vuex] unknown mutation type: ${type}); } return; } // 专用修改state方法,其他修改state方法均是非法修改,在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误 // 不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。 this._withCommit(() => { entry.forEach(function commitIterator(handler) { handler(payload); }); }); // 订阅者函数遍历执行,传入当前的mutation对象和当前的state this._subscribers.forEach(sub => sub(mutation, this.state)); if (process.env.NODE_ENV !== “production” && options && options.silent) { console.warn( [vuex] mutation type: ${type}. Silent option has been removed. + “Use the filter functionality in the vue-devtools” ); } }在 commit 函数中调用了 _withCommit 这个函数, 代码如下。_withCommit 是一个代理方法,所有触发 mutation 的进行 state 修改的操作都经过它,由此来统一管理监控 state 状态的修改。在严格模式下,会深度监听 state 的变化,如果没有通过 mutation 去修改 state,则会报错。官方建议 不要在发布环境下启用严格模式! 请确保在发布环境下关闭严格模式,以避免性能损失。这里就解答了问题1中的疑问。_withCommit(fn) { // 保存之前的提交状态false const committing = this._committing; // 进行本次提交,若不设置为true,直接修改state,strict模式下,Vuex将会产生非法修改state的警告 this._committing = true; // 修改state fn(); // 修改完成,还原本次修改之前的状态false this._committing = committing;}dispatchdispatch 和 commit 的原理相同。如果有多个同名 action,会等到所有的 action 函数完成后,返回的 Promise 才会执行。// 触发action函数 dispatch(_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload); const action = { type, payload }; const entry = this._actions[type]; if (!entry) { if (process.env.NODE_ENV !== “production”) { console.error([vuex] unknown action type: ${type}); } return; } // 执行所有的订阅者函数 this._actionSubscribers.forEach(sub => sub(action, this.state)); return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry0; }至此,整个 installModule 里涉及到的内容已经分析完毕。我们在 options 中传进来的 action 和 mutation 已经在 store 中。但是 state 和 getters 还没有。这就是接下来的 resetStoreVM 方法做的事情。resetStoreVMresetStoreVM 函数中包括初始化 store._vm,观测 state 和 getters 的变化以及执行是否开启严格模式等。state 属性赋值给 vue 实例的 data 属性,因此数据是可响应的。这也就解答了问题 3,用到的属性在 state 中也必须要提前定义好,vue 视图才可以响应。function resetStoreVM(store, state, hot) { //保存老的vm const oldVm = store._vm; // 初始化 store 的 getters store.getters = {}; // _wrappedGetters 是之前在 registerGetter 函数中赋值的 const wrappedGetters = store._wrappedGetters; const computed = {}; forEachValue(wrappedGetters, (fn, key) => { // 将getters放入计算属性中,需要将store传入 computed[key] = () => fn(store); // 为了可以通过this.$store.getters.xxx访问getters Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }); }); // use a Vue instance to store the state tree // suppress warnings just in case the user has added // some funky global mixins // 用一个vue实例来存储store树,将getters作为计算属性传入,访问this.$store.getters.xxx实际上访问的是store._vm[xxx] const silent = Vue.config.silent; Vue.config.silent = true; store._vm = new Vue({ data: { $$state: state }, computed }); Vue.config.silent = silent; // enable strict mode for new vm // 如果是严格模式,则启用严格模式,深度 watch state 属性 if (store.strict) { enableStrictMode(store); } // 若存在oldVm,解除对state的引用,等dom更新后把旧的vue实例销毁 if (oldVm) { if (hot) { // dispatch changes in all subscribed watchers // to force getter re-evaluation for hot reloading. store._withCommit(() => { oldVm._data.$$state = null; }); } Vue.nextTick(() => oldVm.$destroy()); }}开启严格模式时,会深度监听 $$state 的变化,如果不是通过this._withCommit()方法触发的state修改,也就是store._committing如果是false,就会报错。function enableStrictMode(store) { store._vm.$watch( function() { return this._data.$$state; }, () => { if (process.env.NODE_ENV !== “production”) { assert( store._committing, do not mutate vuex store state outside mutation handlers. ); } }, { deep: true, sync: true } );}让我们来看一看执行完 resetStoreVM 后的 store 结构。现在的 store 中已经有了 getters 属性,并且 getters 和 state 都是响应式的。至此 vuex 的核心代码初始化部分已经分析完毕。源码里还包括一些插件的注册及暴露出来的 API 像 mapState mapGetters mapActions mapMutation等函数就不在这里介绍了,感兴趣的可以自行去源码里看看,比较好理解。这里就不做过多介绍。总结vuex的源码相比于vue的源码来说还是很好理解的。分析源码之前建议大家再细读一遍官方文档,遇到不太理解的地方记下来,带着问题去读源码,有目的性的研究,可以加深记忆。阅读的过程中,可以先写一个小例子,引入 clone 下来的源码,一步一步分析执行过程。 ...

December 13, 2018 · 8 min · jiezi

做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现

背景在端上为了提升App的灵活性, 快速解决万变的业务需求,开发者们探索了多种解决方案,如PhoneGap ,React Native ,Weex等,但在Flutter生态还没有好的解决方案。未来闲鱼都会基于Flutter 来跨端开发,如果突破发版周期,在不发版的情况下,完成业务需求,同时能兼容性能体验,无疑是更快的响应了业务需求。因此我们需要探索在Flutter生态下的动态化。方案选择借鉴Android 和Ios上的动态性方案,我们也思考了多种Flutter动态性方案。1.下载替换Flutter编译产物下载新的Flutter编译产物,替换 App 安装目录下的编译产物,来实现动态化,这在Android 端是可行的,但在Ios 端不可行。我们需要双端一体的解决方案,所以这不是最好选择。2.类似React Native 框架我们先来看看React Native 的架构React Native 要转为android(ios) 的原生组件,再进行渲染。用React Native的设计思路,把XML DSL转为Flutter 的原子widget组件,让Flutter 来渲染。技术上说是可行的,但这个成本很大,这会是一个庞大的工程,从投入产出比看,不是很好的选择3.页面动态组件框架由粗粒度的Widget组件动态拼装出页面,Native端已经有很多成熟的框架,如天猫的Tangram,淘宝的DinamicX,它在性能、动态性,开发周期上取得较好平衡。关键它能满足大部分的动态性需求,能解决问题。三种方案的比较图表如:根据实际动态性需求,从两端一致性,和性能,成本,动态性考虑,我们选择一个折中方案,页面动态组件的设计思路是一个不错的选择。页面动态组件框架在Flutter上使用粗力度的组件动态拼装来构建页面,需要一整套的前后端服务和工具。本文我们重点介绍前端界面渲染引擎过程。语法树的选择Native端的Tangram ,DinamicX等框架他们有个共同点,都是Xml或者Html 做为DSL。但是Flutter 是React Style语法。他自己的语法已经能很好的表达页面。无需要自定义的Xml 语法,自定义的逻辑表达式。用Flutter 源码做为DSL 能大大减轻开发,测试过程,不需要额外的工具支持。所以选择了Flutter 源码作为DSL,来实现动态化。如何解析DSLFlutter源码做为DSL,那我们需要对源码进行很好的解析和分析。Flutter analyzer给了我们一些思路,Flutter analyzer是一个代码风格检测工具。它使用package:analyzer来解析dart 源码,拿到ASTNode。看下Flutter analyze 源码结构,它使用了dart sdk 里面的 package:analyzerdart-sdk: analysis_server: analysis_server.dart handleRequest(Request request) analyzer: parseCompilationUnit() parseDartFile parseDirectives Flutter analyze 解析源码得到ASTNode过程。插件或者命令对analysis server发起请求,请求中带需要分析的文件path,和分析的类型,analysis_server经过使用 package:analyzer 获取 commilationUnit (ASTNode),再对astNode,经过computer分析,返回一个分析结果list。同样我们也可以把使用 package:analyzer 把源文件转换为commilationUnit (ASTNode),ASTNode是一个抽象语法树,抽象语法树(abstract syntax tree或者缩写为AST)是源代码的抽象语法结构的树状表现形式.所有利用抽象语法树能很好的解析dart 源码。解析渲染引擎下面重点介绍渲染模块架构图:1.源码解析过程1.AST树的结构如下面这段Flutter组件源码:import ‘package:flutter/material.dart’;class FollowedTopicCard extends StatelessWidget { @override Widget build(BuildContext context) { return new Container( padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0.0), child: new InkWell( child: new Center( child: const Text(‘Plugin example app’), ), onTap: () {}, ), ); }}它的AST结构:从AST结构看,他是有规律的.2.AST 到widget Node我们拿到了ASTNode,但ASTNode 和widget node tree 完全是两个不一样的概念,需要递归ASTNode 转化为 widget node tree.widget Node 需要的元素用Name 来记录是什么类型的widgetwidget的arguments放在 map里面widget 的literals 放在list 里面widget 的children 放在lsit 里面widget 的触发事件 函数map里面widget node 加fromjson ,tojson 方法可以在递归astNode tree 时候,识别InstanceCreationExpression来创建一个widget node。2.组件数据渲染框架sdk 中注册支持的组件,组件包括:a.原子组件:Flutter sdk 中的 Flutter 的widgetb.本地组件:本地写好到一个大颗粒的组件,卡片widget组件c.逻辑组件:本地包装了逻辑的widget组件d.动态组件:通过源码dsl动态渲染的widget具体代码如下: const Map<String, CreateDynamicApi> allWidget = <String, CreateDynamicApi>{ ‘Container’: wrapContainer, ………….} static Widget wrapContainer(Map<String, dynamic> pars) { return new Container( padding: pars[‘padding’], color: pars[‘color’], child: pars[‘child’], decoration: pars[‘decoration’], width: pars[‘width’], height: pars[‘height’], alignment: pars[‘alignment’] );}一般我们通过网络请求拿到的数据是一个map。比如源码中写了这么一个 ‘${data.urls[1]}‘AST 解析时候,拿到这么一个string,或者AST 表达式,通过解析它 ,肯定能从map 中拿到对应的值。3.逻辑和事件a.支持逻辑Flutter 概念万物都是widget ,可以把表达式,逻辑封装成一个自定义widget。如果在源码里面写了if else,变量等,会加重sdk解析的过程。所以把逻辑封装到widget中。这些逻辑widget,当作组件当成框架组件。b.支持事件把页面跳转,弹框,等服务,注册在sdk里面。约定使用者仅限sdk 的服务。4.规则和检测工具a.检测规则需要对源码的格式制定规则。比如不支持 直接写if else ,需要使用逻辑wiget组件来代替if else 语句。如果不制定规则,那ast Node 到widget node 的解析过程会很复杂。理论上都可以解析,只要解析sdk 够强大。制定规则,可以减轻sdk的解析逻辑。b.工具检测用工具来检测源码是否符合制定的规则,以保证所有的源码都能解析出来。性能和效果帧率大于50fps,体验上看比weex相同功能的页面更加流畅,Samsung galaxy s8上,感觉不出组件是通过动态渲染的.数据结构服务端请求到的数据,我们可以约定一种格式如下:class DataModel { Map<dynamic, dynamic> data; String type;}每个page 都是由组件组成的,每个组件的数据都是 DataModel来渲染。根据type 来找到对应的模版,模版+data,渲染出界面。动态模版管理模块我们把Widget Node Tree 转换为一个组件Json模版,它需要一套管理平台,来支持版本控制,动态下载,升级,回滚,更新等。框架的边界该框架是通过组件的组装,组件布局动态变更,页面布局动态变更来实现动态化。所以它适合运营变化较快的首页,详情,订单,我的等页面。一些复杂的逻辑需要封装在组件里面,把组件内置到框架中,当作本地组件。框架侧重于动态组件的组装,而引擎对于源码复杂的逻辑表达式的解析是弱化的。后续拓展1.和UI自动化的结合UI自动化 ,前面已经有文章介绍。UI自动化工具生成组件,再组件转为模版,动态下发,来快速解决运营需求。2.国际化的支持App在不同国家会有不同的功能,我们可以根据区域,来动态拼装我们的页面。3.千人千面根据不同的人群,来动态渲染不一样的界面。总结本文介绍动态化方案的渲染部分。该方案都在初探阶段,还有很多需要完善,后续会继续扩展和修改,等达到开源标准后,会考虑开源。动态方案是一个后端前端一体的方案,需要一整套工具配合,后续会有文章继续介绍整体的动态化方案。敬请关注闲鱼技术公共账号,也邀请您加入闲鱼一起探索有意思的技术。参考资料:Static Analysis:https://www.dartlang.org/guides/language/analysis-optionshttps://www.dartlang.org/tools/analyzerdart analyzer :https://pub.dartlang.org/packages/analyzerhttps://github.com/dart-lang/sdk/tree/master/pkg/analyzer_cli#dartanalyzerdartdevc:https://webdev.dartlang.org/tools/dartdevc本文作者:闲鱼技术-石磬阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 5, 2018 · 1 min · jiezi

开源 serverless 产品原理剖析 - Kubeless

背景Serverless 架构的出现让开发者不用过多地考虑传统的服务器采购、硬件运维、网络拓扑、资源扩容等问题,可以将更多的精力放在业务的拓展和创新上。随着 serverless 概念的深入人心,各大云计算厂商纷纷推出了各自的 serverless 产品,其中比较有代表性的有 AWS lambda、Azure Function、Google Cloud Functions、阿里云函数计算等。另外,CNCF 也于 2016 年创立了 Serverless Working Group,它致力于 cloud native 和 serverless 技术的结合。下图是 CNCF serverless 全景图,它将这些产品分成了工具型、安全型、框架型和平台型等类别。同时,容器以及容器编排工具的出现,大大降低了 serverless 产品的开发成本,促进了一大批优秀开源 serverless 产品的诞生,它们大多构建于 kubernetes 之上,如下图所示。Kubeless 简介本文将要介绍的 kubeless 便是这些开源 serverless 产品的典型代表。根据官方的定义,kubeless 是 kubernetes native 的无服务计算框架,它可以让用户在 kubernetes 之上使用 FaaS 构建高级应用程序。从 CNCF 视角,kubeless 属于平台型产品。Kubless 有三个核心概念:Functions - 代表需要被执行的用户代码,同时包含运行时依赖、构建指令等信息;Triggers - 代表和函数关联的事件源。如果把事件源比作生产者,函数比作执行者,那么触发器就是联系两者的桥梁;Runtime - 代表函数运行时所依赖的环境。原理剖析本章节将以 kubeless 为例介绍 serverless 产品需要具备的基本能力,以及 kubeless 是如何利用 K8s 现有功能来实现它们的。这些基本能力包括:敏捷构建 - 能够基于用户提交的源码迅速构建可执行的函数,简化部署流程;灵活触发 - 能够方便地基于各类事件触发函数的执行,并能方便快捷地集成新的事件源;自动伸缩 - 能够根据业务需求,自动完成扩容缩容,无须人工干预。本文所做的调研基于kubeless v1.0.0和k8s 1.13。敏捷构建CNCF 对函数生命周期的定义如下图所示。用户只需提供源码和函数说明,构建部署等工作通常由 serverless 平台完成。 因此,基于用户提交的源码迅速构建可执行函数是 serverless 产品必须具备的基础能力。在 kubeless 里,创建函数非常简单:kubeless function deploy hello –runtime python2.7 \ –from-file test.py \ –handler test.hello该命令各参数含义如下:hello:将要部署的函数名称;–runtime python2.7: 指定使用 python 2.7 作为运行环境。Kubeless 可供选择的运行环境请参考链接 runtimes。–from-file test.py:指定函数源码文件(支持 zip 格式)。–handler test.hello:指定使用 test.py 中的 hello 方法处理请求。函数资源与 K8s OperatorKubeless 函数是一个自定义 K8s 对象,本质上是 k8s operator。k8s operator 原理如下图所示:下面以 kubeless 函数为例,描述 K8s operator 的一般工作流程:使用 k8s 的 CustomResourceDefinition(CRD) 定义资源,这里创建了一个名为functions.kubeless.io的 CRD 来代表 kubeless 函数;创建一个 controller 监听自定义资源的 ADD、UPDATE、DELETE 事件并绑定 hander。这里创建了一个名为function-controller的 CRD controller,该 controller 会监听针对 function 的 ADD、UPDATE、DELETE 事件,并绑定 handler(参阅 AddEventHandler);用户执行创建、更新、删除自定义资源的命令;Controller 根据监听到的事件调用相应的 handler。除了函数外,下文将要介绍的 trigger 也是一个 k8s operator。函数构成Kubeless 的 function-controller监听到针对 function 的 ADD 事件后,会触发相应 handler 创建函数。一个函数由若干 K8s 对象组成,包括 ConfigMap、Service、Deployment、Pod 等,其结构如下图所示:ConfigMap函数中的 ConfigMap 用于描述函数源码和依赖。apiVersion: v1data: handler: test.hello # 函数依赖的第三方 python 库 requirements.txt: | kubernetes==2.0.0 # 函数源码 test.py: | def hello(event, context): print event return event[‘data’]kind: ConfigMapmetadata: labels: created-by: kubeless function: hello # 该 ConfigMap 名称 name: hello namespace: default…Service函数中的 Service 用于描述该函数的访问方式。该 Service 会与执行 function 逻辑的 Pods 相关联,类型是 ClusterIP。apiVersion: v1kind: Servicemetadata: labels: created-by: kubeless function: hello # 该 Service 名称 name: hello namespace: default …spec: clusterIP: 10.109.2.217 ports: - name: http-function-port port: 8080 protocol: TCP targetPort: 8080 selector: created-by: kubeless function: hello # Service 类型 type: ClusterIP…Deployment函数中的 Deployment 用于编排执行函数逻辑的 Pods,通过它可以描述函数期望的个数。apiVersion: extensions/v1beta1kind: Deploymentmetadata: labels: created-by: kubeless function: hello name: hello namespace: default …spec: # 指定函数期望的个数 replicas: 1…Pod函数中的 Pod 包含真正执行函数逻辑的容器。VolumesPod 中的 volumes 段指定了该函数的 ConfigMap。这会将 ConfigMap 中的源码和依赖添加到 volumeMounts.mountPath 指定的目录里面。从容器视角来看,文件路径为/src/test.py和 /src/requirements。… volumeMounts: - mountPath: /kubeless name: hello - mountPath: /src name: hello-depsvolumes:- emptyDir: {} name: hello- configMap: defaultMode: 420 name: hello…Init ContainerPod 中的 Init Container 主要作用如下:将源码和依赖文件拷贝到指定目录;安装第三方依赖。Func ContainerPod 中的 Func Container 会加载 Init Container 准备好的源码和依赖并执行函数。不同 runtime 加载代码的方式大同小异,可参考 kubeless.py,Handler.java。小结Kubeless 通过综合运用 K8s 中的多种组件以及利用各语言的动态加载能力实现了从用户源码到可执行的函数的构建逻辑;考虑了函数运行的安全性,通过 Security Context 机制限制容器中的进程以非 root 身份运行。灵活触发一款成熟的 serverless 产品需要具备灵活触发能力,以满足事件源的多样性需求,同时需要能够方便快捷地接入新事件源。CNCF 将函数的触发方式分成了如下图所示的几种类别,关于它们的详细介绍可参考链接 Function Invocation Types。对于 kubeless 的函数,最简单的触发方式是使用 kubeless CLI,另外还支持通过各种触发器。下表展示了 kubeless 函数目前支持的触发方式以及它们所属的类别。触发方式类别kubeless CLISynchronous Req/RepHttp TriggerSynchronous Req/RepCronjob TriggerJob (Master/Worker)Kafka TriggerAsync Message QueueNats TriggerAsync Message QueueKinesis TriggerMessage Stream下图展示了 kubeless 函数部分触发方式的原理:HTTP trigger如果希望通过发送 HTTP 请求触发函数执行,需要为函数创建 HTTP 触发器。 Kubeless 利用 K8s ingress 机制实现了 http trigger。Kubeless 创建了一个名为httptriggers.kubeless.io的 CRD 来代表 http trigger 对象。同时,kubeless 包含一个名为http-trigger-controller的 CRD controller,它会持续监听针对 http trigger 和 function 的 ADD、UPDATE、DELETE 事件,并执行对应的操作。以下命令将为函数 hello 创建一个名为http-hello的 http trigger,并指定选用 nginx 作为 gateway。kubeless trigger http create http-hello –function-name hello –gateway nginx –path echo –hostname example.com该命令会创建如下 ingress 对象,可以参考 CreateIngress 深入了解 ingress 的创建逻辑。apiVersion: extensions/v1beta1kind: Ingressmetadata: # 该 Ingress 的名字,即创建 http trigger 时指定的 name name: http-hello …spec: rules: - host: example.com http: paths: - backend: # 指向 kubeless 为函数 hello 创建的 ClusterIP 类型的 Service serviceName: hello servicePort: 8080 path: /echoIngress 只是用于描述路由规则,要让规则生效、实现请求转发,集群中需要有一个正在运行的 ingress controller。可供选择的 ingress controller 有 Contour、F5 BIG-IP Controller for Kubernetes、Kong Ingress Controllerfor Kubernetes、NGINX Ingress Controller for Kubernetes、Traefik 等。这种路由规则描述和路由功能实现相分离的思想很好地提现了 K8s 始终坚持的需求和供给分离的设计理念。上文中的命令在创建 trigger 时指定了 nginx 作为 gateway,因此需要部署一个 nginx-ingress-controller。该 controller 的基本工作原理如下:以 pod 的形式运行在独立的命名空间中;以 hostPort 的形式暴露出来供外界访问;内部运行着一个 nginx 实例;监听和 ingress、service 等资源相关的事件。如果发现这些事件最终会影响到路由规则,ingress controller 会采用向 Lua hander 发送新的 endpoints 列表或者直接修改 nginx.conf 并 reload nginx 等手段达到更新路由规则的目的。想要更深入地了解 nginx-ingress-controller 的工作原理可参考文章 how-it-works。完成上述工作后,我们便可以通过发送 HTTP 请求触发函数 hello 的执行:HTTP 请求首先会由 nginx-ingress-controller 中的 nginx 处理;Nginx 根据 nginx.conf 中的路由规则将请求转发给函数对应的 service;最后,请求会转发至挂载在 service 后的某个函数进行处理。样例如下:curl –data ‘{“Another”: “Echo”}’ \ –header “Host: example.com” \ –header “Content-Type:application/json” \ example.com/echo# 函数返回{“Another”: “Echo”}Cronjob trigger如果希望定期触发函数执行,需要为函数创建 cronjob 触发器。K8s 支持通过 CronJob 定期运行任务,kubeless 利用这个特性实现了 cronjob trigger。Kubeless 创建了一个名为cronjobtriggers.kubeless.io的 CRD 来代表 cronjob trigger 对象。同时,kubeless 包含一个名为cronjob-trigger-controller的 CRD controller,它会持续监听针对 cronjob trigger 和 function 的 ADD、UPDATE、DELETE 事件,并执行对应的操作。以下命令将为函数 hello 创建一个名为scheduled-invoke-hello的 cronjob trigger,该触发器每分钟会触发函数 hello 执行一次。kubeless trigger cronjob create scheduled-invoke-hello –function=hello –schedule="*/1 * * * *“该命令会创建如下 CronJob 对象,可以参考 EnsureCronJob 深入了解 CronJob 的创建逻辑。apiVersion: batch/v1beta1kind: CronJobmetadata: # 该 CronJob 的名字,即创建 cronjob trigger 时指定的 name name: scheduled-invoke-hello …spec: # 该 CronJob 的执行计划,即创建 cronjob trigger 时指定的 schedule schedule: */1 * * * * … jobTemplate: spec: activeDeadlineSeconds: 180 template: spec: containers: - args: - curl - -Lv # HTTP headers,包含 event-id、event-time、event-type、event-namespace 等信息 - ’ -H “event-id: xxx” -H “event-time: yyy” -H “event-type: application/json” -H “event-namespace: cronjobtrigger.kubeless.io”’ # kubeless 会为 function 创建一个 ClusterIP 类型的 Service # 可以根据 service 的 name、namespace 拼出 endpoint - http://hello.default.svc.cluster.local:8080 image: kubeless/unzip name: trigger restartPolicy: Never …自定义 trigger如果发现 kubeless 默认提供的触发器无法满足业务需求,可以自定义新的触发器。新触发器的构建流程如下:为新的事件源创建一个 CRD 来描述事件源触发器;在自定义资源对象的 spec 里描述该事件源的属性,例如 KafkaTriggerSpec、HTTPTriggerSpec;为该 CRD 创建一个 CRD controller。该 controller 需要持续监听针对事件源触发器和 function 的 CRUD 操作并作出正确的处理。例如,controller 监听到 function 的删除事件,需要把和该 function 关联的触发器一并删掉;当事件发生时,触发关联函数的执行。我们可以看到,自定义 trigger 的流程遵循了 K8s Operator 设计模式。小结Kubeless 提供了一些基本常用的触发器,如果有其他事件源也可以通过自定义触发器接入;不同事件源的接入方式不同,但最终都是通过访问函数 ClusterIP 类型的 service 触发函数执行。自动伸缩K8s 通过 Horizontal Pod Autoscaler 实现 pod 的自动水平伸缩。Kubeless 的 function 通过 K8s deployment 部署运行,因此天然可以利用 HPA 实现自动伸缩。度量数据获取自动伸缩的第一步是要让 HPA 能够获取度量数据。目前,kubeless 中的函数支持基于 cpu 和 qps 这两种指标进行自动伸缩。下图展示了 HPA 获取这两种度量数据的途径。内置度量指标 cpuCPU 使用率属于内置度量指标,对于这类指标 HPA 可以通过 metrics API 从 Metrics Server 中获取数据。Metrics Server 是 Heapster 的继承者,它可以通过kubernetes.summary_api从 Kubelet、cAdvisor 中获取度量数据。自定义度量指标 qpsQPS 属于自定义度量指标,想要获取这类指标的度量数据需要完成下列步骤。部署用于存储度量数据的系统,这里选择已经被纳入 CNCF 的 Prometheus。Prometheus 是一套开源监控&告警&时序数据解决方案,并且被 DigitalOcean、Red Hat、SUSE 和 Weaveworks 这些 cloud native 领导者广泛使用;采集度量数据,并写入部署好的 Prometheus 中。Kubeless 提供的函数框架会在函数每次被调用时,将下列度量数据 function_duration_seconds、function_calls_total、function_failures_total 写入 Prometheus(可参考 python 样例)。部署实现了 custom metrics API 的 custom API server。这里,因为度量数据被存入了 Prometheus,因此选择部署 k8s-prometheus-adapter,它可以从 Prometheus 中获取度量数据。完成上述步骤后,HPA 就可以通过 custom metrics API 从 Prometheus Adapter 中获取 qps 度量数据。详细配置步骤可参考文章 kubeless-autoscaling。K8s 度量指标简介有时基于 cpu 和 qps 这两种度量指标对函数进行自动伸缩还远远不够。如果希望基于其它度量指标,需要了解 K8s 定义的度量指标类型及其获取方式。目前,K8s 1.13 版本支持的度量指标类型如下:准备好相应的度量数据和获取数据的组件,HPA 就能基于它们对函数进行自动伸缩。更多关于 K8s 度量指标的介绍可参考文章 hpa-external-metrics。度量数据使用知道了 HPA 获取度量数据的途径后,下面描述 HPA 如何基于这些数据对函数进行自动伸缩。基于 cpu 使用率假设已经存在一个名为 hello 的函数,以下命令将为该函数创建一个基于 cpu 使用率的 HPA,它将运行该函数的 pod 数量控制在 1 到 3 之间,并通过增加或减少 pod 个数使得所有 pod 的平均 cpu 使用率维持在 70%。kubeless autoscale create hello –metric=cpu –min=1 –max=3 –value=70Kubeless 使用的是 autoscaling/v2alpha1 版本的 HPA API,该命令将要创建的 HPA 如下:kind: HorizontalPodAutoscalerapiVersion: autoscaling/v2alpha1metadata: name: hello namespace: default labels: created-by: kubeless function: hellospec: scaleTargetRef: kind: Deployment name: hello minReplicas: 1 maxReplicas: 3 metrics: - type: Resource resource: name: cpu targetAverageUtilization: 70该 HPA 计算目标 pod 数量的公式如下:TargetNumOfPods = ceil(sum(CurrentPodsCPUUtilization) / Target)基于 qps以下命令将为函数 hello 创建一个基于 qps 的 HPA,它将运行该函数的 pod 数量控制在 1 到 5 之间,并通过增加或减少 pod 个数确保所有挂在服务 hello 后的 pod 每秒能处理的请求次数之和达到 2000。kubeless autoscale create hello –metric=qps –min=1 –max=5 –value=2k该命令将要创建的 HPA 如下:kind: HorizontalPodAutoscalerapiVersion: autoscaling/v2alpha1metadata: name: hello namespace: default labels: created-by: kubeless function: hellospec: scaleTargetRef: kind: Deployment name: hello minReplicas: 1 maxReplicas: 5 metrics: - type: Object object: metricName: function_calls target: apiVersion: autoscaling/v2beta1 kind: Service name: hello targetValue: 2k基于多项指标如果计划基于多项度量指标对函数进行自动伸缩,需要直接为运行 function 的 deployment 创建 HPA。使用如下 yaml 文件可以为函数 hello 创建一个名为hello-cpu-and-memory的 HPA,它将运行该函数的 pod 数量控制在 1 到 10 之间,并尝试让所有 pod 的平均 cpu 使用率维持在 50%,平均 memory 使用量维持在 200MB。对于多项度量指标,K8s 会计算出每项指标需要的 pod 数量,取其中的最大值作为最终的目标 pod 数量。kind: HorizontalPodAutoscalerapiVersion: autoscaling/v2alpha1metadata: name: hello-cpu-and-memory namespace: default labels: created-by: kubeless function: hellospec: scaleTargetRef: kind: Deployment name: hello minReplicas: 1 maxReplicas: 10 metrics: - type: Resource resource: name: cpu targetAverageUtilization: 50 - type: Resource resource: name: memory targetAverageValue: 200Mi自动伸缩策略一个理想的自动伸缩策略应当处理好下列场景:当负载激增时,函数能迅速扩展以应对突发流量;当负载下降时,函数能立即收缩以节省资源消耗;具备抗噪声干扰能力,能够精确计算出目标容量;能够避免自动伸缩过于频繁造成系统抖动。Kubeless 依赖的 HPA 充分考虑了上述情形,不断改进和完善其使用的自动伸缩策略。下面以 K8s 1.13 版本为例描述该策略。如果想要更加深入地了解策略原理请参考链接 horizontal。HPA 每隔一段时间会根据获取的度量数据同步一次和该 HPA 关联的 RC / Deployment 中的 pod 个数,时间间隔通过 kube-controller-manager 的参数–horizontal-pod-autoscaler-sync-period指定,默认为 15s。在每一次同步过程中,HPA 需要经历如下图所示的计算流程。计算目标副本数分别计算 HPA 列表中每项指标需要的 pod 数量,记为 replicaCountProposal。选择其中的最大值作为 metricDesiredReplicas。在计算每项指标的 replicaCountProposal 过程中会考虑下列因素:允许目标度量值和实际度量值存在一定程度的误差,如果在误差范围内直接使用 currentReplicas 作为 replicaCountProposal。这样做是为了在可接受范围内避免伸缩过于频繁造成系统抖动,该误差值可以通过 kube-controller-manager 的参数–horizontal-pod-autoscaler-tolerance指定,默认值是 0.1。当一个 pod 刚刚启动时,该 pod 反映的度量值往往不是很准确,HPA 会将这种 pod 视为 unready。在计算度量值时,HPA 会跳过处于 unready 状态的 pod。这样做是为了消除噪声干扰,可以通过 kube-controller-manager 的参数–horizontal-pod-autoscaler-cpu-initialization-period(默认为 5 分钟)和–horizontal-pod-autoscaler-initial-readiness-delay(默认为 30 秒)调整 pod 被认为处于 unready 状态的时间。平滑目标副本数将最近一段时间计算出的 metricDesiredReplicas 记录下来,取其中的最大值作为 stabilizedRecommendation。这样做是为了让缩容过程变得平滑,消除度量数据异常波动造成的影响。该时间段可以通过参数–horizontal-pod-autoscaler-downscale-stabilization-window指定,默认为 5 分钟。规范目标副本数限制 desiredReplicas 最大为 currentReplicas * scaleUpLimitFactor,这样做是为了防止因 采集到了“虚假的”度量数据造成扩容过快。目前 scaleUpLimitFactor 无法通过参数设定,其值固定为 2。限制 desiredReplicas 大于等于 hpaMinReplicas,小于等于 hpaMaxReplicas。执行扩容缩容操作如果通过上述步骤计算出的 desiredReplicas 不等于 currentReplicas,则“执行”扩容缩容操作。这里所说的执行只是将 desiredReplicas 赋值给 RC / Deployment 中的 replicas,pod 的创建销毁会由 kube-scheduler 和 worker node 上的 kubelet 异步完成的。小结Kubeless 提供的自动伸缩功能是对 K8s HPA 的简单封装,避免了将创建 HPA 的复杂细节直接暴露给用户。Kubeless 目前提供的度量指标过少,功能过于简单。如果用户希望基于新的度量指标、综合多项度量指标或者调整自动伸缩的效果,需要深入了解 HPA 的细节。目前 HPA 的扩容缩容策略是基于既成事实被动地调整目标副本数,还无法根据历史规律预测性地进行扩容缩容。总结Kubeless 基于 K8s 提供了较为完整的 serverless 解决方案,但和一些商业 serverless 产品还存在一定差距:Kubeless 并未在镜像拉取、代码下载、容器启动等方面做过多优化,导致函数冷启动时间过长;Kubeless 并未过多考虑多租户的问题,如果希望多个用户的函数运行在同一个集群里,还需要进行二次开发。本文作者:吴波bruce_wu阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 3, 2018 · 6 min · jiezi

AspectJ在Spring中的使用

在上一篇AspectJ的入门中,简单的介绍了下AspectJ的使用,主要是以AspectJ的example作为例子。介绍完后也留下了几个问题:1)我们在spring中并没有看到需要aspectj之类的关键词,而是使用java代码就可以了,这是如何做到的2)Spring中如何做到不使用特殊的编译器实现aop的(AspectJ如何在运行期使用)3)Spring源码中与aspectJ 相关的AjType究竟是啥?这篇文章会继续试着解决这几个问题。aspectJ的几种织入方式compile-time、post-compile 和 load-time Weavers首先了解下AspectJ的几种织入方式,分别是compile-time、post-compile 和 load-time,分别对应着编译期、后编译期、加载期织入编译期织入首先是编译期织入,上一篇博客所介绍的方式就是使用的编译期织入。很容易理解,普通的java源码+ aspectJ特殊语法的‘配置’ 文件 + aspectJ特殊的编译器,编译时候生成已织入后的.class文件,运行时直接运行即可。后编译期织入后编译期织入和编译期的不同在于,织入的是class字节码或者jar文件。这种形式,可以织入一个已经织入过一次的切面。同样这种情况也需要特殊的编译器加载期织入加载期顾名思义,是在类被加载进虚拟机之前织入,使用这种方式,须使用AspectJ agent。了解了这些概念,下面就要知道,spring是使用哪种呢?spring哪一种都不是,spring是在运行期进行的织入。Spring 如何使用AspectJAspectJ 本身是不支持运行期织入的,日常使用时候,我们经常回听说,spring 使用了aspectJ实现了aop,听起来好像spring的aop完全是依赖于aspectJ其实spring对于aop的实现是通过动态代理(jdk的动态代理或者cglib的动态代理),它只是使用了aspectJ的Annotation,并没有使用它的编译期和织入器,关于这个可以看这篇文章 ,也就是说spring并不是直接使用aspectJ实现aop的spring aop与aspectJ的区别看了很多篇博客以及源码,我对spring aop与aspectJ的理解大概是这样;1)spring aop 使用AspectJ语法的一个子集,一些method call, class member set/get 等aspectJ支持的语法它都不支持2)spring aop 底层是动态代理,所以受限于这点,有些增强就做不到,比如 调用自己的方法就无法走代理看下下面的例子:@Componentpublic class A{ public void method1(){ method2(); } public void method2(){ //… }}这个时候method2是无法被切到的,要想被切到可以通过如下奇葩的方式:@Componentpublic class A{ @Autowired private A a; public void method1(){ a.method2(); } public void method2(){ //… }}之前碰到这样的问题时,我还特别不能理解,现在想下aop的底层实现方式就很容易理解了。在之前写的jdk动态代理与cglib动态代理实现原理,我们知道了jdk动态代理是通过动态生成一个类的方式实现的代理,也就是说代理是不会修改底层类字节码的,所以可能生成的代理方法是这样的public void method1(){ //执行一段代码 a.method1() //执行一段代码}public void method2(){ //执行一段代码 a.method2() //执行一段代码}回头看a.method1()的源码,也就明白了,为啥method2()没有被切到,因为a.method1()执行的方法,最后调用的不是 代理对象.method2(),而是它自己的method2()(this.method2()) 这个方法本身没有任何改动反观aspectJ,aspectJ是在编译期修改了方法(类本身的字节码被改了),所以可以很轻松地实现调用自己的方法时候的增强。3)spring aop的代理必须依赖于bean被spring管理,所以如果项目没有使用spring,又想使用aop,那就只能使用aspectJ了(不过现在没有用spring的项目应该挺少的吧。。。)4)aspectJ由于是编译期进行的织入,性能会比spring好一点5)spring可以通过@EnableLoadTimeWeaving 开启加载期织入(只是知道这个东西,没怎么研究。。有兴趣的可以自己去研究下)6)spring aop很多概念和aspectJ是一致的AspectJ的注解在spring aop中的应用了解了spring与aspectJ的关系后,就能更清晰的了解spring 的aop了。先说明一点,虽然我介绍aspect的配置时,一直介绍的aspectJ文件配置方式,但是aspectJ本身是支持注解方式配置的。可以看官方文档,注解在aspectJ中的使用而spring 使用了aspectJ注解的一小部分(正如前面所说的,受限于jdk的动态代理,spring只支持方法级别的切面)回头看看AjType回头看看之前看到的这段源码,什么是AjType,经过aspectJ解析器解析后对类的一种描述,比如正常的方法可能是这样/* * 配置前置通知,使用在方法aspect()上注册的切入点 * 同时接受JoinPoint切入点对象,可以没有该参数 /@Before(“aspect()")public void before(JoinPoint joinPoint) { log.info(“before " + joinPoint);}在AjType中就能获取到很多其他的aspectJ所需的相关信息(除了java反射所能获取到的信息以外)/* * Return the pointcut object representing the specified pointcut declared by this type /public Pointcut getDeclaredPointcut(String name) throws NoSuchPointcutException;/* * Return the pointcut object representing the specified public pointcut */public Pointcut getPointcut(String name) throws NoSuchPointcutException;比如看着两个方法,可以获取到切入点信息。在看看PerClauseKind.SINGLETON 这里就复用了aspectJ的概念,详细可以看这篇文章最后部分总结下这篇文章回答了之前学习aspectJ时候碰到的几个问题,然后讨论了下aspectJ在spring中的应用。最大的收获是了解了spring与aspectJ 的关系,了解了两者对aop的不同实现所造成的使用上的影响。以后当遇到了spring aop相关的概念如果不理解,可以去aspectJ上去搜搜看了 。参考文章:Intro to AspectJspring 使用 load-time weavingspring aop和 aspectJ 的比较本文作者:端吉阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 22, 2018 · 1 min · jiezi

面试官问:能否模拟实现bind

前言用过React的同学都知道,经常会使用bind来绑定this。import React, { Component } from ‘react’;class TodoItem extends Component{ constructor(props){ super(props); this.handleClick = this.handleClick.bind(this); } handleClick(){ console.log(‘handleClick’); } render(){ return ( <div onClick={this.handleClick}>点击</div> ); };}export default TodoItem;那么面试官可能会问是否想过bind到底做了什么,怎么模拟实现呢。附上之前写文章写过的一段话:已经有很多模拟实现bind的文章,为什么自己还要写一遍呢。学习就好比是座大山,人们沿着不同的路登山,分享着自己看到的风景。你不一定能看到别人看到的风景,体会到别人的心情。只有自己去登山,才能看到不一样的风景,体会才更加深刻。先看一下bind是什么。从上面的React代码中,可以看出bind执行后是函数,并且每个函数都可以执行调用它。眼见为实,耳听为虚。读者可以在控制台一步步点开例子1中的obj:var obj = {};console.log(obj);console.log(typeof Function.prototype.bind); // functionconsole.log(typeof Function.prototype.bind()); // functionconsole.log(Function.prototype.bind.name); // bindconsole.log(Function.prototype.bind().name); // bound因此可以得出结论1:1、bind是Functoin原型链中Function.prototype的一个属性,每个函数都可以调用它。<br/>2、bind本身是一个函数名为bind的函数,返回值也是函数,函数名是bound 。(打出来就是bound加上一个空格)。知道了bind是函数,就可以传参,而且返回值’bound ‘也是函数,也可以传参,就很容易写出例子2:后文统一 bound 指原函数original bind之后返回的函数,便于说明。var obj = { name: ‘轩辕Rowboat’,};function original(a, b){ console.log(this.name); console.log([a, b]); return false;}var bound = original.bind(obj, 1);var boundResult = bound(2); // ‘轩辕Rowboat’, [1, 2]console.log(boundResult); // falseconsole.log(original.bind.name); // ‘bind’console.log(original.bind.length); // 1console.log(original.bind().length); // 2 返回original函数的形参个数console.log(bound.name); // ‘bound original’console.log((function(){}).bind().name); // ‘bound ‘console.log((function(){}).bind().length); // 0由此可以得出结论2:1、调用bind的函数中的this指向bind()函数的第一个参数。2、传给bind()的其他参数接收处理了,bind()之后返回的函数的参数也接收处理了,也就是说合并处理了。3、并且bind()后的name为bound + 空格 + 调用bind的函数名。如果是匿名函数则是bound + 空格。4、bind后的返回值函数,执行后返回值是原函数(original)的返回值。5、bind函数形参(即函数的length)是1。bind后返回的bound函数形参不定,根据绑定的函数原函数(original)形参个数确定。根据结论2:我们就可以简单模拟实现一个简版bindFn// 第一版 修改this指向,合并参数Function.prototype.bindFn = function bind(thisArg){ if(typeof this !== ‘function’){ throw new TypeError(this + ‘must be a function’); } // 存储函数本身 var self = this; // 去除thisArg的其他参数 转成数组 var args = [].slice.call(arguments, 1); var bound = function(){ // bind返回的函数 的参数转成数组 var boundArgs = [].slice.call(arguments); // apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果 return self.apply(thisArg, args.concat(boundArgs)); } return bound;}// 测试var obj = { name: ‘轩辕Rowboat’,};function original(a, b){ console.log(this.name); console.log([a, b]);}var bound = original.bindFn(obj, 1);bound(2); // ‘轩辕Rowboat’, [1, 2]如果面试官看到你答到这里,估计对你的印象60、70分应该是会有的。但我们知道函数是可以用new来实例化的。那么bind()返回值函数会是什么表现呢。接下来看例子3:var obj = { name: ‘轩辕Rowboat’,};function original(a, b){ console.log(’this’, this); // original {} console.log(’typeof this’, typeof this); // object this.name = b; console.log(’name’, this.name); // 2 console.log(’this’, this); // original {name: 2} console.log([a, b]); // 1, 2}var bound = original.bind(obj, 1);var newBoundResult = new bound(2);console.log(newBoundResult, ’newBoundResult’); // original {name: 2}从例子3种可以看出this指向了new bound()生成的新对象。可以分析得出结论3:1、bind原先指向obj的失效了,其他参数有效。2、new bound的返回值是以original原函数构造器生成的新对象。original原函数的this指向的就是这个新对象。另外前不久写过一篇文章:面试官问:能否模拟实现JS的new操作符。简单摘要:new做了什么:1.创建了一个全新的对象。<br/>2.这个对象会被执行[[Prototype]](也就是__proto__)链接。<br/>3.生成的新对象会绑定到函数调用的this。<br/>4.通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。<br/>5.如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。所以相当于new调用时,bind的返回值函数bound内部要模拟实现new实现的操作。话不多说,直接上代码。// 第三版 实现new调用Function.prototype.bindFn = function bind(thisArg){ if(typeof this !== ‘function’){ throw new TypeError(this + ’ must be a function’); } // 存储调用bind的函数本身 var self = this; // 去除thisArg的其他参数 转成数组 var args = [].slice.call(arguments, 1); var bound = function(){ // bind返回的函数 的参数转成数组 var boundArgs = [].slice.call(arguments); var finalArgs = args.concat(boundArgs); // new 调用时,其实this instanceof bound判断也不是很准确。es6 new.target就是解决这一问题的。 if(this instanceof bound){ // 这里是实现上文描述的 new 的第 1, 2, 4 步 // 1.创建一个全新的对象 // 2.并且执行[[Prototype]]链接 // 4.通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。 // self可能是ES6的箭头函数,没有prototype,所以就没必要再指向做prototype操作。 if(self.prototype){ // ES5 提供的方案 Object.create() // bound.prototype = Object.create(self.prototype); // 但 既然是模拟ES5的bind,那浏览器也基本没有实现Object.create() // 所以采用 MDN ployfill方案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create function Empty(){} Empty.prototype = self.prototype; bound.prototype = new Empty(); } // 这里是实现上文描述的 new 的第 3 步 // 3.生成的新对象会绑定到函数调用的this。 var result = self.apply(this, finalArgs); // 这里是实现上文描述的 new 的第 5 步 // 5.如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error), // 那么new表达式中的函数调用会自动返回这个新的对象。 var isObject = typeof result === ‘object’ && result !== null; var isFunction = typeof result === ‘function’; if(isObject || isFunction){ return result; } return this; } else{ // apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果 return self.apply(thisArg, finalArgs); } }; return bound;}面试官看到这样的实现代码,基本就是满分了,心里独白:这小伙子/小姑娘不错啊。不过可能还会问this instanceof bound不准确问题。上文注释中提到this instanceof bound也不是很准确,ES6 new.target很好的解决这一问题,我们举个例子4:instanceof 不准确,ES6 new.target很好的解决这一问题function Student(name){ if(this instanceof Student){ this.name = name; console.log(’name’, name); } else{ throw new Error(‘必须通过new关键字来调用Student。’); }}var student = new Student(‘轩辕’);var notAStudent = Student.call(student, ‘Rowboat’); // 不抛出错误,且执行了。console.log(student, ‘student’, notAStudent, ’notAStudent’);function Student2(name){ if(typeof new.target !== ‘undefined’){ this.name = name; console.log(’name’, name); } else{ throw new Error(‘必须通过new关键字来调用Student2。’); }}var student2 = new Student2(‘轩辕’);var notAStudent2 = Student2.call(student2, ‘Rowboat’);console.log(student2, ‘student2’, notAStudent2, ’notAStudent2’); // 抛出错误细心的同学可能会发现了这版本的代码没有实现bind后的bound函数的nameMDN Function.name和lengthMDN Function.length。面试官可能也发现了这一点继续追问,如何实现,或者问是否看过es5-shim的源码实现L201-L335。如果不限ES版本。其实可以用ES5的Object.defineProperties来实现。Object.defineProperties(bound, { ’length’: { value: self.length, }, ’name’: { value: ‘bound ’ + self.name, }});es5-shim的源码实现bind直接附上源码(有删减注释和部分修改等)var $Array = Array;var ArrayPrototype = $Array.prototype;var $Object = Object;var array_push = ArrayPrototype.push;var array_slice = ArrayPrototype.slice;var array_join = ArrayPrototype.join;var array_concat = ArrayPrototype.concat;var $Function = Function;var FunctionPrototype = $Function.prototype;var apply = FunctionPrototype.apply;var max = Math.max;// 简版 源码更复杂些。var isCallable = function isCallable(value){ if(typeof value !== ‘function’){ return false; } return true;};var Empty = function Empty() {};// 源码是 defineProperties// 源码是bind笔者改成bindFn便于测试FunctionPrototype.bindFn = function bind(that) { var target = this; if (!isCallable(target)) { throw new TypeError(‘Function.prototype.bind called on incompatible ’ + target); } var args = array_slice.call(arguments, 1); var bound; var binder = function () { if (this instanceof bound) { var result = apply.call( target, this, array_concat.call(args, array_slice.call(arguments)) ); if ($Object(result) === result) { return result; } return this; } else { return apply.call( target, that, array_concat.call(args, array_slice.call(arguments)) ); } }; var boundLength = max(0, target.length - args.length); var boundArgs = []; for (var i = 0; i < boundLength; i++) { array_push.call(boundArgs, ‘$’ + i); } // 这里是Function构造方式生成形参length $1, $2, $3… bound = $Function(‘binder’, ‘return function (’ + array_join.call(boundArgs, ‘,’) + ‘){ return binder.apply(this, arguments); }’)(binder); if (target.prototype) { Empty.prototype = target.prototype; bound.prototype = new Empty(); Empty.prototype = null; } return bound;};你说出es5-shim源码bind实现,感慨这代码真是高效、严谨。面试官心里独白可能是:你就是我要找的人,薪酬福利你可以和HR去谈下。最后总结一下1、bind是Function原型链中的Function.prototype的一个属性,它是一个函数,修改this指向,合并参数传递给原函数,返回值是一个新的函数。2、bind返回的函数可以通过new调用,这时提供的this的参数被忽略,指向了new生成的全新对象。内部模拟实现了new操作符。3、es5-shim源码模拟实现bind时用Function实现了length。事实上,平时其实很少需要使用自己实现的投入到生成环境中。但面试官通过这个面试题能考察很多知识。比如this指向,原型链,闭包,函数等知识,可以扩展很多。读者发现有不妥或可改善之处,欢迎指出。另外觉得写得不错,可以点个赞,也是对笔者的一种支持。文章中的例子和测试代码放在github中bind模拟实现 github。bind模拟实现 预览地址 F12看控制台输出,结合source面板查看效果更佳。// 最终版 删除注释 详细注释版请看上文Function.prototype.bind = Function.prototype.bind || function bind(thisArg){ if(typeof this !== ‘function’){ throw new TypeError(this + ’ must be a function’); } var self = this; var args = [].slice.call(arguments, 1); var bound = function(){ var boundArgs = [].slice.call(arguments); var finalArgs = args.concat(boundArgs); if(this instanceof bound){ if(self.prototype){ function Empty(){} Empty.prototype = self.prototype; bound.prototype = new Empty(); } var result = self.apply(this, finalArgs); var isObject = typeof result === ‘object’ && result !== null; var isFunction = typeof result === ‘function’; if(isObject || isFunction){ return result; } return this; } else{ return self.apply(thisArg, finalArgs); } }; return bound;}参考OshotOkill翻译的 深入理解ES6 简体中文版 - 第三章 函数(虽然我是看的纸质书籍,但推荐下这本在线的书)MDN Function.prototype.bind冴羽: JavaScript深入之bind的模拟实现《react状态管理与同构实战》候策:从一道面试题,到“我可能看了假源码”关于作者:常以轩辕Rowboat为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。个人博客segmentfault个人主页掘金个人主页知乎github ...

November 21, 2018 · 4 min · jiezi

ES6 系列之我们来聊聊装饰器

Decorator装饰器主要用于:装饰类装饰方法或属性装饰类@annotationclass MyClass { }function annotation(target) { target.annotated = true;}装饰方法或属性class MyClass { @readonly method() { }}function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}Babel安装编译我们可以在 Babel 官网的 Try it out,查看 Babel 编译后的代码。不过我们也可以选择本地编译:npm initnpm install –save-dev @babel/core @babel/clinpm install –save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties新建 .babelrc 文件{ “plugins”: [ ["@babel/plugin-proposal-decorators", { “legacy”: true }], ["@babel/plugin-proposal-class-properties", {“loose”: true}] ]}再编译指定的文件babel decorator.js –out-file decorator-compiled.js装饰类的编译编译前:@annotationclass MyClass { }function annotation(target) { target.annotated = true;}编译后:var _class;let MyClass = annotation(_class = class MyClass {}) || _class;function annotation(target) { target.annotated = true;}我们可以看到对于类的装饰,其原理就是:@decoratorclass A {}// 等同于class A {}A = decorator(A) || A;装饰方法的编译编译前:class MyClass { @unenumerable @readonly method() { }}function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}function unenumerable(target, name, descriptor) { descriptor.enumerable = false; return descriptor;}编译后:var _class;function _applyDecoratedDescriptor(target, property, decorators, descriptor, context ) { /** * 第一部分 * 拷贝属性 / var desc = {}; Object“ke” + “ys”.forEach(function(key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if (“value” in desc || desc.initializer) { desc.writable = true; } /* * 第二部分 * 应用多个 decorators / desc = decorators .slice() .reverse() .reduce(function(desc, decorator) { return decorator(target, property, desc) || desc; }, desc); /* * 第三部分 * 设置要 decorators 的属性 / if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object[“define” + “Property”](target, property, desc); desc = null; } return desc;}let MyClass = ((_class = class MyClass { method() {}}),_applyDecoratedDescriptor( _class.prototype, “method”, [readonly], Object.getOwnPropertyDescriptor(_class.prototype, “method”), _class.prototype),_class);function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}装饰方法的编译源码解析我们可以看到 Babel 构建了一个 _applyDecoratedDescriptor 函数,用于给方法装饰。Object.getOwnPropertyDescriptor()在传入参数的时候,我们使用了一个 Object.getOwnPropertyDescriptor() 方法,我们来看下这个方法:Object.getOwnPropertyDescriptor() 方法返回指定对象上的一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)顺便注意这是一个 ES5 的方法。举个例子:const foo = { value: 1 };const bar = Object.getOwnPropertyDescriptor(foo, “value”);// bar {// value: 1,// writable: true// enumerable: true,// configurable: true,// }const foo = { get value() { return 1; } };const bar = Object.getOwnPropertyDescriptor(foo, “value”);// bar {// get: /the getter function/,// set: undefined// enumerable: true,// configurable: true,// }第一部分源码解析在 _applyDecoratedDescriptor 函数内部,我们首先将 Object.getOwnPropertyDescriptor() 返回的属性描述符对象做了一份拷贝:// 拷贝一份 descriptorvar desc = {};Object“ke” + “ys”.forEach(function(key) { desc[key] = descriptor[key];});desc.enumerable = !!desc.enumerable;desc.configurable = !!desc.configurable;// 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setterif (“value” in desc || desc.initializer) { desc.writable = true;}那么 initializer 属性是什么呢?Object.getOwnPropertyDescriptor() 返回的对象并不具有这个属性呀,确实,这是 Babel 的 Class 为了与 decorator 配合而产生的一个属性,比如说对于下面这种代码:class MyClass { @readonly born = Date.now();}function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}var foo = new MyClass();console.log(foo.born);Babel 就会编译为:// …(_descriptor = _applyDecoratedDescriptor(_class.prototype, “born”, [readonly], { configurable: true, enumerable: true, writable: true, initializer: function() { return Date.now(); }}))// …此时传入 _applyDecoratedDescriptor 函数的 descriptor 就具有 initializer 属性。第二部分源码解析接下是应用多个 decorators:/* * 第二部分 * @type {[type]} /desc = decorators .slice() .reverse() .reduce(function(desc, decorator) { return decorator(target, property, desc) || desc; }, desc);对于一个方法应用了多个 decorator,比如:class MyClass { @unenumerable @readonly method() { }}Babel 会编译为:_applyDecoratedDescriptor( _class.prototype, “method”, [unenumerable, readonly], Object.getOwnPropertyDescriptor(_class.prototype, “method”), _class.prototype)在第二部分的源码中,执行了 reverse() 和 reduce() 操作,由此我们也可以发现,如果同一个方法有多个装饰器,会由内向外执行。第三部分源码解析/* * 第三部分 * 设置要 decorators 的属性 /if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined;}if (desc.initializer === void 0) { Object[“define” + “Property”](target, property, desc); desc = null;}return desc;如果 desc 有 initializer 属性,意味着当装饰的是类的属性时,会将 value 的值设置为:desc.initializer.call(context)而 context 的值为 _class.prototype,之所以要 call(context),这也很好理解,因为有可能class MyClass { @readonly value = this.getNum() + 1; getNum() { return 1; }}最后无论是装饰方法还是属性,都会执行:Object[“define” + “Property”](target, property, desc);由此可见,装饰方法本质上还是使用 Object.defineProperty() 来实现的。应用1.log为一个方法添加 log 函数,检查输入的参数:class Math { @log add(a, b) { return a + b; }}function log(target, name, descriptor) { var oldValue = descriptor.value; descriptor.value = function(…args) { console.log(Calling ${name} with, args); return oldValue.apply(this, args); }; return descriptor;}const math = new Math();// Calling add with [2, 4]math.add(2, 4);再完善点:let log = (type) => { return (target, name, descriptor) => { const method = descriptor.value; descriptor.value = (…args) => { console.info((${type}) 正在执行: ${name}(${args}) = ?); let ret; try { ret = method.apply(target, args); console.info((${type}) 成功 : ${name}(${args}) =&gt; ${ret}); } catch (error) { console.error((${type}) 失败: ${name}(${args}) =&gt; ${error}); } return ret; } }};2.autobindclass Person { @autobind getPerson() { return this; }}let person = new Person();let { getPerson } = person;getPerson() === person;// true我们很容易想到的一个场景是 React 绑定事件的时候:class Toggle extends React.Component { @autobind handleClick() { console.log(this) } render() { return ( <button onClick={this.handleClick}> button </button> ); }}我们来写这样一个 autobind 函数:const { defineProperty, getPrototypeOf} = Object;function bind(fn, context) { if (fn.bind) { return fn.bind(context); } else { return function autobind() { return fn.apply(context, arguments); }; }}function createDefaultSetter(key) { return function set(newValue) { Object.defineProperty(this, key, { configurable: true, writable: true, enumerable: true, value: newValue }); return newValue; };}function autobind(target, key, { value: fn, configurable, enumerable }) { if (typeof fn !== ‘function’) { throw new SyntaxError(@autobind can only be used on functions, not: ${fn}); } const { constructor } = target; return { configurable, enumerable, get() { /* * 使用这种方式相当于替换了这个函数,所以当比如 * Class.prototype.hasOwnProperty(key) 的时候,为了正确返回 * 所以这里做了 this 的判断 */ if (this === target) { return fn; } const boundFn = bind(fn, this); defineProperty(this, key, { configurable: true, writable: true, enumerable: false, value: boundFn }); return boundFn; }, set: createDefaultSetter(key) };}3.debounce有的时候,我们需要对执行的方法进行防抖处理:class Toggle extends React.Component { @debounce(500, true) handleClick() { console.log(’toggle’) } render() { return ( <button onClick={this.handleClick}> button </button> ); }}我们来实现一下:function _debounce(func, wait, immediate) { var timeout; return function () { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } }}function debounce(wait, immediate) { return function handleDescriptor(target, key, descriptor) { const callback = descriptor.value; if (typeof callback !== ‘function’) { throw new SyntaxError(‘Only functions can be debounced’); } var fn = _debounce(callback, wait, immediate) return { …descriptor, value() { fn() } }; }}4.time用于统计方法执行的时间:function time(prefix) { let count = 0; return function handleDescriptor(target, key, descriptor) { const fn = descriptor.value; if (prefix == null) { prefix = ${target.constructor.name}.${key}; } if (typeof fn !== ‘function’) { throw new SyntaxError(@time can only be used on functions, not: ${fn}); } return { …descriptor, value() { const label = ${prefix}-${count}; count++; console.time(label); try { return fn.apply(this, arguments); } finally { console.timeEnd(label); } } } }}5.mixin用于将对象的方法混入 Class 中:const SingerMixin = { sing(sound) { alert(sound); }};const FlyMixin = { // All types of property descriptors are supported get speed() {}, fly() {}, land() {}};@mixin(SingerMixin, FlyMixin)class Bird { singMatingCall() { this.sing(’tweet tweet’); }}var bird = new Bird();bird.singMatingCall();// alerts “tweet tweet"mixin 的一个简单实现如下:function mixin(…mixins) { return target => { if (!mixins.length) { throw new SyntaxError(@mixin() class ${target.name} requires at least one mixin as an argument); } for (let i = 0, l = mixins.length; i < l; i++) { const descs = Object.getOwnPropertyDescriptors(mixins[i]); const keys = Object.getOwnPropertyNames(descs); for (let j = 0, k = keys.length; j < k; j++) { const key = keys[j]; if (!target.prototype.hasOwnProperty(key)) { Object.defineProperty(target.prototype, key, descs[key]); } } } };}6.redux实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);有了装饰器,就可以改写上面的代码。@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {};相对来说,后一种写法看上去更容易理解。7.注意以上我们都是用于修饰类方法,我们获取值的方式为:const method = descriptor.value;但是如果我们修饰的是类的实例属性,因为 Babel 的缘故,通过 value 属性并不能获取值,我们可以写成:const value = descriptor.initializer && descriptor.initializer();参考ECMAScript 6 入门core-decoratorsES7 Decorator 装饰者模式JS 装饰器(Decorator)场景实战ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。本文作者:冴羽阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 21, 2018 · 6 min · jiezi