关于函数:体验有奖函数计算-x-通义千问预体验一键部署AI应用赢Airpods

函数计算团队全新上线“函数计算 FC 一键部署 通义千问 预体验、文生图、图生图、图生文、文生文5大经典 AI 场景,让您取得通义千问 30次对话预体验机会,同时简略、高效实现一键部署图像生成、文字生成服务,速成 AIGC 创作家。 体验流动链接:https://developer.aliyun.com/topic/aigc_fc 双重奖品设置,实现体验场景可得社区400积分兑换奖品,还可加入 AI生成图像较量赢取Airpods、阿里云定制蓝牙音箱及阿里云定制清雅杯。 残缺内容请点击下方链接查看: https://developer.aliyun.com/article/1231408?utm_content=g_10... 版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

June 14, 2023 · 1 min · jiezi

关于函数:详解合约函数调用中的上下文变化

详解合约函数调用中的上下文变动合约函数调用中的上下文变动是智能合约中一个十分重要且常见的概念。当一个函数被调用时,其所处的上下文环境可能会发生变化,这会对函数的执行产生影响。在本文中,咱们将具体探讨合约函数调用中的上下文变动,并为读者提供一些示例。 什么是合约函数调用中的上下文变动?在智能合约中,每个函数都有本人的变量和状态。而当一个函数被调用时,它的上下文环境就会发生变化。具体来说,上下文环境包含了以下几个方面: 合约地址:即调用该函数的合约地址。调用者地址:即调用该函数的用户地址。gas 限度:即该函数可应用的 gas 限度。msg.value:即该函数所附带的以太币数量。block.number:即以后区块的编号。now:即以后工夫戳。这些变动将间接影响函数的执行后果,并且须要开发人员在编写合约代码时充分考虑。 示例接下来,咱们将通过两个示例来演示合约函数调用中的上下文变动。 示例 1假如咱们有一个简略的合约,其中蕴含了一个 add 函数,用于将两个数字相加并返回后果。该合约的代码如下所示: pragma solidity ^0.8.0;contract SimpleContract { uint256 public result; function add(uint256 a, uint256 b) public { result = a + b; }}当初,咱们想要调用 add 函数,并将数字 10 和 20 相加。假如咱们是来自另一个合约的调用者。那么,咱们须要通过以下代码来实现: pragma solidity ^0.8.0;import "./SimpleContract.sol";contract CallerContract { SimpleContract private simpleContract; constructor(address _simpleContractAddress) { simpleContract = SimpleContract(_simpleContractAddress); } function callAddFunction() public { simpleContract.add(10, 20); }}在下面的代码中,咱们首先导入了 SimpleContract,而后创立了一个名为 CallerContract 的新合约。咱们还定义了一个名为 simpleContract 的公有变量,并在构造函数中初始化。 接下来,咱们编写了一个名为 callAddFunction 的函数,它将调用 simpleContract 中的 add 函数,并传递数字 10 和 20 作为参数。 ...

May 24, 2023 · 2 min · jiezi

关于函数:消息服务-Serverless-函数计算如何助力企业降本提效

背景介绍音讯队列服务(下文均以 Message Service 命名)作为云计算 PaaS 畛域的基础设施之一,其高并发、削峰填谷的个性愈发受到开发者关注。Message Service 对上承接音讯生产者服务的申请,对下连贯消费者服务。提到生产:那就不得不引入两个问题? 如何以低成本、高吞吐、低延时的形式将音讯数据从 Message Service 输送给上游生产服务?如何疾速构建免运维、按需弹性伸缩算力的音讯生产服务?明天就来聊聊如何在阿里云上基于 Serverless 计算服务 + Message Service 构建这样一套零碎。 残缺内容请点击下方链接查看: https://developer.aliyun.com/article/1124852?utm_content=g_10... 版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

May 15, 2023 · 1 min · jiezi

关于函数:源码解读PolarDBX中的窗口函数

为什么须要窗口函数?Window是一个罕用且重要的性能,PolarDB-X作为一款分布式数据库,天然也反对了窗口函数。对于业务开发来讲,其能够大大简化业务SQL的设计,比方分组排序功能,如果反对窗口函数,则只需应用排序函数即可,例子如下。 例:我当初有一张表,蕴含学生姓名,学生班级,学生问题,当初请你帮我写一条SQL,实现对每个班级内的同学进行排名的需要? 有窗口函数时: SELECT student_name, class_name, score, DENSE_RANK() OVER (PARTITION BY class_name ORDER BY score DESC) AS rankFROM student_scoresORDER BY class_name, rank ASC;残缺内容请点击下方链接查看: https://developer.aliyun.com/article/1181870?utm_content=g_10... 版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

May 12, 2023 · 1 min · jiezi

关于函数:源码解读PolarDBX中的窗口函数

为什么须要窗口函数?Window是一个罕用且重要的性能,PolarDB-X作为一款分布式数据库,天然也反对了窗口函数。对于业务开发来讲,其能够大大简化业务SQL的设计,比方分组排序功能,如果反对窗口函数,则只需应用排序函数即可,例子如下。 例:我当初有一张表,蕴含学生姓名,学生班级,学生问题,当初请你帮我写一条SQL,实现对每个班级内的同学进行排名的需要? 有窗口函数时: SELECT student_name, class_name, score, DENSE_RANK() OVER (PARTITION BY class_name ORDER BY score DESC) AS rankFROM student_scoresORDER BY class_name, rank ASC;残缺内容请点击下方链接查看: https://developer.aliyun.com/article/1181870 版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

April 25, 2023 · 1 min · jiezi

关于函数:事件总线-函数计算构建云上最佳事件驱动架构应用

间隔阿里云事件总线(EventBridge)和 Serverless 函数计算(Function Compute,FC)发表全面深度集成曾经过来一年。站在零碎元数据互通,产品深度集成的肩膀上,这一年咱们又走过了哪些历程? 从事件总线到事件流,从基于 CloudEvents 的事件总线触发到更具个性化的事件流触发,函数计算已成为事件总线生态不可或缺的重要组成部分,承载了 EventBridge 零碎架构中越来越多的角色,事件流基础架构的函数 Transform,基于函数计算的多种上游 Sink Connector 投递指标反对,函数作为 EventBridge 端点 API Destination;基于事件总线对立,规范的事件通道能力,和基于函数计算麻利、轻量、弹性的计算能力,咱们将又一次起航摸索云上事件驱动架构的最佳实际。 本文主题围绕事件总线+函数计算,构建云上最佳事件驱动架构利用。心愿通过上面的分享,帮忙大家深刻了解 Serverless 函数计算、EventBridge 事件总线对于构建云上事件驱动架构利用的价值和背地的逻辑、 为什么函数计算是云上事件驱动服务最佳实际?为什么咱们如此须要事件总线服务?随同着这些谜题的解开,最初,让咱们一起理解利用于理论生产的一些 Serverless 事件驱动客户案例。 事件驱动架构的实质Back to the Nature of Event-Driven大家可能会纳闷,事件驱动妇孺皆知,为什么咱们又要从新探讨事件驱动呢?我想这也正是咱们须要探讨它的起因,回归实质,从新起航。 事件驱动可能是一个比拟宽泛的概念,而本文聚焦事件驱动架构的探讨,事件驱动架构作为一种软件设计模式,确实不是一个新的概念,随同着计算机软件架构的演进,它曾经存在了一段很久的工夫,大家对它的探讨也从未进行过,当咱们须要从新探讨一个曾经存在的概念的时候,我想咱们有必要从新回到它最开始的定义,一起摸索那些实质的货色,重新认识它。 下面的这些内容是我从相干的一些材料上摘录的对于事件驱动架构的一些形容,“abstract”,“simple”,“asynchronous”,“message-driven” 这些具备代表性的词汇很好的给予事件驱动架构一个宏观的形容;从事件驱动架构的抽象概念,到它简洁的架构,以及事件驱动架构要达成的目标,和它在理论的零碎架构中所展示的状态。 事件驱动架构基本概念及状态在理解了对于事件驱动架构的一些根本形容之后,咱们须要进一步明确事件驱动架构所波及的一些基本概念和架构状态。依据维基百科形容,事件驱动架构波及的外围概念如下所示: 事件驱动架构根本状态 围绕事件的流转,依据事件驱动架构的概念和根本状态,次要波及以下四个外围局部: Event Producer:负责产生事件,并将产生的事件投递到事件通道;Event Channel:负责接管事件,并将接管的事件长久化存储,投递给订阅该事件的后端解决引擎;Event Processing Engine:负责对于订阅的事件做出响应和解决,依据事件更新零碎状态;Downstream event-driven activity:事件处理实现之后,对于事件处理响应的一种展现。事件驱动架构要达成的目标理解了事件驱动架构的根本状态,架构中事件通道的引入,解耦了事件生产和事件处理这两个最根本的零碎角色,那么这样的架构模型所要达成的最终目标到底是什么? 零碎架构松耦合 事件生产者与事件订阅者在逻辑上是离开的。事件的生成与应用的拆散意味着服务具备互操作性,但能够独立扩缩、更新和部署。 只面向事件的涣散耦合能够缩小零碎依赖项,并容许您以不同的语言和框架实现服务。您无需更改任何一个服务的逻辑,即可增加或移除事件生成方和接管方。您无需编写自定义代码来轮询、过滤和路由事件。 零碎的可伸缩性 基于事件驱动架构的松耦合个性,意味着能够独立对事件生产者,事件通道服务,以及事件处理引擎进行独立的扩缩容;尤其对于后端事件处理引擎,能够依据音讯解决响应 SLA 和后端资源供应进行弹性扩缩容;同时能够基于事件粒度构建不同规格的后端解决服务,实现更细粒度的零碎弹性伸缩。 零碎的可扩展性 零碎的可扩展性,次要体现在当零碎须要减少新的性能,如何疾速的基于现有零碎架构疾速构建反对新的业务逻辑,在事件驱动架构利用中,围绕事件粒度的解决模式,可能人造疾速反对减少新的基于事件的数据流形象。 当零碎中减少新的事件类型的时候,无需调整变更公布整个零碎,只须要关注须要订阅的事件进行事件处理逻辑的开发和部署即可,也能够基于原来的零碎做很少的代码变更即可实现,也能够在业务初期通过独立的服务订阅指定的事件实现特定的业务逻辑反对。 为什么函数计算是云上事件驱动服务最佳实际?在探讨完事件驱动架构根本模型之后,我想对于事件驱动的概念,状态咱们有了对立的意识和了解,接下来咱们进入议题的第二个局部,为什么函数计算是云上事件驱动服务最佳实际? 函数计算简介函数计算 FC 是一款基于事件驱动的全托管计算服务,相干的产品细节能够见官网介绍。作为一款通用的事件驱动型计算服务,接下来我会从三个方面进行具体的介绍。 编程范式 应用函数计算,用户无需洽购与治理服务器等基础设施,只需编写并上传代码。函数计算为你筹备好计算资源,弹性地、牢靠地运行工作,并提供日志查问、性能监控和报警等开箱即用性能,编程范式带来开发的“简略,快捷”。 依照函数粒度进行独立的性能单元开发,疾速调试,疾速的部署上线,省去了大量资源购买,环境搭建的运维工作;同时函数计算是一个事件驱动的模型,事件驱动,意味着用户不须要关注服务产品数据传递的问题,省去了在编写代码中波及的大量服务拜访连贯的逻辑;“事件驱动” + “函数粒度开发” + “免服务器运维”等几个维度特色帮忙函数计算撑持“聚焦业务逻辑麻利开发”的底层逻辑。 ...

January 13, 2023 · 1 min · jiezi

关于函数:鱼传科技函数计算只要用上就会觉得香

深圳鱼传科技有限公司是专一以精准营销和互联网生态产品经营为外围的综合互联网营销推广服务商。通过整合全网优质媒体资源,并联合智能数据模型和 AI 标签算法,向企业提供包含流量矩阵搭建经营、媒介流量采买、投放模型设计、产品营销策划、数据监控剖析、成果经营等多层次服务。作为函数计算的资深用户,鱼传科技的 CTO 和技术负责人跟咱们聊了鱼传科技的 Serverless 旅程。 目前鱼传科技的业务次要基于支付宝小程序进行承载,小程序具备轻量、关上不便、内容能够疾速更新等特点,为了适应市场的疾速变动和多变的客户诉求,对麻利开发提出了更高的要求。鱼传科技的业务特点,还具备访问量稳定大、流量突发预测难等特点,尤其是流动期间拜访突增对小程序后端服务的稳固和弹性也是一个很大的考验。而阿里云函数计算是典型的 Serverless 计算平台,具备极强的弹性能力,能够做到百毫秒弹性扩缩,能够很好的撑持业务弹性扩大。同时对函数计算,用户上传代码即可运行,无需关注和保护服务器,也极大地提高了后端开发效率。 函数计算 FC :https://www.aliyun.com/product/fc? 这些特点使得函数计算成为很多企业撑持小程序/挪动 APP 的优先选择,尤其是有突发流量或者流量稳定较大的业务场景。如下是基于鱼传科技的第一视角出现的 Serverless 落地实际。 简单交互小程序如何应答访问量激增?2018 年底,咱们开始尝试应用函数计算。过后,公司的外围业务是在支付宝上制作一些小程序。“多多有礼”小程序就是在那个时候上线的,“多多有礼”是一款主打互动领奖的小程序,以后曾经积攒了百万日活的规模,是一款十分受用户欢送的产品。然而在 2018 年,“多多有礼”最后上线时,咱们遇到了已有业务零碎难以承载突增流量的难题。那时咱们的业务都跑在服务器下面,为了能抗住高并发流量,咱们筹备了大略三、四台高配服务器做负载平衡,然而在业务并发高峰期,服务崩掉的状况还是常常产生。因为这个小程序波及到的业务逻辑,和利用后端交互比拟多,有很多简单流程,比方打卡、签到、庄园经营等,所以遇到突增流量,单纯减少服务器数量很难扛住。 另外咱们还遇到了资源利用率低的问题。“多多有礼”在初期上线的时候,业务高峰期并发大略在 1000-2000,但业务低峰期可能也就几十,这是因为小程序设计的用户打卡、签到等动作,使得用户量非常容易在早上、早晨,或者某一个特定工夫暴增。在这种状况下咱们再用 ECS 的话,不仅须要依照峰值流量预留足够的 ECS 资源,保护起来也会变的非常复杂,资源利用率很难做下来,费用也会成倍的减少。所以咱们过后十分迫切地想把这个事件从咱们零碎里解耦,如果能简化咱们的运维复杂度,还能引入弹性能力就好了。 通过调研咱们发现过后阿里云,只有函数计算 FC 这款产品具备相应的特点,所以咱们就开始尝试把整个业务都迁到阿里云函数计算上来。通过这 3 年多的应用,咱们把新的利用、能够迁徙的旧利用、外部利用/内部利用等都陆续迁徙上函数计算了。能够这么说,如果函数计算崩了,咱们公司的业务根本也就瘫了。然而通过这 3 年来的应用,发现函数计算的稳定性还是超预期的,比咱们保护应用服务器的时候,业务稳定性和性能都有大幅晋升,当初峰值能够达到数万 QPS、数千函数并发同时稳固运行。而且咱们和函数计算也建设了专门的技术支持群,有任何技术问题,都能很快失去响应,这也是为啥咱们敢把公司所有的业务都基于函数计算来部署的起因。应用函数计算,真正帮忙咱们解决了很多稳定性和性能问题。 “多多有礼”小程序页面 最佳实际再来分享下咱们应用函数计算的一些最佳实际,心愿也能帮忙到其余用户应用函数计算。 1.开发流程咱们公司的次要技术栈是基于 PHP 语言,也会应用一些 Web 框架,像 Lavaral,针对 Web 框架,为了能在函数计算上运行起来,咱们也对框架做了些适配,一个我的项目拆成一个或多个文件,对应多个函数,单个文件有的1万行代码,根底文件一百行左右。然而当初函数计算配合 Serverless Devs 工具反对了多语言 Web 框架的“0”革新迁徙,咱们也在尝试应用。 目前咱们每个开发会独立负责一个函数服务,服务上面每个函数会作为一个小的利用,局部我的项目会跨服务依赖一些性能函数,然而咱们都会尽可能都独立开。函数计算也反对了层性能,前面会用层来部署公共函数、依赖,比方给用户发红包,代码只用写一份。另外对新招进来的开发来讲,函数计算上手门槛还是很低的,不必治理服务器搭环境,能够间接在线编辑代码、部署、测试。 2.流水线和灰度公布咱们本地始终采纳的 SVN 存储代码,SVN 提交代码反对触发 Action,咱们封装了函数计算的 API 接口,能够通过关键字触发函数和服务的公布。为了防止公布影响线上服务,咱们还应用了函数计算的版本和别名的性能。失常线上业务会公布成新的版本,同时把 HTTP 流量入口绑定的 release 别名指向新的版本,这样就实现了公布过程,如果最新的代码呈现问题,能够更改别名的指向,就能达到一键回滚到上个版本。同时咱们也会创立一个测试别名,会先实现版本的测试后,才会把承载现网流量的 release 别名指向到新版本。这样通过别名的能力就辨别出了线上环境和测试环境,十分不便。 3.运维治理对函数计算来讲,根本是不须要关怀资源保护的,像咱们最依赖的弹性能力。然而对于业务运维来讲,监控日志就成了十分要害的伎俩。函数计算集成了 SLS,每次申请都会生成一条日志,能够比拟不便的过滤出谬误日志,对线上问题排查还是比拟不便的。另外函数计算也提供了比拟全的监控视图,咱们最罕用的就是申请量、谬误次数、并发、执行耗时等指标,针对谬误次数也加了告警,这样开发就能够间接兼业务运维,效率成倍增加。 成果比照: 比照之前应用服务器,函数计算的确给咱们带来了很大的便利性,咱们也是最早吃螃蟹的人,根本随同着函数计算一路成长,咱们也非常高兴的看到,函数计算的性能越来越丰盛,体验也越来越好。总结下来: ...

December 14, 2022 · 1 min · jiezi

关于函数:graphicsh函数应该在哪里调用

graphics.h函数介绍graphics.h是TC外面的图形库,如果要用的话应该用TC来编译。具体分为:像素函数、直线和线型函数、多边形函数、填充函数等。然而在咱们应用的编译器vc6.0和visual studio 2022编译器中是不反对graphics.h图形库的,当然vc6.0和visual studio 2022编译器也自带了微软的图形库API(也叫Windows API,它的函数基本上蕴含在了windows.h的头文件中),那么如果咱们要在vc6.0和visual studio 2022中应用graphics图形库怎么办? 解决办法保留好正在做的工作后,临时敞开vs或者vc,下载easyx,运行后重启vs或者vc,即可看到函数失常下载地址https://easyx.cn/ EasyX Graphics Library 是针对 Visual C++ 的收费绘图库,反对 VC6.0 ~ VC2022,简略易用,学习老本极低,应用领域宽泛。目前已有许多大学将 EasyX 利用在教学当中。

May 26, 2022 · 1 min · jiezi

关于函数:gets函数方便使它欢迎安全性使它终被遗弃

前言:鱼与熊掌不可兼得,让gets函数被c11规范委员会究竟狠下心抉择抛弃的,我认为是它函数本身的平安问题,正如马斯洛需要档次实践提到的那样,平安是人的最底层需要档次,比起函数调用的不便,平安更让人感觉重要。本篇博客的结尾,以我写的“单词读取计算”程序一小部分为例,在键盘字符输出中,咱们通常想到的是scanf和gets,gets函数相比于scanf,很好的解决了字符串读取空格即进行的特点,因而在某些场景下显得更为简略易用。 puts("hey ,please inter your word here"); gets_s(txt); ....... ...... puts(wors); puts("Done"); //这里不能用scanf然而,这个长处,也成为了它的毛病,咱们大胆的假如下,如果有一位调皮捣蛋的用户不厌其烦的筹备在咱们的程序输出一本柯林斯那么多的单词 那么状况会怎么,可能的答案是缓冲区溢出(当然如果多余字符只是占用尚未应用的内存,就不会立刻呈现这个问题),如果放在网络安全术语里,那就是缓冲区溢出攻打。 gets函数还有一个典型的毛病,那就是攻击者能够很好的插入威逼代码,从而实现攻打目标。 这也因而c99规范委员会开始倡议不再应用,随后,并在c11规范委员会以更强硬的态度抉择了抛弃。 gets的抛弃天然让咱们一些输出解决变得不不便,因而,咱们抉择了其余相似函数进行取代。 fgets(): 会指定检测大小,但会将超出字符串的局部放入缓冲区,下一次调用时又会呈现; 不会将换行符转为空字符,而是保留换行符并增加空字符’\0’,这会意味着字符串比原应有大1,或者读到遇到的第一个换行符为止。 gets_s()和fgets()的区别: (1)gets_s()只从规范输出中读取数据,所以不须要第3个参数; (2)如果gets_s()读到换行符,会抛弃它而不是存储它;这里和gets()函数简直一样,齐全能够用gets_s()替换gets()。 (3)如果gets_s()读到最大字符数都没有读到换行符或文件结尾,会执行以下几步。首先把指标函数数组中的首字符设置为空字符,读取并抛弃随后的输出直至读到换行符或文件结尾,而后返回空指针。接着,调用依赖实现的“处理函数”(或你抉择的其余函数),可能会停止或退出程序。 应用gets()函数不平安,它会擦除现有数据,存在安全隐患。gets_s()函数很平安,然而,如果并不心愿程序终止或退出,就要晓得如何编写非凡的“处理函数”。 所以,当输出与预期不符时,gets_s()齐全没有fgets()函数不便、灵便。兴许这也是gets_s()只作为C库的可选扩大的起因之一。 s_gets() 自定义函数,要求是:读取整行输出并用空字符代替换行符,或者读取一部分输出,并抛弃其余部分

May 8, 2022 · 1 min · jiezi

关于函数:异步请求积压可视化|如何-1-分钟内快速定位函数计算积压问题

简介:本文分为三个局部:概述中引入了积压问题,并介绍了函数计算异步调用根本链路;并在指标介绍局部具体介绍了指标查看形式,分类解读了不同的指标含意;最初以一个常见的异步申请积压场景为例,介绍如何在 1 分钟内疾速定位积压问题。 为异步调用保驾护航应用函数计算异步调用的开发者最关怀的问题是:调用申请是否在预期的工夫内被解决实现。若没能解决实现,那么在客户眼中就是异步调用申请积压了,然而基于之前函数计算异步调用指标体系,无论是定位积压,还是查看积压,过程都是非常繁琐的。 针对以上问题,函数计算推出了一系列异步调用申请积压相干的指标,可能帮忙用户疾速定位申请积压,向用户展现积压量化值。本文将具体介绍如何通过这些监控指标疾速定位到函数异步调用呈现的积压问题,为各位开发者解说降级后的异步调用指标体系。 在开始之前,先简略介绍下函数计算异步调用。 异步调用是函数计算调用函数的一种形式,通过异步调用你不仅能够确保函数会至多执行一次,还能够保留调用执行过程中的状态转换信息和执行后果,其调用链路如下所示: 用户/事件源发动异步调用申请后会立即返回本次申请 ID,随后函数计算零碎将本次调用的相干信息转换为音讯的格局,放入 MNS 音讯队队列中供零碎内上游模块生产,上游模块会基于解析进去的调用音讯进行函数调用。 调用实现后,如果函数配置了 Destination,则零碎会基于调用后果以及 Destination 内容进行进一步解决,Destination 相干内容介绍请参考异步调用文档: https://help.aliyun.com/docum... 指标降级降级后的函数计算异步调用链路监控指标次要新增了如下几类: 上面咱们将对上述指标进行具体解读。 指标查看目前能够通过函数计算控制台或者 Serverless Devs 工具这两种形式查看函数的监控指标大盘,上面咱们将以控制台为例,领导大家如何查看异步调用链路相干的监控指标,基于 Serverless Devs 的查看形式能够参考: https://github.com/devsapp/fc... 上面介绍的步骤前提是已开明了函数计算服务;且胜利创立了服务以及函数,如果还未进行这些操作,请参考应用控制台创立函数: https://help.aliyun.com/docum... 首先关上函数计算控制台,点击左侧 监控大盘 标签,滑倒底部,能够查看到该地区所有服务的异步调用解决状况以及异步音讯解决均匀延时概览表格: 此时咱们点击任意一个服务名称,进入后,能够看到该服务下所有函数的异步调用解决状况;以及异步音讯解决均匀延时概览表格: 接下来咱们点击任意一个函数名称,进入后能够看到所有函数纬度的监控指标,并以图的模式展现: 至此,咱们曾经学会了这些指标的查看路径。上面持续为各位开发者介绍解读上述异步链路相干指标。 指标解读咱们将依据不同的指标类型对监控指标进行分类解读。 异步调用解决状况异步申请入队 异步调用中,达到函数计算的申请数,当入队申请数大于申请解决实现数时,示意有申请积压,函数解决异步申请的速度小于异步申请发动的速度。请调整函数弹性伸缩(含预留资源)下限,参考: https://help.aliyun.com/docum... 或可钉钉搜寻退出阿里函数计算官网客户群(11721331)分割咱们进行解决。 异步申请解决实现 异步调用中,函数计算解决实现的申请数,异步申请解决实现数量,应始终不大于异步申请入队的数量。 异步申请积压数 曾经达到函数计算的异步申请中,期待解决以及正在解决中的申请对立视为积压申请, 这些申请的数量为异步音讯积压数,当这个值不为 0 时,示意异步调用申请是有积压的。 该指标将异步调用申请积压量化,解决积压数不可见问题,极大进步了异步调用的可观测性,也是本次降级的重要内容之一。 异步申请解决延时均匀解决时延 函数异步调用申请从进入解决队列到开始解决的时延,按指定工夫粒度统计求平均值。当该值高于预期时,表明函数异步调用申请可能存在积压。 “异步申请入队”、“异步申请解决实现” 以及 “均匀解决延时” 这三个指标被搁置在监控大盘的概览图表中,旨在帮忙用户疾速定位到呈现积压的函数,解决积压定位难的问题。 1 分钟定位积压问题在之前的异步调用指标体系下,如果想要定位积压问题,首先须要找到积压函数,此时须要一一函数查看其函数监控指标详情,定位胜利后,也无奈直观看到具体的积压量化值。 降级后的异步调用指标体系可能很好地解决积压问题定位难以及积压量化的问题。上面将围绕积压问题的场景,形容如何应用上述指标疾速定位积压问题。 业务场景问题形容: 小张的业务波及到三个函数,且都是异步调用,某天用户的业务出了问题,每个环节的异步解决时延都增大了。为了疾速定位问题,用户想到了异步链路监控指标,进行了如下定位动作。 定位过程: 首先关上地区级别的监控大盘,抉择指标时间段,查看该地区下各个服务的监控指标; 发现多个服务的异步调用均匀解决延时高于预期,同时其异步申请入队数均大于申请解决实现数,示意这些服务都有肯定水平异步调用音讯积压,且 A-Service 的异步申请入队数量和异步调用申请实现数差异最大,积压最重大,点击 A-Service 查看监控指标: ...

February 18, 2022 · 1 min · jiezi

关于函数:花小钱办大事云函数云开发撬动央视晚会的电商大促弹性架构实践

在 2021 腾讯数字生态大会上,一个「花小钱办小事」的客户胜利案例让客户和开发者们印象粗浅的。这正是基于云函数和云开发的小程序利用实际。 往年,某快消品领导品牌冠名了央视大型节日晚会,在晚会当晚,该客户播种的小程序新增注册用户数达到了预期业务指标,当天的拜访次数达到千万级 ,霎时并发最高每分钟数百万申请,而整个云函数计算资源的应用费用仅几万元。整体耗时安稳,均匀 API 耗时在 20 毫秒以下,保障了此次流动的顺利进行。 在评估了不同的小程序技术选型之后,客户从开发效率、弹性反对、平安等角度抉择了云开发和云函数构建该客户的小程序,其外围业务逻辑如登陆认证、抽奖、问答等。利用云开发提供的公有链路、全链路性能优化、根底平安爱护等劣势,联合云函数弹性伸缩、低运维老本、高性价比的劣势,为本次流动提供了稳固等基础设施反对和先进高效的开发工具,保障流动圆满成功。 01.央视晚会-电商节日大促场景需要剖析该小程序承载着此次流动的用户拉新和转化指标,当主持人口播流动开始,电视屏幕上显示小程序二维码,观众扫码进入流动落地页: 1. 需要特点和痛点 利用生命周期短利用生命周期基于商业模式——节日大促、电商秒杀的利用指标在于短时间内疾速暴发抢占市场,生命周期通常在1周至1个月。 定制化需要高为了与流动指标和产品品牌深度联合,流动页面从设计到交互环节设置十分个性化,须要进行定制化开发。 典型的高并发场景霎时访问量通常是平时的几十倍,须要思考计算、存储、网络和服务依赖等方方面面,如果没有辨认出要害的束缚和危险,甚至会有宕机危险。 2. 计划劣势和价值 全链路性能优化从小程序前端接入层到后端数据库,从内部链路到 VPC 网络,针对客户预估的 QPS 做全链路性能剖析、监控及调优,升高响应工夫、进步零碎吞吐量和整体服务的可用性。 高效的低代码开发工具云开发+微搭低代码平台,反对自定义的组件,赋能小程序服务商晋升小程序构建效率,通过页面利落拽的模式即可实现疾速实现页面构建。 弹性伸缩的云上资源面对霎时高并发资源预留痛点,云函数极致的弹性扩容能力实用于高并发场景,无需预估流量大小,依据流量状况主动进行扩缩容,整个过程无需人工干预。 作为本次央视晚会电商大促的计划架构师,腾讯云 Serverless 专家架构师杨政权示意: “ 弹性伸缩、按量计费的云函数冲破传统 PaaS 产品的瓶颈,通过疾速扩缩容轻松应答霎时高并发的秒杀、大促场景,以稳固、牢靠和高性价比的计算服务撑持业务增长。” 02.云函数 + 云开发 ,应答电商行业大促场景央视晚会的小程序场景,是电商行业的用户营销和节日大促的常见需要。每年大促期间,批发行业线上渠道都面临历史级别的流量挑战,中大型电商平台的峰值调用量可达上千万/分钟,面临高于日常 10-20 倍的流量压力。而日常经营流动中,例如精品秒杀、限时抢购等,电商平台也同样面临大流量高并发、波峰波谷用户流量显著分化的典型场景。 作为底层算力,云函数助力云开发为开发者提供高可用、主动弹性扩缩的后端云服务,蕴含计算、存储、托管等 Serverless 化能力。在腾讯云产品的根底上,依据业务场景须要进行性能的整合与定制,更加贴近小程序 / Web 利用开发需要,帮忙开发者对立构建和治理后端服务和云端资源,防止利用开发过程中繁琐的服务器搭建及运维,让开发者能够专一于业务逻辑的实现,升高开发门槛,进步开发效率。 计划劣势 疾速构建通过微搭低代码 WeDa 简略的「利落拽」疾速实现流动页面搭建,创意想法即刻实现,开发效率进步 3-5 倍以上。 超高并发可疾速拉起 10w 并发实例,稳固反对刹时超高流量涌入。 平安稳固提供微信平安网关、风控、公有链路、鉴权等能力守护平安,杜绝羊毛党,防备歹意攻打。 更低成本流动大促专属资源包服务配置,用完即停,升高外围服务资源投入。 《2021 腾讯数字生态大会 - 花小钱办小事客户胜利案例演讲原文》“云原生 2.0 的技术,也就是 Serverless 的技术,首先咱们看一看行业现状,传统的研发模式,有两个比拟显著的问题: 基本上 40% 左右的用户,他们的 CPU 利用率小于10%,这个数据代表什么含意呢?就是这么多的用户,有90%的CPU是闲置的,这是一个极大的节约。企业研发人员和运维人员的配比大略是 3:1,随着企业规模的一直加大,运维人员的需要越来越多,有很大的人力放在在基础设施的保护下面,这是咱们看到的问题。Serverless 能够很好地解决这两个问题,首先,它是依照用户理论的申请量、理论的使用量进行计费的。当空转的时候,齐全不计费;齐全不须要为空转买单,只须要为本人理论的应用买单。特地是对于 CPU 利用率不高的企业,通过采纳 Serverless 技术,能够极大地升高本人的老本。其次是免运维:所谓的 Serverless 是对用户来说,不须要运维,那么运维工作交给谁呢?运维工作都交给私有云的服务商,由咱们承当运维工作。整个都是自动化的过程,对于用户来讲是免运维,所以客户的运维老本失去了大幅度晋升。 ...

December 28, 2021 · 1 min · jiezi

关于函数:跨越行业绊脚石阿里云函数计算发布-7-大技术突破

作者|望宸 等 Serverless 的实质是通过屏蔽底层的计算资源,来实现业务层开发的专一度和自由度。但越是往上形象,云厂商在底层的实现就越是简单。函数计算将服务进一步拆分到函数的颗粒度,这势必会给开发、运维、交付等带来新的挑战,例如如何对函数进行端云联调、如何对函数进行可观测和调试、如何优化 GB 级别的镜像冷启动?这些以往在服务的颗粒度时,都不是问题的事件,成了 Serverless 大规模落地企业外围生产业务的绊脚石。 2021 云栖大会现场,阿里巴巴研究员、阿里云智能云原生利用平台总经理 丁宇(叔同)重磅公布了函数计算的 7 大技术创新和冲破,减速古代利用架构的变革。 Serverless Devs 2.0:业内首发 Desktop,反对端云联调、多环境部署开源近一年, Serverless 开发者平台 Serverless Devs 2.0 版本正式公布。相比 1.0 ,2.0 在性能、应用体验实现全方位晋升,业内首发桌面客户端 Serverless Desktop,对桌面客户端进行了精密设计兼具美感和实用主义,具备更强的企业级服务能力。 作为业内首个反对支流 Serverless 服务/框架的云原生全生命周期治理的平台,Serverless Devs 致力于为开发者打造 Serverless 利用开发一站式服务,Serverless Devs 2.0 提出多模式调试计划,包含买通线上线下环境;本地对接线上环境并进行调试的端云联调计划、本地间接进行开发态调试的本地调试计划、以及云端运维态调试的在线调试/近程调试计划等。新版本减少多环境部署部署能力,Serverless Devs 2.0 已反对一键部署框架 30 余种,包含 Django,Express,Koa,Egg,Flask,Zblog,Wordpress 等。 业内首发实例级别可观测和调试实例是函数资源最小的可被调度的原子单位,类比容器的 Pod。Serverless 将异构根底资源高度形象,因而“黑盒问题”是 Serverless 大规模遍及的外围落地之痛。业内同类产品均没有透出“实例”概念,也从未在可观测性能中将 CPU、内存等指标透出,但可观测就是开发者的眼睛,没有可观测,何谈高可用呢? 函数计算重磅公布实例级别可观测能力,对函数实例进行实时监控和性能数据采集,并进行可视化展现,为开发者提供函数实例端到端的监控排查门路。通过实例级别指标,您能够查看 CPU 和内存应用状况、实例网络状况和实例内申请数等外围指标信息,让“黑盒”不黑。同时,函数计算将通过凋谢局部实例登录权限,做到既能观测,还能调试。 业内首发固定数量、定时、水位主动伸缩的实例预留策略函数计算冷启动受到多个因素影响:代码和镜像大小、启动容器、语言运行时初始化、过程初始化、执行逻辑等,这依赖用户和云厂商的双向优化。云厂商会主动为每个函数调配最合适的实例数量,并进行平台侧的冷启动优化。但对于某些在线业务时延十分敏感,云厂商无奈代替用户进行更深层的业务优化,如对代码或依赖进行精简、编程语言的抉择、过程的初始化、算法优化等。 业内同类产品广泛是采纳预留固定实例数量的策略,即让用户配置 N 个并发值,除非手动调整,否则在调配了 N 个实例后不会再伸或者缩。这种计划只解决了局部业务高峰期的冷启动延时,但大大增加了运维老本和资源老本,对红包大促等带有不定期峰谷的业务,其实并不敌对。 因而,函数计算率先将局部实例资源的调度权限授予用户,容许用户通过固定数量、定时伸缩、按水位伸缩、混合伸缩等多维度的实例预留策略,来预留适量函数实例,别离满足业务曲线绝对安稳(如 AI/ML 场景)、峰谷时间段明确(如游戏互娱、在线教育、新批发等场景)、突发流量无奈预估(如电商大促、广告等场景)、业务混淆(如 Web 后盾、数据处理等场景)等不同场景的诉求,从而升高冷启动对时延敏感型业务的影响,真正实现弹性和性能兼顾的终极目标。 业内率先推出 GPU 实例函数计算提供弹性实例和性能实例两种实例类型,弹性实例规格从 128 MB 到 3 GB,隔离粒度做到了整个云生态最细,能真正实现普适场景下资源利用率 100%;性能实例规格区间范畴蕴含 4 GB、8 GB、16 GB 和 32 GB。资源下限更高,次要实用于计算密集型场景,如音视频解决、AI 建模和企业级 Java 利用等场景。 ...

October 27, 2021 · 1 min · jiezi

关于函数:直播预约云托管-or-云函数业务如何做好技术选型

云开发者较为相熟的CloudBase云函数和基于容器技术的微信云托管: 别离有哪些劣势? 利用场景有何异同? 如何依据本身业务需要抉择最合适的技术? 本期微信云托管「从入门到精通」系列直播将具体比照和剖析云托管和云函数技术,并解答开发者关怀的常见问题。 直播工夫: 10月13日(周三) 19:00 分享嘉宾: 李冠宇,微信云托管、云开发产品经理、架构师 直播地址: 视频号(横屏观看更清晰)Bilibili https://live.bilibili.com/21571381

October 13, 2021 · 1 min · jiezi

关于函数:MySQL函数

MySQL函数MySQL函数介绍 数学函数字符串函数工夫函数加密函数数学函数留神: 每个函数后面都须要加 : SELECT 。 数学函数 ABS() 返回绝对值 如: (-100) 值 : 100PI() 返回的圆规率 如 (不必写) 值 : 3.1415926CEIL() 向上取整数 如:(3.14) 值 :4 ( 留神第三个是i )FLOOR() 向下取整数 如: (3.14) 值 :3POW(x,y) x的y次方 如(2,3) 值 :8RAND() 随机返回0-1值 如 :() 值 : 0.018137501569592863TRUNCATE(x,y) x保留y位小数 如 :(3.1415926,3) 值 :3.141-- abs() 绝对值SELECT ABS (-100);-- pi() 返回圆 规率SELECT PI();-- SQRT ()返回非正数x的二次方SELECT SQRT(2);-- POW 返回 x的 y 次乘方 须要SELECT POW(2,10);SELECT POWER(2,10)-- CELL() or SELECT CEIL(3.14); -- 向上取整数-- floor()SELECT FLOOR(3.14); -- 向下取整数-- round() 四舍五入取整数 还能够保留小数SELECT ROUND(3.4);SELECT ROUND(3.5);SELECT ROUND(3.7,2)-- pow() x 的 y 次方SELECT POW(2,3);-- random 随机0到1 SELECT RAND();-- truncate() 保留小数 抉择 SELECT TRUNCATE(3.14159265758,3);SELECT TRUNCATE(RAND()* 1000 ,3);字符串函数字符串函数 ...

September 24, 2021 · 2 min · jiezi

关于函数:把递归函数改为非递归的通用套路

这里指的通用套路是把递归执行改为在一个函数中循环执行。出于好奇心想找出一种把递归改为非递归的通用形式,并学习其中的思路。在网上找了几篇文章,联合函数调用栈的了解,感觉本人总结的应该比拟全面了,所以记录下来跟大家交换下。 递归执行和一般嵌套执行的区别先看一段简略的代码,计算阶乘的递归函数: // n! = n * (n - 1) * (n - 2) * ... * 1function getFactorial(n) { if (n === 1) { return 1; } else { return n * getFactorial(n - 1); }}递归调用简略点来说就是函数在执行过程中调用了本身。在下一次调用中如果没达到设定的递归完结条件,这个过程会始终继续上来;当递归条件完结时,调用链条中的函数会一个接一个的返回。如以上的代码,当 n === 1 时,就会触发递归的完结条件。 这里咱们思考一下,递归函数的执行跟一般的函数嵌套执行有什么不同?:thinking: 其实没什么本质区别,递归调用和一般函数嵌套调用的层数一样也是无限的,层数的多少由递归完结条件来决定,无非是递归是本身调用本身(直觉上是代码的执行又回到了后面的行数)。接下咱们康康函数嵌套执行时产生了什么。 计算机是如何嵌套执行函数的?如果你理解计算机执行汇编/机器码的原理就会晓得我接下来可能会说运行时函数调用栈。不过我打算用一种简略的形容来疏导你明确或加深印象。 首先,我先问个问题:一个函数在某一次的执行过程中,是什么货色让这一次执行与另外一次执行是有所差异的? 简略点来说,能够把这个问题了解为函数执行上下文。这个上下文有以下内容: 参数;函数体中定义的变量;返回值;举个函数嵌套执行的栗子,a 函数以后在执行中,这个时候其中有条语句要执行 b 函数,这个时候能够简略了解为计算机做了以下的事件: 保留 a 函数的执行上下文;切换到 b 函数的上下文;代码执行来到了 b 函数的结尾,b 函数执行完并返回值;切换回 a 函数的上下文,继续执行 a 函数残余的代码;这个过程就是运行时函数调用栈,用栈这种数据结构来实现了上下文的切换。 咱们下面说到过递归调用能够类比为一般嵌套调用,无非是这一次的执行上下文切换到了下一次的执行上下文,并且留神还有代码执行的管制(后面说过,递归是又回到了后面行数执行)。通过以上的形容咱们能够失去一些思路,模仿递归,要解决两个次要问题: 模仿函数调用栈,切换执行上下文;管制语句的执行程序,从前面的代码回到后面的代码执行;执行上下文的切换,能够用栈来模仿。那么如何管制语句的执行程序呢?上面来介绍一种 Continuation 技术。 Continuation来看个示例: function test() { console.log(1); console.log(2); console.log(3);}以上的输入是 123,咱们当初要从新封装一个函数,内容一样,然而扭转这三句代码的执行程序,2 输入后持续回到 1,第二次输入 2 时再持续输入 3 并完结,也就是输入 12123。当然,不是要你反复写多余的 console.log 的。 ...

September 17, 2021 · 4 min · jiezi

关于函数:COS数据工作流云函数最佳实践-自定义音视频转码

01 背景音视频作为信息流传中流量占比最大的局部在各行业的业务中都弥足重要,而不同的业务场景中对音视频的解决逻辑可能具备行业的特殊性。 私有云尽管提供大量的视频解决服务供用户抉择,但仍然不能做到全面笼罩用户的非凡流程及定制化需要,应用 COS 工作流解决联合云函数定制逻辑此时就是一个绝佳抉择,帮忙用户疾速创立满足需要的各种音视频解决服务。 02 利用场景疾速接入用户自建转码集群,兼容用户原有业务;反对行业非凡格局与解决逻辑,接入电影、安防等非凡行业;反对用户自定义解决逻辑,满足各场景下定制流程需要;触发工作流批量模板化解决,满足视频网站、教育、社交互联行业常见音视频解决需要;03 计划劣势减速开发:不再须要关注资源运维与组件开销,极大地升高了服务架构搭建的复杂性;升高开销:闲暇时没有资源在运行,函数执行时按申请数和计算资源的运行工夫免费,价格优势显著;高可用、高扩大:依据申请主动平行调整服务资源,领有近乎有限的扩容能力,且罢黜单可用区运行的故障危险;04 配置步骤1.到 COS 控制台存储桶详情,创立工作流,能够自定义过滤后缀过滤规定,创立自定义函数节点。 2.在函数节点弹窗里,点击新建函数,浏览器新标签会关上 SCF 的创立云函数的页面。 3.创立云函数步驟: A. 抉择“COS 数据工作流音视频转码”模板;B. 配置足够的内存、执行超时工夫;C. 该函数模板反对五个环境变量;i. targetBucket:指标存储桶,必填;ii. targetRegion:指标存储桶地区,必填;iii. targetKeyTemplate:指标门路模板,可选,默认$$_transcode.$;vi. ffmpegTemplate:转码命令模板,必填,例如$ -loglevel error -i $ -r 10 -b:a 32k $;v. localTmpPath:长期保留门路,当绑定CFS时能够更改长期门路,可选,默认 /tmp;D. 启用权限配置,绑定蕴含以后存储桶读权限和转码后存储桶写权限的角色,创立运行角色请看文档;E. 点击实现。 如需新建运行角色,能够抉择“云函数”作为角色载体,配置 QcloudCOSFullAccess 权限,或新建角色后自行绑定只蕴含存储桶的权限。 4.回到方才创立工作流的页面,选中刚创立的自定义转码函数,并保留工作流,在工作流列表页开启工作流。 5.上传文件,查看工作流解决胜利后,能够看到上传的视频已胜利转码保留为新的文件。

August 6, 2021 · 1 min · jiezi

关于函数:COS-数据工作流-Serverless云函数自定义处理能力发布

01 背景在工业4.0的浪潮下,智能和数据与物理世界联合越加严密,多元化、灵便、高效的数据处理能力成为各行各业的热点需要。尽管COS曾经预置电商、文创、教育、社交、安防等行业须要的根底数据处理能力,但在非凡流程和定制化需要方面私有云仍然难以做到全方位满足客户需要。 02 数据工作流全新能力出炉:反对自定义云函数COS 数据工作流是一套残缺的端到云到端的数据存储、解决、公布等“一站式”云上智能解决方案。 COS 数据工作流最新减少了自定义函数的解决能力,开发者可增加 Serverless 云函数节点,实现业务具体的定制需要。 Serverless 云函数在COS本身数据处理能力之外,为用户提供更多可能性,满足用户自研定制化需要的同时,让开发者只需专一于外围业务逻辑研发,使生产力失去极大的开释,降本增效。 COS+云函数工作流程 在 COS 工作流+云函数联合的工作模式下,能够解决各行业不同用户的定制化流程痛点,满足大批量的定制化需要。 03 计划劣势低成本:毫秒粒度按量计费,在闲暇时主动缩容,极大节俭服务器老本。与传统本地转码相比,老本能够升高 60% 以上;高并发:依据申请主动调整服务资源,反对上万台计算节点同时扩容,打消并发压力;灵便定制:可依据业务须要,自定义函数的解决逻辑,也可平滑迁徙本地逻辑上云,满足开发者不同场景需要;减速开发:聚焦业务开发,无须要关注资源运维与组件开销,升高服务架构搭建的复杂性与运维老本; 04 最佳实际数据工作流作为一种功能强大、灵便易用的数据处理云上智能解决方案,在不同的场景下有不同的利用计划。依据 COS 用户应用工作流的共性需要,咱们提供了两个应用场景的最佳实际: 最佳实际一:自定义转码 私有云尽管提供大量的视频解决服务供用户抉择,但仍然不能做到全面笼罩用户的非凡流程及定制化需要,应用 COS 工作流解决联合云函数定制逻辑此时就是一个绝佳抉择,帮忙用户疾速创立满足需要的各种音视频解决服务。 操作阐明:创立工作流的自定义函数节点时,抉择“COS 数据工作流音视频转码”,即可实现自定义的转码。 最佳实际二:计算文件校验值 COS 文件上传下载场景下,为保障文件完整性,用户须要计算文件的哈希值。目前 COS 只提供 CRC64 值,当用户须要应用MD5、SHA1、SHA256值校验时 ,可应用工作流联合自定义函数模板。 操作阐明:创立工作流的自定义函数节点时,抉择“计算COS对象的哈希值”,即可主动执行文件哈希值计算。以下截图是计算后的哈希值示例。 往期举荐https://segmentfault.com/a/11...

August 5, 2021 · 1 min · jiezi

关于serverless:独家对话阿里云函数计算负责人不瞋你所不知道的-Serverless

简介: 如果你是一名互联网研发人员,那么极有可能理解并利用过 Serverless 这套技术体系。纵观 Serverless 过来十年,它其实因云而生,也在同时扭转云的计算形式。如果套用技术成熟度曲线来形容的话,那么它曾经走过了萌芽期、认知幻灭期,开始朝着成熟稳固的方向倒退。将来,市场对 Serverless 的接受程度将越来越高。 作者 | 杨丽起源 | 雷锋网(ID:leiphone-sz) Serverless 其实离咱们并没有那么边远。如果你是一名互联网研发人员,那么极有可能理解并利用过 Serverless 这套技术体系。纵观 Serverless 过来十年,它其实因云而生,也在同时扭转云的计算形式。如果套用技术成熟度曲线来形容的话,那么它曾经走过了萌芽期、认知幻灭期,开始朝着成熟稳固的方向倒退。将来,市场对 Serverless 的接受程度将越来越高。 不要诧异,阿里云团队在真正开始构建 Serverless 产品体系的最开始的一两年里,也曾遭逢外部的一些争议。而今,单从阿里团体外部的很多业务线来看,曾经在朝着 Serverless 化的方向倒退了。 日前,阿里云凭借函数计算产品能力寰球第一的劣势,入选 Forrester 2021 年第一季度 FaaS 平台评估报告,成为比肩亚马逊成为寰球前三的 FaaS 领导者。这也是首次有国内科技公司进入 FaaS 领导者象限。 在与雷锋网的访谈中,阿里云 Serverless 负责人不瞋阐释了 Serverless 的演进历程、引入 Serverless 面临的难点与挑战、以及无关云原生的趋势预判。 “肯定要想明确做这件事的终局是什么,包含产品体系的定位,对开发者、对服务商的价值等等这些问题。这要求咱们一直通过实际和意识的深入,让这些问题的答复可能逐步清晰起来。这也是咱们这么多年实际积攒的贵重教训。”不瞋指出。 只管企业的实际还存在种种纳闷和挑战,但 Serverless 实际上离咱们并没有那么边远。举一个最近的例子,新冠疫情让近程办公、在线教育、在线游戏的利用需要短期内减少。业务规模的爆发式增长,对每一个需要的响应须要更加及时,这对利用架构的弹性,对底层计算的速度,对研发效率的晋升等,都要求业务减速向新技术架构演进。 而不瞋的现实就是,帮忙更宽泛的客户实现向新技术架构的平滑迁徙,让 Serverless 渗透到所有的云利用中。 不瞋作为阿里云 Serverless 产品体系的负责人,也是国内 Serverless 的晚期实践者。以下将出现是对这次访谈的残缺总结。 Serverless 的定义 在探讨之前,咱们先明确 Serverless 的定义,确保大家对 Serverless 的认知是统一的。 当初 Serverless 越来越热,无论是工业界还是学术界,都将 Serverless 视为云计算倒退的下一阶段。Serverless 有很多种表述,其中伯克利大学的定义绝对谨严一些。 ...

April 22, 2021 · 2 min · jiezi

关于神经网络:基于深度神经网络的噪声标签学习

摘要:介绍带噪学习畛域前沿办法,解决不完满场景下的神经网络优化策略,旨在晋升模型性能。本文分享自华为云社区《Learning from Noisy Labels with Deep Neural Networks》,原文作者:猜沟。 Introduction:神经网络的胜利建设在大量的洁净数据和很深的网络模型根底上。然而在事实场景中数据和模型往往不会特地现实,比方数据层面有误标记的状况,像小狗被标注成狼,而且理论的业务场景考究时效性,神经网络的层数不能特地深。咱们尝试一直迭代数据和模型缺点状况下神经网络的无效训练方法,通过noisy label learning技术,解决网络训练过程中noisy data的问题,该技术曾经在团队理论业务场景中落地,通过从损失函数、网络结构、模型正则化、损失函数调整、样本抉择、标签纠正等多个模块的优化,不局限于全监督、半监督和自监督学习办法,晋升整个模型的鲁棒性 Framework: 【Robust Loss Function】次要是从损失函数去批改,外围思路是当数据整体是洁净的时候,传统的穿插熵损失函数学习到大量的负样本,能够晋升模型的鲁棒性;当数据噪声比拟大时,CE会被噪声数据带跑偏,咱们要批改损失函数使其在训练中每个样本的权重都是一样重要的,因而不难想到采纳GCE Loss,管制超参数,联合了CE Loss和MAE Loss A. Ghosh, H. Kumar, and P. Sastry,“Robust loss functions under label noise for deep neural networks,” in Proc. AAAI, 2017Generalized Cross Entropy Loss for Training Deep Neural Networks with Noisy Labels, NeurlPS 2018另外,还有从KL散度想法借鉴过去的,作者认为在计算熵的时候,原始q, p代表的实在数据分布和预测值在较为洁净的数据上没有问题,然而在噪声比拟大的数据上,可能q并不能代表实在数据分布,相同的是不是p能够示意实在数据分布,因而提出基于对称的穿插熵损失函数(Symmetric cross entropy )Y. Wang, X. Ma, Z. Chen, Y. Luo, J. Yi, and J. Bailey, “Symmetric cross entropy for robust learning with noisy labels,” in Proc. ICCV, 2019, pp. 322–330【Robust Architecture】这一部分次要通过借鉴奇妙的网络结构,在模型训练过程中,通过模型对数据进行筛选,抉择一批较为洁净的数据,逐渐晋升模型的鲁棒性。首先要介绍的就是coteaching framework,首先是基于两个模型互相筛选数据输出给对方计算loss,传递给对方网络的数据是每个min-batch外面loss最低的一些数据,随着epoch减少,数据量有所变动,另外每一轮epoch完结,会shuffle数据,保证数据不会被永恒忘记 ...

April 14, 2021 · 1 min · jiezi

关于函数:函数计算助力高德地图平稳支撑亿级流量高峰

简介: 2020 年的“十一出行节”期间,高德地图发明了记录 ——截止 2020 年 10 月 1 日 13 时 27 分 27 秒,高德地图当日沉闷用户冲破 1 亿,比 2019 年 10 月 1 日提前 3 时 41 分达成此记录。 期间,Serverless 作为其中一个核心技术场景,安稳扛住了流量高峰期的考验。值得一提的是,由 Serverless 撑持的业务在流量高峰期的体现非常优良,每分钟函数调用量靠近两百万次。这再次验证了 Serverless 根底技术的价值,进一步拓展了技术场景。 客户介绍2020 年的“十一出行节”期间,高德地图发明了记录 ——截止 2020 年 10 月 1 日 13 时 27 分 27 秒,高德地图当日沉闷用户冲破 1 亿,比 2019 年 10 月 1 日提前 3 时 41 分达成此记录。 期间,Serverless 作为其中一个核心技术场景,安稳扛住了流量高峰期的考验。值得一提的是,由 Serverless 撑持的业务在流量高峰期的体现非常优良,每分钟函数调用量靠近两百万次。这再次验证了 Serverless 根底技术的价值,进一步拓展了技术场景。 客户痛点自主出行是高德地图的外围业务,波及到用户出行相干的性能诉求,承载了高德地图 APP 内最大的用户流量。自主出行外围业务中利用 Node FaaS 的局部场景包含主图场景页、路线布局页和导航完结页等。 ...

April 9, 2021 · 1 min · jiezi

关于函数:冷启动延时缩短5080阿里云函数计算发布冷启动加速技术

简介: 近日,阿里云函数计算重磅公布冷启动减速技术,将本来属于开发者的镜像优化累赘转由函数计算承当,进一步帮忙开发者进步生产效率,专一业务翻新。该技术源于阿里团体超大规模和场景高度简单的容器环境,对镜像存储、减速技术有深厚的积攒,并杰出地承当了3年双十一,双十二,春节等大促秒杀场景的严苛的挑战。 容器镜像因其颠覆式翻新成为云原生时代利用部署格局的事实标准。头部云厂商 FaaS (Function-as-a-Service) 服务如阿里云函数计算、AWS Lambda 也相继在2020年反对应用容器镜像部署函数,全面拥抱容器生态。自公布以来,开发者陆续将机器学习、音视频解决、事件驱动离线数据处理、前端自动化等多个场景应用镜像疾速无服务器化,提高效率、降低成本。然而,冷启动始终是 Serverless 无奈绕开的问题。容器镜像须要将数据通过网络近程下载并解压,对于GB级别的镜像,拉取工夫可能高达分钟级别,主观上放大了冷启动副作用,妨碍实时利用的 Serverless 演进。 函数计算冷启动减速性能近日,阿里云函数计算重磅公布冷启动减速技术,将本来属于开发者的镜像优化累赘转由函数计算承当,进一步帮忙开发者进步生产效率,专一业务翻新。该技术源于阿里团体超大规模和场景高度简单的容器环境,对镜像存储、减速技术有深厚的积攒,并杰出地承当了3年双十一,双十二,春节等大促秒杀场景的严苛的挑战。据悉,函数计算已将该技术上线至:杭州、北京、上海、美东、美西等 region。 减速成果咱们在抉择了外部生产环境和开源社区的工作负载,笼罩机器学习、人工智能、前端自动化、Web 利用等7种镜像大小、IO 拜访模式、启动命令的不同组合作为 benchmark,部署在 FC 北京区域。如下图所示,函数计算开启镜像减速性能后减速广泛超过 50%,对于机器学习场景中常见的臃肿镜像(如多个团队共享根底镜像, ml-small-import, ml-large-import, ai-cat-or-dog)减速成果更为显著(约 70%-86%),镜像越大优化空间往往越高。 性能特点FC 镜像减速具备以下特点:应用简略:只需在函数上开启镜像减速,函数计算会主动制作减速镜像和缓存,转换实现后(5分钟以内),函数主动采纳减速镜像缓存。专一业务翻新:开发者无需破费工夫刻意精简优化镜像大小或严格辨别 Serverless 和 Serverfull 利用镜像的构建形式,FC 负责依照利用理论应用数据拉取和解压。减速收费,应用门槛低:镜像减速开启不产生额定费用,也不须要开发者额定购买或降级任何其余服务。事实上因为镜像拉取工夫变短,相应的申请费用也随之升高。极速弹性、缩容到 0、事件触发:FaaS 联合容器镜像曾经极大简化了利用迁徙至 Serverless,减速性能进一步解锁了实时、准实时工作负载,已经须要分钟级别的容器启动当初能够几秒内疾速启动,真正实现缩容到0。将来布局此次函数计算公布的冷启动减速技术,通过按需读取和更高效的解压技术在不同场景下减速 50%-80%,即便 GB 级别的镜像也能够在几秒内实现端到端启动。减速性能联合函数计算极致弹性和事件触发的特点,解锁了更多对实时要求高的工作负载。容器利用能够更容易地享受 Serverless 个性,真正做到缩容到0以及疾速大规模扩容。FC 在将来会继续优化冷启动各个环节提供极致弹性,承当更多用户责任,使开发者专一业务翻新。 附录:试验场景数据 原文链接本文为阿里云原创内容,未经容许不得转载。

March 31, 2021 · 1 min · jiezi

关于函数:启动延时缩短-5080函数计算发布镜像加速功能

简介: 容器镜像因其颠覆式翻新成为云原生时代利用部署格局的事实标准。头部云厂商 FaaS (Function-as-a-Service) 服务如阿里云函数计算、AWS Lambda 也相继在 2020 年反对应用容器镜像部署函数,全面拥抱容器生态。 作者 | Shuai Chang  阿里云云原生 Serverless 团队高级技术专家起源 | 阿里巴巴云原生公众号 体验文档:镜像拉取减速文档 FaaS 和容器容器镜像因其颠覆式翻新成为云原生时代利用部署格局的事实标准。头部云厂商 FaaS (Function-as-a-Service) 服务如阿里云函数计算、AWS Lambda 也相继在 2020 年反对应用容器镜像部署函数,全面拥抱容器生态。自公布以来,开发者陆续将机器学习、音视频解决、事件驱动离线数据处理、前端自动化等多个场景应用镜像疾速无服务器化,提高效率、降低成本。然而,冷启动始终是 Serverless 无奈绕开的问题。容器镜像须要将数据通过网络近程下载并解压,对于 GB 级别的镜像,拉取工夫可能高达分钟级别,主观上放大了冷启动副作用,妨碍实时利用的 Serverless 演进。 函数计算镜像减速性能传统的镜像拉取减速强调"开发者负责",如精简镜像,正当调配镜像层,multi-stage 构建,应用工具(如 docker-slim)去除不须要的数据,遵循构建最佳实际等。这些工作不仅减轻了用户累赘,减速成果无限,且有运行时稳定性危险。阿里团体超大规模和场景高度简单的容器环境,对镜像存储、减速技术有深厚的积攒,杰出地承当了 3 年双十一、双十二、春节等大促秒杀场景的严苛的挑战。阿里云 Serverless 同容器镜像、存储等服务深度单干,将外部翻新在函数计算输入:杭州、北京、上海、美东、美西正式公布了镜像减速性能。该性能将本来属于开发者的镜像优化累赘转由函数计算承当,进一步帮忙开发者进步生产效率,专一业务翻新。 减速成果咱们在抉择了外部生产环境和开源社区的工作负载,笼罩机器学习、人工智能、前端自动化、Web 利用等 7 种镜像大小、IO 拜访模式、启动命令的不同组合作为 benchmark,部署在 FC 北京区域。如下图所示,函数计算开启镜像减速性能后减速广泛超过 50%,对于机器学习场景中常见的臃肿镜像(如多个团队共享根底镜像, ml-small-import, ml-large-import, ai-cat-or-dog)减速成果更为显著(约 70%-86%),镜像越大优化空间往往越高。 应用形式镜像减速能够通过控制台、CLI 工具或是 FC SDK 开启,具体步骤参考镜像拉取减速文档。 形式一:在函数计算控制台函数配置下抉择“开启镜像减速”。  形式二:应用 Funcraft 工具部署。在已有的 CustomContainerConfig 配置下增加 AccelerationType: Default 如需敞开则配置 AccelerationType: ...

March 23, 2021 · 1 min · jiezi

关于函数:Serverless-可观测性的过去现在与未来

简介: 函数计算可观测性经历了 1.0 -> 2.0 的倒退,从闭门造车的可观测倒退成开源的可观测,从平台的可观测倒退为开发者的可观测,从FaaS Only 的可观测演进成了云原生的可观测。 作者:夏莞 背景Serverless 将成为下一个十年云的默认编程范式随着 Serverless 概念的进一步遍及,开发者逐步从张望状态进入尝试阶段,越来越多的企业用户开始将业务迁徙到 Serverless 平台。在阿里团体外部,淘宝、飞猪、闲鱼、高德、语雀等外围性能稳步落地,在阿里团体内部,新浪微博、世纪联华、石墨文档、TPLink、蓝墨云班课等各行各业的企业也纷纷解锁 Serverless 应用的不同场景。Serverless 正在成为成为下一个十年云的默认编程范式。 更多案例请参考 函数计算用户案例 Serverless 降本增效免运维的个性为开发者带来了实打实的益处:基于函数计算的 Serverless 计划为蓝墨节俭了 60% 左右的 IT 老本,为石墨文档节约了 58% 的服务器老本;晋升码隆科技的开发效率,实现两周内性能上线;安稳撑持负载的波峰波谷相差 5 倍以上的新浪微博,每天轻松解决数十亿申请。 广告工夫:欢送退出云原生Serverless 团队(函数计算,Serverless工作流,Serverless利用引擎),以公共云、团体、开源社区三位一体的形式打造业界当先的Serverless 产品体系。职位需要见 JD,招聘长期有效,有趣味的同学能够分割本文作者或 @Chang, Shuai(shuai.chang)。 可观测性成为 Serverless 倒退的绊脚石?随着 Serverless 的深刻应用,开发者逐步发现 Serverless 架构下的问题定位比传统利用更加艰难,次要起因如下: 组件散布化:Serverless 架构的利用往往粘合多个云服务,申请须要流经多款云产品,一旦端到端延时变长或体现不合乎预期,问题定位十分复杂,须要顺次去各个产品侧逐渐排查。调度黑盒化:Serverless 平台承当着申请调度、资源分配的责任,实时弹性扩容会带来不可避免的冷启动,Serverless 的资源伸缩是无需开发者参加也不受开发者管制的。冷启动会影响端对端延时,这次申请有没有遇到冷启动,冷启动的工夫都耗费在哪些步骤,有没有可优化的空间都是开发者急于晓得的问题。执行环境黑盒化:开发者习惯于在本人的机器上执行本人的代码,出了问题登录机器查看异样现场,查看执行环境的 CPU/内存/IO 状况。面对 Serverless 利用,机器不是本人的,登也登不上,看也看不了,开发者眼前一片乌黑。产品非标化:在 Serverless 场景下,开发者无法控制执行环境,无奈装置探针,无奈应用开源的三方监控平台,考察问题的形式不得不产生扭转,传统的考察问题教训无奈施展,十分不棘手。函数计算是阿里云的 Serverless 产品,在过来的一年,函数计算团队为了更好地答复以上问题做了很多致力。 本文次要介绍函数计算在可观测性上的尝试与函数计算可观测性现状。 Serverless 下可观测性可观测性是通过内部体现判断零碎外部状态的掂量形式。--维基百科 在利用开发中,可观测性帮忙咱们判断零碎外部的健康状况。在零碎安稳运行时,帮忙咱们评估危险,预测可能呈现的问题。当零碎呈现问题时,帮忙咱们疾速定位问题,及时止损。 一个好的可观测性零碎要帮忙用户尽可能快地发现问题、定位问题并且端到端地解决问题。 在 Serverless 这种免运维的平台体系中,可观测性是开发者的眼睛,没有可观测,何谈高可用? 可观测性 1.0 图1:可观测性根底 可观测性次要蕴含三个局部:日志、指标、链路追踪。 ...

March 18, 2021 · 2 min · jiezi

关于函数:前端面试每日-31-第661天

明天的知识点 (2021.02.05) —— 第661天 (我也要出题)[html] 写html代码时,怎么才减速写代码的速度呢?你有什么办法?[css] 应用css制作一个圣诞树[js] 请应用js实现一个有限累加的函数[软技能] 来这面试前你有做过哪些筹备吗?《论语》,曾子曰:“吾日三省吾身”(我每天屡次检查本人)。前端面试每日3+1题,以面试题来驱动学习,每天提高一点!让致力成为一种习惯,让奋斗成为一种享受!置信 保持 的力量!!!欢送在 Issues 和敌人们一起探讨学习! 我的项目地址:前端面试每日3+1【举荐】欢送跟 jsliang 一起折腾前端,零碎整顿前端常识,目前正在折腾 LeetCode,打算买通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个Star, 同时欢送微信扫码关注 前端剑解 公众号,并退出 “前端学习每日3+1” 微信群互相交换(点击公众号的菜单:交换)。 学习不打烊,充电加油只为遇到更好的本人,365天无节假日,每天早上5点纯手工公布面试题(死磕本人,愉悦大家)。心愿大家在这虚夸的前端圈里,放弃沉着,保持每天花20分钟来学习与思考。在这变幻无穷,类库层出不穷的前端,倡议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢送大家到Issues交换,激励PR,感激Star,大家有啥好的倡议能够加我微信一起交换探讨!心愿大家每日去学习与思考,这才达到来这里的目标!!!(不要为了谁而来,要为本人而来!)交换探讨欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个[Star]

February 5, 2021 · 1 min · jiezi

关于函数:代码-or-指令浅析ARM架构下的函数的调用过程

摘要:linux程序运行的状态以及如何推导调用栈。1、背景常识1、ARM64寄存器介绍: 2、STP指令详解(ARMV8手册): 咱们先看一下指令格局(64bit),以及指令对于存放机执行后果的影响 类型1、STP <Xt1>, <Xt2>, [<Xn|SP>], #<imm> 将Xt1和Xt2存入Xn|SP对应的地址内存中,而后,将Xn|SP的地址变更为Xn|SP + imm偏移量的新地址 类型2、STP <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]! 将Xt1和Xt2存入Xn|SP的地址自加imm对应的地址内存中,而后,将Xn|SP的地址变更为Xn|SP + imm的offset偏移量后的新地址 类型3、STP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}] 将Xt1和Xt2存入Xn|SP的地址自加imm对应的地址内存中 手册中有三种操作码,咱们只探讨程序中波及的后两种 Pseudocode如下: Shared decode for all encodings integer n = UInt(Rn); integer t = UInt(Rt); integer t2 = UInt(Rt2); if L:opc<0> == '01' || opc == '11' then UNDEFINED; integer scale = 2 + UInt(opc<1>); integer datasize = 8 << scale; bits(64) offset = LSL(SignExtend(imm7, 64), scale); ...

January 28, 2021 · 3 min · jiezi

关于函数:其他类成员构造函数属性索引器析构函数

讲了底层实现,咱们持续讲语法。 从语法的角度,当运行new Student()生成对象的时候,实际上是调用了Student类中的 构造函数(constructor) 构造函数是在类中,和类名雷同的、像办法一样能够带参数,但没有返回的,用于创立类的实例的……飞哥,等等,你等等,你说的这玩意儿在哪里呢?我在Student类里看不到啊! Good question!带着脑子听课,才是正确的姿态。 实际上,如果一个类没有显式的申明任何构造函数,默认就自带一个无参的无内容的构造函数。所以你看不到,然而飞哥能够把它写进去给你瞅一眼: internal class Student { //无参构造函数 public /没有返回/ Student(/能够有参数/) //和类同名 { //像办法一样,也能够有内容 } } 咱们挨着讲,首先,想一想,为什么构造函数没有返回?如果办法没有返回值,还要给个void,构造函数是咋回事?其实构造函数是有返回值的,它必然返回以后类的一个实例(对象)!这是不可更改的铁律,所以,C#在语法设计的时候就罗唆省略之,既节俭了代码输出,有能够和办法进行辨别,赞一个!^_^ 接下来,既然能够有参数,有能够有内容,咱们加上试试: class Student { public Student(string name) //有参构造函数 { Console.WriteLine($"你好!{name}同学,源栈欢送你……"); } } 构造函数有参数了,调用的时候就得给参数: Student ywq = new Student("于维谦"); 演示: 同时你会发现再也不能应用new Student()调用无参的构造函数了。这是因为:一旦申明了任何构造函数,之前默认自带的无参构造函数隐没。 如果你还想保留无参构造函数,须要在类中显式申明。即一个类中能够有多个参数不同的构造函数,如下所示: internal class Student { public Student() {} public Student(string name){} public Student(string name, int age){} } 调用的时候,按办法重载规定进行匹配。 演示: 给构造函数传参干嘛呢?它通常用于给字段赋值,就像这样: private string name; public Student(string name) { this.name = name; //增加this辨别字段name和参数name } ...

December 27, 2020 · 2 min · jiezi

关于函数:前端面试每日-31-第492天

明天的知识点 (2020.08.20) —— 第492天 (我也要出题)[html] 应用canvas时你有遇到过哪些坑?是如何解决的?[css] 你最喜爱的是哪个css个性?为什么?[js] 实现一个函数sum, 满足以下需要:[软技能] https是如何保障数据传输的平安的?《论语》,曾子曰:“吾日三省吾身”(我每天屡次检查本人)。前端面试每日3+1题,以面试题来驱动学习,每天提高一点!让致力成为一种习惯,让奋斗成为一种享受!置信 保持 的力量!!!欢送在 Issues 和敌人们一起探讨学习! 我的项目地址:前端面试每日3+1【举荐】欢送跟 jsliang 一起折腾前端,零碎整顿前端常识,目前正在折腾 LeetCode,打算买通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个Star, 同时欢送微信扫码关注 前端剑解 公众号,并退出 “前端学习每日3+1” 微信群互相交换(点击公众号的菜单:交换)。 学习不打烊,充电加油只为遇到更好的本人,365天无节假日,每天早上5点纯手工公布面试题(死磕本人,愉悦大家)。心愿大家在这虚夸的前端圈里,放弃沉着,保持每天花20分钟来学习与思考。在这变幻无穷,类库层出不穷的前端,倡议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢送大家到Issues交换,激励PR,感激Star,大家有啥好的倡议能够加我微信一起交换探讨!心愿大家每日去学习与思考,这才达到来这里的目标!!!(不要为了谁而来,要为本人而来!)交换探讨欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个[Star]

August 20, 2020 · 1 min · jiezi

真香阿里工程师的一段代码让我看饿了

阿里妹导读:打开盒马app,相信你跟阿里妹一样,很难抵抗各种美味的诱惑。颜值即正义,盒马的图片视频技术逼真地还原了食物细节,并在短短数秒内呈现出食物的最佳效果。今天,我们请来阿里高级无线开发工程师莱宁,解密盒马app里那些“美味”视频是如何生产的。一、前言图片合成视频并产生类似PPT中每页过渡特效的能力是目前很多短视频软件带有的功能,比如抖音的影集。这个功能主要包括图片合成视频、转场时间线定义和OpenGL特效等三个部分。 其中图片转视频的流程直接决定了后面过渡特效的实现方案。这里主要有两种方案: 图片预先合成视频,中间不做处理,记录每张图片展示的时间戳位置,然后在相邻图片切换的时间段用OpenGL做画面处理。图片合成视频的过程中,在画面帧写入时同时做特效处理。方案1每个流程都比较独立,更方便实现,但是要重复处理两次数据,一次合并一次加特效,耗时更长。 方案2的流程是相互穿插的,只需要处理一次数据,所以我们采用这个方案。 下面主要介绍下几个重点流程,并以几个简单的转场特效作为例子,演示具体效果。 二、图片合成1.方案 图片合成视频有多种手段可以实现。下面谈一下比较常见的几种技术实现。 I.FFMPEG 定义输出编码格式和帧率,然后指定需要处理的图片列表即可合成视频。 ffmpeg -r 1/5 -i img%03d.png -c:v libx264 -vf fps=25 -pix_fmt yuv420p out.mp4II.MediaCodec 在使用Mediacodec进行视频转码时,需要解码和编码两个codec。解码视频后将原始帧数据按照时间戳顺序写入编码器生成视频。但是图片本身就已经是帧数据,如果将图片转换成YUV数据,然后配合一个自定义的时钟产生时间戳,不断将数据写入编码器即可达到图片转视频的效果。 III.MediaCodec&OpenGL 既然Mediacodec合成过程中已经有了处理图片数据的流程,可以把这个步骤和特效生成结合起来,把图片处理成特效序列帧后再按序写入编码器,就能一并生成转场效果。 2.技术实现 首先需要定义一个时钟,来控制图片帧写入的频率和编码器的时间戳,同时也决定了视频最终的帧率。 这里假设需要24fps的帧率,一秒就是1000ms,因此写入的时间间隔是1000/24=42ms。也就是每隔42ms主动生成一帧数据,然后写入编码器。 时间戳需要是递增的,从0开始,按照前面定义的间隔时间差deltaT,每写入一次数据后就要将这个时间戳加deltaT,用作下一次写入。 然后是设置一个EGL环境来调用OpenGL,在Android中一个OpenGl的执行环境是threadlocal的,所以在合成过程中需要一直保持在同一个线程中。Mediacodec的构造函数中有一个surface参数,在编码器中是用作数据来源。在这个surface中输入数据就能驱动编码器生产视频。通过这个surface用EGL获取一个EGLSurface,就达到了OpenGL环境和视频编码器数据绑定的效果。 这里不需要手动将图片转换为YUV数据,先把图片解码为bitmap,然后通过texImage2D上传图片纹理到GPU中即可。 最后就是根据图片纹理的uv坐标,根据外部时间戳来驱动纹理变化,实现特效。 三、转场时间线对于一个图片列表,在合成过程中如何衔接前后序列图片的展示和过渡时机,决定了最终的视频效果。 假设有图片合集{1,2,3,4},按序合成,可以有如下的时间线: 每个Stage是合成过程中的一个最小单元,首尾的两个Stage最简单,只是单纯的显示图片。中间阶段的Stage,包括了过渡过程中前后两张图片的展示和过渡动画的时间戳定义。 假设每张图片的展示时间为showT(ms),动画的时间为animT(ms)。 相邻Stage中同一张图的静态显示时间的总和为一张图的总显示时间,则首尾两个Stage的有效时长为showT/2,中间的过渡Stage有效时长为showT+animT。 其中过渡动画的时间段又需要分为: 前序退场起始点enterStartT,前序动画开始时间点。前序退场结束点enterEndT,前序动画结束时间点。后序入场起始点exitStartT,后序动画开始时间点。后序入场结束点exitEndT,后序动画结束时间点。动画时间线一般只定义为非淡入淡出外的其他特效使用。为了过渡的视觉连续性,前后序图片的淡入和淡出是贯穿整个动画时间的。考虑到序列的衔接性,退场完毕后会立刻入场,因此enterEndT=exitStartT。 四、OpenGL特效1.基础架构 按照前面时间线定义回调接口,用于处理动画参数: //参数初始化protected abstract void onPhaseInit();//前序动画,enterRatio(0-1)protected abstract void onPhaseEnter(float enterRatio);//后序动画,exitRatio(0-1)protected abstract void onPhaseExit(float exitRatio);//动画结束protected abstract void onPhaseFinish();//一帧动画执行完毕,步进protected abstract void onPhaseStep();定义几个通用的片段着色器变量,辅助过渡动画的处理: //前序图片的纹理uniform sampler2D preTexture//后序图片的纹理uniform sampler2D nextTexture;//过渡动画总体进度,0到1uniform float progress;//窗口的长宽比例uniform float canvasRatio;//透明度变化uniform float canvasAlpha;前后序列的混合流程,根据动画流程计算出的两个纹理的UV坐标混合颜色值: ...

November 4, 2019 · 3 min · jiezi

基于函数计算的-Serverless-AI-推理

前言概述本文介绍了使用函数计算部署深度学习 AI 推理的最佳实践, 其中包括使用 FUN 工具一键部署安装第三方依赖、一键部署、本地调试以及压测评估, 全方位展现函数计算的开发敏捷特性、自动弹性伸缩能力、免运维和完善的监控设施。 1.1 DEMO 概述 通过上传一个猫或者狗的照片, 识别出这个照片里面的动物是猫还是狗 DEMO 示例效果入口: http://sz.mofangdegisn.cnDEMO 示例工程地址: https://github.com/awesome-fc/cat-dog-classify1.2 解决方案 如上图所示, 当多个用户通过对外提供的 url 访问推理服务时候,每秒的请求几百上千都没有关系, 函数计算平台会自动伸缩, 提供足够的执行实例来响应用户的请求, 同时函数计算提供了完善的监控设施来监控您的函数运行情况。 1.3. Serverless 方案与传统自建服务方案对比1.3.1 卓越的工程效率 自建服务函数计算 Serverless基础设施需要用户采购和管理无开发效率除了必要的业务逻辑开发,需要自己建立相同线上运行环境, 包括相关软件的安装、服务配置、安全更新等一系列问题只需要专注业务逻辑的开发, 配合 FUN 工具一键资源编排和部署学习上手成本可能使用 K8S 或弹性伸缩( ESS ),需要了解更多的产品、名词和参数的意义会编写对应的语言的函数代码即可1.3.2 弹性伸缩免运维 自建服务函数计算 Serverless弹性高可用需要自建负载均衡 (SLB),弹性伸缩,扩容缩容速度较 FC 慢FC系统固有毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力,免运维监控报警查询ECS 级别的 metrics提供更细粒度的函数执行情况,每次访问函数执行的 latency 和日志等, 更加完善的报警监控机制1.3.3 更低的成本 函数计算 (FC) 固有自动伸缩和负载均衡功能,用户不需要购买负载均衡 (SLB) 和弹性伸缩。具有明显波峰波谷的用户访问场景(比如只有部分时间段有请求,其他时间甚至没有请求),选择按需付费,只需为实际使用的计算资源付费。对于明显波峰波谷或者稀疏调用具有低成本优势, 同时还保持了弹性能力,以后业务规模做大以后并没有技术切换成本,同时财务成本增长配合预付费也能保持平滑。部分请求持续平稳的场景下,可以配合预付费解决按需付费较高单价问题。函数计算成本优化最佳实践文档。假设有一个在线计算服务,由于是CPU 密集型计算, 因此在这里我们将平均 CPU 利用率作为核心参考指标对成本,以一个月为周期,10台 C5 ECS 的总计算力为例,总的计算量约为 30% 场景下, 各解决方案 CPU 资源利用率使用情况示意图大致如下: ...

October 16, 2019 · 3 min · jiezi

彻底弄懂为什么不能把栈上分配的数组字符串作为返回值

背景最近准备一个教程,案例的过程中准备了如下代码碎片,演示解析http scheme #include <stdio.h>#include <stdlib.h>#include <string.h>char *parse_scheme(const char *url){ char *p = strstr(url,"://"); return strndup(url,p-url);}int main(){ const char *url = "http://static.mengkang.net/upload/image/2019/0907/1567834464450406.png"; char *scheme = parse_scheme(url); printf("%s\n",scheme); free(scheme); return 0;}上面是通过strndup的方式,背后也依托了malloc,所以最后也需要free。有人在微信群私信parse_scheme能用char []来做返回值吗?我们知道栈上的数组也能用来存储字符串,那我们可以改写成下面这样吗? char *parse_scheme(const char *url){ char *p = strstr(url,"://"); long l = p - url + 1; char scheme[l]; strncpy(scheme, url, l-1); return scheme;}大多数人都知道不能这样写,因为返回的是栈上的地址,当从该函数返回之后,那段栈空间的操作权也释放了,当再次使用该地址的时候,值就是不确定的了。 那我们今天就一起探讨下出现这样情况的背后的真正原理。 基础预备每个函数运行的时候因为需要内存来存放函数参数以及局部变量等,需要给每个函数分配一段连续的内存,这段内存就叫做函数的栈帧(Stack Frame)。因为是一块连续的内存地址,所以叫帧;为什么叫要加一个栈呢?想必大家都熟悉了函数调用栈,为什么叫函数调用栈呢?比如下面的表达式 array_values(explode(",",file_get_contents(...)));函数的执行顺序是最内层的函数最先执行,然后依次返回执行外层的函数。所以函数的执行就是利用了栈的数据结构,所以就叫栈帧。 x86_64 cpu上的 rbp 寄存器存函数栈底地址,rsp 寄存器存函数栈顶地址。 实验#include <stdio.h>void foo(void){ int i; printf("%d\n", i); i = 666;}int main(void){ foo(); foo(); return 0;}$gcc -g 2.c$./a.out0666为什么第二次调用foo函数输出的结果都是上次函数调用的赋值呢?先看下反汇编之后的代码 ...

October 15, 2019 · 2 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

开发函数计算的正确姿势借助-Ghostscript-将-PDF-转换成-JPG-精简版-0-0-0

前言首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute):函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息参考。Fun:Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档参考。Ghostscript:Ghostscript 是一套建基于Adobe、PostScript及可移植文档格式(PDF)的页面描述语言等而编译成的自由软件。参见维基百科词条备注: 本文介绍的技巧需要 Fun 版本大于等于 3.0.0-beta.7 。 依赖工具本项目是在 MacOS 下开发的,涉及到的工具是平台无关的,对于 Linux 和 Windows 桌面系统应该也同样适用。在开始本例之前请确保如下工具已经正确的安装,更新到最新版本,并进行正确的配置。 DockerFunFun 工具依赖于 docker 来模拟本地环境。 对于 MacOS 用户可以使用 homebrew 进行安装: brew cask install dockerbrew tap vangie/formulabrew install funWindows 和 Linux 用户安装请参考: https://github.com/aliyun/fun/blob/master/docs/usage/installation.md安装好后,记得先执行 fun config 初始化一下配置。 注意, 如果你已经安装过了 fun,确保 fun 的版本在 3.0.0-beta.7 以上。 $ fun --version3.0.0-beta.7初始化使用 fun init 命令可以快捷地将本模板项目初始化到本地。 fun init vangie/ghostscript-example安装依赖$ fun installInstalling recursively on fun.ymlskip pulling image aliyunfc/runtime-python3.6:build-1.6.1...Task => workaround for update-gsfontmap => bash -c 'mkdir -p /code/.fun/root/etc/ghostscript/cidfmap.d/ && mkdir -p /code/.fun/root/etc/ghostscript/fontmap.d/ && mkdir -p /etc/ghostscript/ && mkdir -p /var/lib/ghostscript/ && mkdir -p /code/.fun/root/var/lib/ghostscript/fonts && ln -s /code/.fun/root/etc/ghostscript/cidfmap.d /etc/ghostscript/ && ln -s /code/.fun/root/etc/ghostscript/fontmap.d /etc/ghostscript/ && ln -s /code/.fun/root/var/lib/ghostscript/fonts /var/lib/ghostscript/'Task => [UNNAMED] => apt-get update (if need) => apt-get install -y -d -o=dir::cache=/code/.fun/tmp/install ghostscript --reinstall => bash -c for f in $(ls /code/.fun/tmp/install/archives/*.deb); do dpkg -x $f /code/.fun/root; mkdir -p /code/.fun/tmp/install/deb-control/${f%.*}; dpkg -e $f /code/.fun/tmp/install/deb-control/${f%.*}; if [ -f "/code/.fun/tmp/install/deb-control/${f%.*}/postinst" ]; then FUN_INSTALL_LOCAL=true /code/.fun/tmp/install/deb-control/${f%.*}/postinst configure; fi; done;Creating config file /etc/papersize with new version => bash -c 'rm -rf /code/.fun/tmp/install/archives'本地调用$ fun local invoke pdf2jpgusing template: template.ymlskip pulling image aliyunfc/runtime-nodejs10:1.6.1...FC Invoke Start RequestId: 21d9c646-1db4-403c-b018-cd4246e193d3load code for handler:index.handler2019-09-18T09:45:38.400Z 21d9c646-1db4-403c-b018-cd4246e193d3 [verbose] stdout =================== START2019-09-18T09:45:38.400Z 21d9c646-1db4-403c-b018-cd4246e193d3 [verbose] GPL Ghostscript 9.26 (2018-11-20)Copyright (C) 2018 Artifex Software, Inc. All rights reserved.This software comes with NO WARRANTY: see the file PUBLIC for details.Processing pages 1 through 1.Page 12019-09-18T09:45:38.401Z 21d9c646-1db4-403c-b018-cd4246e193d3 [verbose] stdout =================== ENDFC Invoke End RequestId: 21d9c646-1db4-403c-b018-cd4246e193d3convert success.JPG file save to /tmp/test.jpg2019-09-18T09:45:38.416Z 21d9c646-1db4-403c-b018-cd4246e193d3 [error](node:21) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.RequestId: 21d9c646-1db4-403c-b018-cd4246e193d3 Billed Duration: 2132 ms Memory Size: 1998 MB Max Memory Used: 78 MB可以查看文件 .fun/tmp/invoke/ghostscript/pdf2jpg/test.jpg ,预留转换后的效果。 ...

September 20, 2019 · 2 min · jiezi

微信小程序模板函数

微信小程序的视图模板的{{}} 方法并不支持直接调用js函数,比如 {{ (new Date()).toLocaleString() }}是无法编译成我们期望的结果的。 我们需要将js函数封装至wxs模块,然后便可以调用 # utils/date.wxsconst date = { now: function() { return (new Date()).toLocaleString() }}module.export = date# pages/date/date.wxml<wxs module="date" src="../../utils/date.wxs"></wxs><view>{{ date.now() }}</view>即刻。

September 8, 2019 · 1 min · jiezi

python-函数-变量和闭包

首先我们认定,python中定义域查找遵循local->Enclosing->Global->Built-in顺序: a=1def func1():... print(a)... a=111... print(a)...func1()Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in func1UnboundLocalError: local variable 'a' referenced before assignment而: a=1def fun():... print(a)... b=111... print(b)...fun()1111print(b)Traceback (most recent call last): File "<stdin>", line 1, in <module>NameError: name 'b' is not defined我们可以得出结论(打脸):内置函数先在内置函数定义域内(前后)寻找变量;找不到之后再从全局变量中引进,且局部变量无法全局。如果global: a=1def func1():... global a... print(a)... a=111... print(a)...func1()1111a111但是不多久后我发现一个问题,代码如下: a=10def test():... a = a + 1... print(a)...test()Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in testUnboundLocalError: local variable 'a' referenced before assignmenttest(a)Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: test() takes 0 positional arguments but 1 was given所以这个问题其实可以被拆分为两个问题,一个是arguments的问题,还有一个是variable的问题。当定义了一个argument的时候只要在括号里把global中的variable代入就是可以运行的,前提就是可一定要定义啊!!!a=1def func1(a):... print(a)... a=111... print(a)...func1(a)1111 ...

August 20, 2019 · 1 min · jiezi

Java-函数优雅之道

导读随着软件项目代码的日积月累,系统维护成本变得越来越高,是所有软件团队面临的共同问题。持续地优化代码,提高代码的质量,是提升系统生命力的有效手段之一。软件系统思维有句话“Less coding, more thinking(少编码、多思考)”,也有这么一句俚语“Think more, code less(思考越多,编码越少)”。所以,我们在编码中多思考多总结,努力提升自己的编码水平,才能编写出更优雅、更高质、更高效的代码。本文总结了一套与Java函数相关的编码规则,旨在给广大Java程序员一些编码建议,有助于大家编写出更优雅、更高质、更高效的代码。这套编码规则,通过在高德采集部门的实践,已经取得了不错的成效。 1.使用通用工具函数案例1: 字符串比较 现象描述: 不完善的写法: thisName != null && thisName.equals(name);更完善的写法: (thisName == name) || (thisName != null && thisName.equals(name));建议方案: Objects.equals(name, thisName);案例2: 判断列表为空 现象描述: !(list == null || list.isEmpty());建议方案: import org.apache.commons.collections4.CollectionUtils;CollectionUtils.isNotEmpty(list);主要收益 函数式编程,业务代码减少,逻辑一目了然;通用工具函数,逻辑考虑周全,出问题概率低。 2.拆分超大函数当一个函数超过80行后,就属于超大函数,需要进行拆分。 案例1: 每一个代码块都可以封装为一个函数 每一个代码块必然有一个注释,用于解释这个代码块的功能。如果代码块前方有一行注释,就是在提醒你——可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。如果函数有一个描述恰当的名字,就不需要去看内部代码究竟是如何实现的。 现象描述: // 每日生活函数public void liveDaily() { // 吃饭 // 吃饭相关代码几十行 // 编码 // 编码相关代码几十行 // 睡觉 // 睡觉相关代码几十行}建议方案: // 每日生活函数public void liveDaily() { // 吃饭 eat(); // 编码 code(); // 睡觉 sleep();}// 吃饭函数private void eat() { // 吃饭相关代码}// 编码函数private void code() { // 编码相关代码}// 睡觉函数private void sleep() { // 睡觉相关代码}案例2: 每一个循环体都可以封装为一个函数 ...

August 20, 2019 · 11 min · jiezi

Python学习过程中遇到的疑问以及我的脑洞

一 关于局部变量整体变量的理解 以及something about不可变对象 def ChangeInt(a):... a=10...b=2ChangeInt(b)print(b)2这个不可变对象的实例让我产生了疑惑,于是我做了如下: def ChangeInt(a):... a=10...a=2ChangeInt(a)print(a)2这个其实很好理解,因为a=10只是个局部变量,也没有return无法对外部造成影响。哪怕我做如下操作: def ChangeInt(a):... a=10... return a...a=2ChangeInt(a)10print(a)2改变的依旧是局部变量内部的值,与外面的那个a无关。但是当我继续: def ChangeInt(a):... print(a)... a=10... print(a)...a=2ChangeInt(a)210print(a)2可以推测,在函数内部,有一个先把函数外部的a代入函数内部,再根据函数命令改变其值的过程;外部的a依旧不受影响那么回到第一个例子: def ChangeInt(a):... a=10...b=2ChangeInt(b)print(b)2是否也有一个:b=2a=b=2a=10的过程? 为了验证我的猜想。。。 def ChangeInt(a):... print(a)... a=10... print(a)... print(b)...b=2ChangeInt(a)2102print(b)2可见过程中确实存在一个a=b=2的过程,然后10被赋值给了a(我也不知道为啥我要把这事儿整得那么复杂)。 二 关于python function中return,以及list作为可变对象实例的事例 def changeme(mylist):... mylist.append([1,2,3,4])... return...mylist=[1,2]changeme(mylist)mylist[1, 2, [1, 2, 3, 4]]光有return只结束function不返回任何值list作为一个可变对象实例不论结果是否被返回,它的值在函数内外是被统一的,即在函数内修改后函数外部的值也会受到影响。for example: def nano(myint):... myint += 2... return...myint=3nano(myint)myint3another example: def nano(myint):... myint += 2... return myint...myint=3nano(myint)5myint3由上述两例可见,当function中return了一个具体对象时,召唤函数直接返回了这个return的对象的值而以int为例的不可变对象在function中,传递的只是a的值,没有影响a对象本身。在 fun内部修改 a 的值,只是修改了另一个复制的对象。 三 不可变对象存储与可变对象存储空间的差异一个神奇的现象: x=1y=1x is yTrue检查xy存储地址 print(hex(id(x)))0x7fff442d7100print(hex(id(y)))0x7fff442d7100可以发现它们存储地址相同,即这个1被同时赋值给了x和y ...

August 20, 2019 · 1 min · jiezi

js函数闭包了解一下两道题测一下水平

介绍你下你理解的闭包?不管怎样!我最近听到很多次!感觉是不好好总结一下没法面对那些犀利的追问!如果觉得闭包理解的很透彻,就直接跳到最后看题目! 1.闭包概念小红书的解释闭包是有权访问另一个函数作用域中的变量的函数。明白了吗?就是一个函数,一个可以访问其他函数中变量的函数。所以常见的创建闭包的方式就是在一个函数内部创建另一个函数。 function bag(num){ return function(){ return num }}var bagc = bag(12)console.log(bagc()) //12可以看到在bag内部的匿名函数可以访问外部bag函数的变量num。 2.闭包的用处闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。 function f1(){ var n=999; nAdd=function(){n+=1} function f2(){ alert(n); } return f2;}var result=f1();result(); // 999 nAdd(); //变量n被保存了result(); // 1000上面是阮一峰在文档中的一个例子,读取函数内部变量我觉得用处一般吧,让变量保持在内存中的用处倒是不少,像经常使用的that=this等。下面看一个常见的问题: for(var i = 0;i <10;i++){ setTimeout(()=>{ console.log(i) },1000) } //上面的代码我们希望按照索引值打印,结果却打印了10个10,为什么就不解释了,i是全局变量。 //换成下面的写法,就能解决问题,正是因为闭包 让变量的值始终保持在内存中,每个i都存在了num这个局部变量中 for(var i = 0;i <10;i++){ (function(num){ setTimeout(()=>{ console.log(num) },1000) })(i) }3.使用闭包需要注意的点闭包虽然在解决一些问题上有好处,但是由此引发的一些问题要注意,而且由于闭包会携带外部函数作用域,所以内存占用比较大,所以尽量少用、慎用闭包。 1.变量问题正是因为闭包可以使用外部变量,所以下面的代码中,返回的匿名函数中对变量i的使用将会是最终的值,数组中存放的函数的返回值将都会是10。 function test() { var result = []; for(var i = 0; i<10; i++){ result.[i] = function () { return i; } } return result}需要将上述代码改写成如下: ...

August 7, 2019 · 2 min · jiezi

阿里云-EMAS-HTTPDNS-联合函数计算重磅推出-SDNS-服务三大能力获得突破

1. 什么是 HTTPDNS ?传统的 DNS(Domain Name System)使开发者常面临着域名劫持、调度不精准的问题。 HTTPDNS 使用 HTTP 协议替换常用的 UDP 协议,完成客户端和递归 DNS 之间的域名解析过程,使得 HTTPDNS 服务器自身可以充当递归 DNS ,这样域名解析请求直接发送到阿里云的 HTTPDNS 服务器,可以绕过Local DNS运营商 ,避免由于 Local DNS 造成的域名劫持和调度不精准问题。产品原理如下图所示: 2.SDNS 软件定义解析虽然,HTTPDNS 在域名劫持和调度上,相对传统 DNS 已经有了质的飞跃,但是还存在如下不足:(1)【权威 DNS 存在不足】传统权威 DNS 的功能非常有限,且不同供应商提供的能力参差不齐,有优化空间;(2)【无法优化结果】HTTPDNS 充当递归DNS的功能时,无法改变权威 DNS 解析的结果;(3)【缺乏管理】客户域名解析缺乏集中管理,大型客户的域名解析往往分布在多个 DNS 供应商。 而 SDNS 不仅可以帮助开发者进一步优化调度质量,而且可以结合函数计算等能力,扩展 HTTPDNS 的服务边界,提供了用户自定义的处理逻辑,让业务更加方便灵活。 2.1 什么是 SDNS ? SDNS(Software-Defined Name System,简称SDNS)即软件定义解析,是在 HTTPDNS 的基础上,创造性的引入了自定义解析功能。其主要特性有:(1)支持客户端自定义参数输入;(2)HTTPDNS 服务器端结合自定义函数处理能力,支持客户实现复杂的自定义解析功能;(3)返回自定义解析结果给客户端。 2.2 SDNS 的三大能力 SDNS 在功能上完成了三个方面的飞跃: (1)自定义能力 域名解析过程,可以基于自身业务诉求和实际检测数据情况,改进权威解析结果,实现业务最优。(2)服务扩展能力 不再局限于域名解析调度。例如可以结合函数计算等云服务,丰富和扩展自身业务能力。(3)系统调度能力 由于域名解析过程可以定义,可以修改权威的结果,增加额外的数据信息,客户可以在现有服务的基础上,整合各个权威 DNS 服务,规避由于多权威 DNS 不规范所导致的调度质量不够高的问题。2.3 SDNS 使用指南 ...

July 16, 2019 · 2 min · jiezi

Go-函数调用-━-栈和寄存器视角

函数的调用过程主要要点在于借助寄存器和内存帧栈传递参数和返回值。虽然同为编译型语言,Go 相较 C 对寄存器和栈的使用有一些差别,同时,Go 语言自带协程并引入 defer 等语句,在调用过程上显得更加复杂。 理解Go函数调用在CPU指令层的过程有助于编写高效的代码,在性能优化、Bug排查的时候,能更迅速的确定要点。本文以简短的示例代码和对应的汇编代码演示了Go的调用过程,展示了不同数据类型的参数的实际传递过程,同时分析了匿名函数、闭包作为参数或者返回值传递时,在内存上的实际数据结构。对于协程对栈的使用和实现细节,本文不展开。 阅读本文需要掌握计算机体系结构基础知识(至少了解程序内存布局、栈、寄存器)、Go 基础语法。参考文档提供了这些主题更详细的知识。 以下: 术语栈:每个进程/线程/goroutine有自己的调用栈,参数和返回值传递、函数的局部变量存放通常通过栈进行。和数据结构中的栈一样,内存栈也是后进先出,地址是从高地址向低地址生长。栈帧:(stack frame)又常被称为帧(frame)。一个栈是由很多帧构成的,它描述了函数之间的调用关系。每一帧就对应了一次尚未返回的函数调用,帧本身也是以栈的形式存放数据的。caller 调用者callee 被调用者,如在 函数 A 里 调用 函数 B,A 是 caller,B 是 callee寄存器(X86) ESP:栈指针寄存器(extended stack pointer),存放着一个指针,该指针指向栈最上面一个栈帧(即当前执行的函数的栈)的栈顶。注意: ESP指向的是已经存储了内容的内存地址,而不是一个空闲的地址。例如从 0xC0000000 到 0xC00000FF是已经使用的栈空间,ESP指向0xC00000FFEBP:基址指针寄存器(extended base pointer),也叫帧指针,存放着一个指针,该指针指向栈最上面一个栈帧的底部。EIP:寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。注意:16位寄存器没有前缀(SP、BP、IP),32位前缀是E(ESP、EBP、EIP),64位前缀是R(RSP、RBP、RIP) 汇编指令 PUSH:进栈指令,PUSH指令执行时会先将ESP减4,接着将内容写入ESP指向的栈内存。POP :出栈指令,POP指令执行时先将ESP指向的栈内存的一个字长的内容读出,接着将ESP加4。注意: 用PUSH指令和POP指令时只能按字访问栈,不能按字节访问栈。CALL:调用函数指令,将返回地址(call指令的下一条指令)压栈,接着跳转到函数入口。RET:返回指令,将栈顶返回地址弹出到EIP,接着根据EIP继续执行。LEAVE:等价于 mov esp,ebp; pop ebp;MOVL:在内存与寄存器、寄存器与寄存器之间转移值LEAL:用来将一个内存地址直接赋给目的操作数注意:8位指令后缀是B、16位是S、32位是L、64位是Q 调用惯例 调用惯例(calling convention)是指程序里调用函数时关于如何传参如何分配和清理栈等的方案。一个调用惯例的内容包括: 参数是通过寄存器传递还是栈传递或者二者混合通过栈传递时参数是从左至右压栈还是从右至左压栈函数结果是通过寄存器传递还是通过栈传递调用者(caller)还是被调用者(callee)清理栈空间被调用者应该为调用者保存哪些寄存器例如,C 的调用惯例(cdecl, C declaration)是: 函数实参在线程栈上按照从右至左的顺序依次压栈。函数结果保存在寄存器EAX/AX/AL中浮点型结果存放在寄存器ST0中编译后的函数名前缀以一个下划线字符调用者负责从线程栈中弹出实参(即清栈)8比特或者16比特长的整形实参提升为32比特长。受到函数调用影响的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS不受函数调用影响的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DSRET指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器) cdecl 将函数返回值保存在寄存器中,所以 C 语言不支持多个返回值。另外,cdecl 是调用者负责清栈,因而可以实现可变参数的函数。如果是被调用者负责清理的话,无法实现可变参数的函数,但是编译代码的效率会高一点,因为清理栈的代码不用在每次调用的时候(编译器计算)生成一遍。(x86的ret指令允许一个可选的16位参数说明栈字节数,用来在返回给调用者之前解堆栈。代码类似ret 12这样,如果遇到这样的汇编代码,说明是被调用者清栈。) ...

July 14, 2019 · 14 min · jiezi

QPS-提升60揭秘阿里巴巴轻量级开源-Web-服务器-Tengine-负载均衡算法

前言在阿里七层流量入口接入层(Application Gateway)场景下, Nginx 官方的Smooth Weighted Round-Robin( SWRR )负载均衡算法已经无法再完美施展它的技能。 Tengine 通过实现新的负载均衡算法Virtual Node Smooth Weighted Round-Robin(VNSWRR )不仅优雅的解决了 SWRR 算法的缺陷,而且QPS处理能力相对于 Nginx 官方的 SWRR 算法提升了60%左右。 问题接入层 Tengine 通过自研的动态 upstream 模块实现动态服务发现,即运行时动态感知后端应用机器扩缩容、权重调整和健康检查等信息。同时该功能可以做很多事情,比如用户可通过调整后端应用某台机器的权重从而达到线上真实引流压测目的。然而,这些操作在 Nginx 原生 SWRR 算法下却可能引起不可逆转的血案。 在接入层(Application Gateway)场景下, Nginx 的负载均衡算法 SWRR 会导致权重被调高机器的QPS瞬间暴涨,如上图App2-host-A机器当权重调整为2时,某一时刻流量会集中转发到该机器;Nginx 的 SWRR 算法的处理时间复杂度是O(N),在大规模后端场景下 Nginx 的处理能力将线性下降;综上所述,对接入层 Tengine 的负载均衡转发策略的改造及性能优化已迫在眉睫。 原生 SWRR 算法分析在介绍案列之前,我们先简单介绍下 Nginx 的负载均衡算法 SWRR 转发策略及特点: SWRR 算法全称是Smooth Weighted Round-Robin Balancing,顾名思义该算法相比于其它加权轮询(WRR)算法多一个smooth(平滑)的特性。 下面我们就一个简单的列子来描述下该算法: 假设有3台机器A、B、C权重分别为5、1、1,其中数组s代表机器列表、n代表机器数量,每个机器的cw初始化为0、ew初始化为机器权重、tw代表本轮选择中所有机器的ew之和、best表示本轮被选中的机器。简单的描述就是每次选择机器列表中cw值最大的机器,被选中机器的cw将会减去tw,从而降低下次被选中的机会,简单的伪代码描述如下: best = NULL;tw = 0;for(i = random() % n; i != i || falg; i = (i + 1) % n) {flag = 0;s[i].cw += s[i].ew;tw += s[i].ew;if (best == NULL || s[i].cw > best->cw) { best = &s[i];}}best->cw -= tw;return best;请求编号 选择前的权重值 被选中的server 选择后的权重值0 {5,1,1} A {-2,1,1}1 {3,2,2} A {-4,2,2}2 {1,3,3} B {1,-4,3}3 {6,-3,4} A {-1,-3,4}4 {4,-2,5} C {4,-2,-2}5 {9,-1,-1} A {2,-1,-1}6 {7,0,0} A {0,0,0} ...

July 12, 2019 · 2 min · jiezi

Aliyun-Serverless-VSCode-Extension-上架并开源

Aliyun Serverless VSCode ExtensionAliyun Serverless VSCode Extension 是阿里云 Serverless 产品 函数计算 Function Compute 的 VSCode 插件,该插件是结合了函数计算 Fun 工具以及函数计算 SDK ,为用户提供 VSCode 图形化开发调试函数计算以及操作函数计算资源的工具。 通过该插件,您可以: 快速地在本地初始化项目、创建函数运行、调试本地函数(调试功能目前支持 nodejs、python、php)拉取云端的服务函数列表,执行云端函数部署服务函数至云端,并更新相关配置前置需求如果您期望使用 Aliyun Serverless VSCode Extension 的所有功能,那么您需要确保系统中有以下组件: VSCode:在 Visual Studio Code 官网 中可以下载安装函数计算 Fun 工具以及 Docker:可以在 aliyun/fun 中根据教程安装配置 Fun 以及 Docker安装插件打开 VSCode 并进入插件市场。在插件市场中搜索 “Aliyun Serverless”,查看详情并安装。重启 VSCode,左侧边栏中会展示已安装的 Aliyun Serverless VSCode Extension 插件。快速入门绑定阿里云账户打开左侧 Aliyun Serverless VSCode Extension,单击绑定阿里云账户的按钮。 依次输入阿里云 Account ID,阿里云 Access Key ID,阿里云 Access Key Secret。 绑定完成后,可以看到所绑定的阿里云账户的云端服务与函数列表。 您可以通过切换区域 Region 来查看不同区域的服务与函数。单击云端资源面板的切换区域按钮或 VSCode 下方的区域信息。 ...

July 12, 2019 · 1 min · jiezi

UI2CODE复杂背景无法识别闲鱼工程师这样打造高准确率方案

复杂背景内容提取指的是从复杂的背景中提取出特定的内容,例如在图片中提取特定的文字,在图片中提取特定的叠加图层等等。这是一个业界难题,基于传统的图像处理的方法存在准确率和召回率的问题,没法解决语义的问题。而主流的机器学习的方法,例如目标检测无法获取像素级别的位置信息,而语义分割的方法则只能提取像素而无法获取半透明叠加前的像素信息。本文考虑到这些痛点,从UI2CODE业务的业务场景出发,采用了目标检测网络来实现内容召回,GAN网络实现复杂背景中特定前景内容的提取和复原。 处理流程:复杂背景的处理流程分为如下几个步骤: 内容召回:通过目标检测网络召回元素,即元素是否需要做背景提取操作。区域判断:根据梯度等视觉方法判断所处区域是否是复杂区域。简单区域:基于梯度的方式找到背景区块。复杂区域:采用SRGAN网络进行内容提取。内容召回:内容召回我们采用目标检测网络来实现,例如Faster-rcnn或者Mask-rcnn等,如下图所示: 区域判断:根据拉普拉斯算子计算周边梯度,判断所处区域是否是复杂区域。 简单背景:由于目标检测模型本身的局限性,会导致没法达到像素级别的精确性,因此需要对位置做修正。如果是简单背景就可以基于梯度的思想做位置修正,具体计算方式如下: 复杂背景:背景是复杂背景时,左图是原图,右图是提取的文字区块: 此时提取出的框不是完全正确,那么此时根据梯度等机器视觉算法已经不能对位置做正确的修正了。本文提出了基于GAN网络的方式来解决复杂背景内容提取问题,网络的主要结构如下图所示: 为什么选择GAN网络?1)基于srGAN网络,该网络加入了特征图的损失函数,这样可以很好保留高频信息,能更好的保留边缘。特征图的损失函数如下图所示: 2)由于有对抗损失的存在,可以很好的降低误检率。 3)最重要的一点是在有透明度的场景下,语义分割网络只能“提取”元素,无法“还原”元素。而GAN网络不仅可以在提取元素的同时还原出未叠加时的像素情况。 网络训练流程图 针对业务场景对GAN网络做的改进1.由于我们不是超分辨率场景,因此不用pixelShuffler模块做上采样 2.由于场景比较复杂,可以引入denseNet和加深网络来提高准确率。 3.内容损失函数对于压制误判的噪点效果不理想,因此加大了误判的惩罚,具体如下图所示: 预测获取的结果图I: 预测获取的结果图II: 结束语本篇我们通过复杂背景内容提取的介绍,提出了一种机器学习为主,图像处理为辅去精确获取特定前景内容的方法,得到了高精确率、高召回率和高定位精度的识别结果。下图分别是传统算法grabcut,语义分割方法deeplab和本文方法的各个指标的情况。 经过数据论证,我们发现了一个值得进一步优化的点——需要大量样本适配不同的特征尺度,这里的投入会相对较大。如何进一步提高打标效率呢,我们将会在后续系列文章中和大家分享。 本文作者:仝辉,深宇阅读原文 本文为云栖社区原创内容,未经允许不得转载。

July 10, 2019 · 1 min · jiezi

js深入二函数的执行与上下文

这一篇简单的说一说js的函数执行和js的执行上下文的概念,之前在我的博客里边也提到过js的堆栈队列,这一篇打算单独的拿出来说一说 是什么是js的执行上下文一段可以执行的代码在被执行的时候,会创建一个函数的执行上下文执行上下文里边有三个重要的属性分别是 变量对象作用域链this指向执行上下文是跟函数相关的,执行上下文分为两个阶段 创建阶段执行阶段首先创建阶段 扫描变量和函数(确定变量环境)确定this指向确定词法环境简单说一下词法环境和变量环境的区别,我个人理解的就是说词法环境是包含变量环境的 在js里边原型链大家都不陌生 ,js在当前的对象里边找不到所使用的属性的话会去他的上一级去找直到Object,再找不到就会undefined ,这里边 当前对象的作用域就是他的变量环境,而词法环境则是与之关联的的执行上下文中声明的变量 在创建阶段 函数的声明会被储存在当前的变量环境之中,var的变量的话则会被设置成undefined,所以我们在声明之前就可以访问到var声明的变量 ,but他是一个undfined 然后就是执行阶段了 这个时候已经完成了对所有变量的分配,开始执行代码 变量对象什么是变量对象,变量对象就是与执行上下文相关得数据得一个作用域,这里边存储了在这个上下文里边定义的而变量和函数声明 函数声明的时候会在创建阶段的时候声明一个函数指针全局上下文和函数上下文全局上下文故名思与函数上下文,在函数被执行的时候创建,那么全局上下文是什么时候被创建的呢,首先我们要知道一个概念就是,全局对象 在一般的函数里边全局对象可以用this引用其属性,客户端的话是window对象console.log(this); 可以尝试着去打印一下this,如下图可以看到,window对象里边有很多我们常用或者常见的属性和方法 当然也有一些其他的情况this并不是指向的window对象,this指向 这个东西,我们会在下边说 当然js 万物皆对象不只是说说用 instanceof 输出一下this 会发现也是一个object console.log(this instanceof Object);所以说全局对象是一个object的实例化对象 所以说说白了全局上下文就是在一开始就被创建的,全局上下文是由在浏览器关闭或者刷新的时候才会销毁, 然后这个window对象其实就是全局上下文的变量对象 函数上下文函数上下文和上边所说的意思差不多,但是,啊,但是,在函数上下文中的时候,函数里边的变量对象是活动对象,他的英文名字叫做 actiation object ,大面上看的意思就是说只有进入了这个执行上下文中的时候,也就是说执行到了这个函数的时候,其中的变量对象才会被激活,才会能被访问,在没有执行到这个函数的时候,其属性是不能访问的,这里是和全局上下文不同的。 函数执行过程function foo(a) { var b = 2; function c() {} var d = function() {}; b = 3;}foo(1);首先看这段代码,上边也说了,函数执行有两个阶段创建阶段和执行阶段 首先进如执行上下文,进入创建阶段 首先形参,也就是调用的方法的时候传进来的值,如果你传了就是有值得,如果没传就是一个undefined,相应得函数里边会有一个arguments对象,存放着形参得值,然后函数里边声明得变量,在创建阶段得时候是没有值得,会被赋值一个undefined。函数声明会由名称和对应值组成一个变量对象的属性被创建,也就是上边说的函数得指针,我是这么理解得,因为这个变量指向了这个函数,如果变量对象已经存在相同名称的属性,则完全替换这个属性上边这段代码在创建阶段是这样得 { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined}然后函数的而执行阶段,这个时候就该干嘛干嘛了,函数里边声明的变量,赋值等等的会在这里根据代码,修改变量的值 ...

July 9, 2019 · 1 min · jiezi

小谈CDN回源函数计算的应用场景

CDN团队联合函数计算团队近期推出了一个全新功能,即通过CDN把回源流量指向函数计算进行处理,该功能旨在帮助CDN用户能通过函数计算快速处理和便捷处理回源数据为目的,用户仅仅需要在CDN回源地址填写函数计算的自定义域名即可把请求转发到函数计算进行处理,配置简单,费用低廉,先前CDN回源可以设置几个目的地,例如回源到IP地址、域名或者对象存储OSS,现在新增了一个回源到函数计算的类型,这种类型不同于其他类型,例如CDN回源到ECS某IP上后,ECS需要启一套服务来监听CDN请求,对系统设计要求较高,再比如CDN回源到OSS上,多半只能是一些静态文件,当用户对回源内容进行个性化处理的时候,以上的方式都不够灵活。新增CDN回源函数架构图: 业务流向图可以概述如下: 主打功能:CDN回源数据动态处理CDN请求地址处理拉通CDN请求和多类数据处理对请求地址进行鉴权和跳转场景优势请求链路缩短(减少负载均衡)请求后的弹性扩容对请求地址进行鉴权和跳转技术特点简单,仅需控制台操作配置内置负载均衡和计算弹性扩容,能支持海量并发多种主流语言支持,java,Nodejs,Python,PHP,C#等阿里云集群级别的安全访问控制台上的操作CDN控制台配置: 函数计算控制台配置: 适用场景网站场景用于CDN源站的静态、动态网站页面元数据处理文件处理场景用于CDN回源源站多媒体数据处理,例如文本、图片、视频、音频等。请求分发场景可以通过函数计算把请求做URL地址动态处理后并把请求302分发到其他应用以上各种场景推出,对用户来说有几个显而易见的好处,例如1、节约流量成本,客户通过CDN回源的函数计算走专有网域,会比走公网流量价格更优惠(价格优惠后续公布),2、客户可以很方便的把很多产品串联起来使用,例如通过函数计算能联合多个产品给CDN后的请求提供数据处理,例如新浪微博图片处理(FC+OSS),例如芒果TV的热数据加载等,3、可以做数据处理,也可以做请求分发渠道。相比较ECS,这种方案具备自动弹性伸缩特性,CDN +函数计算 = CDN + 负载均衡+计算,能通过非常优雅的弹性方式来支撑大量CDN请求。4、轻量数据处理程序便捷,5分钟编写一个函数可以灵活处理请求数据,无需维护运行环境,总之是一个很赞的功能,不妨来试用一下~ 本文作者:文意 阅读原文 本文为云栖社区原创内容,未经允许不得转载。

July 5, 2019 · 1 min · jiezi

Teradata应用迁移到AnalyticDB-for-PostgreSQL指导

AnalyticDB for PostgreSQL(简称:ADB for PG)对Teradata语法有着很好的兼容,将Teradata应用迁移到ADB for PG,只需进行有限的修改。本文介绍将Teradata应用迁移到ADB for PG应该注意的事项。 1 建表语句我们通过一个例子比较ADB for PG和Teradata的建表语句。对于如下的Teradata建表SQL语句, CREATE MULTISET TABLE test_table,NO FALLBACK , NO BEFORE JOURNAL, NO AFTER JOURNAL, CHECKSUM = DEFAULT, DEFAULT MERGEBLOCKRATIO ( first_column DATE FORMAT 'YYYYMMDD' TITLE '第一列' NOT NULL, second_column INTEGER TITLE '第二列' NOT NULL , third_column CHAR(6) CHARACTER SET LATIN CASESPECIFIC TITLE '第三列' NOT NULL , fourth_column CHAR(20) CHARACTER SET LATIN CASESPECIFIC TITLE '第四列' NOT NULL, fifth_column CHAR(1) CHARACTER SET LATIN CASESPECIFIC TITLE '第五列' NOT NULL, sixth_column CHAR(24) CHARACTER SET LATIN CASESPECIFIC TITLE '第六列' NOT NULL, seventh_column VARCHAR(18) CHARACTER SET LATIN CASESPECIFIC TITLE '第七列' NOT NULL, eighth_column DECIMAL(18,0) TITLE '第八列' NOT NULL , nineth_column DECIMAL(18,6) TITLE '第九列' NOT NULL )PRIMARY INDEX ( first_column ,fourth_column )PARTITION BY RANGE_N(first_column BETWEEN DATE '1999-01-01' AND DATE '2050-12-31' EACH INTERVAL '1' DAY );CREATE INDEX test_index (first_column, fourth_column) ON test_table;可以修改成ADB for PG的建表语句: ...

July 2, 2019 · 2 min · jiezi

MaxCompute-费用暴涨之新增SQL分区裁剪失败

现象:因业务需求新增了SQL任务,这SQL扫描的表为分区表,且SQL条件里表只指定了一个分区,按指定的分区来看数据量并不大,但是SQL的费用非常高。费用比预想的结果相差几倍甚至10倍以上。 若只知道总体费用暴涨,但是没明确是什么任务暴涨,可以可以参考查看账单详情-使用记录文档,找出费用异常的记录。分析:我们先明确MaxCompute SQL后付费的计费公式:一条SQL执行的费用=扫描输入量 ️ SQL复杂度 ️ 0.3(¥/GB)。 变量主要是输入量和复杂度,但实际上复杂度最高也就为4,由复杂度引起的费用暴涨是比较罕见,我们不妨先把排查重点放在输入量上。 排查:查看Logview的inputs信息 如上图会发现input的分区量是14个,这个与预想的(SQL条件中只指定一个分区)不一致。问题就出在这里,此时基本可以判断这个SQL的分区并没有裁剪好,也就是说最终输入量不是一个分区而是多个或者全表。 输入的分区量和预计的不一致,排除SQL中确实没有对分区设置条件这因素,那么就是分区裁剪失效了。已知的分区裁剪失效场景主要有:分区条件用了自定义函数进行裁剪;在 Join 关联时的 Where 条件中也有可能会失效。 执行explain sql语句;看执行结果,读取的分区都有哪些,如执行explain select seller_id from xxxxx_trd_slr_ord_1d where ds=rand(); 结果如下: 看上图中红框的内容,表示读取了表 xxxxx_trd_slr_ord_1d 的 1344 个分区,即该表的所有分区,如果直接执行这个sql,最终会因为全表扫描导致输入量增加从而费用增加。 关于分区裁剪失败场景(使用函数或者跟join关联有关的场景)分析可以参考文档《分区剪裁合理性评估》。大家在执行sql前如果对分区的裁剪有疑虑,不放执行一次explain sql语句;再执行SQL语句。 关于分区条件用自定义函数或者内置函数导致分区裁剪失效的解决方案: 内置函数目前已经都支持进行分区裁剪。自定义函数需要支持分区裁剪有两种方式: 在编写UDF的时候,UDF类上加入Annotation。@com.aliyun.odps.udf.annotation.UdfProperty(isDeterministic=true)> 注意: com.aliyun.odps.udf.annotation.UdfProperty定义在odps-sdk-udf.jar文件中。您需要把引用的odps-sdk-udf版本提高到0.30.x或以上。* 在SQL语句前设置Flag:`set odps.sql.udf.ppr.deterministic = true;`,此时SQL中所有的UDF均被视为deterministic。该操作执行的原理是做执行结果回填,但是结果回填存在限制,即最多回填1000个Partition。 因此,如果UDF类加入Annotation,则可能会导致出现超过1000个回填结果的报错。此时您如果需要忽视此错误,可以通过设置Flag:`set odps.sql.udf.ppr.to.subquery = false;`全局关闭此功能。关闭后,UDF分区裁剪也会失效。 本文作者:海清阅读原文 本文为云栖社区原创内容,未经允许不得转载。

June 27, 2019 · 1 min · jiezi

Flink-零基础实战教程如何计算实时热门商品

在上一篇入门教程中,我们已经能够快速构建一个基础的 Flink 程序了。本文会一步步地带领你实现一个更复杂的 Flink 应用程序:实时热门商品。在开始本文前我们建议你先实践一遍上篇文章,因为本文会沿用上文的my-flink-project项目框架。 通过本文你将学到: 如何基于 EventTime 处理,如何指定 Watermark如何使用 Flink 灵活的 Window API何时需要用到 State,以及如何使用如何使用 ProcessFunction 实现 TopN 功能实战案例介绍“实时热门商品”的需求,我们可以将“实时热门商品”翻译成程序员更好理解的需求:每隔5分钟输出最近一小时内点击量最多的前 N 个商品。将这个需求进行分解我们大概要做这么几件事情: 抽取出业务时间戳,告诉 Flink 框架基于业务时间做窗口过滤出点击行为数据按一小时的窗口大小,每5分钟统计一次,做滑动窗口聚合(Sliding Window)按每个窗口聚合,输出每个窗口中点击量前N名的商品数据准备这里我们准备了一份淘宝用户行为数据集(来自阿里云天池公开数据集,特别感谢)。本数据集包含了淘宝上某一天随机一百万用户的所有行为(包括点击、购买、加购、收藏)。数据集的组织形式和MovieLens-20M类似,即数据集的每一行表示一条用户行为,由用户ID、商品ID、商品类目ID、行为类型和时间戳组成,并以逗号分隔。关于数据集中每一列的详细描述如下: 列名称说明用户ID整数类型,加密后的用户ID商品ID整数类型,加密后的商品ID商品类目ID整数类型,加密后的商品所属类目ID行为类型字符串,枚举类型,包括(‘pv’, ‘buy’, ‘cart’, ‘fav’)时间戳行为发生的时间戳,单位秒你可以通过下面的命令下载数据集到项目的 resources 目录下: $ cd my-flink-project/src/main/resources$ curl https://raw.githubusercontent.com/wuchong/my-flink-project/master/src/main/resources/UserBehavior.csv > UserBehavior.csv这里是否使用 curl 命令下载数据并不重要,你也可以使用 wget 命令或者直接访问链接下载数据。关键是,将数据文件保存到项目的 resources 目录下,方便应用程序访问。 编写程序在 src/main/java/myflink 下创建 HotItems.java 文件: package myflink;public class HotItems { public static void main(String[] args) throws Exception { }}与上文一样,我们会一步步往里面填充代码。第一步仍然是创建一个 StreamExecutionEnvironment,我们把它添加到 main 函数中。 ...

June 21, 2019 · 4 min · jiezi

DLA-SQL技巧行列转换和JSON数据列展开

1. 简介在数据库SQL处理中,常常有行转列(Pivot)和列转行(Unpivot)的数据处理需求。本文以示例说明在Data Lake Analytics(https://www.aliyun.com/product/datalakeanalytics)中,如何使用SQL的一些技巧,达到行转列(Pivot)和列转行(Unpivot)的目的。另外,DLA支持函数式表达式的处理逻辑、丰富的JSON数据处理函数和UNNEST的SQL语法,结合这些功能,能够实现非常丰富、强大的SQL数据处理语义和能力,本文也以JSON数据列展开为示例,说明在DLA中使用这种SQL的技巧。 2. 行转列(Pivot)2.1 样例数据 test_pivot表内容: +------+----------+---------+--------+| id | username | subject | source |+------+----------+---------+--------+| 1 | 张三 | 语文 | 60 || 2 | 李四 | 数学 | 70 || 3 | 王五 | 英语 | 80 || 4 | 王五 | 数学 | 75 || 5 | 王五 | 语文 | 57 || 6 | 李四 | 语文 | 80 || 7 | 张三 | 英语 | 100 |+------+----------+---------+--------+2.2 方法一:通过CASE WHEN语句 ...

June 20, 2019 · 4 min · jiezi

箭头函数的写法特点

1、箭头函数简介用 => 来标识箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或 new.target。这些函数表达式更适用于那些本来需要匿名函数的地方,并且它们不能用作构造函数。2、通常函数的定义方法var fn1 = function(a, b){ console.log(a + b);};fn1(1, 2); // 3function fn2(a, b){ console.log(a - b);}fn2(2, 1);//13、简写写法对应上面两个//删掉了functionvar fn11 = (a, b)=>{ console.log(a+b);};fn11(1, 2); // 3//删掉了function和函数名,无意义(a,b)=>{ console.log(a-b)}4、基础语法附加规则 当函数参数只有一个时,可省略小括号,但没有时,不能省略。函数体(中括号)中有且只有一行return语句时,中括号和return关键字可以省略。函数返回json对象,且只有一行return语句时,返回的简写要加小括号;如let add = a =>({"a":2}) (参数1, 参数2, …, 参数N) => { 函数声明 } //相当于:(参数1, 参数2, …, 参数N) =>{ return 表达式; } (参数1, 参数2, …, 参数N) => 表达式(单一) // 当只有一个参数时,圆括号是可选的: (单一参数) => {函数声明} 单一参数 => {函数声明} // 没有参数的函数应该写成一对圆括号。 () => {函数声明} var add = function(a,b){ return a+b; }; // 即: var add = (a,b)=>{ return a+b }; // 即: var add = (a,b)=>a+b; --------------------------------------------------------------------------------------- var ret = function(a){ return a+1; }; // 即: var ret = a=>a+1; --------------------------------------------------------------------------------------- var non = function(){ return 2+1; }; // 即 var non = ()=>2+1; 函数体代码多于一行 let fun1 = function(){ console.log('1'); console.log('2'); return 1+2; } fun1(); // 简写为 let fun2 = ()=>{ console.log('1'); console.log('2'); return 1+2 } fun2(); 函数返回json对象时 let f1 = function(){ return {"a":2}; } let f2 = ()=>{"a":2} // 错误 let f2 = ()=>({"a":2}) 如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错: x => { foo: x } 因为和函数体的{ ... }有语法冲突,所以要改为: // ok: x => ({ foo: x }) 实例 let arr1 = [9,6,1,7]; let arr11 = arr1.sort( function(a,b){ return a-b; } ) console.log(arr11); let arr3= [2,3,9,5]; let arr33 = arr3.sort((a,b)=>a-b) console.log(arr33)5、总结箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。箭头函数写代码拥有更加简洁的语法。不会绑定this,或者说箭头函数中 不会改变this本来的绑定。

June 19, 2019 · 1 min · jiezi

十分钟上线-函数计算构建支付宝小程序的后端

阿里云函数计算服务(FunctionCompute,FC)是一个事件驱动的全托管计算服务。通过函数计算与云端各个服务的广泛集成,开发者只需要编写函数代码,就能够快速地开发出弹性高可用的后端系统。接下来我们使用FC,来快速实现一个图片转换服务, 并把这个图片转换服务作为支付宝小程序的后端。 支付宝小程序demo前端效果图: 资源下载及准备工作示例代码附件 【必须】 支付宝小程序开发工具下载 【非必须】 函数计算FC 快捷入口对象存储OSS 快捷入口日志服务Log Service 快捷入口 简明架构图 函数入口普通函数入口 def my_handler(event, context): return 'hello world'函数名my_handler需要与创建函数时的"Handler"字段相对应:例如创建函数时指定的 Handler 为main.my_handler,那么函数计算会去加载main.py中定义的my_handler函数 event 参数event 参数是用户调用函数时传入的数据,其类型是str context 参数context 参数中包含一些函数的运行时信息(例如 request id/临时 AK 等)。其类型是FCContext,具体结构和使用在下面的使用 context介绍 返回值函数的返回值会作为调用函数的结果返回给用户,它可以是任意类型:对于简单类型会函数计算会把它转换成 str 返回,对于复杂类型会把它转换成 JSON 字符串返回 HTTP 触发器的函数入口 HELLO_WORLD = b"Hello world!\n"def handler(environ, start_response): context = environ['fc.context'] status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [HELLO_WORLD]environ : environ 参数是一个 python 字典,里面存放了所有和客户端相关的信息,具体详情参考 environ 参数,函数计算增加了两个自定义的 key,分别是 fc.context 和 fc.request_uri ...

June 19, 2019 · 2 min · jiezi

说说JavaScript中函数的防抖-Debounce-与节流-Throttle

为何要防抖和节流有时候会在项目开发中频繁地触发一些事件,如 resize、 scroll、 keyup、 keydown等,或者诸如输入框的实时搜索功能,我们知道如果事件处理函数无限制调用,会大大加重浏览器的工作量,有可能导致页面卡顿影响体验;后台接口的频繁调用,不仅会影响客户端体验,还会大大增加服务器的负担。而如果对这些调用函数增加一个限制,让其减少调用频率,岂不美哉? 针对这个问题,一般有两个方案:防抖 (Debounce) 节流 (Throttle) 防抖(Debounce)我对函数防抖的定义:当函数被连续调用时,该函数并不执行,只有当其全部停止调用超过一定时间后才执行1次。 一个被经常提起的例子: 上电梯的时候,大家陆陆续续进来,电梯的门不会关上,只有当一段时间都没有人上来,电梯才会关门。Talk is cheap,我们直接 show code 吧。 先做基本的准备(篇幅原因,HTML部分省略): let container = document.getElementById('container');// 事件处理函数function handle(e) { onsole.log(Math.random()); }// 添加滚动事件container.addEventListener('scroll', handle);我们发现,每滚动一下,控制台就会打印出一行随机数。 基础防抖我们现在写一个最基础的防抖处理: function debounce(func, wait) { var timeout;//标记 return function() { clearTimeout(timeout); timeout = setTimeout(func, wait); }}事件也做如下改写: container.addEventListener('scroll', debounce(handle, 1000));现在试一下, 我们会发现只有我们停止滚动1秒钟的时候,控制台才会打印出一行随机数。 标准防抖以上基础版本会有两个问题,请看如下代码: // 处理函数function handle(e) { console.log(this); //输出Window对象 console.log(e); //undefined}没错,当我们不使用防抖处理时,handle()函数的this指向调用此函数的container,而在外层使用防抖处理后,this的指向会变成Window。其次,我们也要获取到事件对象event。 所以我们要对防抖函数做以下改写: function debounce(fn, wait) { let timeout; return function() { clearTimeout(timeout); timeout = setTimeout(()=>{ fn.apply(this,arguments)//使用apply改变this指向 }, wait); }}当然了,如果使用箭头函数便可以省去外层声明。 ...

June 17, 2019 · 1 min · jiezi

Oracle应用迁移到AnalyticDB-for-PostgreSQL指导

AnalyticDB for PostgreSQL(简称:ADB for PG)对Oracle语法有着较好的兼容,本文介绍如何将Oracle应用迁移到AnalyticDB for PostgreSQL。 1 PL/SQLPL/SQL(Procedural Language/SQL)是一种过程化的SQL语言,是Oracle对SQL语句的拓展,使得SQL的使用可以具有一般编程语言的特点,因此,可以用来实现复杂的业务逻辑。PL/SQL对应了ADB for PG中的PL/PGSQL 1.1PackageADB for PG的plpgsql不支持package,需要把package 转换成 schema,并package里面的所有procedure和 function转换成ADB for PG的function。例如: create or replace package pkg is …end;可以转换成: create schema pkg;Package定义的变量 procedure/function的局部变量保持不变,全局变量在ADB for PG中可以使用临时表进行保存。详见1.4.5节。Package初始化块 如果可以删掉,就删掉,删不掉的话,可以使用function封装,在需要的时候主动调用该function。Package 内定义的procedure/function Package 内定义的procedure和function 转成adb for pg的function,并把function 定义到package对应的schema内。例如,有一个Package名为pkg中有如下函数:FUNCTION test_func (args int) RETURN int is var number := 10;BEGIN… … END;转换成如下ADB for PG的function:CREATE OR REPLACE FUNCTION pkg. test_func(args int) RETURNS int AS $$ … … $$ LANGUAGE plpgsql;1.2 Procedure/function对于oracle的procedure和function,不论是package的还是全局的,都转换成adb for pg 的function。例如: ...

June 17, 2019 · 6 min · jiezi

PyODPS-DataFrame-处理笛卡尔积的几种方式

PyODPS 提供了 DataFrame API 来用类似 pandas 的接口进行大规模数据分析以及预处理,本文主要介绍如何使用 PyODPS 执行笛卡尔积的操作。 笛卡尔积最常出现的场景是两两之间需要比较或者运算。以计算地理位置距离为例,假设大表 Coordinates1 存储目标点经纬度坐标,共有 M 行数据,小表 Coordinates2 存储出发点经纬度坐标,共有 N 行数据,现在需要计算所有离目标点最近的出发点坐标。对于一个目标点来说,我们需要计算所有的出发点到目标点的距离,然后找到最小距离,所以整个中间过程需要产生 M * N 条数据,也就是一个笛卡尔积问题。 haversine 公式首先简单介绍一下背景知识,已知两个地理位置的坐标点的经纬度,求解两点之间的距离可以使用 haversine 公式,使用 Python 的表达如下: def haversine(lat1, lon1, lat2, lon2): # lat1, lon1 为位置 1 的经纬度坐标 # lat2, lon2 为位置 2 的经纬度坐标 import numpy as np dlon = np.radians(lon2 - lon1) dlat = np.radians(lat2 - lat1) a = np.sin( dlat /2 ) **2 + np.cos(np.radians(lat1)) * np.cos(np.radians(lat2)) * np.sin( dlon /2 ) **2 c = 2 * np.arcsin(np.sqrt(a)) r = 6371 # 地球平均半径,单位为公里 return c * rMapJoin目前最推荐的方法就是使用 mapjoin,PyODPS 中使用 mapjoin 的方式十分简单,只需要两个 dataframe join 时指定 mapjoin=True,执行时会对右表做 mapjoin 操作。 ...

June 13, 2019 · 3 min · jiezi

一文读懂架构整洁之道附知识脉络图

程序的世界飞速发展,今天所掌握的技能可能明年就过时了,但有一些东西是历久弥新,永远不变的,掌握了这些,在程序的海洋里就不会迷路,架构思想就是这样一种东西。 本文是《架构整洁之道》的读书笔记,文章从软件系统的价值出发,认识架构工作的价值和目标, 依次了解架构设计的基础、指导思想(设计原则)、组件拆分的方法和粒度、组件之间依赖设计、组件边界多种解耦方式以及取舍、降低组件之间通信成本的方法,从而在做出正确的架构决策和架构设计方面,给出作者自己的解读。 阿里巴巴中间件微信公众号对话框,发送“架构”,可获取《架构整洁之道》知识脉络图。直接访问,点击这里。 一、软件系统的价值架构是软件系统的一部分,所以要明白架构的价值,首先要明确软件系统的价值。软件系统的价值有两方面,行为价值和架构价值。 行为价值是软件的核心价值,包括需求的实现,以及可用性保障(功能性 bug 、性能、稳定性)。这几乎占据了我们90%的工作内容,支撑业务先赢是我们工程师的首要责任。如果业务是明确的、稳定的,架构的价值就可以忽略不计,但业务通常是不明确的、飞速发展的,这时架构就无比重要,因为架构的价值就是让我们的软件(Software)更软(Soft)。可以从两方面理解: 当需求变更时,所需的软件变更必须简单方便。变更实施的难度应该和变更的范畴(scope)成等比,而与变更的具体形状(shape)无关。当我们只关注行为价值,不关注架构价值时,会发生什么事情?这是书中记录的一个真实案例,随着版本迭代,工程师团队的规模持续增长,但总代码行数却趋于稳定,相对应的,每行代码的变更成本升高、工程师的生产效率降低。从老板的视角,就是公司的成本增长迅猛,如果营收跟不上就要开始赔钱啦。 可见架构价值重要性,接下来从著名的紧急重要矩阵出发,看我们如何处理好行为价值和架构价值的关系。 重要紧急矩阵中,做事的顺序是这样的:1.重要且紧急 > 2.重要不紧急 > 3.不重要但紧急 > 4.不重要且不紧急。实现行为价值的需求通常是 PD 提出的,都比较紧急,但并不总是特别重要;架构价值的工作内容,通常是开发同学提出的,都很重要但基本不是很紧急,短期内不做也死不了。所以行为价值的事情落在1和3(重要且紧急、不重要但紧急),而架构价值落在2(重要不紧急)。我们开发同学,在低头敲代码之前,一定要把杂糅在一起的1和3分开,把我们架构工作插进去。 二、架构工作的目标前面讲解了架构价值,追求架构价值就是架构工作的目标,说白了,就是用最少的人力成本满足构建和维护该系统的需求,再细致一些,就是支撑软件系统的全生命周期,让系统便于理解、易于修改、方便维护、轻松部署。对于生命周期里的每个环节,优秀的架构都有不同的追求: 开发阶段:组件不要使用大量复杂的脚手架;不同团队负责不同的组件,避免不必要的协作。部署阶段:部署工作不要依赖成堆的脚本和配置文件;组件越多部署工作越繁重,而部署工作本身是没有价值的,做的越少越好,所以要减少组件数量。运行阶段:架构设计要考虑到不同的吞吐量、不同的响应时长要求;架构应起到揭示系统运行的作用:用例、功能、行为设置应该都是对开发者可见的一级实体,以类、函数或模块的形式占据明显位置,命名能清晰地描述对应的功能。维护阶段:减少探秘成本和风险。探秘成本是对现有软件系统的挖掘工作,确定新功能或修复问题的最佳位置和方式。风险是做改动时,可能衍生出新的问题。三、编程范式其实所谓架构就是限制,限制源码放在哪里、限制依赖、限制通信的方式,但这些限制比较上层。编程范式是最基础的限制,它限制我们的控制流和数据流:结构化编程限制了控制权的直接转移,面向对象编程限制了控制权的间接转移,函数式编程限制了赋值,相信你看到这里一定一脸懵逼,啥叫控制权的直接转移,啥叫控制权的间接转移,不要着急,后边详细讲解。 这三个编程范式最近的一个也有半个世纪的历史了,半个世纪以来没有提出新的编程范式,以后可能也不会了。因为编程范式的意义在于限制,限制了控制权转移限制了数据赋值,其他也没啥可限制的了。很有意思的是,这三个编程范式提出的时间顺序可能与大家的直觉相反,从前到后的顺序为:函数式编程(1936年)、面向对象编程(1966年)、结构化编程(1968年)。 1.结构化编程 结构化编程证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序,并限制了 goto 的使用。遵守结构化编程,工程师就可以像数学家一样对自己的程序进行推理证明,用代码将一些已证明可用的结构串联起来,只要自行证明这些额外代码是确定的,就可以推导出整个程序的正确性。 前面提到结构化编程对控制权的直接转移进行了限制,其实就是限制了 goto 语句。什么叫做控制权的直接转移?就是函数调用或者 goto 语句,代码在原来的流程里不继续执行了,转而去执行别的代码,并且你指明了执行什么代码。为什么要限制 goto 语句?因为 goto 语句的一些用法会导致某个模块无法被递归拆分成更小的、可证明的单元。而采用分解法将大型问题拆分正是结构化编程的核心价值。 其实遵守结构化编程,工程师们也无法像数学家那样证明自己的程序是正确的,只能像物理学家一样,说自己的程序暂时没被证伪(没被找到bug)。数学公式和物理公式的最大区别,就是数学公式可被证明,而物理公式无法被证明,只要目前的实验数据没把它证伪,我们就认为它是正确的。程序也是一样,所有的 test case 都通过了,没发现问题,我们就认为这段程序是正确的。 2.面向对象编程 面向对象编程包括封装、继承和多态,从架构的角度,这里只关注多态。多态让我们更方便、安全地通过函数调用的方式进行组件间通信,它也是依赖反转(让依赖与控制流方向相反)的基础。 在非面向对象的编程语言中,我们如何在互相解耦的组件间实现函数调用?答案是函数指针。比如采用C语言编写的操作系统中,定义了如下的结构体来解耦具体的IO设备, IO 设备的驱动程序只需要把函数指针指到自己的实现就可以了。 struct FILE { void (*open)(char* name, int mode); void (*close)(); int (*read)(); void (*write)(char); void (*seek)(long index, int mode);}这种通过函数指针进行组件间通信的方式非常脆弱,工程师必须严格按照约定初始化函数指针,并严格地按照约定来调用这些指针,只要一个人没有遵守约定,整个程序都会产生极其难以跟踪和消除的 Bug。所以面向对象编程限制了函数指针的使用,通过接口-实现、抽象类-继承等多态的方式来替代。 前面提到面向对象编程对控制权的间接转移进行了限制,其实就是限制了函数指针的使用。什么叫做控制权的间接转移?就是代码在原来的流程里不继续执行了,转而去执行别的代码,但具体执行了啥代码你也不知道,你只调了个函数指针或者接口。 ...

June 12, 2019 · 1 min · jiezi

高性能服务器架构思路不仅是思路

在服务器端程序开发领域,性能问题一直是备受关注的重点。业界有大量的框架、组件、类库都是以性能为卖点而广为人知。然而,服务器端程序在性能问题上应该有何种基本思路,这个却很少被这些项目的文档提及。本文正式希望介绍服务器端解决性能问题的基本策略和经典实践,并分为几个部分来说明: 缓存策略的概念和实例2.缓存策略的难点:不同特点的缓存数据的清理机制 3.分布策略的概念和实例 4.分布策略的难点:共享数据安全性与代码复杂度的平衡 缓存缓存策略的概念我们提到服务器端性能问题的时候,往往会混淆不清。因为当我们访问一个服务器时,出现服务卡住不能得到数据,就会认为是“性能问题”。但是实际上这个性能问题可能是有不同的原因,表现出来都是针对客户请求的延迟很长甚至中断。我们来看看这些原因有哪些:第一个是所谓并发数不足,也就是同时请求的客户过多,导致超过容纳能力的客户被拒绝服务,这种情况往往会因为服务器内存耗尽而导致的;第二个是处理延迟过长,也就是有一些客户的请求处理时间已经超过用户可以忍受的长度,这种情况常常表现为CPU占用满额100%。 我们在服务器开发的时候,最常用到的有下面这几种硬件:CPU、内存、磁盘、网卡。其中CPU是代表计算机处理时间的,硬盘的空间一般很大,主要是读写磁盘会带来比较大的处理延迟,而内存、网卡则是受存储、带宽的容量限制的。所以当我们的服务器出现性能问题的时候,就是这几个硬件某一个甚至几个都出现负荷占满的情况。这四个硬件的资源一般可以抽象成两类:一类是时间资源,比如CPU和磁盘读写;一类是空间资源,比如内存和网卡带宽。所以当我们的服务器出现性能问题,有一个最基本的思路,就是——时间空间转换。我们可以举几个例子来说明这个问题。 当我们访问一个WEB的网站的时候,输入的URL地址会被服务器变成对磁盘上某个文件的读取。如果有大量的用户访问这个网站,每次的请求都会造成对磁盘的读操作,可能会让磁盘不堪重负,导致无法即时读取到文件内容。但是如果我们写的程序,会把读取过一次的文件内容,长时间的保存在内存中,当有另外一个对同样文件的读取时,就直接从内存中把数据返回给客户端,就无需去让磁盘读取了。由于用户访问的文件往往很集中,所以大量的请求可能都能从内存中找到保存的副本,这样就能大大提高服务器能承载的访问量了。这种做法,就是用内存的空间,换取了磁盘的读写时间,属于用空间换时间的策略。 举另外一个例子:我们写一个网络游戏的服务器端程序,通过读写数据库来提供玩家资料存档。如果有大量玩家进入这个服务器,必定有很多玩家的数据资料变化,比如升级、获得武器等等,这些通过读写数据库来实现的操作,可能会让数据库进程负荷过重,导致玩家无法即时完成游戏操作。我们会发现游戏中的读操作,大部分都是针是对一些静态数据的,比如游戏中的关卡数据、武器道具的具体信息;而很多写操作,实际上是会覆盖的,比如我的经验值,可能每打一个怪都会增加几十点,但是最后记录的只是最终的一个经验值,而不会记录下打怪的每个过程。所以我们也可以使用时空转换的策略来提供性能:我们可以用内存,把那些游戏中的静态数据,都一次性读取并保存起来,这样每次读这些数据,都和数据库无关了;而玩家的资料数据,则不是每次变化都去写数据库,而是先在内存中保持一个玩家数据的副本,所有的写操作都先去写内存中的结构,然后定期再由服务器主动写回到数据库中,这样可以把多次的写数据库操作变成一次写操作,也能节省很多写数据库的消耗。这种做法也是用空间换时间的策略。 最后说说用时间换空间的例子:假设我们要开发一个企业通讯录的数据存储系统,客户要求我们能保存下通讯录的每次新增、修改、删除操作,也就是这个数据的所有变更历史,以便可以让数据回退到任何一个过去的时间点。那么我们最简单的做法,就是这个数据在任何变化的时候,都拷贝一份副本。但是这样会非常的浪费磁盘空间,因为这个数据本身变化的部分可能只有很小一部分,但是要拷贝的副本可能很大。这种情况下,我们就可以在每次数据变化的时候,都记下一条记录,内容就是数据变化的情况:插入了一条内容是某某的联系方法、删除了一条某某的联系方法……,这样我们记录的数据,仅仅就是变化的部分,而不需要拷贝很多份副本。当我们需要恢复到任何一个时间点的时候,只需要按这些记录依次对数据修改一遍,直到指定的时间点的记录即可。这个恢复的时间可能会有点长,但是却可以大大节省存储空间。这就是用CPU的时间来换磁盘的存储空间的策略。我们现在常见的MySQL InnoDB日志型数据表,以及SVN源代码存储,都是使用这种策略的。 另外,我们的Web服务器,在发送HTML文件内容的时候,往往也会先用ZIP压缩,然后发送给浏览器,浏览器收到后要先解压,然后才能显示,这个也是用服务器和客户端的CPU时间,来换取网络带宽的空间。 在我们的计算机体系中,缓存的思路几乎无处不在,比如我们的CPU里面就有1级缓存、2级缓存,他们就是为了用这些快速的存储空间,换取对内存这种相对比较慢的存储空间的等待时间。我们的显示卡里面也带有大容量的缓存,他们是用来存储显示图形的运算结果的。 缓存的本质,除了让“已经处理过的数据,不需要重复处理”以外,还有“以快速的数据存储读写,代替较慢速的存储读写”的策略。我们在选择缓存策略进行时空转换的时候,必须明确我们要转换的时间和空间是否合理,是否能达到效果。比如早期有一些人会把WEB文件缓存在分布式磁盘上(例如NFS),但是由于通过网络访问磁盘本身就是一个比较慢的操作,而且还会占用可能就不充裕的网络带宽空间,导致性能可能变得更慢。 在设计缓存机制的时候,我们还容易碰到另外一个风险,就是对缓存数据的编程处理问题。如果我们要缓存的数据,并不是完全无需处理直接读写的,而是需要读入内存后,以某种语言的结构体或者对象来处理的,这就需要涉及到“序列化”和“反序列化”的问题。如果我们采用直接拷贝内存的方式来缓存数据,当我们的这些数据需要跨进程、甚至跨语言访问的时候,会出现那些指针、ID、句柄数据的失效。因为在另外一个进程空间里,这些“标记型”的数据都是不存在的。因此我们需要更深入的对数据缓存的方法,我们可能会使用所谓深拷贝的方案,也就是跟着那些指针去找出目标内存的数据,一并拷贝。一些更现代的做法,则是使用所谓序列化方案来解决这个问题,也就是用一些明确定义了的“拷贝方法”来定义一个结构体,然后用户就能明确的知道这个数据会被拷贝,直接取消了指针之类的内存地址数据的存在。比如著名的Protocol Buffer就能很方便的进行内存、磁盘、网络位置的缓存;现在我们常见的JSON,也被一些系统用来作为缓存的数据格式。 但是我们需要注意的是,缓存的数据和我们程序真正要操作的数据,往往是需要进行一些拷贝和运算的,这就是序列化和反序列化的过程,这个过程很快,也有可能很慢。所以我们在选择数据缓存结构的时候,必须要注意其转换时间,否则你缓存的效果可能被这些数据拷贝、转换消耗去很多,严重的甚至比不缓存更差。一般来说,缓存的数据越解决使用时的内存结构,其转换速度就越快,在这点上,Protocol Buffer采用TLV编码,就比不上直接memcpy的一个C结构体,但是比编码成纯文本的XML或者JSON要来的更快。因为编解码的过程往往要进行复杂的查表映射,列表结构等操作。 缓存策略的难点虽然使用缓存思想似乎是一个很简单的事情,但是缓存机制却有一个核心的难点,就是——缓存清理。我们所说的缓存,都是保存一些数据,但是这些数据往往是会变化的,我们要针对这些变化,清理掉保存的“脏”数据,却可能不是那么容易。 首先我们来看看最简单的缓存数据——静态数据。这种数据往往在程序的运行时是不会变化的,比如Web服务器内存中缓存的HTML文件数据,就是这种。事实上,所有的不是由外部用户上传的数据,都属于这种“运行时静态数据”。一般来说,我们对这种数据,可以采用两种建立缓存的方法:一是程序一启动,就一股脑把所有的静态数据从文件或者数据库读入内存;二就是程序启动的时候并不加载静态数据,而是等有用户访问相关数据的时候,才去加载,这也就是所谓lazy load的做法。第一种方法编程比较简单,程序的内存启动后就稳定了,不太容易出现内存漏洞(如果加载的缓存太多,程序在启动后立刻会因内存不足而退出,比较容易发现问题);第二种方法程序启动很快,但要对缓存占用的空间有所限制或者规划,否则如果要缓存的数据太多,可能会耗尽内存,导致在线服务中断。 一般来说,静态数据是不会“脏”的,因为没有用户会去写缓存中的数据。但是在实际工作中,我们的在线服务往往会需要“立刻”变更一些缓存数据。比如在门户网站上发布了一条新闻,我们会希望立刻让所有访问的用户都看到。按最简单的做法,我们一般只要重启一下服务器进程,内存中的缓存就会消失了。对于静态缓存的变化频率非常低的业务,这样是可以的,但是如果是新闻网站,就不能每隔几分钟就重启一下WEB服务器进程,这样会影响大量在线用户的访问。常见的解决这类问题有两种处理策略: 第一种是使用控制命令。简单来说,就是在服务器进程上,开通一个实时的命令端口,我们可以通过网络数据包(如UDP包),或者Linux系统信号(如kill SIGUSR2进程号)之类的手段,发送一个命令消息给服务器进程,让进程开始清理缓存。这种清理可能执行的是最简单的“全部清理”,也有的可以细致一点的,让命令消息中带有“想清理的数据ID”这样的信息,比如我们发送给WEB服务器的清理消息网络包中会带一个字符串URL,表示要清理哪一个HTML文件的缓存。这种做法的好处是清理的操作很精准,可以明确的控制清理的时间和数据。但是缺点就是比较繁琐,手工去编写发送这种命令很烦人,所以一般我们会把清理缓存命令的工作,编写到上传静态数据的工具当中,比如结合到网站的内容发布系统中,一旦编辑提交了一篇新的新闻,发布系统的程序就自动的发送一个清理消息给WEB服务器。 第二种是使用字段判断逻辑。也就是服务器进程,会在每次读取缓存前,根据一些特征数据,快速的判断内存中的缓存和源数据内容,是否有不一致(是否脏)的地方,如果有不一致的地方,就自动清理这条数据的缓存。这种做法会消耗一部分CPU,但是就不需要人工去处理清理缓存的事情,自动化程度很高。现在我们的浏览器和WEB服务器之间,就有用这种机制:检查文件MD5;或者检查文件最后更新时间。具体的做法,就是每次浏览器发起对WEB服务器的请求时,除了发送URL给服务器外,还会发送一个缓存了此URL对应的文件内容的MD5校验串、或者是此文件在服务器上的“最后更新时间”(这个校验串和“最后更新时间”是第一次获的文件时一并从服务器获得的);服务器收到之后,就会把MD5校验串或者最后更新时间,和磁盘上的目标文件进行对比,如果是一致的,说明这个文件没有被修改过(缓存不是“脏”的),可以直接使用缓存。否则就会读取目标文件返回新的内容给浏览器。这种做法对于服务器性能是有一定消耗的,所以如果往往我们还会搭配其他的缓存清理机制来用,比如我们会在设置一个“超时检查”的机制:就是对于所有的缓存清理检查,我们都简单的看看缓存存在的时间是否“超时”了,如果超过了,才进行下一步的检查,这样就不用每次请求都去算MD5或者看最后更新时间了。但是这样就存在“超时”时间内缓存变脏的可能性。 上面说了运行时静态的缓存清理,现在说说运行时变化的缓存数据。在服务器程序运行期间,如果用户和服务器之间的交互,导致了缓存的数据产生了变化,就是所谓“运行时变化缓存”。比如我们玩网络游戏,登录之后的角色数据就会从数据库里读出来,进入服务器的缓存(可能是堆内存或者memcached、共享内存),在我们不断进行游戏操作的时候,对应的角色数据就会产生修改的操作,这种缓存数据就是“运行时变化的缓存”。这种运行时变化的数据,有读和写两个方面的清理问题:由于缓存的数据会变化,如果另外一个进程从数据库读你的角色数据,就会发现和当前游戏里的数据不一致;如果服务器进程突然结束了,你在游戏里升级,或者捡道具的数据可能会从内存缓存中消失,导致你白忙活了半天,这就是没有回写(缓存写操作的清理)导致的问题。这种情况在电子商务领域也很常见,最典型的就是火车票网上购买的系统,火车票数据缓存在内存必须有合适的清理机制,否则让两个买了同一张票就麻烦了,但如果不缓存,大量用户同时抢票,服务器也应对不过来。因此在运行时变化的数据缓存,应该有一些特别的缓存清理策略。 在实际运行业务中,运行变化的数据往往是根据使用用户的增多而增多的,因此首先要考虑的问题,就是缓存空间不够的可能性。我们不太可能把全部数据都放到缓存的空间里,也不可能清理缓存的时候就全部数据一起清理,所以我们一般要对数据进行分割,这种分割的策略常见的有两种:一种是按重要级来分割,一种是按使用部分分割。 先举例说说“按重要级分割”,在网络游戏中,同样是角色的数据,有些数据的变化可能会每次修改都立刻回写到数据库(清理写缓存),其他一些数据的变化会延迟一段时间,甚至有些数据直到角色退出游戏才回写,如玩家的等级变化(升级了),武器装备的获得和消耗,这些玩家非常看重的数据,基本上会立刻回写,这些就是所谓最重要的缓存数据。而玩家的经验值变化、当前HP、MP的变化,就会延迟一段时间才写,因为就算丢失了缓存,玩家也不会太过关注。最后有些比如玩家在房间(地区)里的X/Y坐标,对话聊天的记录,可能会退出时回写,甚至不回写。这个例子说的是“写缓存”的清理,下面说说“读缓存”的按重要级分割清理。 假如我们写一个网店系统,里面容纳了很多产品,这些产品有一些会被用户频繁检索到,比较热销,而另外一些商品则没那么热销。热销的商品的余额、销量、评价都会比较频繁的变化,而滞销的商品则变化很少。所以我们在设计的时候,就应该按照不同商品的访问频繁程度,来决定缓存哪些商品的数据。我们在设计缓存的结构时,就应该构建一个可以统计缓存读写次数的指标,如果有些数据的读写频率过低,或者空闲(没有人读、写缓存)时间超长,缓存应该主动清理掉这些数据,以便其他新的数据能进入缓存。这种策略也叫做“冷热交换”策略。实现“冷热交换”的策略时,关键是要定义一个合理的冷热统计算法。一些固定的指标和算法,往往并不能很好的应对不同硬件、不同网络情况下的变化,所以现在人们普遍会用一些动态的算法,如Redis就采用了5种,他们是: 1.根据过期时间,清理最长时间没用过的 2.根据过期时间,清理即将过期的 3.根据过期时间,任意清理一个 4.无论是否过期,随机清理 5.无论是否过期,根据LRU原则清理:所谓LRU,就是Least Recently Used,最近最久未使用过。这个原则的思想是:如果一个数据在最近一段时间没有被访问到,那么在将来他被访问的可能性也很小。LRU是在操作系统中很常见的一种原则,比如内存的页面置换算法(也包括FIFO,LFU等),对于LRU的实现,还是非常有技巧的,但是本文就不详细去说明如何实现,留待大家上网搜索“LRU”关键字学习。 数据缓存的清理策略其实远不止上面所说的这些,要用好缓存这个武器,就要仔细研究需要缓存的数据特征,他们的读写分布,数据之中的差别。然后最大化的利用业务领域的知识,来设计最合理的缓存清理策略。这个世界上不存在万能的优化缓存清理策略,只存在针对业务领域最优化的策略,这需要我们程序员深入理解业务领域,去发现数据背后的规律。 分布分布策略的概念任何的服务器的性能都是有极限的,面对海量的互联网访问需求,是不可能单靠一台服务器或者一个CPU来承担的。所以我们一般都会在运行时架构设计之初,就考虑如何能利用多个CPU、多台服务器来分担负载,这就是所谓分布的策略。分布式的服务器概念很简单,但是实现起来却比较复杂。因为我们写的程序,往往都是以一个CPU,一块内存为基础来设计的,所以要让多个程序同时运行,并且协调运作,这需要更多的底层工作。 首先出现能支持分布式概念的技术是多进程。在DOS时代,计算机在一个时间内只能运行一个程序,如果你想一边写程序,同时一边听mp3,都是不可能的。但是,在WIN95操作系统下,你就可以同时开多个窗口,背后就是同时在运行多个程序。在Unix和后来的Linux操作系统里面,都普遍支持了多进程的技术。所谓的多进程,就是操作系统可以同时运行我们编写的多个程序,每个程序运行的时候,都好像自己独占着CPU和内存一样。在计算机只有一个CPU的时候,实际上计算机会分时复用的运行多个进程,CPU在多个进程之间切换。但是如果这个计算机有多个CPU或者多个CPU核,则会真正的有几个进程同时运行。所以进程就好像一个操作系统提供的运行时“程序盒子”,可以用来在运行时,容纳任何我们想运行的程序。当我们掌握了操作系统的多进程技术后,我们就可以把服务器上的运行任务,分为多个部分,然后分别写到不同的程序里,利用上多CPU或者多核,甚至是多个服务器的CPU一起来承担负载。 这种划分多个进程的架构,一般会有两种策略:一种是按功能来划分,比如负责网络处理的一个进程,负责数据库处理的一个进程,负责计算某个业务逻辑的一个进程。另外一种策略是每个进程都是同样的功能,只是分担不同的运算任务而已。使用第一种策略的系统,运行的时候,直接根据操作系统提供的诊断工具,就能直观的监测到每个功能模块的性能消耗,因为操作系统提供进程盒子的同时,也能提供对进程的全方位的监测,比如CPU占用、内存消耗、磁盘和网络I/O等等。但是这种策略的运维部署会稍微复杂一点,因为任何一个进程没有启动,或者和其他进程的通信地址没配置好,都可能导致整个系统无法运作;而第二种分布策略,由于每个进程都是一样的,这样的安装部署就非常简单,性能不够就多找几个机器,多启动几个进程就完成了,这就是所谓的平行扩展。 现在比较复杂的分布式系统,会结合这两种策略,也就是说系统既按一些功能划分出不同的具体功能进程,而这些进程又是可以平行扩展的。当然这样的系统在开发和运维上的复杂度,都是比单独使用“按功能划分”和“平行划分”要更高的。由于要管理大量的进程,传统的依靠配置文件来配置整个集群的做法,会显得越来越不实用:这些运行中的进程,可能和其他很多进程产生通信关系,当其中一个进程变更通信地址时,势必影响所有其他进程的配置。所以我们需要集中的管理所有进程的通信地址,当有变化的时候,只需要修改一个地方。在大量进程构建的集群中,我们还会碰到容灾和扩容的问题:当集群中某个服务器出现故障,可能会有一些进程消失;而当我们需要增加集群的承载能力时,我们又需要增加新的服务器以及进程。这些工作在长期运行的服务器系统中,会是比较常见的任务,如果整个分布系统有一个运行中的中心进程,能自动化的监测所有的进程状态,一旦有进程加入或者退出集群,都能即时的修改所有其他进程的配置,这就形成了一套动态的多进程管理系统。开源的ZooKeeper给我们提供了一个可以充当这种动态集群中心的实现方案。由于ZooKeeper本身是可以平行扩展的,所以它自己也是具备一定容灾能力的。现在越来越多的分布式系统都开始使用以ZooKeeper为集群中心的动态进程管理策略了。 在调用多进程服务的策略上,我们也会有一定的策略选择,其中最著名的策略有三个:一个是动态负载均衡策略;一个是读写分离策略;一个是一致性哈希策略。动态负载均衡策略,一般会搜集多个进程的服务状态,然后挑选一个负载最轻的进程来分发服务,这种策略对于比较同质化的进程是比较合适的。读写分离策略则是关注对持久化数据的性能,比如对数据库的操作,我们会提供一批进程专门用于提供读数据的服务,而另外一个(或多个)进程用于写数据的服务,这些写数据的进程都会每次写多份拷贝到“读服务进程”的数据区(可能就是单独的数据库),这样在对外提供服务的时候,就可以提供更多的硬件资源。一致性哈希策略是针对任何一个任务,看看这个任务所涉及读写的数据,是属于哪一片的,是否有某种可以缓存的特征,然后按这个数据的ID或者特征值,进行“一致性哈希”的计算,分担给对应的处理进程。这种进程调用策略,能非常的利用上进程内的缓存(如果存在),比如我们的一个在线游戏,由100个进程承担服务,那么我们就可以把游戏玩家的ID,作为一致性哈希的数据ID,作为进程调用的KEY,如果目标服务进程有缓存游戏玩家的数据,那么所有这个玩家的操作请求,都会被转到这个目标服务进程上,缓存的命中率大大提高。而使用“一致性哈希”,而不是其他哈希算法,或者取模算法,主要是考虑到,如果服务进程有一部分因故障消失,剩下的服务进程的缓存依然可以有效,而不会整个集群所有进程的缓存都失效。具体有兴趣的读者可以搜索“一致性哈希”一探究竟。 以多进程利用大量的服务器,以及服务器上的多个CPU核心,是一个非常有效的手段。但是使用多进程带来的额外的编程复杂度的问题。一般来说我们认为最好是每个CPU核心一个进程,这样能最好的利用硬件。如果同时运行的进程过多,操作系统会消耗很多CPU时间在不同进程的切换过程上。但是,我们早期所获得的很多API都是阻塞的,比如文件I/O,网络读写,数据库操作等。如果我们只用有限的进程来执行带这些阻塞操作的程序,那么CPU会大量被浪费,因为阻塞的API会让有限的这些进程停着等待结果。那么,如果我们希望能处理更多的任务,就必须要启动更多的进程,以便充分利用那些阻塞的时间,但是由于进程是操作系统提供的“盒子”,这个盒子比较大,切换耗费的时间也比较多,所以大量并行的进程反而会无谓的消耗服务器资源。加上进程之间的内存一般是隔离的,进程间如果要交换一些数据,往往需要使用一些操作系统提供的工具,比如网络socket,这些都会额外消耗服务器性能。因此,我们需要一种切换代价更少,通信方式更便捷,编程方法更简单的并行技术,这个时候,多线程技术出现了。 多线程的特点是切换代价少,可以同时访问内存。我们可以在编程的时候,任意让某个函数放入新的线程去执行,这个函数的参数可以是任何的变量或指针。如果我们希望和这些运行时的线程通信,只要读、写这些指针指向的变量即可。在需要大量阻塞操作的时候,我们可以启动大量的线程,这样就能较好的利用CPU的空闲时间;线程的切换代价比进程低得多,所以我们能利用的CPU也会多很多。线程是一个比进程更小的“程序盒子”,他可以放入某一个函数调用,而不是一个完整的程序。一般来说,如果多个线程只是在一个进程里面运行,那其实是没有利用到多核CPU的并行好处的,仅仅是利用了单个空闲的CPU核心。但是,在JAVA和C#这类带虚拟机的语言中,多线程的实现底层,会根据具体的操作系统的任务调度单位(比如进程),尽量让线程也成为操作系统可以调度的单位,从而利用上多个CPU核心。比如Linux2.6之后,提供了NPTL的内核线程模型,JVM就提供了JAVA线程到NPTL内核线程的映射,从而利用上多核CPU。而Windows系统中,据说本身线程就是系统的最小调度单位,所以多线程也是利用上多核CPU的。所以我们在使用JAVAC#编程的时候,多线程往往已经同时具备了多进程利用多核CPU、以及切换开销低的两个好处。 早期的一些网络聊天室服务,结合了多线程和多进程使用的例子。一开始程序会启动多个广播聊天的进程,每个进程都代表一个房间;每个用户连接到聊天室,就为他启动一个线程,这个线程会阻塞的读取用户的输入流。这种模型在使用阻塞API的环境下,非常简单,但也非常有效。 当我们在广泛使用多线程的时候,我们发现,尽管多线程有很多优点,但是依然会有明显的两个缺点:一个内存占用比较大且不太可控;第二个是多个线程对于用一个数据使用时,需要考虑复杂的“锁”问题。由于多线程是基于对一个函数调用的并行运行,这个函数里面可能会调用很多个子函数,每调用一层子函数,就会要在栈上占用新的内存,大量线程同时在运行的时候,就会同时存在大量的栈,这些栈加在一起,可能会形成很大的内存占用。并且,我们编写服务器端程序,往往希望资源占用尽量可控,而不是动态变化太大,因为你不知道什么时候会因为内存用完而当机,在多线程的程序中,由于程序运行的内容导致栈的伸缩幅度可能很大,有可能超出我们预期的内存占用,导致服务的故障。而对于内存的“锁”问题,一直是多线程中复杂的课题,很多多线程工具库,都推出了大量的“无锁”容器,或者“线程安全”的容器,并且还大量设计了很多协调线程运作的类库。但是这些复杂的工具,无疑都是证明了多线程对于内存使用上的问题。 由于多线程还是有一定的缺点,所以很多程序员想到了一个釜底抽薪的方法:使用多线程往往是因为阻塞式API的存在,比如一个read()操作会一直停止当前线程,那么我们能不能让这些操作变成不阻塞呢?——selector/epoll就是Linux退出的非阻塞式API。如果我们使用了非阻塞的操作函数,那么我们也无需用多线程来并发的等待阻塞结果。我们只需要用一个线程,循环的检查操作的状态,如果有结果就处理,无结果就继续循环。这种程序的结果往往会有一个大的死循环,称为主循环。在主循环体内,程序员可以安排每个操作事件、每个逻辑状态的处理逻辑。这样CPU既无需在多线程间切换,也无需处理复杂的并行数据锁的问题——因为只有一个线程在运行。这种就是被称为“并发”的方案。 实际上计算机底层早就有使用并发的策略,我们知道计算机对于外部设备(比如磁盘、网卡、显卡、声卡、键盘、鼠标),都使用了一种叫“中断”的技术,早期的电脑使用者可能还被要求配置IRQ号。这个中断技术的特点,就是CPU不会阻塞的一直停在等待外部设备数据的状态,而是外部数据准备好后,给CPU发一个“中断信号”,让CPU转去处理这些数据。非阻塞的编程实际上也是类似这种行为,CPU不会一直阻塞的等待某些I/O的API调用,而是先处理其他逻辑,然后每次主循环去主动检查一下这些I/O操作的状态。 多线程和异步的例子,最著名就是Web服务器领域的Apache和Nginx的模型。Apache是多进程/多线程模型的,它会在启动的时候启动一批进程,作为进程池,当用户请求到来的时候,从进程池中分配处理进程给具体的用户请求,这样可以节省多进程/线程的创建和销毁开销,但是如果同时有大量的请求过来,还是需要消耗比较高的进程/线程切换。而Nginx则是采用epoll技术,这种非阻塞的做法,可以让一个进程同时处理大量的并发请求,而无需反复切换。对于大量的用户访问场景下,apache会存在大量的进程,而nginx则可以仅用有限的进程(比如按CPU核心数来启动),这样就会比apache节省了不少“进程切换”的消耗,所以其并发性能会更好。 在现代服务器端软件中,nginx这种模型的运维管理会更简单,性能消耗也会稍微更小一点,所以成为最流行的进程架构。但是这种好处,会付出一些另外的代价:非阻塞代码在编程的复杂度变大。 分布式编程复杂度以前我们的代码,从上往下执行,每一行都会占用一定的CPU时间,这些代码的直接顺序,也是和编写的顺序基本一致,任何一行代码,都是唯一时刻的执行任务。当我们在编写分布式程序的时候,我们的代码将不再好像那些单进程、单线程的程序一样简单。我们要把同时运行的不同代码,在同一段代码中编写。就好像我们要把整个交响乐团的每个乐器的乐谱,全部写到一张纸上。为了解决这种编程的复杂度,业界发展出了多种编码形式。 在多进程的编码模型上,fork()函数可以说一个非常典型的代表。在一段代码中,fork()调用之后的部分,可能会被新的进程中执行。要区分当前代码的所在进程,要靠fork()的返回值变量。这种做法,等于把多个进程的代码都合并到一块,然后通过某些变量作为标志来划分。这样的写法,对于不同进程代码大部份相同的“同质进程”来说,还是比较方便的,最怕就是有大量的不同逻辑要用不同的进程来处理,这种情况下,我们就只能自己通过规范fork()附近的代码,来控制混乱的局面。比较典型的是把fork()附近的代码弄成一个类似分发器(dispatcher)的形式,把不同功能的代码放到不同的函数中,以fork之前的标记变量来决定如何调用。 ...

June 12, 2019 · 1 min · jiezi

MySQL80-新特性-说说InnoDB-Log-System的隐藏参数

InnoDB在设计lock-free的log system时,除了已有的参数外,还通过宏控制隐藏了一些参数,如果你使用源码编译时,打开cmake选项-DENABLE_EXPERIMENT_SYSVARS=1, 就可以看到这些参数了。本文主要简单的过一下这些隐藏的参数所代表的含义 A.innodb_log_write_eventsinnodb_log_flush_events两者的含义类似,表示用来唤醒等待log write/flush的event的个数,默认值都是2048比如你要等待的位置在lsnA,那么计算的slot为:slot = (lsnA - 1) /OS_FILE_LOG_BLOCK_SIZE & (innodb_log_write/flush_events - 1)这意味着:如果事务的commit log的end lsn落在相同block里,他们可能产生event的竞争当然如果不在同一个block的时候,如果调大参数,就可以减少竞争,但也会有无效的唤醒唤醒操作通常由后台线程log_write_notifier 或者log_flush_notifier异步来做,但如果推进的log write/flush还不足一个block的话,那就log_writter/flusher自己去唤醒了。 B.innodb_log_recent_written_size, 默认1MB表示recent_written这个link_buf的大小,其实控制了并发往log buffer中同时拷贝的事务日志量,向前由新的日志加入,后面由log writer通过写日志向前推进,如果写的慢的话,那这个link_buf很可能用满,用户线程就得spin等待。再慢io的系统上,我们可以稍微调大这个参数 innodb_Log_recent_closed_size, 默认2MB表示recent closed这个link_buf的大小,也是维护可以并发往flush list上插入脏页的并罚度,如果插入脏页速度慢,或者lin_buf没有及时合并推进,就会spin wait 简单说下link_buf, 这本质上是一个数组,但使用无锁的使用方式来维护lsn的推进,比如获得一个lsn开始和结束,那就通过设置buf[start_lsn] = end_lsn的类似方式来维护lsn链,基于lsn是连续值的事实,最终必然不会出现空洞,所以在演化的过程中,可以从尾部推进连续的lsn,头部插入新的值.如果新插入的值超过了尾部,表示buf满了,就需要spin wait了C.innodb_log_wait_for_write_spin_delay, innodb_log_wait_for_write_timeout 从8.0版本开始用户线程不再自己去写redo,而是等待后台线程去写,这两个变量控制了spin以及condition wait的timeout时间,当spin一段时间还没推进到某个想要的lsn点时,就会进入condition wait 另外两个变量innodb_log_wait_for_flush_spin_delayinnodb_log_wait_for_flush_timeout含义类似,但是是等待log flush到某个指定lsn 注意在实际计算过程中,最大spin次数,会考虑到cpu利用率,以及另外两个参数:innodb_log_spin_cpu_abs_lwminnodb_log_spin_cpu_pct_hwm 如果是等待flush操作的话,还收到参数innodb_log_wait_for_flush_spin_hwm限制,该参数控制了等待flush的时间上限,如果平均等待flush的时间超过了这个上限的话, 就没必要去spin,而是直接进入condition wait 关于spin次数的计算方式在函数log_max_spins_when_waiting_in_user_thread中": 函数的参数即为配置项innodb_log_wait_for_write_spin_delay或innodb_log_wait_for_flush_spin_delay值 static inline uint64_t log_max_spins_when_waiting_in_user_thread( uint64_t min_non_zero_value) { uint64_t max_spins; /* Get current cpu usage. */ const double cpu = srv_cpu_usage.utime_pct; /* Get high-watermark - when cpu usage is higher, don't spin! */ const uint32_t hwm = srv_log_spin_cpu_pct_hwm; if (srv_cpu_usage.utime_abs < srv_log_spin_cpu_abs_lwm || cpu >= hwm) { /* Don't spin because either cpu usage is too high or it's almost idle so no reason to bother. */ max_spins = 0; } else if (cpu >= hwm / 2) { /* When cpu usage is more than 50% of the hwm, use the minimum allowed number of spin rounds, not to increase cpu usage too much (risky). */ max_spins = min_non_zero_value; } else { /* When cpu usage is less than 50% of the hwm, choose maximum spin rounds in range [minimum, 10*minimum]. Smaller usage of cpu is, more spin rounds might be used. */ const double r = 1.0 * (hwm / 2 - cpu) / (hwm / 2); max_spins = static_cast<uint64_t>(min_non_zero_value + r * min_non_zero_value * 9); } return (max_spins);}D.以下几个参数是后台线程等待任务时spin及condition wait timeout的值log_writer线程:innodb_log_writer_spin_delay,innodb_log_writer_timeout ...

June 4, 2019 · 2 min · jiezi

一个优秀的可定制化Flutter相册组件看这一篇就够了

背景在做图片、视频相关功能的时候,相册是一个绕不开的话题,因为大家基本都有从相册获取图片或者视频的需求。最直接的方式是调用系统相册接口,基本功能是满足的,一些高级功能就不行了,例如自定义UI、多选图片等。 我们调研了官方的image_picker,它也是调用系统的相册接口来处理的,可定制程度不高,不能满足我们的要求。所以我们选择自己来开发Flutter相册组件。 我们的组件需要有如下的功能: 在app内完成图片、视频的选取,完全不用依赖系统相册组件可以多选图片,支持指定选定图片的总数目在多选的时候UI反应出选择的序号。可以控制视频、图片的选择。例如:只让用户选择视频,图片是灰色的。大图预览的时候可以放大缩小,也可直接加入到选取列表。设计思路API使用简单,功能丰富灵活,具有较高的订制性。业务方可以选择完全接入组件,也可以选择在组件上面进行UI定制。 Flutter做UI展现层,具体的数据由各Native平台提供。这种模式,天然从工程上把UI代码和数据代码进行了隔离。我们在开发一个native组件的时候常常会使用MVC架构。Flutter组件的开发的思路也基本类似。整体架构如下: 可以看出,在Flutter侧是一个典型的MVC架构,这里Widget就是View,View和Model绑定,在Model改变的时候View会重新build反映出Model的变化。View的事件会触发Controller去Native获取数据然后更新Model。Native和Flutter通过Method Channel进行通信,两层之间没有强依赖关系,只需要按约定的协议进行通信即可。 Native侧的组成部分,UIAdapter主要是负责机型的适配、刘海屏、全面屏之类的识别。Permission负责媒体读写权限的申请处理。Cache主要负责缓存GPU纹理,在大图预览的时候提高响应速度。Decoder负责解析Bitmap,OpenGL负责Bitmap转纹理。 需要说明的是:我们的这一套实现依赖于flutter外接纹理。在整个相册组件看到的大多数图片都是一个GPU纹理,这样给java堆内存的占用相对于以前的相册实现有大幅的降低。在低端机上面如果使用原生的系统相册,由于内存的原因,app有被系统杀掉的风险。现象就是,从系统相册返回,app重新启动了。使用Flutter相册组件,在低端机上面体验会有所改观。 一些细节1. 分页加载 相册列表需要加载大量图片,Flutter的GridView组件有好几个构造函数,比较容易犯的错误是使用了第一个函数,这需要在一开始就提供大量的widget。应该选择第二个构造函数,GridView在滑动的时候会回调IndexedWidgetBuilder来获取widget,相当于一种懒加载。 GridView.builder({ ... List<Widget> children = const <Widget>[], ... })GridView.builder({ ... @required IndexedWidgetBuilder itemBuilder, int itemCount, ... })滑动过程中,图片滑过后,也就是不可见的时候要进行资源的回收,我们这里这里对应的就是纹理的删除。不断的滑动GridView,内存在上升后会处于稳定,不会一直增长。如果快速的来回滑动纹理会反复的创建和删除,这样会有内存的抖动,体验不是很好。 于是,我们维护了一个图片的状态机,状态有None,Loading,Loaded,Wait_Dispose,Disposed。开始加载的时候,状态从None进入Loading,这个时候用户看到的是空白或者是占位图,当数据回调回来会把状态设置为Loaded的这时候会重新build widget树来显示图片icon,当用户滑走的时候状态进入 Wait_Dispose,这时候并不会马上Dispose,如果用户又滑回来则会从Wait_Dispose进入Loaded状态,不会继续Dispose。如果用户没有往回滑则会从Wait_Dispose进入Disposed状态。当进入Disposed状态后,再需要显示该图片的时候就需要重新走加载流程了。 2. 相册大图展示: 当点击GridView的某张图片的时候会进行这张图片的大图展示,方便用户查看的更清楚。我们知道相机拍摄的图片分辨率都是很高的,如果完全加载,内存会有很大的开销,所以我们在Decode Bitmap的时候进行了缩放,最高只到1080p。大图展示可以概括为三个步骤。 1 从文件Decode出Bitmap2 Bitmap转换成为纹理,并释放Bitmap3 纹理交给Flutter进行展示在步骤1中,Android原生的Bitmap Decode经验同样适用,先Decode出Bitmap的宽高,然后根据要展示的大小计算出缩放倍数, 然后Decode出需要的Bitmap。 Android相册的图片大多是有旋转角度的,如果不处理直接显示,会出现照片旋转90度的问题,所以需要对Bitmap进行旋转,采用Matrix旋转一张1080p的图片在我的测试机器上面大概需要200ms,如果使用OpenGL的纹理坐标进行旋转,大于只需要10ms左右,所以采用OpenGl进行纹理的旋转是一个较好的选择。 在进行大图预览的时候会进入一个水平滑动的PageView,Flutter的PageView一般来说是不会去主动加载相邻的page的。举个例子,在显示index是5的page的时候index为4,6的page也不会提前创建的。这里有一个取巧的办法,对于PageController的viewportFraction参数我们可以设置成为0.9999。对于前面这个例子,就是在显示index是5的page的时候,index为4,6的page也需要显示0.0001。这样index为4,6的page显示不到1个像素,基本上看不出来: PageController(viewportFraction=0.9999)还有另外一种办法,就是在Native侧做预加载。例如:在加载第5张图片的时候,相邻的4,6的图片纹理提前进行加载,当滑动到4,6的时候直接使用缓存的纹理。 纹理缓存后,一个直接的问题:什么时候释放纹理?等到预览页面退出的时候释放所有的纹理显示不是很合适,如果用户一直浏览内存则会无限增长。所以,我们维护了一个5个纹理的LRU缓存,在滑动过程中,最老的纹理会被释放掉。在页面退出的时候整个LRU的缓存会进行销毁。 3. 关于内存 相册图片使用GPU纹理,会大幅减少Java堆内存的占用,对整个app的性能有一定的提升。需要注意的是,GPU的内存是有限的需要在使用完毕后及时删除,不然会有内存的泄漏的风险。另外,在Android平台删除纹理的时候需要保证在GPU线程进行,不然删除是没有效果的。 在华为P8,Android5.0上面进行了对比测试,Flutter相册和原native相册总内存占用基本一致,在GridView列表页面,新增最大内存13M左右。它们的区别在于原native相册使用的是Java堆内存,Flutter相册使用的是Native内存。 总结相册组件API简单、易用,高度可定制。Flutter侧层次分明,有UI订制需求的可以重写Widget来达到目的。另外这是一个不依赖于系统相册的相册组件,自身是完备的,能够和现有的app保持UI、交互的一致性。同时为后面支持更多和相册相关的玩法打好基础。 后续计划由于我们使用的是GPU纹理,可以考虑支持显示高清4K图片,而且客户端内存不会有太大的压力。但是4k图片的Bitmap转纹理需消耗更多的时间,UI交互上面需要做些loading状态的支持。 组件功能丰富,稳定后,进行开源,回馈给社区。 本文作者:闲鱼技术-邻云阅读原文 本文为云栖社区原创内容,未经允许不得转载。

May 31, 2019 · 1 min · jiezi

开发函数计算的正确姿势-移植-nextjs-服务端渲染框架

首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考。Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考。2.0 版本的 Fun,在部署这一块做了很多努力,并提供了比较完善的功能,能够做到将云资源方便、平滑地部署到云端。但该版本,在本地开发上的体验,还有较多的工作要做。于是,我们决定推出 Fun Init 弥补这一处短板。Fun Init: Fun Init 作为 Fun 的一个子命令存在,只要 Fun 的版本大于等于 2.7.0,即可以直接通过 fun init 命令使用。Fun Init 工具可以根据指定的模板快速的创建函数计算应用,快速体验和开发函数计算相关业务。官方会提供常用的模板,用户也可以自定自己的模板。背景next.js 是一种 React 的服务端渲染框架,且 next.js 集成度极高,框架自身集成了 webpack、babel、express 等,使得开发者可以仅依赖 next、react、react-dom 就可以非常方便的构建自己的 SSR React 应用,开发者甚至都不用像以前那样关心路由。 next.js 的高度集成性,使得我们很容易就能实现代码分割、路由跳转、热更新以及服务端渲染和前端渲染。 next.js 可以与 express、koa 等服务端结合使用。为了能让 next.js 在函数计算运行,首先需要让 next.js 在 express 中运行起来,然后再移植 express 到函数计算中运行。express 应用移植相关文章: 开发函数计算的正确姿势——移植 Express移植 express.js 应用到函数计算next.js 运行在 express 中现在,我们提供了一个 fun 模块,通过该模板,三分钟就可以让 next.js 应用在函数计算中运行起来。效果如下: 快速开始1. 安装 node ...

May 24, 2019 · 1 min · jiezi

Flutter高内聚组件怎么做阿里闲鱼打造开源高效方案

fish_redux是闲鱼技术团队打造的flutter应用开发框架,旨在解决页面内组件间的高内聚、低耦合问题。开源地址:https://github.com/alibaba/fish-redux 从react_redux说起redux对于前端的同学来说是一个比较熟悉的框架了,fish_redux借鉴了redux单项数据流思想。在flutter上说到redux,大家可能第一反应会类比到react上的react_redux。在react_redux中有个重要的概念——connect, connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])简单得说,connect允许使用者从Redux store中获取数据并绑定到组件的props上,可以dispatch一个action去修改数据。 那么fish_redux中的connector是做什么的呢?为什么说connector解决了组件内聚的问题?我们应该如何理解它的设计呢? connector in fish_redux尽管都起到了连接的作用,但fish_redux与react_redux在抽象层面有很大的不同。 fish_redux本身是一个flutter上的应用框架,建立了自己的component体系,用来解决组件内的高内聚和组件间的低耦合。从connector角度来说,如何解决内聚问题,是设计中的重要考量。 fish_redux自己制造了Component树,Component聚合了state和dispatch,每一个子Component的state通过connector从父Component的state中筛选。如图所示: 可以看到,fish_redux的connector的主要作用把父子Component关联起来,最重要的操作是filter。state从上之下是一个严谨的树形结构,它的结构复用了Component的树形结构。类似一个漏斗形的数据管道,管理数据的分拆与组装。它表达了如何组装一个Component。 而对于react_redux来说,它主要的作用在于把react框架和redux绑定起来,重点在于如何让React component具有Redux的功能。 从图中可以看到,react_redux和React是平行的结构,经过mapStateToProps后的state也不存在严谨的树形结构,即对于一个React component来说,它的state来自于Redux store而不是父component的state。从框架设计的角度来说,react_redux最重要的一个操作就是attach。 源码分析说完概念,我们从源码的角度来看看fish_redux中的connector是如何运作的,以fish_redux提供的example为例。 class ToDoListPage extends Page<PageState, Map<String, dynamic>> { ToDoListPage() : super( ... dependencies: Dependencies<PageState>( adapter: ToDoListAdapter(), slots: <String, Dependent<PageState>>{ 'report': ReportConnector() + ReportComponent() }), ... );}在ToDoListPage的构造函数中,向父类构造传递了一个Dependencies对象,在构造Dependencies时,参数slots中包含了名叫"report"的item,注意这个item的生成,是由一个ReportConnector+ReportComponent产生的。 从这里我们得出一个简单却非常重要的结论: 在fish_redux中,一个Dependent = connector + Component 。Dependent代表页面拼装中的一个单元,它可以是一个Component(通过buildComponent函数产生),也可以是一个Adapter(由buildAdapter函数产生)。这样设计的好处是,对于View拼装操作来说,Dependent对外统一了API而不需要透出更多的细节。 根据上面我们得出的结论,connector用来把一个更小的Component单元链接到一个更大的Component或Adapter上。这与我们之前的描述相符合。 connector到底是什么知道了connector的基本作用,我们来看一下它到底链接了哪些东西以及如何链接。 先来看一下ReportConnector类的定义: class ReportConnector extends ConnOp<PageState, ReportState>ReportConnector继承了ConnOp类,所有connector的操作包括+操作,都来自于ConnOp类。 set/get 既然是数据管道,就会有获取和放置 set函数的入参很好得表达了T和P的意思,即把一个P类型的subState合并到T类型的state中。 ...

May 24, 2019 · 1 min · jiezi

开发函数计算的正确姿势-Fun-validate-语法校验排错指南

1. 前言首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考。Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考。 template.yml: template.yml 用于定义 serverless 应用的模型。无论是使用 fun local 还是 fun deploy 等功能,都是通过解析 tempalte.yml 的内容,构建出用户定义的云资源模型,进而实现本地云资源的运行调试以及发布等功能。template.yml 支持的规范文档可以参考。 template.yml 所描述的 Serverless 模型,是 Fun 所有功能的基石。template.yml 的正确性对后续能够顺利使用 Fun 的各项功能无疑是非常关键的。为了帮助用户更快速的修正 template.yml 中错误的描述,我们在 Fun 2.14.0 优化了语法校验的错误信息,可以达到更精准定位报错并修复的目的。 下面我们就通过一个示例,学习如何根据报错信息纠正 template.yml 中的错误语法描述。 备注:请确保 Fun 工具版本在 2.14.0+ 2. 错误的 template.yml 示例ROSTemplateFormatVersion: '2015-09-01'Transform: 'Aliyun::Serverless-2018-04-03'Resources: local-http-demo: Type: 'Aliyun::Serverless::InvalidService' Properties: Description: 'local invoke demo' nodejs8: Type: 'Aliyun::Serverless::InvalidFunction' Properties: Handler: index.handler CodeUri: nodejs8/ Description: 'http trigger demo with nodejs8!' Events: http-test: Type: HTTP Properties: AuthType: ANONYMOUS Method: ['GET', 'POST', 'PUT']在上面的示例中,我们原意是想要描述一个叫做 local-http-demo 的服务,并在服务下定义了一个名为 nodejs8 的函数,同时为该函数配置一个匿名的 HTTP 触发器,支持 GET、POST、PUT 的 HTTP 请求。 ...

May 22, 2019 · 3 min · jiezi

架构整洁之道-看这一篇就够了

阿里妹导读:程序的世界飞速发展,今天所掌握的技能可能明年就过时了,但有些知识历久弥新,掌握了它们,你在程序的海洋中就不会迷路,架构思想就是这样的知识。本文是《架构整洁之道》的读书心得,作者将书中内容拆解后再组织,不仅加入了个人的独到见解,而且用一张详细的知识脉络图帮助大家了解整本书的精华。如果你读过这本书,可以将本文当做一次思想交流,如果你还没看过这本书,更要阅读这篇文章,相信你会得到不同于以往的启发。 本篇文章我们将从软件系统的价值出发,首先认识架构工作的价值和目标, 接下来依次了解架构设计的基础、指导思想(设计原则)、组件拆分的方法和粒度、组件之间依赖设计、组件边界多种解耦方式以及取舍、降低组件之间通信成本的方法,从而最终指导我们做出正确的架构决策和架构设计。 一、软件系统的价值架构是软件系统的一部分,所以要明白架构的价值,首先要明确软件系统的价值。软件系统的价值有两方面,行为价值和架构价值。 行为价值是软件的核心价值,包括需求的实现,以及可用性保障(功能性 bug 、性能、稳定性)。这几乎占据了我们90%的工作内容,支撑业务先赢是我们工程师的首要责任。如果业务是明确的、稳定的,架构的价值就可以忽略不计,但业务通常是不明确的、飞速发展的,这时架构就无比重要,因为架构的价值就是让我们的软件(Software)更软(Soft)。可以从两方面理解: 当需求变更时,所需的软件变更必须简单方便。变更实施的难度应该和变更的范畴(scope)成等比,而与变更的具体形状(shape)无关。当我们只关注行为价值,不关注架构价值时,会发生什么事情?这是书中记录的一个真实案例,随着版本迭代,工程师团队的规模持续增长,但总代码行数却趋于稳定,相对应的,每行代码的变更成本升高、工程师的生产效率降低。从老板的视角,就是公司的成本增长迅猛,如果营收跟不上就要开始赔钱啦。 可见架构价值重要性,接下来从著名的紧急重要矩阵出发,看我们如何处理好行为价值和架构价值的关系。 重要紧急矩阵中,做事的顺序是这样的:1.重要且紧急 > 2.重要不紧急 > 3.不重要但紧急 > 4.不重要且不紧急。实现行为价值的需求通常是 PD 提出的,都比较紧急,但并不总是特别重要;架构价值的工作内容,通常是开发同学提出的,都很重要但基本不是很紧急,短期内不做也死不了。所以行为价值的事情落在1和3(重要且紧急、不重要但紧急),而架构价值落在2(重要不紧急)。我们开发同学,在低头敲代码之前,一定要把杂糅在一起的1和3分开,把我们架构工作插进去。 二、架构工作的目标前面讲解了架构价值,追求架构价值就是架构工作的目标,说白了,就是用最少的人力成本满足构建和维护该系统的需求,再细致一些,就是支撑软件系统的全生命周期,让系统便于理解、易于修改、方便维护、轻松部署。对于生命周期里的每个环节,优秀的架构都有不同的追求: 开发阶段:组件不要使用大量复杂的脚手架;不同团队负责不同的组件,避免不必要的协作。部署阶段:部署工作不要依赖成堆的脚本和配置文件;组件越多部署工作越繁重,而部署工作本身是没有价值的,做的越少越好,所以要减少组件数量。运行阶段:架构设计要考虑到不同的吞吐量、不同的响应时长要求;架构应起到揭示系统运行的作用:用例、功能、行为设置应该都是对开发者可见的一级实体,以类、函数或模块的形式占据明显位置,命名能清晰地描述对应的功能。维护阶段:减少探秘成本和风险。探秘成本是对现有软件系统的挖掘工作,确定新功能或修复问题的最佳位置和方式。风险是做改动时,可能衍生出新的问题。三、编程范式其实所谓架构就是限制,限制源码放在哪里、限制依赖、限制通信的方式,但这些限制比较上层。编程范式是最基础的限制,它限制我们的控制流和数据流:结构化编程限制了控制权的直接转移,面向对象编程限制了控制权的间接转移,函数式编程限制了赋值,相信你看到这里一定一脸懵逼,啥叫控制权的直接转移,啥叫控制权的间接转移,不要着急,后边详细讲解。 这三个编程范式最近的一个也有半个世纪的历史了,半个世纪以来没有提出新的编程范式,以后可能也不会了。因为编程范式的意义在于限制,限制了控制权转移限制了数据赋值,其他也没啥可限制的了。很有意思的是,这三个编程范式提出的时间顺序可能与大家的直觉相反,从前到后的顺序为:函数式编程(1936年)、面向对象编程(1966年)、结构化编程(1968年)。 1.结构化编程 结构化编程证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序,并限制了 goto 的使用。遵守结构化编程,工程师就可以像数学家一样对自己的程序进行推理证明,用代码将一些已证明可用的结构串联起来,只要自行证明这些额外代码是确定的,就可以推导出整个程序的正确性。 前面提到结构化编程对控制权的直接转移进行了限制,其实就是限制了 goto 语句。什么叫做控制权的直接转移?就是函数调用或者 goto 语句,代码在原来的流程里不继续执行了,转而去执行别的代码,并且你指明了执行什么代码。为什么要限制 goto 语句?因为 goto 语句的一些用法会导致某个模块无法被递归拆分成更小的、可证明的单元。而采用分解法将大型问题拆分正是结构化编程的核心价值。 其实遵守结构化编程,工程师们也无法像数学家那样证明自己的程序是正确的,只能像物理学家一样,说自己的程序暂时没被证伪(没被找到bug)。数学公式和物理公式的最大区别,就是数学公式可被证明,而物理公式无法被证明,只要目前的实验数据没把它证伪,我们就认为它是正确的。程序也是一样,所有的 test case 都通过了,没发现问题,我们就认为这段程序是正确的。 2.面向对象编程 面向对象编程包括封装、继承和多态,从架构的角度,这里只关注多态。多态让我们更方便、安全地通过函数调用的方式进行组件间通信,它也是依赖反转(让依赖与控制流方向相反)的基础。 在非面向对象的编程语言中,我们如何在互相解耦的组件间实现函数调用?答案是函数指针。比如采用C语言编写的操作系统中,定义了如下的结构体来解耦具体的IO设备, IO 设备的驱动程序只需要把函数指针指到自己的实现就可以了。 struct FILE { void (*open)(char* name, int mode); void (*close)(); int (*read)(); void (*write)(char); void (*seek)(long index, int mode);}这种通过函数指针进行组件间通信的方式非常脆弱,工程师必须严格按照约定初始化函数指针,并严格地按照约定来调用这些指针,只要一个人没有遵守约定,整个程序都会产生极其难以跟踪和消除的 Bug。所以面向对象编程限制了函数指针的使用,通过接口-实现、抽象类-继承等多态的方式来替代。 前面提到面向对象编程对控制权的间接转移进行了限制,其实就是限制了函数指针的使用。什么叫做控制权的间接转移?就是代码在原来的流程里不继续执行了,转而去执行别的代码,但具体执行了啥代码你也不知道,你只调了个函数指针或者接口。 ...

May 14, 2019 · 1 min · jiezi

开发函数计算的正确姿势支持-ES6-语法和-webpack-压缩

首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考。Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考。2.0 版本的 Fun,在部署这一块做了很多努力,并提供了比较完善的功能,能够做到将云资源方便、平滑地部署到云端。但该版本,在本地开发上的体验,还有较多的工作要做。于是,我们决定推出 Fun Init 弥补这一处短板。Fun Init: Fun Init 作为 Fun 的一个子命令存在,只要 Fun 的版本大于等于 2.7.0,即可以直接通过 fun init 命令使用。Fun Init 工具可以根据指定的模板快速的创建函数计算应用,快速体验和开发函数计算相关业务。官方会提供常用的模板,用户也可以自定自己的模板。背景阿里云函数计算是事件驱动的全托管计算服务。通过函数计算,您无需管理服务器等基础设施,只需编写代码并上传。函数计算会为您准备好计算资源,以弹性、可靠的方式运行您的代码,并提供日志查询、性能监控、报警等功能。借助于函数计算,您可以快速构建任何类型的应用和服务,无需管理和运维。而且,您只需要为代码实际运行所消耗的资源付费,代码未运行则不产生费用。 当我们写 nodejs 函数时,函数往往会依赖很多第三方依赖,这样导致函数代码少则几十兆,多则上百兆。代码包太大,会有如下问题: 可能会导致没法成功上传代码到函数计算服务,因为函数计算服务对代码包大小是有限制的,压缩后最大不能超过 50 MB,解压后最大不能超过 250 MB会导致冷启动时间是变大,因为下载代码的过程变大了每次更新代码时间变大另外,函数计算目前只支持 nodejs8 和 nodejs6 这两个版本,这两版本不支持 es6 语法,但是我们可能已经写习惯了 es6 语法该怎么办呢? 熟悉 nodejs 的同学应该知道,项目工程化管理工具 webpack,我们完全可以通过 webpack 将 es6 代码编译成 es5,并且剪切打包压缩成一个 js 文件,然后将该 js 文件上传到函数计算中运行。 快速开始我这里提供了一个 fun 模板,帮助快速搭建一个函数计算 nodejs 项目骨架,支持 es6 代码编译成 es5,并且剪切打包压缩成一个 js 文件,然后将该 js 文件上传到函数计算中运行。操作作步骤如下: ...

May 10, 2019 · 1 min · jiezi

ES6箭头函数5

0.为什么会出现箭头函数? 1.传统的javascript函数语法并没有提供任何的灵活性,每一次你需要定义一个函数时,你都必须输入function () {},这至少会出现两个问题,ES6箭头函数都圆满解决了它, 第一个问题:代码输入快了容易输错成 funciton或者functoin或者其它,但是=>这个玩意你要是再写错就只能说有点过分了。 第二个问题:节省大量代码,我们先不用管下面的ES6代码为什么这样的语法能实现同样的功能,我们就直观的感受一下代码量。 ES5写法: function addFive(num){ return num+5; }alert(addFive(10));ES6写法: var addFive = num=>num+5;alert(addFive(5));没有function、没有return,没有(),没有{},这些全变成了浮云,世界好清静。 从上面我们就可以看到,使用箭头函数不仅仅能够避免错误,同时还能让我们少一丢丢代码,当然实际工作中远比这个代码量节省更多。一方面是因为积累效应,每一部分少一丢丢合起来就多了,一方面是它还有更能节省代码和大幅提高工作效率的场景。 接下来我们就说说今天的主角--箭头函数。 ES6标准新增了一种新的函数:Arrow Function(箭头函数),也称“胖箭头函数”, 允许 使用“箭头”(=>)定义函数,是一种简写的函数表达式。 1、箭头函数语法 在ES5中我们实现一个求和的函数: var sum = function(x, y) { return x + y}要使用箭头函数,可以分两步实现同样的函数功能: 首先使用=>来替代关键词function var sum = (x, y) => { return x + y}这个特性非常好!!! 前面我们已经说过用=>来替代关键词function就意味着不会写错function了,这真是一个绝妙的设计思想! 其次,函数体只有一条返回语句时, 我们可以省略括号{}和return关键词: var sum = (x, y) => x + y再夸张一点点,如果只有一个参数时,()可省略。 这是箭头函数最简洁的形式,常用于作用简单的处理函数,比如过滤: // ES5var array = ['1', '2345', '567', '89'];array = array.filter(function (item) { return item.length > 2;});// ["2345", "567"]// ES6let array = ['1', '2345', '567', '89'];array = array.filter(item => item.length > 2); // ["2345", "567"]箭头函数的主要使用模式如下: ...

May 7, 2019 · 3 min · jiezi

基于TensorFlowjs的JavaScript机器学习

虽然python或r编程语言有一个相对容易的学习曲线,但是Web开发人员更喜欢在他们舒适的javascript区域内做事情。目前来看,node.js已经开始向每个领域应用javascript,在这一大趋势下我们需要理解并使用JS进行机器学习。由于可用的软件包数量众多,python变得流行起来,但是JS社区也紧随其后。这篇文章会帮助初学者学习如何构建一个简单的分类器。 扩展:2019年11个javascript机器学习库 很棒的机器学习库,可以在你的下一个应用程序中添加一些人工智能!Big.bitsrc.io 创建我们可以创建一个使用tensorflow.js在浏览器中训练模型的网页。考虑到房屋的“avgareanumberofrows”,模型可以学习去预测房屋的“价格”。 为此我们要做的是: 加载数据并为培训做好准备。 定义模型的体系结构。 训练模型并在训练时监控其性能。 通过做出一些预测来评估经过训练的模型。 第一步:让我们从基础开始 创建一个HTML页面并包含JavaScript。将以下代码复制到名为index.html的HTML文件中。 <!DOCTYPE html><html><head> <title>TensorFlow.js Tutorial</title> <!-- Import TensorFlow.js --> <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script> <!-- Import tfjs-vis --> <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script> <!-- Import the main script file --> <script src="script.js"></script></head><body></body></html>为代码创建javascript文件 在与上面的HTML文件相同的文件夹中,创建一个名为script.js的文件,并将以下代码放入其中。 console.log('Hello TensorFlow');测试 既然已经创建了HTML和JavaScript文件,那么就测试一下它们。在浏览器中打开index.html文件并打开devtools控制台。 如果一切正常,那么应该在devtools控制台中创建并可用两个全局变量: tf是对tensorflow.js库的引用tfvis是对tfjs vis库的引用现在你应该可以看到一条消息,上面写着“Hello TensorFlow”。如果是这样,你就可以继续下一步了。 注意:可以使用Bit来共享可重用的JS代码 Bit(GitHub上的Bit)是跨项目和应用程序共享可重用JavaScript代码的最快和最可扩展的方式。可以试一试,它是免费的: 组件发现与协作·Bit Bit是开发人员共享组件和协作,共同构建令人惊叹的软件的地方。发现共享的组件…Bit.dev 例如:Ramda用作共享组件 Ramda by Ramda·Bit 一个用于JavaScript程序员的实用函数库。-256个javascript组件。例如:等号,乘…Bit.dev 第2步:加载数据,格式化数据并可视化输入数据 我们将加载“house”数据集,可以在这里找到。它包含了特定房子的许多不同特征。对于本教程,我们只需要有关房间平均面积和每套房子价格的数据。 将以下代码添加到script.js文件中。 async function getData() { Const houseDataReq=awaitfetch('https://raw.githubusercontent.com/meetnandu05/ml1/master/house.json'); const houseData = await houseDataReq.json(); const cleaned = houseData.map(house => ({ price: house.Price, rooms: house.AvgAreaNumberofRooms, })) .filter(house => (house.price != null && house.rooms != null)); return cleaned;}这可以删除没有定义价格或房间数量的任何条目。我们可以将这些数据绘制成散点图,看看它是什么样子的。 ...

April 28, 2019 · 4 min · jiezi

AutoML数据增广

DeepAugment是一个专注于数据扩充的自动化工具。 它利用贝叶斯优化来发现针对您的图像数据集定制的数据增强策略。 DeepAugment的主要优点和特点是: 降低CNN模型的错误率(WRN-28-10显示CIFAR10的错误率降低了60%)通过自动化流程可以节省时间比谷歌之前的解决方案——AutoAugment——快50倍完成的包在PyPI上。你可以通过运行以下命令来在终端上安装它: $ pip install deepaugment你也可以访问项目的自述文件或运行谷歌Colab笔记本教程。要想了解更多关于我是如何构建这个的,请继续阅读! 引言数据是人工智能应用中最关键的部分。没有足够的标记数据常常导致过度拟合,这意味着模型将无法归纳为未发现的示例。这可以通过数据扩充来缓解,数据扩充可以有效地增加网络所看到的数据的数量和多样性。它是通过对原始数据集(如旋转、裁剪、遮挡等)应用转换,人为地生成新数据来实现的。然而,确定哪种增强对手头的数据集最有效并不是一项简单的任务。为了解决这个问题,谷歌去年发布了AutoAugment,它通过使用强化学习发现了给定数据集的优化增强。由于强化学习模块的存在,使用谷歌的AutoAugment需要强大的计算资源。由于获得所需的计算能力代价高昂,因此我开发了一种新的方法——DeepAugment,它使用贝叶斯优化而不是强化学习。 如何获得更好的数据努力改进数据质量通常比努力改进模型获得更高的投资回报。改进数据有三种主要方法:收集更多的数据、合成新数据或扩展现有数据。收集额外的数据并不总是可行的,而且可能很昂贵。GANs所做的数据合成是很有前途的,但也很复杂,可能与实际的例子有所不同。 另一方面,数据扩充简单且影响很大。它适用于大多数数据集,并通过简单的图像转换完成。然而,问题是确定哪种增强技术最适合当前的数据集。发现正确的方法需要耗时的实验。即使经过多次实验,机器学习(ML)工程师仍然可能找不到最佳选择。对于每个图像数据集,有效的增强策略是不同的,一些增强技术甚至可能对模型有害。例如,如果使用MNIST digits数据集,应用旋转会使模型变得更糟,因为在“6”上180度旋转会使它看起来像“9”,而仍然被标记为“6”。另一方面,对卫星图像应用旋转可以显著改善结果,因为无论旋转多少次,从空中拍摄的汽车图像仍然是一辆汽车。 DeepAugment:闪电般迅速的autoMLDeepAugment旨在作为一种快速灵活的autoML数据扩充解决方案。更具体地说,它被设计为AutoAugment的更快和更灵活的替代品。(2018年Cubuk等人的博客)AutoAugment是2018年最令人兴奋的发布之一,也是第一种使用强化学习来解决这一特定问题的方法。在本文发表时,AutoAugment的开源版本没有提供控制器模块,这阻碍了用户为自己的数据集使用它。此外,学习增强策略需要15,000次迭代,这需要巨大的计算资源。即使源代码完全可用,大多数人也无法从中受益。deepaugmented通过以下设计目标来解决这些问题:1.在保证结果质量的前提下,最小化数据扩充优化的计算复杂度。2.模块化和人性化。为了实现第一个目标,与AutoAugment相比,DeepAugment的设计具有以下差异: 使用贝叶斯优化代替强化学习(需要更少的迭代)(~100x加速)最小化子模型大小(降低每次训练的计算复杂度)(~20x加速)减少随机扩充搜索空间设计(减少所需的迭代次数)为了实现第二个目标,即使DeepAugment模块化和人性化,用户界面的设计方式为用户提供了广泛的可能性配置和模型选择(例如,选择子模型或输入自设计的子模型,请参阅配置选项)。 设计扩充策略DeepAugment旨在为给定的图像数据集找到最佳的扩充策略。增强策略被定义为五个子策略的总和,这两个子策略由两种类型的增强技术和两个实值[0,1]组成,决定了每种增强技术的应用能力。我使用imgaug包实现了增强技术,imgaug包以其大量的增强技术(见下文)而闻名。 当多样化和随机应用时,增强是最有效的。例如,与其旋转每个图像,不如旋转图像的某些部分,剪切另一部分,然后对另一部分应用颜色反转。基于这一观察,Deepaugment对图像随机应用五个子策略之一(包括两个增强)。优化过程中,每个图像被五个子策略之一增强的概率(16%)相等,而完全不被增强的概率为20%。虽然这个策略设计受到了autoaugmented的启发,但有一个主要的区别:我没有使用任何参数来应用子策略的概率,以便使策略的随机性更低,并允许在更少的迭代中进行优化。 这个策略设计为贝叶斯优化器创建了一个20维的搜索空间,其中10个维度是分类(增强技术的类型),其他10个维度是实值(大小)。由于涉及到分类值,我将贝叶斯优化器配置为使用随机森林估计器。 DeepAugment如何找到最佳策略DeepAugment的三个主要组件是控制器(贝叶斯优化器),增强器和子模型,整个工作流程如下:控制器采样新的增强策略,增强器按新策略转换图像,子模型是通过增强图像从头开始训练。根据子模型的训练历史计算奖励。奖励返回给控制器,控制器使用此奖励和相关的增强策略更新代理模型(请参阅下面的“贝叶斯优化如何工作”一节)。然后控制器再次采样新策略,并重复相同的步骤。此过程循环,直到达到用户确定的最大迭代次数。控制器(贝叶斯优化器)是使用scikit- optimization库的ask-and-tell方法实现的。它被配置为使用一个随机森林估计器作为其基本估计器,并期望改进作为其获取函数。 贝叶斯优化是如何工作的贝叶斯优化的目的是找到一组最大化目标函数值的参数。 贝叶斯优化的工作循环可以概括为:1.建立目标函数的代理模型2.查找代理上执行得最好的参数3.使用这些参数执行目标函数4.使用这些参数和目标函数的得分更新代理模型5.重复步骤2-4,直到达到最大迭代次数有关贝叶斯优化的更多信息,请阅读高级的这篇解释的博客,或者看一下这篇综述文章。 贝叶斯优化的二维描述,其中x和y轴表示增强类型,点(i,j)处的颜色表示用增强i和j所增强的数据进行训练时CNN模型的精度。 贝叶斯优化的权衡目前用于超参数优化的标准方法有随机搜索、网格搜索、贝叶斯优化、进化算法和强化学习,按方法复杂度排序。在超参数优化的精度、成本和计算时间方面,贝叶斯优化优于网格搜索和随机搜索(参见这里的经验比较)。这是因为贝叶斯优化从先前参数的运行中学习,与网格搜索和随机搜索相反。当贝叶斯优化与强化学习和进化算法进行比较时,它提供了具有竞争力的准确性,同时需要更少的迭代。例如,为了学习好的策略,谷歌的AutoAugment迭代15,000次(这意味着训练子CNN模型15,000次)。另一方面,贝叶斯优化在100-300次迭代中学习良好的策略。贝叶斯优化的经验法则是使迭代次数等于优化参数的次数乘以10。 挑战及对策挑战1:优化增强需要大量的计算资源,因为子模型应该从头开始反复训练。大大减慢了我的工具的开发过程。 尽管使用贝叶斯优化使其更快,但优化过程仍然不够快,无法使开发变得可行。对策:我开发了两种解决方案。首先,我优化了子CNN模型(见下图),这是该过程的计算瓶颈。其次,我以更确定的方式设计了增强策略,使贝叶斯优化器需要更少的迭代。 设计子CNN模型。它在AWS p3.2x大型实例(带有112 TensorFLOPS的Tesla V100 GPU)上以32x32图像在约30秒(120个周期)的时间内完成培训。 挑战2:我在DeepAugment的开发过程中遇到了一个有趣的问题。在通过一遍又一遍地训练子模型来优化增强期间,它们开始过度拟合验证集。当我更改验证集时,我发现的最佳策略表现不佳。这是一个有趣的例子,因为它不同于一般意义上的过度拟合,即模型权重过度拟合数据中的噪声。对策:我没有使用相同的验证集,而是将剩余的数据和训练数据保留为“种子验证集”,并在每次子CNN模型训练时对1000个图像的验证集进行采样(参见下面的数据管道)。这解决了增强过度拟合问题。 如何集成到ML pipeline中DeepAugment发布在PyPI上。你可以通过运行以下命令来在终端安装它: $ pip install deepaugment并且使用方便: from deepaugment.deepaugment import DeepAugmentdeepaug = DeepAugment(my_images, my_labels)best_policies = deepaug.optimize()通过配置DeepAugment,可以获得更高级的用法: from keras.datasets import cifar10# my configurationmy_config = { "model": "basiccnn", "method": "bayesian_optimization", "train_set_size": 2000, "opt_samples": 3, "opt_last_n_epochs": 3, "opt_initial_points": 10, "child_epochs": 50, "child_first_train_epochs": 0, "child_batch_size": 64}(x_train, y_train), (x_test, y_test) = cifar10.load_data()# X_train.shape -> (N, M, M, 3)# y_train.shape -> (N)deepaug = DeepAugment(x_train, y_train, config=my_config)best_policies = deepaug.optimize(300)有关更详细的安装/使用信息,请访问项目的自述文件或运行Google Colab笔记本教程。 ...

April 28, 2019 · 1 min · jiezi

JavaScript中的浅拷贝与深拷贝

上一篇 JavaScript中的继承前言文章开始之前,让我们先思考一下这几个问题: 为什么会有浅拷贝与深拷贝什么是浅拷贝与深拷贝如何实现浅拷贝与深拷贝好了,问题出来了,那么下面就让我们带着这几个问题去探究一下吧! 如果文章中有出现纰漏、错误之处,还请看到的小伙伴多多指教,先行谢过 以下↓ 数据类型在开始了解 浅拷贝 与 深拷贝 之前,让我们先来回顾一下 JavaScript 的数据类型(可以参考这里 JavaScript中的数据类型) 在 JavaScript 中,我们将数据分为 基本数据类型(原始值) 与 引用类型 基本数据类型的值是按值访问的,基本类型的值是不可变的引用类型的值是按引用访问的,引用类型的值是动态可变的由于数据类型的访问方式不同,它们的比较方式也是不一样的 var a = 100;var b = 100;a === b // truevar c = {a: 1, b: 2};var d = {a: 1, b: 2};c == d // false 两个不同的对象基本数据类型的比较是值得比较引用类型的比较是引用地址的比较鉴于以上数据类型的特点,我们可以初步想到:所谓 浅拷贝 与 深拷贝 可能就是对于值的拷贝和引用的拷贝(简单数据类型都是对值的拷贝,不进行区分) 一般来说,我们所涉及的拷贝对象,也都是针对引用类型的。由于引用类型属性层级可能也会有多层,这样也就引出了我们所要去了解的 浅拷贝 与 深拷贝 浅拷贝顾名思义,所谓浅拷贝就是对对象进行浅层次的复制,只复制一层对象的属性,并不包括对象里面的引用类型数据 想象一下,如果让你自己去实现这个功能,又会有怎么的思路呢 首先,我们需要知道被拷贝对象有哪些属性吧,然后还需要知道这些属性都对应了那些值或者地址的引用吧。那么,答案已经呼之欲出了,是的,循环 var person = { name: 'tt', age: 18, friends: ['oo', 'cc', 'yy']}function shallowCopy(source) { if (!source || typeof source !== 'object') { throw new Error('error'); } var targetObj = source.constructor === Array ? [] : {}; for (var keys in source) { if (source.hasOwnProperty(keys)) { targetObj[keys] = source[keys]; } } return targetObj;}var p1 = shallowCopy(person);console.log(p1)在上面的代码中,我们创建了一个 shallowCopy 函数,它接收一个参数也就是被拷贝的对象。 ...

April 26, 2019 · 2 min · jiezi

云原生的新思考为什么容器已经无处不在了

4月24日,中国信息通信研究院主办的首届云原生产业大会在北京举行,在《云原生数字引领未来》的主题演讲中,阿里云容器服务总监易立表示:“云原生不但可以很好的支持互联网应用,也在深刻影响着新的计算架构、新的智能数据应用。以容器、服务网格、微服务、Serverless为代表的云原生技术,带来一种全新的方式来构建应用。”本文根据易立演讲内容整理而成。 拥抱云原生技术,解耦系统复杂度如今,大多数企业开始全面拥抱云计算,在All-in-Cloud全面到来的时代,三个重要转变:基础设施的云化、核心技术的互联网化、业务的数据化和智能化。在各行各业中,都有很多业务应用从诞生之初就生长在云端,各个企业也因此越来越像互联网公司,而技术能力被视为不可或缺的核心竞争力。在2019阿里云峰会·北京站上,阿里云智能总裁张建锋在谈及‘核心技术的互联网化’时,也提到了大力投资云原生。 为什么要拥抱云原生?一方面,云计算已经重塑了软件的整个生命周期,从架构设计到开发,再到构建、交付和运维等所有环节;另一方面,企业IT架构也随之发生巨大变化,而业务又深度依赖IT能力。这带来了一定程度的复杂性和挑战性。 正如人类社会发展伴随着技术革命与社会大分工一样,云原生技术的出现解耦了很多复杂性,这是IT技术的进步。 首先,Docker实现了应用与运行环境的解耦,众多业务应用负载都可以被容器化,而且应用容器化满足了敏捷、可迁移、标准化的诉求;其次,Kubernetes的出现让资源编排调度与底层基础设施解耦,应用和资源的管控也开始得心应手,容器编排实现资源编排、高效调度;随后,Istio为代表的服务网格技术解耦了服务实现与服务治理能力。此外,阿里云还提供了Open API、SDK等丰富的开发工具,实现第三方被集成,为云的生态伙伴提供广阔的可能性。这样的技术分层推动了社会分工,极大促进了技术和业务创新。 在阿里云看来,云原生首先可以支持互联网规模应用,可以更加快速地创新、和低成本试错;其次,屏蔽了底层基础架构的差异和复杂性;同时,服务网格、无服务计算等新的计算范型的不断涌现,给整体IT架构能力带来了极致弹性,从而更好地服务于业务。用户可以基于阿里云容器服务构建面向领域的云原生框架,如面向机器学习的Kubeflow,和面向无服务器的Knative等等。 方兴未艾,容器应用的新思考容器已经无处不在了, 作为容器服务的提供者,我们认为容器技术会继续发展,被应用于“新的计算形态”,“新的应用负载”和“新的物理边界”,在此将相关观察和新思考分享给大家。 1 新的计算形态:云原生的Serverless Runtime已来 云原生技术理念,是使企业用户及开发者只关注应用开发,无需关注基础设施及基础服务。与之相似的Serverless计算,将应用服务资源化并以API接口的方式提供出来,使用者只需从客户端发起调用请求即可,更重要的是,pay as you go 能够真正为用户节省成本。 Serverless Runtime 分为面向基础架构容器的实现,面向应用服务封装的实现,和事件驱动面向函数计算的实现。 云原生Serverless Runtime形态包含多种方式。业界各个厂商也相应地设计出了不同服务解决方案: 面向函数的Function as a Service(FaaS) - 比如AWS Lambda,阿里云的函数计算,提供了事件驱动的编程方式,用户只需提供函数实现响应触发实践,开发效率很高。阿里云函数计算按照调用量计费,可以根据业务流量平滑调整计算资源,在典型场景下,会有10%~90%的成本下降。客户码隆科技做模型预测,利用函数计算降低了40%的计算成本。面向应用 - 比如Google App Engine、新发布的Cloud Run和阿里云EDAS Serverless,用户只需提供应用实现而平台负责应用弹性、自动化运维,这主要面向互联网类型应用。相比于FaaS,面向应用的Serverless形态无需改造现有应用,阿里云EDAS Serverless为流行的开源微服务框架提供了无服务器应用托管平台,支持Spring Cloud,Apache Dubbo,或者阿里云HSF框架。面向容器 – 比如AWS fargate,或者是阿里云的Serverless Kubernetes 应用的载体是容器镜像,灵活性很好,配合调度系统可以支持各种类型应用,而无需管理底层基础架构。针对容器化应用,阿里云在去年5月推出了Serverless Kubernetes容器服务,无需节点管理和容量规划,按应用所需资源付费,弹性扩容。针对阿里云基础能力优化,安全,高效。极大降低了管理Kubernetes集群的。Serverless Kubernetes的底层是构建在阿里云针对容器优化的轻量虚拟化弹性容器实例之上,提供了轻量、高效、安全的容器应用执行环境。Serverless Kubernetes无需修改即可部署容器类型应用。 2 新的应用负载:容器正被用于越来越多类型应用 最早容器被认为不适合传统的已有应用,但是现在状况已大为改观。容器已经开启了对Windows生态的支持,新发布的1.14版本中Kubernetes 的Pod,Service,应用编排,CNI 网络等绝大多数核心能力都已经在 Windows 节点上得到了支持。当今Windows系统依然占有60%的份额,比如企业的ERP软件、基于ASP的应用、大量的Windows的数据库等,这些传统的基于虚拟化的应用,都可以在代码不用重写的情况下实现容器化。 基于容器技术构建的新架构,会催生新的应用业务价值。云原生AI是非常重要的应用场景,快速搭建AI环境,高效利用底层资源,无缝配合深度学习的全生命周期。对于AI工程,云原生系统可以在四个维度上为提效: 优化异构资源调度弹性、高效、细粒度(支持GPU共享)简化异构资源管理复杂性,提升可观测性和使用效率可移植、可组装、可重现的AI流程以深度学习分布式训练为例,通过阿里云容器服务可以获得三重加强。资源优化:统一调度CPU/GPU等异构资源,使用VPC/RDMA网络加速;性能提升:GPU 64卡P100,加速比提升90%,相比原生Tensorflow有45%提升;算法优化:MPI代替gRPC通信、ring-allreduce环形通信、计算和通信重叠、梯度融合等。 还有其他高性能计算的场景,以基因数据处理为例,阿里云某用户在5小时内完成WGS 100GB数据处理,支持5000+步骤的复杂流程, 90秒实现500节点扩容充分发挥容器极致弹性。 3 新的物理边界:云-边-端,容器不止运行在IDC服务器中 容器最被熟知的基础环境是数据中心,在业务流量高峰与低谷之时,凭借容器极致弹性可以实现应用与资源伸缩,有效地保证高利用率与高性价比。 随着5G和物联网时代的到来,传统云计算中心集中存储、计算的模式已经无法满足终端设备对于时效、容量、算力的需求。将云计算的能力下沉到边缘侧、设备侧,并通过中心进行统一交付、运维、管控,将是云计算的重要发展趋势。以Kubernetes为基础的云原生技术,在任何基础设施上提供与云一致的功能和体验,实现云-边-端一体化的应用分发, 支持不同系统架构和网络状况下,应用的分发和生命周期管理,并且针对边缘及设备进行如访问协议、同步机制、安全机制的种种优化。 如前所述,应用容器化实现了标准化的可移植性,促成了敏捷弹性的云原生应用架构。不仅大大简化了多云/混合云的部署,而且优化成本,同时提供更多的选择,比如满足安全合规的要求、提升业务敏捷性、提升地域覆盖性等等。 容器可以适用于多种基础环境,比如数据中心、边缘云、和多云/混合云,使得开发者关注回归到应用本身。 写在最后云原生时代,是开发者最好的时代。 云原生不但可以很好的支持互联网应用,也在深刻影响着新的计算架构、新的智能数据应用。以容器、服务网格、微服务、Serverless为代表的云原生技术,带来一种全新的方式来构建应用。此外,云原生也在拓展云计算的边界,一方面是多云、混合云推动无边界云计算,一方面云边端的协同。 ...

April 26, 2019 · 1 min · jiezi

Kubernetes从懵圈到熟练读懂这一篇集群节点不下线

排查完全陌生的问题,完全不熟悉的系统组件,是售后工程师的一大工作乐趣,当然也是挑战。今天借这篇文章,跟大家分析一例这样的问题。排查过程中,需要理解一些自己完全陌生的组件,比如systemd和dbus。但是排查问题的思路和方法基本上还是可以复用了,希望对大家有所帮助。 问题一直在发生I'm NotReady 阿里云有自己的Kubernetes容器集群产品。随着Kubernetes集群出货量的剧增,线上用户零星的发现,集群会非常低概率地出现节点NotReady情况。据我们观察,这个问题差不多每个月,就会有一两个客户遇到。在节点NotReady之后,集群Master没有办法对这个节点做任何控制,比如下发新的Pod,再比如抓取节点上正在运行Pod的实时信息。 需要知道的Kubernetes知识 这里我稍微补充一点Kubernetes集群的基本知识。Kubernetes集群的“硬件基础”,是以单机形态存在的集群节点。这些节点可以是物理机,也可以是虚拟机。集群节点分为Master和Worker节点。Master节点主要用来负载集群管控组件,比如调度器和控制器。而Worker节点主要用来跑业务。Kubelet是跑在各个节点上的代理,它负责与管控组件沟通,并按照管控组件的指示,直接管理Worker节点。 当集群节点进入NotReady状态的时候,我们需要做的第一件事情,肯定是检查运行在节点上的kubelet是否正常。在这个问题出现的时候,使用systemctl命令查看kubelet状态,发现它作为systemd管理的一个daemon,是运行正常的。当我们用journalctl查看kubelet日志的时候,发现下边的错误。 什么是PLEG 这个报错很清楚的告诉我们,容器runtime是不工作的,且PLEG是不健康的。这里容器runtime指的就是docker daemon。Kubelet通过直接操作docker daemon来控制容器的生命周期。而这里的PLEG,指的是pod lifecycle event generator。PLEG是kubelet用来检查容器runtime的健康检查机制。这件事情本来可以由kubelet使用polling的方式来做。但是polling有其成本上的缺陷,所以PLEG应用而生。PLEG尝试以一种“中断”的形式,来实现对容器runtime的健康检查,虽然实际上,它同时用了polling和”中断”两种机制。 基本上看到上边的报错,我们可以确认,容器runtime出了问题。在有问题的节点上,通过docker命令尝试运行新的容器,命令会没有响应。这说明上边的报错是准确的. 容器runtimeDocker Daemon调用栈分析 Docker作为阿里云Kubernetes集群使用的容器runtime,在1.11之后,被拆分成了多个组件以适应OCI标准。拆分之后,其包括docker daemon,containerd,containerd-shim以及runC。组件containerd负责集群节点上容器的生命周期管理,并向上为docker daemon提供gRPC接口。 在这个问题中,既然PLEG认为容器运行是出了问题,我们需要先从docker daemon进程看起。我们可以使用kill -USR1 <pid>命令发送USR1信号给docker daemon,而docker daemon收到信号之后,会把其所有线程调用栈输出到文件/var/run/docker文件夹里。 Docker daemon进程的调用栈相对是比较容易分析的。稍微留意,我们会发现大多数的调用栈都类似下图中的样子。通过观察栈上每个函数的名字,以及函数所在的文件(模块)名称,我们可以看到,这个调用栈下半部分,是进程接到http请求,做请求路由的过程;而上半部分则进入实际的处理函数。最终处理函数进入等待状态,等待的是一个mutex实例。 到这里,我们需要稍微看一下ContainerInspectCurrent这个函数的实现,而最重要的是,我们能搞明白,这个函数的第一个参数,就是mutex的指针。使用这个指针搜索整个调用栈文件,我们会找出,所有等在这个mutex上边的线程。同时,我们可以看到下边这个线程。 这个线程上,函数ContainerExecStart也是在处理具体请求的时候,收到了这个mutex这个参数。但不同的是,ContainerExecStart并没有在等待mutex,而是已经拿到了mutex的所有权,并把执行逻辑转向了containerd调用。关于这一点,我们可以使用代码来验证。前边我们提到过,containerd向上通过gRPC对docker daemon提供接口。此调用栈上半部分内容,正是docker daemon在通过gRPC请求来呼叫containerd。 Containerd调用栈分析 与输出docker daemon的调用栈类似,我们可以通过kill -SIGUSR1 <pid>命令来输出containerd的调用栈。不同的是,这次调用栈会直接输出到messages日志。 Containerd作为一个gRPC的服务器,它会在接到docker daemon的远程请求之后,新建一个线程去处理这次请求。关于gRPC的细节,我们这里其实不用关注太多。在这次请求的客户端调用栈上,可以看到这次调用的核心函数是Start一个进程。我们在containerd的调用栈里搜索Start,Process以及process.go等字段,很容易发现下边这个线程。 这个线程的核心任务,就是依靠runC去创建容器进程。而在容器启动之后,runC进程会退出。所以下一步,我们自然而然会想到,runC是不是有顺利完成自己的任务。查看进程列表,我们会发现,系统中有个别runC进程,还在执行,这不是预期内的行为。容器的启动,跟进程的启动,耗时应该是差不对的,系统里有正在运行的runC进程,则说明runC不能正常启动容器。 什么是DbusRunC请求Dbus 容器runtime的runC命令,是libcontainer的一个简单的封装。这个工具可以用来管理单个容器,比如容器创建,或者容器删除。在上节的最后,我们发现runC不能完成创建容器的任务。我们可以把对应的进程杀掉,然后在命令行用同样的命令尝试启动容器,同时用strace追踪整个过程。 分析发现,runC停在了向带有org.free字段的dbus写数据的地方。那什么是dbus呢?在Linux上,dbus是一种进程间进行消息通信的机制。 原因并不在Dbus 我们可以使用busctl命令列出系统现有的所有bus。如下图,在问题发生的时候,我看到客户集群节点Name的编号非常大。所以我倾向于认为,dbus某些相关的数据结构,比如Name,耗尽了引起了这个问题。 Dbus机制的实现,依赖于一个组件叫做dbus-daemon。如果真的是dbus相关数据结构耗尽,那么重启这个daemon,应该是可以解决这个问题。但不幸的是,问题并没有这么直接。重启dbus-daemon之后,问题依然存在。 在上边用strace追踪runC的截图中,我提到了,runC卡在向带有org.free字段的bus写数据的地方。在busctl输出的bus列表里,显然带有这个字段的bus,都在被systemd使用。这时,我们用systemctl daemon-reexec来重启systemd,问题消失了。所以基本上我们可以判断一个方向:问题可能跟systemd有关系。 Systemd是硬骨头Systemd是相当复杂的一个组件,尤其对没有做过相关开发工作的同学来说,比如我自己。基本上,排查systemd的问题,我用到了四个方法,(调试级别)日志,core dump,代码分析,以及live debugging。其中第一个,第三个和第四个结合起来使用,让我在经过几天的鏖战之后,找到了问题的原因。但是这里我们先从“没用”的core dump说起。 没用的Core Dump 因为重启systemd解决了问题,而这个问题本身,是runC在使用dbus和systemd通信的时候没有了响应,所以我们需要验证的第一件事情,就是systemd不是有关键线程被锁住了。查看core dump里所有线程,只有以下一个线程,此线程并没有被锁住,它在等待dbus事件,以便做出响应。 ...

April 23, 2019 · 1 min · jiezi

从虚拟化前端Bug学习分析Kernel-Dump

前言也许大家都知道,分析 Kernel Dump 有个常用的工具叫 Crash,在我刚开始学习分析 Kernel Dump 的时候,总是花大量的时间折腾这个工具的用法,却总是记不住这个工具的功能。后来有一次在参加某次内部分享的时候,有位大佬说了一句话让我印象非常深刻:这些工具怎么用的大家不用记,等到真正开始用的时候你就会猜到这个工具有什么功能。这篇文章我想通过分析一个实际的案例,尽量把学习Kernel Dump需要用到的知识串起来,虽然某些知识也许只会在这个案例中用到,但是我相信所用到的方法是可以应用到各个地方的。 起线上有一台 VM 宕机了,刚好有抓到 dump,拿到一台测试机上就可以开始分析了。首先需要的是 kernel 版本对应的 symbol,如果事先不知道 kernel 的版本,可以通过 `strings corefile | grep "Linux version"' 获取到当前 corefile 的 kernel 版本,例如 3.10.0-862.14.4.el7.x86_64 在获取到内核版本之后,根据相应的发行版以及系统架构到特定的 symbol 发布页面下载 symbol,这里的发行版是 Centos,可以到 http://debuginfo.centos.org/ 下载。如果是 Ubuntu 发行版,可以到 http://ddebs.ubuntu.com/ 下载。要找到指定 kernel 版本的 symbol 很简单,只需要拿着 kernel 版本 3.10.0-862.14.4.el7.x86_64 搜一下就能找到了,通常我们需要的 symbol 的只有下面这三个中的两个,但是我总是记不住是哪两个,所以我会把三个都下载下来并安装:kernel-debug-debuginfo-3.10.0-862.14.4.el7.x86_64.rpm、kernel-debuginfo-3.10.0-862.14.4.el7.x86_64.rpm、kernel-debuginfo-common-x86_64-3.10.0-862.14.4.el7.x86_64.rpm。在安装的时候由于依赖的关系需要先安装 common 的 symbol 才能安装其它 symbol,另外如果测试机上的 kernel 版本比 corefile 的版本新,需要加上 --force 选项才能安装上。 承在 symbol 安装完之后,就可以通过 crash 载入 corefile 和 symbol 了。 ...

April 23, 2019 · 6 min · jiezi

Fish Redux中的Dispatch是怎么实现的?

零、前言我们在使用fish-redux构建应用的时候,界面代码(view)和事件的处理逻辑(reducer,effect)是完全解耦的,界面需要处理事件的时候将action分发给对应的事件处理逻辑去进行处理,而这个分发的过程就是下面要讲的dispatch, 通过本篇的内容,你可以更深刻的理解一个action是如何一步步去进行分发的。一、从example开始为了更好的理解action的dispatch过程,我们就先以todo_list_page中一条todo条目的勾选事件为例,来看点击后事件的传递过程,通过断点debug我们很容易就能够发现点击时候发生的一切,具体过程如下:用户点击勾选框,GestureDetector的onTap会被回调通过buildView传入的dispatch函数对doneAction进行分发,发现todo_component的effect中无法处理此doneAction,所以将其交给pageStore的dispatch继续进行分发pageStore的dispatch会将action交给reducer进行处理,故doneAction对应的_markDone会被执行,对state进行clone,并修改clone后的state的状态,然后将这个全新的state返回然后pageStore的dispatch会通知所有的listeners,其中负责界面重绘的_viewUpdater发现state发生变化,通知界面进行重绘更新二、Dispatch实现分析Dispatch在实现的过程中借鉴了Elm。Dispatch在fish-redux中的定义如下typedef Dispatch = void Function(Action action);本质上就是一个action的处理函数,接受一个action,然后对action进行分发。下面我门通过源码来进行详细的分析1.component中的dispatchbuildView函数传入的dispatch是对应的component的mainCtx中的dispatch,_mainCtx和componet的关系如下component -> ComponentWidget -> ComponentState -> _mainCtx -> _dispatch而 _mainCtx的初始化则是通过componet的createContext方法来创建的,顺着方法下去我们看到了dispatch的初始化// redux_component/context.dart DefaultContext初始化方法 DefaultContext({ @required this.factors, @required this.store, @required BuildContext buildContext, @required this.getState, }) : assert(factors != null), assert(store != null), assert(buildContext != null), assert(getState != null), _buildContext = buildContext { final OnAction onAction = factors.createHandlerOnAction(this); /// create Dispatch _dispatch = factors.createDispatch(onAction, this, store.dispatch); /// Register inter-component broadcast _onBroadcast = factors.createHandlerOnBroadcast(onAction, this, store.dispatch); registerOnDisposed(store.registerReceiver(_onBroadcast)); }context中的dispatch是通过factors来进行创建的,factors其实就是当前component,factors创建dispatch的时候传入了onAction函数,以及context自己和store的dispatch。onAction主要是进行Effect处理。这边还可以看到,进行context初始化的最后,还将自己的onAction包装注册到store的广播中去,这样就可以接收到别人发出的action广播。Component继承自Logic// redux_component/logic.dart @override Dispatch createDispatch( OnAction onAction, Context<T> ctx, Dispatch parentDispatch) { Dispatch dispatch = (Action action) { throw Exception( ‘Dispatching while appending your effect & onError to dispatch is not allowed.’); }; /// attach to store.dispatch dispatch = _applyOnAction<T>(onAction, ctx)( dispatch: (Action action) => dispatch(action), getState: () => ctx.state, )(parentDispatch); return dispatch; } static Middleware<T> _applyOnAction<T>(OnAction onAction, Context<T> ctx) { return ({Dispatch dispatch, Get<T> getState}) { return (Dispatch next) { return (Action action) { final Object result = onAction?.call(action); if (result != null && result != false) { return; } //skip-lifecycle-actions if (action.type is Lifecycle) { return; } if (!shouldBeInterruptedBeforeReducer(action)) { ctx.pageBroadcast(action); } next(action); }; }; }; }}上面分发的逻辑大概可以通过上图来表示通过onAction将action交给component对应的effect进行处理当effect无法处理此action,且此action非lifecycle-actions,且不需中断则广播给当前Page的其余所有effects最后就是继续将action分发给store的dispatch(parentDispatch传入的其实就是store.dispatch)2. store中的dispatch从store的创建代码我们可以看到store的dispatch的具体逻辑// redux/create_store.dart final Dispatch dispatch = (Action action) { _throwIfNot(action != null, ‘Expected the action to be non-null value.’); _throwIfNot( action.type != null, ‘Expected the action.type to be non-null value.’); _throwIfNot(!isDispatching, ‘Reducers may not dispatch actions.’); try { isDispatching = true; state = reducer(state, action); } finally { isDispatching = false; } final List<_VoidCallback> _notifyListeners = listeners.toList( growable: false, ); for (_VoidCallback listener in _notifyListeners) { listener(); } notifyController.add(state); };store的dispatch过程比较简单,主要就是进行reducer的调用,处理完成后通知监听者。3.middlewarePage继承自Component,增加了middleware机制,fish-redux的redux部分本身其实就对middleware做了支持,可以通过StoreEnhancer的方式将middlewares进行组装,合并到Store的dispatch函数中。middleware机制可以允许我们通过中间件的方式对redux的state做AOP处理,比如fish-redux自带的logMiddleware,可以对state的变化进行log,分别打印出state变化前和变化后的值。当Page配置了middleware之后,在创建pageStore的过程中会将配置的middleware传入,传入之后会对store的dispath进行增强加工,将middleware的处理函数串联到dispatch中。// redux_component/component.dart Widget buildPage(P param) { return wrapper(_PageWidget<T>( component: this, storeBuilder: () => createPageStore<T>( initState(param), reducer, applyMiddleware<T>(buildMiddleware(middleware)), ), )); }// redux_component/page_store.dartPageStore<T> createPageStore<T>(T preloadedState, Reducer<T> reducer, [StoreEnhancer<T> enhancer]) => _PageStore<T>(createStore(preloadedState, reducer, enhancer));// redux/create_store.dartStore<T> createStore<T>(T preloadedState, Reducer<T> reducer, [StoreEnhancer<T> enhancer]) => enhancer != null ? enhancer(_createStore)(preloadedState, reducer) : _createStore(preloadedState, reducer);所以这里可以看到,当传入enhancer时,createStore的工作被enhancer代理了,会返回一个经过enhancer处理过的store。而PageStore创建的时候传入的是中间件的enhancer。// redux/apply_middleware.dartStoreEnhancer<T> applyMiddleware<T>(List<Middleware<T>> middleware) { return middleware == null || middleware.isEmpty ? null : (StoreCreator<T> creator) => (T initState, Reducer<T> reducer) { assert(middleware != null && middleware.isNotEmpty); final Store<T> store = creator(initState, reducer); final Dispatch initialValue = store.dispatch; store.dispatch = (Action action) { throw Exception( ‘Dispatching while constructing your middleware is not allowed. ’ ‘Other middleware would not be applied to this dispatch.’); }; store.dispatch = middleware .map((Middleware<T> middleware) => middleware( dispatch: (Action action) => store.dispatch(action), getState: store.getState, )) .fold( initialValue, (Dispatch previousValue, Dispatch Function(Dispatch) element) => element(previousValue), ); return store; };}这里的逻辑其实就是将所有的middleware的处理函数都串到store的dispatch,这样当store进行dispatch的时候所有的中间件的处理函数也会被调用。下面为各个处理函数的执行顺序,首先还是component中的dispatch D1 会被执行,然后传递给store的dispatch,而此时store的dispatch已经经过中间件的增强,所以会执行中间件的处理函数,最终store的原始dispatch函数D2会被执行。三、总结通过上面的内容,现在我们可以知道一个action是如何一步步的派送给effect,reducer去进行处理的,我们也可以通过middleware的方式去跟踪state的变化,这样的扩展性给框架本身带来无限可能。本文作者:闲鱼技术-卢克阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 17, 2019 · 2 min · jiezi

如何把创建ECS(CreateInstance)作为触发器来触发函数计算

摘要: 函数计算是阿里云上类似lamda的服务。 本文介绍了如何通过日志服务投递ECS创建等行为的日志,从而触发函数计算服务。问题描述函数计算虽然不支持直接集成到ECS的管控事件上,但是函数计算本身是支持日志服务作为触发器的。即可以配置日志服务中logstore里的增强日志作为触发器来触发函数计算服务中的函数,同时可以传递project 和 logstore的name以及beginCursor/endCursor 等相关日志信息作为event到函数计算服务,供其做二次处理和加工。这样相当于提供了一个思路,即我们可以把创建ECS或者其他相关的操作想办法作为日志投递到日志服务中,这样就可以触发相关的函数计算服务了。那么这种方法是什么呢?一种可行的方式是操作审计服务。操作审计可以记录所有API级别的用户记录,当然也包括CreateInstance这类操作。所以整个流程就变成了:开通操作审计服务->配置操作审计跟踪,将event投递到日志服务中->配置日志服务作为函数计算触发器并传递日志->触发函数举个栗子开通操作审计服务后,创建一个日志跟踪然后创建一个实例,可以看到操作审计记录了这个行为同时日志服务里也找到了这个行为记录接下来我们可以配置一个函数计算服务,具体的过程可以参考文中最后的文档,这里强调下配置触发器的配置,这里要注意的是图中有关logstore的配置,上面的是触发日志的logstore,下面的是写日志的lostore,不能搞混。然后复制进去一段代码,这段代码的核心是拿到触发event的具体日志信息,然后写到函数计算本地的日志库里。# -- coding: utf-8 --import loggingimport jsonfrom aliyun.log import LogClientfrom time import timedef logClient(endpoint, creds): logger = logging.getLogger() logger.info(‘creds info’) logger.info(creds.access_key_id) logger.info(creds.access_key_secret) logger.info(creds.security_token) accessKeyId = ‘XXX’ accessKey = ‘XXX’ client = LogClient(endpoint, accessKeyId, accessKey) return clientdef handler(event, context): logger = logging.getLogger() logger.info(‘start deal SLS data’) logger.info(event.decode().encode()) info_arr = json.loads(event.decode()) fetchdata(info_arr[‘source’],context) return ‘hello world’def fetchdata(event,context): logger = logging.getLogger() endpoint = event[’endpoint’] creds = context.credentials client = logClient(endpoint, creds) if client == None : logger.info(“client creat failed”) return False project = event[‘projectName’] logstore = event[’logstoreName’] start_cursor = event[‘beginCursor’] end_cursor = event[’endCursor’] loggroup_count = 10 shard_id = event[‘shardId’] while True: res = client.pull_logs(project, logstore, shard_id, start_cursor, loggroup_count, end_cursor) res.log_print() next_cursor = res.get_next_cursor() if next_cursor == start_cursor : break start_cursor = next_cursor #log_data = res.get_loggroup_json_list() return True以上配置完成后,一个控制台创建ECS(当然也包括其他可以被审计的行为)的行为就可以用来触发函数计算的函数了。结果我们把刚才创建的实例再释放掉,看到操作审计的日志然后我们在函数计算的日志库里也看到了对应的日志,这个日志是刚才操作审计记录的日志传递给函数计算并记录的。在真正的应用场景下,客户可以拿到这个日志中的相关信息做更多操作。更多sample可以参考:https://github.com/aliyun/aliyun-log-fc-functions总结产品侧无法直接支持的功能,可以看下是否有workaround很多阿里云产品之间的集成,都可以看下是否可以通过日志服务来做。参考资料https://help.aliyun.com/document_detail/84090.html?spm=a2c4g.11174283.6.619.5a6552120001Hlhttps://help.aliyun.com/document_detail/60781.html?spm=a2c4g.11186623.2.12.62727c9fIeDQwY本文作者:拱卒阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 15, 2019 · 1 min · jiezi

Node.js 应用故障排查手册 —— 类死循环导致进程阻塞

楔子在实践篇一中我们看到了两个表象都是和 CPU 相关的生产问题,它们基本也是我们在线上可能遇到的这一类问题的典型案例,而实际上这两个案例也存在一个共同点:我们可以通过 Node.js 性能平台 导出进程对应的 CPU Profile 信息来进行分析定位问题,但是实际在线上的一些极端情况下,我们遇到的故障是没有办法通过轻量的 V8 引擎暴露的 CPU Profile 接口(仅部分定制的 AliNode runtime 版本支持,详见下文)来获取足够的进程状态信息进行分析的,此时我们又回到了束手无策的状态。本章节将从一个生产环境下 Node.js 应用出现进程级别阻塞导致的不再提供服务的问题场景来给大家展示下如何处理这类相对极端的应用故障。本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。最小化复现代码这个例子稍微有些特殊,我们首先给出生产案例的最小化复现代码,有兴趣的同学可以亲自运行一番,这样结合下文的此类问题的排查过程,能更加清晰的看到我们面对这样的问题时的排查思路,问题最小代码如下,基于 Egg.js :‘use strict’;const Controller = require(’egg’).Controller;class RegexpController extends Controller { async long() { const { ctx } = this; // str 模拟用户输入的问题字符串 let str = ‘<br/> ’ + ’ 早餐后自由活动,于指定时间集合自行办理退房手续。’; str += ‘<br/> <br/>’ + ’ <br/> ’ + ’ <br/>’; str += ’ <br/>’ + ’ ’ + ’ ’ + ’ <br/>’; str += ’ <br/> <br/>’; str += ’ ’ + ’ ’ + ’ 根据船班时间,自行前往暹粒机场,返回中国。<br/>’; str += ‘如需送机服务,需增加280/每单。<br/>’; const r = str.replace(/(^(\s*?<br[\s/]?>*?)+|(\s?<br[\s/]?>\s?)+?$)/igm, ‘’); ctx.body = r; }}module.exports = RegexpController;问题应用状态其实这个例子对应的问题场景可能很多 Node.js 开发者都遇到过,它非常有意思,我们首先来看下出现这类故障时我们的 Node.js 应用的状态。当我们收到在平台配置的 CPU 告警信息后,登录性能平台进入对应的告警应用找到出问题的 CPU 非常高的进程:然后点击 数据趋势 按钮查看此进程当前的状态信息:可以看到进程的 CPU 使用率曲线一直处于近乎 100% 的状态,此时进程不再响应其余的请求,而且我们通过跳板机进入生产环境又可以看到进程其实是存活的,并没有挂掉,此时基本上可以判断:此 Node.js 进程因为在执行某个同步函数处于阻塞状态,且一直卡在此同步函数的执行上。Node.js 的设计运行模式就是单主线程,并发靠的是底层实现的一整套异步 I/O 和事件循环的调度。简单的说,具体到事件循环中的某一次,如果我们在执行需要很长时间的同步函数(比如需要循环执行很久才能跳出的 while 循环),那么整个事件循环都会阻塞在这里等待其结束后才能进入下一次,这就是不推荐大家在非初始化的逻辑中使用诸如 fs.readFileSync 等同步方法的原因。排查方法这样的问题其实非常难以排查,原因在于我们没办法知道什么样的用户输入造成了这样的阻塞,所以本地几乎无法复现问题。幸运的是,性能平台目前有不止一种解决办法处理这种类死循环的问题,我们来详细看下。I. CPU Profile这个分析方法可以说是我们的老朋友了,因为类死循环的问题本质上也是 CPU 高的问题,因此我们只要对问题进程抓取 CPU Profile,就能看到当前卡在哪个函数了。需要注意的是,进程假死状态下是无法直接使用 V8 引擎提供的抓取 CPU Profile 文件的接口,因此工具篇章节的 正确打开 Chrome devtools 一节中提到的 v8-profiler 这样的第三方模块是无法正常工作的。不过定制过的 AliNode runtime 采用了一定的方法规避了这个问题,然而遗憾的是依旧并不是所有的 AliNode runtime 版本都支持在类死循环状态下抓取 CPU Profile,这里实际上对大家使用的 Runtime 版本有要求:AliNode V3 版本需要 >= v3.11.4AliNode V4 版本需要 >= v4.2.1AliNode V1 和 V2 版本不支持如果你的线上 AliNode runtime 版本恰好符合需求,那么可以按照前面 Node.js 性能平台使用指南 提到的那样,对问题进程抓取 3 分钟的 CPU Profile,并且使用 AliNode 定制的火焰图分析:这里可以看到,抓取到的问题进程 3 分钟的 CPU 全部耗费在 long 函数里面的 replace 方法上,这和我们提供的最小化复现代码一致,因此可以判断 long 函数内的正则存在问题进行修复。II. 诊断报告诊断报告也是 AliNode 定制的一项导出更多更详细的 Node.js 进程当前状态的能力,导出的信息也包含当前的 JavaScript 代码执行栈以及一些其它进程与系统信息。它与 CPU Profile 的区别主要在两个地方:诊断报告主要针对此刻进程状态的导出,CPU Profile 则是一段时间内的 JavaScript 代码执行状态诊断报告除了此刻 JavaScript 调用栈信息,还包含了 Native C/C++ 栈信息、Libuv 句柄和部分操作系统信息当我们的进程处于假死状态时,显然不管是一段时间内还是此时此刻的 JavaScript 执行状况,必然都是卡在我们代码中的某个函数上,因此我们可以使用诊断报告来处理这样的问题,当然诊断报告功能同样也对 AliNode runtime 版本有所要求:AliNode V2 版本需要 >= v2.5.2AliNode V3 版本需要 >= v3.11.8AliNode V4 版本需要 >= v4.3.0AliNode V1 版本不支持且要求:Agenthub/Egg-alinode 依赖的 Commandx 版本 >= v1.5.3如果你使用的 AliNode runtime 版本符合要求,即可进入平台应用对应的实例信息页面,选中问题进程:然后点击 诊断报告 即可生成此刻问题进程的状态信息报告:诊断报告虽然包含了很多的进程和系统信息,但是其本身是一个相对轻量的操作,故而很快就会结束,此时继续点击 转储 按钮将生成的诊断报告上传至云端以供在线分析展示:继续点击 分析 按钮查看 AliNode 定制的分析功能,展示结果如下:结果页面上面的概览信息比较简单,我们来看下 JavaScript 栈 页面的内容,这里显然也告诉我们当前的 JS 函数卡在 long 方法里面,并且比 CPU Profile 更加详细的是还带上了具体阻塞在 long 方法的哪一行,对比我们提供给大家的最小复现代码其实就是执行 str.replace 这一行,也就是问题的正则匹配操作所在的地方。III. 核心转储分析其实很多朋友看到这里会有疑惑:既然 CPU Profile 分析和诊断报告已经能够找到问题所在了,为什么我们还要继续介绍相对比较重的核心转储分析功能呢?其实道理也很简单,不管是类死循环状态下的 CPU Profile 抓取还是诊断报告功能的使用,都对问题进程的 AliNode runtime 版本有所要求,而且更重要的是,这两种方法我们都只能获取到问题正则的代码位置,但是我们无法知道什么样的用户输入在执行这样的正则时会触发进程阻塞的问题,这会给我们分析和给出针对性的处理造成困扰。因此,这里最后给大家介绍对 AliNode runtime 版本没有任何要求,且能拿到更精准信息的核心转储分析功能。首先按照预备章节的核心转储一节中提到的 手动生成 Core dump 文件的方法,我们对问题进程进行 sudo gcore <pid> 的方式获取到核心转储文件,然后在平台的详情页面,将鼠标移动到左边 Tab 栏目的 文件 按钮上,可以看到 Coredump 文件 的按钮:点击后可以进入 Core dump 文件列表页,然后点击上方的 上传 按钮进行核心转储文件的上传操作:这里需要注意的是,请将 Core dump 文件以 .core 结尾重命名,而对应的 Node 可执行文件以 .node 结尾重命名,推荐的命名方式为 <os info>-<alinode/node>-<version>.node,方便以后回顾,比如 centos7-alinode-v4.7.2.node 这种。最后 Core dump 文件和 Node 可执行文件之间必须是 一一对应 的关系。这里一一对应指的是:这份 Core dump 文件必须是由这个 Node 可执行文件启动的进程生成的,如果这两者没有一一对应,分析结果往往是无效信息。因为 Core dump 文件一般来说都比较大,所以上传会比较慢,耐心等待至上传完毕后,我们就可以使用 AliNode 定制的核心转储文件分析功能进行分析了,点击 分析 按钮即可:此时我们在新打开的分析结果页面可以看到如下的分析结果展示信息:这个页面的各项含义在工具篇的 Node.js 性能平台使用指南的 最佳实践——核心转储分析 一节已经解释过,这里不再赘述,这里直接展开 JavaScript 栈信息:这里可以看到得到的结论和前面的 CPU Profile 分析以及诊断报告分析一致,都能定位到提供的最小复现代码中的 long 方法中的异常正则匹配,但是核心转储文件分析比前面两者多了导致当前 Node.js 进程产生问题的异常字符串: "<br/> 早餐后自由活动,于指定时间集合自行办理退房手续。<br/> <br/> <br/> <br/> <br/> <br/> <br/> <br/> 根据船班时间,自行前往暹粒机场,返回中国。<br/>如需送机服务,需增加280/每单。<br/>" ,有了这个触发正则执行异常的问题字符串,我们无论是构造本地复现样例还是进一步分析都有了重要的信息依靠。分析问题上一节中我们采用了 Node.js 性能平台提供的三种不同的方式分析定位到了线上应用处于假死状态的原因,这里来简单的解释下为什么字符串的正则匹配会造成类死循环的状态,它实际上异常的用户输入触发了 正则表达式灾难性的回溯,会导致执行时间要耗费几年甚至几十年,显然不管是那种情况,单主工作线程的模型会导致我们的 Node.js 应用处于假死状态,即进程依旧存活,但是却不再处理新的请求。关于正则回溯的原因有兴趣的同学可以参见 小心别落入正则回溯陷阱 一文。结尾其实这类正则回溯引发的进程级别阻塞问题,本质上都是由于不可控的用户输入引发的,而 Node.js 应用又往往作为 Web 应用直接面向一线客户,无时不刻地处理千奇百怪的用户请求,因此更容易触发这样的问题。相似的问题其实还有一些代码逻辑中诸如 while 循环的跳出条件在一些情况下失效,导致 Node.js 应用阻塞在循环中。之前我们就算知道是进程阻塞也难以方便的定位到具体的问题代码以及产生问题的输入,现在借助于 Node.js 性能平台 提供的核心转储分析能力,相信大家可以比较容易地来解决这样的问题。本文作者:奕钧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 15, 2019 · 2 min · jiezi

手把手教程:用Python开发一个自然语言处理模型,并用Flask进行部署

摘要: 实用性教程!教你如何快速创建一个可用的机器学习程序!截住到目前为止,我们已经开发了许多机器学习模型,对测试数据进行了数值预测,并测试了结果。实际上,生成预测只是机器学习项目的一部分,尽管它是我认为最重要的部分。今天我们来创建一个用于文档分类、垃圾过滤的自然语言处理模型,使用机器学习来检测垃圾短信文本消息。我们的ML系统工作流程如下:离线训练->将模型作为服务提供->在线预测。1、通过垃圾邮件和非垃圾邮件训练离线分类器。2、经过训练的模型被部署为服务用户的服务。当我们开发机器学习模型时,我们需要考虑如何部署它,即如何使这个模型可供其他用户使用。Kaggle和数据科学训练营非常适合学习如何构建和优化模型,但他们并没有教会工程师如何将它们带给其他用户使用,建立模型与实际为人们提供产品和服务之间存在重大差异。在本文中,我们将重点关注:构建垃圾短信分类的机器学习模型,然后使用Flask(用于构建Web应用程序的Python微框架)为模型创建API。此API允许用户通过HTTP请求利用预测功能。让我们开始吧!构建ML模型数据是标记为垃圾邮件或正常邮件的SMS消息的集合,可在此处找到。首先,我们将使用此数据集构建预测模型,以准确分类哪些文本是垃圾邮件。朴素贝叶斯分类器是一种流行的电子邮件过滤统计技术。他们通常使用词袋功能来识别垃圾邮件。因此,我们将使用Naive Bayes定理构建一个简单的消息分类器。import pandas as pdimport numpy as npfrom sklearn.feature_extraction.text import CountVectorizerfrom sklearn.model_selection import train_test_splitfrom sklearn.naive_bayes import MultinomialNBfrom sklearn.metrics import classification_reportdf = pd.read_csv(‘spam.csv’, encoding=“latin-1”)df.drop([‘Unnamed: 2’, ‘Unnamed: 3’, ‘Unnamed: 4’], axis=1, inplace=True)df[’label’] = df[‘class’].map({‘ham’: 0, ‘spam’: 1})X = df[‘message’]y = df[’label’]cv = CountVectorizer()X = cv.fit_transform(X) # Fit the DataX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)#Naive Bayes Classifierclf = MultinomialNB()clf.fit(X_train,y_train)clf.score(X_test,y_test)y_pred = clf.predict(X_test)print(classification_report(y_test, y_pred))Naive Bayes分类器不仅易于实现,而且提供了非常好的性能。在训练模型之后,我们都希望有一种方法来保持模型以供将来使用而无需重新训练。为实现此目的,我们添加以下行以将我们的模型保存为.pkl文件供以后使用。from sklearn.externals import joblibjoblib.dump(clf, ‘NB_spam_model.pkl’)我们加载并使用保存的模型:NB_spam_model = open(‘NB_spam_model.pkl’,‘rb’)clf = joblib.load(NB_spam_model)上述过程称为“标准格式的持久模型”,即模型以特定的开发语言的特定格式持久存储。下一步就是将模型在一个微服务中提供,该服务的公开端点用来接收来自客户端的请求。将垃圾邮件分类器转换为Web应用程序在上一节中准备好用于对SMS消息进行分类的代码之后,我们将开发一个Web应用程序,该应用程序由一个简单的Web页面组成,该页面具有允许我们输入消息的表单字段。在将消息提交给Web应用程序后,它将在新页面上呈现该消息,从而为我们提供是否为垃圾邮件的结果。首先,我们为这个项目创建一个名为SMS-Message-Spam-Detector 的文件夹,这是该文件夹中的目录树,接下来我们将解释每个文件。spam.csvapp.pytemplates/ home.html result.htmlstatic/ style.css子目录templates是Flask在Web浏览器中查找静态HTML文件的目录,在我们的例子中,我们有两个html文件:home.html和result.html 。app.pyapp.py文件包含将由Python解释器执行以运行Flask Web应用程序的主代码,还包含用于对SMS消息进行分类的ML代码:from flask import Flask,render_template,url_for,requestimport pandas as pd import picklefrom sklearn.feature_extraction.text import CountVectorizerfrom sklearn.naive_bayes import MultinomialNBfrom sklearn.externals import joblibapp = Flask(name)@app.route(’/’)def home(): return render_template(‘home.html’)@app.route(’/predict’,methods=[‘POST’])def predict(): df= pd.read_csv(“spam.csv”, encoding=“latin-1”) df.drop([‘Unnamed: 2’, ‘Unnamed: 3’, ‘Unnamed: 4’], axis=1, inplace=True) # Features and Labels df[’label’] = df[‘class’].map({‘ham’: 0, ‘spam’: 1}) X = df[‘message’] y = df[’label’] # Extract Feature With CountVectorizer cv = CountVectorizer() X = cv.fit_transform(X) # Fit the Data from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42) #Naive Bayes Classifier from sklearn.naive_bayes import MultinomialNB clf = MultinomialNB() clf.fit(X_train,y_train) clf.score(X_test,y_test) #Alternative Usage of Saved Model # joblib.dump(clf, ‘NB_spam_model.pkl’) # NB_spam_model = open(‘NB_spam_model.pkl’,‘rb’) # clf = joblib.load(NB_spam_model) if request.method == ‘POST’: message = request.form[‘message’] data = [message] vect = cv.transform(data).toarray() my_prediction = clf.predict(vect) return render_template(‘result.html’,prediction = my_prediction)if name == ‘main’: app.run(debug=True)1、我们将应用程序作为单个模块运行,因此我们使用参数初始化了一个新的Flask实例,__name__是为了让Flask知道它可以在templates所在的同一目录中找到HTML模板文件夹()。2、接下来,我们使用route decorator(@app.route(’/’))来指定可以触发home 函数执行的URL 。我们的home 函数只是呈现home.htmlHTML文件,该文件位于templates文件夹中。3、在predict函数内部,我们访问垃圾邮件数据集、预处理文本、进行预测,然后存储模型。我们访问用户输入的新消息,并使用我们的模型对其标签进行预测。4、我们使用该POST方法将表单数据传输到邮件正文中的服务器。最后,通过debug=True在app.run方法中设置参数,进一步激活Flask的调试器。5、最后,我们使用run函数执行在服务器上的脚本文件,我们需要确保使用if语句 name == ‘main’。home.html以下是home.html将呈现文本表单的文件的内容,用户可以在其中输入消息:<!DOCTYPE html><html><head> <title>Home</title> <!– <link rel=“stylesheet” type=“text/css” href="../static/css/styles.css"> –> <link rel=“stylesheet” type=“text/css” href="{{ url_for(‘static’, filename=‘css/styles.css’) }}"></head><body> <header> <div class=“container”> <div id=“brandname”> Machine Learning App with Flask </div> <h2>Spam Detector For SMS Messages</h2> </div> </header> <div class=“ml-container”> <form action="{{ url_for(‘predict’)}}" method=“POST”> <p>Enter Your Message Here</p> <!– <input type=“text” name=“comment”/> –> <textarea name=“message” rows=“4” cols=“50”></textarea> <br/> <input type=“submit” class=“btn-info” value=“predict”> </form> </div></body></html>view rawstyle.css文件在home.html的head部分,我们将加载styles.css文件,CSS文件是用于确定HTML文档的外观和风格的。styles.css必须保存在一个名为的子目录中static,这是Flask查找静态文件(如CSS)的默认目录。body{ font:15px/1.5 Arial, Helvetica,sans-serif; padding: 0px; background-color:#f4f3f3;}.container{ width:100%; margin: auto; overflow: hidden;}header{ background:#03A9F4;#35434a; border-bottom:#448AFF 3px solid; height:120px; width:100%; padding-top:30px;}.main-header{ text-align:center; background-color: blue; height:100px; width:100%; margin:0px; }#brandname{ float:left; font-size:30px; color: #fff; margin: 10px;}header h2{ text-align:center; color:#fff;}.btn-info {background-color: #2196F3; height:40px; width:100px;} /* Blue */.btn-info:hover {background: #0b7dda;}.resultss{ border-radius: 15px 50px; background: #345fe4; padding: 20px; width: 200px; height: 150px;}result.html我们创建一个result.html文件,该文件将通过函数render_template(‘result.html’, prediction=my_prediction)返回呈现predict,我们在app.py脚本中定义该文件以显示用户通过文本字段提交的文本。result.html文件包含以下内容:<!DOCTYPE html><html><head> <title></title> <link rel=“stylesheet” type=“text/css” href="{{ url_for(‘static’, filename=‘css/styles.css’) }}"></head><body> <header> <div class=“container”> <div id=“brandname”> ML App </div> <h2>Spam Detector For SMS Messages</h2> </div> </header> <p style=“color:blue;font-size:20;text-align: center;"><b>Results for Comment</b></p> <div class=“results”> {% if prediction == 1%} <h2 style=“color:red;">Spam</h2> {% elif prediction == 0%} <h2 style=“color:blue;">Not a Spam (It is a Ham)</h2> {% endif %} </div></body></html>从result.htm文件我们可以看到一些代码使用通常在HTML文件中找不到的语法例如,{% if prediction ==1%},{% elif prediction == 0%},{% endif %}这是jinja语法,它用于访问从HTML文件中请求返回的预测。我们就要大功告成了!完成上述所有操作后,你可以通过双击appy.py 或从终端执行命令来开始运行API :cd SMS-Message-Spam-Detectorpython app.py你应该得到以下输出:现在你可以打开Web浏览器并导航到http://127.0.0.1:5000/,你应该看到一个简单的网站,内容如下:恭喜!我们现在以零成本的代价创建了端到端机器学习(NLP)应用程序。如果你回顾一下,其实整个过程根本不复杂。有点耐心和渴望学习的动力,任何人都可以做到。所有开源工具都使每件事都成为可能。更重要的是,我们能够将我们对机器学习理论的知识扩展到有用和实用的Web应用程序!完整的工作源代码可在此存储库中找到,祝你度过愉快的一周!本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 9, 2019 · 2 min · jiezi

打通前后端逻辑,客户端Flutter代码一天上线

一、前沿 随着闲鱼的业务快速增长,运营类的需求也越来越多,其中不乏有很多界面修改或运营坑位的需求。闲鱼的版本现在是每2周一个版本,如何快速迭代产品,跳过窗口期来满足这些需求?另外,闲鱼客户端的包体也变的很大,企业包的大小,iOS已经到了94.3M,Android也到了53.5M。Android的包体大小,相比2016年,已经增长了近1倍,怎么能将包体大小降下来?首先想到的是如何动态化的解决此类问题。 对于原生的能力的动态化,Android平台各公司都有很完善的动态化方案,甚至Google还提供了Android App Bundles让开发者们更好地支持动态化。由于Apple官方担忧动态化的风险,因此并不太支持动态化。因此动态化能力就会考虑跟Web结合,从一开始基于 WebView 的 Hybrid 方案 PhoneGap、Titanium,到现在与原生相结合的 React Native 、Weex。 但Native和JavaScript Context之间的通讯,频繁的交互就成了程序的性能瓶颈。于此同时随着闲鱼Flutter技术的推广,已经有10多个页面用Flutter实现,上面提到的几种方式都不适合Flutter场景,如何解决这个问题Flutter的动态化的问题?二、动态方案我们最初调研了Google的动态化方案CodePush。2.1 CodePush CodePush是谷歌官方推出的动态化方案,目前只有在Android上面实现了。Dart VM在执行的时候,加载isolate_snapshot_data 和isolate_snapshot_instr 2个文件,通过动态更改这些文件,就达到动态更新的目的。官方的Flutter源码当中,已经有相关的提交来做动态更新的内容,具体内容可以参考 ResourceExtractor.java。 根据官方给出的Guide,我们这边也做了相关的测试,patch的包体大小会很大(939kb)。为了降低包体大小,还可以通过增量的修改snapshot文件的方式来更新。通过bsdiff生成的snapshot的差异文件,2个文件分别可以缩小到48kb和870kb。 目前看来,CodePush还不能做到很好的工程化。而且如何管理patch文件,需要制定baseline和patch文件的规则。2.2 动态模板 动态模板,就是通过定义一套DSL,在端侧解析动态的创建View来实现动态化,比如LuaViewSDK、Tangram-iOS和Tangram-Android。这些方案都是创建的Native的View,如果想在Flutter里面实现,需要创建Texture来桥接;Native端渲染完成之后,再将纹理贴在Flutter的容器里面,实现成本很高,性能也有待商榷,不适合闲鱼的场景。 所以我们提出了闲鱼自己的Flutter动态化方案,前面已经有同事介绍过方案的原理:《做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现》,下面看下具体的实现细节。三、模板编译自定义一套DSL,维护成本较高,怎么能不自定义DSL来实现模板下发?闲鱼的方案就是直接将Dart文件转化成模板,这样模板文件也可以快速沉淀到端侧。3.1 模板规范 先来看下一个完整的模板文件,以新版我的页面为例,这个是一个列表结构,每个区块都是一个独立的Widget,现在我们期望将“卖在闲鱼”这个区块动态渲染,对这个区块拆分之后,需要3个子控件:头部、菜单栏、提示栏;因为这3部分界面有些逻辑处理,所以先把他们的逻辑内置。内置的子控件分别是MenuTitleWidget、MenuItemWidget和HintItemWidget,编写的模板如下:@overrideWidget build(BuildContext context) { return new Container( child: new Column( children: <Widget>[ new MenuTitleWidget(data), // 头部 new Column( // 菜单栏 children: <Widget>[ new Row( children: <Widget>[ new MenuItemWidget(data.menus[0]), new MenuItemWidget(data.menus[1]), new MenuItemWidget(data.menus[2]), ], ) ], ), new Container( // 提示栏 child: new HintItemWidget(data.hints[0])), ], ), );}中间省略了样式描述,可以看到写模板文件就跟普通的widget写法一样,但是有几点要注意:每个Widget都需要用new或const来修饰数据访问以data开头,数组形式以[]访问,字典形式以.访问 模板写好之后,就要考虑怎么在端上渲染,早期版本是直接在端侧解析文件,但是考虑到性能和稳定性,还是放在前期先编译好,然后下发到端侧。3.2 编译流程 编译模板就要用到Dart的Analyzer库,通过parseCompilationUnit函数直接将Dart源码解析成为以CompilationUnit为Root节点的AST树中,它包含了Dart源文件的语法和语义信息。接下来的目标就是将CompilationUnit转换成为一个JSON格式。 上面的模板解析出来build函数孩子节点是ReturnStatementImpl,它又包含了一个子节点InstanceCreationExpressionImpl,对应模板里面的new Container(…),它的孩子节点中,我们最关心的就是ConstructorNameImpl和ArgumentListImpl节点。ConstructorNameImpl标识创建节点的名称,ArgumentListImpl标识创建参数,参数包含了参数列表和变量参数。定义如下结构体,来存储这些信息:class ConstructorNode { // 创建节点的名称 String constructorName; // 参数列表 List<dynamic> argumentsList = <dynamic>[]; // 变量参数 Map<String, dynamic> arguments = <String, dynamic>{};}递归遍历整棵树,就可以得到一个ConstructorNode树,以下代码是解析单个Node的参数:ArgumentList argumentList = astNode;for (Expression exp in argumentList.arguments) { if (exp is NamedExpression) { NamedExpression namedExp = exp; final String name = ASTUtils.getNodeString(namedExp.name); if (name == ‘children’) { continue; } /// 是函数 if (namedExp.expression is FunctionExpression) { currentNode.arguments[name] = FunctionExpressionParser.parse(namedExp.expression); } else { /// 不是函数 currentNode.arguments[name] = ASTUtils.getNodeString(namedExp.expression); } } else if (exp is PropertyAccess) { PropertyAccess propertyAccess = exp; final String name = ASTUtils.getNodeString(propertyAccess); currentNode.argumentsList.add(name); } else if (exp is StringInterpolation) { StringInterpolation stringInterpolation = exp; final String name = ASTUtils.getNodeString(stringInterpolation); currentNode.argumentsList.add(name); } else if (exp is IntegerLiteral) { final IntegerLiteral integerLiteral = exp; currentNode.argumentsList.add(integerLiteral.value); } else { final String name = ASTUtils.getNodeString(exp); currentNode.argumentsList.add(name); }}端侧拿到这个ConstructorNode节点树之后,就可以根据Widget的名称和参数,来生成一棵Widget树。四、渲染引擎端侧拿到编译好的模板JSON后,就是解析模板并创建Widget。先看下,整个工程的框架和工作流:工作流程:开发人员编写dart文件,编译上传到CDN端侧拿到模板列表,并在端侧存库业务方直接下发对应的模板id和模板数据Flutter侧再通过桥接获取到模板,并创建Widget树对于Native测,主要负责模板的管理,通过桥接输出到Flutter侧。4.1 模板获取模板获取分为2部分,Native部分和Flutter部分;Native主要负责模板的管理,包括下载、降级、缓存等。程序启动的时候,会先获取模板列表,业务方需要自己实现,Native层获取到模板列表会先存储在本地数据库中。Flutter侧业务代码用到模板的时候,再通过桥接获取模板信息,就是我们前面提到的JSON格式的信息,Flutter也会有缓存,已减少Flutter和Native的交互。4.2 Widget创建Flutter侧当拿到JSON格式的,先解析出ConstructorNode树,然后递归创建Widget。创建每个Widget的过程,就是解析节点中的argumentsList和arguments 并做数据绑定。例如,创建HintItemWidget需要传入提示的数据内容,new HintItemWidget(data.hints[0]),在解析argumentsList时,会通过key-path的方式从原始数据中解析出特定的值。解析出来的值都会存储在WidgetCreateParam里面,当递归遍历每个创建节点,每个widget都可以从WidgetCreateParam里面解析出需要的参数。/// 构建widget用的参数class WidgetCreateParam { String constructorName; /// 构建的名称 dynamic context; /// 构建的上下文 Map<String, dynamic> arguments = <String, dynamic>{}; /// 字典参数 List<dynamic> argumentsList = <dynamic>[]; /// 列表参数 dynamic data; /// 原始数据} 通过以上的逻辑,就可以将ConstructorNode树转换为一棵Widget树,再交给Flutter Framework去渲染。至此,我们已经能将模板解析出来,并渲染到界面上,交互事件应该怎么处理?4.3 事件处理在写交互的时候,一般都会通过GestureDector、InkWell等来处理点击事件。交互事件怎么做动态化? 以InkWell组件为例,定义它的onTap函数为openURL(data.hints[0].href, data.hints[0].params)。在创建InkWell时,会以OpenURL作为事件ID,查找对应的处理函数,当用户点击的时候,会解析出对应的参数列表并传递过去,代码如下:…final List<dynamic> tList = <dynamic>[];// 解析出参数列表exp.argumentsList.forEach((dynamic arg) { if (arg is String) { final dynamic value = valueFromPath(arg, param.data); if (value != null) { tList.add(value); } else { tList.add(arg); } } else { tList.add(arg); }});// 找到对应的处理函数final dynamic handler = TeslaEventManager.sharedInstance().eventHandler(exp.actionName);if (handler != null) { handler(tList);}…五、 效果新版我的页面添加了动态化渲染能力之后,如果有需求新添加一种组件类型,就可以直接编译发布模板,服务端下发新的数据内容,就可以渲染出来了;动态化能力有了,大家会关心渲染性能怎么样。5.1 帧率在加了动态加载逻辑之后,已经开放了2个动态卡片,下图是新版本我的页面近半个月的的帧率数据:从上图可以看到,帧率并没有降低,基本保持在55-60帧左右,后续可以多添加动态的卡片,观察下效果。注:因为我的页面会有本地的一些业务判断,从其他页面回到我的tab,都会刷新界面,所以帧率会有损耗。 从实现上分析,因为每个卡片,都需要遍历ConstructorNode树来创建,而且每个构建都需要解析出里面的参数,这块可以做一些优化,比如缓存相同的Widget,只需要映射出数据内容并做数据绑定。5.2 失败率现在监控了渲染的逻辑,如果本地没有对应的Widget创建函数,会主动抛Error。监控数据显示,渲染的流程中,还没有异常的情况,后续还需要对桥接层和native层加错误埋点。六、展望 基于Flutter动态模板,之前需要走发版的Flutter需求,都可以来动态化更改。而且以上逻辑都是基于Flutter原生的体系,学习和维护成本都很低,动态的代码也可以快速的沉淀到端侧。 另外,闲鱼正在研究UI2Code的黑科技,不了解的老铁,可以参考闲鱼大神的这篇文章《重磅系列文章!UI2CODE智能生成Flutter代码——整体设计篇》。可以设想下,如果有个需求,需要动态的显示一个组件,UED出了视觉稿,通过UI2Code转换成Dart文件,再通过这个系统转换成动态模板,下发到端侧就可以直接渲染出来,程序员都不需要写代码了,做到自动化运营,看来以后程序员失业也不是没有可能了。 基于Flutter的Widget,还可以拓展更多个性化的组件,比如内置动画组件,就可以动态化下发动画了,更多好玩的东西等待大家来一起探索。参考文献https://github.com/flutter/flutter/issues/14330https://www.dartlang.org/https://mp.weixin.qq.com/s/4s6MaiuW4VoHr_7f0S_vuQhttps://github.com/flutter/engine本文作者:闲鱼技术-景松阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 4, 2019 · 2 min · jiezi

Node.js 应用故障排查手册 —— 正确打开 Chrome devtools

楔子前面的预备章节中我们大致了解了如何在服务器上的 Node.js 应用出现问题时,从常规的错误日志、系统/进程指标以及兜底的核心转储这些角度来排查问题。这样就引出了下一个问题:我们知道进程的 CPU/Memory 高,或者拿到了进程 Crash 后的核心转储,要如何去进行分析定位到具体的 JavaScript 代码段。其实 Chrome 自带的 Devtools,对于 JavaScript 代码的上述 CPU/Memory 问题有着很好的原生解析展示,本节会给大家做一些实用功能和指标的介绍(基于 Chrome v72,不同的版本间使用方式存在差异)。本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。CPU 飙高问题I. 导出 JS 代码运行状态当我们通过第一节中提到的系统/进程指标排查发现当前的 Node.js 应用的 CPU 特别高时,首先我们需要去通过一些方式将当前 Node.js 应用一段时间内的 JavaScript 代码运行状况 Dump 出来,这样子才能分析知道 CPU 高的原因。幸运的是,V8 引擎内部实现了一个 CPU Profiler 能够帮助我们完成一段时间内 JS 代码运行状态的导出,目前也有不少成熟的模块或者工具来帮我们完成这样的操作。v8-profiler 是一个老牌的 Node.js 应用性能分析工具,它可以很方便地帮助开发者导出 JS 代码地运行状态,我们可以在项目目录执行如下命令安装此模块:npm install v8-profiler –save接着可以在代码中按照如下方式获取到 5s 内地 JS 代码运行状态:‘use strict’;const v8Profiler = require(‘v8-profiler’);const title = ’test’;v8Profiler.startProfiling(title, true);setTimeout(() => { const profiler = v8Profiler.stopProfiling(title); profiler.delete(); console.log(profiler);}, 5000);那么我们可以看到,v8-profiler 模块帮我导出的代码运行状态实际上是一个很大的 JSON 对象,我们可以将这个 JSON 对象序列化为字符串后存储到文件:test.cpuprofile 。注意这里的文件名后缀必须为 .cpuprofile ,否则 Chrome devtools 是不识别的。注意:v8-profiler 目前也处于年久失修的状态了,在 Node.js 8 和 Node.js 10 上已经无法正确编译安装了,如果你在 8 或者 10 的项目中想进行使用,可以试试看 v8-profiler-next。II. 分析 CPU Profile 文件借助于 v8-profiler 拿到我们的 Node.js 应用一段时间内的 JS 代码运行状态后,我们可以将其导入 Chrome devtools 中进行分析展示。在 Chrome 72 中,分析我们 Dump 出来的 CPU Profile 的方法已经和之前有所不同了,默认工具栏中也不会展示 CPU Profile 的分析页面,我们需要通过点击工具栏右侧的 更多 按钮,然后选择 More tools -> JavaScript Profiler 来进入到 CPU 的分析页面,如下图所示:选中 JavaScript Profiler 后,在出现的页面上点击 Load 按钮,然后将刚才保存得到的 test.cpuprofile 文件加载进来,就可以看到 Chrome devtools 的解析结果了:这里默认的视图是 Heavy 视图,在这个视图下,Devtools 会按照对你的应用的影响程度从高到低,将这些函数列举出来,点击展开能够看到这些列举出来的函数的全路径,方便你去代码中对应的位置进行排查。这里解释两个比较重要的指标,以便让大家能更有针对性地进行排查:Self Time: 此函数本身代码段执行地时间(不包含任何调用)Total Time: 此函数包含了其调用地其它函数总共的执行时间像在上述地截图例子中,ejs 模块在线上都应该开启了缓存,所以 ejs 模块的 compile 方法不应该出现在列表中,这显然是一个非常可疑的性能损耗点,需要我们去展开找到原因。除了 Heavy 视图,Devtools 实际上还给我们提供了火焰图来进行更多维度的展示,点击左上角可以切换:火焰图按照我们的 CPU 采样时间轴进行展示,那么在这里我们更容易看到我们的 Node.js 应用在采样期间 JS 代码的执行行为,新增的两个指标这边也给大家解释一下其含义:Aggregated self time: 在 CPU 采样期间聚合后的此函数本身代码段的执行总时间(不包含其他调用)Aggregated total time: 在 CPU 采样期间聚合后的此函数包含了其调用地其它函数总共的执行总时间综上,借助于 Chrome devtools 和能够导出当前 Node.js 应用 Javascript 代码运行状态的模块,我们已经可以比较完备地对应用服务异常时,排查定位到相应的 Node.js 进程 CPU 很高的情况进行排查和定位分析了。在生产实践中,这部分的 JS 代码的性能的分析往往也会用到新项目上线前的性能压测中,有兴趣的同学可以更深入地研究下。内存泄漏问题I. 判断是否内存泄漏在笔者的经历中,内存泄漏问题是 Node.js 在线上运行时出现的问题种类中的重灾区。尤其是三方库自身的 Bug 或者开发者使用不当引起的内存泄漏,会让很多的 Node.js 开发者感到束手无策。本节首先向读者介绍下,什么情况下我们的应用算是有很大的可能在发生内存泄漏呢?实际上判断我们的线上 Node.js 应用是否有内存泄漏也非常简单:借助于大家各自公司的一些系统和进程监控工具,如果我们发现 Node.js 应用的总内存占用曲线 处于长时间的只增不降 ,并且堆内存按照趋势突破了 堆限制的 70% 了,那么基本上应用 很大可能 产生了泄漏。当然事无绝对,如果确实应用的访问量(QPS)也在一直增长中,那么内存曲线只增不减也属于正常情况,如果确实因为 QPS 的不断增长导致堆内存超过堆限制的 70% 甚至 90%,此时我们需要考虑的扩容服务器来缓解内存问题。II. 导出 JS 堆内存快照如果确认了 Node.js 应用出现了内存泄漏的问题,那么和上面 CPU 的问题一样,我们需要通过一些办法导出 JS 内存快照(堆快照)来进行分析。V8 引擎同样在内部提供了接口可以直接将分配在 V8 堆上的 JS 对象导出来供开发者进行分析,这里我们采用 heapdump 这个模块,首先依旧是执行如下命令进行安装:npm install heapdump –save接着可以在代码中按照如下方法使用此模块:‘use sytrict’;const heapdump = require(‘heapdump’);heapdump.writeSnapshot(’./test’ + ‘.heapsnapshot’);这样我们就在当前目录下得到了一个堆快照文件:test.heapsnapshot ,用文本编辑工具打开这个文件,可以看到其依旧是一个很大的 JSON 结构,同样这里的堆快照文件后缀必须为 .heapsnapshot ,否则 Chrome devtools 是不识别的。III. 分析堆快照在 Chrome devtools 的工具栏中选择 Memory 即可进入到分析页面,如下图所示:然后点击页面上的 Load 按钮,选择我们刚才生成 test.heapsnapshot 文件,就可以看到分析结果,如下图所示:默认的视图其实是一个 Summary 视图,这里的 Constructor 和我们编写 JS 代码时的构造函数并无不同,都是指代此构造函数创建的对象,新版本的 Chrome devtools 中还在构造函数后面增加 * number 的信息,它代表这个构造函数创建的实例的个数。实际上在堆快照的分析视图中,有两个非常重要的概念需要大家去理解,否则很可能拿到堆快照看着分析结果也无所适从,它们是 Shallow Size 和 Retained Size ,要更好地去理解这两个概念,我们需要先了解 支配树。首先我们看如下简化后的堆快照描述的内存关系图:这里的 1 为根节点,即 GC 根,那么对于对象 5 来说,如果我们想要让对象 5 回收(即从 GC 根不可达),仅仅去掉对象 4 或者对象 3 对于对象 5 的引用是不够的,因为显然从根节点 1 可以分别从对象 3 或者对象 4 遍历到对象 5。因此我们只有去掉对象 2 才能将对象 5 回收,所以在上面这个图中,对象 5 的直接支配者是对象 2。照着这个思路,我们可以通过一定的算法将上述简化后的堆内存关系图转化为支配树:对象 1 到对象 8 间的支配关系描述如下:对象 1 支配对象 2对象 2 支配对象 3 、4 和 5对象 4 支配对象 6对象 5 支配对象 7对象 6 支配对象 8好了,到这里我们可以开始解释什么是 Shallow Size 和 Retained Size 了,实际上对象的 Shallow Size 就是对象自身被创建时,在 V8 堆上分配的大小,结合上面的例子,即对象 1 到 8 自身的大小。对象的 Retained Size 则是把此对象从堆上拿掉,则 Full GC 后 V8 堆能够释放出的空间大小。同样结合上面的例子,支配树的叶子节点对象 3、对象 7 和对象 8 因为没有任何直接支配对象,因此其 Retained Size 等于其 Shallow Size。将剩下的非叶子节点可以一一展开,为了篇幅描述方便,SZ_5表示对象 5 的 Shallow Size,RZ_5 表示对象 5 的 Retained Size,那么可以得到如下结果:对象 3 的 Retained Size:RZ_3 = SZ_3对象 7 的 Retained Size:RZ_7 = SZ_7对象 8 的 Retained Size:RZ_8 = SZ_8对象 6 的 Retained Size:RZ_6 = SZ_6 + RZ_8 = SZ_6 + SZ_8对象 5 的 Retained Size:RZ_5 = SZ_5 + RZ_7 = SZ_5 + SZ_7对象 4 的 Retained Size:RZ_4 = SZ_4 + RZ_6 = SZ_4 + SZ_6 + SZ_8对象 2 的 Retained Size:RZ_2 = SZ_2 + RZ_3 + RZ_4 + RZ_5 = SZ_2 + SZ_3 + SZ_4 + SZ_5 + SZ_6 + SZ_7 + SZ_8GC 根 1 的 Retained Size:RZ_1 = SZ_1 + RZ_2 = SZ_1 + SZ_2 + RZ_3 + RZ_4 + RZ_5 = SZ_2 + SZ_3 + SZ_4 + SZ_5 + SZ_6 + SZ_7 + SZ_8这里可以发现,GC 根的 Retained Size 等于堆上所有从此根出发可达对象的 Shallow Size 之和,这和我们的理解预期是相符合的,毕竟将 GC 根从堆上拿掉的话,原本就应当将从此根出发的所有对象都清理掉。理解了这一点,回到我们最开始看到的默认总览视图中,正常来说,可能的泄漏对象往往其 Retained Size 特别大,我们可以在窗口中依据 Retained Size 进行排序来对那些占据了堆空间绝大部分的对象进行排查:假如确认了可疑对象,Chrome devtools 中也会给你自动展开方便你去定位到代码段,下面以 NativeModule 这个构造器生成的对象 vm 为例:这里上半部分是顺序的引用关系,比如 NativeModule 实例 @45655 的 exports 属性指向了对象 @45589,filename 属性则指向一个字符串 “vm.js”;下半部分则是反向的引用关系:NativeModule 实例 @13021 的 _cache 属性指向了 Object 实例 @41103,而 Object 实例 @41103 的 vm 属性指向了 NativeModule 实例 @45655。如果对这部分展示图表比较晕的可以仔细看下上面的例子,因为找到可疑的泄漏对象,结合上图能看到此对象下的属性和值及其父引用关系链,绝大部分情况下我们就可以定位到生成可疑对象的 JS 代码段了。实际上除了默认的 Summary 视图,Chrome devtools 还提供了 Containment 和 Statistics 视图,这里再看下 Containment 视图,选择堆快照解析页面的左上角可以进行切换,如下图所示:这个视图实际上是堆快照解析出来的内存关系图的直接展示,因此相比 Summary 视图,从这个视图中直接查找可疑的泄漏对象相对比较困难。结尾Chrome devtools 实际上是非常强大的一个工具,本节也只是仅仅介绍了对 CPU Profile 和堆快照解析能力的介绍和常用视图的使用指南,如果你仔细阅读了本节内容,面对服务器上定位到的 Node.js 应用 CPU 飙高或者内存泄漏这样的问题,想必就可以做到心中有数不慌乱了。本文作者:奕钧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 4, 2019 · 3 min · jiezi

箭头函数你想知道的都在这里

1、基本语法回顾我们先来回顾下箭头函数的基本语法。ES6 增加了箭头函数:var f = v => v;// 等同于var f = function (v) { return v;};如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。var f = () => 5;// 等同于var f = function () { return 5 };var sum = (num1, num2) => num1 + num2;// 等同于var sum = function(num1, num2) { return num1 + num2;};由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。// 报错let getTempItem = id => { id: id, name: “Temp” };// 不报错let getTempItem = id => ({ id: id, name: “Temp” });下面是一种特殊情况,虽然可以运行,但会得到错误的结果。let foo = () => { a: 1 };foo() // undefined上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。2、关于this2.1、默认绑定外层this箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值。这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this。function foo() { setTimeout(() => { console.log(‘id:’, this.id); }, 100);}var id = 21;foo.call({ id: 42 });// id: 42上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。所以,箭头函数转成 ES5 的代码如下。// ES6function foo() { setTimeout(() => { console.log(‘id:’, this.id); }, 100);}// ES5function foo() { var _this = this; setTimeout(function () { console.log(‘id:’, this.id); }, 100);}2.2、 不能用call()、apply()、bind()方法修改里面的this(function() { return [ (() => this.x).bind({ x: ‘inner’ })() // 无效的bind,最终this还是指向外层 ];}).call({ x: ‘outer’ });// [‘outer’]上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this。3、没有 arguments箭头函数没有自己的 arguments 对象,这不一定是件坏事,因为箭头函数可以访问外围函数的 arguments 对象:function constant() { return () => arguments[0]}var result = constant(1);console.log(result()); // 1那如果我们就是要访问箭头函数的参数呢?你可以通过命名参数或者 rest 参数的形式访问参数:let nums = (…nums) => nums;4、 不能通过 new 关键字调用JavaScript 函数有两个内部方法:[[Call]] 和 [[Construct]]。当通过new调用函数时,执行[Construct]]方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。当直接调用的时候,执行[[Call]]方法,直接执行函数体。箭头函数并没有[[Construct]]方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。var Foo = () => {};var foo = new Foo(); // TypeError: Foo is not a constructor5、没有原型由于不能使用new调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在prototype这个属性。var Foo = () => {};console.log(Foo.prototype); // undefined5、不适用场合第一个场合是定义函数的方法,且该方法内部包括this。const cat = { lives: 9, jumps: () => { this.lives–; }}上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。第二个场合是需要动态this的时候,也不应使用箭头函数。var button = document.getElementById(‘press’);button.addEventListener(‘click’, () => { this.classList.toggle(‘on’);});上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。6、使用场景下面这个是我们开发经常遇到的。我们一般会通过this赋值给一个变量,然后再通过变量访问。class Test { constructor() { this.birth = 10; } submit(){ let self = this; $.ajax({ type: “POST”, dataType: “json”, url: “xxxxx” ,//url data: “xxxxx”, success: function (result) { console.log(self.birth);//10 }, error : function() {} }); }}let test = new Test();test.submit();//undefined 这里我们就可以通过箭头函数来解决…success: (result)=> { console.log(this.birth);//10},…箭头函数在react中的运用场景class Foo extends Component { constructor(props) { super(props); } handleClick() { console.log(‘Click happened’, this); this.setState({a: 1}); } render() { return <button onClick={this.handleClick}>Click Me</button>; }}在react中我们这样直接调用方法是有问题的,在handleClick函数中的this是有问题,我们平时需要这么做class Foo extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { console.log(‘Click happened’, this); this.setState({a: 1}); } render() { return <button onClick={this.handleClick}>Click Me</button>; }}这里通过this.handleClick.bind(this)给函数绑定this。但是这样写起来有些麻烦,有没有简单的方法呢?这时候我们的箭头函数就出场了class Foo extends Component { // Note: this syntax is experimental and not standardized yet. handleClick = () => { console.log(‘Click happened’, this); this.setState({a: 1}); } render() { return <button onClick={this.handleClick}>Click Me</button>; }}箭头函数中 this 的值是继承自 外围作用域,很好的解决了这个问题。除此之外我们还可以用箭头函数传参(这个不是必须的),而且会有性能问题。更多信息请查看const A = 65 // ASCII character codeclass Alphabet extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); this.state = { justClicked: null, letters: Array.from({length: 26}, (, i) => String.fromCharCode(A + i)). }; } handleClick(letter) { this.setState({ justClicked: letter }); } render() { return ( <div> Just clicked: {this.state.justClicked} <ul> {this.state.letters.map(letter => <li key={letter} onClick={() => this.handleClick(letter)}> {letter} </li> )} </ul> </div> ) }}最后更多系列文章请看ES6学习(一)之var、let、constES6学习(二)之解构赋值及其原理ES6学习(三)之Set的模拟实现ES6学习(四)之Promise的模拟实现如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star对作者也是一种鼓励。 ...

April 4, 2019 · 2 min · jiezi

懂编译真的可以为所欲为|不同前端框架下的代码转换

背景整个前端领域在这几年迅速发展,前端框架也在不断变化,各团队选择的解决方案都不太一致,此外像小程序这种跨端场景和以往的研发方式也不太一样。在日常开发中往往会因为投放平台的不一样需要进行重新编码。前段时间我们需要在淘宝页面上投放闲鱼组件,淘宝前端研发DSL主要是React(Rax),而闲鱼前端之前研发DSL主要是Vue(Weex),一般这种情况我们都是重新用React开发,有没有办法一键将已有的Vue组件转化为React组件呢,闲鱼技术团队从代码编译的角度提出了一种解决方案。编译器是如何工作的日常工作中我们接触最多的编译器就是Babel,Babel可以将最新的Javascript语法编译成当前浏览器兼容的JavaScript代码,Babel工作流程分为三个步骤,由下图所示:抽象语法树AST是什么在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构,详见维基百科。这里以const a = 1转成var a = 1操作为例看下Babel是如何工作的。将代码解析(parse)成抽象语法树ASTBabel提供了@babel/parser将代码解析成AST。const parse = require(’@babel/parser’).parse;const ast = parse(‘const a = 1’);经过遍历和分析转换(transform)对AST进行处理Babel提供了@babel/traverse对解析后的AST进行处理。@babel/traverse能够接收AST以及visitor两个参数,AST是上一步parse得到的抽象语法树,visitor提供访问不同节点的能力,当遍历到一个匹配的节点时,能够调用具体方法对于节点进行处理。@babel/types用于定义AST节点,在visitor里做节点处理的时候用于替换等操作。在这个例子中,我们遍历上一步得到的AST,在匹配到变量声明(VariableDeclaration)的时候判断是否const操作时进行替换成var。t.variableDeclaration(kind, declarations)接收两个参数kind和declarations,这里kind设为var,将const a = 1解析得到的AST里的declarations直接设置给declarations。const traverse = require(’@babel/traverse’).default;const t = require(’@babel/types’);traverse(ast, { VariableDeclaration: function(path) { //识别在变量声明的时候 if (path.node.kind === ‘const’) { //只有const的时候才处理 path.replaceWith( t.variableDeclaration(‘var’, path.node.declarations) //替换成var ); } path.skip(); }});将最终转换的AST重新生成(generate)代码Babel提供了@babel/generator将AST再还原成代码。const generate = require(’@babel/generator’).default;let code = generate(ast).code;Vue和React的异同我们来看下Vue和React的异同,如果需要做转化需要有哪些处理,Vue的结构分为style、script、template三部分style样式这部分不用去做特别的转化,Web下都是通用的scriptVue某些属性的名称和React不太一致,但是功能上是相似的。例如data需要转化为state,props需要转化为defaultProps和propTypes,components的引用需要提取到组件声明以外,methods里的方法需要提取到组件的属性上。还有一些属性比较特殊,比如computed,React里是没有这个概念的,我们可以考虑将computed里的值转化成函数方法,上面示例中的length,可以转化为length()这样的函数调用,在React的render()方法以及其他方法中调用。Vue的生命周期和React的生命周期有些差别,但是基本都能映射上,下面列举了部分生命周期的映射created -> componentWillMountmounted -> componentDidMountupdated -> componentDidUpdatebeforeDestroy -> componentWillUnmount在Vue内函数的属性取值是通过this.xxx的方式,而在Rax内需要判断是否state、props还是具体的方法,会转化成this.state、this.props或者this.xxx的方式。因此在对Vue特殊属性的处理中,我们对于data、props、methods需要额外做标记。template针对文本节点和元素节点处理不一致,文本节点需要对内容{{title}}进行处理,变为{title}。Vue里有大量的增强指令,转化成React需要额外做处理,下面列举了部分指令的处理方式事件绑定的处理,@click -> onClick逻辑判断的处理,v-if=“item.show” -> {item.show && ……}动态参数的处理,:title=“title” -> title={title}还有一些是正常的html属性,但是React下是不一样的,例如style -> className。指令里和model里的属性值需要特殊处理,这部分的逻辑其实和script里一样,例如需要{{title}}转变成{this.props.title}Vue代码的解析以下面的Vue代码为例<template> <div> <p class=“title” @click=“handleClick”>{{title}}</p> <p class=“name” v-if=“show”>{{name}}</p> </div></template><style>.title {font-size: 28px;color: #333;}.name {font-size: 32px;color: #999;}</style><script>export default { props: { title: { type: String, default: “title” } }, data() { return { show: true, name: “name” }; }, mounted() { console.log(this.name); }, methods: { handleClick() {} }};</script>我们需要先解析Vue代码变成AST值。这里使用了Vue官方的vue-template-compiler来分别提取Vue组件代码里的template、style、script,考虑其他DSL的通用性后续可以迁移到更加适用的html解析模块,例如parse5等。通过require(‘vue-template-compiler’).parseComponent得到了分离的template、style、script。style不用额外解析成AST了,可以直接用于React代码。template可以通过require(‘vue-template-compiler’).compile转化为AST值。script用@babel/parser来处理,对于script的解析不仅仅需要获得整个script的AST值,还需要分别将data、props、computed、components、methods等参数提取出来,以便后面在转化的时候区分具体属于哪个属性。以data的处理为例:const traverse = require(’@babel/traverse’).default;const t = require(’@babel/types’);const analysis = (body, data, isObject) => { data._statements = [].concat(body); // 整个表达式的AST值 let propNodes = []; if (isObject) { propNodes = body; } else { body.forEach(child => { if (t.isReturnStatement(child)) { // return表达式的时候 propNodes = child.argument.properties; data._statements = [].concat(child.argument.properties); // 整个表达式的AST值 } }); } propNodes.forEach(propNode => { data[propNode.key.name] = propNode; // 对data里的值进行提取,用于后续的属性取值 });};const parse = (ast) => { let data = { }; traverse(ast, { ObjectMethod(path) { /* 对象方法 data() {return {}} / const parent = path.parentPath.parent; const name = path.node.key.name; if (parent && t.isExportDefaultDeclaration(parent)) { if (name === ‘data’) { const body = path.node.body.body; analysis(body, data); path.stop(); } } }, ObjectProperty(path) { / 对象属性,箭头函数 data: () => {return {}} data: () => ({}) / const parent = path.parentPath.parent; const name = path.node.key.name; if (parent && t.isExportDefaultDeclaration(parent)) { if (name === ‘data’) { const node = path.node.value; if (t.isArrowFunctionExpression(node)) { / 箭头函数 () => {return {}} () => {} / if (node.body.body) { analysis(node.body.body, data); } else if (node.body.properties) { analysis(node.body.properties, data, true); } } path.stop(); } } } }); / 最终得到的结果 { _statements, //data解析AST值 list //data.list解析AST值 } */ return data;};module.exports = parse;最终处理之后得到这样一个结构:app: { script: { ast, components, computed, data: { _statements, //data解析AST值 list //data.list解析AST值 }, props, methods }, style, // style字符串值 template: { ast // template解析AST值 }}React代码的转化最终转化的React代码会包含两个文件(css和js文件)。用style字符串直接生成index.css文件,index.js文件结构如下图,transform指将Vue AST值转化成React代码的伪函数。import { createElement, Component, PropTypes } from ‘React’;import ‘./index.css’;export default class Mod extends Component { ${transform(Vue.script)} render() { ${transform(Vue.template)} }}script AST值的转化不一一说明,思路基本都一致,这里主要针对Vue data继续说明如何转化成React state,最终解析Vue data得到的是{_statements: AST}这样的一个结构,转化的时候只需要执行如下代码const t = require(’@babel/types’);module.exports = (app) => { if (app.script.data && app.script.data._statements) { // classProperty 类属性 identifier 标识符 objectExpression 对象表达式 return t.classProperty(t.identifier(‘state’), t.objectExpression(app.script.data._statements)); } else { return null; }};针对template AST值的转化,我们先看下Vue template AST的结构:{ tag: ‘div’, children: [{ tag: ’text’ },{ tag: ‘div’, children: [……] }]}转化的过程就是遍历上面的结构针对每一个节点生成渲染代码,这里以v-if的处理为例说明下节点属性的处理,实际代码中会有两种情况:不包含v-else的情况,<div v-if=“xxx”/>转化为{ xxx && <div /> }包含v-else的情况,<div v-if=“xxx”/><text v-else/>转化为{ xxx ? <div />: <text /> }经过vue-template-compiler解析后的template AST值里会包含ifConditions属性值,如果ifConditions的长度大于1,表明存在v-else,具体处理的逻辑如下:if (ast.ifConditions && ast.ifConditions.length > 1) { // 包含v-else的情况 let leftBlock = ast.ifConditions[0].block; let rightBlock = ast.ifConditions[1].block; let left = generatorJSXElement(leftBlock); //转化成JSX元素 let right = generatorJSXElement(rightBlock); //转化成JSX元素 child = t.jSXExpressionContainer( //JSX表达式容器 // 转化成条件表达式 t.conditionalExpression( parseExpression(value), left, right ) );} else { // 不包含v-else的情况 child = t.jSXExpressionContainer( //JSX表达式容器 // 转化成逻辑表达式 t.logicalExpression(’&&’, parseExpression(value), t.jsxElement( t.jSXOpeningElement( t.jSXIdentifier(tag), attrs), t.jSXClosingElement(t.jSXIdentifier(tag)), children )) );}template里引用的属性/方法提取,在AST值表现上都是标识符(Identifier),可以在traverse的时候将Identifier提取出来。这里用了一个比较取巧的方法,在template AST值转化的时候我们不对这些标识符做判断,而在最终转化的时候在render return之前插入一段引用。以下面的代码为例<text class=“title” @click=“handleClick”>{{title}}</text><text class=“list-length”>list length:{{length}}</text><div v-for="(item, index) in list" class=“list-item” :key="item-${index}"> <text class=“item-text” @click=“handleClick” v-if=“item.show”>{{item.text}}</text></div>我们能解析出template里的属性/方法以下面这样一个结构表示:{ title, handleClick, length, list, item, index}在转化代码的时候将它与app.script.data、app.script.props、app.script.computed和app.script.computed分别对比判断,能得到title是props、list是state、handleClick是methods,length是computed,最终我们在return前面插入的代码如下:let {title} = this.props;let {state} = this.state;let {handleClick} = this;let length = this.length();最终示例代码的转化结果import { createElement, Component, PropTypes } from ‘React’;export default class Mod extends Component { static defaultProps = { title: ’title’ } static propTypes = { title: PropTypes.string } state = { show: true, name: ’name’ } componentDidMount() { let {name} = this.state; console.log(name); } handleClick() {} render() { let {title} = this.props; let {show, name} = this.state; let {handleClick} = this; return ( <div> <p className=“title” onClick={handleClick}>{title}</p> {show && ( <p className=“name”>{name}</p> )} </div> ); }}总结与展望本文从Vue组件转化为React组件的具体案例讲述了一种通过代码编译的方式进行不同前端框架代码的转化的思路。我们在生产环境中已经将十多个之前的Vue组件直接转成React组件,但是实际使用过程中研发同学的编码习惯差别也比较大,需要处理很多特殊情况。这套思路也可以用于小程序互转等场景,减少编码的重复劳动,但是在这类跨端的非保准Web场景需要考虑更多,例如小程序环境特有的组件以及API等,闲鱼技术团队也会持续在这块做尝试。本文作者:闲鱼技术-玉缜阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 3, 2019 · 3 min · jiezi

Node.js 应用故障排查手册 —— 利用 CPU 分析调优吞吐量

楔子在我们想要新上线一个 Node.js 应用之前,尤其是技术栈切换的第一个 Node.js 应用,由于担心其在线上的吞吐量表现,肯定会想要进行性能压测,以便对其在当前的集群规模下能抗住多少流量有一个预估。本案例实际上正是在这样的一个场景下,我们想要上线 Node.js 技术栈来做前后端分离,那么刨开后端服务的响应 QPS,纯使用 Node.js 进行的模板渲染能有怎么样的表现,这是大家非常关心的问题。本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。优化过程集群在性能压测下反映出来的整体能力其实由单机吞吐量就可以测算得知,因此这次的性能压测采用的 4 核 8G 内存的服务器进行压测,并且页面使用比较流行的 ejs 进行服务端渲染,进程数则按照核数使用 PM2 启动了四个业务子进程来运行。1. 开始压测完成这些准备后,使用阿里云提供的 PTS 性能压测工具进行压力测试,此时大致单机 ejs 模板渲染的 QPS 在 200 左右,此时通过 Node.js 性能平台 监控可以看到四个子进程的 CPU 基本都在 100%,即 CPU 负载达到了瓶颈,但是区区 200 左右的 QPS 显然系统整体渲染非常的不理想。2. 模板缓存因为是 CPU 达到了系统瓶颈导致整体 QPS 上不去,因此按照第二部分工具篇章节的方法,我们在平台上抓了 压测期间 的 3 分钟的 CPU Profile,展现的结果如下图所示:这里就看到了很奇怪的地方,因为压测环境下我们已经打开了模板缓存,按理来说不会再出现 ejs.compile 函数对应的模板编译才对。仔细比对项目中的渲染逻辑代码,发现这部分采用了一个比较不常见的模块 koa-view,项目开发者想当然地用 ejs 模块地入参方式传入了 cache: true,但是实际上该模块并没有对 ejs 模板做更好的支持,因此实际压测情况下模板缓存并没有生效,而模板地编译动作本质上字符串处理,它恰恰是一个 CPU 密集地操作,这就导致了 QPS 达不到预期的状况。了解到原因之后,首先我们将 koa-view 替换为更好用的 koa-ejs 模块,并且按照 koa-ejs 的文档正确开启缓存:render(app, { root: path.join(__dirname, ‘view’), viewExt: ‘html’, cache: true});再次进行压测后,单机下的 QPS 提升到了 600 左右,虽然大约提升了三倍的性能,但是仍然达不到预期的目标。3. include 编译为了继续优化进一步提升服务器的渲染性能,我们继续在压测期间抓取 3 分钟的 CPU Profile 进行查看:可以看到,我们虽然已经确认使用 koa-ejs 模块且正确开启了缓存,但是压测期间的 CPU Profile 里面竟然还有 ejs 的 compile 动作!继续展开这里的 compile,发现是 includeFile 时引入的,继续回到项目本身,观察压测的页面模板,确实使用了 ejs 注入的 include 方法来引入其它模板:<%- include("../xxx") %>对比 ejs 的源代码后,这个注入的 include 函数调用链确实也是 include -> includeFile -> handleCache -> compile,与压测得到的 CPU Profile 展示的内容一致。那么下面红框内的 replace 部分也是在 compile 过程中产生的。到了这里开始怀疑 koa-ejs 模块没有正确地将 cache 参数传递给真正负责渲染地 ejs 模块,导致这个问题地发生,所以继续去阅读 koa-ejs 的缓存设置,以下是简化后的逻辑(koa-ejs@4.1.1 版本):const cache = Object.create(null);async function render(view, options) { view += settings.viewExt; const viewPath = path.join(settings.root, view); // 如果有缓存直接使用缓存后的模板解析得到的函数进行渲染 if (settings.cache && cache[viewPath]) { return cache[viewPath].call(options.scope, options); } // 没有缓存首次渲染调用 ejs.compile 进行编译 const tpl = await fs.readFile(viewPath, ‘utf8’); const fn = ejs.compile(tpl, { filename: viewPath, _with: settings._with, compileDebug: settings.debug && settings.compileDebug, debug: settings.debug, delimiter: settings.delimiter }); // 将 ejs.compile 得到的模板解析函数缓存起来 if (settings.cache) { cache[viewPath] = fn; } return fn.call(options.scope, options);}显然,koa-ejs 模板的模板缓存是完全自己实现的,并没有在调用 ejs.compile 方法时传入的 option 参数内将用户设置的 cache 参数传递过去而使用 ejs 模块提供的 cache 能力。但是偏偏项目在模板内又直接使用了 ejs 模块注入的 include 方法进行模板间的调用,产生的结果就是只缓存了主模板,而主模板使用 include 调用别的模板还是会重新进行编译解析,进而造成压测下还是存在大量重复的模板编译动作导致 QPS 升不上去。再次找到了问题的根源,为了验证是否是 koa-ejs 模块本身的 bug,我们在项目中将其渲染逻辑稍作更改:const fn = ejs.compile(tpl, { filename: viewPath, _with: settings._with, compileDebug: settings.debug && settings.compileDebug, debug: settings.debug, delimiter: settings.delimiter, // 将用户设置的 cache 参数传递给 ejs 而使用到其提供的缓存能力 cache: settings.cache});然后打包后进行压测,此时单机 QPS 从 600 提升至 4000 左右,基本达到了上线前的性能预期,为了确认压测下是否还有模板的编译动作,我们继续在 Node.js 性能平台 上抓取压测期间 3 分钟的 CPU Profile:可以看到上述对 koa-ejs 模板进行优化后,ejs.compile 确实消失了,而压测期间不再有大量重复且耗费 CPU 的编译动作后,应用整体的性能比最开始有了 20 倍左右的提升。文中 koa-ejs 模块缓存问题已经在 4.1.2 版本(包含)之后被修复了,详情可以见 cache include file,如果大家使用的 koa-ejs 版本 >= 4.1.2 就可以放心使用。结尾CPU Profile 本质上以可读的方式反映给开发者运行时的 JavaScript 代码执行频繁程度,除了在线上进程出现负载很高时能够用来定位问题代码之外,它在我们上线前进行性能压测和对应的性能调优时也能提供巨大的帮助。这里需要注意的是:仅当进程 CPU 负载非常高的时候去抓取得到的 CPU Profile 才能真正反馈给我们问题所在。在这个源自真实生产的案例中,我们也可以看到,正确和不正确地去使用 Node.js 开发应用其前后运行效率能达到二十倍的差距,Node.js 作为一门服务端技术栈发展至今日,其本身能够提供的性能是毋庸置疑的,绝大部分情况下执行效率不佳是由我们自身的业务代码或者三方库本身的 Bug 引起的,Node.js 性能平台 则可以帮助我们以比较方便的方式找出这些 Bug。本文作者:奕钧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 2, 2019 · 2 min · jiezi

图解梯度下降背后的数学原理

摘要: 本文讲解了梯度下降的基本概念,并以线性回归为例详细讲解梯度下降算法,主要以图的形式讲解,清晰简单明了。敏捷在软件开发过程中是一个非常著名的术语,它背后的基本思想很简单:快速构建一些东西,然后得到一些反馈,根据反馈做出改变,重复此过程。目标是让产品更贴合用,让用户做出反馈,以获得设计开发出的产品与优秀的产品二者之间误差最小,梯度下降算法背后的原理和这基本一样。目的梯度下降算法是一个迭代过程,它将获得函数的最小值。下面的公式将整个梯度下降算法汇总在一行中。但是这个公式是如何得出的呢?实际上很简单,只需要具备一些高中的数学知识即可理解。本文将尝试讲解这个公式,并以线性回归模型为例,构建此类公式。机器学习模型考虑二维空间中的一堆数据点。假设数据与一组学生的身高和体重有关。试图预测这些数量之间的某种关系,以便我们可以预测一些新生的体重。这本质上是一种有监督学习的简单例子。现在在空间中绘制一条穿过其中一些数据点的任意直线,该直线方程的形如Y=mX+b,其中m是斜率,b是其在Y轴的截距。预测给定一组已知的输入及其相应的输出,机器学习模型试图对一组新的输入做出一些预测。两个预测之间的差异即为错误。这涉及成本函数或损失函数的概念(cost function or loss function)。成本函数成本函数/损失函数用来评估机器学习算法的性能。二者的区别在于,损失函数计算单个训练示例的错误,而成本函数是整个训练集上错误的平均值。成本函数基本上能告诉我们模型在给定m和b的值时,其预测能“有多好”。比方说,数据集中总共有N个点,我们想要最小化所有N个数据点的误差。因此,成本函数将是总平方误差,即为什么采取平方差而不是绝对差?因为平方差使得导出回归线更容易。实际上,为了找到这条直线,我们需要计算成本函数的一阶导数,而计算绝对值的导数比平方值更难。最小化成本函数任何机器学习算法的目标都是最小化成本函数。这是因为实际值和预测值之间的误差对应着表示算法在学习方面的性能。由于希望误差值最小,因此尽量使得那些m和b值能够产生尽可能小的误差。如何最小化一个任意函数?仔细观察上述的成本函数,其形式为Y=X²。在笛卡尔坐标系中,这是一个抛物线方程,用图形表示如下:为了最小化上面的函数,需要找到一个x,函数在该点能产生小值Y,即图中的红点。由于这是一个二维图像,因此很容易找到其最小值,但是在维度比较大的情况下,情况会更加复杂。对于种情况,需要设计一种算法来定位最小值,该算法称为梯度下降算法(Gradient Descent)。梯度下降梯度下降是优化模型的方法中最流行的算法之一,也是迄今为止优化神经网络的最常用方法。它本质上是一种迭代优化算法,用于查找函数的最小值。表示假设你是沿着下面的图表走,目前位于曲线’绿’点处,而目标是到达最小值,即红点位置,但你是无法看到该最低点。可能采取的行动:可能向上或向下;如果决定走哪条路,可能会采取更大的步伐或小的步伐来到达目的地;从本质上讲,你应该知道两件事来达到最小值,即走哪条和走多远。梯度下降算法通过使用导数帮助我们有效地做出这些决策。导数是来源于积分,用于计算曲线特定点处的斜率。通过在该点处绘制图形的切线来描述斜率。因此,如果能够计算出这条切线,可能就能够计算达到最小值的所需方向。最小值在下图中,在绿点处绘制切线,如果向上移动,就将远离最小值,反之亦然。此外,切线也能让我们感觉到斜坡的陡峭程度。蓝点处的斜率比绿点处的斜率低,这意味着从蓝点到绿点所需的步长要小得多。成本函数的数学解释现在将上述内容纳入数学公式中。在等式y=mX+b中,m和b是其参数。在训练过程中,其值也会发生微小变化,用表示这个小的变化。参数值将分别更新为m = m-m 和b = b-b。最终目标是找到m和b的值,以使得y=mx+b 的误差最小,即最小化成本函数。重写成本函数:想法是,通过计算函数的导数/斜率,就可以找到函数的最小值。学习率达到最小值或最低值所采取的步长大小称为学习率。学习率可以设置的比较大,但有可能会错过最小值。而另一方面,小的学习率将花费大量时间训练以达到最低点。下面的可视化给出了学习率的基本概念。在第三个图中,以最小步数达到最小点,这表明该学习率是此问题的最佳学习率。从上图可以看到,当学习率太低时,需要花费很长训练时间才能收敛。而另一方面,当学习率太高时,梯度下降未达到最小值,如下面所示:导数机器学习在优化问题中使用导数。梯度下降等优化算法使用导数来决定是增加还是减少权重,进而增加或减少目标函数。如果能够计算出函数的导数,就可以知道在哪个方向上能到达最小化。主要处理方法源自于微积分中的两个基本概念:指数法则指数法则求导公式:链式法则链式法则用于计算复合函数的导数,如果变量z取决于变量y,且它本身也依赖于变量x,因此y和z是因变量,那么z对x的导数也与y有,这称为链式法则,在数学上写为:举个例子加强理解:使用指数法则和链式发规,计算成本函数相对于m和c的变化方式。这涉及偏导数的概念,即如果存在两个变量的函数,那么为了找到该函数对其中一个变量的偏导数,需将另一个变量视为常数。举个例子加强理解:计算梯度下降现在将这些微积分法则的知识应用到原始方程中,并找到成本函数的导数,即m和b。修改成本函数方程:为简单起见,忽略求和符号。求和部分其实很重要,尤其是随机梯度下降(SGD)与批量梯度下降的概念。在批量梯度下降期间,我们一次查看所有训练样例的错误,而在SGD中一次只查看其中的一个错误。这里为了简单起见,假设一次只查看其中的一个错误:现在计算误差对m和b的梯度:将值对等到成本函数中并将其乘以学习率:其中这个等式中的系数项2是一个常数,求导时并不重要,这里将其忽略。因此,最终,整篇文章归结为两个简单的方程式,它们代表了梯度下降的方程。其中m¹,b¹是下一个位置的参数;m,b是当前位置的参数。因此,为了求解梯度,使用新的m和b值迭代数据点并计算偏导数。这个新的梯度会告诉我们当前位置的成本函数的斜率以及我们应该更新参数的方向。另外更新参数的步长由学习率控制。结论本文的重点是展示梯度下降的基本概念,并以线性回归为例讲解梯度下降算法。通过绘制最佳拟合线来衡量学生身高和体重之间的关系。但是,这里为了简单起见,举的例子是机器学习算法中较简单的线性回归模型,读者也可以将其应用到其它机器学习方法中。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

March 27, 2019 · 1 min · jiezi

Serverless 风暴来袭,前端工程师如何应对?

阿里妹导读:尽管大部分前端的工作并不涉及server,但最近半年serverless这个词汇以及其引发的热烈的讨论,深深触动了阿里巴巴高级前端技术专家伐薪。作为接触前端十余载的老开发,伐薪认为serverless可能会是接下来引起前端领域革命性变化的技术之一。今天,伐薪将为大家梳理serverless的历史发展进程以及对前端的影响,希望对前端工程师有所启发。上图是serverless 这个词最近5年在 google 的搜索趋势,可以看到最近半年已经达到巅峰。历史上前端领域的重要技术革命Ajax 的诞生先来回顾一下前端技术领域的重要历史节点,第一个节点是2005年,google的Jesse James Garrett 发表了一篇文章——《Ajax:Web应用程序的新方法》,首次发布了Ajax 这个新的词汇(准确说并不是新的技术,只是新的词汇),当时我还在读大二,虽然ajax不是什么新的技术,只是对XmlHttpRequest等技术的包装,但是这个技术被google宣传之后成为全球web开发的标杆,间接促进了富客户端应用(RIA)和单页应用(SPA)的流行,这些应用大都具备丝滑般的体验(局部刷新),并一直伴随着web 2.0的发展,ajax的深入人心,使得前端js的工作更加复杂和重要,专业分工越来越细,间接促进了专职的前端开发人员这一角色诞生,在此之前,web开发并不区分服务端和浏览器端的工作,因此ajax是前端领域的第一次重要事件。Nodejs 对前端规范化和工程化的促进接下来对前端变化最大的一个里程碑事件是2009年诞生的 nodejs(包括common js及npm)的出现和流行,它对前端领域的重要意义并不仅仅是让前端可以快速用js写server那么简单,个人认为它最大的贡献反而是commonjs、npm以及其便捷开发体验促进的前端工程化,它使得前端开始从刀耕火种的和传统软件工程格格不入的部署方式,发展为接近传统企业应用的研发模式,在此之前,前端开发在资源引用、依赖管理以及模块规范上缺乏有效的工具和标准,但是nodejs流行以后,基于commonjs的模块及npm的包部署和依赖管理成为主流(类似java的maven体系),并诞生了多种基于nodejs开发的cli工具辅助前端开发(如grunt、gulp),npm目前是全球最大的包管理仓库,并且成为前端项目的包依赖管理事实标准。而webpack的出现,又使得前端代码的部署更加简便,让前端可以以类似java jar包的形式发布应用(bundle),而不管项目中是何种类型的资源。React 的组件化及vdom理念第三个革命性事件是2013年开始出现的react,尽管web components标准在此之前早已发布,但是真正让组件化理念深入人心并且应用最广的库是react,它还至少有两点特性足以让它成为历史上最具前瞻性的前端库,第一个特性是vdom的出现,在此之前,所有的ui库,都直接与dom关联,但是react在UI创建与渲染引擎之间,增加了一个中间层——vdom(一个使用轻量级json描写UI结构的协议),除了改善了其本身的dom diff性能之外,还有一个重大意义就是UI的编写与渲染开始分离,一次编写,多端渲染的UI得以实现,这个多端包括server端、移动端、pc端以及其他需要展示UI的设备,之后的react native以及weex都是这一分层思想的受益者。除了vdom之外,react还有一个重要的理念非常超前,即UI是一个函数(类),函数输入一个state,一定返回确定的视图,在此之前,大部分框架和库,都会把UI分离成一个html片段(通常支持模板写法以渲染数据),一个为该html片段绑定事件的js,尽管这样比较好理解,但是react对UI这种抽象却反映了UI的实际本质,并且这种函数式理念,在后面可以看到,将与faas及serverless技术产生美妙的碰撞。react 的诞生对此后,甚至此前的框架和库都产生了深远的影响,包括不限于angular和vue都陆续采纳了它很多技术思想,并且成为前端开发领域目前已经趋于稳定的屈指可数的几个技术选型之一。再来总结一下,ajax使得前端的角色逐渐分离出来,nodejs促进了前端的开发模式向传统编程语言靠近(工程化),react的出现,基本结束了后端常常对前端”技术变化快“的吐槽,至此,前端的技术体系逐步成熟和标准化。serverless 理念与前端的关系那么为什么说下一次对前端技术领域有较大影响的理念是serverless呢,事实上,尽管serverless这个词汇由亚马逊提出来还不到几年,但是这个理念并不是什么爆炸性的新理念,在早期,cdn还不普及的时候,web工程师会把js资源和视图文件(可能是静态也可能是动态的)传到服务器,那个时候前端是需要关心服务器的,但是cdn及回源策略的普及,工程及搭建系统的大规模使用,使得前端可以快速把一个js或者静态文件扔到cdn节点,通过回源机制(cdn回源到一个动态服务),半动态的视图层渲染也成为可能,在这整个过程,前端开发无需关心任何服务器的知识,也不知道cdn有多少节点,如何做负载均衡,做gslb的,也不需要知道qps多少,一个cdn可以放各种业务各种开发的资源,可以说cdn是serverless理念的的先行者。回到应用部署,在前几年nodejs刚流行的时期,已有开发者意识到应用与机器的部署与运维成本对业务方会是个问题,出现了一些容器化的思想,比如cbu在15年出的naga,在这个naga容器里,业务逻辑是一个个插件,容器负责请求的路由分发,负载及稳定性管理,业务方只需要编写并上传最直接的业务代码即可,对业务方来说是实现了serverless的理念,因为naga的维护者帮你解决了部署及运维的问题。再说对前端息息相关的页面搭建系统以及bff层,无论是各种搭建系统(如斑马、积木盒子、TMS),还是基于graphQl的平台,还是通过web ide快速编写api gateway的产品——如cbu的mbox,都让业务开发只关心业务逻辑,无需关心部署运维知识,它们一定程度上体现了serverless的理念。serverless 将对前端的影响综上所述,前端早已与serverless产生了联系,但是很多人还没感知,接下来,serverless显示化地爆发将给前端带来更为深远的影响,主要体现在三个方面。前端将会重新回归到web应用工程师这一职能在最前面说了,前端是社会分工的细化,大约起源于2007年左右,在此之前是没有专门的前端开发角色的,通常称作web工程师或网站工程师,早期的网页大都是服务器渲染,使用asp、php、jsp等server page技术,js仅仅是web工程师需要掌握的小小技能之一,但是随着web 2.0及互联网、移动互联网、电子商务的发展,需要专门的人专注于编写具备很好兼容性和体验的UI,因此逐渐产生了专注于浏览器及移动端的前端工程师。但是前端技术领域逐渐趋于稳定,伴随着十几年的发展,各种开箱即用的库、垂直方案以及工程手段唾手可得,甚至目前出现了一些辅助工具可以把设计师的视觉稿生成UI代码,前端可以安心并且以非常低的成本编写UI和业务逻辑,而不用耗费大量精力在选型、造轮、还原视觉、处理兼容性、性能优化、调试和部署上,这种情况,前后端工种分离造成的协同成本反而放大了,因为在前后端角色分离的情况下,后端往往还会充当bff层的角色,比如为前端表现层封装各种api gateway,经常出现相互等待、联调协议的情况,而且bff层通常只是一些数据的加工,其他的角色经过短期的培训可以快速上手,因此前端一直在尝试接入到server端的bff层。在15年前端开始推广使用nodejs来部署应用,阿里内外也出现了不少nodejs的框架,如业界的express,在生产环境,包括给买家、商家以及内部人员使用的系统,有不少使用了nodejs,但是到今年2019年,再来回顾一下,发现这个数字并没有超出预期。造成这一现象的原因,个人认为归根到底还是因为分工太细导致的前端对服务器知识的缺乏,nodejs本身的定位是服务器技术,本质上在服务器要面对的问题与java无异,现有的前端jd招聘的人才,鲜有能在服务器上工作游刃有余的人,除非专门招的nodejs人才,server服务的长期运行会暴露很多问题,比如接口很慢,进程core,cpu飙升,内存泄漏等,另外负载均衡、扩缩容,高并低延等知识,大部分前端都是没有这些经验的。云计算的本质就是要让业务开发专注于业务逻辑,业务之下的硬件及软件设施都是按需采买,开箱即用,而serverless理念及相关技术,将使得开发人员不再需要关心应用及机器的问题,甚至连流量能不能撑住也不用关心了,它能自动扩缩容,因此,未来web开发人员的运维成本会大幅降低,前端也可介入到bff层的开发,而后端可以聚焦于数据处理、业务逻辑与算法。这一变革符合研发效能提升的背景,未来的云设施将会做得非常厚,非常专业、稳定,而前台开发人员可以快速地,最低成本地在云设施上建立业务逻辑,前端和服务器的前端(对整个请求链路来说,前端是相对的,只要离客户请求更近的角色都可以称自己为前端),分工将没那么明确,以前的浏览器端的前端,将逐步承担一部分服务器端接入层以及bff层的工作,返璞归真,回到历史上曾经的web开发工程师角色,这是对前端最大的一个变革。当然,serverless技术让前端回归到传统的web层,不代表前端不用掌握服务器知识了,掌握操作系统内核及网络编程知识仍有助于你编写高性能、高可用的业务应用。实时SSR将成为展示端UI的主要开发模式最早的web开发,其实处理UI都是以服务器渲染为主,比如perl、php等动态网页技术,但是在前端逐渐成为一个工种开始负责了绝大部分UI开发,并且技术域逐渐缩小到客户端范围之后,网页静态化以及客户端的渲染逐渐成为主流。但是这种模式对用户体验肯定是有问题的,导致了较多的白屏时间,而由于新的前端库如react和vue在vdom这层的抽象,服务器渲染的技术成本更低,因此SSR在最近几年,又逐步开始流行。但是SSR的难点恰恰是前面说的,服务器端人才的匮乏,虽然nodejs和vdom的普及提升了SSR的实施效率,但由于服务器知识的缺乏,通常只有少部分具备综合知识的前端会深入的实践这一领域。serverless技术的普及将把这个问题消除掉,借助于serverless技术,前端可以快速搭建一个ssr的场景,在服务器取数,在服务器渲染,直出html给到客户端,而不用关心这个渲染服务能否扛得住流量,会不会挂掉,这些事情云设施供应商会去解决。在前面说过,react有一个核心理念就是把UI看成函数,如果说一个页面是多个组件组成的,那一个组件是函数,我们可以把一个页面看成是多个函数的组合,不同函数的组合,组合成不同的导购场景,如果把一个函数看成一个微服务,一个场景就是微服务的聚合,这恰恰是faas的理念。通过serverless低成本地实时ssr,可以让客户体验更好,借助算法和大数据,还可以快速实现UI的千人n面,构建真正的导购大脑。基于场景的云开发(web ide)将成为主流开发模式在提到serverless技术的时候,有一个关联的领域不得不提,那就是web ide,很多互联网大型企业把一个web ide当成了云的基础设施并且大力投资,这是为何?个人认为有两个原因。第一个是因为serverless目前在业界使用以垂直场景为主,他们有一个共同点,就是代码足够标准、规范,场景较为垂直,代码复杂度不是很大,而且借助web ide可以快速地与云平台结合,做到一键发布,因此这种场景,是比较适合轻量的在线编码到部署全链路打通的。另一个原因是,目前所有的云设施解决的是业务运行问题,但是软件开发有一个非常大但是大部分人可能会忽略的痛点,那就是环境问题,相信很多人都有那种clone别人的项目但是废了九牛二虎之力都无法启动的问题,因为要装各种环境啊?另外就是和别人联调的时候,是不是因为各种环境部署缺失的问题,联调效率很低?借助容器如docker等技术,软件的运行及调试环境,可以快速地移植给别人复用,而目前基于js的代码编辑器已经非常强大,vscode editor就是基于js编写并且沉淀出一个库 Monaco Editor,因此可以说,大部分认为web ide可能在语法提示、智能感知上比不上本地ide的想法是过时了。同时web ide可以快速地与平台集成,深入定制,打通业务平台,一键部署,极大地提升研发效率。web ide还能解决跨地办公的问题,因为解决了环境准备这一老大难问题,你可以在家里,在公司,甚至在火车上,快速编写并交付代码。因此未来的paas平台,都将关联一个深度定制的web ide,需要编写业务逻辑时,一个连接跳转到web ide即可编码,完全无需关心本地环境问题,做到真正的envless。比如你要开发一个TMS模块,那么点击”新建“,跳到了web ide,代码会帮你初始化好,点击运行,会在云端启动服务器运行该组件,编写好之后,一键即可发布到TMS。对于各种faas、api gateway系统以及其他云服务,都会一样,web ide将成为企业云化的基础设施。尽管阿里云目前还未发布媲美cloud9以及coding.net的web ide,但是很欣喜地看到集团内部已经涌现出一些优秀的产品,如aone的ide,以及数据平台的app studio,其体验已经接近业界顶级水准。结语在云计算领域,serverless将会掀起一场革命,即使看起来与这一领域关联不大的前端,也会经受即ajax、nodejs以及react之后的又一重大变革,你做好应对了吗?本文作者:伐薪阅读原文本文来自云栖社区合作伙伴“阿里技术”,如需转载请联系原作者。

March 13, 2019 · 1 min · jiezi

安全多方计算新突破!阿里首次实现“公开可验证” 的安全方案

阿里妹导读:近日,阿里安全双子座实验室与马里兰大学等高校合作的论文《Covert Security with Public Verifiability: Faster, Leaner, and Simpler 》【1】被欧洲密码年会(Eurocrypt)2019接收。这是国内公司在安全多方计算领域的第一篇顶会论文(Eurocrypt2018只有3篇大陆作者论文,难度可见一斑)。今天,我们邀请阿里高级安全专家鸿程,深入解读业界首个“公开可验证(PVC)” 的安全两方计算方案。安全多方计算介绍安全多方计算( Secure Multi-Party Computation,MPC)于1986 年由姚期智院士提出【2】。安全多方计算协议允许多个数据所有者在互不信任的情况下进行协同计算,输出计算结果,并保证任何一方均无法得到除应得的计算结果之外的其他任何信息。换句话说,MPC技术可以获取数据使用价值,却不泄露原始数据内容。互联网已经完成了从IT时代向DT时代的转变,数据已经成为DT时代企业的核心竞争力。数据作为一种新能源,只有流动起来才能产生价值。不过,大多数企业考虑到数据安全和个人隐私等问题,对数据共享都非常谨慎。而MPC对打破数据孤岛,实现数据的可控共享,具有重要的理论和现实意义。MPC方案主要可分为基于混淆电路(Garbled Circuit,GC)和基于秘密共享两种。本文主要关注GC类方案。不经意传输(Oblivious Transfer)我们首先介绍一种基础的安全多方计算协议:不经意传输(Oblivious Transfer, OT)。来看一个例子:假设某旅行社拥有N个景点的旅游资料,小淘想去其中的A景点游玩,希望向旅行社购买相关资料做好出游功课。但是小淘非常在意自己的隐私,不希望向旅行社泄露自己的目的地是哪里。因此双方希望这笔交易能够满足以下隐私条件:小淘不希望向旅行社泄露“我准备去A景点”这一信息;旅行社只希望出售小淘出钱购买的那份资料,而不泄露小淘未购买的N-1份资料;粗看起来这种隐私条件似乎是无法满足的:旅行社只要把景点A的资料给到小淘,就必然了解了“小淘正在关注A景点”这一信息;除非旅行社把所有N份资料都给出,但是这又违背了旅行社的利益;但是神奇的OT可以让交易在这种“不可能的条件”下达成。简而言之,在OT协议中,旅行社把他拥有的N份资料使用某种双方协商同意的加密算法和参数进行加密,然后发送给小淘;小淘可以从密文中解密出A的资料,而无法解密出其他N-1份资料。OT除了可以直接用于构造MPC方案之外,也是GC等许多MPC方案的基石。混淆电路我们知道,任意函数最后在计算机语言内部都是由加法器、乘法器、移位器、选择器等电路表示,而这些电路最后都可以仅由AND和XOR两种逻辑门组成。一个门电路其实就是一个真值表,例如AND门的真值表就是:例如其中右下格表示两根输入线(wire)都取1时,输出wire=1:即 1 AND 1 = 1。假设我们把每个wire都使用不同的密钥加密,把真值表变成这样:例如其中右下格表示如果门的输入是b和d,那么输出加密的f(密钥是b和d)。这个门从控制流的角度来看还是一样的,只不过输入和输出被加密了,且输出必须使用对应的输入才能解密,解密出的f又可以作为后续门的输入。这种加密方式就称为“混淆电路(Garbled Circuit,GC)”。将电路中所有的门都按顺序进行这样的加密,我们就得到了一个GC表示的函数。这个函数接收加密的输入,输出加密的结果。假设有两个参与方A和B各自提供数据a、b,希望安全的计算约定的函数F(a,b),那么一种基于GC的安全两方计算协议过程可以非正式的描述如下:细心的同学一定会指出:第4步中A怎么可以接触B的输入b呢?这不是违背了安全多方计算的假设吗?这里就需要使用OT,A扮演Sender,B扮演Receiver,让B从A处得到Encrypt( b),却不向A透露b的内容。如图所示:需要注意的是,上述流程只是最原始的GC方法的不严谨描述,GC后续还有Point & Permute、Free XOR、Half Gates等多种细节优化,随着最近几年的研究进展,GC的性能已经差不多可以实用了。以求两个百万维向量的汉明距离(Hamming Distance)为例(应用场景是两份数据求相似度,却互相不泄露数据内容),这样的安全两方计算已经可以在1.5秒左右完成。安全多方计算的安全模型半诚实行为模型与恶意行为模型更细心的同学还会进一步提出问题:“怎么确保A给B的就是一个正确的GC呢?例如A和B商定要比a和b的大小,商定了F(a,b)=a>b?1:0,但是A可以制作一个别的函数的GC,例如F(a,b)=b的第1个比特,这样显然是会侵害B的隐私的,但是由于函数是以GC形式发给B的,B是没有办法发现这个问题?”这确实是一个安全问题,事实上,GC还存在如selective failure等其他更多的安全问题。在介绍解决方案之前,我们需要先定义安全多方计算的安全模型。安全多方计算的安全模型包含多个角度的内容,在上述上下文中,我们关注的是其中的“行为模型”,即参与方可能进行何种行为以获取其他方的隐私。常见的行为模型包括“半诚实(Semi Honest)”和“恶意(Malicious)”两种。前者假设所有参与方都是忠实的按照协议步骤进行执行,只是想通过协议内容推测其他方的隐私,而后者假设恶意参与方为了获取其他方的隐私可以不遵循协议内容。用扑克牌打个不严谨的比方,半诚实的牌友会试图从自己的手牌和已经打出的牌来推测他人的手牌,但是还是遵循扑克牌规则的;而一个恶意的牌友则换牌、偷牌等手段无所不用。可见,本节开始提出的问题属于恶意行为的范畴,而原始的GC只能说在半诚实行为模型下是安全的,无法抵御恶意行为攻击。有许多对GC方案的改进方案可以达到恶意行为模型下的安全性,但是它们都需要付出很大的性能代价:仍然以求两个百万维向量的汉明距离为例,其中最快的方法也需要10秒+,比同等的半诚实方案慢7倍以上。事实上,经过我们的调研,若想真正的实现支持大规模数据的MPC产品,基本上只能考虑半诚实方案。这严重影响了安全多方计算的实用性。公开可验证(Public Verifiable Covert, PVC)行为模型PVC是在半诚实、恶意之间的一种折中。其主要思想是:每个参与方的所有行为都自动带有类似签名的机制以供其他参与方存证。假设某个参与方实施恶意行为,那么其他参与方可以有的概率发现(称为威慑因子,一般>=50%,不能100%发现,因为100%那就直接满足恶意行为模型了)这一恶意行为,并将该行为及其签名公开,令作恶者承受名誉损失。考虑到名誉对一个数据所有者的重要性(例如此后可能再也找不到合作),50%左右的威慑力已经足以让理性者不考虑作恶。PVC模型最开始是由学者在Asiacrypt2012【3】提出,Asiacrypt2015【4】上也有学者提出相关的改进方案,但是这些方案不仅效率较低,而且只有复杂的理论描述,实现可能性低。我们提出的新型PVC方案不仅协议简洁,性能有大幅提升,而且首次进行了完整的代码实现。仍然以求两个百万维向量的汉明距离为例,使用我们威慑因子为50%的PVC方法大概只需要2.5秒。以下仍假设有两个参与方A和B各自提供数据a、b,希望安全的计算约定的函数F(a,b),以威慑因子=50%为例,给出我们的PVC方案的非正式描述:A选择两个随机种子s1和s2, B和A运行OT随机选择其中一个(不妨设B获取了s1);A使用s1和s2分别生成GC1和GC2;B和A运行OT获取GC1中B输入wire的加密值(我们后面可以看到GC1不会真正被使用,因此这里可以不与b对应,比如是任意常数值的密文);B和A运行OT获取GC2中B输入wire对应的b的加密值;A对GC1进行Hash,并把Hash发给B;A对GC2进行Hash,并把Hash发给B;A对上述所有流程进行签名,并把签名发送给B;B由于有s1,因此可以自行生成GC1,可以自己模拟第3步和第5步;如果结果与A发的不一致,则公布相关签名作为A作恶证据。如果一致,就用GC2进行真实计算。可见,A如果作恶,总有50%的概率被B抽查到(因为A不知道B到底掌握了哪个GC的随机种子)。因此理性的A会选择不作恶,忠实的执行安全多方计算协议。需要再次强调的是,为便于理解,所有的协议都仅仅是非正式描述,有兴趣进一步研究细节的同学欢迎参阅我们的论文【1】。总结我们与马里兰大学等高校合作,首次实现了一种“公开可验证(PVC)” 的安全两方计算方案,这种方案的性能接近半诚实方案,同时其PVC特性能够对作弊行为形成威慑力,令其具有远强于半诚实模型的安全性,具有很高的实用价值。本文作者:鸿程阅读原文本文来自云栖社区合作伙伴“ 阿里技术”,如需转载请联系原作者。

March 13, 2019 · 1 min · jiezi

开发函数计算的正确姿势——使用 brotli 压缩大文件

大文件问题函数计算对上传的 zip 代码包尺寸限制为 50M。某些场景中代码包中会超过这一限制,比如二进制 serverless-chrome 经过一番裁剪以后 ZIP 压缩包的体积为 43.4M,类似的还有 liboffice ,此外常见的还有机器学习训练的模型文件。目前解决大文件问题有三种方法采用更高压缩比的算法,比如本文介绍的 brotli 算法采用 OSS 运行时下载采用 NAS 文件共享简单的比较一下这三种方法的优劣方法优点缺点高密度压缩发布简单,启动最快上传代码包较慢;要写解压代码;大小受限制不超过 50 MOSS下载解压后文件不超过 512 M需要预先上传至 OSS;要写下载和解压代码,大概 50M/s 的下载速度NAS文件大小没有限制,无需压缩需要预先上传至 NAS;VPC 环境有冷启动时延(~5s)正常情况下如果代码包能控制在 50M 以下启动较快。而且工程上也比较简单,数据和代码放在一起,不需要额外的写脚本去同步更新 OSS 或者 NAS。压缩算法Brotli 是 Google 工程师开发的开源压缩算法,目前已经被新版的主流浏览器支持,作为 HTTP 传输的压缩算法。下面是在网上找到的关于 Brotli 和其他常见压缩算法对比基准测试。从上面三幅图我们可以看出:相比于 gzip、xz 和 bz2,brotli 有最高的压缩比,接近于 gzip 的解压速度,以及最慢的压缩速度。然而在我们的场景对于压缩慢这一缺点不敏感,压缩任务只要在开发准备物料的阶段执行一次就好了。制作压缩文件下面我先介绍一下如何制作压缩文件。下面的代码和用例都来自于项目 packed-selenium-java-example 。安装 brotli 命令Mac 用户brew install brotliWindows 用户可以去这个界面下载,https://github.com/google/brotli/releases打包并压缩打包前两个文件大小分别为 7.5M 和 97M╭─ ~/D/test1[◷ 18:15:21]╰─ lltotal 213840-rwxr-xr-x 1 vangie staff 7.5M 3 5 11:13 chromedriver-rwxr-xr-x 1 vangie staff 97M 1 25 2018 headless-chromium使用 GZip 打包并压缩,大小为 44 M。╭─ ~/D/test1[◷ 18:15:33]╰─ tar -czvf chromedriver.tar chromedriver headless-chromiuma chromedrivera headless-chromium╭─ ~/D/test1[◷ 18:16:41]╰─ lltotal 306216-rwxr-xr-x 1 vangie staff 7.5M 3 5 11:13 chromedriver-rw-r–r– 1 vangie staff 44M 3 6 18:16 chromedriver.tar-rwxr-xr-x 1 vangie staff 97M 1 25 2018 headless-chromiumtar 去掉 z 选项再打包一遍,大小为 104M╭─ ~/D/test1[◷ 18:16:42]╰─ tar -cvf chromedriver.tar chromedriver headless-chromiuma chromedrivera headless-chromium╭─ ~/D/test1[◷ 18:17:06]╰─ lltotal 443232-rwxr-xr-x 1 vangie staff 7.5M 3 5 11:13 chromedriver-rw-r–r– 1 vangie staff 104M 3 6 18:17 chromedriver.tar-rwxr-xr-x 1 vangie staff 97M 1 25 2018 headless-chromium压缩后的大小为 33M,相比 Gzip 的 44M 小了不少。耗时也非常的感人 6 分 18 秒,Gzip 只要 5 秒。╭─ ~/D/test1[◷ 18:17:08]╰─ time brotli -q 11 -j -f chromedriver.tarbrotli -q 11 -j -f chromedriver.tar 375.39s user 1.66s system 99% cpu 6:18.21 total╭─ ~/D/test1[◷ 18:24:23]╰─ lltotal 281552-rwxr-xr-x 1 vangie staff 7.5M 3 5 11:13 chromedriver-rw-r–r– 1 vangie staff 33M 3 6 18:17 chromedriver.tar.br-rwxr-xr-x 1 vangie staff 97M 1 25 2018 headless-chromium运行时解压缩下面以 java maven 项目为例添加解压依赖包<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-compress</artifactId> <version>1.18</version></dependency><dependency> <groupId>org.brotli</groupId> <artifactId>dec</artifactId> <version>0.1.2</version></dependency>commons-compress 是 apache 提供的解压缩工具包,对于各种压缩算法提供一致的抽象接口,其中对于 brotli 算法只支持解压,这里足够了。org.brotli:dec 包是 Google 提供的 brotli 解压算法的底层实现。实现 initialize 方法public class ChromeDemo implements FunctionInitializer { public void initialize(Context context) throws IOException { Instant start = Instant.now(); try (TarArchiveInputStream in = new TarArchiveInputStream( new BrotliCompressorInputStream( new BufferedInputStream( new FileInputStream(“chromedriver.tar.br”))))) { TarArchiveEntry entry; while ((entry = in.getNextTarEntry()) != null) { if (entry.isDirectory()) { continue; } File file = new File("/tmp/bin", entry.getName()); File parent = file.getParentFile(); if (!parent.exists()) { parent.mkdirs(); } System.out.println(“extract file to " + file.getAbsolutePath()); try (FileOutputStream out = new FileOutputStream(file)) { IOUtils.copy(in, out); } Files.setPosixFilePermissions(file.getCanonicalFile().toPath(), getPosixFilePermission(entry.getMode())); } } Instant finish = Instant.now(); long timeElapsed = Duration.between(start, finish).toMillis(); System.out.println(“Extract binary elapsed: " + timeElapsed + “ms”); }}实现 FunctionInitializer 接口的 initialize 方法。解压过程刚开始是四层嵌套流,作用分别如下:FileInputStream 读取文件BufferedInputStream 提供缓存,介绍系统调用带来的上下文切换,提示读取的速度BrotliCompressorInputStream 对字节流进行解码TarArchiveInputStream 把 tar 包里的文件逐个解出来然后 Files.setPosixFilePermissions 的作用是还原 tar 包中文件的权限。代码太长此处略去,参阅 packed-selenium-java-exampleInstant start = Instant.now();…Instant finish = Instant.now();long timeElapsed = Duration.between(start, finish).toMillis();System.out.println(“Extract binary elapsed: " + timeElapsed + “ms”);上面的代码段会打印出解压的耗时,真实执行大概在 3.7 s 左右。最后不要忘记在 template.yml 里配置上 Initializer 和 InitializationTimeout参考阅读https://www.opencpu.org/posts/brotli-benchmarks/https://github.com/vangie/packed-selenium-java-example本文作者:倚贤阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 8, 2019 · 2 min · jiezi

刚刚,阿里宣布开源Flutter应用框架Fish Redux!

3月5日,闲鱼宣布在GitHub上开源Fish Redux,Fish Redux是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用,它最显著的特征是 函数式的编程模型、可预测的状态管理、可插拔的组件体系、最佳的性能表现。下文中,我们将详细介绍Fish Redux的特点和使用过程,以下内容来自InfoQ独家对闲鱼Flutter团队的采访和Fish Redux的开源文档。开源背景在闲鱼接入Flutter之初,由于我们的落地的方案希望是从最复杂的几个主链路进行尝试来验证flutter完备性的,而我们的详情整体来讲业务比较复杂,主要体现在两个方面:页面需要集中状态管理,也就是说页面的不同组件共享一个数据来源,数据来源变化需要通知页面所有组件。页面的UI展现形式比较多(如普通详情、闲鱼币详情、社区详情、拍卖详情等),工作量大,所以UI组件需要尽可能复用,也就是说需要比较好的进行组件化切分。在我们尝试使用市面上已有的框架(google提供的redux以及bloc)的时候发现,没有任何一个框架可以既解决集中状态管理,又能解决UI的组件化的,因为本身这两个问题有一定的矛盾性(集中vs分治)。因此我们希望有一套框架能解决我们的问题,fish redux应运而生。fish redux本身是经过比较多次的迭代的,目前大家看到的版本经过了3次比较大的迭代,实际上也是经过了团队比较多的讨论和思考。第一个版本是基于社区内的flutter_redux进行的改造,核心是提供了UI代码的组件化,当然问题也非常明显,针对复杂的详情和发布业务,往往业务逻辑很多,无法做到逻辑代码的组件化。第二个版本针对第一个版本的问题,做出了比较重大的修改,解决了UI代码和逻辑代码的分治问题,但同时,按照redux的标准,打破了redux的原则,对于精益求精的闲鱼团队来讲,不能接受;因此,在第三个版本进行重构时,我们确立了整体的架构原则与分层要求,一方面按照reduxjs的代码进行了flutter侧的redux实现,将redux的原则完整保留下来。另一方面针对组件化的问题,提供了redux之上的component的封装,并创新的通过这一层的架构设计提供了业务代码分治的能力。至此,我们完成了fish redux的基本设计,但在后续的应用中,发现了业务组装以后的代码性能问题,针对该问题,我们再次提供了对应的adapter能力,保障了在长列表场景下的big cell问题。目前,fish redux已经在线上稳定运行超过3个月以上,未来,期待fish redux给社区带来更多的输入。Fish Redux技术解析分层架构图架构图:主体自底而上,分两层,每一层用来解决不通层面的问题和矛盾,下面依次来展开。ReduxRedux 是来自前端社区的一个数据管理框架,对 Native开发同学来说可能会有一点陌生,我们做一个简单的介绍。Redux 是做什么的?Redux 是一个用来做可预测易调试的数据管理的框架。所有对数据的增删改查等操作都由 Redux 来集中负责。Redux 是怎么设计和实现的?Redux 是一个函数式的数据管理的框架。传统 OOP 做数据管理,往往是定义一些 Bean,每一个 Bean 对外暴露一些 Public-API 用来操作内部数据(充血模型)。函数式的做法是更上一个抽象的纬度,对数据的定义是一些 Struct(贫血模型),而操作数据的方法都统一到具有相同函数签名 (T, Action) => T 的 Reducer 中。FP:Struct(贫血模型) + Reducer = OOP:Bean(充血模型)同时 Redux 加上了 FP 中常用的 Middleware(AOP) 模式和 Subscribe 机制,给框架带了极高的灵活性和扩展性。贫血模型、充血模型请参考:https://en.wikipedia.org/wiki/Plain_old_Java_objectRedux 的缺点Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点。在我们实际使用 Redux 中面临两个具体问题:Redux 的集中和 Component 的分治之间的矛盾;Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性。Fish Redux 的改良Fish Redux 通过 Redux 做集中化的可观察的数据管理。然不仅于此,对于传统 Redux 在使用层面上的缺点,在面向端侧 flutter 页面纬度开发的场景中,我们通过更好更高的抽象,做了改良。一个组件需要定义一个数据(Struct)和一个 Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,我们解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。我们得到了理想的集中的效果和分治的代码。对社区标准的 followState、Action、Reducer、Store、Middleware 以上概念和社区的 ReduxJS 是完全一致的。我们将原汁原味地保留所有的 Redux 的优势。如果想对 Redux 有更近一步的理解,请参考:https://github.com/reduxjs/reduxComponent组件是对局部的展示和功能的封装。 基于 Redux 的原则,我们对功能细分为修改数据的功能(Reducer)和非修改数据的功能(副作用 Effect)。于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的行为。这是一种面向当下,也面向未来的拆分。在面向当下的 Redux 看来,是数据管理和其他。在面向未来的 UI-Automation 看来是 UI 表达和其他。UI 的表达对程序员而言即将进入黑盒时代,研发工程师们会把更多的精力放在非修改数据的行为、修改数据的行为上。组件是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。关于 ViewView 仅仅是一个函数签名: (T,Dispatch,ViewService) => Widget它主要包含三方面的信息视图是完全由数据驱动。视图产生的事件/回调,通过 Dispatch 发出“意图”,不做具体的实现。需要用到的组件依赖等,通过 ViewService 标准化调用。比如一个典型的符合 View 签名的函数。关于 EffectEffect 是对非修改数据行为的标准定义,它是一个函数签名: (Context, Action) => Object它主要包含四方面的信息接收来自 View 的“意图”,也包括对应的生命周期的回调,然后做出具体的执行。它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们不崇尚持有数据,而通过上下文来获取最新数据。它不修改数据, 如果修要,应该发一个 Action 到 Reducer 里去处理。它的返回值仅限于 bool or Future, 对应支持同步函数和协程的处理流程。比如良好的协程的支持:关于 ReducerReducer 是一个完全符合 Redux 规范的函数签名:(T,Action) => T一些符合签名的 Reducer:同时我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。所以有这样的公式 Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)。一个典型的组装:通过 Component 的抽象,我们得到了完整的分治,多纬度的复用,更好的解耦。AdapterAdapter 也是对局部的展示和功能的封装。它为 ListView 高性能场景而生,它是 Component 实现上的一种变化。它的目标是解决 Component 模型在 flutter-ListView 的场景下的 3 个问题:1)将一个"Big-Cell"放在 Component 里,无法享受 ListView 代码的性能优化;2)Component 无法区分 appear|disappear 和 init|dispose ;3)Effect 的生命周期和 View 的耦合,在 ListView 的场景下不符合直观的预期。概括的讲,我们想要一个逻辑上的 ScrollView,性能上的 ListView ,这样的一种局部展示和功能封装的抽象。做出这样独立一层的抽象是我们看实际的效果,我们对页面不使用框架Component,使用框架 Component+Adapter 的性能基线对比。Reducer is long-lived, Effect is medium-lived, View is short-lived.我们通过不断的测试做对比,以某 Android机为例:使用框架前 我们的详情页面的 FPS,基线在 52FPS;使用框架, 仅使用 Component 抽象下,FPS 下降到 40, 遭遇“Big-Cell”的陷阱;使用框架,同时使用 Adapter 抽象后,FPS 提升到 53,回到基线以上,有小幅度的提升。Directory推荐的目录结构会是这样sample_page– action.dart– page.dart– view.dart– effect.dart– reducer.dart– state.dartcomponentssample_component– action.dart– component.dart– view.dart– effect.dart– reducer.dart– state.dart上层负责组装,下层负责实现, 同时会有一个插件提供, 便于我们快速填写。以闲鱼的详情场景为例的组装:组件和组件之间,组件和容器之间都完全的独立。Communication Mechanism组件|适配器内通信组件|适配器间内通信简单的描述:采用的是带有一段优先处理的广播, self-first-broadcast。发出的 Action,自己优先处理,否则广播给其他组件和 Redux 处理。最终我们通过一个简单而直观的 dispatch 完成了组件内,组件间(父到子,子到父,兄弟间等)的所有的通信诉求。Refresh Mechanism数据刷新局部数据修改,自动层层触发上层数据的浅拷贝,对上层业务代码是透明的。层层的数据的拷贝:一方面是对 Redux 数据修改的严格的 follow。另一方面也是对数据驱动展示的严格的 follow。视图刷新扁平化通知到所有组件,组件通过 shouldUpdate 确定自己是否需要刷新。Fish Redux的优点数据的集中管理通过 Redux 做集中化的可观察的数据管理。我们将原汁原味地保留所有的 Redux 的优势,同时在 Reducer 的合并上,变成由框架代理自动完成,大大简化了使用 Redux 的繁琐度。组件的分治管理组件既是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。View、Reducer、Effect 隔离将组件拆分成三个无状态的互不依赖的函数。因为是无状态的函数,它更易于编写、调试、测试、维护。同时它带来了更多的组合、复用和创新的可能。声明式配置组装组件、适配器通过自由的声明式配置组装来完成。包括它的 View、Reducer、Effect 以及它所依赖的子项。良好的扩展性核心框架保持自己的核心的三层关注点,不做核心关注点以外的事情,同时对上层保持了灵活的扩展性。框架甚至没有任何的一行的打印的代码,但我们可通过标准的 Middleware 来观察到数据的流动,组件的变化。在框架的核心三层外,也可以通过 dart 的语言特性 为 Component 或者 Adapter 添加 mixin,来灵活的组合式地增强他们的上层使用上的定制和能力。框架和其他中间件的打通,诸如自动曝光、高可用等,各中间件和框架之间都是透明的,由上层自由组装。精小、简单、完备它非常小,仅仅包含 1000 多行代码;它使用简单,完成几个小的函数,完成组装,即可运行;它是完备的。关于未来开源之后,闲鱼打算通过以下方式来维护Fish Redux:通过后续的一系列的对外宣传,吸引更多的开发者加入或者使用。目前Flutter生态里,应用框架还是空白,有机会成为事实标准;配合后续的一系列的闲鱼Flutter移动中间件矩阵做开源;进一步提供,一系列的配套的开发辅助调试工具,提升上层Flutter开发效率和体验。Fish Redux 目前已在阿里巴巴闲鱼技术团队内多场景,深入应用。最后 Talk is cheap, Show me the code,我们今天正式在GitHub上开源,更多内容,请到GitHub了解。GitHub地址:https://github.com/alibaba/fish-redux本文作者:闲鱼技术-吉丰阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 7, 2019 · 2 min · jiezi

开发函数计算的正确姿势——运行 Selenium Java

前言首先介绍下在本文出现的几个比较重要的概念:函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息参考。Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档参考。备注: 本文介绍的技巧需要 Fun 版本大于等于 2.10.2。依赖工具本项目是在 MacOS 下开发的,涉及到的工具是平台无关的,对于 Linux 和 Windows 桌面系统应该也同样适用。在开始本例之前请确保如下工具已经正确的安装,更新到最新版本,并进行正确的配置。DockerFunFcliFun 和 Fcli 工具依赖于 docker 来模拟本地环境。对于 MacOS 用户可以使用 homebrew 进行安装:brew cask install dockerbrew tap vangie/formulabrew install funbrew install fcliWindows 和 Linux 用户安装请参考:https://github.com/aliyun/fun/blob/master/docs/usage/installation.mdhttps://github.com/aliyun/fcli/releases安装好后,记得先执行 fun config 初始化一下配置。注意, 如果你已经安装过了 fun,确保 fun 的版本在 2.10.2 以上。$ fun –version2.10.1快速开始初始化使用 fun init 命令可以快捷地将本模板项目初始化到本地。fun init vangie/selenium-java-example安装依赖$ fun install…本地测试测试代码 ChromeDemo 的内容为:public class ChromeDemo implements StreamRequestHandler { public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { System.setProperty(“webdriver.chrome.driver”, “/code/chromedriver”); ChromeOptions options = new ChromeOptions(); options.setBinary("/code/headless-chromium"); options.addArguments("–disable-extensions"); // disabling extensions options.addArguments("–disable-gpu"); // applicable to windows os only options.addArguments("–disable-dev-shm-usage"); // overcome limited resource problems options.addArguments("–no-sandbox"); // Bypass OS security model options.addArguments("–headless"); WebDriver driver = new ChromeDriver(options); driver.get(“https://ide.fc.aliyun.com”); outputStream.write((“Page title is: " + driver.getTitle() + “\n”).getBytes()); driver.quit(); }}本地运行$ mvn package && fun local invoke selenium…FC Invoke Start RequestId: 68c83b4c-b053-479c-9b0e-9503582ccb56handle user request is com.aliyun.fc.selenium.ChromeDemo::handleRequestcache is null!Starting ChromeDriver 2.35.528139 (47ead77cb35ad2a9a83248b292151462a66cd881) on port 20652Only local connections are allowed.Mar 05, 2019 11:34:27 AM org.openqa.selenium.remote.ProtocolHandshake createSessionINFO: Detected dialect: OSSPage title is: 云端集成开发环境FC Invoke End RequestId: 68c83b4c-b053-479c-9b0e-9503582ccb56RequestId: 68c83b4c-b053-479c-9b0e-9503582ccb56 Billed Duration: 5265 ms Memory Size: 1998 MB Max Memory Used: 240 MB部署$ mvn package && fun deploy执行$ fcli function invoke -s chrome -f selenium Page title is: 云端集成开发环境关于文件尺寸由于 chromedriver 和 headless-chromium 压缩后体积已经非常接近 50MB,留给用户 Jar 的空间非常少,所以另外制作了一个高压缩比版本,使用压缩比更高的 brotli 算法进行压缩,压缩后的大小为 32.7MB。然后在运行时使用 initializer 进行解压,解压耗时大约为 3.7 S。https://github.com/vangie/packed-selenium-java-example参考阅读https://github.com/smithclay/lambdiumhttps://medium.com/clog/running-selenium-and-headless-chrome-on-aws-lambda-fb350458e4df本文作者:倚贤阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 7, 2019 · 1 min · jiezi

前端进击的巨人(八):浅谈函数防抖与节流

本篇课题,或许早已是烂大街的解读文章。不过春招系列面试下来,不少伙伴们还是似懂非懂地栽倒在(~面试官~)深意的笑容之下,权当温故知新。JavaScript的执行过程,是基于栈来进行的。复杂的程序代码被封装到函数中,程序执行时,函数不断被推入执行栈中。所以 “执行栈” 也称 “函数执行栈”。函数中封装的代码块,一般都有相对复杂的逻辑处理(计算/判断),例如函数中可能会涉及到 DOM 的渲染更新,复杂的计算与验证, Ajax 数据请求等等。前端页面的操作权,大部分都是属于浏览断的客户爸爸们(单身三十年的手速,惹不起惹不起!!!)。如果函数被频繁调用,造成的性能开销绝对不只一点点。前: DOM 频繁重绘的卡顿让客户爸爸们想把你揪出来一顿大招。。。后: 后端同学正在提刀赶来的路上:“为什么我的接口被你玩挂了”。。。既要提升用户体验,又要减少后端服务开销,可见我们大前端的使命不只一页PPT。说好前因,接着就是后果了。既然有优化的需求,必然就要有相应的解决方案。隆重请出主角: “防抖” 与 “节流”。防抖(debounce)在事件被触发 n 秒后再执行回调函数,如果在这 n 秒内又被触发,则重新计时延迟时间。生活化理解:英雄的技能条,技能条读完才能使用技能(R大招60s)防抖的实现方式分两种 “立即执行” 和 “非立即执行”,区别在于第一次触发时,是否立即执行回调函数。非立即执行”非立即执行防抖“ 指事件触发后,回调函数不会立即执行,会在延迟时间 n 秒后执行,如果 n 秒内被调用多次,则重新计时延迟时间// e.g. 防抖 - 非立即执行function debounce(func, delay) { var timeout; return function() { var context = this; var args = arguments; // && 短路运算 == if(timeout) else {…} timeout && clearTimeout(timeout); timeout = setTimeout(function(){ func.apply(context, args); }, delay); }}// 调用var printUserName = debounce(function(){ console.log(this.value);}, 800);document.getElementById(‘username’) .addEventListener(‘keyup’, printUserName);立即执行“立即执行防抖” 指事件触发后,回调函数会立即执行,之后要想触发执行回调函数,需等待 n 秒延迟// e.g. 防抖 - 立即执行function debounce(func, delay) { var timeout; return function() { var context = this; var args = arguments; callNow = !timeout; timeout = setTimeout(function() { timeout = null; }, delay); callNow && func.apply(context, args); }}函数防抖原理:通过维护一个定时器,其延迟计时以最后一次触发为计时起点,到达延迟时间后才会触发函数执行。节流(throttle)规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效(间隔执行)生活化理解:FPS射击游戏子弹射速(即使按住鼠标左键,射出子弹的速度也是限定的)水龙头的滴水(水滴攒到一定重量才会下落)函数节流实现的方式有 “时间戳” 和 “定时器” 两种。时间戳// e.g. 节流 - 时间戳function throttle(func, delay) { var lastTime = 0; return function() { var context = this; var args = arguments; var nowTime = +new Date(); if (nowTime > lastTime + delay) { func.apply(context, args) lastTime = nowTime; } }}“时间戳” 的方式,函数在时间段开始时执行。缺点:假定函数间隔1s执行,如果最后一次停止触发,卡在4.2s,则不会再执行。定时器// e.g. 节流 - 定时器function throttle(func, delay) { var timeout; return function() { var context = this; var args = arguments; if (!timeout) { setTimeout(function(){ func.apply(context, args); timeout = null; }, delay) } }}“定时器” 的方式,函数在时间段结束时执行。可理解为函数并不会立即执行,而是等待延迟计时完成才执行。(由于定时器延时,最后一次触发后,可能会再执行一次回调函数)时间戳 + 定时器(互补优化)// e.g. 节流 - 时间戳 + 定时器function throttle(func, delay) { let lastTime, timeout; return function() { let context = this; let args = arguments; let nowTime = +new Date(); if (lastTime && nowTime < lastTime + delay) { timeout && clearTimeout(timeout); timeout = setTimeout(function(){ lastTime = nowTime; func.apply(context, args); }, delay); } else { lastTime = nowTime; func.apply(context, args); } }}合并优化的原理:“时间戳”方式让函数在时间段开始时执行(第一次触发立即执行),“定时器”方式让函数在最后一次事件触发后(如4.2s)也能触发。函数节流原理:一定时间内只触发一次,间隔执行。通过判断是否到达指定触发时间,间隔时间固定。“防抖” 与 “节流” 的异同相同:都是防止某一时间段内,函数被频繁调用执行,通过时间频率控制,减少回调函数执行次数,来实现相关性能优化。区别:“防抖”是某一时间内只执行一次,最后一次触发后过段时间执行,而“节流”则是间隔时间执行,间隔时间固定。“防抖” 与 “节流” 的应用场景防抖文本输入搜索联想文本输入验证(包括 Ajax 后端验证)节流鼠标点击监听滚动 scroll窗口 resizemousemove 拖拽应用场景还有很多,具体场景需具体分析。只要涉及高频的函数调用,都可参考函数防抖节流的优化方案。鼓起勇气写在结尾:以上代码都不是 “完美” 的 “防抖 / 节流” 实现代码!!!仅就实现方式和基本原理,浅谈分解一二。实际代码开发中,一般会引入lodash 相对 “靠谱” 的第三方库,帮我们去实现防抖节流的工具函数。有兴趣的伙伴们可阅读 lodash 相关源码,加深印象理解可再读以下参考文章。参考文章7分钟理解JS的节流、防抖及使用场景函数防抖和节流 ...

March 4, 2019 · 2 min · jiezi

如何制作可以在 MaxCompute 上使用的 crcmod

之前我们介绍过在 PyODPS DataFrame 中使用三方包。对于二进制包而言,MaxCompute 要求使用包名包含 cp27-cp27m 的 Wheel 包。但对于部分长时间未更新的包,例如 oss2 依赖的 crcmod,PyPI 并未提供 Wheel 包,因而需要自行打包。本文介绍了如何使用 quay.io/pypa/manylinux1_x86_64 镜像制作可在 MaxCompute 上使用的 Wheel 包。本文参考 https://github.com/pypa/manylinux ,quay.io/pypa/manylinux1_x86_64 镜像也是目前绝大多数 Python 项目在 Travis CI 上打包的标准工具,如有进一步的问题可研究该项目。1. 准备依赖项不少包都有依赖项,例如 devel rpm 包或者其他 Python 包,在打包前需要了解该包的依赖,通常可以在 Github 中找到安装或者打包的相关信息。对于 crcmod,除 gcc 外不再有别的依赖,因而此步可略去。2. 修改 setup.py 并验证(建议在 Mac OS 或者 Linux 下)较旧的 Python 包通常不支持制作 Wheel 包。具体表现为在使用 python setup.py bdist_wheel 打包时报错。如果需要制作 Wheel 包,需要修改 setup.py 以支持 Wheel 包的制作。对于一部分包,可以简单地将 distutils 中的 setup 函数替换为 setuptools 中的 setup 函数。而对于部分自定义操作较多的 setup.py,需要详细分析打包过程,这一项工作可能会很复杂,本文就不讨论了。例如,对于 crcmod,修改 setup.py 中的from distutils.core import setup为from setuptools import setup即可。修改完成后,在项目根目录执行python setup.py bdist_wheel如果没有报错且生成的 Wheel 包可在本地使用,说明 setup.py 已可以使用。3. 准备打包脚本在项目中新建 bin 目录,并在其中创建 build-wheel.sh:mkdir bin && vim bin/build-wheel.sh在其中填入以下内容:#!/bin/bash# modified from https://github.com/pypa/python-manylinux-demo/blob/master/travis/build-wheels.shset -e -x# Install a system package required by our library# 将这里修改为安装依赖项的命令# Compile wheelsPYBIN=/opt/python/cp27-cp27m/bin# 如果包根目录下有 dev-requirements.txt,取消下面的注释# “${PYBIN}/pip” install -r /io/dev-requirements.txt"${PYBIN}/pip" wheel /io/ -w wheelhouse/# Bundle external shared libraries into the wheelsfor whl in wheelhouse/*.whl; do auditwheel repair “$whl” -w /io/wheelhouse/done将第一步获知的依赖项安装脚本填入此脚本,在使用 python 或 pip 时,注意使用 /opt/python/cp27-cp27m/bin 中的版本。最后,设置执行权限chmod a+x bin/build-wheel.sh4. 打包使用 Docker 下载所需的镜像(本步需要使用 Docker,请提前安装),此后在项目根目录下打包:docker pull quay.io/pypa/manylinux1_x86_64docker run –rm -v pwd:/io quay.io/pypa/manylinux1_x86_64 /io/bin/build-wheel.sh完成的 Wheel 包位于项目根目录下的 wheelhouse 目录下。本文作者:继盛阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 1, 2019 · 1 min · jiezi

深入解读MySQL8.0 新特性 :Crash Safe DDL

前言在MySQL8.0之前的版本中,由于架构的原因,mysql在server层使用统一的frm文件来存储表元数据信息,这个信息能够被不同的存储引擎识别。而实际上innodb本身也存储有元数据信息。这给ddl带来了一定的挑战,因为这种架构无法做到ddl的原子化,我们在线上经常能够看到数据目录下遗留的临时文件,或者类似server层和innodb层列个数不一致之类的错误。甚至某些ddl可能还遗留元数据在innodb内,而丢失了frm,导致无法重建表…..(我们为了解决这个问题,实现了一个叫drop table force的功能,去强制做清理….)(以下所有的讨论都假定使用InnoDB存储引擎)到了8.0版本,我们知道所有的元数据已经统一用InnoDB来进行管理,这就给实现原子ddl带来了可能,几乎所有的对innodb表,存储过程,触发器,视图或者UDF的操作,都能做到原子化:- 元数据修改,binlog以及innodb的操作都放在一个事务中- 增加了一个内部隐藏的系统表mysql.innodb_ddl_log,ddl操作被记录到这个表中,注意对该表的操作产生的redo会fsync到磁盘上,而不会考虑innodb_flush_log_at_trx_commit的配置。当崩溃重启时,会根据事务是否提交来决定通过这张表的记录去回滚或者执行ddl操作- 增加了一个post-ddl的阶段,这也是ddl的最后一个阶段,会去:1. 真正的物理删除或重命名文件; 2. 删除innodb_ddl_log中的记录项; 3.对于一些ddl操作还会去更新其动态元数据信息(存储在mysql.innodb_dynamic_metadata,例如corrupt flag, auto_inc值等)- 一个正常运行的ddl结束后,其ddl log也应该被清理,如果这中间崩溃了,重启时会去尝试重放:1.如果已经走到最后一个ddl阶段的(commit之后),就replay ddl log,把ddl完成掉;2. 如果处于某个中间态,则回滚ddl由于引入了atomic ddl, 有些ddl操作的行为也发生了变化:- DROP TABLE: 在之前的版本中,一个drop table语句中如果要删多个表,比如t1,t2, t2不存在时,t1会被删除。但在8.0中,t1和t2都不会被删除,而是抛出错误。因此要注意5.7->8.0的复制问题 (DROP VIEW, CREATE USER也有类似的问题)- DROP DATABASE: 修改元数据和ddl_log先提交事务,而真正的物理删除数据文件放在最后,因此如果在删除文件时崩溃,重启时会根据ddl_log继续执行drop database测试:MySQL很贴心的加了一个选项innodb_print_ddl_logs,打开后我们可以从错误日志看到对应的ddl log,下面我们通过这个来看下一些典型ddl的过程root@(none) 11:12:19>SET GLOBAL innodb_print_ddl_logs = 1; Query OK, 0 rows affected (0.00 sec)root@(none) 11:12:22>SET GLOBAL log_error_verbosity = 3; Query OK, 0 rows affected (0.00 sec)CREATE DATABASEmysql> CREATE DATABASE test;Query OK, 1 row affected (0.02 sec)创建数据库语句没有写log_ddl,可能觉得这不是高频操作,如果创建database的过程中失败了,重启后可能需要手动删除目录。CREATE TABLEmysql> USE test;Database changedmysql> CREATE TABLE t1 (a INT PRIMARY KEY, b INT);Query OK, 0 rows affected (0.06 sec)[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=428, thread_id=7, space_id=76, old_file_path=./test/t1.ibd][InnoDB] DDL log delete : by id 428[InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=429, thread_id=7, table_id=1102, new_file_path=test/t1][InnoDB] DDL log delete : by id 429[InnoDB] DDL log insert : [DDL record: FREE, id=430, thread_id=7, space_id=76, index_id=190, page_no=4][InnoDB] DDL log delete : by id 430[InnoDB] DDL log post ddl : begin for thread id : 7InnoDB] DDL log post ddl : end for thread id : 7从日志来看有三类操作,实际上描述了如果操作失败需要进行的三项逆向操作:删除数据文件,释放内存中的数据词典信息,删除索引btree。在创建表之前,这些数据被写入到ddl_log中,在创建完表并commit后,再从ddl log中删除这些记录。另外上述日志中还有DDL log delete日志,其实在每次写入ddl log时是单独事务提交的,但在提交之后,会使用当前事务执行一条delete操作,直到操作结束了才会提交。加列(instant)mysql> ALTER TABLE t1 ADD COLUMN c INT;Query OK, 0 rows affected (0.08 sec)Records: 0 Duplicates: 0 Warnings: 0[InnoDB] DDL log post ddl : begin for thread id : 7[InnoDB] DDL log post ddl : end for thread id : 7注意这里执行的是Instant ddl, 这是8.0.13新支持的特性,加列操作可以只修改元数据,因此从ddl log中无需记录数据删列mysql> ALTER TABLE t1 DROP COLUMN c;Query OK, 0 rows affected (2.77 sec)Records: 0 Duplicates: 0 Warnings: 0[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=487, thread_id=7, space_id=83, old_file_path=./test/#sql-ib1108-1917598001.ibd][InnoDB] DDL log delete : by id 487[InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=488, thread_id=7, table_id=1109, new_file_path=test/#sql-ib1108-1917598001][InnoDB] DDL log delete : by id 488[InnoDB] DDL log insert : [DDL record: FREE, id=489, thread_id=7, space_id=83, index_id=200, page_no=4][InnoDB] DDL log delete : by id 489[InnoDB] DDL log insert : [DDL record: DROP, id=490, thread_id=7, table_id=1108][InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=491, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd, new_file_path=./test/t1.ibd][InnoDB] DDL log delete : by id 491[InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=492, thread_id=7, table_id=1108, old_file_path=test/#sql-ib1109-1917598002, new_file_path=test/t1][InnoDB] DDL log delete : by id 492[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=493, thread_id=7, space_id=83, old_file_path=./test/t1.ibd, new_file_path=./test/#sql-ib1108-1917598001.ibd][InnoDB] DDL log delete : by id 493[InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=494, thread_id=7, table_id=1109, old_file_path=test/t1, new_file_path=test/#sql-ib1108-1917598001][InnoDB] DDL log delete : by id 494[InnoDB] DDL log insert : [DDL record: DROP, id=495, thread_id=7, table_id=1108][InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=496, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd][InnoDB] DDL log post ddl : begin for thread id : 7[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=496, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd][InnoDB] DDL log replay : [DDL record: DROP, id=495, thread_id=7, table_id=1108][InnoDB] DDL log replay : [DDL record: DROP, id=490, thread_id=7, table_id=1108][InnoDB] DDL log post ddl : end for thread id : 7这是个典型的三阶段ddl的过程:分为prepare, perform 以及commit三个阶段:Prepare: 这个阶段会修改元数据,创建临时ibd文件#sql-ib1108-1917598001.ibd, 如果发生异常崩溃,我们需要能把这个临时文件删除掉, 因此和create table类似,也为这个idb写了三条日志:delete space, remove cache,以及free btreePerform: 执行操作,将数据拷贝到上述ibd文件中,(同时处理online dmllog), 这部分不涉及log ddl操作Commit: 更新数据词典信息并提交事务, 这里会写几条日志:DROP : table_id=1108RENAME SPACE: #sql-ib1109-1917598002.ibd文件被rename成t1.ibdRENAME TABLE: #sql-ib1109-1917598002被rename成t1RENAME SPACE: t1.ibd 被rename成#sql-ib1108-1917598001.ibdRENAME TABLE: t1表被rename成#sql-ib1108-1917598001DROP TABLE: table_id=1108DELETE SPACE: 删除#sql-ib1109-1917598002.ibd实际上这一步写的ddl log描述了commit阶段操作的逆向过程:将t1.ibd rename成#sql-ib1109-1917598002, 并将sql-ib1108-1917598001 rename成t1表,最后删除旧表。其中删除旧表的操作这里不执行,而是到post-ddl阶段执行Post-ddl: 在事务提交后,执行最后的操作:replay ddl log, 删除旧文件,清理mysql.innodb_dynamic_metadata中相关信息DELETE SPACE: #sql-ib1109-1917598002.ibdDROP: table_id=1108DROP: table_id=1108加索引mysql> ALTER TABLE t1 ADD KEY(b);Query OK, 0 rows affected (0.14 sec)Records: 0 Duplicates: 0 Warnings: 0[InnoDB] DDL log insert : [DDL record: FREE, id=431, thread_id=7, space_id=76, index_id=191, page_no=5][InnoDB] DDL log delete : by id 431[InnoDB] DDL log post ddl : begin for thread id : 7[InnoDB] DDL log post ddl : end for thread id : 7创建索引采用inplace创建的方式,没有临时文件,但如果异常发生的话,依然需要在发生异常时清理临时索引, 因此增加了一条FREE log,用于异常发生时能够删除临时索引.TRUNCATE TABLEmysql> TRUNCATE TABLE t1;Query OK, 0 rows affected (0.13 sec)[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=439, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd, new_file_path=./test/t1.ibd][InnoDB] DDL log delete : by id 439[InnoDB] DDL log insert : [DDL record: DROP, id=440, thread_id=7, table_id=1103][InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=441, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd][InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=442, thread_id=7, space_id=78, old_file_path=./test/t1.ibd][InnoDB] DDL log delete : by id 442[InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=443, thread_id=7, table_id=1104, new_file_path=test/t1][InnoDB] DDL log delete : by id 443[InnoDB] DDL log insert : [DDL record: FREE, id=444, thread_id=7, space_id=78, index_id=194, page_no=4][InnoDB] DDL log delete : by id 444[InnoDB] DDL log insert : [DDL record: FREE, id=445, thread_id=7, space_id=78, index_id=195, page_no=5][InnoDB] DDL log delete : by id 445[InnoDB] DDL log post ddl : begin for thread id : 7[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=441, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd][InnoDB] DDL log replay : [DDL record: DROP, id=440, thread_id=7, table_id=1103][InnoDB] DDL log post ddl : end for thread id : 7Truncate table是个比较有意思的话题,在早期5.6及之前的版本中, 是通过删除旧表创建新表的方式来进行的,5.7之后为了保证原子性,改成了原地truncate文件,同时增加了一个truncate log文件,如果在truncate过程中崩溃,可以通过这个文件在崩溃恢复时重新truncate。到了8.0版本,又恢复成了删除旧表,创建新表的方式,与之前不同的是,8.0版本在崩溃时可以回滚到旧数据,而不是再次执行。以上述为例,主要包括几个步骤:将表t1.ibd rename成#sql-ib1103-1917597994.ibd创建新文件t1.ibdpost-ddl: 将老文件#sql-ib1103-1917597994.ibd删除RENAME TABLEmysql> RENAME TABLE t1 TO t2;Query OK, 0 rows affected (0.06 sec)DDL LOG:[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=450, thread_id=7, space_id=78, old_file_path=./test/t2.ibd, new_file_path=./test/t1.ibd][InnoDB] DDL log delete : by id 450[InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=451, thread_id=7, table_id=1104, old_file_path=test/t2, new_file_path=test/t1][InnoDB] DDL log delete : by id 451[InnoDB] DDL log post ddl : begin for thread id : 7[InnoDB] DDL log post ddl : end for thread id : 7这个就比较简单了,只需要记录rename space 和rename table的逆操作即可. post-ddl不需要做实际的操作DROP TABLEDROP TABLE t2[InnoDB] DDL log insert : [DDL record: DROP, id=595, thread_id=7, table_id=1119][InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=596, thread_id=7, space_id=93, old_file_path=./test/t2.ibd][InnoDB] DDL log post ddl : begin for thread id : 7[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=596, thread_id=7, space_id=93, old_file_path=./test/t2.ibd][InnoDB] DDL log replay : [DDL record: DROP, id=595, thread_id=7, table_id=1119][InnoDB] DDL log post ddl : end for thread id : 7先在ddl log中记录下需要删除的数据,再提交后,再最后post-ddl阶段执行真正的删除表对象和文件操作代码实现:主要实现代码集中在文件storage/innobase/log/log0ddl.cc中,包含了向log_ddl表中插入记录以及replay的逻辑。隐藏的innodb_log_ddl表结构如下 def->add_field(0, “id”, “id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT”); def->add_field(1, “thread_id”, “thread_id BIGINT UNSIGNED NOT NULL”); def->add_field(2, “type”, “type INT UNSIGNED NOT NULL”); def->add_field(3, “space_id”, “space_id INT UNSIGNED”); def->add_field(4, “page_no”, “page_no INT UNSIGNED”); def->add_field(5, “index_id”, “index_id BIGINT UNSIGNED”); def->add_field(6, “table_id”, “table_id BIGINT UNSIGNED”); def->add_field(7, “old_file_path”, “old_file_path VARCHAR(512) COLLATE UTF8_BIN”); def->add_field(8, “new_file_path”, “new_file_path VARCHAR(512) COLLATE UTF8_BIN”); def->add_index(0, “index_pk”, “PRIMARY KEY(id)”); def->add_index(1, “index_k_thread_id”, “KEY(thread_id)”);记录类型根据不同的操作类型,可以分为如下几类:FREE_TREE_LOG目的是释放索引btree,入口函数log_DDL::write_free_tree_log,在创建索引和删除表时会调用到对于drop table中涉及的删索引操作,log ddl的插入操作放到父事务中,一起要么提交要么回滚对于创建索引的case, log ddl就需要单独提交,父事务将记录标记删除,这样后面如果ddl回滚了,也能将残留的index删掉。DELETE_SPACE_LOG入口函数:Log_DDL::write_delete_space_log用于记录删除tablespace操作,同样分为两种情况:drop table/tablespace, 写入的记录随父事务一起提交,并在post-ddl阶段replay创建tablespace, 写入的记录单独提交,并被父事务标记删除,如果父事务回滚,就通过replay删除参与的tablespaceRENAME_SPACE_LOG入口函数:Log_DDL::write_rename_space_log用于记录rename操作,例如如果我们把表t1 rename成t2,在其中就记录了逆向操作t2 rename to t1.在函数Fil_shard::space_rename()中,总是先写ddl log, 再做真正的rename操作. 写日志的过程同样是独立事务提交,父事务做未提交的删除操作DROP_LOG入口函数: Log_DDL::write_drop_log用于记录删除表对象操作,这里不涉及文件层操作,写ddl log在父事务中执行RENAME_TABLE_LOG入口函数: Log_DDL::write_rename_table_log用于记录rename table对象的逆操作,和rename space类似,也是独立事务提交ddl log, 父事务标记删除REMOVE_CACHE_LOG入口函数: Log_DDL::write_remove_cache_log用于处理内存表对象的清理,独立事务提交,父事务标记删除ALTER_ENCRYPT_TABLESPACE_LOG入口函数: Log_DDL::write_alter_encrypt_space_log用于记录对tablespace加密属性的修改,独立事务提交. 在写完ddl log后修改tablespace page0 中的加密标记综上,在ddl的过程中可能会提交多次事务,大概分为三类:独立事务写ddl log并提交,父事务标记删除, 如果父事务提交了,ddl log也被顺便删除了,如果父事务回滚了,那就要根据ddl log做逆操作来回滚ddl独立事务写ddl log 并提交, (目前只有ALTER_ENCRYPT_TABLESPACE_LOG)使用父事务写ddl log,在ddl结束时提交。需要在post-ddl阶段处理post_ddl如上所述,有些ddl log是随着父事务一起提交的,有些则在post-ddl阶段再执行, post_ddl发生在父事提交或回滚之后: 若事务回滚,根据ddl log做逆操作,若事务提交,在post-ddl阶段做最后真正不可逆操作(例如删除文件)入口函数: Log_DDL::post_ddl –>Log_DDL::replay_by_thread_id根据执行ddl的线程thread id通过innodb_log_ddl表上的二级索引,找到log id,再到聚集索引上找到其对应的记录项,然后再replay这些操作,完成ddl后,清理对应记录崩溃恢复在崩溃恢复结束后,会调用ha_post_recover接口函数,进而调用innodb内的函数Log_DDL::recover(), 同样的replay其中的记录,并在结束后删除记录。但ALTER_ENCRYPT_TABLESPACE_LOG类型并不是在这一步删除,而是加入到一个数组ts_encrypt_ddl_records中,在之后调用resume_alter_encrypt_tablespace来恢复操作,参考文档1. 官方文档2. wl#9536: support crash safe ddl本文作者:zhaiwx_yinfeng阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 26, 2019 · 5 min · jiezi

基于泛型编程的序列化实现方法

写在前面序列化是一个转储-恢复的操作过程,即支持将一个对象转储到临时缓冲或者永久文件中和恢复临时缓冲或者永久文件中的内容到一个对象中等操作,其目的是可以在不同的应用程序之间共享和传输数据,以达到跨应用程序、跨语言和跨平台的解耦,以及当应用程序在客户现场发生异常或者崩溃时可以即时保存数据结构各内容的值到文件中,并在发回给开发者时再恢复数据结构各内容的值以协助分析和定位原因。泛型编程是一个对具有相同功能的不同类型的抽象实现过程,比如STL的源码实现,其支持在编译期由编译器自动推导具体类型并生成实现代码,同时依据具体类型的特定性质或者优化需要支持使用特化或者偏特化及模板元编程等特性进行具体实现。Hello World#include <iostream>int main(int argc, char* argv[]){ std::cout << “Hello World!” << std::endl; return 0;}泛型编程其实就在我们身边,我们经常使用的std和stl命名空间中的函数和类很多都是泛型编程实现的,如上述代码中的std::cout即是模板类std::basic_ostream的一种特化namespace std{ typedef basic_ostream<char> ostream;}从C++的标准输入输出开始除了上述提到的std::cout和std::basic_ostream外,C++还提供了各种形式的输入输出模板类,如std::basic_istream, std::basic_ifstream,std::basic_ofstream, std::basic_istringstream,std::basic_ostringstream等等,其主要实现了内建类型(built-in)的输入输出接口,比如对于Hello World可直接使用于字符串,然而对于自定义类型的输入输出,则需要重载实现操作符>>和<<,如对于下面的自定义类class MyClip{ bool mValid; int mIn; int mOut; std::string mFilePath;};如使用下面的方式则会出现一连串的编译错误MyClip clip;std::cout << clip;错误内容大致都是一些clip不支持<<操作符并在尝试将clip转为cout支持的一系列的内建类型如void*和int等等类型时转换操作不支持等信息。为了解决编译错误,我们则需要将类MyClip支持输入输出操作符>>和<<,类似实现代码如下inline std::istream& operator>>(std::istream& st, MyClip& clip){ st >> clip.mValid; st >> clip.mIn >> clip.mOut; st >> clip.mFilePath; return st;}inline std::ostream& operator<<(std::ostream& st, MyClip const& clip){ st << clip.mValid << ’ ‘; st << clip.mIn << ’ ’ << clip.mOut << ’ ‘; st << clip.mFilePath << ’ ‘; return st;}为了能正常访问类对象的私有成员变量,我们还需要在自定义类型里面增加序列化和反序列化的友元函数(回忆一下这里为何必须使用友元函数而不能直接重载操作符>>和<<?),如friend std::istream& operator>>(std::istream& st, MyClip& clip);friend std::ostream& operator<<(std::ostream& st, MyClip const& clip);这种序列化的实现方法是非常直观而且容易理解的,但缺陷是对于大型的项目开发中,由于自定义类型的数量较多,可能达到成千上万个甚至更多时,对于每个类型我们则需要实现2个函数,一个是序列化转储数据,另一个则是反序列化恢复数据,不仅仅增加了开发实现的代码数量,如果后期一旦对部分类的成员变量有所修改,则需要同时修改这2个函数。同时考虑到更复杂的自定义类型,比如含有继承关系和自定义类型的成员变量class MyVideo : public MyClip{ std::list<MyFilter> mFilters;};上述代码需要转储-恢复类MyVideo的对象内容时,事情会变得更复杂些,因为还需要转储-恢复基类,同时成员变量使用了STL模板容器list与自定义类’MyFilter`的结合,这种情况也需要自己去定义转储-恢复的实现方式。针对以上疑问,有没有一种方法能减少我们代码修改的工作量,同时又易于理解和维护呢?Boost序列化库对于使用C++标准输入输出的方法遇到的问题,好在Boost提供了一种良好的解决方式,则是将所有类型的转储-恢复操作抽象到一个函数中,易于理解,如对于上述类型,只需要将上述的2个友元函数替换为下面的一个友元函数template<typename Archive> friend void serialize(Archive&, MyClip&, unsigned int const);友元函数的实现类似下面的样子template<typename A>void serialize(A &ar, MyClip &clip, unsigned int const ver){ ar & BOOST_SERIALIZATION_NVP(clip.mValid); ar & BOOST_SERIALIZATION_NVP(clip.mIn); ar & BOOST_SERIALIZATION_NVP(clip.mOut); ar & BOOST_SERIALIZATION_NVP(clip.mFilePath);}其中BOOST_SERIALIZATION_NVP是Boost内部定义的一个宏,其主要作用是对各个变量进行打包。转储-恢复的使用则直接作用于操作符>>和<<,比如// storeMyClip clip;······std::ostringstream ostr;boost::archive::text_oarchive oa(ostr);oa << clip;// loadstd::istringstream istr(ostr.str());boost::archive::text_iarchive ia(istr);ia >> clip;这里使用的std::istringstream和std::ostringstream即是分别从字符串流中恢复数据以及将类对象的数据转储到字符串流中。对于类MyFilter和MyVideo则使用相同的方式,即分别增加一个友元模板函数serialize的实现即可,至于std::list模板类,boost已经帮我们实现了。这时我们发现,对于每一个定义的类,我们需要做的仅仅是在类内部声明一个友元模板函数,同时类外部实现这个模板函数即可,对于后期类的成员变量的修改,如增加、删除或者重命名成员变量,也仅仅是修改一个函数即可。Boost序列化库已经足够完美了,但故事并未结束!在用于端上开发时,我们发现引用Boost序列化库遇到了几个挑战端上的编译资料很少,官方对端上编译的资料基本没有,在切换不同的版本进行编译时经常会遇到各种奇怪的编译错误问题Boost在不同的C++开发标准之间兼容性不够好,尤其是使用libc++标准进行编译链接时遇到的问题较多Boost增加了端上发行包的体积Boost每次序列化都会增加序列化库及版本号等私有头信息,反序列化时再重新解析,降低了部分场景下的使用性能基于泛型编程的序列化实现方法为了解决使用Boost遇到的这些问题,我们觉得有必要重新实现序列化库,以剥离对Boost的依赖,同时能满足如下要求由于现有工程大量使用了Boost序列化库,因此兼容现有的代码以及开发者的习惯是首要目标尽量使得代码修改和重构的工作量最小兼容不同的C++开发标准提供比Boost序列化库更高的性能降低端上发行包的体积为了兼容现有使用Boost的代码以及保持当前开发者的习惯,同时使用代码修改的重构的工作量最小,我们应该保留模板函数serialize,同时对于模板函数内部的实现,为了提高效率也不需要对各成员变量重新打包,即直接使用如下定义#define BOOST_SERIALIZATION_NVP(value) value对于转储-恢复的接口调用,仍然延续目前的调用方式,只是将输入输出类修改为alivc::text_oarchive oa(ostr);alivc::text_iarchive ia(istr);好了,到此为止,序列化库对外的接口工作已经做好,剩下的就是内部的事情,应该如何重新设计和实现序列化库的内部框架才能满足要求呢?先来看一下当前的设计架构的处理流程图比如对于转储类text_oarchive,其支持的接口必须包括explicit text_oarchive(std::ostream& ost, unsigned int version = 0);template <typename T> text_oarchive& operator<<(T& v);template <typename T> text_oarchive& operator&(T& v);开发者调用操作符函数<<时,需要首先回调到相应类型的模板函数serialize中template <typename T>text_oarchive& operator<<(T& v){ serialize(*this, v, mversion); return *this;}当开始对具体类型的各个成员进行操作时,这时需要进行判断,如果此成员变量的类型已经是内建类型,则直接进行序列化,如果是自定义类型,则需要重新回调到对应类型的模板函数serialize中template <typename T>text_oarchive& operator&(T& v){ basic_save<T>::invoke(*this, v, mversion); return *this;}上述代码中的basic_save::invoke则会在编译期完成模板类型推导并选择直接对内建类型进行转储还是重新回调到成员变量对应类型的serialize函数继续重复上述过程。由于内建类型数量有限,因此这里我们选择使模板类basic_save的默认行为为回调到相应类型的serialize函数中template <typename T, bool E = false>struct basic_load_save{ template <typename A> static void invoke(A& ar, T& v, unsigned int version) { serialize(ar, v, version); }};template <typename T>struct basic_save : public basic_load_save<T, std::is_enum<T>::value>{};这时会发现上述代码的模板参数多了一个参数E,这里主要是需要对枚举类型进行特殊处理,使用偏特化的实现如下template <typename T>struct basic_load_save<T, true>{ template <typename A> static void invoke(A& ar, T& v, unsigned int version) { int tmp = v; ar & tmp; v = (T)tmp; }};到这里我们已经完成了重载操作符&的默认行为,即是不断进行回溯到相应的成员变量的类型中的模板函数serialize中,但对于碰到内建模型时,我们则需要让这个回溯过程停止,比如对于int类型template <typename T>struct basic_pod_save{ template <typename A> static void invoke(A& ar, T const& v, unsigned int) { ar.template save(v); }};template <>struct basic_save<int> : public basic_pod_save<int>{};这里对于int类型,则直接转储整数值到输出流中,此时text_oarchive则还需要增加一个终极转储函数template <typename T>void save(T const& v){ most << v << ’ ‘;}这里我们发现,在save成员函数中,我们已经将具体的成员变量的值输出到流中了。对于其它的内建类型,则使用相同的方式处理,要以参考C++ std::basic_ostream的源码实现。相应的,对于恢复操作的text_iarchive的操作流程如下图测试结果我们对使用Boost以及重新实现的序列化库进行了对比测试,其结果如下代码修改的重构的工作非常小,只需要删除Boost的相关头文件,以及将boost相关命名空间替换为alivc,BOOST_SERIALIZATION_FUNCTION以及BOOST_SERIALIZATION_NVP的宏替换Android端下的发行包体积大概减少了500KB目前的消息处理框架中,处理一次消息的平均时间由100us降低到了25us代码实现约300行,更轻量级未来还能做什么由于当前项目的原因,重新实现的序列化还没有支持转储-恢复指针所指向的内存数据,但当前的设计框架已经考虑了这种拓展性,未来会考虑支持。总结泛型编程能够大幅提高开发效率,尤其是在代码重用方面能发挥其优势,同时由于其类型推导及生成代码均在编译期完成,并不会降低性能序列化对于需要进行转储-恢复的解耦处理以及协助定位异常和崩溃的原因分析具有重要作用利用C++及模板自身的语言特性优势,结合合理的架构设计,即易于拓展又能尽量避免过度设计参考资料https://www.ibm.com/developerworks/cn/aix/library/au-boostserialization/本文作者:lifesider阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 25, 2019 · 2 min · jiezi

函数运行环境系统动态链接库版本太低?函数计算 fun 神助力分忧解难

背景最近在处理线上工单的时候,遇到一个用户使用 nodejs runtime 时因为函数计算运行环境的 gcc 版本过低导致无法运行的问题,觉得非常有意思,所以深入的帮用户寻找了解决方案。觉得这个场景应该具有一定的通用性,所以在这篇文章里面重点的介绍一下如何使用函数计算的周边工具 fun 解决因为 runtime 中系统版本导致的各种兼容性问题。场景介绍用户问题简要描述一下用户当时遇到的问题:用户使用函数计算的 nodejs8 runtime,在本地自己的开发环境使用 npm install couchbase 安装了 couchbase 这个第三方库。couchbase 封装了 C 库,依赖系统底层动态链接库 libstdc++.so.6。因为用户自己的开发环境的操作系统内核比较新,所以本地安装、编译和调试都比较顺利。所以,最后按照函数计算的打包方式成功创建了 Function,但是执行 InvokeFunction 时,遇到了这样的错误:“errorMessage”: “/usr/lib/x86_64-linux-gnu/libstdc++.so.6: version CXXABI_1.3.9' not found (required by /code/node_modules/couchbase/build/Release/couchbase_impl.node)", "errorType": "Error", "stackTrace": [ "Error: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version CXXABI_1.3.9’ not found (required by /code/node_modules/couchbase/build/Release/couchbase_impl.node)”,…错误发生的原因如堆栈描述,即没有 CXXABI_1.3.9 这个版本,可以看到函数计算 nodejs 环境中的支持情况:root@1fe79eb58dbd:/code# strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 |grep CXXABI_ CXXABI_1.3CXXABI_1.3.1CXXABI_1.3.2CXXABI_1.3.3CXXABI_1.3.4CXXABI_1.3.5CXXABI_1.3.6CXXABI_1.3.7CXXABI_1.3.8CXXABI_TM_1升级底层系统版本的代价比较大,需要长时间的稳定性、兼容性测试和观察,所以,为了支持这类使用场景,我们希望能够有比较简单的方式绕行。场景复现和问题解决前提:先按照 fun 的安装步骤安装 fun工具,并进行 fun config 配置。在本地很快搭建了一个项目目录:- test_code/ - index.js - template.yml其中 index.js 和 template.yml 的 内容分别为# index.jsconst couchbase = require(‘couchbase’).Mock;module.exports.handler = function(event, context, callback) { var cluster = new couchbase.Cluster(); var bucket = cluster.openBucket(); bucket.upsert(’testdoc’, {name:‘Frank’}, function(err, result) { if (err) throw err; bucket.get(’testdoc’, function(err, result) { if (err) throw err; console.log(result.value); // {name: Frank} }); }); callback(null, { hello: ‘world’ })}# template.yml ROSTemplateFormatVersion: ‘2015-09-01’Transform: ‘Aliyun::Serverless-2018-04-03’Resources: fc: # service name Type: ‘Aliyun::Serverless::Service’ Properties: Description: ‘fc test’ helloworld: # function name Type: ‘Aliyun::Serverless::Function’ Properties: Handler: index.handler Runtime: nodejs8 CodeUri: ‘./’ Timeout: 60为了能够在本地模拟函数计算的真实环境进行依赖包安装和调试,这里生成一个 fun.yml 文件用于 fun install 安装使用,内容如下:runtime: nodejs8tasks: - shell: |- if [ ! -f /code/.fun/root/usr/lib/x86_64-linux-gnu/libstdc++.so.6 ]; then mkdir -p /code/.fun/tmp/archives/ curl http://mirrors.ustc.edu.cn/debian/pool/main/g/gcc-6/libstdc++6_6.3.0-18+deb9u1_amd64.deb -o /code/.fun/tmp/archives/libstdc++6_6.3.0-18+deb9u1_amd64.deb bash -c ‘for f in $(ls /code/.fun/tmp/archives/*.deb); do dpkg -x $f /code/.fun/root; done;’ rm -rf /code/.fun/tmp/archives fi - name: install couchbase shell: npm install couchbasefun.yml中参数说明:前面的分析已经了解到函数计算 nodejs8 runtime 的 libstdc++.so.6 的版本偏低,所以,我们找到一个更新的版本来支持,见新版本的 libstdc++.so.6 的 CXXABI_ 参数:$strings .fun/root/usr/lib/x86_64-linux-gnu/libstdc++.so.6|grep CXXABI_CXXABI_1.3CXXABI_1.3.1CXXABI_1.3.2CXXABI_1.3.3CXXABI_1.3.4CXXABI_1.3.5CXXABI_1.3.6CXXABI_1.3.7CXXABI_1.3.8CXXABI_1.3.9CXXABI_1.3.10CXXABI_TM_1CXXABI_FLOAT128执行 fun install 命令安装各种第三方依赖,显示如下:本地执行情况执行 fun local invoke helloworld,可以看到执行成功的效果:$fun local invoke helloworld begin pullling image aliyunfc/runtime-nodejs8:1.4.0………………………………………………………pull image finishedpull image finishedFC Invoke Start RequestId: 78e20963-b314-4d69-843a-35a3f465796cload code for handler:index.handlerFC Invoke End RequestId: 78e20963-b314-4d69-843a-35a3f465796c{“hello”:“world”}2019-02-19T08:16:45.073Z 78e20963-b314-4d69-843a-35a3f465796c [verbose] { name: ‘Frank’ }发布上线使用 fun deploy 发布上线,然后到控制台执行一下线上实际的运行效果:总结fun install 功能能够将代码和依赖文件分离开,独立安装系统依赖文件,而且 fun local 和 fun deply 都能够自动帮你设置第三方库的依赖引用路径,让您无需关心环境变量问题。本文的解法只是提供了一个对于系统版本偏低无法满足用户一些高级库使用需求时的简单绕行方案,仅供参考,对于一些复杂的环境依赖问题,可能还需要具体情况具体分析。更多参考:函数计算 nodejs runtimefun local99dXnVk4I)fun install本文作者:清宵阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 21, 2019 · 2 min · jiezi

基于快速GeoHash,如何实现海量商品与商圈的高效匹配?

小叽导读:闲鱼是一款闲置物品的交易平台APP。通过这个平台,全国各地“无处安放”的物品能够轻松实现流动。这种分享经济业务形态被越来越多的人所接受,也进一步实现了低碳生活的目标。今天,闲鱼团队就商品与商圈的匹配算法为我们展开详细解读。摘要闲鱼app根据交通条件、商场分布情况、住宅区分布情况综合考虑,将城市划分为一个个商圈。杭州部分区域商圈划分如下图所示。 闲鱼的商品是由用户发布的GPS随机分布在地图上的点数据。当用户处于某个商圈范围内时,app会向用户推荐GPS位于此商圈中的商品。要实现精准推荐服务,就需要计算出哪些商品是归属于你所处的商圈。在数据库中,商圈是由多个点围成的面数据,这些面数据形状、大小各异,且互不重叠。商品是以GPS标记的点数据,如何能够快速高效地确定海量商品与商圈的归属关系呢?传统而直接的方法是,利用几何学的空间关系计算公式对海量数据实施直接的“点—面”关系计算,来确定每一个商品是否位于每一个商圈内部。闲鱼目前有10亿商品数据,且每天还在快速增加。全国所有城市的商圈数量总和大约为1万,每个商圈的大小不一,边数从10到80不等。如果直接使用几何学点面关系运算,需要的计算量级约为2亿亿次基本运算。按照这个思路,我们尝试过使用阿里巴巴集团内部的离线计算集群来执行计算,结果集群在运行了超过2天之后也未能给出结果。经过算法改进,我们采用了一种基于GeoHash精确匹配,结合GeoHash非精确匹配并配合小范围几何学关系运算精匹配的算法,大大降低了计算量,高效地实现了离线环境下海量点-面数据的包含关系计算。同样是对10亿条商品和1万条商圈数据做匹配,可以在1天内得到结果。▌点数据GeoHash原理与算法GeoHash是一种对地理坐标进行编码的方法,它将二维坐标映射为一个字符串。每个字符串代表一个特定的矩形,在该矩形范围内的所有坐标都共用这个字符串。字符串越长精度越高,对应的矩形范围越小。对一个地理坐标编码时,按照初始区间范围纬度[-90,90]和经度[-180,180],计算目标经度和纬度分别落在左区间还是右区间。落在左区间则取0,右区间则取1。然后,对上一步得到的区间继续按照此方法对半查找,得到下一位二进制编码。当编码长度达到业务的进度需求后,根据“偶数位放经度,奇数位放纬度”的规则,将得到的二进制编码穿插组合,得到一个新的二进制串。最后,根据base32的对照表,将二进制串翻译成字符串,即得到地理坐标对应的目标GeoHash字符串。以坐标“30.280245, 120.027162”为例,计算其GeoHash字符串。首先对纬度做二进制编码:将[-90,90]平分为2部分,“30.280245”落在右区间(0,90],则第一位取1。将(0,90]平分为2分,“30.280245”落在左区间(0,45],则第二位取0。不断重复以上步骤,得到的目标区间会越来越小,区间的两个端点也越来越逼近“30.280245”。下图的流程详细地描述了前几次迭代的过程: 按照上面的流程,继续往下迭代,直到编码位数达到我们业务对精度的需求为止。完整的15位二进制编码迭代表格如下: 得到的纬度二进制编码为10101 01100 01000。按照同样的流程,对经度做二进制编码,具体迭代详情如下:得到的经度二进制编码为11010 10101 01101。按照“偶数位放经度,奇数位放纬度”的规则,将经纬度的二进制编码穿插,得到完成的二进制编码为:11100 11001 10011 10010 00111 00010。由于后续要使用的是base32编码,每5个二进制数对应一个32进制数,所以这里将每5个二进制位转换成十进制位,得到28,25,19,18,7,2。 对照base32编码表,得到对应的编码为:wtmk72。 可以在geohash.org/网站对上述结果进行验证,验证结果如下:验证结果的前几位与我们的计算结果一致。如果我们利用二分法获取二进制编码时迭代更多次,就会得到验证网站中这样的位数更多的更精确结果。GeoHash字符串的长度与精度的对应关系如下: ▌面数据GeoHash编码实现上一节介绍的标准GeoHash算法只能用来计算二维点坐标对应的GeoHash编码,我们的场景中还需要计算面数据(即GIS中的POLYGON多边形对象)对应的GeoHash编码,需要扩展算法来实现。算法思路是,先找到目标Polygon的最小外接矩形MBR,计算此MBR西南角坐标对应的GeoHash编码。然后用GeoHash编码的逆算法,反解出此编码对应的矩形GeoHash块。以此GeoHash块为起点,循环往东、往北找相邻的同等大小的GeoHash块,直到找到的GeoHash块完全超出MBR的范围才停止。如此找到的多个GeoHash块,边缘上的部分可能与目标Polygon完全不相交,这部分块需要通过计算剔除掉,如此一来可以减少后续不必要的计算量。 上面的例子中最终得到的结果高清大图如下,其中蓝色的GeoHash块是与原始Polygon部分相交的,橘黄色的GeoHash块是完全被包含在原始Polygon内部的。上述算法总结成流程图如下: ▌求临近GeoHash块的快速算法上一节对面数据进行GeoHash编码的流程图中标记为绿色和橘黄色的两步,分别是要寻找相邻的东边或北边的GeoHash字符串。传统的做法是,根据当前GeoHash块的反解信息,求出相邻块内部的一点,在对这个点做GeoHash编码,即为相邻块的GeoHash编码。如下图,我们要计算"wtmk72"周围的8个相邻块的编码,就要先利用GeoHash逆算法将"wtmk72"反解出4个顶点的坐标N1、N2、N3、N4,然后由这4个坐标计算出右侧邻接块内部的任意一点坐标N5,再对N5做GeoHash编码,得到的“wtmk78”就是我们要求的右边邻接块的编码。按照同样的方法,求可以求出"wtmk72"周围总共8个邻接块的编码。这种方法需要先解码一次再编码一次,比较耗时,尤其是在指定的GeoHash字符串长度较长需要循环较多次的情况下。通过观察GeoHash编码表的规律,结合GeoHash编码使用的Z阶曲线的特性,验证了一种通过查表来快速求相邻GeoHash字符串的方法。还是以“wtmk72”这个GeoHash字符串为例,对应的10进制数是“28,25,19,18,7,2”,转换成二进制就是11100 11001 10011 10010 00111 00010。其中,w对应11100,这5个二进制位分别代表“经 纬 经 纬 经”;t对应11001,这5个二进制位分别代表“纬 经 纬 经 纬”。由此推广开来可知,GeoHash中的奇数位字符(本例中的’w’、’m’、‘7’)代表的二进制位分别对应“经 纬 经 纬 经”,偶数位字符(本例中的’t’、‘k’、‘2’)代表的二进制位分别对应“纬 经 纬 经 纬”。‘w’的二进制11100,转换成方位含义就是“右 上 右 下 左”。’t’的二进制11001,转换成方位含义就是“上 右 下 左 上”。根据这个字符与方位的转换关系,我们可以知道,奇数位上的字符与位置对照表如下: 偶数位上的字符与位置对照表如下:这里可以看到一个很有意思的现象,奇数位的对照表和偶数位对照表存在一种转置和翻转的关系。有了以上两份字符与位置对照表,就可以快速得出每个字符周围的8个字符分别是什么。而要计算一个给定GeoHash字符串周围8个GeoHash值,如果字符串最后一位字符在该方向上未超出边界,则前面几位保持不变,最后一位取此方向上的相邻字符即可;如果最后一位在此方向上超出了对照表边界,则先求倒数第二个字符在此方向上的相邻字符,再求最后一个字符在此方向上相邻字符(对照表环状相邻字符);如果倒数第二位在此方向上的相邻字符也超出了对照表边界,则先求倒数第三位在此方向上的相邻字符。以此类推。以上面的“wtmk72”举例,要求这个GeoHash字符串的8个相邻字符串,实际就是求尾部字符‘2’的相邻字符。‘2’适用偶数对照表,它的8个相邻字符分别是‘1’、‘3’、‘9’、‘0’、‘8’、‘p’、‘r’、‘x’,其中‘p’、‘r’、‘x’已经超出了对照表的下边界,是将偶数位对照表上下相接组成环状得到的相邻关系。所以,对于这3个超出边界的“下方”相邻字符,需要求倒数第二位的下方相邻字符,即‘7’的下方相邻字符。‘7’是奇数位,适用奇数位对照表,‘7’在对照表中的“下方”相邻字符是‘5’,所以“wtmk72”的8个相邻GeoHash字符串分别是“wtmk71”、“wtmk73”、“wtmk79”、“wtmk70”、“wtmk78”、“wtmk5p”、“wtmk5r”、“wtmk5x”。利用此相邻字符串快速算法,可以大大提高上一节流程图中面数据GeoHash编码算法的效率。▌高效建立海量点数据与面数据的关系建立海量点数据与面数据的关系的思路是,先将需要匹配的商品GPS数据(点数据)、商圈AOI数据(面数据)按照前面所述的算法,分别计算同等长度的GeoHash编码。每个点数据都对应唯一一个GeoHash字符串;每个面数据都对应一个或多个GeoHash编码,这些编码要么是“完全包含字符串”,要么是“部分包含字符串”。a)将每个商品的GeoHash字符串与商圈的“完全包含字符串”进行join操作。join得到的结果中出现的<商品,商圈>数据就是能够确定的“某个商品属于某个商圈”的关系。b)对于剩下的还未被确定关系的商品,将这些商品的GeoHash字符串与商圈的“部分包含字符串”进行join操作。join得到的结果中出现的<商品,商圈>数据是有可能存在的“商品属于某个商圈”的关系,接下来对这批数据中的商品gps和商圈AOI数据进行几何学关系运算,进而从中筛选出确定的“商品属于某个商圈”的关系。如图,商品1的点数据GeoHash编码为"wtmk70j",与面数据的“完全包含字符串wtmk70j”join成功,所以可以直接确定商品1属于此面数据。商品2的点数据GeoHash编码为“wtmk70r”,与面数据的“部分包含字符串wtmk70r”join成功,所以商品2疑似属于面数据,具体是否存在包含关系,还需要后续的点面几何学计算来确定。 商品3的点数据GeoHash编码与面数据的任何GeoHash块编码都匹配不上,所以可以快速确定商品3不属于此面数据。 实际应用中,原始的海量商品GPS范围散布在全国各地,海量商圈数据也散布在全国各个不同的城市。经过a)步骤的操作后,大部分的商品数据已经确定了与商圈的从属关系。剩下的未能匹配上的商品数据,经过b)步骤的GeoHash匹配后,可以将后续“商品-商圈几何学计算”的运算量从“1个商品 x 全国所有商圈”笛卡尔积的量级,降低为“1个商品 x 1个(或几个)商圈”笛卡尔积的量级,减少了绝大部分不必要的几何学运算,而这部分运算是非常耗时的。在闲鱼的实际应用中,10亿商品和1万商圈数据,使用本文的快速算法,只需要 10亿次GeoHash点编码 + 1万次GeoHash面编码 + 500万次“点是否在面内部”几何学运算,粗略换算为基本运算需要的次数约为1800亿次,运算量远小于传统方法的2亿亿次基本运算。使用阿里巴巴的离线计算平台,本文的算法在不到一天的时间内就完成了全部计算工作。另外,对于给定的点和多边形,通过几何学计算包含关系的算法不止一种,最常用的算法是射线法。简单来说,就是从这个点出发做一条射线,判断该射线与多边形的交点个数是奇数还是偶数。如果是奇数,说明点在多边形内;否则,点在多边形外。▌延伸面对海量点面数据的空间关系划分,本文采用是的通过GeoHash来降低计算量的思路,本质上来说是利用了空间索引的思想。事实上,在GIS领域有多种实用的空间索引,常见的如R树系列(R树、R+树、R*树)、四叉树、K-D树、网格索引等,这些索引算法各有特点。本文的思路不仅能用来处理点—面关系的相关问题,还可以用来快速处理点—点关系、面—面关系、点—线关系、线—线关系等问题,比如快速确定大范围类的海量公交站台与道路的从属关系、多条道路或铁路是否存在交点等问题。本文作者:峰明阅读原文本文来自云栖社区合作伙伴“ 阿里技术”,如需转载请联系原作者。

February 21, 2019 · 1 min · jiezi

Tensorflow源码解析2 -- 前后端连接的桥梁 - Session

1 Session概述Session是TensorFlow前后端连接的桥梁。用户利用session使得client能够与master的执行引擎建立连接,并通过session.run()来触发一次计算。它建立了一套上下文环境,封装了operation计算以及tensor求值的环境。session创建时,系统会分配一些资源,比如graph引用、要连接的计算引擎的名称等。故计算完毕后,需要使用session.close()关闭session,避免引起内存泄漏,特别是graph无法释放的问题。可以显式调用session.close(),或利用with上下文管理器,或者直接使用InteractiveSession。session之间采用共享graph的方式来提高运行效率。一个session只能运行一个graph实例,但一个graph可以运行在多个session中。一般情况下,创建session时如果不指定Graph实例,则会使用系统默认Graph。常见情况下,我们都是使用一个graph,即默认graph。当session创建时,不会重新创建graph实例,而是默认graph引用计数加1。当session close时,引用计数减1。只有引用计数为0时,graph才会被回收。这种graph共享的方式,大大减少了graph创建和回收的资源消耗,优化了TensorFlow运行效率。2 默认sessionop运算和tensor求值时,如果没有指定运行在哪个session中,则会运行在默认session中。通过session.as_default()可以将自己设置为默认session。但个人建议最好还是通过session.run(operator)和session.run(tensor)来进行op运算和tensor求值。operation.run()operation.run()等价于tf.get_default_session().run(operation)@tf_export(“Operation”)class Operation(object): # 通过operation.run()调用,进行operation计算 def run(self, feed_dict=None, session=None): _run_using_default_session(self, feed_dict, self.graph, session) def _run_using_default_session(operation, feed_dict, graph, session=None): # 没有指定session,则获取默认session if session is None: session = get_default_session() # 最终还是通过session.run()进行运行的。tf中任何运算,都是通过session来run的。 # 通过session来建立client和master的连接,并将graph发送给master,master再进行执行 session.run(operation, feed_dict)tensor.eval()tensor.eval()等价于tf.get_default_session().run(tensor), 如下@tf_export(“Tensor”)class Tensor(_TensorLike): # 通过tensor.eval()调用,进行tensor运算 def eval(self, feed_dict=None, session=None): return _eval_using_default_session(self, feed_dict, self.graph, session) def _eval_using_default_session(tensors, feed_dict, graph, session=None): # 如果没有指定session,则获取默认session if session is None: session = get_default_session() return session.run(tensors, feed_dict)默认session的管理tf通过运行时维护的session本地线程栈,来管理默认session。故不同的线程会有不同的默认session,默认session是线程作用域的。# session栈_default_session_stack = _DefaultStack()# 获取默认session的接口@tf_export(“get_default_session”)def get_default_session(): return _default_session_stack.get_default()# _DefaultStack默认session栈是线程相关的class _DefaultStack(threading.local): # 默认session栈的创建,其实就是一个list def init(self): super(_DefaultStack, self).init() self._enforce_nesting = True self.stack = [] # 获取默认session def get_default(self): return self.stack[-1] if len(self.stack) >= 1 else None3 前端Session类型session类图会话Session的UML类图如下分为两种类型,普通Session和交互式InteractiveSession。InteractiveSession和Session基本相同,区别在于InteractiveSession创建后,会将自己替换为默认session。使得之后operation.run()和tensor.eval()的执行通过这个默认session来进行。特别适合Python交互式环境。InteractiveSession自带with上下文管理器。它在创建时和关闭时会调用上下文管理器的enter和exit方法,从而进行资源的申请和释放,避免内存泄漏问题。这同样很适合Python交互式环境。Session和InteractiveSession的代码逻辑不多,主要逻辑均在其父类BaseSession中。主要代码如下@tf_export(‘Session’)class Session(BaseSession): def init(self, target=’’, graph=None, config=None): # session创建的主要逻辑都在其父类BaseSession中 super(Session, self).init(target, graph, config=config) self._default_graph_context_manager = None self._default_session_context_manager = None@tf_export(‘InteractiveSession’)class InteractiveSession(BaseSession): def init(self, target=’’, graph=None, config=None): self._explicitly_closed = False # 将自己设置为default session self.default_session = self.as_default() self.default_session.enforce_nesting = False # 自动调用上下文管理器的__enter()方法 self.default_session.enter() self.explicit_graph = graph def close(self): super(InteractiveSession, self).close() ## 省略无关代码 ## 自动调用上下文管理器的__exit()方法,避免内存泄漏 self._default_session.exit(None, None, None) self._default_session = NoneBaseSessionBaseSession基本包含了所有的会话实现逻辑。包括会话的整个生命周期,也就是创建 执行 关闭和销毁四个阶段。生命周期后面详细分析。BaseSession包含的主要成员变量有graph引用,序列化的graph_def, 要连接的tf引擎target,session配置信息config等。4 后端Session类型在后端master中,根据前端client调用tf.Session(target=’’, graph=None, config=None)时指定的target,来创建不同的Session。target为要连接的tf后端执行引擎,默认为空字符串。Session创建采用了抽象工厂模式,如果为空字符串,则创建本地DirectSession,如果以grpc://开头,则创建分布式GrpcSession。类图如下DirectSession只能利用本地设备,将任务创建到本地的CPU GPU上。而GrpcSession则可以利用远端分布式设备,将任务创建到其他机器的CPU GPU上,然后通过grpc协议进行通信。grpc协议是谷歌发明并开源的远程通信协议。5 Session生命周期Session作为前后端连接的桥梁,以及上下文运行环境,其生命周期尤其关键。大致分为4个阶段创建:通过tf.Session()创建session实例,进行系统资源分配,特别是graph引用计数加1运行:通过session.run()触发计算的执行,client会将整图graph传递给master,由master进行执行关闭:通过session.close()来关闭,会进行系统资源的回收,特别是graph引用计数减1.销毁:Python垃圾回收器进行GC时,调用session.del()进行回收。生命周期方法入口基本都在前端Python的BaseSession中,它会通过swig自动生成的函数符号映射关系,调用C层的实现。5.1 创建先从BaseSession类的init方法看起,只保留了主要代码。def init(self, target=’’, graph=None, config=None): # graph表示构建的图。TensorFlow的一个session会对应一个图。这个图包含了所有涉及到的算子 # graph如果没有设置(通常都不会设置),则使用默认graph if graph is None: self._graph = ops.get_default_graph() else: self._graph = graph self._opened = False self._closed = False self._current_version = 0 self._extend_lock = threading.Lock() # target为要连接的tf执行引擎 if target is not None: self._target = compat.as_bytes(target) else: self._target = None self._delete_lock = threading.Lock() self._dead_handles = [] # config为session的配置信息 if config is not None: self._config = config self._add_shapes = config.graph_options.infer_shapes else: self._config = None self.add_shapes = False self.created_with_new_api = ops.USE_C_API # 调用C层来创建session self.session = None opts = tf_session.TF_NewSessionOptions(target=self.target, config=config) self.session = tf_session.TF_NewSession(self.graph.c_graph, opts, status)BaseSession先进行成员变量的赋值,然后调用TF_NewSession来创建session。TF_NewSession()方法由swig自动生成,在bazel-bin/tensorflow/python/pywrap_tensorflow_internal.py中def TF_NewSession(graph, opts, status): return pywrap_tensorflow_internal.TF_NewSession(graph, opts, status)pywrap_tensorflow_internal包含了C层函数的符号表。在swig模块import时,会加载pywrap_tensorflow_internal.so动态链接库,从而得到符号表。在pywrap_tensorflow_internal.cc中,注册了供Python调用的函数的符号表,从而实现Python到C的函数映射和调用。// c++函数调用的符号表,Python通过它可以调用到C层代码。符号表和动态链接库由swig自动生成static PyMethodDef SwigMethods[] = { // .. 省略其他函数定义 // TF_NewSession的符号表,通过这个映射,Python中就可以调用到C层代码了。 { (char )“TF_NewSession”, _wrap_TF_NewSession, METH_VARARGS, NULL}, // … 省略其他函数定义}最终调用到c_api.c中的TF_NewSession()// TF_NewSession创建session的新实现,在C层后端代码中TF_Session TF_NewSession(TF_Graph* graph, const TF_SessionOptions* opt, TF_Status* status) { Session* session; // 创建session status->status = NewSession(opt->options, &session); if (status->status.ok()) { TF_Session* new_session = new TF_Session(session, graph); if (graph != nullptr) { // 采用了引用计数方式,多个session共享一个图实例,效率更高。 // session创建时,引用计数加1。session close时引用计数减1。引用计数为0时,graph才会被回收。 mutex_lock l(graph->mu); graph->sessions[new_session] = Status::OK(); } return new_session; } else { DCHECK_EQ(nullptr, session); return nullptr; }}session创建时,并创建graph,而是采用共享方式,只是引用计数加1了。这种方式减少了session创建和关闭时的资源消耗,提高了运行效率。NewSession()根据前端传递的target,使用sessionFactory创建对应的TensorFlow::Session实例。Status NewSession(const SessionOptions& options, Session** out_session) { SessionFactory* factory; const Status s = SessionFactory::GetFactory(options, &factory); // 通过sessionFactory创建多态的Session。本地session为DirectSession,分布式为GRPCSession out_session = factory->NewSession(options); if (!out_session) { return errors::Internal(“Failed to create session.”); } return Status::OK();}创建session采用了抽象工厂模式。根据client传递的target,来创建不同的session。如果target为空字符串,则创建本地DirectSession。如果以grpc://开头,则创建分布式GrpcSession。TensorFlow包含本地运行时和分布式运行时两种运行模式。下面来看DirectSessionFactory的NewSession()方法class DirectSessionFactory : public SessionFactory { public: Session NewSession(const SessionOptions& options) override { std::vector<Device> devices; // job在本地执行 const Status s = DeviceFactory::AddDevices( options, “/job:localhost/replica:0/task:0”, &devices); if (!s.ok()) { LOG(ERROR) << s; return nullptr; } DirectSession* session = new DirectSession(options, new DeviceMgr(devices), this); { mutex_lock l(sessions_lock); sessions.push_back(session); } return session; }GrpcSessionFactory的NewSession()方法就不详细分析了,它会将job任务创建在分布式设备上,各job通过grpc协议通信。5.2 运行通过session.run()可以启动graph的执行。入口在BaseSession的run()方法中, 同样只列出关键代码class BaseSession(SessionInterface): def run(self, fetches, feed_dict=None, options=None, run_metadata=None): # fetches可以为单个变量,或者数组,或者元组。它是图的一部分,可以是操作operation,也可以是数据tensor,或者他们的名字String # feed_dict为对应placeholder的实际训练数据,它的类型为字典 result = self.run(None, fetches, feed_dict, options_ptr,run_metadata_ptr) return result def run(self, handle, fetches, feed_dict, options, run_metadata): # 创建fetch处理器fetch_handler fetch_handler = FetchHandler( self.graph, fetches, feed_dict_tensor, feed_handles=feed_handles) # 经过不同类型的fetch_handler处理,得到最终的fetches和targets # targets为要执行的operation,fetches为要执行的tensor _ = self.update_with_movers(feed_dict_tensor, feed_map) final_fetches = fetch_handler.fetches() final_targets = fetch_handler.targets() # 开始运行 if final_fetches or final_targets or (handle and feed_dict_tensor): results = self.do_run(handle, final_targets, final_fetches, feed_dict_tensor, options, run_metadata) else: results = [] # 输出结果到results中 return fetch_handler.build_results(self, results) def do_run(self, handle, target_list, fetch_list, feed_dict, options, run_metadata): # 将要运行的operation添加到graph中 self.extend_graph() # 执行一次运行run,会调用底层C来实现 return tf_session.TF_SessionPRunSetup_wrapper( session, feed_list, fetch_list, target_list, status) # 将要运行的operation添加到graph中 def extend_graph(self): with self.extend_lock: if self.graph.version > self.current_version: # 生成graph_def对象,它是graph的序列化表示 graph_def, self.current_version = self.graph.as_graph_def( from_version=self.current_version, add_shapes=self.add_shapes) # 通过TF_ExtendGraph将序列化后的graph,也就是graph_def传递给后端 with errors.raise_exception_on_not_ok_status() as status: tf_session.TF_ExtendGraph(self.session, graph_def.SerializeToString(), status) self.opened = True逻辑还是十分复杂的,主要有一下几步入参处理,创建fetch处理器fetch_handler,得到最终要执行的operation和tensor对graph进行序列化,生成graph_def对象将序列化后的grap_def对象传递给后端master。通过后端master来run。我们分别来看extend和run。5.2.1 extend添加节点到graph中TF_ExtendGraph()会调用到c_api中,这个逻辑同样通过swig工具自动生成。下面看c_api.cc中的TF_ExtendGraph()方法// 增加节点到graph中,proto为序列化后的graphvoid TF_ExtendGraph(TF_DeprecatedSession* s, const void* proto, size_t proto_len, TF_Status* status) { GraphDef g; // 先将proto反序列化,得到client传递的graph,放入g中 if (!tensorflow::ParseProtoUnlimited(&g, proto, proto_len)) { status->status = InvalidArgument(“Invalid GraphDef”); return; } // 再调用session的extend方法。根据创建的不同session类型,多态调用不同方法。 status->status = s->session->Extend(g);}后端系统根据生成的Session类型,多态的调用Extend方法。如果是本地session,则调用DirectSession的Extend()方法。如果是分布式session,则调用GrpcSession的相关方法。下面来看GrpcSession的Extend方法。Status GrpcSession::Extend(const GraphDef& graph) { CallOptions call_options; call_options.SetTimeout(options.config.operation_timeout_in_ms()); return ExtendImpl(&call_options, graph);}Status GrpcSession::ExtendImpl(CallOptions* call_options, const GraphDef& graph) { bool handle_is_empty; { mutex_lock l(mu); handle_is_empty = handle.empty(); } if (handle_is_empty) { // 如果graph句柄为空,则表明graph还没有创建好,此时extend就等同于create return Create(graph); } mutex_lock l(mu); ExtendSessionRequest req; req.set_session_handle(handle); *req.mutable_graph_def() = graph; req.set_current_graph_version(current_graph_version); ExtendSessionResponse resp; // 调用底层实现,来添加节点到graph中 Status s = master->ExtendSession(call_options, &req, &resp); if (s.ok()) { current_graph_version = resp.new_graph_version(); } return s;}Extend()方法中要注意的一点是,如果是首次执行Extend(), 则要先调用Create()方法进行graph的注册。否则才是执行添加节点到graph中。5.2.2 run执行图的计算同样,Python通过swig自动生成的代码,来实现对C API的调用。C层实现在c_api.cc的TF_Run()中。// session.run()的C层实现void TF_Run(TF_DeprecatedSession* s, const TF_Buffer* run_options, // Input tensors,输入的数据tensor const char** c_input_names, TF_Tensor** c_inputs, int ninputs, // Output tensors,运行计算后输出的数据tensor const char** c_output_names, TF_Tensor** c_outputs, int noutputs, // Target nodes,要运行的节点 const char** c_target_oper_names, int ntargets, TF_Buffer* run_metadata, TF_Status* status) { // 省略一段代码 TF_Run_Helper(s->session, nullptr, run_options, input_pairs, output_names, c_outputs, target_oper_names, run_metadata, status);}// 真正的实现了session.run()static void TF_Run_Helper() { RunMetadata run_metadata_proto; // 调用不同的session实现类的run方法,来执行 result = session->Run(run_options_proto, input_pairs, output_tensor_names, target_oper_names, &outputs, &run_metadata_proto); // 省略代码}最终会调用创建的session来执行run方法。DirectSession和GrpcSession的Run()方法会有所不同。后面很复杂,就不接着分析了。5.3 关闭session通过session.close()来关闭session,释放相关资源,防止内存泄漏。class BaseSession(SessionInterface): def close(self): tf_session.TF_CloseSession(self.session, status)会调用到C API的TF_CloseSession()方法。void TF_CloseSession(TF_Session* s, TF_Status* status) { status->status = s->session->Close();}最终根据创建的session,多态的调用其Close()方法。同样分为DirectSession和GrpcSession两种。::tensorflow::Status DirectSession::Close() { cancellation_manager->StartCancel(); { mutex_lock l(closed_lock); if (closed) return ::tensorflow::Status::OK(); closed = true; } // 注销session if (factory != nullptr) factory->Deregister(this); return ::tensorflow::Status::OK();}DirectSessionFactory中的Deregister()方法如下void Deregister(const DirectSession* session) { mutex_lock l(sessions_lock); // 释放相关资源 sessions.erase(std::remove(sessions.begin(), sessions.end(), session), sessions.end()); }5.4 销毁sessionsession的销毁是由Python的GC自动执行的。python通过引用计数方法来判断是否回收对象。当对象的引用计数为0,且虚拟机触发了GC时,会调用对象的__del()方法来销毁对象。引用计数法有个很致命的问题,就是无法解决循环引用问题,故会存在内存泄漏。Java虚拟机采用了调用链分析的方式来决定哪些对象会被回收。class BaseSession(SessionInterface): def del(self): # 先close,防止用户没有调用close() try: self.close() # 再调用c api的TF_DeleteSession来销毁session if self.session is not None: try: status = c_api_util.ScopedTFStatus() if self.created_with_new_api: tf_session.TF_DeleteSession(self.session, status)c_api.cc中的相关逻辑如下void TF_DeleteSession(TF_Session* s, TF_Status* status) { status->status = Status::OK(); TF_Graph* const graph = s->graph; if (graph != nullptr) { graph->mu.lock(); graph->sessions.erase(s); // 如果graph的引用计数为0,也就是graph没有被任何session持有,则考虑销毁graph对象 const bool del = graph->delete_requested && graph->sessions.empty(); graph->mu.unlock(); // 销毁graph对象 if (del) delete graph; } // 销毁session和TF_Session delete s->session; delete s;}TF_DeleteSession()会判断graph的引用计数是否为0,如果为0,则会销毁graph。然后销毁session和TF_Session对象。通过Session实现类的析构函数,来销毁session,释放线程池Executor,资源管理器ResourceManager等资源。DirectSession::~DirectSession() { for (auto& it : partial_runs) { it.second.reset(nullptr); } // 释放线程池Executor for (auto& it : executors) { it.second.reset(); } for (auto d : device_mgr->ListDevices()) { d->op_segment()->RemoveHold(session_handle); } // 释放ResourceManager for (auto d : device_mgr->ListDevices()) { d->ClearResourceMgr(); } // 释放CancellationManager实例 functions.clear(); delete cancellation_manager; // 释放ThreadPool for (const auto& p_and_owned : thread_pools) { if (p_and_owned.second) delete p_and_owned.first; } execution_state.reset(nullptr); flib_def.reset(nullptr);}6 总结Session是TensorFlow的client和master连接的桥梁,client任何运算也是通过session来run。它是client端最重要的对象。在Python层和C++层,均有不同的session实现。session生命周期会经历四个阶段,create run close和del。四个阶段均由Python前端开始,最终调用到C层后端实现。由此也可以看到,TensorFlow框架的前后端分离和模块化设计是多么的精巧。本文作者:扬易阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 21, 2019 · 5 min · jiezi

30秒的PHP代码片段(3)字符串-String & 函数-Function

本文来自GitHub开源项目点我跳转30秒的PHP代码片段精选的有用PHP片段集合,您可以在30秒或更短的时间内理解这些片段。字符串endsWith判断字符串是否以指定后缀结尾,如果以指定后缀结尾返回true,否则返回false。function endsWith($haystack, $needle){ return strrpos($haystack, $needle) === (strlen($haystack) - strlen($needle));}ExamplesendsWith(‘Hi, this is me’, ‘me’); // truefirstStringBetween返回参数start和end中字符串之间的第一个字符串。function firstStringBetween($haystack, $start, $end){ return trim(strstr(strstr($haystack, $start), $end, true), $start . $end);}ExamplesfirstStringBetween(‘This is a [custom] string’, ‘[’, ‘]’); // customisAnagram检查一个字符串是否是另一个字符串的变位元(不区分大小写,忽略空格、标点符号和特殊字符)。就是所谓的字谜function isAnagram($string1, $string2){ return count_chars($string1, 1) === count_chars($string2, 1);}ExamplesisAnagram(‘fuck’, ‘fcuk’); // trueisAnagram(‘fuckme’, ‘fuckyou’); // falseisLowerCase如果给定字符串是小写的,则返回true,否则返回false。function isLowerCase($string){ return $string === strtolower($string);}ExamplesisLowerCase(‘Morning shows the day!’); // falseisLowerCase(‘hello’); // trueisLowerCase如果给定字符串为大写,则返回true,否则返回false。function isUpperCase($string){ return $string === strtoupper($string);}ExamplesisUpperCase(‘MORNING SHOWS THE DAY!’); // trueisUpperCase(‘qUick Fox’); // falsepalindrome如果给定字符串是回文,则返回true,否则返回false。回文,顾名思义,即从前往后读和从后往前读是相等的function palindrome($string){ return strrev($string) === (string) $string;}Examplespalindrome(‘racecar’); // truepalindrome(2221222); // truestartsWith检查字符串是否是以指定子字符串开头,如果是则返回true,否则返回false。function startsWith($haystack, $needle){ return strpos($haystack, $needle) === 0;}ExamplesstartsWith(‘Hi, this is me’, ‘Hi’); // truecountVowels返回给定字符串中的元音数。使用正则表达式来计算字符串中元音(A, E, I, O, U)的数量。function countVowels($string){ preg_match_all(’/[aeiou]/i’, $string, $matches); return count($matches[0]);}ExamplescountVowels(‘sampleInput’); // 4decapitalize使字符串的第一个字母去大写。对字符串的第一个字母进行无头化,然后将其与字符串的其他部分相加。省略upperRest参数以保持字符串的其余部分完整,或将其设置为true以转换为大写。function decapitalize($string, $upperRest = false){ return lcfirst($upperRest ? strtoupper($string) : $string);}Examplesdecapitalize(‘FooBar’); // ‘fooBar’isContains检查给定字符串输入中是否存在单词或者子字符串。使用strpos查找字符串中第一个出现的子字符串的位置。返回true或false。function isContains($string, $needle){ return strpos($string, $needle);}ExamplesisContains(‘This is an example string’, ’example’); // trueisContains(‘This is an example string’, ‘hello’); // false函数compose返回一个将多个函数组合成单个可调用函数的新函数。function compose(…$functions){ return array_reduce( $functions, function ($carry, $function) { return function ($x) use ($carry, $function) { return $function($carry($x)); }; }, function ($x) { return $x; } );}…为可变数量的参数,http://php.net/manual/zh/func…Examples$compose = compose( // add 2 function ($x) { return $x + 2; }, // multiply 4 function ($x) { return $x * 4; });$compose(3); // 20memoize创建一个会缓存func结果的函数,可以看做是全局函数。function memoize($func){ return function () use ($func) { static $cache = []; $args = func_get_args(); $key = serialize($args); $cached = true; if (!isset($cache[$key])) { $cache[$key] = $func(…$args); $cached = false; } return [‘result’ => $cache[$key], ‘cached’ => $cached]; };}Examples$memoizedAdd = memoize( function ($num) { return $num + 10; });var_dump($memoizedAdd(5)); // [‘result’ => 15, ‘cached’ => false]var_dump($memoizedAdd(6)); // [‘result’ => 16, ‘cached’ => false]var_dump($memoizedAdd(5)); // [‘result’ => 15, ‘cached’ => true]curry(柯里化)把函数与传递给他的参数相结合,产生一个新的函数。function curry($function){ $accumulator = function ($arguments) use ($function, &$accumulator) { return function (…$args) use ($function, $arguments, $accumulator) { $arguments = array_merge($arguments, $args); $reflection = new ReflectionFunction($function); $totalArguments = $reflection->getNumberOfRequiredParameters(); if ($totalArguments <= count($arguments)) { return $function(…$arguments); } return $accumulator($arguments); }; }; return $accumulator([]);}Examples$curriedAdd = curry( function ($a, $b) { return $a + $b; });$add10 = $curriedAdd(10);var_dump($add10(15)); // 25once只能调用一个函数一次。function once($function){ return function (…$args) use ($function) { static $called = false; if ($called) { return; } $called = true; return $function(…$args); };}Examples$add = function ($a, $b) { return $a + $b;};$once = once($add);var_dump($once(10, 5)); // 15var_dump($once(20, 10)); // nullvariadicFunction(变长参数函数)变长参数函数允许使用者捕获一个函数的可变数量的参数。函数接受任意数量的变量来执行代码。它使用for循环遍历参数。function variadicFunction($operands){ $sum = 0; foreach($operands as $singleOperand) { $sum += $singleOperand; } return $sum;}ExamplesvariadicFunction([1, 2]); // 3variadicFunction([1, 2, 3, 4]); // 10相关文章:30秒的PHP代码片段(1)数组 - Array30秒的PHP代码片段(2)数学 - Math ...

February 19, 2019 · 2 min · jiezi

如何在Flutter上优雅地序列化一个对象

序列化一个对象才是正经事对象的序列化和反序列化是我们日常编码中一个非常基础的需求,尤其是对一个对象的json encode/decode操作。每一个平台都会有相关的库来帮助开发者方便得进行这两个操作,比如Java平台上赫赫有名的GSON,阿里巴巴开源的fastJson等等。而在flutter上,借助官方提供的JsonCodec,只能对primitive/Map/List这三种类型进行json的encode/decode操作,对于复杂类型,JsonCodec提供了receiver/toEncodable两个函数让使用者手动“打包”和“解包”。显然,JsonCodec提供的功能看起来相当的原始,在闲鱼app中存在着大量复杂对象序列化需求,如果使用这个类,就会出现集体“带薪序列化”的盛况,而且还无法保证正确性。来自官方推荐聪明如Google官方,当然不会坐视不理。json_serializable的出现就是官方给出的推荐,它借助Dart Build System中的build_runner和json_annotation库,来自动生成fromJson/toJson函数内容。(关于使用build_runner生成代码的原理,之前兴往同学的文章已经有所提及)关于如何使用json_serializable网上已经有很多文章了,这里只简单提一些步骤:Step 1 创建一个实体类Step 2 生成代码:来让build runner生成序列化代码。运行完成后文件夹下会出现一个xxx.g.dart文件,这个文件就是生成后的文件。Step 3 代理实现:把fromJson和toJson操作代理给上面生成出来的类我们为什么不用这个实现json_serializable完美实现了需求,但它也有不满足需求的一面:使用起来有些繁琐,多引入了一个类很重要的一点是,大量的使用"as"会给性能和最终产物大小产生不小的影响。实际上闲鱼内部的《flutter编码规范》中,是不建议使用"as"的。(对包大小的影响可以参见三笠同学的文章,同时dart linter也对as的性能影响有所描述)一种正经的方式基于上面的分析,很明显的,需要一种新的方式来解决我们面临的问题,我们暂且叫它,fish-serializable需要实现的功能我们首先来梳理一下,一个序列化库需要用到:获取可序列化对象的所有field以及它们的类型信息能够构造出一个可序列化对象,并对它里面的fields赋值,且类型正确支持自定义类型最好能够解决泛型的问题,这会让使用更加方便最好能够轻松得在不同的序列化/反序列化方式中切换,例如json和protobuf。困难在哪里flutter禁用了dart:mirrors,反射API无法使用,也就无法通过反射的方式new一个instance、扫描class的fields。泛型的问题由于dart不进行类型擦出,可以获取,但泛型嵌套后依然无法解开。Let’s rock无法使用dart:mirrors是个“硬”问题,没有反射的支持,类的内容就是一个黑盒。于是我们在迈出第一步的时候就卡壳了- -!这个时候笔者脑子里闪过了很多画面,白驹过隙,乌飞兔走,啊,不是…是c++,c++作为一种无法使用反射的语言,它是如何实现对象的 序列化/反序列化 操作的呢?一顿搜索猛如虎之后,发现大神们使用创建类对象的回调函数配合宏的方式来实现c++中类似反射这样的操作。这个时候,笔者又想到了曾经朝夕相处的Android(现在已经变成了flutter),Android中的Parcelable序列化协议就是一个很好的参照,它通过writeXXXAPIs将类的数据写入一个中间存储进行序列化,再通过readXXXAPIs进行反序列化,这就解决了我们上面提到的第一个问题,既如何将一个类的“黑盒子”打开。同时,Parcelable协议中还需要使用者提供一个叫做CREATOR的静态内部类,用来在反序列化的时候反射创建一个该类的对象或对象数组,对于没有反射可用的我们来说,用c++的那种回调函数的方式就可以完美解决反序列化中对象创建的问题。于是最终我们的基本设计就是:ValueHolder这是一个数据中转存储的基类,它内部的writeXXX APIs提供展开类内部的fields的能力,而readXXX则用来将ValueHolder中的内容读取赋值给类的fields。readList/readMap/readSerializable函数中的type argument,我们把它作为外部想要解释数据的方式,比如readSerializable<T>(key: ‘object’),表示外部想要把key为object的值解释为T类型。FishSerializableFishSerializable是一个interface,creator是个一个get函数,用来返回一个“创建类对象的回调”,writeTo函数则用来在反序列化的时候放置ValueHoder->fields的代码。JsonSerializer它继承于FishSerializer接口,实现了encode/decode函数,并额外提供encodeToMap和decodeFromMap功能。JsonSerializer类似JsonCodec,直接面向使用者用来json encode/decode以上,我们已经基本做好了一个flutter上支持对象序列化/反序列化操作的库的基本架构设计,对象的序列化过程可以简化为:由于ValueHolder中间存储的存在,我们可以很方便得切换 序列化/反序列器,比如现有的JsonSerializer用来实现json的encode/decode,如果有类似protobuf的需求,我们则可以使用ProtoBufSerializer来将ValueHolder中的内容转换成我们需要的格式。困难是不存在的有了基本的结构设计之后,实现的过程并非一帆风顺。如何匹配类型?为了能支持泛型容器的解析,我们需要类似下面这样的逻辑:List<SerializableObject> list = holder.readList<SerializableObject>(key: ’list’);List<E> readList<E>({String key}){ List<dynamic> list = _read(key);}E _flattenList<E>(List<dynamic> list){ list?.map<E>((dynamic item){ // 比较E是否属于某个类型,然后进行对应类型的转换 });}在Java中,可以使用Class#isAssignableFrom,而在flutter中,我们没有发现类似功能的API提供。而且,如果做下面这个测试,你还会发现一些很有意思的细节:void main() { print(‘int test’); test<int>(1); print(’\r\nint list test’); test<List<int>>(<int>[]); print(’\r\nobject test’); test<A<int>>(A<int>());}void test<T>(T t){ print(T); print(t.runtimeType); print(T == t.runtimeType); print(identical(T, t.runtimeType));}class A<T>{}输出的结果是:可以看到,对于List这样的容器类型,函数的type argument与instance的runtimeType无法比较,当然如果使用t is T,是可以返回正确的值的,但需要构造大量的对象。所以基本上,我们无法进行类型匹配然后做类型转换。如何解析泛型嵌套?接下去就是如何分解泛型容器嵌套的问题,考虑如下场景:Map<String, List<int>> listMap;listMap = holder.readMap<String, List<int>>(key: ’listMap’);readMap中得到的value type是一个List<int>,而我们没有API去切割这个type argument。所以我们采用了一种比较“笨”也相对实用的方式。我们使用字符串切割了type argument,比如:List<int> => <String>[List<int>, List, int]然后在内部展开List或Map的时候,使用字符串匹配的方式匹配类型,在目前的使用中,完美得支持了标准List和Map容器互相嵌套。但目前无法支持标准List和Map之外的其他容器类型。What’s moreIDE插件辅助写过Android的Parcelable的同学应该有种很深刻的体会,Parcelable协议中有大量的“机械”代码需要写,类似设计的fish-serializable也一样。为了不被老板和使用库的同学打死,同时开发了fish-serializable-intelij-plugin来自动生成这些“机械”代码。与json_serializable的对比fish-serializable在使用上配合IDE插件,减少了大量的"as"操作符的使用,同时在步骤上也更加简短方便。相比于json_annotation生成的代码,fish-serializable生成的代码也更具可读性,方便手动修改一些代码实现。fish-serializable可以通过手动接管 序列化/反序列化 过程的方式完美兼容json_annotation等其他方案。目前闲鱼app中已经开始大量使用。开源计划fish-serializable和fish-serializable-intelij-plugin都在开源计划中,相信不久就可以与大家见面,尽请期待~本文作者:闲鱼技术-海潴阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 30, 2019 · 1 min · jiezi

开发函数计算的正确姿势——网页截图服务

前言首先介绍下在本文出现的几个比较重要的概念:函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息参考。Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档参考。fun install: fun install 是 fun 工具的一个子命令,用于安装 pip 和 apt 依赖,提供了命令行接口和 fun.yml 描述文件两种形式。备注: 本文介绍的技巧需要 Fun 版本大于等于 2.9.3。依赖工具本项目是在 MacOS 下开发的,涉及到的工具是平台无关的,对于 Linux 和 Windows 桌面系统应该也同样适用。在开始本例之前请确保如下工具已经正确的安装,更新到最新版本,并进行正确的配置。DockerFunFcliFun 和 Fcli 工具依赖于 docker 来模拟本地环境。对于 MacOS 用户可以使用 homebrew 进行安装:brew cask install dockerbrew tap vangie/formulabrew install funbrew install fcliWindows 和 Linux 用户安装请参考:https://github.com/aliyun/fun/blob/master/docs/usage/installation.mdhttps://github.com/aliyun/fcli/releases安装好后,记得先执行 fun config 初始化一下配置。初始化使用 fun init 命令可以快捷的将本模板项目初始化到本地。$ fun init vangie/puppeteer-example? Please input oss bucket to upload chrome shell? chrome-headless? Please select a region? cn-hangzhou? Please input oss accessKeyId for upload? xxxxxxxxxxxKbBS? Please input oss accessKeySecret for upload? xxxxxxxxxxxx5ZgM上面会提示输入一个 OSS 的 BUCKET,注意 OSS Bucket 是全球唯一的,上面的 chrome-headless 已经被占用了,请换一个新的名称或者一个已经创建好的(已经创建好的,请确保 region 一致)。然后选择一个 OSS 的 Region,请保持和部署函数计算 Region 一致输入一个具备 OSS 写权限的秘钥。安装依赖$ fun installskip pulling image aliyunfc/runtime-nodejs8:build-1.2.0…Task => [UNNAMED] => apt-get update (if need) => apt-get install -y -d -o=dir::cache=/code/.fun/tmp libnss3 => bash -c ‘for f in $(ls /code/.fun/tmp/archives/*.deb); do dpkg -x $f /code/.fun/root; done;’ => bash -c ‘rm -rf /code/.fun/tmp/archives’Task => [UNNAMED] => bash -c ‘curl -L https://github.com/muxiangqiu/puppeteer-fc-starter-kit/raw/master/chrome/headless_shell.tar.gz –output headless_shell.tar.gz’…fun install 会执行 fun.yml 文件里的任务,这些任务会:安装 puppeteer 依赖的 .so 文件;将 puppeteer 依赖的 chrome headless 二进制文件上传到 OSS;安装 npm 依赖。部署$ fun deployusing region: cn-shanghaiusing accountId: ***********4733using accessKeyId: ***********KbBSusing timeout: 60Waiting for service puppeteer to be deployed… Waiting for function html2png to be deployed… Waiting for packaging function html2png code… package function html2png code done function html2png deploy successservice puppeteer deploy success执行$ fcli function invoke -s puppeteer -f html2pngThe screenshot has been uploaded to http://chrome-headless.oss-cn-shanghai.aliyuncs.com/screenshot.png打开上面的返回链接,看到截取出来的是全屏滚动的长图,考虑的篇幅下面只截取了部分:如果想换一个网址,可以使用如下命令格式fcli function invoke -s puppeteer -f html2png –event-str ‘http://www.alibaba.com’调试如果需要在本地调试代码,可以使用如下命令fun local invoke html2png <<<‘http://www.alibaba.com’参考阅读三分钟学会如何在函数计算中使用 puppeteer本文作者:倚贤阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 29, 2019 · 2 min · jiezi

序列模型简介——RNN, Bidirectional RNN, LSTM, GRU

摘要: 序列模型大集合——RNN, Bidirectional RNN, LSTM, GRU既然我们已经有了前馈网络和CNN,为什么我们还需要序列模型呢?这些模型的问题在于,当给定一系列的数据时,它们表现的性能很差。序列数据的一个例子是音频的剪辑,其中包含一系列的人说过的话。另一个例子是英文句子,它包含一系列的单词。前馈网络和CNN采用一个固定长度作为输入,但是,当你看这些句子的时候,并非所有的句子都有相同的长度。你可以通过将所有的输入填充到一个固定的长度来解决这个问题。然而,它们的表现仍然比RNN要差,因为这些传统模型不了解给定输入的上下文环境。这就是序列模型和前馈模型的主要区别所在。对于一个句子,当看到一个词的时候,序列模型试图从在同一个句子中前面的词推导出关系。当我们读一个句子的时候,不会每次遇到一个新词都会再从头开始。我们会根据对所读过单词的理解来处理之后的每个单词。循环神经网络(Recurrent Neural Network,RNN)循环神经网络如上图所示。在一个时间步骤中的每个节点都接收来自上一个节点的输入,并且这可以用一个feedback循环来表示。我们可以深入这个feedback循环并以下图来表示。在每个时间步骤中,我们取一个输入x_i和前一个节点的输出a_i-1,对其进行计算,并生成一个输出h_i。这个输出被取出来之后再提供给下一个节点。此过程将一直继续,直到所有时间步骤都被评估完成。描述如何在每个时间步骤上计算输出的方程式,如下所示:在循环神经网络中的反向传播发生在图2中所示箭头的相反方向上。像所有其它的反向传播技术一样,我们评估一个损失函数,并获取梯度来更新权重参数。循环神经网络中有意思的部分是从右到左出现的反向传播。由于参数从最后的时间步骤更新到最初的时间步骤,这被称为通过时间的反向传播。长短期记忆(Long Short-Term Memory)— LSTM网络循环神经网络的缺点是,随着时间步骤长度的增大,它无法从差得很远的时间步骤中获得上下文环境。为了理解时间步骤t+1的上下文环境,我们有可能需要了解时间步骤0和1中的表示。但是,由于它们相差很远,因此它们所学的表示无法在时间步骤t+1上向前移动,进而对其起作用。“我在法国长大……我能说一口流利的法语”,要理解你说的法语,网络就必须远远地往后查找。但是,它不能这么做,这个问题可以归咎于梯度消失的原因。因此,循环神经网络只能记住短期存储序列。为了解决这个问题,Hochreiter & Schmidhuber提出了一种称为长短期记忆网络。LSTM网络的结构与循环神经网络保持一致,而重复模块会进行更多的操作。增强重复模块使LSTM网络能够记住长期依赖关系。让我们试着分解每个操作,来帮助网络更好地记忆。1、忘记门操作我们从当前时间步骤获取输入,并从前一时间步骤获取学习的表示,之后将它们连接起来。我们将连接后的值传递给一个sigmoid函数,该函数输出一个介于0和1之间的值(f_t)。我们在f_t和c_t-1之间做元素的乘积。如果一个值为0,那么从c_t-1中去掉,如果这个值为1,则完全通过。因此,这种操作也被称为“忘记门操作”。2、更新门操作上图表示的是“更新门操作”。我们将来自当前时间步骤中的值和前一时间步骤中已学习的表示连接起来。将连接的值通过一个tanh函数进行传递,我们生成一些候选值,并通过一个sigmoid函数传递,从候选值中选择一些值,所选的候选值将会被更新到c_t-1。3、输出门操作我们将当前时间步骤的值和前一时间步骤已学习的表示连接起来,并经由一个sigmoid函数传递来选择将要用作输出的值。我们获取单元状态并请求一个tanh函数,然后执行元素方式操作,其只允许选定的输出通过。现在,在一个单一单元中要完成很多的操作。当使用更大的网络时,与循环神经网络相比,训练时间将显著地增加。如果想要减少你的训练时间,但同时也使用一个能记住长期依赖关系的网络,那么还有另一个替代LSTM网络的方法,它被称为门控循环单元。门控循环单元(Gated Recurrent Unit ,GRU Network)与LSTM网络不同的是,门控循环单元没有单元状态,并且有2个门而不是3个(忘记、更新和输出)。门控循环单元使用一个更新门和一个重置门。更新门决定了应该让多少之前的信息通过,而重置门则决定了应该丢弃多少之前的信息。 在上面的图中,z_t表示更新门操作,通过使用一个sigmoid函数,我们决定让哪些之前的信息通过。h_t表示重置门操作,我们将前一时间步骤和当前时间步骤的连接值与r_t相乘。这将产生我们希望从前一时间步骤中所放弃的值。尽管门控循环单元在计算效率上比LSTM网络要高,但由于门的数量减少,它在表现方面仍然排在LSTM网络之后。因此,当我们需要更快地训练并且手头没有太多计算资源的情况下,还是可以选择使用门控循环单元的。双向循环神经网络所有上述双向RNN网络的一个主要问题是,它们从之前的时间步骤中学习表示。有时,你有可能需要从未来的时间步骤中学习表示,以便更好地理解上下文环境并消除歧义。通过接下来的列子,“He said, Teddy bears are on sale” and “He said, Teddy Roosevelt was a great President。在上面的两句话中,当我们看到“Teddy”和前两个词“He said”的时候,我们有可能无法理解这个句子是指President还是Teddy bears。因此,为了解决这种歧义性,我们需要往前查找。这就是双向RNN所能实现的。双向RNN中的重复模块可以是常规RNN、LSTM或是GRU。双向RNN的结构和连接如图10所示。有两种类型的连接,一种是向前的,这有助于我们从之前的表示中进行学习,另一种是向后的,这有助于我们从未来的表示中进行学习。正向传播分两步完成:我们从左向右移动,从初始时间步骤开始计算值,一直持续到到达最终时间步骤为止;我们从右向左移动,从最后一个时间步骤开始计算值,一直持续到到达最终时间步骤为止;结论将双向循环神经网络与LSTM模块相结合可以显著地提高性能,当将它们与监控机制相结合的时候,你可以在机器翻译、情感化分析等实例中获得最高水品的性能表现。希望本文对大家有帮助。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 28, 2019 · 1 min · jiezi

TensorFlow 2.0深度强化学习指南

摘要: 用深度强化学习来展示TensorFlow 2.0的强大特性!在本教程中,我将通过实施Advantage Actor-Critic(演员-评论家,A2C)代理来解决经典的CartPole-v0环境,通过深度强化学习(DRL)展示即将推出的TensorFlow2.0特性。虽然我们的目标是展示TensorFlow2.0,但我将尽最大努力让DRL的讲解更加平易近人,包括对该领域的简要概述。事实上,由于2.0版本的焦点是让开发人员的生活变得更轻松,所以我认为现在是使用TensorFlow进入DRL的好时机,本文用到的例子的源代码不到150行!代码可以在这里或者这里获取。建立由于TensorFlow2.0仍处于试验阶段,我建议将其安装在独立的虚拟环境中。我个人比较喜欢Anaconda,所以我将用它来演示安装过程:> conda create -n tf2 python=3.6> source activate tf2> pip install tf-nightly-2.0-preview # tf-nightly-gpu-2.0-preview for GPU version让我们快速验证一切是否按能够正常工作:>>> import tensorflow as tf>>> print(tf.version)1.13.0-dev20190117>>> print(tf.executing_eagerly())True不要担心1.13.x版本,这只是意味着它是早期预览。这里要注意的是我们默认处于eager模式!>>> print(tf.reduce_sum([1, 2, 3, 4, 5]))tf.Tensor(15, shape=(), dtype=int32)如果你还不熟悉eager模式,那么实质上意味着计算是在运行时被执行的,而不是通过预编译的图(曲线图)来执行。你可以在TensorFlow文档中找到一个很好的概述。深度强化学习一般而言,强化学习是解决连续决策问题的高级框架。RL通过基于某些agent进行导航观察环境,并且获得奖励。大多数RL算法通过最大化代理在一轮游戏期间收集的奖励总和来工作。基于RL的算法的输出通常是policy(策略)-将状态映射到函数有效的策略中,有效的策略可以像硬编码的无操作动作一样简单。在某些状态下,随机策略表示为行动的条件概率分布。演员,评论家方法(Actor-Critic Methods)RL算法通常基于它们优化的目标函数进行分组。Value-based诸如DQN之类的方法通过减少预期的状态-动作值的误差来工作。策略梯度(Policy Gradients)方法通过调整其参数直接优化策略本身,通常通过梯度下降完成的。完全计算梯度通常是难以处理的,因此通常要通过蒙特卡罗方法估算它们。最流行的方法是两者的混合:actor-critic方法,其中代理策略通过策略梯度进行优化,而基于值的方法用作预期值估计的引导。深度演员-批评方法虽然很多基础的RL理论是在表格案例中开发的,但现代RL几乎完全是用函数逼近器完成的,例如人工神经网络。具体而言,如果策略和值函数用深度神经网络近似,则RL算法被认为是“深度”。异步优势演员-评论家(actor-critical)多年来,为了提高学习过程的样本效率和稳定性,技术发明者已经进行了一些改进。首先,梯度加权回报:折现的未来奖励,这在一定程度上缓解了信用分配问题,并以无限的时间步长解决了理论问题。其次,使用优势函数代替原始回报。优势在收益与某些基线之间的差异之间形成,并且可以被视为衡量给定值与某些平均值相比有多好的指标。第三,在目标函数中使用额外的熵最大化项以确保代理充分探索各种策略。本质上,熵以均匀分布最大化来测量概率分布的随机性。最后,并行使用多个工人加速样品采集,同时在训练期间帮助它们去相关。将所有这些变化与深度神经网络相结合,我们得出了两种最流行的现代算法:异步优势演员评论家(actor-critical)算法,简称A3C或者A2C。两者之间的区别在于技术性而非理论性:顾名思义,它归结为并行工人如何估计其梯度并将其传播到模型中。有了这个,我将结束我们的DRL方法之旅,因为博客文章的重点更多是关于TensorFlow2.0的功能。如果你仍然不了解该主题,请不要担心,代码示例应该更清楚。如果你想了解更多,那么一个好的资源就可以开始在Deep RL中进行Spinning Up了。使用TensorFlow 2.0的优势演员-评论家让我们看看实现现代DRL算法的基础是什么:演员评论家代理(actor-critic agent)。如前一节所述,为简单起见,我们不会实现并行工作程序,尽管大多数代码都会支持它,感兴趣的读者可以将其用作锻炼机会。作为测试平台,我们将使用CartPole-v0环境。虽然它有点简单,但它仍然是一个很好的选择开始。在实现RL算法时,我总是依赖它作为一种健全性检查。通过Keras Model API实现的策略和价值首先,让我们在单个模型类下创建策略和价值估计NN:import numpy as npimport tensorflow as tfimport tensorflow.keras.layers as klclass ProbabilityDistribution(tf.keras.Model): def call(self, logits): # sample a random categorical action from given logits return tf.squeeze(tf.random.categorical(logits, 1), axis=-1)class Model(tf.keras.Model): def init(self, num_actions): super().init(‘mlp_policy’) # no tf.get_variable(), just simple Keras API self.hidden1 = kl.Dense(128, activation=‘relu’) self.hidden2 = kl.Dense(128, activation=‘relu’) self.value = kl.Dense(1, name=‘value’) # logits are unnormalized log probabilities self.logits = kl.Dense(num_actions, name=‘policy_logits’) self.dist = ProbabilityDistribution() def call(self, inputs): # inputs is a numpy array, convert to Tensor x = tf.convert_to_tensor(inputs, dtype=tf.float32) # separate hidden layers from the same input tensor hidden_logs = self.hidden1(x) hidden_vals = self.hidden2(x) return self.logits(hidden_logs), self.value(hidden_vals) def action_value(self, obs): # executes call() under the hood logits, value = self.predict(obs) action = self.dist.predict(logits) # a simpler option, will become clear later why we don’t use it # action = tf.random.categorical(logits, 1) return np.squeeze(action, axis=-1), np.squeeze(value, axis=-1)验证我们验证模型是否按预期工作:import gymenv = gym.make(‘CartPole-v0’)model = Model(num_actions=env.action_space.n)obs = env.reset()# no feed_dict or tf.Session() needed at allaction, value = model.action_value(obs[None, :])print(action, value) # [1] [-0.00145713]这里要注意的事项:模型层和执行路径是分开定义的;没有“输入”图层,模型将接受原始numpy数组;可以通过函数API在一个模型中定义两个计算路径;模型可以包含一些辅助方法,例如动作采样;在eager的模式下,一切都可以从原始的numpy数组中运行;随机代理现在我们可以继续学习一些有趣的东西A2CAgent类。首先,让我们添加一个贯穿整集的test方法并返回奖励总和。class A2CAgent: def init(self, model): self.model = model def test(self, env, render=True): obs, done, ep_reward = env.reset(), False, 0 while not done: action, _ = self.model.action_value(obs[None, :]) obs, reward, done, _ = env.step(action) ep_reward += reward if render: env.render() return ep_reward让我们看看我们的模型在随机初始化权重下得分多少:agent = A2CAgent(model)rewards_sum = agent.test(env)print("%d out of 200" % rewards_sum) # 18 out of 200离最佳转台还有很远,接下来是训练部分!损失/目标函数正如我在DRL概述部分所描述的那样,代理通过基于某些损失(目标)函数的梯度下降来改进其策略。在演员评论家中,我们训练了三个目标:用优势加权梯度加上熵最大化来改进策略,并最小化价值估计误差。import tensorflow.keras.losses as klsimport tensorflow.keras.optimizers as koclass A2CAgent: def init(self, model): # hyperparameters for loss terms self.params = {‘value’: 0.5, ’entropy’: 0.0001} self.model = model self.model.compile( optimizer=ko.RMSprop(lr=0.0007), # define separate losses for policy logits and value estimate loss=[self._logits_loss, self._value_loss] ) def test(self, env, render=True): # unchanged from previous section … def _value_loss(self, returns, value): # value loss is typically MSE between value estimates and returns return self.params[‘value’]*kls.mean_squared_error(returns, value) def _logits_loss(self, acts_and_advs, logits): # a trick to input actions and advantages through same API actions, advantages = tf.split(acts_and_advs, 2, axis=-1) # polymorphic CE loss function that supports sparse and weighted options # from_logits argument ensures transformation into normalized probabilities cross_entropy = kls.CategoricalCrossentropy(from_logits=True) # policy loss is defined by policy gradients, weighted by advantages # note: we only calculate the loss on the actions we’ve actually taken # thus under the hood a sparse version of CE loss will be executed actions = tf.cast(actions, tf.int32) policy_loss = cross_entropy(actions, logits, sample_weight=advantages) # entropy loss can be calculated via CE over itself entropy_loss = cross_entropy(logits, logits) # here signs are flipped because optimizer minimizes return policy_loss - self.params[’entropy’]*entropy_loss我们完成了目标函数!请注意代码的紧凑程度:注释行几乎比代码本身多。代理训练循环最后,还有训练回路本身,它相对较长,但相当简单:收集样本,计算回报和优势,并在其上训练模型。class A2CAgent: def init(self, model): # hyperparameters for loss terms self.params = {‘value’: 0.5, ’entropy’: 0.0001, ‘gamma’: 0.99} # unchanged from previous section … def train(self, env, batch_sz=32, updates=1000): # storage helpers for a single batch of data actions = np.empty((batch_sz,), dtype=np.int32) rewards, dones, values = np.empty((3, batch_sz)) observations = np.empty((batch_sz,) + env.observation_space.shape) # training loop: collect samples, send to optimizer, repeat updates times ep_rews = [0.0] next_obs = env.reset() for update in range(updates): for step in range(batch_sz): observations[step] = next_obs.copy() actions[step], values[step] = self.model.action_value(next_obs[None, :]) next_obs, rewards[step], dones[step], _ = env.step(actions[step]) ep_rews[-1] += rewards[step] if dones[step]: ep_rews.append(0.0) next_obs = env.reset() _, next_value = self.model.action_value(next_obs[None, :]) returns, advs = self._returns_advantages(rewards, dones, values, next_value) # a trick to input actions and advantages through same API acts_and_advs = np.concatenate([actions[:, None], advs[:, None]], axis=-1) # performs a full training step on the collected batch # note: no need to mess around with gradients, Keras API handles it losses = self.model.train_on_batch(observations, [acts_and_advs, returns]) return ep_rews def _returns_advantages(self, rewards, dones, values, next_value): # next_value is the bootstrap value estimate of a future state (the critic) returns = np.append(np.zeros_like(rewards), next_value, axis=-1) # returns are calculated as discounted sum of future rewards for t in reversed(range(rewards.shape[0])): returns[t] = rewards[t] + self.params[‘gamma’] * returns[t+1] * (1-dones[t]) returns = returns[:-1] # advantages are returns - baseline, value estimates in our case advantages = returns - values return returns, advantages def test(self, env, render=True): # unchanged from previous section … def value_loss(self, returns, value): # unchanged from previous section … def logits_loss(self, acts_and_advs, logits): # unchanged from previous section …训练和结果我们现在已经准备好在CartPole-v0上训练我们的单工A2C代理了!训练过程不应超过几分钟,训练完成后,你应该看到代理成功达到200分中的目标。rewards_history = agent.train(env)print(“Finished training, testing…")print("%d out of 200” % agent.test(env)) # 200 out of 200在源代码中,我包含了一些额外的帮助程序,可以打印出运行的奖励和损失,以及rewards_history的基本绘图仪。静态计算图有了所有这种渴望模式的成功的喜悦,你可能想知道静态图形执行是否可以。当然!此外,我们还需要多一行代码来启用它!with tf.Graph().as_default(): print(tf.executing_eagerly()) # False model = Model(num_actions=env.action_space.n) agent = A2CAgent(model) rewards_history = agent.train(env) print(“Finished training, testing…”) print("%d out of 200" % agent.test(env)) # 200 out of 200有一点需要注意,在静态图形执行期间,我们不能只有Tensors,这就是为什么我们在模型定义期间需要使用CategoricalDistribution的技巧。事实上,当我在寻找一种在静态模式下执行的方法时,我发现了一个关于通过Keras API构建的模型的一个有趣的低级细节。还有一件事…还记得我说过TensorFlow默认是运行在eager模式下吧,甚至用代码片段证明它吗?好吧,我错了。如果你使用Keras API来构建和管理模型,那么它将尝试将它们编译为静态图形。所以你最终得到的是静态计算图的性能,具有渴望执行的灵活性。你可以通过model.run_eagerly标志检查模型的状态,你也可以通过设置此标志来强制执行eager模式变成True,尽管大多数情况下你可能不需要这样做。但如果Keras检测到没有办法绕过eager模式,它将自动退出。为了说明它确实是作为静态图运行,这里是一个简单的基准测试:# create a 100000 samples batchenv = gym.make(‘CartPole-v0’)obs = np.repeat(env.reset()[None, :], 100000, axis=0)Eager基准%%timemodel = Model(env.action_space.n)model.run_eagerly = Trueprint(“Eager Execution: “, tf.executing_eagerly())print(“Eager Keras Model:”, model.run_eagerly) = model(obs)######## Results #######Eager Execution: TrueEager Keras Model: TrueCPU times: user 639 ms, sys: 736 ms, total: 1.38 s静态基准%%timewith tf.Graph().as_default(): model = Model(env.action_space.n) print(“Eager Execution: “, tf.executing_eagerly()) print(“Eager Keras Model:”, model.run_eagerly) _ = model.predict(obs)######## Results #######Eager Execution: FalseEager Keras Model: FalseCPU times: user 793 ms, sys: 79.7 ms, total: 873 ms默认基准%%timemodel = Model(env.action_space.n)print(“Eager Execution: “, tf.executing_eagerly())print(“Eager Keras Model:”, model.run_eagerly) = model.predict(obs)######## Results #######Eager Execution: TrueEager Keras Model: FalseCPU times: user 994 ms, sys: 23.1 ms, total: 1.02 s正如你所看到的,eager模式是静态模式的背后,默认情况下,我们的模型确实是静态执行的。结论希望本文能够帮助你理解DRL和TensorFlow2.0。请注意,TensorFlow2.0仍然只是预览版本,甚至不是候选版本,一切都可能发生变化。如果TensorFlow有什么东西你特别不喜欢,让它的开发者知道!人们可能会有一个挥之不去的问题:TensorFlow比PyTorch好吗?也许,也许不是。它们两个都是伟大的库,所以很难说这样谁好,谁不好。如果你熟悉PyTorch,你可能已经注意到TensorFlow 2.0不仅赶上了它,而且还避免了一些PyTorch API的缺陷。在任何一种情况下,对于开发者来说,这场竞争都已经为双方带来了积极的结果,我很期待看到未来的框架将会变成什么样。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 28, 2019 · 4 min · jiezi

开发函数计算的正确姿势 —— 安装第三方依赖

前言首先介绍下在本文出现的几个比较重要的概念:函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息参考。Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档参考。fun install: fun install 是 fun 工具的一个子命令,用于安装 pip 和 apt 依赖,提供了命令行接口和 fun.yml 描述文件两种形式。备注: 本文介绍的技巧需要 Fun 版本大于等于 2.9.3。函数计算安装第三方依赖一大痛点,文章 函数计算安装依赖库方法小结 对可能会遇到的问题和解决方法做了细致总结,fun install 是基于之前的经验和成果将最佳实践的方法固化到工具中,方便用户便捷的安装依赖。初始化使用 fun install init 在当前目录初始化一个 fun.yml 文件。(这一步不是必须的,如果您打算手写 fun.yml 然后通过 fun install 命令批量执行 task,init 是一个好的开始。)在函数计算项目根目录执行 fun install init 命令,选择一个 runtime。$ fun install init? Select runtime (Use arrow keys) python2.7 python3 nodejs6 nodejs8 java8 php7.2然后会在当前目录生成一个 fun.yml 文件,内容如下:runtime: python2.7tasks: []安装 pip 包依赖下面的命令安装 python 的 tensorflow 包$ fun install –runtime python2.7 –package-type pip tensorflowskip pulling image aliyunfc/runtime-python2.7:build-1.2.0…Task => [UNNAMED] => PYTHONUSERBASE=/code/.fun/python pip install –user tensorflow说明–runtime 指定 runtime,如果已经初始化 fun.yml 文件, 由于 fun.yml 里声明了 runtime ,该选项可以省略。–package-type 指定安装依赖的类型,pip 和 apt 是目前的两个可选值。tensorflow 是一个 pip 包名。命令执行在 fc-docker 提供的 container 中,容器内部执行的命令会逐行打印出来,比如上面命令中内部真实执行了 PYTHONUSERBASE=/code/.fun/python pip install –user tensorflow 命令。安装完成以后会在生成一个 .fun 目录, 可执行文件会被放置到 .fun/python/bin 目录下,库文件放置到 .fun/python/lib/python2.7/site-packages 下。.fun└── python ├── bin │ ├── freeze_graph │ ├── markdown_py │ ├── pbr │ ├── saved_model_cli │ ├── tensorboard │ ├── tflite_convert │ ├── toco │ └── toco_from_protos └── lib └── python2.7 └── site-packages ├── tensorboard ├── tensorboard-1.12.2.dist-info ├── tensorflow ├── tensorflow-1.12.0.dist-info ├── termcolor-1.1.0.dist-info …相比之前的 pip install -t . <package-name> 方式,fun install 安装文件的存放位置更有组织,依赖文件和代码文件分离开了,便于清理、拆分后借助 OSS 或 NAS 初始化依赖文件。但是组织过后也带来一个新问题,需要用户自定义环境变量库文件才能被程序找到。为了方便用户使用提供了一个 fun install env 打印出必要的环境变量。$ fun install envLD_LIBRARY_PATH=/code/.fun/root/usr/lib/x86_64-linux-gnu:/code:/code/lib:/usr/local/libPATH=/code/.fun/root/usr/local/bin:/code/.fun/root/usr/local/sbin:/code/.fun/root/usr/bin:/code/.fun/root/usr/sbin:/code/.fun/root/sbin:/code/.fun/root/bin:/code/.fun/python/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/binPYTHONUSERBASE=/code/.fun/python关于如果设定函数计算的环境变量,请参考 https://help.aliyun.com/document_detail/69777.html 。如果您使用 fun local 和 fun deploy 进行调试和部署,您无需关注环境变量问题,已经帮您设定好了。使用 –save 持久化install 命令加上 –save 参数,会将命令持久化成 task 保存到 fun.yml 文件中。$ fun install –runtime python2.7 –package-type pip –save tensorflowskip pulling image aliyunfc/runtime-python2.7:build-1.2.0…Task => [UNNAMED] => PYTHONUSERBASE=/code/.fun/python pip install –user tensorflow上面的命令多加了一行 –save 参数,查看 fun.yml 内容:runtime: python2.7tasks: - pip: tensorflow local: true之后直接执行 fun install 不带参数,就可以依次执行任务。$ fun installskip pulling image aliyunfc/runtime-python2.7:build-1.2.0…Task => [UNNAMED] => PYTHONUSERBASE=/code/.fun/python pip install –user tensorflow使用 -v 显示详细日志$ fun install -vskip pulling image aliyunfc/runtime-python3.6:build-1.2.0…Task => [UNNAMED] => apt-get update (if need)Ign http://mirrors.aliyun.com stretch InReleaseGet:1 http://mirrors.aliyun.com stretch-updates InRelease [91.0 kB]Get:2 http://mirrors.aliyun.com stretch-backports InRelease [91.8 kB]Get:3 http://mirrors.aliyun.com stretch/updates InRelease [94.3 kB]Hit http://mirrors.aliyun.com stretch Release.gpgHit http://mirrors.aliyun.com stretch ReleaseGet:4 http://mirrors.aliyun.com stretch-updates/main Sources [3911 B]….安装 apt 包依赖函数计算使用 apt-get 安装依赖是另一类常见的安装问题,使用 fun install 也可以方便的安装。$ fun install –runtime python3 –package-type apt libzbar0skip pulling image aliyunfc/runtime-python3.6:build-1.2.0…Task => [UNNAMED] => apt-get update (if need) => apt-get install -y -d -o=dir::cache=/code/.fun/tmp libzbar0 => bash -c ‘for f in $(ls /code/.fun/tmp/archives/.deb); do dpkg -x $f /code/.fun/root; done;’ => bash -c ‘rm -rf /code/.fun/tmp/archives’使用方法及其参数和 pip 包依赖类似,只需要将 –package-type 设定成 apt, 包名使用日常 apt-get 可以安装的 deb 包名即可。使用 fun.ymlfun.yml 由一组 task 组成,执行 fun install 命令时会依次执行 task ,达到批量安装的效果。fun.yml 的文件格式如下runtime: python3tasks: - name: install libzbar0 apt: libzbar0 local: true - name install Pillow by pip pip: Pillow local: true - name: just test shell task shell: echo ‘111’ > 1.txtruntime 是必填的字段。目前 task 有三种类型:apt, pip 和 shell。fun.yml 文件放置在 template.yml 文件中函数 codeUri 指向的目录,如果 template.yml 里声明了多个函数,并且放置在不同的 codeUri 目录,需要创建多个 fun.yml 文件。所有 task 的 name 字段是可选的,没有 name 字段的时候执行的时候会输出为Task => [UNNAMED]apt/pip taskapt 和 pip 类型的 task 都是 install task 的子类型,描述格式类似name: install libzbar0apt: libzbar0local: true上面的 task 描述与下面的命令是等价的fun install –package-type apt libzbar0在使用 fun install 安装的过程中,使用 –save 参数可以在当前目录的 fun.yml 文件中生成上面 task 的描述结构。local 字段默认为 true,表示依赖会被装在当前目录的 .fun 子目录下,打包 zip 的时候回一并打包进去。设定为 false,依赖安装到系统目录,这种情况一般用于编译依赖,比如某个执行文件或者库是编译或者构建期需要的,运行期不要,那可以设定 local: false,打包的时候会被忽略,不影响最终 zip 包的文件尺寸。shell taskshell 类型的 task 是为基于源码编码的安装场景设计的。name: install from sourceshell: ./autogen.sh –disable-report-builder –disable-lpsolve –disable-coinmp示例下面是一个 python3 实现简单二维码识别程序部署到函数计算的例子。源码位于 https://github.com/aliyun/fun/tree/master/examples/install/pyzbar_example本例子使用 pip 的 pyzbar 库进行二维码识别,pyzbar 依赖 apt-get 安装的 libzbar0 库。装载图片需要 pip 的 Pillow 库。所以 fun.yml 的文件描述如下runtime: python3tasks: - apt: libzbar0 local: true - pip: Pillow local: true - pip: pyzbar local: true使用 fun install 安装依赖$ fun installskip pulling image aliyunfc/runtime-python3.6:build-1.2.0…Task => [UNNAMED] => apt-get update (if need) => apt-get install -y -d -o=dir::cache=/code/.fun/tmp libzbar0 => bash -c ‘for f in $(ls /code/.fun/tmp/archives/.deb); do dpkg -x $f /code/.fun/root; done;’ => bash -c ‘rm -rf /code/.fun/tmp/archives’Task => [UNNAMED] => PYTHONUSERBASE=/code/.fun/python pip install –user PillowTask => [UNNAMED] => PYTHONUSERBASE=/code/.fun/python pip install –user pyzbartemplate.yml 文件内容如下ROSTemplateFormatVersion: ‘2015-09-01’Transform: ‘Aliyun::Serverless-2018-04-03’Resources: pyzbar-srv: Type: ‘Aliyun::Serverless::Service’ pyzbar-fun: Type: ‘Aliyun::Serverless::Function’ Properties: Handler: index.handler Runtime: python3 Timeout: 60 MemorySize: 128 CodeUri: .index.py 文件内容如下:from pyzbar.pyzbar import decodefrom pyzbar.pyzbar import ZBarSymbolfrom PIL import Imagedef handler(event, context): img = Image.open(’./qrcode.png’) return decode(img, symbols=[ZBarSymbol.QRCODE])[0].data使用 fun local 在本地执行fun local invoke pyzbar-funskip pulling image aliyunfc/runtime-python3.6:1.2.0…ThalassiodraconRequestId: 964980d1-1f1b-4f91-bfd8-eadd26a307b3 Billed Duration: 630 ms Memory Size: 1998 MB Max Memory Used: 32 MBThalassiodracon 即为识别后的输出结果。小结本文介绍了 fun 工具的一个新特性 fun install ,使用 fun install 可以方便的安装 apt 和 pip 软件包,对于多次安装的工程化需求可以考虑将安装步骤持久化为 fun.yml 文件. fun.yml 文件提供了比命令行更多的功能,可以编写 shell 类型的 task,以支持源码安装的场景。可以通过设定 local: false 将依赖安装的系统目录,以解决编译依赖而非运行依赖的情况。本文作者:倚贤阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 25, 2019 · 4 min · jiezi

pandas指南:做更高效的数据科学家

摘要:Python是开源的,所以有很多开源固有的问题。如果你是Python新手,很难知道针对特定任务的包哪个是最好的。你需要有经验的人来告诉你。今天我要告诉你们的是:在数据科学中,有一个软件包是你们绝对需要学习的,那就是pandas。而pandas真正有趣的地方是,很多其他的包也在里面。pandas是一个核心包,因此它具有来自其他各种包的特性。pandas类似于Python中的Excel:它使用表(即DataFrame)并对数据进行转换,但它还能做更多。如果你已经熟悉Python,可以直接进入第三部分现在让我们开始:import pandas as pdpandas包最基本的功能1、读取数据:data = pd.read_csv(‘my_file.csv’)data=pd.read_csv(‘my_file.csv’,sep=’;’,encoding=‘latin-1’,nrows=1000, kiprows=[2,5])sep变量代表分隔符。因为Excel中的csv分隔符是“;”,因此需要显示它。编码设置为“latin-1”以读取法语字符。nrows=1000表示读取前1000行。skiprows=[2,5]表示在读取文件时将删除第2行和第5行最常用的函数:read_csv, read_excel还有一些很不错的函数:read_clipboard、read_sql2、写入数据data.to_csv(‘my_new_file.csv’, index=None)index=None将简单地按原样写入数据。如果你不写index=None,会得到额外的行。我通常不使用其他函数,比如to_excel,to_json,to_pickle,to_csv,虽然它们也做得很好,但是csv是保存表最常用的方法。3、检查数据:data.shapedata.describe()data.head(3).head(3)打印数据的前3行,.tail()函数将查看数据的最后一行。data.loc[8]打印第8行。data.loc[8, ‘column_1’]将第8行值打印在“column_1”上。data.loc[range(4,6)]打印第4行到第6行。pandas的初级功能1、逻辑运算data[data[‘column_1’]==‘french’]data[(data[‘column_1’]==‘french’) & (data[‘year_born’]==1990)]data[(data[‘column_1’]==‘french’)&(data[‘year_born’]==1990)&(data[‘city’]==‘London’)]如果要根据逻辑操作对数据进行运算,在使用& (AND)、~ (NOT)和| (OR)等逻辑操作之前和之后添加“(”&“)”。data[data[‘column_1’].isin([‘french’, ’english’])]不要为同一列编写多个OR,最好是使用.isin()函数。2、基本绘图多亏了matplotlib包,这个特性才得以实现。就像我们在介绍中说的,它可以直接用在pandas身上。data[‘column_numerical’].plot()data[‘column_numerical’].hist()绘制分布图(直方图)%matplotlib inline如果你使用Jupyter,在绘图之前,不要忘记写这一行(在代码中只写一次)3、更新数据data.loc[8, ‘column_1’] = ’english’将’ column_1 ‘的第8行值替换为’ english ‘data.loc[data[‘column_1’]==‘french’, ‘column_1’] = ‘French’在一行中更改多行值pandas的中级功能现在你可以做一些在Excel中很容易做的事情。让我们来挖掘一些在Excel中做不到的神奇事情。1、计算功能data[‘column_1’].value_counts()2、对全行、全列或所有数据的操作data[‘column_1’].map(len)len()函数应用于“column_1”的每个元素map()操作将一个函数应用于列的每个元素。data[‘column_1’].map(len).map(lambda x : x/100).plot()pandas的另一个特点是进行链式操作。它可以帮助你在一行代码中执行多个操作,从而更加简单和高效。data.apply(sum).apply()将函数应用于列。.applymap()将一个函数应用于表(DataFrame)中的所有单元格。3、tqdm包在处理大型数据集时,pandas可能需要一些时间来运行.map()、.apply()、.applymap()操作。tqdm是一个非常有用的包,它可以帮助预测这些操作何时完成。from tqdm import tqdm_notebooktqdm_notebook().pandas()用pandas设置tqdmdata[‘column_1’].progress_map(lambda x : x.count(’e’))将.map()替换为.progress_map(),.apply()和.applymap()也是一样4、相关矩阵和散射矩阵data.corr()data.corr().applymap(lambda x : int(x*100)/100)pd.plotting.scatter_matrix(data, figsize=(12,8))pandas的高级功能1、行列合并在pandas中,行列合并非常简单。data.merge(other_data, on=[‘column_1’, ‘column_2’, ‘column_3’])合并3列只需要一行代码2、分组分组一开始并不简单,但是如果掌握其语法,你将发现这非常简单。data.groupby(‘column_1’)[‘column_2’].apply(sum).reset_index()按列分组,选择要在其上操作函数的另一列。reset_index()将数据重新生成DataFrame(表)3、遍历行dictionary = {}for i,row in data.iterrows():dictionary[row[‘column_1’]] = row[‘column_2’]iterrows()循环两个变量:行索引和行(上面代码中的i和row)。总体来说,pandas是一个帮助数据科学家快速阅读和理解数据的工具包,它也可以说是Python如此优秀的原因之一。我还可以展示更多pandas包其他有趣的特点,但以上所述足以让人理解为什么数据科学家离不开pandas包。总之,pandas包有以下特点:1、 简单易用,隐藏了所有复杂和抽象的计算;2、非常直观;3、快速。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 25, 2019 · 1 min · jiezi

MaxCompute studio与权限那些事儿

背景知识MaxCompute拥有一套强大的安全体系,来保护项目空间里的数据安全。用户在使用MaxCompute时,应理解权限的一些基本概念:权限可分解为三要素,即主体(用户账号或角色),客体(表/资源/函数等),以及操作(与特定客体类型相关),详细参考 https://help.aliyun.com/document_detail/27935.html。授权有两种方式:ACL(基于对象,grant语句)和Policy(基于策略,policy file)。跨项目授权使用package:https://help.aliyun.com/document_detail/34602.html。可通过列标签实现表中列不同的访问控制:https://help.aliyun.com/document_detail/34604.html。为了方便用户更好的理解与使用MaxCompute权限,studio实现了以下功能:权限查看用户在project下有哪些权限,可通过show grants语句获得。studio编辑器已集成权限相关的语句(https://help.aliyun.com/document_detail/27936.html)) 通过快捷键(Windows: Ctrl + J , MAC: Command + J )唤出live template,然后搜索即可:另外,studio对此也提供了图形化的方式显示用户的权限。如下图,点击工具栏上的show privileges按钮,弹出Show user privileges对话框,点击search button, 下方就会显示用户在该project下的权限:json标签页是所有权限的汇总,点击table标签页,则显示用户在table上的权限。鼠标悬停在table标签页上,则提示table的权限说明:权限异常诊断当因缺少权限导致任务报鉴权失败异常时,可通过studio的权限异常诊断,快速寻找解决方案。如下图,点击工具栏上的权限异常诊断按钮,弹出权限异常诊断对话框,在上方文本框中输入完整的鉴权异常信息,然后点击ok按钮,则下方文本框会显示可能的解决方案:权限语句编写MaxCompute提供了一系列的权限语句,studio SQL编辑器已集成这些语句,用户可以利用studio来执行这些语句以完成相应的权限操作。具体的,通过快捷键(Windows: Ctrl + J , MAC: Command + J )唤出live template,然后搜索:另外,在编写授权语句过程中,也支持相应的代码智能提示:授权语句生成除了手写授权语句,studio也支持图形化给用户授权,点击工具栏上的show privileges按钮,弹出Show user privileges对话框,点击Grant privilege标签页,选择好授权对象,下方的SQL窗格就会同步显示其对应的授权语句,然后点击execute grant command,等待后台完成即可。studio中的权限添加MaxCompute project时,studio会尝试列举project下的所有客体到本机,即用户必须有project的list权限。显示表详情时,用户必须具备table的describe权限;显示自定义函数,则必须具备function的read权限。在编辑器中编写SQL,用到的table或function,则也必须有上述读权限。在编辑器中运行某条SQL,则必须具备SQL中表的select权限,同时还必须有project的CreateInstance权限以能提交SQL任务。开发好了UDF,要想发布,则必须有function的write权限。权限好文官方文档 https://help.aliyun.com/document_detail/27926.htmlMaxCompute安全管理指南 https://yq.aliyun.com/articles/686800本文作者:昊一阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 24, 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