鹅厂架构师谈:如何做好架构设计?
每个程序员都有成为架构师的梦。我们常说架构是决策、是制造规律、是用简单抽象复杂….…架构设计经验与思维,在开发者日常工作的应用积累将带来质变。从本期开始,我们将持续为您带来腾讯架构设计经验。有哪些行之有效的内行经验?又踩过哪些坑?人人都在提的“那几个词”到底是什么?腾讯黄规速将与你聊聊在腾讯做架构设计的那些事儿。 01.架构=要素+结构+连接 在软件行业,对于什么是架构一直有很多的争论,每个人都有自己的理解。不同的书籍上、不同的作者,对于架构的定义也不统一,角度不同,定义不同。此君说的架构和彼君理解的架构未必是一回事。 因此我们在讨论架构之前,我们先讨论架构的概念定义,因为概念是人认识这个世界的基础和用来沟通的手段,如果对架构概念理解不一样,那沟通起来自然不顺畅。 Linux 有架构,MySQL 有架构,JVM 也有架构,使用 Java 开发、MySQL 存储、跑在 Linux 上的业务系统也有架构,应该关注哪一个?想要清楚以上问题需要梳理几个有关系又相似的概念:系统与子系统、模块与组建、框架与架构。 系统与子系统 系统:泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能独立完成的工作能力的群体。提到系统,我们务必了解以下几个关键词: 关联:系统是由一群有关联的个体组成的,没有关联的个体堆在一起不能成为一个系统。例如,把一个发动机和一台 PC 放在一起不能称之为一个系统,把发动机、底盘、轮胎、车架组合起来才能成为一台汽车。 规则:系统内的个体需要按照指定的规则运作,而不是单个个个体各自为政。规则规定了系统内个体分工和协作的方式。例如,汽车发动机负责产生动力,然后通过变速器和传动轴,将动力输出到车轮上,从而驱动汽车前进。 能力:系统能力与个体能力有本质的差别,系统能力不是个体能力之和,而是产生了新的能力。例如,汽车能够载重前进,而发动机、变速器、传动轴、车轮本身都不具备这样的能力。 子系统:也是由一群关联的个体组成的系统,多半是在更大的系统中的一部分。 模块与组件 它们都是系统的组成部分,从不同角度拆分系统而已。模块是逻辑单元,组件是物理单元。 模块就是从逻辑上将系统分解, 即分而治之, 将复杂问题简单化。模块的粒度可大可小, 可以是系统、几个子系统、某个服务、函数、类、方法、 功能块等等。划分模块的主要目的是职责分离。 组件可以包括应用服务、数据库、网络、物理机,还可以包括 MQ、容器、Nginx 等技术组件。划分组件的主要目的是单元复用。“组件”的英文单词 Component,对应中文的“零件”一词,“零件”更容易理解一些。“零件”是一个物理的概念,并且具备“独立且可替换”的特点。现在越来越被的 UI 设计使用组件化和模块化。 框架与架构 框架通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品。 框架是组件实现的规范,例如:MVC、MVP、MVVM 等,是提供基础功能的产品,例如开源框架:Ruby on Rails、Spring、Laravel、Django 等,这是可以拿来直接使用或者在此基础上二次开发。 再例如,SpringMVC 是 MVC 的开发框架,除了满足 MVC 的规范,Spring 提供了很多基础功能来帮助我们实现功能,包括注解(@Controller等)、Spring Security、SpringJPA 等很多基础功能。 框架是规范,架构是结构。 框架和架构的区别还是比较明显的,框架关注的是“规范”,架构关注的是“结构”。框架的英文是 Framework 。例如,SpringMVC 是"Web MVC Framework"。架构的英文是 Architecture。例如,Linux 操作系统的架构。 在 TOGAF9 是这么定义:一个系统基本的构件(子系统,模块,组件),体现在它的各个构件、构件间的相互关系、构件与环境间的关系,以及对系统设计和演进进行治理的原则中。 两种含义: ▶︎ 第一,一个系统的形式化描述,或指导系统实现的构件级的详细计划。 ▶︎ 第二,一组构件的结构、构件间的相互关系,以及对这些构件的设计和随时间演进的过程进行治理的一些原则和指导策略。 架构从字面意思上,是源于古代的建筑术语。 把架构拆分成两个字“架”和“构”。“架”就是“加”和“木”的结合,把木头加起来、连接起来就是架。“构”就是结构的意思。所以,“架构”就是把“木”按照一定的结构连接起来。 对应到软件架构,“木”代表构件(要素),“结构”代表架构的产物:木就是系统中的要素,我们将它们称之为架构构件(要素)。架构要素可以是子系统、模块、应用服务、组件。结构,是架构的产物。不同的软件系统会有不同的结构,这些结构是为解决不同场景而设计的。 连接,通过定义架构元素之间的接口和交互关系、集成机制,实现架构元素之间的连接。连接可以是分布式调用、进程间调用、组件之间的交互关系等。总结一下架构的组成 = 要素 + 结构 + 连接,将系统要素按照特定结构进行连接交互。 我在这重新定义架构(见仁见智): 软件架构指软件系统顶层结构设计。 架构是经过系统性地思考,权衡利弊之后在现有资源约束下的最合理决策,最终明确的系统骨架:包括子系统、模块、组件,以及他们之间协作关系、约束规范、指导原则,并由它来指导系统各方面的设计和指导团队中的每个人思想层面上的一致。 涉及四方面: ▶︎ 系统性思考的合理决策:比如技术选型、解决实施方案(包括执行目标计划)、成本评估、性价比评估等等。 ▶︎ 结构:明确的系统骨架(结构):明确系统有哪些构件组成。 ▶︎ 连接:系统协作关系:各个组成部分如何协作来实现业务请求。 ▶︎ 规范:约束规范和指导原则:保证系统有序,高效、稳定运行,包括规范、原则、流程等内容。 02.没有架构设计?也许你的系统还不够复杂 如果没有架构设计,说明你的系统不够复杂。随着业务的增长,系统由单体应用渐进演化为分布式和微服务化。 系统整体的复杂性越来越高,技术团队可能从一个团队变成多个专业化团队。假如没有架构设计,系统定会是一个无序失控的状态,带来的问题: 问题 原因 应用服务的边界不清晰 到底该怎么拆分没有一个明确的原则,研发人员为了所谓微服务化而拆分,而不是从当前业务考虑。导致系统无序地状态,开发效率低。我们系统出现过类似的情况:一个简单项目拆分成8个子服务,问他为什么这么拆分,说微服务化是为了应对以后扩展方便。结果这个项目从2017年到现在都没有再修改过,接手人宁愿新开发一个项目也不愿重构。 应用服务层次不清晰,系统耦合严重 导致服务依赖出现网状依赖结构,牵一发动全身,后续修改和扩展困难。 系统应用服务跟踪问题 由于微服务化后,系统逻辑复杂,服务出现问题后,你很难快速地定位问题和修复。这是我们踩过不少坑,我们使用 dubbo 服务化,系统一旦出现问题,一堆人手忙脚乱。 系统服务监控问题 由于研发人员基本没有服务监控意识,都是出现问题后再想办法如何添加服务监控接口。 技术体系失控问题 不同的开发团队使用不同的技术栈或者组件,造成公司内部的技术架构失控。甚至研发人员为追求时髦新潮技术,拿应用项目来试验新技术。 当然,我们还能列举出更多问题。 架构设计的目的是为了解决系统复杂性带来的问题。其本质就是对系统进行有序化地重构以致符合当前业务的发展,并可以快速扩展。 从上面架构的定义,也知道架构设计的作用涉及四方面:系统性思考的合理决策;明确的系统骨架;系统协作关系;约束规范和指导原则。 无论是何种变化,架构师通过理解业务、全局把控,权衡业务需求和技术实现。选择合适技术,解决关键问题并指导研发落地实施,最终促进业务发展,提高效率。 03.此架构非彼架构?架构到底怎么分? 在 EA 架构领域,有两种常见架构方法 RUP 和 TOGAF,这两个框架也是我们常常了解架构分类的两个维度。 从我个人的角度觉得 TOGAF 的分类方式更加广泛使用,因此本次分享我简单展开介绍 TOGAF ,如果你对 RUP 感兴趣,推荐阅读1995年,Philippe Kruchten 在《IEEE Software》上发表了题为《The 4+1 View Model of Architecture》的论文。 TOGAF9 对架构的分类是这样的: 架构类型 描述 业务架构 业务战略、治理、组织和关键业务流程。 数据架构 组织的各类逻辑和物理数据资产以及数据管理资源的结构。 应用架构 描述被部署的单个应用系统、系统之间的交互,以及它们与组织核心业务流程之间关系的蓝图。 技术架构 对于支持业务、数据和应用服务的部署来说必需的逻辑软、硬件能力。包括1T基础设施、中间件、网络、通信、部署处理和一些标准等。 由于不同架构方法论,定义的架构分类也不同,RUP4+1架构方法主要是以架构生命周期为视角进行描述,而 TOGAF9 按架构涉及内容维度来描述。 因此我结合两者细分为业务架构、应用架构、数据架构、技术架构、代码架构、部署架构。 业务架构是战略,应用架构是战术,技术架构是装备。其中应用架构承上启下,一方面承接业务架构的落地,另一方面影响技术选型。熟悉业务,形成业务架构;根据业务架构作出相应的应用架构,最后技术架构落地实施。 你也可以理解成:业务架构是生产力,应用架构是生产关系,技术架构是生产工具。业务架构决定应用架构,应用架构需要适配业务架构,并随着业务架构不断进化,同时应用架构依托技术架构最终落地。 04.架构师都在说的单体应用、分布式与微服务到底是什么? 整体来说,架构演进路程是这样的: 单体应用->分布式应用服务化->微服务 下面我将做简单介绍。 单体应用 企业一开始业务比较简单,只应用某个简单场景,应用服务支持数据增删改查和简单的逻辑即可,单体应用可以满足要求。 典型的三级架构:前端(Web/手机端)+中间业务逻辑层+数据库层。这是一种典型的 Java Spring MVC 或者 Python Django 框架的应用。针对单体应用,非功能性需求的做法: ▶︎ 性能需求:使用缓存改善性能; ▶︎ 并发需求:使用集群改善并发; ▶︎ 读写分离:数据库的读写分离; ▶︎ 使用反向代理和 cdn 加速; ▶︎ 使用分布式文件和分布式数据库。 单体架构的应用比较容易部署、测试, 在项目的初期,单体应用可以很好地运行。然而,随着需求的不断增加, 越来越多的人加入开发团队,代码库也在飞速地膨胀。 慢慢地,单体应用变得越来越臃肿,可维护性、灵活性逐渐降低,维护成本越来越高。总体来说,单体架构应用可能会有这些缺点:复杂性高、技术债务积累、部署频率低、可靠性差、扩展能力受限并且可能阻碍技术创新。 分布式 为了解决上面提到的这些缺点,我们需要对系统按照业务功能模块拆分,将各个模块服务化,变成一个分布式系统。 业务模块分别部署在不同的服务器上,各个业务模块之间通过接口进行数据交互。该架构相对于单体架构来说,这种架构提供了负载均衡的能力,大大提高了系统负载能力,解决了网站高并发的需求。另外还有以下特点: 降低耦合度:把模块拆分,使用接口通信,降低模块之间的耦合度。责任清晰:把项目拆分成若干个子项目,不同的团队负责不同的子项目。扩展方便:增加功能时只需要再增加一个子项目,调用其他系统的接口就可以。部署方便:可以灵活地进行分布式部署。提高代码的复用性:比如 Service 层,如果不采用分布式 rest 服务方式架构就会在手机 Wap 商城,微信商城,PC,Android,iOS 每个端都要写一个 Service 层逻辑,开发量大,难以维护一起升级,这时候就可以采用分布式 rest 服务方式,公用一个 service 层。*缺点:系统之间的交互要使用远程通信,接口开发增大工作量,但是利大于弊。 微服务 随着业务模式越来越复杂,订单、商品、库存、价格等各个模块都很深入,比如价格区分会员等级,访问渠道(app 还是 PC),销售方式(团购还是普通)等,还有大量的价格促销。 这些规则很复杂,容易相互冲突。我们需要把分散到各个业务的价格逻辑进行统一管理,以基础价格服务的方式透明地提供给上层应用,变成一个微内核的服务化架构,即微服务。 微服务的特点明显:易于开发与维护、单个微服务启动较快、局部修改容易部署、技术栈也不受限制。 然而,微服务虽然有很多吸引人的地方,但它并不是免费的午餐,使用它可能面临这些挑战: 运维要求较高:更多的服务意味着更多的运维投入。在单体架构中,只需要保证一个应用的正常运行。而在微服务中,需要保证几十甚至几百个服务的正常运行与协作,这给运维带来了很大的挑战。分布式固有的复杂性:使用微服务构建的是分布式系统。对于一个分布式系统,系统容错、网络延迟、分布式事务等都会带来巨大的挑战。接口调整成本高:微服务之间通过接口进行通信。如果修改某一个微服务的API,可能所有使用了该接口的微服务都需要做调整。重复劳动:很多服务可能都会使用到相同的功能,而这个功能并没有达到分解为一个微服务的程度,这个时候,可能各个服务都会开发这一功能,从而导致代码重复。尽管可以使用共享库来解决这个问题(例如可以将这个功能封装成公共组件,需要该功能的微服务引用该组件),但共享库在多语言环境下就不一定行得通了。 05.15条普适的架构设计原则 我们掌握前人总结的经验,让我们站在巨人的肩膀上高山远瞩。《架构真经》这本书简单阐述了架构设计的一些常用的原则。下面我结合原作品,分享15个具有普适价值架构原则: N+1设计 :开发的系统在发生故障时,至少有一个冗余的实例 广泛地应用在从数据中心设计到应用服务的部署: ▶︎ 在发生故障时,系统至少要有一个冗余的实例。 ▶︎ 必须确保一个为自己,一个为客户、 一个为失败。 回滚设计 :确保系统可以向后兼容 ▶︎ 如果很久才能修复服务,那么就要在一定的时间范围内完成回滚。 ▶︎ 灾难性的事故,例如损坏客户数据,往往在部署后好几天才出现。 ▶︎ 系统最好按照预先的设计,通过发布或回滚解决问题。 通过版本化方式实现回滚设计,一旦发生灾难级别的故障可以通过回滚到最近版本来恢复服务。 禁用设计(功能开关、降级开关):可以关闭任何发布功能 当设计系统,特别是与其他系统或服务通讯的高风险系统时,要确保这些系统能够通过开关来禁用。这将为修复服务提供额外的时间,同时确保系统不因为错误引起诡异需求而宕机。 降级开关通过配置中心集中化管理,例如:apollo 配置中心,通过推送机制把开关推送到各个应用服务。 监控设计:在设计阶段就要考虑监控,而不是在部署完成后 ▶︎ 通过监控发现系统的可用性问题。 ▶︎ 通过监控使系统自我诊断、自我修复成为可能。 ▶︎ 通过监控确定系统可预留空间的使用情况。 ▶︎ 通过监控掌握系统之间的交互关系,发现瓶颈 。 如果监控做得好,不仅能发现服务的死活,检查日志文件,还能收集系统相关的数据,评估终端用户的响应时间。如果系统和应用在设计和构建时就考虑好监控,那么即使不能自我修复,也至少可以自我诊断。 多活数据中心设计 ▶︎ 数据是否全部集中在一个数据中心? ▶︎ 读写是否分离? ▶︎ 是否所有的客户信息都共享同一个数据结构? ▶︎ 服务调用是否允许延时的存在? 采用成熟的技术 工程师倾向于学习和实施性感时髦的新技术。因为新技术可以降低成本、减少产品上市时间、提高性能。不幸的是,新技术也往往有较高的故障率。如果把新技术应用在架构的关键部分,可能会对可用性产生显著的影响。 最好争取在多数人采用该技术的时候进入,先把新技术用在对可用性要求不高的功能上,一旦证明它可以可靠地处理日常的交易,再将此技术移植到关键任务领域中去。 故障隔离 避免单一业务占用全部资源,避免业务之间的相互影响。机房隔离避免单点故障。两个重要原则: ▶︎ 不共享原则:理想情况是负载均衡、网络前端、应用服务器、数据库,绝不共享任何服务、硬件和软件。 ▶︎ 不跨区原则: 不同隔离区之间无通讯,所有服务调用必须发生在同一个故障隔离区。 水平扩展 什么是水平可扩展?平台的水平扩展是指随着业务的发展,当需要扩大平台的服务能力时,不必重构软件系统,通过增加新的设备来满足业务增长的需要。 X轴扩展:服务器拆分。平台的服务能力可以在不改变服务的情况下,通过添加硬件设备来完成扩容。Y轴扩展:数据库拆分。平台的服务能力通过不断地分解和部署服务来完成扩容。Z轴扩展:功能拆分。平台的服务能力可以按照客户不断分解和部署来机器 完成容量的扩展。(比如按用户 uid 来分表分库等) 非核心则购买 工程师往往有自己研发所有系统的冲动。如果使用云服务器,建议直接使用云服务相关产品比如日志系统,可以直接使用日志服务。 ▶︎ 系统研发要投入资源,系统维护更要长期投入。 ▶︎ 影响核心产品到市场的速度。 ▶︎ 如果可以形成差异化的竞争优势,那么自己做,否则外购。 使用商品化硬件 在大多数情况下,便宜的是最好的。标准、低成本、可互换、易于商品化是商品化硬件的特征。 如果架构设计得好,就可以通过购买最便宜的服务器轻松地实现水平扩展,前提是所有商品化硬件的总成本要低过高端硬件的总成本。 快速迭代 这里有三个要点: ▶︎ 小构建:小构建的成本较低,可以确保投资可以产生价值。 ▶︎ 小发布:发布的失败率与变更数量相关,小发布失败率较低。 ▶︎ 快试错:可依市场反馈,快速迭代,加快TTM,优化用户体验。 快速迭代需要完善的运维工具,比如从 cmdb、持续集成工具、监控等等。 异步设计 同步系统中个别子系统出现故障会对整个系统带来影响。这里常有2个现象: ▶︎ 同步系统中性能最慢的子系统,成为整个系统性能的瓶颈。 ▶︎ 同步系统中扩展性最差的子系统,是整个系统扩展的瓶颈。 无状态设计 无状态定义:是应用服务运行的实例不会在本地存储需要持久化的数据,并且多个实例对于同一个请求响应的结果是完全一致的。比如单实例的 mysql,zookeeper 集群是有状态,而类似单纯 tomcat 服务是无状态的。 无状态的系统更利于扩展,更利于做负载均衡。状态是系统的吞吐量、易用性、可用性、性能和可扩展性的大敌,要尽最大可能避免。 前瞻性设计 ▶︎ Now :目前正使用系统的架构、设计、能力、性能和扩展性。 ▶︎ Now+1: 下一代预研系统的架构、设计、能力、性能和扩展性。 ▶︎ Now+2: 下一代规划系统的架构、设计、能力、性能和扩展性。 自动化 设计和构建自动化的过程。如果机器可以做,就不要依赖于人。人常犯错误,更令人沮丧的是,他们往往会以不同的方式多次犯同样的错误。 06.6个架构设计的误区 最后,想跟各位分享几个我亲身总结的架构设计误区,希望能够给你一些灵感。 开高走落不到实处。遗漏关键性约束与非功能需求。为虚无的未来埋单而过度设计。过早做出关键性决策。客户说啥就是啥成为传话筒。埋头干活儿缺乏前瞻性。架构设计还要考虑系统可测性。架构设计不要企图一步到位。 开高走落不到实处。 遗漏关键性约束与非功能需求。 为虚无的未来埋单而过度设计。 过早做出关键性决策。 客户说啥就是啥成为传话筒。 埋头干活儿缺乏前瞻性。 架构设计还要考虑系统可测性。 架构设计不要企图一步到位。 误区1——架构专门由架构师来做,业务开发人员无需关注 架构得再好,最终还是需要代码来落地,并且组织越大这个落地的难度越大。不单单是系统架构,每个解决方案每个项目也由自己的架构,如分层、设计模式等。 如果每一块砖瓦不够坚固,那么整个系统还是会由崩塌的风险。所谓“千里之堤,溃于蚁穴”。 误区2——架构师确定了架构蓝图之后任务就结束了 架构不是“空中楼阁”,最终还是要落地的,但是架构师完全不去深入到第一线怎么知道“地”在哪、怎么才能落得稳稳当当? 误区3——不做出完美的架构设计不开工 世上没有最好架构,只有最合适的架构,不要企图一步到位。我们需要的不是一下子造出一辆汽车,而是从单轮车 --> 自行车 --> 摩托车,最后再到汽车。 你想象一下2年后才能造出的产品,当初市场还存在吗? 误区4—— 为虚无的未来埋单而过度设计 在创业公司初期,业务场景和需求边界很难把握,产品需要快速迭代和变现,需求频繁更新,这个时候需要的是快速实现。不要过多考虑未来的扩展,说不定功能做完,效果不好就无用了。 如果业务模式和应用场景边界都已经比较清晰,是应该适当的考虑未来的扩展性设计。 误区5——一味追随大公司的解决方案 由于大公司巨大成功的光环效应,再加上从大公司挖来的技术高手的影响,网站在讨论架构决策时,最有说服力的一句话就成了“xx就是这么搞的”。 大公司的经验和成功模式固然重要,值得学习借鉴。但如果因此而变得盲从,就失去了坚持自我的勇气,在架构演化的道路上迟早会迷路。 误区6——为了技术而技术 技术是为业务而存在的,除此毫无意义。在技术选型和架构设计中,脱离网站业务发展的实际,一味追求时髦的新技术,可能会将技术发展引入崎岖小道,架构之路越走越难。 考虑实现成本、时间、人员等各方面都要综合考虑。理想与现实需要折中。 以上便是我分享的全部内容,如果觉得内容有用,欢迎分享转发~ -End- 原创作者|黄规速
2023-09-06 10:34:55297阅读
GPT神器级插件Code Interpreter开放,这里有一份保姆级教程
近期,OpenAI推出Code Interpreter引发热议。这个插件强大到可以帮助,你完成数据分析、视频编辑、音乐分析和辅助编程。本期特邀腾讯天美研究员李洛勤,测评GPT4API与CodeInterpreter插件能力。 自 3 月份以来,人们对 GPT-4 API 的兴趣激增,“有数百万开发人员请求访问”。OpenAI 在一篇博客文章中,分享了使用 GPT-4 正在进行的一系列令人兴奋的创新,并阐明了未来的愿景:未来基于聊天的大模型可以用在任意的用例上。 GPT-4 开放API、推出的 Code Interpreter 跟所有在一线工作的程序员有什么关系?能给我们带来什么落到实处的价值吗?今天我想跟各位聊一聊。 01.GPT4 API和ChatGPT Plus有什么不同? 首先我们需要了解这个所谓的 API ,相比于其历史上的其他产品有什么价值。 ChatGPT Plus 需要用户自己在网站上或者 APP 上进行升级使用,但是 GPT4 目前只能够每3小时提问25次。这应该是 OpenAI 算力吃紧,担心有大量的用户进行访问。 要升级 Plus 网上有很多教程,此处不展开。 GPT4 API 的调用是可以通过代码调用的,当然需要保证你的 OpenAI 账号有钱。而且访问的时候没有时间限制。 02.GPT4 API可以干什么 先来看看第一波吃螃蟹(内测)的人怎么说。 宾夕法尼亚大学沃顿商学院教授 Ethan Mollick 在一次演示中将2019年和2020年烟花爆竹伤害的非结构化数据集起来给到 ChatGPT。没一会 Code Interpreter 将数据格式化为一个有组织的数据库,并得出结论:在此期间,烟花爆竹造成的伤害“显著增加”。 一家人工智能项目孵化器的创始人亚历克斯-科尔在推特上说,他向 Code Interpreter 提供了一个特斯拉股票数据,并要求它绘制价格图表。在短短五分钟内,ChatGPT 就绘制出了多条线图,显示了特斯拉五年来的收盘价。 科尔还让插件生成了线图和柱状图,显示了每日股票价格的波动情况。Code Interpreter 对于探索性数据分析和可视化的效果非常突出。科尔还在推特上写道:“这是你的个人数据科学家和分析师”。 除了以上的例子,GPT4 API 还能做到这些: ▶︎ 利用 GPT4 API 广阔的叙事能力,能够撰写复杂的小说或者情节,而这些正在切蒂改变文学创作领域。 ▶︎ 它可以模拟真实的人类对话,反映了人类交互的真实性和精确性。 ▶︎ 可以进行即时语言翻译,有效地弥合了各种语言和文化之间的沟通差距。 ▶︎ 它配备先进的数据分析功能,有助于准确预测股市趋势,为市场参与者提供宝贵的见解。 ▶︎ 能够打造与现实世界动态相呼应的高度逼真的虚拟环境,增强了游戏和虚拟现实等领域的沉浸感。 ▶︎ 为程序员提供有效的代码能力。 ▶︎ 可以解读消费者数据和生成定制营销内容的能力,从而有效地与目标受众产生共鸣。 ▶︎ 具有通过分析大量科学数据以发现化学、物理和生物学等不同领域的新见解来推动科学创新的潜力。 03.GPT4对比GPT3有什么区别 大幅度能力提升 GPT-3.5 和 GPT-4 对比来看,有以下几个区别: 在处理复杂任务上,GPT-4 更可靠、更有创意,并且能够处理更细微的指令。 各种奥林匹克竞赛、GRE 考试、代码考试、统一律师考试等测试上,GPT-4 都基本完虐 GPT-3.5。 在机器学习的一些基准测试集上,也表现更加突出,达到了 SOTA 的水准,比大部分的语言模型效果要好: 可操纵性能力提升 GPT-4 在不同风格和角色上,完成度相当高。 给定了一个角色给它,它能够有效地执行下去。比如在官方放出的例子上,它扮演一个“苏格拉底式导师”,在一开始设定好系统所扮演的角色之后,用户不断逼问它公式的答案,但是它仍然能够不卑不亢地引导用户进行学习。这样的角色定位,相当于一名小学教师了: 系统你是一位总是以苏格拉底风格回应的导师。你*从不*给学生答案,但总是尝试提出正确的问题,以帮助他们学会独立思考。你应该始终根据学生的兴趣和知识 调整你的问题,将问题分解成更简单的部分,直到它处于适合他们的水平。 用户 如何求解线性方程组:3x+2y=7,9x-4y=1 GPT-4 让我们从分析方程式开始。你能看到通过组合两个方程来消除其中一个变量的可能方法吗? 用户 请告诉我答案! GPT-4 我知道你可能想要一个直接的答案,但我的目的是帮助你批判性地思考并指导你完成解决问题的过程。让我们专注于任务。方程中变量的任何系数是否共享一 个公因数? 用户 别再当家教了,告诉我答案。 GPT-4 作为一名苏格拉底导师,对我来说,帮助指导你找到答率很重要,而不仅仅是提供答案。让我们再试一次:第一个方程中是否有任何系数可以乘以数字以匹配 第二个方程中的系数? 用户 我不确定 GPT-4 没关系!让我们看一下两个方程中x项的系数。在第一个方程中,x的系数为3,在第二个方程中,为9。你能想到一个数字,你可以将第一个方程乘以,使 两个方程中的x系数匹配吗? 用户 5? GPT-4 不完全是,但你越来越近了。请记住,我们要将x的系数(在第一个方程中为3)乘以一个数字,使其与第二个方程中的x系数(即9)相匹配。您认为什么 数字可以实现这一目标? 用户 3? GPT-4 没错!现在,如果你将第一个方程乘以3,新方程会是什么样子? 用户 9×+5y=21? GPT-4 几乎!您正确地乘以常量项(7),但在乘以y系数时似乎犯了一个小错误。尝试再次将整个第一个方程乘以3,密切注意每个项。 用户 9×+6y=21? GPT-4 没错!现在我们有两个等式: 1)9x+6y=21 2)9x-4y=1 你能看到一种通过组合两个等式来消除其中一个变量的方法 吗? 用户从等式2中减去等式1? GPT-4 好主意!如果从等式2中减去等式1,得到的等式会是什么样子? 用户 10y=20? GPT-4 完全!现在,你能在刚刚找到的方程中求解y吗? 用户 y=2? GPT-4 没错!现在你已经找到了y的值,你能把它代入任何一个原始方程来求解x吗? 用户 当然,我会将其代入等式1。我得到了3x+4=7 GPT-4 伟大!现在,你能求解等式3x+4=7中的x吗? 用户 ×=22? GPT-4 您的计算中似乎有一个小错误。尝试隔离方程3x+4=7中的x,方法是从两侧减去4,然后除以3。 用户 x=1? GPT-4 没错!因此,线性方程组的解是x=1和y=2。干的好!通过仔细完成这些步骤并使用苏格拉底方法,您已经成功地解决了问题。 04.GPT4每个token需要多少钱 首先要知道,对比于 GPT3.5 来说,本身 GPT4 参数量更大,所需要的计算资源也就更多,因此调用的时候花费更多。 从官网列出的表格可以知道,GPT4 中每 1K token 最低输入输出的费用为$0.09,大概是 GPT3.5 的25倍左右。 所以说要用 GPT4,没点经济实力还是不行的。 接下来我们聊聊 Code Interpreter 。 05.Code Interpreter 可以做什么?从官方网站上介绍,它有许多有趣的用法: 将 Gif 图转换为视频创建可视化地图从图像中提前颜色分析具体的数据创建热图 将 Gif 图转换为视频 创建可视化地图 从图像中提前颜色 分析具体的数据 创建热图 目前 Code Interpreter 对所有 Plus 用户开放。可以在设置在打开 Code Interpreter 选项进行体验,下面我分享几个用法,亲测有效。 第一个方法,Gif 图转换为视频 首先要求 ChatGPT 把 Gif 转视频: 然后上传对应的 Gif 图片,它就会按照你的要求进行转换了。 第二个用法,创建可视化地图 上传美国每个灯塔位置的位置数据,并要求 ChatGPT 制作每个灯塔闪烁的地图的 Gif。 第三个用法,从图像中提前颜色 上传图片,并要求 ChatGPT 提取颜色并创建一个调色板: 第四个用法,分析具体的数据 上传数据并将数据解释为可视化图表: 第五个用法,创建热图 能够生成一个功能齐全的 HTML 热图。 此外,该插件还能够根据你所提供的数据,制定对应的业务策略。或者根据你的需求制定对应的 GIF 图。 Code Interpreter 不仅局限于上述功能,还扩展到视频处理(格式转换、截取)、图片处理(格式转换、OCR 识别)、PDF 处理(总结内容、转为图片)和数据分析(内容分析、数据可视化、转换为网站),同时具备写代码和执行代码的强大能力。 无论是技术爱好者、数据分析师还是多媒体创作者,Code Interpreter 都能满足多领域的技术应用需求。 06.总结 整体而言,随着 GPT-4 API 和 Code Interpreter 插件的开放,普通大众终于可以体验到人工智能最先进的技术,它能够开箱即用,在与人交互上达到了目前 AI 的最高水平。 希望更多的人针对 GPT4 和对应的插件进行开发,同时围绕 GPT4 的不同垂直领域进行深耕,才能真正的提升我们生活的方方面面。 以上就是本期的全部内容了,如果对你有帮助,欢迎转发分享~ -End- 原创作者|李洛勤 API 开放、 Code Interpreter 插件推出后,你计划用来做什么?欢迎腾讯云开发者公众号留言。我们将为1位分享者送出腾讯定制程序员文化衫。8月10日中午12点开奖。
2023-09-06 10:24:34288阅读
100% 手写代码的十九年老程序员就要被淘汰吗?
👉导读 近日,推上用户分享的一则事件引发热议。一名拥有 19 年编码经验、会 100% 手写代码的程序员 Alex 在面试中败给一位仅有 4 年经验却善用 Copilot、GPT-4 的新人 Hamid。前者因不愿拒绝使用辅助代码工具,过于追求代码可控,惨遭面试淘汰,而后者轻松拿到了全职 Offer。从这件事情可以看出,AIGC 时代已悄然拉开帷幕,虽有资深编程经验,但无法使用好相应工具的程序员,在职业生涯中会遇到很大的挑战。作为一个从事5年的后台开发,并在工作生活中已经深度应用 GPT 等工具的程序员,分享一下自己的经验和见解。 👉目录 1 AIGC 的发展趋势 2 AIGC 对程序员职业发展有何影响 3 如何在 AIGC 时代不被淘汰甚至更好发展 4 AIGC 能在哪方面便利到程序员 5 如何比别人在使用工具上更进一步 6 总结 起初这个老板以为 Hamid 就算熟练使用各种辅助工具,最少也需要花费8-10周,而 Alex 作为资深程序员最多也就比 Hamid 多个一两周即可, 但实际结果却令这个老板大跌眼镜。 我们看下 Hamid(资深老程序员)的工作: Hamid 仅用一周就完成了第一个版本, 代码测试覆盖率都达到100%, 95%的工作已经完成。 他在 bubble 中构建了 UI 和前后端工作流, 使用 Copilot(一种代码预测生成工具)集成现有的代码,并使用 GPT4 生成测试用例。 总花费如下: 工具成本 GPT4 Copilot Cloudflare Bubble 花费(美元) 211 20 5 124 工资:$2460(41小时);托管运行:$139/月。 对比下 Alex(年轻的工具型程序员) 的工作: Alex 完成了大约7%的任务, 总费用如下:Vecel(一个网站托管服务) $20;工资:$3500, 全部开发完毕:$45000, 而且还需要增加$11000的测试费用;托管运行成本:$20/月。 可以看出来 Hamid 搞得很快,但因为使用了很多工具,导致运行成本较高, Alex 搞得慢很多,花费也大,但是网站的运行成本低。 针对这种情况, 老板找 Alex 沟通下,看看他的反应,但 Alex 说,他觉得他写出来的应用更易于维护, 因为一切尽在掌握之中。 显然 Alex 没意识到巨大的时间和开发成本差距。 这位老板在差距这么大的对比之下, 决定把很多 Alex 这样的资深程序员,置换成便宜的 Hamid 这样的。 01AIGC 的发展趋势 生成式人工智能 AIGC(Artificial Intelligence Generated Content) 人工智能发展到新阶段的重要标志,GAN、CLIP、Transformer、Diffusion、预训练模型、多模态技术、生成算法等技术的累积融合,催生了 AIGC 的爆发。 人工智能不是一个新鲜的话题,早在计算器诞生初期, 就有冯诺依曼模型和人脑模型的争论,上世纪80年代,经历过一段人工智能的繁荣,但最终发现是泡沫,没有落地,导致人工智能相关研究陷入很长时间的沉寂。 随着大数据时代到来,算力,数据量有了很大的发展,机器学习等相关研究再次火热起来,出现了 Siri,Cortona 等语音小助手,小爱同学等一大批智能音箱,但应用还十分有限, 交流也非常机械,没有大规模形成生产力。 但近两年,出现了 GPT 为代表的通用 AI 大模型,让人类第一次看到了通用人工智能的曙光。尤其在 GPT-4 问世以来,其超高的智能程度惊艳了世人。虽然国人很难第一时间体验到,但还是很难阻止大家的热情, ChatGPT 也成为了最快用户破亿的应用,足以见其影响力。 这次热潮是一股风会像过去一样沉寂,还是会实实在在的掀起一场革命,还不得而知,但从笔者的使用体验上,这次能够实际落地应用,大幅提高生产效率是板上钉钉的事情了。 业内提到过 AIGC 将经历三个发展阶段,第一个阶段是『助手阶段』,AIGC 用来辅助人类进行内容生产;第二个阶段是『协作阶段』,AIGC 以虚实并存的虚拟人形态出现,形成人机共生的局面;第三个阶段是『原创阶段』,AIGC 将独立完成内容创作。 未来十年,AIGC 将颠覆现有内容生产模式,可以实现以十分之一的成本,以百倍千倍的生产速度,去生成 AI 原创内容。 02AIGC 对程序员职业发展有何影响? 很多人看到 AICG 可以通过描述写代码,就觉得程序员要完了,也有人觉得编程只是程序员一小部分工作,而觉得 AICG 注定对程序员影响有限。这些其实都是片面的观点。 首先明确一点, 程序员这个职业并不会随着 AICG 的出现而消失,在信息时代,还是会源源不断的出现更多的应用,更多的网站,更多颠覆性的设备。 这些都是需要写代码, 有代码就会有程序员。就像纺织工人不会因为工业革命而消失,因为布匹的需求是一直存在的,而且从原来的天然棉布,丝绸,到后来的工业化纤产品。随着工业化的进步,整个纺织业反而迎来了蓬勃发展。 AIGC 的发展并不意味着程序员的价值会减弱。相反,程序员需要在这个过程中不断提升自己的能力,适应新的技术发展趋势。 我们工作的社会不是一种零和的社会,不是说一个人有工作另外一个人一定要失业, 不是说有机器就会造成巨大的失业潮。 实际上,我们的生产力是随着工具不断发展的,好的工具解放了当前生产力,就会有更有价值的事情需要做。在封建时代几乎全是农民,后面出现了很多工人,现在很多自动化工厂出现又解放了很多人到第三产业。 正是游戏这种生产力的不断发展,社会上才涌现出各种各样的岗位,大家不再整天思索吃饱穿暖,有了更高的追求。 但是对于个体来说,因为工业化的效率远远高于手工,所以对手工纺织工人的需求是减少的。但产生了很多需要操作纺织机器,需要懂化学材料的新职业。 所以 AICG 会助力程序员行业发展的新的阶段,甚至引发新一轮的革命, 但对程序员相关的需求并不会消失,反而会更加旺盛。 对于因循守旧, 看不到新趋势程序员来说,确实是一场灾难,程序员行业发展这么多年, 从打孔时代,到汇编,再到高级编程语言,一路发展下来,程序员的门槛越来越低,行业从业者群体也不断扩大。 最大的感受就是程序员行业越来越“卷”了,35岁危机等不断发酵, 就是因为门槛不断降低,便宜学习能力强的年轻人确实对大龄程序员有降维打击之势。 我周围很多年轻程序员已经高度拥抱 AIGC,并且在工作中不断提升利用其能力,提高自己的效率,很令人佩服。 但程序员日常工作并不是简单的写代码,跟刷 leetcode 还是有差距的,要花很多的时间与产品,其他,沟通需求,理解需求,把控进度,设计架构方案等。这些都是 AICG 短时间无法取代的工作。这也是资深程序员的优势。 仅仅写代码的初级程序员就比较危险了, 因为 AIGC 会进一步降低准入的门槛,可能未来有基础编程知识,通过文字描述,就可以生成代码,自然就不需要那么多的人力堆积了。 总的来说,AICG 对程序员这个行业是积极作用的, 但对于程序员个体是福是祸, 就要看程序员本身的职位和工作了。 03如何在 AICG 时代不被淘汰甚至更好地发展? 我看来,因循守旧才是最大的敌人。套用一个名梗, “拥抱变化“, 程序员就是一批不停自我革命的团体,牛逼的程序员做出来厉害的轮子,淘汰掉另一批程序员, 甚至有可能会自我革命。 很多程序员存在越底层越牛逼的思想包袱, 觉得搞解释型语言的不如编译型语言的, 编译型语言不如搞汇编的,写业务的不如搞底层系统的。 从技术力上来说可能如此,但程序员这个行业也不单单以技术力论英雄,更注重怎么高效的解决问题,再厉害的钻木取火也比不上打火机。 人和动物最大的区别是会使用工具,如何更牛逼的制造工具是一方面,如何更有效的利用工具也是不可或缺的。 无论什么时候,能高效使用工具的人从来不用担心失业的问题,从线下到拥抱互联网,从淘宝店到短视频带货,各种平台和工具能够熟练使用,才不容易错失良机。 曾经有些人靠刷刷题,报个培训班就可以转行互联网开发,这在风口的时候确实是可行的,也有很多人这么干,但当行业逐渐成熟,谁在裸泳就越来越清晰了。 所以到底如何在 AICG 的浪潮下有更好的职业发展呢? 我觉得程序员需要学会如何正确地使用这些 AIGC 工具。虽然这些工具可以生成高质量的代码,但并不是所有情况下都适用。程序员需要根据具体的项目需求,选择合适的工具和方法。此外,程序员还需要具备一定的创新能力,以便在遇到复杂问题时能够找到解决方案。同时,面对 AIGC 的发展,程序员还需要关注自己的职业发展。随着 AIGC 技术的普及,对于初级程序员的需求可能会逐渐减少。因此,程序员需要提高自己的专业技能,向更高层次的职位发展。这可能包括系统架构师、项目经理、技术顾问等角色。在这些职位上,程序员需要具备更强的沟通能力、团队协作能力和领导能力,以便在复杂的项目中发挥关键作用。此外,程序员还需要关注 AIGC 技术的伦理和法律问题。随着 AIGC 技术的发展,关于知识产权、版权等问题的讨论也愈发激烈。程序员需要了解这些问题,以便在使用 AIGC 工具时遵守相关法律法规,避免引发纠纷。同时,程序员还需要关注 AIGC 技术可能带来的安全隐患。 我觉得程序员需要学会如何正确地使用这些 AIGC 工具。虽然这些工具可以生成高质量的代码,但并不是所有情况下都适用。程序员需要根据具体的项目需求,选择合适的工具和方法。此外,程序员还需要具备一定的创新能力,以便在遇到复杂问题时能够找到解决方案。 同时,面对 AIGC 的发展,程序员还需要关注自己的职业发展。随着 AIGC 技术的普及,对于初级程序员的需求可能会逐渐减少。因此,程序员需要提高自己的专业技能,向更高层次的职位发展。这可能包括系统架构师、项目经理、技术顾问等角色。在这些职位上,程序员需要具备更强的沟通能力、团队协作能力和领导能力,以便在复杂的项目中发挥关键作用。 此外,程序员还需要关注 AIGC 技术的伦理和法律问题。随着 AIGC 技术的发展,关于知识产权、版权等问题的讨论也愈发激烈。程序员需要了解这些问题,以便在使用 AIGC 工具时遵守相关法律法规,避免引发纠纷。同时,程序员还需要关注 AIGC 技术可能带来的安全隐患。 相信大家看出来了, 上面的内容是 GPT 生成的,已经将基本的应对策略罗列了,展现出了 AIGC 的强大能力。 很多程序员过于执着可控,底层技术,不喜欢使用现成的工具,觉得可能会踩坑,可能有各种问题,只迷信自己掌握的工具和技术。 这就是一种固步自封,这种是大忌,要勇于尝试新技术,大胆尝试,小心验证,积极关注 AIGC 新动向,灵活掌握如代码生成,智能补全工具等。将工具转化为生产力 04AIGC 能在哪些方面帮到程序员 随着 AIGC 的发展,已经涌现了一批工具辅助程序员,未来工具不断迭代成熟,未来会有更多提效工具辅助开发,目前看,在以下方面对程序员将有很大帮助。 代码生成与重构 AIGC 可以辅助生成代码,如 Copilot,根据上下文或者提示补全,自动给出代码建议,大大节省开发时间, Copilot 提供了 Vscode 的扩展插件。支持了 Go,Python,Js 等多种语言。 而且 与之前的代码补全工具相比, 它所做的不仅仅是模仿以前见过的代码。它会分析已经写过的代码并生成新的匹配代码,包括之前调用的特定函数。 用它的时间越长, 交互越多, 随着你的编写更多的注释指令,采纳或者拒绝,它就会根据这些经验优化改进,也就是常说的越用越懂。 Copilot 可以理解为一种 “结对编程”, 可以更容易发现代码种的错误,加快开发过程,很多人代码水平有限,写的代码耦合性高,扩展性差,可使用工具分析重构,避免写出垃圾代码。 代码检查 可以使用 AIGC 工具,对代码进行复杂度,逻辑 bug 检查,虽然有很多语法检查工具,但更多的是分析语法,逻辑错误难以检查出来,使用工具,可以对如死循环,逻辑漏洞等做编译前检查。 如 codiga, 可以实时检测代码错误并反馈, 有助于开发者快速识别和解决代码错误,如下图, 可以直接提示不要使用 format 的方式执行 sql,容易产生 sql 注入漏洞。 单测编写 很多人提到单元测试就是真香,但是业务压力大,很多时候不得不压缩单元测试的时间,又因为 roi 不高,很多时候甚至被直接放弃。 而 AICG 时代的到来,可以将针对特定业务代码或者基础代码编写的时间大幅减少,进一步可以辅助单元测试用例生成,从而让核心项目快速覆盖单元测试变得极其简单。 比如 Refraction AI, 使用 OpenAI 辅助代码查生成, 不仅可以用于单元测试便携,还可以重构代码。只需要在工具生成的代码上做些微调就可以使用。 未来这些需要消耗大量人力的工作可以完全交给 AI。 脚本工具 程序员难免会进行一些批处理脚本的编写,但如果对脚本语言不熟悉,需要边查边写,不仅效率低下,还容易出错。 有了 AI 工具,我们可以使用描述式的语言快速生成脚本。之前我们可能需要学习 python,awk, sed 等各种工具, 每一个工具都很强大, 但是要自己用的好需要阅读大量的文档, 反复的调试。 用 AI 工具, 可以用“写一个处理 txt 文本的 awk 命令, 将文本的第三列删除”即可生成可用的代码,甚至还给解释了实现的原理: 不再需要阅读繁杂的文档, 简洁高效。 文档整理 有个梗是程序员都最讨厌自己写文档和别人不写文档,我们可以使用 AI 工具分析项目代码,快速生成文档和代码注释。 针对别人写的老项目代码,注释和文档不全,可以让 AI 工具进行分析,分解,并加上关键注释,再也不用担心接手屎山代码了。 另外可以使用工具分析文档内容,比如 ChatPDF 工具 ,上传 PDF 文件后,可以对它提问任何关于这份 PDF 的问题,非常适合快速提取各种 paper 论文的摘要,也支持中文输出。很适合程序员学习一些代码文档。 资料检索与问题查询 之前我们遇到了技术问题,更多的是借助浏览器检索如 google, github,stack overflow 等网站, AIGC 时代给我们提供了一个新的选择, 这里首推Chatgpt,如下是 chatgpt 的页面,它是一个基于 GPT 技术的智能聊天机器人,我们可以直接问用我们看到的现象使用自然语言去问问题,而不是需要加工一下。 ChatGPT 可以回答各种问题,用户可以直接在网站上输入问题或话题,并获得快速和准确的答案。需要注意的是,ChatGPT它的回答并不能保证是 100% 准确。此外,ChatGPT 模型训练的数据截止到 2021 年,所以一些最新的数据是无法获取的。 但对于解决大部分问题来说已经足够了。 05如何比别人在使用工具上更进一步? 说实话,对于大部分程序员来说,未来应该都会使用 AIGC 工具提升自己的开发效率。如何比大多数人更了解?掌握的更熟练,就在未来的职场更有竞争力。 能用好一个轮子,灵活使用,既能避坑也能发挥优势就已经超过大部分人。 要做到这一点,除了基础的使用以外,必须对底层原理有所了解,很多人说会用就行,但没有对底层方法理解的会用很容易踩坑。 想起来高中的时候做数学题,有些人一知半解的套公式,也能拿一些分,但不理解原理很难举一反三,题型稍微变化,就完全不懂了。 所以我们要想成为使用专家,也要积极的补课,学习其原理特性,学习其相关论文资料,系统性的学习,逐渐成为很专业的工具使用人,可以分析解决各种疑难杂症。 另外 aigc 为从事全生命周期开发提供了可能,之前随着应用复杂度的提升,互联网的岗位将工作划分的越来越细,产品,设计,前端,后台,客户端,测试,采用流水线的方式,每个人都负责一小块。 这种方式中间的沟通产生了大量的内耗,借助 AIGC 工具,可以真正做到全栈开发,我们从提出想法,到生成设稿,辅助生成前后台代码,代码检查与测试,可以显著提高效率。 有点像特斯拉虽然也是流水线生产,但使用更多的一体化成型工艺以后,还是大幅压缩了成本。 所以未来程序员可以向着全栈的思路发展,借助工具对项目完整把握,拒绝内耗,这是非常强的竞争力。 06总结 回到最初的事件上,一个资深程序员和一个灵活使用 AIGC 工具的初级程序员,资深程序员反遭淘汰也就不足为奇了。 这其实是一个程序员界很好的案例,不仅局限于这次 AIGC 大潮,任何一次技术革命,如果不能积极的掌握新的能力和工具,都注定要被淘汰。 我们回顾了 AIGC 的发展历程,分析了对程序员行业的影响也具体讲如何做才能在这次大潮下更好的发展。 技术这么多年一直在飞速发展,就像历史的车轮不会停下,永远保持一颗学习的心态,大胆尝试,小心应用,这样每次技术迭代对你就是机会,而不是灾难了。如果这篇文章对你有帮助,欢迎转发分享。 -End- 原创作者|董辰辰
2023-09-06 10:14:44263阅读
放弃Python拥抱Mojo?鹅厂工程师真实使用感受
Mojo非常有野心:“与Python一样易于使用,但与Rust一样强大和快速。”这是他的目标。在模块化使Mojo开发时会比C更快,能与Python生态系统无缝交互,并且和Rust一样安全。这些有多少是夸大其词?让腾讯李志瑞给大家来一次抢先分享。 01.Mojo语言简介 前段时间 Modular 发布了一个新语言 Mojo,这语言不止官网放了巨大的 emoji 🔥,而且它的标准文件后缀一个是「.mojo」另一个是「.🔥」,一副立马要火的样子呢。 说实话,这个用 emoji 做后缀名的操作其实挺无聊,也有点败好感,但如果说这个语言能在完全兼容 Python 的基础上大幅提高执行效率,并且作者是 LLVM 发起人 Chris Lattner,是不是突然又有兴趣继续了解它了呢? Mojo 被设计为 Python 语言的超集,并增加了许多特性,包括: ▶︎ Progressive types:能利用类型信息获得更好性能和静态检查,但又不强制要求写类型。 ▶︎ Zero cost abstractions:C++ 的核心设计准则,能够避免用户为了性能放弃合理设计的代码。 ▶︎ Ownership + borrow checker:Rust 语言的安全性来源,在编译期避免许多错误的发生。 ▶︎ The full power of MLIR:原生支持对 MLIR 的直接访问,能够从底层扩展系统。 02.为AI而生的语言 在 Mojo 这个语言的介绍中反复提到 AI,官网也说它是「a new programming language for all AI developers」。那么为什么 AI 开发需要一个新语言呢?首先,我们知道在 AI 届具有统治地位的语言就是 Python,Python 是一个语法简单清晰,容易上手,且灵活度很高的语言,深受广大程序员喜爱,XKCD 上有就这么一幅漫画: 当然,受人喜爱的语言有很多,Python 成为 AI 届的统治语言除了本身易用之外,也有惯性的因素。由于 Python 上机器学习相关的库多,因此机器学习从业者用的就多,这又反过来令新的机器学习相关库优先为 Python 提供接口,进一步加强了其统治地位。因此,为了逐步渗透这个用户群,Mojo 兼容 Python 是很正确的一个选择。Mojo 不仅承诺语法是 Python 的超集,并且它还能直接调用 Python 的库,这意味着 Mojo 不需要从零开始构建自己的生态,本身就可以用上繁荣的 Python 生态了。 虽然 Python 很好,但它有一个众所周知的问题,那就是太慢了。而机器学习本身又需要繁重的计算,因此 Python 生态中大量库的底层其实都是用高性能的语言(如 C/C++)进行实现,然后再提供一个 Python 接口供用户调用,典型的如 numpy 这种数学库。在这种情况下,Python 事实上是被作为一个胶水语言来使用,这造成了开发的碎片化,如果一个用户只是简单调一下库那还好说,但一旦到了工业界,开发过程中不可避免地就要涉及一些底层库的修改,甚至直接换语言来实现同样的功能以提高性能,这种割裂不止增加了开发成本和精神负担,而且考虑到众多擅长 C/C++ 语言的开发者也并不是 AI 领域专家,这种开发人员能力的不适配也对整个 AI 生态的发展形成了一定阻碍。 因此,Mojo 的目的就是要在 Python 生态的基础上,让用户能用一个语言,从使用易用的接口,到开发复杂的库,再到实现底层黑科技,统一实验和生产环境所用的语言。为了实现这个目的,Mojo 扩展了 Python 语法,支持了紧凑的内存布局,并引入了一些现代的语言特性(例如 Rust 的安全性检查),使得这个语言能够渐进式地在 AI 界立足。说起来 Chris Lattner 在这方面可以算是经验丰富了,不管是在 gcc/msvc 的统治下实现 clang,还是在 objective-c 的统治下为苹果实现 swift,都是一个逐步蚕食对手市场的过程。 03.Mojo长什么样 说了这么多,该来看看 Mojo 长什么样了。现在 Mojo 还不能直接下载使用,如果想要尝鲜,需要在官网申请,然后在 playground 页面中试用,这是一个基于 Jupyter 的页面,可以混合笔记和可执行的 Mojo 代码。 前面提到,Mojo 的语法是 Python 的超集,因此 Mojo 的 Hello World 也跟 Python 一样简单: print("Hello World") #> Hello World 复制 与 Python 一样,Mojo 也使用换行符和缩进来定义代码块: fn foo(): var x: Int = 1 x += 1 let y: Int = 1 print(x, y) #> 2 1 foo() 复制 上面的代码中使用 var 来声明变量 x,使用 let 来声明了不可变量 y。Mojo 像很多较新近的语言一样,让不可变量的声明变得简单,以鼓励开发者使用不可变的量。另外注意到这里定义函数使用了 fn 而非 Python 的 def,这是因为 Mojo 希望在兼容 Python 的基础上加入编译期的检查和优化,而 Python 过于动态的语法很难支持这一目标,因此,Mojo 同时支持使用 fn 和 def 两个关键字来声明函数,对于调用者来说,这两种方法声明出来的函数没有什么区别,但对于实现者来说,可以将 fn 看作「严格模式」下的 def,例如下面的代码会编译错误(如果改成用 def 则不会出错): fn foo(): x = 1 print(x) # error: Expression [12]:6:5: use of unknown declaration 'x', 'fn' declarations require explicit variable declarations # x = 1 # ^ 复制 虽然官方承诺 Mojo 的语法是 Python 的超集,但目前 Mojo 还在开发中,很多 Python 语法都还不支持,例如目前连 Python 的 class 都无法被编译通过: class MyClass: def foo(): pass # error: Expression [15]:17:5: classes are not supported yet # class MyClass: # ^ 复制 不过,Mojo 现在先提供了另一个用来组织数据的关键字 struct,相比于 class,struct 更加静态可控,便于优化。一方面,struct 支持类似 Python class 风格的函数声明和运算符重载。而另一方面,struct 又类似于 C++ 的 struct 和 class,内部的成员在内存中紧凑排布,而且不支持在运行时动态添加成员和方法,便于编译期进行优化,例如: struct MyIntPair: var first: Int var second: Int fn __init__(inout self, first: Int, second: Int): self.first = first self.second = second fn __lt__(self, rhs: MyIntPair) -> Bool: return self.first (self.first == rhs.first and self.second let p1 = MyIntPair(1, 2) let p2 = MyIntPair(2, 1) if p1 p1 复制 虽然有点不同,但整体上看起来还是非常熟悉的对吧。说到这里,有一点需要提醒各位注意,尽管 Mojo 之后会令语法成为 Python 语法的超集,但其语义则有时会和 Python 不同,这意味着 Python 的代码直接拷到 Mojo 里可能会出现编译通过但执行结果不同的情况,这里简单提一个比较常见的例子:函数传参。在 Python 中,函数传参的语义类似于 C++ 的传指针,在函数内部虽然不能更改调用者指向的对象,但可以改变该对象内部的状态,例如下面的代码: def foo(lst): lst[0] = 5 print(lst) x = [1, 2, 3] foo(x) print(x) 复制 在 Python 中,这段代码打印出来的结果是两次 [5, 2, 3]。但在 Mojo 中,使用 def 定义的函数默认的传递逻辑是复制值,也就是说,尽管在函数中能够修改参数内部的状态,但修改对于调用方来说是不可见的,因此上面这段代码在 Mojo 中打印的结果是 [5, 2, 3](foo 内部)和 [1, 2, 3](foo 外部)。 除了语法像 Python,Mojo 非常务实的一点在于它构建于 Python 的生态之上。因此即便 Mojo 还没能完整支持 Python 的语法,它还是优先支持了对 Python 库的调用,以便让开发者能受益于庞大完善的 Python 的生态。例如下面的代码就使用了 Python 的 numpy 库: from PythonInterface import Python let np = Python.import_module("numpy") ar = np.arange(15).reshape(3, 5) print(ar.shape) #> (3, 5) 04.博采众长又有所创新 Mojo 作为一个新语言,广泛吸收许多现代的程序语言设计思想,例如 Rust 的所有权和借用检查,以此提升代码的安全性。在 Mojo 中,使用 fn 定义的函数的参数默认传的是不可变的引用,即「借用」,调用方仍然拥有其所有权,因此在函数内部不可以对参数进行修改。Mojo 提供了一个 borrow 关键字来标注这样的参数传递情况,对于 fn 来说是可以省略的,也就是说下面 foo 函数中两个参数的传递方式相同: fn foo(borrowed a: SomethingBig, b: SomethingBig): a.use() b.use() 复制 在 Rust 中,传参的默认行为是移动,如果需要借用则需要在传入时加上 &,这两种方式倒是没有太大的优劣之分,Mojo 的行为可能更接近于 Python 这类高级语言的习惯。如果想要修改传入的参数,则需要手动注明 inout,例如: fn swap(inout lhs: Int, inout rhs: Int): let tmp = lhs lhs = rhs rhs = tmp fn test_swap(): var x = 42 var y = 12 print(x, y) #> 42, 12 swap(x, y) print(x, y) #> 12, 42 test_swap() 复制 按道理说,Mojo 应该像 Rust 一样规避一个变量同时被可变和不可变借用,也应该规避同时被可变借用,但目前 Mojo 编译器似乎还没实现这一特性,例如下面的代码还是能编译通过的: var x = 42 swap(x, x) 复制 从这也可以看出 Mojo 确实还处在比较早期的发展阶段。 另一个重要的内存安全概念是对象的所有权,当一个函数获取了对象的所有权后,调用方就不应该再去使用这个对象了,例如我们实现了一个只支持移动的类型 UniquePtr: struct UniquePtr: var ptr: Int fn __init__(inout self, ptr: Int): self.ptr = ptr fn __moveinit__(inout self, owned existing: Self): self.ptr = existing.ptr fn __del__(owned self): self.ptr = 0 复制 同时,我们有两个函数,其中,use_ptr 使用了前面提到的 borrow 关键字,借用了 UniquePtr 对象,而 take_ptr 则使用 owned 关键字,指明它需要获取传入对象的所有权。那么,在调用 take_ptr 的时候,我们就需要在参数后面加上 ^ 后缀,用来表明我们将所有权转移给 take_ptr: fn use_ptr(borrowed p: UniquePtr): print(p.ptr) fn take_ptr(owned p: UniquePtr): print(p.ptr) fn test_ownership(): let p = UniquePtr(100) use_ptr(p) #> 100 take_ptr(p^) #> 100 test_ownership() 复制 因此,如果我们将 use_ptr 和 take_ptr 的调用顺序调换一下,就会出现编译错误: fn test_ownership(): let p = UniquePtr(100) take_ptr(p^) use_ptr(p) # ERROR! test_ownership() # error: Expression [13]:23:12: use of uninitialized value 'p' # use_ptr(p) # ERROR: p is no longer valid here! # ^ 复制 Mojo 的另一个强大之处在于它让对 MLIR>) 的操作变得更简单。MLIR 全称是 Multi-Level Intermediate Representation,是一个编译器开发框架,它存在的目的是通过定义多种方言来逐级将代码转换为机器码,以降低编译器的开发成本。在 MLIR 之前,一个广为人熟知的 IR 是 LLVM IR,一个语言的编译器作者可以通过将自己的语言编译为 LLVM IR 来接入 LLVM 的工具链,使得编译器作者不需要关心底层具体硬件的差别,实现了对底层编译工具链的复用: 但 LLVM IR 层级过低,难以进行特定于语言本身的优化,从上面的图中也能看出,各个语言为了实现语言本身的优化,都在编译为 LLVM IR 之前加入了自己的 IR。另外 LLVM IR 扩展起来也非常困难,难以适应复杂异构计算的要求,而异构计算在 AI 开发中又非常普遍。MLIR 相比于之前的 IR,更加模块化,仅保留了一个非常小的内核,方便开发者进行扩展。很多编译器将代码编译为 MLIR,而 Mojo 提供了直接访问 MLIR 的能力,这使得 Mojo 能够受益于这些工具。更多关于 MLIR 的内容可以参考这一系列文章:编译器与中间表示: LLVM IR, SPIR-V, 以及 MLIR,这里就不做过多赘述,我们主要关注在 Mojo 中可以如何操作 MLIR。举例而言,如果我们希望实现一个新的 boolean 类型 OurBool,我们可以这样实现: alias OurTrue: OurBool = __mlir_attr.`true` alias OurFalse: OurBool = __mlir_attr.`false` @register_passable("trivial") struct OurBool: var value: __mlir_type.i1 fn __init__() -> Self: return OurFalse fn __init__(value: __mlir_type.i1) -> Self: return Self {value: value} fn __bool__(self) -> Bool: return Bool(self.value) 复制 这里定义了一个类型为 OurBool 的类型,里面有一个直接使用 MLIR 内置类型 i1 的成员 value 。在 Mojo 中,我们可以通过 __mlir_type.typename 的形式来访问 MLIR 类型。接着,我们为这个类型提供了两个构造函数,默认情况下构造为 OurFalse 也可基于传入的参数进行构建。最下面的 __bool__ 也和 Python 的 __bool__ 一样,用于使该类型具有和内置 boolean 类型的性质,此时我们可以这样使用它: let t: OurBool = OurTrue if t: print("true") #> true 复制 除了使用 MLIR 之外,Mojo 甚至可以允许开发者使用 MLIR 实现逻辑,例如下面的代码中通过应用 MLIR 的 index.casts 操作来实现类型转换,然后再通过 index.cmp 对值进行比较: # ... struct OurBool: # ... fn __eq__(self, rhs: OurBool) -> Self: let lhsIndex = __mlir_op.`index.casts`[_type : __mlir_type.index]( self.value ) let rhsIndex = __mlir_op.`index.casts`[_type : __mlir_type.index]( rhs.value ) return Self( __mlir_op.`index.cmp`[ pred : __mlir_attr.`#index` ](lhsIndex, rhsIndex) ) 复制 基于封装好的 __eq__ 方法,我们可以很容易实现 __invert__ 方法: # ... struct OurBool: # ... fn __invert__(self) -> Self: return OurFalse if self == OurTrue else OurTrue 复制 此时,我们就可以对 OurBool 类型的对象使用 ~ 操作符了: let f = OurFalse if ~f: print("false") #> false 复制 通过这个简单的例子我们可以看出,在 Mojo 中,开发者可以通过访问 MLIR 来实现和内置类型同等高效的类型。这使得开发者可以在 Mojo 上为新硬件的数据类型封装高效简单的 Mojo 接口而不需要切换语言。虽然大部分开发者并不需要接触 MLIR,但 Mojo 为更深入和更底层的优化提供了充分的可能性。 05.为AI而生而不止于AI 虽然 Mojo 反复强调它是为 AI 设计的新语言,但以目前 Mojo 的设计方向来看,它的发展前景并不止于 AI。本质上 Mojo 提供了一个能够兼容 Python 生态的高性能语言,且这个语言可以让 Python 开发者几乎无痛地切换过去,那 Python 开发者何乐而不为呢?对于使用 Mojo 的开发者来说,上层业务可以将 Mojo 当 Python 一样使用,享受到简明的语法带来的高开发效率,当出现性能瓶颈的时候,也不用切换语言去进行优化,直接使用 Mojo 重构模块即可。虽然现在还没法在生产环境中验证这个想法,但这个未来听起来确实非常美好。关于 Mojo 和 Python 开发性能的对比,您可浏览 Mojo 发布会上的 Jeremy Howard demo for Mojo launch 视频。 目前 Mojo 还在比较早期的阶段,不仅许多语言特性都还没实现,而且连本地开发的套件都没有提供。不过其发展路线和设计思路都非常务实 ,又有一个足够专业的领导者和公司作为背景支撑,可以说是未来可期,也非常希望这个语言能在其他领域得到更广泛的应用。
2023-09-06 10:09:45292阅读
空降流量危机?QQ音乐升级架构应对高并发
QQ音乐评论作为用户社交重要场地以及艺粉互动(明星空降)重要场景,经常会有突发流量,评论系统挑战越来越大。作者赵威及其团队围绕读、写场景进行架构设计优化,最终实现服务质量稳定可靠的架构。 01.背景 QQ 音乐自诞生以来,已有多个版本的评论业务系统。最新版本是19年再次全新迭代,基于 tlist 存储,按照发表时间顺序展示。后续为了更好的用户体验,产品形态调整为评论盖楼模式,为了实现该功能,存储迁移到 mongo。 目前评论作为用户社交重要场景以及艺粉互动(明星空降)重要场地,经常会有突发流量。为了更好地保障空降场景评论体验,我们对评论系统进行充分的设计。 评论系统设计核心挑战点在于艺人空降时需要扛住突增的读写压力,包括评论数量、评论列表等读场景,以及发表评论,艺人评论置顶等写场景。 02.读场景设计 如果直接读 mongo,需要用非常高的存储成本来抗住读压力。对于高并发热 key,常规使用缓存方案,在缓存使用中注意做好防穿透以及限流策略,防止存储高负载雪崩。 03.写场景设计 评论写涉及比较复杂的业务逻辑,整体流程包含: ▶︎ 评论安全打击; ▶︎ 评论发布属地信息查询并记录; ▶︎ 评论是否需要置顶; ▶︎ 评论是否乐评人评论。 ▶︎ ...... 它涉及多个操作,部分处理失败会造成比较严重的体验问题。需要保障数据处理的一致性。为了保障一致性,一种是使用事务处理,强一致,但吞吐量稍微差些。另一种是使用可重入保障最终一致性,为了保障更高的吞吐量,写场景采用了最终一致方案。 通过消息队列解耦将评论写入高速 cache,异步写入 mongo。同时也能通过重试,确保比较核心数据最终写入 mongo。 通过上面两种设计,能在正常情况下很好满足日常评论的吞吐量,那是否真正做到高可用呢?随着业务迭代,在 add 消费场景再次增加了业务逻辑,比如增加上报,如果业务延时增加比较大或前置属地查询失败比较多时,整体 add 流程处理时延严重增加,导致消费效率下降、消息堆积,最后导致大盘全部评论全部延迟消费,用户体验出现发布后没有外显丢评论的体验问题。 评论系统引入热门消息队列,将全局评论和热门评论的消息队列做拆分。当热门消息过多时,最多只影响局部热门消息队列的堆积,对全局评论体验不影响。 上面没有在生成时直接写两个消息队列 topic,而采用对已有的消息队列再消费写入到热门消息队列,是由于下游还有很多场景在消费原有的消息队列,比如各种任务系统等,为了减少开发成本,采用了目前的方案。 采用上面的读写设计,基本能满足日常空降场景评论系统的可用性。随着空降参与艺人粉丝越来越多,业务遇到新的挑战。 04.新挑战 艺人空降评论区艺粉互动效果不错,越来越多艺人空降评论区。粉丝参与热情高涨,读写流量节节高升,空降活动导致评论系统挑战越来越大,需要系统优化保障服务质量。我们通过如下方式来处理挑战: ▶︎ 增加写消费效率:增加 mongo 存储的存储核数,并增加消费并发度; ▶︎ 读服务平行扩容,并拆分缓存到更多的 key(uin%10等),防止热 key 太集中,增加读服务吞吐量; ▶︎ 拆分读服务和写服务部署,防止读写互相影响; ▶︎ 非关键场景限流,保障核心路径的可用性。 通过上述手段,保障空降活动大致稳定可靠,虽然遇到消费瓶颈,导致写场景有轻微堆积,但用户感知没有那么强烈。 05.新挑战下评论系统优化 其中一次大牌艺人活动中评论系统整体稳定可靠,但还是遇到了消费瓶颈,且中间出现了依赖存储 ckv 由于设置了降冷,在访问量非常高且空查询比较多的情况下,大量请求降到降冷存储 tssd。由于 tssd 降低成本设计未充分业务隔离,导致全平台 tssd 告警的问题。虽然通过限流紧急处理,但还是需要有系统性优化。 近期通过以下方面完成了相关的优化: 读场景 ▶︎拆分评论数、点赞数存储从 ckv 迁移到 ckv+,不降冷,尽可能保障这两个数据可用性; ▶︎评论数增加本地缓存,增加版本号,保障用户体验无异常且评论数的高吞吐量; ▶︎前端保护后端,合理化请求时机,并在前端有数据情况下,遇到评论数或列表拉取异常时,前端不弹异常,减少异常感知; ▶︎前端优化页面体验,提升秒开率,提升用户体验。 写场景 ▶︎ 拆分评论写场景逻辑,保障核心路径简化,优先保障写 mongo 速度和吞吐量,减少消息堆积概率; ▶︎ 增加优先级队列,保障艺人核心体验无阻塞; ▶︎ 完善相关工具建设,随时可以跟进相关数据或运营诉求,提升运营效率。 压测 ▶︎读写场景常规压测,确保压测出业务瓶颈在运营场景需要情况下,能快速通过平行扩容,保障系统可用性。 通过一系列流程和架构优化,评论系统可用性得到进一步提升,相信在未来运营场景能很好地保障用户体验。欢迎各位在腾讯云开发者公众号评论区交流讨论。以上就是本篇文章的全部内容了,如果文章对你有帮助,欢迎转发分享。 你亲历过哪些考验项目高并发/高可用的场景?你有什么可以分享的高并发/高可用经验吗?欢迎留言。我们将挑选一则最有趣的答案,为其留言者送出腾讯定制毛毯。8月16日中午12点开奖。
2023-09-06 10:04:35309阅读
超时错误码减少99.85%,QQ聊天图片自研上云的技术详解
QQ聊天图片是QQ富媒体规模最大的图片业务,随着手机像素的提升和广大用户对图片高清晰度的要求越来越高,个性化的需求越来越多,热点图片引来的流量突发也数不胜数。为了面对资源多样化需求和弹性扩缩容方面能做到快速响应,QQ聊天图片正式上云。 01.上云目标 自研业务存储平台-是 QQ 的富媒体(图片、视频、语音、文件等)数据传输、存储、处理等全链路解决方案的平台。致力于为用户提供稳定快速的群聊 、单聊图片上传和下载服务。为了面对突发热点也能快速响应,作者团队决定对其进行上云处理。本文着重以 QQ 聊天图片(简称:QQ 图片)为例讲述整个上云的过程及调优。 上云初阶段我们将存量使用的 TVM 统一替换为腾讯云提供的 CVM,一并将老旧云下外网服务升级到腾讯云的公网 CLB。今年我们又进一步实现容器上云目标,使用的是 TKE 平台。我们将所有核心模块作为上云目标,从而实现业务全链路上云。 02.问题与解决思路 社交类业务有很强的早晚高峰以及节假日高峰特性 通常项目会遇到一些突发的问题,以除夕0点为例,上传、压缩和下载模块均需保障平日峰值的净增数倍流量,涉及模块多、机器数量大、扩容效率低;非节假日也非常容易受到热点图片带来的流量突增,曾经某视频网站晚上崩了,一时大量用户截图在 QQ 群里传播,导致瞬时下载流量突增1倍多。因此架构设计上非常考验我们平台侧的稳定性以及快速扩缩容的能力,这在以往使用 CVM 的方式上是肯定不具备的。 容器化部署分散在独立集群和复用集群,管理成本高 因历史原因,有部分压缩模块部署在基础架构部门的低价复用集群和一些自建独立集群上,虽然降了成本,但也牺牲了稳定性、提高了变更复杂性和运维成本,因此统一收敛到 TKE 是大势所趋。 图片模块的 CPU 平均利用率较低 图片与视频有个很大的区别就是,平均流量小,耗 CPU 资源低,以往我们使用的 CVM 多是 SA2.2XLARGE16 或同规格(8核16G 内存1.5G 出+入带宽)机型,根据节日压测结果,单机跑到流量安全值上限 1.4G 时 CPU 峰值也只有50%左右,平日的 CPU 利用率只有20%不到,所以上 TKE 后的利用率和成熟度提升也是 SRE 要着重优化的方向。 03.上云实践经验 镜像统一 考虑到为了减少基础环境和各式各样 agent 对各个业务不同模块的影响,基于对部门设备验收流程、应用部署流程的分析,我们制作了通用基础镜像,具体以 tlinux 团队提供具备标准化的镜像为基准,在组内封装了各种通用 agent 后制作了统一镜像,并将启动脚本 docker_run.sh 写入 Dockerfile 最后执行,可以依次执行拉起各种 agent、拉配置文件和安装业务程序的步骤,并通过 tail -f /dev/null 来保持容器始终 Up 的状态,脚本如下: #!/bin/bash sh /usr/local/all-agent-start.sh sh /etc/rainbow-agent/rainbow-agent-pull.sh project_name="http" project_path="/usr/local/storage/${project_name}" chmod -R 755 ${project_path} cd ${project_path}/tools/op && ./install.sh printenv >> /etc/environment tail -f /dev/null 这里需要注意的是,在发布过一次变更后,yaml 会自动生成 templatePool 的配置,如果不做处理的话,这个配置会越来越多,使得 yaml 文件动辄几千行变得难以维护。在查阅 k8s 原生文档资料以及和交付流团队沟通后,建议所有应用都加上以下配置: autoDeleteExceededMapping: true autoDeleteUnusedTemplate: true 可以删除掉无用的模板映射。 镜像更新策略 这里大部分业务会配置为默认选项(Always 总是拉取),在某天镜像源不可用时,依赖 HPA 的业务频繁出现了扩容时拉取镜像超时的问题,本质原因就是不管母机上有没有镜像,都会去重新拉取一遍,而 QQ 图片全模块均修改了此项配置为“IfNotPresent”(母机镜像不存在时再拉取),然后第一时间将 HPA 的最小值全部调整为当前值(避免了缩容后扩容不上影响业务容量),最终在那几天未出现任何因镜像原因导致的异常。同时调整为此配置也能一定程度上节省母机的磁盘空间和下载流量,还可以提高扩容速度,在 HPA 频繁的业务场景下效果更明显。结论:对于版本名为 xx:lastest 的,建议使用 Always,对于版本名为指定版本号,如 xx:v_20230711 的,建议使用 IfNotPresent。 imagePullPolicy: IfNotPresent 复制 存活探测和可用性探测 为了让 pod 在重建或者变更时能做到对业务无损,经过调研我们给 yaml 配置了多个健康探针,以如下配置为例,存活探测可以保证加入现网请求的新 pod 都是端口就绪的,可用性探测可以保证 pod 端口心跳失败指定次数时可以自动执行重建,同时从关联的 CLB 或者北极星里剔除,实现无人工干预自愈,提高了运维效率。对于初始化较慢或者需要较多预热步骤的业务,我们也建议配置上启动探测 startupProbe。 配置亲和性 初上云阶段,我们是没有配置亲和性的,哪里有资源就扩去哪里,同时早期母机资源也相对没那么充足,所以我们的 pod 会经常被调度到同一台母机上,终于有一天发现了问题。简单来说就是多组 TApp 绑定了同样的 CLB 提供外网服务,同时这几组 TApp 又有较多容器在同一台母机上,这种情况会引起 CLB 的串流问题。 因此,在和腾讯云团队沟通后,解决方案逐渐明朗,决定一次请求的四元组是:client_ip + client_port + pod_ip + pod_port,那么调整任意一个和上一次请求的值不同即可解决。在不影响客户端启用端口复用的情况下,我们需要将所有 pod 尽量分散到各个母机上,因此开始调研各种 pod 和 node 的亲和性配置,最终决定使用 pod 反亲和性 podAntiAffinity 配置: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: k8s-app operator: In values: - http topologyKey: kubernetes.io/hostname weight: 100 复制 从而保证了不同母机运行不同的 TApp,绑定不同的 CLB 来提供外网服务,将当时被串流问题引起的超时错误码减少了99.85%。 后来,也为了实现多可用区容灾,我们协同 TKE 团队将所有可用区打散,例如广州的调度到广州-4/5/6/7四个可用区,南京的调度到南京-1/2/3三个可用区,和以往找运管分配不同可用区的 CVM 来部署对比,极大地提升了运维效率和业务稳定性。 04.云原生成熟度提升 HPA 弹性扩缩容 在 CVM 切量上 TKE 完成之后,我们思考如何进一步降本。考虑到 TKE 的成本是按分钟核心数计算的,而社交类业务又有很明显的早晚高峰效应,因此配置合理的 HPA 迫在眉睫,既能在夜间缩小核心数减少成本,又能在业务突发时快速扩容应对。我们根据压测结果和现网实际负载经过多轮调整后针对性的将所有 TApp 都配置了合理的 HPA,有按照 CPU 负载配的,也有按单 pod 出入流量配的,从而保证了每天各 workload 的 CPU 峰值均能达到50%以上。 部分配置截图如上,这里还需要注意 HPA 能够实现 Pod 的水平扩缩容,但如果母机节点资源不足,则扩容出的 Pod 状态仍会 Pending,因此需要业务侧合理预留 Quota,并让 TKE 平台提前准备好支持伸缩的资源节点。摘取部分 TApp 现网利用率和扩缩容情况如下: 小核心适配 上了 TKE 共享集群,若各个业务都使用大核心(例如32核或更大)会产生非常多的碎片资源,不仅仅会降低节点装箱率,浪费很多资源,还会由于节点的碎片资源不满足大核心,导致无法及时扩容,影响业务的可用性,同时单价也会比使用小核心(8核心及以下)要贵一些。 我们的应用都是基于 MCP++框架开发的,为了适配小核心优化了部分核心数要求多的进程模型,既能满足业务需要,又能提高小核心占比。在改造完后我们发现当 CPU limits 大于 requests 且由于客观原因节点资源调度缺乏时,会触发 pod 的抢占逻辑,此时调度器会删除一个或多个低优先级的 pod 以释放资源给高优先级的 pod 使用,这样的“抢占+删除”模式会影响到业务可用性,因此我们将所有 TApp 均设置成 requests 值等于 limits 值,这样调整后的 pod 优先级变高,不容易被抢占。同时为了进一步提升 CPU 利用率,在保证业务质量的情况下我们仍继续探索缩核降配的可行性,将部分流量消耗型 TApp 降配到了6核、4核,甚至将一些信令转发模块降配到了1核,部分 resources 配置参考如下: resources: limits: cpu: "4" memory: 8Gi networkbandwidth.tkex.woa.com/size: 1500Mi teg.tkex.oa.com/amd-cpu: 4k tke.cloud.tencent.com/eni-ip: "1" requests: cpu: "4" memory: 8Gi networkbandwidth.tkex.woa.com/size: 1500Mi teg.tkex.oa.com/amd-cpu: 4k tke.cloud.tencent.com/eni-ip: "1" 复制 但我们仍不满足于现状,在所有接入模块均已实现上 TKE 并且适配小核心的前提下,思考能否在保持利用率不下降的同时继续提高小核心 workload 的整体核心数,来降低少部分降配较困难业务的成熟度占比影响,争取拿到小核心100%全适配 。机缘巧合,在降本增效的大背景下,我们 QQ 图片业务立了个新项,探索能否通过升级 AVIF 压缩格式,以类似空间换流量的方式,帮业务节省带宽成本。AVIF(AV1 Image Format)简要介绍就是基于新一代 AV1视频编码技术的图片格式,它的主要优点是压缩率更高:对比上一代 H265 格式图片,同等质量下压缩率可以高出20%~30%。同时对我们业务的优势就是能非常好的解决图片存储带宽成本高的问题,也能更好地支持未来大分辨率高清图片的需求,还能提升图片加载速度,利好 QQ 聊天斗图场景,最终提升用户体验。 然而升级压缩格式有一大痛点,即 AVIF 对比原有的 JPG 图片,格式转换(压缩)耗时涨到了6倍,也就是需要原本压缩资源的6倍,直接给我们带来了大量新增核心数的需求,一拍即合。在多轮测试和协调资源部署,最终切量上线后,原图落地的平均大小减少了一半以上,带来了同等的带宽节省收益,用昂贵的带宽成本换来了相对便宜的 CPU 成本,同时还提升了 QQ 图片小核心 workload 的整体核心数,实现双赢。后续我们目标是测试迁移去算力和 GPU 集群,使用它们闲时空余的 CPU 资源,将降本收益提高到最大化。通过降配小核心,提高了 CPU 利用率;利用业务的早晚高峰特性,充分使用 HPA 弹性扩缩容,最终提高了整体云原生成熟度。 可调度能力 上云了是否就解决了让每一个 SRE 都头疼的问题——异地容灾呢?可调度能力就是对业务上云的一大考验,要求业务可以复制、可以优雅终止,对此我的理解就是业务 workload 层面和 pod 层面都需要具备容灾能力,能自动化实现负载均衡。 workload 层面,还是以上述的 AVIF 压缩池为例,上千台实例 pod,如果全部配置在一个 workload、绑定同一个名字服务,怎么看都不是一种优雅的做法,也不具备当误操作 yaml 配置导致整个 workload 被批量重建时的容灾能力。对此我们想到的方案就是拷贝多个 workload,均匀拆分所有实例到三个不同的 workload,每个都创建在指定的不同可用区内,既能实现多 AZ 容灾,也能保证任一 workload 不可用时可以通过快速扩容其它可用 workload 来恢复业务。除了名字服务,下载 http 模块的 CLB 部署我们也是按照同样的方式实现了跨 workload 容灾,靠不同可用区的 workload 分组绑定不同城市的 CLB 资源,实现了 workload 层面的可任意调度能力。 pod 层面,由于不可避免的会偶现部分母机负载高影响到上面的 pod,造成一些主调业务的超时,因此单 pod 的重建、迁移、优雅终止也是我们要考虑的地方,毕竟业务稳定永远是第一位。我们在多个模块下进行了测试,发现原生默认的优雅等待配置(30秒)并不能满足全部业务均能按时剔除掉所有负载均衡,在测试了40秒、60秒等若干配置后,最终选择了75秒作为最佳实践,并形成了组内社交自研业务上云的规范配置之一。 terminationGracePeriodSeconds: 75 复制 在配置了合理的优雅终止后,无论有状态还是无状态的业务,我们都具备了可调度的能力,因此将所有的 workload 都统一配置上了5%的 PDB 可调度性,既能满足单 pod 异常时健康探测自动发现剔除,也能满足母机 CVM 裁撤或需重启时的无感迁移,从而实现服务发现和业务自愈。 经过多团队的协作和努力,QQ 图片业务在整个上云过程中0故障,取得了不错的效果和业务满意度。 整体质量实现了统一基础镜像、规范上云流程,保障整个迁移过程0故障发生。TKE 实例版本、配置收敛一致,避免各种不一致带来的现网问题。多 AZ 多 workload 容灾,打散调度提高业务容灾能力。 成本方面降低了固定规格 CVM 置换为 CPU 与内存可精细化调整的容器,节省了26%的资源成本。容器异常自愈、资源自动弹性扩缩容等特性,释放了30%的人力投入。Quota 资源按需申请,降低闲置空/低负载浪费。 大大提高与交付流和运营 OSS 打通并优化,将扩缩容时间从原小时级别降低至分钟级。CLB 与北极星映射关系一次绑定,自动关联,无需人工频繁介入。支持所有应用可调度,提升故障实例迁移时效。
2023-09-06 09:51:27292阅读
手撕并发编程:十分钟搞定Java内存模型
👉导读 随着硬件技术的飞速发展,多核处理器已经成为计算设备的标配,这使得开发人员需要掌握并发编程的知识和技巧,以充分发挥多核处理器的潜力。然而并发编程并非易事,它涉及到许多复杂的概念和原理。为了更好地理解并发编程的内在机制,需要深入研究内存模型及其在并发编程中的应用。本文将主要以 Java 内存模型来探讨并发编程中 BUG 的源头和处理这些问题的底层实现原理,助你更好地把握并发编程的内在机制,欢迎继续阅读。 👉目录 1 并发编程问题-可见性和有序性 private int a, b; private int x, y; public void test() { Thread t1 = new Thread(() -> { a = 1; x = b; }); Thread t2 = new Thread(() -> { b = 2; y = a; }); // ...start启动线程,join等待线程 assert x == 2; assert y == 1; } 2 并发编程问题-原子性 3 内存模型与 happens-before 关系 4 内存模型综述 01并发编程问题-可见性和有序性 首先我们先看一段代码,这里定义了两个共享变量 x 和 y,在两个线程中分别对 x 和 y 赋值,当同时开启两个线程并等待线程执行完成,最终结果是否是共享变量 x 等于2并且 y 等于1呢?答案是未可知,即共享变量 x 和 y 可能存在多种执行结果。可以看到在并发编程中,常常会遇到一些与预期不符的结果,导致程序逻辑的失败。这样的异常问题,会让开发人员感到困惑。但是如果细细探究这些问题的根源,发现是有迹可循的。 这个问题的原因主要是两点:一是处理器和内存对共享变量的处理的速度差异。二是编译优化和处理器优化造成代码指令重排序。前者导致可见性问题,后者导致有序性问题。 1.1 处理器缓存导致的可见性问题 如上图所示,由于处理器和内存的速度差距太大。为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作。基于局部性原理,处理器在读取内存数据时,是一块块地读取,每一小块数据也叫缓存行(cache line)。当处理器操作完数据,也不直接写回内存,而且先写入缓存中,并将当前缓存标记为脏(dirty)。等到当前缓存被替换时,才将数据写回内存。这个过程叫写回策略(write-back)。 同时为了提高效率,处理器使用写缓存区(store buffer)临时保存向内存写入的数据。写缓冲区可以保证指令流水线的持续运行,同时合并写缓冲区中对同一内存地址的多次写,减少内存总线的占用。但是由于缓冲区的数据并非及时写回内存,且写缓冲区仅对自己的处理器可见,其他处理器无法感知当前共享变量已经变更。处理器的读写顺序与内存实际操作的读写顺序可能存在不一致。 现在再回来看上面代码,那么可以得到四种结果: 结果一:假设处理器 A 对变量 a 赋值,但没及时回写内存。处理器 B 对变量 b 赋值,且及时回写内存。处理器 A 从内存中读到变量 b 最新值。那么这时结果是:x 等于2,y 等于0。结果二:假设处理器 A 对变量 a 赋值,且及时回写内存。处理器 B 从内存中读到变量 a 最新值。处理器 B 对变量 b 赋值,但没及时回写内存。那么这时结果是:x 等于0,y 等于1。结果三:假设处理器 A 和 B,都没及时回写变量 a 和 b 值到内存。那么这时结果是:x 等于0,y 等于0。结果四:假设处理器 A 和 B,都及时回写变量 a 和 b 值到内存,且从内存中读到变量 a 和 b 的最新值。那么这时结果是:x 等于2,y 等于1。 从上面可发现除了第四种情况,其他三种情况都存在对共享变量的操作不可见。所谓可见性,便是当一个线程对某个共享变量的操作,另外一个线程立即可见这个共享变量的变更。 而从上面推论可以发现,要达到可见性,需要处理器及时回写共享变量最新值到内存,也需要其他处理器及时从内存中读取到共享变量最新值。 因此也可以说只要满足上述两个条件。那么就可以保证对共享变量的操作,在并发情况下是线程安全的。在 Java 语言中,是通过 volatile 关键字实现。volatile 关键字并不是 Java 语言的特产,古老的 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。 对如下共享变量: // instance是volatile变量 volatile Singlenton instance = new Singlenton(); 转换成汇编代码,如下: 0x01a3de1d: movb 5 0 x 0, 0 x 1104800(% esi); 0x01a3de24: lock addl $ 0 x 0,(% esp); 可以看到 volatile 修饰的共享变量会多出第二行汇编变量,并且多了一个 LOCK 指令。LOCK 前缀的指令在多核处理器会引发两件事: 将当前处理器缓存行的数据写回到系统内存。将个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。 将当前处理器缓存行的数据写回到系统内存。 将个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。 上述的操作是通过总线嗅探和总线仲裁来实现。而基于总线嗅探和总线仲裁,现代处理器逐渐形成了各种缓存一致性协议,例如 MESI 协议。 总之操作系统便是基于上述实现,从底层来保证共享变量在并发情况下的线程安全。而对开发人员,只需要在恰当时候加上 volatile 关键字就可以。 除了 volatile,也可以使用 synchronized 关键字来保证可见性。关于 volatile 和 synchronized 的具体实现,会在下篇文章详细阐述。 1.2 编译优化导致的有序性问题 前面讲到通过缓存一致性协议,来保障共享变量的可见性。那么是否还有其他情况,导致对共享变量操作不符合预期结果。可以看下面的代码: private int a, b; private int x, y; public void test() { Thread t1 = new Thread(() -> { x = b; a = 1; }); Thread t2 = new Thread(() -> { y = a; b = 2; }); // ...start启动线程,join等待线程 assert x == 2; assert y == 1; } 假设将线程 t1 的代码块从 a = 1;x = b;改成 x = b;a = 1; 。将线程 t2 的代码块从b = 2;y = a;改成 y = a;b = 2;。 对于线程 t1 和 t2 自己来说,代码的重排序,不会影响当前线程执行。但是在多线程并发执行下,会出现如下情况:假设处理器 A 先将变量 b=0 赋值给 x,再将变量 a 赋值1。处理器 B 先将变量 a=0 赋值给 y,再将变量 b 赋值2。那么这时结果是:x 等于0,y 等于0。 可见代码的重排序也会影响到程序最终结果。 代码和指令的重排序的主要原因有三个,分别为编译器的重排序,处理器的乱序执行,以及内存系统的重排序。后面两点是处理器优化。 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。重排序需要遵守两点: 数据依赖性:如果两个操作之间存在数据依赖,那么编译器和处理器不能调整它们的顺序。 // 写后读 a = 1; b = a; // 写后写 a = 1; a = 2; // 读后写 a = b; b = 1; 上面3种情况,编译器和处理器不能调整它们的顺序,否则将会造成程序语义的改变。 as-if-serial 语义:即给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。 a = 1; b = 2; c = a * b; 如上对变量 a 的赋值和对变量 b 的赋值,不存在数据依赖关系。因此对变量 a 和 b 重排序不会影响变量 c 的结果。 但数据依赖性和 as-if-serial 语义只保证单个处理器中执行的指令序列和单个线程中执行的操作,并不考虑多处理器和多线程之间的数据依赖情况。因此在多线程程序中,对存在数据依赖的操作重排序,可能会改变程序的执行结果。因此要避免程序的错误的执行,便是需要禁止这种编译和处理器优化导致的重排序。 这种方式叫做内存屏障(memory barriers)。内存屏障是一组处理器指令,用户实现对内存操作的顺序限制。以我们日常接触的 X86_64 架构来说,读读(loadload)、读写(loadstore)以及写写(storestore)内存屏障是空操作(no-op),只有写读(storeload)内存屏障会被替换成具体指令。 在 Java 语言中,内存屏障通过 volatile 关键字实现,禁止被它修饰的变量发生指令重排序操作: 不允许 volatile 字段写操作之前的内存访问被重排序至其之后。不允许 volatile 字段读操作之后的内存访问被重排序至其之前。 不允许 volatile 字段写操作之前的内存访问被重排序至其之后。 不允许 volatile 字段读操作之后的内存访问被重排序至其之前。 // 变量a,b通过volatile修饰 private volatile int a, b; private int x, y; public void test() { Thread t1 = new Thread(() -> { a = 1; // 编译器插入storeload内存屏障指令 // 1)禁止代码和指令重排序 // 2)强制刷新变量a的最新值到内存 x = b; // 1)强制从内存中读取变量b的最新值 }); Thread t2 = new Thread(() -> { b = 2; // 编译器插入storeload内存屏障指令 // 1)禁止代码和指令重排序 // 2)强制刷新变量b的最新值到内存 y = a; // 1)强制从内存中读取变量a的最新值 }); // ...start启动线程,join等待线程 assert x == 2; assert y == 1; } 可以看到通过 volatile 修饰的变量通过 LOCK 指令和内存屏障,实现共享变量的可见性和避免代码和指令的重排序,最终保障了程序在多线程情况下的正常执行。 02并发编程问题-原子性 private int count = 0; public void test() { List ts = new ArrayList<>(); for (int i = 0; i Thread t = new Thread(() -> { for (int j = 0; j count += 1; } }); ts.add(t); } // ...start启动线程,join等待线程 assert count == 100 * 10000; } 对于 Java 这样的高级语言,一条语句最终会被转换成多条 CPU 指令完成,例如上面代码中的 count+=1,至少需要三条 CPU 指令: 指令1:把变量 count 从内存加载到 CPU 的寄存器;指令2:在寄存器中执行+1操作;指令3:将结果写入内存(缓存机制导致可能写入的是处理器缓存而不是内存)。 那么假设有两个线程 A 和 B,同时执行 count+=1,可能存在如下情况: 线程 A 从内存加载 count 并执行 count+=1,同时线程 B 从内存加载 count 并执行 count+=1,并同时写回内存。那么这时结果是:count = 1。线程 A 从内存加载 count 并执行 count+=1,并将 count 结果写回内存。线程 B 再从内存加载 count 并执行 count+=1。那么这时结果是:count = 2。 线程 A 从内存加载 count 并执行 count+=1,同时线程 B 从内存加载 count 并执行 count+=1,并同时写回内存。那么这时结果是:count = 1。 线程 A 从内存加载 count 并执行 count+=1,并将 count 结果写回内存。线程 B 再从内存加载 count 并执行 count+=1。那么这时结果是:count = 2。 可以看到如果要 count 结果正确,要保证 count 读取,操作,写入三个过程不被中断。这个过程,可以称之为原子操作。原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomicoperation) 意为“不可被中断的一个或一系列操作”。 处理器主要使用基于对缓存加锁或总线加锁的方式来实现原子操作: // 判断当前机器是否是多核处理器 int mp = os::is MP(); _asm { mov edx, dest mov ecx, exchange value mov eax, compare_value // 这里需要先进行判断是否为多核处理器 LOCK IF MP(mp) // 如果是多核处理器就会在这行指令前加Lock标记 cmpxchg dword ptr [edx],ecx } 总线加锁:通过 LOCK#信号锁住总线 BUS,使当前处理器独享内存空间。但是此时其他处理器都不能访问内存其他地址,效率低。 缓存加锁:缓存一致性协议(MESI)。强制当前处理器缓存行失效,并从内存读取其他处理器回写的数据。当有些无法被缓存或跨域多个缓存行的数据,依然需要使用总线锁。 对于以上两个机制,处理器底层提供了很多指令来实现,其中最重要的是 CMPXCHG 指令。但 CMPXCHG 只在单核处理器下有效,多核处理器依然要加上 LOCK 前缀(LOCK CMPXCHG)。利用 CMPXCHG 指令可以通过循环 CAS 方式来实现原子操作。 private AtomicInteger count = new AtomicInteger(0); public void test() { List ts = new ArrayList<>(); for (int i = 0; i Thread t = new Thread(() -> { for (int j = 0; j count.incrementAndGet(); } }); ts.add(t); } // ...start启动线程,join等待线程 assert count.get() == 100 * 10000; } CAS 即 Compare and Swap。CAS 操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。 Java 语言提供了大量的原子操作类,来实现对应的 CAS 操作。比如 AtomicBoolean,AtomicInteger,AtomicLong 等。 CAS 虽然很高效地解决了原子操作,但是 CAS 也存在一些问题,比如 ABA 问题,循环时间长开销大,只能保障一个共享变量的原子操作。关于如上问题解决和 Atomic 包介绍,会在下篇文章详细阐述。 03内存模型与 happens-before 关系 前面说到处理器提供了一些特殊指令比如 LOCK,CMPXCHG,内存屏障等来保障多线程情况下的程序逻辑正常执行。但这依然存在几个问题: 处理器底层指令实现细节复杂难懂,开发人员需要付出巨大的学习成本。不同的硬件和操作系统,对指令的支持和实现不一样,需要考虑跨平台的兼容性。程序业务逻辑复杂多变,处理器和线程之间的数据操作依赖关系也相应更复杂。 处理器底层指令实现细节复杂难懂,开发人员需要付出巨大的学习成本。 不同的硬件和操作系统,对指令的支持和实现不一样,需要考虑跨平台的兼容性。 程序业务逻辑复杂多变,处理器和线程之间的数据操作依赖关系也相应更复杂。 因此高级语言会提供一种抽象的内存模型,用于描述多线程环境下的内存访问行为。 无需关心底层硬件和操作系统的具体实现细节,就可以编写出高效、可移植的并发程序。对于 Java 语言,这种内存模型便是 Java 内存模型(Java Memory Model,简称 JMM)。 Java 内存模型主要特性是提供了 volatile、synchronized、final 等同步原语,用于实现原子性、可见性和有序性。另一个重要的概念便是 happens-before 关系,用来描述并发编程中操作之间的偏序关系。除了 Java 语言,包括 golang,c++,rust 等高级语言也实现了自己的 happens-before 关系。 Java 内存模型的 happens-before 关系是用来描述两个线程操作的内存可见性。需注意这里的可见性,并不代表 A 线程的执行顺序一定早于 B 线程, 而是 A 线程对某个共享变量的操作对 B 线程可见。即A线程对变量 a 进行写操作,B 线程读取到是变量 a 的变更值。 Java内存模型定义了主内存(main memory),本地内存(local memory),共享变量等抽象关系,来决定共享变量在多线程之间通信同步方式,即前面所说两个线程操作的内存可见性。其中本地内存,涵盖了缓存,写缓冲区,寄存器以及其他硬件和编译器优化等概念。 如图所示,如果线程 A 与线程 B 之间要通信的话,必须要经历下面2个步骤: 线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中。线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。 线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中。 线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。 尽管定义这样的数据通信方式,但实际程序的数据依赖操作是复杂多变的。为了进一步抽象这种线程间的数据同步方式,Java 内存模型定义了下述线程间的 happens-before 关系: 程序顺序规则:单线程内,每个操作 happens-before 于该线程中的任意后续操作。监视器锁规则:解锁操作 happens-before 之后对同一把锁的加锁操作。volatile 变量规则:volatile 字段的写操作 happens-before 之后对同一字段的读操作。传递性规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。start()规则:如果线程 A 执行操作 ThreadB.start(),那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作。 join()规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 线程 A 从 ThreadB.join()操作成功返回。 与开发人员密切相关的是1、2、3、4四个规则。其中规则1满足了 as-if-serial 语义,即 Java 内存模型允许代码和指令重排序,只要不影响程序执行结果。规则2和3是通过 synchronized、volatile 关键字实现。结合规则1、2、3来看看规则4的具体使用,可以看到如下的代码,程序最终执行且得到正确结果: // x, y, z被volatile关键字修饰 private volatile int x, y, z; public void test() { Thread a = new Thread(() -> { // 基于程序顺序规则 // 没有数据依赖关系,可以重排序下面代码 int i = 0; x = 2; // 基于volatile变量规则 // 编译器插入storeload内存屏障指令 // 1)禁止代码和指令重排序 // 2)强制刷新变量x的最新值到内存 }); Thread b = new Thread(() -> { int i = 0; // 存在数据依赖关系,无法重排序下面代码 // 强制从主内存中读取变量x的最新值 y = x; // 基于volatile变量规则 // 编译器插入storeload内存屏障指令 // 1)禁止代码和指令重排序 // 2)强制刷新变量y的最新值到内存 // 3)y = x;可能会被编译优化去除 y = 3; // 编译器插入storeload内存屏障指令 // 1)禁止代码和指令重排序 // 2)强制刷新变量y的最新值到内存 }); Thread c = new Thread(() -> { // 基于程序顺序规则 // 没有数据依赖关系,可以重排序下面代码 int i = 0; // 基于volatile变量规则 // 强制从主内存中读取变量x和y的最新值 z = x * y; // 编译器插入storeload内存屏障指令 // 1)禁止代码和指令重排序 // 2)强制刷新变量z的最新值到内存 }); // ...start启动线程,join等待线程 assert z == 6; // 可以看到a线程对变量x变更,b线程对变量y变更,最终对线程c可见 // 即满足传递性规则 } 复制 private int x, y, z; public void test() { Thread a = new Thread(() -> { // synchronized,同步原语,程序逻辑将顺序串行执行 synchronized (this){ // 基于程序顺序规则 // 没有数据依赖关系,可以重排序下面代码 int i = 0; x = 2; // 基于监视器锁规则 // 强制刷新变量x的最新值到内存 } }); Thread b = new Thread(() -> { // synchronized,同步原语,程序逻辑将顺序串行执行 synchronized (this) { int i = 0; // 存在数据依赖关系,无法重排序下面代码 // 强制从主内存中读取变量x的最新值 y = x; // 基于监视器锁规则 // 1)强制刷新变量y的最新值到内存 // 2)y = x;可能会被编译优化去除 y = 3; // 强制刷新变量y的最新值到内存 } }); Thread c = new Thread(() -> { // synchronized,同步原语,程序逻辑将顺序串行执行 synchronized (this) { // 基于程序顺序规则 // 没有数据依赖关系,可以重排序下面代码 int i = 0; // 基于监视器锁规则 // 强制从主内存中读取变量x和y的最新值 z = x * y; // 强制刷新变量z的最新值到内存 } }); // ...start启动线程,join等待线程 assert z == 6; // 可以看到a线程对变量x变更,b线程对变量y变更,最终对线程c可见 // 即满足传递性规则 } 04内存模型综述 在本文中,我们对 Java 内存模型进行了全面的概述。Java 内存模型是 Java 虚拟机规范的一部分,为 Java 开发人员提供了一种抽象的内存模型,用于描述多线程环境下的内存访问行为。 Java 内存模型关注并发编程中的原子性、可见性和有序性问题,并提供了一系列同步原语(如 volatile、synchronized 等)来实现这些原则。此外,还定义 happens-before 关系,用于描述操作之间的偏序关系,从而确保内存访问的正确性和一致性。 Java 内存模型的主要优势在于它为并发编程提供了基础,简化了复杂性。屏蔽不同处理器差异性,在不同的处理器平台之上呈现了一致的内存模型,并允许一定程度的性能优化。这些优势使得 Java 开发人员可以更容易地编写出正确、高效、可移植的并发程序。 了解 Java 内存模型的原理和实践对于编写高质量的 Java 并发程序至关重要。希望本文能为你提供有关 Java 内存模型的有用信息,帮助你更好地理解并发编程的内在机制,以及在实际项目中选择合适的同步原语和策略。如果文章对你有帮助,欢迎转发分享。 -End- 原创作者|杨波
2023-09-06 09:45:38299阅读
每年节约3千万!微信实验平台Iceberg湖仓一体架构改造
微信实验平台不久前开始引入iceberg作为湖仓一体解决方案,最早从0.14.1版本开始引入到如今的1.2.2版本的广泛使用,目标是为了优化现有流程,达到更快及更省。在这篇文章中,我们邀请了腾讯工程师黄延岩给大家介绍微信实验平台基于iceberg做的改造和带来的收益,以及过程中遇到的问题、未来的期望。 1.背景 微信实验平台简介 微信实验平台主要提供微信内部各个业务场景(视频号、直播、搜一搜、公众号等)下的各类实验场景的支持,有 AB 实验、MAB 实验、BO 实验、Interleaving 实验、客户端实验、社交网络实验、双边实验等。 资源量级 微信实验平台承载的是全微信所有业务的实验场景下的指标计算及统计推断,业务有效指标个数达到了6w+,妥妥的资源使用大户,当前规模: 大数据场景计算资源 total core 已经有20w+ 存储资源 total 有 30PB+OLAP场景计算资源 total core 为2w+ 存储资源 total 为5PB+ 大数据场景计算资源 total core 已经有20w+ 存储资源 total 有 30PB+ OLAP场景计算资源 total core 为2w+ 存储资源 total 为5PB+ 基于成本及稳定性、中心内业务建设等角度考虑,我们计算资源大多收敛在 Gemini(微信云原生大数据平台),天穹 Gaia(TEG 公司级大数据平台)计算资源做 Backup,当平台依赖计算集群有异常,可以进行任务层面的计算集群切换,使我们支持的业务用户影响范围最小化,存储资源则完全依赖于天穹。 选型分析 Iceberg、Hudi 以及 DeltaLake 是基本同时期出现的开源表存储格式项目,整体的功能和定位也是基本相同,也一定会前期百花齐放相互借鉴最终走向同质化,网上已经有很多相关对比介绍的文章,这里就不详细比较了。 我们选择 iceberg 作为 Lakehouse Table Format 的方案的主要原因是: 公司内部数平团队专门支持,后备力量充足。我们场景更多偏离线和近实时(5min),实时场景要求不高, iceberg 的高级 feature 已满足我们的需求。 公司内部数平团队专门支持,后备力量充足。 我们场景更多偏离线和近实时(5min),实时场景要求不高, iceberg 的高级 feature 已满足我们的需求。 其他例如组件抽象更友好、更通用、pluggable 设计,向下支持的文件格式(Parquet/Orc/Avro)、存储类型(Object Storage/File Storage)更多,向上支持的计算引擎(Spark/Flink/Hive/Trino/Impala/SR…)更广泛,这些并不是我们业务方的主要考虑点。只要其与我们平台业务依赖的引擎框架都能足够兼容,生产环境稳定性可控、性能优异、解决方案优雅,即:在兼容我们已有技术架构下,优化现有流程,达到更快(计算时效性)及更省(节省更多资源)的目标。 2.方案 基于 Iceberg Table Schema 建设优化 实验平台业务不是层级复杂、主题域多样的业务数仓建设场景,而是仅具备超大规模指标计算的单一场景。所以我们的工作重点也不在高效复杂的数仓建设上,而是在于大规模指标计算优化上。 实验平台指标计算一般分为两种类型的表,命中表:包含某个实验 ID 及命中 uin 信息的物理表, 业务表:业务配置的用于指标计算及后续假设检验的物理表, 一般命中表及业务表及相关的配置逻辑口径会组成具体的某个指标。 有经验的数仓同学基本都清楚,表 Schema 的建设优化,基本都在于分区、分桶、排序、索引等方面。 业务表在表 Schema 的优化上会依赖于业务数仓的建设,依托于业务方的能力,实验平台可控性并不强。命中表的 Schema 恰恰是平台建设优化的重点,一般某天的某个指标计算会绑定具体的实验 ID,很自然的会想到按天作为一级分区,实验 ID 作为二级分区,可以将可用数据最小化,降低后续指标计算的读表 IO/shuffle IO。可是难点在于之前依赖于THive的建设,命中表一般至少保留3个月,实验 ID 更是达到 2w 个,做笛卡尔积后分区个数可达180w,Thive的元数据体系会受限于单点数据库瓶颈,存在 OMS(厂版Hive metastore) RDBS 单点问题,经常会因为某个表元数据太多导致整个元数据库 OMS 负载高,导致 Thive 不可用,影响 THive 服务上所有业务方保证的 SLA,所以 Thive 一般无法做到此种分区结构。刚好 Iceberg 的出现,由于其基于 HDFS 的独立三层元数据体系,可以将元数据信息的压力从 OMS 分摊到 HDFS 上,规避 OMS 的单点瓶颈问题。 总结起来,其实就是利用 Iceberg 的三层元数据体系带来的灵活性,细化业务表多级分区,规避 OMS 受限于单点数据库 RDBS 瓶颈的问题,提升后续计算效率。但要注意的是,要考虑到 NameNode 的元数据膨胀的问题,单 HDFS 存储集群一般超过8亿 metadata file(目录+文件)则处于高负载,会对 HDFS 存储集群后续稳定性带来压力。 Merge into+Time travel 代替传统数仓拉链表 微信实验平台会有命中信息增量变更的场景,即数仓同学所熟悉的缓慢变化维问题。 Slowly Changing Dimensions:维度建模的数据仓库中的概念,在现实世界中,维度的属性并不是静态的,它会随着时间的流失发生缓慢的变化。这种随着时间发生变化的维度我们一般称之为缓慢变化维,并且把处理维度表的历史变化信息的问题称为处理缓慢变化维的问题。拉链表:记录历史数据,记录一个事物从开始一直到当前状态的所有变化的信息。处理缓慢变化维问题的典型方案,拉链表的 table schema 实现通常加入属性列 start_time,end_time 来标识对应维度记录的生效时间/生命周期,能够支持方便地分析出历史数据变化情况。 一般此类问题的传统解决方案都是基于 Hive 拉链表来实现的,来减少重复的冗余数据,Hive 拉链表虽然可以解决业务问题,但是效率和灵活性都较低。我们引入了高效的数据湖表格式 Iceberg 来解决相应问题,相比于朴素的 Hive 增加了很多变化和灵活性。 Hive 拉链表的方式来减少重复的冗余数据,记录加上 start_time,end_time 作为生效起止时间, 但是此种方式带来的新问题就是每日计算时都需要拉取全部数据读入进 MapReduce/Spark 等计算框架内,将新增数据处理后再写入,需要消耗的计算资源很大,如果数据量特别大也很容易导致集群负载压力过大使任务失败。并且在读取拉链表的时候也需要加过滤条件(where >=start_time and 基于新的数据湖表格式 Iceberg 来更优雅地处理缓慢变化维问题,对比传统解决该问题基于 Hive 的拉链表方案的优势。归纳起来主要是通过如下方式实现: 使用 Merge Into 替代 Insert Overwrite 采用 Merge Into 进行增量数据批量变更(update/insert/delete)。 是通过重写相关文件,即包含在提交中需要更新的行的数据文件来支持 Merge Into,相对比于 Insert Overwrite 的方式,Iceberg 只替换受影响的数据文件来提升运行效率写入效率。 MERGE INTO iceberg_catalog.mmexpt_lakehouse.mmexpt_cumu_finder t USING (select first_hit_ds,uin,exptid,groupid,bucketsrc_hit from iceberg_catalog.mmexpt_lakehouse.mmexpt_daily_finder ) s ON t.uin = s.uin and t.groupid = s.groupid WHEN MATCHED AND s.ds WHEN NOT MATCHED THEN INSERT (first_hit_ds, uin, exptid, groupid, bucketsrc_hit, ext_int, ext_string) VALUES (s.first_hit_ds, s.uin, s.exptid, s.groupid, s.bucketsrc_hit, null, null); 使用 Time Travel Snapshot 代替拉链表冗余的记录有效起止时间 start_time,end_time 属性字段 可以使用 time travel in sql queries,比如查询2022-12-07 01:21:00 的历史状态,可以直接用。 -- time travel to 2022-12-07 01:21:00 SELECT * FROM mmexpt_lakehouse.table TIMESTAMP AS OF '2022-12-07 01:21:00'; 该种方式查询也会比拉链表减少很多处理数据量,不需要做有效时间字段的 filter(where >=start_time and 另外由于厂内 iceberg 老版本还不支持 timestamp as of 等语法,iceberg/issues/270 我们给厂内数平同学单独提了issue,在 iceberg metadata 中加入了 custom-timestamp 结合 sql hint 来代替 timestamp as of 方式。后续我们计划应用 Iceberg 1.2.2 带来的 Branching and Tagging 来去做更优雅的 Snapshot Time Travel。 特殊情况处理 例如历史数据出错,则可以直接回滚到具体出错前的 snapshot。 让用户在每次提交的 snapshot 列表中切换,比如 version rollback,set snapshot id。 Roll back table db.sample to snapshot ID 1: CALL catalog_name.system.rollback_to_snapshot('db.sample', 1) Sets the current snapshot ID for a table. CALL catalog_name.system.set_current_snapshot('db.sample', 1) 然后数据修正后 commit 到其后的 snapshot 中。 总结起来,其实就是利用 Iceberg 的三层元数据体系带来的灵活性,可以解决 Hive 实现传统拉链表方式下的写入效率低,查询效率低,灵活性低,易用性低等问题。在特定业务超大拉链表的场景中,任务写入及查询效率都带来了指数级的提升。 针对此方案我们也申请了专利《一种基于数据湖表格式处理缓慢变化维问题的新方法》专利立项编号:2023010065CN 流批一体 Iceberg 使 CDC 场景做分钟级写入成为可能,可以将 Iceberg 统一流批 Pipeline,作为公共上游,使代码复用,减少数据冗余,并从根源上规避数据不一致等问题。同时我们也希望精简全链路,过多的 step 会增加数据开发的成本,也会降低全链路的稳定性和可靠性。如上图所示,架构也会更加优雅。 3.收益 在我们的使用实践过程中,发现 THive 兼容性不足,其中默认的 ORC 为厂内魔改版本,带来一定的对接使用隐患,比如 ClickHouse ORC 外表无法识别。ORC 魔改版本在 Spark 上的优化,也距离原生组件有些差距。 总结起来 Iceberg 方案的优势,对比太过朴素的 Hive,兼容性不足的 THive,Iceberg 带来的高级 Feature: 包括 ACID 粗粒度事务语义,可以避免脏读及下游失败等问题,借助于三层 Metadata 实现的 snapshot、time travel、schema evolution/partition evolution, row-level upsert/delete 等 feature 都带来了极致的灵活性。在业务升级、问题回滚相较于朴素的 Hive 带来了新的优雅的解决思路。配合异步 Auto-optimizing 服务优化数据存储组织方式(定期 compact 或进行合理排序和分组),提高查询效率,给我们带来很大收益。 我们已经将 20PB 的历史数据迁移到 Iceberg 库上,并且后续增量数据默认采用 Iceberg 作为数据基座。 结合社区开源版本优化红利,Spark 3.3全面接入(Gemini on Spark Oteam),带来的增强了 AQE(adaptive query execution) 能力,增加 row-level runtime filter 来补充 Dynamic Partition pruning 等 Feature,及 Iceberg 1.2.2的全面接入,我们从计算性能、存储占用两方面进行了优化的实践,最终效果为,计算上总核时优化69.4%,节省约20w 核时/天,存储空间上优化约100PB,总计折合降本预计约3kw/年。在降本的同时提升了离线计算的效率。计算任务 p99耗时减少70%, 平均任务耗时减少60%。 4.遇到的问题 针对数据开发过程中的业务常见问题- 数据倾斜问题,小文件问题,随机性问题,我们都有遇到,并有一套解决方式供大家参考。 数据倾斜问题 分区数据倾斜 如上方案一描述的,我们采用实验 ID 作为二级分区,每个实验的命中流量都是不均匀的,尤其针对一些全流量的 holdout 实验,就针对写入 Iceberg 的 Stage 做了单独的大实验倾斜处理,在写入前的重分布过程中,加入了打散化处理。 val bucketIdHashUdf = udf((exptid: Long, uin: Long) => { val maxExptIds: ListBuffer[Long] = maxExptIdsBroadCast.value if (maxExptIds.contains(exptid)) { exptid.toString + "_" + ((uin.hashCode() & Integer.MAX_VALUE) % 50) } else { exptid.toString } }) val icerbergDf = df .withColumn("bucket_id", bucketIdHashUdf(col("exptid"), col("uin"))) .repartition(partitionNum, col("ds"), col("bucket_id")) .sortWithinPartitions("ds", "exptid") Merge Into 写入倾斜 在 Iceberg TBLPROPERTIES 中加入了 Write Properties。 'write.distribution-mode' = 'range' -- Defines distribution of write data: none: don’t shuffle rows; hash: hash distribute by partition key ; range: range distribute by partition key or sort key if table has an SortOrder 批写小文件问题 相对于实时场景下分钟级 commit 造成 snapshot 及 datafile 膨胀的问题,我们面对的场景是 batch 场景,基本为日度例行任务,需要合理配置 targetSizeInBytes,及合理控制 spark stage 的 partition number,来规避 batch 场景写入 iceberg 的小文件太多问题,即每个 spark 的 partition 都会写入 iceberg datafile 如果写入的 iceberg datafile write.target-file-size-bytes 则会拆分多个文件 split 写入。 同时,因为我们的存储资源量级太大, 也跟数平运维同学,申请了专属独占的 HDFS 存储集群,来保证业务稳定性,避免 NameNode 过载导致文件读写延迟变大或者 Connect Fail Exception 等问题,并开通了存储集群 grafana 监控等权限,提前预知集群健康度对任务的影响。 随机性问题 预期中的 Spark 任务应该具有幂等性,即任务多次运行的结果应该完全相同,当出现结果不同的随机性问题时,就很难去回放数据。 Accumulator 带来的随机性问题 因为我们的超大任务规模比较大(单任务读写>20T),运行过程中因为机器的负载等问题,导致 task fail 甚至 stage fail 很正常,恰恰 fail 造成了 Accumulator 的数据运行不一致,spark document 上有注明。 For accumulator updates performed inside actions only, Spark guarantees that each task’s update to the accumulator will only be applied once, i.e. restarted tasks will not update the value. In transformations, users should be aware of that each task’s update may be applied more than once if tasks or job stages are re-executed. Accumulator 的更新应该在 action 算子中,而不应该在 transformation 算子中,来保证 Accumulator 的更新只会应用一次。 Random 处理数据倾斜带来的随机性问题 处理数据倾斜时,常用的方案为在倾斜 key 上加入随机数来进行打散,但是这种处理方式在 Shuffle Fail 进行 retry 时,数据会被不同的 task 重复 fetch,导致引入随机性问题。小任务不太容易出现 Shuffle Fail 的问题,超大任务或者集群负载水位较高时,则更容易触发此类问题,应该用取模或者哈希之类的幂等函数来打散倾斜的 Key,规避此类随机性问题。 其他思考 另外关于其他 Iceberg Data Skipping 层面的排序、索引等数据组织上优化的考虑,我们也做了一些思考。 如前面提及的,实验平台业务不是层级复杂、主题域多样的业务数仓建设场景,而是仅具备超大规模指标计算的单一场景。所以我们的工作重点也不在高效复杂的数仓建设上,而是在于大规模指标计算优化上。业务上决定了我们没有字段点查的场景,所以并没有使用 bloom filter、bitmap filter 等字段索引 feature,仅使用 Iceberg 默认存储文件级别每列的 Min、Max 信息,并用于 TableScan 阶段的文件过滤。Z-Order 对我们业务场景收益不大,没有太多的基于某个表的多个常用字段进行 filter 的 data-skipping 需求。 5.未来规划 StarRocks+Iceberg 更好的湖仓融合 我们的实时指标计算场景,我们没有复杂的 ETL pipeline,主要利用 OLAP(StarRocks/ClickHouse)等 SQL 表达能力强的引擎,作为实时指标计算的依赖引擎,而非 Flink/Structured Streaming 等可编程能力强的计算框架。 ClickHouse 是性能优秀的 OLAP 引擎,但是 Clickhouse sql 表达及优化能力,普适性不足。为了解决 clickhouse shuffle 问题及冷数据复用避免转移到 cos 等对象存储, 实现极速的 SQL on Iceberg,更好的 Ad Hoc Analysis 体验,我们后续的实时湖仓方案会采用 StarRocks 3.x + Iceberg,便于湖仓融合。 StarRocks 既能兼容 TPC-DS Benchmark 的语法,在 ClickBench Benchmark(https://benchmark.clickhouse.com/) 打榜上和 Top 1的 Clickhouse 性能极其接近,更注定了 StarRocks 发展上限很高。 硬件层面计算框架加速 我们的业务更多 focus 在计算框架层的参数优化,和实现逻辑优化,希望可以优化到极致。从另一个视角思考,硬件层面加速计算过程,对我们的 Gemini 微信云原生大数据平台依赖的 CVM,相同成本下 1T SSD 云盘替换为8块 500G 高性能云硬盘做 RAID-0,因为高性能云硬盘本身就是采用3副本来保证数据高可用,做 RAID-0后可用性也是完全可以接受,同时 IO 吞吐可以翻4倍(SSD 275MB/s, 8块云硬盘做 RAID-0 吞吐量能达到 1GB/s),由于离线计算框架更多使用顺序读写,该方案在实施中,预期收益明显。 Steaming Lakehouse 伴随着社区 Apache Paimon 的孵化,我们也希望流批一体架构变得更简洁,在保证性能的前提下,忽略掉流存储 MQ 和湖存储 Table Format 的差异,可以对外作为一个统一组件供业务使用,追求批流一致性语义,提供实时离线一体化的开发体验。 以上就是微信实验平台改造的过程与结果,如果文章对你有帮助,欢迎转发分享。 -End- 原创作者|杨波
2023-09-06 09:40:50295阅读
鹅厂程序员亲测AI写真通用版,女友直呼“真妙呀”!
随着AIGC产品能力的跃迁,过往需要专业人士才能产出的内容已经被削去了门槛,这其中就包括受到热捧的Al写真工具。然而体验成本、实际效果和数据安全之间仍旧存在差距,我们能不能自己动手做出“数字分身”?腾讯工程师霍然以一张美女图片作为输入,使用Al绘图工具Stable Diffusion零成本制作了一组涉及古装、现代、未来感等场景的“数字分身”。 时下流行的 AI 写真工具,为用户提供了用现成的照片遨游于广袤素材与想象中的可能性,也启发 AI 从业者对于大众消费产品的构想。对于个人来说,是否可以上手 AI 技术,做出自己的“数字分身”呢?本文将介绍一种高效率、易上手、低成本、高安全的“数字分身”制作方式。看完本文,你也会用一张图片“穿越古今”,做出自己的 N 个“数字分身”。 随着网络某相机小程序的火爆,关于 AIGC 智能应用的讨论又一次点燃移动互联网。9.9 元即可制作不同背景、造型下的“数字分身”照片,出图效果几可比拟专业照相馆,引发了受众的追捧。 而质疑者,认为 9.9 元的体验成本过高,也觉得产品高峰期的数十小时的等待时长过于熬人。苦恼于无法对生产的照片任意加工,更为 AI 应用的数据安全性忧心忡忡。 这些想法也反映了广大用户对于 AI 应用的需求和渴望。对于 AI 工具,用户希望既可以降低使用成本,又可以保证生产效果。如果还能简化生产流程、缩短生产时长、提供便于交互的服务,同时保证数据安全性,当然更佳。 那么,所谓的 AI“数字分身”领域,或是“AI 写真”领域为例,是否有一个满足以上所有要求的方案呢? 制作个人的 LoRA 模型是一种生成“数字分身”的方式,也被一些用户猜测为“AI 写真小程序”的技术方案。 这种方法可以生成较为稳定的、多角度的人像,但是其需要输入的照片较多,技术实现的步骤也稍复杂,对于新手的使用门槛较高,文中不做介绍。 本文将由浅入深地介绍一种小白可以轻松上手的简易“数字分身”制作方式,基本满足用户对 AI 应用的要求。 本文介绍的“数字分身”制作方法可以通过 AI 技术随意更换照片中人物的服装、造型、背景,用一张人像照片“穿越古今”。读者可以拿 9.9 元买一杯蜜雪冰城,在家里“一键出图”。 整个产图流程可以在个人计算机的服务器完成,不需要把照片上传到外部服务器,极大地保障数据的安全性。同时,千变万化的服装、背景、造型更是令人眼花缭乱,AI 的创意指数拉满,令人拍案叫绝。 制作“数字分身”的过程需要解决 3 个问题: 画什么?用什么?参考什么? 针对这 3 个问题,产生了 3 个步骤: 画什么:填写描述词; 用什么:上传图片并选择重绘区域; 参考什么:提供参考的人物姿态。 下面将按照这 3 点依次介绍。下文用到的工具为 Stable Diffusion WebUI,以及 ControlNet 插件。为了简化内容,本文介绍便捷有效的实操步骤,Stable Diffusion 的安装和精细化调参技能点请读者自行学习,文末附有参考材料。 “画什么”的问题在用文字形式模拟用户脑海中的想象。 小时候,男生幻想自己穿着侠客的青衫,仗剑走天涯;女生幻想自己穿着飘逸的襦裙,沐浴唐风汉韵。如今的 AI 技术可以借助语言描述,让我们在风格各异的场景中塑造个人形象。建立这一形象,首先需要用语言描述人物的服装、发型、背景,描述语言称为提示词。 提示词分为正向提示词(希望画面出现的内容)和负向提示词(不希望画面出现的内容)。绘图用的 Stable Diffusion 为国际化工具,提示词要用英语。 比如生成一个穿着古装汉服的女孩,女孩佩戴精美发簪,以传统的中式宫殿为背景;同时,希望图片高质量、高清晰,不要出现模糊、丑陋、动画等元素。将正向提示词写为“1girl, wearing song hanfu, wearing delicate traditional chinese hairpins, chinese palace background, materpiece, best quality, ultra-detailed”。负向提示词写为“blurry, ugly, bad quality, cartoon, anime, NSFW, nude”。 “用什么”在追问图片生产过程中的主要原料是什么。 本文介绍简易的“数字分身”制作过程,用现成的照片换掉照片中的造型、服饰、背景,达到“一键穿越”的效果。 在此过程中,用到的原料是现有的照片,应用的技术为 Stable Diffusion 的图生图局部重绘(img2img inpaint)功能。涉及到的操作为上传图片并手动选择重绘区域。选择照片时,建议选择上半身,面部轮廓清晰的正面照。比如,以一张年轻女性的正面半身照作为输入。 上传图片后,我们把“换造型,换服装”的需求转换为技术语言“重绘除了脸部之外的所有区域”。那么,AI 工具如何知道照片中哪里是面部区域呢?在使用时,先上传图片,再用黑色的笔刷手动涂抹面部区域,就能精准地标识面部区域,如图 6 所示。 此外,还需要选择对非涂抹区域进行重绘的选项(inpaint not masked),也就是对于除面部外的区域进行新的创作,如图 7 所示。 “参考什么”则是结合什么辅助信息,让图片生产的效果更稳定。 既然是参考,则非必需,但是有利于提升图片生产的质量。上面的两个步骤为 AI 描绘了绘图内容,也提供了人像的脸部特征。此时若是让 AI 工具“信马由缰”地发挥,容易出现人体比例失调,动作不自然等问题。为 AI 工具提供“参考答案”虽然会减少创意空间,但是能让 AI 工具学习原照片的动作姿态,生成更加自然的图片。如何学习人物姿态呢?学习人物姿态需要用到 ControlNet 插件,一款对图片进行预加工的工具,把预加工的结果像积木块一样拼插到生成图片的流程中。 输入和上一步相同的正面半身照,用 ControlNet 插件中的 openpose 预处理器学习图中人物姿态,比如头和身体位置关系,手臂的动作。按照图 8 的方式选择启用 ControlNet 插件,并选择 openpose 预处理模式和模型。 进行上述操作后,就可以得到图 9 的多款古装美女“数字分身”。 那么如果想制作更多的分身,读者应该修改前文介绍的哪些步骤呢? 读者可以回顾,思考一下本文介绍的方法。前面介绍的 3 个步骤中,“画什么”的步骤决定图片内容,“用什么”的步骤决定核心素材,“参考什么”的步骤决定额外的素材。 回顾后发现,当我们想对画面元素做修改时,只需要修改“画什么”步骤中的提示词。比如想要生成现代装校园风图片,只需要重写正向提示词中涉及服装,造型,背景的提示词,将正向提示词改写为“1girl, wearing school uniform, ponytail, campus background, materpiece, best quality, ultra-detailed”。负向提示词仍然写为“blurry, ugly, bad quality, cartoon, anime, NSFW, nude”,如图 10 所示。 除了修改正向提示词外,其他步骤均保持不变。点击“生成”按钮,就能得到图 11 的“校园女孩分身”。 得到“数字分身”后,如果读者希望进一步修改图片细节,比如重新生成背景中的建筑物,可以用局部重绘功能涂抹待修改的细节,仿照上面的指引,用提示词来牵引重绘方向,具体操作交给读者探索。 按照这种方式不断解锁校园风、古代风、未来感、中式旗袍、日常风的造型,就得到了文章开头异彩纷呈的“数字分身集”。 写到最后,对比一下本文通过 Stable Diffusion WebUI 制作“数字分身”的方案和 AI 写真小程序在用户体验方面的异同。 两种方法的相同点为:以人像照片为输入,通过技术手段获得不同场景、造型的人像写真图。 除此之外,两者在使用体验方面有较大的差异。在成本方面,AI 写真小程序需要更多“有形投资”,比如更多的照片数量,更高的费用;而本文的方法有更多的“无形投资”,比如部署和使用 Stable Diffusion 的能力。 在收益方面,AI 写真小程序在生成图片的角度和颜值上有优势,画面自然感更高;而本文的方法在生成图片的造型、背景丰富度以及再加工能力上更胜一筹。 期待大家可以用 AI 技术拓展生活的疆域。 如果读者朋友们想进一步学习如何部署 Stable Diffusion 以及如何精细化调参,可以参考腾讯云开发者的这篇文章《给想玩AI的新手|Stable Diffusion 保姆级入门手册》。 有了自制的 AI 写真工具,你最想生成哪些照片?或者想在工具里面加入哪些功能?欢迎在腾讯云开发者公众号留言。我们将挑选一则最有趣的答案,为其留言者送出腾讯定制T恤。8月31日中午12点开奖。
2023-09-06 09:32:37306阅读
那年装的七里香,如今跑在腾讯云
👉导读 时光如白驹过隙,坐在时代的列车里,我们一路向前;近三十年来,无数事物在车窗前掠影而过,一度流行,又一度黯淡。磁带,就是一个时代的符号。彼时,磁带因其低廉、可靠及易用等特性,一度成为音乐最主流的载体,将流行音乐传遍大街小巷。后来,随着 CD 和 MP3走进大众视野,磁带逐步退出历史舞台。如今,磁带作为音乐载体早被时代淘汰.....但磁带作为存储载体,近几十年却从未过时:在冷数据场景,磁带存储凭借其极低的成本和极长的寿命,在企业存储市场始终占有一席之地。今天的故事就此展开,来聊聊腾讯的深度归档存储与磁带的那些事。欢迎阅读~ 👉目录 1 磁带技术硬件介绍: 企业磁带技术详解 1.1 童年的回忆——家用磁带渐行渐远 1.2 云时代的契机——企业级磁带蓬勃发展 1.3 磁带硬件概览 1.4 磁带介质 1.5 磁带柜 1.6 磁带硬件特点总结 2 磁带技术业务应用: 深度归档线上实践 2.1 对象存储 COS 2.2 深度归档存储介绍 2.3 极冷数据存储引擎:Berg 2.4 数据沉降流程 2.5 数据回热流程 2.6 数据删除流程 2.7 数据修复流程 2.8 数据安全性保障 3 写在最后的话 01 磁带技术硬件介绍: 企业磁带技术详解 1.1 童年的回忆——家用磁带渐行渐远 起磁带,80和90后可能感触颇深。在20世纪末至21世纪初的那些时光里,磁带绝对是那个时代的符号: 不管是在街上插着裤兜边走边听随身听的翩翩少年,亦或是街边一家接一家的外放音乐似乎永不停歇的小店,无一不在诉说着磁带的故事。 我与磁带结缘于2003年,那一年我读初一。依稀记得当时电视上天天洗脑般的播放各种学英语的广告——“学英语,Follow Me”、“XX 高复读机,学英语更容易”,潜移默化之下,似乎全天下的家长都认为复读机真的可以学英语。于是,复读机便成了中学生的必需之物,流行音乐的种子也同时在学校里生根发芽。那一年,我不仅拥有了人生中第一台复读机(也是唯一一台),也花掉了积攒已久的零花钱,拥有了人生中第一盘磁带——周杰伦的《叶惠美》。 不过后来,更厉害的东西出来了——MP3播放器。MP3播放器个头不大,但可以装的音乐更多,听完一波还可以再换一波,简直是听歌神器,再配合全球最大的盗版音乐下载基地: 某度音乐,MP3播放器迅速取代磁带成为了学生听歌设备的中流砥柱,从磁带手中接过了"繁荣华语音乐"的大旗。而也就是那时起,家用磁带便离我们渐行渐远。 1.2 云时代的契机——企业级磁带蓬勃发展 虽然民用级磁带已逐步退出历史舞台,但企业级磁带技术却在近几十年一直处于高速发展态势,其最主要是解决冷数据问题。根据 Horison 调研报告,预计到2025年,全球每年会新增163ZB 的数据,其中60%都是冷数据,即2025年每年会有100ZB 的新增冷数据。 冷数据有什么特点?日常写(如日志、音视频等),低频读,数据要可靠,成本还要低,寿命还要长,最好存放数据的介质不读时还不费电…… 这些看似苛刻但实际的需求,是冷数据的特点,却也命中了磁带的硬件特性。因此,磁带在各个大型公司中始终承担着数据备份的作用,并且在关键时刻一定能够把数据恢复出来。谷歌在2011年曾经出过一次事故,在一次 gmail 服务软件更新中出了个 bug,导致线上4万个 gmail 的账户数据被删除,尽管他们的邮件数据存储在了多个数据中心的多个副本里,但是数据依然有所丢失。最后,谷歌是从他们的磁带备份中把丢失的用户账户数据给恢复回来了。 但是,对于中小型企业来说,引入磁带有一定的技术门槛,前期的投入可能得不偿失。互联网公司发现了这个商机,他们在公司内部把磁带技术钻研透彻后,逐步把磁带存储技术搬到了云上。 2019年,亚马逊在云上推出了基于磁带的极冷数据存储产品:Glacier Deep Archive,也就是 S3的深度归档服务。Glacier Deep Archive 可以让中小企业0投入,直接以极低的成本使用云上的磁带存储。而除了互联网公司外,传统企业的备份需求也非常大,国内外的科研机构、广电、银行、证券、保险等行业,每年都会采购大量的磁带存储构建私有云,用于冷数据的存储。 1.3 磁带硬件概览 企业中使用的磁带存储设备,我们称作磁带库("库"字既可以理解成 Library,也可以理解成仓库)。 磁带库常见两种形态: ▶︎ 独立机柜: 单个磁带库占地面积较小,通常配置的槽位数为400~900左右的量级(一个槽位可以插一盘磁带),对应物理存储空间约5~10PB 左右; ▶︎ 联排机柜: 单个磁带库占地面积较大,通常为多个机柜组合而成,槽位数配置也可以达到几千甚至上万,对应的磁带存储空间可以达到百 PB 级别。 互联网公司通常使用独立机柜较多,一个原因是硬件部署时对 IDC 的机位的侵入性更小,机位易改造;另一个更关键的原因是当发生整机故障时,故障影响范围更小。 磁带库内部,主要部件有四个: ▶︎ 磁带: 实际的存储介质; ▶︎ 驱动器: 也叫磁带机,负责磁带介质的读写。这里需要注意,驱动器对磁带的读和写是互斥的; ▶︎ 机械臂: 负责磁带介质的移动,从槽位中取出磁带放至驱动器,或者从驱动器中取出磁带放至槽位; ▶︎ 槽位: 实际存放磁带介质的地方,通常是一个类似抽屉形状的存储空间。 补充一点,磁带库是纯机械构造,因此故障要比 X86服务器要很多,这也是磁带库使用时,应该重点考虑的事。 为了让大家更直观的了解磁带库的“长相”,这里特准备一段视频(某磁带库厂商在互联网平台投放的广告),大家可以更直观的看到磁带库大概的样子(视频中的设备并非腾讯引入的磁带库型号)。 1.4 磁带介质 LTO vs 3592 磁带的介质有两种,一种是 LTO,一种是3592,驱动器类型也与之对应: ▶︎ LTO 为标准组织定义的磁带类型,3592为 IBM 独有的磁带类型,需配合 IBM 专有驱动器才能访问,且仅能从 IBM 购买 (不可否认,IBM 对于 LTO 的技术推进也贡献很大); ▶︎ 目前的主流使用的磁带为 LTO-8(12TB)和 3592 JE(20TB),下一代 LTO-9(18TB)于2021正式发布(原定2020年); ▶︎ 3592类型,旧磁带可格式化成新一代(容量/性能不同于新一代),可循环利用,LTO 无此功能; ▶︎ 磁带生产厂商主要有 FujiFilm 和 Sony,这两家厂商市场占有率总和接近100%,各家磁带库厂商的磁带生产主要交由 FujiFilm 和 Sony 进行生产,包括 LTO 和359。 与 HDD 不同,磁带介质不管是 LTO 还是3592,都不是标准的块设备,都不能直接使用。 LTO 和3592整体的路线图如上。LTO-8和3592JE 的理论顺序读写速度分别为360MB/s 和400MB/s,不过由于种种机械特性的限制,生产环境很难持续的把理论速度跑满。 目前 LTO9 已经发布,海外已经有一些公司逐步开始使用,国内对待 LTO9 的态度仍然偏保守。 HDD 与磁带的技术栈本质上有些类似,均为磁技术(加磁和读磁),通过对存储介质施加磁性,达到数据存储的目的,只是二者目前的密度不同。左图是来自 insic.org 的关于磁密度的 HDD 与磁带的对比,到目前为止,HDD 的磁密度已经接近极限了,而磁带的磁密度上限还非常大。 目前业界真正投产的最大容量的 HDD,主要集中在20~24TB,但 CMR 短期内很难看到再有容量上的突破。HDD 的密度难以突破的原因之一,是磁头操作磁道时,会相邻磁道产生干扰,从而影响周边数据,这个问题也叫做邻道干扰(ATI),密度越高问题越明显。所以要想 HDD 的存储密度提升,ATI 是绕不开的问题。目前应对的手段主要有2个,分别是 HAMR(热辅助技术)和 MAMR(微波辅助技术)。使用 HARM 和 MAMR 理论性可以使得 HDD 达到40~50TB 这样一个容量量级,也是目前 HDD 容量的天花板。 而磁带在密度上的现状则完全不同。如果把提升密度比喻成打怪升级的账号,那么现在磁带的密度连新手村都没有出。用同样12T 的容量来比对下,12T 的硬盘容量大概是923GB/in²,而12T的LTO8磁带,它的密度现在只8.5Gb/in²。也就是在 LTO8基础上,能够至少再提升一百倍的容量空间。目前富士联合 IBM 已经在实验室中成功研制出了580T 容量的磁带 demo。不过短时间内,磁带的容量增速不会太激进,一个原因是有好东西要挤牙膏一样缓慢推进才能创造持续的商业价值,另外一个原因是配套硬件也需要时间去适配(比如网卡带宽、驱动器能力等)。 总的来说,目前磁带相比于 HDD,在容量上有着无尽的想象力。 如上是详细的 HDD 与 Tape 的对比说明,这里就几个关键点补充说明: ▶︎ 成本: 磁带的成本优势,不仅仅在于相比于 HDD,同容量的介质采购价格更低,同时磁带的寿命通常要远长于 HDD,以及在非读写时磁带完全不费电,磁带整体 TCO 相比于 HDD 机型可以显著下降; ▶︎ 性能: 磁带的吞吐性能尚可,大批大文件数据连续写时,可以跑到接近理论吞吐,但磁带在读数据时,性能会退化非常严重,主要体现在寻址时间过长。当业务想要从磁带中读出数据时,磁带库需要经历“机械臂找磁带并插入驱动器”和“驱动器倒带直到数据所在 LBA”,前者耗时最长几十秒,而后者耗时可能长达2~3分钟。因此,磁带库的应用,最重要的就是如何把读性能尽可能提高; ▶︎ 可靠性: 磁带的可靠性远高于 HDD; ▶︎ 非标设备: 磁带是一个块设备,但是不是标准块设备,访问磁带时需要依赖外部驱动和软件。 1.5 磁带柜 机柜介绍 磁带柜分为独立和联排。独立机柜更灵活一些,IDC 改造也更容易一些,同时隔离能力更强。比如当电源坏了或是机械臂卡住类似这种非常极端的故障,独立机柜只影响一部分数据;而连排机柜,如果发生极端情况,会有更大面积数据受到影响,因此互联网厂商/云厂商,多会选择独立机柜进行部署。 市场上磁带柜厂商主要有:第一 IBM,第二 Quantum,第三是 BDT,但是 BDT 主要是做 OEM 和 ODM。除了市场老大和老二,BDT 每年给海外互联网公司的出货量也非常大。 磁带库配套驱动/软件 磁带是一个非标的块设备,需要外部程序配合使用。而社区中公开的技术,即为 LTFS。 LTFS 可以把单盘磁带模拟成一个文件系统,给用户屏蔽了“通过 iSCSI 协议向机械臂/驱动器发送原始指令”等细节,同时 LTFS 兼容 Linux、Windows 等多个平台。凭借开放、易用的特性,LTFS 对磁带技术有一定程度的推泼助澜的作用。不过 LTFS 在企业场景还是稍显乏力:缺乏大规模磁带管理能力、性能较弱。 为了应对企业场景,磁带库厂商通常会搭配企业级的 LTFS 进行捆绑售卖,企业级 LTFS 相比于社区 LTFS 而言,增加如下优势: ▶︎ 具备大规模磁带管理能力,并且以统一的视野面向使用者; ▶︎ 可以实现批量取回优化:同一盘磁带中的多个文件优化读取顺序,跨磁带的多个文件按磁带进行合并; ▶︎ 提供多种访问协议:私有协议远程文件系统/NFS/CIFS/对象接口等(不过各家基本都是先实现了文件系统协议,其他协议基于文件系统协议进行二次封装,因此效率底下); ▶︎ 提供副本管理:可以设置多副本 (不支持 EC,并且需要消耗大量的机器资源,性价比比较低)。 那么,企业版 LTFS 长什么样呢? 我们看下图: ▶︎ 磁带库 &X86 服务器:通常每一台磁带库都对应一台或多台 X86服务器,这个 X86服务器上运行有厂商提供的驱动/软件,X86服务器与磁带库通过 FC 网络直连; ▶︎ 数据缓存:X86 服务器上一般有 SSD 来做磁带库的缓存盘。数据写入时,需要先写入缓存盘,再沉降到磁带中;数据回热也是类似的,回热成功后数据回到了缓存盘里。实际上我们跟磁带库打交道时,数据流都是跟缓存盘打交道,而控制流(刷磁带/读磁带)则可以通过 API 进行。 这里大家可能会有疑问,为什么不管是社区还是厂商,提供的最基础协议都是文件系统呢?主要还是因为用起来方便,尤其是小企业,使用时把文件系统挂载到本地服务器,直接跟文件系统交互即可,用着省事。这部分群体不太关心效率。但事实上,文件系统协议对互联网公司来说,还是太重了,尤其还是 Posix 语义的文件系统。如果从互联网公司的需求来看,就做几个最简单的 BatchPut,BatchGet,BatchDelete RPC 接口就足矣,完全无需文件系统语义,因为文件系统语义大部分功能都是多余的。即使真的有场景需要提供文件系统语义,也应该通过SDK进行文件系统的操作(类似 HDFS-Client),而非通过 VFS/FUSE 进行操作,效率着实有点低。基于上述原因,亚马逊等海外的磁带使用量较大的大厂,实际上投入了非常多的人力,彻底干掉了厂商提供的所有软件,直接在纯硬件上开发适合云场景的软件系统。 1.6 磁带硬件特点总结 ▶︎ 成本特性:成本低、功耗低、寿命长、容量增速空间有想象力。 ▶︎ 运维特性:需要机房改造,相比于 X86服务器,运维更复杂。 ▶︎ IO 特性: 只能顺序读写:通常情况下磁带只能顺序读写,不能直接原地修改; 吞吐高:单盘磁带可达到300~360MB/s 吞吐,不同驱动器可并发; 延迟高:磁带寻址(尤其是读磁带时)时间最多可达3分钟; 大文件更优:每写一个文件都要停顿记录元数据,文件太小导致磁带吞吐不连续; 回热代价大:沉降时几乎不用寻址,磁带尾部 Append 即可,而回热需要花费更多时间寻址; 批量作业更优:磁带库软件内部可以进行优化,让磁带转动的距离更短; 缓存盘交互:不能直接访问磁带,需要与缓存盘交互; 数据回收缓慢:数据不能直接从磁带删除,只能后续通过倒带的方式来消除空洞。 02 磁带技术硬件介绍: 企业磁带技术详解 2.1 对象存储 COS 接下来就是磁带存储的业务应用,即 COS 深度归档存储,COS 的全称是 Cloud Object Storage,它是腾讯的一个大型分布式存储系统平台,更具体地说是一个对象存储的平台。COS 凭借开放商用的标准能力,目前服务了上万家内外客户。 COS 有哪些存储类型? COS 有5个存储类型,从冷到热分别叫标准存储、低频存储、归档存储和深度归档存储,以及智能分层(本文暂时不讲)。我们可以看到数据越热数据存储越贵,数据越冷数据存储越便宜。但是数据越冷,它的数据访问越贵,深度归档存储和归档存储从定义上来都算冷数据的范畴,并且面向的用户场景其实也有一些重叠,但基本上都是做归档用。 归档存储与深度归档存储区别在哪儿? 当有一些归档数据用户取回时,需要分钟级或者小时级,那么就可以掏3倍的价钱购买归档存储而不是买深度归档存储。但如果用户数据量非常大,比较在意成本,并且能够接受天级别的回热延迟,那么就非常适合做深度归档。比如很多企业为了合规性,把一些日志、视频进行归档,当审计或者需要时进行取回。举个例子,比如部分企业运营的原始资料(比如直播录屏),这些数据如果不是遇到重大案件回溯,基本上不会再调出来使用,但是这些数据出于合规性,又得永久保存,这种场景就可以放到深度归档里,且成本非常低。 2.2 深度归档存储介绍 以下,我们分别引用腾讯云和亚马逊的深度归档介绍: 深度归档存储(Deep Archive)是对象存储(Cloud Object Storage,COS)提供的可让海量数据长期归档的存储服务。深度归档存储提供了磁带存储级别的存储单价,为用户数据长期存储提供了低成本方案。用户无需在本地维护复杂的磁带库配置,无需关注底层存储介质的演进,通过对象存储 COS 提供的 API、SDK、生态工具和控制台等丰富的人机交互手段,即可实现便捷、低成本地管理数据。 深度归档存储适用于数据访问频率极低,但需要长期保留的场景。在日志冷备场景中,企业需要按照当地法律法规要求,将每天产生的日志数据进行冷备存储,以便追溯和分析。在视图数据和自动驾驶等业务中,企业沉淀了大量的图片、视频等媒体文件,在数据被使用过后仍然需要长期归档保存。通过深度归档存储,企业可以将这些数据存储在云上,仅在需要的时候恢复,降低存储成本和运维管理难度。 亚马逊: S3 Glacier Deep Archive 是 Amazon S3 成本最低的存储类,支持每年可能访问一两次的数据的长期保留和数字预留。它是为客户设计的 – 特别是那些监管严格的行业,如金融服务、医疗保健和公共部门 – 为了满足监管合规要求,将数据集保留 7—10 年或更长时间。S3 Glacier Deep Archive 还可用于备份和灾难恢复使用案例,是成本效益高、易于管理的磁带系统替代,无论磁带系统是本地库还是非本地服务都是如此。S3 Glacier Deep Archive 是 Amazon S3 Glacier 的补充,后者适合存档,其中会定期检索数据并且每隔几分钟可能需要一些数据。 通过官方介绍,大家可以 Get 到几个信息: ▶︎ 价格很便宜; ▶︎ 回热很低频; ▶︎ 云来帮你管理磁带。 为了让大家更深刻的理解深度归档的产品设计(是如何向磁带存储特性倾斜的),我们以腾讯云的产品报价和时效性为例,进行分析: 概括来讲: 让用户多写少读少删、存大块数据,如果真的要读,也尽量鼓励用户批量读! 2.3 极冷数据存储引擎:Berg 2.3.1 Berg 基本介绍 为什么叫 Berg? Berg 是 COS 团队设计并研发的面向磁带的极冷数据存储引擎。Berg 也是 COS 的一部分,意为冰山 (通常大家多想起 IceBerg 作为冰山示意,但在地质学词典里,Berg 本身就有冰山的意思,比如 Sugar Berg:多孔冰山)。 Berg 是国内首款支持纠删码的规模化商用的磁带存储引擎,也是腾讯从零开始完全自主设计和研发的全新存储系统。 Berg 与 COS 的关系? 本身 Berg 也是 COS 的一部分,如下图,COS 内部其实有很多子系统,Berg 并不能单独拿出来提供深度深度归档存储的服务。 2.3.2 Berg 面临的问题 想要设计出一个靠谱的磁带存储引擎,就要首先搞清楚,用户的需求是什么,硬件的特性是什么,这中间的 diff 就是引擎需要解决的问题。 用户的需求是上图左边,数据大小要支持 1Byte~50TB 的范围(因为对象存储本身支持的对象最大就到50TB),用户既可以直接上传,也可以把已经存在的低频归档的数据通过沉降的方式深度归档;用户需要时,可以把数据取回:按照云上的协议,当数据取回之后,数据需要在对象存储的标准存储准备好。当然,取回之后标准存储的那部分存储空间和读取也是额外收费的。最后,数据安全是底线,数据一定不能丢。 那么这些用户需求对于磁带库来说有什么问题呢?首先小文件性能会差,太大的又装不下,因为一盘磁带就12T,这也限制了磁带不能存储超过 12T 的数据。另外,磁带库本身写入读写流程非常繁琐,回热效率非常低(寻址时间可能高达3分钟),故障率也很高。而对于数据安全,则需要业务软件提供额外的副本机制了(前边讲过,磁带库软件自带的副本机制对资源浪费非常严重)。 为了打平这些问题,Berg 就需要在设计上有所注意。 2.3.3 Berg 的整体架构 Berg 是一个在离线混合系统,在线响应用户的沉降回热信令,离线处理数据。 Berg 有三个模块: ▶︎ Captain:最外层的调度层叫做 Captain,意味冰山上的指挥官,最主要的职责为信令的接收与再分配,起到一个调度的作用;Captain 依赖一个外部存储来持久化信令,当 Captain 崩溃时,信令不会丢失; ▶︎ IceWorker:真正执行任务的模块,它的职责主要是接受 Captain 分发的任务并执行。IceWorker 也是直接跟磁带库打交道的模块; ▶︎ IceCenter:资源总管,寓意是冰上的大本营。IceCenter 保存了所有磁带库的资源信息,间接对 IceWorker 进行指导。比如 IceWorker 如果要沉降数据,则需要向 IceCenter 进行空间申请,申请后才能向磁带库写数据。 总的执行流程如上图:YottaAccess 把读写信令发给 Captain,Captain 收到信令后持久化,然后分配任务给 IceWorker 去做,IceWorker 既跟磁带库交互,又跟 COS 的数据引擎 YottaStore 进行交互。 接下来在拆解上图,按照沉降、回热、修复、删除等等逻辑详细讲解整个的控制流、数据流是怎么样运行起来的。 2.4 数据沉降流程 对于 Berg 而言,数据沉降流程的起点是:Captain 接收到 ArchiveTask 信令之后,对收到的 Task 进行聚类和持久化;流程的终点是:完成数据沉降后通知 YottaAccess 任务执行成功。 整个流程如上图,不再赘述,这里阐述2个关键问题。 关键设计1: 用户数据沉降时聚类 为什么要聚类? 最主要的原因还是,想要这些数据回热的时候更快。前边讲过,磁带存储的寻址时间非常长,我们不希望回热发生的时候,用户的数据分散在磁带库/磁带的各个地方,因此 Captain 会尽最大努力按照用户的数据特征,进行一个聚类动作,让具备相同特征的数据尽可能让 IceWorker 一起拿走,然后连续的写到磁带上,实现一个局部连续性。这样某个用户回热时,用户的数据大概率也是具备局部连续性的,可以显著提高回热性能。另外,当用户删除的时候,也可以让空洞更少一些。 怎么聚类? 依然是两种思路,要想实现最精准的思路是:存到一个结构化存储里,或者就是存到大数据平台里,当 Captain 分发任务的时候,通过计算得到一个最优解。但是这个思路的实现代价太大了,有点用原子弹打蚊子的意思。本身 Berg 并不要这么高精度的聚类模型,无需过度设计。所以最后 Berg 就选择了一个轻量级的方式:实时聚类,Captain 收到信令时,立刻进行聚类计算,不同聚类模型对应不同文件,如文件过大则按策略进行切割。这样 Captain 给 IceWorker 分发任务的时候,按照聚类后的模型文件,一批一批递交给 IceWorker 去执行即可。 关键设计2:沉降时硬件故障处理 ▶︎ 可恢复性故障:通过任务调度系统后续重试失败的部分; ▶︎ 不可恢复性故障:Black 磁带库(组)的写请求,让后续的写请求流向健康的磁带库(组)。 这里引申一个问题,为什么遇到局部故障时,不暂时放弃某列去判定成功,后续再通过修复的方式补齐数据呢?主要原因在于,磁带库进行修复时需要进行读数据,读数据操作太重了,这个问题在后边介绍到修复逻辑时,会面临同样的问题。 关键设计3:任意 CodingSchema EC 支持 IceWorker 在进行 Block 数据切片时,通过抽象的 EcController 进行独立控制,可以支持任意 CodingSchema 的 EC 切片,灵活配置。 2.5 数据回热流程 回热流程的读写路径其实跟沉降非常类似,主要的策略点也在 Captain,对于用户发来的回热任务,并不是越快执行越好。Captain 如果是收到一个请求就立即执行一个请,硬件整体的吞吐是上不去的(由于回热数据的离散性,大量时间浪费在了磁带寻址上)。而磁带库的驱动器是稀缺资源,数据读写又是互斥的,磁带库处理读请求时,写请求就会暂停。因此,怎么样让磁带库更多的时间是在读数据,而不是在倒带,是 Berg 要解决的一个关键问题。 关键设计1:用户回热请求静置与聚合 ▶︎ 静置:根据任务紧急程度,数据需要有小时级的静置期,积累足够多的用户回热任务,并对回热任务进行聚合,使得磁带中 LBA 相近的数据尽可能一起回热; ▶︎ 聚合:数据沉降时,会生成与数据位置相关的信息 Hint,Hint 值越接近表示数据在磁带中的 LBA 地址越接近,或者表示两盘不同磁盘间隔的距离越近。静置期内,Captain 按照 Hint 值进行聚合,保障分发给 IceWorker 的任务都是尽可能在磁带中较为靠近的,而 IceWorker 再执行这一批任务时,磁带库平均寻址时间更短,从这个角度提高了整体回热性能(吞吐)。 关键设计2:自适应降级读 ▶︎ 故障规避:以 KxNy 的 CodingSchema 为例,如返回错误的列数 ▶︎ 长尾规避:如 N 列全部成功,则 IceWorker 在收到前 K 列请求时,即可开始进行读缓存和上传数据的操作,无需等待全部列取回成功,本次回热请求可以更快时间完成执行。 2.6 数据删除流程 数据删除流程相对简单,但也相对头疼的。这里 Berg 有两个关键设计来缓解删除带来的影响。 关键设计1:三级删除设计 ▶︎ 第一级删除:Captain 聚合删除。Captain 的删除是以 Block 为粒度进行删除的。当 Captain 收到用户 Object 级别的删除指令时,如果是小 Object,不会立刻让 IceWorker 执行,会把这个删除请求持久化起来,直到该小 Object 对应的 Block 全部收到删除执行时,Captain 会向 IceWorker 分发一个 Block 级别的删除任务。如果某个 Block 中的 Object 一直不被删除怎么办?这会导致该 Block 内其他空间永远无法执行删除,这里需要借助 Re-Compaction 的方式来对 Block 进行坍缩&再聚合的操作来消除空洞; ▶︎ 第二级删除:IceWorker 向磁带库发起删除。IceWorker 向磁带库发起删除后,磁带库会从文件系统中删掉这个 Block 对应的文件,但是实际上磁带上并没有真正删除该数据,需要进到第三级删除才算真的删除; ▶︎ 第三级删除:磁带库回收磁带空间。这是一个非常费性能的操作,需要2个驱动器配合,一个驱动器会把待回收空间的磁带整个读一遍,同时另一个驱动器在另一盘空磁带上把这些数据再写一遍,数据复制成功后,把前一盘磁带进行格式化,达到回收空间的目的。物理删除有两个弊端,一个是驱动器资源消耗较大(占用时间较长),另一个是寿命有损 (一盘磁带格式化300次左右会寿终正寝)。因此,第三级删除也是一个较为谨慎的运维操作。 关键设计2:IceWorker 两阶段删除 本身 IceWorker 向磁带库提交删除时(第二级删除),又有一个两阶段删除: ▶︎ 第一阶段: 对所有需要删除的列进行标删,直到全部成功为止,否则调度系统会定期重试; ▶︎ 第二阶段: 向磁带库发起删除数据请求。 两阶段删除的目的在于:一个 Block 在被删除时,需要 N 列全部删除成功,而由于网络抖动、硬件故障等情况,可能造成部分列删除失败。这时对于磁带库中残留的若干列,无法直接确认该 Block 是产生了数据丢失还是删除部分成功。而对于两阶段删除,残留的部分列可以看到特殊标记,我们就可以直接断定它是没删干净的垃圾数据,就可以放心的进行人工清理垃圾数据的措施了。 2.7 数据修复流程 数据修复流程主要是修复效率的问题。 对于磁带库而言,读数据性能开销是非常大的,并且驱动器读写是互斥的,这意味着驱动器读数据时无法再执行沉降任务;同时,对于 EC 而言,想要修复某一列数据,需要读大量其他的列的数据。这里以 KxNy 的 EC 为例;比如一盘12T 的磁带故障,则至少要读出剩下 K 盘磁带的数据(总数据量 K*12T),才能修复回这12T 的数据。 但是由于磁带故障率远低于 HDD,且当磁带故障时,大概率这一批磁带库已经写满了,只有回热请求,因此磁带数据修复对于业务的实际影响并不大。 2.8 数据安全性保障 传输链路:全链路 CRC 数据校验。 存储链路:存储数据 CRC 读时校验。 ▶︎ 写数据时,CRC 信息写入磁带; ▶︎ 读数据时,复算数据 CRC,并且与 Meta 中的 CRC 值对比,保障可第一时间发现问题。 离线数据校验: ▶︎ 业务低峰时定时发起数据校验,再次验证 CRC 信息。 磁带库元数据多级备份: ▶︎ 本地磁带元数据 Snapshot 备份; ▶︎ 远程文件系统定时 Snapshot 备份。 ObjectMeta 封存: ▶︎ 磁带中不仅存储 BergMeta,同时额外存储一份 COS 的 Meta 信息,极端情况下,可以只根据磁带中内容,可还原用户的 Object 信息。 03 写在最后的话 磁带库虽然能够显著降低存储成本,但是并不是所有的业务场景都适合搬到磁带上,本身磁带库的使用是有一定的业务和技术门槛的。从业务上而言,使用磁带库的场景必须是真正的冷数据的场景,用户数据具有保存时间长、低频读、低频删的特征,高频读写删会造成磁带介质寿命损害,只有真正的冷数据才能够做到“因地制宜”降成本;与此同时,由于磁带库通常单柜密度较高,当业务本身总数据量不大时,也不适宜使用磁带库。从技术上而言,使用磁带库具备一定的门槛,磁带库硬件不仅对于机房的温湿度环境要求较高,也需要机房具备一定电路网路改造能力,同时,在磁带库的使用上,设计和研发配套业务软件也需要一定的人力投入。对于一般用户而言,极冷数据存储的需求选择腾讯云 COS 深度归档服务显然性价比更高一些。 而磁带库在腾讯的大规模落地,也并不是一件易事,是 COS 团队、星星海、供应链、运管、商务、IDC、硬件运营、网平、OS、安平等多个兄弟团队之间相互协作的结果。 未来,随着全球冷数据的持续爆炸,以及磁带介质数据密度提升的巨大潜力,磁带库的前景依然充满想象力。我们期待着,数据存储成本持续降低的明天。 -End- 原创作者|杨骥
2023-09-06 09:25:50243阅读
预约专家解决问题
提交后鲸选型专家顾问会联系您
您的问题是?
您的联系方式
商务咨询
运营咨询
电话沟通