阿里云服务-米姆科技官方网站 http://www.51mimu.com 广州米姆信息科技有限公司,深度服务阿里云用户,为用户提供产品及技术支持:云服务器ECS、数据库RDS、网站加速CDN、阿里云邮箱、对象存储OSS、阿里云企业邮箱、WAF防火墙、高防IP、企业短信等产品服务,为用户给予阿里云优惠折扣解决方案、阿里云沙龙及阿里云峰会、 免费云主机试用、阿里云促销优惠活动、代金券优惠等活动,让用户享受米姆服务中心阿里云全栈服务。,在线咨询电话:020-22822863 Wed, 08 Apr 2020 03:03:10 +0800 zh-CN hourly 1 https://www.s-cms.cn/?v=4.7.5 公司文化 Wed, 08 Apr 2020 03:03:10 +0800

目前米姆采用人性化管理及制度综合管理相结合的方式让企业管理效率最大化,目前我公司的管理制度,涉及到安全、流程、认证等各方面,各种制度健全合理,包含部分岗位流程SOP、销售服务准则、职能认证规范及要求等十数种程序文件。从各种制度的执行情况来看,各种制度制定的有效合理,能够有效地给予生产进行指导和员工行为规范的约束


   

米姆核心目标:

米姆立志并已致力成为全球领先的企业数字化转型一站式解决方案和营销一体化的服务提供商。价值观:技术创新、诚信合作、以人为本、服务企业员工:员工相互信任、尊重、积极向上,为员工提供更多的培训机会,鼓励员工技术与管理创新、分享知识与经验,为员工提供一个公平公正的可持续发展平台,为客户提供一个搞效率服务平台。
诚信:铸诚魂 弘商誉品质:品质第一 客户至上创新:持续创新 不断超越分享:整合资源 分享世界
]]>
米姆(MEME)科技简介 Wed, 08 Apr 2020 03:03:10 +0800            

我们初创 却并非新生


    广州米姆信息科技有限公司简称米姆或MEME,伴随着企业上云和产业互联网化的趋势快速发展,米姆立志并已致力成为企业数字化转型一站式解决方案和营销一体化的服务提供商。
我们的核心团队汇聚了来自于阿里、华为和中软等业内领先企业的高端人才,完全具备了理解用户,提供物超所值的解决方案和服务的能力。
    目前,我们已经成为阿里云和华为的生态合作伙伴,为更好地提供数字化解决方案和服务,我们也与相关领域的优秀平台达成战略合作。
    米姆的核心理念在于创新,在于进步,更在于分享。选择我们,就是选择未来!







]]>
为什么我们需要边缘计算?-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 当时间敏感事件发生时,边缘计算胜过云处理。
08.16.18-Why-do-we-need-edge-computing-1068x656_副本.jpg

在过去的15到20年间,已经从本地软件向云计算发生了巨大转变。现在,我们可以从任何地方访问所需的一切,而不受固定位置服务器的限制。但是,云计算运动即将向分散计算的另一方向倾斜。那么为什么我们需要边缘计算呢?

考虑到云网络带来的巨大机遇,这一概念似乎有悖常理。但为了让我们充分利用物联网(IoT)所提供的一切优势,技术必须再次成为本地技术。

看一下农业历史可以得出一些相似之处。一个世纪或更早以前,人们食用当地种植的食物。如果它不在您居住的地方的50-100英里内生长或繁殖,那么您可能将没有机会食用它。

然后,技术出现并打开了新的大门。运输变得更快了,制冷意味着食物可以在不损坏的情况下运输,并且新的耕作技术允许大量生产。随着这些发展,消费者可以从世界各地获得食物。

我们仍在利用容易获得的全球食品的优势,但是由于多种原因,人们已经转向了本地食物。长途运输食品会影响环境。消费者希望为当地经济做出贡献。我们中的许多人都希望我们食用的食物中的人造成分更少。

那么这对云计算意味着什么呢? 就像全球获得食物一样,云计算并没有完全消失。但是,定期进行处理的地方将从云端转移到现在所谓的“边缘”。

什么是边缘计算

如果我们回想对云的了解,就可以将其与本地计算进行比较。本地计算意味着在公司大型机或服务器上集中存储和管理数据。可以说,云计算可转换为一系列“远程”服务器上的数据存储和处理。

因此,如果云计算发生在远程服务器上,则边缘计算的发生位置更接近其记录的动作。边缘计算包括收集数据的传感器(例如RFID标签),现场数据中心以及将它们全部连接起来以支持本地计算的网络。数据处理发生在远离云的源头或“边缘”。边缘计算网络在必要时仍可以连接到云,但是它们不需要云也可以正常运行。

您可能需要一个Nest Thermostat来控制您家里的气候,一个FitBit来衡量您的个人健康状况,甚至可能是Alexa或Google Home作为个人助手。但是对于这些设备,没有任何紧急事件需要解决。您可以等待对Alexa的请求由云处理。

当时间敏感事件发生时,边缘计算胜过云处理。为了使无人驾驶汽车成为现实,这些汽车需要实时对外部因素做出反应。如果自动驾驶汽车在道路上行驶,并且有行人从汽车前走出来,则汽车必须立即停车。它没有时间将信号发送到云端然后等待响应,它必须能够立即处理信号。

边缘计算的好处是什么

显然,速度是使用边缘计算的重要因素,并且有很多解决速度的用例。工厂可以使用边缘计算通过检测人体来大幅度减少工作中受伤的发生率。TSA检查站可以收集通过不同闸门而来的化学物质数据,这些数据可以组合起来制造炸弹。在出现问题之前,城市可以使用边缘计算来解决道路和交叉路口的维护问题。

另一大好处是流程优化。如果自动驾驶汽车、工厂和TSA检查站使用云而不是edge,它们将把收集到的所有数据推送到云上。但是,如果edge做出本地决策,云可能不会立即需要所有这些数据,甚至根本不需要。

借助边缘计算,数据中心可以执行对时间敏感的规则(例如“停车”),然后在带宽需求不那么高时将数据分批流式传输到云中。然后,云可以花时间从边缘分析数据,并发送建议的规则更改,例如“当汽车在50英尺内感觉到人类活动时,缓慢减速”。

除了速度和优化之外,减少停机也是使用边缘计算的主要原因。通过将所有内容推送到云端,您可以使企业不受ISP故障和云服务器停机的影响。今天,许多关键任务操作(如铁路和化工厂)甚至都不会使用云。拥有自己的服务器是保证正常运行的唯一方法。

边缘计算依赖于单个传感器与本地数据中心之间的连接,从而大大减少了停机的机会。

边缘计算的下一步是什么

即使具有提高速度、优化和减少停机等好处,采用边缘计算仍将需要一些关键的工作。毕竟,看看云的采用到底花了多长时间!但是随着时间的流逝,企业将学习边缘计算如何在减少常见风险因素的同时加快运营速度。

原文链接

]]>
【0406 - 0410 直播导视 | PPT 下载】CIO学院揭晓盒马新零售的秘密,物联网之AIoT智慧、边缘计算的未来-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 *本预告时间仅供参考,最终直播时间以直播间信息为准。
*本文提供直播PPT下载,请在对应直播介绍处查看。

本周直播重磅推荐:

4月06日:

【Elasticsearch入门公开课 】23 | 课程总结

直播时间:04-06 19:30
直播亮点:
Elasticsearch 是一款非常强大的开源搜索及分析引擎,在 Search Engine 分类中长期位列第一。而Elasticsearch 除了搜索以外,结合生态 Kibana、Logstash和Beats,Elasticsearch还被广泛运用在大数据近实时分析场景中,包括日志分析、运维监控、信息安全等多个领域。
分享嘉宾
阮一鸣,eBay Pronto 平台技术负责人。
Elasticsearch核心技术系列回顾1-5
01|课程介绍Elasticsearch核心技术与实战
02|内容综述及学习建议
03|Elasticsearch简介及其发展历史
04|Elastic Stack家族成员及其应用场景
05|Elasticsearch的安装与简单配置

*PPT下载待更新

4月07日:

物联网实战:树莓派4温湿度监测、解读AIoT智慧产业园区、边缘计算未来展望

直播时间:04-07 14:00
直播亮点:
数字新基建按下快进键,机遇来了,智联网也登上热门话题。本期邀请阿里内外物联网专家,分享树莓派4实战,演示湿温度检测。用边缘计算打开周边设备快速互联的大门。在线物联网实验平台LinkLab究竟有何魅力应用于在线教育?通过真实落地案例带您了解AIoT技术在产业园区带来哪些新场景、新价值。超强动手能力的物联网开发者,快速解锁新技能新场景。

Service Mesh 的实践及落地风险控制

直播时间:04-07 19:00
直播亮点:
介绍阿里巴巴对 Serivce Mesh 的理解,分享在技术选型方面和发展方面的思考,以及分享如何降低落地的技术风险。
分享嘉宾
至简,阿里云云原生应用平台团队高级技术专家

【阿里CIO学院“技术攻疫大咖说第十五期】罗汉堂秘书长讲述:疫情下的全球经济

直播时间:04-07 19:00
直播亮点:
此次分享围绕疫情、经济、全球三个关键词,从宏观经济的角度,探
讨疫情的持续性、蔓延势头以及对全球经济、金融格局和供应链等影
响。
分享嘉宾
陈龙教授|罗汉堂秘书长、湖畔大学执行教育长

CIO 学院往期视频回看:

*PPT下载待更新

4月08日:

阿里云新品发布会第88期:分析型数据库AnalyticDB for MySQL基础版重磅发布

直播时间:04-08 15:00
直播亮点:
阿里云对象存储同城冗余存储正式商业化,基于快照提供ECS数据保护全面升级。面对云上数据安全防护场景,阿里云提供云原生数据备份容灾能力。一键式解决企业上云面临的严峻数据挑战,保障企业数据安全,助力企业成功上云。

*PPT下载待更新

4月09日:

RocketMQ Go 客户端实践

直播时间:04-02 19:00
直播亮点:
设计与实现,基础用法与高级用法
分享嘉宾
王文锋,开源爱好者,Apache

*PPT下载待更新

【阿里CIO学院“技术攻疫大咖说第十六期】透视盒马:新零售操作系统的秘密

直播时间:04-09 19:00
直播亮点:
企业面对数字化转型,线上线下一体化的背景下,做为CIO和产研团队,我们所要面对的主要挑战是什么?如何重新定位自身的角色?我们会有什么机会呢?大少做为新零售领军企业盒马的产研负责人,他是如何思考的,又有什么最佳实践呢。
通过阿里巴巴研究员大少的直播你将会了解到:
1、数字化下企业IT团队的角色变化及挑战
2、智能硬件设备对企业信息化的帮助
3、新零售数字化下产品最佳实践
分享嘉宾
何崚(大少)|阿里巴巴研究员、盒马产品技术负责人

CIO 学院往期视频回看:

*PPT下载待更新

4月10日

中小企业如何实现在家研发软件

直播时间:04-02 19:30
直播亮点:
通过阿里云云效产品,演示多人多角色如何在线研发软件,包括持续集成、持续交付等过程
分享嘉宾
焦霸,阿里巴巴研发协同平台持续交付负责人

*PPT下载待更新
]]>
达摩院悬壶,看医疗 AI 如何济世-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800

--------点击屏幕右侧或者屏幕底部“+订阅”,关注我,随时分享机器智能最新行业动态及技术干货----------

5.png

阿里达摩院,并不专为抗疫而生。

甚至连马云都可能预料不到,这个机构这么快就展现出及时的作用。

抗疫正当时,不论是与浙江疾控中心合作的基因检测平台,将疑似病例基因分析时间缩至半小时,还是率先在郑州“小汤山”应用的 CT 影像系统,达摩院、扫地僧,都成为阿里 AI 抗疫的代表性标签。

而且影响力还在几何级扩散,据阿里统计,截止到 3 月 31 日,达摩院 CT 影像 AI 已在浙江、河南、湖北、上海、广东、江苏、安徽等 16 个省市近 170 家医院落地,已诊断 34 万临床病例。

随着疫情全球扩散,这个出自阿里达摩院、代表中国 AI 技术前沿的抗疫系统,也在进一步承担起“One world,One Fight”的作用。

但也别神话了达摩院和背后的扫地僧——外界只看见了他们的动能速度,而不知道他们之前为此积累的势能和高度。

这把抗疫宝剑,之前已磨砺整整 4 年。

“扫地僧”带队,达摩院如何 4 年磨一剑?

6.png

阿里巴巴集团副总裁、达摩院高级研究员华先胜

达摩院医疗 AI,源自 2016 年,由时任阿里巴巴 iDST 副院长的华先胜带队打造。

对于华先胜,外界不陌生。

他出自黄冈中学,2001 年北大数学系博士毕业,之前在微软工作 14 年,直到被老领导“阿里云之父”王坚挖角,开启阿里在 AI 视觉研发和落地方面的探索。

但即便已是 2016 年,人工智能如何在医疗领域发挥作用并不太清晰,只是华先胜坚信:人工智能进入医疗健康领域,是一个必然的事情。

他回忆说:“医疗健康领域存在的不平衡问题比较多(医患供需不平衡,医保供需不平衡,医患知识不平衡等),AI 应该能为全民健康的问题发挥重要作用。”

7.png

正是这样的预判,才逐渐演变出了抗疫期间的达摩院医疗 AI。

当时也是医疗 AI 的爆发点,大量创业者与资本玩家涌入。目前发展势头不错的推想、体素等,都在 2016 年创立。

一直深耕视觉领域的依图,也在同年发布肺癌影像智能诊断系统,开辟了医疗业务线。

作为视觉智能领域深耕多年的专家,华先胜团队从肺部 CT 影像开始切入医疗 AI,很快就做出成绩。

2017 年 7 月,达摩院正式成立前夕,他们交出了一份“贺礼”——在国际权威的肺结节检测大赛 LUNA16 上打破世界纪录,凭借 89.7% 的平均召回率(在样本数据中成功发现结节占比的比例)夺冠。

8.png

华先胜说,这项工作“当时只道是寻常”,没想到直接为阿里达摩院新冠肺炎的 CT 自动诊断系统打下了基础。

那次破纪录之后,达摩院医疗 AI 的研究范围进一步扩大,在肝结节、心血管、骨科、病理等方面均取得了进展。

其中代表性的成果有:

2018 年 12 月,达摩院 AI 从近百支队伍中脱颖而出,在全球 LiTS(Liver Tumor Segmentation Challenge,肝脏肿瘤病灶区 CT 图像分割挑战)获得两项第一。

2019 年的 EMNLP BB task 关系抽取世界大赛,达摩院 AI 获得了第一名。同年,按照鹿特丹(Rotterdam)国际比赛标准,达摩院 AI 的全自动心脏冠脉中心线提取超越了现有的业界最好成绩,相关论文被国际顶级医学影像会议 MICCAI 2019 提前接收。

技术持续突破下, 达摩院医疗 AI 团队在 Nature 子刊、CVPR 等顶尖学术期刊与会议上,发表了不少的论文,加上国际、国内专利,超过了百篇,也给达摩院医疗 AI 系统在临床诊断和医学研究上大规模应用,提供了支撑。

如果这样回顾,自然又是一个“大牛带队”、“顺风顺水”的传奇往事。

但华先胜说,各种辛苦,做过才知。医疗 AI,不是一个单兵作战就能搞定的领域。

达摩院医疗 AI 一路走过来,这些技术突破、业务落地背后,坑踩得一个都不少。

医疗 AI,只有技术可不够

大道理其实也简单:想要打造好的 AI 系统,高质量的数据集是关键。

然而对于医疗 AI 来说,这从来不是一个简单的事情。

医疗影像数据质量参差不齐,标准化程度低、人工标注难度大、数据敏感度高等行业性难题,导致 AI 在医学上的学习和应用面临诸多挑战,从而难倒了诸多英雄好汉,甚至还是一些巨头公司的短板。

比如此前名震江湖的 IBM 沃森医疗系统,宣称超过人类医生的存在,在 2018 年被美国健康医疗媒体 STAT 曝光,其 AI 系统训练数据量最高的肺癌只有 635 例,而最低的卵巢癌只有 106 例,引起哗然一片。

而且数据难题之外,还有更现实的挑战:医疗机构不买账。

9.png

据 2018 年 9 月中国信息通信研究院、Gartner 联合发布的《2018 世界人工智能产业蓝皮书》,在中国,医疗健康领域的 AI 企业在所有 AI 企业中占比达到了 22%,在所有垂直行业中占比最高。

但在医疗 AI 行业火热的同时,客户并不怎么感兴趣。

《财经》杂志在 2019 年 3 月份的报道中就指出,资本捧场,使产品同质化严重,送进医院、无人使用的 AI 医疗产品不在少数。AI 逐渐演变为医疗领域的嵌入品,锦上添花的功效有,雪中送炭的本事无。

作为行业中的一份子,达摩院医疗 AI,绕不开,无法回避,唯有迎难而上解决问题。

扫地僧如何破局?

华先胜说,如今回顾,大道至简:“以技术平台为轴心,联合产业伙伴打天下”。

对于阿里来说,这不是一个陌生的模式,不论是淘宝、支付宝,还是现在的阿里云、平头哥,本质上都是以技术建立平台,拥抱行业玩家,来推动产业发展。

医疗AI领域,阿里的优势还更甚,它旗下还有一个在香港上市的公司阿里健康,已经在医药电商及新零售、互联网医疗、消费医疗、智慧医疗等领域深耕多年。

在阿里健康以及众多医疗行业合作伙伴,比如万里云、卫宁健康、古珀科技等的支持下,达摩院医疗 AI 在高质量数据集上不断训练优化,通过阿里云推进技术在医疗行业落地的速度,在行业中都处于前列。

所以才有了当前抗疫中的达摩院医疗 AI。

危急关头,如何一步步见真章?

速度,速度,还是速度。

华先胜说,疫情大面积爆发之初,达摩院医疗 AI 团队就放弃假期行动起来。

连点成线,覆盖疫情咨询、药物研发、病毒基因分析、临床诊断等多个环节。

2020 年 1 月 27 日,达摩院连夜研发的智能疫情机器人。上线后便在全国各地投入使用,很快落地全国 27 个省、直辖市、自治区,免费为 57 座城市拨打 1600 万通防控摸排电话,摸排超过 20 万身体异常人群。

2 天后,阿里云宣布向全球公共科研机构免费开放一切 AI 算力,以加速本次新型肺炎新药和疫苗研发。

钟南山团队、全球健康药物研发中心 GHDDI、北京大学、晶泰科技等机构,都成为受益者。

紧接着 2 月 1 日,达摩院医疗 AI 算法,正式应用于新冠肺炎的病原学检测。

达摩院与浙江省疾控中心合作,利用算法将疑似病例基因分析时间缩至半小时,该技术可以避免核酸检测出现的漏检情况,同时可以及时检测到变异病毒。

华先胜解释:“它每天都在工作,准确率近乎 100%,正进一步推广到更多地方使用。”

而且 ,达摩院也在进一步优化算法,将时间缩短到了 10 分钟,进一步提高效率,并在疫区压力最大的武汉金银潭医院上线。

2 月 15 日,达摩院医疗 AI 团队与合作伙伴一起,基于 5000 CT 影像样本数据,快速研发出了 CT 影像算法,在郑州小汤山上线,可以在 20 秒内对新冠疑似患者 CT 影像做出判读,并量化病症的轻重程度,分析结果准确率达到 96%。

10.png

至今这套 AI 系统已在浙江、河南、湖北、上海、广东、江苏、安徽等 16 个省市的 170 家医院落地,诊断超过 34 万临床病例。

这还不够,为了全方位抗疫。达摩院医疗 AI 团队还提供了医疗专业翻译系统、疫情预测等系统,来为更大范围、更高层次的抗疫,提供信息支撑。

同时,疫情全球化蔓延的情况下,达摩院医疗 AI 随着阿里云一同出海。

很快,日本知名医疗科技机构 JBC 正式上线阿里云新冠肺炎 AI 诊断技术,开始向日本医院提供服务,帮助医生通过 CT 影像快速进行新冠肺炎筛查。

11.png

并且更多的欧洲国家也在跟进。在华先胜的透露中,先后有 30 多个国家和地区,希望达摩院通过阿里云提供医疗 AI 支持。

当然,达摩院并非这次全球科技抗疫中的全部。

疫情之下,全球医疗 AI 的多数玩家,都参与到了抗疫之中。比如国内的依图、推想等公司都推出了相应产品,以自身之长提供解决方案。

国外如谷歌,在美国疫情爆发之时,同样投入了 1700 名程序员,联合专攻“医疗科技”的兄弟公司 Verily,打造新冠病毒检测网站。

“扫地僧”华先胜对此怎么看?

他很开心,认为这次疫情对于整个行业来说,是一次大练兵。

“很多竞争对手都成了并肩作战的伙伴。(抗疫)也让我们与合作伙伴的协同变得更加深入。“

而且更重要的是,经此一疫,医疗 AI 的社会定位、落地推广和作用发挥,也得到意想不到的市场教育和检验。

扫地僧要做一个怎样的医疗 AI?

答案很简单:近期帮助人类医生的 AI,远期帮助大众对健康有更强的把控能力的 AI。

众所周知,抗疫过程中,医生人手缺少的局面一直存在。

2 月 5 日,国家卫健委公布的诊疗方案第五版中,正式将 CT 影像临床诊断结果作为新冠肺炎病例判断的标准之一。

虽然这直接加快了新冠肺炎疑似病例的确诊速度与准确度,但对于前线医生来说,却是不小的负担。

一位新冠肺炎病人的 CT 影像大概在 300 张左右,每诊断一个病例,影像医生需要投入大约为 5-15 分钟时间。一名医生每天连续不间断工作 12 个小时,只能诊断大概 72 个病例。

2 月 4 日晚,全国一共有疑似病例 23260 例,追踪到(新冠肺炎患者)密切接触者 252154 人……提升临床诊断效率,成为抗疫期间核心需求之一。

12.png

而达摩院等医疗 AI 机构,就在这样的险峻形势中出手,联合伙伴打造的新冠病毒 CT 影响诊断系统,从上传数据到得到结果,诊断一个病例平均仅需 20 秒,计算时间最快仅 2 秒。

虽然仍旧需要医生进一步配合才能够得到更准确诊断结果,但对效率的提升, 无疑是巨大的。这也是医疗 AI 能够在抗疫期间得到推广的原因之一。

因此,华先胜认为,这也会成为医疗系统的常态:AI 助攻,人类医生提效。

据新华网 2018 年 12 月份报道,中国医学影像数据的年增长率约为 30%,而放射科医师数量的年增长率约为 4.1%,而且需求缺口不断加大。

对于医疗 AI 行业来说, 这是其进一步发展的机会。

“医疗 AI 的价值在抗疫中得到验证,会对医疗行业和公众产生深远的影响。在接下来几年,将会看到整个医疗行业的数字化和智能化程度大幅度提升。”华先胜说。

达摩院医疗 AI,早已行动起来了。

阿里达摩院透露,他们打造的医疗 AI 系统,已经落地了 170 家医院,在北京、上海、广州、杭州、武汉、郑州等一线城市之外,也部署到了二三线城市——往往都是医疗资源相对匮乏的地方。

华先胜解释,二三线城市未来对医疗健康AI的需求应该会更为迫切,用 AI 提升诊断效率和水准在医疗资源缺乏的区域,是当前和未来医疗健康 AI 落地的一个较大的场景。

13.png

然而,要将医疗 AI 应用到更多的场景,进入寻常生活,还有很长的路要走。

不论是获取更多的医疗数据,还是寻找更加切合的商业模式,以及政策法规的支持等,都是医疗 AI 行业需要解决的问题。

但突发疫情也让其前景变得更明朗。

正所谓“上医治未病,中医治欲病,下医治已病”。现在,医疗 AI 正在疾病治疗中发挥作用。

但华先胜认为,接下来医疗 AI 将会从医生走向大众,从高成本走向普惠,从应用于医疗走向应用于健康。

这是阿里健康体系、达摩院医疗 AI 等将要重点发力的方向。

阿里范式,阿里打法,要再一次在医疗健康领域复刻——成为基础设施,成为医疗健康行业的数字经济基础设施。

它们将长在抗疫号角之下,为不再需要抗疫而生。

在耳熟能详的武侠江湖中,达摩院代表了武学武德的最高成就,扫地僧更是大隐于寺的大神象征,并且总能在危难关头挺身而出。

从这个角度来说,倒与马云和阿里的达摩院创立初心,都有呼应。

而且也给新时代的扫地僧、科研人员,工程师们,提供了更大舞台和机会。

侠之大者,为国为民。

image.png

原文链接:https://yqh.aliyun.com/detail/8253

]]>
跨域请求出现preflight request失败的问题的解决-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 问题出现

这两天在项目联调过程中突然前端同学报告出现CORS跨域问题无法访问。刚听到很奇怪,因为已经在项目里面设置了CORS规则,理论上不会出现这个问题。

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String orignalHeader = request.getHeader("Origin");

        if (orignalHeader != null ) {
            Matcher m = CORS_ALLOW_ORIGIN_REGEX.matcher(orignalHeader);
            if (m.matches()) {
                response.addHeader("Access-Control-Allow-Origin", orignalHeader);
                response.addHeader("Access-Control-Allow-Credentials", "true");
                response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
                response.addHeader("Access-Control-Allow-Headers", "x-dataplus-csrf, Content-Type");
            }
        }
    }

拿到前端给的错误提示后发现了一个奇怪的问题,提示Response to preflight request doesn't pass access control check中的preflight request是什么?

image.png

Preflight request介绍

了解得知跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。同时规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

一个Preflight request的流程可以如下图所示

image.png

什么样的请求会产生Preflight request呢?当请求满足下述任一条件时,即应首先发送Preflight request请求:

  • 使用了下面任一 HTTP 方法:

    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • 人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (需要注意额外的限制)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 的值不属于下列之一:

    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • 请求中的XMLHttpRequestUpload 对象注册了任意多个事件监听器。
  • 请求中使用了ReadableStream对象。

在我们的例子中正是使用了POST方法传递了一个Content-Type为application/json的数据到后端。而这个OPTION请求返回失败后浏览器并没有继续下发POST请求。

解决方案

在弄清楚问题后,我们了解只要给Preflight request优先通过就可以引导后续请求继续下发。对此,我们改造CORS Filter来解决这个问题。

  • 首先对OPTION请求放入HTTP 200的响应内容。
  • 对于Preflight request询问中的的Access-Control-Request-Headers予以通过
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String orignalHeader = request.getHeader("Origin");

        if (orignalHeader != null ) {
            Matcher m = CORS_ALLOW_ORIGIN_REGEX.matcher(orignalHeader);
            if (m.matches()) {
                response.addHeader("Access-Control-Allow-Origin", orignalHeader);
                response.addHeader("Access-Control-Allow-Credentials", "true");
                response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
                response.addHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
            }
        }

        if ("OPTIONS".equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            filterChain.doFilter(request, response);
        }
    }

注意事项

但是天不遂人愿,在上述改造后理论上应该是可以解决Preflight request问题,可以测试发现依然有问题。这时我们注意到错误信息中提到的另外一句Redirect is not allowed for a preflight request.

为什么会有Redirect事情发生呢,原来所有请求在进入我们的CORS Filter之前,会首先通过SSO Filter做登录检测。而这个Preflight request并没有携带登录信息,导致OPTION请求被跳转到了登录页面。同理如果引用了Spring Security组件的的话也会出现首先被登录验证给过滤的问题。

找到问题就比较好办了,调整CORS Filter优先级,让其先于登录验证进行就好了。对此我们调整registrationBean的order从默认的Integer.MAX_VALUE到1就好了。

    @Bean(name = "corsFilter")
    public FilterRegistrationBean corsFilter() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(corsFilterBean());
        registrationBean.setUrlPatterns(Lists.newArrayList("/*"));
        registrationBean.setOrder(1);
        return registrationBean;
    }
]]>
深度学习经典算法PPO的通俗理解-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 1 前置知识点

基本概念
https://www.yuque.com/docs/share/04b60c4c-90ec-49c7-8a47-0dae7d3c78c7?#
(部分符合的定义在这里)

要理解PPO,就必须先理解Actor-Critic.
Actor负责输出policy,也就是在某个状态下执行各种action的概率分布
Critic负责输出Vaue of state。
Actor和Critic的默契:Actor相信Critic给的状态的value就是真的; Critic也相信Actor选送过来的(s,a)中的a就是最优的action。通过不断的迭代,强化这个信任关系的正确性。
(这体现了我们的价值观 [因为信任,所以简单],哈哈哈~)
image.png

所以这样就不难理解Critic的Loss是怎么来的了,Critic的输出就是state的Value,那就让Critic模型的输出使得以下公式成立:
$$V_s=r_{s,a}+gamma V_{s'}$$ 
其中,$r_{s,a}, s,a,s'$是训练Critic需要的数据,$s'$是在状态$s$下执行动作$a$得到新状态, $r_{s,a}$是reward, $gamma$ 是discount factor。
跟基础概念的区别是,这里的系统假定是执行动作$a$只能到$s'$, 没有体现执行$a$可以得到不同的状态; (但是其实这种概率可以体现在训练数据中,因为$(s,a,r_{s,a})
$$和$$s'$ 不一定是一一对应,其概率可以通过sampling得到的数据分布体现)
所以Critic的Loss就是$|r_{s,a}+gamma V_{s'}-Vs|$,也就是所谓的TD(Time Difference)-Error的L1,或者L2也可以.
那么Actor的Loss怎么计算呢?
这里就先来明白Advantage的概念,其实也就是TD-Error
 $$Adv=r_{s,a}+gamma V_{s'}-Vs$$ 
之所以称之为Advantage,是因为假如Advantage>0, 就是说实际执行$a$之后,发现当前的状态Value实际上比当前Critic估计出来的要大,所以这是个好的Action,它能够让$V_s$ 变大,Actor应该增大这个action的概率;反之,Advantage<0,这个action就是不好的,应该减小执行它概率。
所以Actor的Loss就是$$-log(pi(a|s))*Adv$$, 因为要最小化Loss,所以前面加个负号;Adv的符号代表了应该增大这个action的输出概率,还是减小这个action的输出概率;Adv的大小代表了增加或减小的幅度应该有多大。

2 Proximal Policy Optimization(PPO)

2.1 PPO主要是来解决什么问题?

它是为了让我们在训练Agent的时候能够take the biggest possible improvement step on a policy using the data we currently have, without stepping so far that we accidentally cause performance collapse。就是更新policy的时候尽可能步子迈大点,但也要防止扯着蛋,即模型参数崩了。

2.2 PPO怎么解决这个问题的?

简单来说,相同的网络结构,维护两组模型参数Old和New,在每次迭代的时候去更新New的参数,但是要控制New的模型输出Policy和Old的Policy不要差距太大,本轮迭代结束后,把New的参数覆盖掉Old的参数。

怎么去控制差距不要太大呢?作者给了两种方式: PPO-Penalty, PPO-Clip

2.2.1 PPO-Clip

先说PPO-Clip, 它通过下面的公式来更新策略:
$$theta_{k+1}=arg max_{theta}E_{s,a sim pi_{theta_k}}[L(s,a,theta_k,theta)]$$ 
就是最大化$L(s,a,theta_k,theta)$,
$$L(s,a,theta_k,theta)=min left( frac{pi_{theta}(a|s)}{pi_{theta_k}(a|s)}A^{pi_{theta_k}}(s,a) , clip left( frac{pi_{theta}(a|s)}{pi_{theta_k}(a|s)}, 1-epsilon, 1+epsilon right)A^{pi_{theta_k}}(s,a) right)$$
这个形式主要是为了让我们理解为啥叫PPO-Clip(我感觉直接用后面那个Clip项其实就够了,这个表达有点冗余),$theta_k$ 就是当前Old的参数,$theta$ 是New的参数。$pi_{theta}(a|s)$ 是New Actor输出的Policy上状态$s$时执行$a$的概率,$pi_{theta_k}(a|s)$ 表示的Old Actor输出的Policy上状态$s$时执行$a$的概率。$A^{pi_{theta_k}}(s,a)$是基于Old Critic得到的Advantage.
对这个公式进行改写,更容易看出它的真实目的,
$$L(s,a,theta_k,theta)=min left( frac{pi_{theta}(a|s)}{pi_{theta_k}(a|s)}A^{pi_{theta_k}}(s,a) , g left( epsilon, A^{pi_{theta_k}}(s,a) right) right)$$
其中,

$$g left( epsilon, A right)=left{ begin{aligned} &(1+epsilon)A & Age 0 \ &(1-epsilon)A & A< 0 end{aligned} right.$$  当Advantage>=0的时候, $$L(s,a,theta_k,theta)=min left( frac{pi_{theta}(a|s)}{pi_{theta_k}(a|s)}, (1+epsilon) right)A^{pi_{theta_k}}(s,a) $$ 这就清楚的说明,这时候应该增大$pi_{theta}(a|s)$,也就是认为这个action是好的,增加选择$a$的概率。但是通过$1+epsilon$ 限制增大的幅度。 同理,当Advantage<0的时候 $$L(s,a,theta_k,theta)=min left( frac{pi_{theta}(a|s)}{pi_{theta_k}(a|s)}, (1-epsilon) right)A^{pi_{theta_k}}(s,a) $$ 缩小$pi_{theta}(a|s)$,但是幅度不能小于$1-epsilon$ 另外,根据我的理解,$pi_{theta_k}(a|s)$应该截断梯度,也就是反向传到的时候用不着去更新Old Actor的参数。在OpenAI Spinningup的代码([https://github.com/openai/spinningup/blob/master/spinup/algos/pytorch/ppo/ppo.py](https://github.com/openai/spinningup/blob/master/spinup/algos/pytorch/ppo/ppo.py))确实是这样处理的,但是在Tianshou的代码里([https://github.com/thu-ml/tianshou/blob/master/tianshou/policy/ppo.py](https://github.com/thu-ml/tianshou/blob/master/tianshou/policy/ppo.py))没有做截断,结果也OK,想来对于$pi_{theta}(a|s)$来说,$pi_{theta_k}(a|s)$就是一个scalar factor, 这个factor是变量还是静态值,也许影响不那么大,而且本轮迭代结束后$theta_k$也会被覆盖掉,反向传导更新了也白搭。 到这里,其实说的都是如何更新Actor。 怎么更新Critic的参数呢? $$L_c(s,a,r_{s,a},s')=|r_{s,a}+V^{pi_{theta_k}}_{s'}-V^{pi_{theta}}|$$ 唯一的不同是target value是用Old Critic计算的,这也是DRL领域的常规操作了. 小结一下,PPO-Clip就是通过Clip操作来防止步子迈太大的。作者实验证明Clip的效果比Penalty好。 ### 2.2.2 PPO-Penalty $$L^{KLPEN}(theta)=frac{pi_{theta}(a|s)}{pi_{theta_k}(a|s)}A^{pi_{theta_k}}(s,a) -beta KLDleft( pi_{theta}(*|s), pi_{theta_k}(*|s) right)$$ 理解上上面的,这个理解起来也就容易了,就是增加一个新旧Policy差异的惩罚项,差异通过KL divergence来衡量 (PS: 如理解有误支持,欢迎批评指正~)

]]>
CCP OAuth2.0 隐藏式授权实践-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800

内容协作平台(Content Collaboration Platform, 后面简称CCP)是为开发者提供的面向企业、个人数据管理、内容识别、协作的开放平台。CCP 提供多种OAuth2.0协议接口,方便其他第三方应用接入。

本文主要讲解纯前端应用(例如SPA, Chrome插件等)接入CCP所使用的OAuth2.0隐藏式授权的场景和实践方案。

什么是 OAuth2.0? 可以看阮老师的文章: OAuth 2.0 的一个简单解释

OAuth2.0 隐藏式: 有些纯前端 Web 应用,没有后端,无法采用 OAuth2.0 授权码模式,因为将 client_secret 放在前端是很危险的行为。OAuth2.0 隐藏式(implicit) 即是授权服务直接向前端Web应用发令牌。

1. CCP 使用介绍

只需3步配置,即可拥有一个云盘系统。

(1) 创建域

用户可以在 阿里云CCP官网控制台,创建一个域(domain),假设domainID为 hz01, CCP会分配1个 API 子域名: https://hz01.api.alicloudccp.com

image.png

(2) 配置OAuth登录页面

配置好域的用户体系配置:具体的配置方法请看这里

image.png

CCP会分配1个认证授权服务子域名: https://hz01.auth.alicloudccp.com

(3) 直接开通官方提供的BasicUI云盘应用

image.png

允许访问后,再以超级管理员登入一次激活,即可开通成功。

image.png

BasicUI 提供1个子域名: https://hz01.apps.alicloudccp.com。您的用户可以通过此子域名访问云盘系统了。

更多BasicUI的介绍请看:Basic UI简介

2. CCP OAuth2.0 隐藏式授权流程

image.png

(1) 授权请求

用户浏览CCP的云盘应用https://hz01.apps.alicloudccp.com,想要用一个第三方应用在线markdown编辑器打开 a.md 这个文件。

  • 这个编辑器是一个纯前端应用,假设域名为 https://a.com
  • 编辑器提供 redirect_uri 为: https://a.com/callback.html
  • 我们可以构造下授权请求url:
https://hz01.auth.alicloudccp.com/v2/oauth/authorize?
response_type=token&
client_id=xxx&
redirect_uri=CALLBACK_URL&
scope=FILE.ALL
  • 其中 CALLBACK_URL 为 encodeURIComponent('https://a.com/callback.html')
  • client_id 为markdown编辑器的appId(可以在CCP官网控制台创建应用获得,请先看应用概述)。

(2) 用户认证和授权

浏览器请求这个url,会跳转到登录页面,用户登录确认后,会重定向到CALLBACK_URL且通过hash返回access_token等信息,如: https://a.com/callback.html#access_token=xxxxx&expire_in=3600&token_type=Bearer

image.png

(3) callback

编辑器的callback.html页面,解析location的hash。

  • access_token等参数解析出来,保存到本地存储中。
  • callback.html 需要清空历史记录,因为access_token是在url中的,会保留在历史记录里。

(4) 调用CCP API

编辑器就可以通过 access_token 来操作 a.md 文件了。

3. 使用 OAuth Widget

CCP 官方提供了一些拥有特定功能的 Widget, 供第三方应用接入时使用。详情请看Widget 介绍

OAuth Widget即是将上面的OAuth2.0 隐藏式授权逻辑封装起来,做成一个可重用的组件。

下面介绍此widget的用法:

(1) 引入js

<button id="btn_1">登录</button>
<script src="/media/20200407155347mFb.js"></script>

(2) 点击按钮,即可弹出登录窗口

window.onload = function () {
  document.getElementById('btn_1').onclick = async function () {
    var tokenInfo = await CCPWidgets.oAuthLogin({
        domain_id: '<Your Domain ID>',
        client_id: '<Your App ID>' 
    })
    //用户登录授权后,即可拿到tokenInfo
    console.log(tokenInfo)
  }
}

(3) 弹出登录框效果

image.png

参考:JS Widget 授权原理和调用的API

]]>
IDEA 自动注释插件-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 推荐一个自己写的IDEA插件,能帮助java工程师快速生成代码注释(俗话说不会偷懒的程序员不是好程序员)~

打开IntelliJ IDEA -> plugin,搜索 easy-javadoc,安装重启即可

示例图片

github:https://github.com/starcwang/easy_javadoc
IDEA插件官网:https://plugins.jetbrains.com/plugin/12977-easy-javadoc

]]>
让服务器突破性能极限 阿里云神龙论文入选计算机顶会ASPLOS-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 疫情肆虐,全球多个科技领域盛会宣布改为线上举办,计算机领域顶会 ASPLOS也不例外。

日前,ASPLOS 2020公布了计算机界最新科技成果,其中包括阿里云提交的名为《High-density Multi-tenant Bare-metal Cloud》的论文,该论文阐述了阿里云自研的神龙服务器架构如何解决困扰云计算行业多年的虚拟化性能损耗问题,打破物理机的性能神话,让云服务器突破性能极限。

此次入选意味着全球计算机顶会对阿里云自研技术的认可,也意味着中国创新技术在全球计算机界争得了一席之地。

ASPLOS 是综合体系结构、编程语言和操作系统三个方向的计算机系统领域顶级会议,从1982年创办至今推动了多项计算机系统技术的发展,一般论文录用率在20%左右。

阿里云本次入选的论文题为《High-density Multi-tenant Bare-metal Cloud》,由阿里云研究员张献涛带领的神龙技术团队撰写,详细解读了神龙架构的技术优势:超越传统物理机100%的算能、分钟级交付能力、安全物理隔离和云平台全系打通等。

虚拟化是云计算的基础,它将物理服务器虚拟化成想要的计算单元,进而拥有最大的弹性,然而却会导致性能损耗。如何解决这样的矛盾?阿里在2017年推出了“神龙架构”,弥补虚拟化的性能损耗,同时拥有云的弹性和运维优势。

2019年杭州云栖大会上,阿里云发布了第三代神龙架构,全面支持ECS虚拟机、裸金属、云原生容器等,在IOPS、PPS等方面提升5倍性能,可帮助用户降低50%的计算成本。去年双11核心系统100%上云,神龙大放异彩,成功扛住了54.4万笔/秒的订单创建峰值,与同配置物理机相比,不仅业务系统性能提升20%,而且抗高负载压力表现更好,整个业务性能非常平稳和线性。

不仅如此,神龙还是目前最流行的容器技术的最佳拍档。基于神龙架构的阿里云容器服务对比物理机有10%-30%的性能优势。

目前,神龙架构已大规模应用于淘宝、天猫、菜鸟等业务,用于解决高峰值的性能瓶颈问题。

本次入选ACM ASPLOS论文题目为《High-density Multi-tenant Bare-metal Cloud》,由阿里云研究员兼创新产品线总负责人张献涛、阿里云高级技术专家郑晓、阿里云资深技术专家杨航及其他神龙团队共同撰写。论文原文可到以下链接下载https://dl.acm.org/doi/10.1145/3373376.3378507

该文首次全面解析了时下流行的裸金属云计算服务、神龙架构内涵。将作为新一代虚拟化技术发展方向的神龙,与现有架构作对比,详细阐述了两者在软硬件、核心计算性能、虚拟化开销的异同。论文中对多种业务表现上面的性能数据充分揭示了神龙裸金属架构的特有优势。该论文的详细解读请移步文章《阿里云最新ASPLOS论文解读:High-density Multi-tenant Bare-metal Cloud》阅读。

 

]]>
做了那么多架构,你真的懂 SOA 了吗?-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 自从提倡 SOA 架构风格以来,个人觉得软件架构并未有特别突破的变革,主要是在 SOA 面向服务架构风格基础上不断演化迭代,基于服务的 EA 明确分层架构也好,微服务也罢,都是在面向服务架构基础上的适应不同的场景的迭代升级。

我先抛出一个观点,我觉得服务化架构的本质,和西方教育界深受影响的古希腊哲学家苏格拉底的“产婆术”的教育思想本质上是非常相通的:苏格拉底的“产婆术”思想强调教育是一个“接生”的过程,教师就是“接生婆”,人们之所以接受教育是为了寻找“原我”以不断完善自身。也就是教育的目的在于唤醒而不再于塑造。同理服务化架构的本质也不仅仅在采用什么样的技术框架实现和塑造,更重要的是在于通过不停地在共创中反问、反思、反省等方式进行对业务的本质的不断追溯、抽象、综合归纳演绎,我们的每一个架构师都是服务化架构的接生婆,我们的使命是建立真正反映业务本质并驱动业务不断向前的架构。

我们是否足够深入理解业务的本质,做了足够的归纳演绎以及综合抽象,是否清晰的反应到了我们的服务化的根基:业务模型、域模型以及平台公共语义模型上?这是我们每一个参与服务化的每一个产品、架构师、TL 和核心开发同学需要回答的第一个根本问题。

定义

面向服务的架构(SOA):SOA 是一种架构风格,致力于将业务功能保持一致的服务(系统服务,应用服务,技术服务)作为设计、构建和编排组合业务流程以及解决方案的基本单元。

目的

我们采用 SOA 的架构是为了什么呢?

为了更好的复用?为了更好的责任切分?为了接口和实现的分离,提升灵活性和隔离性?还是为了更好的接口分类和管理?

以上说法其实都没错,但是面向服务化的架构 SOA 的目的远远超过接口技术细节的设计与定义,其核心的关注点在于服务的业务内容以及内涵,而不仅仅是如何设计和实现。

同时,SOA 更多的也不是如何构建一个服务,任何人都可以很容易地创建一个服务,这并不是 SOA 的核心挑战,而是如何赋能企业构建有业务价值意义的完整业务语义的服务集合。

面向服务的架构致力于在企业内的不同的业务环境内,建设业务功能驱动的服务,从而将服务组装成有价值、更高级别的业务流程和解决方案平台。

面向服务的架构的真正的价值体现在当可重用的服务被灵活组合、编排在一起来构建敏捷的、灵活的业务流程,其中敏捷体现在服务可以快速调整,独立演化;灵活性体现在服务由于其业务功能定义明确,边界清晰且功能内聚性强,同时服务具备各自独立完整生命周期,可被灵活组装。

如果面向服务架构能为企业提供了重大的价值,那么这些价值通过什么来体现的呢?

价值体现

  • 行为一致性

面向服务的架构允许我们为业务流程、任务或者决策拥有唯一的共同的入口,也就是,不管服务访问的路径如何,服务给业务提供的业务行为都是一致的。

  • 数据一致性

面向服务的架构允许我们为业务数据信息提供单一的访问入口,也就是它提供给业务一致的、企业内部共识的公用数据访问。

  • 模块化及敏捷性

面向服务的架构 SOA 为业务功能、业务决策和业务信息的模块化提供了非常好的机制。同时,在模块化实现好的情况下,这些模块可以在多个业务流程和场景中被灵活复用和重新组合,从而为业务竞争力和创造性提供灵活性和敏捷度支持。

  • 功能与数据的解耦

面向服务的架构 SOA 提供了业务功能和信息集成的同时,减少了他们之间的依赖和耦合性。也就是,独立的业务功能单元,应用系统,可以一起协同工作,同时各自又具备各自的演进计划,生命周期和业务目标。

  • 高度可管理性

SOA 提供给我们通过定义服务水平协定在服务模块粒度支撑我们的业务目标,我们可以不断的设定、监控和优化调整组件,应用以及系统所承载服务的考核。

其中行为一致性和数据一致性作为服务的核心价值根基。

服务

一、定义

首先我们先定义一下服务是什么?

服务是通过服务契约的方式来提供业务功能的独立单元,同时受服务契约所明确管理。

服务是设计、构建和编排组合一个完整业务实体中业务解决方案的基础单元。服务契约指定了服务消费方和提供方之间所有的交互约定,包括:

  • 服务接口
  • 接口文档
  • 服务策略
  • 服务质量
  • 服务可用性
  • 性能

那我们经常听到模块、组件等其他的软件构件,服务和他们有什么区别呢?其中最核心的区别在于服务本身是被明确管理的,其服务质量和性能是通过服务水平协定(SLA)被明确管理的,而模块以及组件并无此约束。此外,服务的全生命周期包含从设计、部署到增强升级和维护都是可管理的。

举例(下列内容仅做示例展示用,非适用于严格场景):

补货计算服 服务策略 服务质量 性能要求
补货建议量计算服务 针对行业下商家/供应商维度的入仓货品补货建议计算 在销量预测符合分布要求且满足准确率水平要求的情况下,根据缺货率服务水平要求的产生的补货建议量符合业务期望的周转天数 10W + 货品 * 30 仓,品+仓补货及建议量 <= 30min
订单创建服务 包含购物车下单+立即下单场景,满足所有优惠计算后的订单生成 订单创建成功率 99.999999999% 峰值支撑:100w 单/s

二、服务构成

服务自身主要包含两个主要方面,第一方面也是服务最核心的方面就是服务的接口,另外一方面则是服务的实现。服务非常好的实现了接口和实现的分离。

image.png

1)服务接口

服务接口指定了服务的操作,也就是服务是做什么的(What),操作的输入输出参数,以及用来约定如何使用和提供这些能力的协议。

服务通常包含围绕着一个核心的业务功能操作以及相关联的操作。例如补货建议计算服务中核心的操作是生成货品+仓维度的补货建议单,其他相关操作包含查询补货建议单相关销量预测操作,查询补货建议单对应计划库存操作。

服务 核心功能操作 关联操作
补货建议计算服务
品+仓维度补货建议计算
补货建议单对应销量预测查询
补货建议单对应计划库存操作

2)服务实现

服务实现指的是服务如何通过其明确定义的接口提供其能力。服务实现可以通过以下方式实现:

  • 完全基于编码实现
  • 基于其他服务的编排而成
  • 基于已有应用适配封装而成
  • 以上情况混合实现

核心点是服务如何被实现的对于服务消费方来说是透明的,服务消费方仅仅需要关心的是服务是做什么的,而不是如何被实现的。

服务可以提供在保持服务接口或者行为约定不改变的情况下,提供根据不同的行业不同场景提供各种不同的实现。

服务实现在保持服务接口或者行为约定不改变的情况下,可以自由进行升级和切换。服务实现既可以是静态的更新升级,也可以使动态路由实时切换实现,如对应到不同的行业以及不同的业务场景的自动实现切换。

不管服务实现如何升级或者按需自动路由切换,只用服务的行为和契约不会发生改变,用户也就是服务的消费者根本不会感知到任何不同。

我们可以把服务接口想象成室内普通电源国标插口,服务策略为室内非防水情况下适用,服务契约想象成 24X7 的 220v 电压供电能力(其中 180V~250V 50Hz 是质量要求,24x7 稳定性要求,电流供给 <= 10A 是性能要求),此国标插座(服务提供方)可以给包含与此接口匹配且符合契约的任何电器(消费方)交互并提供供电能力,支持其运转。

服务接口定义了交互的的风格和细节,而服务的实现定义了一个特定的服务提供方或者特定的业务实现如何提供其能力。

这种类似连接点/插口的设计极大的方便了更松耦合的业务功能解决方案。

三、服务接口与服务实现的逻辑构成

服务接口与实现的构成也有两个重要的不同方面,分别是执行功能的方法和执行的信息数据。换句话说,一个服务是由一个业务服务操作集合以及对应操作的输入输出的抽象业务服务数据模型组成。这层业务服务数据模型是企业业务层次或者平台业务层次的业务实体的抽象,独立于底层数据存储与实现。此业务数据模型是和各子域密切相关联,但是超越各子域以上的,在完整的业务线或者平台层次上达成一致的业务数据模型,也就是说在各子域之间达成共识且约定的严格明确的公共模型,主要用于平台业务流程中不同域服务的交互,是平台层次统一的业务语言,我把它暂时称为平台业务数据模型。 此平台业务数据模型通常需要包含平台统一语义的业务术语表,平台各域核心实体表,平台各域核心实体交互图等。

image.png

接口与实现的逻辑构成:

1)服务操作

服务操作声明定义了这个操作的输入以及输出参数。

2)平台业务实体模型

平台业务实体模型描述了服务中输入输出数据的结构以及含义。服务接口中的信息和服
务实现中逻辑数据之间的差异是至关重要的。

在服务接口层次上,最重要的是信息必须在业务服务之间进行交互来赋能业务流程并完成业务流程。这些信息必须在参与流程的所有业务服务间达成一致且在服务之间通用,也就是平台层次所有服务公用且标准的业务实体模型,同时此业务实体模型必须在平台业务语义上明确且完成,确保可以支撑平台所有端到端的业务。此平台层级的业务实体模型并不是一蹴而就的,但是可以随着平台的重心变化不断迭代完善成型的。

然而不同的是,从内部来看,很多服务在各自实现的子域内部都有这些信息的不同的超集,可能潜在的存在不同的数据格式。幸运的是,我们不需要感知也不需要在所有关联服务的相关子域实体模型上达成共识,即使不是不可能,但是也不太现实。与之相反,服务接口和服务实现的分离设计允许非常方便的进行平台业务实体模型和服务所在子域领域模型进行映射转换。

3)服务接口最后一个重要的方面就是服务水平协议 SLA。服务水平 SLA 协议指定了服务的的两个重要方面的指标,分别是业务上的指标和技术上的指标:

  • 技术指标:响应时间RT,并发吞吐量 Throughput,可用性 Availability,可靠性 Reliablity。
  • 业务指标:完成的业务功能的质量或者完成度,如产生的补货建议是否满足业务预期的周转缺货KPI要求:周转下降 10 天,缺货率下降5%。

服务化分层架构

理解服务化分层架构,首先要对 TOGAF Meta-Model 有个清晰的理解,从元模型可以看出业务服务和业务流程的上承业务,下启系统平台的核心作用,一定要深刻理解业务服务和业务流程在企业架构中的重要性,下面我把我翻译后画的版本给大家放在这里,给大家做个参考,TOGAF 不多做解释,如有需要,大家可以交流,后面有时间尝试写下我对 TOGAF 的学习和理解。

image.png

通常情况下,我们会按照不同行业的不同的业务流程去搭建系统,如供应链最初在大家电 3W 行业孕育,我们按照 3W 的行业和业务场景搭建了平台商家相适应的计划系统;后续自营行业又根据自己的行业也搭建了自营的计划系统;后续小电数码、国际以及其他业务快速发展,跟随业务快跑的同时,也各自建立的各自的业务流程。在这个过程中,BPM 为建造不同的业务系统提供非常好的抽象支撑,但是经常的结果是,BPM 被用作构建了更高层抽象的,也更高效的,但是却是烟囱式的应用,而不没有更好的贡献更多的支撑到整体上能快速应对业务变化而更灵活,更敏捷的业务平台或者系统。

而这正是面向服务的架构中业务规则以及决策作为服务要发挥更大作用的地方。面向服务的架构允许我们将特定业务流程中的业务规则和业务决策抽象分离出来变成业务规则或者决策服务,这些规则和决策服务就可以被灵活应用到不同的业务流程中,从而这些服务可以被统一管理和演化升级。

BPM + SOA 一起提供了支撑企业架构的完美组合。BPM 提供更高层抽象定义业务流程的能力,以及与流程相关联的重要监控和管理能力;业务服务提供了支撑业务流程的核心的功能、决策以及信息。面向服务的架构则提供能力将服务组合在一起来支撑和创建灵活且敏捷的端到端的企业业务。如果只有 BPM 而没有 SOA 对于创建单独的业务应用或许非常有用,但是通常是创建的烟囱式的应用,很难扩展到企业内或者平台内不同的业务线。如果只有 SOA 而没有BPM虽然可以创建可重用且一致性高的服务,但是缺少将这些服务快速搭建业务流程并支撑端到端业务的能力,也无法支撑建立具有竞争力且可以随着外部竞争环境进行敏捷反应的业务。

下图显示了一个建议的的封层服务化架构图,各分层如下:

  • 端到端业务流程

业务流程是按照一定业务规则决定的顺序执行的业务操作组成。高层级的业务功能,通常跨越应用域或者业务线。通常由行业开发团队开发,此行业开发团队可以具备明确的实现组织结构,也可以由跨团队的相关域共同组成虚线团队。例如,电商业务中,用户选购下单交互流程;供应链业务中的补货调拨计划流程等。

  • 平台业务服务

高度模块化的业务功能单元,由不同类型的子域服务组合编排而来,可作为业务流程的编排单元。跨行业通用的业务服务可由功能所在核心域开发团队编排开发,行业内通用的业务服务可以由行业开发团队负责编排开发。例如,补货审批服务

  • 子域服务

平台各功能子域提供的服务,对平台可见,用于平台业务服务的组合编排,也可以作为更高层的业务流程编排的基础单元。子域服务通常由平台各子域开发团队负责开发。例如,销量计划服务,补货建议计算服务。

  • 子域基础服务

用于支撑各功能子域服务的基础服务,对子域可见,对平台不可见,用于子域服务的编排。

子域基础服务通常由平台各子域开发团队负责开发。例如,入仓决策服务,计划单据服务,计划库存服务等。

  • 基础子域服务

或称为基础业务域服务,提供平台基础业务服务,为各个功能子域或平台业务服务提供基础业务功能及数据服务。例如:商家服务,货品服务,库存服务等。

  • 基础架构服务层

提供不同层次所公用的基础架构服务,如用用户管理,权限管理,操作审计等等。

image.png

我们通常按照上述分层结构来描述平台架构或者企业内部架构,看上去好像层次结构清晰明了,但是却是不完整的,因为此面向服务的架构描述缺失了平台系统架构中一个核心部分,暨信息及信息模型分层,这一点非常之关键,往往会决定架构的成功与否。

为了使架构更完整同时也更真实,我们需要添加对应的完整信息抽象(实体模型 or 领域模型):

  • 核心单据模型

端到端业务流程中操作的核心单据,承载业务核心价值的信息单元模型,例如,销售订单,采购订单,补货计划单等。此模型通常是平台公共语义模型的核心子集。

  • 平台公共语义模型

定义了平台层业务流程、业务服务交互数据。在平台层面或企业层面,端到端业务流程中交互信息的公共语义模型,此模型不仅对平台业务流程中交互的各实体进行了明确的定义,而且包含了业务流程中所需要的完整的业务语义实体,同时各业务语义实体边界明确,责任清晰。核心单据模型通常是平台公共语义模型的子集。平台公共语义模型包含下层子域的对外服务实体子集,按照端到端的完整平台业务语义,可由平台各功能子域模型所共享给平台的核心实体子集有机整合而成,也可由平台业务模型全新定义,或者从 TOP-DOWN 以及 BOTTOM-UP 两个方向共同融合而成。需要注意的是此模型必然是无法一蹴而就,需要经过无数迭代而不断完善,但其一定是不可或缺的。平台的诸多架构决策和不断演化完善需要基于此模型来进行。

  • 子域领域模型

平台各功能子域的领域模型,用于驱动各功能子域的应用系统设计和开发。子域领域模型需要保持动态稳定,通过防腐层同所依赖的外域或者外部服务进行隔离,防止外部服务污染子域内的核心业务语义,同时保持域内业务功能灵活可控。子域领域模型仅通过其对外服务实体子集对外可见,其余对外不可见。

  • 跨域映射模型

用于各子域领域模型实现对外部模型的防腐依赖。

  • 基础架构服务层

提供不同层次所公用的基础架构信息模型,如用户模型,权限模型等。

image.png

信息架构模型框架

现在来讨论下服务化分层架构重视度并不太高的另一个重要侧面:信息架构,之所以说信息架构非常之重要,是因为信息架构与服务化架构是一个密不可分的完整的整体。我对信息架构模型进行了分层划分,下面从 TOP_DOWN 方向来讨论不同的分层模型。

image.png

  • Level 0:战略与决策模型(高层战略视角)

这层次模型用于定义企业的战略方向和商业目的,从而定义了企业内任何系统平台开发的方向和终局。这必然作为企业内任何系统平台开发的基本背景和基调,影响任何系统平台开发项目的中长期目标定义和终局设定。

  • Level 1:商业模式(业务线 owner 视角)

这层模型从业务线 owner 的视角,用运营主体的业务术语描述其商业模式的本质,包括其整体结构,业务流程,以及组织结构等。

  • Level 2:业务抽象概念模型

这层模型从业务架构的视角用信息化的方式对单个业务线或者多个业务线的业务进行抽象。Level 1 描述是对于企业业务来说有意义的东西或者事情,而 Level 2 则给予这些有意义的东西以更严格且清晰的定义,明确其内涵以及外延并体系化,同时根据不同行业线的业务内容进行提取抽象,抽象出共性的内容,用于更高效灵活的描述和定义业务 。

Level 1 描述的是业务运营人员所感知的业务流程,Level 2 不仅描述了这些业务流程,更重要的是抽象并描述了了这些业务流程所应该包含的底层业务功能。

同样的,Level 1 描述对企业业务来讲所有重要的东西,Level 2 描述的是组织想要管理的信息后面最根本的内容。Level 1 描述的事情是 Level 2 定义的基本实体的实际业务中对应的样本或事例。

简而言之,Level 2 是 Level 1 的抽象(Abstraction)与综合(Synthesis)。 为了达成这一视图,必须要仔细分析和归纳,有时候需要演绎的方式来定义出隐藏在企业业务运营主体视图下根本结构和内容。

  • Level 3:平台公共语义模型

Level 3 层公共语义模型同 Level 2 层业务概念模型保持紧密一致,在此基础上增加了服务化视角的语义。Level 3 公共语义模型描述的内容是在必须在平台层业务服务间共享的具有一致语义的业务实体和信息,是平台层一致的共享信息模型。这层模型用于描述平台层服务接口交互的共享信息,基于平台完整业务语义下所有服务所公共数据的标准化视图模型。简而言之,平台公共语义模型,定义了业务平台层次基本业务服务语义,是平台各业务服务之间,平台业务流程和平台业务服务交互的统一语言。

  • Level 4:域模型

Level 4 层域模型定位于平台各子域的领域模型/实体模型,用于对各子域的核心业务功能进行抽象。域模型是平台各子域的标准模型,不仅明确定义的各子域功能服务暨服务接口的语义,同时也包含各子域内服务实现中的关键实体的定义。域模型从整体上来说是平台各子域的私有模型,除了服务语义外整体不对外可视。公共信息中的服务视图是域模型的子集。

域模型核心用于除了用于暴露到平台子域的业务服务设计与实现外,同时也用于驱动域内服务功能的设计和实现。

域模型是需要保持动态稳定的,除非域内业务发生本质变化,域模型应该是相对稳定的。域模型稳定性最大的敌人是外部的依赖,如何不受外部依赖的侵蚀而逐渐腐败,域防腐层存在的最主要原因。子域防腐层维护外部依赖服务和子域模型之间的动态映射,维护域模型的独立性,保护域模型不受有害侵蚀。

域模型我理解基本和我们通常谈的领域模型基本接近,对于各域内业务的抽象,驱动各域技术设计方案设计和实现,至于具体的模型表现形式,采用基于亚里士多德的物质本源的思想(“Material Cause,Formal Cause,Efficient Cause,Final Cause" —> 实体+属性+关系)的ER图,还是基于我们老祖宗老子道家思想("人法地、地法天、天法道、道法自然" —> 实体+行为)的思想的领域驱动 DDD 的方式,个人认为各有伯仲,组中能清楚表达出业务本质即可,后面单独写一篇抽象建模的文章聊一下这两种不同的思想。

  • Level 5:实现模型

此层模型为开发者视角的实现模型,也就是我们系统实现核心的对象模型,是我们系统落地的基石。

设计服务

我们初步了解的什么是服务,以及什么是服务化的分层?那如何设计服务以及服务化架构呢?下面给出基本步骤和方案。

一、理解整体背景

首先,我们要理解服务化架构的整体背景。我们必须理解我们所支撑的业务和业务根本驱动力以及所有的业务流程,业务场景以及业务用例;同时对于平台系统,我们还必须理解公司的战略所赋予平台的使命是什么?我们平台中长期的目标是什么?平台的终局是什么?这些组合和在一起才是服务化架构的完整的上下文背景。这些必须要反映到我们的业务模型、平台公共语义模型和各域模型中去。

然后,我们需要提出并回答如下问题:

  • 我们当前支撑的是什么样的业务?(业务模型)
  • 这个业务或者这些业务的中长期目标和短期目标分别是什么?
  • 平台的短中长期目标是什么?平台的终局是什么?
  • 上述目标是否存在冲突,如何平衡和取舍?
  • 实现这些目标,需要完成什么样的成果?
  • 这些成果如何衡量?
  • 取得这些成果,需要什么样的能力和信息?
  • 实现这些能力需要什么样的流程、服务、实体以及规则
  • 现有的服务、应用或者系统提供了那些基本能力和信息?

前面六个问题描述了整体的架构需求(包括业务和平台),而剩下的问题则描述了整个服务化架构的上下文以及引入了服务目录库的需求。我们服务不能只从单个服务的角度来看,而必须从整个服务集合的角度来反应完整的业务语义和平台语义。我们的服务集合也就是服务目录库必须具备完整的上下文语义,必须能识别出:

  • 整体的上下文背景,包括完整的业务语义和平台语义。
  • 服务职责范围
  • 关联的服务的分组
  • 服务的类型和角色

服务目录库的设计必须支持两个主要的设计时目标:

  • 第一个目标是要提供一种机制来帮助理解服务整体的上下文背景,用于更好的服务选择及更高效的服务重用。特别是,这个服务实现了什么样的责任,以及如何和其他的服务相关联。
  • 第二个目标是要提供一种机制来识别一个特定服务的责任边界,用来指引服务的实现。这是一个非常关键的点,特别是在避免服务的功能和数据重复上非常重要,不仅仅是避免重复建设,更核心的是要以此保证业务功能和数据的一致性。

服务目录库中的服务可以按照服务类型以及服务角色来进行组织。服务类型请参照服务化分层架构内容里的描述;服务角色包含任务服务角色、实体服务角色和决策服务角色,请参照后面小节描述。

二、服务设计原则

面向服务化的架构的其中一个成功的关键是创建一个具备完整业务语义的服务集合以便于可以方便一起进行组合编排来支撑不同的业务流程以及丰富的业务场景。

我们经常谈论各功能域要提供松耦合的服务,是因为服务间的松耦合是非常重要的,特别是通过减少服务间的依赖以便于服务可以在不同的场景中被复用,以及可以起到隔离变更影响的作用。但是如何才能尽可能的实现这个目标呢?

首先我们来看下对于服务最重要点是什么?首先就是这个服务提供了什么样的业务功能,其次这个服务对业务有价值的数据产生了那些影响。从这两个点上我们就可以比较
容易得出两种类型的耦合在服务接口设计中是特别重要的:

  • 数据依赖
  • 功能依赖

举例来说明下:

交易服务协调所有的活动,然后依赖其他服务来帮助完成流程。交易服务依赖于或者说耦合于用户服务,商品服务,库存服务,营销服务、订单服务以及支付服务等。

为啥交易服务没有实现所有的功能?

首先是因为我们想在其他高级别流程或者服务中重用底层的能力。

第二是交易服务服务并不负责用户服务,商品服务,库存服务,营销服务、订单服务以及支付服务。交易服务只是使用它们,而不是负责实现它们。

用户服务被用作管理客户信息访问,它具有唯一的责任来提供、维护和更新客户信息,这样做的目的是为了可以在任何需要访问客户数据服务的地方重用客户服务。比代码重用更重要的是隔离或者是集中式访问客户信息,因为只有唯一的路径访问数据,数据就总是一致的,真正实现 Source Of Truth。因此,尽管有很多服务包含交易服务,购物车,订单历史等服务需要访问客户服务,通过松耦合的这种模式去管理这些依赖是比较容易被理解的。

通过创建服务来执行用户管理,商品管理,库存管理,以及营销管理等,就可以在任何可以用到的地方,执行保持一致性的这些业务功能。

敲黑板:好的服务设计并不仅仅是关注重用性,更重要的是要提供一致性,既包含功能一致性,也包含数据一致性。

那么下一个问题是你如何决定有哪些服务以及这些服务分别是什么呢?同样,你用功能分解和信息隔离组合在一起来决定服务有哪些并且各自是什么?

  • 对线上交易功能的分解引导去识别用户、商品、库存、营销、订单以及支付等相关功能服务。
  • 对信息的隔离引导我们去识别用户和商品等作为交易订单中的共享信息。
  • 面向服务的架构中服务设计的问题需要跨越多个以致于所有的流程中来一起考虑。

因此,服务设计原则基本原则如下:

  • 避免服务间的功能重复
  • 避免服务间的功能缺失
  • 避免数据重复
  • 实现数据的协同访问
  • 具备统一、一致的方式来执行给定的功能

在服务化设计中,如何实现上述的这些原则呢?答案是提出并回答如下问题:

  • 谁负责这个功能?
  • 这个功能在哪里被用到的?
  • 谁负责管理这些指定的数据?
  • 谁负责定义和实现那些特别的业务规则
  • 流程中的哪个步骤具备执行这个任务所需要的特定的知识

这些问题的答案会帮你来识别如下信息:

  • 服务应该做什么?
  • 服务对什么负责?
  • 同样重要的是,识别服务不应该做什么,而应该依赖其他的服务来支撑

三、服务颗粒度与类型

我们通常设计服务时候一个很大的疑惑是我的服务到底要设计成什么样的颗粒度,应该更粗粒度一些,还是更细粒度一些?答案是:没有一个统一正确的服务颗粒度标准。那怎么办?我如何设计我的服务的颗粒度呢?虽然没有统一的标准,但是我们可以依赖下面的因素来决定合适的服务粒度:

  • 谁是服务的潜在消费方?其他服务,业务流程还是外部合作方?
  • 服务在哪里被消费,通过什么样的路径被消费,也就是服务的拓扑结构是什么?
  • 服务的性能要求是什么?
  • 服务预期的业务范围或者边界是什么?

在几乎任何复杂的环境或者系统平台中,我们可以预期到多种多样类型的服务。这些服务具有不同的类型和颗粒度,可以参考服务化分层中的内容,也可以见下面的描述:

  • 端到端的业务流程

业务流程通常跨越整个企业或者平台多个业务域,通常是由底层服务构建而成

  • 平台业务服务

业务服务是最粗粒度的服务,业务服务提供高度抽象的,组合的业务功能给到平台或者企业。业务服务的功能和数据同业务流程所需要的业务语义紧密结合。数据整合服务在这个层次提供端到端的业务流程所需要的整合后的数据。

  • 子域服务

子域服务是中等粒度的,他们提供特别针对于每个业务子域的业务相关服务,被本域内的不同业务服务所使用,但是未必暴露出子域外

  • 子域基础服务

子域基础服务通常是最小粒度的服务,他们提供更低层次的服务,用来提供子域内子域业务功能的基本功能支撑

  • 基础子域服务

子域基础服务通常也提供教小粒度的服务,用于支撑上层业务功能服务的业务功能完整实现。

  • 基础架构服务层

基础架构提供了在更高层级服务构建中细粒度的能力,独立于任何业务域。这些服务需要和业务相关明确区分开来,例如安全认证,权限管理以及纯粹技术编排服务。

四、服务角色

独立于服务的粒度,职责范围以及服务创建以外的另外一个重要考量或者说是侧面是:服务在服务组合或者流程编排中所承担的角色是什么?

那么怎么来区分不同的角色呢?我们使用关注点隔离的架构原则。例如,我们在构建应用中就使用了将数据同逻辑隔离作为重要的概念。这样不仅提供了不同关注点解耦的可能以及机会,而且允许采用不同的方式,在不同的地方来实现这些不同的关注点。

对业务流程进行单独管理的BPM就是一个非常好的例子,BPM作为另外一个关注点分离的例子,将业务流程方案从其他逻辑中分离出来,可以使工作流程可以在一个特定的层次或者环境内进行执行和管理, 这样就可以实现通过快速的建立新的流程模型来快速响应业务的变化。同时面向服务的架构SOA提供了将业务服务作为构建业务流程的基础构件的功能。业务规则系统BRMS同样也作为一个关注点分离的例子,将业务规则或者业务决策从其他应用逻辑中区分开来,这样业务规则和业务决策也可以在一个特定的层次被执行和管理,从而就可以很容易的被变更来支持新的业务需求。这里,业务规则以及决策服务也是面向服务的机构来暴露出规则和决策服务来支撑规则和决策与业务流程的分离。

通常我们通过较粗粒度的来定义三大类服务角色来构建不同的服务层次:

  • 任务服务角色

任务服务通常实现一个完整的业务功能,既可以是基本业务功能,也可以是复杂的业务功能,如计算某个货品在某个仓的补货量,或者一个简单的业务校验,如此货品在此仓是否可补。

此服务类型颗粒度范围较广,包含从独立的子域基础服务到大的平台业务服务都可以具有任务服务角色,更小颗粒度的服务倾向于具有更通用的目的,更大的可重用的潜力。业务服务几乎总是承担任务服务的角色,通常是小颗粒度服务较大的组合,可以被设计成支持一个或者更多特定的流程。因此这些服务通常在跨业务流程中广泛复用的潜力更低。但是也是正常的,因为他们通常是有其他可重用的服务组成的。

通常,具有业务角色的服务是主动服务,通过主动行为来提供价值

  • 实体服务角色

主要管理访问业务实体的服务具有这个角色。业务实体的例子如用户、类目、商品、价格、库存、购物车,主要对应主要的业务信息。实体通常是中到大型实体,倾向于独立于任何特定的业务流程,而可做为多个不同业务流程的组成部分。具有实体服务角色的服务通常通过适配和提供需要的信息来实现任务的方式来支撑任务服务。实体服务通常都具备较大的重用的潜力。

  • 规则 / 决策服务角色

规则 / 决策服务是通过执行业务规则来提供业务决策的服务,如补货计划自动审核服务。

规则 / 决策服务通常用作对复杂问题进行判断或者支持变化频繁的业务规则,如复杂且多变的审核规则等。

规则 / 决策服务通常为小到中等大小颗粒度,通常用来组装成更大的服务。规则/决策服务是可以不同层次不同类型的服务,包括平台业务服务,子域服务,子域基础服务等,但是通常情况下规则/决策服服务也来支撑这些服务类型。

image.png

我们通过组合这些不同类型的服务角色来提供灵活的业务能力,从而用来支持业务流程内的活动。我们提供了一些基本原则来帮助我们进行服务组合以便于帮我们减少依赖,限制耦合以及最大化灵活性。

服务层次以及组合基本原则:

  • 业务流程的任务通过任务服务实现,业务流程路由的核心规则由规则/决策服务来提供,而不是定义在流程网关内。这一块内容后续详细说明。
  • 更高层次的任务为核心的业务服务由其他更小的服务组成
  • 服务依赖严格单向原则,上层服务可以依赖下层次服务以及同一层次服务,但是下层服务不可以依赖上层服务
  • 一个任务服务可以组合规则/决策服务、实体服务以及其他任务服务
  • 但是一个实体服务不允许直接调用其他实体服务

现在我们可以通过丰富的流程,实体和决策服务的集合,可以创建新的不同的服务组合,把规则的灵活可变的好处同服务化架构的模块化,灵活性以及重用性结合起来作为业务系统平台级别的基本架构方式。

服务化如何成功?

一、大规划

大的规划首先要明确 2-3 年内的服务化的目标。大的规划切记事无巨细,而是根据长期规划设定明确的指导性原则和要求,在体系化的基础上鼓励协同和创新。

二、小目标

服务化不应该是运动式的大跃进推进,而应该是坚持试点、推广、总结、扩大试点,从而由点到面,逐步落实的方法,由各域根据规划的体系化要求,再各自情况暨各自成熟度来设定各自服务化目标,制定一个个小目标,快速迭代,敏捷式的总结推进。

三、真共识

建立共识的根本是要讲清楚服务化的目标、架构、设计、开发背后的清楚的逻辑,让每个人想的清楚,听的明白。

四、接地气

接地气同达成共识一样,要用朴素的工程师语言讲清楚目标和逻辑,而不是拿各种看上去非常光鲜亮丽的各种名词来充当台面,讲的人解释不清楚,听得人一头雾水,没有体系化逻辑来支撑落地,最终很难达到服务化真正的目标的。

五、结硬寨

服务化是一个庞大的,迭代的,渐进的体系化工程,不是快闪战,不是突袭战,是场持久战,一定要有曾国藩的“结硬寨,打呆仗”的耐心和准备,踏踏实实落地迭代推进,小步快跑,在坚持体系化思考的基础上进行持续总结改进,通过一个接一个战斗,一个小胜利接一个小胜利,一个战役接一个战役不停的攻城略地的基础上逐渐迈向成功。

六、Think Fast & Slow

一句话,高效的方式就是慢想、快干。我们不一定缺少高执行力的人,但是一定缺少能独立思考并体系化行事的人。

]]>
从零开始入门 K8s | 理解 RuntimeClass 与使用多容器运行时-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 4.7头图.png

作者 | 贾之光  阿里巴巴高级开发工程师

本文整理自《CNCF x Alibaba 云原生技术公开课》第 30 讲,点击直达课程页面
关注“阿里巴巴云原生”公众号,回复关键词“入门”,即可下载从零入门 K8s 系列文章 PPT。

一、RuntimeClass 需求来源

容器运行时的演进过程

我们首先了解一下容器运行时的演进过程,整个过程大致分为三个阶段:

1.png
 

  • 第一个阶段:2014 年 6 月

Kubernetes 正式开源,Docker 是当时唯一的、也是默认的容器运行时;

  • 第二个阶段:Kubernetes v1.3

rkt 合入 Kubernetes 主干,成为了第二个容器运行时。

  • 第三个阶段:Kubernetes v.15

与此同时,越来越多的容器运行时也想接入到 Kubernetes 中。如果还是按 rkt 和 Docker 一样内置支持的话,会给 Kubernetes 的代码维护和质量保障带来严重挑战。

社区也意识到了这一点,所以在 1.5 版本时推出了 CRI,它的全称是 Container Runtime Interface。这样做的好处是:实现了运行时和 Kubernetes 的解耦,社区不必再为各种运行时做适配工作,也不用担心运行时和 Kubernetes 迭代周期不一致所带来的版本维护问题。比较典型的,比如 containerd 中的 cri-plugin 就实现了 CRI、kata-containers、gVisor 这样的容器运行时只需要对接 containerd 就可以了。

随着越来越多的容器运行时的出现,不同的容器运行时也有不同的需求场景,于是就有了多容器运行时的需求。但是,如何来运行多容器运行时还需要解决以下几个问题:

  • 集群里有哪些可用的容器运行时?
  • 如何为 Pod 选择合适的容器运行时?
  • 如何让 Pod 调度到装有指定容器运行时的节点上?
  • 容器运行时在运行容器时会产生有一些业务运行以外的额外开销,这种「额外开销」需要怎么统计?

RuntimeClass 的工作流程

为了解决上述提到的问题,社区推出了 RuntimeClass。它其实在 Kubernetes v1.12 中就已被引入,不过最初是以 CRD 的形式引入的。v1.14 之后,它又作为一种内置集群资源对象 RuntimeClas 被引入进来。v1.16 又在 v1.14 的基础上扩充了 Scheduling 和 Overhead 的能力。

2.png

下面以 v1.16 版本为例,讲解一下 RuntimeClass 的工作流程。如上图所示,左侧是它的工作流程图,右侧是一个 YAML 文件。

YAML 文件包含两个部分:上部分负责创建一个名字叫 runv 的 RuntimeClass 对象,下部分负责创建一个 Pod,该Pod 通过 spec.runtimeClassName 引用了 runv 这个 RuntimeClass。

RuntimeClass 对象中比较核心的是 handler,它表示一个接收创建容器请求的程序,同时也对应一个容器运行时。比如示例中的 Pod 最终会被 runv 容器运行时创建容器;scheduling 决定 Pod 最终会被调度到哪些节点上。

结合左图来说明一下 RuntimeClass 的工作流程:

  1. K8s-master 接收到创建 Pod 的请求;
  2. 方格部分表示三种类型的节点。每个节点上都有 Label 标识当前节点支持的容器运行时,节点内会有一个或多个 handler,每个 handler 对应一种容器运行时。比如第二个方格表示节点内有支持 runc 和 runv 两种容器运行时的 handler;第三个方格表示节点内有支持 runhcs 容器运行时的 handler;
  3. 根据 scheduling.nodeSelector, Pod 最终会调度到中间方格节点上,并最终由 runv handler 来创建 Pod。

二、RuntimeClass 功能介绍

RuntimeClass 的结构体定义

3.png

我们还是以 Kubernetes v1.16 版本中的 RuntimeClass 为例。首先介绍一下 RuntimeClass 的结构体定义。

一个 RuntimeClass 对象代表了一个容器运行时,它的结构体中主要包含 Handler、Overhead、Scheduling 三个字段。

  • 在之前的例子中我们也提到过 Handler,它表示一个接收创建容器请求的程序,同时也对应一个容器运行时;
  • Overhead 是 v1.16 中才引入的一个新的字段,它表示 Pod 中的业务运行所需资源以外的额外开销;
  • 第三个字段Scheduling 也是在 v1.16 中被引入的,该 Scheduling 配置会被自动注入到 Pod 的 nodeSelector 中。

RuntimeClass 资源定义例子

4.png
5.png

在 Pod 中引用 RuntimeClass 的用法非常简单,只要在 runtimeClassName 字段中配置好 RuntimeClass 的名字,就可以把这个 RuntimeClass 引入进来。

Scheduling 结构体的定义

顾名思义,Scheduling 表示调度,但这里的调度不是说 RuntimeClass 对象本身的调度,而是会影响到引用了 RuntimeClass 的 Pod 的调度。

6.png

Scheduling 中包含了两个字段,NodeSelector 和 Tolerations。这两个和 Pod 本身所包含的 NodeSelector 和 Tolerations 是极为相似的。

NodeSelector 代表的是支持该 RuntimeClass 的节点上应该有的 label 列表。一个 Pod 引用了该 RuntimeClass 后,RuntimeClass admission 会把该 label 列表与 Pod 中的 label 列表做一次合并。如果这两个 label 中有冲突的,会被 admission 拒绝。这里的冲突是指它们的 key 相同,但是 value 不相同,这种情况就会被 admission 拒绝。另外需要注意的是,RuntimeClass 并不会自动为 Node 设置 label,需要用户在使用前提前设置好。

Tolerations 表示 RuntimeClass 的容忍列表。一个 Pod 引用该 RuntimeClass 之后,admission 也会把 toleration 列表与 Pod 中的 toleration 列表做一个合并。如果这两处的 Toleration 有相同的容忍配置,就会将其合并成一个。

为什么引入 Pod Overhead?

7.png

上图左边是一个 Docker Pod,右边是一个 Kata Pod。我们知道,Docker Pod 除了传统的 container 容器之外,还有一个 pause 容器,但我们在计算它的容器开销的时候会忽略 pause 容器。对于 Kata Pod,除了 container 容器之外,kata-agent, pause, guest-kernel 这些开销都是没有被统计进来的。像这些开销,多的时候甚至能超过 100MB,这些开销我们是没法忽略的。

这就是我们引入 Pod Overhead 的初衷。它的结构体定义如下:

8.png

它的定义非常简单,只有一个字段 PodFixed。它这里面也是一个映射,它的 key 是一个 ResourceName,value 是一个 Quantity。每一个 Quantity 代表的是一个资源的使用量。因此 PodFixed 就代表了各种资源的占用量,比如 CPU、内存的占用量,都可以通过 PodFixed 进行设置。

Pod Overhead 的使用场景与限制

Pod Overhead 的使用场景主要有三处:

  • Pod 调度

在没有引入 Overhead 之前,只要一个节点的资源可用量大于等于 Pod 的 requests 时,这个 Pod 就可以被调度到这个节点上。引入 Overhead 之后,只有节点的资源可用量大于等于 Overhead 加上 requests 的值时才能被调度上来。

  • ResourceQuota

它是一个 namespace 级别的资源配额。假设我们有这样一个 namespace,它的内存使用量是 1G,我们有一个 requests 等于 500 的 Pod,那么这个 namespace 之下,最多可以调度两个这样的 Pod。而如果我们为这两个 Pod 增添了 200MB 的 Overhead 之后,这个 namespace 下就最多只可调度一个这样的 Pod。

  • Kubelet Pod 驱逐

引入 Overhead 之后,Overhead 就会被统计到节点的已使用资源中,从而增加已使用资源的占比,最终会影响到 Kubelet Pod 的驱逐。

以上是 Pod Overhead 的使用场景。除此之外,Pod Overhead 还有一些使用限制和注意事项:

  • Pod Overhead 最终会永久注入到 Pod 内并且不可手动更改。即便是将 RuntimeClass 删除或者更新,Pod Overhead 依然存在并且有效;
  • Pod Overhead 只能由 RuntimeClass admission 自动注入(至少目前是这样的),不可手动添加或更改。如果这么做,会被拒绝;
  • HPA 和 VPA 是基于容器级别指标数据做聚合,Pod Overhead 不会对它们造成影响。

三、多容器运行时示例

9.png

目前阿里云 ACK 安全沙箱容器已经支持了多容器运行时,我们以上图所示环境为例来说明一下多容器运行时是怎么工作的。

如上图所示有两个 Pod,左侧是一个 runc 的 Pod,对应的 RuntimeClass 是 runc,右侧是一个 runv 的Pod,引用的 RuntimeClass 是 runv。对应的请求已用不同的颜色标识了出来,蓝色的代表是 runc 的,红色的代表是 runv 的。图中下半部分,其中比较核心的部分是 containerd,在 containerd 中可以配置多个容器运行时,最终上面的请求也会到达这里进行请求的转发。

我们先来看一下 runc 的请求,它先到达 kube-apiserver,然后 kube-apiserver 请求转发给 kubelet,最终 kubelet 将请求发至 cri-plugin(它是一个实现了 CRI 的插件),cri-plugin 在 containerd 的配置文件中查询 runc 对应的 Handler,最终查到是通过 Shim API runtime v1 请求 containerd-shim,然后由它创建对应的容器。这是 runc 的流程。

runv 的流程与 runc 的流程类似。也是先将请求到达 kube-apiserver,然后再到达 kubelet,再把请求到达 cri-plugin,cri-plugin 最终还回去匹配 containerd 的配置文件,最终会找到通过 Shim API runtime v2 去创建 containerd-shim-kata-v2,然后由它创建一个 Kata Pod。

下面我们再看一下 containerd 的具体配置。

10.png

containerd 默认放在 [file:///etc/containerd/config.toml]() 这个位置下。比较核心的配置是在 plugins.cri.containerd 目录下。其中 runtimes 的配置都有相同的前缀 plugins.cri.containerd.runtimes,后面有 runc, runv 两种 RuntimeClass。这里面的 runc 和 runv 和前面 RuntimeClass 对象中 Handler 的名字是相对应的。除此之外,还有一个比较特殊的配置 plugins.cri.containerd.runtimes.default_runtime,它的意思是说,如果一个 Pod 没有指定 RuntimeClass,但是被调度到当前节点的话,那么就默认使用 runc 容器运行时。

下面的例子是创建 runc 和 runv 这两个 RuntimeClass 对象,我们可以通过 kubectl get runtimeclass 看到当前所有可用的容器运行时。

11.png

下图从左至右分别是一个 runc 和 runv 的 Pod,比较核心的地方就是在 runtimeClassName 字段中分别引用了 runc 和 runv 的容器运行时。

12.png

最终将 Pod 创建起来之后,我们可以通过 kubectl 命令来查看各个 Pod 容器的运行状态以及 Pod 所使用的容器运行时。我们可以看到现在集群中有两个 Pod:一个是 runc-pod,另一个是 runv-pod,分别引用的是 runc 和 runv 的 RuntimeClass,并且它们的状态都是 Running。

13.png

四、本文总结

本文的主要内容就到此为止了,这里为大家简单总结一下:

  • RuntimeClass 是 Kubernetes 一种内置的集群资源,主要用来解决多个容器运行时混用的问题;
  • RuntimeClass 中配置 Scheduling 可以让 Pod 自动调度到运行了指定容器运行时的节点上。但前提是需要用户提前为这些 Node 设置好 label;
  • RuntimeClass 中配置 Overhead,可以把 Pod 中业务运行所需以外的开销统计进来,让调度、ResourceQuota、Kubelet Pod 驱逐等行为更准确。

4.7尾图.png

活动报名链接:https://yqh.aliyun.com/live/CloudNative

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”

]]>
前端生产方式:过去 10 年回顾和未来 10 年展望-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 作者:卓风

image.png

在写这篇文章之前,我一直在思考该用什么的方式能讲清楚前端为什么要向智能化方向切换的理由,真的反复思考很久,后来决定还是以我做前端的过去 10 年的所见所闻来做个解答吧,这样让大家也都更有些体感。

起源

这段是我跟前端的结缘,想必很多人也跟我一样,懵懵懂懂地就撞入了前端这个行业。

一脚入坑

image.png

我接触前端,那还是 2010 年的时候,在那个时候最火的是 网络三剑客 —— Adobe Dreamweaver、Adobe Flash、Adobe Fireworks。

这三款软件都很热门,第一款可以通过可视化编辑器拖拖拽拽、填填配配就可以搞定一张网页,虽然上手起来概念众多、也挺难用的,但至少是那个时代做网页最牛逼的软件了;

第二款是做 Flash 的,配备一门 ActionScript 的语言,当时网上下载了不少大牛做的很极客的 Flash 网站源码,不过代码读起来很吃力;

第三款是做海报的(因为海报图比较大、比较长,切割起来比较耗费内存,这款软件速度比较快)和 Gif 动画的,但我用的少,大部分时间都用 Photoshop CS4 来搞定。

虽说这三款软件最火,但真正让我入坑前端(那个时候还没有“前端”这个称呼,有的就是“切图仔”)的理由,是因为我想当一位网页设计师。

当时,想当一位网页设计师的理由有二:

  1. 软件工程搞 Java、C++、C 真是挺枯燥无聊的,写一段程序,还得编译、部署,等上个两三分钟的,特别无语;而当初接触 Web 页面开发时(当时还是一位外教授课),发现网页这东西很神奇,在一个 Text 文本编辑器里敲上几行代码,改个扩展名,双击页面就展示出来了,这种所见即所得的美的视觉冲击力,当时让我向这个方向上蠢蠢欲动,埋下了祸根]]> Python实现urllib3和requests库使用 | python爬虫实战之五-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 python爬虫AJAX数据爬取和HTTPS访问 | python爬虫实战之四

    urllib3库

    https://urllib3.readthedocs.io/en/latest/
    标准库urllib缺少了一些关键的功能, 非标准库的第三方库urllib3提供了, 比如说连接池管理。

    安装

    $ pip install urllib3

    之后,我们来借用之前的json数据来看一下:

    import urllib3
    from urllib.parse import urlencode
    from urllib3.response import HTTPResponse
    
    jurl = 'https://movie.douban.com/j/search_subjects'
    
    d = {
        'type':'movie',
        'tag':'热门',
        'page_limit':10,
        'page_start':10
    }
    
    with urllib3.PoolManager as http:
      #  http.urlopen()
         response = http.request('GET', '{}?{}'.format(jurl, urlencode(d)), headers={
        'User-agent': "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36"
        })
        print(type(response))
        # response:HTTPResponse = HTTPResponse()
        print(response.status)
        print(response.data)

    执行结果:

    image.png

    image.png

    这个封装的属性和方法还是比较原始的,我们对于这样的使用肯定是不行的,那我们需要用什么呢?接着来讲requests库。

    requests库

    requests使用了urllib3, 但是API更加友好, 推荐使用。
    需要先安装,跟之前一样。
    安装:

    $ pip install requests

    我们对上面的例子做出修改:

    import urllib3
    from urllib.parse import urlencode
    from urllib3.response import HTTPResponse
    
    import requests
    
    jurl = 'https://movie.douban.com/j/search_subjects'
    
    d = {
        'type':'movie',
        'tag':'热门',
        'page_limit':10,
        'page_start':10
    }
    
    
    url = '{}?{}'.format(jurl, urlencode(d))
    
    response = requests.request('GET', url, headers = {
        'User-agent': "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36"
    })
    
    
    with response:
        print(response.text)
        print(response.status_code)
        print(response.url)
        print(response.headers)
        print(response.request)

    执行结果:

    image.png

    我们具体来看一下request:

        print(response.headers, '~~~~~')
        print(response.request.headers)

    上面的headers是response的,下面的是请求的headers
    执行结果:

    image.png

    里面还有别的参数,大家可以去尝试一下。

    image.png

    requests默认使用Session对象, 是为了在多次和服务器端交互中保留会话的信息, 例如cookie。

    直接使用Session:

    image.png
    image.png

    我们也来尝试去打印一下这些信息:

    import requests
    
    urls = ['https://www.baidu.com/s?wd=magedu', 'https://www.baidu.com/s?wd=magedu']
    session = request.session()
    with session:
        for url in urls:
            response = session.get(url, headers = {
            'User-agent': "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36"
            })
        
            with response:
                print(response.text[:50])
                print('-'*30)
                print(response.cookies)
                print('-'*30)
                print(response.headers, '~~~~~')
                print(response.request.headers)

    执行结果:

    image.png
    image.png
    image.png

    通过结果可以看出,Session对象对cookie起了作用。观察第一次返回的cookie与第二次发起请求的response.request.headers的cookie。返回的结果依然是键值对,只是之中value的值依然是用键值对来表示的。

    配套视频课程,点击这里查看

    获取更多资源请订阅Python学习站

    ]]>
    Ubuntu 快速更换阿里源-阿里云开发者社区 Wed, 08 Apr 2020 03:03:10 +0800 70.jpeg
    镜像下载、域名解析、时间同步请点击 阿里巴巴开源镜像站

    一、查看ubuntu的Codename

    lsb_release -a | grep Codename | awk '{print $2}' # 输出结果为下文中的Codename

    1.png

    二、备份系统源

    cd /etc/apt
    sudo mv sources.list sources.list.bak

    三、写入阿里云的源

    vi sources.list

    下面源信息中$Codename为第一步中系统的Codename,用记事本批量替换即可。

    deb http://mirrors.aliyun.com/ubuntu/ $Codename main multiverse restricted universe
    deb http://mirrors.aliyun.com/ubuntu/ $Codename-backports main multiverse restricted universe
    deb http://mirrors.aliyun.com/ubuntu/ $Codename-proposed main multiverse restricted universe
    deb http://mirrors.aliyun.com/ubuntu/ $Codename-security main multiverse restricted universe
    deb http://mirrors.aliyun.com/ubuntu/ $Codename-updates main multiverse restricted universe
    deb-src http://mirrors.aliyun.com/ubuntu/ $Codename main multiverse restricted universe
    deb-src http://mirrors.aliyun.com/ubuntu/ $Codename-backports main multiverse restricted universe
    deb-src http://mirrors.aliyun.com/ubuntu/ $Codename-proposed main multiverse restricted universe
    deb-src http://mirrors.aliyun.com/ubuntu/ $Codename-security main multiverse restricted universe
    deb-src http://mirrors.aliyun.com/ubuntu/ $Codename-updates main multiverse restricted universe

    四、执行更新

    执行以下命令,完成更新。

    apt-get update

    说明:可以通过http://mirrors.aliyun.com/ubuntu/dists/查看是否支持当前系统的Codename。

    阿里巴巴开源镜像站 提供全面,高效和稳定的系统镜像、应用软件下载、域名解析和时间同步服务。”

    ]]>
    声学传感器能诊断机器的健康状况吗? Wed, 08 Apr 2020 03:03:10 +0800 09.27.18-Can-Acoustic-Sensors-be-Deterministic-of-Machine-Health-1068x656_副本.jpg

    随着工业4.0的到来,制造商正在使用各种类型的传感器来收集有关资产健康状况的信息。这些指标为预测分析流程提供了信息,例如工作单生成和预测潜在的机器停机时间。

    在将制造商的传感器数据集成到IoT平台中时,必须考虑部署的传感器类型范围。 一些最杰出的传感器可测量温度,电压,振动,电和湿度。本文提出一个问题:声传感器能否有效诊断机器健康?

    predict_副本.png

    我们经常根据是否能听到噪音来诊断机器的问题。人类只能听到20至22000赫兹的声音。然而,超出人类听觉范围的声音也会对机器健康产生有价值的见解。

    光或超声波传感器问题

    由于机器由相互磨削的运动部件组成,从而导致摩擦和噪音,因此可以通过声学方法检测到许多机器故障。由于可见光无法穿过资产组件,因此无法确定任何关键问题,因此此类机器无法使用可见光等其他手段。

    超声波可以检测到轻微的声音,但它价格昂贵,并且需要在机器周围移动接收器和发射器(类似于医院中的超声波机器),因此超声波并不理想。

    另外,工业操作员不喜欢侵入性解决方案。声传感器允许无创设置,并且对工作空间的侵入最小。

    声传感器可以诊断机器健康吗

    结合预测算法,非侵入性声传感器可以在机器故障之前很久就检测到微弱的噪音。 可以进行频率分析来分析我们在没有传感器的情况下可能听到的轻微声像差的发生率。

    实时检测声音的另一种方法是声相机,它拾取声波并以热成像方式(即在热图像中)可视化声波。 然后可以对这些信息进行算法分析,以确定故障的根本原因。 例如,在动力传输系统中,此类摄像机可以确定异常声音的特定点,并将其用于预测组件故障的早期阶段。在运送空气或液体的加压管道系统中,此类摄像机可以检测出维修人员视线之外的确切泄漏点。

    可以通过将多个传感器放置在机器中的目标点并将它们连接到无线边缘设备来收集此声学信息,该无线边缘设备直接将数据传输并将其上传到云服务器,在此可以对其进行分析。结合资产管理系统和预测分析,可以提供对关键资产效率参数的详细了解。

    机械可持续性的声学诊断

    根据一些研究,高达40%的工厂能源成本可能是由漏气引起的。当电动机开始退化时,机器的整体效率会降低。为了弥补效率的降低,电动机会消耗更多的能量。这会导致额外的电力消耗和更高的电费。

    利用超人声波传感器发现机械缺陷,制造商可以在这种破坏性循环开始前修复机器,在延长机器寿命的同时减少停机时间和电费。这可以为运营部门节省大量资金。例如,我们已经看到一些公司减少了10%的电力消耗。结合预测分析的途径,这些好处可以进一步提高。

    考虑到上述所有优点,声学传感器和摄像机可以成为进行预测分析的强大工具。

    原文链接

    ]]>
    为什么我们需要边缘计算? Wed, 08 Apr 2020 03:03:11 +0800 当时间敏感事件发生时,边缘计算胜过云处理。
    08.16.18-Why-do-we-need-edge-computing-1068x656_副本.jpg

    在过去的15到20年间,已经从本地软件向云计算发生了巨大转变。现在,我们可以从任何地方访问所需的一切,而不受固定位置服务器的限制。但是,云计算运动即将向分散计算的另一方向倾斜。那么为什么我们需要边缘计算呢?

    考虑到云网络带来的巨大机遇,这一概念似乎有悖常理。但为了让我们充分利用物联网(IoT)所提供的一切优势,技术必须再次成为本地技术。

    看一下农业历史可以得出一些相似之处。一个世纪或更早以前,人们食用当地种植的食物。如果它不在您居住的地方的50-100英里内生长或繁殖,那么您可能将没有机会食用它。

    然后,技术出现并打开了新的大门。运输变得更快了,制冷意味着食物可以在不损坏的情况下运输,并且新的耕作技术允许大量生产。随着这些发展,消费者可以从世界各地获得食物。

    我们仍在利用容易获得的全球食品的优势,但是由于多种原因,人们已经转向了本地食物。长途运输食品会影响环境。消费者希望为当地经济做出贡献。我们中的许多人都希望我们食用的食物中的人造成分更少。

    那么这对云计算意味着什么呢? 就像全球获得食物一样,云计算并没有完全消失。但是,定期进行处理的地方将从云端转移到现在所谓的“边缘”。

    什么是边缘计算

    如果我们回想对云的了解,就可以将其与本地计算进行比较。本地计算意味着在公司大型机或服务器上集中存储和管理数据。可以说,云计算可转换为一系列“远程”服务器上的数据存储和处理。

    因此,如果云计算发生在远程服务器上,则边缘计算的发生位置更接近其记录的动作。边缘计算包括收集数据的传感器(例如RFID标签),现场数据中心以及将它们全部连接起来以支持本地计算的网络。数据处理发生在远离云的源头或“边缘”。边缘计算网络在必要时仍可以连接到云,但是它们不需要云也可以正常运行。

    您可能需要一个Nest Thermostat来控制您家里的气候,一个FitBit来衡量您的个人健康状况,甚至可能是Alexa或Google Home作为个人助手。但是对于这些设备,没有任何紧急事件需要解决。您可以等待对Alexa的请求由云处理。

    当时间敏感事件发生时,边缘计算胜过云处理。为了使无人驾驶汽车成为现实,这些汽车需要实时对外部因素做出反应。如果自动驾驶汽车在道路上行驶,并且有行人从汽车前走出来,则汽车必须立即停车。它没有时间将信号发送到云端然后等待响应,它必须能够立即处理信号。

    边缘计算的好处是什么

    显然,速度是使用边缘计算的重要因素,并且有很多解决速度的用例。工厂可以使用边缘计算通过检测人体来大幅度减少工作中受伤的发生率。TSA检查站可以收集通过不同闸门而来的化学物质数据,这些数据可以组合起来制造炸弹。在出现问题之前,城市可以使用边缘计算来解决道路和交叉路口的维护问题。

    另一大好处是流程优化。如果自动驾驶汽车、工厂和TSA检查站使用云而不是edge,它们将把收集到的所有数据推送到云上。但是,如果edge做出本地决策,云可能不会立即需要所有这些数据,甚至根本不需要。

    借助边缘计算,数据中心可以执行对时间敏感的规则(例如“停车”),然后在带宽需求不那么高时将数据分批流式传输到云中。然后,云可以花时间从边缘分析数据,并发送建议的规则更改,例如“当汽车在50英尺内感觉到人类活动时,缓慢减速”。

    除了速度和优化之外,减少停机也是使用边缘计算的主要原因。通过将所有内容推送到云端,您可以使企业不受ISP故障和云服务器停机的影响。今天,许多关键任务操作(如铁路和化工厂)甚至都不会使用云。拥有自己的服务器是保证正常运行的唯一方法。

    边缘计算依赖于单个传感器与本地数据中心之间的连接,从而大大减少了停机的机会。

    边缘计算的下一步是什么

    即使具有提高速度、优化和减少停机等好处,采用边缘计算仍将需要一些关键的工作。毕竟,看看云的采用到底花了多长时间!但是随着时间的流逝,企业将学习边缘计算如何在减少常见风险因素的同时加快运营速度。

    原文链接

    ]]>
    混合现实如何将虚拟医院带给你 Wed, 08 Apr 2020 03:03:11 +0800 2-1068x656_副本.jpg

    医疗保健领域正在不断革新。尽管如此,医生和其他医疗专业人员在治疗病人时仍面临许多挑战。微软HoloLens技术的进步带来了医院、诊所和其他现代医疗机构如何为病人提供医疗服务的巨大转变。

    医疗保健的主要挑战

    许多医院和医疗诊所仍然依靠传统技术与患者互动,阅读纸质图表了解患者的健康状况。在许多情况下,神经外科医生在对病人进行CT扫描时面临困难。这是因为CT扫描由于噪音和机器的封闭模式而导致一些病人患幽闭恐惧症。此外,医生需要医疗扫描和记录来进行详细分析,但加载和修改成千上万病人的电子病历是一个庞大的过程。

    由专业人员组成的医疗团队由接待员,护士,医生和其他专业人员组成,他们在患者的治疗期间需要彼此之间的无缝协调。此外,依靠多个设备进行通信,调出诊断图和其他数据来使他们成为一个团队来工作也变得很困难。

    混合现实Mixed Reality在医疗行业创造奇迹

    Microsoft HoloLens的新版本已发布了可以在很大程度上影响医疗保健行业的前沿功能。我们来看看这些功能增强医疗专业人员改善医患关系的方式。

    患者数据可视化

    MR耳机(MR headsets)可以检测患者并立即向医生提供相关的医疗信息,从而节省了互动过程中的时间,并使医生对突发事件的响应比以前更快。仅需观察患者的生命体征而无需阅读屏幕或获取文书工作,就可以节省很多宝贵的时间,并使医患之间的交流更加方便。

    此外,使用MR可以使老年患者在家中接受医院级的护理和治疗,使他们感到更舒适,而医院也可以为其他危重患者提供更多病床。

    HoloLens应用程序还可以节省时间和金钱,同时仍为患者提供个性化的护理,从而为因经常与医生约会而需要频繁出行的患者提供帮助。这使医疗机构可以将节省的时间用于其他危重患者。

    通常,混合现实应用程序可以通过全息技术与医疗专业人员进行实时交互。它还允许护理人员免提共享信息,并通过虚拟仪表板实时记录患者数据。这一进步结合了现实生活、视频会议和投影全息图,可帮助护士和临床医生在需要时访问信息和服务。

    全息手术计划

    MR应用程序为医生提供了虚拟手术情报(VSI),向患者展示他们自己的MRI扫描图像,并以视觉格式解释手术过程中的并发症程度。不仅如此,MR应用程序还有助于减少医生的响应时间并提高手术准确性,从而改善患者体验。例如,如果患者需要进行复杂的手术,则医生可以借助混合现实的VSI功能向患者显示诊断图像。 此功能可帮助患者和医生共享相同的视野(FOV)。通过这种方式,医生可以讨论,计划和启动他们的治疗程序,从而减少住院治疗的响应时间。

    现场/远程手术协助

    此外,通过佩戴HoloLens,外科医生可以腾出双手来进行手术,并且可以使用麦克风和传感器与世界各地的其他外科医生进行通讯,从而实现无缝协作。所有这些功能(包括模拟和信息提取)使混合现实成为改善手术性能的宝贵资产。

    小结

    这些只是Microsoft HoloLens的一些显着优势,证明了医疗保健的未来在很大程度上依赖于混合现实技术。这些进步使医疗专业人员可以磨练自己的技能,甚至不动手就可以照顾数百名患者。此外,使用MR应用程序可以实现物理对象与数字对象之间的无缝协作,从而提供更好的治疗质量和患者体验。

    原文链接

    ]]>
    达摩院悬壶,看医疗 AI 如何济世 Wed, 08 Apr 2020 03:03:11 +0800

    --------点击屏幕右侧或者屏幕底部“+订阅”,关注我,随时分享机器智能最新行业动态及技术干货----------

    5.png

    阿里达摩院,并不专为抗疫而生。

    甚至连马云都可能预料不到,这个机构这么快就展现出及时的作用。

    抗疫正当时,不论是与浙江疾控中心合作的基因检测平台,将疑似病例基因分析时间缩至半小时,还是率先在郑州“小汤山”应用的 CT 影像系统,达摩院、扫地僧,都成为阿里 AI 抗疫的代表性标签。

    而且影响力还在几何级扩散,据阿里统计,截止到 3 月 31 日,达摩院 CT 影像 AI 已在浙江、河南、湖北、上海、广东、江苏、安徽等 16 个省市近 170 家医院落地,已诊断 34 万临床病例。

    随着疫情全球扩散,这个出自阿里达摩院、代表中国 AI 技术前沿的抗疫系统,也在进一步承担起“One world,One Fight”的作用。

    但也别神话了达摩院和背后的扫地僧——外界只看见了他们的动能速度,而不知道他们之前为此积累的势能和高度。

    这把抗疫宝剑,之前已磨砺整整 4 年。

    “扫地僧”带队,达摩院如何 4 年磨一剑?

    6.png

    阿里巴巴集团副总裁、达摩院高级研究员华先胜

    达摩院医疗 AI,源自 2016 年,由时任阿里巴巴 iDST 副院长的华先胜带队打造。

    对于华先胜,外界不陌生。

    他出自黄冈中学,2001 年北大数学系博士毕业,之前在微软工作 14 年,直到被老领导“阿里云之父”王坚挖角,开启阿里在 AI 视觉研发和落地方面的探索。

    但即便已是 2016 年,人工智能如何在医疗领域发挥作用并不太清晰,只是华先胜坚信:人工智能进入医疗健康领域,是一个必然的事情。

    他回忆说:“医疗健康领域存在的不平衡问题比较多(医患供需不平衡,医保供需不平衡,医患知识不平衡等),AI 应该能为全民健康的问题发挥重要作用。”

    7.png

    正是这样的预判,才逐渐演变出了抗疫期间的达摩院医疗 AI。

    当时也是医疗 AI 的爆发点,大量创业者与资本玩家涌入。目前发展势头不错的推想、体素等,都在 2016 年创立。

    一直深耕视觉领域的依图,也在同年发布肺癌影像智能诊断系统,开辟了医疗业务线。

    作为视觉智能领域深耕多年的专家,华先胜团队从肺部 CT 影像开始切入医疗 AI,很快就做出成绩。

    2017 年 7 月,达摩院正式成立前夕,他们交出了一份“贺礼”——在国际权威的肺结节检测大赛 LUNA16 上打破世界纪录,凭借 89.7% 的平均召回率(在样本数据中成功发现结节占比的比例)夺冠。

    8.png

    华先胜说,这项工作“当时只道是寻常”,没想到直接为阿里达摩院新冠肺炎的 CT 自动诊断系统打下了基础。

    那次破纪录之后,达摩院医疗 AI 的研究范围进一步扩大,在肝结节、心血管、骨科、病理等方面均取得了进展。

    其中代表性的成果有:

    2018 年 12 月,达摩院 AI 从近百支队伍中脱颖而出,在全球 LiTS(Liver Tumor Segmentation Challenge,肝脏肿瘤病灶区 CT 图像分割挑战)获得两项第一。

    2019 年的 EMNLP BB task 关系抽取世界大赛,达摩院 AI 获得了第一名。同年,按照鹿特丹(Rotterdam)国际比赛标准,达摩院 AI 的全自动心脏冠脉中心线提取超越了现有的业界最好成绩,相关论文被国际顶级医学影像会议 MICCAI 2019 提前接收。

    技术持续突破下, 达摩院医疗 AI 团队在 Nature 子刊、CVPR 等顶尖学术期刊与会议上,发表了不少的论文,加上国际、国内专利,超过了百篇,也给达摩院医疗 AI 系统在临床诊断和医学研究上大规模应用,提供了支撑。

    如果这样回顾,自然又是一个“大牛带队”、“顺风顺水”的传奇往事。

    但华先胜说,各种辛苦,做过才知。医疗 AI,不是一个单兵作战就能搞定的领域。

    达摩院医疗 AI 一路走过来,这些技术突破、业务落地背后,坑踩得一个都不少。

    医疗 AI,只有技术可不够

    大道理其实也简单:想要打造好的 AI 系统,高质量的数据集是关键。

    然而对于医疗 AI 来说,这从来不是一个简单的事情。

    医疗影像数据质量参差不齐,标准化程度低、人工标注难度大、数据敏感度高等行业性难题,导致 AI 在医学上的学习和应用面临诸多挑战,从而难倒了诸多英雄好汉,甚至还是一些巨头公司的短板。

    比如此前名震江湖的 IBM 沃森医疗系统,宣称超过人类医生的存在,在 2018 年被美国健康医疗媒体 STAT 曝光,其 AI 系统训练数据量最高的肺癌只有 635 例,而最低的卵巢癌只有 106 例,引起哗然一片。

    而且数据难题之外,还有更现实的挑战:医疗机构不买账。

    9.png

    据 2018 年 9 月中国信息通信研究院、Gartner 联合发布的《2018 世界人工智能产业蓝皮书》,在中国,医疗健康领域的 AI 企业在所有 AI 企业中占比达到了 22%,在所有垂直行业中占比最高。

    但在医疗 AI 行业火热的同时,客户并不怎么感兴趣。

    《财经》杂志在 2019 年 3 月份的报道中就指出,资本捧场,使产品同质化严重,送进医院、无人使用的 AI 医疗产品不在少数。AI 逐渐演变为医疗领域的嵌入品,锦上添花的功效有,雪中送炭的本事无。

    作为行业中的一份子,达摩院医疗 AI,绕不开,无法回避,唯有迎难而上解决问题。

    扫地僧如何破局?

    华先胜说,如今回顾,大道至简:“以技术平台为轴心,联合产业伙伴打天下”。

    对于阿里来说,这不是一个陌生的模式,不论是淘宝、支付宝,还是现在的阿里云、平头哥,本质上都是以技术建立平台,拥抱行业玩家,来推动产业发展。

    医疗AI领域,阿里的优势还更甚,它旗下还有一个在香港上市的公司阿里健康,已经在医药电商及新零售、互联网医疗、消费医疗、智慧医疗等领域深耕多年。

    在阿里健康以及众多医疗行业合作伙伴,比如万里云、卫宁健康、古珀科技等的支持下,达摩院医疗 AI 在高质量数据集上不断训练优化,通过阿里云推进技术在医疗行业落地的速度,在行业中都处于前列。

    所以才有了当前抗疫中的达摩院医疗 AI。

    危急关头,如何一步步见真章?

    速度,速度,还是速度。

    华先胜说,疫情大面积爆发之初,达摩院医疗 AI 团队就放弃假期行动起来。

    连点成线,覆盖疫情咨询、药物研发、病毒基因分析、临床诊断等多个环节。

    2020 年 1 月 27 日,达摩院连夜研发的智能疫情机器人。上线后便在全国各地投入使用,很快落地全国 27 个省、直辖市、自治区,免费为 57 座城市拨打 1600 万通防控摸排电话,摸排超过 20 万身体异常人群。

    2 天后,阿里云宣布向全球公共科研机构免费开放一切 AI 算力,以加速本次新型肺炎新药和疫苗研发。

    钟南山团队、全球健康药物研发中心 GHDDI、北京大学、晶泰科技等机构,都成为受益者。

    紧接着 2 月 1 日,达摩院医疗 AI 算法,正式应用于新冠肺炎的病原学检测。

    达摩院与浙江省疾控中心合作,利用算法将疑似病例基因分析时间缩至半小时,该技术可以避免核酸检测出现的漏检情况,同时可以及时检测到变异病毒。

    华先胜解释:“它每天都在工作,准确率近乎 100%,正进一步推广到更多地方使用。”

    而且 ,达摩院也在进一步优化算法,将时间缩短到了 10 分钟,进一步提高效率,并在疫区压力最大的武汉金银潭医院上线。

    2 月 15 日,达摩院医疗 AI 团队与合作伙伴一起,基于 5000 CT 影像样本数据,快速研发出了 CT 影像算法,在郑州小汤山上线,可以在 20 秒内对新冠疑似患者 CT 影像做出判读,并量化病症的轻重程度,分析结果准确率达到 96%。

    10.png

    至今这套 AI 系统已在浙江、河南、湖北、上海、广东、江苏、安徽等 16 个省市的 170 家医院落地,诊断超过 34 万临床病例。

    这还不够,为了全方位抗疫。达摩院医疗 AI 团队还提供了医疗专业翻译系统、疫情预测等系统,来为更大范围、更高层次的抗疫,提供信息支撑。

    同时,疫情全球化蔓延的情况下,达摩院医疗 AI 随着阿里云一同出海。

    很快,日本知名医疗科技机构 JBC 正式上线阿里云新冠肺炎 AI 诊断技术,开始向日本医院提供服务,帮助医生通过 CT 影像快速进行新冠肺炎筛查。

    11.png

    并且更多的欧洲国家也在跟进。在华先胜的透露中,先后有 30 多个国家和地区,希望达摩院通过阿里云提供医疗 AI 支持。

    当然,达摩院并非这次全球科技抗疫中的全部。

    疫情之下,全球医疗 AI 的多数玩家,都参与到了抗疫之中。比如国内的依图、推想等公司都推出了相应产品,以自身之长提供解决方案。

    国外如谷歌,在美国疫情爆发之时,同样投入了 1700 名程序员,联合专攻“医疗科技”的兄弟公司 Verily,打造新冠病毒检测网站。

    “扫地僧”华先胜对此怎么看?

    他很开心,认为这次疫情对于整个行业来说,是一次大练兵。

    “很多竞争对手都成了并肩作战的伙伴。(抗疫)也让我们与合作伙伴的协同变得更加深入。“

    而且更重要的是,经此一疫,医疗 AI 的社会定位、落地推广和作用发挥,也得到意想不到的市场教育和检验。

    扫地僧要做一个怎样的医疗 AI?

    答案很简单:近期帮助人类医生的 AI,远期帮助大众对健康有更强的把控能力的 AI。

    众所周知,抗疫过程中,医生人手缺少的局面一直存在。

    2 月 5 日,国家卫健委公布的诊疗方案第五版中,正式将 CT 影像临床诊断结果作为新冠肺炎病例判断的标准之一。

    虽然这直接加快了新冠肺炎疑似病例的确诊速度与准确度,但对于前线医生来说,却是不小的负担。

    一位新冠肺炎病人的 CT 影像大概在 300 张左右,每诊断一个病例,影像医生需要投入大约为 5-15 分钟时间。一名医生每天连续不间断工作 12 个小时,只能诊断大概 72 个病例。

    2 月 4 日晚,全国一共有疑似病例 23260 例,追踪到(新冠肺炎患者)密切接触者 252154 人……提升临床诊断效率,成为抗疫期间核心需求之一。

    12.png

    而达摩院等医疗 AI 机构,就在这样的险峻形势中出手,联合伙伴打造的新冠病毒 CT 影响诊断系统,从上传数据到得到结果,诊断一个病例平均仅需 20 秒,计算时间最快仅 2 秒。

    虽然仍旧需要医生进一步配合才能够得到更准确诊断结果,但对效率的提升, 无疑是巨大的。这也是医疗 AI 能够在抗疫期间得到推广的原因之一。

    因此,华先胜认为,这也会成为医疗系统的常态:AI 助攻,人类医生提效。

    据新华网 2018 年 12 月份报道,中国医学影像数据的年增长率约为 30%,而放射科医师数量的年增长率约为 4.1%,而且需求缺口不断加大。

    对于医疗 AI 行业来说, 这是其进一步发展的机会。

    “医疗 AI 的价值在抗疫中得到验证,会对医疗行业和公众产生深远的影响。在接下来几年,将会看到整个医疗行业的数字化和智能化程度大幅度提升。”华先胜说。

    达摩院医疗 AI,早已行动起来了。

    阿里达摩院透露,他们打造的医疗 AI 系统,已经落地了 170 家医院,在北京、上海、广州、杭州、武汉、郑州等一线城市之外,也部署到了二三线城市——往往都是医疗资源相对匮乏的地方。

    华先胜解释,二三线城市未来对医疗健康AI的需求应该会更为迫切,用 AI 提升诊断效率和水准在医疗资源缺乏的区域,是当前和未来医疗健康 AI 落地的一个较大的场景。

    13.png

    然而,要将医疗 AI 应用到更多的场景,进入寻常生活,还有很长的路要走。

    不论是获取更多的医疗数据,还是寻找更加切合的商业模式,以及政策法规的支持等,都是医疗 AI 行业需要解决的问题。

    但突发疫情也让其前景变得更明朗。

    正所谓“上医治未病,中医治欲病,下医治已病”。现在,医疗 AI 正在疾病治疗中发挥作用。

    但华先胜认为,接下来医疗 AI 将会从医生走向大众,从高成本走向普惠,从应用于医疗走向应用于健康。

    这是阿里健康体系、达摩院医疗 AI 等将要重点发力的方向。

    阿里范式,阿里打法,要再一次在医疗健康领域复刻——成为基础设施,成为医疗健康行业的数字经济基础设施。

    它们将长在抗疫号角之下,为不再需要抗疫而生。

    在耳熟能详的武侠江湖中,达摩院代表了武学武德的最高成就,扫地僧更是大隐于寺的大神象征,并且总能在危难关头挺身而出。

    从这个角度来说,倒与马云和阿里的达摩院创立初心,都有呼应。

    而且也给新时代的扫地僧、科研人员,工程师们,提供了更大舞台和机会。

    侠之大者,为国为民。

    image.png

    原文链接:https://yqh.aliyun.com/detail/8253

    ]]>
    CCP OAuth2.0 隐藏式授权实践 Wed, 08 Apr 2020 03:03:11 +0800

    内容协作平台(Content Collaboration Platform, 后面简称CCP)是为开发者提供的面向企业、个人数据管理、内容识别、协作的开放平台。CCP 提供多种OAuth2.0协议接口,方便其他第三方应用接入。

    本文主要讲解纯前端应用(例如SPA, Chrome插件等)接入CCP所使用的OAuth2.0隐藏式授权的场景和实践方案。

    什么是 OAuth2.0? 可以看阮老师的文章: OAuth 2.0 的一个简单解释

    OAuth2.0 隐藏式: 有些纯前端 Web 应用,没有后端,无法采用 OAuth2.0 授权码模式,因为将 client_secret 放在前端是很危险的行为。OAuth2.0 隐藏式(implicit) 即是授权服务直接向前端Web应用发令牌。

    1. CCP 使用介绍

    只需3步配置,即可拥有一个云盘系统。

    (1) 创建域

    用户可以在 阿里云CCP官网控制台,创建一个域(domain),假设domainID为 hz01, CCP会分配1个 API 子域名: https://hz01.api.alicloudccp.com

    image.png

    (2) 配置OAuth登录页面

    配置好域的用户体系配置:具体的配置方法请看这里

    image.png

    CCP会分配1个认证授权服务子域名: https://hz01.auth.alicloudccp.com

    (3) 直接开通官方提供的BasicUI云盘应用

    image.png

    允许访问后,再以超级管理员登入一次激活,即可开通成功。

    image.png

    BasicUI 提供1个子域名: https://hz01.apps.alicloudccp.com。您的用户可以通过此子域名访问云盘系统了。

    更多BasicUI的介绍请看:Basic UI简介

    2. CCP OAuth2.0 隐藏式授权流程

    image.png

    (1) 授权请求

    用户浏览CCP的云盘应用https://hz01.apps.alicloudccp.com,想要用一个第三方应用在线markdown编辑器打开 a.md 这个文件。

    • 这个编辑器是一个纯前端应用,假设域名为 https://a.com
    • 编辑器提供 redirect_uri 为: https://a.com/callback.html
    • 我们可以构造下授权请求url:
    https://hz01.auth.alicloudccp.com/v2/oauth/authorize?
    response_type=token&
    client_id=xxx&
    redirect_uri=CALLBACK_URL&
    scope=FILE.ALL
    • 其中 CALLBACK_URL 为 encodeURIComponent('https://a.com/callback.html')
    • client_id 为markdown编辑器的appId(可以在CCP官网控制台创建应用获得,请先看应用概述)。

    (2) 用户认证和授权

    浏览器请求这个url,会跳转到登录页面,用户登录确认后,会重定向到CALLBACK_URL且通过hash返回access_token等信息,如: https://a.com/callback.html#access_token=xxxxx&expire_in=3600&token_type=Bearer

    image.png

    (3) callback

    编辑器的callback.html页面,解析location的hash。

    • access_token等参数解析出来,保存到本地存储中。
    • callback.html 需要清空历史记录,因为access_token是在url中的,会保留在历史记录里。

    (4) 调用CCP API

    编辑器就可以通过 access_token 来操作 a.md 文件了。

    3. 使用 OAuth Widget

    CCP 官方提供了一些拥有特定功能的 Widget, 供第三方应用接入时使用。详情请看Widget 介绍

    OAuth Widget即是将上面的OAuth2.0 隐藏式授权逻辑封装起来,做成一个可重用的组件。

    下面介绍此widget的用法:

    (1) 引入js

    <button id="btn_1">登录</button>
    <script src="/media/202004071552407Ub.js"></script>

    (2) 点击按钮,即可弹出登录窗口

    window.onload = function () {
      document.getElementById('btn_1').onclick = async function () {
        var tokenInfo = await CCPWidgets.oAuthLogin({
            domain_id: '<Your Domain ID>',
            client_id: '<Your App ID>' 
        })
        //用户登录授权后,即可拿到tokenInfo
        console.log(tokenInfo)
      }
    }

    (3) 弹出登录框效果

    image.png

    参考:JS Widget 授权原理和调用的API

    ]]>
    前端生产方式:过去 10 年回顾和未来 10 年展望 Wed, 08 Apr 2020 03:03:11 +0800 作者:卓风

    image.png

    在写这篇文章之前,我一直在思考该用什么的方式能讲清楚前端为什么要向智能化方向切换的理由,真的反复思考很久,后来决定还是以我做前端的过去 10 年的所见所闻来做个解答吧,这样让大家也都更有些体感。

    起源

    这段是我跟前端的结缘,想必很多人也跟我一样,懵懵懂懂地就撞入了前端这个行业。

    一脚入坑

    image.png

    我接触前端,那还是 2010 年的时候,在那个时候最火的是 网络三剑客 —— Adobe Dreamweaver、Adobe Flash、Adobe Fireworks。

    这三款软件都很热门,第一款可以通过可视化编辑器拖拖拽拽、填填配配就可以搞定一张网页,虽然上手起来概念众多、也挺难用的,但至少是那个时代做网页最牛逼的软件了;

    第二款是做 Flash 的,配备一门 ActionScript 的语言,当时网上下载了不少大牛做的很极客的 Flash 网站源码,不过代码读起来很吃力;

    第三款是做海报的(因为海报图比较大、比较长,切割起来比较耗费内存,这款软件速度比较快)和 Gif 动画的,但我用的少,大部分时间都用 Photoshop CS4 来搞定。

    虽说这三款软件最火,但真正让我入坑前端(那个时候还没有“前端”这个称呼,有的就是“切图仔”)的理由,是因为我想当一位网页设计师。

    当时,想当一位网页设计师的理由有二:

    1. 软件工程搞 Java、C++、C 真是挺枯燥无聊的,写一段程序,还得编译、部署,等上个两三分钟的,特别无语;而当初接触 Web 页面开发时(当时还是一位外教授课),发现网页这东西很神奇,在一个 Text 文本编辑器里敲上几行代码,改个扩展名,双击页面就展示出来了,这种所见即所得的美的视觉冲击力,当时让我向这个方向上蠢蠢欲动,埋下了祸根]]> Python实现urllib3和requests库使用 | python爬虫实战之五 Wed, 08 Apr 2020 03:03:11 +0800 python爬虫AJAX数据爬取和HTTPS访问 | python爬虫实战之四

      urllib3库

      https://urllib3.readthedocs.io/en/latest/
      标准库urllib缺少了一些关键的功能, 非标准库的第三方库urllib3提供了, 比如说连接池管理。

      安装

      $ pip install urllib3

      之后,我们来借用之前的json数据来看一下:

      import urllib3
      from urllib.parse import urlencode
      from urllib3.response import HTTPResponse
      
      jurl = 'https://movie.douban.com/j/search_subjects'
      
      d = {
          'type':'movie',
          'tag':'热门',
          'page_limit':10,
          'page_start':10
      }
      
      with urllib3.PoolManager as http:
        #  http.urlopen()
           response = http.request('GET', '{}?{}'.format(jurl, urlencode(d)), headers={
          'User-agent': "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36"
          })
          print(type(response))
          # response:HTTPResponse = HTTPResponse()
          print(response.status)
          print(response.data)

      执行结果:

      image.png

      image.png

      这个封装的属性和方法还是比较原始的,我们对于这样的使用肯定是不行的,那我们需要用什么呢?接着来讲requests库。

      requests库

      requests使用了urllib3, 但是API更加友好, 推荐使用。
      需要先安装,跟之前一样。
      安装:

      $ pip install requests

      我们对上面的例子做出修改:

      import urllib3
      from urllib.parse import urlencode
      from urllib3.response import HTTPResponse
      
      import requests
      
      jurl = 'https://movie.douban.com/j/search_subjects'
      
      d = {
          'type':'movie',
          'tag':'热门',
          'page_limit':10,
          'page_start':10
      }
      
      
      url = '{}?{}'.format(jurl, urlencode(d))
      
      response = requests.request('GET', url, headers = {
          'User-agent': "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36"
      })
      
      
      with response:
          print(response.text)
          print(response.status_code)
          print(response.url)
          print(response.headers)
          print(response.request)

      执行结果:

      image.png

      我们具体来看一下request:

          print(response.headers, '~~~~~')
          print(response.request.headers)

      上面的headers是response的,下面的是请求的headers
      执行结果:

      image.png

      里面还有别的参数,大家可以去尝试一下。

      image.png

      requests默认使用Session对象, 是为了在多次和服务器端交互中保留会话的信息, 例如cookie。

      直接使用Session:

      image.png
      image.png

      我们也来尝试去打印一下这些信息:

      import requests
      
      urls = ['https://www.baidu.com/s?wd=magedu', 'https://www.baidu.com/s?wd=magedu']
      session = request.session()
      with session:
          for url in urls:
              response = session.get(url, headers = {
              'User-agent': "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36"
              })
          
              with response:
                  print(response.text[:50])
                  print('-'*30)
                  print(response.cookies)
                  print('-'*30)
                  print(response.headers, '~~~~~')
                  print(response.request.headers)

      执行结果:

      image.png
      image.png
      image.png

      通过结果可以看出,Session对象对cookie起了作用。观察第一次返回的cookie与第二次发起请求的response.request.headers的cookie。返回的结果依然是键值对,只是之中value的值依然是用键值对来表示的。

      配套视频课程,点击这里查看

      获取更多资源请订阅Python学习站

      ]]>
      MNS服务批量删除问题 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      操作步骤

      1、ak,sk参数获取:阿里云常见参数获取位置

      2、endpoint参数获取
      image.png

      3、pom.xml

      <dependency>
               <groupId>com.aliyun.mns</groupId>
               <artifactId>aliyun-sdk-mns</artifactId>
               <version>1.1.8</version>
               <classifier>jar-with-dependencies</classifier>
           </dependency>

      4、Code Sample

      import com.aliyun.mns.client.CloudAccount;
      import com.aliyun.mns.client.MNSClient;
      import com.aliyun.mns.model.PagingListResult;
      import com.aliyun.mns.model.QueueMeta;
      import com.aliyun.mns.model.TopicMeta;
      import java.util.List;
      
      public class DeleteQueueOrTopicDemo {
      
          public static void main(String[] args) {
      
              //初始参数设置 ak,sk信息及对应区域所在的地址
              String accessKeyId = "********";
              String accessKeySecret = "********";
              String endpoint = "http://********.mns.cn-********.aliyuncs.com/";
      
              CloudAccount account = new CloudAccount(accessKeyId, accessKeySecret, endpoint);
              MNSClient client = account.getMNSClient();
      
              String prefix = ""; //全部删除设置
              String marker = "";
              Integer retNumber = 10000;
      //        deleteQueues(client, prefix, marker, retNumber); // 删除队列
              deleteTopics(client, prefix, marker, retNumber);  // 删除主题
      
              client.close();
          }
      
          /***
           * delete queue
           * @param client client对象
           * @param prefix 队列名称前缀,如果是批量全部删除,直接使用 "" 即可
           * @param marker 列举的起始位置,""表示从第一个开始,也可以是前一次列举返回的marker
           * @param retNumber 最多返回的个数
           */
          public static void deleteQueues(MNSClient client,String prefix,String marker,Integer retNumber)
          {
              PagingListResult<QueueMeta> listResult = client.listQueue(prefix, marker, retNumber);
      
              System.out.println(listResult.getResult().size());
              List<QueueMeta> queues = listResult.getResult();
      
              //循环遍历删除Queue
              int i = 0;
      
              System.out.println(queues.size());
              if(queues.size() != 0) {
      
                  for (QueueMeta queue : queues) {
                      //删除消息队列
                      String queueName = queue.getQueueName();
                      client.getQueueRef(queueName).delete();
                      i++;
                      System.out.println("Delete queue " + queueName);
                  }
              }else{
                  System.out.println("没有满足筛选条件的队列");
              }
      
              System.out.println("The number of deleted queues is" + i);
          }
      
          /**
           * delete topic
           * @param client client对象
           * @param prefix 主题名称前缀,如果是批量全部删除,直接使用 "" 即可
           * @param marker 列举的起始位置,""表示从第一个开始,也可以是前一次列举返回的marker
           * @param retNumber 最多返回的个数
           */
          public static void deleteTopics(MNSClient client,String prefix,String marker,Integer retNumber)
          {
              PagingListResult<TopicMeta> listResult = client.listTopic(prefix, marker, retNumber);
      
              List<TopicMeta> topics = listResult.getResult();
      
              //循环遍历删除Queue
              int i = 0;
      
              if (topics.size() != 0) {
                  //循环遍历删除Topic
                  for (TopicMeta topic : topics) {
                      //删除消息Topic
                      String topicName = topic.getTopicName();
                      client.getTopicRef(topicName).delete();
                      System.out.println("Delete Topic " + topicName);
                      i++;
                  }
              }
              else{
                  System.out.println("没有满足筛选条件的主题");
              }
              System.out.println("The number of deleted topics is" + i);
          }
      }

      参考链接
      MNS Java SDK下载

      ]]>
      RabbitMQ消息队列学习笔记 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述

      初次使用AMQP的过程中,总是容易被AMQP支持的消息模型绕晕,这里结合官方的教程,对AMQP的消息模型做一个简要总结,供参考。目前官方给出了六种消息发送/接收模型,这里主要介绍前五种消息模型。

      消息模型

      1、Hello World

      简单模式就是生产者将消息发送到队列、消费者从队列中获取消息。一条消息对应一个消费者。

      image.png

      示例代码说明:

      测试使用的是阿里云的AMQP消息队列服务,具体的代码配置过程可以参考阿里云官方链接

      工具类

      import AMQP.AliyunCredentialsProvider;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      
      public class ConnectionUtil {
      
        public static Connection getConnection() throws Exception{
            // 初始化参数设置
            String AccessKey= "********";
            String SecretKey = "********";
            Long Uid = ********16617278L;
            String VhostName = "********";
            String host = "********16617278.mq-amqp.cn-hangzhou-a.aliyuncs.com";
      
            // 定义连接工厂
            ConnectionFactory connectionFactory = new ConnectionFactory();
            // 设置服务地址
            connectionFactory.setHost(host);
            // 端口
            connectionFactory.setPort(5672);
            // 设置用户名、密码、vhost
            connectionFactory.setCredentialsProvider(new AliyunCredentialsProvider(AccessKey,SecretKey,Uid));
            connectionFactory.setAutomaticRecoveryEnabled(true);
            connectionFactory.setNetworkRecoveryInterval(5000);
            connectionFactory.setVirtualHost(VhostName);
      
            // 通过工厂获取连接对象
            Connection connection = connectionFactory.newConnection();
            return connection;
        }
      }

      发送端示例代码

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      
      // hello world 单个消费者和接收者
      public class Send {
      
          private final static String Queue_name = "helloDemo";
          public static void main(String[] args) throws Exception {
              // 获取连接及mq通道
              Connection connection = ConnectionUtil.getConnection();
              Channel channel = connection.createChannel();
      
              channel.queueDeclare(Queue_name,false,false,false,null);
              //消息内容
              String message = "Hello World!";
              // 1、交换机,此处无   2、发送到那个队列 3、属性  4、消息内容
              channel.basicPublish("",Queue_name,null,message.getBytes());
      
              System.out.println("发送数据:" + message);
      
              // 关闭连接
              channel.close();
              connection.close();
          }
      }

      消费端示例代码

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.*;
      import java.io.IOException;
      
      public class Receiver {
          private final static String Queue_name = "helloDemo";
          public static void main(String[] args) throws Exception{
              Connection connection = ConnectionUtil.getConnection();
              final Channel channel =  connection.createChannel();
              
              // 开始消费消息
              channel.basicConsume(Queue_name, false, "ConsumerTag", new DefaultConsumer(channel) {
                  @Override
                  public void handleDelivery(String consumerTag, Envelope envelope,
                                             AMQP.BasicProperties properties, byte[] body)
                          throws IOException {
                      //接收到的消息,进行业务逻辑处理
                      System.out.println("message receive: ");
                      System.out.println("Received: " + new String(body, "UTF-8") + ", deliveryTag: " + envelope.getDeliveryTag());
                      channel.basicAck(envelope.getDeliveryTag(), false);
                  }
              });
              Thread.sleep(100000);
              channel.close();
              connection.close();
          }
      }

      2、Work Queues

      一条消息可以被多个消费者尝试接收,最终只有一个消费者能够获取到消息。

      image.png

      发送端示例代码

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      
      // 1:N  消费者各自接收消息
      public class Sender {
      
          private final static String queueName = "workQueue";
          public static void main(String[] args) throws Exception {
              Connection connection = ConnectionUtil.getConnection();
              Channel channel = connection.createChannel();
      
              // 声明队列
              channel.queueDeclare(queueName,false,false,false,null);
              for (int i = 0; i < 100; i++) {
      
                  String message = "workqueues message " + i;
                  channel.basicPublish("",queueName,null,message.getBytes());
                  System.out.println("发送消息: " + message);
      
                  Thread.sleep(10);//休眠
              }
              // 关闭连接
              channel.close();
              connection.close();
          }
      }

      消费端示例代码1

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.*;
      import java.io.IOException;
      
      public class Receiver1 {
      
          private final static String queueName = "workQueue";
          public static void main(String[] args) throws Exception{
      
              Connection connection = ConnectionUtil.getConnection();
              final Channel channel = connection.createChannel();
              channel.queueDeclare(queueName,false,false,false,null);
              channel.basicQos(1);//告诉服务器,在没有确认当前消息完成之前,不要给我发新的消息。
      
              DefaultConsumer consumer = new DefaultConsumer(channel){
                  public void handleDelivery(String consumerTag, Envelope envelope,
                                             AMQP.BasicProperties properties, byte[] body)
                          throws IOException {
                      //接收到的消息,进行业务逻辑处理
                      System.out.println("message receive1: ");
                      System.out.println("Received1: " + new String(body, "UTF-8") + ", deliveryTag: " + envelope.getDeliveryTag());
                      try {
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      channel.basicAck(envelope.getDeliveryTag(), false);// 参数2 false为确认收到消息, true为拒绝收到消息
                  }
              };
              channel.basicConsume(queueName,false,consumer);// 参数2 手动确认,代表我们收到消息后需要手动确认告诉服务器我们收到消息了
          }
      }

      消费端示例代码2

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.*;
      import java.io.IOException;
      
      public class Receiver2 {
      
          private final static String queueName = "workQueue";
          public static void main(String[] args) throws Exception{
      
              Connection connection = ConnectionUtil.getConnection();
              final Channel channel = connection.createChannel();
              channel.queueDeclare(queueName,false,false,false,null);
              channel.basicQos(1);//告诉服务器,在没有确认当前消息完成之前,不要给我发新的消息。
      
              DefaultConsumer consumer = new DefaultConsumer(channel){
                  public void handleDelivery(String consumerTag, Envelope envelope,
                                             AMQP.BasicProperties properties, byte[] body)
                          throws IOException {
                      //接收到的消息,进行业务逻辑处理
                      System.out.println("message receive2: ");
                      System.out.println("Received2: " + new String(body, "UTF-8") + ", deliveryTag: " + envelope.getDeliveryTag());
                      try {
                          Thread.sleep(10);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      channel.basicAck(envelope.getDeliveryTag(), false);// 参数2 false为确认收到消息, true为拒绝收到消息
                  }
              };
              channel.basicConsume(queueName,false,consumer);// 参数2 手动确认,代表我们收到消息后需要手动确认告诉服务器我们收到消息了
          }
      }

      3、Publish/Subscribe

      一条消息可以被多个消费者同时获取,生产者将消息发送给交换机,消费者将自己对应的队列注册到交换机,当发送消息后,所有注册的队列的消费者都可以收到消息。

      image.png

      发送端示例代码

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      
      public class Sender {
      
          private static String Exchange_Name = "ExchangeDemo";//声明交换机
          public static void main(String[] args) throws Exception {
              Connection connection = ConnectionUtil.getConnection();
              Channel channel = connection.createChannel();
      
              // 声明exchange
              // Producer 将消息发送到 Exchange ,由 Exchange 将消息路由到一个或多个 Queue 中(或者丢弃),Exchange 按照相应的 Binding 逻辑将消息路由到 Queue。
              channel.exchangeDeclare(Exchange_Name,"fanout");
              String message = "Exchange message demo";
      
              // 消息发送端交换机,如果此时没有队列绑定,则消息会丢失,因为交换机没有存储消息的能力
              channel.basicPublish(Exchange_Name,"",null,message.getBytes());
              System.out.println("发送消息: " + message);
          }
      }

      消费端示例代码1

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.*;
      import java.io.IOException;
      
      public class Sub1 {
          private static String Exchange_Name = "ExchangeDemo";//声明交换机
          public static void main(String[] args) throws Exception{
              Connection connection = ConnectionUtil.getConnection();
              final Channel channel = connection.createChannel();
      
              channel.queueDeclare("testqueue1",false,false,false,null);
              // 绑定到交换机
              channel.queueBind("testqueue1",Exchange_Name,"");
              channel.basicQos(1);
      
              DefaultConsumer consumer = new DefaultConsumer(channel){
                  @Override
                  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException{
      
                      System.out.println("sub1: " + new String(body));
                      channel.basicAck(envelope.getDeliveryTag(),false);
                  }
              };
              channel.basicConsume("testqueue1",false,consumer);
          }
      }

      消费端示例代码2

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.*;
      import java.io.IOException;
      
      public class Sub2 {
          private static String Exchange_Name = "ExchangeDemo";//声明交换机
      
          public static void main(String[] args) throws Exception{
              Connection connection = ConnectionUtil.getConnection();
              final Channel channel = connection.createChannel();
              channel.queueDeclare("testqueue2",false,false,false,null);
              // 绑定到交换机
              channel.queueBind("testqueue2",Exchange_Name,"");
              channel.basicQos(1);
      
              DefaultConsumer consumer = new DefaultConsumer(channel){
                  @Override
                  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException{
                      System.out.println("sub2: " + new String(body));
                      channel.basicAck(envelope.getDeliveryTag(),false);
                  }
              };
              channel.basicConsume("testqueue2",false,consumer);
          }
      }

      4、Routing

      生产者将消息发送到type为direct模式的交换机,消费者的队列将自己绑定到路由的时候给自己绑定一个key,只有生产者发送的消息key和绑定的key一致时,消费者才能收到对应的消息。

      image.png

      发送端示例代码

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      
      public class Sender {
          private static final String ExchangeName = "Rout_Change";//路由消息交换机
      
          public static void main(String[] args) throws Exception {
              Connection connection = ConnectionUtil.getConnection();
              Channel channel = connection.createChannel();
              channel.exchangeDeclare(ExchangeName,"direct");
              channel.basicPublish(ExchangeName,"key3",null,"route 消息".getBytes());
      
              channel.close();
              connection.close();
          }
      }

      消费端示例代码

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.*;
      import java.io.IOException;
      
      public class Sub {
          private static final String ExchangeName = "Rout_Change";//路由消息交换机
          public static void main(String[] args) throws Exception{
      
              Connection connection = ConnectionUtil.getConnection();
              final Channel channel = connection.createChannel();
      
              // 声明队列
              channel.queueDeclare("testroutequeue1",false,false,false,null);
      
              // 绑定交换机
              // 参数3 标记 绑定到交换机的时候会有一个标记,只有和它一样标记的消息才会别消费到
              channel.queueBind("testroutequeue1",ExchangeName,"key1");
              channel.queueBind("testroutequeue1",ExchangeName,"key2");//绑定多个标记
              channel.basicQos(1);
      
              DefaultConsumer consumer = new DefaultConsumer(channel){
                  public void handleDelivery(String consumerTag, Envelope envelope,
                                             AMQP.BasicProperties properties, byte[] body)
                          throws IOException {
                      //接收到的消息,进行业务逻辑处理
                      System.out.println("message route receive1: ");
                      System.out.println("Received1: " + new String(body, "UTF-8") + ", deliveryTag: " + envelope.getDeliveryTag());
                      try {
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      channel.basicAck(envelope.getDeliveryTag(), false);// 参数2 false为确认收到消息, true为拒绝收到消息
                  }
              };
              channel.basicConsume("testroutequeue1",false,consumer);// 参数2 手动确认,代表我们收到消息后需要手动确认告诉服务器我们收到消息了
          }
      }

      5、Topics

      该类型与 Direct 类型相似,只是规则没有那么严格,可以模糊匹配和多条件匹配,即该类型 Exchange 使用 Routing key 模式匹配和字符串比较的方式将消息路由至绑定的 Queue。

      示例:

      Routing key 为 use.stock 的消息会转发给绑定匹配模式为 .stock, use.stock, . 和 #.use.stock.# 的 Queue; 表是匹配一个任意词组,# 表示匹配 0 个或多个词组。

      image.png

      发送端示例代码

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      
      public class Sender {
          private static String Exchange_Name = "Exchange_Topic";
          public static void main(String[] args) throws Exception{
      
              Connection connection = ConnectionUtil.getConnection();
              Channel channel = connection.createChannel();
      
              // 声明exchange类型为Topic 也就是通配符模式
              channel.exchangeDeclare(Exchange_Name,"topic");
              channel.basicPublish(Exchange_Name,"abc.1.2",null,"Topic 模式消息".getBytes());
      
              // 关闭通道和连接
              channel.close();
              connection.close();
          }
      }

      接收端示例代码

      import AMQP.RabbitMQTutorials.ConnectionUtil;
      import com.rabbitmq.client.*;
      import java.io.IOException;
      
      public class Sub {
          private static String ExchangeName = "Exchange_Topic";
          public static void main(String[] args) throws Exception{
              Connection connection = ConnectionUtil.getConnection();
              final Channel channel = connection.createChannel();
      
              // 声明队列
              channel.queueDeclare("topicqueue",false,false,false,null);
              // 绑定交换机
              // 参数3 标记 绑定到交换机的时候会有一个标记,只有和它一样标记的消息才会别消费到
              channel.queueBind("topicqueue",ExchangeName,"key.*");
              channel.queueBind("topicqueue",ExchangeName,"abc.#");//绑定多个标记
              channel.basicQos(1);
      
              DefaultConsumer consumer = new DefaultConsumer(channel){
                  public void handleDelivery(String consumerTag, Envelope envelope,
                                             AMQP.BasicProperties properties, byte[] body)
                          throws IOException {
                      //接收到的消息,进行业务逻辑处理
                      System.out.println("message route receive1: ");
                      System.out.println("Received1: " + new String(body, "UTF-8") + ", deliveryTag: " + envelope.getDeliveryTag());
                      try {
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      channel.basicAck(envelope.getDeliveryTag(), false);// 参数2 false为确认收到消息, true为拒绝收到消息
                  }
              };
              channel.basicConsume("topicqueue",false,consumer);// 参数2 手动确认,代表我们收到消息后需要手动确认告诉服务器我们收到消息了
          }
      }

      参考链接
      RabbitMQ Tutorials

      Rabbitmq企业级消息队列视频课程

      ]]>
      阿里云MNS Queue Rest API操作示例 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      签名验证工具

      如果在使用过程中因为签名问题一直无法通过,建议直接使用签名验证工具进行快速的测试验证。

      image.png

      Code Sample

      创建队列

      import org.apache.commons.codec.binary.Base64;
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpResponse;
      import org.apache.http.client.HttpClient;
      import org.apache.http.client.methods.HttpPut;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.impl.client.HttpClients;
      import org.apache.http.util.EntityUtils;
      import org.apache.http.entity.StringEntity;
      
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import java.io.UnsupportedEncodingException;
      import java.math.BigInteger;
      import java.net.URI;
      import java.security.InvalidKeyException;
      import java.security.MessageDigest;
      import java.security.NoSuchAlgorithmException;
      import java.text.DateFormat;
      import java.text.SimpleDateFormat;
      import java.util.Date;
      import java.util.Locale;
      import java.util.TimeZone;
      
      public class CreateMNSQueue {
      
          public static void main(String[] args) {
              //参数设置
              String AccessKeySecret = "********";//Access Key Secret
              String AccessKeyId = "********";//AccessKey ID
              String AccountId = "********";//Account ID
      
              //获取GMT英文格式时间
              Date d=new Date();
              DateFormat format=new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z",Locale.ENGLISH);//英文格式的时间转换时需要带上Locale.ENGLISH,否则会转换失败,因为它默认的是本地化的设置,除非你的操作系统是英文的,总之时间转换时需要时间格式与模式保持一致。
              format.setTimeZone(TimeZone.getTimeZone("GMT"));
              String date = format.format(d);
      
              String body = "<?xml version="1.0" encoding="UTF-8"  ?>n" +
                      "    <Queue xmlns="http://mns.aliyuncs.com/doc/v1/">n" +
                      "    <VisibilityTimeout >60</VisibilityTimeout>n" +
                      "    <MaximumMessageSize>1024</MaximumMessageSize>n" +
                      "    <MessageRetentionPeriod>120</MessageRetentionPeriod>n" +
                      "    <DelaySeconds>0</DelaySeconds>n" +
                      "    </Queue>";
      
              //构造签名String
              String SignString = "PUTn" +
                      "n"+
                      "application/xml"+
                      "n"+
                      date + "n"+
                      "x-mns-version:2015-06-06" +"n"+
                      "/queues/TestQueue123";
      
              String sign = encode(AccessKeySecret,SignString);
              String Authorization = "MNS " + AccessKeyId + ":" + sign;
              HttpClient httpclient = HttpClients.createDefault();
      
              try
              {
                  URIBuilder builder = new URIBuilder("http://" + AccountId + ".mns.cn-hangzhou.aliyuncs.com/queues/TestQueue123");//在杭州区域
                  URI uri = builder.build();
                  HttpPut request = new HttpPut(uri);
                  request.setHeader("Authorization", Authorization);
                  request.setHeader("Date", date);
                  request.setHeader("Host", AccountId + ".mns.cn-hangzhou.aliyuncs.com");
                  request.setHeader("Content-Type","application/xml");
                  request.setHeader("x-mns-version","2015-06-06");
      
                  // Request body
                  StringEntity reqEntity = new StringEntity(body,"UTF-8");
                  request.setEntity(reqEntity);
      
                  HttpResponse response = httpclient.execute(request);
                  HttpEntity entity = response.getEntity();
      
                  if (entity != null)
                  {
                      System.out.println(EntityUtils.toString(entity));
                  }
              }
              catch (Exception e)
              {
                  System.out.println("error");
                  System.out.println(e.getMessage());
              }
          }
      
          //写一个md5加密的方法
          public static String md5(String plainText) {
              //定义一个字节数组
              byte[] secretBytes = null;
              try {
                  // 生成一个MD5加密计算摘要
                  MessageDigest md = MessageDigest.getInstance("MD5");
                  //对字符串进行加密
                  md.update(plainText.getBytes());
                  //获得加密后的数据
                  secretBytes = md.digest();
              } catch (NoSuchAlgorithmException e) {
                  throw new RuntimeException("没有md5这个算法!");
              }
              //将加密后的数据转换为16进制数字
              String md5code = new BigInteger(1, secretBytes).toString(16);// 16进制数字
              // 如果生成数字未满32位,需要前面补0
              for (int i = 0; i < 32 - md5code.length(); i++) {
                  md5code = "0" + md5code;
              }
              return md5code;
          }
      
          //计算签名
          public static String encode(String accessKey, String data) {
              try {
                  byte[] keyBytes = accessKey.getBytes("UTF-8");
                  byte[] dataBytes = data.getBytes("UTF-8");
                  Mac mac = Mac.getInstance("HmacSHA1");
                  mac.init(new SecretKeySpec(keyBytes, "HmacSHA1"));
                  return new String(Base64.encodeBase64(mac.doFinal(dataBytes)));
              } catch (UnsupportedEncodingException var5) {
                  throw new RuntimeException("Not supported encoding method UTF-8", var5);
              } catch (NoSuchAlgorithmException var6) {
                  throw new RuntimeException("Not supported signature method hmac-sha1", var6);
              } catch (InvalidKeyException var7) {
                  throw new RuntimeException("Failed to calculate the signature", var7);
              }
          }
      }

      删除队列

      import org.apache.commons.codec.binary.Base64;
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpResponse;
      import org.apache.http.client.HttpClient;
      import org.apache.http.client.methods.HttpDelete;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.impl.client.HttpClients;
      import org.apache.http.util.EntityUtils;
      
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import java.io.UnsupportedEncodingException;
      import java.math.BigInteger;
      import java.net.URI;
      import java.security.InvalidKeyException;
      import java.security.MessageDigest;
      import java.security.NoSuchAlgorithmException;
      import java.text.DateFormat;
      import java.text.SimpleDateFormat;
      import java.util.Date;
      import java.util.Locale;
      import java.util.TimeZone;
      
      public class DeleteMNSQueue {
      
          public static void main(String[] args) {
              //参数设置
              String AccessKeySecret = "********";//Access Key Secret
              String AccessKeyId = "********";//AccessKey ID
              String AccountId = "********";
      
              //获取GMT英文格式时间
              Date d=new Date();
              DateFormat format=new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z",Locale.ENGLISH);//英文格式的时间转换时需要带上Locale.ENGLISH,否则会转换失败,因为它默认的是本地化的设置,除非你的操作系统是英文的,总之时间转换时需要时间格式与模式保持一致。
              format.setTimeZone(TimeZone.getTimeZone("GMT"));
              String date = format.format(d);
      
              //构造签名String
              String SignString = "DELETEn" +
                      "n"+"n"+
                      date + "n"+
                      "x-mns-version:2015-06-06" +"n"+
                      "/queues/TestQueue123";
      
              String sign = encode(AccessKeySecret,SignString);
              String Authorization = "MNS " + AccessKeyId + ":" + sign;
              HttpClient httpclient = HttpClients.createDefault();
      
              try
              {
                  URIBuilder builder = new URIBuilder("http://" + AccountId + ".mns.cn-hangzhou.aliyuncs.com/queues/TestQueue123");//在青岛区域创建Project
                  URI uri = builder.build();
                  HttpDelete request = new HttpDelete(uri);
                  request.setHeader("Authorization", Authorization);
                  request.setHeader("Date", date);
                  request.setHeader("Host", AccountId + ".mns.cn-hangzhou.aliyuncs.com");
                  request.setHeader("x-mns-version","2015-06-06");
      
                  HttpResponse response = httpclient.execute(request);
                  HttpEntity entity = response.getEntity();
      
                  if (entity != null)
                  {
                      System.out.println(EntityUtils.toString(entity));
                  }
              }
              catch (Exception e)
              {
                  System.out.println("error");
                  System.out.println(e.getMessage());
              }
          }
      
          //写一个md5加密的方法
          public static String md5(String plainText) {
              //定义一个字节数组
              byte[] secretBytes = null;
              try {
                  // 生成一个MD5加密计算摘要
                  MessageDigest md = MessageDigest.getInstance("MD5");
                  //对字符串进行加密
                  md.update(plainText.getBytes());
                  //获得加密后的数据
                  secretBytes = md.digest();
              } catch (NoSuchAlgorithmException e) {
                  throw new RuntimeException("没有md5这个算法!");
              }
              //将加密后的数据转换为16进制数字
              String md5code = new BigInteger(1, secretBytes).toString(16);// 16进制数字
              // 如果生成数字未满32位,需要前面补0
              for (int i = 0; i < 32 - md5code.length(); i++) {
                  md5code = "0" + md5code;
              }
              return md5code;
          }
      
          //计算签名
          public static String encode(String accessKey, String data) {
              try {
                  byte[] keyBytes = accessKey.getBytes("UTF-8");
                  byte[] dataBytes = data.getBytes("UTF-8");
                  Mac mac = Mac.getInstance("HmacSHA1");
                  mac.init(new SecretKeySpec(keyBytes, "HmacSHA1"));
                  return new String(Base64.encodeBase64(mac.doFinal(dataBytes)));
              } catch (UnsupportedEncodingException var5) {
                  throw new RuntimeException("Not supported encoding method UTF-8", var5);
              } catch (NoSuchAlgorithmException var6) {
                  throw new RuntimeException("Not supported signature method hmac-sha1", var6);
              } catch (InvalidKeyException var7) {
                  throw new RuntimeException("Failed to calculate the signature", var7);
              }
          }
      }

      发送消息

      import org.apache.commons.codec.binary.Base64;
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpResponse;
      import org.apache.http.client.HttpClient;
      import org.apache.http.client.methods.HttpPost;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.entity.StringEntity;
      import org.apache.http.impl.client.HttpClients;
      import org.apache.http.util.EntityUtils;
      
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import java.io.UnsupportedEncodingException;
      import java.math.BigInteger;
      import java.net.URI;
      import java.security.InvalidKeyException;
      import java.security.MessageDigest;
      import java.security.NoSuchAlgorithmException;
      import java.text.DateFormat;
      import java.text.SimpleDateFormat;
      import java.util.Date;
      import java.util.Locale;
      import java.util.TimeZone;
      
      public class SendMessageToMNSQueue {
      
          public static void main(String[] args) {
              //参数设置
              String AccessKeySecret = "******";//Access Key Secret
              String AccessKeyId = "******";//AccessKey ID
              String AccountId = "********";
      
              //获取GMT英文格式时间
              Date d=new Date();
              DateFormat format=new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z",Locale.ENGLISH);//英文格式的时间转换时需要带上Locale.ENGLISH,否则会转换失败,因为它默认的是本地化的设置,除非你的操作系统是英文的,总之时间转换时需要时间格式与模式保持一致。
              format.setTimeZone(TimeZone.getTimeZone("GMT"));
              String date = format.format(d);
      
              //延迟消息
              String body = "<?xml version="1.0" encoding="UTF-8"?>n" +
                      "    <Message xmlns="http://mns.aliyuncs.com/doc/v1/">n" +
                      "        <MessageBody>The Test Message!</MessageBody>n" +
                      "        <DelaySeconds>0</DelaySeconds>n" +
                      "        <Priority>1</Priority>n" +
                      "    </Message>";
      
              //构造签名String
              String SignString = "POSTn" +
                       "n"+
                      "text/xml;charset=utf-8"+
                      "n"+
                      date + "n"+
                      "x-mns-version:2015-06-06" +"n"+
                      "/queues/TestQueue123/messages";
      
              String sign = encode(AccessKeySecret,SignString);
              String Authorization = "MNS " + AccessKeyId + ":" + sign;
              HttpClient httpclient = HttpClients.createDefault();
      
              try
              {
                  URIBuilder builder = new URIBuilder("http://" + AccountId + ".mns.cn-hangzhou.aliyuncs.com/queues/TestQueue123/messages");
                  URI uri = builder.build();
                  HttpPost request = new HttpPost(uri);
                  request.setHeader("Authorization", Authorization);
                  request.setHeader("Date", date);
                  request.setHeader("Host", AccountId + ".mns.cn-hangzhou.aliyuncs.com");
                  request.setHeader("Content-Type","text/xml;charset=utf-8");
                  request.setHeader("x-mns-version","2015-06-06");
      
                  // Request body
                  StringEntity reqEntity = new StringEntity(body,"UTF-8");
                  request.setEntity(reqEntity);
      
                  HttpResponse response = httpclient.execute(request);
                  HttpEntity entity = response.getEntity();
      
                  if (entity != null)
                  {
                      System.out.println(EntityUtils.toString(entity));
                  }
              }
              catch (Exception e)
              {
                  System.out.println("error");
                  System.out.println(e.getMessage());
              }
          }
      
          //写一个md5加密的方法
          public static String md5(String plainText) {
              //定义一个字节数组
              byte[] secretBytes = null;
              try {
                  // 生成一个MD5加密计算摘要
                  MessageDigest md = MessageDigest.getInstance("MD5");
                  //对字符串进行加密
                  md.update(plainText.getBytes());
                  //获得加密后的数据
                  secretBytes = md.digest();
              } catch (NoSuchAlgorithmException e) {
                  throw new RuntimeException("没有md5这个算法!");
              }
              //将加密后的数据转换为16进制数字
              String md5code = new BigInteger(1, secretBytes).toString(16);// 16进制数字
              // 如果生成数字未满32位,需要前面补0
              for (int i = 0; i < 32 - md5code.length(); i++) {
                  md5code = "0" + md5code;
              }
              return md5code;
          }
      
          //计算签名
          public static String encode(String accessKey, String data) {
              try {
                  byte[] keyBytes = accessKey.getBytes("UTF-8");
                  byte[] dataBytes = data.getBytes("UTF-8");
                  Mac mac = Mac.getInstance("HmacSHA1");
                  mac.init(new SecretKeySpec(keyBytes, "HmacSHA1"));
                  return new String(Base64.encodeBase64(mac.doFinal(dataBytes)));
              } catch (UnsupportedEncodingException var5) {
                  throw new RuntimeException("Not supported encoding method UTF-8", var5);
              } catch (NoSuchAlgorithmException var6) {
                  throw new RuntimeException("Not supported signature method hmac-sha1", var6);
              } catch (InvalidKeyException var7) {
                  throw new RuntimeException("Failed to calculate the signature", var7);
              }
          }
      }

      消费消息

      import org.apache.commons.codec.binary.Base64;
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpResponse;
      import org.apache.http.client.HttpClient;
      import org.apache.http.client.methods.HttpGet;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.impl.client.HttpClients;
      import org.apache.http.util.EntityUtils;
      
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import java.io.UnsupportedEncodingException;
      import java.math.BigInteger;
      import java.net.URI;
      import java.security.InvalidKeyException;
      import java.security.MessageDigest;
      import java.security.NoSuchAlgorithmException;
      import java.text.DateFormat;
      import java.text.SimpleDateFormat;
      import java.util.Date;
      import java.util.Locale;
      import java.util.TimeZone;
      
      public class GetMessageFromMNSQueue {
      
          public static void main(String[] args) {
              //参数设置
              String AccessKeySecret = "********";//Access Key Secret
              String AccessKeyId = "********";//AccessKey ID
              String AccountId = "********";
      
              //获取GMT英文格式时间
              Date d=new Date();
              DateFormat format=new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z",Locale.ENGLISH);//英文格式的时间转换时需要带上Locale.ENGLISH,否则会转换失败,因为它默认的是本地化的设置,除非你的操作系统是英文的,总之时间转换时需要时间格式与模式保持一致。
              format.setTimeZone(TimeZone.getTimeZone("GMT"));
              String date = format.format(d);
      
              //构造签名String
              String SignString = "GETn" +
                       "n"+
                      "n"+
                      date + "n"+
                      "x-mns-version:2015-06-06" +"n"+
                      "/queues/TestQueue123/messages?waitseconds=30";
      
              String sign = encode(AccessKeySecret,SignString);
              String Authorization = "MNS " + AccessKeyId + ":" + sign;
              HttpClient httpclient = HttpClients.createDefault();
      
              try
              {
                  URIBuilder builder = new URIBuilder("http://" + AccountId + ".mns.cn-hangzhou.aliyuncs.com/queues/TestQueue123/messages?waitseconds=30");
                  URI uri = builder.build();
                  HttpGet request = new HttpGet(uri);
                  request.setHeader("Authorization", Authorization);
                  request.setHeader("Date", date);
                  request.setHeader("Host", AccountId + ".mns.cn-hangzhou.aliyuncs.com");
                  request.setHeader("x-mns-version","2015-06-06");
      
                  HttpResponse response = httpclient.execute(request);
                  HttpEntity entity = response.getEntity();
      
                  if (entity != null)
                  {
                      System.out.println(EntityUtils.toString(entity));
                  }
              }
              catch (Exception e)
              {
                  System.out.println("error");
                  System.out.println(e.getMessage());
              }
          }
      
          //写一个md5加密的方法
          public static String md5(String plainText) {
              //定义一个字节数组
              byte[] secretBytes = null;
              try {
                  // 生成一个MD5加密计算摘要
                  MessageDigest md = MessageDigest.getInstance("MD5");
                  //对字符串进行加密
                  md.update(plainText.getBytes());
                  //获得加密后的数据
                  secretBytes = md.digest();
              } catch (NoSuchAlgorithmException e) {
                  throw new RuntimeException("没有md5这个算法!");
              }
              //将加密后的数据转换为16进制数字
              String md5code = new BigInteger(1, secretBytes).toString(16);// 16进制数字
              // 如果生成数字未满32位,需要前面补0
              for (int i = 0; i < 32 - md5code.length(); i++) {
                  md5code = "0" + md5code;
              }
              return md5code;
          }
      
          //计算签名
          public static String encode(String accessKey, String data) {
              try {
                  byte[] keyBytes = accessKey.getBytes("UTF-8");
                  byte[] dataBytes = data.getBytes("UTF-8");
                  Mac mac = Mac.getInstance("HmacSHA1");
                  mac.init(new SecretKeySpec(keyBytes, "HmacSHA1"));
                  return new String(Base64.encodeBase64(mac.doFinal(dataBytes)));
              } catch (UnsupportedEncodingException var5) {
                  throw new RuntimeException("Not supported encoding method UTF-8", var5);
              } catch (NoSuchAlgorithmException var6) {
                  throw new RuntimeException("Not supported signature method hmac-sha1", var6);
              } catch (InvalidKeyException var7) {
                  throw new RuntimeException("Failed to calculate the signature", var7);
              }
          }
      }
      

      更多参考
      签名验证工具
      RESTfulAPI概述

      备注: 示例代码仅供快速测试使用,未做冗余处理。

      ]]>
      RocketMQ Spring 集成 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述

      本文介绍如何在 Spring 框架下用消息队列 RocketMQ 收发消息。主要包括以下两部分内容:

      • 普通消息生产者和 Spring 集成
      • 消息消费者和 Spring 集成

      测试流程

      资源创建
      1、管理门户创建实例、Topic及Group;
      2、注意:如果程序在本地测试运行,请选择在公网区域创建。

      代码测试

      1、pom.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
      
          <groupId>MavenSpringDemoMQ</groupId>
          <artifactId>MavenSpringDemoMQ</artifactId>
          <version>1.0-SNAPSHOT</version>
      
          <properties>
              <org.springframework.version>5.0.8.RELEASE</org.springframework.version>
          </properties>
          <dependencies>
              <!-- https://mvnrepository.com/artifact/org.springframework/org.springframework.context -->
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-beans</artifactId>
                  <version>${org.springframework.version}</version>
              </dependency>
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-core</artifactId>
                  <version>${org.springframework.version}</version>
              </dependency>
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-context</artifactId>
                  <version>${org.springframework.version}</version>
              </dependency>
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-expression</artifactId>
                  <version>${org.springframework.version}</version>
              </dependency>
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-context-support</artifactId>
                  <version>${org.springframework.version}</version>
              </dependency>
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-context-indexer</artifactId>
                  <version>${org.springframework.version}</version>
              </dependency>
              <!--spring core end-->
      
              <!--spring aop start-->
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-aop</artifactId>
                  <version>${org.springframework.version}</version>
              </dependency>
              <!--spirng aop end-->
      
              <!--spring aspects start-->
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-aspects</artifactId>
                  <version>${org.springframework.version}</version>
              </dependency>
              <!--spring aspects end-->
      
              <!--spring instrumentation start -->
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-instrument</artifactId>
                  <version>${org.springframework.version}</version>
              </dependency>
              <!--spring instrumentation end-->
      
              <!--RocketMQ jar依赖-->
              <dependency>
                  <groupId>com.aliyun.openservices</groupId>
                  <artifactId>ons-client</artifactId>
                  <version>1.7.9.Final</version>
              </dependency>
          </dependencies>
      </project>
      

      2、发送端配置文件producer.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
          <bean id="producer" class="com.aliyun.openservices.ons.api.bean.ProducerBean" init-method="start" destroy-method="shutdown">
              <!-- Spring 接入方式支持 Java SDK 支持的所有配置项 -->
              <property name="properties" > <!--生产者配置信息-->
                  <props>
                      <prop key="AccessKey">********</prop>
                      <prop key="SecretKey">********</prop>
                      <prop key="NAMESRV_ADDR">http://MQ_INST_********_BaQUuiNE.mq-internet-access.mq-internet.aliyuncs.com:80</prop>
                  </props>
              </property>
          </bean>
      </beans>

      3、发送端代码

      import com.aliyun.openservices.ons.api.Message;
      import com.aliyun.openservices.ons.api.Producer;
      import com.aliyun.openservices.ons.api.SendResult;
      import com.aliyun.openservices.ons.api.exception.ONSClientException;
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.support.ClassPathXmlApplicationContext;
      public class ProduceWithSpring {
          public static void main(String[] args) {
              /**
               * 生产者 Bean 配置在 producer.xml 中,可通过 ApplicationContext 获取或者直接注入到其他类(比如具体的 Controller)中
               */
              ApplicationContext context = new ClassPathXmlApplicationContext("producer.xml");
              Producer producer = (Producer) context.getBean("producer");
              //循环发送消息
              for (int i = 0; i < 100; i++) {
                  Message msg = new Message( //
                          // Message 所属的 Topic
                          "yutopic",
                          // Message Tag 可理解为 Gmail 中的标签,对消息进行再归类,方便 Consumer 指定过滤条件在消息队列 RocketMQ 的服务器过滤
                          "TagSpring",
                          // Message Body 可以是任何二进制形式的数据, 消息队列 RocketMQ 不做任何干预
                          // 需要 Producer 与 Consumer 协商好一致的序列化和反序列化方式
                          "Hello MQ".getBytes());
                  // 设置代表消息的业务关键属性,请尽可能全局唯一
                  // 以方便您在无法正常收到消息情况下,可通过控制台查询消息并补发
                  // 注意:不设置也不会影响消息正常收发
                  msg.setKey("ORDERID_100");
                  // 发送消息,只要不抛异常就是成功
                  try {
                      SendResult sendResult = producer.send(msg);
                      assert sendResult != null;
                      System.out.println("send success: " + sendResult.getMessageId());
                  }catch (ONSClientException e) {
                      System.out.println("发送失败");
                  }
              }
              //关系producer
              producer.shutdown();
          }
      }

      4、消费端配置文件consumer.xml

      <?xml version="1.0" encoding="UTF-8"?>
      
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
      <bean id="msgListener" class="demo.DemoMessageListener"></bean> <!--Listener 配置-->
      <!-- Group ID 订阅同一个 Topic,可以创建多个 ConsumerBean-->
      <bean id="consumer" class="com.aliyun.openservices.ons.api.bean.ConsumerBean" init-method="start" destroy-method="shutdown">
          <property name="properties" > <!--消费者配置信息-->
              <props>
                  <prop key="AccessKey">********</prop>
                  <prop key="SecretKey">********</prop>
                  <prop key="NAMESRV_ADDR">http://MQ_INST_********_BaQUuiNE.mq-internet-access.mq-internet.aliyuncs.com:80</prop>
                  <prop key="GROUP_ID">GID_Spring</prop>
                  <!--将消费者线程数固定为 50 个
                  <prop key="ConsumeThreadNums">50</prop>
                  -->
              </props>
          </property>
          <property name="subscriptionTable">
              <map>
                  <entry value-ref="msgListener">
                      <key>
                          <bean class="com.aliyun.openservices.ons.api.bean.Subscription">
                              <property name="topic" value="yutopic"/>
                              <property name="expression" value="*"/><!--expression 即 Tag,可以设置成具体的 Tag,如 taga||tagb||tagc,也可设置成 *。 * 仅代表订阅所有 Tag,不支持通配-->
                          </bean>
                      </key>
                  </entry>
                  <!--更多的订阅添加 entry 节点即可,如下所示-->
                  <!--<entry value-ref="msgListener">-->
                      <!--<key>-->
                          <!--<bean class="com.aliyun.openservices.ons.api.bean.Subscription">-->
                              <!--<property name="topic" value="TopicTestMQ-Other"/> &lt;!&ndash;订阅另外一个 Topic &ndash;&gt;-->
                              <!--<property name="expression" value="taga||tagb"/> &lt;!&ndash; 订阅多个 Tag &ndash;&gt;-->
                          <!--</bean>-->
                      <!--</key>-->
                  <!--</entry>-->
              </map>
          </property>
      </bean>
      

      5、DemoMessageListener

      import com.aliyun.openservices.ons.api.Action;
      import com.aliyun.openservices.ons.api.ConsumeContext;
      import com.aliyun.openservices.ons.api.Message;
      import com.aliyun.openservices.ons.api.MessageListener;
      public class DemoMessageListener implements MessageListener {
      
      public Action consume(Message message, ConsumeContext context) {
          System.out.println("Receive: " + message.getMsgID());
          try {
              //do something..
              return Action.CommitMessage;
          }catch (Exception e) {
              //消费失败
              return Action.ReconsumeLater;
          }
      }
      }`
      

      6、消费端启动程序

      import org.springframework.context.ApplicationContext;
      import org.springframework.context.support.ClassPathXmlApplicationContext;
      public class ConsumeWithSpring {
          public static void main(String[] args) {
              /**
               * 消费者 Bean 配置在 consumer.xml 中,可通过 ApplicationContext 获取或者直接注入到其他类(比如具体的 Controller)中
               */
              ApplicationContext context = new ClassPathXmlApplicationContext("consumer.xml");
              System.out.println("Consumer Started");
          }
      }
      

      测试效果

      • 发送端
        image.png
      • 消费端
        image.png

      更多参考
      Spring 集成

      订阅关系一致

      ]]>
      如何使用phpMQTT连接阿里云微服务消息队列for IoT Wed, 08 Apr 2020 03:03:11 +0800 概述

      关于阿里云的微服务消息队列,目前官方网站在接入示例部分给出了多种语言的SDK参考示例,但是还没有关于php的SDK及接入示例参考。下面主要介绍在使用目前主流的第三方SDK phpMQTT的过程中遇到的问题及解决办法。

      主要流程

      参数准备

      在使用MQTT的过程中,关于参数的配置是一个比较让人头疼的问题,很多用户都在配置参数的过程中遇到过问题,这里建议用户首先使用第三方的工具MQTT.fx完成参数的配置调通工作,后续在代码中直接使用即可,避免在参数配置的过程中犯错。具体可以参考博客:如何使用MQTT.fx连接微服务消息队列。

      包的引用

      用户可以直接参考GitHub官方链接,使用composer的方式加载包,也可以直接下载整个项目使用。

      SendDemo

      <?php
      
      use BluerhinosphpMQTT;
      
      require("phpMQTT.php");
      
      $server = "******.mqtt.aliyuncs.com";     // change if necessary
      $port = 1883;                     // change if necessary
      $username = "******";                   // set your username
      $password = "******";                   // set your password
      $client_id = "GID_******@@@devicename"; // make sure this is unique for connecting to sever - you could use uniqid()
      
      $mqtt = new phpMQTT($server, $port, $client_id);
      
      if ($mqtt->connect(true, NULL, $username, $password)) {
          $mqtt->publish("mqtt_topic/notice/", "Hello World PHP! at " . date("r"), 0);
          $mqtt->close();
          echo "send success!";
      } else {
          echo "Time out!n";
      }
      ?>

      问题分析及解决

      现象

      用户使用在上面介绍的工具测试可行的参数直接配置到代码中,会报:Uninitialized string offset 的错误,跟踪到源码,发现:

      $string = $this->read(4)

      并未读入任何信息,但是参数的配置又没有什么问题,部分用户发现使用同样的代码连接自己的MQTT服务器可以正常连接。

      原因

      目前phpMQTT仅支持mqttv3.1,但是目前阿里云的微服务消息队列使用的是v3.1.1协议,MQTT v3.1.1协议名称长度 4,而3.1的协议长度为6。

      解决方案

      源码:

      $buffer .= chr(0x06); $i++;
        $buffer .= chr(0x4d); $i++;
        $buffer .= chr(0x51); $i++;
        $buffer .= chr(0x49); $i++;
        $buffer .= chr(0x73); $i++;
        $buffer .= chr(0x64); $i++;
        $buffer .= chr(0x70); $i++;
        $buffer .= chr(0x03); $i++;

      调整为:

      $buffer .= chr(0x04); $i++;
        $buffer .= chr(0x4d); $i++;
        $buffer .= chr(0x51); $i++;
        $buffer .= chr(0x54); $i++;
        $buffer .= chr(0x54); $i++;
        $buffer .= chr(0x04); $i++;

      测试效果

      1、发送端使用调整后的源码运行SendDemo;

      2、接收监听:

      image.png

      参考链接

      phpMQTT
      GitHub Issue

      ]]>
      如何使用MQTT.fx连接微服务消息队列 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述

      MQTT.fx 是目前主流的 MQTT 桌面客户端,它支持 Windows, Mac, Linux,可以快速验证是否可以与 IoT Cloud 进行连接并发布或订阅消息。目前很多同学在使用阿里云微服务消息队列时不知道如何使用该工具进行连接测试,本文主要演示如何使用该工具测试连接阿里云微服务消息队列的Broker。

      步骤

      1、MQTT.fx下载链接;
      image.png

      2、微服务消息队列UserName、Password的生成,目前用户可以参考官方的SDK示例进行创建,也可以快速使用门户提供的工具创建。下面演示使用工具快速创建的方法:
      image.png

      3、MQTT.fx配置连接参数:
      image.png

      订阅及发布测试

      1、父类Topic预创建
      image.png

      2、订阅设置:
      image.png

      3、发布消息:
      image.png

      4、消息订阅效果:
      image.png

      更多参考

      MQTT 签名计算

      微服务消息队列名词解释

      ]]>
      MQ消息队列.NET SDK的使用 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述

      MQ消息队列的SDK目前支持Java、C/C++及.NET三种语言,关于.NET SDK的使用目前因为版本更新的问题,用户在使用的过程中多少会出现这样或那样的问题,特别是SDK中example的使用问题。本文主要介绍.NET SDK的使用注意事项及用户可能遇到的问题。

      准备工作

      1、SDK下载 下载地址链接,建议下载最新版本的SDK;

      2、加载相关文件到已经创建的.NET控制台项目,配置参考链接;

      3、配置管理器示例:

      image.png

      4、如果找不到x64,可以通过如下方式新建:

      image.png

      示例程序运行注意事项

      1、基本信息的录入,具体创建方法可以在管理门户完成,注意绝大部分的用户测试环境都是在本地,所以请在公网区域创建Topic、生产者及消费者;

      image.png

      2、SDK sample 默认代码的运行测试文件是: ConsumerAndProducerForEx.cs,默认的示例程序并未给出ONSAddr的设置,且默认指向的是内网的地址,所以用户完成了规定参数的配置后仍然无法连通,所以需要添加ONSAddr参数的配置,参数对应的是生产者或消费者的接入点,代码修改示例如下:

      ONSFactoryProperty factoryInfo = new ONSFactoryProperty();
      factoryInfo.setFactoryProperty(ONSFactoryProperty.ONSAddr, "http://onsaddr-internet.aliyun.com/rocketmq/nsaddr4client-internet");
      factoryInfo.setFactoryProperty(ONSFactoryProperty.AccessKey, Ons_AccessKey);

      dll文件的复制粘贴问题

      直接复制SDK->lib->x64下的文件到项目的bin->x64->Debug。

      image.png

      测试运行效果

      image.png

      新版本SDK使用参考

      截止2019.2.16,目前最新的SDK版本为1.1.3。基本配置方式与之前一致。只是在参数设置的地方略有不同,示例如下:

      private static ONSFactoryProperty getFactoryProperty()
       {
           ONSFactoryProperty factoryInfo = new ONSFactoryProperty();
           factoryInfo.setFactoryProperty(ONSFactoryProperty.NAMESRV_ADDR, "http://MQ_INST_1848217816617278_BaQUuiNE.mq-internet-access.mq-internet.aliyuncs.com:80");
           factoryInfo.setFactoryProperty(ONSFactoryProperty.AccessKey, Ons_AccessKey);
           factoryInfo.setFactoryProperty(ONSFactoryProperty.SecretKey, Ons_SecretKey);
           factoryInfo.setFactoryProperty(ONSFactoryProperty.ConsumerId, Ons_ConsumerId);
           factoryInfo.setFactoryProperty(ONSFactoryProperty.ProducerId, Ons_ProducerID);
           factoryInfo.setFactoryProperty(ONSFactoryProperty.PublishTopics, Ons_Topic);
          return factoryInfo;
       }

      中文编码处理问题

      处理思路:对发送的消息进行base64编码,消费的消息进行base64解码。

      using System;
      using System.Text;
      using System.Threading;
      using ons;
      
      namespace test
      {
          /// <summary>
          /// 消费监听类
          /// </summary>
          public class MyMsgListener : MessageListener
          {
              public MyMsgListener()
              {
              }
      
              ~MyMsgListener()
              {
              }
      
              public override ons.Action consume(Message value, ConsumeContext context)
              {
                  Byte[] text = Encoding.Default.GetBytes(value.getBody());
                  string s = System.Text.Encoding.UTF8.GetString(text, 0, text.Length);
                  Console.WriteLine(base64tstring.UnBase64String(s));
                  return ons.Action.CommitMessage;
              }
          }
      
          /// <summary>
          /// base64编码解码类
          /// </summary>
          public class base64tstring {
      
              public static string ToBase64String(string value)
              {
                  if (value == null || value == "")
                  {
                      return "";
                  }
                  byte[] bytes = Encoding.UTF8.GetBytes(value);
                  return Convert.ToBase64String(bytes);
              }
      
              public static string UnBase64String(string value)
              {
                  if (value == null || value == "")
                  {
                      return "";
                  }
                  byte[] bytes = Convert.FromBase64String(value);
                  return Encoding.UTF8.GetString(bytes);
              }
          }
      
          class ConsumerAndProducerForEx
          {
              static void Main(string[] args)
              {
                  ONSFactoryProperty factoryInfo = new ONSFactoryProperty();
                  factoryInfo.setFactoryProperty(ONSFactoryProperty.AccessKey, "******");
      
                  factoryInfo.setFactoryProperty(ONSFactoryProperty.SecretKey, "******");
                  factoryInfo.setFactoryProperty(ONSFactoryProperty.ConsumerId, "GID_test");
                  factoryInfo.setFactoryProperty(ONSFactoryProperty.ProducerId, "GID_test");
                  factoryInfo.setFactoryProperty(ONSFactoryProperty.PublishTopics, "newtopictest");
                  factoryInfo.setFactoryProperty(ONSFactoryProperty.NAMESRV_ADDR, "http://******.mq-internet-access.mq-internet.aliyuncs.com:80");
                  factoryInfo.setFactoryProperty(ONSFactoryProperty.LogPath, "E://代码//示例//消息队列//阿里消息MQ//AliMQTest//log//AliSend");
      
      
      
                  //// 消息发送
                  Producer producer = ONSFactory.getInstance().createProducer(factoryInfo);
                  producer.start();
      
                  string myString = "Example message body测试"; //中英文混合字符
      
                  myString = base64tstring.ToBase64String(myString);
                  Message msg = new Message(factoryInfo.getPublishTopics(), "tagA", myString);
                  msg.setKey(Guid.NewGuid().ToString());
                  for (int i = 0; i < 10; i++)
                  {
                      try
                      {
                          try
                          {
                              SendResultONS sendResult = producer.send(msg);
      
                              Console.WriteLine("send success {0}", sendResult.getMessageId());
                          }
                          catch (Exception ex)
                          {
                              Console.WriteLine("Error message: " + ex.Message);
                          }
                      }
                      catch (Exception ex)
                      {
                          Console.WriteLine("send failure{0}", ex.ToString());
                      }
                  }
                  producer.shutdown();
      
                  // 创建消费者实例
                  PushConsumer consumer = ONSFactory.getInstance().createPushConsumer(factoryInfo);
      
                  Console.WriteLine("开始消费消息:");
                  // 订阅 Topics
                  consumer.subscribe("newtopictest", "*", new MyMsgListener());
                  // 启动客户端实例
                  consumer.start();
                  //该设置仅供 demo 使用,实际生产中请保证进程不退出
                  Thread.Sleep(300000);
      
                  Console.ReadLine();
              }
          }
      }

      测试效果:
      image.png

      参考链接

      NET SDK 环境准备
      收发普通消息

      ]]>
      MQTT获取离线消息小议 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述

      微消息队列MQ for IoT在处理离线消息时,为了简化离线消息获取机制,微消息队列系统在客户端成功建立连接并通过权限校验后,会自动加载离线消息并下发到客户端,但是实际在使用过程中会出现消费端启动后迟迟无法获取离线消息的问题,本文主要介绍延迟消息的发送与接收环节需要注意的问题。

      协议相关

      注意在使用SDK进行离线消息的发送过程中需要特别注意QoS和cleanSession两个参数。

      • QoS 指代消息传输的服务质量(主要针对发送端)
      取值 1 2 3
      意义 最多分发一次 最多分发一次 仅分发一次
      • cleanSession 建立 TCP 连接后是否关心之前状态(主要针对接收端)
      true false
      客户端再次上线时,将不再关心之前所有的订阅关系以及离线消息 客户端再次上线时,还需要处理之前的离线消息,而之前的订阅关系也会持续生效

      为了处理的方便,对于处理离线消息的情况,建议不论是发送端还是接收端,参数都设置为:

      QoS = 1
      
      cleanSession = false

      Java示例代码

      Send Code

      import org.eclipse.paho.client.mqttv3.*;
      import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
      import java.io.IOException;
      import java.util.Date;
      
      import static org.eclipse.paho.client.mqttv3.MqttConnectOptions.MQTT_VERSION_3_1_1;
      
      public class MQTTSendMsg1 {
      
          public static void main(String[] args) throws IOException {
      
              final String broker ="tcp://******.mqtt.aliyuncs.com:1883";
              final String acessKey ="******";
              final String secretKey ="******";
              final String topic ="******";
              final String clientId ="GID_******@@@ClientID_device1";
              String sign;
              MemoryPersistence persistence = new MemoryPersistence();
              try {
                  final MqttClient sampleClient = new MqttClient(broker, clientId, persistence);
                  final MqttConnectOptions connOpts = new MqttConnectOptions();
                  System.out.println("Connecting to broker: " + broker);
                  sign = MacSignature.macSignature(clientId.split("@@@")[0], secretKey);
                  connOpts.setUserName(acessKey);
                  connOpts.setServerURIs(new String[] { broker });
                  connOpts.setPassword(sign.toCharArray());
                  connOpts.setCleanSession(false);
                  connOpts.setKeepAliveInterval(90);
                  connOpts.setAutomaticReconnect(true);
                  connOpts.setMqttVersion(MQTT_VERSION_3_1_1);
                  sampleClient.setCallback(new MqttCallbackExtended() {
                      public void connectComplete(boolean reconnect, String serverURI) {
                          System.out.println("connect success");
                          //连接成功,需要上传客户端所有的订阅关系
                      }
                      public void connectionLost(Throwable throwable) {
                          System.out.println("mqtt connection lost");
                      }
                      public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
                          System.out.println("messageArrived:" + topic + "------" + new String(mqttMessage.getPayload()));
                      }
                      public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
                          System.out.println("deliveryComplete:" + iMqttDeliveryToken.getMessageId());
                      }
                  });
                  sampleClient.connect(connOpts);
                  for (int i = 0; i < 5; i++) {
                      try {
                          String scontent = new Date()+"MQTT Test body" + i;
                          //此处消息体只需要传入 byte 数组即可,对于其他类型的消息,请自行完成二进制数据的转换
                          final MqttMessage message = new MqttMessage(scontent.getBytes());
                          message.setQos(1);//设置离线消息的情况
                          System.out.println(i+" pushed at "+new Date()+" "+ scontent);
                          sampleClient.publish(topic+"/notice/", message);
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                  }
              } catch (Exception me) {
                  me.printStackTrace();
              }
          }
      }

      Receive Code

      import org.eclipse.paho.client.mqttv3.*;
      import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.LinkedBlockingQueue;
      import java.util.concurrent.ThreadPoolExecutor;
      import java.util.concurrent.TimeUnit;
      
      public class MQTTRecvMsg {
              public static void main(String[] args) {
      
                  final String broker ="tcp://******.mqtt.aliyuncs.com:1883";
                  final String acessKey ="******";
                  final String secretKey ="******";
                  final String topic ="******";
                  final String clientId ="GID_******@@@ClientID_device2";
                  String sign;
                  MemoryPersistence persistence = new MemoryPersistence();
                  try {
                      final MqttClient sampleClient = new MqttClient(broker, clientId, persistence);
                      final MqttConnectOptions connOpts = new MqttConnectOptions();
                      System.out.println("Connecting to broker: " + broker);
      
                      sign = MacSignature.macSignature(clientId.split("@@@")[0], secretKey);
                      final String[] topicFilters=new String[]{topic+"/notice/"};
                      final int[]qos={1};
                      connOpts.setUserName(acessKey);
                      connOpts.setServerURIs(new String[] { broker });
                      connOpts.setPassword(sign.toCharArray());
                      connOpts.setCleanSession(false);//设置确定是否继续接受离线消息
                      connOpts.setKeepAliveInterval(90);
                      connOpts.setAutomaticReconnect(true);
                      final ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>());
                      sampleClient.setCallback(new MqttCallbackExtended() {
                          public void connectComplete(boolean reconnect, String serverURI) {
                              System.out.println("connect success");
                              //连接成功,需要上传客户端所有的订阅关系
                              executorService.submit(new Runnable()
                              {
                                  public void run()
                                  {
                                      try
                                      {
                                          sampleClient.subscribe(topicFilters, qos);
                                      } catch(Exception me)
                                      {
                                          me.printStackTrace();
                                      }
                                  }
                              });
                          }
                          public void connectionLost(Throwable throwable) {
                              System.out.println("mqtt connection lost");
                          }
                          public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
                              System.out.println("messageArrived:" + topic + "------" + new String(mqttMessage.getPayload()));
                          }
                          public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
                              System.out.println("deliveryComplete:" + iMqttDeliveryToken.getMessageId());
                          }
                      });
                      //客户端每次上线都必须上传自己所有涉及的订阅关系,否则可能会导致消息接收延迟
                      sampleClient.connect(connOpts);
                      //每个客户端最多允许存在30个订阅关系,超出限制可能会丢弃导致收不到部分消息
                      sampleClient.subscribe(topicFilters,qos);
                      Thread.sleep(Integer.MAX_VALUE);
                  } catch (Exception me) {
                      me.printStackTrace();
                  }
              }
      }
      

      特别注意:

      离线消息生成需要一定的时间,因为推送的消息需要等待客户端的 ack 超时才会被判成离线消息,所以获取离线消息一般也需要订阅端等待一定的时间。

      参考链接

      微消息队列名词解释

      MQTT 获取离线消息

      ]]>
      物联网平台规则引擎使用说明 Wed, 08 Apr 2020 03:03:11 +0800

      作者:三烽

      一、数据流转

      1、基本概念

      当设备基于Topic进行通信时,您可以在规则引擎的数据流转中,编写SQL对Topic中的数据进行处理,并配置转发规则将处理后的数据转发到其他Topic或阿里云其他服务。

      2、方案对比

      规则引擎和服务端订阅都可以进行数据流转,两者的对比详见文档https://help.aliyun.com/document_detail/102335.html

      3、创建规则

      数据类型可选JSON和二进制。因为数据流转是基于Topic处理数据的,所以数据格式要和被处理Topic中的数据格式保持一致。【若选择为二进制,该规则不能处理系统Topic的消息,且不能将数据转发至表格存储、时序时空数据库和云数据库RDS版。】
      image.png

      规则详情页的配置说明请参考文档https://help.aliyun.com/document_detail/42733.html

      4、SQL表达式举例说明

      产品中有一个属性为struct,数据类型为结构体,结构体中JSON对象分别是length、height和weight。
      image.png

      要正确提取struct中的各参数值,sql语句应如下所示(注意items的使用,详见5数据流转过程)
      image.png

      SQL中的数组使用说明如下所示
      image.png

      【注意事项】
      a.SELECT语句中的字段最多支持50个
      b.不支持子SQL查询
      c.可以使用SQL内置函数,详见文档。

      5、数据流转过程

      a.通过自定义topic上传,透传结构不变,sql语句中直接填写payload中的key值。
      image.png

      b.通过系统topic上传,注意经过物模型解析后的结果,sql语句中要使用items.xxx的形式。
      image.png

      6、数据格式

      上传到系统topic中的数据格式由平台定义,详见文档https://help.aliyun.com/document_detail/73736.html

      二、数据流转使用示例

      1、数据转发到另一Topic

      https://help.aliyun.com/document_detail/42734.html

      2、数据转发到消息队列(RocketMQ)

      https://help.aliyun.com/document_detail/59000.html

      3、数据转发到表格存储(Table Store)

      https://help.aliyun.com/document_detail/42735.html

      4、数据转发到DataHub

      https://help.aliyun.com/document_detail/42734.html

      5、数据转发到云数据库(RDS)

      https://help.aliyun.com/document_detail/42736.html

      6、数据转发到消息服务(Message Service)

      https://help.aliyun.com/document_detail/44008.html

      7、数据转发到时序时空数据库(TSDB)

      https://help.aliyun.com/document_detail/64143.html

      8、数据转发到函数计算(FC)

      https://help.aliyun.com/document_detail/64234.html

      三、场景联动

      1、基本概念

      场景联动是规则引擎中,一种开发自动化业务逻辑的可视化编程方式,您可以通过可视化的方式定义设备之间联动规则,并将规则部署至云端或者边缘端。
      例如,您每天18:00下班回家。在炎热的夏天,您希望您到家后,家里的温度是凉爽、舒适的。您可以创建一条规则,使空调设备自动化,实现这个需求。

      2、规则配置

      请参考文档https://help.aliyun.com/document_detail/102241.html

      ]]>
      物联网平台服务端订阅排查流程 Wed, 08 Apr 2020 03:03:11 +0800

      作者:三烽

      简介

      服务端可以直接订阅产品下所有类型的消息:设备上报消息、设备状态变化通知、网关发现子设备上报、设备生命周期变更、设备拓扑关系变更。配置服务端订阅后,物联网平台会将产品下所有设备的已订阅类型的消息转发至您的服务端。
      ⭕️消息流转过程为:设备——物联网平台——服务端
      ⭕️如果是老的HTTP2订阅,请尽快更新到AMQP订阅

      排查过程

      一、检查控制台相应参数配置。

      A.相应产品订阅类型(AMQP还是MNS)

      1.如果是AMQP订阅,检查这个产品选择的消费组(例如:如果只选择了消费组A,但是服务端代码里用的是消费组B的消费组ID,自然订阅不到消息)
      2.MNS订阅不涉及消费组概念,检查产品对应的队列是否正常创建即可

      B.勾选的推送消息类型

      1.如果只勾选了设备状态变化通知,那服务端自然订阅不到“设备上报消息”了
      image.png

      image.png

      二、检查上报的数据格式

      大多数情况都是设备端进行属性上报,然后在服务端订阅不到消息。
      这个时候看一下控制台上的日志,找到物模型数据分析的日志,检查物模型解析是否正常。

      (在上行消息分析日志里看到消息,只能表示消息到了平台,但是很有可能物模型解析失败,所以不会推送到服务端。因此一定要看物模型数据分析的日志,看看是不是物模型解析失败了。)

      image.png

      三、检查是不是开启了多个客户端进行订阅

      如果一和二都检查无误,那要看一下是不是启动了多个客户端。
      image.png

      四、提交工单,提供相应信息

      设备三元组信息
      消息的messageID
      如果是设备上下线的消息,提供设备上下线的日志截图(也就是日志里的设备行为分析)
      MNS订阅实际上是规则引擎的数据流转,提供上行消息分析里(Transmit to MNS……)日志的相关内容

      ]]>
      阿里云物联网平台设备历史属性上报及MNS服务端订阅 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述

      阿里云物联网平台不仅支持设备即时属性的上报,也支持因为某些原因,没有及时上报的属性数据,通过历史属性上报的方式进行上报,历史属性上报Topic:/sys/{productKey}/{deviceName}/thing/event/property/history/post。本文结合物联网平台最新推出的独享实例,在新的实例下面创建产品及设备,进行历史属性的上报测试,并进行 MNS 历史属性服务端订阅。

      操作步骤

      1、创建独享实例,在独享实例下面创建产品和设备

      image.png

      image.png

      image.png

      2、获取独享实例设备端MQTT接入点

      image.png

      3、设备端接入,参考[链接
      ](https://yq.aliyun.com/articles/719051?spm=a2c4e.11153940.0.0.50666a54kkDYBk)

      import com.alibaba.taro.AliyunIoTSignUtil;
      import org.eclipse.paho.client.mqttv3.*;
      import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
      import java.util.HashMap;
      import java.util.Map;
      
      public class IoTDemoPubSubDemo {
      
          public static String productKey = "g028S******";
          public static String deviceName = "device1";
          public static String deviceSecret = "aKEHNK1w0UBvGcA1BA7gf2eHC********";
          public static String regionId = "cn-shanghai";
          public static String isntanceId = "iot-instc-public-cn-0pp1g*******";
      
          // 物模型 - 历史属性上报topic
          private static String pubTopic = "/sys/" + productKey + "/" + deviceName + "/thing/event/property/history/post";
      
          private static MqttClient mqttClient;
      
          public static void main(String [] args){
      
              initAliyunIoTClient();
              postDeviceProperties();
          }
      
          /**
           * 初始化 Client 对象
           */
          private static void initAliyunIoTClient() {
      
              try {
                  // 构造连接需要的参数
                  String clientId = "java" + System.currentTimeMillis();
                  Map<String, String> params = new HashMap<>(16);
                  params.put("productKey", productKey);
                  params.put("deviceName", deviceName);
                  params.put("clientId", clientId);
                  String timestamp = String.valueOf(System.currentTimeMillis());
                  params.put("timestamp", timestamp);
                  // MQTT 设备接入 公网终端节点(Endpoint)
                  String targetServer = "tcp://" + isntanceId + ".iot-as-mqtt."+regionId+".iothub.aliyuncs.com:1883";
      
                  String mqttclientId = clientId + "|securemode=3,signmethod=hmacsha1,timestamp=" + timestamp + "|";
                  String mqttUsername = deviceName + "&" + productKey;
                  String mqttPassword = AliyunIoTSignUtil.sign(params, deviceSecret, "hmacsha1");
      
                  connectMqtt(targetServer, mqttclientId, mqttUsername, mqttPassword);
      
              } catch (Exception e) {
                  System.out.println("initAliyunIoTClient error " + e.getMessage());
              }
          }
      
          public static void connectMqtt(String url, String clientId, String mqttUsername, String mqttPassword) throws Exception {
      
              MemoryPersistence persistence = new MemoryPersistence();
              mqttClient = new MqttClient(url, clientId, persistence);
              MqttConnectOptions connOpts = new MqttConnectOptions();
              // MQTT 3.1.1
              connOpts.setMqttVersion(4);
              connOpts.setAutomaticReconnect(false);
      //        connOpts.setCleanSession(true);
              connOpts.setCleanSession(false);
      
              connOpts.setUserName(mqttUsername);
              connOpts.setPassword(mqttPassword.toCharArray());
              connOpts.setKeepAliveInterval(60);
      
              mqttClient.connect(connOpts);
          }
      
          /**
           * 汇报属性
           */
          private static void postDeviceProperties() {
      
              try {
                  //上报数据
                  //高级版 物模型-属性上报payload
                  System.out.println("历史上报属性值");
                  String payloadJson = "{ "id": 123, "version": "1.0", "method": "thing.event.property.history.post", "params": [ { "identity": { "productKey": "g028S******", "deviceName": "device1" }, "properties": [ { "AreaId": { "value": "history data test 4", "time": 1578198990000 } } ] } ] }";
                  MqttMessage message = new MqttMessage(payloadJson.getBytes("utf-8"));
                  message.setQos(1);
                  mqttClient.publish(pubTopic, message);
              } catch (Exception e) {
                  System.out.println(e.getMessage());
              }
          }
      }
      

      payLoad格式参考

      4、设备运行状态查看

      image.png

      5、MNS服务端订阅配置
      image.png

      6、使用 MNS Python SDK 获取MNS订阅到Queue中的消息

      • 6.1 安装SDK
        image.png
      • 6.2 Code Sample
      #init my_account, my_queue
      from mns.account import Account
      import sys
      import json
      import base64
      
      endpoint = "http://18482178********.mns.cn-shanghai.aliyuncs.com/"
      accid = "LTAIOZZg********"
      acckey = "v7CjUJCMk7j9aKduMAQLjy********"
      token = ""
      my_account = Account(endpoint, accid, acckey, token)
      queue_name = "aliyun-iot-g028S******"
      my_queue = my_account.get_queue(queue_name)
      
      #循环读取删除消息直到队列空
      #receive message请求使用long polling方式,通过wait_seconds指定长轮询时间为3秒
      
      ## long polling 解析:
      ### 当队列中有消息时,请求立即返回;
      ### 当队列中没有消息时,请求在MNS服务器端挂3秒钟,在这期间,有消息写入队列,请求会立即返回消息,3秒后,请求返回队列没有消息;
      
      wait_seconds = 3
      print("%sReceive And Delete Message From Queue%snQueueName:%snWaitSeconds:%sn" % (10*"=", 10*"=", queue_name, wait_seconds))
      while True:
          #读取消息
          try:
              recv_msg = my_queue.receive_message(wait_seconds)
              print("Receive Message Succeed! ReceiptHandle:%s MessageBody:%s MessageID:%s" % (recv_msg.receipt_handle, recv_msg.message_body, recv_msg.message_id))
              print("Message Body: ",base64.b64decode(json.loads(recv_msg.message_body)['payload'])) # 转发到mns 的messagebody经过了base64编码,获取具体消息的内容需要做base64解码
          except Exception as e:
          #except MNSServerException as e:
              if e.type == u"QueueNotExist":
                  print("Queue not exist, please create queue before receive message.")
                  sys.exit(0)
              elif e.type == u"MessageNotExist":
                  print("Queue is empty!")
                  sys.exit(0)
              print("Receive Message Fail! Exception:%sn" % e)
              continue
      
          #删除消息
          try:
              my_queue.delete_message(recv_msg.receipt_handle)
              print("Delete Message Succeed!  ReceiptHandle:%s" % recv_msg.receipt_handle)
          except Exception as e:
              print("Delete Message Fail! Exception:%sn" % e)
      print("Delete Message Fail! Exception:%sn" % e)
      

      6.3 Test Result

      Receive Message Succeed! ReceiptHandle:8-2zuDHj20LzZz8zcFz0z6YEzcSFqxyIKefW MessageBody:{"payload":"eyJkZXZpY2VUeXBlIjoiQ3VzdG9tQ2F0ZWdvcnkiLCJpb3RJZCI6IlY1WGJXekluY0EyOW41aWNib3RYZzAyODAwIiwicmVxdWVzdElkIjoiMTIzIiwicHJvZHVjdEtleSI6ImcwMjhTR1o4RlJDIiwiZ210Q3JlYXRlIjoxNTc4MjkxNzI0NjY4LCJkZXZpY2VOYW1lIjoiZGV2aWNlMSIsIml0ZW1zIjp7IkFyZWFJZCI6eyJ0aW1lIjoxNTc4MTk4OTkwMDAwLCJ2YWx1ZSI6Imhpc3RvcnkgZGF0YSB0ZXN0*********","messagetype":"thing_history","topic":"/g028S******/device1/thing/event/property/history/post","messageid":1214069604465248256,"timestamp":1578291724} MessageID:5F8092E53A67656C7F811CD50E19A150
      Message Body:  b'{"deviceType":"CustomCategory","iotId":"V5XbWzIncA29n5icbo********","requestId":"123","productKey":"g028S******","gmtCreate":1578291724668,"deviceName":"device1","items":{"AreaId":{"time":1578198990000,"value":"history data test 4"}}}'
      DEBUG: v7CjUJCMk7j9aKduMAQLjyCmb8cmCm hAbKzezvRlqeVXC18CzChL4OZZk= DELETE
      

      更多参考

      使用MNS服务端订阅
      Python SDK 队列使用手册

      ]]>
      阿里云物联网平台设备数据转发到消息队列RocketMQ全链路测试 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述

      您可以使用规则引擎,将物联网平台数据转发到消息队列(RocketMQ)中存储。从而实现消息从设备、物联网平台、RocketMQ到应用服务器之间的全链路高可靠传输能力。文本从物联网平台的产品及设备的创建开始,逐步介绍整个链路的完整实现。

      操作步骤

      1、创建物联网产品及设备

      参考 阿里云物联网平台Qucik Start 快速创建产品和设备。

      2、RocketMQ控制台 创建实例、Topic和Group,这个为了方便本地测试消费MQ的消息,选择在公网区域创建相关资源

      image.png

      3、配置规则引擎

      a、配置总览
      image.png

      b、处理数据
      image.png

      c、转发数据
      image.png

      d、配置完开启规则引擎

      image.png

      相关参考:
      SQL语句参考
      [数据转发到消息队列RocketMQ
      ](https://help.aliyun.com/document_detail/59000.html?spm=a2c4e.11153940.0.0.24bc367dZhn976)
      设备上报属性消息

      import com.alibaba.taro.AliyunIoTSignUtil;
      import org.eclipse.paho.client.mqttv3.*;
      import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
      
      import java.util.HashMap;
      import java.util.Map;
      
      public class IoTDemoPubSubDemo1 {
          public static String productKey = "*********";
          public static String deviceName = "*********";
          public static String deviceSecret = "*********";
          public static String regionId = "cn-shanghai";
      
          // 物模型-属性上报topic
          private static String pubTopic = "/sys/" + productKey + "/" + deviceName + "/thing/event/property/post";
      
          private static MqttClient mqttClient;
      
          public static void main(String [] args){
      
              // 初始化Mqtt Client对象
              initAliyunIoTClient();
              // 汇报属性
              postDeviceProperties();
      
          }
      
          /**
           * 初始化 Client 对象
           */
          private static void initAliyunIoTClient() {
      
              try {
                  // 构造连接需要的参数
                  String clientId = "java" + System.currentTimeMillis();
                  Map<String, String> params = new HashMap<>(16);
                  params.put("productKey", productKey);
                  params.put("deviceName", deviceName);
                  params.put("clientId", clientId);
                  String timestamp = String.valueOf(System.currentTimeMillis());
                  params.put("timestamp", timestamp);
                  // cn-shanghai
                  String targetServer = "tcp://" + productKey + ".iot-as-mqtt."+regionId+".aliyuncs.com:1883";
      
                  String mqttclientId = clientId + "|securemode=3,signmethod=hmacsha1,timestamp=" + timestamp + "|";
                  String mqttUsername = deviceName + "&" + productKey;
                  String mqttPassword = AliyunIoTSignUtil.sign(params, deviceSecret, "hmacsha1");
      
                  connectMqtt(targetServer, mqttclientId, mqttUsername, mqttPassword);
      
              } catch (Exception e) {
                  System.out.println("initAliyunIoTClient error " + e.getMessage());
              }
          }
      
          public static void connectMqtt(String url, String clientId, String mqttUsername, String mqttPassword) throws Exception {
      
              MemoryPersistence persistence = new MemoryPersistence();
              mqttClient = new MqttClient(url, clientId, persistence);
              MqttConnectOptions connOpts = new MqttConnectOptions();
              // MQTT 3.1.1
              connOpts.setMqttVersion(4);
              connOpts.setAutomaticReconnect(false);
      //        connOpts.setCleanSession(true);
              connOpts.setCleanSession(false);
      
              connOpts.setUserName(mqttUsername);
              connOpts.setPassword(mqttPassword.toCharArray());
              connOpts.setKeepAliveInterval(60);
      
              mqttClient.connect(connOpts);
          }
      
          /**
           * 汇报属性
           */
          private static void postDeviceProperties() {
      
              try {
                  //上报数据
                  //高级版 物模型-属性上报payload
                  System.out.println("上报属性值");
                  String payloadJson = "{"params":{"Status":1,"Data":"33"}}";
                  MqttMessage message = new MqttMessage(payloadJson.getBytes("utf-8"));
                  message.setQos(1);
                  mqttClient.publish(pubTopic, message);
              } catch (Exception e) {
                  System.out.println(e.getMessage());
              }
          }
      }
      

      运行状态显示
      image.png

      参考链接:基于开源JAVA MQTT Client连接阿里云IoT

      MQ Topic消息订阅
      1、pom.xml

      <dependencies>
              <dependency>
                  <groupId>com.aliyun.openservices</groupId>
                  <artifactId>ons-client</artifactId>
                  <version>1.8.0.Final</version>
              </dependency>
          </dependencies>
      

      2、Code Sample

      import com.aliyun.openservices.ons.api.*;
      import java.util.Properties;
      
      public class ConsumerTest {
          public static void main(String[] args) {
              Properties properties = new Properties();
              // 您在控制台创建的 Group ID
              properties.put(PropertyKeyConst.GROUP_ID, "GID_****");
              // AccessKey 阿里云身份验证,在阿里云服务器管理控制台创建
              properties.put(PropertyKeyConst.AccessKey,"********");
              // SecretKey 阿里云身份验证,在阿里云服务器管理控制台创建
              properties.put(PropertyKeyConst.SecretKey, "********");
      
              properties.put(PropertyKeyConst.ConsumeThreadNums,50);
              // 设置 TCP 接入域名,进入控制台的实例管理页面的“获取接入点信息”区域查看
              properties.put(PropertyKeyConst.NAMESRV_ADDR,
                      "http://MQ_INST_184821781661****_BaQU****.mq-internet-access.mq-internet.aliyuncs.com:80");
      
              Consumer consumer = ONSFactory.createConsumer(properties);
              consumer.subscribe("Topic_MQDemo", "*", new MessageListener() { //订阅多个 Tag
                  public Action consume(Message message, ConsumeContext context) {
                      System.out.println("Receive: " + message);
                      System.out.println("Message : " + new String(message.getBody()));//打印输出消息
                      return Action.CommitMessage;
                  }
              });
              consumer.start();
              System.out.println("Consumer Started");
          }
      }
      

      3、测试结果

      Receive: Message [topic=Topic_MQDemo, systemProperties={KEYS=1202850299555940352, __KEY=1202850299555940352, __RECONSUMETIMES=0, __BORNHOST=/11.115.104.187:51935, __MSGID=0B7368BB19AF531D72CA1D0A9B540077, MIN_OFFSET=520, __BORNTIMESTAMP=1575616834388, MAX_OFFSET=525}, userProperties={UNIQ_KEY=0B7368BB19AF531D72CA1D0A9B540077, MSG_REGION=cn-qingdao-publictest, eagleTraceId=0bc5f2c215756168339094214d0a1b, TRACE_ON=true, eagleData=s3bef5fb6, CONSUME_START_TIME=1575616834470, eagleRpcId=0.1.11.10.10.1.1.1.1}, body=38]
      Message : {"data1":"33","deviceName":"MQDevice"}
      

      日志查询(方便对问题进行跟踪定位排查)

      1、物联网平台 -》 运维监控 -》 日志服务 -》 上行日志
      image.png

      2、消息队列RocketMQ -》 消息查询
      image.png

      AMQP服务端订阅

      1、管理门户配置

      image.png

      image.png

      2、代码订阅,参考链接

      3、Code Sample

      import java.net.URI;
      import java.util.Hashtable;
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import javax.jms.Connection;
      import javax.jms.ConnectionFactory;
      import javax.jms.Destination;
      import javax.jms.Message;
      import javax.jms.MessageConsumer;
      import javax.jms.MessageListener;
      import javax.jms.MessageProducer;
      import javax.jms.Session;
      import javax.naming.Context;
      import javax.naming.InitialContext;
      import org.apache.commons.codec.binary.Base64;
      import org.apache.qpid.jms.JmsConnection;
      import org.apache.qpid.jms.JmsConnectionListener;
      import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      public class AmqpJavaClientDemo {
      
          private final static Logger logger = LoggerFactory.getLogger(AmqpJavaClientDemo.class);
      
          public static void main(String[] args) throws Exception {
              //参数说明,请参见上一篇文档:AMQP客户端接入说明。
              String accessKey = "******";
              String accessSecret = "******";
              String consumerGroupId = "******";
              long timeStamp = System.currentTimeMillis();
              //签名方法:支持hmacmd5,hmacsha1和hmacsha256
              String signMethod = "hmacsha1";
              //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。
              //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。
              String clientId = "yutaodemotest";
      
      logger.debug("demo");
      
              logger.error("error","this is my test error info.");
              logger.info("info");
              logger.error("error");
      
              //UserName组装方法,请参见上一篇文档:AMQP客户端接入说明。
              String userName = clientId + "|authMode=aksign"
                      + ",signMethod=" + signMethod
                      + ",timestamp=" + timeStamp
                      + ",authId=" + accessKey
                      + ",consumerGroupId=" + consumerGroupId
                      + "|";
              //password组装方法,请参见上一篇文档:AMQP客户端接入说明。
              String signContent = "authId=" + accessKey + "&timestamp=" + timeStamp;
              String password = doSign(signContent,accessSecret, signMethod);
              //按照qpid-jms的规范,组装连接URL。
              String connectionUrl = "failover:(amqps://******.iot-amqp.cn-shanghai.aliyuncs.com?amqp.idleTimeout=80000)"
                      + "?failover.maxReconnectAttempts=10&failover.reconnectDelay=30";
      
              Hashtable<String, String> hashtable = new Hashtable<>();
              hashtable.put("connectionfactory.SBCF",connectionUrl);
              hashtable.put("queue.QUEUE", "default");
              hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory");
              Context context = new InitialContext(hashtable);
              ConnectionFactory cf = (ConnectionFactory)context.lookup("SBCF");
              Destination queue = (Destination)context.lookup("QUEUE");
              // Create Connection
              Connection connection = cf.createConnection(userName, password);
              ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener);
              // Create Session
              // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()
              // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)
              Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
              connection.start();
              // Create Receiver Link
              MessageConsumer consumer = session.createConsumer(queue);
              consumer.setMessageListener(messageListener);
          }
      
          private static MessageListener messageListener = new MessageListener() {
              @Override
              public void onMessage(Message message) {
                  try {
                      byte[] body = message.getBody(byte[].class);
                      String content = new String(body);
                      String topic = message.getStringProperty("topic");
                      String messageId = message.getStringProperty("messageId");
                      System.out.println("Content:" + content);
                      logger.info("receive message"
                              + ", topic = " + topic
                              + ", messageId = " + messageId
                              + ", content = " + content);
                      //如果创建Session选择的是Session.CLIENT_ACKNOWLEDGE,这里需要手动ACK。
                      //message.acknowledge();
                      //如果要对收到的消息做耗时的处理,请异步处理,确保这里不要有耗时逻辑。
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
              }
          };
      
          private static JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() {
              /**
               * 连接成功建立。
               */
              @Override
              public void onConnectionEstablished(URI remoteURI) {
                  logger.info("onConnectionEstablished, remoteUri:{}", remoteURI);
              }
      
              /**
               * 尝试过最大重试次数之后,最终连接失败。
               */
              @Override
              public void onConnectionFailure(Throwable error) {
                  logger.error("onConnectionFailure, {}", error.getMessage());
              }
      
              /**
               * 连接中断。
               */
              @Override
              public void onConnectionInterrupted(URI remoteURI) {
                  logger.info("onConnectionInterrupted, remoteUri:{}", remoteURI);
              }
      
              /**
               * 连接中断后又自动重连上。
               */
              @Override
              public void onConnectionRestored(URI remoteURI) {
                  logger.info("onConnectionRestored, remoteUri:{}", remoteURI);
              }
      
              @Override
              public void onInboundMessage(JmsInboundMessageDispatch envelope) {}
      
              @Override
              public void onSessionClosed(Session session, Throwable cause) {}
      
              @Override
              public void onConsumerClosed(MessageConsumer consumer, Throwable cause) {}
      
              @Override
              public void onProducerClosed(MessageProducer producer, Throwable cause) {}
          };
      
          /**
           * password签名计算方法,请参见上一篇文档:AMQP客户端接入说明。
           */
          private static String doSign(String toSignString, String secret, String signMethod) throws Exception {
              SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), signMethod);
              Mac mac = Mac.getInstance(signMethod);
              mac.init(signingKey);
              byte[] rawHmac = mac.doFinal(toSignString.getBytes());
              return Base64.encodeBase64String(rawHmac);
          }
      }
      

      4、测试结果

      Content:{"deviceType":"None","iotId":"QyLOC9FsRiJePV*********","requestId":"null","productKey":"a1QVZRPkS5g","gmtCreate":1575632021248,"deviceName":"MQDevice","items":{"Status":{"value":1,"time":1575632021255},"Data":{"value":"33","time":1575632021255}}}
      

      5、服务端订阅门户监控
      image.png

      更多参考

      控制台配置AMQP服务端订阅

      Rocket MQ订阅消息

      ]]>
      阿里云物联网平台数据转发到消息服务(MNS)示例 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述


      您可以使用规则引擎将数据转到DataHub上,再由DataHub将数据流转至实时计算、MaxCompute等服务中,以实现更多计算场景。本文主要演示通过规则引擎将消息流转到MNS Topic,然后通过Queue订阅Topic中的消息。


      Step By Step




      产品、设备创建及设备数据上行

      参考:阿里云物联网平台数据转发到函数计算示例 产品及设备准备部分。




      MNS 控制台创建Topic及Queue

      1、创建Topic


      _


      2、创建Queue


      _


      3、创建订阅


      _


      _




      规则引擎配置



      1、配置处理数据


      _


      deviceName() as deviceName, items.Humidity.value as Humidity, items.CurrentTemperature.value as Temperature, timestamp('yyyy-MM-dd HH:mm:ss') as time,items.Humidity.time as tttt

      2、配置转发数据


      _


      注意:配置完后启动规则引擎


      3、消息流转情况查询


      _


      _


      _


      Queue消息接收测试

      1、门户快速测试


      _


      _


      2、程序接收测试


      参考链接:队列使用手册


      参考链接


      数据转发到消息服务

      ]]>
      跨域请求出现preflight request失败的问题的解决 Wed, 08 Apr 2020 03:03:11 +0800 问题出现

      这两天在项目联调过程中突然前端同学报告出现CORS跨域问题无法访问。刚听到很奇怪,因为已经在项目里面设置了CORS规则,理论上不会出现这个问题。

          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                          FilterChain filterChain) throws ServletException, IOException {
              String orignalHeader = request.getHeader("Origin");
      
              if (orignalHeader != null ) {
                  Matcher m = CORS_ALLOW_ORIGIN_REGEX.matcher(orignalHeader);
                  if (m.matches()) {
                      response.addHeader("Access-Control-Allow-Origin", orignalHeader);
                      response.addHeader("Access-Control-Allow-Credentials", "true");
                      response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
                      response.addHeader("Access-Control-Allow-Headers", "x-dataplus-csrf, Content-Type");
                  }
              }
          }

      拿到前端给的错误提示后发现了一个奇怪的问题,提示Response to preflight request doesn't pass access control check中的preflight request是什么?

      image.png

      Preflight request介绍

      了解得知跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。同时规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

      一个Preflight request的流程可以如下图所示

      image.png

      什么样的请求会产生Preflight request呢?当请求满足下述任一条件时,即应首先发送Preflight request请求:

      • 使用了下面任一 HTTP 方法:

        • PUT
        • DELETE
        • CONNECT
        • OPTIONS
        • TRACE
        • PATCH
      • 人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:

        • Accept
        • Accept-Language
        • Content-Language
        • Content-Type (需要注意额外的限制)
        • DPR
        • Downlink
        • Save-Data
        • Viewport-Width
        • Width
      • Content-Type 的值不属于下列之一:

        • application/x-www-form-urlencoded
        • multipart/form-data
        • text/plain
      • 请求中的XMLHttpRequestUpload 对象注册了任意多个事件监听器。
      • 请求中使用了ReadableStream对象。

      在我们的例子中正是使用了POST方法传递了一个Content-Type为application/json的数据到后端。而这个OPTION请求返回失败后浏览器并没有继续下发POST请求。

      解决方案

      在弄清楚问题后,我们了解只要给Preflight request优先通过就可以引导后续请求继续下发。对此,我们改造CORS Filter来解决这个问题。

      • 首先对OPTION请求放入HTTP 200的响应内容。
      • 对于Preflight request询问中的的Access-Control-Request-Headers予以通过
      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                          FilterChain filterChain) throws ServletException, IOException {
              String orignalHeader = request.getHeader("Origin");
      
              if (orignalHeader != null ) {
                  Matcher m = CORS_ALLOW_ORIGIN_REGEX.matcher(orignalHeader);
                  if (m.matches()) {
                      response.addHeader("Access-Control-Allow-Origin", orignalHeader);
                      response.addHeader("Access-Control-Allow-Credentials", "true");
                      response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
                      response.addHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
                  }
              }
      
              if ("OPTIONS".equals(request.getMethod())) {
                  response.setStatus(HttpServletResponse.SC_OK);
              } else {
                  filterChain.doFilter(request, response);
              }
          }

      注意事项

      但是天不遂人愿,在上述改造后理论上应该是可以解决Preflight request问题,可以测试发现依然有问题。这时我们注意到错误信息中提到的另外一句Redirect is not allowed for a preflight request.

      为什么会有Redirect事情发生呢,原来所有请求在进入我们的CORS Filter之前,会首先通过SSO Filter做登录检测。而这个Preflight request并没有携带登录信息,导致OPTION请求被跳转到了登录页面。同理如果引用了Spring Security组件的的话也会出现首先被登录验证给过滤的问题。

      找到问题就比较好办了,调整CORS Filter优先级,让其先于登录验证进行就好了。对此我们调整registrationBean的order从默认的Integer.MAX_VALUE到1就好了。

          @Bean(name = "corsFilter")
          public FilterRegistrationBean corsFilter() {
              FilterRegistrationBean registrationBean = new FilterRegistrationBean();
              registrationBean.setFilter(corsFilterBean());
              registrationBean.setUrlPatterns(Lists.newArrayList("/*"));
              registrationBean.setOrder(1);
              return registrationBean;
          }
      ]]>
      做了那么多架构,你真的懂 SOA 了吗? Wed, 08 Apr 2020 03:03:11 +0800 自从提倡 SOA 架构风格以来,个人觉得软件架构并未有特别突破的变革,主要是在 SOA 面向服务架构风格基础上不断演化迭代,基于服务的 EA 明确分层架构也好,微服务也罢,都是在面向服务架构基础上的适应不同的场景的迭代升级。

      我先抛出一个观点,我觉得服务化架构的本质,和西方教育界深受影响的古希腊哲学家苏格拉底的“产婆术”的教育思想本质上是非常相通的:苏格拉底的“产婆术”思想强调教育是一个“接生”的过程,教师就是“接生婆”,人们之所以接受教育是为了寻找“原我”以不断完善自身。也就是教育的目的在于唤醒而不再于塑造。同理服务化架构的本质也不仅仅在采用什么样的技术框架实现和塑造,更重要的是在于通过不停地在共创中反问、反思、反省等方式进行对业务的本质的不断追溯、抽象、综合归纳演绎,我们的每一个架构师都是服务化架构的接生婆,我们的使命是建立真正反映业务本质并驱动业务不断向前的架构。

      我们是否足够深入理解业务的本质,做了足够的归纳演绎以及综合抽象,是否清晰的反应到了我们的服务化的根基:业务模型、域模型以及平台公共语义模型上?这是我们每一个参与服务化的每一个产品、架构师、TL 和核心开发同学需要回答的第一个根本问题。

      定义

      面向服务的架构(SOA):SOA 是一种架构风格,致力于将业务功能保持一致的服务(系统服务,应用服务,技术服务)作为设计、构建和编排组合业务流程以及解决方案的基本单元。

      目的

      我们采用 SOA 的架构是为了什么呢?

      为了更好的复用?为了更好的责任切分?为了接口和实现的分离,提升灵活性和隔离性?还是为了更好的接口分类和管理?

      以上说法其实都没错,但是面向服务化的架构 SOA 的目的远远超过接口技术细节的设计与定义,其核心的关注点在于服务的业务内容以及内涵,而不仅仅是如何设计和实现。

      同时,SOA 更多的也不是如何构建一个服务,任何人都可以很容易地创建一个服务,这并不是 SOA 的核心挑战,而是如何赋能企业构建有业务价值意义的完整业务语义的服务集合。

      面向服务的架构致力于在企业内的不同的业务环境内,建设业务功能驱动的服务,从而将服务组装成有价值、更高级别的业务流程和解决方案平台。

      面向服务的架构的真正的价值体现在当可重用的服务被灵活组合、编排在一起来构建敏捷的、灵活的业务流程,其中敏捷体现在服务可以快速调整,独立演化;灵活性体现在服务由于其业务功能定义明确,边界清晰且功能内聚性强,同时服务具备各自独立完整生命周期,可被灵活组装。

      如果面向服务架构能为企业提供了重大的价值,那么这些价值通过什么来体现的呢?

      价值体现

      • 行为一致性

      面向服务的架构允许我们为业务流程、任务或者决策拥有唯一的共同的入口,也就是,不管服务访问的路径如何,服务给业务提供的业务行为都是一致的。

      • 数据一致性

      面向服务的架构允许我们为业务数据信息提供单一的访问入口,也就是它提供给业务一致的、企业内部共识的公用数据访问。

      • 模块化及敏捷性

      面向服务的架构 SOA 为业务功能、业务决策和业务信息的模块化提供了非常好的机制。同时,在模块化实现好的情况下,这些模块可以在多个业务流程和场景中被灵活复用和重新组合,从而为业务竞争力和创造性提供灵活性和敏捷度支持。

      • 功能与数据的解耦

      面向服务的架构 SOA 提供了业务功能和信息集成的同时,减少了他们之间的依赖和耦合性。也就是,独立的业务功能单元,应用系统,可以一起协同工作,同时各自又具备各自的演进计划,生命周期和业务目标。

      • 高度可管理性

      SOA 提供给我们通过定义服务水平协定在服务模块粒度支撑我们的业务目标,我们可以不断的设定、监控和优化调整组件,应用以及系统所承载服务的考核。

      其中行为一致性和数据一致性作为服务的核心价值根基。

      服务

      一、定义

      首先我们先定义一下服务是什么?

      服务是通过服务契约的方式来提供业务功能的独立单元,同时受服务契约所明确管理。

      服务是设计、构建和编排组合一个完整业务实体中业务解决方案的基础单元。服务契约指定了服务消费方和提供方之间所有的交互约定,包括:

      • 服务接口
      • 接口文档
      • 服务策略
      • 服务质量
      • 服务可用性
      • 性能

      那我们经常听到模块、组件等其他的软件构件,服务和他们有什么区别呢?其中最核心的区别在于服务本身是被明确管理的,其服务质量和性能是通过服务水平协定(SLA)被明确管理的,而模块以及组件并无此约束。此外,服务的全生命周期包含从设计、部署到增强升级和维护都是可管理的。

      举例(下列内容仅做示例展示用,非适用于严格场景):

      补货计算服 服务策略 服务质量 性能要求
      补货建议量计算服务 针对行业下商家/供应商维度的入仓货品补货建议计算 在销量预测符合分布要求且满足准确率水平要求的情况下,根据缺货率服务水平要求的产生的补货建议量符合业务期望的周转天数 10W + 货品 * 30 仓,品+仓补货及建议量 <= 30min
      订单创建服务 包含购物车下单+立即下单场景,满足所有优惠计算后的订单生成 订单创建成功率 99.999999999% 峰值支撑:100w 单/s

      二、服务构成

      服务自身主要包含两个主要方面,第一方面也是服务最核心的方面就是服务的接口,另外一方面则是服务的实现。服务非常好的实现了接口和实现的分离。

      image.png

      1)服务接口

      服务接口指定了服务的操作,也就是服务是做什么的(What),操作的输入输出参数,以及用来约定如何使用和提供这些能力的协议。

      服务通常包含围绕着一个核心的业务功能操作以及相关联的操作。例如补货建议计算服务中核心的操作是生成货品+仓维度的补货建议单,其他相关操作包含查询补货建议单相关销量预测操作,查询补货建议单对应计划库存操作。

      服务 核心功能操作 关联操作
      补货建议计算服务
      品+仓维度补货建议计算
      补货建议单对应销量预测查询
      补货建议单对应计划库存操作

      2)服务实现

      服务实现指的是服务如何通过其明确定义的接口提供其能力。服务实现可以通过以下方式实现:

      • 完全基于编码实现
      • 基于其他服务的编排而成
      • 基于已有应用适配封装而成
      • 以上情况混合实现

      核心点是服务如何被实现的对于服务消费方来说是透明的,服务消费方仅仅需要关心的是服务是做什么的,而不是如何被实现的。

      服务可以提供在保持服务接口或者行为约定不改变的情况下,提供根据不同的行业不同场景提供各种不同的实现。

      服务实现在保持服务接口或者行为约定不改变的情况下,可以自由进行升级和切换。服务实现既可以是静态的更新升级,也可以使动态路由实时切换实现,如对应到不同的行业以及不同的业务场景的自动实现切换。

      不管服务实现如何升级或者按需自动路由切换,只用服务的行为和契约不会发生改变,用户也就是服务的消费者根本不会感知到任何不同。

      我们可以把服务接口想象成室内普通电源国标插口,服务策略为室内非防水情况下适用,服务契约想象成 24X7 的 220v 电压供电能力(其中 180V~250V 50Hz 是质量要求,24x7 稳定性要求,电流供给 <= 10A 是性能要求),此国标插座(服务提供方)可以给包含与此接口匹配且符合契约的任何电器(消费方)交互并提供供电能力,支持其运转。

      服务接口定义了交互的的风格和细节,而服务的实现定义了一个特定的服务提供方或者特定的业务实现如何提供其能力。

      这种类似连接点/插口的设计极大的方便了更松耦合的业务功能解决方案。

      三、服务接口与服务实现的逻辑构成

      服务接口与实现的构成也有两个重要的不同方面,分别是执行功能的方法和执行的信息数据。换句话说,一个服务是由一个业务服务操作集合以及对应操作的输入输出的抽象业务服务数据模型组成。这层业务服务数据模型是企业业务层次或者平台业务层次的业务实体的抽象,独立于底层数据存储与实现。此业务数据模型是和各子域密切相关联,但是超越各子域以上的,在完整的业务线或者平台层次上达成一致的业务数据模型,也就是说在各子域之间达成共识且约定的严格明确的公共模型,主要用于平台业务流程中不同域服务的交互,是平台层次统一的业务语言,我把它暂时称为平台业务数据模型。 此平台业务数据模型通常需要包含平台统一语义的业务术语表,平台各域核心实体表,平台各域核心实体交互图等。

      image.png

      接口与实现的逻辑构成:

      1)服务操作

      服务操作声明定义了这个操作的输入以及输出参数。

      2)平台业务实体模型

      平台业务实体模型描述了服务中输入输出数据的结构以及含义。服务接口中的信息和服
      务实现中逻辑数据之间的差异是至关重要的。

      在服务接口层次上,最重要的是信息必须在业务服务之间进行交互来赋能业务流程并完成业务流程。这些信息必须在参与流程的所有业务服务间达成一致且在服务之间通用,也就是平台层次所有服务公用且标准的业务实体模型,同时此业务实体模型必须在平台业务语义上明确且完成,确保可以支撑平台所有端到端的业务。此平台层级的业务实体模型并不是一蹴而就的,但是可以随着平台的重心变化不断迭代完善成型的。

      然而不同的是,从内部来看,很多服务在各自实现的子域内部都有这些信息的不同的超集,可能潜在的存在不同的数据格式。幸运的是,我们不需要感知也不需要在所有关联服务的相关子域实体模型上达成共识,即使不是不可能,但是也不太现实。与之相反,服务接口和服务实现的分离设计允许非常方便的进行平台业务实体模型和服务所在子域领域模型进行映射转换。

      3)服务接口最后一个重要的方面就是服务水平协议 SLA。服务水平 SLA 协议指定了服务的的两个重要方面的指标,分别是业务上的指标和技术上的指标:

      • 技术指标:响应时间RT,并发吞吐量 Throughput,可用性 Availability,可靠性 Reliablity。
      • 业务指标:完成的业务功能的质量或者完成度,如产生的补货建议是否满足业务预期的周转缺货KPI要求:周转下降 10 天,缺货率下降5%。

      服务化分层架构

      理解服务化分层架构,首先要对 TOGAF Meta-Model 有个清晰的理解,从元模型可以看出业务服务和业务流程的上承业务,下启系统平台的核心作用,一定要深刻理解业务服务和业务流程在企业架构中的重要性,下面我把我翻译后画的版本给大家放在这里,给大家做个参考,TOGAF 不多做解释,如有需要,大家可以交流,后面有时间尝试写下我对 TOGAF 的学习和理解。

      image.png

      通常情况下,我们会按照不同行业的不同的业务流程去搭建系统,如供应链最初在大家电 3W 行业孕育,我们按照 3W 的行业和业务场景搭建了平台商家相适应的计划系统;后续自营行业又根据自己的行业也搭建了自营的计划系统;后续小电数码、国际以及其他业务快速发展,跟随业务快跑的同时,也各自建立的各自的业务流程。在这个过程中,BPM 为建造不同的业务系统提供非常好的抽象支撑,但是经常的结果是,BPM 被用作构建了更高层抽象的,也更高效的,但是却是烟囱式的应用,而不没有更好的贡献更多的支撑到整体上能快速应对业务变化而更灵活,更敏捷的业务平台或者系统。

      而这正是面向服务的架构中业务规则以及决策作为服务要发挥更大作用的地方。面向服务的架构允许我们将特定业务流程中的业务规则和业务决策抽象分离出来变成业务规则或者决策服务,这些规则和决策服务就可以被灵活应用到不同的业务流程中,从而这些服务可以被统一管理和演化升级。

      BPM + SOA 一起提供了支撑企业架构的完美组合。BPM 提供更高层抽象定义业务流程的能力,以及与流程相关联的重要监控和管理能力;业务服务提供了支撑业务流程的核心的功能、决策以及信息。面向服务的架构则提供能力将服务组合在一起来支撑和创建灵活且敏捷的端到端的企业业务。如果只有 BPM 而没有 SOA 对于创建单独的业务应用或许非常有用,但是通常是创建的烟囱式的应用,很难扩展到企业内或者平台内不同的业务线。如果只有 SOA 而没有BPM虽然可以创建可重用且一致性高的服务,但是缺少将这些服务快速搭建业务流程并支撑端到端业务的能力,也无法支撑建立具有竞争力且可以随着外部竞争环境进行敏捷反应的业务。

      下图显示了一个建议的的封层服务化架构图,各分层如下:

      • 端到端业务流程

      业务流程是按照一定业务规则决定的顺序执行的业务操作组成。高层级的业务功能,通常跨越应用域或者业务线。通常由行业开发团队开发,此行业开发团队可以具备明确的实现组织结构,也可以由跨团队的相关域共同组成虚线团队。例如,电商业务中,用户选购下单交互流程;供应链业务中的补货调拨计划流程等。

      • 平台业务服务

      高度模块化的业务功能单元,由不同类型的子域服务组合编排而来,可作为业务流程的编排单元。跨行业通用的业务服务可由功能所在核心域开发团队编排开发,行业内通用的业务服务可以由行业开发团队负责编排开发。例如,补货审批服务

      • 子域服务

      平台各功能子域提供的服务,对平台可见,用于平台业务服务的组合编排,也可以作为更高层的业务流程编排的基础单元。子域服务通常由平台各子域开发团队负责开发。例如,销量计划服务,补货建议计算服务。

      • 子域基础服务

      用于支撑各功能子域服务的基础服务,对子域可见,对平台不可见,用于子域服务的编排。

      子域基础服务通常由平台各子域开发团队负责开发。例如,入仓决策服务,计划单据服务,计划库存服务等。

      • 基础子域服务

      或称为基础业务域服务,提供平台基础业务服务,为各个功能子域或平台业务服务提供基础业务功能及数据服务。例如:商家服务,货品服务,库存服务等。

      • 基础架构服务层

      提供不同层次所公用的基础架构服务,如用用户管理,权限管理,操作审计等等。

      image.png

      我们通常按照上述分层结构来描述平台架构或者企业内部架构,看上去好像层次结构清晰明了,但是却是不完整的,因为此面向服务的架构描述缺失了平台系统架构中一个核心部分,暨信息及信息模型分层,这一点非常之关键,往往会决定架构的成功与否。

      为了使架构更完整同时也更真实,我们需要添加对应的完整信息抽象(实体模型 or 领域模型):

      • 核心单据模型

      端到端业务流程中操作的核心单据,承载业务核心价值的信息单元模型,例如,销售订单,采购订单,补货计划单等。此模型通常是平台公共语义模型的核心子集。

      • 平台公共语义模型

      定义了平台层业务流程、业务服务交互数据。在平台层面或企业层面,端到端业务流程中交互信息的公共语义模型,此模型不仅对平台业务流程中交互的各实体进行了明确的定义,而且包含了业务流程中所需要的完整的业务语义实体,同时各业务语义实体边界明确,责任清晰。核心单据模型通常是平台公共语义模型的子集。平台公共语义模型包含下层子域的对外服务实体子集,按照端到端的完整平台业务语义,可由平台各功能子域模型所共享给平台的核心实体子集有机整合而成,也可由平台业务模型全新定义,或者从 TOP-DOWN 以及 BOTTOM-UP 两个方向共同融合而成。需要注意的是此模型必然是无法一蹴而就,需要经过无数迭代而不断完善,但其一定是不可或缺的。平台的诸多架构决策和不断演化完善需要基于此模型来进行。

      • 子域领域模型

      平台各功能子域的领域模型,用于驱动各功能子域的应用系统设计和开发。子域领域模型需要保持动态稳定,通过防腐层同所依赖的外域或者外部服务进行隔离,防止外部服务污染子域内的核心业务语义,同时保持域内业务功能灵活可控。子域领域模型仅通过其对外服务实体子集对外可见,其余对外不可见。

      • 跨域映射模型

      用于各子域领域模型实现对外部模型的防腐依赖。

      • 基础架构服务层

      提供不同层次所公用的基础架构信息模型,如用户模型,权限模型等。

      image.png

      信息架构模型框架

      现在来讨论下服务化分层架构重视度并不太高的另一个重要侧面:信息架构,之所以说信息架构非常之重要,是因为信息架构与服务化架构是一个密不可分的完整的整体。我对信息架构模型进行了分层划分,下面从 TOP_DOWN 方向来讨论不同的分层模型。

      image.png

      • Level 0:战略与决策模型(高层战略视角)

      这层次模型用于定义企业的战略方向和商业目的,从而定义了企业内任何系统平台开发的方向和终局。这必然作为企业内任何系统平台开发的基本背景和基调,影响任何系统平台开发项目的中长期目标定义和终局设定。

      • Level 1:商业模式(业务线 owner 视角)

      这层模型从业务线 owner 的视角,用运营主体的业务术语描述其商业模式的本质,包括其整体结构,业务流程,以及组织结构等。

      • Level 2:业务抽象概念模型

      这层模型从业务架构的视角用信息化的方式对单个业务线或者多个业务线的业务进行抽象。Level 1 描述是对于企业业务来说有意义的东西或者事情,而 Level 2 则给予这些有意义的东西以更严格且清晰的定义,明确其内涵以及外延并体系化,同时根据不同行业线的业务内容进行提取抽象,抽象出共性的内容,用于更高效灵活的描述和定义业务 。

      Level 1 描述的是业务运营人员所感知的业务流程,Level 2 不仅描述了这些业务流程,更重要的是抽象并描述了了这些业务流程所应该包含的底层业务功能。

      同样的,Level 1 描述对企业业务来讲所有重要的东西,Level 2 描述的是组织想要管理的信息后面最根本的内容。Level 1 描述的事情是 Level 2 定义的基本实体的实际业务中对应的样本或事例。

      简而言之,Level 2 是 Level 1 的抽象(Abstraction)与综合(Synthesis)。 为了达成这一视图,必须要仔细分析和归纳,有时候需要演绎的方式来定义出隐藏在企业业务运营主体视图下根本结构和内容。

      • Level 3:平台公共语义模型

      Level 3 层公共语义模型同 Level 2 层业务概念模型保持紧密一致,在此基础上增加了服务化视角的语义。Level 3 公共语义模型描述的内容是在必须在平台层业务服务间共享的具有一致语义的业务实体和信息,是平台层一致的共享信息模型。这层模型用于描述平台层服务接口交互的共享信息,基于平台完整业务语义下所有服务所公共数据的标准化视图模型。简而言之,平台公共语义模型,定义了业务平台层次基本业务服务语义,是平台各业务服务之间,平台业务流程和平台业务服务交互的统一语言。

      • Level 4:域模型

      Level 4 层域模型定位于平台各子域的领域模型/实体模型,用于对各子域的核心业务功能进行抽象。域模型是平台各子域的标准模型,不仅明确定义的各子域功能服务暨服务接口的语义,同时也包含各子域内服务实现中的关键实体的定义。域模型从整体上来说是平台各子域的私有模型,除了服务语义外整体不对外可视。公共信息中的服务视图是域模型的子集。

      域模型核心用于除了用于暴露到平台子域的业务服务设计与实现外,同时也用于驱动域内服务功能的设计和实现。

      域模型是需要保持动态稳定的,除非域内业务发生本质变化,域模型应该是相对稳定的。域模型稳定性最大的敌人是外部的依赖,如何不受外部依赖的侵蚀而逐渐腐败,域防腐层存在的最主要原因。子域防腐层维护外部依赖服务和子域模型之间的动态映射,维护域模型的独立性,保护域模型不受有害侵蚀。

      域模型我理解基本和我们通常谈的领域模型基本接近,对于各域内业务的抽象,驱动各域技术设计方案设计和实现,至于具体的模型表现形式,采用基于亚里士多德的物质本源的思想(“Material Cause,Formal Cause,Efficient Cause,Final Cause" —> 实体+属性+关系)的ER图,还是基于我们老祖宗老子道家思想("人法地、地法天、天法道、道法自然" —> 实体+行为)的思想的领域驱动 DDD 的方式,个人认为各有伯仲,组中能清楚表达出业务本质即可,后面单独写一篇抽象建模的文章聊一下这两种不同的思想。

      • Level 5:实现模型

      此层模型为开发者视角的实现模型,也就是我们系统实现核心的对象模型,是我们系统落地的基石。

      设计服务

      我们初步了解的什么是服务,以及什么是服务化的分层?那如何设计服务以及服务化架构呢?下面给出基本步骤和方案。

      一、理解整体背景

      首先,我们要理解服务化架构的整体背景。我们必须理解我们所支撑的业务和业务根本驱动力以及所有的业务流程,业务场景以及业务用例;同时对于平台系统,我们还必须理解公司的战略所赋予平台的使命是什么?我们平台中长期的目标是什么?平台的终局是什么?这些组合和在一起才是服务化架构的完整的上下文背景。这些必须要反映到我们的业务模型、平台公共语义模型和各域模型中去。

      然后,我们需要提出并回答如下问题:

      • 我们当前支撑的是什么样的业务?(业务模型)
      • 这个业务或者这些业务的中长期目标和短期目标分别是什么?
      • 平台的短中长期目标是什么?平台的终局是什么?
      • 上述目标是否存在冲突,如何平衡和取舍?
      • 实现这些目标,需要完成什么样的成果?
      • 这些成果如何衡量?
      • 取得这些成果,需要什么样的能力和信息?
      • 实现这些能力需要什么样的流程、服务、实体以及规则
      • 现有的服务、应用或者系统提供了那些基本能力和信息?

      前面六个问题描述了整体的架构需求(包括业务和平台),而剩下的问题则描述了整个服务化架构的上下文以及引入了服务目录库的需求。我们服务不能只从单个服务的角度来看,而必须从整个服务集合的角度来反应完整的业务语义和平台语义。我们的服务集合也就是服务目录库必须具备完整的上下文语义,必须能识别出:

      • 整体的上下文背景,包括完整的业务语义和平台语义。
      • 服务职责范围
      • 关联的服务的分组
      • 服务的类型和角色

      服务目录库的设计必须支持两个主要的设计时目标:

      • 第一个目标是要提供一种机制来帮助理解服务整体的上下文背景,用于更好的服务选择及更高效的服务重用。特别是,这个服务实现了什么样的责任,以及如何和其他的服务相关联。
      • 第二个目标是要提供一种机制来识别一个特定服务的责任边界,用来指引服务的实现。这是一个非常关键的点,特别是在避免服务的功能和数据重复上非常重要,不仅仅是避免重复建设,更核心的是要以此保证业务功能和数据的一致性。

      服务目录库中的服务可以按照服务类型以及服务角色来进行组织。服务类型请参照服务化分层架构内容里的描述;服务角色包含任务服务角色、实体服务角色和决策服务角色,请参照后面小节描述。

      二、服务设计原则

      面向服务化的架构的其中一个成功的关键是创建一个具备完整业务语义的服务集合以便于可以方便一起进行组合编排来支撑不同的业务流程以及丰富的业务场景。

      我们经常谈论各功能域要提供松耦合的服务,是因为服务间的松耦合是非常重要的,特别是通过减少服务间的依赖以便于服务可以在不同的场景中被复用,以及可以起到隔离变更影响的作用。但是如何才能尽可能的实现这个目标呢?

      首先我们来看下对于服务最重要点是什么?首先就是这个服务提供了什么样的业务功能,其次这个服务对业务有价值的数据产生了那些影响。从这两个点上我们就可以比较
      容易得出两种类型的耦合在服务接口设计中是特别重要的:

      • 数据依赖
      • 功能依赖

      举例来说明下:

      交易服务协调所有的活动,然后依赖其他服务来帮助完成流程。交易服务依赖于或者说耦合于用户服务,商品服务,库存服务,营销服务、订单服务以及支付服务等。

      为啥交易服务没有实现所有的功能?

      首先是因为我们想在其他高级别流程或者服务中重用底层的能力。

      第二是交易服务服务并不负责用户服务,商品服务,库存服务,营销服务、订单服务以及支付服务。交易服务只是使用它们,而不是负责实现它们。

      用户服务被用作管理客户信息访问,它具有唯一的责任来提供、维护和更新客户信息,这样做的目的是为了可以在任何需要访问客户数据服务的地方重用客户服务。比代码重用更重要的是隔离或者是集中式访问客户信息,因为只有唯一的路径访问数据,数据就总是一致的,真正实现 Source Of Truth。因此,尽管有很多服务包含交易服务,购物车,订单历史等服务需要访问客户服务,通过松耦合的这种模式去管理这些依赖是比较容易被理解的。

      通过创建服务来执行用户管理,商品管理,库存管理,以及营销管理等,就可以在任何可以用到的地方,执行保持一致性的这些业务功能。

      敲黑板:好的服务设计并不仅仅是关注重用性,更重要的是要提供一致性,既包含功能一致性,也包含数据一致性。

      那么下一个问题是你如何决定有哪些服务以及这些服务分别是什么呢?同样,你用功能分解和信息隔离组合在一起来决定服务有哪些并且各自是什么?

      • 对线上交易功能的分解引导去识别用户、商品、库存、营销、订单以及支付等相关功能服务。
      • 对信息的隔离引导我们去识别用户和商品等作为交易订单中的共享信息。
      • 面向服务的架构中服务设计的问题需要跨越多个以致于所有的流程中来一起考虑。

      因此,服务设计原则基本原则如下:

      • 避免服务间的功能重复
      • 避免服务间的功能缺失
      • 避免数据重复
      • 实现数据的协同访问
      • 具备统一、一致的方式来执行给定的功能

      在服务化设计中,如何实现上述的这些原则呢?答案是提出并回答如下问题:

      • 谁负责这个功能?
      • 这个功能在哪里被用到的?
      • 谁负责管理这些指定的数据?
      • 谁负责定义和实现那些特别的业务规则
      • 流程中的哪个步骤具备执行这个任务所需要的特定的知识

      这些问题的答案会帮你来识别如下信息:

      • 服务应该做什么?
      • 服务对什么负责?
      • 同样重要的是,识别服务不应该做什么,而应该依赖其他的服务来支撑

      三、服务颗粒度与类型

      我们通常设计服务时候一个很大的疑惑是我的服务到底要设计成什么样的颗粒度,应该更粗粒度一些,还是更细粒度一些?答案是:没有一个统一正确的服务颗粒度标准。那怎么办?我如何设计我的服务的颗粒度呢?虽然没有统一的标准,但是我们可以依赖下面的因素来决定合适的服务粒度:

      • 谁是服务的潜在消费方?其他服务,业务流程还是外部合作方?
      • 服务在哪里被消费,通过什么样的路径被消费,也就是服务的拓扑结构是什么?
      • 服务的性能要求是什么?
      • 服务预期的业务范围或者边界是什么?

      在几乎任何复杂的环境或者系统平台中,我们可以预期到多种多样类型的服务。这些服务具有不同的类型和颗粒度,可以参考服务化分层中的内容,也可以见下面的描述:

      • 端到端的业务流程

      业务流程通常跨越整个企业或者平台多个业务域,通常是由底层服务构建而成

      • 平台业务服务

      业务服务是最粗粒度的服务,业务服务提供高度抽象的,组合的业务功能给到平台或者企业。业务服务的功能和数据同业务流程所需要的业务语义紧密结合。数据整合服务在这个层次提供端到端的业务流程所需要的整合后的数据。

      • 子域服务

      子域服务是中等粒度的,他们提供特别针对于每个业务子域的业务相关服务,被本域内的不同业务服务所使用,但是未必暴露出子域外

      • 子域基础服务

      子域基础服务通常是最小粒度的服务,他们提供更低层次的服务,用来提供子域内子域业务功能的基本功能支撑

      • 基础子域服务

      子域基础服务通常也提供教小粒度的服务,用于支撑上层业务功能服务的业务功能完整实现。

      • 基础架构服务层

      基础架构提供了在更高层级服务构建中细粒度的能力,独立于任何业务域。这些服务需要和业务相关明确区分开来,例如安全认证,权限管理以及纯粹技术编排服务。

      四、服务角色

      独立于服务的粒度,职责范围以及服务创建以外的另外一个重要考量或者说是侧面是:服务在服务组合或者流程编排中所承担的角色是什么?

      那么怎么来区分不同的角色呢?我们使用关注点隔离的架构原则。例如,我们在构建应用中就使用了将数据同逻辑隔离作为重要的概念。这样不仅提供了不同关注点解耦的可能以及机会,而且允许采用不同的方式,在不同的地方来实现这些不同的关注点。

      对业务流程进行单独管理的BPM就是一个非常好的例子,BPM作为另外一个关注点分离的例子,将业务流程方案从其他逻辑中分离出来,可以使工作流程可以在一个特定的层次或者环境内进行执行和管理, 这样就可以实现通过快速的建立新的流程模型来快速响应业务的变化。同时面向服务的架构SOA提供了将业务服务作为构建业务流程的基础构件的功能。业务规则系统BRMS同样也作为一个关注点分离的例子,将业务规则或者业务决策从其他应用逻辑中区分开来,这样业务规则和业务决策也可以在一个特定的层次被执行和管理,从而就可以很容易的被变更来支持新的业务需求。这里,业务规则以及决策服务也是面向服务的机构来暴露出规则和决策服务来支撑规则和决策与业务流程的分离。

      通常我们通过较粗粒度的来定义三大类服务角色来构建不同的服务层次:

      • 任务服务角色

      任务服务通常实现一个完整的业务功能,既可以是基本业务功能,也可以是复杂的业务功能,如计算某个货品在某个仓的补货量,或者一个简单的业务校验,如此货品在此仓是否可补。

      此服务类型颗粒度范围较广,包含从独立的子域基础服务到大的平台业务服务都可以具有任务服务角色,更小颗粒度的服务倾向于具有更通用的目的,更大的可重用的潜力。业务服务几乎总是承担任务服务的角色,通常是小颗粒度服务较大的组合,可以被设计成支持一个或者更多特定的流程。因此这些服务通常在跨业务流程中广泛复用的潜力更低。但是也是正常的,因为他们通常是有其他可重用的服务组成的。

      通常,具有业务角色的服务是主动服务,通过主动行为来提供价值

      • 实体服务角色

      主要管理访问业务实体的服务具有这个角色。业务实体的例子如用户、类目、商品、价格、库存、购物车,主要对应主要的业务信息。实体通常是中到大型实体,倾向于独立于任何特定的业务流程,而可做为多个不同业务流程的组成部分。具有实体服务角色的服务通常通过适配和提供需要的信息来实现任务的方式来支撑任务服务。实体服务通常都具备较大的重用的潜力。

      • 规则 / 决策服务角色

      规则 / 决策服务是通过执行业务规则来提供业务决策的服务,如补货计划自动审核服务。

      规则 / 决策服务通常用作对复杂问题进行判断或者支持变化频繁的业务规则,如复杂且多变的审核规则等。

      规则 / 决策服务通常为小到中等大小颗粒度,通常用来组装成更大的服务。规则/决策服务是可以不同层次不同类型的服务,包括平台业务服务,子域服务,子域基础服务等,但是通常情况下规则/决策服服务也来支撑这些服务类型。

      image.png

      我们通过组合这些不同类型的服务角色来提供灵活的业务能力,从而用来支持业务流程内的活动。我们提供了一些基本原则来帮助我们进行服务组合以便于帮我们减少依赖,限制耦合以及最大化灵活性。

      服务层次以及组合基本原则:

      • 业务流程的任务通过任务服务实现,业务流程路由的核心规则由规则/决策服务来提供,而不是定义在流程网关内。这一块内容后续详细说明。
      • 更高层次的任务为核心的业务服务由其他更小的服务组成
      • 服务依赖严格单向原则,上层服务可以依赖下层次服务以及同一层次服务,但是下层服务不可以依赖上层服务
      • 一个任务服务可以组合规则/决策服务、实体服务以及其他任务服务
      • 但是一个实体服务不允许直接调用其他实体服务

      现在我们可以通过丰富的流程,实体和决策服务的集合,可以创建新的不同的服务组合,把规则的灵活可变的好处同服务化架构的模块化,灵活性以及重用性结合起来作为业务系统平台级别的基本架构方式。

      服务化如何成功?

      一、大规划

      大的规划首先要明确 2-3 年内的服务化的目标。大的规划切记事无巨细,而是根据长期规划设定明确的指导性原则和要求,在体系化的基础上鼓励协同和创新。

      二、小目标

      服务化不应该是运动式的大跃进推进,而应该是坚持试点、推广、总结、扩大试点,从而由点到面,逐步落实的方法,由各域根据规划的体系化要求,再各自情况暨各自成熟度来设定各自服务化目标,制定一个个小目标,快速迭代,敏捷式的总结推进。

      三、真共识

      建立共识的根本是要讲清楚服务化的目标、架构、设计、开发背后的清楚的逻辑,让每个人想的清楚,听的明白。

      四、接地气

      接地气同达成共识一样,要用朴素的工程师语言讲清楚目标和逻辑,而不是拿各种看上去非常光鲜亮丽的各种名词来充当台面,讲的人解释不清楚,听得人一头雾水,没有体系化逻辑来支撑落地,最终很难达到服务化真正的目标的。

      五、结硬寨

      服务化是一个庞大的,迭代的,渐进的体系化工程,不是快闪战,不是突袭战,是场持久战,一定要有曾国藩的“结硬寨,打呆仗”的耐心和准备,踏踏实实落地迭代推进,小步快跑,在坚持体系化思考的基础上进行持续总结改进,通过一个接一个战斗,一个小胜利接一个小胜利,一个战役接一个战役不停的攻城略地的基础上逐渐迈向成功。

      六、Think Fast & Slow

      一句话,高效的方式就是慢想、快干。我们不一定缺少高执行力的人,但是一定缺少能独立思考并体系化行事的人。

      ]]>
      从零开始入门 K8s | 理解 RuntimeClass 与使用多容器运行时 Wed, 08 Apr 2020 03:03:11 +0800 4.7头图.png

      作者 | 贾之光  阿里巴巴高级开发工程师

      本文整理自《CNCF x Alibaba 云原生技术公开课》第 30 讲,点击直达课程页面
      关注“阿里巴巴云原生”公众号,回复关键词“入门”,即可下载从零入门 K8s 系列文章 PPT。

      一、RuntimeClass 需求来源

      容器运行时的演进过程

      我们首先了解一下容器运行时的演进过程,整个过程大致分为三个阶段:

      1.png
       

      • 第一个阶段:2014 年 6 月

      Kubernetes 正式开源,Docker 是当时唯一的、也是默认的容器运行时;

      • 第二个阶段:Kubernetes v1.3

      rkt 合入 Kubernetes 主干,成为了第二个容器运行时。

      • 第三个阶段:Kubernetes v.15

      与此同时,越来越多的容器运行时也想接入到 Kubernetes 中。如果还是按 rkt 和 Docker 一样内置支持的话,会给 Kubernetes 的代码维护和质量保障带来严重挑战。

      社区也意识到了这一点,所以在 1.5 版本时推出了 CRI,它的全称是 Container Runtime Interface。这样做的好处是:实现了运行时和 Kubernetes 的解耦,社区不必再为各种运行时做适配工作,也不用担心运行时和 Kubernetes 迭代周期不一致所带来的版本维护问题。比较典型的,比如 containerd 中的 cri-plugin 就实现了 CRI、kata-containers、gVisor 这样的容器运行时只需要对接 containerd 就可以了。

      随着越来越多的容器运行时的出现,不同的容器运行时也有不同的需求场景,于是就有了多容器运行时的需求。但是,如何来运行多容器运行时还需要解决以下几个问题:

      • 集群里有哪些可用的容器运行时?
      • 如何为 Pod 选择合适的容器运行时?
      • 如何让 Pod 调度到装有指定容器运行时的节点上?
      • 容器运行时在运行容器时会产生有一些业务运行以外的额外开销,这种「额外开销」需要怎么统计?

      RuntimeClass 的工作流程

      为了解决上述提到的问题,社区推出了 RuntimeClass。它其实在 Kubernetes v1.12 中就已被引入,不过最初是以 CRD 的形式引入的。v1.14 之后,它又作为一种内置集群资源对象 RuntimeClas 被引入进来。v1.16 又在 v1.14 的基础上扩充了 Scheduling 和 Overhead 的能力。

      2.png

      下面以 v1.16 版本为例,讲解一下 RuntimeClass 的工作流程。如上图所示,左侧是它的工作流程图,右侧是一个 YAML 文件。

      YAML 文件包含两个部分:上部分负责创建一个名字叫 runv 的 RuntimeClass 对象,下部分负责创建一个 Pod,该Pod 通过 spec.runtimeClassName 引用了 runv 这个 RuntimeClass。

      RuntimeClass 对象中比较核心的是 handler,它表示一个接收创建容器请求的程序,同时也对应一个容器运行时。比如示例中的 Pod 最终会被 runv 容器运行时创建容器;scheduling 决定 Pod 最终会被调度到哪些节点上。

      结合左图来说明一下 RuntimeClass 的工作流程:

      1. K8s-master 接收到创建 Pod 的请求;
      2. 方格部分表示三种类型的节点。每个节点上都有 Label 标识当前节点支持的容器运行时,节点内会有一个或多个 handler,每个 handler 对应一种容器运行时。比如第二个方格表示节点内有支持 runc 和 runv 两种容器运行时的 handler;第三个方格表示节点内有支持 runhcs 容器运行时的 handler;
      3. 根据 scheduling.nodeSelector, Pod 最终会调度到中间方格节点上,并最终由 runv handler 来创建 Pod。

      二、RuntimeClass 功能介绍

      RuntimeClass 的结构体定义

      3.png

      我们还是以 Kubernetes v1.16 版本中的 RuntimeClass 为例。首先介绍一下 RuntimeClass 的结构体定义。

      一个 RuntimeClass 对象代表了一个容器运行时,它的结构体中主要包含 Handler、Overhead、Scheduling 三个字段。

      • 在之前的例子中我们也提到过 Handler,它表示一个接收创建容器请求的程序,同时也对应一个容器运行时;
      • Overhead 是 v1.16 中才引入的一个新的字段,它表示 Pod 中的业务运行所需资源以外的额外开销;
      • 第三个字段Scheduling 也是在 v1.16 中被引入的,该 Scheduling 配置会被自动注入到 Pod 的 nodeSelector 中。

      RuntimeClass 资源定义例子

      4.png
      5.png

      在 Pod 中引用 RuntimeClass 的用法非常简单,只要在 runtimeClassName 字段中配置好 RuntimeClass 的名字,就可以把这个 RuntimeClass 引入进来。

      Scheduling 结构体的定义

      顾名思义,Scheduling 表示调度,但这里的调度不是说 RuntimeClass 对象本身的调度,而是会影响到引用了 RuntimeClass 的 Pod 的调度。

      6.png

      Scheduling 中包含了两个字段,NodeSelector 和 Tolerations。这两个和 Pod 本身所包含的 NodeSelector 和 Tolerations 是极为相似的。

      NodeSelector 代表的是支持该 RuntimeClass 的节点上应该有的 label 列表。一个 Pod 引用了该 RuntimeClass 后,RuntimeClass admission 会把该 label 列表与 Pod 中的 label 列表做一次合并。如果这两个 label 中有冲突的,会被 admission 拒绝。这里的冲突是指它们的 key 相同,但是 value 不相同,这种情况就会被 admission 拒绝。另外需要注意的是,RuntimeClass 并不会自动为 Node 设置 label,需要用户在使用前提前设置好。

      Tolerations 表示 RuntimeClass 的容忍列表。一个 Pod 引用该 RuntimeClass 之后,admission 也会把 toleration 列表与 Pod 中的 toleration 列表做一个合并。如果这两处的 Toleration 有相同的容忍配置,就会将其合并成一个。

      为什么引入 Pod Overhead?

      7.png

      上图左边是一个 Docker Pod,右边是一个 Kata Pod。我们知道,Docker Pod 除了传统的 container 容器之外,还有一个 pause 容器,但我们在计算它的容器开销的时候会忽略 pause 容器。对于 Kata Pod,除了 container 容器之外,kata-agent, pause, guest-kernel 这些开销都是没有被统计进来的。像这些开销,多的时候甚至能超过 100MB,这些开销我们是没法忽略的。

      这就是我们引入 Pod Overhead 的初衷。它的结构体定义如下:

      8.png

      它的定义非常简单,只有一个字段 PodFixed。它这里面也是一个映射,它的 key 是一个 ResourceName,value 是一个 Quantity。每一个 Quantity 代表的是一个资源的使用量。因此 PodFixed 就代表了各种资源的占用量,比如 CPU、内存的占用量,都可以通过 PodFixed 进行设置。

      Pod Overhead 的使用场景与限制

      Pod Overhead 的使用场景主要有三处:

      • Pod 调度

      在没有引入 Overhead 之前,只要一个节点的资源可用量大于等于 Pod 的 requests 时,这个 Pod 就可以被调度到这个节点上。引入 Overhead 之后,只有节点的资源可用量大于等于 Overhead 加上 requests 的值时才能被调度上来。

      • ResourceQuota

      它是一个 namespace 级别的资源配额。假设我们有这样一个 namespace,它的内存使用量是 1G,我们有一个 requests 等于 500 的 Pod,那么这个 namespace 之下,最多可以调度两个这样的 Pod。而如果我们为这两个 Pod 增添了 200MB 的 Overhead 之后,这个 namespace 下就最多只可调度一个这样的 Pod。

      • Kubelet Pod 驱逐

      引入 Overhead 之后,Overhead 就会被统计到节点的已使用资源中,从而增加已使用资源的占比,最终会影响到 Kubelet Pod 的驱逐。

      以上是 Pod Overhead 的使用场景。除此之外,Pod Overhead 还有一些使用限制和注意事项:

      • Pod Overhead 最终会永久注入到 Pod 内并且不可手动更改。即便是将 RuntimeClass 删除或者更新,Pod Overhead 依然存在并且有效;
      • Pod Overhead 只能由 RuntimeClass admission 自动注入(至少目前是这样的),不可手动添加或更改。如果这么做,会被拒绝;
      • HPA 和 VPA 是基于容器级别指标数据做聚合,Pod Overhead 不会对它们造成影响。

      三、多容器运行时示例

      9.png

      目前阿里云 ACK 安全沙箱容器已经支持了多容器运行时,我们以上图所示环境为例来说明一下多容器运行时是怎么工作的。

      如上图所示有两个 Pod,左侧是一个 runc 的 Pod,对应的 RuntimeClass 是 runc,右侧是一个 runv 的Pod,引用的 RuntimeClass 是 runv。对应的请求已用不同的颜色标识了出来,蓝色的代表是 runc 的,红色的代表是 runv 的。图中下半部分,其中比较核心的部分是 containerd,在 containerd 中可以配置多个容器运行时,最终上面的请求也会到达这里进行请求的转发。

      我们先来看一下 runc 的请求,它先到达 kube-apiserver,然后 kube-apiserver 请求转发给 kubelet,最终 kubelet 将请求发至 cri-plugin(它是一个实现了 CRI 的插件),cri-plugin 在 containerd 的配置文件中查询 runc 对应的 Handler,最终查到是通过 Shim API runtime v1 请求 containerd-shim,然后由它创建对应的容器。这是 runc 的流程。

      runv 的流程与 runc 的流程类似。也是先将请求到达 kube-apiserver,然后再到达 kubelet,再把请求到达 cri-plugin,cri-plugin 最终还回去匹配 containerd 的配置文件,最终会找到通过 Shim API runtime v2 去创建 containerd-shim-kata-v2,然后由它创建一个 Kata Pod。

      下面我们再看一下 containerd 的具体配置。

      10.png

      containerd 默认放在 [file:///etc/containerd/config.toml]() 这个位置下。比较核心的配置是在 plugins.cri.containerd 目录下。其中 runtimes 的配置都有相同的前缀 plugins.cri.containerd.runtimes,后面有 runc, runv 两种 RuntimeClass。这里面的 runc 和 runv 和前面 RuntimeClass 对象中 Handler 的名字是相对应的。除此之外,还有一个比较特殊的配置 plugins.cri.containerd.runtimes.default_runtime,它的意思是说,如果一个 Pod 没有指定 RuntimeClass,但是被调度到当前节点的话,那么就默认使用 runc 容器运行时。

      下面的例子是创建 runc 和 runv 这两个 RuntimeClass 对象,我们可以通过 kubectl get runtimeclass 看到当前所有可用的容器运行时。

      11.png

      下图从左至右分别是一个 runc 和 runv 的 Pod,比较核心的地方就是在 runtimeClassName 字段中分别引用了 runc 和 runv 的容器运行时。

      12.png

      最终将 Pod 创建起来之后,我们可以通过 kubectl 命令来查看各个 Pod 容器的运行状态以及 Pod 所使用的容器运行时。我们可以看到现在集群中有两个 Pod:一个是 runc-pod,另一个是 runv-pod,分别引用的是 runc 和 runv 的 RuntimeClass,并且它们的状态都是 Running。

      13.png

      四、本文总结

      本文的主要内容就到此为止了,这里为大家简单总结一下:

      • RuntimeClass 是 Kubernetes 一种内置的集群资源,主要用来解决多个容器运行时混用的问题;
      • RuntimeClass 中配置 Scheduling 可以让 Pod 自动调度到运行了指定容器运行时的节点上。但前提是需要用户提前为这些 Node 设置好 label;
      • RuntimeClass 中配置 Overhead,可以把 Pod 中业务运行所需以外的开销统计进来,让调度、ResourceQuota、Kubelet Pod 驱逐等行为更准确。

      4.7尾图.png

      活动报名链接:https://yqh.aliyun.com/live/CloudNative

      阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”

      ]]>
      Alibaba Cloud Native Day | 4 月 18 日杭州场线上直播 Wed, 08 Apr 2020 03:03:11 +0800 Alibaba Cloud Native Day 将于 4 月 18 日全天进行线上直播!届时,多名阿里技术专家线上齐聚,畅聊 K8s、Serverless 、 OAM 及微服务等实践案例。通过本场活动,您将了解到:

      • 如何用 Start.aliyun.com 5 分钟搭建一套云上应用;
      • 阿里巴巴容器服务 ACK 助力在线教育行业抗击疫情;
      • 阿里巴巴函数计算 Serverless/FaaS 的详解。

      时间:4 月 18 日  9:30 - 16:50
      地点:线上直播
      报名方式:点击进行报名

      详细信息看这里
      4.7直播海报 公众号专用.jpg

      阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术圈。”

      ]]>
      Gartner报告:阿里云与AWS并列,容器产品最完善 Wed, 08 Apr 2020 03:03:11 +0800 近日,国际知名调研机构 Gartner 发布 2020 年容器公有云竞争格局报告,阿里云再度成为国内唯一入选厂商。Gartner 报告显示,阿里云容器服务在中国市场表现强劲,产品形态丰富,在如 Serverless 容器、服务网格、安全沙箱容器、混合云和边缘等领域,具备良好的技术发展策略。

      2020 年 3 月,Gartner 第二次公开《竞争格局:公共云容器服务》年度调研报告,报告针对 Serverless Kubernetes、服务网格、容器镜像等十项功能维度进行对比,阿里云和 AWS 覆盖九项产品能力,产品丰富度领先 Google、微软、IBM 和 Oracle 四家厂商。

      image.png

      阿里云连续两年入选 Gartner 容器报告,一方面是因为阿里云拥有全球第三的市场份额,另一方面是因为其已经拥有近十年的容器技术储备。

      目前,阿里云容器服务(ACK)已在中国及海外 19 个公有云可用区开服,同时也支持客户在自有机房和边缘端的部署使用 Kubernetes。同时,阿里云还提供了丰富的差异化产品:兼容 Istio 的托管版服务网格、基于弹性容器实例的无服务器 Kubernetes(ASK)、提供镜像扫描的独享版容器镜像服务 (ACR)、基于轻量虚拟机技术的安全沙箱容器运行时和托管服务网格(ASM)等等。在此前的 Forrester 公共云容器服务评测中,阿里云作为 Strong Performer 位列国内第一。

      过去几年,容器服务被各行业企业广泛接受,而阿里云凭借业界最丰富的容器产品家族,容器服务已经连续数年规模超 400% 高速增长。如今,阿里云容器服务支撑着上万个集群,数百万容器。这背后离不开的,是阿里云广大云原生客户及阿里集团内部的千锤百炼。容器服务 ACK 以一套架构支撑了外部客户、云产品和阿里巴巴内部核心系统容器化上云。

      2019 年天猫 双11,阿里巴巴核心系统首次实现 100% 上云。面对全球最大的交易洪峰,阿里云扛住了每秒 54.4 万笔的交易峰值,这是“云原生”与“天猫全球狂欢节”的一次完美联名。在新的 双11 来临前,容器镜像服务新增了数 PB 的镜像数据,月均镜像拉取达数亿次。

      同时产品提供了云原生应用交付链等功能,全面覆盖阿里巴巴经济体及云上用户在云原生时代的使用需求。

      容器技术走向成熟,业内方案日趋完整

      Gartner 指出,如今更多的主流企业开始落地容器技术,他们关注的是完整体系化的容器解决方案而非某些单独零散的产品服务。在经典的 Kubernetes 服务之外,同全球一流的技术与服务提供商一样,阿里云也在深耕与 Kubernetes 相关周边领域,比如无服务化容器、混合云、生态体系、aPaaS 集成、安全和自动化 DevOps 等方面。

      这里选取 Gartner 报告中重点提及的三个发展方向:无服务器(Serverless)容器、如服务网格等 aPaaS 组件、容器生态。

      给 Kubernetes 减负,Serverless 容器备受瞩目

      Kubernetes 已经成为业界容器编排系统的事实标准,但不可否认的是,Kubernetes 的复杂性让一些初学者望而却步进退两难。业内厂商因此都开始思考如何通过Serverless 的方式根本性解决 K8s 自身的管理复杂性,让用户无需受困于 K8s 集群容量规划、安全维护、故障诊断。

      Gartner 认为无服务化容器已迅速崛起,并将会在未来的容器领域扮演重要角色。此类产品与底层 IaaS 资源紧密结合,免去用户管理服务器的负担,具备更好的扩缩容能力。阿里云的 Serverless Kubernetes(ASK)、AWS 的 Fargate 和微软云的虚拟节点,不仅可以帮助一线开发者简化容器运行环境,而且也被越来越多企业用于海量规模 Kubernetes 集群的管配。

      阿里云 Serverless Kubernetes 是业界 Kubernetes 兼容性最好的 Serverless Container 服务,其调度容器实例服务 ECI 底层资源,可以实现 10 秒启动容器,30 秒弹性扩容 500 个 pod,轻松支持单集群 1 万个 Pod,弹性能力堪称业界一流。其同时提供了丰富的产品能力,包括镜像缓存、GPU、抢占式 spot 实例等,满足在线业务弹性、数据计算、CI/CD 等多种场景需求。

      着眼于全生命周期,更好支撑基于 Kubernetes 的微服务应用

      Gartner 报告同样指出各公有云容器平台,还提供了如 API 网关、服务网格、安全容器运行时、可观测性等工具,以更好地支撑基于 Kubernetes 的微服务架构及完整的应用生命周期。

      阿里云服务网格是托管式的微服务应用流量统一管理平台,为每个微服务提供一致的流量控制和可观测能力,并支持多个 Kubernetes 集群统一流量管理。而对于更强安全隔离需求的情况,用户可以搭配选用阿里云安全沙箱容器,其拥有独立内核可提供强大安全性,几乎没有性能损耗,在日志、监控、弹性和存储等各方面具备与 Docker 容器一样的用户体验。

      根据不同观测需求,用户可以选择云监控、ARMS Prometheus、ARMS前端监控和日志服务等,实现云资源、容器集群、容器节点、Pod 等指标的完善监控,对集群变更状态、pod 创建拉起删除、组件异常等信息及时告警。

      独木不成林,容器市场生态潜力巨大

      为了方便广大企业获得完整的容器解决方案,Gartner认为第三方软件供应商应与各容器公共云平台紧密合作。第三方软件供应商通过容器市场提供其应用镜像,用户可以一键部署各类所需的标准容器应用。

      阿里云已于 2019 年 9月正式上线容器应用市场,支持容器镜像及 Helm Chart 两类容器应用商品。市场提供开源免费和商业化收费的容器应用商品,用户可以便捷选择并一键部署至 ACK 集群上。目前,容器应用市场已入驻 Fortinet、Intel、驻云、奥哲 等多家合作伙伴,覆盖了从容器应用自身安全监控到容器化部署的商品,便于用户获得完整的容器化解决方案。

      企业正在拥抱云原生

      Gartner 报告曾指出,到 2020 年,将有 50% 的传统老旧应用被以云原生化的方式改造,到 2022 年,将有 75% 的全球化企业将在生产中使用云原生的容器化应用。

      云原生,是指一种构建和运行应用程序的方法,它利用了云计算交付模型的优势。云原生是关于如何创建和部署应用程序,而与在何处创建与部署无关。云原生架构充分利用按需交付、全球部署、弹性和更高级别的服务,极大提高了开发人员的工作效率、业务灵活性、可扩展性、可用性、利用率和成本节约。而容器就是云原生的核心技术之一。

      阿里云容器技术负责人易立指出云原生技术可以为企业带来三大优势:

      • 加速核心架构互联网化。帮助企业IT基础设施更加具备弹性和自治能力,提升业务敏捷性,更加灵活地应对商业发展中的不确定性;
      • 助力业务的数字化、智能化升级。云原生技术与异构算力和大数据、AI技术结合,将释放巨大的威力,把数据资产转变成企业核心竞争力;
      • 随着 5G、AIoT 等技术的成熟,新基建战略将为企业带来全新的创新机遇。云原生技术将推动云边端应用一体协同,构建下一代动态、大规模、无边界的云应用架构。

      如今,阿里云容器服务每天服务数千家企业的业务系统,上万个集群和数百万容器支撑这各行各业特点各异的服务。

      以云原生大数据及 AI 场景为例,微博使用容器服务 ACK,实时计算的性能较开源方案提升 2.4 倍,实现百亿级样本和万亿维度超大规模系数模型。

      工业领域的百年老店西门子,已经通过云原生突破传统模式上线周期长、扩容难、运维难等难题,仅用半年多时间就在阿里云上部署起物联网平台 MindSphere,为制造企业提供快速的数字化改造和迁移服务。

      新冠疫情期间,百家云、洋葱学院、希沃课堂等数家企业基于容器服务扩容十倍甚至数十倍应对流量洪峰,支撑千万学生在线学习。此外,2020 年 1月华大基因和阿里云联合宣布,基于阿里云容器服务的基因计算服务 AGS通过异构算力的高效调度和算法创新可以实现计算百倍加速,15 分钟完成个人全基因组测序;疫情爆发后,阿里云还面向全球免费开放基因计算服务 AGS,60 秒内即可获得数千种已知病毒基因(包括冠状病毒等)比对结果。

      阿里云在云原生领域的投入广泛而深入,在容器、服务网格和 Serverless 均有丰富的产品服务,目前阿里云已经拥有国内最丰富的云原生产品家族、最全面的云原生开源贡献、最大规模的云原生应用实践、最大的云原生客户群体。

      经过 9 年的内部技术实践,阿里云已拥有国内最丰富的云原生产品家族,覆盖八大类别 20 余款产品,涵盖底层基础设施、数据智能、分布式应用等,可以满足不同行业场景的需求。

      image1.png

      “新基石、新算力、新生态‘是容器产品发展策略”,易立称,“云原生技术正成为释放云价值的最短路径,团队会帮助企业更好支撑混合云、云边一体的分布式云架构和全球化应用交付。基于云原生的软硬一体化技术创新,如神龙架构,含光芯片,GPU共享调度等,阿里云将加速业务智能化升级。同时我们还会通过开放技术生态和全球合作伙伴计划,让更多企业分享云时代技术红利。”

      ]]>
      【升级】4月8日域名、虚机订单系统升级维护公告 Wed, 08 Apr 2020 03:03:11 +0800

      【阿里云】【域名】【虚机】【订单系统维护】

      维护时间:北京时间2020年4月8日 00:00 – 03:00

      维护内容:阿里云域名、虚机订单系统将于上述时间进行后台维护升级。

      维护影响:届时域名、虚机等产品的购买、续费等订单,将会无法支付。在此期间会对您造成的具体影响如下:

      1、通过PC/APP/H5普通下单流程提交的域名注册、续费、赎回、转入、万网预订等操作和虚机购买、续费等操作无法进行订单支付。升级期间如果已经下单,在升级过后可以继续支付,支付路径“控制台--费用中心--万网产品订单--未付款订单”。

      2、通过API/控制台批量操作提交的域名注册、续费、赎回、转入等操作的,无法提交成功,请您避开系统升级维护时间进行操作。

      3、您提交的一口价(优选)、域名抢注、保留词一口价等购买操作,无法完成订单支付,请避开系统升级维护时间进行订单支付操作。

      一口价(万网)、域名竞价交易、域名带价push交易理论上不受此次系统维护影响;域名过户、实名认证等不涉及订单支付的管理操作不受影响。

      请您避开系统升级维护时间进行相关操作,由此给您造成的不便,敬请谅解!

      ]]>
      【升级】4月8日Donuts注册局维护通知 Wed, 08 Apr 2020 03:03:11 +0800

      【阿里云】【域名】【注册局 / 注册商维护通知】

      维护时间:北京时间2020年4月8日 01:00 - 05:00

      维护内容:接到注册局的通知,注册局 / 注册商将于上述时间对后台系统进行维护升级。

      维护影响:届时,您的 .ltd/.group/.pub/.live/.rocks/.band/.market/.software/.social/.lawyer/.engineer/.news/.video/.studio/.today /.plus/.world/.run/.show/.city/.gold/.today/.cool 等域名的续费、转入转出、信息修改和过户域名等操作,将会无法使用,在此期间会对您造成的影响如下:

      1、您提交的续费、转入、转出域名等操作在支付费用后状态为“处理中”,且可能出现“不成功”等状态;

      2、维护过程中您无法对域名注册信息进行修改,将提示修改失败。

      如果您需要注册或管理以上业务操作,建议您避开该时间段,以免给您的业务造成影响。

      如您的业务操作失败,建议维护后再次尝试。

      由此给您带来的不便,我们表示歉意,敬请谅解。

      ]]>
      【风险预警】深信服SSL VPN被境外非法组织利用发起APT攻击风险 Wed, 08 Apr 2020 03:03:11 +0800

      2020年4月6日,深信服官方发布一起《关于境外非法组织利用深信服SSL VPN设备下发恶意文件并发起APT攻击活动》的事件,风险较大。


      漏洞描述

      在深信服科技的事件通报中披露了一起APT攻击行动。该APT行动中,境外非法组织通过非法手段控制部分深信服SSL VPN设备,并利用客户端升级漏洞下发恶意文件到客户端的APT攻击活动。攻击者在获取控制深信服SSL VPN设备的权限后,可以利用SSL VPN设备Windows客户端升级模块签名验证机制的缺陷,从而下发恶意文件并进行下一步利用。阿里云应急响应中心提醒深信服VPN用户尽快采取安全措施,联系官方寻求技术排查和漏洞修复。


      影响版本

      M6.3R1

      M6.1


      安全建议

      建议用户尽快采用止损操作规避风险:关闭ssl vpn相关端口,可直接kill进程或使用阿里云安全组功能,然后联系深信服官方寻求技术排查和漏洞修复。


      相关链接

      《关于境外非法组织利用深信服SSL VPN设备下发恶意文件并发起APT攻击活动》



      我们会关注后续进展,请随时关注官方公告。

      如有任何问题,可随时通过工单或服务电话95187联系反馈。

      阿里云应急响应中心

      2020.04.06


      ]]>
      阿里云物联网平台规则引擎综述-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述


      使用物联网平台规则引擎的数据流转功能,当设备基于Topic进行通信时,您可以在规则引擎的数据流转中,编写SQL对Topic中的数据进行处理,并配置转发规则将处理后的数据转发到其他设备Topic或阿里云其他服务。针对官方文档目前这部分示例介绍的比较简单,数据转发部分介绍的较少,本文对规则引擎常见的使用场景做一个整理,方便使用者结合自己的业务场景参考。


      原理图

      _


      数据流转实例

      阿里云物联网平台数据转发到DataHub示例
      阿里云物联网平台数据转发到函数计算示例
      阿里云物联网平台设备上下线信息通过规则引擎流转到RDS示例
      阿里云物联网平台设备数据转发到消息队列RocketMQ全链路测试
      阿里云物联网平台数据转发到表格存储(Table Store)示例参考
      阿里云物联网平台数据转发到消息服务(MNS)示例
      阿里云物联网平台数据转发到时序时空数据库(TSDB)示例


      参考链接


      云产品流转概述
      SQL表达式
      数据流转使用示例

                                                              </div>
      ]]>
      阿里云物联网平台数据转发到DataHub示例-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述


      您可以使用规则引擎将数据转到DataHub上,再由DataHub将数据流转至实时计算、MaxCompute等服务中,以实现更多计算场景。本文主要演示通过规则引擎将消息流转到DataHub,并通过Dataconnector 将消费流转到MaxCompute的表。


      Step By Step


      物联网平台创建产品和设备

      _


      参考链接:产品及设备准备部分。


      创建DataHub Project 和 Topic

      1、创建Datahub Project
      _


      2、创建Topic
      _


      规则引擎配置

      1、创建规则引擎、配置处理数据


      参考链接 规则引擎配置配置部分。


      2、配置转发数据
      _
      _


      3、启动设备端SDK,消息流转
      _


      4、流转日志查询


      _


      创建Dataconnector

      1、maxcompute创建表


      CREATE TABLE datahubforiot (
      <span class="hljs-string">`devicename`</span> string,
      <span class="hljs-string">`humidity`</span> string,
      <span class="hljs-string">`temperature`</span> string,
      <span class="hljs-string">`time`</span> string

      )
      PARTITIONED BY (ds string,hh string,mm string)
      LIFECYCLE 100;


      2、创建Dataconnector


      _


      _


      _


      _


      3、Dataworks SQL任务查询


      _


      参考链接


      数据转发到DataHub

      ]]>
      阿里云物联网平台数据转发到函数计算示例-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述


      使用物联网平台规则引擎的数据流转功能,可将Topic中的数据消息转发至其他Topic或其他阿里云产品进行存储或处理。本文主要演示通过规则引擎将设备上行消息流转到函数计算,并通过函数计算发送消息到钉钉机器人。


      Step By Step


      产品及设备准备



      1、创建产品
      _


      2、定义物模型
      _


      3、添加设备
      _


      _


      4、使用SDK 上行消息,参考链接:基于开源JAVA MQTT Client连接阿里云IoT


      import com.alibaba.taro.AliyunIoTSignUtil;
      import com.google.common.util.concurrent.ThreadFactoryBuilder;
      import org.eclipse.paho.client.mqttv3.*;
      import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
      import java.util.HashMap;
      import java.util.Map;
      import java.util.concurrent.ScheduledExecutorService;
      import java.util.concurrent.ScheduledThreadPoolExecutor;
      import java.util.concurrent.TimeUnit;

      public class IoTDemoPubSubDemo {

      <span class="hljs-comment">// 设备三元组信息</span>
      public <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> productKey = <span class="hljs-string">"a16MX********"</span>;
      public <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> deviceName = <span class="hljs-string">"device1"</span>;
      public <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> deviceSecret = <span class="hljs-string">"YGLHxUr40E1JaWhk3IVAm0uk********"</span>;
      public <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> regionId = <span class="hljs-string">"cn-shanghai"</span>;
      
      <span class="hljs-comment">// 物模型-属性上报topic</span>
      private <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> pubTopic = <span class="hljs-string">"/sys/"</span> + productKey + <span class="hljs-string">"/"</span> + deviceName + <span class="hljs-string">"/thing/event/property/post"</span>;
      <span class="hljs-comment">// 自定义topic,在产品Topic列表位置定义</span>
      private <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> subTopic = <span class="hljs-string">"/sys/"</span> + productKey + <span class="hljs-string">"/"</span> + deviceName + <span class="hljs-string">"/thing/event/property/post_reply"</span>;
      
      private <span class="hljs-keyword">static</span> MqttClient mqttClient;
      
      public <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> main(<span class="hljs-built_in">String</span> [] args){
      
          initAliyunIoTClient();
          ScheduledExecutorService scheduledThreadPool = <span class="hljs-keyword">new</span> ScheduledThreadPoolExecutor(<span class="hljs-number">1</span>,
                  <span class="hljs-keyword">new</span> ThreadFactoryBuilder().setNameFormat(<span class="hljs-string">"thread-runner-%d"</span>).build());
      
          scheduledThreadPool.scheduleAtFixedRate(()-&gt;postDeviceProperties(), <span class="hljs-number">10</span>,<span class="hljs-number">5</span>, TimeUnit.SECONDS);
      
          <span class="hljs-keyword">try</span> {
              mqttClient.subscribe(subTopic); <span class="hljs-comment">// 订阅Topic</span>
          } <span class="hljs-keyword">catch</span> (MqttException e) {
              System.out.println(<span class="hljs-string">"error:"</span> + e.getMessage());
              e.printStackTrace();
          }
      
          <span class="hljs-comment">// 设置订阅监听</span>
          mqttClient.setCallback(<span class="hljs-keyword">new</span> MqttCallback() {
              @Override
              public <span class="hljs-keyword">void</span> connectionLost(Throwable throwable) {
                  System.out.println(<span class="hljs-string">"connection Lost"</span>);
      
              }
      
              @Override
              public <span class="hljs-keyword">void</span> messageArrived(<span class="hljs-built_in">String</span> s, MqttMessage mqttMessage) throws Exception {
                  System.out.println(<span class="hljs-string">"Sub message"</span>);
                  System.out.println(<span class="hljs-string">"Topic : "</span> + s);
                  System.out.println(<span class="hljs-keyword">new</span> <span class="hljs-built_in">String</span>(mqttMessage.getPayload())); <span class="hljs-comment">//打印输出消息payLoad</span>
              }
      
              @Override
              public <span class="hljs-keyword">void</span> deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
      
              }
          });
      
      }
      
      <span class="hljs-comment">/**
       * 初始化 Client 对象
       */</span>
      private <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> initAliyunIoTClient() {
      
          <span class="hljs-keyword">try</span> {
              <span class="hljs-comment">// 构造连接需要的参数</span>
              <span class="hljs-built_in">String</span> clientId = <span class="hljs-string">"java"</span> + System.currentTimeMillis();
              <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>&gt; params = <span class="hljs-keyword">new</span> HashMap&lt;&gt;(<span class="hljs-number">16</span>);
              params.put(<span class="hljs-string">"productKey"</span>, productKey);
              params.put(<span class="hljs-string">"deviceName"</span>, deviceName);
              params.put(<span class="hljs-string">"clientId"</span>, clientId);
              <span class="hljs-built_in">String</span> timestamp = <span class="hljs-built_in">String</span>.valueOf(System.currentTimeMillis());
              params.put(<span class="hljs-string">"timestamp"</span>, timestamp);
              <span class="hljs-comment">// cn-shanghai</span>
              <span class="hljs-built_in">String</span> targetServer = <span class="hljs-string">"tcp://"</span> + productKey + <span class="hljs-string">".iot-as-mqtt."</span>+regionId+<span class="hljs-string">".aliyuncs.com:1883"</span>;
      
              <span class="hljs-built_in">String</span> mqttclientId = clientId + <span class="hljs-string">"|securemode=3,signmethod=hmacsha1,timestamp="</span> + timestamp + <span class="hljs-string">"|"</span>;
              <span class="hljs-built_in">String</span> mqttUsername = deviceName + <span class="hljs-string">"&amp;"</span> + productKey;
              <span class="hljs-built_in">String</span> mqttPassword = AliyunIoTSignUtil.sign(params, deviceSecret, <span class="hljs-string">"hmacsha1"</span>);
      
              connectMqtt(targetServer, mqttclientId, mqttUsername, mqttPassword);
      
          } <span class="hljs-keyword">catch</span> (Exception e) {
              System.out.println(<span class="hljs-string">"initAliyunIoTClient error "</span> + e.getMessage());
          }
      }
      
      public <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> connectMqtt(<span class="hljs-built_in">String</span> url, <span class="hljs-built_in">String</span> clientId, <span class="hljs-built_in">String</span> mqttUsername, <span class="hljs-built_in">String</span> mqttPassword) throws Exception {
      
          MemoryPersistence persistence = <span class="hljs-keyword">new</span> MemoryPersistence();
          mqttClient = <span class="hljs-keyword">new</span> MqttClient(url, clientId, persistence);
          MqttConnectOptions connOpts = <span class="hljs-keyword">new</span> MqttConnectOptions();
          <span class="hljs-comment">// MQTT 3.1.1</span>
          connOpts.setMqttVersion(<span class="hljs-number">4</span>);
          connOpts.setAutomaticReconnect(<span class="hljs-literal">false</span>);

      // connOpts.setCleanSession(true);

          connOpts.setCleanSession(<span class="hljs-literal">false</span>);
      
          connOpts.setUserName(mqttUsername);
          connOpts.setPassword(mqttPassword.toCharArray());
          connOpts.setKeepAliveInterval(<span class="hljs-number">60</span>);
      
          mqttClient.connect(connOpts);
      }
      
      <span class="hljs-comment">/**
       * 汇报属性
       */</span>
      private <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> postDeviceProperties() {
      
          <span class="hljs-keyword">try</span> {
              <span class="hljs-comment">//上报数据</span>
              <span class="hljs-comment">//高级版 物模型-属性上报payload</span>
              System.out.println(<span class="hljs-string">"上报属性值"</span>);
              <span class="hljs-built_in">String</span> payloadJson = <span class="hljs-string">"{"params":{"CurrentTemperature":13,"Humidity":10}}"</span>;
              MqttMessage message = <span class="hljs-keyword">new</span> MqttMessage(payloadJson.getBytes(<span class="hljs-string">"utf-8"</span>));
              message.setQos(<span class="hljs-number">1</span>);
              mqttClient.publish(pubTopic, message);
          } <span class="hljs-keyword">catch</span> (Exception e) {
              System.out.println(e.getMessage());
          }
      }

      }


      5、运行状态查看
      _




      函数计算创建与配置

      1、创建应用
      _


      _


      2、应用下面添加函数


      _


      _


      3、编辑脚本


      const https = require('https');

      const accessToken = '填写accessToken,即钉钉机器人webhook的accessToken';
      module.exports.handler = function(event, context, callback) {
      var eventJson = JSON.parse(event.toString());
      console.log(event.toString());
      //钉钉消息格式
      const postData = JSON.stringify({
      "msgtype": "markdown",
      "markdown": {
      "title": "设备温湿度传感器",
      "text": "#### 温湿度传感器上报n" +
      "> 设备名称:" + eventJson.deviceName+ "nn" +
      "> 实时温度:" + eventJson.Temperature + "℃nn" +
      "> 相对湿度:" + eventJson.Humidity + "%nn" +
      "> ###### " + eventJson.time + " 发布 by 物联网平台 n"
      },
      "at": {
      "isAtAll": false
      }
      });
      const options = {
      hostname: 'oapi.dingtalk.com',
      port: 443,
      path: '/robot/send?access_token=' + accessToken,
      method: 'POST',
      headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(postData)
      }
      };
      const req = https.request(options, (res) => {
      res.setEncoding('utf8');
      res.on('data', (chunk) => {});
      res.on('end', () => {
      callback(null, 'success');
      });
      });
      // 异常返回
      req.on('error', (e) => {
      callback(e);
      });
      // 写入数据
      req.write(postData);
      req.end();
      };


      钉钉机器人webhook的accessToken获取参考链接:阿里云IoT Studio服务开发定时关灯功能示例Demo: 2.3 钉钉机器人Webhook获取 部分。


      4、快速测试
      _


      _




      规则引擎配置

      1、创建规则引擎
      _


      2、配置处理数据


      _


      SQL字段


      deviceName() as deviceName, items.Humidity.value as Humidity, items.CurrentTemperature.value as Temperature, timestamp('yyyy-MM-dd HH:mm:ss') as time

      3、配置转发数据


      _


      4、启动设备端SDK,周期性上行消息,钉钉群查看通知


      _


      5、上行日志查看


      _


      参考链接


      温湿度计上报数据到钉钉群机器人

      ]]>
      阿里云数据分析最佳实践:二维数据可视化 + 设备数据下发-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述


      物联网数据分析,又称Link Analytics,是阿里云为物联网开发者提供的设备智能分析服务,全链路覆盖了设备数据生成、管理(存储)、清洗、分析及可视化等环节。有效降低数据分析门槛,助力物联网开发工作。这里分别演示通过二维数据可视化功能展示设备位置 + 通过数据分析实现定时下发数据到设备。


      Step By Step


      1、创建产品,导入物模型,参考链接


      物模型json内容


      {
      "schema": "https://iotx-tsl.oss-ap-southeast-1.aliyuncs.com/schema.json",
      "profile": {
      <span class="hljs-string">"productKey"</span>: <span class="hljs-string">"a1kVHWEOsM2"</span>

      },
      "properties": [

      {
        <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"GeoLocation"</span>,
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"地理位置"</span>,
        <span class="hljs-string">"accessMode"</span>: <span class="hljs-string">"rw"</span>,
        <span class="hljs-string">"required"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-string">"dataType"</span>: {
          <span class="hljs-string">"type"</span>: <span class="hljs-string">"struct"</span>,
          <span class="hljs-string">"specs"</span>: [
            {
              <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Longitude"</span>,
              <span class="hljs-string">"name"</span>: <span class="hljs-string">"经度"</span>,
              <span class="hljs-string">"dataType"</span>: {
                <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                <span class="hljs-string">"specs"</span>: {
                  <span class="hljs-string">"min"</span>: <span class="hljs-string">"-180"</span>,
                  <span class="hljs-string">"max"</span>: <span class="hljs-string">"180"</span>,
                  <span class="hljs-string">"unit"</span>: <span class="hljs-string">"°"</span>,
                  <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"度"</span>,
                  <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                }
              }
            },
            {
              <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Latitude"</span>,
              <span class="hljs-string">"name"</span>: <span class="hljs-string">"纬度"</span>,
              <span class="hljs-string">"dataType"</span>: {
                <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                <span class="hljs-string">"specs"</span>: {
                  <span class="hljs-string">"min"</span>: <span class="hljs-string">"-90"</span>,
                  <span class="hljs-string">"max"</span>: <span class="hljs-string">"90"</span>,
                  <span class="hljs-string">"unit"</span>: <span class="hljs-string">"°"</span>,
                  <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"度"</span>,
                  <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                }
              }
            },
            {
              <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Altitude"</span>,
              <span class="hljs-string">"name"</span>: <span class="hljs-string">"海拔"</span>,
              <span class="hljs-string">"dataType"</span>: {
                <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                <span class="hljs-string">"specs"</span>: {
                  <span class="hljs-string">"min"</span>: <span class="hljs-string">"0"</span>,
                  <span class="hljs-string">"max"</span>: <span class="hljs-string">"9999"</span>,
                  <span class="hljs-string">"unit"</span>: <span class="hljs-string">"m"</span>,
                  <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"米"</span>,
                  <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                }
              }
            },
            {
              <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CoordinateSystem"</span>,
              <span class="hljs-string">"name"</span>: <span class="hljs-string">"坐标系统"</span>,
              <span class="hljs-string">"dataType"</span>: {
                <span class="hljs-string">"type"</span>: <span class="hljs-string">"enum"</span>,
                <span class="hljs-string">"specs"</span>: {
                  <span class="hljs-string">"1"</span>: <span class="hljs-string">"WGS_84"</span>,
                  <span class="hljs-string">"2"</span>: <span class="hljs-string">"GCJ_02"</span>
                }
              }
            }
          ]
        }
      },
      {
        <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CurrentHumidity"</span>,
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"湿度"</span>,
        <span class="hljs-string">"accessMode"</span>: <span class="hljs-string">"rw"</span>,
        <span class="hljs-string">"required"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-string">"dataType"</span>: {
          <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
          <span class="hljs-string">"specs"</span>: {
            <span class="hljs-string">"min"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-string">"max"</span>: <span class="hljs-string">"100"</span>,
            <span class="hljs-string">"unit"</span>: <span class="hljs-string">"%"</span>,
            <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"百分比"</span>,
            <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
          }
        }
      },
      {
        <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CurrentTemperature"</span>,
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"温度"</span>,
        <span class="hljs-string">"accessMode"</span>: <span class="hljs-string">"rw"</span>,
        <span class="hljs-string">"required"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-string">"dataType"</span>: {
          <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
          <span class="hljs-string">"specs"</span>: {
            <span class="hljs-string">"min"</span>: <span class="hljs-string">"-40"</span>,
            <span class="hljs-string">"max"</span>: <span class="hljs-string">"120"</span>,
            <span class="hljs-string">"unit"</span>: <span class="hljs-string">"℃"</span>,
            <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"摄氏度"</span>,
            <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
          }
        }
      }

      ],
      "events": [

      {
        <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"post"</span>,
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"post"</span>,
        <span class="hljs-string">"type"</span>: <span class="hljs-string">"info"</span>,
        <span class="hljs-string">"required"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-string">"desc"</span>: <span class="hljs-string">"属性上报"</span>,
        <span class="hljs-string">"method"</span>: <span class="hljs-string">"thing.event.property.post"</span>,
        <span class="hljs-string">"outputData"</span>: [
          {
            <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"GeoLocation"</span>,
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"地理位置"</span>,
            <span class="hljs-string">"dataType"</span>: {
              <span class="hljs-string">"type"</span>: <span class="hljs-string">"struct"</span>,
              <span class="hljs-string">"specs"</span>: [
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Longitude"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"经度"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"min"</span>: <span class="hljs-string">"-180"</span>,
                      <span class="hljs-string">"max"</span>: <span class="hljs-string">"180"</span>,
                      <span class="hljs-string">"unit"</span>: <span class="hljs-string">"°"</span>,
                      <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"度"</span>,
                      <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                    }
                  }
                },
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Latitude"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"纬度"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"min"</span>: <span class="hljs-string">"-90"</span>,
                      <span class="hljs-string">"max"</span>: <span class="hljs-string">"90"</span>,
                      <span class="hljs-string">"unit"</span>: <span class="hljs-string">"°"</span>,
                      <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"度"</span>,
                      <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                    }
                  }
                },
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Altitude"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"海拔"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"min"</span>: <span class="hljs-string">"0"</span>,
                      <span class="hljs-string">"max"</span>: <span class="hljs-string">"9999"</span>,
                      <span class="hljs-string">"unit"</span>: <span class="hljs-string">"m"</span>,
                      <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"米"</span>,
                      <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                    }
                  }
                },
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CoordinateSystem"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"坐标系统"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"enum"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"1"</span>: <span class="hljs-string">"WGS_84"</span>,
                      <span class="hljs-string">"2"</span>: <span class="hljs-string">"GCJ_02"</span>
                    }
                  }
                }
              ]
            }
          },
          {
            <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CurrentHumidity"</span>,
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"湿度"</span>,
            <span class="hljs-string">"dataType"</span>: {
              <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
              <span class="hljs-string">"specs"</span>: {
                <span class="hljs-string">"min"</span>: <span class="hljs-string">"0"</span>,
                <span class="hljs-string">"max"</span>: <span class="hljs-string">"100"</span>,
                <span class="hljs-string">"unit"</span>: <span class="hljs-string">"%"</span>,
                <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"百分比"</span>,
                <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
              }
            }
          },
          {
            <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CurrentTemperature"</span>,
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"温度"</span>,
            <span class="hljs-string">"dataType"</span>: {
              <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
              <span class="hljs-string">"specs"</span>: {
                <span class="hljs-string">"min"</span>: <span class="hljs-string">"-40"</span>,
                <span class="hljs-string">"max"</span>: <span class="hljs-string">"120"</span>,
                <span class="hljs-string">"unit"</span>: <span class="hljs-string">"℃"</span>,
                <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"摄氏度"</span>,
                <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
              }
            }
          }
        ]
      }

      ],
      "services": [

      {
        <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"set"</span>,
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"set"</span>,
        <span class="hljs-string">"required"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-string">"callType"</span>: <span class="hljs-string">"async"</span>,
        <span class="hljs-string">"desc"</span>: <span class="hljs-string">"属性设置"</span>,
        <span class="hljs-string">"method"</span>: <span class="hljs-string">"thing.service.property.set"</span>,
        <span class="hljs-string">"inputData"</span>: [
          {
            <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"GeoLocation"</span>,
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"地理位置"</span>,
            <span class="hljs-string">"dataType"</span>: {
              <span class="hljs-string">"type"</span>: <span class="hljs-string">"struct"</span>,
              <span class="hljs-string">"specs"</span>: [
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Longitude"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"经度"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"min"</span>: <span class="hljs-string">"-180"</span>,
                      <span class="hljs-string">"max"</span>: <span class="hljs-string">"180"</span>,
                      <span class="hljs-string">"unit"</span>: <span class="hljs-string">"°"</span>,
                      <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"度"</span>,
                      <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                    }
                  }
                },
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Latitude"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"纬度"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"min"</span>: <span class="hljs-string">"-90"</span>,
                      <span class="hljs-string">"max"</span>: <span class="hljs-string">"90"</span>,
                      <span class="hljs-string">"unit"</span>: <span class="hljs-string">"°"</span>,
                      <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"度"</span>,
                      <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                    }
                  }
                },
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Altitude"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"海拔"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"min"</span>: <span class="hljs-string">"0"</span>,
                      <span class="hljs-string">"max"</span>: <span class="hljs-string">"9999"</span>,
                      <span class="hljs-string">"unit"</span>: <span class="hljs-string">"m"</span>,
                      <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"米"</span>,
                      <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                    }
                  }
                },
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CoordinateSystem"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"坐标系统"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"enum"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"1"</span>: <span class="hljs-string">"WGS_84"</span>,
                      <span class="hljs-string">"2"</span>: <span class="hljs-string">"GCJ_02"</span>
                    }
                  }
                }
              ]
            }
          },
          {
            <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CurrentHumidity"</span>,
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"湿度"</span>,
            <span class="hljs-string">"dataType"</span>: {
              <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
              <span class="hljs-string">"specs"</span>: {
                <span class="hljs-string">"min"</span>: <span class="hljs-string">"0"</span>,
                <span class="hljs-string">"max"</span>: <span class="hljs-string">"100"</span>,
                <span class="hljs-string">"unit"</span>: <span class="hljs-string">"%"</span>,
                <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"百分比"</span>,
                <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
              }
            }
          },
          {
            <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CurrentTemperature"</span>,
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"温度"</span>,
            <span class="hljs-string">"dataType"</span>: {
              <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
              <span class="hljs-string">"specs"</span>: {
                <span class="hljs-string">"min"</span>: <span class="hljs-string">"-40"</span>,
                <span class="hljs-string">"max"</span>: <span class="hljs-string">"120"</span>,
                <span class="hljs-string">"unit"</span>: <span class="hljs-string">"℃"</span>,
                <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"摄氏度"</span>,
                <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
              }
            }
          }
        ],
        <span class="hljs-string">"outputData"</span>: []
      },
      {
        <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"get"</span>,
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"get"</span>,
        <span class="hljs-string">"required"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-string">"callType"</span>: <span class="hljs-string">"async"</span>,
        <span class="hljs-string">"desc"</span>: <span class="hljs-string">"属性获取"</span>,
        <span class="hljs-string">"method"</span>: <span class="hljs-string">"thing.service.property.get"</span>,
        <span class="hljs-string">"inputData"</span>: [
          <span class="hljs-string">"GeoLocation"</span>,
          <span class="hljs-string">"CurrentHumidity"</span>,
          <span class="hljs-string">"CurrentTemperature"</span>
        ],
        <span class="hljs-string">"outputData"</span>: [
          {
            <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"GeoLocation"</span>,
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"地理位置"</span>,
            <span class="hljs-string">"dataType"</span>: {
              <span class="hljs-string">"type"</span>: <span class="hljs-string">"struct"</span>,
              <span class="hljs-string">"specs"</span>: [
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Longitude"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"经度"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"min"</span>: <span class="hljs-string">"-180"</span>,
                      <span class="hljs-string">"max"</span>: <span class="hljs-string">"180"</span>,
                      <span class="hljs-string">"unit"</span>: <span class="hljs-string">"°"</span>,
                      <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"度"</span>,
                      <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                    }
                  }
                },
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Latitude"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"纬度"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"min"</span>: <span class="hljs-string">"-90"</span>,
                      <span class="hljs-string">"max"</span>: <span class="hljs-string">"90"</span>,
                      <span class="hljs-string">"unit"</span>: <span class="hljs-string">"°"</span>,
                      <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"度"</span>,
                      <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                    }
                  }
                },
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"Altitude"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"海拔"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"min"</span>: <span class="hljs-string">"0"</span>,
                      <span class="hljs-string">"max"</span>: <span class="hljs-string">"9999"</span>,
                      <span class="hljs-string">"unit"</span>: <span class="hljs-string">"m"</span>,
                      <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"米"</span>,
                      <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
                    }
                  }
                },
                {
                  <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CoordinateSystem"</span>,
                  <span class="hljs-string">"name"</span>: <span class="hljs-string">"坐标系统"</span>,
                  <span class="hljs-string">"dataType"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"enum"</span>,
                    <span class="hljs-string">"specs"</span>: {
                      <span class="hljs-string">"1"</span>: <span class="hljs-string">"WGS_84"</span>,
                      <span class="hljs-string">"2"</span>: <span class="hljs-string">"GCJ_02"</span>
                    }
                  }
                }
              ]
            }
          },
          {
            <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CurrentHumidity"</span>,
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"湿度"</span>,
            <span class="hljs-string">"dataType"</span>: {
              <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
              <span class="hljs-string">"specs"</span>: {
                <span class="hljs-string">"min"</span>: <span class="hljs-string">"0"</span>,
                <span class="hljs-string">"max"</span>: <span class="hljs-string">"100"</span>,
                <span class="hljs-string">"unit"</span>: <span class="hljs-string">"%"</span>,
                <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"百分比"</span>,
                <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
              }
            }
          },
          {
            <span class="hljs-string">"identifier"</span>: <span class="hljs-string">"CurrentTemperature"</span>,
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"温度"</span>,
            <span class="hljs-string">"dataType"</span>: {
              <span class="hljs-string">"type"</span>: <span class="hljs-string">"double"</span>,
              <span class="hljs-string">"specs"</span>: {
                <span class="hljs-string">"min"</span>: <span class="hljs-string">"-40"</span>,
                <span class="hljs-string">"max"</span>: <span class="hljs-string">"120"</span>,
                <span class="hljs-string">"unit"</span>: <span class="hljs-string">"℃"</span>,
                <span class="hljs-string">"unitName"</span>: <span class="hljs-string">"摄氏度"</span>,
                <span class="hljs-string">"step"</span>: <span class="hljs-string">"0.01"</span>
              }
            }
          }
        ]
      }

      ]
      }


      _


      2、设备端通过开源MQTT SDK上传数据,基于开源JAVA MQTT Client连接阿里云IoT


      import com.alibaba.taro.AliyunIoTSignUtil;
      import org.eclipse.paho.client.mqttv3.*;
      import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
      import java.util.HashMap;
      import java.util.Map;

      public class IoTDemoPubSubDemo {

      <span class="hljs-comment">// 设备三元组信息</span>
      public <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> productKey = <span class="hljs-string">"a1kVH******"</span>;
      public <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> deviceName = <span class="hljs-string">"device1"</span>;
      public <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> deviceSecret = <span class="hljs-string">"XADek3EYXzzTtxJ6a****************"</span>;
      public <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> regionId = <span class="hljs-string">"cn-shanghai"</span>;
      
      <span class="hljs-comment">// 物模型-属性上报topic</span>
      private <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> pubTopic = <span class="hljs-string">"/sys/"</span> + productKey + <span class="hljs-string">"/"</span> + deviceName + <span class="hljs-string">"/thing/event/property/post"</span>;
      <span class="hljs-comment">// 物模型-属性订阅topic</span>
      private <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> subTopic = <span class="hljs-string">"/sys/"</span> + productKey + <span class="hljs-string">"/"</span> + deviceName + <span class="hljs-string">"/thing/service/property/set"</span>;
      private <span class="hljs-keyword">static</span> MqttClient mqttClient;
      
      public <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> main(<span class="hljs-built_in">String</span> [] args){
      
          initAliyunIoTClient();
          <span class="hljs-comment">// 汇报属性</span>
          postDeviceProperties();
          <span class="hljs-keyword">try</span> {
              mqttClient.subscribe(subTopic); <span class="hljs-comment">// 订阅Topic</span>
          } <span class="hljs-keyword">catch</span> (MqttException e) {
              System.out.println(<span class="hljs-string">"error:"</span> + e.getMessage());
              e.printStackTrace();
          }
      
          <span class="hljs-comment">// 设置订阅监听</span>
          mqttClient.setCallback(<span class="hljs-keyword">new</span> MqttCallback() {
              @Override
              public <span class="hljs-keyword">void</span> connectionLost(Throwable throwable) {
                  System.out.println(<span class="hljs-string">"connection Lost"</span>);
              }
      
              @Override
              public <span class="hljs-keyword">void</span> messageArrived(<span class="hljs-built_in">String</span> s, MqttMessage mqttMessage) throws Exception {
                  System.out.println(<span class="hljs-string">"Sub message"</span>);
                  System.out.println(<span class="hljs-string">"Topic : "</span> + s);
                  System.out.println(<span class="hljs-keyword">new</span> <span class="hljs-built_in">String</span>(mqttMessage.getPayload())); <span class="hljs-comment">//打印输出消息payLoad</span>
              }
      
              @Override
              public <span class="hljs-keyword">void</span> deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
      
              }
          });
      
      }
      
      <span class="hljs-comment">/**
       * 初始化 Client 对象
       */</span>
      private <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> initAliyunIoTClient() {
      
          <span class="hljs-keyword">try</span> {
              <span class="hljs-comment">// 构造连接需要的参数</span>
              <span class="hljs-built_in">String</span> clientId = <span class="hljs-string">"java"</span> + System.currentTimeMillis();
              <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>&gt; params = <span class="hljs-keyword">new</span> HashMap&lt;&gt;(<span class="hljs-number">16</span>);
              params.put(<span class="hljs-string">"productKey"</span>, productKey);
              params.put(<span class="hljs-string">"deviceName"</span>, deviceName);
              params.put(<span class="hljs-string">"clientId"</span>, clientId);
              <span class="hljs-built_in">String</span> timestamp = <span class="hljs-built_in">String</span>.valueOf(System.currentTimeMillis());
              params.put(<span class="hljs-string">"timestamp"</span>, timestamp);
              <span class="hljs-comment">// cn-shanghai</span>
              <span class="hljs-built_in">String</span> targetServer = <span class="hljs-string">"tcp://"</span> + productKey + <span class="hljs-string">".iot-as-mqtt."</span>+regionId+<span class="hljs-string">".aliyuncs.com:1883"</span>;
      
              <span class="hljs-built_in">String</span> mqttclientId = clientId + <span class="hljs-string">"|securemode=3,signmethod=hmacsha1,timestamp="</span> + timestamp + <span class="hljs-string">"|"</span>;
              <span class="hljs-built_in">String</span> mqttUsername = deviceName + <span class="hljs-string">"&amp;"</span> + productKey;
              <span class="hljs-built_in">String</span> mqttPassword = AliyunIoTSignUtil.sign(params, deviceSecret, <span class="hljs-string">"hmacsha1"</span>);
      
              connectMqtt(targetServer, mqttclientId, mqttUsername, mqttPassword);
      
          } <span class="hljs-keyword">catch</span> (Exception e) {
              System.out.println(<span class="hljs-string">"initAliyunIoTClient error "</span> + e.getMessage());
          }
      }
      
      public <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> connectMqtt(<span class="hljs-built_in">String</span> url, <span class="hljs-built_in">String</span> clientId, <span class="hljs-built_in">String</span> mqttUsername, <span class="hljs-built_in">String</span> mqttPassword) throws Exception {
      
          MemoryPersistence persistence = <span class="hljs-keyword">new</span> MemoryPersistence();
          mqttClient = <span class="hljs-keyword">new</span> MqttClient(url, clientId, persistence);
          MqttConnectOptions connOpts = <span class="hljs-keyword">new</span> MqttConnectOptions();
          <span class="hljs-comment">// MQTT 3.1.1</span>
          connOpts.setMqttVersion(<span class="hljs-number">4</span>);
          connOpts.setAutomaticReconnect(<span class="hljs-literal">false</span>);

      // connOpts.setCleanSession(true);

          connOpts.setCleanSession(<span class="hljs-literal">false</span>);
      
          connOpts.setUserName(mqttUsername);
          connOpts.setPassword(mqttPassword.toCharArray());
          connOpts.setKeepAliveInterval(<span class="hljs-number">60</span>);
      
          mqttClient.connect(connOpts);
      }
      
      <span class="hljs-comment">/**
       * 汇报属性
       */</span>
      private <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> postDeviceProperties() {
      
          <span class="hljs-keyword">try</span> {
              System.out.println(<span class="hljs-string">"上报属性值"</span>);
              <span class="hljs-built_in">String</span> payloadJson = <span class="hljs-string">"{"params":{"CurrentHumidity":12.3,"CurrentTemperature":12.3,"GeoLocation":{"CoordinateSystem":1,"Latitude":29.93089,"Longitude":121.59923,"Altitude":10}}}"</span>;
              <span class="hljs-comment">// https://yq.aliyun.com/articles/706989</span>
              MqttMessage message = <span class="hljs-keyword">new</span> MqttMessage(payloadJson.getBytes(<span class="hljs-string">"utf-8"</span>));
              message.setQos(<span class="hljs-number">1</span>);
              mqttClient.publish(pubTopic, message);
          } <span class="hljs-keyword">catch</span> (Exception e) {
              System.out.println(e.getMessage());
          }
      }

      }


      payLoad设备参考链接


      3、属性上报情况查看


      _


      4、通过物联网数据分析中的二维数据可视化功能,接入设备位置到地图


      _


      _


      _


      5、物联网数据分析通过SQL将数据下发至设备




      • 5.1 查询数据
        _

      _


      _


      • 5.2 使用SQL下发数据

      insert into ${pk.a1kVH.device1} select 53.3 as CurrentHumidity;  -- 导入数据到表,下发数据到设备

      _


      • 5.3 设备端订阅情况

      上报属性值
      Sub message
      Topic : /sys/a1kVH/device1/thing/service/property/set
      {"method":"thing.service.property.set","id":"419651605","params":{"CurrentHumidity":53.3},"version":"1.0.0"}



      参考链接


      快速接入设备位置到地图
      数据开发之分析决策直达设备

      ]]>
      阿里云物联网平台设备上下线信息通过规则引擎流转到RDS示例-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述


      阿里云物联网平台数据流转Topic:/as/mqtt/status/{productKey}/{deviceName} 获取设备的上下线状态。这里演示如何将设备的上下线信息通过规则引擎将消息流转到RDS数据库。


      Step By Step


      1、产品、设备创建及SDK连接


      基于开源JAVA MQTT Client连接阿里云IoT
      阿里云物联网平台Qucik Start


      2、创建表


      /------- CREATE SQL---------/
      CREATE TABLE onlineoffline (
      deviceName text,
      status text,
      time datetime DEFAULT NULL,
      productKey text
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8

      3、规则引擎配置


      • 3.1 处理数据

      _


      • 3.2 转发数据

      _
      _


      注意:配置完规则引擎后务必启动。

      4、流转数据查看


      • 4.1 日志查询

      _


      • 4.2 数据库查询

      _


      更多参考


      数据转发到云数据库RDS

      ]]>
      阿里云物联网平台IoT Studio调用数据分析API示例-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:俏巴

      概述


      前面在博客阿里云物联网平台数据分析API调用介绍了如何使用SDK调用数据分析开发的API,本文主要介绍如何在IoT Sudio Web可视化和服务开发中如果调用数据分析API。


      Step By Step


      1、创建数据开发


      使用示例


      注意:查询车位使用状态SQL 当前文档还不完善,后续会更新。

      2、查询停车场使率SQL API创建


      • 2.1 测试运行SQL

      _


      • 2.2 创建及发布API

      _
      _
      _
      _
      _


      3、服务开发中对API的调用


      • 3.1 模块及参数配置

      _


      _


      _




      • 3.2 NodeJS节点脚本

      /**
      @param {Object} payload 上一节点的输出@param {Object} node 指定某个节点的输出@param {Object} query 服务流第一个节点的输出@param {Object} context { appKey, appSecret }
      */

      module.exports = async function(payload, node, query, context) {

      console.log("payload: ", payload);

      const Core = require('@alicloud/pop-core');

      <span class="hljs-keyword">var</span> client = <span class="hljs-keyword">new</span> Core({
        accessKeyId: <span class="hljs-string">'LTAIOZZg********'</span>,
        accessKeySecret: <span class="hljs-string">'v7CjUJCMk7j9aKduMAQLjy********'</span>,
        endpoint: <span class="hljs-string">'https://iot.cn-shanghai.aliyuncs.com'</span>,
        apiVersion: <span class="hljs-string">'2018-01-20'</span>
      });
      
      <span class="hljs-keyword">var</span> params = {
        <span class="hljs-string">"RegionId"</span>: <span class="hljs-string">"cn-shanghai"</span>,
        <span class="hljs-string">"ApiSrn"</span>: <span class="hljs-string">"acs:iot:*:18482178********:serveapi/getrate"</span>
      }
      
      <span class="hljs-keyword">var</span> requestOption = {
        method: <span class="hljs-string">'POST'</span>
      };
      
      result1 = client.request(<span class="hljs-string">'InvokeDataAPIService'</span>, params, requestOption);
      <span class="hljs-keyword">return</span> result1;

      }


      • 3.3 部署调试

      _




      4 Web 可视化工具中使用API


      注意:之前必须要在服务开发中使用上述方式封装后才能调用,现在支持直接调用数据分析API

      • 4.1 为文本空间配置数据源

      _


      _


      • 4.2 设置过滤脚本

      _


      function _filter(data) {
      // do something...
      return data.data[0].usage_ratio;
      }

      • 4.3 预览测试

      _


      参考链接


      阿里云物联网平台数据分析API调用
      阿里云数据分析最佳实践:二维数据可视化 + 设备数据下发

      ]]>
      阿里云新品发布会周刊第47期 丨 加入阿里技术团队三年,哪些习惯让我在工作上持续受益? -阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 点击订阅新品发布会

      新产品、新版本、新技术、新功能、价格调整,评论在下方,下期更新!关注更多新品发布会!

      热门阅读

      1、加入阿里技术团队三年,哪些习惯让我在工作上持续受益?
      9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

      根据工作经验和观察身边优秀的同事,我发现优良的工作习惯是区别一般工程师和专家工程师的重要素质。想要提升自己,必须要认识到哪些工作习惯会拖延工作效率,提升项目复杂度,增加沟通难度,甚至让合作伙伴失望,然后改正它们。刻意练习那些被证明有效实用的工作方式,成为习惯。 查看原文

      2、当打之年,非你莫属——阿里云 MVP第12期全球发布
      9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg
      阿里巴巴董事会主席兼首席执行官张勇认为:如今,5G网络、工业互联网、物联网等网络基础、数据中心等数字基础、人工智能等运算基础,成为必要而普遍的新型基础设施。加快“新基建”进度,不是简单的基础设施建设,而是与产业化应用协调推进,既能增强基建稳增长的传统属性,又可以助推创新和拓展新消费、新制造、新服务。 查看原文

      3、五分钟学后端技术:一篇文章告诉你什么是云计算!

      早在十年前,市场上就出现了很多和云计算相关的岗位,当时正是云计算技术最火热的时代,不管是BAT还是华为等企业都开始布局云计算,于是OpenStack研发、容器研发、底层开发等相关岗位相应地也越来越多,虽然这几年大数据和AI的风头已经完全压过了云计算,但是这一门技术仍然在现如今的技术体系中占有很重要的位置。那么,到底什么是云计算,就是我们每一个要学习云计算技术的朋友要了解的事情了。 查看原文

      精品直播

      第88期:分析型数据库AnalyticDB for MySQL基础版重磅发布

      直播时间:2020年4月8日 15:00-16:00 预约观看
      TB1JzgxAHj1gK0jSZFuXXcrHpXa-780-388.jpg
      AnalyticDB for MySQL作为新一代云原生敏捷数据仓库,旨在帮助用户快速构建一套高性能、高可用、高可靠、高弹性和智能化的云数据仓库,更好的实现用户数据价值在线化。本次分享主要针对AnalyticDB for MySQL基础版特性以及用户如何0门槛和超低成本构建实时数据仓库。

      第89期: Serverless工作流重磅发布

      直播时间:2020年4月15日 15:00-16:00 预约观看

      对于分布式应用和微服务而言,往往涉及构建复杂的、多步骤的、有状态的和长时间运行的业务流程。本次发布会将向大家隆重介绍阿里云Serverless工作流,致力于简化开发和运行业务流程所需要的任务协调、状态管理以及错误处理等繁琐工作。此外,该服务可协调各类分布式组件,减少流程代码量,在实现丰富控制逻辑的同时提高应用容错性,其自动扩展助力客户免于管理硬件预算和扩展。


      往期回顾

      第87期:云数据库SQL Server 2019新版本发布会 活动页面 直播视频

      RDS SQL Server作为一款重要的关系型数据库产品,它以优秀的性能、可靠的稳定性以及高安全性被广大客户广泛用于生产系统中,阿里云RDS SQL Server拥有完整的产品形态和版本,部署形态从基础版、高可用版到集群版;规格上从独享型、通用型到共享型,从单租户到多租户;并以此构建完整的生命周期管理、备份还原和智能化运维体系。最新推出了经济易用的重磅产品和特性,这里将会有详细的解读。

      第86期:OceanBase新品发布 活动页面 直播视频

      OceanBase是100%自研的金融级分布式关系数据库,在普通硬件上实现金融级高可用,在金融行业首创’三地五中心’城市级故障自动无损容灾新标准,同时具备在线水平扩展能力,创造了6100万次/秒处理峰值的业内纪录,在功能、稳定性、可扩展性、性能方面都经历过严格的检验 。有着优异成绩的它还有什么亮点呢?

      第85期:解决方案体验中心SEC重磅发布 活动页面 直播视频

      想了解一个解决方案的更多细节, 图文介绍限制了想象力怎么办? 线下Demo不能随时随地体验怎么办? 阿里云全新打造的线上解决方案深度体验平台SEC将给您带来7*24小时在线的一站式解决方案新体验。本次直播将进行SEC的发布跟全面介绍, 并通过SEC平台深度展示跟介绍阿里云智慧教学解决方案。

      第84期:Redis企业版 & 专享主机组新品发布会 活动页面 直播视频

      随着淘宝网的流量快速增长,数据库的压力与日俱增,基于后端系统的缓存技术应运而生。从服务淘宝详情和验证码等业务的持久化系统TBStore,到初始服务于淘宝用户中心的TDBM等等,后端系统缓存技术经历了多个系统和阶段的演变与积累,到2009年,这些系统、技术经验经过进一步的研发,融合成了阿里巴巴大规模高速存储系统Tair,也就是阿里云数据库Redis企业版 。

      第83期:阿里云混合云管理平台重磅发布 活动页面 直播视频

      混合云管理平台是面向混合云场景的一体化企业级运营管理平台,提供对公有云,私有云和混合云统一的集成管理,从关注应用、敏捷应变、优化成本出发,提供简单、智能、开放的云管平台,让客户更好的管理云环境,获得更好的运维管理体验。

      第82期:阿里云视觉智能开放平台发布会 活动页面 直播视频

      视觉智能技术作为AI落地最广的技术之一如今已应用在社会的各个角落,包括人脸人体,文字识别,商品识别,视频理解,内容安全等等耳熟能详的应用。视觉能力赋予了计算机感知世界的能力,如今随着技术的发展,视觉计算正逐渐从感知走向认知。阿里巴巴经济体在这一块投入了大量的资源和长期的努力(包括达摩院),研发并应用了众多优质的视觉能力,形成了体系化的视觉智能技术,这些能力在经济体上诸多明星应用上发挥价值,包括淘宝、天猫、支付宝、优酷等。如今,阿里云将这些在明星产品和解决方案中得以锤炼过的视觉API原子能力公开,帮助企业和开发者高效研发,创造更多,更强的视觉智能应用。

      产品动态

      新功能/版本:

      1、智能语音交互 - 公共云实时转写语音识别服务支持高级vad参数功能,可进一步满足用不同场景下的定制化vad需求 查看产品 产品文档

      实时转写语音识别服务支持高级vad参数功能,可进一步满足用不同场景下的定制化vad需求,比如延迟敏感型用户可以通过vad特定优化 。

      2、分析型数据库PostgreSQL版 - 云原生数仓 AnalyticDB for PostgreSQL 支持 SQLServer 数据同步 查看产品 产品文档

      分析型数据库PostgreSQL版(原HybridDB for PostgreSQL)为您提供简单、快速、经济高效的 PB 级云端数据仓库解决方案。分析型数据库PostgreSQL版 兼容 Greenplum 开源数据仓库,为一种采用 MPP 全并行架构的数仓服务,其广泛兼容 PostgreSQL/Oracle 的语法生态,新一代向量引擎性能超越传统数据库引擎 10 倍以上,分布式SQL优化器实现复杂查询语句免调优。通过分析型数据库PostgreSQL版可以实现对海量数据的即席查询分析、ETL 处理及可视化探索,是各行业有竞争力的云上数据仓库解决方案。

      3、数据管理 - 数据管理DMS数据开发功能升级新增数仓开发 查看产品 产品文档

      数据管理(Data Management)支持MySQL、SQL Server、PostgreSQL、PPAS、Petadata等关系型数据库,DRDS等OLTP数据库,ADS、DLA等OLAP数据库和MongoDB、Redis等NoSQL的数据库管理,同时还支持Linux服务器管理。它是一种集数据管理、结构管理、用户授权、安全审计、数据趋势、数据追踪、BI图表、性能与优化和服务器管理于一体的数据管理服务。用户使用数据管理服务实现易用的数据库和服务器统一管理入口,让数据更安全、管理更高效、数据价值更清晰。

      4、 Elasticsearch - 支持用户手动更新至最新AliElasticsearch通用内核版本,一键体验最新功能特性 查看产品 产品文档

      智能顾问Advisor根据用户情况,结合阿里云长期以来的客户侧最佳实践,基于TAM(Technical Account Management)服务体系的核心基础能力,全方位地为用户提供云资源、应用架构、业务性能及安全上的诊断和优化建议。现在,越来越多的阿里云云原生客户可以通过Advisor便捷地享受专业的TAM基础服务,更好地用好云。同时,我们也会围绕Advisor为有相关需求的客户提供专项深度的TAM服务。

      4月特惠
      102354402a5946aca8b51c3069de1839.jpg
      点击领取大礼包

      资料下载

      首发 | YouTube深度学习推荐模型最全总结

      基于 YouTube 的商业模式和内容特点,其推荐团队构建了两个深度学习网络分别考虑召回率和准确率的要求,并构建了以用户观看时长为优化目标的排序模型,最大化用户观看时长并进而产生更多的广告曝光机会,下面详细介绍 YouTube 推荐系统的模型结构和技术细节。

      重磅下载!《2020前端工程师必读手册》,阿里巴巴前端委员会推荐!

      阿里巴巴前端委员会推荐!覆盖5大热点前端技术方向10+核心实战的前端手册--《2020前端工程师必读手册》已经正式上线了,大家可以免费下载了,解锁前端新方式,挖掘前端新思路,尽在此刻,赶紧来先睹为快!

      扫描二维码加入社区圈
      5555

      ]]>
      MYSQL SUBQUERY执行过程-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 实为吾之愚见,望诸君酌之!闻过则喜,与君共勉

      环境

      version | 5.6.24-debug |
      | version_comment | Source distribution |
      | version_compile_machine | x86_64 |
      | version_compile_os | Linux |

      SQL

      该SQL是一个subquery SQL

      SELECT h_1.*, o.S FROM h h_1, p o WHERE o.id = h_1.T AND h_1.id IN ( SELECT substring_index(GROUP_CONCAT(h_11.id ORDER BY h_11.C DESC), ',', 1) FROM h h_11, p o1 WHERE h_11.HI = 90 AND h_11.F = 81 AND o1.id = h_11.T GROUP BY T )

      问题

      subquery内的单独的SQL耗时0.01S,合并起来后,整个SQL耗时4min20S,耗时非常长

      执行时间与执行计划对比

      整个SQL的执行时间与执行计划:

      SELECT h_1.*, o.S FROM h h_1, p o WHERE o.id = h_1.T AND h_1.id IN ( SELECT substring_index(GROUP_CONCAT(h_11.id ORDER BY h_11.C DESC), ',', 1) FROM h h_11, p o1 WHERE h_11.HI = 90 AND h_11.F = 81 AND o1.id = h_11.T GROUP BY T )

      7 rows in set (4 min 20.57 sec)

      id select_T table T possible_keys key key_len ref rows Extra
      1 PRIMARY o ALL PRIMARY NULL NULL NULL 150 NULL
      1 PRIMARY h_1 ref idx_T idx_T 5 alitest.o.id 278 Using where
      2 DEPENDENT SUBQUERY h_11 index_merge index_HI,idx_T,idx_F idx_F,index_HI 5,5 NULL 6 Using intersect(idx_F,index_HI); Using where; Using filesort
      2 DEPENDENT SUBQUERY o1 eq_ref PRIMARY PRIMARY 4 alitest.h_11.T 1 Using index

      SQL拆分执行时间如下:

      subquery SQL:

      SELECT substring_index(GROUP_CONCAT(h_11.id ORDER BY h_11.C DESC), ',', 1) FROM h h_11, p o1 WHERE h_11.HI = 90 AND h_11.F = 81 AND o1.id = h_11.T GROUP BY T

      7 rows in set (0.01 sec)

      id select_T table T possible_keys key key_len ref rows Extra
      1 SIMPLE h_11 index_merge index_HI,idx_T,idx_F idx_F,index_HI 5,5 NULL 6 Using intersect(idx_F,index_HI); Using where; Using filesort
      1 SIMPLE o1 eq_ref PRIMARY PRIMARY 4 alitest.h_11.T 1 Using index

      外层SQL:

      SELECT h_1.*, o.S FROM h h_1, p o WHERE o.id = h_1.T

      60000 rows in set (1.38 sec)

      id select_T table T possible_keys key key_len ref rows Extra
      1 SIMPLE o ALL PRIMARY NULL NULL NULL 150 NULL
      1 SIMPLE h_1 ref idx_T idx_T 5 alitest.o.id 278 NULL

      问题分析

      分析方法

      借助GDB调试MYSQL,确认问题

      耗时环节代码

      该SQL整体执行时,代码的主要执行部分分为2部分,这两部分构成了MYSQL的nested loop算法,分别如下:

      代码1

      sub_select (join=0x7fbe78005808, join_tab=0x7fbe78006738, end_of_records=false) at /opt/mysql-5.6.24/sql/sql_executor.cc:1203

      主要代码块:该代码块以while进行循环,获取多表关联时第一个表的数据(取决于执行计划的执行顺序)循环读取并进行比较判断,while循环结束的前提是error<0,也就是数据取完

      while (rc == NESTED_LOOP_OK && join->return_tab >= join_tab)
      
        {
      
          int error;
      
          if (in_first_read)
      
          {
      
            in_first_read= false;
             //表的read first record记录
            error= (*join_tab->read_first_record)(join_tab);
      
          }
      
          else
             ////取出表的下一行记录直到最后一条记录
            error= info->read_record(info);
      
          DBUG_EXECUTE_IF("bug13822652_1", join->thd->killed= THD::KILL_QUERY;);
      
          if (error > 0 || (join->thd->is_error()))   // Fatal error
      
            rc= NESTED_LOOP_ERROR;
      
          else if (error < 0)
            //以error状态判断数据是否取完,取完后循环在此终止
            break;
      
          else if (join->thd->killed)     // Aborted by user
      
          {
      
            join->thd->send_kill_message();
      
            rc= NESTED_LOOP_KILLED;
      
          }
      
          else
      
          {
      
            if (join_tab->keep_current_rowid)
      
              join_tab->table->file->position(join_tab->table->record[0]);
             //对获取到的行记录,进行比较,该函数内部可能会继续调用sub select,产生nest loop
            rc= evaluate_join_record(join, join_tab);
      
          }
      
        }

      代码2

      evaluate_join_record (join=0x7fbe64005478, join_tab=0x7fbe640063a8) at /opt/mysql-5.6.24/sql/sql_executor.cc:1449

      主要代码块:

      @@1部分主要对拿到的数据进行判断,确认是否符合where后的条件,以该SQL为例,如果从表h h_1里拿到了一行数据,因为该表where后有判断条件,条件为:

      h_1.id IN ( SELECT substring_index(GROUP_CONCAT(h_11.id ORDER BY h_11.C DESC), ',', 1) FROM h h_11, p o1 WHERE h_11.HI = 90 AND h_11.F = 81 AND o1.id = h_11.T GROUP BY T )

      则该代码块(@@1)会对这个subquery进行调用(相当于重新执行一次这个subquery,gdb跟踪时可以跟踪到最终调用JOIN::exec->do_select->sub_select->evaluate_join_record),所以没取一次数据,就要对其进行判断,故这个subquery每一次都要重新执行,它并不是只执行一次拿到数据然后对比。

      @@2 部分的*join_tab->next_select会重新调用sub_select,进入循环部分,获取下一个关联表的数据,并再次进入evaluate_join_record 进行一系列判断,直至数据取完

      @@1

       if (condition)
      
        {
      
          found= MY_TEST(condition->val_int());
      
          if (join->thd->killed)
      
          {
      
            join->thd->send_kill_message();
      
            DBUG_RETURN(NESTED_LOOP_KILLED);
      
          }
      
          /* check for errors evaluating the condition */
      
          if (join->thd->is_error())
      
            DBUG_RETURN(NESTED_LOOP_ERROR);
      
        }

      @@2

      enum enum_nested_loop_state rc;
      
            /* A match from join_tab is found for the current partial join. */
      
            rc= (*join_tab->next_select)(join, join_tab+1, 0);
      
            join->thd->get_stmt_da()->inc_current_row_for_warning();
      
            if (rc != NESTED_LOOP_OK)
      
              DBUG_RETURN(rc);

      推测和结论

      从代码调试的结果看,subquery并不是执行一次就结束,mysql针对这个查询,会先执行外层查询(while循环,具体循环次数取决于记录数),然后每一次都要调用evaluate_join_record 进行判断(无论是p o表还是h h_1表),当取h h_1表时,每一次读取都会对subquery进行一次编译,循环往复,直至数据取完,所以在这个过程中,subquery的SQL会被执行很多次,造成耗时增加。

      https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-extra-information

      For DEPENDENT SUBQUERY, the subquery is re-evaluated only once for each set of different values of the variables from its outer context.

      解决办法

      改写为join查询:

      SELECT h_1.*, o.S FROM h h_1, p o, ( SELECT SUBSTRING_INDEX(GROUP_CONCAT(h_11.id ORDER BY h_11.C DESC), ',', 1) AS ceshi FROM h h_11, p o1 WHERE h_11.HI = 90 AND h_11.F = 81 AND o1.id = h_11.T GROUP BY T ) alitest WHERE o.id = h_1.T AND h_1.id = alitest.ceshi

      id select_T table T possible_keys key key_len ref rows filtered Extra
      1 PRIMARY ALL NULL NULL NULL NULL 5 100.00 Using where
      1 PRIMARY h_1 eq_ref PRIMARY,idx_T PRIMARY 4 alitest.ceshi 1 100.00 Using where
      1 PRIMARY o eq_ref PRIMARY PRIMARY 4 alitest.h_1.T 1 100.00 NULL
      2 DERIVED h_11 index_merge index_HI,idx_T,idx_F idx_F,index_HI 5,5 NULL 6 83.33 Using intersect(idx_F,index_HI); Using where; Using filesort
      2 DERIVED o1 eq_ref PRIMARY PRIMARY 4 alitest.h_11.T 1 100.00 Using index
      ]]>
      MaxCompute问答整理之2020-03月-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 本文是基于本人对MaxCompute产品的学习进度,再结合开发者社区里面的一些问题,进而整理成文。希望对大家有所帮助。

      问题一、在 MaxCompute SQL执行过程中,报错Table xx has n columns, but query has m columns如何处理?
      MaxCompute SQL使用INSERT INTO/OVERWRITE插入数据时,需要保证SELECT查询出 来的字段和插入的表的字段匹配,匹配内容包括顺序、字段类型,总的字段数量。目前 MaxCompute不支持插入表的指定字段,其他字段为NULL或者其他默认值的情况,您可以 在SELECT的时候设置成NULL,例如SELECT ‘a’,NULL FROM XX。

      问题二、MaxCompute 中使用什么方法可以实现相同字段连接,将group by产生的同一个分组中的值连接起来,返回一个字符串结果。类似MySql中的group_concat()函数?
      MaxCompute可以使用WM_CONCAT函数来实现相同字段连接。具体函数说明可参考:
      https://help.aliyun.com/document_detail/48975.html

      问题三、如何在MaxCompute Java SDK上使用Logview排错?
      MaxCompute Java SDK提供了Logview接口 i = SQLTask.run(odps, sql); String logview = odps.logview().generateLogView(i, 7 * 24);

      问题四、如何进行增量数据同步到MaxCompute?
      可以尝试两种方式,一种不变的数据进行增量同步,一种是会变的数据进行增量同步(不推荐使用此方式,只有如不支持Delete语句),可参考文档:https://help.aliyun.com/document_detail/87157.html

      问题五、MaxCommpute中,如何修改表的Hash Clustering属性?
      增加表的Hash Clustering属性语句如下:
      ALTER TABLE table_name [CLUSTERED BY (col_name [, col_name, ...]) [SORTED BY (col_name [ASC | DESC] [, col_name [ASC | DESC] ...])] INTO number_of_buckets BUCKETS]
      去除表的Hash Clustering属性的语法格式如下:
      ALTER TABLE table_name NOT CLUSTERED;

      问题六、Tunnel上传数据的时候报错信息如下:You cannot complete the specified operation under the current upload or download status
      错误原因:Session过期或者已经Commit过,需要重新创建Session上传。Tunnel上传时每个Session的生命周期是一天,如果源表数据太大,导致Session超时任务失败时,建议将源表拆分成2个任务执行。

      问题七、如何使用Clone table实现同region不同账号之间的MaxCompute的数据迁移?
      使用Clone table进行数据迁移操作,命令格式为CLONE TABLE <[src_project_name.]src_table_name> [PARTITION(spec), ...]TO <[dest_project_name.]desc_table_name> [IF EXISTS (OVERWRITE | IGNORE)] ;
      可参考文章:https://developer.aliyun.com/article/748158

      问题八、MaxCompute Python UDF如何开启Python 3?
      在执行Python 3 UDF的SQL语句前增加set odps.sql.python.version=cp37;语句一起执行,即可开启Python 3。

      问题九、MaxCompute中如何禁止/恢复生命周期功能?
      可使用禁止/恢复生命周期SQL来设置。具体语法如下:

         ALTER TABLE table_name [partition_spec] ENABLE|DISABLE LIFECYCLE;
      

      问题十、MaxCommpute中,如何查看指定的表或者分区是否存在?
      可使用使用函数TABLE_EXISTS,查询指定的表是否存在。
      使用函数PARTITION_EXISTS,查询指定的分区是否存在。
      具体函数说明可参考:
      https://help.aliyun.com/document_detail/48976.html

      欢迎扫码加入 MaxCompute开发者社区钉钉群,或点击 申请加入。
      2群.png

      ]]>
      面向数据编程:ECS设计模式在数仓中应用的思考-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 前言

      作为一个从Java转去做大数据的开发,尤其是基于Hiv采用SQL的开发来说,抛弃了使用了很久的OOP,面向对象编程的设计思想后,总觉得有点不习惯。传统的web项目中,对SQL的使用更多还是在数据的增删改查上,而在大数据领域,更多复杂的数据分析,数据交并差的处理,导致SQL代码量急速增加,可维护性大幅降低。而SQL本身就是一个面向过程描述的语言,Java中常见的MVC,MVVP等设计模式也不适合套用在SQL身上。那么,是不是应该存在一种设计模式,适用于面向过程的编程设计呢?

      带着这样的疑问,我开始关注面向数据编程。面向数据编程,核心在于数据。我希望数据可以变得更加灵活,方便开发者对它进行加工。同时,加工过程可以做到高内聚,低耦合。带着这样的需求,查阅了很多资料。直到有一次,无意中看到游戏引擎Unity3D采用的ECS设计模式,突发奇想,意识到这是不是可以满足我的需求呢?

      关于游戏开发中的ECS简单介绍

      ECS是Entity-Component-System三个单词的缩写。最早是在2002年的Game Dungeon Siege上被提出来,是为了解决游戏设计中,物体直接数据交互和性能的问题。

      它在游戏开发中的演变逻辑可以参考这篇文章:https://zhuanlan.zhihu.com/p/32787878

      简单的说,Entity、Component、System分别代表了三类模型。

      实体(Entity):实体是一个普通的对象。通常,它只包含了一个独一无二的ID值,用来标记它是一个独立的对象。通常使用整型数字作为它的实现。

      组件(Component):对象一个方面的数据,以及对象如何和世界进行交互。用来标记实体是否需要进行这一方面的处理,通常使用结构体,类或关联数组实现。

      系统(System):每个系统不间断地运行(就像每个系统运行在自己的私有线程上),处理标记使用了该系统处理的组件的每个实体。

       

      它跟传统OOP编程有什么不一样呢?

      最核心差异点在于:传统OOP编程里,我们会先对编程对象进行虚拟化抽象,将共同的一类数据归到父类或者接口中,子类继承或实现对应的接口。在游戏开发中,父类往往是被锁死的,而一旦需要对逻辑作出修改,要么重写实现,要么继承基类进行覆盖。但游戏策划的创意是天天都可能会变化的。从而造成大量子类重复出现,大幅降低。此外,在对于C++语言中,使用对象池优化时就会造成灾难性的后果——一种类型一个池。

      其次,从计算机底层数据传输上来说,传统OOP在传递数据时都是采用对象进行封装。但通常需要用到的数据只是对象中一两个属性。对于大部分web应用上来说,多读取的对象数据影响不大,但对数据密集型计算(例如游戏图像领域),则对性能会产生影响。

       

      而ECS就是可以解决以上问题。ECS全写即“实例-组件-系统”的设计模式。简言之,实例就是一个游戏对象实体,一个实体拥有众多的组件,而游戏系统则负责依据组件对实例做出更新。

      举个例子,如果对象A需要实现碰撞和渲染,那么我们就给它加一个碰撞组件和一个渲染组件;如果对象B只需要渲染不需要碰撞,那么我们就给它加一个渲染组件即可。而在游戏循环中,每一个系统都会遍历一次对象,当渲染系统发现对象持有一个渲染组件时,就会根据渲染组件的数据来执行相应的渲染过程。同样的碰撞系统也是如此。

      也就是说游戏对象需要什么就会给自己加一个组件。而系统会依据游戏对象增加了哪些组件来做出行为。换言之实例只需要持有必要的数据,由系统负责逻辑就行了。由于只需要持有必要数据,因此对于缓存是非常友好的。这也就是ECS模式能和数据驱动很好结合的一个原因。

       

      对于ECS在数仓建设应用中的一些思考

      对于数仓建设,也是一个面向数据驱动的开发。因此我将ECS和数仓的代码联系起来,思考如何将ECS的设计模式在数仓中应用。我给出了以下的一些想法:

      一个基本假设:

      在数仓中,如果可以抛弃pk依赖后,一张表就是一群Schema的合集。

      这是我对数仓中数据构成的根本假设。如果一张表里的其他Schema被PK约束,自然会导致Schema直接产生逻辑关系。如果没有PK,那么各个Schema互相之间是平等的,Schema之间可以互相组合。表只是由一个个的Schema填充而成的。这样听起来是不是很像Entity和Component之间的关系呢?

      所以我大胆的列出一个映射关系。

      与ECS的关系映射:

      Entity对应于数仓中的Table,Component对应Schema,System对应数仓中SQL逻辑。

      image.png

       

      对于一张表来说,又若干个Schema构成。对于SQL代码来说,它关心的只是要用到的Schema,而不是表的业务逻辑。一张表可以由多个不同的SQL共同产出。所以依赖关系可以是这样的:

      image.png

      SQL只需关心它加工逻辑中需要用到什么Schema,产出什么Schema;Table只需要关心,它的业务逻辑是由哪几个Schema组成;而Schema自己只需要关心,自己代表什么原子含义。

      ECS模式下的SQL伪代码简单实现

      在SQL语言,我们一般代码会写成这样:

      Select A1

      From Tbale1

      Where Condition1

      A1代表我们需要的Schema,Table1是表,Condition1是需要满足的条件。

      对于ECS架构来说,这样写违背了System不跟Entity交互的原则。理想的ECS实现是:

      Select Table1.A1

      Where  Condition1

      如果不同表中的Schema都是平等的,那么只需要指出使用的是哪个表里的Schema,和对应的加工条件。无需再将表名列入其中。

      当然,有人会说,不就是多个From Table嘛,多写这一句话也不会怎么样。

      是的,但大多数数仓开发中,并不是简简单单的一张表的处理。往往我们还会遇到很多表之间交并差的情况。这个时候,我们写的最多的代码是:

      select t1.a,t2.b

      from (

      select *

      from table1

      where condition1

      ) as t1 left

      join table2 as t2

      on condition2

       

      对于一个ECS架构,我们的实现是:

      select table1.a,table2.b

      where condition1 and condition2

      这样看起来,代码是不是就简洁明了多了呢?(当然,现阶段SQL语法并不支持这种写法)

       

      另外,我们在处理表数据的时候,经常还会遇到这样一种情况:

      insert into tmp_table1

      select a1,a2,a3……a31,a32,cast(a33 as bigint) as b1

      from table

      where condition1

       

      inset into result_table1

      select a1,a2,a3……a31,a32,b1+1 as c

      from tmp_table1

       

      inset into result_table2

      select a1,a2,a3……a31,a32,b1+2 as c

      from tmp_table1

      从a1到a32 一共32个列名,其实是不需要做任何特殊处理的,只需要根据condition1条件筛选出来。之后我们又要带着a1……a32在两张结果表中进行插入。且不提这样复制粘贴列名操作十分麻烦,容易出错,就是我们是否有必要这么做?

      我们的诉求可能只是修改某一张表里的某一列值,但不得不把这张表的其他字段反复提取插入。

      根据ECS的设计思想,所有列值都是互相平等的。每张表(Entity)只是由列(Component)填充,Sql(System)只是负责逻辑行为。

      那么,实际操作应该是:

      insert into tmp_table1

      select table.a1,table.a2,table.a3……table.a31,table.a32

      where condition1

       

      insert into tmp_table2

      select ,cast(table.a33 as bigint) as b1

      where condition1

       

      inset into result_table1

      from tmp_table1 add colum tmp_table2.b1+1 as c

       

      inset into result_table2

      from tmp_table1 add colum tmp_table2.b1+2 as c

       

      (以上都是伪代码)

      这样写看上去代码行数没变化,但好处是,如果table中结构发生变更,只需修改上层tmp_table1的结构即可,对结果表无感知。这一点上反而有点像OOP中的继承关系。

       

      总结

      思考将ECS设计模式引入数仓设计,本意是希望开发者可以更加关注于逻辑,关注数据如何处理,也就是S的部分。业务则由从列构建表的时候产生。将表结构和数据处理逻辑进行拆分,从而希望能提升SQL代码的可读性和结构性。

      SQL本身是一个非常优秀的描述型语言,给数据处理带来了极大的便利。但在表结构越发复杂的今天,我已经感觉到传统的SQL的局限性。希望通过ECS设计模式的思考,可以大家带来更多的启发,可以让SQL代码像其他工程语言一样,简洁优雅。

       

       

       

       

      ]]>
      阿里云上企业数据安全工作指南-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 概述

      数据是企业的核心资产,如何保护企业的云上数据,是每个企业管理者都应当重视的课题。在云平台提供更为安全便捷的数据保护能力的同时,阿里云根据自身多年的经验积累,结合大量云上客户的最佳实践,提供了一套完整的数据安全解决方案,帮助企业提升云上数据风险防御能力,实现企业核心及敏感数据安全可控。

       

      数据安全相关法律法规和参考标准

      国内:

      《网络安全法》:主要从“个人信息保护”、“数据存储与跨境安全”、“数据(信息)内容安全”几方面予以规制。

      《网络安全等级保护制度2.0》:分别在安全审计(8.1.3.5)、个人信息保护(8.1.4.11)和资产管理(8.1.10.2)等部分提出了有关数据安全的相关要求。

      《个人信息安全规范》(GB/T 35273-2017):规范了企业在开展数据收集、保存、使用、共享、转让、公开披露等个人信息处理活动过程中应当遵循的原则和安全要求。

      《数据安全能力成熟度模型》(GB/T 37988-2019):基于以数据为中心的管理思路,从数据生命周期的角度出发,结合业务发展过程中的安全需求,开展数据安全保障。

      《数据安全法》和《个人信息保护法》:均在“条件比较成熟,任期内拟提请审议的法律草案”之列,《数据安全法》进程较快已在征求意见稿阶段。

      国外:

      《一般数据保护条例》(General Data Protection Regulation,缩写GDPR)是欧盟于2018年5月25日出台的,在欧盟法律中对所有欧盟个人关于数据保护和隐私的规范,涉及了欧洲境外的个人资料出口。GDPR 主要目标为取回公民以及住民对于个人资料的控制,以及为了国际商务而简化在欧盟内的统一规范。

      美国的医疗服务行业必须遵守该国政府1996年颁布的《健康保险隐私及责任法案》(Health Insurance Portability and Accountability Act,缩写HIPAA)。该法案制定了一系列安全标准,就保健计划、供应商以及结算中心如何以电子文件的形式来传送、访问和存储受保护的健康信息做出详细的规定。法案规定在确保私密性的情况下保存病人信息档案六年,还详细规定了医疗机构处理病人信息规范,以及违法保密原则、通过电子邮件或未授权的网络注销病人档案的处罚方案。

       

      阿里巴巴集团及阿里云在数据安全上的相关工作:

      阿里云数据安全承诺:

      用户的云上数据安全,是用户的生命线,也是云上安全整体能力的一个最重要的具象表现。早在2015年7月,阿里云就发起了中国云计算服务商首个“数据保护倡议”,并在公开倡议书明确:运行在云计算平台上的开发者、公司、政府、社会机构的数据,所有权绝对属于客户;云计算平台不得将这些数据移作它用。平台方有责任和义务,帮助客户保障其数据的机密性、完整性和可用性。

      数据全生命周期防御理念:

      《数据安全能力成熟度模型(DSMM)》(GB/T 37988-2019)是阿里巴巴结合多年数据安全实践经验总结并起草的,旨在助力提升全社会、全行业数据安全水位的一份评估标准以及能力建设参考。该标准的发布也填补了数据安全领域在能力成熟度评估标准方面的空白,为组织机构评估自身数据安全水平,进一步提升数据安全能力,提供了科学依据和参考。

      模型中将数据安全过程维度分为数据全生命周期安全和数据通用安全两个过程,并将数据全生命周期划分为:数据采集安全、数据传输安全、数据存储安全、数据处理安全、数据交换安全、数据销毁安全六个阶段。

      数据生命周期安全共包含30个过程域,为企业保护数据提供了可落地的参考。

      阿里云安全架构与数据安全:

      数据是企业的核心资产,企业信息安全建设的最终目标一定是保护企业核心数据,同时数据安全能力建设也一定不应该是某些功能点的堆砌,一定会和企业内部各维度的安全能力发生融合。阿里云在企业云上安全架构层面提出了五横两纵共七个维度的安全架构最佳实践。五个横向维度自下而上分别是云平台安全、用户基础安全、用户数据安全、用户应用安全和用户业务安全;两个纵向维度分别是账户安全以及安全监控和运营管理。对于数据安全的建设,一定是结合各个维度的安全能力,共同提升安全能力的结果。

      阿里云数据安全最佳实践:

      阿里云建议,在企业开展数据安全相关工作的伊始,数据安全管理人员应当在以下三个层面进行梳理,以便在未来构建数据安全能力的过程中,实现有效落地。

      第一步

      数据资产盘点、权责明确与分类分级标准制定

      • 数据资产盘点与权责明确

      随着企业应用的快速发展,数据存储早已不再仅仅依赖于结构化的数据库,各类非结构化数据(如文本、图片、视频类文件等)、缓存类数据、日志类数据以及结构化数据仓库、数据中台系统,都在企业业务发展的过程中扮演着不同的角色。大量的企业数据分散在各类型的云上数据源中,同时在不同的数据源之间进行传输和流转,如何统一化管理如此纷繁复杂的动态和静态数据,对于企业数据安全人员提出了很高的要求。

      因此,在开始数据安全工作前,应当对企业业务系统使用的数据源进行资产盘点。只有明确了数据安全应当关注的目标对象,才能选择合适的安全能力和手段进行防御体系的搭建。

      同时,对于云中的数据权责,通常会根据不同的业务和应用系统进行划分。数据完成采集、传输和存储后,相关数据控制者在使用数据的同时,也应当承担相应的责任,保证数据的安全;同时,对于第三方数据使用者,也应当按需进行数据使用权申请,减小数据滥用的风险。对于数据权责,可以参考GDPR法规中定义的三类数据主体的权责,分别是数据所有者(Data Subject)、数据控制者(Data Controller)和数据处理者(Data Processor),此处不再赘述。

      • 数据分类分级

      对于分类分级,每个企业都会有适用于其自身所在行业以及业务特点的标准和体系。应当明确的是,分类分级标准的制定,并不应当只是纸面上的工作,而应该结合企业所在的行业和业务特点,有针对性的进行设计,其目的是在未来通过自动化的手段,辅以手工的方式,对海量的企业数据资产进行识别与分类,有效定位企业关键以及敏感类信息在企业数据资产中的分布和流转情况,并通过定级有针对性的进行保护。

      阿里云根据多年来服务云上客户的丰富经验,推荐的企业数据分级为3级,分别为:

      数据分级

      数据敏感度

      数据资产描述

      3级

      高敏感程度

      企业核心或高敏感度资产,一旦泄漏,会对企业带来巨大的经济损失,或是对企业的正常运转带来毁灭性的打击。

      2级

      中敏感程度

      企业重要或中敏感度资产,发生泄漏会给企业带来一定的经济或名誉损失,但不会危害企业业务的正常开展。

      1级

      低敏感程度

      企业公开或低敏感度资产,发生泄漏会对企业带来轻量的经济或是名誉上的损失,且不会对业务开展造成影响。

      需要注意的是,企业应当根据自身实际业务情况,制定适合自身的分级标准。对于中小企业,可以根据企业及业务成熟度,将数据分级进行浓缩,仅区分敏感与非敏感类数据,便于安全防护措施快速落地;同时,对于数据量庞大,业务系统繁多,安全能力成熟度较高的企业,可以对分级进行扩展至4或是5级,以实现更为精细化的数据安全管控。阿里云不建议企业数据分级高于5级,因为过多的分级会给对应的措施落地带来很大的难度,致使分级标准形同虚设,也不利于企业的数据安全管理工作的开展。

      最后,阿里云根据多年客户实践,结合阿里巴巴集团自身经验,对于大型企业的数据分类分级标准设计,建议在分级的标准上,增加分类维度作为分级参考,形成二维的数据分级标准,实现更为精细化的保护,避免产生对于同种不同类的数据发生“一刀切”式保护的情况。最常见的分类维度有企业数据与客户数据的区分,阿里巴巴始终倡导“客户第一”,通过二维化的分级标准,能够更明确的区分数据带给企业的价值,同时将保护客户数据作为第一要务,真正做到“客户第一”。以个人敏感信息为例,当个人敏感信息的主体是企业人力资源相关的内部信息时,可以采取2级作为敏感分级,进行必要的管控保障安全,但如果主体为客户时,就需要将分级定为3级并施加强管控,来保护此类企业关键信息。

      第二步

      理解数据生命周期,评估各阶段潜在风险

      • 数据生命周期

      对于数据生命周期的理解,可以将其比喻为人群与地区的关系。数据好比是人群,会在不同的地区间流动。道路是人员流动的通道,房屋是人员停留的场所。通过理解人员的流动来类比数据生命周期,是一种最便于数据安全管理人员理解的方式。不同的区域,房屋的种类也会不同,有常规的民宅,有高档的办公区,功用各自不同,所需配备的安保措施也会不同。不同的区域间有各自的围墙,同时进出不同的区域也可能需要不同的验证方式,这些都是数据安全管理过程中需要思考的问题。能够理解人群与地区的关系,就不难理解数据从采集、传输、存储,到处理、交换,最终销毁的过程了,当然,人群是不存在销毁的场景的。

      《数据安全能力成熟度模型》(GB/T 37988-2019)能够为企业梳理数据安全工作提供可参考的标准,特别是在每个数据生命周期阶段,均列举出了多个数据安全防控点,供企业搭建数据安全体系参考。DSMM模型将此类防控点归总为30个过程域,以此指导数据安全能力构建:

      对于数据各生命周期阶段,通常需要根据实际安全需求,结合紧迫程度和成本分析,选择性的进行风险止血,才能避免企业的重要信息和数据发生泄漏,而这一切,都应该从围绕企业数据风险进行评估开始。

      • 数据风险评估

      数据安全风险评估是企业信息系统建设的安全根基,进行安全评估可以达到“以最小成本获得最大安全保障”的效果。未经安全评估的系统,可能存在大量安全漏洞,给企业业务带来巨大损失,而其中对于数据的风险评估,又是保护企业核心资产的重中之重。对于数据风险评估,通常可以从多个维度进行分析,常见的方法有头脑风暴、定性定量分析、历史事件分析、沙盘推演和红蓝对抗模拟等,在分析的过程中,一般会结合数据生命周期各阶段的数据特点,有针对性地进行判断。常见的数据安全风险及评估记录样例如下表所示:

      数据安全风险

      发生渠道

      发生概率

      造成损失

      风险评估

      阶段应对措施

      核心数据泄漏

      外部入侵

      小(5%)

      ¥¥¥¥¥

      0.25

      采集:分类分级和校验

      存储:加密保存

      内部窃取

      中(10%)

      ¥¥¥¥¥

      0.5

      处理:访问控制、脱敏…

      业务数据泄漏

      合作伙伴

      大(20%)

      ¥¥

      0.4

      存储:分类识别

      传输:通道加密

      共享:脱敏

      业务数据篡改

      内部操作

      中(10%)

      ¥¥

      0.2

      处理:权限管理、审计…

      重要数据丢失

      数据存储

      小(5%)

      ¥¥¥

      0.15

      存储:高可用配置…

      在完成风险分析和评估后,应当将数据风险预判和分析结果进行记录,并在一定时间段后进行复盘,以便跟踪风险发生情况,应对措施是否完善,以及更新据安全潜在风险,保障整体数据安全风险可控。

      第三步

      分析并权衡可接受的风险及成本,理清防御侧重点

      • 成本评估

      安全是企业的成本中心,永远只有相对的安全,没有绝对的安全。通过数据安全风险评估后,企业数据安全管理者能够对目前企业在云上的数据安全风险有一定的认识,同时在分析完应对措施后,会相对应地寻找到一些安全产品及解决方案,这时就要评估,如果采取该措施,是否能够将对应的风险降低,结合风险发生概率,判断该措施所带来的回报是否能够高于发生风险所造成的损失,综合评估成本后,决定最终的能力选择。

      对于云上环境,尤其是公共云,最大的优势就是将CAPEX转换为OPEX以降低成本,对于安全解决方案亦是如此。以最常见的数据库安全审计为例,目前业界普遍的做法是基于数据库为计费单位,按审计的数据库数量以许可的方式进行计价,这种模式本质上仍然是传统的方式,并没有利用到云计算的特性,尽管这种方式便于估算成本,但却没有与实际的业务数据量实现关联,导致的直接结果就是极易发生浪费。阿里云所推荐的安全审计模式是与需要被审计的数据库息息相关的。当一个数据库的数据量较小时,开启安全审计功能所需的成本也应该相对应的低;同样当一个数据库的业务量猛增带来大量的数据访问时,相应的安全审计费用也会上升。这是云平台为云上数据安全赋能的成果,企业也能在进行数据安全方案和产品选型中获益。同时,对于节省下来的安全成本,又能够覆盖更多的数据源,从而进一步提升企业整体数据安全水位。

      • 理清防御重点

      根据阿里云的经验,企业性质、业务属性和行业特性都会影响一个企业对于数据安全的关注点。对于国内企业出海和海外企业入华,通常会着眼于个人隐私数据保护,数据跨境等场景,以满足国内外的监管要求;对于本土的企业,通常会基于国内相关监管与合规要求开展数据安全工作;同时也有大量的企业,例如金融、零售等以数据为核心驱动力的行业,会把数据安全重点放在核心数据资产的保护上。数据对于不同的企业,价值也不尽相同,只有从企业最应当保护的敏感或核心数据入手,逐步搭建或完善企业数据安全体系,才能最大化体现数据安全工作的价值。

      结合数据生命周期,通过数据资产盘点、分类分级、风险分析和成本评估,企业此时应当已经大致明确了需要重点关注的数据安全风险点,即将开启数据安全规划并设计落地相关工作。阿里云为云上客户提供了一整套的数据安全解决方案,协助客户搭建云上数据安全体系,提升防御能力,保护客户的云上数据,实现安全可控。

       

      阿里云数据安全解决方案与建议:

      阿里云为企业客户提供专业的数据安全整体解决方案。通过聚焦企业重点关注的数据安全相关领域和话题,帮助企业客户在云上搭建完善的数据安全体系,提升防御和响应能力,降低整体数据安全风险。

      云上企业数据安全“4+4”核心能力建设

      阿里云参考数据安全成熟度框架(DSMM),基于阿里云最佳实践,通过总结和沉淀大量云上企业数据安全工作经验,提炼出云上企业数据安全需要构建的八大核心能力。分别是:

      • 数据生命周期安全维度四大能力:
        • 数据分类分级(采集阶段)
        • 数据传输加密(传输阶段)
        • 逻辑存储安全(存储阶段)
        • 数据脱敏(使用阶段)
      • 数据通用安全维度四大能力:
        • 数据资产管理
        • 鉴别与访问控制
        • 监控与审计
        • 终端数据安全

      同时,对于企业云上数据安全能力建设,阿里云建议分别从“云端”、“管道”和“终端”三个层面来构建核心能力。

      云端:数据上云后的存储形态,包含各类型数据存储,常见的有数据库、数据仓库、对象存储、缓存等,企业会根据业务形态和应用场景选择合适的存储进行数据的保存。

      管道:数据通信和流转的媒介,主要为网络通道,一般分为站点到站点(Site-to-Site)和客户端到服务端(Client-Server)两种模式。

      终端:各类实现数据采集、输入和使用的用户交互用设备及终端,如APP、网页、应用程序等,通常通过手机和电脑进行操作。

      企业在构建八大数据安全核心能力时,将会在不同的管控层面开展建设。

      1. 数据采集阶段

      组织内部系统中新产生数据,以及从外部系统收集数据的阶段。

      在数据采集阶段,企业通常将通过网络通道,将大量的数据采集并保存在云端,进行数据处理并提供服务。常见的通信管道包含互联网接入和企业专线/专网接入。

      互联网接入:对于海量终端的信息采集,企业一般会通过互联网通道进行。通常在终端侧收集的信息包含个人类信息,业务类信息以及设备类信息;

      专线/专网接入:对于稳定性和通信安全要求较高的大型企业,在内部信息传输时会通过专线接入,通常传输的数据包含业务类信息、公司类信息和设备类信息。

      对于上述信息完成存储后的分类和分级,过往由于缺乏自动化的手段,企业通常会采用手工的方式进行统计,并且在数据量增大的同时无法保持定期更新。伴随着数字化转型,企业在保存海量敏感和重要信息的同时,又面临着个人隐私保护相关法律的要求,如何在采集阶段保证海量数据的安全可治理,是这一阶段的重点。

      • 核心能力1:分类分级

      能力描述:基于法律法规以及业务需求确定组织内部的数据分类分级方法,对生成或收集的数据进行分类分级标识。

      • 阿里云提供的核心服务:敏感数据保护(SDDP)服务

      敏感数据保护(Sensitive Data Discovery and Protection,简称SDDP)作为阿里云第一款自主研发的数据安全可视化产品,充分利用阿里巴巴大数据分析能力以及人工智能相关技术,通过智能化的手段识别敏感数据,并基于业务需求实现自动分类分级,为企业实现高效的数据安全治理提供手段。

      • 自动识别:通过深度学习神经网络,结合关键字和正则匹配等方式,精准识别敏感数据,并可根据业务规则自定义敏感属性。
      • 分类分级:针对敏感数据的识别结果,自动实现数据分类和基于敏感程度的分级,并提供外部数据系统的集成能力。
      1. 数据传输阶段

      数据从一个实体传输到另一个实体的阶段;

      在数据传输阶段,常见的数据流转包括数据入云,在云中的流转和数据出云。

      对于不同传输与交换场景下的数据保护,企业需要灵活运用各种防御措施,常见的防御手段有加密、鉴权、脱敏等方式,而其中最为广泛使用的是传输过程中的加密。

      数据传输过程中使用的网络通道不同,保护方式也会不同。对于客户端通过互联网发起的访问,一般建议企业使用SSL/TLS证书来加密传输通道,防止发生中间人攻击窃取传输过程中的重要数据;对于企业站点间的数据传输,通常会使用VPN或专线对通信链路进行加固。

      同时,建议企业在云内将网络隔离为对外和内部专用网络区域,数据越往内部传输,相应的保护措施也越严格。针对不同的数据传输和流转场景,同样需要不同的技术手段加以保护。

      • 核心能力2:传输加密

      能力描述:根据组织内部和外部的数据传输要求,采用适当的加密保护措施,保证传输通道、传输节点和传输数据的安全,防止传输过程中的数据泄漏。

      • 阿里云提供的核心服务:SSL证书服务

      证书服务(SSL Certificates)为网站和移动应用(APP)提供HTTPS保护,可对Web流量加密,防止数据遭窃取和篡改。阿里云提供完善的运维功能,支持将第三方证书上传管理,支持一键部署证书到CDN、负载均衡(SLB)、OSS等云产品,轻松集中地运维大量证书。

      • 品牌合作
      • 快速签发
      • 一键部署
      1. 数据存储阶段

      数据以任何数字格式进行存储的阶段;

      随着企业的数字化转型,数据会存储在各类云中提供的存储服务中。从最传统的块和文件类存储,到数据库、数据仓库类型的结构化存储,再到缓存、对象存储等新型存储方式,企业的数据会分布在各类应用系统和所使用的数据存储中。如何在数据存储的过程中保护数据的保密性、一致性和可用性,是数据安全在存储阶段的重点。其中对于数据的加密,是企业最常见的保护手段。

      对于不同类型的数据存储,使用加密技术保护数据的核心是对于加密密钥的管理。密钥好比是打开保险箱的钥匙,保管好了密钥,就能从根本上实现对数据存储场景下的安全保护。

      • 核心能力3:逻辑存储安全

      能力描述:基于组织内部的业务特性和数据存储安全要求,建立针对数据逻辑存储、存储容器等的有效安全控制。

      • 阿里云提供的核心服务:密钥管理服务(KMS)

      密钥管理服务(KMS)提供安全合规的密钥托管和密码服务,助您轻松使用密钥来加密保护敏感的数据资产,控制云上的分布式计算和存储环境。您可以追踪密钥的使用情况,配置密钥的自动轮转策略,以及利用托管密码机所具备的中国国家密码管理局或者FIPS认证资质,来满足您的监管合规需求。

      • 完全托管:KMS为您提供了密钥托管和密码服务,阿里云负责密码基础设施的完全托管,保证服务和设施的可用性,安全性以及可靠性。客户还可以通过BYOK的方式,保持一份密钥的拷贝从而获得更多的持久性。
      • 可用、可靠和弹性:KMS在每个地域构建了多可用区冗余的密码计算能力,保证向KMS发起的请求可以得到低延迟处理。
      • 云产品集成加密:KMS与云服务器、云数据库、对象存储、文件存储、大数据计算等阿里云产品广泛集成。
      1. 数据处理阶段

      组织在内部对数据进行计算、分析、可视化等操作的阶段;

      企业在数据使用和处理阶段,通常会涉及大量的不同种类的应用系统,小到一台设备,大到数据中台。伴随着企业数字化转型,对于需要处理的数据量也呈现几何式的增长,同时也不可避免地会放大企业敏感和重要数据的暴露面。通常在大型企业中会通过网络隔离的方式,将核心数据保存在诸如“内网区”的内部区域内,实现访问隔离。但伴随着业务开放、跨部门使用、对外分享等场景的出现,传统的单纯依赖网络隔离的方式越发捉襟见肘。

      在数据处理阶段,阿里云建议引入“数据消敏区”的概念。这里的“数据消敏区”,可以类比为数据层面的“DMZ区域”,实现类似医院手术室中的“消毒区”的功能。首先通过分析数据治理结果,掌握企业敏感数据在云上的分布情况,随后对于需要进行使用、分析和对外分享的敏感数据,在“数据消敏区”中进行统一加工,实现脱敏化处理。将敏感类数据根据不同的业务需求进行“消敏”后,存放在敏感区域外,供分析使用和外部调用,以此减小敏感数据暴露面,降低由于系统直接接触生产或敏感数据而造成的泄漏风险。

      • 核心能力4:数据脱敏

      能力描述:根据相关法律法规、标准的要求以及业务需求,给出敏感数据的脱敏需求和规则,对敏感数据进行脱敏处理,保证数据可用性和安全性之间的平衡。

      • 阿里云提供的核心服务:敏感数据保护(SDDP)服务

      敏感数据保护服务的数据脱敏功能能够实现对云环境内数据源产品间的数据脱敏能力。通过SDDP的静态脱敏能力,用户能够实现生产数据向开发测试环境进行数据的脱敏转存,供进一步的分析使用,避免由于测试系统直接访问源数据带来的数据泄漏风险。同时在数据对外分享时,实现敏感数据脱敏后的分发。

      • 多源异构:SDDP支持的数据源类型包括结构化数据源(如RDS、MaxCompute等)和非结构化数据源(如OSS中保存的结构化数据),并支持异构数据源间的脱敏。
      • 算法丰富:SDDP支持6大类30多种常见的脱敏算法,包括哈希、遮盖、替换、变换、加密及洗牌,能够满足各类业务场景的脱敏需要。
      • 数据通用安全

      针对《数据安全能力成熟度模型》中定义的30个数据安全过程域,阿里云根据企业数据安全能力建设维度,将数据安全能力分为三类:

      云平台提供能力:由云平台提供基础安全能力和数据服务安全配置功能,需要客户根据业务所使用的云上数据服务,进行相应数据安全配置,保护数据安全。

      核心技术能力:由云平台提供的核心数据安全类服务,客户通过购买和配置相关服务的方式,在云上构建企业数据安全核心能力,提升整体云上数据安全水位。

      高阶能力:通常分为管理能力和技术能力两大方面。管理能力需要企业根据国家法律法规相关要求,在数据安全领域建立和维护企业自身的数据安全标准以及内部规章制度,降低企业的数据安全风险;技术能力则会根据业务形态,数据要求和企业成熟度,有选择性地在技术层面进行相关数据安全能力的构建。

      由于《数据安全能力成熟度模型》中所定义的数据安全生命周期和通用部分涉及面较广,阿里云建议客户在构建企业数据安全能力时,充分利用云平台能力,并重点关注核心能力建设,进而根据企业实际情况和成熟度,对高阶能力进行选择性地完善。通过采取分阶段快速迭代的方式,逐步建立有效的数据安全体系。切忌急于求成,丢失建设工作的焦点。

      • 核心能力5:数据资产管理

      能力描述:通过建立针对组织数据资产的有效管理手段,从资产的类型、管理模式方面实现统一的管理要求。

      • 阿里云提供的核心服务:敏感数据保护(SDDP)+各类数据源管理服务

      敏感数据保护服务通过对存储在云上海量的结构化和非结构化数据源中的数据识别,自动对敏感文件和数据进行识别、归类和分级,包括对国内外个人隐私信息、云平台密钥信息、敏感图片类文件等。敏感数据保护服务同时还提供了丰富的API接口,供各类云上数据管理服务调用,通过被集成的方式,实现云上数据安全的统一管理,提升整体数据安全治理能力。

      • 核心能力6:鉴别与访问控制

      能力描述:通过基于组织的数据安全需求和合规性要求建立身份鉴别和数据访问控制机制,防止对数据的未授权访问风险。

      • 阿里云提供的核心服务:权限管理服务(RAM)+数据管理服务(DMS)+应用身份服务(IDaaS)

      针对数据的访问鉴别与控制,需要结合不同的数据存储和使用场景,配置相对应的鉴权能力。参考阿里云安全架构,常见的数据访问包括对云平台的访问、对操作系统的访问、对数据的访问和应用层级的访问。针对不同的访问维度,需要通过不同的手段实现访问的鉴别与控制。阿里云提供了多种安全访问管理和防护机制,帮助企业实现各维度的鉴别与访问控制能力,全方位提升企业云上数据访问控制能力。

      • 核心能力7:监控与审计

      能力描述:针对数据生存周期各阶段开展安全监控和审计,以保证对数据的访问和操作均得到有效的监控和审计,以实现对数据生存周期各阶段中可能存在的未授权访问、数据滥用、数据泄漏等安全风险的防控。

      • 阿里云提供的核心服务:敏感数据保护服务(云原生审计)+自建数据库审计服务

      阿里云敏感数据保护服务通过云原生的手段,提供对云上各类型数据资产的安全监控与审计。除了提供常规的安全审计能力用以检测诸如高危操作、数据库注入等风险以外,SDDP还创新性地为云上数据提供异常检测能力,通过收集和分析相关日志,围绕权限使用和数据流转场景,自动生成历史基线,并基于画像评估数据安全整体运行态势,在发现潜在风险时,通过告警提醒用户,并分析原因和提供修复建议,将事后追溯向事中检测靠拢,提升整体数据安全事件响应能力。同时,阿里云还为云上客户提供了自建数据库的审计服务。

      • 核心能力8:终端数据安全

      能力描述:基于组织对终端设备层面的数据保护要求,针对组织内部的工作终端采取相应的技术和管理方案。

      • 阿里云提供的核心服务:堡垒机服务+终端DLP服务

      阿里云通过提供堡垒机服务,为云上客户提供集中化的云上运维能力,全程记录操作数据并实时还原运维场景,保障云端运维权限可管控、操作可审计、合规可遵从。同时对于远程终端,提供终端防泄漏服务、识别终端中保存的敏感数据,对违规使用、扩散等敏感行为实现策略响应控制。

       

      有关更详细的阿里云数据安全解决方案,欢迎参考:https://www.aliyun.com/solution/security/datasecurity

      ]]>
      Serverless可观察性的最佳实践-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      原文: https://medium.com/adobetech/best-practices-for-serverless-observability-a99d8dc8af5c

      WRITTEN BY

      Ran Ribenzaft

      Co-Founder & CTO @epsagon | AWS Serverless Hero | Entrepreneur, passionate about serverless and microservices.

      翻译:祝坤荣(时序)

      现在看起来每个工程师都熟悉serverless这个词,但离在生产环境大规模使用还很远。意思是,实际上,大部分人在使用serverless时还是没有经验的,并且由于这个原因,很多最佳实践还是缺失的。

      在这篇文章里,我们会深入可观察性,它是每个工程和运维团队在移动到生产环境的核心组件。我们会讨论每个重要的基础:度量,日志和分布式追踪,并提供serverless在真实世界的最佳实践的例子。

      image.png

      可观察性中的度量

      传统简单直接做监控的套路就是去看度量数据。 度量, 尤其是度量中体现的趋势, 可以展示出我们系统的基本统计情况,比如:

      • CPU或内存的使用峰值
      • 流量和请求的趋势
      • 我们使用的跨服务的延迟情况
      image.png
      Google SRE图书中指出了监控分布式系统的4个黄金指标是延迟,流量,错误和饱和度。尽管看起来很简单,它仍然需要一些经验和时间来进行正确的监控。这个流程包括:

      • 从环境,应用,资源和服务上收集所有度量指标。这包括,比如,我们的k8s集群,云资源,Java和Node.js应用,我们的Redis集群。想要记录这些度量信息每个实体都需要一套不同的处理方法。
      • 将度量信息传给一个统一平台,它要处理正确的量级,聚合所有度量指标,展示正确的数据(比如,用百分位替代平均值)。
      • 最终,我们需要为每个应用或环境建一个仪表盘,并且为其中重要的定义合适的报警。

      在serverless化后,CPU和内存变得不那么相关了;不要只盯着调用和错误的基本图表。多看和观察你的function有的每个动作。你调用的任何API都需要被监控。

      可观察性中的日志

      度量指标只能告诉我们’好或坏’。他们不会提供任何信息和一种方式来告诉我们为什么一个应用不工作了。

      要定位问题的种类,我们需要理解我们代码或服务的流程。要达到这个目的我们要打印包括所有从开始到详细异常的日志(到一个文件,socket或服务)。

      image.png
      Elastic中的日志

      每个工程师都熟悉定位问题或bug的场景和要找到正确日志时持续升高的压力。这些问题都是由于日志的一些缺陷和固有的问题:

      • 它们基本上是手动的。日过你没有记录一些事情,它不会出现(然后你补了日志,部署你的代码,并且等着问题再次出现)。
      • 通常,它们没有上下文。这表示你要找到你想要找的日志,你需要对发生在你代码或服务的事件的相关日志进行搜索。
      • 在众多服务中有很多的日志,这很难在它们之间进行导航。

      想要从日志中得到有效的信息:

      • 在你的日志行中填加元数据;例如,服务/函数function名称,场景,请求ID等。
      • 在你使用的代码中自动化日志事件的处理。我们会在下面追踪章节讨论这个。
      • 保证在你的服务中用正确的方式索引日志,然后你可以使用工具来进行分析。使用工具分析日志(用元数据和维度)可以帮助你理解你应用中的复杂趋势。
      • 记录自定义的度量指标。这也适用于之前的基础指标,但它可以帮你发现业务核心指标。比如,上周用户注册的数量。

      可观察性中的分布式追踪

      追踪是可观察性中的重要基础,它在微服务和serverless中度量和日志中扮演重要角色。追踪的目的是收集一次操作的数据,这样我们可以在不同的服务中看到流程。
      image.png

      在一个运行微服务和serverless的现代应用中,我们需要对追踪的“分布式”部分有更多的关注。追踪里最流行的标准是OpenTracing(或新的OpenTelemetry)。分布式追踪描述了一个框架来收集关于事件的数据(比如,一个DB查询,我们会收集主机名,表名,持续时间,操作等),这叫做spans与上下文。它也描述了在你的服务中注入和抽取“追踪ID”。

      有效在代码中抓取追踪的一种推荐的方式是进行增强instrument(https://epsagon.com/blog/instrumentation-for-better-monitoring-and-troubleshooting/),所以每个调用不需要手动。Instrumentation增强修改了一些调用;比如,每次你调用HTTP,它会路由到一个中间件,由其保存追踪信息。

      由于追踪是被一种结构化的方式捕捉的,它让我们可以对日志问一些更有意思的问题;比如,你可以查找所有“insert”操作超过300ms的事件,其被打了一个特定customer ID的标签。

      image.png

                          追踪捕捉了结构化数据
      

      要牢记以下几个关键问题:
      • 增强和追踪你的应用是一个非常长的过程,需要长时间维护。如果你选择自己实现不会快速获胜。
      • 我们只讨论了追踪收集的部分。下一步是传送它们到一些服务。Jaeger(https://www.jaegertracing.io/)可能是展现和搜索追踪信息的主流服务。
      如果要从追踪中得到最大的收获:
      • 用标签来充实你的追踪。标签让你们可以在你的复杂系统中精确定位事件,按维度进行分析,比如,userId=X的一个特定事件有多少次,用了多久。好的标签可以是user ID,商品ID,事件类型,或任何你系统中特定的信息。
      • 因为追踪给日志加入了上下文所以它在问题定位中扮演了核心组件。要做到那样,请考虑下载追踪里记录payload。比如,每一个对DB的调用,增加查询信息;每一个HTTP调用,填加request/reponse的header和body信息。
      • 要在没有任何上下文信息里在成吨的日志或图表里搜索是很难的。通过使用追踪你可以可视化这些在你系统中的复杂服务和事务。

      image.png
      可视化追踪和payload信息是一个故障排查的强大工具(Epsagon)

      总结

      可观察性在每个现代应用中扮演了很重要的部分。它需要很多规划,繁重的维护来应用最佳实践。将每个基础部分分离到不同的工具可以让工程团队有很大的生产力,强化他们的合作。当选择一个工具来整合一切事物时这很重要。

      另外,自动化你的流程以便它们不会对日常工程的工作流产生影响很重要。选一个受控的解决方案可以有很大的优势,就像你从云供应商选择数据库,消息队列,或服务器。

      在Epsagon(https://epsagon.com/),我们在建立一个针对serverless和微服务的追踪和监控的定制方案。你过你有兴趣可以联系我们。

      ]]>
      Flink 生产实践系列文章合集-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 Flink 生产实践系列文章合集

      本文合集涵盖FFA大会中众多大厂所分享的企业实践的优质精华内容:

      | 美团点评基于 Flink 的实时数仓平台实践
      | bilibili 实时平台的架构与实践
      | 小米流式平台架构演进与实践
      | SQL 开发任务超 50% !滴滴实时计算的演进与优化
      | OPPO 实时数仓揭秘:从顶层设计实现离线与实时的平滑迁移
      | 日均万亿条数据如何处理?爱奇艺实时计算平台这样做
      | Netflix:如何打造开放协作的实时 ETL 平台?
      | 菜鸟供应链实时数仓的架构演进及应用场景
      | 覆盖电商、推荐、ETL、风控等多场景,网易的实时计算平台做了啥?
      | 基于 Flink 构建 CEP 引擎的挑战和实践
      | 实时计算在贝壳的实践

      渠道文章宣传内页.png

      ]]>
      Flink 生态系列文章合集-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 Flink 生态系列文章合集

      Flink 生态系列优质内容推荐如下:

      | 趣头条基于 Flink+ClickHouse 构建实时数据分析平台
      | 携程实时智能检测平台建设实践
      | Hive 终于等来了 Flink
      | Flink 生态:一个案例快速上手 PyFlink
      | Flink 如何支持特征工程、在线学习、在线预测等 AI 场景?
      | 性能提升约 7 倍!Apache Flink 与 Apache Hive 的集成
      | 如何在 Apache Flink 1.10 中使用 Python UDF?
      | 为什么说 Flink + AI 值得期待?
      | Flink 1.10 Native Kubernetes 原理与实践
      | Flink 1.10 和 Hive 3.0 性能对比(附 Demo 演示 PPT)
      | 在 Cloudera Data Flow 上运行你的第一个 Flink 例子

      渠道文章宣传内页.png

      ]]>
      零基础入门系列-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 零基础入门系列

      Flink 零基础入门系列优质内容推荐如下:

      | 《Apache Flink 零基础入门(一):基础概念解析》
      | 《Apache Flink 零基础入门(二):开发环境搭建和应用的配置、部署及运行》
      | 《Apache Flink 零基础入门(三):DataStream API 编程》
      | 《Apache Flink 零基础入门(四):客户端操作的 5 种模式》
      | 《Apache Flink 零基础入门(五):流处理核心组件 Time&Window 深度解析》
      | 《Apache Flink 零基础入门(六):状态管理及容错机制》
      | 《Apache Flink 零基础入门(七):Table API 编程》
      | 《Apache Flink 零基础入门(八): SQL 编程实践》
      | 《Apache Flink 进阶(一):Runtime 核心机制剖析》
      | 《Apache Flink 进阶(二):Flink Time 深度解析》
      | 《Apache Flink 进阶(三):Checkpoint 原理解析与应用实践》
      | 《Apache Flink 进阶(四):Flink on Yarn/K8S原理剖析及实践》
      | 《Apache Flink 进阶(五):Flink 数据类型与序列化》
      | 《Apache Flink 进阶(六):Flink 作业执行深度解析》
      | 《Apache Flink 进阶(七):Flink网络流控及反压剖析》
      | 《Apache Flink 进阶(八):详解 Metrics 原理与实战》
      | 《Apache Flink 进阶(九):Flink Connector 开发》

      渠道文章宣传内页.png

      ]]>
      追寻教育行业的下一个时代风口,阿里云数据库和蓝墨are ready!-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 智启蓝墨是一家智能互联网教育公司,公司主要业务是为中高等院校提供智能云教学平台服务,其次提供云教材的制作、出版、发行等云生态平台服务。自2012年成立以来,蓝墨一直致力于帮助学校从教学信息化跨入教学智能化,实现课堂教学的现代化。

      成立第二年,蓝墨便选择使用了阿里云,除了主机产品,最主要的就是数据库产品。由于蓝墨是一个已经运行了一段时间的产品,所以更换数据库的选型过程中,主要的考虑因素是平滑迁移,将应用改动量和迁移风险都降到最低。

      主要的选型范围还是在阿里云的产品来选择,最终选择了PolarDB和AnalyticDB这两个十分给力的产品,分别在事务处理和数据分析领域有着出色的性能,而且先天具备兼容MySQL的优势,都能做到快速上线。

      01、PolarDB :“非常抗打,几十万的 QPS 不在话下”

      从 MySQL 5.5 到 MySQL 5.6,随着用户量的增加,MySQL 明显不足以满足业务需求了,经过介绍,蓝墨了解到了PolarDB。由于它和 MySQL 100% 兼容,迁移到 PolarDB 应用无需做任何改动,只需修改数据库指向就可以。

      所以在 2019 年,蓝墨将业务的数据库迁移到了 PolarDB,迁移之后,运行良好。偶尔有高 CPU 占用的情况,阿里云专家组及时出动,与蓝墨技术同学一起分析场景和SQL,帮助解决问题。

      由于历史原因,蓝墨的业务有很多是 PHP 编写的,众所周知,PHP 编写的 Web 应用到数据库之间是短连接,虽然 MySQL号称对短连接做过优化,并且PolarDB 对短连接的表现也是很好的,但是由于今年疫情,大量的课程转移到线上开课,导致系统负载比去年增加了 25 倍,在这样的压力下,短连接的弱点爆发了。

      由于频繁的向 PolarDB 创建连接,导致 PolarDB CPU 高,通过和阿里云专家组的共同分析找出了关键所在,通过PolarDB透明读写分离方式和在应用与PolarDB 之间增加了一个代理层,代理层负责接受应用的短连接,同时维护到 PolarDB 的长连接。经过这样的结构调整,PolarDB 重回王者地位,QPS 可以稳定的跑在 30万+ 的同时,CPU 负载控制在 30% 以下。

      010101.jpg

      对此,蓝墨的技术总监袁玉表示:“只要应用架构中处理好读写请求区不同的节点,利用PolarDB的读写节点分离功能,就可以很好的分离业务负载。
      计算和存储分离,可以使得在系统容量不足的时候,几分钟就可以扩充一个 PolarDB 的只读节点进来,快速平滑的完成扩容,而无需等待传统读写分离漫长的数据复制的过程,只要使用得当,PolarDB 是非常抗打的,几十万的 QPS 不在话下。”

      PolarDB 计算与存储分离的架构和可以被多个数据库节点共享的分布式存储,所带来的快速弹性能力,是云原生架构的最佳搭档。

      PolarDB产品经理乙休表示:“应对这次疫情,很多诸如智启蓝墨这样的教育企业在云上使用PolarDB都顺利地度过了计划外的突发增长。智启蓝墨在使用过程中也给阿里云PolarDB提出了很多高价值的建议。业务中遇到的解决关键问题的PHP短连接优化功能最近已在PolarDB上线,免费给所有用户使用,欢迎大家体验。“

      02、AnalyticDB:海量数据实时分析,想怎么查就怎么查

      随着业务的突发增长,蓝墨的实时分析诉求越发强烈。实时洞察公司运营状态,全面提升业务快速响应能力,对蓝墨当前的发展至关重要。经过与阿里云专家多次讨论,基于分析性能与使用门槛两大重要因素考量,蓝墨最终采用AnalyticDB作为报表分析库。

      通过阿里云的数据传输服务(DTS)将PolarDB业务数据实时同步到AnalyticDB中,AnalyticDB全面兼容MySQL协议和SQL:2003语法标准,无需修改代码即可快速构建一套实时报表分析系统。该解决方案实现了业务库和报表库全面隔离,完全解耦,业务高峰期时再也不用担心业务库和报表库相互影响。

      蓝墨单表最大数据量超过1T,在高并发情况下可实现毫秒级响应。蓝墨的技术总监开玩笑说:“有一次数据同学写错了SQL,少了一个关联条件而导致全表扫描,同学竟然没有发现,AnalyticDB执行效率太高了“,紧接着他又继续强调:“可以秒甚至毫秒查询出几分钟之前的数据,给我们精细化运营增加了很多可能性,可以进一步降低获客成本。我相信蓝墨一定会在阿里云平台上迎来全新的机遇。”

      03、追寻教育行业下一个时代风口,蓝墨与阿里云携手而来

      信息化、数字化是时代发展的潮流,作为为社会育材的教育行业,教育技术现代化、信息化、数字化也将是必然的趋势,这场突如其来的疫情尤其凸显出了在线教育、教育信息化的重要性。

      毫无疑问, “互联网+” 已成为当今时代经济和社会发展的显著特点,网络信息技术的更新与发展,加速了传统教育数字化的进程,促进了学科课程与信息技术的深度融合,也推动了关于信息化教育的实践与探索。今年这场突如其来的疫情尤其凸显了信息化教育的重要性。

      蓝墨的主营业务属于教育信息化行业里的中高等院校教学信息化领域。本次疫情之前中高等院校教学信息化普及率在30%左右,这次疫情发生后,由于学校被迫实施在线教学,院校教学信息化普及率会达到95%以上,呈现出产业互联网集中化的趋势。

      教学信息化发展的下一个阶段是教学智能化,未来5年预计超过50%的中高等院校教学信息化会跨入教学智能化的行列。

      未来,在追寻教育行业新风口的这条跑道上,蓝墨将与阿里云一起探索奔跑。


      直播预告
      4月8号15:00-16:00
      AnalyticDB for MySQL基础版线上发布会
      教你如何0门槛构建实时数据仓库
      扫描下方二维码预约直播!~

      4。8 ADB.jpg

      ]]>
      深入剖析 Delta Lake:Schema Enforcement & Evolution-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 编译:辰山,阿里巴巴计算平台事业部 EMR 高级开发工程师,目前从事大数据存储方面的开发和优化工作


      在实践经验中,我们知道数据总是在不断演变和增长,我们对于这个世界的心智模型必须要适应新的数据,甚至要应对我们从前未知的知识维度。表的 schema 其实和这种心智模型并没什么不同,需要定义如何对新的信息进行分类和处理。

      这就涉及到 schema 管理的问题,随着业务问题和需求的不断演进,数据结构也会不断发生变化。通过 Delta Lake,能够很容易包含数据变化所带来的新的维度,用户能够通过简单的语义来控制表的 schema。相关工具主要包括 Schema 约束(Schema Enforcement)和 Schema 演变(Schema Evolution),前者用以防止用户脏数据意外污染表,后者用以自动添加适当的新数据列。本文将详细剖析这两个工具。

      理解表的 Schemas

      Apache Spark 的每一个 DataFrame 都包含一个 schema,用来定义数据的形态,例如数据类型、列信息以及元数据。在 Delta Lake 中,表的 schema 通过 JSON 格式存储在事务日志中。

      什么是 Schema 约束?

      Schema 约束(Schema Enforcement),也可称作 Schema Validation,是 Delta Lake 中的一种保护机制,通过拒绝不符合表 schema 的写入请求来保证数据质量。类似于一个繁忙的餐厅前台只接受预定坐席的顾客,这个机制会检查插入表格的每一列是否符合期望的列(换句话说,就是检查每个列是否已经“预定坐席”),那些不在期望名单上的写入将被拒绝。

      Schema 约束如何工作?

      Delta Lake 对写入进行 schema 校验,也就是说所有表格的写入操作都会用表的 schema 做兼容性检查。如果 schema 不兼容,Delta Lake 将会撤销这次事务(没有任何数据写入),并且返回相应的异常信息告知用户。

      Delta Lake 通过以下准则判断一次写入是否兼容,即对写入的 DataFrame 必须满足:

      • 不能包含目标表 schema 中不存在的列。相反,如果写入的数据没有包含所有的列是被允许的,这些空缺的列将会被赋值为 null。

      • 不能包含与目标表类型不同的列。如果目标表包含 String 类型的数据,但 DataFrame 中对应列的数据类型为 Integer,Schema 约束将会返回异常,防止该次写入生效。

      • 不能包含只通过大小写区分的列名。这意味着不能在一张表中同时定义诸如“Foo”和“foo”的列。不同于 Spark 可以支持大小写敏感和不敏感(默认为大小写不敏感)两种不同的模式,Delta Lake 保留大小写,但在 schema 存储上大小写不敏感。Parquet 在存储和返回列信息上面是大小写敏感的,因此为了防止潜在的错误、数据污染和丢失的问题,Delta Lake 引入了这个限制。

      以下代码展示了一次写入过程,当添加一次新计算的列到 Delta Lake 表中。

      # Generate a DataFrame of loans that we'll append to our Delta Lake table
      loans = sql("""
                  SELECT addr_state, CAST(rand(10)*count as bigint) AS count,
                  CAST(rand(10) * 10000 * count AS double) AS amount
                  FROM loan_by_state_delta
                  """)
      
      # Show original DataFrame's schema
      original_loans.printSchema()
       
      """
      root
        |-- addr_state: string (nullable = true)
        |-- count: integer (nullable = true)
      """
       
      # Show new DataFrame's schema
      loans.printSchema()
       
      """
      root
        |-- addr_state: string (nullable = true)
        |-- count: integer (nullable = true)
        |-- amount: double (nullable = true) # new column
      """
       
      # Attempt to append new DataFrame (with new column) to existing table
      loans.write.format("delta") 
                 .mode("append") 
                 .save(DELTALAKE_PATH)
      
      """ Returns:
      
      A schema mismatch detected when writing to the Delta table.
       
      To enable schema migration, please set:
      '.option("mergeSchema", "true")'
       
      Table schema:
      root
      -- addr_state: string (nullable = true)
      -- count: long (nullable = true)
       
       
      Data schema:
      root
      -- addr_state: string (nullable = true)
      -- count: long (nullable = true)
      -- amount: double (nullable = true)
       
      If Table ACLs are enabled, these options will be ignored. Please use the ALTER TABLE command for changing the schema.

      不同于自动添加新的列,Delta Lake 受到 schema 约束并阻止了这次写入生效。并且为了帮助定位是哪个列造成了不匹配,Spark 会在错误栈中打印出两者的 schema 作为对照。

      Schema 约束有何作用?

      由于 Schema 约束是一种严格的校验,因此可以用于已清洗、转化完成的数据,保证数据不受污染,可用于生产或者消费。典型的应用场景包括直接用于以下用途的表:

      • 机器学习算法

      • BI 仪表盘

      • 数据分析和可视化工具

      • 任何要求高度结构化、强类型、语义 schema 的生产系统

      为了准备好最终的数据,很多用户使用简单的“多跳”架构来逐步往表中添加结构。更多相关内容可以参考 Productionizing Machine Learning With Delta Lake.

      当然,Schema 约束可以用在整个工作流程的任意地方,不过需要注意的是,有可能因为诸如不经意对写入数据添加了某个列,导致写入流失败的情况。

      防止数据稀释

      看到这,你可能会问,到底需不需要大费周章做 Schema 约束?毕竟,有时候一个意料之外的 schema 不匹配问题反而会影响整个工作流,特别是当新手使用 Delta Lake。为什么不直接让 schema 接受改变,这样我们就能任意写入 DataFrame 了。

      俗话说,防患于未然,有些时候,如果不对 schema 进行强制约束,数据类型兼容性的问题将会很容易出现,看上去同质的数据源可能包含了边缘情况、污染列、错误变换的映射以及其他可怕的情况都可能会一夜之间污染了原始的表。所以更好的做法应该从根本上阻止这样的情况发生,通过 Schema 约束就能够做到,将这类错误显式地返回进行恰当的处理,而不是让它潜伏在数据中,看似写入时非常顺利,但埋下了无法预知的隐患。

      Schema 约束能够确保表 schema 不会发生改变,除非你确切地执行了更改操作。它能有效的防止“数据稀释”——当新的列频繁添加,原本简洁的表结构可能因为数据泛滥而失去原有的含义和用处。Schema 约束的设计初衷就是通过设定严格的要求来保证质量,确保表数据不受污染。

      另一方面,假如经过再三确认之后,确定的确需要添加新的列,那解决方法也非常简单,也就是下文即将介绍的 Schema 演变!

      什么是 Schema 演变

      Schema 演变(Schema Evolution)允许用户能够方便地修改表的当前 schema,来适应不断变化的数据。最常见的用法就是在执行添加和覆盖操作时,自动地添加一个或多个列来适应 schema。

      Schema 演变如何工作?

      继续沿用上文的例子,对于之前由于 schema 不匹配导致请求被拒绝的情况,开发人员可以方便地使用 Schema 演变来添加新的列。Schema 演变的使用方式是在 .write 或 .writeStream 的 Spark 命令后面添加上 .option('mergeSchema', 'true')。

      # Add the mergeSchema option
      loans.write.format("delta") 
                 .option("mergeSchema", "true") 
                 .mode("append") 
                 .save(DELTALAKE_SILVER_PATH)

      可以执行以下 Spark SQL 语句来察看图表。

      # Create a plot with the new column to confirm the write was successful
      %sql
      SELECT addr_state, sum(`amount`) AS amount
      FROM loan_by_state_delta
      GROUP BY addr_state
      ORDER BY sum(`amount`)
      DESC LIMIT 10

      当然,也可以选择通过添加 spark.databricks.delta.schema.autoMerge = True 到 Spark 配置文件中使得该选项对整个 Spark session 生效。不过需要注意的是,这样使用的话, Schema 约束将不再会对 schema 不匹配问题进行报警提示。

      通过指定 mergeSchema 选项,所有在输入 DataFrame 中存在但在目标表中不存在的列都将被作为该事务操作的一部分添加到 schema 末尾。也允许添加嵌套字段,这些字段将被添加到对应列的末尾。

      数据科学家可以利用这个选项来添加新的列(例如一个新增的跟踪指标,或是这个月的销售数据)到已有的机器学习表中,而不必废弃现有依赖于旧的列信息的模型。

      以下对表的添加和覆盖操作都是合法的 Schema 演变的操作:

      • 添加新列(这是最常用的场景)

      • 修改数据类型,Null->其他类型,或者向上类型转换 Byte->Short->Integer

      其他改动都是非法的 Schema 演变操作,需要通过添加 .option("overwriteSchema", "true") 选项来覆盖 schema 以及数据。举个例子,表原本包含一个类型为 integer 的列“Foo”,而新的 schema 需要改成 string 类型,那么所有的 Parquet 数据文件都需要覆盖重写。包括以下步骤:

      • 删除这个列

      • 修改列的数据类型

      • 修改列名,仅用大小写区分(例如“Foo”和“foo”)
      最后,在 Spark 3.0 中,支持了显式 DDL(通过 ALTER TABLE 方式),允许用户能够对 schema 执行以下操作:

      • 添加列

      • 修改列注释

      • 设置表的属性来定义表的行为,例如设置事务日志的保留时间

      Schema 演变有何作用?

      Schema 演变可以用来显式地修改表的 schema(而不是意外添加了并不想要的列)。这提供了一种简单的方式来迁移 schema,因为它能自动添加上正确的列名和数据类型,而不需要进行显式的定义。

      总结

      Schema 约束能够拒绝与表不兼容的任何的新的列或者 schema 的改动。通过设置严格的限制,数据工程师们可以完全信任他们的数据,从而能够作出更好的商业决策。

      另一方面,schema 演变则对 schema 约束进行了补充,使得一些期望的 schema 变更能够自动地生效。毕竟,添加一个新的列本就不应该是一件困难的事情。

      Schema 约束和 Schema 演变相互补益,合理地结合起来使用将能方便地管理好数据,避免脏数据侵染,保证数据的完整可靠。

      原文链接:https://databricks.com/blog/2019/09/24/diving-into-delta-lake-schema-enforcement-evolution.html


      相关阅读推荐:
      Delta Lake,让你从复杂的Lambda架构中解放出来
      【译】Databricks使用Spark Streaming和Delta Lake对流式数据进行数据质量监控介绍
      【译】Delta Lake 0.5.0介绍
      Delta Lake - 数据湖的数据可靠性


      阿里巴巴开源大数据技术团队成立Apache Spark中国技术社区,定期推送精彩案例,技术专家直播,问答区近万人Spark技术同学在线提问答疑,只为营造纯粹的Spark氛围,欢迎钉钉扫码加入!image.png
      对开源大数据和感兴趣的同学可以加小编微信(下图二维码,备注“进群”)进入技术交流微信群。image.png
      Apache Spark技术交流社区公众号,微信扫一扫关注image.png

      ]]>
      Netflix:如何打造开放协作的实时 ETL 平台?-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 摘要:本文由 Netflix 高级软件工程师徐振中分享,内容包含有趣的案例、分布式系统基础方面的各种挑战以及解决方案,此外还讨论了其在开发运维过程中的收获,对开放式自助式实时数据平台的一些新愿景,以及对 Realtime ETL 基础平台的一些新思考。文章内容主要分为以下三部分:

      1. 产品背景
      2. 产品功能
      3. 挑战&解决方案

      Netflix 致力于会员的喜悦。我们不懈地专注于提高产品体验和高质量内容。近年来,我们一直在技术驱动的 Studio 和内容制作方面进行大量投资。在这个过程中,我们发现在实时数据平台的领域里中出现了许多独特并有意思的挑战。例如,在微服务架构中,领域对象分布在不同的 App 及其有状态存储中,这使得低延迟高一致性的实时报告和实体搜索发现特别具有挑战性。

      产品背景

      Netflix 的长久愿景是把欢乐和微笑带给整个世界,通过在全球各地拍摄一些高质量、多元化的内容产品放在平台上,分享给平台超过一个亿级别的用户。更远大的目标为了给用户带来愉悦的体验,Netflix 的努力方向分为两个:

      1. 一方面是通过数据整合知识来反馈并用于提高用户的产品体验中去;
      2. 另一方面通过建立一个技术驱动的 Studio 去帮助产出内容质量更高的产品。

      而作为一个数据平台团队,需要关注的是怎么帮助公司中不同的开发人员、数据分析人员等实现其在公司中的价值,最终为解决上述两方面问题做出自己的贡献。

      640.png

      简单地介绍一下 Netflix 数据平台团队及相应的产品,Keystone。它的主要功能是帮助公司在所有的微服务中埋点、建立 Agent、发布事件、收集事件信息,然后放到不同的数据仓库中进行存储,比如 Hive 或 ElasticSearch,最后帮助用户在数据实时存储的情况下实现计算和分析。

      • 用户的角度来讲,Keystone 是一个完整的自容(Self-contained)的平台,支持多用户,用户可以通过所提供的 UI 很方便地声明并创建自己想要的 pipeline。
      • 平台角度来说,Keystone 提供底层所有分布式系统中实现比较困难的解决方案,如容器编排(Container Orchestration)、工作流管理(Workflow Management)等等,这些对于用户是不可见的。
      • 产品的角度来说,主要有两个功能,一个是帮助用户将数据从边缘设备移到数仓,另一个是帮助用户实时计算的功能。
      • 数字的角度来说,Keystone 在 Netflix 的使用是非常有必要的,只要跟数据打交道的开发者,一定会用到,因此 Keystone 在整个公司中有几千个用户,并有一百个 Kafka 的集群支持每天 10PB 数量级左右的数据。

      Keystone 的整个架构分为两层,底层是 Kafka 和 Flink 作为底层引擎,底层对所有分布式系统中比较困难的技术方案进行抽象,对用户不可见,在上层构建整个应用;服务层会提供抽象的服务,UI 对于用户来讲比较简单,不需要关心底层实现。

      下面介绍一下 Keystone 产品在过去四五年的发展历程。最初的动机是收集所有设备的数据并将其存储到数据仓库中,当时使用的是 Kafka 技术,因为数据移动比较好解决,本质上来讲仅是一个多并发的问题。

      在此之后,用户给出了新的需求,即在数据移动的过程中对数据进行一些简单的处理操作,比如筛选(Filter),还有一个很通用的功能 —— projection,为此 Keystone 推出了针对该需求推出了相应的功能特性。

      经过一段时间后,用户表示想做更加复杂的 ETL,比如 Streaming Join 等,因此产品决定将底层的 API 提供给用户,并将底层的关于所有分布式系统的解决方案抽象化,让其更好地关注上层的内容。

      ## 产品功能

      产品功能介绍将围绕 Netflix 中的两个“超级英雄” Elliot 和 Charlie 来展开。Elliot 是来自数据科学工程组织的一个数据科学家,他的需求是在非常大的数据中寻找响应的 pattern,以帮助提高用户体验;Charlie 是一个来自 Studio 组织的应用开发者,其目标是通过开发一系列的应用来帮助周边的其他开发者产出更高质量的产品。

      这两个人的工作对于产品来讲都非常重要,Elliot 的数据分析结果可以帮助给出更好的推荐和个性化定制,最终提高用户体验;而 Charlie 的工作可以帮助周边的开发者提高效率。

      Recommendation & Personalization

      640 1.png

      Elliot 作为一个数据科学家,需要的是一个简单易用的实时 ETL 操作平台,他不希望写非常复杂的编码,同时需要保证整个 pipeline 的低延时。他所从事的工作和相关需求主要有以下几个:

      640 3.png

      • 推荐和个性化定制。该工作中可以根据个人特点的不同将同样的视频通过不同的形式推送给相应的用户,视频可以分为多个 row,每一个 row 可以是不同的分类,根据个人的喜好可以对不同的 row 进行更改。此外,每一个视频的题目都会有一个 artwork,不同国家、不同地域的不同用户对 artwork 的喜好也可能不同,也会通过算法进行计算并定制适合用户的 artwork。

      640 4.png

      • A/B Testing。Netflix 提供给非会员用户 28 天免费的视频观看机会,同时也相信给用户看到了适合自己的视频,用户更有可能会购买 Netflix 的服务,而在进行A/B Testing 的时候,就需要 28 天才能做完。对于 Elliot 来讲,进行 A/B Testing 的时候可能会犯错误,他所关心的是怎么样才能在不用等到 28 天结束的时候就可以提前发现问题。

      当在设备上观看 Netflix 的时候,会以请求的形式和网关进行交互,然后网关会将这些请求分发给后端的微服务,比如说用户在设备上点击播放、暂停、快进、快退等操作,这些会有不同的微服务进行处理,因此需要将相应的数据收集起来,进一步处理。

      对于 Keystone 平台团队来讲,需要收集不同的微服务中产生的数据并进行存储。Elliot 需要将不同的数据整合起来,以解决他关注的问题。

      640 5.jpg

      至于为什么要使用流处理,主要有四方面的考量,即实时报告、实时告警、机器学习模型的快速训练以及资源效率。相比于前两点,机器学习模型的快速训练以及资源效率对 Elliot 的工作更加重要。尤其需要强调的是资源效率,针对前面的 28 天的 A/B Testing,目前的做法是每天将数据与前 27 天做 Batch Processing,这个过程中涉及了很多重复处理,使用流处理可以很好地帮助提高整体的效率。

      640 6.png

      Keystone 会提供命令行的工具给用户,用户只需要在命令行中输入相应的命令来进行操作,工具最开始会询问用户一些简单的问题,如需要使用什么 repository 等,用户给出相应的回答后,会最终产生一个模板,用户便可以开始使用工具进行开发工作;产品还提供一系列简单的 SDK,目前支持的是Hive、Iceberg、Kafka 和 ElasticSearch 等。

      需要强调的是 Iceberg,它是在 Netflix 主导的一个 Table Format,未来计划取代 Hive。其提供了很多特色功能来帮助用户做优化;Keystone 向用户提供了简单的 API,可以帮助其直接生成 Source 和 Sink。

      Elliot 在完成一系列的工作之后,可以选择将自己的代码提交到 repository 中,后台会自动启动一个 CI/CD pipeline,将所有的源代码和制品等包装在 Docker 镜像中,保证所有的版本一致性。Elliot 在 UI 处只需要选择想要部署哪一个版本,然后点击部署按钮可以将 jar 部署到生产环境中。

      产品会在后台帮助其解决底层分布式系统比较困难的问题,比如怎么做容器编排等,目前是基于资源的编排,未来计划向 K8S 方向发展。部署 Job(作业)包的过程中会部署一个 JobManager 的集群和一个 TaskManager 的集群,因此每一个 Job 对于用户来说是完全独立的。

      产品提供默认的配置选项,同时也支持用户在平台 UI 上修改并覆盖配置信息,直接选择部署即可生效,而不需重写代码。Elliot 之前有一个需求是在 Stream Processing 的过程中,比如从不同的 Topic 中去读取数据,出现问题的情况下可能需要在 Kafka 中操作,也可能需要在数据仓库中操作,面对该问题,其需求是在不改动代码的情况下切换不同的 Source,而目前平台提供的UI很方便地完成该需求。此外平台还可以帮助用户在部署的时候选择需要多少资源来运行作业。

      很多用户从 Batch Processing 转到 Stream Processing 的过程中,已经有了很多需要的制品,比如 Schema 等,因此平台还帮助其简单地实现这些制品的集成。

      640 7.jpg

      平台拥有很多需要在其之上写 ETL 工程的用户,当用户越来越多的时候,平台的可伸缩性显得尤为重要。为此,平台采用了一系列的 pattern 来解决该问题。具体来讲,主要有三个 pattern 正在使用,即 Extractor Pattern、Join Pattern 和 Enrichment Pattern。

      Content Production

      先简要介绍一下什么是 Content Production。包括预测在视频制作方面的花费、制定 program、达成 deal、制作视频、视频后期处理、发布视频以及金融报告。

      640 8.png

      Charlie 所在的是 Studio 部门主要负责开发一系列的应用来帮助支持 Content Production。每一个应用都是基于微服务架构来开发部署的,每一个微服务应用会有自己的职责。举个最简单的例子,会有专门管理电影标题的微服务应用,也会有专门管理 deals 和 contracts 的微服务应用等等。

      面对如此多的微服务应用,Charlie 面临的挑战问题是当其在进行实时搜索的过程中,比如搜索某一个电影的演员,需要将数据从不同的地方 join 起来;另外数据每天都在增加,保证实时更新的数据的一致性比较困难,这本质上是分布式微服务系统的特点导致,不同的微服务选择使用的数据库可能不同,这给数据一致性的保证又增加了一定的复杂度。针对该问题,常用的解决方案有以下三个:

      • Dual writes: 当开发者知道数据需要放到主要的数据库中的时候,同时也要放到另一个数据库中,可以很简单地选择分两次写入到数据库中,但是这种操作是不容错的,一旦发生错误,很有可能会导致数据的不一致;
      • Change Data Table: 需要数据库支持事务的概念,不管对数据库做什么操作,相应的变更会加到事务变更的 statement 中并存入单独的表中,之后可以查询该 change 表并获取相应的变更情况并同步到其他数据表;
      • Distributed Transaction:指的是分布式事务,在多数据环境中实现起来比较复杂。

      Charlie 的一个需求是将所有的电影从 Movie Datastore 复制到一个以 Elasticsearch 来支持的 movie search index 中,主要通过一个 Polling System 来做数据拉取和复制,数据一致性的保证采用的是上述的 Change data table 的方法。

      该方案的弊端是只支持定期数据拉取,另外 Polling System 和数据源直接紧密结合,一旦 Movie Search Datastore 的 Schema 改变,Polling System 就需要修改。为此,该架构在后来做了一次改进,引入了事件驱动的机制,读取数据库中所有实现的事务,通过 stream processing 的方式传递到下一个 job 进行处理。为了普适化该解决方案,在 source 端实现了不同数据库的 CDC(Change Data Capture)支持,包括 MySQL、PostgreSQL 和 Cassandra 等在 Netflix 中比较常用的数据库,通过 Keystone 的 pipeline 进行处理。

      挑战及解决方案

      下面分享一下上述方案存在的挑战和相应的解决方案:

      640 9.png

      • Ordering Semantics

      在变更数据事件中,必须要保证 Event ordering,比如一个事件包含 create、update 和 delete 是三个操作,需要返回给消费者侧一个严格遵守该顺序的操作事件。一个解决方案是通过 Kafka 来控制;另一个解决方案是在分布式系统中保证捕获的事件与实际从数据库中读取数据的顺序是一致的,该方案中当所有的变更事件捕获出来后,会存在重复和乱序的情况,会通过 Flink 进行去重和重新排序。

      640 10.png

      • Processing Contracts

      在写 stream processing 的时候,很多情况下不知道 Schema 的具体信息,因此需要在消息上定义一个契约 contract,包括 Wire Format 以及在不同的层级上定义与Schema 相关的信息,如基础设施(Infrastructure)、平台(Platform)等。Processor Contract 的目的是帮助用户将不同的 processor metadata 组合起来,尽量减少其写重复代码的可能。

      举一个具体的案例,比如 Charlie 希望有新的 deal 的时候被及时通知,平台通过将相关的不同组件组合起来,DB Connector、Filter 等,通过用户定义契约的方式帮助其实现一个开放的可组合的流数据平台。

      以往所看到的 ETL 工程大多数适用于数据工程师或数据科学家。但从经验上来讲,ETL 的整个过程,即 Extract、Transform 和 Load,其实是有被更广泛应用的可能。最早的 Keystone 简单易用,但灵活性不高,之后的发展过程中虽然提高了灵活性,但复杂性也相应地增大了。因此未来团队计划在目前的基础上进一步优化,推出开放的、合作的、可组合的、可配置的 ETL 工程平台,帮助用户在极短的时间解决问题。

      作者简介:

      徐振中,Netflix 软件工程师,在 Netflix 从事高度可扩展和弹性的流媒体数据平台的基础设施工作,热衷于研究分享与实时数据系统、分布式系统基本原理相关的任何有趣的事情!

      ]]>
      阿里达摩院自动驾驶新突破,实现 3D 物体检测精度与速度兼得 | CVPR 2020 论文解读-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      --------点击屏幕右侧或者屏幕底部“+订阅”,关注我,随时分享机器智能最新行业动态及技术干货----------

      4.png

      近期,阿里巴巴达摩院的一篇论文入选计算机视觉顶会 CVPR 2020,该论文提出了一个通用、高性能的自动驾驶检测器,首次实现 3D 物体检测精度与速度的兼得,有效提升自动驾驶系统安全性能。

      image.png

      3D 目标检测需输出物体类别及在三维空间中的长宽高、旋转角等信息

      与普通的 2D 图像识别应用不同,自动驾驶系统对精度和速度的要求更高,检测器不仅需要快速识别周围环境的物体,还要对物体在三维空间中的位置做精准定位。然而,目前主流的单阶段检测器和两阶段检测器均无法平衡检测精度和速度,这极大地限制了自动驾驶安全性能。

      此次,达摩院在论文中提出了新的思路即将两阶段检测器中对特征进行细粒度刻画的方法集成到单阶段检测器。具体来说,达摩院在训练中利用一个辅助网络将单阶段检测器中的体素特征转化为点级特征,并施加一定的监督信号,同时在模型推理过程中辅助网络无需参与计算,因此,在保障速度的同时又提高了检测精度。

      以下是第一作者 Chenhang He 对该论文做出的解读:

      1. 背景

      目标检测是计算机视觉领域的传统任务,与图像识别不同,目标检测不仅需要识别出图像上存在的物体,给出对应的类别,还需要将该物体通过 Bounding box 进行定位。根据目标检测需要输出结果的不同,一般将使用 RGB 图像进行目标检测,输出物体类别和在图像上 2D bounding box 的方式称为 2D 目标检测。而将使用 RGB 图像、RGB-D 深度图像和激光点云,输出物体类别及在三维空间中的长宽高、旋转角等信息的检测称为 3D 目标检测。

      1.png

      2.png

      从点云数据进行 3D 目标检测是自动驾驶(AV)系统中的的关键组件。与仅从图像平面估计 2D 边界框的普通 2D 目标检测不同,AV 需要从现实世界估计更具信息量的 3D 边界框,以完成诸如路径规划和避免碰撞之类的高级任务。这激发了最近出现的 3D 目标检测方法,该方法应用卷积神经网络(CNN)处理来自高端 LiDAR 传感器的点云数据。

      目前基于点云的 3D 物体检测主要有两种架构:

      1. 单阶段检测器 (single-stage): 将点云编码成体素特征 (voxel feature), 并用 3D CNN 直接预测物体框, 速度快但是由于点云在 CNN 中被解构, 对物体的结构感知能力差, 所以精度略低。
      2. 两阶段检测器 (two-stage): 首先用 PointNet 提取点级特征, 并利用候选区域池化点云 (Pooling from point cloud) 以获得精细特征. 通常能达到很高的精度但速度很慢。

      image.png

      2. 方法

      目前业界主要以单阶段检测器为主,这样能保证检测器能高效地在实时系统上进行。 我们提出的方案将两阶段检测器中对特征进行细粒度刻画的思想移植到单阶段检测中,通过在训练中利用一个辅助网络将单阶段检测器中的体素特征转化为点级特征,并施加一定的监督信号,从而使得卷积特征也具有结构感知能力,进而提高检测精度。而在做模型推断时,辅助网络并不参与计算(detached), 进而保证了单阶段检测器的检测效率。另外我们提出一个工程上的改进,Part-sensitive Warping (PSWarp), 用于处理单阶段检测器中存在的 “框 - 置信度 - 不匹配” 问题。

      image.png

      主体网络

      用于部署的检测器, 即推断网络, 由一个骨干网络和检测头组成。骨干网络用 3D 的稀疏网络实现,用于提取含有高语义的体素特征。检测头将体素特征压缩成鸟瞰图表示,并在上面运行 2D 全卷积网络来预测 3D 物体框。

      辅助网络

      在训练阶段,我们提出一个辅助网络来抽取骨干网络中间层的卷积特征,并将这些特征转化成点级特征 (point-wise feature)。在实现上,我们将卷积特征中的非零信号映射到原始的点云空间中, 然后在每个点上进行插值,这样我们就能获取卷积特征的点级表示。 令 {():j=0,…,M} 为卷积特征在空间中的表示, {:i=0,…,N}为原始点云, 则卷积特征在原始点上的表示 等于

      image.png

      辅助任务

      我们提出两种基于点级特征的监督策略来帮助卷积特征获得很好的结构感知力,一个前景分割任务,一个中心点回归任务。

      image.png

      具体来说,相比于 PointNet 特征提取器 (a), 卷积网络中的卷积操作和下采样会造成点云结构的破坏(b)使得特征对物体的边界与内部结构不敏感。我们利用分割任务来保证部分卷积特征在下采样时不会被背景特征影响 ©,从而加强对边界的感知。我们利用中心点回归任务来加强卷积特征对物体内部结构的感知能力 (d),使得在少量点的情况下也能合理的推断出物体的潜在大小、形状。我们使用 focal loss 和 smooth-l1 对分割任务与中心回归任务分辨进行优化。

      3. 工程上的改进

      image.png

      在单阶段检测中, feature map 和 anchor 的对齐问题是普遍存在的问题, 这样会导致预测出来的边界框的定位质量与置信度不匹配,这会影响在后处理阶段(NMS)时, 高置信度但低定位质量的框被保留, 而定位质量高却置信度低的框被丢弃。在 two-stage 的目标检测算法中,RPN 提取 proposal,然后会在 feature map 上对应的的位置提取特征(roi-pooling 或者 roi-align),这个时候新的特征和对应的 proposal 是对齐的。我们提出了一个基于 PSRoIAlign 的改进,Part-sensitive Warping (PSWarp), 用来对预测框进行重打分。

      如上图, 我们首先修改最后的分类层以生成 K 个部分敏感的特征图,用{X_k:k = 1,2,…,K}表示,每个图都编码对象的特定部分的信息。例如,在 K = 4 的情况下,会生成 {左上,右上,左下,右下} 四个局部敏感的特征图。同时,我们将每个预测边界框划分为 K 个子窗口,然后选择每个子窗口的中心位置作为采样点。这样,我们可以生成 K 个采样网格{S^k:k = 1,2,…,K},每个采样网格都与该局部对应的特征图相关联。如图所示,我们利用采样器, 用生成的采样网格在对应的局部敏感特征图上进行采样,生成对齐好的特征图。最终能反映置信度的特征图则是 K 个对齐好特征图的平均。

      4. 效果

      image.png

      我们提出的方法 (黑色) 在 KITTI 数据库上的 PR Curve, 其中实线为两阶段方法, 虚线为单阶段方法。 可以看到我们作为单阶段方法能够达到两阶段方法才能达到的精度。

      image.png

      在 KITTI 鸟瞰 (BEV) 和 3D 测试集的效果。优点是在保持精度的同时,不增加额外的计算量,能达到 25FPS 的检测速度。

      image.png

      image.png

      image.png

      作者介绍:

      第一作者为达摩院研究实习生 Chenhang He,其他作者分别分别为达摩院高级研究员、IEEE Fellow 华先胜,达摩院高级研究员、香港理工大学电子计算学系讲座教授、IEEE Fellow 张磊,达摩院资深算法专家黄建强及达摩院研究实习生 Hui Zeng。

      image.png

      原文链接:
      https://www.infoq.cn/article/1QPiVc3BjFMPcUELhJb5

      ]]>
      深度评测 | 十年磨一剑,阿里云RDS MySQL和AWS RDS谁的性能更胜一筹?-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 MySQL代表了开源数据库的快速发展。

      从2004年前后的Wiki、WordPress等轻量级Web 2.0应用起步,到2010年阿里巴巴在电商及支付场景大规模使用MySQL数据库,再到2012年开始阿里云RDS for MySQL为成千上万家客户提供可靠的关系数据库服务,阿里云RDS积累了来自内外部功能、性能、安全方面的众多需求,打造了面向企业场景的AliSQL分支,包含了丰富企业级数据库特性:

      DynamicThreadPool(DTP):在内核中动态管理数据库线程池,通过有限资源持续支撑大量创建数据库连接请求,维持高效稳定的请求处理能力。

      IndexMutexTuning(IMT):通过IMT优化大大降低索引节点分裂成本,大大提升类TPCC场景处理能力,在全内存的测试中,单机达到了39W的TpmC,相对于原生版本提升35-50%不等。

      TDE&SM4:全新优化的TDE数据加密,并且支持国产加密算法SM4。

      PerformanceInsight:针对数据库SQL性能扩展多方面信息,包括表统计信息、索引统计信息、SQL单次性能数据、IO统计信息和关键性能数据输出等。

      AsyncFilePurge:优化大表删除,有效降低系统IO抖动影响。

      SlowLogRotate:对慢日志设计切换机制,可有效降低大量慢日志造成的不稳定影响。

      SQLOutline:通过Hint优化SQL语句执行计划,可保障在各种环境变化中SQL执行计划不变,如大版本升级、统计信息变更等。

      StatementConcurrencyControl(SCC):DBA可通过SCC干预系统语句执行,对具体SQL限流控制并发度,紧急状态保障数据库稳定运行。

      DDLRecycleBin:内核中增加回收站,防止误执行DROPTABLE/TRUNCATE TABLE造成的不可挽回的损害。

      AWS作为全球云市场的标杆,是其他云厂商不断追赶的目标。十年磨一剑,阿里巴巴MySQL数据库历经十年的发展后,阿里云RDS MySQL和AWS RDS相比,性能上谁会更胜一筹?本文将一测究竟。

      01、测试概述

      本测试对比了阿里云RDS通用型实例(5.7/8.0版本)和AWS RDS通用T3型实例,在OLTP读写混合模型下的性能。测试使用了标准的sysbench 1.0工具,选择的规格是8核32GB规格数据库。测试场景选用的是内存命中型场景,250张表,每张表25000行数据。

      测试结论是用同规格下性能最好的T3型AWS RDS 5.7/8.0和阿里云对应版本的RDS通用型实例对比,阿里云RDS通用型实例有绝对的优势,读写混合场景峰值QPS比AWS RDS高80%左右。

      02、测试环境

      (*点击查看大图)

      3.30配图.jpg

      AWS RDS通用型实例分为四种类型
      https://aws.amazon.com/cn/rds/instance-types/
      前期实测下来,相同CPU核数和内存规格的情况下,T3型实例性能最好。选用的规格是t3.2xlarge,8个vCPU。

      AWS RDS的主备同步不依赖binlog。同步方式的说明见:
      https://aws.amazon.com/cn/blogs/database/amazon-rds-under-the-hood-multi-az/
      跨AZ的情况下,AWS RDS是同步写,所以对比测试中,阿里云RDS选用多可用区+半同步开启+默认严格参数模板。

      03、测试数据

      3.1 RDS 5.7读写混合场景

      在不同并发条件下,阿里云RDS 5.7均较AWS性能占优。

      阿里云RDS 5.7的峰值QPS比AWS高80.41%

      (*点击查看大图)

      02.png

      3.2 RDS 8.0读写混合场景

      在不同并发条件下,阿里云RDS 8.0均较AWS性能占优。

      阿里云RDS 8.0的峰值QPS比AWS高77.88%

      (*点击查看大图)
      3.png

      04、总结和展望

      通过上述测试数据可以看出,阿里云RDS通用型实例相比于AWS RDS有较大的优势。阿里云数据库RDS MySQL在2020年将继续快速奔跑,除了性能以外,将继续围绕企业诉求,构建数据库核心能力,覆盖安全、可靠性、可用性、可扩展性等诸多方面。

      Gartner预计,到2021年,云数据库在整个数据库市场中的占比将首次达到50%;而到2023年,75%的数据库要跑在云平台之上。

      去年11月,国际知名调研机构Gartner公布2019年全球数据库魔力象限评选结果,阿里云成功进入“挑战者”象限,连续两年作为唯一的中国企业入选。

      根据Gartner 统计数据,阿里云已经位居全球云数据库市场份额第三位以及中国市场第一位,年增长率达到116%。目前,已有超过40万个数据库实例迁移到阿里云上,包含政务、零售、金融、电信、制造、物流等多个领域的龙头企业。

      01.png

      只有保持一路快跑的势态,才能在日益严峻的竞争环境中持续领先,才能满足云上企业快速增长需求。阿里云数据库RDS MySQL一直走在前列。

      ]]>
      15篇面试通关经验+10大热招岗位,给你足够底气斩获offer!|开发者必读(164期)-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      最炫的技术新知、最热门的大咖公开课、最有趣的开发者活动、最实用的工具干货,就在《开发者必读》!

      每日集成开发者社区精品内容,你身边的技术资讯管家。


      每日头条

      15篇面试通关经验+10大热招岗位,给你足够底气斩获offer!

      人生有许多种可能,只要勇敢一次,你会发现收获的东西比想象中多很多,譬如说,我们正在招人,你勇敢投简历了吗?如果你还没有准备好,没关系,我们给你足够底气斩获offer!10大热招岗位需求+15篇师兄师姐面试经验分享,手把手教你面试通关!


      最强干货

      一遇到复杂分析查询就卡顿?MySQL分析实例了解一下

      随着企业数据爆发式增长,MySQL分析查询卡顿问题越来越多,用户时效性不能保证,精细化运营诉求不能满足。如何能无缝对接业务库,实现毫秒级针对万亿级数据进行即时的多维分析透视和业务探索,MySQL分析实例给出完美解决方案。

      2020 年 AI 和机器学习的重要趋势是什么 ?

      在竞争日益激烈的技术市场中,从高科技初创公司到全球跨国公司都将人工智能视为关键竞争优势。但是,人工智能行业发展如此之快,以至于很难跟踪最新的研究突破和成就,甚至很难应用科学成果来实现业务成果。在 2020 年为了帮助业务制定强大的 AI 策略,本文总结了不同研究领域的最新趋势,包括自然语言处理,对话式 AI,计算机视觉和强化学习。

      纳米镜系列文章|使闲鱼各种业务“雨露均沾”

      我们介绍了纳米镜的功能和背后的分析算法,而闲鱼目前业务线多且复杂,怎么构建一个可扩展性强的系统,使每个业务线都能够便捷地接入,成为首要关注的问题。


      每天读本书

      重磅下载!《2020前端工程师必读手册》,阿里巴巴前端委员会推荐!

      阿里巴巴前端委员会推荐!覆盖5大热点前端技术方向10+核心实战的前端手册--《2020前端工程师必读手册》已经正式上线了,大家可以免费下载了,解锁前端新方式,挖掘前端新思路,尽在此刻,赶紧来先睹为快!


      精品公开课

      如何快速提升员工效能?(2020春季创业节)

      直播内容
      1) 一次性解决企业最头疼的3个管理问题
      2) 高效实现:新人活、老人猛、中层强


      每日集成开发者社区精品内容,请持续关注开发者必读

      ]]>
      调度参数在MaxCompute的使用-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 一、调度参数和MaxCompute的关系

      首先明确调度参数是属于DataWorks上的和MaxCompute之间是没有关系的。

      二、调度参数执行的原理

      调度参数是经过DataWorks的调度系统进行解析,然后将解析的值传到MaxCompute上MaxCompute根据对应的key获取对应的value,所以想要取到值必须经过DataWorks的调度系统解析。

      三、如何测试调度参数

      1.系统参数(2个)

      • 主要包括业务时间bdp.system.bizdate
      • 定时时间bdp.system.cyctime
      说明
      这两个值由于是DataWorks的系统参数,可以直接在代码中使用,在页面点击高级运行可以解析
      **使用方法:
      **
      在DataWorks直接点击高级运行可以看到结果

      select ${bdp.system.bizdate}

      结果:
      image.png

      2.时间参数

      内置参数
      ($bizdate和$cyctime)、${…}和$[…],
      说明
      由于不是系统的必须经过调度系统才能测试,在页面点击高级运行也是无法解析的
      使用方法
      • 在数据流程->MaxCompute->数据开发->新建一个odspsql节点
      image.png

      • 双击打开节点,编写sql

      image.png
      • 点击调度配置,配置调度参数

      image.png
      • 将当前节点保存,关闭退出运行

      image.png
      • 查看结果

      image.png

      3.自定义常量参数

      说明
      在页面点击高级运行可以解析
      • 在临时查询中编写sql

      select '${key}';

      • 点击高级运行

      image.png
      • 查看结果
      image.png

      大家如果对MaxCompute有更多咨询或者建议,欢迎扫码加入 MaxCompute开发者社区钉钉群,或点击链接 申请加入。
      image.png

      ]]>
      小姐姐亲身体验:在阿里数据库科研团队实习是种怎样的体验?-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 作者简介:

      张心怡,北京大学前沿交叉研究院研究生,中国人民大学信息学院本科生。从18年底开始在POLARDB-X团队智能数据库组的实习,现已在阿里度过了一年多的时光。

      心怡说,对于有志于数据库领域研究的小伙伴,这里是最好的学习和工作平台。

      01、优秀的同行人,助我成长

      我所在组的研究方向是智能数据库,目标是利用机器学习和统计优化等技术,实现数据库系统各个组件的自动优化,如存储引擎,并发控制,SQL优化器等,以减少系统成本,提升系统性能,以实现一个self-driving的数据库系统。

      这是一个很有前景的方向。大四上学期,初来实习的我内心其实颇为忐忑,面对组里的同事前辈,“跟不上进度”成了我最担心的事情。然而,进入到工作状态之后,我心里的石头落了地:mentor给实习生安排的任务是循序渐进的,一次次讨论与指导,使我能够快速上手。

      经过和mentor的讨论,我选择把“智能查询优化”作为第一个研究项目,并且与大四学期的毕设结合,基于阿里线上平台的实际问题,展开研究。

      查询优化属于数据库比较底层的部分,之前我没有很深的了解。在开展研究的过程中,除了自己阅读文献,同事成为了我的“知识宝库”。遇到场景落地问题时,我会请教PolarDB-X优化器开发的同事,他们往往能够一针见血地指出实际问题。

      我的成长离不开组里各位老师的帮助与分享,组内还会定期或不定期组织reading group,讲解工作成果与学界进展。在这里,你会发现身边的同事大多对深耕于某一领域,实力扎实,与他们交流会收获很多!

      02、快乐工作,认真生活

      “快乐工作,认真生活”,记得我刚刚入职时HR提到了这个观点,入职之后我发现这是阿里人身体力行的一句话。

      在工作上,身边的人都很努力。在这种氛围的感召下,遇到难题,我也会情不自禁地在工位上多坐一会。暑期实习的时候,时常9点之后结束工作,打车回宿舍。

      生活上,团队里组织了丰富多彩的活动。聚餐已经成为了常规项目。工作间隙还可以去健身房锻炼一波,园区的按摩椅也成为了养生女孩的午休项目。

      印象最深的是团队组织的运动会,女子项目是平板支撑。听到这个消息之后,我基本每天都进行练习。运动会那天,杭州base、北京base、硅谷base进行了三地PK,在同事的加油下,我坚持了平板支持7分25秒,最后拿到了女子组冠军。

      大家的工作与生活模式都很健康充实。在阿里,我见识到了工作发展的可持续性与优秀的团队交互模式。

      冠军.jpg

      获得运动会冠军

      03、阿里实习,带我打开科研大门

      来到阿里之前,我是一个对科研比较懵懂的门外汉。特别幸运的是,在这里我遇到了很棒的mentor们指导我进行研究工作。

      不论是基础的代码风格还是研究思路、遇到的问题,mentor都会事无巨细地进行引导。以前我写代码,能跑起来、自己看得懂就行。

      我在阿里提交的第一次merge request,有不少随意的空行和一些tricky且难以维护的逻辑。印象很深的是,当时mentor逐行写了comment指出问题。我认识到了代码的规范性和可维护性,以及别人是否能够理解自己的代码都是要考虑的问题。

      2019年我从中国人民大学毕业,来到北京大学攻读数据科学研究生,感谢我的研究生导师崔斌老师对我在阿里实习的支持。当时,我在阿里研究的第一个课题,也画上了圆满的句号:我在NDBC(CCF National Database Conference)进行了课题报告,投稿论文并被评为best student paper。

      NDBC.jpg

      参加NDBC

      我在阿里参与研究的第二个课题是数据库的智能调参。传统的数据库调参中DBA基于经验与尝试推荐参数值,而我们要做的是基于机器学习算法自动高效给出推荐。

      这个课题在进行过程中遇到了不少困难,算法的适用性与有效性是我们重点考虑的。在进行了很久的实验之后,会发现一些坑和问题,挫败感是有的,但是会马上被新的尝试与期待替代。

      我发现,在这里的研究并不是为了学术灌水而做,有意义研究是问题导向的。mentor时常强调要找到可复现的场景和实际问题,这样才有实际意义。我的mentor base在硅谷,因为时差我时不时在早上收到消息和反馈,这成为了我起床开启新的一天的最大动力。mentor是我科研路上的引路人,也是并肩作战的战友,大家一起为了攻克问题而努力!

      阿里的实习经历,帮我找到了打开科研大门的钥匙,让我从对科研的懵懵懂懂,到爱上了这一发现问题、攻克问题的过程。我希望将来能继续数据库领域的研究工作,在玉洁冰清的逻辑世界继续追寻。

      ]]>
      Spring Cloud Gateway的动态路由怎样做?集成Nacos实现很简单-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 file

      一、说明

      网关的核心概念就是路由配置和路由规则,而作为所有请求流量的入口,在实际生产环境中为了保证高可靠和高可用,是尽量要避免重启的,所以实现动态路由是非常有必要的;本文主要介绍 Spring Cloud Gateway 实现的思路,并且以Nacos为数据源来讲解

      PS:关于 Spring Cloud Zuul 的动态路由请看文章《Spring Cloud Zuul的动态路由怎样做?集成Nacos实现很简单

       

      二、实现要点

      要实现动态路由只需关注下面4个点

      1. 网关启动时,动态路由的数据怎样加载进来
      2. 静态路由动态路由以那个为准,ps:静态路由指的是配置文件里写死的路由配置
      3. 监听动态路由的数据源变化
      4. 数据有变化时怎样通知gateway刷新路由

       

      三、具体实现

      Spring Cloud Gateway 中加载路由信息分别由以下几个类负责

      1. PropertiesRouteDefinitionLocator:从配置文件中读取路由信息(如YML、Properties等)
      2. RouteDefinitionRepository:从存储器中读取路由信息(如内存、配置中心、Redis、MySQL等)
      3. DiscoveryClientRouteDefinitionLocator:从注册中心中读取路由信息(如Nacos、Eurka、Zookeeper等)

       
      我们可以通过自定义 RouteDefinitionRepository 的实现类来实现动态路由的目的

       

      3.1. 实现动态路由的数据加载

      创建一个NacosRouteDefinitionRepository实现类

      NacosRouteDefinitionRepository类可查看:NacosRouteDefinitionRepository.java
      file

      重写 getRouteDefinitions 方法实现路由信息的读取

       
      配置Nacos监听器,监听路由配置信息的变化
      file

      路由变化只需要往 ApplicationEventPublisher 推送一个 RefreshRoutesEvent 事件即刻,gateway会自动监听该事件并调用 getRouteDefinitions 方法更新路由信息

       

      3.2. 创建配置类

      DynamicRouteConfig类可查看:DynamicRouteConfig.java
      file

       

      3.3. 添加Nacos路由配置

      file
      新增配置项:

      • Data Id:scg-routes
      • Group:SCG_GATEWAY
      • 配置内容:
      [
          {
              "id": "csdn",
              "predicates": [{
                  "name": "Path",
                  "args": {
                          "pattern": "/csdn/**"
                  }
              }],
              "uri": "https://www.csdn.net/",
              "filters": []
          },
          {
              "id": "github",
              "predicates": [{
                  "name": "Path",
                  "args": {
                          "pattern": "/github/**"
                  }
              }],
              "uri": "http://github.com/",
              "filters": []
          }
      ]

      添加两条路由数据

       

      四、测试

      启动网关通过 /actuator/gateway/routes 端点查看当前路由信息

      file

      可以看到 Nacos 里配置的两条路由信息

       
      完整的Spring Cloud Gateway代码请查看
      https://gitee.com/zlt2000/microservices-platform/tree/master/zlt-gateway/sc-gateway

       

      推荐阅读

      ]]>
      接入CDN/WAF后出现循环重定向问题的排查记录-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:睿得

      一、问题描述

      客户反馈一个CDN加速域名解析切换到CDN的CNAME之后,访问出现301循环重定向的现象。

      image.png

      与客户确认目前的业务架构如下:

      image.png

      测试直接访问WAF(阿里云CDN源站),响应301 Redirect;

      测试直接访问AWS CloudFront(阿里云WAF源站),可以正常访问到网站内容。

      image.png

      二、分析验证

      经过CDN/WAF后得到的结果与直接访问源站的结果不一致的情况,一般来说是由于CDN/WAF回源时的请求与直接访问源站时的请求有区别,导致了源站做出了不同的响应结果。

      对于301循环重定向的情况,最常见的一种情况就是http-->https的跳转。问题可以稳定复现,所以很容易测试和定位。

      image.png

      通过http直接访问AWS CloudFront,可以看到AWS响应了301 Redirect,重定向到https。

      image.png

      进一步核实,WAF上配置了“Enable HTTP back-to-source”的开关,强制WAF使用http协议回源。

      image.png

      循环重定向的访问路径:

      客户浏览器 https --> 阿里云CDN https--> 阿里云WAF http --> AWS CloudFront CDN 301 --> 客户浏览器 https

      四、解决方案

      1、首先,联系客户在WAF控制台上关闭“Enable HTTP back-to-source”的开关,使WAF回源时使用https协议。

      2、此外,注意到客户在CDN上开启了“Use the same protocol as the back-to-source protocol”的开关,这时,如果用户通过http协议访问CDN,CDN使用http协议回源,所以建议客户同时在CDN控制台上关闭“Use the same protocol as the back-to-source protocol”的开关,强制CDN使用https协议回源WAF。

      image.png

      最后,为了清空CDN上301状态的缓存,请客户在CDN控制台上进行手工刷新操作。

      客户反馈操作完成后,测试业务恢复正常。

      五、新的问题

      问题解决几分钟后,客户反馈部分的PC、手机上访问网站还是会出现301循环重定向的情况。有的人,同一台电脑上的不同浏览器,有些正常,有些不正常。尝试清理浏览器的缓存后问题依旧。

      image.png

      六、分析测试

      通过在有问题的客户端上分析http响应头,发现一些CDN节点上还有301状态的缓存。难道是刷新没有完全成功?

      进一步查看CDN节点的缓存时间,是在客户操作之后才缓存的。

      image.png

      通过检查客户CDN配置,发现客户没有关闭“Use the same protocol as the back-to-source protocol”的开关。

      而另一方面,当有https的请求发到AWS上时,AWS响应的200 OK的响应头中有no-cache的header。

      image.png

      当有http的请求发到AWS上时,AWS响应的301 Redirect的响应头中没有no-cache的header。

      image.png

      所以无论有多少人正常的通过https协议成功网站,CDN都不会缓存。而一旦有人不小心用http协议访问了这个网址后,CDN就会缓存住301的状态,导致后续的访问者(无论是http还是https)都会出现301循环重定向的错误。

      此外,对于301的状态,一些浏览器(FireFox、移动端的Safari等)会一直进行缓存,所以还需要手工清理浏览器的本地缓存。

      七、彻底解决

      1、联系客户关闭“Use the same protocol as the back-to-source protocol”的开关。

      2、与客户确认业务不需要http协议的访问,在阿里云CDN上配置http --> https的跳转。

      image.png

      3、操作后,手工刷新CDN缓存,测试全部恢复正常。

      八、附录:CDN/WAF访问协议处理流程

      image.png

      ]]>
      【CDN 常见问题】CDN回源Host的意义-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:烨烁

      CDN的配置项中包括源站设置和回源host设置两个参数。这两个参数概念可能会出现混淆并且设置错误会导致访问出现异常,因此本文主要向大家介绍源站设置和回源host两个参数的概念和设置方法。
      产品文档参考:CDN回源HOST

      一、基本概念

      源站设置的详细设置方法在【CDN 常见问题】CDN 接入配置及常见问题文档中已经详细说明了各业务类型的源站设置的方法。从该文档中可以知晓源站设置不管是什么类型,其实其意义都是需要将该域名解析成IP后根据该IP回源到源站服务器上。而真正是哪个站点提供服务其实与源站设置是没有关系的。因此这里需要回源Host的概念。
      回源Host其意义表示为CDN节点回源请求头中带有的Host字段。在HTTP 1.1协议中请求头中必须要有Host字段,根据HTTP RFC官方文档说明:

      The Host field value MUST represent the naming authority of the origin server or gateway given by the original URL.

      即表示了Host字段的值表示的是原始URL给出的服务器或者网关的命名授权。
      而当源站设置解析得到的IP对应的服务器上有多个站点配置了不同的命名Host,那么CDN回源就会根据回源Host字段决定是由哪个站点提供服务。
      举例说明,如下是一段nginx配置server的常用配置方法,从该配置上我们可以查看到该服务器上配置一个名为www.aliyun.com的站点监听服务器的80端口,并设置了该站点的根目录路径。

      server {
      
      listen 80; #default_server;
      server_name www.aliyun.com;
      location / {
          root /alidata/www/www-aliyun-com/;
      }
      }

      然后创建cdn加速域名cdntest1.aliyun.com,设置其源站为www.aliyun.com,并设置回源端口为80端口。此时如果设置回源host为关闭或者设置为cdntest1.aliyun.com时,将导致CDN回源时到源站查找server_name为cdntest1.aliyun.com的站点,而由于源站配置配置对应的站点导致出现4XX的错误了。因此此时正确的配置应该将回源host改成www.aliyun.com,这样才可以找到对应的server并到其location目录中查找对应的文件返回给CDN节点。

      image.png

      图1. CDN的回源Host正确配置

      二、CDN加速OSS的回源Host设置

      在CDN加速OSS的经典场景中回源Host的设置是有两种方法,下面逐一介绍:
      1、设置回源Host为CDN域名本身。当设置回源Host为加速域名时那么要求其域名是必须在OSS的域名管理中添加该域名后才可以设置的,否则会导致CDN回源无法查找到该名称对应的是哪个bucket导致无法回源。
      image.png

      图2. OSS绑定域名示意图


      2、设置回源Host为OSS域名本身。这种情况下OSS是不需要做任何的调整即可保证CDN正常回源到该bucket获取文件。但是此种设置时在OSS的日志中记录的Sync Request记录项为-,而不会记录为CDN(OSS日志格式请参考:OSS日志格式),因此建议在CDN加速OSS的场景中建议以第一种方式。]]>
      【CDN 常见问题】CDN HTTPS配置及常见问题-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:烨烁

      CDN提供了HTTPS的加密传输方式保证在客户端访问CDN的L1节点的链路上对传输数据进行加密避免被恶意查看和篡改。客户通过将自行向证书CA机构申请的SSL证书上传到CDN上,CDN会完成对所有的L1节点的配置同步保证后续所有的L1节点支持HTTPS方式访问。那么在配置CDN的HTTPS协议时有哪些是需要特别注意的呢?本文就阐述HTTPS配置需要注意的内容及常见问题。
      一、申请证书。
      在配置CDN的HTTPS的第一步当然是需要先为加速域名申请购买对应的SSL证书才可以正确的添加在CDN中的。
      市场中有CA机构作为证书颁发者可以提供站点提供者使用。用户可以向CA机构购买对应域名的证书,同时阿里云云盾也联合多家国内外知名 CA 证书厂商直接提供服务器数字证书(证书服务详细请参考:证书服务)。
      用户在购买证书时需要特别注意SSL证书根据其适用范围可以分为:通配符域名、单个域名和多个域名。根据其名称即可查看购买的证书分别适用于主域名下某个级别的全部子域名、单个域名或者多个域名。用户是需要保证购买的证书必须适用于加速域名后续才可以添加在CDN中生效。如图1所示的即是添加的SSL证书(适用于主域名和www的子域名两个域名)与CDN加速域名(cdn的子域名)是不相匹配的,因此会抛出NET::ERR_CERT_COMMON_NAME_INVALID的错误。
      image.png

      图1. SSL证书适用范围不匹配导致NET::ERR_CERT_COMMON_NAME_INVALID错误

      二、配置证书至CDN。
      在按照流程申请得到CA证书后即可将该证书添加在CDN服务中供所有CDN的L1节点使用。CDN添加证书可以通过控制台的方式以及API/SDK的方式添加,但是配置参数是一致的,控制台配置截图如图2所示。主要包括证书名称、公钥和私钥三个参数。
      证书名称代表的是用户自己给予该对证书识别名称,用户可根据自己的业务要求定义其证书名称,仅需要保证本账号中不存在同名证书即可。公钥和私钥分别对应CA机构提供的证书内容和私钥内容。证书内容是以-----BEGIN CERTIFICATE-----和-----END CERTIFICATE-----开头和结束的证书链组成,而私钥内容是以-----BEGIN RSA PRIVATE KEY-----和 -----END RSA PRIVATE KEY----- 开头和结尾私钥组成。
      image.png

      图2. CDN控制台配置HTTPS证书示意图

      配置过程中需要注意以下内容:
      1、CA机构提供的证书为了兼容性可能会提供多种形式的证书,CDN支持的证书仅有PEM格式,并且私钥需要RSA格式。如果客户获取得到的是其他格式的证书是需要转换后然后提交到CDN服务中的,常见格式切换格式请参考:[CDN 证书格式说明](https://help.aliyun.com/document_detail/66710.html?spm=a2c6h.12873639.0.0.6cb223fba76KC8,而其中的私钥文件如果是-----BEGIN PRIVATE KEY-----, -----END PRIVATE KEY-----样式的话是需要通过如下命令转换成RSA格式:
      openssl rsa -in old_server_key.pem -out new_server_key.pem
      2、CDN是不支持设置密码的私钥。如图3所示即是经过加密的私钥,这类私钥文件是需要经过解密后才可以正常使用,因此CDN是无法正常使用的。
      image.png

      图3. 设置密码的私钥示意图

      3、证书链需要补全中间证书。对于中级CA机构提供的证书,那么拿到的证书将包括多份证书,而CDN需要添加的是包括中间证书的完整证书链,拼接规则为:服务器证书放第一份,中间证书放第二份,中间不要有空行。
      另外有一些中间证书CA机构提供了不同的服务器使用的证书,由于CDN是基于Tengine提供服务的,因此用户是需要使用Nginx对应的证书到视频中心的。如图4所示。

      image.png

      图4. 选择nginx证书示意图

      4、CDN的HTTPS技术是基于SNI技术实现的。SNI技术主要是用来在同一台服务器上配置多个证书的需求,而SNI是需要客户端发送请求的时候带有SNI的信息以标识是哪个域名的SSL请求,因此SNI技术对客户端有一定的要求,部分低版本系统中的低版本浏览器不满足该要求。SNI技术对于客户端的限制详细请参考:[SNI对客户端浏览器限制](https://www.ssllabs.com/ssltest/clients.html
      5、生效时间。CDN证书由于需要应用于所有的L1节点上因此会导致配置和更新都是需要一段时间的,设置HTTPS证书后约1小时后生效,更新HTTPS证书后约10分钟后生效。因此请用户提前部署好HTTPS证书后再正式上线业务。
      三、强制跳转。
      用户对于站点常有以下两种跳转需求:
      所有的 HTTP 请求跳转为 HTTPS 请求或者 HTTPS 请求跳转为 HTTP 请求;
      部分资源可以将 HTTP 请求跳转到 HTTPS 或者 HTTPS 跳转为 HTTP 请求。
      这两种需求对于第一种需求 CDN 是可以直接完成该需求的,可以直接在 HTTPS 的信息中直接添加对应的跳转设置(如图5)。而对于第二种需求 CDN 现在无法实现,需要用户的源站实现对应的 rewrite 功能,而为了保证每次请求都可以触发源站的重定向设置就需要用户针对于特定的资源设置在CDN 上不缓存(可以在 CDN 控制台设置缓存 0 秒或者源站设置 no-cache等禁止 CDN 缓存的头信息)。

      image.png

      图5. CDN配置HTTP与HTTPS跳转示意图


      另外,CDN配置HTTPS后常会出现508错误。508 错误是重定向回环的错误,该错误一般是由于用户在 CDN 开启 HTTPS 服务,并且设置回源端口为 80 ;而源站设置了 80 端口强制跳转 HTTPS协议,这样就会导致该请求又重新请求到 CDN 节点上,出现重定向回环,因此建议 HTTP 和 HTTPS 协议之间的跳转功能可以直接在 CDN 控制台进行设置即可。]]>
      Linux 发行版更新软件源-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 6.jpg
      镜像下载、域名解析、时间同步请点击 阿里巴巴开源镜像站

      一、前提条件

      更新软件之前,您已添加包含该软件包更新的软件源。具体步骤,请参见添加软件源

      二、更新CentOS上的软件

      1、连接Linux实例。详情请参见连接方式介绍
      2、更新软件。

      • 若您需要更新单个软件,运行以下命令。
      yum update <package> #将<package>替换为您需要更新的软件
      • 例如,您需要更新Apache服务器,则运行的命令为:
      yum update httpd
      • 若您需要更新系统的全部软件,运行以下命令。
        注意 由于该命令可能会更新系统内核,导致系统无法启动、内核与软件不兼容等问题。建议您完成不升级系统内核的配置后,再运行该命令。
      yum update

      三、更新Ubuntu或Debian上的软件

      1、连接Linux实例。详情请参见连接方式介绍
      2、运行以下命令获取软件包的更新列表。

      apt-get update

      3、更新软件。

      • 若您需要更新单个软件,运行以下命令。
      apt-get install <package> #将<package>替换为您需要更新的软件
      • 例如,您需要更新Python,则运行的命令为:
      apt-get install python
      • 若您需要更新系统全部软件,运行以下命令。
        注意 由于该命令可能会更新系统内核,导致系统无法启动、内核与软件不兼容等问题。建议您完成不升级系统内核的配置后,再运行该命令。
      apt-get upgrade

      四、更新OpenSUSE上的软件

      1、连接Linux实例。详情请参见连接方式介绍
      2、运行以下命令获取软件包的更新列表。

      zypper list-updates

      3、更新软件。

      • 若您需要更新单个软件,运行以下命令。
      zypper update <package>   #将<package>替换为您需要更新的软件
      • 例如,您需要更新Python,则运行的命令为:
      zypper update python
      • 若您需要更新系统全部软件,运行以下命令。
        注意 由于该命令可能会更新系统内核,导致系统无法启动、内核与软件不兼容等问题。建议您完成不升级系统内核的配置后,再运行该命令。
      zypper update

      五、执行结果

      更新完成后,您可以查看该软件版本号。如果显示为最新版本号,表明该软件更新成功。

      阿里巴巴开源镜像站 提供全面,高效和稳定的系统镜像、应用软件下载、域名解析和时间同步服务。”

      ]]>
      借助IoT平台云端数据解析能力,转换Modbus,电力协议,hex数据-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 1.整体技术方案

      在IoT场景中,很多传感器采集到的都是二进制数据,或者私有协议格式数据流,设备端又不具备转换成结构化JSON的能力,这时候我们可以借助IoT物联网平台云端自定义数据解析能力,转换Modbus,电力协议,hex数据,私有协议为结构化的JSON,再流转到业务系统。

      数据流转链路

      image.png

      消息变化

      image.png

      2.物联网平台开发

      消息通信Topic

      image.png

      hex转换脚本配置


      原始数据:0x035e8192fd0000000d0000001b00000a8c
      数据业务格式:
      image.png
      脚本配置
      image.png


      完整脚本内容

      /**
       * 将设备自定义topic数据转换为json格式数据, 设备上报数据到物联网平台时调用
       * 入参:topic   字符串,设备上报消息的topic     
       * 入参:rawData byte[]数组                  不能为空
       * 出参:jsonObj JSON对象                    不能为空
       */
      function transformPayload(topic, rawData) {
          var jsonObj = {}
          
          //原始hex数据 : 0x035e8192fd0000000d0000001b00000a8c
      /*
      {
        "heartbeat": 15,
        "id": 1585549855,
        "steps": 2700,
        "speed": 56
      }
      */
          if (topic.endsWith('/user/update')) {
                  var uint8Array = new Uint8Array(rawData.length);
                  for (var i = 0; i < rawData.length; i++) {
                      uint8Array[i] = rawData[i] & 0xff;
                  }
                  var dataView = new DataView(uint8Array.buffer, 0);
      
                  var fHead = uint8Array[0]; // command
                  if (fHead == 0x03) {
                      //
                      jsonObj['id'] = dataView.getInt32(1);
                      //心跳
                      jsonObj['heartbeat'] = dataView.getInt32(5);
                      //速度
                      jsonObj['speed'] = dataView.getInt32(9);
                      //总步数
                      jsonObj['steps'] = dataView.getInt32(13);
                  }
          }
          
          return jsonObj;
      }
        

      3.设备开发

      设备上报hex原始数据

      // 消息Topic携带?_sn=default标识
      const topic = '/aiDerw9823s/dn308/user/update'+'?_sn=default';
      // 原始数据
      var payloadArray = [ 3, 94, 129, 169, 59, 0, 0, 0, 23, 0, 0, 0, 79, 0, 0, 30, 220 ];
      var payload = new Buffer(payloadArray);
      // 发布数据到topic
      client.publish(topic, payload);
      
      

      4.联调日志

      设备上报原始hex数据

      image.png

      脚本转换后日志

      image.png

      业务消息报文日志

      消息详情(topic和payload)
      image.png

      ]]>
      Docker 安装 mysql8-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 hailuo_579095140_RF.jpg
      镜像下载、域名解析、时间同步请点击 阿里巴巴开源镜像站

      一、下载mysql8镜像

      docker pull mysql

      二、创建mysql8配置文件

      vi /etc/my.cnf #编辑MySQL配置文件

      my.cnf文件内容

      # Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
      #
      # This program is free software; you can redistribute it and/or modify
      # it under the terms of the GNU General Public License as published by
      # the Free Software Foundation; version 2 of the License.
      #
      # This program is distributed in the hope that it will be useful,
      # but WITHOUT ANY WARRANTY; without even the implied warranty of
      # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      # GNU General Public License for more details.
      #
      # You should have received a copy of the GNU General Public License
      # along with this program; if not, write to the Free Software
      # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
      #
      # The MySQL  Server configuration file.
      #
      # For explanations see
      # http://dev.mysql.com/doc/mysql/en/server-system-variables.html
      [mysqld]
      pid-file        = /var/run/mysqld/mysqld.pid
      socket          = /var/run/mysqld/mysqld.sock
      datadir         = /var/lib/mysql
      secure-file-priv= NULL
      # Disabling symbolic-links is recommended to prevent assorted security risks
      symbolic-links=0
      # Custom config should go here
      !includedir /etc/mysql/conf.d/

      三、创建mysql容器

      docker run -p 60306:3306 -e MYSQL_ROOT_PASSWORD=123 -v /etc/my.cnf:/etc/mysql/my.cnf:rw -v /etc/localtime:/etc/localtime:ro --name mysql8 --restart=always -dit mysql
      -p 60306:3306 #本机60306端口映射到容器3306端口
      -e MYSQL_ROOT_PASSWORD=123 #设置MySQL的root用户密码
      -v /etc/my.cnf:/etc/mysql/my.cnf:rw #本机的MySQL配置文件映射到容器的MySQL配置文件
      -v /etc/localtime:/etc/localtime:ro #本机时间与数据库时间同步
      --name mysql8 #设置容器别名
      --restart=always #当重启Docker时会自动启动该容器
      -dit mysql #后台运行并可控制台接入

      四、进入mysql控制台

      docker exec -it b6cfb244d0c0 bash #进入MySQL容器
      mysql -uroot -p123 #进入MySQL控制台
      ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; #修改root用户密码

      阿里巴巴开源镜像站 提供全面,高效和稳定的系统镜像、应用软件下载、域名解析和时间同步服务。”

      ]]>
      构建安全可靠的微服务 | Nacos 在颜铺 SaaS 平台的应用实践-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 1.jpeg

      作者 | 殷铭  颜铺科技架构师

      本文整理自架构师成长系列 3 月 19 日直播课程。
      关注“阿里巴巴云原生”公众号,回复 “319”,即可获取对应直播回放链接及 PPT 下载链接。

      导读:颜铺科技因美业⽽⽣,“颜铺专家”是一款专为美业商家打造的 SaaS 平台,为了能够给商户提供更加安全、稳定、高效的平台,我们在技术方面做了很多尝试,经过几次演进,使系统变得更加稳定可靠。今天主要和大家分享一下颜铺科技的架构演进,以及 Nacos 在颜铺的应用实践。

      单体应用时代

      2.png

      上图是我们单体服务时的架构图,分为会员、订单、门店等很多模块,看架构图似乎还算清晰,但是真正看到包结构的时候,真的令人头秃!改起代码特别头痛。单体服务带来的几个挑战:

      • 发布周期慢:虽然当时业务量不算大,但是代码量很大,业务迭代牵一发而动全身,每次发布需要对整个服务进行重新编译打包部署。特别是最开始在没有构建工具的时候,发布过程需要一堆的命令,总有一种 “一顿操作猛如虎,定睛一看原地杵” 的感觉。
      • 协同效率低:合并冲突多,有时你在前面开心地写代码,而别人在解决冲突时,可能也在开心地删着你的代码,增加了很多的沟通成本。
      • 稳定性差:当服务出现故障时,可能会导致整个系统不可用,并且系统不易扩容,当商户搞促销时,可能活动结束了,服务器还没有扩容完成。
      • 性能差:因为在单体服务内,有些开发人员为了满足自己的业务,很多无关业务间 SQL 联表查询,并且不关注性能问题,导致线上时常出现负载警告。 

      另外,我们在业务上也遇到了一些挑战:

      • 业务转型: 2018 年 6 月,我们公司决定从泛行业转向美业,需要打造一个专为美业商户提供技术支持的丽人 SaaS 平台。
      • 快速占领市场:业务的转型带来了更多商户的新需求,如果不能快速迭代,则意味着被市场淘汰。因此,提升开发效率,快速占领市场成为我们急需解决的问题。
      • 商户体验差:随着越来越多的商户入住,性能和可靠性的问题逐渐显现,出现问题,不能及时修正,商户体验变得很差,违背我们客户第一的原则。 

      综上所述,我们认为进行服务化改造刻不容缓。

      微服务改造

      经过公司开发同学们的讨论,我们最终决定分两步进行改造:
      服务化改造 1.0 的目标:

      • 用最小的改造成本先将新、旧商户平台进行打通,做到功能上的快速迁移;
      • 业务抽象,将新旧商户中心的公用部分进行抽象,并优化旧商户中心的代码逻辑,为后续的业务中台建设做好铺垫。

      服务化改造 2.0 的目标:初步建设业务中台,让平台的各种能力能够快速复用、快速组合,支持业务更快捷地探索与发展。
      服务化改造 1.0 预期效果:
      3.png

      • 我们希望老商户中心在对外提供服务的同时,还能够作为提供者,对新商户中心提供服务支持;
      • 新商户中心仅对外提供服务,不直连数据库,业务层只对美业的特殊逻辑进行处理。 

      因此,我们的想法是:新商户中心直接调用旧商户中心通过 Controller 暴露出的接口,进行远程调用,于是我们决定尝试使用 Spring Cloud 。

       服务发现选型:
      4.png

      • Consul 支持服务发现的同时,支持 kv 存储服务,因为我们想做一个配置中心的 KV 存储,所以想利用 Consul 做一个尝试;
      • 服务健康检查相对更为详细;
      • 在我们选型的期间,突然出现了 Eureka 2.x 开源工作宣告停止的消息,虽然后来发现,这个对我们并没有什么太大的影响,但在当时的决策让我们最终选择了 Consul 。 

      服务化改造 1.0 架构图:
      5.png

      服务化 1.0 我们的技术改造方案是:将旧的商户中心注册到 Consul 上面,新商户中心到 Consul 上获取服务器列表,通过 Feign 进行远程调用,打通了新老商户中心的功能。 

      经过服务化 1.0 的改造,我们解决了如下几个问题:

      • 功能快速完善:旧商户中心的功能快速迁移到了新的商户中心,并完成对美业的适配;
      • 迭代速度加快:新商户中心大部分功能,能够通过旧商户中心进行修改兼容,为后续的业务中台的抽象打好基础;
      • 性能优化:业务开发的同时,我们对旧商户中心的老代码进行优化,性能和稳定性均有所提高。 

      但服务化 1.0 改造后,还是有一些挑战没有解决:

      • 发布周期依旧不够快:大部分代码还是在就商户中心,业务迭代依然牵一发而动全身;
      • 协同效率没有提高:在代码冲突多,沟通成本高的同时,又出现了令开发同学头痛的新老业务的兼容问题;
      • 维护成本:Consul 是 Go 语言开发的,不易维护;Spring Cloud 在开发过程中体验不佳,在写业务的同时,还要摸索 Spring Cloud 的最佳实践,花费了一些时间去做 Spring Cloud 的基础建设。

      于是我们决定开启,服务化 2.0 的改造。
      服务化改造 2.0 的预期效果:
      6.png

      • 完成业务中台的初步建设,将模块重新划分,抽象为独立服务;
      • 新、旧商户中心服务仅做自己的业务兼容,并对外暴露接口;
      • 新增专门支持 H5、小程序 的 C 端 WEB 服务。 因 Spring Cloud 体验不佳,我们决定服务化改造 2.0 尝试使用 Dubbo 作为基础服务的 RPC 远程调用框架,因此我们要对注册中心进行选型。 

      首先,注册中心我认为应该具备的基本功能 :

      • 服务注册及时被发现,异常时的及时下线;
      • 服务管理,能够手动恢复/剔除服务;
      • 健康检查,检测服务是否可用;
      • 元数据管理;
      • 注册中心保证自身的高可用。

      7.png

      Zookeeper :

      • 不能保证每次服务请求都是可达的,当 zk 集群 master 挂掉时,需要进行选举,在选举期间中,服务是不可用的;
      • 不支持跨机房的路由,比如 eureka 的 zone,当前机房不可用时,可以路由到其他机房;
      • “惊群效应”, zk 的节点过多的时候,当 service 的节点发生变更,会同时通知到客户端,瞬时流量有可能将网卡瞬间打满,并且会有重复通知的问题。

      Nacos :

      • 注册中心部分更侧重于可用性
      • 服务发现与服务管理
      • 服务元数据的管理
      • 动态配置管理

      8.png

      在此期间,我们也关注到了 Spring Cloud Alibaba。阿里巴巴技术经受多年“双十一”的考验,其性能和稳定性是值得信任的。Spring Cloud Alibaba 的组件开源社区活跃度很高,并且比起国外开源项目更容易交流。其组件由 Java 语言开发,对我们来说更易维护,在出现问题时能够更快地定位问题进行修复。而且与阿里云配合,更加容易上云,比如 Nacos 可以与阿里云的 MSE 和 ACM 配合,将注册中心及配置管理全部上云。 9.png

      因此,我们决定拥抱阿里技术栈。 

      服务化改造2.0架构图:

      10.png

      我们将之前的模块直接抽到基础服务之中,新增了 会员、订单、门店 等服务作为Provider,暴露自己的Service,并注册到 Nacos 上。新商户中心服务做美业业务逻辑的处理,旧商户中心服务做泛行业的业务处理,C端服务同理对外提供服务。通过 Dubbo 进行远程调用。 

      通过服务化 2.0 的改造,效果如下:

      • 服务器成本降低30%:20+台服务器,由4核16G 降配到2核8G;
      • 系统可靠性提升80%:load 告警明显减少,线上的问题修正能够快速修复,完成部署;
      • 代码冲突减少75%:因为边界有了基本各自维护,冲突显著减少;
      • 发布迭代效率提升50%:之前5个人每个迭代开发评估可完成30个点,现在可完成45个点左右。

      Nacos 落地实践与问题分析

      Nacos 在我们公司处理做注册中心之外,配置管理也对我们提供了很好的服务。下面说一下,Nacos 我们的使用情况,以及我们遇到的问题。 

      首先是使用情况:

      • 部署方式:开发/测试环境单机部署,生产环境 3 台集群部署;
      • 版本:生产环境从 0.9.0 开始使用,目前生产环境使用的版本为 1.1.4 ;
      • 使用时间:2019 年 3 月份开始在生产环境下使用;
      • 服务数量:线上 20+ 台服务器,提供了 600+ 个服务;
      • 稳定性:一年的时间里没有出现大的问题,并且平滑升级;
      • 兼容性:新老服务,在我们公司无论是 Spring 4.3+ 的工程,还是 Spring Boot 的工程均兼容良好。 

      Nacos 注册中心:

      11.png

      • 服务注册:将后端服务注册到 Nacos,通过 Dubbo 进行调用。目前开发环境中我们正在测试Seata,并且也将 Seata 服务注册到 Nacos 上;
      • Namespace:服务统一注册到 public 中。 

      Nacos 配置管理:

      12.png

      每个服务设置独立的 Namespace 。 

      • 服务的配置文件信息:application.properties 全部配置到 Nacos,工程的配置文件仅保留 Nacos 相关配置;
      • 业务层的 KV 配置:比如业务开关,属性默认值,定时任务配置等;
      • MQ Topic 的动态配置:Binlog 服务采集动态发送到在 Nacos 配置的 topic 及其需要的表信息;
      • Sentinel 的规则配置:Sentinel 限流规则持久化到 Nacos 。

      问题描述:

      13.png

      2019 年 12 月 31 日,下午 3 点 15 分左右,线上突然出现大量服务告警,Dubbo 服务出现报错,整个过程持续约 3 多分钟。各个业务组当天均没有任何发布,数据库状态也良好。

      通过日志发现,报错原因是门店服务无法调用。而门店服务日志,出现问题的时间段内,没有任何的调用记录。系统恢复正常时,出现了很多服务注册的通知。

      因此,我们将问题瞄准了 Nacos。查看 Nacos 的日志发现,在系统恢复过程中,有大量的服务正在上线。

       就在排查的过程中,线上突然又出现了之前相同的告警,Nacos 上的服务列表开始大量变成不健康的状态,于是我们紧急重启了线上的 Nacos ,在这期间又经历了一个 3 分多钟的惊魂后,再次恢复了平静。 

      问题分析:

      • 两次出现的问题均是门店服务,但出现问题期间 JVM 和数据库的运行状态均良好;
      • 报错信息都是 Dubbo 调用超时,且出现问题期间,门店服务没有任何流量进入;
      • 出现问题时,注册在 Nacos 上的服务开始大量不健康。恢复正常时,这些服务又开始上线,说明出现问题时,服务被下线又重新上线。

       综上,我们开始怀疑是网络原因造成的。

       问题确认:

      14.png

      经过排查,发现我们的服务大多部署在 阿里云华东 1 可用区 B ,只有门店服务和 Nacos 集群没有部署在可用区 B ,说明这段时间可用区 B 与其他区之间的发生了网络隔离。

      于是,我们在可用区 B 紧急部署了门店服务,之后没有再出现问题。

      经过与阿里云的沟通确认于北京时间 2019 年 12 月 31 日 14:05 分左右开始,部分用户反馈阿里云华东 1 地域可用区 B 部分网络出现异常,影响部分云资源访问。

       问题复盘:

      • 问题出现:下午 3 点多,突然连续出现的服务告警, Dubbo 服务出现报错;
      • Nacos:Nacos 服务列表里大量服务出现不健康的状态;
      • 网络不通:可用区 B 与其它区网络不通,导致服务无法调用;
      • 紧急部署:在 B 区部署缺失的 门店服务;
      • 恢复正常。

       问题思考:

      • 服务部署:应用服务和Nacos建议多机房部署,即使在云上可用区之间也需要考虑;
      • 容灾:问题出现时,可用区 B 并没有部署 Nacos,但可用区B内的服务之间依然能调通,且能够读到 Nacos 上的配置。因此,我们认为 Nacos 以及 Dubbo 的容灾策略都是值得信赖的。 

      回顾与展望:

      15.png

      “颜铺专家”经过不断地快速迭代,帮助美业商家⾼效快捷地管理门店,进行经营数据分析,数据化管理门店,建⽴完善的会员周期管理体系,为美业商家在经营管理中,提供⼀体化的解决方案,将美业传统的门店经营模式进⾏互联网升级。截止到目前我们累计服务 3000 多个品牌,1.1W + 个⻔店。我们提供了店务管理系统、会员管理系统、营销拓客系统、大数据决策系统、供应链管理系统、员工绩效管理系统6⼤系统能力,同时⽀持 PC 端、手机 APP 、 pos 机、 iPad 操作,满⾜⻔店多端操作需求,覆盖⻔店经营管理中的所有场景需求。

      未来规划

      提升系统高可用

      • Seata :目前我们公司的分布式事务主要依赖 MQ 的补偿,今年准备引入 Seata 来完善分布式事务,保证数据一致性,减少开发修数据的情况;
      • Sentinel :目前 Sentinel 我们只是在商户做活动时启用,因此我们要配置出适用于我们公司的最佳实践,保证系统的高可用;
      • 全链路跟踪:我们公司现在定位问题主要靠日志和告警,做不到全链路的跟踪,所以我们要把这部分做好,做到故障快速定位,各调用环节性能分析,以及数据分析;
      • 异地容灾:随着来自全国各省的商户越来越多,我们需要对商户的数据保障,避免数据丢失,确保服务的可靠性。 

      社区回馈

      16.png

      因为我们的公司体量现在不大,我们能够做到的是尽可能地使用最新的版本,及时尝试新特性,对发现的问题提 issues,但我们也希望能够对 Nacos 开源社区尽一份我们的力量。

      作者信息:殷铭,颜铺科技架构师,负责颜铺 SAAS 平台中间件的应用和实践,主导了平台架构在颜铺向分布式演进的全过程,目前也负责大数据在颜铺平台的实践和落地。

      17.png

      阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”

      ]]>
      【OSS 排查方案-4】OSS + RTMP 推流的 JAVA 方法-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:张医博

      背景

      目前有很多直播爱好者使用的是 OSS + RTMP 做的推流直播,其中不乏一些企业级别应用,由于 OSS 作为推流的接收略微有一些复杂,故单独开篇讲一下。其实不建议使用 OSS+RTMP 做直播推流,因为功能性相比专业的阿里云直播几乎为 0 ,而且性能上并不好。建议大家使用单独的直播服务。

      首先

      看下官网对于 OSS 推流的过程定义

      • 只能使用RTMP推流的方式,不支持拉流。
      • 必须包含视频流,且视频流格式为H264。
      • 音频流是可选的,并且只支持AAC格式,其他格式的音频流会被丢弃。
      • 转储只支持HLS协议。
      • 一个LiveChannel同时只能有一个客户端向其推流。

      RTMP 的推流格式:

      rtmp://your-bucket.oss-cn-hangzhou.aliyuncs.com/live/test-channel

      • live 等同于 RTMP 的 APP 挂载点
      • test-channel 等同于 RTMP 的 stream name

      RTMP URL 推流签名:

      • rtmp://${bucket}.${host}/live/${channel}?OSSAccessKeyId=xxx&Expires=yyy&Signature=zzz&${params}
      • 推流前 LiveChannel有两种Status:enabled和disabled,用户可以使用本接口在两种Status之间进行切换。处于disabled状态时,OSS会禁止用户向该LiveChannel进行推流操作;如果有用户正在向该LiveChannel推流,那么推流的客户端会被强制断开(可能会有10s左右的延迟)

      我们现在用 java 的 SDK 演示一下如上的推理过程,在跑 SDK 之前,需要先搭建好一套本地的 eclipse 环境,如下是我用的 eclipse,如果有没搭建请网上搜索一下 eclpse 的搭建方式(之前需要安装 JDK 且配置环境变量)

      • Eclipse 版本:Version: Neon.3 Release (4.6.3)
      • JDK 版本:jdk1.8.0_144
      • OSS:公开读(为了验证推流功能是否正常,我们用公开读的方式快速测试)

      我们采用主函数入口的方式,实例化其他类进行调用,这样方便类的拆分,不用都集合在主函数中。
      主函数 domain,实例化 OSSClient 对象传入到 RtmpTest 类中测试。

      所有的 jar 都会通过官方的 maven 解决的依赖关系,https://help.aliyun.com/document_detail/32009.html?spm=5176.doc32008.6.672.HR4Dbr

      package javasdk;
      
      import java.io.FileNotFoundException;
      import java.text.ParseException;
      import java.util.HashMap;
      import java.util.Map;
      
      import com.aliyun.oss.OSSClient;
      
      public class domain {
          public static void main( String[] args ) throws ParseException, FileNotFoundException
          {
              
              System.out.println( "Hello World!" );
              String accessid = "AK";
              String secretkey = "SK";
              String objectpath = "C://Users//hanli.zyb//Desktop//running.png";
              String bucket = "bucket";
              String object = "running";
              String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";
      
              
              // OSS + rtmp 推流
              String bucketName = "ali-hangzhou";
              RtmpTest pushoss = new RtmpTest();
              OSSClient ossClient = new OSSClient(endpoint, AK, SK);
              pushoss.testCreateLiveChannel(bucketName,ossClient);
      
          }
      }
      ​

      RtmpTest

      /*
       * Licensed to the Apache Software Foundation (ASF) under one
       * or more contributor license agreements.  See the NOTICE file
       * distributed with this work for additional information
       * regarding copyright ownership.  The ASF licenses this file
       * to you under the Apache License, Version 2.0 (the
       * "License"); you may not use this file except in compliance
       * with the License.  You may obtain a copy of the License at
       *
       *     http://www.apache.org/licenses/LICENSE-2.0
       *
       * Unless required by applicable law or agreed to in writing,
       * software distributed under the License is distributed on an
       * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       * KIND, either express or implied.  See the License for the
       * specific language governing permissions and limitations
       * under the License.
       */
      
      package javasdk;
      
      import java.text.ParseException;
      import java.util.Date;
      import java.util.List;
      import junit.framework.Assert;
      
      import org.junit.Ignore;
      import org.junit.Test;
      
      import com.aliyun.oss.OSSClient;
      import com.aliyun.oss.OSSErrorCode;
      import com.aliyun.oss.OSSException;
      import com.aliyun.oss.common.utils.DateUtil;
      import com.aliyun.oss.model.CannedAccessControlList;
      import com.aliyun.oss.model.CreateLiveChannelRequest;
      import com.aliyun.oss.model.CreateLiveChannelResult;
      import com.aliyun.oss.model.ListLiveChannelsRequest;
      import com.aliyun.oss.model.LiveChannel;
      import com.aliyun.oss.model.LiveChannelInfo;
      import com.aliyun.oss.model.LiveChannelListing;
      import com.aliyun.oss.model.LiveChannelStat;
      import com.aliyun.oss.model.LiveChannelStatus;
      import com.aliyun.oss.model.LiveChannelTarget;
      import com.aliyun.oss.model.LiveRecord;
      import com.aliyun.oss.model.PushflowStatus;
      import com.aliyuncs.DefaultAcsClient;
      import com.aliyuncs.profile.DefaultProfile;
      import com.aliyuncs.profile.IClientProfile;
      
      /**
       * Test rtmp
       */
      public class RtmpTest  {
          String bucketName = "bucket";
          final String liveChannel = "stream name";
          @Test
          public void testCreateLiveChannelDefault(String bucketname,OSSClient ossClient) {
      
              try {
                  CreateLiveChannelRequest createLiveChannelRequest = new CreateLiveChannelRequest(
                          bucketName, liveChannel);
                  CreateLiveChannelResult createLiveChannelResult = ossClient.createLiveChannel(createLiveChannelRequest);
                  LiveChannelInfo liveChannelInfo = ossClient.getLiveChannelInfo(bucketName, liveChannel);
      
                  
                  ossClient.deleteLiveChannel(bucketName, liveChannel);
              } catch (Exception e) {
                  Assert.fail(e.getMessage());
              }
          }
          
          @Test
          public void testCreateLiveChannel(String bucketname,OSSClient ossClient) {
              final String liveChannel = "normal-create-live-channel";
              final String liveChannelDesc = "my test live channel";
      
              try {
                  LiveChannelTarget target = new LiveChannelTarget("HLS", 100, 99, "myplaylist.m3u8");
                  CreateLiveChannelRequest createLiveChannelRequest = new CreateLiveChannelRequest(
                          bucketName, liveChannel, liveChannelDesc, LiveChannelStatus.Enabled, target);
                  
                  CreateLiveChannelResult createLiveChannelResult = ossClient.createLiveChannel(createLiveChannelRequest);
                  System.out.println(createLiveChannelResult.getPublishUrls());
                  /*Assert.assertEquals(createLiveChannelResult.getPublishUrls().size(), 1);
                  Assert.assertTrue(createLiveChannelResult.getPublishUrls().get(0).startsWith("rtmp://"));
                  Assert.assertTrue(createLiveChannelResult.getPublishUrls().get(0).endsWith("live/" + liveChannel));
                  Assert.assertEquals(createLiveChannelResult.getPlayUrls().size(), 1);
                  Assert.assertTrue(createLiveChannelResult.getPlayUrls().get(0).startsWith("http://"));
                  Assert.assertTrue(createLiveChannelResult.getPlayUrls().get(0).endsWith(liveChannel + "/myplaylist.m3u8"));*/
                  
                 /* LiveChannelInfo liveChannelInfo = ossClient.getLiveChannelInfo(bucketName, liveChannel);
                  Assert.assertEquals(liveChannelInfo.getDescription(), liveChannelDesc);
                  Assert.assertEquals(liveChannelInfo.getStatus(), LiveChannelStatus.Disabled);
                  Assert.assertEquals(liveChannelInfo.getTarget().getType(), "HLS");
                  Assert.assertEquals(liveChannelInfo.getTarget().getFragDuration(), 100);
                  Assert.assertEquals(liveChannelInfo.getTarget().getFragCount(), 99);
                  Assert.assertEquals(liveChannelInfo.getTarget().getPlaylistName(), "myplaylist.m3u8");*/
                  
                  
                 // ossClient.deleteLiveChannel(bucketName, liveChannel);
              } catch (Exception e) {
                  Assert.fail(e.getMessage());
              }
          }
      }
      

      其中我们最关注的是 testCreateLiveChannel 类,创建了推流地址,其中参数 LiveChannelStatus.enable 是要让推流变为可用状态。

      public void testCreateLiveChannel(String bucketname,OSSClient ossClient) {
              final String liveChannel = "normal-create-live-channel";
              final String liveChannelDesc = "my test live channel";
      
              try {
                  LiveChannelTarget target = new LiveChannelTarget("HLS", 100, 99, "myplaylist.m3u8");
                  CreateLiveChannelRequest createLiveChannelRequest = new CreateLiveChannelRequest(
                          bucketName, liveChannel, liveChannelDesc, LiveChannelStatus.Enabled, target);
                  
                  CreateLiveChannelResult createLiveChannelResult = ossClient.createLiveChannel(createLiveChannelRequest);
                  System.out.println(createLiveChannelResult.getPublishUrls());
                  /*Assert.assertEquals(createLiveChannelResult.getPublishUrls().size(), 1);
                  Assert.assertTrue(createLiveChannelResult.getPublishUrls().get(0).startsWith("rtmp://"));
                  Assert.assertTrue(createLiveChannelResult.getPublishUrls().get(0).endsWith("live/" + liveChannel));
                  Assert.assertEquals(createLiveChannelResult.getPlayUrls().size(), 1);
                  Assert.assertTrue(createLiveChannelResult.getPlayUrls().get(0).startsWith("http://"));
                  Assert.assertTrue(createLiveChannelResult.getPlayUrls().get(0).endsWith(liveChannel + "/myplaylist.m3u8"));*/
                  
                  
                 // ossClient.deleteLiveChannel(bucketName, liveChannel);
              } catch (Exception e) {
                  Assert.fail(e.getMessage());
              }
          }​
      

      running 主函数,打印出推流地址。
      rtmp://hangzhou.oss-cn-hangzhou.aliyuncs.com/live/normal-create-live-channel
      请网友们自己亲自搭建环境动手实际测试,有问题可随时留言帖子。

      ]]>
      Service Mesh——后 Kubernetes 时代的微服务-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 本文是以前所写内容的重新修订并收录于 ServiceMesher 社区的 Istio Handbook 中,其他章节仍在编纂中。

      刚听说 Service Mesh 并试用过 Istio 的人可能都会有下面几个疑问:

      1. 为什么 Istio 要绑定 Kubernetes 呢?
      2. Kubernetes 和 Service Mesh 分别在云原生中扮演什么角色?
      3. Istio 扩展了 Kubernetes 的哪些方面?解决了哪些问题?
      4. Kubernetes、xDS 协议(EnvoyMOSN 等)与 Istio 之间又是什么关系?
      5. 到底该不该上 Service Mesh?

      这一节我们将试图带您梳理清楚 Kubernetes、xDS 协议以及 Istio Service Mesh 之间的内在联系。此外,本节还将介绍 Kubernetes 中的负载均衡方式,xDS 协议对于 Service Mesh 的意义以及为什么说及时有了 Kubernetes 还需要 Istio。

      使用 Service Mesh 并不是说与 Kubernetes 决裂,而是水到渠成的事情。Kubernetes 的本质是通过声明式配置对应用进行生命周期管理,而 Service Mesh 的本质是提供应用间的流量和安全性管理以及可观察性。假如你已经使用 Kubernetes 构建了稳定的微服务平台,那么如何设置服务间调用的负载均衡和流量控制?

      Envoy 创造的 xDS 协议被众多开源软件所支持,如 IstioLinkerdMOSN 等。Envoy 对于 Service Mesh 或云原生来说最大的贡献就是定义了 xDS,Envoy 本质上是一个 proxy,是可通过 API 配置的现代版 proxy,基于它衍生出来很多不同的使用场景,如 API Gateway、Service Mesh 中的 Sidecar proxy 和边缘代理。

      本节包含以下内容

      • 说明 kube-proxy 的作用。
      • Kubernetes 在微服务管理上的局限性。
      • 介绍 Istio Service Mesh 的功能。
      • 介绍 xDS 包含哪些内容。
      • 比较 Kubernetes、Envoy 和 Istio Service Mesh 中的一些概念。

      重要观点

      如果你想要提前了解下文的所有内容,那么可以先阅读下面列出的本文中的一些主要观点:

      • Kubernetes 的本质是应用的生命周期管理,具体来说就是部署和管理(扩缩容、自动恢复、发布)。
      • Kubernetes 为微服务提供了可扩展、高弹性的部署和管理平台。
      • Service Mesh 的基础是透明代理,通过 sidecar proxy 拦截到微服务间流量后再通过控制平面配置管理微服务的行为。
      • Service Mesh 将流量管理从 Kubernetes 中解耦,Service Mesh 内部的流量无需 kube-proxy 组件的支持,通过为更接近微服务应用层的抽象,管理服务间的流量、安全性和可观察性。
      • xDS 定义了 Service Mesh 配置的协议标准。
      • Service Mesh 是对 Kubernetes 中的 service 更上层的抽象,它的下一步是 serverless。

      Kubernetes vs Service Mesh

      下图展示的是 Kubernetes 与 Service Mesh 中的的服务访问关系(每个 pod 一个 sidecar 的模式)。

      kubernetes 对比 service mesh

      流量转发

      Kubernetes 集群的每个节点都部署了一个 kube-proxy 组件,该组件会与 Kubernetes API Server 通信,获取集群中的 service 信息,然后设置 iptables 规则,直接将对某个 service 的请求发送到对应的 Endpoint(属于同一组 service 的 pod)上。

      服务发现

      Service Mesh 中的服务注册

      Istio Service Mesh 可以沿用了 Kubernetes 中的 service 做服务注册,还可以通过控制平面的平台适配器对接其他服务发现系统,然后生成数据平面的配置(使用 CRD 声明,保存在 etcd 中),数据平面的透明代理(transparent proxy)以 sidecar 容器的形式部署在每个应用服务的 pod 中,这些 proxy 都需要请求控制平面来同步代理配置。之所以说是透明代理,是因为应用程序容器完全无感知代理的存在,该过程 kube-proxy 组件一样需要拦截流量,只不过 kube-proxy 拦截的是进出 Kubernetes 节点的流量,而 sidecar proxy 拦截的是进出该 Pod 的流量,详见理解 Istio Service Mesh 中 Envoy Sidecar 代理的路由转发

      Service Mesh 的劣势

      因为 Kubernetes 每个节点上都会运行众多的 Pod,将原先 kube-proxy 方式的路由转发功能置于每个 pod 中,将导致大量的配置分发、同步和最终一致性问题。为了细粒度地进行流量管理,必将添加一系列新的抽象,从而会进一步增加用户的学习成本,但随着技术的普及,这样的情况会慢慢地得到缓解。

      Service Mesh 的优势

      kube-proxy 的设置都是全局生效的,无法对每个服务做细粒度的控制,而 Service Mesh 通过 sidecar proxy 的方式将 Kubernetes 中对流量的控制从 service 一层抽离出来,可以做更多的扩展。

      kube-proxy 组件

      在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程。kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)的形式。 在 Kubernetes v1.0 版本,代理完全在 userspace 实现。Kubernetes v1.1 版本新增了 iptables 代理模式,但并不是默认的运行模式。从 Kubernetes v1.2 起,默认使用 iptables 代理。在 Kubernetes v1.8.0-beta.0 中,添加了 ipvs 代理模式。关于 kube-proxy 组件的更多介绍请参考 kubernetes 简介:service 和 kube-proxy 原理使用 IPVS 实现 Kubernetes 入口流量负载均衡

      kube-proxy 的缺陷

      kube-proxy 的不足之处

      首先,如果转发的 pod 不能正常提供服务,它不会自动尝试另一个 pod,当然这个可以通过 liveness probes 来解决。每个 pod 都有一个健康检查的机制,当有 pod 健康状况有问题时,kube-proxy 会删除对应的转发规则。另外,nodePort 类型的服务也无法添加 TLS 或者更复杂的报文路由机制。

      Kube-proxy 实现了流量在 Kubernetes service 多个 pod 实例间的负载均衡,但是如何对这些 service 间的流量做细粒度的控制,比如按照百分比划分流量到不同的应用版本(这些应用都属于同一个 service,但位于不同的 deployment 上),做金丝雀发布(灰度发布)和蓝绿发布?Kubernetes 社区给出了 使用 Deployment 做金丝雀发布的方法,该方法本质上就是通过修改 pod 的 label 来将不同的 pod 划归到 Deployment 的 Service 上。

      Kubernetes Ingress vs Istio Gateway

      上文说到 kube-proxy 只能路由 Kubernetes 集群内部的流量,而我们知道 Kubernetes 集群的 Pod 位于 CNI 创建的外网络中,集群外部是无法直接与其通信的,因此 Kubernetes 中创建了 ingress 这个资源对象,它由位于 Kubernetes 边缘节点(这样的节点可以是很多个也可以是一组)的 Ingress controller 驱动,负责管理南北向流量,Ingress 必须对接各种 Ingress Controller 才能使用,比如 nginx ingress controllertraefik。Ingress 只适用于 HTTP 流量,使用方式也很简单,只能对 service、port、HTTP 路径等有限字段匹配来路由流量,这导致它无法路由如 MySQL、Redis 和各种私有 RPC 等 TCP 流量。要想直接路由南北向的流量,只能使用 Service 的 LoadBalancer 或 NodePort,前者需要云厂商支持,后者需要进行额外的端口管理。有些 Ingress controller 支持暴露 TCP 和 UDP 服务,但是只能使用 Service 来暴露,Ingress 本身是不支持的,例如 nginx ingress controller,服务暴露的端口是通过创建 ConfigMap 的方式来配置的。

      Istio Gateway 的功能与 Kubernetes Ingress 类似,都是负责集群的南北向流量。Istio Gateway 描述的负载均衡器用于承载进出网格边缘的连接。该规范中描述了一系列开放端口和这些端口所使用的协议、负载均衡的 SNI 配置等内容。Gateway 是一种 CRD 扩展,它同时复用了 sidecar proxy 的能力,详细配置请参考 Istio 官网

      xDS 协议

      下面这张图大家在了解 Service Mesh 的时候可能都看到过,每个方块代表一个服务的实例,例如 Kubernetes 中的一个 Pod(其中包含了 sidecar proxy),xDS 协议控制了 Istio Service Mesh 中所有流量的具体行为,即将下图中的方块链接到了一起。

      Service Mesh 示意图

      xDS 协议是由 Envoy 提出的,在 Envoy v2 版本 API 中最原始的 xDS 协议指的是 CDS(Cluster Discovery Service)、EDS(Endpoint Discovery service)、LDS(Listener Discovery Service) 和 RDS(Route Discovery Service),后来在 v3 版本中又发展出了 Scoped Route Discovery Service(SRDS)、Virtual Host Discovery Service (VHDS)、Secret Discovery Service(SDS)、Runtime Discovery Service(RTDS)详见 xDS REST and gRPC protocol

      下面我们以各有两个实例的 service,来看下 xDS 协议。

      xDS 协议

      上图中的箭头不是流量进入 Proxy 后的路径或路由,也不是实际顺序,而是想象的一种 xDS 接口处理顺序,其实 xDS 之间也是有交叉引用的。

      支持 xDS 协议的代理通过查询文件或管理服务器来动态发现资源。概括地讲,对应的发现服务及其相应的 API 被称作 xDS。Envoy 通过订阅(subscription)方式来获取资源,订阅方式有以下三种:

      • 文件订阅:监控指定路径下的文件,发现动态资源的最简单方式就是将其保存于文件,并将路径配置在 ConfigSource 中的 path 参数中。
      • gRPC 流式订阅:每个 xDS API 可以单独配置 ApiConfigSource,指向对应的上游管理服务器的集群地址。
      • 轮询 REST-JSON 轮询订阅:单个 xDS API 可对 REST 端点进行的同步(长)轮询。

      以上的 xDS 订阅方式详情请参考 xDS 协议解析。Istio 使用 gRPC 流式订阅的方式配置所有的数据平面的 sidecar proxy。

      文章中介绍了 Istio pilot 的总体架构、proxy 配置的生成、pilot-discovery 模块的功能,以及 xDS 协议中的 CDS、EDS 及 ADS,关于 ADS 详情请参考 Envoy 官方文档

      xDS 协议要点

      最后总结下关于 xDS 协议的要点:

      • CDS、EDS、LDS、RDS 是最基础的 xDS 协议,它们可以分别独立更新。
      • 所有的发现服务(Discovery Service)可以连接不同的 Management Server,也就是说管理 xDS 的服务器可以是多个。
      • Envoy 在原始 xDS 协议的基础上进行了一些列扩充,增加了 SDS(秘钥发现服务)、ADS(聚合发现服务)、HDS(健康发现服务)、MS(Metric 服务)、RLS(速率限制服务)等 API。
      • 为了保证数据一致性,若直接使用 xDS 原始 API 的话,需要保证这样的顺序更新:CDS –> EDS –> LDS –> RDS,这是遵循电子工程中的先合后断(Make-Before-Break)原则,即在断开原来的连接之前先建立好新的连接,应用在路由里就是为了防止设置了新的路由规则的时候却无法发现上游集群而导致流量被丢弃的情况,类似于电路里的断路。
      • CDS 设置 Service Mesh 中有哪些服务。
      • EDS 设置哪些实例(Endpoint)属于这些服务(Cluster)。
      • LDS 设置实例上监听的端口以配置路由。
      • RDS 最终服务间的路由关系,应该保证最后更新 RDS。

      Envoy

      Envoy 是 Istio Service Mesh 中默认的 Sidecar,Istio 在 Enovy 的基础上按照 Envoy 的 xDS 协议扩展了其控制平面,在讲到 Envoy xDS 协议之前我们还需要先熟悉下 Envoy 的基本术语。下面列举了 Envoy 里的基本术语及其数据结构解析,关于 Envoy 的详细介绍请参考 Envoy 官方文档,至于 Envoy 在 Service Mesh(不仅限于 Istio) 中是如何作为转发代理工作的请参考网易云刘超的这篇深入解读 Service Mesh 背后的技术细节 以及理解 Istio Service Mesh 中 Envoy 代理 Sidecar 注入及流量劫持,本文引用其中的一些观点,详细内容不再赘述。

      Envoy proxy 架构图

      基本术语

      下面是您应该了解的 Enovy 里的基本术语:

      • Downstream(下游):下游主机连接到 Envoy,发送请求并接收响应,即发送请求的主机。
      • Upstream(上游):上游主机接收来自 Envoy 的连接和请求,并返回响应,即接受请求的主机。
      • Listener(监听器):监听器是命名网地址(例如,端口、unix domain socket 等),下游客户端可以连接这些监听器。Envoy 暴露一个或者多个监听器给下游主机连接。
      • Cluster(集群):集群是指 Envoy 连接的一组逻辑相同的上游主机。Envoy 通过服务发现来发现集群的成员。可以选择通过主动健康检查来确定集群成员的健康状态。Envoy 通过负载均衡策略决定将请求路由到集群的哪个成员。

      Envoy 中可以设置多个 Listener,每个 Listener 中又可以设置 filter chain(过滤器链表),而且过滤器是可扩展的,这样就可以更方便我们操作流量的行为,例如设置加密、私有 RPC 等。

      xDS 协议是由 Envoy 提出的,现在是 Istio 中默认的 sidecar proxy,但只要实现 xDS 协议理论上都是可以作为 Istio 中的 sidecar proxy 的,例如蚂蚁金服开源的 MOSN

      Istio Service Mesh

      Istio service mesh 架构图

      Istio 是一个功能十分丰富的 Service Mesh,它包括如下功能:

      • 流量管理:这是 Istio 的最基本的功能。
      • 策略控制:通过 Mixer 组件和各种适配器来实现,实现访问控制系统、遥测捕获、配额管理和计费等。
      • 可观测性:通过 Mixer 来实现。
      • 安全认证:Citadel 组件做密钥和证书管理。

      Istio 中的流量管理

      Istio 中定义了如下的 CRD 来帮助用户进行流量管理:

      • GatewayGateway 描述了在网络边缘运行的负载均衡器,用于接收传入或传出的HTTP / TCP连接。
      • VirtualServiceVirtualService 实际上将 Kubernetes 服务连接到 Istio Gateway。它还可以执行更多操作,例如定义一组流量路由规则,以便在主机被寻址时应用。
      • DestinationRuleDestinationRule 所定义的策略,决定了经过路由处理之后的流量的访问策略。简单的说就是定义流量如何路由。这些策略中可以定义负载均衡配置、连接池尺寸以及外部检测(用于在负载均衡池中对不健康主机进行识别和驱逐)配置。
      • EnvoyFilterEnvoyFilter 对象描述了针对代理服务的过滤器,这些过滤器可以定制由 Istio Pilot 生成的代理配置。这个配置初级用户一般很少用到。
      • ServiceEntry:默认情况下 Istio Service Mesh 中的服务是无法发现 Mesh 外的服务的,ServiceEntry 能够在 Istio 内部的服务注册表中加入额外的条目,从而让网格中自动发现的服务能够访问和路由到这些手工加入的服务。

      Kubernetes vs xDS vs Istio

      在阅读完上文对 Kubernetes 的 kube-proxy 组件、xDS 和 Istio 中流量管理的抽象概念之后,下面将带您仅就流量管理方面比较下三者对应的组件/协议(注意,三者不可以完全等同)。

      Kubernetes xDS Istio Service Mesh
      Endpoint Endpoint -
      Service Route VirtualService
      kube-proxy Route DestinationRule
      kube-proxy Listener EnvoyFilter
      Ingress Listener Gateway
      Service Cluster ServiceEntry

      总结

      如果说 Kubernetes 管理的对象是 Pod,那么 Service Mesh 中管理的对象就是一个个 Service,所以说使用 Kubernetes 管理微服务后再应用 Service Mesh 就是水到渠成了,如果连 Service 你也不想管了,那就用如 knative 这样的 serverless 平台,但这就是后话了。

      Envoy/MOSN 的功能也不只是做流量转发,以上概念只不过是 Istio 在 Kubernetes 之上新增一层抽象层中的冰山一角,但因为流量管理是服务网格最基础也是最重要的功能,所以这将成为本书的开始。

      参考

      ]]>
      为什么支付宝有底气说“你敢付我敢赔”?-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 随着技术纵深发展,“万物互联”的概念已不鲜见,在数字化世界的游戏规则里,除了网络基础信息安全隐患外,业务安全风险也已经越来越被重视。

      陈锣斌是支付宝大安全的资深架构师,已拥有近十年“蚁龄”。

      从2010年9月加入蚂蚁后,陈锣斌主要负责业务风控平台的实时计算以及数据相关工作,经历过整个业务风控从2代平台到5代平台进入智能化阶段的建设,近两年陈锣斌同时进入到基础安全领域,带领平台研发团队建设“安全全域态势威胁感知平台”,为支付宝整体安全的攻防提供有力支撑。

      当我们在谈互联网安全的时候,我们在谈什么

      在陈锣斌看来,互联网安全目前主要两个大领域,一块是网络信息安全或者说是基础安全,另一块是在网络上各种应用、服务所涉及的业务本身的业务安全。在此之后才有“风控系统”的到来。

      前者其实就是互联网上的信息安全。涉及到互联网上信息的保密性、完整性、可用性、真实性和可控性的相关技术。比如,个人在网站上的隐私信息保护、网络通信的时候防止信息被窃取监听,为防止黑客入侵造成服务攻击或者数据泄露所建设的各种安全防护能力等。

      最开始是1988 年,世界上第一例蠕虫病毒“Morris”出现,之后世界上各种病毒、木马和网络攻击层出不穷,严重威胁到互联网的繁荣和用户数据的安全。特别是制造网络病毒逐渐成为一种有利可图的产业,网络安全就基本上成为伴随互联网扩散的常态问题,且愈演愈烈。

      从“Morris”到“wannacry”,蠕虫病毒挑战的不只是安全防护技术

      后者业务安全,其实就是在互联网上开展各种业务所面临的一些风险防控的技术。比如社交的相关业务就会涉及到内容的安全(暴恐政、黄赌毒),电商业务的刷单、营销作弊,还有就是支付的时候的支付风控、洗钱、欺诈等风险防控。

      网络安全的过去、现在和未来

      陈锣斌认为,早期对安全的认知都相对简单,很多人认为个人电脑就是杀毒软件,网络就是防火墙,业务安全可能就是数字证书、密码加上简单的异步监控。但随着市场的扩张,不断升级的安全风控的能力成为各大互联网公司的标配。

      互联网是一个开放的世界,不法分子、黑产都会想在其中寻找牟利的机会,互联网重要业务的开展现在都离不开多种网络安全的能力,当一个公司不具备这方面足够能力的时候,很容易使得业务开展举步维艰,甚至直接影响公司的生存,比如,游戏(入侵系统、游戏外挂)、社交(内容安全)、营销广告(营销作弊)、电商(刷单、欺诈),愈演愈烈的风险手法已经不是在单一的安全领域能独立解决的,更需要从整个安全体系来看,综合的安全防控方案是什么,基础安全和业务安全深入联动也已成为行业共识。

      相对应地,基础安全技术和业务安全技术也两者从原先的相对比较独立到逐渐走向了融合,业务安全主要基于大数据计算、模型算法来做风险检测,基础安全在反入侵、反爬主机安全等方面也使用很多的大数据技术,两者在风险的联合防控上越来越紧密,在技术上大数据计算、人工智能方面也有越来越多的交集。支付宝AlphaRisk就是这方面的典范。

      陈锣斌重点揭示:网络安全趋势将走向需要综合性防控的道路,单一安全/风险技术都很难防控住目前互联网上的新型风险。

      支付宝为什么腰杆这么硬:“你敢付,我敢赔。”

      早在2005年,支付宝就有相应的风控系统以及对应的职能部门,为守护客户的每一分钱而努力。“你敢付,我敢赔”的理念一直延续至今。

      相信有不少人看过《智造将来》黑客攻击支付宝的电视节目,节目中几位顶尖黑客拿着“真枪实弹”一层层攻入一位普通用户的支付宝,试图转走5元。5元在普通的转账过程中其实是极为正常的,这样小额的数目为支付宝的安全系统带去了更大的挑战。据支付宝大安全掌门人芮雄文回忆,当时确实捏了一把冷汗,黑客已经拿到了支付宝用户的密码、银行卡,甚至已经用软件控制了手机可以发送验证码当场修改密码……在看似360度无死角的黑客攻击后,支付宝的AlphaRisk依旧是完美地阻止了所有非正常的支付行为。

      当“黑客”攻入支付系统时,雄文在内的所有人都捏了一把汗

      支付宝已经成为用户全球排名第一的移动支付工具,而保障支付宝毫发未损的风控系统经历了五代升级,现如今,它的正式名称为“AlphaRisk”。

      AlphaRisk是支付宝风控多年实践与技术创新的智慧结晶,是全球最先进的风控系统之一,在其保护下,支付宝交易资损率不到千万分之一,远低于国际同行。“如果拿自动驾驶的等级定义来比喻,目前AlphaRisk处于L2和L3之间。在智能化的加持下,处于行业领先水平。”陈锣斌说。

      AlphaRisk的原理是应用AI技术颠覆传统风控的运营模式,通过Perception(风险感知)、AI Detect(风险识别)、Evolution(智能进化)、AutoPilot(自动驾驶)4大模块的构建,将人类直觉AI(Analyst Intuition)和机器智能AI(Artificial Intelligence)完美结合,打造具有机器智能的风控系统,愿景是实现风控领域的“无人驾驶”技术。

      支付宝平台上每天都有上亿笔交易,通过AlphaRisk智能风控引擎,不仅能够对每个用户的每笔支付进行7×24小时的实时风险扫描;同时通过不断新增的风险特征挖掘和优化算法迭代的模型,自动贴合用户行为特征进行进化风险对抗,不足0.1秒就能完成风险预警、检测、管控等流程,确保用户账户安全和支付交易的万无一失。

      安全一直是支付宝发展的生命线,支付宝从一而终地践行着“你敢付,我敢赔”的理念,时至今日,支付宝在风控领域的探索、创新,以及落地应用都处于世界的前列。

      “热情加实践经验是最重要的”

      目前支付宝大安全也一定程度上代表了当今世界安全技术的巅峰,加入这个团队你必然会被培养成在网络安全技术领域的中流砥柱。

      在陈锣斌眼里,安全风控领域是集基础建设、安全攻防、大数据技术、人工智能于大成的一个交叉领域。由于在激烈的复杂对抗一线,在这个领域的人能得到最充分的锻炼,技术也最有希望快速做到业界顶尖。

      对于想加入大安全的同学来说,陈锣斌建议同学们在夯实基础的同时,还是要多多动手,多做,无论实验室项目还是企业实习项目,实际去做和停留在理论上是不同的结果,具体还有如下三点:

      1.技术基础:计算机基础知识扎实、数据结构、算法等能活学活用,有1~2门掌握的较好的语言。

      2.有成果:有较多的实践,有实际项目经验很重要,真正用技术、算法去解决过一些问题,也可以是去参加过什么比赛,有过何种收获等。

      3.有热情:学习能力,平时在自己的领域之外,是否有关注更多的前沿技术,学了什么,去思考过什么。

      加入我们

      > 团队介绍

      蚂蚁金服大安全事业群是管理蚂蚁金服账户安全、资金安全、内容安全和数据信息安全、系统平台安全的核心部门。团队运用最新的生物核身技术和大数据技术,结合专家经验、数据分析方法和AI手段,保障用户的权益和资金安全。

      蚂蚁金服的旗下产品包括支付宝、蚂蚁财富、芝麻信用、花呗、相互宝、余额宝、蚂蚁金服等。

      > 你想要的我们都有

      前沿领域:科技驱动金融,金融科普大众,支付宝的核心业务在于安全。参与建设各种安全项目,感受安全如何成为实现普惠金融、构建新金融生态的核心驱动力。

      一流平台:全球10亿用户的大平台,全球一流的互联网安全技术体系!近距离接触支付宝风控多年实践与新技术创新的最新成果——第五代智能风控引擎AlphaRisk!

      最新技术:体验应用AI技术颠覆传统风控的最新动向,参与建设具有机器智能的风控系统,一起实现风控领域的“无人驾驶”技术。

      > 岗位简介

      社招工作地点:杭州

      校招/实习工作地点:杭州/上海/成都/北京

      校招/实习岗位类型:

      1. 研发类:Java、C++、安全、前端、客户端、数据研发

      2. 算法类:机器学习、图像图形、NLP等

      > 简历投递

      邮箱:luobin.chen@antfin.com

      蚂蚁金服“共战‘疫情’,技术破局”数字课堂线火热直播中。4月2日,走进阿里云云栖号在线课堂,蚂蚁金服数据技术专家王嘉喆和高级体验设计师徐海豪将针对新版Ant Design 4.0的整体设计思路,以及在图表自动生成的用法和原理上的突破进行详细介绍。扫描下方二维码即可观看直播!

      ]]>
      E-MapReduce弹性低成本离线大数据分析-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 作者:明誉


      大数据是一项涉及不同业务和技术领域的技术和工具的集合,海量离线数据分析可以应用于多种商业系统环境,例如,电商海量日志分析、用户行为画像分析、科研行业的海量离线计算分析任务等场景。

      离线大数据分析概述

      主流的三大分布式计算框架系统分别为Hadoop、Spark和Storm:

      • Hadoop可以运用在很多商业应用系统,可以轻松集成结构化、半结构化以及非结构化数据集。
      • Spark采用了内存计算,允许数据载入内存作反复查询,融合数据仓库、流处理和图形计算等多种计算范式,能够与Hadoop很好地结合。
      • Storm适用于处理高速、大型数据流的分布式实时计算,为Hadoop添加可靠的实时数据处理能力。

      海量离线数据分析可以应用于多种场景,例如:

      • 商业系统环境:电商海量日志分析、用户行为画像分析。
      • 科研行业:海量离线计算分析和数据查询。
      • 游戏行业:游戏日志分析、用户行为分析。
      • 商业用户:数据仓库解决方案的BI分析、多维分析报表。
      • 大型企业:海量IT运维日志分析。

      架构图

      image.png

      方案优势

      • 高性能、低成本
      • 快速部署
      • 弹性
      • 多种计算模式
      • 无缝对接开源生态
      • 一站式管理平台

      方案详情

      详情请参见E-MapReduce弹性低成本离线大数据分析最佳实践


      对开源大数据感兴趣的同学可以加小编微信(图一二维码,备注进群)进入技术交流微信2群。也可钉钉扫码加入社区的钉钉群

      image.png

      阿里巴巴开源大数据技术团队成立Apache Spark中国技术社区,定期推送精彩案例,技术专家直播,问答区数个Spark技术同学每日在线答疑,只为营造纯粹的Spark氛围,欢迎钉钉扫码加入!
      image.png

      Apache Spark技术交流社区公众号,微信扫一扫关注

      image.png

      ]]>
      【CDN 排查方案-1】认识 CDN 网络调优-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:张医博

      背景:面向不同业务类型的网站,很多人都选择了 CDN 加速来优化自己的网站,目的在于加速网民的体验效果,赢取流量。

      在网站调优的过程中,如果正确理解基于 CDN 的网络调优以及正确的配合 CDN 服务方来快速提供调优信息做了详细的讲解,

      希望对大家有用,希望对从事 CDN 的人和对网络调优感兴趣的人能有作用。

      1、确认调优类型

      1.1、静态文件

      • 小文件: URL 小于 64KB
      • 大文件: URL 大于 1MB
      • 视频流:相当于大文件,要求速度平稳无卡顿
      • 全页面:网页包含多个域名及元素

      1.2 动态

      • 小文件: URL 小于 64KB
      • 全页面网页: 包含多个域名及元素
      • 事物流程:模拟访问,登录,点击,提交等行为

      1.3 协议

      • http、https

      1.4 调优评测机制

      • NetworkBench 、Gomez、 Bonree

      1.5 用户自定义评测

      • 日志统计(下载日志分析对比)
      • client 端探测(内嵌入播放器)
      • 自定义监测点(苹果 ADSL 检测)

      2 观察指标时间,找出阻塞时间点

      DNS 时间

      image.png

      建联时间

      image.png

      • 定义:

      浏览器和 WEB 服务建立 TCP/IP 连接时间

      • 特征

      代表 RTT ,直接体现 ping 值,反应 CDN 边缘节点质量

      建联时间应该在 30ms 以内,小文件建联时间应越小越好。

      建连时间是基础,建联时间长会引发一系列慢的情况,不同 CDN 服务商会有不同的 TCP/IP 的优化。

      SSL 握手时间

      image.png

      • 定义

      浏览器和 WEB 服务端建立安全套接层(SSL)链接的消耗时间

      • 特征

      只有 HTTPS 协议中才有这个时间指标,反应服务端的处理能力,SSL 握手时间长会导致整体时间长

      首包时间

      • 定义:是从浏览器发送 HTTP 请求结束后,收到 WEB 服务器返回的第一个有效载荷数据包
      • 特征:代表建联时间和服务器的响应时间之和

      建联时间长且首包时间长:网络问题

      建连时间端但首包时间长:服务器负载过高,或者静态数据没能缓存住

      内容下载时间

      image.png

      • 定义:浏览器从收到首包开始计时,至数据包全部接收全所需要的时间
      • 特征:

      受网路质量(RTT 和丢包率)影响

      受设备负载影响

      受 TCP 协议栈技术影响

      3、CDN 调优可调的指标有哪些

      • 解析层级 :应控制台二层以内,解析层级过多损耗更多。
      • CDN 节点配置:节点精细化比较好,一般都是到省级别的骨干节点,辅助个别地市级别的节点
      • 如果遇到域名劫持,需要向当地运营商反馈,目前 CDN 在 client 端防劫持的手段可以通过 HTTPS 预防,效果不一定是 100% 但是在 DNS 阶段不太好禁止。DNS 劫持我们可以通过 nslookup 或者 dig 验证。

      检查设备负载

      • CPU 负载高,导致应用处理能力下降
      • 磁盘 I/O 负载高,导致服务器给出资源耗时长
      • cache server 响应时间
      • 网络负载
      • 带宽负载

        • 设备带宽

      虚拟机(网桥性能问题)接近阈值丢包响应时间长

      物理机(网卡级别)接近阈值丢包,响应时间长

      • 连接数异常高,导致网络处理能力下降

      长连接

      keepalive 是使用同一个 TCP 链接来发送和接收多个 HTTP 请求/应答,而不是每一个新的请求/应答打开新的连接的方法。

      image.png

      未完待续

      ]]>
      【OSS 排查方案-3】OSS 的网络排查-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:张医博

      背景

      鉴于之前遇到很多 本地-》OSS ,上传、下载总是慢的情况,或者上传、下载经常出现错误或者异常的问题。根据多个典型案例,抽象出一下排查方案,希望对大家快速定位问题有所帮助。

      一、必要了解信息

      • requestID ,一般情况都会有,标识此次 OSS request 到达 OSS 服务端后返回的标志。除非建联失败,否则都会有这个 requestID 的,当问题比较难排查时可以将 requestID 提供给阿里云客服作为定位线索。
      • 明确 上传/下载 的方式(阿里的工具、SDK、API、浏览器),不同的方式会有不同的思路,类似 JAVA SDK 如果 maxconnection 设小了,也会造成链接等待超时。
      • 使用内网还是公网,大多数我们建议在 ECS 和 OSS 同可用区时最好使用内网,这样费用低,速度还快,公网一般出现拥塞,会被网络无限扩大。
      • 在 client 端 ping traceroute MTR 到 OSS 服务端的截图信息,看看到公/私网是否有丢包,或者用 tcpdump 抓包看下网络是否有高延迟高丢包以及 TCP 协议栈异常的问题。
      • 是否有明显报错,基本上遇到 socket timeout 的情况,都是网络超时,可以从本机的网络以及本机连接数,或者 client 是否有超时设置来排查。。
      • 以上信息收集到后准备测试,源 OSS URL 测试(举例):curl -svo /dev/null http://img.oss-cn-hangzhou.aliyuncs.com/uploads/temp/2018-01-02%20%2013-52-15-operate-stat.xls
      • 如果要是 CDN -》 OSS 的问题,可以分别固定 CDN 和 OSS 源站进行测试 curl -I -x CDNIP:80 http://xxx/xxx,固定源站 curl -I -x http://xxx.oss-cn-xxx.aliyuncs.com/xxx 如果测试 OSS 200 正常, CDN 异常,则问题可能发生在 CDN 侧。

      二、明确自己的服务架构逐层排查

      • 本地 -》 OSS
      • 本地 -》proxy -》OSSproxy 种类有很多,
      • SLB
      • WAF
      • 高防

      三、场景拟合

      1、长时间或者间断性不能访问到 OSS

      这种情况,先确认一下自己 bucket 有没有欠费,是否有被拉黑等,其次再本地的 PC 端 ping 下自己要访问的 OSS 公网域名(如果是用内网 ECS 通过私网 OSS 域名上传,可以 ping 私网域名)是否能通,以及 traceroute 到对端 OSS 的路由路径,看下是否断在了哪一跳。同时请自己网外的他人协助下同时发起 ping 和
      traceroute 测试,看是否同样访问不通。如果是一样场景,可以将搜集的信息反馈给阿里云客服排查服务端是否异常。

      image.png

      场景2、本机上传文件到 OSS 超时,然后自动恢复

      image.png

      如图,遇到这类问题,需要先获取到必要信息。然后结合图中的 error 来看 socket 的异常,基本判断是由于网络问题导致了 Header 响应超时。侧面的 MTR TRACEROUTE 也可以看出来当时的网络质量如何,如网络正常但依然超时,可以直接抓包看下是哪端导致。

      场景3、使用 JAVA SDK 上传文件超时返回 502

      [chat-service] 2017-12-22 11:09:17,443 - com.aliyun.oss:73 -51224385 [http-nio-9081-exec-137] WARN - [Server]Unable to execute HTTP request: The difference between the request time and the current time is too large. [ErrorCode]: RequestTimeTooSkewed [RequestId]: 5A3C77583373BA19746BB032 [HostId]: sobot.oss-cn-beijing.aliyuncs.com [ResponseError]: <?xml version="1.0" encoding="UTF-8"?> <Error> <Code>RequestTimeTooSkewed</Code> <Message>The difference between the request time and the current time is too large.</Message> <RequestId>5A3C77583373BA19746BB032</RequestId> <HostId>xxx.oss-cn-beijing.aliyuncs.com</HostId> <MaxAllowedSkewMilliseconds>900000</MaxAllowedSkewMilliseconds> <RequestTime>2017-12-22T02:53:44.000Z</RequestTime> <ServerTime>2017-12-22T03:09:12.000Z</ServerTime> </Error>
      

      image.png

      如图,可以出现这种 Message ,已经明确告诉你本地与 OSS 服务端的时间差 >15min 导致。“The difference between the request time and the current time is too large”,出现此问题,一般与以下两个原因有关系:
      1)用户本地的时间与服务端器的时区不一致,要求用户本地是标准的 GMT 或者 UTC 时间。
      2)网络拥堵导致的等待时间过长超过 15min 。
      3)JAVA SDK 参数配置不合理,比如 max connection

      具体处理工作流如下

      image.png

      场景4、OSSFTP 上传到 OSS 时,抓包出现 zeroWindows

      image.png

      1、OSSFTP 是将远端的 OSS 挂载到本地,但操作的文件每次都是发起 HTTP 请求远端 OSS ,所以受到网络和本地 IO 的影响,高敏感的业务是不太适合的。

      2、看数据包中客户端发生的 ZeroWindows (代表 本地协议栈的 cache buffer 出现过满,应用层无法消费掉 buffer 的数据)

      3、通过 ECS 机器查看自己 CPU 、内网、网卡 是否有跑满情况,这种情况负载过高必然回导致慢的情况。

      建议:

      1、由于 OSSFTP 是串行,而且是 FTPCLIET->FTPSERVER->OSS SERVER 两段操作性能无法保证,推荐使用 ossutil ,
      链接:https://help.aliyun.com/document_detail/50452.html?spm=5176.doc31935.6.1032.YMtcGp

      2、ossutil 在上传大文件时可以采用分片多线程的粒度上传,而我们的 ossftp 是不存在分片的。所以还是推荐 ossutil

      未完待续

      ]]>
      当打之年,非你莫属——阿里云 MVP第12期全球发布-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 阿里巴巴董事会主席兼首席执行官张勇认为:如今,5G网络、工业互联网、物联网等网络基础、数据中心等数字基础、人工智能等运算基础,成为必要而普遍的新型基础设施。加快“新基建”进度,不是简单的基础设施建设,而是与产业化应用协调推进,既能增强基建稳增长的传统属性,又可以助推创新和拓展新消费、新制造、新服务。

      未来需要更多拥有创新能力和实战能力的开发者参与到各个行业中,利用“新基建”拓展商业的边界。

      image.png

      阿里云最有价值专家,简称 MVP(Most Valuable Professional),是专注于帮助他人充分了解和使用阿里云的技术实践领袖。近期,我们重磅发布阿里云 MVP 第12期全球名单。 MVP在各行各业里与阿里云一起改变世界,利用新技术打破商业的边界,创造更多新应用、新工具。他们影响着广大的开发者们,成为行业里的弄潮儿!

      曾任58集团技术委员会主席、转转首席架构师的孙玄,多年的大厂经验,在每个周末沉心修炼技术功底,提升演讲能力。认清自我做自己擅长的事情,拉通集团资源打造弹性的开发平台,让转转在2018年世界杯时顺滑的完成了业务高峰。2020年初放弃优厚的收入创建了“奈学教育”,他专注于数字化转型,在技术驱动和技术革新的环境下,新时代技术人才培训教学体系的革新,现在他拥有了新的身份——阿里云 MVP!

      查看特立独行的架构师——孙玄个人专访!

      豆瓣9.0分的Java大神周志明,他的著作《深入理解Java虚拟机》系列总销量超30万册,他喜欢研究“为什么”:新技术背后的思考是什么?新行业背后的逻辑是为什么?他说技术只是工具,重要的是要用开放的心态去拥抱未来。他虽然是主管但不愿脱离一线开发,一直热衷于开源技术。他建议开发者们通过“说出来、写出来、做给别人看”提升自己的技术水平。这就是阿里云 MVP周志明。

      查看职业电竞选手的Java大神路——周志明个人专访

      杨飞现任每日瑜伽技术负责人,他放下跨国公司的从业经验,从零开始从后端服务器转到系统架构。他说创业公司无边界,大厂有体系,工作就是最好的练兵场,无论身份是什么。啃下难啃的技术难题,与团队形成“正反馈”,不断扩张自己的边界,是他自己多次跨越式成长的心得。

      image.png

      他们扎根行业,磨炼着技术的广度和深度;他们纯粹地拥抱新技术,接受着行业的挑战;他们成为管理者,但仍热爱代码;他们不畏挑战,也勇于接受挑战,快速学习新技术大胆验证。三人行必有我师,他们可能少年老成,他们可能已获取功名,成为所在行业的中坚技术力量,成为公司保驾护航的盾牌,成为行业的革新者,引领着行业走向更高的赛道。

      阿里云 MVP就是一群技术领袖,拓展商业的边界,引领行业的变革,普惠更多的行业及开发者,这是阿里云MVP们的信念!

      一群热爱技术、纯粹的技术人,他们利用云计算、大数据、人工智能、区块链等等技术,让行业中的不可能变成了可能。

      ]]>
      【OSS 排查方案-2】CDN+OSS 基础排查工具-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:张医博

      工具说明: CDN+OSS 的服务架构。

      目的:当用户遇到 CDN + OSS 投诉问题后,可以先用此工具测试一下基本的测试指标,初步判断问题故障点,同时可以将脚本结果粘贴给阿里云客服更快定位信息。

      使用方法:sh check_cdn_oss.sh 域名 请求URL OSS公网域名

      Usge: check_cdn_oss.sh www.zhangyb.mobi http://www.zhangyb.mobi/index.html youkou.oss-cn-beijing.aliyuncs.com

      如有需求,可提改进意见,工具会继续完善,谢谢。

      #!/bin/bash
      #-*- coding: utf-8 -*-
      # author: hanli
      # mail: zhang_15127648471@163.com
      # describe: 在 LINUX 端上执行用来检查初步的排障信息。适用于 CDN+OSS 的服务场景
      # Initial variable
      # update: 2018-01-17
      
      DOMAIN=$1
      REQURL=$2
      ENDPOINT=$3
      LGREE="33[32m"
      LRED="33[31m"
      RGREE="33[0m"
      RESULT=""
      CODE=""
      STATUS=""
      CACHE=""
      
      # 检查脚本输入参数
      function Judge(){
                   
        local DOMAINS="www.zhangyb.mobi"
        local REQURLS="http://www.zhangyb.mobi/index.html"
        local ENDPOINTS="youkou.oss-cn-beijing.aliyuncs.com"
          
        if [ $1 -ne 3 ]; then
          echo -e "${LRED}Usge${RGREE}: $0 ${DOMAINS} ${REQURLS} ${ENDPOINTS}" && exit 1
        fi
        CheckEnvoirment
      }   
      
      # 检查本机依赖环境
      function CheckEnvoirment(){
        echo -e "${LGREE}INFO: CHECKING SCRIPTS DENPENDENT ENVIORMENT... ${RGREE}"
        which sed >/dev/null 2>&1 && DealWith sed || echo -e "${LRED}INFO: THE SED CANNOT BE USE, START USE AWK${RGREE}"
        which awk >/dev/null 2>&1 && DealWith awk || echo -e "${LRED}INFO: THE AWK CANNOT BE USE${RGREE}"
        echo -e "${LRED}INFO: UNABLE FIND SCRIPTS DEPENDENT ENVIORMENT!!!${RGREE}" && exit 1
      }
      
      function DealWith(){
      
        if [ "$1" == "sed" ]; then
          OSSURL=$(echo ${REQURL} | sed -r 's/(http://)([^/]*)(/.*)/1'${ENDPOINT}'3/' 2>&1 )
        elif [ "$1" == "awk" ]; then
          OSSURL=http://${ENDPOINT}$(echo ${REQURL} | awk -F"//" '{print $2}' | awk -F "/" '{print "/"$2}' 2>&1)
        else
          echo "${LRED}INFO: GET OSS URL ERROR!!!${RGREE}" && exit 1
        fi
        CheckPing
      }    
      
      # 开始各项检查
      function CheckPing(){
        
        echo -e "${LGREE}INFO: CHECKING NETWORKING...${RGREE}"
        ping www.taobao.com -w 4 -d >/dev/null 2>/dev/null
        [ "$?" != "0" ] && echo -e "${LGREE}INFO: MACHINE NETWORKING FAILURE${RGREE}" && exit 1
      
        echo -e "${LGREE}INFO: NETWORK IS GOOD ${DOMAIN} CHECKING...${RGREE}"
        ping ${DOMAIN} -w 4 -d >/dev/null 2>/dev/null
        [ "$?" != "0" ] && echo -e "${LGREE}INFO: DOMAIN ${DOMAIN} NETWORK FAILURE${RGREE}"
      
        echo -e "${LGREE}INFO: OSS DOMAIN ${ENDPOINT} CHECKING...${RGREE}"
        ping ${ENDPOINT} -w 4 -d >/dev/null 2>/dev/null
        [ "$?" != "0" ] && echo -e "${LGREE}INFO: OSS ${ENDPOINT} NETWORK FAILURE${RGREE}"
      
        Check 
      }
       
      function Check(){
      
        if GetInfo; then
          echo -e "${LGREE}INFO: ${RESULT}${RGREE}"
        fi
      
        if GetDomain; then
          echo -e "${LGREE}INFO: CHECK WORK LEGAL${RGREE}"
        else
          echo -e "${LRED}INFO: DOMAIN SERVICE NOT IN ALIBABA${RGREE}" && exit 1
        fi
      
        if GetCurl; then
          echo -e "${LGREE}INFO: URL HTTP CODE ${STATUS}, CACHE TIME ${CACHE}${RGREE}"
        else
          echo -e "${LRED}INFO: CHECK CDN DOMAIN WORK IS FAILURE${RGREE}"
        fi
      
        if GetOSS; then
          echo -e "${LGREE}INFO: OSS IS WORK GOOD${RGREE}"
        else
          echo -e "${LRED}INFO: OSS IS WORK FAILURE${RGREE}"
        fi
        exit 0
      }
      
      # 测试 CDN 访问连通性
      function GetCurl(){
      
        CODE=$(curl -svo /dev/null ${REQURL} -m 2 2>&1 | egrep -i "HTTP/[1|2].[0|2|1] .*" | fgrep 200)
      
        if [ ! -z "${CODE}" ]; then
          STATUS=$(curl -svo /dev/null ${REQURL} -m 2 2>&1 | grep -i "x-cache" | grep -i hit >/dev/null && echo "HIT" || echo "MISS")
          CACHE=$(curl -svo /dev/null ${REQURL} -m 2 2>&1 | grep -i "X-Swift-CacheTime" | awk '{print $3}')
          return 0
        fi
        return 1
      }
      
      # 测试 OSS 访问连通性
      function GetOSS(){
        local OSSCODE=$(curl -svo /dev/null ${OSSURL} -H "Host: ${DOMAIN}" -m 2 2>&1 | egrep -i "HTTP/[1|2].[0|2|1] .*" | fgrep 200)
        local OSSRESP=$(curl -sv0 /dev/null ${OSSURL} -m 2 2>&1 | egrep -i "HTTP/[1|2].[0|2|1] .*" | fgrep 200)
        [ ! -z "${OSSCODE}" ] || [ ! -z "${OSSRESP}" ] && return 0 || return 1
      }
      
      # 获取本地的 DNS CLIENT IP
      function GetInfo(){
      
        local TIMESTART=$(date +%s)
        local TIMEEND=$(date +%s)
        local URL="https://42-120-74-167-408161718.dns-detect.alicdn.com/api/cdnDetectHttps"
        local METHOD="commitDetectHttps"
        local DETECTID="408161718"
        local JQUERY="jQuery110104439492578113773"
        local INFO=$(curl -v "${URL}?method=${METHOD}&detectId=${DETECTID}&cb=${JQUERY}_${TIMESTART}&_=${TIMEEND}" 2>&1 | xargs | awk '{print $NF}')
      
        if [ ! -z "${INFO}" ]; then
          RESULT=$(echo ${INFO} | egrep -o "ldns:.*,localIp:[^}]*")
          [ ! -z "${RESULT}" ] && return 0 || return 1
        fi
        return 1
      }
      
      # 判断是否有 CNAME 记录,是否在阿里云加速
      function GetDomain(){
      
        local CNAME=$(dig ${DOMAIN} +time=1 2>&1 | fgrep CNAME | awk '{print $NF}' |fgrep kunlun)
        [ ! -z "${CNAME}" ] && return 0 || return 1
      }
      
      
      Judge $#
      ]]>
      算法笔试模拟题精解之“平行线”-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 在线编程介绍

      阿里云开发者社区在线编程产品,针对广大开发者学习、实践、面试、应聘、考试认证等打造的免费在线刷题神器。题库来自笔试模拟题、算法大赛模拟题等,界面整洁明了,操作简单,为用户营造专心答题的学习环境。点击链接开始体验:https://developer.aliyun.com/coding

      本文为大家介绍其中的第72题:平行线 的题目解析,具体如下:

      题目描述

      查看题目:平行线 为了管理动物园不听话的大猩猩们,动物管理员决定去远方的动物之城找一些平行线。
      当他逛到一个神奇的店铺时,他发现了一副黑色的图,上面依稀可见一些白色的点。
      动物管理员询问店铺老板这幅画是什么,老板说:“天机不可泄露”。动物管理员仔细端详了一会这幅画后,他发现其中的一些白点可以连成两条平行线。
      动物管理员问这幅画多少钱,老板说:“原价¥99999999999,但现在你只要计算出来这里面有几对平行线,就可以打折,有几对平行线就价值多少钱”。请你计算出动物管理员最少需要支付多少钱?

      输入一个整数n,表示总共有n个点(1 <= n <= 1000);和一个含有n组数据(xi, yi)的数组,(xi, yi)表示二维平面上一个点(1 <= xi,yi <= 1000),且每个点均不重复。

      输出n个点能够找出几对平行线。(答案不超过int)
      示例1
      输入:
      6
      [[0,0],[1,0],[1,1],[3,1],[3,3],[5,4]]
      输出:
      10

      解题思路:排序问题

      任意两点确定一条直线。判断两条直线是否平行就是比较两条直线的斜率。这道题对于由不同的点确定的但是重合在一起的直线应该也判断为一组平行线。
      一共有n个点,就有n*(n-1)/2条直线。对于点i和j确定的直线斜率为(yi-yj)/(xi-xj).
      使用一个数组保存斜率,然后排序。
      排序过后相同斜率的直线就在连续的位置了。
      假设斜率为k的直线有a条,对应着有a*(a-1)/2组平行线(任意两条相互平行)。
      遍历一次排序后的数组就可以得到结果。
      使用简单的冒泡排序可能会超时,考虑更快的快速排序,归并排序或者直接使用java的排序函数。
      时间空间复杂度与排序算法有关。

      看完之后是不是有了想法了呢,快来练练手吧>>查看题目
      image.png

      ]]>
      Serverless 解惑——函数计算如何安装字体-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 0745C687-1851-464a-928B-2B0DE9FB5D56.png

      前言

      首先介绍下在本文出现的几个比较重要的概念:

      函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考
      buried_point
      Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考

      备注: 本文介绍的技巧需要 Fun 版本大于等于 3.6.7。

      函数计算运行环境中内置一些常用字体,但仍不满足部分用户的需求。如果应用中需要使用其它字体,需要走很多弯路。本文将介绍如何通过 Fun 工具将自定义字体部署到函数计算,并正确的在应用中被引用。

      你需要做什么?

      1. 在代码(CodeUri)目录新建一个 fonts 目录
      2. 将字体复制到 fonts 目录
      3. 使用 fun deploy 进行部署

      工具安装

      建议直接从这里下载二进制可执行程序,解压后即可直接使用。下载地址

      执行 fun --version 检查 Fun 是否安装成功。

      $ fun --version
      3.7.0

      示例

      demo 涉及的代码,托管在 github 上。项目目录结构如下:

      $ tree -L -a 1
      
      ├── index.js
      ├── package.json
      └── template.yml

      index.js 中代码:

      'use strict';
      
      var fontList = require('font-list')
      
      module.exports.handler = async function (request, response, context) {
          response.setStatusCode(200);
          response.setHeader('content-type', 'application/json');
          response.send(JSON.stringify(await fontList.getFonts(), null, 4));
      };

      index.js 中借助 node 包 font-list 列出系统上可用的字体。

      template.yml:

      ROSTemplateFormatVersion: '2015-09-01'
      Transform: 'Aliyun::Serverless-2018-04-03'
      Resources:
        fonts-service: # 服务名
          Type: 'Aliyun::Serverless::Service'
          Properties:
            Description: fonts example
          fonts-function: # 函数名
            Type: 'Aliyun::Serverless::Function'
            Properties:
              Handler: index.handler
              Runtime: nodejs8
              CodeUri: ./
              InstanceConcurrency: 10
            Events:
              http-test:
                Type: HTTP
                Properties:
                  AuthType: ANONYMOUS
                  Methods:
                    - GET
                    - POST
                    - PUT
      
        tmp_domain: # 临时域名
          Type: 'Aliyun::Serverless::CustomDomain'
          Properties:
            DomainName: Auto
            Protocol: HTTP
            RouteConfig:
              Routes:
                /:
                  ServiceName: fonts-service
                  FunctionName: fonts-function

      template.yml 中定义了名为 fonts-service 的服务,此服务下定义一个名为 fonts-function 的 http trigger 函数。tmp_domain 中配置自定义域名中路径(/)与函数(fonts-service/fonts-function)的映射关系。

      1. 下载字体

      你可以通过这里下载自定义字体 Hack,然后将复制字体到 fonts 目录。
      此时 demo 目录结构如下:

      $ tree -L 2 -a
      
      ├── fonts(+)
      │   ├── Hack-Bold.ttf
      │   ├── Hack-BoldItalic.ttf
      │   ├── Hack-Italic.ttf
      │   └── Hack-Regular.ttf
      ├── index.js
      ├── package.json
      └── template.yml

      2. 安装依赖

      $ npm install

      3. 部署到函数计算

      可以通过 fun deploy 直接发布到远端。
      image.png

      4. 预览线上效果

      fun deploy 部署过程中,会为此函数生成有时效性的临时域名:
      image.png

      打开浏览器,输入临时域名并回车:

      image.png


      可以看到字体 Hack 已生效!!!

      原理介绍:

      1. fun deploy 时,如果检测到 CodeUri 下面有 fonts 目录,则为用户在 CodeUri 目录生成一个 .fonts.conf 配置文件。在该配置中,相比于原来的 /etc/fonts/fonts.conf 配置,添加了 /code/fonts 作为字体目录。
      2. 自动在 template.yml 中添加环境变量,FONTCONFIG_FILE = /code/.fonts.conf,这样在函数运行时就可以正确的读取到自定义字体目录。

      如果依赖过大,超过函数计算的限制(50M)则:

      1. 将 fonts 目录添加到 .nas.yml
      2. 将 fonts 对 nas 的映射目录追加到 .fonts.conf 配置

      fun deploy 对大依赖的支持可参考《开发函数计算的正确姿势——轻松解决大依赖部署》

      总结

      你只需要在代码(CodeUri)目录新建一个 fonts 目录,然后复制所有字体到该目录即可。Fun 会自动帮你处理配置文件(.fonts.conf),环境变量以及大依赖场景的情况。如果大家在使用 Fun 的过程中遇到了一些问题,可以在 github 上提 issue,或者加入我们的钉钉群 11721331 进行反馈。

      阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术圈。”

      ]]>
      RPA没死,只是更专注于设计提供最高级体验的流程。-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800 确切地说,什么是“死亡”,当涉及到过程软件和企业自动化时,什么是生命的迹象?

      “死亡”的RPA是定义不清的“RPA”,它被大肆炒作,为投资者带来刺激增长。但是准确的来说,其实它只是桌面自动化与pure-RPA(无人值守的后台办公室)的混合,目前市场上所有签署的协议都是“有人值守的”,所以甚至都不是“机器人”。

      RPA幸存下来的部分则是流程编排工具(发现、设计、自动化和挖掘),它们构成了像今天我们所看到的像“智能数字工作者”发展的一部分,这些“智能数字工作者”增强了人类的办公能力,并帮助实现了客户与员工之间真正的亲密关系。但是我们要知道,这些应用程序需要依靠企业级的兼容ITIL和安全因素。并且只有当业务设计和IT支持时,才会规模出现。因此未来的赢家必定是聪明的企业,因为他们通常在客户了解自己之前,就利用技术来预测客户的未来。

      距离上次宣布RPA“死亡”已经一年了,那以后又发生了什么?

      距离撰写RPA已死,谈RPA的新出路已经快一年了。2012年首次将RPA引入世界,就引起了不小的轰动。投资者、企业纷纷涌入这个市场,可是虚假的营销、过早的透支技术最后只可能演变成互联网泡沫。当大多数企业都还在埋头苦干试图找到这个市场的经济所在的时候,一批企业已经成功部署,超卖出了一大批license。

      许多优秀的人把他们的职业生涯押在了天花乱坠的宣传、虚假的希望和喧嚣的谎言上,他们仍然在就业市场上努力让自己的生活重回正轨。事实上,整个惨败几乎摧毁了这些工具可以帮助催化的真实市场。然而,这个市场变化很快,将来把很多产品在相同的“市场”试图变化成一个包含如此多的超过基本屏幕抓取,宏和流程循环记录是没有意义的。

      相反,我们需要专注于向智能数字劳动力的发展,帮助我们提供真正的客户和员工体验。

      没有死亡的事实是,RPA创造了一个通向正在发展的更大市场的路径或者说是对话,一旦你真正了解了公司领导的决策和业务流程,RPA就可以发挥出它强大的实力。RPA的发展是一个长期的过程,目前我们只处在马拉松的初期,他还有很长的、令人兴奋的未来可以期待。目前,我们仍有很多问题需要考虑:实现高度自动化、智能数字劳动力有哪些实际可行的方案,我们需要改变什么才可以达到这个目标?我们现在讨论智能数字劳动力是不是还为时尚早?我们是否可以研究一个管理型机器人有效的将旧的流程循环和应用程序连接在一起?以及企业该如何投资于帮助实现更多智能交互和体验的智能工作者? 我们能否专注于智能数字工作者,从我们设计的流程中传递(并学习)最优秀的经验,将我们的客户和员工聚集在一起?

      智能数字劳动力的出现是开发OneOffice体验的关键组成部分,RPA奠定了基础。

      这种向智能数字体验的转变是阿里云RPA的员工体验(EX)和客户体验(CX) OneOffice体验的一个基本元素。CX将越来越多地被认为是与整个组织(无论是客户、合作伙伴、员工还是任何其他实体)交互的经验的总称。前文化是指人们在一起工作,从事务性的互动转变为更深层次的关系。组织需要确保他们得到正确的平衡;这包括优化使用新兴技术和强大的业务案例,以提高CX的业务的长期利益,获得正确的信息流动,激发战略优势,并确保特殊的CX:
      image.png
      OneOffice体验代表了如何将客户、合作伙伴和员工的体验融合在一起,以驱动统一的思维方式、目标和业务成果。OneOffice将以客户为中心的体验通过我们过去称为前端和后台办公室的端到端流程进行设计和支持概念化。如今的RPA机器人在实质上已经嵌入“数字底线”,成为了构建企业基础处理层的一部分,而创造出更加智能的,可以增强人来办公能力的工具正是RPA未来的发展趋势。当今的软件供应商能够开发出超越当前静态工具的机器人,可是这些静态工具实际上只是保持旧进程的分块运行。智能数字劳动力正在成为一种使能技术,它正逐渐成为开发CX设计和交付经验的关键组成部分。

      HFS强调了使用智能数字工作者的五个重要原则,所有希望实现这些解决方案的公司都需要考虑这些原则:
      image.png
      最后:智能数字工作者是将客户和员工的体验联系起来以驱动统一的思维方式、目标和业务成果的强大工具。如果RPA供应商想要在市场中找到自己的优势,他们就需要达到这个目标。

      ]]>
      【CDN 常见问题】CDN 接入配置及常见问题-阿里云开发者社区 Wed, 08 Apr 2020 03:03:11 +0800

      作者:烨烁

      CDN 顺利接入线上系统的域名是每个客户头疼的问题,本文档通过详述 CDN 接入配置各步骤配置以及每部设置常遇到的疑惑点进行解惑,让客户可以将顺利迁移至 CDN 提供加速服务。

      产品文档参考:CDN开通产品文档

      一、配置流程及概念详解

      在正常开通 CDN 服务后即可进入控制台使用,按照产品文档流程,我们可以依次执行“添加加速域名”->“配置 CNAME 解析”两个步骤进行接入。下面我们就依次详述两个过程。

      在点击“添加域名”按钮后控制台将进入详细加速域名的配置页面,如图1所示。其中包括多项影响 CDN 使用情况的配置项,因此这些设置对于后续的 CDN 使用至关重要。下面我们队每项配置详细说明。
      image.png

      图 1. CDN 配置加速域名示意图

      1)加速域名:表示该域名即为 CDN 的加速域名,也就是说真正在业务系统中提供给线上客户使用的域名,后续所有需要 CDN 加速访问的资源即通过该域名对外提供服务。而不能使用一个“假想域名”配置。同时这里支持泛域名加速,如果用户有多个子域名源站为统一服务器 IP 希望能够同时设置时可以配置类似于 *.aliyun.com 的域名,这样即可对 aliyun.com 下的所有的二级域名提供加速服务。更多泛域名注意事项请参考:CDN泛域名注意事项
      “假想域名”。常有客户真正提供服务的域名是 www.aliyun.com,但是在这里添加的加速域名却是 cdn.aliyun.com ,那么后续业务系统中继续使用 www.aliyun.com 域名将不会起到 CDN 加速效果。
      泛域名范围。泛域名是有严格的范围限定的。举例来说,当配置加速域名为 .aliyun.com 时,该加速域名对应的是 aliyun.com 主域名下的所有二级域名。因此即该域名既不能给 aliyun.com 主域名提供服务,也不能给类似于 test2.test1.aliyun.com 这样的三级域名提供服务的。因此使用域名与该加速域名不相匹配是需要在创建对应的加速域名的。例如 aliyun.com 只可以设置主域名为加速域名,而test2.test1.aliyun.com 可以使用 .test1.aliyun.com 。

      2)业务类型 。现在CDN提供图片小文件、大文件下载、视音频点播、直播流媒体、移动加速和全站加速几种业务类型。业务类型一经设置将无法修改。用户可以根据自己的主要加速的资源和业务场景选择合适的业务类型。很多用户对自己需要选择的业务类型产生疑虑,这里对几种业务类型的区别做详细描述。
      首先 全站加速 即为动态加速,正常CDN主要通过缓存机制加速静态资源的GET请求,而对于动态请求CDN是不做加速的,这就导致对于动态站点或者大量POST请求的站点无法使用CDN,而全站加速即是针对于这类站点提供服务,其通过最优链路算法及协议层优化动态请求回源链路,实现快速回源获取最新数据。
      直播流媒体 表示该域名用来做直播业务,其接受rtmp协议的推流到视频中心,并提供过rtmp、flv和hls几种协议分别提供在线播放的功能,该业务类型除了常规CDN支持的http协议外还支持rtmp直播流协议。
      移动加速 是CDN推出的针对于移动应用的动静态全网加速产品,其也可以同时实现动静资源加速,相比于全站加速来讲本业务类型主要应用于Android或者IOS的APP上,并且其需要对APP做一定的改造以集成移动加速的SDK。
      剩下的三种业务类型( 图片小文件、大文件下载、视音频点播 )是很多客户困惑的点。这三种业务类型均是针对于静态资源的GET请求加速,这三种业务类型均可以对所有的静态文件实现加速功能(并不是图片小文件业务类型仅加速图片,其同样可以加速大文件或者视频;同时图片小文件以及大文件下载也可以使用视音频的拖拽播放功能),并且其缓存规则也是完全一致的;这三种业务类型主要CDN针对于不同类型的文件做了不同的TCP协议栈的优化策略。因此用户结合自己的业务场景的资源进行选择即可。如果主要资源为图片文件、样式文件、js文件等可以使用图片小文件类型、如果主要资源是apk包、程序安装文件等可以选择大文件下载类型,而如果主要资源是视频资源提供在线点播播放的话则可以使用视音频点播类型。

      3)源站类型和源站地址 :表示CDN回源到源站服务器的类型及其地址。现在支持IP、源站域名、OSS源站、直播中心服务器几种类型。下面我们详细对几种类型进行详细描述。
      IP,顾名思义即是源站服务器的IP。这里特别需要注意的是CDN的源站不管是在阿里云上的ECS、SLB等服务、其他的云服务提供商的服务还是IDC机房的物理机,其回源都是走的公网链路回源的。因此这里填写的IP必须是公网IP,否则将导致CDN回源出现5XX的错误。同时这里是可以设置源站为IP的,并且可以根据该IP对应服务器能够承载的压力设置其优先级。关于多源优先级的说明请参考多源优先级设置。这里需要特别注意的一点是当CDN设置为多源时,用户是需要自行保证每台服务器上对应的资源均是完整并且同步的。常有客户误解这里的多源表示我在主服务器上查找某资源发现不存在返回404的错误,然后CDN会自动去备服务器上查找该资源,CDN是不会进行该操作的。只要某一条服务器四层健康检查通过返回任何的状态码(2XX、3XX、4XX或者5XX)都会直接返回给客户端的。
      域名,表示解析在源站服务器上的域名。其在CDN上的作用是会将该域名解析为IP,然后按照与上述的IP回源一致到该服务器上获取资源。因此域名其实与IP基本类似,设置为域名的场景主要是用户的源站服务器IP经常会发生变动,而为了避免源站服务器IP变动导致CDN回源异常可以这里设置为域名,这样用户仅需要保证该域名可以正常解析即可,并不需要再修改CDN的配置。另外这里的域名仅仅是用来解析成IP的,源站服务器上如果有多个站点需要哪个站点提供服务并不是根据该设置决定的,而是根据回源host设置决定的。
      OSS域名,CDN加速OSS是常见的使用场景,其使用方法及常见问题请参考【 CDN 最佳实践】CDN 加速 OSS 常见问题及处理思路。如果需要回源到本账号中的OSS可以这里直接选择对应bucket的公网域名即可。但是如果需要回源到其他账号的bucket中的话是需要设置源站类型为域名,然后自行填写该bucket的公网域名。同时请注意OSS域名类型是不支持多源设置的。
      最后一种直播中心服务器是当选择直播流媒体业务类型系统自动配置的,并不需要用户自己配置。
      L1和L2节点网络。CDN的节点是分两层架构的(CDN架构请参考CDN基础架构),其中L1是靠近客户端的,会根据客户端的local dns分配就近的同一运营商的节点,因此L1节点是区分具体运营商的,而L2是靠近源站服务器端的节点,为了保证CDN回源不会受到网络隔离导致回源异常,因此CDN的L2的节点均是BGP网络类型的,用户上述配置的源站服务器是不需要担心自己的服务器是单线服务器导致CDN回源异常的。
      健康检查。在将服务器IP加在CDN上后,CDN是会定期做健康检查,主要包括四层的网络监察,因此用户的源站服务器日志可能会记录到CDN的健康检查的日志的。当设置为多源回源时当某台服务器健康检查失败即暂时不会回源到该服务器,而选择其他的服务器回源,避免由于源站单台服务器异常导致线上异常。

      4)端口 :表示CDN回源时与源站的哪个端口获取数据。CDN支持的回源端口包括80和443,分别对应HTTP协议和HTTPS协议。这里如果设置为443端口时用户是需要保证源站服务器对应端口的server是配置好SSL证书的。

      5)加速区域 :对于L3以上的客户是可以选择CDN的海外加速的,客户是可以根据自己的服务对象选择对应的CDN的加速区域,现在可以选择中国大陆、全球加速和港澳台及海外三种类型。如果这里选择的是中国大陆,那么该域名的调度节点是仅有中国大陆内的L1和L2节点。那么这个时候海外的客户也是会调度到大陆的节点,其访问就有可能受国际链路波动影响导致访问异常的。

      二、配置CNAME解析

      在配置完成CDN加速域名后用户即可得到对应的CNAME域名,如图2所示即是对应的CNAME域名。用户接下来即可配置加速域名对应的CNAME解析后即可。
      image.png

      图 2. CDN CNAME域名示意图

      详细的CNAME解析设置请参考CDN域名CNAME解析设置,这里需要特别注意的是主机记录必须与CDN添加的加速域名一致(泛域名需满足统一级别)。
      DNS解析限制同一个主机记录只能够保留一个A记录或者CNAME记录,否则添加将会导致冲突。因此想要添加CDN是需要删除之前解析到源站服务器的A记录并添加对应的CNAME记录,等TTL时间后才可以生效的。
      为了保证切换CDN后不影响业务,建议先通过绑定hosts的方式先本地测试添加CDN后正常再正式修改DNS记录。不同系统有不同绑定hosts的方法(例如Mac、linux系统中可以使用/etc/hosts文件设置)。具体设置方法步骤为:

      • ping CDN提供的CNAME地址得到CDN节点的IP,如图3所示。
        image.png

      图 3. CDN 节点IP获取示意图

      • 修改/etc/hosts文件将该域名设置解析到该IP上,如图4所示。
        image.png

      图 4. 绑定hosts配置示意图

      • 然后就可以在浏览器中实际访问测试是否正常。

      三、验证CDN生效情况

      在配置完成dns解析后可以通过多种方法验证CDN是否生效。包括以下三种方法:

      • 通过ping你所添加的加速域名,如果被转向.kunlun*.com的域名,即表示CDN功能已生效,如图5所示。
        image.png

      图 5. 通过ping验证解析生效示意图

      • 通过 nslookup或dig命令,可以查看相应的加速域名访问CDN节点的IP和延时丢包等基本信息。用户可以根据解析出来的IP在CDN的控制台上的IP检测工具查看是否为CDN节点的IP,是的话就说明CDN已生效:
        image.png

      图 6. CDN检查IP是否为CDN节点IP示意图

      • 用户也可以获取对应加速域名的资源的response头查看是否有CDN加速对应的节点信息来判断CDN是否生效,如图7所示,如果response头中包括Via、X-Cache等头就表明其已经CDN加速。
        image.png

      图 7. CDN查看response头示意图

      ]]>
      DataWorks百问百答08:如何进行任务调度执行、周期实例运行状况分析? Wed, 08 Apr 2020 03:03:11 +0800
      在datastidio数据开发界面中配置好任务并发布后,便可以在运维中心查看任务的依赖关系图示、实例的运行状态以及实例的上下游关系。打开运维中心,可以看到如下的界面:



      Q1:周期任务和周期实例是什么关系?
      A1:任务产生实例。任务(即数据开发中的"节点")是起源点,实例是根据任务以及任务配置情况定时生成的执行任务的载体(暂且这么理解)。
      注1:实例为固定时间一次性生成。 生成实例时间:当地时间23:30-24点(生成第二天的所有实例)
       
      Q2:实例生成方式( T+1次日生成/即时生成 )有什么区别?

      A2:T+1次日生成指的是实例生成延迟一天,例如今天下午5点发布的任务,选了T+1则今天夜里23:30生成明天的的实例,明天该任务可正常调度。
           立即生成指的是任务发布后立刻生成实例,比如说今天下午5点发布的任务,选了立即生成实例,那么发布后就能在运维中心--周期实例里面看到该实例了。
       
      注1:小时任务一次性生成第二天的所有实例(大多为24个),分钟级任务亦然。月、周调度每天生成一个,调度日执行,其他时间该实例为空跑
       
      注:为什么我的任务选了T+1生成实例第二天没有生成实例?为什么配置任务时选的即时生成实例但是实际并没有生成实例
      =》
      这是因为,发布任务的时间点和后台生成实例的时间点(23:30)冲突了。如果在该时段内发布任务,则影响该任务的实例的正常生成(任务实例会在第三天生成),发布任务时间尽量避过该时段。
       
      Q3:什么是空跑? 除了月/周任务,小时、分钟和天任务为什么会空跑?**



      A3:空跑即已经过了当日该实例的定时调度时间所造成的实例运行成功,执行时间为0且没有日志信息的情况
      举个栗子:小时任务,一天一次性生成24个实例,从0点到23点,这些实例是一次性生成的,但是,发布时间是当天14:40,那么14点40之前的这些实例都属于空跑调度,因为前面的时间都已经过去了,所以实例是空跑调度。而从15点开始,实例则会正常调度运行。
      分钟、天任务也是一样的道理,比如说定时时间是0点10分,但是实际任务发布时间是11点,那么这个节点当天的实例也会是空跑调度,因为0点10分的定时时间已过。第二天新实例会正常调度。
      周/月任务,由于是每天都会产生一个实例,那么如果周/月任务仍然有下游,为了不阻塞下游任务,在非调度时间里亦将其置位成功(空跑)状态以不阻塞下游任务运行。
       
      Q4:实例正常产生为什么没有调度运行起来?处于黄色/灰色状态?
      A4:实例运行状态主要分为四种:
      1.绿色==>正常运行结束(包括空跑) 
      2.红色==》出错 
      3.黄色==》等待(没到定时时间或没有等到执行资源)
      4.灰色==》未运行()
      需要注意的是,节点是否能运行由两个主要因素决定:
      1.该节点依赖的所有上游实例都已运行完毕。
      2.该节点的定时执行时间已经到达(如未到该实例指定的调度时间,则该实例处于黄色等待状态)。

      注:如果实例处于灰色未运行状态,请检查上游任务运行状况,是否有上游实例出错/未运行/等待,逐级排查。该问题一般都是由于上游未运行/阻塞导致的。
       
      Q5:任务延迟严重?
      A5:任务实例的实际运行时间是受上游影响的。比如说你依赖了10个上游任务,本节点定时时间是3月4号0点,但是上游的10个任务最晚的结束时间是3月5号11点,那么你的这个实例虽然定时执行的时间设定是3月4号0点,由于上游结束的晚该实例也会推迟到5号11点之后执行,等所有上有都运行完毕了才会开始运行。**
       

      DataWorks百问百答历史记录请点击查看

      采购季限时!原价2500元现仅需99元,3分钟入门DataWorks标准版6大场景!点击查看

      更多DataWorks技术和产品信息,欢迎加入【DataWorks钉钉交流群】

      ]]>
      阿里云达摩院资深算法专家浅谈:视觉生产技术探索及应用 Wed, 08 Apr 2020 03:03:11 +0800   何为视觉生产?
        在介绍视觉生产之前我们需要给它进行定义,到底什么是视觉生产。简单来说视觉生产就是通过一个/一系列视觉过程,产出新的视觉表达。这里的产出是指人或机器能够感知的图像视频,而不是标签或者特征并且必须是新的视觉表达,和输入的不一样。在过去,这个过程大多数是由人工来实现,但是现在我们希望通过AI技术,来产生一系列新的图像,本篇文章主要介绍的也是这一过程。
        总体来说视觉生产是有分类的,主要分为以下几个分类:生成、拓展、摘要以及升维,生成就是从零到一从无到有的过程,拓展是指1到N的过程,摘要则是和拓展相反,是N到1的过程,将内容浓缩起来,把最主要的信息找出来。而升维就比较特殊,打个比方图像是2d的形式,但是如果加了时间轴就变成了动态的了,就变成了二维+t,这样就是从二维升到三维,这一过程称之为升维。除此以外对图片的增、删、改、查的过程也属于视觉生产范畴。这些视觉生产相关的内容其实我们也都有在做,也上线了一些产品,例如鹿班、alibabawood、画蝶,以及我们近期上线的视觉智能开放平台,这些产品都是聚焦在视觉生产上面的,后面也会跟大家详细介绍。
      图1.png

      图1

        另外视觉生产它也有一个基本的通用框架,具体内容可看图2,这里面要着重介绍的是视觉生产引擎部分,因为它是整个框架里面最为核心的部分。视觉生成引擎一般分成两大类,一种是生产引擎,基于相关模型去真正的生成一些内容,类似从无到有的一个过程。当然了,还有一种是搜索引擎,解决的思路就是我生产很困难,那么我去找到或者搜索到一个和我要求一致或者类似的素材然后在对他进行一定的改变,从而得到一个新的素材。对于产出的素材我们也会遵循一定的规范,一般会从可看、合理、多样、可控以及可用这几个维度去判断,其中是否可用是从能否为产品或者平台带来用户以及它的商业价值的角度进行判断。通过这5个维度能保证我们的视觉生产它可以有一个比较好的结果,或者说比较合理的一个结果。
      图2.png

      图2

        想要生产出一份优质的素材,首先你需要去精细的理解它,因为唯有理解才能生成。理解的阶段大致分为识别、检测以及分割,最终的目的是知道每一个像素是什么。做分割,事实上这也是学术界和工业界研究很久的任务,但事实上要想把它做好还是非常不容易的。因为分割的话,它会有复杂的背景、各种各样的遮挡关系以及某些特定场景下特别高的要求。所以在图片分割上我们进行了深入的研究并制定了相关的模型框架如图3所示,第一个步就是进行一个mask粗分割,然后我们对这些数据质量非常高标注效果非常好的进行一个精分割的网络,再将他们结合起来,这样做的好处是他能够像粗分割那样获取数据,并且结合精确的、细腻的高质量的数据得到一个结果,我们也将这个模型发表到CVPR2020上面,大家有兴趣可以去看看。
      图3.png

      图3

        视觉生成技术的应用
        那介绍完视觉生成技术,接下来我们给大家介绍3个视觉生成技术的应用案例。像文章开头时候说的鹿班(https://luban.aliyun.com)就是我们早些时候针对平面图像做的一个设计,也算是整个业界中第一个大规模落地的在线AI服务。除了鹿班类的平面图像生产外,针对视频我们同样制作了一款工具,名字叫做AlibabaWOOD(https://alibabawood.aliyun.com),它就专注于短视频的生成,目前也是一个云上的公共产品。详细的平台介绍可以点击产品官网链接进行查看。
      图4.png

      图4

        视频既然可以通过视觉生产去制作,我们也在思考通过视觉生产能否对现有的视频进行处理呢?答案是可以的。
        我这里举个例子,如图5红线框的位置它本来没有东西,但是为了达到广告投放效果又不影响观看者的观看体验,我们通过视觉生产的方式在视频中插入了一个广告,从而实广告和内容融合在一起。
      图5.png

      图5

        前面说了这么多视觉生产技术,其实我们已经将这些技术形成的算法沉淀在了统一的平台上。阿里云视觉智能开放平台(https://vision.aliyun.com)截止现在,这个平台已经上线了100+的AI算法,这些算命法主要是通过API的方式实现调用。平台目前处于公测期,所有的AI算法都是免费开放的,也欢迎广大开发者前来调用,创建更多有价值的产品和解决方案!
      图6.png

      图6

        以上内容只是星瞳此次分享的一部分,由于篇幅原因,其他内容就不在这里展示了,感兴趣的同学可以点击下方的视频链接进行观看。如果在观看期间有视觉相关的疑问,都可通过钉钉搜索23109592进群和我们沟通。
      直播回看链接:https://edu.csdn.net/course/play/28249/388355

      ]]>
      创造快乐工作的Ant Design 4.0 Wed, 08 Apr 2020 03:03:11 +0800 一个开源项目的成活,除了维护者的保持更新外,也需要来自社区的力量。开源界中,你会看到非常多的优秀项目。但是随着时间推移,逐渐不再维护。因而如何保持项目的活力是重中之重。2015 年,Ant Design 发布第一个版本,到现在时间过去整整 5 年了,第一代 Ant Designers 陆续退出舞台中心,第二代、第三代 Ant Designers 正在扛起大旗,如自然中的万事万物,有发展、有死亡、更有进化。

      Ant Design 是蚂蚁金服推出的一套企业级 UI 设计语言和 React 组件库,从 2015 年推出开始便受到广泛的关注与使用,目前在 GitHub 上已收获超过 5.7 万个 star。

      近日,Ant Design 发布了 4.0 版本,带来了重大更新,继续传承着 Ant Designer 的精神。这是两年多以来的首次发布的里程碑版本。新版本中进行了多项改进,包括:

      • 设计规范升级。增加了暗色主题和无边框组件。
      • 兼容性调整。最低支持IE 11,依赖的React最低版本升级到16.9。
      • 更小的尺寸。调整了图标使用API以支持tree shaking,并对相关依赖进行精简。
      • 组件重做。包括Table、Form、DatePicker等组件进行重做。

      Ant Design认为“每个人都追求快乐工作”,是全情投入的快乐,也是成长的快乐。因为每个人,都曾经有快乐工作的经历。只要让挑战持续匹配技能,就能创造连续的快乐工作。如果我们想要一个快乐的人生,那么快乐工作,对我们每个人都意义重大。基于“每个人都追求快乐工作”的追求,Ant Design从用户和设计者的角度去带着产品和服务让其感受快感和乐趣。让每个 Ant Designer 都有一颗心,一张图,打好一场仗。

      此时的 Ant Design ,已经远不止是一个 UI 组件库。而是可视化资产(AntV)、插画资产(海兔),以及体验设计、增长设计、品牌设计等各种策略;同时,在可预见的未来,也将会涉及工业设计、运营设计等工作范畴。

      新版Ant Design 4.0的整体设计思路是什么?在图表自动生成的用法和原理上又有哪些突破?4月2日,走进阿里云云栖号在线课堂,蚂蚁金服数据技术专家王嘉喆和高级体验设计师徐海豪将针对以上问题进行详细介绍。扫描下方二维码即可进入直播间~

      ]]>
      使用米芯智能模块快速、低成本、标准化接入阿里云生活物联网(飞燕)开发生活类智能家电 Wed, 08 Apr 2020 03:03:11 +0800 1.写在开头

      智能这个词在家电领域用的很多,大家肯定不陌生。很多所谓的智能家电大部分指内置单片机控制的家电。 而本文要说的智能家电指接入物联网云端,通过云端可以与手机端、AI音箱端互通信息的家电。 阅读本文您会发现实用米芯智能模块开发智能家电会如此简单。

      为了更好的阅读本文,您需要基本了解阿里生活物联网及平台操作。因涉及内容比较多,本文只简述重点。

      2.米芯智能模块介绍

      米芯智能模块是米芯信息团队专为电子、单片机工程师使用而设计,不仅仅是一个硬件模块而是一种设备硬件接入阿里云生活物联网的解决方案。米芯智能模块基于米芯框架开发,内置米芯固件,支持米芯串口通讯协议。米芯智能模块大大降低了设备接入云端的复杂度。
      了解详情 点击这里

      3.实战案例

      下面通过开发一个智能 电风扇 demo 作为案例。
      使用米芯智能模块快速开发物联网智能电风扇。实现 电风扇(设备)接入阿里云生活物联网(飞燕)。手机APP、天猫精灵音箱与电风扇联动及控制。

      3.1 物料清单

      • 普通电风扇 1台
      • demo控制板 1块
      • 米芯智能模块 1个
      • 安卓智能手机 1个
      • 天猫精灵音箱 1个

      3.2 设备端开发

      我们使用一把普通的电风扇作为原型机,在此基础上开发一个智能电风扇的demo。
      普通电风扇功能:开关、3个风速档位;

      1400015786.jpg

      3.2.1 更换电风扇控制板

      普通电风扇内置的控制板需要替换为demo控制板。

      808930180.jpg
      demo控制板有3个继电器 正好可以实现 3个风速档位的控制(即对应普通电风扇的电机的3个输入)

      2011533227.jpg
      将原控制板连接的电缆拔出插入demo控制板对应插座上。
      电缆连接后的样子如下:
      1873054452.jpg
      (亮灯的为按键板,代替原电风扇上面的操作按键)
      我们手上的这块demo控制板上面有一个4P串口插座并且控制板上的8位单片机已经对接好米芯串口通讯协议。
      将米芯智能模块串口与demo控制板串口电缆连接。
      1966423224.jpg

      至此demo设备端已经开发好;

      3.3 云端开发

      阿里云生活物联网(飞燕)是在阿里云IoT基础(laaS)上搭建的PaaS,我们只需要在PaaS上定义不同的产品、参数即可使用。

      3.1 创建产品

      20200401112922.jpg

      注意: 数据格式选择 “透传/自定义”, 后面有用。

      3.1 功能定义

      根据demo设备的功能,定义 “电源开关”、“风速” 属性, 其他的属性不需要。
      20200401113317.jpg

      3.2 数据解析脚本

      米芯模块的设备功能会透传到云端,在云端数据解析转为标准的alink协议。
      数据解析脚本有米芯工具根据产品功能定义自动生成。
      20200401115324.jpg

      3.3 人机交互设置

      在阿里云生活物联网平台提供的模板的基础上,我们根据demo的功能制作控制面板。
      20200401115951.jpg

      至此云端开发完成。

      4 手机APP

      对于智能电风扇demo,云平台提供的公版APP已经基本满足需求。 我们直接使用公版APP作为手机控制端及设备配网使用。

      产品没发布前,调试阶段需要使用 开发板APP。

      4.2 设备进入配网模式

      长按按键板上的配网键,使米芯模块进入配网模式

      控制板上的MCU向米芯模块发送重置配网指令:55 AA 01 04 00 00 04

      4.1 通过手机APP给设备配网

      公版APP扫描 刚才创建的产品的配网二维码,按提示完成配网。

      4.2 手机APP控制设备:

      配网成功,设备成功连接到云端后,即可通过手机APP控制demo设备。
      707862060 (1).jpg

      5 天猫精灵控制

      生活网平台的电风扇品类已经与天猫精灵云平台打通。因此可以直接使用天猫精灵音箱语音指令控制demo。

      057501015E8429FA5A50FC941E6B1803.jpg

      我拍了一个天猫精灵控制电风扇demo的视频并上传到了优酷。
      视频地址

      6 写在最后

      因篇幅及文笔水平有限,本文的文字和图片可能存在错误之处还请读者多包涵。
      欢迎一起探讨学习家电物联网智能化;

      481682353 (2).jpg

      ]]>
      MySQL:互联网公司常用分库分表方案汇总 Wed, 08 Apr 2020 03:03:11 +0800 来源:cnblogs.com/littlecharacter/p/9342129.html

      一、数据库瓶颈

      不管是IO瓶颈,还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载活跃连接数的阈值。在业务Service来看就是,可用数据库连接少甚至无连接可用。接下来就可以想象了吧(并发量、吞吐量、崩溃)。

      1、IO瓶颈

      第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度 -> 分库和垂直分表。

      第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 -> 分库。

      2、CPU瓶颈

      第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,在业务Service层进行业务计算。

      第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 -> 水平分表。

      二、分库分表

      1、水平分库

      image.png

      概念:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。

      结果:

      • 每个库的结构都一样;
      • 每个库的数据都不一样,没有交集;
      • 所有库的并集是全量数据;

      场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。

      分析:库多了,io和cpu的压力自然可以成倍缓解。

      2、水平分表

      image.png

      概念:以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。

      结果:

      • 每个表的结构都一样;
      • 每个表的数据都不一样,没有交集;
      • 所有表的并集是全量数据;

      场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。推荐:一次SQL查询优化原理分析

      分析:表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。

      3、垂直分库

      image.png

      概念:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。

      结果:

      • 每个库的结构都不一样;
      • 每个库的数据也不一样,没有交集;
      • 所有库的并集是全量数据;

      场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。

      分析:到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。

      4、垂直分表

      image.png

      概念:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。

      结果:

      • 每个表的结构都不一样;
      • 每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;
      • 所有表的并集是全量数据;

      场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。

      分析:可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。

      但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。

      三、分库分表工具

      • sharding-sphere:jar,前身是sharding-jdbc;
      • TDDL:jar,Taobao Distribute Data Layer;
      • Mycat:中间件。

      注:工具的利弊,请自行调研,官网和社区优先。

      四、分库分表步骤

      根据容量(当前容量和增长量)评估分库或分表个数 -> 选key(均匀)-> 分表规则(hash或range等)-> 执行(一般双写)-> 扩容问题(尽量减少数据的移动)。

      扩展:MySQL:分库分表与分区的区别和思考

      五、分库分表问题

      1、非partition key的查询问题

      基于水平分库分表,拆分策略为常用的hash法。

      端上除了partition key只有一个非partition key作为条件查询

      映射法

      image.png

      基因法

      image.png

      注:写入时,基因法生成user_id,如图。关于xbit基因,例如要分8张表,23=8,故x取3,即3bit基因。根据user_id查询时可直接取模路由到对应的分库或分表。

      根据user_name查询时,先通过user_name_code生成函数生成user_name_code再对其取模路由到对应的分库或分表。id生成常用snowflake算法。

      端上除了partition key不止一个非partition key作为条件查询

      映射法

      image.png

      冗余法
      image.png

      注:按照order_id或buyer_id查询时路由到db_o_buyer库中,按照seller_id查询时路由到db_o_seller库中。感觉有点本末倒置!有其他好的办法吗?改变技术栈呢?

      后台除了partition key还有各种非partition key组合条件查询

      NoSQL法

      image.png

      冗余法

      image.png

      2、非partition key跨库跨表分页查询问题

      基于水平分库分表,拆分策略为常用的hash法。

      注:用NoSQL法解决(ES等)。

      3、扩容问题

      基于水平分库分表,拆分策略为常用的hash法。

      水平扩容库(升级从库法)
      image.png

      注:扩容是成倍的。

      **水平扩容表(双写迁移法)
      **

      image.png

      • 第一步:(同步双写)修改应用配置和代码,加上双写,部署;
      • 第二步:(同步双写)将老库中的老数据复制到新库中;
      • 第三步:(同步双写)以老库为准校对新库中的老数据;
      • 第四步:(同步双写)修改应用配置和代码,去掉双写,部署;

      注:双写是通用方案。

      六、分库分表总结

      • 分库分表,首先得知道瓶颈在哪里,然后才能合理地拆分(分库还是分表?水平还是垂直?分几个?)。且不可为了分库分表而拆分。
      • 选key很重要,既要考虑到拆分均匀,也要考虑到非partition key的查询。
      • 只要能满足需求,拆分规则越简单越好。

      七、分库分表示例

      示例GitHub地址:https://github.com/littlecharacter4s/study-sharding


      本文转载自公众号: Spark学习技巧
      原文链接:https://mp.weixin.qq.com/s/QZnGCtvU7CvThC47ITQAmA


      阿里巴巴开源大数据技术团队成立Apache Spark中国技术社区,定期推送精彩案例,技术专家直播,问答区近万人Spark技术同学在线提问答疑,只为营造纯粹的Spark氛围,欢迎钉钉扫码加入!
      image.png

      对开源大数据和感兴趣的同学可以加小编微信(下图二维码,备注“进群”)进入技术交流微信群。image.png

      Apache Spark技术交流社区公众号,微信扫一扫关注image.png

      ]]>
      使用米芯智能模块C-8082US快速、低成本、标准化接入阿里云生活物联网(飞燕)开发生活类智能家电 Wed, 08 Apr 2020 03:03:11 +0800 1.写在开头

      智能这个词在家电领域用的很多,大家肯定不陌生。很多所谓的智能家电大部分指内置单片机控制的家电。 而本文要说的智能家电指接入物联网云端,通过云端可以与手机端、AI音箱端互通信息的家电。 阅读本文您会发现实用米芯智能模块开发智能家电会如此简单。

      为了更好的阅读本文,您需要基本了解阿里生活物联网及平台操作。因涉及内容比较多,本文只简述重点。

      2.米芯智能模块介绍

      米芯智能模块是米芯信息团队专为电子、单片机工程师使用而设计,不仅仅是一个硬件模块而是一种设备硬件接入阿里云生活物联网的解决方案。米芯智能模块基于米芯框架开发,内置米芯固件,支持米芯串口通讯协议。米芯智能模块大大降低了设备接入云端的复杂度。
      了解详情 点击这里

      3.实战案例

      下面通过开发一个智能 电风扇 demo 作为案例。
      使用米芯智能模块快速开发物联网智能电风扇。实现 电风扇(设备)接入阿里云生活物联网(飞燕)。手机APP、天猫精灵音箱与电风扇联动及控制。

      3.1 物料清单

      • 普通电风扇 1台
      • demo控制板 1块
      • 米芯智能模块 1个 点击购买
      • 安卓智能手机 1个
      • 天猫精灵音箱 1个

      3.2 设备端开发

      我们使用一把普通的电风扇作为原型机,在此基础上开发一个智能电风扇的demo。
      普通电风扇功能:开关、3个风速档位;

      1400015786.jpg

      3.2.1 更换电风扇控制板

      普通电风扇内置的控制板需要替换为demo控制板。

      808930180.jpg
      demo控制板有3个继电器 正好可以实现 3个风速档位的控制(即对应普通电风扇的电机的3个输入)

      2011533227.jpg
      将原控制板连接的电缆拔出插入demo控制板对应插座上。
      电缆连接后的样子如下:
      1873054452.jpg
      (亮灯的为按键板,代替原电风扇上面的操作按键)
      我们手上的这块demo控制板上面有一个4P串口插座并且控制板上的8位单片机已经对接好米芯串口通讯协议。
      将米芯智能模块串口与demo控制板串口电缆连接。
      1966423224.jpg

      至此demo设备端已经开发好;

      3.3 云端开发

      阿里云生活物联网(飞燕)是在阿里云IoT基础(laaS)上搭建的PaaS,我们只需要在PaaS上定义不同的产品、参数即可使用。

      3.1 创建产品

      20200401112922.jpg

      注意: 数据格式选择 “透传/自定义”, 后面有用。

      3.1 功能定义

      根据demo设备的功能,定义 “电源开关”、“风速” 属性, 其他的属性不需要。
      20200401113317.jpg

      3.2 数据解析脚本

      米芯模块的设备功能会透传到云端,在云端数据解析转为标准的alink协议。
      数据解析脚本有米芯工具根据产品功能定义自动生成。
      20200401115324.jpg

      3.3 人机交互设置

      在阿里云生活物联网平台提供的模板的基础上,我们根据demo的功能制作控制面板。
      20200401115951.jpg

      至此云端开发完成。

      4 手机APP

      对于智能电风扇demo,云平台提供的公版APP已经基本满足需求。 我们直接使用公版APP作为手机控制端及设备配网使用。

      产品没发布前,调试阶段需要使用 开发板APP。

      4.2 设备进入配网模式

      长按按键板上的配网键,使米芯模块进入配网模式

      控制板上的MCU向米芯模块发送重置配网指令:55 AA 01 04 00 00 04

      4.1 通过手机APP给设备配网

      公版APP扫描 刚才创建的产品的配网二维码,按提示完成配网。

      4.2 手机APP控制设备:

      配网成功,设备成功连接到云端后,即可通过手机APP控制demo设备。
      707862060 (1).jpg

      5 天猫精灵控制

      生活网平台的电风扇品类已经与天猫精灵云平台打通。因此可以直接使用天猫精灵音箱语音指令控制demo。

      057501015E8429FA5A50FC941E6B1803.jpg

      我拍了一个天猫精灵控制电风扇demo的视频并上传到了优酷。
      视频地址

      6 写在最后

      因篇幅及文笔水平有限,本文的文字和图片可能存在错误之处还请读者多包涵。
      欢迎一起探讨学习家电物联网智能化;

      481682353 (2).jpg

      ]]>
      python爬虫URL编码和GETPOST请求 | python爬虫实战之三 Wed, 08 Apr 2020 03:03:11 +0800 urllib.parse模块

      该模块可以完成对url的编解码。
      先看一段代码,进行编码。

      image.png
      image.png

      此时查看结果,程序显示TypeError错误,urlencode函数第一参数要求是一个字典或者二元组序列。
      我们修改代码:

      from urllib import parse
      
      d = {
            'id':1
            'name': 'tom'
      }
      
      url = 'http://www.magedu.com/python'
      u = parse.urlencode(d)
      print(u)

      执行结果:

      image.png

      我们将结果拼接:

      url = 'http://www.magedu.com/python?id=1&name=tom'

      此时,类似于查询字符串,相当于get方法
      若再次修改:

      url = 'http://www.magedu.com/python'
      body 'id=1&name=tom'

      则此时相当于post请求。

      from urllib import parse
      
      d = {
            'id':1
            'name': 'tom'
            'url': 'http://www.magedu.com/python?id=1&name=tom'
      }
      
      u = parse.urlencode(d)
      print(u)

      执行结果:

      image.png

      我们修改name为“张三”:

      'name': '张三'

      执行结果:

      image.png
      image.png

      从运行结果来看冒号、斜杠、&、等号、问号等符号全部被编码了,%之后实际上是单字节十六进制表示的值。

      一般来说url中的地址部分, 一般不需要使用中文路径, 但是参数部分, 不管GET还是POST方法, 提交的数据中,可能有斜杆、等号、问号等符号,这样这些字符表示数据,不表示元字符。如果直接发给服务器端,就会导致接收方无法判断谁是元字符, 谁是数据了。为了安全, 一般会将数据部分的字符做url编码, 这样就不会有歧义了。后来可以传送中文, 同样会做编码, 一般先按照字符集的encoding要求转换成字节序列, 每一个字节对应的十六进制字符串前加上百分号即可。

      网页使用utf-8编码:

      image.png

      之前都是进行编码过程,现在来看一下解码的过程:

      from urllib import parse
      
      d = {
            'id':1
            'name': 'tom'
            'url': 'http://www.magedu.com/python?id=1&name=tom'
      }
      
      u = parse.urlencode(d)
      print(u)
      
      x = parse.unquote(u)
      print(x)

      执行结果:

      image.png

      以上就是对parse模块的介绍,其余的我们不再进行演示了,下面来了解method方法。

      提交方法method

      最常用的HTTP交互数据的方法是GET、POST。

      GET方法, 数据是通过URL传递的, 也就是说数据是在HTTP报文的header部分。POST方法, 数据是放在HTTP报文的body部分提交的。
      数据都是键值对形式, 多个参数之间使用&符号连接。例如a=1&b=abc

      GET方法

      连接 必应 搜索引擎官网,获取一个搜索的URLhttp://cn.bing.com/search?q=马哥教育
      需求
      请写程序完成对关键字的bing搜索, 将返回的结果保存到一个网页文件。

      from urllib import parse
      
      base_url = 'http://cn.bing.com/search'
      d = {
            'q':'马哥教育'
      }
      
      u = parse.urlencode(d)
      url = '{}?{}'.format(base_url, u)
      
      print(url)
      print(parse.unquote(url))

      执行结果:

      image.png

      此时不能发出请求。我们添加代码:

      from urllib.request import urlopen, Request
      
      ua = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36"
      
      req = Request(url, headers={
          'User-agent':ua
      })
      
      with urlopen(req) as res:
          with open('o:/bing.html', 'wb+') as f:
              f.write(res.read())
              f.flush()

      程序执行成功。这是对特定页面的爬取。

      image.png
      image.png

      POST方法

      http://httpbin.org/ 测试网站

      image.png
      image.png

      我们来测试一下:

      from urllib import parse
      from urllib.request import urlopen, Request
      import simplejson
      
      url = 'http://httpbin.org/post'  # POST
      data = parse.urlencode({'name':'张三,@=/&*', 'age':'6' })    # body
      ua = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36"
      
      req = Request(url, headers={
          'User-agent':ua
      })
      
      print(data)
      
      with urlopen(req, data=data.encode()) as res:  # POST请求,data不能为None
          text = res.read()

      执行结果:

      image.png

      打印一下d的类型

      print(type(d))

      执行结果:

      image.png

      通过这种方式就实现了post交互,我们将data提交上去,就是发送post请求,如果对方的网站有响应,会返回数据,返回的数据是正好是json,所以对其用simplejson进行转换。我们是需要根据网站返回的结果,去选择合适的方法处理转换数据。

      配套视频课程,点击这里查看

      获取更多资源请订阅Python学习站

      ]]>
      访问 cdn、oss 跨域问题 Wed, 08 Apr 2020 03:03:11 +0800

      作者:张医博

      背景:

      经常遇到有跨域的问题,老生长谈,却又屡禁不止,谈到跨域我们就了解下它是什么?

      一句话简单说明

      一个资源请求一个其它域名的资源时会发起一个跨域 HTTP 请求 (cross-origin HTTP request)。比如说,域名A(http://domaina.example) 的某 Web 应用通过标签引入了域名 B(http://domainb.foo) 的某图片资源(http://domainb.foo/image.jpg),域名 A 的 Web 应用就会导致浏览器发起一个跨域 HTTP 请求

      http://www.123.com/index.html 调用 http://www.123.com/server.php (非跨域)
      
      http://www.123.com/index.html 调用 http://www.456.com/server.php (主域名不同:123/456,跨域)
      
      http://abc.123.com/index.html 调用 http://def.123.com/server.php (子域名不同:abc/def,跨域)
      
      http://www.123.com:8080/index.html 调用 http://www.123.com:8081/server.php (端口不同:8080/8081,跨域)
      
      http://www.123.com/index.html 调用 https://www.123.com/server.php (协议不同:http/https,跨域)
      
      请注意:localhost和127.0.0.1虽然都指向本机,但也属于跨域。

      跨域请求标识

      origin ,当浏览器识别出 client 发起的请求需要转到另外一个域名上处理是,会在请求的 request header 中增加一个 origin 标识,如下我用 curl 测试了一个域名

      curl -voa http://mo-im.oss-cn-beijing.aliyuncs.com/stu_avatar/010/personal.jpg -H "Origin:www.mobby.cn"
        % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                       Dload  Upload   Total   Spent    Left  Speed
        0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 59.110.190.173...
      * TCP_NODELAY set
      * Connected to mo-im.oss-cn-beijing.aliyuncs.com (59.110.190.173) port 80 (#0)
      > GET /stu_avatar/010/personal.jpg HTTP/1.1
      > Host: mo-im.oss-cn-beijing.aliyuncs.com
      > User-Agent: curl/7.54.0
      > Accept: */*
      > Origin:www.mo.cn
      > 
      < HTTP/1.1 200 OK
      < Server: AliyunOSS
      < Date: Sun, 09 Sep 2018 12:30:28 GMT
      < Content-Type: image/jpeg
      < Content-Length: 8407
      < Connection: keep-alive
      < x-oss-request-id: 
      < Access-Control-Allow-Origin: www.mobby.cn
      < Access-Control-Allow-Credentials: true
      < Access-Control-Allow-Methods: GET, POST, HEAD
      < Access-Control-Max-Age: 0
      < Accept-Ranges: bytes

      可以看到挡我发起 origin 的请求头后,如果目标的网页服务允许来源的域名访问,就会在响应的 respond 头上带上跨域的响应头。(以下 header 目标域名如果设置了才会有响应)

      < Access-Control-Allow-Origin: www.mobby.cn (允许的跨域来源,可以写 *,或者绝对域名) 
      < Access-Control-Allow-Headers: *(允许跨域时携带哪些 header )
      < Access-Control-Allow-Methods: GET, POST, HEAD (允许哪些跨域请求方法,origin 是默认支持的)

      跨域设置分类 cdn-cdn 、cdn-oss

      类型一:cdn-cdn

      image.png

      通过报错可以看出来 发起跨区域请求的源头 是 bo3.ai.com 加载了 www.ai.com 网站的资源,这两个域名都在 阿里云 cdn 加速。既然找到了请求目的 www.ai.com,那么直接检查下目的域名上是否新增了跨域头。这种情况基本都是目的域名没有加上允许的跨域头导致。

      类型二:cdn-oss

      image.png

      这个问题比较特殊,拆分两部分说明。出现这种情况,通过截图我们发现用户有两种请求,分别是 get 和 post 两种,由于 get 好测试,我们先说 get

      1、出现跨域错误,首先就要检查原是否添加了跨域头,于是我们使用 curl 测试一下,如果最简单的 get 测试成功返回跨域头,说明目的 域名设置了跨域响应,如果测试失败说明原没有添加跨域响应。通过如下图很显然看到了目的添加了跨域头。

      curl -voa http://mo-im.oss-cn-beijing.aliyuncs.com/stu_avatar/010/personal.jpg -H "Origin:www.mo.cn"
        % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                       Dload  Upload   Total   Spent    Left  Speed
        0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 59.110.190.173...
      * TCP_NODELAY set
      * Connected to mo-im.oss-cn-beijing.aliyuncs.com () port 80 (#0)
      > GET /stu_avatar/010/personal.jpg HTTP/1.1
      > Host: mo-im.oss-cn-beijing.aliyuncs.com
      > User-Agent: curl/7.54.0
      > Accept: */*
      > Origin:www.mo.cn
      > 
      < HTTP/1.1 200 OK
      < Server: AliyunOSS
      < Date: Sun, 09 Sep 2018 12:30:28 GMT
      < Content-Type: image/jpeg
      < Content-Length: 8407
      < Connection: keep-alive
      < x-oss-request-id: 5B951264980F8FDB749972B3
      < Access-Control-Allow-Origin: www.mo.cn
      < Access-Control-Allow-Credentials: true
      < Access-Control-Allow-Methods: GET, POST, HEAD
      < Access-Control-Max-Age: 0
      

      2、通过 get 测试发现 oss 是加了跨域头的,但是为什么 post 请求就返回 405 呢?没有任何跨域头呢?用户反馈为什么手动 curl 测试也是失败

      curl -v -X POST -d '{"user":"xxx"}' http://mo-im.oss-cn-beijing.aliyuncs.com/stu_avatar/010/personal.jpg -H "Origin:www.mo.cn"
      Note: Unnecessary use of -X or --request, POST is already inferred.
      *   Trying 59.110.190.173...
      * TCP_NODELAY set
      * Connected to mo-im.oss-cn-beijing.aliyuncs.com (59.110.190.173) port 80 (#0)
      > POST /stu_avatar/010/personal.jpg HTTP/1.1
      > Host: mo-im.oss-cn-beijing.aliyuncs.com
      > User-Agent: curl/7.54.0
      > Accept: */*
      > Origin:www.mo.cn
      > Content-Length: 14
      > Content-Type: application/x-www-form-urlencoded
      > 
      * upload completely sent off: 14 out of 14 bytes
      < HTTP/1.1 405 Method Not Allowed
      < Server: AliyunOSS
      < Date: Sun, 09 Sep 2018 13:06:28 GMT
      < Content-Type: application/xml
      < Content-Length: 337
      < Connection: keep-alive
      < x-oss-request-id: 
      < Allow: GET DELETE HEAD PUT POST OPTIONS
      < 
      <?xml version="1.0" encoding="UTF-8"?>
      <Error>
        <Code>MethodNotAllowed</Code>
        <Message>The specified method is not allowed against this resource.</Message>
        <RequestId></RequestId>
        <HostId>mo-im.oss-cn-beijing.aliyuncs.com</HostId>
        <Method>POST</Method>
        <ResourceType>Object</ResourceType>
      </Error>
      

      2.1、 首先我们看 oss 对于 post 的要求

      https://help.aliyun.com/document_detail/31988.html?spm=a2c4g.11186623.6.1008.5bb84b4e1JEoA4
      

      2.2 、发现无论是 curl 测试还是代码的请求,都出先一个共性的问题。

      • 请求的格式不是 rfc 标准规定的 content-type:multipart/form-data
      • 请求头不是内存不是 rfc 规定的表单域提交
      • 既然不是表单域,那么要求的 filename 肯定也不知最后一个域。

      2.3 、在以上情况下既然是非法的提交那么 oss 肯定返回了 405 ,所以不会出触发跨域的响应头,必要要正确的返回 200 状态才可以,那么我们就用一段 Java 代码演示下跨域的操作以及正确的响应头抓包信息。

      JAVA 跨域请求源码

      package com.alibaba.edas.carshop.OSS;
      
      import javax.activation.MimetypesFileTypeMap;
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      
      import org.apache.commons.codec.binary.Base64;
      
      import java.io.*;
      import java.net.HttpURLConnection;
      import java.net.URL;
      import java.security.InvalidKeyException;
      import java.security.NoSuchAlgorithmException;
      import java.util.Iterator;
      import java.util.LinkedHashMap;
      import java.util.Map;
      import java.util.Map.Entry;
      
      public class OSSPostFile {
          // The local file path to upload.
          private String localFilePath = "C:\T\1.txt";
          // OSS domain, such as http://oss-cn-hangzhou.aliyuncs.com
          private String endpoint = "http://oss-cn-beijing.aliyuncs.com";
          // Access key Id. Please get it from https://ak-console.aliyun.com
          private String accessKeyId = "";
          private String accessKeySecret = "";
          // The existing bucket name
          private String bucketName = "您自己的bucket名称";
          // The key name for the file to upload.
          private String key = "1.txt";
      
          public void PostObject() throws Exception {
              // append the 'bucketname.' prior to the domain, such as
              // http://bucket1.oss-cn-hangzhou.aliyuncs.com.
              String urlStr = endpoint.replace("http://", "http://" + bucketName + ".");
      
              // form fields
              Map<String, String> formFields = new LinkedHashMap<String, String>();
      
              // key
              formFields.put("key", this.key);
              // Content-Disposition
              formFields.put("Content-Disposition", "attachment;filename=" + localFilePath);
              // OSSAccessKeyId
              formFields.put("OSSAccessKeyId", accessKeyId);
              // policy
              String policy = "{"expiration": "2120-01-01T12:00:00.000Z","conditions": [["content-length-range", 0, 104857600000]]}";
              String encodePolicy = new String(Base64.encodeBase64(policy.getBytes()));
              formFields.put("policy", encodePolicy);
              // Signature
              String signaturecom = computeSignature(accessKeySecret, encodePolicy);
              formFields.put("Signature", signaturecom);
      
              String ret = formUpload(urlStr, formFields, localFilePath);
      
              System.out.println("Post Object [" + this.key + "] to bucket [" + bucketName + "]");
              System.out.println("post reponse:" + ret);
          }
      
          private static String computeSignature(String accessKeySecret, String encodePolicy)
                  throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
              // convert to UTF-8
              byte[] key = accessKeySecret.getBytes("UTF-8");
              byte[] data = encodePolicy.getBytes("UTF-8");
      
              // hmac-sha1
              Mac mac = Mac.getInstance("HmacSHA1");
              mac.init(new SecretKeySpec(key, "HmacSHA1"));
              byte[] sha = mac.doFinal(data);
      
              // base64
              return new String(Base64.encodeBase64(sha));
          }
      
          private static String formUpload(String urlStr, Map<String, String> formFields, String localFile) throws Exception {
              String res = "";
              HttpURLConnection conn = null;
              String boundary = "9431149156168";
      
              try {
                  URL url = new URL(urlStr);
                  conn = (HttpURLConnection) url.openConnection();
                  conn.setConnectTimeout(5000);
                  conn.setReadTimeout(30000);
                  conn.setDoOutput(true);
                  conn.setDoInput(true);
                  conn.setRequestMethod("POST");
                  conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
                  conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
                  OutputStream out = new DataOutputStream(conn.getOutputStream());
      
                  // text
                  if (formFields != null) {
                      StringBuffer strBuf = new StringBuffer();
                      Iterator<Entry<String, String>> iter = formFields.entrySet().iterator();
                      int i = 0;
      
                      while (iter.hasNext()) {
                          Entry<String, String> entry = iter.next();
                          String inputName = entry.getKey();
                          String inputValue = entry.getValue();
      
                          if (inputValue == null) {
                              continue;
                          }
      
                          if (i == 0) {
                              strBuf.append("--").append(boundary).append("rn");
                              strBuf.append("Content-Disposition: form-data; name="" + inputName + ""rnrn");
                              strBuf.append(inputValue);
                          } else {
                              strBuf.append("rn").append("--").append(boundary).append("rn");
                              strBuf.append("Content-Disposition: form-data; name="" + inputName + ""rnrn");
                              strBuf.append(inputValue);
                          }
      
                          i++;
                      }
                      out.write(strBuf.toString().getBytes());
                  }
      
                  StringBuffer strBuf1 = new StringBuffer();
                  String callback = "{"callbackUrl":"http://47.93.116.168/Revice.ashx","callbackBody":"{\"bucket\"=${bucket},\"size\"=${size}}"}";
      
                  byte[] textByte = callback.getBytes("UTF-8");
                  strBuf1.append("rn").append("--").append(boundary).append("rn");
      
                  String callbackstr = new String(Base64.encodeBase64(textByte));
                  strBuf1.append("Content-Disposition: form-data; name="callback"rnrn" + callbackstr + "rnrn");
                  out.write(strBuf1.toString().getBytes());
      
                  // file
                  File file = new File(localFile);
                  String filename = file.getName();
                  String contentType = new MimetypesFileTypeMap().getContentType(file);
                  if (contentType == null || contentType.equals("")) {
                      contentType = "application/octet-stream";
                  }
      
                  StringBuffer strBuf = new StringBuffer();
                  strBuf.append("rn").append("--").append(boundary).append("rn");
                  strBuf.append("Content-Disposition: form-data; name="file"; " + "filename="" + filename + ""rn");
                  strBuf.append("Content-Type: " + contentType + "rnrn");
                  out.write(strBuf.toString().getBytes());
      
                  DataInputStream in = new DataInputStream(new FileInputStream(file));
                  int bytes = 0;
                  byte[] bufferOut = new byte[1024];
                  while ((bytes = in.read(bufferOut)) != -1) {
                      out.write(bufferOut, 0, bytes);
                  }
                  in.close();
      
                  byte[] endData = ("rn--" + boundary + "--rn").getBytes();
                  out.write(endData);
                  out.flush();
                  out.close();
      
                  // Gets the file data
                  strBuf = new StringBuffer();
                  BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                  String line = null;
                  while ((line = reader.readLine()) != null) {
                      strBuf.append(line).append("n");
                  }
                  res = strBuf.toString();
                  reader.close();
                  reader = null;
              } catch (Exception e) {
                  System.err.println("Send post request exception: " + e.getLocalizedMessage());
                  throw e;
              } finally {
                  if (conn != null) {
                      conn.disconnect();
                      conn = null;
                  }
              }
      
              return res;
          }
      
      }
      

      经过代码测试以及抓包验证在 正确的 post 的前提下,oss 的跨域规则已经生效。

      image.png

      ]]>
      借助IoT平台云端数据解析能力,转换Modbus,电力协议,hex数据 Wed, 08 Apr 2020 03:03:11 +0800 1.整体技术方案

      在IoT场景中,很多传感器采集到的都是二进制数据,或者私有协议格式数据流,设备端又不具备转换成结构化JSON的能力,这时候我们可以借助IoT物联网平台云端自定义数据解析能力,转换Modbus,电力协议,hex数据,私有协议为结构化的JSON,再流转到业务系统。

      数据流转链路

      image.png

      消息变化

      image.png

      2.物联网平台开发

      消息通信Topic

      image.png

      hex转换脚本配置


      原始数据:0x035e8192fd0000000d0000001b00000a8c
      数据业务格式:
      image.png
      脚本配置
      image.png


      完整脚本内容

      /**
       * 将设备自定义topic数据转换为json格式数据, 设备上报数据到物联网平台时调用
       * 入参:topic   字符串,设备上报消息的topic     
       * 入参:rawData byte[]数组                  不能为空
       * 出参:jsonObj JSON对象                    不能为空
       */
      function transformPayload(topic, rawData) {
          var jsonObj = {}
          
          //原始hex数据 : 0x035e8192fd0000000d0000001b00000a8c
      /*
      {
        "heartbeat": 15,
        "id": 1585549855,
        "steps": 2700,
        "speed": 56
      }
      */
          if (topic.endsWith('/user/update')) {
                  var uint8Array = new Uint8Array(rawData.length);
                  for (var i = 0; i < rawData.length; i++) {
                      uint8Array[i] = rawData[i] & 0xff;
                  }
                  var dataView = new DataView(uint8Array.buffer, 0);
      
                  var fHead = uint8Array[0]; // command
                  if (fHead == 0x03) {
                      //
                      jsonObj['id'] = dataView.getInt32(1);
                      //心跳
                      jsonObj['heartbeat'] = dataView.getInt32(5);
                      //速度
                      jsonObj['speed'] = dataView.getInt32(9);
                      //总步数
                      jsonObj['steps'] = dataView.getInt32(13);
                  }
          }
          
          return jsonObj;
      }
        

      3.设备开发

      设备上报hex原始数据

      // 消息Topic携带?_sn=default标识
      const topic = '/aiDerw9823s/dn308/user/update'+'?_sn=default';
      // 原始数据
      var payloadArray = [ 3, 94, 129, 169, 59, 0, 0, 0, 23, 0, 0, 0, 79, 0, 0, 30, 220 ];
      var payload = new Buffer(payloadArray);
      // 发布数据到topic
      client.publish(topic, payload);
      
      

      4.联调日志

      设备上报原始hex数据

      image.png

      脚本转换后日志

      image.png

      业务消息报文日志

      消息详情(topic和payload)
      image.png

      ]]>
      调度参数在MaxCompute的使用 Wed, 08 Apr 2020 03:03:11 +0800 一、调度参数和MaxCompute的关系

      首先明确调度参数是属于DataWorks上的和MaxCompute之间是没有关系的。

      二、调度参数执行的原理

      调度参数是经过DataWorks的调度系统进行解析,然后将解析的值传到MaxCompute上MaxCompute根据对应的key获取对应的value,所以想要取到值必须经过DataWorks的调度系统解析。

      三、如何测试调度参数

      1.系统参数(2个)

      • 主要包括业务时间bdp.system.bizdate
      • 定时时间bdp.system.cyctime
      说明
      这两个值由于是DataWorks的系统参数,可以直接在代码中使用,在页面点击高级运行可以解析
      **使用方法:
      **
      在DataWorks直接点击高级运行可以看到结果

      select ${bdp.system.bizdate}

      结果:
      image.png

      2.时间参数

      内置参数
      ($bizdate和$cyctime)、${…}和$[…],
      说明
      由于不是系统的必须经过调度系统才能测试,在页面点击高级运行也是无法解析的
      使用方法
      • 在数据流程->MaxCompute->数据开发->新建一个odspsql节点
      image.png

      • 双击打开节点,编写sql

      image.png
      • 点击调度配置,配置调度参数

      image.png
      • 将当前节点保存,关闭退出运行

      image.png
      • 查看结果

      image.png

      3.自定义常量参数

      说明
      在页面点击高级运行可以解析
      • 在临时查询中编写sql

      select '${key}';

      • 点击高级运行

      image.png
      • 查看结果
      image.png

      大家如果对MaxCompute有更多咨询或者建议,欢迎扫码加入 MaxCompute开发者社区钉钉群,或点击链接 申请加入。
      image.png

      ]]>
      为什么支付宝有底气说“你敢付我敢赔”? Wed, 08 Apr 2020 03:03:11 +0800 随着技术纵深发展,“万物互联”的概念已不鲜见,在数字化世界的游戏规则里,除了网络基础信息安全隐患外,业务安全风险也已经越来越被重视。

      陈锣斌是支付宝大安全的资深架构师,已拥有近十年“蚁龄”。

      从2010年9月加入蚂蚁后,陈锣斌主要负责业务风控平台的实时计算以及数据相关工作,经历过整个业务风控从2代平台到5代平台进入智能化阶段的建设,近两年陈锣斌同时进入到基础安全领域,带领平台研发团队建设“安全全域态势威胁感知平台”,为支付宝整体安全的攻防提供有力支撑。

      当我们在谈互联网安全的时候,我们在谈什么

      在陈锣斌看来,互联网安全目前主要两个大领域,一块是网络信息安全或者说是基础安全,另一块是在网络上各种应用、服务所涉及的业务本身的业务安全。在此之后才有“风控系统”的到来。

      前者其实就是互联网上的信息安全。涉及到互联网上信息的保密性、完整性、可用性、真实性和可控性的相关技术。比如,个人在网站上的隐私信息保护、网络通信的时候防止信息被窃取监听,为防止黑客入侵造成服务攻击或者数据泄露所建设的各种安全防护能力等。

      最开始是1988 年,世界上第一例蠕虫病毒“Morris”出现,之后世界上各种病毒、木马和网络攻击层出不穷,严重威胁到互联网的繁荣和用户数据的安全。特别是制造网络病毒逐渐成为一种有利可图的产业,网络安全就基本上成为伴随互联网扩散的常态问题,且愈演愈烈。

      从“Morris”到“wannacry”,蠕虫病毒挑战的不只是安全防护技术

      后者业务安全,其实就是在互联网上开展各种业务所面临的一些风险防控的技术。比如社交的相关业务就会涉及到内容的安全(暴恐政、黄赌毒),电商业务的刷单、营销作弊,还有就是支付的时候的支付风控、洗钱、欺诈等风险防控。

      网络安全的过去、现在和未来

      陈锣斌认为,早期对安全的认知都相对简单,很多人认为个人电脑就是杀毒软件,网络就是防火墙,业务安全可能就是数字证书、密码加上简单的异步监控。但随着市场的扩张,不断升级的安全风控的能力成为各大互联网公司的标配。

      互联网是一个开放的世界,不法分子、黑产都会想在其中寻找牟利的机会,互联网重要业务的开展现在都离不开多种网络安全的能力,当一个公司不具备这方面足够能力的时候,很容易使得业务开展举步维艰,甚至直接影响公司的生存,比如,游戏(入侵系统、游戏外挂)、社交(内容安全)、营销广告(营销作弊)、电商(刷单、欺诈),愈演愈烈的风险手法已经不是在单一的安全领域能独立解决的,更需要从整个安全体系来看,综合的安全防控方案是什么,基础安全和业务安全深入联动也已成为行业共识。

      相对应地,基础安全技术和业务安全技术也两者从原先的相对比较独立到逐渐走向了融合,业务安全主要基于大数据计算、模型算法来做风险检测,基础安全在反入侵、反爬主机安全等方面也使用很多的大数据技术,两者在风险的联合防控上越来越紧密,在技术上大数据计算、人工智能方面也有越来越多的交集。支付宝AlphaRisk就是这方面的典范。

      陈锣斌重点揭示:网络安全趋势将走向需要综合性防控的道路,单一安全/风险技术都很难防控住目前互联网上的新型风险。

      支付宝为什么腰杆这么硬:“你敢付,我敢赔。”

      早在2005年,支付宝就有相应的风控系统以及对应的职能部门,为守护客户的每一分钱而努力。“你敢付,我敢赔”的理念一直延续至今。

      相信有不少人看过《智造将来》黑客攻击支付宝的电视节目,节目中几位顶尖黑客拿着“真枪实弹”一层层攻入一位普通用户的支付宝,试图转走5元。5元在普通的转账过程中其实是极为正常的,这样小额的数目为支付宝的安全系统带去了更大的挑战。据支付宝大安全掌门人芮雄文回忆,当时确实捏了一把冷汗,黑客已经拿到了支付宝用户的密码、银行卡,甚至已经用软件控制了手机可以发送验证码当场修改密码……在看似360度无死角的黑客攻击后,支付宝的AlphaRisk依旧是完美地阻止了所有非正常的支付行为。

      当“黑客”攻入支付系统时,雄文在内的所有人都捏了一把汗

      支付宝已经成为用户全球排名第一的移动支付工具,而保障支付宝毫发未损的风控系统经历了五代升级,现如今,它的正式名称为“AlphaRisk”。

      AlphaRisk是支付宝风控多年实践与技术创新的智慧结晶,是全球最先进的风控系统之一,在其保护下,支付宝交易资损率不到千万分之一,远低于国际同行。“如果拿自动驾驶的等级定义来比喻,目前AlphaRisk处于L2和L3之间。在智能化的加持下,处于行业领先水平。”陈锣斌说。

      AlphaRisk的原理是应用AI技术颠覆传统风控的运营模式,通过Perception(风险感知)、AI Detect(风险识别)、Evolution(智能进化)、AutoPilot(自动驾驶)4大模块的构建,将人类直觉AI(Analyst Intuition)和机器智能AI(Artificial Intelligence)完美结合,打造具有机器智能的风控系统,愿景是实现风控领域的“无人驾驶”技术。

      支付宝平台上每天都有上亿笔交易,通过AlphaRisk智能风控引擎,不仅能够对每个用户的每笔支付进行7×24小时的实时风险扫描;同时通过不断新增的风险特征挖掘和优化算法迭代的模型,自动贴合用户行为特征进行进化风险对抗,不足0.1秒就能完成风险预警、检测、管控等流程,确保用户账户安全和支付交易的万无一失。

      安全一直是支付宝发展的生命线,支付宝从一而终地践行着“你敢付,我敢赔”的理念,时至今日,支付宝在风控领域的探索、创新,以及落地应用都处于世界的前列。

      “热情加实践经验是最重要的”

      目前支付宝大安全也一定程度上代表了当今世界安全技术的巅峰,加入这个团队你必然会被培养成在网络安全技术领域的中流砥柱。

      在陈锣斌眼里,安全风控领域是集基础建设、安全攻防、大数据技术、人工智能于大成的一个交叉领域。由于在激烈的复杂对抗一线,在这个领域的人能得到最充分的锻炼,技术也最有希望快速做到业界顶尖。

      对于想加入大安全的同学来说,陈锣斌建议同学们在夯实基础的同时,还是要多多动手,多做,无论实验室项目还是企业实习项目,实际去做和停留在理论上是不同的结果,具体还有如下三点:

      1.技术基础:计算机基础知识扎实、数据结构、算法等能活学活用,有1~2门掌握的较好的语言。

      2.有成果:有较多的实践,有实际项目经验很重要,真正用技术、算法去解决过一些问题,也可以是去参加过什么比赛,有过何种收获等。

      3.有热情:学习能力,平时在自己的领域之外,是否有关注更多的前沿技术,学了什么,去思考过什么。

      加入我们

      > 团队介绍

      蚂蚁金服大安全事业群是管理蚂蚁金服账户安全、资金安全、内容安全和数据信息安全、系统平台安全的核心部门。团队运用最新的生物核身技术和大数据技术,结合专家经验、数据分析方法和AI手段,保障用户的权益和资金安全。

      蚂蚁金服的旗下产品包括支付宝、蚂蚁财富、芝麻信用、花呗、相互宝、余额宝、蚂蚁金服等。

      > 你想要的我们都有

      前沿领域:科技驱动金融,金融科普大众,支付宝的核心业务在于安全。参与建设各种安全项目,感受安全如何成为实现普惠金融、构建新金融生态的核心驱动力。

      一流平台:全球10亿用户的大平台,全球一流的互联网安全技术体系!近距离接触支付宝风控多年实践与新技术创新的最新成果——第五代智能风控引擎AlphaRisk!

      最新技术:体验应用AI技术颠覆传统风控的最新动向,参与建设具有机器智能的风控系统,一起实现风控领域的“无人驾驶”技术。

      > 岗位简介

      社招工作地点:杭州

      校招/实习工作地点:杭州/上海/成都/北京

      校招/实习岗位类型:

      1. 研发类:Java、C++、安全、前端、客户端、数据研发

      2. 算法类:机器学习、图像图形、NLP等

      > 简历投递

      邮箱:luobin.chen@antfin.com

      蚂蚁金服“共战‘疫情’,技术破局”数字课堂线火热直播中。4月2日,走进阿里云云栖号在线课堂,蚂蚁金服数据技术专家王嘉喆和高级体验设计师徐海豪将针对新版Ant Design 4.0的整体设计思路,以及在图表自动生成的用法和原理上的突破进行详细介绍。扫描下方二维码即可观看直播!

      ]]>
      免费下载 | 全景揭秘阿里文娱智能算法 Wed, 08 Apr 2020 03:03:11 +0800 文娱大脑究竟能有多“智能”?答案是全生命周期的人工智能技术!从内容智能到增长营销,全景揭秘阿里文娱智能算法,就在这一本!

      点击这里免费获取电子书

      image.png

      阿里是一家坚信数据力量的公司,而文娱涉及的相关产业非常广泛,从线上到线下、从影剧综漫到现场娱乐以及文学小说等,其组成、形式、展现、分发的复杂性交织在一起为业务数据化带来了巨大的挑战。

      近三年来,阿里文娱摩酷实验室始终以助力业务发展和增长为核心驱动,形成如下四个的技术方向:

      image.png

      内容理解是文娱相关算法技术的基石,IP、小说、剧本、视频、音乐等不同形态的内容对构建起领域知识图谱带来了很多困难,在这其中计算机视觉、自然语言处理、图谱&推理、图神经网络、多模态内容分析等技术被广泛应用于内容解构。以视频为例,影剧综视频的时长很难用一些低层级的标签来抽象表达其内容,基于多模态的分析技术在这类内容上也会碰壁,因此融合内容专家及机器学习系统的半自动化微标签体系成为一种可行的出路。与短视频快速的线上反馈闭环不同,即使制作周期最短的综艺节目也需要 3 个月以上,期间还面临内容监察审核的不确定,这就导致影剧综内容制作高度的不确定性,如何基于复杂的数据分析线索以及历史的成败规律来选择评估内容是各个综合视频平台所面临的核心挑战之一,而阿里文娱北斗星系统就是用来解决这一问题的。

      搜索和推荐作为两种解决信息爆炸的重要手段被广泛应用于各个 APP 中,而影剧综内容的复杂性导致用户想精确描述一个内容非常困难,仅通过节目名、演员名去检索给用户也造成了很大的困扰。在文娱内容的分发体系中对搜索模式、推荐模式的融合成为新的用户需求,如何更为准确的通过类强化学习的用户意图理解过程来协助他们尽快找到喜爱的内容,成为文娱搜推体系下一阶段的首要任务。

      文娱作为产业互联网发展的重要行业,人工智能技术在这个领域中的应用空间广大,而我们也仅仅是迈出了一小步,期待工程师们能够创造出更大的奇迹,加速文娱产业数字工业化时代的到来。

      ]]>
      E-MapReduce弹性低成本离线大数据分析 Wed, 08 Apr 2020 03:03:11 +0800 作者:明誉


      大数据是一项涉及不同业务和技术领域的技术和工具的集合,海量离线数据分析可以应用于多种商业系统环境,例如,电商海量日志分析、用户行为画像分析、科研行业的海量离线计算分析任务等场景。

      离线大数据分析概述

      主流的三大分布式计算框架系统分别为Hadoop、Spark和Storm:

      • Hadoop可以运用在很多商业应用系统,可以轻松集成结构化、半结构化以及非结构化数据集。
      • Spark采用了内存计算,允许数据载入内存作反复查询,融合数据仓库、流处理和图形计算等多种计算范式,能够与Hadoop很好地结合。
      • Storm适用于处理高速、大型数据流的分布式实时计算,为Hadoop添加可靠的实时数据处理能力。

      海量离线数据分析可以应用于多种场景,例如:

      • 商业系统环境:电商海量日志分析、用户行为画像分析。
      • 科研行业:海量离线计算分析和数据查询。
      • 游戏行业:游戏日志分析、用户行为分析。
      • 商业用户:数据仓库解决方案的BI分析、多维分析报表。
      • 大型企业:海量IT运维日志分析。

      架构图

      image.png

      方案优势

      • 高性能、低成本
      • 快速部署
      • 弹性
      • 多种计算模式
      • 无缝对接开源生态
      • 一站式管理平台

      方案详情

      详情请参见E-MapReduce弹性低成本离线大数据分析最佳实践


      对开源大数据感兴趣的同学可以加小编微信(图一二维码,备注进群)进入技术交流微信2群。也可钉钉扫码加入社区的钉钉群

      image.png

      阿里巴巴开源大数据技术团队成立Apache Spark中国技术社区,定期推送精彩案例,技术专家直播,问答区数个Spark技术同学每日在线答疑,只为营造纯粹的Spark氛围,欢迎钉钉扫码加入!
      image.png

      Apache Spark技术交流社区公众号,微信扫一扫关注

      image.png

      ]]>
      【OSS 排查方案-3】OSS 的网络排查 Wed, 08 Apr 2020 03:03:11 +0800

      作者:张医博

      背景

      鉴于之前遇到很多 本地-》OSS ,上传、下载总是慢的情况,或者上传、下载经常出现错误或者异常的问题。根据多个典型案例,抽象出一下排查方案,希望对大家快速定位问题有所帮助。

      一、必要了解信息

      • requestID ,一般情况都会有,标识此次 OSS request 到达 OSS 服务端后返回的标志。除非建联失败,否则都会有这个 requestID 的,当问题比较难排查时可以将 requestID 提供给阿里云客服作为定位线索。
      • 明确 上传/下载 的方式(阿里的工具、SDK、API、浏览器),不同的方式会有不同的思路,类似 JAVA SDK 如果 maxconnection 设小了,也会造成链接等待超时。
      • 使用内网还是公网,大多数我们建议在 ECS 和 OSS 同可用区时最好使用内网,这样费用低,速度还快,公网一般出现拥塞,会被网络无限扩大。
      • 在 client 端 ping traceroute MTR 到 OSS 服务端的截图信息,看看到公/私网是否有丢包,或者用 tcpdump 抓包看下网络是否有高延迟高丢包以及 TCP 协议栈异常的问题。
      • 是否有明显报错,基本上遇到 socket timeout 的情况,都是网络超时,可以从本机的网络以及本机连接数,或者 client 是否有超时设置来排查。。
      • 以上信息收集到后准备测试,源 OSS URL 测试(举例):curl -svo /dev/null http://img.oss-cn-hangzhou.aliyuncs.com/uploads/temp/2018-01-02%20%2013-52-15-operate-stat.xls
      • 如果要是 CDN -》 OSS 的问题,可以分别固定 CDN 和 OSS 源站进行测试 curl -I -x CDNIP:80 http://xxx/xxx,固定源站 curl -I -x http://xxx.oss-cn-xxx.aliyuncs.com/xxx 如果测试 OSS 200 正常, CDN 异常,则问题可能发生在 CDN 侧。

      二、明确自己的服务架构逐层排查

      • 本地 -》 OSS
      • 本地 -》proxy -》OSSproxy 种类有很多,
      • SLB
      • WAF
      • 高防

      三、场景拟合

      1、长时间或者间断性不能访问到 OSS

      这种情况,先确认一下自己 bucket 有没有欠费,是否有被拉黑等,其次再本地的 PC 端 ping 下自己要访问的 OSS 公网域名(如果是用内网 ECS 通过私网 OSS 域名上传,可以 ping 私网域名)是否能通,以及 traceroute 到对端 OSS 的路由路径,看下是否断在了哪一跳。同时请自己网外的他人协助下同时发起 ping 和
      traceroute 测试,看是否同样访问不通。如果是一样场景,可以将搜集的信息反馈给阿里云客服排查服务端是否异常。

      image.png

      场景2、本机上传文件到 OSS 超时,然后自动恢复

      image.png

      如图,遇到这类问题,需要先获取到必要信息。然后结合图中的 error 来看 socket 的异常,基本判断是由于网络问题导致了 Header 响应超时。侧面的 MTR TRACEROUTE 也可以看出来当时的网络质量如何,如网络正常但依然超时,可以直接抓包看下是哪端导致。

      场景3、使用 JAVA SDK 上传文件超时返回 502

      [chat-service] 2017-12-22 11:09:17,443 - com.aliyun.oss:73 -51224385 [http-nio-9081-exec-137] WARN - [Server]Unable to execute HTTP request: The difference between the request time and the current time is too large. [ErrorCode]: RequestTimeTooSkewed [RequestId]: 5A3C77583373BA19746BB032 [HostId]: sobot.oss-cn-beijing.aliyuncs.com [ResponseError]: <?xml version="1.0" encoding="UTF-8"?> <Error> <Code>RequestTimeTooSkewed</Code> <Message>The difference between the request time and the current time is too large.</Message> <RequestId>5A3C77583373BA19746BB032</RequestId> <HostId>xxx.oss-cn-beijing.aliyuncs.com</HostId> <MaxAllowedSkewMilliseconds>900000</MaxAllowedSkewMilliseconds> <RequestTime>2017-12-22T02:53:44.000Z</RequestTime> <ServerTime>2017-12-22T03:09:12.000Z</ServerTime> </Error>
      

      image.png

      如图,可以出现这种 Message ,已经明确告诉你本地与 OSS 服务端的时间差 >15min 导致。“The difference between the request time and the current time is too large”,出现此问题,一般与以下两个原因有关系:
      1)用户本地的时间与服务端器的时区不一致,要求用户本地是标准的 GMT 或者 UTC 时间。
      2)网络拥堵导致的等待时间过长超过 15min 。
      3)JAVA SDK 参数配置不合理,比如 max connection

      具体处理工作流如下

      image.png

      场景4、OSSFTP 上传到 OSS 时,抓包出现 zeroWindows

      image.png

      1、OSSFTP 是将远端的 OSS 挂载到本地,但操作的文件每次都是发起 HTTP 请求远端 OSS ,所以受到网络和本地 IO 的影响,高敏感的业务是不太适合的。

      2、看数据包中客户端发生的 ZeroWindows (代表 本地协议栈的 cache buffer 出现过满,应用层无法消费掉 buffer 的数据)

      3、通过 ECS 机器查看自己 CPU 、内网、网卡 是否有跑满情况,这种情况负载过高必然回导致慢的情况。

      建议:

      1、由于 OSSFTP 是串行,而且是 FTPCLIET->FTPSERVER->OSS SERVER 两段操作性能无法保证,推荐使用 ossutil ,
      链接:https://help.aliyun.com/document_detail/50452.html?spm=5176.doc31935.6.1032.YMtcGp

      2、ossutil 在上传大文件时可以采用分片多线程的粒度上传,而我们的 ossftp 是不存在分片的。所以还是推荐 ossutil

      未完待续

      ]]>
      当打之年,非你莫属——阿里云 MVP第12期全球发布 Wed, 08 Apr 2020 03:03:11 +0800 阿里巴巴董事会主席兼首席执行官张勇认为:如今,5G网络、工业互联网、物联网等网络基础、数据中心等数字基础、人工智能等运算基础,成为必要而普遍的新型基础设施。加快“新基建”进度,不是简单的基础设施建设,而是与产业化应用协调推进,既能增强基建稳增长的传统属性,又可以助推创新和拓展新消费、新制造、新服务。

      未来需要更多拥有创新能力和实战能力的开发者参与到各个行业中,利用“新基建”拓展商业的边界。

      image.png

      阿里云最有价值专家,简称 MVP(Most Valuable Professional),是专注于帮助他人充分了解和使用阿里云的技术实践领袖。近期,我们重磅发布阿里云 MVP 第12期全球名单。 MVP在各行各业里与阿里云一起改变世界,利用新技术打破商业的边界,创造更多新应用、新工具。他们影响着广大的开发者们,成为行业里的弄潮儿!

      曾任58集团技术委员会主席、转转首席架构师的孙玄,多年的大厂经验,在每个周末沉心修炼技术功底,提升演讲能力。认清自我做自己擅长的事情,拉通集团资源打造弹性的开发平台,让转转在2018年世界杯时顺滑的完成了业务高峰。2020年初放弃优厚的收入创建了“奈学教育”,他专注于数字化转型,在技术驱动和技术革新的环境下,新时代技术人才培训教学体系的革新,现在他拥有了新的身份——阿里云 MVP!

      查看特立独行的架构师——孙玄个人专访!

      豆瓣9.0分的Java大神周志明,他的著作《深入理解Java虚拟机》系列总销量超30万册,他喜欢研究“为什么”:新技术背后的思考是什么?新行业背后的逻辑是为什么?他说技术只是工具,重要的是要用开放的心态去拥抱未来。他虽然是主管但不愿脱离一线开发,一直热衷于开源技术。他建议开发者们通过“说出来、写出来、做给别人看”提升自己的技术水平。这就是阿里云 MVP周志明。

      查看职业电竞选手的Java大神路——周志明个人专访

      杨飞现任每日瑜伽技术负责人,他放下跨国公司的从业经验,从零开始从后端服务器转到系统架构。他说创业公司无边界,大厂有体系,工作就是最好的练兵场,无论身份是什么。啃下难啃的技术难题,与团队形成“正反馈”,不断扩张自己的边界,是他自己多次跨越式成长的心得。

      image.png

      他们扎根行业,磨炼着技术的广度和深度;他们纯粹地拥抱新技术,接受着行业的挑战;他们成为管理者,但仍热爱代码;他们不畏挑战,也勇于接受挑战,快速学习新技术大胆验证。三人行必有我师,他们可能少年老成,他们可能已获取功名,成为所在行业的中坚技术力量,成为公司保驾护航的盾牌,成为行业的革新者,引领着行业走向更高的赛道。

      阿里云 MVP就是一群技术领袖,拓展商业的边界,引领行业的变革,普惠更多的行业及开发者,这是阿里云MVP们的信念!

      一群热爱技术、纯粹的技术人,他们利用云计算、大数据、人工智能、区块链等等技术,让行业中的不可能变成了可能。

      ]]>
      Serverless 解惑——函数计算如何安装字体 Wed, 08 Apr 2020 03:03:11 +0800 0745C687-1851-464a-928B-2B0DE9FB5D56.png

      前言

      首先介绍下在本文出现的几个比较重要的概念:

      函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考
      buried_point
      Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考

      备注: 本文介绍的技巧需要 Fun 版本大于等于 3.6.7。

      函数计算运行环境中内置一些常用字体,但仍不满足部分用户的需求。如果应用中需要使用其它字体,需要走很多弯路。本文将介绍如何通过 Fun 工具将自定义字体部署到函数计算,并正确的在应用中被引用。

      你需要做什么?

      1. 在代码(CodeUri)目录新建一个 fonts 目录
      2. 将字体复制到 fonts 目录
      3. 使用 fun deploy 进行部署

      工具安装

      建议直接从这里下载二进制可执行程序,解压后即可直接使用。下载地址

      执行 fun --version 检查 Fun 是否安装成功。

      $ fun --version
      3.7.0

      示例

      demo 涉及的代码,托管在 github 上。项目目录结构如下:

      $ tree -L -a 1
      
      ├── index.js
      ├── package.json
      └── template.yml

      index.js 中代码:

      'use strict';
      
      var fontList = require('font-list')
      
      module.exports.handler = async function (request, response, context) {
          response.setStatusCode(200);
          response.setHeader('content-type', 'application/json');
          response.send(JSON.stringify(await fontList.getFonts(), null, 4));
      };

      index.js 中借助 node 包 font-list 列出系统上可用的字体。

      template.yml:

      ROSTemplateFormatVersion: '2015-09-01'
      Transform: 'Aliyun::Serverless-2018-04-03'
      Resources:
        fonts-service: # 服务名
          Type: 'Aliyun::Serverless::Service'
          Properties:
            Description: fonts example
          fonts-function: # 函数名
            Type: 'Aliyun::Serverless::Function'
            Properties:
              Handler: index.handler
              Runtime: nodejs8
              CodeUri: ./
              InstanceConcurrency: 10
            Events:
              http-test:
                Type: HTTP
                Properties:
                  AuthType: ANONYMOUS
                  Methods:
                    - GET
                    - POST
                    - PUT
      
        tmp_domain: # 临时域名
          Type: 'Aliyun::Serverless::CustomDomain'
          Properties:
            DomainName: Auto
            Protocol: HTTP
            RouteConfig:
              Routes:
                /:
                  ServiceName: fonts-service
                  FunctionName: fonts-function

      template.yml 中定义了名为 fonts-service 的服务,此服务下定义一个名为 fonts-function 的 http trigger 函数。tmp_domain 中配置自定义域名中路径(/)与函数(fonts-service/fonts-function)的映射关系。

      1. 下载字体

      你可以通过这里下载自定义字体 Hack,然后将复制字体到 fonts 目录。
      此时 demo 目录结构如下:

      $ tree -L 2 -a
      
      ├── fonts(+)
      │   ├── Hack-Bold.ttf
      │   ├── Hack-BoldItalic.ttf
      │   ├── Hack-Italic.ttf
      │   └── Hack-Regular.ttf
      ├── index.js
      ├── package.json
      └── template.yml

      2. 安装依赖

      $ npm install

      3. 部署到函数计算

      可以通过 fun deploy 直接发布到远端。
      image.png

      4. 预览线上效果

      fun deploy 部署过程中,会为此函数生成有时效性的临时域名:
      image.png

      打开浏览器,输入临时域名并回车:

      image.png


      可以看到字体 Hack 已生效!!!

      原理介绍:

      1. fun deploy 时,如果检测到 CodeUri 下面有 fonts 目录,则为用户在 CodeUri 目录生成一个 .fonts.conf 配置文件。在该配置中,相比于原来的 /etc/fonts/fonts.conf 配置,添加了 /code/fonts 作为字体目录。
      2. 自动在 template.yml 中添加环境变量,FONTCONFIG_FILE = /code/.fonts.conf,这样在函数运行时就可以正确的读取到自定义字体目录。

      如果依赖过大,超过函数计算的限制(50M)则:

      1. 将 fonts 目录添加到 .nas.yml
      2. 将 fonts 对 nas 的映射目录追加到 .fonts.conf 配置

      fun deploy 对大依赖的支持可参考《开发函数计算的正确姿势——轻松解决大依赖部署》

      总结

      你只需要在代码(CodeUri)目录新建一个 fonts 目录,然后复制所有字体到该目录即可。Fun 会自动帮你处理配置文件(.fonts.conf),环境变量以及大依赖场景的情况。如果大家在使用 Fun 的过程中遇到了一些问题,可以在 github 上提 issue,或者加入我们的钉钉群 11721331 进行反馈。

      阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术圈。”

      ]]>
      阿里云千万级架构的构建——阿里云 MVP 乔锐杰 Wed, 08 Apr 2020 03:03:11 +0800 乔帮主直播内容精炼整理、分以下5篇:
      一、分享介绍&架构三原则
      二、云架构、架构的原始阶段和基础阶段
      三、架构动静分离和分布式阶段
      四、架构数据缓存阶段和两个维度拓展阶段
      五、架构微服务阶段

      直接观看视频
      乔锐杰-阿里云千万级架构的构建.jpg

      分享简介

      本次分享阿里云千万级架构的构建——架构的成长演变之路,出自本人的一本书籍《「阿里云」运维架构实践秘籍》。

      书籍主要有四大篇:
      一、云端选型篇
      二、云端实践篇
      三、云端安全篇
      四、云端架构篇

      共十八个章节,所以我更喜欢把书的内容称为云端实践秘籍:“降云十八掌”。
      书籍已上架京东了,正在印刷出版中,大家感兴趣的可以下单预购阅读一下。书籍的内容总共有四五十万字,三百副运维架构图。我历时八年、累积了千家一线互联网企业、在云端实践的干货以及经验,包含了云端二十余款热门产品实践、五十余项常见开源热门技术实践,以及包含云端最热门的:监控、DevOps/容器、智能运维等技术实践。

      主题介绍

      本次的分享的主题内容,主要摘要书籍中的《第17章 云端千万级架构的演变》的精华内容。
      以下为内容提纲
      image.png

      我将通过案例及经验,给大家分享以下4点内容
      一、最原始的单机阶段,如何逐步演变发展到千万级的架构。
      二、架构的成长演变之路。
      三、作为架构师在设计架构的过程中需要注意的点。
      四、阿里云最热门及最新的技术趋势,让大家通往架构师之路少走些弯路。

      我们知道编程的本质是『确定性』,同样一段代码,在任何时候执行,结果应该是确定的,有bug也是确定的。而架构设计的本质是『不确定性』,同样的一个系统,不同公司不同架构师做出的设计差异可能很大,但是呢,都能正常运转。也就是说架构设计更多的是面对多种可能性时进行选择,架构设计领域也没有通用的规范,更多的是依赖每个架构师的经验和直觉。但是,它有3个共性原则隐含其中,分别是:合适原则、简单原则、演化原则。

      架构的三个原则

      首先来看看一个云端汽车官网的案例,主要是一个PHP网站,这是我之前接触到的一个真实的客户案例。
      image.png

      客户连续提了四个需求,很令人崩溃“一个简单的官网有多少人访问,难道还指望做成千万级架构的?”然而,客户还有理有据,说要高可用、要分布式保障业务稳定性等等,有什么问题?这个案例,侧面说明了客户并没有真正理解技术架构所核心依赖的业务场景,只是单纯的为了架构而架构。

      image.png

      (一)架构的第一个原则—合适原则:即合适优于技术本身

      前文汽车官网的案例,是否『高可用』,是否『分布式』,是否采用『热门技术』其实都不重要,重要的是合适。一个官网本身就是业务系统中的边缘系统,一个简单的PHP网站,我们用一台4核8G的云主机来部署即可。真正优秀的架构是核心结合业务情况,能够合理地将资源整合在一起并发挥出最大功效,并且能够快速落地。在架构设计过程中,我们很容易陷入技术本身的误区,而忽略我们业务的本质特点。一味追求最流行的技术架构、追随一线大厂技术架构、追寻大而全的技术架构是没有任何意义的。所以架构的本质就是取舍,不求最新,不求最全,只求最合适。

      (二)架构的第二个原则:简单原则:即简单优于复杂。

      复杂,这就意味着难度增加,不可控风险增加。如果保持简单,能让系统方便理解,方便扩展,并且耦合度降低。简单并不代表没有技术含量,反而简单的实现更为实用,比花哨设计也更能适应系统一步步演化。所以『复杂』在软件领域,代表的不是领先或者先进,而是『问题』。如果简单方案和复杂方案都可以满足要求,架构设计应该选择简单方案。
      image.png

      (三)架构的第三个原则:演化原则:『演化优于一步到位』

      一个好的架构是靠演变而来,而不是单纯的靠设计。我们刚开始做架构设计时,不可能全方位地考虑到架构的高性能、高扩展性、高安全等各方面的因素。随着业务需求越多越多、业务访问压力越多越大,架构不断地演变及进化,因而造就了一个成熟稳定的大型架构。如淘宝网、如Facebook等大型网站的架构,无不从一个小型规模架构,不断进化及演变成为一个大型网站架构。所以不要期望一步到位的设计一个软件架构,不要期望我们设计的一次性架构都能满足业务的不断变化。架构师在进行架构设计时需要牢记这个原则,时刻提醒自己不要贪大求全,或者盲目照搬大公司的做法。

      总结:合适原则>简单原则>演化原则。
      第一原则:合适原则,优先满足现有业务需求;
      第二原则:简单原则,选择简单方案快速落地验证;
      第三原则:演化原则,适当预测业务发展,在问题出现时及时演进。
      这三个原则是一体的,相辅相成的。

      聊完架构的三个原则,接下来我们聊一下云架构。
      下一篇:云架构、架构的原始阶段和基础阶段

      ]]>
      云架构、架构的原始阶段和基础阶段——阿里云 MVP乔锐杰 Wed, 08 Apr 2020 03:03:11 +0800 乔帮主的直播内容经精炼整理、分以下5篇:
      一、分享介绍&架构三原则
      二、云架构、架构的原始阶段和基础阶段
      三、架构动静分离和分布式阶段
      四、架构数据缓存阶段和两个维度拓展阶段
      五、架构微服务阶段

      直接观看视频

      云架构

      聊完架构的三个原则,接下来我们聊一下云架构。
      随着云计算的普及,在未来对IT资源的需求,都会全部通过云平台获取。那云对技术架构方面又有什么影响及变革呢?
      image.png

      我们先来看看IT三大体系发展方面:
      (1)、物理机体系阶段:传统IOE的架构,其实是物理机的典型代表。对计算资源的使用,需要我们去购买或者租用对应的硬件。

      (2)、然后到了云计算体系阶段:基于传统硬件服务器基础上,通过虚拟化及分布式技术形成对应的云资源平台。对计算资源的使用,我们如同使用水和电一样,在云资源平台上按需索取即可,而我们不用再关注和底层物理硬件打交道。

      (3)、最后到达容器体系阶段:我们既不用关注底层是物理硬件,也不用关注云平台用的是亚马逊、还是阿里云,我们业务都能无缝过渡及运行。让我们对计算资源的使用,脱离硬件、甚至各个云平台的依赖。容器就是大家熟知的Docker技术,有点类似Java的JVM,可以跨平台部署,不依赖底层环境,不管是硬件环境还是云平台等等。

      这三大IT体系,那在IT技术架构方面又有什么样的变化呢(看一下图中的箭头方向,也就是我们技术架构演变进化的方向)
      由物理机体系中的单机架构、集群架构,演变到云计算体系中的分布式架构,然后最终演变到容器体系中的微服务架构。

      接下来主要通过现如今最流行的Web类应用的案例场景,跟大家详细分享在云端如何从一个单机的简单架构,演变成千万级的大型架构。

      架构的最原始阶段:万能的单机

      image.png

      架构的最原始阶段,即一台ECS服务器搞定一切,我们叫万能的单机。传统官网、论坛等应用,只需要一台ECS。对应的 Web服务器、数据库、静态文件资源等,都部署到一台ECS上即可。一般5万PV到30万PV访问量,结合内核参数调优、结合web应用性能参数优化、以及结合数据库调优,基本上都能稳定的运行。在架构原始阶段,采用的云产品,仅仅采用一台ECS部署即可。该阶段技术特点主要有两个:
      一个是应用服务器+数据库+文件数据都部署在一起,另外一个是在传统物理机IT体系下,小型机是单机架构的初期典型代表,用一台小型机(都是高配、几百G的内存)主要部署核心应用及业务。

      那当我们访问压力增大,就进入下一阶段,架构的基础阶段:物理分离Web和数据库。

      架构的基础阶段:物理分离Web和数据库

      image.png

      当我们访问压力达到50万PV到100万PV的时候,部署在一台服务器上面的web应用及数据库等服务应用,会对服务器的系统资源CPU/内存/磁盘/带宽进行争抢。显然单机已经出现性能瓶颈。我们将web应用和数据库物理分离单独部署,解决性能资源争抢的问题。

      在云端我们主要把数据库单独剥离出来,部署在RDS中即可,所以这阶段采用的云产品,相比上阶段,主要增加了RDS。那架构基础阶段的技术特点是什么呢?主要有两点:
      第一点
      在架构原始阶段,我们用一台服务器部署应用服务、数据库、文件数据。随着压力增加,我们架构的演变其实也就是解耦部署的应用服务、数据库、文件数据。物理分离是架构解耦的开始,即解耦应用服务和数据库。解耦战术,其实也决定了架构的高度。

      第二点
      虽然Web应用服务和数据库分离,但是web应用服务和数据库都还是单点部署,整体存在单点故障问题,即web应用服务或数据库宕机,就影响业务正常访问了。

      所以在我们传统物理机体系中:IOE的单机架构(即:IBM的小型机、和Oracle数据库、和EMC存储),是单机架构中典型的代表。都是高配性能的计算资源,在这个阶段的架构,业务基本都是单机部署。有时候数据库和业务代码都甚至部署在一台高配机器上,完全要靠单机的硬件性能来支持更多业务访问。

      当访问压力进一步增大,就到达了架构动静分离阶段:静态缓存 + 对象存储阶段。
      下一篇:架构动静分离和分布式阶段

      ]]>
      架构数据缓存阶段和两个维度拓展阶段 Wed, 08 Apr 2020 03:03:11 +0800 乔帮主的直播内容经精炼整理、分以下5篇:
      一、分享介绍&架构三原则
      二、云架构、架构的原始阶段和基础阶段
      三、架构动静分离和分布式阶段
      四、架构数据缓存阶段和两个维度拓展阶段
      五、架构微服务阶段

      直接观看视频

      数据缓存阶段:数据库缓存

      image.png

      当访问压力达到500万PV到1000万PV,虽然负载均衡结合多台 Web服务器,解决了动态请求的性能压力。但是这时候我们发现,数据库出现压力瓶颈,常见的现象就是RDS的连接数增加并且堵塞、CPU100%、IOPS飙升。这个时候我们通过数据库缓存,有效减少数据库访问压力,进一步提升性能。

      在这个架构阶段采用的云产品,如左边架构图所示,相比上阶段,主要增加了云memcache或者云Redis。值得注意的是,数据库缓存,需要业务代码改造及支持。哪些热点数据需要缓存,这需要业务方面重点规划。并且云端数据库缓存,已不推荐在ECS中自行搭建Redis、memcache等,我们直接使用对应的云缓存产品即可。这阶段架构有两个技术特点跟大家分享:

      一个是缓存五分钟法则:即如果一条记录频繁被访问,就应该考虑放到缓存里。否则的话客户端就按需要直接去访问数据源,而这个临界点就是五分钟。

      第二个特点,缓存其实是在数据库层面,对数据库的读写分离中,对读的一定程度解耦。

      接着到达架构扩展阶段:垂直扩展。

      架构扩展阶段:垂直扩展

      image.png

      当访问量达到1000万PV到5000万PV,虽然这个时候我们可以看到通过分布式文件系统OSS已经解决了文件存储的性能问题,虽然通过CDN已经解决静态资源访问的性能问题。但是当访问压力再次增加,这个时候 Web服务器和数据库方面依旧是瓶颈。在此我们通过垂直扩展,进一步切分 Web服务器和数据库的压力,解决性能问题。“那何为垂直扩展,按照不同的业务(或者数据库)切分到不同的服务器(或者数据库)之上,这种切分称之为垂直扩展。”如左边架构图所示,在这个架构阶段采用的云产品,相比上阶段,主要增加了数据库层面的读写分离或者对应的切库。

      这个阶段主要有三个技术特点:
      第一点,业务拆分
      在业务层,可以把不同的功能模块拆分到不同的服务器上面进行单独部署。比如,用户模块、订单模块、商品模块等,拆分到不同服务器上面部署。

      第二点,读写分离
      在数据库层,当结合数据库缓存,数据库压力还是很大的时候。我们通过读写分离的方式,进一步切分及降低数据库的压力。

      第三点,分库
      结合业务拆分、读写分离,在数据库层,比如我们同样可以把用户模块、订单模块、商品模块等。所涉及的数据库表:用户模块表、订单模块表、商品模块表等,分别存放到不同数据库中,如用户模块库、订单模块库、商品模块库等。然后把不同数据库分别部署到不同服务器中。

      此阶段架构有两个注意点:
      架构的中后期,业务的瓶颈往往都是集中在数据库层面。因为在业务层,我们通过负载均衡能够快速添加更多服务器进行水平扩容,很方便快速解决业务服务器处理的压力。而数据库怎么来做性能扩展,不是简单加几台服务器就能解决的,这往往涉及到复杂的数据库架构变更。垂直拆分,相对在业务上改动较少,并且数据库性能提升最为高效的一种方式,是我们中大型应用首要采用的架构方案。

      接着到达架构分布式+大数据阶段:水平扩展。

      架构分布式+大数据阶段:水平扩展

      image.png

      当访问量达到5000万PV及以上时,当真正达到千万级架构以上访问量的时候,我们可以看到垂直扩展的架构也已经开始“山穷水尽”。比如,读写分离仅解决读的压力,面对高访问量,在数据库“写”的压力上面开始“力不从心”,出现性能瓶颈。另外,分库虽然将压力拆分到不同数据库中。但单表的数据量达到TB级别以上,显然已经达到传统关系型数据库处理的极限。如左边架构图所示,在这个架构阶段采用的云产品,相比垂直扩展阶段,主要增加了DNS轮询解析、非关系型数据库MongoDB、以及表格存储列数据库OTS。

      此阶段有四个技术特点:
      第一点,增加更多的web应用服务器
      当后续压力进一步增大,在负载均衡后面增加更多的web应用服务器进行水平扩展。

      第二点,增加更多的SLB
      单台SLB也存在单点故障的风险,及SLB也存在性能极限,如七层SLB的QPS最大值为50000。我们可以通过DNS轮询,将请求轮询转发至不同可用区的SLB上面,实现SLB水平扩展。

      第三点,采用分布式缓存
      虽然阿里云memcache内存数据库已经是分布式结构,保障了存储在缓存中的数据高可用。但是使用缓存的业务场景是比较多的,我们不可能仅仅只是部署一个缓存服务,那么业务之间的使用可能存在相互影响。所以我们用多个云缓存,可以在代码层通过hash算法将数据分别缓存至不同的云缓存中,或者不同的业务场景直接连接使用不同的云缓存,来提高我们业务的健壮性。

      第四点,Sharding分片和NoSQL
      面对高并发、大数据的需求,传统的关系型数据库已不再适合,需要采用非关系型数据库NoSQL了。MongoDB和OTS作为NoSQL的典型代表,支持数据分片Sharding技术,根本上解决了关系型数据库面对高并发高存储量的痛点问题。

      此阶段架构注意点:
      大型应用中,海量业务数据的存储、分析给我们数据库提出巨大挑战。而传统关系型数据库明显已经力不从心,分布式数据库NoSQL是适用技术发展的未来趋势。有了海量业务数据的基础,我们可以结合云端MaxCompute大数据分析服务,来辅助业务进行价值创造。

      虽然到达架构分布式+大数据阶段,基本上能满足绝大多数千万级架构的业务需求。但同时带来新的挑战:
      image.png

      第一个挑战,分布式架构下,在业务层面的扩展,业务后期迭代、维护管理、敏捷开发等问题。所以借助前面的“云对技术架构变革”的这个图我们再次来看看。其实最后到达了容器体系下,微服务架构阶段解决了分布式架构带来的业务层面扩展性问题。微服务是业务功能层面的一种切分,切分成一个单个小型的独立业务功能的微服务。多个微服务通过API Gateway提供统一服务入口,让微服务对前台透明,而每个微服务也可以通过分布式架构进行部署,这给我们研发灵活性、业务后期迭代带来了极大的扩展性,这是我们未来软件技术架构的主流。并且微服务,在云平台基础上结合Docker容器技术进行部署。能让业务、运维、架构在技术和非技术方面的稳定性、成本、效率、扩展等都能达到完美。

      第二个挑战,Big Data所带来的离线计算问题。Big Data强调数据量,PB级以上,是静态数据。并且强调离线计算,结算结果并不是实时的,需要等到离线任务执行完毕才能汇总结果。随着企业数据需求不断变化,近年来对数据采集的实时性、对数据在线计算的实时性要求日益明显。现如今,基于Big Data海量数据的基础上 ,已经在逐步过渡到大数据实时计算分析的Fast Data时代了。Fast Data在数据量的基础上,意味着速度和变化,意味着客户可以更加实时化、更加快速地进行数据处理。

      最终演变成为当前最热门的微服务、容器、Fast Data架构。
      下一篇:架构微服务阶段

      ]]>
      架构微服务阶段:容器、Fast Data架构——阿里云 MVP乔锐杰 Wed, 08 Apr 2020 03:03:11 +0800 乔帮主的直播内容经精炼整理、分以下5篇:
      一、分享介绍&架构三原则
      二、云架构、架构的原始阶段和基础阶段
      三、架构动静分离和分布式阶段
      四、架构数据缓存阶段和两个维度拓展阶段
      五、架构微服务阶段

      架构微服务阶段:容器、Fast Data架构

      image.png

      在微服务阶段,结合容器技术,未来业务跨云平台分布式架构才是最主流的形态。如左边架构图所示,在这个架构阶段采用的云产品,相比水平扩展的分布式阶段,主要增加了DNS智能解析、时序数据库InfluxDB、阿里云容器服务kubernetes(ACK)、数据库传输服务DTS。

      这阶段有四个核心技术特点。
      第一点:我们通过容器技术DOCKER+K8S,让业务部署跨平台,不依赖云平台或者底层物理环境。

      第二点:通过DNS智能解析,我们能将用户请求分别调度转发到不同平台中。说到DNS智能解析,其实CDN的就近访问核心功能就是依靠CDN的智能解析。

      第三点:在跨云平台分布式架构中,最难的技术点其实就是核心技术点,就是数据同步。我们把业务部署在不同平台上,意味着数据在不同地域上,我们怎么样保障不同地域连接的数据库的数据做实时同步,保障数据一致性呢。阿里云有DTS成熟的同步方案,加上专线高速通道解决数据传输速度等问题,这也是这方面较成熟的解决方案。

      第四点:采用时序数据库InfluxDB,实现海量数据实时采集及存储,并且实现海量数据的实时查询计算。具体我们看一个FAST DATA的案例:驻云DataFlux产品功能架构图。
      image.png

      驻云DataFlux当然也是采用微服务+容器的架构。在这里主要跟大家分享的是fast data技术特点,即如何体现在fast上?如图架构图,我主要说三个实时的技术特点,让大家理解fast data阶段的技术特性。
      第一,数据的采集上,DataFlux可以采集各类业务场景数据源,并且是实时的采集的。采集器有驻云自主研发的datakit、wdf、pdf采集器,也支持telegraf、prometheus等热门采集器。这是第一个实时性。

      第二,数据实时采集后,通过数据网关(类似zabbix proxy代理),上报到数据处理开发平台,进行实时处理。

      第三,最为核心的实时性特点,数据经过实时处理后,核心通过influxdb时序数据库进行统一存储。

      Influxdb是个分布式数据库,读写速度能轻松达到秒级千万级数据量。特别是在查询分析上这是influxdb的核心优势。利用这第三个实时性的特点,我们把数据输出到数据洞察等多种企业级应用场景中,能轻松应对大规模海量数据的实时分析。这也influxdb为什么适合物联网IOT海量数据读写的重要原因,也基本上是物联网IOT架构中不可缺少的技术环节。

      通过“Fast Data”的一个案例,本次围绕“阿里云千万级架构构建”的主题分享就这些,更多精彩可购阅我新出版书籍哦。
      image.png

      ]]>
      阿里云发布 Spring Boot 新脚手架,真香 Wed, 08 Apr 2020 03:03:11 +0800 1.png

      作者 | 良名  阿里巴巴技术专家

      背景

      相信很多人都使用过 start.spring.io 来初始化自己的 Spring Boot 工程,这个工具为开发者提供了丰富的可选组件,并且可以选择多种打包方式,大大方便了开发人员的使用。最近,阿里的 Nacos、Sentinel 也进入 start.spring.io 的选项中,进一步的方便开发者使用阿里云的产品。

      2.png

      但是,生成的工程骨架中,只有组件坐标信息,缺少对应的使用方法和 Demo 代码;于是,开发者还是需要去寻找相关使用教程,或者样例代码;如果找的不对,或者是版本不匹匹配,还需要花费不少时间去排查和解决问题;这些问题都在无形中增加用户的工作量。

      我们将对软件工程的抽象层次自上而下进行切分,会得到如下的几个层级:行业、解决方案、应用、功能、组件;明显的, start.spring.io 目前只能提供组件级别的支持。再将组件这层展开,会发现这样一个生命周期:组件引入、组件配置、功能开发、线上运维。start.spring.io 也只实现了“组件引入”这一功能。

      我们的目标是“让阿里云成为广大 Java 开发者最好用的云”。要实现这个目标,是否可以再向前走几步,在解决“组件引入”问题的基础上,将组件的典型使用方法、样例代码、使用说明也加入到工程中呢?

      基于这种思考,我们上线了自己的 bootstrap 站点 start.aliyun.com :

      https://start.aliyun.com/

      当然,本着不重复造轮子的原则,我们不再构建一套工程生成底层框架,而是使用 Spring Initializr 来实现这部分功能。在此之上专注于增加新特性,实现服务广大开发者的目标。

      Spring Initializr:https://github.com/spring-io/initializr

      在 start.aliyun.com 中,我们为广大开发者带来了如下便利特性:

      • 为每个组件提供了单独的 DemoCode 和对应的配置样例(本次已发布);
      • 工程内置说明,减少用户查找文档的困难(部分实现);
      • 开发者只需要做减法,而非加法的使用方式(部分实现);
      • 提供多组件集成的解决方案(开发中);
      • 定期跟进 start.spring.io 的更新,方便大家使用到 spring 的最新功能。

      start.aliyun.com:https://start.aliyun.com/

      未来,我们还需要再助力开发者这条路上继续发力,不仅仅是做好组件集成的工作,还要需要继续向上支持,提供更多功能、服务、应用层级的快速构建能力。

      本文,围绕 spring initializr 框架,以 start.spring.io 为例,全面的给大家介绍如何使用和扩展这个框架,以及背后的运行原理。

      使用篇

      由于 spring-initializr 提供了灵活的扩展能力,以及丰富的默认实现;其使用方式也是非常的灵活多变;为了便于说明,我们直接通过 start.spring.io ,看看 Spring 自己是怎么使用这套框架的。

      1. 基本用法

      基本用法的原则,是尽量少写代码,甚至是不写代码。只通过配置就可以实现 initializr 工程的创建。

      依赖引入

      要使用 spring-initializr ,首先要引入这套框架。很简单,直接依赖 bom 即可:

      <dependencyManagement>
          <dependencies>
              <dependency>
                  <groupId>io.spring.initializr</groupId>
                  <artifactId>initializr-bom</artifactId>
                  <version>0.9.0.BUILD-SNAPSHOT</version>
                  <type>pom</type>
                  <scope>import</scope>
              </dependency>
          </dependencies>
      </dependencyManagement>

      有了这个 bom 依赖,我们就不用再关心内部组件的版本等信息了。

      一般来说,我们还需要引入具体组件:

      <dependency>
                  <groupId>io.spring.initializr</groupId>
                  <artifactId>initializr-generator-spring</artifactId>
              </dependency>
              <dependency>
                  <groupId>io.spring.initializr</groupId>
                  <artifactId>initializr-version-resolver</artifactId>
              </dependency>
              <dependency>
                  <groupId>io.spring.initializr</groupId>
                  <artifactId>initializr-web</artifactId>
              </dependency>

      具体每个子模块的用途,这里列出来,供读者参考:

      • initializr-actuator: 监控诊断的附加信息,这个暂时忽略;
      • initializr-bom: 便于外部使用的bom依赖;
      • initializr-docs: 使用文档;
      • initializr-generator: 核心工程生成库;
      • initializr-generator-spring: 用于生成典型的spring boot工程;
      • initializr-generator-test: 测试框架;
      • initializr-metadata: 项目各个方面的元数据基础结构;
      • initializr-service-sample: 基本使用案例;
      • initializr-version-resolver:版本号解析能力;
      • initializr-web: 提供给三方客户端使用的web入口。

      基本配置

      • 完成了框架引入,就需要做一些基础配置了
      • 支持哪些语言:Java、groovy、Kotlin
      • 支持哪些版本:1.8、11、13
      • 支持哪些打包方式:jar、war

      将这些信息全部配置到 application.yml 文件中,如下:

      initializr:
          packagings:
          - name: Jar
            id: jar
            default: true
          - name: War
            id: war
            default: false
        javaVersions:
          - id: 13
            default: false
          - id: 11
            default: false
          - id: 1.8
            name: 8
            default: true
        languages:
          - name: Java
            id: java
            default: true
          - name: Kotlin
            id: kotlin
            default: false
          - name: Groovy
            id: groovy
            default: false

      其中 name 是可选的, id 是必填的。

      每个配置项下,可以有一个默认值(将 default 这是为 true 即可),除了这些基本配置,我们还需要定义可以支持的项目类型:

      initializr:
          types:
          - name: Maven Project
            id: maven-project
            description: Generate a Maven based project archive.
            tags:
              build: maven
              format: project
            default: true
            action: /starter.zip
          - name: Maven POM
            id: maven-build
            description: Generate a Maven pom.xml.
            tags:
              build: maven
              format: build
            default: false
            action: /pom.xml
          - name: Gradle Project
            id: gradle-project
            description: Generate a Gradle based project archive.
            tags:
              build: gradle
              format: project
            default: false
            action: /starter.zip
          - name: Gradle Config
            id: gradle-build
            description: Generate a Gradle build file.
            tags:
              build: gradle
              format: build
            default: false
            action: /build.gradle

      默认情况下, initializr 已经支持 4 种项目类型:

      • /pom.xml 生成一个 Maven 的 pom.xml 配置文件
      • /build.gradle 生成 Gradle 的配置文件
      • /starter.zip 生成 zip 方式压缩的工程文件
      • /starter.tgz 生成以 tgz 方式压缩的工程文件

      通过 tags 标签,我们可以定义不同配型的编译方式 (build) 和打包格式(format)。

      配置基本依赖

      完成了基本配置以后,就可以配置可选的依赖组件了。

      依赖配置以 dependency 为 key ,同样配置在 application.yml 的 initializr 下面,这里给出一个简单的样例:

      initializr:
        dependencies:
          - name: Web
            content:
              - name: Web
                id: web
                description: Full-stack web development with Tomcat and Spring MVC
              - name: Developer Tools
            content:
              - name: Spring Boot DevTools
                id: devtools
                groupId: org.springframework.boot
                artifactId: spring-boot-devtools
                description: Provides fast application restarts, LiveReload, and configurations for enhanced development experience.
              - name: Lombok
                id: lombok
                groupId: org.projectlombok
                artifactId: lombok
                description: Java annotation library which helps to reduce boilerplate code.

      dependencies 下定义分组。分组的作用是便于展示和快速查找,所以不需要 id ,只需要 name 信息;每个分组的 content 是分组的具体内容,也就是这个分组下的组件定义;支持以列表形式定义多个;另外,每个分组都可以设置当前分组内组件公用的配置信息。

      每一依赖,包含如下的基本信息:

      • id:组件的唯一标识符
      • groupId & artifactId:组件的坐标
      • name:显示名称
      • description:描述信息,主要用于展示用途
      • version:组件版本

      关于 groupId & artifactId:如果设置了坐标,生成的项目里会使用这里的坐标定位组件;但是如果没有设置坐标,框架会认为这是一个标准的 spring-boot 组件,自动添加 spring-boot-starter-{id} 作为生成的依赖坐标。

      关于 version:如果直接在组件上设置版本信息,框架会直接使用这个值作为组件依赖的版本;但是很多时候,组件的版本会受到 spring-boot 版本的影响,此时就需要对版本做特殊的定义 & 管理。

      配置依赖版本管理

      这里需要先了解一下版本命名规则:一个典型的版本,一般包含如下 4 个信息:大版本、小版本、修正版本、版本限定符。

      版本范围有一个上界和下界,可以方括号 [] 或者圆括号 () 表示。方括号代表上下界的闭区间,圆括号代表上下界的开区间。

      例如:“[1.1.6.RELEASE,1.3.0.M1)”代表所有从 1.1.6.RELEASE 到 1.3.0.M1 之间所有的版本(包含 1.1.6.RELEASE ,但不包含 1.3.0.M1 )。

      同时,可以使用单一版本号作为版本范围,例如 “1.2.0.RELEASE”。单一版本号的版本范围代表“从这个版本以及之后的所有版本”。

      如果需要使用“最新的 Release 版本”的概念,可以使用一个字母 x 代表具体的版本号。

      例如, 1.4.x.BUILD-SNAPSHOT 代表 1.4.x 的最新快照版本。

      再比如:如果需要表达,从 1.1.0.RELEASE 到 1.3.x 之间的所有版本,可以用[1.1.0.RELEASE,1.3.x.RELEASE]来表达。

      另外,版本限定符也是有顺序的(升序):

      • M:里程碑版本
      • RC:发布候选版本
      • RELEASE:发布版本
      • BUILD-SNAPSHOT:为开发构建的快照版本

      所以快照版本是所有限定符里优先级最高的。假设某个组件需要 Spring Boot 的最新版本,可以使用 1.5.x.BUILD-SNAPSHOT  (假设 1.5 版是 Spring Boot 的最新版本)。

      最后,版本范围中讨论的版本,指的都是 Spring Boot的版本,而不是组件自己的版本。

      前面介绍了,可以使用 version 属性定义组件的具体版本号;但是,如果组件版本与Spring Boot 的版本存在关联关系,就需要使用 compatibilityRange 来配置依赖的版本范围。

       compatibilityRange 可以定义在两个地方:

      • 直接定义在组件(或 Bom )上

      这种定义方式,代表组件只支持  Spring Boot 的某一个版本范围,例如下面的配置:

      initializr:
        dependencies:
          - name: Stuff
            content:
              - name: Foo
                id: foo
                ...
                compatibilityRange: 1.2.0.M1
              - name: Bar
                id: bar
                ...
                compatibilityRange: "[1.5.0.RC1,2.0.0.M1)"

      Foo 可以支持 Spring boot 1.2.0  之后的所有版本;而Bar只能支持 Spring Boot 1.5.0  到  2.0.0 之间的版本,且不包含 2.0.0 ;

      • 定义在组件的 mappgin 属性下

      可以支持在 Spring Boot 不同版本之下对组件做不同的设置(可以重置组件部分或者是所有的属性),下面的例子中对 artifactId 做了特殊定义:

      initializr:
        dependencies:
          - name: Stuff
            content:
              - name: Foo
                id: foo
                groupId: org.acme.foo
                artifactId: foo-spring-boot-starter
                compatibilityRange: 1.3.0.RELEASE
                mappings:
                  - compatibilityRange: "[1.3.0.RELEASE,1.3.x.RELEASE]"
                    artifactId: foo-starter
                  - compatibilityRange: "1.4.0.RELEASE"

      这个例子中, foo 在 Spring Boot 的 1.3 使用 foo-starter 作为坐标的 artifactId ;在 1.4.0.RELEASE 以及之后的版本中,还是使用 foo-spring-boot-starter 作为 artifactId 的值;

      使用 Bom 管理版本:有时候,需要使用 Bom 的方式管理组件版本;此时不需要对组件单独设置版本号。

      要使用 Bom ,首先要配置 Bom 定义:

      initializr:
        env:
          boms:
            my-api-bom:
              groupId: org.acme
              artifactId: my-api-dependencies
              version: 1.0.0.RELEASE
              repositories: my-api-repo-1

      注意:Bom 信息,定义在 initializr.env.boms下面。

      其属性和依赖组件基本一致,都是坐标、版本;同时, Bom 也支持版本范围管理。

      完成了 Bom 的定义,就需要在组件中引用 Bom :

      initializr:
        dependencies:
          - name: Other
            content:
              - name: My API
                id : my-api
                groupId: org.acme
                artifactId: my-api
                bom: my-api-bom

      一旦用户选择了 my-api 组件,框架会自动为生成的项目添加了 my-api-dependencies 的 Bom 依赖;

      2. 高级定制

      启用缓存

      如果你启动过 start.spring.io 项目,你会在日志里发现这样的输出 “Fetching boot metadata from spring.io/project_metadata/spring-boot” 为了避免过于频繁的检查 Spring Boot 版本,官方是建议配合缓存一起使用。

      首先需要引入缓存框架:

      <dependency>
          <groupId>javax.cache</groupId>
          <artifactId>cache-api</artifactId>
      </dependency>
      <dependency>
          <groupId>org.ehcache</groupId>
          <artifactId>ehcache</artifactId>
      </dependency>

      然后,在 SpringBootApplication 类上增加 @EnableCaching 注解:

      3.png

      如果需要自己定义缓存,可以调整如下缓存配置:

      4.png

      增加 Demo代码:由于不同的组件有不同的功能,如果需要为项目增加 Demo 代码。

      为不同的组件增加独立配置:还记得原理篇中提到的 spring.factories 吗?对,我们要增加自己的配置项,就需要在这里增加针对不同组件样例代码的扩展入口。

      io.spring.initializr.generator.project.ProjectGenerationConfiguration=
      com.alibaba.alicloud.initializr.extension.dependency.springboot.SpringCloudProjectGenerationConfiguration

      在 SpringCloudProjectGenerationConfiguration 中,我们通过 ConditionalOnRequestedDependency 注解来识别不同组件:

      @ProjectGenerationConfiguration
      public class SpringCloudAlibabaProjectGenerationConfiguration {
          private final InitializrMetadata metadata;
          private final ProjectDescription description;
          private final IndentingWriterFactory indentingWriterFactory;
          private final TemplateRenderer templateRenderer;
          public SpringCloudAlibabaProjectGenerationConfiguration(InitializrMetadata metadata,
                                                                  ProjectDescription description,
                                                                  IndentingWriterFactory indentingWriterFactory,
                                                                  TemplateRenderer templateRenderer) {
              this.metadata = metadata;
              this.description = description;
              this.indentingWriterFactory = indentingWriterFactory;
              this.templateRenderer = templateRenderer;
          }
          @Bean
          @ConditionalOnRequestedDependency("sca-oss")
          public OSSDemoCodeContributor ossContributor() {
              return new OSSDemoCodeContributor(description, templateRenderer);
          }
          ......
      }

      上面的代码,会在选择了 sca-oss 组件时,创建一个 OSSDemoCodeContributor 用于对应 Demo 代码的生成。

      生成具体的 Demo 代码:继续以 OSSDemoCodeContributor 为例,它是一个 ProjectContributor ,会在项目文件空间创建完成了调用。我们需要为这个 Contributor 在实例化时增加生成过程中需要的元数据信息,例如 ProjectDescription 。

      代码生成过程,比较简单,可以直接复用框架中就提供的 mstache 模板引擎。

      我们直接将 Demo 代码,以模板的形式,放置在 resources 文件夹之下:

      5.png

      然后,我们再通过模板引擎,解析这些模板文件,再拷贝到项目目录下即可:

      private void writeCodeFile(TemplateRenderer templateRenderer, Language langeuage,
                                     Map<String, Object> params, Path path, String temp) throws IOException {
              ......
              Path pkgPath = 生成包路径
              Path filePath = 成成代码文件路径
              // 渲染模板
              String code = templateRenderer.render(temp, params);
              // demo 文件写入
              Files.createDirectories(pkgPath);
              Files.write(filePath, code.getBytes("UTF-8"));
          }

      除了模板代码以外,我们通常还需要在 applicatioin.properties 文件写入模块的配置信息。

      这里,我们依然可以使用代码生成的方式:创建模板、解析模板,追加文件的方式来实现。具体代码这里就不贴了,读者可以自己发挥。

      原理篇

      原理篇,主要介绍 spring.initializr 是如何实现项目工程构建的,以及作为一个框架,如何提供丰富的扩展能力的。

      在原理篇,我们将 initializr 的执行分为两个阶段:启动阶段和生成阶段。

      • 启动阶段:启动应用,加载配置,扩展信息初始化;
      • 生成阶段:一个项目生成,从收到请求,到返回内容的完整流程。

      1. 启动阶段

      再开始启动流程之前,先要看一下 initializr 的扩展体系。

      整个架构大量使用了 spring 的 spi 机制,我们来看一下一共有哪些 spring.factories :

      • initializr-generator/src/main/resources/META-INF/spring.factories
      • initializr-generator-spring/src/main/resources/META-INF/spring.factories
      • initializr-web/src/main/resources/META-INF/spring.factories
      • initializr-actuator/src/main/resources/META-INF/spring.factories
      • start-site/src/main/resources/META-INF/spring.factories

      其中只有一个在 start.spring.io 中,其他 4 个都在 initializr 工程中(各 spring.factories 的具体内容见参考资料)。

      不过要注意,这些 spring.factories 定义,仅仅代表了各个 SPI 有哪些扩展。不同spi的实现创建和使用完全是在不同的阶段进行的。

      在应用启动阶段,其实只有一个 spi 会被加载(暂不考虑 actuator):io.spring.initializr.web.autoconfigure.InitializrAutoConfiguration 。

      @Configuration
      @EnableConfigurationProperties(InitializrProperties.class)
      public class InitializrAutoConfiguration {
          @Bean
          @ConditionalOnMissingBean
          public ProjectDirectoryFactory projectDirectoryFactory()
          @Bean
          @ConditionalOnMissingBean
          public IndentingWriterFactory indentingWriterFactory()
          @Bean
          @ConditionalOnMissingBean(TemplateRenderer.class)
          public MustacheTemplateRenderer templateRenderer(Environment environment, ObjectProvider<CacheManager> cacheManager)
          @Bean
          @ConditionalOnMissingBean
          public InitializrMetadataUpdateStrategy initializrMetadataUpdateStrategy(RestTemplateBuilder restTemplateBuilder,
                  ObjectMapper objectMapper)
          @Bean
          @ConditionalOnMissingBean(InitializrMetadataProvider.class)
          public InitializrMetadataProvider initializrMetadataProvider(InitializrProperties properties,
                  InitializrMetadataUpdateStrategy initializrMetadataUpdateStrategy)
          @Bean
          @ConditionalOnMissingBean
          public DependencyMetadataProvider dependencyMetadataProvider()
          @Configuration
          @ConditionalOnWebApplication
          static class InitializrWebConfiguration {
              @Bean
              InitializrWebConfig initializrWebConfig()
              @Bean
              @ConditionalOnMissingBean
              ProjectGenerationController<ProjectRequest> projectGenerationController(
                      InitializrMetadataProvider metadataProvider, ApplicationContext applicationContext)
              @Bean
              @ConditionalOnMissingBean
              ProjectMetadataController projectMetadataController(InitializrMetadataProvider metadataProvider,
                      DependencyMetadataProvider dependencyMetadataProvider)
              @Bean
              @ConditionalOnMissingBean
              CommandLineMetadataController commandLineMetadataController(InitializrMetadataProvider metadataProvider,
                      TemplateRenderer templateRenderer)
              @Bean
              @ConditionalOnMissingBean
              SpringCliDistributionController cliDistributionController(InitializrMetadataProvider metadataProvider)
          }
      }

      这里会做如下几件事情:

      • 初始化元数据 Provider
      • 创建模板引擎
      • 创建目录、缩进工厂
      • 初始化 web 配置
      • 创建 spring mvc 的 web 入口
      • 各种 ProjectGenerationController

      其中最关键的元数据加载部分,使用了 EnableConfigurationProperties 注解,将 spring 环境中的配置项写到 InitializrProperties 上:

      6.png

      在 application.yml 文件中,可以找到如下的配置信息,这里就是实际的项目依赖关系元数据的配置存储点:

      7.png

      整体来看,启动阶段的动作还是比较简单的,这也是为什么 start.spring.io 启动只需要数秒的原因。

      更多的逻辑,都被留在了工程生成阶段。

      2. 生成阶段

      生成阶段,spring-initializr 使用了一个很有意思的实现方式:initializr 框架会为每一次项目生成,创建一个独立的 context 用于存放生成流程中需要使用到的各种 bean 。

      先来一张时序图:

      8.png

      • 蓝色的类,是在应用启动阶段就完成了创建和数据填充;其生命周期和整个应用一致;
      • 黄色的类,会在具体的项目构建过程中生成;其生命周期在一次项目生成流程之内结束。

      从上面的时序图中可以看出:一个典型的创建行为,通常从 ProjectGenerationController收到web端的创建请求开始,通过 ProjectGenerationInvoker 这个中间层转换,最终进入 ProjectGenerator 的核心构建流程。

      主干流程

      下图,是 ProjectGenerator 的核心构建流程:

      9.png

      106 行,通过 contextFactory 构建了一个新的 ProjectGenerationContext 。

      看一下这个context的继承关系,原来于spring提供的AnnotationConfigApplicationContext 。

      再结合 110 行的 refresh() 方法,是不是发现了什么?就是 spring 的 ApplicationContext 的刷新流程。

      10.png

      107 行的 resolve 方法,向 context 中注册了一个 ProjectDescription的Provider,代码如下:

      10(1).png

      由于注册的是 Provider ,所以这段逻辑会在 Context 执行 refresh 时运行。

      这里的 ProjectDescriptionCustomizer 就是针对 ProjectDescription 的扩展,用于对用户传入的 ProjectDescription 做调整。这里主要是一些强制依赖关系的调整,例如语言版本等。

      这时候再看 108 行,这里向 Context 注册一个 Configuration 。

      那么这个 Configuration 包含了什么内容呢?一起来看下面这段代码:

      11.png

      ProjectGenerationConfiguration!!!前面提到的 spring.factories 中有很多这个 SPI 的实现(参见参考资料)。

      原来,initializr 的整个扩展体系,在这里才开始创建实例;

      ProjectGenerator 的 109 行,对一个 consumer 做了 accept 操作;其实就是调用了下面的代码:

      12.png

      这里通过 setParent 将应用的主上下文设置为这次 ProjectGenerationContext 的父节点。

      并且向这次 ProjectGenerationContext 中注册了元数据对象。

      最后,在 ProjectGenerator 的 112 行,调用了 projectAssetGenerator 的 generate 方法,实现如下:

      13.png

      通过上面的代码可以发现,这里对实际的工程构建工作,其实就是很多的 ProjectContributor 共同叠加;

      至此,主干流程已经结束了。

      我们可以发现,在主干流程中,没有做任何写文件的操作(只创建了根文件夹);它仅仅是定义了一套数据加载、扩展加载的机制与流程,将所有的具体实现都作为扩展的一部分。

      扩展流程

      spring-initializr 提供了 2 种主要扩展途径:ProjectContributor 和 xxxxxCustomizer。

      14.png

      从方法签名就可以看出,入参只有一个项目的根路径,其职责就是向这个路径下些人项目文件。这个扩展点非常的灵活,几乎可以支持任何的代码、配置文件写入工作。

      实现过程中,可以通过 ProjectGenerationContext 获取相关依赖,然后通过自定义逻辑完成文件生成工作。

      下面是 initializr 和 start.spring.io 提供的 ProjectContributor 实现:

      15.png

      拿几个主要的实现看看:

      • MavenBuildProjectContributor:写入 maven 项目 pom.xml 文件;
      • WebFoldersContributor:创建 web 项目的资源文件夹;
      • ApplicationPropertiesContributor:写入 application.properties 文件;
      • MainSourceCodeProjectContributor:写入应用入口类 xxxApplication.java 文件;
      • HelpDocumentProjectContributor:写入帮助文档 HELP.md 文件。

      相对于 ProjectContributor,xxxxxCustomizer  不是一个统一的接口,我把他理解为一种感念和与之对应的命名习惯;每个 Customizer 都有自己明确的名字,同时也对应了明确的触发逻辑和职责边界。

      下面列出框架提供的 Customizer 的说明:

      • MainApplicationTypeCustomizer:自定义 MainApplication 类;
      • MainCompilationUnitCustomizer:自定义 MainApplication 编译单元;
      • MainSourceCodeCustomizer:自定义 MainApplication 源码;
      • BuildCustomizer:自定义项目构建工具的配置内容;
      • GitIgnoreCustomizer:自定义项目的 .gitignore 文件;
      • HelpDocumentCustomizer:自定义项目的帮助文档;
      • InitializrMetadataCustomizer:自定义项目初始化配置元数据;这个 Customizer 比较特殊,框架会在首次加载元数据配置时调用;
      • ProjectDescriptionCustomizer:自定义 ProjectDescription ;即在生成项目文件之前,允许调整项目描述信息;
      • ServletInitializerCustomizer:自定义 web 应用在类上的配置内容;
      • TestApplicationTypeCustomizer:自定义测试 Application 类;
      • TestSourceCodeCustomizer:自定义测试 Application 类的源码。

      参考资料

      1. 相关链接

      • initializr 说明文档

      https://docs.spring.io/initializr/docs/current-SNAPSHOT/reference/html/

      • spring-initializr 项目地址

      https://github.com/spring-io/initializr

      • start.spring.io 项目地址

      https://github.com/spring-io/start.spring.io

      2. spring.factories 明细

      initializr-generator/src/main/resources/META-INF/spring.factoriesio.spring.initializr.generator.buildsystem.BuildSystemFactory=
      io.spring.initializr.generator.buildsystem.gradle.GradleBuildSystemFactory,
      io.spring.initializr.generator.buildsystem.maven.MavenBuildSystemFactory
      io.spring.initializr.generator.language.LanguageFactory=
      io.spring.initializr.generator.language.groovy.GroovyLanguageFactory,
      io.spring.initializr.generator.language.java.JavaLanguageFactory,
      io.spring.initializr.generator.language.kotlin.KotlinLanguageFactory
      io.spring.initializr.generator.packaging.PackagingFactory=
      io.spring.initializr.generator.packaging.jar.JarPackagingFactory,
      io.spring.initializr.generator.packaging.war.WarPackagingFactory

      initializr-generator-spring/src/main/resources/META-INF/spring.factories:

      io.spring.initializr.generator.project.ProjectGenerationConfiguration=
      io.spring.initializr.generator.spring.build.BuildProjectGenerationConfiguration,
      io.spring.initializr.generator.spring.build.gradle.GradleProjectGenerationConfiguration,
      io.spring.initializr.generator.spring.build.maven.MavenProjectGenerationConfiguration,
      io.spring.initializr.generator.spring.code.SourceCodeProjectGenerationConfiguration,
      io.spring.initializr.generator.spring.code.groovy.GroovyProjectGenerationConfiguration,
      io.spring.initializr.generator.spring.code.java.JavaProjectGenerationConfiguration,
      io.spring.initializr.generator.spring.code.kotlin.KotlinProjectGenerationConfiguration,
      io.spring.initializr.generator.spring.configuration.ApplicationConfigurationProjectGenerationConfiguration,
      io.spring.initializr.generator.spring.documentation.HelpDocumentProjectGenerationConfiguration,
      io.spring.initializr.generator.spring.scm.git.GitProjectGenerationConfiguration

      initializr-web/src/main/resources/META-INF/spring.factories:

      org.springframework.boot.autoconfigure.EnableAutoConfiguration=
      io.spring.initializr.web.autoconfigure.InitializrAutoConfiguration
      org.springframework.boot.env.EnvironmentPostProcessor=
      io.spring.initializr.web.autoconfigure.CloudfoundryEnvironmentPostProcessor

      initializr-actuator/src/main/resources/META-INF/spring.factories:

      org.springframework.boot.autoconfigure.EnableAutoConfiguration=
      io.spring.initializr.actuate.autoconfigure.InitializrActuatorEndpointsAutoConfiguration,
      io.spring.initializr.actuate.autoconfigure.InitializrStatsAutoConfiguration

      start-site/src/main/resources/META-INF/spring.factories:

      io.spring.initializr.generator.project.ProjectGenerationConfiguration=
      io.spring.start.site.extension.build.gradle.GradleProjectGenerationConfiguration,
      io.spring.start.site.extension.build.maven.MavenProjectGenerationConfiguration,
      io.spring.start.site.extension.dependency.DependencyProjectGenerationConfiguration,
      io.spring.start.site.extension.dependency.springamqp.SpringAmqpProjectGenerationConfiguration,
      io.spring.start.site.extension.dependency.springboot.SpringBootProjectGenerationConfiguration,
      io.spring.start.site.extension.dependency.springcloud.SpringCloudProjectGenerationConfiguration,
      io.spring.start.site.extension.dependency.springdata.SpringDataProjectGenerationConfiguration,
      io.spring.start.site.extension.dependency.springintegration.SpringIntegrationProjectGenerationConfiguration,
      io.spring.start.site.extension.dependency.springrestdocs.SpringRestDocsProjectGenerationConfiguration,
      io.spring.start.site.extension.description.DescriptionProjectGenerationConfiguration,
      io.spring.start.site.extension.code.kotin.KotlinProjectGenerationConfiguration

      作者信息
      陈曦(花名:良名)阿里巴巴技术专家。目前在应用容器&服务框架团队,Spring Cloud Alibaba 项目成员,致力于将阿里云打造为Java开发者最好用的云。2014 年加入 B2B,多次参与 双11、618 作战。

      3 群直播海报.png

      阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”
      /

      ]]>
      这么有趣的设计师小姐姐是怎么进入支付宝的? Wed, 08 Apr 2020 03:03:11 +0800

      就现在!蚂蚁「校招季」重磅来袭!除了介绍蚂蚁的技术大咖,我们还邀请了一些通过校招来到蚂蚁的过来人分享他们的通关经验和心得,这里随时可能有行业技术大咖和你的直系学长学姐出没哦~ 「校招季」栏目会持续输出有关“蚂蚁校招”的丰富内容,敬请期待!

      大家好,我是俞亦慧,算上在蚂蚁的实习期,到今天也快满两年了。

      来阿里其实是我大一专业分流时就定下的目标,当时只是单纯的觉得目标要定得高一点,万一实现不了,也不至于太差。而阿里在互联网人心目中,应该就是理想一般的存在,所以并没有过多思考,我就先把我的目标定在了阿里。

      我的目标:向蚂蚁冲鸭!

      我是在大三准备实习的时候开始投递简历的,一开始当然没有通过,但是对于没有实习经历,且是一个大三学生的我来说,能够象征性的接到一个来自阿里的面试电话,就已经非常开心了。印象特别深刻是第一个面试电话快要挂断的时候,面试官告诉我这次面试没有通过,同时也鼓励我让我继续加油,并且切实的告诉我哪些地方需要改进,特别的平易近人,让我更加坚定了要去阿里的决心。

      第一次的失败让我了解到,其实在众多的面试者中,我存在着太多的问题。本科学历不算高/ 无实习经历 / 作品集不够丰富不够多元 / 面试还会紧张到磕巴,问题实在是非常多,但是毕竟目标定在那儿,到大四也还有充足的时间继续尝试,于是在接下来的一年里,我一边做毕设,一边找离学校近而且还不错的公司实习,每天完成毕设任务和公司需求后,再花一小时时间,把作品集的作品重新整理排版优化。

      对于设计师来说,面试的第一块敲门砖一定是作品集。

      因为应届生经验不足,实际落地的项目相对少,作品集一定是越多元越好,越有创意点越好。动效、视频、平面、UI …… 只要会的,都可以拿出来晒。但多元并不代表追求数量,在作品集的制作过程中,应该追求质,不追求量。只保留自己满意的作品,最优秀的作品放在最前面。另外一个重要的点就是创意,学生最大的优势,其实就在于初生牛犊不怕虎,敢想。所以作品中应该体现出多样的想法,可以不切实际也没关系。还有一个小小的建议,就是作品集尽量不要出现临摹作品,如果真的要作为练习放进去,就要写清楚临摹,这也是非常关键的一点。

      在设计团队中,设计师们都非常崇尚自由平等的氛围,也没有什么架子,尤其是当你通过了第一关的作品集筛选后,基本上你的能力已经得到了认可,面试过程中,只要保持平常心,不要过分紧张,听清楚问的问题是什么,然后说出自己的想法就可以了。如果有不明白的,不要害怕发问,适当的问面试官一些问题,不但可以让自己更了解这个部门的工作,也可以让面试官看到你的积极性。

      一般情况下,除了作品集和面试以外不会有额外的考验,但是由于我实在是缺乏经验,作品集也做得不够好(亲身经历告诉你一定要好好做作品集~),在一轮面试后,面试官给我出了一道小小的考题,让我完成设计方案。在我自己心里有数我做的作品集不够看的情况下,我花了非常多的时间投入到这个考题里,甚至在两天时间内,还做了一次问卷调研。如果有这种额外考验,说明你可能已经是个幸运儿了,一定要抓住机会,卯足劲头把这个设计做好。不过额外的收获就是,我在后面的面试过程中,有机会来到办公区,提前看到了未来的同事们(当时就感觉希望应该还蛮大的吧)。

      事实上,我的运气是真的还不错。记得接到offer电话的时候,是在一个芦苇丛旁边,我正跟着我们班主任在阿里西溪园区参观,当时别提多开心了,虽然等电话的过程比较紧张,感觉时间特别漫长,但好在最后心愿成真了。

      在蚂蚁,认真生活快乐工作

      入职后最大的感受,就是同事们都很厉害,我的日常是每天被各路大佬按在地上摩擦,感受到自己的渺小和无能。起初对体验技术部的了解就仅限于to B,做中后台产品的部门,但觉得设计到哪里都是一样的做。但是真正入职后发现,对于实习期一直在做to C的电商业务的我来说,to B其实也是不小的挑战。从设计风格,到产品属性,都和我之前的经验完全不一样,一开始真的常常四处碰壁,好在蚂蚁有师兄文化,大家都乐于把自己会的东西教给别人,从同事身上让我学到了很多。在蚂蚁的时光里,虽然也有过焦虑,也有过烦躁,也有过特别累,觉得坚持不下去的时候,但是只要坚持下去了再回头看,就会看到自己的进步和成长。

      在我看来,艺术来源于生活。对于设计师来说,除了认真工作,快乐生活也很重要。闲暇周末,我会约着朋友看看展览,或是拿着相机拍摄美好瞬间,又或是在家弹弹吉他,放松放松平日紧绷的神经。感受生活的美好,才能更好激发自己的灵感,更爱自己的工作。

      其实设计师是一个挺直观的职业,因为视觉很直观,你看到什么就是什么,没有任何虚假。所以设计师更看重作品,像学历这些并没有什么特别的门槛,比如说你必须得985还是211,或者必须是几大美院出来的之类,最重要的还是你的作品做得好不好,做得足够好就可以进来。

      我现在在的部门是蚂蚁金服CTO 线核心的中台团队-体验技术部,也是西湖区(日耳又)优秀的前端技术、企业级设计团队!没错,就是那个传说中的大神与网红云集,200+ 人员规模,有着强大的技术基石与设计灵魂,致力于「打造全球领先的体验科技」,为社区贡献了 Ant Design、AntV、UmiJS、Egg、Dva 等工具库,打造了十万阿里人都在用的新一代 Office 语雀,广受追捧的设计生产利器 Kitchen 、海兔以及下一代前端研发工具云凤蝶等众多产品的体验技术部!

      简而言之,我们已经准备好了免费实习卡,2021 届毕业的同学们,你们还不快到碗里来?我在这里等你哦~~

      加入我们

      1. 职位方向:体验设计师/创意设计师(3D 动效、影视、平面)

      > 岗位描述

      天上星亮晶晶,工专路上放光明。作为设计师,我们是一群有爱有梦想,脑洞大开,集技术与艺术于一身的超级靓仔。想体验一把闪闪发光吗?想成为未来现象级产品的缔造者吗?

      来这里,Ant Design 的组件等你来画,海兔的资产由你来拼,栩栩如生的数据请你画龙点睛,蚂蚁金服中台产品以及数据大屏都期待你的来临。免费体验卡已经准备就绪,就等你了!

      > 岗位要求

      • 同时具备交互和视觉方面的设计能力(用作品说话);
      • 具有良好的艺术修养,有扎实的设计基础,对设计趋势有敏锐感受;
      • 熟练掌握 PS、AE、AI、PR、Sketch 等设计软件;
      • 积极主动,善于沟通,有良好的团队合作能力,能很好的阐述视觉观点和设计价值;

      > 加分项

      • 有 WEB / 移动产品设计经验,有中后台设计经验者优先;
      • 视觉设计方面具备手绘、品牌、GUI、3D、动效,影视等相关特殊能力;
      • 具备体系化的思维,已有相关设计规范和方法论总结; 
      • 拥有良好的英文读写能力和国际视野,有阅读国外先进作品和文献的能力;

      2. 职位方向:前端工程师

      > 岗位描述

      在体验技术部,你将和一众大神、网红橙黄绿蓝靛紫等专业同事并肩作战,背靠完备和强大的技术平台,参与蚂蚁金服中台产品建设,参与开发维护 Ant Design,AntV,UmiJS,Egg 等等知名开源产品,追求极致产品与研发体验。

      > 岗位要求

      • 编程基础扎实,熟练掌握 JavaScript、HTML、CSS等前端基础技术;
      • 熟悉计算机与网络,了解数据结构与算法;
      • 熟悉模块化、前端编译与构建工具,了解 React、Vue 等主流前端框架,能理解其设计原理;
      • 关注新事物、新技术,有较强的学习能力,喜欢挑战;

      > 加分项

      • 同时具备 PC/无线 端开发能力,有成功的中大型 Web 产品或移动应用开发经验;
      • 有参与前端开源项目开发,或有优秀的个人项目;
      • 有服务端开发经验(微服务、Serverless、PaaS 等,熟悉 Node、Java、Go 等语言);
      • 了解可视化知识,熟悉SVG、Canvas、WebGL,使用过D3、G2、ECharts 等类库;

      3. 职位方向:图形/图像算法岗位

      > 岗位描述

      我们专注于利用图形图像技术创造饱含艺术想象力的交互式数字艺术作品,创造大型数字孪生城市和数字指挥中心,希望你对图形与艺术充满热情,有扎实的代码能力与图形相关技术,能利用算法提高艺术展示和交互体验。你将会负责超高分辨率实时渲染、高效城市建模以及数字驾驶舱场景中人机交互方案等关键技术的探索与研究,期待你的加入!

      > 岗位要求

      • 本科及以上学历,计算机 / 数字媒体专业或数学专业者优先;
      • 熟悉至少一门编程语言(如 C++),图形学 / 机器学习相关专业或主修过相关课程优先;
      • 积极乐观,认真负责,有良好的团队沟通协作能力;
      • 有主动学习的能力,对创新及解决具有挑战性的问题充满激情;

      > 加分项

      • 熟悉 OpenGL 或者 DirectX 者优先;
      • 熟悉深度学习框架(如 TensorFlow、PyTorch)者优先;
      • 有主流3D引擎(如 Unity、Unreal )使用经验者优先;
      • 发表过高质量的顶级会议、期刊(SIGGRAPH、TVCG、 ICCV、CVPR 等)论文者优先;

      > 简历投递方式

      方法一:获取内推资格。千方百计找到你在蚂蚁的师兄师姐!果断把自己安利给TA,交流最适合自己的岗位,去到最想去的面试团队!收到师兄内推链接后,确认并完善简历,进入招聘流程。

      方法二:自主投递简历

      • 投递邮箱:afx-talent@cloud.alipay.com
      • 邮件标题命名:实习 - 岗位 - 学校 - 名字
      • 简历附件(若有)命名:实习 - 岗位 - 学校 - 名字
      ]]>
      校招面经 | 一位90后少年面试支付宝后的“肺腑之言” Wed, 08 Apr 2020 03:03:11 +0800

      就现在!蚂蚁「校招季」重磅来袭!除了介绍蚂蚁的技术大咖,我们还邀请了一些通过校招来到蚂蚁的过来人分享他们的通关经验和心得,这里随时可能有行业技术大咖和你的直系学长学姐出没哦~ 「校招季」栏目会持续输出有关“蚂蚁校招”的丰富内容,敬请期待!

      即将或正在饱受校招面试折磨的学弟学妹们好,我是来自蚂蚁金服CTO线研发效能部的开发工程师李煜超。从惴惴不安准备春招至今也已过去两年,现在的我也想作为过来人为你们的春招尽一份力。

      先不提我平淡的经历和不足下酒的心路历程,这里我先为你们奉上你们最关心的面经宝典。由于我并没有作为面试官的经验,因此特地采访了颇有面试经验的师兄,结合自身感受整理如下几条。

      简历

      相信在校招季开始之前,优秀的你们已经网罗各种简历模板,并整理出了n稿漂亮的简历。其实,简历的要求是尽量做到简洁、清晰,那么一份只能容纳寥寥数字的简历,该如何包含你们横溢的才华呢?

      首先要知道,简历的主体是项目经历。作为在校大学生,面试官并不会要求你们有多庞大或多辉煌的项目经验(当然有更好!),他们关心的是,经过项目塑造起来的你们是否是部门所需要的——即所罗列的项目与部门工作是否相关,以及你们在这些项目中所展现出来的自我学习能力。

      这也就说明你们在筛选项目经历时也应当有所选择,侧重于展现你拥有部门想要的技能,侧重于你在项目实践过程中使用的技术手段以及这种手段带来的效果,如果你在学校要求之外开发了一定规模的项目,或是项目存在一定影响力,那是最吼滴。

      同时,在编写简历时请务必做到实事求是。阿里是一家十分注重价值观的公司,而诚信一直是涵盖在价值观中的重要内容。简历上最明显的体现便是对自己技能精通程度用词的描述上,请谨慎使用过于强烈的用词(如精通)。

      简单来说,简历是递给面试官的个人门面,是面试官对你们所提问题的指引,也是你们进行面试准备时的导航。

      准备

      通常春招的时候,大多数同学已经提前搜集一些或是前人留下,或是勤奋的同学整理的面经资料和面试题集锦。那么面试经验尚不多的同学可能会问,背这些有用吗?

      就我个人感觉来说,是有用的。五六年的大学课程,难免会有遗漏,面经作为对学习内容的提炼是很好的助记手段。但也不可过于依赖,这里以Java后端开发为例分几种情况介绍。

      1.基础的知识如JVM、数据结构、数据库等

      这是你们手上每份资料中大部分都会涵盖的内容,也是你们在校招过程中会被提问无数次的知识,仅靠背诵面经是不足以对付面试的。在准备过程中你们需要时刻问自己是否真的熟悉,做到知其然,知其所以然。有不确定的地方提出来,看看以前上课的ppt,或是查资料进行系统复习。如果准备时间较为充分,推荐将相关权威书籍再好好阅读一遍。

      值得一提的是,蚂蚁的SOFA/SOFABoot是基于Spring框架自研的金融级分布式中间件。因此,若项目经历中有涉及Spring使用的,也务必要作为重点进行准备,除了是面试的常问问题,他也有助于你们顺利通过面试后,能够尽早投入实际工作中。

      2.简历中涉及的项目相关知识

      很多同学在进行面试资料整理的时候,会把自己面试过程中被提过的问题也归纳进去——通常是项目相关的技术,或者再抽象出一系列复习点。这对他/她来说是很有好处的,但在其他人手上可能就成了“干扰项”。

      在你们复习面经资料的过程中,如果碰到类似这种问题,应该进行合理安排,没必要投入过多精力——指疲于准备面试的情况下,若时间富裕,则多学无害。你们也可以请教整理的同学,这些问题是面试官在什么场景下提出来的,或者说是怎么引导出这个问题的,如此有利于你们学会揣度面试官的心思,并根据自己项目中提到的技术进行发散思考。

      上一趴提到,简历是面试准备时的导航。其项目中涉及的技术更是重中之重,对于这些技术,在做到知其所以然之后,最好连其祖宗所以然都知道。准备期间也可以借助脑图进行整理,梳理相关知识进行学习。

      比如项目中涉及了Spring技术栈,那么对于IOC和AOP的原理势必要掌握的。既然提到IOC,那么说一下IOC的好处是什么,你还需要知道有哪些相关的软件设计原则,Spring中的几种注入方式,和Spring的注解作用等等,那么顺便的,注解中的@Autowired和@Resource你可以说说有什么区别吗?提到AOP,几个基本概念,几种通知方法,可以用于开发什么功能等都可以进行复习。

      3.工具相关知识

      如git、mvn使用、命令行使用,甚至idea/eclipse快捷键使用,在专业知识和项目准备充分之余也可以进行复习。

      除了知识的总结学习,算法题和项目回顾也是面试的重点。

      对于项目本身需要把脉络理清,对于整个项目、以及项目的每个模块,做到用精简的语言让面试官能够明白。回顾项目时,理清楚项目的难点和成果。面对多种方案时是如何考虑的,碰到难题时是怎么解决的。

      算法题就是多刷多做多归纳。

      面试

      蚂蚁的面试通常是有3-4轮技术面,还有一轮HR面,各轮面试的问题视面试官风格而定,但有以下几种能力或者说是特质,是面试官希望能够在你们身上看到的。

      一个是基础能力。面试官对于在校生通常不会有太高的技术水平要求,但会很希望看到你们有扎实的功底。万丈高楼平地起,只要发现你们是有潜力的原石,蚂蚁就有信心把你们雕塑出龙章凤姿。

      一个是自我学习能力。面试官会乐于看到你们勇于发现问题,全力解决问题,用新方法、新思路来创造变化,带来突破性的结果。

      最为重要的,是需要再一次强调的诚信。对于自己没做过的,不知道的,没有必要隐瞒或伪装。如果想凭经验进行临场发挥,也请先勇敢承认。面试不是毕业答辩,没有必要唯唯诺诺或含糊其辞,面试官其实也是在找自己往后工作中的战友。可以有不同的见解,面试官不会喜欢弄虚作假的伙伴,但会欣赏一个承认短板,能够灵活应变找到答案的人。综合素质和非专业水平的亮点也是面试官考量的因素。

      在进入蚂蚁后你会发现,这些其实都是成为一个阿里人的潜质,是阿里寻找同路人的方向。

      以我自身的面试经历来看,我的整个面试流程经历了三轮技术面和一轮HR面。

      第一轮面试的主要内容是基础知识和在线算法题。事后与同学交流发现算法题难度不尽相同。作为过来人回头看,除了最终结果,面试官还会看中你们的解题思路和编程习惯(说人话:把代码写好看点,推荐看《重构》和《代码整洁之道》)。

      可能由于写的项目经历和部门业务较为相关,第二三轮技术面基本围绕项目展开,依稀记得第三轮面试的时候,我就和面试官就我做过的项目方向有不同意见,产生了讨论,当时一度觉得自己可能与蚂蚁无缘了。

      最后的HR面其实更像一个互相了解的过程,HR会问你关于工作的看法(包括base地、方向等)或是考量你价值观的小问题(并不确定是不是),当然你也可以提出你对工作或部门的疑问。

      个人经历

      说完干货,接下来便是“水货”。

      我本硕就读于南京某500本大学——南哪大学的软件学院。学校不是真的500本,但我的成绩大概差不多是500本的水平,因此在学习经验上并不能给你们很好的建议。

      我们学院的部分课程工程氛围颇为浓厚(至少本科是),头几年常常会被数不尽的文档淹没,其中大作业ddl是比期末考更令人疲惫和在意的事。因此我的本科生活也及其简单,没事就泡球馆,大作业卖力做,考试要求不挂科(排名分先后),就这样,波澜不惊地来到了研究生阶段和春招季,开始了和蚂蚁的邂逅。记得当时同一实验室的师兄已经在蚂蚁实习,恰好我们正在做的课题与部门工作有一定联系,便推荐我内推。作为一个刚出新手村的菜鸟,为了抓住摆在眼前进入心怡大厂的机会,我特地问他,能不能晚些进行面试,让我先多刷刷怪涨涨经验。师兄善解人意,通情达理,我接的第二个面试电话就是蚂蚁的。

      好在最后还是有惊惊惊惊惊无险地拿到了offer,你们看,我的经历就是这么朴实无华且枯燥。

      回过头来看过去几年,如果可以,还是想给年轻的自己几点建议,也希望能给或是还处在美好的大学生活中,或是在焦急准备面试,或是刚刚拿到offer的你们一点帮助:

      1.所有的专业课程,都好好上

      并不是说非专业课程就能荒废,只是对于专业课程有更深的体会。在入职后,当事人表示非常后悔。

      曾经有诸多课程摆在我面前,我没有珍惜,老师在台上声情并茂的介绍,我在台下不屑地想着:“这东西我以后工作能用到?能用到我把这课本吃下去。”。现在追悔莫及,作为研发效能部的开发工程师,我一次一次地切身体会到学校课程安排诚不欺我,只能在空闲时间重新捡起来,为年轻的自己买单。

      2.课余时间多了解一些专业前沿知识

      入职后,你会有机会与同届毕业生接触、合作,会发现总是有那么些闪闪发光的人,永远有自己的想法,永远走在前方。多了解一些前沿知识,能帮助你们培养专业素养,保持前瞻意识。

      3.认真取花名!

      对于顺利拿到阿里offer的同学,首先恭喜你们,其次作为阿里的一个文化,希望你在取花名前能认真考虑,不要重蹈我的覆辙!我的花名是黎荍(qiáo),取花名时误以为花名一定要古风,一通瞎琢磨后取了这么个我查词典前都不认识字的花名。

      4.认真思考就业方向

      除了hc,在投简历的时候你们需要好好问问自己,自己梦想什么岗位,是算法还是开发,是做业务还是做平台,对于所投部门与自己的研究方向是否匹配,还在踌躇的小朋友,也可以借着实习的机会切身感受一下实际工作与想象是否一致。

      那么,有没有一个部门,岗位选择多,实习生活丰富多彩,工作内容与学校所学课程高度match呢?

      当!然!有!啦!

      蚂蚁金服CTO线研发效能部门面向全体2020校招实习毕业生开启春招啦!不管你是前端后端,Java,C++,Android还是iOS,是研发还是数据、算法,只要你有技术激情,勇于挑战,我们,欢迎你的到来!

      加入我们

      JAVA/ C++研发工程师

      1. 计算机,数学,统计学, 运筹学或相关专业应届毕业生;

      2. 良好的计算机专业基础,熟练掌握数据结构、算法、操作系统等基础知识;

      3. 优秀的编程能力,熟练使用C/C++或者Java编程语言;

      4. 良好的团队协作能力,有技术激情,能够胜任有挑战的工作;

      5. 有分布式系统或者其它底层系统研发经验优先。

      数据/算法工程师

      1. 有分布式系统或者其它底层系统研发经验优先。

      2. 计算机、数学、统计等相关专业应届毕业生,机器学习、深度学习、NLP等泛人工智能领域研究方向的硕士、博士优先;良好的计算机专业基础,熟练掌握数据结构、算法、程序设计等基础知识;

      3. 熟练掌握SQL、R、Python以及相关进行大规模分析的工具和Hadoop/Spark/Cosmos/ODPS等大数据分布式平台,熟悉大规模分布式机器学习框架(Spark MLLib,MPI,ParameterServer等)、深度学习开源工具(Caffe,Theano,Torch,Ten-sorFlow,MXNet,CNTK等),Coding能力较强,有Java语言编程经验优先;

      4. 良好的团队协作能力,有技术激情,能够胜任有挑战的工作;

      5. 在顶级机器学习、信息安全和AI领域会议和期刊有文章发表的优先考虑(比如NIPS、ICML、ICLR、AAAI、IJCAI、KDD、SIGIR、CCS、WWW、JMLR等)。

      Android/iOS研发工程师

      1. 计算机、数学、统计、通信等相关专业应届毕业生。

      2. 良好的计算机专业基础,熟练掌握数据结构、算法、操作系统等基础知识;

      3. 熟悉iOS/Android平台原理机制,具备iOS或Android客户端应用实际开发经验;

      4. 有一定软件架构设计能力,熟悉常见的异步,同步,多线程,跨进程,组件,容器的设计方法;

      5. 具备创新业务技术攻关和落地能力者优先(不限于算法,生物识别,图形图像,3D建模,AR,多媒体等领域)

      前端研发工程师

      • 职位要求

      1. 编程基础扎实, 熟练掌握JavaScript、HTML、CSS等前端基础技术;

      2. 熟悉计算机与网络,了解数据结构与算法;

      3. 熟悉模块化、前端编译与构建工具,了解React、Vue等主流前端框架,能理解其设计原理;

      4. 关注新事物、新技术,有较强的学习能力,喜欢挑战;

      • 加分项

      1. 同时具备PC/无线端开发能力,有成功的中大型Web产品或移动应用开发经验;

      2. 有参与前端开源项目开发,或有优秀的个人项目;

      3. 有服务端开发经验(微服务、Serverless、PaaS等, 熟悉Node、Java、Go等语言) ;

      4. 了解可视化知识, 熟悉SVG、Canvas、WebGL, 使用过D3、G2、ECharts等类库。

      程序分析技术工程师

      • 职位描述

      1. 搭建及维护程序静态分析基础框架,基础引擎;

      2. 设计实现及维护分析器程序语言特性建模系统,属性描述系统等系统;

      3. 设计实现扩展及维护程序分析结果查询语言;

      4. 研发及维护基础程序分析算法例如指针分析,数值分析等;

      5. 研发各程序语言代码漏洞检查器,支持检查标准如CWE,CERT,OWASP等;

      6. 研发及维护约束求解,打分,修复点计算等周边算法。

      • 职位要求

      1. 计算机或相关专业全日制本科或以上学历;

      2. 熟悉程序语言的基础理论及概念;

      3. 扎实的算法及计算理论基础(如图灵机模型);

      4. 较强的总结能力,系统化思维及能力以及抽象思维的能力;

      5. 较强的学习能力,可以很快上手并理解一门新的程序语言;

      6. 熟练掌握Java/C/C++语言,能编写稳定,高效,可靠的代码。

      • 加分项

      1. 有从事过程序语言方面理论研究,有研究生以上学历者优先;

      2. 有从事编译器/程序分析器/程序漏洞检查器开发经验;

      3. 熟悉编译器中间代码格式以及程序分析相关概念,比如控制流等;

      4. 有静态程序分析工具使用经验和程序漏洞挖掘经验;

      5. 熟悉LLVM中间代码。

      简历投递

      邮箱:AntLinkE@antfin.com

      ]]>
      Flink kafka source & sink 源码解析 Wed, 08 Apr 2020 03:03:11 +0800

      摘要:本文基于 Flink 1.9.0 和 Kafka 2.3 版本,对 Flink Kafka source 和 sink 端的源码进行解析,主要内容分为以下两部分:

      1.Flink-kafka-source 源码解析

      • 流程概述
      • 非 checkpoint 模式 offset 的提交
      • checkpoint 模式下 offset 的提交
      • 指定 offset 消费

      2.Flink-kafka-sink 源码解析

      • 初始化
      • Task运行
      • 小结

      1.Flink-kafka-source 源码解析

      流程概述

      一般在 Flink 中创建 kafka source 的代码如下:

      StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
      //KafkaEventSchema为自定义的数据字段解析类
      env.addSource(new FlinkKafkaConsumer<>("foo", new KafkaEventSchema(), properties)

      而 Kafka 的 KafkaConsumer API 中消费某个 topic 使用的是 poll 方法如下:

      KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
      consumer.poll(Duration.ofMillis(100));

      下面将分析这两个流程是如何衔接起来的。

      初始化

      初始化执行 env.addSource 的时候会创建 StreamSource 对象,即 final StreamSource sourceOperator = new StreamSource<>(function);这里的function 就是传入的 FlinkKafkaConsumer 对象,StreamSource 构造函数中将这个对象传给父类 AbstractUdfStreamOperator 的 userFunction 变量,源码如下:

      ■ StreamSource.java

      public StreamSource(SRC sourceFunction) {
          super(sourceFunction);
          this.chainingStrategy = ChainingStrategy.HEAD;
      }

      ■ AbstractUdfStreamOperator.java

      public AbstractUdfStreamOperator(F userFunction) {
         this.userFunction = requireNonNull(userFunction);
         checkUdfCheckpointingPreconditions();
      }

      Task运行

      task 启动后会调用到 SourceStreamTask 中的 performDefaultAction() 方法,这里面会启动一个线程 sourceThread.start();,部分源码如下:

      private final LegacySourceFunctionThread sourceThread;
      
      @Override
      protected void performDefaultAction(ActionContext context) throws Exception {
          sourceThread.start();
      }

      在 LegacySourceFunctionThread 的 run 方法中,通过调用 headOperator.run 方法,最终调用了 StreamSource 中的 run 方法,部分源码如下:

      public void run(final Object lockingObject,
                      final StreamStatusMaintainer streamStatusMaintainer,
                      final Output<StreamRecord<OUT>> collector,
                      final OperatorChain<?, ?> operatorChain) throws Exception {
      
        //省略部分代码
        this.ctx = StreamSourceContexts.getSourceContext(
          timeCharacteristic,
          getProcessingTimeService(),
          lockingObject,
          streamStatusMaintainer,
          collector,
          watermarkInterval,
          -1);
      
        try {
          userFunction.run(ctx);
          //省略部分代码
        } finally {
          // make sure that the context is closed in any case
          ctx.close();
          if (latencyEmitter != null) {
            latencyEmitter.close();
          }
        }
      }

      这里最重要的就是 userFunction.run(ctx);,这个 userFunction 就是在上面初始化的时候传入的 FlinkKafkaConsumer 对象,也就是说这里实际调用了 FlinkKafkaConsumer 中的 run 方法,而具体的方法实现在其父类 FlinkKafkaConsumerBase中,至此,进入了真正的 kafka 消费阶段。

      Kafka消费阶段

      在 FlinkKafkaConsumerBase#run 中创建了一个 KafkaFetcher 对象,并最终调用了 kafkaFetcher.runFetchLoop(),这个方法的代码片段如下:

      /** The thread that runs the actual KafkaConsumer and hand the record batches to this fetcher. */
      private final KafkaConsumerThread consumerThread;
      
      @Override
      public void runFetchLoop() throws Exception {
        try {
          final Handover handover = this.handover;
      
          // kick off the actual Kafka consumer
          consumerThread.start();
          
          //省略部分代码
      }

      可以看到实际启动了一个 KafkaConsumerThread 线程。进入到 KafkaConsumerThread#run 中,下面只是列出了这个方法的部分源码,完整代码请参考 KafkaConsumerThread.java。

      @Override
      public void run() {
        // early exit check
        if (!running) {
          return;
        }
        // This method initializes the KafkaConsumer and guarantees it is torn down properly.
        // This is important, because the consumer has multi-threading issues,
        // including concurrent 'close()' calls.
        try {
          this.consumer = getConsumer(kafkaProperties);
        } catch (Throwable t) {
          handover.reportError(t);
          return;
        }
        try {
      
          // main fetch loop
          while (running) {
            try {
              if (records == null) {
                try {
                  records = consumer.poll(pollTimeout);
                } catch (WakeupException we) {
                  continue;
                }
              }
            }
            // end main fetch loop
          }
        } catch (Throwable t) {
          handover.reportError(t);
        } finally {
          handover.close();
          try {
            consumer.close();
          } catch (Throwable t) {
            log.warn("Error while closing Kafka consumer", t);
          }
        }
      }

      至此,终于走到了真正从 kafka 拿数据的代码,即 records = consumer.poll(pollTimeout);。因为 KafkaConsumer 不是线程安全的,所以每个线程都需要生成独立的 KafkaConsumer 对象,即 this.consumer = getConsumer(kafkaProperties);。

      KafkaConsumer<byte[], byte[]> getConsumer(Properties kafkaProperties) {
        return new KafkaConsumer<>(kafkaProperties);
      }

      小结:本节只是介绍了 Flink 消费 kafka 数据的关键流程,下面会更详细的介绍在AT_LEAST_ONCE和EXACTLY_ONCE 不同场景下 FlinkKafkaConsumer 管理 offset 的流程。

      非 checkpoint 模式 offset 的提交

      消费 kafka topic 最为重要的部分就是对 offset 的管理,对于 kafka 提交 offset 的机制,可以参考 kafka 官方网。

      而在 flink kafka source 中 offset 的提交模式有3种:

      public enum OffsetCommitMode {
      
         /** Completely disable offset committing. */
         DISABLED,
      
         /** Commit offsets back to Kafka only when checkpoints are completed. */
         ON_CHECKPOINTS,
      
         /** Commit offsets periodically back to Kafka, using the auto commit functionality of internal Kafka clients. */
         KAFKA_PERIODIC;
      }

      初始化 offsetCommitMode

      在 FlinkKafkaConsumerBase#open 方法中初始化 offsetCommitMode:

      // determine the offset commit mode
      this.offsetCommitMode = OffsetCommitModes.fromConfiguration(
                      getIsAutoCommitEnabled(),
                      enableCommitOnCheckpoints,
              ((StreamingRuntimeContext)getRuntimeContext()).isCheckpointingEnabled());
      • 方法 getIsAutoCommitEnabled() 的实现如下:
      protected boolean getIsAutoCommitEnabled() {
         return getBoolean(properties, ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true) &&
            PropertiesUtil.getLong(properties, ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 5000) > 0;
      }
      • 也就是说只有 enable.auto.commit=true 并且 auto.commit.interval.ms>0 这个方法才会返回 true
      • 变量 enableCommitOnCheckpoints 默认是 true,可以调用 setCommitOffsetsOnCheckpoints 改变这个值
      • 当代码中调用了 env.enableCheckpointing 方法,isCheckpointingEnabled 才会返回 true

      通过下面的代码返回真正的提交模式:

      /**
       * Determine the offset commit mode using several configuration values.
       *
       * @param enableAutoCommit whether or not auto committing is enabled in the provided Kafka properties.
       * @param enableCommitOnCheckpoint whether or not committing on checkpoints is enabled.
       * @param enableCheckpointing whether or not checkpoint is enabled for the consumer.
       *
       * @return the offset commit mode to use, based on the configuration values.
       */
      public static OffsetCommitMode fromConfiguration(
            boolean enableAutoCommit,
            boolean enableCommitOnCheckpoint,
            boolean enableCheckpointing) {
      
         if (enableCheckpointing) {
            // if checkpointing is enabled, the mode depends only on whether committing on checkpoints is enabled
            return (enableCommitOnCheckpoint) ? OffsetCommitMode.ON_CHECKPOINTS : OffsetCommitMode.DISABLED;
         } else {
            // else, the mode depends only on whether auto committing is enabled in the provided Kafka properties
            return (enableAutoCommit) ? OffsetCommitMode.KAFKA_PERIODIC : OffsetCommitMode.DISABLED;
         }
      }

      暂时不考虑 checkpoint 的场景,所以只考虑 (enableAutoCommit) ? OffsetCommitMode.KAFKA_PERIODIC : OffsetCommitMode.DISABLED;。

      也就是如果客户端设置了 enable.auto.commit=true 那么就是 KAFKA_PERIODIC,否则就是 KAFKA_DISABLED。

      offset 的提交

      ■ 自动提交

      这种方式完全依靠 kafka 自身的特性进行提交,如下方式指定参数即可:

      Properties properties = new Properties();
      properties.put("enable.auto.commit", "true");
      properties.setProperty("auto.commit.interval.ms", "1000");
      new FlinkKafkaConsumer<>("foo", new KafkaEventSchema(), properties)

      ■ 非自动提交

      通过上面的分析,如果 enable.auto.commit=false,那么 offsetCommitMode 就是 DISABLED 。

      kafka 官方文档中,提到当 enable.auto.commit=false 时候需要手动提交 offset,也就是需要调用 consumer.commitSync(); 方法提交。

      但是在 flink 中,非 checkpoint 模式下,不会调用 consumer.commitSync();, 一旦关闭自动提交,意味着 kafka 不知道当前的 consumer group 每次消费到了哪。

      可以从两方面证实这个问题:

      • 源码
        KafkaConsumerThread#run 方法中是有 consumer.commitSync();,但是只有当 commitOffsetsAndCallback != null 的时候才会调用。只有开启了checkpoint 功能才会不为 null,这个变量会在后续的文章中详细分析。
      • 测试
        可以通过消费 __consumer_offsets 观察是否有 offset 的提交

      重启程序,还是会重复消费之前消费过的数据

      小结:本节介绍了在非 checkpoint 模式下,Flink kafka source 提交 offset 的方式,下文会重点介绍 checkpoint 模式下提交 offset 的流程。

      checkpoint 模式下 offset 的提交

      上面介绍了在没有开启 checkpoint 的时候,offset 的提交方式,下面将重点介绍开启 checkpoint 后,Flink kafka consumer 提交 offset 的方式。

      初始化 offsetCommitMode

      通过上文可以知道,当调用了 env.enableCheckpointing 方法后 offsetCommitMode 的值就是 ON_CHECKPOINTS,而且会通过下面方法强制关闭 kafka 自动提交功能,这个值很重要,后续很多地方都是根据这个值去判断如何操作的。

      /**
       * Make sure that auto commit is disabled when our offset commit mode is ON_CHECKPOINTS.
       * This overwrites whatever setting the user configured in the properties.
       * @param properties - Kafka configuration properties to be adjusted
       * @param offsetCommitMode offset commit mode
       */
      static void adjustAutoCommitConfig(Properties properties, OffsetCommitMode offsetCommitMode) {
         if (offsetCommitMode == OffsetCommitMode.ON_CHECKPOINTS || offsetCommitMode == OffsetCommitMode.DISABLED) {
            properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
         }
      }

      保存 offset

      在做 checkpoint 的时候会调用 FlinkKafkaConsumerBase#snapshotState 方法,其中 pendingOffsetsToCommit 会保存要提交的 offset。

      if (offsetCommitMode == OffsetCommitMode.ON_CHECKPOINTS) {
         // the map cannot be asynchronously updated, because only one checkpoint call can happen
         // on this function at a time: either snapshotState() or notifyCheckpointComplete()
         pendingOffsetsToCommit.put(context.getCheckpointId(), currentOffsets);
      }

      同时,下面的变量会作为 checkpoint 的一部分保存下来,以便恢复时使用。

      /** Accessor for state in the operator state backend. */
      private transient ListState<Tuple2<KafkaTopicPartition, Long>> unionOffsetStates;
      
      在 snapshotState 方法中会同时保存 offset:
      
      for (Map.Entry<KafkaTopicPartition, Long> subscribedPartition : subscribedPartitionsToStartOffsets.entrySet()) {
          unionOffsetStates.add(Tuple2.of(subscribedPartition.getKey(), subscribedPartition.getValue()));
      }

      提交 offset

      在 checkpoint 完成以后,task 会调用 notifyCheckpointComplete 方法,里面判断 offsetCommitMode == OffsetCommitMode.ON_CHECKPOINTS 的时候,调用fetcher.commitInternalOffsetsToKafka(offsets, offsetCommitCallback); 方法,最终会将要提交的 offset 通过 KafkaFetcher#doCommitInternalOffsetsToKafka 方法中的 consumerThread.setOffsetsToCommit(offsetsToCommit, commitCallback); 保存到 KafkaConsumerThread.java 中的 nextOffsetsToCommit 成员变量里面。

      这样就会保证当有需要提交的 offset 的时候,下面代码会执行 consumer.commitAsync,从而完成了手动提交 offset 到 kafka。

      final Tuple2<Map<TopicPartition, OffsetAndMetadata>, KafkaCommitCallback> commitOffsetsAndCallback = nextOffsetsToCommit.getAndSet(null);
      
      if (commitOffsetsAndCallback != null) {
        log.debug("Sending async offset commit request to Kafka broker");
      
        // also record that a commit is already in progress
        // the order here matters! first set the flag, then send the commit command.
        commitInProgress = true;
        consumer.commitAsync(commitOffsetsAndCallback.f0, new CommitCallback(commitOffsetsAndCallback.f1));
      }

      小结:本节介绍了在 checkpoint 模式下,Flink kafka source 提交 offset 的方式,后续会介绍 consumer 读取 offset 的流程。

      指定 offset 消费

      消费模式

      在 Flink 的 kafka source 中有以下5种模式指定 offset 消费:

      public enum StartupMode {
      
         /** Start from committed offsets in ZK / Kafka brokers of a specific consumer group (default). */
         GROUP_OFFSETS(KafkaTopicPartitionStateSentinel.GROUP_OFFSET),
      
         /** Start from the earliest offset possible. */
         EARLIEST(KafkaTopicPartitionStateSentinel.EARLIEST_OFFSET),
      
         /** Start from the latest offset. */
         LATEST(KafkaTopicPartitionStateSentinel.LATEST_OFFSET),
      
         /**
          * Start from user-supplied timestamp for each partition.
          * Since this mode will have specific offsets to start with, we do not need a sentinel value;
          * using Long.MIN_VALUE as a placeholder.
          */
         TIMESTAMP(Long.MIN_VALUE),
      
         /**
          * Start from user-supplied specific offsets for each partition.
          * Since this mode will have specific offsets to start with, we do not need a sentinel value;
          * using Long.MIN_VALUE as a placeholder.
          */
         SPECIFIC_OFFSETS(Long.MIN_VALUE);
      }

      默认为 GROUP_OFFSETS,表示根据上一次 group id 提交的 offset 位置开始消费。每个枚举的值其实是一个 long 型的负数,根据不同的模式,在每个 partition 初始化的时候会默认将 offset 设置为这个负数。其他的方式和 kafka 本身的语义类似,就不在赘述。

      指定 offset

      此处只讨论默认的 GROUP_OFFSETS 方式,下文所有分析都是基于这种模式。但是还是需要区分是否开启了 checkpoint。在开始分析之前需要对几个重要的变量进行说明:

      • subscribedPartitionsToStartOffsets

        • 所属类:FlinkKafkaConsumerBase.java
        • 定义:
      /** The set of topic partitions that the source will read, with their initial offsets to start reading from. */
      private Map<KafkaTopicPartition, Long> subscribedPartitionsToSt

      说明:保存订阅 topic 的所有 partition 以及初始消费的 offset。

      • subscribedPartitionStates

        • 所属类:AbstractFetcher.java
        • 定义:
      /** All partitions (and their state) that this fetcher is subscribed to. */
      private final List<KafkaTopicPartitionState<KPH>> subscribedPar

      说明:保存了所有订阅的 partition 的 offset 等详细信息,例如:

      /** The offset within the Kafka partition that we already processed. */
      private volatile long offset;
      /** The offset of the Kafka partition that has been committed. */
      private volatile long committedOffset;

      每次消费完数据之后都会更新这些值,这个变量非常的重要,在做 checkpoint 的时候,保存的 offset 等信息都是来自于这个变量。这个变量的初始化如下:

      // initialize subscribed partition states with seed partitions
      this.subscribedPartitionStates = createPartitionStateHolders(
        seedPartitionsWithInitialOffsets,
        timestampWatermarkMode,
        watermarksPeriodic,
        watermarksPunctuated,
        userCodeClassLoader);

      消费之后更新相应的 offset 主要在 KafkaFetcher#runFetchLoop
      方法 while 循环中调用 emitRecord(value, partition, record.
      offset(), record);。

      • restoredState

        • 所属类:FlinkKafkaConsumerBase.java
        • 定义:
      /**
           * The offsets to restore to, if the consumer restores state from a checkpoint.
           *
           * <p>This map will be populated by the {@link #initializeState(FunctionInitializationContext)} method.
           *
           * <p>Using a sorted map as the ordering is important when using restored state
           * to seed the partition discoverer.
           */
      private transient volatile TreeMap<KafkaTopicPartition, Long> restoredState;

      说明:如果指定了恢复的 checkpoint 路径,启动时候将会读取这个变量里面的内容获取起始 offset,而不再是使用 StartupMode 中的枚举值作为初始的 offset。

      • unionOffsetStates

        • 所属类:FlinkKafkaConsumerBase.java
        • 定义:
      /** Accessor for state in the operator state backend. */
      private transient ListState<Tuple2<KafkaTopicPartition, Long>> unionOffsetStates;

      说明:保存了 checkpoint 要持久化存储的内容,例如每个 partition 已经消费的 offset 等信息

      ■ 非 checkpoint 模式

      在没有开启 checkpoint 的时候,消费 kafka 中的数据,其实就是完全依靠 kafka 自身的机制进行消费。

      ■ checkpoint 模式

      开启 checkpoint 模式以后,会将 offset 等信息持久化存储以便恢复时使用。但是作业重启以后如果由于某种原因读不到 checkpoint 的结果,例如 checkpoint 文件丢失或者没有指定恢复路径等。

      • 第一种情况,如果读取不到 checkpoint 的内容

      subscribedPartitionsToStartOffsets 会初始化所有 partition 的起始 offset为 -915623761773L 这个值就表示了当前为 GROUP_OFFSETS 模式。

      default:
         for (KafkaTopicPartition seedPartition : allPartitions) {
            subscribedPartitionsToStartOffsets.put(seedPartition, startupMode.getStateSentinel());
         }

      第一次消费之前,指定读取 offset 位置的关键方法是 KafkaConsumerThread#reassignPartitions 代码片段如下:

      for (KafkaTopicPartitionState<TopicPartition> newPartitionState : newPartitions) {
        if (newPartitionState.getOffset() == KafkaTopicPartitionStateSentinel.EARLIEST_OFFSET) {
          consumerTmp.seekToBeginning(Collections.singletonList(newPartitionState.getKafkaPartitionHandle()));
          newPartitionState.setOffset(consumerTmp.position(newPartitionState.getKafkaPartitionHandle()) - 1);
        } else if (newPartitionState.getOffset() == KafkaTopicPartitionStateSentinel.LATEST_OFFSET) {
          consumerTmp.seekToEnd(Collections.singletonList(newPartitionState.getKafkaPartitionHandle()));
          newPartitionState.setOffset(consumerTmp.position(newPartitionState.getKafkaPartitionHandle()) - 1);
        } else if (newPartitionState.getOffset() == KafkaTopicPartitionStateSentinel.GROUP_OFFSET) {
          // the KafkaConsumer by default will automatically seek the consumer position
          // to the committed group offset, so we do not need to do it.
          newPartitionState.setOffset(consumerTmp.position(newPartitionState.getKafkaPartitionHandle()) - 1);
        } else {
          consumerTmp.seek(newPartitionState.getKafkaPartitionHandle(), newPartitionState.getOffset() + 1);
        }
      }

      因为是 GROUP_OFFSET 模式 ,所以会调用 newPartitionState.setOffset(consumerTmp.position(newPartitionState.getKafkaPartitionHandle()) - 1); 需要说明的是,在 state 里面需要存储的是成功消费的最后一条数据的 offset,但是通过 position 这个方法返回的是下一次应该消费的起始 offset,所以需要减1。这里更新这个的目的是为了 checkpoint 的时候可以正确的拿到 offset。

      这种情况由于读取不到上次 checkpoint 的结果,所以依旧是依靠 kafka 自身的机制,即根据__consumer_offsets 记录的内容消费。

      • 第二种情况,checkpoint 可以读取到

      这种情况下, subscribedPartitionsToStartOffsets 初始的 offset 就是具体从checkpoint 中恢复的内容,这样 KafkaConsumerThread#reassignPartitions 实际走的分支就是:

      consumerTmp.seek(newPartitionState.getKafkaPartitionHandle(), newPartitionState.getOffset() + 1);

      这里加1的原理同上,state 保存的是最后一次成功消费数据的 offset,所以加1才是现在需要开始消费的 offset。

      小结:本节介绍了程序启动时,如何确定从哪个 offset 开始消费,下文会继续分析 flink kafka sink 的相关源码。

      2.Flink-kafka-sink 源码解析

      初始化

      通常添加一个 kafka sink 的代码如下:

      input.addSink(
         new FlinkKafkaProducer<>(
            "bar",
            new KafkaSerializationSchemaImpl(),
               properties,
            FlinkKafkaProducer.Semantic.AT_LEAST_ONCE)).name("Example Sink");

      初始化执行 env.addSink 的时候会创建 StreamSink 对象,即 StreamSink sinkOperator = new StreamSink<>(clean(sinkFunction));这里的 sinkFunction 就是传入的 FlinkKafkaProducer 对象,StreamSink 构造函数中将这个对象传给父类 AbstractUdfStreamOperator 的 userFunction 变量,源码如下:

      ■ StreamSink.java

      public StreamSink(SinkFunction<IN> sinkFunction) {
        super(sinkFunction);
        chainingStrategy = ChainingStrategy.ALWAYS;
      }

      ■ AbstractUdfStreamOperator.java

      public AbstractUdfStreamOperator(F userFunction) {
         this.userFunction = requireNonNull(userFunction);
         checkUdfCheckpointingPreconditions();
      }

      Task 运行

      StreamSink 会调用下面的方法发送数据:

      @Override
      public void processElement(StreamRecord<IN> element) throws Exception {
         sinkContext.element = element;
         userFunction.invoke(element.getValue(), sinkContext);
      }

      也就是实际调用的是 FlinkKafkaProducer#invoke 方法。在 FlinkKafkaProducer 的构造函数中需要指 FlinkKafkaProducer.Semantic,即:

      public enum Semantic {
         EXACTLY_ONCE,
         AT_LEAST_ONCE,
         NONE
      }

      下面就基于3种语义分别说一下总体的向 kafka 发送数据的流程。

      ■ Semantic.NONE

      这种方式不会做任何额外的操作,完全依靠 kafka producer 自身的特性,也就是FlinkKafkaProducer#invoke 里面发送数据之后,Flink 不会再考虑 kafka 是否已经正确的收到数据。

      transaction.producer.send(record, callback);

      ■ Semantic.AT_LEAST_ONCE

      这种语义下,除了会走上面说到的发送数据的流程外,如果开启了 checkpoint 功能,在 FlinkKafkaProducer#snapshotState 中会首先执行父类的 snapshotState方法,里面最终会执行 FlinkKafkaProducer#preCommit。

      @Override
      protected void preCommit(FlinkKafkaProducer.KafkaTransactionState transaction) throws FlinkKafkaException {
         switch (semantic) {
            case EXACTLY_ONCE:
            case AT_LEAST_ONCE:
               flush(transaction);
               break;
            case NONE:
               break;
            default:
               throw new UnsupportedOperationException("Not implemented semantic");
         }
         checkErroneous();
      }

      AT_LEAST_ONCE 会执行了 flush 方法,里面执行了:

      transaction.producer.flush();

      就是将 send 的数据立即发送给 kafka 服务端,详细含义可以参考 KafkaProducer api:
      http://kafka.apache.org/23/javadoc/index.html?org/apache/kafka/clients/producer/KafkaProducer.html

      flush()
      Invoking this method makes all buffered records immediately available to send (even if linger.ms is greater than 0) and blocks on the completion of the requests associated with these records.

      ■ Semantic.EXACTLY_ONCE

      EXACTLY_ONCE 语义也会执行 send 和 flush 方法,但是同时会开启 kafka producer 的事务机制。FlinkKafkaProducer 中 beginTransaction 的源码如下,可以看到只有是 EXACTLY_ONCE 模式才会真正开始一个事务。

      @Override
      protected FlinkKafkaProducer.KafkaTransactionState beginTransaction() throws FlinkKafkaException {
         switch (semantic) {
            case EXACTLY_ONCE:
               FlinkKafkaInternalProducer<byte[], byte[]> producer = createTransactionalProducer();
               producer.beginTransaction();
               return new FlinkKafkaProducer.KafkaTransactionState(producer.getTransactionalId(), producer);
            case AT_LEAST_ONCE:
            case NONE:
               // Do not create new producer on each beginTransaction() if it is not necessary
               final FlinkKafkaProducer.KafkaTransactionState currentTransaction = currentTransaction();
               if (currentTransaction != null && currentTransaction.producer != null) {
                  return new FlinkKafkaProducer.KafkaTransactionState(currentTransaction.producer);
               }
               return new FlinkKafkaProducer.KafkaTransactionState(initNonTransactionalProducer(true));
            default:
               throw new UnsupportedOperationException("Not implemented semantic");
         }
      }

      和 AT_LEAST_ONCE 另一个不同的地方在于 checkpoint 的时候,会将事务相关信息保存到变量 nextTransactionalIdHintState 中,这个变量存储的信息会作为 checkpoint 中的一部分进行持久化。

      if (getRuntimeContext().getIndexOfThisSubtask() == 0 && semantic == FlinkKafkaProducer.Semantic.EXACTLY_ONCE) {
         checkState(nextTransactionalIdHint != null, "nextTransactionalIdHint must be set for EXACTLY_ONCE");
         long nextFreeTransactionalId = nextTransactionalIdHint.nextFreeTransactionalId;
      
         // If we scaled up, some (unknown) subtask must have created new transactional ids from scratch. In that
         // case we adjust nextFreeTransactionalId by the range of transactionalIds that could be used for this
         // scaling up.
         if (getRuntimeContext().getNumberOfParallelSubtasks() > nextTransactionalIdHint.lastParallelism) {
            nextFreeTransactionalId += getRuntimeContext().getNumberOfParallelSubtasks() * kafkaProducersPoolSize;
         }
      
         nextTransactionalIdHintState.add(new FlinkKafkaProducer.NextTransactionalIdHint(
            getRuntimeContext().getNumberOfParallelSubtasks(),
            nextFreeTransactionalId));
      }

      小结:本节介绍了 Flink Kafka Producer 的基本实现原理,后续会详细介绍 Flink 在结合 kafka 的时候如何做到端到端的 Exactly Once 语义的。

      作者介绍:

      吴鹏,亚信科技资深工程师,Apache Flink Contributor。先后就职于中兴,IBM,华为。目前在亚信科技负责实时流处理引擎产品的研发。

      ]]>
      云原生网络代理 MOSN 多协议机制解析 | SOFAChannel#13 直播整理 Wed, 08 Apr 2020 03:03:11 +0800

      ,有趣实用的分布式架构频道。
      回顾视频以及 PPT 查看地址见文末。欢迎加入直播互动钉钉群 : 21992058,不错过每场直播。
      本文根据 SOFAChannel#13 直播分享整理,主题:云原生网络代理 MOSN 多协议机制解析。

      image.png

      大家好,我是今天的讲师无钩,目前主要从事蚂蚁金服网络代理相关的研发工作,也是 MOSN 的 Committer。今天要和大家分享的是《云原生网络代理 MOSN 多协议机制解析》,并介绍对应的私有协议快速接入实践案例以及对 MOSN 实现多协议低成本接入的设计进行解读。

      我们将按以下顺序进行介绍:

      • 多协议机制产生的背景与实践痛点;
      • 常见的协议扩展思路初探;
      • SOFABolt 协议接入实践;(重点)
      • MOSN 多协议机制设计解读;(重点)
      • 后续规划及展望;

      其中第三点「接入实践」是今天分享的重点,希望能给大家就「如何在 MOSN 中快速扩展私有协议接入」有一个具体的感受。另外「MOSN 如何实现多协议框架」也是很多人关心和问题,我们将摘选几个技术功能,对其背后的设计思考进行解读。

      MOSN 简介

      image.png

      云原生网络代理 MOSN 定位是一个全栈的网络代理,支持包括网络接入层(Ingress)、API Gateway、Service Mesh 等场景,目前在蚂蚁金服内部的核心业务集群已经实现全面落地,并经受了 2019 年双十一大促的考验。今天要向大家介绍的是云原生网络代理 MOSN 核心特性之一的多协议扩展机制,目前已经支持了包括 SOFABolt、Dubbo、TARS 等多个协议的快速接入。

      MOSN:https://github.com/mosn

      多协议机制产生的背景与实践痛点

      首先介绍一下多协议机制产生的背景。

      image.png

      前面提到,蚂蚁金服 2019 年双十一核心链路百分之百 Mesh 化,是业界当时已知的最大规模的 Service Mesh 落地,为什么我们敢这么做?因为我们具备能够让架构平滑迁移的方案。"兼容性"是任何架构演进升级都必然要面对的一个问题,这在早已实践微服务化架构的蚂蚁金服内部同样如此。为了实现架构的平滑迁移,需要让新老节点的外在行为尽可能的表现一致,从而让依赖方无感知,这其中很重要的一点就是保持协议兼容性。

      因此,我们需要在 Service Mesh 架构下,兼容现有微服务体系中的通信协议——也就是说需要在 MOSN 内实现对目前蚂蚁金服内部通信协议的扩展支持。

      image.png

      基于 MOSN 本身的扩展机制,我们完成了最初版本的协议扩展接入。但是在实践过程中,我们发现这并不是一件容易的事情:

      • 相比编解码,协议自身的处理以及与框架集成才是其中最困难的环节,需要理解并实现包括请求生命周期、多路复用处理、链接池等等机制;
      • 社区主流的 xDS 路由配置是面向 HTTP 协议的,无法直接支持私有协议,存在适配成本;

      基于这些实践痛点,我们设计了 MOSN 多协议框架,希望可以降低私有协议的接入成本,加快普及 ServiceMesh 架构的落地推进。

      常见的协议扩展思路初探

      前面介绍了背景,那么具体协议扩展框架要怎么设计呢?我们先来看一下业界的思路与做法。

      协议扩展框架 - Envoy

      image.png
      注:图片来自 Envoy 分享资料

      第一个要介绍的是目前发展势头强劲的 Envoy。从图上可以看出,Envoy 支持四层的读写过滤器扩展、基于 HTTP 的七层读写过滤器扩展以及对应的 Router/Upstream 实现。如果想要基于 Envoy 的扩展框架实现 L7 协议接入,目前的普遍做法是基于 L4 filter 封装相应的 L7 codec,在此基础之上再实现对应的协议路由等能力,无法复用 HTTP L7 的扩展框架。 

      协议扩展框架 - Nginx

      image.png

      第二个则是老牌的反向代理软件 Nginx,其核心模块是基于 Epoll/Kqueue 等 I/O 多路复用技术之上的离散事件框架,基于事件框架之上构建了 Mail、Http 等协议模块。与 Envoy 类似,如果要基于 Nginx 扩展私有协议,那么也需要自行对接事件框架,并完整实现包括编解码、协议处理等能力。

      协议扩展框架 - MOSN

      image.png

      最后回过头来,我们看一下 MOSN 是怎么做的。实际上,MOSN 的底层机制与 Envoy、Nginx 并没有核心差异,同样支持基于 I/O 多路复用的 L4 读写过滤器扩展,并在此基础之上再封装 L7 的处理。但是与前两者不同的是,MOSN 针对典型的微服务通信场景,抽象出了一套适用于基于多路复用 RPC 协议的扩展框架,屏蔽了 MOSN 内部复杂的协议处理及框架流程,开发者只需要关注协议本身,并实现对应的框架接口能力即可实现快速接入扩展。

      三种框架成本对比

      图片 3.png

      最后对比一下,典型微服务通信框架协议接入的成本,由于 MOSN 针对此类场景进行了框架层面的封装支持,因此可以节省开发者大量的研发成本。

      SOFABolt 协议接入实践

      初步了解多协议框架的设计思路之后,让我们以 SOFABolt 协议为例来实际体验一下协议接入的过程。

      SOFABolt 简介

      image.png

      这里先对 SOFABolt 进行一个简单介绍,SOFABolt 是一个开源的轻量、易用、高性能、易扩展的  RPC 通信框架,广泛应用于蚂蚁金服内部。

      SOFABolt:https://github.com/sofastack/sofa-bolt

      基于 MOSN 的多协议框架,实际编写了 7 个代码文件,一共 925 行代码(包括 liscence、comment 在内)就完成了接入。如果对于协议本身较为熟悉,且具备一定的 MOSN/Golang 开发经验,甚至可以在一天内就完成整个协议的扩展,可以说接入成本是非常之低。

      image.png

      Github:
      https://github.com/mosn/mosn/tree/master/pkg/protocol/xprotocol/bolt

      下面让我们进入正题,一步一步了解接入过程。

      Step1:确认协议格式

      第一步,需要确认要接入的协议格式。为什么首先要做这个,因为协议格式是一个协议最基本的部分,有以下两个层面的考虑:

      • 任何协议特性以及协议功能都能在上面得到一些体现,例如有无 requestId/streamId 就直接关联到协议是否支持连接多路复用;
      • 协议格式与报文模型直接相关,两者可以构成逻辑上的映射关系;而这个映射关系也就是所谓的编解码逻辑;

      image.png

      以 SOFABolt 为例,其第一个字节是协议 magic,可以用于校验当前报文是否属于 SOFABolt 协议,并可以用于协议自动识别匹配的场景;第二个字节是 type,用于标识当前报文的传输类型,可以是 Request / RequestOneway / Response 中的一种;第三个字节则是当前报文的业务类型,可以是心跳帧,RPC 请求/响应等类型。后面的字段就不一一介绍了,可以发现,理解了协议格式本身,其实对于协议的特性支持和模型编解码就理解了一大半,因此第一步协议格式的确认了解是重中之重,是后续一切工作开展的前提。

      Step2:确认报文模型

      image.png

      顺应第一步,第二步的主要工作是确认报文编程模型。一般地,在第一步完成之后,应当可以很顺利的构建出相应的报文模型,SOFABolt 例子中可以看出,模型字段设计基本与协议格式中的 header / payload 两部分相对应。有了编程模型之后,就可以继续进行下一步——基于模型实现对应的框架扩展了。

      Step3:接口实现 - 协议

      image.png

      协议扩展,顾名思义,是指协议层面的扩展,描述的是协议自身的行为(区别于报文自身)。

      目前多协议框架提供的接口包括以下五个:

      • Name:协议名称,需要具备唯一性;
      • Encoder:编码器,用于实现从报文模型到协议传输字节流的映射转换;
      • Decoder:解码器,用于实现从协议传输字节流到报文模型的映射转换;
      • Heartbeater:心跳处理,用于实现心跳保活报文的构造,包括探测发起与回复两个场景;
      • Hijacker:错误劫持,用于在特定错误场景下错误报文的构造;

      Step3:接口实现 - 报文

      image.png

      前面介绍了协议扩展,接下里则是报文扩展,这里关注的是单个请求报文需要实现的行为。

      目前框架抽象的接口包括以下几个:

      • Basic:需要提供 GetStreamType、GetHeader、GetBody 几个基础方法,分别对应传输类型、头部信息、载荷信息;
      • Multiplexing:多路复用能力,需要实现 GetRequestId 及 SetRequestId;
      • HeartbeatPredicate:用于判断当前报文是否为心跳帧;
      • GoAwayPredicate:用于判断当前报文是否为优雅退出帧;
      • ServiceAware:用于从报文中获取 service、method 等服务信息;

      举个例子

      image.png

      这里举一个例子,来让大家对框架如何基于接口封装处理流程有一个体感:服务端心跳处理场景。当框架收到一个报文之后:

      • 根据报文扩展中的 GetStreamType 来确定当前报文是请求还是响应。如果是请求则继续 2;
      • 根据报文扩展中的 HeartbeatPredicate 来判断当前报文是否为心跳包,如果是则继续 3;
      • 当前报文是心跳探测(request + heartbeat),需要回复心跳响应,此时根据协议扩展中的 Heartbeater.Reply 方法构造对应的心跳响应报文;
      • 再根据协议扩展的 Encoder 实现,将心跳响应报文转换为传输字节流;
      • 最后调用 MOSN 网络层接口,将传输字节流回复给发起心跳探测的客户端;

      当协议扩展与报文扩展都实现之后,MOSN 协议扩展接入也就完成了,框架可以依据协议扩展的实现来完成协议的处理,让我们实际演示一下 SOFABolt 接入的 example。

      Demo 地址:https://github.com/mosn/mosn/tree/master/examples/codes/sofarpc-with-xprotocol-sample

      MOSN 多协议机制设计解读

      通过 SOFABolt 协议接入的实践过程,大家对如何基于 MOSN 来做协议扩展应该有了一个初步的认知。那么 MOSN 多协议机制究竟封装了哪些逻辑,背后又是如何思考设计的?接下来将会挑选几个典型技术案例为大家进行解读。

      协议扩展框架

      协议扩展框架 -  编解码

      image.png

      最先介绍的是编解码机制,这个在前面 SOFABolt 接入实践中已经简单介绍过,MOSN 定义了编码器及解码器接口来屏蔽不同协议的编解码细节。协议接入时只需要实现编解码接口,而不用关心相应的接口调用上下文。

      协议扩展框架 - 多路复用

      图片 1.png

      接下来是多路复用机制的解读,这也是流程中相对不太好理解的一部分。首先明确一下链接多路复用的定义:允许在单条链接上,并发处理多个请求/响应。那么支持多路复用有什么好处呢?

      以 HTTP 协议演进为例,HTTP/1 虽然可以维持长连接,但是单条链接同一时间只能处理一个请求/相应,这意味着如果同时收到了 4 个请求,那么需要建立四条 TCP 链接,而建链的成本相对来说比较高昂;HTTP/2 引入了 stream/frame 的概念,支持了分帧多路复用能力,在逻辑上可以区分出成对的请求 stream 和响应 stream,从而可以在单条链接上并发处理多个请求/响应,解决了 HTTP/1 链接数与并发数成正比的问题。

      类似的,典型的微服务框架通信协议,如 Dubbo、SOFABolt 等一般也都实现了链接多路复用能力,因此 MOSN 封装了相应的多路复用处理流程,来简化协议接入的成本。让我们跟随一个请求代理的过程,来进一步了解。

      图片 2.png

      1. MOSN 从 downstream(conn=2) 接收了一个请求 request,依据报文扩展多路复用接口 GetRequestId 获取到请求在这条连接上的身份标识(requestId=1),并记录到关联映射中待用;
      2. 请求经过 MOSN 的路由、负载均衡处理,选择了一个 upstream(conn=5),同时在这条链接上新建了一个请求流(requestId=30),并调用文扩展多路复用接口 SetRequestId 封装新的身份标识,并记录到关联映射中与 downstream 信息组合;
      3. MOSN 从 upstream(conn=5) 接收了一个响应 response,依据报文扩展多路复用接口 GetRequestId 获取到请求在这条连接上的身份标识(requestId=30)。此时可以从上下游关联映射表中,根据 upstream 信息(connId=5, requestId=30) 找到对应的 downstream 信息(connId=2, requestId=1);
      4. 依据 downstream request 的信息,调用文扩展多路复用接口 SetRequestId 设置响应的 requestId,并回复给 downstream;

      在整个过程中,框架流程依赖的报文扩展 Multiplexing 接口提供的能力,实现了上下游请求的多路复用关联处理,除此之外,框架还封装了很多细节的处理,例如上下游复用内存块合并处理等等,此处限于篇幅不再展开,有兴趣的同学可以参考源码进行阅读。

      统一路由框架

      image.png

      接下来要分析的是「统一路由框架」的设计,此方案主要解决的是非 HTTP 协议的路由适配问题。我们选取了以下三点进行具体分析:

      • 通过基于属性匹配(attribute-based)的模式,与具体协议字段解耦;
      • 引入层级路由的概念,解决属性扁平化后带来的线性匹配性能问题;
      • 通过变量机制懒加载的特定,按需实现深/浅解包;

      统一路由框架 – 基于属性匹配

      image.png

      首先来看一下典型的 RDS 配置,可以看到其中的 domains、path 等字段,对应的是 HTTP 协议里的域名、路径概念,这就意味着其匹配条件只有 HTTP 协议才有字段能够满足,配置结构设计是与 HTTP 协议强相关的。这就导致了如果我们新增了一个私有协议,无法复用 RDS 的配置来做路由。

      那么如何解决配置模型与协议字段强耦合呢?简单来说就是把匹配字段拆分为扁平属性的键值对(key-value pair),匹配策略基于键值对来处理,从而解除了匹配模型与协议字段的强耦合,例如可以配置 key: $http_host,也可以配置 key:$dubbo_service,这在配置模型层面都是合法的。

      但是这并不是说匹配就有具体协议无关了,这个关联仍然是存在的,只是从强耦合转换为了隐式关联,例如配置 key: $http_host,从结构来说其与 HTTP 协议并无耦合,但是值变量仍然会通过 HTTP 协议字段来进行求值。

      统一路由框架 -  层级路由

      image.png

      在引入「基于属性的匹配」之后,我们发现了一个问题,那就是由于属性本身的扁平化,其内在并不包含层级关系。如果没有层级关系,会导致匹配时需要遍历所有可能的情况组合,大量条件的场景下匹配性能近似于线性的 O(n),这显然是无法接受的。 

      举例来说,对于 HTTP 协议,我们总是习惯与以下的匹配步骤:

      • 匹配 Host(:authority) ;
      • 匹配 Path ;
      • 匹配 headers/args/cookies ;

      这其实构成了一个层级关系,每一层就像是一个索引,通过层级的索引关系,在大量匹配条件的情况下仍然可以获得一个可接受的耗时成本。但是对于属性(attribute),多个属性之间并没有天然的层级关系(相比于 host、path 这种字段),这依赖于属性背后所隐式关联的字段,例如对于 Dubbo 协议,我们希望的顺序可能是:

      • 匹配 $dubbo_service;
      • 匹配 $dubbo_group;
      • 匹配 $dubbo_version;
      • 匹配 $dubbo_attachments_xx;

      因此在配置模型上,我们引入了对应的索引层级概念,用于适配不同协议的结构化层级路由,解决扁平属性的线性匹配性能问题。

      统一路由框架 - 浅解包优化

      image.png

      最后,介绍一下浅解包优化的机制。利用 MOSN 变量懒加载的特性,我们可以在报文解析时,先不去解析成本较高的部分,例如 dubbo 协议的 attachments。那么在代理请求的实际过程中,需要使用到 attachments 里的信息时,就会通过变量的 getter 求值逻辑来进行真正的解包操作。依靠此特性,可以大幅优化在不需要深解包的场景下 dubbo 协议代理转发的性能表现,实现按需解包。

      解读总结

      最后,对设计部分的几个技术案例简单总结一下,整体的思路仍然是对处理流程进行抽象封装,并剥离可扩展点,从而降低用户的接入成本。

      在协议扩展支持方面:

      • 封装编解码流程,抽象编解码能力接口作为协议扩展点
      • 封装协议处理流程,抽象多路复用、心跳保活、优雅退出等能力接口作为协议扩展点

      在路由框架方面:

      • 通过改为基于属性匹配的机制,与具体协议字段解耦,支持多协议适配;
      • 引入层级路由机制,解决属性扁平化的匹配性能问题;
      • 利用变量机制懒加载特性,按需实现深/浅解包;

      后续规划及展望

      更多流模式支持、更多协议接入

      image.png

      当前 MOSN 多协议机制,已经可以比较好的支持像 Dubbo、SOFABolt 这样基于多路复用流模型的微服务协议,后续会继续扩展支持的类型及协议,例如经典的 PING-PONG 协议、Streaming 流式协议,也欢迎大家一起参与社区建设,贡献你的 PR。

      社区标准方案推进

      image.png

      与此同时,我们注意到 Istio 社区其实也有类似的需求,希望设计一套协议无关的路由机制——"Istio Meta Routing API"。其核心思路与 MOSN 的多协议路由框架基本一致,即通过基于属性的路由来替代基于协议字段的路由。目前该草案还处于一个比较初级的阶段,对于匹配性能、字段扩展方面还没有比较完善的设计说明,后续 MOSN 团队会积极参与社区方案的讨论,进一步推动社区标准方案的落地。

      以上就是本期分享的全部内容,如果大家对 MOSN 有问题以及建议,欢迎在群内与我们交流。

      本期视频回顾以及 PPT 查看地址

      https://tech.antfin.com/community/live/1131

      MOSN Logo 社区投票结果公示

      MOSN 的 Logo 升级,在进过社区投票后,在本期直播结束后截止。截止 2020年3月26日20:00,有效票数 35 票。方案一 25 票,占比 71.43%;方案二 2 票,占比 5.71%;方案三 8 票,占比 22.86%。最终,方案一大比分胜出,方案一 为 MOSN 最终 Logo 。感谢大家参与社区投票~

      image.png

      恭喜以下社区同学,你们投票与最终结果一致~Github ID @CodingSinger @trainyao @JasonRD @taoyuanyuan @wangfakang @ujjboy @InfoHunter @Tony-Hangzhou @GLYASAI @carolove @tanjunchen @bruce-sha @hb-chen @luxious @echooymxq @qunqiang @f2h2h1 @sunny0826 @token01 @Ayi- @cytyikai @fanyanming2016 @inkinworld @dllen @meua

      具体 issue 地址:https://github.com/mosn/community/issues/2

      ]]>
      校招经验贴 | 那天我喝着咖啡撸着猫,就接到了支付宝offer… Wed, 08 Apr 2020 03:03:11 +0800

      就现在!蚂蚁「校招季」重磅来袭!除了介绍蚂蚁的技术大咖,我们还邀请了一些通过校招来到蚂蚁的过来人分享他们的通关经验和心得,这里随时可能有行业技术大咖和你的直系学长学姐出没哦~ 「校招季」栏目会持续输出有关“蚂蚁校招”的丰富内容,敬请期待!

      大学期间,在一家有金融背景的互联网金融创业公司实习,当时刚好在调研信用评分的时候,看到了蚂蚁金服,对于这家公司的小微金融业务有了一定的了解,这个时候心里就埋下了一粒种子。

      准备充分,然而结果却很意外

      大学时光如梭,一转眼就到了校招季,当时抱着对BAT公司巨大技术挑战和复杂管理协同经验的憧憬,选择了在本科阶段迈出应届生招聘这一步。恰好当时有个同学在蚂蚁金服实习,为了把握这个内推的机会,我就花了一整天的时间,把简历好好整理了下,期间也是在简历阶段费了非常多的时间,因为希望面试官看到简历的时候,能看到我擅长的一面,并且能引导面试官在一些我非常了解的技术点上,深入去讨论。这个在准备简历的阶段是非常重要的。准备完简历后,就开始准备笔试和面试。

      一开始肯定是需要把大学里面的一些网络、系统、语言基础、数据库、数据结构与算法等基础知识再好好的复写归纳归纳,把逃的课又花时间补回来了。当时是面试的Java研发岗,主要就深入学习了Java并发编程艺术、JVM、Spring框架等,深入看些了框架源码,了解一些框架的设计模式和思想。其实开源框架的代码都是非常值得学习的,无论是一些核心的技术实现,还是一些巧妙的技术解法,甚至于代码的风格,阅读开源代码,都是一个非常有效的实践过程。建议阅读开源代码的时候可以借助UML、流程图或者自己习惯的记录、分析方式,辅助理解开源项目的整体设计思路。后面顺利地参加完了整个笔试流程,通过了笔试的考核,进入了面试阶段。一面、二面的时候,和面试官都交流的非常流畅,无论是在技术点上,还是实践项目上,自我感觉还是得到了面试官的认可,但是在三面的时候,就稍微有一些挫败感。其实三面的技术问题范围也和一面、二面差不多,只是在技术深度上,问了很多为什么,以及还能怎么优化,确实在一些开源项目的阅读上,之前没有更多的去思考这些问题,仅仅停留在看懂层面。面完之后心里面还是有一丝小小的紧张,感觉可能就因为这一个问题会错失这次机会。

      中间还有一段小插曲。在三面的时候,我其实是在上厕所,当时也没有拒绝面试官,就这样继续聊下去,现在回想起来,那会三面的状态并没有发挥的特别好,也没有在一个相对充分的环境和思考中准备面试。过了几天,收到了一个HR的电话,以当时网上的各种面试经验来看,基本上到了HR面试环节,基本上offer就稳了。可是没想到在过了几天的焦急等待后,还是收到了被婉拒的通知,当时心里面还是非常失落的,自己也大概知道应该是因为三面那次技术深度的灵魂拷问没有回答的很好导致的,后续在紧张的秋招环境中,又继续回过神投入各大公司的招聘中,同时一边又继续学习巩固技术。

      柳暗花明又一村,终于得偿夙愿

      差不多到了11月份,基本上手上也有了几份大厂的offer。还记得一天正在成都的猫咖喝着咖啡撸着猫,突然一个杭州的电话过来,正是蚂蚁金服的电话,电话里说现在有一个机会,问是否还有意愿来蚂蚁金服,在他简单介绍了下工作的内容和岗位后,抱着将信将疑的态度,暂且接受了。后面接着HR也来了电话,简单聊了下薪资待遇后,我提出了一个要在毕业前去公司实习的要求,当时的想法是,想用实习的机会去看下,这样一个工作的岗位和内容是否适合我,HR也答应了我的请求。

      现在,我在蚂蚁金服技术风险部。可以说,这是一个全世界一流的技术团队,不仅有学术大佬,有业界大牛,还有从Google/AWS/MicroSoft的海归和全世界最核心的金融级场景。所涉及到的业务场景量级以及技术深度挑战,都是行业类顶尖的,而且团队基本上都是90后,团队非常的年轻有活力,平时团队氛围也非常的欢乐,也是一群有着工程师情怀,并怀揣技术改变世界梦想的人,最终我也是在亲身实习体验后,选择了留下来。