「深度学习福利」大神带你进阶工程师,立即查看>>>
前言
首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute) : 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考 。
Fun : Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考 。
NAS : 阿里云文件存储NAS是一个可共享访问,弹性扩展,高可靠,高性能的分布式文件系统。在函数计算的场景中,由于其有代码包的限制,可以使用 NAS 存放一些不经常变动的文件,比如数据模型、静态资源等。 参考 。
ROS : 阿里云资源编排服务(ROS)助您简化云计算资源的管理。您可以遵循ROS定义的模板规范,在模板中定义所需云计算资源的集合及资源间依赖关系。ROS自动完成所有资源的创建和配置,实现自动化部署和运维。更多文档 参考 。
备注: 本文介绍的技巧需要 Fun 版本大于等于 3.4.0。
基本上所有的 faas 平台为了优化函数的冷启动,都会加入代码包的限制。阿里云 函数计算 (FC)也不例外。FC 要求压缩后的代码包大小不超过 50MB。由于用户的函数代码可能需要大量的依赖库,所以代码包很容易达到函数计算设定的阈值。
传统解决方式
基于平台硬性要求下,依然有很多途径来解决函数计算上传代码包大小受限的问题。下面列举 3 种常见的解决方案。 备注 : 包括不局限于以下方案。
1. 提交工单
提交函数计算工单,由后台人员为您账号开通上传限制白名单。但提交工单依然有代码包的限制。部署时代码包上传时间长,并且也增加了函数的冷启动时间,影响函数性能。 PS:通过使用 预留模式 可以完全去除冷启动,由于超出本文范围,这里不再阐述。
2. 利用 OSS 来实现
对代码包进行分类,除项目代码和少量依赖库可以在创建函数时上传到函数计算,用户将部分依赖库预先上传到 OSS,并在函数被触发执行时开始从 OSS 上加载依赖, 这类依赖的加载操作均可定义应用层冷启动,当加载依赖结束后,应用层冷启动才结束,函数的处理逻辑才开始执行,应用层冷启动的开销往往会导致毛刺的产生,影响函数的性能。您可以将从 OSS 加载代码包的代码逻辑放在 initializer 函数中。既可以解决函数计算对上传代码包的限制问题,又不影响函数的性能。但需要用户进行额外的操作。参考 使用 initializer 接口解决函数计算上传代码包大小受限问题 。
3. 基于文件系统 Nas
我们通过 NAS 存放一些体积比较大且不易变动的资源。这使得即使在依赖比较大的场景下依旧适用。NAS 在帮助函数计算解决大依赖问题的同时,由于其自身的配置也比较复杂,也增加了函数计算的使用难度。如何管理 Nas 资源、上传本地资源到Nas 以及服务级别的 Nas 相关配置参考 开发函数计算的正确姿势 —— 使用 Fun NAS 管理 NAS 资源 。
本文旨在介绍第四种方式 -- 使用 Fun 工具来解决函数计算对上传代码包的限制,使得部署步骤简单明了,不需关心额外的配置。同时也展现 Fun 工具对大依赖场景的顺滑体验,Let's go!
注 :已经获得 fc 支持的 runtime 有 nodejs6,nodejs8,nodejs10,python2.7,python3,java8,php7.2,dotnetcore2.1,custom。目前基于所有 fc 支持的 runtime,大依赖场景下除 php7.2 和 dotnetcore2.1 其他都支持。
环境准备
Fun 安装教程 可以直接在这里下载二进制版本的 Fun,解压后即可使用。
执行 fun --version 检查 Fun 是否安装成功。 $ fun --version 3.4 . 0
Fun 判定大依赖的标准是什么?
fun install 是 fun 工具的一个子命令,用于安装 pip,apt 依赖等,提供了命令行接口和 Funfile 描述文件两种形式。对于 fun install 安装的依赖,当 fun deploy 部署时会自动处理大依赖。
当 Fun 检测打包的代码压缩后超过限制(50M),会根据对应的 runtime 分离大依赖和代码。Fun 会将大依赖目录分为:系统依赖和语言依赖。系统依赖的本地路径为 .fun/root ,语言依赖根据函数 runtime 得到,各个 runtime 对应的大依赖目录映射如下:
语言(runtime) 大依赖目录(directory)
nodejs6 | node_modules nodejs8 | node_modules |
---|
nodejs10 | node_modules | python2.7 | .fun/python | python3 | .fun/python java8 | custom .fun/build/artifacts | / | 自定义执行环境 custom 大依赖目录为 / ,可以理解为其他 runtime 大依赖目录的合集。例如:函数 runtime 为 custom,若目录下存在 node_modules 或 .fun/python 等,Fun 在部署向导过程中会把它们都认定为大依赖,会分别对其处理。 Fun deploy 对大依赖的支持 函数计算的命令行工具 Fun 现在原生支持了这种大依赖部署,不需要任何额外操作。仅仅执行 fun deploy : $ fun deploy 整体流程如下图所示: fun deploy 会自动完成依赖的部署。而当检测到打包的函数目录超过了平台的限制时,会进入到配置向导,帮助用户自动化地配置。即上图可以理解为:Fun 通过内置 NAS(阿里云文件存储)解决方案,可以一键帮用户创建、配置 NAS,并上传依赖到 NAS 上。而函数计算在运行时,可以自动从 NAS 读取到函数依赖。 大依赖向导 大依赖向导部分截图如下: Fun 部署当前函数时,检测到压缩后(.zip)依赖超过了 50M,提示配置向导(后续日志省略...)。只需要输入回车或 yes 即可。最后 Fun 会自动完成配置,成功部署资源到函数计算。 体验升级 fun deploy 大依赖向导完成后,函数会部署到函数计算并对外提供服务。此时大依赖和代码通过 NAS 进行了分离,再次部署时打包本地代码目录时由于没有了大依赖,所以部署速度会非常的快。 这里推荐一篇使用 fun deploy 进行大依赖部署的实战案例,展示了 Fun 工具对大依赖场景的顺滑体验 Serverless 实战 —— 快速开发一个分布式 Puppeteer 网页截图服务 。 Fun package 对大依赖的支持 Fun Package 是用来将代码、编译产物、静态资源等本地资源上传到 OSS 的功能。使用 fun Package 的场景,通常是,想仅仅通过一个模板文件进行部署的场景。比如,本地开发完成后,可以通过 fun package ,将模板依赖的本地资源上传到 OSS,这样,无论是在其他服务器上部署,还是使用 ROS 部署时,仅仅通过一个文本格式的模板文件,就可以完成了。推荐阅读 Fun Package 功能介绍 。 非大依赖场景 流程如下图所示: 通过 Fun Package 可以将模板文件包含的 本地资源 一键上传到 OSS 上,完成资源的打包操作。 模版文件差异 将打包后的模板文件(template.packaged.yml)与原文件相比较,可以发现,差异仅仅在使用了本地资源的场景,比如: - CodeUri: './' + CodeUri: 'oss://bucket/PackageDemo/function/39ce6e9109a23d313bc267b1a5211273' 大依赖场景 流程如下图所示: 当遇到打包的函数体积过大时,同样会进入大依赖向导,Fun 内置 Ros 的解决方案,帮你完成自动配置。同时 Fun 会分开大依赖和代码并分别上传到 OSS。这样做的目的下文中会有提到。 模版文件差异 大依赖场景下打包完成后生成的 template.packaged.yml 模版文件会与非大依赖场景下有所不同,除上述 CodeUri 的差异外,还会新增许多资源描述作为使用 Ros 部署时的前置条件,例如 NasCpFunc , 这里只介绍一种,其它不做赘述。 NasCpFunc: Type: 'Aliyun::Serverless::Function' Properties: Handler : index.cpFromOssToNasHandler Runtime: nodejs8 CodeUri: 'oss://ellison-hongkong/9e610f5540e21ace83d5b742241da6aa' MemorySize: 3072 Timeout : 300 NasCpFunc 为大依赖场景下 Fun 为用户内置的资源函数,可以将它理解为一个辅助函数。当用 Ros 方式部署时,将自动执行辅助函数。它用于实现从 OSS 上下载大依赖(.zip)以及解压到 Nas 的功能。这也就是为什么上述 fun pakckage 打包时,要将大依赖和代码分离开并分别上传到 OSS 的原因。 Fun Packge + Ros 部署实战 Serverless 实战——使用 Rendertron 搭建 Headless Chrome 渲染解决方案 使用 Rendertron + 函数计算 快速搭建一个可以直接用于生产的 Headless Chrome 渲染解决方案,以便于帮助网站更好的进行 SEO。基于文章,我们将升级文章中 一键部署 的体验。您可以参照上述文章中步骤,其中 依赖安装 , 项目编译 等均无需额外操作。 Rendertron 项目代码依赖过大,基于 Fun 工具对大依赖项目的支持,现将其原 Fun deploy 部署改造为 Fun Packge + Ros 部署方式。Fun package 自动处理大依赖上传到 OSS,Ros 部署将大依赖从 OSS 解压到 Nas,同时模版中描述的资源自动创建成功。基于函数计算,项目的服务架构如下: 1. Fun pakcage 按照 RenderTron 文章中步骤操作,在 一键部署 前,执行 fun package 命令: fun package --oss-bucket aliyun-ellison 这里的 --oss-bucket 名称为自己所拥有读写权限的 OSS 的 Bucket 名称。完整日志如下: 2. Ros 部署 ROS 通过 Transform 宏实现了将函数计算的模板语法转换为 ROS 支持的语法。这意味着对于 Fun 规范文档 里描述的语法规则,ROS 是同样支持的。同时, ROS 支持的资源 也能在 Fun 模板文件中进行声明了,比如 RAM、 函数工作流 等等。 在体验上,由于 ROS 部署,要求资源必须“云化”。也就是没办法直接使用本地的代码资源。必须先通过 fun package 命令将资源上传到 oss。 可见这一步我们已经完成,不管是大依赖场景还是非大依赖场景,fun package 打包完成后,后续的部署操作,只需要完全基于这个打包后的模板文件(template.packaged.yml)即可。不再依赖本地的代码等资源,可以简化部署的难度。 最后将资源通过 ROS 的方式进行部署,推荐阅读 开发函数计算的正确姿势 —— 使用 ROS 进行资源编排 。 fun deploy --use-ros -- stack -name bucket-name --stack-name 表示要部署的环境,可以基于该名称的不同,建立多套开发环境,比如 test、staging、prod。 3. 验证 可通过上述 RenderTron 文章中提到的方式验证,这里不做赘述。 常见问题梳理 FAQ 1. 大依赖被一个服务下的两个函数同时引用,Fun 是怎么处理的呢? Fun 支持大依赖的场景是函数级别的,即当打包某一函数时发现超过限制才会进入向导。当两个函数处于相同 runtime 和 codeUri,Fun 会在结束第一次向导时,同时自动配置第二个函数,确保部署后,两个函数都部署成功且可用。 2. 大依赖自动配置后,如果我本地添加了新的依赖,部署时会自动将依赖更新到 Nas 吗? 不会。如果添加了新的依赖,比如 node_modules 目录添加了新的依赖库,需要在 template.yml 模版文件所在目录执行 fun nas sync,将本地 nas 资源同步到 nas 服务。如果修改了代码,只需要使用 fun deploy 重新部署即可。由于大依赖和代码通过 NAS 进行了分离,依赖通常不需要频繁变化,所以调用的频率比较低,而 fun deploy 的由于没有了大依赖,部署速度也会非常的快。 3. 为什么 java8 环境 Fun 支持大依赖的目录是 .fun/build/artifacts? 在很多场景,编译型语言从源码距离交付物其实是有一定的距离,比如 java,写完 java 代码后,还要考虑如何编译、打包的问题。Fun build 的职责就是完成从源码到交付产物的构建过程,推荐阅读 《开发函数计算的正确姿势 —— 使用 Fun Build 构建函数》 。 Fun build 会将编译打包后的交付产物 copy 到 .fun/build/artifacts 目录,在部署时检测到代码大小超过限制,自然会去 .fun/build/artifacts 下查找对应 serviceName/functionName 目录,并将所有的 jar 包上传到 Nas。所以 Fun 大依赖部署支持 java8 是以 Fun build 的场景为基础。未来 Fun 会集成更多的解决方案,敬请期待! 总结 Fun 通过内置 NAS(阿里云文件存储)解决方案,可以一键帮用户创建、配置 NAS,并上传依赖到 NAS 上。而函数计算在运行时,可以自动从 NAS 读取到函数依赖。同时也展现 Fun 工具对大依赖场景的顺滑体验。 如果大家在使用 Fun 的过程中遇到了一些问题,可以在 github 上提 issue ,或者加入我们的钉钉群 11721331 进行反馈。 查看更多:https://yq.aliyun.com/articles/744357?utm_content=g_1000104314 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 18年有个数学家宣布成功证明了RH,引起了业界轰动,虽然最后发现其证明错了,但也由此引发众人对RH的关注,上百年来众多数学家针对RH开展的各种传奇一般的工作过程也颇为有趣。 《素数之恋》的作者称“如果你看了本书还是不能理解RH,那你永远都不会理解了”。 第一章 从纸牌游戏开始说起,介绍了级数、收敛等术语。 第三章 介绍素数是无穷的,并找到N每大100倍,N/π(N)增加7的关系,从而提出了对数的公式(素数定理):π(N) ~N/lnN,简写为PNT 第五章 从欧拉解决了巴塞尔问题开始(无穷和为什么会有π出现),介绍了幂运算,并引出了Riemann的Zeta函数ζ(S),还初步指出S>1时才收敛 第七章 把ζ(S)从无穷和换成了无穷素数积(欧拉积公式),并介绍了微积分,从而给出更好的PNT:Li(N) 第九章 先通过一个无穷和的例子说明了级别可能只表达了函数的一部分。然后经过某种方式让ζ(S)在<1时也收敛,这时能得到负偶数的零点(平凡零点) ,欧拉还给出了一个公式关联了ζ(S)和ζ(1-S),公式中有π,有Sin,而且1/2很特别,因为这时正好1-S=S 第十一章 从人类对数字的认识,介绍了根号-1的计算,得出了虚数,并引起复数和复平面:数字不再是实数轴的一条线,而是加上了虚数变成了二维平面。 第十二章 介绍了Riemann论文的内容:认为Li(N)是OK,而且给出了 消除误差的公式 ,这个公式就涉及到了“非平凡零点”。 https://zhuanlan.zhihu.com/p/45121719 有拟合曲线,堪称完美。而且曲线还表现了某种与傅利叶变换类似的特点。 第十三章 表现了自变量与函数值在复平面的对照关系,自变量蚂蚁的例子很生动,图13.6是ζ(S)的图形,其中有一些线条从正虚数变成负虚数,变化的“急转弯”可以猜就是非平凡零点所在。图13.8则是ζ(1/2+ti),看得出蚂蚁在临界线上不停的往上走时,函数值在转圈并不时回到零点。 第十四章 Li(N)-π(N)就是误差,数学家发现这个误差并不永远是正的,而是在一个范围内不断的有正负值,第一个被认为的反例(拟合曲线交叉点)在“斯克维斯数”:e-e-e-79(庞大的怪物)之前。 第十五章 数据结构与算法的书已经介绍过大O了,而本书这里正好用了大O来表示误差项的范围,这其实就是基于RH的应用之一。默比乌斯函数累加值对应的M函数则是另一个与RH有关联的推论--是个更强的推论:如果他成立,RH也成立;如果他不成立,不能说明RH不成立。 第十六章 展示了非平凡零点的计算历程,零点的精度情况。这些内容也存在于在很多关于RH的科普文章中,数学家们的探索过程还是很有意思的。 第十七章 介绍了矩阵,并把埃尔米特矩阵与ζ(S)关联,得出一个猜想:非平凡零点对应于某种埃尔米特算子的本征值。 第十八章 随机矩阵对量子计算有很大的帮助,正好有一个研究零点分布情况的数学家与一个研究量子能级分布的物理学家聊天,发现了零点分布与GUE算子在统计意义上一致。素数的分布与亚原子粒子的行为有什么关系? 第十九章 相当复杂,大体是有一个J(X)函数,由π(N)构成。然后Riemann用微积分,写出了由J(X)构成的ζ(S)函数,换言之,π(N)与ζ(S)存在着某种联系!(Riemann就是用ζ(S)的零点来消除Li函数的误差) 第二十章 也很复杂,看起来象是RH的一些应用,在一些与物理相关的计算能很好拟合(虽然大家不知道这是为什么)。现在还没找到反例,从逻辑学的角度来看,最后证明或证伪都有可能。 第二十一章 深度介绍了误差项消除公式(J(X)公式的第二项),太复杂已经看不懂了。只知道这个误差项的计算非常有效果,于是得出了Riemann论文的目标:如果你知道所有零点的值,我就能准确告诉你π(N)的值,而且我猜这些零点的实部都是1/2。 第二十二章 数学家不关心这些理论的实用性,所以别问如果RH成立,能不能去火星,能不能提高人类健康的问题。而且,问RH到达会不会成立的问题也没用,数学家现在真的不知道。 「深度学习福利」大神带你进阶工程师,立即查看>>> 场景描述 本实践适用于使用弹性高性能计算EHPC+ 对象存储OSS运行仿真软件进行模型仿真 的场景中,这里运行的是LAMMPS这款开 源的仿真软件,数据通过OSS上传。 部署架构图 解决问题 1. 使用EHPC运行工业仿真软件 2. 使用OSS存储数据和代码 3. 可视化计算结果 产品列表 l 弹性高性能计算E-HPC l 对象存储OSS 查看更多:https://www.aliyun.com/acts/best-practice/preview?id=52161 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 按照本文档操作,您可以快速上手阿里云CDN,加速您的域名。快速入门主要包含开通CDN服务、添加加速域名和配置CNAME三个步骤。 前提条件 开通CDN服务前,请确保您已经完成阿里云账号注册和实名认证。如果没有,请分别参见 账号注册 和 实名认证 。 步骤一:开通CDN服务 登录 CDN控制台 ,开通CDN服务。 在 云产品开通页 ,选择适合您的 计费类型 。在这里,您可以选择 按使用流量计费 或 按带宽峰值计费 。 选中 我已阅读并同意《内容分发网络CDN(按量付费)服务协议》 复选框,单击 立即开通 。 在 确认订单 页面,单击 管理控制台 ,进入 CDN管理控制台 。 步骤二:添加加速域名 在左侧导航栏,单击 域名管理 。 在 域名管理 页面,单击 添加域名 。 配置 加速域名 、 业务类型 、 源站信息 、 端口 和 加速区域 。 说明 每个配置项的具体配置说明,请参见 添加加速域名 。 单击 下一步 。 出现 加速域名添加成功 弹窗。 步骤三:配置CNAME 加速域名添加成功后,会进入审核流程。当您的加速域名审核通过后,阿里云CDN会为您分配对应的CNAME地址。您需要将加速域名指向CNAME地址,访问加速域名的请求才能转发到CDN节点上,达到加速效果。 这里以您的域名在阿里云解析(原万网)为例。 获取加速域名的CNAME地址。 在左侧导航栏,单击 域名管理 。 进入 域名管理 页面,复制加速域名对应的CNAME值。 添加CNAME记录。 登录 云解析DNS控制台 。 进入 域名解析 页面,在您需要设置的域名后面,单击 解析设置 。 单击 添加记录 ,添加CNAME记录。 单击 确定 。 配置CNAME完毕。CNAME配置生效后,加速服务也会立即生效。 新增CNAME记录会实时生效,修改CNAME记录72小时之内生效。 验证CNAME配置是否生效。 打开Windows的CMD命令行程序。 输入 ping 或 dig 和加速域名。 如果下行出现 *.*kunlun*.com ,即表示CNAME配置已经生效,域名加速也已生效。 查看更多:https://help.aliyun.com/document_detail/141972.html 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 包年包月模式 计费项 :按所选购的套餐版本计费。 付费方式 :预付费。 计费周期 :按月/年计费,购买时生成账单付费。 扣费周期 :自购买当日起,根据所选择的购买时长(月/年)计算;购买时长支持选择一个月、三个月、半年、一年。 到期说明 :当您购买的防护服务到期时,服务自动停止。 距离服务到期前7天,您会收到短信或邮件,提醒您服务即将到期并提示您及时续费。如在服务到期前没有续费,则在服务到期时您将失去防护效果。 服务到期后Web应用防火墙服务中的配置将为您继续保留7天。如果7天内您完成续费则可以使用原有配置继续进行防护;否则在7天后,Web应用防火墙中的相关配置将被释放,您只能重新购买才能继续使用。 按量付费模式 计费项 :当日已添加防护的网站的QPS峰值 * 所选用的功能规格对应系数 付费方式 :后付费 扣费周期 :根据网站QPS峰值和当日已启用的功能规格系数按日计费。按照前一日实际发生的网站的QPS峰值及已启用的功能规格所对应的系数计算,生成后付费账单。 注意 开通按量付费实例后,如果WAF实例连续五天不存在业务流量且未产生后付费账单,实例将会自动释放。 说明 网站QPS峰值指的是当日从00:00:00-23:59:59之间访问网站的每秒请求数峰值。如果网站当日有请求访问,则QPS峰值最小计费单位为1。系统将每10秒统计一次QPS峰值,即一天共有 (24 小时 * 60 分钟/小时 * 60 秒/分钟) / 10 秒/次 = 8640个峰值点,自动去除QPS峰值 > 0 的前2‰个峰值点,即以第18个最高峰值点作为当日最终的网站QPS峰值。 网站QPS峰值不区分请求是否为攻击,即以Web应用防火墙接收到的您已配置的所有网站域名的每秒请求总数为准。 为了避免因遭受攻击导致QPS暴涨而产生巨额防护费用,当日单个网站最多以500次作为QPS峰值进行计费。即某网站因遭受攻击导致当日QPS峰值超过500次时,以500次作为该网站当日的QPS峰值。 只要您在Web应用防火墙管理控制台添加了域名配置,系统检测到对该网站域名的请求就会产生费用,与域名的DNS解析是否指向WAF实例无关。由于可能有各种因素导致网站域名流量经过WAF实例,因此如果您不想产生任何相关费用,请确保删除所有域名配置记录。 更多关于按量付费模式Web应用防火墙的计费说明,请查看 按量付费模式常见问题 。 欠费说明 欠费 :当您的账号出现欠费后,您的WAF实例将在第二日停止提供服务。在您的账号欠费后,WAF实例当天仍将正常提供服务并且生成当天的费用账单。 结清 :在WAF实例停止提供服务前,只要您将所欠费用结清,WAF实例将继续正常提供服务。 Web应用防火墙资源包 开通按量付费模式Web应用防火墙后,您可以 购买Web应用防火墙资源包 (预付费)来抵扣Web应用防火墙根据网站QPS峰值和已启用的功能规格折算后的QPS消费值。您购买Web应用防火墙资源包后,在实际使用过程中将优先抵扣您Web应用防火墙资源包中的剩余QPS消费值。相比按量付费模式,购买Web应用防火墙资源包可以以更低廉的价格使用按量付费模式的Web应用防火墙服务。 查看更多:https://help.aliyun.com/document_detail/28525.html 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> zookeeper集群基础安装可参考 https://my.oschina.net/dingyibo/blog/3160232 zk测试集群 3个节点组成的测试集群 server.1=192.168.189.133 server.2=192.168.189.135 server.3=192.168.189.130 一、zk集群间server节点认证配置( Server-Server mutual authentication ) 1、 ZooKeeper SASL配置 zoo.cfg添加如下内容 #Server-Server mutual authentication #开启sasl认证 quorum.auth.enableSasl=true #zk集群角色分为 leader和learner(follower、observer),其中 observer 节点没有投票权,即它不参与选举和写请求的投票。 quorum.auth.learnerRequireSasl=true quorum.auth.serverRequireSasl=true quorum.auth.learner.saslLoginContext=QuorumLearner quorum.auth.server.saslLoginContext=QuorumServer #quorum.auth.kerberos.servicePrincipal=servicename/_HOST #建议集群节点数n*2 quorum.cnxn.threads.size=6 2、 创建${ZK_HOME}/conf/java.env 文件 [root@localhost conf]# cat java.env SERVER_JVMFLAGS="-Djava.security.auth.login.config=/usr/local/apache-zookeeper-3.5.5-bin/conf/jaas/zk_server.conf" #java.env文件在zkEnv.sh中会被加载 [root@localhost bin]# grep "java.env" zkEnv.sh if [ -f "$ZOOCFGDIR/java.env" ] . "$ZOOCFGDIR/java.env" 3、 DIGEST-MD5 based authentication配置 zookeeper sasl实现提供2种认证机制,分别为Kerberos和DIGEST-MD5认证机制,本文基于DIGEST-MD5 JAAS configuration file #SASL是一个Internet标准协议,Java JAAS只是这个协议的一个种Java实现 [root@localhost conf]# cat jaas/zk_server.conf QuorumServer { org.apache.zookeeper.server.auth.DigestLoginModule required user_test="test123"; }; QuorumLearner { org.apache.zookeeper.server.auth.DigestLoginModule required username="test" password="test123"; }; 二、zk集群间server节点认证测试 1、启动zk集群(成功配置集群认证的情况下) leader节点的启动日志: [root@localhost bin]# ifconfig eth0 Link encap:Ethernet HWaddr 00:0C:29:E4:57:C6 inet addr:192.168.189.135 Bcast:192.168.189.255 Mask:255.255.255.0 inet6 addr: fe80::20c:29ff:fee4:57c6/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:7359 errors:0 dropped:0 overruns:0 frame:0 TX packets:9000 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:633128 (618.2 KiB) TX bytes:985418 (962.3 KiB) lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope:Host UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:1500 errors:0 dropped:0 overruns:0 frame:0 TX packets:1500 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:106198 (103.7 KiB) TX bytes:106198 (103.7 KiB) [root@localhost bin]# ./zkServer.sh status ZooKeeper JMX enabled by default Using config: /usr/local/apache-zookeeper-3.5.5-bin/bin/../conf/zoo.cfg Client port found: 2181. Client address: localhost. Mode: leader [root@localhost bin]# grep "Sasl" ../logs/zookeeper-root-server-localhost.localdomain.out 2020-02-17 15:10:08,937 [myid:2] - INFO [main:QuorumPeer@2156] - quorum.auth.enableSasl set to true 2020-02-17 15:10:08,937 [myid:2] - INFO [main:QuorumPeer@2141] - quorum.auth.serverRequireSasl set to true 2020-02-17 15:10:08,937 [myid:2] - INFO [main:QuorumPeer@2147] - quorum.auth.learnerRequireSasl set to true 2020-02-17 15:10:10,081 [myid:2] - INFO [QuorumConnectionThread-[myid=2]-1:SaslQuorumAuthLearner@152] - Successfully completed the authentication using SASL. server addr: /192.168.189.133:3888, status: SUCCESS 2020-02-17 15:10:11,396 [myid:2] - INFO [LearnerCnxAcceptor-/192.168.189.135:2888:SaslQuorumServerCallbackHandler@143] - Successfully authenticated learner : authenticationID=test; authorizationID=test. 2020-02-17 15:10:11,397 [myid:2] - INFO [LearnerCnxAcceptor-/192.168.189.135:2888:SaslQuorumAuthServer@115] - Successfully completed the authentication using SASL . learner addr: /192.168.189.133:40802 2020-02-17 15:10:12,579 [myid:2] - INFO [QuorumConnectionThread-[myid=2]-2:SaslQuorumServerCallbackHandler@143] - Successfully authenticated learner: authenticationID=test; authorizationID=test. 2020-02-17 15:10:12,579 [myid:2] - INFO [QuorumConnectionThread-[myid=2]-2:SaslQuorumAuthServer@115] - Successfully completed the authentication using SASL. learner addr: /192.168.189.130:44406 2020-02-17 15:10:12,625 [myid:2] - INFO [LearnerCnxAcceptor-/192.168.189.135:2888:SaslQuorumServerCallbackHandler@143] - Successfully authenticated learner: authenticationID=test; authorizationID=test. 2020-02-17 15:10:12,626 [myid:2] - INFO [LearnerCnxAcceptor-/192.168.189.135:2888:SaslQuorumAuthServer@115] - Successfully completed the authentication using SASL. learner addr: /192.168.189.130:45244 2、启动zk集群(模拟192.168.189.130节点配置错误的情况下) --修改zk_server.conf将QuorumLearner下的password更改为错误的test12 leader启动日志: 2020-02-17 15:20:29,243 [myid:2] - INFO [/192.168.189.135:3888:QuorumCnxManager$Listener@888] - Received connection request /192.168.189.130:44462 2020-02-17 15:20:29,267 [myid:2] - ERROR [QuorumConnectionThread-[myid=2]-7:SaslQuorumAuthServer@132] - Failed to authenticate using SASL javax.security.sasl.SaslException: DIGEST-MD5: digest response format violation. Mismatched response. at com.sun.security.sasl.digest.DigestMD5Server.validateClientResponse(DigestMD5Server.java:627) at com.sun.security.sasl.digest.DigestMD5Server.evaluateResponse(DigestMD5Server.java:244) at org.apache.zookeeper.server.quorum.auth.SaslQuorumAuthServer.authenticate(SaslQuorumAuthServer.java:99) at org.apache.zookeeper.server.quorum.QuorumCnxManager.handleConnection(QuorumCnxManager.java:546) at org.apache.zookeeper.server.quorum.QuorumCnxManager.receiveConnection(QuorumCnxManager.java:470) at org.apache.zookeeper.server.quorum.QuorumCnxManager$QuorumConnectionReceiverThread.run(QuorumCnxManager.java:506) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) learner问题节点启动日志: 2020-02-17 15:21:20,479 [myid:3] - INFO [QuorumPeer[myid=3](plain=/0:0:0:0:0:0:0:0:2181)(secure=disabled):FastLeaderElection@919] - Notification time out: 60000 2020-02-17 15:21:20,484 [myid:3] - INFO [QuorumConnectionThread-[myid=3]-2:SecurityUtils@68] - QuorumLearner will use DIGEST-MD5 as SASL mechanism. 2020-02-17 15:21:20,502 [myid:3] - INFO [QuorumConnectionThread-[myid=3]-4:SecurityUtils@68] - QuorumLearner will use DIGEST-MD5 as SASL mechanism. 2020-02-17 15:21:20,509 [myid:3] - ERROR [QuorumConnectionThread-[myid=3]-2:QuorumCnxManager@336] - Exception while connecting, id: [2, /192.168.189.135:3888], addr: {}, closing learner connection javax.security.sasl.SaslException: Authentication failed against server addr: /192.168.189.135:3888 at org.apache.zookeeper.server.quorum.auth.SaslQuorumAuthLearner.authenticate(SaslQuorumAuthLearner.java:126) at org.apache.zookeeper.server.quorum.QuorumCnxManager.startConnection(QuorumCnxManager.java:425) at org.apache.zookeeper.server.quorum.QuorumCnxManager.initiateConnection(QuorumCnxManager.java:334) at org.apache.zookeeper.server.quorum.QuorumCnxManager$QuorumConnectionReqThread.run(QuorumCnxManager.java:385) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) 2020-02-17 15:21:20,527 [myid:3] - ERROR [QuorumConnectionThread-[myid=3]-4:QuorumCnxManager@336] - Exception while connecting, id: [1, /192.168.189.133:3888], addr: {}, closing learner connection javax.security.sasl.SaslException: Authentication failed against server addr: /192.168.189.133:3888 at org.apache.zookeeper.server.quorum.auth.SaslQuorumAuthLearner.authenticate(SaslQuorumAuthLearner.java:126) at org.apache.zookeeper.server.quorum.QuorumCnxManager.startConnection(QuorumCnxManager.java:425) at org.apache.zookeeper.server.quorum.QuorumCnxManager.initiateConnection(QuorumCnxManager.java:334) at org.apache.zookeeper.server.quorum.QuorumCnxManager$QuorumConnectionReqThread.run(QuorumCnxManager.java:385) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) 至此zk集群server间认证配置成功,将jaas配置文件密码修改正确后,问题节点就会正常加入集群。 参考链接: https://cwiki.apache.org/confluence/display/ZOOKEEPER/Server-Server+mutual+authentication 「深度学习福利」大神带你进阶工程师,立即查看>>> 近日,阿里云弹性计算发布全新一代云服务ECS共享标准型S6,性能相对上一代实例提升15%以上,价格相对上一代最高降低42%,是目前国内云计算厂商更能够提供的最具性价比的云服务器产品。一些中小型网站、轻量数据库以及轻量企业应用的客户在阿里云有更多的产品选择,也是个人开发者和小企业的首选。 什么是共享型云主机? 共享性云主机指的是没有做主机和vCPU绑定的主机类型,该类型云主机使用的vCPU是在物理服务器中根据一定算法自动选择的。共享型云主机的优势是性价比高,同时可能会受其他用户的资源争抢的影响。 共享标准型主机S6的产品优势 基于阿里自研神龙芯片和全新的轻量化Hypervisor 基于阿里云智能神龙芯片和全新的轻量化VMM,将大量传统虚拟化功能卸载到专用硬件上,大大降低了虚拟化的性能开销,同时用户几乎可以获得所有的宿主机CPU和内存资源,提高整机和大规格实例的各项能力。 硬件升级,使用最新第二代英特尔至强可扩展处理器 使用最新一代Intel Cascade Lake CPU, 睿频提升至3.2GHz, 各场景E2E性能大幅提升,并在深度学习的多种场景有数倍的提升。同时搭配DDR4内存,底层环境开启NUMA(不可见),降低内存时延。 更高的产品能性能 共享标准型主机S6的处理器和内存配比1:1(仅限1核1GB),1:2,1:4。最高规格为8核32GB。网络带宽最高可突发至6 Gbit/s。 实例规格存储共享整机资源,支持ESSD云盘、SSD云盘和高效云盘。 性能相对上一代实例提升15%以上。 CPU性能测试 应用测试 网络PPS值测试 内存性能测试 阿里云弹性计算一直致力于推出更丰富的产品,满足更多客户的不同需求。共享型实例S6的推出,让阿里云弹性计算云服务器产品的性价比又上一个新台阶,在行业内无其他产品可出其右,是个人开发者及中小型企业上云首选。 查看更多:https://www.aliyun.com/daily-act/ecs/s6_sales 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 近日,阿里云弹性计算发布全新一代云服务ECS共享标准型S6,性能相对上一代实例提升15%以上,价格相对上一代最高降低42%,是目前国内云计算厂商更能够提供的最具性价比的云服务器产品。一些中小型网站、轻量数据库以及轻量企业应用的客户在阿里云有更多的产品选择,也是个人开发者和小企业的首选。 什么是共享型云主机? 共享性云主机指的是没有做主机和vCPU绑定的主机类型,该类型云主机使用的vCPU是在物理服务器中根据一定算法自动选择的。共享型云主机的优势是性价比高,同时可能会受其他用户的资源争抢的影响。 共享标准型主机S6的产品优势 基于阿里自研神龙芯片和全新的轻量化Hypervisor 基于阿里云智能神龙芯片和全新的轻量化VMM,将大量传统虚拟化功能卸载到专用硬件上,大大降低了虚拟化的性能开销,同时用户几乎可以获得所有的宿主机CPU和内存资源,提高整机和大规格实例的各项能力。 硬件升级,使用最新第二代英特尔至强可扩展处理器 使用最新一代Intel Cascade Lake CPU, 睿频提升至3.2GHz, 各场景E2E性能大幅提升,并在深度学习的多种场景有数倍的提升。同时搭配DDR4内存,底层环境开启NUMA(不可见),降低内存时延。 更高的产品能性能 共享标准型主机S6的处理器和内存配比1:1(仅限1核1GB),1:2,1:4。最高规格为8核32GB。网络带宽最高可突发至6 Gbit/s。 实例规格存储共享整机资源,支持ESSD云盘、SSD云盘和高效云盘。 性能相对上一代实例提升15%以上。 CPU性能测试 应用测试 网络PPS值测试 内存性能测试 阿里云弹性计算一直致力于推出更丰富的产品,满足更多客户的不同需求。共享型实例S6的推出,让阿里云弹性计算云服务器产品的性价比又上一个新台阶,在行业内无其他产品可出其右,是个人开发者及中小型企业上云首选。 查看更多:https://www.aliyun.com/daily-act/ecs/s6_sales 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 新冠肺炎疫情牵动人心,危机面前,阿里巴巴打响全面战“疫”:集团设立10亿元医疗物资供给专项基金、菜鸟开通医疗防护用品全球绿色通道、阿里健康推出居民“线上义诊“、阿里云为全国28个省市搭建“数字防疫系统”......全集团共同为抵抗疫情贡献力量。 当下无论是政府还是企业,对数字化服务的需求都已到达高潮。 作为阿里云生态落地的最后一公里,面对疫情,阿里云云市场积极响应,联合数家生态伙伴“千万补贴,共抗疫情”,助力政企打赢这场没有硝烟的“云上战役”。 阿里云云市场是中国最大的云计算/企业服务平台之一,目前已经覆盖了企业应用、基础软件、上云服务、解决方案、IoT等多种场景。 助力政府,全场景疫情防控 肺炎疫情防控战目前正处于关键时期,阿里云云市场面向政府和公益组织,免费开放了多项云端服务,第一时间提供了疫情信息管理、收集、上报、展示等覆盖全场景的疫情防控综合解决方案,保障防疫工作高效开展,打赢这场没有硝烟的“云上战役”。 疫情管理/企业复工系统,无门槛搭建-宜搭 疫情期间免费开放。 宜搭是阿里巴巴自主研发的,无门槛应用搭建平台。面对疫情,宜搭快速响应,搭建了“疫情上报系统”和“企业复工申报系统”,10万+基层组织正基于平台进行疫情信息管控。宜搭通过可视化拖拽的方式,让不会代码的人员也能迅速搭建应用,对抗疫情。 防疫物资信息高效管理与分发-云枢 疫情期间免费开放。 云枢是一款面向开发者的云端低代码应用服务平台。云枢疫情响应机制快速搭建服务平台,2天内部署上线云枢物资管理系统,实现了医院及相关部门紧缺物资实时上报等在线管理。目前,已上线某市610+家医疗机构,节省50+人力,为防疫争分夺秒。 疫情讯息、预防指南及时公示-党建云疫情监控云屏 疫情期间, 面向各地政府组织和公益组织开放免费服务。 云屏打造了数据可视化的宣传模式,全国疫情实时更新、本地疫情自定义播报、疫情资讯和政策宣传、症状科普、预防指南......全部一屏搞定。全国已有800多个县市开通使用该服务。 一站式解决综合防疫需求-防疫小程序 疫情期间, 面向各地政府组织和公益组织开放免费服务。 疫情防控多端小程序围绕辟谣、官方发布、预防技巧、疫情上报,智能问诊、口罩预约、出入登记等刚需场景,帮助政府更好的做好疫情防控助,让政府工作人员有更多的时间精力投入到防控工作中去。 在线签署企业复工、监控承诺书-E签宝 企事业单位可通过E签宝在线高效签署企业复工、健康承诺书,疫情期间, 免费提供100份(政府全免) 。在提升签署效率的同时,实现无接触签署,杜绝病毒传播风险,还有多种电子合同在线签约,业务不掉线。 助力教育,智慧校园停课不停学 湖北省产品免费一年 其他地区免费至疫情结束。 通过智慧校园,全国学校可进行线上教务、教学管理、统计、分析,减少学校人员聚集造成疫情扩散的风险,停课不停学,守护师生健康。疫情发生后,太原市教育局快速行动,启动空中课堂直播方案,解决了50w+中小学生在家学习问题。 助力企业,线上办公有“神器” 受疫情影响,很多线下门店和企业无法正常复工,线下没有客源,又不具备线上获客能力,生存压力陡增。在全面互联网化的趋势下,如何利用数字化手段对销售模式升级,如何协同员工开展远程办公,特殊时期怎么保障财务做账和报税......无论是什么类型的企业,都必须思考这些问题。 基于此,阿里云云市场从前端的销售获客,中端的协同办公到后端的财务管理,为企业提供了全链路的线上商业模式,助力企业线上持续增长,化“危”为“机”,部分远程办公产品免费开放。 线下生意搬到线上-智慧门店小程序 疫情期间免费开放。 针对闭店和寻求线上生意的企业商家,夺冠互动免费提供智慧门店小程序,用蜂鸟的运力提供同城配送到家的功能,帮助传统门店顺利转型。 会打字就会做网站-云·速成美站 云梦网络自助型网站模板+阿里云共享服务器,预置1000套精美设计模板,覆盖125个行业,模板随意切换,即开即用,会打字就会做网站。 离店经营,营销活动工具包-伯俊智能营销 基于伯俊社交电商管理工具,实现社群营销,支持直营、经销等各种业务模式,并能集成常用的营销工具实现多种营销活动。帮助企业找到业绩突破口。 解决远程办公难题-蓝凌智能OA 免费提供1个月(100个账号)的使用权限。 蓝凌在智慧办公与知识管理领域的经验沉淀,融合钉钉的数字化能力,提供在家考勤、在线审批、在线会议、Ding通知、工作总结、员工关怀等功能。 传递信息安全稳定高效-阿里云邮箱 疫情期间低至五折。 阿里云开发并运营的企业邮箱,即买即用,安全无忧,打造随时随地收发使用的全生态开放电子邮箱系统。 让项目管理更容易-Teambition 免费开放至 2020年5月1日。 Teambition帮助企业更好的在线管好工作事:项目管理、任务协同、日程同步、文档协作、知识共享、数据统计......是超700万用户的协同办公首选。 小微企业财务管理神器-畅捷通 疫情期间部分功能免费。 畅捷通财务软件可以实现多端协同,随时随地移动记账,数据还能实时完成自动备份。财务在家办公轻松做账,老板在线实时查询企业经营数据。 在疫情难关面前,阿里云云市场利用大平台的响召能力,不断提供阿里云底层的云计算能力,召集和孵化了一个又一个的硬核产品和解决方案,合力为政府和企业疫情期间的各种数字化需求助力,相信全国人民万众一心,一定可以共克时艰。 查看更多:https://yq.aliyun.com/articles/744484?utm_content=g_1000104170 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 一场突如其来的新冠病毒肺炎疫情,让在无数在备考阶段的高三学子陷入苦恼。为了阻断疫情蔓延,减少人员聚集,教育部下发“关于2020年春季延期开学的通知”。距离高考仅剩120多天,虽停课,不能停学。 1天开通直播 助力黄冈高三学子安心冲刺 对于高三的学子来说,居家“隔离”时期更是复习冲刺的关键时期。为了实现正常授课,湖北黄冈中学高三班采用线上直播的方式进行授课,确保复习计划有序开展。 黄冈中学高三班直播中 而备受高三班师生青睐的线上直播课正是由阿里云视频云与合作伙伴保利威共同提供,仅有1天时间,保利威就完成了线上直播应急方案的搭建,帮助湖北黄冈中学高三班实现了正常授课,而阿里云视频云作为技术底座,提供资源弹性伸缩、海量数据的处理和数据智能化能力以及直播、点播等垂直视频场景的云产品能力部署,全力保障高并发量下系统的平稳运行。 经过多方共同的努力,目前,学生们在家中可以使用电脑、平板、电视等各种设备终端,扫描二维码或打开链接即可进入课堂。还可以使用答题、签到、语音连麦等功能和老师实现高效互动。学校也表示有计划把线上直播方案推广到其他年级。 迅速集结 联手打造“空中课堂”解决方案 “空中课堂”不仅能保证师生健康安全,也是现阶段满足高三学子迫切学习需求的最佳途径。自从大年二十九开始,保利威与阿里云接到项目任务,双双迅速进入“战疫”状态。 保利威深入“前线”快速响应,集中调度资源,提供场景化、一站式的在线直播解决方案,开放全终端直播教学工具,并有技术专家7*24小时在线进行保障,力求最快速度完成与现有教育系统的对接,帮助学校搭建多端远程授课应用。 阿里云作为大后方,为保利威提供视频直播、窄带高清转码、CDN、ECS、OSS、云安全等产品,输出阿里云强大的基础设施与先进的云技术,确保在整个线上直播系统在增长数倍的情况下,依旧能保持高清的画质和流畅的交互体验,为疫情期间在线教育平台、视频会议等业务高并发情况下,提供强有力的技术后盾。 截至目前,阿里云与保利威联手打造的“空中课堂”,已经协助了包括黄冈中学、衡水中学、新东方优能中学等在内的上百所公立学校接入了应急直播,帮助学校快速的实现线下到线上教学的平移,在保证学生安全健康同时,也让学生轻松得跟上复习进度。 停课不停学 阿里云视频云一直在行动 面对疫情,阿里云技术人义不容辞。春节假期期间,各地教育部门先后官宣延期开学并着手搭建“空中课堂”,阿里云视频云团队的小伙伴们也一直在行动。 我们第一时间集结精兵强将,面向在线教育业务的爆发式增长,提前进行了系统优化和运维保障,同时积极输出基于场景的视频PaaS方案,面对IT基础设施较差和存在技术短板的机构,我们也提供特殊点对点技术支持和7*24小时在线响应。不光是黄冈,杭州、宁波、武汉、河南等数十个地市空中课堂的顺利开课,背后都离不开团队的保障。 为了帮助更多中小学生“停课不停学”,我们推出在线教育专项扶持计划,组建“停课不停学”联盟,释放底层资源与技术,与数十家视频技术平台合力推出在线教育解决方案,力保全国中小学校更顺畅、稳定地开展在线教学与管理。 面对疫情,全民阻击。学校虽然远离前线,却也积极备战,通过互联网与云计算技术确保广大学生的学习进度。在这背后,阿里云与合作伙伴们也将持续提供技术保障,携手学校、师生共同打好“停课不停学”这场硬仗。 查看更多:https://yq.aliyun.com/articles/744701?utm_content=g_1000104169 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 在BI产品的所有图表组件中,地图图表组件,由于其展示效果直观且生动,一直是报表中必不可少的部分。地图图表的插入,也可以立马让报表变得高大上,分分钟吸引老板的注意! 在这个风和日丽的好日子里,Quick BI上线了筹备已久的新版地图组件(撒花)。一起跟着小编来看看Quick BI里面有哪些地图组件以及怎么用好这些好看的图表吧~ 1. 新版色彩地图 作图要求: 1个地理区域(维度),色彩饱和度(度量)至少1个,至多5个(色彩地图用色彩的深浅来展示数据的大小和分布范围) 注意️️: 此处的维度类型必须为地理类型,如果非地理类型需要在数据集中将维度字段修改为地理类型哦。 Quick BI的地图还支持不同的样式模版,同样的数据设计,只要切换了不同的样式模板,整个色彩地图立马大变样! 轻轻松松达到匹配不同仪表板主题的效果。 更厉害的是,Quick BI新版的色彩地图,还支持世界地图哦~只要在统计设置中将显示范围切换为世界地图,就可以轻松地从中国地图切换到世界地图,目前支持中国、新加坡、德国、墨西哥、日本、美国这6个国家。 2. 新版气泡地图 作图要求: 1个地理区域(维度),气泡大小(度量)至少1个,至多5个(气泡地图主要由地理区域和气泡大小构成,可以直观地显示国家或地区的相关数据指标大小和分布范围) 注意️️: 气泡地图的维度类型必须为地理类型,如果非地理类型需要在数据集中将维度字段修改为地理类型哦~ 气泡地图也可以在样式配置中选择不同的模板类型来满足不同的展示需求,下面的这些样式只需要动动小手改改图表的样式就能实现啦~
除此之外,我们还可以改变气泡的配色(包括配色范围、透明度等等)、气泡的大小,甚至是气泡的形状。老板如果不喜欢普通的圆形气泡,没关系!Quick BI还提供三角形、六边形等的气泡样式,总有一款老板心仪的。 3. LBS气泡地图 作图要求: 1个地理区域(维度),LBDS气泡大小(度量)至少1个,至多5个 LBS气泡地图与气泡地图类似,都是以一个地图轮廓为背景,用附着在地图上面的气泡来反映数据的大小。Quick BI的LBS气泡地图还可以切换地图的底图样式,例如高德地图、谷歌地图和GeoQ来显示国家或地区的相关数据指标大小和分布范围。 4. LBS热力地图 作图要求: 1个地理区域(维度),LBS热力深度(度量)至少1个,至多5个(LBS热力地图用热力的深浅来展示数据的大小和分布范围) 5. LBS飞线地图 作图要求: 2个地理区域(维度),需要分别选择飞线的起始点,1个LBS飞线(度量)。LBS飞线地图以一个地图轮廓为背景,用动态的飞线反映两地或者多地之间的数据关系。 这么多丰富的图表配置,大家都了解了吗? 如此酷炫的图表,赶快来Quick BI体验一下吧~ 阿里巴巴数据中台团队,致力于输出阿里云数据智能的最佳实践,助力每个企业建设自己的数据中台,进而共同实现新时代下的智能商业! 阿里巴巴数据中台解决方案,核心产品: • Dataphin,以阿里巴巴大数据核心方法论OneData为内核驱动,提供一站式数据构建与管理能力; • Quick BI,集阿里巴巴数据分析经验沉淀,提供一站式数据分析与展现能力; • Quick Audience,集阿里巴巴消费者洞察及营销经验,提供一站式人群圈选、洞察及营销投放能力,连接阿里巴巴商业,实现用户增长。 查看更多:https://yq.aliyun.com/articles/744447?utm_content=g_1000104167 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 本文主要记录如何使用 hutool 工具类解析 json 对象 1、在 pom 文件内添加 hutool 依赖: cn.hutool hutool-all 5.0.6 2、字符串 转 JSONObject String jsonStr = "{\"DispositionNotificationListObject\":{\"DispositionNotificationObject\":[{\"PersonObject\":{\"GenderCode\":0,\"EthicCode\":0,\"DeviceID\":\"0\",\"SourceID\":\"022019121117052900016\",\"SubImageList\":{\"SubImageInfoObject\":[{\"Type\":\"11\",\"StoragePath\":\"xxx\",\"DeviceID\":\"0\",\"ImageID\":\"022019121117052900016\",\"EventSort\":0,\"ShotTime\":\"19700119134716\",\"Height\":-1,\"FileFormat\":\"jpg\",\"Width\":-1},{\"Type\":\"11\",\"StoragePath\":\"xxx\",\"DeviceID\":\"0\",\"ImageID\":\"022019121117052900016\",\"EventSort\":0,\"ShotTime\":\"19700119134600\",\"Height\":-1,\"FileFormat\":\"jpg\",\"Width\":-1},{\"Type\":\"14\",\"StoragePath\":\"xxx\",\"DeviceID\":\"0\",\"ImageID\":\"022019121117052900016\",\"EventSort\":0,\"ShotTime\":\"19700119134600\",\"Height\":-1,\"FileFormat\":\"jpg\",\"Width\":-1}]},\"LeftTopY\":141,\"LeftTopX\":104,\"Name\":\"图片姓名测试\",\"InfoKind\":1,\"PersonID\":\"0220191211170529000180100019\",\"RightBtmY\":806,\"RightBtmX\":932,\"IDNumber\":\"\"},\"NotificationID\":\"202002031736\",\"DispositionID\":\"71\",\"TriggerTime\":\"2020-02-03 15:34:15\",\"Title\":\"第三方1400告警信息接收测试\"}]}}"; JSONObject json = new JSONObject(jsonStr); 3、解析 JSONObject: 1)获取对象 JSONObject dispositionNotificationListObject = json.getJSONObject("DispositionNotificationListObject"); 2)获取单值 String alertPersonName = personObject.getStr("Name"); Integer eventSort = subImageInfoObject.getInt("EventSort"); 4、字符串 转 JSONArray String jsonArrayStr = "[{\"Type\":\"11\",\"StoragePath\":\"http://192.168.166.203:11180/storage/v1/image/global?cluster_id=ShenSi&uri_base64=bm9ybWFsOi8vcmVwb3NpdG9yeS1idWlsZGVyLzIwMjAwMTIwL1ZRQnV1LVlsWFpGd29lN1dJSnlXUmc9PUAx\",\"DeviceID\":\"0\",\"ImageID\":\"022019121117052900016\",\"EventSort\":0,\"ShotTime\":\"19700119134716\",\"Height\":-1,\"FileFormat\":\"jpg\",\"Width\":-1},{\"Type\":\"11\",\"StoragePath\":\"http://192.168.166.203:11180/storage/v1/image/global?cluster_id=ShenSi&uri_base64=bm9ybWFsOi8vcmVwb3NpdG9yeS1idWlsZGVyLzIwMjAwMTIwL0gzc1dYN3lXRmh1Zmd0Sjd6Tlo1cnc9PUAx\",\"DeviceID\":\"0\",\"ImageID\":\"022019121117052900016\",\"EventSort\":0,\"ShotTime\":\"19700119134600\",\"Height\":-1,\"FileFormat\":\"jpg\",\"Width\":-1},{\"Type\":\"14\",\"StoragePath\":\"http://192.168.166.203:11180/storage/v1/image/global?cluster_id=ShenSi&uri_base64=bm9ybWFsOi8vcmVwb3NpdG9yeS1idWlsZGVyLzIwMjAwMTIwL0JXS3RIQm1aVXpUT3prNzJOYW50S1E9PUAx\",\"DeviceID\":\"0\",\"ImageID\":\"022019121117052900016\",\"EventSort\":0,\"ShotTime\":\"19700119134600\",\"Height\":-1,\"FileFormat\":\"jpg\",\"Width\":-1}]"; JSONArray jsonArray = new JSONArray(jsonArrayStr); 5、解析 JSONArray: 1)获取数组对象 JSONArray subImageInfoObjectList = subImageList.getJSONArray("SubImageInfoObject"); 2)获取单值 for(int j=0; j< jsonArray.size(); j++){ JSONObject subImageInfoObject = jsonArray.getJSONObject(j); String Type = subImageInfoObject.getStr("Type"); Integer width = subImageInfoObject.getInt("Width"); System.out.println(Type); } 注:以上代码可能有些值对应不上,本文只是提供一个切实有效的思路,测试代码需要根据示例自我调整。 点关注,不迷路 好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是 人才 。 白嫖不好,创作不易。 各位的支持和认可,就是我创作的最大动力,我们下篇文章见! 如果本篇博客有任何错误,请批评指教,不胜感激 ! 点关注,不迷路 好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是 人才 。 白嫖不好,创作不易。 各位的支持和认可,就是我创作的最大动力,我们下篇文章见! 如果本篇博客有任何错误,请批评指教,不胜感激 ! 本文来自: 微信公众号【大数据实战演练】。阅读更多精彩好文,欢迎关注微信公众号【大数据实战演练】。 「深度学习福利」大神带你进阶工程师,立即查看>>> 一、前言 很多小伙伴也都知道,最近一直在做 Ambari 集成自定义服务的教学笔记和视频。 之前在准备 Ambari 环境的时候,考虑到有朋友会在 Ambari 安装部署时遇到问题,所以贴心的我呢,就在搭建 Ambari 环境的时候,把这个视频录制好了,总共时长共 87 分钟,将近1个半小时,附带移除 SmartSense 服务及 FAQ 。 也提前介绍一下搭建好的 Ambari 相关版本信息: Ambari:2.7.3.0 hdp:3.1.0.0 二、课程特色 三、课程大纲 四、观看须知 五、再唠叨一下 以上为安装 Ambari 以及部署 Hadoop 集群的视频介绍及宣传,完整视频已上传到 哔哩哔哩-bilibili 平台,视频链接: Ambari 2.7.3.0 安装部署 hadoop 3.1.0.0 集群完整版,附带移除 SmartSense 服务及 FAQ 。 很多小伙伴也都知道,最近一直在做 Ambari 集成自定义服务的教学笔记和视频。目前笔记已经完成 90 %,笔记一整理完,就会参考笔记录制 Ambari 集成自定义服务的教学视频。 Ambari 集成自定义服务脑图 ,如下图所示: **目前已整理好的笔记,**如下图所示: 一直在行动!希望大家也多多关注,多多宣传,谢谢! 点关注,不迷路 好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是 人才 。 白嫖不好,创作不易。 各位的支持和认可,就是我创作的最大动力,我们下篇文章见! 如果本篇博客有任何错误,请批评指教,不胜感激 ! 点关注,不迷路 好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是 人才 。 白嫖不好,创作不易。 各位的支持和认可,就是我创作的最大动力,我们下篇文章见! 如果本篇博客有任何错误,请批评指教,不胜感激 ! 本文来自: 微信公众号【大数据实战演练】。阅读更多精彩好文,欢迎关注微信公众号【大数据实战演练】。 「深度学习福利」大神带你进阶工程师,立即查看>>> 前段时间写了一个数据导入工具,数据导入的路径为Hadoop-->公司自研的时序数据库。运行一段时间后发现出现Kerberos认证过期的问题。日志如下: 一开始很奇怪,因为额外开了一个线程,每隔8小时更新Kerberos认证,照理说不该出现这个问题。看直接报错,显示是找不到Kerberos票据,更是一头雾水。 直到看到红框框住的这句话,知道问题所在了。代码中有个逻辑为忽略大小为0的文件,是这样写的: val hadoopConf:Configuration = new Configuration() hadoopConf.addResource( new Path("core-site.xml")) hadoopConf.addResource( new Path("hdfs-site.xml")) hadoopConf.addResource( new Path("hive-site.xml")) hadoopConf.addResource( new Path("yarn-site.xml")) val path = new Path("hdfs://hacluster/user/oper/private/123") val hdfs = FileSystem.get(hadoopConf) val status = hdfs.listStatus(path) status.foreach( file=>{ val filePath = file.getPath().toString; val fileSize = hdfs.getContentSummary(file.getPath()).getLength() if (fileSize > 0 ) { //逻辑代码 } else { logger.info("path:{}, fileSize = {}.",filePath,fileSize) } } ) 这个情况应该是hdfs对象的Kerberos认证过期了,Kerberos认证更新的代码对这个对象不起作用。认清问题后,就知道如何解决这个问题了,不需要边迭代,边判断大小,可以提前过滤掉这些文件。代码如下: val hadoopConf:Configuration = new Configuration() hadoopConf.addResource( new Path("core-site.xml")) hadoopConf.addResource( new Path("hdfs-site.xml")) hadoopConf.addResource( new Path("hive-site.xml")) hadoopConf.addResource( new Path("yarn-site.xml")) val path = new Path("hdfs://hacluster/user/oper/private/123") val hdfs = FileSystem.get(hadoopConf) val status = hdfs.listStatus(path).filter(file=>(hdfs.getContentSummary(file.getPath()).getLength())>0).map(p=>p.getPath.toString) hdfs.close() FileSystem.closeAll() status.foreach( file=>{ //逻辑代码 } ) 重新打包程序,上传并运行。36小时后,程序仍无异常,问题解决。 「深度学习福利」大神带你进阶工程师,立即查看>>> nbdev是一个使用Jupyter Notebook进行多模块软件开发的辅助工具,可以将多个Notebook组装为一个大型的软件系统,自动生成代码和文档,并能够在Notebook中进行交互运行和探索性测试。 nbdev 源码: https://github.com/fastai/nbdev nbdev 文档: https://nbdev.fast.ai/ nbdev主要完成下面的功能: 建立python开发框架和git repo版本。 在notebook中的cell中使用#export标记为输出代码区块。 使用库函数notebook2script()将#export标记的代码转化为python库*.py。 所生成的代码放在project name的目录下,可以在notebook中引用或者直接运行。 例程项目,用于nCoV分析(初级阶段): https://github.com/openthings/anti2020ncov 1、安装 nbdev已经加入 PyPI,使用下面的命令安装: pip install nbdev 对于 editable install ,使用: git clone https://github.com/fastai/nbdev pip install -e nbdev 2、快速开始 2.1 创建项目 开始自己的 project, 点击: nbdev template (会要求登录到 GitHub )。填写要求的信息,然后点击 Create repository from template , 新的 GitHub repo将会在你的账户下创建出来。 注意: 项目的名称将成为 Python package的名称,由 nbdev 自动创建。建议选择短的全部小写而且没有连接线的名称(下划线可以)。 现在,打开终端,将该 repo 拉去到本地(git clone ...),可以使用Jupyter里面创建的终端窗口。 可选方式,可以使用nbdev的客户端命令 nbdev_new 来创建 nbdev project,并且初始化git repository到本地,不会创建 github repo。 2.2 设置参数 下一步,编辑 settings.ini 文件。 该文件包含所有的打包你的库的信息因此不需要改变 setup.py 文件,这是由模版工程创建的 。基本结构 (that can be personalized provided you change the relevant information in settings.ini ) 是 repo的根目录将包含你的 notebooks, 目录 docs 包含文档是自动创建的,可用于 jekyll -powered 站点。因为 GitHub Pages supports Jekyll , 可以将该文档放到 GitHub ,不需要做任何额外的设置。 你的 settings.ini 放在 nbdev,用于任何需要的配置信息。如果编辑了该文件,运行命令 nbdev_build_lib (安装 nbdev时自动安装)。 你将发现有了一个新的新的目录,按照 lib_name 在 settings.ini 中的设置。 现在运行 jupyter notebook , 点击 00_core.ipynb。 这是你创建的第一个模块。可以在notebook中创建多个Jupyter cells。 在需要包含在python模块的cell最前面加上 #export ,必须是第一行。 注意: 对于新创建的 .ipynb,需要加入 #default_exp target_module_name , 用于定义创建的模块名称(lib_name/target_module_name.py)。 在notebook的最后一个cell,可以运行下面的函数调用,用于创建*.py文件。 from nbdev.export import * notebook2script() Converted 00_export.ipynb. Converted 01_sync.ipynb. Converted 02_showdoc.ipynb. Converted 03_export2html.ipynb. Converted 04_test.ipynb. Converted 05_merge.ipynb. Converted 06_cli.ipynb. Converted 07_clean.ipynb. Converted 99_search.ipynb. Converted index.ipynb. Converted tutorial.ipynb. 或者在命令行下运行命令: nbdev_build_lib 将会更新模块,包含所有的exported cells。 为了在 GitHub repo启用文档功能,到github.com的项目中点击 'Settings' ,到 'GitHub Pages', 在 'Source' 选择'master branch /docs folder'。GitHub 将现实文档站点的连接。 最后,编辑 edit index.ipynb 。该页面将转化为项目文档的 README 文件,也是你的文档的索引。你可以使用输出库中的模块,意味着可以显示正式的工作代码和实际输出 。一旦准备好了,到命令行运行 nbdev_build_docs,将输出 将notebook输出到 HTML 版本并保存到 docs 目录。同时将创建链接 ,创建所有notebook的菜单,以及内容的目录表等。 注意,如何使用子目录保存 .ipynb 文件 如果你设置过参数 nbs_path 在 project root之外,将不能 import 创建的模块,需要一些额外的操作: 手动安装模块, relative import 将与top-level package关联,通过运行 pip install -e . 在项目根目录,安装模块到 环境中。 创建软连接到notebook 目录到library folder,通过运行 ln -s lib_path lib_name (修改 lib_path 和 lib_name为你的项目情况 ). 其它功能 nbdev 还有很多其他功能,查看相应的文档。下面简单提一下: 添加project 到 pypi 如果你希望其他人可以通过pypi来进行安装,需要将其提交到 pypi 。好消息是,我们已经创建了一个 pypi compliant installer for your project!创建一个文件为 ~/.pypirc 填入登录信息。内容看起来如下所示: [pypi] username = your_pypi_username password = your_pypi_password 需要安装 twine ,运行: pip install twine 上载 project 到pypi,只需要运行 make pypi , 在 project root 目录下。完成后,指向 pypi 的连接会被打印出来。 NB : 确保每一次增加 version number,位于 settings.py ,用于向pypi推送一个新的版本。 避免和处理 git conflicts Jupyter Notebooks 会引起 git conflicts, 但使用 nbdev 就要轻松很多。 第一步,运行 nbdev_install_git_hooks将设置 git hooks,每一次commit时会移除元数据,会极大减少 conflict。 但是,如果遇上 conflict, 简单滴运行 nbdev_fix_merge filename.ipynb . 将用你的版本替换任何 conflicts in cell outputs , 如果 conflicts in input cells,两个 cells 都将被包含在 merged file, 使用标准的冲突标示 (e.g. ===== )。然后你可以打开 notebook到 Jupyter,然后选择保留哪一个。 使用nbdev作为 CI的一部分 可以使用 GitHub actions 来提升 nbdev的功能,容易滴构建 CI : 检查 notebooks are readable (with nbdev_read_nbs ) 检查the notebooks have been cleaned of needless metadata to avoid merge conflicts (with nbdev_clean_nbs ) 检查there is no diff between the notebooks and the exported library (with nbdev_diff_nbs ) 运行notebooks 中的测试(with nbdev_test_nbs ) 该template 包含基本的 CI,使用了上面四点,编辑文件 .github/workflows/main.yml 修改相关的部分。 Math 公式支持 nbdev 支持公式 (采用 KaTeX library )。使能 use_math: true 到 your _config.yml (缺省已开启)。应用方法如下: 使用 $$ , e.g.: $$\sum_{i=1}^{k+1}i$$ 显示为: $$\sum_{i=1}^{k+1}i$$ 使用 $ , e.g.: This version is diplayed inline: $\sum_{i=1}^{k+1}i$ . You can include text before and after. 显示为: This version is diplayed inline: $\sum_{i=1}^{k+1}i$ . You can include text before and after. 更多参考: 关于nbdev的介绍, https://tech.sina.com.cn/roll/2020-02-03/doc-iimxxste8470190.shtml 运用Jupyter Notebook进行系统管理 将Jupyter/JupyterHub/JupyterLab运行为系统服务 快速设置JupyterHub for K8s 抗击新冠病毒(2)-基于Jupyter+nbdev的数据分析 「深度学习福利」大神带你进阶工程师,立即查看>>> 2020年的1月份过去了,漫长的像过了一个年。新型冠状病毒疫情牵动着无数人的心。 面对疫情导致的封停隔离,众多企业在复工后,都选择了远程协同办公。远程办公不是个新鲜事儿,国外已经玩了多年,但中国人还不太习惯。在毫无准备的情况下,突然开展对很多企业和团队管理者还是蛮有杀伤力的。 越是危难时刻,越要凝聚人心。这时候需要leader思考怎么带队伍,怎么发挥领导力,怎么让我们的团队更加精进,怎么把远程协同开发做到更高效,同时也给团队成员把握住这次千载难逢的学习机会。 msup作为拥有11年经验的软件研发团队的培训咨询机构,在这场疫情防控阻击战中,我们也希望能够践行我们的企业责任。 为此,我们 决定把msup价值数百万元的线下技术大会视频实录放送出来 ,供大家免费学习,这些内容汇聚了全球300+国内外顶尖大厂技术实践(视频已陆续上线中), 包含产品、架构、AI、大数据、数字化转型、研发管理、测试运维等几十个领域。 希望这些实录课程能够给疫情期间影响下的研发团队提供关于人才成长的一些帮助,让大家在疫情期间不间断学习。下面是关于限免视频的一些介绍,欢迎感兴趣的同学接着向下看。 同时,也欢迎大家将此活动分享给同事、朋友,一起学习,共同成长。疫情当前,msup衷心祝愿大家平安健康,若大家有任何研发问题或苦恼,随时召唤我们,我们会一直在您身边。 具体参与方式:微信公号搜索msup或msupclub,及会弹出视频地址,已关注的小伙伴可移步到msup菜单栏进入案例学院观看学习。 「深度学习福利」大神带你进阶工程师,立即查看>>> 甩开技术包袱,做出业务特色,越发成为垂直电商的生存之道 今天,人们通过天猫、淘宝、苏宁进行网络购物,不仅方便,而且快乐,通过盒马、饿了么享受更加快捷的本地生活服务,与此同时,家电、食品等垂直领域的电商平台,也越来越有特色。 A公司是中国较早的食品领域相关产品和服务供应商,旗下的A1网致力于打造安全的大型食品购物网站。 对食品进行优选,在保质期上严格把关,对食品库存温湿度以及人员健康严格把关的同时,网站不断丰富食品种类,满足不同人群的需求,和其他垂直领域电商平台一样,A1网也决定把更多的精力用在不断提升服务质量和特色,网站本身往云上迁移,借助云计算的优势,不断提升系统响应速度,让购物更加快捷,这已经成为该领域正在发生着的一个显著变化。 把IT基础设施重资产(包括基于IDC建设的服务器、存储设备、网络与安全设备等)转移到云上,实现IT基础设施的虚拟化,根据业务量弹性灵活的使用资源,做到按量使用、按需付费,还有另一个附加好处,增强了经济贸易风险抵御能力。当经济形势触底的时候,对于最坏的情况进行兜底,对于经济形势随着拐点上行的时候,能够轻装前进,支持业务的快速扩张。 本文将以案例说明上云为支撑业务和降低成本带来的转变,以及客户的顾虑如何消除,最后通过什么样的步骤上云。 系统越复杂越需要上云,才能更敏捷的支撑业务 业务的敏捷性要求,对使用传统的IDC部署开源软件运维的方式提出了挑战。 上云之前,网站已经具有了相当的规模,在北京世纪互联主机房有数十个机柜,在同城的备机房也有几个机柜,主要是存放备份、数据库和代码。 图1 云下信息图 使用了万网的DNS服务和第三方的CDN服务,采用7层负载均衡,主机为物理机+kvm+vmware+docker的组合,存储设备在主机房,有着数十TB的数据,使用了开源的漏洞扫描软件,应用服务器采用了Nginx+Apache+Resin+Tomcat的组合,数据库使用了SQLServer+MySql+Oracle等多种产品。 另外还使用了Redis缓存、ZooKeeper、MongoDB、ELK、Docker、K8S以及监控软件和SVN,应用服务器+web前端+缓存+数据库一共有上千台服务器,操作系统则包括了centos和windows的多个版本。 与复杂的系统对应的是一支数十人的运维团队,随着时间的推移,不同厂商的产品越来越错综复杂,而业务的增长进一步为系统的平稳运行带来了更大的挑战,如何减少服务之间的依赖关系,如何打通数据,提高对新业务新需求的响应速度,如何提高运维人机比,这一系列的问题成为需要长期应对的挑战。 而云上通过微服务架构,通过使用企业分布式服务EDAS、消息队列、容器等服务和产品,极大的保障了系统的服务化和异步化,通过云上数据库、中间件、移动和物联网套件以及小程序,可以快速搭建应用试错,快速响应业务需求。 上云后可以大幅度缩减成本 图2 云上架构图 由于电商特色的各种打折促销活动,带来了系统流量的波峰波谷,导致平时服务器的利用率远远不到50%,对于这部分业务,云上可以使用包年包月的服务,性价比足够高,而对于其他波峰的业务在云上则可以使用按量付费的服务,在SLB设置规则当流量达到一定量的时候弹起一台新机器,还可以考虑纵向的弹性扩展,比如提高配置规格,使得总体成本缩减到原来的一半。 上云后,客户需要通过等保三级测评,可以通过我们和合作伙伴的服务帮助客户准备材料,指导客户顺利通过测评,和自建机房相比,成本更低。 存储都通过OSS上云,云下不存放数据,后续根据需要可以增加异地OSS备份,既节省了原来的存储成本,云上三副本的AZ内高可用、同城双AZ容灾和异地三AZ备份,很好的确保了数据可靠性与可用性。 以客户第一的理念消除顾虑 在行业大背景下,以实实在在的业务和技术的痛点为驱动,上云带来的好处为指引,使得今天更多的客户不是谈论什么是云计算,为什么要上云,而是在谈论如何快速高效的上云。与此同时,作为一家专门提供云服务的技术输出公司,我们正以专业的态度服务客户,从而打消一切顾虑,下面列举了一些客户因为顾虑而提出的问题,以及我们的解答。 双11阿里会不会先保障天猫、淘宝,由此可能伤害客户? 面对这样尖锐的问题,我们其实想到了前面,早在每年十月份或更早就开始询问客户的增量,提前为客户做好扩容准备,以确保客户的业务为首先要保障的目标。 如果要下云的时候会不会像某云计算厂商一样制造阻碍,关掉Redis的相应功能,使得客户不能使用DTS进行不停服迁移? 我们的回答是,绝对不会的,可以通过实际使用来验证我们的承诺。 对于A1网而言,由于客服线路等原因,有些数据库迁移不上云,能否云上应用访问云下数据库? 回答是可以的,通过ECC建立云上和IDC之间的高速通道就可以很好的满足业务需要。 我们为客户提供的安全合规服务收费吗? 我们提供资料都是免费的,如果需要购买安全产品比如WAF则要按照WAF产品付费。 上云后的成本怎么样? 通常比原成本节省30%,可以使用TCO计算器进行成本分析和现有IDC成本对比。 部分运维人员担心上云后会不会没有工作? 事实上,上云后运维同学可以解放出来从事云服务管理等更加高级的工作,大数据开发工程师可以做数据分析师和技术支持工作,产生的价值更大,因为业务价值更大。同时,阿里云还提供了从Apsara Clouder、ACA助理工程师、ACP专业工程师到ACE高级工程师的云计算专业技能和资格认证服务,基于云计算、大数据、云安全、人工智能、中间件几大领域,证明技术人员可以基于阿里云产品在对应的技术方向上解决企业的基本业务问题。 上云七步走 图3 上云方案图 网络迁移,是首先要解决的问题 。迁移前,了解机房和办公区之间、机房之间的专线情况,包括带宽、类型等;迁移时,建立ECC高速通道。 应用迁移,一般先于数据库进行 。迁移前,确定双机房之间是否实现了主从,比如通过Bind服务域名解析配置主从服务器等;迁移时,在云上重新部署,或者使用SMC做应用迁移;技术细节可以参考“ 服务器迁移最佳实践 ”。 缓存迁移,包括Redis、MemCache迁移 。迁移前,确定存放的是什么数据,比如session,用户登录信息等,确定存放的哪些数据是可以丢弃的,应用重新从数据库更新,确定需要迁移的数据量;迁移时,使用DTS工具进行迁移,且支持增量;应用和Redis之间如果有代理程序,需要先进行失效处理。 数据库迁移,主要是MySQL迁移 。迁移前,确定每个业务线对应的实例使用情况,实例共享物理机情况,确定端口是否都使用3306,不同的实例是否都使用不同的域名,MySQL以哪些版本为主,确认云上是否覆盖,统计实例总数,一台物理机多少个实例,每个实例数据量级别,通过POC来熟悉DTS用法;迁移时,使用DTS工具迁移;如果出现异常则可采用回退策略,DTS回流到线下,客户可能做业务改动,比如将IP改成域名,需要提前检查。技术细节可以参考 最佳实践 半结构化数据迁移,包括MongoDB迁移 。迁移前,需要了解有几个集群,总共多少TB数据,需要做云服务POC验证;迁移时,使用云自建方式,加分片方式迁移,也可以使用云服务方式,通过使用DTS工具从自建迁移过来。 存储迁移,主要指非结构化数据迁移。迁移前,了解现有存储的数据是哪些,含副本数据量和实际数据量,负载情况,读写比例等,需要POC来熟悉产品用法,包括测试使用OSSImport来将IDC文件迁移到OSS;迁移时,历史数据使用闪电立方迁移到OSS,热数据使用OSSImport迁移到OSS云上存储;迁移到OSS需要改应用,需要注意检查避免出错,另外,如果小文件较多,则迁移耗时可能会较长,需要提前做好安排。 如上所述,通过分析需求和关键的技术难点,制定了上云方案,该方案按照标准化的流程对应考虑的技术问题做了规范约定,包括准备工作阶段、迁移方法和风险预案,通过方案的实施,能够有效的帮助客户提高上云的效率,降低风险。 查看更多:https://yq.aliyun.com/articles/740083?utm_content=g_1000102637上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> “通过宜搭开放的接口能力,我们将审批流程与财务系统、支付系统打通。现在日常报销、对外付款,资金上交下拨等业务只需手机点一点审批,后续记账、付款数据自动生成,大幅提升了效率,节约了人工。” ——居然之家家居新零售财务总监:朱蝉飞 案例背景 居然之家是中国家居建材流通行业的龙头企业,2018年市场销售额超过750亿元。企业经过了20年的持续发展,目前全国设有20余家分公司、300余家分店,设立各类部门组织1800多个,管理直营及加盟店员工超14000人。为支撑业务的快速发展,居然之家从未停止建设IT系统的步伐,一方面斥巨资打造内部大ERP,建立前、中、后台架构的“智汇家”系统,另一方面引入阿里的理念与技术建设了新零售平台,成为了家居新零售的排头兵。 然而,与业务系统的不断创新与完善相比,企业内部协同系统的建设水平却相对滞后。庞大的组织带来了企业内部沟通不便、信息上传下达链路过长等问题。为使企业管理更加扁平化,组织协同更加高效,2017年底居然之家使用了钉钉,从组织在线入手开启了企业数字化转型之路。 钉钉的产品能力很好地解决了组织在线、沟通在线的问题,但面对企业流程协同、应用搭建及业务集成方面需要更专业的产品来支持,以满足集团型企业下的复杂表单及流程场景需求。审批不仅仅是文件的上传下达,而是作为整个业务流程中的重要一环,要与各个业务系统集成并实现在线化。大型企业亟需一套能满足复杂场景并且能低成本使用的表单流程搭建工具。 基于以上痛点,居然之家信息化部门开始寻找合适的OA产品,但后来发现传统OA虽然能满足场景需要,但产品一般较为复杂,另外,集团正在打造以钉钉为统一入口的移动办公门户,这就要求所有的应用和流程搭建能快速地发布在钉钉上。传统OA需要额外投入资源打通产品与钉钉的组织人员权限,并不是最优的选择。 直到“宜搭”的出现为上述问题提供了全新解决方案:高度可配置的表单和流程;天然可“长”在钉钉上的组织人员数据;PC和移动双端适配的开发平台;强大的接口能力及可配置化的服务调用等,大幅减少了系统集成难度和开发量,满足了大型企业面对复杂业务场景的应用开发需求。 方案架构 案例效果 行政办公流程应用 行政办公流程应用中包含办公和物资的相关审批流程。根据使用用户视角分为集团 / 家居连锁集团流程类、外埠分公司及分店流程类、设计家网络公司流程类。行政办公应用内主要搭建了刻印公章申请流程、单位间盖章流程、公文发布申请流程、借用公章申请流程、借用营业执照申请流程。 办公门户建设 通过宜搭搭建的流程展示页,作为 PC 端用户进入相关流程的入口。根据使用视角对所有展示页进行分组展现,共分集团展示页、北京分店展示页、外埠分店展示页、外埠分公司展示页、设计家网络公司展示页和矩阵维护展示页,共计 6 个展示页面。 宜搭集成 通过宜搭处理人事、综合、行政、财务的审批流程,为了保持宜搭与 HR 数据的一致性,将基于 active MQ、RFC 接口通过以下方式将 HR 和财务系统与宜搭进行同步。 https 数据服务,提供宜搭单据中 HR 数据的及时查询及返回,宜搭单据中配置数据池,通过 js 将数据池中数据写入单据中显示。 https 回调服务,提供宜搭审批节点提交时,通过自定义回调服务进行数据处理。 主数据同步,将 HR 中人员信息、岗位信息、组织架构信息及时同步至宜搭中搭建的主数据单据中。 定时任务同步,通过配置同步时间及频率,自动检查并同步 HR 系统中至宜搭数据单据中,同步宜搭中审批实例至集成方。 手动同步,通过运维管理应用可手动触发完成同步。将同步包括不限于以下数据:HR 中组织架构信息、员工信息、岗位信息、人事范围、员工组等信息。 同步逻辑:HR 为主数据,系统同步均为单向同步。员工信息、组织架构、岗位等信息以 HR 系统中编码为唯一标识,存在时通过标识更新数据,否则新增该条数据。 财务集成:员工报销、对外付款、资金下拨等财务流程审批后同步至财务系统,财务自动记账并调用银企直连支付接口实现付款。 查看更多: https:// yqh.aliyun.com/ 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问: 云栖号 - 上云就看云栖号 「深度学习福利」大神带你进阶工程师,立即查看>>> 这篇文章来自一个读者在面试过程中的一个问题,Hadoop在shuffle过程中使用了一个数据结构-环形缓冲区。 环形队列是在实际编程极为有用的数据结构,它是一个首尾相连的FIFO的数据结构,采用数组的线性空间,数据组织简单。能很快知道队列是否满为空。能以很快速度的来存取数据。 因为有简单高效的原因,甚至在硬件都实现了环形队列。 环形队列广泛用于网络数据收发,和不同程序间数据交换(比如内核与应用程序大量交换数据,从硬件接收大量数据)均使用了环形队列。 环形缓冲区数据结构 Map过程中环形缓冲区是指数据被map处理之后会先放入内存,内存中的这片区域就是环形缓冲区。 环形缓冲区是在MapTask.MapOutputBuffer中定义的,相关的属性如下: // k/v accounting // 存放meta数据的IntBuffer,都是int entry,占4byte private IntBuffer kvmeta; // metadata overlay on backing store int kvstart; // marks origin of spill metadata int kvend; // marks end of spill metadata int kvindex; // marks end of fully serialized records // 分割meta和key value内容的标识 // meta数据和key value内容都存放在同一个环形缓冲区,所以需要分隔开 int equator; // marks origin of meta/serialization int bufstart; // marks beginning of spill int bufend; // marks beginning of collectable int bufmark; // marks end of record int bufindex; // marks end of collected int bufvoid; // marks the point where we should stop // reading at the end of the buffer // 存放key value的byte数组,单位是byte,注意与kvmeta区分 byte[] kvbuffer; // main output buffer private final byte[] b0 = new byte[0]; // key value在kvbuffer中的地址存放在偏移kvindex的距离 private static final int VALSTART = 0; // val offset in acct private static final int KEYSTART = 1; // key offset in acct // partition信息存在kvmeta中偏移kvindex的距离 private static final int PARTITION = 2; // partition offset in acct private static final int VALLEN = 3; // length of value // 一对key value的meta数据在kvmeta中占用的个数 private static final int NMETA = 4; // num meta ints // 一对key value的meta数据在kvmeta中占用的byte数 private static final int METASIZE = NMETA * 4; // size in bytes 环形缓冲区其实是一个数组,数组中存放着key、value的序列化数据和key、value的元数据信息,key/value的元数据存储的格式是int类型,每个key/value对应一个元数据,元数据由4个int组成,第一个int存放value的起始位置,第二个存放key的起始位置,第三个存放partition,最后一个存放value的长度。 key/value序列化的数据和元数据在环形缓冲区中的存储是由equator分隔的,key/value按照索引递增的方向存储,meta则按照索引递减的方向存储,将其数组抽象为一个环形结构之后,以equator为界,key/value顺时针存储,meta逆时针存储。 初始化 环形缓冲区的结构在MapOutputBuffer.init中创建。 public void init(MapOutputCollector.Context context ) throws IOException, ClassNotFoundException { ... //MAP_SORT_SPILL_PERCENT = mapreduce.map.sort.spill.percent // map 端buffer所占的百分比 //sanity checks final float spillper = job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8); //IO_SORT_MB = "mapreduce.task.io.sort.mb" // map 端buffer大小 // mapreduce.task.io.sort.mb * mapreduce.map.sort.spill.percent 最好是16的整数倍 final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100); // 所有的spill index 在内存所占的大小的阈值 indexCacheMemoryLimit = job.getInt(JobContext.INDEX_CACHE_MEMORY_LIMIT, INDEX_CACHE_MEMORY_LIMIT_DEFAULT); ... // 排序的实现类,可以自己实现。这里用的是改写的快排 sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class", QuickSort.class, IndexedSorter.class), job); // buffers and accounting // 上面IO_SORT_MB的单位是MB,左移20位将单位转化为byte int maxMemUsage = sortmb << 20; // METASIZE是元数据的长度,元数据有4个int单元,分别为 // VALSTART、KEYSTART、PARTITION、VALLEN,而int为4个byte, // 所以METASIZE长度为16。下面是计算buffer中最多有多少byte来存元数据 maxMemUsage -= maxMemUsage % METASIZE; // 元数据数组 以byte为单位 kvbuffer = new byte[maxMemUsage]; bufvoid = kvbuffer.length; // 将kvbuffer转化为int型的kvmeta 以int为单位,也就是4byte kvmeta = ByteBuffer.wrap(kvbuffer) .order(ByteOrder.nativeOrder()) .asIntBuffer(); // 设置buf和kvmeta的分界线 setEquator(0); bufstart = bufend = bufindex = equator; kvstart = kvend = kvindex; // kvmeta中存放元数据实体的最大个数 maxRec = kvmeta.capacity() / NMETA; // buffer spill时的阈值(不单单是sortmb spillper) // 更加精确的是kvbuffer.length spiller softLimit = (int)(kvbuffer.length * spillper); // 此变量较为重要,作为spill的动态衡量标准 bufferRemaining = softLimit; ... // k/v serialization comparator = job.getOutputKeyComparator(); keyClass = (Class)job.getMapOutputKeyClass(); valClass = (Class)job.getMapOutputValueClass(); serializationFactory = new SerializationFactory(job); keySerializer = serializationFactory.getSerializer(keyClass); // 将bb作为key序列化写入的output keySerializer.open(bb); valSerializer = serializationFactory.getSerializer(valClass); // 将bb作为value序列化写入的output valSerializer.open(bb); ... // combiner ... spillInProgress = false; // 最后一次merge时,在有combiner的情况下,超过此阈值才执行combiner minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3); spillThread.setDaemon(true); spillThread.setName("SpillThread"); spillLock.lock(); try { spillThread.start(); while (!spillThreadRunning) { spillDone.await(); } } catch (InterruptedException e) { throw new IOException("Spill thread failed to initialize", e); } finally { spillLock.unlock(); } if (sortSpillException != null) { throw new IOException("Spill thread failed to initialize", sortSpillException); } } init是对环形缓冲区进行初始化构造,由mapreduce.task.io.sort.mb决定map中环形缓冲区的大小sortmb,默认是100M。 此缓冲区也用于存放meta,一个meta占用METASIZE(16byte),则其中用于存放数据的大小是maxMemUsage -= sortmb << 20 % METASIZE(由此可知最好设置sortmb转换为byte之后是16的整数倍),然后用maxMemUsage初始化kvbuffer字节数组和kvmeta整形数组,最后设置数组的一些标识信息。利用setEquator(0)设置kvbuffer和kvmeta的分界线,初始化的时候以0为分界线,kvindex为aligned - METASIZE + kvbuffer.length,其位置在环形数组中相当于按照逆时针方向减去METASIZE,由kvindex设置kvstart = kvend = kvindex,由equator设置bufstart = bufend = bufindex = equator,还得设置bufvoid = kvbuffer.length,bufvoid用于标识用于存放数据的最大位置。 为了提高效率,当buffer占用达到阈值之后,会进行spill,这个阈值是由bufferRemaining进行检查的,bufferRemaining由softLimit = (int)(kvbuffer.length * spillper); bufferRemaining = softLimit;进行初始化赋值,这里需要注意的是softLimit并不是sortmb spillper,而是kvbuffer.length * spillper,当sortmb << 20是16的整数倍时,才可以认为softLimit是sortmb spillper。 下面是setEquator的代码 // setEquator(0)的代码如下 private void setEquator(int pos) { equator = pos; // set index prior to first entry, aligned at meta boundary // 第一个 entry的末尾位置,即元数据和kv数据的分界线 单位是byte final int aligned = pos - (pos % METASIZE); // Cast one of the operands to long to avoid integer overflow // 元数据中存放数据的起始位置 kvindex = (int) (((long)aligned - METASIZE + kvbuffer.length) % kvbuffer.length) / 4; LOG.info("(EQUATOR) " + pos + " kvi " + kvindex + "(" + (kvindex * 4) + ")"); } buffer初始化之后的抽象数据结构如下图所示: 环形缓冲区数据结构图 环形缓冲区数据结构图 写入buffer Map通过NewOutputCollector.write方法调用collector.collect向buffer中写入数据,数据写入之前已在NewOutputCollector.write中对要写入的数据进行逐条分区,下面看下collect // MapOutputBuffer.collect public synchronized void collect(K key, V value, final int partition ) throws IOException { ... // 新数据collect时,先将剩余的空间减去元数据的长度,之后进行判断 bufferRemaining -= METASIZE; if (bufferRemaining <= 0) { // start spill if the thread is not running and the soft limit has been // reached spillLock.lock(); try { do { // 首次spill时,spillInProgress是false if (!spillInProgress) { // 得到kvindex的byte位置 final int kvbidx = 4 * kvindex; // 得到kvend的byte位置 final int kvbend = 4 * kvend; // serialized, unspilled bytes always lie between kvindex and // bufindex, crossing the equator. Note that any void space // created by a reset must be included in "used" bytes final int bUsed = distanceTo(kvbidx, bufindex); final boolean bufsoftlimit = bUsed >= softLimit; if ((kvbend + METASIZE) % kvbuffer.length != equator - (equator % METASIZE)) { // spill finished, reclaim space resetSpill(); bufferRemaining = Math.min( distanceTo(bufindex, kvbidx) - 2 * METASIZE, softLimit - bUsed) - METASIZE; continue; } else if (bufsoftlimit && kvindex != kvend) { // spill records, if any collected; check latter, as it may // be possible for metadata alignment to hit spill pcnt startSpill(); final int avgRec = (int) (mapOutputByteCounter.getCounter() / mapOutputRecordCounter.getCounter()); // leave at least half the split buffer for serialization data // ensure that kvindex >= bufindex final int distkvi = distanceTo(bufindex, kvbidx); final int newPos = (bufindex + Math.max(2 * METASIZE - 1, Math.min(distkvi / 2, distkvi / (METASIZE + avgRec) * METASIZE))) % kvbuffer.length; setEquator(newPos); bufmark = bufindex = newPos; final int serBound = 4 * kvend; // bytes remaining before the lock must be held and limits // checked is the minimum of three arcs: the metadata space, the // serialization space, and the soft limit bufferRemaining = Math.min( // metadata max distanceTo(bufend, newPos), Math.min( // serialization max distanceTo(newPos, serBound), // soft limit softLimit)) - 2 * METASIZE; } } } while (false); } finally { spillLock.unlock(); } } // 将key value 及元数据信息写入缓冲区 try { // serialize key bytes into buffer int keystart = bufindex; // 将key序列化写入kvbuffer中,并移动bufindex keySerializer.serialize(key); // key所占空间被bufvoid分隔,则移动key, // 将其值放在连续的空间中便于sort时key的对比 if (bufindex < keystart) { // wrapped the key; must make contiguous bb.shiftBufferedKey(); keystart = 0; } // serialize value bytes into buffer final int valstart = bufindex; valSerializer.serialize(value); // It's possible for records to have zero length, i.e. the serializer // will perform no writes. To ensure that the boundary conditions are // checked and that the kvindex invariant is maintained, perform a // zero-length write into the buffer. The logic monitoring this could be // moved into collect, but this is cleaner and inexpensive. For now, it // is acceptable. bb.write(b0, 0, 0); // the record must be marked after the preceding write, as the metadata // for this record are not yet written int valend = bb.markRecord(); mapOutputRecordCounter.increment(1); mapOutputByteCounter.increment( distanceTo(keystart, valend, bufvoid)); // write accounting info kvmeta.put(kvindex + PARTITION, partition); kvmeta.put(kvindex + KEYSTART, keystart); kvmeta.put(kvindex + VALSTART, valstart); kvmeta.put(kvindex + VALLEN, distanceTo(valstart, valend)); // advance kvindex kvindex = (kvindex - NMETA + kvmeta.capacity()) % kvmeta.capacity(); } catch (MapBufferTooSmallException e) { LOG.info("Record too large for in-memory buffer: " + e.getMessage()); spillSingleRecord(key, value, partition); mapOutputRecordCounter.increment(1); return; } } 每次写入数据时,执行bufferRemaining -= METASIZE之后,检查bufferRemaining, 如果大于0,直接将key/value序列化对和对应的meta写入buffer中,key/value是序列化之后写入的,key/value经过一些列的方法调用Serializer.serialize(key/value) -> WritableSerializer.serialize(key/value) -> BytesWritable.write(dataOut) -> DataOutputStream.write(bytes, 0, size) -> MapOutputBuffer.Buffer.write(b, off, len),最后由MapOutputBuffer.Buffer.write(b, off, len)将数据写入kvbuffer中,write方法如下: public void write(byte b[], int off, int len) throws IOException { // must always verify the invariant that at least METASIZE bytes are // available beyond kvindex, even when len == 0 bufferRemaining -= len; if (bufferRemaining <= 0) { // writing these bytes could exhaust available buffer space or fill // the buffer to soft limit. check if spill or blocking are necessary boolean blockwrite = false; spillLock.lock(); try { do { checkSpillException(); final int kvbidx = 4 * kvindex; final int kvbend = 4 * kvend; // ser distance to key index final int distkvi = distanceTo(bufindex, kvbidx); // ser distance to spill end index final int distkve = distanceTo(bufindex, kvbend); // if kvindex is closer than kvend, then a spill is neither in // progress nor complete and reset since the lock was held. The // write should block only if there is insufficient space to // complete the current write, write the metadata for this record, // and write the metadata for the next record. If kvend is closer, // then the write should block if there is too little space for // either the metadata or the current write. Note that collect // ensures its metadata requirement with a zero-length write blockwrite = distkvi <= distkve ? distkvi <= len + 2 * METASIZE : distkve <= len || distanceTo(bufend, kvbidx) < 2 * METASIZE; if (!spillInProgress) { if (blockwrite) { if ((kvbend + METASIZE) % kvbuffer.length != equator - (equator % METASIZE)) { // spill finished, reclaim space // need to use meta exclusively; zero-len rec & 100% spill // pcnt would fail resetSpill(); // resetSpill doesn't move bufindex, kvindex bufferRemaining = Math.min( distkvi - 2 * METASIZE, softLimit - distanceTo(kvbidx, bufindex)) - len; continue; } // we have records we can spill; only spill if blocked if (kvindex != kvend) { startSpill(); // Blocked on this write, waiting for the spill just // initiated to finish. Instead of repositioning the marker // and copying the partial record, we set the record start // to be the new equator setEquator(bufmark); } else { // We have no buffered records, and this record is too large // to write into kvbuffer. We must spill it directly from // collect final int size = distanceTo(bufstart, bufindex) + len; setEquator(0); bufstart = bufend = bufindex = equator; kvstart = kvend = kvindex; bufvoid = kvbuffer.length; throw new MapBufferTooSmallException(size + " bytes"); } } } if (blockwrite) { // wait for spill try { while (spillInProgress) { reporter.progress(); spillDone.await(); } } catch (InterruptedException e) { throw new IOException( "Buffer interrupted while waiting for the writer", e); } } } while (blockwrite); } finally { spillLock.unlock(); } } // here, we know that we have sufficient space to write if (bufindex + len > bufvoid) { final int gaplen = bufvoid - bufindex; System.arraycopy(b, off, kvbuffer, bufindex, gaplen); len -= gaplen; off += gaplen; bufindex = 0; } System.arraycopy(b, off, kvbuffer, bufindex, len); bufindex += len; } write方法将key/value写入kvbuffer中,如果bufindex+len超过了bufvoid,则将写入的内容分开存储,将一部分写入bufindex和bufvoid之间,然后重置bufindex,将剩余的部分写入,这里不区分key和value,写入key之后会在collect中判断bufindex < keystart,当bufindex小时,则key被分开存储,执行bb.shiftBufferedKey(),value则直接写入,不用判断是否被分开存储,key不能分开存储是因为要对key进行排序。 这里需要注意的是要写入的数据太长,并且kvinde==kvend,则抛出MapBufferTooSmallException异常,在collect中捕获,将此数据直接spill到磁盘spillSingleRecord,也就是当单条记录过长时,不写buffer,直接写入磁盘。 下面看下bb.shiftBufferedKey()代码 // BlockingBuffer.shiftBufferedKey protected void shiftBufferedKey() throws IOException { // spillLock unnecessary; both kvend and kvindex are current int headbytelen = bufvoid - bufmark; bufvoid = bufmark; final int kvbidx = 4 * kvindex; final int kvbend = 4 * kvend; final int avail = Math.min(distanceTo(0, kvbidx), distanceTo(0, kvbend)); if (bufindex + headbytelen < avail) { System.arraycopy(kvbuffer, 0, kvbuffer, headbytelen, bufindex); System.arraycopy(kvbuffer, bufvoid, kvbuffer, 0, headbytelen); bufindex += headbytelen; bufferRemaining -= kvbuffer.length - bufvoid; } else { byte[] keytmp = new byte[bufindex]; System.arraycopy(kvbuffer, 0, keytmp, 0, bufindex); bufindex = 0; out.write(kvbuffer, bufmark, headbytelen); out.write(keytmp); } } shiftBufferedKey时,判断首部是否有足够的空间存放key,有没有足够的空间,则先将首部的部分key写入keytmp中,然后分两次写入,再次调用Buffer.write,如果有足够的空间,分两次copy,先将首部的部分key复制到headbytelen的位置,然后将末尾的部分key复制到首部,移动bufindex,重置bufferRemaining的值。 key/value写入之后,继续写入元数据信息并重置kvindex的值。 spill 一次写入buffer结束,当写入数据比较多,bufferRemaining小于等于0时,准备进行spill,首次spill,spillInProgress为false,此时查看bUsed = distanceTo(kvbidx, bufindex),此时bUsed >= softLimit 并且 (kvbend + METASIZE) % kvbuffer.length == equator - (equator % METASIZE),则进行spill,调用startSpill private void startSpill() { // 元数据的边界赋值 kvend = (kvindex + NMETA) % kvmeta.capacity(); // key/value的边界赋值 bufend = bufmark; // 设置spill运行标识 spillInProgress = true; ... // 利用重入锁,对spill线程进行唤醒 spillReady.signal(); } startSpill唤醒spill线程之后,进程spill操作,但此时map向buffer的写入操作并没有阻塞,需要重新边界equator和bufferRemaining的值,先来看下equator和bufferRemaining值的设定: // 根据已经写入的kv得出每个record的平均长度 final int avgRec = (int) (mapOutputByteCounter.getCounter() / mapOutputRecordCounter.getCounter()); // leave at least half the split buffer for serialization data // ensure that kvindex >= bufindex // 得到空余空间的大小 final int distkvi = distanceTo(bufindex, kvbidx); // 得出新equator的位置 final int newPos = (bufindex + Math.max(2 * METASIZE - 1, Math.min(distkvi / 2, distkvi / (METASIZE + avgRec) * METASIZE))) % kvbuffer.length; setEquator(newPos); bufmark = bufindex = newPos; final int serBound = 4 * kvend; // bytes remaining before the lock must be held and limits // checked is the minimum of three arcs: the metadata space, the // serialization space, and the soft limit bufferRemaining = Math.min( // metadata max distanceTo(bufend, newPos), Math.min( // serialization max distanceTo(newPos, serBound), // soft limit softLimit)) - 2 * METASIZE; 因为equator是kvbuffer和kvmeta的分界线,为了更多的空间存储kv,则最多拿出distkvi的一半来存储meta,并且利用avgRec估算distkvi能存放多少个record和meta对,根据record和meta对的个数估算meta所占空间的大小,从distkvi/2和meta所占空间的大小中取最小值,又因为distkvi中最少得存放一个meta,所占空间为METASIZE,在选取kvindex时需要求aligned,aligned最多为METASIZE-1,总和上述因素,最终选取equator为(bufindex + Math.max(2 * METASIZE - 1, Math.min(distkvi / 2, distkvi / (METASIZE + avgRec) * METASIZE)))。equator选取之后,设置bufmark = bufindex = newPos和kvindex,但此时并不设置bufstart、bufend和kvstart、kvend,因为这几个值要用来表示spill数据的边界。 spill之后,可用的空间减少了,则控制spill的bufferRemaining也应该重新设置,bufferRemaining取三个值的最小值减去2 METASIZE,三个值分别是meta可用占用的空间distanceTo(bufend, newPos),kv可用空间distanceTo(newPos, serBound)和softLimit。这里为什么要减去2 METASIZE,一个是spill之前kvend到kvindex的距离,另一个是当时的kvindex空间????此时,已有一个record要写入buffer,需要从bufferRemaining中减去当前record的元数据占用的空间,即减去METASIZE,另一个METASIZE是在计算equator时,没有包括kvindex到kvend(spill之前)的这段METASIZE,所以要减去这个METASIZE。 接下来解析下SpillThread线程,查看其run方法: public void run() { spillLock.lock(); spillThreadRunning = true; try { while (true) { spillDone.signal(); // 判断是否在spill,false则挂起SpillThread线程,等待唤醒 while (!spillInProgress) { spillReady.await(); } try { spillLock.unlock(); // 唤醒之后,进行排序和溢写到磁盘 sortAndSpill(); } catch (Throwable t) { sortSpillException = t; } finally { spillLock.lock(); if (bufend < bufstart) { bufvoid = kvbuffer.length; } kvstart = kvend; bufstart = bufend; spillInProgress = false; } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { spillLock.unlock(); spillThreadRunning = false; } } run中主要是sortAndSpill, private void sortAndSpill() throws IOException, ClassNotFoundException, InterruptedException { //approximate the length of the output file to be the length of the //buffer + header lengths for the partitions final long size = distanceTo(bufstart, bufend, bufvoid) + partitions * APPROX_HEADER_LENGTH; FSDataOutputStream out = null; try { // create spill file // 用来存储index文件 final SpillRecord spillRec = new SpillRecord(partitions); // 创建写入磁盘的spill文件 final Path filename = mapOutputFile.getSpillFileForWrite(numSpills, size); // 打开文件流 out = rfs.create(filename); // kvend/4 是截止到当前位置能存放多少个元数据实体 final int mstart = kvend / NMETA; // kvstart 处能存放多少个元数据实体 // 元数据则在mstart和mend之间,(mstart - mend)则是元数据的个数 final int mend = 1 + // kvend is a valid record (kvstart >= kvend ? kvstart : kvmeta.capacity() + kvstart) / NMETA; // 排序 只对元数据进行排序,只调整元数据在kvmeta中的顺序 // 排序规则是MapOutputBuffer.compare, // 先对partition进行排序其次对key值排序 sorter.sort(MapOutputBuffer.this, mstart, mend, reporter); int spindex = mstart; // 创建rec,用于存放该分区在数据文件中的信息 final IndexRecord rec = new IndexRecord(); final InMemValBytes value = new InMemValBytes(); for (int i = 0; i < partitions; ++i) { // 临时文件是IFile格式的 IFile.Writer writer = null; try { long segmentStart = out.getPos(); FSDataOutputStream partitionOut = CryptoUtils.wrapIfNecessary(job, out); writer = new Writer(job, partitionOut, keyClass, valClass, codec, spilledRecordsCounter); // 往磁盘写数据时先判断是否有combiner if (combinerRunner == null) { // spill directly DataInputBuffer key = new DataInputBuffer(); // 写入相同partition的数据 while (spindex < mend && kvmeta.get(offsetFor(spindex % maxRec) + PARTITION) == i) { final int kvoff = offsetFor(spindex % maxRec); int keystart = kvmeta.get(kvoff + KEYSTART); int valstart = kvmeta.get(kvoff + VALSTART); key.reset(kvbuffer, keystart, valstart - keystart); getVBytesForOffset(kvoff, value); writer.append(key, value); ++spindex; } } else { int spstart = spindex; while (spindex < mend && kvmeta.get(offsetFor(spindex % maxRec) + PARTITION) == i) { ++spindex; } // Note: we would like to avoid the combiner if we've fewer // than some threshold of records for a partition if (spstart != spindex) { combineCollector.setWriter(writer); RawKeyValueIterator kvIter = new MRResultIterator(spstart, spindex); combinerRunner.combine(kvIter, combineCollector); } } // close the writer writer.close(); // record offsets // 记录当前partition i的信息写入索文件rec中 rec.startOffset = segmentStart; rec.rawLength = writer.getRawLength() + CryptoUtils.cryptoPadding(job); rec.partLength = writer.getCompressedLength() + CryptoUtils.cryptoPadding(job); // spillRec中存放了spill中partition的信息,便于后续堆排序时,取出partition相关的数据进行排序 spillRec.putIndex(rec, i); writer = null; } finally { if (null != writer) writer.close(); } } // 判断内存中的index文件是否超出阈值,超出则将index文件写入磁盘 // 当超出阈值时只是把当前index和之后的index写入磁盘 if (totalIndexCacheMemory >= indexCacheMemoryLimit) { // create spill index file // 创建index文件 Path indexFilename = mapOutputFile.getSpillIndexFileForWrite(numSpills, partitions * MAP_OUTPUT_INDEX_RECORD_LENGTH); spillRec.writeToFile(indexFilename, job); } else { indexCacheList.add(spillRec); totalIndexCacheMemory += spillRec.size() * MAP_OUTPUT_INDEX_RECORD_LENGTH; } LOG.info("Finished spill " + numSpills); ++numSpills; } finally { if (out != null) out.close(); } } sortAndSpill中,有mstart和mend得到一共有多少条record需要spill到磁盘,调用sorter.sort对meta进行排序,先对partition进行排序,然后按key排序,排序的结果只调整meta的顺序。 排序之后,判断是否有combiner,没有则直接将record写入磁盘,写入时是一个partition一个IndexRecord,如果有combiner,则将该partition的record写入kvIter,然后调用combinerRunner.combine执行combiner。 写入磁盘之后,将spillx.out对应的spillRec放入内存indexCacheList.add(spillRec),如果所占内存totalIndexCacheMemory超过了indexCacheMemoryLimit,则创建index文件,将此次及以后的spillRec写入index文件存入磁盘。 最后spill次数递增。sortAndSpill结束之后,回到run方法中,执行finally中的代码,对kvstart和bufstart赋值,kvstart = kvend,bufstart = bufend,设置spillInProgress的状态为false。 在spill的同时,map往buffer的写操作并没有停止,依然在调用collect,再次回到collect方法中, // MapOutputBuffer.collect public synchronized void collect(K key, V value, final int partition ) throws IOException { ... // 新数据collect时,先将剩余的空间减去元数据的长度,之后进行判断 bufferRemaining -= METASIZE; if (bufferRemaining <= 0) { // start spill if the thread is not running and the soft limit has been // reached spillLock.lock(); try { do { // 首次spill时,spillInProgress是false if (!spillInProgress) { // 得到kvindex的byte位置 final int kvbidx = 4 * kvindex; // 得到kvend的byte位置 final int kvbend = 4 * kvend; // serialized, unspilled bytes always lie between kvindex and // bufindex, crossing the equator. Note that any void space // created by a reset must be included in "used" bytes final int bUsed = distanceTo(kvbidx, bufindex); final boolean bufsoftlimit = bUsed >= softLimit; if ((kvbend + METASIZE) % kvbuffer.length != equator - (equator % METASIZE)) { // spill finished, reclaim space resetSpill(); bufferRemaining = Math.min( distanceTo(bufindex, kvbidx) - 2 * METASIZE, softLimit - bUsed) - METASIZE; continue; } else if (bufsoftlimit && kvindex != kvend) { ... } } } while (false); } finally { spillLock.unlock(); } } ... } 有新的record需要写入buffer时,判断bufferRemaining -= METASIZE,此时的bufferRemaining是在开始spill时被重置过的(此时的bufferRemaining应该比初始的softLimit要小),当bufferRemaining小于等最后一个METASIZE是当前record进入collect之后bufferRemaining减去的那个METASIZE。 于0时,进入if,此时spillInProgress的状态为false,进入if (!spillInProgress),startSpill时对kvend和bufend进行了重置,则此时(kvbend + METASIZE) % kvbuffer.length != equator - (equator % METASIZE),调用resetSpill(),将kvstart、kvend和bufstart、bufend设置为上次startSpill时的位置。此时buffer已将一部分内容写入磁盘,有大量空余的空间,则对bufferRemaining进行重置,此次不spill。 bufferRemaining取值为Math.min(distanceTo(bufindex, kvbidx) - 2 * METASIZE, softLimit - bUsed) - METASIZE private void resetSpill() { final int e = equator; bufstart = bufend = e; final int aligned = e - (e % METASIZE); // set start/end to point to first meta record // Cast one of the operands to long to avoid integer overflow kvstart = kvend = (int) (((long)aligned - METASIZE + kvbuffer.length) % kvbuffer.length) / 4; LOG.info("(RESET) equator " + e + " kv " + kvstart + "(" + (kvstart * 4) + ")" + " kvi " + kvindex + "(" + (kvindex * 4) + ")"); } 当bufferRemaining再次小于等于0时,进行spill,这以后就都是套路了。环形缓冲区分析到此结束。 声明:本号所有文章除特殊注明,都为原创,公众号读者拥有优先阅读权,未经作者本人允许不得转载,否则追究侵权责任。 关注我的公众号,后台回复【JAVAPDF】获取200页面试题! 5万人关注的大数据成神之路,不来了解一下吗? 5万人关注的大数据成神之路,真的不来了解一下吗? 5万人关注的大数据成神之路,确定真的不来了解一下吗? 欢迎您关注 《大数据成神之路》 「深度学习福利」大神带你进阶工程师,立即查看>>> Spark发展至今,应该说已经非常成熟了。是大数据计算领域不得不学习的框架。尤其是Spark在稳定性和社区发展的成熟度方面,吊打其他的大数据处理框架。 Spark至今只经历过1.x、2.x和3.x三个大版本的变化,在核心实现上,我们在Github能看到的最早的实现是0.5版本,这个版本只有1万多行代码,就把Spark的核心功能实现了。 当然我们不可能从这么古老的版本看,假如你接触过Spark,现在准备看源码,那么我建议从2.x版本中选取一个,最好是2.3或者2.4。但是经过如此多的迭代,Spark的代码量已经暴增了几倍。所以你需要抓住重点,本文就是想跟大家说说,我们该怎么看,看哪些东西。 基础概念篇 首先假如你是第一次接触Spark,那么你需要对Spark的设计思想有所了解,知道Spark用了哪些抽象,Spark在提出RDD的时候是基于什么样的考虑。 在这里给大家推荐几篇论文如下: 第一篇: 这个不多说,直接给出一个链接,这篇文章不长,很快能看完。 弹性分布式数据集:一种为内存化集群计算设计的容错抽象 https://fasionchan.com/blog/2017/10/19/yi-wen-tan-xing-fen-bu-shi-shu-ju-ji-yi-zhong-wei-nei-cun-hua-ji-qun-ji-suan-she-ji-de-rong-cuo-mo-xing/ 第二篇: 大型集群上的快速和通用数据处理架构 这篇论文长达170多页,当然我们没有必要看完,我在网上找到一个总结,我们在《弹性分布式数据集:一种为内存化集群计算设计的容错抽象》这篇论文基础上看起来也会轻松不少。 链接如下: https://blog.csdn.net/weixin_44024821/article/details/89948115 环境准备 Spark的源码环境搭建网上有很多资源,主要是环境依赖比如JDK、Scala、Maven等,整个过程大概耗时要1-4个小时,主要是编译源码费时。大家可以在网上任意找一篇文章搭建起来环境就可以。 直到你在编译时出现下图就大功告成了。(我个人强烈不建议使用sbt的方式编译,推荐Maven) Spark核心设计篇 上图是一个最简单的Spark任务的执行图。 在核心设计篇,我们最重要的模块列表如下: Spark的初始化 SparkContext SparkEnv SparkConf RpcEnv SparkStatusTracker SecurityManager SparkUI MetricsSystem TaskScheduler Spark的存储体系 SerializerManager BroadcastManager ShuffleManager MemoryManager NettyBlockTransferService BlockManagerMaster BlockManager CacheManager Spark的内存管理 MemoryManager MemoryPool ExecutionMemoryPool StorageMemoryPool MemoryStore UnifiedMemoryManager Spark的运算体系 LiveListenerBus MapOutputTracker DAGScheduler TaskScheduler ExecutorAllocationManager OutputCommitCoordinator ContextClearner Spark的部署模式 LocalSparkCluster Standalone Mater/Executor/Worker的容错 Spark Streaming StreamingContext Receiver Dstream 窗口操作 Spark SQL Catalog TreeNode 词法解析器Parser RuleExecutor Analyzer与Optimizer Hive SQL相关 其他 假如你对图计算Spark GraphX和机器学习Spark MLlib感兴趣,可以单独看看。 整个跟实时计算相关的包和类大部分都已经包含在上述目录中了。假如你在使用的过程中出现了问题,就需要针对其中的具体部门去看。 源码阅读是我们每一个开发者都需要经历的阶段,阅读源码的好处就不说了。你我都懂。 声明:本号所有文章除特殊注明,都为原创,公众号读者拥有优先阅读权,未经作者本人允许不得转载,否则追究侵权责任。 关注我的公众号,后台回复【JAVAPDF】获取200页面试题! 5万人关注的大数据成神之路,不来了解一下吗? 5万人关注的大数据成神之路,真的不来了解一下吗? 5万人关注的大数据成神之路,确定真的不来了解一下吗? 欢迎您关注 《大数据成神之路》
「深度学习福利」大神带你进阶工程师,立即查看>>> 小文件的定义 一般来说小于等于30M的文件,都叫小文件。 在HDFS中会将一个文件分成若干个block块(128M)存储,如:129M=128M+1M,就会生成2个block块,而在hadoop中的目录、文件、block都会以元数据的方式存储下来(200字节)。 在HDFS中,通常NN维护一个文件的名称,目录结构等大约是250字节。现实中,HDFS的小文件如果不做任何操作增长会很快,现在假设NN节点的内存为4G,差不多42亿字节,现在在HDFS上有一亿个小文件,那么需要250乘一亿大约是250亿字节,这样会将NN撑爆。小文件到达一定数目,就会将NN节点撑爆。就算NN能够存储,对于hive,spark计算时,小文件意味着需要更多的task和资源,同样也会将节点弄挂掉。 为什么会有小文件 批处理时造成过多的小文件 数据源有大量小文件,未做处理就直接拷贝到hadoop集群中 MR运算时reduce输出没有做很好的设置 小文件给Hadoop集群带来的瓶颈问题 在处理数据时,读取数据会有进程开启和销毁的过程,小文件过多会导致大量的网络磁盘IO的消耗。 在MapReduce过程中,会开启大量的MR任务,也就是说会造成大量的shuffle过程。也会造成大量任务处于等待资源的状态。 小文件问题的解决(合并) 源头减少小文件的产生 如果是由数据源产生大量小文件并直接拷贝到Hadoop,可以调研了解数据源是否能生成一些大文件,或者从数据源到HDFS的数据抽取过程中进行数据处理合并小文件。如果每小时只抽取10MB的数据,考虑是否改为每天一次,这样创建1个240MB的文件而不是24个10MB的文件。但是,可能无法控制数据源的改动配合或业务对数据抽取间隔的需求,这样小文件问题无法避免,这时可能需要考虑其他的解决方案。 批量文件合并 当产生小文件是不可避免时,你可以 定期运行一个MapReduce任务,读取某一个文件夹中的所有小文件,并将它们重写为较少数量的大文件。 比如一个文件夹中有1000个文件,你可以在一个MapReduce任务中指定reduce的数量为5,这样1000个输入文件会被合并为5个文件。随后进行一些简单的HDFS文件/文件夹操作(将新文件覆盖回原目录),则可以将NameNode的内存使用减少到200分之1,并且可以提高以后MapReduce或其他计算引擎对同一数据处理的性能。 批量合并文件的方法无法保留原始文件名,如果原始文件名对于你了解数据来源非常重要,则批量合并文件的方法也不适用。但一般来说,我们一般只会设计HDFS的各级目录的文件名,而不会细化到每个文件的名字,所以理论来说这种方法问题也不大。 使用Sequence文件 当需要维护原始文件名时,常见的方法是使用Sequence文件。 在此解决方案中, 文件名作为key保存在sequence文件中,然后文件内容会作为value保存。 如果一个sequence文件包含10000个小文件,则同时会包含10000个key在一个文件中。sequence文件支持块压缩,并且是可被拆分的。这样MapReduce作业在处理这个sequence文件时,只需要为每个128MB的block启动一个map任务,而不是每个小文件启动一个map任务。当你在同时抽取数百个或者数千个小文件,并且需要保留原始文件名时,这是非常不错的方案。 但是,如果你一次仅抽取少量的小文件到HDFS,则sequence文件的方法也不太可行,因为 sequence文件是不可变的,无法追加 。比如3个10MB文件将产生1个30MB的sequence文件,根据本文前面的定义,这仍然是一个小文件。另外一个问题是如果需要检索sequence文件中的文件名列表则需要遍历整个文件。 另外一个问题是Hive并不能较好的处理由该方法合并出来的sequence文件。Hive将value中的所有数据视为单行。这样会导致Hive查看这些数据不方便,因为以前小文件中的一行的所有数据也是Hive中的单行,即相当于只有一个字段。同时,Hive没办法访问这种sequence的key,即文件名。当然你可以自定义Hive serde来实现,不过这个超过了本文需要讨论的范围。 「深度学习福利」大神带你进阶工程师,立即查看>>> 本文来源于云栖社区: https://yq.aliyun.com/articles/743514 作者:xy_xin 共同点 定性上讲,三者均为 Data Lake 的数据存储中间层,其数据管理的功能均是基于一系列的 meta 文件。meta 文件的角色类似于数据库的 catalog/wal,起到 schema 管理、事务管理和数据管理的功能。与数据库不同的是,这些 meta 文件是与数据文件一起存放在存储引擎中的,用户可以直接看到。这种做法直接继承了大数据分析中数据对用户可见的传统,但是无形中也增加了数据被不小心破坏的风险。一旦某个用户不小心删了 meta 目录,表就被破坏了,想要恢复难度非常大。 Meta 文件包含有表的 schema 信息。因此系统可以自己掌握 Schema 的变动,提供 Schema 演化的支持。Meta 文件也有 transaction log 的功能(需要文件系统有原子性和一致性的支持)。所有对表的变更都会生成一份新的 meta 文件,于是系统就有了 ACID 和多版本的支持,同时可以提供访问历史的功能。在这些方面,三者是相同的。 下面来谈一下三者的不同。 Hudi 先说 Hudi。Hudi 的设计目标正如其名,Hadoop Upserts Deletes and Incrementals(原为 Hadoop Upserts anD Incrementals),强调了其主要支持 Upserts、Deletes 和 Incremental 数据处理,其主要提供的写入工具是 Spark HudiDataSource API 和自身提供的 DeltaStreamer,均支持三种数据写入方式:UPSERT,INSERT 和 BULK_INSERT。其对 Delete 的支持也是通过写入时指定一定的选项支持的,并不支持纯粹的 delete 接口。 其典型用法是将上游数据通过 Kafka 或者 Sqoop,经由 DeltaStreamer 写入 Hudi。DeltaStreamer 是一个常驻服务,不断地从上游拉取数据,并写入 hudi。写入是分批次的,并且可以设置批次之间的调度间隔。默认间隔为 0,类似于 Spark Streaming 的 As-soon-as-possible 策略。随着数据不断写入,会有小文件产生。对于这些小文件,DeltaStreamer 可以自动地触发小文件合并的任务。 在查询方面,Hudi 支持 Hive、Spark、Presto。 在性能方面,Hudi 设计了 HoodieKey,一个类似于主键的东西。HoodieKey有 Min/Max 统计,BloomFilter,用于快速定位 Record 所在的文件。在具体做 Upserts 时,如果 HoodieKey 不存在于 BloomFilter,则执行插入,否则,确认 HoodieKey 是否真正存在,如果真正存在,则执行 update。这种基于 HoodieKey + BloomFilter 的 upserts 方法是比较高效的,否则,需要做全表的 Join 才能实现 upserts。对于查询性能,一般需求是根据查询谓词生成过滤条件下推至 datasource。Hudi 这方面没怎么做工作,其性能完全基于引擎自带的谓词下推和 partition prune 功能。 Hudi 的另一大特色是支持 Copy On Write 和 Merge On Read。前者在写入时做数据的 merge,写入性能略差,但是读性能更高一些。后者读的时候做 merge,读性能查,但是写入数据会比较及时,因而后者可以提供近实时的数据分析能力。 最后,Hudi 提供了一个名为 run_sync_tool 的脚本同步数据的 schema 到 Hive 表。Hudi 还提供了一个命令行工具用于管理 Hudi 表。 hudiimage Iceberg Iceberg 没有类似的 HoodieKey 设计,其不强调主键。上文已经说到,没有主键,做 update/delete/merge 等操作就要通过 Join 来实现,而 Join 需要有一个 类似 SQL 的执行引擎。Iceberg 并不绑定某个引擎,也没有自己的引擎,所以 Iceberg 并不支持 update/delete/merge。如果用户需要 update 数据,最好的方法就是找出哪些 partition 需要更新,然后通过 overwrite 的方式重写数据。Iceberg 官网提供的 quickstart 以及 Spark 的接口均只是提到了使用 Spark dataframe API 向 Iceberg 写数据的方式,没有提及别的数据摄入方法。至于使用 Spark Streaming 写入,代码中是实现了相应的 StreamWriteSupport,应该是支持流式写入,但是貌似官网并未明确提及这一点。支持流式写入意味着有小文件问题,对于怎么合并小文件,官网也未提及。我怀疑对于流式写入和小文件合并,可能 Iceberg 还没有很好的生产 ready,因而没有提及(纯属个人猜测)。 在查询方面,Iceberg 支持 Spark、Presto。 Iceberg 在查询性能方面做了大量的工作。值得一提的是它的 hidden partition 功能。Hidden partition 意思是说,对于用户输入的数据,用户可以选取其中某些列做适当的变换(Transform)形成一个新的列作为 partition 列。这个 partition 列仅仅为了将数据进行分区,并不直接体现在表的 schema 中。例如,用户有 timestamp 列,那么可以通过 hour(timestamp) 生成一个 timestamp_hour 的新分区列。timestamp_hour 对用户不可见,仅仅用于组织数据。Partition 列有 partition 列的统计,如该 partition 包含的数据范围。当用户查询时,可以根据 partition 的统计信息做 partition prune。 除了 hidden partition,Iceberg 也对普通的 column 列做了信息收集。这些统计信息非常全,包括列的 size,列的 value count,null value count,以及列的最大最小值等等。这些信息都可以用来在查询时过滤数据。 Iceberg 提供了建表的 API,用户可以使用该 API 指定表明、schema、partition 信息等,然后在 Hive catalog 中完成建表。 Delta 我们最后来说 Delta。Delta 的定位是流批一体的 Data Lake 存储层,支持 update/delete/merge。由于出自 Databricks,spark 的所有数据写入方式,包括基于 dataframe 的批式、流式,以及 SQL 的 Insert、Insert Overwrite 等都是支持的(开源的 SQL 写暂不支持,EMR 做了支持)。与 Iceberg 类似,Delta 不强调主键,因此其 update/delete/merge 的实现均是基于 spark 的 join 功能。在数据写入方面,Delta 与 Spark 是强绑定的,这一点 Hudi 是不同的:Hudi 的数据写入不绑定 Spark(可以用 Spark,也可以使用 Hudi 自己的写入工具写入)。 在查询方面,开源 Delta 目前支持 Spark 与 Presto,但是,Spark 是不可或缺的,因为 delta log 的处理需要用到 Spark。这意味着如果要用 Presto 查询 Delta,查询时还要跑一个 Spark 作业。更为蛋疼的是,Presto 查询是基于 SymlinkTextInputFormat。在查询之前,要运行 Spark 作业生成这么个 Symlink 文件。如果表数据是实时更新的,意味着每次在查询之前先要跑一个 SparkSQL,再跑 Presto。这样的话为何不都在 SparkSQL 里搞定呢?这是一个非常蛋疼的设计。为此,EMR 在这方面做了改进,支持了 DeltaInputFormat,用户可以直接使用 Presto 查询 Delta 数据,而不必事先启动一个 Spark 任务。 在查询性能方面,开源的 Delta 几乎没有任何优化。Iceberg 的 hidden partition 且不说,普通的 column 的统计信息也没有。Databricks 对他们引以为傲的 Data Skipping 技术做了保留。不得不说这对于推广 Delta 来说不是件好事。EMR 团队在这方面正在做一些工作,希望能弥补这方面能力的缺失。 Delta 在数据 merge 方面性能不如 Hudi,在查询方面性能不如 Iceberg,是不是意味着 Delta 一无是处了呢?其实不然。Delta 的一大优点就是与 Spark 的整合能力(虽然目前仍不是很完善,但 Spark-3.0 之后会好很多),尤其是其流批一体的设计,配合 multi-hop 的 data pipeline,可以支持分析、Machine learning、CDC 等多种场景。使用灵活、场景支持完善是它相比 Hudi 和 Iceberg 的最大优点。另外,Delta 号称是 Lambda 架构、Kappa 架构的改进版,无需关心流批,无需关心架构。这一点上 Hudi 和 Iceberg 是力所不及的。 deltaimage 总结 通过上面的分析能够看到,三个引擎的初衷场景并不完全相同,Hudi 为了 incremental 的 upserts,Iceberg 定位于高性能的分析与可靠的数据管理,Delta 定位于流批一体的数据处理。这种场景的不同也造成了三者在设计上的差别。尤其是 Hudi,其设计与另外两个相比差别更为明显。随着时间的发展,三者都在不断补齐自己缺失的能力,可能在将来会彼此趋同,互相侵入对方的领地。当然也有可能各自关注自己专长的场景,筑起自己的优势壁垒,因此最终谁赢谁输还是未知之数。 声明:本号所有文章除特殊注明,都为原创,公众号读者拥有优先阅读权,未经作者本人允许不得转载,否则追究侵权责任。 关注我的公众号,后台回复【JAVAPDF】获取200页面试题! 5万人关注的大数据成神之路,不来了解一下吗? 5万人关注的大数据成神之路,真的不来了解一下吗? 5万人关注的大数据成神之路,确定真的不来了解一下吗? 欢迎您关注 《大数据成神之路》
「深度学习福利」大神带你进阶工程师,立即查看>>> 新型冠状病毒(ncov)肺炎疫情分析项目。 采用Jupyter Notebook,基于nbdev创建,目前还处于 初级阶段。 项目数据, https://github.com/openthings/anti2020ncov/tree/master/data 项目源码, https://github.com/openthings/anti2020ncov 其它项目, 2020-nCoV open-source projects “这不是数据,是生命。” 1、说明 本项目仅作为nbdev应用示范,nCoV数据及分析结论请参考专业机构发布的报告,如: 丁香园-新型冠状病毒肺炎疫情实时动态, https://ncov.dxy.cn/ncovh5/view/pneumonia?from=singlemessage&isappinstalled=0 全国卫健委-新型冠状病毒肺炎疫情防控, http://www.nhc.gov.cn/xcs/xxgzbd/gzbd_index.shtml 全国卫健委-官方网站, http://www.nhc.gov.cn 2、运行环境 运行环境为Jupyter Notebook。 Jupyter Notebook参考: https://my.oschina.net/u/2306127?q=jupyter 本项目采用nbdev( https://github.com/fastai/nbdev )创建。 nbdev使用参考《 文学式编程-nbdev入门教程 》。 数据将被保存为文件,然后使用 pandas 进行分析,使用 CuteCharts 、 pyechart 进行可视化。 3、项目结构 该文件是本项目的索引文件。 包含子目录: data,本项目用到的数据。 events,发生的事件记录。 analyst,基于上述数据和事件的分析结果。 docs,本项目的文档。 anti2020ncov,自动生成的python代码。 下图为使用matplotlib绘制的2020-02-08的病例数量直方图。 遍历绘制各省的地市情况图:
「深度学习福利」大神带你进阶工程师,立即查看>>> 作者 | 董鹏 阿里巴巴技术专家 微服务 好处:实现跨团队的解耦,实现更高的并发(目前单机只能实现 c10k)不用再拷贝代码,基础服务可以公用,更好的支持服务治理,能够更好的兼容云计算平台。 RPC rpc:像调用本地方法一样调用远程函数; 客户端:一般利用动态代理生成一个接口的实现类,在这个实现类里通过网络把接口名称、参数、方法序列化后传出去,然后控制同步调用还是异步调用,异步调用需要设置一个回调函数; 客户端还需要维护负载均衡、超时处理、连接池管理等,连接池维护了和多个 server 的连接,靠此做负载均衡,当某个服务器宕机后去除该连接。请求上下文维护了请求 ID 和回调函数,超时的请求当回复报文到达后由于找不到请求上下文就会丢弃。 服务端:维护连接,网络收到请求后反序列化获得方法名称,接口名称,参数名称后通过反射进行调用,然后将结果在传回客户端; 序列化的方式:一种是只序列化字段的值,反序列化的时候重新构建对象再把值设置进去,另外一种方式直接将整个对象的结构序列化成二进制。 前者节省空间,后者反序列化速度快,目前的序列化框架也是在反序列化时间和占用空间之间权衡。有点类似哈夫曼编码,或者数据库怎么存储一行一行的数据。 注册中心 一般有 3 种模式: f5 做集中式代理; 客户端嵌入式代理例如 dubbo; 还有一种是综合上面两种,多个客户端共用一个代理,代理作为一个独立进程部署在和客户端服务器同一台物理机上,ServiceMesh 就是这种模式。 zookeeper 不适合做注册中心的原因:zookeeper 为了一致性牺牲了可用性,但是注册中心实际上对一致性要求并不高,不一致产生的后果也就是某个服务下线了而客户端并不知道,但是客户端通过重试其他节点就可以了。 另外当发生网络分区的时候,如果超过半数节点挂了,zookeeper 就不可用,但是实际上它应该仍然可以对它所在机房的节点提供注册服务,例如三个机房分别放了 2 台、2 台、1 台,如果各个机房之间网络断了,但是机房内部上是通的,这样注册中心不可用即使内部节点也不能服务了。 zookeeper 并不是严格的一致性,它支持读写分离,其它节点收到写请求会转发给 master 节点,而其它节点可以支持读请求,当数据还没有从主节点复制过来的时候读到的可能是过期的数据。 配置中心 配置中心的需求:保证高可用、实时通知、灰度发布、权限控制、一键回滚、环境隔离(开发/测试/生产)等,目前的开源实现:nacos disconf apollo。 disconf:scan 模块扫描注解和监听器; store 模块将远程获取到的配置存储到本地,本地一个 job 检测配置是否有变化,有变化就通知监听器; fetch 模块从远程通过 http 获取配置; watch 模块监听 zookeeper 上节点的变化,有变化就会调用 fetch 进行获取。 apollo 有以下 4 个模块: portal 作为一个管理后台,提供管理员操作的入口。 有独立的数据库; adminservice 提供配置的修改和发布服务的底层服务,和 configservice 公用一个数据库 configdb,每次修改配置就会往数据库里插入一条记录 releasemessage; configservice 用一个定时任务去扫描数据库是否有新的 releasemessage,有的话就通知客户端,而客户端采用定时轮询的方式去查询 configservice 是否有新消息,这里采用 deferredresult 异步执行; eruka 为 adminservice 和 configservice 提供了注册发现的服务。客户端获取到配置文件后也会写入磁盘。 任务调度 执行器也就是应用本身,任务单元也就是具体执行任务的线程,能够主动注册调度器中,并在启动的时候进行更新,例如删除已经清空的任务; 调度中心支持集群部署避免单点,可以选举一个主节点,其它为 slave; 支持负载均衡算法为每个任务随机选择执行器,能够支持失败重试,将执行很慢或者失去连接的执行器移除; 支持控制任务并发,例如是否允许一个任务没执行完又被调度; 支持任务依赖,例如一个任务没执行完另一个任务不能执行,或者自动执行另外一个任务; 支持任务分片,将一个任务根据参数分片到不同的执行器上一起执行; 可以取消一个任务; 已经支持 glue 模式,可以不用发布就执行一个任务单元。 分布式锁 redis setnx 里面已经有参数可以支持分布式锁,但是最好能把锁的拥有方存到 value 里,释放的时候做比较,不然可能释放错锁,也就是会出现 A 释放了 B 的锁; zk 采用创建临时节点,其他创建失败的线程监听锁的状态。 SET resource_name my_random_value NX PX 30000 统一监控 收集日志并分析,日志也可以和 rpc 链路进行关联,也可以对日志进行降噪或者压缩存储; 提供 api 的方式以及拦截器模式,可以基于 javaagent 做到无嵌入; 实现 opentracing 链路追踪; 可以基于 disruptor ringbuffer 的生产消费者模式; 海量数据的存储 elasticsearch; 报表生成,监控指标设置; 各个节点进行收集,消息上传到服务端统一处理; 监控指标:rpc 链路、数据库、cpu 指标等、http 状态、各种中间件; 日志收集可以通过直接在日志框架上加拦截器,或者用 flink+kafka 收集。
缓存 先清空缓存还是先更新数据库? 如果是更新缓存而不是删除缓存:则不管哪种方式都会造成缓存和数据库不一致; 如果是删除缓存:则先删除缓存在更新数据库,如果更新数据库失败了也没有太大影响,缓存被清了重新加载即可。但是也要考虑到缓存穿透的问题,如果这个时候大流量进来是否会压垮数据库? 以上是考虑到分布式事务中一个成功一个失败的情况,但是这种概率毕竟是小的,可以用在并发量不是很高但是对数据一致性要求很高的情况,如果并发很高建议先更新数据库后清空缓存。 如果先清空缓存,后更新数据库,在还没有更新到数据库的情况下另外一个事务去查询,发现缓存没命中就去数据库取,然后又写入缓存,之后上一个事务的数据库更新,这样就导致了缓存和数据库不一致,如果先更新数据库再清空缓存,更新完数据库后缓存还没更新,这个时候来读取缓存是旧的值,也出现不一致,但是最终清空缓存后会一致。 不过这种方式也会产生永久不一致,但是概率很小,例如一个读请求,没有命中缓存,这个时候可能另一个线程刚好清空缓存,然后它就去数据里面取,但是又有一个线程在它读完数据库后将数据库改为另外一个值,这样那个读请求写入到缓存的数据就是脏数据了。
redis 采用单线程模型,对只有 io 操作来说性能很好,但是 redis 也提供了计算功能,如排序聚合,cpu 在计算的时候所有的 io 操作都是阻塞的。 memecached 先申请一块内存,将其分割成大小不等的若干内存块以存储不同大小的键值对。这种方式效率高但是可能产生空间浪费。而 redis 只是单纯的包装了下 malloc 和 free。 redis 提供了两种方式持久化数据,一种方式是把某一时刻所有的数据都写入磁盘,另外一种方式通过增量日志的形式 memecache 提供了 cas 来保证数据一致性;redis 提供了事务,将一连串指令一起执行或者回滚。 memechache 只能通过一致性哈希来进行集群,而 redis 提供了集群功能,客户端做路由选择那个 master 节点,master 节点可以有多个 slave 节点做为备用和读。 redis 中的字符串没有采用 c 语言里的结构,额外加上了空闲内存和已占用内存,这样读取的时候由于已经知道 char 数组大小,所以可以直接取出,避免遍历操作,当字符串变大或缩小的时候可以避免重新分配内存,可以用到空闲空间,也就是 redis 会预分配一个空间。 另外 redis 里的哈希,用了两个 table 存储,主要为了扩容,也就是 rehash,这样当扩容的时候双方就可以互换,redis 采用渐近式扩容,也就是每一次操作都执行两个哈希表,当新增的时候只在新表。set 数据结构可以用来存储总的点赞次数,而 zset 是一个有序链表,为了加快查询用跳表进行存储。 如何防止缓存雪崩:缓存要高可用,可以设置多级缓存; 如何预防缓存穿透:设置不同的失效时间。 消息队列 如何保证消息的顺序 严格的一致,只能一个生产者,发送到一个 broker 上,然后只有一个队列一个消费者,但是这种模式有很多弊端,一个地方异常将阻塞整个流程,RocketMQ 将这个问题交给应用层处理,也就是发送端自己选择发送到哪个队列,例如同一个订单的消息发送到同一个队列。但是算法在其中一个队列异常的时候也会有问题。 如何保证消息不重复 只要网络上传输肯定会有这种问题,所以应用层最好能够支持幂等,或者用一张去重表存储每一个处理过的消息 ID。 发送消息流程 先获取 topic 对应的路由信息(路由信息会从 namesrv 返回,在客户端缓存,返回这个 topic 对应哪几个 broker 以及每个 broker 上有多少个队列); 如果没有获取到,可能没有 topic,需要自动创建,自动创建是客户端发信息个 namesrv,namesrv在去请求 broker,broker 创建好后返回 根据路由策略获取一个 queue(从所有的 queue 中根据对应的路由策略获取 queue,然后再判断这个 queue 对应的 broker 是否健康,健康就返回),这个地方就可以做到 broker 的高可用; 所以我们发现消息是发给哪个 broker 的哪个 queue 是在客户端发送的时候决定的,不是在生成 commitlog 之后再派发的,这样我们就可以指定都到某一个固定 queue 了; 消息发送的时候会构建发送请求,里面包含了消息体、队列信息和 topic 信息等,消息体里面会增加一个消息ID; 如果消息重试多次后还是失败就会进入死信队列,一个固定的 topic。 消息存储 每个 commitlog 大小为 1G,第二个文件的起始偏移量就是 1G 的 byte 大小,当根据一个偏移量获取对应某个文件的时候,根据偏移量对 1G 取余就可以,这些 commitlog 文件通过一个文件队列维护,每次写文件返回队列的最后一个文件,然后需要加锁。 创建完文件后会进行预热,预热的时候会在每一个内存页 4kb 里面写一个 byte0,让系统对缓存页缓存,防止真正写入的时候发生缺页,mmap 的机制是只会记录一个虚拟地址,当缺页时才会去获取物理内存的地址。 创建文件有两种方式: 一种是 FileChannel.map 获取 MappedByteBuffer; 另外一种是使用堆外内存池,然后 flush。 消息的消费 一个队列只能被一个客户端消费。 当存在多个队列,但只有一个客户端的时候,这个客户端需要去 4 个队列上消费,当只有一个队列的时候只会有一个客户端可以收到消息,所以一般情况下需要客户端数量和队列数量一致,客户端一般会保存每个队列消费的位置,因为这个队列只会有一个客户端消费,所以这个客户端每次消费都会记录下队列的 offset,broker 端,也会记录同一个 grouo 消费的 offset。 MappedByteBuffer 的原理是老的 read 是先将数据从文件系统读取到操作系统内核缓存,然后再将数据拷贝到用户态的内存供应用使用,而使用 mmap 可以将文件的数据或者某一段数据映射到虚拟内存,这个时候并没有进行数据读取,当用户访问虚拟内存的地址的时候会触发缺页异常,这个时候会从底层文件系统直接将数据读取到用户态内存。 而 MappedByteBuffer 通过 FileChannel 的 map 方法进行映射的时候会返回一个虚拟地址,MappedByteBuffer就是通过这个虚拟地址配合 UnSafe 获取字节数据。 操作系统在触发缺页异常的时候会去文件系统读取数据加载到内存,这个时候一般会进行预读取,一般为 4KB,当系统下次访问数据的时候就不会发生缺页异常,因为数据已经在内存里了,为了让 MappedByteBuffer 读取文件的速度更高,我们可以对 MappedByteBuffer 所映射的文件进行预热,例如将每个 pagecache 写一个数据,这样在真正写数据的时候就不会发生缺页了。 分库分表 一般三种方式:在 dao 层和 orm 层利用 mybatis 拦截器,基于 jdbc 层进行拦截重写 JDBC 接口做增强,基于数据库代理。 jdbc 代理,实现 datasource,connection,preparestatement,druid 解析 sql,生成执行计划,利用 resultset 对结果集进行合并(group by order max sum)。
分表策略,一般是哈希,要保证分库和分表的算法完全没有关联,不然会数据分布不均匀。 数据扩容的时候可以通过配置中心动态的修改写入策略,如何一开始可以先读老表,数据同时写入新表和老表,等数据迁移完成后,在读新表并双写,之后在读新表写新表。 唯一 id 数据库自增 id,一次取多个,单机限制,另外数据库自增 id 内部也用了个锁,只是在 sql 执行结束即使事务没提交也会释放锁。 雪花算法变种 : 15 位时间戳,4 位自增序列,2 位区分订单类型,7 位机器ID,2 位分库后缀,2 位分表后缀,共 32 位。 利用 zookeeper 的顺序节点获取自增 ID。 分布式事务 两阶段提交:事务管理器,资源管理器,一阶段准备,二阶段提交 (XA 方案对业务无侵入,由数据库厂商提供支持,但是性能很差)。 事物补偿 TCC :也是两阶段,第一阶段尝试锁定资源,第二阶段确认或者回滚。 设计规范: 业务操作分成两部,例如转账:尝试阶段为冻结余额,第二阶段提交为从冻结余额扣款,回滚为解冻; 事务协调器记录主事务日志和分支事务日志,支持在任意一步发生异常后进行补偿或者逆向补偿保证最终一致性; 并发控制,降低锁的粒度提高并发,保证两个事务间不需要加排他锁,例如热点账户的转账操作,由于第一阶段进行了冻结,所以后面的扣减余额不同事务之间没有影响; 允许空回滚:可能一阶段的尝试操作发生超时,然后二阶段发起回滚,回滚的时候要判断一阶段是否进行过操作,如果一阶段没有收到请求,回滚操作直接返回成功; 避免一阶段操作悬挂:可能一阶段超时,二阶段回滚后,一阶段的请求到达,这时候要拒绝一阶段的尝试操作; 幂等控制,由于第一阶段和第二阶段的操作可能都会执行多次,另外操作接口最好能提供状态查询接口供后台的补偿任务正常执行。 框架事务(seata) 一阶段:框架会拦截业务 sql,根据语句执行前结果生成 undolog , 根据语句执行后对结果生成 redolog , 根据数据库表名加主键生成行锁; 二阶段:如果事务正常结束,将删除 undolog redolog 行锁,如果事务将回滚,则执行 undolog sql , 删除中间数据,在执行 undolog 的时候会校验脏写,也就是有没有其他事务已经修改了这行记录,就用 redolog 做对比,如果出现脏写只能人工修数据 (二阶段的清理工作可以异步执行)。 开启事务的时候会向 tc 申请一个全局的事务 id,这个事务 id 会通过 rpc 框架的拦截器传入到被调用端,然后放入 threadlocal,被调用方在执行 sql 的时候会去检查一下是否在一个全局事务里。 默认的隔离级别为读未提交,因为事务一阶段已经本地事务提交而全局事务并没有完成,后续可能会回滚,其他事务可以看到这个状态,提供的读已提交的方式是通过 for update,当解析到该语句的时候会检查是否存在行锁冲突,如果存在冲突就等待直到释放。 tm 向 tc 发起开启一个全局事务,生成一个全局唯一的 xid; xid 在微服务调用链上进行传递; rm 向 tc 注册分支事务; tm 向 tc 发起全局提交或者回滚决议; tc 向 rm 发起回滚或提交请求。 一致性消息队列:先发送半消息,如果成功了在执行本地事务,本地事务成功就提交半消息,本地事务失败就回滚半消息,如果消息队列长期没有收到确认或者回滚可以反查本地事务的状态,消费端收到消息后,执行消费端业务,如果执行失败可以重新获取,执行成功发送消费成功的确认。 MYCAT CAP C:一致性 A:可用性 P:分区容忍性 可以简单地这样理解:MySQL 单机是C;主从同步复制 CP;主从异步复制 AP。 Zookeeper 选择了 P,但是既没有实现 C,也没有实现 A,而是选择最终一致性。可以在多个节点上读取,但是只允许一个节点接受写请求,其他节点接收的写请求会转发给主节点,只要过半节点返回成功就会提交。 如果一个客户端连接的正好是没有被提交的 follower 节点,那么这个节点上读取到的数据就是旧的,这样就出现了数据的不一致,所以没有完全实现 C。由于需要过半节点返回成功才提交,如果超过半数返回失败或者不返回,那么 zookeeper 将出现不可用,所以也没有完全实现 A。 当然衡量一个系统是 CP 还是 AP,可以根据它牺牲 A 更多还是牺牲 C 更多,而 ZK 其实就是牺牲了 A 来满足 C,当超过集群半数的节点宕机后,系统将不可用,这也是不建议使用 zk 做注册中心的原因。 CAP 理论只是描述了在分布式环境中一致性、可用性、分区容忍不能同时满足,并没有让我们一定要三选二,由于网络分区在分布式环境下是不可避免的,所以为了追求高可用,往往我们会牺牲强一执行,采用弱一致性和最终一致性的方案,也就是著名的 BASE 理论,而 base 理论其实是针对传统关系型数据的 ACID 而言的。 但 ACID 的提出是基于单节点下的,在分布式环境下,如何协调数据一致性,也就是在数据的隔离级别上做出取舍,即使是单机的关系型数据库为了提高性能,也就是可用性,定义了隔离级别,去打破 ACID 里面的强一致性 C,当然数据库也是为业务服务的,某些业务或者说大部分业务都没有强一致性的需求。 秒杀的处理 动静分离:ajax 不刷新页面,缓存,cdn; 发现热点数据:业务流程上变通让热点业务隔离出来,也通过链路监控获取一段时间的热点数据; 隔离:业务隔离,数据库隔离; 兜底方案:服务降级,限流; 流量削峰:排队,过滤无效请求,答题或者验证码,消息队列; 减库存:(下单减库存用户不付款需要回滚,付款减库存最终可能库存不足需要退款,下单后占库存一段时间后在回滚)。 正常电商采用第三种,秒杀采用第一种,不超卖的控制不用放在应用层,直接在 sql 层加 where 语句进行判断,但是 mysql 针对同一行记录也就是同一个商品的减库存,肯定会高并发下争取行锁,这将导致数据库的 tps 下降(死锁检测会遍历所有需要等待锁的连接,这个操作非常耗 cpu),从而影响其他商品的销售,所以我们可以将请求在应用层进行排队,如果份额较少可以直接舍弃,另一种方案是在数据库层排队,这种方案需要采用 mysql 的补丁。 docker namespace docker 在创建容器进程的时候可以指定一组 namespace 参数,这样容器就只能看到当前 namespace 所限定的资源、文件、设备、网络、用户、配置信息,而对于宿主机和其他不相关的程序就看不到了,PID namespace 让进程只看到当前 namespace 内的进程,Mount namespace 让进程只看到当前 namespace 内的挂载点信息,Network namespace 让进程只看到当前 namespace 内的网卡和配置信息, cgroup 全名 linux control group,用来限制一个进程组能够使用的资源上限,如 CPU、内存、网络等,另外 Cgroup 还能够对进程设置优先级和将进程挂起和恢复,cgroup 对用户暴露的接口是一个文件系统,/sys/fs/cgroup 下这个目录下面有 cpuset,memery 等文件,每一个可以被管理的资源都会有一个文件,如何对一个进程设置资源访问上限呢? 在 /sys/fs/cgroup 目录下新建一个文件夹,系统会默认创建上面一系列文件,然后 docker 容器启动后,将进程 ID 写入 taskid 文件中,在根据 docker 启动时候传人的参数修改对应的资源文件。 chroot 通过 chroot 来更改 change root file system 更改进程的根目录到挂载的位置,一般会通过 chroot 挂载一个完整的 linux 的文件系统,但是不包括 linux 内核,这样当我们交付一个 docker 镜像的时候,不仅包含需要运行的程序还包括这个程序依赖运行的这个环境,因为我们打包了整个依赖的 linux 文件系统,对一个应用来说,操作系统才是它所依赖的最完整的依赖库。 增量层 docker 在镜像的设计中引入层的概念,也就是用户在制作 docker 镜像中的每一次修改,都是在原来的 rootfs 上新增一层 roofs,之后通过一种联合文件系统 union fs 的技术进行合并,合并的过程中如果两个 rootfs 中有相同的文件,则会用最外层的文件覆盖原来的文件来进行去重操作。 举个例子,我们从镜像中心 pull 一个 mysql 的镜像到本地,当我们通过这个镜像创建一个容器的时候,就在这个镜像原有的层上新加了一个增 roofs,这个文件系统只保留增量修改,包括文件的新增删除、修改,这个增量层会借助 union fs 和原有层一起挂载到同一个目录,这个增加的层可以读写,原有的其他层只能读,于是就保证了所有对 docker 镜像的操作都是增量。 之后用户可以 commit 这个镜像将对该镜像的修改生成一个新的镜像,新的镜像就包含了原有的层和新增的层,只有最原始的层才是一个完整的 linux fs, 那么既然只读层不允许修改,我怎么删除只读层的文件呢?这时只需要在读写层(也就是最外层),生成一个 whiteout 文件来遮挡原来的文件就可以了。 发布与部署 目前的大部分公司采用下面的部署方式。 创建 pileline 指定项目名称和对应的 tag,以及依赖工程。一个 pipeline 指一个完整的项目生命周期(开发提交代码到代码仓库、打包、部署到开发环境、自动化测试、部署到测试环境、部署到生产环境); 根据项目名称和 tag 去 gitlab 上 拉取最新的代码(利用 java 里的 Runtime 执行 shell 脚本); 利用 maven 进行打包,这个时候可以为 maven 创建一个单独的 workspace (shell 脚本); 根据预先写好的 docfile,拷贝 maven 打的包生成镜像,并上传镜像 (shell 脚本); 通过 K8s 的 api 在测试环境发布升级; 通过灰度等方案发布到生产环境。 查看更多:https://yq.aliyun.com/articles/743558?utm_content=g_1000102890 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 项目背景 阿里巴巴内部从最早引入混沌工程解决微服务的依赖问题,到业务服务、云服务稳态验证,进一步升级到公共云、专有云的业务连续性保障,以及在验证云原生系统的稳定性等方面积累了比较丰富的场景和实践经验。并且当时混沌工程相关的开源工具存在场景能力分散、上手难度大、缺少实验模型标准,场景难以扩展和沉淀等问题。这些问题就会导致很难实现平台化,你很难通过一个平台去囊括这些工具。所以我们开源了 ChaosBlade 这个混沌工程实验执行工具,目的是服务于混沌工程社区,共同推进混沌工程领域的发展。 项目介绍 ChaosBlade 项目托管在 Github 平台,放在 chaosblade-io 组织下,方便项目管理和社区发展。设计 ChaosBlade 初期就考虑了易用性和场景扩展的便捷性,方便大家上手使用以及根据各自需要扩展更多的实验场景,遵循混沌实验模型提供了统一的操作简洁的执行工具,并且根据领域划分将场景实现封装成一个一个单独的项目,方便实现领域内场景扩展。目前包含的场景领域如下: 基础资源:比如 CPU、内存、网络、磁盘、进程等实验场景 Java 应用:比如数据库、缓存、消息、JVM 本身、微服务等,还可以指定任意类方法注入各种复杂的实验场景 C++ 应用:比如指定任意方法或某行代码注入延迟、变量和返回值篡改等实验场景 Docker 容器:比如杀容器、容器内 CPU、内存、网络、磁盘、进程等实验场景 Kubernetes 平台:比如节点上 CPU、内存、网络、磁盘、进程实验场景,Pod 网络和 Pod 本身实验场景如杀 Pod,容器的实验场景如上述的 Docker 容器实验场景 云资源:比如阿里云 ECS 宕机等实验场景 以上场景领域都单独封装成一个项目来实现,目前包含的项目如下: chaosblade :混沌实验管理工具,包含创建实验、销毁实验、查询实验、实验环境准备、实验环境撤销等命令,是混沌实验的执行工具,执行方式包含 CLI 和 HTTP 两种。提供完善的命令、实验场景、场景参数说明,操作简洁清晰。 chaosblade-spec-go : 混沌实验模型 Golang 语言定义,便于使用 Golang 语言实现的场景都基于此规范便捷实现。 chaosblade-exec-os : 基础资源实验场景实现。 chaosblade-exec-docker : Docker 容器实验场景实现,通过调用 Docker API 标准化实现。 chaosblade-operator : Kubernetes 平台实验场景实现,将混沌实验通过 Kubernetes 标准的 CRD 方式定义,很方便的使用 Kubernetes 资源操作的方式来创建、更新、删除实验场景,包括使用 kubectl、client-go 等方式执行,而且还可以使用上述的 chaosblade cli 工具执行。 chaosblade-exec-jvm : Java 应用实验场景实现,使用 Java Agent 技术动态挂载,无需任何接入,零成本使用,而且支持卸载,完全回收 Agent 创建的各种资源。 chaosblade-exec-cplus : C++ 应用实验场景实现,使用 GDB 技术实现方法、代码行级别的实验场景注入。 以上项目都遵循混沌实验模型定义实验场景,这样不仅实现实验场景水平领域扩展,而且每个场景领域单独一个项目,使用该领域下标准方式去设计实现场景,所以很方便的实现领域内场景垂直扩展。 除了实验场景相关项目,还有相关的文档项目: chaosblade-help-doc : ChaosBlade 工具和场景使用文档 chaosblade-dev-doc : ChaosBlade 项目开发文档 awesome-chaosblade : ChaosBlade 相关的外部文档 实验模型 前面提到 ChaosBlade 项目是遵循混沌实验模型设计,不仅简化了实验场景定义,而且可以很方便的扩展场景,并且通过 chaosblade cli 工具可以统一调用,便于构建上层的混沌实验平台。下面通过实验模型的推导、介绍、意义和具体的应用来详细介绍此模型。 实验模型的推导 目前的混沌实验主要包含故障模拟,我们一般对故障的描述如下: 10.0.0.1 机器上挂载的 A 磁盘满造成了服务不可用 所有节点上的 B dubbo 服务因为执行缓慢造成上游 A dubbo 服务调用延迟,从而造成用户访问缓慢 Kubernetes A 集群中 B 节点上 CPU 所有核使用率满载,造成 A 集群中的 Pod 调度异常 Kubernetes C 集群中 D Pod 网络异常,造成 D 相关的 Service 访问异常 通过上述,我们可以使用以下句式来描述故障:因为某某机器(或集群中的资源,如 Node,Pod)上的哪个组件发生了什么故障,从而造成了相关影响。我们也可以通过下图来看故障描述拆分: 可以通过这四部分来描述现有的故障场景,所有我们抽象出了一个故障场景模型,也称为混沌实验模型 实验模型的介绍 此实验模型详细描述如下: Scope: 实验实施范围,指具体实施实验的机器、集群及其资源等 Target: 实验靶点,指实验发生的组件。如基础资源场景中的 CPU、网络、磁盘等,Java 场景中的应用组件如 Dubbo、Redis、RocketMQ、JVM 等,容器场景中的 Node、Pod、Container自身等 Matcher: 实验规则匹配器,根据所配置的 Target,定义相关的实验匹配规则,可以配置多个。由于每个 Target 可能有各自特殊的匹配条件,比如 RPC 领域的 Dubbo、gRPC 可以根据服务提供者提供的服务和服务消费者调用的服务进行匹配,缓存领域的 Redis,可以根据 set、get 操作进行匹配。还可以对 matcher 进行扩展,比如扩展实验场景执行策略,控制实验触发时间。 Action: 指实验模拟的具体场景,Target 不同,实施的场景也不一样,比如磁盘,可以演练磁盘满,磁盘 IO 读写高,磁盘硬件故障等。如果是应用,可以抽象出延迟、异常、返回指定值(错误码、大对象等)、参数篡改、重复调用等实验场景。如果是容器服务,可以模拟 Node、Pod、Container 资源异常或者其上的基础资源异常等。 使用此模型可以很清晰表达出以下实施混沌实验需要明确的问题: 混沌实验的实施范围是什么 实施混沌实验的对象是什么 实验对象触发实验的条件有哪些 具体实施什么实验场景 实验模型的意义 此模型具有以下特点: 简洁:层次清晰,通俗易懂 通用:覆盖目前所有的故障场景,包含基础资源、应用服务、容器服务、云资源等 易实现:很方便的定义清晰的接口规范,实验场景扩展实现简单 语言、领域无关:可以扩展多语言、多领域的模型实现 此模型具有以下的意义: 更精准的描述混沌实验场景 更好的理解混沌实验注入 方便沉淀现有的实验场景 依据模型发掘更多的场景 混沌实验工具更加规范、简洁 实验模型的应用 ChaosBlade 下的项目遵循此混沌实验模型设计,需要注意的是此模型定义了混沌实验场景如何设计,但是实验场景的具体实现每个领域各不相同,所以将 ChaosBlade 依据领域实现封装成各自独立的项目,每个项目根据各领域的最佳实践来实现,不仅能满足各领域使用习惯,而且还可以通过混沌实验模型来建立与 chaosblade cli 项目的关系,方便使用 chaosblade 来统一调用,各领域下的实验场景依据混沌实验模型生成 yaml 文件描述,暴露给上层混沌实验平台,混沌实验平台根据实验场景描述文件的变更,自动感知实验场景的变化,无需新增场景时再做平台开发,使混沌平台更加专注于混沌工程其他部分。以下分为基于混沌实验模型的 chaosblade cli 设计、基于混沌实验模型的 chaosblade operator 设计和基于混沌实验模型构建混沌实验平台三部分详细介绍混沌实验模型的应用。 基于混沌实验模型的 chaosblade cli 设计 chaosblade 项目本身使用 Golang 构建,解压即用,工具采用 CLI 方式执行,使用简单,具备完善的命令提示。根据 chaosblade-spec-go 项目对混沌实验模型的定义,解析遵循混沌实验模型实现的实验场景 yaml 描述,将实验场景转换为 cobra 框架所支持的命令参数,实现变量参数化、参数规范化,而且将整个实验对象化,每个实验对象都会有个 UID,方便管理。 通过一个具体的实验场景来说明 chaosblade cli 的使用。 我们执行的实验是对其中一个 provider 服务实例注入调用 mk-demo 数据库延迟的故障,可以看到上图左下角,这个就是对 demo 数据库注入延迟的命令,可以看出命令非常简洁清晰,比如很明确的表达出我们的实验目标是 mysql,我们的实验场景是做延迟,后面这些都是这些数据库的匹配器,比如表,查询类型,还有控制实验的影响条数等等,使用 ChaosBlade 可以很有效的控制实验的爆炸半径。执行这条命令就可以对这台机器的 provider 服务注入故障,大家可以看到我注入故障之后,这里这个图就是我立刻收到了钉钉的报警,那么这个 case 是符合预期的 case,但是即使符合预期的case,也是有价值的,需要相关的开发和运维人员是要去排查延迟的问题根因并恢复,有助于提高故障应急效率。 chaosblade 的中文使用文档: https://chaosblade-io.gitbook.io/chaosblade-help-zh-cn 基于混沌实验模型的 chaosblade operator 设计 chaosblade-operator 项目是针对 Kubernetes 平台所实现的混沌实验注入工具,遵循上述混沌实验模型规范化实验场景,把实验定义为 Kubernetes CRD 资源,将实验模型中的四部分映射为 Kubernetes 资源属性,很友好的将混沌实验模型与 Kubernetes 声明式设计结合在一起,依靠混沌实验模型便捷开发场景的同时,又可以很好的结合 Kubernetes 设计理念,通过 kubectl 或者编写代码直接调用 Kubernetes API 来创建、更新、删除混沌实验,而且资源状态可以非常清晰的表示实验的执行状态,标准化实现 Kubernetes 故障注入。除了使用上述方式执行实验外,还可以使用 chaosblade cli 方式非常方便的执行 kubernetes 实验场景,查询实验状态等。 遵循混沌实验模型实现的 chaosblade operator 除上述优势之外,还可以实现基础资源、应用服务、Docker 容器等场景复用,大大方便了 Kubernetes 场景的扩展,所以在符合 Kubernetes 标准化实现场景方式之上,结合混沌实验模型可以更有效、更清晰、更方便的实现、使用混沌实验场景。 下面通过一个具体的案例来说明 chaosblade-operator 的使用:对 cn-hangzhou.192.168.0.205 节点本地端口 40690 访问模拟 60% 的网络丢包。 使用 yaml 配置方式,使用 kubectl 来执行实验 apiVersion: chaosblade.io/v1alpha1 kind: ChaosBlade metadata: name: loss-node-network-by-names spec: experiments: - scope: node target: network action: loss desc: "node network loss" matchers: - name: names value: [ "cn-hangzhou.192.168.0.205" ] - name: percent value: [ "60" ] - name: interface value: [ "eth0" ] - name: local -port value: [ "40690" ] 执行实验: kubectl apply -f loss-node-network-by-names.yaml 查询实验状态,返回信息如下(省略了 spec 等内容): ~ » kubectl get blade loss-node-network- by -names -o json { "apiVersion" : "chaosblade.io/v1alpha1" , "kind" : "ChaosBlade" , "metadata" : { "creationTimestamp" : "2019-11-04T09:56:36Z" , "finalizers" : [ "finalizer.chaosblade.io" ], "generation" : 1 , "name" : "loss-node-network-by-names" , "resourceVersion" : "9262302" , "selfLink" : "/apis/chaosblade.io/v1alpha1/chaosblades/loss-node-network-by-names" , "uid" : "63a926dd-fee9-11e9-b3be-00163e136d88" }, "status" : { "expStatuses" : [ { "action" : "loss" , "resStatuses" : [ { "id" : "057acaa47ae69363" , "kind" : "node" , "name" : "cn-hangzhou.192.168.0.205" , "nodeName" : "cn-hangzhou.192.168.0.205" , "state" : "Success" , "success" : true , "uid" : "e179b30d-df77-11e9-b3be-00163e136d88" } ], "scope" : "node" , "state" : "Success" , "success" : true , "target" : "network" } ], "phase" : "Running" } } 通过以上内容可以很清晰的看出混沌实验的运行状态,执行以下命令停止实验: kubectl delete -f loss-node-network- by -names.yaml 或者直接删除此 blade 资源 kubectl delete blade loss-node-network- by - names 还可以编辑 yaml 文件,更新实验内容执行,chaosblade operator 会完成实验的更新操作。 使用 chaosblade cli 的 blade 命令执行 blade create k8s node-network loss --percent 60 --interface eth0 --local-port 40690 --kubeconfig config --names cn-hangzhou .192 .168 .0 .205 如果执行失败,会返回详细的错误信息;如果执行成功,会返回实验的 UID: { "code" : 200 , "success" : true , "result" : "e647064f5f20953c" } 可通过以下命令查询实验状态: blade query k8s create e647064f5f20953c --kubeconfig config { "code" : 200 , "success" : true , "result" : { "uid" : "e647064f5f20953c" , "success" : true , "error" : "" , "statuses" : [ { "id" : "fa471a6285ec45f5" , "uid" : "e179b30d-df77-11e9-b3be-00163e136d88" , "name" : "cn-hangzhou.192.168.0.205" , "state" : "Success" , "kind" : "node" , "success" : true , "nodeName" : "cn-hangzhou.192.168.0.205" } ] } } 销毁实验: blade destroy e647064f5f20953c 除了上述两种方式调用外,还可以使用 kubernetes client-go 方式执行,具体可参考: https://github.com/chaosblade-io/chaosblade/blob/master/exec/kubernetes/executor.go 代码实现。 通过上述介绍,可以看出在设计 ChaosBlade 项目初期就考虑了云原生实验场景,将混沌实验模型与 Kubernetes 设计理念友好的结合在一起,不仅可以遵循 Kubernetes 标准化实现,还可以复用其他领域场景和 chaosblade cli 调用方式,所谓的历史包袱根本不存在 :-)。 基于混沌实验模型构建混沌实验平台 前面也提到了遵循混沌实验模型实现的实验场景,可通过 yaml 文件来描述,上层实验平台可以自动感知实验场景的变更,无需平台再做开发,达到实验平台与实验场景解耦的目的,使大家可以更加专注于混沌实验平台本身的开发上。下面拿 AHAS Chaos 平台举例来说明如何基于混沌实验模型和 ChaosBlade 构建混沌实验平台。 可以看到: chaosblade 会合并所有领域场景的 yaml 文件,提供给 ChaosBlade SDK ChaosBlade SDK 感知 yaml 文件变化,重新解析场景描述文件,透传给上层平台,包含场景和场景参数的变更 ChaosBlade SDK 透传用户在平台上所配置的参数,调用 chaosblade 工具执行 chaosblade 工具会根据调用参数,和解析各领域 yaml 场景描述文件来调用不同的执行器 总结 混沌实验模型的应用可归纳为以下几点: 混沌实验模型使实验场景变量参数化,参数规范化 可遵循模型实现实验场景领域化的水平扩展 可将混沌实验模型和领域内标准化实现相结合,便捷实现领域内场景垂直扩展 上层的领域场景可以复用遵循混沌实验模型定义的场景 通过混沌实验模型声明的场景描述可以很好的接入到 chaosblade cli 中 遵循实验模型可以很方便的构建上层混沌实验平台 项目意义 混沌工程领域已提出多年,混沌工程社区的每一个人都贡献着自己的力量来完善整个混沌工程领域体系,尤其是混沌工程理论的提出推动了整个混沌工程领域快速发展。我们在阿里巴巴内部实践混沌工程很多年,深知落地混沌工程之路充满各种挑战,也知道注入混沌实验只是混沌工程中的一环,混沌工程背后的思考、落地方案和实践经验也是很重要的一部分。我们只是想把我们认为好用的内部工具奉献给社区,随后将刚才提到的实践经验也通过各种渠道分享给大家,大家可以将此工具与实践经验相结合,作为企业落地混沌工程的一个入手点,共同推进混沌工程领域的进步,仅此而已。 上述详细介绍了 ChaosBlade 工具的设计和背后的思考,以及将混沌实验模型与各领域标准实现相结合的优势,欢迎对高可用架构感兴趣的各位加入到 ChaosBlade 社区中来,加入到混沌工程社区中来。总而言之,ChaosBlade 相信:开源世界中,任何帮助都是贡献。 未来规划 ChaosBlade 社区在增强原有领域的同时,比如增强云原生领域场景,还会增加更多领域的场景,例如: Golang 应用混沌实验场景 NodeJS 应用混沌实验场景 除实验场景外,还会以下规划: 提供一个混沌实验平台供大家使用 完善 ChaosBlade 各项目的开发文档 完善 chaosblade 工具的英文文档 欢迎大家加入,一起共建,不限于: bug report feature request performance issue help wanted doc incomplete test missing feature design any question on project ChaosBlade 项目才刚刚开始,欢迎开源爱好者在使用 ChaosBlade 过程中产生的任何想法和问题,都可以通过 issue 或者 pull request 的方式反馈到 Github 上。 查看更多:https://yq.aliyun.com/articles/743102?utm_content=g_1000102888 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 去年,阿里云发布了本地 IDE 插件 Cloud Toolkit,仅 IntelliJ IDEA 一个平台,就有 15 万以上的开发者进行了下载,体验了一键部署带来的开发便利。时隔一年的今天,阿里云正式发布了 Visual Studio Code 版本,全面覆盖前端开发者,帮助前端实现一键打包部署,让开发提速 8 倍。 VSCode 版本的插件,目前能做到什么? 安装插件之后,开发者可以立即体验以下任何一个功能: 将本地 Visual Studio Code 中的代码,一键打包、部署到任意的阿里云 ECS 服务器; 将本地 Visual Studio Code 中的代码,一键打包、部署到任意的远程服务器; 向任意的阿里云 ECS 或者 任意远程服务器 发送并执行指定的命令(下周上线); VSCode 版本的插件正不断更新迭代中,不久将会有更多强大的功能上线,请期待!或向我们提出需求! 如何下载插件? 开发者可以通过在线安装或离线安装方式来下载插件: 在线安装:从 Visual Studio Code 的 Marketplace 进行安装,访问插件页面,点击 这里 。 离线安装:在插件的群里(文末扫码进群)获得离线安装包,进行离线安装; 阿里云 Cloud Toolkit 和其他任何插件安装方式一致,因此不做赘述,下面为大家详细介绍插件功能。 一键打包部署 一、添加服务器 如下图所示,在 Visual Studio Code 左侧,点击阿里云图标,在出现的菜单中,点击Alibaba Cloud Toolkit - Host View打开机器视图界面。 然后点击右上角Add Host按钮,出现添加机器界面,如下图。按照表单提示,填入对应的机器 IP,端口、用户名和密码即可。 二、部署 点击 Run Configurations - Deployments - Deploy to Host 创建一个新的部署配置,配置界面如下: 在 Deploy to Host 对话框设置部署参数,然后单击 Apply,即可保存。 部署参数说明 : Name:未配置定义一个名字,以便更容易区分多个配置; File:选择打包方式; Project:选择待部署工程的根目录; Build Output Directory:打包之后的 Output 目录; Webpack Configuration:Webpack 配置; Target Host:部署的远程目标服务器; Target Directory:远程目标服务器上的指定部署目录; After deploy:输入应用启动命令,如 sh /root/restart.sh,表示在完成应用包的部署后,需要执行的命令 —— 对于 Java 程序而言,通常是一句 Tomcat 的启动命令。 查看更多:https://yq.aliyun.com/articles/743040?utm_content=g_1000102887 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ 「深度学习福利」大神带你进阶工程师,立即查看>>> 引言 在可观察性领域,Metrics,Tracing 和 Logging 的介绍由来已久。三种之间的边界越来越模糊。OpenTracing 中已经支持 LogEvent,OpenTelemetry 已经把 OpenMetric 和 OpenTracing 整合到一块。今天我们要介绍的链路追踪的业务分析功能,通过对链路数据进行聚合统计,可以查看各种业务报表。 为什么需要业务分析关联链路数据? 精细化分析,全链路数据,快速发现和定位问题。 在传统的 APM 分析中,主要是关注各种接口的响应时间,对业务不够贴切,这样会带来一些问题: 发现问题 同一个接口被很多业务方调用,某一个重要业务调用出了问题,由于这个业务占比不高,无法通过平均数或者 p90 分析出来。例如创建订单中重要客户,这些客户订单占比不高,但是重要性比较高,需要重点分析和监控。 排查定位问题 某些用户反馈下单或者付款出现问题,但是这种情况只存在少量用户,重现比较困难。如果把用户的异常和链路关联起来,分析异常用户的链路数据,比如出入参数,用户属性等,可以发现一些线索。例如查看用户 ID 对应的失败订单,分析订单上链路,发现失败订单的来源都是某个旧版的客户端调用。 业务分析概述 在介绍业务分析功能之前,先简单提两个问题,大家发散思考下怎么解决这两个问题。 1、某个应用的对外接口流量突增,是不是由于某个用户或者地域流量暴涨? 2、应用出现比较多空指针异常,这些异常对业务有多大的影响? 通用的监控系统主要是对操作系统,分布式调用,数据库组件进行监控。如果对业务监控,那就需要开发同学对调用链埋点添加业务属性(用 tag 来记录各种业务属性)。通过对 tag 标签进行过滤和统计聚合,达到业务分析和监控的效果。 业务分析的实现方式 业务分析是在链路上标注一些业务数据,以下我们用 Jaeger 和 Skywalking 为例子讲解业务埋点。 Jaeger 中添加 Tag 信息 以 Java 语言为例, // 获取业务中的用户Id String userId = ****; Tracer tracer = GlobalTracer. get (); if (tracer != null && tracer.activeSpan() != null ) { // 将 UserId以 Tag 的形式存放到链路中 tracer.activeSpan().setTag( "userId" , userId); } Skywalking 中添加 Tag 信息, 不插入代码的方式: https://github.com/apache/skywalking/blob/master/docs/en/setup/service-agent/java-agent/Customize-enhance-trace.md < class class_name = "test.apache.skywalking.testcase.customize.service.TestService2" > < method method = "staticMethod(java.lang.String,int.class)" operation_name = "/is_2_static_method" static = "true" > < tag key = "tag_2_1" > arg[0] tag > < log key = "log_1_1" > arg[1] log > method > < /class> 用 OpenTracing 插入代码方式 // global tracer object private static Tracer tracer = new SkywalkingTracer(); ... if (tracer != null && tracer.activeSpan() != null ) { tracer.activeSpan().setTag( "userId" , yourUserId); } Java 代码方式: ActiveSpan .tag( "userId" , yourUserId); Java 注解方式 @Tag (key = "userId" , value = "arg[0]" ) @Tag (key = "requestId" , value = "arg[1]" ) public someMethod ( final String userId, final String requestId) { // ... } 对链路数据进行业务分析 Tag 信息就好比给图书打上编号分类信息,让我们可以在图书馆海量的图书中找到我们需要图书。通过 Tag 来进行分类、查找、统计,可以快速找到我们需要的数据。 业务标签的过滤 可以通过筛选 Tag 进行过滤,寻找所需要的链路。如下图,查看手机端下单的请求的变化趋势,详情等。 业务标签的聚合 指定 Tag 进行聚合,分析 Tag 聚合的统计。如下图查看每个入口(PC 和 Mobile)的请求数,响应时间,异常查询。可以识别是不同业务的对比情况。 业务分析的一些使用场景案例 Tag 功能主要是业务开发同学用来进行业务监控的, 通过在链路上写入 Tag,可以把业务和链路打通,从而很好的发现,定位,跟踪问题。 我们接下来做一些业务场景举例。 业务异常分析 生成订单时,会调用优惠,库存,物流,商品等系统接口。这些接口都有可能会返回异常,有些异常有些比较重要,有些只是用于展示。我们怎么快速识别当前应用的异常严重程度呢?可以通过加 Tag 的方式来实现。我们对异常进行分 1~5 级,1 级表示非常严重。 那我们每次调用远程接口完成后,对返回的异常添加 Tag,例如调用优惠时返回系统异常加 Tag: tracer.activeSpan().setTag("bizErr", “1-PromotionSysExcep”); ,调用库存返回库存不足,可以添加tracer.activeSpan().setTag("bizErr", “5-InventoryNotEmpty”); 那么通过 Tag bizErr 进行聚合,对数量进行排序下,如果 1-** 开头的比较多,那表示当前系统比较严重,需要紧急处理。查看这些异常链路的请求参数或者返回值,我们可以快速定位问题。 用户分析 将 UserId 用 Tag 方式写入到 Span 中,可以统计到没有用户的数据,例如访问量,响应时间。我们之前遇到一个问题,Web 的流量突然变大,响应比较慢。从监控来看流量比较大,但为什么流量会比较大呢,通过 UserId 分组统计后,发现某个用户的流量暴涨,导致整个后台响应比较慢,对这个用户进行限流后,web流量和相应时间恢复正常。 业务大盘 中台系统中各个业务的创建订单大盘。 在中台应用中,各个业务使用同一个交易系统,通过订单中的一个业务类型字段来区分不同的业务。通过在链路上的标签,可以查看到不同业务线的下单成功和失败曲线。比如大盘上可以展示天猫,聚划算,盒马生鲜等业务的订单情况。一旦有异常就可以分析链路的明细信息来查找原因。 和传统的统计不同点 传统的 ELK 方式,用户通过插入代码,将业务的属性都打到日志里面。通过数据聚合,可以生成各种 Metrics,添加告警等。这样做监控是合适的,但是需要更深层次的挖掘问题的话,还需要和链路打通。将监控的内容上下游的信息都串起来,可以更方便的定位问题。 比如:更新库存失败,发现异常都是上游某个业务的某台机器上,而这条机器是灰度机器,使用了非稳定版本的。 总结 业务排查的问题方式有很多种,调用链和业务信息打通已经是一种行之有效的方法。通过在调用链写入业务信息,将会给调用链配上显微镜,快速发现隐藏在链路数据中的各种问题。而业务数据关联调用链,也给业务排查插上翅膀,从链路的上下文信息中快速找到线索。 查看更多:https://yq.aliyun.com/articles/743030?utm_content=g_1000102886 上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/ |