• 盘点一下数据库的误操作有哪些后悔药?

    1.不要用聊天工具发sql语句 通常开发人员写好sql语句之后,习惯通过聊天工具,比如:qq、钉钉、或者腾讯通等,发给团队老大或者DBA在线上环境执行。但由于有些聊天工具,对部分特殊字符会自动转义,而且有些消息由于内容太长,会被自动分成多条消息。 这样会导致团队老大或者DBA复制出来的sql不一定是正确的。 他们需要手动拼接成一条完整的sql,有时甚至需要把转义后的字符替换回以前的特殊字符,无形之中会浪费很多额外的时间。即使最终sql拼接好了,真正执行sql的人,心里一定很虚。 所以,强烈建议你把要在线上执行的sql语句用邮件发过去,可以避免使用聊天工具的一些弊端,减少一些误操作的机会。而且有个存档,方便今后有问题的时候回溯原因。很多聊天工具只保留最近7天的历史记录,邮件会保留更久一些。 别用聊天工具发sql语句! 别用聊天工具发sql语句! 别用聊天工具发sql语句! 重要的事情说三遍,它真的能减少一些误操作。 4.操作数据sql加limit 即使通过上面的select语句确认了sql语句没有问题,执行后影响的记录行数是对的。 也建议你不要立刻执行,建议在正在执行的时候,加上limit+select出的记录行数。例如: update order set status=1 where status=0 limit 1000; 假设有一次性更新的数据太多,所有相关记录行都会被锁住,造成长时间的锁等待,而造成用户请求超时。 此外,加limit可以避免一次性操作太多数据,对服务器的cpu造成影响。 还有一个最重要的原因:加limit后,操作数据的影响范围是完全可控的。 7.操作数据之前先做备份 如果只是修改了少量的数据,或者只执行了一两条sql语句,通过上面的修改人和修改时间字段,在需要回滚时,能快速的定位到正确的数据。 但是如果修改的记录行数很多,并且执行了多条sql,产生了很多修改时间。这时,你可能就要犯难了,没法一次性找出哪些数据需要回滚。 为了解决这类问题,可以将表做备份。 可以使用如下sql备份: create table order_bak_2021031721 like`order`;insert into order_bak_2021031721 select * from`order`; 先创建一张一模一样的表,然后把数据复制到新表中。 也可以简化成一条sql: create table order_bak_2021031722 select * from`order`; 创建表的同时复制数据到新表中。 此外,建议在表名中加上bak和时间,一方面是为了通过表名快速识别出哪些表是备份表,另一方面是为了备份多次时好做区分。因为有时需要执行多次sql才能把数据修复好,这种情况建议把表备份多次,如果出现异常,把数据回滚到最近的一次备份,可以节省很多重复操作的时间。 恢复数据时,把sql语句改成select语句,先在备份库找出相关数据,每条数据对应一条update语句,还原到老表中。 10.字段增删改的限制 很多时候,我们少不了对表字段的操作,比如:新加、修改、删除字段,但每种情况都不一样。 根据实际情况修改字段 修改字段要分为这三种情况: 3.修改字段长度 字段长度建议改大,通常情况下,不建议改小。如果一定要改小,要先确认该字段可能会出现的最大长度,避免insert操作时出现字段太长的异常。 此外,建议改大也需要设置一个合理的长度,避免数据库资源浪费。

    架构师社区 数据库 sql

  • 95后程序员业余帮人鉴定毒蘑菇,竟成百万粉丝的网络大V!

    免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 程序员 微博大V

  • java双重检查锁单例真的线程安全吗?

    相信大多数同学在面试当中都遇到过手写单例模式的题目,那么如何写一个完美的单例是面试者需要深究的问题,因为一个严谨的单例模式说不定就直接决定了面试结果,今天我们就要来讲讲看似线程安全的双重检查锁单例模式中可能会出现的指令重排问题。 双重检查锁单例模式 乍一看下面单例模式没啥问题,还加了同步锁保证线程安全,从表面上看确实看不出啥问题,当在同一时间多个线程同时执行该单例时就会出现JVM指令重排的问题,从而可能导致某一个线程获取的single对象未初始化对象。 //1:分配对象的内存空间 memory = allocate(); //3:设置instance指向刚分配的内存地址,此时对象还没被初始化 instance = memory; //2:初始化对象 ctorInstance(memory); 当A线程执行到第二步(3:设置instance指向刚分配的内存地址,此时对象还没被初始化)变量single指向内存地址之后就不为null了,此时B线程进入第一个if,由于single已经不为null了,那么就不会执行到同步代码块,而是直接返回未初始化对象的变量single,从而导致后续代码报错。 解决方案 问题也搞清楚了,接下来我们就来看下如何解决这个问题。 解决问题的关键就在于volatile关键字,我们来看下volatile关键字的特性。 可见性: - 写volatile修饰的变量时,JMM会把本地内存中值刷新到主内存 读 -  volatile修饰的变量时,JMM会设置本地内存无效 有序性: 要避免指令重排序,synchronized、lock作用的代码块自然是有序执行的,volatile关键字有效的禁止了指令重排序,实现了程序执行的有序性; 看完volatile关键字的特性之后我们应该就明白了,是volatile关键字禁止了指令重排序从而解决了指令重排的问题。 更正后的单例 对比上面单例,下面单例在私有静态变量single前面加了修饰符volatile能够防止JVM指令重排,从而解决了single对象可能出现成员变量未初始化的问题。

    架构师社区 java 单例模式 双重检查锁

  • 干货!如何设计实现一个通用的分布式事务框架?

    来源:

    架构师社区 分布式 事务

  • 胡晓明卸任蚂蚁CEO,下一站:阿里CEO?

    据媒体爆料,3月12日晚,蚂蚁金服首席执行官孙权(本名胡晓明)辞职。 孙权在内部邮件中表示:自己将辞去蚂蚁金服CEO一职,未来将负责蚂蚁生态社会公益事业关联项目。 随后#胡晓明辞任蚂蚁金服CEO#话题也登上了新浪微博热搜,引发网友讨论。 蚂蚁集团表示证实这一消息:情况属实,董事会基于个人意愿同意这一请求,感谢胡晓明对公司的努力与付出。 蚂蚁金服官方亦确认,胡去职后将由蚂蚁金服董事长井贤栋(Eric)兼任蚂蚁金服首席执行官。 胡晓明于2010年毕业于中欧国际工商学院,拥有高级管理人员工商管理硕士(EMBA)学位。2005年6月,他加入阿里巴巴集团,先后担任阿里云总裁、天弘基金董事长、网商银行董事长等职位。(据公开资料汇总) 在他的带领下,阿里云进入了全球云计算市场的前三名竞争。他还将中国自主研发的大规模计算操作系统——飞天,推广至全球舞台。 2018年11月29日出任蚂蚁金服总裁,2019年12月19日升任CEO,支付宝和蚂蚁金服多项业务的创建、发展与他密不可分。 2020年11月2日,蚂蚁金服IPO被紧急叫停。中国人民银行、中国银保监会、中国证监会、国家外汇管理局对蚂蚁集团实际控制人马云、董事长井贤栋、CEO胡晓明进行监管约谈。 11月3日,蚂蚁集团被上交所暂缓上市,随后H股也暂缓上市。 胡晓明(图源:网络) 当晚蚂蚁集团宣布,在金融管理部门的指导下,成立整改工作组,全面落实约谈意见,规范金融业务的经营和发展,继续沿着“稳妥创新、拥抱监管、服务实体、开放共赢”的十六字指导方针,继续提升普惠服务能力,助力经济和民生发展。 阿里集团曾在2021年第三季度财报中表示,由于最近中国金融科技监管环境的重大变化,蚂蚁集团正在制定整改方案,还须履行监管部门程序,因此其业务前景和上市计划存在重大的不确定性。 2019年,胡晓明重新回到蚂蚁金服。随后阿里宣布程立不再担任蚂蚁金服COO与CTO,转任阿里巴巴集团CTO,向张勇汇报,蚂蚁金服CEO交由胡晓明接任,向井贤栋汇报。 此番组织架构的调整,被认为是“孙权班子”搭建的开始。 与其它互联网集团的CTO相比,阿里巴巴的CTO不仅拥有各业务线技术体系的实权,更拥有实际业务运营的权力,从而连接和融合集团各个业务条线。 同时阿里新零售事业群负责人吴泽明向程立汇报,该事业群是阿里最重要的技术体系。 “孙权”班子的第二大核心是本地生活。阿里本地生活包含饿了么和口碑网,王磊为公司CEO,王磊上任伊始即宣布本地生活的运营与研发投入没有上限,张勇也提出阿里要赢得本地生活市场。 2020年底,吴泽明兼任阿里本地生活CTO,向阿里集团CTO程立和王磊双线汇报。所有变化,包括技术线和业务线的重心全部向胡晓明倾斜。 有人说,蚂蚁金服没有上市成功,胡晓明难辞其昝,必须为结果负责;从以上的组织变化来看,胡晓明只是在公益领域稍息片刻,用不了多少时间,可能将跃迁阿里集团的更高层,孙权将再战江湖。 编辑:万能的大雄 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 互联网 阿里巴巴 蚂蚁金服

  • 打开线程 | 进程 | 协程的大门

    不知从几何起,可能是大三那年的操作系统考试,也可能是刚经历完的秋招,这些概念总是迷迷糊糊,可能自己回答的和其他人的答复也差不多,并没有什么亮点,通常都会以:「我们换个题」的方式结束,有时候也挺尴尬的。我们不妨看看这样几个题应该怎么去回答 进程和线程是什么 进程和线程有什么区别 为什么有了进程又出现线程 内核态和用户态有啥不同 协程有什么特点 太多太多一系列的问题伴随到学习,工作的各个阶段,这些问题确实不怎么好回答,除非你真的理解到它的底层原理,否则很容易就把自己套进去,那么今天我们一起来看看这些问题都是怎么产生的,为什么总是会问这些题,开始吧 前言 进程线程协程 进程和线程 进程,平时我们打开一个播放器,开一个记事本,这些都是应用程序,一个软件的执行副本,这就是进程。从操作系统层面而言,进程是分配资源的基本单位,线程在很长时间被称为轻量级的进程,是程序执行的基本单位。 这样看来一个分配资源的基本单位,一个是程序执行的基本单元。以前面试的时候,我经常也就这样背给面试官了,当自己成为了面试官才发现这些孩子答案为啥都是这个,原来网上大部分的资料也就说了这些呢,直接这样死记硬背当然不行,让我们回到最初的计算机时代。 最初的计算机时代是什么样子呢 那个时代呀,程序员会将写好的程序放入闪存中,然后插入到机器里,通过电能推动芯片计算,那么芯片从闪存中取出指令,然后执行下一条执行,一旦闪存中的执行执行完了,计算机就要关机了 闪存时代 这在早期叫做单任务模型,也叫做作业(Job)。随着人们的需求越来越多,生活的多元化,慢慢出现了办公,聊天,游戏等,这个时候不得不在同一台计算机中来回的切换,人们就想要不通过线程和进程来处理这个问题 那是怎么处理的方式? 比如说一个游戏,启动后为一个进程,但是一个游戏场面的呈现需要图形的渲染,联网,这些操作不能相互的阻塞,如果阻塞了,卡起就很难受,总觉得这游戏怎么这么 low,我们希望它们能同时的运行,所以将其各个部分设计为线程,这就出现了一个进程有多个线程 既然一个进程有多个线程,这个资源分配如何处理? 启动一个游戏,首先需要存储这些游戏参数,所以需要内存资源,当进行攻击等动作时候,发出的各种动作指令需要计算,所以需要计算资源 CPU,还需要需要存储一些文件,所以还需要文件资源。由于早期的 OS 没有线程的概念,所以让各个进程采用分时的技术交替执行,通过管道等技术让各个进程进行通信。 这样看上去比较完美了,启动一个游戏后出来这么多进程,那么能不能启动游戏后,在这个进程下面安排一种技术,让其仅仅分配 CPU 资源呢,这就出现了线程 这个线程如何分配的? 线程概念被提出来以后,因为只分配了CPU 计算资源,所以也叫做轻量级的进程。通过操作系统来调度线程,也就是说操作系统创建进程后,“牵个线”,进程的入口程序被放在主线程中,看起来就感觉是操作系统在调度进程,实际上调度的是进程中线程,这种被操作系统直接调度的线程叫做内核级线程。 既然有内核级别线程,当然有用户级线程,相当于操作系统调度线程,主线程通过程序的方式实现子线程,这就是用户级线程,典型的即 Linux 中的 Phread API。既然说到内核态和用户态,我们来看看两者有什么作用 用户态线程 它完全是在用户空间创建,对于操作系统而言是不知情的,用户级线程的优势如下: 切换成本低:用户空间自己维护,不用走操作系统的调度 管理开销小:创建和销毁不用系统调用,系统调用所造成的上下文切换下文会讲解 用户态线程有什么缺点 与内核沟通成本大:因为这种线程大部分时间在用户空间,如果进行 IO 操作,很难利用内核的优势,且需要频繁的用户态和内核态的切换 线程之间的协作麻烦:想象两个线程 A 和 B需要通信,通信通常会涉及到 IO 操作,IO 操作涉及到系统调用,系统调用又要发生用户态和内核套的切换成本,难 操作系统无法针对线程的调度进行优化:如果一个进程的用户态线程阻塞了操作系统无法及时的发现和处理阻塞问题,它不会切换其他线程从而造成浪费 内核态线程 内核态线程执行在内核态,一般通过系统调用创造一个内核级线程,那么有哪些优点? 操作系统级优化:内核中的线程即使执行 IO 操作也不需要进行系统调用,一个内核阻塞可以让其他立即执行 充分利用多核优势:内核权限足够高,可以在多个 CPU 核心执行内核线程 内核级线程有什么缺点? 创建成本比较高:创建的时候需要使用系统调用即切换到内核态 切换成本高:切换的时候需要进行内核操作 扩展性差:因为一个内核管理,坑位有限,不可能数量太多 用户态线程和内核态线程的映射关系是怎样的呢 上面谈到用户态线程和内核态线程都有缺点,用户态线程创建成本低,不可以利用多核,而内核态线程创建成本高,虽可以利用多核,但是切换速度慢。所以,通常都会在内核中预留一些线程并反复使用这些线程,至此出现了以下几种映射关系 用户态和内核态映射之一--多对一 内核线程的创建成本既然高,那么我们就是多个用户态进程的多线程复用一个内核态线程,可是这样线程不能并发,所以此模型用户很少 用户态线程与内核态线程多对一 用户态和内核态映射之二--一对一 让每个用户态线程分配一个单独的内核态线程,每个用户态线程通过系统调用创建一个绑定的内核线程,这种模型能够并发执行,充分利用多核的优势,出名的 Windows NT即采用这种模型,但是如果线程比较多,对内核的压力就太大 用户态线程与内核态线程一对一 用户态和内核态映射之三--多对多 即 n 个用户态线程对应 m 个内核态线程。m通常小于等于n,m通常设置为核数,这种多对多的关系减少了内核线程且完成了并发,Linux即采用的这种模型 用户态线程与内核态线程多对一用户态线程与内核态线程多对多 一台计算机会启动很多进程,其数量当然是大于 CPU 数量,只好让 CPU 轮流的分配给它们,让我们产生了多任务同时执行的错觉,那有没有想过这些任务执行之前,CPU都会干啥? CPU 既然要执行它,势必会去了解从哪里加载它,又从哪里开始运行,也就是说,需要系统提前将它们设置好 CPU 寄存器和程序计数器 眼中的寄存器和程序计数器是什么? 它虽小不过威力却很大,速度很快的内存。而程序计数器用来记录正在执行指令的位置,这些CPU需要依赖的环境即 CPU 的上下文。上下文知道了,那么 CPU 的切换是不是就很好理解 将前一个任务的 CPU 上下文保存下来,加载新任务的上下文到寄存器和程序计数器中,然后跳转到程序计数器所指向的位置。根据任务的不同又分为进程的上下文和线程的上下文 进程的上下文 进程在用户空间运行的时候叫做用户态,陷入到内核空间叫做进程的内核态,如果用户态的进程想转变到内核态,则可以通过系统调用的方式完成。进程由内核调度,进程的切换发生在内核态 进程的上下文包含哪些数据? 既然进程的切换发生在内核态,那么进程的上下文不仅仅包括虚拟内存,栈,全局变量等用户空间资源,还包括了内核堆栈,寄存器等内核空间的状态 这里的保存上下文和恢复上下文也不是说免费的,需要内核在 CPU 上运行才能完成 上下文保存 线程上下文切换 看到这里,你肯定可以脱口而出两者的区别在于线程是调度的基本单位,而进程是资源拥有的基本单位。讲白了,内核的任务调度实际上调度的是线程,进程只是为线程提供虚拟内存,全局变量等资源,所以这样理解可能更好: 进程如果只有一个线程,那么认为进程就是线程 如果进程有多个线程,那么多个线程会共享相同的虚拟内存和全局变量等资源,上下文的切换不会影响这些资源 线程拥有自己的私有数据比如栈和寄存器,上下文切换的时候需要提前保存 综上,线程的上下文切换将分为两个部分 两个线程不属于同一个进程,那么资源不共享,所以切换过程就会涉及到进程的上下文切换 第二种情况即两个线程属于同一个进程。因为共享虚拟内存,所以切换的时候这些资源保持不动,只需要切换线程的私有数据等不共享的数据 这也从侧面表明了,进程内的线程切换比多进程间的切换会节省不少资源,这也是多线程逐渐替代多进程的一个优势 那么系统调用又是怎么执行的? 真的是一环接一环,是不是像极了面试,是的,我们对面试官的每一次回答都应该尽全力的让面试官上钩,问自己所能回答的问题不是。 如果用户态的程序要执行系统调用,则需要切换到内核态执行,这个过程如下图所示,一图胜千言 系统调用过程 既然分为了用户态和内核态,两者权限级别不尽相同,用户态的程序发起系统调用,因为涉及到权限问题,不得不牵扯到特权指令,所以就会通过中断的方式执行,即上图的 Trap。 发生中断以后,内核程序就开始执行,处理完成又要触发 Trap,切换到用户态的工作,这里又涉及到了中断,我们这篇就先简单了解下中断 中断做了什么? 我们以平时经常接触的键盘为例,当我们敲下键盘,主板收到按键后通知 CPU ,CPU 此时可能在忙处理其他程序,需要先中断当前执行的程序,然后将 PC 指针跳转到固定的位置,这就是一次中断的简单描述 可是我们不同的组合按键对应不同的事件,所以需要根据中断类型判断 PC 指针到底跳转到哪儿,中断类型的不同,PC 指针所执行的位置也就不同,因此进行了分类,这个类型呢我们称为中断识别码。CPU 通过 PC 指针知道需要跳转到哪个地址进行处理,这个地址叫做 中断向量表 举个例子,使用编号 8 表示按键中断类型A的识别码,编号 9 表示中断类型 B 的识别码。当中断发生的时候,对于CPU而言,是需要知道到底让 PC 指针指向哪个地址,这个地址就是中断向量 假设我们设置了 255 个中断,编号为 0 - 255,在 32 位机器中差不多需要 1k 的内存地址存储中断向量,这里的 1k 空间就是中断向量表。 因此,当 CPU 接收到中断,根据中断类型操作 PC 指针,找到中断向量,修改中断向量,插入指令实现跳转功能 进程和线程都出现了,那么怎么调度? 计算机资源有限,太多的进程消耗机器自然受不住,我们人也一样,胃也有限嘛,一顿不吃饿得慌,可是吃多了也会走路脚颤抖不是,所以聪明的计算机也会想办法来处理这个问题。两手一挥,既然我们的 CPU 的核数有限,要不咋们给每个进程分配一个时间片,排队一个个执行,超出给定的时间就直接让另一个进程执行如何 那时间片怎么分配? 假设此时有三个进程,进程1只需要 2 个时间片,进程2需要1个时间片,进程3需要3个时间片。进程1执行到一半的时候,累了,不想执行了,休息会(挂起),进程2执行,进程2一梭子就执行完了,进程3等不及了马上执行,执行三分之一后,进程1开始执行,这样循环根据时间片的执行方式即分时技术 分时技术 刚才有说到进程的状态,那么有哪些状态? 一个进程的周期一般会分为下面三种状态 就绪状态:进程创建好了会开始排队,这个时候叫做“就绪状态” 运行状态:当一切准备就绪,天时地利人和后开始执行,此时为“运行状态” 如果将时间片用完了会再次变为就绪状态 运行就绪 如果进程因为等待某个进程的完成,此时会进入阻塞状态 进程阻塞 为什么需要阻塞状态 我们想想,有的时候计算机会因为各种原因不能响应我们的请求,可能是因为等待磁盘,可能因为等待打印机,毕竟不会总是的及时的满足我们的需求,所以它这个时候通过中断告诉 CPU ,CPU 通过执行中断处理程序,将控制权给操作系统,操作系统随后将阻塞的进程状态修改为就绪状态,安排重新排队,再加上因为进程进入阻塞状态无事可做,但是又不能干瘪瘪的让他去排队(因为需要等待中断),所以进入到阻塞状态。 下面对以上所说的三种状态进行一个小结 就绪状态( Ready ):可运行,只不过其他进程在运行暂时停止 运行( Running):此时进程占用 CPU 阻塞状态( blo ck ): 此时可能因为等待相关事件(请求 IO/等待 IO 完成等) 而停止运行,此时即使把 CPU 控制权给它,仍然无法运行 其实,进程还有两种基本状态 创建状态 ( New ):进程刚被创建还没有提交时的状态,主要功能为分配和建立进程控制块等初始化工作。创建进程有两个阶段,第一个阶段为为新的进程创建必要的管理信息。第二个阶段为让进程进入就绪状态 终止状态 ( Exit ):进程退出的状态,即回收除了进程控制块以外的资源。也分为两个阶段,第一个阶段为等待操作系统进行善后处理,第二个阶段为释放主存 所以一共就包含了五个状态,为了更加直观,其变迁图如下 五种形态 Null---->创建状态:最初创建的第一个状态 创建状态----->就绪状态:进行一些列的初始化称为就绪状态 就绪状态----->运行状态:当操作系统调度就绪状态的进程并分配给 CPU 变为运行状态 运行状态------>结束状态:当进程完成相应任务或出错则被操作系统结束的状态 运行状态------>阻塞状态:运行状态的进程由于时间片用完,操作系统将进程更改为就绪状态 阻塞状态------->就绪状态:阻塞状态的进程等待某事件结束进入就绪状态 其实不是卖光子,实际上还有两种状态,分别是就绪挂起和阻塞挂起,那我们看看那这两者有啥不一样 挂起是一种行为,而阻塞是进程的状态 导致进程挂起的原因通常是因为内存不足或者用户的请求,进程的修改等,而进程的阻塞是进程正在等待某个事件发生,可能是等待资源或响应 挂起对应的是行为的激活,将外存中的进程掉入内存中,而处于阻塞状态的进程需要等待其他进程或系统唤醒 挂起属于被动行为,进程被迫从内存转移到外存,而进入阻塞为主动的行为 综上,现在咋们的进程图就变为了七种状态,如下 进程的七种状态 进程与线程的底层原理 上面我们了解了进程,线程的由来以及状态变迁,但是显然不能让我自如的了解进程和线程,至于其如何在内存表示等问题还是比较空虚的,所以我们继续往下看 进程和线程在内存中如何表示 在整个设计过程中,涉及了两张表,分别是进程表和线程表。其中进程表会记录进程在内存的位置,PID是多少,以及当前什么状态,内存给它分配了多大使用空间以及属于哪个用户,假设没有这张表,操作系统就不知道有哪些进程,也就更不清楚怎么去调度,就仿佛失去XXX,不知道了方向 进程表 尤其需要注意进程表这样几个部分 资源信息 资源信息会记录这个进程有哪些资源,比如进程和虚拟内存怎么映射,拥有哪些文件等 内存布局 内存的知识点太多,如果在这里写文章将会非常的长,所以打算单独使用一篇文章写。 在 Linux 中,操作系统采用虚拟内存管理技术,使得进程都拥有独立的虚拟内存空间,理由也比较直接,物理内存不够用且不安全(用户不能直接访问物理内存),使用虚拟内存不但更安全且可以使用比物理内存更大的地址空间。 另外,在 32 位的操作系统中,4GB 的进程地址空间分为两个部分,用户空间和内核空间,用户空间为 0~3G,内核地址空间占据 3~4G,用户不能直接操作内核空间虚拟地址,只有通过系统调用的方式访问内核空间。 操作系统会告诉进程如何使用内存,大概分为哪些区域以及每个区域做什么。简单描述下下图各个段的作用。 栈:系统自动分配释放,平时经常使用的函数参数值,局部变量,返回地址等就在此 堆:存放动态分配的数据,通常由开发人员自行管理,如果开发人员使用后不释放,那么程序结束后可能会被操作系统收回 数据段:存放的是全局变量和静态变量。其中初始化数据段(.data)存放显示初始化的全局变量和静态变量,未初始化数据段,此段通常也被称为BSS段(.bss),存放未进行显示初始化的全局变量和静态变量。 进程内存布局 描述信息 描述信息包含进程的唯一识别号,进程的名称以及用户等 除了给进程安排一张表以外,给线程也安排了一张表,这就是线程表。线程表也包含了一个 ID,这 ID 叫做 ThreadID,同时也会记录自己在不同阶段的状态,比如阻塞,运行,就绪。由于多个线程会共用 CPU 且需要不停的切换,所以需要记录程序计数器和寄存器的值。 说到了用户级的线程和内核级的线程,两者又是怎么个亲密关系 两者映射的关系如何去表示 可以想像在内核中有一个线程池,给予用户空间使用,每次用户级线程把程序计数器等传递过去,执行结束后,内核线程不销毁,等待下一个任务,从这里可以看出创建进程开销大、成本高;创建线程开销小,成本低。 这么多进程难道共用内存? 操作系统太多的进程,为了让他们各司其职,互不干扰,考虑为他们分配完全隔离的内存区域,即使程序内部读取相同的内存地址,但实际上他们的物理地址也不一样。就仿佛我在 X 座的 501 和你在 Y 座的501一样却不是一个房子,这就是地址空间 所以在正常的情况下 A 进程不能访问 B 进程的内存,除非你植入一个木马,恶意操作 B 进程的内存或者通过我们后面说的进程间通信的方式进行访问 那进程线程怎么切换的呢? 操作系统的大量进程需要来回的切换,保持有借有还再借不难的传统美德,每次切换之前需要先记录下当前寄存器值的内存地址,方便下次回到原位置继续执行。恢复执行的时候就从内存中读取,然后恢复状态执行即可 进程切换 为了详细的让大家理解这个过程,我将其拆分为下面几个步骤 操作系统感知到有个进程需要切换,先发出一个中断信号给 CPU ,让其停止当前进程 CPU 收到中断信号后,正在执行的进程会停止,好心的操作系统会想办法先保存当前的状态 操作系统接管中断后,执行一段汇编程序帮助寄存器之前进程的状态 当操作系统保存好状态后就会执行调度程序,让其决定下一个将要执行的进程 最后操作系统会执行下一个进程 进程与中断 中断以后如何恢复之前进程运行呢 上面说到操作系统会执行一段代码帮助进程恢复状态,其实现方式中,有一种方式即通过栈的先进后出的数据结构,所以对吧,大学中的基础课程真的好重要。 进程(线程)中断后,操作系统负责压栈关键数据(比如寄存器)。恢复执行时,操作系统负责出栈和恢复寄存器的值。 协程 第一次接触协程是一次自动驾驶项目中,一起干活的同事说这个库底层使用了协程,我一脸懵逼,啊?携程?准备收拾行李回家了?半天想过来了,其有个底层库使用了协程,当时还一脸懵逼,进程,线程就已经够折腾人了,怎么又来个协程,当时想着到时候面试官是不是又多了问问题的思路 什么是协程 协程和进程,线程的区别是什么 协程有什么优缺点 你们说头不秃怎么破?行嘛,为了生活,不,喜爱计算机,止不住学习的步伐,下面我们看看这个东西是什么 为什么需要协程? 我们在执行多任务的时候通常采用多线程的方式并发执行。我们以最近非常火热的电商促销茅台为例,不管茅台是在缓存中还是后端的数据,最开始的用户也就是10个,每当收到10条付款信息就开启10个线程去查询数据库,此时用户量少,马上就可返回,第二天增加到100人,使用100个线程去查询,感觉确实效果不错,加大促销力度,当同时出现1000个人的时候感觉到有点吃力了 增长的线程 1000-10000,看了前面的内容应该清楚创建销毁线程还是挺费资源的,假设每个线程占用 4M内存空间,那么10000个线程大概需要消耗 39G 内存,可是服务器也就 8G 内存。 此时的方案要么增加服务器要么提升代码效率。多个线程在进行作业的时候,难免会遇到某个线程等待 IO 的情况,此时会阻塞当前线程切换到其他线程,使得其他线程照常执行,线程少的时候没什么问题,当线程数量变多就会出现问题,线程数量的增加不仅占用非常多的内存空间且过多的线程的切换也会占用大量的系统时间 线程开销 此时就可以通过协程的方式解决这个问题 协程运行在线程之上,协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。即协程并没有增加线程的数量,而是在线程的基础上通过分时复用的方式运行多个协程,还有关键一点是它的切换发生在用户态,所有也不存在用户态到内核态的切换,代价更低 协程开销 类比上面,我们只需要启动 100 个线程,然后每个线程跑100个协程就可以完成上述同时处理10000个任务 那么协程在使用的过程中需要主要哪些内容呢 刚说协程运行于线程之上,如果线程等待 IO 的时候阻塞了,这时候会出现什么情况?其实操作系统主要关心线程,协程调用阻塞IO的时候,操作系统会让进程处于阻塞状态,此时当前的协程和绑定在线程之上的协程都会陷入阻塞而得不到调度,这样就很难受了 因此协程中,不能调用导致线程阻塞的操作,即协程最好了异步 IO 结合起来才能发挥最大的威力 怎么处理在协程中调用阻塞IO的操作呢 比较简答的思路是当调用阻塞 IO 的时候,重新启动一个线程去执行这个操作,等执行完成后,协程再去读取结果,这是不是和多线程很像 将系统 IO 进行封装,改为异步调用的方式,此时需要大量的工作,所以需要寄生于编程语言的原生支持 所以对于计算密集型的任务不太建议使用协程,计算机密集型的任务需要大量的线程切换,线程切换涉及太多的资源交换 总结 线程进程涉及的知识点好复杂,本文包含了线程,进程是什么,两者的区别,内核级线程与用户态线程,线程进程的上下文切换,系统调用的过程等一系列知识点,并没有对进程的调度等做详细的介绍,自己还需要多多补充知识。 不知不觉中这篇文章从素材的确认,关键字的过滤,上下文的衔接,画图,算下来差不多两周,不过在这个过程确实学习了不少新的知识点。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 进程 线程 协程

  • C++模版的本质

    我想知道上帝的構思,其他的都祇是細節。 ——爱因斯坦 Content C++模版的诞生 C++模板的实现 C++类模板(class template)技术 C++函数模板(function template)技术 C++模板的核心技术 C++模版应用场景 C++模版的展望 什么是参数化容器类? 首先C++是可以提供OOP(面向对象)范式编程的语言,所以支持类概念,类本身就是现实中一类事物的抽象,包括状态和对应的操作,打个比喻,大多数情况下我们谈论汽车,并不是指具体某辆汽车,而是某一类汽车(某个品牌),或者某一类车型的汽车。 所以我们设计汽车这个类的时候,各个汽车品牌的汽车大体框架(骨架)都差不多,都是4个轮子一个方向盘,而且操作基本上都是相同的,否则学车都要根据不同厂商汽车进行学习,所以我们可以用一个类来描述汽车的行为: class Car{public:Car(...);//other operations...private:Tire m_tire[4];Wheel m_wheel;//other attributes...}; 但这样设计扩展性不是很好,因为不同的品牌的车,可能方向盘形状不一样,轮胎外观不一样等等。所以要描述这些不同我们可能就会根据不同品牌去设计不同的类,这样类就会变得很多,就会产生下面的问题: 1. 代码冗余,会产生视觉复杂性,本身相似的东西比较多; 2. 用户很难通过配置去实现一辆车设计,不好定制化一个汽车; 3. 如果有其中一个属性有新的变化,就得实现一个新类,扩展代价太大。 这个时候,就希望这个类是可以参数化的(属性参数化),可以根据不同类型的参数进行属性配置,继而生成不同的类。类模板就应运而生了,类模板就是用来实现参数化的容器类。 C++模板的实现 C++标准委员会采用一套类似函数式语言的语法来设计C++模板,而且设计成图灵完备 (Turing-complete)(详见参考),我们可以把C++模板看成是一种新的语言,而且可以看成是函数式编程语言,只是设计依附在(借助于)C++其他基础语法上(类和函数)。 C++实现类模板(class template)技术 1.定义模板类,让每个模板类拥有模板签名。 templateclass X{...}; 上面的模板签名可以理解成:X; 主要包括模板参数和模板名字X(类名), 基本的语法可以参考《C++ Templates: The Complete Guide》,《C++ primer》等书籍。 模板参数在形式上主要包括四类,为什么会存在这些分类,主要是满足不同类对参数化的需求: type template parameter: 类型模板参数,以class或typename 标记;此类主要是解决朴实的参数化类的问题(上面描述的问题),也是模板设计的初衷。 non-type template parameter: 非类型模板参数,比如整型,布尔,枚举,指针,引用等;此类主要是提供给大小,长度等整型标量参数的控制,其次还提供参数算术运算能力,这些能力结合模板特化为模板提供了初始化值,条件判断,递归循环等能力,这些能力促使模板拥有图灵完备的计算能力。 template template parameter,模板参数是模板,此类参数需要依赖其他模板参数(作为自己的入参),然后生成新的模板参数,可以用于策略类的设计policy-base class。 parameter pack,C++11的变长模板参数,此类参数是C++11新增的,主要的目的是支持模板参数个数的动态变化,类似函数的变参,但有自己独有语法用于定义和解析(unpack),模板变参主要用于支持参数个数变化的类和函数,比如std::bind,可以绑定不同函数和对应参数,惰性执行,模板变参结合std::tuple就可以实现。 2. 在用模板类声明变量的地方,把模板实参(Arguments)(类型)带入模板类,然后按照匹配规则进行匹配,选择最佳匹配模板. 模板实参和形参类似于函数的形参和实参,模板实参只能是在编译时期确定的类型或者常量,C++17支持模板类实参推导。 3. 选好模板类之后,编译器会进行模板类实例化--记带入实际参数的类型或者常量自动生成代码,然后再进行通常的编译。 C++实现模板函数(function template)技术 模板函数实现技术和模板类形式上差不多: templateretType function_name(T t); 其中几个关键点: 函数模板的签名包括模板参数,返回值,函数名,函数参数, cv-qualifier; 函数模板编译顺序大致:名称查找(可能涉及参数依赖查找)->实参推导->模板实参替换(实例化,可能涉及 SFINAE)->函数重载决议->编译; 函数模板可以在实例化时候进行参数推导,必须知道每个模板的实参,但不必指定每个模板的实参。编译器会从函数实参推导缺失的模板实参。这发生在尝试调用函数、取函数模板地址时,和某些其他语境中; 函数模板在进行实例化后会进行函数重载解析, 此时的函数签名不包括返回值(template  argument deduction/substitution); 函数模板实例化过程中,参数推导不匹配所有的模板或者同时存在多个模板实例满足,或者函数重载决议有歧义等,实例化失败; 为了编译函数模板调用,编译器必须在非模板重载、模板重载和模板重载的特化间决定一个无歧义最佳的模板; C++模版的应用场景 1. C++ Library: 可以实现通用的容器(Containers)和算法(Algorithms),比如STL,Boost等,使用模板技术实现的迭代器(Iterators)和仿函数(Functors)可以很好让容器和算法可以自由搭配和更好的配合; 2. C++ type traits 通过模板技术,C++ type traits实现了一套操作类型特性的系统,C++是静态类型语言,在编译时候需要对变量和函数进行类型检查,这个时候type traits可以提供更多类型信息给编译器, 能让程序做出更多策略选择和特定类型的深度优化,Type Traits有助于编写通用、可复用的代码。 C++创始人对traits的理解: "Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine "policy" or "implementation details". - Bjarne Stroustrup 而这个技术,在其他语言也有类似实现,比如go的interface,java的注解,反射机制等。 3. Template metaprogramming-TMP 随着模板技术发展,模板元编程逐渐被人们发掘出来,metaprogramming本意是进行源代码生成的编程(代码生成器),同时也是对编程本身的一种更高级的抽象,好比我们元认知这些概念,就是对学习本身更高级的抽象。TMP通过模板实现一套“新的语言”(条件,递归,初始化,变量等),由于模板是图灵完备,理论上可以实现任何可计算编程,把本来在运行期实现部分功能可以移到编译期实现,节省运行时开销,比如进行循环展开,量纲分析等。 4. Policy-Based Class Design C++ Policy class design 首见于 Andrei Alexandrescu 出版的 《Modern C++ Design》一书以及他在C/C++ Users Journal杂志专栏 Generic,参考wiki。通过把不同策略设计成独立的类,然后通过模板参数对主类进行配置,通常policy-base class design采用继承方式去实现,这要求每个策略在设计的时候要相互独立正交。STL还结合CRTP (Curiously recurring template pattern)等模板技术,实现类似动态多态(虚函数)的静态多态,减少运行开销。 5. Generic Programming(泛型编程) 由于模板这种对类型强有力的抽象能力,能让容器和算法更加通用,这一系列的编程手法,慢慢引申出一种新的编程范式:泛型编程。泛型编程是对类型的抽象接口进行编程,STL库就是泛型编程经典范例。

    C语言与CPP编程 模板 C

  • Microchip推出可防止GPS干扰和欺骗的新版SyncServerÒ S600系列时间服务器

    Microchip推出可防止GPS干扰和欺骗的新版SyncServerÒ S600系列时间服务器

    任务关键型网络和其他重要的企业基础设施需要不断从网络时间服务器接收到准确的时间信息,才能保持可靠运行。但这些服务器易遭受到全球定位系统(GPS)干扰和欺骗这类的网络安全威胁。Microchip Technology Inc.(美国微芯科技公司)今日推出一项解决方案,通过将其BlueSky技术信号异常检测软件集成到SyncServer S600系列网络时间服务器和仪器中,成功解决这一难题。 Microchip是首家将GPS干扰和欺骗检测和保护以及本地射频(RF)数据记录和分析完全集成在一个时间服务器内的公司。通过 SyncServer S600系列Stratum 1仪器与基于BlueSky技术的智能干扰和欺骗检测器协同工作,可以连续监视本地GPS卫星星座是否正常,同时检查GPS和本地RF信号的完整性,以确保有效性。 如果检测到异常,该解决方案将发送警报,如有必要,可以将SyncServer仪器切换至备用时间源或内部振荡器, 从而保护持续的时序输出,同时确保在重要网络和业务运营中的时序衰减保持最小且可预测。新推出的解决方案可广泛应用于银行和股票交易、电力设施、航空航天和国防等重要行业。 SyncServer BlueSky技术能针对GPS干扰和欺骗提供连续检测和保护,包括一套完整的日志记录、制图和测量工具,用于描述随时间变化的本地GPS卫星信号以及本地RF事件。这有助于实现关联、故障排除,同时识别和纠正局部异常,其中一些局部异常可能与消费类电子产品或附近的RF信号广播有关。该解决方案可通过SyncServer v4.1软件版本提供(可选),该版本提供了Microchip经过验证的BlueSky GNSS防火墙解决方案中的精选功能,一般用于第三方GPS接收器和关键基础设施。

    Microchip GPS Microchip 时间服务器

  • 微博千万级规模高性能高并发的网络架构设计

    架构以及我理解中架构的本质 在开始谈我对架构本质的理解之前,先谈谈自己的个人见解,千万级规模的网站感觉数量级是非常大的,对这个数量级我们战略上要重视它 ,战术上又要藐视它。 先举个例子感受一下千万级到底是什么数量级?现在的优步(Uber),从媒体公布的信息看,它每天接单量平均在百万左右,假如每天有10个小时的服务时间,平均QPS只有30左右。 对于一个后台服务器,单机的平均QPS可以到达800-1000,单独看写的业务量很简单 。为什么我们又不能说轻视它? 第一,我们看它的数据存储,每天一百万的话,一年数据量的规模是多少?其次,刚才说的订单量,每一个订单要推送给附近的司机、司机要并发抢单,后面业务场景的访问量往往是前者的上百倍,轻松就超过上亿级别了。 今天我想从架构的本质谈起之后,希望大家理解在做一些建构设计的时候,它的出发点以及它解决的问题是什么。 架构,刚开始的解释是我从知乎上看到的。什么是架构?有人讲, 说架构并不是一 个很悬乎的 东西 , 实际 上就是一个架子 ,放一些 业务 和算法,跟我们的生活中的晾衣架很像。更抽象一点,说架构其实是对我们重复性业务的抽象和我们未来业务拓展的前瞻,强调过去的经验和你对整个行业的预见。 我们要想做一个架构的话需要哪些能力?我觉得最重要的是架构师一个最重要的能力就是你要有战略分解能力。这个怎么来看呢: 第一,你必须要有抽象的能力,抽象的能力最基本就是去重,去重在整个架构中体现在方方面面,从定义一个函数,到定义一个类,到提供的一个服务,以及模板,背后都是要去重提高可复用率。 第二, 分类能力。做软件需要做对象的解耦,要定义对象的属性和方法,做分布式系统的时候要做服务的拆分和模块化,要定义服务的接口和规范。 第三, 算法(性能),它的价值体现在提升系统的性能,所有性能的提升,最终都会落到CPU,内存,IO和网络这4大块上。 这一页PPT举了一些例子来更深入的理解常见技术背后的架构理念。 第一个例子,在分布式系统我们会做 MySQL分库分表,我们要从不同的库和表中读取数据,这样的抽象最直观就是使用模板,因为绝大多数SQL语义是相同的,除了路由到哪个库哪个表,如果不使用Proxy中间件,模板就是性价比最高的方法。 第二看一下加速网络的CDN,它是做速度方面的性能提升,刚才我们也提到从CPU、内存、IO、网络四个方面来考虑,CDN本质上一个是做网络智能调度优化,另一个是多级缓存优化。 第三个看一下服务化,刚才已经提到了,各个大网站转型过程中一定会做服务化,其实它就是做抽象和做服务的拆分。第四个看一下消息队列,本质上还是做分类,只不过不是两个边际清晰的类,而是把两个边际不清晰的子系统通过队列解构并且异步化。 新浪微博整体架构是什么样的 接下我们看一下微博整体架构,到一定量级的系统整个架构都会变成三层,客户端包括WEB、安卓和IOS,这里就不说了。 接着还都会有一个接口层, 有三个主要作用: 第一个作用,要做 安全隔离,因为前端节点都是直接和用户交互,需要防范各种恶意攻击; 第二个还充当着一个 流量控制的作用,大家知道,在2014年春节的时候,微信红包,每分钟8亿多次的请求,其实真正到它后台的请求量,只有十万左右的数量级(这里的数据可能不准),剩余的流量在接口层就被挡住了; 第三,我们看对PC端和移动端的需求不一样的,所以我们可以进行拆分。接口层之后是后台,可以看到微博后台有三大块: 一个是平台服务, 第二, 搜索, 第三, 大数据。 到了后台的各种服务其实都是处理的数据。像平台的业务部门,做的就是数据存储和读取,对搜索来说做的是 数据的检索,对大数据来说是做的数据的挖掘。微博其实和淘宝是很类似 微博其实和淘宝是很类似的。一般来说,第一代架构,基本上能支撑到用户到 百万 级别,到第二代架构基本能支撑到 千万 级别都没什么问题,当业务规模到 亿级别时,需要第三代的架构。 从LAMP的架构到面向服务的架构,有几个地方是非常难的,首先不可能在第一代基础上通过简单的修修补补满足用户量快速增长的,同时线上业务又不能停,这是我们常说的在飞机上换引擎的问题。 前两天我有一个朋友问我,说他在内部推行服务化的时候,把一个模块服务化做完了,其他部门就是不接。我建议在做服务化的时候,首先更多是偏向业务的梳理,同时要找准一个很好的切入点,既有架构和服务化上的提升,业务方也要有收益,比如提升性能或者降低维护成本同时升级过程要平滑,建议开始从原子化服务切入,比如基础的用户服务, 基础的短消息服务,基础的推送服务。 第二,就是可 以做无状 态 服 务,后面会详细讲,还有数据量大了后需要做数据Sharding,后面会将。第三代 架构 要解决的 问题,就是用户量和业务趋于稳步增加(相对爆发期的指数级增长),更多考虑技术框架的稳定性, 提升系统整体的性能,降低成本,还有对整个系统监控的完善和升级。 大型网站的系统架构是如何演变的 我们通过通过数据看一下它的挑战,PV是在10亿级别,QPS在百万,数据量在千亿级别。我们可用性,就是SLA要求4个9,接口响应最多不能超过150毫秒,线上所有的故障必须得在5分钟内解决完。 如果说5分钟没处理呢?那会影响你年终的绩效考核。2015年微博DAU已经过亿。我们系统有上百个微服务,每周会有两次的常规上线和不限次数的紧急上线。我们的挑战都一样,就是数据量,bigger and bigger,用户体验是faster and faster,业务是more and more。 互联网业务更多是产品体验驱动,技术在产品体验上最有效的贡献 ,就是你的性能越来越好 。每次降低加载一个页面的时间,都可以间接的降低这个页面上用户的流失率。 微博的技术挑战和正交分解法解析架构 下面看一下第三代的架构图以及我们怎么用正交分解法阐述。 我们可以看到我们从两个维度,横轴和纵轴可以看到。一个维度是水平的分层 拆分,第二从垂直的维度会做拆分。水平的维度从接口层、到服务层到数据存储层。垂直怎么拆分,会用业务架构、技术架构、监控平台、服务治理等等来处理。 我相信到第二代的时候很多架构已经有了业务架构和技术架构的拆分。我们看一下, 接口层有feed、用户关系、通讯接口;服务层,SOA里有基层服务、原子服务和组合服务,在微博我们只有原子服务和组合服务。原子服务不依赖于任何其他服务,组合服务由几个原子服务和自己的业务逻辑构建而成 ,资源层负责海量数据的存储(后面例子会详细讲)。 技术框架解决独立于业务的海量高并发场景下的技术难题,由众多的技术组件共同构建而成 。在接口层,微博使用JERSY框架,帮助你做参数的解析,参数的验证,序列化和反序列化;资源层,主要是缓存、DB相关的各类组件,比如Cache组件和对象库组件。监 控平台和服 务 治理 ,完成系统服务的像素级监控,对分布式系统做提前诊断、预警以及治理。包含了SLA规则的制定、服务监控、服务调用链监控、流量监控、错误异常监控、线上灰度发布上线系统、线上扩容缩容调度系统等。 下面我们讲一下常见的设计原则。 第一个,首先是系统架构三个利器: 一个, 我们RPC服务组件 (这里不讲了), 第二个,我们消息中间件 。消息中间件起的作用:可以把两个模块之间的交互异步化,其次可以把不均匀请求流量输出为匀速的输出流量,所以说消息中间件异步化解耦和流量削峰的利器。 第三个是配置管理,它是代码级灰度发布以及保障系统降级的利器。 第二个 , 无状态接口层最重要的就是无状态。 我们在电商网站购物,在这个过程中很多情况下是有状态的,比如我浏览了哪些商品,为什么大家又常说接口层是无状态的,其实我们把状态从接口层剥离到了数据层。像用户在电商网站购物,选了几件商品,到了哪一步,接口无状态后,状态要么放在缓存中,要么放在数据库中, 其实它并不是没有状 态 , 只是在这个过程中我们要把一些有状态的东西抽离出来到了数据层。 第三个, 数据 层 比服 务层 更需要 设计,这是一条非常重要的经验。对于服务层来说,可以拿PHP写,明天你可以拿JAVA来写,但是如果你的数据结构开始设计不合理,将来数据结构的改变会花费你数倍的代价,老的数据格式向新的数据格式迁移会让你痛不欲生,既有工作量上的,又有数据迁移跨越的时间周期,有一些甚至需要半年以上。 第四,物理结构与逻辑结构的映射,上一张图看到两个维度切成十二个区间,每个区间代表一个技术领域,这个可以看做我们的逻辑结构。另外,不论后台还是应用层的开发团队,一般都会分几个垂直的业务组加上一个基础技术架构组,这就是从物理组织架构到逻辑的技术架构的完美的映射,精细化团队分工,有利于提高沟通协作的效率 。 第五, www .sanhao.com 的访问过程,我们这个架构图里没有涉及到的,举个例子,比如当你在浏览器输入www.sanhao网址的时候,这个请求在接口层之前发生了什么?首先会查看你本机DNS以及DNS服务,查找域名对应的IP地址,然后发送HTTP请求过去。这个请求首先会到前端的VIP地址(公网服务IP地址),VIP之后还要经过负载均衡器(Nginx服务器),之后才到你的应用接口层。在接口层之前发生了这么多事,可能有用户报一个问题的时候,你通过在接口层查日志根本发现不了问题,原因就是问题可能发生在到达接口层之前了。 第六,我们说分布式系统,它最终的瓶颈会落在哪里呢?前端时间有一个网友跟我讨论的时候,说他们的系统遇到了一个瓶颈, 查遍了CPU,内存,网络,存储,都没有问题。我说你再查一遍,因为最终你不论用上千台服务器还是上万台服务器,最终系统出瓶颈的一定会落在某一台机(可能是叶子节点也可能是核心的节点),一定落在CPU、内存、存储和网络上,最后查出来问题出在一台服务器的网卡带宽上。 微博多级双机房缓存架构 接下来我们看一下微博的Feed多级缓存。我们做业务的时候,经常很少做业务分析,技术大会上的分享又都偏向技术架构。其实大家更多的日常工作是需要花费更多时间在业务优化上。 这张图是统计微博的信息流前几页的访问比例,像前三页占了97%,在做缓存设计的时候,我们最多只存最近的M条数据。这里强调的就是做系统设计 要基于用 户 的 场 景 , 越细致越好 。 举了一个例子,大家都会用电商,电商在双十一会做全国范围内的活动,他们做设计的时候也会考虑场景的,一个就是购物车,我曾经跟相关开发讨论过,购物车是在双十一之前用户的访问量非常大,就是不停地往里加商品。在真正到双十一那天他不会往购物车加东西了,但是他会频繁的浏览购物车。针对这个场景,活动之前重点设计优化购物车的写场景, 活动开始后优化购物车的读场景。 你看到的微博是由哪些部分聚合而成的呢?最右边的是Feed,就是微博所有关注的人,他们的微博所组成的。微博我们会按照时间顺序把所有关注人的顺序做一个排序。 随着业务的发展,除了跟时间序相关的微博还有非时间序的微博,就是会有广告的要求,增加一些广告,还有粉丝头条,就是拿钱买的,热门微博,都会插在其中。分发控制,就是说和一些推荐相关的,我推荐一些相关的好友的微博,我推荐一些你可能没有读过的微博,我推荐一些其他类型的微博。 当然对非时序的微博和分发控制微博,实际会起多个并行的程序来读取,最后同步做统一的聚合。这里稍微分享一下, 从SNS社交领域来看,国内现在做的比较好的三个信息流: 微博 是 基于弱关系的媒体信息流 ; 朋友圈是基于 强 关系的信息流 ; 另外一个做的比 较 好的就是今日 头 条 , 它并不是基于关系来构建信息流 , 而是基于 兴趣和相关性的个性化推荐 信息流 。 信息流的聚合,体现在很多很多的产品之中,除了SNS,电商里也有信息流的聚合的影子。比如搜索一个商品后出来的列表页,它的信息流基本由几部分组成:第一,打广告的;第二个,做一些推荐,热门的商品,其次,才是关键字相关的搜索结果。信息流 开始的时候很简单 , 但是到后期会发现 , 你的这 个流如何做控制分发 , 非常复杂, 微博在最近一两年一直在做这样的工作。 刚才我们是从业务上分析,那么技术上怎么解决高并发,高性能的问题?微博访问量很大的时候,底层存储是用MySQL数据库,当然也会有其他的。 对于查询请求量大的时候,大家知道一定有缓存,可以复用可重用的计算结果。可以看到,发一条微博,我有很多粉丝,他们都会来看我发的内容,所以 微博是最适合使用缓存的系统,微博的读写比例基本在几十比一。 微博使用了双层缓存,上面是L1,每个L1上都是一组(包含4-6台机器),左边的框相当于一个机房,右边又是一个机房。在这个系统中L1缓存所起的作用是什么? 首先,L1 缓存增加整个系 统 的 QPS, 其次以低成本灵活扩容的方式增加系统 的 带宽 。想象一个极端场景,只有一篇博文,但是它的访问量无限增长,其实我们不需要影响L2缓存,因为它的内容存储的量小,但它就是访问量大。这种场景下,你就需要使用L1来扩容提升QPS和带宽瓶颈。 另外一个场景,就是L2级缓存发生作用,比如我有一千万个用户,去访问的是一百万个用户的微博 ,这个时候,他不只是说你的吞吐量和访问带宽,就是你要缓存的博文的内容也很多了,这个时候你要考虑缓存的容量, 第二 级缓 存更多的是从容量上来 规划,保证请求以较小的比例 穿透到后端的数据库中 ,根据你的用户模型你可以估出来,到底有百分之多少的请求不能穿透到DB, 评估这个容量之后,才能更好的评估DB需要多少库,需要承担多大的访问的压力。 另外,我们看双机房的话,左边一个,右边一个。两个机房是互为主备 , 或者互 为热备 。如果两个用户在不同地域,他们访问两个不同机房的时候,假设用户从IDC1过来,因为就近原理,他会访问L1,没有的话才会跑到Master,当在IDC1没找到的时候才会跑到IDC2来找。 同时有用户从IDC2访问,也会有请求从L1和Master返回或者到IDC1去查找。IDC1 和 IDC2 ,两个机房都有全量的用户数据,同时在线提供服务,但是缓存查询又遵循最近访问原理。 还有哪些多级缓存的例子呢?CDN是典型的多级缓存。CDN在国内各个地区做了很多节点,比如在杭州市部署一个节点时,在机房里肯定不止一台机器,那么对于一个地区来说,只有几台服务器到源站回源,其他节点都到这几台服务器回源即可,这么看CDN至少也有两级。 Local Cache+ 分布式缓存,这也是常见的一种策略。有一种场景,分布式缓存并不适用, 比如单点资源的爆发性峰值流量,这个时候使用Local Cache + 分布式缓存,Local Cache在应用服务器上用很小的内存资源挡住少量的极端峰值流量,长尾的流量仍然访问分布式缓存,这样的Hybrid缓存架构通过复用众多的应用服务器节点,降低了系统的整体成本。 我们来看一下Feed的存储架构,微博的博文主要存在MySQL中。首先来看内容表,这个比较简单,每条内容一个索引,每天建一张表,其次看索引表,一共建了两级索引。首先想象一下用户场景,大部分用户刷微博的时候,看的是他关注所有人的微博,然后按时间来排序。 仔细分析发现在这个场景下, 跟一个用户的自己的相关性很小了。所以在一级索引的时候会先根据关注的用户,取他们的前条微博ID,然后聚合排序。我们在做哈希(分库分表)的时候,同时考虑了按照UID哈希和按照时间维度。很业务和时间相关性很高的,今天的热点新闻,明天就没热度了,数据的冷热非常明显,这种场景就需要按照时间维度做分表, 首先冷热数据做了分离(可以对冷热数据采用不同的存储方案来降低成本),其次, 很容止控制我数据库表的爆炸。像微博如果只按照用户维度区分,那么这个用户所有数据都在一张表里,这张表就是无限增长的,时间长了查询会越来越慢。 二级索引,是我们里面一个比较特殊的场景,就是我要快速找到这个人所要发布的某一时段的微博时,通过二级索引快速定位。 分布式服务追踪系统 分布式追踪服务系统,当系统到千万级以后的时候,越来越庞杂,所解决的问题更偏向稳定性,性能和监控。刚才说用户只要有一个请求过来,你可以依赖你的服务RPC1、RPC2,你会发现RPC2又依赖RPC3、RPC4。分布式服务的时候一个痛点,就是说一个请求从用户过来之后,在后台不同的机器之间不停的调用并返回。 当你发现一个问题的时候,这些日志落在不同的机器上,你也不知道问题到底出在哪儿,各个服务之间互相隔离,互相之间没有建立关联。所以导致排查问题基本没有任何手段,就是出了问题没法儿解决。 我们要解决的问题,我们刚才说日志互相隔离,我们就要把它建立联系。建立联系我们就有一个请求ID,然后结合RPC框架, 服务治理功能。假 设请求从客户端过来,其中包含一个ID 101,到服务A时仍然带有ID 101,然后调用RPC1的时候也会标识这是101 ,所以需要一个唯一的请求ID标识递归迭代的传递到每一个相关节点。 第二个,你做的时候,你不能说每个地方都加,对业务系统来说需要一个框架来完成这个工作, 这个框架要对业务系统是最低侵入原则 , 用 JAVA 的 话 就可以用 AOP,要做到零侵入的原则,就是对所有相关的中间件打点,从接口层组件(HTTP Client、HTTP Server)至到服务层组件(RPC Client、RPC Server),还有数据访问中间件的,这样业务系统只需要少量的配置信息就可以实现全链路监控 。 为什么要用日志?服务化以后,每个服务可以用不同的开发语言, 考虑多种开发语言的兼容性 , 内部定义标准化的日志是唯一且有效的办法。 最后,如何构建基于GPS导航的路况监控?我们刚才讲分布式服务追踪。 分布式服务追踪能解决的问题, 如果单一用户发现问题后 ,可以通过请求 ID 快速找到发生问题的节点在什么,但是并没有解决如何发现问题。 我们看现实中比较容易理解的道路监控,每辆车有GPS定位,我想看北京哪儿拥堵的时候,怎么做?第一个 , 你肯定要知道每个车在什么位置,它走到哪儿了。 其实可以说每个车上只要有一个标识,加上每一次流动的信息,就可以看到每个车流的位置和方向。其次如何做监控和报警,我们怎么能了解道路的流量状况和负载,并及时报警。 我们要定义这条街道多宽多高,单位时间可以通行多少辆车,这就是道路的容量。有了道路容量,再有道路的实时流量,我们就可以基于实习路况做预警? 对应于分布式系统的话如何构建? 第一 , 你要定义每个服务节点它的 SLA A 是多少 ?SLA可以从系统的CPU占用率、内存占用率、磁盘占用率、QPS请求数等来定义,相当于定义系统的容量。第二个 , 统计线上动态的流量,你要知道服务的平均QPS、最低QPS和最大QPS,有了流量和容量,就可以对系统做全面的监控和报警。 刚才讲的是理论,实际情况肯定比这个复杂。微博在春节的时候做许多活动,必须保障系统稳定,理论上你只要定义容量和流量就可以。但实际远远不行,为什么?有技术的因素,有人为的因素,因为不同的开发定义的流量和容量指标有主观性,很难全局量化标准,所以真正流量来了以后,你预先评估的系统瓶颈往往不正确。 实际中我们在春节前主要采取了三个措施: 第一,最简单的就是有降级的预案,流量超过系统容量后,先把哪些功能砍掉,需要有明确的优先级 。 第二个, 线上全链路压测,就是把现在的流量放大到我们平常流量的五倍甚至十倍(比如下线一半的服务器,缩容而不是扩容),看看系统瓶颈最先发生在哪里。我们之前有一些例子,推测系统数据库会先出现瓶颈,但是实测发现是前端的程序先遇到瓶颈。 第三,搭建在线 Docker 集群 ,所有业务共享备用的 Docker集群资源,这样可以极大的避免每个业务都预留资源,但是实际上流量没有增长造成的浪费。 总结 接下来说的是如何不停的学习和提升,这里以Java语言为例, 首先,一定要 理解 JAVA; 第二步,JAVA完了以后,一定要理解JVM; 其次,还要理解操作系统; 再次还是要了解一下DesignPattern,这将告诉你怎么把过去的经验抽象沉淀供将来借鉴; 还要学习 TCP/IP、 分布式系统、数据结构和算法。 最后就是我想说的就是今天我所说的可能一切都是错的!大家通过不停的学习、练习和总结, 形成自己的一套架构设计原则和方法,谢谢大家。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 网络架构 高并发

  • 对于注册中心,ZooKeeper、Eureka 哪个更合适?

    简介 对比 ZooKeeper Eureka 总结 简介 Eureka本身是Netflix开源的一款提供服务注册和发现的产品,并且提供了相应的Java封装。在它的实现中,节点之间相互平等,部分注册中心的节点挂掉也不会对集群造成影响,即使集群只剩一个节点存活,也可以正常提供发现服务。哪怕是所有的服务注册节点都挂了,Eureka Clients(客户端)上也会缓存服务调用的信息。这就保证了我们微服务之间的互相调用足够健壮。 ZooKeeper主要为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。曾经是Hadoop项目中的一个子项目,用来控制集群中的数据,目前已升级为独立的顶级项目。很多场景下也用它作为Service发现服务解决方案。 对比 在分布式系统中有个著名的CAP定理(C-数据一致性;A-服务可用性;P-服务对网络分区故障的容错性,这三个特性在任何分布式系统中不能同时满足,最多同时满足两个); ZooKeeper ZooKeeper是基于CP来设计的,即任何时刻对ZooKeeper的访问请求能得到一致的数据结果,同时系统对网络分割具备容错性,但是它不能保证每次服务请求的可用性。从实际情况来分析,在使用ZooKeeper获取服务列表时,如果zookeeper正在选主,或者ZooKeeper集群中半数以上机器不可用,那么将无法获得数据。所以说,ZooKeeper不能保证服务可用性。 诚然,在大多数分布式环境中,尤其是涉及到数据存储的场景,数据一致性应该是首先被保证的,这也是zookeeper设计成CP的原因。但是对于服务发现场景来说,情况就不太一样了:针对同一个服务,即使注册中心的不同节点保存的服务提供者信息不尽相同,也并不会造成灾难性的后果。因为对于服务消费者来说,能消费才是最重要的——拿到可能不正确的服务实例信息后尝试消费一下,也好过因为无法获取实例信息而不去消费。(尝试一下可以快速失败,之后可以更新配置并重试)所以,对于服务发现而言,可用性比数据一致性更加重要——AP胜过CP。 Eureka 而Spring Cloud Netflix在设计Eureka时遵守的就是AP原则。Eureka Server也可以运行多个实例来构建集群,解决单点问题,但不同于ZooKeeper的选举leader的过程,Eureka Server采用的是Peer to Peer对等通信。这是一种去中心化的架构,无master/slave区分,每一个Peer都是对等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的serviceUrl指向其他节点。每个节点都可被视为其他节点的副本。 如果某台Eureka Server宕机,Eureka Client的请求会自动切换到新的Eureka Server节点,当宕机的服务器重新恢复后,Eureka会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行replicateToPeer(节点间复制)操作,将请求复制到其他Eureka Server当前所知的所有节点中。 一个新的Eureka Server节点启动后,会首先尝试从邻近节点获取所有实例注册表信息,完成初始化。Eureka Server通过getEurekaServiceUrls()方法获取所有的节点,并且会通过心跳续约的方式定期更新。默认配置下,如果Eureka Server在一定时间内没有接收到某个服务实例的心跳,Eureka Server将会注销该实例(默认为90秒,通过eureka.instance.lease-expiration-duration-in-seconds配置)。当Eureka Server节点在短时间内丢失过多的心跳时(比如发生了网络分区故障),那么这个节点就会进入自我保护模式。 什么是自我保护模式?默认配置下,如果Eureka Server每分钟收到心跳续约的数量低于一个阈值(instance的数量(60/每个instance的心跳间隔秒数)自我保护系数),并且持续15分钟,就会触发自我保护。在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。当它收到的心跳数重新恢复到阈值以上时,该Eureka Server节点就会自动退出自我保护模式。它的设计哲学前面提到过,那就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。该模式可以通过eureka.server.enable-self-preservation = false来禁用,同时eureka.instance.lease-renewal-interval-in-seconds可以用来更改心跳间隔,eureka.server.renewal-percent-threshold可以用来修改自我保护系数(默认0.85)。 总结 ZooKeeper基于CP,不保证高可用,如果zookeeper正在选主,或者ZooKeeper集群中半数以上机器不可用,那么将无法获得数据。Eureka基于AP,能保证高可用,即使所有机器都挂了,也能拿到本地缓存的数据。作为注册中心,其实配置是不经常变动的,只有发版和机器出故障时会变。对于不经常变动的配置来说,CP是不合适的,而AP在遇到问题时可以用牺牲一致性来保证可用性,既返回旧数据,缓存数据。 所以理论上Eureka是更适合做注册中心。而现实环境中大部分项目可能会使用ZooKeeper,那是因为集群不够大,并且基本不会遇到用做注册中心的机器一半以上都挂了的情况。所以实际上也没什么大问题。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 ZooKeeper Eureka 注册中心

  • 分布式 Session 解决方案

    分布式Session一致性? 说白了就是服务器集群Session共享的问题 Session的作用? Session 是客户端与服务器通讯会话跟踪技术,服务器与客户端保持整个通讯的会话基本信息。 客户端在第一次访问服务端的时候,服务端会响应一个sessionId并且将它存入到本地cookie中,在之后的访问会将cookie中的sessionId放入到请求头中去访问服务器,如果通过这个sessionid没有找到对应的数据那么服务器会创建一个新的sessionid并且响应给客户端。 分布式Session存在的问题? 假设第一次访问服务A生成一个sessionid并且存入cookie中,第二次却访问服务B客户端会在cookie中读取sessionid加入到请求头中,如果在服务B通过sessionid没有找到对应的数据那么它创建一个新的并且将sessionid返回给客户端,这样并不能共享我们的Session无法达到我们想要的目的。 解决方案: 使用cookie来完成(很明显这种不安全的操作并不可靠) 使用Nginx中的ip绑定策略,同一个ip只能在指定的同一个机器访问(不支持负载均衡) 利用数据库同步session(效率不高) 使用tomcat内置的session同步(同步可能会产生延迟) 使用token代替session 我们使用spring-session以及集成好的解决方案,存放在redis中 目前项目中存在的问题 启动两个项目端口号分别为8080,8081。 依赖:

    架构师社区 分布式 Session

  • Redis最佳实践:7个维度+43条使用规范

    架构师社区 使用规范 Redis

  • 某快手程序员爆料:给小厂随便投投简历,大厂背书确实有用!

    “背书”有保证、担保的意思,“大厂背书”意即有大厂工作背景做担保,在大厂工作过的人,会给人一种技术好、能力强的感觉,人们往往觉得在大厂待过的人不会太差。 那么在找工作时,大厂背书究竟有没有用?一个快手程序员发帖讲了自己求职小厂的经历:觉得自己绩效差,准备跑路,投了一些小厂面试练手,随便改的简历,居然很快就受到五六个面试邀请,面试时表现很差,依然进了复试…… 楼主感觉内心有点慌,自己背景一般,在快手还不满一年,想去的大厂还没投,不知道结果如何。 楼主说,虽说快手比不上老牌互联网大厂的名声响亮,但这个背景到哪里也不会被嫌弃。如果腾讯阿里算是清华北大,字节美团算是交大复旦的话,快手也能算个很强的985学校吧。 有程序员表示大厂背书确实有用,楼主既然能进快手,能力应该也不错,加油干吧。 有人和楼主经历类似,本人所在中厂,面了五六家小公司,自己根本没做准备,但感觉面试都好简单,太简单反而自己有点慌了…… 有人说大厂背景的人可以去小厂积累一下面试经验,刷刷题。 不过也有人觉得有些不知名小厂的面试经验没什么用,都是些犄角旮旯的破问题,面试官技术也有限,如果要练手,最好是拿不想去的大厂练手,比如说阿里…… 也有许多网友觉得大厂背书没什么用,一个前蚂蚁员工说自己简历也是随便写的,被猎头推给小厂后也没音信了,可见不是每个大厂背书都有用。 有人觉得大厂背书没用是因为小厂钱给的不到位,活又脏又累,即使面试通过楼主也不会去,还是大厂才能有比较可观的涨幅。 有人说,小厂之所以让楼主通过面试,是想打听大厂消息和运作模式。 有人说,小厂给的钱少,还会招几个实习生让你带,等带得差不多,项目稳定运行了,老板就会恶心你,让你走人……看来这位发言的程序猿小哥亲身体验过某些不良小厂的做派,都是血淋淋的教训。 总之,大厂和小厂完全是两个赛道,就看楼主习惯哪个。 还有人和楼主方向相反,拿大厂去练手面试,面了几个大厂结果都不好,估计只能去小厂了,感觉职业生涯走到了尽头……这位同学不要气馁,小厂未必没有好的未来啊~ 说到这里,终于有人为小厂鸣不平了,原来大厂员工来小厂面试就是为了练手,小厂们招谁惹谁了?让hr也太不体面了吧? 在不了解一个人的前提下去评判这个人,大部分人会选择看他的履历背景。而大厂背景就像名校背景一样,都能给人增光添彩。从这个角度上说,大厂背书未尝不是一件好事,帮助那些想跳槽的大厂程序员得到更多更好的机会。 但大厂光环毕竟只是光环,进入小厂后光环能否继续闪耀,就要看自己的真本事了。只有能拿出和大厂光环相匹配的能力和技术,才能继续得到公司的器重,得到优厚的待遇,否则大厂光环能保一时,绝对保不了一世。 另外,很多程序员会选择大小厂互跳,但大厂和小厂的组织架构、薪资待遇、工作状态都不同,是两条不同的赛道,贸然进入未必能适应。想转换赛道的人一定要了解清楚是否适合自己再下决定,以免耽误自己宝贵的职场生涯。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 程序员 快手

  • 字节二面 | 26图揭秘线程安全

    想必都知道线程是什么,也知道怎么用了,但是使用线程的时候总是没有达到自己预期的效果,要么是值不对,要么是无限等待,为了尽全力的避免这些问题以及更快定位所出现的问题,下面我们看看线程安全的这一系列问题 前言 什么是线程安全 常见的线程安全问题 在哪些场景下需要特别注意线程安全 多线程也会带来性能问题 死锁的必要条件 必要条件的模拟 多线程会涉及哪些性能问题 什么是线程安全 来说说关于线程安全 what 这一问题,安全对立面即风险,可能存在风险的事儿我们就需要慎重了。之所以会产生安全问题,无外乎分为主观因素和客观因素。 先来看看大佬们是怎么定义线程安全的。《Java Concurrency In Practice》的作者Brian Goetz对线程安全是这样理解的,当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行问题,也不需要进行额外的同步,而调用这个对象的行为都可以获得正确的结果,那这个对象便是线程安全的。 他所表达的意思为:如果对象是线程安全的,那么对于开发人员而言,就不需要考虑方法之间的协调问题,说白了都不需要考虑不能同时写入或读写不能并行的问题,更别说使用各种锁来保证线程安全,所以对于线程的安全还是相当的苛刻。 那么平时的开发过程中,通常会遇到哪些线程安全问题呢 运行结果千奇八怪 最典型了莫过于多个线程操作一个变量导致的结果,这是显然的了 执行结果如下所示 此过程中,你会发现结果几乎每次都不一样,这是为啥呢? 这是因为在多线程的情况下,每个线程都有得到运行的机会,而 CPU 的调度是以时间片的方式进行分配,意味着每个线程都可以获取时间片,一旦线程的时间片用完,它将让出 CPU 资源给其他的线程,这样就可能出现线程安全问题。 看似 i++ 一行代码,实际上操作了很多步 读取数据 增加数据 保存 看上面这个图,线程 1 先拿到 i=1 的结果,随后进行 +1 操作,刚操作完还没有保存,此时线程 2 插足,CPU开始执行线程 2 ,和线程 1 的操作一样,执行 i++ 操作,那对于线程 2 而言,此时的 i 是多少呢?其实还是 1,因为线程 1 虽然操作了,但是没有保存结果,所以对于线程 2 而言,就没看到修改后的结果 此时又切换到线程 1 操作,完成接下来保存结果 2,随后再次切换到线程 2 完成 i=2 的保存操作。总上,按道理我们应该是得到结果 3,最后结果却为 2 了,这就是典型的线程安全问题了 活跃性问题 说活跃性问题可能比较陌生,那我说死锁你就知道了,因为确实太常见,面试官可能都把死锁嚼碎了吧,不问几个死锁都仿佛自己不是面试官了,随便抛出来几个问题看看 死锁是什么 死锁必要条件 如何避免死锁 写一个死锁案例 如果此时不知道如何回答,当大家看完下面的内容再回头应该就很清楚,不用死记硬背,理解性的记忆一定是会更长久啦。 死锁是什么 两个线程之间相互等待对方的资源,但又都不互让,如下代码所示 死锁有什么危害 首先我们需要知道,使用锁是不让其他线程干扰此次数据的操作,如果对于锁的操作不当,就可能导致死锁。 描述下死锁 说直白一点,占着茅坑不拉屎。死锁是一种状态,两个或多个线程相互持有相互的资源而不放手,导致大家都得不到需要的东西。小 A 和 小 B谈恋爱,毕业了,一个想去北京,一个想去广东,互不相让让,怎么办?可想而知,两者都想挨着家近一点的地方工作,又舍不得如此美好的爱情 再举一个生活上的例子 A:有本事你上来啊 B:有本事你下来啊 A:我不下,有本事你上来啊 B:我不上,你有本事下来啊 线程 A 和 线程 B的例子 上图两个线程,线程 A 和 线程 B,线程 A 想要获取线程 B 的锁,当然获取不到,因为线程 B 没有释放。同样的线程 B 想要获取线程 A 也不行,因为线程 A 也没有释放,这样一来,线程 A 和线程 B 就发生了死锁,因为它们都相互持有对方想要的资源,却又不释放自己手中的资源,形成相互等待,而且会一直等待下去。 多个线程导致的死锁场景 刚才的两个线程因为相互等待而死锁,多个线程则形成环导致死锁。 线程 1、2、3 分别持有 A B C。此时线程 1 想要获取锁 B,当然不行,因为此时的锁 B 在线程 2 手上,线程 2 想要去获取锁 C,一样的获取不到,因为此时的锁 C 在线程 3 手上,然后线程 3 去尝试获取锁 A ,当然它也获取不到,因为锁 A 现在在线程 1 的手里,这样线程 A B C 就形成了环,所以多个线程仍然是可能发生死锁的 死锁会造成什么后果 死锁可能发生在很多不同的场景,下面举例说几个 JVM 在 JVM 中发生死锁的情况,JVM 不会自动的处理,所以一旦死锁发生就会陷入无穷的等待中 数据库 数据库中可能在事务之间发生死锁。假设此时事务 A 需要多把锁,并一直持有这些锁直到事物完成。事物 A 持有的锁在其他的事务中也可能需要,因此这两个事务中就有可能存在死锁的情况 这样的话,两个事务将永远等待下去,但是对于数据库而言,这样的事儿不能发生。通常会选择放弃某一个事务,放弃的事务释放锁,从而其他的事务就可以顺利进行。 虽然有死锁发生的可能性,但并不是 100% 就会发生。假设所有的锁持有时间非常短,那么发生的概率自然就低,但是在高并发的情况下,这种小的累积就会被放大。 所以想要提前避免死锁还是比较麻烦的,你可能会说上线之前经过压力测试,但仍不能完全模拟真实的场景。这样根据发生死锁的职责不同,所造成的问题就不一样。死锁常常发生于高并发,高负载的情况,一旦直接影响到用户,你懂的! 写一个死锁的例子 上图的注释比较详细了,但是在这里还是梳理一下。 可以看到,在这段代码中有一个 int 类型的 level,它是一个标记位,然后我们新建了 o1 和 o2、作为 synchronized 的锁对象。 首先定义一个 level,类似于 flag,如果 level 此时为 1,那么会先获取 o1 这把锁,然后休眠 1000ms 再去获取 o2 这把锁并打印出 「线程1获得两把锁」 同样的,如果 level 为 2,那么会先获取 o2 这把锁,然后休眠 1000ms 再去获取 o1 这把锁并打印出「线程1获得两把锁」 然后我们看看 Main 方法,建立两个实例,随后启动两个线程分别去执行这两个 Runnable 对象并启动。 程序的一种执行结果: 从结果我们可以发现,程序并没有停止且一直没有输出线程 1 获得了两把锁或“线程 2 获得了两把锁”这样的语句,此时这里就发生了死锁。 然后我们对死锁的情况进行分析 下面我们对上面发生死锁的过程进行分析: 第一个线程起来的时候,由于此时的 level 值为1,所以会尝试获得 O1 这把锁,随后休眠 1000 毫秒 线程 1 启动一会儿后进入休眠状态,此时线程 2 启动。由于线程 2 的 level 值为2,所以会进入 level=2 的代码块,即线程 2 会获取 O2 这把锁,随后进入1000 毫秒的休眠状态。 线程 1 睡醒(休眠)后,还想去尝试获取 O2 这把锁,由于此时的 02 被线程2使用着,自然线程 1 就无法获取 O2。 同样的,线程 2 睡醒了后,想去尝试获取 O1 这把锁,O1 被线程 1 使用着,线程 2 自然获取不到 O1 这把锁。 好了,我们总结下上面的情况。应该是很清晰了,线程 1 拿着 O1的锁想去获取 O2 的锁,线程 2 呢,拿着 O2 的锁想去获取 O1 的锁,这样一来线程 1 和线程 2 就形成了相互等待的局面,从而形成死锁。想必大家这次就很清晰的能理解死锁的基本概念了,这样以来,要死锁,它的必要条件是什么呢?ok,我们继续往下看。 发生死锁的必要条件 互斥条件 如果不是互斥条件,那么每个人都可以拿到想要的资源就不用等待,即不可能发生死锁。 请求与保持条件 当一个线程请求资源阻塞了,如果不保持,而是释放了,就不会发生死锁了。所以,指当一个线程因请求资源而阻塞时,则需对已获得的资源保持不放 不剥夺条件 如果可剥夺,假设线程 A 需要线程 B 的资源,啪的一下抢过来,那怎么会死锁。所以,要想发生死锁,必须满足不剥夺条件,也就是说当现在的线程获得了某一个资源后,别人就不能来剥夺这个资源,这才有可能形成死锁 循环等待条件 只有若干线程之间形成一种头尾相接的循环等待资源关系时,才有可能形成死锁,比如在两个线程之间,这种“循环等待”就意味着它们互相持有对方所需的资源、互相等待;而在三个或更多线程中,则需要形成环路,例如依次请求下一个线程已持有的资源等 案例解析四大必要条件 上面和大家一起走过这个代码,相信大家都很清晰了,我也将必要的注释放在了代码中,需要的童鞋可以再过一遍。现在我们主要通过这份代码,来分析分析死锁的这四个必要条件 第一个必要条件为互斥条件 在代码中,很明显,我们使用 了 synchronized 互斥锁,它的锁对象 O1、O2 只能同时被一个线程所获得,所以是满足互斥的条件 第二个必要条件为请求与保持条件 不仅要请求还要保持。从代码我们可以发现,线程 1 获得 O1 这把锁后不罢休,还要尝试获取 O2 这把锁,此时就被阻塞了,阻塞就算了,它也不会释放 O1 这把锁,意味着对已有的资源保持不放。所以第二个条件也满足了。 第 3 个必要条件是不剥夺条件,在我们这个代码程序中,JVM 并不会主动把某一个线程所持有的锁剥夺,所以也满足不剥夺条件。 第 4 个必要条件是循环等待条件,在我们的例子中,这两个线程都想获取对方已持有的资源,也就是说线程 1 持有 o1 去等待 o2,而线程 2 则是持有 o2 去等待 o1,这是一个环路,此时就形成了一个循环等待。 这样通过代码的形式,更加深刻的了解死锁问题。所以,在以后再遇到死锁的问题,只要破坏任意一个条件就可以消除死锁,这也是我们后面要讲的解决死锁策略中重点要考虑的内容,从这样几个维度去回答是不是更清晰勒。那如果发生了死锁该怎么处理呢? 发生死锁了怎么处理 既然死锁已经发生了,那么现在要做的当然是止损,最好的办法为保存当前 JVM ,日志等数据,然后重启。 为什么要重启? 我们知道发生死锁是有很多前提的,而且通常情况下是在高并发的情况才会发生死锁,所以重启后发生的几率很小且可以暂时保证当前服务的可用,随后根据保存的信息排查死锁原因,修改代码,随后发布 有哪些修复的策略呢 常见的修复策略有三个,避免策略,检测与恢复策略以及鸵鸟策略。下面分别说说这三种策略 避免策略 发生死锁的原因无外乎是采用了相反的顺序去获取锁,那么就要思考如何将方向掉过来。 下面以转账的例子来看看死锁的形成与避免。 在转账之前,为了保证线程安全通常会获取两把锁,分别为转出的账户与转入的账户。说白了,在没有获取这两把锁的时候,是不能对余额做操作的,即只有获取了这两把锁才会进行接下来的转账操作。看看下面的代码 执行结果如下 在这里插入图片描述 通过之前的代码分析,再看这个代码是不是会简单很多。代码中,定义 int 类型的 flag,不同的 flag 对应不同的执行逻辑,随后建立了两个账户 对象 a 和 对象 b,两者账户最初都为 1000 元。 再来看 run 方法,如果此时 flag 值为 1 ,那么代表着 a 账户会向 b账户转账 100 元,如果 flag 为 0 则表示 b 账户往 a 账户转账 100 元。 再来看 transferMoney 方法,会尝试获取两把锁 O1 和 O2,如果获取成功则判断当前余额是否足以转出,如果不足则会 return。如果余额足够则会转出账户并减余额,对应的给被转入的账户加余额,最后打印成功转账"XX"元 在代码中,首先定义了 int 类型的 flag,它是一个标记位,用于控制不同线程执行不同逻辑。然后建了两个 Account 对象 a 和 b,代表账户,它们最初都有 1000 元的余额。 再看主函数,分别创建两个对象,并设置 flag 值,传入两个线程并启动,结果如下 呀哈,结果完全正确,符合正常逻辑。那是因为此时对锁的持有时间比较短,释放也快,所以在低并发的情况下不容易发生死锁,下面我们将代码做下调整。 我在两个 synchonized 之间加上一个休眠 Thread.sleep(1000),就反复模拟银行转账的网络延迟现象。所以此时的 transferMoney 方法变为这样 可以看到 的变化就在于,在两个 synchronized 之间,也就是获取到第一把锁后、获取到第二把锁前,我们加了睡眠 1000 毫秒的语句。此时再运行程序,会有很大的概率发生死锁,从而导致控制台中不打印任何语句,而且程序也不会停止。 为什么加了一句睡眠时间就可能出现死锁呢。原因就在于有了这个休息时间,让其他的线程有了得逞的机会,想一想什么时候是追下女票最快的方式,哈哈哈哈。 这样,两个线程获取锁的方式是相反的,意味着第一个线程的“转出账户”正是第二个线程的“转入账户”,所以我们就可以从这个“相反顺序”的角度出发,来解决死锁问题。, 既然是相反顺序,那我们就想办法控制线程间的执行顺序,这里可以使用 HashCode 的方式,来保证线程安全 修复之后的 transferMoney 方法如下: 上面代码,首先计算出 两个 Account 的 HashCode,随后根据 HashCode 的大小来决定获取锁的顺序。所以,不管哪个线程先执行,也无论是转出和转入,获取锁的顺序都会严格按照 HashCode大小来决定,也就不会出现获取锁顺序相反的情况,也就避免了死锁。 除了使用 HashCode 的方式决定锁获取顺序以外 ,不过我们知道还是会存在 HashCode 冲突的情况。所以在实际生产中,排序会使用一个实体类,这个实体类有一个主键 ID,既然是主键,则有唯一,不重复的特点,所以也就没必要再去计算 HashCode,这样也更加方便,直接使用它主键 ID 进行排序,由主键 ID 大小来决定获取锁的顺序,从而确保避免死锁。 其实,使用 HashCode 方式有个问题,如果出现 Hash 冲突还有有点麻烦,虽然概率比较低。在实际生产上,通常会排序一个实体类,这个实体类有一个主键 ID,既然是主键 ID,也就有唯一,不重复的特点,所以所以如果我们这个类包含主键属性的话就方便多了,我们也没必要去计算 HashCode,直接使用它的主键 ID 来进行排序,由主键 ID 大小来决定获取锁的顺序,就可以确保避免死锁。 以上我们介绍了死锁的避免策略。 检测与恢复策略 检测与恢复策略,从名字可以猜出,大体意思为可以先让死锁发生,只不过会每次调用锁的时候,记录下调用信息并形成锁的调用链路图,然后每隔一段时间就用死锁检测算法检测下,看看这个图中是否存在环路,如果存在即发生了死锁,就可以使用死锁恢复机制,比如剥夺某个资源来解开死锁并进行恢复。 那到底如何解除死锁呢? 线程终止 第一种解开死锁的方式比较直接,直接让线程或进程终止,这样的话,系统会终止已经陷入死锁的线程,线程终止,释放资源,这样死锁就会解开 当然终止也是要讲究顺序的,不是随便随时终止 第一个考量优先级: 当进程线程终止的时候,会终止优先级比较低的线程。如果是前台线程,那么直接影响到界面的显示,这对用户而言是无法接受的,所以通常来说前台线程优先级会高于后台线程。 第二个考量已占有资源,还需要资源: 如果一个线程占有的很多资源,只差百分之一的资源就可以完成任务,那么这个时候系统可能就不会终止这样的线程额,而是会选择终止其他的线程来有限促进该线程的完成 第三个考量已经运行的时间: 如果一个线程运行很长的时间了,很快就要完成任务,那么突然终止这样的一个线程也不是一个明智的选择,我们可以让那些刚刚开始运行的线程终止,并在之后把它们重新启动起来,这样成本更低。 这里会有各种各样的算法和策略,我们根据实际业务去进行调整就可以了。 方法2——资源抢占 其实第一种方式太暴力了,我们只需要把它已经获得的资源进行剥夺,比如让线程回退几步、 释放资源,这样一来就不用终止掉整个线程了,这样造成的后果会比刚才终止整个线程的后果更小一些,成本更低。 不过这样还是有个缺点,如果我们抢占的线程一直是同一个线程,那么线程也扛不住会出现线程饥饿的情况,这个线程一直被剥夺已经得到的资源,那它将长期得不到运行。 鸵鸟策略 还是从名字出发,鸵鸟嘛,有啥特点?就是当遇到危险的时候就会将头埋到沙子里,这样就看不到危险了。 在低并发的情况下,比如很多内部系统,发生死锁的概率很低,如果即使发生了也不会特别严重,那还花这么多心思去处理它,完全没有必要。 哪些场景需要额外注意线程安全问题? 访问共享变量或资源 上面最开始说的 i++ 就是这样的情况,访问共享变量和共享资源,共享缓存等。这些信息被多个线程操作就可以出现并发读写的情况。 依赖时序的操作 如果在开发的过程中,相应的需求或场景依赖于时序的关系,那么在多线程又不能保障执行顺序和预期一致,这个时候依然要考虑线程安全的问题。如下简单代码 这样先检查再执行的操作不是原子性操作,中间任意一个环节都有可能被打断,检查后的结果可能出现无效,过期的情况,所以,想要获取正确的结果可能取决于时序,所以这种情况需要通过枷锁等方式保护保障操作的原子性。 对方没有声明自己是线程安全的 因为有很多内置的库函数,比如集合中的 ArrayList,本身就不是线程安全的,如果多个线程同时对 ArrayList 进行并发的读写,那就自然有可能出现线程安全问题,从而造成数据出错,这个责任不在于 ArrayList,而是因为它本身就不是并发安全的。我们也可以看看源码中的注释 描述的也很清晰,如果我们要使用 ArrayList 在多线程的场景,请在外部使用 synchronized 等保证并发安全。 多线程会有哪些性能问题 我们经常听到的是通过多线程来提升效率,多个线程同时工作,加快程序运行速度,而这里想说的是多线程会带来哪些问题。单线程是个单身汉儿,啥时候自己干,也不和别人牵扯,可多线程不一样,需要和别人协同办公,既然要协同办公,那就涉及到沟通的成本,这样的调度和合作就会带来性能开销。 哪些可能会有性能开销? 性能开销多种多样,其表现形式也多样。常见的响应慢,内存占用过多都属于性能问题。我们通过购买服务器来横向提升服务器的处理能力,通过购买更大的带宽提升网络处理能力,总是用户是上帝,我们需要想尽一切办法让用户有更好的体验,不卸载,勤分享。 多线程带来哪些开销 第一个就是上面说的信息交互涉及的上下文切换,通常我们会指定一定数量的线程,但是 CPU 的核心又比线程数少,所以无法同时照顾到所有的线程,自然就有一部分线程在某个时间点处于等待的状态 操作系统就会按照一定的调度算法,给每个线程分配时间片,让每个线程都有机会得到运行。而在进行调度时就会引起上下文切换,上下文切换会挂起当前正在执行的线程并保存当前的状态,然后寻找下一处即将恢复执行的代码,唤醒下一个线程,以此类推,反复执行。但上下文切换带来的开销是比较大的,假设我们的任务内容非常短,比如只进行简单的计算,那么就有可能发生我们上下文切换带来的性能开销比执行线程本身内容带来的开销还要大的情况。 第二个带来的问题是缓存失效。对了,如果我们把经常使用的比如数据线等物品放在固定的地方,下次需要的时候就不会惊慌失措,浪费时间了。同样的,我们把经常访问的数据缓存起来,下次需要的时候直接取就好了。常见的数据库的连接池,线程池等都有类似的思想。 如果没有缓存,一旦进行了线程调度,切换到其他的线程,CPU 就会去执行其他代码,这时候就可能出现缓存失效了,一旦失效,就要重新缓存新的数据,从而引起开销。所以线程调度器为了避免频繁地发生上下文切换,通常会给被调度到的线程设置最小的执行时间,也就是只有执行完这段时间之后,才可能进行下一次的调度,由此减少上下文切换的次数。 更可怕的是,如果多线程频繁的竞争锁或者 IO 读写,就有可能出现大量的阻塞,此时就可能需要更多的上下文切换,即更大的开销 线程协作带来的开销 很多时候多个线程需要共享数据,为了保证线程安全,就会禁止编译器和 CPU 对其进行重排序等优化,正是因为要同步,需要反复把线程工作内存的数据 flush 到主存中,随后将主存的内容 refresh 到其他线程的工作内存中,等等。 这些问题在单线程中并不存在,但在多线程中为了确保数据的正确性,就不得不采取上述方法,因为线程安全的优先级要比性能优先级更高,这也间接降低了我们的性能。 总结 在本篇文章中,我们首先介绍了什么是死锁,接着介绍了死锁在生活中、两个线程中以及多个线程中的例子。然后我们分析了死锁的影响,在 JVM 中如果发生死锁,可能会导致程序部分甚至全部无法继续向下执行的情况,所以死锁在 JVM 中所带来的危害和影响是比较大的,我们需要尽量避免。最后举了一个必然会发生死锁的例子代码,并且对此代码进行了详细的分析。 另外也学习了解决死锁的策略。从线程发生死锁,到保存重要数据,恢复线上服务。最后也提到了三种修复的策略。 一是避免策略,其主要思路就是去改变锁的获取顺序,防止相反顺序获取锁这种情况的发生;二是检测与恢复策略,它是允许死锁发生,但是一旦发生之后它有解决方案;三是鸵鸟策略。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 字节 线程 线程安全

  • Hive MetaStore 在快手遇到的挑战与优化

    导读:快手基于Hive构建数据仓库,并把Hive的元数据信息存储在MySql中,随着业务发展和数据增长,一方面对于计算引擎提出了更高的要求,同时也给Hive元数据库的服务稳定性带来了巨大的挑战。本文将主要介绍Hive MetaStore服务在快手的挑战与优化,包括: Hive MetaStore在快手的挑战 快手SQL on Hadoop的技术规划 快手SQL on Hadoop智能引擎架构Hive把用户SQL通过解释器转换为一系列MR作业提交到hadoop环境中运行,MR存在作业启动、调度开销大、落盘多磁盘IO重的问题,这导致其性能注定无法太好,针对Hive查询速度慢的问题,业界先后推出了包括presto/impala/spark等查询引擎,在实现和适用场景上各有优缺点。 高性能:业务要求更高的查询性能,需要引入更高效的计算引擎 扩展性:技术是发展非常快的,未来随着技术发展可能还会有其他更高效的引擎不断出现,我们在架构设计上需要能够考虑到很好地扩展性支持这些新的计算引擎,需要做到计算引擎的可插拔、易扩展 基于上述考虑,我们最终基于HiveServer本身的Hook架构,实现一个BeaconServer。所有的查询仍然以HiveServer作为统一入口,从而解决易用性和低成本的问题。 BeaconServer本身是一个无状态服务,我们可以很方便进行水平扩容,并且BeaconServer服务调整升级不影响HiveServer服务本身。 这里特别提一下,除了引入presto、spark等高效计算引擎并对齐进行优化之外,我们针对Hive本身的FetchTask机制(本地读取hdfs文件返回结果,不存在作业提交开销)也进行了系列改进,使其适用场景更广,查询效率更高,在日常查询中也占了很大比重。 Hive MetaStore在快手的挑战 首先,我们基于整体服务架构简单说明一下Hive MetaStore服务的作用以及其重要性:Hive Metastore是hive用来管理元数据的服务,包含database、table、partition等元信息,presto/spark也都以Hive Metastore作为统一的元数据中心。 接下来介绍一下Hive MetaStore服务当前所面临的挑战。 访问量非常大,目前每天查询量是50万+,此外数据地图、数据依赖服务也会直接调用Hive MetaStore服务API Hive表总分区达到1.5亿,由于多级分区的存在,元数据库中存储分区信息的单表记录已超过10亿 03 针对上述问题,我们考虑从几个方面进行优化: 其次,在HIVE的元数据查询上,存在大量的多表联合查询,尤其存储分区信息的两个大表(PARTITONS和PARTITION_KEY_VALS)之间的联合查询,会对服务带来很大压力,可能导致查询超时以及慢查询等问题,因此第二个优化方向是优化元数据API调用; 接下来我们从四个优化方向分别进行介绍: 首先,我们介绍一下MetaStore读写分离架构设计。 在查询场景中,既有读请求也有写请求,没有办法直接从服务层面进行拆分。由于大数据场景下普遍写少读多,大量读请求直接发送到主库会导致QPS峰值高,服务抖动引发慢查询,进而影响服务稳定性。对此我们的优化方案是在查询层面实现HiveMetaStore API粒度的读写分离,通过把对主库的读请求尽量路由到从库,从而有效降低主库的QPS压力。这个方案要解决的一个主要问题是如何保证数据一致性,避免由于主从同步延迟,导致读请求在从库中漏读数据或者读取到错误的过期数据。 具体流程为:在HiveServer或者Spark提交SQL创建会话链接时,会首先从主库获取并保存当前最新的GTID,在同一个会话中,每次写请求操作完成后,都会更新当前会话所持有的GTID;对于读请求,会首先获取从库当前的GTID,通过比较GTID来判断从库是否已经完成了数据同步,只有当从库GTID大于等于当前会话持有的GTID时,这次读操作才会被真正路由到从库。 2. MetaStore API优化 根据我们分析定位,MetaStore API调用当前主要面临的问题包括: 第二,在Hive MetaStore层,单次API调用访问的数据量过大,容易导致服务瞬时压力大; 第二个单次访问数据量大,造成服务瞬时压力高,改成分批方式返回,就能起到削峰作用;第三个分析对两张大表的查询性能瓶颈,针对具体问题采用合适的优化方案。 HIVE的DDL DESC TABLE命令,社区默认行为除了返回表相关元信息外,还会遍历获取这个表所有的分区信息,对于一个包含大量分区的表来说,这个操作会非常耗时同时也是不必要的。对此我们做的优化是默认跳过这个遍历获取分区元信息的操作。通过测试对比,对于一个包含十万分区的表执行DESC命令,优化前需要两百多秒,优化之后只需要0.2秒。 其次针对单次访问数据量大,造成服务瞬时压力高的问题,我们可以考虑改成分批方式完成大数据量的扫描,从而起到削峰作用。 然后我们再分析一下存储分区信息的两个大表查询时延过高的问题,看看性能瓶颈究竟在什么地方以及如何进行针对性优化。 这个查询表达式使用PARTITION_KEY_VAL表中的PART_KEY_VAL字段来进行匹配过滤,存在的问题是:PARTITION_KEY_VAL表中没有TBL_ID字段,导致会扫描到无关表的同名分区;PARTITION_KEY_VAL表中没有索引列,无法通过索引加速。 通过分析expresssionTree,解析时间范围子树,获取最长子串前缀:‘20200101’,从而得到优化后的查询表达式为:where ((( “FILTER0”.“PART_KEY_VAL”= ?) and (“FILTER2”.“PART_KEY_VAL”= ?))) and(“PARTITIONS”.“PART_NAME”like ? )。 接下来我们再看另一种可优化的场景,对于select * from table where p_date=20200101 and p_hourmin=1000这样一条Hive查询,由于其分区字段类型是string类型,但是Hive查询中给的是整型值,导致无法通过分区名进行过滤,会命中该表的全部子分区。 优化前耗时:32288ms,优化后耗时:586ms。 我们接着聊一下MetaStore流量控制架构设计: 整体流量控制架构设计如上图所示,核心点在于引入BeaconServer服务作为中控层。Beacon Server作为中控层,支持动态更新设置流控策略,以及实时获取当前元数据服务压力状况。Hive MetaStore作为客户端会周期性去中控层获取当前最新的元数据服务压力状况和流控策略,并针对不同优先级的API调用请求采取对应的流控措施。 流量控制架构在上线后,有效缓解了生命周期管理服务定期清理大批量分区时对于元数据服务造成TPS过高的压力。 最后介绍一下MetaStore Federation架构设计,长远看,随着业务量持续增加,MySQL单机依然会存在性能及存储瓶颈的风险。解决MySQL单机瓶颈和压力,业界通用方案是分库分表,由于Hive元数据信息存储采用三范式设计,表关联较多,直接在MySQL层面进行拆库拆表会存在改造成本大、风险高且不利于未来扩容的问题,因此我们考虑采取HiveMetaStore层面的Federation方案,实现元数据水平扩展能力。 基于上述原理,我们首先想到的方案1是基于ObjectStore已有功能和代码实现KwaiStore,在KwaiStore中实现Hive DB路由数据源的功能,配置不同Hive DB到对应MySQL数据源的映射关系。 方案2是通过引入路由层,使用代理转发请求的方式来实现。这个方案下Metastore代码不用做任何改动,新增Router层,根据请求数据的Hive DBName来决定路由到哪个Metastore上去。Router层可以水平扩容,可以在Router层做很多扩展功能,白名单、多数据源支持(统一元数据)、Hive DB 禁用、元数据权限等操作。在扩容时,可以在Router层添加规则,指定某个Hive DB 暂时不可访问,待数据源完全准备好后,再添加路由规则到Router层,同时取消DB不可用限制;可以做到只影响部分Hive DB的使用。 综合考虑方案1和方案2的优缺点,我们最终选择了在HMHandler层来实现路由功能,在HMSHandler中维护一组HiveDB与RawStore的映射关系,在getMS()时传入 Hive DB,路由根据db判断应该使用那个RawStore,不修改RawStore中API的实现,不涉及到持久化层的侵入改造。 ▌结束语

    架构师社区 快手 Hive MetaStore

发布文章